开头:本文由 dbaplus 社群授权转载。
其实我挺早就接触 Docker 和 Kubernetes,时间大概在 3、4 年前吧,但是由于当时所在技术团队的业务模式所限制,还没有真正对容器云有技术需求,所以我更多还是以一种技术玩具的心态接触容器技术。
直到去年开始才正式接触基于容器云平台的技术架构,我从业务运维和 DevOps 的角度来看,容器云平台与之前的物理机和虚拟机等 IaaS 层基础上的运维模式有着非常大的差异。
根据这段时间的运维经验,我尝试总结一下某些容器云的运维方法的共同特性,并将其称为“容器运维模式”,简单百度谷歌了一下,没有这个名词,希望是我的首创:)
这个名词灵感来自软件工程的“设计模式”,设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
而“容器运维模式”,指的是由 DevOps(题外话:DevOps、SRE、SA、运维等等,其实都差不多是同一个意思,业界喜欢创一个新的名词来代替运维,主要是为了区分自己和一些低端系统维护人员)在日常运维容器化项目的一些经验总结,为了区别于传统的物理机、虚拟机的运维套路,而归纳出来的容器运维方法。
回顾过去
从大概 10 年前,大家都是以【自建 IDC】+【物理服务器】的形式进行生产环境基础架构的建设。
然后持续到大概 5 年前,私有云技术和公有云的兴起,让大批中小型企业减少对物理设备资源建设的人力和资金投入,可以专注于业务研发和运营。
最后到大概 3、4 年前,容器技术 Docker 和以 Kubernetes 为代表的容器编排技术的崛起,以及微服务技术的同步普及,宣告了容器云平台的来临。
而事实上,以 Kubernetes 为首的相关周边项目,已经成为了容器云领域的首选标准,所以绝大部分技术团队如果现在需要选型容器编排体系,可以无脑选 k8s 了。
需求的根本——应用交付
在传统裸机(bare metal)或虚拟化的时代,当开发团队将代码交付给运维进行生产环境中部署,但是它却未能正常工作时,挑战就出现了。
“运行环境不一致”、“没有安装相关依赖软件”、“配置文件不一样”等等已经成了开发和运维沟通的惯用语。
在传统的开发场景中,开发和测试团队使用的是与生产环境不同的基础设施,尽管做到了代码和配置解耦,但是在运行环境的转换中,依然会得到像前面所述的团队协作和环境依赖问题。
而贯穿软件生命周期共享相同的容器镜像是容器化带来的最大好处,它简化了开发与运维团队之间的协作关系。
由于本地开发/测试服务器和生产环境的不一致以及应用程序打包部署的过程,一直是让研发和运维纠结的难题,但有了容器之后,由于容器镜像里打包的不仅是应用,而是整个操作系统的文件和目录,即其运行所需的所有依赖,都能被封装一起。
有了容器镜像的打包能力之后,这些应用程序所需的基础依赖环境,也成为了这个应用沙盒的一部分,这可以给这个应用包赋予这样的能力:无论在开发、测试还是生产环境运行,我们只需要解压这个容器镜像,那么这个应用所需的所有运行依赖都是存在的、一致的。
如果熟悉 Docker 容器技术原理的话,我们知道它主要由 Linux 内核的 Namespace 和 CGroups 以及 rootfs 技术隔离出来一种特殊进程。
把 Docker 形容为一个房子的话,Namespace 构成了四面墙,为 PID\NET\MNT\UTS\IPC 等资源进行隔离;CGroups 形成了它的天花板,限制了对系统资源的占用;而 rootfs 是其地基,是通过 copy-on-write 机制构成的分层镜像,也是开发者最为关心的应用信息的传递载体。
作为开发者,他们可能不关心由前两者构成的容器运行时的环境差异,因为真正承载容器化应用的传递载体,是这个不变的容器镜像。
在 Docker 技术的普及后不久,为了整个完整的 DevOps 链条的打通,包括 CI/CD、监控、网络、存储、日志收集等生产环境的刚需,以及整个容器生命周期的管理和调度,以 Kubernetes 为首的容器编排体系也作为上层建筑也迎来了一波快速的增长。从容器到容器云的蜕变,标志着容器运维时代的来临。
容器运维模式的主要场景分析
1、声明式 vs 命令行
我们知道 Kubernetes 是通过 yaml 文件(样例如上所示)来对其 API 对象,如 Deployment、Pod、Service、DaemonSet 等进行期望状态的描述,然后 k8s 的控制器有一套状态调谐的机制让各种 API 对象按要求所述的状态运行。由于这样一套运行机制的存在,所以使得 k8s 和过往运维常见的命令行,也包括脚本式的运行方式有着很大的差异。
深度使用过 puppet 的运维工程师可能会比较清楚两者的区别,puppet 也是一套基于声明式机制的配置管理和状态管理的工具。在没有 puppet 之前,运维工程师喜欢用简单的 shell、python 脚本对众多服务器进行统一的软件安装、配置管理,但随着服务器数量增多和配置项的递增,命令行式的配置管理往往出现各种缺陷。如状态不一致、历史版本无法回滚、配置没有幂等性、需要很多状态判断才能执行最终的操作等等。
而声明式的配置管理方法,可以规避以上弊端,原因如下:
当我们确认了一个版本 yaml 配置文件后,表示向 k8s 的 Kube-Controller-Manager 提交了我们所期望的对象状态信息,然后 k8s 使用 patch 的方式对 API 对象进行修改。而声明式 API 是 k8s 项目编排能力的核心所在,它可以在无需干预的情况下对 api 对象进行增删改查,完成对“期望状态”和“实际状态”的 reconcile 过程。
以我们最常用的 deployment 对象为例。
1)方式一
2)方式二
首次创建使用 create ,修改 yaml 使用 edit,然后用 replace 使之生效。
k8s 对这两种机制的处理方法是完全不同的,前者是声明式,后者是命令式。
两者的结果虽然都是触发滚动更新,但是前者是对原有 API 对象打 patch,后者是对象的销毁和替换。前者能一次处理多个 yaml 配置变更的写操作并具备相同配置项的 merge 能力,后者只能逐个处理,否则有冲突的可能。
所以,我们只需要确认 yaml 文件的版本,一律通过 kubectl apply 命令进行执行,无需再考虑第一步创建、第二步修改、第三步替换之类的命令行。那么我们统一用 apply 命令,可以通过 history 命令进行回溯版本,也可以保证 apply 的结果的幂等性等等。
使用声明式只需要描述最终所需的状态,无需用户关心过多的实现流程和细节,没有像命令行式的那么多上下文关系或者运行环境依赖,甚至可以由开发人员直接编写,运维进行 code review 即可。特别在使用 Kubernetes 这样的容器编排工具,更加要深刻理解和灵活运用声明式的运维模式。
2、API 对象
Kubernetes 大量的 API 对象的存在是导致其运维方法和传统系统层运维有区别较大的重要原因之一。
如果我们要深入了解 k8s,则需要理解一些它核心的 API 对象,才能更好地理解这个容器的运行系统。如果把容器理解成一种特殊带有资源隔离、资源限制的进程,那么 Pod 对象是一组进程组,最后,k8s 是运行众多有关联的进程组(Pod)的操作系统。
这一层操作系统运行在 PaaS 层,比我们传统运维的 Linux 系统所在的 IaaS 层要高一层。
而我们在理解这个在 PaaS 层的 k8s 对象的概念时,需要一些面向对象的编程思想,会让整个思路梳理地更加清晰。
所谓的面向对象,即在编码过程中设定一切事物皆对象,通过面向对象的方式,将现实世界的事物抽象成对象,现实世界中的关系抽象成类、继承,帮助人们实现对现实世界的抽象与数字建模。
通过面向对象的方法,更利于用人理解的方式对复杂系统进行分析、设计与编程。同时,面向对象能有效提高编程的效率,通过封装技术,消息机制可以像搭积木的一样快速开发出一个全新的系统。
面向对象是指一种程序设计范型,同时也是一种程序开发的方法。对象指的是类的集合。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性。
在系统层运维时候,我们关注的有 CPU、内存、IO 等硬件对象,以及软件安装卸载、系统服务启停、环境变量、内核版本等软件对象等等,就足以理解和把控整个操作系统运行环境。
理解这些对象可以当成是一种面向过程的思维,因为最初操作系统的设计就是当时的计算机大牛们通过面向过程的思维所写出来的,所以系统很多组成概念无需要面向对象思维就可以理解。
众所周知,Kubernetes 是根据谷歌内部运行多年的 Borg 项目的架构体系所创造出来,所以它具备天生的项目架构前瞻性。一般的开源项目是理论基础走在工程应用的后面,比如 docker + swarm 为代表,都是现实应用中遇到什么需求,就新增一个功能,慢慢从一个单独容器 docker 再到了具备基本编排能力的 swarm。反观 Kubernetes,是一套自顶向下的架构设计,几乎能适配当前所有的应用架构模式,应对什么 web-db、lb-web-redis-db、db-master-slave 之类的常见架构根本不在话下。
再回到 Kubernetes 的 API 对象,k8s 使用这些 API 对象来描述一个集群所期望的运行状态。
通常一个 Kubernetes 对象包含以下信息:需要运行的应用以及运行在哪些 Node 上、应用可以使用哪些资源、应用运行时的一些配置,例如副本数、重启策略、升级以及容错性等等。
通过上图可见 API 对象种类非常多,其实我们应该先重点掌握最核心的 Node、Pod、Deployment、RS、Service、Namespace,以及它们之间的关系,这里就不详述了,请参考相关文档。
3、控制器模式
在说 Kubernetes 的控制器模式之前,我们先看看软件架构中十分常见的 MVC 模式,即 Model(模型)、View(视图)、Controller(控制器)。
1)模型(Model)
用于封装与应用程序的业务逻辑相关的数据以及对数据的处理方法。“ Model ”有对数据直接访问的权力,例如对数据库的访问。“Model”不依赖“View”和“Controller”,也就是说, Model 不关心它会被如何显示或是如何被操作。但是 Model 中数据的变化一般会通过一种刷新机制被公布。为了实现这种机制,那些用于监视此 Model 的 View 必须事先在此 Model 上注册,从而,View 可以了解在数据 Model 上发生的改变。比如:观察者模式(软件设计模式)。
2)视图(View)
能够实现数据有目的的显示(理论上,这不是必需的)。在 View 中一般没有程序上的逻辑。为了实现 View 上的刷新功能,View 需要访问它监视的数据模型(Model),因此应该事先在被它监视的数据那里注册。
3)控制器(Controller)
起到不同层面间的组织作用,用于控制应用程序的流程。它处理事件并作出响应。“事件”包括用户的行为和数据 Model 上的改变。
MVC 模式强调职责分离,即视图和数据模型的分离,并利用控制器来作为这两者的逻辑控制的中介,使之具有逻辑复用、松散耦合等优点。
数据模型(Model),它描述了“应用程序是什么”,用于封装和保存应用程序的数据,同时定义操控和处理该数据的逻辑和运算。而且,Model 通常是可以复用的。
一个良好的 MVC 应用程序应该将所有重要的数据都封装到 Model 中,而应用程序在将持久化的数据(文件、数据库)加载到内存中时,也应该保存在 Model 中。
因为 Model 本身就代表着业务的特定数据对象,而在 k8s 里面,最典型的 Model 就是 Pod。
视图(View),它是展现给用户的界面,这个不用多说。这个在 k8s 的应用不多,例如 kubectl 的信息输出或者 Dashbord 等,都可以算是一种 View 的应用。
控制器(Controller),它充当 View 和 Model 的媒介,将模型和视图绑定在一起,包括处理用户的配置输入,以此修改 Model。反过来,View 需要知道 Model 中数据的变化,也是通过 Controller 来完成。除此之外,Controller 还可以为应用程序协调任务,管理其它对象的生命周期。在 k8s 里面,最典型的 Controller 就是 Deployment。
在上文中我们提到了 k8s 拥有很多 API 对象,而其中一部分是属于控制器类型的特殊对象,我们可以进入 k8s 的代码目录:kubernetes/pkg/controller/*,查看所有控制机类型的 API 对象,包含:deployment\job\namespace\replicaset\cronjob\serviceaccount\volume 等等。
由于 k8s 的架构体系中,View 不算是其核心的功能模块,我们这里重点关注 Controller 和 Model 的关系,代入 k8s 对象的话,我们以最典型的 Deployment 和 Pod 的关系,作为主要的研究对象。
我们回头看看文章连载前面的 Deployment 的 yaml 配置文件样例,可以划分为两大部分进行分析,配置文件的上半部分是属于控制器,下半部分是数据模型:
其实要深究起来,Deployment 不是直接控制 Pod,而是通过一个叫 ReplicaSet 的对象对 Pod 进行编排控制,所在在 Pod 的 matadata 里面会显示其 owerReference 是 ReplicaSet。
也就是说在控制器对象的范围内,也会进行功能的分层,因为不同的控制机之间,存在着可以复用的功能逻辑,比如对 Pod 的副本数控制。
那么这时候可以抽象出一层例如像 ReplicaSet 的对象,进行对 Pod 的副本控制,除了 Deployment 以外,也存在其他的控制器对象可以利用 ReplicaSet 进行对 Model 的控制。
基于这样的分层思想,我们在生产环境场景的所遇到的需求,可以将其控制逻辑都在控制器这一层进行实现。
比如无状态的 Deployment 和有状态的 StatefuleSet,或者每个 Node 只有一个 DeamonSet,尽管各自实现的功能各不相同,但是它们都是可以共用同一套 Pod 对象的逻辑,而差异的部分都封装在控制器层。
4、接口和实现
接口这个词广泛存在于各种技术文档中,到底接口是什么?
其实,狭义的接口是指代码编写的一个技巧,比如在 Java 语言里面,一个接口(interface)的特性是只定义了方法返回值、名称、参数等,但没有定义其具体的实现。
接口(interface)无法被实例化,但是可以被实现。一个实现(implements)接口的类(class),必须实现接口内所描述的所有方法,否则就必须声明为抽象类(Abstract Class)。
Java 接口实现:
以上是 Java 的接口类型,但除了狭义的接口,我们在开发各种软件中也会用到广义的接口。
接口对于调用方来说就是一种事先约定好的协议,它也许是一些预先定义的函数,目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节。
而在 Kubernetes 里面,其很多组件或者实现都采用了接口的形式,留给使用者非常灵活的扩展空间。
比如 CRI \ CSI \ CNI 等等,都是 Kubernetes 留给其底层实现的接口方式。
Kubernetes 作为云原生应用的最佳部署平台,已经开放了容器运行时接口(CRI)、容器网络接口(CNI)和容器存储接口(CSI),这些接口让 Kubernetes 的开放性变得最大化,而 Kubernetes 本身则专注于容器调度。
我们逐个了解一下以上 3 个接口,就可以对 Kubernetes 的实现思想有一定的感受,从而更深地理解其它类似的接口实现。
1)CRI (Container Runtime Interface,容器运行时接口)
Kubernetes 其实不会直接和容器打交道,Kubernetes 的使用者能接触到的概念只有 pod,而 pod 里包含了多个容器。
CRI 中定义了容器和镜像的服务的接口,因为容器运行时与镜像的生命周期是彼此隔离的。
当我们在 Kubernetes 里用 kubectl 执行各种命令时,这一切是通过 Kubernetes 工作节点里所谓“容器运行时”的软件在起作用。大家最熟悉的容器运行时软件当然是 Docker,然而 Docker 只是 Kubernetes 支持的容器运行时技术的一种。
为了让 Kubernetes 不和某种特定的容器运行时(Docker)技术绑死,而是能无需重新编译源代码就能够支持多种容器运行时技术的替换,和我们面向对象设计中引入接口作为抽象层一样,在 Kubernetes 和容器运行时之间我们引入了一个抽象层,即容器运行时接口。以后就算 Docker 不再流行了,甚至有了 Eocker、Focker 等等,就可以通过 CRI 接口无缝地融入 Kubernetes 体系。
2)CSI (Container Storage Interface,容器存储接口)
CSI 代表容器存储接口,CSI 试图建立一个行业标准接口的规范,借助 CSI 容器编排系统(CO)可以将任意存储系统暴露给自己的容器工作负载。
类似于 CRI,CSI 也是基于 gRPC 实现。CSI 卷类型是一种 in-tree(即跟其它存储插件在同一个代码路径下,随 Kubernetes 的代码同时编译的) 的 CSI 卷插件,用于 Pod 与在同一节点上运行的外部 CSI 卷驱动程序交互。部署 CSI 兼容卷驱动后,用户可以使用 csi 作为卷类型来挂载驱动提供的存储。
3)CNI (Container Network Interface,容器存储接口)
CNI(Container Network Interface)是 CNCF 旗下的一个项目,由一组用于配置 Linux 容器的网络接口的规范和库组成,同时还包含了一些插件。CNI 仅关心容器创建时的网络分配,和当容器被删除时释放网络资源。
Kubernetes 网络的发展方向是希望通过插件的方式来集成不同的网络方案, CNI 就是这一努力的结果。CNI 只专注解决容器网络连接和容器销毁时的资源释放,提供一套框架,所以 CNI 可以支持大量不同的网络模式,并且容易实现。
CNI 的接口中包括以下几个方法:
有四个方法:添加网络、删除网络、添加网络列表、删除网络列表。
5、Master-Node 模式与 Api-server
Kubernetes 有几个核心组件:kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy、CRI(一般是 docker)等等。
它们分别是运行在 Master 或者 Node 节点上面,我把 Master 和 Node 称为物理组件,因为它们是运行于物理环境的,如物理机或者虚拟机。其中 Master 提供集群的管理控制中心,而 Node 是真正接受执行任务的工作节点,可以拟人化地理解为:Master 是用人经理,Node 是工作人员。
而 Etcd 是用于存储配置信息或者其他需要持久化的数据,独立于 Master 和 Node 节点,一般也是三副本的方式运行。
1)Master
区别于物理组件,逻辑组件是指在程序内的虚拟概念,例如运行在 Master 的逻辑组件有 kube-apiserver、kube-controller-manager、kube-scheduler。
kube-apiserver 用于暴露 Kubernetes API。任何的资源请求/调用操作都是通过 kube-apiserver 提供的接口进行。
kube-controller-manager 运行管理控制器,它们是集群中处理常规任务的后台线程。逻辑上,每个控制器是一个单独的进程,但为了降低复杂性,它们都被编译成单个二进制文件,并在单个进程中运行。
kube-scheduler 监视新创建没有分配到 Node 的 Pod,为 Pod 选择一个 Node。
这几个组件的用途不作特别展开,我们后面将详细聊聊 Apiserver。
2)Node
Node 是 Kubernetes 中的工作节点,最开始被称为 minion。一个 Node 可以是 VM 或物理机。每个 Node(节点)具有运行 pod 的一些必要服务,并由 Master 组件进行管理。
然后介绍运行于 Node 节点的组件:kubelet、kube-proxy、CRI(一般是 docker)。
kubelet 是主要的节点代理,它会监视已分配给节点的 pod,具体功能如:安装 Pod 所需的 volume;下载 Pod 的 Secrets;Pod 中运行的 docker(或 experimentally,rkt)容器;定期执行容器健康检查等等。
kube-proxy 通过在主机上维护网络规则并执行连接转发来实现 Kubernetes 服务抽象。
Docker 等容器运行时,作用当然就是用于运行容器。
对于以上的 Kubernetes 的 Master 和 Node 的节点模式,在很多支持分布式架构的软件中都是类似的,如 Hadoop 等。他们的 Master 节点往往需要有 3 个以上,以实现高可用架构。很多软件架构也采取了这样的设计方式,都是为了生产环境所需的高可用性服务。
3)Api-server
前面介绍过 Master 和 Node,它们之间从 Master (apiserver)到集群有两个主要的通信路径。第一个是从 Apiserver 到在集群中的每个节点上运行的 kubelet 进程。第二个是通过 Apiserver 的代理功能从 Apiserver 到任何 Node、pod 或 service。
所以说 Apiserver 对于 Master-Node 模式来说是非常重要的沟通桥梁。
从 Apiserver 到 kubelet 的连接用于获取 pod 的日志,通过 kubectl 来运行 pod,并使用 kubelet 的端口转发功能。这些连接在 kubelet 的 HTTPS 终端处终止。
从 Apiserver 到 Node、Pod 或 Service 的连接默认为 HTTP 连接,因此不需进行认证加密。也可以通过 HTTPS 的安全连接,但是它们不会验证 HTTPS 端口提供的证书,也不提供客户端凭据,因此连接将被加密但不会提供任何诚信的保证。这些连接不可以在不受信任/或公共网络上运行。
总结
从过去的【单体式应用+物理机】,到现在【微服务应用+容器云】的运行环境的变革,需要运维工程师同步改变以往的运维技术思维。新技术的应用,会引发更深层次的思考,深入了解容器之后,我们会自然而然地去学习业务最主流的编排工具——Kubernetes。
Kubernetes 前身是谷歌的 Borg 容器编排管理平台,它充分体现了谷歌公司多年对编排技术的最佳实践。而容器云字面意思就是容器的云,实际指的是以容器为单位,封装环境、提供构建、发布、运行分布式应用平台。
而运维工程师在面对业界更新迭代极快的技术潮流下,需要选定一个方向进行深耕,无疑,Kubernetes 是值得我们去深入学习的,毕竟它战胜了几乎所有的编排调度工具,成为业内编排标准。
我们通过搭建容器云环境下的应用运行平台,并实现运维自动化,快速部署应用、弹性伸缩和动态调整应用环境资源,提高研发运营效率,最终实现自身的运维价值。
作者介绍:
温峥峰,小鹏汽车互联网中心运维高级经理,专注于运维自动化、DevOps 实践、运维服务体系建设与容器运维时代下的价值挖掘。知乎专栏:HiPhone 运维之道
原文链接:
https://mp.weixin.qq.com/s/GPJyfackiUXr27asapbOdQ
评论