云计算百科
云计算领域专业知识百科平台

【高频场景题系列】| 如何设计百万并发 Web 服务器?

👋 关于作者

你好,这里是一条泥憨鱼 | 专注于 后端开发、AI 前沿技术与计算机基础 。
请添加图片描述
这道题在后端面试里出现频率很高。表面上问的是服务器怎么设计,实际考的是你能不能把网络编程、操作系统、负载均衡、缓存、数据库和分布式架构串成一条线,给出一个站得住脚的容量方案。

现在要设计一个 Web 服务器,要求支持 100 万并发连接,你会怎么设计?

如果一上来就是 Nginx 集群、Redis 缓存、MySQL 分库分表,对方大概率会接着追问:

  • 你说的百万并发,是 100 万条 TCP 连接,还是每秒 100 万次请求?
  • 一台机器能承载多少连接?
  • 传统 BIO 为什么扛不住大量连接?
  • Netty 凭什么支持高并发?
  • 100 万连接要吃掉多少内存?
  • 请求突然暴增,系统怎么保护自己?
  • 数据库怎么才不会被压垮?

这些问题都指向同一件事:答题要从容量估算出发,逐层找瓶颈,而不是把中间件名词排一遍。

一、先界定:百万并发到底指什么

三个概念经常被混着用:

指标含义示例
并发连接数 同一时刻保持连接的客户端数量 100 万个 WebSocket 连接
QPS 每秒处理的查询或请求数量 每秒 10 万次请求
TPS 每秒完成的事务数量 每秒 1 万笔订单事务

三者之间没有固定的换算关系。

举个具体的:一个 WebSocket 服务器挂着 100 万个客户端,每个客户端平均 60 秒发一条消息,平均消息速率是

QPS=1,000,00060≈16,667
QPS=\\frac{1,000,000}{60}\\approx 16,667
QPS=601,000,000​≈16,667

也就是说,连接数看着吓人,实际每秒只有大约 1.67 万条客户端消息。反过来,1 万个并发连接如果每个都在高频发请求,压力同样可能把单机打满。

