概览
十二要素应用原则(The Twelve-Factor App) 在如今的微服务领域非常流行,相信大家或多或少有所耳闻,但了解其中细节的并不多。
今天,我将对这12个原则做一个概要分析,结合Go语言中的相关例子,根据开源与大厂的具体实践,和大家一起看看个中究竟。
官方资料
核心概念
先对12个概念做一个简单的讲解,让不熟悉的朋友心中有个初步概念。
1. Codebase 基准代码 - 一份基准代码,多份部署
示例:一个应用程序的代码,放在一个Git仓库里。
分支算不算一个仓库呢?
这点常有不同的理解。我个人会把一个分支认为是一个轻量级的仓库,每个分支对应一个具体的业务场景。
痛点:在分布式的环境下,保证多个机器上运行的应用程序源代码一致,提升排查问题的效率。
2. Dependencies 依赖 - 显式声明依赖关系
示例:Go语言的go module就显式地声明了项目依赖库与版本,方便维护。
痛点:以前Go vendor模式,将依赖代码都放在本地,没有一个文件集中说明项目的依赖,这就导致想找依赖关系必须去看代码细节。如果某个库的v1.0版本有bug,需要升级到1.1,go module只需查看一个文件,而vendor模式得遍历具体的项目,甚至多次递归。
3. Config 配置 - 在环境中存储配置
示例:将配置信息(端口、数据库、外部服务等)放在环境中,而不要硬编码。
环境定义的扩展:在微服务中,这个环境指的是代码具体的运行环境,包括配置文件、环境变量、配置中心的数据。
值得一提的是,按照这种实践,配置文件不应该和应用程序代码放在一个代码仓库中,而是单独管理。
痛点:保证多机器上运行的代码一致后,把变化点转移到配置中,避免硬编码到代码仓库中、导致更新需要整体升级。
4. Backing Services 后端服务 - 把后端服务当作附加资源
示例:当前的数据库选型为MySQL,有一天突然转变成了阿里云的RDS,只需要修改一个配置。
痛点:这是一种面向标准接口编程的模式,重点是让非业务相关的代码变得灵活可替代,如数据库、缓存、第三方服务。从被调用方的角度来看,过于理想化,很多重量级的组件很难被替代;而从调用方来看,这一切又显得理所当然,谁都希望外部系统的变化是无感知的。
这一点,非常考验每个服务的设计者的能力,尤其是API接口。
5. Build, release, run 构建,发布,运行 - 严格分离构建和运行
示例:Go程序在一个构建机器上用 go build命令生成二进制文件,在发布阶段将这个二进制文件分发到各个待运行的机器上,最后在各个机器上运行这个二进制文件。
痛点:强调了CICD一整套流水线,不要让构建和运行强绑定。举个极端的例子,如果有一台centos和一台ubuntu的机器,我们去两个机器上各自编译+运行,可能因为内核不同而发生奇怪的现象;而如果采用在一台专用的centos机器上编译成统一的可执行文件,再分发到这两台机器上,更加合理。
如果centos上可执行文件无法在ubuntu机器上运行,就在部署阶段报错,提醒我们去修复。我们更应该去把部署的机器改成centos统一管理,而不是去花大量精力兼容ubuntu。
随着Docker技术的推广,大量简化了这一步骤。
6. Processes 进程 - 以一个或多个无状态进程运行应用
示例:程序的内存中不保存具体业务相关的数据,而应该保存在共享存储上。
痛点:程序有状态是横向扩容的一大阻碍。想要完全去掉有状态的数据过于理想化,毕竟放在共享存储上肯定不如本地内存访问快,这对一些性能敏感的应用来说影响很大;但微服务架构强调要面向失败编程,这些有状态的数据在程序崩溃时无法恢复,可能导致级联雪崩的等更可怕情况。这一点,需要服务的负责人仔细权衡。
7. Port Binding 端口绑定 - 通过端口绑定提供服务
示例:通过开放端口、以RPC形式提供服务
痛点:历史上有些程序以unix socket的本地形式提供服务,维护成本比较高。目前微服务以HTTP为代表的RPC方式提供服务,适合横向扩展。
8. Concurrency 并发 - 通过进程模型进行扩展
示例:当并发量达到一定程度,通过横向扩容进程(Docker实例)来满足需求
痛点:在并发度比较小时,最简单的提升方式是纵向扩容 - 即提高机器的配置。但当并发程度达到一定级别后,无法再通过提升硬件配置来解决。如果进程做到了无状态,利用外部的调度平台(如k8s)进行扩缩容,更适合长期发展。
9. Disposability 易处理 - 快速启动和优雅终止可最大化健壮性
示例:程序能够快速启动,遇到严重异常时也能尽可能地完成现有的任务,优雅终止。
痛点:启动时间少能让程序遇到问题后快速恢复,也可以更快速地横向扩容;而优雅终止更多地是为了保障数据的一致性。
10. Dev/prod parity 开发环境与线上环境等价 - 尽可能的保持开发,预发布,线上环境相同
示例:开发、预发布、线上等环境上部署的代码、配置、第三方服务等,尽可能地保证一致,让故障提前在测试环境发生。
痛点:尽可能地保证线上环境的稳定性,让问题尽可能地在开发环境中暴露出来,结合CICD工作保障环境的一致性。
11. Logs 日志 - 把日志当作事件流
示例:在微服务架构中,通过日志将一个事件(如一个RPC请求)串联起来。
痛点:利用格式化的日志+ELK,将日志收集到统一的管理平台,用于分析数据。
12. Admin processes 管理进程 - 后台管理任务当作一次性进程运行
示例:将脚本、定时任务等,也作为一个应用程序提交,由k8s等调度平台执行
痛点:手动执行脚本或任务往往会有两个严重问题:误操作与可追溯性差。更合理的方式是走一个正式的发布流程,方便管理。
案例串讲
开发阶段
你(研发人员)收到了一个需求,要实现一个分布式的订单服务。于是,你新建了一个git仓库(1.基准代码),准备用go语言编写,部署在多个机器上(8.并发)。开发期间引用了大量的开源库,用Go Module将这些依赖都管理了起来(2.依赖)。
开发过程中,由于测试和线上的数据库地址不同,所以你把这些信息放在了配置中心(3.配置),如Etcd、k8s的ConfigMap中。
由于外部服务会依赖这个订单服务,所以要求研发提供的API接口必须向前兼容,所以选用了经典的RESTful风格的HTTP接口作为对外的协议(7.端口绑定)。这样,即便订单服务后续不断迭代,对外部来说也只是修改一个调用地址即可(4.后端服务)。
上线阶段
在测试、预发布环境,为了开发与测试人员更好地验证功能,绝大部分的配置参数都保证与线上一致(10.开发环境与线上环境等价)。
整个上线流程包括了测试、预发布、线上三个环境的部署和验证,统一采用CICD的技术自动化执行(5.构建,发布,运行)。
维护阶段
订单服务很复杂,经常会发生崩溃,导致很多数据恢复不了。于是,研发组长决定把关键的数据都放在Redis集群中(6.进程),保证应用的无状态化。
数据不丢了,但程序崩溃后恢复很慢,发现花大量的时间在加载初始数据上。于是又做了一次优化,利用Lazy Load的思路,按需加载(9.易处理)。
程序正常运行了,但用户反馈有个请求经常失败。这时,你利用ELK收集上的日志,排查了整个链路中的信息(11.日志),发现某处代码逻辑搞混了。当前的问题修复了,你还需要去数据库修复历史数据,就又写了个sql脚本提交到专门用来维护修改数据的代码仓库,提交给平台自动运行(12.管理进程)。
总结
业界对这12个原则的理解并不完全一致,这也是因为不同角色处于不同的阶段所导致的视野差异性。
总体来说,这12个原则强调了简单性、可维护性,我们可以把它们作为微服务架构设计的指导性原则。