Kubernetes 是现代最常用的容器编排系统之一。主要云提供商(AWS、Azure、GCP、DigitalOcean)已采用它并开发了托管服务。因此,听到 Kubernetes 或 K8s 用于管理和扩展基于容器的应用程序的名字已不再是新闻。
但使用 Kubernetes 不仅仅是设置它并向其部署 pod。Kubernetes 中许多使应用程序更具弹性和高可用性的丰富功能不仅仅是一件事,而是不同流程和配置的组合。从如何在不停机的情况下部署应用程序,到调度Pod 以确保它们在节点之间正确分布。这些是我们将在本文中讨论的配置和技术的要点:
- Pod 副本
- Pod反亲和力
- 部署策略
- 优雅终止
- 探针
- 资源分配
- 缩放
- Pod 中断预算
我们要讨论的第一系列方法是:
Pod 副本
如果为工作负载配置了单个 Pod,并且该 Pod 不可用,则应用程序将自动变得不可访问。为了确保 Kubernetes 中工作负载的高可用性,建议至少有两个 pod。这意味着,如果一个 Pod 出现问题(可能是代码级问题、基础设施问题或网络问题)。这些问题很可能不会影响其他 Pod。在某些情况下,一个 Pod 可以位于三个副本中,从而提供更高级别的可用性。部署和有状态集是可以从此配置中受益的资源。默认情况下,守护程序集部署在集群上可用的节点数量上。以下代码是具有两个副本的部署示例。
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 2
spec:
containers:
image: nginx: 1.14.2
上面的代码是具有 2 个副本的部署示例,这意味着它将创建同一应用程序的两个 pod。
虽然这种方法在创建 Pod 的多个副本方面很好,但它仍然需要真正可用。原因是 Pod 副本可以在节点内创建。在不明确告诉 Kubernetes 调度程序的情况下,它会决定将 pod 调度到哪里。可以配置 pod 的三个副本,并且所有三个副本都调度在单个节点中。但没问题,有一个解决方案,我们将在下一节 PodAntiAffinity 中讨论。
PodAntiAffinity Pod反亲和力
虽然多个副本可确保我们的应用程序具有副本,但重复 Pod 的分布会在 Pod 之上创建另一层保证。Pod 亲和性配置的作用是与 Kubernetes 沟通它应该如何分配 Pod 的调度。
例如,如果我们有一个包含三个节点的集群,我们可以决定将 Pod 副本分布在三个节点上。除了确保应用程序在节点中断期间仍然可用之外,它在节点耗尽或节点更换操作期间也非常有帮助。节点替换操作会导致节点在短时间内不可用。通过replicas + pod antiaffinity,我们可以保证即使一个节点和该节点中的 pod 不可用,其他节点中的 pod 也将确保用户可以访问应用程序。在节点更换或升级期间,用户永远不会遇到中断。以下代码是 podantiaffinity 配置示例:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
spec:
selector:
matchLabels:
app: web-store
replicas: 3
template:
metadata:
labels:
app: web-store
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web-store
topologyKey: "kubernetes.io/hostname"
containers:
- name: web-app
image: nginx:1.16-alpine
上面的 Kubernetes 清单确保当有多个 pod 副本时,它不允许将两个 pod 调度到同一节点上。相反,它会将其分布到集群中的节点上。它还可以配置为将 Pod 分布到 Pod 节点所在的区域。
例如,当在 Amazon EKS 上创建节点时。每个节点都有一组附加到它的标签。这些标签包含有关节点的相关信息,包括实例类型、AMI ID、区域和创建节点的可用区。可以为该标签配置反关联性,以确保 Pod 跨可用区传播。该区域的标签如下所示:topology.kubernetes.io/zone=eu-west-1b
这意味着节点当前运行的区域是eu-west-1b。
我们已经能够确定如何确保复制Pod,并且反亲和力有助于确保Pod的正确传播。那么在部署过程中以及部署新的 Pod 时,如何确保不会破坏已经运行的 Pod?因此就有了部署策略的概念。
部署策略
部署期间应用的策略或技术决定了 Pod 在部署期间是否仍然可用,或者是否会完全关闭并恢复。我们的目标是确保用户不会注意到任何事情,并且每个新的更改都会顺利、无缝地发生。告诉用户“我们目前将在 x 时间到 y 时间之间进行维护升级”的日子已经一去不复返了。Kubernetes 部署策略允许进行切换,而不会导致 Pod 运行和应用程序使用出现故障。
Kubernetes 有多种部署策略,但我们这里的重点是滚动更新,这是允许增量部署的策略。它用新的 Pod 替换旧的 Pod,并在删除旧的 Pod 之前首先确认 Pod 已准备好开始接收流量(这是与探针配合完成的,我们将在接下来的两个主题中讨论)。副本还可以更有效地确保部署过程中 Pod 和应用程序的更高可用性。以下是滚动更新的基本代码部署
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 5
selector:
matchLabels:
app: nginx
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: nginx
spec:
containers:
— name: nginx
image: nginx:1.14.2
ports:
— containerPort: 80
在上面的配置中,第 12 行到第 15 行是配置滚动更新的位置。滚动更新还允许确定更新期间应不可用的 Pod 数量。在上面的配置中,maxSurge和maxUnavailable是用于确定部署期间不可用的 Pod 数量的参数。从上面的配置来看,滚动部署过程会一次部署一个 pod,一次删除一个 pod,直到所有旧 pod 都被新 pod 替换。
滚动更新对于每个 Pod 滚动更新非常有用。但 Pod 如何终止也非常重要。如果 Pod 突然停止,可能会导致服务中断,下一节将解释如何在创建新 Pod 之前管理 Pod 关闭。
优雅终止
这描述了如何使用 SIGTERM 优雅地终止 pod。它是在应用程序级别和基础设施级别上完成的。应用程序应该准备好接收关闭信号,以便它可以正常停止接收流量、停止数据库连接以及应用程序正在执行的所有其他操作。默认情况下,Kubernetes 等待 30 秒以允许进程处理 SIGTERM。但如果应用需要较长时间才能关闭,新应用才能完全部署并准备好接收流量,则可以将其更改为更长的时间。允许您更新允许终止 Pod 的时间的参数是TerminationGracePeriodSeconds。以下清单显示了如何为关闭时间长于默认值的应用程序实现终止GracePeriodSeconds的示例
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
containers:
- name: nginx-container
image: nginx:latest
terminationGracePeriodSeconds: 60
在上面的示例代码片段中,最后一行将 pod 配置为在 60 秒内终止,这比默认的 Kubernetes 时间长。这不仅可以确保新 Pod 已部署、运行并已接收流量,还可以确保用户不会遇到任何停机时间,因为在同一时刻,新旧 Pod 都会接收流量,并且旧 Pod 将被终止Kubernetes 让新的 Pod 继续运行并接收流量。但我们如何知道应用程序运行良好并准备好接收流量呢?这就是验证集群内 Pod 可用性的另一种技术的用武之地。这些技术称为探针。
探针
来自“探测”一词。谷歌对“调查”一词的最初定义是“对犯罪或其他事项进行彻底调查”。让我们离开“犯罪”部分,面对“其他事项”部分。因此,探测器只是进行调查、检查和验证。Kubernetes 探针也做同样的事情。
Kubernetes 中的探针主要分为三种类型;准备情况、活跃度和启动情况。它们的正式名称是 Readiness Probe、Liveness Probe 和 Startup Probe。这三个是用来验证的;如果 Pod/容器已准备好接收流量(就绪),如果 Pod/容器仍在运行且尚未进入睡眠状态(活动),以及 Pod/容器是否已成功启动(启动)。有了这三个,我们就可以知道应用程序是否已准备好运行,然后终止旧的 Pod/容器,如上面的“优雅终止”部分所述。
这些探针通过根据应用程序对其进行一些特定配置来实现这一点。出于示例目的,最基本的实现是 API。我们配置一个运行状况检查端点,该端点应返回 HTTP 状态代码 200。探针通过间歇性地向容器发送 HTTP 请求并返回响应来检查这些端点。如果请求成功,则启动和准备状态将停止,而活性将继续运行以保持 Pod/容器处于活动状态。如果由于任何原因探测失败,它会将容器标记为不健康,从而停止部署过程。这将不允许有故障的 Pod 接收流量,从而确保用户不会注意到应用程序中出现故障。它将确保旧的/现有的 Pod 继续接收流量。以下清单是运行状况检查路径为“ /health ”的应用程序的示例,探针配置为检查应用程序是否运行状况良好并准备好接收流量
apiVersion: v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
containers:
- name: myapp-container
image: myapp-app:latest
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 15
periodSeconds: 20
startupProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 10
periodSeconds: 5
探针还具有允许您配置探针频率以及频率之间的时间间隔的参数,这就是initialDelaySeconds和periodSeconds的作用。列表中的下一项是资源分配。
资源分配/管理
不向 Pod 分配任何特定资源意味着所有 Pod 都可以消耗任意数量的 CPU 或内存。这意味着需要大量内存的 Pod 可能会消耗现有节点中的所有内存,从而导致其他 Pod 挨饿。这种情况可能会导致不相关的应用程序变得不稳定,因为共享资源没有被故意分配给特定的 Pod。因此,始终为 Pod 分配资源非常重要。Kubernetes 部署中的配置是请求和限制配置。请求是应用程序工作或运行所需的最低限度,限制是应用程序应使用的最高限度,不得超过该限度。请求和限制在 Pod 运行时应消耗的 CPU 和内存上创建了错误/范围。以下代码是为部署配置的请求和限制的示例。
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
containers:
- name: nginx-container
image: nginx
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
从上面的配置来看,意味着 Pod 至少可以以 64MB 内存和 250m CPU 启动,并且不得超过 128MB 内存和 500m CPU。这可确保应用程序被封装起来,并且在任何给定时间都不会使用超过这些资源的资源。
即使将资源分配给 pod,我们也需要确保当 pod 需要更多资源时它不会饥饿,而是以不会扭曲现有 pod 的方式策略性地分配资源。下一个主题缩放将说明这一点。
缩放
扩展是确保 Pod/容器高可用性的另一种有效方法。扩展有两个主要类别:内部(基于资源分配的 Pod/容器的扩展)和外部(连接到集群的节点的扩展)。内部缩放有两种主要类型:VerticalPodAutoscaler 和 HorizontalPodAutoscaler。该概念借鉴了垂直扩展或纵向扩展(基于现有机器的资源进行扩展)和水平扩展或横向扩展(基于服务器数量进行扩展)的原始含义。我们将分析不同的内部缩放方法
VerticalPodAutoscaler 或 VPA
如前所述 ,这会根据当前消耗增加资源,以确保 Pod 不会因高资源消耗(例如Pod 内存不足时经常出现的OOM错误)而变得不可用。要使用VerticalPodAutoscaler ,必须首先将其安装 在 Kubernetes 中。使用以下命令安装VerticalPodAutoscaler。
kubectl apply -f https://github.com/kubernetes/autoscaler/releases/download/vertical-pod-autoscaler-0.10.0/vertical-pod-autoscaler-crd.yaml
kubectl apply -f https://github.com/kubernetes/autoscaler/releases/download/vertical-pod-autoscaler-0.10.0/vertical-pod-autoscaler-deployment.yaml
下一步是将VPA配置附加到特定部署。以下代码显示如何为特定部署配置VPA 。
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: nginx-vpa
spec:
targetRef:
apiVersion: "apps/v1"
kind: "Deployment"
name: "nginx-deployment"
updatePolicy:
updateMode: "On"
从上面的代码来看,应用的部署 VPA 称为nginx-deployment。当 Pod 需要更多资源时,它将根据 Pod 资源分配中的配置来增加资源。这种扩展技术对于不需要副本或副本的后台进程和作业非常有价值。
HorizontalPodAutoscaler 或 HPA
HorizontalPodAutoscaler就像VerticalPodAutoscaler一样,涉及在集群上安装并配置它。它将自身附加到部署并读取 Pod 的指标。当为部署配置HorizontalPodAutoscaler时,它会在pod 的限制中配置内存和 CPU 耗尽时增加内存和 CPU 。它增加了内存/CPU 以确保 pod 不会变得不稳定。要安装HorizontalPodAutoscaler,您必须首先安装metrics-server。可以使用以下命令安装
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
安装metrics-server后,我们可以为特定部署配置HPA,如以下Kubernetes清单所示。
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: nginx-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: nginx-deployment
minReplicas: 2
maxReplicas: 5
metrics:
- type: Resource
resource:
name: cpu
targetAverageUtilization: 50
此配置执行以下操作:
- 确保部署nginx-deployment中 pod 的最小数量为 2,最大数量为 5。仅在以下情况下增加副本:
- Pod CPU 利用率超过 50%,
- 它通过检索 Pod 的 CPU 利用率并将其与为 Pod 配置的 CPU 资源限制进行比较来实现此目的。
这种扩展技术对于 API 来说非常有价值。
接下来是外部缩放。外部扩展仅涉及在需要时向 Kubernetes 集群添加更多节点。该领域的主要行业领导者是 ClusterAutoscaler 和 Karpenter。
ClusterAutoscaler
原本是 唯一按需增加节点数量的工具。ClusterAutoscaler受到许多托管 Kubernetes 提供商的支持。当 Pod 无法调度时,它只是根据节点池(需要新节点时应创建的虚拟机的大小)配置添加一个新节点。为此,需要在 Kubernetes 集群中安装并配置ClusterAutoscaler 。以下代码用于部署ClusterAutoscaler。
helm repo add cluster-autoscaler https://kubernetes.github.io/autoscaler
helm install my-cluster-autoscaler cluster-autoscaler/cluster-autoscaler --version 9.34.1
注意:确保根据您的云提供商配置额外的权限参数,请阅读此处的文档。
Karpenter是ClusterAutoscaler的更新且成本优化版本,下一节将介绍Karpenter 的工作原理以及如何部署它。
Karpenter
Karpenter 是一个开源 Kubernetes 节点自动缩放服务。该项目最初从 AWS 开始,严格用于 Amazon EKS。但其他 Kubernetes 提供商(例如 Azure Kubernetes 服务)也已加入。请在此处查看有关在 Azure Kubernetes 上部署 Karpenter 的更多信息。
与基于节点池配置创建新节点的 ClusterAutoscaler 不同,Karpenter 分析要部署的工作负载并选择最佳实例来创建并在该节点上调度 pod。这确保了节点不会过度配置,从而确保集群中的工作节点具有成本效益。Karpenter 需要安装并配置才能在 Kubernetes 集群中工作。使用此处的官方网站文档来安装和配置 Karpenter。
Kubernetes 中的另一种配置可用于确保我们的 Pod 始终可用,称为PodDisruptionsBudget。
PodDisruptionBudget
这是 Kubernetes 中的原生对象。顾名思义,它用于确定 pod 是否应该突然中断,以及 pod 允许中断多少次。这确保了无论集群内发生什么情况,都不会允许意外删除 Pod 或其他导致 Pod 不可用的操作。PDB 可以限制节点升级或更换,因为在升级过程中,需要重新调度 Pod。因此,在节点升级/更换期间,需要暂时删除/禁用 PDB 以允许升级过程完成。以下是部署的 PDB 配置示例
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: my-pdb
spec:
minAvailable: 2 # Minimum number of pods that must remain available
selector:
matchLabels:
app: my-app
这selector.matchLabels.app就是 PDB 选择哪个 Pod/部署来启用中断预算的方式。根据上面的配置,至少有 2 个 Pod 必须始终保持可用。
结论
确保 Kubernetes 上的 Pod/容器已配置所有这些内容,以确保部署无缝且零停机。这可以让您的用户在使用容器/pod 内运行的应用程序时获得无缝体验。这可确保您在部署和更改应用程序期间无需关闭或查找维护时段。
简单介绍项目实践背景
Jenkins
自2018年开始,我主要的工作是集成DevOps工具链提升研发部门应用的发布效能。当时主要的持续集成工具就是Jenkins,使用自由风格类型的作业来完成一些自动化任务。随着Jenkins 2.0核心的特性PipelineAsCode的理念设计出现,我们逐步完成了从自由风格类型转换到Pipeline类型的作业。
自由风格类型的作业,这里提一点就是“自由”, 所谓的自由都是要付出一些代价的。因为太“自由”所以当我维护上千个作业时,除了点点点就是点点点,这一点经历过的人应该会懂得的。
Pipeline As Code
Pipeline类型的作业我们可以基于Groovy代码编排和定义工作流程,期初我们为每个项目编写了一套jenkinsfile,但是随着功能的扩展,Jenkinsfile维护也成了一个复杂的问题。于是通过Jenkins共享库的实践,抽取Jenkinsfile中定义的功能函数,然后通过一个标准的Jenkinsfile来维护许许多多不同技术栈和类型的作业,这样也就实现了统一的维护和管理,减轻了运维的工作量,实现“一处修改,全部应用”的效果。
SonarQube代码质量
持续集成阶段需要加上工程的质量检查阶段,我们搭建了SonarQube代码质量平台。当时我记得是LTS 6.x版本, 后来升级到7.x版本。SonarQube是一个代码质量平台,其安装部署不算复杂,需要DB和Java, ElastiSearch等组件。
但是后来7.x版本发生了变化,从Java8 升级到Java 11, 以及不支持MySQL数据库转换成PostgreSQL数据库。我们使用SonarScanner CLI作为客户端,统一的进行代码扫描。SonarQube支持的扫描工具也可以与常见的项目构建工具Maven、Gradle进行集成,也就是通过命令行即可完成代码扫描。开源版本的软件可靠性稳定性都需要自己保障了, 在很多团队自定义代码质量阈和质量规则的时候,就需要调用API对每个项目代码扫描之前,进行配置定义对应的质量阈和质量规则。
Nexus3 制品库管理
Nexus我们作为制品库的管理软件, 其可靠性和稳定性是经过验证的。使用Nexus3 作为一些yum,go,linuxs,python源管理,还可以将项目生成的应用制品纳入制品库管理,这样可以实现“同一个软件包,发布到多个环境” 或者同一个环境,基础设施环境分为Dev、Test、 Uat、STG、PROD等环境,但是每个环境的配置应该是相同的,除了硬件资源配置外,软件操作系统环境方面不应该出现不同的情况。如果是使用的私有云、公有云的VM虚机,那就需要通过Ansible或者SaltStack等配置管理工具进行统一配置以实现环境标准化。
Docker Kubernetes 容器化
随着容器和Kubernetes的盛行,项目应用逐渐容器化,编写Dockerfile生成Docker镜像,然后将Docker镜像上传到镜像仓库管理。在Kubernetes环境,我们还需要编写应用的基础资源配置YAML清单文件,最后通过Kubectl,helm通过进行应用的发布。
GitOps 基础设施即代码
应用的标准化配置GitOps已经实现了,那么基础设施层面的GitOps如何实现呢。基础设施即代码,可以通过Terraform 工具编写代码定义资源,然后自动化的开通云上资源。这样就可以实现了以代码的方式描述基础设施资源,一键的发布。