持续交付2.0

做好依赖管理的十五条准则(上)

Image

“直接使用在互联网上找到的一个依赖包,就如同聘请了一个完全不了解的软件开发者。”

1

依赖管理是Code Review
的一个重要检查项

2021 年元旦过后的第一周,我们对 1000 多行用 golang 代码进行 Mob CR,类似于「扎堆编程」,一共持续了 4 个多小时,大家都很投入。

Mob CR简单地说,就是一组开发人员一起对同一段代码进行代码检视,

Mob CR 到底如何做?有哪些步骤,请看这里。

其中的一个讨论点就是:「如何正确引入和使用外部依赖包」。

打开 go.sum 文件,你会发现,有很多依赖包,它们会存在多个不同的版本。而且,有一些版本号甚至是以「0.」开头,后面是一长串日期加数字的版本标识,这可能意味着,它们都是不稳定的版本,如下图所示。

Image

最终通过代码重构以后,我们这段代码的依赖数量大幅度减少。

这要求每个开发人员都要克制,

  • 不要因图一时之方便快捷,就随便引入外部依赖

  • 如果真的需要使用这个依赖,先要在组织所用的依赖库管理系统中查找是否已经引入了类似功能的外部包

Image

2

为什么依赖管理这么重要

与本世纪初相比,当今软件的生产速度已经加快了很多,一个很重要的原因是有大量的开源类库可用,不需要重复造轮子。

但并不是所有的类库都是经过严格测试,安全和质量是有保证的。

相反,绝大多数开源类库的质量是存疑的。即便是久经考验的 Log4j ,在 2021 年 12 月也暴露出了漏洞。

而大厂支持的开源项目,也同一样有可能存在安全疑点,比如阿里在 Github 上的项目 Nacos,有一个Issue,#4593:Report a security vulnerability in nacos to bypass authentication 。

所以,对于一个商业组织来说,软件系统的依赖管理挑战要比二十年前严峻得多。

软件依赖中的很多严重风险被我们忽略了。

简单、细粒度的软件复用转变得如此之快,快到我们来不及梳理出如何选择依赖、有效使用依赖的最佳实践,甚至无法确定何时应使用依赖,何时不应使用依赖。

我写本文的目的在于:提高对软件依赖中的风险的认识,并希望抛砖引玉,共寻良策。

3

什么是依赖管理

在现今的软件开发世界里,依赖(Dependency)是指:「你的应用程序需要调用的外部代码」。

使用依赖软件包,可避免一些重复劳动,包括设计、编写、测试、调试、维护。

在本文,我们称这样的代码单元为包( package ),有些系统可能称之为库( library )或模块(module),但意思都一样。

使用别人已编写好的依赖包,这种情况早已有之。

任何呈个开发人员可能都经历过:手动下载依赖、安装依赖,譬如 C 语言的 PCRE、zlib,或 C++ 的 Boost、Qt,或 Java 的 JodaTime、JUnit。这些包都是高质量的、已被充分调试过的,并且需要一定专业能力才能开发出来。

在二十年前,对于开发者来说,为得到这些功能,需手动下载、安装、更新。

这种琐事虽然很无聊,但总比从头开发容易得多。由于需要比较多的手动操作,所以这些包通常都比较大,毕竟操作一次不容易,一次就下载全吧。

依赖包管理器( Dependency Manager ,或叫包管理器 —— package manager )可自动下载、安装所需的依赖包。

依赖包管理器的出现,让依赖包的下载和安装都变得更容易。

手工开销变低了,再小的包也可轻松地发布、复用。

举例来说,Node.js 的依赖包管理器 NPM 提供了超过 750,000 个包。其中有一个叫 escape-string-regexp 的包,它仅有一个函数,用于转义正则表达式。其全部代码如下所示:

var matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g;

module.exports = function (str) {
if (typeof str !== 'string') {
throw new TypeError('Expected a string');
}
return str.replace(matchOperatorsRe, '\\$&');
};

在依赖包管理器出现以前,很难想象可以发布这样一个仅 8 行代码的函数库。因为那时的发布代价太高,而收益甚微。

但 NPM 将这些成本降到几乎为 0 ,让再微小的功能都可以打包复用。截至 2019 年 01 月 下旬,the escape-string-regexp 这个包已被大约 1000 个其他 NPM 包依赖,更别提那些开发者为自己写的没公开发布的包了。

现在几乎每一门编程语言都有自己的依赖包管理器,如下面每个包管理器都包含超过十万个包:

  • Maven(Java)

  • Nuget(.NET)

  • Packagist(PHP)

  • PyPI(Python)

  • RubyGems(Ruby)

这种细粒度、广泛的软件复用方式,是过去二十年软件开发中最重要的变化之一。 面对这些变化,如果不引起重视,后果会很严重。

4

会在哪儿出问题

在本文中,一个包( Package )是指:你从互联网下载下来的一份代码(或者二进制包),它是由你不认识的人设计、编写、测试、调试的。

一旦开始使用这些代码,你的程序可能会面临因依赖问题而导致的失败、缺陷,和安全漏洞。

