大约是七年前,笔者写了一篇大开脑洞的文章,畅想了运维的一些理念是如何可以作用于物理世界从而产生一些积极的影响。

七年过去了,曾经的一些想法也许已经不再是臆想了。政府监控市民活动的手段越来越健全,却并不是为了防止踩踏事件的再次发生,社会数字化也不再是一句空话,各种 DAO( decentralized autonomous organization )组织如雨后春笋般层出不穷,企图革掉传统公司的命,变革还在继续…

时至今日,以 Web3 和 Crypto 为代表掀起的去中心化运动正在如火如荼的进行着,究竟这波浪潮过后会带来什么?是像郁金香泡沫那样泡沫破裂后烟消云散,还是会如同这过往二十年的互联网浪潮,从根本上改变人们的生活方式呢?在谜底揭晓前,也许每个人心中都会有自己的答案。

在这里,与其继续臆想往后十年可能会发生的一些变革,笔者倒想借此机会,通过这篇文章来谈谈一个微观领域的去中心化:运维是如何去中心化的,希望可以对整个去中心化运动如何真正引领一些实质性的改变带来一点启发。

Pull Model

在机器世界里,最早的去中心化也许可以从分布式系统说起。原本掌握生杀大权的主节点自那以后被随之拆分成了地位相对平等的多个副本节点,最常见的组合应该是 3 台或 5 台机器。

然而,如果再细分到运维领域的话,个人认为第一次去中心化恐怕应当是始于监控开始引进 pull 模式这套全新的玩法。

Prometheus,那个从神灵那里盗出火种的人,这次又在监控领域大展身手。依托 Google 内部 BorgMon 这套经过生产检验的成熟体系,创立该项目的团队向社区提出了一种和传统的 Zabbix 式的监控系统完全不同的玩法:我们也许可以让每台机器自己提供它想要暴露的 metrics 指标,然后这些机器不必再主动推送( push )数据到一个集中的地方,相反,我们可以让想要采集这些数据的服务主动去拉取( pull )它们的监控指标。

但是,从某种程度上来说,采用 pull 模式也许并不会给运维省下多少工作量:pull 和 push 模式都需要运维在机器上部署对应的 agent ( exporter )来提供相应的监控指标,采集和存储数据的服务端都会面临性能瓶颈的问题。

那么问题来了,既然如此,pull 模式到底给我们带来了什么好处呢?

首先,采用 pull 模式就意味着 agent 不再拥有一个名义上的 master 了,换句话说,任何实体都可以作为服务端去采集这份数据,这也就自然而然地规避了当一个 master 机器挂了的时候我们还得想办法让 agent fallback 到备用节点的这种尴尬情况,整体的监控架构变得更为简单和易于维护。

其次,除了在 agent 端不需要指定 master,服务端与此同时也同样获得了指定想要采集监控指标的机器名单的自主权利。比如 Prometheus 就支持各种服务发现( SD )插件,允许通过服务发现的形式来拿到目标监控机器的列表,这在高度动态的云原生环境里几乎是一项必备能力。

最后,由于 agent 和 server 没有一个确定的关联关系,这也变相地推动了 metric 格式的标准化。这其实就像是人与人之间对话一样,如果是确定的两个对象的话,那只要掌握彼此熟悉的语言即可实现对话。但是如果某个人不知道有谁会找他搭话的话,那他最好是掌握一门标准通用的语言,不然保不准会鸡同鸭讲、语言不通。

那么,既然去中心化的 pull 模式能够给我们带来这么多好处,Prometheus 的这套 pull 方案又最终被实际验证是可行的,那我们能否把这个玩法推广到运维的其他细分领域呢,比如配置管理?实际执行下来,也许没有想象的那么简单…

Pull Model Job?

和 Prometheus 利用 pull 模式来采集监控数据这种做法有所区别的是,如果想要让配置管理去中心化,我们需要先解决几个问题。

首当其冲的便是该如何告诉目标机器,它的预期配置应该是怎样的呢。要知道,配置管理并不像监控那样只需要请求一个接口拿到监控指标的数据即可。它的核心诉求在于通过某种类似 agent-server 架构的方式实现对目标机器某些状态的变更,例如让一些机器装上某些软件,又或者是重启某台机器上跑着的 nginx 服务。那么,如果采用 pull 模式的话,该如何让这些目标机器知道服务端的诉求呢?

此外,即便假定这些目标机器有办法知道它们要达到的预期状态,像 Prometheus 需要在机器上安装 exporter 一样,我们也需要一个 agent 去帮助实际执行达到预期状态所需要执行的一系列操作。

最后,撇开前面这两个问题,在实际执行任务的过程中,不同于 Prometheus 去执行一个 http 调用,一次 job 调用可能会是一个冗长的过程,甚至是需要异步执行的,该如何控制这个任务的执行过程呢?倘若应用了错误的状态,我们至少需要一个办法去终止这个错误的任务吧?那岂不是又需要重新启用 push 模式了?

这里,我们不妨来看看业内实际是怎么做的。以 Ansible 为例,它同样也提供了 ansible-pull 来实现去中心化的配置管理。

