热点 IO 不停到达,事件循环怎样保住控制时延¶
一个数据通道持续可读,owner 一直处理它。吞吐看起来很好,停止命令和 deadline 却越来越晚。问题已经超出 LT / ET 的选择:事件循环也是一个调度器,需要决定谁先运行、一次运行多久、未完成的工作保存在哪里。
前置阅读:事件、就绪与时间、异步 SDK。本章先分析当前实现,再推导扩展到多通道时需要的调度规则。后面的多源 ready 队列是设计方案,尚未加入公开 SDK。
把执行路径和进展条件分别列出来¶
公开源码 platform/runtime/linux/runtime.c 的 owner 大致按以下顺序运行:
正常入口预算为 64 项,单次 input() 预算为 32 条,数据消费预算为 8 项。socket 使用 LT;只有输出队列非空时才订阅 EPOLLOUT。timerfd 目前按 1 ms 周期唤醒,callback 同步运行在 owner 中。
这些数字来自代码,不表示已经满足某个产品的控制时延。预算限制了数量,但每项成本、同一 batch 中的处理顺序、callback 阻塞和 owner 的调度等待仍会延长一轮。
先区分三个进展条件:
| 条件 | 必须保存的状态 | 仅靠什么不能证明 |
|---|---|---|
| 数据最终会继续处理 | 内核 LT 条件或应用 ready 状态 | 收到过一次通知 |
| 控制工作获得机会 | 有界批次、独立入口与轮转规则 | 数据线程一直有吞吐 |
| 停止义务全部完成 | admission 已关闭、有限剩余集合、结果账 | 普通循环执行了一轮 |
数量预算怎样变成时间预算¶
假设一轮中最多处理 B_i 个第 i 类工作,每项包含回调的最坏成本为 C_i,其它固定成本为 C_fixed。在工作成本确实有界的前提下:
这是边界推导,不是实测保证。若 callback 能等待网络或无限持锁,C_i 没有已知上界,这个式子也就不能证明有界时延。
用一个规划算例:接收预算 32,每条同步回调最多 100 μs,仅回调就可占用 3.2 ms。把预算降到 8 能缩短一个接收批次,却增加循环、系统调用和快照发布的次数。若业务要求 2 ms 内观察 STOP,除了调小批次,还要验证其它阶段、调度等待以及当前正在执行的回调。
时间预算与条数预算可以同时使用:达到任一上限就让出当前源。但非抢占回调一旦开始,时间预算不能在中途强制撤回它。需要将重工作转交独立消费者,并为那一层另设队列和退出责任。
ET 的预算必须配 ready 队列¶
ET 源还可读,但本轮预算已经用完。如果此时直接回到 epoll_wait,可能没有新的边沿让它继续执行。Linux 手册因此要求应用处理非阻塞 IO,并保留没有耗尽的就绪工作。epoll 手册
多源设计可以维护一个应用 ready 队列。以下是调度伪代码,并非当前实现:
收到 epoll batch:
根据 slot / generation 验证源身份
未在 ready 队列里的有效源入队
每一轮:
先检查停止与到期请求
从 ready 队列头取一个源
执行至 EAGAIN、EOF、错误,或条数 / 字节 / 时间预算耗尽
EAGAIN:等待下一次内核就绪
预算耗尽且尚有工作:放到 ready 队列尾
队列非空:下一次 epoll_wait 使用零等待,继续接收其它源的事件
队列为空:按最近 deadline 安排阻塞等待
入队去重防止一个热点源占满 ready 队列。轮转规则防止同一源连续消耗多个批次;字节预算防止大报文绕过条数预算。错误和 EOF 必须撤销源,并处理已经取出的旧 batch。slot 的 generation 解决身份复用,引用或 owner 独占规则解决对象仍能访问的问题。
EPOLLONESHOT 可以把重新启用事件的责任交给应用,但它不自动实现轮转、引用管理或公平性。选用它以后,还必须规定谁在什么状态下 rearm。
唤醒与排队状态不能脱节¶
owner 读取 eventfd 后,计数可能清零,而入口中仍有超出本轮预算的工作。单纯继续睡眠,会把正常预算变成遗漏工作。
当前 SDK 有周期 timerfd,预算后残留的入口工作还会有后续周期唤醒机会;这也是它目前的进展路径之一。该路径不意味着 1 ms 内一定得到 CPU。若以后改为按最近 deadline 设置一次性 timer,必须同时补上入口未空时的继续调度规则。
两种方案各有代价:
| 方案 | 收益 | 要验证的成本 |
|---|---|---|
| 周期 tick | 实现简单,定期复查工作 | 空闲唤醒、调度开销、功耗;仍可能晚处理 |
| 最近 deadline + 本地 ready 状态 | 空闲时减少唤醒 | 定时器重设、取消、遗漏唤醒与状态一致性 |
改定时策略属于事件循环行为变化,需要重新跑 deadline、取消、停止和过载回归,不能只比较空闲 CPU。
背压怎样穿过每一层¶
把一条输入路径拆成 API 入口、accepted 槽、输出队列、后端队列和 callback 消费。逐层记录容量、单位、满时返回,以及下一层慢下来时如何向上游传播。
控制请求通常需要明确拒绝或完成;持续数据流可能允许丢弃,但应说明丢旧还是丢新、是否保留序号和丢弃计数。把控制和数据放在一个 FIFO 中,会让控制延迟直接受数据积压影响。单独入口也还需要调度预算,不能只靠分队列解决饥饿。
公开 SDK 能报告 pause_data 不支持。因而本地减少消费不等于远端停止生产:背压到达协议能力边界以后,必须选择丢弃、限流或拒绝,不能假设所有层都能停下来。
设计验证与现有证据¶
已有 labs/extra.c:fairness() 核对一个热点源让出后控制数据仍被处理,runtime_test.c 核对停止守恒。它们是局部功能证据,尚未覆盖上面的多源调度器或最坏控制时延。
进一步验证时固定版本与负载,分别改变一个因素:
| 对照 | 要记录的指标 | 能区分的问题 |
|---|---|---|
| 单热点 / 多热点 | 各源处理量、最长轮转间隔、STOP 观察延迟 | 源间饥饿 |
| 快 callback / 有界慢 callback | owner 一轮耗时、入口水位、terminal 交付延迟 | 同步交付阻塞 |
| LT / ET + ready 队列 | 残留工作、空闲等待、CPU、完整性 | 遗漏工作与忙循环 |
| 周期 / deadline timer | 空闲唤醒、超时偏差、停止时间 | 功耗与进展的交换 |
这些对照尚未执行,不给出通过结论。评审一个 reactor,应能解释工作如何继续、控制怎样获得机会、何时拒绝,以及关闭以后仍欠什么结果。容量的具体推导见 突发负载与 admission。