微保服务容器化落地实践

【编者的话】微保的服务可以笼统的分为前端服务和后端服务两类,其中微信小程序是面向C端的主要入口,但实际上是托管运行在微信的后台,不涉及到容器化的事项,所以也不在本文的讨论范围内。

其他前端服务包括内部系统前端及ToB业务系统前端,这一块的技术栈相对单一,大部分是JavaScript实现。后端服务包括小程序后台、数据分析后台、内部系统后台以及ToB业务服务后台,涉及的技术栈主要集中在Java、Golang和Python三种开发语言及框架上。

图1:服务分类

从以上的分析可以看出,微保的服务涉及到的技术栈比较集中,容器化的方案能涵盖上述几种主流的业务基本上就能支撑90%以上的场景了,至于那些比较小众或者非主流业务就要具体场景具体分析了,这里不做展开讨论。

下面再从微服务的角度分析一下容器化改造的背景,微保在成立之初技术架构选型时就确定了采用分布式架构,后台服务全部微服务化,微服务之间通过gRPC通信,有统一的服务注册中心;微服务架构不仅在开发迭代方面更敏捷高效也给容器化改造打下了坚实的基础,因为传统企业在做容器化改造之前很重要的一步就是对现有的单体架构服务做微服务拆分,然后才是容器化,只有这样才能发挥出容器技术在敏捷开发、持续交付过程中的优势。

而在微保,我们可以直接跳过这一步繁琐的改造工作直接进入正题——容器化,接下来将会介绍容器化过程中遇到的一些典型问题以及相应的解决方案与落地实践。

过程与挑战

容器化

容器化,简单的理解就是将服务放在容器环境中运行,其中最关键的一步是制作镜像即服务的Docker Image。制作镜像有几个需要重点考虑的问题,首先是精简,容器部署的优势很大一部分来自其灵活快捷的发布与部署过程,如果镜像过于庞大和臃肿必然会影响发布速度,使容器的优势大打折扣;其次是安全,一方面要考虑基础环境的安全,另一方面要考虑自身程序与代码安全,毕竟安全生产无小事,保证业务稳定运行的前提下努力提高生产环境安全性是每个公司都要持续追求的目标;再次是标准化,这一块主要是从开发、测试、运维整个流程考虑,规范的标准不仅能降低各个环节人员的沟通成本、提高工作效率,还能促进自动化工具的推进与落实,可谓一石二鸟。

精简镜像

精简并不意味着镜像体积越小越好,确实体积越小发布越快,但也会带来一些现实问题。最初我们也曾经误入歧途,基础镜像都采用Alpine来制作,测试过程中发现有些底层的基础库缺失导致程序部分功能异常,有些常用的调试工具没有提供,给调试和解决问题带来了困难。后来,经过讨论还是决定采用官方的CentOS基础镜像,毕竟该有的都有而且稳定,所以精简和实用稳定还是需要权衡的。

镜像的构建原则是只放程序运行时必要的文件,包括编译后的二进制文件、配置文件、启动脚本等。无关的源码、文档及配置等是尽量避免放到镜像中的,可执行文件也要采用精简编译模式来编译,争取把包体做到最小,除非特殊场景有debug需求再制作临时的调试镜像。

镜像安全

在选取基础镜像时,一定要在镜像市场选择官方制作的基础镜像,比如CentOS。打开DockerHub,搜索CentOS镜像会看到有很多条目,大部分是个人或第三方组织制作的,这种镜像有很大风险,因为我们不清楚制作者在镜像中放了什么东西,所以绝对不要使用。官方的CentOS镜像也会有很多tag,有些是测试性的,有些是修复bug的,我们通常选择一个基准稳定版比如CentOS 7.0 release版就够了,做为程序运行的基础容器环境没必要用最新的版本,稳定是永远是第一位。

程序自身的安全主要考虑避免敏感信息泄露,比如源码、鉴权用的token、证书,数据库密码等。通常这些信息会写到配置文件中或者放在固定的目录下,一旦镜像泄露被不法分子利用可能会造成很大损失。所以要严格控制镜像仓库的准入授权,做好审计;同时利用公司内部的统一网关做好token与密码回收工作,数据库方面做好准入控制,尽可能的将风险降到最低。

标准化

标准化从基础镜像这一步就要开始考虑,目前公司的所有镜像底层都是一样的,最底层的镜像有专门的运维人员负责维护。这样做有两个好处:一是统一了运行环境,方便调试与定位问题;二是有助于提高部署速度,因为Docker镜像是分层存储的,所有容器共用底层相同的镜像层,见下图:

