世界上最大的Web服务商Dropbox是如何从Nginx迁移到Envoy的?


在此博客文章中,我们将讨论基于Nginx的旧的交通基础设施,其痛点以及通过迁移到Envoy所获得的好处。我们将在许多软件工程和运营方面将Nginx与Envoy进行比较。我们还将简要介绍迁移过程,迁移过程的当前状态以及在此过程中遇到的一些问题。
当将大部分Dropbox流量移至Envoy时,我们必须实现无缝地迁移一个系统,该系统已经处理了数千万个打开的连接,每秒数百万个请求和数以兆计的带宽。这实际上使我们成为了世界上最大的Envoy用户之一。 

基于Nginx的传统流量基础架构
我们的Nginx配置大部分是静态的,并结合使用Python2,Jinja2和YAML进行渲染。对其进行任何更改都需要完全重新部署。所有动态部分,例如上游管理和统计信息导出器,都用Lua编写。任何足够复杂的逻辑都移到了用Go语言编写的下一个代理层
Nginx为我们服务了近十年。但这并不能适应我们当前的开发最佳实践:

  • 我们的内部和(私有)外部API逐渐从REST迁移到gRPC,这需要来自代理的各种转码功能。
  • 协议缓冲区实际上已成为服务定义和配置的标准。
  • 所有软件,无论使用哪种语言,都使用Bazel构建和测试。
  • 我们的工程师大量参与开源社区中的基本基础结构项目。

另外,在操作上,Nginx的维护成本非常高:
  • 配置生成逻辑太灵活,无法在YAML,Jinja2和Python之间进行分配。
  • 监视是Lua,日志解析和基于系统的监视的组合。
  • 越来越依赖第三方模块会影响稳定性,性能以及后续升级的成本。
  • Nginx部署和流程管理与其余服务完全不同。它完全依赖于其他系统的配置:syslog,logrotate等,而不是与基本系统完全分离。

综上所述,这是10年来的首次,我们开始寻找Nginx的潜在替代产品。

为什么不使用Bandaid?
在内部,我们在很大程度上依赖于基于Golang的代理Bandaid。它可以与Dropbox基础架构很好地集成,因为它可以访问内部Golang库的广阔生态系统:监视,服务发现,速率限制等。我们考虑过从Nginx迁移到Bandaid,但是有一些问题使我们无法这样做:

  • Golang比C / C ++占用更多资源。资源的低利用率对于我们在Edge上尤其重要,因为我们无法轻松地“自动扩展”那里的部署。
    • CPU开销主要来自GC,HTTP解析器和TLS,后者的优化程度低于Nginx / Envoy使用的BoringSSL。
    • “按请求分类”模型和GC开销大大增加了像我们这样的高连接服务中的内存需求。
  • Go的TLS堆栈不支持FIPS。
  • Bandaid在Dropbox之外没有社区,这意味着我们只能依靠自己进行功能开发。

考虑到所有这些,我们决定开始将流量基础结构迁移到Envoy。

基于Envoy的新基础设施
让我们一个个地研究主要的开发和运营维度,以了解为什么我们认为Envoy对我们来说是一个更好的选择,以及从Nginx迁移到Envoy所获得的收益。

