云互联如何分享:谐云课堂云原生业务上云迁移的最佳实践
云互联如何分享:谐云课堂云原生业务上云迁移的最佳实践租户:从公司的组织架构上可以认为是一个部门或者大的项目集。租户、项目、命名空间资源规划主机资源规划,即资源池规划,主要指应用所在的主机资源规划。在K8S集群中,Namespace是逻辑上的概念,如上图所示,集群中任意一台主机都能运行各个Namespace的Pod,存在主机资源抢占的情况。因此需要对主机资源池进行精细化管理。如上图所示,可以根据业务性能、是否为核心业务等特性对集群中的主机资源进行资源池划分,限制其运行主机,防止资源抢占:
本期课堂由谐云研发工程师顾荣云作为讲师,带来《云原生业务上云迁移经验分享》,围绕容器资源规划、应用上云、应用管理、应用上云问题等进行详细解读。
01
容器资源规划
主机资源规划
主机资源规划,即资源池规划,主要指应用所在的主机资源规划。
在K8S集群中,Namespace是逻辑上的概念,如上图所示,集群中任意一台主机都能运行各个Namespace的Pod,存在主机资源抢占的情况。因此需要对主机资源池进行精细化管理。
如上图所示,可以根据业务性能、是否为核心业务等特性对集群中的主机资源进行资源池划分,限制其运行主机,防止资源抢占:
- 高性能应用,例如中间件,依赖本地磁盘,而通用业务一般不依赖本地磁盘,就可以考虑为中间件单独规划对应的资源池;
- 租户项目规模多,可以根据业务的特性(核心、非核心)规划资源池。
- 租户项目规模少,且没有核心业务,可以考虑共享的方式不再单独规划资源池,即为默认资源池;
租户、项目、命名空间资源规划
租户:从公司的组织架构上可以认为是一个部门或者大的项目集。
项目:项目一般会对应到一个具体的项目组。
命名空间:可运行服务的一个命名空间,为K8S中的Namespaces。
事先需要规划好租户、项目、命名空间,避免重复性工作。在容器云平台中,命名空间资源是有限制的,即限制CPU、内存、存储等资源使用,在规划命名空间时,需要预留部分资源,服务升级、弹性扩容等行为可能会由于资源不足导致对应行为失败的情况。分以下几种情况划分命名空间:
情况一:当租户项目规模少时,可以按照项目划分命名空间,每个项目独享自己的命名空间,不同命名空间的资源是隔离的。
情况二:租户项目规模多,如果按照项目来规划命名空间,每个命名空间都需要预留额外的资源(用于升级、扩容等),租户的资源需要冗余很多资源,有碍提升资源利用率,在这种情况下,就可以按照租户划分命名空间,租户下的项目共享一个命名空间,如果个别项目需要资源隔离可以单独规划一个命名空间。
多租户管理
资源管理思路:
- 平台管理员:容器平台运营负责人,管理整体计算资源池以及平台各个租户权限管理;
- 租户负责人:对应各部门、地市、省份等的管理员,划分租户内的各项资源和管理租户成员、项目管理员权限;
- 项目管理员:对应到租户下面有具体资源需求的项目组的负责人,管理项目组成员权限;
- 项目成员:对应一个运营团队的成员,负责服务各项管理工作的研发、测试、运维等人员;
- 工作空间:即命名空间(Namespace):对应一个应用/模块的运行空间,可运行多个服务,与其他工作空间之间存在网络隔离。
资源分配流程:
- 平台管理员创建部门的租户负责人,并配置关联其拥有的集群资源配额;
- 租户负责人将自己的资源进行划分,分配给具体的部门/项目组使用;
- 项目管理员将相关项目组/部门成员添加进项目组,按开发/测试/运维等角色赋予操作权限;
- 项目成员按需创建工作空间,发布相应的服务;
网络资源规划
网络资源,主要指K8S中的Pod IP规划(MacVLAN、Calico BGP):
- 需要开通不同网络域网络策略,建议采用固定IP池的方式部署,IP的个数建议为应用实例数的1.5倍(向上取整);
- 当IP资源紧张时,适当集群采用多种网络方案(CalicoIPIP)。
当一个集群存在多个网络域,并且网络域之间是隔离的。
网络域管理思路:
- 网络域:子网的集合,一个网络域属于一个集群,一个集群下有多个网络域,网络域之间相互隔离。
- 子网:等同于在交换机上配置的VLAN,一个网络域可以有多个子网,一个子网只能属于一个网络域。
资源分配流程:
- 平台管理员维护好网络域内的未分配IP数即可,不需要关注不同租户对于IP地址数的需求差异,以及实际业务的用量情况,在网络域IP余量不足时,通过添加子网的方式扩容,并分配各个租户网络域资源。
- 租户/项目人员选择相关网络域发布对应应用。
负载均衡规划
负载均衡节点规划,主要指的是部署负载均衡Ingress-Controller节点规划。
- 项目规模大,建议事先规划好负载均衡节点;
- 按照业务重要性规划负载均衡,不同类型的项目可以使用不同的IngressController的节点,比如重要项目和非重要项目可以使用不同的主机。
负载均衡数量规划,主要指的是负载均衡节点上部署负载均衡数量,需注意同一台主机上同网络的负载均衡端口冲突问题:
- 统一规划HTTP/HTTP端口范围,以及nginx的配置模版;
- 按照业务类别规划不同的HTTP/HTTPS端口;
- 业务请求量大的单独建议规划一个端口。
负载均衡TCP/UDP端口规划,主要指的是IngressController的TCP/UDP端口规划:
平台统一规划好TCP端口范围;HTTP服务建议使用http对外服务方式,慎用TCP端口;按照业务规划对应的TCP端口池。
案例一:多网络域且项目规模小
在K8S集群中,单独规划一组负载均衡节点(两台及两台以上)分别部署各个网络域的负载均衡(80/443端口),各个网络域的负载均衡分别代理自己网络域的应用。
案例二:多网络域且项目规模大
在大规模K8S集群中,为大流量或核心的业务分别在各网络域规划一组负载均衡节点,承担其业务;其他通用的业务另规划一组负载均衡节点。
镜像资源规划
基础公共镜像:
- 平台建议提供统一系统基础镜像;
- 基础镜像大小尽量小, 轻量级的应用便于大规模集群中快速传输分发,更符合容器敏捷的理念;
- 基础镜像版本规划,便于基础镜像更新和升级;
- 基础镜像提供统一的端口规划,便于监控,如tomcat镜像建议使用统一的web端口(8080);
- 基础配置参数外部化,配置参数外部化于配置文件或者环境变量,便于应用容器适配不同的运行环境;
- 基础镜像通过安全扫描;
- 集成常用的工具命令以及内网源;
业务镜像:
- 每个项目每个集群建议有自己的私有镜像仓库;
- 私有镜像仓库清理规则管理:
- 平台可以提供统一的清理规则,比如保留每个镜像的最近10个版本;
- 项目又可以自定义自己的清理规则;
- 项目的基础镜像与应用的镜像名称建议分开,基础镜像更新频率低,容易被误清理。
- 镜像仓库备份,建议开启镜像备份功能
存储资源规划
远程共享存储,尽量使用非高IO和非高实时性场景,应用日志不建议放到共享存储,建议使用EmptyDir方式挂载,使用其他日志收集组件收集。
适当复用本地存储,存储IO高的应用不建议共享存储,可以引用本地存储,如CI流水线依赖,公共依赖(如公共java的本地仓库目录,npm缓存目录)不建议使用共享存储,可以使用本地存储,共享存储依赖网络,影响构建时间。
02
应用上云
应用上云图谱
不推荐容器化
- 应用单个实例资源需求巨大的应用
- 需要固定硬件,或者是必须依赖特定硬件等;
- 对内核有依赖或者对内核有操作的应用;
- 非UTF-8编码的应用。
可以容器化
- 对IO等运行环境要求比较高的应用;
- 高性能计算的应用;
- Windows应用
- 商用化应用;
- 多进程应用;
推荐容器化
- 功能单一的单进程应用;
- 无状态的应用;
- 变更频繁的应用;
- 有重复部署需求的应用;
- 具备横向扩展能力的应用。
镜像打包最佳实践
业务等镜像打包最佳实践:
- 使用安全的基础镜像,并进行安全扫描;
- 尽可能使用统一基础镜像,增加镜像分层的复用性;
- 减少镜像的层数,尽量把一些功能上面统一的命令合到一起来做;
- 尽量使用yum,apt-get等在线安装方式安装软件包,并注意清理其中间产物,比如一些安装包在装完之后就把它删掉;
- 尽量使用单独目录进行镜像否决,减少空间损耗;
- 多阶段进行镜像构建,确保镜像最干净/最小;
- 使用CMD exec运行方式;
- 尽量去构建缓存,尽量把一些不变的东西或者变动比较少的东西放在前面,因为不变的东西是可以被缓存的,如果不想使用缓存,则可以在docker build时添加--no-cache=true选项;
- 使用COPY而非ADD;
- 使用Supervisor 、S6等init进程管理多个进程;
- 统一规划应用安装路径和启动脚本,如统一为/root/app;
- 应用配置参数外部化,使用环境变量、配置文件方式注入。
容器迁移步骤最佳实践
应用容器化步骤最佳实践
03
应用管理
应用容器化原则
应用容器化需要满足以下的原则:
- 不要在容器中存储数据
· 由于容器是一次性的,当容器被停止、销毁或替换时,应用在容器中存储的数据同样会被销毁。如果应用需要存储数据,需存储在共享数据存储中。
- 不要发布两份应用
· 一些人将容器视为虚拟机。他们中的大多数倾向于认为他们应该在现有的运行容器里发布自己的应用。在开发阶段这样是对的,此时你需要不断地部署与调试;但对于质量保证与生产中的一个连续部署的管道,你的应用本该成为镜像的一部分。记住:容器应该保持不变。
- 清除不必要的包和文件
· 容器的一个显著特点是秒级启动,一旦制作了一个超大的镜像将难以分发。在镜像中应该确保仅有运行应用/进程的必需的文件和库存在,其他不必要的包、文件等建议应用打包前进行清除。
- 不要在容器中运行多个进程
· 容器能完美地运行单个进程(http守护进程,应用服务器,数据库),但是如果不止有一个进程,管理、获取日志、独立更新都会遇到麻烦。
- 尽可能设计成无状态
· 无状态服务能够非常容易的做水平扩展,可以做到快速扩容。
- 服务配置与容器镜像内容分离
· 不要在应用中写死服务的IP和端口(如数据库IP和端口、服务接口IP和端口),采用域名的方式。环境变量应该以传参的形式传入容器,而不是直接写死在容器中,需要的环境变量可写入系统集成文档中。
- 使用非root用户运行
· docker容器默认以root运行。随着docker的成熟,越来越多的安全默认选项变得可用。请求root是危险的,可能无法在所有环境中可用。所以镜像应该使用USER命令来指令容器以一个非root用户来运行。
- 不要依赖IP地址
· 每个容器都有自己的内部IP地址,如果你启动并停止它地址可能会变化。如果应用或微服务需要与其他容器通讯,由于容器IP是不固定的,应用依赖IP地址将无法与其他容器通讯,可以使用K8S内部dns代替ip地址配置形式与其他容器进行通讯。
- 控制输出到stdout和stderr的日志写入量日志
- 采用集中化处理方案
- 采用独立的容器处理定时任务
- 不要在镜像中存储凭据
· 不要将镜像中的任何用户名/密码写死。使用环境变量来从容器外部获取此信息。
应用镜像tag使用规范
部署容器镜像时,避免使用latest tag:
- 难以追踪当前部署的镜像版本;
- 难以进行回滚操作;
- ImagesPullPolicy为IfNotPresent时,本地镜像不会更新。
每个镜像tag使用应用版本号来标识。
应用健康检查规范
应用健检查规范是实现自动化运维的重要组成部分,也是系统故障自动发现和自我恢复的重要手段。目前有两种健康检查方式,分别是进程级和业务级。
进程级健康检查是Kubernetes本身具备的,它用来检验容器进程是否存活,是默认开启的。
业务级的健康检查由业务实现,它有三点要求:
- 必须要检查自身核心业务是否正常;
- 健康检查程序执行时间要小于健康检查周期;
- 健康检查程序消耗资源要合理控制,避免出现服务抖动。
应用健康检查实现
健康检查程序在不同环境下有着不同的实现:
- web服务下采用HTTP GET方式进行健康检查,需要实现类似一个“/healthz”URL,这个URL对应的程序需要检查所有核心服务是否正常,健康检查程序还应该在异常情况下输出每一个检查项的状态明细。
- 其他网络服务下可以采用探查容器指定端口状态来判断容器健康状态。
- 非网络服务下需要在容器内部执行特定命令,根据退出码判断容器健康状态。
主机网络模式
一般应用不建议使用主机网络。如果使用主机网络模式,需要考虑以下问题:
- 避免与主机其他系统端口冲突;
- 多实例服务需要强制分散,部署在不同的主机上;
- 避免容器故障或者容器所在的主机出现故障,建议引入额外的负载均衡器或其他工具来避免单独故障。
应用存储卷设置
将存储放置与容器内部
- 优点:配置简单,便于容器实例水平扩展;存储性能也和宿主机上启动相当,几乎没有额外的损耗。
- 缺点:容器销毁或删除,容器内的存储也会被销毁,数据持久化比较困难;同时,在业务逻辑上要求每个容器实例存储文件相互没有管理。
- Volume类型选择:使用EmptyDir类型,可以供一个Pod内多个容器共享;也可以不使用任何Volume类型,由容器引擎管理应用的文件存储。
- 适用场景:适用无状态容器应用,在系统运行过程中产生的临时文件可以被保存在容器的存储空间里。
- 注意事项:需要考虑存储空间的限制。
将存储挂载在外部宿主机上
- 优点:数据不会因容器销毁而丢失,可永久保存;存储性能与直接在物理机使用相当,没有磁盘I/O的额外损耗。
- 缺点:多实例的应用在同一台宿主机上的目录配置变的复杂,要求实例对存储的使用互补干扰;历史数据在后期业务中仍需使用,需要将容器应用与主机形成绑定关系,不利于应用在故障恢复时选用其他可以的节点重建。
- Volume类型选择:使用HostPath类型,将宿主机目录挂载到容器内;使用local类型的PV,如lvm
- 适用场景:适用有状态容器应用,以及对磁盘I/O性能要求高的应用,同时这类应用应该不能随意水平扩展;还需要通过其他机制实现存储高可用保障,如可以使用数据远程备份数据。
- 注意事项:需要考虑存储空间的限制;避免多实例运行在同一台主机。
使用外部共享存储
- 优点:配置简单,数据的持久化、备份都由共享存储提供解决方案,也便于容器的水平扩展。
- 缺点:由于共享存储多是网络存储,所以在进行文件读写时需要经过网络传输,存储性能比较差。
- Volume类型选择:使用PV或者StroageClass类型的Volume。
- 适用场景:适用对磁盘I/O性能要求不高的应用。
- 注意事项:需要考虑PV存储空间的限制;需要考虑存储的性能。
应用弹性伸缩管理
应用弹性伸缩有以下几种方式:
- 手工扩缩容
· 应用场景:手工扩缩容通常用于预先知道业务量的变化的情况,如有计划的市场促销活动,可以预先通过手工方式对Pod镜像水平扩容,已达到对预期增加量的支持,在业务高峰过后,预期业务量下降,也可以手工方式缩减容器实例释放系统资源。
· 局限性:手工方式对于不可预料的突发业务量的变化很难提供支持。
- 基于CPU/内存使用率的自动扩缩容
· 应用场景:在CPU/内存使用率提高时能够水平扩展来完成更多业务。
- 自定义业务指标的自动扩缩容
· 应用场景:可以通过监控等组件来实现自定义业务指标给HPA控制器进行扩缩容决策。
- 基于时间段进行扩缩容
· 应用场景:有明显业务量特征规律的应用,设置时间段进行实例扩缩,如白天业务量比较多,半夜业务量少,就可以设置对应的时间段,在白天进行实例扩增,半夜进行实例缩容。
业务无感知弹性伸缩
应用场景:当业务量临近触发阈值,会频繁触发业务弹性伸缩;当前弹性缩容过程中存在丢失连接,Pod尚未处理完请求就被Kubelet强制销毁,从⽽影响⼩部分业务。
方案:
- 根据时间点扩缩容
- 扩缩容指标分开配置
- 比如达到CPU 80% request值进行扩容,达到CPU30%的值缩容
- 内存类似
- 解决CPU or Memory在一定时间窗口内飙高造成业务延时,影响用户体验的问题
c. 增加Prestop 优雅终止
d. 业务侧优化
- 优雅处理SIGTERM信号。
e. 修改缩容窗口时间
- 修改弹性伸缩容器的启动命令,增加如下参数:--downscaleForbiddenWindow=30
03
应用上云中遇到的问题
镜像应该包含哪些内容
容器镜像应该包含以下内容:
- 基本的OSS
- 基本的语言运行环境,如lib、bin文件
- 可运行的程序包
除外部依赖外,镜像可以单独运行,如果容器运行时需要依赖的一些插件,可以通过init container来进行数据的预热和预处理。
容器时区配置
很多官方镜像都使用的是UTC时区,我们使用这些镜像时,需要把时区进行修改为CST时区,我们可以在制作应用镜像时就对时区做正确的配置,防止后期容器运行时一些出现时间不正确问题。
或者在部署应用时,采用Volume挂载的方式挂载宿主机的localtime文件方式。
容器中字符集配置
使用官方镜像作为Base镜像,经常会遇见在容器的控制台输出目录,会显示中文乱码;或者应用标准日志输出的日志显示为乱码。显示乱码问题可以通过在Dockerfile中指定字符集来进行解决。
Java应用内存配置
容器镜像的容器化后的Java应用(Java9以前),运行一段时间后会因总内存超过cgroups限制不断重启。
一个Pod内存分配预估:
- Java需要使用的内存:
· 堆内存:X
· 类加载区:64M/128M/256M
· 线程占用:256KB/1M *N ~~ 250M-1.5G
· 代码缓存:48M/96M/240M
· JVM其他内存:约几十~100MB
· 堆外内存:未限制,需调整为256M~600M
- 操作系统使用:约200多M
Java应用的Pod总内存推荐分配:Java堆内存*1.5/2倍;
集群内部访问
同分区服务之间的调用
- 在分区namespacea下,有两个服务,分别为服务A(svca):80、B(svcb):80和有状态服务M(集群模式:M-0为主,读写;M-1为从,只读):3306,服务A调用服务B,则在服务A里配置B服务地址为svcb:80或svcb.namespacea:80,服务B调研服务M,则在服务B中配置M-0.M:3306和M-1.M:3306。
不同分区服务之间的调用
- 在上述分区namespacea的基础上,另一个分区namspaceb下,有一个服务C(svcc),服务C需要调用namespacea分区的A服务,则在服务C里配置A服务地址为svca.namespacea:80,并且C服务需要访问分区namespacea下的有状态服务M,则在服务C里配置M-0.M.namespacea:3306和M-1.M.namespacea:3306
外部访问集群内服务
通过负载均衡器(F5/Ingress-Controller)代理。
- 集群外的服务调用集群内的服务需要通过集群内服务对外暴露的方式,平台支持HTTP和TCP/UDP方式暴露服务。
通过NodePort或者LoadBalancer向外暴露。
直接访问容器(不建议)
- 基于部分的平台网络方案,集群外的服务可以直接通过PodIP 容器端口就可以实现访问。
集群内访问集群外服务
直接访问集群外服务域名或IP。
访问集群内部服务名
- 通过平台创建外部服务。例如数据库、中间件等服务注册为平台内部服务,并产生内部服务名。