软件开发的依赖问题

  • engineering

posted on 24 Oct 2021 under category translation

最近读到一篇 Russ Cox 写的关于软件依赖管理的文章, 其中有些示例很有趣. 整理一下, 分享给大家.


几十年前, “软件重用” 这件事, 更多停留在讨论层面. 想要使用别人写的功能, 需要费很大劲. 那时候你得 手动编译, 安装. 重用并不是很容易的事儿. 这也意味着被重用的软件一般都比较大, 一般有专门的团队 去开发和维护它们, 质量也因此更高一些.

随着各种依赖管理工具的和开源社区的发展, 繁琐的依赖安装过程被自动化, 可以很容易写一个很小的库(或 包), 然后发布出去让别人用. 这是件好事儿, 但是也带来一些问题. 而且随着软件越来越依赖第三方提供的功能, 我们必须重视起来其中的问题.

假设我们的软件依赖了几个有问题的包, 那么我们怎么预期这些包带来的代价呢? 基本上, 可以用一个公式表示:

预期代价 = (每个包的代价 * 它出问题的概率) 之和

其中, 每个包带来的开销, 基本上取决于怎么使用它. 如果我们的软件只是自己写着玩儿, 那基本上开销为零; 而如果它被用到了生产环境, 服务器可能挂掉, 敏感数据可能泄露, 客户可能遭受损失, 公司也可能因之倒闭. 这么高的代价, 要求我们必须认真评估风险, 并尽力减少依赖管理中的问题. 但现实是, 很多依赖管理工具, 都把精力放到了如何减少下载和安装依赖的成本上, 而非如何减少依赖出错带来的成本上.

除了期待能有一个更好依赖管理工具, 我们能做点什么?

仔细检查依赖

你不会不面试就直接雇佣一个员工, 也不应该随随便便就依赖于完全陌生的另一个人开发的包.

设计

  • 包的文档是否清晰? 有没有提到为了向后兼容, 你的代码需要注意什么?
  • API 设计的如何?

代码质量

读一些它的代码:

  • 作者是不是用心
  • 代码风格一致吗
  • 你愿意自己去调试这种代码吗? 可能你未来真的需要去调试它

发展一套系统性的方式去检查代码质量.

一些工具可能会有帮助, 比如 Infer 或者 SpotBugs. Linter 其实没什么帮助, 你不应该把焦点放到 “花括号应该在什么位置” 这种事儿上.

同时也要保持开放心态, 别人的设计理念可能和你不一样, 但不代表代码质量差. 比如 SQLite 源码全放到了单个 200,000 多行的源文件中, 但如果仔细研究一下的话, 它有很好的理由这么做.

测试

它有自动测试吗? 能跑过吗?

调试

找找它的问题追踪页面, 报告的 bug 多吗? bug 有多久没解决了? 有多少修复了?

维护

看看它的版本提交历史, 是否还在积极维护中?

积极维护的时间一般来说越长越好, 但的确有些包真的算是 “完成” 了, 这种包可能会有很久没有提交记录.

使用

是不是有很多其他软件也在使用它?

安全

它是否小心处理的输入? 它有没有公开在 NVD (https://nvd.nist.gov/vuln/search) 的安全问题?

许可

它有许可吗? 这种许可符合你公司的项目吗?

比如, Google 就不允许 AGPL 一类许可(义务太繁重)以及 WTFPL 一类许可(太模糊不清)下的包.

依赖的依赖

理想情况下, 你应该根据上述内容逐一对依赖的依赖进行同样的检查.

很多人都没看过自己软件的依赖树, 也无从知道自己的软件到底依赖了哪些包.

2016 年, 很多 Node.js 用户发现自己的软件构建时莫名失败, 最后发现是因为一个叫做 left-pad 的包的作者把它删除了, 而很多流行的项目都依赖于它, 比如 Bable, Ember, React…

这个包只有 8 行代码, 而大部分人都从来没听说过它.

left-pad 绝非个例, 比如 NPM 上面发布的大约 750,000 个包中的 30% 都直接或间接的依赖于 escape-string-regexp 包.

对依赖进行测试

下一步, 你应该针对自己的软件所需要的功能, 写一些新的测试去测试它. 这些测试一般用于检查你对它的使用走否理解正确. 一开始这些测试可能是一些短小独立的代码, 但是很值得把它们做成自动化测试. 这样每次升级包之后, 可以用这些测试检查升级是不是破坏了预期行为.

把依赖抽象出来

很有可能有一天, 你发现了更合适的包, 你决定不再使用现有的某些包了.

如何能更容易的迁移到其他包呢?

你可以在决定使用一个包时, 就把它的功能抽象出来: 自己定义一套 API, 使用这个包的功能实现你的 API, 然后让其他代码都跟你自己的 API 打交道. 这样一来, 如果你想换成其它的包, 只要用新包实现一遍 API 就行, 而其他地方甚至都不用知道已经在使用新包了.

让依赖在运行时独立出来

为了降低依赖发生问题带来的风险, 可以让它们在运行时独立出来.

比如 Chrome 提供的沙盒机制, 就让用户写的扩展运行在一个独立的系统进程内.

GVisor 可能对于这点有所帮助.

干脆不用它

如果这个依赖看起来风险太大, 并且也没办法把它独立出去, 可能还是避免使用它才是最好的选择.

Go 社区也有句谚语 “复制一点点好于依赖一点点” (https://go-proverbs.github.io)…

升级依赖

有新版本了, 该不该升级依赖呢? 有句话是 “如果它没坏, 就别去碰它”. 但是这句话忘了两点:

  • 有些升级最终还是要做的, 而一个大的升级往往比多次小升级更糟心
  • 你觉得它没坏, 而知道它的 Bug 的人可能随时都能利用 Bug 来攻击你的软件

2017 年

3 月 7 号, Apache Struts 发现一个新漏洞, 并发布了一版补丁修复它

3 月 8 号, Equifax 收到 US-CERT 通知, 需要对所有使用 Struts 的项目进行更新

3 月 9-15 号, Equifax 进行了源码和网络扫描, 没发现需要进行升级的对外服务器

3 月 13 号, 骇客却发现了可以用来攻击的服务器, 并盗窃了 1 亿四千万的用户个人和财务信息

7 月 29 号, Equifax 终于发觉了这次数据泄露, 并与 8 月 4 号公开这次泄露

9 月底: Equifax 的 CEO, CIO, CSO 辞职

升级依赖很重要, 同时也意味着升级基于前文的对依赖的评估, 重新运行测试. 升级也不应被完全自动化, 你需要确定新版本适合你的软件环境后才能决定是否部署它.

监控依赖

即使已经做了这么多工作, 可是还没完…

你需要监控依赖

  • 确保它始终是预想中的版本. 很多依赖管理工具你帮你做这件事儿
  • 确保没有间接性的依赖悄无声息的产生
  • 周期性的检查哪些依赖一直没变过也很重要 (被遗弃了? 是不是该考虑其他包替代它了)
  • 当然, 还有定期检查每个依赖的安全记录

结论

我们应该开始重视依赖带来的问题了. 类比专门的测试工程师, 或许是时候有专门的人来进行依赖管理这项工作. 未来的依赖管理技术, 应该把更多焦点放到减少依赖评估和维护的开销上.

原文地址: https://research.swtch.com/deps