JDOS 2.0:Kubernetes的工业级实践

 张伟伟 分布式实验室 

JDOS 2.0:Kubernetes的工业级实践


JDOS(Jingdong Datacenter Operating System)1.0于2014年推出,基于OpenStack进行了深度定制,并在国内率先将容器引入生产环境,经历了2015年618/双十一的考验。团队积累了大量的容器运营经验,对Linux内核、网络、存储等深度定制,实现了容器秒级分配。


意识到OpenStack架构的笨重,2016年当JDOS 1.0逐渐增长到十万、十五万规模时,团队已经启动了新一代容器引擎平台(JDOS 2.0)研发。JDOS 2.0致力于打造从源码到镜像,再到上线部署的CI/CD全流程,提供从日志、监控、排障,终端,编排等一站式的功能。JDOS 2.0迅速成为Kubernetes的典型用户,并在Kubernetes的官方博客分享了从OpenStack切换到Kubernetes的过程。 JDOS 2.0的发展过程中,逐步完善了容器的监控、网络、存储,镜像中心等容器生态建设,开发了基于BGP的Skynet网络、ContainerLB、ContainerDNS、ContainerFS等多个项目,并将多个项目进行了开源。本次分享,我们主要分享的是在JDOS2.0的实践过程中关于Kubernetes的一些经验和教训。


:Kubernetes版本我们目前主要稳定在了1.6版本。本文中的实践也是主要基于此版本。我们做的一些feature和bug,有的在Kubernetes后续发展过程中进行了实现和修复。大家有兴趣也可以同社区版进行对照。


1. Kubernetes定制开发

JDOS 2.0:Kubernetes的工业级实践


为什么定制?


很多人可能会问,原生的Kubernetes不好么,为什么要定制。首先,我先来解释下定制的原因。在我们使用开源项目的过程中,无论是OpenStack还是Kubernetes,都会有一种体会,就是理想和现实是存在较大的差距的。出于各种原因,开源项目需要做很多的妥协,而且很多功能是很理想化的,这就导致了Kubernetes直接应用于生产中会遇到很多的问题。因此,我们对Kubernetes进行了定制开发,秉持了两个基本的理念,加固与裁剪:


  1. 加固主要指的是在任何时候、任何情况下,将容器的非故障迁移、服务的非故障失效的概率降到最低,最大限度的保障线上集群的安全与稳定,包括etcd故障、apiserver全部失联、apiserver拒绝服务等等极端情况。

  2. 裁剪则体现为我们删减了很多社区的功能,修改了若干功能的默认策略,使之更适应我们的生产环境的实际情况。


在这样的原则下,我们对Kubernetes进行了许多的定制开发。下面将对其中部分进行介绍。


1.1 IP保持不变


线上很多用户都希望自己的应用下的Pod做完更新操作后,IP依旧能保持不变,于是我们设计实现了一个IP保留池来满足用户的这个需求。简单的说就是当用户更新或删除应用中的Pod时,把将要删除的pod的IP放到此应用的IP保留池中,当此应用又有创建新Pod的需求时,优先从其IP保留池中分配IP,只有IP保留池中无剩余IP时,才从大池中分配IP。IP保留池是通过标签来与Kubernetes的资源来保持一致,因此ip保持不变功能不仅支持有状态的StatefulSet,还可以支持rc/rs/deployment。


重复利用IP会带来一个潜在的问题,就是当前一个Pod还未完全删除的时候,后一个Pod的网络就不能提早使用,否则会存在IP的二义性。为了提升Pod更新速度,我们对容器删除的流程进行了优化,将CNI接口的调用提前到了stop容器之前,从而大大加快了IP释放和新Pod创建的速度。


1.2 检查IP连通性


Kubernetes创建Pod,调度时Pod处于Pending的状态,调度完成后处于Creating的状态,磁盘分配完成,IP分配完成,容器创建完成后即处于Running状态,而此时有可能IP还未真正生效,用户看到Running状态但是却不能登录容器可能会产生困惑,为了让用户有更好的体验,在Pod转变为Running状态之前,我们增加了检查IP连通性的步骤,这样可以确保状态的一致性。


1.3 修改默认策略


