夜天之书 #34 企业如何实践开源协同
随着开源概念的红火,越来越多的企业将内部项目公开托管到 GitHub 等平台,也有越来越多依托开源项目建立起来的企业。对于这些企业来说,它们的目标不只是开放项目源代码,更希望能够形成开源共同体,打造围绕项目的软件生态。
然而,其中大部分项目由于成员背景的单一性,最终都终结于仅源码可得的形态。对于这些新兴项目来说,初始成员从属于同一企业是既定事实。在这样的前提下,企业应该如何实践开源协同,形成开源共同体呢?
共享工作流
从开发者的角度出发,根本问题是要共享工作流。共享工作流,即项目开发的核心流程只有一套,所有 contributor 无论背景都基于这套核心流程工作。
对于企业内部项目开放源代码的情况,要做到这一点并不容易。项目往往在企业内部已经有一套成熟的工作流。如果在设计开源方案的时候,没有把共享工作流考虑在内,即使代码公开,大部分开发流程也会保持在企业内部。如果 contributor 不是企业员工,则根本无法参与。
Case Study: OceanBase
这个问题的典型案例是 OceanBase[1] 项目。
OceanBase 项目的源代码托管在 GitHub 和 Gitee 两个平台上,同时接受问题报告和补丁提交。通常来说,一个项目只会有唯一的问题报告和补丁提交方式。例如,Linux 采用 Bugzilla 记录问题,邮件列表提交和评审补丁。GitHub 上有 Linux 的镜像,但是是只读的。其他的例子包括 GCC 和 PostgreSQL 等,都会有唯一的工作流,其他代码仓库只是镜像。OceanBase 两边都接受问题报告和补丁提交,反而是对两边的反馈都不重视。
可以猜测,它的核心流程既不是 GitHub 上的工作流,也不是 Gitee 上的,而是企业内部的工作流。这种情况下,能从开放可参与的平台上提交的大概率就只有简单的拼写错误或者代码重构补丁。因为即使是资深的开发者,缺少必要的信息和充分的讨论,也无法更进一步参与。实际情况也是如此,内部的活动别说讨论和设计文档,就连提交都不是实时同步的。此外,项目在两个平台上的活动,基本只有一名维护者出面在处理。
企业开放内部项目源代码,允许任何人学习和使用,是有社会价值的。但是内外两套工作流,甚至开放可参与的工作流只是个添头,那就不可能形成开源共同体。如果这就是预期的目标,那倒也没事。只是对于辛苦应付这些留下来的缺口进来的简单补丁的维护者来说,他是否会觉得这只是另一种值班呢?无论如何,工作流的统一都有助于减少损耗。不管是干脆只保留内部工作流,托管平台上的所有活动都没有回应保证,还是尝试融合到开放工作流,真正做到开源协同,都比牺牲一部分人,做一些创造出来的边缘工作要好。
Case Study: Apache InLong (incubating)
致力于融合到开放工作流的典型案例是 Apache InLong (incubating)[2] 项目。这个项目是由腾讯捐赠给 Apache 软件基金会的数据流处理平台。
在项目开放初期,也存在只有内部工作流的情形。不过得益于主要维护者的软件工程经验,在明确项目要以开源协同的方式运作以后,经过对维护两套开发流程弊端的分析,得出了要融合工作流的结论。既然是开源协同,那么融合的工作流就是共享工作流了。
一段时间的改造后,原先内部工作流的核心流程被迁移到共享工作流当中,包括问题报告、补丁提交和版本发布。原先内部工作流服务于企业需求的部分则基于共享工作流构建。
企业内部仍然有用户问题报告,但是归结到项目本身缺陷的问题,会脱敏之后报告到 GitHub Issue 上。为了解决紧急问题,企业内部的 fork 版本仍然会打临时补丁快速上线,但是救火之后正式修复的补丁会以 contributing back 的形式提交到开源项目上。最后是版本发布。一开始,只有内部项目在发版。开放源代码之后,就有两个同类项目要分开发版。经过一系列的改进,主要是问题报告和补丁提交的及时同步,最终两个项目能够以较小的同步开销同时发版。换句话说,GitHub 上托管的版本,就是企业内部使用的版本。企业内部可能有一些临时补丁,但是并不构成一个差异化内部版本,并且这些补丁是积极地被推进 contributing back 上游的。
可以看到,确定开源协同开发项目的方向后,共享工作流不是形式主义,而是能切实提高软件工程效率和减少摩擦的方案。
对于企业本身依托开源项目建立的情况,要维持共享工作流也存在很多挑战。这些挑战大多出自一个原因,那就是最佳实践的匮乏导致节外生枝的私下讨论。
Case Study: TiDB
TiDB[3] 的代码仓库中有专门存放设计文档的目录。理论上,新功能,行为变更,以及其他重要改动,都需要一个设计文档。
我们可以从设计文档的时间线看出这一工作流的变迁。
•2018 年下半年,共 17 份设计文档。•2019 年全年,共 6 份设计文档。•2020 年全年,共 13 份设计文档。•2021 年至今,共 19 份设计文档。
从另一个维度看,2019 年 5 月到 10 月,2020 年 10 月到次年 2 月,一共将近一年的时间里,项目没有提出过任何设计文档。
那么,TiDB 项目在此期间是停止开发了吗?没有。它一直以每个工作日合并 10 个 PR 以上的开发速度在前进。在此期间关于功能设计的讨论,其实是转进了企业的即时通讯工具或内部文档当中了。我们可以看几个例子。
•Raft Async IO[4]•SPM Enhancement[5]•Cardinality Estimation Enhancement[6]
这几个功能并不是没有设计,而是只在小范围内通过中文文档做出设计,就开始实现。甚至在 Cardinality Estimation Enhancement 的例子当中,以为 contributor 想了解功能设计和背景,被 assignee 以时间紧迫为由回绝。虽然 assignee 承诺会在完成后进一步披露消息,但是却没了下文。
•Announcing remove required integration test check when merge pr[7]
另一个例子是 pull request 上的检查项变更。不仅整个过程是在企业内部决策后直接在开源项目上上线,共同体内的其他成员一无所知,而且对于 bad case 的处理依赖于企业内部的群聊,让人摸不着头脑。
其实这些案例,我相信相关成员并不是刻意要伤害开源共同体。设计和开发的需求是天然存在的,持续集成的改动也不是不能做,但是实际推动落实的成员,缺乏开源共同体当中工作的经验,难以站在一家企业之上的视角,以合理的方式开展工作,才导致了这些实际伤害了开源共同体的做法。
我在这两个方向都做过一些改良的工作。对于设计文档,我发起了一个 Public Design 的讨论,并且推动了几个重大改动的公开设计。在此过程中和复数的开发者沟通了公开设计的技巧,以及在此前提下如何高效地推进重要改动的落实。实际上,公开设计并不会损失效率。因为并不是内部讨论完成后拿出来公示,而是从一开始就放在公开渠道讨论。既然是开源协同,补丁提交本身也是公开的,这些材料有什么好隐藏的呢?相反,因为得到了潜在的更多反馈,能够在设计等早期阶段避免缺陷,反而公开设计是更加高效的手段。
•Discuss: Public Design[8]•Tracking issue for Region Label Feature[9]•RFC: Substitute RocksDB write stall[10]•Tracking issue for heuristic rules enhancement for index selection[11]
对于持续集成,企业内部把研发和工程效能分成两个竖井,又把开源共同体仅关联到研发的工作上去,是这个问题的根源。组织结构问题不好解,只能先改变工程效能团队的员工的认知。当他以开源共同体成员的身份变更项目基础设施的时候,也通过提交议题,达成共识后实施的工作流来推进。实际上,这样改变以后,关注到项目功能开发的成员与维护基础设施的成员更能坦诚的交换意见,避免意料之外的改变激发矛盾。
•Integrate UT coverage with CI pipeline[12]
Case Study: Taichi
Taichi[13] 是一个主要面向计算机图形学的并行编程框架,由胡渊鸣博士发明。去年,他作为联合创始人创立了太极图形公司来支持项目的发展。
项目早期基本是胡老师一个人的工作。开放源代码并有 contributor 加入后,画风是这样的。
•[async] Implement basic StateFlowGraph[14]•Allow ti test_python to take in individual test files[15]
这两个 pull request 的三位参与者,彼时分别在美国波士顿、日本东京和中国上海。当时也没有成立公司,更不谈有企业内部的即时通讯工具或文档空间。所以你可以看到所有必要的讨论都发生在 GitHub 平台上。
时间拉回到现在,部分项目的开发仍然是有迹可循的。比如有个置顶的 RoadMap[16] 作为当前正在投入的工作的地图,比如 Taichi 编译器前端类型检查[17]有个 tracking issue 来记录工作。
不过,也会出现我在昨天看到的无描述 4000 行改动无评论合并的案例。
•[Mesh] The ti.Mesh class & mesh-for loop implementation[18]
经过社交媒体的传播,目前这个 pull request 更新了部分描述。其实是一个学术研究相关的功能,在发出论文后希望 contributing back 到上游。由于变更较为复杂,早期设计出于研究原因不便公开,加上持续集成流水线的效率问题,所以采用了一步到位的合并方案。代码 review 私下发生在提交之前。
那么,这些信息昨天凌晨看到的我能够知道吗?答案是不能。
其实这种提交一个大改动的案例并不少见。Apache Flink 项目曾经多次发生过这样的事情,包括 2014 年 7 月合并 streaming 的原型,2019 年合并阿里巴巴内部版本 BLINK 等等。项目接受来自企业或学术团体的 contribution 是很正常的,其他开源项目也有研究室基于项目做出优化策略后 contributing back 的案例。
•[DISCUSS] A strategy for merging the Blink enhancements[19]•[ANNOUNCE] Contributing Alibaba's Blink[20]
开源共同体接受 contribution 的标准做法仍然是公开讨论。只需要说明这件事情,解答潜在的疑问之后决定接受或拒绝 contribution 即可。如果 ti.Mesh 的研究结果是以这样的形式合并到代码仓库的,我想在一开始我就不会有疑惑和疑惑导致的误会。另一方面,公开讨论和 contribute 对开源项目也是一种保护。Apache 项目在接收重要 contribution 时都会考虑引入一个知识产权清理[21]流程,确保接收 contribution 不会引入知识产权相关的争端。
Taichi 项目当中缺乏背景信息的还有这些例子。
•[bug] Remove fallback in C++ code[22]•[gui] Show f16 image as f32.[23]•[Lang] Support more SNode trees for LLVM backends[24]
当然,必须说明的是 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[25] 由 Uber 捐赠给 Apache 软件基金会,在项目快速发展过程中吸引到了阿里巴巴和 T3 出行等企业的员工的参与,并吸纳了上述企业背景的开发者作为项目 PMC 成员。对于后续参与的企业的员工来说,他们在企业当中虽然也有项目团队,但是显然不会觉得项目归企业内的项目团队所有。对于 Uber 来说,来自其他企业的核心 contributor 的声音也不可忽视。这样,Apache Hudi 成功建立了一个开源共同体。
要想为项目招募开发核心逻辑的参与者,我觉得应该做到以下三点。
第一点是改变认知。上面已经介绍了错误认知的危害和避免错误认知的最终形态。我把这种正确的认知称为“开发者的两顶帽子”。同一个开发者,既是开源共同体的参与者,也是企业的员工。这两个身份虽然从属于同一个人,但是却有着不同的诉求。只有区分开这些不同的诉求,一部分是开源共同体的目标,一部分是企业基于开源项目创造商业价值的目标,才能避免认知混乱导致人为制造出参与的高墙。
第二点是公开讨论。前面讨论的很详细了,这里再补充一个点。当你真的身处一个开源共同体当中,不做公开讨论才是奇怪的。例如 Apache Hudi 的例子,如果 T3 出行的开发者想要实现某个功能,除了公开讨论寻求共识,别无他法。
公开讨论还有一个额外的好处,那就是方便引用。不少基于开源项目建立起来的企业,运营人员整天发愁哪里有技术内容可以发布,写技术文章好像变成了一个苦差事。其实技术话题公开讨论,天然的就有高质量的内容可以推送,其中悬而未决的议题,也是 contributor 参与的绝佳切入点。例如 Engula[26] 项目在社交媒体的输出,基本就是设计文档或者开放式讨论里值得发布的内容。
最后一点是积极招募。前面分析 Taichi 的例子也提到过,认知改变的假设需要多样化的开源共同体成员来验证,保持公开讨论的做法也需要不同背景的 contributor 参与。除了公开讨论能够吸引到潜在的参与者,积极招募更意味着共同体的领袖要主动思考谁是你要找的人。
对于每个项目来说,这个问题的答案都不一样。但是认为这个问题没有答案,或者说人才都在企业当中了,则是一种傲慢。
同样举数据库的例子,哪怕你有 Oracle 那么大,世界上也还有相当一批人在开发 PostgreSQL 等项目。这些人并不是一辈子就做这一件事的。只要你的项目足够有趣,他们就有可能投入。
另一方面,泛泛而谈数据库这样一个复杂的领域其实是一种懒惰。既然复杂的项目本身会分模块开发,为什么在招募新成员的时候就只想着完全理解整个领域的人呢?如果项目的并发设计不佳,只要是精通该语言并发编程的专家,愿意 contribute 做改进,你管他懂不懂数据库的专业概念。醉心于编译器前端的开发者,也许能解决 SQL Parser 当中经年的性能问题。进入 Apache 孵化器的项目的导师,往往也不是项目所在领域的专家,甚至不是开发者,但是他们能够帮助项目以 Apache 的方式建立起开源共同体。
以这样的方式去寻找潜在的开发核心逻辑的成员,相信你的视野会更加广阔。
其实,这才是“开源共同体”的含义。不止于项目,也不是社区居委会,而是围绕开源项目的发展,基于对项目的认同,形成的多层次合作共同体。
References
[1]
OceanBase: https://github.com/oceanbase/oceanbase[2]
Apache InLong (incubating): https://github.com/apache/incubator-inlong[3]
TiDB: https://github.com/pingcap/tidb/tree/master/docs/design[4]
Raft Async IO: https://github.com/tikv/tikv/issues/10540[5]
SPM Enhancement: https://github.com/pingcap/tidb/issues/25970[6]
Cardinality Estimation Enhancement: https://github.com/pingcap/tidb/issues/26085[7]
Announcing remove required integration test check when merge pr: https://internals.tidb.io/t/topic/372/7[8]
Discuss: Public Design: https://internals.tidb.io/t/topic/399[9]
Tracking issue for Region Label Feature: https://github.com/tikv/pd/issues/3839[10]
RFC: Substitute RocksDB write stall: https://github.com/tikv/rfcs/pull/67[11]
Tracking issue for heuristic rules enhancement for index selection: https://github.com/pingcap/tidb/issues/26020[12]
Integrate UT coverage with CI pipeline: https://github.com/pingcap/tidb/issues/28947[13]
Taichi: https://github.com/taichi-dev/taichi[14]
[async] Implement basic StateFlowGraph: https://github.com/taichi-dev/taichi/pull/1836[15]
Allow ti test_python to take in individual test files: https://github.com/taichi-dev/taichi/pull/479[16]
RoadMap: https://github.com/taichi-dev/taichi/issues/2398[17]
Taichi 编译器前端类型检查: https://github.com/taichi-dev/taichi/issues/3301[18]
[Mesh] The ti.Mesh class & mesh-for loop implementation: https://github.com/taichi-dev/taichi/pull/3567[19]
[DISCUSS] A strategy for merging the Blink enhancements: https://lists.apache.org/thread/mc4622swvv1y4dbty5x20rlh3whdjst5[20]
[ANNOUNCE] Contributing Alibaba's Blink: https://lists.apache.org/thread/mn1nf4p11t054zmhdglorjt40xoyz7wv[21]
知识产权清理: http://incubator.apache.org/ip-clearance/[22]
[bug] Remove fallback in C++ code: https://github.com/taichi-dev/taichi/pull/3538[23]
[gui] Show f16 image as f32.: https://github.com/taichi-dev/taichi/pull/3433[24]
[Lang] Support more SNode trees for LLVM backends: https://github.com/taichi-dev/taichi/pull/3279[25]
Apache Hudi: http://hudi.apache.org/[26]
Engula: https://twitter.com/engulaio