Skip to content

HTTP/2 并发请求限制

TIP

  • 限制最终由浏览器决定:虽然服务器可以建议并发数,但浏览器内部存在一个硬性上限,最终生效的并发数是 服务器许可值浏览器硬性上限 中的较小者决定的。
  • chrome 的限制策略chrome 浏览器的行为分为两个阶段:
    1. 初始默认值:在未收到服务器明确指示时,遵循 http2 规范建议,默认采用 100 个并发流作为初始值。
    2. 硬性上限值:无论服务器许可多少并发流,chrome 内部存在一个绝对的、不可逾越的上限,这个值是 256

背景要知

客户端服务器 可以启动的流的数量不是无限的,是由每个对等方在连接开始时发送的 SETTINGS 帧的 SETTINGS_MAX_CONCURRENT_STREAMS 参数规定:section 6.5.2 of RFC 7540, 默认值为无限制,并且 RFC 有以下建议:

It is recommended that this value be no smaller than 100, so as to not unnecessarily limit parallelism.


建议这个值不小于 100,以避免不必要的并行限制。

Node.js 对并发流数量的限制

node.jspeerMaxConcurrentStreams 配置项描述如下:

Sets the maximum number of concurrent streams for the remote peer as if a SETTINGS frame had been received. Will be overridden if the remote peer sets its own value for maxConcurrentStreams. Default: 100.


设置远程对等方作为如果收到 SETTINGS 帧而看到的最大并发流数。如果远程对等方设置了自己的 maxConcurrentStreams 值,则将覆盖此值。默认值:100。

peerMaxConcurrentStreams 是一个启动阶段的、对对方能力的、临时的、安全的默认限制,确保了在双方完成握手和能力交换之前,通信能够有序、安全地开始。

可以通过修改 maxConcurrentStreams 配置项来限制并发流数量。

Specifies the maximum number of concurrent streams permitted on an Http2Session. There is no default value which implies, at least theoretically, 232-1 streams may be open concurrently at any given time in an Http2Session. The minimum value is 0. The maximum allowed value is 232-1. Default: 4294967295.


指定在 Http2Session 上允许的最大并发流数。没有默认值,这意味着在任何给定时间理论上最多可以打开 2^32-1 个并发流。最小值为 0。最大允许值为 2^32-1。默认值:4294967295。

接下来探索下 chrome 浏览器在 http/2 连接中,对并发流数量的限制策略。

Chrome 对并发流数量的限制

测试环境

  1. 版本限制

    • chrome137.0.7151.104 (Official Build) (arm64)
    • node.js22.14.0
  2. 测试场景

    • 客户端:创建一个 html 页面 (index.html),通过 javascript 发起大量对上述延迟接口的请求。
    • 服务器端:使用 node.js 创建了一个本地 http/2 服务器 (server.js),该服务器能提供一个对请求进行短暂延迟(1000ms)响应的接口 (/slow-resource)。
  3. 测试源码

