阅读视图

发现新文章,点击刷新页面。

从参与 Rust 标准库开发看开源贡献的源动力

首先介绍一下我在 Rust 标准库当中做的两个微小的工作。

第一个是从去年 8 月 14 日发起,今年 4 月 6 日合并,历时约 8 个月,目前仍在等待 stabilize 的为 OnceCellOnceLock 增加新接口的提案:

第一次提交贡献

第一次贡献成功合并后,马上第二个工作是从今年 4 月 8 号开始,7 月 6 号合并,历时约 3 个月,同样还在等待 stabilize 的为 PathBufPath 增加拼接扩展名新接口的提案:

第二次提交贡献

可以看到,这两次贡献的内容可算是同种类型的,第二次提交从发起到合并的时间比第一次缩短了一半以上。本文先介绍 Rust 标准库提案的基本工作流程,然后介绍这两个贡献背后的故事,最终讨论开源贡献的源动力从何而来。

Rust 标准库提案的工作流程

标准库的贡献有很多种,上面我所做的两个贡献都是扩充现有数据结构的接口,也就是 API Change 类型的提案。

Rust 社群为 API Change Proposal (ACP) 设计了专门的流程,流程文档记录在 std-dev-guide 手册中。

简单来说,一共分为以下三个步骤:

  1. rust-lang/libs-team 仓库中创建一个新的 ACP 类型的 Issue 开始讨论;
  2. 讨论形成初步共识以后,修改 rust-lang/rust 仓库中标准库的代码并提交 PR 开始评审;
  3. 针对实现达成共识以后,在 rust-lang/rust 仓库中创建对应的 Tracking Issue 并由此获得标注 unstable feature 需要的参数(Issue 编码);PR 合并后,由 Tracking Issue 记录后续的 stabilize 流程

不过,其实我所做的两个简单改动并没有严格遵循这个流程。因为我首先不知道这个流程,加上实际改动的代码非常少,我是直接一个 PR 打上去,然后问一句:“老板们,这里流程是啥?我这有个代码补丁还不错,你看咋进去合适。”

开源贡献的源动力从何而来

为什么要给 OnceCell 和 OnceLock 加接口?

真要说起来,给 OnceCell 和 OnceLock 加接口的起点,还真不是直接一个 PR 怼脸。上面 PR 截图里也能看到,首先是我在原先加入 once_cell 功能的 Tracking Issue 提出为何没有加这两个接口的问题:


进一步地,之所以会提出这个问题,是我当时在实现 Apache Kafka API 当中 RecordBatch 接口,其中涉及到一个解码后缓存结果的优化。因为 Rust 对所有权和可变性的各种检查和契约,如果想用 OnceCell 来实现仅解码(初始化)一次的效果,又想要在特定上下文里取得一个可变引用,那就需要一个 get_mut_or_init 语义的接口。

实际上,这个接口的实现跟现有的 get_or_init 逻辑几乎一致,除了在接收者参数和结果都会加上 &mut 修饰符以外,没有区别。所以我倾向于认为是一开始加的时候没有具体的需求,就没立刻加上,而不是设计上有什么特殊的考量。

于是,开始讨论后的下一周,我就提交了实现功能的代码补丁。很快有一位志愿者告诉我应该到 rust-lang/libs-team 仓库走 ACP 的流程,我读了一下文档,尝试跟提出这个要求的志愿者 confirm 一下具体的动作。不过马上就是两个多月的毫无响应,我也就把这件事情暂时搁置了。

经典开始等待……

11 月,两位 Rust 语言的团队成员过问这个补丁。尤其是我看到 Rust 社群著名的大魔法师 @dtolnay 表示这个补丁确实有用,应该合进去以后,我感觉信心倍增,当周就把 ACP 的流程走起来了。

随后就是又两个月的杳无音讯……

今年 1 月,@dtolnay 的指令流水线恰巧把这个 PR 调度上来,给我留了一个 Request Changes 让我修一下编译。发现我凭直觉写出来的合理代码遇到了另一个 Rust 编译器的 BUG 导致编译不过:

解决以后,社群志愿者 @tgross35 指导我给新的功能添加 unstable feature 的属性注解,也就是:

1
#[unstable(feature = "once_cell_get_mut", issue = "121641")]

另一位社群志愿者 @programmerjake 帮我指出几个文档注释的问题,为标准库添加新接口不得不写好接口文档,摆烂不了。

在此过程中,我给 OnceLock 也同样加了 get_mut_or_initget_mut_or_try_init 接口,因为它和 OnceCell 本来就是同期进来的,接口设计也一模一样。我想那就一起加上这个接口,免得 OnceLock 后面有同样的需要还要另走一次 ACP 流程。

这悄然间又是两个月过去了……

终于,在 @programmerjake Review 完之后一周,@dtolnay 出现并 Appove 后合并了代码。

第一次 Rust 主仓库参与贡献至此告一段落,唯一剩下的就是等 libs-team 主观认为合适之后启动功能 stabilize 的流程。

上个月,有人发现之前写的文档注释还是有错误,于是提交 PR 修复。这就是某种开源协同让代码质量向完美收敛的过程。

为什么会想到给 Path 加接口?

这个动机要追溯到我用 Rust 重写 License Header 检测工具 HawkEye 的时候了。

Rust 重写 HawkEye 的动机,则是为了支持调用 Git 库跟 gitignore 的机制做集成。虽然原先 Java 的实现可以有 JGit 来做,但是 JGit 的接口很脏,而且加上 JGit 以后就不能 Native Image 了导致分发产物体积显著变大。虽然都不是什么大事,但是工具类软件本来就是强调细节处的开发者体验,正好我逐渐掌握了 Rust 编码的技巧,就拿上来练练手。

今年 3 月重写 HawkEye 的时候,我发现了 Rust 标准库里 Path 结构缺一个接口:

1
2
3
4
let mut extension = doc.filepath.extension().unwrap_or_default().to_os_string();
extension.push(".formatted");
let copied = doc.filepath.with_extension(extension);
doc.save(Some(&copied))

我在写上面这段代码的时候,目的是给文件名加一个 .formatted 后缀。但是,Path 的 with_extension 接口是直接替换掉扩展名,也就是说,原本我想要的是 file.rs 变成 file.rs.formatted 的效果,如果用 with_extension 接口,就会变成 file.formatted 这样不符合预期的结果。

最终,只能用上面这样的写法手动绕过一下。显然代码变得啰嗦,而且这段代码是依赖 extension 的底层实现细节,某种程度上说是 brittle 的。

遇到这个问题的时候,我就想给标准库直接提一个新的 PR 了。不过当时 OnceCell 和 OnceLock 的 PR 还没合并,我拿不准 Rust 社群的调性,也不想开两个 PR 都长期 pending 下去。

好在上一个 PR 在几周后顺利合并了,我于是同时创建了 ACP 和 PR 以供社群其他成员评审。因为改动面很小,比起口述如何实现,不如直接在 ACP 里链接到 PR 讲起来清楚。

这次,提案很快得到 Rust 团队成员,也是我的前同事 @kennytm 的回应。不到两周,我就按照上次的动作把 PR 推到了一个可以合并的状态。

然后又是两个月的杳无音讯……

最后,我根据其他社群成员的指导,跑到 Rust 社群的 Zulip 平台上,在 libs-team 的频道里几次三番的问:“这个 ACP 啥时候上日程啊?”

终于,在今年 7 月初,libs-team 在会议上 appove 了 ACP 的提议,并在一周内再次由 @dtolnay 完成合并。

开发者的需求是开源贡献的源动力

可以看到,上面两个贡献的源动力,其实都是我在开发自己的软件的时候,遇到的 Rust 标准库缺失的接口,为了填补易用接口的缺失,我完成了代码开发,并了解社群流程以最终合并和发布。在最新的 Rust Nightly 版本上,你应该已经能够使用上这两个我开发的接口了。

在开源共同体当中,一个开发者应该能够发现所有的代码都是可以更改的。这是软件自由和开源定义都保证的事情,即开发者应该能够自由的更改代码,并且自由地使用自己修改后的版本。

那么,开发者在什么时候会需要修改代码呢?其实就是上面这样,自己的需求被 block 住的时候。小到 Rust 标准库里接口的缺失,大到我之前在 Apache Flink 社群里参与的三家公司合作实现 Application Mode 部署,甚至完全重新启动一个开源项目,说到底核心的源动力还是开发者真的对这样的软件有需要。

因此,开发者的需求是开源贡献的源动力。一旦一个开源软件不再产生新需求,那么它就会自然进入仅维护状态;而一旦一个开源软件不再能够满足开发者实际的需求,甚至开发者连直接修改代码的意愿也没有,那么它的生命也就到此为止了。

Logforth: 从需求中来的 Rust 日志功能实现

由此,我想介绍一个最近在开发 Rust 应用的时候,由自己需求出发所开发的基础软件库:

Logforth: A versatile and extensible logging implementation

Rust 生态的日志组件,由定义 Logging 接口的 log 库和实现 Logging 接口的各个实现库组成。其中,log 库可以类比 Java 日志生态里的 SLF4J 库,而日志实现则是 Logback 或 Apache Log4j 这样的库。实际上,Logforth 的名字就来自于对 Logback 的致敬。

编写 Logforth 的动机,是我在选择 Rust log 实现的时候,现有所有实现都不满足我的灵活定制需求。生态当中实际定位在完整 log 实现的库,只有 fern 和 log4rs 这两个。其中,fern 已经一年多没有新的发布了,一些显而易见的问题挂在那里,内部设计也有些叠床架屋。而 log4rs 则是对 Log4j 的直接翻译,也有许多非常诡异的接口设计(底层问题是 Java 的生态和设计模式到 Rust 不能直接照搬)。

于是,我花了大概两个小时的时间,梳理出 Rust 和 Java 日志生态里日志实现库的主要抽象:

  • Appender 定义日志写出到目标端的逻辑;
  • Filter 定义日志是否要打印的逻辑;
  • Layout 定义日志文本化的格式。

然后花了一天的时间,把 log4rs 的主要 Appender 即标准输出和(滚动)文件输出给实现了,同时完成了基本的基于日志级别的 Filter 和纯文本以及 JSON 格式的 Layout 实现,就此发布的 Logforth 库。

经过两周的完善,目前 Logforth 库已经具备了全部日志实现所需的基本能力,且可以自由扩展。接口都非常干净,文档也都补齐了。除我以外,一开始一起讨论需求的 @andylokandy 也极大地帮助了 API 的设计跟多种常用 Appender 的实现和优化。应该说,目前的 Logforth 已经是超越 fern 和 log4rs 的库了。

而回到本文的主题,之所以我能在一天时间内就写出第一个版本,我跟 @andylokandy 能目标明确、充满动力地实现 Logforth 的功能,就是因为看到了自己和整个 Rust 生态的需求,并且我们清楚应该怎么实现一个足够好的日志库。

Rust 生态的诸多潜在机会

最后,为坚持到这里的读者分享几个我看到的 Rust 生态潜在的机会。

总的来说,目前整个 Rust 生态接近 Java 1.5 到 1.7 时期的生态,即语言已经流行开来,核心语言特性和标准库有一些能用的东西,整体的调性也已经确定。但是,距离 Java 1.8 这样一个全面 API 革新的版本还有明显的距离,核心语言特性的易用性有很大的提升空间,标准库的 API 能用但算不上好用,开源生态里开始出现一些看起来不错的基础库,但是还远远没有达到 battle-tested 的状态。

第一个巨大的缺失点就是 Async Rust 的实用工具。我找一个 CountdownLatch 的实现,找了半天才发现去年底发布的 latches 库符合我的期待。要是去年我有这个需求,就真没人能搞定了。后来我要找一个 Async 版 WaitGroup 的实现,找来找去没有一个合适的,只能自己 fork waitgroup-rs 改改来用。

Crossbeam 是个很不错的高质量库,可惜它提供的是同步版本的并发原语,不能在 Async Rust 里使用。上面 latches 和 waitgroup-rs 单看实现得还可以。但是这种单文件库,真的很难长期维护,而且像 latches 这样一个结构硬整出各种 feature flags 的做法,其实是反模式的,没必要。

所以一个 async-crossbeam 可能是目前我最想看到的社群库,或许它可以是 futures-util 的扩展和优化。这些东西不进标准库或者事实标准库,各家整一个,真的有 C++ 人手一个 HashMap 实现的味道了。

第二个缺失点,顺着说下来就是 Async Runtime 的实现。Tokio 虽然够用,但是它出现的时间真的太早了,很多接口设计没有跟 Async Rust 同步走,带来了很多问题。前段时间 Rust Async Working Group 试图跟 Tokio 协商怎么设计标准库的 Async Runtime API 最终无疾而终,也是 Tokio 设计顽疾和社群摆烂的一个佐证。

async-std 基本已经似了,glommio 的作者跑路了,其他 xxx-io 的实现也有种说不出的违和感。这个真没办法,只能希望天降猛男搞一个类似 Java ExecutorService 这样的体系了。而且最好还要对 Send + Sync + 'static trait bound 做一些参数化,不用全部都强行要求……

IO 和 Schedule 虽然有关系,但还是不完全一样的概念。总的来说 Rust 生态里暂时没有出现跟 Netty 一样 battle-tested 的网络库,也没有能赶上 ExecutorService 生态的 battle-tested 的异步调度库。不过好的调度原语库还是有一些的,比如 crossbeam-deque 和 soml-rs 里的相关库,等待一个能合并起来做高质量 Runtime 的大佬!

第三个缺失点还是跟 Tokio 有关。应该说 Tokio 确实做得早而且够用,但是很多设计到今天看就不是很合适了,然而社群里用得还是很重,就变得更不好了。比如上面的 log 库,Tokio 生态里搞了个叫 Tracing 的跟官方 log 库不兼容的接口和实现,也是一大堆槽点。

这里想说的是 bytes 库。很多 Rust 软件闭眼睛就用 bytes 处理字节数组,这其实有很大的性能风险。例如,基于 bytes 搞的 PROST 就有一些莫名其妙的的多余拷贝,可参考《Rust 解码 Protobuf 数据比 Go 慢五倍?》。值得一提的是,PROST 也是 Tokio 生态的东西。

Apache OpenDAL 内部实现了一个 Buffer 抽象,用来支持不连续的字节缓冲区。这个其实也是 Netty 的 ByteBuf 天生支持的能力。如果现在 Rust 有一个 Netty 质量的网络库,有一个 ExecutorService 的接口跟一些基本可用而不像 tokio 一样全家桶的实现,再把文件系统操作搞好点,我想整个 Rust 生态的生产力还能再上一个台阶。

Rust 的 Async FileSystem API 实现现在没有一个能真正 Async 的,这个其实 Java 也半斤八两。不过 Rust 的 io::copy 不能充分利用 sendfile syscall 这个,就被 Java 完爆了。这点是 Apache Kafka 实现网络零拷贝的重要工程支撑。

最后一个,给国人项目做个宣传。Rust 的 Web 库也是一言难尽,同样是 Tokio 生态的 Axum 设计让人瞠目结舌,整个 Rust 生态对 Axum 的依赖和分发导致了一系列痛苦的下游升级体验,可参考 GreptimeDB 升级 Axum 0.7 的经历,至今仍然未能完成。

国内开发者油条哥搞的 Poem 是更符合 Web 开发习惯的一个框架,用起来非常舒服,也没有奇怪的类型挑战要突破。Poem 的最新版本号是 3.0.4 而不是像 Axum 的 0.x 系列,这非常好。

不过,Poem 的接口文档、回归测试跟一些细节的易用性问题,还需要更多开发者使用慢慢磨出问题跟修复。希望 Poem 能成为一个类似 Spring 的坚固 Web 框架,这样我写 Rust 应用的时候,也能省点心……

举个例子,我的一个 Poem 应用里有 query handle panic 的情况,就踩到了 Poem 一个并发对齐的设计没有处理 panic 的缺陷。当然,我顺手就给修了,所以新版本里应该没这个问题。

至于文档不全的例子,主要是错误处理的部分,应该需要更多最佳实践。

无论如何,目前 Rust 生态整体的生产力和生产热情,还是比 Java 要高出太多。长期我是看好 Rust 的发展的,短期我只能安慰自己 Netty 是一个在 Java 1.6 才出现,1.8 才开始逐渐为人熟知使用,5.x 胎死腹中,发展时间超过 15 年的项目,生态要发展并不容易了。

改良 SQL Interval 语法:一次开源贡献的经历

本文是 GreptimeDB 首位独立 Committer Eugene Tolbakov 所作。

在上一篇文章《GreptimeDB 首位独立 Committer Eugene Tolbakov 是怎样炼成的?》当中,我从社群维护者的视角介绍了 Eugene 的参与和成长之路。这篇文章是在此之后 Eugene 受到激励,从自己的角度出发,结合最近为 GreptimeDB 改良 SQL Interval 语法的实际经历,分享他对于开源贡献的看法和体验。

以下原文。


开发者参与开源贡献的动机不尽相同。

开源贡献本身是一种利用自身技能和时间,回馈社群和造福更广大受众的方式。开源社群是一个绝佳的环境。参与贡献的人在其中能够与高水平开发者自由交流并建立联系,甚至可能找到可靠的导师或合作者。对于寻求职业发展的开发者,开源贡献可以作为个人技能和经验的展示。当然,也有许多开发者参与贡献只是出于个人的热爱。这很好!当你真正投入到一个开源项目当中后,你可能会发现软件的缺陷或缺失的功能。通过提交补丁修复问题或实现功能,不仅可以消除挫败感,还能让每个使用该软件的用户都受益。

成功参与开源贡献的秘诀不仅仅是编写代码。强烈的学习欲望才是內源动力,由此推动开发者才会主动了解本不熟悉的代码库,并应对其中出现的种种挑战。社群的及时响应和其他资深成员的支持,是这种学习欲望转换为真正参与贡献的重要保障。

社群的及时响应让开源开发者感到宾至如归;其他资深成员提供的指导和反馈,帮助新成员改进自己的贡献水平。理想情况下,你应该可以测试自己的补丁,并将修改版本应用到工作或个人项目当中。这种来自真实世界的需求,尤其是本人的需求,为开源贡献提供了重要的使用场景支撑,确保你的贡献是真的有用,而不是闭门造车的产物。只有如此,通过开源参与做出的贡献才能对整个社群产生持久的影响。

回到我本人的例子上来。虽然我已经花了不少时间锻炼自己的软件开发技能,但事 Rust 对我来说仍然颇有挑战。这种“新手”的感觉可能会让一些人不愿做出贡献,担心他们的代码不够好。然而,我把愚蠢的错误当作提高技能的垫脚石,而不是气馁的理由。

GreptimeDB 社群一年多的参与贡献经历,是一段不断学习并获得丰厚回报的旅程。今天,我会向你介绍其中的一次具体的贡献。让我们亲自动手吧!(或者我应该说,爪子?🦀)

动机和背景

这次贡献主要的目的是支持一个 SQL Interval 字面量的简化语法

1
select interval '1h';

SQL 标准定义了 Interval 的语法:

1
select interval '1 hour';

这个语法相对冗长,我们希望支持上面展示的简化语法,让 select interval '1 hour'select interval '1h' 返回相同的结果。

