不完美的 K8S 与阿里的解决之道
技术
作者:莫源
译者:小君君
2018-08-08 14:39

Kubernetes 逐渐成为容器编排的标准,越来越多的实现方式和使用方式已经成为了标准化的流程,但是在应用容器化、DevOps、监控、性能调优、发布方式等方面都缺乏 Production ready 的实现,本文将会针对上述的场景,分别介绍阿里云在产品和开源领域进行的思考与实践。 

在学习和实施容器交付的时候,我们对于 Kubernetes 的认知和理解多少会存在一些偏差;在一些项目落地实施的时候,开发者经常会对 Kubernetes 本身没有包含的问题或者是没有解决的问题而感到束手无策。今天,我就和大家聊聊在 Kubernetes 中存在哪些问题或是说存在哪些缺失,以及如何解决这些问题。 

 微服务、容器与DevOps 

此图是 CNCF 的一个 Land Scape。从上到下分别是部署场景的一些开源组件、管理、运行时、环境初始化、云以及右侧的平台、监控和分析的部分。 

CNCF 社区包含了很多内容。CNCF(Cloud Native Computing Foundation)是关注云原生一些开源组件的社区。在此图中大部分都是开发者在做原生应用开发、原生架构、微服务或者容器时所用到的一些开源组件。但如果把时间退到三年至五年,这张图中会有 50% 的部分是空缺的。在这 5 年中有这么多的开源组件像雨后春笋一样涌出。那么是什么让原生社区变得如此兴盛呢? 

这离不开三件事情即微服务,容器和 DevOps。关于微服务和容器这两个部分在这里就不多做解释。我重点介绍一下 DevOps。DevOps 是 2007 年比利时的一个 IT 咨询师提出来的。当时他在给比利时的一个国企做一个大型数据中心迁移的项目。他白天支持开发人员做敏捷开发,晚上他接着支持运维团队把白天迭代出来的产品进行上线。 

那时开发和运维之间存在很大差异。比如白天与开发人员未沟通清楚 Issue,在晚上运维人员就不知如何操作,如此产生很多弊端,导致工作效率低下。但是,这个 IT 咨询师有很强的抽象能力,在他不断的实践思考之后,提出了 DevOps 这个理念。DevOps 讲的就是是否存在更好的组织架构、流程来节约交付时间,是否存在更自动化的方式提高交付速度,更好地验证交付质量。这就是 DevOps 的一个核心诉求,它关心的是时间和质量两个问题。 

但是从 DevOps 最开始产生到现在,它从未明确定义应该利用什么工具、方式以及流程。DevOps 一直都是一个概念,一种文化,这也是自 2007 年到现在 DevOps 才真正被大家认可,被更多企业接受的原因。因为它是一个无形的东西,它不像 Docker,可以实际进行容器化交付。 

什么是 DevOps? 

DevOps 不存在一个能够被大家借鉴和认可的范式一样的流程。在以前,大部分企业可能是采用一个非常大型的单体化架构设计,使用瀑布流的方式进行开发,这样会导致组织结构变得异常复杂。信息流在不同的部门之间流转很慢。因为这是一个很大型的单体应用,很难保证每一个 Feature 上线的质量。这就是 DevOps 难以在很短的时间落地,被大家认可的原因。 

为什么最近 DevOps 被人们认同且有很大的影响力? 

首先,这个可能要归功于微服务的这个概念。微服务这个概念是 2014 年提出来的。微服务简单来说是一种分而治之的思想。它把一个大型的单体系统切分成很多自包含的,有自己生命周期的子系统。每个子系统都有一个自己的服务边界,通过独立的负责人员,进行更快交付、更快迭代,产生更小代价来验证自己的质量。 

所以从这个角度来讲,微服务的提出,以及企业对微服务的实现,无形中帮助了 DevOps。当你去切分这样一个微服务的一个场景,你会发现你的每一个模块都会有独立的负责人。原本大兵团作战的场景,逐渐演进成向小团队快速迭代的方式。 

第二,当微服务以一个非常小的 Scope 进行连续迭代时,迭代的速度以及验证的速度都会很快,如此 DevOps 原本关注的时间部分就被很好的解决了。所以微服务从架构、流程和质量上解决了 DevOps 的一些缺陷。但微服务并不是万能的,当你把一个大型的单体系统切分成十个或二十个子系统时,对于微服务的管理实际上是一个很大的挑战。而且微服务是对于不同场景,不同模块,选择一个合适的框架或语言来构建的。 

