干货 | 携程容器云优化实践
王潇俊,多年来致力于云平台及持续交付的实践。2015年加入携程,参与携程部署架构的全面改造,主导设计和打造新一代的适用于微服务的发布系统。同时负责基于携程私有云的兼容虚机与容器的持续交付平台。ROR狂热粉丝,敏捷文化的忠实拥趸。
随着微服务架构的流行,把容器技术推到了一个至高点上;而随着Docker,K8S等容器技术的日趋成熟,DevOps的概念也再次热度上升;面对容器化的大潮趋势,各家公司都在积极地响应和实践,携程也在这方面做了不少工作,形成了自己的容器云平台。
从容器云的打造思路上,携程将其划分成了水上、水下两大部分:
水下部分是指容器云服务的基础架构
水上部分是指面向容器而产生的一系列工程实践配套
水下部分对Dev来说相对透明,而水上部分则会对Dev工作有直接影响,也就是DevOps概念里所提到“混乱之墙”所在的地方。所以只要水上、水下同时做好了,容器云才能真正落地,并符合DevOps理念的设想。
一、基础架构
容器云在携程主要经历了以下3个阶段:
第一阶段,模拟虚机,通过OpenStack进行管理
在这个阶段,主要的目的是验证携程已有的应用是否能够在Docker容器下正常运行,并提升系统与容器的兼容性。这个过程中系统的主要架构还是使用OpenStack的nova模块,把Docker模拟成虚机的形式进行管理,除了应用实际运行的环境产生了变化,其他任何流程,工具都不变,从而使影响范围控制在最小。
第二阶段,实现镜像发布,使用Chronos运行Job应用
在这个阶段,主要的目的是通过镜像的方式实现应用的发布和变更。真正实现 Immutable Delivery,即一旦部署后,不再对容器进行变化。并且在这个过程中,架构从比较繁重的OpenStack体系中解脱出来。使用轻量级的Mesos+Chronos来调度Job应用,在这个过程中我们同时去掉了对Long Running的Service类型应用的支持,以方便测试在极端情况下调度的消耗,和整个系统的稳定性。
此时,整个容器云的架构如下图,
实践证明这个架构在应对大量并发job的调度时,Mesos自身调度消耗过大,因为每启动一次job都需要拉起一个docker实例,开销客观。同时也证明在携程这样的应用体量下,直接使用开源Framework是无法满足我们的需求的,这也促使我们开始走向自研Framework的方向。
第三阶段,自研Framework
在这个阶段,我们主要要解决的问题有:
同时支持Job与Service两种类型的应用
为每个docker实例分配独立的IP
支持stateful的应用
完善容器的监控体系
此时的总体架构如下图:
与第二阶段的架构不同之处:
首先,重新封装了Mesos的Rest API层,使得对外提供的API更丰富(可以与其他已有系统结合,提供更多的功能),同时基于一些规范统一的考虑,收拢了一些个性化参数的使用。除此之外,独立抽象API层也是为了将来能够快速适配其他架构体系,如K8S时,可以做到对上应用透明。
其次,对Mesos做了集群化分布,从而提高Mesos本身的可用性。
最后,为了应对大量Job类应用的调度,采用了与long running一样的方式,将executor放置于容器内部。做到Job调度时,不重新启动容器,而是在容器内部调度一个进程。
说完了系统架构后,还有2个比较重要的问题:
网络
携程对容器实例的要求是,单容器单IP,且可路由,所以网络选项上采用的仍旧是Neutron+OVS+VLan这个模式,这个模式比较稳定,网络管理也比较透明。在实际对每个容器配置网络的过程中,携程自研了一套初始化hook机制,以通过该机制在容器启动后从外部获取对应的网络信息,如网段,或者Neutronport等,在配置到容器内,这样就完成了网络配置的持久化。大致的机制如下图所示:
当然利用这个hook机制还能处理其他一些特殊的case,之后也会有提到。
监控
监控分为2个部分,一块是对Mesos集群的监控。携程用了很多开源技术,如:Telegraf、influxdb、Grafana等,并做了一些扩展来实现mesos集群的监控,采集mesos-master状态、task执行数量、executor状态等等,以便当mesos集群出现问题时能第一时间知道整个集群状态,进而进行修复。
另一块是对容器实例的监控,携程监控团队开发了一套监控系统hickwall, 实现了对容器的监控支持。 hickwall agent部署在容器物理机上,通过Docker client 、cgroup等采集容器的运行情况,包括 CPU 、Memory、Disk IO等常规监控项;由于容器镜像发布会非常频繁的创建、删除容器,因此我们把容器的监控也做成自动发现,由hickwall agent发现到新的容器,分析容器的label信息(比如: appid、版本等)来实现自动注册监控;在单个容器监控的基础上,还可以按照应用集群来聚合显示整个集群的监控信息;
自研Framework的动机
轻量化,专注需求
开源Framework为了普适性,和扩展性考虑,相对都比较重,而携程实际的使用场景,并不是特别复杂,只需要做好最基础的调度即可。因此自研的话更可以专注业务本身的需求,也可以更轻量化。
兼容性,适配原有中间件
由于携程已经形成了比较完整的应用架构体系,以及经过多年打造已经成熟的中间件系列。所以自研Framework可以很好地去适配原有的这些资源,使用开源项目反而适配改造的成本会比较大,比如路由系统,监控系统,服务治理系统等等。
程序员的天性,改不如重写
最后一点就比较实在了,开源项目使用的语言,框架比较分散,长远来说维护成本比较大
自研Framework的甜头
正如前面所说,自研Framework能够很方便地解决一些实际问题,下面就举一个我们碰到的实际例子。
我们知道mesos本身调度资源的方式是以offer的模式来处理的,简单来说就是mesos将剩余资源的总和以offer的形式发送出来,如果有需求则占用,没有需求则回收,待下次发送offer。但是如果碰到下图这样的情况,即mesos一直给出2核的资源,并且每次都被占用,那一个需要4核的实例什么时候能拿到资源呢?
我们把这种情况叫做offer碎片,也就是一个先到的大资源申请,可能一直无法得到合适的offer的情况。
解决这个问题的办法其实很简单,无非2种:
1、将短时间内的offer进行合并,再看资源申请的情况
2、缩短mesosoffer的timeout时间,使其强制回收合并资源,再次offer
携程目前采用的方案2,实现非常简单。
以上大致介绍了一下携程容器云的水下部分,即基础架构的情况,以及自研Framework带来的一些好处。关于k8s,由于我们封装了容器云对外的API层,所以其实对于底层架构到底用什么,已经可以很好的掌控,我们也在逐步尝试将一些stateful的应用跑在k8s上,做到2套架构的并存,充分发挥各自的优势。
二、工程实践
容器化的过程除了架构体系的升级,对原先的工程实践会带来比较大的冲击。也会遇到许多理念与现实相冲突的地方,下面分别介绍携程遇到的一些实际问题和解决思路。
代码包到镜像,交付流程如何适配,如何迁移过渡?
DevOps理念提倡“谁开发,谁运行”,借助docker正好很方便的落地了这个概念。携程的CI/CD系统同时支持了基于镜像与代码包的发布。这样做的好处是能够在容器化迁移的过程中做到无缝和灰度。
能像虚机一样登陆机器吗?SSH?
docker本身提倡单容器单进程,所以是否需要sshd是个很尴尬的问题。但是对于docker实例的控制,以及执行一些必须的命令还是很有必要的,至少对于ops而言是一种非常有效的排障手段。所以,携程采用的方式是,通过web console与宿主机建立连接,然后通过exec的方式进入容器。
Tomcat能否作为容器的主进程?
我们知道主进程挂掉,则容器实例也会被销毁。而Java开发都知道,tomcat启动失败是很正常的case。由此就产生了一个矛盾,tomcat启动失败,并不等同于容器实例启动失败,我们需要去追查tomcat启动失败的原因。由此可见,tomcat不能作为容器的主进程。因此,携程仍旧使用Supervisord来维护tomcat进程。同时在启动时会注册一些自定义hook,以应对一些特殊的应用场景。比如:某些应用需要在tomcat成功启动,或成功停止后进行一些额外的操作,等等。
JVM配置是谁的锅?
容器上线后一段时间,团队一直被一个JVM OOM的问题所困扰,原来在虚机跑的好好的应用,为什么到容器就OOM了呢?最后定位到问题的原因是,容器采用了cpu quota的模式,但JVM无法准确的获取到cpu的数量,只能获取到宿主机cpu的数量;同时由于一些java组件会根据cpu的数量来开启thread数量,这样就造成了堆外内存殆尽,最终造成OOM。
虽然,找到了OOM的原因,但是对于容器云来说,却面临了一个棘手的问题。容器实例不像虚机,在虚机上,用户可以按需定义JVM配置,然后再将代码进行发布。在容器云上,发布的是镜像,JVM的配置则变成了镜像的包含物,无法在runtime时进行灵活修改。
而且,容器本身并不考虑研发流程上的一些问题。比如,我们有不同的测试环境,不同的测试环境可能有不同的JVM配置,这显然与docker设想的,一个镜像走天下的想法矛盾了。
最后,对于终端用户而言,在选择容器时,往往挑选的是flavor,因此我们需要对应不同的flavor定义一套标准的JVM配置,利用之前提到的容器启动时的hook机制,从外部获取该容器匹配的标准JVM配置。
我们也总结了一些对于对外内存的最佳实践,如下:
• Xmx = Xms = Flavor * 80%
• Xss = 256K
• 堆外最小800,最大2G,符合这个规则之内,以20%计
问题又来了,用户需要自定义JVM?
最终,我们将JVM配置划分成了3个部分:
1、系统默认推荐部分
2、用户自定义override部分
3、系统强制覆盖部分
允许用户通过代码或外部配置系统,对应用的JVM参数进行配置,这些配置会覆盖掉系统默认推荐的配置,但是有一些配置是公司标准,不允许覆盖的,比如统一的jmx服务地址等,这些内容则会在最终被按标准替换成公司统一的值。
Dockerfile的原罪
Dockerfile有很多好处,但同时也存在很多坏处:
无法执行条件运算
不支持继承
维护难度大
可能成为一个后面,破坏环境标准
因此,如果允许PD对每个应用都自定义dockerfile的话,很有可能破坏已有的很多标准,产生各种各样的个性化行为,使得统一运维变成不可能,这种情况在携程这样的运维体谅下,是无法接受的。
打造“plugin”服务平台
所以,携程决定通过 “plugin”服务的方式,把dockerfile的使用管控起来,将一些常规的通过dockerfile实现的功能形成为“plugin”,在Image build的过程中进行执行。这样做的好处是,所提供的服务可标准化,并且可复用,还可以任意组装。比如:我们分别提供“安装FTP”,“安装Jacoco”等插件服务。用户在完成自己的代码后,进行image build时就可以单选或多选这些服务,那最后形成的image中就会附带这些插件。并且针对不同的测试环境可以选择不同的插件,形成不同的镜像。
对于一个“plugin”而言,甚至可以定义一些hook(注册supervisord hook),以及一些可exec执行的脚本,从而进一步扩展了“plugin”的能力。比如可以插入一个tomcat的启停脚本,从而获取从外部控制容器内tomcat的能力。
公司内的每个PD都可以申请注册“plugin”,审核通过后,就可以在平台上被其他应用所使用。注册步骤:
1、为服务定义名称和说明
2、选择服务可支持的环境(如:测试,生产)
3、上传自定义的dockerfile
4、上传自定义的可运行脚本
“Jacoco Plugin”的实例
Jacoco是一个在服务端收集代码覆盖率的工具,以帮助测试人员确认测试覆盖率。这个工具的使用有以下几个需求:
1、需要在代码允许环境中安装Jacoco agent
2、只需要在特定的测试环境进行安装,生产环境不能安装
3、被测应用启动后,需要往Jacoco后端服务进行注册
4、测试过程中可以方便控制Jacoco的启停(通过tomcat启动参数控制)
针对以上的需求,定制一个“JacocoPlugin”的工作,如下图:
1、通过dockerfile安装 jacoco agent
2、注册一个supervisord hook,在tomcat启动成功后向Jacoco service进行注册
3、利用一个自定义tomcat重启脚本,并在平台的web server上暴露api来控制jacoco的启停
这样,所有容器云上的应用在image build时就都可以按需选择是否需要开通 jacoco 服务了。
利用这样的平台机制,还提供了一系列其他类型的“plugin”服务,以解决环境个性化配置的问题。
三、总结
1、devops或者容器化是理念的变化,更需要接地气的实施方案
2、基础架构,工程实践和配套服务,需要并进,才能落地
3、适合自己的方案才是最好的方案
携程的容器云进程还在不断的进化之中,很多新鲜的事务和问题等待着我们去发现和探索。
推荐阅读: