写点什么

Netflix 工作 10 年,我收获的一些关键经验

作者:Phillipa Avery

  • 2022-08-02
  • 本文字数:5061 字

    阅读完需:约 17 分钟

Netflix工作10年,我收获的一些关键经验

开发者体验在很大程度上取决于开发人员所在的公司——与公司的价值观、文化和业务驱动力有关。随着公司的发展,公司的价值也会发生变化,相应地,开发人员创建、部署和维护代码的方式也会发生变化。本文讨论了为什么以及何时会发生对开发人员需求的变更、如何赶在变更之前做出改变,以及在发生变更时如何适应它们。在本文中,我将分享自己和同事多年来在 Netflix 收获的一些关键经验。

公司成长带来的影响

 

速度和稳定性之间的平衡会随着公司的成长而发生变化。当一家公司在开始它的旅程时,他们会快速迭代并尝试不同的方法。他们通常需要快速构建出一些东西并对其进行测试,然后放弃它们或让它们继续演进。这种“原型”阶段通常对稳定性要求不高,因为失败的影响范围有限。

 

然而,随着产品用户基数的增长,对稳定性和成本效益的期望将变得更高。这通常会导致对产品变更采取更谨慎(和更缓慢)的做法,稳定性将影响保持高速增长的能力。这可以通过建立稳定的 CI/CD 系统来实现,这个系统有助于达成一定程度的信任,从而实现更快速的增长。当开发人员相信他们的变更不会破坏生产系统,他们就能更专注于提升创新速度,而不会在手动验证变更上花费过多的时间。

 

随着业务的增长,这种信任在发布过程中就变得至关重要,良好的测试实践(如测试金字塔)将为产品取得成功铺平道路。尽管如此,CI/CD 过程仍然会影响验证过程的速度,并且随着整个产品系统的复杂性的增长,这种影响也将随之增加。

 

多年来,Netflix 在我们的标准 CI/CD 流程中引入了许多验证步骤,包括库版本依赖锁定(隔离特定库的故障)、自动化集成和功能测试,以及金丝雀测试。根据服务的复杂性程度不同,每个阶段需要花费相应长短的时间。如果一个服务有很多依赖项,那么其中一个依赖项在构建过程中导致失败并需要对其进行调试的可能性就会更大。功能更复杂的服务将占用更多的测试资源,也将花费更长的运行时间,特别是在需要进行更多集成和功能测试的情况下。在对具有多种流量模式(例如不同的设备、请求类型、数据需求)的服务进行金丝雀测试时,需要更长的时间来消除噪音并确保覆盖所有的流量模式。

 

为了保持上述需求的灵活性,我们采用微服务架构来创建服务,因为微服务的依赖图更小、测试时间更短和噪音更少。此外,我们避免在没有简单的覆盖过程的情况下阻塞发布过程。如果一个依赖项版本导致构建失败,可以很容易回滚到或锁定前一个版本。测试失败可以根据所做的变更进行分析和修复、忽略(之后再进行重新评估)或修改。对于金丝雀测试失败,可以单独分析原因,开发人员可以根据需要选择继续发布。CI/CD 的速度与稳定性之间的平衡最终由服务维护者根据他们自己的舒适度和业务影响来决定。

集中式管理与个体选择

 

在某些时候,公司可能需要做出决定,是否让开发人员独立选择和维护符合他们业务需求的技术,或者推荐(或强制)由公司集中提供支持的技术。在我看来,集中式工具和本地工具之间的区别是集中式工具让公司能够在整个产品生态系统层面实现一致性。这种一致性可以是一致的集成(安全性、洞察力、弹性、CI/CD 等)或最佳实践(架构、模式、依赖管理等)。从整体业务角度来看,这种集中式一致性可能非常强大,但可能对特定的应用场景不利。如果你定义了一个单一的一致性解决方案,那么几乎总是会有一些应用场景需要不同的方法才能在其业务驱动下获得成功。

 

例如,我们指定 Java 和 SpringBoot 作为服务技术栈基础。然而,在很多情况下,数据工程需要使用 Python 或 Scala 来满足他们的业务需求。我们使用 Gradle 作为构建工具,它对于我们选择的技术栈来说非常有效,但对于使用 Scala 的开发人员来说,使用 SBT 可能更适合。因此,我们需要评估是否需要为个别的应用场景增强 Gradle,或者允许(和支持)Scala 开发人员使用 SBT。

 

在集中式与本地业务需求的决策权重方面做出正确的平衡,并能够对其评估权衡,这是一个持续演化的过程。要了解一个应用场景在什么情况下应该采用集中式技术栈,这需要通过查看数据来进行评估——用户的数量、工作流对业务的影响、集中式技术栈需要多少人提供支持?所有这些因素都需要考虑到,如果有足够的优先级和增长空间,那么就应该转向集中式技术栈。

 

