从应用开发角度认识 K8S
云原生应用
我们正经历从单体应用转向分布式微服务架构应用的技术趋势。分布式微服务架构作为越来越多的软件开发设计模式,以领域设计模型来指导业务需求的抽象与封装。对业务的实体抽象还是边界划分,会以微服务架构作为落地点,形成微服务集群。并实施运行在云原生编排平台。
云原生应用的基石是干净整洁,业务逻辑相对单一,并与其他领域对象独立的代码实现。这一阶段保证业务质量的主要是编程基本功,以及高覆盖率的自动化测试能力。
领域设计驱动是近年微服务技术热潮下的主流设计模式,主要解决的问题是如何拆解一个复杂的业务场景需求到多个微服务单元。领域设计驱动是微服务架构的设计模式,微服务架构是基于领域设计模式的实现方式。一个微服务可以对应一个领域对象,也可以是一个领域服务。
分布式微服务架构实现的云原生应用具有高可用,弹性伸缩,容忍失败以及健康自省等特点。它使得我们处理日益增长的业务需求的能力从开发编程的复杂性逐渐转移到了资源整合,操作与管理的复杂性上。
微服务是单一的,运行在一个单进程中的简单应用。容器技术恰好能够提供这种隔离封装,将一个简单的微服务以 Dockfile 模版方式标准化,可无差别得运行在分布式集群的任意资源节点。
K8S 作为目前最流行的云原生平台架构,对于一组微服务的交互,持续化数据的存储,或者实施多个具有依赖关系的微服务运行,以及容量规划等问题,能够提供一套自动化的系统性解决方案。
用 OOP 方式解读 K8S
对于应用开发者,面向对象模式想必了然于胸。OOP 设计了一套对一个逻辑对象的生命周期管理的方法论,类比 OOP 思路,笔者接下来详细介绍一些 K8S 核心资源对象以及应用方式。
构建/部署保持隔离性 Pod/Deployment
Image
容器镜像类比 OOP 的类,定义了一个模块的全部属性与功能,提供了唯一暴露在外的 API 调用方式以及参数集合,对应着一个独立完整的发布周期,就像容器的设计蓝图。这种静态定义方式,可以定义并初始化容器进行,使其在任意环境任意时刻行为完全一致。一个容器镜像对应着一个微服务,属于开发团队的产物。
Container
容器类比 OOP 的对象,是容器镜像的运行态。一个容器是一个容器镜像的运行进程,而一个容器镜像则可以在任意时刻任意环境下创建任何数量容器。
Pod
Java 应用开发者都知道基于 Springboot-MVC 框架的 Java 应用部署时只需提供一个 Jar 包,Jar 包内部源码被编译后不可再改变。Pod 是云原生编排平台的资源调度部署的最小单元。Pod 和容器的关系类似 Java 的 Jar 包和对象,应用开发者交付的容器镜像通过 Pod 在 K8S 集群上部署并调度,容器则是 Pod 的内部资源对象,K8S 无法感知也无法干预。
K8S 设计 Pod 为部署调度的最小单元,是因为 Pod 实现了内部一组容器在存储空间,网络空间及进程空间可共享该 Pod 资源,类似一个虚拟机上同时运行多个进程。容器间通信类似单节点进程间通信。Pod 的设计,就是要让它里面的容器尽可能多地共享 Linux Namespace,仅保留必要的隔离和限制能力。
Pod 类似 OOP 的 Module,即逻辑紧密的对象集合通常属于一个独立模块。Pod 的定义示例如下:
Pod->spec 下包含了一个或多个容器模版定义。PodYAML 提供了很多属性,有兴趣深入的读者可以参考学习https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/api/core/v1/types.go
Pod->spec 有一个很特殊的容器模版定义,关键词为 initContainer,顾名思义是做初始化的容器。初始化容器必须先于应用容器启动执行完毕,并且只能执行成功。
Pod->spec 除了定义了容器模版,还定义容器间共享的存储资源 Volume 挂载方式,和影响 Pod 资源调度的节点选择器标签,亲和性以及容忍性等。
NameSpace
命名空间是一个组的概念,为集群资源提供了逻辑划分的能力。这种使用方式类似 OOP 的 Package。当项目变大,时常有同名的类或者对象,为了区分会以 packge 为路径前缀,定义和引用相应对象。K8S 集群中常常运行着数百个应用服务,也会出现同名资源的情况。命名空间可以在 K8S 上实现对一组资源对象隔离与权限管理。
命名空间最常用的场景是在一个 K8S 集群区分开发环境和测试环境。命名空间也可以提供多租户运行环境,或者为了应用运行具有隔离性,为某个应用部署命名一个单独的命名空间。
命名空间虽然提供了资源范围逻辑划分的能力,但是并没有真正隔离一个集群内部 Pod 之间的通信,即属于同一集群内的多个命名空间下的 Pod 仍能在集群内互相通信。如果需要做到命名空间之间的完全隔离,可以采用 NetworkPolicy 实现。
Deployment
在 K8S 运行的应用服务升级就是创建一个新版本 Pod,并销毁旧版本 Pod 的过程,由 Deployment 资源对象定义。微服务架构应用的数量成百上千,如果手动部署,则会引入人为错误并且部署操作很容易成为整个系统的瓶颈。K8S 将部署 Pod 全部操作定义在 Deployment 资源对象,由平台自动化地部署 Pod。
Deployment 资源对象除了要定义部署 Pod 是什么样的以外,还会定义预期部署状态。比如,预期部署 Pod 数量,或在哪个节点上部署。
部署一个新版本要么生成新版本 Pod,待健康检查等确认新 Pod 可对外提供服务,再将旧版本 Pod 销毁,最终达到预期部署状态;要么就销毁原有旧版本 Pod,再生成新版本 Pod,直到达到预期部署状态。第一种方式为 Rolling Update,好处是部署时没有任何 downtime,坏处是会存在多个版本同时提供服务,导致服务状态不一致。第二种方式为 Recreate,好处是在任意时刻都不会存在多个版本的应用服务,坏处则是部署时存在 downtime。K8S 默认的部署策略为 Rolling Update。
Deployment 通过 ReplicaSet 控制器创建和管理 Pod,确保 Pod 能如预期运行成功,并且满足 Deployment 的部署定义,比如开启几个副本,满足部署策略等。ReplicaSet 解耦了部署与 Pod 运行,Pod 处于 Running 状态并不能代表应用部署成功。Deployment 对 Pod 副本数量以及应用容器的 Health Probe 做了设置,以确保进程启动以及应用运行成功。只有当新建 ReplicaSet 所属的 Pod 运行成功,并且副本数量达到 Deployment 设置 ReplicaSet 的数量,旧的 ReplicaSet 管理的 Pod 不再承接任何负载或被销毁时,Deployment 达到预期部署状态,才能代表部署成功。
构造器 InitContainer
初始化在 OOP 中,比如 Java 类定义,是构造器。封装了对象使用之前必要的初始化操作。初始化容器 InitContainer,InitContainer 是 Pod 级别的初始化,同理,是 Pod->spec 的一类特殊的容器模版定义,隔离了主应用容器进程与初始化操作,确保初始化操作能够在应用容器启动之前完成。
在容器级别也可以完成初始化,利用容器镜像模版 Dockerfile->ENTERPOINT 定义。容器级别的初始化影响范围是该容器镜像定义内部,而 Pod 级别初始化操作 InitContainer 是对 Pod 内的全部容器组定义。一般情况下,容器初始化更多是 Devops 关心,与应用开发者工作相关性不大。InitContainer 可以将初始化容器模版定义从研发周期上隔离。
Pod 级别初始化操作 InitContainer 的好处是可以统一设置访问共享 Volume 的访问权限;在应用服务启动之前准备好依赖的组件或者数据;验证应用服务运行的依赖运行健康等。在应用服务主容器进程启动之前,确保前置条件准备完备,进而确保应用服务能够运行成功。
处于访问控制等安全性考虑,一般不建议在应用容器镜像定义里开放 Pod 共享资源的访问权限。我们尽量将 Pod 的共享资源管理操作留给编排平台设置与控制,与应用服务本身隔离。最大化地确保应用服务本身不带有平台依赖属性。所以,InitContainer 也提升了微服务应用开发的安全性。
一个 Pod 模版可定义多个 InitContainer 以及多个应用容器。K8S 确保 InitContainer 按定义顺序依次执行初始化操作,在应用容器启动前执行完毕,而应用容器启动是并行的。
InitContainer 与一般的应用容器基本相同,但是,InitContainer 一般为 Completed,不会存在 Failure 终止状态,因为当 InitContainer 执行失败会直接导致 Pod 重启,重新开始执行 InitContainer。
组合模式 Sidecar
在前文我们把容器镜像和容器类比 OOP 的类和对象,因为容器镜像定义了一个职责单一的应用微服务。那如果在应服务运行时,我们需要扩展或添加一些旁路操作,此时,类似 OOP 的组合设计模式,我们其实可以直接整合另一个容器镜像定义到同一个 Pod,这就是 Sidecar。
Sidecar 保证了应用容器的职责单一,同时,也能在 Pod 级别为其添加更新数据,配置文件,静态资源或者采集日志数据这种能够独立复用的旁路操作集合。Sidecar 能够通过组合多个职责单一的容器,提供一个功能完备,具备上线能力的应用微服务,同时,确保开发团队只用考虑业务应用功能本身。
Sidecar 与 InitContainer 是两种不同的容器定义。首先 InitContainer 定义的是 Pod 级别的初始化操作集合,必须在所有应用容器启动之前执行完毕,具有严格的执行顺序。Sidecar 与应用容器执行顺序没有严格控制,两者通常是同时运行在一个 Pod 内,共享 Pod 资源,共同完成 Pod 暴露的服务能力。
配置管理 ConfigMap/Secret
应用开发的 12 原则中有一条是在环境中存储配置。配置信息与应用隔离,可以通过环境变量来存储应用的配置信息。环境变量具有全局性,可将其在应用进程运行时加载。当配置信息较大时,利用环境变量传递配置信息就不是什么好办法了。Java-Springboot 应用提供了 Profile 文件,记录和保存应用相关的配置信息,开发者可以依据环境区分不同的 Profile 文件。
在 K8S 里配置管理 ConfigMap 和 Secret 同时支持环境变量 Key/Value 形式和应用配置 Profile 文件形式。环境变量的 Key 一般是全大写字母字母表示;应用配置 Profile 文件是小写字母表示额文件名。
K8S 提供 Secret 资源对象来配置敏感数据。比如,数据库链接的用户名,密码等。
ConfigMap 与 Secret 对象通过 Volume 挂载到 Pod 里,所以该配置信息被 Pod 内的容器组共享。ConfigMap 与 Secret 的数据存储上限为 1MB,故当应用配置文件过大,可考虑使用 InitContainer 初始化一个配置管理容器在同一个 Pod 下。在应用容器启动前,传递应用配置到应用指定挂载目录,从而更新应用配置信息。
异步/并发执行 Job/Cronjob
应用开发过程中,常会面对批处理任务/定时任务需求。目前流行的应用框架,比如 Java 的 Spring-Batch 或者 Python 的 Celery 都可以实现异步任务/定时任务。但是这种应用级别的实现方法,在云原生中,会使应用服务实现的很重,比如异步任务通常要求所属应用满足高可用,资源弹性伸缩以及故障自愈。这些特性都是 K8S 平台天然自带的,可以考虑将应用的异步任务实现委托给 K8S 的 Job/Cronjob 控制器对象。
K8S 的 Job 控制器对象,类似 Deployment,是创建与管理 Pod 生命周期的一种实现方式。与 Deployment 不同的是,Job 控制的 Pod 是运行结束就终止,即 Pod 的终态为 Completed。Job 的 Pod 默认不会直接销毁,主要目的是提供查看任务运行日志结果。
K8S 的 Cronjob 控制器对象,顾名思义,在 Job 对象之上组合了定时触发事件逻辑。主要使用的场景包括但不限于文件传输,发送邮件或者短信通知,以及备份与定时清理过期备份等。
结语
笔者整理了一部分 K8S 基础知识点的初衷是为了审视一下 K8S 这个庞大的技术栈里开发者掌握和使用 K8S 所要了解的最小知识点集合。笔者相信未来的应用都是建立在云之上,所以不论是哪个角色,都得掌握必要的 K8S 知识点才能流畅地开启云原生开发之旅。
以上很多内容都是笔者在学习https://time.geekbang.org/column/intro/116以及 Kubernetes-patternshttps://developers.redhat.com/blog/2020/05/11/top-10-must-know-kubernetes-design-patterns/时的心得和读书体会,受益于大师们对 K8S 技术栈多种角度的解读与梳理。
最后这次比较系统的学习梳理的契机也源于团队对于 K8S 技术的重视,特别感谢团队领导的重视与支持。
版权声明: 本文为 InfoQ 作者【LorraineLiu】的原创文章。
原文链接:【http://xie.infoq.cn/article/ba8e55743e91bae763e671e05】。文章转载请联系作者。
评论 (4 条评论)