第5章 认证与调度系统
不知道大家有没有意识到一个现实,就是大部分情况下,我们已经不像以前一样通过命令行或者可视窗口来使用一个系统了。现在我们上微博或者网络购物,操作的其实不是眼前这台设备,而是一个又一个集群。图5-1所示为数据中心内景。
图5-1 数据中心内景
展示了集群物理服务器的典型外观,通常成百上千台服务器整齐排列在机架上。
通常,这样的集群拥有成百上千个节点,每个节点是一台物理机或虚拟机。集群一般远离用户,坐落在数据中心。为了让这些节点互相协作,对外提供一致且高效的服务,集群需要操作系统。
Kubernetes 就是这样的操作系统。
如图5-2所示,比较Kubernetes和单机操作系统,Kubernetes相当于内核,它负责集群软硬件资源管理,并对外提供统一的入口,用户可以通过这个入口来使用集群,和集群沟通。
图5-2 Kubernetes与单机操作系统
对比示意图:单机操作系统(如Linux)包含硬件、内核、系统调用接口、应用程序;Kubernetes中包含物理机/虚拟机节点作为“硬件”,Kubernetes本身作为“内核”,API Server作为“系统调用接口”,Pod/Service等作为“应用程序”。Kubernetes对外提供统一的集群入口(API Server)。
而运行在集群之上的程序,与普通程序有很大的不同。这样的程序是“关在笼子里”的程序,它们从被制作,到被部署,再到被使用都不寻常。我们只有深挖根源,才能理解其本质。
5.1 “关在笼子里”的程序
5.1.1 代码
我们使用Go语言写了一个简单的Web服务器程序 app.go,这个程序监听 2580 这个端口。通过HTTP协议访问这个服务的根路径,服务会返回 "This is a small app for kubernetes..." 字符串。
使用 go build 命令编译这个程序,会产生一个可执行文件 app。这是一个普通的可执行文件,它在操作系统里运行,会依赖系统里的库文件。
5.1.2 “笼子”
为了让这个程序不依赖于操作系统自身的库文件,我们需要制作容器镜像,即隔离的运行环境。
Dockerfile是制作容器镜像的“菜谱”,包括了制作镜像的两个步骤:
- 下载一个Centos基础镜像。
- 把
app这个可执行文件放到镜像中。
5.1.3 地址
制作好的镜像存在本地环境中,我们需要把这个镜像上传到镜像仓库里去。这里的镜像仓库相当于应用商店。我们使用阿里云的镜像仓库,上传之后镜像地址是:
registry.cn-hangzhou.aliyuncs.com/kube-easy/app:latest
镜像地址可以拆分成四个部分:仓库地址/命名空间/镜像名称:镜像版本。
显然,上面的镜像地址在阿里云杭州镜像仓库,使用的命名空间是 kube-easy,镜像名:镜像版本是 app:latest。至此,我们有了一个可以在Kubernetes集群上运行的“关在笼子里”的程序。
5.2 得其门而入
5.2.1 入口
Kubernetes作为操作系统,和普通的操作系统一样有API的概念。有了API,集群就有了入口;有了API,我们使用集群才能得其门而入。
Kubernetes的API被实现为运行在集群节点上的组件 API Server,如图5-3所示。这个组件是典型的Web服务器程序,通过对外暴露 http(s) 接口来提供服务。
图5-3 Kubernetes及其管理入口
用户/客户端通过互联网或内网访问集群API Server,API Server管理底层节点。
这里我们创建一个阿里云Kubernetes集群。登录集群管理页面,我们可以看到API Server的公网入口。
API Server内网连接端点:https://xx.xx.197.238:6443
5.2.2 双向数字证书验证
阿里云Kubernetes集群API Server组件,使用基于CA签名的双向数字证书认证来保证客户端与API Server之间的安全通信。这句话很拗口,初学者不太好理解,我们来深入解释一下。
从概念上来讲,数字证书是用来验证网络通信参与者的一个文件。这和学校颁发给学生的毕业证书类似。
在学校和学生之间,学校是可信第三方CA,而学生是通信参与者。如果人们普遍信任一个学校的话,那么这个学校颁发的毕业证书也会得到社会认可。参与者证书和CA证书好比毕业证和学校的办学许可证。
这里有两类参与者:CA和普通参与者。与此对应,我们有两种证书:CA证书和参与者证书。另外我们还有两种关系:证书签发关系和信任关系。这两种关系至关重要。
我们先看签发关系。如图5-4所示,我们有两张CA证书,三张参与者证书。其中最上面的CA证书签发了两张证书,一张是中间的CA证书,另一张是右边的参与者证书;中间的CA证书签发了下面两张参与者证书。这五张证书以签发关系为联系,形成了树状的证书签发关系图。
图5-4 证书与证书之间的关系
树状结构(签发关系):
- 根CA证书
- 签发 → 中间CA证书
- 签发 → 参与者证书B
- 签发 → 参与者证书C
- 签发 → 参与者证书A
graph TD RootCA[根CA证书] -->|签发| MidCA[中间CA证书] RootCA -->|签发| ParticipantA[参与者证书A / 网站] MidCA -->|签发| ParticipantB[参与者证书B / 浏览器?] MidCA -->|签发| ParticipantC[参与者证书C / 客户端?]信任关系独立于签发关系:信任关系指某参与者相信某个CA(从而信任该CA签发的所有参与者证书)。例如浏览器信任根CA,则浏览器信任由根CA签发的网站证书。
然而,证书以及签发关系本身,并不能保证可信的通信可以在参与者之间进行。
以图5-4为例,假设最右边的参与者是一个网站,最左边的参与者是一个浏览器,浏览器“相信”网站的数据,不是因为网站有证书,也不是因为网站的证书是CA签发的,而是因为浏览器相信最上面的CA,这就是信任关系。
理解了CA证书、参与者证书、签发关系以及信任关系之后,我们回头看看“基于CA签名的双向数字证书认证”是什么意思。
客户端和API Server作为通信的普通参与者,各有一张证书,如图5-5所示。这两张证书都是由CA签发的,我们简单地称它们为集群CA和客户端CA。客户端信任集群CA,所以它信任拥有集群CA签发证书的API Server;反过来,API Server需要信任客户端CA,然后才愿意与客户端通信。
图5-5 Kubernetes集群证书实现
阿里云Kubernetes集群中,集群CA和客户端CA实际上是同一张证书(双向共用一张CA证书)。关系如下:
- 同一CA证书同时作为集群CA和客户端CA。
- CA证书签发 API Server证书(CN=kube-apiserver)和 客户端证书(CN=子账号用户名)。
- 客户端证书的签发者CN = 集群ID,与CA证书的CN一致。
- API Server通过
--client-ca-file参数指定信任的客户端CA证书(即Cluster CA证书)。- 客户端通过
KubeConfig文件中的集群CA证书来验证API Server的证书。
阿里云Kubernetes集群CA证书和客户端CA证书,在实现上其实是一张证书,所以我们有图5-5所示的关系图。
5.2.3 KubeConfig文件
登录集群管理控制台,我们可以看到KubeConfig文件。这个文件包括了客户端证书、集群CA证书,以及其他证书。
证书使用Base64编码,所以我们可以使用Base64工具解码证书,并使用 openssl 查看证书文本。
- 首先,客户端证书签发者的公用名CN是集群ID:
c0256a,而证书本身的CN是子账号252771。 - 其次,只有在API Server信任客户端CA证书的情况下,上面的客户端证书才能通过API Server的验证。
kube-apiserver进程通过--client-ca-file这个参数指定其信任的客户端CA证书,其指定的证书是/etc/kubernetes/pki/apiserver-ca.crt。这个文件实际上包含了两张客户端CA证书,其中一张和集群管控有关系,这里不做解释,另外一张如下,它的CN与客户端证书的签发者CN一致。 - 再次,API Server使用的证书,由kube-apiserver的参数
--tls-cert-file决定,这个参数指向证书/etc/kubernetes/pki/apiserver.crt。这个证书的CN是kube-apiserver,签发者是c0256a,即集群CA证书。 - 最后,客户端需要验证上面这张API Server的证书,因而KubeConfig文件里包含了其签发者,即集群CA证书。对比集群CA证书和客户端CA证书,发现两张证书完全一样,这符合我们的预期。
5.2.4 访问
理解了原理之后,我们可以做一次简单的测试。我们以证书作为参数,使用 curl 访问API Server,并得到预期结果。
# 示例:使用客户端证书、客户端密钥、CA证书访问API Server
curl --cert client.crt --key client.key --cacert ca.crt https://xx.xx.197.238:6443/api/v1/namespaces/default/pods(具体响应结果略去,通常返回JSON格式的Pod列表)
5.3 择优而居
5.3.1 两种节点,一种任务
如开始所讲,Kubernetes是管理集群多个节点的操作系统,这些节点在集群中的角色却不必完全一样。Kubernetes集群有两种节点:Master节点 和 Worker节点,如图5-6所示。
图5-6 Kubernetes集群和集群节点
架构示意图:一个Master节点(运行kube-apiserver, kube-controller-manager, kube-scheduler, etcd等管理组件)和多个Worker节点(运行kubelet, kube-proxy, 容器运行时,实际承载Pod任务)。
这种角色的区分,实际上就是一种分工:Master节点负责整个集群的管理,其上运行的以集群管理组件为主,这些组件包括实现集群入口的API Server;而Worker节点主要负责承载普通任务。
在Kubernetes集群中,任务被定义为**Pod**。Pod是集群可承载任务的原子单元。Pod被翻译成“容器组”,其实是意译,因为一个Pod实际上封装了多个容器化的应用。从原则上讲,被封装在一个Pod里边的容器之间应该存在较大程度上的耦合关系。
5.3.2 择优而居
调度算法需要解决的问题,是替Pod选择一个舒适的“居所”,让Pod所定义的任务可以在这个节点上顺利地完成。
为了实现“择优而居”的目标,Kubernetes集群调度算法采用了两步走的策略:
- 第一步:从所有节点中排除不满足条件的节点,这一步是 预选(Predicates)。
- 第二步:给剩余的节点打分,最后得分高者胜出,这一步是 优选(Priorities)。
下面,我们使用5.1节中制作的镜像创建一个Pod,并通过日志来具体分析一下,这个Pod怎样被调度到某一个集群节点。
5.3.3 Pod配置
首先,我们创建Pod的配置文件,配置文件格式是json。这个配置文件有三个地方比较关键,分别是镜像地址、命令及容器的端口。
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "test-app"
},
"spec": {
"containers": [
{
"name": "app",
"image": "registry.cn-hangzhou.aliyuncs.com/kube-easy/app:latest",
"command": ["./app"],
"ports": [
{
"containerPort": 2580
}
]
}
]
}
}5.3.4 日志级别
集群调度算法被实现为运行在Master节点上的系统组件,这一点和API Server类似。其对应的进程名是 kube-scheduler。
kube-scheduler 支持多个级别的日志输出,但社区并没有提供详细的日志级别说明文档。在查看调度算法对节点进行筛选、打分的过程中,我们需要把日志级别提高到10,即加入参数 --v=10。
5.3.5 创建Pod
使用 curl,以证书和Pod配置文件等作为参数,通过 POST 请求访问API Server的接口,我们可以在集群里创建对应的Pod。
curl --cert client.crt --key client.key --cacert ca.crt \
-X POST \
-H "Content-Type: application/json" \
-d @pod.json \
https://xx.xx.197.238:6443/api/v1/namespaces/default/pods5.3.6 预选
预选是Kubernetes调度的第一步,这一步要做的事情,是根据预先定义的规则,把不符合条件的节点过滤掉。不同版本的Kubernetes所实现的预选规则有很大的不同,但基本趋势是,预选规则会越来越丰富。
比较常见的两个预选规则是 PodFitsResourcesPred 和 PodFitsHostPortsPred。前一个规则用来判断一个节点上的剩余资源能不能满足Pod的需求,后一个规则检查一个节点上某一个端口是不是已经被其他Pod所使用了。
以下日志是调度算法在处理测试Pod时输出的预选规则的日志。这段日志记录了预选规则 CheckVolumeBindingPred 的执行情况。某些类型的存储卷只能挂载到一个节点上,这个规则可以过滤掉不满足Pod对存储卷需求的节点。
预选规则 CheckVolumeBindingPred 日志:
I... predicated node "node1", predicate "CheckVolumeBindingPred" success
I... predicated node "node2", predicate "CheckVolumeBindingPred" success
I... predicated node "node3", predicate "CheckVolumeBindingPred" success从app的编排文件里可以看到,Pod对存储卷并没有什么需求,所以这个条件并没有过滤掉节点。
5.3.7 优选
调度算法的第二个阶段是优选阶段。在这个阶段,kube-scheduler 会根据节点可用资源及其他一些规则给剩余节点打分。
目前,CPU和内存是调度算法考量的两种主要资源,但考量的方式并不是简单地认为剩余CPU、内存资源越多得分就越高。
日志记录了两种计算方式:LeastResourceAllocation 和 BalancedResourceAllocation。前一种方式计算Pod调度到节点之后,节点剩余CPU和内存占总CPU和内存的比例,比例越大得分就越高;第二种方式计算节点上CPU和内存使用比例之差的绝对值,绝对值越大得分就越低。
- 资源打分: 这两种方式,第一种倾向于选出资源使用率较低的节点,第二种希望选出两种资源使用比例接近的节点。这两种方式有一些矛盾,最终依靠一定的权重来平衡这两个因素。
除了资源之外,优选算法会考虑其他一些因素,比如Pod与节点的亲和性,或者当一个服务由多个相同Pod组成时,多个Pod在不同节点上的分散程度,这是保证高可用的一种策略。
- 其他因素打分:
(日志片段显示类似:
"other factor xxx score: 10")
5.3.8 得分
最后,调度算法会让所有的得分乘它们的权重,然后求和得到每个节点最终的得分。因为测试集群使用的是默认调度算法,而默认调度算法调度算法会把日志中出现的得分项所对应的权重都设置成了1,所以如果按日志里有记录的得分项来计算,最终三个节点的得分应该分别是29、28和29。
- 节点得分:
日志输出与实际计算的得分不一致。之所以会出现日志输出的得分和我们自己计算的得分不一致的情况,是因为日志并没有输出所有的得分项,猜测漏掉的策略应该是 NodePreferAvoidPodsPriority,这个策略的权重是10 000,每个节点得分为10,所以才得出日志最终输出的结果。
(注:由于日志未完全展示,实际输出的最终得分可能为每个节点的原始得分加上10000*10=100000,导致节点间排序不变但数字大幅增加,而日志中显示的最终得分可能是归一化或带权重的最终结果。)
5.4 总结
在本章中,我们以一个简单的容器化Web程序为例,着重分析了客户端怎么样通过Kubernetes集群API Server认证,以及容器应用怎么样被分配到合适的节点这两件事情。
在分析过程中,我们弃用了一些便利的工具,比如 kubectl 和控制台。我们用了一些更接近底层的方法,比如拆解KubeConfig文件,再比如分析调度器日志来分析认证和调度算法的运作原理。希望这些对大家进一步理解Kubernetes集群有所帮助。