随着 Netflix 的文化开始推崇自由和责任,我们经常会看到开发人员决定在他们的应用场景中选择自己的解决方案,并对他们的选择责任。对于业务影响较小的小型应用场景,这是一种很好的选择。但如果存在影响范围不断扩大的可能性(越来越多的人开始使用它,或对业务的影响较高),那么这个选择从长期来看对业务是有害的——如果只有一个人提供技术支持,或者那个人转到其他项目,当没有其他人能够提供技术支持时就会出现技术债务,因此这个选择会成为快速发展能力的瓶颈。

 

因为并非所有的应用场景都能从集中式技术栈中受益,所以我们尝试采取分层的方式,我们提供了解耦的组件,这些组件可被用在具有高度集中式需求(例如安全性)的技术栈中。它们可以被单独使用(和管理),但随着你对整个生态系统依赖得越来越多,它们的集成和集中管理程度也会越来越高——我们称之为引导路线(Paved Path)。普通开发人员更容易接受集中式需求管理,而那些具有独特性质的业务可以选择自我管理和选择自己的道路——做出决定并承担相应的职责,例如,在出现问题时需要投入额外的时间,在未来可能需要迁移到集中式技术栈(如果受支持)的成本,如果这项技术被证明成本过高,将它从生态系统中移除有多容易。

 

要走上这条引导路线,通常需要将服务迁移到新技术。在某些情况下,将遗留服务迁移到新技术所带来的破坏和成本比只在出现问题时才在服务上花费时间来得更低。我们在实践当中体会到了这一点,例如,对于最近曝出的 Log4Shell 漏洞,我们需要(反复地)升级所有系统的 log4j 版本。对于已经走上引导路线的服务,开发人员无需操心,所有东西都在几个小时内完成升级。对于那些部分处于引导路线上的服务,需要少量的互动,并在一天内完成升级。对于那些还未走上引导路线的服务,开发者需要花费好几天的时间进行密集的调试,经历了多个发布周期才能完成升级。不过,在宏大的计划中,这仍然比将预先迁移到引导路线更具成本效益,且对业务的影响更小。

单代码库与多代码库策略

 

对于公司如何在单代码库与多代码库策略之间做出决定,目前还没有明确的答案,因为随着产品规模的扩大,这两种方法都存在重大缺陷。我能说的是它们在产品规模达到某个百分比时对发布速度产生的影响。如果采用了单代码库,要针对产品的子集(根据设计)发布版本是比较困难的。例如,如果你想发布代码变更或新版本(例如新的 JDK 版本),应用程序所有者可能很难在其他人之前发布变更。此外,单代码库发布变更的速度可能会慢得多,因为它必须通过所有产品的验证才能发布。

 

相反,Netflix 的多代码库方案提供了一种高度通用和快速的发布方法——每当发布了一个新的库版本,就通过自动依赖项 CI/CD 流程来更新依赖了这些库的应用程序。各个应用程序的所有者可以定义他们希望使用的代码变更版本(不管是好的还是坏的),并且在发布之后就可以立即使用。这种方法有几个关键的缺点:依赖版本管理非常复杂,版本问题调试的负担在于使用了这些依赖项的应用程序这边(如果你想更深入地了解 Netflix 是如何解决这种复杂性的,可以参考“使用Nebula进行大规模依赖管理”)。如果一个服务发布了一个新库,虽然它对 99%的应用程序来说是完全可行的,但通常会有一小部分应用程序存在一些传递性依赖问题,问题,必须加以识别和解决。

 

从长远来看,我们正在朝着一种混合方法的方向发展,我们将支持多代码库但单点发布的方法——个体代码库所有者可以发布新版本,但必须通过一个集中式的测试管道,为他们的消费者应用程序构建和运行库。如果管道失败,库所有者就有责任决定下一步采取什么步骤来解决问题。

技术栈的融合

 

关于如何将整个公司迁移到一致的技术栈,你可能会听到“胡萝卜还是大棒”的比喻——你是提供吸引人的新特性和功能(胡萝卜)让人们自己选择,还是强制开发人员使用引导路线上的产品(大棒)?在 Netflix,我们试图倾向于胡萝卜的方法,并为少数业务需求保留大棒。

 

理想情况下,人们会采用胡萝卜的方法。集中式方法可能更适用于一些特定的场景,但从整体业务的角度来看,它具有很高的强制性。在这些情况下,个别开发人员不会有什么好处,甚至会给他们现有的开发工作流增加额外的障碍和复杂性。对于这些情况,我们强调从公司利益出发,并提供明确的理由说明其重要性。我们尽量减少任何额外的负担,并尽可能展示一致性方法的好处。

 

