访谈文章|Interview
七年前选择用Go和Rust做数据库的创业公司,如今怎么评价这个决定?
“我现在会很辩证地看待这件事情,只能说是不好不坏,但当时所谓的主流选择可能会让我们的产品变成一个平庸的系统。”
即便是在此时此刻创业的公司,公司的产品决定全部采用Go和Rust也是非常艰难的决定,更何况是七年前。
2015到2016年,Go不到五岁,Rust还没发布v1.0版本,没有太多公司和开发者看好这两种语言,怎么会有公司选择全面采用这两种语言,还是用来写数据库和存储层代码?
如果你在七八年前听到这个故事,直觉大概是这家公司活不了太久。事实上,这家公司不仅走过了七年,还拿到了三轮融资,这家公司就是PingCAP。
当然,编程语言只是工具,绝不是PingCAP可以走到今天最重要的因素,但这家公司确实因为这一选择收获了不少。本文通过与PingCAP创始人之一黄东旭和两位工程师的交谈,还原了这家创业公司最初选择Go和Rust的原因,以及如何解决随之而来语言本身的问题、人才问题以及对不同语言适用场景的思考。
选择背后的原因
“首先,我不是某一个具体的编程语言或者工具的信仰者,但在做项目时选择一个好的工具也是十分必要的。”——黄东旭
选择的第一个要点:开发效率
Go语言的缔造者中有一位全世界程序员公认的大神级人物——肯尼思·汤普森(Kenneth Thompson),他是UNIX操作系统的主要开发者;其另一位主要设计者和早期实现者罗布·派克虽然没有直接参与最初版本的UNIX的开发,但同样属于贝尔实验室UNIX开发组的最资深成员,并且是字符编码UTF-8的主要实现者。
所以,Go的出身决定了该语言具备极高的品质。而且,Go语言从第一个版本起就开源,所以来自世界各地的程序员第一时间发现并使用了它,然后立刻就被其美妙的语言特性所吸引,比如Go语言使用比线程轻量得多的goroutine完成上下文切换可以节省高达80%左右的时间,这些关注者中就包括黄东旭与另一位合伙人刘奇。
早在创办PingCAP之前,黄东旭与刘奇就曾使用Go语言写过一个叫codis的开源软件解决当时豌豆荚业务在缓存扩展方面的问题。深入了解Go语言之后,二人被Go带来的效率提升所吸引。“同样的系统使用C++开发可能需要一个月,但使用Go可能仅需要三天,这种级别的开发效率提升很难不让人动心。”
过去,因为效率而选择一门新兴语言的人并不是没有,比如《黑客与画家》的作者保罗·格雷厄姆Paul Graham曾用Lisp写了最早的Web应用Viaweb,最终被雅虎以5千万余美金收购。这个故事也对PingCAP创始团队带来了一些影响。
PingCAP的目标是做一个分布式数据库,也就是现在我们熟知的TiDB。团队最开始确实考虑过C++,毕竟大部分成员都有着C++背景,大多还不错的数据库都是用C++开发的,但是C++非常依赖团队内部研发人员的经验、水平以及团队内部相关规范的制定,否则很容易出现问题。对于刚刚创业的PingCAP而言,团队显然是很难找到厉害的C++研发人才。
“这里还有一个有趣的现象,业内鲜少有程序员承认自己精通C++,即便是拥有20年经验的开发者,但可能学一个月就有人说自己精通Go了,复杂性一目了然。”
当时还存在一个安全的选择,那就是Java。那段时间,主流的分布式系统大部分使用Java编写的,比如Hadoop、Zookeeper、Cassandra、HBase等。但面向云计算时代,团队认为需要更敏捷、更高效、同时更安全的新方式构建系统。
在当时,同样有很多软件产品选择工具时信奉其可以最大程度榨干硬件性能。“老实讲,我并不信奉开发出来的产品一定要能榨取硬件的最后一点性能,招聘具备这种能力程序员的成本可能并不比加几台机器低(分布式时代,这个问题显然可以通过加机器的方式来解决),而且用户也不会因为榨取的这一点性能而付更多钱”,黄东旭在采访中如是说道。
综合权衡下来,Go成为了当时创业最合适的起步选择,可以快速把TiDB的原型开发出来。当年的9月份,TiDB就在GitHub上开源了,后续的迭代也很快,随后一年就有客户在生产环境试用,再往后一年TiDB 1.0 GA版本正式发布。
选择的第二个要点:语言本身的特性
既然Go的效率得到了验证,团队为什么在后来开发TiDB的存储引擎TiKV时又选择了Rust呢?
TiKV起始于2015年底,当时团队在Pure Go/Go+Cgo/C++11/Rust几个语言之间纠结,虽然PingCAP的核心团队有大量的Go语言开发经验,另外TiDB的SQL层已经完全采用Go语言开发,Go带来的开发效率的极大提升也让团队受益良多。但是在存储层的选型上,团队首先排除的就是Pure Go的选项,理由很简单,底层已经决定接入RocksDB,RocksDB本身就是个C++的项目,而Go的LSM-Tree的实现大多成熟度不太够没有能和RocksDB相提并论的项目,如果选Go的话,只能选择用Cgo来bridge,但是当时Cgo的问题同样明显,在2015年底,在Go code里调用Cgo的性能损失比较大,并不是在goroutine所在的线程直接Call cgo的代码,而且对于数据库来说,调用底层的存储引擎库是很频繁的,如果每次调用RocksDB的函数都需要这些额外开销,非常不划算,当然也可以通过一些技巧增大Cgo这边的调用的吞吐,比如一段时间内的调用打包成一个cgo batch call,通过增加单个请求的延迟来增大的整体的吞吐,抹平cgo调用本身的开销,但是这样一来,实现就会变得非常复杂。另一方面,GC问题仍然没有办法彻底解决,存储层希望尽可能高效的利用内存,大量使用syscall.Mmap或者对象复用这些有些hacky的技巧,会让整体的代码可读性降低。
其实C++11也没什么问题,性能上肯定没问题,RocksDB是C++11写的,在纠结了一小段时间后,团队认真评估了成员背景和要做的东西,最后还是没有选择C++,原因主要是:
核心团队过去都是C++的重度开发者,基本都有维护过大型C++项目的经历,每个人都有点心里阴影……悬挂指针、内存泄漏、Data race在项目越来越大的过程中几乎很难避免,当然你可以说靠老司机带路,严格Code Review和编码规范可以将问题发生的概率降低,但是一旦出现问题,Debug的成本很高,心智负担很重,而且第三方库不满足规范怎么办。
C++的编程范式太多,而且差异很大,又有很多奇技淫巧,统一风格同样也需要额外的学习成本,特别是团队的成员在不断的增加,不一定所有人都是C++老司机,特别是大家这么多年了都已经习惯了GC的帮助,已经很难回到手动管理内存的时代。
缺乏包管理,集成构建等现代化的周边工具,虽然这点看上去没那么重要,但是对于一个大型项目这些自动化工具是极其重要的,直接关系到大家的开发效率和项目的迭代的速度。而且C++的第三方库参差不齐,很多轮子得自己造。
Rust在2015年底已经发布了1.0,Rust有几点特性非常吸引团队:
• 内存安全性
• 高性能(得益于llvm的优秀能力,运行时实际上和C++几乎没区别),与C/C++的包的亲缘性
• 强大的包管理和构建工具Cargo
• 更现代的语法
• 和C++几乎一致的调试调优体验,之前熟悉的工具比如perf之类的都可以直接复用
• FFI,可以无损失的链接和调用RocksDB的C API
一方面,团队把安全性放在第一位,C++的内存管理和避免Data race的问题虽然靠老司机可以解决,但是仍然没有在编译器层面上强约束,把问题扼杀在摇篮之中解决的彻底,Rust很好地解决了这个问题。另一方面,Rust是一个非常现代化的编程语言,现代的类型系统,模式匹配,功能强大的宏,trait等熟悉以后会极大提升开发效率。
最终,Rust也没让团队失望,四五个人的团队从零开始花费了四个月左右的时间就开发出了TiKV的第一个版本。2016年1月1日开始开发,4月1日开源。同年10月份,TiKV第一次被使用在生产环境,那时TiDB甚至都还没有发布beta版。TiKV的开发非常快,发布的版本都很稳定,生产效率比C++高出许多。
选择的第三个要点:人才
虽然当年并没有太多开发者使用Rust,但早期探索者们的自身能力是很强的,这群人对编程本身有着极强的热爱,这对于创业公司而言是非常宝贵的人才(起步阶段的创业公司人才在精不在多),PingCAP后续的发展也验证了这一点,曾经是Rust核心团队成员的Brian Anderson在2018年选择加入该公司并参与TiKV的研发,这种案例在2018年之前是极少数的。Brian之所以愿意加入,除个人因素之外,与PingCAP在开源和社区方面的持续努力是分不开的。2017年,Brian就应邀出席过PingCAP举办的国内首场Rust Meetup。2019年, Rust Core Team的元老nrc也加入了PingCAP。
“直到今天,我依旧认为这是创业公司吸纳人才时很好的思路,否则一家创业公司通过什么去跟互联网大厂竞争,只能是更好的理念和工具。我们在没有做任何全球化品牌的时候就是靠着这张名片(指全面拥抱Rust生态)吸引人才加入我们的社区和公司。但很多时候,国内很多公司的问题是不敢想,认为自己凭什么可以吸引到这样的大牛加入,但凡事都要先试试。”
选择Rust带来的问题
如开篇所言,黄东旭如今认为当初的决定“不好不坏”,虽然获得了开发效率上的提升,也承受了当时语言不够成熟带来的问题。
“很多时候,人们会先通过广告了解一件事情。我们当初对Rust的看法也是内存安全、性能好,没有GC效率肯定高等,实际并不是这样的。如果你代码写的很挫,凭什么认为自己手动分配内存就比GC搞得好。”
起初,团队基本是把Rust当成Java、C++在用,性能并没有明显提升,直到更专业的人才加入才把整个代码扭转到更好的道路上。
此外,Rust的编译时间较长。“当时我们内部的Rust程序员经常开玩笑说一天只有24次编译机会,用一次少一次”,团队做了大量工作去降低Rust的编译时间,参与并贡献了rust-gRPC、Raft库,open-tracing等项目中,并产出了大量相关的博客文章。
虽然解决这些问题占用了团队的很多时间,但团队也因此获益。“我们将Talent Plan的所有教程用Rust实现了一遍,虽然这离我们的主页主业有点远,但对后来的招聘和培训极其重要,这也是我们第一次在全球范围内好评如潮。”
怎么判断要不要选择或者切换Rust、GO?
不少企业创业之初会选择基于某些开源产品来实现商业版本,这种情况下编程语言其实已经是确定的了,不需要太过纠结。如果从零开始开发某项产品,可以从以下几个方面进行考虑:
1. 如果公司内部在C/C++、Java上开发规范已经做得很好了,可以先不考虑切换至Rust。Rust相当于自带严格的安全性限制,让程序员在大部分情况下没办法写出存在安全隐患的代码,语言本身的设计帮助规避了一些常规问题。
2. 如果是基础软件类型的企业,相关从业人员的水平还是值得信任的,一般不会犯太多低级错误。此时,Rust的收益主要体现在数据库最核心的组件或者功能编写上,对剩下90%的部分而言,效率可能比安全更重要。
3. 非Rust不可的场景有写驱动,比如操作系统内核等,效率绝对高;SSL加密或者产品内部的某个关键链路,比如浏览器里面的渲染引擎,这类CPU密集型又对安全要求较高的场景。非必要场景最好选用社区比较大的编程语言,比如Java,相对来说也很好招人。
4. 考虑语言的向后兼容性。Go语言在这一点上至今都做得非常好,并且也响应社区用户的意见添加了范型。
5. 社区的风格。Rust的社区是非常开放的,这给该语言带来了很多好处,但也可能带来一些副作用,比如可能会有些分裂,这可能是Rust社区未来要考虑的事情,但社区内的氛围相对活跃,可以在其中寻找问题的答案或者志同道合的朋友。
6. “Rust近几年最新加入的复杂度主要是Pin异步编程,但二者的加入确实解决了一些问题,无论是哪种语言都会面临各种选择,这是不同目标平衡取舍的结果。”
7. 代码实现逻辑。Rust语言在设计上与Go、Java等都不同,其规避了一些问题。使用Rust写出来的程序可以专注优化程序逻辑本身,比如让程序更加适应操作系统、减少线程切换等。
Rust如今已经得到了越来越多企业和开发者的验证,但依旧有新的小众语言出现,如同当年新生的Rust,企业应该如何判断呢?
新型语言Zig也来了,怎么看?
Zig就是一门新的、小众的语言,目前还没有发布1.0版本,其将竞争对手定为了C语言,注意不是C++,而就是最基础的C,但也吸收了Rust等语言的一些特性。
去年中旬因为Uber的使用,Zig引起了一些开发者的关注。Uber使用Zig来编译其C/C++代码。现在,Uber只在Go Monorepo(据其内部工程师介绍,Go Monorepo比Linux内核还要大,有几千名工程师在开发和维护)中使用bazel-zig-cc,但计划尽可能地将zigcc推广到其他需要C/C++工具链的语言。
Uber的工程师表示,与其他工具链相比,zig-cc提供的C/C++工具链的主要优势是glibc版本可配制与macOS交叉编译。
如今的Zig与七八年前的Rust情况相似又不同,当年的Rust背后有一群Molliza的工程师支持,其理念、质量和解决实际问题的能力是值得信任的,现在的Zig虽然有基金会支持,但实际能力还有待商榷。
受访的PingCAP工程师们对该语言的发展持观望态度,并建议企业在选择这类新兴小众的语言时最好是可以找到那个“非它不可”的理由,如果找不到,就证明这件事情没必要冒险采用新语言,如果有想法,最好可以亲自与创始团队面对面交流,从中得到一些判断。
结语
自从ChatGPT诞生,我们无数次对AI的生产力感到惊讶,其可以根据简单的输入生成代码并将这些代码轻松转换成其他编程语言。未来,开发者的工作模式可能会发生翻天覆地的变化,最重要的是我们构建这一切的思路和想法,而不是工具本身。从这个角度出来,关于编程语言优劣的争论就会变得不那么重要。
当然,如果你是某一个语言/工具的信仰者,着手将其带向全新高度也会比争论更有意义。
参考资料
《与开源同行-揭秘PingCAP七年创业实践》
相关阅读
Rust in TiKV (https://www.zenlife.tk/project)