说来也简单,和 ansible 自身类似,ansible-pull 也是一份简单的脚本,但是它的工作原理则和 ansible 存在很大的出入。首先,用户需要为 ansible-pull 指定一个 git 仓库,作为配置的数据源。其次,用户需要指定一份对应的 inventory host,这样 ansible-pull 在具体执行时便可以判断当前这台机器是否需要应用对应的角色。然后,为了保证 git 仓库发生变化时目标机器会触发相应的任务去更新对应的状态,我们还需要再配置一个 crontab 定期去执行 ansible-pull 。

看上去,这样做的话,前面提到的第一个问题和第二个问题似乎都算是迎刃而解:git 仓库里的配置数据会告诉每台目标机器它们各自应该达到的预期状态,通过 crontab 我们也能定期调协目标机器,使其达到预期的配置状态。但是,似乎最后一个问题还是没有能够解决?假设某个运维工程师不小心配错了某台机器的角色,如果不采用 push 模式去打断正在执行的 ansible-pull 任务的话,变更导致的故障显然就是无法避免的了。

但是倘若这个问题无法解决的话,ansible-pull 这种模式也许只适用于做一些危害不大的变更操作,比如安装一些必备的工具软件包等。如果真是这样的话,采用 pull 模式来执行配置管理就会变得比较鸡肋。

到这里,事情似乎走到了一个死胡同,难道就真的没有其他办法了吗?

Operator

坦白讲,在配置管理的时代里,这也许真的是一个无解的问题。这也是为什么主流的配置管理工具推崇的仍然是 push 模式的原因。然而,不曾想容器和云原生革命竟然会给 pull model job 带来一次重新焕发活力的机会。

在云原生的时代,我们可以通过一份简单的 YAML 文件来声明服务的预期配置状态,K8s 则会通过自身提供的一套调谐控制逻辑,努力让这些服务达到对应的预期状态。此外,配置管理时代看似无解的控制 job 的问题,在云原生时代 K8s 给出的解法则是:放弃精细控制( micromanage )具体的 job 过程,转而尝试去定义资源的配置( spec )和状态( status ),它会容忍局部的失败,然而它会尝试通过一次次的调谐,试图让服务重新回到正轨。

换句话说,job 无法中断并不是什么大问题,下次跑的时候再修正就是了。实现这套机制背后的功臣,自然就是 K8s 的这套 watch 机制。在一个高度动态的环境里,出错显然是再正常不过的事情了,我们要做的就是能够通过某种事件驱动的机制及时发现错误和停止继续执行错误的操作,并且能够通过一个优雅的方式去尝试修复这个问题。

令人惊艳的是,依托着 K8s 的这套调谐-控制模式,社区甚至已经做到了像发布这样原本看上去是非要采用 push 模式不可的 job 也下沉到了 Operator 里!比如,今天我们可以通过 Argo Rollout 去定义服务采用的发布策略,以及具体的执行步骤,包括每个步骤允许灌入多少比例的新版本流量等细节。而这一切全都是在 Operator 里预定义好的一套通用逻辑,换句话说,这个过程甚至可以是全自动化的!

那么,他们是怎么做到将原本 push 逻辑的发布过程转变成 pull 模式的呢?答案依然是 K8s 的 spec + status + reconcile 这套核心逻辑。举个例子,ArgoRollout 在它的 spec 里定义了一个 restartAt 字段,所有启动时间在 restartAt 时间戳之前的 Pod 在控制器调谐时都将会被踢出,控制器随后会创建出一个新的 Pod 来替换旧的实例,如此间接地实现了重启服务的效果。

这么说起来,我们之前似乎是有点一叶障目了。看上去,我们需要做的是放弃精细控制具体作业的想法,转而去为每个服务定义出一份理想的配置声明。这就好比是在一家企业里为员工制定 OKR 标准一样,我们只需要制定出这个 O( Objective,目标 ),每个员工自己去努力实现对应的 KR( Key Result,关键结果 )即可,老板有时候往往未必需要像工厂主那样,时时刻刻盯着员工,看员工是在干活还是在偷懒摸鱼,生怕因为偷懒而耽误项目进度。

GitOps

那么,话说回来,今天这个时代,我们还可以借助什么方案来实现更高程度的去中心化吗?答案自然是肯定的,采用 GitOps 就是了!

我们只需要在 Git 仓库里以 YAML 或类似 DSL 格式的文件形式声明定义我们想要达到的预期配置( spec ),然后驱动各种引擎(可以是 k8s、terraform 甚至一堆保证操作幂等的脚本)去调谐( reconcile )基础设施,让它达到这个预期的状态( status )即可。任何人,只要有权限,都可以去尝试为他所关注的基础设施部分定义一个理想的状态,而底下的基础设施则会根据这份声明,自治式的去努力达到这个预期状态。

结语

最后,尽管我们仍然没有做到完全 100% 的去中心化(注1),这一次去中心化运维的浪潮却也确确实实地解放了运维的生产力。更重要地是,通过去中心化,我们实现了基础设施层面更高程度的自治。见微知著,推广到现实世界的话,这波去中心化浪潮或许真的有机会实现社会和政府层面某种程度上的去中心化呢?且行且看罢。

注1:笔者认为,现实世界实际上也不可能会实现 100% 的去中心化,就如同不可能实现 100% 的国有化一样。