全部的 K8S学习笔记总目录,请点击查看。
以下主要是介绍数据持久化、健康检查、重启策略、镜像拉取策略、资源限制等常用设置、 confingmap&secret 以及 Pod 生命周期的配置的使用。
本节所有的内容都基于假设我们创建了一个mysql的数据库以及一个基于 Django 的 web 服务的 Pod 之后的操作。
Pod 数据持久化
如果我们因为各种原因删除了 Pod,由于 mysql 的数据都在容器内部,会造成数据丢失,因此需要数据进行持久化,数据持久化的数据会保存在对应的硬盘上,不随容器的关闭而删除。
使用 hostpath 挂载
因为保存在对应的宿主机上,所以需要使用 nodeSelector 定点,下面是 pod-with-volume.yaml 的文件内容。
1 | apiVersion: v1 |
有了上面的文件就可以进行创建了。
1 | # 若存在旧的同名服务,先删除掉 |
使用 PV+PVC连接分布式存储解决方案
hostpath 方式只能将 Pod 固定在特定的机器上,但是 k8s 支持网络存储,可以将数据存储在网络存储上,这样就可以实现 Pod 的迁移,下面是支持 PV+PVC 的协议方式。
- ceph
- glusterfs
- nfs
具体关于 PV+PVC 的用法会在之后介绍,具体目录可以参考 k8s 学习笔记。
服务健康检查
检测容器服务是否健康的手段,若不健康,会根据设置的重启策略(restartPolicy)进行操作,三种检测机制可以分别单独设置,若不设置,默认认为 Pod 是健康的。
以下是两种机制的介绍。
LivenessProbe 探针 (存活探针)
存活性探测:用于判断容器是否存活,即 Pod 是否为 running 状态,如果 LivenessProbe 探针探测到容器不健康,则 kubelet 将 kill 掉容器,并根据容器的重启策略是否重启,如果一个容器不包含 LivenessProbe 探针,则 Kubelet 认为容器的 LivenessProbe 探针的返回值永远成功。
1 | ... |
可配置的参数如下:
- initialDelaySeconds:容器启动后第一次执行探测是需要等待多少秒。
- periodSeconds:执行探测的频率。默认是10秒,最小1秒。
- timeoutSeconds:探测超时时间。默认1秒,最小1秒。
- successThreshold:探测失败后,最少连续探测成功多少次才被认定为成功。默认是1。
- failureThreshold:探测成功后,最少连续探测失败多少次。默认是3,最小值是1。
上面例子配置的情况,健康检查的逻辑为:K8S 将在 Pod 开始启动 20s(initialDelaySeconds) 后探测 Pod 内的 8000 端口是否可以建立 TCP 连接,并且每 15 秒钟探测一次,如果连续 3 次探测失败,则 kubelet 重启该容器
ReadinessProbe 探针 (就绪探针)
可用性探测:用于判断容器是否正常提供服务,即容器的 Ready 是否为 True,是否可以接收请求,如果 ReadinessProbe 探测失败,则容器的 Ready 将为 False, Endpoint Controller 控制器将此 Pod 的 Endpoint 从对应的 service 的 Endpoint 列表中移除,不再将任何请求调度此 Pod 上,直到下次探测成功。(剔除此 Pod 不参与接收请求不会将流量转发给此 Pod)。
1 | ... |
上面例子配置的情况,健康检查的逻辑为:K8S 将在 Pod 开始启动 10s(initialDelaySeconds) 后利用 HTTP 访问 8002 端口的 /blog/index/,如果超过 2s 或者返回码不在 200~399 内,则健康检查失败
StartupProbe 探针 (启动探针)
启动探针:kubelet 使用 startup probe 来指示容器中的应用是否已经启动。如果提供了启动探针,则所有其他探针都会被禁用,直到该探针成功为止。此探针主要用于判断容器是否已经启动完成,如果 StartupProbe 探针探测失败,则 kubelet 将 kill 掉容器,并根据容器的重启策略是否重启,如果一个容器不包含 StartupProbe 探针,则 Kubelet 认为容器的 StartupProbe 探针的返回值永远成功。
如果你的容器需要在启动期间加载大型数据、配置文件等操作,那么这个时候我们可以使用启动探针。
该探针在 Kubernetes v1.20 版本才变成稳定状态,对于所包含的容器需要较长时间才能启动就绪的 Pod 而言,启动探针是有用的。你不再需要配置一个较长的存活态探测时间间隔,只需要设置另一个独立的配置选项,对启动期间的容器执行探测,从而允许使用远远超出存活态时间间隔所允许的时长。
如果你的容器启动时间通常超出 initialDelaySeconds + failureThreshold × periodSeconds 总值,你应该设置一个启动探针,对存活态探针所使用的同一端点执行检查。 periodSeconds 的默认值是 10 秒,还应该将其failureThreshold 设置得足够高,以便容器有充足的时间完成启动,并且避免更改存活态探针所使用的默认值。 这一设置有助于减少死锁状况的发生。
1 | ... |
检查方式
probe 是由 kubelet 对容器执行的定期诊断,要执行诊断,kubelet 既可以在容器内执行代码,也可以发出一个网络请求。使用探针来检查容器有四种不同的方法。每个探针都必须准确定义为这四种机制中的一种:
exec:通过执行命令来检查服务是否正常,返回值为0则表示容器健康
1
2
3
4
5
6
7
8
9...
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 5
periodSeconds: 5
...httpGet方式:通过发送http请求检查服务是否正常,返回200-399状态码则表明容器健康,如上面的例子
tcpSocket:通过容器的IP和Port执行TCP检查,如果能够建立TCP连接,则表明容器健康。实际上就是检查容器的端口。
1
2
3
4
5
6
7
8...
livenessProbe:
tcpSocket:
port: 8002
initialDelaySeconds: 10 # 容器启动后第一次执行探测是需要等待多少秒
periodSeconds: 10 # 执行探测的频率
timeoutSeconds: 2 # 探测超时时间
...grpc:使用 gRPC 执行一个远程过程调用,目标应该实现 gRPC 健康检查。如果响应的状态是 SERVING,则认为诊断成功。不过需要注意 gRPC 探针是一个 Alpha 特性,只有在启用了 GRPCContainerProbe 特性门户时才可用。
1
2
3
4
5...
livenessProbe:
grpc:
port: 8002
...
另外,对于 HTTP 和 TCP 存活检测可以使用命名的 port
1 | ports: |
每次探测都将获得以下三种结果之一:
- Success(成功):容器通过了诊断。
- Failure(失败):容器未通过诊断。
- Unknown(未知):诊断失败,因此不会采取任何行动。
不过需要注意应尽量避免使用 TCP 探测,因为 TCP 探测实际就是 kubelet 向指定端口发送 TCP SYN 握手包,当端口被监听内核就会直接响应 ACK,探测就会成功。当程序死锁或 hang 住的情况,这些并不影响端口监听,所以探测结果还是健康,流量打到表面健康但实际不健康的 Pod 上,就无法处理请求,从而引发业务故障。
健康检查示例
比如上面的 pod-with-volume.yaml
文件,我们改造一下,添加上面的探针配置
1 | apiVersion: v1 |
上面的文件已经给 myblog-api 添加了探针配置, K8S 将在 Pod 开始启动 10s(initialDelaySeconds)后利用 HTTP 访问 8002 端口的 /blog/index/ ,如果超过2s或者返回码不在 200~399 内,则健康检查失败
Readiness 决定了 Service 是否将流量导入到该 Pod,Liveness 决定了容器是否需要被重启
重启策略
Pod 的重启策略(RestartPolicy)应用于 Pod 内的所有容器,并且仅在 Pod 所处的 Node 上由 kubelet 进行判断和重启操作。当某个容器异常退出或者健康检查失败时,kubelet 将根据 RestartPolicy 的设置来进行相应的操作。
Pod 的重启策略包括 Always、OnFailure 和 Never,默认值为 Always。
- Always:当容器进程退出后,由kubelet自动重启该容器;
- OnFailure:当容器终止运行且退出码不为0时,由kubelet自动重启该容器;
- Never:不论容器运行状态如何,kubelet都不会重启该容器。
演示重启策略:
1 | apiVersion: v1 |
- 使用默认的重启策略,即 restartPolicy: Always ,无论容器是否是正常退出,都会自动重启容器
- 使用OnFailure的策略时
- 如果把exit 1,去掉,即让容器的进程正常退出的话,则不会重启
- 只有非正常退出状态才会重启
- 使用Never时,退出了就不再重启
可以看出,若容器正常退出,Pod 的状态会是Completed,非正常退出,状态为CrashLoopBackOff
镜像拉取策略
1 | spec: |
设置镜像的拉取策略,默认为IfNotPresent
- Always,总是拉取镜像,即使本地有镜像也从仓库拉取
- IfNotPresent ,本地有则使用本地镜像,本地没有则去仓库拉取
- Never,只使用本地镜像,本地没有则报错
Pod 资源限制
为了保证充分利用集群资源,且确保重要容器在运行周期内能够分配到足够的资源稳定运行,因此平台需要具备 Pod 的资源限制的能力。 对于一个 Pod 来说,资源最基础的2个的指标就是:CPU和内存。
Kubernetes 提供了个采用 requests 和 limits 两种类型参数对资源进行预分配和使用限制。
将最开始的文件加上目前的所有字段,完整文件如下:
1 | apiVersion: v1 |
requests:
- 容器使用的最小资源需求,作用于 schedule 阶段,作为容器调度时资源分配的判断依赖
- 只有当前节点上可分配的资源量 >= request 时才允许将容器调度到该节点
- request 参数不限制容器的最大可使用资源
- requests.cpu 被转成 docker 的 –cpu-shares 参数,与 cgroup cpu.shares 功能相同 (无论宿主机有多少个 cpu 或者内核,–cpu-shares 选项都会按照比例分配 cpu 资源)
- requests.memory 没有对应的 docker 参数,仅作为 k8s 调度依据
limits:
- 容器能使用资源的最大值
- 设置为 0 表示对使用的资源不做限制, 可无限的使用
- 当 Pod 内存超过 limit 时,会被 oom
- 当 cpu 超过 limit 时,不会被 kill,但是会限制不超过 limit 值
- limits.cpu 会被转换成 docker 的 –cpu-quota 参数。与 cgroup cpu.cfs_quota_us 功能相同
- limits.memory 会被转换成 docker 的 –memory 参数。用来限制容器使用的最大内存
对于 CPU,我们知道计算机里 CPU 的资源是按 “时间片”
的方式来进行分配的,系统里的每一个操作都需要 CPU 的处理,所以,哪个任务要是申请的 CPU 时间片越多,那么它得到的 CPU 资源就越多。
然后还需要了解下 CGroup 里面对于 CPU 资源的单位换算:
1 | 1 CPU = 1000 millicpu(1 Core = 1000m) |
这里的 m
就是毫、毫核的意思,Kubernetes 集群中的每一个节点可以通过操作系统的命令来确认本节点的 CPU 内核数量,然后将这个数量乘以1000,得到的就是节点总 CPU 总毫数。比如一个节点有四核,那么该节点的 CPU 总毫量为 4000m。
docker run
命令和 CPU 限制相关的所有选项如下:
选项 | 描述 |
---|---|
--cpuset-cpus="" |
允许使用的 CPU 集,值可以为 0-3,0,1 |
-c ,--cpu-shares=0 |
CPU 共享权值(相对权重) |
cpu-period=0 |
限制 CPU CFS 的周期,范围从 100ms~1s,即[1000, 1000000] |
--cpu-quota=0 |
限制 CPU CFS 配额,必须不小于1ms,即 >= 1000,绝对限制 |
举例如下:
1 | docker run -it --cpu-period=50000 --cpu-quota=25000 ubuntu:16.04 /bin/bash |
将 CFS 调度的周期设为 50000,将容器在每个周期内的 CPU 配额设置为 25000,表示该容器每 50ms 可以得到 50% 的 CPU 运行时间。
注意:若内存使用超出限制,会引发系统的 OOM 机制,因 CPU 是可压缩资源,不会引发 Pod 退出或重建
配置优化
yaml 的环境变量中有很多敏感的信息,比如账号密码,直接暴漏在 yaml 文件中存在安全性问题。对于开发、测试、生产环境,由于配置均不同,每套环境部署的时候都要修改yaml,带来额外的开销。
k8s提供了两类资源,configMap
和 Secret
,可以用来实现业务配置的统一管理, 允许将配置文件与镜像文件分离,以使容器化的应用程序具有可移植性 。
configMap
通常用来管理应用的配置文件或者环境变量
1 | apiVersion: v1 |
创建并查看configmap:
1 | $ kubectl create -f configmap.yaml |
或者可以使用命令的方式,从文件中创建,比如:
1 | $ cat env-configs.txt |
Secret
管理敏感类的信息,默认会base64编码存储,有三种类型
- Service Account :用来访问 Kubernetes API ,由 Kubernetes 自动创建,并且会自动挂载到 Pod 的 /run/secrets/kubernetes.io/serviceaccount 目录中;创建 ServiceAccount 后,Pod 中指定 serviceAccount 后,自动创建该 ServiceAccount 对应的 secret;
- Opaque : base64编码格式的 Secret,用来存储密码、密钥等;
- kubernetes.io/dockerconfigjson :用来存储私有docker registry的认证信息。
secret的创建和configmap类似,可以通过文件或者命令的方式创建,比如:
1 | apiVersion: v1 |
创建并查看secret:
1 | $ kubectl create -f secret.yaml |
或者可以使用命令的方式,从文件中创建,比如:
1 | $ cat env-secret.txt |
使用示例
在配置中使用 configmap 和 secret:
1 | apiVersion: v1 |
使用场景
在部署不同的环境时,Pod 的 yaml 无须再变化,只需要在每套环境中维护一套 ConfigMap
和 Secret
即可。但是注意 configmap
和 secret
不能跨 namespace
使用,且更新后,Pod 内的 env 不会自动更新,重建后才会更新。
如何编写资源yaml
这里主要说一下如何编写资源yaml,主要是一些技巧。
从机器中已有的资源中提取yaml
1
2# 获取系统命名空间的所有资源,根据资源本身的yaml文件,可以进行修改
$ kubectl -n kube-system -o yaml get po,deployment,ds学会在官网查找, https://kubernetes.io/docs/home/
从kubernetes-api文档中查找, https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.16/#pod-v1-core
kubectl explain 查看具体字段含义
Pod 状态与生命周期
下图展示了一个 Pod 的完整生命周期过程,其中包含 Init Container
、Pod Hook
、健康检查
三个主要部分。
生命周期示意图:
启动和关闭示意:
Pod 的基本状态如下表所示,这些信息都是集群自动维护的:
状态值 | 描述 |
---|---|
Pending | API Server已经创建该 Pod,等待调度器调度 |
ContainerCreating | 拉取镜像启动容器中 |
Running | Pod 内容器均已创建,且至少有一个容器处于运行状态、正在启动状态或正在重启状态 |
Succeeded|Completed | Pod 内所有容器均已成功执行退出,且不再重启 |
Failed|Error | Pod 内所有容器均已退出,但至少有一个容器退出为失败状态 |
CrashLoopBackOff | Pod 内有容器启动失败,比如配置文件丢失导致主进程启动失败 |
Unknown | 由于某种原因无法获取该 Pod 的状态,可能由于网络通信不畅导致 |
初始化容器
Init Container:顾名思义,用于初始化工作,执行完就结束,可以理解为一次性任务。可以是一个或者多个,如果有多个的话,这些容器会按定义的顺序依次执行。我们知道一个 Pod 里面的所有容器是共享数据卷和 Network Namespace的,所以 Init Container 里面产生的数据可以被主容器使用到。从上面的 Pod 生命周期的图中可以看出初始化容器是独立与主容器之外的,只有所有的初始化容器执行完之后,主容器才会被启动。
含有以下特点:
- 支持大部分应用容器配置,但不支持健康检查
- 优先应用容器执行
主要用来实现如下目的:
- 验证业务应用依赖的组件是否均已启动
- 修改目录的权限
- 调整系统参数
- 初始化应用配置,比如集群的配置信息等
- 比如将 Pod 注册到注册中心、配置中心等
参考配置文件:
1 | apiVersion: v1 |
Pod Hook
我们知道 Pod 是 Kubernetes 集群中的最小单元,而 Pod 是由容器组成的,所以在讨论 Pod 的生命周期的时候我们可以先来讨论下容器的生命周期。
实际上 Kubernetes 为我们的容器提供了生命周期的钩子,就是我们说的 Pod Hook。Pod Hook 是由 kubelet 发起的,当容器中的进程启动前或者容器中的进程终止之前运行,这是包含在容器的生命周期之中。我们可以同时为 Pod 中的所有容器都配置 hook。
Kubernetes 为我们提供了两种钩子函数:
- PostStart:Kubernetes 在容器创建后立即发送 postStart 事件,然而 postStart 处理函数的调用不保证早于容器的入口点(entrypoint) 的执行。 postStart 处理函数与容器的代码是异步执行的,但 Kubernetes的容器管理逻辑会一直阻塞等待 postStart 处理函数执行完毕。只有 postStart 处理函数执行完毕,容器的状态才会变成 RUNNING 。
- PreStop:这个钩子在容器终止之前立即被调用。它是阻塞的,所以它必须在删除容器的调用发出之前完成。主要用于优雅关闭应用程序、通知其他系统等。
PostStart用的不是很多,而PreStop用的相对很多.
Kubernetes 只有在 Pod 结束(Terminated)的时候才会发送 preStop 事件,这意味着在 Pod 完成(Completed)时 preStop 的事件处理逻辑不会被触发
验证Pod生命周期
编写以下 yaml 文件,包括 initContainer、lifecycle、readinessProbe、livenessProbe、preStop、postStart的配置。
1 | apiVersion: v1 |
创建 Pod 测试:
1 | $ kubectl create -f pod-lifecycle.yaml |
上面的输出没有 PRE-STOP
是因为必须主动杀掉 Pod 才会触发 pre-stop hook
,如果是 Pod 自己 Down 掉,则不会执行 pre-stop hook
,且杀掉Pod进程前,进程必须是正常运行状态,否则不会执行pre-stop钩子。
Hook 调用的日志没有暴露给 Pod,如果处理程序由于某种原因失败,它将产生一个事件。对于 PostStart,这是FailedPostStartHook 事件,对于 PreStop,是 FailedPreStopHook 事件,我们可以通过运行 kubectl -n test describe pod pod-lifecycle 来查看事件。
Pod 的终止
Pod 的终止有两种方式:正常终止和强制终止。
正常终止
终止流程如下:
- 用户发出删除 Pod 指令,Pod 被删除,状态变为 Terminating,从 API 层面看就是 Pod metadata 中的deletionTimestamp 字段会被标记上删除时间。
- kube-proxy watch 到了就开始更新转发规则,将 Pod 从 service 的 endpoints 列表中摘除掉,新的流量不再转发到该 Pod。
- kubelet watch 到了就开始销毁 Pod。
- 如果 Pod 中有 container 配置了 preStop Hook ,则 Pod 被标记为 Terminating 状态时,以同步的方式启动执行;若宽限期结束后 preStop 仍未执行结束,则会额外获得一个 2 秒的小宽限期。
- 发送 SIGTERM 信号给容器内主进程以通知容器进程开始优雅停止。
- 等待 container 中的主进程完全停止,如果在宽限期结束后还未完全停止,就发送 SIGKILL 信号将其强制杀死。
- 所有容器进程终止,清理 Pod 资源。
- 通知 APIServer Pod 销毁完成,完成 Pod 删除。
对于长连接类型的业务,比如游戏类应用,我们可以将 terminationGracePeriodSeconds 设置大一点,避免过早的被 SIGKILL 杀死,但是具体多长时间是不好预估的,所以最好在业务层面进行优化。比如 Pod 销毁时的优雅终止逻辑里面主动通知下客户端,让客户端连到新的后端,然后客户端来保证这两个连接的平滑切换。等旧 Pod 上所有客户端连接都连切换到了新 Pod 上,才最终退出。
强制终止 Pod
默认情况下,所有的删除操作都会有 30 秒钟的宽限期限。kubectl delete 命令支持 grace-period=<seconds>
选项,允许你重载默认值,设定自己希望的期限值。
将宽限期限强制设置为 0 意味着立即从 APIServer 删除 Pod,如果 Pod 仍然运行于某节点上,强制删除操作会触发 kubelet 立即执行清理操作。 注意:你必须在设置 grace-period=0 的同时额外设置 force 参数才能发起强制删除请求。
执行强制删除操作时,APIServer 不再等待来自 kubelet 关于 Pod 已经在原来运行的节点上终止执行的确认消息。APIServer 直接删除 Pod 对象,这样新的与之同名的 Pod 即可以被创建。在节点侧,被设置为立即终止的 Pod 仍然会在被强行杀死之前获得一点点的宽限时间。
对于已失败的 Pod 而言,对应的 API 对象仍然会保留在集群的 API 服务器上,直到用户或者控制器进程显式地将其删除。
控制器组件会在 Pod 个数超出所配置的阈值 (根据 kube-controller-manager 的 terminated-pod-gc-threshold 设置)时删除已终止的 Pod(phase 值为 Succeeded 或 Failed)。这一行为会避免随着时间不断创建和终止 Pod 而引起的资源泄露问题。
当然,这种方法伴随着一些风险,所以用强制删除 Pod 的命令需要慎用。
业务代码处理 SIGTERM 信信号
要实现优雅退出,我们需要业务代码得支持下优雅退出的逻辑,在业务代码里面处理下 SIGTERM 信号,一般主要逻辑就是”排水”,即等待存量的任务或连接完全结束,再退出进程。下面我们给出几种常用编程语言实现优雅退出的示例。
Golang
1 | package main |
Python
1 | import signal, time, os |
Node JS
1 | process.on('SIGTERM', () => { |
Java
1 | import sun.misc.Signal; |
1 | import sun.misc.Signal; |
SHELL
1 |
|
收不到 SIGTERM 信信号
上面我们给出了几种常见的捕捉 SIGTERM 信号的代码,然后我们就可以执行停止逻辑以实现优雅退出了。在 Kubernetes环境中,业务发版时经常会对工作负载进行滚动更新,当旧版本 Pod 被删除时,K8s 会对 Pod 中各个容器中的主进程发送 SIGTERM 信号,当达到退出宽限期后进程还未完全停止的话,就会发送 SIGKILL 信号将其强制杀死。但是有的场景下在 Kubernetes 环境中实际运行时,有时候可能会发现在滚动更新时,我们业务的优雅终止逻辑并没有被执行,现象是在等了较长时间后,业务进程直接被 SIGKILL 强制杀死了。
这是什么原因造成的呢?通常情况下这都是因为容器启动入口使用了 shell,比如使用了类似 /bin/sh -c my-app 这样的启动入口。或者使用 /entrypoint.sh 这样的脚本文件作为入口,在脚本中再启动业务进程,比如下面的entrypoint.sh 文件:
1 |
|
这就可能就会导致容器内的业务进程收不到 SIGTERM 信号,原因是:
- 容器主进程是 shell,业务进程是在 shell 中启动的,变成了 shell 进程的子进程了。
- shell 进程默认会忽略 SIGTERM 信号,父进程不退出,子进程也不会退出,导致业务逻辑不会触发停止逻辑。
- 等到 k8s 的 terminationGracePeriodSeconds 时间(默认30秒)到了,shell 进程被 SIGKILL 强制杀死,子进程也会被 SIGKILL 强制杀死。
怎么解决这个问题呢?解决方法有几种:
使用 exec 启动
在 shell 中启动二进制的命令前加一个 exec 命令即可让该二进制启动的进程代替当前 shell 进程,即让新启动的进程成为主进程:
1
2
3
exec python /app/my-app.py然后业务进程就可以正常接收所有信号了,实现优雅退出当然也可以了。
多进程场景
通常我们一个容器只会有一个进程,但有些时候我们不得不启动多个进程,比如从传统部署迁移到 Kubernetes 的过渡期间,使用了富容器,即单个容器中需要启动多个业务进程,这时候我们可以通过 shell 来启动,但却无法使用上面的 exec 方式来传递信号了,因为 exec 只能让一个进程替代当前 shell 成为主进程。
这个时候我们可以在 shell 中使用 trap 来捕获信号,当收到信号后触发回调函数来将信号通过 kill 命令传递给业务进程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/bin/app1 & pid1="$!" # 启动第一个业务进程并记录 pid
echo "app1 started with pid $pid1"
/bin/app2 & pid2="$!" # 启动第二个业务进程并记录 pid
echo "app2 started with pid $pid2"
handle_sigterm() {
echo "[INFO] Received SIGTERM"
kill -SIGTERM $pid1 $pid2 # 传递 SIGTERM 给业务进程
wait $pid1 $pid2 # 等待所有业务进程完全终止
}
trap handle_sigterm SIGTERM # 捕获 SIGTERM 信号并回调 handle_sigterm 函数
wait # 等待回调执行完,主进程再退出使用 tini
前面一种方案实际是用脚本实现了一个极简的 init 系统 (或 supervisor ) 来管理所有子进程,只不过它的逻辑很简陋,仅仅简单的透传指定信号给子进程,其实社区有更完善的方案,dumb-init (https://github.com/Yelp/dumb-init)和 tini (https://github.com/krallin/tini)都可以作为 init 进程,作为主进程 (PID 1) 在容器中启动,然后它再运行 shell 来执行我们指定的脚本 (shell 作为子进程),shell 中启动的业务进程也成为它的子进程,当它收到信号时会将其传递给所有的子进程,从而也能完美解决 shell 无法传递信号问题,并且还有回收僵尸进程的能力,这也是我们强烈推荐的一种方式。
如下所示是一个以 dumb-init 制作镜像的 Dockerfile 示例:
1
2
3
4
5
6
7FROM ubuntu:22.04
RUN apt-get update && apt-get install -y dumb-init
ADD start.sh /
ADD app1 /bin/app1
ADD app2 /bin/app2
ENTRYPOINT ["dumb-init", "*"]
CMD ["/start.sh"]下面则是以 tini 为例制作镜像的 Dockerfile 示例:
1
2
3
4
5
6
7FROM ubuntu:22.04
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /tini /entrypoint.sh
ENTRYPOINT ["/tini", "*"]
CMD ["/start.sh"]此时 start.sh 脚本中当然也可以是多个进程:
1
2
3
4
/bin/app1 &
/bin/app2 &
wait
优雅退出是 K8s 中非常重要的一个特性,对于实现应用零宕机滚动更新非常重要。
小结
- 实现 k8s 平台与特定的容器运行时解耦,提供更加灵活的业务部署方式,引入了 Pod 概念
- k8s 使用 yaml 格式定义资源文件,yaml比json更加简洁
- 通过kubectl apply | get | exec | logs | delete 等操作k8s资源,必须指定 namespace
- 每启动一个 Pod ,为了实现网络空间共享,会先创建 Infra 容器(也就是 pause 容器),并把其他容器网络加入该容器,实现Pod内所有容器使用同一个网络空间
- 通过 livenessProbe和readinessProbe 实现 Pod 的存活性和就绪健康检查
- 通过 requests 和 limit 分别限定容器初始资源申请与最高上限资源申请
- Pod 通过 initContainer 和 lifecycle 分别来执行初始化、Pod 启动和删除时候的操作,使得功能更加全面和灵活
- 编写 yaml 讲究方法,学习 k8s,养成从官方网站查询知识的习惯
那么只使用 Pod ,会有哪些问题呢?
- 业务应用启动多个副本怎么做?
- Pod重建后IP会变化,外部如何访问Pod服务?
- 运行业务Pod的某个节点挂了,可以自动帮我把Pod转移到集群中的可用节点启动起来?
- 我的业务应用功能是收集节点监控数据,需要把Pod运行在k8集群的各个节点上?
为了解决上面的问题,k8s 需要使用Controller来管理Pod,比如Deployment、StatefulSet、DaemonSet、Job、CronJob等,下面就会详细介绍其中的几类。