陈聪 分布式实验室 

滴滴弹性云Kubernetes实践_Java

当前Kubetnetes已经成为容器编排领域事实的行业标准,越来越多的公司选择使用Kubernetes来搭建其容器云平台。本次分享主要介绍滴滴弹性云在围绕Kubernetes打造企业级私有云过程中的一些实践经验和教训,为同样正走在企业云化转型道路上的朋友们提供一些启发和帮助。
当前滴滴国际化业务全部运行在弹性云平台上,国内的顺风车,金融,汽车后市场、企业安全、客服以及专车快车等业务的全部或部分模块也运行在我们的平台上。平台规模大约1K+宿主,2W+容器实例。 
今天的分享我主要聚焦一些我们平台相对有特色的地方,希望能给各位带来一些启发。

 

滴滴弹性云Kubernetes实践_Java_02


首先介绍一下平台的网络模型,我们最开始使用的是Flannel,这种网络模型的问题大家应该都有体会,这里不多说。 
之后我们过度到了使用ONOS为主体集中式SDN网络,这种网络模型的功能可以说非常丰富,容器可以在overlay网络上随意漂移,且在容器名不变的情况下保证IP不变。于此同时容器IP相互独立,和物理网络双向互通,所以设置各类白名单也和物理机完全一致,不占用物理网络IP资源。利用Kubernetes的CNI可以很方便的为容器配置网络,用户使用起来和物理机无异。
但是此种模式的最大问题是:集中式的网络控制器在出现故障或是任何一点性能问题时,都会对集群的稳定性造成显著的影响。在经历了一些稳定性问题后,我们将网络模型演进到了当前的使用物理网络的模式。在此种场景下,容器只能在同一个接入交换机下的宿主上进行漂移,所以我们在部署新集群的时候每个交换机下的宿主数量尽量趋同。 
同时我们预先为每个APP提供一个IP池,用户在创建容器之前可以先申请一个IP池(默认30个IP,未来创建的容器的IP不会超出这个IP池的范围),使用平台提供的白名单功能,预先将IP池内的IP自动化的加入到MySQL或Codis这样的公共服务白名单中,最后再启动容器。这样就实现了容器启动后不会因为连不上数据库而报错,或者扩容后新容器异常。当然,IP池是能够随着业务的变化而随时扩缩容的。调度系统在预分配IP池的时候也会根据各个tor IP的实际使用量均匀分配。 
值得一提的是我们利用Kubernetes提供的nodeAffinity和podAntiAffinity实现同一服务、同一产品线、同一服务等级等多种维度下的容器实例平均分配到不同的接入交换机和宿主下的目的。

 

滴滴弹性云Kubernetes实践_Java_03

接下来给大家介绍一下弹性云在服务发现方面做的一些改进。首先我们在很早的时候就抛弃了kube-proxy。在我们运维云平台的过程中我们发现当一个集群有几百、几千个service之后其iptables的规模要再乘以10(通常是service背后Endpoint的数量),如此多的iptables条目让我们比较难以维护和故障定位。
所以除了老集群,我们的新集群全部使用公司自研的服务发现:DSI & DGW。DSI就是基于NGINX做的公司内7层代理服务,其核心是借助服务树获取到节点对应的容器或物理机信息,将他们动态的、自动的挂载到NGINX的upstream中,实现高可用及负载均衡。容器变化时DSI可以通过watch服务树接口实时感知,实时调整,用户无需操心。
DGW则是四层的负载均衡,他同样实现了容器作为RealServer动态实时变更的功能。

 

滴滴弹性云Kubernetes实践_Java_04


下面再说一下针对业务上云稳定性做的一些改进,我们认为云上稳定性主要分为大概三个方面:

  • 容器相对于此前物理机模式下发布更新、线上扩容等流程异构导致的稳定性问题;

  • 容器自身特性导致的稳定性隐患;

  • Kubernetes本身特性带来的稳定性隐患。


首先对于业务发布流程,我们首先要做到的是尽量避免因发布导致的流量丢失或服务不可用问题,为此我们首先默认不使用Liveness探针(用户需要可以自己配),仅保留一个Readiness探针保证容器服务在创建时能正常启动。在Readiness探针探测通过后,我们会根据service的Endpoint将容器变化推送到周边系统,如之前提到的DSI、DGW等,后续的探活就由这些前端设备完成。 
此外我们为每个服务强制的分为n个分组(通常是4个),每次上线过程中用户都必须逐一分组更新服务,组与组之间强制时间隔离用于检查,前一组不发布更新完成后一组无法操作。使用这种方式一是规避了Kubernetes来做服务探活的“不确定性”,二是同物理机发布更新模式趋同,用户认同度更高,从流程上规避可能的故障风险。 
其次,针对容器自身特性的稳定性隐患可以归纳为容器隔离性不强的隐患。大家都知道直到1.10版本的Kubernetes才正式引入了cgroup的cpu_set功能,此前只有cpu_share,内存只能限制大小,磁盘和网络限制更是没有。当一台宿主上运行了多个在线业务容器时,经常遇到某容器性能下降的case,而同服务的其他容器无异常。检查发现,这通常是因为问题容器所在宿主上的其他容器负载突增或容器异常导致的消耗宿主资源过多,进而影响同宿主其他容器。为此我们在混部资源隔离方面做了一些拓展:首先对每个容器进行服务定级,根据容器的cpu request值为容器绑CPU核(cpu_set),利用cgroup v2的blk-throttle实现对非driect io场景下的磁盘读写速度隔离,利用Intel的CAT功能实现对系统三级缓存的资源优化分配,利用MBA和MBM技术实现对内存带宽的使用限制,利用tc实现对容器网络带宽的限制,利用xfs_quota实现对容器hostpath挂载目录的容量限制。我们的目标是:动态的变更以上容器所分配的资源配比,而更改的依据则是容器内业务指标的变化而变化。这块我们还在努力探索中,也欢迎有其他感兴趣的朋友和我们交流。

 

滴滴弹性云Kubernetes实践_Java_05


针对Kubernetes本身的安全加固主要体现在:

  1. 限制容器的重建次数:我们在运维过程中发现:某些配置了探针的容器因为服务没有成功启动而不断的被Kubernetes杀掉重建,在重建超过几百或数千次之后会偶发性的导致宿主的一些异常。这个问题的原因我们尚未查明,但是容器不断的重建本身就显得没有意义,再加上我们使用了自研的服务发现,所以就对每个容器的重启次数做了限制,让负载均衡层去做健康检查;

  2. 调高controller驱逐Pod的容忍时间,宿主偶发的notready可能导致容器触发evict动作,对于有状态容器来说漂移并不是完全无损的,所以调高驱逐时间是完全有必要的;

  3. 添加判断宿主状态旁路系统,对宿主状态变化做二次确认。宿主notready带来的最大影响就是容器的强制漂移,而生杀大权完全依靠kubelet向kube-apiserver上报状态数据。很多复杂的因素都有可能导致这条通路的不稳定进而影响状态更新,造成健康宿主被误诊成故障进而触发漂移最终雪崩的结果。为此我们对kube-controller-manager做了改造,controller在决定要对宿主状态做修改后还需要通过一个旁路流程确认宿主状态,两边一致之后再做修改。