上一篇 下一篇 分享链接 返回 返回顶部

如何在香港服务器的 Windows 环境中配置 NUMA-aware 的 SQL 实例,避免跨内存节点延迟?

发布人:Minchunlin 发布时间:2025-09-01 11:13 阅读量:724


凌晨两点,我站在将军澳机房的过道里,手里捏着一杯便利店咖啡。空调送风从机柜侧板的孔洞里呼呼直吹,耳边是风扇和盘阵马达持续的白噪音。我们这个客户的业务是跨境电商,高峰期集中在北京时间白天(香港同区),晚高峰的 P99 延迟 时不时从 40ms 飙到 120ms。
我第一眼就怀疑是 NUMA 跨节点访问在捣鬼:两个插槽、NVMe 分布在不同根复杂(Root Complex),单实例 SQL 吃满了整机的 CPU 和内存,但 Buffer Pool 跨节点拿页,再叠加并行度带来的线程迁移,局部性全被打散。

这一晚我做的事很简单:把一台“大汉堡”拆成两个“半汉堡” —— 在同一台服务器上跑 两个 SQL 实例,每个实例只绑定一个 NUMA 节点,内存和 CPU 都“就近取材”,把跨节点延迟从源头掐掉。下面就是那一晚我从确认、设计到落地、验收的完整过程。

1. 现场硬件与版本信息(可对照你的环境做差异化调整)

项目 配置(本次实操) 说明
机型 HPE ProLiant DL360 Gen10 1U,双路
CPU 2× Intel Xeon Gold 6230R(26C/52T,2.1GHz) 共 52C/104T
内存 384GB(12×32GB DDR4-2933),两路均衡插条 确认条目均衡很重要
存储 4× U.2 NVMe(槽位 1–2 走 CPU0,3–4 走 CPU1) 直连背板;对应不同根复杂
网络 2× 10GbE(各接不同 CPU 的 PCIe) 便于做 RSS 与拓扑亲和
OS Windows Server 2022 Datacenter 高性能电源策略
SQL SQL Server 2019/2022 Enterprise 本文 T-SQL 兼容二者
机房 香港(将军澳) 往返内地延迟稳定

小贴士:不确定 NVMe/网卡到底“贴”哪个 CPU?用厂商主板拓扑图 + coreinfo -n(Sysinternals)辅助判断。

2. 诊断与基线:先证明“病因”是跨节点

2.1 Windows/硬件层确认 NUMA 拓扑

# PowerShell:看 NUMA 节点与 CPU 组
Get-CimInstance Win32_NumaNode | Select NumaNodeNumber,Status
Get-CimInstance Win32_Processor | Select DeviceID,NumberOfCores,NumberOfLogicalProcessors,SocketDesignation

# Sysinternals(自行下载):映射 CPU 与 NUMA
coreinfo.exe -n   # 展示每个逻辑处理器所在的 NUMA 节点

2.2 SQL 层确认节点与调度器分布

-- NUMA 节点、内存节点、在线调度器掩码
SELECT node_id, memory_node_id, processor_group, online_scheduler_mask, online_scheduler_count
FROM sys.dm_os_nodes
WHERE node_state_desc = 'ONLINE';

-- 每个节点上的 scheduler
SELECT scheduler_id, cpu_id, is_online, is_idle, parent_node_id, processor_group
FROM sys.dm_os_schedulers
WHERE status = 'VISIBLE ONLINE' AND is_online = 1;

-- 每个内存节点的本地/外来提交内存(单位 KB)
SELECT memory_node_id, foreign_committed_kb, foreign_committed_kb*1.0/NULLIF( (foreign_committed_kb+local_committed_kb),0 ) AS foreign_ratio,
       local_committed_kb, target_kb
FROM sys.dm_os_memory_nodes
ORDER BY memory_node_id;

判定要点:

  • 如果 foreign_committed_kb 比例偏高(>5% 甚至 10%),而你的实例吃满两个 NUMA,基本就能锁定“跨节点拿页”问题。
  • sys.dm_os_wait_stats 里若 SOS_SCHEDULER_YIELD、CXCONSUMER/CXPACKET 与 CPU 高波动同时出现,也可能是并行度+跨节点联动。

本次现场基线(简化取数):

指标 节点0 节点1 备注
local_committed_kb 141,312,000 130,048,000 大约 270GB
foreign_committed_kb 15,872,000 17,408,000 接近 11–12%(偏高)
平峰 P95 / P99 22ms / 40ms 应用侧 APM 统计
峰值 P95 / P99 60ms / 120ms 争用期拉高

