从单机容器化技术Docker到分布式容器化架构方案Kubernetes,当今容器化技术发展盛行。本文面向小白读者,旨在快速带领读者了解Docker、Kubernetes的架构、原理、组件及相关使用场景。
Docker
1.什么是Docker
Docker 是一个开源的应用容器引擎,是一种资源虚拟化技术,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上。虚拟化技术演历路径可分为三个时代:
1. 物理机时代,多个应用程序可能跑在一台物理机器上
2. 虚拟机时代,一台物理机器启动多个虚拟机实例,一个虚拟机跑多个应用程序
3. 容器化时代,一台物理机上启动多个容器实例,一个容器跑多个应用程序
在没有 Docker 的时代,我们会使用硬件虚拟化(虚拟机)以提供隔离。这里,虚拟机通过在操作系统上建立了一个中间虚拟软件层 Hypervisor ,并利用物理机器的资源虚拟出多个虚拟硬件环境来共享宿主机的资源,其中的应用运行在虚拟机内核上。但是,虚拟机对硬件的利用率存在瓶颈,因为虚拟机很难根据当前业务量动态调整其占用的硬件资源,加之容器化技术蓬勃发展使其得以流行。
Docker、虚拟机对比:
特性 | Docker | 虚拟机 |
启动 | 秒级 | 分钟级 |
交付/部署 | 开发、测试、生产环境一致 | - |
性能 | 近似物理机 | 性能损耗大 |
体量 | 极小(MB) | 较大(GB) |
迁移/扩展 | 跨平台,可复制 | - |
另外开发人员在实际的工作中,经常会遇到测试环境或生产环境与本地开发环境不一致的问题,轻则修复保持环境一致,重则可能需要返工。但 Docker 恰好解决了这一问题,它将软件程序和运行的基础环境分开。开发人员编码完成后将程序整合环境通过 DockerFile 打包到一个容器镜像中,从根本上解决了环境不一致的问题。
2.Docker的构成
Docker 由镜像、镜像仓库、容器三个部分组成
- 镜像: 跨平台、可移植的程序+环境包
- 镜像仓库: 镜像的存储位置,有云端仓库和本地仓库之分,官方镜像仓库地址
- 容器: 进行了资源隔离的镜像运行时环境
3.Docker的实现原理
到此读者们肯定很好奇 Docker 是如何进行资源虚拟化的,并且如何实现资源隔离的,其核心技术原理主要有(内容部分参考自Docker 核心技术与实现原理):
(1).Namespace
> 在日常使用 Linux 或者 macOS 时,我们并没有运行多个完全分离的服务器的需要,但是如果我们在服务器上启动了多个服务,这些服务其实会相互影响的,每一个服务都能看到其他服务的进程,也可以访问宿主机器上的任意文件,这是很多时候我们都不愿意看到的,我们更希望运行在同一台机器上的不同服务能做到完全隔离,就像运行在多台不同的机器上一样。
命名空间 (Namespaces) 是 Linux 为我们提供的用于分离进程树、网络接口、挂载点以及进程间通信等资源的方法。Linux 的命名空间机制提供了以下七种不同的命名空间,通过这七个选项我们能在创建新的进程时设置新进程应该在哪些资源上与宿主机器进行隔离。
1. CLONE_NEWCGROUP
2. CLONE_NEWIPC
3. CLONE_NEWNET
4. CLONE_NEWNS
5. CLONE_NEWPID
6. CLONE_NEWUSER
7. CLONE_NEWUTS
在Linux系统中,有两个特殊的进程,一个是 pid 为 1 的 /sbin/init 进程,另一个是 pid 为 2 的 kthreadd 进程,这两个进程都是被 Linux 中的上帝进程 idle 创建出来的,其中前者负责执行内核的一部分初始化工作和系统配置,也会创建一些类似 getty 的注册进程,而后者负责管理和调度其他的内核进程。
当在宿主机运行 Docker,通过`docker run`或`docker start`创建新容器进程时,会传入 CLONE_NEWPID实现进程上的隔离。
接着,在方法`createSpec`的`setNamespaces`中,完成除进程命名空间之外与用户、网络、IPC 以及 UTS 相关的命名空间的设置。
func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
s := oci.DefaultSpec()
// ...
if err := setNamespaces(daemon, &s, c); err != nil {
return nil, fmt.Errorf("linux spec namespaces: %v", err)
}
return &s, nil
}
func setNamespaces(daemon *Daemon, s *specs.Spec, c *container.Container) error {
// user
// network
// ipc
// uts
// pid
if c.HostConfig.PidMode.IsContainer() {
ns := specs.LinuxNamespace{Type: "pid"}
pc, err := daemon.getPidContainer(c)
if err != nil {
return err
}
ns.Path = fmt.Sprintf("/proc/%d/ns/pid", pc.State.GetPID())
setNamespace(s, ns)
} else if c.HostConfig.PidMode.IsHost() {
oci.RemoveNamespace(s, specs.LinuxNamespaceType("pid"))
} else {
ns := specs.LinuxNamespace{Type: "pid"}
setNamespace(s, ns)
}
return nil
}
网络
当 Docker 容器完成命名空间的设置,其网络也变成了独立的命名空间,与宿主机的网络互联便产生了限制,这就导致外部很难访问到容器内的应用程序服务。Docker提供了4种网络模式,通过`–net`指定。
- host
- container
- none
- bridge
由于后续介绍 Kubernetes 利用了 Docker 的 bridge 网络模式,所以仅介绍该模式。Linux中为了方便各网络命名空间的网络互相访问,设置了Veth Pair和网桥来实现,Docker也是基于此方式实现了网络通信。
下图中 `eth0` 与 `veth9953b75` 是一个 Veth Pair,`eth0` 与 `veth3e84d4f` 为另一个 Veth Pair。Veth Pair 在容器内一侧会被设置为 `eth0` 模拟网卡,另一侧连接 Docker0 网桥,这样就实现了不同容器间网络的互通。加之 Docker0 为每个容器配置的 iptables 规则,又实现了与宿主机外部网络的互通。
挂载点
解决了进程和网络隔离的问题,但是 Docker 容器中的进程仍然能够访问或者修改宿主机器上的其他目录,这是我们不希望看到的。
在新的进程中创建隔离的挂载点命名空间需要在 clone 函数中传入 CLONE_NEWNS,这样子进程就能得到父进程挂载点的拷贝,如果不传入这个参数子进程对文件系统的读写都会同步回父进程以及整个主机的文件系统。当一个容器需要启动时,它一定需要提供一个根文件系统(rootfs),容器需要使用这个文件系统来创建一个新的进程,所有二进制的执行都必须在这个根文件系统中,并建立一些符号链接来保证IO不会出现问题。
另外,通过 Linux 的`chroot`命令能够改变当前的系统根目录结构,通过改变当前系统的根目录,我们能够限制用户的权利,在新的根目录下并不能够访问旧系统根目录的结构个文件,也就建立了一个与原系统完全隔离的目录结构。
(2).Control Groups(CGroups)
Control Groups(CGroups) 提供了宿主机上物理资源的隔离,例如 CPU、内存、磁盘 I/O 和网络带宽。主要由这几个组件构成:
1. **控制组(CGroup)**一个 CGroup 包含一组进程,并可以在这个 CGroup 上增加 Linux Subsystem 的各种参数配置,将一组进程和一组 Subsystem 关联起来。
2. Subsystem 子系统 是一组资源控制模块,比如CPU子系统可以控制CPU时间分配,内存子系统可以限制 CGroup 内存使用量。可以通过`lssubsys -a`命令查看当前内核支持哪些 Subsystem。
3. Hierarchy 层级树 主要功能是把 CGroup 串成一个树型结构,使 CGruop 可以做到继承,每个 Hierarchy 通过绑定对应的 Subsystem 进行资源调度。
4. Task 在 CGroups 中,task 就是系统的一个进程。一个任务可以加入某个 CGroup,也可以从某个 CGroup 迁移到另外一个 CGroup。
在 Linux 的 Docker 安装目录下有一个 docker 目录,当启动一个容器时,就会创建一个与容器标识符相同的 CGroup,举例来说当前的主机就会有以下层级关系:
每一个 CGroup 下面都有一个 tasks 文件,其中存储着属于当前控制组的所有进程的 pid,作为负责 cpu 的子系统,cpu.cfs_quota_us 文件中的内容能够对 CPU 的使用作出限制,如果当前文件的内容为 50000,那么当前控制组中的全部进程的 CPU 占用率不能超过 50%。
当我们使用 Docker 关闭掉正在运行的容器时,Docker 的子控制组对应的文件夹也会被 Docker 进程移除。
(3).UnionFS
联合文件系统(Union File System),它可以把多个目录内容联合挂载到同一个目录下,而目录的物理位置是分开的。UnionFS可以把只读和可读写文件系统合并在一起,具有写时复制功能,允许只读文件系统的修改可以保存到可写文件系统当中。Docker之前使用的为AUFS(Advanced UnionFS),现为Overlay2。
Docker 中的每一个镜像都是由一系列只读的层组成的,Dockerfile 中的每一个命令都会在已有的只读层上创建一个新的层:
FROM ubuntu:15.04
COPY . /app
RUN make /app
CMD python /app/app.py
容器中的每一层都只对当前容器进行了非常小的修改,上述的 Dockerfile 文件会构建一个拥有四层 layer 的镜像
当镜像被 `docker run` 命令创建时就会在镜像的最上层添加一个可写的层,也就是容器层,所有对于运行时容器的修改其实都是对这个容器读写层的修改。容器和镜像的区别就在于,所有的镜像都是只读的,而每一个容器其实等于镜像加上一个可读写的层,也就是同一个镜像可以对应多个容器。
Kubernetes
Kubernetes,简称 K8s,其中8代指中间的8个字符。Kubernetes 项目庞大复杂,文章不能面面俱到,因此这个部分将向读者提供一种主线学习思路:
+ 为什么要Kubernetes?
+ 什么是Kubernetes?
+ Kubernetes提供的组件及适用场景
+ Kubernetes的架构
+ Kubernetes架构模块实现原理
有更多未交代或浅尝辄止的地方读者可以查阅文章或书籍深入研究。
1.为什么要Kubernetes
尽管Docker为容器化的应用程序提供了开放标准,但随着容器越来越多出现了一系列新问题:
- 单机不足以支持更多的容器
- 分布式环境下容器如何通信?
- 如何协调和调度这些容器?
- 如何在升级应用程序时不会中断服务?
- 如何监视应用程序的运行状况?
- 如何批量重新启动容器里的程序?
- …
Kubernetes应运而生。
2.什么是Kubernetes
Kubernetes 是一个全新的基于容器技术的分布式架构方案,这个方案虽然还很新,但却是 Google 十几年来大规模应用容器技术的经验积累和升华的重要成果,确切的说是 Google 一个久负盛名的内部使用的大规模集群管理系统——Borg的开源版本,其目的是实现资源管理的自动化以及跨数据中心的资源利用率最大化。
Kubernetes具有完备的集群管理能力,包括多层次的安全防护和准入机制、多租户应用支撑能力、透明的服务注册和服务发现机制、内建的智能负载均衡器、强大的故障发现和自我修复能力、服务滚动升级和在线扩容能力、可扩展的资源自动调度机制,以及多力度的资源配额管理能力。同时,Kubernetes 提供了完善的管理工具,这些工具涵盖了包括开发、部署测试、运维监控在内的各个环节,不仅是一个全新的基于容器技术的分布式架构解决方案,还是一个一站式的完备分布式系统开发和支撑平台。
3.Kubernetes术语
(1).Pod
Pod 是 Kubernetes 最重要的基本概念,可由多个容器(一般而言一个容器一个进程,不建议一个容器多个进程)组成,它是系统中资源分配和调度的最小单位。下图是 Pod 的组成示意图,其中有一个特殊的 Pause 容器:
Pause 容器的状态标识了一个 Pod 的状态,也就是代表了 Pod 的生命周期。另外 Pod 中其余容器共享 Pause 容器的命名空间,使得 Pod 内的容器能够共享 Pause 容器的IP,以及实现文件共享。以下是一个 Pod 的定义:
apiVersion: v1 # 分组和版本
kind: Pod # 资源类型
metadata:
name: myWeb # Pod名
labels:
app: myWeb # Pod的标签
spec:
containers:
- name: myWeb # 容器名
image: kubeguide/tomcat-app:v1 # 容器使用的镜像
ports:
- containerPort: 8080 # 容器监听的端口
env: # 容器内环境变量
- name: MYSQL_SERVICE_HOST
value: 'mysql'
- name: MYSQL_SERVICE_PORT
value: '3306'
resources: # 容器资源配置
requests: # 资源下限,m表示cpu配额的最小单位,为1/1000核
memory: "64Mi"
cpu: "250m"
limits: # 资源上限
memory: "128Mi"
cpu: "500m"
EndPoint : PodIP + containerPort,代表一个服务进程的对外通信地址。一个Pod也存在具有多个Endpoint的情 况,比如当我们把Tomcat定义为一个Pod时,可以对外暴露管理端口与服务端口这两个Endpoint。
(2).Label
Label 是 Kubernetes 系统中的一个核心概念,一个 Label 表示一个 key=value 的键值对,key、value的值由用户指定。Label可以被附加到各种资源对象上,例如 Node、Pod、Service、RC 等,一个资源对 象可以定义任意数量的 Label,同一个 Label 也可以被添加到任意数量的资源对象上。Label 通常在资源对象定义时确定,也可以在对象创建后动态添加或者删除。给一个资源对象定义了 Label 后,我们随后可以通过Label Selector 查询和筛选拥有这个 Label 的资源对象,来实现多维度的资源分组管理功能,以便灵活、方便地进行资源分配、调 度、配置、部署等管理工作。
Label Selector 当前有两种表达式,基于等式的和基于集合的:
- `name=redis-slave`: 匹配所有具有标签`name=redis-slave`的资源对象。
- `env!=production`: 匹配所有不具有标签`env=production`的资源对象。
- `name in(redis-master, redis-slave)`:`name=redis-master`或者`name=redis-slave`的资源对象。
- `name not in(php-frontend)`:匹配所有不具有标签`name=php-frontend`的资源对象。
以myWeb Pod为例:
apiVersion: v1 # 分组和版本
kind: Pod # 资源类型
metadata:
name: myWeb # Pod名
labels:
app: myWeb # Pod的标签
当一个 Service 的selector中指明了这个Pod时,该Pod就会与该Service绑定
apiVersion: v1
kind: Service
metadata:
name: myWeb
spec:
selector:
app: myWeb
ports:
- port: 8080
(3).Replication Controller
Replication Controller,简称RC,简单来说,它其实定义了一个期望的场景,即声明某种Pod的副本数量在任意时刻都符合某个预期值。
RC的定义包括如下几个部分:
- Pod期待的副本数量
- 用于筛选目标Pod的Label Selector
- 当Pod的副本数小于预期数量时,用于创建新Pod的模版(template)
apiVersion: v1
kind: ReplicationController
metadata:
name: frontend
spec:
replicas: 3 # Pod 副本数量
selector:
app: frontend
template: # Pod 模版
metadata:
labels:
app: frontend
spec:
containers:
- name: tomcat_demp
image: tomcat
ports:
- containerPort: 8080
当提交这个RC在集群中后,Controller Manager会定期巡检,确保目标Pod实例的数量等于RC的预期值,过多的数量会被停掉,少了则会创建补充。通过`kubectl scale`可以动态指定RC的预期副本数量。
目前,RC已升级为新概念——Replica Set(RS),两者当前唯一区别是,RS支持了基于集合的Label Selector,而RC只支持基于等式的Label Selector。RS很少单独使用,更多是被Deployment这个更高层的资源对象所使用,所以可以视作RS+Deployment将逐渐取代RC的作用。
(4).Deployment
Deployment和RC相似度超过90%,无论是作用、目的、Yaml定义还是具体命令行操作,所以可以将其看作是RC的升级。而 Deployment 相对于RC的一个最大区别是我们可以随时知道当前Pod“部署”的进度。实际上由于一个Pod的创建、调度、绑定节点及在目 标Node上启动对应的容器这一完整过程需要一定的时间,所以我们期待系统启动N个Pod副本的目标状态,实际上是一个连续变化的“部署过程”导致的最终状态。
apiVersion: v1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 3
selector:
matchLabels:
app: frontend
matchExpressions:
- {key: app, operator: In, values [frontend]}
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: tomcat_demp
image: tomcat
ports:
- containerPort: 8080
(5).Horizontal Pod Autoscaler
除了手动执行`kubectl scale`完成Pod的扩缩容之外,还可以通过Horizontal Pod Autoscaling(HPA)横向自动扩容来进行自动扩缩容。其原理是追踪分析目标Pod的负载变化情况,来确定是否需要针对性地调整目标Pod数量。当前,HPA有一下两种方式作为Pod负载的度量指标:
+ CPUUtilizationPercentage,目标Pod所有副本自身的CPU利用率的平均值。
+ 应用程序自定义的度量指标,比如服务在每秒内的相应请求数(TPS或QPS)
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: php-apache
namespace: default
spec:
maxReplicas: 3
minReplicas: 1
scaletargetRef:
kind: Deployment
name: php-apache
targetCPUUtilizationPercentage: 90
根据上边定义,当Pod副本的CPUUtilizationPercentage超过90%时就会出发自动扩容行为,数量约束为1~3个。
(6).StatefulSet
在Kubernetes系统中,Pod的管理对象RC、Deployment、DaemonSet 和Job都面向无状态的服务。但现实中有很多服务是有状态的,特别是 一些复杂的中间件集群,例如MySQL集群、MongoDB集群、Akka集 群、ZooKeeper集群等,这些应用集群有4个共同点。
1. 每个节点都有固定的身份ID,通过这个ID,集群中的成员可 以相互发现并通信。
2. 集群的规模是比较固定的,集群规模不能随意变动。
3. 集群中的每个节点都是有状态的,通常会持久化数据到永久
存储中。
4. 如果磁盘损坏,则集群里的某个节点无法正常运行,集群功 能受损。
因此,StatefulSet具有以下特点:
+ StatefulSet里的每个Pod都有稳定、唯一的网络标识,可以用来 发现集群内的其他成员。假设StatefulSet的名称为kafka,那么第1个Pod 叫kafka-0,第2个叫kafka-1,以此类推。
+ StatefulSet控制的Pod副本的启停顺序是受控的,操作第n个Pod 时,前n-1个Pod已经是运行且准备好的状态。
+ StatefulSet里的Pod采用稳定的持久化存储卷,通过PV或PVC来 实现,删除Pod时默认不会删除与StatefulSet相关的存储卷(为了保证数 据的安全)。
+ StatefulSet除了要与PV卷捆绑使用以存储Pod的状态数据,还要与 Headless Service配合使用。
> Headless Service : Headless Service与普通Service的关键区别在于, 它没有Cluster IP,如果解析Headless Service的DNS域名,则返回的是该 Service对应的全部Pod的Endpoint列表。
(7).Service
Service 在 Kubernetes 中定义了一个服务的访问入口地址,前段的应用(Pod)通过这个入口地址访问其背后的一组由 Pod 副本组成的集群实例,Service 与其后端 Pod 副本集群之间则是通过 Label Selector 来实现无缝对接的。
apiVersion: v1
kind: service
metadata:
name: tomcat_service
spec:
ports:
- port: 8080
name: service_port
- port: 8005
name: shutdown_port
selector:
app: backend
Service的负载均衡
在 Kubernetes 集群中,每个 Node 上会运行着 kube-proxy 组件,这其实就是一个负载均衡器,负责把对 Service 的请求转发到后端的某个 Pod 实例上,并在内部实现服务的负载均衡和绘画保持机制。其主要的实现就是每个 Service 在集群中都被分配了一个全局唯一的 Cluster IP,因此我们对 Service 的网络通信根据内部的负载均衡算法和会话机制,便能与 Pod 副本集群通信。
Service的服务发现
因为 Cluster IP在Service的整个声明周期内是固定的,所以在 Kubernetes 中,只需将 Service 的 Name 和 其 Cluster IP 做一个 DNS 域名映射即可解决。
(8).Volume
Volume 是 Pod 中能够被多个容器访问的共享目录,Kubernetes 中的 Volume 概念、用途、目的与 Docker 中的 Volumn 比较类似,但不等价。首先,其可被定义在 Pod 上,然后被 一个Pod里的多个容器挂载到具体的文件目录下;其次,Kubernetes 中的 Volume 与 Pod 的生命周期相同,但与容器的生命周期不相关,当容器终止或者重启时,Volume 中的数据也不会丢失。
template:
metadata:
labels:
app: frontend
spec:
volumes: # 声明可挂载的volume
- name: dataVol
emptyDir: {}
containers:
- name: tomcat_demo
image: tomcat
ports:
- containerPort: 8080
volumeMounts: # 将volume通过name挂载到容器内的/mydata-data目录
- mountPath: /mydata-data
name: dataVol
Kubernetes提供了非常丰富的 Volume 类型:
- emptyDir,它的初始内容为空,并且无须指定宿主机上对应的目录文件,因为这是 Kubernetes 自动分配的一个目录,当 Pod 从 Node 上移除 emptyDir 中的数据也会被永久删除,适用于临时数据。
- hostPath,hostPath为在Pod上挂载宿主机上的文件或目录,适用于持久化保存的数据,比如容器应用程序生成的日志文件。
- NFS,可使用NFS网络文件系统提供的共享目录存储数据。
- 其他云持久化盘等
(9).Persistent Volume
在使用虚拟机的情况下,我们通常会先定义一个网络存储,然后从中 划出一个“网盘”并挂接到虚拟机上。Persistent Volume(PV) 和与之相关联的 Persistent Volume Claim(PVC) 也起到了类似的作用。PV 可以被理解成Kubernetes 集群中的某个网络存储对应的一块存储,它与Volume类似,但有以下区别:
- PV 只能是网络存储,不属于任何 Node,但可以在每个 Node 上访问。
- PV 并不是被定义在 Pod 上的,而是独立于 Pod 之外定义的。
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv001
spec:
capacity:
storage: 5Gi
accessMods:
- ReadWriteOnce
nfs:
path: /somePath
server: xxx.xx.xx.x
> accessModes,有几种类型,1.ReadWriteOnce:读写权限,并且只能被单个Node挂载。 2. ReadOnlyMany:只读权限,允许被多个Node挂载。 3.ReadWriteMany:读写权限,允许被多个Node挂载。
如果 Pod 想申请某种类型的 PV,首先需要定义一个PersistentVolumeClaim对象,
apiVersion: v1
kind: PersistentVolumeClaim # 声明pvc
metadata:
name: pvc001
spec:
resources:
requests:
storage: 5Gi
accessMods:
- ReadWriteOnce
然后在 Pod 的 Volume 中引用 PVC 即可。
volumes:
- name: mypd
persistentVolumeClaim:
claimName: pvc001
PV有以下几种状态:
- Available:空闲
- Bound:已绑定到PVC
- Relead:对应PVC被删除,但PV还没被回收
- Faild: PV自动回收失败
(10).Namespace
Namespace在很多情况下用于实现多租户的资源隔离。分组的不同项目、小组或用户组,便于不同的分组在共享使用整个集群的资源的同时还能被分别管理。Kubernetes集群在启动后会创建一个名为default的Namespace,通过 kubectl可以查看:
(11).ConfigMap
我们知道,Docker 通过将程序、依赖库、数据及 配置文件“打包固化”到一个不变的镜像文件中的做法,解决了应用的部署的难题,但这同时带来了棘手的问题,即配置文件中的参数在运行期如何修改的问题。我们不可能在启动 Docker 容器后再修改容器里的配置 文件,然后用新的配置文件重启容器里的用户主进程。为了解决这个问题,Docker 提供了两种方式:
- 在运行时通过容器的环境变量来传递参数;
- 通过 Docker Volume 将容器外的配置文件映射到容器内。
在大多数情况下,后一种方式更合 适我们的系统,因为大多数应用通常从一个或多个配置文件中读取参数。但这种方式也有明显的缺陷:我们必须在目标主机上先创建好对应 配置文件,然后才能映射到容器里。上述缺陷在分布式情况下变得更为严重,因为无论采用哪种方式, 写入(修改)多台服务器上的某个指定文件,并确保这些文件保持一致,都是一个很难完成的目标。针对上述问题, Kubernetes给出了一个很巧妙的设计实现。
首先,把所有的配置项都当作 key-value 字符串,这些配置项可以 作为 Map 表中的一个项,整个Map的数据可以被持久化存储在 Kubernetes的 Etcd 数据库中,然后提供 API 以方便 Kubernetes 相关组件或 客户应用 CRUD 操作这些数据,上述专门用来保存配置参数的Map就是 Kubernetes ConfigMap 资源对象。Kubernetes 提供了一种内建机制,将存储在 etcd 中的 ConfigMap 通过 Volume 映射的方式变成目标 Pod 内的配置文件,不管目标 Pod 被调度到哪台服务器上,都会完成自动映射。进一步地,如果 ConfigMap 中的 key-value 数据被修改,则映射到 Pod 中的“配置文件”也会随之自动更新。
4.Kubernetes的架构
Kubernetes 由 Master 节点、 Node 节点以及外部的 ETCD 集群组成,集群的状态、资源对象、网络等信息存储在 ETCD 中,Mater 节点管控整个集群,包括通信、调度等,Node 节点为工作真正执行的节点,并向主节点报告。Master 节点由以下组件构成:
(1).Master组件
1. API Server —— 提供HTTP Rest接口,是所有资源增删改查和集群控制的唯一入口。(在集群中表现为名称是 kubernetes 的 service)。可以通过 Dashboard 的 UI 或 kubectl 工具来与其交互。
(1)集群管理的API入口;
(2)资源配额控制入口;
(3)提供完备的集群安全机制。
2. Controller Manager —— 资源对象的控制自动化中心。即监控Node,当故障时转移资源对象,自动修复集群到期望状态。
3. Scheduler —— 负责Pod的调度,调度到最优的Node。
(2).Node组件
1. kubelet —— 负责Pod内容器的创建、启停,并与Master密切协作实现集群管理(注册自己,汇报Node状态)。
2. kube-proxy —— 实现k8s Service的通信与负载均衡。
3. Docker Engine —— Docker引擎,负责本机容器的创建和管理。
5.Kubernetes架构模块实现原理
(1).API Server
Kubernetes API Server 通过一个名为 kube-apiserver 的进程提供服务,该进程运行在 Master 上。在默认情况下,kube-apiserver 进程在本机的8080端口(对应参数–insecure-port)提供 REST 服务。我们可以同时启动HTTPS安全端口(–secure-port=6443)来启动安全机制,加强 REST API 访问的安全性。
由于API Server是Kubernetes集群数据的唯一访问入口,因此安全性与高性能就成为API Server设计和实现的两大核心目标。通过采用 HTTPS安全传输通道与CA签名数字证书强制双向认证的方式,API Server的安全性得以保障。此外,为了更细粒度地控制用户或应用对 Kubernetes 资源对象的访问权限,Kubernetes启用了RBAC访问控制策略。Kubernetes的设计者综合运用以下方式来最大程度地保证API Server的性能。
- 1. API Server拥有大量高性能的底层代码。在API Server源码中 使用协程(Coroutine)+队列(Queue)这种轻量级的高性能并发代码, 使得单进程的API Server具备了超强的多核处理能力,从而以很快的速 度并发处理大量的请求。
- 2. 普通List接口结合异步Watch接口,不但完美解决了 Kubernetes中各种资源对象的高性能同步问题,也极大提升了Kubernetes 集群实时响应各种事件的灵敏度。
- 3. 采用了高性能的etcd数据库而非传统的关系数据库,不仅解决 了数据的可靠性问题,也极大提升了API Server数据访问层的性能。在 常见的公有云环境中,一个3节点的etcd集群在轻负载环境中处理一个请 求的时间可以低于1ms,在重负载环境中可以每秒处理超过30000个请求。
(2).安全认证
RBAC
Role-Based Access Control(RBAC),基于角色的访问控制。
4种资源对象
- Role
- RoleBinding
- ClusterRole
- ClusterRoleBinding
Role与ClusterRole
一个角色就是一组权限的集合,都是以许可形式,不存在拒绝的规则。Role作用于一个命名空间中,ClusterRole作用于整个集群。
apiVersion:rbac.authorization.k8s.io/v1beta1
kind:Role
metadata:
namespace: default #ClusterRole可以省略,毕竟是作用于整个集群
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pod"]
verbs: ["get","watch","list"]
RoleBinding 和 ClusterRoleBinding 是把 Role 和 ClusterRole 的权限绑定到 ServiceAccount上。
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: default
name: app-admin
subjects:
- kind: ServiceAccount
name: app
apiGroup: ""
namespace: default
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
ServiceAccount
Service Account也是一种账号,但它并不是给 Kubernetes 集群的用户 (系统管理员、运维人员、租户用户等)用的,而是给运行在Pod里的进程用的,它为Pod里的进程提供了必要的身份证明。在每个 Namespace 下都有一个名为 default 的默认 Service Account 对象,在这个 Service Account里面有一个名为 Tokens 的可以当作 Volume 被挂载到 Pod 里的 Secret,当Pod启动时,这个 Secret 会自动被挂载到 Pod 的指定目录下,用来协助完成Pod中的进程访问API Server时的身份鉴权。
(3).Controller Manager
下边介绍几种Controller Manager 的实现组件
ResourceQuota Controller
kubernetes 的配额管理使用过 Admission Control来控制的,提供了两种约束,LimitRanger和 ResourceQuota。LimitRanger 作用于 Pod 和 Container 之上(limit ,request),ResourceQuota 则作用于 Namespace。 资源配额,分三个层次:
- 容器级别,对容器的CPU、memory做限制
- Pod级别,对一个Pod内所有容器的可用资源做限制
- Namespace级别,为namespace做限制,包括:
- + pod数量
- + RC数量
- + Service数量
- + ResourceQuota数量
- + Secrete数量
- + PV数量
Namespace Controller
管理Namesoace创建删除.
Endpoints Controller
Endpoints表示一个service对应的所有Pod副本的访问地址,而Endpoints Controller就是负责生成和维护所有Endpoints对象的控制器。
> + 负责监听 Service 和对应 Pod 副本的变化,若Service被创建、更新、删除,则相应创建、更新、删除与 Service 同名的 Endpoints 对象。
> + EndPoints 对象被 Node 上的 kube-proxy 使用。
(4).Scheduler
Kubernetes Scheduler的作用是将待调度的Pod(API新创 建的Pod、Controller Manager为补足副本而创建的Pod等)按照特定的调 度算法和调度策略绑定(Binding)到集群中某个合适的Node上,并将绑定信息写入etcd中。Kubernetes Scheduler当前提供的默认调度流程分为以下两步。
1. 预选调度过程,即遍历所有目标Node,筛选出符合要求的候 选节点。为此,Kubernetes内置了多种预选策略(xxx Predicates)供用户选择。
2. 确定最优节点,在第1步的基础上,采用优选策略(xxx Priority)计算出每个候选节点的积分,积分最高者胜出。
(5).网络
Kubernetes 的网络利用了 Docker 的网络原理,并在此基础上实现了跨 Node 容器间的网络通信。
1. 同一个Node下Pod间通信模型:
2. 不同Node下Pod间的通信模型(CNI模型实现):
CNI提供了一种应用容器的插件化网络解决方案,定义对容器网络 进行操作和配置的规范,通过插件的形式对CNI接口进行实现,以Flannel举例,完成了Node间容器的通信模型。
可以看到,Flannel首先创建了一个名为flannel0的网桥,而且这个 网桥的一端连接docker0网桥,另一端连接一个叫作flanneld的服务进程。flanneld进程并不简单,它上连etcd,利用etcd来管理可分配的IP地 址段资源,同时监控etcd中每个Pod的实际地址,并在内存中建立了一 个Pod节点路由表;它下连docker0和物理网络,使用内存中的Pod节点 路由表,将docker0发给它的数据包包装起来,利用物理网络的连接将 数据包投递到目标flanneld上,从而完成Pod到Pod之间的直接地址通信。
服务发现
从Kubernetes 1.11版本开始,Kubernetes集群的DNS服务由CoreDNS 提供。CoreDNS是CNCF基金会的一个项目,是用Go语言实现的高性能、插件式、易扩展的DNS服务端。
结语
文章包含的内容说多不多,说少不少,但对于Docker、Kubernetes知识原理的小白来说是足够的,笔者按照自己的学习经验,以介绍为出发点,让大家更能了解相关技术原理,所以实操的部分较少。Kubernetes技术组件还是十分丰富的,文章有选择性地进行了介绍,感兴趣的读者可以再自行从官方或者书籍中学习了解。(《Kubernetes权威指南——从Docker到Kubernetes实践全接触》第四版)