docker 美团容器平台架构及容器技术实践

laofo · 2018年11月27日 · 最后由 laofo 回复于 2018年11月27日 · 1 次阅读

本文根据美团基础架构部/容器研发中心技术总监欧阳坚在 2018 QCon(全球软件开发大会)上的演讲内容整理而成。

背景

美团的容器集群管理平台叫做 HULK。漫威动画里的 HULK 在发怒时会变成 “绿巨人”,它的这个特性和容器的 “弹性伸缩” 很像,所以我们给这个平台起名为 HULK。貌似有一些公司的容器平台也叫这个名字,纯属巧合。

2016 年,美团开始使用容器,当时美团已经具备一定的规模,在使用容器之前就已经存在的各种系统,包括 CMDB、服务治理、监控告警、发布平台等等。我们在探索容器技术时,很难放弃原有的资产。所以容器化的第一步,就是打通容器的生命周期和这些平台的交互,例如容器的申请/创建、删除/释放、发布、迁移等等。然后我们又验证了容器的可行性,证实容器可以作为线上核心业务的运行环境。

2018 年,经过两年的运营和实践探索,我们对容器平台进行了一次升级,这就是容器集群管理平台 HULK 2.0。

  • 把基于 OpenStack 的调度系统升级成容器编排领域的事实标准 Kubernetes(以后简称 K8s)。
  • 提供了更丰富可靠的容器弹性策略。
  • 针对之前在基础系统上碰到的一些问题,进行了优化和打磨。

美团的容器使用状况是:目前线上业务已经超过 3000 个服务,容器实例数超过 30000 个,很多大并发、低延时要求的核心链路服务,已经稳定地运行在 HULK 之上。本文主要介绍我们在容器技术上的一些实践,属于基础系统优化和打磨。

美团容器平台的基本架构

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

首先,容器平台对外对接服务治理、发布平台、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 等应用的性能)。

隔离:root 权限回收

有一段时间,我们的容器是使用 root 权限进行运行,实现的方法是在 docker run 的时候加入 ‘privileged=true’ 参数。这种粗放的使用方式,使容器能够看到所在服务器上所有容器的磁盘,导致了安全问题和性能问题。安全问题很好理解,为什么会导致性能问题呢?可以试想一下,每个容器都做一次磁盘状态扫描的场景。当然,权限过大的问题还体现在可以随意进行 mount 操作,可以随意的修改 NTP 时间等等。

在新版本中,我们去掉了容器的 root 权限,发现有一些副作用,比如导致一些系统调用失败。我们默认给容器额外增加了 sys_ptrace 和 sys_admin 两个权限,让容器可以运行 GDB 和更改主机名。如果有特例容器需要更多的权限,可以在我们的平台上按服务粒度进行配置。

隔离:容器 IO

Linux 有两种 IO:Direct IO 和 Buffered IO。Direct IO 直接写磁盘,Buffered IO 会先写到缓存再写磁盘,大部分场景下都是 Buffered IO。

我们使用的 Linux 内核 3.X,社区版本中所有容器 Buffer IO 共享一个内核缓存,并且缓存不隔离,没有速率限制,导致高 IO 容器很容易影响同主机上的其他容器。Buffer IO 缓存隔离和限速在 Linux 4.X 里通过 Cgroup V2 实现,有了明显的改进,我们还借鉴了 Cgroup V2 的思想,在我们的 Linux 3.10 内核实现了相同的功能:每个容器根据自己的内存配置有对应比例的 IO Cache,Cache 的数据写到磁盘的速率受容器 Cgroup IO 配置的限制。

Docker 本身支持较多对容器的 Cgroup 资源限制,但是 K8s 调用 Docker 时可以传递的参数较少,为了降低容器间的互相影响,我们基于服务画像的资源分配,对不同服务的容器设定不同的资源限制,除了常见的 CPU、内存外,还有 IO 的限制、ulimit 限制、PID 限制等等。所以我们扩展了 K8s 来完成这些工作。

隔离: coreDump 存储

业务在使用容器的过程中产生 core dump 文件是常见的事,比如 C/C++ 程序内存访问越界,或者系统 OOM 的时候,系统选择占用内存多的进程杀死,默认都会生成一个 core dump 文件。

社区容器系统默认的 core dump 文件会生成在宿主机上,由于一些 core dump 文件比较大,比如 JVM 的 core dump 通常是几个 GB,或者有些存在 Bug 的程序,其频发的 core dump 很容易快速写满宿主机的存储,并且会导致高磁盘 IO,也会影响到其他容器。还有一个问题是:业务容器的使用者没有权限访问宿主机,从而拿不到 dump 文件进行下一步的分析。

为此,我们对 core dump 的流程进行了修改,让 dump 文件写到容器自身的文件系统中,并且使用容器自己的 Cgroup IO 吞吐限制。

稳定性

我们在实践中发现,影响系统稳定性的主要是 Linux Kernel 和 Docker。虽然它们本身是很可靠的系统软件,但是在大规模、高强度的场景中,还是会存在一些 Bug。这也从侧面说明,我们国内互联网公司在应用规模和应用复杂度层面也属于全球领先。

在内核方面,美团发现了 Kernel 4.x Buffer IO 限制的实现问题,得到了社区的确认和修复。我们还跟进了一系列 CentOS 的 Ext4 补丁,解决了一段时间内进程频繁卡死的问题。

我们碰到了两个比较关键的 Red Hat 版 Docker 稳定性问题:

在 Docker 服务重启以后,Docker exec 无法进入容器,这个问题比较复杂。在解决之前我们用 nsenter 来代替 Docker exec 并积极反馈给 RedHat。后来 Red Hat 在今年初的一个更新解决了这个问题。https://access.redhat.com/errata/RHBA-2017:1620

是在特定条件下 Docker Daemon 会 Panic,导致容器无法删除。经过我们自己 Debug,并对比最新的代码,发现问题已经在 Docker upstream 中得到解决,反馈给 Red Hat 也很快得到了解决。https://github.com/projectatomic/containerd/issues/2

面对系统内核、Docker、K8s 这些开源社区的系统软件,存在一种观点是:我们不需要自己分析问题,只需要拿社区的最新更新就行了。但是我们并不认同,我们认为技术团队自身的能力很重要,主要是如下原因:

  • 美团的应用规模大、场景复杂,很多问题也许很多企业都没有遇到过,不能被动的等别人来解答。
  • 对于一些实际的业务问题或者需求(例如容器内正确返回 CPU 数目),社区也许觉得不重要,或者不是正确的理念,可能就不会解决。
  • 社区很多时候只在 Upstream 解决问题,而 Upstream 通常不稳定,即使有 Backport 到我们正在使用的版本,排期也很难进行保障。
  • 社区会发布很多补丁,通常描述都比较晦涩难懂。如果没有对问题的深刻理解,很难把遇到的实际问题和一系列补丁联系起来。
  • 对于一些复杂问题,社区的解决方案不一定适用于我们自身的实际场景,我们需要自身有能力进行判断和取舍。 美团在解决开源系统问题时,一般会经历五个阶段:自己深挖、研发解决、关注社区、和社区交互,最后贡献给社区。
需要 登录 后方可回复。