后台工作边界
This content is not available in your language yet.
Valkey 有多种“后台工作”,但它们的安全边界完全不同。IO worker 处理网络字节,BIO 处理阻塞或释放任务,fork child 处理进程级快照;把三者混为一谈,会导致错误的性能和一致性判断。
先给答案:后台任务的关键是“谁拥有状态”和“完成后如何回到主线程”
Section titled “先给答案:后台任务的关键是“谁拥有状态”和“完成后如何回到主线程””Valkey 的后台执行者不是一支可以随便跑命令的线程池。BIO 适合关闭文件、释放资源等阻塞操作;IO worker 负责网络搬运;fork 子进程则通过进程隔离执行快照等任务。它们的共同点是把耗时工作移走,差异在于是否共享地址空间、是否需要访问 Redis 核心状态以及结果如何汇合。
所以异步化的完整链路必须包含投递、执行、完成通知和失败处理。只把工作放入队列而不说明谁消费、谁回收、主线程何时观察结果,不能算讲清后台任务。
IO worker socket read/write, poll, acceptBIO thread close/fsync/lazy free 等后台任务fork child RDB/AOF/Module 的进程级后台工作signal callback 主线程可安全处理的轻量事件通知BIO 的队列模型
Section titled “BIO 的队列模型”src/bio.c:155 初始化 BIO 线程和队列,src/bio.c:179 提交任务,src/bio.c:241 执行任务。提交者只负责把任务放入对应队列并唤醒 worker;真正执行时仍要按任务类型选择关闭文件、同步文件或释放内存等动作。
caller -> bioCreateBackgroundJob -> type-specific queue -> condition variable wakeup -> BIO thread executesIO worker 与 BIO 的差别
Section titled “IO worker 与 BIO 的差别”IO worker 的任务通常绑定 client/socket,完成后要回到 IO 事件流;BIO 任务则是“提交后最终完成”,不参与命令语义,也不应直接修改主线程拥有的数据结构。两者使用不同队列和生命周期,是因为完成通知与所有权要求不同。
fork 的差别
Section titled “fork 的差别”fork child 继承地址空间的逻辑视图,但通过写时复制隔离后续修改。它可以把 RDB/AOF 重写从主线程移走,却不能消除内存峰值:父进程继续写热数据时会触发页面复制。线程共享地址空间,进程 fork 则共享初始页面,这个差异决定了排查方法完全不同。
signal callback 的边界
Section titled “signal callback 的边界”信号回调只能做极少的异步信号安全操作,复杂逻辑应转化为主循环下一次可处理的状态。不要在信号上下文直接调用分配器、锁、复杂日志或命令执行路径。
为什么不让 BIO 直接改数据
Section titled “为什么不让 BIO 直接改数据”后台释放对象若与主线程 mutation 并行,就必须证明对象不可达、引用计数和模块析构都安全。Valkey 将“决定释放什么”留在主线程,把“执行可能阻塞的释放”交给 BIO,形成决策与执行分离。
- BIO 队列延迟会使文件关闭、lazy free 的完成时间晚于命令返回。
- fork 重写期间监控 RSS,不能只看逻辑数据集大小。
- IO worker 不应执行需要访问共享数据结构的命令逻辑;出现这种设计通常意味着所有权边界被破坏。
后台任务设计至少要回答三件事:谁决定任务、谁执行任务、谁确认完成。把这三者写在接口和队列拓扑里,比用一个“异步线程池”名称更能防止数据竞态。
面试锚点:BIO 为什么不能直接执行命令?fork 为什么会增加内存?IO worker 与 BIO 的完成语义有何不同?