Pod的restart策略其实是Rebuild,就是当Pod故障(可能是容器自身问题,也可能是因为物理机重启等)后,kubelet会为Pod重新创建新的容器。但是在实际过程中,其实很多用户的应用会在根目录写入一些数据或者配置,因此用户会更加期望使用先前的容器。因此我们为Pod增加了一个reUseAlways的策略,并成为restart的默认策略。而将原来的Always策略,即rebuild容器的策略作为可选的策略之一。当使用reUseAlways策略时,kubelet将会首先检查是否有对应容器,如果有,则会直接start该容器,而不会重新create一个新的容器。


对于Service,我们进行了自己的实现,可以选用HAProxy/Nginx/LVS进行导流。当节点故障时,Controller会将该节点上的Pod从对应的Service进行摘除。但是在实际生产中,其实很容易遇到另外一个问题,就是节点实际没有完全故障,是处于一个不稳定状态,比如网络时通时不通,会表现为Node的状态在ready和notready之间反复切换,会导致Service的Endpoint会反复修改,最终会影响到HAProxy/Nginx进行频繁reload。其实这个可以通过给Service配置annotation使得ep不受node notready的影响。但是我们为了安全起见,将该策略设置为了默认策略,而配置额外的annotation可以使其能够在not ready时被摘除。因为我们的LB上都默认开启了健康检查(默认是端口检查,还可以进行配置路径健康检查)。因此不健康节点的流量切除可以通过LB自身进行。


1.4 定制Controller


在实践过程中,我们有一个深刻的体会,就是官方的Controller其实是一个参考实现,特别是Node Controller和Taint Controller。Node的健康状态来自于其通过apiserver的上报。而Controller仅仅依据通过apiserver中获取的上报状态,就进行了一系列的操作。这样的方式是很危险的。因为Controller的信息面非常窄,没法获取更多的信息。这就导致在中间任何一个环节出现问题,比如Node节点网络不稳定,apiserver繁忙,都会出现节点状态的误判。假设出现了交换机故障,导致大量kubelet无法上报Node状态,Controller进行大量的Pod重建,导致许多原先的健康节点调度了许多Pod,压力增大,甚至部分健康节点被压垮为notready,逐渐雪崩,最终导致整个集群的瘫痪。这种灾难是不可想象的,更是不可接受的。


因此,我们对于Controller进行了定制。节点的状态不仅仅由kubelet上报,在Controller将其置为notready之前,还会进行复核。复核部分交由一个单独的分布式系统MAGI完成,其在多个物理POD上进行部署,收到请求会对节点分别进行独立的分析和体检,最终投票,做出节点是否notready的判断。这样最大限度的降低了节点误判的概率。


1.5 资源限制


Kubernetes默认提供了CPU和Memory的资源管理,但是这对生产环境来说,这样的隔离和资源限制是不够的,因此我们增加了磁盘读写速率限制、Swap使用限制等,最大限度保证Pod之间不会互相影响。


我们对大部分的Pod,还提供了本地存储。目前Kubernetes支持的一种容器数据本地存储方式是emptyDir,也就是直接在物理机上创建对应的目录并挂载给容器,但是这种方式不能限制容器数据盘的大小,容易导致物理机上的磁盘被打满从而影响其它进程。鉴于此,我们为Kubernetes的新开发了一个存储插件LvmPlugin,使其支持基于LVM(Logical Volume Manager)管理本地逻辑卷的生命周期,并挂载给容器使用。LvmPlugin可以执行创建删除挂载卸载逻辑盘LV,并且还可以上报物理机上的磁盘总量及剩余空间给kube-scheduler,使得创建新Pod时Scheduler把LVM的磁盘是否满足也作为一个调度指标。


对于数据库容器或者使用磁盘频率较高的业务,用户会有限制磁盘读写的需求。我们的实现方案是把这限制指标看作容器的资源,就像CPU、Memory一样,可以在创建Pod的yaml文件中指定,同一个Pod的不同容器可以有不同的限制值,而kubelet创建容器时可以获取当前容器对应的磁盘限制指标的值并进行相应的设置。


1.6 gRPC升级


生产环境中,当集群规模迅速膨胀时,即使利用负载均衡的方式部署kube-apiserver,也常常会出现某个或某几个apiserver不稳定,难以承担访问压力的情况。经过了反复的实验排查,最终确定是grpc的问题导致了apiserver的性能瓶颈。我们对于gRPC包进行了升级,最终地使得apiserver的性能及稳定性都有了大幅度的提升。


1.7 平滑升级


