原文连接:https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310html
关于Envoy代码库的低级技术文档目前至关稀少。 为了纠正这个问题,我打算作一系列关于各类子系统的博客文章。 因为这是第一篇文章,请让我知道您的想法以及您但愿了解的其余主题。git
我获得的关于Envoy的最多见技术问题之一是要求对其使用的线程模型进行低级描述。 这篇文章将介绍Envoy如何将链接映射到线程,以及内部使用的线程本地存储(TLS)系统的描述,以使代码极其平行且性能高。github
Envoy使用三种不一样类型的线程,如图1所示。编程
Main:此线程拥有服务器启动和关闭,全部xDS API处理(包括DNS,运行情况检查和常规集群管理),运行时,统计刷新,管理和通常进程管理(信号,热启动等)。 在此线程上发生的全部事情都是异步的而且是“非阻塞的”。一般,主线程协调全部不须要大量CPU来完成的关键过程功能。 这容许将大多数管理代码编写为单线程编写。缓存
Worker:默认状况下,Envoy为系统中的每一个硬件线程生成一个工做线程。 (这能够经过--concurrency 选项控制)。 每一个工做线程运行一个“非阻塞”事件循环,负责监听每一个侦听器(当前没有侦听器分片),接受新链接,为链接实例化过滤器堆栈,以及处理全部IO的生命周期。 链接。 一样,这容许将大多数链接处理代码写成好像是单线程的。服务器
文件刷新器:Envoy写入的每一个文件(主要是访问日志)当前都有一个独立的阻塞刷新线程。 这是由于即便使用O_NONBLOCK写入文件系统缓存文件有时也会阻塞(叹息)。 当工做线程须要写入文件时,数据实际上被移入内存缓冲区,最终经过文件刷新线程刷新。 这是代码的一个区域,技术上全部工做人员均可以阻止同一个锁尝试填充内存缓冲区。 还有一些其余的将在下面进一步讨论。网络
链接处理数据结构
如上所述,全部工做线程都会在没有任何分片的状况下监听全部侦听器。 所以,内核用于智能地将接受的套接字分派给工做线程。 现代内核通常都很擅长这个; 他们使用诸如IO优先级提高之类的功能来尝试填充线程的工做,而后开始使用同时监听同一套接字的其余线程,以及不使用单个自旋锁来处理每一个接受。架构
一旦工人接受了链接,它就永远不会离开那个工人。 全部进一步的链接处理都在工做线程内彻底处理,包括任何转发行为。 这有一些重要的含义:并发
Envoy中的全部链接池都是每一个工做线程。 所以,尽管HTTP / 2链接池一次只与每一个上游主机创建一个链接,但若是有四个工做站,则每一个上游主机在稳定状态下将有四个HTTP / 2链接。
Envoy以这种方式工做的缘由是由于经过将全部代码保存在单个工做线程中,几乎全部代码均可以在没有锁的状况下编写,就像它是单线程同样。 这种设计使得大多数代码更易于编写,而且能够很是好地扩展到几乎无限数量的工做人员。
然而,一个主要的问题是,从内存和链接池效率的角度来看,调整 -- 并发选项实际上很是重要。 拥有比所需更多的工做人员将浪费内存,建立更多空闲链接,并致使更低的链接池命中率。 在Lyft,咱们的边车Envoys以很是低的并发性运行,所以性能大体与他们旁边的服务相匹配。 咱们只以最大并发性运行咱们的边缘Envoys。
什么是非阻塞
到目前为止,在讨论主线程和工做线程如何操做时,已经屡次使用术语“非阻塞”。 全部代码都是在假设没有任何阻塞的状况下编写的。 然而,这并不彻底正确(彻底是真的吗?)。 特使确实采用了一些过程宽锁:
如前所述,若是正在写入访问日志,则全部工做程序在填充内存访问日志缓冲区以前都会获取相同的锁。 锁定保持时间应该很是低,可是这种锁能够在高并发性和高吞吐量下竞争。
Envoy采用了一个很是复杂的系统来处理线程本地的统计数据。 这将是一个单独的帖子的主题。 可是,我将简要提一下,做为线程本地统计处理的一部分,有时须要获取对中央“stat store”的锁定。这种锁定不该该高度争用。
主线程须要按期与全部工做线程协调。 这是经过从主线程“发布”到工做线程(有时从工做线程返回到主线程)来完成的。 发布须要锁定,以便将发布的消息放入队列中以便之后发送。 这些锁永远不该该高度争用,但它们仍然能够在技术上阻止。
当Envoy将本身记录到标准错误时,它会获取进程范围的锁定。 通常来讲,Envoy本地记录被认为是表现糟糕的,因此没有多少考虑改善这一点。
还有一些其余随机锁,但它们都不在性能关键路径中,永远不该该争用。
线程本地存储
因为Envoy将主线程职责与工做线程职责分开,所以须要在主线程上完成复杂处理,而后以高度并发的方式使每一个工做线程可用。 本节介绍了Envoy的高级线程本地存储(TLS)系统。 在下一节中,我将描述如何使用它来处理集群管理。
如已经描述的那样,主线程基本上处理Envoy过程当中的全部管理/控制平面功能。 (控制平面在这里有点过载可是当在特使过程当中考虑并与工人作的转发进行比较时,彷佛是合适的)。 主线程进程执行某些工做是一种常见模式,而后须要使用该工做的结果更新每一个工做线程,而且工做线程不须要在每次访问时获取锁定。
Envoy的TLS系统的工做原理以下:
在主线程上运行的代码能够分配进程范围的TLS槽。 虽然是抽象的,但实际上,这是一个容许O(1)访问的向量索引。
主线程能够将任意数据设置到其槽中。 完成此操做后,数据将做为正常事件循环事件发布到每一个工做程序中。
工做线程能够从其TLS槽读取,并将检索那里可用的任何线程本地数据。
虽然很是简单,但这是一个很是强大的范例,与RCU锁定概念很是类似。 (实质上,工做线程在工做时从不会看到TLS插槽中的数据发生任何变化。更改只发生在工做事件之间的静止期间)。 特使以两种不一样的方式使用它:
经过在没有任何锁定的状况下访问每一个工做人员存储不一样的数据
经过将共享指针存储到每一个worker的只读全局数据。 所以,每一个工做者都具备对在工做时不能递减的数据的引用计数。 只有当全部工做人员都已停顿并加载新的共享数据时,旧数据才会被销毁。 这与RCU相同。
集群线程更新
在本节中,我将描述TLS如何用于集群管理。 群集管理包括xDS API处理和/或DNS以及运行情况检查。
图3显示了涉及如下组件和步骤的整体流程:
集群管理器是Envoy内部的组件,用于管理全部已知的上游集群,CDS API,SDS / EDS API,DNS和活动(带外)运行情况检查。 它负责建立每一个上游集群的最终一致视图,其中包括已发现的主机以及运行情况。
运行情况检查程序执行活动运行情况检查,并将运行情况更改报告回集群管理器。
执行CDS / SDS / EDS / DNS以肯定群集成员资格。 状态更改将报告回集群管理器。
每一个工做线程都在不断运行事件循环。
当集群管理器肯定集群的状态已更改时,它会建立集群状态的新只读快照,并将其发布到每一个工做线程。
在下一个静止期间,工做线程将更新分配的TLS插槽中的快照。
在须要肯定要负载均衡的主机的IO事件期间,负载均衡器将在TLS插槽中查询主机信息。 没有得到锁定来执行此操做。 (另请注意,TLS还能够在更新时触发事件,以便负载平衡器和其余组件能够从新计算高速缓存,数据结构等。这超出了本文的范围,但在代码中的各个位置使用)。
经过使用先前描述的过程,Envoy可以处理每一个请求而不须要任何锁定(除了以前描述的那些)。 除了TLS代码自己的复杂性以外,大多数代码都不须要理解线程如何工做,而且能够编写为单线程。 这使得大多数代码更容易编写,并产生出色的性能。
其余使用TLS的子系统
TLS和RCU在Envoy中普遍使用。 其余一些例子包括:
运行时(功能标志)覆盖查找:在主线程上计算当前功能标志覆盖映射。 而后使用RCU语义为每一个工做程序提供只读快照。
路由表交换:对于RDS提供的路由表,路由表在主线程上实例化。 而后使用RCU语义为每一个工做程序提供只读快照。 这使得路由表交换有效地原子化。
HTTP日期标头缓存:事实证实,在每一个请求上计算HTTP日期标头(当每一个核心执行~25K + RPS时)很是昂贵。 Envoy大约每半秒计算一第二天期标题,并经过TLS和RCU将其提供给每一个工做人员。
还有其余状况,但前面的例子应该提供TLS所用事物的良好品味。
已知的性能陷阱
虽然Envoy总体表现至关不错,可是当它以很是高的并发性和吞吐量使用时,有一些已知领域须要注意:
正如本文中已经描述的那样,当前全部工做者在写入访问日志的内存缓冲区时都会得到锁定。 在高并发性和高吞吐量的状况下,当写入最终文件时,将须要以按顺序交付为代价对每一个工做人员批量访问日志进行批处理。 或者,访问日志能够成为每一个工做线程。
尽管统计信息已通过很是优化,但在很是高的并发性和吞吐量下,个别统计信息可能存在原子争用。 对此的解决方案是每一个工人计数器,按期冲洗到中央计数器。 这将在后续文章中讨论。
若是Envoy部署在几乎没有须要大量资源来处理的链接的场景中,现有架构将没法正常运行。 这是由于没法保证链接在工做人员之间均匀分布。 这能够经过实现工做者链接平衡来解决,其中工做人员可以将链接转发给另外一个工做人员进行处理。
结论
Envoy的线程模型旨在支持编程的简单性和大规模并行性,但若是调整不当可能会浪费内存和链接使用。 该模型容许它在很是高的工人数量和吞吐量下表现良好。
正如我在Twitter上简要提到的那样,该设计也适合在DPDK之类的完整用户模式网络堆栈上运行,这可能致使商用服务器在执行完整的L7处理时每秒处理数百万个请求。 看看将来几年建成什么将是很是有趣的。
最后一个快速评论:我屡次被问到为何咱们为Envoy选择C ++。 缘由仍然是它仍然是惟一普遍部署的生产等级语言,在该语言中能够构建本文中描述的体系结构。 C ++固然不适合全部项目,甚至许多项目,但对于某些用例,它仍然是完成工做的惟一工具。
代码连接
本文中讨论的一些接口和实现头的一些连接: