服务器部署的决策与实践(2) 软件版本的选择

上周讲了服务器系统的选择,这周回来继续讲软件版本的选择,更具体一点来说是服务端运行环境中软件版本的选择。

每个人遇到的情况不同,做出的选择也不同,所以这个问题并没有标准答案,而会是一些方向性的思考。

我的出发点是让尽量少的人来维护尽量多的项目,所以得出的结论可能会和网上常见的文章不同。

就让我学学《程序员修炼之道》先给出一个一般性原则:

Tip
出于减少开发和维护成本的目的,必须减少组件数量,如果组件数量无法减少,就减少它们组合的数量。

这里的组件是一个非常宽泛的概念,既可以指操作系统,也可以指编程语言里面用到的包或库,还可以指数据库、缓存这样独立运行的应用。

这是一个寻找最大公约数的过程。

本文是 服务器部署的决策与实践 系列的一部分。

减少组件

比如你的项目既有面向微信用户的纯互联网类应用,又有要求将核心数据存放在单位内部的政府类项目,那么就不要让前者使用 Serverless 环境,因为这与后者的需求不符,你还是得搞一套本地部署的环境,结果就要应对两种环境。

再比如在 Node.js 中,网络请求库可以用 axios 也可以用 request,那么首先,你就不该在一个项目中用 axios ,另一个项目中用 request,再考虑到 axios 还可以在浏览器环境中使用而 request 不行,那么你就该选择 axios。

数据库也类似,不要一会用 MySQL,一会用 SQL Server。但数据库又比较特殊,有时候客户会指定数据库,或者给你一个已有的数据库进行对接甚至在其基础上进行开发。有意思的是,如果客户指定编程语言且不是常用的,你大概率不会接下这个项目,如果数据库不是常用的,你多半还是会想办法去适应它,而非直接放弃项目。这种情况下,可以从编程语言下手,通过在平时就使用非数据库绑定的 ORM 库等手段,消除或减少数据库的影响。这个问题上我的解决方法有点特殊,是通过 PostgreSQL 的 Foreign Data Wrapper 功能,它可以把一个外部数据源包装到 PostgreSQL 里面,编程语言只要和 PostgreSQL 交互,读写第三方数据库的操作由 PostgreSQL 作为代理进行。

极端情况是用 Node.JS 作为服务端的开发语言,以 PL/V8 编写 PostgreSQL 中的函数,用 Html5 或 Electron 做客户端,端到端的采用 JavaScript。但这种就属于偏执而不是务实,因为用 PL/V8 写数据库函数并不能取代 SQL,所以被裁掉的是 PL/V8。

裁剪不能过度的牺牲功能、性能或用户体验。但合理的减少组件有助于将经验集中到少数被选定的工具上,促使开发人员熟练掌握这些工具,充分发挥它们的特性和性能。

减少组件的组合

如果你已经选定了核心的组件:比如操作系统、编程语言、数据库,接下来就要减少组件的组合,这里主要就是针对版本了。

使用发行版中的版本

最简单的方法是选择一个发行版,然后用官方源的版本,比如 RHEL 8 里默认的 Node.js 版本是 10,那么你就固定获得了 RHEL 8 + Node.js 10 的组合,而不会在 RHEL 8 上运行 Node.js 12。好处是环境单一且维护方便,所有更新都只要一个 yum update,甚至可以设置成定时任务自动运行。但问题是过于“稳定“,以及虚假的“安全”。

过于“稳定”的问题大家都懂,就是版本太旧,开发为维护买单。开发者只能使用过时的版本,新语法用不了,“语法糖”吃不上,不但影响开发效率,还影响开发者的心情,明明有更好的东西却不能用。这时再出个好用又只支持新版本的库,就会进一步放大效率和心情上问题。

虚假的“安全”是指什么呢?系统升级补丁解决的只是运行环境本身的安全问题,解决不了一大批第三方库的问题。第三方库主要还是围绕官方支持的版本进行开发和维护,等官方废弃了某个版本之后,第三方库也会年久失修,某些安全补丁不会反向 patch 到旧版,导致你的应用也留有一堆漏洞,如果这个第三方库是一个被广泛使用的框架,那么其危害就更大。

现在的应用对外部依赖库的使用越来越多,上面两个问题也愈发突出,使用发行版自带的过时软件会越来越不可行。

所以 RHEL/CentOS 8 也引入了 AppStream 的概念,对一个软件同时提供多个版本,让想用新版本的用户有了一个更可靠和便捷的途径。

debian

版本的“对齐”

如果你用的发行版不像 RHEL/CentOS 8 具备 AppStream,那么可以考虑下面这个策略。

Tip
列出核心组件的生命周期,然后尽量进行“对齐”,组合出一种适合自己的“发行版”,然后锁定版本直到下一个周期。

