阿里妹导读:本文主要介绍阿里巴巴和蚂蚁金服在大规模生产环境中落地 Kubernetes 的过程当中,在集群规模上遇到的典型问题以及对应的解决方案,内容包含对 etcd、kube-apiserver、kube-controller 的若干性能及稳定性加强,这些关键的加强是阿里巴巴和蚂蚁金服内部上万节点的 Kubernetes 集群可以平稳支撑 2019 年天猫 618 大促的关键所在。
文内藏福利,向下滑滑滑,免费课程马上领取~
从阿里巴巴最先期的 AI 系统(2013)开始,集群管理系统经历了多轮的架构演进,到 2018 年全面的应用 Kubernetes ,这期间的故事是很是精彩的。这里忽略系统演进的过程,不去讨论为何 Kubernetes 可以在社区和公司内部全面的胜出,而是将焦点关注到应用 Kubernetes 中会遇到什么样的问题,以及咱们作了哪些关键的优化。node
在阿里巴巴和蚂蚁金服的生产环境中,容器化的应用超过了 10k 个,全网的容器在百万的级别,运行在十几万台宿主机上。支撑阿里巴巴核心电商业务的集群有十几个,最大的集群有几万的节点。在落地 Kubernetes 的过程当中,在规模上面临了很大的挑战,好比如何将 Kubernetes 应用到超大规模的生产级别。golang
罗马不是一天就建成的,为了了解 Kubernetes 的性能瓶颈,咱们结合阿里和蚂蚁的生产集群现状,估算了在 10k 个节点的集群中,预计会达到的规模:算法
咱们基于 Kubemark 搭建了大规模集群模拟的平台,经过一个容器启动多个(50个)Kubemark 进程的方式,使用了 200 个 4c 的容器模拟了 10k 节点的 kubelet。在模拟集群中运行常见的负载时,咱们发现一些基本的操做好比 Pod 调度延迟很是高,达到了惊人的 10s 这一级别,而且集群处在很是不稳定的状态。数据库
当 Kubernetes 集群规模达到 10k 节点时,系统的各个组件均出现相应的性能问题,好比:后端
为了解决这些问题,阿里云容器平台在各方面都作了很大的努力,改进 Kubernetes 在大规模场景下的性能。设计模式
首先是 etcd 层面,做为 Kubernetes 存储对象的数据库,其对 Kubernetes 集群的性能影响相当重要。api
为了解决该问题,咱们设计了基于 segregrated hashmap 的空闲页面管理算法,hashmap 以连续 page 大小为 key, 连续页面起始 page id 为 value。经过查这个 segregrated hashmap 实现 O(1) 的空闲 page 查找,极大地提升了性能。在释放块时,新算法尝试和地址相邻的 page 合并,并更新 segregrated hashmap。安全
经过这个算法改进,咱们能够将 etcd 的存储空间从推荐的 2GB 扩展到 100GB,极大的提升了 etcd 存储数据的规模,而且读写无显著延迟增加。除此以外,咱们也和谷歌工程师协做开发了 etcd raft learner(类 zookeeper observer)/fully concurrent read 等特性,在数据的安全性和读写性能上进行加强。这些改进已贡献开源,将在社区 etcd 3.4 版本中发布。性能优化
Efficient node heartbeats服务器
在 Kubernetes 集群中,影响其扩展到更大规模的一个核心问题是如何有效的处理节点的心跳。在一个典型的生产环境中 (non-trival),kubelet 每 10s 汇报一次心跳,每次心跳请求的内容达到 15kb(包含节点上数十计的镜像,和若干的卷信息),这会带来两大问题:
由于 Lease 对象很是小,所以其更新的代价远小于更新 node 对象。kubernetes 经过这个机制,显著的下降了 API Server 的 CPU 开销,同时也大幅减少了 etcd 中大量的 transaction logs,成功将其规模从 1000 扩展到了几千个节点的规模,该功能在社区 Kubernetes-1.14 中已经默认启用,更多细节详见 KEP-0009。
API Server load balancing
在生产集群中,出于性能和可用性的考虑,一般会部署多个节点组成高可用 Kubernetes 集群。但在高可用集群实际的运行中,可能会出现多个 API Server 之间的负载不均衡,尤为是在集群升级或部分节点发生故障重启的时候。这给集群的稳定性带来了很大的压力,本来计划经过高可用的方式分摊 API Server 面临的压力,但在极端状况下全部压力又回到了一个节点,致使系统响应时间变长,甚至击垮该节点继而致使雪崩。
下图为压测集群中模拟的一个 case,在三个节点的集群,API Server 升级后全部的压力均打到了其中一个 API Server 上,其 CPU 开销远高于其余两个节点。
解决负载均衡问题,一个天然的思路就是增长 load balancer。前文的描述中提到,集群中主要的负载是处理节点的心跳,那咱们就在 API Server 与 kubelet 中间增长 lb,有两个典型的思路:
经过压测环境验证发现,增长 lb 并不能很好的解决上面提到的问题,咱们必需要深刻理解 Kubernetes 内部的通讯机制。深刻到 Kubernetes 中研究发现,为了解决 tls 链接认证的开销,Kubernetes 客户端作了不少的努力确保“尽可能复用一样的 tls 链接”,大多数状况下客户端 watcher 均工做在下层的同一个 tls 链接上,仅当这个链接发生异常时,才可能会触发重连继而发生 API Server 的切换。其结果就是咱们看到的,当 kubelet 链接到其中一个 API Server 后,基本上是不会发生负载切换。为了解决这个问题,咱们进行了三个方面的优化:
如上图左下角监控图所示,加强后的版本能够作到 API Server 负载基本均衡,同时在显示重启两个节点(图中抖动)时,可以快速的自动恢复到均衡状态。
List-Watch & Cacher
List-Watch 是 Kubernetes 中 Server 与 Client 通讯最核心一个机制,etcd 中全部对象及其更新的信息,API Server 内部经过 Reflector 去 watch etcd 的数据变化并存储到内存中,controller/kubelets 中的客户端也经过相似的机制去订阅数据的变化。
在 List-Watch 机制中面临的一个核心问题是,当 Client 与 Server 之间的通讯断开时,如何确保重连期间的数据不丢,这在 Kubernetes 中经过了一个全局递增的版本号 resourceVersion 来实现。以下图所示 Reflector 中保存这当前已经同步到的数据版本,重连时 Reflector 告知 Server 本身当前的版本(5),Server 根据内存中记录的最近变动历史计算客户端须要的数据起始位置(7)。
这一切看起来十分简单可靠,可是……
在 API Server 内部,每一个类型的对象会存储在一个叫作 storage 的对象中,好比会有:
每一个类型的 storage 会有一个有限的队列,存储对象最近的变动,用于支持 watcher 必定的滞后(重试等场景)。通常来讲,全部类型的类型共享一个递增版本号空间(1, 2, 3, ..., n),也就是如上图所示,pod 对象的版本号仅保证递增不保证连续。Client 使用 List-Watch 机制同步数据时,可能仅关注 pods 中的一部分,最典型的 kubelet 仅关注和本身节点相关的 pods,如上图所示,某个 kubelet 仅关注绿色的 pods (2, 5)。
由于 storage 队列是有限的(FIFO),当 pods 的更新时队列,旧的变动就会从队列中淘汰。如上图所示,当队列中的更新与某个 Client 无关时,Client 进度仍然保持在 rv=5,若是 Client 在 5 被淘汰后重连,这时候 API Server 没法判断 5 与当前队列最小值(7)之间是否存在客户端须要感知的变动,所以返回 Client too old version err 触发 Client 从新 list 全部的数据。为了解决这个问题,Kubernetes 引入 watch bookmark 机制:
bookmark 的核心思想归纳起来就是在 Client 与 Server 之间保持一个“心跳”,即便队列中无 Client 须要感知的更新,Reflector 内部的版本号也须要及时的更新。如上图所示,Server 会在合适的适合推送当前最新的 rv=12 版本号给 Client,使得 Client 版本号跟上 Server 的进展。bookmark 能够将 API Server 重启时须要从新同步的事件下降为原来的 3%(性能提升了几十倍),该功能有阿里云容器平台开发,已经发布到社区 Kubernetes-1.15 版本中。
Cacher & Indexing
除 List-Watch 以外,另一种客户端的访问模式是直接查询 API Server,以下图所示。为了保证客户端在多个 API Server 节点间读到一致的数据,API Server 会经过获取 etcd 中的数据来支持 Client 的查询请求。从性能角度看,这带来了几个问题:
为了解决这个问题,咱们设计了 API Server 与 etcd 的数据协同机制,确保 Client 可以经过 API Server 的 cache 获取到一致的数据,其原理以下图所示,总体工做流程以下:
这个方式并未打破 Client 的一致性模型(感兴趣的能够本身论证一下),同时经过 cache 响应用户请求时咱们能够灵活的加强查询能力,好比支持 namespace nodename/labels 索引。该加强大幅提升了 API Server 的读请求处理能力,在万台规模集群中典型的 describe node 的时间从原来的 5s 下降到 0.3s(触发了 node name 索引),其余如 get nodes 等查询操做的效率也得到了成倍的增加。
Context-Aware
API Server 接收请求并完成请求须要访问外部服务,如访问 etcd 将数据持久化、访问 Webhook Server 完成扩展性的 Admission 或者 Auth,甚至是 API Server 本身访问本身(loopback client) 去完成 ServiceAccount 的鉴权工做。
在这种 API Server 处理请求模型的框架下,就有以下这样的问题:当一个客户端的请求已经被客户端主动结束、或者超时结束时,若是 API Server 还依然还在为这个请求去请求外部的服务的数据、并无也在第一时间及时中止请求,那么就会致使 Goruntine 和资源的“积压”。而客户端在主动结束、或者超时结束它的请求以后,由于 Kubernetes 面向终态的架构,客户端势必会马上又发起新的请求,从而使得“积压”甚至是泄露的 Goruntine 和资源愈来愈多,最终致使 API Server OOM 和 crash。
咱们都知道 golang 中使用 context 来表示“上下文”的含义。API Server 请求外部服务的“上下文”就是客户端发起请求,那么当客户端的请求结束以后,API Server 也应该马上回收 API Server 请求外部服务的资源,即这类请求也应该马上中止并退出,只有这样,API Server 才能提升吞吐并不会被积压的 Goruntine 和资源所拖累。
阿里巴巴和蚂蚁金服的工程师发现并参与了 API Server 全链路的 context-aware 的优化工做,Kubernetes v1.16 版本已经将 Admission、Webhook 等优化为 context-aware,从而进一步提高 API Server 的性能和吞吐。
Requests Flood Prevention
API Server 对于接收处理请求的自我保护能力太过薄弱,目前能够说除了 max-inflight filter 作了限制最大读、写并发外,没有其它可以限制请求数量和并发的功能。这带来一个很是大的问题:API Server 可能由于接收并处理太多的请求从而致使 API Server OOM 或者崩溃。
虽然 API Server 是一个内部的系统,几乎没有外来请求的攻击,全部的请求都来自 Kubernetes 内部的组件和模块,API Server 也可能由于内部的请求量过大而致使本身身崩溃。根据咱们的观察和经验,API Server 接收过多请求处理而致使崩溃的主要场景有以下两部分:
咱们知道 Kubernetes 是以 API Server 为中心的系统。当 API Server 重启或者升级以后,全部的组件 client 都断开了链接并开始从新请求 API Server,特别是从新创建 List/Watch 须要比较大的资源开销。而 API Server 与 etcd 有本身的 cache 层,当客户端的 Informer List 请求到来之时,若是 cache 还未 ready 就会去请求 etcd,而大量的从 etcd List 资源可能会将 API Server 与 etcd 网络链路打满,甚至出现 API Server 和 etcd 的 OOM。而刚启动的 API Server 就陷入 crash,势必会致使客户端更大量的请求,从而陷入雪崩状态。
对于这种场景,咱们采用“主动拒绝”请求的方式。在 API Server 刚启动之时,若是 API Server 和 etcd 之间的 cache 还未 Ready,API Server 就会拒绝耗资源较大的请求,如 List 资源的请求:只有在 cache Ready 以后,API Server 才向客户端提供 List 资源的服务,不然返回 429 让客户端等待一短期后重试。只有这样,API Server 才能将接受大规模请求的主要瓶颈优化到 API Server 和客户端网络上 IO 的瓶颈。
特别是 DaemonSet 组件出现 Bug,那么请求量将乘以节点数目。咱们在线上发生过 Daemonset 出现 Bug,上万个节点一直疯狂 List Pod 从而致使 API Server crash 的案例。
对于这种的场景,咱们采用应急限流的方案。咱们实现了能够动态配置,根据请求来源的 User-Agent 去作限流。当再次出现此类问题时,咱们从监控图表里发现有问题的 User-Agent 并将它限流。只有在 API Server 健康的前提下,咱们才能对 DaemonSet 作出修复并升级。在 API Server crash 时,DaemonSet 的升级也失效了,从而陷入集群没法挽救的局面。
采用 User-Agent 而非根据 Identity 信息(请求的用户信息) 作限流缘由是由于 API Server 作请求的身份识别也须要耗费资源,极可能由于在为大量请求作身份识别过程当中就出现 API Server 资源耗尽的状况。其次,咱们能够从监控中快速的发现有问题的请求的 User-Agent,从而作到更快速的响应。
阿里和蚂蚁金服的工程师已经将该限流方案的 User Story 和优化方式已经提交到社区。
在 10k node 的生产集群中,Controller 中存储着近百万的对象,从 API Server 获取这些对象并反序列化的开销是没法忽略的,重启 Controller 恢复时可能须要花费几分钟才能完成这项工做,这对于阿里巴巴规模的企业来讲是不可接受的。为了减少组件升级对系统可用性的影响,咱们须要尽可能的减少 controller 单次升级对系统的中断时间,这里经过以下图所示的方案来解决这个问题:
经过这个方案,咱们将 controller 中断时间下降到秒级别(升级时 < 2s),即便在异常宕机时,备仅需等待 leader lease 的过时(默认 15s),无须要花费几分钟从新同步数据。经过这个加强,显著的下降了 controller MTTR,同时下降了 controller 恢复时对 API Server 的性能冲击。该方案一样适用于 scheduler。
因为历史缘由,阿里巴巴的调度器采用了自研的架构,因时间的关系本次分享并未展开调度器部分的加强。这里仅分享两个基本的思路,以下图所示:
Equivalence classes:典型的用户扩容请求为一次扩容多个容器,所以咱们经过将 pending 队列中的请求划分等价类的方式,实现批处理,显著的下降 Predicates/Priorities 的次数;
Relaxed randomization:对于单次的调度请求,当集群中的候选节点很是多时,咱们并不须要评估集群中所有节点,在挑选到足够的节点后便可进入调度的后续处理(经过牺牲求解的精确性来提升调度性能)。
阿里巴巴和蚂蚁金服经过一系列的加强与优化,成功将 Kubernetes 应用到生产环境并达到了单集群 10000 节点的超大规模,具体包括:
经过这一系列功能加强,阿里巴巴和蚂蚁金服成功将内部最核心的业务运行在上万节点的 Kubernetes 集群之上,并经历了 2019 年 618 大促的考验。
Kubernetes 是为生产环境而设计的容器调度管理系统,一经推出便迅速蹿红,它的不少设计思想都契合了微服务和云原生应用的设计法则。随着对 K8s 系统使用的加深和加广,会有愈来愈多有关云原生应用的设计模式产生出来,使得基于 K8s 系统设计和开发生产级的复杂云原生应用变得像启动一个单机版容器服务那样简单易用。
那么什么是“云原生”?做为云计算时代的开发者和从业者,咱们该如何在“云原生”的技术浪潮中站稳脚跟,在将云原生落地的同时实现自我价值的有效提高呢?
如今,咱们邀请来自全球“云原生”技术社区的亲历者和领军人物,为每一位中国开发者讲解和剖析关于“云原生”的方方面面,用视频的方式揭示此次云计算变革背后的技术思想和本质。
金牌讲师带来重磅课程,点击“CNCF×Alibaba云原生技术公开课”,登陆后,便可在线学习。
课程全程免费,咱们将带来:
适合人群:
部分讲师介绍
本文做者:曾凡松(逐灵)
本文来自云栖社区合做伙伴“阿里技术”,如需转载请联系原做者。