Envoy的线程模型[翻译]

Envoy threading Model

关于envoy 代码的底层文档至关稀少。为了解决这个问题我计划编写一系列文档来描述各个子系统的工做。因为是第一篇, 请让我知道你但愿其余主题覆盖哪些内容。html

一个我所了解到最共同的技术问题是关于envoy使用的线程模型的底层描述,这篇文章将会描述envoy如何使用线程来处理链接的(how envoy maps connections to threads), 同事也会描述Thread Local Storage(TLS)系统是如何使内部代码平行且高效的。linux

Threading overview

img

Figure 1: Threading overviewgit

如同图一所示,envoy使用了三中不一样类型的线程。github

  • Main: 这个线程管理了服务器自己的启动和终止, 全部xDS API的处理(包括DNS, 健康检查(health checking) 和一般的集群管理), running, 监控刷新(stat flushing), 管理界面 还有 通用的进程管理(signals, hot restart。在这个线程中发生的全部事情都是异步和非阻塞的(non blocking)。一般,主线程用来协调全部不须要大量cpu完成的关键功能。这容许大部分代码编写的如同单线程同样。编程

  • Worker 在envoy系统中,默认为每一个硬件(every harware)生成一个worker线程(能够经过--concurrency参数控制),每一个工做线程运行一个无阻塞的事件循环(event loop),for listening on every listener (there is currently no listener sharding), accept 新的链接,为每一个链接实例化过滤栈,以及处理这个链接生命周期的全部io。同时,也容许全部链接代码编写的如同单线程同样。api

  • File flusher Envoy 写入的每一个文件如今都有一个单独的block的刷新线程。这是由于使用O_NONBLOCK在写入文件系统有时也会阻塞。当Worker线程须要写入文件时,这个数据实际上被移动至in-memory buffer中,最终会被File Flusher线程刷新至文件中。envoy的代码会在一个worker试图写入memory buffer时锁住全部worker。还有一些其余的会在下面讨论。缓存

链接处理

如上所述, 全部worker线程都会监听端口而没有任何分片,所以,内核智能的分配accept的套接字给worker进程。现代内核通常都很擅长这一点,在开始使用其余监听同一套接字的其余线程以前,内核使用io 优先级提高等特性来分配线程的工做,一样的还有不适用spin-lock来如理全部请求。服务器

一旦woker accept了一个链接,那么这个链接永远不会离开这个worker(一个链接只由同一worker进行处理), 全部关于链接的处理都将会在worker线程内进一步处理,包括任何转发行为。这有一些重要的含义:网络

  • Envoy链接池中都是worker线程,所以经过http/2 链接池每次只与上游主机创建一个链接,若是有4个worker,那在稳定状态将只有四个http/2链接与上游主机进行链接。数据结构

  • Envoy以这种方式工做的缘由在于这样几乎全部的代码均可以如同单线程同样以没有锁的方式进行编写。这个设计使得大部分代码易于编写和扩展。

  • 从内存和链接池效率的角度来看,调整 - concurrency选项实际上很是重要。拥有比所须要的worker更多的worker将会浪费大量的内存,形成大量空置的链接,并致使较低的链接池命中率。在lyft,envoy以较低的concurerency运行,性能大体与他们旁边的服务相匹配。

non-blocking意味着什么

到目前为止,在讨论主线程和worker线程时咱们已经屡次使用术语'non-blocking'.几乎全部代码都是假设没有阻塞的状况下进行编写。可是,这并不是彻底正确(哪里有彻底正确的东西),Envoy确实使用了一些process wide locks.

  • 正如上述,若是在写入访问日志, 全部worker都会得到一样的锁在将访问日志填充至内存缓存1前。锁应当保持较短的时间,可是这个锁有可能在高并发和高吞吐量时进行竞争。
  • Envoy使用了一个很是复杂的系统来处理线程本地的统计数据,这将是另外一个帖子的主题,我简要的介绍一下,做为线程本地统计处理的一部分,它有时会得到一个对于中央统计商店2的锁, 这个锁不该当被常常竞争。
  • 主线程按期须要协调全部worker线程。这是经过主线程发不到工做线程来完成的。发布过程须要lock来锁定消息队列。这个锁不该当被高度争用,但从技术上能够进行阻止。
  • 当envoy记录日志至stderr, 它将会得到process wide lock。 一般来讲, envoy 本地日志被认为对于性能来讲是糟糕的,因此没有改善该过程的想法。
  • 还有一切其余随机锁。但他们都不在影响性能的过程当中,也永远不当被争用。

线程局部变量3

因为envoy将主线程的职责和worker线程的职责彻底分开,须要在主线程完成复杂的处理同时使每一个worker线程高度可用。本节将介绍Envoy的高级线程本地存储(TLS)系统。在下一节中,我将描述如何使用它来处理集群管理。

如已经描述的那样,主线程基本上处理Envoy过程当中的全部管理/控制平面功能。(控制平面在这里有点过载,但在特使程序自己考虑并与工人作的转发进行比较时,彷佛是合适的)。主线程进程执行某些操做是一种常见模式,而后须要使用该工做的结果更新每一个工做线程,而且工做线程不须要在每次访问时获取锁定

img

Envoy的TLS系统工做以下:

  • 运行在主线程的代码分配了一个线程范围的TLS插槽4,虽然是abstracted的,但实际上时容许o(1)访问的索引
  • 主线程能够设置任意数据在slot中,当它完成时,这个数据将会发送到全部worker做为一个正常的事件循环
  • 工做线程能够从TLS插槽中读取获取它所能获取的局部数据。

虽然很是简单,但它很是强大与只读副本更新锁相似.(实质上,worker线程在工做时从不会看到插槽的数据发生任何改变, 变化只发生在event切换的时候5), Envoy用两种不一样的方式来使用它:

  • 存储不一样的数据在每一个worker上,获取时不须要任何锁
  • 经过将指向只读全局数据的共享指针存储到每一个worker上,从而,每一个worker具备该数据的引用计数,该计数在工做时不会递减。仅当全部worker查询和读取新的共享数据时会将原数据摧毁,这与RCU相同。

集群更新线程

在本节中,我将描述TLS如何用于集群管理。群集管理包括xDS API处理和DNS以及运行情况检查。

img

图3显示了涉及如下组件和步骤的整体流程:

  1. 集群管理器(Cluster Manager)在envoy中管理已知上游的集群,包括CDS API, SDS/EDS API DNS 和活跃的健康检查。它负责建立每一个上游主机最终一致的视图,上游主机包括了发现的主机和健康统计。
  2. 健康检查器(Health Checker)进行活跃健康检查,并将健康检查的统计结果返回给集群管理器(Cluster Manager).
  3. CDS/SDS/EDS/DNS 用来决定集群的成员,一旦状态改变会返回给集群管理器(Cluster Manager).
  4. 每一个Worker 线程包含一个 Event Loop。
  5. 当集群管理器要改变集群的状态时,它将建立一个集群状态只读的快照(Snapshot), 并将它发送给每个worker 线程。
  6. 在下一个静止期中,worker线程将更新存在TLS中的快照
  7. 在IO事件须要肯定主机如何负载均衡时,负载均衡器(Load Banlancer)将会查询TLS中存有的集群状态信息,这个过程当中是无锁的。(TLS也能够触发事件使得负载均衡器和其余组件从新计算高速缓存和数据结构。但这超出了本文的范围)

经过这些过程,Envoy可以处理每一个请求而不使用任何锁。除了TLS代码自己的复杂度以外,大部分代码无需理解线程是如何具体工做的,使用单线程的方式便可。这使得大部分代码易于修改且性能较好。

其余使用TLS的子系统

TLS和RCU(Read-Copy Update6)在Envoy内普遍使用。
其余一些例子包括:

  • Runtime (feature flag) overide lookup 运行时覆盖查找:如今的feature flag overide map是在主线程进行计算的。为每一个线程提供只读的快照用来实现RCU。
  • Route table swapping 路由表交换:对于RDS提供的路由表,路由表在主线程上实例化。
    而后使用RCU语义为每一个工做程序提供只读快照.这使得路由表在原子级别上进行交换。
  • HTTP data header caching http数据头缓存: 事实证实,在每一个请求上计算HTTP日期标题很是昂贵。Envoy大约每半秒计算一第二天期标题,并经过TLS和RCU将其提供给每一个worker线程。

还有其余状况,但前面的例子应该已经足够阐释TLS在envoy中的做用。

已知的性能陷阱

虽然Envoy总体表现至关不错,可是当它以很是高的并发和高吞吐使用时,有一些已知的点须要注意:

  • 正如前文描述的那样,如今当写入日志的内存缓冲区时,全部worker线程都会得到锁,在高并发和吞吐使用时,若要写入最终文件,就会被要求处理全部worker线程的访问日志,这将致使日志的无序。
  • 尽管统计信息已经通过了不少的优化,在很是高的并发性和吞吐量下,个别统计数据仍然可能存在原子争用。解决方案时每一个工人进行统计,并按期刷新到中央计数器。
  • 若是Envoy部署在须要大量资源来处理的少许链接的场景下,现有的体系结构将没法正常工做。这是由于没法保证链接在worker 线程之间均匀分布。这能够经过实现再平衡来解决,其中worker 线程可以将链接转发给另外一个worker 线程进行处理。

总结

Envoy的线程模型旨在简化编程和提升性能,但若是设置不当,可能会浪费内存和链接使用。Envoy的线程模型容许它在很是高的worker数量和高吞吐量下表现良好。

正如我在Twitter上简略提到的那样,该设计也适合在DPDK之类的完整用户模式网络堆栈上运行,这使得商用服务器在进行完整的L7处理时每秒处理数百万个请求。观察envoy再接下来几年如何进展是一件很是有趣的事情

最后一个评论:我屡次被问到为何咱们为Envoy选择C ++?
缘由是它仍然是惟一普遍部署的生产语言,在该语言中能够构建本文所述的体系结构。C ++固然不适合全部项目,甚至许多项目,但对于某些用例,它仍然是完成工做的惟一工具。


  1. 原文 in-memory buffer

  2. central "stat store"

  3. Thread Local Storage(TLS)

  4. 原文 slot

  5. 原文 Change only happens during the quiescent period between work events

  6. 能够查看IBM对RCU的介绍

相关文章
相关标签/搜索