一统江湖:Kubernetes(一)
前文我们详细的介绍了容器与容器掀起的 DevOps 风潮,本节我们跳出容器,开始学习容器管理与编排,并思考基础设施的建设。我们将带着如下的问题开始本节的内容:
- 为什么需要容器编排?
- Kubernetes 到底是什么?
- 如何系统学习 Kubernetes?
为什么需要容器编排?
按照 Docker 官网的文档,编排器的定义是
Tools to manage, scale, and maintain containerized applications are called orchestrators
即:虽然容器为应用提供了 DevOps 的有力支持,但容器本身的能力是受限的:
- 从用户应用角度来说,微服务体系中大量服务的服务暴露、服务注册、服务发现、服务通信、负载均衡等等能力,仅靠容器本身无法有效支持。单个容器只是一种能力,而服务才是价值所在。
- 从应用运维角度来说,容器的生命周期管理,包括部署、更新、删除、错误恢复、扩缩容,容器的调度、迁移、资源分配、监控、追踪等能力,都需要额外的机制支撑;从分布式系统角度来说,基础设施节点的管理、运维,更是云平台的重中之重。
所以我们不仅需要容器,还需要容器编排与基础设施管理。典型的容器编排系统有:Docker-Swarm、mesos、Kubernetes等。在长达数年的竞争、合作后,Kubernetes 最终一统江湖,成为容器编排的事实标准。
Kubernetes 的前世今生
首先,我们把时间线拨回到 Golang 问世的 2007 年,此后经过若干年的发展,服务端高并发的优势、简单易入门的特性、Google 的背书,使得 Golang 迅速发展,并积累了一批企业用户,其中包括了 Docker 的母公司 dotCloud。
同时,2007 年开始,众多云计算厂家开始将目光投向 PaaS,Saleforce、AWS、Google 等陆续开始在 PaaS 领域发力,dotCloud 公司也是 PaaS 浪潮中的一家小公司。此时云计算还是 SaaS 和 IaaS 的天下,经过五六年的发展,PaaS 逐渐渡过了筚路蓝缕、以启山林的日子,CloudFoundry、Heroku、OpenShift 等产品不断涌现,但应用在不同环境中的迁移、打包仍一直是云计算的一个痛点。
2013 年 3 月,dotCloud 公司开源了 Docker 容器技术。Docker 解决了后端应用的环境与打包问题,一出生就引发了广泛的关注和讨论。Docker 的开源给 PaaS 带来了一次降维打击,众多云厂商纷纷开始拥抱 Docker,将 docker 作为底层的容器运行时。一时间 Docker 风光无二,社区迅速壮大,隐隐有成为容器的运行时标准的趋势;Docker 母公司也趁机推出了 Docker Swarm、Docker Compose 等项目,希望将整个 Docker 生态 PaaS 化。
Docker 走向 PaaS 化的操作与 CoreOS、Google 等公司产生了利益冲突。PaaS 巨头并不希望自己的蛋糕被分走,他们更希望 Docker 仅仅是一个标准的容器运行时。为了对抗 Docker 一家独大的现状,2014 年 CoreOS 推出了自家的容器运行时 Rocket,与 Docker 分道扬镳,PaaS 产品也同时开始支持多种容器运行时。而 Docker 自己在 Swarm 等项目上也并不算成功,随着时间推移,Docker 隐隐有衰落之势。
在容器编排方面,谷歌有深厚的技术积累。Kubernetes 项目就起源于谷歌内部的容器管理系统 Borg,在 Kubernetes 官网 2015 年 4 月的博客中我们可以找到这样一句话:
Google has been running containerized workloads in production for more than a decade.
Kubernetes traces its lineage directly from Borg.
2014 年,Google 开源了内部项目 Borg,并用 Go 语言进行了重写,这就是 Kubernetes。Kubernetes 含着金钥匙出身,自己实力又出众,迅速吸引了大批公司。Google 希望通过 Kubernetes 成为 PaaS 领域的领头羊。而在容器方面,Docker 与其他公司各退了一步。Docker 公司牵头将 Docker 的底层运行时:libcontainer 捐赠给基金会管理,并改名为 RunC。众多厂商,如 CoreOS、Docker、Google、RedHat 等,共同制定标准的容器与镜像规范,这就是 OCI(Open Container Initiative),这意味着容器的核心能力从 Docker 公司中剥离,容器之争升级为容器编排之争。
随着 OCI 一起诞生的还有 CNCF(云原生基金会),旨在以 Kubernetes 项目为基础,建立一个由开源基础设施领域厂商主导的、按照独立基金会方式运营的平台级社区,对抗 Docker 公司的容器生态。之后 Kubernetes 发展越来越火热,Docker 不断式微,最终在 2017 年 10 月,Docker 宣布在 Docker 企业版内置 Kubernetes。最终商业巨头们瓜分了容器市场,Kubernetes 一统江湖。
在本地快速部署一个 Kubernetes 集群
现在我们了解了 Kubernetes 的历史,在正式开始学习 Kubernetes 之前,我们首先在本地部署一个 Kubernetes 集群,这样照着 Demo 学习会更直观一些。
网上有很多 Kubernetes 的部署项目,如 kubeAdm 等等,都可以一键启动一个 Kubernetes 集群,但对我们来说,kubeAdm 还是太复杂了,而且***下载镜像的体验十分糟糕。所以,我们需要 KinD (Kubernets in Docker)。但是社区的 KinD 项目对电脑的配置有一定的要求,且启动过程仍然偏慢。我们需要一个更快、更轻便的 KinD,这就是今天的主角:byscorp/kind。
Kubernetes-in-Docker - A single node cluster to run your CI tests against thats ready in 30 seconds
由于我们只需要进行最基本的学习,这个单节点的 KinD 已经足够了。它足够快,足够方便,足够省资源。为了启动和使用这个 KinD 集群,首先确保
- 你的电脑里装了 Docker,安装说明见这里。
- KinD 的版本可以调整,如果想使用其他版本,在这里挑选一个即可。
- 保证自己的环境中 8843 与 10080 端口没有被占用。
- 应保证
kubectl已经安装,见这里。
注:我的电脑是 Macbook Air,8G 内存,i5 双核,运行 KinD 绰绰有余。
打开命令行工具,执行
docker run -tid --rm --name=kind --privileged -p 8443:8443 -p 10080:10080 bsycorp/kind:v1.15.8
之后执行
docker logs -f kind | grep "Kubernetes ready"
等到屏幕有日志输出后,代表 KinD 已经初始化完毕。我们还需要获取 kubeconfig 文件:
curl localhost:10080/config > $HOME/.kube/config
- 应保证
kubectl已经安装,见这里。
查看一下这个集群:
$ kubectl get nodes NAME STATUS ROLES AGE VERSION minikube Ready master,node 340d v1.15.1
这个单节点的 Kubernetes 集群已经可以使用了。
Kubernetes 基础知识扫盲
首先介绍一下 Kubernetes 的字面含义。Docker 单词是集装箱的意思,Kubernetes 在希腊语中的含义是舵手,代表着当应用打包进“集装箱”开始出海远航时,Kubernetes 是一艘船,装载、管理着所有的容器应用。对了,Kubernetes 可以缩写为 K8s,因为 K 和 S 中间有 8 个字母。对于 Kubernetes,更专业的定义是
Kubernetes (K8s) is an open-source system for automating deployment, scaling, and management of containerized applications.
系统核心组件简介
上一小节我们已经搭建了一个 K8s 集群,现在看一下这个集群上部署了哪些东西。执行
$ kubectl get pod -n kube-system NAME READY STATUS RESTARTS AGE coredns-5c98db65d4-skm27 1/1 Running 1 340d coredns-5c98db65d4-vmdg6 1/1 Running 1 340d etcd-minikube 1/1 Running 0 340d kube-addon-manager-minikube 1/1 Running 0 23m kube-apiserver-minikube 1/1 Running 0 340d kube-controller-manager-minikube 1/1 Running 0 340d kube-proxy-lk5pf 1/1 Running 0 340d kube-scheduler-minikube 1/1 Running 0 340d storage-provisioner 1/1 Running 0 340d
这里涉及第一个知识点:什么是 pod ?pod 是 Kubernetes 进行容器编排的最小单位,是若干容器的载体。pod 的详细含义可以参考概念-pods。形象一点的解释是:pod 的原意是“豆荚”,一个 pod 可以包含一个或多个容器,就像一个豆荚可以包含许多豆子一样,直观地反映了 K8s 侧重的并非容器本身,而是容器编排。
看着上面的输出,相信大多数初学者是懵逼的,所以我们先看一下 K8s 集群的架构图:
Kubernetes 的架构是 CU 分离的,CU 分离意味着控制面的组件不涉及用户任务的具体执行,只涉及控制信令的执行。集群的节点分为 Master 节点与 Node 节点,Master 节点组成控制面板,Node 节点组成用户面;一个节点可单独作为 Master 节点或 Node 节点,也可以两种角色都担任。用户可通过 kubectl CLI 与 K8s 集群进行交互。
Master 是 K8s 系统的控制面板,有如下核心组件:
- APIServer:APIServer 是一个大型 Web Server,它负责接收与中转(几乎)所有进出 K8s 集群的流量。APIServer 除了做一些默认值填充、校验等操作外,几乎不直接处理收到的请求,而是通过启动 Web Server 时注册的 HTTP Handler,将请求转发给其他控制面板组件,或转发给用户面的组件进行处理。
- Kube-Scheduler:Scheduler 是 K8s 集群的调度器,负责为每个待调度的 pod 选择一个节点进行调度。与其说这是一个调度器,不如说这是一个任务的放置器,它只告诉系统哪个 pod 应当被放到哪个节点运行,但不绝对保证 pod 真的可以在这个节点被启动,更不管理这个任务占有多少时间片。对初学者来说,K8s 调度器和我们认为的调度器,如 linux kernel scheduler 有所不同,一定要注意他们的区别。
- Controller-Manager:Controller Manager 是众多 Controller 的集合,其作用是监控集群资源的变化,确保集群资源始终处于预期的状态。例如:默认情况下某个 Pod 意外失败退出时,Pod Controller 会监听到这一事件,并重新启动这个 Pod。当然,Controller Manager 还有许多其他 Controller,这一点我们后面再细说。
- ETCD:ETCD 是一个高可用的分布式键值数据库。K8s 默认使用 ETCD 存储集群数据,且对 etcd 的所有操作都通过 APIServer 进行,任何组件无法僭越。多说一句,在 k3s 项目中,etcd 可被 SQLite 替换。
Node 节点时 K8s 系统的用户面,有如下核心组件:
- Kubelet:集群中每个 Node 节点上运行的代理,负责 Pod 的生命周期管理,如创建、更新、删除等,此外,
- kube-proxy:集群中每个节点上运行的网络代理,负责 K8s 集群中应用的服务通信、路由转发。
- Docker:kubelet 通过 CRI (Container Runtime Interface) 可支持多种容器运行时,如 Docker、Rocket、kata container 等等。默认情况下,K8s 使用 Docker 作为底层的容器运行时。为了方便理解,这里先写成 Docker,不一下子引入过多概念。
对比架构图中提到的核心组件与 KinD 集群的结果,我们发现有一些组件还没有提到,如
- coreDNS:用作 K8s 集群中的服务注册与服务发现。
- Storage-provisioner:kubeadm 独有的组件,为 K8s 集群提供外部存储的接入能力。
- Addon-manager:kubeadm 独有的组件,管理 K8s 插件。
忽略这三个组件,整个 K8s 最核心的组件就是我们上面提到的这些了。下面,我们结合一个 Pod 的创建流程,体会一下 K8s 各组件究竟起到了什么作用。这个例子中,我们将创建的 Pod 的 Yaml 文件为
apiVersion: v1
kind: Pod
metadata:
name: test-create-pod
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80 注意由于我们没有为该 nginx 服务做服务暴露,在集群外部我们是无法访问到的。
将上面的代码复制到本地的 pod.yaml 中,在命令行中执行
kubectl create -f pod.yaml
之后,可以通过 kubectl get pod 观察到一个名叫 test-create-pod-xxxxx 的 Pod 被启动了。下面我们讲解一下 Pod 的简化版启动过程,以分析 K8s 集群各组件的作用,以及 K8s 的工作模式:
- kubectl 执行用户命令。首先 kubectl 读取我们之前保存的
config文件,它记录了集群的信息,如 APIServer 的 IP,TLS Certs 等,用来连接目标集群的 APIServer 并鉴权。由于我们指定了-f的 flag,kubectl 还会读取 YAML 文件。kubectl 发现是一个创建 Pod 的请求,于是经过一系列的填充、校验等预处理之后,kubectl 向 APIServer 发出一个 REST 请求,告诉 APIServer 某个用户希望创建 Pod,并包含了 Pod 的描述信息。 - APIServer 接受 kubectl 发送的请求,于是将这个 pod 的信息存储到 ETCD。此时 Pod 还没有真正被创建出来。如果存储到 ETCD 成功,APIServer 向 kubectl 返回成功信息。此后,用户就可以通过
kubectl get pod来查看 pod 的信息和状态了。Pod 的信息写入 ETCD 成功之后,APIServer 还会通知 Kube-Scheduler,告知调度器有一个 Pod 的 CREATE 事件到来。 - Kube-Scheduler 接收到一个创建 Pod 的事件,开始为 Pod 选择一个绑定的节点。经过调度器内部的调度算法和流程,最终挑选出一个合适的节点。选出一个合适的节点之后,调度器向 APIServer 发出一个 REST 请求,请求更新 Pod 的状态(其关键细节为填充 Pod 的 nodeName),从而驱使目标节点的 Kubelet 创建该 Pod。
- APIServer 接收到 Kube-Scheduler 的请求,向 ETCD 中更新 Pod 的信息。更新完成后,APIServer 向目标集群的 Kubelet 发送事件,告知 Kubelet 处理该 Pod 的创建请求。
- Kubelet 接收到 APIServer 的请求,经过进行一系列的检查、校验,开始创建 Pod。创建过程中,Kubelet 会调用宿主机上的 Docker 创建容器,并设置容器的网络、存储卷。在这个过程中,Kubelet 会发送若干个 REST 请求给 APIServer,请求更新 Pod 的信息。
- APIServer 也将接收到若干个 Kubelet 的请求,向 ETCD 更新 Pod 的信息。在其中某个请求中,APIServer 会向调度器发送一个事件,通知 Pod 已在目标主机真正创建成功。
- Kube-Scheduler 接收到 Pod 真正创建成功的消息,于是结束调度器对该 Pod 的处理。
- 如果一切顺利,容器成功创建并处于 Running 状态,且没有其他错误,Kubelet 将认为 Pod 处于 Running 且 Ready 的状态。最终向 APIServer 发出更新 Pod 信息的请求。最后,用户就可以看到 Pod 已经在成功运行了。
注意:为了不涉及太多太细节、太底层的知识,也为了让初学者对 K8s 有最直观、又不失一定深度的认识,这里省略了大量的细节和术语,只保留了最关键的某些步骤,旨在说明 K8s 的大致工作方式。大家切不可认为这就是完整的创建 Pod 的过程。
我们可以总结出 K8s 的核心工作模式:
- APIServer 是集群的流量出入口。用户所有的请求都将到达 APIServer;内部所有的请求也均通过 APIServer 进行转发,各组件之间并不做直接的交互。
- 只有 APIServer 直接操作后端的 ETCD 数据库。
- 各组件是异步工作的,通过某种“通知”机制同步数据与状态(该机制在后面细说)。
- CU 分离,控制面不负责用户任务的真正创建。
K8s 的大脑:controller manager
上面为了尽量简单,我们只创建了一个 Pod,而创建 Pod 的流程没有涉及到 controller manager,这一 K8s 的大脑。但实际使用中,我们极少单独创建一个 Pod。考虑这样的场景:我需要部署一个 nginx 服务器。
第一种方案:部署一个 Pod,启动一个单节点 Redis 服务。然而无法解决的问题有:
- 多副本
- 负载均衡
- 服务暴露:集群外部无法访问该服务,集群内部只能通过 Pod IP 访问
- 自动扩缩容
- ...
所以显然,在 Pod 为我们做了应用的容器化封装后,我们需要一种新的资源类型,为我们做应用运维。像 Deployment, StatefulSet, DaemonSet, Job, CronJob 等等,就具有应用运维能力。它们都是对 Pod 的上层封装,这些工作负载将包含若干个 Pod。
每种工作负载有自己的工作逻辑,以最常见的 Deployment 为例。用户需要在 Deployment 资源中指定 Replicas 值。比如 Replicas 为 2,意味着该 Deployment 将始终试图保持有 2 个 Pod 同时在运行。一旦数量不等于 Replicas 值,系统将新建/删除 Pod,以达到期望值。例如下面是一个 Deployment 样例,为了不干扰大家的思路,这里不对 Deployment 的其他字段进行说明。
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
namespace: default
labels:
app: nginx
spec:
replicas: 3 # 最核心的能力
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80 注:这里的 YAML 内容可以保存成文件,然后 kubectl create -f xx.yaml 来创建。创建这个 Deployment 后,可以用 kubectl get deployment 来查看。
这样的应用运维能力由 Controller manager 提供。Controller manager 是一组 Controllers 的集合,例如 Replicas Controller, Deployment Controller, Job Controller 等等,这些 Controller 彼此独立,为不同的资源类型提供各自的应用运维能力。可以说,Controller Manager 是 K8s 的大脑。
有了工作负载,我们可以解决多副本问题了。现在,我们试着解决服务暴露的问题。这部分内容不属于 Controller Manager 的范畴,我们只简单提一下。
用户可以在 K8s 中定义 Service,对自己的容器化应用进行服务注册、服务暴露。CoreDNS 组件提供了服务发现的能力,当 Service 被注册进 K8s 系统中后,K8s 会为服务创建一个固定的 IP,这个 IP 称为 ClusterIP,同时,创建一条 DNS 规则,使得用户可以通过特定的域名访问该服务。K8s 通过 Iptables 进行服务到后端的 Pod 的路由转发与负载均衡,该能力是通过用户面的 Kube-Proxy 实现的。这样一来,不用担心 Pod 宕机、重启后 IP 变化,因为 Service 的 ClusterIP 是不变的。例如,下面是一个 Service 的样例:
apiVersion: v1
kind: Service
metadata:
name: nginx-service
namespace: default
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80 创建该服务后,用户在集群内可以通过 nginx-service.default.svc 这个域名查找到服务的 ClusterIP,即服务名.命名空间.svc:端口的模式。
K8s 开放接口
K8s 的成功不仅在于设计良好的架构,还在于其强大的可拓展性,如 Aggregated APIServer + Custom Resource Definition 增强了 K8s 在资源类型上的可拓展性,CNI/CSI/CRI 增强了用户面的拓展性,使 K8s 可以更专注于容器编排。
我们首先看一下 Kubelet 中最核心的三个接口:CNI/CRI/CSI:
- CRI (Container Runtime Interface):容器运行时接口,可兼容多种容器运行时,如 runC, kata containers 等等
- CSI (Container Storage Interface):容器存储接口,可对接多种第三方存储,如 ceph, nfs 等等
- CNI (Container Network Interface):容器网络接口,可对接多种网络插件,如 flannel, calico 等等
Kubelet 通过这三个接口,极大的拓展了 K8s 的能力,使得 K8s 可兼容多种基础设施。
小小的总结
K8s 是一个开源的容器管理平台,核心组件包括了控制面的 APIServer, ETCD, controller manager, scheduler,其中
- APIServer 是所有流量的出入口
- ETCD 是 K8s 存储自身所有信息的数据库
- Controller manager 是 K8s 的大脑,提供强大的应用运维能力
- Scheduler 是一个 Pod 放置器,负责调度 Pod 到某个节点
还有用户面的 Kube-Proxy, Kubelet 等等,其中
- Kube-Proxy 提供了路由转发能力
- Kubelet 真正负责 Pod 的生命周期管理,是每个节点上运行的代理
初次之外,K8s 通过标准化的开放接口 CNI/CSI/CRI 实现了用户面的可拓展性,并使得 K8s 可以专注于容器管理。