1.性能
Nginx的架构是事件驱动和多进程的。它支持SO_REUSEPORTEPOLLEXCLUSIVE和工作者到CPU的固定。尽管它是基于事件循环的,但它不是完全非阻塞的。这意味着某些操作(例如打开文件或访问/错误日志记录)可能会导致事件循环停止(即使启用了aio,aio_write和线程池。)这会导致尾部延迟增加,从而可能导致数秒的延迟在旋转磁盘驱动器上。
Envoy具有类似的事件驱动的体系结构,只是它使用线程而不是进程。它还具有SO_REUSEPORT支持(带有BPF过滤器支持),并且依赖libevent进行事件循环实现(换句话说,没有像ePOLLEXCLUSIVE这样的epoll(2)功能。)Envoy在事件循环中没有任何阻塞的IO操作。甚至日志记录也以非阻塞方式实现,因此不会引起停顿。
从理论上讲,Nginx和Envoy应该具有相似的性能特征。我们的测试结果表明,在大多数测试工作负载下,Nginx和Envoy的性能相似:每秒高请求(RPS),高带宽以及混合的低延迟/高带宽gRPC代理。
,结果有几个显着的差异:

  • Nginx显示出更高的长尾延迟。这主要是由于在I / O繁重的情况下事件循环停滞所致,尤其是与SO_REUSEPORT一起使用时,因为在这种情况下,可以代表当前阻塞的worker接受连接
  • 不带统计信息收集的Nginx性能只是Envoy的一部分,但我们的Lua统计信息收集在高RPS测试中将Nginx减慢了3倍。考虑到我们对lua_shared_dict的依赖,这是可以预期的。

我们确实了解统计数据收集的效率低下。我们考虑过在用户空间中实现类似于FreeBSD的counter(9)的功能:CPU固定,按工时无锁的计数器以及一个提取例程,该例程循环遍历所有工作人员,汇总各自的统计信息。但是我们放弃了这个想法,因为如果我们想检测Nginx内部(例如所有错误情况),那就意味着要支持一个巨大的补丁程序,这将使后续的升级真正成为现实。
由于Envoy不会受到这两个问题的困扰,因此在迁移到Envoy之后,我们可以释放多达60%的服务器(以前由Nginx独占)。

2.可观察性
可观察性是任何产品最基本的操作需求,但对于代理这样的基础架构而言尤其如此。在迁移期间,这一点尤为重要,以便监视系统可以检测到任何问题,而沮丧的用户也可以报告任何问题。
非商业Nginx带有一个“ 存根状态 ”模块,具有7个统计信息。
这绝对是不够的,因此我们添加了一个简单的log_by_lua处理程序,该处理程序根据Lua中可用的标头和变量添加每个请求的统计信息:状态代码,大小,缓存命中率等。
除了每个请求的Lua统计信息外,我们还有一个非常脆弱的error.log解析器,负责上游,http,Lua和TLS错误分类。
最重要的是,我们有一个单独的导出器来收集Nginx内部状态:自上次重新加载以来的时间,工作人员数量,RSS / VMS大小,TLS证书使用期限等。
而一个典型的Envoy设置为我们提供了数千种不同的指标(以prometheus格式),描述了代理流量和服务器的内部状态。
这包括具有不同汇总的大量统计信息:

  • 每个群集/每个上游/每个虚拟主机的HTTP统计信息,包括连接池信息和各种时序直方图。
  • 每个侦听器的TCP / HTTP / TLS下游连接统计信息。
  • 从基本版本信息和正常运行时间到内存分配器状态和不赞成使用的功能使用情况计数器,各种内部/运行时状态。

除统计数据外,Envoy还支持可插入的跟踪提供程序。这不仅对拥有多个负载平衡层的Traffic团队有用,对于希望从边缘到应用服务器端到端跟踪请求延迟的应用程序开发人员也很有用。
最后但并非最不重要的一点是,Envoy能够通过gRPC流式传输访问日志。这消除了我们的流量团队支持syslog到配置单元桥接的负担。此外,在Dropbox生产中启动通用gRPC服务比添加自定义TCP / UDP侦听器更容易(更安全!)。
像其他所有操作一样,Envoy中访问日志的配置通过gRPC管理服务即访问日志服务(ALS)进行。管理服务是将Envoy数据平面与生产中的各种服务集成的标准方法。这将我们带入下一个主题。

