原本打算写一个 Kubernetes 系列,标题就是这个,但后来技术方向改变,没有坚持下去。抽空整理了下,把原来写的序言和部分章节的摘要发出来。

『架构』这个词不同的人有不同的理解,有人觉得『架构』就好像一种工程师努力追寻的武学秘籍,也有人觉得『架构』就是画 PPT 或者在大会吹牛。确实不像学习编程,可以有许多教程可用,没有教程类的书教人如何做架构。脱离具体的业务场景和系统,谈架构总是让人感觉很虚,所以一直想找个系统作为依托,写写自己对架构的思考。

去年录制了一套 《Kubernetes 完全教程》,主要介绍了 Kubernetes 本身的特性以及使用,到最后发现只能让大家做到”知其然”,做不到”知其所以然”。正好经过几年的 Kubernetes 使用与研发经历,我个人也积累了许多问题,有的自己想明白了,但有的还在思考中。于是想到写一个高级版的 Kubernetes 架构系列,通过解析 Kubernetes 的架构,同时比较业界对同一个问题的不同解决方案,和大家一起探讨为什么 Kubernetes 选择了当前的方案,经过什么样的讨论,它背后的架构思想是什么。一方面可以深入理解和学习 Kubernetes,做到”知其所以然”,另外一方面可以学习它的架构思想,在以后自己研发的系统中应用。

基础研发领域,Kubernetes 的架构有非常多的借鉴之处。一方面它的许多设计是 Google 内部多年积累的最佳实践的沉淀,是 Borg,Omega 演进的结果。另外一方面,它是一个综合的系统工程,不是一个单纯的技术工程。系统工程的特点是任何一部分单独抽取出来,貌似都没有太高的技术含量,但它的复杂度在于如何让各个子系统优雅的结合在一起,对外表现出统一的设计思想,而不是堆砌和拼凑,如何设计对用户更友好,如何设计更利于扩展。这也意味着,它的许多设计和选择没有最优解,是一种优劣权衡的结果。而我们设计的大多数系统,都是这类型的系统,所以更值得借鉴。

Kubernetes 本身的架构在这里不细说,网上的资料非常多,如果不熟悉大家检索一下。这里抽几个值得思考和借鉴的点简单分析下。

设计思想之 声明式 VS 命令式

我们大多数系统的设计都是命令式的,或者叫过程式。系统的组件以及层级之间,通过一层一层调用和封装,组成一个大的系统。组件和层级之间都是基本都是强依赖关系,即便是要设计成弱依赖,也是代码显式完成的,比如调用超时后设置个默认值,也只是一种权宜之计。这种方式带来如下问题:

  1. 组件和层级间耦合严重,重构代价高昂,只能依赖工程师的代码抽象能力实现一定程度的解耦。
  2. 系统调用边界不可控,A 调用 B,但不知 B 又依赖什么组件和服务,可能很小一个错误,导致整个大的任务失败,并且很难重试和回滚。

以一个部署系统为例,这个系统需要下载安装包,推送到各服务器上(或者从各服务器上拉取,道理一样),从集群中摘掉该节点,关闭旧的进程,部署,启动新的进程,再加入到集群中对外服务。这个还是一个简化了的流程。这个流程中,任何一步都可能出现失败,超时等问题。如果是通过调用的方式完成,就需要有一个总的超时时间,然后各步骤也需要超时,重试机制,很难有一个优雅的方式处理。如果其中一步出错,重试几次后也失败,只能让整个任务失败,进行人工排查错误,如果贸然重试任务可能导致状态不一致。

而 Kubernetes 通过声明式方式实现的 Controller,则比较优雅的解决了这个问题。每个组件都只需要和 ETCD 这样的状态中心交互,完成自己的任务后把状态更新到 ETCD 中即可。如果失败则反复重试,直到失败条件解除。虽然任务的完成时间不可控,但可达到最终一致。

如果想进一步了解可以深入学习 Kubernetes 的 Pod,Service,ReplicaSet,Deployment,Autoscaling 不同组件之间的关系以及 Controller 的实现。

当然,申明式设计并不适合所有的系统,可以思考下哪些场景下比较适合这种设计。

思考:为什么 Kubernetes 一直没提供重启 Pod 的接口?大家普遍的概念里,一个部署系统,重启应用程序是一个必备功能。参看这个从 15 年开始讨论的 Issue https://github.com/kubernetes/kubernetes/issues/13488

如何设计出通用的 IAM 机制?

IAM(Identity and Access Management) 是所有企业级应用服务平台必须要考虑的功能之一。本想通过和各公有云的 IAM 机制的比较,来解析 Kubernetes 对安全认证机制的抽象和实现,学习和思考如何在自己的系统中设计出通用的 IAM 机制,但这里受限于篇幅,就简单分析下。

IAM 要解决的关键问题是:

  1. 身份认证:通过什么凭证来证明自己的身份,比如 用户名密码,Token,证书等。
  2. 访问权限控制:有哪些资源,不同的资源有什么操作(对应权限),如何定义权限集合,又如何和用户关联。

为了方便定义权限集合,一般需要引入 Role 的概念,一个 Role 对应一个权限集合,另外为了方便批量操作,会引入 Group 的概念把用户做一个集合。这个大体就是 Kubernetes RBAC(Role-based access control)的设计思路。

IAM 需要解决的另外一个问题是服务器端调用的身份认证问题。终端用户使用时,可以通过通用的身份凭证来校验用户,但如果两个系统之间需要互相调用呢?一种办法是创建一些系统账号,给比较高的权限,系统之前调用也是使用和终端用户一样的认证机制。带来的问题是这些账号的维护以及凭证的管理,密码等凭证泄露可能带来安全隐患。参考最近的某酒店拖库事件,最直接的原因是数据库地址以及账号密码被误提交到 GitHub,但根本原因是服务之间互相认证的凭证管理问题。