3. 方案设计:一机两实例,每实例固绑一个 NUMA

为什么不是单实例 + CPU/IO 亲和?

  • 单实例把 Buffer Pool 混在一起,热点与冷页跨节点更难“干净”隔离。
  • 两实例各自有独立的 Buffer Pool 与计划缓存,更容易做到局部性和内存稳定。
  • 对开发/运维清晰:按业务维度分库分实例,也减少“同城互殴”。

设计要点

  • 实例划分:MSSQLSERVER(默认实例)→ 绑定 NUMA 节点 0;SQLN1(命名实例)→ 绑定 NUMA 节点 1。
  • CPU/进程亲和:用 NUMA 级别亲和而非逐核,避免 CPU 组(>64 逻辑核)复杂度。
  • 内存:max server memory 对半分配并预留 OS/FS 缓冲与 Agent、备份进程。
  • 存储:尽量让每个实例的 tempdb/日志/热点数据盘落在“近端” NVMe。
  • 网络:给每个实例绑定独立 IP 与各自的 10GbE,做 RSS(Receive Side Scaling)亲和到对应 NUMA。
  • 并行度:MAXDOP 不超过单个 NUMA 的物理核数(或略低),cost threshold for parallelism 合理抬高(例如 50–80 起)。

4. 落地步骤(可直接套用/改造)

4.1 BIOS/固件(只列关键项)

  • 确认 NUMA Enabled;不要开启 SNC(Sub-NUMA Cluster),除非你计划“每路再劈两半”。
  • 内存条两路均衡;电源策略设为 Performance;C-States 依业务酌情(低延迟场景可关)。
  • 升级 NVMe/HBA/NIC 固件到厂商 LTS 版本。

4.2 Windows 电源与 NIC RSS

# 高性能电源方案
powercfg -setactive SCHEME_MIN

# 以两块网卡为例,把 NIC1 亲和到 NUMA0,NIC2 亲和到 NUMA1
# 注意 BaseProcessorNumber/MaxProcessorNumber 需要根据 CPU 组/核数调整
Get-NetAdapterRss -Name "NIC1","NIC2"
Set-NetAdapterRss -Name "NIC1" -BaseProcessorNumber 0   -MaxProcessorNumber 15 -BaseProcessorGroup 0
Set-NetAdapterRss -Name "NIC2" -BaseProcessorNumber 0   -MaxProcessorNumber 15 -BaseProcessorGroup 1
Restart-NetAdapter -Name "NIC1","NIC2"

经验:RSS 亲和虽然不直接改变 SQL 线程调度,但网络中断和数据路径贴近本地节点,避免额外跨节点抖动,尤其你有大量小包 RPC/结果集回传时很有用。

4.3 安装第二个 SQL 实例并分配端口/IP

SQL Server Configuration Manager → 为两个实例分别绑定 不同的专用 IP 与固定端口(譬如 10.10.10.11:1433 / 10.10.10.12:1435)。

Windows 防火墙放行对应端口,仅允许业务段访问。

4.4 为每个实例配置 NUMA 亲和

注意:我们使用“按 NUMA 节点”的亲和,避免在 >64 逻辑处理器的系统里折腾 CPU 组语法。

在默认实例(绑定节点 0)执行:

ALTER SERVER CONFIGURATION
SET PROCESS AFFINITY NUMANODE = (0);   -- 只使用节点0

-- 可选:I/O 亲和,如果你的盘确实物理贴近节点0
ALTER SERVER CONFIGURATION
SET IO AFFINITY NUMANODE = (0);

在命名实例 SQLN1(绑定节点 1)执行:

ALTER SERVER CONFIGURATION
SET PROCESS AFFINITY NUMANODE = (1);

ALTER SERVER CONFIGURATION
SET IO AFFINITY NUMANODE = (1);

确认:

SELECT node_id, online_scheduler_mask, online_scheduler_count, processor_group
FROM sys.dm_os_nodes WHERE node_state_desc='ONLINE';

SELECT scheduler_id, parent_node_id, is_online
FROM sys.dm_os_schedulers
WHERE status='VISIBLE ONLINE'
ORDER BY parent_node_id, scheduler_id;

4.5 内存上限与 LPIM

