【翻译】Envoy线程模型

文章翻译: Envoy 线程模型 - https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

我发现这篇文章非常有趣,并且由于 Envoy 最常用作“istio”的一部分或简单地用作 kubernetes 的“入口控制器”,因此大多数人与它的直接交互方式与典型的交互方式不同。 Nginx 或 Haproxy 安装。 然而,如果有什么东西坏了,最好从内部了解它是如何工作的。 我尝试将尽可能多的文本翻译成俄语,包括特殊单词;对于那些觉得看这些很痛苦的人,我将原文留在了括号中。 欢迎来到猫。

Envoy 代码库的低级技术文档目前相当稀疏。 为了解决这个问题,我计划撰写一系列有关 Envoy 各个子系统的博客文章。 由于这是第一篇文章,请让我知道您的想法以及您可能对以后的文章感兴趣的内容。

我收到的有关 Envoy 的最常见技术问题之一是要求对其使用的线程模型进行低级描述。 在这篇文章中,我将描述 Envoy 如何将连接映射到线程,以及它内部使用的线程本地存储系统,以使代码更加并行和高性能。

线程概述

【翻译】Envoy线程模型

Envoy 使用三种不同类型的流:

  • 主要的: 该线程控制进程的启动和终止、XDS(xDiscovery Service)API 的所有处理,包括 DNS、健康检查、常规集群和运行时管理、统计重置、管理和常规进程管理 - Linux 信号、热重启等。该线程中发生的事情是异步且“非阻塞”的。 一般来说,主线程协调所有不需要大量 CPU 运行的关键功能进程。 这使得大多数控制代码都可以像单线程一样编写。
  • 工人: 默认情况下,Envoy 为系统中的每个硬件线程创建一个工作线程,这可以使用选项进行控制 --concurrency。 每个工作线程运行一个“非阻塞”事件循环,负责监听每个监听器;在撰写本文时(29 年 2017 月 XNUMX 日),监听器没有分片,接受新连接,实例化一个过滤器堆栈连接,并在连接的生命周期内处理所有输入/输出 (IO) 操作。 同样,这允许大多数连接处理代码像单线程一样编写。
  • 文件刷新器: Envoy 写入的每个文件(主要是访问日志)目前都有一个独立的阻塞线程。 这是因为即使在使用时写入文件系统缓存的文件 O_NONBLOCK 有时会被阻塞(叹气)。 当工作线程需要写入文件时,数据实际上会移动到内存中的缓冲区,最终通过线程刷新 文件刷新。 从技术上讲,这是一个代码区域,所有工作线程在尝试填充内存缓冲区时都可以阻塞同一锁。

连接处理

正如上面简要讨论的,所有工作线程都会侦听所有侦听器,而无需任何分片。 因此,内核用于优雅地将接受的套接字发送到工作线程。 现代内核通常非常擅长这一点,它们使用输入/输出(IO)优先级提升等功能来尝试在开始使用也在同一套接字上侦听的其他线程之前为线程填充工作,并且也不使用循环法锁定(Spinlock)来处理每个请求。
一旦工作线程接受连接,它就永远不会离开该线程。 连接的所有进一步处理完全在工作线程中处理,包括任何转发行为。

这有几个重要的后果:

  • Envoy 中的所有连接池都分配给一个工作线程。 因此,虽然 HTTP/2 连接池一次只与每个上游主机建立一个连接,但如果有四个工作线程,则每个上游主机在稳定状态下将有四个 HTTP/2 连接。
  • Envoy 以这种方式工作的原因是,通过将所有内容保留在单个工作线程上,几乎所有代码都可以在不阻塞的情况下编写,就像单线程一样。 这种设计使得编写大量代码变得容易,并且可以很好地扩展到几乎无限数量的工作线程。
  • 然而,主要的收获之一是,从内存池和连接效率的角度来看,配置 --concurrency。 拥有过多的工作线程会浪费内存、创建更多空闲连接并降低连接池的速率。 在 Lyft,我们的 Envoy Sidecar 容器以非常低的并发性运行,因此性能与它们旁边的服务大致匹配。 我们仅在最大并发时将 Envoy 作为边缘代理运行。

