科技界的大多数新生事物都只是一波又一波的潮流:说话和做事的模式来了又走,没有留下永恒的印记。微内核,IA-64 架构,对象请求代理,20 世纪 90 年代的神经网络,这些东西都已经不复存在,也不会再回来了。时间已经证明了哪些东西是昙花一现,为了说明问题,我们必须追溯到很久以前。
今天的我们很难想象,在鼎盛时期,那些技术有多么的受欢迎。它们有很多极具魅力、真诚且聪明的倡导者,倡导者们得到了看似合理的基本原理论证的支持,这些论证证明了他们选择的技术必然会取得胜利。这些潮流催生了运动、宣言、会议和公司。但请不要把这些潮流与蓄意欺诈混为一谈,后者要少见得多。推动这些技术潮流的动机是发自内心的真诚,不管它们以何种方式出场,二者产生的结果是完全不同的。
另一方面,一些重要的新技术是革命性的:它们强大而持久的变化为技术采用者带来了长期的优势。面向对象编程、硬件虚拟化、万维网、公有云、CI/CD 和 20 世纪 90 年代的神经网络(深度学习的重生)现在成了计算机世界永久的组成部分,而它们曾经“混迹”在潮流之中,难以区分。我们被技术浪潮所包围,在它们展露头角之前,我们并不知道如何用它们来获得成功。
与其他科技公司一样,Slack 希望在正确的时间采用革命性的技术,避免把过多的精力浪费在追逐潮流上。Slack 采取了什么样的战略来确保做到这一点?这篇文章介绍了 Slack 用来解决这个问题的方法。
区分潮流和革命
我们不能依靠个别领导者的直觉来区分哪个才是赢家。相反,我们要积极地探索新事物,虽然我们也知道这些尝试大部分都不会有任何回报。但为了让我们的投入偏向于有用的新事物,避免踩太多潮流的坑,我们应该在早期就要无情地扼杀那些没有价值的实验。我们希望每件事都去尝试一下,这意味着我们仍然要与潮流为伍。我们希望最终能够乘着潮流到达彼岸,我们与潮流打交道的经验不断为我们提供积极的回报。
技术采用曲线
我们创建了一个描述性的新技术采用曲线模型。
这是一条典型的 S 曲线,描述了技术的采用随时间变化的情况。S 形反映了技术采用的变化。一开始,当只有几个实验者在做实验时,我们别无选择,只能慢慢地接受。稍后,当情况变得清晰的时候,就会有更多的人参与进来,我们会在中间陡峭向上倾斜的部分快速地将新技术应用到生产中。当大多数有成果的试验圆满完成,就会剩下寥寥无几难啃的“硬骨头”。于是,在周期接近结束时,采用速度又慢了下来。
我们把这三个阶段画出来:
我们并不是第一波发现技术采用遵循这种 S 型曲线的人。Everett Rogers 在 1962 年的《创新扩散理论》(diffusion of innovation)一书中就已提出了这一模型。不过,Rogers 并没有提到 Erlang 或者 MongoDB,因为他是一位农村社会学家,他观察的是农业技术采用的模式。但事实证明,计算机领域与人类活动的其他领域并没有太大的不同。
为了让这个抽象的概念更具象一些,我们将例举一些已经在 Slack 经历了探索、扩张和迁移阶段的技术。
React
从 2015 年发布第一个稳定版本以来,React 席卷了前端开发领域。虚拟 DOM 和单向数据绑定让它成为开发 Slack 桌面用户界面的一项引人注目的技术。
2016 年,对于 Slack 的前端开发来说,React 还只是一项陌生的技术。在第一阶段,对 React 感兴趣的工程师开始尝试使用它,将它用在试验项目中。当他们确信 React 可以为 Slack 带来重要的价值时,他们开发了一个有说服力的演示项目,使用 React 重建了 Slack 的表情选择器。这个使用 React 开发的 Slack UI(之前有延迟感)比之前表现得更好。原型比理论验证或白板草图更容易让开发者接受。
当我们的团队意识到 React 将会对我们的代码库产生重大影响时,只是做一些零碎的小手术是无法让 React 的性能优势得以体现的。但进行大规模的重写也是不可能的,因为风险与回报比不允许我们这么做。当 React 进入第二阶段,一个真正的迁移项目开始启动了。项目制定了一个计划,并配备了大量的开发人员。随着项目的进行,有很多团队也选择了新的视图样式。但在这个中间阶段,很多旧的视图仍然存在,并需要维护。
在第三阶段,我们将客户端代码库中的遗留视图清理干净。我们终于在 2019 年 7 月发布了一个只使用 React 的桌面版 Slack。
Hack
在服务器端,我们从 2016 年开始从 PHP 迁移到 Hack。这次迁移的一个关键部分是在我们的 PHP 代码中逐步引入类型:
在 2017 年,我们进入了第一阶段,一些类型爱好者开始在代码库中使用简单的类型。
有些人在代码进入生产环境之前发现这些类型可以捕获 bug,于是也开始使用类型。这无意中就让 Hack 的类型系统进入了第二阶段(其他用户也受到了影响)。不过,新的类型注释也带来了一些问题,于是我们展开了一场有关标准静态类型与动态类型的讨论。通过讨论和积累经验,我们达成了一个大致的共识——加大类型使用规模利大于弊,并且大多数团队都选择使用类型。在第二阶段,我们做出了更大的努力进行代码迁移,让新加入的代码可以使用静态类型。
困难的部分留给了第三阶段。对 Slack 自身的内部对象变量进行合理化调整,并对一些复杂的核心模块进行转换,这些都是非常耗时的。
Vitess
Vitess 是一个用来进行 MySQL 水平扩展的集群系统,在进行数据分片策略演化过程中,我们选择了它。
第一阶段开始时,我们对 Vitess 的能力进行了严格的评审。我们花了很多时间在手动管理自有分片解决方案上,而 Vitess 的自动化能力解决了我们的大部分痛点。Vitess 团队最终确信这项技术是值得采用的。
将一些低风险的工作负载(如 RSS 提要)迁移到 Vitess 的工作始于第二阶段。在第二阶段早期不太需要 Vitess 团队之外的人参与,但因为是一个新的数据存储系统,所以仍然需要运维支持。随着越来越多的表被迁移到 Vitess,我们逐渐降低了风险,让 Vitess 满足了我们的应用场景需求。我们开发了用于回填的工具,制定了一些在迁移过程中会用到的术语(例如把重复数据叫作“暗读”),总结了可能会出现的各种问题。
我们已经迁移了数百个表,总计超过了查询工作负载的 50%,但在第三阶段仍然要处理一些“难表”和“长尾表”。一些关键表存在复杂的依赖关系和查询模式,它们比在第二阶段迁移的表更难处理。另外,我们有一些“长尾表”,进行逐张手动迁移很不值得,因此我们正在开发工具进行批量迁移。
LibSlack
与以上那些经历了各个采用阶段的技术相比,我们的跨平台 C++ 客户端库并没有走到第三阶段,并且最终停止了。
在第一阶段,LibSlack 工程师验证了业务逻辑和数据缓存可共享客户端库的概念,并深入开展了编译和交付跨平台库的相关工作。
然而,该项目在第二阶段并没有取得进展。库与我们的桌面客户端之间的技术和战略不兼容性变得很明显。事实证明,在我们的 iOS 和 Android 客户端中使用 LibSlack 库重新实现现有的逻辑和缓存会非常麻烦。不过,由于 Windows 手机的停产,Slack 需要维护的客户代码库减少了一份。
到最后我们并没有进行全面的迁移。我们将从 LibSlack 中学到的东西以各种方式应用到移动和桌面客户端开发工作中。代码工件并没有被长期采用,但这个项目让我们学会了应该如何构建客户端以及如何组织我们的工程团队。
曲线分析
需要注意的是,这个模型是描述性的,而不是说明性的。我们并不是想要强迫人们接受这个 S 型曲线,尽管我们想要这样。实际上,这是一个自然的过程。早期的探索阶段不可能像中期阶段那样迅速地进行,最后的阶段也不可能像中期阶段那样迅速地进行全面采用。这三个阶段并不是任何里程碑、过程、工具或 Slack 工程人员作用的结果。它们是技术变革的一部分,不管我们有没有注意到它们,它们都会存在。
现在,我们已经注意到了它们,我们可以利用它们,让我们的努力更加卓有成效。每个阶段的战术和战略是不一样的。
第一阶段:探索
第一阶段是无门槛进入。当工程师第一次开始调研他们感兴趣的技术时,不需要任何许可授予过程或仪式。在 Slack,这样的事情一天可能会发生几十次:有人发现了新技术,或者发明了新东西,然后就开始研究它们。他们可能是因为读过关于 Elixir、Cassandra、WebAssembly 或 TCR 的博文,或者下载了一些软件,编译打包,四处看看,浏览一些介绍性的材料,还可能尝试把它们应用到日常工作中。
大多数的探索工作都在这个阶段进行。在这个阶段,为了避免在不必要的事情上花费太多精力,我们要懂得放弃掉一些东西。不过确实还是有一些东西被用到了实际的工作流程和代码库中。有时,工程师可以直接应用某些解决方案,因为它们解决了一些局部问题。有时候会出现更加令人兴奋的结果:某些解决方案也解决了其他团队所面临的问题。这个阶段的工程师相信他们知道一些其他人不知道的东西:一些事情可以用更好的方式来做。一旦他们的工作开始影响到其他人,我们就进入了第二阶段。
第二阶段:扩张
进入第二阶段,工程师就有点可怜了!因为他们现在正试图改变其他工程师的行为,这将涉及沟通、说服,如果一切进行得顺利的话,他们还需要做大量的技术工作。对于大多数项目来说,第二阶段是最困难、最耗时、最令人沮丧的阶段。在技术周期中,这是“产品与市场匹配”阶段,很多进入该阶段的项目无法成功地走完这个阶段。
在 Slack,用户团队可以自由选择是否要依赖你的系统,很少有例外。如果你习惯了“基础设施驱动”的公司,那么我们的情况可能会让你大吃一惊。在其他公司,领导层会在第二阶段产品市场适应得出结论之前就选出了赢家和输家。他们之所以这么做,是为了提供清晰的信息(比如未来会怎样、我们应该采用哪个系统)和降低成本,因为在这一阶段需要支持更多的做事方式。
虽然这些都是合理的目标,但 Slack 并没有选择这种方式。我们优先考虑的是适应潮流的能力,而不是适应的速度。因此,我们(有意地)将促进其他团队采用新技术的主要负担放在了小部分人身上。虽然这可能会让这些人感到沮丧,但我们知道没有其他更好的办法。为了清除这一障碍,我们不得不选择更加有效的方法。如果新技术真的像我们所希望的那么美妙,那么它们应该能够帮助依赖它们的团队顺利完成工作。反过来,这种结果会促使他们采用和推广这些新技术。
第二阶段的一些工作更像是产品工作,而不是工程工作。你需要研究用户,以便确定哪些问题是重要的。你需要按照用户所期望的方式将你的解决方案的价值与之前的实践联系起来。你需要想办法缩小当前实践与你正在做出的改变之间的差距,让用户更容易接受。
在第二阶段取得的成功最终会导致一些自发式的采用,用户可以自由地选择是否采用新技术。当新系统成为事实上的标准,第二阶段就接近尾声了。偶然性地遇到这种采用情况是很不寻常的,因为这真的很难,而且并不是每个工程师都具备相关的技能。
第三阶段:迁移
自发式的采用最终会逐步减少,最后剩下一些顽固的人,他们似乎很抵制新的做事方式。一些后台运行的系统尤其没有动力去做出改变,因为它们的开发度不够活跃。在某些情况下,我们到了后期才发现一些旧系统在某些方面按照旧有方式运行会更好。最后,总是会有一些顽固的用户坚持使用老方法。
虽然我们一直在讨论“技术采用曲线”,但实际上在第三阶段会出现一个分岔口。即使非常成功的项目也不可能彻底采用全新的技术。例如,在 Slack,我们已经广泛地采用 gRPC 作为内部的 API 技术。但是,我们不太可能构建一个全新的基于 gRPC 的 memcached。memcached 的自定义协议很不错,并得到了客户端的良好支持。这种例外并不意味着采用 gRPC 是失败的。
对于其他一些情况,采用多种技术的成本(工程师的认知负担、运行旧系统的负担)太高,所以我们有必要彻底采用新技术。对于这样的项目,我们需要制定一个应对顽固派的计划,不同的情况需要采用不同的策略。长时间没有发生改动的系统可能需要使用代理,并逐步对其进行迁移。如果顽固派的存在是因为新系统缺乏必要的功能,那就需要增强新系统,或者对新系统进行封装,模拟旧系统的功能。
对于对旧系统产生了情感依恋的情况,进行面对面的沟通通常比高风险的公开辩论有效得多。请温柔些,如果你的新系统足够成功,总有一天它也会成为旧系统。
技术人的期望
作为 Slack 的工程师和工程负责人,我们对彼此的期望是什么呢?
首先,我们要去做一些探索工作。外面的世界很大,我们偶尔也要抬头看看外面发生了什么。但没有人可以做到探索一切,也没有人可以做到一直在探索新事物。我们有对外部的承诺和内部的路线图,这些事情仍然具有高优先级。不过,我们还是要分出一些能量用于探索新事物。
在成为其他团队技术产品的用户时,我们要讲道理。提供底层技术支持的团队需要将他们的系统向前推进,有时候,这会给上层的用户带来成本。如果这种成本是不合理的,或者当它朝着与你的团队需求相反的方向发展时,你需要以他们能够理解的方式与他们沟通。
有时候,我们需要避免依赖无法满足我们需求的底层技术。在设定团队技术发展方向时需要注意这一点。不过,这并意味着一定是他们做错了什么,我们需要用成熟和专业的方式来应对这种依赖分离。
当我们试图推动他人做出改变时,我们会对试图理解和使用新系统的团队抱持以用户为中心的态度。他们的快乐是你成功的唯一晴雨表,包括沟通、需求收集、反馈、迭代、有目的性的培训和技能分享。
如果有疑问,请记住:你要对技术采用的成功与否负责,而从长远来看,这是由你的产品的用户来决定的。
英文原文
How Big Technical Changes Happen at Slack
评论