查看原文
其他

Netflix 在运营 Serverless 平台方面的心得

2017-09-06 云头条

作者:Vasanth Asokan、Ludovic Galibert和Sangeeta Narayanan

 

Netflix API基于一种每天处理数千个更改的动态脚本平台。该平台让我们的客户端开发人员可以在1000多种设备上打造一种定制的API体验,只需要执行响应HTTP请求的服务器端适配器代码。开发人员只对自己编写的适配器代码负责,他们不必为与服务器管理和运维有关的基础设施问题操心。在这些开发人员看来,该脚本平台提供的体验实际上类似serverless或函数即服务(FaaS)平台提供的那种体验。值得一提的是,两者相似的地方仅限于开发者体验(DevEx);运行时环境是一种自定义实现,并不是旨在支持通用的serverless用例(use case)。这几年来我们为一群广泛的开发人员开发和运营这个平台,获得了DevEx方面的几个经验。下面是我们最大的心得:


在serverless中,较小的部署单元和较高的抽象级别这个组合带来了引人入胜的优点,比如加快了速度、提高了可扩展性、降低了成本,更常见的是,能够专注于产品的功能特性。然而,运维方面的难题并没有消除;它们只不过换了新的形式,或者甚至被放大了。运维工具需要与时俱进,以满足这些新的要求。开发者体验方面的门槛总体上提高了。


本文是我们总结经验的这个系列文章的第一篇,旨在概述我们为下一代平台着力解决的各个方面。我们认为,这些方面同样适用于通用serverless解决方案。本文将介绍应用程序开发、交付和代码组合。后续文章会深入探讨诸多话题,比如部署、洞察力、性能、可扩展性及其他运维问题。


开发


本地开发起来有多轻松?


我们的脚本平台让开发人员可以编写含有应用程序逻辑的函数。开发人员将代码(脚本)上传到脚本平台,该脚本平台不但提供运行时环境,还处理诸如API路由和扩展之类的基础设施问题。脚本可通过开发人员定义的HTTP路由(又叫端点)来实现寻址,并在远程虚拟机上执行。


按照定义,远程运行时环境模型防止用户的脚本在本地执行,这给开发/测试迭代增添了诸多的不便。即使本地更改以某种方式无缝部署,往返时间(即使只有几十秒)对开发人员来说也是极为漫长的。



本地开发工作流程


为了缓解这个问题,我们有一个基于云的交互式解释器(REPL),用于交互式浏览和执行脚本。然而,我们发现脚本很少是简单的函数元件。随着时间的推移,它们往往变得更像微型服务,逻辑则分布在多个模块和源文件上――面对实际生产环境的脚本,REPL毫无可扩展性而言。它也无法满足诸多要求,比如支持用户青睐的集成开发环境(IDE),或允许通过断点(breakpoint)来进行调试。


我们还注意到反模式(anti-pattern)开始慢慢出现――开发人员喜欢详细的调试日志机制或其他的防御措施,就为了避免上传迭代。这还带来了风险,比如不小心泄露调试日志中的敏感数据。这些经历让我们格外重视一等公民(first-class)、低延迟的本地开发体验,支持实时重新加载,并支持为我们的下一代平台调试和模拟云执行环境。


包装和版本控制


部署工件是否可移植、易于版本控制和管理?


在我们目前的平台中,我们专注于部署工件(deployment artifact)的这三个方面


1. 提供“构建一次,可部署于任何地方”的模式

2. 实现简单的可寻址性

3. 为可追溯性和生命周期管理提供便利


可移植、不可变的工件必不可少,以确保代码在众多环境中运行起来一致,可以放心地转移到生产环境。由于我们的平台在JVM上运行,JAR文件显然是这方面的不二选择。


一旦构建完毕,工件需要在任何环境中都可寻址,因为工件经由持续交付管道,从低级环境一路进入到生产环境。我们发现,最简单的方案是使用名称和可选版本。只要给出名称和版本,任何环境或区域都可以对脚本实现寻址,并提供脚本以便执行。虽然这听起来很简单,但人类可读的命名模式让用户不必处理系统生成的晦涩难懂的资源标识符,这些资源标识符较难理解。


我们还将丰富的元数据附加到包含诸多元素的工件,比如运行时环境(如Java版本)、TTL、SCM提交及其他元素。这种元数据支持众多用例。举例说,提交指针能够对诸多版本和环境的源代码做到可追溯性,还能实现升级流程自动化。主动包括这些元数据有助于解决方案应用于serverless所独有的用例,比如资源的生命周期管理。


