美团容器平台的基本架构

首先介绍一下美团容器平台的基础架构,相信各家的容器平台架构大体都差不多。




首先,容器平台对外对接服务治理、发布平台、CMDB、监控告警等等系统。通过和这些系统打通,容器实现了和虚拟机基本一致的使用体验。研发人员在使用容器时,可以和使用VM一样,不需要改变原来的使用习惯。

此外,容器提供弹性扩容能力,能根据一定的弹性策略动态增加和减少服务的容器节点数,从而动态地调整服务处理能力。这里还有个特殊的模块——“服务画像”,它的主要功能是通过对服务容器实例运行指标的搜集和统计,更好的完成调度容器、优化资源分配。比如可以根据某服务的容器实例的CPU、内存、IO等使用情况,来分辨这个服务属于计算密集型还是IO密集型服务,在调度时尽量把互补的容器放在一起。再比如,我们可以知道某个服务的每个容器实例在运行时会有大概500个进程,我们就会在创建容器时,给该容器加上一个合理的进程数限制(比如最大1000个进程),从而避免容器在出现问题时,占用过多的系统资源。如果这个服务的容器在运行时,突然申请创建20000个进程,我们有理由相信是业务容器遇到了Bug,通过之前的资源约束对容器进行限制,并发出告警,通知业务及时进行处理。






往下一层是“容器编排”和“镜像管理”。容器编排解决容器动态实例的问题,包括容器何时被创建、创建到哪个位置、何时被删除等等。镜像管理解决容器静态实例的问题,包括容器镜像应该如何构建、如何分发、分发的位置等等。

最下层是我们的容器运行时,美团使用主流的Linux+Docker容器方案,HULK Agent是我们在服务器上的管理代理程序。

把前面的“容器运行时”具体展开,可以看到这张架构图,按照从下到上的顺序介绍:






  • 最下层是CPU、内存、磁盘、网络这些基础物理资源。
  • 往上一层,我们使用的是CentOS7作为宿主机操作系统,Linux内核的版本是3.10。我们在CentOS发行版默认内核的基础上,加入一些美团为容器场景研发的新特性,同时为高并发、低延时的服务型业务做了一些内核参数的优化。
  • 再往上一层,我们使用的是CentOS发行版里自带的Docker,当前的版本是1.13,同样,加入了一些我们自己的特性和增强。HULK Agent是我们自己开发的主机管理Agent,在宿主机上管理Agent。Falcon Agent同时存在于宿主机和容器内部,它的作用是收集宿主机和容器的各种基础监控指标,上报给后台和监控平台。
  • 最上一层是容器本身。我们现在主要支持CentOS 6和CentOS 7两种容器。在CentOS 6中有一个container init进程,它是我们开发容器内部的1号进程,作用是初始化容器和拉起业务进程。在CentOS 7中,我们使用了系统自带的systemd作为容器中的1号进程。我们的容器支持各种主流编程语言,包括Java、Python、Node.js、C/C++等等。在语言层之上是各种代理服务,包括服务治理的Agent、日志Agent、加密Agent等等。同时,我们的容器也支持美团内部的一些业务环境,例如set信息、泳道信息等,配合服务治理体系,可以实现服务调用的智能路由。

美团主要使用了CentOS系列的开源组件,因为我们认为Red Hat有很强的开源技术实力,比起直接使用开源社区的版本,我们希望Red Hat的开源版本能够帮助解决大部分的系统问题。我们也发现,即使部署了CentOS的开源组件,仍然有可能会碰到社区和Red Hat没有解决的问题。从某种程度上也说明,国内大型互联公司在技术应用的场景、规模、复杂度层面已经达到了世界领先的水平,所以才会先于社区、先于Red Hat的客户遇到这些问题。

容器遇到的一些问题