打个比方,我计划使用 Debian,Node.js 和 PostgreSQL,那么从各渠道可以获知它们最近几个版本的生命周期分别是这样的。

Table 1. Debian 生命周期

Version

Code name

Release date

End of life date

EOL LTS

11

Bullseye

~2021-07*

10

Buster

2019-07-06

~2022

9

Stretch

2017-06-17

2020-07-06

~2022

Table 2. Node.js 生命周期

Release

Codename

Initial Release

Active LTS Start

End-of-life

v16

2021-04-20

2021-10-26

2024-04-30

v14

Fermium

2020-04-21

2020-10-27

2023-04-30

v12

Erbium

2019-04-23

2019-10-21

2022-04-30

v10

Dubnium

2018-04-24

2018-10-30

2021-04-30

Table 3. PostgreSQL 生命周期

Version

Current minor

First Release

Final Release

13

13.1

2020-09-24

2025-11-13

12

12.5

2019-10-03

2024-11-14

11

11.10

2018-10-18

2023-11-09

10

10.15

2017-10-05

2022-11-10

我们会发现 Debian 10 的 Release date 是 2019-07-06,Node.js 12 的 Active LTS Start 是 2019-10-21,PostgreSQL 12 的 First Release 是 2019-10-03,相对是比较接近的,那么就可以把他们作为一种组合来使用,接下来直到 2021年10月之间,你所有的服务器都采用 Debian 10 + Node.js 12 + PostgreSQL 12 的组合。到了 2021年11月,你再切换到 Debian 11 + Nodejs 16 + PostgreSQL 14 的组合(Debian 11 和 PostgreSQL 14 的发行时间还未确定,但根据发布周期可以大致推测,并按实际情况进行调整)。

这样一来减少组合的目的就实现了,我们实现了一个两年一轮的周期。在每个周期中,新部署的服务器只使用一种固定且较新的组合。据此推算,在 4 年中,我们只要维护 2 种开发环境,一种作为当前版本,一种作为维护版本。在 6 年中,还是只要维护 2 种开发环境,因为最早的一种已经过时了,将被新的组合替代,形成隔代替换的“滚动”更新形式。

这个周期,你可以按照自己采用的核心组件的发布周期来确定,但要注意不要太短,过短会增加维护量,也不要太长,过长容易超出官方的支持周期,难以获得安全补丁。

上面讲的周期内的版本固定,并不是 RHEL 中的那种固定,而是一种向下兼容的相对固定,比如在一个周期之内 PostgreSQL 是允许从 12.0 升级到 12.5 的,但不允许升级到 13。这个兼容性的升级也看自己的需求,如果没有安全问题或遇到明确的BUG,也可以选择不升级,包括主要编程语言中使用的第三方库也得遵循这个原则,只进行兼容性的升级。但是一旦决定升级,就要将所有活跃项目一起升级。

这种方法也有明确的缺点,到了轮换的周期,必须把更早的环境淘汰掉,否则就会增加需要维护的环境数量,而即便你愿意维护,也将很快面临没有安全补丁可用的境况。另一方面,公司里可能会有无人维护的项目(更多是不愿意继续投入的项目),它们要何去何从呢?在我看来,这样的项目其实已经死了。

非核心组件

比如在我的项目里,基本上都会用到 nginx ,但通常只是做个简单的反向代理或者负载均衡,这样的组件就不必费心思去搞版本的“对齐”,只要使用系统自带的就够了。所有非核心的组件没有特殊需要都使用发行版自带的来降低维护成本。

尾声

本文中,我们给出了软件版本选择的一般性原则,通过减少组件和它们之间不同版本的组合来平衡维护、开发和安全性之间的关系。讨论了使用发行版自带软件和自组软件的优缺点。还给出了一个自组“发型版“的策略。

最后也留下了几个问题没有解答,自组“发行版”的软件通过什么形式来安装,又如何更新?有多台服务器时怎么优化这个流程?组件的官方生命周期太短,坚持不到隔代升级怎么办?新旧版本何时切换,正在开发中的项目如何选择版本?

我们下周继续。

花絮

我们用服务器也好,用服务器上的操作系统也好,都不是为了用它们本身,而是为了运行我们的应用。正是基于这个前提,Docker、Serverless 才会应运而生。

Serverless 完全免除了用户对操作系统和运行环境本身的管理,但也有几问题:首先你的语言不一定有 Serverless 的版本,其次使用上也会有一定的限制,不能完全按照本地部署的方式去编写软件,要避开一些特定的功能,这是一个踩坑的过程,更麻烦的是目前的 Serverless 没有标准,是厂商绑定的,每家云服务商不仅提供的语言和版本不同,连坑都不尽相同,后面一旦要切换就得付出代价。如果不是有极端的动态扩容需求,单纯为了免除环境配置,并不十分上算。