但是这并不适用于所有情况。举个例子,在一个团队中,原本大家都是用 Java 去写  Code,每个成员都可以总结出很多公共的类库,大家可以一起统一代码风格。但如果这个事情变成了很多语言,不同的框架去完成这样一个大型系统时,你会发现很多公共的部分是不能使用的。这种公共部分在交付的时候就会变成一个没有标准的场景。发生这种情况应该如何解决?诸如此类情况就可以由 Docker 来解。

但是不论是 Java 的应用,还是 NodeJs 的应用,或是 Golang 的应用,最终都会变成一个 Docker image。你可以用同样的方式去生成一个中间构建的产物,而这个交付的产物可以用同样的命令进行运行。比如说不论你是 Java 应用,还是 NodeJs 应用,都可以用 Docker 去执行它。Docker 通过标准化的方式去协助提升微服务实现。  

这么好用的 Docker 是否也会存在一些问题? 

Docker 学习成本很高,以前开发者只需感知应用开发便可,现在还要感知 Dockerfile  如何写,如何编排模板,如何搭建上层的容器框架。在每一次交付时,都要生成一个新的 Docker image。 

这一部分的成本应该如何去解决?就是通过 DevOps 的一些开源自动化的流程,或是开源的系统来实现的。说到这,我们就能发现刚刚提出的三个概念,它们是统一的、优势互补的、敏捷交付的。所以这三个概念就是把整个原生社区炒得很热的一个根本原因。 

多视角解读 Kubernetes 

在容器社区里,以前并不是 Kubernetes 一家独大。从最开始的 Mesos 到后来的  Swarm,还有类似像 Rancher 等等各种各样的解决方案。其实 Kubernetes 相对前面这几个来说是一个后起之秀。

为什么 Kubernetes 逐渐被大家默认为是容器编排标准的制定者?  

从我自己的理解来看,Swarm 是这几个容器编排里最简单、最友好,开发者学习成本最低的。但 Kubernetes 它不仅仅定义了编排系统怎么做,还说明了开发者如何与社区进行结合。就比如在 Kubernetes 中,它并不关注容器网络如何做,而是关注开发者定义了一个什么样的标准,是否符合开发者的某个标准开源组件,是否可以与开发者的 Kubernetes 进行结合。 

在 Kubernetes 中可以使用 Flannel,可以使用 Calico,可以使用各种各样的网络方案。这些都符合 Kubernetes 定义的 CNI 标准。所以 Kubernetes 相比 Swarm,定义了一个标准、一个平台。而 Swarm 更多的是通过一个 lib 的方式去执行。 

从架构的视角看 Kubernetes 

这是 Kubernetes 里的一个架构图。如果让我去选择一个编排系统给我的团队作交付系统。我会关注这两件事情:

  • 第一,这个编排系统的架构是不是合理,之后将如何演进,技术成本有多大;
  • 第二,我的团队想去使用这个编排系统做交付,学习成本有多大?使用成本有多大?

 基于这两点,我们深入剖析一下 Kubernetes 这张架构图。 

从物理节点上看,Kubernetes 分为 Master 节点和 Worker 节点。在 Master 节点上面会部署 kube-apiserver,API Server 是 API 的一个提供者。etcd 是作为数据存储的部分。kube-scheduler 用来负责 Pod 调度的部分。cloud-contoller-manager 是和 IaaS层沟通的组件。最下面的 kube-contoller-manager,是对于很多资源对象的一个管理组件。右侧是每一个 Worker 节点运行的组件,包括 Kubelet 和 kube-proxy。Kubelet 用来执行一些具体任务、具体命令。而 kube-proxy 主要是解决一些网络问题。 

我们通过一个具体的 Pod 生命周期,来了解这些组件之间是如何进行通信以及协作运转。 

这是一个 Pod 的生命周期。举个例子,我在 Kubernetes 里边创建了一个 Pod,它的执行流程是什么?首先我下达一个 Pod 的 yaml,API Server 是整个 Kubernetes 的驱动机,接收到 Pod 的 yaml。

Kubernetes 的 API Server 是把一些相应的配置透传下去,然后提供一些接口,能够让别人获得这个数据。当你创造一个 Pod 时,API Server 只是把相应的一个文件透传下去,然后回写给 etcd。而 etcd 会去 watch API Server 的一个接口。 

