想象一个大物流园区:几百个分拣员(CPU)同时忙着手头的包裹,但园区里只有一个中央备件库。每天取件高峰,所有人都涌向同一个窗口,排队、拥堵、甚至互相绊倒。后来园区把备件分散到每个分拣台旁边,建起一个又一个“分区备件屋”。分拣员取自己屋里的备件,一伸手就够到;可要取隔壁屋的零件,就得穿过园区的传送带,绕一圈才回来。这就是 NUMA(非一致内存访问)的核心真相:内存也有远近亲疏。理解 NUMA,就是理解园区怎么分区、传送带有多宽、以及聪明的调度员如何让每个分拣员尽量“就近取料”。
对 AI 从业者来说,现代训练和推理服务器动辄几十上百核,NUMA 拓扑是性能调优的必修课。掌握它,你才能在大规模多核机器上定位神秘卡顿、优化数据库与 HPC 作业,让每一次内存访问都花在刀刃上。
可视化演示
展示NUMA架构三核心:多节点拓扑与本地远程访存延迟差异、QPI/UPI互联带宽与访存延迟对比、first-touch亲和性绑定策略,动态演示非一致内存访问机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
节点拓扑:NUMA多节点互联结构 青色为NUMA节点CPU 绿色为本地内存 红色为远程访存 local_access << remote_access via QPI/UPI 每个NUMA节点含本地CPU和内存,节点间通过QPI/UPI互联,远程访存需跨节点传输导致更高延迟
NUMA基础
最早的大型多处理器系统,就像只有一个“中央大备件库”的物流园。无论你的分拣台在园区的哪个角落,取备件都必须挤上同一条主路,走到中央库房再回头。这种“所有人共用一条内存总线”的架构,叫 UMA(统一内存访问)。核心数量少时问题不大——只有四五个分拣员,主路还扛得住。可当核心增加到十几个、几十个,所有 CPU 同时发访存请求,总线立刻成为全园最拥堵的瓶颈。
NUMA 的设计初衷很简单:与其让所有人挤一条路,不如把园区“分区”。每个分区拥有自己的分拣台和备件屋,分区之间再用高速传送带连接。于是“分拣台 + 专属备件屋”构成了一个 NUMA 节点(Node),CPU 访问本节点的内存叫本地访问,访问其他节点的内存叫远程访问。这种内存访问延迟不一致的架构,就是 NUMA——非一致内存访问。
从硬件实现看,过去内存控制器位于主板的北桥芯片,所有 CPU 都通过前端总线去抢这唯一的内存控制器;而在 NUMA 时代,内存控制器被集成进每个 CPU 内部,每个 CPU 直接管理着自己身边的一批内存条。这样一来,增加 CPU 时也能同步扩展内存带宽,不再是所有人共用一根总线的独木桥。
NUMA 放弃了对所有内存访问“一刀切”的公平,换来了整体吞吐的扩展性。它不像原来只有一个大仓库,而是把每个分拣台的备件屋都填满货。取件虽然有了远近之分,但绝大多数时间,大家都能在家门口完成工作——这正是现代多路服务器的必然选择。
- 总线总带宽内存总线每秒最多能传输的字节数,比如 20 GB/s。这个值由硬件决定,上限固定。
- 核心数 n同时发起访存请求的 CPU 核心数量。在等式中它是分母,代表“分蛋糕的人数”。
- 有效带宽把总带宽平均切给每个核心后,单个核心实际能获得的带宽。
节点拓扑
把物流园划分成若干个分区后,每个分区内部严格排列着三种设施:分拣台(CPU 核心)、理货工位(内存控制器)、以及紧邻的备件屋(本地内存条)。在硬件世界里,这三者合在一起就是一个完整的 NUMA 节点。一台双路服务器通常有两个节点,四路服务器则有四个节点,节点之间通过高速互连环路相连。
当你把目光聚焦到单个节点:CPU 访问本地内存时,请求直接交给内部的内存控制器,无需惊动任何“外部门卫”;可当 CPU 需要访问另一个节点的数据时,请求必须先打包成消息,通过节点间的互连环路送过去,再由对方节点的内存控制器完成读取,最后原路返回数据。一进一出,路径上多了无数道手续。
系统如何描述这种“远近”关系?答案是用节点距离(Node Distance)。两台服务器直连时,节点间的距离是 1;如果节点之间没有直连链路,就必须经过第三方节点中转,距离变为 2 或更大。lscpu 和 numactl --hardware 输出的 node distance 矩阵,正是这张“园区地图”——距离值越大,代表取件绕路越远。
回到园区比喻:每个分区都有自己的备件屋,取本区备件只需一转身;取隔壁区分区的备件,则要先走上传送带,到了对方分区再下传送带。节点拓扑就是这张“谁和谁相邻、中间隔几个区”的地图——指挥官只有看清地图,才能做出合理的调度决策。
- 基础延迟 T_base即使是最理想的本地访问,也要消耗的内存芯片响应时间。
- 距离系数 F每跨越一跳链路额外付出的转发代价,包括打包、传输、解包的开销。
- 跳数 D(i, j)CPU 所在节点与内存所在节点之间需要经过多少条链路。直连为 1,中转则累加。
访存延迟
在实际物流中,“取件时间”取决于两件事:备件离你多远,以及中途会不会堵车。NUMA 的延迟差异正是如此。本地访问无需离开 CPU 芯片,数据从本地 DRAM 传回核心通常只需几十到一百纳秒;而远程访问要穿过节点间互连,请求报文的打包、传输、对方内存控制器响应、数据原路返回,整个过程轻松翻到两到四倍延迟,在某些极端拓扑下甚至更高。
这种差异不是理论数字,而是实实在在的性能杀手。对于随机小数据访存(比如数据库索引查找、哈希表探测),每一次远程访问都像在循环“走出园区取一个小件”;延迟翻倍意味着整条链路的响应时间翻倍。对于大块数据顺序读写,虽然延迟可以被流水线掩盖一部分,但跨节点传输还占用了互连带宽,导致吞吐同样受挫。
由此引出 NUMA 调优最核心的指标:本地命中率(local hit rate)。如果程序 80% 的访存都落在本地节点,平均延迟就被压低;反之,如果 80% 的访存都在远程节点,再快的 CPU 也会被访存等待拖垮。
分拣员如果总跑到隔壁分区取件,哪怕传送带再快,一天下来也要多走几万步。NUMA 优化的本质,就是让内存访问的“脚步”尽量留在本地——这不仅靠硬件拓扑,更靠操作系统和我们的部署策略。
- 本地命中率 h程序访问内存在自己所在节点发生的比例,范围是 0 到 1。
- 本地延迟 T_local访问同节点内存的耗时,作为快速基准。
- 远程延迟 T_remote访问其他节点内存的耗时,通常为本地 2~4 倍。
- 加权求和把两种访问按比例混合,得到整个程序的期望访存时间。
QPI/UPI互联
分区之间靠什么传递备件?答案是传送带——硬件上叫互连环路。Intel 在初代 Core i7 / Nehalem 架构上推出了 QPI(QuickPath Interconnect),替代了老旧的并行前端总线。它是一种点对点高速串行链路,让每个 CPU 都能与另一个 CPU 直接通信。随着处理器规模扩大,Intel 在 Skylake-SP 及之后引入了新一代 UPI(Ultra Path Interconnect),支持更高的带宽、更大的拓扑规模,让四路、八路服务器也能高效组网。
为什么互连这么关键?因为每一次远程内存访问都要向它“借道”。链路的带宽是有限的:QPI 早期可以做到单链路 20~25 GB/s 左右,UPI 提升到更高的数据传输率,但依然无法与本地内存动辄上百 GB/s 的带宽相媲美。当多个核心同时发起跨节点访问,互连就会像高峰期的传送带一样“堵件”。
所以系统设计者会在拓扑上做文章:尽量让相邻节点直连,给每个节点规划多条链路。多链路聚合出来后,跨节点总带宽大约是单条链路带宽乘以链路条数。但无论怎么优化,远程访问的“过桥费”都不可能消失——它只是把缓存一致性和数据移动的成本集中到了一条有限的通道上。
物流园的传送带带宽是全局共享资源:本分区取件用不上它,跨分区取件才会挤占它。数据库的分布式事务、HPC 的共享内存通信,若大量依赖跨节点访存,最终都会汇聚到这条传送带上。因此,优化不仅是“少跨节点”,也是“错峰使用传送带”。
- 链路条数 L两个节点之间能够并行工作的互连接口数量。
- 单链路带宽 B_link一条 QPI/UPI 链路每秒能承载的最大数据量。
- 乘法聚合多条链路并行传输,总带宽得到倍增。
调度分配
一个聪明的园区调度员,会怎么安排分拣员和备件呢?他会记住每个分拣员常取的包裹类型,把相关备件尽量放在他手边;如果某个分拣台太忙,还会把新任务派给空闲的分拣台,并顺手把需要的备件也挪过去。操作系统里的 NUMA 调度,就是这个调度员的自动化版本。
Linux 内核的调度器不仅看 CPU 负载,还看内存位置。当一个线程被选到某个 CPU 上运行时,调度器会优先选择那些“本地已有所需内存页”的节点;反过来,分配内存页时,内存管理子系统会把新页面优先放到发起访问的 CPU 所在节点。线程和内存互相“追着跑”,这就是 NUMA-aware 调度的核心思想。
当然,调度不是只知道“就近”这么简单。一个节点就算内存里都有你要的页面,但 CPU 已经塞满 100 个任务,硬往那里挤也是灾难。所以内核会做成本权衡:既有距离成本,也有负载成本。Linux 内核的 AutoNUMA 机制还会定期采样页表访问位,发现页面被远程节点高频访问时,主动把页面迁移过去,让“货随人走”。
调度员的智慧在于平衡:既不能让大家扎堆取最便捷的备件屋,也不能把备件搬到离使用者十万八千里。操作系统在幕后持续计算这些“距离 × 负载”的分数,并不断微调。但调度始终是启发式的,偶尔也有开小差的时候,于是我们需要更显式的手段——亲和性绑定。
- 距离成本 dist(p, m)进程所在节点与内存所在节点之间的跳数。
- 权重 α调度器看重“就近”的程度,α 越大越不愿绕路。
- 负载 load(m)目标节点当前的任务繁忙程度或内存压力。
- 权重 β系统对负载不均的容忍度,β 越大越不愿让某个节点过载。
亲和性绑定
如果调度员是“建议式”地安排,那亲和性绑定就是“白纸黑字”的合同:把某位分拣员固定在一个分拣台,并规定备件必须放在他手边。这就是 NUMA 亲和性的两个维度——CPU 亲和性和内存亲和性。前者通过 taskset 或 pthread_setaffinity_np() 把线程固定在指定核心上;后者通过 numactl --membind 或 mbind() 指定内存只能落在某个节点。
更隐蔽也更重要的是 first-touch(首次接触)策略。它的规则很简单:内存页在第一次被访问时,由哪个 CPU 触发缺页异常,页面就分配在那个 CPU 所在节点的内存里。换句话说,数据最终的“家”不是由 malloc 决定,而是由第一个碰它的线程决定。很多程序的性能隐患就藏在这里:一个线程分配了巨大的缓冲区并初始化为零,之后其他线程来并发读写——此时页面早已成为“分配者”节点上的本地页,后续所有使用者都在跨节点访问。
遵循 first-touch 的优化之道:让每个线程只初始化自己将要频繁访问的那片数据。你可以把一个大数组分成若干段,让线程 0 写入并计算前 1/n,线程 1 负责第二段……这样每个页面的首次访问都由真正使用它的 CPU 完成,数据自然落在“家门口”。
物流园里最顺手的操作方式,就是让负责某批货的分拣员同时拥有这批货的备件屋钥匙。first-touch 是那把钥匙的默认分配规则,亲和性绑定则是你主动调整钥匙归属的手段。两者配合,才能在纷繁的多核调度中牢牢锁住性能。
- 本地访问次数 N_local在进程运行的节点内完成的内存访问计数。
- 总访问次数 N_total进程所有内存访问的总和,无论本地还是远程。
- 比值 h本地访问占全部访问的比例,越接近 1 说明“家门取料”越彻底。
工具与调优
再聪明的调度也需要仪表盘和方向盘。Linux 提供了整套 NUMA 监控与配置工具:numactl 既能查看拓扑,也能下发绑定命令;numastat 则实时监控每个节点的内存命中与失配情况;lscpu 可以快速列出每个节点包含哪些 CPU;lstopo 还能把整套拓扑画成直观的图。对部署 AI 服务的工程师来说,这些是定位性能抖动的基础工具。
实际调优场景中,最常用的一行命令是:
它把应用绑在节点 0 的 CPU 上运行,并且强制所有内存分配只落在节点 0,彻底断绝跨节点访问。对于数据库,通常会为每个 socket 启动独立实例或连接组,让连接、线程、缓冲池内存全部本地化;对于 HPC 和深度学习训练,则可能把进程绑定到相邻的核心组,并用 interleave 模式把内存分散到多个节点,以便利用所有节点的总带宽。
这些优化带来的收益可以用一个加速比公式直观刻画:若本地命中率从 50% 提升到 90%,而远程延迟是本地延迟的 3 倍,理论上限可达到约 1.9 倍加速。这不是锦上添花,而是让多核服务器的内存带宽真正为应用所用。
从 numactl 的拓扑输出,到数据库的实例拆分,再到 HPC 作业的核组绑定,整套工具链就像物流园的管理平台:先看地图,再定策略,最后用监控数据验证效果。理解 NUMA,不是为了背命令,而是为了养成“数据放身边”的系统思维——这是所有高性能系统工程师都会受益终身的习惯。
- 延迟比 r远程延迟除以本地延迟,代表远程访问多昂贵。
- 本地命中率 h优化后本地访问占全部访问的比例。
- 分母 h + (1-h)·r优化后的平均访存成本,越接近 1 越好。
- 比值 S基线成本除以优化后成本,得到的性能提升倍数。
numactl --cpunodebind=0 --membind=0 ./my_applicationAI 应用场景
大模型推理服务的 NUMA 亲和化部署
阶段:推理服务部署与性能调优
解决的问题:大语言模型推理时,KV cache 和中间激活张量会占用大量内存。若线程与内存页在多个 NUMA 节点间随意散布,每一次 token 生成都伴随远程访问,导致 p95 延迟显著上升。传统做法只调整 batch size 或换卡,却忽略了“内存放错节点”这一隐性瓶颈。
具体案例:在一台双路 32 核服务器上部署推理服务,使用 numactl --cpunodebind=0 --membind=0 将 worker 进程绑定到节点 0 的 CPU 和内存,同时把显存与 CPU 端的传输缓冲区按节点就近分配。调整后,模型推理的 p95 延迟从 280ms 降到 190ms,吞吐提升约 25%。
分布式训练数据管道的 NUMA 优化
阶段:训练数据预处理与加载
解决的问题:数据加载进程用 mmap 读取超大数据集时,页面首次触碰的位置决定了后续所有 worker 的本地命中率。若初始化线程与真正读取数据的 worker 不在同一节点,data loader 就会长期处于“跨节点等数据”状态。
具体案例:在 8 卡训练机上启动多进程 DataLoader,先用 numactl --interleave=all 将共享数据集的物理页分散到所有节点,再让每个 worker 绑定到对应节点的 CPU 核心。由于每个 worker 都能在本地节点命中大部分页面,训练步骤时间缩短 12%,GPU 利用率提升。
Python 演示
import random LOCAL = 1REMOTE = 4 class NumaSystem: def __init__(self): self.pages = {} def alloc(self, page, node): self.pages[page] = node def access(self, page, cpu_node): owner = self.pages.get(page, 0) return LOCAL if owner == cpu_node else REMOTE def run(strategy): sys = NumaSystem() work = {0: list(range(50)), 1: list(range(50, 100))} total = 0 for cpu, pages in work.items(): for p in pages: if strategy == 'all_node0': sys.alloc(p, 0) elif strategy == 'striped': sys.alloc(p, p % 2) elif strategy == 'first_touch': sys.alloc(p, cpu) total += sys.access(p, cpu) return total if __name__ == '__main__': random.seed(42) t0 = run('all_node0') t1 = run('striped') t2 = run('first_touch') print('all_node0 :', t0) print('striped :', t1) print('first_touch:', t2) print('first_touch 效率提升: %.1f%%' % ((1 - t2 / t0) * 100))
LOCAL = 1 / REMOTE = 4定义本地访问延迟为 1 个单位,远程访问为 4 个单位,模拟 NUMA 的延迟差距。sys.alloc(p, cpu)核心的 first-touch 分配逻辑:页面归首次使用它的 CPU 所在节点所有。LOCAL if owner == cpu_node else REMOTE判断一次访问是本地还是远程,从而累加不同延迟。total += sys.access(p, cpu)把每个 CPU 上所有页面的访问延迟累加,作为整体性能指标。run('first_touch')每个 CPU 只访问自己节点内的页面,所有访问都命中本地内存。print('first_touch 效率提升: %.1f%%' % ((1 - t2 / t0) * 100))输出 first-touch 相对全放节点 0 的性能收益。
常见误区
误区很多人一看到 NUMA 就头疼,干脆把所有内存设置成交错模式(interleave),本以为这样能“平均分配、雨露均沾”,性能应该最好。
正解交错模式确实能够分散内存压力,但它并不理解程序到底在哪里使用数据。对于强局部性的工作负载(如数据库连接、推理 worker),交错反而会把大量访问变成远程访问。正确的做法是先识别应用特征:少量大内存就 interleave 摊薄带宽,大量小请求就 membind 到本地节点。盲目禁止或一刀切,等于把物流园的智能分区改回一个乱糟糟的中央仓库。
为何常见:因为从单 socket 开发环境转过来的工程师,习惯“所有内存都一样”的心智模型,看到交错模式还能“均衡负载”,误以为它是万能解。
误区很多人用 `taskset -c 0` 把进程固定到一个核心上,就觉得“已经绑定了,性能稳了”。实际运行时发现远程访问照样很多。
正解CPU 亲和性只解决了“线程固定”问题,没有解决“内存固定”问题。若页面早已分配在另一个节点,线程即使运行在本地核心上,每次访存仍要跨节点。完整方案是 CPU 和内存双绑定:先保证线程所在节点与数据所在节点一致,例如 numactl --cpunodebind=0 --membind=0,再利用 first-touch 让初始化线程把页面“种”在真正使用它的节点上。
为何常见:因为线程在哪个 CPU 上运行是显眼的、可观测的,而内存页归属隐藏在页表里,肉眼看不到,自然容易被忽略。
误区有些开发者知道内核有 first-touch 后,便放心大胆地写代码,认为“反正页面会自动放到合适的节点”。
正解first-touch 只在页面首次缺页时决定归属。如果先由一个线程把大数组全部初始化为零,之后再由另一个节点上的线程密集读写,那么所有页面早就“属于”初始化线程的节点了,后续访问全是远程。想要用好 first-touch,必须让真正高频访问数据的线程去抢先触碰页面——这是一个需要编程配合的分配策略,而不是自动性能保证。
为何常见:因为“首次接触”听起来像内核的智能功能,大家容易把它和自动页面迁移(AutoNUMA)混为一谈。实际上 AutoNUMA 是事后迁移,first-touch 是事前定位,两者各有局限。
误区很多 Web 后端开发者觉得,自己的服务不是数据库也不是科学计算,NUMA 优化与自己毫无关系。
正解任何运行在多路服务器上的服务都会受影响。高并发的 Redis、Nginx、消息队列,一旦内存页和线程错位,远程访问就会造成毫秒级抖动和偶发超时。现代 CPU 甚至在同一颗芯片内部也存在类似 NUMA 的远近形态。理解 NUMA,不是只为一两类专用软件服务,而是为所有追求稳定低延迟的线上系统打好地基。
为何常见:因为大多数开发者使用云虚拟机时只看到 vCPU,宿主机处理器是单路还是多路、内存如何分布都被虚拟化层屏蔽了,于是“没见过 NUMA 问题”被误认为“不存在 NUMA 问题”。
面试题
Q1. NUMA 中的“非一致”主要指什么?
- A.物理内存总容量不一致
- B.CPU 访问不同位置内存的延迟不一致← 正确答案
- C.磁盘与内存的读取速度不一致
- D.不同 CPU 核心的主频不一致
答案:B。NUMA 的“非一致”指 CPU 访问本地内存比访问远程内存更快,就像分拣员取自家备件屋比取隔壁分区更快。B 正确,其余选项描述的都不是 NUMA 的核心矛盾。
Q2. 一个 NUMA 节点通常包含什么?
- A.一个 CPU 插槽和它直接管理的本地内存← 正确答案
- B.一组 GPU 和显存
- C.一块主板上的所有内存插槽
- D.一块独立的磁盘阵列
答案:A。NUMA 节点 = CPU 插槽 + 内存控制器 + 本地 DRAM,相当于物流园里的“分区备件屋”。GPU 显存和磁盘不参与 NUMA 节点的定义。
Q3. 远程内存访问比本地内存访问慢,主要原因是:
- A.远程内存条频率更低
- B.必须经过节点间互连(QPI/UPI)并产生额外传输开销← 正确答案
- C.远程内存容量小
- D.操作系统强制限速
答案:B。远程访问要把请求打包、经互连链路传输、由对方内存控制器响应、再原路返回,相当于跨传送带取件。内存本身频率未必不同,容量也不是延迟问题的根源。
Q4. 在 Linux 中,用来查看 NUMA 节点内存分配和命中率的工具是:
- A.top
- B.numastat← 正确答案
- C.ps
- D.df
答案:B。numastat 能显示每个节点的内存总览、本地命中与失配情况,是观察“备件屋使用率”的仪表盘。top 侧重 CPU 和进程,ps 看进程状态,df 看磁盘空间。
Q5. first-touch 策略的含义是:
- A.总是把数据存入第一个节点
- B.首次访问某内存页的 CPU 所在节点将持有该页← 正确答案
- C.每次访问都先写入磁盘
- D.页面只能被一个进程访问
答案:B。first-touch 指页面首次缺页时,由哪一个 CPU 触发缺页,页面就分配在那颗 CPU 所在的本地节点。就像“谁先取货,货就放在谁的备件屋”,B 正确。
Q6. 关于 NUMA 亲和性绑定,下列做法正确的有:
- A.只绑定 CPU 核,不绑定内存节点
- B.同时通过 numactl 的 --cpunodebind 和 --membind 绑定← 正确答案
- C.绑定到没有本地内存的节点
- D.开启交换分区并调大 swap 优先级
答案:B。正确做法是 CPU 与内存双绑定,让线程和数据落在同一节点。只绑核不绑内存会留下跨节点隐患;绑定到无本地内存的节点会使所有访问都变远程;swap 属于磁盘交换,与 NUMA 优化无关。
Q7. 多 socket 数据库服务器上出现大量远程访问时,最合理的优化手段是:
- A.增加内存条容量
- B.将连接按 socket 分组并绑定到对应本地内存← 正确答案
- C.禁用超线程
- D.降低 CPU 主频以节省功耗
答案:B。数据库连接可以按 NUMA 节点分组,每个连接组的线程和缓冲池都留在同一节点,能显著提升本地命中率。加内存不解决远近问题,禁超线程和降频只会帮倒忙。
Q8. 关于 QPI/UPI 互连,下列说法正确的有:
- A.QPI 是 Intel 的点对点互连,UPI 是其后继改进版本← 正确答案
- B.互连带宽足够高时,跨节点访问的代价可以完全消除为零
- C.跨节点访问会占用互连带宽,可能影响其他跨节点通信← 正确答案
- D.UPI 主要用于连接显卡
答案:AC。QPI 是初代点对点互连,UPI 是后继演进。任何跨节点消息都要借道互连,因此会争抢带宽。即使互连带宽再高,也不能把多一跳的物理开销归零;UPI 连接 CPU 而非显卡。
Q9. “本地内存命中率”指的是:
- A.本地内存占整机内存的比例
- B.进程在本地节点完成的内存访问次数占总访问次数的比例← 正确答案
- C.CPU 缓存命中次数的比例
- D.磁盘对内存的命中率
答案:B。本地命中率是 NUMA 调优的核心指标,衡量有多少访存没有离开本节点,相当于分拣员在本区备件屋取件的比例。它和缓存命中率、磁盘命中率不是一个层级的概念。
Q10. 在 NUMA 系统中,哪些工具或组合有助于优化内存访问?
- A.numactl --physcpubind 指定运行 CPU← 正确答案
- B.numactl --membind 指定内存节点← 正确答案
- C.把 swap 分区放在高速 NVMe 上代替本地内存
- D.把所有内存都强制分配到一个节点以减少距离
答案:AB。numactl 的 CPU 绑定与内存绑定组合使用,能把线程和数据锁定在同一节点,是标准的 NUMA 优化手段。把内存换到 swap 会使访存退化为磁盘 IO;把所有内存压到一个节点反而制造远程访问和带宽拥塞。