携程容器云优化实践


随着微服务架构的流行,把容器技术推到了一个至高点上;而随着Docker,Kubernetes等容器技术的日趋成熟,DevOps的概念也再次热度上升;面对容器化的大潮趋势,各家公司都在积极地响应和实践,携程也在这方面做了不少工作,形成了自己的容器云平台。

从容器云的打造思路上,携程将其划分成了水上、水下两大部分:
  • 水下部分是指容器云服务的基础架构
  • 水上部分是指面向容器而产生的一系列工程实践配套


水下部分对Dev来说相对透明,而水上部分则会对Dev工作有直接影响,也就是DevOps概念里所提到“混乱之墙”所在的地方。所以只要水上、水下同时做好了,容器云才能真正落地,并符合DevOps理念的设想。

一、基础架构

容器云在携程主要经历了以下3个阶段:
01.png

第一阶段,模拟虚机,通过OpenStack进行管理

在这个阶段,主要的目的是验证携程已有的应用是否能够在Docker容器下正常运行,并提升系统与容器的兼容性。这个过程中系统的主要架构还是使用OpenStack的nova模块,把Docker模拟成虚机的形式进行管理,除了应用实际运行的环境产生了变化,其他任何流程,工具都不变,从而使影响范围控制在最小。

第二阶段,实现镜像发布,使用Chronos运行Job应用

在这个阶段,主要的目的是通过镜像的方式实现应用的发布和变更。真正实现 Immutable Delivery,即一旦部署后,不再对容器进行变化。并且在这个过程中,架构从比较繁重的OpenStack体系中解脱出来。使用轻量级的Mesos+Chronos来调度Job应用,在这个过程中我们同时去掉了对Long Running的Service类型应用的支持,以方便测试在极端情况下调度的消耗,和整个系统的稳定性。

此时,整个容器云的架构如下图:
02.jpg

实践证明这个架构在应对大量并发Job的调度时,Mesos自身调度消耗过大,因为每启动一次Job都需要拉起一个Docker实例,开销客观。同时也证明在携程这样的应用体量下,直接使用开源Framework是无法满足我们的需求的,这也促使我们开始走向自研Framework的方向。

第三阶段,自研Framework

在这个阶段,我们主要要解决的问题有:
  • 同时支持Job与Service两种类型的应用
  • 为每个Docker实例分配独立的IP
  • 支持Stateful的应用
  • 完善容器的监控体系


此时的总体架构如下图:
03.jpg

与第二阶段的架构不同之处:
  • 首先,重新封装了Mesos的Rest API层,使得对外提供的API更丰富(可以与其他已有系统结合,提供更多的功能),同时基于一些规范统一的考虑,收拢了一些个性化参数的使用。除此之外,独立抽象API层也是为了将来能够快速适配其他架构体系,如Kubernetes时,可以做到对上应用透明。
  • 其次,对Mesos做了集群化分布,从而提高Mesos本身的可用性。
  • 最后,为了应对大量Job类应用的调度,采用了与long running一样的方式,将Executor放置于容器内部。做到Job调度时,不重新启动容器,而是在容器内部调度一个进程。


说完了系统架构后,还有2个比较重要的问题:

网络

携程对容器实例的要求是,单容器单IP,且可路由,所以网络选项上采用的仍旧是Neutron+OVS+VLan这个模式,这个模式比较稳定,网络管理也比较透明。在实际对每个容器配置网络的过程中,携程自研了一套初始化hook机制,以通过该机制在容器启动后从外部获取对应的网络信息,如网段,或者Neutronport等,在配置到容器内,这样就完成了网络配置的持久化。大致的机制如下图所示:
04.jpg

当然利用这个hook机制还能处理其他一些特殊的case,之后也会有提到。

监控

05.jpg

监控分为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、版本等)来实现自动注册监控;在单个容器监控的基础上,还可以按照应用集群来聚合显示整个集群的监控信息。
06.jpg

自研Framework的动机


  • 轻量化,专注需求

    开源Framework为了普适性,和扩展性考虑,相对都比较重,而携程实际的使用场景,并不是特别复杂,只需要做好最基础的调度即可。因此自研的话更可以专注业务本身的需求,也可以更轻量化。

  • 兼容性,适配原有中间件

    由于携程已经形成了比较完整的应用架构体系,以及经过多年打造已经成熟的中间件系列。所以自研Framework可以很好地去适配原有的这些资源,使用开源项目反而适配改造的成本会比较大,比如路由系统,监控系统,服务治理系统等等。

  • 程序员的天性,改不如重写

    最后一点就比较实在了,开源项目使用的语言,框架比较分散,长远来说维护成本比较大


自研Framework的甜头

正如前面所说,自研Framework能够很方便地解决一些实际问题,下面就举一个我们碰到的实际例子。

我们知道Mesos本身调度资源的方式是以offer的模式来处理的,简单来说就是Mesos将剩余资源的总和以offer的形式发送出来,如果有需求则占用,没有需求则回收,待下次发送offer。但是如果碰到下图这样的情况,即Mesos一直给出2核的资源,并且每次都被占用,那一个需要4核的实例什么时候能拿到资源呢?
07.jpg

我们把这种情况叫做offer碎片,也就是一个先到的大资源申请,可能一直无法得到合适的offer的情况。

解决这个问题的办法其实很简单,无非2种:
  1. 将短时间内的offer进行合并,再看资源申请的情况
    08.jpg
  2. 缩短mesosoffer的timeout时间,使其强制回收合并资源,再次offer
    09.jpg


