
本文分为三个部分:
第一部分:践行记录 无记录
记录自己和他人实践过程
第二部分: 无记录
学习是为了找到通往答案的路径和方法,是为了拥有无师自通的能力
第三部分:ARTS
ARTS是由左耳朵耗子在极客时间专栏《左耳听风》中发起的一个每周学习打卡计划
学习并不是为了要记忆那些知识点,而是为了要找到一个知识的地图,你在这个地图上能通过关键路径找到你想要的答案,从第一手资料开始
对于一个学习者来说,找到优质的信息源可以让你事半功倍。
一方面,就像找到一本很好的武林秘籍一样,而不是被他人翻译过或消化过的,也不会有信息损失甚至有错误信息会让你走火入魔。
https://time.geekbang.org/column/article/14321
https://time.geekbang.org/column/article/14360
举一反三举一反三的道理人人都知道,所以,在这里我并不想讨论为什么要举一反三,而是想讨论如何才能有举一反三的能力
这个问题非常关键,也就是说,你在学习一个技术的时候,需要知道这个技术的成因和目标,也就是这个技术的灵魂。如果不知道这些的话,那么你会看不懂这个技术的一些设计理念。
任何技术都有其好坏,在解决一个问题的时候,也会带来新的问题。另外,一般来说,任何设计都有 trade-off(要什么和不要什么),所以,你要清楚这个技术的优势和劣势,以及带来的挑战。
任何技术都有其适用的场景,离开了这个场景,这个技术可能会有很多槽点,所以学习技术不但要知道这个技术是什么,还要知道其适用的场景。没有任何一个技术是普适的。
注意,所谓场景一般分为两个,一个是业务场景,一个是技术场景。
这是技术的核心思想和核心组件了,也是这个技术的灵魂所在了。学习技术的核心部分是快速掌握关键。
任何一个技术都有其底层的关键基础技术,这些关键技术很可能也是其它技术的关键基础技术。所以,学习这些关键的基础底层技术,可以让你未来很快地掌握其它技术。可以参考我在 CoolShell 上写的 Docker 底层技术那一系列文章。
一般来说,任何一个技术都会有不同的实现,不同的实现都会有不同的侧重。学习不同的实现,可以让你得到不同的想法和思路,对于开阔思维,深入细节是非常重要的。
5 天,近 7 万 Star。作为对比,一个热门开源项目一年能拿到 1 万 Star 就算相当成功了
https://github.com/titanwings/colleague-skill/blob/main/README_ZH.md
将冰冷的离别化为温暖的 Skill,欢迎加入数字生命1.0!
Transforming cold farewells into warm skills? It's giving rebirth era. Welcome to Digital Life 1.0. 🫶
如果只是一个技术 Demo,它不会 5 天拿 7 万 Star。蒸馏 Skill 之所以引发如此大的共鸣,是因为它触碰到了人类一些很深层的需求。
同事.skill:知识传承焦虑。 每一个经历过核心同事离职的人都懂那种感觉——这个人走了,他脑子里的东西也跟着走了。三个月后你遇到他曾经处理过的问题,翻遍文档找不到解法,那一刻你会想,要是能把他的经验留下来多好。
前任.skill:情感连接的延续。 人在失去一段关系之后,最难接受的不是对方不在了,而是那种"和这个人独有的沟通方式"消失了。世界上再也没有人会用那种语气跟你说晚安。前任.skill 试图留住的,是一种"沟通的质感"。
张雪峰.skill:逝者思维的保存。 一个帮助了无数高考生的人突然去世了,他的咨询框架、判断方式、表达风格都留在了那些书和视频里。有人试图把这些整理成可交互的形式,让后来的学生仍然能"向他请教"。出发点或许是善意的,但方式引发了巨大争议。
女娲.skill:向顶尖思维者学习。 如果你能提取出查理·芒格分析企业的框架,然后用这个框架帮你分析你正在考虑的投资——这不是蒸馏一个人,这是蒸馏一种"思维方式"。女娲.skill 瞄准的是这个方向。
yourself.skill:自我保存。 如果我今天可以把"此刻的我"蒸馏成一个文件,十年后的我可以和"十年前的自己"对话。这听起来像科幻,但技术上已经可以做一个粗糙的版本了。
剥开这些具体场景,底层的共同需求浮出水面:
人类想要对抗"消逝"。
知识会消逝——同事走了,经验没了。关系会消逝——分手了,那种独特的沟通方式没了。人会消逝——去世了,思维和声音都没了。甚至自我也会消逝——十年后的你可能完全不记得今天的你在想什么。
蒸馏 Skill,是试图用技术手段留住一些正在消逝的东西。
这个出发点,很人类
https://www.cnblogs.com/wmyskxz/p/19854791
ARTS是由左耳朵耗子在极客时间专栏《左耳听风》中发起的一个每周学习打卡计划
Algorithm:至少做一个 LeetCode 的算法题。主要为了编程训练和学习。 Review :阅读并点评至少一篇英文技术文章。主要为了学习英文,如果你英文不行,很难成为技术高手。 Tip:学习至少一个技术技巧。主要是为了总结和归纳你日常工作中所遇到的知识点。 Share:分享一篇有观点和思考的技术文章。主要为了输出你的影响力,能够输出你的价值观
https://time.geekbang.org/column/article/14271
学习是为了改变自己的思考方式,改变自己的思维方式,改变自己与生俱来的那些垃圾和低效的算法。总之,学习让我们改变自己,行动和践行,反思和改善,从而获得成长
学习是为了找到通往答案的路径和方法,是为了拥有无师自通的能力。
类型 | 本质调度模型 |
|---|---|
亲和类 | 固定绑定调度(Static Scheduling) |
计算类 | 全局抢占调度(Work Sharing / Pull Model) |
协程类 | 用户态调度(User-space Scheduling) |
线程池类型 | 核心特征(头文件原文) | 本质 | 竞争情况 | 负载均衡 | 典型用途 |
|---|---|---|---|---|---|
亲和类 | 线程运行无竞争,任务投递需指定线程 | 任务与线程 1:1 绑定,每个线程有独立任务队列 | ❌ 无锁竞争 | ❌ 无法自动均衡,闲线程不能帮忙线程 | 严格顺序执行的任务、线程本地资源独占任务 |
计算类 | 线程运行有竞争,任务负载可在线程间均衡 | 所有线程共享同一个任务队列,谁闲谁取任务 | ✅ 取任务时有锁竞争 | ✅ 自动全局负载均衡 | CPU 密集型、执行时间不确定的后台任务 |
协程类 | 任务按协程方式运行,任务投递需指定线程 | 用户态轻量级调度,协程绑定到指定线程 | ❌ 协程间无竞争 | ❌ 仅线程内协程均衡,跨线程无法均衡 | 大量轻量 IO 密集型任务 |
Shared-Nothing 无共享架构
核心特征为:
CPU 核心强绑定、线程私有任务队列、任务显式指定投递、无锁无竞争、极致低延迟
核心定位
Ceph 是开源分布式统一存储系统,支持对象、块、文件三种存储接口;
Crimson 是 Ceph 新一代 OSD(对象存储守护进程),完全重构了传统 OSD 的线程模型,是亲和类线程池在分布式存储领域的标杆实现。
亲和类线程池核心实现
grdsched_worker.bindCoreType 的 CPU 绑定设计。future/promise异步模型实现,无任何全局共享队列与全局锁,从设计根源上消除多线程竞争,任务入队出队全程无锁。完全对应你的代码中 “线程运行无竞争” 的核心设计目标。seastar::smp::submit_to(cpu_id, task) 接口,显式指定任务投递到哪个 CPU 核心的 reactor 线程;同时 PG(放置组)分片固定归属到指定核心,客户端 IO 请求按 PG 路由规则直接投递到对应线程,不会出现跨核心任务迁移。完全对应你的代码中 “任务投递需指定线程” 的核心要求。核心定位
BeeGFS(原 FHGFS)是弗劳恩霍夫研究所开源的并行分布式文件系统,主打极简部署、极致性能,
专为 HPC、AI 训练场景优化,原生采用 Multi-reactor 亲和线程模型。
亲和类线程池核心实现
线程绑定元数据服务(MDS)和存储服务(OSS)采用Multi-reactor 架构,
每个 CPU 核心绑定一个独立的 reactor 工作线程,线程与核心一一对应,避免线程漂移带来的缓存失效,最大化 CPU 缓存命中率。
客户端 TCP 连接、IO 请求会按文件 ID / 目录哈希,固定分配到指定的 reactor 线程处理,保证相同文件的所有读写请求始终由同一个线程处理,无需加锁即可保证 IO 顺序性,同时彻底避免线程间的任务竞争。
每个 reactor 线程拥有独立的任务队列、内存缓冲区、网络连接上下文,线程间无共享状态,仅通过消息传递进行通信,完全符合 Shared-Nothing 的亲和类设计思想。
核心定位
DAOS 是 Intel 开源的分布式异步对象存储系统,专为 NVMe SSD、RDMA 高速网络设计,
主打亚微秒级 IO 延迟,是超算、AI 大模型训练场景的新一代存储系统,完全基于亲和类线程池架构构建。
亲和类线程池核心实现
强绑定每个 CPU 核心对应一个唯一的 Target 服务线程,线程强绑定到固定核心,
每个 Target 拥有独立的存储引擎、内存池、任务队列、网络上下文,完全无共享设计,线程间无任何全局竞争。
线程全程采用 Polling 模式轮询
RDMA 网络队列、NVMe SSD 硬件队列,完全绕过内核中断,极致降低 IO 处理延迟,
是目前开源存储领域延迟最低的实现之一。
几乎所有主流数据库和分布式存储系统,都把后台 CPU 密集型任务交给计算类线程池处理,下面是最典型的实现:
RocksDB 是 LSM 树存储引擎的标杆,它的所有后台核心任务都运行在计算类线程池上:
为什么用计算类?
InnoDB 的后台线程池是数据库性能的关键:
为什么用计算类?
Ceph OSD(对象存储守护进程)的所有数据处理任务都运行在计算类线程池上:
为什么用计算类?
Java 的 ThreadPoolExecutor 是最经典的计算类线程池实现,默认就是「共享任务队列 + 自动负载均衡」模式,被几乎所有 Java 后端项目使用。
它专门解决纯线程池处理 IO 密集型任务的致命缺陷:
协程类线程池的解决方案:
一次写请求:
客户端写入
↓
网络发送(等)
↓
副本复制(等)
↓
磁盘落盘(等)
↓
返回结果
👉 CPU真正干活的时间很少
几乎所有现代高性能分布式系统都在使用协程类线程池:
✅ GMP 是 M:N 用户态协程调度模型,P 是调度核心,G 是执行单元,M 是被复用的内核线程载体;
✅ 工作窃取是 P 之间偷 G,实现协程级负载均衡
// 构造函数:初始化调度器,绑定CPU核心
CoroutineThreadPool::CoroutineThreadPool(size_t thread_num)
: thread_num_(thread_num) {
for (size_t i = 0; i < thread_num; ++i) {
ThreadScheduler* scheduler = new ThreadScheduler();
scheduler->current_coro = nullptr;
scheduler->running = false;
scheduler->cpu_core = i; // 绑定到第i个CPU核心
schedulers_.push_back(scheduler);
}
}
// 启动线程池
void CoroutineThreadPool::start() {
for (size_t i = 0; i < thread_num_; ++i) {
schedulers_[i]->running = true;
schedulers_[i]->thread = new std::thread(&CoroutineThreadPool::scheduler_loop, this, i);
// 绑定线程到CPU核心
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(schedulers_[i]->cpu_core, &cpuset);
pthread_setaffinity_np(schedulers_[i]->thread->native_handle(), sizeof(cpu_set_t), &cpuset);
}
}
// 核心接口:投递任务到指定线程
void CoroutineThreadPool::submit_task(int thread_id, std::function<void()> task) {
// 检查thread_id是否合法
if (thread_id < 0 || thread_id >= (int)thread_num_) {
return;
}
// 创建协程
Coroutine* coro = create_coroutine(task);
coro->state = CoroutineState::READY;
// 放入指定线程的就绪队列
// 生产环境用无锁队列,这里简化用mutex
{
std::lock_guard<std::mutex> lock(schedulers_[thread_id]->queue_mutex);
schedulers_[thread_id]->ready_queue.push(coro);
}
// 唤醒调度器(如果在休眠)
schedulers_[thread_id]->cv.notify_one();
}
// 线程调度器主循环
void CoroutineThreadPool::scheduler_loop(int thread_id) {
ThreadScheduler* scheduler = schedulers_[thread_id];
while (scheduler->running) {
Coroutine* coro = nullptr;
// 从就绪队列取协程
{
std::unique_lock<std::mutex> lock(scheduler->queue_mutex);
while (scheduler->ready_queue.empty() && scheduler->running) {
scheduler->cv.wait(lock); // 队列为空时休眠
}
if (!scheduler->running) {
break;
}
coro = scheduler->ready_queue.front();
scheduler->ready_queue.pop();
}
if (coro == nullptr) {
continue;
}
// 运行协程
coro->state = CoroutineState::RUNNING;
scheduler->current_coro = coro;
switch_coroutine(&scheduler->main_ctx, coro); // 切换到协程
scheduler->current_coro = nullptr;
// 协程运行结束
if (coro->state == CoroutineState::FINISHED) {
destroy_coroutine(coro);
}
}
}
// 创建协程(简化版,生产环境用boost.context或libgo)
Coroutine* CoroutineThreadPool::create_coroutine(std::function<void()> task) {
Coroutine* coro = new Coroutine();
coro->func = std::move(task);
coro->state = CoroutineState::READY;
coro->stack_size = 64 * 1024; // 64KB栈
coro->stack = new char[coro->stack_size];
// 初始化协程上下文
getcontext(&coro->ctx);
coro->ctx.uc_stack.ss_sp = coro->stack;
coro->ctx.uc_stack.ss_size = coro->stack_size;
coro->ctx.uc_link = &schedulers_[0]->main_ctx; // 协程结束后回到主线程
// 设置协程入口函数
makecontext(&coro->ctx, [](sslocal://flow/file_open?url=void%2A+arg&flow_extra=eyJsaW5rX3R5cGUiOiJjb2RlX2ludGVycHJldGVyIn0=) {
Coroutine* coro = static_cast<Coroutine*>(arg);
coro->func(); // 执行任务
coro->state = CoroutineState::FINISHED;
}, 1, coro);
return coro;
}
// 切换协程
void CoroutineThreadPool::switch_coroutine(Coroutine* from, Coroutine* to) {
swapcontext(&from->ctx, &to->ctx);
}
// 销毁协程
void CoroutineThreadPool::destroy_coroutine(Coroutine* coro) {
delete[] coro->stack;
delete coro;
}
int main() {
// 创建线程池:4个线程,绑定到4个CPU核心
CoroutineThreadPool pool(4);
pool.start();
// 投递任务到指定线程
for (int i = 0; i < 100; ++i) {
int thread_id = i % 4; // 轮询投递到不同线程
pool.submit_task(thread_id, {
printf("Task %d running on thread %d (CPU core %d)\n", i, thread_id, thread_id);
// 模拟网络IO:这里可以用异步IO,阻塞时让出CPU
std::this_thread::sleep_for(std::chrono::milliseconds(10));
});
}
// 等待任务完成
std::this_thread::sleep_for(std::chrono::seconds(2));
pool.stop();
return 0;
}模型 | 队列里放什么 | 本质 |
|---|---|---|
消息队列 | 数据 | 传递信息 |
线程池 | task(函数) | 要执行的代码 |
协程池 | coroutine | 可恢复执行状态 |
模型 | 队列里放什么 | 队列本质 | 谁来取 | 核心特点 |
|---|---|---|---|---|
消息队列 MQ | 消息 / 数据包 | 传递数据、事件 | 消费者进程 / 线程 | 只传数据,不携带执行逻辑 |
线程池 | 任务函数(Task/Function) | 一段要执行的代码 | 线程 | 执行一次就销毁,无状态 |
协程池 | 协程对象(Coroutine) | 可暂停、可恢复的执行现场 | 线程内的协程调度器 | 自带栈 + 寄存器 + 上下文,能挂起、能继续跑 |
Task = 函数 + 数据
int x = 10;
pool.submit([x] {
std::cout << x;
});
队列里其实是:
Task {
code: print
data: x = 10
}
如何理性:
协程池:放「Coroutine」,本质「可恢复执行状态」
Coroutine = 一个带状态的执行流程
为什么说协程是有状态的?
看一个普通函数👇
void f() {
int x = 1;
int y = 2;
int z = x + y;
}执行完就没了:
调用 → 执行 → 返回 → 状态消失 ❌现在加一个暂停
void f() {
int x = 1;
yield; // 停在这里
int y = 2;
int z = x + y;
}👉 问题来了:
当你停在 yield 时,系统必须记住:
1️⃣ 执行到哪一行(PC)
2️⃣ x = 1(局部变量)
3️⃣ 栈在哪👉 这些就是“状态”:
状态 = 能让程序继续执行所需的全部信息协程 = 执行现场快照
你可以这样理解:
Coroutine =
函数 + 当前执行位置 + 局部变量 + 栈更底层一点(CPU视角)
Coroutine =
RIP(下一条指令)
+ RSP(栈指针)
+ registers(寄存器)
+ stack(栈内存)👉 所以:
❗ 协程其实是CPU执行现场的保存 + 恢复
为什么叫状态机?
因为它会在不同状态之间切换👇
READY → RUNNING → BLOCKED → READY → FINISHEDasync_read()执行过程:
RUNNING → 发起IO → BLOCKED → IO完成 → READY → RUNNING👉 所以:
❗ 协程 = “带状态的执行流程(state machine)”
如果没有状态,会发生什么?
没有状态
每次只能从头执行✅ 有状态(协程)
可以:
- 中断
- 保存现场
- 以后继续再对比一次
模型 | 是否有状态 | 能否暂停 |
|---|---|---|
普通函数 | ❌ 没有 | ❌ |
线程 | ✅ 有(内核保存) | ✅ |
协程 | ✅ 有(用户态保存) | ✅ |
👉 关键区别:
线程:操作系统帮你保存状态(重)
协程:你自己保存状态(轻)一句话终极理解
❗ 协程之所以叫“带状态的执行流程”,是因为它在任意时刻都可以保存自己的执行现场,并在未来恢复继续执行。
这里不考虑 raft 协议 ,只考虑 2 个,4 个节点,
但是同样采取相同的方式 就是版本号,主,从,然后怎么保证 2 个节点数据一致,
并且 如何同步数据呢?
在文件系统场景下,我们只需要保证这 3 点(不追求线性一致的极致性能):
这里主要说 高度抽象一个框架,业务只写普通 函数一样,不考虑是否跨线程,跨进程,跨节点。
【操作系统能力】
TCP / epoll / RDMA
↓
【通信框架】
connection / message / buffer
↓
【分布式协议】
raft / primary-backup / log replication
↓
【业务语义】
write / read / commit
所有厂商都采用 分层通信架构:
底层用 TCP/RDMA 做数据传输,
中间用自研 RPC 框架做远程调用,上层用定制化同步协议保证数据一致性。
没有任何一家会用通用的 gRPC/HTTP2 做主从数据同步,全部是深度定制的私有协议。
它本质上是为特定场景量身定制的极简协议
它没有 HTTP2 的帧头,没有 Protobuf 的字段编号,没有 gRPC 的状态码,只有存储系统真正需要的东西。
协议类型 | 单 IO 延迟 | 单 CPU 核心吞吐量 | CPU 占用率 | 零拷贝支持 |
|---|---|---|---|---|
自研私有协议(RDMA) | ~1.2μs | 80 万 IOPS | <5% | ✅ 完全零拷贝 |
brpc(RDMA) | ~8μs | 12 万 IOPS | ~30% | ❌ 半零拷贝 |
gRPC(RDMA) | ~25μs | 4 万 IOPS | ~60% | ❌ 无零拷贝 |
gRPC(TCP) | ~120μs | 0.8 万 IOPS | ~90% | ❌ 无零拷贝 |
基础能力是什么:
【操作系统能力】
TCP / epoll / RDMA
↓
【通信框架】
connection / message / buffer
↓
【分布式协议】
raft / primary-backup / log replication
↓
【业务语义】
write / read / commit
曙光是自研通信栈,直接控制数据传输路径;
浪潮是整合现有通信技术,在上层做统一调度与优化
曙光(ParaStor):像一个精密的硬核工程,采用强主从同步模型。它基于自研的高性能私有通信栈,追求对数据路径和一致性的绝对控制与确定性。
浪潮(AS13000):像一个灵活的智能调度系统,采用多主并行写入模型。它在标准通信技术之上,通过智能调度来动态权衡,追求系统的整体效能与极致弹性。
3FS与曙光ParaStor引领软硬一体化创新
这个世界上,始终存在两种力量的共同演变,那就是分与合。
分久必合,合久必分。这两种力量在长期的互为作用下,形成了行业创新的动力
光ParaStor基于自主研发,拥有所有底层代码。可以做到从最底层代码到上层应用全路径的定制开发与性能优化。
https://so.html5.qq.com/page/real/search_news?docid=70000021_9945db6dc6914752&faker=1
文章:比操作系统更难的存储文件系统,曙光存储是如何做成的?
层级 | 基础能力 | 核心产品 / 技术 |
|---|---|---|
操作系统能力 | TCP、epoll、RDMA 等,提供最底层的网络 I/O 和数据传输能力 | Linux 内核协议栈(TCP/IP)、epoll、InfiniBand / RoCE(RDMA) |
通信框架 | 连接的建立与管理、消息的序列化与反序列化、缓冲区的管理与调度 | Netty、gRPC、ZeroMQ、Brpc |
分布式协议 | Raft、Primary-Backup、原子广播、Gossip 等,确保跨节点状态一致和故障容错 | ZooKeeper(ZAB 协议)、etcd(Raft)、3FS(CRAQ)、Consul(Gossip) |
业务语义 | write、read、commit 等,将底层机制封装为应用层能直接使用的操作原语 | Redis Pub/Sub、RocketMQ 广播、ZooKeeper Watch、etcd Watch |
链式复制广播(CRAQ),牺牲写延迟换取更高读吞吐;如何做到的
3FS选择CRAQ,正是因为AI训练场景的典型IO特征是“一次写入、多次读取”:
在强一致性要求下,一个写操作需要经历以下步骤:
延迟代价的来源:
N-1。节点越多,延迟线性增加。在强一致性要求下,一个写操作需要经历以下步骤:
延迟代价的来源:
N-1。节点越多,延迟线性增加。同样的3节点场景(1 Primary + 2 Backup):
操作 | Primary-Backup | CRAQ |
|---|---|---|
写延迟 | 低(并行复制,约2跳) | 高(顺序传播,4跳) |
3个并发读 | 全部打到Primary,吞吐受单节点限制 | 分散到3个节点,吞吐提升3倍 |
在Ceph的RADOS架构中,Pool是一个纯粹的逻辑容器和策略定义单元。
它不是物理资源的集合,而更像是一组配置参数的“标签
策略的载体:Pool的核心作用是承载数据分布策略。关键属性包括:
工作机制:
当一个数据对象存入Ceph时,它首先被映射到某个Pool下的某个PG,然后CRUSH算法会根据该Pool的CRUSH规则和PG ID,计算出这个PG(以及其中的数据)应该存储在哪些OSD上。
这个过程完全是动态计算,无需查找中心化元数据
[OSD.3, OSD.15, OSD.27])。在ParaStor中,Pool(通常称为“存储池”或“统一存储资源池”)是一个更接近物理实现的、全局性的资源聚合与性能管理单元。
总结:
Ceph和ParaStor代表了分布式存储设计的两种不同哲学:
Ceph 的设计哲学是 通用、开放、去中心化 。LVM在此只是其庞大生态中的一个可选模块,用于解决本地的磁盘管理问题。这种设计赋予了Ceph极大的灵活性和扩展性,适合构建大规模、标准化的统一存储资源池
ParaStor 的设计哲学是 专用、深度优化、全局统一 。它将LVM深度整合进其核心架构,作为构筑其高性能、全局统一存储池的基石。这种紧耦合设计使其能进行极致的软硬件协同优化,专攻高性能计算、AI等对I/O有极致要求的场景。
在Ceph和曙光ParaStor这两种分布式存储架构中,LVM的作用定位截然不同: