文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Dropbox是如何将接入层从Nginx迁移到Envoy的

2024-12-03 16:08

关注

在这篇文章里,我们将会介绍 Dropbox 之前曾经使用过的一套基于 Nginx 的流量基础设施,它的一些痛点,以及迁移到 Envoy 之后带来的一些好处。我们将会针对 Nginx 和 Envoy 就多个软件工程及运维方面进行比较。我们还将简单地介绍一下我们的迁移流程,迁移后的现状,以及在这个过程中遇到的一些问题。

在我们大部分的 Dropbox 流量转到 Envoy 后,我们还必须无缝地将一个已经处理了数千万个建立的连接,每秒数百万个请求,并拥有数个 TB 带宽的复杂系统迁移到上面。这实际上已经让我们成为了世界上最大的 Envoy 用户之一。

免责声明:尽管我们试图保持客观性,但是本文中有很多对比是针对 Dropbox 以及我们软件开发的方式进行的:我们的技术栈选型是 Bazel,gRPC 和 C++/Golang。

另外请注意,下文提到的 Nginx 指的是其开源版本,而不是具有附加功能的商业版本。

旧的一套基于 Nginx 的流量基础设施

我们的 Nginx 配置绝大部分都是静态的,然后通过结合 Python2、Jinja2 以及 YAML 等渲染。任意一处变动都需要一次完整的重新部署才能生效。所有动态的部分,比如 upstream 管理以及 stats exporter,这些是用 Lua 编写的。更复杂的逻辑放到了用 Go 编写的,下面一层的代理层。

Nginx 在我们的环境里良好运行了近十年。但是它已经无法再适应我们现在的开发优秀实践:

此外,在运维方面,Nginx 的维护成本非常高:

正是出于上述种种原因,10 年来我们首次开始寻找 Nginx 的潜在替代产品。

为什么不用 Bandaid?

正如我们上面提到的,在 Dropbox 内部,我们严重依赖一个 Golang 实现的代理(称为 Bandaid)。之所以它可以和 Dropbox 的整个基础设施很好地集成,原因是在于它可以访问到我们内部 Golang 库的广阔生态:监控、服务发现、限流等。我们考虑过从 Nginx 迁移到 Bandaid 的方案,但是这里面有一些问题阻碍了我们这样做:

综合上述原因,我们决定将所有流量基础设施迁移到 Envoy。

全新的基于 Envoy 的流量基础设施

让我们逐一看看开发和运维几个主要的维度,从中了解为什么我们认为 Envoy 对我们来说是一个更好的选择,以及我们从 Nginx 迁移到 Envoy 之后获得了哪些收益。

性能

Nginx 的架构是一个事件驱动和多进程的模式。它支持 SO_REUSEPORT,EPOLLEXCLUSIVE 以及 Worker 绑核。尽管它是基于事件循环的,但它并不是完全非阻塞的。这意味着某些操作(例如打开一个文件或记录 access/error 日志)可能会导致事件循环中止(即使启用了 aio,aio_write 和线程池),这样会使得尾部延迟增加,从而导致旋转磁盘驱动器上出现几秒钟的延迟。

Envoy 是一个类似的事件驱动式的架构,只是它使用的是线程而不是进程。它也同样支持 SO_REUSEPORT(自带一个 BPF 过滤器的支持),并且依赖 libevent 实现事件循环(换句话说,没有用到像 EPOLLEXCLUSIVE 这样的 epoll(2) 功能)。Envoy 在事件循环里并没有任何阻塞 IO 的操作。甚至日志记录也是以非阻塞方式实现的,因此它不会出现停顿的现象。

从理论上讲,Nginx 和 Envoy 应该具有相似的性能特征。但是“希望”不是我们的做事风格,因此我们的第一步便是针对经过类似调整的 Nginx 和 Envoy 设置运行各种工作负载测试。

我们的测试结果表明,在大多数测试工作负载下,Nginx 和 Envoy 拥有相近的性能表现:单秒高请求(RPS),高带宽以及混合的低延迟/高带宽的 gRPC 代理。

