You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

170 lines
14 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 75 | 软件版本迭代的规划
你好,我是七牛云许式伟。
到今天为止,我们专栏的话题主要集中在软件工程的质量与效率上。我们在专栏的开篇中就已经明确:
> 从根本目标来说,软件架构师要对软件工程的执行结果负责,这包括:按时按质进行软件的迭代和发布、敏捷地响应需求变更、防范软件质量风险(避免发生软件质量事故)、降低迭代维护成本。
但是今天,我们将探讨一个更高维的话题:软件版本迭代的规划。后续我们简称为 “版本规划”。简单说,就是下一步的重点应该放在哪里,到底哪些东西应该先做,哪些东西应该放到后面做。
这是一个极其关键的话题。它可以影响到一个业务的成败,一个企业的生死存亡。方向正确,并不代表能够走到最后,执行路径和方向同等重要。
那么,版本规划的套路是什么?
探讨这个问题前我想先看一个实际的案例。这个案例大家很熟悉Go 语言的版本迭代。
我们从 Go 语言的演进,一起来看看 Go 团队是如何做软件版本迭代规划的。这有点长,但是细致地琢磨对我们理解版本规划背后的逻辑是极其有益的。
## Go 版本的演进历史
Go 语言的版本迭代有明确的周期,大体是每半年发布一个版本。
Go 1.0 发布于 2012 年 3 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1](https://tip.golang.org/doc/go1)。它是 Go 语言发展的一个里程碑。
在这个版本Go 官方发布了兼容性文档:[https://tip.golang.org/doc/go1compat](https://tip.golang.org/doc/go1compat),承诺会保证未来的 Go 版本将保持向后兼容。也就是说,将始终兼容已有的代码,保证已有代码在 Go 新版本下编译和运行的正确性。
在 Go 1.0 之前Go 在持续迭代它的使用范式,语法规范也在迭代优化。比如 os.Error 到了 Go 1.0 就变成了内置的 error 类型。这个改变看似很小,但实际上是一个至关重要的改变。因为 Go 推荐可能出错的函数返回值都带上 err 值,如果 os.Error 不改为内建类型,就会导致很多模块不得不因为 os.Error 类型而依赖 os 包。
Go 1.0 最被诟病的问题是它的 GC 效率。相比 Java 近 20 年的长期优化,其成熟度只能以稚嫩来形容。
与此相对应的是Go 从一开始就是一门极度重视工程的语言。Go 1.0 就已经有非常完善的工程工具支持。比如:
* 单元测试go test
* 文档go doc
* 静态检查工具go vet
* 性能 Profile 工具: go tool pprof。
Go 1.1 发布于 2013 年 5 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.1](https://tip.golang.org/doc/go1.1)。这个版本主要专注于语言内在机制的改善和性能提升编译器、垃圾回收、map、goroutine调度。改善后的效果如下
![](https://static001.geekbang.org/resource/image/8f/99/8fbfe652213163c2ae447ecdd7825e99.png)
这个版本还发布了一个竞态探测器(race detector),它对 Go 这种以高并发著称的语言显然是重要的。详细可参考 Go 官方博客文章:[https://blog.golang.org/race-detector](https://blog.golang.org/race-detector)。
Go 1.2 发布于 2013 年 12 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.2](https://tip.golang.org/doc/go1.2)。这个版本发布了单元测试覆盖率检查工具go tool cover。详细可参考 Go 官方博客文章:[https://blog.golang.org/cover](https://blog.golang.org/cover)。
Go 1.3 发布于 2014 年 6 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.3](https://tip.golang.org/doc/go1.3)。这个版本栈的内存分配引入了连续段contiguous segment的分配模式以提升执行效率。之前的分页式的栈分配方式segment stack存在频繁地分配/释放栈段导致栈内存分配耗时不稳定且效率较低。引入新机制后,分配稳定性和性能都有较大改善。
Go 1.3 还引入了 sync.Pool即内存池组件以减少内存分配的次数。标准库中的 encoding/json、net/http 等都受益于它带来的内存分配效率提升。另外Go 还对 channel 进行了性能优化:
![](https://static001.geekbang.org/resource/image/4c/e3/4c78ebd5be2a3c24e011afa6026af2e3.png)
Go 1.4 发布于 2014 年 12 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.4](https://tip.golang.org/doc/go1.4)。从功能来说,这个版本最大的一件事情是增加了 Android/iOS 支持([http://golang.org/x/mobile](http://golang.org/x/mobile)Gopher 可以使用 Go 编写简单的 Android/iOS 应用。
但实际上如果从重要程度来说Go 1.4 最重要的变化是将之前版本中大量用 C 语言和汇编语言实现的 runtime 改为用 Go 实现,这让垃圾回收器执行更精确,它让堆内存的分配减少了 10~30%。
另外Go 1.4 引入了 go generate 工具。这是在没有泛型之前解决重复性代码问题的方案。详细见[https://blog.golang.org/generate](https://blog.golang.org/generate)。
Go 1.5 发布于 2015 年 8 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.5](https://tip.golang.org/doc/go1.5)。这个版本让 Go 实现了自举。这让GC 效率优化成为可能。所以在这个版本中GC 被全面重构。由于引入并发垃圾回收,回收阶段带来的延迟降低了一个数量级。
这个版本还有一个很重要的尝试,是引入了 vendor 机制以试图解决 Go 模块的版本管理问题。自从 Go 解决了 GC 效率后Go 版本管理就成了老大难问题。下图是 Go 社区对 Go 面临的最大挑战的看法:
![](https://static001.geekbang.org/resource/image/4b/cf/4b6aafbca1ce62c50ca9a822359065cf.png)
当然后来事实证明 vendor 机制并不成功。
另外Go 1.5 引入了 go tool trace通过该命令我们可以实现执行器的跟踪(trace)。详细参考 [https://golang.org/cmd/trace/](https://golang.org/cmd/trace/)。
Go 1.6 发布于 2016 年 2 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.6](https://tip.golang.org/doc/go1.6)。垃圾回收器的延迟在这个版本中进一步降低。如下:
![](https://static001.geekbang.org/resource/image/c3/6c/c3ca946093cbef50d6a5f42349ac216c.png)
从功能上来说,这个版本支持了 HTTP/2。
Go 1.7 发布于 2016 年 8 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.7](https://tip.golang.org/doc/go1.7)。这个版本有一个很重要的变化,是 context 包被加入标准库。这事之所以重要,是因为它和 os.Error 变成内建的 error 类型类似在网络接口中context 是传递上下文、超时控制及取消请求的一个标准设施。
另外Go 编译器的性能得到了较大幅度的优化编译速度更快二进制文件size更小有些时候幅度可达 20~30%。
Go 1.8 发布于 2017 年 2 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.8](https://tip.golang.org/doc/go1.8)。GC 延迟在这个版本中进一步得到改善,延迟时间降到毫秒级别以下。
另外这个版本还大幅提升了defer的性能。如下
![](https://static001.geekbang.org/resource/image/eb/03/ebd7dc28203a1de76ee968184b83d203.png)
Go 1.9 发布于 2017 年 8 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.9](https://tip.golang.org/doc/go1.9)。这个版本引入了 type alias 语法。例如:
```
type byte = uint8
```
这实际上是一个迟到的语法。我在 Go 1.0 就认为它应该被加入了。另外sync 包增加了 Map 类型,以支持并发访问(原生 map 类型不支持)。
Go 1.10 发布于 2018 年 2 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.10](https://tip.golang.org/doc/go1.10)。在这个版本中go test 引入了一个新的缓存机制,所有通过测试的结果都将被缓存下来。当 test 没有变化时,重复执行 test 会节省大量时间。类似地go build 也维护了一个已构建的包的缓存以加速构建效率。
Go 1.11 发布于 2018 年 8 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.11](https://tip.golang.org/doc/go1.11)。这个版本最重要的新功能是 Go modules。前面我们说 Go 1.5 版本引入 vendor 机制以解决模块的版本管理问题,但是不太成功。这是 Go 团队决定推翻重来引入 module 机制的原因。
另外,这个版本引入了一个重要的试验功能:支持 WebAssembly。它允许开发人员将 Go 源码编译成一个兼容当前主流浏览器的 wasm 文件。这让 Go 作为 Web 开发语言成为可能。
Go 1.12 发布于 2019 年 2 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.12](https://tip.golang.org/doc/go1.12)。这个版本的 go vet 命令基于 analysis 包进行了重写,使得 go vet 更为灵活并支持 Gopher 编写自己的 checker。详细参考 “[How to Build Your Own Analyzer](https://medium.com/@blanchon.vincent/go-how-to-build-your-own-analyzer-f6d83315586f)” 一文。
Go 1.13 发布于 2019 年 8 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.13](https://tip.golang.org/doc/go1.13)。这个版本的 sync.Pool 性能得到进一步的改善。当 GC 时Pool 中对象不会被完全清理掉。它引入了一个 cache用于在两次 GC 之前清理 Pool 中未使用的对象实例。
另外这个版本的逃逸分析escape analysis被重新实现了这让 Go 更少地在堆上分配内存。下图是新旧逃逸分析的基准测试对比:
![](https://static001.geekbang.org/resource/image/5e/92/5e415ba622b53a7f9d63060a02125092.png)
另外Go modules 引入的 GOPROXY 变量的默认值被改为:
```
GOPROXY=https://proxy.golang.org,direct
```
但在国内无法访问 Go 官方提供的 proxy.golang.org 站点。建议改为:
```
export GOPROXY=https://goproxy.cn,direct
```
这里 [https://goproxy.cn](https://goproxy.cn/) 由七牛云赞助支持。
## Go 版本迭代的背后
Go 语言的版本迭代的规划非常值得认真推敲与学习。
Go 的版本迭代还是比较高频的,但是有趣的是,在 Go 1.0 版本之后,语言本身的功能基本上已经非常稳定,只有极少量的变动。比如 type alias 这样的小特性,都已经可以算是关键语法变化了。
那么,这些年 Go 语言都在变化些什么?
其一,性能、性能、性能!尤其在 GC 效率这块,持续不断地优化。为了它,大范围重构 Go 的实现完成了自举。其他还有很多比如连续栈、内存池sync.Pool、更快的编译速度、更小的可执行文件尺寸。
其二,强化工程能力。各种 Go tool 的增加就不说了,这其中最为突出的就是 Go 模块的版本管理,先后尝试了 vendor 和 module 机制。
其三,标准库的能力增强,如 contextHTTP 2.0 等等。这块大部分比较常规,但 context 的引入可以算是对网络编程最佳实践的一次标准化过程。
其四,业务领域的扩展。这块 Go 整体还是比较专注于服务端领域,只是对 Android、iOS、WebAssembly 三个桌面平台做了经验性的支持。
## 如何做版本规划
蛮多技术背景的同学在做版本规划的时候,往往容易一开始就陷入到技术细节的泥潭。但其实对于一个从 0 到 1 的业务来说,首先应该把焦点放到什么地方,这个选择才至关重要。
Go 语言在这一点上给出了非常好的示范。它首先把焦点放在了用户使用姿势的迭代上。凡与此无关的事情,只要达到及格线了就可以先放一放。这也是 Go 为什么一上来虽然有很多关于 GC 效率的吐槽,但是他们安之若素,仍然专注于用户使用姿势的迭代。
但是一旦语言开始大规模推广,进入从 1 到 100 的扩张阶段,版本迭代的关注点反而切换到了用户看不见的地方:非功能性需求。生产环境中用户最关心的指标,就成了 Go 团队最为关注的事情,日复一日,不断进行迭代优化。
这是很了不起的战略定力:知道什么情况下,最该做的事情是什么。
那么,遇到重大的客户需求,对之前我们培养的用户习惯将形成重大挑战怎么办?一些人可能会习惯选择快速去支持这类重大需求,因为这些需求通常很可能听起来很让人振奋。
其实 Go 语言也遇到了这样的需求:泛型的支持。
泛型被 Go 团队非常认真地对待。可以预期的是Go 2.0 一定会支持泛型。但是他们并没有急着去实现它。Go 社区不少人在 Go 1.9 的时候,很激动地期待着 Go 2.0,期待着泛型,但是 Go 出来了 Go 1.10,甚至到现在的 Go 1.13。
显然,泛型被放到了一个旁路的版本。这个旁路版本独立演化直到最终验证已经成熟,才会被合并到 Go 1.x 中。这时Go 2.0 就会诞生了。
这其实才是正确响应会招致巨大影响面的功能需求的姿势。
客户是需要尊重的。而尊重客户的正确姿势毫无疑问是:别折腾他们。
## 结语
今天我们聊的话题是版本迭代的规划。在不同阶段,版本迭代的侧重点会有极大的不同。从 0 到 1 阶段,我们验证的是用户使用姿势,性能并不是第一位的。但是进入扩张阶段,产品竞争力就是关键指标,这时候我们迭代的是用户价值最大的,也是用户真正最在乎的那部分。
遇到会对产品产生巨大冲击的需求,头脑别发热,谨慎处理。回到从 0 到 1 阶段的方法论,在少量客户上先做灰度。
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “软件工程的未来”。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。