架构与 epoll1/poll/Windows 多实现原理)
深入解析 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**的持续监控。iomgrI/O Manager的设计中轮询引擎Polling Engine这一组件被单独抽象出来主要出于两点原因gRPC 代码需要监控大量 fd 上的三类事件fd 可读readable、可写writable、出错errorgRPC 代码知道这些事件发生时应执行的动作例如grpc_endpoint相关代码在 fd 可读时调用recvmsg在 fd 可写时调用sendmsgtcp_client连接代码发起异步connect直到 fd 变为可写即连接真正完成时才完成 client 的创建。因此 gRPC 需要一个能高效完成上述监听与派发的组件。文档强调两个关键约束使用应用提供的线程不创建任何新线程所谓高效是指面向**延迟latency与吞吐throughput**的优化。换句话说gRPC 的异步 RPC 是在用户工作线程如调用grpc_completion_queue消费事件的线程上驱动完成的轮询引擎必须把 epoll_wait/poll/IOCP 等底层等待机制借用这些线程来完成事件分发。不同平台上的实现一览幸运的是无论底层使用何种 OS 机制所有轮询引擎都暴露相同的接口。官方文档列出的实现有平台实现名称选择条件Linuxepoll1glibc 版本 2.9即内核/glibc 支持 epollLinuxpoll内核不支持 epoll 时回退使用macOSOS Xpoll默认实现Windows无特定名称基于 I/O Completion Port 的专用实现说明文档撰写于 2018 年作者 Sree Kuchibhotla。从当前仓库源码看POSIX 平台事件引擎的选择与注册仍位于 src/core/lib/iomgr/ev_posix.cc其中静态注册表g_vtables只包含三个开源引擎grpc_ev_epoll1_posixname 为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_STRATEGYepoll1Polling 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_setgrpc_fd、grpc_pollset以及其他grpc_pollset_set注意它可以嵌套包含别的grpc_pollset_set的分组容器。四个对象的组合关系可参见官方架构示意图grpc_fd 的 APIgrpc_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()底层 fdrelease_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等函数都会被原样转发给当前选中的事件引擎 vtableg_event_engine-fd_orphan(...)等这正是所有实现共享同一接口的落地体现。grpc_pollset 的 APIgrpc_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 相关代码了解典型用法。返回条件满足其一即返回到达 deadline 截止时间pollset 中有 fd 被发现可读/可写/出错且相关闭包已被 scheduled调度——注意只是调度而不要求已执行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 的 APIgrpc_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 fdg_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_neighborhoodepoll1 内部把 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 fdg_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_SUPPORT1时重新初始化 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 PortIOCP在其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),仅供参考