乐趣区

关于云原生-cloud-native:12-Factor-App-in-Action-12要素应用实战

发现一些老铁在构建容器或应用 kubernetes 中还有些不得要领, 于是我联合本人的教训和思考, 把 “CloudNative 圣经 ” 的 12 Factor App 从新解读了一下, 全文如下:

首先

首先确保你曾经仔细阅读了 12 Factor App 的原文:

  • 12 Factor App 中文.
  • 12 Factor App in English.

而后

本文次要是依据我的一些实践经验和思考来解释, 为什么应用容器化或 CloudNative 或 k8s 要恪守 “12 Factor” 中的因素来构建利用.

咱们逐条来

1. 基准代码 (一份基准代码, 多份部署)

What

基准代码讲的是, 须要有个外围的代码库来存储所有版本.

How

简略来讲, 就是你须要用 Github, 或公有采纳 Gitlab 这样的集中化计划来治理你的代码.

Why

集中化意味着方便管理, 试想一下你刚上线的业务忽然产生了问题, 你得 BOSS 认为对着你吼就能给你加个 Buff 让你智力 +10 从而能迅速修复 Bug. 不堪重压的你间接连贯到了线上服务器手动修改了问题. 就在这所有平息之后, 度过周末的你实现了新的性能, 遗记了线上有个飞天补丁在运行, 间接上线了新的版本笼罩掉了这一修改, 就在你公布结束的一刹那, 你听到了你的 BOSS 收回了如同被人踢到了蛋的吼声后朝你走来 …

别缓和, 归根结底, 这即是你的问题, 又不是你的问题, 你可能违反了公司的上线流程, 但也裸露了公司自身治理上的破绽. Anyway, 罗嗦了这么一大段就是为了通知你有基准代码的益处.

另外, 采纳基准代码势必会须要一个平台, 这些集成化治理平台催生了自动化 CI/CD 零碎, 设想一下, 你提交代码后, CI/CD 零碎主动帮你生成了文档, 主动生成了测试用例, 主动进行了测试, 主动进行了性能测试, 主动生成了报告, 期待最终确认后, 还能实现主动上线. 这个过程你能够全程摸鱼. 是不是很爽? 而这当初曾经是事实了.

这所有正是因为采纳了基准代码而取得的劣势.

2. 依赖 (显式申明依赖关系)

What

显式申明依赖关系指的是通过一份 “ 依赖清单 ”, 来申明须要的依赖.

How

这个倒是很简略, 现有的编程语言大部分都提供了包管理系统. 不便大家显示申明依赖. 如果依赖零碎库, 这点在 Dockerfile 中也失去了较好的解决, 个别 Dockerfile 都会在初始化阶段去装置该我的项目依赖的库, 这就是 ” 显式申明依赖关系 ” 的具体实现形式. 但须要留神的是, 如果有模糊不清的内部依赖, 或外部依赖, 或者底层依赖, 也须要去做基础设施建设, 这是容易疏漏的中央.

Why

显式申明依赖关系的目标是不便进行再构建. 置信各位都有在 Linux 下手动装置某软件, 而后运行的时候提醒短少动静库 (xxx.so) 或运行 apt 或 yum 装置软件而后装置失败或者遇到了包抵触的经验. 实质上显式申明依赖关系正是为了防止相似问题.

明确申明的依赖能够提供一份残缺的清单, 通知工程师只有满足这些条件, 你须要的货色就能失常运行起来. 试想一下你送了你敌人一个口红, 后果他怄气了, 你认为是因为色号不对, 于是又买了一个送他, 没想到他更怄气了. 是不是有一种既视感? 对的, 这就跟零碎提醒短少 libcurl.so, 后果你扔进去个 libcurl.so.4 还是不能运行, 没想到其实人家要的是 libcurl.so.6…