所以 Kubernetes 引入了一个 ServiceAccount 的机制。部署应用的时候,在配置文件中声明该应用拥有某 ServiceAccount 权限,这样 Kubernetes 会自动在容器中生成并挂载 ServiceAccount 的凭证,该应用通过标准 SDK 调用 Kubernetes 的接口的时候会自动使用该凭证,应用中无需配置身份凭证信息。

这个和 AWS 的 AWS Role(AWS IAM 中的 Role 和通常的理解不太一样),GCE ServiceAccount 机制类似,有兴趣的朋友可以深入调研一下。

思考:

  1. 为什么 Kubernetes 从最初的 ABAC(Attribute-based access control)演进到了 RBAC?
  2. 很多云服务将资源和用户账号关联,主要是为了方便权限控制以及计费。但企业场景下,由于员工离职等原因,可能需要在账号间迁移资源。如何设计 IAM,可以使这种迁移更便捷?

如何通过设计让资源提供方和使用方解耦?

我们的系统中总有许多依赖外部资源的地方,不同的环境,外部资源的供给方式不同,如何通过抽象,让资源供给放对使用方透明?Kubernetes 存储的 Provision 机制是一个典型案例。

Kubernetes 存储的 Provision 机制,将不同云,不同分布式存储系统,提供的存储能力抽象成统一的接口,使用方只需要在配置文件中申明需要多少存储,无需知道具体的资源提供方的信息,由 Kubernetes 系统将需求和供给进行撮合,如果当前供给不足就通过调度系统自动创建新的资源。

这种方式可以最大程度的弥合环境的异构,让应用可以在不同的环境中无缝迁移。

同时 Kubernetes 上的 service-catalog,也通过 Service Broker 的方式给其他服务提供了 Provision 机制。比如应用需要依赖另外一个服务,按照原来的方式,要么应用将依赖服务也一起打包进来,要么让用户手动操作,解决依赖问题。而 Service Broker 的方式是应用只需要声明,如果系统中尚未有类似服务,则会自动创建。

思考:自己开发的应用系统是否有和资源环境的耦合?当前是如何解决的?

复杂系统如何提供自定义机制?

任何一个软件系统本身都是对一种通用模式的沉淀,但任何通用的模式都会遇到特例,环境和场景的变化,新的技术的出现等都会给原有系统带来挑战。这时候设计一种扩展机制,以应对变化成为了必选项。

Kubernetes 提供了以下扩展能力:

  1. Admission Kubernetes 的 Admission 属于其安全系统的一部分,通过插件机制提供了一系列拦截器,以实现自定义的安全策略以及默认属性的注入。可以利用它来设计自己的安全策略以及运维规范。
  2. CloudProvider 给不同的云厂商的扩展
  3. Volume 存储的扩展,和前面说的 Provision 机制相关。
  4. CNI(Container Network Interface)网络扩展,实现不同的网络方案。
  5. CRD(CustomResourceDefinition) Kubernetes 的 CRD 是它的声明式设计的优势集中体现,有了这种扩展能力后,几乎可以把整个 DevOps 平台中涉及的工具统一纳入 Kubernetes 中,通过 Kubernetes 提供统一的控制平面,屏蔽后端工具的差异。

每一种扩展,都有不同的演进历程,当前的成熟度也不同。并且扩展的具体实现方式和编程语言也有关系。Kubernetes 是 Go 实现的,它的动态加载机制还不成熟,所以 Kubernetes 中的扩展大多数都是通过标准化的命令行调用或者 RPC 接口实现的。

思考:列举下自己知道的插件式系统,分析下它们的插件是如何抽象的,如何实现的,不同的编程语言有什么差异。

如何用代码生成机制减少重复代码,提高开发效率?

Kubernetes 中大量使用了代码生成机制,一方面是因为 Kubernetes 是用 go 语言开发的,go 当前不支持泛型,也无法支持像 java 那样的动态字节码生成技术,所以很多情况需要依赖静态代码生成机制。

另外一方面 Kubernetes 的 RESE API 设计的非常优雅,SDK 这种完全可以自动生成。Kubernetes 中的代码生成工具都已经单独抽取成独立的项目了,了解一下它的运作方式,对自己设计和架构系统也很有帮助。

思考:如何设计出便于代码生成的系统?

综合系统如何确定自己的边界?

Kubernetes 是一个综合系统,这样的系统在演化过程中会很容易膨胀成一个无所不能的系统。Kubernetes 又是如何控制自己的膨胀的『欲望』,探索和界定自己的边界的?这个问题是一个值得深入探讨和思考的问题。

后记

Kubernetes 中可以琢磨研究的点实际上很多,上面只是列举了几点,也只是抛出了问题,但并没有给出详细分析。这里推荐一个张磊的 Kubernetes 的课程,《深入剖析 Kubernetes》,看了他的课程目录,非常完备,可以兼顾初学者以及想进一步深入了解 Kubernetes 的朋友。

我已经听了几期,张磊兄从最初的 Docker 诞生以及演进开始分析,有技术,有故事,普通话也很好听,娓娓道来,兼顾趣味性与知识性,即便是老手也能有新的收获。

希望大家在听的过程中,一遍学习 Kubernetes 的特性,一遍思考它的架构。学习的第一境界是学会使用,第二境界是知道它的实现方式,第三境界是可以做到居高临下,把学习到的点用到自己的创造过程中去。

相关链接


  1. Kuberneters 中 Go 的泛型 https://medium.com/@arschles/go-experience-report-generics-in-kubernetes-25da87430301
  2. 深入 Kubernetes 的 CustomResource 代码生成机制 https://blog.openshift.com/kubernetes-deep-dive-code-generation-customresources/