我们于2016年就着手设计研发基于Kubernetes的JDOS 2.0,彼时使用的版本是Kubernetes 1.5,2017年社区发布了Kubernetes 1.6的release版本,其中新增了很多新的特性,比如支持etcd V3,支持节点亲和性(Affinity)、Pod亲和性(Affinity)与反亲和性(anti-affinity)以及污点(Taints)与容忍(Tolerations)等调度,支持调用Container Runtime的统一接口CRI,支持Pod级别的Cgroup资源限制,支持GPU等,这些新特性都是我们迫切需要的,于是我们决定由Kubernetes 1.5升级至当时1.6最新release的版本Kubernetes 1.6.3。但是此时生产环境已经基于Kubernetes 1.5上线大量容器,如何在保证这些业务容器不受任何影响的情况下平滑升级呢?


对比了两个版本的代码,我们讨论了对于Kubernetes进行改造兼容,实现以下几点:


  1. Kubernetes 1.6默认的Cgroup资源限制层级是Pod,而老节点上的Cgroup资源限制层级是Container,所以升级后要添加相应配置保证老节点的资源限制层级不发生改变。

  2. Kubernetes 1.6默认会清理掉leacy container也就是老的Container,通过对kuberuntime的二次开发,我们保证了升级到1.6后在Kubernetes 1.5上创建的老容器不被清理。

  3. 新老版本的containerName格式不一致导致获取Pod状态时获取不到IP,从而升级后老Pod的IP不能正常显示,通过对dockershim部分代码的适当调整,我们将老版本的Pod的containerName统一成新版本的格式,解决了这个问题。


经过如上的改造,我们实现了线上几千台物理机由Kubernetes 1.5到Kubernetes 1.6的平滑升级。而业务完全无感知。


1.8 bug fix和其他feature


我们还修复了诸如GPU中的NVIDIA卡重复分配给同一容器,磁盘重复挂载bug等。这些大部分社区在后面的版本也做了修复。还增加了一些小的功能,比如增加了Service支持set-based的selector,kubelet image gc优化,kubectl get node显示时增加Node的版本信息等等。这里就不详述了。


JDOS 2.0:Kubernetes的工业级实践


2.1 参数调优和配置


Kubernetes的各个组件有大量的参数,这些参数需要根据集群的规模进行优化调整,并进行适当的配置,来避免问题以及定制自己的特殊需求。比如说有次我们其中一个集群个别节点出现了不停的在Ready和NotReady的状态之间来回切换的问题,而经过检查,集群的各个服务都处于正常的状态。很是认真研究了一下,才发现是此Node上的容器数量太多,并且每个容器的ConfigMap也比较多,导致Node节点每秒向apiserver发送的请求数也很多,超过了kubelet的配置api-qps的默认值,才影响了Node节点向apiserver更新状态,导致Node状态的切换。将相关配置值调大后就解决了这个问题。另外apiserver的api-qps,api-burst等配置也需要根据集群规模以及apiserver的个数做出正确的估量并设置。


再比如说,当Node节点挂掉多久后才允许Kubernetes自动迁移上面的容器,不管是使用node-controller或taint-controller经过适当的配置都可以实现。以及kubelet重启时,会再一次进行predicates检查,对于不符合二次检查要求的Pod会将它们删除,而如果有些Pod很重要你绝对不希望它们在这种检查中被删掉,那么其实给Pod设置一下对应的annotation就可以实现。关于这样的配置涉及到很多的细节,因为文档可能没有更新的那么及时,最好对于源码有一定掌握。


2.2 组件部署


Apiserver使用域名的方式做负载均衡,可以平滑扩展,Controller-manager和Scheduler使用Leader选举的方式做高可用。同时为了分担压力和安全,我们每个集群部署了两套etcd。一套专门用于event的存储。另一套存储其他的资源。


2.3 Node管理


使用标签管理Node的生命周期,从接管物理机到装机完成再到服务部署完毕网络部署完毕NodeReady直至最终下线,每一个步骤都会自动给Node添加对应标签,以方便自动化运维管理。Node生命周期如下图:

JDOS 2.0:Kubernetes的工业级实践


同时,Node上还添加了区域zone。可以方便一些将一些部门独立的物理机纳入统一管理,同时资源又能保证其独享。对于一些特殊的资源,比如物理机上有GPU、SSD等特殊资源,对应会给节点打上专属的标签用以标识。用户申请时可以根据需要申请对应的资源,我们在申请的Pod上配属相应的标签,从而将其调度至相应的节点。


节点发生故障或者需要下线维护时,首先将该节点置为disable,禁止调度,如果超过一定时间,节点尚未恢复,Controller会自动迁移其上的容器到其他正常的节点上。