3.集成
Nginx的集成方法最好描述为“ Unix-ish”。配置是非常静态的。它严重依赖文件(例如配置文件本身,TLS证书和票证,允许列表/阻止列表等)和众所周知的行业协议(通过HTTP 记录到syslog和auth子请求)。对于小型安装而言,这样的简单性和向后兼容性是一件好事,因为可以使用几个Shell脚本轻松实现Nginx的自动化。但是随着系统规模的扩大,可测试性和标准化变得越来越重要。
Envoy对于如何将交通数据平面与其控制平面以及因此与其他基础架构集成在一起的观点更加坚定。通过提供通常称为xDS的稳定API,它鼓励使用protobufsgRPC。Envoy通过查询一个或多个这些xDS服务来发现其动态资源。
这对于Dropbox尤其有用,Dropbox的所有服务已在内部通过基于gRPC的API进行交互。我们已经实现了自己的xDS控制平面版本,该版本将Envoy与我们的配置管理,服务发现,秘密管理和路由信息集成在一起。
我们本土的Envoy控制平面实现了越来越多的xDS API。它被部署为生产中的常规gRPC服务,并充当我们基础结构构建块的适配器。它通过一组通用的Golang库来执行此操作,以与内部服务进行对话,并通过稳定的xDS API向Envoy公开它们。整个过程不涉及任何文件系统调用,信号,cron,logrotate,syslog,日志解析器等。

4.配置
Nginx具有简单易读的配置的不可否认的优势。但是,随着配置变得越来越复杂,并且开始生成代码,这种胜利就失去了。
有两个基本问题:

  • 没有关于配置格式的声明性描述。如果我们想以编程方式生成和验证配置,则需要自己进行发明。
  • 从C代码的角度来看,语法上有效的配置仍然可能无效。例如,某些与缓冲区相关的变量具有值限制,对齐限制以及与其他变量的相互依赖性。为了从语义上验证配置,我们需要通过nginx -t运行它。

另一方面,Envoy具有用于配置的统一数据模型:其所有配置都在协议缓冲区中定义。这不仅解决了数据建模问题,而且还将键入信息添加到配置值中。鉴于protobuf是Dropbox生产中的头等公民,并且是描述/配置服务的通用方式,因此集成变得非常容易。 
我们针对Envoy的新配置生成器基于protobufs和Python3。所有数据建模均在原始文件中完成,而所有逻辑均在Python中进行。

5.可扩展性
将Nginx扩展到标准配置所不能提供的范围之外,通常需要编写C模块。Nginx的开发指南对可用的构建块进行了扎实的介绍。就是说,这种方法是相对重量级的。实际上,需要相当资深的软件工程师来安全地编写Nginx模块。
就模块开发人员可用的基础架构而言,他们可以期待基本容器,例如哈希表/队列/ rb-tree,(非RAII)内存管理以及请求处理所有阶段的挂钩。还有一些外部库,例如pcre,zlib,openssl,当然还有libc。
为了提供更轻量级的功能扩展,Nginx提供了PerlJavascript接口。可悲的是,它们的能力都相当有限,大部分都局限于请求处理的内容阶段。
由社区采用最常用的扩展方法是基于第三方升UA- nginx的-模及各种OpenResty库。这种方法几乎可以挂接到请求处理的任何阶段。我们使用log_by_lua收集统计信息,并使用balancer_by_lua进行动态后端重新配置。
Envoy的主要扩展机制是通过C ++插件。该过程的记录不如Nginx的那样,但它更简单。部分原因是:

  • 干净且注释良好的界面。C ++类充当自然的扩展和文档点。例如,签出HTTP过滤器接口
  • C ++ 14语言和标准库。从诸如模板和lambda函数之类的基本语言功能,到类型安全的容器和算法。通常,编写现代C ++ 14与使用Golang并没有多大区别,或者甚至连说Python也没有什么不同。
  • C ++ 14及其标准库以外的功能。abseil库提供,这些包括来自较新C ++标准的直接替换,具有内置静态死锁检测和调试支持的互斥锁,更多/更有效的容器等等

