蛮荆

Kubernetes 应用最佳实践 - 开篇

2023-02-27

概述

本文从应用开发人员的角度,总结一下 Kubernetes 的业务开发中的最佳实践,本文不会展示任何相关的 kubelet 命令以及 yaml 格式文件,通过尽可能精简篇幅来提高读者阅读体验和效率。

面向开发人员的 Kubernetes 资源结构图

Namespace

Kubernetes 通过 Namespace 将资源进行逻辑上的隔离,默认情况下集群中有三个命名空间:default、kube-public 和 kube-system, 通常情况下,有一个非生产的 Kubernetes 集群用于特定环境 (开发、测试、集成),如果出于成本和业务服务独立性考虑,将生产和非生产环境混合到一个集群中, 则应该建立新的 Namespace 来进行业务服务区分与隔离,例如常见的 Namespace 是 test prod

此外,建立新的 Namespace 之后,应该为不同的 Namespace 来配置不同的资源配额和资源限制。例如给测试环境分配较少的资源额度,给生产环境分配较多的资源额度, 这样即使在测试环境对某个服务进行压测,也不会影响到生产环境。

阿里云 Kubernetes 集群命名空间与配额

使用 RBAC 进行访问控制

Kubernetes 中支持使用 RBAC 为集群中的用户和组定义细粒度的访问控制规则,例如可以将无状态应用、有状态应用、路由管理的功能开放给开发人员,将命名空间、资源配额与限制的功能开放给运维人员。

Pod

将 Pod 视为独立的机器,并将单个应用或者多个紧密耦合的应用放入单个 Pod 中。

对于多层应用,应该分散放到多个 Pod 中,典型的如 Web 应用,应该将前端页面、后端接口、缓存、数据库分别放入不同的 Pod 中。 此外,不要将应用部署到单独的 Pod 中,在保持应用可以水平扩展的前提下:

  • 使用 Deployment 部署无状态应用
  • 使用 StatefulSet 部署有状态应用

如何判定单个 Pod 中是否应该放入多个容器?这里给出几个判断条件:

  • 多个应用需要一起运行还是相互独立运行?
  • 多个应用表示的是一个整体应用还是相互独立的组件?
  • 多个应用是否可以一起扩容还是需要独立进行? (例如多个生产者,单个消费者的应用场景)

组合大于继承

这条软件设计原则针对 Pod 依然适用,大多数情况下,单个应用容器都是最佳实践,对于某些多应用容器场景,可以通过将辅助容器中的应用使用子进程的方式运行,这样就可以将多个容器应用集成到一个容器应用中。


容器

每个容器中的应用都应该遵循单一职责,只负责一个功能或执行一项任务。如果应用内部发生不可恢复的错误,应该让它崩溃并自动退出,Kubernetes 会自动重启该容器。

避免以 root 身份运行

容器共享主机的内核,因此隔离程度无法到达虚拟机级别,安全的策略就是禁止使用 root 身份运行应用。

镜像优化

应该尽可能缩小镜像体积,因为当集群自动扩容添加新节点时,新节点必须下载容器镜像。容器的镜像体积越小,节点下载速度越快,应用的启动速度也就越快。

如果 CRI 容器使用的是 Docker,可以参考之前的 Docker 最佳实践

镜像仓库

如果所在企业没有自己的基础设施团队,建议镜像仓库和云服务器的提供商保持一致,这样保证镜像仓库高可用的同时,可以节省带宽费用并提升镜像拉取速度 (因为走的是内网)。

镜像标签和拉取策略

网上大多数文章会提到 不要使用 latest 作为镜像标签,笔者不是特别认同,还是需要具体情况具体分析,笔者的建议是:

  1. 首先保证镜像仓库的高可用和访问速度,这是重中之重,如果这一点做到了,那么可以使用 latest 标签并将镜像拉取策略 imagePullPolicy 设置为 Always
  2. 对于测试环境,直接使用 latest 标签并将 imagePullPolicy 设置为 Always, 配置 CI 构建脚本在打包应用镜像时,除了 latest 标签镜像外,再额外打包一个以 git commit 哈希值作为标签的镜像 (回滚时专用镜像)
  3. 对于生产环境,使用应用迭代语义化版本号作为镜像的标签 (例如 v1.2.3-prod)

使用多维度而不是单维度的标签

维度 名称
发布版本 release “stable”, “canary”
发布环境 environment “dev”, “qa”, “prod”
应用类型 tier “frontend”, “backend”, “cache”

Deployment

对于无状态应用,直接使用 Deployment 部署。

阿里云 Kubernetes Deployment 滚动升级策略

下面主要说下几个重要的参数。

replicas

Pod 的副本数量。

maxUnavailable

指定滚动更新过程中不可用的 Pod 数量上限,数值可以是绝对数字,也可以是百分比 (最终会转换成数字),例如 Pod 数量为 10, maxUnavailable 值的 30%, 那么在滚动升级过程中,始终保证可用的 Pod 数量为 7 。

maxSurge

指定可以创建超出 replicas 的 Pod 数量,数值可以是绝对数字,也可以是百分比 (最终会转换成数字),例如 Pod 数量为 10, maxSurge 值为 30%, 那么在滚动升级过程中,始终 Pod 最大数量不超过 13 。

在滚动升级期间设置合理的 maxUnavailable 和 maxSurge 值,在资源消耗和保证服务可用性之间取得平衡。

minReadySeconds

指定新创建的 Pod 的最小就绪时间,只有超过这个时间 Pod 才会被认为可用,使用该参数更好掌握升级部署过程,避免 Pod 中的进程启动后立即接受流量导致错误,同时减缓滚动升级的速率。

