本文通过围绕微博业务需求和大家分享如何使用 Kubernetes 来解决具体的业务问题和相应的解决方案。
文中分享了基于 Kubernetes 的 PaaS 层弹性混合部署方案,其中有 Kubernetes 的优点,也有部分 Kubernetes 在企业落地的缺陷,希望本文对大家有一定的借鉴意义。
微博容器平台简介
2016 年微博平台实现基于混合云的弹性平台 DCP,提升了 Feed、手机微博、广告、搜索、话题、视频、直播等多个核心业务热点应对能力。
2017 年微博平台率先探索基于 Kubernetes 的 PaaS 层弹性混合部署解决方案,并且积极的和社区保持同步。
2018 年实现 CI/CD 与生产环境混合部署,2019 年春晚实现部分核心业务混合部署与弹性伸缩。
本文主要介绍微博平台落地 Kubernetes 过程中的一些经验教训。
为什么选择 Kubernetes
因为历史原因,微博 Docker 容器治理是采用独占物理机(虚拟机)模式,直接使用物理机的网络协议栈,服务治理采用服务池方式。
随着设备计算能力的提升,这种治理方式有几个问题急需解决:
- 利用率问题:一个服务池内新、老设备共存,因为业务容器需要兼容老设备规格,导致服务无法充分发挥出新设备应有的计算能力。
- 容器网络问题:因为直接采用物理机网络栈,导致业务混合部署只能采用调整业务监听端口的方式,造成接入成本、管理成本高。
- 调度治理问题:因为默认采用独占策略,服务池之间资源相互隔离,不同类型的业务类型无法共享资源,导致资源总是相对紧缺。
Kubernetes 提供标准化的容器管理,CNI 网络虚拟化,自定义弹性调度策略,能够很好的解决上述问题。
但是 Kubernetes 面向公有 PaaS 的实现方案在内网环境下有些方面不适用,主要有如下几个方面:
- 网络虚拟化:在 BGP 的虚拟网络方案和隧道的虚拟网络方案中都会引入 iptables 来完成流量牵引,和防火墙的相关功能。
在企业内网使用的过程中并没有很强烈的防火墙需求,引入 iptables 往往会造成性能下降(nf_conntrack 表被打满,NAT 性能太低)。
所以微博平台现阶段没有使用 BGP 虚拟化网络方案和隧道虚拟化网络方案。
- 滚动发布:目前的 Kubernetes 的滚动发布(Deployment)不支持 In-place rolling updates ,每次一个 Pod 都有可能被分配不同的 IP 地址。
在企业内部使用容器的时候,固定 IP 的需求往往很强烈,所以我们抛弃了 Kubernetes 而选择了整合了公司内部的滚动发布系统。
- 资源隔离:原生的内存隔离策略中不支持 Swap 的限制,容器会占用物理机的 Swap,我们修改了内存隔离限制。
- 负载均衡:原生的 Service 模式会引入 iptables 来做 NAT,同时 Service 的负载是硬负载,没法调整流量权重。
我们基于 Kubernetes 搭建了一套 PaaS 平台,对 Kubernetes 进行了改进,提供了以下功能:
- 网络虚拟化:基于 CNI,提供了隔离内网和公有云网络差异的虚拟化网络方案。
- 调度管理:基于 kube-scheduler,提供了锁定 IP 的调度系统,该系统支持带宽初筛,硬盘初筛,机房就近调度,返回库存状态,提前锁定 IP 功能等功能。
- CI/CD:一键发布代码,替代 Kubernetes 的 Deployment 进行滚动发布。
- 资源隔离:在原有的隔离策略上,扩展出计算资源隔离,网络资源隔离,存储资源隔离。
- 负载均衡:整合已有的调度系统,利用微服务快速部署+弹性调度提前锁定 IP,减少服务抖动耗时。
- 模块化运维:把已有的物理机运维工具整合到容器中,在 Pod 里面共享存储,共享网络。
- 弹性扩缩容:通过对 DCP 的整合,使其具有了容器弹性扩缩容的功能。
- 监控:通过模块化的运维体系,整合了监控所需日志,无缝连接已有功能。
图一
整体方案如图一,微博容器平台划分出如下几层:
- 服务层:平台的主要入口提供容器扩缩容、上下线、维护服务池、负载均衡,监控管理等功能。
- PaaS 层:提供容器管理和调度管理等相关功能,负责将服务层的请求转化成对应容器的操作。
- IaaS 层:提高机器资源、网络资源、存储资源供 PaaS 生成的容器使用,负责对容器使用资源进行管理。
容器弹性化扩缩容平台建设
微博容器弹性扩缩容平台,是在 Kubernetes 基础上进行了改进,充分利用了微博平台已有的资源,避免重复造轮子。具体的工作如下:
基础建设之网络虚拟化
之前已经说过了,微博的容器会独占物理机的网络协议栈,虽然能够做到网络效率的最大化,但是会导致多容器部署时出现端口冲突。
为了解决端口冲突需要使用虚拟化网络技术提供容器独立的 IP 地址。多个容器独立 IP 需要解决以下的三个问题:
- 容器的 IP 地址分配问题。
- 容器的 IP 路由问题。
- 虚拟化网络对网络的性能损失要最小化。
第一个问题因为采用 Kubernetes IP 分配都是通过记录在 etcd 中,所以不会出现分配重复或者分配错误的问题,而第二个问题社区里面通常会采用隧道类型方案和 BGP 方案。
以下是隧道模式和 BGP 模式的优缺点对比如表一, 性能测试如表二(BGP 主要工作是路由交换,转发等不受影响,等同于物理机性能。)
表一
表二
在测试结果中显示 vxlan 因为需要封装和解封隧道导致带宽损耗过 5%,所以隧道方案不适用于内网的网络环境。
而 BGP 的方案 Calico 会引入 Iptables 去做 ACL,不仅在业务峰值流量的情况下会触发 nf_conntrack 表被打满丢包的风险。
而且 BGP 方案在公有云和内网落地的时候也存在问题:
- 公有云方面:从公有云虚拟机发出的报文必须是 Mac 地址和 IP 地址匹配的,所以导致在公有云机器上 BGP 虚拟网络的容器根本无法通信。
- 内网方面:内网机器的上联交换机上做了 Vlan 和 IP 的绑定,如果在内网机器上起了一个其他网段的 IP 地址,报文发送不出本机。
接下来先来看看在网络方案上我们做的一些工作,见图二:
图二
微博虚拟网络主要是四方面内容:
- 对机房网络进行改造,修改机器的上联交换机为 trunk 模式,支持多 Vlantag 的网络通信。
- 在物理机层面通过创建网卡子接口(如图一左侧),通过对网卡子接口做 MacVlan 虚拟网卡插入 Kubernetes 的 Pause 容器中,把容器网络与物理网络打通。
- 公有云方面通过创建弹性网卡,让一个机器上有多个网卡,且每块网卡带独立 IP 地址,然后对新加的网卡做 host-device,将网卡的所属 network namespace 修改为 Kubernetes 的 Pause 容器,把容器和物理网络打通。
- 对 CNI 插件进行修改,能够给容器分配指定 IP 地址 。
图二左侧是简化后的内网网络拓扑,容器的虚拟网卡通过 MacVlan 与物理网卡的网卡子接口相连,发出的报文会带上网卡子接口的 Vlantag。
而这部分的流量上到上联交换机之后就和物理机发出的没有任何区别,之后的都是交换机和网关去解决掉路由的问题。
这个方案的设计对现有的环境依赖最小,同时改动量少。实现机房物理网络与容器网络的扁平化,解决了容器网络和物理网络互联互通的问题。
由于没有隧道解封性能问题,性能基本上持平物理机性能。本质上这是一个私有云的网络解决方案,但是很好的解决了问题。
图二右侧是简化后的公有云网络拓扑,通过把物理机上的网卡迁移到容器里面来间接的实现多 IP。由于是虚拟机的弹性网卡,等同于虚拟机上的物理网卡,性能没有问题。
虚拟网络后续的演近:对 Calico 进行改进取消 Iptables 依赖。利用 Calico 去解决内网网络方案中 IP 浪费的问题。同时可以对 Calico 做进一步的研究,如动态迁移容器如何保持 IP 漂移。
基础建设之调度管理
容器调度,其实是为了提高资源利用率,同时减少资源碎片化。
Kubernetes 的调度策略做的相对灵活,对 Pod 的调度通过三个阶段来实现,初筛阶段用于筛选出符合基本要求的物理机节点,优选阶段用于得到在初筛的节点里面根据策略来完成选择最优节点。
在优选完毕之后,还有一个绑定过程,用于把 Pod 和物理机进行绑定,锁定机器上的资源。这三步完成之后,位于节点上的 kubelet 才能开始真正的创建 Pod。
在实际的接入过程中,Kubernetes 的基础调度策略不能满足平台的业务需求,主要有如下两点:
- 因为没有规格的概念,所以无法给出库存状态。
- 初筛的纬度太少,目前只支持 CPU,内存的初筛,优选不支持同机房就近调度。
整体方案
图三
整体的调度管理分成如下几层:
- 接口层:用于接收请求返回特定规格,特定调度需求下的库存数量,同时返回锁定好的虚拟 IP 地址。也用于接收请求释放虚拟 IP 地址。
- 初筛层:对缓存中的节点信息进行初筛,返回能部署规格的全部物理机节点信息。
- 优选层:根据优选结果,模拟部署 Pod,统计整体库存。
- 部署策略层:按照部署策略挑选物理机,并且锁定物理机上的虚拟 IP 地址,返回库存信息。
调度管理之接口层
锁定库存接口层的逻辑:
- 把请求参数转化为 Kubernetes 的 Pod 对象 -> v1.Pod。
- 把 Scheduler 的 Cache 进行一次深拷贝,后续的动作都会在这个深拷贝的 Cache 中完成。
- 请求监控返回物理机的实时硬盘信息,实时带宽信息。整合到深拷贝 Cache 中。
- 连同请求参数都传递给初筛层。
释放库存接口层的逻辑:
- 调用 Kubernetes 接口,把物理机节点上虚拟 IP 的 label 改成 unusing。
调度管理
图四
初筛层:对上述的 Cache 部分里面的 Node 节点进行 CPU,内存,硬盘和带宽的初步筛选,返回通过筛选的所有物理机节点。
优选层:对上述的物理机节点信息进行打分,对节点进行排序。然后根据请求所需部署的容器数量,按照物理机节点的进行模拟部署(挑选物理机按照分数从高到低排列),直到全部节点上可部署容器数量为 0,统计每个节点能部署的容器个数。
部署策略层:根据请求参数的不同(目前只支持集中/平铺),锁定物理机上的 IP 地址(调用 Kubernetes 的 API 把物理机上虚拟 IP 的 Label 置为 Using 状态),并且返回这些 IP 地址。
整体流程图
图五
后续演进:支持调度优先级,且能根据实际的资源使用情况进行调度而不是 Kubernetes 预分配的资源进行调度。
基础建设之资源隔离
Kubernetes 支持 CPU、内存的隔离。在宿主机层面支持驱逐模式。通过虚拟化网络的方案,主容器网络和混部容器的网络分割开来。整体的资源隔离目标是为了隔离开主容器和混部容器对资源的使用。
以下是我们做的一些改进:
- 计算资源隔离:K8S 提供了内存的限制能力,是通过 OOM 来限制内存的使用。在此基础上,我们还增加了限制容器使用物理机的 Swap。
- 存储资源隔离:K8S 没有提供基于物理机的存储资源配额方案,但是提供了相关的框架接口。在此基础上,开发了有配额大小限制的物理机静态存储解决方案。
- 网络资源隔离:针对容器的网络限速方案,已经在内部测试通过,同时帮助社区完善了相关的限速代码。
后续的演进:资源隔离的后续方向很多,首先要解决的是 Kubernetes 如何能动态设置资源阈值。其后需要能够设置内存 OOM 优先级,以及满足资源超卖的需求。
基础建设之 CI/CD
平台于 2018 年基于 Gitlab 开发了 CI/CD,通过 CI/CD 和 Kubernetes 的配合来完成从代码提交到上线的完整流程。
其中使用 Kubernetes 的上线流程(如 Deployment)的滚动发布存在着容器 IP 不固定,动态化的问题是因为 Kubernetes 的设计原则中对集群的管理尤其是服务升级过程中需要保持“无损”升级(升级过程中提供服务的副本数一直符合预期)。
如果对一个 Deployment 进行滚动升级,那么这个 Deployment 里面的 IP 地址和滚动升级之前的 IP 地址是不会相同的。
而如果集群够大,一次滚动发布就会导致负载均衡变更 (集群副本数/滚动发布步长)次。
对于微博服务来说,频繁变更会导致这个负载均衡辖下,所以后端实例的接口不稳定。
而平台内部的之前的上线系统是根据业务冗余度及业务实际需要来调整上线的步长,减少在上线过程中业务的抖动,也就是通常说的 In-place rolling updates。保证在上线过程中容器的 IP 不变。
整体流程的核心思路为:
- 切断容器的流量
- 进行流量检测,确保已经没有线上流量
- 清理旧容器
- 部署新容器
- 检测服务是否正常启动(端口检测,接口验证)
- 接收线上流量,提供服务
针对该问题,容器平台没有使用 Kubernetes 的原生滚动发布而是做了以下几个改进和适配:
- 首先不使用 DP,RC 的概念来完成滚动发布,只使用 Pod 的概念。
- 集成已有的上线系统,来完成滚动发布,回滚功能。
- 流量引入/流量拒绝 利用 Kubernetes 容器生命周期管理的 lifecycle(修改了其中的 postStar 的原生实现,因为原生里面只调用一次,不管成功与否都会杀掉容器。改进成了如果不成功会按照指定的次数或时间进行重试),服务检查利用 liveness probe、readiness probe 来完成。
主要流程有:
- 提前给每个机器上划分虚拟 IP 段,并给机器打上虚拟 IP 的 Label。
- 在原有的上线系统中增加对 Kubernetes 管理容器的上线流程,上线过程中通过服务池中已有 IP 反查 Pod Name,然后删掉旧 Pod,然后用新的镜像 tag 生成 Pod 的 json 字符串(其中 nodeSelect=${IP}),然后提交 Kubernetes 去创建新 tag 版本的 Pod。
- Kubelet 接到创建请求之后,提取其中的 IP 地址发给 CNI,创建指定 IP 的新 tag 版本 Pod。
上线回滚的流程变成了删除 Pod,创建 Pod 的固定化操作,见图六:
图六
由于给机器打好了虚拟 IP 标签,所以 Pod 的创建会被分配给固定的物理机去执行,配合修改之后的 CNI 就能创建指定新 tag +固定 IP 的 Pod 来提供服务。滚动发布和回滚变成了删除 Pod,创建 Pod 的固定化操作。
基础建设之模块化运维
由于之前的容器是独占物理机的模式,所以对于容器的运维也是在物理机上进行的,一些功能如日志处理、域名解析、时钟同步、运维 Agent 管理以及定时任务等都是在物理机层面操作。
如果开始多容器混合部署,以上的功能都兼容改动工作量大。再加上平台面向的业务方众多,需求各不相同。
例如日志推送的方式已经出现 scribe、flume、filebeat 等不同的方式,业务运维需要根据自身去定制运维容器,由此可见模块化运维的必要性。
图七
我们基于 Kubernetes 的 Pod 概念做了如下的工作,整体架构见图七:
- 单独做了运维容器,把运维相关的工具集成在容器里面。
- 运维容器和业务容器共享网络协议栈,共享日志存储。
- 在容器里面共享存储是带配额的静态存储。
模块化运维之定时任务
物理机上的定时任务依赖于 Crontab,而 Crontab 会由 Systemd 来启动。
在容器中使用 Systemd 启动会涉及到提权问题,在实践过程中发现用 Systemd 如果权限控制不当会造成容器被 Kill 的情况。
所以单独开发了兼容 Linux Crontab 语法的定时任务工具 -gorun,把这个工具集成在了运维容器里面,替代了 Crontab 来完成定时任务。
模块化运维之日志处理
图八
日志的处理主要包括监控采集、日志推送、日志查询、日志压缩清理四方面:
- 日志推送:通过 scribe,flume 等方式接收业务日志,推送给信息系统部等数据处理部门,如图八。
- 日志查询:容器产生的日志需要能够静态存储三天左右,方便故障定位。所以日志会存储基于 Kubernetes 的 PVC 概念开发的本地带配额的静态存储里面。
- 日志压缩清理:磁盘空间有限,打印的日志需要定期的清理和压缩。
- 监控采集:通过监听文件变化或者监听端口来采集需要的监控数据。
通过上述手段,能够利用现有的日志体系,同时开发的工作量最小,通过这样的操作,以后的容器想要接入,只需要在 Pod 的配置文件里面多写一个 Container 的配置即可。
后续的演进:后续的运维容器将会进一步的拆分,做成标准化的服务,例如域名解析容器,日志推送容器。让业务的接入变得更加的容易。
基础建设之弹性扩缩容
弹性伸缩在微博的应用很广,作为支持了全公司春晚扩容的 DCP 系统其主要能力就是进行 IaaS 层虚拟机的弹性伸缩。它是对业务进行保障的重要手段之一。
弹性扩缩容保证在峰值流量来临时通过扩容提高接口的可用性,在业务量级下降后回收资源节省了成本,而 PaaS 层的扩缩容比 IaaS 层的扩缩容更具有优势。
一来是因为 PaaS 的启动更轻,没有虚拟机的初始化阶段,所以启动更快;二来是因为我们的弹性调度系统能提前锁定 IP,可以做到业务容器和变更 Nginx 同步进行。
所以在 PaaS 层的弹性扩缩容,我们目前做到了如下几点工作:
- 定时扩缩容实现
- 自动化扩缩容的实现
定时扩缩容是复用了 DCP 系统的定时扩缩容功能,并做了一定的适配,目前可以选择使用原生扩容模式(IaaS 层)和 Kubernetes 扩容模式。
选择好了模式之后需要填的就是 Pod 的调度参数和本身 Pod 的标准化 Json 文件,之后就是常规功能。
自动化扩缩容的实现,是基于 CI/CD 系统中的放量系统完成的,CI/CD 会在上线过程中有单机放量的步骤,其中会采集放量容器的监控数据,业务数据和日志数据。
横向对比当前服务池的整体接口耗时和纵向对比历史七天内单机的接口耗时。
通过这套系统,把相关的阈值指标导出之后,集成到监控系统中,如果判断接口的 QPS 和耗时产生了恶化,会相应的触发扩容操作,不同于 IaaS 层的扩容,PaaS 的扩容在存量机器上是免费的。
例如前端某接口的 QPS 过高,处理不过来了可以在机器学习服务池的机器上进行容器扩缩容去先扛住量。
基础建设之负载均衡
微博平台的 7 层负载均衡方案是使用 Nginx。目前仍使用的是静态文件的进行路由信息管理。
Nginx 变更系统实现了一套动态的解决方案,将 upstream 与对应的服务池进行关联,确保对应的接口由对应的服务池来提供服务,当服务池内有节点变更时,自动触发配置下发进行 Nginx 的变更。
Kubernetes 从最开始的 Service 上就开始了负载均衡和服务发现的尝试,例如在 Service 上利用 Iptables 进行请求的随机负载(问题在于后端某个实例的压力已经很高了,由于是 Iptables 的硬负载不能感知到,依然把请求传递过来)。
而且和网络虚拟化一样引入 Iptables 会有 nf_conntrack 打满的风险,考虑到公司内部已经有成熟的负载均衡系统,这块进行了一个适配。
由于弹性调度的功能,我们会在创建 Pod 之前就可以锁定 IP 地址。当 IP 地址锁定后,我们可以同步的变更我们的负载均衡系统+启动我们的业务容器,能够更快的响应业务请求,见图九:
图九
基础建设之监控系统
监控和日志都是平台内部成熟的组件,通过模块化运维中日志信息的采集推送,监控数据采集推送已经兼容原有方式推送到监控平台。而物理机监控已经通过运维物理机部署 Agent 来采集,不需要重新引入别的工具去完成。
整体的监控总共有如下的几个部分:
①物理机信息监控:物理机 CPU、内存、IOPS、带宽、负载、网络等基础监控信息。
②业务容器的业务监控:包括接口 QPS、耗时、返回状态码占比、err/warn 日志记数、链接资源耗时等,见图十:
图十
③容器使用物理资源监控:CPU、内存、IOPS、带宽、负载、网络等基础监控信息,由于已有一套监控体系,修改了的实现把原先需要集中处理的部分落在了计算节点本地,然后通过已有的物理机监控数据推送到远端监控上。
一来避免了之前 Heapster 结构中的单点问题;二来可以复用现有的日志推送架构,见图十一:
图十一
④Kubernetes 组件监控:包括单机的 Kubelet 接口耗时、成功率监控、日志中 err、warn 监控,同样包括 Master 节点的同类数据监控。
基础平台的建设不限于上述的部分,还做了鉴权,DNS 优化等相关服务。限于篇幅不便展开,于此同时微博平台 Kubernetes 也在积极的与社区保持迭代,争取做到能回馈社区。
春晚红包飞业务落地
2019 年春晚期间,后端服务支持红包飞业务,如果按照传统方式需要近百台的公有云设备按需成本。
我们通过把垂直业务从大容器中抽离形成微服务,利用 Kubernetes PaaS 整合能力,在未增加资源成本的情况下完成春晚红包飞保障。
目前有 6 大服务池接入 Kubernetes PaaS,共管理数千核 CPU,数 TB 内存的计算资源。
通过 Kubernetes 整合闲散资源与合理的混合部署,可提供近整体的 30% 的弹性伸缩能力。
综上,是微博平台在探索 Kubernetes 与业务更好结合道路上的一些体会,实际业务接入过程中依然会有很多的挑战。
但是在团队成员的努力下,最终实现了方案的落地并保障了春晚的线上业务的稳定。
通过这次的平台建设充分体会到只有和业务结合,并且服务好业务才能更好的促进自身架构的成长,很多时候看似很完美的方案,面对真实的业务需求还是会出现纰漏。
我们会吸取历史的教训,总结经验。争取利用先进的技术来为公司创造更大的价值。
展望未来
未来我们将长期运行混合部署的微服务架构,优化调度系统,对接更多的 IaasS 层提供商。
更进一步提高接口可用性和资源利用率,也会对服务的稳定性和资源的利用率做进一步探索,利用技术提升研发效率也是我们后续的方向。
在探索的同时我们也会对 ServiceMesh、Serverless 持续关注,结合着业务需求,更进一步优化容器基础平台。
作者:彭涛、王琨
简介:彭涛,主要负责微博容器平台的开发工作。Kubernetes 代码贡献者。曾就职于百度基础架构部,百度公有云事业部。长期从事云计算行业。熟悉网络虚拟化,SDN,OpenStack,Kubernetes。致力于推动 Kubernetes 在微博的应用落地,构建高效的 PaaS 平台。
王琨,微博平台高级产品运维工程师,主要负责微博 Feed、用户关系、架构业务的运维支撑及改造工作。擅长大规模分布式系统集群的管理与运维,疑难问题分析,故障定位与处理。致力于推进运维自动化,构建微博平台高效的运维平台。