有关细节,这是HTTP过滤器模块规范示例
通过简单地实现Envoy stats接口,我们能够仅用200行代码将Envoy与Vortex2 (我们的监视框架)集成在一起。
Envoy 通过moonjit 获得了Lua支持,moonjit是具有改进的Lua 5.2支持的LuaJIT分支。与Nginx的第三方Lua集成相比,它的功能和挂钩要少得多。由于开发,测试和故障排除解释代码的额外复杂性的成本,这使Envoy的Lua吸引力大大降低。专门从事Lua开发的公司可能会不同意,但是在我们的案例中,我们决定避免使用它,而仅将C ++用于Envoy的可​​扩展性。
Envoy与其他Web服务器的不同之处在于它对WebAssembly(WASM)的新兴支持-一种快速,可移植且安全的扩展机制。WASM不能直接使用,而可以用作任何通用编程语言的编译目标。Envoy实现了WebAssembly for Proxies规范(还包括参考RustC ++ SDK),该规范描述了WASM代码和通用L4 / L7代理之间的边界。代理服务器代码和扩展代码之间的这种分隔允许安全的沙箱操作,而WASM低级紧凑二进制格式允许接近本机的效率。最重要的是,在Envoy中,代理wasm扩展与xDS集成在一起。这样可以进行动态更新,甚至可以进行潜在的A / B测试。
借助WASM,服务提供商可以安全有效地在其边缘运行客户的代码。客户从可移植性中受益:他们的扩展可以在实现代理代理ABI的任何云上运行。另外,它允许您的用户使用任何语言,只要它可以编译为WebAssembly。这使他们能够安全有效地使用更广泛的非C ++库集。
当前,我们不在Dropbox上使用WebAssembly。但是,当Go for SDK for proxy-wasm可用时,这可能会改变。

6. 构建和测试
默认情况下,Nginx是使用基于外壳的自定义配置系统和基于make的构建系统构建的。这是简单而优雅的方法,但是将其集成到B Azel构建的monorepo中需要花费大量的精力,才能获得增量,分布式,密封和可复制构建的所有优点。
在测试方面,Nginx 在单独的存储库中有一组Perl驱动的集成测试,没有任何单元测试。
鉴于我们对Lua的大量使用以及缺乏内置的单元测试框架,我们求助于使用模拟配置和基于Python的简单测试驱动程序进行测试.
最重要的是,我们通过预处理所有生成的配置(例如,用127/8替换所有IP地址,切换到自签名TLS证书等)并在结果上运行nginx -c来验证所有语法的语法正确性。
在Envoy方面,主要的构建系统已经是Bazel。因此,将其与我们的monorepo集成起来很简单:Bazel轻松地允许添加外部依赖项。Bazel是我们开发人员经历过的最好的事情之一。它的学习曲线非常陡峭,并且是一笔大笔的前期投资,但在投资上却有很高的回报:增量构建远程缓存分布式构建/测试等。
使用Envoy,我们可以灵活地使用带有一组预先编写的模拟的单元测试(基于gtest / gmock)或Envoy的集成测试框架,或同时使用两者。对于每一个小小的变化,都不再需要依靠缓慢的端到端集成测试。
亚秒级的测试往返对生产率产生复合影响。它使我们能够付出更多的努力来增加测试范围。能够在单元测试和集成测试之间进行选择,使我们能够平衡Envoy测试的覆盖范围,速度和成本。