非阻塞是什么意思?

到目前为止,在讨论主线程和工作线程如何工作时,术语“非阻塞”已被多次使用。 所有代码都是在没有任何内容被阻塞的假设下编写的。 然而,这并不完全正确(什么不完全正确?)。

Envoy 使用了几个长进程锁:

  • 如前所述,在写入访问日志时,所有工作线程在内存日志缓冲区被填满之前都会获取相同的锁。 锁的持有时间应该很低,但是在高并发、高吞吐量的情况下有可能发生锁竞争。
  • Envoy 使用非常复杂的系统来处理线程本地的统计信息。 这将是另一篇文章的主题。 但是,我将简要提及,作为本地处理线程统计信息的一部分,有时需要获取中央“统计信息存储”上的锁。 永远不需要这种锁定。
  • 主线程需要定期与所有工作线程进行协调。 这是通过从主线程“发布”到工作线程,有时从工作线程“发布”回主线程来完成的。 发送需要锁定,以便发布的消息可以排队等待稍后传送。 这些锁永远不应该受到严重争议,但从技术上讲它们仍然可以被阻止。
  • 当 Envoy 将日志写入系统错误流(标准错误)时,它会获取整个进程的锁。 总的来说,从性能的角度来看,Envoy 的本地日志记录被认为很糟糕,因此没有太多关注对其进行改进。
  • 还有一些其他随机锁,但它们都不是性能关键的,并且永远不应该受到挑战。

线程本地存储

由于Envoy将主线程的职责与工作线程的职责分开的方式,因此要求能够在主线程上完成复杂的处理,然后以高并发的方式提供给每个工作线程。 本节从较高层面描述了 Envoy 线程本地存储 (TLS)。 在下一节中,我将描述如何使用它来管理集群。
【翻译】Envoy线程模型

如前所述,主线程实际上处理 Envoy 进程中的所有管理和控制平面功能。 控制平面在这里有点过载,但是当您在 Envoy 进程本身中查看它并将其与工作线程所做的转发进行比较时,它是有道理的。 一般规则是主线程进程执行一些工作,然后需要根据该工作的结果更新每个工作线程。 在这种情况下,工作线程不需要在每次访问时获取锁.

Envoy 的 TLS(线程本地存储)系统的工作原理如下:

  • 在主线程上运行的代码可以为整个进程分配一个 TLS 槽。 尽管这是抽象的,但实际上它是向量的索引,提供 O(1) 访问。
  • 主线程可以将任意数据安装到其槽中。 完成此操作后,数据将作为正常事件循环事件发布到每个工作线程。
  • 工作线程可以从其 TLS 插槽中读取并检索其中可用的任何线程本地数据。

虽然它是一个非常简单且非常强大的范例,但它与 RCU(读取-复制-更新)阻塞的概念非常相似。 本质上,工作运行时,工作线程永远不会看到 TLS 槽中的任何数据更改。 变化仅发生在工作活动之间的休息期间。

Envoy 以两种不同的方式使用它:

  • 通过在每个工作线程上存储不同的数据,可以无任何阻塞地访问数据。
  • 通过在每个工作线程上以只读模式维护指向全局数据的共享指针。 因此,每个工作线程都有一个数据引用计数,该计数在工作运行时无法递减。 只有当所有工作人员冷静下来并上传新的共享数据时,旧数据才会被销毁。 这与 RCU 相同。

集群更新线程

在本节中,我将描述如何使用 TLS(线程本地存储)来管理集群。 集群管理包括 xDS API 和/或 DNS 处理以及运行状况检查。
【翻译】Envoy线程模型

