容器生态是现在非常火热的技术生态之一,个人认为它主要囊括着四个方面的技术栈:一是容器核心技术栈(包括 Docker、rkt 及第三方公司自主研发的容器 Engine 等);二是容器基础技术栈(包括容器网络、存储、安全及服务发现等);三是容器编排技术栈(包括 Mesos/Marathon、Swarm、Kubernetes 及 OpenShift 等);四是容器应用技术栈(主要包括 CI/CD、监控、日志及微服务框架等)。而 Docker 和 Kubernetes 分别又是目前容器与容器编排界的小宠儿,所以我将带着小宠从这四方面分享一些容器云在万达的落地经验。
万达容器云平台支持快速部署、弹性伸缩、负载均衡、灰度发布、高效运维及微服务等特性。用户可以基于 Dashboard 简单方便地上线和管理着各自业务。目前在我们的容器云平台上,平稳高效运行着近 400 款包括支付、酒店等核心业务,管理着公司上万个容器。经历住了双旦、618 及双 11 等大型活动的考验。
一、容器云的平台高可用架构与部署
“经济基础决定上层建筑”,对于整个容器云平台来说,Kubernetes 平台就是我们的“经济基础”,所以在建设之初我们为了保证平台的稳定、高可用还是做了不少工作。先来看看 Kubernetes 平台的建设涉及到的几个技术点:
- 组件的高可用性如何保证?
- 组件以何种方式部署安装?
- 集群以何种方式快速扩缩容?
- 如何实现环境的批量配置及组件的批量升级?
为了较好的描述这些,我画了一张我们容器云中 Kubernetes 各组件高可用架构与部署图,请见:
Kubernetes 所有组件都容器化了,而不采用二进制安装的方式。这样对于组件的部署、配置更新和升级等都比较容易,只需要先安装好 Docker,提前做好镜像,然后直接 docker run –restart=always –name xxx_component xxx_component_image:xxx_version就可以了,这里 –restart=always 非常重要,用来保证主机重启后或由于突发状况引起的非错误性退出后组件服务自动恢复。
注意在升级过程中,为了减少组件服务的宕机时间,需要提前下载好新制作的镜像版本,因为如果镜像挺大的话,在 docker restart 进行更新前会耗费一些时间在 docker pull 上面。
在批量部署方面,我们采用 Ansible 工具。由于 Kubernetes 集群部署的文档网上蛮多的,所以这里就简要介绍一下组件的高可用部分。我们都知道 Kubernetes 实现了应用的高可用,而针对 Kubernetes 自身的 HA,主要还是在 Etcd 和 Kubernetes Master 组件上面。
1、 Etcd 的高可用性部署
Etcd 使用的是 V3(3.0.3)的版本,比 V2 版本性能强很多。Etcd 组件作为一个高可用强一致性的服务发现存储仓库,它的 HA 体现在两方面:一方面是 Etcd 集群自身需要以集群方式部署,以实现 Etcd 数据存储的冗余、备份与高可用;另一方面是 Etcd 存储的数据需要考虑使用可靠的存储设备。
为了展示一下组件容器化在部署方面带来的一些便利效果,以 Etcd 的部署为例,先在 Ansible inventory文件中规划好 Etcd 的分组信息,一般是采用 3 台做为集群即可。例如:
然后编写好类似如下的 template 文件,就可以用 Ansible Playbook 实现一键秒级的部署 Etcd 集群了,注意我这里使用了主机 IP 作为 Etcd 的名字。
如果需要升级则只需要修改一下 etcd_version 并提前拉取新版本镜像,重新跑一下 ansible-playbook –limit=etcds -i hosts/cd-prod k8s.yaml –tags etcd 即可完成秒级升级,这样一般显得简洁易用。当然 Etcd 在升级过程中还涉及到数据的迁移备份等,这个可以参考官方文档进行。
2、Kubernetes Master 的高可用性部署
Kubernetes 的版本我们更新的不频繁,目前使用的是 V1.6.6。Master 的三个组件是以 Static Pod 的形式启动并由 kubelet 进行监控和自动重启的,而 kubelet 自身也以容器的方式运行。
对于 kube-apiserver 的 HA,我们会配置一个 VIP,通过 haproxy 和 keepalived 来实现,其中 haproxy 用于负载均衡,而 keepalived 负责对 haproxy 做监控以保证它的高可用性。后面就可以通过 VIP:Port 来访问 apiserver。
对于 kube-controller-manager 和 kube-scheduler 的 HA,由于它们会修改集群的状态,所以对于这两个组件,高可用不仅要保证有多个实例,还需要保证在多个实例间实现 leader 的选举,因此在启动参数中需要分别设置 –leader-elect=true。
对于 Kubernetes 集群的扩缩容,只需要采用 Ansible 进行批量操作或者 kubectl 命令(一般需要用到 drain, cordon , uncordon 等)完成即可。另外我们还需要注意一下图中标注的各组件启动时依赖的参数配置,这些比较容易搞混。
二、容器云的技术架构介绍
目前使用的容器云架构方案如图所示,我将其分为四个部分,每个部分包含对应的容器技术栈,下面对于主流的技术介绍如下:
1、存储方案
后端存储主要采用 ceph 驱动,这里主要介绍它在有状态服务和 docker-registry 两方面的应用。
有状态是应用程序的高级需求,它们需要在 Pod 挂了特别是飘移到其他 Node 后,可以持续的访问后端存储。因此,Kubernetes 通过网络存储提供了丰富的 Persistent Volume 支持,比如:GCE 的 pd,AWS 的 ebs,还有 GlusterFS,Ceph 等先进的分布式文件系统。我们目前使用了 Ceph 的 rbd 来支持有状态服务。Kubernetes 集群环境中,由于所有的组件采用容器化部署,因此除了在主机上安装 ceph-common 之外,还需要在 kubelet、kube-controller-manager 容器中安装它,而且在启动时挂载如下三个 volume,其他的与二进制方式差不多:
具体 ceph rbd 配置,详见:Persistent Volume Provisioning 官网。
rbd 支持动态供应,支持单节点读写,多节点读,但不支持多节点写。如果有业务需要多节点写的话,rbd 就比较受限制。目前由于只有 GlusterFS 既允许动态供应,又支持单节点和多节点读写,所以我们也正在调研其相关使用。
docker-registry 做为容器的核心部分,起初我们采用 Swift 作为后端存储,为了提高 push/pull 的效率,采用 Redis 作为 Metadata 缓存,然后直接以容器的方式运行官方提供的镜像,比如:
具体的 config.yml 配置,详见:docker-registry 官方配置。但后来为了保证 docker-registry 的高可用,我们采用 Harbor 做 HA,并以 pod 的形式运行在 Kubernetes 集群上,镜像数据以及 Harbor-db全部通过 Ceph 的 PV 来挂载,这样就保证在 Harbor 主机挂了或者 Pod 故障后,Harbor 也可以 HA 了,同时我们也不需要额外维护 Swift 了。
另外注意一个问题,由于 PV,StorageClass 都局限于单个 Namespace 下,所以对于想通过 Namespace 来区分多租户使用动态存储的情况目前是不满足的。
2、网络方案
底层容器网络我们最初使用的是官方推荐的 Flannel,现在部分集群已经由 Flannel 切换成了 OVS 。 Flannel 可以很容易的实现 Pod 跨主机通信,但不能实现多租户隔离,也不能很好的限制 Pod 网络流量,所以我们网络同事开发了 K8S-OVS 组件来满足这些需求。它是一个使用 Open VSwitch 为 Kubernetes 提供 SDN 功能的组件。该组件基于 OpenShift SDN 的原理进行开发。由于 OpenShift 的 SDN 网络方案和 OpenShift 自身的代码耦合在一起,无法像 Flannel 和 Calico 等网络方案以插件的方式独立的为 Kubernetes 提供服务,所以开发了 K8S-OVS 插件,它拥有 OpenShift 优秀的 SDN 功能,又可以独立为 Kubernetes 提供服务。
K8S-OVS 支持单租户模式和多租户模式,主要实现了如下功能:
- 单租户模式直接使用 Openvswitch+Vxlan 将 Kubernetes 的 Pod 网络组成一个大二层,所有 Pod 可以互通。
- 多租户模式也使用 Open vSwitch+Vxlan 来组建 Kubernetes 的 Pod 网络,但是它可以基于 Kubernetes 中的 Namespace 来分配虚拟网络从而形成一个网络独立的租户,一个 Namespace 中的 Pod 无法访问其他 Namespace 中的 Pod 和 Service。
- 多租户模式下可以对一些 Namespace 进行设置,使这些 Namespace 中的 Pod 可以和其他所有 Namespace 中的 Pods 和 Services 进行互访。
- 多租户模式下可以合并某两个 Namespace 的虚拟网络,让他们的 Pods 和 Services 可以互访。
- 多租户模式下也可以将上面合并的 Namespace 虚拟网络进行分离。
- 单租户和多租户模式下都支持 Pod 的流量限制功能,这样可以保证同一台主机上的 Pod 相对公平的分享网卡带宽,而不会出现一个 Pod 因为流量过大占满了网卡导致其他 Pod 无法正常工作的情况。
- 单租户和多租户模式下都支持外联负载均衡。
下面举例解释一下:
合并是指两个不同租户的网络变成一个虚拟网络从而使这两个租户中的所有 Pod 和 Service 能够互通;分离是指针对合并的两个租户,如果用户希望这两个租户不再互通了则可以将他们进行分离;全网化是指有一些特殊的服务需要能够和其他所有的租户互通,那么通过将这种特殊的租户进行全网化操作就可以实现。
不同租户的网络隔离是通过为每个 Kubernetes 命名空间分配一个 VNI(VXLAN 中的概念)来实现的,在 VXLAN 中不同的 VNI 可以隔离不同的网络空间。K8S-OVS 将具体的 Kubernetes 命名空间和 VNI 的对应关系存储在 Etcd 中,如下:
这是在我们通过 Kubernetes 创建 Namespace 时,K8S-OVS 自动检测并为我们创建的。其中 NetName 是指租户的 Kubernetes 命名空间;NetID 是指为该租户分配的VNI;Action 是指可以对该租户网络进行的操作,它包括 join :合并, isolate :分离, global :全网化,其中 join 需要指定上面的第四个参数 Namespace,用于表示需要和哪个租户进行合并,其他两个操作则不需要设置 Namespace。
合并之后观察 helloworld1 和 helloworld2 ,发现两个租户的 NetID 变为相同的了。这样两个网络的 Pod 和 Service 就可以相互访问了。其他场景一样,通过 etcdctl update 来控制 Action 从而改变 NetID 来实现租户的隔离与互通。这里不过多演示。
在应用方面,我们实现了 LVS 的四层,Ingress(基于 Nginx+Ingress Controller) 的七层负载均衡,各 Kubernetes 集群环境中陆续弃用了 Kubernetes 自带的 kube-proxy 及 Service 方案。
3、CI/CD 方案
CI/CD(持续集成与部署)模块肩负着 DevOps 的重任,是开发与运维人员的桥梁,它实现了业务(应用)从代码到服务的自动上线,满足了开发过程中一键的持续集成与部署的需求。
我们采用了前端基于 Opads(一个比较成熟的在线打包平台)和后端 Pluto(一个将 Kubernetes APIServer 的接口进行封装,并提供 RestfulAPI 服务的库项目)的方案。目前通过该平台上线的业务有近 400 款。其架构图大致如下:
业务代码从公司内部的 GitLab/Gerrit 中拉取后,用户选择不同的分支版本(比如:sit/uat/prod)经过 Jenkins 构建后,生成相应的镜像并 Push 到相应的镜像仓库中,底层 CD 模块从相应的仓库上线到相应的 Kubernetes 集群(sit/uat/prod)中,而不需要 care 底层平台实现。
在整个 CI/CD 过程中,基础镜像的制作比较关键,我们按不同的业务分类提前制作不同的应用镜像(如:Tomcat、PHP、Java、NodeJS等)并打上所需的版本号,后面源代码既可以 mount 到相应的容器目录,也可以利用在 Dockerfile 的 ONBUILD 命令完成业务代码的加载。挂载的方式优点是比较灵活,也不需要重复构建,上线效率高;缺点是对于基础镜像环境的依赖较高,如果一个业务的基础镜像一直未改动,但代码中又有了对新组件库的调用或者依赖,则很容易失败。onbuild 的方式和 mount 方式恰好相反,它每次都进行 build,解决了环境的依赖,后面版本回滚也方便,缺点是需要每次进行镜像构建,效率低。这些看业务自己的选择。
我们提供的基础镜像版本在业务上线的时候,业务开发者可以提前搜索确定基础镜像是否存在,如果不存在需要提 Jira 单子后,交由我们进行制作,部分截图如下:
之后,业务可以通过选择代码分支、版本等上线服务。之后就可以看到上线的业务详情了,包括 Pod 副本个数,存活时间,名字,创建时间,所在的 Kubernetes 节点及 node selector 等。也可以通过基于 Gotty 实现的 Web console 登录查看业务容器,如图所示。
我们还实现了服务扩缩容,弹性伸缩(HPA)、负载均衡、灰度发布等,也加入了代码质量检查(Sonar)、自动化测试及性能测试插件等,这些都是 CI/CD PaaS 平台的重要组成部分。
4、容器监控与告警方案
容器监控的对象主要包括 Kubernetes 集群(各组件)、应用服务、Pod、容器及网络等。这些对象主要表现为以下三个方面:
- Kubernetes 集群自身健康状态监控(5 个基础组件、Docker、Etcd、Flannel/OVS 等)
- 系统性能的监控,比如:CPU、内存、磁盘、网络、filesystem 及 processes 等;
- 业务资源状态监控,主要包括:rc/rs/deployment、Pod、Service 等;
Kubernetes 组件相关的监控,我们写了相关的 shell 脚本,通过 crond 开启后监控各组件状态。
容器相关的监控,我们采用传统的 Heapster+InfluxDB+Grafana 方案:
Heapster 首先从 Kubernetes Master 获取集群中所有 Node 的信息,每个 Node 通过 kubelet 调用 cAdvisor API 来采集所有容器的数据信息(资源使用率和性能特征等)。这样既拿到了 Node 级别的资源使用状况信息,又拿到了容器级别的信息,它可以通过标签来分组这些信息,之后聚合所有监控数据,一起 sink到 Heapster 配置的后端存储中(Influxdb),通过 Grafana 来支持数据的可视化。所以需要为 Heapster 设置几个重要的启动参数,一个是 –source 用来指定 Master 的 URL 作为数据来源,一个是 –sink 用来指定使用的后端存储系统(InfluxDB),还有就是 –metric_resolution 来指定性能指标的精度,比如:30s 表示将过去 30 秒的数据进行聚合并存储。
这里说一下 Heapster 的后端存储,它有两个,一个是 metricSink,另一个是 influxdbSink。metricSink 是存放在本地内存中的 metrics 数据池,会默认创建,当集群比较大的时候,内存消耗会很大。Heapster API 获取到的数据都是从它那获取的。而 influxdbSink 接的是我们真正的数据存储后端,在新版本中支持多后端数据存储,比如可以指定多个不同的 influxDB 。
通过 Grafana, 用户可以使用各种正则表达式或者选项查看自己的业务监控详情,还可以按系统性能(CPU、内存、磁盘、网络、filesystem)进行排序查看等等。
当监控到一定的数量级,超过某个阈值时,将产生告警。虽然 Grafana 目前支持邮件进行一些简单的告警,但我们还是通过制定一些监控点、告警机制、告警等级等,然后接入公司内部现有 Zabbix 平台来进行告警。
邮件告警示例如下:
5、日志方案:
容器平台的日志系统一般包括:Kubernetes 组件的日志,资源的事件日志及容器所运行的应用的日志。所以一个好的日志系统至少需要 cover 到这几块。
日志这块一直是我们头痛的地方,之前我们主要关心容器中的业务日志,所以是直接将其日志对接到公司的统一日志平台(Hippo)中。他们需要我们采用 Flume 来进行日志采集,每个业务以 Pod 的方式运行 Flume ,在 Flume 中配置好 source, channel 和 sink 参数。source 用来指定业务挂载的日志目录,然后通过 channel 进行输送,最后 sink 到后端的 Hippo 所使用的 Kafka 中。业务需要查看日志,则需要登录到 Hippo 平台中。
这个方案有一些不如意的地方,比如:每个业务需要单独额外运行一个 Flume 的 Pod,浪费资源;业务需要登录到另一个系统查看,由于不是对接到我们的平台中,不方便;另外由于 Hippo 是针对公司的统一的日志平台,不是容器云专用的,经常会在高峰期响应慢,很难处理过来。所以我们决定设计一个全新的平台,采用的方案是 Kubernetes 官方推荐的(Fluentd+Kafka+ES+ 自定义界面),具体架构如下:
容器中输出的日志都会以 *-json.log 的命名方式保存在 /var/lib/docker/containers/ 中,系统日志都在 /var/log 中。Fluentd 以 Daemon Set 运行于所有的 Node 中进行数据采集,为了保证性能,引进了 Kafka 作为消息队列及日志转发,由于不想维护多个组件,中间转发不用 Logstash ,所以需要引入了两个 Fluentd 的插件 fluentd-kafka 及 fluentd-es,前者用于推送数据到 Kafka,后者用于将数据推到 ElasticSearch 中。最后实现一个 Log API Engine 用于供上层 Log GUI 调用。这个 Engine 封装了实时日志、历史日志下载和查询、应用日志占比、日志等级占比等 Restful API。下面是我们的部分截图:
三、容器云的填坑实践
下面挑几个坑说一下,并分享一下解决方法。
Docker 相关
早期由于混合使用了 Deployment 和 RC,此时如果使用了同名 Pod label 则会产生冲突,RC 会删除 Deployment 后创建的 Pod,造成 Pod 的反复创建与删除,最终导致 Node 上的 Docker daemon 挂僵掉。原因是 Docker device mapper 互锁而触发 Linux kernel bug(特别是有大量的 outbound traffic 时),解决方法是升级内核到 3.10.0-327.22.2,或者添加内核补丁。
Kubernetes 相关
业务应用在升级过程中,如果 Docker 删除出错, 偶偶会导致 device mapper busy,则会显示 Pod 一直在销毁,但其实它的 Deployment 已经被删除了,这种我们没有找到很好的处理方法,现有 workaround 是先重启 docker daemon,如果不能解决,再 reboot 主机。一般的做法是先 drain 掉所有的 pod,然后待重启解决后,再 uncordon 回来。
在使用容器的方式部署 kubelet 时,我们发现删除 Pod 时,在 apiserver log 中一直会出现 UnmountVolume TearDown secrect 资源失败的错误。其中是当时在挂载 /var/lib/kubelet 时采用了 rw 的方式,这个问题困扰我们很久了,解决方法是加上 shared 即 –volume=/var/lib/kubelet:/var/lib/kubelet:rw,shared。
存储相关
当某一 Pod 挂载 Ceph rbd 的 Volume 时,如果删除 Pod,再重新创建,由于 PVC 被 lock 导致无法挂载,会出现 volume 死锁问题。由于我们的 kubelet 是容器部署,而 ceph 组件是以挂载的方式开启的,所以猜测可能是由于 kubelet 容器部署引起,但后面改用二进制方式部署后也还是会出现 ceph lock。当时的版本是 V1.5.2,解决方法是升级到 V1.6.6 版本。
网络相关
在业务上线过程中,一定要进行规范化约束,比如,当时将 Flannel 升级到 K8S-OVS 网络升级过程中出现有些业务采用 Service 进行负载均衡的情况,这种依赖于 kube-dns,由于 Flannel 和 OVS 的网段不一样,Service 层又没有打通,导致以 Service 运行并通过 kube-dns 解析时会出问题,且网络不通,本可以采用新建一个以 OVS 同网段的 kube-dns 来完成不同网段的兼容,但最后面发现该业务是根本不使用 Service,而是直接利用了 Pod,这样非规范化上线的业务很容易导致升级相关的故障出现,猜测可能当时平台建设初期手动上过业务,其实这方面我们也可以加些监控。
网络不稳定的时候,偶偶发现业务访问突然就慢起来了,然后发现其 TIME_WAIT 会出现过高,这个是没有对网络内核进行优化处理,此时需要设置 net.ipv4.tcp_tw_recycle = 1 和 net.ipv4.tcp_tw_reuse = 1 ,前者表示开启 TCP 连接中 TIME-WAIT Sockets 的快速回收,后者表示允许将 TIME-WAIT Sockets 重新用于新的 TCP 连接。当然还有其他网络内核优化。
告警相关
CPU load 是通过每个核的 running queue(待运行进程队列)计算出来的,某些情况下 running queue 会变成 -1 也就是 4294967295。由于在 cpu 过载的时候,我们设置了告警,所以会被触发,但其实这时的 CPU 负载是正常的。此时,如果通过 sar -q ,top,w,uptime 等看到的 running queue 都是有问题后的值,只有用 vmstat 查看才是真实的。解决方法是重启 Node,因为这个值只有在重启后才会重置为零,或者升级内核补丁。
本文转自中文社区-基于Kubernetes的容器云在万达的落地