另外我提到了一些依赖要做根底建设, 举个例子, 你的项目组依赖公司根底平台部门的一个框架, 版本 1.2, 然而其余组却须要这个框架的版本 1.4, 试想一下当你走了当前, 接手的同学决定把依赖的这个框架降级到最新版本. 而后发现版本 1.2 的个性被根底平台部门干掉了 … 而且 1.2 的源码找不到了 …

而如果公司在基础设施方面建设得好, 比方这个框架作为公共根底我的项目, 在公司的平台中任何人都能够看到并且能够拜访历史版本并且还能够提交 pull request, 这个问题就迎刃而解了. 新来的同学能够提交 issue 通知基础设施部门, 还有我的项目依赖这个个性, 甚至如果不简单的话, 他能够本人批改后提交 pull request, 而后根底平台部门就能够将这个个性从新合并到骨干. 这样无论是对于开发者还是管理者, 都是大有裨益的. 甚至能够推动公司外部的工程师文化氛围和合作气氛, 带来更好的工作形式.

3. 配置 (在环境中存储配置)

What

在环境中存储配置指的是, 删掉你的配置文件或硬编码在程序中的配置常量. 改用环境变量中存储配置来取代他们.

How

个别语言都提供了读取环境变量的办法, 如果没有, 也能够尝试在初始化阶段通过读取 stdin 或 argv 的形式来读取. 最差的状况, 也能够让 Dockerfile 依据环境变量生成一个配置文件, 而后程序去读取该长期生成的配置文件.

Why

次要目标是解耦

所有为理解耦, 只有配置与实例涣散耦合才适宜更大规模的利用, 能力自动化, 程序化部署. 试想一下线上有 20 万个容器, 数据库连贯是硬编码在程序中的. 当初数据库扩容, 要切换主从节点的连贯配置信息. 20 万个, 手动批改后上线, 好嘛, 一周工夫都不必干别的了. 而如果配置存在于环境变量中, 这所有将会变得很简略, 更新下 k8s 环境变量, 而后批量 reload 容器就完事了.

留神这里的在环境变量中存储配置并不代表着必须要存储在环境变量中, 其更狭义的意义其实是将配置与代码拆散, 不要把配置存储到代码管理系统中.

其次配置注入的机会和配置基准化也很重要

更狭义上, 我倡议将配置也基准化, 即, 配置也须要一个管理系统. 有的公司喜爱弄一个配置核心程序. 我的想法是, 配置注入须要在运行之前完结. 一旦要推延到运行时再获取的话, 如果获取不到配置, 服务就间接挂掉了.

有同学会说, 那 k8s 不是在运行时获取配置的么? 这里的获取配置指的是从基准配置零碎获取, 而 k8s 更像一个缓存, 不要用 k8s 来当这些配置的永恒存储, 现实的状况, 会存在一个基准化的配置散发安装, 比方咱们魔改了 Gitlab, 而后每次配置更新, 通过 CI/CD 零碎, 将配置更新到 k8s 集群中, 而后利用读取 k8s 注入容器的环境变量. 这样既解决了配置的基准化问题, 也防止了配置核心可用性不高带来的运行时获取配置失败导致利用挂掉的问题. 而 k8s 的可用性是间接跟利用强相干的, k8s 不可用, 天然利用也运行不了, 不会呈现 k8s 挂了, 利用获取不到配置, 却奇观的能启动的状况.

总结

我对配置这一因素的要求比 “12 Factor” 中的要求要更严格一些. 外围总结为:

  • 配置要与代码拆散
  • 配置要有基准化
  • 配置对利用来说应该是常量, 即配置要在运行时之前筹备好. 不要推延到运行时再去获取或进行求值.

4. 后端服务 (把后端服务当作附加资源)

What

把后端服务当作附加资源, 更精确地说, 是通过对立资源定位符去调用资源. 实质上讲其实还是强调与其余业务或资源进行解耦.

How

简略来讲, 即不要与资源强耦合, 他的标记是, 切换资源不须要进行批改代码, 仅进行切换配置就能够了.

Why

得益于当初 web 组件的标准化, 当初资源与业务隔离根本都做得很好, 很少有切换个 MySQL 数据库还要批改代码去兼容的状况了. 所以这里更多状况指的是内部第三方服务或须要非凡逻辑 (比方须要注入一些存储过程) 的状况.

具体来讲, 比方以来的第三方发送电子邮件的服务, 每个第三方服务调用办法可能都不尽相同, 这就须要一个边缘业务网关去封装这些调用形式, 而后提供给所有业务一个对立的调用接口. 让调用在这个业务网关进行收束. 隔离复杂度, 让业务外部调用趋于对立.

业务大起来后, 这些边缘型服务的存在是不可避免的. 而这些边缘服务一旦适配结束, 后续的变更会很少, 所以不必过多放心. 而他们自身只有是无状态的, 也十分不便扩容. 是一钟不错的两头计划.

那么什么时候应该开始建设这些服务呢? 我倡议的判断规范 / 指征是, 只有应用第三方服务或须要非凡逻辑, 就要将这些与业务无关与第三方调用过程无关的逻辑封装为边缘业务网关. 不要让这些逻辑耦合进入业务.

至于一些其余的非凡的状况, 比方原来用的是 MySQL, 当初要切换为 MongoDB. 这种状况曾经超脱了领域, 请间接当作新业务进行设计. 不要想着兼容了.

5. 构建, 公布, 运行 (严格拆散构建和运行)

What

这里指的是严格辨别, 构建, 公布, 运行这几个阶段.

How

想要实现这一条, 须要基准代码管理系统, CI/CD 零碎, 以及严格的线上服务治理流程.

Why

方才也举例了间接批改线上代码会导致问题. 这里还要强调的是, 流程的标准化易于业务的规模化和自动化. 这意味着能够节俭工程师的大量工时. 尤其是这种重复性的事务, 每次节生 1 分钟, 累积下来节俭的工夫就十分可观.

在一开始, 大家可能相熟了手动部署上线的模式. 但手动操作非常容易出错. 于是公司倒退到肯定水平后, 可能就会呈现主动上线脚本等相似的货色. 业务再倒退之后, 便会应用 CI/CD 零碎. 一个良好运行的 CI/CD 零碎是须要投入很大老本的, 因而我并不拥护最开始应用简便的形式去公布业务. 但意识形态的建设能够后行. 作为技术管理者和优良的工程师, 应该意识到严格辨别构建, 公布, 运行等阶段的意义和必要性. 并在工作中严格执行.

有些公司这方面执行得十分板滞, 比方我见过上线要 5 个领导签字的, 还有员工没有权限上线而后加班到中午要紧急公布, 于是利用 Linux 提权破绽获取 root 强行上线的. 这既是公司构造收缩到肯定水平和管理水平低下造成的问题. 也有可能是老本和投入之间均衡的考量. 但无论如何, 适宜现阶段业务规模的流程模式, 才是好的模式.

6. 过程 (以一个或多个无状态过程运行利用)

What

12 Factor 利用是以一个或多个无状态过程运行利用, 这极大中央便了业务的伸缩. 简略的启动新的过程或敞开现有过程就能够了.

How

想要实现这一点, 须要严格剥离业务中的长久存储到存储资源 (例如数据库) 中. 置信现有 web 业务都能很好地组织这一点了.

Why

合乎这一点的业务人造具备了伸缩性, 比方 UNIX 下的组件, 每个都是无状态的, 通过管道等组件即可组建出弱小的利用来解决问题. 而 “12 Factor” 业务则强调了其伸缩性, 从而不便构建适应古代互联网倒退速度的巨大业务.