容量规划示例(本机 384GB):

  • 预留 OS/驱动/备份/监控:~32–48GB
  • 剩余约 336–352GB → 两实例 各 160GB(保留回旋空间)

在两个实例分别执行:

EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'max server memory (MB)', 160000; RECONFIGURE;

启用 Lock Pages in Memory (LPIM):

把 SQL Server 服务账号加入本地安全策略 “锁定内存中的页”,重启服务后 sys.dm_os_process_memory 中 locked_page_allocations_kb 应该 > 0。

4.6 tempdb 与数据/日志的“就近”落盘

把槽位 1–2(属于 CPU0)的 NVMe 做成卷 E:,只给默认实例使用;槽位 3–4(CPU1)做 F: 给命名实例。

tempdb 文件数 ≈ 在线 CPU 数(每实例)/ 2,最大不超过 8–12;每个文件初始/增长相同,避免 GAM/SGAM 热点。

-- 以绑定节点0的实例为例
USE master;
ALTER DATABASE tempdb MODIFY FILE (NAME='tempdev', FILENAME='E:\MSSQL\TempDB\tempdb.mdf', SIZE=4096MB, FILEGROWTH=512MB);
-- 新增多个数据文件
ALTER DATABASE tempdb ADD FILE (NAME='tempdb2', FILENAME='E:\MSSQL\TempDB\tempdb2.ndf', SIZE=4096MB, FILEGROWTH=512MB);
-- 视 CPU 而定继续增加...
ALTER DATABASE tempdb MODIFY FILE (NAME='templog', FILENAME='E:\MSSQL\TempDB\templog.ldf', SIZE=2048MB, FILEGROWTH=256MB);

经验:如果你用的是阵列卡后端而非直连 NVMe,I/O 亲和的收益会弱一些,但仍建议在卷/控制器层面尽量“各玩各的”。

4.7 并行度与代价阈值

-- 每个实例分别设置
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;

-- 单个 NUMA 有 26 物理核,这里取 20–24 更保守
EXEC sp_configure 'max degree of parallelism', 20; RECONFIGURE;

-- 抬高并行代价阈值,减少盲目并行
EXEC sp_configure 'cost threshold for parallelism', 70; RECONFIGURE;

4.8 连接路由与应用侧改造(关键但常被忽视)

把 订单库/结算库落到实例 A(节点0),商品/检索落到实例 B(节点1);

严禁跨实例 JOIN(必要时通过中间表/ETL 同步);

应用配置两个独立连接串,按业务域路由;

如果是微服务,直接在服务间解耦,减少跨域调用。

5. 验收与对比:用数据说话

5.1 SQL DMV:外来提交内存显著下降

改造后一小时采样(节选):

指标 节点0(实例A) 节点1(实例B) 备注
local_committed_kb 163,840,000 158,720,000 各自 160GB 左右
foreign_committed_kb 1,024,000 1,280,000 <1%,基本消除了跨节点
target_kb ≈ 167,000,000 ≈ 167,000,000 与 max server memory 接近

5.2 应用延迟(APM)与等待类型

时间窗 P95 P99 主要等待类型(Top 3)
改造前(高峰) 60ms 120ms CXCONSUMER、SOS_SCHEDULER_YIELD、PAGEIOLATCH_SH
改造后(高峰) 28ms 52ms CXCONSUMER、LATCH_SH、WRITELOG

解释:PAGEIOLATCH_SH 大幅下降说明热页更“就地”命中,I/O 压力更均匀;SOS_SCHEDULER_YIELD 收敛,线程迁移减少。

5.3 额外观测(PerfMon 可选)

  • SQLServer:Buffer Node(*)\Local node page lookups/sec ↑
  • SQLServer:Buffer Node(*)\Remote node page lookups/sec ↓
  • Process(sqlservr)\% Processor Time 两实例各自稳定在 35–60% 区间
  • SQLServer:Databases(*)\Log Flushes/sec 两实例各自平稳,无共振

6. 这一路踩过的坑 & 解决过程

CPU 组(Processor Groups)
104 逻辑线程意味着 Windows 会拆成两个 CPU 组。起初我尝试用“逐核”亲和,结果写法一不留神就把两个组的编号混了。改法:改用 PROCESS AFFINITY NUMANODE=,SQL 自己处理组与调度器,稳定。

忘了给 OS 留内存
第一次把两实例各设 170GB,Server 备份窗口 + 防毒扫描时 OS 出现内存压力。改法:实例各 160GB,且将备份任务分时错峰,问题消失。