当 Pod 有变化时,kube-scheduler 会计算整个 Kubernetes 集群里 Worker 节点的资源利用率,相应的调度会约束选择一个节点给这个 Pod。但它并不是直接以命令的方式执行创建 Pod,而是把相应的这个 Worker 节点的信息传到  yaml 上,在 yaml 上增加一条配置,指定这个 Pod 在那个节点上执行。接下来,真正去执行这个 Pod 创建的动作是 Kubelet 调用的 Docker。 

当 Kubelet 发现 Pod 配置的变化,并且分配节点的 Node 是自己,那么 Kubelet 会把这个 yaml 拿下来,调用 Docker 去真正执行这个 Pod 创建动作。当这个 Pod 创建有任何问题时,它会更新 yaml 的 Status 字段,然后再回写给 etcd。这就是一个 Pod 的生命周期。 

这种设计和传统的设计有什么区别?如果是 Swarm,它又会怎么做?Swarm 是命令式设计,Swarm Master 拿到一个 yaml 会直接把这个透传到底下的 Docker 再去创建。这是命令式的创建。而 Kubernetes 是基于状态机的创建。这是 Swarm 和 Kubernetes 在原理上一个比较大的差异。

 从设计的思路分析 Kubernetes 

第一,Kubernetes 是面向资源的一个简化模型,在 Kubernetes 里有很多抽象的概念。对不同的应用场景会有不同的抽象。而这种抽象方式,在 API Server 里,是一个面向 RESTful 的 API 实现。在 API Server 里,它通过一个 go-restful 的一个类库去实现。通过这个类库的学习可以帮你更好地理解 Kubernetes 的架构。

第二,是异步动作保证性能。刚才我们也看到了那张图,就是不同的组件之间是通过  API Server 之间的异步交互去实现的。它通过监听 API Server 的状态去实现这个动作。这个在底层的实现有一个很重要的组件叫 Informer。  

Informer 是 Kubernetes 里异步监听的一个对象。当我要监听 Pod 的变化时,我需要创建一个 Pod 的 Informer,Informer 不止提供给你监听的能力,还给你定期 resync 的能力。就比如说在分布式系统里有一个概念是“网络是不可靠的”。那在 Kubernetes 的设计里也考虑到这一点,就是任何一个时刻,你创建的一个 Pod 都有可能失败。失败之后你需要用什么方式来补偿呢?

答案就是,用 resync 的一个能力来补偿。它通过你平时的监听,定期进行全量拉取。所以这是  Informer 机制,就是它既提供监听的能力,也提供定期 resync 的能力。所有的组件,基本上都是通过这种 Informer 的机制去进行异步监听去实现的。  

比如说在 Swarm 里,可能会遇到这样一个问题。当我并发创建很多容器时,对 etcd 会有一个非常高频的并发写,会在这种分布式抢锁的场景下造成很多性能的损耗,但是这个在 Kubernetes 里面就可以被避免。在真正执行创建这个动作时,它可以选择频度,选择到底什么时候该创建,什么时候不该创建,是该排队,还是该并发。 

第三,Kubernetes 基于状态机去提供状态基线。所有的信息流都通过这种期望将实施和反馈的机制存储在 etcd 里。刚才我们说的 Pod 创建的那个例子。我首先提交了一个 Pod 的 yaml,然后系统组件会在这个 yaml 上面添加一些期望信息,比如在哪个节点运行等等。期望这个东西是可以 run 的,当真正执行这个 yaml 的角色时,就会更新一个状态,然后再去修改这个 yaml。 

第四,就是反馈机制保证状态。Informer 里边可以定义实现这个 resync 的 handler,通过这个 resync 处理中间态。这一部分就是大部分 Kubernetes 里的 controller-manager 来做的一个动作。举个例子,当我下发一个 Pod,这个时候我的集群里会出现很多节点不正常。我的这个 Pod 可能部署不成功,但过一段时间这个 Pod 又部署成功了,那是谁来帮我做的 recorrect 这个动作呢?是这些包含了 Informer 的 Controller 通过 resync 机制来实现的。 