所以开场先跟面试官对齐条件:

  • 是百万长连接,还是百万级请求并发?
  • 是 HTTP 短连接、HTTP Keep-Alive,还是 WebSocket 长连接?
  • 每秒请求量大概多少?
  • 每个请求需要消耗多少 CPU、内存和数据库资源?
  • 响应时间要求是什么,比如 P99 延迟低于 200 ms?
  • 要不要跨机房容灾,可用性目标几级?
  • 对方不补充,就自己给出合理假设再往下推。后面为了方便,按两个场景分析:

    • 场景 A:百万 HTTP 并发请求。
    • 场景 B:百万 WebSocket 长连接。

    两者的底层网络技术相通,容量模型和业务处理方式差别不小。

    二、整体架构

    常见的高并发架构大致长这样:

    用户请求
    |
    v
    DNS / CDN / WAF
    |
    v
    四层负载均衡
    (L4 Load Balancer)
    |
    +———-+———-+
    | | |
    v v v
    Nginx节点1 Nginx节点2 Nginx节点N
    | | |
    +———-+———-+
    |
    v
    应用服务集群
    +———-+———-+
    | | |
    v v v
    Java服务1 Java服务2 Java服务N
    | | |
    +———-+———-+
    |
    +———-+———-+
    | | |
    v v v
    Redis 消息队列 MySQL
    集群 集群 集群

    这是逻辑视图,不代表每个请求都得穿透所有组件。静态资源可以直接由 CDN 返回,WebSocket 连接交给专门的网关集群维护,热点数据由缓存挡在前面,能异步处理的先丢进消息队列,数据库压力大就做读写分离或者分库分表。

    整套架构的分工可以用一句话概括:入口层管接入,应用层管业务,缓存和消息队列分担压力,数据库只做必要的数据读写。

    不过架构图画得再漂亮也没用。真正决定能不能扛住百万并发的,是下面这些具体问题。

    三、网络接入层如何承载百万连接

    先假设真有 100 万客户端同时连进来。每个 TCP 连接建立后,内核都要维护状态:

    • TCP 连接状态
    • Socket 文件描述符
    • 接收缓冲区和发送缓冲区
    • 定时器和超时处理
    • 用户态程序里的连接对象

    所以百万连接不是让一个程序循环 100 万次那么简单。

    3.1 BIO 为什么撑不住

    传统 Java BIO 长这样:

    ServerSocket serverSocket = new ServerSocket(8080);

    while (true) {
    Socket socket = serverSocket.accept();

    new Thread(() -> {
    try {
    handleRequest(socket);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }).start();
    }

    流程很直白:等客户端连进来,建一个线程,在线程里处理请求。

    问题在于线程数会跟着连接数一起涨。100 万连接对应 100 万线程,光线程栈、调度和上下文切换就够呛,而且这些线程大部分时间都在阻塞。

    补充一句,BIO 不等于必须一个连接一个线程。可以用线程池限制线程数量,但连接数远大于工作线程时,阻塞和调度问题又回来了。大量长连接需要更高效的 I/O 模型。

    3.2 NIO 为什么更适合高并发

    Java NIO 提供了非阻塞 I/O 和 Selector 这类机制。核心就两件事:不用给每个连接配一个线程,让少量线程监听大量连接的 I/O 就绪事件。

    Selector
    |
    +———+———+
    | | |
    v v v
    Socket A Socket B Socket C
    | | |
    v v v
    有数据 无数据 有数据
    | |
    +———+———+
    |
    v
    处理就绪事件

    Selector 可以监听多个 Channel。某个连接可读或者可写,Selector 就返回对应的就绪事件,程序再去处理。一个线程不会一直卡在一个 Socket 上。

    Java 里常见的选择是 NIO 和 Netty,Linux 侧的 epoll、Nginx 的事件驱动模型也是同一套思路。

    这里要说清楚:NIO 并没有让 I/O 操作凭空消失,它只是把大量连接需要的线程数降下来,让线程专注于已经就绪的事件。

    另外,Selector 不意味着所有逻辑都得塞进一个线程。实际系统通常会把网络 I/O 和耗时业务分开处理。

    3.3 Netty 怎么用这套思路

    Java 后端做高性能网络服务,Netty 是常用选择。典型结构:

    客户端连接
    |
    v
    Boss EventLoop
    负责接收新连接
    |
    v
    Worker EventLoop
    负责连接上的 I/O
    |
    v
    ChannelPipeline
    |
    v
    业务处理逻辑

    Linux 环境下,Netty 底层可以用 epoll 处理大量连接,用 EventLoop 管理一批 Channel,在事件循环里处理网络 I/O。

    几个要点:

    • Boss 负责接收新连接,Worker 负责已建立连接上的 I/O。
    • 一个 EventLoop 通常由一个线程驱动。
    • Channel 和 EventLoop 之间保持稳定的绑定关系。
    • 耗时的阻塞业务不能放在 I/O 事件循环里跑。

    最后一条,比如在 EventLoop 里直接执行一条耗时 500 ms 的数据库查询,这个 EventLoop 上其他连接的 I/O 都会被拖慢。

    正确做法是把耗时业务交给独立线程池,或者用异步方式调用下游服务。

    用了 Netty 不代表自动拥有百万并发能力。实际连接数还取决于服务器内存、文件描述符限制、TCP 参数、网卡带宽、单连接内存开销、消息频率和大小,以及业务处理速度。Netty 解决的是网络 I/O 效率问题,不是所有容量问题。

    四、百万连接需要多少内存

    假设要维护 100 万条长连接,先用一个简化模型估算:

    M=N×m
    M=N\\times m
    M=N×m

    M 是总内存,N 是连接数量,m 是每个连接的平均内存开销。

    假设应用侧每个连接平均占用 16 KB(这只是容量规划用的假设值,不是 Netty 的固定开销):

    M=1,000,000×16 KB=16 GB
    M=1,000,000\\times16\\text{ KB}=16\\text{ GB}
    M=1,000,000×16 KB=16 GB

    按每个连接 32 KB 算:

    M=1,000,000×32 KB=32 GB
    M=1,000,000\\times32\\text{ KB}=32\\text{ GB}
    M=1,000,000×32 KB=32 GB

    而且这还没算 JVM 自身开销、业务缓存、消息队列缓冲区、线程栈、JIT 编译和其他运行时开销、内核内存、TCP Socket 缓冲区,以及其他系统进程占用的内存。

    所以"一台 32 GB 内存的服务器就能扛 100 万连接"这种说法站不住。真实容量只能靠压测确定。

    连接数越大,单连接开销越容易被放大:

    单连接平均开销100 万连接的理论内存
    4 KB 约 4 GB
    8 KB 约 8 GB
    16 KB 约 16 GB
    32 KB 约 32 GB
    64 KB 约 64 GB

    这张表就是简单的乘法,没算任何额外开销。它想说明的是,优化单连接内存往往比单纯增加机器数量更划算。

    具体可以做的事:

  • 别为每个连接创建一堆业务对象。
  • 控制每个连接的发送缓冲区。
  • 消息队列要有界。
  • 连接断开后及时清理状态。
  • 配上合理的心跳和空闲超时机制。
  • 根据实际负载调整内存分配和对象生命周期。
  • 但也不能为了省内存把 TCP 缓冲区一味调小,它直接影响吞吐量,高延迟网络里尤其明显。

    五、负载均衡与水平扩容

    单台服务器资源有限,百万并发只能靠水平扩容,把连接和请求摊到多台机器上。

    100 万客户端
    |
    v
    负载均衡集群
    |
    +————+————+
    | | |
    v v v
    网关服务器1 网关服务器2 网关服务器3
    | | |
    约 10 万 约 10 万 约 10 万
    连接 连接 连接

    这只是示意图。100 万连接配 10 台网关,理想情况下每台平均承担 10 万连接,但这不代表 10 台一定够。单机到底能扛多少,得压测出安全容量。

    5.1 需要多少台服务器

    Nserver=⌈CtotalCsafe⌉
    N_{\\text{server}}=\\left\\lceil\\frac{C_{\\text{total}}}{C_{\\text{safe}}}\\right\\rceil
    Nserver​=⌈Csafe​Ctotal​​⌉

    C_total 是总连接数,C_safe 是单台服务器的安全连接容量。假设总连接数 100 万,单机经过压测后确定安全容量为 8 万:

    Nserver=⌈1,000,00080,000⌉=13
    N_{\\text{server}}=\\left\\lceil\\frac{1,000,000}{80,000}\\right\\rceil=13
    Nserver​=⌈80,0001,000,000​⌉=13

    至少需要 13 台。如果还要求任意两台服务器故障后,剩余机器仍然能承担全部流量,那就得再加冗余。

    单机最大连接数不等于单机安全连接数。一台机器极限测试能压到 12 万连接,但到 10 万时 P99 延迟已经明显恶化,生产环境就不能按 12 万来规划。

    生产系统要留余量,应对流量突增、机器故障、连接重新分配、业务请求变重,还有连接重建风暴。

    5.2 负载均衡怎么选

    类型典型用途
    DNS 调度 把用户引导到不同地域或接入集群
    CDN 静态资源缓存与边缘分发
    四层负载均衡 按 TCP/UDP 等传输层信息分发流量
    七层负载均衡 按 HTTP 等应用层信息分发流量
    服务发现与客户端负载均衡 服务之间的相互调用

    普通 HTTP 请求用七层负载均衡就够。WebSocket 长连接要多考虑连接建立后的持续性:连接一旦建立,后续通信通常都经过维护它的那个网关节点。

    轮询策略也不会让每条消息到达时重新选服务器。连接建在哪个节点,消息就走哪个节点。

    需要维持会话状态的业务,还得问自己几个问题:要不要会话粘滞,状态能不能外置,网关节点故障后怎么重连,重连后订阅关系怎么恢复,怎么避免大量客户端同时重连。

    对 WebSocket 来说,连接分配只是第一步,故障后的恢复能力同样重要。

    六、业务层和数据库怎么不被压垮

    网络层接得住请求,不代表整个系统处理得了。网关集群每秒收进 10 万个请求,每个都要查 MySQL,而数据库只能稳定处理 2 万 QPS,那网络层再强也白搭,瓶颈在数据库。

    所以要把两种能力分开看:

  • 接入能力:系统能接多少连接和请求。
  • 处理能力:系统能完成多少业务操作。
  • 这两者要分别设计、分别压测。

    6.1 用 Redis 挡热点数据

    以商品详情接口为例。原始链路是:客户端 → Java 应用 → 查 MySQL → 返回结果。

    每个请求都直接打库,大量重复请求会成倍放大数据库负载。加上缓存后就变成先查 Redis,命中直接返回,未命中再查 MySQL 并回写缓存:

    客户端
    |
    v
    Java 应用
    |
    v
    Redis 查询
    |
    +—- 缓存命中 —-> 返回结果
    |
    +—- 缓存未命中 –> 查询 MySQL
    |
    v
    写入缓存
    |
    v
    返回结果

    假设接口 10 万 QPS,缓存命中率 95%,未命中的请求量约为:

    QPSmiss=100,000×(1−95%)=5,000
    QPS_{\\text{miss}}=100,000\\times(1-95\\%)=5,000
    QPSmiss​=100,000×(1−95%)=5,000

    也就是说,数据库查询量从 10 万次/秒降到大约 5000 次/秒。这个结论有两个前提:请求分布和命中率在统计窗口内相对稳定,而且缓存未命中没有引发额外的查询放大。

    实际系统还要处理缓存穿透、缓存击穿和缓存雪崩。热点 Key 一过期,大量请求可能同时打到数据库。常见手段:

    • 互斥锁或 Singleflight 合并重复查询
    • 热点数据提前刷新
    • 逻辑过期
    • 过期时间加随机值
    • 本地缓存与分布式缓存组合

    缓存也不是越多越好。需要强一致性的业务,必须把失效策略、更新顺序和一致性要求讲清楚。

    6.2 用消息队列削峰

    活动开场时用户瞬间涌入,大量请求要创建订单。如果所有步骤同步执行(创建订单、扣库存、调支付、发通知),请求耗时会很长,所有下游都要承受瞬时流量。

    可以把适合异步处理的任务放进消息队列:接入层先限流,订单服务写消息,消费者慢慢处理。

    用户请求
    |
    v
    接入层限流
    |
    v
    订单业务服务
    |
    v
    消息队列
    |
    +——-+——-+
    | | |
    v v v
    消费者1 消费者2 消费者3

    但消息队列只能缓冲流量,不能凭空提高处理能力。假设每秒产生 5 万条消息,消费者每秒只能处理 3 万条,那每秒积压 2 万条:

    20,000×60=1,200,000
    20,000\\times60=1,200,000
    20,000×60=1,200,000

    一分钟就是 120 万条消息。所以要盯积压量、最老消息年龄、消费速度、重试次数、死信消息和下游服务健康状态。

    同步还是异步要看业务。用了消息队列,不等于订单创建、库存扣减和支付结果之间的一致性问题就解决了。业务幂等、事务设计和补偿机制仍然要做。

    6.3 限流、熔断与降级

    高并发系统不能只想着处理更多请求,还得避免超载后全面崩溃。保护可以分三层:

    • 入口限流:限制单位时间内进入系统的请求数,常见算法有固定窗口、滑动窗口、漏桶、令牌桶。
    • 服务级限流与隔离:限制特定接口、业务线程池或下游调用的并发量,防止某个慢服务耗尽整个系统的资源。
    • 熔断与降级:下游持续失败或变慢时,暂时停止部分调用,或者返回允许降级的结果。

    请求进来先过限流,超阈值就拒绝或返回降级结果;进入业务后调用下游,下游异常就熔断或降级。

    原则只有一条:系统过载时优先保护核心业务,而不是无条件接收所有请求。

    七、Linux 和 TCP 参数怎么调

    这一层是初级回答和深入回答的分水岭。面试官可能会追问:要承载百万连接,操作系统层面要注意什么?

    7.1 文件描述符限制

    Linux 里 Socket 也占文件描述符。单进程限制只有几万个,就算 CPU 和内存还有富余,也建不了更多连接。

    要检查进程级限制、系统级限制,看应用有没有 fd 泄漏,以及监听 Socket 和其他文件占掉了多少描述符。

    ulimit -n

    这条命令看的是当前 Shell 的限制,不一定等于服务进程实际生效的限制。生产环境还要看服务管理器配置和进程真实的资源上限。

    7.2 TCP 监听队列

    建连过程中,内核既要处理还没完成握手的连接,也要缓存已完成握手、等待应用 accept 的连接。

    相关参数主要是 net.core.somaxconn 和 net.ipv4.tcp_max_syn_backlog,分别涉及监听队列上限和 SYN 请求队列。

    但别直接拉到最大。实际配置要看内核版本、应用传入的 backlog、建连速率、网络环境,以及有没有出现队列溢出。

    监听队列只影响连接建立阶段,解决不了已建立连接的内存和业务处理问题。

    7.3 TIME_WAIT 与建连速率

    频繁建立和关闭 TCP 连接会产生大量 TIME_WAIT。但数量多不一定是故障。

    真正要看的是建连速率、哪一方主动关闭、可用端口和地址资源、文件描述符占用、内核资源消耗,以及有没有不必要的短连接。

    用 HTTP Keep-Alive、连接复用和合理的超时配置,可以减少无谓的建连关连。别把 TIME_WAIT 当成所有高并发问题的根因,也别乱改 TCP 参数。

    7.4 网卡带宽

    很多人只算 CPU 和内存,忘了带宽。

    假设服务器要向客户端持续发数据,每个客户端每秒收 1 KB,100 万客户端的总速率是:

    1,000,000×1 KB/s≈1 GB/s
    1,000,000\\times1\\text{ KB/s}\\approx 1\\text{ GB/s}
    1,000,000×1 KB/s≈1 GB/s

    换算成比特速率大约 8 Gbit/s。这还没算协议开销、重传和其他流量。

    所以能维持 100 万连接,不等于能持续向这 100 万连接灌数据。长连接系统要分别估算建连速率、每秒消息数、平均消息大小、上下行带宽和突发流量。

    连接容量、消息处理能力、带宽容量,是三个独立的维度。

    八、百万 WebSocket 连接怎么设计

    假设你要做一个 IM 系统,100 万在线用户,用户之间能发消息,服务端要能把消息推到指定用户。

    这种业务不该把长连接和所有业务塞进同一个巨型 HTTP 服务。更合理的方式是把长连接接入和业务处理分开。

    8.1 推荐架构

    100 万在线用户
    |
    v
    负载均衡集群
    |
    v
    WebSocket 网关
    +———+———+
    | | |
    v v v
    网关 A 网关 B 网关 C
    | | |
    +———+———+
    |
    v
    消息路由与业务服务
    |
    +——+——+
    | |
    v v
    消息队列 Redis
    | |
    v v
    消息处理 用户路由信息
    |
    v
    MySQL

    各部分职责:

    WebSocket 网关负责建立和维护长连接、处理心跳、解析协议、管理本机连接、把消息路由给业务服务、把业务消息推回连接。

    业务服务负责用户权限校验、消息业务逻辑、消息持久化、群聊单聊规则和消息状态处理。

    Redis 这类存储维护用户与网关节点的映射关系,给跨节点消息路由提供必要信息。

    消息队列在合适的业务流程里解耦生产者和消费者,缓冲可异步处理的消息,并在消费者故障时支持重试。

    这里澄清一个常见误解:Redis 不是必须保存每个连接的全部状态。通常网关在本地维护真实的 Socket 和连接对象,分布式存储只放必要的路由元数据。

    8.2 跨网关消息怎么推

    用户 A 连接在网关 1,用户 B 连接在网关 8。A 给 B 发消息,业务服务如果不知道 B 连在哪个网关,就没法把消息送到正确的连接。

    所以要维护类似这样的路由关系:

    user_1001 -> gateway_1
    user_1002 -> gateway_8
    user_1003 -> gateway_3

    流程是:网关 1 收到消息,交给消息业务服务,服务查 B 的网关,发现是网关 8,把消息发给网关 8,再通过 B 的 WebSocket 连接推下去。

    问题在于,B 可能在查到路由之后立刻断线,或者已经重连到别的网关。路由信息必须有明确的生命周期,在连接断开、重连和网关故障时及时更新。

    有重复消息和超时重试的情况,还得考虑消息 ID、幂等和去重。

    8.3 网关宕机怎么办

    网关 8 突然挂了,上面的连接全部断开。要做的事:

  • 发现网关节点故障。
  • 清理失效路由,或者标记为无效。
  • 让客户端带退避和随机抖动重连。
  • 让负载均衡把新连接分配到健康节点。
  • 恢复必要的用户订阅和会话状态。
  • 按业务规则补偿未送达的消息。
  • 为什么非要退避和抖动?10 万个客户端同时发现断线、同时重连,剩余网关会瞬间被打爆,可能引发级联故障。指数退避加随机抖动,能把重连请求摊到不同时间窗口。

    长连接架构不能只想怎么维持连接,也得想好大规模断开时怎么收场。

    九、Java 后端怎么落地

    把系统拆成几块:

    模块可选技术主要职责
    网络接入 Nginx、L4 负载均衡 流量接入与分发
    长连接网关 Netty 连接管理、网络 I/O
    业务服务 Spring Boot 业务逻辑
    缓存 Redis 热点数据、路由元数据
    消息队列 Kafka、RabbitMQ 等 异步解耦与消息缓冲
    数据存储 MySQL 持久化数据
    服务治理 限流、熔断、超时控制 保护系统
    可观测性 Prometheus、Grafana 等 指标监控与告警

    选型没有标准答案。纯 HTTP 服务不一定需要自己用 Netty 写接入层,普通 CRUD 系统不一定上 Kafka,低频业务不一定搞复杂的分布式缓存,WebSocket 服务也不必把所有业务都塞进 Netty 的事件循环。

    高并发设计的关键是解决实际瓶颈,不是把热门中间件都塞进项目。

    9.1 业务线程池怎么设计

    Netty 的 I/O 线程收到请求后要查数据库,直接在 I/O 线程里执行阻塞查询,会拖累这个线程负责的其他连接。可以把耗时业务提交给专门的执行器:

    ExecutorService businessExecutor =
    new ThreadPoolExecutor(
    16,
    64,
    60,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(2000),
    new ThreadPoolExecutor.AbortPolicy()
    );

    这段代码只是展示隔离思路,线程数、队列大小和拒绝策略都要按压测结果和业务特征来定。

    它的作用有四条:限制并发业务执行数量、限制排队任务数量、防止请求无限积压、过载时触发明确的拒绝策略。

    为什么不用无界队列?任务到达速度长期高于处理速度时,队列会一路涨到 OOM,请求延迟也会越来越高。

    AbortPolicy 会直接抛出拒绝执行异常,业务里要把它转成合适的错误响应或降级处理。

    线程池也不是越大越好。CPU 密集型任务线程太多反而增加调度开销;I/O 密集型任务的并发数要结合等待时间、下游容量和整体延迟来测算。

    十、怎么证明系统真能扛住百万并发

    聊到这儿,面试官会从架构设计转向容量验证。

    一个原则:架构图上画了 20 台服务器,不等于系统支持百万并发,必须测出来。

    10.1 先建立容量模型

    至少要明确这些指标:

    指标需要回答的问题
    最大连接数 每台机器能稳定维持多少连接?
    连接建立速率 每秒能接受多少新连接?
    QPS 每秒能处理多少业务请求?
    消息吞吐量 每秒能处理多少条消息?
    网络带宽 上下行流量有没有到瓶颈?
    内存占用 连接增加时内存怎么增长?
    延迟 P95、P99 延迟是多少?
    错误率 超载时失败率怎么变化?
    故障恢复 节点故障后多久恢复?

    重点看 P99。平均响应时间可能很漂亮,但仍可能有少量请求等很久,尾部延迟往往更直接影响用户体验。

    10.2 分阶段压测

    别一上来就压 100 万连接,逐步加:1 万、5 万、10 万、25 万、50 万、100 万。

    每个阶段都要观察 CPU 使用率、内存、GC、网络吞吐、文件描述符数量、TCP 连接状态、请求延迟、错误率和线程池队列长度。

    WebSocket 服务还要分别测:大量连接建立、稳定维持空闲连接、所有连接同时发消息、服务端向大量连接推送、部分网关故障、客户端集中重连。

    这几个场景的性能可能完全不同。比如系统能稳挂 100 万空闲连接,但所有客户端同时发消息时 CPU 直接打满。压测要模拟真实的消息分布,不能只测"连上了"。

    10.3 压测机自己也会成为瓶颈

    用一台普通机器去发 100 万客户端连接,压测机自己可能先撑不住:文件描述符不够、内存耗尽、网卡打满、CPU 饱和、建连速率上不去。服务端资源还富余,目标连接数却怎么也到不了。

    所以大规模压测通常要用多台压测机,并单独监控压测端的资源使用情况。

    十一、面试官可能追问的 8 个问题

    这些问题可以用来检验自己是不是真懂了。

    追问 1:为什么不能一个连接对应一个线程

    连接数很大时,阻塞式 I/O 会产生高昂的线程栈内存、调度和上下文切换成本。NIO、Selector、epoll 和 Netty 这些机制,能让少量线程处理大量连接的 I/O 事件。

    但业务里需要大量 CPU 运算的部分,还是要有合理的执行模型。

    追问 2:百万连接是不是需要百万线程

    不是。连接数和线程数没有一一对应关系。一个事件循环线程可以管理很多连接,具体能管多少取决于消息频率、处理逻辑、CPU、内存和延迟要求。

    追问 3:缓存命中率 99%,是不是就不会压垮数据库

    不是。1% 的未命中在高 QPS 下依然可能是一大笔查询;热点 Key 失效会导致缓存击穿;缓存自身也有容量、网络带宽和处理能力的限制。还要配合热点保护、请求合并、限流和数据库容量设计。

    追问 4:加服务器是不是就能无限扩容

    不能。水平扩容能增加一部分处理能力,但系统还会被数据库容量、Redis 吞吐量、消息队列吞吐量、网络带宽、共享资源竞争、全局锁、单分片热点和外部依赖限额卡住。

    扩容之前先找到真正的瓶颈。

    追问 5:数据库只有 2 万 QPS,入口有 10 万 QPS 怎么办

    用缓存挡掉大部分读,可异步的业务走消息队列,入口和业务层限流,优化 SQL 与索引,读写分离或分片,再用隔离和降级保住核心业务。

    另外要问清楚,这个 2 万 QPS 是在什么 SQL、什么数据规模、什么事务隔离级别和硬件条件下测出来的。不同查询成本差别很大,QPS 不能脱离业务条件当固定指标用。

    追问 6:一台网关宕机,怎么保证用户不掉线

    TCP 和 WebSocket 长连接在网关故障时通常就是断了,没法靠负载均衡把原连接无缝迁到另一台机器。一般要靠客户端重连、会话恢复、路由信息更新,以及必要的消息补偿机制。

    追问 7:百万 WebSocket 连接和百万 HTTP 请求有什么区别

    HTTP 更关注请求吞吐量、连接复用、响应延迟和业务资源消耗。WebSocket 除了消息吞吐,还要维护长期连接状态、心跳、路由关系和重连恢复机制。

    如果连接大部分时间空闲,系统主要受内存和连接管理成本影响;如果所有连接都在持续发消息,CPU、带宽和业务处理能力就会成为限制。

    追问 8:必须给一个最终方案,你怎么选

    典型的 Java 长连接业务,我会这样排:

  • 四层负载均衡分发 TCP 连接。
  • 用 Netty 构建无状态或轻状态的 WebSocket 网关。
  • 网关本地维护真实连接对象。
  • 用 Redis 等组件维护必要的用户路由元数据。
  • 耗时业务与 I/O 线程隔离。
  • 消息队列解耦适合异步处理的业务。
  • 按访问特征使用 Redis 缓存。
  • 用限流、超时、熔断和有界队列保护系统。
  • 通过监控和压测确定单机安全容量。
  • 设计网关故障后的客户端重连和业务恢复机制。
  • 具体上不上某个组件,还是要看业务需求和压测结果。

    十二、两分钟版本

    上面的内容全讲一遍,面试时间肯定不够。

    先给整体结构,再根据追问展开。下面这份可以直接拿来练。

    如果让我设计一个支持百万并发的 Web 服务器,我会先确认"并发"的定义。百万 TCP 连接、百万级并发请求和每秒百万请求,是三个完全不同的容量目标。

    明确场景之后,我从网络接入、应用处理、数据存储和系统保护四个层面设计。

    第一,接入层用负载均衡集群把流量分发到多台服务器。大量长连接可以用 Netty 这类事件驱动的框架,靠 NIO 和 epoll 让少量线程管理大量连接,避免阻塞式 I/O 带来的线程开销。

    第二,应用层水平扩展,多实例部署。网络 I/O 线程和耗时业务分离,用有界线程池、超时控制和并发隔离,防止慢请求拖垮服务。

    第三,数据层用 Redis 缓存热点数据,降低数据库访问压力;适合异步处理的业务放进消息队列解耦和削峰。同时要考虑缓存击穿、消息积压、业务幂等和数据一致性。

    第四,系统层面合理配置文件描述符、TCP 监听队列和网络资源,并结合单连接内存、CPU、网卡带宽和业务吞吐量做容量估算。

    最后,我不会拍脑袋说多少台机器就能扛百万并发,而是先压测出单机安全容量,再计算实例数量,预留故障冗余,并监控连接数、QPS、P99 延迟、错误率和资源使用情况,验证流量突增和节点故障时系统是否仍然稳定。

    如果场景是 WebSocket 长连接,我还会重点设计用户与网关的路由、客户端重连、心跳检测和消息恢复机制。

    总的来说,百万并发设计的关键不是堆服务器和中间件,而是明确容量目标、找到系统瓶颈,用合理的架构和压测验证,让系统具备扩展性和故障恢复能力。

    十三、要理解的

    准备 Java 后端面试,别把这道题当标准答案背,把它当成一张知识地图。

    核心问题需要掌握的知识
    少量线程为什么能管理大量连接? BIO、NIO、Selector、epoll、Netty
    百万连接为什么会消耗大量资源? TCP、Socket、文件描述符、内存模型
    为什么需要多台服务器? 负载均衡、水平扩容、容量规划
    接住请求为什么还不够? Redis、MySQL、消息队列、限流与熔断
    怎么证明系统能承载百万并发? 性能压测、容量估算、监控、故障演练

    学习顺序:

    第一阶段,网络基础。TCP 建连、HTTP Keep-Alive、WebSocket、BIO 和 NIO 的区别。

    第二阶段,Java 高性能网络编程。Netty 的 Reactor 模型、EventLoop、ChannelPipeline,以及为什么不能在 I/O 线程里跑耗时的阻塞任务。

    第三阶段,分布式系统设计。负载均衡、Redis 缓存、消息队列、限流、熔断和幂等。

    第四阶段,性能与容量。容量估算、压测、监控,以及怎么根据 CPU、内存、网络和数据库指标定位瓶颈。

    Netty 为什么能用少量线程维护大量连接,以及百万连接为什么不等于百万 QPS。这两点很重要

    这两个问题想通了,再看 Nginx、Redis、消息队列和分布式架构,就不会觉得它们是一堆孤立的名词,而是在解决同一个系统里不同位置的瓶颈。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【高频场景题系列】| 如何设计百万并发 Web 服务器?
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!