可以说,进行一个良好的性能测试非常困难。Nginx 有提供一个用于性能测试的准则,但是这些准则尚未形成规范。Envoy 也有提供一个基准测试指南,甚至在 envoy-perf 项目下也提供了一些工具,但遗憾的是后者似乎不怎么维护了。

我们转而使用内部的测试工具。之所以称其为“绿巨人(hulk)”,是因为它在粉碎我们的服务方面享有盛誉。

这么说吧,我们发现测试结果里有几处显著的差异:

我们也知道统计数据收集模块效率很低下。我们有考虑过在用户空间里实现一个类似于 FreeBSD 的 counter(9) 的功能:绑定 CPU 核心,为每个 worker 分配一个无锁的计数器以及一个取值的例程,该例程会循环遍历所有 worker 并聚合汇总他们各自的统计信息。但是我们最终放弃了这个想法,因为如果我们想要监控 Nginx 内部状态(比如所有错误情况),那就意味着我们要维护一个巨大的补丁程序,这将使得后续升级变成一个真正的地狱。

由于 Envoy 不会受到这两个问题的困扰,因此在迁移到 Envoy 之后,我们可以释放多达 60% 的服务器资源(之前被 Nginx 独占)。

可监察性

可监察性是任何产品的最基本的运维需求,尤其是对于像代理这样的基础设施而言。在迁移期间,这一点尤为关键,这样一来任何问题都可以被监控系统检测到,而不用等到沮丧的用户报障时才发现。

非商业版本的 Nginx 自带一个“stub status”模块,提供了 7 个统计信息: 

  1. Active connections: 291  
  2. server accepts handled requests 
  3. 16630948 16630948 31070465  
  4. Reading: 6 Writing: 179 Waiting: 106 

