☰
深入解析 gRPC 轮询引擎(Polling Engine)架构与 epoll1/poll/Windows 多实现原理
2026/10/19 14:32:11 网站建设 项目流程

深入解析 gRPC 轮询引擎(Polling Engine)架构与 epoll1/poll/Windows 多实现原理

【免费下载链接】grpcC++ based gRPC (C++, Python, Ruby, Objective-C, PHP, C#)项目地址: https://gitcode.com/GitHub_Trending/gr/grpc

导读:本文以官方文档 doc/core/grpc-polling-engines.md 为骨架,系统讲解 gRPC 事件轮询层的设计动机、对外接口与三种核心实现。你将理解 gRPC 如何在不自建线程的前提下,借助应用线程高效监听 fd 可读/可写/错误事件;掌握grpc_fd/grpc_pollset/grpc_pollset_worker/grpc_pollset_set四个不透明对象的完整 API 语义;并通过 ev_epoll1_linux.cc 等源码实例,看清 epoll1 的 designated poller、per-core neighborhood、kick 唤醒等关键机制的落地细节,为阅读与调试 iomgr 层代码奠定基础。

Polling Engine 的由来:为什么 gRPC 需要一个"轮询引擎"组件

在 gRPC Core 的 I/O 层中,网络通信依赖对一批**文件描述符(fd)**的持续监控。iomgr(I/O Manager)的设计中,轮询引擎(Polling Engine)这一组件被单独抽象出来,主要出于两点原因:

  • gRPC 代码需要监控大量 fd 上的三类事件:fd 可读(readable)、可写(writable)、出错(error);
  • gRPC 代码知道这些事件发生时应执行的动作,例如:
    • grpc_endpoint相关代码在 fd 可读时调用recvmsg,在 fd 可写时调用sendmsg;
    • tcp_client连接代码发起异步connect,直到 fd 变为可写(即连接真正完成)时才完成 client 的创建。

因此 gRPC 需要一个能"高效"完成上述监听与派发的组件。文档强调两个关键约束:

  1. 使用应用提供的线程,不创建任何新线程;
  2. 所谓"高效",是指面向**延迟(latency)与吞吐(throughput)**的优化。

换句话说,gRPC 的异步 RPC 是在用户工作线程(如调用grpc_completion_queue消费事件的线程)上驱动完成的,轮询引擎必须把 epoll_wait/poll/IOCP 等底层等待机制"借用"这些线程来完成事件分发。

不同平台上的实现一览

幸运的是,无论底层使用何种 OS 机制,所有轮询引擎都暴露相同的接口。官方文档列出的实现有:

平台实现名称选择条件
Linuxepoll1glibc 版本 >= 2.9(即内核/glibc 支持 epoll)
Linuxpoll内核不支持 epoll 时回退使用
macOS(OS X)poll默认实现
Windows(无特定名称)基于 I/O Completion Port 的专用实现

说明:文档撰写于 2018 年(作者 Sree Kuchibhotla)。从当前仓库源码看,POSIX 平台事件引擎的选择与注册仍位于 src/core/lib/iomgr/ev_posix.cc,其中静态注册表g_vtables只包含三个开源引擎:grpc_ev_epoll1_posix(name 为"epoll1",见 ev_epoll1_linux.cc)、grpc_ev_poll_posix与grpc_ev_none_posix,头部与尾部空位留给通过grpc_register_event_engine_factory()注入的高/低优先级自定义轮询引擎。

引擎的选型结果可在运行期通过环境变量GRPC_POLL_STRATEGY强制指定(逗号分隔、按序尝试)。在 src/core/config/config_vars.cc 中该变量读取逻辑为LoadConfig(FLAGS_grpc_poll_strategy, "GRPC_POLL_STRATEGY", overrides.poll_strategy, "all"),即默认值为"all",表示按注册表顺序挑选第一个"可用"(check_engine_available()返回真)的引擎;每次try_engine()都会打印形如Using polling engine: epoll1的日志。例如强制使用 epoll1 可设置:

export GRPC_POLL_STRATEGY=epoll1

Polling Engine 对外接口:四个不透明结构与完整 API 语义

不同实现内部的数据结构差异很大,因此接口层只暴露**不透明(opaque)**的类型句柄。它们被 iomgr 的其他子系统(如grpc_endpoint、timer、resolver)以统一方式使用。

四个核心不透明结构

  • grpc_fd:封装一个文件描述符的结构,各实现内部定义完全不同(详见下文 epoll1 的实现片段)。
  • grpc_pollset:一组(一个或多个)grpc_fd的集合,统一被轮询以发现可读/可写/错误事件。一个grpc_fd可以同时属于多个grpc_pollset。
  • grpc_pollset_worker:代表一个"轮询线程"——更准确地说,是那个调用了grpc_pollset_work()的线程上下文。
  • grpc_pollset_set:grpc_fd、grpc_pollset以及其他grpc_pollset_set(注意它可以嵌套包含别的grpc_pollset_set)的分组容器。

四个对象的组合关系可参见官方架构示意图:

grpc_fd 的 API

grpc_fd_notify_on_[read|write|error]

  • 签名:grpc_fd_notify_on_XXX(grpc_fd* fd, grpc_closure* closure)
  • 作用:注册一个 closure,当 fd 变为可读/可写/发生错误时被调度执行(gRPC 术语中称之为"arming the fd",即给 fd 上膛/布防)。
  • 语义要点:每个事件只触发一次闭包。fd 一旦可读(或可写/出错),闭包即被触发并执行,同时该 fd 转为"未布防(unarmed)"状态;若想再次收到通知,必须重新布防。

这一"单次触发、需重新布防"的语义保证了事件回调与 fd 生命周期之间的清晰时序。从 ev_epoll1_linux.cc 的实现看,每个grpc_fd内部由三个grpc_core::LockfreeEvent分别承载 read/write/error 三类闭包(read_closure/write_closure/error_closure),保证在无锁读路径下也能安全地"设置一次事件、消费一次通知"。

grpc_fd_shutdown

  • 签名:grpc_fd_shutdown(grpc_fd* fd)
  • 作用:所有当前已注册(以及未来将注册)的可读/可写/错误闭包,都会被立即以错误状态调度执行。这是关闭 fd 时解除所有等待者的统一手段。

grpc_fd_orphan

  • 签名:grpc_fd_orphan(grpc_fd* fd, grpc_closure* on_done, int* release_fd, char* reason)
  • 作用:释放grpc_fd结构本身,操作完成时回调on_done闭包。
  • release_fd == nullptr时:同时close()底层 fd;
  • release_fd非空时:把底层 fd 放入release_fd返回给调用方,并跳过close()。后者用于"底层 fd 并非由 gRPC Core 拥有"的场景,文档明确指出典型例子就是C-Ares DNS 解析器所使用的 fd。

在 ev_posix.cc 的 POSIX 公共分发层中,grpc_fd_orphan、grpc_fd_shutdown、grpc_fd_notify_on_read/write/error等函数都会被原样转发给当前选中的事件引擎 vtable(g_event_engine->fd_orphan(...)等),这正是"所有实现共享同一接口"的落地体现。

grpc_pollset 的 API

grpc_pollset_add_fd

  • 签名:grpc_pollset_add_fd(grpc_pollset* ps, grpc_fd* fd)
  • 作用:把 fd 加入 pollset。
  • 注意:接口中刻意没有grpc_pollset_remove_fd。原因在于调用grpc_fd_orphan()会"顺带"把该 fd 从它所属的所有pollset 中移除,因此单独的解绑操作是不必要的。

grpc_pollset_work

  • 签名:grpc_pollset_work(grpc_pollset* ps, grpc_pollset_worker** worker, grpc_core::Timestamp deadline)
  • 调用前提:调用前必须持有该 pollset 的 mutex。进入函数后会很快填充*worker指针并释放 mutex;当grpc_pollset_work()返回时,*worker指针已失效,不得再使用(官方文档提示可查看 completion queue 相关代码了解典型用法)。
  • 返回条件(满足其一即返回):
    1. 到达 deadline 截止时间;
    2. pollset 中有 fd 被发现可读/可写/出错,且相关闭包已被 'scheduled'(调度)——注意只是"调度"而不要求"已执行";
    3. worker 被"kick"(踢醒)(见下)。

grpc_pollset_kick

  • 签名:grpc_pollset_kick(grpc_pollset* ps, grpc_pollset_worker* worker)
  • 作用:强制指定 worker 从grpc_pollset_work()返回。
  • 若worker == nullptr,则踢醒该 pollset 上任意一个活跃的worker。

grpc_pollset_set 的 API

grpc_pollset_set_[add|del]_fd

  • 签名:grpc_pollset_set_[add|del]_fd(grpc_pollset_set* pss, grpc_fd* fd)
  • 作用:向 pollset_set 增加/移除一个 fd。

grpc_pollset_set_[add|del]_pollset

  • 签名:grpc_pollset_set_[add|del]_pollset(grpc_pollset_set* pss, grpc_pollset* ps)
  • 语义:把一个 pollset 加入 pollset_set 后,在该 pollset 上调用grpc_pollset_work()时,也会一并轮询 pollset_set 内的所有 fd——等价于"把 pollset_set 里的所有 fd 都加进该 pollset";一旦 pollset 被从 pollset_set 中移除,此保证即失效。

grpc_pollset_set_[add|del]_pollset_set

  • 签名:grpc_pollset_set_[add|del]_pollset_set(grpc_pollset_set* bag, grpc_pollset_set* item)
  • 语义:把bagpollset_set 中的所有 fd 都加入itempollset_set,实现分组间的嵌套聚合。这正是 DNS resolver 等需要把异步 fd 共享给多个 pollset 的场景所用的组合手段。

POSIX 层对应分发表为 ev_posix.cc 中的grpc_posix_pollset_vtable与grpc_posix_pollset_set_vtable,它们把pollset_init/work/kick与pollset_set_add/del等操作桥接到当前引擎实现。

实现深潜之一:epoll1 引擎

epoll1 是 Linux 平台默认的核心引擎,实现位于 src/core/lib/iomgr/ev_epoll1_linux.cc(整体仅在有GRPC_LINUX_EPOLL宏时才编译,对应支持epoll_create()/epoll_create1()的 Linux 内核)。该引擎的总体结构如下:

全局单例 epoll 集合与事件分派

epoll1 维护一个进程级全局单例 epoll fd(g_epoll_set),源码注释明确说明其同步模型:该结构中的字段仅由 designated poller(指定轮询者)修改,因此无需加锁;但num_events与cursor字段必须是原子类型,原因在于多个轮询线程轮值 designated poller 时,"写入这些值的线程"与"读取这些值的线程"往往不同,原子类型用于提供内存可见性保证。

#define MAX_EPOLL_EVENTS 100 #define MAX_EPOLL_EVENTS_HANDLED_PER_ITERATION 1

即每次epoll_wait()最多取出 100 个事件、每轮迭代最多处理 1 个事件,由cursor记录当前处理进度。epoll fd 通过epoll_create1(EPOLL_CLOEXEC)(在支持GRPC_LINUX_EPOLL_CREATE1时)或epoll_create()+fcntl(FD_CLOEXEC)创建。

designated poller 与 pollset_neighborhood

该引擎最关键的设计是"每时刻只有一个线程真正阻塞在 epoll_wait 上",即 designated poller(也译作主轮询线程/看门线程)。官方文档指出其选择逻辑相当复杂,核心机制包括:

  • pollset_neighborhood:epoll1 内部把 pollset 分片到若干个"邻域"中。在当前源码里可见MAX_NEIGHBORHOODS 1024的上限,邻域内通过gpr_mu保护一个active_root指针。
  • root_worker 链表:所有在某个 pollset 上调用grpc_pollset_work()的grpc_pollset_worker都被挂进该 pollset 的一条双向链表,链表头部称为 "root worker"。grpc_pollset结构体中正是通过root_worker指针维护这条链。
  • 邻域与 CPU 核数对应:邻域数量与 CPU 核数相关,pollset 依据其 root worker 线程所在的 CPU 核心被放入某个邻域。
  • 下一任 designated poller 的挑选:当需要更换轮值者时,先尝试在当前 pollset 上找其他 worker;若当前 pollset 已无其他 worker,则扫描当前 pollset 所属的 pollset_neighborhood 列表,挑出下一个可成为 designated poller 的 pollset 与 worker。官方文档也坦诚地指出:这块实现尚有调优空间,真正的需求只是"一种按 pollset 分组维护 worker 列表(以支持grpc_pollset_kick语义)、且能随机选出新 designated poller 的方法"。

对应的两个关键函数是:begin_worker()(负责把当前线程接入 pollset 的 worker 链表并竞争成为 designated poller)与end_worker()(由刚退出epoll_wait()的 worker 调用,负责挑选下一任 designated poller)。从 ev_epoll1_linux.cc 的结构体定义可以印证:grpc_pollset_worker带有next/prev链表指针、条件变量gpr_cv,以及用于记录唤醒状态的kick_state(取值UNKICKED、KICKED、DESIGNATED_POLLER,并有专门的宏SET_KICK_STATE在每次变更时记录行号便于调试)。grpc_pollset中还维护了kicked_without_poller、seen_inactive、shutting_down、begin_refs等状态,用来协调"worker 即将加入"与"关停"等竞态。

线程模型:非 designated 的 worker 如何等待

当一个 worker 竞争 designated poller 失败后,它并不会自旋忙等,而是在自己的条件变量上睡眠;designated poller 处理完事件(或需要交接)时会唤醒它们。这与 Windows 引擎"一个线程等在 IOCP、其余线程等条件变量"的思路如出一辙(见下文)。

wakeup_fd:打破 epoll_wait 阻塞的万能钥匙

gRPC 的 alarm(定时器)系统有时需要"唤醒某个 poller"——典型场景是新 alarm 的触发时刻早于当前下一次 epoll 周期。为此 epoll1 维护了一个全局的wakeup fd(g_global_wakeup_fd/grpc_global_wakeup_fd):当需要打断阻塞中的 epoll_wait 时向该 fd 写入一个字节即可。wakeup fd 的具体实现(eventfd、pipe 等)由 wakeup_fd_posix.cc 及相关文件按平台选择。

设计取舍:fd freelist 与 fork 支持

值得注意的实现细节是,epoll1 维护了一个fd freelist(空闲链表)。源码注释解释其动机并非 malloc 性能,而是为了处理"多线程并发 epoll_wait 时,pollset 移除与尚未到达的 poll 通知之间的竞态":poller 最终持有对该对象的引用,若无昂贵的同步手段很难判定何时可安全释放;把对象放入 freelist 后,即使输掉这场竞态,最坏情况也只是"对已复用 fd 产生一次虚假的可读通知"。

此外 epoll1 还通过fork_fd_list全局链表登记所有活跃grpc_fd,供 fork 之后(GRPC_ENABLE_FORK_SUPPORT=1时)重新初始化 epoll 实例使用;grpc_fd内部也有is_pre_allocated标记。关于错误跟踪,epoll1 仅在平台支持 errqueue(内核错误队列)时才对 fd 使能错误追踪(grpc_event_engine_can_track_errors()会检查KernelSupportsErrqueue())。

实现深潜之二:poll 引擎与其他平台实现

poll 引擎(macOS 等无 epoll 平台)

gRPC 的poll轮询引擎实现于 src/core/lib/iomgr/ev_poll_posix.cc,用于 macOS 等没有 epoll 的平台。官方文档特别提醒:

gRPC 的 poll 引擎相当复杂,因为它基于poll()系统调用完成轮询;而poll()是水平触发(level-triggered)的——当你阅读 ev_poll_posix.cc 中看似绕弯的代码时,要记住这一点:水平触发意味着只要 fd 仍处于就绪状态,每次poll()都会再次报告该事件,因此引擎必须自行维护"该事件是否已经被消费"的状态,以避免事件被重复派发。

从 ev_posix.cc 还可看到,grpc_poll_function被设计为可被测试覆盖的函数指针(AIX 平台还有专门的aix_poll包装),说明 poll 引擎的适配与测试都是围绕这一抽象进行的。

Windows 轮询引擎(I/O Completion Port)

官方文档用大量篇幅强调:Windows 的轮询引擎与其他引擎"长得完全不一样":

  • 与 Unix 系(epoll1、poll)不同,Windows 的 endpoint 实现与轮询引擎实现高度耦合;
  • Windows endpoint 的读写 API 基于 Windows I/O API 实现,而这些 API要求必须指定一个 I/O Completion Port(IOCP);
  • 在其grpc_pollset_work()实现中,只挑选一个线程阻塞在 I/O completion port 上等待完成包,其余线程则等待在条件变量上——这与 epoll1 的 turnstile(旋转门)式轮值唤醒结构非常相似。

当前仓库中对应实现文件包括 src/core/lib/iomgr/iocp_windows.cc、src/core/lib/iomgr/pollset_windows.cc、src/core/lib/iomgr/pollset_set_windows.cc 以及 src/core/lib/iomgr/tcp_windows.cc 等,感兴趣的读者可以对照pollset_windows.cc中的 work 循环理解"一线等 IOCP、多线等条件变量"的实现。

总结:一条贯穿三端的统一抽象

纵观全文可以看到,尽管 epoll1、poll、Windows 三套引擎的底层机制南辕北辙,gRPC 通过一张稳定的 vtable 接口(ev_posix.h 与 ev_posix.cc 中的分发层)+ 四个不透明结构,把"监控 fd 事件、调度闭包、不新建线程、兼顾延迟与吞吐"这一核心诉求固化下来:

  • grpc_fd回答"监控谁、发生什么就绪事件";
  • grpc_pollset/grpc_pollset_worker回答"由哪些应用线程、以何种方式阻塞与唤醒"(deadline、事件调度、kick 三条返回路径);
  • grpc_pollset_set回答"如何把跨模块的 fd 共享给多个轮询方"(嵌套组合语义);
  • 而每台具体的机器上,GRPC_POLL_STRATEGY(默认all)决定最终由哪个引擎 vtable 承担这一切。

对 gRPC 做性能诊断或源码级调试时,不妨先确认进程实际选择了哪个引擎(日志中的Using polling engine: xxx),再顺着本文梳理的 API 语义进入 src/core/lib/iomgr/ev_epoll1_linux.cc 或 src/core/lib/iomgr/ev_poll_posix.cc 跟踪grpc_pollset_work()与begin_worker()/end_worker()的调用链——这正是理解 iomgr 事件驱动的关键入口。

【免费下载链接】grpcC++ based gRPC (C++, Python, Ruby, Objective-C, PHP, C#)项目地址: https://gitcode.com/GitHub_Trending/gr/grpc

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询