把空闲 keep-alive 连接交给监听线程
Apache Event MPM 用 pollset 和按超时值分组的队列维护空闲连接,让工作线程返回处理其他请求。
工程问题#
HTTP keep-alive 在请求之间保留连接。如果每条空闲连接都占着工作线程等下一次输入,有限的线程会被等待消耗;同时服务器仍须回收超时连接并处理连接重新就绪。
实际实现#
工作线程将连接改为 KEEPALIVE,记录入队时间并加入对应超时队列和 pollset,然后返回。单个队列共用 timeout,实现采用队尾追加、从队首回收的方式,避免为每条连接维护通用排序树。监听线程看到可读事件后移除队列和 pollset 项,再尝试交给工作线程;超时维护从队首取出到期前缀,并在释放 timeout_mutex 后执行清理。
处理流程#
- 请求完成且允许复用时,把连接置为 KEEPALIVE
- 记录时间,加入共用超时值的队列与 pollset,释放工作线程
- 可读时移出等待结构并尝试分派给工作线程
- 定期处理每个队列的到期前缀,摘链后在锁外关闭连接
源码片段#
固定版本 · 原文摘录1293 if (cs->pub.state == CONN_STATE_KEEPALIVE) {1294 ap_update_child_status(cs->sbh, SERVER_BUSY_KEEPALIVE, NULL);12951296 /* It greatly simplifies the logic to use a single timeout value per q1297 * because the new element can just be added to the end of the list and1298 * it will stay sorted in expiration time sequence. If brand new1299 * sockets are sent to the event thread for a readability check, this1300 * will be a slight behavior change - they use the non-keepalive1301 * timeout today. With a normal client, the socket will be readable in1302 * a few milliseconds anyway.1303 */1304 cs->queue_timestamp = apr_time_now();1305 notify_suspend(cs);13061307 /* Add work to pollset. */1308 cs->pub.sense = CONN_SENSE_DEFAULT;1309 update_reqevents_from_sense(cs, CONN_SENSE_WANT_READ);1310 apr_thread_mutex_lock(timeout_mutex);1311 TO_QUEUE_APPEND(cs->sc->ka_q, cs);1312 rv = apr_pollset_add(event_pollset, &cs->pfd);同队列使用统一超时值,连接记录时间后加入超时队列和 pollset。
299static void TO_QUEUE_APPEND(struct timeout_queue *q, event_conn_state_t *el)300{301 apr_time_t elem_expiry;302 apr_time_t next_expiry;303304 APR_RING_INSERT_TAIL(&q->head, el, event_conn_state_t, timeout_list);305 ++*q->total;306 ++q->count;307308 /* Cheaply update the global queues_next_expiry with the one of the309 * first entry of this queue (oldest) if it expires before.310 */311 el = APR_RING_FIRST(&q->head);312 elem_expiry = el->queue_timestamp + q->timeout;313 next_expiry = queues_next_expiry;314 if (!next_expiry || next_expiry > elem_expiry + TIMEOUT_FUDGE_FACTOR) {315 queues_next_expiry = elem_expiry;316 /* Unblock the poll()ing listener for it to update its timeout. */317 if (listener_is_wakeable) {318 apr_pollset_wakeup(event_pollset);319 }320 }队尾追加,参考队首更新时间上限,并在必要时唤醒监听线程。
2149 case CONN_STATE_KEEPALIVE:2150 cs->pub.state = CONN_STATE_PROCESSING;2151 remove_from_q = cs->sc->ka_q;2152 break;21532154 case CONN_STATE_LINGER_NORMAL:2155 remove_from_q = linger_q;2156 break;21572158 case CONN_STATE_LINGER_SHORT:2159 remove_from_q = short_linger_q;keep-alive 可读时转为 PROCESSING,并选择移除其等待队列。
2190 /* If we don't get a worker immediately (nonblocking), we2191 * close the connection; the client can re-connect to a2192 * different process for keepalive, and for lingering close2193 * the connection will be shutdown so the choice is to favor2194 * incoming/alive connections.2195 */2196 get_worker(&have_idle_worker, blocking,2197 &workers_were_busy);2198 if (!have_idle_worker) {2199 shutdown_connection(cs);2200 }2201 else if (push2worker(cs, NULL, NULL) == APR_SUCCESS) {2202 have_idle_worker = 0;2203 }获得工作线程后才分派;非阻塞获取失败会关闭连接。
1875 * - no expiry was given (zero means all), or1876 * - it expired (according to the queue timeout), or1877 * - the system clock skewed in the past: no entry should be1878 * registered above the given expiry (~now) + the queue1879 * timeout, we won't keep any here (eg. for centuries).1880 *1881 * Otherwise stop, no following entry will match thanks to the1882 * single timeout per queue (entries are added to the end!).1883 * This allows maintenance in O(1).1884 */1885 if (expiry && cs->queue_timestamp + qp->timeout > expiry1886 && cs->queue_timestamp < expiry + qp->timeout) {1887 /* Since this is the next expiring entry of this queue, update1888 * the global queues_next_expiry if it's later than this one.1889 */1890 apr_time_t elem_expiry = cs->queue_timestamp + qp->timeout;1891 apr_time_t next_expiry = queues_next_expiry;1892 if (!next_expiry1893 || next_expiry > elem_expiry + TIMEOUT_FUDGE_FACTOR) {1894 queues_next_expiry = elem_expiry;1895 }1896 break;遇到尚未到期的有效队首便停止;同时保留时钟回退处理条件。
1912 APR_RING_UNSPLICE(first, last, timeout_list);1913 APR_RING_SPLICE_TAIL(&trash.timeout_list, first, last, event_conn_state_t,1914 timeout_list);1915 AP_DEBUG_ASSERT(*q->total >= count && qp->count >= count);1916 *q->total -= count;1917 qp->count -= count;1918 total += count;1919 }1920 if (!total)1921 return;19221923 apr_thread_mutex_unlock(timeout_mutex);1924 first = APR_RING_FIRST(&trash.timeout_list);1925 do {1926 cs = APR_RING_NEXT(first, timeout_list);1927 TO_QUEUE_ELEM_INIT(first);1928 func(first);1929 first = cs;1930 } while (--total);批量摘除到期前缀,解锁后才逐项调用清理函数。
摘录分别保留调用入口、核心机制及边界;完整上下文见各段源码链接。
收益与取舍#
工程分析带来的收益
- 空闲 keep-alive 连接不必长期占用一个工作线程
- 在每队列固定超时值的条件下,队尾追加和已知节点摘除无需维护通用排序树
付出的代价
- 仍需保存每条连接的状态、FD 和 pollset 元数据;不等于无限连接容量
- 队列维护在锁内进行,清理多个到期项的总成本随项数增加,不能把源码中的 O(1) 注释解释为整批清理常数时间
- 此路径在没有可用工作线程时可能关闭 keep-alive 连接;过滤器和协议也会影响异步处理条件
可比较的选择
工作线程直接等待下一请求控制流简单,但大量空闲连接会占用工作线程。
所有连接放入通用定时器树支持每条连接任意期限,不过在同超时值队列这一条件下需要更多索引维护。
版本与适用范围#
固定到提交 3e2a8828a784。限 Event MPM 的空闲 keep-alive 路径,不代表所有 Apache MPM 或所有协议。各队列的 timeout 可以不同,源码另处理时钟向后跳变;时间戳在入队锁外采样,多线程入队顺序可能与采样顺序不同,因此不承诺严格期限排序或精确触发。
参考来源#
源码、官方文档与原始论文server/mpm/event/event.c · process_socket
相关实现与比较#
4 个相关案例nginx 事件定时器以到期时间为键放入红黑树,事件循环读取最小键确定等待上限。
伙伴分配器把空闲物理页组织为不同 order 的连续块,并用空闲链表管理同阶块。
limit_req 把漏桶计算、红黑树查状态与队列回收组合起来,按配置的 key 管理请求速率。
QEMU 的 QEMUTimerList 用有序单链表维护定时器,链表头就是下一到期项。