再谈十二要素方法论
本文回顾了 12 要素方法论的各个原则,探讨了这些原则在当前云原生技术环境下的应用情况、适用性和实践,并讨论了在实际操作中是否需要对其进行调整。原文: 12 Factor: 13 years later
导言
最近一次关于 CI/CD 的演讲中简要提到了 12 要素方法论,大致意思是"里面可能包含了一些好的做法",可将其总结为:
从 12 要素方法论中可以获得很多好的做法,但是否所有原则都仍然有效?或者说,在某些情况下,完全照搬有没有可能会适得其反?
我曾将大量应用接入 Kubernetes,这些应用在构建时就已经考虑到了 12 要素。这个过程通常相当顺利,所以你开始认为一切都理所应当,直到遇到难以运维的应用。
如果仔细观察,通常会发现这些应用违反了 12 要素中的某些原则。
12 要素方法论是在13年前由Heroku提出的,这是一家“云原生”公司,专注于开发人员体验和运维的便利性。所以毫不奇怪,这些原则仍然重要。
让我们回顾一下这 12 个原则,并将它们与现代云原生应用结合起来。
1. 代码库
在变更控制中跟踪代码库,进行多次部署
看一下,如今我们会在代码库和部署之间添加工件。工件可以是容器,也可以是压缩文件(无服务器)。
值得注意的是,对于本地开发,根据不同设置,通常会以某种形式的实时重载来代替创建实际的工件。
2. 依赖关系
明确声明并隔离依赖关系
这在容器化应用中已变得越来越自然。
不过有一部分描述有点过时:"12 要素应用也不隐式依赖于任何系统工具。例如,使用 ImageMagick 或 curl"。
在容器化应用中,边界就是容器,其内容是明确定义的。因此,应用向 curl
发送命令就不成问题了,因为 curl
现在是随容器一起存在,而不是被假定存在的。
同样,在 AWS Lambda 等无服务器设置中,执行环境定义得非常明确,因此可以安全使用环境提供的任何依赖关系。
3. 配置
在环境中存储配置
这一点在特定解决方案上可能过于具体,主要点在于:
配置不在应用程序代码中
工件 + 配置 = 部署
令人困惑的是,特别是随着 GitOps 的兴起,配置被置于代码库中,但又与应用代码分离。
只要遵循上述概念,使用环境变量或配置文件都是实施细节。
使用 Kubernetes 时,根据安全要求,可能需要考虑使用文件而不是环境变量,并可选择加密。关于这个问题,推荐观看:KubeCon EU 2023: A Confidential Story of Well-Kept Secrets — Lukonde Mwila, AWS (video).
4. 后端服务
将后端服务视为附加资源
这已成为常见做法。在 Kubernetes 中,通常很容易配置本地单节点(非开发)Redis 或 Postgres,或 RDS 或 Elasticache 等远程云管理资源。
使用本地文件系统或内存也有一定原因,例如性能或简单性。只要数据生命周期是短暂的,并且实施过程不会对其他因素产生负面影响,那么使用本地文件系统或内存就没有问题。
5. 构建、发布、运行
严格隔离构建和运行阶段
从 Kubernetes 到 AWS Lambda:如今很难再违反这一原则了。将前面的总结完善一下:
6. 进程
将应用作为一个或多个无状态进程执行
全文中有一句话更好地概括了这一点:
12 要素进程是无状态的,不共享任何东西
一些启示:
单容器、单进程、单服务。
无粘性会话。会话存储在外部,如 Redis。另请参见要素 4。
考虑使用 init 容器或 Helm charts 钩子来简化流程。另请参见要素 12。
该要素与要素 4 有一定程度重叠,意味着尽可能使用外部服务。例如使用外部 Redis 而不是嵌入式 Infinispan。
7. 端口绑定
通过端口绑定输出服务
这对基于 TCP 的应用来说是可行的,但对基于 AWS Lambda 或 SpinKube (Kubernetes 上的 WASM 框架)等事件驱动型系统来说,就不再适用了。
8. 并发性
通过进程模型扩容
使应用具有水平扩展性,在一定程度上与要素 4 有关,因为要素 4 会保证应用进程无共享数据。
此外,应用应让操作系统或调度器负责进程管理。
9. 可替代性
通过快速启动和优雅关机最大限度提高稳健性
在某种程度上,这可以看作是对前一个要素的补充:正如要易于横向扩展一样,要易于删除或替换进程。
具体到 Kubernetes,可以归结为:
遵守终止信号。应用程序应优雅关闭。要么在应用程序中处理 SIGTERM 信号,要么设置 PreStop 钩子(更多信息请参考:Kubernetes best practices terminating with grace)。
设置探针。只有当应用程序真正准备好接收流量时,探针才会返回
OK
。节点是短暂的,因此总是可以重新调度 pod:无共享状态概念。
10. 开发、生产环境一致
尽可能保持开发、预发和生产环境的一致性
这是一个宽泛的话题,而且一如既往具有现实意义。概括起来就是"左移":尽可能可靠、快速的验证变更。
解决方案有很多,可能包括 Docker Compose、VS Code 开发容器、Telepresence、Localstack 或设置临时 AWS 账户作为无服务器应用开发环境。
11. 日志
将日志视为事件流
不要在文件中存储日志,不要在应用程序中"发送"日志。
操作系统或调度器应捕获输出流,并将其路由到所选的日志存储。
12 要素方法略显陈旧的地方在于,没有提及度量和跟踪,二者和日志通常被称为"可观测性的三大支柱"。
将这种方法推广到日志记录,可以考虑"封装"应用程序,而不需要详细的实施。OpenTelemetry 零代码仪表可以作为一个很好的起点,也可以采用可观测性 SaaS 平台的 APM 代理(如 New Relic 或 Datadog)。
12. 管理进程
将管理任务作为一次性进程运行
完整描述中的这段话或许能更好的概括这一点:"管理代码应与应用程序代码一起提供"。
这与更改数据库模式或将资产捆绑上传至集中存储位置等任务有关。
目的是排除任何同步问题。关键词是:
相同的环境
相同的代码库
总结 12 要素
只要我们努力把握这些要素背后的理念,而不是关注每个细节,我认为大多数要素都是站得住脚的。
多年来,一些建议已或多或少成为了惯例。还有一些建议有些重叠,例如状态外部化(要素 4)使并发性(要素 8)和可替代性(要素 9)更容易实现。
要素 13:前向和后向兼容性
根据我的经验,有一点在 12 要素方法中没有涉及,但总是能使应用更易于运维:前后兼容性。
我们希望能够频繁部署应用,并且不停机。这意味着要么进行滚动更新,要么进行蓝/绿部署。在大型分布式平台中,即使是蓝/绿部署也很难做到真正的原子部署。而金丝雀部署等部署模式则意味着可以回滚。
因此,做好这一点就能为频繁顺滑的部署开辟道路。
这与数据库、缓存数据和 API 约定有关。我们需要考虑:
当版本
N
和N+1
同时运行时,应用如何处理数据?如果需要从
N+1
回滚到N
,会发生什么情况?
一些建议:
更改数据库模式时,首先添加列。只有在迁移数据后,才能在后续版本中删除列。
首先在 API 或事件模式定义中添加字段,然后再更新消费者,使其真正使用新字段。
考虑缓存对象的兼容性。在缓存键前加上应用程序版本独有的前缀会有所帮助。
过渡时期的数据将如何处理?以新旧格式存储数据?是否需要与数据一起存储版本信息并支持多个版本?
对于提供给他人运维的应用来说,与开发团队自己运维并通过 CI/CD 发布的应用不同,可能会更复杂。外部用户通常不会关注所有次要版本,因此更有可能不具备向后兼容性。
结论
上述某些建议可能需要额外的努力。不过,根据我的经验,这些努力都是值得的,而且会因运维简便、省心和减少对发布协调的需求而得到连本带利的回报。
你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!
版权声明: 本文为 InfoQ 作者【俞凡】的原创文章。
原文链接:【http://xie.infoq.cn/article/4888bf08e025293ee04618727】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论