深入研究代码后,我发现处理转换的核心功能已经存在。为了实现上面的效果,只需针对 Interval 数据类型添加一条新规则:把简化语法格式的 Interval 将自动扩展为标准语法。让我们仔细看看代码中执行相关逻辑的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn visit_expr(&self, expr: &mut Expr) -> ControlFlow<()> {
match expr {
Expr::Interval(interval) => match *interval.value.clone() {
Expr::Value(Value::SingleQuotedString(value)) => {
if let Some(data) = expand_interval_name(&value) {
*expr = create_interval_with_expanded_name(
interval,
single_quoted_string_expr(data),
);
}
}
Expr::BinaryOp { left, op, right } => match *left {
Expr::Value(Value::SingleQuotedString(value))=> {
if let Some(data) = expand_interval_name(&value) {
let new_value = Box::new(Expr::BinaryOp {
left: single_quoted_string_expr(data),
op,
right,
});
*expr = create_interval_with_expanded_name(interval, new_value);
}
// ...

代码评审

上面是我第一次尝试写成的代码。GreptimeDB 的资深 Rust 开发者 Dennis 很快发现了改进空间

Dennis 发现了不必要的复制

代码评审是一个优秀的学习渠道。

除了简单地接受建议(因为“减少复制”的理由很明确!),我决定深入研究。通过分析建议并尝试自己解释,我巩固了对 Rust 代码和最佳实践的理解。

避免不必要的 Clone 和所有权转移

起初,我直接在 interval.value 上调用 clone 来取得一个具有所有权的结构:

1
match *interval.value.clone() { ... }

这里的 clone 每次都会创建一个新的数据实例,如果数据结构很大或克隆成本很高,那么这可能是一个性能的影响因子。Dennis 建议通过使用引用来避免这种情况:

1
match &*interval.value { ... }

匹配引用(&*interval.value)避免了 clone 的开销。同样的手法可以用在对二元运算符 left 的匹配逻辑里:

1
match &**left { ... }

这个稍微复杂一些:它使用双重解引用来获取对 Box 内部值的引用。因为我们只需要读取数据结构中的部分信息,而不需要转移所有权,因此只获取引用是可行的。这也减少了 clone 的开销。

清晰的模式匹配

在模式匹配中使用引用可以强调仅读取数据而不是转移所有权的意图:

1
match &*interval.value { ... }

只在需要所有权的时候进行克隆

在第一版代码当中,opright 总是被复制一份:

1
2
3
4
5
let new_value = Box::new(Expr::BinaryOp {
left: single_quoted_string_expr(data),
op,
right,
});

但是,其实克隆只需发生在匹配左侧变量是 Expr::ValueSingleQuotedString 变体,同时 expand_interval_name 成功的情况下。Dennis 建议把 clone 调用移动到 if let 块内,从而只在需要所有权的时候进行克隆:

1
2
3
4
5
6
7
if let Some(data) = expand_interval_name(&value) {
let new_value = Box::new(Expr::BinaryOp {
left: single_quoted_string_expr(data),
op: op.clone(),
right: right.clone(),
});
// ...

直接引用

在第一版代码当中,我用 expand_interval_name(&value) 显式借用了 value 的值。

然而,valueString 类型的值,它实现了 AsRef<str> 特质,所以它可以被自动解引用成 &str 类型。修改后的版本直接写成 expand_interval_name(value) 不需要再手动 & 取引用。

译注:

这个说的不对。其实是因为 String 实现了 Deref<Target=str> 所以不 clone 以后 &String 可以被自动转成 &str 类型,但是之前 clone 的时候传过去的是带所有权的 String 类型结构,这个时候 &value 取引用而不是把带所有权的结构整个传过去是必要的。

Deref 的魔法可以查看这个页面

总结

在这次贡献当中,代码的“效率”体现在以下三个方面:

  • 避免不必要的克隆,减少运行时开销;
  • 让借用和所有权转移的模式更清晰和安全;
  • 提升整体的可读性和可维护性。

最终版本的 visit_expr 大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fn visit_expr(&self, expr: &mut Expr) -> ControlFlow<()> {
match expr {
Expr::Interval(interval) => match &*interval.value {
Expr::Value(Value::SingleQuotedString(value))
| Expr::Value(Value::DoubleQuotedString(value)) => {
if let Some(normalized_name) = normalize_interval_name(value) {
*expr = update_existing_interval_with_value(
interval,
single_quoted_string_expr(normalized_name),
);
}
}
Expr::BinaryOp { left, op, right } => match &**left {
Expr::Value(Value::SingleQuotedString(value))
| Expr::Value(Value::DoubleQuotedString(value)) => {
if let Some(normalized_name) = normalize_interval_name(value) {
let new_expr_value = Box::new(Expr::BinaryOp {
left: single_quoted_string_expr(normalized_name),
op: op.clone(),
right: right.clone(),
});
*expr = update_existing_interval_with_value(interval, new_expr_value);
}
}
_ => {}
},
_ => {}
},
// ...

开源贡献是我找到的加速 Rust 学习的绝佳方式。参与贡献 GreptimeDB 只是一个例子,说明了我如何通过开源贡献获得知识。根据读者反馈,我很高兴在未来的帖子中分享更多这些学习经验!

非常感谢整个 Greptime 团队,特别是 Dennis 提供的帮助和指导,感谢他们在我贡献过程中的支持和指导。让我们继续贡献和学习!

Rust 社群何以走到今天?

本文有些标题党,实际想讲的内容,是我从部分 Rust 曾经的核心开发者的自述当中,所发现的 Rust 项目社群开源协同模式发展至今的一些特点。

我会从这些自述发言的内容切入和展开,对比其他社群遇到相似挑战的状况和应对方式,讨论 Rust 项目社群在协同方式维度上走到今天的沿革。

Graydon Hoare 的博文

说起 Rust 语言的核心成员,你会先想起谁?

不像 C# 之父 Anders Hejlsberg 和 Python 之父 Guido van Rossum 这样在很长一段时间里可以代表语言本身的人物,也不像 Brian Behlendorf 早期经常代表 Apache Web Server 项目发言,Rust 项目社群的一大特色就是不仅没有所谓的终身的仁慈独裁者(BDFL),甚至很难找到一个有足够权威拍板的人。

这就是 Rust 社区协同模式发展成今天这样的一个核心影响因素。

Graydon Hoare 是 Rust 的第一作者,但是如果你不对 Rust 的历史感兴趣,在日常讨论 Rust 的对话中,你很难听到这个人的名字或者他的观点。这是为什么呢?

通过查看 Graydon 在 Rust 仓库的提交记录,我们可以看到自 2014 年起,他就可以说不再参与 Rust 核心的开发了。

2014 年起,Graydon 的主要精力就投入在某区块链项目和 Swift 语言上:

apple/swift

apple/swift-source-compat-suite

不过,有趣的是,随着 Rust 在区块链领域越来越火,近一段时间 Graydon 又开始写起了 Rust 代码

Rust 1.0 的发布要追溯到 2015 年,而此时 Graydon 早已从 Rust 的核心开发中离开。这其中的原因可以从 Garydon 今年发布的一篇博文《我想要的 Rust 语言没有未来》中窥得端倪。

文章讨论了 Garydon 在设计 Rust 之初和早期开发过程中对 Rust 的定位,以及他对 Rust 语言当前某些特性的锐评,其中包括他不喜欢的一系列语言特性:

  • 跨 Crate 的内联和单态化;Garydon 引用了 Rust 另一位早期开发者 @brson 关于 Rust 编译时间的评论
  • 以库形式定义的容器、迭代器和智能指针;Garydon 希望这些都作为语法特性从而可以在编译器层面做更加极致的优化,这也是 Golang 的做法;
  • 外部迭代器,Async/Await 语法,& 作为类型,以及显式的生命周期;这些都是如今 Rust 语言基石级别的设计,Garydon 有不同的看法。
  • Lambda 表达式的环境捕获。
  • Traits 的设计,不完全的 Existentials 实现;后者简单地说就是跟 dyn Trait 相关的一系列设计和实现的问题。
  • 复杂的类型推导;这个评论可以参考《对 Rust 语言的分析》中的“类型推导”一节。
  • Nominal 类型;Garydon 喜欢 Golang 的 Structral 类型,这个问题跟 Traits 和 dyn Trait 的使用体验是相关的。
  • 缺少反射的支持。
  • 缺少错误处理系统;Garydon 喜欢 Swift 的错误处理方案。
  • 缺少 quasiquotes 功能,缺少语言级别的大整数支持,缺少语言级别的十进制浮点数支持;这些功能现在部分由生态中的第三方库实现,例如 @dtolnay 的 quote 库实现了 quasiquotes 的能力,但是 Garydon 认为它应该是语言的一部分。
  • 复杂的语法。
  • 缺少尾调用的保证。

可以看到,Garydon 设计和期望中的 Rust 语言跟如今实际成长出来的 Rust 语言大相径庭:关于引用和生命周期的设计,关于 Traits 和类型系统的设计,关于性能和编程效率之间的取舍,Garydon 的思路都不同于 Rust 如今的主流思路。

Garydon 对上面这些异议都分享了他 argue 的历史,实际上,他在博文一开始就对他在 argue 中失败的情形做了分类:

  1. 我计划或初步实现了 X 方案,其他人以高度的热情和坚持支持 Y 方案,包括利用舆论的影响力。最终,他们得到了自己想要的结果,而我失败了。
  2. 我初步实现了 X 方案,其他人更喜欢 Y 方案并做出了原型。Y 方案很吸引人,于是大家转而投入 Y 方案的开发,而我们时间紧迫,没有重新审视 Y 方案的所有缺点。
  3. 我初步实现了 X 方案,但是实际情况(通常是 LLVM 的限制)要求我们采用 Y 方案快速实现。我们暂时开发除了 Y 方案,然后同样由于时间紧迫,我们就一直将错就错的在 Y 方案上持续发展。
  4. 我初步实现了 X 方案,但是实现得明显有问题。于是在正式发布前我们把它从语言核心中移出,免得开发者在错误的基础上构建软件。于是一个语言级别的功能没有实现,后来生态系统或许填补了这一空白,甚至或许有多个竞争者。

对号入座,上面的众多 Garydon 不满意的特性都能分类进这四项中来。Garydon 在介绍这些他不喜欢的特性时多次提到了“我输了”,甚至在关于尾调用的讨论中,他写到:

由于实现尾调用的计划和其他成员对“性能上胜过 C++ 语言”的目标有冲突,我最终被说服不要使用尾调用。于是,我写了一篇悲伤的帖子来表达我对这个结果的不满,这是这个话题中最令人悲伤的事情之一……如果我是“终身的仁慈独裁者”,我可能会让语言朝着保留尾调用的方向发展。早期的 Rust 有尾调用,但主要是 LLVM 的原因让我们放弃了这个功能,而对跟 C++ 在性能上比拼的执著,使得这个结论几乎永远不会被推翻。

这就是 Rust 项目社群不同于其他编程语言项目社群的一个重要特点:它的作者不喜欢语言实际采用的许多核心设计,他没有妥协,也没有说服其他人,最终只能选择离开。

开源世界里有没有作者后来离开的例子,或者朝着不同于最初目标发展的案例呢?其实是有的。

Python 的作者 Guido van Rossum 在前几年也开始输掉不少关于 Python 新语法的争论。终于在 2018 年,他精疲力竭地离开了。但是至少 Python 的几乎所有核心功能都是在他的领导下实现的,同时他也能够以 BDFL 的身份强推社群从 Python 2 迁移到 Python 3 上来,尽管他事后表示这样的事情再来一次他或许从一开始就不会做。而在 Rust 项目社群当中,你很难想象有人能够做出这种级别的 breaking changes 并让社群接受。

C# 的作者 Anders Hejlsberg 在开始搞 TypeScript 以后实际也不怎么参与 C# 的发展了,但是他奠定了 C# 的基础和核心设计,且 C# 总体由思路一致的微软研发团队主导开发,所以这没有带来什么重大的影响。

Apache Flink 的作者最近也去创业搞别的事情了,不过由于社群人丁兴旺,社群发展过程一直是开放讨论达成共识,在早期开发者离开后,现在的开发者持续维护没有什么问题。如果熟悉 Flink 的历史,你会发现起初 Flink 是一个跟 Spark 正面竞争的批处理引擎,是在 2014 年中 Gyula Fora 带着他的实习生在 Flink Runtime 的基础上把整个项目改造成了流计算框架。不同于 Rust 的是,Flink 的作者们愉快地接受了这个改造,并把 Flink 重新定位成带状态的流计算框架全力发展,最终走出了一条不同于 Spark 的竞争之路。

当然,除了作者离开的,还有坚守在原地且不断整合不同人的意见的。在这一点上,做得最出色的毫无疑问是 Linux 的 BDFL 林纳斯·托瓦兹。

《时代周刊》曾经评论到:

有些人生来就注定能领导几百万人,有些人生来就注定能写出翻天覆地的软件。但只有一个人两样都能做到,这就是林纳斯·托瓦兹。

其实,这样的例子在开源世界中实在是少之又少。跟 Graydon 一样离开项目的有没有呢?Redis 的作者或许可以算一个。即使 Redis 的基石是他奠定的,但是后来 RedisLabs 的很多发展明显跟他对 Redis 的期望是有很大出入的,他于是在 2020 年公开声明不在担任 Redis 的维护者。

@withoutboats 的博文

Garydon Hoare 的博文分享了上古时期 Rust 团队的争论,而 @withoutboats 最近的几篇博文补充了许多 Rust “中世纪”的故事。

例如,上一节中 Garydon Hoare 不喜欢的外部迭代器,Async/Await 语法,& 作为类型,以及显式的生命周期这些特性,在 Why Async Rust 一文里 @withoutboats 是高度正面评价的。

那么,@withoutboats 是谁?

他是 Async Rust 的主要实现者之一,主导确定了 Async/Await 的语法,并实现了 Pin 和 Waker 等关键接口。

可惜的是,由于一些 GitHub Commit 和账号关联的问题,我们并不能简单地列出他的所有贡献。不过,就算是所有的 Commits 都能正确显示,@withoutboats 从提交数上看仍然不能排到 Rust 哪怕前 100 的贡献者名单里,从 2020 年开始,@withoutboats 就没有在 Rust 语言相关仓库的新提交了。

如果我们看看 @withoutboats 文章中提到的另外两位 Async Rust 的核心开发者,情况会更加有趣。

aturon-contribution

Aaron Turon 从 2017 年开始就没有任何参与了。

alexcrichton-contribution

Alex Crichton 是非常重要的 Rust 核心开发者。除了 Async Rust 的开发以外,他是 Cargo 项目的核心作者,且以 rust-lang-nursery 为阵地,打造了一批 Rust 早期的关键生态。

然而,他从 2022 年起也慢慢淡出了 Rust 项目社群,投入到 wasmtime 项目的开发中去了。

当然,wasmtime 项目的核心也是以 Rust 语言编写的。Alex Crichton 的这一决定,其实有点像从 2018 年开始淡出了 Rust 项目社群,2019 年加入 PingCAP 开发 TiKV 项目的 Brian Anderson

brson-contribution

2021 年,Brian Anderson 从 PingCAP 离开,关注起区块链公司 Solana Labs 的项目,甚至最近还跟 Graydon Hoare 主持的 Rust 项目 stellar/rs-soroban-env 有些合作,也是一种循环。

这个时候,我们再来看看 @withoutboats 对 Rust 语言演化建议的几篇博文,可能就会有不一样的感受:

《大教堂与集市》第三篇《开垦心智层》里讨论了关于开源软件话语权的问题,里面提到话语权的两种来源:

  1. 代码是你写的,于是你拥有代码的“所有权”,根据“责任背后是权力”的规则,你能够对如何演进这部分代码做定夺。
  2. 争议的双方并没有明确的所有权,但是一方在整个项目中投入更多,也就是在整个项目中拥有更多的领土权,所以他作为资深者胜出。

书中提到,如果这两条规则不能解决,那么“则由项目领导人来决断”。

很显然,Rust 项目不怎么符合这些条件。如前所述,拥有 Async Rust 的代码“所有权”的人都已经离开,且很难说有什么明确的传承。“项目领导人”在 Rust 社群中可以认为并不存在,第一作者 Garydon Hoare 伤心地离开了项目,堪称继父的 Brian Anderson 也从 2017 年起投入到 Rust 写成的项目而非 Rust 语言本身。

如此,Rust 项目社群就进入了 @withoutboats 所观察到的现状:尽管用户对 Async Rust 的后续进展很不满意,尽管 Rust 语言对 Immoveable / Unforgetable / Undroppable 这些能够与编译器深度协作的基础类型的需求是清楚的,但是放眼整个 Rust 社群,对于已经确定要做的事情如何做,争论几乎总是无法收敛,而对于不确定要不要做的提案,尤其是核心假设的进化与兼容方案,更是在未来几年内都看不到达成一致的希望。

@withoutboats 在《关于 Async Rust 的四年计划》的最后一段无不担忧地分享了一个过往的失败经历:

对于那些不了解的人来说,有一个关于 Rust 中 await 运算符应该是前缀运算符(就像其他语言一样)还是后缀运算符(最终的选择)的大辩论。这引起了极大的争议,产生了超过 1000 条评论。事情的发展是,几乎所有语言团队的人都达成了运算符应该是后缀的共识,但我是唯一的反对者。此时,明显已经没有新的论点出现的可能性,也没有人会改变主意。我允许这种状态持续了几个月。我对自己的这个决定感到后悔。显然,除了我屈服于大多数人的意见外,没有其他的出路,然而我却没有第一时间这样做。我让情况不断恶化,越来越多的“社群反馈”重复着已经提出的相同观点,让每个人都筋疲力尽,尤其是我自己。

我从这个经历中学到的教训是:区分真正关键的因素和无关紧要的因素。如果你对某个问题固执己见,最好能够清楚地阐述为什么它很重要,并且它的重要性应该超过语法选择之间微小的差异。自那以后,我试图将这一点铭记在心,以改变我在技术问题上的参与方式。

我担心 Rust 项目从这个经验中得出了错误的教训。正如 Graydon 在这篇文章中提到的,Rust 开发团队坚持只要有足够的构思和头脑风暴,就能找到每个争议的双赢解决方案,而不是接受有时必须做出艰难决策的事实。Rust 开发团队提出的解决这种无休止的争议所带来的 burn out 的方式,是转向内部讨论。设计决策现在主要以 Zulip 线程和 HackMD 文档等未索引的格式进行记录。在公开表达设计方面,主要是在相关开发者的个人博客上发布。如果你是一个开发团队以外的人,那么你几乎不可能理解 Rust 开发团队认为什么是优先事项,以及这些事项的当前状态如何。

我从未见过 Rust 项目与其社群的关系如此糟糕。社群当中存在着无价的知识,封闭自己不是解决方案。我希望看到项目成员重建与社群成员之间的相互信任和尊重的关系,而不是无视目前的敌对和不满的现状。对此,我要感谢那些在过去几个月中与我就设计问题进行交流的项目成员。

Nick Cameron 的博文

对于 @withoutboats 在上一节的最后提出的问题,Nick Cameron 在今年早些时候有几篇文章做出了讨论,直接相关的是这篇《关于开放式协作的一些想法》

非常有趣,在我介绍 Rust 的历史沿革的时候,我大量引用了其核心参与者的博客,这可以看做是 @withoutboats 所提到的“在公开表达设计方面,主要是在相关开发者的个人博客上发布”的习俗对整个 Rust 项目社群工作方式的一个更深远的影响。

那么,Nick Cameron 又是谁?

nrc-contribution

他是 Rust 语言团队曾经的成员,Rust 曾经的核心开发者。跟 Brian Anderson 一样,他在 2019 年加入了 PingCAP 公司,并于 2021 年离开 PingCAP 加入 VS Code 团队做 Rust 语言集成。于是,他在 Rust 主仓库的参与也从 2019 年开始消失,只在做回 Rust 语言层面相关的工作后做了一点工作。可以说,他所写的代码,如今也进入到上一节里提到的困境中:他是作者,但是已经离开太久,且他并未找到明确的继任者,于是这些代码如今是无主的。他对相关代码的演进没有说一不二的话语权,其他人也没有。

其实,在 Rust 项目社群中对编译器或标准库还能有很强势话语权的人,我能想到的大概就是:

  • 语言团队的领袖 Niko Matsakis
  • 编译器团队的成员 Oli Scherer
  • 发布团队的领袖和 Leadership Concil 的成员 Mark Rousskov

其他人就算是所谓 Leadership Concil 的成员或者是某个团队的领袖,从《开垦心智层》里提到的朴素的代码“所有权”的理解方式来看,也不一定是有说服力的。

说回 Nick Cameron 的博文,《关于开放式协作的一些想法》的核心是重申了 Rust RFC 流程的价值,并希望 Rust 在代码以外的治理流程里也坚持相同的开放原则和勇敢做出决定而不是征求所有人意见,这跟 @withoutboats 博文的观点是一致的。

实际上,Nick Cameron 在前两年年强力推进 Async Rust 的部分工作和 GAT 进入稳定版本。后者引起了巨大的争论,这同样让他精疲力竭,写下这两篇文章表达自己对这些争论的看法:

在具体项目的开放式协作以外,Nick Cameron 的其他几篇博文揭示了 Rust 项目社群今日协同格局的另一个重要影响因素:Rust 基金会。

评论

Rust 项目社群发展成今天的样子,其最核心影响因素,就是开发层面没有一个说一不二的领导人,或者一个团结的核心团队。

相信很多人还记得前两年 Rust Mod Team 集体辞职的事情,作为某种后续,实际上 Mod Team 批评的 Core Team 成员包括 Core Team 本身也都从 Rust 社群中消失了。

取代 Core Team 的是所谓的 Leadership Council 组织.该组织于今年六月份成立,起初每周有一次会议,现在减少到每个月有一次会议。讨论的内容主要关注治理、流程和标准问题。

这种情况是否经常发生呢?实际上它也不算罕见。

几乎可以称作 1:1 复刻的例子是 Perl 社群 2021 年的一个故事:致力于激进推动 Perl 发展的项目领袖 Sawyer X 想要推动 Perl 7 版本的发布,结果被其他项目成员联手弹劾。最终项目夭折,他也失望地离开了 Perl 语言团队。Perl 如今也是由一个叫 Perl Steering Council 组织管理。不过不同点是 PSC 尽管在语言发展上相对保守,但确实是领导语言开发的,且工作内容全面公开

作为补充,有心的读者可能已经发现上面列举的三位撰写跟 Rust 发展相关的博文的前核心开发者,如今并不在 Rust 的任何治理机构上。所以尽管没有那么激烈,但是他们的处境和 Sawyer X 是有某种相似之处的。

最后,Rust 项目社群的未来会如何发展呢?我想最有可能的结局,就是像 C 或者 C++ 那样,演化成由标准化委员会主导的语言项目。

实际上,这个新的 Leadership Council 做的一件重要的事情就是开始搞所谓的 Rust 标准

Rust 不像 C# 和 Golang 那样,语言本身就是某家公司的独占软件;也不像 Java 那样,虽然有 JCP 和委员会,但是 Oracle 以模块化提案为契机,奠定了自己几乎说一不二的地位;更不像 Ruby 或者 Elixir 这样的个人作者可以作为 BDFL 拍板。

Rust 基金会的成员就像 C++ 标准化委员会里的成员那样,哪个不是行业大鳄,哪个不是已经有或者打算有海量 Rust 生产代码。为了保护自己生产代码不被 Rust 演进制造出庞大的维护迁移成本,这些厂商势必要尽己所能的向 Rust 项目社群发挥自己的影响力。

由于 Rust 的第一作者和绝大多数早期核心作者已经长期离开项目社群,即使现在回来也不可能再建影响力。唯一有足够长时间和技术经验的 Niko Matsakis 又只关心语言技术发展,甚至 Leadership Council 的语言团队代表也让其他成员参与。这种情况下,Rust 项目社群的个人开发者,是不可能跟基金会里的企业有对等的话语权的。

实际上,如果 Rust 真能发展到 C++ 那样的状况,即使 C++ 有公认的第一作者 Bjarne Stroustrup 存在,他也无法在 C++ 委员会中强力推行自己的主张。

如果你对这样的未来感到好奇,推荐阅读 Bjarne Stroustrup 的论文 Thriving in a Crowded and Changing World: C++ 2006–2020 的第三节 C++ 标准委员会。我预计是已有之事,后必再有。

共建的神话

今天,在许多开源软件项目乃至任何稍带开放性的项目的宣传中,我们经常能看到营销内容包含“共建”的字样,似乎说了这个魔法的词汇,项目就能迎接纷至沓来的“共建者”,其乐融融地“一起做大蛋糕”。

然而,现实情况却是作为被营销对象的开发者日渐对这个词失去兴趣到产生反感。一眼看到某个项目营销言必称“共建”,心里就默默给它扣分拉黑甚至拉黑。共建的神话是如何产生的,为什么这个口号今天不能吸引和凝聚开发者呢?

“共建”一词的释义是:共同建设,体现共同参与,发挥自身优势和潜能,形成新的合作优势。

商业公司起初提这个词的动机,我想是成功的开源协同案例大多社群生态丰富、成员活动频繁。公司希望自己发起的项目也能一步到位整出一个人见人爱的社群,于是直接无差别宣传拉人共建,企图跨过中间阶段直达结果。

我曾经把共同创造价值作为开源社群发展的重要指导理念。为了做到共同创造价值,你需要回答潜在的用户和开发者他们能为你做什么和应该怎么做到的问题。

今天许多项目“共建”的营销内容,上来先说的是自己如何如何的牛逼,或者庆祝完成了一二三四五件事情,到底期望营销的对象来共建个啥,全凭自己领悟,其画风大致如下:

  • 我出弹药,谁能帮我打下这个山头?
  • 我指出这个山头,谁能自备弹药帮我打下这个山头?
  • 我也不知道哪里有山头,谁能自备弹药和地图,帮我找到和打下一个山头?

一份有号召力的“共建”宣言,应当形如以下模式:

  1. 一个新的产品或功能发布,期待用户给出反馈或使用测试报告;
  2. 一个新的集成点已经就绪,诚邀生态项目和开发者评估并支持集成;
  3. 一个新的技术难题被攻破,欢迎同领域的专家评鉴、探讨和改进;
  4. 一个新的标准由权威机关提出,号召相关项目和开发者参与认证;
  5. 一个新的组织或活动立项,介绍清楚纲领或目的之后招募成员。

可以看到,这些模式在包括事实的基础上,明确了“谁”是我们的营销对象,我们希望给你带来“什么价值”,期待你做“什么具体的事情”。否则,所有可以“共建”的点,在营销文案里都不存在。要么说某个组织已经成立了,但是跟我有什么关系呢?要么说某个功能已经完成并发布,那还要我共建什么呢?

这样漫无目的的“共建”营销,只会让被营销对象走向两个极端:

  1. 完全不知道你要做什么,只能理解为我需要自己发现问题并解决问题,你在期待有人 freework 无偿提供 idea 及其实现;
  2. 拉人头“共建”是吧?我来了,什么时候打卡发小礼物?

最后简单总结一下,社群需要成员共同建设以持续发展,但是在营销的时候,应该明确『共同创造价值』理念中的谁、为什么、做什么、怎么做的问题。否则只是喊一嗓子就坐等社群爆火共建者纷至沓来,是不可能实现的。反证假设今天真的就有一大群“共建者”出现了,恐怕你自己也不知道,他们是来干嘛的。

Code Review 的方法

开源社群可以通过开展广泛的开源协同来规模化开发软件的生产力。为了实现提升生产力的目标,一般而言需要解决两个阶段的问题:第一个阶段是如何找到适合的潜在开发者人群,吸引他们参与软件开发和社群建设;第二个阶段是社群的原始生产力上来以后,应对项目复杂度和沟通成本随人员规模平方级别增长的难题。

第一阶段的解决方案在《共同创造价值》当中有过相关讨论,第二阶段的问题可以通过工作的模块化和 Review 质量的提升来解决。本文即聚焦在开源协同当中的 Code Review 应该秉承什么原则,做到什么程度,如何避免常见问题来展开。

Code Review 的目的

《大教堂与集市》中有一个著名的 Linus 定律,“只要眼睛多,bug 容易捉”。这从某种方面体现了 Code Review 的价值,即发现代码补丁和主干代码当中可能的缺陷,在合并之前或合并之后予以修复,以保证软件的质量。

不过,Code Review 的价值远不止于发现代码缺陷,它至少还有两个重要的功能:减缓软件代码的熵增和传播项目社群的知识。前者从评估改动的必要性,改动方式是否与项目风格保持一致,以及保证代码的测试覆盖和文档覆盖等为切入点;后者主要是在 Patch Author 和 Reviewer 的交流过程中,传递社群成员共识的技术判断和做事方式。

Code Review 的内容

可以从开源社群常见的 Code Review 内容看出各个社群对 Code Review 的定位。

合理的动机

Code Review 的第一步并不是扎到代码里面去看写得对不对跟好不好,而是项目要不要做这个改动。

实际上,相当部分的参与者提交补丁之后迟迟未能得到 Reviewer 反馈的重要原因,就是决口不提为什么要做相关的改动,假设这一改动是社群共识。尤其是公司环境中被委派的工作,完成后想要将对应改动合并到上游,问及缘由只说是甲方或老板的需求,而不能从技术上或者广泛社群用户的角度上论证其价值,往往不会被上游社群所接受。

试想当你看到一个等待 review 的补丁,其中包含成百上千行改动,或者虽然只改了几行几十行但是意义不明,你还会花时间去思考其背后的深意吗?对于 Reviewer 来说,从整个项目的工程效率来看,遇到这种情况要么是直接不看了,要么是让补丁作者说明清楚动机和改动内容。只有判断这个补丁可能是有价值的,Reviewer 才可能分配时间仔细看具体的代码改动。

从另一个角度看,对于影响用户界面的改动,重大代码重构和功能提案,开源社群几乎总是要求提议者在提交代码补丁之前先撰写一个技术提案。Rust 是 RFC 机制,Apache Flink 是 FLIP 机制,Python 是 PEP 机制,诸如此类。

只有提案通过 Review 达成了一致,实现提案的代码补丁才有被 Review 的意义。提案本身不是代码,而是对要做的事情的动机的阐述和方案高度概括的设计。可以看到,Code Review 不是从代码本身开始做起的,而是先判断动机是否合理。

各个开源社群的实践中,PostgreSQL: Reviewing a Patch 文档就有一个 Do we want that? 的问题评估必要性,TiDB 开发者指南里也提到了 The pull request should resolve a real problem for TiDB users. 即改动必须有意义。

符合描述的实现

对代码补丁的动机和整体修改方案达成一致后,Code Review 的下一个阶段也是其核心阶段,就是检验代码实现是否与此前达成的共识一致。

Reviewer 进行 Code Review 的真相是这样的:首先,Reviewer 认同补丁的意图是合理的,随后,Reviewer 会在心里有一个预期的实现,对比这个预期的实现和代码补丁的实现,通常会快速略过和预期相同的代码,针对和预期不符的改动给出 Review 意见。

典型的情况是补丁的改动遗漏了某些特殊条件的判断,在配置的某种排列组合不能正确工作,或者引入了新的问题。这些情况下 Reviewer 会指出问题,负责的 Reviewer 还会尽量给出复现方式、技术原理和可能的改动方案。

为此,Reviewer 需要拉取补丁到本地,结合开发工具分析新版本代码,实际运行测试,以及按照自己的设想做出改动后再测试,最终给出需要改动的 Review 意见。

关于代码补丁的修改与描述一致,Flink 的 Review 指南有一点提到:

Does the Implementation Follow the Agreed Upon Overall Approach/Architecture? In this step, we check if a contribution folllows the agreed upon approach from the previous discussion in Jira or the mailing lists.

TiDB 开发者指南也提到:

The pull request should implement what the author intends to do and fit well in the existing code base.

夸奖优质的贡献

在上面提到的这个比对预期实现和补丁实现的过程里,除了发现补丁实现的问题,为补丁查漏补缺,还有一种情况,是补丁作者提出了意料之外的好方案。

老到的 Reviewer 不吝惜称赞亮眼的改动,虽然新人 Reviewer 可能会误以为 Code Review 的主题就是给补丁挑错,但是其实吸纳高水平的补丁,点出补丁作者做得好的地方,对社群发展有重要意义。这不仅是社群共识和软件质量螺旋上升的必经之路,对于补丁作者而言,得到正面反馈也是非常重要的一份激励。

曾经有位 Hadoop 的参与者在提交完第一个补丁之后,由于实现简洁明了而得到 Reviewer 的热情称赞,他受到强烈的正反馈持续参与,最终成为 Hadoop PMC 成员,并在不同场合分享这段经历,也传承了这个理念将更多新人带到开源参与的正向循环里。

关于正向激励的部分,TiDB 开发者指南有这样一段描述:

Offer sincere praise. Good reviewers focus not only on what is wrong with the code but also on good practices in the code. As a reviewer, you are recommended to offer your encouragement and appreciation to the authors for their good practices in the code. In terms of mentoring, telling the authors what they did is right is even more valuable than telling them what they did is wrong.

Python 开发者指南也有简短的一句:

Comment on what is “good” about the pull request, not just the “bad”. Doing so will make it easier for the PR author to find the good in your comments.

统一的代码风格

Code Review 的一大作用是减缓软件代码的熵增。

随着软件版本不断迭代,开发者们不断实现新的功能,引入新的抽象,就地修复代码缺陷。这些改动重重叠加,会把代码的复杂度也就是熵抬高到任何一个开发者都无法单独理解的程度。

控制代码复杂度的重要手段就是保持统一的代码风格。统一的代码风格能减少开发者入门的学习成本,跨越不同模块的理解成本,以及相互之间交流的沟通成本。

统一的代码风格,首要的一点就是功能实现的风格。

例如,几乎所有大型项目都会遇到不稳定测试的考验。这种不稳定性来到分布式系统领域,则主要是并发问题处理不当,尤其是没有统一的并发风格所导致的。

Apache Flink 的 Task Executor 进程在引入 Mailbox 设计重构之前,进程内的线程逻辑是犬牙交错的。开发者很难判断某个函数会在什么线程上下文当中被调用,也就不知道如何正确的在代码补丁中引用其他接口而不导致死锁。Apache Pulsar 的并发风格更是八仙过海各显神通,而开发者也最终要为这份代码熵付出巨大精力事后熵减的代价

反观开发超过 15 年的 Apache ZooKeeper 虽然并发工具的使用涵盖了直接使用 Thread 到 Executor 框架再到 CompletableFuture 组合子,但是核心的服务端处理逻辑和客户端通信逻辑的并发模型一直保持了连贯的风格,其他的并发工具使用范围都是局部的,这才造就了十余年来挑战者众多但是 ZooKeeper 一直保持分布式共识系统重要选型考虑的地位。另一个例子,Apache SkyWalking 的线程模型是核心模块统一设计、控制和调度的,其他模块通过接口提交任务,而核心模块调度保证这些任务不会出现竞争条件。这样,Code Review 的时候只要关注核心模块的线程变动,同时限制其他模块自建并发模型的范围,就可以很好地规避并发问题。

统一的代码风格,还包括语言范式和特性一致的的取舍。

典型的多范式编程语言,例如 C++ 和 Scala 都需要项目的核心开发者确定语言支持的各个范式和覆盖相同功能的不同特性之间的取舍。否则,一个 Scala 项目里部分代码对外暴露命令式调用方法,另一部分采用函数式编程提供一系列功能函数和组合子,在两个模块的交界处的代码就很容易产生不必要的胶水代码和引入难以分析的缺陷。同样,一个 C++ 项目既有 C 风格的函数尤其是对字符串和指针的用例,又有叠床架屋的模板代码设计出来的高度抽象的功能模块,这对跨越这两部分代码调试问题的开发者,就会引入极高的学习成本和理解成本。

哪怕是特性相对正交,几乎只有一条路实现特定功能的语言来说,这个问题也是存在的。例如,业内评价为暴力美学 Golang 即使代码写的差,也都是整整齐齐的“垃圾”。尽管如此,它还是面临错误处理风格没有语言级别约束的问题。前者虽然大部分 Golang 代码都是通过多返回值同时返回可空的错误和可空的结果来实现的,但是在 TiDB 代码里也有把 error 指针当成传出参数放到函数参数列表的 C 风格代码。类似问题放到 Java 代码里,虽然 Java 宣扬面向对象编程的范式,几乎凡事都可以通过抽象接口、不同实现,调用方只要调接口就行来建模,但是面向对象的几十个设计模式之间的取舍,也是风格不一致的诱发因素。

这是因为,无论语言是怎么设计的,不同的范式是客观存在的。对于熟悉 C 风格代码的开发者来说,他会在编写所有语言代码的时候都复用 C 风格代码的知识,在新的环境下找到熟悉的开发体验。这是难以避免的。对于 Code Review 来说,至少要保持模块内风格一致,模块间调用约定清晰,关系密切或说紧密耦合的模块采用相同的风格,而对于模块内部的实现,则不用锱铢必较。

统一的代码风格,最后是相同的代码格式和惯例。

例如,模块代入的是否有序、什么顺序,注释的风格,空格的风格,换行的风格。新生代的语言大多自带代码格式化工具,例如 Rust 的 rustfmt 和 Golang 的 gofmt 等,甚至 Golang 将某些传统代码格式的偏好固定成编译器检察项,例如 if 后左花括号换行就报错。对于早前的编程语言,包括 C 和 Java 等等,也都有业内共识度较高的工具,对应的是 clang-formatcheckstylespotless 等。

对于开源社群的协同开发而言,重要的不是选择什么风格,而是有一个确定的风格。当然,由于代码风格不适应严重的甚至会导致生理不适,一般而言所选的风格至少是核心开发者的共识。有了确定的风格,再将代码格式化作为提交前的必要检察项,这样就能减少由于不同人偏好的代码风格不同,导致提交上来的代码补丁总是包含形成干扰的格式变更带来的 Review 负担,以及格式化过程中难以控制的重构冲动导致引入新的缺陷。

最后这点,在 TiDB 开发者指南中强调为代码补丁需要专注于一件事:

Concentration. One pull request should only do one thing. No matter how small it is, the change does exactly one thing and gets it right. Don’t mix other changes into it.

显然,实现功能同时格式化代码,这就是两件事。

TiDB 开发者指南对代码风格还有另一段独立的说明:

Style. Code in the pull request should follow common programming style. For Go and Rust, there are built-in tools with the compiler toolchain. However, sometimes the existing code is inconsistent with the style guide, you should maintain consistency with the existing code or file a new issue to fix the existing code style first.

Flink 的代码风格指南则包括了上面提到的所有三个方面。

关键路径的性能因素

在知道算法的情况下,人是能像机器一样解决特定问题的,甚至依靠人的直觉和并行思考能力人的解决方案会更具现实意义。程序相较于人的突出优势,就在于处理重复的工作,尤其是不会出错的重复处理底层逻辑,并且这种处理速度远远超出人的极限。因此,程序的性能几乎是每个开发者心中的圣杯,每每会被提出来考量。

不过,Code Review 当中关注性能的部分,主要是关键路径上的性能因素。所谓关键路径,就是从输入到输出经过的延时最长的逻辑路径。

对于业务代码和实用工具来说,只要代码核心流程各个步骤不要出现时间复杂度的回退,尤其是不要出现与输入成指数级别时间复杂度的代码,基本上不会有太大的性能问题。

对于基础软件来说,处理的对象大多是更加底层的概念,例如单条数据记录或单个字节。在这些环节上就算出现常数级别的重复,在输入数据量的放大下,最终的时间和空间开销都有可能有显著的回退。例如许多 Java 网络系统都会做缓冲区零拷贝优化,就是避免语言运行时默认将网络传输过来的字节从网卡拷贝到用户空间再拷贝到 Java 堆上。从复杂度的大 O 记法来看,这只是常数级别的差异,但是在网络数据量级的放大下,这就是整个系统性能显著的回退了。

关于性能的 Code Review 工作,分析时间和空间占用的时候,不仅要看大 O 记法下的量级,还要考虑实际参数的取值范围和常数,两者综合的结果才是生产环境的实际性能表现。而在分析性能之前,要先判断相关代码改动是否是性能敏感的路径,或者补丁作者声称做出改动是为了改善性能。一个衡量性能优化的常用手段是提供基准测试(Benchmark)结果,这可以类比功能型修复对应的防止回退的测试。

各个开源社群的实践中,类似 TiDB 和 PostgreSQL 等以性能取胜的数据处理系统往往都会在 Code Review 指南中着重点出考虑性能方面的问题。而对于其他性能并非主要特性的项目,这类共识则一般隐含在 Reviewer 的共享知识当中,或者对于特定的几个关键路径会有注释或文档说明需要特别关注性能问题。

测试和文档

最后一个 Code Review 常见的内容是测试和文档。

虽然最后才提及测试和文档,但是它们在实现时却很可能在功能代码之前。上面提到的动机描述和设计文档,就可以算作是文档的一部分;而测试驱动开发的模式,测试是先于功能代码编写,随后实现功能或修复缺陷以通过测试。

测试和文档单独讨论都是一个内涵丰富的主题,在 Code Review 的语境下一并提出,是因为测试和文档的角度都是功能代码的用户。文档主要说明了代码实现了什么功能,调用约定和返回值内容都是什么,进一步的文档会提供代码使用的样例。测试则是功能代码的第一个消费者,Reviewer 和其他阅读源码的开发者都应该掌握阅读测试来理解一个功能模块或者整个系统的意图的能力。

TiDB 开发者指南提到 Code Review 中需要关注测试和文档的以下方面:

Tests. A pull request should be test covered, whether the tests are unit tests, integration tests, or end-to-end tests. Tests should be sufficient, correct and don’t slow down the CI pipeline largely.

Documentation. If a pull request changes how users build, test, interact with, or release code, you must check whether it also updates the related documentation such as READMEs and any generated reference docs. Similarly, if a pull request deletes or deprecates code, you must check whether or not the corresponding documentation should also be deleted.

值得一提的是,测试和文档并不是必要的,也不是越多越好。好的代码是明显没有错误的自解释的代码。只是把一眼看到函数名称,返回值和参数的类型和名称就能明白的内容写成文档其实是冗余的;测试显然正确的代码例如 Getter/Setter 也没有什么意义。

测试应该只检验模块的契约,也就是在不同类别的输入参数下,返回值和副作用是否符合预期。一个常见的测试误区是测试不属于当前模块的代码,尤其是测试外部依赖的逻辑。依赖模块的逻辑应该自己保证正确,下游只会在测试自身逻辑是发现上游不可靠,从而替换成新的实现或者向上游提交补丁,拉取新的版本。

Code Review 的暗礁

虽然大部分开源社群只有小部分成员才有向主干提交代码的权限,但是大部分开源社群都是鼓励所有社群成员参与 Code Review 的。从新人 Reviewer 成长为老到的 Reviewer 的过程中,有一些常见的 Review 技能以外的认识误区。本节从 Review 的几个常被忽略的真相出发,讨论如何规避 Code Review 的暗礁

Code Review 是一个交流的过程

Code Review 虽然有流程,但却不是无需人类活动参与的程序。软件工程没有银弹,同样也没有尽善尽美的代码补丁。程序设计几乎就是关于权衡(trade-off)的艺术,而 Code Review 就是 Patch Author 和 Reviewer 之间关于如何权衡的讨论。

不过,这种讨论又不是完全开放式的讨论。技术交流有一些行业或领域内的共识,正确性、性能报告和成规模的用户反馈实相对客观的。因此,Code Review 是一个技术事实和数据胜过主观感受和偏好的讨论过程。尽管 Reviewer 可能认为某个改动非常“脏”,但是在必要的性能权衡下,或者立即解决正确性问题的权衡下,没有更好的解法,也不应该出于个人主观判断否决提案。这是绝大多数社群都会要求的,给出 -1 的同时必须附带理由,否则 -1 无效。

此外,Code Review 的讨论是务实准确的。TiDB 开发者指南特别强调了这一点:

Asking questions instead of making statements. The wording of the review comments is very important. To provide review comments that are constructive rather than critical, you can try asking questions rather than making statements.

Provide additional details and context of your review process. Instead of simply “approving” the pull request. If your test the pull request, report the result and your test environment details. If you request changes, try to suggest how.

Python 开发者指南也提到,如果你在 Code Review 中检查了补丁确实具备什么功能,那么在 Approve 的时候也请带上相关信息,如果发现了问题,也尽量说明复现方式和环境。

其实这些原则贯穿开源社群的所有交流场景。把发现问题报告问题的原则放到已经合并的代码上,就成了 Issue Report 的原则;而把 Approve 的时候说明检查了什么内容,就变成了 Release Verification 的一个重要步骤。

最后,Code Review 通过交流传递知识。无论是补丁作者还是 Reviewer 提供了好的代码实例,还是交流过程中学习到了其他人分享的知识,都不要吝惜称赞。这种正反馈循环是开源协同长期运转的重要支柱。

开源社群的 Reviewer 都是志愿者

当然,Reviewer 有可能是因为受雇于某家公司才参与社群帮助 Review 的。但是这不妨碍从社群视角来看,开源社群的 Reviewer 都是志愿者。毕竟,如果你跟某个特定的 Reviewer 不在同一家公司,他对你而言是不是一个十足的志愿者呢?

Python 开发者指南中关于 Reviewing 的第一段话就是:

To begin with, please be patient! There are many more people submitting pull requests than there are people capable of reviewing your pull request. Getting your pull request reviewed requires a reviewer to have the spare time and motivation to look at your pull request (we cannot force anyone to review pull requests and no one is employed to look at pull requests).

所以,开源协同当中的 Code Review 以天或周为单位沟通合并是常有的事。为了提高自己在开源社群当中的效率,你不能死等在一个 Code Review 的反馈上,而应该尝试同时进行多个工作,哪一个给出反馈就调度上来再给一个回复。也就是说,开发者在开源协同当中像是一个并发的处理器。

从 Reviewer 的角度来看,你不是社群的雇员,更不是社群的奴隶。为了保持长久的参与热情和个人精神健康,认识到你是社群当中的一个志愿者至关重要:你不欠社群什么,社群也不欠你什么。当然,为了社群茁壮成长,掌握上面所有 Code Review 的方法,高效的完成 Code Review 也是社群生产力规模化的核心源动力。

Approve 意味着同意合并代码补丁

对于一个增长的社群来说,Code Review 是严格把关代码质量的一个重要环节。同时,通过 Code Review 向社群新成员传递的正确理念,将会极大提升他们后续参与的积极性和质量。

然而,如果 Code Review 标准太低,甚至出现为了某些 KPI 而选择先合并低质量代码再修复的策略,不仅损害了当下的代码质量,也会传递出一种类似破窗效应的信号,让其他社群成员误以为这个社群对待软件生产的标准就到这了。

Rust 标准库的开发者文档一开始就强调了 Approve 的意义和严肃性:

You are always welcome to review any PR, regardless of who it is assigned to. However, do not approve PRs unless:

  • You are confident that nobody else wants to review it first. If you think someone else on the team would be a better person to review it, feel free to reassign it to them.
  • You are confident in that part of the code.
  • You are confident it will not cause any breakage or regress performance.
  • It does not change the public API, including any stable promises we make in documentation, unless there’s a finished FCP for the change.

这其中,由于 Maintainer 有提交代码到主干的权限,他们的 Approve 会更加关键。实际上,这是一个选择 Maintainer 的标准。以 Apache 的权限模型为例:

For the committer bar, I always think of whether the candidate is easy to work with - make decisions with caution while bravely, knowing when to ask for help.

For too many new committers, it hurts when their contributions always need revision, especially trivial mistakes. If we elect a new committer while his/her contribution needs more attention to avoid merging wrongly quickly, we lose the reason to invite the very person.

Apache doesn’t set up fine-grained permissions so it’s extremely important not to approve something you’re unsure with.

当然,随着项目固有复杂性的增长,很可能任何单独一个 Reviewer 都不敢确定是否应该合并一个相对复杂的补丁。

面对这种情况,如果补丁是由多个连续的步骤,或者相互独立的几个部分组成的,可以让补丁作者拆分成多个提交。

如果确实不好拆分,则可以由多名 Reviewer 各自 Review 一部分,然后由经验丰富的一位整合 Review 意见。Flink 的 Review 指南的第三点提到:

Does the Contribution Need Attention from some Specific Committers and Is There Time Commitment from These Committers? Some changes require attention and approval from specific committers. For example, changes in parts that are either very performance sensitive, or have a critical impact on distributed coordination and fault tolerance need input by a committer that is deeply familiar with the component.

侧面来看,这也强调了 Approve 的时候带上自己检验过的内容,明确自己 Approve 的是哪一部分的意义。

上游优先的故事

开源软件的用户在使用过程中遇到问题时,几乎总是先在自己的环境上打补丁绕过或快速修复问题。开源协同的语境下,开源软件以及维护开源软件的社群统称为该软件的上游,用户依赖上游软件的应用或基于上游软件复刻(fork)的版本统称为下游。上游优先(Upstream First),指的就是用户将下游发现的问题、做出的修改反馈到上游社群的策略。

网络上已经有不少文章讨论上游优先的定义、意义和通用的做法。例如,小马哥为极狐 GitLab 撰写了《Upstream First: 参与贡献开源项目的正确方式》。不过,这些文章往往是站在社群、平台或布道师的层面做笼统的介绍。本文希望从一个开发者的角度出发,由几个具体的上游优先的故事,讨论开发者角度实践上游优先策略的动机和方法。

故事和经历

Spotless

第一个要讲的是我在 Spotless 社群的参与经历。Spotless 是一个主要关注在 JVM 系语言的代码自动格式化软件,被 Apache Flink 等项目广泛采用。

我第一次给 Spotless 提交补丁是因为在一个个人项目当中同时使用 Java 17 和 Spotless 插件,遇到了 Java 17 严格约束 JDK 接口导致的 Spotless 插件启用 google-java-format 规则时不工作的情况。

起初,我按照 issue-834 上的绕过方法解决了问题。但是一方面绕过方法不太舒服,需要用户感知和主动修改;另一方面,这个解法不向后兼容:如果一个项目想要同时支持 Java 8 和 Java 17 编译,该绕过方法会导致 Java 8 编译失败。

我遇到这个问题的时候,这个问题已经发生一年了。而且这种 Java 上游改内部接口导致下游爆炸的情况,一般很容易导致其他项目出问题。于是我尝试搜索相关问题,幸运地发现了 Kotlin 提供了解决问题根源的方法:

依样画葫芦就在 Spotless 上游把问题解了。

听起来简单,其实搜索到的代码片段只是提示了核心解法,减少了思考可行方案的时间。要把相关代码移植到另一个项目里,并且理解原项目的构建方式打包后测试,以及按照项目的惯例完成文档更新等等工作,最终让上游接受合并请求,就需要积极和上游维护者沟通,也需要发现和理解上游社群运行规则的方法和耐心。

好在 Spotless 的维护者 Ned Twigg 相当外向,很快进行代码评审并解释了代码以外需要做的工作,一周以内就完成了补丁合并。通过自动化发布的流程,补丁合并以后新版本就会自动发布,下游几乎能在合并当天就更新版本用上自己提交的补丁。

这里有个小插曲。虽然这个修复乍一看也可以在 google-java-format 上游做,但是我经历了 GCP 相关的一个 pull request 被挂数月的折磨以后,对 Google 项目实在没什么期待,所以选择在 Spotless 解决问题。事后发现这个解法是正确的,因为这样解能搞定 Spotless + google-java-format 1.7 的组合,而在 google-java-format 修大概率是不会为早期版本 pick 的。此外,Spotless 上的修复是模块化的,起初只是在 google-java-format 的路径上启用,随后发现了其他格式化规则也有类似的问题,只要简单地把修复模块在其他规则上启用就可以了。

一周以内上游合并发布以后,我在下游使用新版本的 Spotless 解决了之前的问题。然后,趁着热乎劲,我把之前搜索解法的时候搜到的其他有同样问题的项目,看着顺眼的就把我的修复版本提交给他们。今天我再去看原来的 issue 和提交的补丁,能够看到一系列下游项目引用我的工作修复他们的问题,我很开心。

然后,就在国庆假期前给 Flink 更新 Spotless 版本解决这个问题的时候,我又发现了另外一个问题,原先可以用过 skip 参数跳过某些 Maven 模块 Spotless 检查的功能在升级以后不管用了。

我的第一反应就是到上游搜索相似问题,发现 issue-1227 也报告了这个问题。由于升级前的版本是好的,升级后的版本出了问题,加上我能定位到好的版本里 skip 参数起作用的相关代码是哪些,很快我就二分找到了引入问题的提交,并且在一个小时之内提交了修复。

这次轻车熟路,所有该办的我能办的事情,我都一次性搞完了。Ned Twigg 的反应还是很迅速,半小时内就反馈了 review 意见,我再求助其他开发者进行验证以后,当天 Ned Twigg 就合并了补丁并且自动发布了新版本。我接着在 Flink 提交的补丁上更新了依赖版本,第二天经过 review 以后 Flink 的补丁也合并了。而我本来还以为要到假期后才能搞定。

atomic

这个例子是我在 Golang 生态的参与。不同于上一个例子,我在 contribute back 相关改动的时候并没有急切的下游需求,只是我在使用的过程里发现上游有可以做得更好的地方,于是就顺手实现了。首先讲这个例子是因为 uber-go/atomic 的参与经历给了我一个催发布的定型文。

我应该是在迁移 TiDB 测试的时候,发现了部分代码使用了 Golang 早期只有操作指针的函数的 atomic 库。这种代码要求操作对应的变量必须都用 atomic 库提供的方法,一旦直接访问就有破坏一致性的风险。在其他语言的实现里,往往都会有原子类型,例如 Java 里的 AtomicIntegerAtomicReference 类,来保证所有操作都是在原子类型的方法上的,也就避免了应用逻辑上是 atomic int64 但是只能定义成 int64 的问题。

我从其他开发者那里得知了 Uber 的 atomic 库提供了原子类型定义,马上就愉快的用上了。在使用的过程里,虽然没有实际的需求,但是从完整性上我发现 Uber 的 atomic 库没有定义 uintptrunsafe.Pointer 对应的原子类型,而 Golang 的 atomic 库函数里有操作相应类型的函数。因为闲暇时候我也是写点代码打发时间,所以我先提交 issue 询问维护者添加这两个原子类型是否合适,得到肯定的答案之后就顺手实现了一个补丁提交。

因为改动很简单,所以几轮 review 过后 24 小时内就合并了。

不过,不像 Spotless 通过自动化发布流水线,合并补丁之后基本一天内就可以从 Maven 中央资源库引用新版本,大部分的项目包括 Uber 的 atomic 库都需要人来触发或完成发布流程的。非自动化的发布的节奏,Apache Pulsar 和 Apache Flink 这样的项目会有一个相对稳定的发布周期,并且社群成员能够从文档上看到发布的时间规则,因此我也能知道大致什么时间会发布新版本,而且急也没用。但是有些项目开发活动并不活跃,它们会倾向于开发一段时间后“差不多了就发布”。

虽然 go.mod 其实允许直接引用一个 commit 标识的版本,但是我出于软件工程的最佳实践,我当时认为只有已经正式发布的版本里包括了我的更改,这个 contribution 才算正式完成。于是我用下面这段话向维护者请求发布一个包含我的改动的新版本:

@abhinav is there a release cycle or trigger description. I may hurry a bit but it is the nature a developer want to know how and when the work released :P

维护者表示他们确实是“差不多了就发布”的风格,但是很乐意在当周就发布一个包含我的改动的新版本。实际上,一天以后就发布了 v1.8.0 版本。

这段经历不仅带给我一个日后重复使用的催发布定型文,也是在偶然之后提醒我可以更加积极地与维护者沟通:如果你有什么需求,为什么要假设其他人不接受,而不是试着问一下呢?

一个有趣的后续是,目前 Golang 最新的 1.19 版本包含了从 Uber 的 atomic 库“借鉴”的原子类型。虽然我很好奇他们为什么只选取了一部分类型,比如没有包括 float 和 duration 等类型,但是我发现我写的 Uintptr 类型也被包含其中。尽管 Golang 的作者并没有在代码里说明这段代码是来自 Uber 的 atomic 库的,或许是因为他们觉得这是平凡的实现,在具体的方法集合上也有裁剪,但是 Golang 的开发者都知道是怎么回事(笑)。

Maven Shade Plugin

这个案例的起因是我在 Pulsar 社群恰好和其他几个开发者同时发现 Pulsar 的 pom.xml 配置触发了 Maven Shade Plugin 的一个缺陷。

一般来说,Java 开发者对于应用代码和库函数是比较熟悉的,而一旦涉及到构建系统比如 Maven 或 Gradle 等,则会天然地产生一种陌生的刻板印象。这对于其他语言生态也是类似的,C++ 开发者哪怕能够写出很复杂的模板代码,也很有可能在调试 CMake 的配置的时候抓瞎。因此,除非专门的构建系统开发者,其他开发者几乎总是优先考虑绕过问题,而不是怀疑构建系统本身有缺陷,或者把构建系统的缺陷都当成需要理解和共处的特性。

我也不例外。不过我折腾构建系统的时间还算有一些,知道 Maven 增量构建问题一堆,所以马上发现了执行 mvn clean 清理构建产物以后再跑构建流程(“重启一下试试”)就可以绕过问题。

然而,一周以后,这个 issue 还是开着,不像有上面的绕过方法就算了的样子。我一时强迫症上来了,就开始搜索同类问题。很快,我就发现 Hadoop 和 Elastic 都有人遇到过类似的问题:

不过,他们的解决方法都不是我想要的。Hadoop 的开发者发现先 clean 就行以后就心满意足的关掉了问题。Elastic 把会导致问题的依赖给重构掉了来绕过这个问题,而 Pulsar 的情形里这个依赖是无法规避的。

几乎确定是上游的问题,我在 Maven Shade Plugin 的问题列表上提交了一份报告。没错,不像之前几个例子马上开始尝试实现,出于上面提到的开发人员的惯性,我还是下意识地规避构建系统的问题,只是提交一个 issue 并期望上游维护者能够帮我解决。

不过,提交问题后不久,机缘巧合之下我开始替换 Pulsar Docker Image 构建的 Maven 插件以支持在 Apple M1 平台上运行:这或许归功于公司给我配备的新机器。这个过程里,我理解了数个构建容器镜像的 Maven 插件具体执行的逻辑,以及为什么在 Apple M1 上会报出相应的错误。这让我对以前认为看也看不懂并敬而远之的 Maven Plugin 生态有了新的看法:好像也不怎么难嘛。

顺理成章地,半个月后我看到上面 Shade 插件的问题还是杳无音讯,就决定动手调试解决了。

这一次,不像 Spotless 的经历那样有现成可以借鉴的代码,需要我自己定位问题。不过从调试 Maven 插件的经历里,我大致知道了 Mojo 抽象的基本概念和执行路径。从报错信息里定位到相关类以后,我在代码中间加入了一系列日志来打印中间变量。在不好使用 debugger 来劫持执行流程的环境里,直接用 print 输出变量值是最值得依赖的手段。

因为问题的表征是创建 Zip 文件的时候有重复的被压缩的 Service 文件,我重点打印了 Shade 插件里合并 Service 文件时候的文件名,立刻发现被 relocate 的文件没有正确合并,而是重复处理了两次。顺着这个事实回过去看 Shade 插件的代码,很容易发现一个基本的逻辑错误。一开始,Shade 插件没有处理 relocate 规则。后来改了两次,但都没改完全。

虽然报告问题以后半个月上游没有处理,但是我定位了问题,明确分析出原因和提供了易懂的解法以后,加上从 commit 历史逮捕最近比较活跃的 maintainer at 上,第二天就有两位 reviewer 参与 review 并且最终合并了我的补丁。这改变了我打破了 Maven 社群的刻板印象。

合并以后,这次确实有下游 Pulsar 的用例等着升级版本来修复问题,因为我自己没有权限发布新版本,所以我再次使用上面提到的定型文催促发布:

@rmannibucau @slawekjaranowski I may be a bit in a hurry but I’d like to know whether/when we can have a release for this fix. It resolves one or several downstream use cases and I’m happy to upgrade for this fix.

It’s not a request, though.

两位 reviewer 告诉我可以到邮件列表上寻求帮助。我就订阅了 dev@maven.apache.org 邮件列表,直接请问有没有维护者愿意帮我这个忙。

没想到 Maven 的 PMC Chair Karl Heinz Marbaise 马上回复可以在周末的时候发起新版本发布的投票,最终也确实在当周就发布完成,我也在下游升级到新版本解决了问题。

这段经历给我的启示是,上游优先可以是无处不在的。Hadoop 和 Elastic 社群里报告问题的人没有想过相对陌生的构建系统也可以接受补丁修复问题,而是习惯性把它当做一个外部的依赖,一个自己无法干涉的依赖。但是,或许我们还有更好的方式来解决自己的问题。如果一个问题技术上应该在上游解决,为什么不试着就在上游解决呢?

Protobuf

虽然我给 GCP SDK 的 pull request 被挂了几个月,直到现在也还没人搭理,但是参与 Google 的另一个开源项目 Protobuf 的体验还是不错的。

我在瞎鼓捣 Pulsar Ruby Client 的时候,碰到了 Pulsar 的 Proto 文件定义的枚举类型内部字段是小写字母开头,而由于 Ruby 没有枚举类型,Protobuf 把枚举类型的字段映射成 Ruby 里的常量,Ruby 的常量又必须是大写字母开头,最终导致定义失败的问题。

虽然这个问题看起来前提条件很复杂,但实际上是一个 Ruby 开发者和 Proto 定义的消息交互时非常容易遇到的情形。上游在 2016 年就有相关报告:

逻辑上的解法其实很简单,在定义枚举字段映射到 Ruby 的常量的时候,自动把字段名首字母大写就行了。这样既不会影响现有代码,又能够解决原来常量定义失败的故障。虽然对字段名做了自动调整,但是原本小写字母开头的常量定义是失败的,根本也用不了,而实际到二进制转换不看名字只看编号,到文本的转换走的是符号解析支持小写字母开头。

经过这轮分析以后,我确定这个路径是可以走通的。于是从报错信息定位到相关代码,把“自动大写字段名首字母”的逻辑原地打了个补丁上去。当时我也不懂怎么触发测试,也不知道会不会有其他问题,但是先做自己能做的事情,提交到上游让其他干系人发现有人在努力解决这个问题,并且已经有一些进度了,这能够在原本大家都观望的环境里抛出一个凝结核,吸引用户测试补丁和维护者评审代码。

不同于 GCP SDK 的源码只读状况,Protobuf 的维护者隔天就帮我触发了测试,这让我感觉到这个社群还是会关注我的工作的。一周以后,Ruby 模块的维护者之一 Jason Lunn 开始 review 我的代码,由此开始了近一个月的 review 循环。

中间过程我就不再赘述,如果你去看我提交的补丁的对话,你就会发现:

  1. 因为虽然我对这个改动有需求,但是不是特别着急,所以对话经常是以周为单位。每周末我闲着没事的时候,有时就能想起来还有这件事没搞完,于是看一下 review 意见和测试结果还有哪些要改的,集中思考和解决一波。
  2. 因为我对 Ruby 并不熟悉,而且一上来搞的就是 Ruby + C 和 JRuby 的元编程,所以这个过程里我其实不是一开始就知道符号的部分不用动,写出了一堆问题。解决问题的方向错了,reviewer 好像也没看出来,大部分时间都是我自己在纠结、测试和补丁之上的补丁,碎碎念的状态活像一个孤独患者自我拉扯。
  3. 因为 contribute code 最好还是本地可以跑全量测试提升反馈效率,所以整个过程下来我把 bazel 这套构建尤其用于 Ruby 项目编译的各种 trouble shooting 都搞了个遍。以前我总觉得 bazel 的概念晦涩难懂,但是实际直接用起来一个配置好的项目,不仅体验不错,还帮我理解了很多设计的原因。
  4. 最后,虽然我在错误的道路上走了太远,甚至一度以为这件事情没法实现,不过就像我上面对问题的总结,我回归到一开始要解决的问题,加上一个月来对这段代码的深入理解,终于发现了正确的解法,最终用不到 50 行代码就把问题给解决了。

代码合并以后,我自然是再次用定型文催上游发布我好早点用上。不过这次上游没有给我反馈,于是我主动观察了一下发布的规律,发现 21.x 的版本每半个月到一个月就会发布新版本,然而由于我的补丁只在 master 分支上,只有等到 22.0 发布的时候才能用上。我觉得这个改动不大,所以就询问维护者能不能 backport 到 21.x 的分支上赶上下一个短周期的发布。

另一个维护者 Mike Kruskal 支持这个做法,并且跟我确认了 21.x 和 22.0 的发布节奏。我得到支持以后就把这个不到 50 行的补丁轻松地 backport 到了 21.x 版本,并在三天前得到合并。期待发布中。

这个故事可以拓展成一个典型的上游优先模式。许多程序员在面对自己的问题的时候,一开始做出的改动就跟我原地打一个 monkey patch 一样,对自己的用例有效,其他自己用不到的地方就不管了。但是把自己的修改提交到上游接受评审的时候,才发现原来这个改动可能牵扯到这个那个模块。上游同时被许多下游依赖着,因此它们所选择的解决方案很可能不是 monkey patch 的方式。通过这样的上游优先参与,能够逐渐锻炼自己下游使用修改时候符合上游的设计哲学,从而尽力避免由于理念不同而最终不得不分支的情况。

当然,这个例子可能稍显简单了。一个年代比较久远的例子是 2019 年前后我在 Flink 社群参与发起和实现的 FLIP-73FLIP-74FLIP-85 这三个提案。我在腾讯内部其实做了不一样的实现,在上游社群和其他 committer 沟通以后形成了最终上游的解决方案。不过关键的思路是一样的,所以内部版本追上上游也不困难,不会因为有截然不同的假设导致被存量拖死。

另外,Protobuf 的问题是 2016 年提出的,今年我解了,参考某司解决一个 etcd 悬挂三年的边缘问题吹上天,我是不是可以标题党地写一个《震惊!他竟然解决了 Protobuf 一个长达六年的痛点!》。

Apache Ratis

Ratis 是一个 Raft 算法的 Java 实现,完成了 Raft 共识算法的核心逻辑,实现了一个服务框架,包括网络层,日志同步和落盘的功能,状态机及其快照的抽象,还支持运行时增减成员、动态配置和同时运行多个 Raft Group 等高级功能。

我对分布式共识算法的关注由来已久,可能跟我第一个稍有难度的工作就是改良 Flink 基于 ZooKeeper 的高可用模块有关。Ratis 作为共识算法 Raft 的实现,自然也进入我的视野。

真正开始探索 Ratis 的实现,起源于我从分布式计算做到分布式存储以后,了解到 Spanner、TiKV 和 Oceanbase 等等系统都是基于共识算法来实现数据的复制和一致性的。在理解相关的代码和论文之余,我也想要自己写一个个人项目来实践自己的理解。Ratis 自己曾经想过做一个 Replicated Map 实现,类似于 ZooKeeper 或 etcd 来解决大部分用户的简单读写场景,但是最终没做成。我顺着这个思路,时不时做一些实验和源码阅读,并在今年以 zeronos 为项目名开始做一个完整的实现。

在这个过程里,我发现过 RATIS-1619 Group 创建时约束的问题,顺手就解了。这跟前面给 atomic 提补丁没什么区别,不做展开。

重点要说的是下面这个例子。我在琢磨怎么实现类似 etcd 的 watch 功能的时候,发现 Ratis 在框架层面实现了网络通信,虽然方便了下游只需要定义状态机就可以起集群,但是网络通信只实现了简单的 request-response 模式,不能照搬 etcd 的全双工长连接实现。

在之前跟 Ratis 的作者施子和博士的沟通过程里,我发现主要能找到他的渠道在邮件列表上,于是我就把自己的需求在 user@ratis.apache.org 上反馈。

果然,施子和博士在一小时内就回复了我的问题。经过几轮沟通,我们得出结论:watch 和 put 主要的区别在于乱序返回,也就是一个 watch 请求到来之后,必须先处理完后续的写请求才能出发 watcher 并返回给客户端。而 Ratis 同一个客户端的请求默认是排序的。解决了这个问题,只要在请求和返回的时候带上键值状态机里 key (range) 的 revision 就能保证获取变更信息不会错过,至于是不是采用全双工链接来实现,反而不是特别重要。

得出结论以后,我又开始 push 上游推进。当然,开源社群的参与者都是志愿者,没有催促别人做事情的道理。但是我可以做我力所能及的事情,激励其他参与者发现这件事情有人在关注从而提升优先级。所以,在两天之后没有后续的情况下,我就把 user 邮件列表上的结论总结成一个技术上的问题报告,提交到 Ratis 的 JIRA 项目上。

几分钟后,施子和博士在 issue 上问我是否已经开始实现了,我只能坦诚地说没有。于是他表示他可以实现,并且确实在三天之后就提交了补丁。经过几轮 review 以后,我 approve 了相关变更。

然后,就是定番询问发布的计划。因为我知道 master 分支上要等 3.0 版本发布才能用上,而 3.0 发布还没确切的日子,相对而言这个改动并不大,包含在 2.x 版本按照以往的节奏一两个月以内就可以发布了。施子和博士也同意了这个说法。

这个案例是一个代码以外的参与案例。本文开篇就提到,上游优先包括提交代码,也包括报告问题。实际上,将自己创作的软件开源发布,对于软件工程师来说很关键的一个好处就是收获同行评审和用户反馈。相反,仅作公司内部使用的软件往往少有人关注代码的技艺,只关心最终效果,并且通常使用场景有限,很难得到解决复杂问题的锻炼机会。

从参与者的角度看,这个案例体现出来的仍然是积极和上游沟通,尤其是做一些力所能及的工作。开源社群的成员都是志愿者,他们不会因为某个需求是你提出的就另眼相待。他们几乎总是从完成需求的难度和回报来衡量自己是否应该投入时间。做一些力所能及的工作,哪怕是前期调研和技术分析,一方面能降低解决问题的难度,另一方面让其他成员看到你为这个问题付出的努力,你有相当的主动性,解决这个问题能帮助到你。谁又不想和这样的同伴合作呢?

match-template

站在维护者的角度,我也处理过其他社群成员出于上游优先理念提交的请求。

TiKV 的 maintainer @andylokandy 参与到另一个 Rust 数据系统项目 Databend 的开发以后,在实际编码的过程里发现自己需要和 TiKV 里内部的模块 match-template 相似的功能。

虽然 match-template 模块的代码不过几百行,不过 Andy 没有想着简单拷贝代码,而是希望 TiKV 社群能够把这个模块 promote 成一个顶级项目,并且在 Rust 的中央资源库 crates.io 上发布。

这个提案得到了其他 maintainer 的支持。作为 TiKV Infra Team 的在编人员,我自然很乐意帮忙完成相关设定和发布的工作。在一个简单的社群投票之后,我花了一个小时左右的时间把新仓库创建、match-template 库发布和相关权限设置的工作都做好了。隔天,Andy 就在 Databend 的 PR-6712 里用上了这个库。

Andy 其实比我还要晚毕业一年,不过我们都是从刚一毕业甚至还在学校的时候就开始参与开源社群的。开源社群广泛合作的理念根植在这样环境里成长起来的新一代开发者,对于他们(我们)来说,上游优先是再正常不过的事情了。

Pulsar Flink Connector

这个例子里,我同时拥有公司员工和上游维护者两顶帽子

一方面,作为 Flink Committer 的我是 Flink 社群的维护者,拥有合并补丁的权限。另一方面,作为公司员工的我因为 Flink 的经历自然地想要帮助公司业务依赖的 Pulsar Flink Connector 和上游更好的协作。

在我和公司同事 Yufan 的合作下,我们把 Pulsar Connector 的端到端测试覆盖、一系列缺陷修复和新功能实现推到了上游。由于公司的客户和 Pulsar 软件的用户不都是使用最新版本,我们还把缺陷修复和端到端测试的部分推到了上游维护的其他过往版本。

这可能是公司员工角色的开发者一个典型的特征。作为个人开发者,往往没有太多版本依赖包袱,只需要最新版本里有自己提交的补丁,追到最新版本就行。但是在公司里许多历史问题累积的应用系统的环境中,激进地升级版本可能会带来其他问题。因此他们做上游优先的反馈的时候,可能会更加关注修复能否被 backport 到自己使用的版本上。

通过与上游的紧密合作,Pulsar Flink Connector 成为了 Flink 官方维护和发布的 Connector 的一部分,让 Flink PMC 为这一软件背书,从而使得用户采用的倾向性更强。同时,代码进入上游跟随上游迭代,添加的相关测试保证 Connector 功能的测试被上游回归测试所包含。如果上游在核心模块做了影响 Connector 的改动,可以提醒开发者相应调整关联代码,或者至少维护这个 Connector 的 Yufan 能够知悉。

这也是我时隔一年以后重新参与 Flink 开发活动,并间接导致了阅读代码的过程中发现和解决了上面提到的 Spotless 的问题。可以看到,存在一定用户基数的开源软件的生命周期相对都比较长,作为上游维护者,有可能因为公司上游优先的活动而重新回到社群。

这里值得一提的另一个例子是 Gyula Fora 和 Márton Balassi 发起的 Flink Kubernetes Operator 项目。

Gyula Fora 和 Márton Balassi 是 Flink Streaming API 的核心作者,在 2014 年向 Flink 提交了几乎改变项目性质的重要改动并成为 PMC 成员之后,他们从 2016 年起几乎就从上游社群消失了。

今年一月份,这两位开发者一起加入苹果公司并开始用 Flink 搭建数据流水线,在生产部署的场景里遇到了需要 Flink Kubernetes Operator 的需求。由于上游没有提供相应的软件,加上苹果公司的数据流水线只是成本的一部分,并不需要依靠 Flink Kubernetes Operator 来提供商业竞争力,他们于是在公司内部实现了初版以后,就在上游社群提交议案发起新项目。

目前,Flink Kubernetes Operator 已经发布了 1.2.0 版本,并且持续快速迭代中。这个软件现在不止是苹果自己在用,蚂蚁集团和阿里巴巴集团也开始关注和整合这部分代码,回推公司内部的实现,避免内部实现和上游软件产生方向上分歧。

go-redis

最后一个正面的上游优先的故事,我想讲一下 Apache Kvrocks (Incubating)go-redis 相互的合作。

上游优先,进一步分析,并不只是 fork 到 upstream 的方向,也不总是从依赖的软件到被依赖的软件的方向;上游优先换个角度看,可以说是一个包含软件 A 和 B 的整体解决方案,自己作为软件 A 的开发者,在发现解决方案的部分问题更适合在软件 B 解决的时候,首先选择在软件 B 的范畴内解决,而不是在自己的“领地”里费尽心思的绕过。

Kvrocks 是一个兼容 Redis 协议的分布式 NoSQL 系统,go-redis 是实现 Redis 协议的 Golang 客户端。我在选型替换 Kvrocks 社群里无人熟悉的 TCL 测试的时候,最终选择基于 go-redis 来写集成测试。

当然,这里选型的主要是 Golang 语言 Kvrocks 的开发者都比较熟悉,而且写起测试这样非关键路径的代码简单粗暴,而 go-redis 是 Redis Golang 客户端几乎最好的实现。不过,有一件小事让我对选用这个软件更有信心。

偶然之中,我发现 go-redis 其实很早就在 README 里提及了自己支持访问 Kvrocks 服务端。由于 Kvrocks 今年四月份捐赠到 Apache 孵化器,我顺手改正了上游 README 的表述和链接。go-redis 维护者 @vmihailenco 不仅很快合并了修改,还对 Kvrocks 捐赠这件事表达了祝贺。

当时我就觉得,我很愿意和这个人合作。因此我在下面留言说明我将要做的替换工作,而他也很热心地表示如果出现什么问题,欢迎和他反馈。

一开始的迁移很顺利,我就夸赞了 go-redis 真不错。随后一些复杂测试用例的迁移过程里,我逐渐发现了 go-redis 的一些问题,其中有部分是 Kvrocks 的实现和 Redis 不一致需要改进。每次遇到问题,我基本都会 @vmihailenco 告知他我们用例测出来的问题,并且向他寻求 go-redis 使用上的帮助。

迁移过程中,我还发现 go-redis 没有实现部分 Redis 命令的封装接口。跟 @vmihailenco 确认这只是工程工作量太大没有全部实现,其实是需要的之后,我随手就提交了一个补丁帮助 go-redis 实现了相关接口。

因为 Kvrocks 可以使用底层接口完成测试,而且我知道上游会在 v9 版本包括这个改动,也就是说发布是确定的,而我不着急使用,所以这一次我没有用定型文催促上游明确发布时间。

这种上游优先是相互的。显然我为 go-redis 提交了一些代码,并且向上游反馈了不少使用案例,不过 @vmihailenco 也为 Kvrocks 和 Redis 的兼容性提供了非常有价值的输入,并且帮助 review 替换过程中遇到的问题。

应该说,这是开源社群当中的一种常态。大家不是完全基于契约和合同行动,不在契约之内的事情就想方设法避开,而是基于善良和合作的假设,帮助别人,并时不时得到别人的帮助。上下游的合作是相互的,作为积极反馈上游社群的下游,往往也更能得到上游社群反过来的帮助。

Trino

第一个印象深刻的遭受挫折的故事,来自给 Trino 提交的一个补丁。

这个补丁来自于 Pulsar 实现 Pulsar Trino Plugin 的过程中发现上游核心模块里的 AvroColumnDecoder 类功能可以增强。其实在此之前,Pulsar 社群就想过把整个 Pulsar Trino Plugin 捐赠给 Trino 上游维护。

但是 Trino 作为一个公司项目,对待他们眼里的 “external contributor” 的要求不可谓不高。如果一视同仁,我还可以理解成项目一贯的高要求。然而就像我在 PR-13070 里回复的,分明 Starburst 公司自己的员工,就可以分批交付功能,发布在不同版本上,怎么到了 “external contributor” 身上,就要我啥都调研一下,啥都尝试着做呢?

更不用说每次我有什么回复,上游没有个一二十天是不会回复的,甚至我如果不到 Slack 频道上 pop 这个合并请求,根本不会有人再看。当然,我个人其实是支持如果你的补丁被上游忽略了,应该尽可能的增大声量,哪怕上游拒绝合并你的修改,也是一个结果。但是重复做这样的事情,只会让我觉得像是一个垃圾信息制造者。

这就引出来一个要点,上游有可能出于种种原因没能和你找到一个最大公因数完成上游优先的代码反馈,这种情况并不少见,作为下游项目,此时也要有自己的解决方案。

比如,虽然我们说尽可能在上游解决,但是上游已经不维护了,那也只能绕过或者替换了。前面故事里客串出场的 Docker Maven Plugin 就是我替换已经不维护了的 Dockerfile Maven Plugin 的选择。尽管我知道可以怎么修,但是上游也不想修了。

另一个手段是分支。对于上面 Dockerfile Maven Plugin 的例子来说,我也想过分支维护的思路,但是它其实又依赖了同一个组织下另一个已经不维护了的核心库,这一下维护成本暴涨,我也就没了兴趣。但是下游分支的情况并不少见。比如 Google 的 cpplint 几乎不维护,就有人拉出一个 cpplint 组织来重新维护这个项目。

回到 Pulsar Trino Plugin 的例子上来,如果上游不接受我回推到核心模块的补丁,大不了我就像现在的实现一样在 Plugin 里把我需要的修改后的类单独写出来用就是了。虽然这样有不必要的代码重复,但是至少是个方法。进一步的,如果上游不接受 Pulsar Trino Plugin 捐赠,Pulsar 社群也可以自己维护。只不过确实 Trino 的后向兼容性极差,如果不能和上游同步迭代发版的话,外部的插件很可能很难和上游 Trino 服务端保持兼容。从用户层面看,就是同时使用 Pulsar 和 Trino 很难找到一个所有需要的功能都有,而且两者还能融洽相处的版本向量。

goleak

虽然 Trino 的情况让实践上游优先策略难度陡增,但是上游优先的失败案例并不总是上游的问题。

最典型的情况是,如果你对项目的理解和项目维护者不一致,或者说和项目的定位和要解决的问题方向不同,那么上游优先的反馈就很可能被拒绝。

我在迁移 TiDB 测试的时候,部分工作是使用 goleak 库来替换手写的 leaktest 工具。这主要是因为手写的工具功能有限,甚至后来发现与漏报的情况,而由于没人维护这个手写的工具,这些问题长期都无人解决。

替换成 goleak 的过程里,我发现了一个重复的模式。由于 goleak 的接口设计,在调用检查方法之后,如果失败就调用 os.Exit 直接退出进程,这导致任何 teardown 的逻辑都没有办法执行。面对这个问题,我设计了一个 goleak.TestingM 的子类型和一组接口来支持退出逻辑。

这个包装类型用的还不少,正好 goleak 也是 Uber 开源的 Golang 库,我前不久在 atomic 库有不错的合作体验,于是就提议直接在上游提供这样定制退出逻辑的功能。

虽然上游维护者对实现方式有自己的想法,但是我觉得我的实现方式更合适一些。于是我直接怼了一个补丁上去:

然后就是一年杳无音讯。直到上个月一个新加入 Uber 不久的员工按照上游原来的想法实现了这个能力,并把 issue 关联关闭了。虽然我觉得他的实现其实跟我当时指出的问题一样,不能很好地在编译时就阻止错误用法,而为了追求接口的“一致性”,有可能在运行时才抛出异常,但是既然这是上游的决定,这又是个公司项目,我也没什么好说的。

当然,哪怕不是公司项目,只要你不能做约束性投票,或者说你的理念不被项目核心维护者认同,那么下游更改无法推回上游也是很常见的。Apache SkyWalking 的作者吴晟就多次拒绝方向上不符合自己理念的反馈,或者确认问题,但是按照自己的理解来实现。

我自己作为维护者的时候也时有拒绝 contribution 的情况。这其实也是对开源项目维护者的一个要求:你不能对 contribution 来者不拒。开源软件的制造不是纯粹的人多力量大,而是一种精英主义。项目维护者需要依赖自己对项目的设计和理解,有选择性地吸纳社群成员和接受补丁。Linus 曾经说过,他做开源软件的过程中觉得最愉快的事情,就是可以只跟自己想要合作的人合作。

当然,这就意味着总有一些出于上游优先理念提交补丁或反馈问题的成员得不到正面反馈。但是,失败并不总是坏事。批判性的看待上游的反馈,我仍然觉得 Trino 的维护者不如我了解那部分代码,甚至我都解释累了;goleak 上游的实现我也觉得不如我的视线。还有上游的实现更好,我被折服了也学到许多的例子。

尾声

上面的故事和经历包括了十个不同类型的上游优先案例,包括代码的非代码的、有需求的纯顺手的、顺利的失败的、自己提交的做 reviewer 的。相信在开源社群当中和他人协作,或者想要参与开源社群的读者都能从中找到似曾相识的画面。

希望你在开源参与的过程里锻炼自己的技艺,收获友谊和成就!

Maintainer 的标准

开源社群存在的目的,主要是制造高质量的开源软件,并促进该软件的使用。为了达到这两个目标,开源社群需要调动参与者的积极性,并且协同背景多样的参与者的贡献,共同修复软件缺陷、改善软件体验、增加软件功能、组织社群活动和发展软件生态。大多数开源社群的环境里,实际进行组织协调工作的成员,就是社群的维护者(Maintainer)。

不同开源社群对角色的定位和命名有着各自的风格。Vim 社群生态丰富,但是 Bram Moolenaar 是唯一的“仁慈的独裁者”Kubernetes 制作了一套基于 SIG 划分的从 Reviewer 到 Appover 再到 Owner 的体系。Apache 基金会的每个项目都使用 Committer + Project Management Committee 的治理结构。Rust 和如今的 Linux 采用分模块的 Team Maintainer 模式。PostgreSQL 则由整个项目级别的 Core Team + Committer 来治理。

对于刚起步的开源项目而言,这些眼花缭乱的标准背后,其实是一个大致相同的对项目维护者的标准。对于想要深入参与开源社群的人来说,理解了项目维护者的标准,也就明白该做些什么以成为一名维护者了。本文主要对这个标准的不同层面进行讨论,顺带对比上面这些经过演变的不同版本。

做出承诺的人

开源社群是围绕开源软件建立起来的,开发软件是开源社群主要的生活内容。虽然开源软件在开源协议的许可下允许任何人用于任何用途,但是想要参与软件开发,向上游版本提交补丁,在合并之前则通常需要维护者的评审。这也是绝大多数开发者对社群维护者的第一印象:维护者是少部分具有提交代码到主分支的权限的人。

由于 Git 的 commit 命令跟代码提交相关联,且许多开源社群的维护者都有 Committer 头衔,于是开发者就简单的把维护者等同于有提交代码权限的人。其实,commit 的本意有做出承诺的意思,把 committer 解释成做出承诺的人会贴切一些。社群成员承诺在代码、文档、设计或组织活动等方面做出贡献,并且已经坚持一段时间如此,当前的维护者群体跟这名成员合作愉快,于是吸纳为团队的一员。

这种语言体系能够囊括不同类型的贡献参与,同时强调了为社群做出贡献的衡量标准,而不只是写了一段好的代码。

亲力亲为赢得权威

开源社群的维护者都是对社群做出承诺的人,这种承诺不是口头说说,也不是赌咒发誓将来会做,而是实实在在的已经做出相应的贡献。

The Apache Way 的第一条就是 Earned Authority 赢得权威,或者说,权威是参与者亲自赢得的。开源社群对项目维护者大致相同的标准,也就是能够亲力亲为赢得权威这一点。

比如,我从 Flink 使用 Curator 的切入点进入到 Curator 的社群以后,积极地参与 Curator 补丁的评审。在阅读 Curator 代码的过程中,我按照遗留代码的 TODO 注释完成了重构,又出于自己的需求实现了新的重试策略。这些表现让 Curator 的维护者团队(PMC)认为我是一个不错的维护者候选人,于是在某天早上,我收到了邀约邮件并欣然接受。

比如,@PragmaTwice 通过改进 CMake 构建逻辑的契机进入 Kvrocks 社群之后,持续发起并主导了一系列的改进,包括对 TLS 的支持。同时,他也积极评审其他参与者提交的补丁,并且对自己完成的工作不尽完善的地方能够及时响应追加补丁。他在项目需要的技术方向表现的能力和对社群发展的贡献赢得了当前维护者的一致认同,就在上周,Kvrocks PMC 通过决议邀请他成为 PMC 的一员。

比如,我在腾讯同期的朋友早年参与 Kubernetes Dashboard 项目,边改逻辑边做文档的中文翻译。成为部分模块的 Reviewer 之后,由于种种原因不再活跃,也就没有进一步成为 Kubernetes 体系下的 Approver 角色。

亲力亲为赢得权威这条看似简单唯一的标准,其实包含了几个层面的高要求。

第一点,必须是本人的参与。Earned Authority 可以衍生成 Earn Authority by Contributions, not by the Position. 这就是说,赢得权威的贡献都是你自己做成的,而不是因为你在公司的下属是某个项目的维护者,你是他们的老板,因此你就继承了他们的权威。

这一点在公司主导的项目体现得尤为明显。无论贡献属于个人怎么被强调,社群当中 $dayjob 就跟开发这个软件相关的人,无论如何都会受到老板的影响。这本来是一种公司利益借由雇员表达的正常手段,但是如果想要把公司里的 credit 直接平移到社群的 credit 上,就会导致一些非公平的结果。例如,当 PingCAP 想要为 TiDB 引入关键配置 double check 的 config reviewer 的时候,下意识的就把各个团队的 leader 直接列上,其中甚至有刚加入公司不久的员工。我好歹用“领导也不希望处理这些日常事务”给应对过去了,但是其实还有一点没有提及,那就是今天是因为他是团队 leader 给予了 config reviewer 的权限,明天转岗或者离职了怎么办呢?难道角色也跟着平移,开源社群只是说说而已,实则是公司的一个部门吗?

第二点,必须有的放矢地创造价值。这实际上是对参与者能力的考量。你不会仅仅因为修了一千个拼写错误就成为项目的维护者。虽然开源社群认可代码、文档、设计或组织活动等等多样的贡献,但是作为维护者有决策项目接受什么变更,举办什么活动,以及未来的发展方向。为了保证项目尽可能不受外行领导内行,拍脑门的决策的影响,吸纳新的维护者的时候,对候选人在对应领域已有的贡献和能力会有一个隐形的评估。

这一点也意味着参与开源社群的时候,不能一味地求多,还应该求精。上面提到的修正一千个拼写错误大部分人能够理解,但是为了刷一个项目层面的重要贡献,以自己非专业的能力强行做专业的事情,也往往会引起维护者的负面评价。

比如自己从来没有写过一行代码,却一上来就要在社群里推行新的代码风格或者工作流程。由于没有亲身参与过软件开发的过程,对这个社群的风格及其主要成员的关注点的认识有偏差,想象出来的问题和解决方案往往也是不切实际的。

我在 PingCAP 推行过 Pull Request 必须对应 issue 的策略,现在回想起来就是一个不合适的做法。虽然当时我也知道“必须”是过了,总会在推行过程里引入各种各样的折衷,但是一方面我没有把这件事情彻底做完,另一方面其实这一个打点并不犀利。实际问题是开发者提交补丁和记录问题的时候缺少上下文和交叉引用,以至于大家都在写代码,但是却往往只关注自己当前就在写的这一个补丁,这在复杂项目当中是行不通的。

当然,这不是说没有亲自写过代码就不能提出建议,毕竟参与是多样性的,对于软件工程的老手来说,有些问题只一看也是明显的。不过在实际提案和实行的过程中,以商量的态度跟实际会受建议影响的人竭诚沟通,实地考察和验证提案的影响,最终在当前维护者团队的支持下实施。换句话说,你只能发现和解决已经存在的、社群成员承认的问题,而不能因为要解决某个问题是 OKR 的一部分,例如提高代码测试覆盖率、降低代码圈复杂度,而强推新的标准或流程。

对于代码开发来说也是一样。Twice 在 Kvrocks 提交补丁的时候就遇到过一个新成员主动 review 他的代码,但是所提的问题却非常初级。一个利用新的 C++ 特性改进代码质量的补丁,这位成员不了解 C++ 的标准细节,就提出一些想象出来的边界情况。虽然我赞同不懂就问,但是提问请教的措辞和 request changes 的措辞给到维护者的印象是不一样的。另外,社群成员都不熟悉的领域和都应该熟悉的领域里的措辞也是不一样的。

我在 Kvrocks 也拒绝过两个参与者。一个是说着要帮助改进测试,但是却来了一句“你能告诉我具体要怎么做吗,我不知道要干嘛”的。我差点回我不是你的保姆,你能干就干,不懂可以问,不知道问什么可以不做。另一个是要改进 Kvrocks 传参逻辑的,这种重大的用户界面变更没有任何设计直接就怼了一个用脚本缝合的 monkey patch 来,我只能建议先想清楚要做成的使用方式是什么样的再来。

反转的例子也有。我在 Curator 提出的修复分布式选主模块的活锁问题的时候,一位 Flink Committer 老哥上来指点江山。一开始,我看他都没注意 PR 内容是啥就泛泛而谈 PR 拆分,以前也没见过这人,就不客气的回了一句“你告诉我怎么拆分”。后来他也先道了歉,然后仔细看了补丁内容,给出了不错的评审意见和测试用例,我也认同他是一个有能力的开发者。如果后续有持续的参与,有可能会提他做维护者。

这就引出第三点,必须持续参与以赢得权威。开源社群作为由参与者组成的社群,是一个有机体。这就意味着它的发展是连贯的,而不是由一锤子买卖形成的。

Twice 将 Kvrocks 的构建系统从 Makefile 迁移到 CMake 之后,并不是做完就算了,而是后续还有其他成员包括他自己踩出来的问题,还有新功能加入是跟 CMake 整合的需要和进一步带来的新问题。如果 Twice 没有为他的补丁负责而是做完就算,如果社群当中也没人接过维护的职责,那么这份 credit 也就自然消散了,说不定还需要其他人回滚这部分变更。

比如 BookKeeper 社群想要把构建系统从 Maven 换成 Gradle 以利用增量编译的好处,以及想要把网站迁移到新的架构上面去,这些工作都半途而废了,最终社群维护者需要付出额外的努力来回滚,最初做出这些变更的人自然也算不上赢得权威。又比如,TiKV 的 VerKV 方案没有经过仔细的评审,因为一些外部原因强行推进,但是从来没人正式使用也没人响应问题,到头来维护者清理相关代码也花费了不少时间。

不同项目在不同阶段对参与时间的要求自然是不一样的。新生的项目一片勃勃生机、万物竞发的气象,亟需扩大维护者的队伍来响应参与者提交的补丁和反馈建议,往往在一个水平不错的参与者持续贡献一个月或三个月后就开始考虑邀请成为新的维护者。成熟的项目内容复杂,哪怕掌握一个模块及其相关知识也要花费相当的时间。为了避免在不够了解项目和模块背景的情况下新的维护者做出冒进的决策,往往会延长到半年、一年乃至数年的考察周期。

比如 PostgreSQL 在 EDB 的收购案之前,Core Team 的五名成员加上二十几名 Committer 基本能够响应 PG 社群内所有的事情,直到收购案导致 Core Team 成员多样性不足,才推动引入了来自其他公司的两名新成员。这两名新成员都是 PG 社群十年以上的长期参与者。

反过来看另一个极端,Raku 语言还叫 Perl 6 的时候,它的成员吸纳策略就是任何对项目有兴趣的人,只要提交若干个补丁并被合并,哪怕只是文档的修正,也会给予这个人整个项目除了解释器以外的维护者权限。这样的策略让它在短时间内建成了两百余人的维护者团队,并且其中十几个非常热情的参与者领导完成了测试、文档和工具库的开发。这种策略有一个前提,那就是项目必须事实上有一个仁慈的独裁者或者精英团队,才能在低门槛吸纳的新成员做出主观恶意的行为或者水平有限却热衷于提出关键改动的时候予以制止甚至踢出。如果社群没有这样众望所归的人,那么一旦有新成员的理念与项目原本的主旨背道而驰,就容易反客为主,吞噬原本的社群。

参与社群事务

如果说 Apache 区分 Committer 和 PMC Member 有什么标准的话,那就是看社群成员多大程度上会去参与社群事务了。

相当部分的社群成员来到社群,只是为了提问和得到解答。其中有能力解决自己问题的成员,如果能够持续的提交高质量的补丁或者撰写文档,则有可能被维护者授予对应仓库的权限以简化他参与的流程。但是,尤其是出于工作需要而参与到一个开源社群的成员,往往并不关心其他人在干什么,也不关心项目的未来、生态的发展和软件的影响力。如果某个开源社群对维护者团队设置具体的角色,那么这样的成员就不可能成为整个社群层面的维护者或者所谓核心团队(Core Team)的成员。

对社群事务的关注和参与,包括但不限于:

  1. 回答用户问题,为软件站台。这点非常重要,一下子可以把只是 $dayjob 跟本软件相关的人筛选出大半。许多打工人只是工作需要写点代码,恰巧这些代码需要写在开源软件上。他们不会关注软件社群里其他用户的问题,因为“这跟我有什么关系呢”。而社群的维护者是对社群生产的软件有极高的认同感和责任心,并且自觉有义务推广它的使用的人,自然会关注使用软件的人碰到的问题,解决问题促进使用,总结问题看看软件哪些方面还有不足。Flink 的参与指南就明确把 Support Flink Users (User Mailing List, StackOverflow, Jira, etc.) 和 Spread the Word About Flink 作为社群贡献的一部分。
  2. 参与软件版本发布。越是复杂的软件,发布流程和发布前的测试、校验就越是复杂。Flink 社群把参与发版作为邀请成为 PMC Member 的重要加分项。虽然一般来说只有当前的维护者群体才有最终发布软件的权限,但是作为社群成员,帮助测试和校验,提供反馈,或者和当前的发布团队(Release Manager)沟通参与构件的准备和 blocking issue 的处理,都是为交付软件做出的重要贡献。对于大部分用户而言,不稳定的主分支上的代码是不能在生产环境使用的,只有经过 PMC 认证投票通过发布的版本才是可信赖的版本。
  3. 建设社群基础设施。有些开源社群,例如 Rust 和 Kubernetes 会有专门的团队负责建设和维护基础设施,包括仓库权限和各项功能的设置,CI 流水线的架设和资源使用的监控,网站构建和部署的自动化,等等。对于没有那么复杂生态的社群,这些工作仍然需要有人做,做好了对社群开发、迭代和宣传的效率有明显的提升,而大部分只关心写核心代码的人很少会关注到基础设施的建设和维护。因此充分理解社群的目标和参与到社群日常生活的基础上,愿意承担基础设施的建设和维护工作的成员,在维护者团队考虑吸纳新成员的时候会有相对的优势。
  4. 关注艰苦和乏味的工作,又能领导重要的功能扩展的设计实现。这两点稍微有些冲突,出自《大教堂与集市》 3.13 节“什么才是好礼物”。一方面,社群维护者对项目的责任心促使他能够去做艰苦和乏味的工作,比如极难调试的软件缺陷、修复不稳定的测试、编写技术文档和优化开发构建流程,等等。另一方面,高质量的项目之所以是高质量的项目,不是因为流程良好随便栓条狗都能自动写出来,高质量的项目根本上是由极具才能的工程师写出来的好代码造就的。Flink 的流计算 API 的作者,Pulsar Functions 的作者,都毫无疑问的是对应项目的维护者。
  5. 指导新人,传递知识。开源社群合作开发的一大优势就在于能够最大程度的避免知识单点故障、知识孤岛和知识断层的现象。为了做到这一点,社群维护者积极指导新人,传递社群工作方式和软件专有知识是必不可少的。即使社群就软件使用、设计和社群运转的规则特供了详尽的文档,经过人的连接解释和畅聊一遍,给到新人的体验和理解也很不一样。基本上,每个新的维护者都有自己的领路人,而此后他也会成为其他人的领路人。

关于第四点和“社群事务”之间的关联,我再多做一些讨论。

艰苦和乏味的工作,实则是一个维护者对开源社群当中开发者体验设身处地的考察。如果发现和修复不稳定测试有一套相对可靠稳定的方法论,那么这项工作未必是艰苦和乏味的。能够耐下性子来做大家不愿意做但是对项目的易用性或开发者体验有帮助的工作,很难不说体现出一种对社群的责任心。也只有亲自体验过这些工作为什么艰苦和乏味,才有资格推动流程优化。否则只是捧着软件工程书本上的说辞,甚至拍脑袋想出来的点子,越积极推动,对社群的负面影响反而越大。

重要的功能扩展的设计实现,进一步说是软件的未来要往何处走。比如 Flink 的流计算 API 显然从根本上影响了 Flink 项目未来的走向,Flink Application Mode 部署模式的推出和对 Kubernetes 的支持则显著扩张了 Flink 应用在生产环境的使用场景,也让 StreamPark 项目的作者下定决心做流计算应用开发、部署和监控平台。一个深度参与到项目未来发展的成员,不是维护者也说不过去。

很多人在我今年成为 Apache 软件基金会的正式成员以后,问我成为 ASF Member 的诀窍是什么,这跟这里提到的参与社群事务是一致的。因为我相信 The Apache Way 的理念,在中文世界里大力推广 Apache 的开源理念;不止参与一个 Apache 项目,而是积极推动不同项目之间的联动,例如 Pulsar 踩出了 Maven Shade Plugin 的问题,就在上游修复;参与到孵化器项目的讨论。如此反复,当前的 ASF Member 认同我在基金会层面的协同的开源理念传播的贡献,再由姜宁老师提名,成为 ASF Member 也就水到渠成。

我参与 Flink 社群并成为 Committer 花了一年左右的时间,期间完成了对 Runtime 代码的重构并修复了数十个 Runtime 的并发问题,也主导完成了 FLIP 的设计、发起和实现。但是我对 Flink 的发布和代码以外的工作不是特别关心,后来因为工作内容的变化,也没有太多参与到社群事务当中去,于是成为 Committer 三年以来也就没有被邀请成为 PMC 的成员。反过来,我在成为 Curator PMC Member 以后,把 Curator 积压的 Jira 问题和 PR 处理一空,并作为 Release Manager 发布了 5.3.0 版本,参与完成了对 ZooKeeper 新版本新功能的接入兼容。

这就引出在讨论 Maintainer 的标准后的最后一个点:邀请谁作为 Maintainer 要基于对他已有工作的评估,而不能基于假设;但是 Maintainer 的标准对于大部分社群来说最好也不要太高,很多人在成为 Maintainer 之后会爆发出超人的热情。

前者出自我在 PingCAP 的时候回答当时 TiKV Team Leader 问为什么不能直接给员工写权限的问题。现在看来,其实公司项目上这么做也不是不行。但是就相对中立的开源项目来说,你给予员工写权限,只是因为你基于劳动合同和面试流程,认为他会在未来做出足以匹配项目维护者的贡献和赢得权威。这对于其他背景的社群成员来说只是个说法而不是已经实现的参与贡献。对于全职员工来说,每天大部分工作都与开源软件有关,其实已经是非常大的优势了,只要能够按照社群理念积极参与合作,数个月内成为维护者不是什么大问题。相反,如果基于我当时从事 Flink 工作,持续以当时的强度参与一年也能达到 PMC Member 的标准就邀请我,那么后来我工作转变投入减少,这种提前兑现的权威就很容易造成对其他成员的不公平。某些开源项目的维护者基于对社群成员未来的期待授予权限,但是社群成员随后因为种种原因,比如就是想混个头衔进简历,马上就神隐,这个时候邀请新人的维护者就破防了,这就是基于对未来的期待会导致的问题。

后者,对于真心想要打造优秀社群的参与者,他们是天生的技术领袖、社群领袖,如果能够准确的评估他们当前贡献表现出来的责任心和能力,而不是拘泥于僵化的贡献数量指标,这些人在成为 Maintainer 之后可能会爆发出超人的热情。比如 Twice 一开始可能只是听我提起 Kvrocks 需要帮助,因为他刚刚已经在 OneFlow 做过类似的事情了,正想要推广自己的实现。但是社群在他能把这件事情做好并承担后续维护责任后果断邀请成为 Committer 一员,我想对他的激励也是明显的。再加上社群当中其他维护者以身作则回答用户问题和协同参与者的贡献,他在不是 PMC Member 的时候也承担了 PMC Member 的职责,我们就认为应该让他成为 PMC Member 以帮助他做出更大的贡献。

对于维护者的门槛,Apache Incubator PMC Chair Justin Mclean 还有这样一段话:

IMO setting the bar to be a) signficant feature contributions or b) long-term participation in community building is too high. You want to be again to give committership to people after a short time to encourage people to make further contributions, not to get a goal that only some people will reach. Remember, not everyone will be able to work full time on your project due to their time zone, day job, family commitment, or other factors. While each project is free to set the bar, please don’t make the mistake of setting the bar too high.

开源社群的协同模式像是《合作的进化》当中提到的志同道合愿意合作的小群体,在复杂世界当中生存下来并不断通过合作产生 1+1>2 的效果扩张群体范围。虽然对参与者的热情和能力的评估是有必要的,但是开源协同更多的是基于善意的假设。就像《合作的进化》当中提到的一样,如果倾向于背叛的人太多,那么最优策略就是你也选择背叛,但是如果开源社群一开始就有一个选择合作的领导核心和运行理念,那么即使吸纳进来选择背叛的新成员,他也会很快因为得不到收益而离开。

最后罗列一些关于 Maintainer 的标准、职责和权利的参考资料。

饱和沟通:开源社群的消息传递准则

分布式系统的开发者知道,不同于本地方法调用总是被执行,要么成功要么失败,分布式系统之间各个组件的远程调用还存在第三种可能,那就是超时。

从消息发送者的角度看来,超时意味着没有确认信息返回。但是当前调用对应的一系列操作到底是已经成功,只是回复丢失,还是其中某些失败某些成功,或者全部失败,甚至是请求本身没发出去,这些情况一概无法断言。

大部分开源社群是分布式组织,社群成员分布在不同的地域乃至不同的时区。相比于线下集中办公的组织而言,分布式组织与分布式系统一样存在着“超时”的挑战。

线下集中办公时,负责同一个工作项目的人经常会坐在一起,有什么事情转过头、走几步也就当面说清楚了。工作关系紧密的几个人往往会共享午餐时间,休息娱乐时间。这种面对面的合作带来的信任感和大小事情都能当面沟通的效率,是分布式组织很难直接做到的。

不过,无法效仿线下集中办公的方式直接面对面沟通,并不意味着开源社群这样的分布式组织的沟通效率就总是低下的。从我在开源社群数年的观察和实践来看,要想做到在开源社群这样的分布式组织当中高效地协同,确保事情在异步沟通和分布式合作的情形下仍然能够稳步前进,一个不可缺少的点就是饱和沟通(Overcommunicate)。

饱和沟通的重点落在“饱和”,这意味着沟通反馈必须是及时的,甚至会稍微超出必要的限度。

我刚进入到开发者社群关系的角色的时候,曾经被问过这样一个问题:为什么你能够在 Flink 社群持续参与,直到成为 Committer 呢。稍加思索过后,我给出了这样的答复:因为在 Flink 社群里,我的提问会有人回复,我的补丁会有人评审,这是一个能够得到反馈的社群,我喜欢这样的正反馈循环。

要想在开源社群建立起正反馈循环,关键在于沉默的参与者能否主动发声,相对少数的维护者能否给予必要的回复。

前不久,我在推特上看到这样一条推文

现实:

  1. 提了 PR,经过了几轮 review,PR 的作者消失了,PR 就一直挂在那儿;
  2. 提了 PR,经过了几轮 review,maintainer 不跟进了,PR 就一直挂在那儿;
  3. 提了 PR,maintainer 根本没有任何反馈,PR 就一直挂在那儿。

这反映出开源社群当中沟通的缺失,已经成为众多参与者面临的共同问题。

我在《高效参与开源的诀窍》当中提出,新成员首先要明确加入开源社群,跟社群建立起联系。

虽然刚开始的一段时间可能会感到无从下手,先观察其他成员讨论的内容和做法也不失为一个选择,但是很快你就应当试着加入到讨论当中,针对你不懂的问题大胆的提出自己的疑惑。绝大部分社群成员愿意解答其他人的问题,或者引导你到能够解决问题的地方。经过这样的几轮沟通,社群成员对你也能建立起基本的了解,你也知道什么问题应该在哪里求助。反之,如果像我在《高效参与开源的诀窍》所举的反例那样,闭门造车式地想要搞个大功能,而社群完全不知道你在做的工作,那么社群前进的过程中就有可能无意间破坏了你所做的功能设计的假设,使得费劲心力设计的大教堂图纸付诸东流。

Kvrocks 的参与者 @xiaobiaozhao 在今年六月份的时候提出可以用 LuaJIT 替换 Lua 实现更好的执行性能。在得到两位项目维护者的回复和鼓励以后,他拿出一个原型以供测试。最终经过近一个月四名 Reviewer 一共五十余条消息的讨论和建议以后,成功地以后向兼容的方式用上了 LuaJIT 依赖。这是 Redis 上游都没有办成的事情。

这个过程当中,所有参与者都及时地在 GitHub PR Review 这个平台上同步自己测试的结果和改动意见,对于自己拿不准的地方提出自己的问题和阶段性的想法。当补丁合并被其他问题阻塞住一度搁置的时候,一旦阻塞问题解决,也能有人想起来重提这个补丁让几名 reviewer 再次判断是否可以合并。

这就是开源协同非常典型的一种合作方式。愿意投入时间发起或推进某项工作的成员,积极地与其他相关成员沟通获取必要的信息,做出自己力所能及的贡献,并同步结果和请求进一步的反馈。

我在进行 TiDB 测试迁移工作的过程中,会随时同步编码上的最佳实践以及与其他参与者协同的过程中得出的结论。这些结论在后续其他相关工作里被多次引用,迁移工作本身也被作为 tracking issue 管理工程的实践被其他项目所借鉴。在将近一年的过程中,tracking issue 和不少 subtask 都有我和其他社群成员的沟通,有些 issue 在估期和细节上的沟通甚至有些啰嗦,但是这种多次发送消息和确认的过程却实在地避免了不知道对方是否理解了自己意思的问题。

即使是在面对面的沟通当中,误会也时常发生。异步沟通的方式本来就缺少微表情和肢体语言的信息量,如果合作者对于相关信息的同步再三缄其口,假设其他人能够独立得出和自己一样的结论,那就是异想天开了。

当然,overcommunicate 一词也有两面性。如果饱和沟通超出了必要的限度,变成信息轰炸,对于所有参与者来说就成了一种负担和消耗。要想避免饱和沟通变成信息轰炸,可以从两个方面入手。

第一个,虽然饱和沟通要及时反馈进展和提出问题,但这并不等同于任何中间过程都要急火火地发布出来。由于异步协同天然的滞后性,阅读消息并发出回复的成本比起面对面沟通是要高出许多的。只有适当整合自己的观点,简明扼要地分点提出问题并说明期望的回复,才能减少接收者的阅读理解负担。

我在 Pulsar 社群提议开启 Update Branch 按钮以改善开发者体验的时候,就采用了这种方式。首先以一句话说明期望读者做的事情,表明是否支持这个提案,然后再有一个完整版本分点说明这个提案的背景、意义和可能存疑的方面。在信息爆炸的时代,大部分人都会先判断这件事情是否与自己有关,是否应该付出时间了解细节。这是 TL;DR 大行其道的原因,也是在饱和沟通时不得不顺应的环境。

第二个,饱和沟通绝不是 at 所有人或者随机找人搭话。参与社群一段时间后,你应该能够知道项目不同领域的大致划分。例如,谁是某个功能模块的专家,谁负责项目构建逻辑的维护,谁对 CI 的问题最清楚,谁是总体协调社群的领袖。如果你想让自己的提案得到回复,最好根据提案涉及的领域和决策的影响范围来确定干系人,把饱和沟通的策略应用在这些干系人上,而不是无差别地骚扰社群成员。

要想做到这一点,可以参考 RACI 模型来对当前工作及其干系人建模。RACI 模型把干系人分成四类:实际完成工作的责任人(Responsible)、参与投票的决策人(Accountable)、可以寻求帮助的领域专家(Consulted)和需要知悉这项工作正在发生的相关人员(Informed)。

例如,提出拆分 Pulsar SQL 的提案的过程中,我是实际完成工作的责任人。首先,我要密切联系的是参与投票的决策人,在 Pulsar 社群当中,这主要是关注整体模块演进的领袖,比如多次关注此事的 Matteo Merli 等。其次,我要尝试寻求帮助的是此前参与过相关工作的成员,尤其是向上游 Trino 社群提过 Pulsar Plugin 补丁的 Marvin 和补丁的 reviewer 们。最后,由于拆分方案涉及到打包和 CI 的变化,我需要在以 Pulsar Improvement Proposal (PIP) 的方式全社群可见的提出这个提案,知悉所有人。其中,我会专门跟当前 CI 的作者 Lari Hotari 打个招呼,并在修改打包内容的过程中发现有相关的提案,都告知他们有另一个和打包相关的提案正在进行。

例如,作为 Kvrocks 项目里替换 Lua 提案的决策人之一,首先我会跟其他决策人同步意见。其次我会关注责任人对进度的把握,目前是否有阻塞的环节。如果 review 过程中间遇到了我自己拿不准主意的内容,我会试着找领域专家提供意见。到了 Kvrocks 项目里要采用现代 C++ 风格改写现有代码的决策里,我会放缓这个决策的推进,确保主要的开发者都知悉代码风格取向的变化,充分表达自己观点以后再做决策。

如果作为 Consulted 或 Informed 的角色,则一般不需要你主动推动工作的进展,只需要提示可能存在的风险,说明清楚问题并响应提问即可。

开源运动几十年来,发展出了一套适合于开源社群这类分布式组织的一套协同工作方式。依靠开源协同的力量,开源共同体调动起全球范围内全行业的精英的积极性,一起开发高质量软件。越来越多的软件公司也试图借鉴开源协同的模式来提高软件开发的效率,越来越多的公司因为自己的业务依托于开源软件的发展而不得不了解开源协同的工作方式。

要想参与到开源协同当中来,就必须适应饱和沟通的工作方式,否则将始终游离于多样化的社群之外。要想学习开源协同的方式建设高效的分布式组织,实践饱和沟通的经验是一个很好的切入点。分布式组织独特的挑战,很大部分是由于地域和时区的隔阂带来的沟通难题,如果能以饱和沟通的经验在不同地域的成员之间建立起信任,保证工作不断地向前推进并且每个干系人都能被“饱和”覆盖到,那么解决其他组织管理问题,也将势如破竹。

开源世界当中到底存不存在“白嫖”?

开源软件不是凭空出现的,开发开源软件是一项艰苦卓绝的工作。每个开源软件的背后少则有原作者一人的投入,多则协同了成千上万人组成的开源社群的共同努力。然而,开源软件的源代码总是免费可得,并且开源软件协议总是不限制用户的使用形式。

基于开源软件完成工作乃至搭建业务盈利的用户,并不总是参与软件开发的人,这种形似经济学中“搭便车”的行为,在国内被提及的时候总会被称为“白嫖”,以至于后者称为圈内的一个热词。那么,开源世界当中到底存不存在“白嫖”,不同角色眼中的“搭便车”行为到底是怎么样的?本文将对此做些讨论。

用户的“搭便车”行为

从经济学的角度上,“搭便车”行为意即不付成本而坐享他人之利。由于开源运动的精神就包括了制造出来的开源软件的源代码免费可得,并且不限制任何人将其用于任何用途,所以我们可以说,开源世界当中“搭便车”的行为是广泛存在的。

实际上,当我们编译一个 C 程序的时候,很可能我们就依赖了自由软件 GCC 或开源软件 Clang 作为实现编译的编译器。大多数程序员都不曾直接参与过这两个编译器的开发,因此做过这样操作的人都可以算是这两个软件的“搭便车”者。

然而,自由软件运动发起的根本,就是 Richard M. Stallman 对软件自由的追求,即他认为软件开发应当像学术研究一样,其成果是公开透明并且允许其他人演绎的。开源运动方面,前面提到的“开源软件的源代码免费可得,并且不限制任何人将其用于任何用途”就是开源定义的一部分,并且 Apache 基金会的元老级成员 Ted Dunning 在纪录片 Trillions and Trillions Served 当中也提到,当他不再纠结于其他人使用软件盈利,而是彻底使用开源软件协议授权自己开发的开源软件的使用以后,软件的生命力和他本人得到的反馈和声誉回报反而增强了。

也就是说,纯粹使用开源软件的用户,并不会挑战一个秉承自由软件精神或开源精神的开发者的认识。这样的“搭便车”行为在开发者选择对应的开源软件协议的时候就被视为做出了对应的承诺,这也意味着开发者应该谨慎选择开源软件协议

另一方面,Apache SkyWalking 的作者吴晟曾经说过,“真正伤害开源的是开发者本身”。通过原文的描述和我走访若干开源软件的维护者得到的回应,以及自己作为开源软件的开发者的体会来看,当开发者痛斥用户的“搭便车”行为为“白嫖”的时候,往往指的是这样的一种情况:

一个是开发者,特别是中国的开发者认为,软件作者去帮助他人是天经地义的,因为整个软件是你写的,所以我来问你问题,你就应该有问必答。如果你不答,就认为你这个人摆架子。而不是考虑因为软件作者用了自己的时间提供服务,所以应该表示感谢。

简而言之,就是无偿使用了开源软件,却丝毫不尊重投入了巨大精力开发出开源软件的生产者们,反而认为这些生产者理所应当提供各种支持。

关于这个话题,我和吴晟在推特话题 #开源逸闻上抛出了不少例子。

其中我印象最深的是 Vue.js 作者的尤雨溪拉黑挑衅者的案例。这个用户认为 Vue.js 的某个库用 TypeScript 写实在是太难懂的,于是抱怨难道作者不应该照顾纯 JavaScript 用户(言下之意,尤其是“我”)的心情吗?尤雨溪的回复堪称模板,也讲清楚了这种行为在维护者眼里的性质。这里做一个简单的翻译引用:

嘿,没有人强迫你用这个库,你也不需要为使用它付费。作为一个用户,你至少应该以一种互相尊重的态度进行有建设性的交流,但是你没有。源代码当中的复杂类型是为了给日常使用提供更好的提示,这是一个显而易见的折衷取舍。你把你自己在 TypeScript 上的挫败感发泄到库作者身上,而库作者却在免费地生产开源软件并试图为你提供帮助。你是一个典型的开源软件“搭便车”者,请你滚蛋并且再也不要使用这个库。

P.S. 你已经被所有 vuejs 组织下的项目永久拉黑,请不要再试图回复。实际上,我相信你可以停止使用开源软件并且从头开始写所有东西,这会对你更好!

这个模板很快被其他的一些开源软件的维护者复用(笑)。当然,库本身仍然是开源软件,这个被拉黑的人仍然可以免费下载到源代码。但是从这个案例里我们可以看到开源软件作者最反感的“白嫖”行为,就是这种理所应当地索取。

吴晟在多年经营 SkyWalking 项目的过程当中也遇到了很多这类案例,

我也在邮件列表上看到过几个典型的案例。

一个是某公司的软件供应链审计人员“要求” ZooKeeper 说明自己软件的定位和不同版本使用风险,这是把 ZooKeeper 开源软件社群当成与自家公司签订了某种合同的软件提供商了。

另一个是 Flink 中文列表上,不止一名用户总会像上面吴晟见到的案例一样,把自己的本职工作的问题抛到邮件列表上,并心急如焚地等待别人的解答,一旦收不到回应,就会颇有些气急败坏地抱怨“这个问题没人解答吗?!”当然,在用户列表上抛出问题,这是软件用户的权利。但是 Apache 开源社群强调了每个社群成员都是志愿者,如果有人出于社群责任感、热心或者解答问题锻炼自己等等动机帮助你,这是值得感谢的。但是你本人才是领薪水做工作的那个员工,其他人没有义务回答你的本职工作带来的问题。或许你的老板很急,你也很急,但是你先别急,才有可能在社群的帮助下解决问题。

最后,我想提倡的一点是如果社群当中其他成员对你提供帮助,我个人希望听到的是感谢而不是“辛苦了”。因为我一不觉得辛苦,二不觉得这是个义务。我没有辛苦地为你做什么,你也不要以为我是在辛苦地为你做什么。至于我是在“非工作时间”或者你当地时间的凌晨回复,你也不要感到惊讶。不要以自己对这件事情的投入程度来揣测别人为什么要在你当地的这个时间做这件事情。很多情况下,他不是为你而做,而是出于前面提到的社群责任感、热心或者解答问题锻炼自己等等动机而采取行动。至于别人希望在什么时间做什么事情,也不需要你做评判,主观地觉得别人“辛苦”了。

行文至此也到一段落,原本准备写的“云厂商的‘搭便车’行为”和“开发者的‘搭便车’行为”两个主题就等日后再有动力的时候再写吧。

云厂商的“搭便车”行为

另一个经常被拿出来讨论的“搭便车”行为,是云厂商使用开源软件打包成云服务售卖盈利的例子。

为什么 MongoDB Inc. 要说云厂商伤害了 MongoDB 的社群,Elastic、CockroachLabs 和 Airbyte 也紧随其后呢?

这四篇公告,我在《免费增值的商业模式》当中也提到过。

我想“云厂商的搭便车行为伤害了制造开源软件的公司”这样的说法,是开源的动机不同所导致的。

这些制造开源软件商业公司一开始开发开源软件,是看到了开源软件在技术圈里的传播潜力。从他们的思考模式出发,其他公司想要复刻同样的商业价值,必然会因为跟不上核心团队的研发能力而被市场淘汰。由于分布式系统的部署和运维足够复杂,只要开源软件能够维持 MySQL 时代一家话事的话语权,拿下缺乏维护这些分布式系统的客户是有可能的。换句话说,商业公司的基本诉求是盈利,当 MongoDB Inc、Elastic、CockroachLabs 和 Airbyte 选择开源全栈技术的时候,他们考虑的商业模式是免费增值的市场策略 + 软件复杂度带来的维护成本引发的付费意愿。

然而,云厂商拥有强大的云上软件部署优化能力。云厂商与上面这些商业公司做商业竞争,并不需要研发出具有代差的高水平软件。只要完成云上打包部署的工作,调配厂商内部的网络、存储和机器资源,调动遍布全球的销售团队,在企业服务市场的组合拳足以在直接使用上述开源软件的情况下赚取高出原厂的利润。

这一下子,制造开源软件的公司就不干了。

你没有听过 Kubernetes 的作者指责阿里云销售 Kubernetes 发行版损害了他们的利益,也没有听说诉讼专家 Oracle 打击其他 JDK 的提供商,Linux、Spring 和 Netty 等等众多开源项目的维护者也不在乎其他厂商如何打包销售解决方案。因为这些厂商的经营跟他们的获利途径没有冲突。然而,MongoDB Inc. 流派的公司,就是打着规模化销售软件服务来盈利的算盘,云厂商的行为是实打实的利益冲突。

另一方面,虽然也有部分开源软件作者为自己的付出得不到回报愤愤不平,但是大量高质量的开源软件开发的目的,是为了解决作者自己的问题或者出于 Because I can 这样的理由。这些作者有着不同于开公司销售标准化软件以外的诉求,乐于见到云厂商使用它们的技术构建自己的产品方案。反观制造开源软件的公司是实打实的支付了员工工资开发这个软件的,为了回本和盈利就指望把软件和服务卖出去赚钱,除此以外也没啥别的想法。这样的模式下云厂商赚走的钱,即使还给自己留下了一些盈利空间,也是这些做着有朝一日成为 Microsoft 体量的公司梦想的商业公司所不能接受的。

前文提到,“开源软件的源代码免费可得,并且不限制任何人将其用于任何用途”是开源定义的一部分。这就意味着云厂商对开源软件的打包销售在开源协议许可的范围内。制造开源软件商业公司愤而斥责云厂商“白嫖”、“吸血”,是因为他们把自己制造的开源软件当成了专有软件,不希望其他人能够与他们进行商业竞争。

这一点,从 Elastic 即使能够打赢和 AWS 的商标官司,在开源协议保护知识产权的范围内迫使 AWS 将其 ElasticSearch 服务更名为 Amazon OpenSearch Service 以外,还是要以禁止提供同类服务和破解密钥的 Elastic License 来重新许可自己的产品可以看出来。

从 ELv2 的措辞当中我们可以看出来,这些厂商的主要诉求就是禁止商业竞争。ELv2 并未限制不提供同质服务的情况下的商业使用,对于 Elastic 来说,如果用户具备相应的技能,或者用户公司已经支付给其工程师用于运维 ElasticSearch 的内部使用,那么也就不是自己的目标客户。这也是 Airbyte 和 Starrocks 的选择。CockroachLabs 使用 Business Source License + Cockroach Community License 达成了一样的效果。

这种模式的主要缺点如同 Eric S. Raymond 在《大教堂与集市》中《魔法锅》一文所说:

封闭条款往往限制了同行评审参与。

换个角度看,在组织员工投入精力参与开发的开源软件,有可能被云厂商或其他竞争对手直接使用的情况下,商业公司应该如何理解和投入在开源方向上的资源呢?

我们可以从社群和公司的关系来分类讨论。

如果是公司依附型的关系,也就是上面讨论的 MongoDB Inc. 这样的公司,雇佣员工开发软件,那么只要云厂商或者其他强势的对手参与商业竞争,以 ELv2 或者 BSL 重新许可基本是定局。或许在自己研发的软件尚不突出,竞争对手发现没有直接复制的价值的时候,才能维持开源许可的外表。GPL 和 AGPL 可以提示下游软件作者期望整个社群在一个共同的上下文当中工作,但是无法抵抗一行不改只是增强部署实施和销售团队碾压的来自云厂商的商业竞争。

如果是社群依附型的关系,也就是 RedHat 和 Linux 以及 Kubernetes 这样的关系,开源社群可以独立生存发展,那么商业公司应该不会有其他厂商“白嫖”或“吸血”的体验,因为自己也是社群强大生产力的获利者。这类公司需要理解与自己经营关系紧密的开源社群的运作方式,通过制作发行版或者一揽子解决方案来形成自己的商业价值。形成后者能力的代码是专有代码,因此也就不存在被“搭便车”的问题。例如 Tetrate.io 制作 Istio 发行版和应用网络治理框架,虽然使用了开源的 Istio 和 Apache SkyWalking 等软件,但是也有大量内部代码。阿里和腾讯的 Kubernetes 发行版也是类似的逻辑。

如果是相互依附型的关系,也就是项目并非公司所有,但是社群又需要公司投入支持的情况,这种情况产生的项目往往一开始并不是为了作为商品销售而产生的。例如 Apache DolphinScheduler 目前是白鲸开源公司的技术图谱上的重要基础软件,但是它的启动阶段是在易观公司内部完成的。同样的软件还包括 Apache Pulsar 和 Apache Doris 等等。对于这些例子当中的公司,由于没有那种“这是我的软件”的执念和软件确实几乎 100% 是公司出钱投人开发的背景,在经营上可以参考社群依附型的做法,同时加强原厂品牌,积极营建社群伙伴关系,提高其他竞争对手攻占定位的难度。

公司在技术上选择开源路线的时候,应该遵循开源本身就是跨越组织边界的集体智慧的性质,寻找已经存在的方案。例如白鲸开源公司为了完善自己 DataOps 的流水线,没有选择从头开发一个数据管道,而是联合原 Waterdrop 社群孵化 Apache SeaTunnel 项目。例如最近参与 Apache Ambari 项目复活的成员,有部分人的背景是希望将 Ambari 用在公司大数据套件产品当中。从一开始就加入到开源协同的环境里,也就不会出现抱着盈利的目的付出巨大精力以后没有回报的挫折和矛盾了。

当然,虽然把公司的全部营收赌在一个全开源的技术栈能大卖上不现实,但这并不意味着企业就不能从头开发一个开源软件。上面提到的这些可以拿来就用的软件,本身是在商业公司内部作为基础支持服务开发,由于公司并不需要依靠他们盈利,进而出于开源协同对质量的促进、生态的建设和声誉的积累等优势,将代码开源。这其实也是《大教堂与集市》当中论证为何应该开源的一部分,在这些情况下,开源并不会损害公司的利益。

最后,生产开源软件的参与者们到底还是一个一个具体的工程师。不是每个人都必须要创建公司,不是每家公司都必须要做得像微软那样大。Vue.js 的作者尤雨溪曾经透露他通过 Patreon 接受捐赠和其他途径获得的收入,在 2016 年就已经超过在 Meteor 和 Google 的收入。前端圈子的曝光量和捐助习惯是值得学习的,可以看看其中几位的 Sponsor 情况。

256 "324 人 funding @antfu"

256 "407 人 funding @squidfunk"

其中,由于重度使用 @squidfunk 的作品 Materials for Mkdocs 作为网站框架,我发现一些关键的功能只在 Insider 版本当中才有,作者实现得很好,而我没有时间精力在开源版本上做出一样好的改动,这就促使我也成为这 407 个赞助者的一员。

最初的 Apache 成员大多有正式工作,Linux 的核心成员乘着 VALinux 和红帽上市的东风分到了相当数额的股票,Netty 的两位核心作者,Trustin Lee 目前在 Databricks 工作,Norman Maurer 目前在 Apple 工作。开源的经历可以是你职场生涯的坚实基础,如果你有尤雨溪类似的投入到社群并积极维护资助关系、生产周边或接受广告的话,开源的声誉也可以为你提供额外的收入甚至成为主要收入。

这些例子的存在说明了“开源”和“云厂商”并不是天然的对立关系。实际上,现实情况是某些商业公司通过在云上打包销售基于开源软件的解决方案盈利,云厂商和部分这样盈利的商业公司有竞争关系。开源本身是一个包容的概念,你不必将自己代入到企业主的视角,在重重假设的基础上参与他们在舆论上的攻讦。

Apache 开源社群的“石头汤”

《程序员修炼之道》讲了一个有趣的“石头汤”寓言。这个寓言里,饿着肚子的外来人在村子里烧了一锅水,放了三块石头,开始煮“石头汤”。这样的行为引来好奇的村民围观,外来人顺势在“石头汤”的基础上引导村民们添加食材以改善这锅料理。最后,村民和外来人一起煮出了一锅靓汤,外来人于是把石头从汤里扔掉,所有人分享了这顿美餐。

开源协同的工作方式与制作“石头汤”的方式有些相似。开源社群的核心成员与寓言中的外来人一样,充当了催化剂的角色,将这些各自拥有不同背景的人群组织起来。这样,社群成员才能聚在一起做出他们单独无法做到的事情。最后,所有人都是赢家。

当然,在这个版本的“石头汤”寓言里,村民被外来人骗了,石头并没有为最终的美味产生直接价值。《开放式组织》指出这种行为是一次性的,并且价值仅仅单向地从村民一方流向外来人一方,以至于它被冠以“汤姆·索亚合作模式”的恶名。

开源协同的模式保留了“石头汤”寓言当中催化剂的内核,但是这一次,外来人提供的不是水煮石头,而是初具规模的汤底和食材。《大教堂与集市》在揭示集市模式的必要条件时阐述了这一点,这个隐喻意味着一个能运行的软件,并且让潜在的合作开发者相信,这个软件在可以预见的未来,能够演变成一个非常棒的东西。

Apache 开源社群由三百多个项目组成,其中不乏开源版本“石头汤”的现实案例。

Apache Hudi

Apache Hudi 就是这样的一个例子。实际上,就是近期几次引用 Hudi 的例子说明开源协同的工作机制的经历才促使我写这篇文章。

如果用一句话介绍 Hudi 的第一个版本做的事情,那就是写一个 Spark 程序,把数据从 HDFS 读出来,根据用户通过 upsert 接口传入的数据更新请求修改数据,然后写回到 HDFS 上。

就这么简单?

就这么简单。

众所周知,HDFS 的文件不支持随机读写,而数据分析的流水线上需要更新历史数据是个客观存在的需求,各个公司里同类型的 Spark 程序或者不用 Spark 实现相同功能的程序实现过许多遍了。这样的功能做一个平凡的实现,甚至有经验的工程师不出数日就可以写出来。

那么是什么让 Hudi 与众不同呢?答案就在“石头汤”的寓言里。

Hudi 的主要作者,也是现在项目的 PMC Chair Vinoth 敏锐地察觉到了这个需求的普遍性,并且相信跳出公司的局限,集合整个开源共同体的力量开发这样一个公共的需求对项目而言是最好的选择。因此,他推动 Hudi 项目从 Uber 公司的内部作品捐赠给 Apache 软件基金会,借助 Apache 的平台向每一个实现同类功能的开发者发出邀请参与协同。

虽然前面介绍 Hudi 的功能非常简单,但是其实从 Hudi 进入孵化器的提案当中可以看到,它在一个平凡的 Spark 程序以外,还实现了和当时的大数据生态的初步整合,可以通过 Hive 等现成方案和 Hudi 生产的数据进行交互,这就意味成熟的大数据生态和各种工具可以迁移到 Hudi 的用例上。

这两点对于一个新项目来说是至关重要的。如果没有可行的软件,只是一个想法,那么相比那么多公司内部实现的同类型程序,一个大家都能想到的想法毫无价值。如果作为一个大数据领域的解决方案,不能和大数据生态融合,那么没人会相信它能拥有光明的未来,大部分开发者会持观望态度而不是花费自己宝贵的时间参与协同,因为有这时间还不如改善自己已经实现的同类型程序。

然而 Hudi 做到了起步阶段这个小小的身位领先,并且紧紧围绕着用户需求开发功能、打磨产品和吸纳贡献。既然 Hudi 已经做好的工作我要花费数月才能追上,尤其是其中还包括了许多我不愿意做的“脏活”,那么我为什么不把自己想要实现而 Hudi 尚未支持的功能直接在上游实现呢?反正 Hudi 是 Apache 社群的项目,向上游做出的贡献我自己仍然能够随时用于任何目的。

这样的想法在 Hudi 项目孵化早期推动了诸如 @vinoyang@leesf 这样的开发者的参与。他们在 Hudi 的稳定性和可用性上做出了显著的贡献,而秉承开放和合作的理念的 Hudi 社群也很快吸纳他们成为项目 PPMC 的成员。

Hudi 相对于其他方案的小小优势,加上社群做出这样的表态,以实际行动实践开源社群 Meritocracy 的原则,很快聚拢起来一批有实力的开发者参与其中。这样的正向循环让一开始的小小优势逐渐扩展成今天数据湖领域相对于大部分其他解决方案明显的领先,这也进一步地让领域中潜在的用户和开发者被吸引到 Hudi 社群当中来。

2019 年开始孵化以后,开发活动与日俱增,甚至原始作者都不是最活跃的提交者

自然增长的 Star 代表的声量曲线近似二次函数

参与开发的人数曲线甚至接近指数函数

来自 T3 出行的开发者写出了 Flink on Hudi 的方案与最初实现,来自阿里巴巴的开发者将其完善到生产可用并且具有竞争力。新的 RFC 正在路上,实现一个 Hudi Server 来以内存状态读写取代目前开销显著的元数据文件读写,实现 Record 级别的 CDC 服务等等。数百个将自己的聪明才智和宝贵的时间投入到让 Hudi 变成一个更好的开源软件的参与者,组成了 Hudi 3000 个提交里一点一滴的改善。这正是“石头汤”里村民们从家中带来各自的食材,最终做出一锅美味的翻版。

这样的案例在 Apache 当中并不是唯一的。

Apache BookKeeper

Apache BookKeeper(BK) 从代码角度最早可追溯到 2008 年,当时的它是 Yahoo! 巴萨罗那研究院的研究项目。起初,其首要目的是解决 HDFS NameNode 的可用性问题,后来成为 Apache ZooKeeper 的子项目。2014 年年底从 ZooKeeper 社群孵化成为顶级项目。

BK 完全对等节点的设计使得它被许多寻找分布式日志存储系统的团队所青睐,进而被广泛使用在多个公司的不同场景当中。

  • Diennea 的工程师在开发 HerdDB 时使用 BK 存储预写日志。
  • Twitter 的工程师基于 BK 创建了 DistributedLog 项目,后者在 BK 上层封装了面向终端用户的分布式日志接口。后来,这个项目被合并回 BK 社群成为一个子项目。
  • Dell EMC 的工程师基于 BK 创建了 Pravega 项目,旨在提供流式数据的存储。目前是 CNCF 的沙箱项目。
  • Yahoo! 的工程师基于 BK 创建了 Apache Pulsar 项目,它是一个能够同时支持 RabbitMQ 式的消息队列语义和 Apache Kafka 式的消息流语义的云原生消息平台。
    • 后来,这个项目的创始成员成立了 StreamNative 公司来提供企业级的 Pulsar 服务。
    • 另外一家企业服务公司 DataStax 使用 Pulsar 来补齐其商业产品 AstraDB 在数据同步和数据变更订阅上的短板。
    • StreamNative 围绕 Pulsar 发起的 Kafka on Pulsar 项目吸引了来自腾讯和 DataStax 等公司的开发者的参与。

围绕着 BK 形成的庞大生态持续反哺着 BK 社区,使其在十多年后仍然能够保持强大的生命力和迭代活力。同时,虽然社群当中存在着不同公司背景的参与者,但是 Apache 的开源之道将所有参与者都认为是个体参与者,并且强调社群独立于其他组织影响的中立性。BK 社群和 Pulsar 社群都坚持了这样的原则,因此社群成员无论是什么背景,大都能够和谐友好地相处。

“石头汤”的寓言不只有开始的石头与结尾的美味,重要的是如何促使这个变化发生的过程。BK 和 Hudi 相同的地方,在于社群维护者都在初始项目解决了一个特定问题的基础上,向社群抛出自己合理的请求,然后不断完善。无论是来自用户需求的反馈,还是工程师设计的方案,一旦有成果产出,社群维护者会及时发布新版本以鼓励做出贡献的参与者并向全体社群成员展示最新的进展。

在这之后,社群维护者引导或者社群成员自发地提出“这个软件还可以更好,只要我们再完成……”的想法,就能清晰地在开发者当中传达出下一步可以做什么的信息。具体的待办事项好过一个模糊的愿景,开源共同体的开发者几乎总是倾向于加入到一个推进中的成功项目,而不是一个刚有设计的项目。架构设计和第一个版本是项目创始团队的责任,这也是开源协同与原版“石头汤”寓言的重要不同:如果你只是丢出两块石头,不会有参与者能够从无到有开发出整个开源软件。

Apache Kvrocks

Apache Kvrocks 是今年四月份进入 Apache 孵化器的项目,我是这个项目孵化期导师的一员。最后我想从这个项目出发,具体讲一个引导“村民”向“石头汤”当中添加“佐料”的例子。

Kvrocks 是一个 Redis 协议兼容的分布式 KV NoSQL 数据库,不同于 Redis 采用全内存存储,Kvrocks 的存储是基于磁盘的。不同于企业放弃维护后捐赠给开源社群的项目,2019 年发起自美图基础架构团队开发的 Kvrocks 早在 2020 年就开始以开源项目的形式运作,历经一年多的发展吸引到了来自百度和携程等公司的开发者的参与,并在国内外多家公司的生产环境上线部署。

恭喜Kvrocks 加入 Apache 软件基金会孵化器

上面这篇 Kvrocks 发布的加入 Apache 孵化器的文章当中几次提到,社群维护团队选择加入 Apache 的核心原因是“建立更大和多样化的开发者社区”。事实上,Apache 开源之道的指导和 Apache 品牌的帮助确实为 Kvrocks 打开了一个新的大门。

我在成为 Kvrocks 项目的导师之后,自然而然地参与到项目社群当中。如同这条推文提到的,接触一个新的开源项目,第一步就是克隆代码并尝试构建。我在构建过程 Kvrocks binary 当中发现了项目 CMake 脚本存在优化空间。这个时候,我想起来在《CMake 是怎么工作的?》文章评论区里 @PragmaTwice 分享了他使用 CMake 的一些经验,正好跟我想做的改进相符合。因此,我邀请他把他的经验实践在 Kvrocks 项目上。

很快,Twice 在我和 Kvrocks 的主要作者 @git-hulk 等人的帮助下系统地改造了基于 CMake 的构建逻辑,取代了此前 Git Submodule + Makefile 的方案。此外,Twice 出于自己对 C++ 编码实践的理解,在阅读源码的过程中发现了许多“这个软件还可以更好,只要我们再完成……”的点子。遵循开源社群一直以来的协同惯例,他把这些想法发布成若干个 issue 并自己开始实现。就在最近几周,他所发起的工作吸引到了更多开发者的参与。

Apache 孵化器的主席 Justin Mclean 总是建议孵化期项目在参与者做出具体贡献之后尽快授予他们 Committer 身份,以鼓励人们持续做出贡献。他所理解的 Apache 之道应该关注到绝大部分参与者的情况,出于时区、本职工作和陪伴家人等等原因,参与者并不总是全力为某个开源项目工作。

Kvrocks 的 PMC 成员基于这样的认识,结合 Twice 在六月上旬时已经完成的工作和表现出来的能力水平,经过 Apache 社群议事的标准流程投票通过邀请 Twice 成为 Kvrocks Committer 的一员。成为 Kvrocks Committer 之后至今的两周里,Twice 在保持原本的参与水准之外,更加积极地 review 其他社群成员的补丁,并协调不同 pull request 和合并参与者贡献的代码。

可以看到,Apache 开源社群激励参与者共同制作“石头汤”的具体方式,就是以参与程度和具体贡献回馈参与者相应的声誉和权威。

总结

软件行业的经典著作《程序员修炼之道》描述了一个“石头汤”的寓言,在开源社群当中也存在着类似“石头汤”的协作流程。不同于原始版本多少带点欺骗的味道,开源协同的模式强调最初的软件本身即是一个可用的软件。而与原始版本相同的是,开源协同与外来人制作“石头汤”时采取的策略重点都在于做推动变革的催化剂。

开源软件的维护者们也可以借鉴“石头汤”的魔法,在一个基本可用的软件的基础上,抛出可以做的更好的可能性,身体力行并团结潜在的开发者一起不断实现做出的预言,最终为行业制造出一个高质量的开源软件。

共同创造价值

如何吸引开源开发人员参与项目?如何让他们留下来,成为项目共同体的一部分?这是两个做开源运营必须回答的问题。

我对这两个问题的回答,简而言之是和开源参与者共同创造价值,使得开源项目和开源共同体能够回答潜在参与者的两个事关去留的灵魂提问。

  1. 我能为你做什么?
  2. 我应该怎么做到?

从共同创造价值的角度出发,通过开源运营回答参与者可以做什么的问题,只有可做的事情是令人兴奋的价值创造,才有可能触发潜在的参与者的兴趣。进一步,只有潜在的参与者能够在文档材料与其他成员的帮助下共同完成价值创造,这样的正向激励才能让参与者留下来,成为项目共同体的一部分。

本文内容部分整理自我在开源社举办的 COSCon’21 活动上的主题分享《Why Contributors Stay and Grow》的第二部分。

我能为你做什么?

考虑一个开源开发人员接触项目的典型旅程。他首先要知道这个项目的定位是什么,以决定是否进一步了解它。

如果项目的定位看起来很有趣,或者像是正在解决他希望解决的问题,那么进一步激发他参与贡献的诱因,就是他发现可以为这个项目做点什么。

如果这个项目复杂到无从下手,没有任何引导回答“我能为你做什么”这个问题的入口,那么大部分潜在的参与者都会选择直接放弃理解,也就无法参与。如果这个项目简单到不需要添加什么功能,例如 LineNoiseatoi-rs 等等,或者稳定到没遇到任何迫切的新需求,例如 ZooKeeper 等等,那么大家已经用得特别开心,也就少有参与贡献的动机了。当然,后者或许是件好事。

我们考虑一个蓬勃发展当中的项目,它足够复杂以不断产生新需求,它又很有活力以致力于把问题解决得足够好。在这样的项目里,潜在的参与者通常能够为项目做什么呢?从这个角度出发,也就能够知道哪些是可能的动机触发点,以拓展共同创造价值的故事。

代码之内的参与

开源项目最终生产的是开源软件,很容易想到围绕软件代码参与贡献。典型的代码贡献可以分成这四类。

  • 评审代码
  • 修复缺陷
  • 改进实现
  • 增加功能

首先要讲的是评审代码。

这是一种经常被忽略的参与动机。因为相当部分开源共同体,总是考核并引导参与者自己写代码和提交补丁,只把写代码当成贡献,而将评审代码视作一种不得已而为之的成本,甚至当做是 committer 和 maintainer 的特权。

这当然都是不对的。

实际上,code review 是开源开发人员之间交流技术和建立信任的主要手段。大部分软件的生产过程中,解决问题的办法往往不止一种。每个项目会在众多可能性当中选择自己认可的技术实践,形成自己的调性。通过代码和文档,以及生产代码和文档的过程,这种调性以共识的形式从核心成员同步到每一个参与者的认知当中。这里所说的生产代码和文档的过程,code review 就是其中重要的一环。

对于潜在的参与者来说,参与代码评审并提出问题,门槛较之提交补丁整体要低一些。当然,评审代码并留下意见建议,很多时候要求 reviewer 对软件的理解比补丁作者本身要更高。然而,这里所说的参与代码评审并不局限于针对补丁的实现提出形如补丁的补丁的意见建议,而是强调围绕补丁代码观察和讨论本身。

例如,PostgreSQL 共同体的 CommitFest 就允许任何参与者以 reviewer 的角色评审待合并的补丁。合并代码的动作自然只能由经验丰富的 committer 来执行,但是任何对这个补丁感兴趣的人,都可以参与评审。你可以针对实现提出问题,可以针对文档提出建议,可以本地测试补丁并回报结果,可以就自己不理解的地方,遵守提问的礼仪提出问题。当然,如果某个补丁解决了你的问题,或者实现得非常精彩,不要吝啬感谢、赞扬和鼓励。

TiDB 开发者指南当中有专门的一个章节,告诉潜在的参与者他可以通过评审代码的方式为开源共同体做出贡献。其中关于撰写 review comments 的建议值得在这里分享。

  • Be respectful to pull request authors and other reviewers. Code review is a part of your community activities. You should follow the community requirements.
  • Asking questions instead of making statements. The wording of the review comments is very important. To provide review comments that are constructive rather than critical, you can try asking questions rather than making statements.
  • Offer sincere praise. Good reviewers focus not only on what is wrong with the code but also on good practices in the code. As a reviewer, you are recommended to offer your encouragement and appreciation to the authors for their good practices in the code. In terms of mentoring, telling the authors what they did is right is even more valuable than telling them what they did is wrong.
  • Provide additional details and context of your review process. Instead of simply “approving” the pull request. If your test the pull request, report the result and your test environment details. If you request changes, try to suggest how.

另外,代码评审并不局限于补丁合并之前,也不是必须发表评论。

如果一个已合并的补丁激起了你的好奇心,或者你发现了其中存在的问题,完全可以 review after commit 即合并后评审。这对于高速发展的项目来说尤其重要,因为它们往往会在较短的时间窗口内合并补丁,乃至直接向主分支推送代码。代码评审不是形式主义,不像某些极力推行某种流程的人所宣称的必须在合并前收集到两个赞同才能合并,务实的评审和合并策略应该是持续发生、交错进行的。

不是必须发表评论,意思是所有的参与者在提交代码之前,都应该了解他所要参与的这个开源共同体的惯例,也就是常说的“入乡随俗”。这个过程一般也是通过浏览现有补丁来完成的,其实也算是评审代码的一类。我在撰写提交信息和实际提交补丁之前,往往都会先观察共同体当中的其他人是怎么做的,避免不必要的摩擦,提高协同的效率。如果当前流程有明显的缺陷,也是一个潜在的参与点。

修复缺陷、改进实现和增加功能,这三种都是代码贡献。

理想情况下,潜在的参与者具备代码贡献所需的技术背景,在使用软件或阅读代码的过程中,自己发现可能的改进项,仿照现有的 pull request 风格提交补丁。

这种情况也不算少见。常见的案例是参与者掌握了一项新技能,例如一种新的代码风格或设计模式,一种项目管理或持续集成实践,或者一种具有一定普遍性的功能,从而把已经完成的工作,带到其他缺乏这个改进项的项目当中。具体的例子,比如我在看到我的工作里引用的 atoi-rs 项目,没有按照 Rust 项目的惯例配置支持的 Rust 版本,同时功能开发趋于稳定但却没有发布 1.0 版本导致我不是特别敢用,就通过 issue 和 pull request 的方式直接把我掌握的技能复刻到该项目当中。

上面的例子里有一点值得强调,就是当潜在的参与者不确定能够为开源项目做什么的时候,可以直接询问。当然,这种询问应该是有的放矢的,询问之前应该对项目做基本的了解,而不是居高临下的要求维护者准备好甚至生造出待完成的工作并交给自己。

反过来,从项目维护者的角度出发,回答代码贡献层面潜在的参与者可以做什么的问题,能够采取的方式包括以下几种。

第一种,也是 GitHub 原生支持和倡导的方式,就是为 issue 标注 good first issue 或 help wanted 标签。我在夜天之书 #7 一文里讨论过这两种标签的使用方式。绝大部分 GitHub 的用户的心智模型能够适配这两个标签,并且首先会尝试寻找 good first issue 或 help wanted 标签标注的 issue 作为参与的切入点。

第二种,以 tracking issue 的形式组织起待办事项。通过将零散的 issue 和 pull request 按照主题和模块组织起来,以类似于重构和组织代码的形式管理 issue 和 pull request 来降低潜在参与者的理解成本。

这种做法全面推行,就会在顶层形成一个项目的路线图。例如 Flink 的路线图就把项目的模块分得比较清楚。每个模块现在是稳定状态,快速开发状态,还是原型状态也会标注出来。进一步的,列出每个模块当前正在进行的 Flink Improvement Proposal 和背景,开发人员就可以从这个入口了解相关工作,再从 proposal 关联的 tracking issue 参与到开发工作里面来。当然,这个过程里还有许多可以做代码之外的参与的切入点,这里不做展开。

路线图的更新频率比较慢,日常开发工作里接触得多的是 tracking issue 和对应的改进提案。成熟项目基本都会有一个完整的提案流程,从“我能为项目做什么”的角度出发,这个流程里包含的信息应该可以教会潜在的参与者分辨不同阶段的提案,并且理解当前阶段提案发起人需要得到哪些帮助。另一方面,关于 tracking 的组织,我做过一个视频《如何在开源社区做项目管理?》具体讨论的实践手法。

这些手段的最终目的是一致的。因为开发活动过于琐碎,单个开发活动的价值很难吸引潜在的参与者付出时间和注意力自己补全所需的细节。这种情况下,作为项目维护者和开发活动的发起人,应该尽可能地从多个层面以人能理解的方式把这些具体的开发活动归纳总结,以提供不同详略程度的内容呈现。

如同前文提到的,这个过程类似于重构和组织代码。你不能指望开发人员一上来就盯着每一行代码看,这样低效率的理解项目的定位、设计和发展方向。同样,你也不能指望潜在的参与者从大量琐碎的开发活动里归纳出项目共同体正在做什么,接下来要干嘛。只有把开源项目的开发活动模块化和主题化,才有可能把理解和参与的门槛降低到当前时代下潜在参与者所愿意付出的精力的范围之内。

这其实不止是开源项目的问题,而是一个普遍的项目管理的问题。把潜在的参与者换成项目团队的萌新成员,推理过程和结论一样成立。项目维护者采取这样的做法,也不是全然无私奉献,而主要是控制软件工程由于复杂度增加带来的熵。这对于项目本身的健康快速发展也是有利的。

代码之外的参与

开源共同体虽然是围绕开源软件形成的,但是可能的参与形式远不止于代码贡献。The Apache Way 经常被人引用的一点就是“共同体高于代码”。

Community Over Code: the maxim “Community Over Code” is frequently reinforced throughout the Apache community, as the ASF asserts that a healthy community is a higher priority than good code. Strong communities can always rectify problems with their code, whereas an unhealthy community will likely struggle to maintain a codebase in a sustainable manner.

其实,这里的 community 是包括 code 的,两者并非互斥关系,只是强调决策的基点是共同体的利益,而不只是关注代码。不过,我们首先看到开源共同体当中代码之外的参与形式。

第一类是使用。

开发软件的最终目的是投入使用,因此,使用软件并报告反馈,本身就是参与贡献。我见过不少数据库开发人员,并不了解数据分析师和业务开发人员等具体用户到底是怎么使用数据库的,生产环境里最常见的用法,最常被使用的 SQL 特性,沉浸在内核开发当中的工程师未必能够非常准确的把握。通过测试软件在生产环境下的表现,使用真实业务负载验证软件最终交付的价值,对开源软件的打磨价值都是不可忽视的。

TiDB 共同体非常重视用户使用。“我们已经用起来了”,是他们最喜欢听到的话。TiDB 的 AskTUG 论坛上每天都有海量的问题。这些问题大部分都比较初级,但是初级问题反复出现,意味着开源项目的文档建设和知识库建设不够充分,缺少一个快速上手的文档和常见问题的列表(FAQ)。除去这部分问题,进阶的问题能够描述清楚一个具体的使用场景和遇到的障碍。这类问题往往可以使用某种技巧绕过,这些技巧集合起来,就是使用软件的最佳实践。

前面提过,开发软件的最终目的是投入使用,而软件最终投入生产是一个复杂的过程,任何一个环节无法满足需求,没有绕过方案,都有可能导致用户放弃使用。而任何软件刚刚发布的时候,都是存在这样那样的问题的。通过使用软件并报告反馈,其实就是常说的“踩坑”过程,逐步把软件使用的“坑”给填平,开源软件触达更多用户也就成为可能。为什么 Java 生态比 C# 生态好这么多?为什么 Python 生态比 Elixir 生态好这么多?相当一部分原因就是在漫长的用户使用过程当中,许多未来用户可能遇到的问题,都被先行者给解决了。

对于具备技术能力的开发者来说,使用上游软件并发现问题,还有可能是深入参与的契机。例如我在改进 Apache Flink 的高可用模块的时候,深入理解了 Apache ZooKeeper 和 Apache Curator 的设计实现,并以此为契机为两个项目做出贡献,并成为 Apache Curator PMC 的一员。

另一方面,上面提到的旅程当中,除了使用者以外,还有一个角色就是答疑者。只有少部分提出问题的人,能够自己解决问题。大部分提出问题的人,都需要有另一个经验更丰富的共同体成员协助解决问题。

AskTUG 论坛有版主机制,赋予积极答疑并愿意承担一定协调责任的成员版主的头衔和权限。通过开放合作,AskTUG 论坛聚集起近十位版主,为解答 TiDB 系列产品用户遇到的问题发挥了中流砥柱的作用。可以从《TiDB 社区版主,一群平凡又伟大的 TiDBer》一文当中窥见一二。

Apache 基金会治下的项目共同体,往往也强调承担答疑职责的重要性和共同体对这种行为的认可。例如,Apache Flink 项目对 committer 候选人的第一个期待,就是能够积极回答用户问题

Community contributions include helping to answer user questions on the mailing list, …

第二类是发布。

发布一个软件并非易事,《Perlbrew 中文简介》一文中介绍了 Perl 5.8.8 到 5.10.0 版本之间的发布工作燃尽了两位顶级黑客的精力和热情的故事。

这个故事以发布流程的改进为结局,后来的 Perl 版本发布短到一个月内就可完成。不过,发布这一活动作为开源软件生命周期的重要一环这个事实,却从来没有改变过。PostgreSQL 共同体每次发布都会有一个两到三人组成的临时发布团队,并且整个发布流程都被记录在 Wiki 文档里。我在夜天之书 #20 The PostgreSQL Community 里有一整段详细讨论。Engula 项目第一份开发者文档,就是发布指南Apache 项目成熟度模型里,也对发布的质量详细区分了五个等级。

软件的发布是持续交付或叫持续部署的一环,涉及的专业知识不比开发软件核心功能少。一个项目有可靠的持续交付,下游用户才能基于它构建起繁荣的生态。

举个例子,我在分析 TiDB 软件工程上的问题的时候,就提出过发布过程和产物不够清晰,版本支持策略未定义等问题。现在的 TiDB 不像 Apache 项目有明确的文件服务器和稳定获取发布产物的地址,而是依靠工具来完成部署。耦合发布和部署不是一个好选择,尤其是在上一个部署工具生命不过三年新工具生命不过两年的的情况下。Bytebase 支持 TiDB 的旅程当中,虽然不乏对部署工具的赞许,但也指出了新工具只支持新版本的缺点。由于耦合,部署工具不支持,发布产物也就难以获取。另外,文中也直接明了地提出了版本策略不清晰带给下游的问题。

随着开源吞噬软件,融合不同组件的解决方案在交付时如何进行合规审计和安全审计也是一个日渐复杂的问题,这些问题都归属于发布这一开发活动当中。如果你是一个掌握软件持续交付经验的人才,从这个角度切入参与开源共同体将是非常犀利的,这也是当前不少开源项目求之不得的人才。

第三类是内容。

围绕开源软件开展内容创作,是几何级数乃至指数级数壮大开源共同体的秘诀。内容创作可以大体可以分为四个类型,即文档、博客、演讲和演示。

文档是最贴近开源软件的内容创作。实际上,不少开源软件把用户文档作为版本交付的一部分。TiDB 的用户文档经常受到赞扬,Kubernetes 的共同体文档也被其他开源共同体用作治理和运营的参考材料。

类似的例子数不胜数。一位经验丰富的技术写作者在交流过程当中曾经提到,好的内容创作能够简明扼要的抓住潜在参与者的注意力,在这个信息爆炸的社会当中为开源共同体赢得劳动时间的投入。

好的用户文档获取用户的注意力,提高软件普及程度,扩大开源共同体生存的基本面。好的开发者文档获取参与者的注意力,减少参与贡献过程当中的摩擦损耗,提高协作效率。好的愿景和价值观,能够聚拢起志同道合的参与者长期为项目发展持续做出贡献。

高质量的文档,必定基于对核心开源软件的理解,对受众的共情,以及文字写作的功底。Apache Pulsar 的 PMC 成员 Jennifer Huang 正是出于为 Pulsar 共同体创作技术文档等内容而收获认同的。

博客可以认为是文档的延伸。它是不那么正式的,具备个人风格的内容。同样,许多知名的开源项目,在文档之外都会维护一个博客列表。开源共同体当中也会传阅列表之外的优质博客。例如,我一下子就能想起来 Flink 相关的两篇让我获益匪浅的博客。

类似的例子也比比皆是,我在撰写 This Week in TiDB 期间也多次引用了共同体成员发布的高质量博客。博客的下一步就是书籍,例如 Eric S. Raymond 为 Linux 共同体做宣传的《大教堂与集市》。很明显,这些内容的传播效率较之软件代码、改进提案和用户文档本身,具有更好的传播效果。这一点还体现在中文社群当中多有对知名开源项目提案的解读上。

演讲的传播效果较之博客又要更进一步,因为它不需要受众阅读并理解文字的含义,而是通过视听体验直接获得感觉。Linus 的经典演讲相当之多,他在这些演讲当中表达的观点为 Linux 项目聚拢起志同道合的核心成员发挥了重要作用。

Flink Forward 大会,KubeCon 大会,COSCon 中国开源年会,越来越多开源共同体举办的峰会集中输出开源文化和开源软件的价值,以影响整个软件行业对开源软件的认识和使用。作为开源世界的一员,无论是参与演讲还是运营支持,以至于发起峰会,都是对开源共同体壮大的重要贡献。

最后要讲的是演示。演示可以融入到上面提到的三种方式当中。例如,文档可以加入快速开始的演示项目,上面提到的《基于 Flink SQL 构建流式应用》博文本身就是一个示例应用的讲解,演讲当中也经常插入演示环节增强表现效果。

单独谈论演示的原因,自然是因为它非常重要。在信息爆炸、注意力稀缺的今天,短视频以其能够快速抓住眼球,从而大量占用用户时间成为应用新贵。演示起到的就是一个类似的效果。例如,Perl 的代码诗在相当一段时间内吸引了众多开发者的兴趣和尝试。例如,TiDB 的热力图活动既强调了 TiDB 重视可观测性的调性,又足够有趣以吸引潜在用户试用。例如,太极图形的太极开物宣传视频就将它在计算机图形学领域的先进性和应用价值生动的传达给每一个观众,相当惊艳。

在软件开发越来越趋于复杂的今天,能够通过演示为开源共同体赢得更多的关注乃至劳动时间的投入,就是为开源共同体做出的重大贡献。

再谈代码贡献

如同我在《Why Contributors Stay and Grow》演讲里最后要回过来强调的一样,虽然比起代码贡献,代码之外的参与种类繁多,并且能够达到的传播效果远超代码贡献本身,但是开源共同体的价值,始终是建立在优秀的开源软件之上的,而开源软件,说到底还是代码支撑起核心功能,编译产物是交付物的主要内容。

我们在强调 Community Over Code 的时候,需要避免过犹不及。Community Over Code 不应该忽略核心成员的能力和影响力,开源共同体的价值核心,还是依靠这些核心成员尤其是写出最初的代码,一开始规范软件开发流程的人来定义的。用户使用和反馈也好,内容创作也好,前提是有一个好用的软件。虽然上游软件应该适应下游需求来创造,但是最终形成的开源共同体,还是上游定义的软件及围绕开发软件的共同体的价值观推向下游的。

这一点很好理解。创作 GNU 系列项目的人,通过软件及共同体传播自由软件的理念。Linux 选择了 GPLv2 协议,进而影响了整个 Linux 发行版的生态的调性。试图抛开软件,抛开具体的核心成员,定义一套任何人都可以采用的方案建设成功的开源共同体,是不切实际的。这就像是没有一个确保成功的创业法则一样。

只有深入参与到共同体的发展历程当中,通过长时间的投入赢得声誉,建立影响力,并且对软件的定位和共同体的价值取向有相当的理解,对参与共同体发展的核心成员乃至所有成员都有良好的联系,才能够结合具体的情况从共同体层面为现在该干什么,下一步该怎么走做出正确决策。

我应该怎么做到?

前面讨论“我能为你做什么”的问题的时候,针对每一种可能的参与手段,其实已经或多或少回答了“应该怎么做”这个问题。因此,本段不再对前文已经提过的内容做复述,而是从回答“我应该怎么做到”这个角度出发,给出一个结构化的答案。

我认为,要想解决潜在的参与者提出的“我应该怎么做到”参与贡献的问题,项目维护者应该从这三个方面入手。

  • 内容
  • 论坛
  • 聊天室

内容

前文已经花了不少篇幅讨论为开源共同体创造内容是重要的贡献。这种贡献一部分就体现在能够回答“我应该怎么做到”这个问题上。

不同于论坛和聊天室的形式,内容是一种较为单向的信息传递方式,即潜在的参与者单方面的接收内容(主要是文档)传递的信息。因此,内容应该着重考虑受众的体验,解答最为常见,答案最为明确甚至固定的问题。例如,共同体的角色划分和治理体系,共同体认可的价值观,报告缺陷、评审代码、提交补丁等活动的一般性流程等等。

以 TiDB 开发者指南为例,其第二章 Contribute to TiDB 就是我和张翔专门为了回答“我应该怎么做到”撰写的文档,从它的结构就可以看出我们是如何系统性地回答这些问题的。

  • Community Guideline
  • Report an Issue
  • Issue Triage
  • Contribute Code
  • Cherry-pick a Pull Request
  • Review a Pull Request
  • Make a Proposal
  • Code Style and Quality Guide
  • Write Document
  • Committer Guide
  • Miscellaneous Topics

其他成熟的开源共同体也有类似的材料。

更不用说称得上“卷帙浩繁”的 Apache 基金会的文档和 Kubernetes 共同体的文档。

这些文档,加上共同体当中热衷于布道和培养新人的成员在博客和演讲上的内容分享,是潜在参与者了解如何具体参与一次贡献的直接材料。你正在阅读的这篇文章本身,也作为内容有着回答应该怎么参与贡献的作用。

论坛 + 聊天室

上面提到,文档和博客等形式,潜在的参与者作为被动接收方往往只能单向地获取信息。然而,真实世界当中参与开源共同体的活动,往往每个人都会遇到对于自己独一无二的问题。这些问题不够普遍,所以并不能在解决一般性问题的文档上找到答案。为了解决参与贡献旅程上“最后一公里”的问题,我们需要一个对等的沟通工具。

我在夜天之书 #16 Open Discussion 一文里介绍过 TiDB 共同体解决这个问题的心路历程,这个经验应用到建设 Engula 开源共同体当中,就是 GitHub Discussions + Zulip 即论坛 + 聊天室的模式。

其中论坛是唯一信源,也就是说,类似于 Apache 共同体强调的一切都发生在邮件列表上,所有共同体成员只需要关注论坛这一个渠道,就应该能够接收所有必要的信息。同时,在论坛上提出的问题,应该具有最高的响应优先级。

通过定义唯一信源,信息交互才能在当下这个沟通工具琳琅满目的环境下以一种尽可能低消耗的方式进行。否则,每个人都有他喜欢的沟通方式,甚至是非持久化或非公开的方式,例如不录屏的视频会议,或者微信聊天等等。如果在这些地方的讨论也可以作为信源,那么共同体当中发生的事情,将有可能只是一小部分人未经记录的部落共识。这种共同体当中具有排他性的小团体,对信息对等的流通是极其有害的。

回到解决“我应该怎么做到”这个问题上来,信源的分裂也将提高潜在的参与者理解到底应该在哪里解决他“最后一公里”问题的难度。也就是说,解决“最后一公里”问题的路径,本身成为了“最后一公里”问题的一部分。

选择论坛而不是邮件列表,体现了用户喜好的差异。其实对我来说,论坛和邮件列表的价值是一样的,无非是共同体成员更喜欢哪一个用户界面罢了。类似的,聊天室之于论坛,是用户沟通习惯上的差异。IRC 和 Zulip 在我看来也是一样的,无非是共同体成员更喜欢哪一个罢了。只要指定一个,就可以减少工具数量爆炸带来的信息熵。

不过,聊天室作为即时沟通工具,较之论坛或者相对更为正式的邮件,在即时响应问题上有独特的优势。一两句话能解决的问题,即时通信工具更加轻量级。模糊概念的讨论,即时通信工具能够更好地完成实时的头脑风暴。前者的典型例如 Akka 的 Gitter 聊天室,后者的典型例如 Rust 的 Zulip 聊天室

但是,聊天室核心的问题就在于它的信息密度太低,一旦超过一百人,昨天的消息基本上就已经完全归入历史,要想重提就得从头再来了。虽然现代聊天室大多支持主题化讨论的功能,但是较之论坛的设计理念,还是比较薄弱。因此我在设计沟通工具方案的时候,总是以论坛作为唯一信源。同时在选择聊天室方案的时候,总是选择公开的聊天室,选择支持主题化讨论的聊天室,选择支持引用内容链接的聊天室。支持引用内容链接,就可以在论坛当中直接链接到聊天室的特定讨论串当中,从而打通两边内容的交叉引用。这样避免了大量内容存在于聊天室当中,削弱论坛定位的情况,既能维护论坛唯一信源的定位,又能利用即时通信的优势。

开源共同体的治理模型

随着越来越多新的要素进入开源领域,如何建立一个高效的开源共同体,如何维护一个富有价值的开源共同体,逐渐成为每个参与者或多或少关注的问题。

Apache 软件基金会为每个项目提供了基础的治理原则,并在项目孵化到顶级项目的过程中通过孵化器导师的帮助建立起开源项目的治理模型,但是这一模型对每个具体项目在特定时期未必是最优的。同时,其他不在 Apache 软件基金会治理的项目,虽然拥有灵活设计治理模型的自由,却也时常陷入不知道该如何开始的窘境。

本文从开源治理的目的出发,介绍一个开源共同体什么时候需要考虑设计治理模型,然后讨论开源治理的原则,结合实例分析如何设计开源共同体的治理模型。这里所说的开源共同体,主要指的是围绕单一项目或单一主题项目群形成的开源共同体。

开源治理的目的

讨论开源治理的首要问题是,对于特定的开源共同体,当前阶段下是否需要治理。这个问题的解答可以从《社区运营的艺术》一书中对相同问题的讨论得到。书中认为,需要开源治理的主要原因包括

  • 社区规模激增
  • 冲突越来越多
  • 资源多
  • 商业利益

总的来看是两个问题。其之一是参与者和资源增加以后,维持开源共同体高效运转的挑战;其之二是不同背景的成员加入,不同要素进入之后,维护开源共同体的核心价值,也就是生产满足目标用户的高质量软件。

建立高效的开源共同体

开源共同体围绕开源软件而建立,其主要目的就是制造高质量软件,以满足目标用户的需要。这意味着生产代码、撰写文档、解答问题和市场营销都是开源共同体工作内涵的一部分。

项目启动阶段,尤其是功能尚未丰富的时候,交付核心功能是第一要务。这个阶段,往往项目参与者不过几个人甚至就是一个人的工作,也不会有太多的关注者和用户。这个阶段是不需要引入复杂的治理模型的,简单的项目创始人掌握所有权力并决策所有事务即可。没有核心开源软件创造价值而沉迷于设计治理模型,只不过是空中楼阁。唯一跟治理模型沾得上边的,就是留心并记录这个阶段当中软件开发的流程和决策惯例及案例。

随着项目核心功能的实现和迭代,产生的价值能够吸引到目标用户使用,更多的参与者被项目吸引或用户需求所推动着了解项目。同时,开源项目的成长也带来了对资源的需求,例如持续集成的基础设施,沟通渠道的维护,直接支持项目开发的资金捐赠的处理,围绕开源软件的活动和宣传的运营和开销,等等。新成员的加入带来沟通成本的提高,丰富的资源需要一定的流程和负责人维护才能合理得到使用。这个时候,设计一个治理模型以维持开发效率和决策效率,就是有必要的了。

治理模型通常体现为一个或几个治理机构及其职责和沟通方式。在维持高效的开源共同体的话题上,这些治理机构主要解决以下问题。

  • 批准或拒绝新成员加入。特别地,你可能为成员定义不同的身份,例如一般成员、开发者或其他贡献者,等等。
  • 解决冲突。
  • 明确项目价值,落实开源共同体的核心价值观。
  • 修改流程。
  • 如果开源共同体日渐复杂化,治理机构可能需要派生出新的治理机构。
  • 决定方向。

维护开源共同体的核心价值

如同前文所述,开源运动热火朝天引得越来越多新的要素进入这个领域,政治要素和资本要素就是其中不可忽视的两个。许多开源共同体都有商业赞助商和投资者。这些赞助商的员工当中往往有不少是开源共同体的参与者,通过参与做出对赞助商有利的贡献。有时候开源共同体只有一个赞助商,它可能就是一开始创立项目的企业。这种情况下,赞助商和投资者会主动寻求建立某种治理模型,以明确自己为开源共同体做出的投资所能得到的回报的预期。

例如,《社区运营的艺术》提到,Linux 发行版 Fedora 和 OpenSuSE 对应的赞助商 RedHat 和 Novell 在各自的开源共同体当中都通过制度保证了足够多的席位以推行任何他们想要推行的主张,而 Ubuntu 对应的赞助商 Canonical 仅在所有七名成员中保留一个席位且其他成员多数不是为该公司工作的。国内首个开源基金会开放原子开源基金会,其技术指导委员会的席位为主要捐赠人每家预留一位。TiDB 设计的技术委员会成员,必须是 PingCAP 员工、PingCAP 社区伙伴员工或社区项目代表。

反之,Apache 软件基金会则强调 Community of Peers 即共同体由个人组成,而非组织。Kubernetes 虽然会展示成员的组织关系,但是治理模型当中也不体现赞助商或投资者的特权。

《社区运营的艺术》提到,一般的志愿者社区,比如开源项目,赞助和投资方应该不参与治理机构。

我这样说并不是因为商业赞助是不可信的,而是因为与志愿者关联的社区往往是建立在所有成员的贡献之上的,而这些成员积极贡献的目的,就是保证他们的辛勤工作有助于同侪和社区的未来。

《People Powered》当中进一步解释了这个论点。

你的社区受到两个因素的驱动,一个是成员自身的利益,另一个支持社区获得更大的成功。除组织内部社区等极少数外,你的社区成员为社区工作,而不是为你工作。这常常使那些坚持“如果公司成功,社区也将从中受益”的公司受挫。这句话没错,但不重要。这不是大多数社区成员的想法,也不是社会经济运作的方式。

为了应对赞助和投资方的提案,协调开源共同体当中各方的需求以达到共同体利益最大化,避免分化,需要设计一个相应的治理模型。

开源治理的原则

不同开源软件的特点,开源共同体的成员组成和所处阶段的差异,都会影响治理模型的设计。我在 夜天之书 #18 Evolving Governance夜天之书 #25 Evolving TiDB Governance 里已经讨论了特定开源共同体的治理模型设计权衡。

本文尝试从开源治理的原则出发,讨论根本的设计思路。开源治理的原则总的来说是以下两条。

  • 开源共同体利益至上
  • 摈除丑陋的形式主义

责任感

要想落实“开源共同体利益至上”的原则,必须保证治理机构的成员对开源共同体有一种真正切身的责任感。

与其说开源治理是制定一系列规则并任其自动运行,不如说是找到具备领导能力且愿意为共同体发展承担责任的优秀人才。开源共同体当中常见的角色 Committer 抛开其提交代码的权限,词源本意即“做出承诺的人”。

开源软件由志愿者开发,在志愿者环境中是无法强制一个人参与的。有些志愿者能够每天都投入到共同体工作中,而其他志愿者仅在情绪不错时参与工作。后者的贡献值得感谢,但是他们通常不能承担治理责任,也不具备领导能力。对于围绕开源共同体做出的重大决策以及合并补丁和发布版本等核心活动,掌握权限的核心成员必须维护开源共同体利益,必须显示出强烈的责任感和使命感。换句话说,核心成员要对自己的行为负责,当开源共同体为了完成某些任务而全力以赴时,核心成员必须腾出时间来为之工作。

《社区治理的艺术》提到,责任感是宝贵财富,它相当于你在社区得到的承诺宣言。一些成员会有这样的责任感,而另一些则没有。有责任感的人是宝贵的资源,你可以委以重任,但是不可以利用他们的责任感。

例如,我在多次和 Apache Flink 的 PMC Chair Stephan Ewen 沟通的过程中,都能感受到他对项目的热忱。这种热忱支持着他十多年来投入到项目的开发、发布和宣传当中。即使自己所创办的基于 Apache Flink 提供服务的公司已经被收购从而财富自由,即使面对诸多邀约有不少早期成员开始投入新的项目,他却始终坚持发展 Apache Flink 以满足用户的需求。例如,Linus 在 2022 年伊始就在合并提交到 Linux 项目的补丁。例如,Apache SkyWalking 的创始人吴晟和 ClickHouse 的创始人 Alexey Milovidov 都以快速处理开源项目当中的评论、议题和补丁著名。

项目的创始人通常是对项目抱有最高责任感的人。因此,在设计治理模型的时候,直接把创始人排除在外是不可接受的。《大教堂与集市》当中讨论“Locke 及土地所有权”的时候也提到过这个模型。然而,不少形式化开源的项目,指派了一个或几个所谓的“社区运营专员”来架设治理模型,往往就把创始人反而排除在外。这是彻底的形式主义,也是错误分工带来的恶果。

Zoom.Quiet 对此有过评论,

这其实才是最大的天劫。项目开始时,大家的目标是不言而明的,因为是小团队多年的相同愿望。但是,有新人进来时,才发现没有之前多年的共事根本无法简单说清。所以,有所谓专家或布道师来接替创始团队来解释时,核心成员如果不认同,嘴又笨,直接表现就是不活跃了。看起来,是更加专业的专家替代了土领导,可其实,社区已经空心化了。毕竟不是谁都有 Linus 的嘴炮能力。

不得不感慨,Linux 能成功,Linus 功不可没。《时代周刊》做过一个有些极端的评论,但不无道理。

有些人生来就注定能领导几百万人,有些人生来就注定能写出翻天覆地的软件。但是只有一个人两样都能做到,这就是林纳斯。

创始人的缺位会给开源治理留下很深的阴影。例如,Rust 项目的创始人 Graydon Hoare 并没有坚持做 Rust 项目,核心成员 Brian Anderson 和 Niko Matsakis 还有 Alex Crichton 等等都不在最高治理机构 Core Team 里。同时,Core Team 的人员更替并不谨慎,导致部分成员难以服众。这是前不久 Moderation Team 和 Core Team 矛盾的深层原因之一。

核心成员的传承也要基于责任感。例如 Apache 软件基金会现任董事会成员 Justin Mclean 和吴晟,都非常关注孵化器当中新项目的加入和成长,关注 ASF 项目面临的法务挑战,关注 Apache License 的采用情况,等等。例如 2020 年新加入 PostgreSQL 的 Core Team 的成员都是经年参与 PostgreSQL 共同体的志愿者。

可以说,只有核心成员对开源共同体有切身的责任感,才能践行“开源共同体利益至上”的原则,其他成员也才能信任这样的治理机构能够领导和鼓励共同体向前发展。

精英领导制

这里提到的精英领导制是一种组织管理体系,成员在共同体当中受到的尊敬和能够承担的职责,取决于他所做的贡献,而不是金钱、社会阶层和家庭关系。换句话说,就是“唯才是举”的理念。

要想落实“开源共同体利益至上”的原则,强调精英领导制是有益的。当然,对于这个词感到陌生的人,可以多谈论一些平等性,多提供一些成员如何通过自身的努力,去建立声望的例子。

精英领导制强调了对开源共同体的贡献是赢得权威的唯一方式。这种贡献的衡量与个人所绑定,与其所属的组织无关。避开资本要素的影响,使得开源共同体能够最大限度的接纳新的参与者,并通过对参与者贡献的衡量和他们与其他核心成员共同工作的方式来选拔新的核心成员。

不同于彻底的独裁治理模型,精英领导制实际上暗示着项目由多人共同治理,而加入治理机构的核心成员必须通过其贡献和工作方式赢得其他核心成员的认同。这种方式在 Linux 项目当中也被部分采用。虽然 Linus 仍然是名义上的独裁者,但是 Linux 的维护者列表早就突破了一千人。当然,他们并不享有和 Linus 一样的权利,但是他们获得对应职责的基础,也是对相应模块的贡献。

这样,我们就确定了项目所有权的分配方式,即完全通过贡献来衡量。衡量者是现有的核心成员。建立项目自然是最大的贡献之一,持续参与,软件重大改进,催化参与热情与质量,完善文档,代表共同体或项目发声,都是值得考虑的因素。

例如,PostgreSQL Core Team 现在和过去的成员做出的贡献在其 Contributor 页面上展示。

核心成员主要贡献
Peter Eisentraut构建系统,软件移植,文档及其国际化,持续的代码贡献。
Andres FreundJIT 机制,逻辑解码,数据复制,性能和可扩展性,修复 bug 和 review 补丁。
Magnus Hagander完成向 Win32 系统的移植,鉴权机制,维护项目网站和基础设施。
Jonathan Katz项目推广,版本发布新闻稿,维护网站,作为其他 PG 相关委员会成员的贡献。
Tom Lane所有方面。其中包括评估缺陷和修复,性能改进,新功能落地。同时是优化器模块的责任人。
Bruce Momjian维护项目 TODO 列表,处理补丁,代表项目在会议上发言。
Dave PagepgAdmin 的作者,维护项目网站和基础设施,以及安装包。
Josh Berkus项目推广,用户群组推广,性能测试,调优和文档撰写。
Marc G. Fournier协调员,管理项目网站,邮件列表,文件服务器和代码仓库,等等。
Thomas G. Lockhart文档撰写,实现数据类型尤其是时间类型和几何类型,SQL 标准适配。
Vadim B. Mikheev重大功能实现,包括 WAL 和 MVCC 等功能。
Jan Wieck数据复制系统 Slony 的作者,实现了一系列重大功能。

可以看到,代码贡献是主轴,但也包括了维持项目运转和扩张的其他多层面的贡献。

精英领导制还有另一层含义,就是任何人都可以参与到技术讨论,技术实现和共同体决策当中来。虽然核心成员承担决策的职责,但是他们并不形成寡头政治。也就是说,核心成员的身份及其承担的职责是一种权力,而非特殊的权利。在一个健康的开源共同体当中,任何成员都可以做技术讨论,提交补丁,也可以就共同体发展话题提出自己的观点。技术最优的方案应当获胜,共同体利益最大化的决策应当通过。

精英领导制也并非是完美的。《精英的傲慢》一书当中提到,无论怎样强调“唯才是举”,精英的成功与天赋和运气等因素都是有关的。过分强调精英领导制,会助长赢得权威的人认为一切都是自己所应得的,对于没有赢得权威的人,则会感到屈辱和被排除在外。

《精英的傲慢》主要讨论的是社会问题,但是对于开源共同体这样一个特殊的群体,也是有启示的。我们前面强调了权力而非权利,即从根本上结构了共同体当中赢家与输家的关系。不过,不同身份的存在仍然有潜在的冲突风险,因此在选拔核心成员的时候,也要注重他对于权威的认知和个人的品质。

例如,《大教堂与集市》一书当中提到了谦逊的价值。

谈吐柔和也是有用的,如果某人希望成为一个成功项目的维护者,他必须让社区信服他良好的判断力,因为维护者的主要工作是判断他人的代码。谁愿意将代码贡献给一个明显不能正确判断自己代码质量的人?或者一个试图从项目中沽名钓誉的人?潜在的贡献者希望项目领导人在客观采用他人代码时,能够谦逊而有风度地说:“是的,这个的确比我的代码好,就用这个了。”然后将荣誉给予应得之人。

最后,由精英领导制建立起来的治理机构应当能够自省,以永远保持质量,代表开源共同体的前进方向。这不意味着要对每个成员限制任期,如果有人能把开源共同体治理得很好,应该允许他们一直干下去。但是,根据核心成员参与共同体各项活动的程度,工作情况的综合评价来判断是否应该取消某位成员的资格。

例如,PostgreSQL 共同体将在每年的 pgCon 峰会上发布新一年的 Committer 名单,不再活跃的成员将被移出。

坦诚沟通

开源共同体要想高效运转,坦诚的沟通是必不可少的。

例如,前文提到的取消核心成员资格这样的决策,如果没有事前沟通,不仅当事人基本不可接受,而且其他成员将笼罩在无形监视的阴影下。例如,前文提到的 Rust 共同体 Moderation Team 和 Core Team 的冲突,没有任何关于细节内容的披露,每个人都在说那里有一些问题,是什么问题不方便说,谁牵扯其中不方便说,后续要如何处理不方便说。这样的表现只会让人对这两个治理机构都丧失信心,无法信任他们做出的决定是公平的。

坦诚沟通依赖相应的渠道。Apache 软件基金会要求所有讨论都发生在邮件列表上,因而是公开可追溯的。我在设计 Engula 共同体的沟通渠道时,根据项目的特点,把 Single Source of Truth 设置成 GitHub 平台,包括 issue 和 pull request 以及 discussion 论坛。所有其他渠道沟通的结果,都必须回到唯一的渠道上再次确认。通过这样的设计,能够支持全球性开源共同体的异步协作,并且保证所有信息都是公开的,也就消除了关于决策缘由的顾虑。

实行坦诚沟通的好处不必多说,绝大部分人都希望得到真实的信息,收获真实的反馈。真实的信息帮助共同体成员为最大化共同体利益努力,真实的反馈帮助共同体成员正确地行事。不过在设计治理模型的时候,关于沟通有两个点需要强调。

第一个是坦诚不代表攻击性。《不拘一格》一书中强调坦诚沟通的重要性,但也提示组织管理者需要警惕“有才华的混蛋”。

如果周围全是聪明人,你可能就有危险了。有时候,有才华的人听到的赞美之词太多,就会觉得自己真的比其他人更优秀。如果有他们认为不明智的想法,他们可能会报以嘲笑;如果有人发言不够清晰,他们可能会翻白眼;他们还会侮辱那些他们认为天赋不如自己的人。换句话说,这些人就是浑蛋。

如果你在团队中倡导坦诚的文化氛围,就必须把这样的人剔除出去。许多人可能会认为“这个人确实很聪明,没有他不行”,但是,不管这样的人有多么出色,如果让他留在团队里,你营造坦诚氛围所付出的努力就不会有太好的效果。浑蛋对整个团队的效率有很大的影响,他们可能会将你的组织从内部撕裂。因为他们老是喜欢中伤同事,然后丢下一句:“我这是坦诚。”

对于开源共同体而言,这就意味着需要切实践行行为准则(Code of Conduct),对这类攻击性行为说“不”。

额外多说一句,不少共同体从 GitHub 或者推崇的其他项目当中拷贝了行为准则,但是核心成员可能都不知道行为准则当中写了什么。这是不可接受的。我在撰写 Engula 共同体的参与者文档的时候强调了行为准则的重要性,并认为如果一个人想要成为核心成员,必须了解和实践行为准则。否则任何的文档不过是一纸空文。

第二个是必要的隐私渠道,包括安全问题报告、违反行为准则报告以及选拔成员讨论等等。安全问题不必多说。后面两者涉及到对人的评判,在得到合适的结论之前不应该全部披露。

这是因为,根据精英领导制的精神,唯才是举的评判应该来自于现有的核心成员团队,而不是民粹主义式的公投。一个开源项目会发展成什么样子,取决于核心成员是什么样的。他们是最有话语权的一群人,应该对项目抱有切身的责任感。如果核心成员团队被腐化了,或者他们本来就抱持着其他目的,那么这个项目就完蛋了。核心成员承担项目所有权,为项目的发展负责。如果其他成员认为核心成员违背了开源共同体的核心利益,标准做法是发起分支。

不过,决议的内容应该公示并向共同体成员解释原因。这就意味着秘密推举新的核心成员是不可接受的,同样,以莫须有的罪名惩罚共同体成员也是不可接受的。

适应性

适应性体现的是“摈除丑陋的形式主义”的原则。也就是说,治理模型的设计应该倾向于适应开源共同体的发展,而不是做空中楼阁的设计。

大多数开源共同体需要应对三类问题,即综合治理,确定方向以及专业化治理。综合治理指的是围绕共同体的综合性话题做出决策,例如如何加入共同体,资源和基础设施的维护,工作流程,治理的改变等等。确定方向指的是围绕共同体的目标、愿景和当前焦点做出决策,例如开发软件所要解决的问题,目标用户,以及当前所要关注的特性集等等。专业化治理适用于开源共同体成长到一定规模以后,针对特定的专业知识领域需要专业的治理。例如,用户文档体积膨胀,而恰好有技术写作人才的参与。例如,Rust 的 Reddit 频道有自己的组织体系。例如,针对代码补丁的评审和缺陷的评估解决,也是专业化治理的一类。

对于一个刚起步的开源共同体来说,所有方向上的问题只需要一个治理机构来处理就够了。因为此时并没有太多的事情要处理,同时你也没有太多的成员能够参与到治理当中来。

不少设计开源共同体治理模型的人,盲目参考 Apache 软件基金会,或者 Kubernetes 等项目的玩法,货物崇拜式的对其治理模型进行像素级拷贝,最终发现并不适合自己项目的情况,而饱尝苦果。这其中最典型的就是引入多余的设计。预设具体的参与路径或治理细则,往往会导致无端的“运营成本”。随着项目的演进,参与情景的改变,预设前提不再生效,需要频繁修改,而并没有人实际践行。也就是说,所有设计和修改的付出都白费了。

《社区运营的艺术》提到,在理想的世界里我们不需要把治理放在首位,更不用说设置附加的分委会。每增加一层治理,你的社区就会丧失一些让大家易于理解的简单灵活性。只有在绝对必要时,你的社区才应设置附加的治理。

只有当现有的治理机构不能扩展或满足社区需求时,附加的治理才是必须的。然而,有些社区不太明白这个道理,决意设计空洞的委员会:无非就是使新委员会的成员感到很特别。

…他们希望有一个治理委员会。在他们眼中,治理委员会能带来各种有趣的东西:对项目的控制感、权利和权力。不仅如此,他们认为一个社区之为社区,真正的社区委员会是必不可少的。

胡说。

此类委员会的成立根本没有理由。社区委员会的存在是为了解决问题,但是如果整个社区相对比较小,就没那么多问题需要解决。创建这个委员会,你实际上增加了官僚主义的风险。

一个典型的例子就是单一项目阶段切分出许多模块团队,并为之建立起复杂的治理层级。我在此前讨论 TiDB 和 TiKV 的治理经验和教训的时候已经提到过,这种方式不过是割裂开源共同体而已。这种分割主要的缺点是很难切得恰到好处,出现问题讨论和修复带来的治理开销太大。如果不修订,实际情况一般是团队成员权限不足,核心团队不停兜底。

例如,早期 TiDB 将代码分成 DDL 和 Planner 还有 Execution 三个部分,那么数据库权限的问题谁来处理呢?一般性的并发实用函数,数据格式等等问题,谁来解呢?后来又把 DDL 团队泛化成 SQL Infra 团队以期兜底,但是实际上他们还是只处理部分问题。例如,TiDB 的 telemetry 模块目前就是无人负责的。

实际上,代码的关联是普遍存在的。哪怕是 Apache Flink 这样可以区分成 Runtime 和 DataStream API 以及 Flink SQL 的项目,也不需要单独区分出若干个团队来分散治理。Apache Hadoop 200 多名 Committer 完全依靠工作流程和负责任的态度,以相同的权力推进代码的演进。PostgreSQL 的 28 名 Committer 也没有再细分。

区分团队的项目,往往与不同的资源和关注点关联。例如,Apache 软件基金会下的每个项目都有自己的项目管理委员会,这其实就是某种意义上的团队区分,但是没人会觉得这增加了形式主义,因为不同的项目之前确实是不同的。

例如,Rust 共同体区分了若干个团队。虽然我对其中的 Release Team 保持怀疑,因为 PostgreSQL 可以很简单地由核心成员担任,ASF 项目通常也由志愿者根据发布手册担任特定版本的发布负责人,但是其他的团队区分是很清晰的。例如对于一门语言,编译器和标准库分开是没有问题的。中央资源库有独立的维护人员也是合理的,他们完全不需要关心编译器和标准库的问题。不过,我相信在真正出现这些团队独立的志愿者之前,也不需要分化出特定的团队。换句话说,因人设岗在开源共同体当中非常常见。同时,一个志愿者负责多项工作也是正常的,比如 Nick Cameron 就同时是编译器团队和开发工具团队的成员。

不要在没有人的时候首先架设出复杂的治理机构,因为没有人实际担任这些角色,空洞的治理体系是可笑的。同样,复杂的治理机构无法拯救空心化的开源共同体。比如,TiDB 有 60 余名 Committer 或 Maintainer 成员,但是真正处理 community 事务的人是不足的。前面提到的 Execution 团队的活跃成员全是 PingCAP 公司同一部门的员工,经过公司组织调整后工作重点改变,也就导致对应模块的治理立即缺失。空心化的开源共同体无法响应参与者,因为实质的有价值的回应来自于人与人的沟通,而非机械化的流程。

出于关注点不同的团队划分,还有一个值得考虑的问题是应当采用虚拟组织或者实际组织。Kubernetes 设计的 SIG 和 WG 就区分了这一点。SIG 作为实际与权限相关的团队,前面已经讨论过了。我在分析 TiDB 的治理模型的时候讨论过 WG 模型在 Kubernetes 共同体当中的采用情况。

Kubernetes 社区的影响力和专注于社区运营的人数保证你的 WG 有人关注,有人推送,有人宣传,再不济任务结束或者生命终结可以回收。另外,Kubernetes 的 WG 都是非常 General 的,例如可靠性工作组,命名工作组,策略工作组,等等。TiDB 和 TiKV 对 WG 的理解基本停留在功能特性小组的层面。这种需求通过项目内的功能设计提案和 tracking issue 等形式发起和追踪即可。

Rust 共同体当中也有同样的设计,例如最近 Nick Cameron 大力推进的异步编程工作组,这一工作需要协同编译器和标准库以及三方库等多方努力才能在 Rust 生态当中提供一个统一且高效的异步编程抽象和灵活适应不同负载的多种实现。

Engula 共同体考虑未来发展的时候,讨论了区分不同团队的思路。我在主题下回复的建议就是针对不同的模块关注点,可以尝试组织起不与权限挂钩的兴趣小组,针对特定的主题,可以组织起工作组。对于权限本身的区分,由于共同体规模并不大,适当扩充核心成员即 Maintainer 的人数达到复数即可。否则,在接口不稳定的情况下,跨越当下设定的模块的变更非常可能发生。另外,对于讨论中区分出来的日志、存储和计算引擎三个模块之外,部署逻辑、异步实用工具等等实际存在的功能又该归属到哪个团队呢?还是前面提到的,“切分主要的缺点是很难切得恰到好处,出现问题讨论和修复带来的治理开销太大。如果不修订,实际情况一般是团队成员权限不足,核心团队不停兜底”。

那么,什么时候应该考虑扩大治理机构的规模呢?这里提供几个方向性指标。

第一个是遇到瓶颈。

如果开源共同体当中的事务变多或变复杂,导致当前的核心成员团队无法处理,那么就有可能要考虑扩大治理机构规模。例如,Linux 的 1000 多名拥有部分提交权限的维护者,为 Linus 分担了大量与具体驱动或架构相关的模块的补丁审查。例如,C++ 委员会面临方向统合上的问题,于是成立方向委员会来应对。

不过,并非遇到的所有瓶颈都需要通过扩大治理机构来解决。比如,流程事务繁多可能是流程文档不足或者流程本身有形式主义的问题,通过改进流程或取消流程即可解决瓶颈。另外,PostgreSQL 绝大部分工作是由 7 名 Core Team 成员和 28 名 Committer 处理的,如果你的核心成员有同样的能力,那么可能也不需要刻意扩大规模。

我在参与 Engula 共同体的过程中也不断重复这一观点,即“如无必要,勿增实体”。如果当前项目创始人足以处理所有事务,那么就不需要形式主义地扩充成员,更不需要设计复杂的治理机构。大多数人认为治理既不酷也不有趣。我们的目的是,在保持想要达到的质量和客观性的同时,将治理的基础设施和文件的数量保持尽可能少。

第二个是关注点分离。

如果一个无关权限的兴趣小组或者工作组最终围绕特定领域形成了一个大的子共同体,这个子共同体可能需要一些特定的知识,而这些知识是核心成员所不知道的。这个时候,就可能需要把独立出这部分决策权。例如,Rust Team 有 Community 团队,Kubernetes 有 Community 团队,甚至你把整个 Linux 共同体视作一个整体,Linux Foundation 也是一个独立的团队。

关注点分离强调的是事后追认,也就是真的有分离的需求的时候,追认事实,而不是事先设计出空中楼阁式的团队,期待有人自告奋勇加入。这样做,最可能的结果是吸引到沽名钓誉的人工于心计地获得头衔。

另外,关注点分离也强调权限的独立。除非明确需要层级结构,例如 PostgreSQL 的 Core Team 对整个开源软件负责,因此它需要决策 Committer 团队的人员组成,否则你并不需要把所有的子共同体组织成一个层级结构,网络结构也是可行的。例如,Linux Foundation 实际上与 Linus 控制的开源软件共同体并没有层级关系。例如,StreamNative 公司建立的 Apache Pulsar 中文社区也不需要对 Apache Pulsar 的 PMC 负责。这种独立往往发生在分地区和语言成立的本地用户组上,或者围绕同一开源软件但是面向不同目标的群体之间。

总的来说,所有这些情况都超出了现有治理机构在时间、知识或技能方面的管理能力。事务的多少是判断是否扩大治理机构的正当理由。如果你只有少数几个特定领域的清楚,当前的核心成员努力一下就能处理完,那就不应该成立一个独立挂钩权限的团队。但是如果核心团队定期且重复地抽到一些请求,那么就有必要考虑成立新团队应对瓶颈了。当然,这不是唯一的解法。

《大教堂与集市》书评

历时五天,我总算把《大教堂与集市》这本经典的开源文化著作认真读了一遍,真是酣畅淋漓。

本书是作者 Eric S. Raymond 的文集,其中最著名的一篇就是《大教堂与集市》,其他几篇分别是《黑客圈简史》《开垦心智层》《魔法锅》和《黑客的反击》。最有价值的是《大教堂与集市》和《开垦心智层》两章,系统解释了开源软件是如何生产的,开源开发的优势在哪,开源软件的传承是如何做到的。《魔法锅》解答了一些常见的关于开源软件使用价值和销售价值的问题,但是受限于时代背景,对商业化的讨论局限在夸大使用价值的部分,不能很好的指导基于开源软件提供软件服务的商业模式。

在进入具体的内容讨论之前,必须着重提到译者卫剑钒对中译本创造的价值。翻译是对原内容的二次创作,软件开发领域外文著作众多,大部分译本都让原文表意有明显的损失。卫剑钒翻译的《大教堂与集市》,阮一峰翻译的《黑客与画家》,以及云风翻译的《程序员修炼之道(第二版)》是我近一年来读过的本领域最佳译作。

开源协同的优势

《大教堂与集市》一章主要讲的就是开源协同的优势。集市模式就是开源协同的模式,本章的要点在于论证这种模式能够生产出高水平的软件,以至于超过任何商业公司闭门造车的软件。原文的论述重点在同行评审的价值,辅以拥有用户的重要性,落点在如何以集市模式领导开源项目。出于讨论流畅性的考虑,我把前两点的顺序调换然后展开。

拥有用户

对于任何软件来说,获取用户都是一个艰难的生存挑战,持续的用户反馈能够帮助软件不断修正前进方向,没有用户也即意味着软件的死亡。开源软件能够在早期发展阶段吸引到足够的注意力和用户。

一种形式是如原文作者继承 popclient 项目,从而直接继承其用户群。这在商业公司开发的软件当中是不容易做到的,因为涉及到专有软件的所有权转移,总是非常的繁琐且挫败的。大部分商业公司开发的软件,一旦因为种种原因不再维护,往往无法为人所继承而是彻底死亡。

因此,如今的用户对全新的专有软件往往抱有很强的怀疑态度。例如,最近一段时间迸发出来的新兴数据库软件,如果我无法获得它的源码,那么我如何能够自由地探索它呢?成熟的闭源商业软件辅以用户手册或许没有这个烦恼,但是软件的更替是不可避免的,新兴软件刚出世时,往往欠缺文档,功能不全,只有阅读源码甚至加以修改才堪堪能用。这种情况下,开源软件不是比闭源软件更好的问题,而是只有开源软件才能生存下来的问题。

此外,围绕开源软件或软件群形成的开源共同体有内部共通的价值观。如果你制作了一个新的开源软件,在潜在的用户群组里发帖介绍自己是不会被排斥的,如果软件质量不错,还会得到用户的自发传播。例如,我在 GitHub DCO App 异常期间,顺手开发了一个基于 GitHub Actions 的方案并在原 issue 下评论介绍自己的解决方案。没有回复会认为这是恶意竞争,而是出于解决问题的群策群力。又例如,Engula 项目开发过程中,向 Rust Community 和其他相关主题的资深开源开发者均寻求过意见和建议,其中认可 Engula 项目价值的人,就会自发的传播它。这对于商业软件来说是不可想象的,如果上面的行为替换成一个闭源商业软件,则参与者会认为你是一个销售人员而不是黑客同行,并且对一个完全黑盒的全新解决方案兴趣寥寥。

最后,开源软件不会将自己的用户局限在销售关系以内,这往往能保证软件开发者有更强的主导能力,按照符合软件工程的方式开发高质量软件,而不是在需求爆发的压力下将软件绑定在单一用户的需求上。

同行评审

同行评审是原文论述的重点,实际上,集市模式的核心价值就在于跨越组织边界的独立的同行评审验证设计和保证正确性。原文将其称为“Linus 定律”,即

如果有足够多的 beta 测试者和合作开发者,几乎所有问题都会很快显现,然后自然有人会把它解决。

不过针对这个定律有两点需要解释。

第一点是它所强调的是独立的同行评审实施的简单性和有效性,而不是单纯的“人多力量大”。

开源开发的价值之一就是源代码公开使得任何人都可以分析代码逻辑以定位问题。时至今日,传统的研发组织仍然把开发人员和测试人员区分成两个竖井,测试人员几乎只能完成黑盒测试。可想而知,缺乏分析的现象型 bug 报告往往需要耗费开发人员相当多的时间重新验证、复现和定位。如果让对源代码一无所知的测试人员为 bug 定级,则两类人员之间的冲突会更加尖锐。

开源开发打破了这种困境。由于大家都有真实的源码,开发者和测试者很容易发展出一个共享的表达模式并进行有效的交流。一个现象型 bug 报告和一个直接关联到源码的分析型bug报告,对开发者解决问题的帮助简直是天壤之别。Linus 定律建立在开源开发的基础上,强调的是拥有源码以后加入新的眼睛的成本不在包含商业公司管控带来的限制和摩擦,从而能够从基数足够大的同行评审当中获取高价值的报告。

原文引用《人月神话》的 Brook 定律,提到随着开发人员数目的增长,项目复杂度和沟通成本按人数的平方增加,而工作成果只会呈线性增长。对于这个论点,原文作者是认同的。但是,开源项目所采用的沟通方式,区分成少部分核心开发人员与由 beta 测试者和潜在的贡献者组成的外围人员。外围开发者实际工作在分散而并行的子任务上,他们之间几乎不交流;代码修改和bug报告都会流向核心团队,只有在那个小的核心团队里才会有 Brooks 开销。

这揭示了开源开发的精英领导制内核,也解释了 Linus 定律虽然常被简化成“只要眼睛多,bug 容易捉”,但是却不是简单的“人多力量大”。

第二点是开源软件当中出现 bug 是正常的。这一点过于天经地义以至于当我发现我需要强调它的时候有些震惊。近年来出现的“心脏滴血”和前几天的 Log4Shell 漏洞,导致部分声音认为开源项目的使用是有风险的。

对此,我只能说,这当然啊!软件有 bug 不是正常的事儿么?开源开发不是银弹,任何复杂的软件都会有 bug 存在。Linus 定律成立的案例 Linux 是在高速发展的过程中保持了相对稳定的质量,而不是从来没有 bug 出现。如果你认为开源软件有不可承受的风险,最佳做法是参与其中对它做出改良。

此外,开源软件的许可证往往附带了免责声明,也即这个软件的源代码就这样(AS IS)给你了,没有任何保证(WITHOUT WARRANTIES)。在应用当中整合开源软件之后,保证应用的正确运行与安全性是应用开发者的责任。开源软件会因为安全问题损失声誉,因此作者会尽力提高安全性和正确性,并辅以相应的测试验证,但是这些都是尽力而为,没有保证。

集市模式

《大教堂与集市》一章的落点在如何以集市模式领导开源项目,这种模式相较传统的管理架构有何不同。

其中很多原则和技巧不是开源特有的,并通过敏捷等理念渗透到商业公司的软件开发当中。例如,“好的软件作品,往往源自开发者的个人需要”,“早发布,常发布,倾听用户的反馈”,以及“想出好主意是好事,从你的用户那里发现好主意也是好事”等等。

其中最重要的一点是关于发布的。开发者在需求列表不能调整和最后期限不能拖延的双重要求下,会完全顾不上质量,整个工作很可能会变成一团乱麻。Linux 通过发布两种不同类型的版本,各自宽松其中一个要求来保证软件质量和进度的协调。

一种办法是保持最后期限不变而让需求列表灵活一些,允许某些到最后期限时仍未完成的需求被舍弃,这基本上就是“稳定版”核心采取的策略。 Alan Cox(稳定版核心的维护人)以相当规律的时间间隔将核心发布,但并不保证某个特定bug何时被修复,也不保证实验版中的某个特性何时会搬到稳定版中。

另一个办法是设定好想要的需求列表,并在其完成时发布,这基本上是“实验版”核心的策略。 De Marco 和 Lister 引用研究结果,指出这个进度策略即是“好了告诉我”,这不仅能够保证最高质量,而且就平均而言,与“保守”或“激进”的进度安排相比,它的交付时间更短。

对于与传统的管理架构比较的部分,其理论基础可以参考《开放式组织》《企业的人性面》相关的论述。概括地说,开源的方式给予开发者足够的自由,以吸引高水平的黑客自发地创造价值。这种超越了对安全需要乃至生理需要的追求的模式,激发的是参与者对社会需要和自我实现需要的热忱。

在这里,没有预设的团队和资源,不需要在办公室环境下吞并其他团队的资源或者对其他团队的进攻做出防守。开源开发者是志愿者,是因为兴趣和能力自主选择的,他们会把自己的资源带到工作中,而不需要关心团队之间的领土争端和倾轧。

在这里,参与者凭借其创造的价值赢得权威。也就是说,最有才华的人能够对项目的发展做出最合适的决定。这不同于雇佣关系下被强制调配的人与项目之间的关系,而是对于特定的人,自由选择适合自己的项目,对于特定的项目,自然筛选出最合适的人。

原文还提到一种观点,即传统开发管理能保证艰苦和乏味的工作总能落实。我想这点毫无疑问是错误的。Linux 和 Kubernetes 的文档充足到令人难以置信,反过来只为了领工资才上班的人往往消极对抗撰写文档和测试或调试问题等工作。

开源共同体的目的是制造高质量的软件,在这个共同目标的引领下,不同方面的人才聚拢起来发挥自己的价值,反而是能够找到对传统开发管理认为艰苦和乏味的工作甘之如饴的人才。对于项目维护者来说,认识到这些所谓“无聊”部分的价值,协同参与者完成它们,是项目能够脱颖而出的必要条件。经过二十年来的经验积累,这逐渐成为最有才华的黑客当中的共识。

最后,对于想要实行集市模式的人,这里转述原文提到的“集市模式的必要条件”。

集市从成立伊始,就需要一个可以运行和测试的东西。当开始建设开源共同体的时候,你需要拿出一个像样的承诺。程序此时并不需要特别好,它可以简陋、有错、不完整,文档可以少得可怜。但它至少要做到能运行,且让潜在的合作开发者相信,这个软件在可预见的未来,能演变成一个非常棒的东西。

项目领导人需要能识别出别人的优秀创意,掌握一定水准的设计和编码能力,并且必须具备很好的人际交往和沟通能力。最后一点应该是显而易见的,为了建立一个开源开发共同体,你需要吸引人们,让他们对你做的事感兴趣,让他们乐于看到自己的贡献。一些技巧可能有助于实现这些,但远远不是全部,你的人格特征也很重要。

开源软件的传承

《开垦心智层》一章讨论开源共同体的发展,以及发展过程中开源软件的所有权及转让的问题。

所有权

原文提到,开源软件的所有权获取有三种形式。

第一种也是最显然的,就是去创建这个项目,当这个项目在开始时就只有一个维护者而且这个维护者仍然起作用的时候,所有权问题是连提都不该提的。

第二种方式是获取前任对所有权的移交(有点像“接力棒传递”)。这在社区中很容易理解,当项目“所有者”不愿意或者不能在开发和维护中投入必要的时间时,他(她)有义务将项目移交给一个有能力的继任者。

第三种方式是一个项目需要维护但项目所有者已经消失或失去兴趣了。如果你想维护该项目,你的责任是努力找到这个“所有者”,如果找不到,你可以在相关场所(比如 Usenet上专注于该应用领域的新闻组)声明该项目似乎是一个“孤儿”,而你想为之负责。

我在协助处理 TiDB 里两个合并子项目的工作的时候,实质上就遵循了这里的原则。原来的项目由多名 contributor 参与完成,在当时的 CLA 设置下,要求每位 contributor 都必须和 TiDB 项目签 CLA 才能合并。比起强硬的改变 commit author 绕过 CLA 检查,我建议尝试联系未签署 CLA 的参与者补上。这些参与者被 at 以后很快响应并且解决了问题。

其实参与开源开发的人不是坏人,在项目没有发展出多样性之前,不要擅自以“内部”“外部”这种二元视角界定参与者的属性,也不要假设“外部”是邪恶的。

分化

原文将分化行为列为开源文化当中的禁忌。分化指的是派生出一个随后不能交换代码的竞争项目,并导致开发者群体的分裂。

黑客厌恶项目分化的另一个原因是,他们惋惜那些被浪费的重复工作分化后的两个子项目总是有着或多或少平行的演化路线。他们也会注意到分支倾向于分裂合作开发者社区,使得两个子项目的人手都比父项目的人手更少。

近年来雨后春笋般冒出来的开源项目,在分支和合作问题上起码有两点值得关注。

第一点是对合作的漠视。相当部分项目,号称开源,实则核心成员还是都来自同一个公司团队,规模往往超不出十几人。他们有很强的领地意识,拒绝其他人的参与,或者将其他人的贡献打包进项目整体说成都是该公司的贡献。这样做,使得不同组织的参与者失去动力甚至有种被驱逐出去的意味,实质上只是源码可得的传统项目开发模式。

当然,也有好的案例,且大多来自公司背景不强的项目。例如 SeaTunnel 还叫 WaterDrop 的时候,就吸引了不同组织成员的关注和参与,现在又被 Apache 成员关注到,合作进入 Apache 孵化器孵化。

第二点是对分支的痴迷。也就是公司喜欢 fork 出来搞个魔改版本,从不考虑 contributing back 还以为自己占了便宜。且不说这种行为禁锢了原本可以参与共同体的成员,代码分化带来的兼容性问题魔改版本从来不能解决。回过头来把魔改版本抛头换面又煞有介事的“开源”,应该被整个黑客社会所唾弃。

如果说还有一点,那就是那些所谓的“开源技术公司”,如果试图对开源共同体实施某种形式的管控,让商业公司凌驾于志愿者之上,那么这样的项目实际上更容易分化。Elastic 和 OpenSearch 就是一个典型的例子。

对于相对开放的民主制度而言,它的一个主要优势在于,绝大多数潜在的革命者发现通过在系统中工作比攻击该系统更容易让自己向目标前进。但如果既有政党联合起来“提高门槛”,导致那些较小的不满意团体觉得更难实现自己目标的话,这种优势就很容易被侵蚀破坏。

准入门槛不高的开放过程鼓励参与而非分裂,因为参与者能从中获得成果,而不用付出分裂所需的高昂成本。尽管这种成果可能不像分裂所得成果那样令人印象深刻,但其成本较低,且大多数人都能接受这种折衷。

冲突与解决

原文提到,项目当中的冲突与解决主要围绕三个问题展开

  1. 谁来负责做设计决策?
  2. 如何决定哪个贡献者应该被授予荣誉,如何授予?
  3. 如何保持项目团队和产品不被分裂为多个分支?

第一个问题由上述所有权问题回答。关于分支的问题在上一节已经讨论过了。现在看第二个问题。

无论采用独裁者模型还是委员会模型,黑客的荣誉都跟他创造的价值相关。也就是说,黑客的声誉在礼物文化的大背景下,由他的贡献即赠与开源共同体的礼物的价值所决定。对于独裁者模型来说,独裁者本人需要能够践行这样的规则,否则高水平的参与者就会选择离开。对于委员会模型来说,还有一个额外的问题是委员会自身应该避免冲突。原文质疑委员会模型难以避免冲突

在这种形式中,我们很难看到内部边界,并因此很难避免冲突,除非委员会内部享有极高水平的和谐与信任。

但是,今天的软件复杂度越来越不支持独裁者模式。如果独裁者本人已经把部分决策权交给参与者,那么他在运行上就类似于委员会的模式。即使独裁者名义上拥有最终决定权,他与维护某一模块的核心成员仍然需要保持高水平的信任以减少项目当中的摩擦。

结合如今一部分商业公司创建或大规模参与开源项目的背景,如果项目建立的是同侪共同体(community of peers),也就是说成员的角色与个体相关,而不是与他在某个组织的职位相关,在这种情况下依然把委员会的人员增加与企业员工入离职挂钩,这种组织形式就是非常危险的。

具体地说,部分项目照猫画虎地搬来了 Apache 软件基金会式的同侪共同体设计,在决定项目 PMC 成员和 committer 人选时,却变成了公司同事入职,“理应”有 commit 权限,就稀里糊涂的成了什么 committer 或 PMC 成员。一旦离职,则完全不理会项目的发展,甚至出于不愉快恶意捣乱项目的日常事务。这就是没有基于项目的需要和个体对项目的认可和贡献选择委员会成员的弊病。

这是说缺乏多样性的项目中,单一公司的员工需要避嫌吗?当然不是。实际上,成为大力投资该项目的公司的雇员,能够尽可能多的时间投入到项目发展上,公司的员工确实有更大的可能性成为核心成员。但是必须注意的是他的推举应该是客观的,基于项目的需要和个体对项目的认可和贡献来选择。只有这样,才能努力做到委员会内部有极高水平的和谐与信任,这才是这种组织形式下项目长久发展的根基。

开源与商业模式

《魔法锅》一章的主题是开源与商业模式,着重讨论了反公地模型,软件的使用价值和销售价值,以及当时存在的开源相关的商业模式。

反公地模型

我曾多次听到有人拿“公地悲剧”来类比开源协同的开发模式,认为后者也会如前者一样失败。

所谓的公地悲剧,指的是假设一个村庄里的所有人都可以不受限制地在一片公共的草地上放牧,如果没有一个共识来抑制过度放牧,出于自身利益的考虑,每个人都会尽可能多的放牧,以期在公地资源耗尽之前从中获取最大价值。

但是,Linux 项目持续三十年,长寿的开源项目比比皆是,这种类比显然是有谬误的。原文从公地悲剧两个必要条件来反驳,一是过度使用,二是供应不足。

公地悲剧的一个必要条件是所有人都放牧会使得草地退化,但是开源软件一旦制造出来,不会因为被过度使用而损失价值。反过来,广泛的使用会提升开源软件带来的价值。这一点很好理解。

公地悲剧的另一个必要条件是没有人会修缮草地,因为公地奖励“搭便车”行为,即你修缮了草地别人就可以无偿分享你的成果,而你的付出别人并不承担,结果是付出比不上被分摊后的收益,于是所有人都不付出。

在开源开发中不会遇到这种情况。这是因为参与者不仅需要解决方案,他们还需要问题被及时解决。因此解决这个问题本身带来的收益就足够偿还成本,而等待别人解决问题则完全无法预期它会在何时被解决。

这部分解释了解决方案必然会被生产出来的问题,但是其创造者为何会无偿发布这个补丁,还需要进一步的讨论。

一方面,很多情况下开发者无法为其确定一个公允的市场价格。另一方面,坐等在补丁上不会有任何收益,反而会带来额外的成本,因为你现在要在上游发布新版本时重复合并这个补丁。由于上游对该补丁的存在并不知晓,这种重复合并甚至有可能是多次重做。毫无疑问,这是非常挫败的。由此看来,只要你需要上游的更新,无偿发布补丁就是最优策略。

但是,这里还有一个问题,如果补丁有足够的差异性,补丁作者为什么不将其闭源以获取其销售价值?对于 GPL 许可的项目来说,项目本身的演化需要与其他各方分享,这不是个问题。但是软件结合的形式有很多,GPL 对软件即服务等方式难以产生约束,还有以其他宽容开源协议如 APL 等许可的项目,这些情形正是当下开源与商业的讨论焦点。

软件的使用价值和销售价值

在讨论这个焦点之前,我们先看看软件的使用价值和销售价值。

软件的使用价值是它作为一个工具的经济价值,软件的销售价值是它作为一个可买卖商品的价值。大部分软件是作为内部系统被生产出来的,原文认为,这个比例达到九成以上。开发者的薪资实际是出于维护软件的使用价值的目的支付的。

如果你创造的软件主要用于内部系统,而你的薪资也来自于维护它的使用价值,那么通过闭源来保护销售价值是没有意义的,因为你不会将它用于销售。这种情况下,通过开源协同来提高软件本身的开发效率和质量,就是有收益的。

值得注意的是,只要软件的开发在隶属于不同组织的参与者之间共享工作流,采用开源协同的开发方式就没有额外的成本,因为公司为了用上这个软件,总是要付出开发成本的。这个共享工作流的前提条件也是《魔法锅》一文成书时未曾想到的,居然还有人为了形式开源而给同一套代码区分出两套工作流。

原文提到两个常见的反论意见。一个是通过闭源代码保护商业机密。这是无稽之谈,主要是代码设计糟糕。通常来说,你应该将机制开源,编写通用的逻辑,而将商业知识相关的策略单独实现。当然,后者并没有什么开源的必要。

另一个是说闭源能够保护软件安全。这也是谬论。除了上面商业机密泄露的场景,对于纯粹的骇客攻击行为,二进制照样能被破解,开放源码只是多了一种破解的手段。

类似近几天的 Log4Shell 漏洞,难道黑客不读代码就找不到这个问题了吗?如果公司内重新实现日志框架,且不说要达到 log4j2 的水平要付出多大成本,以及生态兼容性的种种问题,难道重新实现的软件就没有其他安全问题吗?

即使不说分析源码的破解手段并不比破解二进制的手段轻松多少,可靠的安全性也依赖于算法及其事先经过彻底的同行评审。这么看来,开源软件反而更容易修复安全问题。Log4Shell 通过同行评审发现后通过必要的 private 邮件列表上报,在上游修复后进行披露,正是这种安全同盟的一般做法。

直接收费的问题

当然,上面这些讨论仍然没有覆盖当前这波开源浪潮下新出现的商业公司群体,这些公司创造开源软件,并希望基于它们创造的开源软件获利。

原文对直接收费类型的许可证做出了批驳,指出希望在源代码可得的前提下添加某种收费或变相收费的条款,会遭到黑客的反感,从而失去开源共同体的支持。这是因为这类许可证违背了三个开源共同体的共识。

第一个与对等性有关。大多数开源开发者并不反对别人利用他们的礼物获利,只是不能要求有任何人站在一个特权地位上牟利。MongoDB 的 SSPL 在理念上或许沿袭了 GPL 的一些理念,只是它对形成派生作品的描述“形成服务”太过笼统,得不到广泛的支持。但是 MongoDB Inc. 自己并没有按照 SSPL 的要求开放它的整个服务栈的源代码,这种对等性的破坏遭到了黑客的唾弃。实际上,MongoDB 的核心代码几乎只由其公司雇佣的员工开发和评审。

第二个与非有意后果有关。原文提到,对商业使用或销售进行限制并收费的许可证有着令人扫兴的效果。特别是这条规定给某些分发行为笼上了一层法律阴影,而这些活动正是黑客非常愿意鼓励的事。还是 SSPL 的例子,由于“形成服务”太过笼统,几乎所有黑客都倾向于不分发该软件以避免潜在的法律风险。原文认为,黑客很少在这一点上让步。实际上,这也是 OSI 拒绝承认 SSPL 是开源许可证的主要原因。

第三个与保持礼物文化相关,这也是最关键的一个原因。如果许可证在法律上就禁止产生分支,那么黑客们绝对不会认同这样的条款。原文解释到,虽然黑客们不赞成分支,但是分支是“最后一招”。如果维护者不能胜任或者背叛开源文化,可以通过分支来保护礼物的传递。Elastic 与 OpenSearch 就是活生生的例子,以 AWS 的工程师为首的开发者在 Elastic 转向更加封闭的时候基于开源版本分支并独立发展,保持新分支的开源属性。

开源的商业未来

《魔法锅》随后介绍了当时作者所看到的的若干种基于开源软件的商业模式。这里不需要展开,因为它们都统一在同一个模型下。这个模型就是基础架构和中间件开放,应用和服务收费的模型。

开源基础架构,并利用同行评审的价值,协同跨越组织的参与者创造出类别杀手,做到这点的收益实在太大了。类别杀手指的是即好到没人再想使用其他备选的高质量开源原创项目,例如 Linux 和 Kubernetes 等。

Google 愿意开放 Kubernetes 的源代码,很大一部分原因就是为了联合其他商业公司以及整个开源共同体形成事实标准的垄断,而要做到这一点,开源协同的方式是最高效的。Kubernetes 形成垄断后,越是早期参与项目的组织,越是投入资源大的组织,越能够获得某种程度上的原厂品牌效应,并积累足以应付软件在使用上的种种问题的开发者团队。这些组织通过提供应用级别的定制和维护服务收取报酬。

原文认为应用非常倾向于继续封闭,这种封闭尤其可能出现在自成一体的垂直市场当中,其网络效应也较弱。这其实就是针对特定场景开发的插件或者是针对具体业务接入基础架构的实施。时过境迁,如今的软件复杂度已经不是当年一个全栈工程师从购买服务器到整个网站都能负责开发的年代,雇佣业务实施团队将越来越常见。

这些插件某种意义上也可以算作中间件。实际上,应用和中间件之间的差别会随着时代的发展而变化。原文认为数据库是中间件,但是如今却更被认为是某种基础软件。中间件走向闭源还是开源,取决于软件失效的代价,代价越高,走向开放的市场的压力就越大。

举个例子,AWS 的不少服务是闭源的,但是它们的客户端是开源的。这些客户端就是中间件,如果它们的维护更封闭,那么失效的可能性就会越高。广泛的用户会倾向于使用开源的替代品。一个案例是 AWS S3 的 Rust 客户端 rusoto 和官方后来提供开源版本。

Confluent 依靠提供 Apache Kafka 的服务盈利,整个商业模型包括三个部分。

第一部分是实施,也就是帮助客户业务与 Apache Kafka 对接,乃至于设计整个业务消息平台。这是传统上所说的“外包”工作,由于软件复杂度日益升高,这类工作所需的软件开发技能也越来越丰富,相应的雇佣薪资也就水涨船高。这种模式也被称为订阅,在一个订阅周期内,客户能够获得实施工程师的支持,商业公司在提供工单响应的保障。实施包括支持私有化部署,也包括帮助客户对接云服务。

第二部分是提供基于开源软件的云服务,也就是云上的 Apache Kafka 资源,客户按照使用的节点数或访问量交费,这种模式实际上是商业公司通过出租商业地产盈利。一方面,CPU 和内存等资源本身是成本,用户无论如何也要为这些成本付费。另一方面,商业公司在资源之上提供了消息平台的抽象,屏蔽了部署和运维软件的复杂度,并以此来赚取差价。对于无力自行维护的企业来说,购买云服务就是最优选择。

值得一提的是,这种部署的附加值是工程师水平和硬件成本的函数,云厂商往往能够获取更廉价的硬件成本,因此独立服务提供商最好追求部署和运维本身的开销下降,这种运维和部署的策略是商业机密和盈利的基础。另一方面,可以通过维持云中立,避免供应商锁定等优势,利用云厂商之间的竞争激发用户的优先选择意愿。

第三部分是专有软件,例如 ksqlDB 等。只不过 ksqlDB 的位置更像是接近基础架构的中间件,被 Apache Flink 和 Materialize 等项目挤压了不少生存空间。反观 Apache Pulsar 和 Apache RocketMQ 就没有将类似功能做成专有软件以期销售,避免被其他项目分化用户。

对于哪些软件不适合通过闭源获取商业价值,《魔法锅》一文介绍了应该考虑开放源码的软件,时至今日仍然是正确的

  1. 可靠性、稳定性、可扩展性非常重要。
  2. 除了独立的同行评审,没有其他便捷易行的方法验证设计和实现的正确性。
  3. 该软件对客户的业务非常关键,因此客户期望避免供应商锁定。
  4. 该类软件受网络效应主导,即你无法实现压倒性的市场控制力。
  5. 关键方法属于公共知识。

开源与闭源在几乎所有层面上都是并存的,并且呈现出一种动态发展的趋势。

起初,Windows 垄断了操作系统的市场。当 Linux 出现以后,服务端操作系统的份额开始逆转,并且出现 RedHat 等商业公司。原文称为中间件的数据库,起初被 Oracle 主宰,如今它也承受着 PostgreSQL 的冲击,海量提供 PostgreSQL 服务的商业公司也能生存下来。今天,云原生技术和软件即服务的概念改变了软件生产和使用的格局,越来越多的商业公司创造开源软件或参与到其开发当中,目的就是推出下一个类别杀手,并取得之后的软件服务战争的优势。

实际上,最好的商业价值获取方式仍然依赖创造性的垄断,这也是知名商业著作《从 0 到 1》的观点。只不过,软件的复杂度以及开源开发应对这种复杂度在生产力上的显著优势,使得你无法在一个很大的范围内实现垄断。但是你仍然可以找到合适的垂直领域,或者就是为客户做实施——这也许是最垂直的一种方式了。

如今,想要创造局部垄断的一种新方式,是通过开源协同的集市模式创造出一个类别杀手,在此过程中获得某种程度上的原厂品牌效应,并积累足以应付软件在使用上的种种问题的开发者团队。进一步的,将原本的市场格局改变,在不改变固有需求的情况下改变产生商业价值的位置。以操作系统为例,原本商业公司以创造出商业操作系统为竞争优势,Linux 出现后,如何基于 Linux 提供更好的服务,或者看到 RedHat 如今的上云策略,提供海量 Linux 服务器资源的运维和应用的部署服务。

改变不利于自己的商业格局,并在环境有利于自己的时候做好下一次颠覆的准备,才是开源时代的商业未来。我也相信这种形式,能够促使企业家真正成为创新的先锋,而不是被长时间垄断所麻痹,不思进取乃至阻止社会生产力的进步。

高效参与开源的诀窍

大部分人参与开源社区会面临的一个巨大挑战,那就是缺乏时间。本文试图提供一种方式,帮助想要参与开源社区的同学高效利用有限的时间。

在一个开源社区里,maintainers 需要关注的范围比 contributors 要大得多。本文分别讨论这两类人群适用的参与开源社区的技巧,以减少过程中的摩擦,提高时间的利用率。

Contributors

第一步要加入社区

参与开源社区的第一步就是加入社区。加入社区的方式有很多,可以订阅邮件列表,关注开发活动,参与技术或非技术讨论,等等。很多希望参与开源社区的人迟迟迈不出第一步就是忽略了自己首先要加入社区,跟社区建立起联系。

一个典型的错误做法是完全不顾开源社区是开发开源项目的主体,一头钻进技术细节里,暗搓搓地做一个“大功能”,然后希望社区尽快合并这个补丁,让自己得到荣誉。

Linux Foundation 有一篇博文明确反对了这种做法。

Some organizations make the mistake of developing big chunks of code in house and then dumping them into the open source project, which is almost never seen as a positive way to engage with the community. The reality is that open source projects can be complex, and what seems like an obvious change might have far reaching side effects in other parts of the project. Any significant change is likely to require some community discussion before it moves to implementation to make sure that there are no side effects and that the solution is aligned with the broader goals for the project. While you discuss it with the community, it can help to focus on the problem, rather than a specific solution, before you invest too much time in the creation of a body of code.

一个现实的例子,前几天有人问我,自己做了一个 Flink StateBackend 的实现,提交给社区是不是就能当 PMC 了。这个问题属实把我整不会了。从来没有在社区当中亮相的人,突然出现并提出自己实现了一个“大功能”,在其他成员眼里跟民科没什么不同。绝大部分情况下,这种实现跟上游社区的开发节奏是脱节的,很难合回去。也就是说,闭门造车的形式自我感动地开发项目,即使花费了时间,大概率还是白忙一场。

刚开始接触 Flink 社区的时候,我就按照项目文档的提示订阅了 users 和 dev 两个邮件列表。实话说,最初的三个月,我基本看不懂他们在说什么。当时的我尽可能地读每一封邮件,从邮件里面引用的链接一个个点进去了解背景,混沌当中建立起对项目的初步印象。直到四个月后第一次提交代码,这个祛魅的过程才算完成。从此以后,我逐渐能够轻松地参与到技术讨论,也掌握了 review 的沟通习惯。

最近,我在跟人介绍 Engula 项目的时候,也是先发讨论区聊天室的链接。新成员可以阅读过往的讨论,挂在聊天室里,观察社区讨论问题和推进工作的方式,了解已有的设计实现和结论。参与闲聊或者回复感兴趣的话题,找到自己愿意投入的工作。只有这样,才能进一步深入参与开源社区,而不是接触了好几年,却始终迈不出第一步。

找到感兴趣的问题

要想利用有限的时间创造更多价值,最好的方法是找到一个感兴趣的问题,然后持续投入进去直到解决。

一个典型的错误做法是强迫自己做着不感兴趣的工作。这种情况下,由于内心是抗拒的,即使投入再多的时间,也几乎不会有产出。

可能有人会不理解,开源社区的 contributor 都是自愿参与,如果不想做某个工作,不是不做就可以了吗。其实不然,社区成员身处其中很容易感受到无形的社交压力。

一种情况是不懂得拒绝。知乎上有个问题,如何优雅地拒绝开源项目的 PR 邀请,讲的就是这种情况。我在回答里分享了一个自己拒绝 Flink 社区成员里的 PR 邀请的案例。另一种情况是错误估计难度,即自以为能搞定这个工作,做的过程里发现不对,又不好意思改口说自己搞不定。应对这些情况的方法非常简单,直截了当地说明情况即可,解放自己避免浪费时间。

另外一个难题是自己往往对比较有挑战性的工作感兴趣,但是从一个刚接触项目的 contributor 到能够完成一个复杂任务之间有一道坎。

要跨过这道坎,同样需要积极采取行动,而不要独自纠结。首先可以考虑从简单的工作入手,比如阅读项目文档时发现的拼写错误。一个简单的贡献能带你走完整个 contribution 流程。一回生二回熟,做其他有挑战性的工作也就不会在流程上踩坑。其次可以保持和 maintainers 的交流,以了解现有逻辑的设计背景和演进过程。只有对工作涉及的逻辑有充分的了解,才能写出高质量的代码。高质量的代码也意味着更少的返工和不必要的争论,也就避免了时间的浪费。

建立与其他成员的联系

随着参与的深入,总有你一个人无法完成的工作。开源协同的价值就在于跨越所属组织的边界合作开发项目。合作的基础是成员之间的信任,也就是良好的关系。

开源社区是围绕开源软件建立起来的。但是并不只有软件本身带来技术价值,人与人的连结带来认同感和归属感,这些也能满足社区成员的需要。此外,相互信任的基础能很大程度提升价值创造的效率,例如减少浪费在同步和对齐上的时间。因此,建立并保持与其他项目成员的关系至关重要。

做到这一点的方式就是充分的沟通。同样,这需要以开放的心态对待平时的交流。不要把所有事情都憋在心里。不要纠结于想清楚所有细节再开始沟通,其他成员一时间内往往没办法追上你所想的所有细节。我的建议是,当你有一个初步的想法,也做了力所能及的调研,就可以整理一下,发布到社区当中征求意见。

我给 Engula 项目做了一个社区计划。老实说,内容并不成熟,但是我一个人干想也得不出结论,所以在经过几轮自我 Review 以后,就先抛出来征求意见。另一个例子是 Engula 的 maintainer @huachaohuang 想为 contributor 提供开发文档,于是就发起了一个关于 Dev Guide 的讨论。正好我对这个话题也早有想法,当我看到发出来的讨论以后,发现他也在关注这个话题。于是我花了一个小时把自己的想法写下来,经过讨论以后提 PR 推进主分支。

沟通协作的过程里冲突在所难免。我在好几个项目里都别人讨论甚至争论过很多次技术问题,给别人的行为提过意见,也夸赞过好的做法。开源社区解决冲突的方式比较朴素,一般是有话直说,尽量客观地达成共识,按照流程约定做出决策。不用整那么多弯弯绕浪费时间。

举一个现实的例子,曾经有人跟我抱怨提上去的 PR 被 maintainer 挑战了,问我应该怎么回复。怀疑 maintainer 是不是有偏见,抱怨很难跟 maintainer 沟通,大量的时间精力浪费在纠结这些臆想出来的问题,自然是筋疲力竭,感觉在开源社区里寸步难行。

适合参与开源协同的工作

最后,关注到相当一部分 contributors 的公司员工的身份。这显然会影响到他们参与社区的动力和能力。

主要的挑战是,如果工作期间不允许参与开源社区,同时工作本身已经消耗了太多的时间精力,那么 contributors 对参与开源社区也只能是有心无力。这其实是很长一段时间里开源社区的参与在国内发展缓慢的原因。大量的开发者都在过度工作,下班只想躺平休息,没有动力再谈什么开源贡献。

不过,随着时间的发展,情况也在发生着变化。越来越多的公司采用更加灵活合理的工作时间,尤其是以研发为核心竞争力的公司。如果你所在的公司仍然要求超负荷工作,燃烧生命赚血汗钱,那么是时候找份新工作了。时代已经变了,就让这些公司被无情的淘汰吧。

另一个方向是考虑在工作期间参与开源社区。如果你确实喜欢某个开源项目,那么最佳策略就是找一份允许你全职投入这个项目的工作。这样的工作岗位如今并不少见。尤其是随着企业级解决方案越来越倾向于采用开源组件,企业对熟悉开源软件的人才的需求只会日益增加。如果找不到全职投入开源项目的工作,与之相关的工作也是备选方案。

不过,即使这份工作允许你全职投入开源项目,也并不意味着你能够参与开源社区。特别是当你的老板认为参与开源社区不能为公司创造价值的时候。面对这个问题,首先你可以问问你的老板,说不定他不这么觉得,那就省事儿了。如果你的老板确实难以理解,那你就得像兜售一个技术方案一样向他宣传参与开源社区的价值了。我在其他的文章里对这一点已经有不少讨论,你可以看看。

普适的时间管理手段这里就不展开介绍了,各种相关书籍和 GTD 方法论都很值得一看。

Maintainers

发展新成员

Maintainers 比起 contributors 需要关注的更多的事情。随着开源项目日渐复杂,开源社区逐渐成长,单靠一个人的力量很难处理好所有的事务。这个时候,就需要 maintainer 适时地发展项目维护的队伍。

首先需要理清 maintainer 头衔的定位。实际上,大部分项目的维护是个苦力活,而 maintainers 就是一群承担这些工作的社区成员。Maintainers 可能会拥有合并 PR 的权限,在社区治理中能投票做决策,确定项目发展的方向。但是,这种权限并非特权。在一个健康的社区里,任何社区成员都可以做技术讨论,也可以就社区发展话题提出自己的观点。对于技术观点,客观上更加合理的方案理应被采纳。对于社区发展话题,maintainers 也一定会考虑建设性的提议。

可能有不少人把成为 maintainer 当成参与开源社区的目标,这是很好的。如果你理解了 maintainer 的职责,通过 contribution 积累了足够的信誉,成为 maintainer 为开源社区服务,这个头衔是一个显式的认可。不过,大可不必过分纠结于 maintainer 头衔。这只是对 contribution 认可形式的其中一种,而不是唯一一种。

Maintainers 的职责并不轻松,所以 Python 社区和 Apache 软件基金会下的项目社区都会有一个询问 contributor 是否愿意成为 maintainer 的流程。也存在 contributor 拒绝邀请的情况,因为就像前面提到的,健康的开源社区里,只要提议是合理的,就能凭借其客观的优势胜出。成为 maintainer 并不意味着在方案选择上有特权。

对于 contributor 的感谢,也可以通过宣传渠道发布。比起一个模糊的 maintainer 头衔,作为技术人员,我会更在意这个人实际在开源社区里实际完成的事情。

基于上面的认识,我们引出下一个观点。Maintainers 发展新成员,必须是有选择性的。

这种选择性的主要依据是维护项目的需要,而不是追求数量或者过分在意 diversity 等等。这可以类比到开发软件的目的是提供技术价值,而不是代码行数或者所采用的编程语言的数量。

一个典型的错误案例是出于自己同时是公司员工的身份,被命令将 maintainers 的人数发展到某个数字。这种指标只关注数字而不关注具体的人,而且往往定得脱离实际。公司员工迫于指标压力很容易降低 maintainers 的标准,逮到一个算一个的凑人头,或者为了 diversity 对不同背景的 contributor 采取不同的标准。这样发展出来的 maintainers 不仅不能分担项目维护的职责,还很有可能因为不胜任而产生新的问题。

另一个典型的错误经常出现在个人项目上,当个人项目发展壮大,唯一的 maintainer 想要发展新成员时,很容易陷入到要找一个自己的分身的误区。 也就是说,新的 maintainer 必须和自己一样能够关注到项目的方方面面。这是不对的。没有两个人完全相同。只要一个 contributor 有足够的信誉,并且能在项目或社区的维护的某个方面上承担职责,他就是一个好的 maintainer 人选。

不过,这里讲到的信誉是一个非常主观的概念,提名 maintainer 的倾向每个项目也各有不同。

  • Perl 社区最初由 Larry Wall 独裁。近年来,随着他逐渐淡出核心成员圈子,Perl 社区的治理实际上已经变成由 28 人组成的 core team 负责。
  • PostgreSQL 社区由 7 人组成的 core team 和 28 位 committers 处理所有工作。
  • ASF 治下的项目有一套比较固定的治理模型。具体到每个项目,例如 Apache PulsarApache Flink 会有自己具体的要求和倾向。
  • Spring Project 社区的 committers 都是 Pivotal 公司或 VMWare 公司的员工。但是它显然也是诞生于开源协同的作品。
  • Linux Kernel 基本上还是由 Linus 独裁。同时,海量的驱动和架构支持有各自的 maintainer 进行维护。参考 Linux Kernel Maintainers 页面
  • Netty 社区没有明确的规则。Trustin Lee 发起项目并独自维护了三年。随后,Norman Maurer 和 Scott Mitchell 等少数几个人持续参与,成为 maintainer 并共同维护 Netty 项目至今。

如果让我对 maintainer 提一个基础要求,我会希望他在项目或社区中做出了卓越的贡献,并且当前的 maintainers 团队乐于和他一起工作。

结构化流程

除了增加项目维护的人员,另一个基本的减少时间浪费的手段就是结构化流程。我们分点介绍其内涵。

第一点是直觉大于文档。对于托管在 GitHub 上的项目来说,help wanted 和 good first issue 标签是一个众所周知的约定。合理标记 issue 能让 contributor 按照过往的经验快速找到切入点。我在修订 TiDB 社区的治理方案的时候,也是以跟 GitHub 开箱即用的功能亲和为主要目标之一。如果参与一个开源项目有太多新东西要学,那么 maintainers 就有的是要解释的东西了。大部分人效率最高的路径是完全凭直觉做事,并取得好的结果。所以如无必要,请勿设立复杂的规则。

第二点是文档大于口述。直觉毕竟只能解决部分问题,对于特殊的或者需要强调的内容,明确记录下来作为文档绝对是个好主意。

不过文档首要的还不是记录流程,而是项目的目标或者叫定位。这是每个对项目感兴趣的人都会问的问题,高水平的 contributor 尤甚。他们不仅仅是想在开源社区里做简单的工作,更想成为一个伟大的或富有价值的项目的缔造者。如果你想为你的项目吸引到高水平的开发者,那么最好是确定一个清晰且令人振奋的目标,并将它展示在最显眼的地方。例如,Apache Flink 的定位是数据流上的有状态计算,其中有状态这点是开源世界里开创性的工作。例如,PostgreSQL 的定位是世界最先进的开源关系型数据库。例如,Elixir 语言的目标是构建可扩展和可维护的应用。

其次是约定俗成的文档,包括 README 和 CONTRIBUTING 等等。其中一般包含项目的简介,开始使用的方法,参与贡献的基本流程,和指向更多文档的链接。大部分 contributor 会尝试寻找和阅读这些文档。如果他们能从其中解决自己的问题,就不需要 maintainer 花时间说明了。至少,在有人提问的时候,直接发一个文档的链接,也能省不少事儿。

另一个值得强调的是 Code of Conduct 即行为准则。提名新的 maintainer 之前最好确保被提名人知悉和理解社区行为准则。行为准则通常是一些涉及平等、尊重和避免冒犯的原则。虽然大多数开源社区很少遇到严重违反行为准则的情况,但是 maintainers 应该对此保持敏感。这类问题一旦处理不当,很容易演变成政治斗争,甚至导致社区分裂或项目停摆。

最后是设计文档。Contributors 要深度参与技术贡献需要了解相关代码的设计背景和演进过程,设计文档就是最好的参考材料。良好的代码质量有助于避免 contributor 阅读源码时受挫,但是项目固有的复杂度还是需要设计文档来辅助解释。如果代码质量和设计文档都缺位,想要深度参与技术贡献的 contributor 就不得不指望 maintainer 花费大量的时间解释和指导了。这点对于 maintainer 自己也是一样的。当你想要做一个新的功能,如果没有好的技术文档,你也得懵圈,也得拉人反复对齐。

第三点是避免私下讨论。有关项目和社区的讨论,唯一的信源应该是一个公开的渠道。例如,ASF 治下的项目要求所有有效的讨论都应该发生在邮件列表上。例如,大部分托管在 GitHub 上的项目隐含了讨论应该发生在 GitHub 平台上。社区成员可能还会通过其他的沟通渠道辅助交流,例如即时通信软件。但是这些辅助渠道的讨论需要被抄送到唯一信源上才实际生效。这样,contributor 才能在无需了解诸多渠道的前提下有能力获取所有有价值的信息。

这些公开讨论的内容以及表现出来的做事方式,就是社区当中的“活文档”。模仿是人类的天性,如果你希望别人遵循某种做事方式以减少冲突,那么最好以身作则,再带动更多的人跟随。前面讨论 contributor 的参与技巧时候说过,加入社区并首先观察别人是怎么做的,是一种避免浪费时间的好方法。那么与之相对的,maintainer 也要在项目维护和日常交流方面为此提供方便。

Open Communications: as a virtual organization, the ASF requires all communications related to code and decision-making to be publicly accessible to ensure asynchronous collaboration, as necessitated by a globally-distributed community.

第四点是考虑自动化。结构化的流程更容易自动化。当你的流程越来越结构化,那么是时候考虑自动化它了。显然,无需 maintainer 亲自动手的自动化流程能够减轻项目维护的压力。

同样,最好的自动化是符合直觉的。GitHub 平台提供了一系列自动化的支持。尤其是 GitHub Actions 发布以后,自动化的灵活性得到了进一步的提升。利用项目代码托管的平台提供的开箱即用的能力做自动化,能够最大程度的避免各种冲突。

自动化还应该建立在现有的成熟流程上,而不应该凭空生造一个流程。好的案例包括提交文档变更后自动部署文档页面,利用 merge bot 提高 pull requests review 和 merge 的效率等等。

其中,后者的采用是有两面性的。许多代码提交极其活跃的开源社区也仍然不需要引入自动化流程。当然,测试基本是自动化的,至少有脚本。不过 review 和 merge 还是可以人工完成的。我比较认同 merge bot 的地方是有些实现了排队合并功能以及 roll up 打包测试功能。这两个功能在保证合入主分支的代码是基于最新的主分支测试过的前提下,减少了需要进行测试的次数和人为协调的负担。但是,有些 merge bot 强制要求 review 和 merge 走非常严格的审批流程,把这个过程变得复杂不堪,这是我非常反对的。所以在引入 merge bot 之前,请确保你清楚地知道它如何改善协作效率,并保留回滚的能力。

另一个典型的错误案例是 stale bot 的自动关闭功能。真的,没人喜欢这个功能。开发者来到社区是为了和人建立联系,共同开发好的软件,而不是为了被机器人支配。应对 issue 或 PR 的积压问题,首先应该尽可能的及时处理。其次,大部分积压的 issue 是无效的内容,例如愿望清单和模糊的想法,这些只需要快速关闭即可。对于低优先级的 bug issue 的积压,既然问题是实际存在的,也不是 wontfix 的情形,凭什么关掉呢?如果当前的 maintainers 积极主动地处理 issue 和 PR 还是处理不过来,那么是时候寻找一个新的 maintainer 了。

Users SHALL NOT log feature requests, ideas, suggestions, or any solutions to problems that are not explicitly documented and provable.

流程自动化的标杆案例包括 Kubernetes 社区Rust 社区。在学习这两个社区的做法的时候,需要强调的是

  1. 请关注这两个社区为流程自动化投入了多少人力。
  2. 请关注这两个社区是在什么时候引入了何种自动化逻辑。
  3. 请关注这两个社区的成员如何利用自动化流程。
  4. 请关注这两个社区在流程自动化上的异同。
  5. 请关注这两个社区推行流程自动化时的讨论,尤其是争议。
  6. 请勿货物崇拜,直接照抄它们的方案。否则你会死得很惨。

既然 Rust 社区都不抄 Kubernetes 社区的方案,你为啥贸贸然就要抄?

日常事务

前面讲的是一些整体的做法,回到每个 maintainer 身上,实际的项目维护工作其实是日常事务。

最常见的问题是开发的风险控制。开源项目通常会有自己的版本发布周期。有时候你希望下个版本能交付某几个关键功能或改进,而这些工作并不都是由你一个人完成。尤其是,你之所以想交付这些变更,是因为公司的要求,而开发团队包括并非公司员工的成员。这个时候就需要你做好项目的风险控制。

从公司员工的角度,我介绍过开源项目和商业公司独立运营的协同模型。运用这个模型,可以把商业上紧急的需求实现在 fork 仓库上,交付 hotfix 应对紧急情况。稍后,把改动 contribute back 到开源项目当中。这样就可以把商业要求和软件开发的工程要求隔离开来,避免向开源社区倾倒粗糙的补丁。Stream Native 就在公司组织下有 Apache Pulsar 的 fork 仓库。我没有仔细研究过他们的具体做法,但是显然他们把一些公司关心的内容都放在 fork 仓库上记录。让上帝的归上帝,凯撒的归凯撒。这是好文明。

如果评估出来更合适的做法是把改动直接做在上游,那么我会建议在需要严格控制风险的情况下,直接由公司员工组成开发团队。当然,这些员工得靠自己的努力在开源社区当中赢得信誉,而不是只根据职位就被允许直接提交代码。如果同样的需求已经有其他团队在做,那么沟通就是必要的。如果信得过这个团队,保持关注并提供帮助即可。否则,可以尝试接管项目开发。Flink 社区的 FLIP-85 提案是我和 Uber 的工程师分别独立提出的。经过几轮邮件列表上的讨论,最终由阿里的工程师主导实现。我参与了 review 和提供了部分参考实现。

上面讲的是一个好的案例。其实对于一个活跃的开源社区来说,PR 冲突的情况不会太少,种类也很多。

TiDB 社区发生过一起有名的 Xuanwo 事件。完全相同的两个补丁,后提交的反而先被合入,导致先提交的被迫关闭。尤其是这个事件发生在并不繁忙的仓库上,并且两个补丁提交的时间相差一个月。这是一种非常典型的情况,需要 maintainers 保持对项目范围内发生的活动的关注。

Flink 社区有不少经典的乐子。FLINK-10052 作为我从 2019 年就和 @lamberken 配合修复完成并经过生产环境验证的高严重性问题,在过去的三年里提交的三个补丁都因为缺乏响应最终没有合并。这也导致不少用户被迫手动打补丁。FLINK-11937 是另一个例子。两家员工提供了不同的方案,其中一方缺少社区话语权,无力单独推进合并,另一方有能力但是无意推进,也不允许其他人推进。同样的案例还有 FLIP-44Queryable State 等等。

Flink 的例子其实证明了商业公司需要通过 fork 仓库的来应对商业需求。另外也可以看到这些讨论的发起人是如何被 stale bot 二次伤害的。

从开源协同的角度,contributor 不是程序,而是真实的人。上面提到的沟通手段,去掉公司员工的背景也同样适用。商业公司要做风险控制,开源社区也是一个组织,也可以做风险控制。只不过,开源社区是一个开放式组织。在这个环境下控制风险的手段不是管控,而是协同。前面讲到的文档和结构化的流程在这里同样可以起作用。信息在 contributor 之间自由流通,就不会有 FUD 产生的伤害。平时保持和其他 contributor 的联系,就能知道当前的工作最应该找谁一起做。

大部分情况下,contributor 是能够自我驱动和自我激励的。他们爆发出的创造力不可小觑。单就时间上的风险而言,如果你在开发文档里明确写下开发周期和发布模型,contributor 是乐于见到自己参与或主导开发的工作随新版本一起发布的。越是自我驱动参与开源社区的 contributor 越重视积累信誉。这个过程中,如果你作为 shepherd 指导或参与进去,只需要切实地关注和解决开发团队成员遇到的困难,并在需要时帮助他们管理好进度。

其他的沟通技巧和 maintainer 的最佳实践这里不再展开。Open Source Guides 提供了这个话题非常有益的补充,推荐延伸阅读。

Have fun

不论是 contributor 还是 maintainer 你都已经通过参与开源社区为社会创造出了价值。时不时想想你为什么要参与或维护这个项目,回顾这个项目已经取得的成就。你已经做得很好了。

软件都有自己的生命周期,开源软件也不例外。开源社区的工作也不是你生活的全部。如果你找到了新的乐趣,完全可以把项目交给其他 maintainers 维护,或者直接归档。如果开源项目的维护已经超出你的能力范围或者消耗了太多的时间精力,也可以休息一段时间甚至放弃对项目的维护。作为开源社区成员的你没有义务非得维护这个项目或者响应别人的请求。你把自己的工作自由的提供给其他人利用,已经创造了非常客观的价值。

企业如何实践开源协同

随着开源概念的红火,越来越多的企业将内部项目公开托管到 GitHub 等平台,也有越来越多依托开源项目建立起来的企业。对于这些企业来说,它们的目标不只是开放项目源代码,更希望能够形成开源共同体,打造围绕项目的软件生态。

然而,其中大部分项目由于成员背景的单一性,最终都终结于仅源码可得的形态。对于这些新兴项目来说,初始成员从属于同一企业是既定事实。在这样的前提下,企业应该如何实践开源协同,形成开源共同体呢?

共享工作流

从开发者的角度出发,根本问题是要共享工作流。共享工作流,即项目开发的核心流程只有一套,所有 contributor 无论背景都基于这套核心流程工作。

对于企业内部项目开放源代码的情况,要做到这一点并不容易。项目往往在企业内部已经有一套成熟的工作流。如果在设计开源方案的时候,没有把共享工作流考虑在内,即使代码公开,大部分开发流程也会保持在企业内部。如果 contributor 不是企业员工,则根本无法参与。

Case Study: OceanBase

这个问题的典型案例是 OceanBase 项目。

OceanBase 项目的源代码托管在 GitHub 和 Gitee 两个平台上,同时接受问题报告和补丁提交。通常来说,一个项目只会有唯一的问题报告和补丁提交方式。例如,Linux 采用 Bugzilla 记录问题,邮件列表提交和评审补丁。GitHub 上有 Linux 的镜像,但是是只读的。其他的例子包括 GCC 和 PostgreSQL 等,都会有唯一的工作流,其他代码仓库只是镜像。OceanBase 两边都接受问题报告和补丁提交,反而是对两边的反馈都不重视。

可以猜测,它的核心流程既不是 GitHub 上的工作流,也不是 Gitee 上的,而是企业内部的工作流。这种情况下,能从开放可参与的平台上提交的大概率就只有简单的拼写错误或者代码重构补丁。因为即使是资深的开发者,缺少必要的信息和充分的讨论,也无法更进一步参与。实际情况也是如此,内部的活动别说讨论和设计文档,就连提交都不是实时同步的。此外,项目在两个平台上的活动,基本只有一名维护者出面在处理。

企业开放内部项目源代码,允许任何人学习和使用,是有社会价值的。但是内外两套工作流,甚至开放可参与的工作流只是个添头,那就不可能形成开源共同体。如果这就是预期的目标,那倒也没事。只是对于辛苦应付这些留下来的缺口进来的简单补丁的维护者来说,他是否会觉得这只是另一种值班呢?无论如何,工作流的统一都有助于减少损耗。不管是干脆只保留内部工作流,托管平台上的所有活动都没有回应保证,还是尝试融合到开放工作流,真正做到开源协同,都比牺牲一部分人,做一些创造出来的边缘工作要好。

Case Study: Apache InLong (incubating)

致力于融合到开放工作流的典型案例是 Apache InLong (incubating) 项目。这个项目是由腾讯捐赠给 Apache 软件基金会的数据流处理平台。

在项目开放初期,也存在只有内部工作流的情形。不过得益于主要维护者的软件工程经验,在明确项目要以开源协同的方式运作以后,经过对维护两套开发流程弊端的分析,得出了要融合工作流的结论。既然是开源协同,那么融合的工作流就是共享工作流了。

一段时间的改造后,原先内部工作流的核心流程被迁移到共享工作流当中,包括问题报告、补丁提交和版本发布。原先内部工作流服务于企业需求的部分则基于共享工作流构建。

企业内部仍然有用户问题报告,但是归结到项目本身缺陷的问题,会脱敏之后报告到 GitHub Issue 上。为了解决紧急问题,企业内部的 fork 版本仍然会打临时补丁快速上线,但是救火之后正式修复的补丁会以 contributing back 的形式提交到开源项目上。最后是版本发布。一开始,只有内部项目在发版。开放源代码之后,就有两个同类项目要分开发版。经过一系列的改进,主要是问题报告和补丁提交的及时同步,最终两个项目能够以较小的同步开销同时发版。换句话说,GitHub 上托管的版本,就是企业内部使用的版本。企业内部可能有一些临时补丁,但是并不构成一个差异化内部版本,并且这些补丁是积极地被推进 contributing back 上游的。

可以看到,确定开源协同开发项目的方向后,共享工作流不是形式主义,而是能切实提高软件工程效率和减少摩擦的方案。

对于企业本身依托开源项目建立的情况,要维持共享工作流也存在很多挑战。这些挑战大多出自一个原因,那就是最佳实践的匮乏导致节外生枝的私下讨论。

Case Study: TiDB

TiDB 的代码仓库中有专门存放设计文档的目录。理论上,新功能,行为变更,以及其他重要改动,都需要一个设计文档。

我们可以从设计文档的时间线看出这一工作流的变迁。

  • 2018 年下半年,共 17 份设计文档。
  • 2019 年全年,共 6 份设计文档。
  • 2020 年全年,共 13 份设计文档。
  • 2021 年至今,共 19 份设计文档。

从另一个维度看,2019 年 5 月到 10 月,2020 年 10 月到次年 2 月,一共将近一年的时间里,项目没有提出过任何设计文档。

那么,TiDB 项目在此期间是停止开发了吗?没有。它一直以每个工作日合并 10 个 PR 以上的开发速度在前进。在此期间关于功能设计的讨论,其实是转进了企业的即时通讯工具或内部文档当中了。我们可以看几个例子。

这几个功能并不是没有设计,而是只在小范围内通过中文文档做出设计,就开始实现。甚至在 Cardinality Estimation Enhancement 的例子当中,以为 contributor 想了解功能设计和背景,被 assignee 以时间紧迫为由回绝。虽然 assignee 承诺会在完成后进一步披露消息,但是却没了下文。

另一个例子是 pull request 上的检查项变更。不仅整个过程是在企业内部决策后直接在开源项目上上线,共同体内的其他成员一无所知,而且对于 bad case 的处理依赖于企业内部的群聊,让人摸不着头脑。

其实这些案例,我相信相关成员并不是刻意要伤害开源共同体。设计和开发的需求是天然存在的,持续集成的改动也不是不能做,但是实际推动落实的成员,缺乏开源共同体当中工作的经验,难以站在一家企业之上的视角,以合理的方式开展工作,才导致了这些实际伤害了开源共同体的做法。

我在这两个方向都做过一些改良的工作。对于设计文档,我发起了一个 Public Design 的讨论,并且推动了几个重大改动的公开设计。在此过程中和复数的开发者沟通了公开设计的技巧,以及在此前提下如何高效地推进重要改动的落实。实际上,公开设计并不会损失效率。因为并不是内部讨论完成后拿出来公示,而是从一开始就放在公开渠道讨论。既然是开源协同,补丁提交本身也是公开的,这些材料有什么好隐藏的呢?相反,因为得到了潜在的更多反馈,能够在设计等早期阶段避免缺陷,反而公开设计是更加高效的手段。

对于持续集成,企业内部把研发和工程效能分成两个竖井,又把开源共同体仅关联到研发的工作上去,是这个问题的根源。组织结构问题不好解,只能先改变工程效能团队的员工的认知。当他以开源共同体成员的身份变更项目基础设施的时候,也通过提交议题,达成共识后实施的工作流来推进。实际上,这样改变以后,关注到项目功能开发的成员与维护基础设施的成员更能坦诚的交换意见,避免意料之外的改变激发矛盾。

Case Study: Taichi

Taichi 是一个主要面向计算机图形学的并行编程框架,由胡渊鸣博士发明。去年,他作为联合创始人创立了太极图形公司来支持项目的发展。

项目早期基本是胡老师一个人的工作。开放源代码并有 contributor 加入后,画风是这样的。

这两个 pull request 的三位参与者,彼时分别在美国波士顿、日本东京和中国上海。当时也没有成立公司,更不谈有企业内部的即时通讯工具或文档空间。所以你可以看到所有必要的讨论都发生在 GitHub 平台上。

时间拉回到现在,部分项目的开发仍然是有迹可循的。比如有个置顶的 RoadMap 作为当前正在投入的工作的地图,比如 Taichi 编译器前端类型检查有个 tracking issue 来记录工作。

不过,也会出现我在昨天看到的无描述 4000 行改动无评论合并的案例。

经过社交媒体的传播,目前这个 pull request 更新了部分描述。其实是一个学术研究相关的功能,在发出论文后希望 contributing back 到上游。由于变更较为复杂,早期设计出于研究原因不便公开,加上持续集成流水线的效率问题,所以采用了一步到位的合并方案。代码 review 私下发生在提交之前。

那么,这些信息昨天凌晨看到的我能够知道吗?答案是不能。

其实这种提交一个大改动的案例并不少见。Apache Flink 项目曾经多次发生过这样的事情,包括 2014 年 7 月合并 streaming 的原型,2019 年合并阿里巴巴内部版本 BLINK 等等。项目接受来自企业或学术团体的 contribution 是很正常的,其他开源项目也有研究室基于项目做出优化策略后 contributing back 的案例。

开源共同体接受 contribution 的标准做法仍然是公开讨论。只需要说明这件事情,解答潜在的疑问之后决定接受或拒绝 contribution 即可。如果 ti.Mesh 的研究结果是以这样的形式合并到代码仓库的,我想在一开始我就不会有疑惑和疑惑导致的误会。另一方面,公开讨论和 contribute 对开源项目也是一种保护。Apache 项目在接收重要 contribution 时都会考虑引入一个知识产权清理流程,确保接收 contribution 不会引入知识产权相关的争端。

Taichi 项目当中缺乏背景信息的还有这些例子。

当然,必须说明的是 Taichi 项目的大部分 pull request 是有背景信息的。上面这些案例的参与者,我想也不是刻意隐藏信息,而是成立公司之后,自然地在线下或者内部平台讨论。既然已经通过私下讨论得出结论,再刻意搬到 GitHub 上反而就是低效的。对于具备项目假设 contributor 应该有的知识就能理解的补丁,也不需要做作的讨论。

要想避免因为已经私下讨论得出结论,从而把共同工作流的一部分切换成内部工作流的情况,应该从两个方面入手。

第一个是在确定开源协同开发项目的方向后,所有技术讨论都以 GitHub 平台的内容为唯一信源。私下讨论是无法禁止的,只能从技术领袖开始以身作则,推动公开讨论。其实对于大部分企业员工来说,在哪讨论并不重要。真正让他们转向私下讨论的原因,是在 GitHub 上的评论得不到回复,而钉一下或者内部文档 at 有奇效。值得一提的是,Taichi 也有我曾经到的 TiDB 的问题,那就是没有一个活跃的开放式讨论渠道,即没有邮件列表的替代品。有个 Discourse 论坛,但是是面向中文用户而不是全球开发者的。开通了 GitHub Discussion 功能,但是只有唯一一个版本发布的公告。

第二个是作为共同体的领袖,应当积极寻找不同背景的参与者。如果已经形成了私下讨论的习惯,仅仅要求员工改变习惯是很难有效的。因为公开讨论的主要原因,是为了和企业以外的 contributor 交流,以获得有意义的输入和提高生产力。如果员工发现换个地方发言,得到的回应还是同事的回应,并且 GitHub 上的评论还是得不到即时的回复,这件事就推不下去。

前面的例子提到过,当 Taichi 的主要开发者天各一方,没有成立公司之前,这种沟通是自然而然的。实际上,Linux 和 Apache Httpd 也是这样的。除了邮件列表,Linus 很难找到另一个渠道收获他所需要的反馈。Apache Httpd 的早期成员一开始就是在邮件列表上沟通的。只有实际存在组织以外的高水平参与者,开源协同的最佳实践才有意义。对于企业员工来说,也才有直接合理的理由不在内部讨论。毕竟就某个特定的问题,他更希望听一听那个不同背景的共同体成员的意见。

招募新成员

寻找不同背景的参与者,其实就是作为共同体的领袖为共同体招募新成员。这是企业实践开源协同的另一个难题。除了为企业招募以外,应该如何为共同体招募呢?

End user

第一个要讨论的是用户。不过,用户是开源协同之外的内容。商业产品同样需要自己的用户。大部分用户也不会关心软件是如何实现的。

所以,要讨论用户,其实是要驳斥一些错误的观念。用户能够为你提供使用反馈,能够通过付费或捐赠支持项目开发人员持续投入,但是期待从用户群体中大规模地发现核心 contributor 则是不切实际的。

我听到过很多项目领袖跟我说,他的项目是独特的,因为不像大数据项目那样,用户本身也是开发者。它可能是一个数据库。哎呀,用户都是 DBA 或者数据分析师,根本不知道数据库怎么实现的嘛。它可能是一个机器学习框架。哎呀,用户都只会操作 Python 接口,根本搞不来核心 C++ 代码。

那我就想问了,你咋不去找那些就做数据库的人,就搞机器学习框架的人呢?你给团队招聘的时候知道找这些人,怎么到了给共同体招募新成员,眼里就只看到用户了。

其实我也可以理解。因为开源协同不够普及,大部分人提到 open-source 这个概念,第一印象还是一个市场营销的手段。或者提到“运营开源社区”,就把用户社区那些已有经验都搬过来。在这样的认识下 open-source community 就是开源社区,而不是开源共同体。其中“我们”是唯一的开发者,是懂行的。其他人是只会小修小补的爱好者,或者干脆啥也不懂的用户。

这个误区有点像思维定式。你现在要找的是有能力开发项目的参与者,那就去对应的群体里找就可以了。

当然,如果你就想做用户社区,就没打算搞开源协同,也是一种选择。对于这类需求,我建议研究 MongoDB 的做法。它们搞得挺好,这里就不展开了。

Ecosystem

抛开用户不谈,开源共同体当中的 contributor 还可以进一步细分。其中有一类 contributor 关注生态互连,另一类关注项目的核心逻辑。

如果项目提供了足够多的扩展点,或者策略替换机制,那么关注生态互连的 contributor 就能够快速参与进来。

例如,Flink/Spark/Presto 等项目都设计了 connector 机制,连来连去就能创造出大量的工作。例如,几乎所有项目都可以搞多语言 SDK 玩玩。TiKV 就有不少于五种编程语言的客户端实现。例如,PostgreSQL 提供 FDW 机制,不仅支持连接外部数据源,更暴露了参与 planning 阶段的计算下推接口。例如,Linux 其实也有丰富的扩展机制,支持多种架构和驱动就是一个例子。

上面这些都是项目本身的机制,更广泛的生态还包括解决方案的整合。例如,从 Netty 的角度看,Flink 就是它的生态的一部分。从 Flink 的角度看,serverless 技术栈 StateFun 又是它的生态的一部分。经常听 database 的开发者说自己的软件直面终端用户,但是其实就互联网业务开发者来说,中间是隔了一层 ORM 框架的。哪怕是数据分析师,大概率也隔了一层可视化框架。另外,数据的同步和搬迁也是应用设计不可缺少的一部分,这就是各种中间件能发挥作用的地方了。

总之,这类 contributor 还可以再细分。一类是关注项目提供的机制替换实现的,大部分可以从有可能提供实现的项目开发者当中寻找。例如项目的部署机制希望支持 Kubernetes 环境,那找一个热衷于写 Kubernetes Operator 或者刚学会跃跃欲试的开发者参与,就很有可能产生正面效果。另一类是关注项目整合形成用户解决方案的。实际上,项目开发者最终基于项目实现盈利,往往就是以某种解决方案出现。只要你发挥想象力,生态整合的可能性就是个乘法,不愁找不到参与者。即使是核心逻辑被单一企业掌控的 MongoDB 项目,其生态也是非常繁荣的。

Kernel

当然,项目的核心逻辑也是非常重要的。如果项目本身不够坚挺,那么就不会有用户使用,也无法激起 contributor 连接生态的动力。

项目的核心逻辑是一个项目的主要价值。这些逻辑通常由项目的初始成员定义。在企业主导项目的情况下,这些初始成员往往背景单一。同时,出于传统组织观念的影响,初始成员往往以企业当中的项目团队作为自我认同,团队等同于项目,也因此将核心逻辑的开发层层“保护”在看不见的高墙之内。

以项目团队作为自我认同,无怪乎招募新成员的时候,自我认知自动翻译成团队招聘,而想不到还有其他可能性。

反观成功的开源项目,数据湖项目 Apache Hudi 由 Uber 捐赠给 Apache 软件基金会,在项目快速发展过程中吸引到了阿里巴巴和 T3 出行等企业的员工的参与,并吸纳了上述企业背景的开发者作为项目 PMC 成员。对于后续参与的企业的员工来说,他们在企业当中虽然也有项目团队,但是显然不会觉得项目归企业内的项目团队所有。对于 Uber 来说,来自其他企业的核心 contributor 的声音也不可忽视。这样,Apache Hudi 成功建立了一个开源共同体。

要想为项目招募开发核心逻辑的参与者,我觉得应该做到以下三点。

第一点是改变认知。上面已经介绍了错误认知的危害和避免错误认知的最终形态。我把这种正确的认知称为“开发者的两顶帽子”。同一个开发者,既是开源共同体的参与者,也是企业的员工。这两个身份虽然从属于同一个人,但是却有着不同的诉求。只有区分开这些不同的诉求,一部分是开源共同体的目标,一部分是企业基于开源项目创造商业价值的目标,才能避免认知混乱导致人为制造出参与的高墙。

第二点是公开讨论。前面讨论的很详细了,这里再补充一个点。当你真的身处一个开源共同体当中,不做公开讨论才是奇怪的。例如 Apache Hudi 的例子,如果 T3 出行的开发者想要实现某个功能,除了公开讨论寻求共识,别无他法。

公开讨论还有一个额外的好处,那就是方便引用。不少基于开源项目建立起来的企业,运营人员整天发愁哪里有技术内容可以发布,写技术文章好像变成了一个苦差事。其实技术话题公开讨论,天然的就有高质量的内容可以推送,其中悬而未决的议题,也是 contributor 参与的绝佳切入点。例如 Engula 项目在社交媒体的输出,基本就是设计文档或者开放式讨论里值得发布的内容。

最后一点是积极招募。前面分析 Taichi 的例子也提到过,认知改变的假设需要多样化的开源共同体成员来验证,保持公开讨论的做法也需要不同背景的 contributor 参与。除了公开讨论能够吸引到潜在的参与者,积极招募更意味着共同体的领袖要主动思考谁是你要找的人。

对于每个项目来说,这个问题的答案都不一样。但是认为这个问题没有答案,或者说人才都在企业当中了,则是一种傲慢。

同样举数据库的例子,哪怕你有 Oracle 那么大,世界上也还有相当一批人在开发 PostgreSQL 等项目。这些人并不是一辈子就做这一件事的。只要你的项目足够有趣,他们就有可能投入。

另一方面,泛泛而谈数据库这样一个复杂的领域其实是一种懒惰。既然复杂的项目本身会分模块开发,为什么在招募新成员的时候就只想着完全理解整个领域的人呢?如果项目的并发设计不佳,只要是精通该语言并发编程的专家,愿意 contribute 做改进,你管他懂不懂数据库的专业概念。醉心于编译器前端的开发者,也许能解决 SQL Parser 当中经年的性能问题。进入 Apache 孵化器的项目的导师,往往也不是项目所在领域的专家,甚至不是开发者,但是他们能够帮助项目以 Apache 的方式建立起开源共同体。

以这样的方式去寻找潜在的开发核心逻辑的成员,相信你的视野会更加广阔。

其实,这才是“开源共同体”的含义。不止于项目,也不是社区居委会,而是围绕开源项目的发展,基于对项目的认同,形成的多层次合作共同体。

❌