Worker 会话表如何支持配置面查询

1. 背景

  在 DPDK 多核架构中,每个 Worker(数据面线程)维护自己的本地会话表,报文处理不跨核。用户查询时,需要把这些分散的会话汇总起来。例如,按 IP、端口查找连接,或者查看当前设备上的全部会话。

  最直接的做法是把查询命令发给各个 Worker,由它们扫描本地会话表、复制结果,再交给配置面汇总。会话少时,这样做比较简单;到了百万、千万条会话,扫描和复制就会占用大量报文处理时间。分批执行能缩短一次查询任务连续占用的时间,但该扫描的会话、该复制的数据以及总时间并没有减少。

  这里考虑把扫描和复制交给配置面。Worker 仍然维护本地会话,只在创建时额外传递一次对象指针。配置面保存这些指针,查询时直接遍历,不再让 Worker 执行查询任务。

2. 传递会话指针

Worker 会话查询架构

  每个 Worker 与配置面线程之间建立一条独立的单生产者、单消费者(SPSC)无锁队列。Worker 批量发送会话指针,配置面接收后,按会话过期时间将指针放入时间轮。双方访问同一个会话对象,通过引用计数管理它的生命周期。

  创建会话时,Worker 完成对象初始化,将其加入本地会话表,并持有一个引用。准备发送指针前,再增加一个引用,供队列传递和配置面使用。配置面取出指针后接管这份引用,不需要再加一次。即使 Worker 在指针出队之前删除了会话,对象也不会被提前回收。

  更新会话时,Worker 修改原有对象,不重复发送指针。续期也只更新对象中的过期时间,配置面扫描时间轮时再按最新时间调整槽位。这里省掉了更新通知。

  删除会话时,Worker 将会话从本地表中移除,释放自身引用。此时会话已经不再参与正常报文处理,但只要队列或配置面还持有引用,对象内存就继续保留。Worker 不需要等待查询结束,也不单独发送删除通知。

3. 配置面查询

  配置面轮询各 Worker 的队列,批量取出会话指针,按过期时间放入时间轮。如果取出时已经到期,就放入下一次待扫描槽位,避免落到已经处理过的槽位。指针在槽位之间移动时,仍然使用同一份配置面引用,不改变引用计数。

  收到查询请求后,配置面遍历时间轮中的会话,依次检查:

  1. 查询字段是否匹配,不匹配就跳过。
  2. 过期时间是否大于当前时间,已经到期的会话跳过。
  3. Worker 是否仍持有引用,已经被 Worker 删除的会话跳过。

  第三项判断依赖一个前提:对象只有 Worker 和队列或配置面这两份引用。配置面持有对象时,引用计数大于 1,表示 Worker 还没有释放引用;等于 1,表示只剩配置面持有,对象虽然还在,但不再作为有效会话返回。如果以后增加其他引用持有者,这个判断方法也要调整。引用计数为 0 则属于生命周期管理错误,不能当作普通无效会话继续访问。

  检查通过后,将需要展示的字段复制到结果缓冲区。会话数量较多时,查询分批进行,每批限制扫描数量、执行时间和返回结果数量,达到任一限制就保存进度,后续继续。扫描数量要包含没有匹配的会话,不能只限制结果条数,否则一次没有命中的查询仍可能扫完整张表。

  时间轮按过期时间组织对象,没有为 IP、端口等查询条件建立索引,因此条件查询仍然需要遍历。这里改变的是谁来扫描,没有减少扫描本身的工作量。

4. 清理与回收

  配置面每秒处理当前到期槽位,重新读取会话的过期时间和引用计数。时间轮中的位置只是一次检查安排,不能仅凭“到了这个槽位”就释放对象:Worker 可能已经续期,也可能还没有释放自己的引用。具体处理如下:

时间轮扫描与清理

  Worker 删除会话和对象最终回收是两个步骤。前者将会话移出本地表并释放 Worker 引用;后者由配置面在清理条目时释放最后一份引用。会话从创建到回收的引用变化如下:

会话生命周期与引用变化

  这种做法允许配置面在 Worker 删除会话后继续持有对象,但也带来了延迟回收。会话可能已经退出报文处理,内存却还留在队列或时间轮中,直到配置面处理到它。会话创建和删除很快、配置面消费较慢时,这部分内存不能忽略。

5. 实现注意事项

  先持有引用,再发送指针。 引用计数需要使用原子操作,对象由最后一个释放引用的线程回收。发布前完成初始化,队列的同步也要保证配置面取出指针后能看到初始化结果。只把指针传过去,并不能自动保证这些条件成立。

  队列满时要有处理办法。 可以根据会话创建速度和配置面的消费情况设置容量,但容量再大也可能用完。发送失败后,是保留待发送引用稍后重试,还是回退并释放引用,需要有明确约定。不能泄漏引用,也不能让 Worker 一直等队列腾出空间;如果放弃发送,还要考虑配置面会漏掉这条会话。

  清理任务也要分批。 某个时间槽位可能集中存放大量会话,不能让一次清理占满配置面线程。扫描未完成时要保留进度,延迟期间到期的槽位也要补处理,避免查询和清理互相拖住。

6. 问题

基于当前设计,还有以下两个问题需要关注:

  全量会话清理。 用户清空全部会话时,Worker 需要删除本地记录,配置面也需要识别这些失效对象并释放引用。仅靠原有时间轮逐步扫描,可能让大量已删除对象继续占用内存。如何让全量清理尽快完成,同时避免集中删除和回收长时间占用 Worker,需要兼容。

  可变字段的一致性。 引用计数只能保证对象还活着,不能保证配置面读到的是同一时刻的数据。如果 Worker 正在修改用于条件匹配或结果展示的字段,配置面可能先用旧值完成筛选,再复制到新值,或者读到多个字段更新到一半的组合。哪些字段创建后不变,哪些字段允许独立读取,哪些字段必须一起取得一致结果,需要先区分清楚,再决定查询和复制时的同步方式。

留言