通常情况下需要根据业务场景计算出应用的初始化时间上限,同时还要考虑到应用的就绪时间条件,通过两者的综合考量,将 minReadySeconds 值设置地尽量高一些。

progressDeadlineSeconds

默认情况下, 10 分钟内无法完成滚动升级将被判定为失败,对于大多数无状态应用来说这个时间太长了,可以更新 spec.progressDeadlineSeconds 来自定义超时时间。

Recreate 策略

Deployment 的默认策略是 RollingUpdate, 也就是滚动升级。

此外还可以将策略设置为 Recreate, Recreate 策略首先终止当前版本中的所有 Pod,然后创建并启动新 Pod, 两个过程之间会有一定的服务不可用空窗期, 该策略的应用场景是: 无法接受应用在滚动升级策略中同时存在的两个版本,并且可以接受一定的服务不可用空窗期 (例如金融相关业务),可以在业务低峰期采用这种策略,并在用户界面给出对应的维护公告。

DaemonSet

将所有由 systemd 管理的进程使用 DaemonSet 管理

ConfigMap

针对不同的环境单独配置 ConfigMap, 可以将多个小的配置文件合并到一个大的配置文件中,参考 这个示例

配置热更新

热更新 ConfigMap 会导致应用重新加载新的配置,但不会触发应用程序重启。

  1. 创建一个 ConfigMap 对象,包含应用的配置数据
  2. 在 Pod 配置中,将 ConfigMap 挂载为一个卷(Volume), 应用可以直接从卷中读取配置数据
  3. 当配置数据需要更新时,修改 ConfigMap
  4. 修改 ConfigMap 后,Kubernetes 会自动触发相关的事件,通知 Pod 更新其挂载的卷
  5. Pod 接收到更新事件,重新加载卷中的配置文件,从而实现配置热更新

图片来源: https://www.manning.com/books/kubernetes-in-action-second-edition

Secret

确保采用 Secret 的方式存储敏感数据,禁止使用环境变量、自定义加密方式等毫无意义的奇技淫巧。 同时限制特定容器集合才能访问 Secret, 读取 Secret 之后保持只读状态,避免以明文存储 Secret 数据或将 Secret 传输给第三方。

注意: Secrets 默认情况下采用 base64 编码存储非加密的形式存储在 etcd 中,需要配置为静态加密方式


Job

正常情况下,Job 会在 Pod 中运行直到完成,但是,如果节点发生故障,或者当 Pod 在运行时被驱逐出节点时,调度器会将 Job 放在一个新的节点上重新运行。 所以 Job 要保证幂等性 (例如创建数据表时的 IF NOT EXISTS 判断语句),并且在状态为失败时自动停止,不再重新启动 (将 Restart Policy 配置为 Never)。 对于涉及到网络请求的 Job, 笔者的建议是 将请求重试等策略放在应用代码中,而不是通过配置 Job 的 Restart Policy 来完成

比较重要的两个声明字段:

  • spec.completions: 指定 Job 需要运行的次数
  • spec.parallelism: 指定 Job 并行数量

CronJob

将周期性的自动化任务通过 CronJob 来执行,例如数据备份、回归测试、日志分析、运营数据邮件、已有 Cron 任务的迁移等。

注意: 因为工作节点本身受到资源使用和调度的限制,所以可能发生 Job 运行时间延时的情况 (和在应用层使用的定时任务组件延时情况类似),如果 Job 本身有较高的时间敏感度, 可以通过指定 startingDeadlineSeconds 字段来指定 Job 运行截止日期,这样的话,如果 Job 运行时会检测,如果运行时间大于截止日期,Job 将不再运行并且直接标记状态为失败。

此外,不应该将 Job 的运行时间作为业务时间序列属性,对于多副本运行的 Job 要保证幂等性

不管是传统的 Linux Cron 任务,还是通过守护进程 + 定时器组件模式,亦或是 Kubernetes 中的 Job, 这些都只是具体的运行时载体,最重要的是保证业务代码的健壮性。

日志

将 Pod 内应用日志输出到文件或标准输出,然后通过日志采集组件,最后将日志数据发送到自建日志仓库或云服务商提供的日志聚合服务。

防火墙和网络策略

大多数情况下,使用云服务商提供的默认配置就可以满足需求,如果需要自定义网络策略,可以参考 官方文档

监控集群

监控 Kubernetes 集群对于确保应用程序的运行状况和性能至关重要,使用 Prometheus、Grafana 等工具或云服务商提供的原生监控解决方案,来收集和分析集群中的指标, 例如 CPU 使用率、内存使用率和网络流量等,同时设置警报和主动通知方式,保证集群出现任何问题时收到通知。

集群伸缩

集群伸缩可以自动添加或者删除工作节点,需要主要的是: 如果遇到业务在某个时刻流量突增的场景,则最好提前手动添加节点,毕竟新节点从启动到加入到集群并开始接收流量也需要时间,提前增加节点可以避免突增的流量造成负载过大甚至宕机。

升级到稳定的新版本

新的版本除了引入新功能之外,还包括漏洞和安全修复,可以定期升级到最新的稳定版本,享受官方升级带来的红利。担心已有组件和新版本不兼容? 主流云服务商都有兼容性自动检测功能,直接使用即可。

小结

随着微服务架构的普及,实现一个简单的服务都需要对云原生中涉及到的分布式技术栈和容器编排基础知识有较深入的理解。因此,开发人员必须精通现代编程语言来实现业务功能,并且同时精通云原生技术来解决非功能性需求。

扩展阅读

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,商业转载请联系作者获得授权。