html
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>HTTP/2 并发请求限制测试</title>
    <style>
      body {
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
          'Helvetica Neue', Arial, sans-serif;
        line-height: 1.6;
        color: #333;
        max-width: 800px;
        margin: 20px auto;
        padding: 0 20px;
        background-color: #f9f9f9;
      }
      h1 {
        color: #111;
        text-align: center;
      }
      .container {
        background-color: #fff;
        padding: 30px;
        border-radius: 8px;
        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
      }
      button {
        display: block;
        width: 100%;
        padding: 15px;
        font-size: 1.2em;
        font-weight: bold;
        color: #fff;
        background-color: #007bff;
        border: none;
        border-radius: 5px;
        cursor: pointer;
        transition: background-color 0.3s;
      }
      button:hover {
        background-color: #0056b3;
      }
      button:disabled {
        background-color: #cccccc;
        cursor: not-allowed;
      }
      #status {
        margin-top: 20px;
        padding: 15px;
        border-radius: 5px;
        text-align: center;
        font-weight: bold;
      }
      .status-info {
        background-color: #e9f7ef;
        color: #1d643b;
      }
      .status-done {
        background-color: #d4edda;
        color: #155724;
      }
      ol {
        padding-left: 20px;
      }
      li {
        margin-bottom: 10px;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>HTTP/2 并发请求限制测试</h1>
      <p>
        此页面用于验证浏览器对单个HTTP/2连接的并发请求数量限制(通常为100)。
      </p>

      <h3>操作步骤:</h3>
      <ol>
        <li>
          <b>打开开发者工具</b> (按 <code>F12</code> 或
          <code>Ctrl+Shift+I</code>)。
        </li>
        <li>切换到 <b>网络 (Network)</b> 面板。</li>
        <li>点击下方的"开始测试"按钮。</li>
        <li>
          观察网络瀑布图。你会看到大约前100个请求被立即发送,
          而后续的请求会显示为"已停止(Stalled)"或"排队中(Queued)",直到前面的请求完成。
        </li>
      </ol>

      <button id="test-btn">开始测试</button>
      <div id="status"></div>
    </div>

    <script>
      const testButton = document.getElementById('test-btn');
      const statusDiv = document.getElementById('status');
      const TOTAL_REQUESTS = 300;

      testButton.addEventListener('click', async () => {
        testButton.disabled = true;
        statusDiv.textContent = `准备发送 ${TOTAL_REQUESTS} 个请求...`;
        statusDiv.className = 'status-info';

        console.clear();
        console.log(`--- 测试开始,将发送 ${TOTAL_REQUESTS} 个请求 ---`);

        const requests = [];
        for (let i = 1; i <= TOTAL_REQUESTS; i++) {
          const requestPromise = fetch(`/slow-resource?id=${i}`)
            .then(response => {
              if (!response.ok) {
                console.error(`请求 #${i} 失败:`, response.statusText);
              }
              return response.text();
            })
            .catch(err => {
              console.error(`请求 #${i} 发生网络错误:`, err);
            });
          requests.push(requestPromise);
        }

        await Promise.all(requests);

        console.log(`--- 所有 ${TOTAL_REQUESTS} 个请求已完成 ---`);
        statusDiv.textContent = `测试完成!所有 ${TOTAL_REQUESTS} 个请求均已发送并收到响应。请检查网络瀑布图。`;
        statusDiv.className = 'status-done';
        testButton.disabled = false;
      });
    </script>
  </body>
</html>
js
const http2 = require('http2');
const fs = require('fs');
const path = require('path');

const PORT = 8443;
const SLOW_RESPONSE_DELAY_MS = 1000;

let options;
try {
  options = {
    key: fs.readFileSync('localhost-privkey.pem'),
    cert: fs.readFileSync('localhost-cert.pem'),
  };
} catch (e) {
  console.error(
    'Please ensure you have run the following command in the project root:'
  );
  console.error(
    "openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' -keyout localhost-privkey.pem -out localhost-cert.pem"
  );
  process.exit(1);
}

console.log('Starting HTTP/2 server...');

const server = http2.createSecureServer(options, (req, res) => {
  const protocol = req.stream.session.alpnProtocol;
  console.log(`收到请求: ${req.url} (协议: ${protocol})`);

  if (req.url === '/') {
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
    fs.createReadStream(path.join(__dirname, 'index.html')).pipe(res);
  } else if (req.url.startsWith('/slow-resource')) {
    setTimeout(() => {
      res.writeHead(200, { 'Content-Type': 'text/plain' });
      res.end('OK');
    }, SLOW_RESPONSE_DELAY_MS);
  } else {
    res.writeHead(404);
    res.end('Not Found');
  }
});

server.on('error', err => console.error(err));

server.listen(PORT, () => {
  console.log(`Listening on https://localhost:${PORT}`);
});

基线测试

  • 配置:服务器端未对并发流数量做任何特殊配置,采用 node.js 的默认值。

  • 观察:在浏览器开发者工具的网络面板中,观测到大约在第 100 个请求之后,后续请求开始出现明显的 排队(Stalled/Queued)现象。

  • 分析:此阶段结果存在模糊性。因为 node.jschrome 的默认并发流限制 恰好都是 100,无法判断瓶颈究竟在哪一方。

解除服务器限制,隔离浏览器

  • 配置:修改 server.js,在创建 http/2 服务器时,明确地将最大并发流数设置为一个远超默认值(如300)的许可值

    javascript
    const options = {
      // ... key 和 cert ...
      settings: {
        maxConcurrentStreams: 300
      }
    };

    排除了服务器端成为性能瓶颈的可能性。如果此时观察到的并发数仍低于 300,则可确认限制来自于浏览器。

  • 观察:在浏览器开发者工具的网络面板中,请求在达到 第 256 个 之后,第 257 个请求开始出现显著的 排队 现象。

  • 分析:限制源于浏览器,且其硬性上限为 256

Chromium 源代码分析

为了给实验结论提供最权威的支撑,对 Chromium 核心 网络模块 源代码进行了分析。

  • 初始默认值 (spdy_session.h) 在该头文件中,找到了初始并发数的定义:

    spdy_session.h
    cpp
    // Maximum number of concurrent streams we will create, unless the server
    // sends a SETTINGS frame with a different value.
    const size_t kInitialMaxConcurrentStreams = 100;

    这证实了 chrome 在连接建立之初,遵循规范,以 100 作为默认值。

  • 硬性上限 (spdy_session.cc) 在实现文件中,找到了硬性上限的定义:

    spdy_session.cc
    cpp
    // The maximum number of concurrent streams we will ever create.  Even if
    // the server permits more, we will never exceed this limit.
    const size_t kMaxConcurrentStreamLimit = 256;

    注释明确指出,即使服务器允许更多,浏览器也绝不会超过 256 这个限制

  • 决策逻辑 (spdy_session.cc) 在处理服务器 SETTINGS 帧的 HandleSetting 函数中,看到了应用该上限的具体逻辑:

    spdy_session.cc
    cpp
    case spdy::SETTINGS_MAX_CONCURRENT_STREAMS:
      max_concurrent_streams_ =
          std::min(static_cast<size_t>(value), kMaxConcurrentStreamLimit);
      ProcessPendingStreamRequests();
      break;

    最终生效的并发数 (max_concurrent_streams_) 是服务器提供的值 (value) 和硬性上限 (kMaxConcurrentStreamLimit) 中的 较小者。这与我们的实验结果 min(300, 256) = 256 是吻合的。

结论

chrome 浏览器对于单个 HTTP/2 连接的并发请求数限制,采用的是一种 礼貌协商,底线坚守 的策略。以 100 个并发流作为 初始值 与服务器协商,并愿意听从服务器通过 SETTINGS 帧提出的更高建议,但其自身内置了一个 256 个并发流的绝对硬性上限,任何来自服务器的更高请求都将被此上限所限制。

贡献者

页面历史

Discuss

根据 CC BY-SA 4.0 许可证发布。 (abd9c64)