第一百八十九章:可扩展性
大乘期涉及内核源码:
一
离开讲道堂,林小源来到一座庞大的数据中心。数据中心内排列着无数机柜,每个机柜中都有多个CPU在高速运转。机柜之间用光纤相连,数据在其中川流不息。
一位身穿蓝色工装的工程师站在控制台前,面前的屏幕上显示着各种监控指标。
"欢迎来到可扩展性的世界。"工程师头也不抬,"我是调度器的守护者。当系统只有一个CPU时,一切都简单。但当CPU数量增长到几十、几百、几千个时,曾经不是问题的问题都会变成瓶颈。"
"什么瓶颈?"
工程师终于抬起头。"锁竞争。缓存一致性。NUMA距离。每多一个CPU,共享数据的争抢就多一分。全局锁就像一条独木桥——10个人过桥没问题,1000个人过桥就堵死了。"
二
"解决方案是什么?"林小源追问。
工程师走到一个机柜前,打开柜门。"Per-CPU数据。每个CPU有自己的数据副本,互不干扰。统计计数器、运行队列、内存缓存——能Per-CPU化的,全部Per-CPU化。"
他又走到另一排机柜前。"NUMA感知。大型服务器有多个NUMA节点,每个节点有自己的本地内存。访问本地内存快,访问远程内存慢——可能差三到五倍。内核的内存分配器会尽量让CPU访问本地内存。"
"还有RCU和无锁数据结构——前面你已经学过了。减少锁竞争,是可扩展性的核心。"
三
"怎么衡量可扩展性?"林小源问道。
工程师回到控制台,指着屏幕上的三条曲线。"三个指标:吞吐量——每秒处理多少请求;延迟——单个请求处理多久;并发——同时能处理多少请求。理想的可扩展性是:CPU数量翻倍,吞吐量也翻倍。但现实中,由于锁竞争和缓存一致性开销,很难做到线性扩展。"
"所以可扩展性是一种持续的优化?"
"没错。"工程师关上柜门,"内核开发者花了二十年优化可扩展性——从BKL(大内核锁)到细粒度锁,从全局数据到Per-CPU数据,从单一运行队列到每CPU运行队列。这条路没有终点,因为硬件在不断进化,新的瓶颈会不断出现。"
/*
* 内核可扩展性:
*
* 1. Per-CPU 数据
* 每个 CPU 有自己的数据副本
* 避免缓存颠簸
* 减少锁竞争
*
* 2. RCU
* 读取无锁
* 读多写少场景
*
* 3. 无锁数据结构
* CAS 操作
* 减少锁竞争
*
* 4. NUMA 感知
* 本地内存访问
* 减少跨节点访问
*
* 5. 工作队列
* 分散工作到多个 CPU
*
* 6. 中断亲和性
* 中断绑定到特定 CPU
*
* 可扩展性指标:
* 吞吐量 — 每秒处理请求数
* 延迟 — 单个请求处理时间
* 并发 — 同时处理请求数
*/
/* 模拟 Per-CPU 数据 */
struct percpu_counter {
long count;
long local[4]; /* 模拟 4 个 CPU */
};
void percpu_init(struct percpu_counter *c) {
c->count = 0;
for (int i = 0; i < 4; i++)
c->local[i] = 0;
}
void percpu_add(struct percpu_counter *c, int cpu, long val) {
c->local[cpu] += val;
}
long percpu_read(struct percpu_counter *c) {
long sum = c->count;
for (int i = 0; i < 4; i++)
sum += c->local[i];
return sum;
}
printf("=== 可扩展性 — 处理增长的负载 ===\n\n");
/* Per-CPU 计数器 */
struct percpu_counter counter;
percpu_init(&counter);
printf("--- Per-CPU 数据 ---\n");
percpu_add(&counter, 0, 10);
percpu_add(&counter, 1, 20);
percpu_add(&counter, 2, 30);
percpu_add(&counter, 3, 40);
printf("总计: %ld\n", percpu_read(&counter));
printf("\n--- 可扩展性机制 ---\n");
printf("1. Per-CPU 数据:\n");
printf(" 每个 CPU 有自己的副本\n");
printf(" 避免缓存颠簸\n");
printf(" 减少锁竞争\n\n");
printf("2. RCU:\n");
printf(" 读取无锁\n");
printf(" 读多写少场景\n\n");
printf("3. 无锁数据结构:\n");
printf(" CAS 操作\n");
printf(" 减少锁竞争\n\n");
printf("4. NUMA 感知:\n");
printf(" 本地内存访问\n");
printf(" 减少跨节点访问\n\n");
printf("--- 可扩展性指标 ---\n");
printf("吞吐量:\n");
printf(" 每秒处理请求数\n\n");
printf("延迟:\n");
printf(" 单个请求处理时间\n\n");
printf("并发:\n");
printf(" 同时处理请求数\n\n");
printf("--- 可扩展性挑战 ---\n");
printf("1. 锁竞争:\n");
printf(" 全局锁限制扩展\n");
printf(" 解决: 细粒度锁、Per-CPU\n\n");
printf("2. 缓存一致性:\n");
printf(" 多核缓存同步\n");
printf(" 解决: 减少共享数据\n\n");
printf("3. NUMA 距离:\n");
printf(" 跨节点访问慢\n");
printf(" 解决: 本地化数据\n\n");
printf("--- 内核优化 ---\n");
printf("调度器:\n");
printf(" Per-CPU 运行队列\n");
printf(" NUMA 感知\n\n");
printf("内存分配:\n");
printf(" Per-CPU 缓存\n");
printf(" 伙伴系统\n\n");
printf("网络:\n");
printf(" 多队列网卡\n");
printf(" RPS/RFS\n");#include <stdio.h>
/*
* 内核可扩展性:
*
* 1. Per-CPU 数据
* 每个 CPU 有自己的数据副本
* 避免缓存颠簸
* 减少锁竞争
*
* 2. RCU
* 读取无锁
* 读多写少场景
*
* 3. 无锁数据结构
* CAS 操作
* 减少锁竞争
*
* 4. NUMA 感知
* 本地内存访问
* 减少跨节点访问
*
* 5. 工作队列
* 分散工作到多个 CPU
*
* 6. 中断亲和性
* 中断绑定到特定 CPU
*
* 可扩展性指标:
* 吞吐量 — 每秒处理请求数
* 延迟 — 单个请求处理时间
* 并发 — 同时处理请求数
*/
/* 模拟 Per-CPU 数据 */
struct percpu_counter {
long count;
long local[4]; /* 模拟 4 个 CPU */
};
void percpu_init(struct percpu_counter *c) {
c->count = 0;
for (int i = 0; i < 4; i++)
c->local[i] = 0;
}
void percpu_add(struct percpu_counter *c, int cpu, long val) {
c->local[cpu] += val;
}
long percpu_read(struct percpu_counter *c) {
long sum = c->count;
for (int i = 0; i < 4; i++)
sum += c->local[i];
return sum;
}
int main() {
printf("=== 可扩展性 — 处理增长的负载 ===\n\n");
/* Per-CPU 计数器 */
struct percpu_counter counter;
percpu_init(&counter);
printf("--- Per-CPU 数据 ---\n");
percpu_add(&counter, 0, 10);
percpu_add(&counter, 1, 20);
percpu_add(&counter, 2, 30);
percpu_add(&counter, 3, 40);
printf("总计: %ld\n", percpu_read(&counter));
printf("\n--- 可扩展性机制 ---\n");
printf("1. Per-CPU 数据:\n");
printf(" 每个 CPU 有自己的副本\n");
printf(" 避免缓存颠簸\n");
printf(" 减少锁竞争\n\n");
printf("2. RCU:\n");
printf(" 读取无锁\n");
printf(" 读多写少场景\n\n");
printf("3. 无锁数据结构:\n");
printf(" CAS 操作\n");
printf(" 减少锁竞争\n\n");
printf("4. NUMA 感知:\n");
printf(" 本地内存访问\n");
printf(" 减少跨节点访问\n\n");
printf("--- 可扩展性指标 ---\n");
printf("吞吐量:\n");
printf(" 每秒处理请求数\n\n");
printf("延迟:\n");
printf(" 单个请求处理时间\n\n");
printf("并发:\n");
printf(" 同时处理请求数\n\n");
printf("--- 可扩展性挑战 ---\n");
printf("1. 锁竞争:\n");
printf(" 全局锁限制扩展\n");
printf(" 解决: 细粒度锁、Per-CPU\n\n");
printf("2. 缓存一致性:\n");
printf(" 多核缓存同步\n");
printf(" 解决: 减少共享数据\n\n");
printf("3. NUMA 距离:\n");
printf(" 跨节点访问慢\n");
printf(" 解决: 本地化数据\n\n");
printf("--- 内核优化 ---\n");
printf("调度器:\n");
printf(" Per-CPU 运行队列\n");
printf(" NUMA 感知\n\n");
printf("内存分配:\n");
printf(" Per-CPU 缓存\n");
printf(" 伙伴系统\n\n");
printf("网络:\n");
printf(" 多队列网卡\n");
printf(" RPS/RFS\n");
return 0;
}道藏笔记
内核启示
可扩展性是系统处理增长负载的能力。
可扩展性机制:
- Per-CPU 数据 — 避免缓存颠簸
- RCU — 读取无锁
- 无锁数据结构 — 减少锁竞争
- NUMA 感知 — 本地内存访问
可扩展性指标:
- 吞吐量 — 每秒请求数
- 延迟 — 请求处理时间
- 并发 — 同时请求数
挑战:
- 锁竞争
- 缓存一致性
- NUMA 距离
可扩展性是保障——系统在增长中保持高效。
可扩展性之试
可扩展性一章讨论读多写少、避免全局锁瓶颈时,代表性机制是什么?