2.4 上线流程管理


上线之前制定上线步骤,经相关人员review确认无误后,严格按照上线步骤操作。上线操作按照先截停控制台等入口,而后Controller、Scheduler停止,再停止kubelet。上线结束后按照反向,依次做验证,启动。


先截停控制台以及apiserver是为了阻止用户继续创建删除,而后停止Controller和Scheduler对Pod的调度和迁移等操作,最后停止kubelet对Pod生命周期的管理。这样的顺序可以最大程度保证运行pod不受上线过程的影响,否则的话容易造成Controller和Scheduler的误判,做出错误的决策。


2.5 故障演练与应急恢复


为了防止一些极端情况和故障的发生,我们也进行了多次的故障演练,并准备了应急恢复的预案。在这里我们主要介绍下etcd和apiserver的故障恢复。


2.5.1 etcd的故障恢复


使用etcd恢复的大致流程。在etcd无法恢复情况下,另外启动一个etcd集群的方式。


etcd在整个集群中的非常重要,一旦有差错,整个集群都会处于瘫痪状态,更不要说数据出现丢失的情况。线上etcd集群的运行还是相对稳定的,但是显然还是要防患于未然,为此我们特地定制了etcd备份和恢复。线上所有集群每隔1小时都会自动做一次备份,并且发邮件通知备份成功与否。恢复则分为原地恢复和利用原集群的数据另外启动一个集群恢复两种方式,大致的恢复流程如下:


  • 将原etcd数据目录备份

  • 另选3台机器,搭建一个全新的etcd集群(带证书认证)

  • 将新etcd集群的etcd停止,数据目录下的内容全部删除

  • 将备份数据拷贝到3台新etcd机器上,使用etcdctl snapshot restore逐个节点恢复数据,注意观察恢复后id是否一致。数据恢复完成后查看endpoint status状态是否正常。


2.5.2 apiserver的故障恢复


一般单台apiserver故障,将其进行维护即可。如果apiserver同时发生故障时,会导致Node节点状态出现异常,此时则需要立刻停掉Controller和Scheduler服务,防止状态判断失误造成的误决策。在apiserver修复后进行验证后,再启动其他组件。


3. 运维工具JDOS 2.0:Kubernetes的工业级实践


3.1 Ansible


JDOS 2.0日常管理的物理机和容器规模庞大,平时的部署和运维如果没有好用的工具会非常繁琐,为此我们主要选用Ansible开发了2.0专属的部署和运维工具,极大的提高了工作效率。


集群部署使用Ansible新搭建集群或者扩容集群或者升级都及其方便,只需要事先把模板写好,具体操作时执行简单的命令即可,同时也不用担心由于操作失误引发问题。


3.2 Kubernetes Connection Plugin


为了方便操作各个容器,我们还开发了Ansible的Kubernetes插件,可以通过Ansible对容器进行批量的诸如更新密码、分发文件、执行命令等操作。


hosts配置:JDOS 2.0:Kubernetes的工业级实践



结果样例:JDOS 2.0:Kubernetes的工业级实践



3.3 巡检工具


日常巡检系统对于及时发现物理机及各个服务的异常配置和状态非常重要,尤其是大促期间,系统的角落有些许异常可能就带来及其恶劣的影响,因此特殊时期我们还会加大巡检的频率。


巡检的系统的巡检模块都是可插拔的,巡检点可以根据需求灵活配置,随时增减,其中一个系统的控制节点的巡检结果样例如下:JDOS 2.0:Kubernetes的工业级实践


巡检结果出现问题,会在巡检详报中以红色字体标示。


3.4 其他工具


为了方便运维统计和监控,我们还开发了一些其他的工具:


  • API日志分析工具:使用Python对日志进行预处理,形成结构化数据。而后使用Spark进行统计分析。可以对请求的时间、来源、资源、耗时长短、返回值等进行分析。

  • kubesql:可以将Kubernetes的如Pod、Service、Node等资源,处理成类似于关系数据库中的表。这样就可以使用SQL语句对于相关资源进行查询。比如可以使用SQL语句来查询MySQL、default namespace下的所有Pod的名字。

  • event事件通知:监听event,并根据event事件进行分级,对于紧急事件接入告警处理,可以通过邮件或者短信通知到相关运维人员。


上一篇:TableView版ScrollRect


下一篇:一文了解如何源码编译Rainbond基础组件