事实上,你现在手上正写的程序就依赖于这些从互联网上的陌生人那里得到的代码(比如 github 上的仓库)。

这其实并不是很安全的做法,但是,为什么还有那么多人愿意这么做呢?那是因为:

  • 简单方便

  • 程序能快速跑起来

  • 其他人都在这么做

  • 更重要的是:这似乎是一种惯例并延续到现在。

然而,我们忽略了一些重要的区别。

几十年以前,大部分开发者是信任他们所依赖的别人写的软件,譬如:操作系统、编译器。这些软件是从已知的来源购买的,通常还包含一些支持协议。尽管这些软件依然会有潜在的缺陷,或奇怪的问题(引:3),但至少我们知道该找谁,甚至可以寻求商业或法律支持。

但是,现在的开源软件几乎可以零成本在互联网上分发,这颠覆了以前那种「软件需要花钱购买」的方式。

在软件复用的方式比较麻烦时,很少有项目会去发布可复用的代码包。

尽管开源软件都有许可证,并隐含「适用的范围」,但通常都会被大家忽略。

一个项目的名声如何,反而往往是人们决定是否使用它的重要因素。对软件的信任已从「商业和法律条款」变成了「名声」。

许多常见的早期的包仍保持着很好的声誉,包括:

  • BLAS(1979)

  • Netlib(1987)

  • libjpeg(1991)

  • LAPACK(1992)

  • HP STL(1994)

  • zlib(1995)

依赖包管理器已将开源代码的复用粒度降低到 1 个函数几十行代码的规模,这真的是一项重大的技术成就。

依赖包管理器提供了无数的包供使用,我们信任这些包,但是,其在商业、法律、信誉的支持并没跟上步伐。

对于开源软件,我们用得多,却对其潜在风险关注得不多。

采用不良依赖的成本可认为是所有可能的不良结果的总和,也即每个不良结果的成本乘以其发生的概率(风险)。

Image

在不同类型的项目上,使用这些外部依赖,产生的结果也不相同。

比如,在某个人的个人爱好类项目上,只要他自己开心就行,无非是浪费一些时间,缺陷的影响也不大,其不良后果的成本几乎为零。甚至发现并调试这种缺陷会给他自己带来一些乐趣。

然而,在商业生产环境中的软件需要长期运行,这类外部依赖引发的错误成本就可能很高,比如导致服务器停机、敏感数据泄露、客户利益受损,甚至公司也可能因此而倒闭。这种故障引发的高成本使得管理这类问题带来的风险变得尤为重要。

我们实际上已经积累了一些相关的经验。

5

使用依赖包的注意事项

在公司招聘员工时,我们通常不会聘请一个我们完全不了解的软件开发者,而是先要了解一些相关信息,比如笔试、面试、背景调查等等。

直接使用在互联网上找到的一个依赖包,就如同聘请了一个完全不了解的软件开发者。

我们还是有必要做一些基本的检查,例如看一看这些代码是否会产生问题。如果发现的是一些小问题,你可以通过自行修订,或者提出 Issue ,提交补丁(如果能找到源头作者或社区的话)来规避它们。

但是,如果是严重问题,就应不使用这个软件包,而应该再去寻找替代品,或者干脆自己动手开发一个。

请记住,在互联网上发布开源软件包,它的作者是希望它有用,但并不保证其可用性或提供支持。

在开源包的开发和使用过程中,你甚至需参与调试优化。

正如 GNU 通用公共许可证所警告的那样,「程序的质量和性能的全部风险是并存的。若程序有缺陷,导致的维护和修复成本需自行负责。」(引:4)

关于,如何评判一个依赖包,需要注意哪些呢?我们将在下一篇文章中讨论这个问题。

这些注意事项主要包含两个方面,(1)如何选择依赖包?(2)如何正确使用依赖包?

如下所示:

(1)关于如何选择依赖包?

  1. 设计方面

  2. 代码质量

  3. 自动化测试

  4. 调试与维护

  5. 被使用的次数

  6. 安全性

  7. 许可证

  8. 检查一下依赖包的依赖

(2)如何使用依赖包?

  1. 通过自己的测试来检查

  2. 对外部依赖进行封装

  3. 隔离依赖

  4. 避开依赖

  5. 依赖的更新策略

  6. 对依赖的监控

  7. 什么时机用来检查是否继续使用原有的依赖

关于每一点的详细讨论,请参考本系统的下两篇文章。

(未完待续)

参考

  1. Rachel Potvin and Josh Levenberg, “Why Google Stores Billions of Lines of Code in a Single Repository,” Communications of the ACM 59(7) (July 2016), pp. 78-87. https://doi.org/10.1145/2854146

  2. Russ Cox, “Go & Versioning,” February 2018. https://research.swtch.com/vgo

  3. Ken Thompson, “Reflections on Trusting Trust,” Communications of the ACM 27(8) (August 1984), pp. 761–763. https://doi.org/10.1145/358198.358210

  4. GNU Project, “GNU General Public License, version 1,” February 1989. https://www.gnu.org/licenses/old-licenses/gpl-1.0.html