Kubernetes容器
每个运行的容器都是可重复的; 包含依赖环境在内的标准,意味着无论你在哪里运行它都会得到相同的行为。
容器将应用程序从底层的主机设施中解耦。 这使得在不同的云或 OS 环境中部署更加容易。
Kubernetes 集群中的每个节点都会运行容器, 这些容器构成分配给该节点的 Pod。 单个 Pod 中的容器会在共同调度下,于同一位置运行在相同的节点上。
容器镜像
容器镜像是一个随时可以运行的软件包, 包含运行应用程序所需的一切:代码和它需要的所有运行时、应用程序和系统库,以及一些基本设置的默认值。
容器镜像(Image)所承载的是封装了应用程序及其所有软件依赖的二进制数据。 容器镜像是可执行的软件包,可以单独运行;该软件包对所处的运行时环境具有良定(Well Defined)的假定。
你通常会创建应用的容器镜像并将其推送到某仓库(Registry),然后在 Pod 中引用它。
容器旨在设计成无状态且不可变的: 你不应更改已经运行的容器的代码。如果有一个容器化的应用程序需要修改, 正确的流程是:先构建包含更改的新镜像,再基于新构建的镜像重新运行容器。
镜像名称
容器镜像通常会被赋予 pause、example/mycontainer 或者 kube-apiserver 这类的名称。 镜像名称也可以包含所在仓库的主机名。例如:fictional.registry.example/imagename。 还可以包含仓库的端口号,例如:fictional.registry.example:10443/imagename。
如果你不指定仓库的主机名,Kubernetes 认为你在使用 Docker 公共仓库。
在镜像名称之后,你可以添加一个标签(Tag)(与使用 docker 或 podman 等命令时的方式相同)。 使用标签能让你辨识同一镜像序列中的不同版本。
镜像标签可以包含小写字母、大写字母、数字、下划线(_)、句点(.)和连字符(-)。 关于在镜像标签中何处可以使用分隔字符(_、- 和 .)还有一些额外的规则。 如果你不指定标签,Kubernetes 认为你想使用标签 latest。
更新镜像
当你最初创建一个 Deployment、 StatefulSet、Pod 或者其他包含 Pod 模板的对象时,如果没有显式设定的话, Pod 中所有容器的默认镜像拉取策略是 IfNotPresent。这一策略会使得 kubelet 在镜像已经存在的情况下直接略过拉取镜像的操作。
镜像拉取策略
容器的 imagePullPolicy 和镜像的标签会影响 kubelet 尝试拉取(下载)指定的镜像。
以下列表包含了 imagePullPolicy 可以设置的值,以及这些值的效果:
- IfNotPresent
- 只有当镜像在本地不存在时才会拉取。
- Always
- 每当 kubelet 启动一个容器时,kubelet 会查询容器的镜像仓库, 将名称解析为一个镜像摘要。 如果 kubelet 有一个容器镜像,并且对应的摘要已在本地缓存,kubelet 就会使用其缓存的镜像; 否则,kubelet 就会使用解析后的摘要拉取镜像,并使用该镜像来启动容器。
- Never
- Kubelet 不会尝试获取镜像。如果镜像已经以某种方式存在本地, kubelet 会尝试启动容器;否则,会启动失败。 更多细节见提前拉取镜像。
只要能够可靠地访问镜像仓库,底层镜像提供者的缓存语义甚至可以使 imagePullPolicy: Always 高效。 你的容器运行时可以注意到节点上已经存在的镜像层,这样就不需要再次下载。
为了确保 Pod 总是使用相同版本的容器镜像,你可以指定镜像的摘要; 将 <image-name>:<tag> 替换为 <image-name>@<digest>,例如 image@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2。
当使用镜像标签时,如果镜像仓库修改了代码所对应的镜像标签,可能会出现新旧代码混杂在 Pod 中运行的情况。 镜像摘要唯一标识了镜像的特定版本,因此 Kubernetes 每次启动具有指定镜像名称和摘要的容器时,都会运行相同的代码。 通过摘要指定镜像可固定你运行的代码,这样镜像仓库的变化就不会导致版本的混杂。
有一些第三方的准入控制器 在创建 Pod(和 Pod 模板)时产生变更,这样运行的工作负载就是根据镜像摘要,而不是标签来定义的。 无论镜像仓库上的标签发生什么变化,你都想确保你所有的工作负载都运行相同的代码,那么指定镜像摘要会很有用。
默认镜像拉取策略
当你(或控制器)向 API 服务器提交一个新的 Pod 时,你的集群会在满足特定条件时设置 imagePullPolicy 字段:
- 如果你省略了 imagePullPolicy 字段,并且容器镜像的标签是 :latest, imagePullPolicy 会自动设置为 Always。
- 如果你省略了 imagePullPolicy 字段,并且没有指定容器镜像的标签, imagePullPolicy 会自动设置为 Always。
- 如果你省略了 imagePullPolicy 字段,并且为容器镜像指定了非 :latest 的标签, imagePullPolicy 就会自动设置为 IfNotPresent。
必要的镜像拉取
如果你想总是强制执行拉取,你可以使用下述的一中方式:
- 设置容器的 imagePullPolicy 为 Always。
- 省略 imagePullPolicy,并使用 :latest 作为镜像标签; 当你提交 Pod 时,Kubernetes 会将策略设置为 Always。
- 省略 imagePullPolicy 和镜像的标签; 当你提交 Pod 时,Kubernetes 会将策略设置为 Always。
- 启用准入控制器 AlwaysPullImages。
ImagePullBackOff
当 kubelet 使用容器运行时创建 Pod 时,容器可能因为 ImagePullBackOff 导致状态为 Waiting。
ImagePullBackOff 状态意味着容器无法启动, 因为 Kubernetes 无法拉取容器镜像(原因包括无效的镜像名称,或从私有仓库拉取而没有 imagePullSecret)。 BackOff 部分表示 Kubernetes 将继续尝试拉取镜像,并增加回退延迟。
Kubernetes 会增加每次尝试之间的延迟,直到达到编译限制,即 300 秒(5 分钟)。
带镜像索引的多架构镜像
除了提供二进制的镜像之外, 容器仓库也可以提供容器镜像索引。 镜像索引可以指向镜像的多个镜像清单, 提供特定于体系结构版本的容器。 这背后的理念是让你可以为镜像命名(例如:pause、example/mycontainer、kube-apiserver) 的同时,允许不同的系统基于它们所使用的机器体系结构取回正确的二进制镜像。
Kubernetes 自身通常在命名容器镜像时添加后缀 -$(ARCH)。 为了向前兼容,请在生成较老的镜像时也提供后缀。 这里的理念是为某镜像(如 pause)生成针对所有平台都适用的清单时, 生成 pause-amd64 这类镜像,以便较老的配置文件或者将镜像后缀硬编码到其中的 YAML 文件也能兼容。
使用私有仓库
从私有仓库读取镜像时可能需要密钥。 凭据可以用以下方式提供:
- 配置节点向私有仓库进行身份验证
- 所有 Pod 均可读取任何已配置的私有仓库
- 需要集群管理员配置节点
- kubelet 凭据提供程序,动态获取私有仓库的凭据
- kubelet 可以被配置为使用凭据提供程序 exec 插件来访问对应的私有镜像库
- 预拉镜像
- 所有 Pod 都可以使用节点上缓存的所有镜像
- 需要所有节点的 root 访问权限才能进行设置
- 在 Pod 中设置 ImagePullSecrets
- 只有提供自己密钥的 Pod 才能访问私有仓库
- 特定于厂商的扩展或者本地扩展
- 如果你在使用定制的节点配置,你(或者云平台提供商)可以实现让节点向容器仓库认证的机制
容器运行时
容器运行环境是负责运行容器的软件。
Kubernetes 支持许多容器运行环境,例如 containerd、 CRI-O 以及 Kubernetes CRI (容器运行环境接口) 的其他任何实现。
通常,你可以允许集群为一个 Pod 选择其默认的容器运行时。如果你需要在集群中使用多个容器运行时, 你可以为一个 Pod 指定 RuntimeClass, 以确保 Kubernetes 会使用特定的容器运行时来运行这些容器。
你还可以通过 RuntimeClass,使用相同的容器运行时,但使用不同设定的配置来运行不同的 Pod。
容器运行时类(Runtime Class)
RuntimeClass 是一个用于选择容器运行时配置的特性,容器运行时配置用于运行 Pod 中的容器。
动机
你可以在不同的 Pod 设置不同的 RuntimeClass,以提供性能与安全性之间的平衡。 例如,如果你的部分工作负载需要高级别的信息安全保证,你可以决定在调度这些 Pod 时尽量使它们在使用硬件虚拟化的容器运行时中运行。 这样,你将从这些不同运行时所提供的额外隔离中获益,代价是一些额外的开销。
你还可以使用 RuntimeClass 运行具有相同容器运行时但具有不同设置的 Pod。
设置
1. 在节点上配置 CRI 实现
RuntimeClass 的配置依赖于运行时接口(CRI)的实现。
所有这些配置都具有相应的 handler 名,并被 RuntimeClass 引用。 handler 必须是有效的 DNS 标签名。
2. 创建相应的 RuntimeClass 资源
在上面步骤 1 中,每个配置都需要有一个用于标识配置的 handler。 针对每个 handler 需要创建一个 RuntimeClass 对象。
RuntimeClass 资源当前只有两个重要的字段:RuntimeClass 名 (metadata.name) 和 handler (handler)。 对象定义如下所示:
# RuntimeClass 定义于 node.k8s.io API 组 apiVersion: node.k8s.io/v1 kind: RuntimeClass metadata: # 用来引用 RuntimeClass 的名字 # RuntimeClass 是一个集群层面的资源 name: myclass # 对应的 CRI 配置的名称 handler: myconfiguration
RuntimeClass 对象的名称必须是有效的 DNS 子域名。
使用说明
一旦完成集群中 RuntimeClasses 的配置, 你可以在 Pod spec 中指定 runtimeClassName 来使用它。例如:
apiVersion: v1 kind: Pod metadata: name: mypod spec: runtimeClassName: myclass # ...
这一设置会告诉 kubelet 使用所指的 RuntimeClass 来运行该 Pod。 如果所指的 RuntimeClass 不存在或者 CRI 无法运行相应的 handler, 那么 Pod 将会进入 Failed 终止阶段。 你可以查看相应的事件, 获取执行过程中的错误信息。
如果未指定 runtimeClassName,则将使用默认的 RuntimeHandler,相当于禁用 RuntimeClass 功能特性。
调度
通过为 RuntimeClass 指定 scheduling 字段, 你可以通过设置约束,确保运行该 RuntimeClass 的 Pod 被调度到支持该 RuntimeClass 的节点上。 如果未设置 scheduling,则假定所有节点均支持此 RuntimeClass。
为了确保 pod 会被调度到支持指定运行时的 node 上,每个 node 需要设置一个通用的 label 用于被 runtimeclass.scheduling.nodeSelector 挑选。在 admission 阶段,RuntimeClass 的 nodeSelector 将会与 Pod 的 nodeSelector 合并,取二者的交集。如果有冲突,Pod 将会被拒绝。
如果 node 需要阻止某些需要特定 RuntimeClass 的 Pod,可以在 tolerations 中指定。 与 nodeSelector 一样,tolerations 也在 admission 阶段与 Pod 的 tolerations 合并,取二者的并集。
Pod 开销
你可以指定与运行 Pod 相关的开销资源。声明开销即允许集群(包括调度器)在决策 Pod 和资源时将其考虑在内。
Pod 开销通过 RuntimeClass 的 overhead 字段定义。 通过使用这个字段,你可以指定使用该 RuntimeClass 运行 Pod 时的开销并确保 Kubernetes 将这些开销计算在内。
容器环境
Kubernetes 的容器环境给容器提供了几个重要的资源:
- 文件系统,其中包含一个镜像 和一个或多个的卷
- 容器自身的信息
- 集群中其他对象的信息
容器信息
一个容器的 hostname 是该容器运行所在的 Pod 的名称。通过 hostname 命令或者调用 libc 中的 gethostname 函数可以获取该名称。
Pod 名称和命名空间可以通过 下行 API 转换为环境变量。
Pod 定义中的用户所定义的环境变量也可在容器中使用,就像在 container 镜像中静态指定的任何环境变量一样。
集群信息
创建容器时正在运行的所有服务都可用作该容器的环境变量。 这里的服务仅限于新容器的 Pod 所在的名字空间中的服务,以及 Kubernetes 控制面的服务。
对于名为 foo 的服务,当映射到名为 bar 的容器时,定义了以下变量:
FOO_SERVICE_HOST=<其上服务正运行的主机> FOO_SERVICE_PORT=<其上服务正运行的端口>
服务具有专用的 IP 地址。如果启用了 DNS 插件, 可以在容器中通过 DNS 来访问服务。
容器生命周期回调
概述
类似于许多具有生命周期回调组件的编程语言框架,例如 Angular、Kubernetes 为容器提供了生命周期回调。 回调使容器能够了解其管理生命周期中的事件,并在执行相应的生命周期回调时运行在处理程序中实现的代码。
容器回调
有两个回调暴露给容器:
- PostStart
- 这个回调在容器被创建之后立即被执行。 但是,不能保证回调会在容器入口点(ENTRYPOINT)之前执行。 没有参数传递给处理程序。
- PreStop
- 在容器因 API 请求或者管理事件(诸如存活态探针、启动探针失败、资源抢占、资源竞争等) 而被终止之前,此回调会被调用。 如果容器已经处于已终止或者已完成状态,则对 preStop 回调的调用将失败。 在用来停止容器的 TERM 信号被发出之前,回调必须执行结束。 Pod 的终止宽限周期在 PreStop 回调被执行之前即开始计数, 所以无论回调函数的执行结果如何,容器最终都会在 Pod 的终止宽限期内被终止。 没有参数会被传递给处理程序。
回调处理程序的实现
容器可以通过实现和注册该回调的处理程序来访问该回调。 针对容器,有两种类型的回调处理程序可供实现:
- Exec - 在容器的 cgroups 和名字空间中执行特定的命令(例如 pre-stop.sh)。 命令所消耗的资源计入容器的资源消耗。
- HTTP - 对容器上的特定端点执行 HTTP 请求。
回调处理程序执行
当调用容器生命周期管理回调时,Kubernetes 管理系统根据回调动作执行其处理程序, httpGet 和 tcpSocket 在 kubelet 进程执行,而 exec 则由容器内执行。
回调处理程序调用在包含容器的 Pod 上下文中是同步的。 这意味着对于 PostStart 回调,容器入口点和回调异步触发。 但是,如果回调运行或挂起的时间太长,则容器无法达到 running 状态。
PreStop 回调并不会与停止容器的信号处理程序异步执行;回调必须在可以发送信号之前完成执行。 如果 PreStop 回调在执行期间停滞不前,Pod 的阶段会变成 Terminating并且一直处于该状态, 直到其 terminationGracePeriodSeconds 耗尽为止,这时 Pod 会被杀死。 这一宽限期是针对 PreStop 回调的执行时间及容器正常停止时间的总和而言的。 例如,如果 terminationGracePeriodSeconds 是 60,回调函数花了 55 秒钟完成执行, 而容器在收到信号之后花了 10 秒钟来正常结束,那么容器会在其能够正常结束之前即被杀死, 因为 terminationGracePeriodSeconds 的值小于后面两件事情所花费的总时间(55+10)。
如果 PostStart 或 PreStop 回调失败,它会杀死容器。
用户应该使他们的回调处理程序尽可能的轻量级。 但也需要考虑长时间运行的命令也很有用的情况,比如在停止容器之前保存状态。
回调递送保证
回调的递送应该是至少一次,这意味着对于任何给定的事件, 例如 PostStart 或 PreStop,回调可以被调用多次。 如何正确处理被多次调用的情况,是回调实现所要考虑的问题。
通常情况下,只会进行单次递送。 例如,如果 HTTP 回调接收器宕机,无法接收流量,则不会尝试重新发送。 然而,偶尔也会发生重复递送的可能。 例如,如果 kubelet 在发送回调的过程中重新启动,回调可能会在 kubelet 恢复后重新发送。
调试回调处理程序
回调处理程序的日志不会在 Pod 事件中公开。 如果处理程序由于某种原因失败,它将播放一个事件。 对于 PostStart,这是 FailedPostStartHook 事件,对于 PreStop,这是 FailedPreStopHook 事件。 要自己生成失败的 FailedPostStartHook 事件,请修改 lifecycle-events.yaml 文件将 postStart 命令更改为 “badcommand” 并应用它。 以下是通过运行 kubectl describe pod lifecycle-demo 后你看到的一些结果事件的示例输出:
Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 7s default-scheduler Successfully assigned default/ lifecycle-demo to ip-XXX-XXX-XX-XX.us-east-2... Normal Pulled 6s kubelet Successfully pulled image "nginx" in 229.604315ms Normal Pulling 4s (x2 over 6s) kubelet Pulling image "nginx" Normal Created 4s (x2 over 5s) kubelet Created container lifecycle-demo-container Normal Started 4s (x2 over 5s) kubelet Started container lifecycle-demo-container Warning FailedPostStartHook 4s (x2 over 5s) kubelet Exec lifecycle hook ([badcommand]) for Container "lifecycle-demo-container" in Pod "lifecycle-demo_default (30229739-9651-4e5a-9a32-a8f1688862db)" failed - error: command 'badcommand' exited with 126: , message: "OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec: \"badcommand\": executable file not found in $PATH: unknown\r\n" Normal Killing 4s (x2 over 5s) kubelet FailedPostStartHook Normal Pulled 4s kubelet Successfully pulled image "nginx" in 215.66395ms Warning BackOff 2s (x2 over 3s) kubelet Back-off restarting failed container