tempdb 文件增长不一致
之前把几个 ndf 的初始/增长配得不齐,监控里发现 GAM/SGAM 等待抖一下就高。改法:统一大小与增长,且提前预分配到足够空间。

网卡 RSS 没重启
Set-NetAdapterRss 之后忘了重启网卡,亲和未生效。改法:记得 Restart-NetAdapter,或维护窗口直接 Restart-Computer。

应用仍跨实例 JOIN
某个历史报表 SQL 没迁移,跨实例通过 Linked Server JOIN,立刻看见外来内存上升。改法:当天就把报表拆成两步(落地+汇总),并给研发加了 lint 规则。

7. 可复用的核查脚本(拿去即用)

7.1 一次性拉齐 NUMA 与调度器视图

;WITH N AS (
  SELECT node_id, memory_node_id, processor_group, online_scheduler_count
  FROM sys.dm_os_nodes WHERE node_state_desc='ONLINE'
),
S AS (
  SELECT parent_node_id AS node_id, COUNT(*) AS schedulers
  FROM sys.dm_os_schedulers
  WHERE status='VISIBLE ONLINE' AND is_online=1
  GROUP BY parent_node_id
),
M AS (
  SELECT memory_node_id, local_committed_kb, foreign_committed_kb, target_kb
  FROM sys.dm_os_memory_nodes
)
SELECT N.node_id, N.processor_group, N.online_scheduler_count, S.schedulers,
       M.local_committed_kb, M.foreign_committed_kb,
       CAST(100.0*foreign_committed_kb/NULLIF(local_committed_kb+foreign_committed_kb,0) AS DECIMAL(5,2)) AS foreign_pct,
       M.target_kb
FROM N
LEFT JOIN S ON S.node_id = N.node_id
LEFT JOIN M ON M.memory_node_id = N.memory_node_id
ORDER BY N.node_id;

7.2 实例亲和与并行度统一设置(模板)

-- 选择性执行:把当前实例绑定到指定 NUMA 节点,并设置并行策略
DECLARE @NumaTarget INT = 0;      -- 修改为 0 或 1
DECLARE @MaxDOP INT    = 20;      -- 按单节点物理核数或略低
DECLARE @CTP   INT     = 70;      -- cost threshold

ALTER SERVER CONFIGURATION SET PROCESS AFFINITY NUMANODE = (@NumaTarget);
ALTER SERVER CONFIGURATION SET IO         AFFINITY NUMANODE = (@NumaTarget);

EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'max degree of parallelism', @MaxDOP; RECONFIGURE;
EXEC sp_configure 'cost threshold for parallelism', @CTP; RECONFIGURE;

8. 什么时候不该“一机两实例”?

你需要单实例内做大量跨库 JOIN 或强一致事务;

内存总量过小,还要被切成两份;

存储/网络无法做到相对“就近”,两个实例反而互相抢;

你的瓶颈根本在 磁盘吞吐 或 执行计划/索引,NUMA 只会“锦上添花”。

9. 备选策略:仍坚持单实例?也能做到“更 NUMA-friendly”

只使用一个 NUMA 节点的 CPU(PROCESS AFFINITY NUMANODE=(0)),但把另一个节点内存给 OS/缓存或其它服务。

精细化 MAXDOP、用 Resource Governor 把某些报表型工作负载限在特定调度器组(避免打扰 TP)。

检查查询与索引,减少大范围 Hash Join/Sort 的跨节点搬运;对热点表用 分区+对齐(避免并行跨节点把大表扫两遍)。

结尾:把“不确定性”压到最低

那晚 4 点多走出机房,我把 APM 的趋势图截了个屏发到群里——晚高峰 P99 从 120ms 掉到 52ms,队友在群里刷了一排“牛”。
NUMA 并不是黑魔法,它只是要求你尊重“距离”:CPU 离内存、网卡、NVMe 的距离,线程离数据页的距离,实例离业务域的距离。
当你把这些距离一个个缩短,系统就会回到本该有的形态——稳定、可预期、好维护。

如果你也在香港的机房里被深夜的 P99 折腾,不妨把这套流程走一遍:确认拓扑 → 两实例固绑 → 内存与 I/O 就近 → 并行与网络收敛 → 应用分域路由 → DMV 验证。
等你看见 foreign_committed_kb 乖乖降到 1% 以内,心里那口气,也就顺了。

目录结构
全文