这当然是不够的,因此我们添加了一个简单的 log_by_lua 处理程序,该处理程序根据 Lua 中可用的 HTTP header 和变量添加每个请求的统计信息:状态代码,大小,缓存命中率等。以下是一个简单的吐出统计数据的函数的例子: 

  1. function _M.cache_hit_stats(stat) 
  2. if _var.upstream_cache_status then 
  3.     if _var.upstream_cache_status == "HIT" then 
  4.         stat:add("upstream_cache_hit"
  5.     else 
  6.         stat:add("upstream_cache_miss"
  7.     end 
  8. end 
  9. end 

除了每个请求的 Lua 统计信息外,我们还有一个非常脆弱的 error.log 解析器,它负责 upstream,http,Lua 和 TLS 的错误分类。

在这些之上,我们有一个单独的 exporter ,用于收集 Nginx 的内部状态:自上次 reload 的时间,worker 数量,RSS/VMS 大小,TLS 证书使用期限等。

典型的 Envoy 配置为我们提供了数千种不同的指标(采用 Prometheus 格式),用于描述代理流量和服务器的内部状态: 

  1. $ curl -s http://localhost:3990/stats/prometheus | wc -l  
  2. 14819 

这里面囊括了五花八门的来自不同地方汇总的大量统计信息:

Envoy 的管理接口真心赞。它不仅通过 /certs,/clusters 和 /config_dump 端点提供额外的结构化的统计信息,而且还提供了非常重要的运维功能:

除了统计数据外,Envoy 还支持可插拔的 tracing 实现。这不仅对拥有多个负载均衡层的接入层团队有用,对于希望从边缘到应用服务器端到端跟踪请求延迟的应用程序开发人员也很有用。

从技术上讲,Nginx 也支持通过第三方的 OpenTracing 集成进行跟踪,但是该功能开发尚不成熟。

最后,同等重要的是,Envoy 能够通过 gRPC 流式传输 access 日志。这减轻了我们接入层团队不得不支持打通 syslog 到 hive 的负担。此外,在 Dropbox 生产中启动通用 gRPC 服务比添加自定义的 TCP/UDP 监听器更容易(也更安全!)。

和其他所有操作类似,Envoy 里面 access 日志的配置是通过 gRPC 管理服务,即访问日志服务(ALS)设置。管理服务是将 Envoy 数据平面与生产中的各种服务集成的标准方式。这也是我们下一节要介绍的内容。

集成

Nginx 提供的集成方案,一个最佳描述便是 “Unix-ish”。配置是非常静态的。而且它严重依赖于文件(例如配置文件本身,TLS 证书和 Ticket 、白名单/黑名单等)以及一些知名的行业协议(记录日志到 syslog 以及通过 HTTP 发送认证子请求)。对于小型部署而言,这样做的简单性和向后兼容性是一件好事,因为我们可以通过编写几个 Shell 脚本轻松实现 Nginx 的自动化。但是随着系统规模的扩大,可测试性和标准化变得更加重要。

Envoy 对于是否应该将接入层数据平面和它的控制平面,以及因此也即是与其他基础设施集成在一起,持更加坚定的态度。它通过提供一个稳定的 API(通常称为 xDS ),鼓励用户使用 protobuf 和gRPC。Envoy 通过查询一个或多个这样的 xDS 服务来发现其动态资源。

如今,xDS API 的发展已然超越 Envoy:通用数据平面 API(UDPA)的宏伟目标是“成为事实上的 L4/L7 负载均衡器标准”。

根据我们的经验,这一雄心壮志是靠谱的。我们已经在内部负载测试中使用了开放请求成本汇总(ORCA),并且正在考虑将 UDPA 用于非 Envoy 的负载均衡,例如我们基于 Katran 的 eBPF/XDP 4层负载均衡代理。

这对于 Dropbox 尤其适合,Dropbox 的所有服务都已经实现了在内部通过基于 gRPC 的 API 进行交互。我们已经实现了自主版本的 xDS 控制平面,它将 Envoy 同我们的配置管理、服务发现、私密信息管理以及路由信息集成在一起。

以下是一些可用的 xDS 服务,它们的 Nginx 替代品以及我们如何使用它们的一些示例:

关于 Envoy 如何与现有生产系统集成的示例,这里有一个将 Envoy 与自定义服务发现集成的一个经典例子。还有一些开源的 Envoy 控制面板实现,例如 Istio 和更少复杂度的 go-control-plane。

我们自己的 Envoy 控制平面实现了越来越多的 xDS API。它在生产中以普通的 gRPC 服务的形式部署,并充当我们基础设施构建块的一个适配器。它通过一组通用的 Golang 库来实现与内部服务进行对话,并通过稳定的 xDS API 向 Envoy 公开它们。整个过程不涉及任何文件系统调用,信号量,cron,logrotate,syslog,日志解析器等。

配置

Nginx 在配置方面具有简单易读这样一项不可否认的优势。但是,随着配置变得越来越复杂,并且配置开始变成是代码生成式的,这项优势就没了。

正如上面所提到的,我们的 Nginx 配置是通过 Python2,Jinja2 和 YAML 的混合生成的。 你们中的一些人可能已经在 erb,pug,Text::Template 甚至 m4 里面看到或甚至编写了这样类似的变体: 

  1. {% for server in servers %} 
  2. server { 
  3. {% for error_page in server.error_pages %} 
  4. error_page {{ error_page.statuses|join(' ') }} {{ error_page.file }}; 
  5. {% endfor %} 
  6. ... 
  7. {% for route in service.routes %} 
  8. {% if route.regex or route.prefix or route.exact_path %} 
  9. location {% if route.regex %}~ {{route.regex}}{% 
  10.         elif route.exact_path %}= {{ route.exact_path }}{% 
  11.         else %}{{ route.prefix }}{% endif %} { 
  12.     {% if route.brotli_level %} 
  13.     brotli on
  14.     brotli_comp_level {{ route.brotli_level }}; 
  15.     {% endif %} 
  16.     ... 

我们的 Nginx 配置的生成方式有一个大问题:配置生成中涉及的所有语言都允许代入 和/或 逻辑。YAML 有 anchor,Jinja2 有 loop/ifs/macroses,然后,Python 当然是图灵完备的。如果没有一个干净的数据模型,复杂性会迅速扩散到这三者。

这个问题自然是可以解决的,但是有两个基本问题:

另一方面,Envoy 自带一套用于配置的统一数据模型:它的所有配置都放到 protobuffer 中定义。这不仅解决了数据建模问题,而且还将输入的信息添加到了配置值里。鉴于 protobuf 是Dropbox 生产环境里的头等公民,并且是描述/配置服务的通用方式,因此,集成就变得更加简单了。

我们针对 Envoy 设计的新的配置生成器是基于 protobuf 和 Python3 实现的。所有数据建模均在原始文件中完成,而所有逻辑均在 Python 中执行。下面是一个例子: 

  1. from dropbox.proto.envoy.extensions.filters.http.gzip.v3.gzip_pb2 import Gzip 
  2. from dropbox.proto.envoy.extensions.filters.http.compressor.v3.compressor_pb2 import Compressor 
  3.  
  4. def default_gzip_config( 
  5. compression_level: Gzip.CompressionLevel.Enum = Gzip.CompressionLevel.DEFAULT
  6. ) -> Gzip: 
  7.     return Gzip( 
  8.         # Envoy's default is 6 (Z_DEFAULT_COMPRESSION). 
  9.         compression_level=compression_level, 
  10.         # Envoy's default is 4k (12 bits). Nginx uses 32k (MAX_WBITS, 15 bits). 
  11.         window_bits=UInt32Value(value=12), 
  12.         # Envoy's default is 5. Nginx uses 8 (MAX_MEM_LEVEL - 1). 
  13.         memory_level=UInt32Value(value=5), 
  14.         compressor=Compressor( 
  15.             content_length=UInt32Value(value=1024), 
  16.             remove_accept_encoding_header=True
  17.             content_type=default_compressible_mime_types(), 
  18.         ), 
  19.     ) 

注意上述代码里的 Python3 类型注解!结合 mypy-protobuf protoc 插件,它们可以在 config 生成器里提供端到端的输入。如果你用的 IDE 支持自动检查的话,它将会立即高亮显示输入信息不匹配。

在某些情况下,经过类型检查的 protobuf 在逻辑上仍然可能是无效的。在上面的示例中,gzip window_bits 只能取 9 到 15 之间的值。这类限制可以在 protoc-gen-validate protoc 插件的帮助下轻松完成定义:

  1. google.protobuf.UInt32Value window_bits = 9 [(validate.rules).uint32 = {lte: 15 gte: 9}]; 

最后,使用官方定义的配置模型的一个潜在好处是,它有机地引领了文档与配置定义并排配置。 以下是一个 gzip.proto 的示例: 

  1. // Value from 1 to 9 that controls the amount of internal memory used by zlib. Higher values.            
  2. // use more memory, but are faster and produce better compression results. The default value is 5.             
  3. google.protobuf.UInt32Value memory_level = 1 [(validate.rules).uint32 = {lte: 9 gte: 1 

可扩展性

要想让 Nginx 提供超出标准配置所提供的功能范畴之外的特性的话,通常需要编写一个 C 模块。 Nginx 的开发指南对于可用的构建块提供了详尽的介绍。这也就是说,这种方式是相对重量级的。实际上,要想安全地编写一个 Nginx 模块的话,需要一位相当资深的软件工程师。

关于可供模块开发人员选用的基础设施的话,他们可以期待一些像哈希表、队列、rb树这样的基础容器,(非RAII)内存管理、以及可用于所有请求处理阶段的钩子。另外,还有一些外部库,比如 pcre、zlib、openssl,当然,还有libc。

为了提供更轻量级的功能扩展,Nginx 提供了 Perl 和 JavaScript 的接口。可悲的是,它们所提供的功能都相当有限,绝大部分都局限于请求处理的内容阶段。

社区采纳的最常用的扩展方式是基于第三方的 lua-nginx 模块和各种 Openresty 类库。这一方案几乎可以外挂到请求处理的任意阶段。我们使用 log_by_lua 进行统计信息的收集,然后使用 balancer_by_lua 进行动态地后端再配置。

从理论上讲,Nginx 提供了使用 C++ 开发模块的能力。然而实际上,对于所有原语,它都缺少适当的 C++ 接口/包装器,这样就显得不太值得了。尽管如此,仍有一些社区对此进行尝试。这些还远远没有达到生产就绪。

Envoy 的主要扩展机制是通过 C++ 插件。这个流程在文档方面不如 Nginx,但是它更为简单。部分原因是:

具体地,这里有一份 HTTP 过滤模块的经典例子。

通过简单地实现 Envoy stats 接口,我们仅用200行代码就可以将 Envoy 与 Vortex2(我们的监视框架)集成在一起。

Envoy 还通过 moonjit 添加了对 Lua 的支持,moonjit 是一个对 Lua 5.2 做了诸多改进支持的 LuaJIT fork。与 Nginx 的第三方 Lua 集成相比,它所提供的功能和开放的钩子要少得多。由于在开发、测试和解释后代码的排障等诸多方面存在的额外复杂性成本,这使得 Lua 对 Envoy 的吸引力大大降低。专门从事 Lua 开发的公司可能会不同意这一观点,但是在我们的案例中,我们决定避免使用它,而只是将 C++ 用于 Envoy 的可​​扩展性。

Envoy 与其他 Web 服务器的区别就在于它提供了对 WebAssembly(WASM)的新兴支持 —— 一种快速的,可移植且安全的扩展机制。WASM 不能直接使用,但是可以作为任何通用编程语言的编译目标。Envoy 实现了一个适用于代理的 WebAssembly 规范(还包括相关的 Rust 和 C++ SDK),该规范描述了 WASM 代码和通用的 L4/L7 代理之间的边界。代理和扩展之间代码的分隔提供了一个安全的沙箱,而 WASM 低级凝练的二进制格式又为之提供了接近原生的效率。在此之上,在 Envoy 里,proxy-wasm 扩展是和 xDS 集成在一起的。这样便可以进行动态地更新,甚至可以进行潜在的 A/B 测试。

在 KubeCon’s 19 上,《通过 WebAssembly 扩展 Envoy》这个演讲很好地概述了 Envoy 里集成的 WASM 及其潜在用途。它还暗示已经达到了原生 C++ 代码性能水平的 60-70%。

使用 WASM,服务提供方可以安全有效地在其边缘环境运行客户的代码。客户获益的则是可移植性:他们的扩展可以在实现 proxy-wasm ABI 的任何云上运行。此外,它允许用户使用任意一种语言,只要它可以编译为 WebAssembly。这使得他们能够安全有效地使用更广泛的非 C++ 类库。

Istio 正在向 WebAssembly 的开发倾注大量资源:他们已经有了基于 WASM 的遥测扩展的实验版本以及用于共享扩展的 WebAssemblyHub 社区。

当前,Dropbox 并未用到 WebAssembly。但是,等到 proxy-wasm 的 GO SDK 可用时,这一情况也许会发生变化。

构建和测试

默认情况下,Nginx 使用的是一套自定义的基于 shell 的配置系统和基于 make 的构建系统构建的。这是简单而优雅的,但是将其集成到 Bazel 构建的 monorepo 的话需要花费大量的精力才能获得增量、分布式、封闭和可重现构建这些所有优点。

Google 开源了他们用 Bazel 构建的 Nginx 版本,该版本由 Nginx,BoringSSL,PCRE,ZLIB 和 Brotli 库/模块组成。

在测试方面,Nginx 在一个单独的仓库里有一组 Perl 驱动的集成测试,没有任何单元测试。

鉴于我们对 Lua 的大量使用以及缺乏内置的单元测试框架,我们求助于使用模拟配置和基于 Python 的简单测试驱动程序进行测试: 

  1. class ProtocolCountersTest(NginxTestCase): 
  2. @classmethod 
  3. def setUpClass(cls): 
  4.     super(ProtocolCountersTest, cls).setUpClass() 
  5.     cls.nginx_a = cls.add_nginx( 
  6.         nginx_CONFIG_PATH, endpoint=["in"], upstream=["out"], 
  7.     ) 
  8.     cls.start_nginxes() 
  9.  
  10. @assert_delta(lambda d: d == 0, get_stat("request_protocol_http2")) 
  11. @assert_delta(lambda d: d == 1, get_stat("request_protocol_http1")) 
  12. def test_http(self): 
  13.     r = requests.get(self.nginx_a.endpoint["in"].url("/")) 
  14.     assert r.status_code == requests.codes.ok 

最重要的是,我们通过预处理所有生成的配置(例如用 127/8 替换所有 IP 地址,切换到自签名 TLS 证书等)并在结果上运行 nginx -c 来验证所有语法的语法正确性。

在 Envoy 方面,其主流的构建系统已经是 Bazel。因此,将它和我们的 monorepo 集成起来并不繁琐:Bazel 支持轻松添加外部依赖项。

我们还使用 copybara 脚本来同步 Envoy 和 udpa 的 protobuf。当你需要进行简单的转换而无需永远维护大型补丁集时,Copybara 十分方便。

通过 Envoy,我们可以灵活地选择使用一组预先编写好的 mock 来进行单元测试(基于 gtest/gmock ),也可以选择使用 Envoy 的集成测试框架,或者同时使用两者。至此,针对每一项细小的改动,我们不再需要依靠缓慢的端到端集成测试了。

gtest 是 Chromium 和 LLVM 等项目所采用的一套相当有名的单元测试框架。如果你想进一步了解 googletest,这里有两份不错的介绍资料:googletest 和 googlemock。

开源的 Envoy 开发要求每次更改达到 100% 的单元测试覆盖率。它会通过 Azure CI 流水线针对每个 PR 自动触发测试。

使用 google/becnhmark 对性能敏感的代码进行微基准测试也是一种常见做法: 

  1. $ bazel run --compilation_mode=opt test/common/upstream:load_balancer_benchmark -- --benchmark_filter=".*LeastRequestLoadBalancerChooseHost.*" 
  2. BM_LeastRequestLoadBalancerChooseHost/100/1/1000000          848 ms          449 ms            2 mean_hits=10k relative_stddev_hits=0.0102051 stddev_hits=102.051 
  3. ... 

改用 Envoy 之后,我们开始完全依赖单元测试来进行内部模块开发: 

  1. TEST_F(CourierClientIdFilterTest, IdentityParsing) { 
  2. struct TestCase { 
  3. std::vector uris; 
  4. Identity expected; 
  5. }; 
  6. std::vector tests = { 
  7. {{"spiffe://prod.dropbox.com/service/foo"}, {"spiffe://prod.dropbox.com/service/foo""foo"}}, 
  8. {{"spiffe://prod.dropbox.com/user/boo"}, {"spiffe://prod.dropbox.com/user/boo""user.boo"}}, 
  9. {{"spiffe://prod.dropbox.com/host/strange"}, {"spiffe://prod.dropbox.com/host/strange""host.strange"}}, 
  10. {{"spiffe://corp.dropbox.com/user/bad-prefix"}, {""""}}, 
  11. }; 
  12. for (auto& test : tests) { 
  13. EXPECT_CALL(*ssl_, uriSanPeerCertificate()).WillOnce(testing::Return(test.uris)); 
  14. EXPECT_EQ(GetIdentity(ssl_), test.expected); 
  15. }  

实施亚秒级的往返测试在生产力方面会带来复合效果。它让我们能够把更多精力放在增加测试覆盖范围。由于可以在单元测试和集成测试之间进行自由选择,这使得我们能够在 Envoy 测试的覆盖范围、速度和成本之间获得一个平衡。

学习上手 Bazel 对我们开发人员来说是一次绝佳的经历。它的学习曲线非常陡峭,而且前期需要付出大量的投资,但是它却有很高的回报:增量式构建,远程缓存,分布式构建/测试等。

Bazel 很少讨论的好处之一是,它让我们能够查询甚至扩展依赖图谱。依赖关系图的编程接口,以及跨语言的通用构建系统,这是一项非常强大的功能。它可以用作代码提示器,代码生成,漏洞跟踪以及部署系统等这类服务的基础构建块。

安全性

Nginx 的代码面非常小,它的外部依赖很少。通常,对生成的二进制文件来说只能看到 3 个外部依赖项:zlib(或者更快的变体之一),一个 TLS 库以及 PCRE。Nginx 自研实现了所有相关的协议解析器、事件库,甚至还重新实现了某些 libc 函数。

在某些情况下,Nginx 被认为非常安全,以至于它被用作 OpenBSD 里默认的 Web 服务器。后来,两个开发社区发生了冲突,也因此有了 httpd。您可以在 BSDCon《介绍OpenBSD的新 httpd 》中了解此举背后的动机。

这种极简主义在实践中得到了回报。Nginx 在11年多的时间里仅报告了30个安全漏洞。

另一方面,Envoy 的代码量更大,尤其是考虑到 C++ 代码比用于 Nginx 的基本 C 代码更加密集时,这一点更为明显。它还包含来自外部依赖项的数百万行代码。从事件通知到协议解析器的所有内容都推给了第三方库。这会增加攻击面并造成最终产出的可执行文件愈加膨胀。

为了解决这个问题,Envoy 高度依赖现代安全实践。它使用 AddressSanitizer,ThreadSanitizer 和 MemorySanitizer。它的开发人员甚至超纲地采用了模糊测试。

任何对于全球IT基础架构至关重要的开源项目都可以接入到 OSS-Fuzz,这是一个用于自动化模糊测试的免费平台。要了解更多信息,请参阅“OSS-Fuzz/架构”。

实际上,尽管如此,所有这些预防措施都不能完全抵消增加的代码所留下的痕迹。结果便是,在过去的两年里,Envoy 已经发布了 22 条安全公告。

Envoy 的《安全发布策略》一文对此进行了详细描述,针对某些指定的漏洞还有详细的检查报告。Envoy 还是 Google 漏洞奖励计划(VRP)的成员之一。VRP 向所有安全研究人员开放,根据它们设计的规则,对发现和报告的漏洞提供奖励。

为了应对不断增长的漏洞风险,我们使用了来自上游发行版供应商 Ubuntu 和 Debian 的最佳可执行文件来强化安全措施。我们为所有边缘环境曝光的可执行文件定义了特殊加固后的构建配置文件。它包括 ASLR,堆栈保护器以及符号表的加固: 

  1. build:hardened --force_pic 
  2. build:hardened --copt=-fstack-clash-protection 
  3. build:hardened --copt=-fstack-protector-strong 
  4. build:hardened --linkopt=-Wl,-z,relro,-z,now 

在绝大多数环境里,fork 的 Web 服务器(如 Nginx)在堆栈保护器方面会存在问题。由于主进程和工作进程共享同一套堆栈来进行灰度,而在灰度验证失败时,工作进程会被杀死,因此,我们可以通过大约 1000 次尝试逐位对灰度的实例进行暴力破解。使用线程作为并发原语的 Envoy 不受此攻击的影响。

我们还希望尽可能的给第三方依赖的安全性加固。我们在 FIPS 模式下使用 BoringSSL,该模式包括启动自检和可执行文件的完整性检查。我们还考虑在某些边缘环境的灰度服务器上运行启用 ASAN 的可执行文件。

功能性

这是帖子中最主观的部分,请做好准备。

Nginx 最初是设计成一个 Web 服务器,致力于以最少的资源消耗来提供静态文件服务。它的功能性在这里是最重要的:静态文件服务,缓存(包括惊群保护)以及 range 缓存。

但是,作为代理的话,Nginx 缺乏现代基础架构所需的功能。它的后端没有 HTTP/2。尽管可以代理 gRPC 服务,但是它不支持连接多路复用。而且不支持 gRPC 转码。最重要的是,Nginx的“核心开放(open-core)”模型限制了可以纳入开源版本代理服务的功能集。结果便是,某些重要功能(如统计信息)在它的“社区”版本里是不可用的。

相比之下,Envoy 已经演变为一个 ingress/egress 代理,经常用于以 gRPC 为主要负载的环境。尽管它的 Web 服务功能还是相当初级的:没有文件服务,而且缓存功能仍然在开发中,brotli或者预压缩这些功能也都不支持。针对这些场景,我们仍然提供了一个小的 fallback Nginx 实例,Envoy 会把它当成上游集群。

等到 Envoy 的 HTTP 缓存功能生产就绪时,我们便可以将大多数静态服务用例迁移过去,使用 S3 取代文件系统作为长期存储。

Envoy 还提供了许多 gRPC 相关能力的原生支持:

此外,Envoy 还可以用作一个出站代理。我们通过它统一了另外几个用例场景:

社区

Nginx 的开发是相当中心化的。它的绝大部分开发活动都是核心团队内部进行的。nginx-devel 邮件列表上有一些外部活动,然后官方 Bug 跟踪上偶尔有一些与开发相关的讨论。

FreeNode 上有一个 #nginx 频道。我们可以随时加入到里面进行更多互动性质的社区对话。

Envoy 的开发则是开放和去中心化的:通过 GitHub issue/pull request,邮件列表和社区会议进行协同。

Slack 上的社区活动也很活跃。你可以在这里收到邀请。

开发风格和工程社区这些方面很难量化定性,因此,我们不妨一起来看一个开发 HTTP/3 的特定例子。

F5 最近发表了 Nginx QUIC 和 HTTP/3 实现。这块代码是干净的,没有任何外部依赖。但是开发过程本身并不透明。在此之前的半年,Cloudflare 提出了自己的 Nginx HTTP/3 实现。结果便是,该社区现在拥有两套单独的用于 Nginx 的 HTTP/3 实验版本。

再来看看 Envoy ,HTTP/3 的实现这块也正在开发中,它是基于 chromium 的 “quiche”(QUIC,HTTP等)库。该项目的进展可以通过 这个 GitHub issue 跟踪。在补丁开发完成前,设计文档便已经对外公布了。剩下的工作里,想要从社区参与中获益的部分会带有 “help wanted” 标签。

如你所见,后者显然更加开放透明,而且非常鼓励协作开发。对我们来说,这意味着我们能够从上游获得大量的 Envoy 相关的大大小小的变动 —— 包括从运维改进和性能调优到新的 gRPC 转码功能以及负载均衡功能的一些变动。

迁移后的现状

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

迁移的最后一件事便是 www.dropbox.com 自己。在完成迁移后,我们便可以开始停用我们在边缘环境部署的 Nginx 服务。一个时代即将结束。

我们遇到的一些问题

当然,整个迁移的过程并非完美无瑕。不过至少没有导致任何明显的中断。整个迁移过程中最困难的部分是我们的 API 服务这块。大量不同的设备通过我们的公共 API 和 Dropbox 进行通信——从 curl/wget 驱动的 Shell 脚本,以及具有自定义 HTTP/1.0 堆栈的嵌入式设备,到每个可能访问到这里的 HTTP 库。 Nginx 是经过实践检验的行业事实标准。可以理解,大多数库都隐式地依赖于它的某些行为。除了我们的 api 用户在依赖的 Nginx 行为这块,在转到 Envoy 后遇到了一些行为不一致的地方以外,我们在使用 Envoy 和它提供的类库的过程中还发现了许多 bug。在社区的帮助下,所有这些问题都很快得到了解决并且反馈到了上游。

下面只是其中一些“异常的”/没有出现在 RFC 里的行为的摘要:

另外,值得一提的是,我们还遇到了一些常见的配置问题:

下一步

 

来源:Dockone.io内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