这里有同学会问, 那业务中的本地缓存是否算是状态呢? 比方为了进步业务性能, 会把局部数据间接缓存到过程内存中, 从而缩小了申请 Redis 等缓存的开销, 以进一步晋升性能. 这部分的状态是能够承受的, 但肯定要留神这部分的数据状态必须是容许随时抛弃或存在不同步的. 又或者数据的预热问题肯定要解决好. 这样就能够扔进过程中.

另外还存在一种负载平衡形式, 按节点 hash 进行负载平衡, 即把特定的流量依据其 hash 特色分发给特定实例. 这是违反了无状态个性的, 因为这种负载平衡模式粒度还是太粗了, 不能假设每单位连贯所需要的资源都是固定的, 这种负载平衡模式会造成部分过热, 因而不应该施行. 或仅可作为某些状况下的调试行为存在.

7. 端口绑定 (通过端口绑定提供服务)

What

端口绑定指的是业务通过裸露固定端口的形式来对外提供服务.

How

配置好业务端口就能够了, 当初简直所有业务和组件都反对通过裸露端口的形式来提供服务.

Why

置信当初大部分业务也都是通过端口来提供服务的了, 所以这一点简直不必非凡强调. 其实这更多的是为了兼容现有零碎, 现有零碎跨节点通信最不便的形式依然是通过 IP 协定进行通信. 而 unix socket 等本地协定不反对跨节点通信, CGI 相干的协定尽管能够跨机但反对度不够. 因而通过端口通信形式是最佳兼容计划.

8. 并发 (通过过程模型进行扩大)

What

其实这一点跟第 6 点是一样的, 通过无状态过程来组件业务, 天然就取得了不便扩大的个性.

How

同第 6 点.

Why

这里强调用过程, 行将业务伸缩归于平台治理, 而不是业务本人治理. 多线程模型更适宜业务是个单体利用, 在这种状况业务只有关怀本人的性能就能够了. 然而在微服务的状况下, 业务间是要合作的, 因而业务须要将自身的伸缩交给调度零碎去治理. 而无状态过程模型更不便调度 (启动新的过程就能够了).

我的倡议是, 如果你的业务须要跨机器伸缩, 那么间接应用 k8s. 否则不须要跨机器调度的话, 那就间接用 docker + systemd 来治理容器 (等同于过程) 的生命周期就能够了.

9. 易解决 (疾速启动和优雅终止可最大化健壮性)

What

易解决谋求更小的启动和进行工夫. 以及业务通信尽可能趋于原子性.

How

尽量避免须要预热的业务逻辑并优化启动和退出工夫.

Why

更少的启动和进行工夫意味着调度工夫破费会更少, 能够晋升调度性能. 这对于任何调度零碎都是一样的. 比方地铁调度零碎, 如果每辆车的进站和出站工夫越短, 那么单条线路上能包容的最大车辆数就会晋升. 同样, 如果过程启动和退出的越迅速, k8s 调度也会越迅速, 调度能力 (同时调度的容器数量) 也会更高, 达到调度指标的耗时也会更短 (比方零碎遇到了流量洪峰, 须要紧急扩容 1000 容器, 在 10 秒内启动 1000 容器和在 10 分钟内启动 1000 容器, 这种差距能间接决定现有业务会不会被流量打垮).

更非凡地, 这一点还要求业务的通信尽可能趋于原子性, 这样能够让启停过程对业务造成的冲击降到最低. 如果业务通信始终处于事务当中, 那么一旦遇到启停, 就会造成事务回滚, 这对于性能和稳定性是冲击性的. 这种状况可能就须要针对业务做出批改, 比方采纳补偿性事务来解决这些问题.

10. 开发环境与线上环境等价 (尽可能的放弃开发,预公布,线上环境雷同)

What

即线下的环境须要有一个线上仿真环境, 这须要组件, 工具, 数据等全套工作流程的克隆.

How

强调本地应用的组件与线上统一, 防止与线上的差别. 数据的同步能够思考从定期备份数据中进行复原以取得同步.

Why

