Apache HTTP Server/Event MPM 连接管理

把空闲 keep-alive 连接交给监听线程

Apache Event MPM 用 pollset 和按超时值分组的队列维护空闲连接,让工作线程返回处理其他请求。

3e2a8828a通信与流量核验 2026-10-12
相关概念

工程问题#

HTTP keep-alive 在请求之间保留连接。如果每条空闲连接都占着工作线程等下一次输入,有限的线程会被等待消耗;同时服务器仍须回收超时连接并处理连接重新就绪。

实际实现#

工作线程将连接改为 KEEPALIVE,记录入队时间并加入对应超时队列和 pollset,然后返回。单个队列共用 timeout,实现采用队尾追加、从队首回收的方式,避免为每条连接维护通用排序树。监听线程看到可读事件后移除队列和 pollset 项,再尝试交给工作线程;超时维护从队首取出到期前缀,并在释放 timeout_mutex 后执行清理。

处理流程#

  1. 请求完成且允许复用时,把连接置为 KEEPALIVE
  2. 记录时间,加入共用超时值的队列与 pollset,释放工作线程
  3. 可读时移出等待结构并尝试分派给工作线程
  4. 定期处理每个队列的到期前缀,摘链后在锁外关闭连接

源码片段#

固定版本 · 原文摘录
server/mpm/event/event.c3e2a8828a784e560c717ed957ea3d10203ac2d51 · L1293–L1312
查看原文件
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);
process_socket

同队列使用统一超时值,连接记录时间后加入超时队列和 pollset。

server/mpm/event/event.c3e2a8828a784e560c717ed957ea3d10203ac2d51 · L299–L320
查看原文件
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    }
TO_QUEUE_APPEND

队尾追加,参考队首更新时间上限,并在必要时唤醒监听线程。

server/mpm/event/event.c3e2a8828a784e560c717ed957ea3d10203ac2d51 · L2149–L2159
查看原文件
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;
listener_thread

keep-alive 可读时转为 PROCESSING,并选择移除其等待队列。

server/mpm/event/event.c3e2a8828a784e560c717ed957ea3d10203ac2d51 · L2190–L2203
查看原文件
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                    }
listener_thread

获得工作线程后才分派;非阻塞获取失败会关闭连接。

server/mpm/event/event.c3e2a8828a784e560c717ed957ea3d10203ac2d51 · L1875–L1896
查看原文件
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;
process_timeout_queue

遇到尚未到期的有效队首便停止;同时保留时钟回退处理条件。

server/mpm/event/event.c3e2a8828a784e560c717ed957ea3d10203ac2d51 · L1912–L1930
查看原文件
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);
process_timeout_queue

批量摘除到期前缀,解锁后才逐项调用清理函数。

摘录分别保留调用入口、核心机制及边界;完整上下文见各段源码链接。

收益与取舍#

工程分析

带来的收益

  • 空闲 keep-alive 连接不必长期占用一个工作线程
  • 在每队列固定超时值的条件下,队尾追加和已知节点摘除无需维护通用排序树

付出的代价

  • 仍需保存每条连接的状态、FD 和 pollset 元数据;不等于无限连接容量
  • 队列维护在锁内进行,清理多个到期项的总成本随项数增加,不能把源码中的 O(1) 注释解释为整批清理常数时间
  • 此路径在没有可用工作线程时可能关闭 keep-alive 连接;过滤器和协议也会影响异步处理条件

可比较的选择

工作线程直接等待下一请求控制流简单,但大量空闲连接会占用工作线程。

所有连接放入通用定时器树支持每条连接任意期限,不过在同超时值队列这一条件下需要更多索引维护。

版本与适用范围#

固定到提交 3e2a8828a784。限 Event MPM 的空闲 keep-alive 路径,不代表所有 Apache MPM 或所有协议。各队列的 timeout 可以不同,源码另处理时钟向后跳变;时间戳在入队锁外采样,多线程入队顺序可能与采样顺序不同,因此不承诺严格期限排序或精确触发。

参考来源#

源码、官方文档与原始论文
01
Apache HTTP Server · process_socket

server/mpm/event/event.c · process_socket

源码3e2a8828a784e560c717ed957ea3d10203ac2d51
02官方文档官方文档;实现以固定提交为准

相关实现与比较#

4 个相关案例
nginx·事件循环release-1.26.3

nginx 事件定时器以到期时间为键放入红黑树,事件循环读取最小键确定等待上限。

附源码片段 · 调度与资源分配
12 个匹配案例