图2:镜像分层图

如果宿主机上已经存在某个镜像的下几层数据,在拉取镜像的时候只需拉取本地不存在的上面那几层就够了,节省了网络带宽与时间。

镜像内的环境变量与目录结构也有明确的规范,明确哪些是可用的系统环境变量以及各变量的具体含义,为持续集成或灵活的编写启动脚本提供有力支撑;明确哪些是必须提供的业务环境变量,给运维系统提供变量模板,避免在业务上线时错配、漏配,提高运维人员的工作效率。

日志的输出路径是按照如下格式规范的:/data/logs/开发语言/ES索引/应用名/环境/podID/*.log。这样做一方面方便日志的采集与归档,另一方面可以避免落盘文件杂乱无章,给采集和分析带来难度。

应用编排与发布

在全行业大力宣传的云原生时代,Kubernetes做为标准的容器调度引擎已经被广泛接纳和使用,它在资源调度、故障迁移、资源隔离以及安全性等方面的突出表现具有很大诱惑力,同时有腾讯云TKE平台的加持更坚定了我们使用的信心。依赖Kubernetes强大的应用编排体系可以将现有应用方便的托管到集群中运行,同时依赖其提供的各种抽象组件的特性可以实现流畅稳定的发布过程,并且能够对服务的运行状态进行全方位的监控。

基于Deployment编排应用

Deployment是Kubernetes中最常用的用于部署无状态服务的方式,它是一种声明式的资源控制器,控制器能管理集群中最小调度单元——Pod,Pod又是实际运行在宿主机上的真实容器的抽象组合,正是这种分层的抽象管理关系使Kubernetes具备了管理底层容器的能力。

图3:Deployment,Pod,容器关系图

通常一个应用的编排文件由以下几部分组成:版本信息、元信息、模板信息。版本信息指明了当前使用过的Deployment是什么版本,元信息记录了当前应用的名字以及所在namespace,模板信息提供了创建容器及Pod所需的关键参数。

图4:Deployment声明文件

应用的更新发布

得益于Deployment声明式的资源管理方式,在更新一个应用的时候我们只需要修改编排文件中的Image字段即可触发应用的滚动更新,具体更新过程如下:

图5:Pod滚动更新

从图中可以看出整个应用的升级过程是平滑进行的,对外提供的服务并不会因为升级而中断,也就是说升级过程对上层业务是透明的。k8s的强大之处在于上述过程完全是自动化进行的,整个过程无需人工干涉;如果出现异常还可以进行一键回滚,回滚过程也是滚动式的,步骤与升级相反。

日志采集

与传统主机部署方式相比,服务容器化后日志的采集难度也增大了,因为日志写到了容器内的文件系统,想要从外部采集到需要借助其他技术手段;另外同一个宿主机上会同时运行多种服务的容器实例,每个实例都有日志输出这也从一定程度上增加了采集难度。当时有两种采集方案:

方案一:将Filebeat容器以Sidecar方式和业务容器放在同一个Pod中运行。好处是不需要对现有服务做任何改动,只需要逐一配置Filebeat的采集路径即可;缺点是浪费资源,因为一个业务容器就要搭配一个Sidecar容器,当业务规模很庞大的时候这部分额外开销将会很恐怖,而且每个服务单独配置采集路径这件事听起来就让人抓狂。

方案二:所有业务容器均挂载宿主机的/data/logs目录,日志按规范输出到指定的路径下。同时利用Kubernetes中DaemonSet资源的特性在每一个集群节点上都保证运行一个日志采集实例,这个实例同样也会挂载宿主机的/data/logs目录,按约定好的规则采集上报日志。这个方案的优势是节省资源,日志输出路径也很规范;缺点是日志采集实例可能成为瓶颈或者出现单点故障。

最终经过测试和权衡,我们选择了方案二,测试过程中发现只要保证日志采集实例的资源充足,Filebeat运行的稳定性还是没有问题的,从一年多的线上运行经验来看这种方案也足够稳定。最终的日志采集上报架构图如下:

图:日志采集架构图

监控告警

容器集群的监控对象主要分两个层面:一是容器层面的监控,这部分主要关注每一个容器的运行状态及资源使用情况;二是Kubernetes的各种资源的运行状态,如Pod,Deployment,Service等。

容器层面监控

容器层面的指标是通过cAdvisor暴露出来的,目前的Kubernetes版本kubelet都内置了cAdvisor组件,节点运行正常后直接采集数据即可。通常我们关注容器的如下几项指标:CPU利用率,内存利用率,网络IO,磁盘IO。

图7:容器服务监控图

通过利用率数据可以评估容器实例的资源值是否合理,为精细化运营提供数据依据;结合资源利用率数据与IO利用率数据可以评估服务的负载,为扩缩容提供数据依据。

Kubernetes资源层面监控

想要实现对应用的整个生命周期全方位的监控,光有容器层面的监控数据是不够的,应用的启停、扩缩容、调度等信息需要从Kubernetes层面的资源监控来获取。这一块我们采用的是社区的开源组件kube-state-metrics,该组件通过监听集群apiserver,收集集群中各种资源的最新状态以Metrics的形式暴露出来。相比于其他监控插件,该组件的好处是不会对原始数据做任何聚合与修饰,我们拿到的数据就是集群中各种资源最真实的状态,这点对于我们想要了解集群的真实运行状态非常重要。

最新版的kube-state-metrics插件已经涵盖将近30种Kubernetes资源数据,从最常用的Deployment、Pod、Service到应用比较低频的StorageClass、Job等都有涉及。实践中关注最多的是Pod的各项指标,包括运行状态、容器重启次数、调度事件以及所在节点信息等,有了这些数据就可以将Pod从生到死的整个过程记录下来了。

图8:监控告警实际应用

网络安全

熟悉Kubernetes的同学应该知道,它的网络模型是一个大的、扁平化的网络,网络模型要求Pod与Pod之间以及Pod与Node之间可以无障碍的直连访问,总结起来有如下几条原则:

  1. 所有容器都可以在不用NAT的方式下同别的容器通信
  2. 所有节点都可以在不用NAT的方式下同所有容器通信,反之亦然
  3. 容器的地址和别人看到的地址是同一个地址

网络模型这样设计的出发点是为了方便集群管理与服务组网,同时也能够减轻用户的使用负担;但是从微服务治理以及网络安全的角度考虑这样的设计就太过于开放了,微服务治理中访问授权与流量调度功能要求实现对流量的精准管控,服务之间的相互访问都是要经过认证授权的;网络安全管理员也要求容器服务的出口流量是可以管控的,例如某一个活动服务只能访问活动DB,而不能访问核心数据DB。

k8s网络策略是需要通过网络插件来实现的,也就是说官方只提供了管理接口,具体的实现要依赖底层网络解决方案。我们采用了社区提供的kube-router插件,实现了基于Namespace以及Pod之间的网络管控,使用方式也很友好,只需要给集群下发具体的网络策略就好了:

图9:网络策略声明

上面的实例文件表示对test这个namespace生效,对10.0.32.0/24这个网段的5978端口开白,也就是说test namespace下面的Pod只能访问10.0.32.0/24这个网段的5978端口,其余的出口网络地址都不是不可达的。这种网络管理方式虽然有效但太过于粗糙了,不能满足服务精细化治理的要求,还需要我们继续探索新的管理手段与方法。

运行现状

从开始推动容器化至今的一年时间,公司内部开发、测试及预发布环境已经完成100%的容器化改造,96%的服务在生产环境实现了容器化运行,以上数据基数是经评估可进行容器化改造的服务,不包含那些不适合也不准备做容器化改造的项目。

所有容器服务整体运行稳定,结合逐渐完善的监控体系与Kubernetes弹性伸缩功能已经可以轻松应对流量高峰以及公司对服务高可用的技术要求。

公司内部在很早之前也启动了对Service Mesh的探索,且已经取得了不错的成果,目前部分生产服务已经搭载Mesh容器运行了相当长的时间,更多的功能如熔断、限流、鉴权等也在持续开发中。

展望

随着公司的发展未来必然会诞生越来越多的微服务,服务之间的依赖也会更加复杂,配置更加繁琐;如何高效的解决服务发布、监控、降级、鉴权、上下线等问题是我们必须面对的挑战;微服务治理在近几年一直是一个热点话题,容器化算是给微服务治理打了一个基础,希望将来能够借助ServiceMesh的能力将公司的服务治理水平提到更高的层次。

目前集群采用的调度策略、服务弹性伸缩策略都是官方的标准解决方案,我们在实际使用中发现有些场景应该可以结合自身的业务特性做一些灵活的自定义策略,以达到精细化运营的目标。比如单Pod中多容器的场景可以根据单一容器的资源利用率数据进行弹性扩缩容、根据集群节点负载使用情况实现动态的Pod调度等;希望在未来能够结合自身业务场景,探索出更多的容器化实践。

原文链接: https://mp.weixin.qq.com/s/pX6IOd0NchR1FI7335zfZQ