folly/io/async 详解Facebook 基于 libevent 的 C 异步 I/O 事件框架【免费下载链接】follyAn open-source C library developed and used at Facebook.项目地址: https://gitcode.com/GitHub_Trending/fol/follyfolly/io/async 是 Folly 中面向异步网络编程的核心模块它为 libevent 提供了一整套 C 对象封装EventBase事件循环、EventHandler文件描述符回调、AsyncSocket/AsyncServerSocket等常见 fd 使用场景的实现以及跨线程消息传递、定时器和生命周期安全工具。读完本文你将掌握 EventBase 线程模型与消息投递机制、EventHandler 事件类型及取舍、AsyncSocket 非阻塞读写模式以及高并发服务在事件线程划分上的工程实践。模块定位libevent 之上的 C 对象封装folly/io/async 官方文档自述为 “An object-oriented wrapper around libevent”。libevent 是一个优秀的跨平台事件库而 Folly 的 async 模块在其之上提供了针对文件描述符fd回调与 event_base 的 C 对象封装针对多种常见 fd 用途TCP socket、UDP socket、server socket、pipe、signal、timeout的现成实现与 Folly 生态IOBuf、RequestContext、Futures、executor的无缝衔接。在源码层面模块的核心头文件与实现都位于 folly/io/async 目录例如 EventBase.h、EventHandler.h、AsyncSocket.h 等并有 test 目录下的大量测试用例支撑。需要注意的是EventBase 内部封装了底层事件后端EventBaseBackendBase.h在 Linux 上默认使用 epoll并可通过 Options 注入自定义后端如 IoUringBackend.h 的 io_uring 后端。EventBase每线程一个的主事件循环EventBase是主要的 libevent/epoll 事件循环封装见 EventBase.h 的类注释。它的基本模型是一个 EventBase 属于一个线程该线程一旦启动循环除了 fd 回调之外不再做其他事情。要利用多核异步服务器通常采用每 CPU 一线程、每线程一个 EventBase的结构。最基础的启动方式EventBase base; auto thread std::thread([](){ base.loopForever(); });跨线程消息传递runInEventBaseThread()EventBase 内建了线程间消息传递能力。要把一个函数投递到 EventBase 线程中执行使用runInEventBaseThread()线程安全可从任意线程调用EventBase base; auto thread1 std::thread([](){ base.loopForever(); }); base.runInEventBaseThread([](){ printf(This will be printed in thread1\n); });从源码看EventBase.hrunInEventBaseThread()的语义包括若调用时循环未在运行函数会延迟到下一次循环启动时执行若循环被终止且不再重启函数会在 EventBase 析构时执行同一线程连续两次调用函数按调度顺序执行不同线程之间的顺序不做保证回调不得抛出异常。EventBase 同时实现了 folly 的Executor接口add()直接调用runInEventBaseThread()、DrivableExecutor、ScheduledExecutor、IOExecutor等EventBase.h因此可以直接作为 executor 传递给基于 folly futures 的代码。EventBase::add()的实现即runInEventBaseThread(std::move(fn))。循环的多种运行方式EventBase 提供了多种驱动循环的方式EventBase.h方法行为loop()当没有更多注册事件时返回所有事件处理完毕或调用terminateLoopSoon()后退出loopForever()与loop()类似但即使没有用户注册的 EventHandler/AsyncTimeout 也持续运行直到terminateLoopSoon()适合等待其他线程投递消息loopOnce(flags)只调用一次 epoll()处理一批事件后返回EVLOOP_NONBLOCK时无事件也不阻塞loopPoll()反复轮询的循环方式配合loopPollSetup()/loopPollCleanup()使用每次迭代把控制权交还给调用线程loopWithSuspension()支持挂起状态的循环依赖支持 pollable fd 的后端允许外部通过 fd 等待就绪事件后恢复循环loop()与loopForever()在返回后都可以再次调用以重启事件处理。loop()正常结束处理完所有事件或收到terminateLoopSoon()返回 true等待事件出错返回 false。延迟执行与超载监控runAfterDelay()/runInLoop()runInLoop(LoopCallback*)在当前循环迭代完成后运行回调适合事件处理器在返回后安排后续动作只能在 EventBase 线程内调用EventBase.h。setMaxLatency(maxLatency, callback)当循环运行过慢即事件过多、应把部分代码迁移到其他线程时触发回调。注意该功能依赖时间测量enableTimeMeasurement_assert(enableTimeMeasurement_)强制了这一前提EventBase.h。setLoadAvgMsec()、getAvgLoopTime()基于指数平滑的循环负载与平均循环时间统计SmoothLoopTime类EventBase.h。setObserver(EventBaseObserver)通过EventBaseObserver::loopSample(busyTime, idleTime)周期采样循环的忙/闲时间。时间测量、定时器粒度与循环配置EventBase 的构造函数有多个重载EventBase.hEventBase()等价于EventBase(true)默认开启时间测量EventBase(bool enableTimeMeasurement)关闭时间测量可提升性能但会禁用依赖它的 observer、max latency、平均循环时间等特性EventBase(std::chrono::milliseconds tickInterval)额外指定 HHWheelTimer 的 tick 粒度EventBase(event_base* evb)接管外部传入的 libevent event_base析构时event_base_freeEventBase(Options options)通过 Options 全面配置。EventBase::OptionsEventBase.h包含三个实用配置项skipTimeMeasurement跳过循环耗时统计默认 falsebackendFactory自定义事件后端工厂可替换默认的 epoll 后端timerTickInterval事件循环内置 HHWheelTimer 的 tick 间隔默认HHWheelTimer::DEFAULT_TICK_INTERVALloopCallbacksTimeslice非零时每轮迭代中处理 loop callback 与通知队列回调的时间片上限防止队列饿死事件处理默认 0 表示不限制。回调内联执行的影响EventBase总是内联执行所有回调——不存在显式或隐式的排队。这带来三个关键影响README 原文要点尾延迟P99远优于任何排队实现——这是 EventBase 设计上最核心的性能优势EventHandler 实现必须保证单个回调不占用过长时间本模块内所有 EventHandler 都做到了这一点如果你直接继承 EventHandler 写回调务必牢记回调内部不能直接 delete EventBase 或 EventHandler——因为对象仍位于调用栈上必须使用下文介绍的DelayedDestruction配合 shared_ptr 管理生命周期。EventHandlerfd 的事件回调封装EventHandler是 fd 的对象封装EventHandler.h。任何需要接收 fd 回调的类都应继承EventHandler并实现handlerReady(uint16_t events)然后通过registerHandler(EventType)注册感兴趣的事件类型。构造方式EventHandler.hEventHandler(EventBase* eventBase, NetworkSocket fd)两个参数都可缺省分别用attachEventBase()/changeHandlerFD()或initHandler()在注册前补设。支持的事件类型EventHandler::EventFlags枚举EventHandler.h与 libevent 标志一一对应标志含义READ读就绪与 EOF 事件WRITE写事件内核写缓冲区为空时触发READ_WRITE两者兼有PERSIST事件在handlerReady()触发后仍保持注册否则默认触发一次后注销registerHandler()在已注册时会更新监听的事件集合失败后 handler 一定处于注销状态EventHandler.h。此外还有registerInternalHandler()注册内部事件它不计入决定循环是否退出的活跃事件因此不会阻止loop()在无其他工作时退出EventHandler.h。不支持的 libevent 事件类型及原因README 明确解释了以下 libevent 事件类型为何不被支持TIMEOUT本库把超时支持从 fd 读写中分离出来有专门的超时体系见下文 AsyncTimeout / HHWheelTimerSIGNAL信号单独处理见 AsyncSignalHandlerEV_ET边缘触发目前所有 EventHandler 实现均按水平触发level-triggered设计基准测试未显示边缘触发有明显收益。README 还给出了深层理由水平触发下只要仍有可读/可写数据每次event_wait事件都会持续触发而边缘触发只在事件变为活跃时回调一次需要库自己维护活跃 fd 列表增加额外复杂度。边缘触发唯一的好处是可用EPOLLONESHOT保证事件只在单个 event_base 上触发——而本库本身就假设每个事件只注册在单个线程上因此无此必要EV_FINALIZEEventBase 只能单线程使用少数方法除外。若要在其他线程安全地注销事件必须经由runInEventBaseThread()完成线程切换——多数 API 已自动完成这一切换或在错误线程调用时CHECK()报错EV_CLOSED这是一个优化方向——不用把数据全部读完再收到 EOF而是在数据读尽之前就触发 EV_CLOSED。README 标注为 TODO推测只在请求/响应型服务器中比较有用。EventHandler 的具体实现AsyncSocket非阻塞 TCP socketAsyncSocketAsyncSocket.h是非阻塞 socket 实现写操作被排队并以异步方式写出甚至在connect()成功之前也可以排队。读 API 由两个方法构成getReadBuffer()libevent 无法预知内核读缓冲区有多少数据可读。虽然 Linux 上可以额外发一个 syscall 获取大小但 syscall 很慢。因此通常做法是提供一个固定大小缓冲区——建议使用 folly/io 的IOBufQueuereadDataAvailable()报告实际读到了多少数据与getReadBuffer()配对使用。从源码看读回调通过setReadCB(ReadCallback*)设置AsyncSocket.h回调上下文中的文档明确说明这两个方法成对出现AsyncSocket.h。AsyncSocket 提供发送超时但不提供读超时——因为读超时通常与应用场景强相关应使用下文的 AsyncTimer 实现。使用要点README 原文建议使用 IOBuf 链调用writeChain()AsyncSocket.h是一种非常省 syscall 的发送方式——可以在不产生多余拷贝的情况下追加/修改待发送数据setMaxReadsPerEvent()AsyncSocket.h防止 AsyncSocket 在单次事件中阻塞事件循环过久不要在 AsyncSocket 使用期间自行对该 fd 发 syscall应使用它提供的封装如close()、shutdown()等。AsyncSSLSocket与 AsyncSocket 类似但基于 OpenSSLAsyncSSLSocket.h额外提供HandshakeCallback用于校验服务器证书。其工厂方法newSocket()有多个重载AsyncSSLSocket.h用于从 EventBase、已有 fd 或 AsyncSocket 派生。AsyncUDPSocket读写 UDP 数据包的 socket。由于 UDP 几乎无状态可维护它比 AsyncSocket 简单得多AsyncUDPSocket.h。README 中提及的 TAsyncUDPSocket 当时位于 fbthrift 代码库中属历史遗留说明。AsyncServerSocketaccept() 分发器AsyncServerSocketAsyncServerSocket.h是 listen() 的 socketaccept() 出 fd 后分发给其他 event base。标准用法模式EventBase base; auto socket AsyncServerSocket::newSocket(base); socket-bind(port); // 0 表示选择任意空闲端口 socket-addAcceptCallback(object, base); // object 实现 accept 回调base 是它的 event base // 内部会调用 base::runInEventBaseThread() 投递消息 socket-listen(backlog); socket-startAccepting();对应源码newSocket()AsyncServerSocket.h、addAcceptCallback()AsyncServerSocket.h、startAccepting()AsyncServerSocket.h。startAccepting()之前必须先bind()和listen()回调为空时调用startAccepting()会延迟到回调添加后自动开始。设计要点通常有一个 accept() 线程和多个AcceptCallback对象Acceptee 对象再各自管理单独的 AsyncSocketAsyncSocket可以在 event base 之间迁移但大多数用户将其绑定到单一 event base以获得更好的缓存局部性并避免加锁多个 ServerSocket 可以共存但 Linux 内核在 accept() 端口上有锁会限制约 2 万次/秒的 accept 速率可尝试 SO_REUSEPORT 等方案绕过但更推荐客户端使用连接池由于 AsyncServerSocket 提供的是 fdAsyncSSLSocket 或 AsyncSocket 可以走完全相同的代码路径从它构造。NotificationQueue进程内线程间消息队列NotificationQueue用于同一进程内的线程间消息传递正是EventBase::runInEventBaseThread()的底层实现——因此一般不需要直接使用它直接调runInEventBaseThread()即可NotificationQueue.h。实现原理README 详解一个 eventfd内核 2.6.30或 pipe更老的内核被加入 EventBase 循环用于唤醒接收消息的线程队列本身是一个 spinlock 保护的链表。由于几乎总是单发送者 单接收者虽然代码也支持多生产者/多消费者spinlock 几乎不被竞争实践中没有性能问题只有线程不在唤醒状态时才通知 eventfd/pipe避免 syscall。朴素实现每入队一条消息就 write 一次或更糟把整条消息写进队列会显著更慢如果需要跨进程发消息则必须把整条消息写入 pipe 并管理 pipe 容量参见 AsyncPipe。AsyncTimeout 与 HHWheelTimer定时器体系AsyncTimeoutAsyncTimeout.h是安装进事件循环的单个超时回调。出于代码整洁与清晰考虑超时与 socket 分离——每个 AsyncTimeout 使用一个 fd这是一个比较严重的限制因此下面两个子类被设计为用单个 fd 支撑多个超时。HHWheelTimer哈希分层轮盘定时器HHWheelTimerHHWheelTimer.h实现了哈希分层轮盘定时器hashed hierarchical wheel timer任意超时时间都可用插入、删除、回调均为 O(1)代价是轮盘本身占用一定空间且轮盘必须恒定 tick消耗恒定的 CPU。README 对比了另一种方案——按超时时间排序的回调堆但插入/删除会变成 O(log n)。实际服务器通常有几千到几十万个并发连接常见场景是超时添加/删除但从未触发前提是服务器能跟上负载此时 O(log n) 的插入开销反而超过了轮盘 tick 的额外 CPU 消耗。EventBase 内置的timer()即HHWheelTimerEventBase.h其 tick 粒度可通过构造参数配置。TAsyncTimeoutSet同超时时间优化README 说明该实现当时位于 proxygen 代码库。思路是若假设所有超时都使用相同的超时时间则可保持 O(1) 插入——把新超时挂在链表尾部并记录实际加入时间当前超时触发时根据新的链表头与当前时间的差再安排 AsyncTimeout 触发。代价是要求所有超时时间相同实践中意味着每个应用需要多个 AsyncTimeoutSet。改用单个 HHWheelTimer每线程一个可以显著简化代码。AsyncSignalHandler 与 AsyncPipeAsyncSignalHandlerAsyncSignalHandler.h处理异步信号。与 AsyncTimeout 类似为了代码清晰不把接收信号的 fd 与 socket 复用AsyncPipeAsyncPipe.h对 unix pipe 的异步读写用于进程间传输数据。Helper 类跨线程与生命周期安全RequestContext跨线程的上下文传递由于消息经常通过runInEventBaseThread()在线程间传递ThreadLocal对这类消息不适用。取而代之的是RequestContextRequest.h它在线程间保存/恢复上下文。两个主要用途NUMA保存代码运行所在的 numa 节点在其他线程池/event base 上显式地继续运行在同一节点追踪dapper 风格的进程内请求追踪以及线程之间的追踪。在本库中只有runInEventBaseThread()会保存/恢复请求上下文Facebook 其他跨线程传请求的库folly::future、fbthrift::ThreadManager 等也会这样做。EventBase 还通过RequestEventBaseEventBase.h把当前 EventBase挂进 RequestContext方便在任意线程定位发起请求的 EventBase。DelayedDestruction回调安全的延迟析构由于 EventBase 回调执行时 EventHandler 和 EventBase 都在调用栈上直接delete它们几乎必然导致段错误。因此这些对象继承自DelayedDestructionDelayedDestruction.h通过回调内的引用计数实现安全析构不用delete改调destroy()通知可以销毁了每个回调中都有一个DestructorGuard阻止销毁直到所有 Guard 离开调用栈届时才真正执行 delete。README 坦言DelayedDestruction用起来有些痛苦——shared_ptr/unique_ptr 需要特殊的 DelayedDestruction 析构类型而且很容易忘记在调用回调的代码里加DestructorGuard。但相比排队回调它能换来更优的 P99 延迟因此值得。DestructorCheck轻量的析构探测对于向其他组件定时器、socket connect 等请求回调的对象存在请求者在收到回调前已被释放的风险。如果让对象常驻到所有回调触发完成本太高或请求者无法拥有私有析构对象在栈上、或作为更大对象的成员可用DestructorCheckDestructorCheck.hDestructorCheck不影响对象生命周期只帮助其他组件安全地探测被跟踪对象是否已释放请求回调的对象必须继承自DestructorCheck回调上下文应包含一个DestructorCheck::Safety对象用请求者的引用初始化Safety 可被值捕获进 lambda或显式加入预定义的回调上下文类同一对象可实例化多个 Safety回调触发后、解引用请求者之前应检查对应 Safety 的destroyed()方法是否返回 false。EventBaseManager线程局部 EventBase 管理慎用EventBaseManagerEventBaseManager.h把每线程一个 EventBase做成 threadlocal 管理README 直接标注DANGEROUSEventBaseManager::get()-getEventBase()返回的 EventBase 可能并未在运行线程里可能有不止一个 event base虽然少见或代码中的 EventBase 未注册进 EventBaseManager不同 EventBase 可能用途不同有些是 AsyncSocket 线程、有些是 AsyncServerSocket 线程——不能抓取 EventBase 列表后对全部调用runInEventBaseThread()并指望行为正确。更安全的做法是显式传递 EventBase或使用显式的 EventBase 池例如模块内的 ScopedEventBaseThread.h 与 EventBaseThread.h 提供了便捷的 EventBase 线程管理。SSLContextSSL 辅助例程用于加载/校验证书SSLContext.h与 AsyncSSLSocket 配合使用。另有密码文件读取工具 PasswordInFile.h 等配套设施。通用多线程建议Facebook 的服务实践README 最后给出了 Facebook 运行大规模服务积累的工程实践背景资料可参考 The C10k problem 与 Fast UNIX servers每个 EventBase 线程只承担单一职责更容易维持延迟预期要么是 AsyncServerSocket要么是入站 AsyncSocket要么在代理中是出站 AsyncSocket 调用。理想情况下每核一线程、每线程一个 EventBase即可但实现者必须极其勤勉地把所有 CPU 工作移出 IO 线程防止 fd 的读/写/关闭变慢任何 CPU 密集型工作都应卸载到 CPU 绑定的线程池而不是在 EventBase 线程内执行。runInEventBaseThread()非常快——在 spinlock 成为瓶颈之前每秒可调用数百万次——所以把请求转交给其他线程在性能上通常是划算的与上述两点相对如果总线程数超过核数上下文切换开销会成为问题。实践中曾遇到 CPU 密集线程阻塞 IO 线程调度的情况可用 Linux 的perf sched工具观测异步系统中负载管理极其困难与同步系统相反——最好用带外方式通知过载如超时、CPU 使用率。同步系统几乎总是受线程数限制。详见 No Time for Asynchrony。小结folly/io/async 的核心设计可以浓缩为几点单线程事件循环每线程一个 EventBase回调一律内联执行以 P99 尾延迟换取队列复杂度fd 与超时分离EventHandler 管 fd、AsyncTimeout/HHWheelTimer 管超时各自职责单一跨线程只投递消息通过 eventfd/pipe spinlock 队列实现runInEventBaseThread()跨进程则用 AsyncPipe生命周期安全第一DelayedDestruction 防段错误、DestructorCheck 防悬垂引用、RequestContext 保跨线程上下文工程纪律IO 线程不做 CPU 工作、每线程单一职责、线程数不超过核数。这些实现与约定在 folly/io/async 目录的源码与 test 测试中均可逐一对证是理解 Facebook 大规模异步服务基础设施的绝佳入口。【免费下载链接】follyAn open-source C library developed and used at Facebook.项目地址: https://gitcode.com/GitHub_Trending/fol/folly创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?