在极少数情况下,我们会通过自上而下的方式提供一致性的技术栈,此时对于个体团队来说,迁移到新技术的优先级高于其他优先级。这通常是出于安全方面的考虑(如前面提到的 Log4Shell 漏洞案例),或者当一致性技术栈的整体业务利益超过单个团队的需求时——例如,在迁移的后半段,对剩余部分的支持成本变得过于昂贵且难以维护。

自己构建与外部购买

 

构建与购买指的是完全内部构建与使用外部产品。在 Netflix,我们在可能的情况下倾向于开源(OS),我们既生产也消费大量的开源产品。

 

如果可能的话,我们倾向于“购买”,并且偏爱开源产品。如果我们能找到一个与需求高度一致的开源项目,并且这个项目有蓬勃发展的社区,那么它极有可能成为我们的候选方案。但是,如果找不到理想的开源项目,或者与现有项目在功能上有显著差异,我们将考虑在内部自己构建。在功能需求较小的情况下,我们通常会选择完全在内部构建和维护。如果项目比较大或者对外部有很大的影响,我们会考虑将其作为一个开源项目发布。

 

如果你选择开源,不管你是选择发布自己的开源项目还是使用外部项目——这两种选择都需要开发成本。发布开源项目是需要成本的,因为需要围绕产品建立社区——代码和功能评审、举行会议、与内部使用保持一致。受欢迎的开源项目通常需要至少一名开发人员全职从事开源管理工作。如果选择使用外部产品,就需要保持与社区的工作关系——为产品做出贡献、影响未来的发展方向,并与内部使用保持一致。如果外部产品的发展方向因公司的使用而发生重大变化或开源项目被解散,这可能会成为一个风险点。

开发者体验的演化

 

随着公司规模的增长,一致性开始变得越来越重要。在规模较小的发展阶段,开发人员可能需要跨多个技术栈和领域——他们管理着整个技术栈。随着技术栈的增长,将工作重点放在特定部分的需求变得越来越明显,于是一个技术栈有了更多的开发人员。随着越来越多的人参与到这个工作流中,技术栈的特定部分变得越来越专业,他们就有了更多的机会来优化他们不需要关心的东西——更集中化的基础设施、抽象和工具。从集中化的角度接管这些工作可以将他们解放出来,让他们专注于特定的业务需求,而为这些集中化组件提供支持的小团队可以为大量特定于业务的开发人员提供服务。

 

此外,随着公司的增长,我们需要面对技术和需求会不断变化的事实,在过去可能失败的东西到了现在可能是可行的解决方案。我们需要树立一种接受和包容失败的态度——快速失败,然后再尝试。例如,我们长期以来一直通过A/B测试系统来测试面向Netflix用户群的新功能,并经常会取消那些被认为对用户增长没有好处的功能。如果产品有了改进,或者用户需求发生了变化,我们也会回来重新试用这些功能。

 

这方面的另一个内部例子是我们的 Publisher Feedback 功能,它用于在候选库发布到我们的多代码库生态系统之前对其进行验证。对于每一个候选发布版本,我们都会用指定的验收测试阈值测试依赖项的下游消费者,并在发生故障时向库发布者提供反馈,或者将这个版本自动作为库构建的一部分。不幸的是,因为基于常规 CI 工作流提供带外构建环境比较困难,所以我们也很难提供比编译时更多关于依赖关系的反馈,并且当我们意识到我们不打算使用与我们最初计划相同的基础设施进行声明式 CI 时,我们不得不重新进行评估。我们通过 Rocket CI 构建了一些基于 PR(拉取请求)的功能,在现有的 Jenkins 基础设施上提供 API、抽象和功能,同时避免与 Jenkins 构建环境产生耦合。

 

我对在快速增长的公司工作的工程经理们的建议是:不要害怕尝试新事物,即使以前失败过。技术和需求在不断变化,在过去可能失败的东西到了现在可能是可行的解决方案。


作者简介:

Phillipa Avery 是 Netflix Java 平台团队的经理,该团队支持 Netflix 所有后端服务从构建到部署的开发者体验。她来自澳大利亚,在过去的 10 年(大约 10 年)一直住在加州,喜欢花时间阅读、散步和举重训练——有时还尝试同时做这三件事。

 

原文链接

Scaling and Growing Developer Experience at Netflix

 

2022-08-02 08:005729

评论

发布
暂无评论
发现更多内容
Netflix工作10年,我收获的一些关键经验_文化 & 方法_InfoQ精选文章