7.安全
Nginx的代码面非常小,具有最小的外部依赖性。通常只对生成的二进制文件看到3个外部依赖项:zlib(或其更快的变体之一),TLS库和PCRE。Nginx具有所有协议解析器,事件库的自定义实现,甚至还可以重新实现某些libc函数。
在某些时候,Nginx被认为非常安全,以至于它被用作OpenBSD中的默认Web服务器。后来,两个开发社区陷入了混乱,导致创建了   httpd。您可以在BSDCon的“ 介绍OpenBSD 的新httpd ”中了解此举背后的动机。
这种极简主义在实践中得到了报应。Nginx 在过去11年中仅报告了30个漏洞和暴露
另一方面,Envoy拥有更多的代码,尤其是当您考虑到C ++代码比用于Nginx的基本C语言要密集得多时。它还包含来自外部依赖项的数百万行代码。从事件通知到协议解析器的所有内容均已卸载到第三方库。这会增加攻击面,并使生成的二进制文件膨胀。
为了解决这个问题,Envoy高度依赖现代安全实践。它使用AddressSanitizerThreadSanitizerMemorySanitizer。它的开发人员甚至超越了这个范围,采用了模糊测试
为了应对增加的漏洞风险,我们使用了来自上游OS供应商UbuntuDebian的最佳二进制强化安全实践。我们为所有边缘曝光的二进制文件定义了特殊的强化构建配置文件。它包括ASLR,堆栈保护器和符号表强化。
我们还希望在可能的情况下加强对第三方的依赖。我们在FIPS模式下使用BoringSSL,该模式包括启动自检和二进制文件的完整性检查。我们还考虑在某些边缘Canary服务器上运行支持ASAN的二进制文件。

8.特点
Nginx最初是一个Web服务器,专门用于以最少的资源消耗提供静态文件。它的功能是最重要的:静态服务,缓存(包括防雷群保护)和范围缓存。
但是,在代理方面,Nginx缺少现代基础架构所需的功能。后端没有HTTP / 2。gRPC代理可用,但没有连接多路复用。不支持gRPC转码。最重要的是,Nginx的“开放核”模型限制了可以纳入代理的开源版本的功能。结果,某些重要功能(如统计信息)在“社区”版本中不可用。
相比之下,Envoy已经发展成为一个入口/出口代理,经常用于重载gRPC的环境。它的Web服务功能是基本的:没有文件服务,仍在进行中的缓存brotli或预压缩。对于这些用例,我们仍然有一个小的后备Nginx设置,Envoy将其用作上游群集。
Envoy还对许多与gRPC相关的功能提供了本机支持:

  • gRPC代理。这是一项基本功能,使我们能够为应用程序(例如Dropbox桌面客户端)端对端使用gRPC。
  • HTTP / 2到后端。此功能使我们可以大大减少流量层之间的TCP连接数,从而减少内存消耗和保持活动的流量。
  • gRPC→HTTP桥(+ 反向。)这些使我们能够使用现代gRPC堆栈公开旧版HTTP / 1应用程序。
  • gRPC-WEB。此功能使我们即使在中间盒(防火墙,IDS等)尚不支持HTTP / 2的环境中也可以端到端使用gRPC。
  • gRPC JSON转码器。这使我们能够将所有入站流量(包括Dropbox公共API)从REST转换为gRPC。

此外,Envoy还可以用作出站代理。我们使用它来统一其他几个用例:
  • 出口代理:由于Envoy 添加了对HTTP CONNECT方法的支持,因此可以用作Squid代理的替代产品。我们已经开始用Envoy替换出站Squid安装。这不仅极大地提高了可视性,而且还通过使用通用数据平面和可观察性统一堆栈来减少操作麻烦(不再为统计信息解析日志)。
  • 第三方软件服务发现:我们依靠软件中的Courier gRPC库,而不是使用Envoy作为服务网格。但是,在需要一次性花费很少的精力将开源服务与服务发现连接起来的情况下,我们确实使用了Envoy。例如,Envoy在我们的分析堆栈中用作服务发现辅助工具。Hadoop可以动态发现其名称和日记节点。Superset可以发现气流,预存和配置单元后端。Grafana可以发现其MySQL数据库。