携程目前采用的方案2,实现非常简单。

以上大致介绍了一下携程容器云的水下部分,即基础架构的情况,以及自研Framework带来的一些好处。关于Kubernetes,由于我们封装了容器云对外的API层,所以其实对于底层架构到底用什么,已经可以很好的掌控,我们也在逐步尝试将一些stateful的应用跑在Kubernetes上,做到2套架构的并存,充分发挥各自的优势。

二、工程实践

容器化的过程除了架构体系的升级,对原先的工程实践会带来比较大的冲击。也会遇到许多理念与现实相冲突的地方,下面分别介绍携程遇到的一些实际问题和解决思路。

代码包到镜像,交付流程如何适配,如何迁移过渡?

DevOps理念提倡“谁开发,谁运行”,借助docker正好很方便的落地了这个概念。携程的CI/CD系统同时支持了基于镜像与代码包的发布。这样做的好处是能够在容器化迁移的过程中做到无缝和灰度。
10.jpg

能像虚机一样登陆机器吗?SSH?

Docker本身提倡单容器单进程,所以是否需要sshd是个很尴尬的问题。但是对于Docker实例的控制,以及执行一些必须的命令还是很有必要的,至少对于ops而言是一种非常有效的排障手段。所以,携程采用的方式是,通过web console与宿主机建立连接,然后通过exec的方式进入容器。
11.jpg

Tomcat能否作为容器的主进程?

我们知道主进程挂掉,则容器实例也会被销毁。而Java开发都知道,Tomcat启动失败是很正常的case。由此就产生了一个矛盾,Tomcat启动失败,并不等同于容器实例启动失败,我们需要去追查Tomcat启动失败的原因。由此可见,Tomcat不能作为容器的主进程。因此,携程仍旧使用Supervisord来维护Tomcat进程。同时在启动时会注册一些自定义hook,以应对一些特殊的应用场景。比如:某些应用需要在Tomcat成功启动,或成功停止后进行一些额外的操作,等等。
12.jpg

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服务地址等,这些内容则会在最终被按标准替换成公司统一的值。
13.jpg

Dockerfile的原罪

Dockerfile有很多好处,但同时也存在很多坏处:
  • 无法执行条件运算
  • 不支持继承
  • 维护难度大
  • 可能成为一个后面,破坏环境标准


因此,如果允许PD对每个应用都自定义Dockerfile的话,很有可能破坏已有的很多标准,产生各种各样的个性化行为,使得统一运维变成不可能,这种情况在携程这样的运维体谅下,是无法接受的。

打造“Plugin”服务平台

所以,携程决定通过 “Plugin”服务的方式,把Dockerfile的使用管控起来,将一些常规的通过Dockerfile实现的功能形成为“Plugin”,在Image build的过程中进行执行。这样做的好处是,所提供的服务可标准化,并且可复用,还可以任意组装。比如:我们分别提供“安装FTP”,“安装Jacoco”等插件服务。用户在完成自己的代码后,进行image build时就可以单选或多选这些服务,那最后形成的image中就会附带这些插件。并且针对不同的测试环境可以选择不同的插件,形成不同的镜像。
14.jpg

对于一个“Plugin”而言,甚至可以定义一些hook(注册supervisord hook),以及一些可exec执行的脚本,从而进一步扩展了“Plugin”的能力。比如可以插入一个Tomcat的启停脚本,从而获取从外部控制容器内Tomcat的能力。
15.jpg

公司内的每个PD都可以申请注册“Plugin”,审核通过后,就可以在平台上被其他应用所使用。注册步骤:
  1. 为服务定义名称和说明
  2. 选择服务可支持的环境(如:测试,生产)
  3. 上传自定义的Dockerfile
  4. 上传自定义的可运行脚本
    16.jpg

    17.jpg


“Jacoco Plugin”的实例

Jacoco是一个在服务端收集代码覆盖率的工具,以帮助测试人员确认测试覆盖率。这个工具的使用有以下几个需求:
  1. 需要在代码允许环境中安装Jacoco agent
  2. 只需要在特定的测试环境进行安装,生产环境不能安装
  3. 被测应用启动后,需要往Jacoco后端服务进行注册
  4. 测试过程中可以方便控制Jacoco的启停(通过Tomcat启动参数控制)


针对以上的需求,定制一个“JacocoPlugin”的工作,如下图:
18.jpg

  1. 通过dockerfile安装 jacoco agent
  2. 注册一个supervisord hook,在tomcat启动成功后向Jacoco service进行注册
  3. 利用一个自定义tomcat重启脚本,并在平台的web server上暴露API来控制Jacoco的启停


这样,所有容器云上的应用在image build时就都可以按需选择是否需要开通Jacoco服务了。
19.jpg

利用这样的平台机制,还提供了一系列其他类型的“Plugin”服务,以解决环境个性化配置的问题。

三、总结

  1. DevOps或者容器化是理念的变化,更需要接地气的实施方案
  2. 基础架构,工程实践和配套服务,需要并进,才能落地
  3. 适合自己的方案才是最好的方案


携程的容器云进程还在不断的进化之中,很多新鲜的事务和问题等待着我们去发现和探索。

作者简介:王潇俊,多年来致力于云平台及持续交付的实践。2015年加入携程,参与携程部署架构的全面改造,主导设计和打造新一代的适用于微服务的发布系统。同时负责基于携程私有云的兼容虚机与容器的持续交付平台。ROR狂热粉丝,敏捷文化的忠实拥趸。

原文链接

0 个评论

要回复文章请先登录注册