这 里的基础设施,我们可以理解为服务器、虚拟机或者是容器。
可变与不可变基础设施
传统的开发运维体系中,软件开发完成后,需要工程师或管理员通过SSH
连接到他们的服务器上,然后进行一些脚本安装、deb/rpm
包的安装工作,并逐个机器地调整对应的配置参数及文件。后续还会根据需要对该环境进行不断更改,比如 kernel 升级、配置更新、打补丁等。
随着这种类似变更的操作越来越多,没有人能弄清楚这个环境具体经历了哪些操作,而后续的变更也经常会遇到各种意想不到的诡异事情,比如软件包的循环依赖、参数的配置不一致、版本漂移等问题。
基础设施会变得越来越脆弱、敏感,一些小的改动都有可能引发大的不可预知的结果,这令广大开发者和环境管理员异常抓狂,他们需要凭借自己丰富的技术积累,耗费大量的时间去排查解决。
- 持续的变更修改给服务运行态引入过多的中间态,增加了不可预知的风险;
- 故障发生时,难以及时快速构建出新的服务副本;
- 不易标准化,交付运维过程异常痛苦,虽然可以通过 Ansible、Puppet 等部署工具进行交付,但是也很难保证对底层各种异构的环境支持得很好,还有随时会出现的版本漂移问题。比如你可能经常遇到的,某个软件包几个月之前安装还能够正常运行,现在到一个新环境安装后,竟然无法正常工作了。
不可变基础设施则是另一种思路,部署完成以后,便成为一种只读状态,不可对其进行任何更改。如果需要更新或修改,就使用新的环境或服务器去替代旧的。不可变基础设施带来了更一致、更可靠、更可预测的设计理念,可以缓解或完全避免可变基础设施中遇到的各种常见问题。
同时,借助容器技术我们可以自动化地构建出不可变的、可版本化管理的、可一致性交付的应用服务体系,这里包括了标准化实例、运行环境等。还可以依赖持续部署系统,进行应用服务的自动化部署更新,加快迭代和部署效率。
最原子化单元—-Pod
同一个 Pod 中的容器共享网络、存储资源。
如果在一个 Pod 内有多个容器,那么这几个容器最好是密切相关的
。
一般来说,在一个 Pod 内运行多个容器,比较适应于以下这些场景。
- 容器之间会发生文件交换等,上面提到的例子就是这样。一个写文件,一个读文件。
- 容器之间需要本地通信,比如通过 localhost 或者本地的 Socket。这种方式有时候可以简化业务的逻辑,因为此时业务就不用关心另外一个服务的地址,直接本地访问就可以了。
- 容器之间需要发生频繁的 RPC 调用,出于性能的考量,将它们放在一个 Pod 内。
- 希望为应用添加其他功能,比如日志收集、监控数据采集、配置中心、路由及熔断等功能。这时候可以考虑利用边车模式(Sidecar Pattern),既不需要改动原始服务本身的逻辑,还能增加一系列的功能。比如 Fluentd 就是利用边车模式注入一个对应 log agent 到 Pod 内,用于日志的收集和转发。 Istio 也是通过在 Pod 内放置一个 Sidecar 容器,来进行无侵入的服务治理。
Pod 背后的设计理念
1. 为什么 Kubernetes 不直接管理容器,而用 Pod 来管理呢?
直接管理一个容器看起来更简单,但为了能够更好地管理容器,Kubernetes 在容器基础上做了更高层次的抽象,即 Pod。因为使用一个新的逻辑对象 Pod 来管理容器,可以在不重载容器信息的基础上,添加更多的属性,而且也方便跟容器运行时进行解耦,兼容度高。比如:
- 存活探针(Liveness Probe)可以从应用程序的角度去探测一个进程是否还存活着,在容器出现问题之前,就可以快速检测到问题;
- 容器启动后和终止前可以进行的操作,比如,在容器停止前,可能需要做一些清理工作,或者不能马上结束进程;
- 定义了容器终止后要采取的策略,比如始终重启、正常退出才重启等;
2. 为什么要允许一个 Pod 内可以包含多个容器?
由于容器实际上是一个“单进程”的模型,这点非常重要。因为如果你在容器里启动多个进程,这将会带来很多麻烦。不仅它们的日志记录会混在一起,它们各自的生命周期也无法管理。毕竟只有一个进程的
PID 可以为 1,如果 PID 为 1
的进程这个时候挂了,或者说失败退出了,那么其他几个进程就会自然而然地成为“孤儿”,无法管理,也无法回收资源。
很多公司在刚开始容器化改造的时候,都会这么去使用容器,把容器当作 VM 来使用,有时候也叫作富容器模式。这其实是一种非常不好的尝试,也不符合不可变基础设施的理念。我们可以接受将富容器当作容器化改造的一个短暂的过渡形态,但不能将其作为改造的终态。后续,还需要进一步对这些富容器进行拆分、解耦。
用一个 Pod 管理多个容器,既能够保持容器之间的隔离性,还能保证相关容器的环境一致性。使用粒度更小的容器,不仅可以使应用间的依赖解耦,还便于使用不同技术栈进行开发,同时还可以方便各个开发团队复用,减少重复造轮子。
如何声明一个 Pod
在 Kubernetes 中,所有对象都可以通过一个相似的 API 模板来描述,即元数据 (metadata)、规范(spec)和状态(status)。这种方式也是从 Borg 吸取的经验,避免过多的 API 定义设计,不利于统一和对接。Kubernetes 有了这种统一风格的 API 定义,方便了通过 REST 接口进行开发和管理。
元数据(metadata)
metadata 中一般要包含如下 3 个对该对象至关重要的元信息:
namespace(命名空间)、name(对象名)和 uid(对象 ID)。
- namespace是 Kubernetes 中比较重要的一个概念,是对一组资源和对象的抽象集合,namespace 主要用于逻辑上的隔离。Kubernetes 中有几个内置的 namespace:
- default,这是默认的缺省命名空间;
- kube-system,主要是部署集群最关键的核心组件,比如一般会将 CoreDNS 声明在这个 namespace 中;
- kube-public,是由 kubeadm 创建出来的,主要是保存一些集群 bootstrap 的信息,比如 token 等;
- kube-node-lease,是从 v1.12 版本开始开发的,到 v1.14 版本变为 beta 可用版本,在 v1.17 的时候已经正式 GA 了,它要用于 node 汇报心跳,每一个节点都会有一个对应的 Lease 对象。
- 对象名比较好理解,就是用来标识对象的名称,在 namespace 内具有唯一性,在不同的 namespace 下,可以创建相同名字的对象。
- uid 是由系统自动生成的,主要用于 Kubernetes 内部标识使用,比如某个对象经历了删除重建,单纯通过名字是无法判断该对象的新旧,这个时候就可以通过 uid 来进行唯一确定。
当然, Kubernetes 中并不是所有对象都是 namespace 级别的,还有一些对象是集群级别的,并不需namespace 进行隔离,比如 Node 资源等。
除此以外,还可以在 metadata 里面用各种标签 (labels)和注释(annotations)来标识和匹配不同的对象,比如用户可以用标签env=dev来标识开发环境,用env=testing来标识测试环境。
规范 (Spec)
在 Spec 中描述了该对象的详细配置信息,即用户希望的状态(Desired State)。Kubernetes 中的各大组件会根据这个配置进行一系列的操作,将这种定义从“抽象”变为“现实”,我们称之为调和(Reconcile)。用户不需要过度关心怎么达到终态,也不用参与。
状态(Status)
在这个字段里面,包含了该对象的一些状态信息,会由各个控制器定期进行更新。也是不同控制器之间进行相互通信的一个渠道。在 Kubernetes 中,各个组件都是分布式部署的,围绕着 kube-apiserver 进行通信,那么不同组件之间进行信息同步,就可以通过 status 进行。像 Node 的 status 就记录了该节点的一些状态信息,其他的控制器,就可以通过 status 知道该 Node 的情况,做一些操作,比如节点宕机修复、可分配资源等。现在我们来看一个 Pod 的 API 长什么样子。
一个 Pod 的真实例子
apiVersion: v1 #指定当前描述文件遵循v1版本的Kubernetes API
kind: Pod #我们在描述一个pod
metadata:
name: twocontainers #指定pod的名称
namespace: default #指定当前描述的pod所在的命名空间
labels: #指定pod标签
app: twocontainers
annotations: #指定pod注释
version: v0.5.0
releasedBy: david
purpose: demo
spec:
containers:
- name: sise #容器的名称
image: quay.io/openshiftlabs/simpleservice:0.5.0 #创建容器所使用的镜像
ports:
- containerPort: 9876 #应用监听的端口
- name: shell #容器的名称
image: centos:7 #创建容器所使用的镜像
command: #容器启动命令
- "bin/bash"
- "-c"
- "sleep 10000"
你可以通过 kubectl 命令在集群中创建这个 Pod。kubectl 的功能比较强大、也比较灵活。
$ kubectl create -f ./twocontainers.yaml
kubectl get pods
NAME READY STATUS RESTARTS AGE
twocontainers 2/2 Running 0 7s
创建出来后,稍微等待一下,我们就可以看到,该 Pod 已经运行成功了。现在我们可以通过 exec 进入shell这个容器,来访问sise服务:
$ kubectl exec twocontainers -c shell -i -t -- bash
[root@twocontainers /]# curl -s localhost:9876/info
{"host": "localhost:9876", "version": "0.5.0", "from": "127.0.0.1"}