总的来说,我们的方法对用户来说非常管用,但是与每种技术一样,有时到头来使用方式却与初衷大不一样。比如说,由于我们让版本控制成为可选,结果看到了一种模式:团队将自定义版本控制方案做入到资源名称中,因而需要重新发明轮子(即做重复性工作),这实际上没有必要。我们的架构重建工作从几个方面来解决这个问题:


  • 由于我们在架构重建工作中深入了解语义版本控制(http://semver.org/)的概念,版本控制被提升为是一等公民的概念。

  • 使用Docker来包装有助于通过捆绑系统依赖项来保证不变性和可移植性。回到前面本地开发那个部分,它还让开发人员能够运行生产脚本。


测试和持续集成


开发速度加快带来了什么样的影响?


将代码更改部署到我们的平台上极其容易,我们当然看到开发人员在最大限度地利用这项功能。这反过来突显了预生产环境与生产环境之间的几个明显区别。


举例说,开发人员的频繁提交结合持续集成/持续交付(CI/CD)运行,这使得预生产环境中的部署速度加快了近10倍。每10个到20个测试部署中只有一个可能进入到生产环境。这些测试部署通常是短时间的,可是给开发人员造成了维护负担,因为它们留下了一些闲置未用的部署,需要清理以节省资源。


另一个区别是,预生产环境中的流量比生产环境中少得多。加上短时间的部署,这带来了不利影响:加剧了脚本的冷启动问题。因此,开发和测试运行常常面临执行延误不可预测这个难题,很难以一致的方式调整客户端超时。这导致了一种大家不希望看到的情形:开发人员依赖生产环境来做所有测试,这反过来使隔离目标落空,导致测试耗用生产环境的配额资源。


最后,远程执行模式还要求预生产环境中需要一组不同的功能(比如运行集成测试、生成代码覆盖率、分析执行概况等),而这些功能在生产环境中是有风险的或不可行的。


考虑到所有这些,一个重要的经验是,一开始就要有明确定义的、隔离的、易于使用的预生产环境。在我们的下一代平台中,我们旨在让预生产环境体验与生产环境体验不相上下,有自己的一套功能演进和创新。


代码模块化和组合


可以放心地迅速组合细粒度函数吗?


由于采用我们脚本平台的人越来越多,需要重用实现共享功能的代码的人也越来越多。通过复制粘贴来重用,扩展性很有限;如果需要更改,这给通用组件添加了额外的运维成本。我们需要一种解决办法,动态、轻松地组合松散耦合的组件,同时提供洞察力,以便深入了解代码重用情况。为此,我们将动态共享库和依赖项这两个概念视作一等公民,另外还非常注重运行时解析和连接直接依赖项及传递依赖项的功能。


这里的一个重要经验是,共享模块的生产者(producer)和消费者(consumer)在更新方面有着不同的要求。消费者想要牢牢控制何时获得更改,而提供者在迅速发布更新时想要做到高度灵活,并且与消费者脱离开来。


使用语义版本控制来支持依赖项的规格和解析,有助于支持这种用例的双方。提供者和消费者可以使用严格版本或语义版本范围,根据自己的喜好来协商合约。



在这种松散耦合的模式中,大规模下保持更改的速度需要能够深入了解两个方向的依赖项链。我们的平台让消费者和提供者能够跟踪他们依赖或交付的共享模块的更新情况。


这种洞察力以下面这些方式帮助我们的客户:


  • 共享模块提供者可以跟踪错误修复程序或功能更新的使用情况。



在这种松散耦合的模式中,大规模下保持更改的速度需要能够深入了解两个方向的依赖项链。我们的平台让消费者和提供者能够跟踪他们依赖或交付的共享模块的更新情况。


这种洞察力以下面这些方式帮助我们的客户:


  • 共享模块提供者可以跟踪错误修复程序或功能更新的使用情况。



跟踪依赖项


  • 它们能够支持更好的生命周期管理(下一篇文章会探讨)。只要消费者在使用共享代码,就需要维护代码。如果版本范围(主要及/或次要版本)不再使用,它就可以被弃用,系统资源释放出来、用于其他用途。


我们在下一篇文章中将探讨运维问题。敬请关注!


相关阅读:

中高端IT圈人群,欢迎加入!

赏金制:欢迎来爆料!长期有效!

AWS上的Serverless架构详谈

Serverless Computing 是公共云的下一站吗?

Netflix 的顶级开源项目,你知道几个?

Netflix发展背后的技术


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存