在容器技术本身,我们主要遇到了4个问题:隔离、稳定性、性能和推广。

  • 隔离包含两个层面:第一个问题是,容器能不能正确认识自身资源配置;第二个问题是,运行在同一台服务器上的容器会不会互相影响。比如某一台容器的IO很高,就会导致同主机上的其他容器服务延时增加。
  • 稳定性:这是指在高压力、大规模、长时间运行以后,系统功能可能会出现不稳定的问题,比如容器无法创建、删除,因为软件问题发生卡死、宕机等问题。
  • 性能:在虚拟化技术和容器技术比较时,大家普遍都认为容器的执行效率会更高,但是在实践中,我们遇到了一些特例:同样的代码在同样配置的容器上,服务的吞吐量、响应时延反而不如虚拟机。
  • 推广:当我们把前面几个问题基本上都解决以后,仍然可能会碰到业务不愿意使用容器的情况,其中原因一部分是技术因素,例如容器接入难易程度、周边工具、生态等都会影响使用容器的成本。推广也不是一个纯技术问题,跟公司内部的业务发展阶段、技术文化、组织设置和KPI等因素都密切相关。

容器的实现






容器本质上是把系统中为同一个业务目标服务的相关进程合成一组,放在一个叫做namespace的空间中,同一个namespace中的进程能够互相通信,但看不见其他namespace中的进程。每个namespace可以拥有自己独立的主机名、进程ID系统、IPC、网络、文件系统、用户等等资源。在某种程度上,实现了一个简单的虚拟:让一个主机上可以同时运行多个互不感知的系统。

此外,为了限制namespace对物理资源的使用,对进程能使用的CPU、内存等资源需要做一定的限制。这就是Cgroup技术,Cgroup是Control group的意思。比如我们常说的4c4g的容器,实际上是限制这个容器namespace中所用的进程,最多能够使用4核的计算资源和4GB的内存。

简而言之,Linux内核提供namespace完成隔离,Cgroup完成资源限制。namespace+Cgroup构成了容器的底层技术(rootfs是容器文件系统层技术)。

美团的解法、改进和优化

隔离

之前一直和虚拟机打交道,但直到用上容器,才发现在容器里面看到的CPU、Memory的信息都是服务器主机的信息,而不是容器自身的配置信息。直到现在,社区版的容器还是这样,比如一个4c4g的容器,在容器内部可以看到有40颗CPU、196GB内存的资源,这些资源其实是容器所在宿主机的信息。这给人的感觉,就像是容器的“自我膨胀”,觉得自己能力很强,但实际上并没有,还会带来很多问题。






上图是一个内存信息隔离的例子。获取系统内存信息时,社区Linux无论在主机上还是在容器中,内核都是统一返回主机的内存信息,如果容器内的应用,按照它发现的宿主机内存来进行配置的话,实际资源是远远不够的,导致的结果就是:系统很快会发生OOM异常。

我们做的隔离工作,是在容器中获取内存信息时,内核根据容器的Cgroup信息,返回容器的内存信息(类似LXCFS的工作)。






CPU信息隔离的实现和内存的类似,不再赘述,这里举一个CPU数目影响应用性能例子。

大家都知道,JVM GC(垃圾对象回收)对Java程序执行性能有一定的影响。默认的JVM使用公式“ParallelGCThreads = (ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / 8)” 来计算做并行GC的线程数,其中ncpus是JVM发现的系统CPU个数。一旦容器中JVM发现了宿主机的CPU个数(通常比容器实际CPU限制多很多),这就会导致JVM启动过多的GC线程,直接的结果就导致GC性能下降。Java服务的感受就是延时增加,TP监控曲线突刺增加,吞吐量下降。针对这个问题有各种解法:

  • 显式的传递JVM启动参数“-XX:ParallelGCThreads”告诉JVM应该启动几个并行GC线程。它的缺点是需要业务感知,为不同配置的容器传不同的JVM参数。
  • 在容器内使用Hack过的glibc,使JVM(通过sysconf系统调用)能正确获取容器的CPU资源数。我们在一段时间内使用的就是这种方法。其优点是业务不需要感知,并且能自动适配不同配置的容器。缺点是必须使用改过的glibc,有一定的升级维护成本,如果使用的镜像是原生的glibc,问题也仍然存在。
  • 我们在新平台上通过对内核的改进,实现了容器中能获取正确CPU资源数,做到了对业务、镜像和编程语言都透明(类似问题也可能影响OpenMP、Node.js等应用的性能)。