Skip to content

第一百八十九章:可扩展性

大乘期

涉及内核源码:

离开讲道堂,林小源来到一座庞大的数据中心。数据中心内排列着无数机柜,每个机柜中都有多个CPU在高速运转。机柜之间用光纤相连,数据在其中川流不息。

一位身穿蓝色工装的工程师站在控制台前,面前的屏幕上显示着各种监控指标。

"欢迎来到可扩展性的世界。"工程师头也不抬,"我是调度器的守护者。当系统只有一个CPU时,一切都简单。但当CPU数量增长到几十、几百、几千个时,曾经不是问题的问题都会变成瓶颈。"

"什么瓶颈?"

工程师终于抬起头。"锁竞争。缓存一致性。NUMA距离。每多一个CPU,共享数据的争抢就多一分。全局锁就像一条独木桥——10个人过桥没问题,1000个人过桥就堵死了。"

"解决方案是什么?"林小源追问。

工程师走到一个机柜前,打开柜门。"Per-CPU数据。每个CPU有自己的数据副本,互不干扰。统计计数器、运行队列、内存缓存——能Per-CPU化的,全部Per-CPU化。"

他又走到另一排机柜前。"NUMA感知。大型服务器有多个NUMA节点,每个节点有自己的本地内存。访问本地内存快,访问远程内存慢——可能差三到五倍。内核的内存分配器会尽量让CPU访问本地内存。"

"还有RCU和无锁数据结构——前面你已经学过了。减少锁竞争,是可扩展性的核心。"

"怎么衡量可扩展性?"林小源问道。

工程师回到控制台,指着屏幕上的三条曲线。"三个指标:吞吐量——每秒处理多少请求;延迟——单个请求处理多久;并发——同时能处理多少请求。理想的可扩展性是:CPU数量翻倍,吞吐量也翻倍。但现实中,由于锁竞争和缓存一致性开销,很难做到线性扩展。"

"所以可扩展性是一种持续的优化?"

"没错。"工程师关上柜门,"内核开发者花了二十年优化可扩展性——从BKL(大内核锁)到细粒度锁,从全局数据到Per-CPU数据,从单一运行队列到每CPU运行队列。这条路没有终点,因为硬件在不断进化,新的瓶颈会不断出现。"

c
/*
 * 内核可扩展性:
 *
 * 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");

道藏笔记

内核启示

可扩展性是系统处理增长负载的能力。

可扩展性机制:

  • Per-CPU 数据 — 避免缓存颠簸
  • RCU — 读取无锁
  • 无锁数据结构 — 减少锁竞争
  • NUMA 感知 — 本地内存访问

可扩展性指标:

  • 吞吐量 — 每秒请求数
  • 延迟 — 请求处理时间
  • 并发 — 同时请求数

挑战:

  • 锁竞争
  • 缓存一致性
  • NUMA 距离

可扩展性是保障——系统在增长中保持高效。


破关试炼

可扩展性之试

可扩展性一章讨论读多写少、避免全局锁瓶颈时,代表性机制是什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道