## 网易云容器服务基于 Kubernetes 的实践探索 文/娄超 >大红大紫的容器编排工具 Kubernetes 已然成为这个领域的领导者,但是,其定位主要是面向私有云市场,在公有云市场下,Kubernetes 的实践面临诸多问题,本文作者将就网易云容器如何解决 Kubernetes 在公有云上的问题展开探讨,为读者提供相应的思路。 ### Kubernetes 的特点 近年来 Docker 容器作为一种轻量级虚拟化技术革新了整个 IT 领域软件开发部署流程,如何高效自动管理容器和相关的计算、存储等资源,将容器技术真正落地上线,则需要一套强大容器编排服务,当前大红大紫的 Kubernetes 已经被公认为这个领域的领导者。Google 基于内部 Borg 十多年大规模集群管理经验在2014年亲自倾心打造了 Kubernetes 这个开源项目,欲倚之与 AWS 在云计算2.0时代一争高下,即便如此,Kubernetes 的定位主要是面向私有云市场,它最典型的部署模式是在 GCE 或 AWS 平台上基于云主机、云网络、云硬盘及负载均衡等技术给用户单独部署一整套 Kubernetes 容器管理集群,其本质上是卖的 IAAS 服务,Kubernetes 集群还是需要靠用户自己维护,大家知道 Kubernetes 虽然功能强大但使用、管理、运维门槛也高,出问题了大多数用户会束手无策。 这几年国内容器云领域也是群雄割据,但多数还是以私有云为主,提供公有云容器服务的却很少,主要是公有云要考虑的问题多、挑战大,虽然如此网易云的还是提供了公有云模式的容器服务。网易云从2015年开始做容器服务时也被 Kubernetes 强大的功能、插件化思想和背后强大的技术实力所吸引,至今已经跟随 Kubernetes 一起走过两年多,积累了不少经验。因为我们特别希望以公有云的方式提供一种更易用容器服务,任何对容器感兴趣的用户都能快速上手,为此也遇到了很多存私有云下不会出现的问题。 列举几个 Kubernetes 在公有云容器场景下的需要特别解决的关键问题。一是 Kubernetes 里没有用户(租户)的概念,只有一个很弱的命名空间来做逻辑隔离。二是 Kubernetes 和 Docker 的安全问题很突出,API 访问控制较弱且没有用户流控机制,一些资源全局可见。而 Docker 容器与宿主机共享内核的轻量级隔离从根本上没法做到彻底安全。三是 Kubernetes 集群所需要 IAAS 资源(如 Node、PV)都要预先准备足够,否则容器随时会创建失败,公有云这样的话会造成严重的资源浪费,产生巨大的成本问题。四是,Kubernetes 单个集群能支撑的节点总数有限,最大安全规模只有5千个 Node,公有云下扩展性将会有问题。 ### 网易云容器如何解决 Kubernetes 在公有云上的问题 先看下网易云容器服务的架构图(如图1),这里的 Kubernetes 处于底层 IAAS 服务和上层容器平台的中间,因为我们的容器服务不仅仅提供 Kubernetes 本身容器编排管理功能,更是为提供一整套专业的容器解决方案,还包括容器镜像服务,负载均衡服务,通过使用 DevOps 工具链高效管理微服务架构。考虑到 Kubernetes 概念较多、普通用户使用复杂,也为了便于整合其他配套服务,我们并没有直接暴露 Kubernetes 的 API 和所有概念给普通用户。 图1  网易云容器服务整体架构 #### 公有云租户概念 网易云容器服务基于 Kubernetes 已有的 Namespace 的逻辑隔离特性,虚拟出一个租户的概念,并与 Namespace 进行永久绑定:一个 Namespace 只能属于一个租户,一个租户则可以有多个 Namespace。这样 Kubernetes 里不同租户之间的 Pod、Service、Secret 就能自然分割,而且可以直接在原生的 Namespace/Resouce 级别的认证授权上进行租户级别的安全改造。 #### 多租户安全问题 关于 Kubernetes 的 API 的安全访问控制,尽管网易云容器没有直接暴露 Kubernetes 的 API 给用户,但用户容器所在的 Node 端也都要访问 API,Node 本质就是用户的资源。我们在最早基于 Kubernetes 1.0 开发的时候就专门增加了一套轻量级扩展授权控制插件:基于规则访问控制,比如配置各租户只能 Get 和 Watch 属于自己 Namespace 下的 Pod 资源,解决对 Kubernetes 资源 API 权限控制粒度不够精确且无法动态增减租户的问题。值得欣慰的是几个月前官方发布的1. 6新推出 RBAC(基于角色访问控制)功能,使得授权管理机制得以增强。但服务端对用户 Node 端访问的异常流量控制的缺乏依然是一个隐患,为此,我们也在 apiserver 端增加请求数来源分类统计和控制模块,避免有不良用户从容器里逃逸到 Node 上进行恶意攻击。 原生的 kube-proxy 提供的内部负载必须要 List&Watch 集群所有 Service 和 Endpoint,导致就算在多租户场景下 Service 和 Endpoint 也要全部暴露,同时导致 iptables 规则膨胀转发效率极低;为此我们对 kube-proxy 也做了优化改造:每个租户的 Node 上只会 List&Watch 自己的相关 Namespace 下资源即可,这样既解决了安全问题又优化性能,一箭双雕。 至于 Docker 的隔离不彻底的问题,我们则选择了最彻底的做法:在容器外加了一层用户看不见的虚拟机,通过 IAAS 层虚拟机的 OS 内核隔离保证容器的安全。   图2  网易云容器服务租户与kubernetes的命名空间关系 图2 网易云容器服务租户与 kubernetes 的命名空间关系 #### 容器的 IAAS 资源管理 容器云作为新一代的基础设施云服务,资源管理必然也是非常关键的。私有云场景下整个集群资源都属于企业自己,预留的所有资源都可以一起直接使用、释放、重用;而公有云多租户下的所有资源首先是要进行租户划分的,一旦加入 kubernetes 集群,Node、PV、Network 的属主租户便已确定不变,如果给每个租户都预留资源,海量租户累计起来就非常恐怖了,没法接受。当然可以让公有云用户在创建容器前,提前把所有需要的资源都准备好,但这样又会让用户用起来更复杂,与容器平台易用性的初衷不符,我们更希望能帮用户把精力花在业务本身。 于是我们需要改造 kubernetes,以支持按需动态申请、释放资源。既然要按需实时申请资源,那就先理下容器的创建流程,简单起见,我们直接创建 Pod 来说明这个过程,如图3所示。 图3  原生kubernetes创建容器流程 图3 原生 kubernetes 创建容器流程 Pod 创建出来后,首先控制器会检查是否有 PV(网易云容器为支持网络隔离还增加租户 Network 资源),PV 资源是否匹配,不匹配则等待。如果 Pod 不需要 PV 或者 PV 匹配后调度器才能正常调度 Pod,然后 scheduler 从集群所有 Ready 的 Node 列表找合适 Node 绑定到 Pod 上,没有则调度失败,并从1秒开始以2的指数倍回退(backoff)等待并重新加入调度队列,直到调度成功。最后在调度的 Node 的 kubelet 上拉镜像并把容器创建并运行起来。 通过分析上述流程可以发现,可以在控制器上匹配 PV 或 Network 时实时创建资源,然后在调度器因缺少Node而调度失败时再实时创建 Node(虚拟机 VM),再等下次失败 backoff 重新调度。但是仔细分析后会发现还有很多问题,首先是 IAAS 中间层提供的创建资源接口都是异步的,轮询等待效率会很多,而且 PV、Network、Node 都串行申请会非常慢,容器本来就是秒级启动,不能到云服务上就变成分钟级别;其次 Node 从创建 VM,初始化安装 Docker、kubelet、kube-proxy 到启动进程并注册到 Kubernetes 上时间漫长,调度器 backoff 重新调度多次也不一定就绪;最后,基于 Kubernetes 的修改要考虑少侵入,Kubernetes 社区极度活跃一直保持3个月发布一个大版本的节奏,要跟上社区发展可能需要不断升级线上版本。 图4  网易云改造后的kubernetes创建流程 图4 网易云改造后的 kubernetes 创建流程 最终,我们通过增加独立的 ResourceController,借助 watch 机制采用全异步非阻塞、全事件驱动模式。资源不足就发起资源异步申请,并接着处理后面流程,而资源一旦就绪立马触发再调度,申请 Node 时中间层也提前准备虚拟机资源池,并将 Node 初始化、安装步骤预先在虚拟机镜像中准备好。于是,我们详细的创建流程演变为图4所示。(注:最新 Kubernetes 已经通过 StorageClass 类型支持 PV dynamic provisioning)与原生的 Kubernetes 相比,我们增加了一个独立的 ResourceController 管理所有 IAAS 资源相关的事情,具体的 Pod 创建步骤如下: 1. 上层 client 请求 apiserver 创建一个 Pod; 2. ResourceController watch 到有新增 Pod,检查 PV 和 Network 是否已经创建;同时,另一边的 scheduler 也发现有新 Pod 尚未调度,也尝试对 Pod 进行调度; 3. 因为资源都没有提前准备,最初 ResourceController 检查时发现没有与 Pod 匹配的 PV 和 Network,会向 IAAS 中间层请求创建云盘和网络资源;scheduler 则也因为找不到可调度的 Node 也同时向 IAAS 中间层请求创建对应规格的 VM 资源(Node),这时 Pod 也不再重入调度队列,后面一切准备就绪才会重调度。 4. 因为 IAAS 中间层创建资源相对较慢,也只提供异步接口,待底层资源准备完毕,便立即通过 apiserver 注册 PV、Network、Node 资源; 5~6. ResourceController 当发现 PV 和 Network 都满足了,就将它们与 Pod 绑定;当发现 Pod 申请的 Node 注册上来,且 PV 和 Network 均绑定,会把 Pod 设置为 ResourceReady 就绪状态; 7. Scheduler 再次 watch 到 Pod 处于 ResourceReady 状态,则重新触发调度过程; 8. Pod 调度成功与新动态创建 Node 进行绑定; 9~10. 对应 Node 的 kubelet watch 到新调度的 Pod 还没有启动,则会先拉取镜像再启动容器。 #### 集群最大规模问题 从正式发布1.0版本至今最新的1.7,Kubernetes 共经历了2次大规模的性能优化,从1.0的200个 node 主要通过增加 apiserver cache 提升到1000个 node,再到1.6通过升级 etcdv3 和 json 改 protobuf 最终提升到 5000 node。但是官方称后续不会再考虑继续优化单集群规模了,已有的集群联邦功能又太过简陋。如果公有云场景下随着已有用户规模不断增大,一旦快接近集群最大规模时,就只能将其中一些大用户一批批迁移出去来腾空间给剩余用户。 于是我们自己在社区版本基础上又做了大量定制化的性能优化,目前单集群性能测试最大安全规模已经超过3万,验收测试包括集群高水位下,大并发创建速度 deployment 和快速重启 master 端服务和所有 node 端 kubelet 等在内的多种极端异常操作,保证创建时间均值<5s,99值<15s,集群中心管控服务最差在3分钟内快速恢复正常。 具体的优化措施包括: - scheduler 优化 根据租户之间资源完全隔离互补影响的特性,我们将原有的串行调度流程,改造为租户间完全并行的调度模式,再配合协程池来争夺可并行的调度任务。在调度算法上,还采用预先排除资源不足的 node、优化过滤函数顺序等策略进行局部优化。 - Controller 优化 熟悉 Kubernetes 的人都知道,Kubernetes 有个核心特点就是事件驱动,实时性很好,但是有个 Sync 事件却干扰了 FIFO 的顺序,我们通过将 Add、Update、Delete、Sync 事件排序并增加多优先级队列的方式解决这种异常干扰。 - apiserver 优化 apiserver 的核心是提供类似 CRUD 的 restful 接口,优化方向无外乎降低响应时间,减少 CPU、内存消耗以提高吞吐量,我们最主要的一个优化是增加以租户 ID 为过滤条件的查询索引,这样就能实现在租户内跨 Namespace 聚合查询的效果。另外 apiserver 的客户端原生的流控策略太暴力,客户端默认在流控被限制后会反复重试,进一步加剧 apiserver 的压力,我们增加了一种基于反馈的智能重试的策略抹平这种突发流量。 - Node 端优化 kube-proxy 本来需要控制整个集群负载转发的,Apiserver 有了租户查询索引后,我们就能只 watch 自己租户内的 Service/Endpoint,急剧缩小 iptables 规则数量,提高查找转发效率。而且我们还精简 kubelet 和 kube-proxy 内存占用和连接数。 ### 其他实践及总结 容器的网络是非常复杂一块,容器云服务至少要提供稳定、灵活、高效的跨主机网络,虽然开源网络实现很多,但是它们要么不支持多租户、要么性能不好,且直接拿没有经过大规模线上考验的开源软件问题总会很多。幸运的时网易云有自己专业的 IAAS 云网络团队,他们能提供专业级的 VPC 网络解决方案,天生就支持多租户、安全策略控制和高性能扩展,已经做到容器与虚拟主机的网络是完全互通且地位对等的。 网易云容器服务还在 Kubernetes 社区版本基础上结合产品需求新增了很多功能,包括支持特有的有状态容器,及 Node 故障时容器系统目录也能自动迁移以保持数据不变,多副本 Pod 可按 Node 的 AvailableZone 分布强制均衡调度(社区只尽力均衡)、容器垂直扩容、有状态容器动态挂卸载外网 IP 等。 相比容器的轻量级虚拟化,虚拟机虽然安全级别更高,但是在 cpu、磁盘、网络等方面都存在一定的性能损耗,而有些业务却又对性能要求非常高。针对这些特殊需求,最近我们也在开发基于 Kubernetes 的高性能裸机容器,绕过虚拟机将网络、存储等虚拟化技术直接对接到 Docker 容器里,在结合 SR-IOV 网络技术、网易高性能云盘 NBS(netease block storage)等技术将虚拟化的性能损耗降到最低。 最后,分享一些网易云容器服务上线近两年来的遇到的比较典型的坑。 - Apiserver 作为集群 hub 中心本身是无状态的可水平扩展,但是多 apiserver 读写会在 Apiserver 切换时可能会出现写入的数据不能立马读到的问题,原因是 etcd 的 raft 协议不是所有节点强一致写的。 - haproxy 连接的问题,多 Apiserver 前用 haproxy 做负载均衡,haproxy 很容易出现客户端端口不够用和连接数过多的问题,可以通过扩大端口范围、增加源 ip 地址等方式解决端口问题,通过增加 client/service 的心跳探活解决异常连接 GC 的问题。 - 用户覆盖更新已有 tag 的私有容器镜像问题,强烈建议大家不要覆盖已有 tag 的镜像,也不要使用 latest 这样模糊的镜像标签,否则 RS 多 Pod 副本或者同一个 Node 上同镜像容器很容易出现版本不一致的诡异问题。 - 有些容器小文件非常多,很容易把 inode 用光而磁盘空间却剩余很多的问题,建议把这种类型应用调度到 inode 配置多的 node 上,另外原生 kubelet 也存在不会检查 inode 过多触发镜像回收的问题。 - 有些 Pod 删除时销毁过慢的问题,Pod 支持 graceful 删除,但是如果容器镜像启动命令写得不好,可能会导致信号丢失不光没法 graceful 删除还会导致延迟 30s 的问题。 总之,在公有云场景下,用户来源广泛,使用习惯千变万化没法控制,我们已经碰到过很多纯私有云场景下很难出现的问题,如用户镜像跑起不来,Pod 多容器端口冲突,日志直接输出到标准输出,或者日志写太快没有切割,甚至把容器磁盘100%写满等,因为篇幅有限,所以只能挑选几个有代表性的专门说明。因为云上要考虑的问题太多,特别是这种基础设施服务类的,使用场景又非常灵活,线上出现的一些问题之前完全想不到,包括很多还是用户自己使用的问题,但为了要让用户有更好的体验,也只能尽力而为,优先选择一些通用的问题去解决。