集群流管理包括以下组件和步骤:

  1. 集群管理器是 Envoy 中的一个组件,用于管理所有已知的集群上游、集群发现服务 (CDS) API、秘密发现服务 (SDS) 和端点发现服务 (EDS) API、DNS 以及主动外部检查、健康检查。 它负责创建每个上游集群的“最终一致”视图,其中包括发现的主机以及健康状态。
  2. 健康检查器执行主动健康检查并向集群管理器报告健康状态变化。
  3. 执行 CDS(集群发现服务)/SDS(秘密发现服务)/EDS(端点发现服务)/DNS 以确定集群成员资格。 状态改变被返回给集群管理器。
  4. 每个工作线程连续执行一个事件循环。
  5. 当集群管理器确定集群的状态已更改时,它会创建集群状态的新只读快照并将其发送到每个工作线程。
  6. 在下一个安静期间,工作线程将更新分配的 TLS 槽中的快照。
  7. 在确定要进行负载平衡的主机的 I/O 事件期间,负载平衡器将请求 TLS(线程本地存储)插槽以获取有关主机的信息。 这不需要锁。 另请注意,TLS 还可以触发更新事件,以便负载均衡器和其他组件可以重新计算缓存、数据结构等。 这超出了本文的范围,但在代码中的多个位置使用。

使用上述过程,Envoy 可以无任何阻塞地处理每个请求(除非前面描述过)。 除了 TLS 代码本身的复杂性之外,大多数代码不需要了解多线程如何工作,并且可以编写单线程。 除了卓越的性能之外,这使得大多数代码更容易编写。

使用 TLS 的其他子系统

Envoy 中广泛使用了 TLS(线程本地存储)和 RCU(读复制更新)。

使用示例:

  • 在执行期间更改功能的机制: 当前启用的功能列表是在主线程中计算的。 然后,每个工作线程都会使用 RCU 语义获得一个只读快照。
  • 替换路由表:对于RDS(路由发现服务)提供的路由表,路由表是在主线程上创建的。 随后将使用 RCU(读取复制更新)语义将只读快照提供给每个工作线程。 这使得更改路由表的原子效率很高。
  • HTTP 标头缓存: 事实证明,计算每个请求的 HTTP 标头(每个核心运行约 25K+ RPS)的成本相当昂贵。 Envoy 大约每半秒集中计算一次标头,并通过 TLS 和 RCU 将其提供给每个工作人员。

还有其他情况,但前面的示例应该可以很好地理解 TLS 的用途。

已知的性能缺陷

虽然 Envoy 总体表现相当不错,但在极高并发性和吞吐量的情况下使用时,有一些值得注意的地方需要注意:

  • 如本文所述,当前所有工作线程在写入访问日志内存缓冲区时都会获取锁。 在高并发和高吞吐量的情况下,您需要对每个工作线程的访问日志进行批处理,但在写入最终文件时会出现乱序交付。 或者,您可以为每个工作线程创建单独的访问日志。
  • 尽管统计信息经过高度优化,但在非常高的并发性和吞吐量下,单个统计信息可能会出现原子争用。 此问题的解决方案是为每个工作线程设置计数器,并定期重置中央计数器。 这将在后续帖子中讨论。
  • 如果 Envoy 部署在连接很少且需要大量处理资源的场景中,当前架构将无法正常工作。 无法保证连接将在工作线程之间均匀分布。 这可以通过实现工作连接平衡来解决,这将允许工作线程之间的连接交换。

结论

Envoy 的线程模型旨在提供易于编程和大规模并行性,但如果配置不正确,则可能会浪费内存和连接。 该模型使其能够在非常高的线程数和吞吐量下表现良好。
正如我在 Twitter 上简要提到的,该设计还可以在完整的用户模式网络堆栈(例如 DPDK(数据平面开发套件))之上运行,这可以使传统服务器通过完整的 L7 处理每秒处理数百万个请求。 看看未来几年会建造什么将会非常有趣。
最后一点简短的评论:我多次被问到为什么我们为 Envoy 选择 C++。 原因仍然是,它仍然是唯一可以构建本文中描述的架构的广泛使用的工业级语言。 C++ 绝对不适合所有甚至许多项目,但对于某些用例,它仍然是完成工作的唯一工具。

代码链接

具有本文讨论的接口和标头实现的文件的链接:

来源: habr.com

添加评论