迁移现状
我们已经将Nginx和Envoy并排运行了半年多,并通过DNS逐步将流量从一个切换到另一个。到目前为止,我们已经将各种各样的工作负载迁移到Envoy:

  • 入口高吞吐量服务。Dropbox桌面客户端的所有文件数据都通过Envoy通过端到端gRPC提供。通过切换到Envoy,由于从边缘进行更好的连接重用,我们还略微提高了用户的性能。
  • 入口高RPS服务。这是Dropbox桌面客户端的所有文件元数据。我们获得了端到端gRPC的相同好处,并且删除了连接池,这意味着我们不受每个连接一次请求的限制。
  • 通知和遥测服务。在这里,我们处理所有实时通知,因此这些服务器具有数百万个HTTP连接(每个活动客户端一个)。现在可以通过流gRPC而不是昂贵的长轮询方法来实现通知服务。
  • 高吞吐量/高RPS混合服务。API流量(元数据和数据本身)。这使我们可以开始考虑公共gRPC API。我们甚至可以直接在Edge上转换为对现有的基于REST的API进行代码转换。
  • 出口高吞吐量代理。在我们的案例中,是Dropbox与AWS的通信,主要是S3。这将使我们最终能够从生产网络中删除所有Squid代理,从而使我们只有一个L4 / L7数据平面。 

要迁移的最后一件事是www.dropbox.com本身。迁移之后,我们可以开始停用我们的边缘Nginx部署。一个时代将会过去。

遇到的问题
当然,迁移并非完美无缺。但这并没有导致任何明显的中断。迁移中最困难的部分是我们的API服务。Dropbox通过我们的公共API与很多不同的设备进行通信。除了我们的api用户所依赖的Nginx和Envoy行为之间的许多不一致之外,Envoy及其库中还存在许多错误。我们在社区的帮助下迅速解决了所有这些问题并将其上游。
这只是一些“异常的”/non-RFC行为的要点:

  • 合并URL中的斜杠。URL标准化和斜杠合并是Web代理的非常常见的功能。Nginx默认启用斜线归一化和斜线合并,但是Envoy不支持后者。我们向上游提交了补丁,以添加该功能,并允许用户使用 merge_slashes选项选择加入。
  • 虚拟主机名中的端口。Nginx允许接收两种形式的 Host标头: example.com或 example.com:port。我们有几个曾经依赖于此行为的API用户。首先,我们通过在配置中复制虚拟主机(有端口和无端口)来解决此问题,但后来在Envoy端添加了一个忽略匹配端口的选项: strip_matching_host_port
  • 传输编码区分大小写。出于某种未知原因,一个小型子集API客户端使用了 Transfer-Encoding:Chunked(请注意大写的“ C”)标头。这在技术上是有效的,因为RFC7230声明 Transfer-Encoding / TE标头不区分大小写。该修复程序很简单,已提交给上游特使。
  • 同时具有Content-Length和Transfer-Encoding的请求:c hunked。以前曾经与Nginx一起使用的请求,但因Envoy迁移而中断。RFC7230有点棘手,但是一般的想法是Web服务器应该错误地处理这些请求,因为它们很可能是“走私的”。另一方面,下一个句子表示代理应该只删除 Content-Length标头并转发请求。我们扩展了http-parse,以允许图书馆用户选择支持此类请求,并且目前正在努力将支持添加到Envoy本身。

还值得一提的是,我们遇到了一些常见的配置问题:
  • 断路配置错误。根据我们的经验,如果您将Envoy作为入站代理运行,尤其是在HTTP / 1&HTTP / 2混合环境中,则错误地设置断路器会在流量高峰或后端中断期间导致意外停机。如果您不使用Envoy作为网状代理,请考虑放松它们。值得一提的是,默认情况下,Envoy中的断路限制非常严格-请注意!
  • 缓冲。Nginx允许在磁盘上进行请求缓冲。这在您具有无法理解分块传输编码的传统HTTP / 1.0后端的环境中尤其有用。Nginx可以通过将它们缓存在磁盘上,将它们转换为具有Content-Length的请求。Envoy有一个Buffer过滤器,但是由于无法将数据存储在磁盘上,因此我们只能在内存中缓冲多少内存。