得益于容器化, 代码的线下与线上同步是比拟容易的, 只有在 CI/CD 零碎中减少一个向本地环境公布的渠道就能够了.

组件是略微简单的, 我的倡议是组件作为基础设施的一部分进行保护, 比方在镜像库中保护现有线上应用的版本的组件, 本地须要部署的时候间接从镜像库中进行部署. 更高层次则是线上也用本人保护的版本的组件镜像. 这须要肯定的投入来保护根底组件.

数据是最艰难的, 维持一份与线上同步的数据来提供给开发或测试环境天然能够最大水平的复现实在生产场景. 但维持数据同步自身是非常复杂的. 较好的形式有从定期备份的数据正本中复原到本地环境. 但如果定期备份的跨度太大, 本地与线上的数据差距还是很大的. 另一种形式是线上数据库自身的存储是网络的, 比方数据库的存储是 iSCSI, SAN, 又或者是 CEPH, 在这样的零碎中, 能够通过 ByPass 或其余零碎提供的复制形式进行大规模复制. 但这种形式的老本十分高. 又或者能够利用数据库软件自身的正本机制来进行同步, 但这须要投入肯定的开发. 甚至还能够复制线上流量到线下进行实时正本写入, 这样的本地老本很可能无奈承受.

我的倡议是, 在小规模的状况, 利用定期备份数据进行同步是最经济的抉择. 而大型企业因为资源比拟短缺, 就能够依照本人的需要进行定制化了.

当做到了最大化的同部之后, 整个开发流程中测试和公布的工夫将会极大地缩短. 从原有的每周内公布甚至能够晋升为每天内公布. 享受麻利带来的晋升. 传统软件行业可能开发和部署人员都不是雷同部门的, 甚至会存在现场施行人员. 一次交付可能是按月计算的. 而云部署的微服务, 一次小型交付甚至能够在一天内实现数次. 这就是不同架构带来的质的变动.

11. 日志 (把日志当作事件流)

What

即齐全不思考日志组件, 而是最简略的将日志信息写到 stdout 上.

How

将日志信息写到 stdout 上很简略, 大多数编程语言用 printf 等内置办法就能实现. 而后通过对立的日志收集零碎, 日志进入大数据处理系统, 索引零碎, 时序数据库进行存储并最初归档.

Why

适宜伸缩的平台是为了海量流量的大型业务而筹备的, 而大型业务的日志天然也是海量的. 试想一下线上有几万实例的业务, 当初想要寻找特定的日志, 这无异于海底捞针. 而对立收集并解决业务, 就能够利用现有的大数据组件, 索引零碎进行日志检索和解决. 而更先进零碎与日志的交融, 让日志自动化剖析, 阈值解决与自动化调度都变为了可能. 质变产生了量变. 传统的日志解决模式曾经齐全不适宜云服务了. 只有这样组织的日志零碎能力持续提供服务.

12. 治理过程 (后盾治理工作当作一次性过程运行)

What

即一次性工作也该当当作业务去运行, 走失常的编写, 提交, 构建, 公布流程. 而不是轻易写一些脚本扔到线上间接跑.

How

依照失常流程来就好, 而后再配置中将该业务标记为一次性的. 比方 k8s 配置中将 restartPolicy 设置为 Never. 这里要留神须要让业务设置执行结束 Flag. 比方将执行结束报告写入日志零碎, 不便追踪.

Why

我见过一个最乏味的例子是, 有一个同学须要荡涤一批线上数据, 于是他编写了一个脚本, 而后将脚本扔到了线上在 terminal 中间接运行. 过了一会, 他上班了. 于是间接敞开了显示器, 让机器持续放弃 session 继而脚本继续执行. 后果第二天下班. 发现昨晚物业停电电脑关机了. 数据非但没荡涤结束, 反而变成了只荡涤了一半的更脏了的状态. 兴许你会说他应该用 screen 命令. 但我想说的是, 如果这个一次性工作时长很长呢? 比方须要一周能力实现. 这期间的过程治理该怎么办? 这就是这条守则存在的意义.