第五,是组件松耦合可插拔。在 Kubernetes 里组件之间通信不是通过 API Server 来进行代理,就是这些类似 apiGroup 来进行解耦的。所以组件之间是没有强依赖关系的,而且类似像 Dashboard 这样的组件,还内置了一些像熔断器这样的机制,所以如果从架构的角度来讲,我觉得 Kubernetes 可以说是一个分布式系统教科书一样的设计。 

从交付的视角看 Kubernetes 

在交付上我们该如何去使用 Kubernetes?这个图是我从才云科技(Caicloud)的 Blog 上面扒下来的,我觉得这个图画的非常好,上面是 Kubernetes 在不同的应用维度进行的一个抽象。比如说有 Department,有 Statefulset,有 Daemonset 等等。通过这几种概念,把 Pod 进行包装,然后选择适合自己的应用抽象去交付应用。 

Kubernetes 落地与实施 

如果要部署一个简单的应用拓扑,那我可能需要的就是一个 Service,通过下面的一个Deployment 来完成。Kubernetes 基本上把它所有交付流程都变成了一个抽象的东西来进行代表。比如说接入层可以用 Service 去代表,部署可以用 Pod 或者 Deployment 代表。这样我就可以把整个的交付流程很快的自动化,并很好的进行验证。 

这张图可能是很多开发者在自己落地实施时一个完整的 CI/CD 流程图。从开发者代码提交,到源代码管理,然后在 CI Server 里面进行一些 unittests,或是一些其他测试,之后再通过 CI Server 编译构建形成镜像,推送镜像,最后再分发给不同的 Kubernetes集群进行部署。

 这样完整的一套实施方案会产生什么问题? 

 其实我们会发现,有很多内容其实并不在 Kubernetes 范围之内:

  • 容器前的场景,应该如何去做容器化和标准化,DevOps 的流程怎么实现?
  • 交付中怎么快速验证,多种发布模式怎么在 Kubernetes 里进行简单的实现。这个在 Kubernetes 里是没有涉及到的。
  • 发布后,怎么能像传统的应用监控一样来监控我的 Kubernetes 应用?
  • 运维时,我们出现了问题后该怎么去解决?当运维的人员拿到的 Kubernetes 集群出现问题时应该找谁去解决?怎么去排查?怎样去验证?

标准化开发的产生,主要就是因为不同的应用将会涉及到不同的框架,容器化会有各种不同的实践。开发团队也会需要有统一规约的测试流程。所以在引入容器之后,就会增加很多成本。 

 针对以上难点阿里云是如何解决的? 

那阿里云是怎么解决的呢?我们提供了一个开源工具叫 Derrick。每一个应用都是分层的。Derrick 把一个应用拆成三层,上边是 Application Layer,然后是 Runtime Layer,最下面是 System Layer。

容器其实也是一样,Derrick 也是基于这种概念去实现的。它是通过一种代码探测的方式来帮开发者进行应用容器化。比如说,NodeJs 的应用可以通过 Derrick 在本地一键生成 Dockerfile 或 Kubernetes  的 yaml文件。我们甚至可以生成类似 Jenkinsfile 来帮你实现快速的应用容器化,应用拓扑的管理,以及 CICD 的流程实现。  

在交付中的流程控制部分,因为 Kubernetes 会把很多的逻辑变成一个原子性的抽象。这样就会导致我们很多的操作需要在上层实现。Kubernetes 本身的 StatefulSet 支持一种分批发布的机制,但是这种分批发布其实和我们想象的发布是有很大差别的。 

因为传统的应用是要包含上层网络,应用载体以及存储。但在 Kubernetes 中,每一个发布的动作,都是基于一个实体来做的。它和接入层是没有关联的。那在阿里云是怎么做的呢? 

阿里云是基于 CRD 来实现的。首先,一个 CRD 是由 Service 和 statefulset 进行组合。你可以给这个 CRD 来设置一个分批发布的批次。

当第一批发布时,它会将流量完全上到你这个新的 Pod 分组上。这样可以进行快速的验证。当第一批验证完毕之后,你可以继续发第二批,发第三批,也就是 CRD 是将 Service 和 statefulset 进行整合的一种发布方式。我们利用组合的 Kubernetes 中的一些原子性的概念来实现发布流程的交付。  

 容器监控的难点 

监控的部分,在 Kubernetes 社区里边有 heapster,有 metrics-server,有 Prometheus,每一种方案都有它各自的问题。那对于一个容器的监控来讲,它的难点在什么地方? 

