转载:https://www.thebyte.com.cn/architecture/Immutable.html
如果你对一位开发工程师说:“你的软件有 bug”,他大概率这样回:“我本地跑好好的,怎么到你那就不行?”,或者你是个运维工程师,维护线上系统时,肯定吐槽过:“谁又改了配置文件…”。
本节,我们讨论上述问题的根源 —— 基础设施的“可变”与“不可变”。
可变的基础设施
从管理基础设施的层面看:“可变”的基础设施与传统运维操作相关。例如,有一台服务器部署的是 Apache,现在想换成 Nginx。传统手段是先卸载掉 Apache,重新安装一个 Nginx,再重启系统让这次变更生效。
上述的过程中,装有 Apache 的 Linux 操作系统为了满足业务需求,进行了一次或多次变更,该 Linux 操作系统就是一个可变的基础设施。可变的基础设施会导致以下问题:
上述的过程中,装有 Apache 的 Linux 操作系统为了满足业务需求,进行了一次或多次变更,该 Linux 操作系统就是一个可变的基础设施。可变的基础设施会导致以下问题:
- 重大故障时,难以快速重新构建服务:持续过多的手动操作并且缺乏记录,会导致很难由标准初始化的服务器来重新构建起等效的服务;
- 不一致风险:类似于程序变量因并发修改而带来的状态不一致风险。服务运行过程中,频繁的修改基础设施配置,同样会引入中间状态,导致出现无法预知的问题。
可变的基础设施带来的运维之痛,引得业内技术专家 Chad Fowler 这样吐槽:
要把一个不知道打过多少个升级补丁,不知道经历了多少任管理员的系统迁移到其他机器上,毫无疑问会是一场灾难。
不可变基础设施
2013 年 6 月,Chad Fowler 撰写了一篇名为 《Trash Your Servers and Burn Your Code: Immutable Infrastructure and Disposable Components》的文章,提出了 Immutable Infrastructure(不可变基础设施)的概念[1]。这一前瞻性的构想,伴随着 Docker 容器技术的兴起、微服务架构的流行,得到了事实上的检验。
不可变基础设施思想的核心是,任何基础设施的运行实例一旦创建之后就变成只读状态。如需修改或升级,应该先修改基础设施的配置模版(例如 yaml、Dockerfile 配置),之后再使用新的运行实例替换。例如上面提到的 Nginx 升级案例,应该准备一个新的装有 Nginx 的 Linux 操作系统,而不是在 Linux 操作系统上原地更新。

此刻,读者是否灵光一现想起前面介绍的容器技术。构建镜像运行容器之后,如果出现问题,我们不会在容器内修改解决,而是修改 Dockerfile 在容器构建阶段去解决。
从容器的角度看,镜像就是一个不可变基础设施。工程师交付的产物从有着各种依赖条件的安装包变成一个不依赖任何环境的镜像文件,当软件需要升级或者修改配置时,我们修改镜像文件,新起一个容器实例替换,而不是在运行容器内修改。有了镜像之后,本地与测试环境不一致、测试环境与正式环境不一致问题消失殆尽了。
相比可变基础设施,不可变基础设施通过标准化描述文件(如 yaml、dockerfile 等)统一定义,同样的配置拉起的服务,绝对不可能出现不一致的情况。从此,我们可以快速拉起成千上万一模一样的服务,服务的版本升级、回滚也成为常态。
声明式设计
声明式设计是指一种软件设计理念:“我们描述一个事物的目标状态,而非达成目标状态的流程”。至于目标状态如何达成,则由相应的工具在其内部实现。
和声明式设计相对的是命令式设计(又叫过程式设计),两者的区别是:
- 命令式设计:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现;
- 声明式设计:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。
很多常用的编程语言都是命令式。例如,有一批图书的列表,你会编写下面类似的代码来查询列表中名为“深入高可用系统原理与设计”的书籍:
function getBooks() { var results = [] for( var i=0; i< books.length; i++) { if(books[i].name == "深入高可用系统原理与设计") { results.push(books) } } return results }
命令式语言告诉计算机以特定的顺序执行某些操作,实现最终目标:“查询名为《深入高可用系统原理与设计》的书籍”,必须完全推理整个过程。
再来看声明式的查询语言(如 SQL)是如何处理的呢?
使用 SQL,只需要指定所需的数据、结果满足什么条件以及如何转换数据(如排序、分组和聚合),数据库直接返回我们想要的结果。这远比自行编写处理过程去获取数据容易的多。
SELECT * FROM books WHERE author = 'xiaoming' AND name LIKE '深入高可用系统原理与设计%';
接下来,再看以声明式设计为核心的 Kubernetes。
下面的 YAML 文件中定义了一个名为 nginx-deployment 的 Deployment 资源。其中 spec 部分声明了部署后的具体状态(以 3 个副本的形式运行)。
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment labels: app: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80
该 YAML 文件提交给 Kubernetes 之后,Kubernetes 会创建拥有 3 个副本的 nginx 服务实例,将持续保证我们所期望的状态。
通过编写 YAML 文件表达我们的需求和意图,资源如何创建、服务如何关联,至于具体怎么实现,我们完全不需要关心,全部甩手给 Kubernetes。
只描述想要什么,中间流程、细节不需关心。工程师们专注于 what,正是我们开发软件真正的目标。