更狭义的来说, 一些定时工作也归为此列 (比方定期给用户发送邮件的业务), 定时工作同样须要纳入失常的流程来治理. 这样能力保障这些业务具备与线上业务统一的可靠性与可管理性.

一些实际

对于一个尝试进行容器化, 微服务化的小型公司而言, 我倡议的配置大略如下.

代码托管采纳 Gitlab, CI/CD 采纳 Jenkins 搭配 kubernetes 组件. 容器调度零碎采纳 kubernetes. 容器运行时用 docker (runc). 镜像存储用 harbor. 分布式数据存储 / 对象存储用 CEPH.

Jenkins 用 webhook 与 Gitlab 连贯. 而后 Gitlab 建设一个公布专用账号, 每当有新的提交, 就会通过 webhook 触发 Jenkins 的 CI/CD 流程. 通过 repo 外部保护的脚本进行镜像构建 (公布脚本, Dockerfile, k8s 配置全都是跟 repo 走的, 这些都是外部配置. 只有数据库等内部配置才是须要写到环境变量的). 如果你有配置管理系统, 就把配置提交到管理系统, 如果没有, 那就用一个脚本来将配置间接提交到 kubernetes.

构建结束的镜像存储到 harbor 中. 而后构建流程结束后, 进入容器内运行过程, k8s 启动容器, 容器管理系统会主动从 harbor 将刚 build 好的镜像拖下来运行. 至此整个公布流程就完结了.

另外, 还须要保护一个基础设施镜像库, 罕用的根底组件, 比方数据库, web 服务器, 动静库等, 都须要保护一个镜像并提交到基础设施镜像库中. 不便开发的版本对立和后续降级.

本地最好有 2 组至多 8 节点 kubernetes 集群作为测试和 beta 测试应用. 数据同步局部则像我下面提出的, 能够思考用定期备份的数据来进行从新构建.

预期这些业务的搭建和保护须要一个业余的工程师能力搞定. 计算资源的话至多须要 3 台 32 外围的服务器能力实现. 这大略就是最初期的投入了.

总结

总的来讲, “12 Factor” 业务的提出是革命性的. 这些独特的理念催生了 Docker, kubernetes 和 CloudNative. 我回想起我在 2010 年做后端开发的时候, 还没有这些概念. 但我开发的业务或多或少的都有了一些这样的特质.

比方我常常用 Go 和 PHP 编写大型的数据处理业务. 这些业务齐全无状态, 而后利用 systemd 来治理工作的生命周期而不是靠 daemon 过程. 尽管这些业务不须要主动伸缩. 但当我须要他们跑的快一点的时候, 我就会在多个机器上启动多个过程, 来进步从数据队列获取数据的速度. 而他们挂了也不用放心, 间接重启就能够了. 甚至我看到有些偷懒的工程师更是如此, 看到程序有内存透露, 懒得排查问题, 于是让过程定期重启从而防止了被 OOM Kill 掉.

kubernetes 并不是忽然呈现的. 事实中我身边学习 Go 语言最多的恰好是 PHP 工程师. PHP 甚至连 GC 都没有, 为什么? 恰好是因为它自身在作为 fpm 的时候执行的生命周期十分短, 没等填满内存就曾经退出了. 而 kubernetes 内的容器也借鉴了 “ 就让它解体 ” 的相似思维 (当然有很多零碎都是这样的, 比方 Erlang). 容器挂了间接重启. 这样的思维成为了组建宏大零碎的外围.

而 “12 Factor” 这是这些思维中提炼进去的精华. 每一条都值得认真的思考. 大家看完我这篇狗尾续貂之作后, 能在日常工作的架构中看到相似的影子, 或者有一些感同身受的话, 就是我最大的荣幸了.

退出移动版