本文从应用开发人员的角度,总结一下 Kubernetes 的业务开发中的最佳实践,本文不会展示任何相关的 kubelet 命令以及 yaml 格式文件,通过尽可能精简篇幅来提高读者阅读体验和效率。
Kubernetes 通过 Namespace 将资源进行逻辑上的隔离,默认情况下集群中有三个命名空间:default、kube-public 和 kube-system,
通常情况下,有一个非生产的 Kubernetes 集群用于特定环境 (开发、测试、集成),如果出于成本和业务服务独立性考虑,将生产和非生产环境混合到一个集群中,
则应该建立新的 Namespace 来进行业务服务区分与隔离,例如常见的 Namespace 是 test
和 prod
。
此外,建立新的 Namespace 之后,应该为不同的 Namespace 来配置不同的资源配额和资源限制。例如给测试环境分配较少的资源额度,给生产环境分配较多的资源额度, 这样即使在测试环境对某个服务进行压测,也不会影响到生产环境。
Kubernetes 中支持使用 RBAC 为集群中的用户和组定义细粒度的访问控制规则,例如可以将无状态应用、有状态应用、路由管理的功能开放给开发人员,将命名空间、资源配额与限制的功能开放给运维人员。
将 Pod 视为独立的机器,并将单个应用或者多个紧密耦合的应用放入单个 Pod 中。
对于多层应用,应该分散放到多个 Pod 中,典型的如 Web 应用,应该将前端页面、后端接口、缓存、数据库分别放入不同的 Pod 中。此外,不要将应用部署到单独的 Pod 中,在保持应用可以水平扩展的前提下:
如何判定单个 Pod 中是否应该放入多个容器?这里给出几个判断条件:
组合大于继承
这条软件设计原则针对 Pod 依然适用,大多数情况下,单个应用容器都是最佳实践,对于某些多应用容器场景,可以通过将辅助容器中的应用使用子进程的方式运行,这样就可以将多个容器应用集成到一个容器应用中。
每个容器中的应用都应该遵循单一职责,只负责一个功能或执行一项任务。如果应用内部发生不可恢复的错误,应该让它崩溃并自动退出,Kubernetes 会自动重启该容器。
容器共享主机的内核,因此隔离程度无法到达虚拟机级别,安全的策略就是禁止使用 root 身份运行应用。
应该尽可能缩小镜像体积,因为当集群自动扩容添加新节点时,新节点必须下载容器镜像。容器的镜像体积越小,节点下载速度越快,应用的启动速度也就越快。
如果 CRI 容器使用的是 Docker,可以参考之前的 Docker 最佳实践[1]。
如果所在企业没有自己的基础设施团队,建议镜像仓库和云服务器的提供商保持一致,这样保证镜像仓库高可用的同时,可以节省带宽费用并提升镜像拉取速度 (因为走的是内网)。
网上大多数文章会提到 不要使用 latest 作为镜像标签,笔者不是特别认同,还是需要具体情况具体分析,笔者的建议是:
git commit
哈希值作为标签的镜像 (回滚时专用镜像)维度 | 名称 | 值 |
---|---|---|
发布版本 | release | "stable", "canary" |
发布环境 | environment | "dev", "qa", "production" |
应用类型 | tier | "frontend", "backend", "cache" |
对于无状态应用,直接使用 Deployment 部署。
下面主要说下几个重要的参数。
Pod 的副本数量。
指定滚动更新过程中不可用的 Pod 数量上限,数值可以是绝对数字,也可以是百分比 (最终会转换成数字),例如 Pod 数量为 10, maxUnavailable 值的 30%, 那么在滚动升级过程中,始终保证可用的 Pod 数量为 7 。
指定可以创建超出 replicas 的 Pod 数量,数值可以是绝对数字,也可以是百分比 (最终会转换成数字),例如 Pod 数量为 10, maxSurge 值为 30%, 那么在滚动升级过程中,始终 Pod 最大数量不超过 13 。
在滚动升级期间设置合理的 maxUnavailable 和 maxSurge 值,在资源消耗和保证服务可用性之间取得平衡。
指定新创建的 Pod 的最小就绪时间,只有超过这个时间 Pod 才会被认为可用,使用该参数更好掌握升级部署过程,避免 Pod 中的进程启动后立即接受流量导致错误,同时减缓滚动升级的速率。
通常情况下需要根据业务场景计算出应用的初始化时间上限,同时还要考虑到应用的就绪时间条件,通过两者的综合考量,将 minReadySeconds 值设置地尽量高一些。
默认情况下, 10 分钟内无法完成滚动升级将被判定为失败,对于大多数无状态应用来说这个时间太长了,可以更新 spec.progressDeadlineSeconds 来自定义超时时间。
Deployment 的默认策略是 RollingUpdate, 也就是滚动升级。
此外还可以将策略设置为 Recreate, Recreate 策略首先终止当前版本中的所有 Pod,然后创建并启动新 Pod, 两个过程之间会有一定的服务不可用空窗期, 该策略的应用场景是: 无法接受应用在滚动升级策略中同时存在的两个版本,并且可以接受一定的服务不可用空窗期 (例如金融相关业务),可以在业务低峰期采用这种策略,并在用户界面给出对应的维护公告。
将所有由 systemd 管理的进程使用 DaemonSet 管理。
针对不同的环境单独配置 ConfigMap, 可以将多个小的配置文件合并到一个大的配置文件中,参考 这个示例[2]。
热更新 ConfigMap 会导致应用重新加载新的配置,但不会触发应用程序重启。
确保采用 Secret 的方式存储敏感数据,禁止使用环境变量、自定义加密方式等毫无意义的奇技淫巧。同时限制特定容器集合才能访问 Secret, 读取 Secret 之后保持只读状态,避免以明文存储 Secret 数据或将 Secret 传输给第三方。
注意: Secrets 默认情况下采用 base64 编码存储非加密的形式存储在 etcd 中,需要配置为静态加密方式。
正常情况下,Job 会在 Pod 中运行直到完成,但是,如果节点发生故障,或者当 Pod 在运行时被驱逐出节点时,调度器会将 Job 放在一个新的节点上重新运行。所以 Job 要保证幂等性 (例如创建数据表时的 IF NOT EXISTS
判断语句),并且在状态为失败时自动停止,不再重新启动 (将 Restart Policy 配置为 Never)。对于涉及到网络请求的 Job, 笔者的建议是 将请求重试等策略放在应用代码中,而不是通过配置 Job 的 Restart Policy 来完成。
比较重要的两个声明字段:
将周期性的自动化任务通过 CronJob 来执行,例如数据备份、回归测试、日志分析、运营数据邮件、已有 Cron 任务的迁移等。
注意: 因为工作节点本身受到资源使用和调度的限制,所以可能发生 Job 运行时间延时的情况 (和在应用层使用的定时任务组件延时情况类似),如果 Job 本身有较高的时间敏感度, 可以通过指定 startingDeadlineSeconds 字段来指定 Job 运行截止日期,这样的话,如果 Job 运行时会检测,如果运行时间大于截止日期,Job 将不再运行并且直接标记状态为失败。
此外,不应该将 Job 的运行时间作为业务时间序列属性,对于多副本运行的 Job 要保证幂等性。
不管是传统的 Linux Cron 任务,还是通过守护进程 + 定时器组件模式,亦或是 Kubernetes 中的 Job, 这些都只是具体的运行时载体,最重要的是保证业务代码的健壮性。
将 Pod 内应用日志输出到文件或标准输出,然后通过日志采集组件,最后将日志数据发送到自建日志仓库或云服务商提供的日志聚合服务。
大多数情况下,使用云服务商提供的默认配置就可以满足需求,如果需要自定义网络策略,可以参考 官方文档[3]。
监控 Kubernetes 集群对于确保应用程序的运行状况和性能至关重要,使用 Prometheus、Grafana 等工具或云服务商提供的原生监控解决方案,来收集和分析集群中的指标, 例如 CPU 使用率、内存使用率和网络流量等,同时设置警报和主动通知方式,保证集群出现任何问题时收到通知。
集群伸缩可以自动添加或者删除工作节点,需要主要的是: 如果遇到业务在某个时刻流量突增的场景,则最好提前手动添加节点,毕竟新节点从启动到加入到集群并开始接收流量也需要时间,提前增加节点可以避免突增的流量造成负载过大甚至宕机。
新的版本除了引入新功能之外,还包括漏洞和安全修复,可以定期升级到最新的稳定版本,享受官方升级带来的红利。担心已有组件和新版本不兼容?主流云服务商都有兼容性自动检测功能,直接使用即可。
随着微服务架构的普及,实现一个简单的服务都需要对云原生中涉及到的分布式技术栈和容器编排基础知识有较深入的理解。因此,开发人员必须精通现代编程语言来实现业务功能,并且同时精通云原生技术来解决非功能性需求。