就是应用和容器的生命周期不一致,会造成监控数据和 Pod 缺乏一致性和连续性。比如 Deployment 的信息都丢失掉了,那我们设置报警时,对 Pod 设置报警是没有意义的。因为 Pod 重启或者升级的时候生命周期就终止了,但对于 Deployment 报警是有意义的。这将如何实现呢? 

我们将数据采集的链路和监控生命周期的链路进行拆分,数据采集的链路和原本的  Kubernetes 的链路是一致的,而我们在新增加的 alicloud-monitor-controller 里边做了一件事情,就是监听 Kubernetes  里面的实体抽象,利用各种各样的变化来重造我们的应用拓扑,通过转换成一个监控拓扑,来实现这个监控的一个拓扑和真正的 Pod 之间的一个映射关系。 

实现的最终效果就是,当你创建了一个 Deployment,就可以在监控里边看到一个 Deployment 的分组,在这个分组之下会包含相应的 Pod,这个 Pod 里的数据会与监控进行聚合,实现应用的监控告警。而且你所有的告警策略可以基于分组级别,进行告警,也可以基于 Pod 进行告警。

现场问答互动  

K8S 集群中节点级别的弹性伸缩如何实现? 

答:在 Kubernetes 社区里有一个组件叫 Autoscaler,它是用来做节点级别的弹性伸缩。在 Kubernetes 中弹性伸缩分为三种:

  • HPA,基于容器的水平的伸缩;
  • 资源的垂直伸缩,相当于一个 Pod 的动态资源调增的伸缩;
  • 弹性伸缩,是基于 Node 的开源组件 Autoscaler。

Kubernetes 本身与 IaaS 并不相关。但是 Kubernetes 里边有一个专门对接云平台的组件 Cloud Provider,它与 IaaS 层有密切的联系,所以用户可以通过 Autoscaler 组件去监听 Node 的负载变化,然后动态地来弹性伸缩这个集群节点的数目。

在 Kubernetes 里设置好用户需要的资源申请值和限制值。不论是 HPA,还是 Autoscaler,它都是基于调度来计算的。就比如 Autoscaler 和传统的弹性伸缩是有区别的。传统的伸缩是基于阈值水位去实现的。 

比如 CPU 到了 80%,就可以开始扩容。但是在 Kubernetes 里的伸缩是基于调度的。也就是用户把这个 request 设置好,当 Kubernetes 发现这个 request 值不能满足调度时,它会弹性伸缩这个节点。所以用户经常在 Kubernetes 里能看到资源利用率可能还没到达顶峰,但是对于 Kubernetes 来说,request 已经达到这个阈值了,而不能去调度,此时就会产生弹性节点。 

如何看待 Docker 商业化? 

答:首先 DockerCE 的版本可能是大家最熟悉的、用得最多的版本。这个版本在社区里会一直存在,所以大家不用担心在商业化之后这个版本就不存在的事情。Docker EE 版本和 EC 版本之间存在着一些不同。区别在于 Docker CE 版本是在社区主导下对一些通用场景做一些支持,而在 Docker EE 版本里,无论是对网络,还是对存储等等一些相关的场景,都做了一些额外的支持,其实这也是商业化能力的一个体现。
 

此外,Docker 企业版不止是 DockerEE 容器引擎,同时它也包含了自己的上层编排系统的解决方案 DDC 以及镜像仓库 DTR 的一些组件。所以 Docker 的企业版更像一个解决方案的场景输出,并不是来替换 DockerCE 这个版本。 

第二个是关于 Kubernetes 和 Docker 之间的关系。很多人会觉得 Docker 是目前唯一一个容器使用方案,但事实并非如此。在社区里还有类似的像 ContainerD 等等各种各样的容器实现。Kubernetes 对 Docker 的依赖,其实是对一种容器实现的适配。而 Docker 是 ContainerD 的一个实现。所以如果大家不想使用 Docker 了,就可以换 ContainerD 来实现,上层是不用做任何变化的。 

 莫源/阿里云技术专家

阿里云高级研发工程师。在加入阿里巴巴之前,先后在北京天方地圆科技有限公司、微软亚洲研究院任职。现主要负责阿里云容器服务产品的底层服务发现系统、集群管理系统的研发,从事容器的持续交付、持续集成的方案的设计与实现。在云计算、分布式系统、图像识别与虚拟现实方向有多年的开发经验。  

257 comCount 0