首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >系统之美第一期(下)(2026-04-10):开启每周学习打卡计划

系统之美第一期(下)(2026-04-10):开启每周学习打卡计划

作者头像
早起的鸟儿有虫吃
发布2026-04-28 11:50:06
发布2026-04-28 11:50:06
820
举报

本文分为三个部分:

第一部分:践行记录 无记录

记录自己和他人实践过程

第二部分: 无记录

学习是为了找到通往答案的路径和方法,是为了拥有无师自通的能力

第三部分:ARTS

ARTS是由左耳朵耗子在极客时间专栏《左耳听风》中发起的一个每周学习打卡计划

二、学习是为了找到通往答案的路径和方法,是为了拥有无师自通的能力

学习并不是为了要记忆那些知识点,而是为了要找到一个知识的地图,你在这个地图上能通过关键路径找到你想要的答案,从第一手资料开始

对于一个学习者来说,找到优质的信息源可以让你事半功倍。

一方面,就像找到一本很好的武林秘籍一样,而不是被他人翻译过或消化过的,也不会有信息损失甚至有错误信息会让你走火入魔。

https://time.geekbang.org/column/article/14321

https://time.geekbang.org/column/article/14360

举一反三举一反三的道理人人都知道,所以,在这里我并不想讨论为什么要举一反三,而是想讨论如何才能有举一反三的能力

  1. 这个技术出现的背景、初衷和要达到什么样的目标或是要解决什么样的问题

这个问题非常关键,也就是说,你在学习一个技术的时候,需要知道这个技术的成因和目标,也就是这个技术的灵魂。如果不知道这些的话,那么你会看不懂这个技术的一些设计理念。

  1. 这个技术的优势和劣势分别是什么,或者说,这个技术的 trade-off 是什么

任何技术都有其好坏,在解决一个问题的时候,也会带来新的问题。另外,一般来说,任何设计都有 trade-off(要什么和不要什么),所以,你要清楚这个技术的优势和劣势,以及带来的挑战。

  1. 这个技术适用的场景

任何技术都有其适用的场景,离开了这个场景,这个技术可能会有很多槽点,所以学习技术不但要知道这个技术是什么,还要知道其适用的场景。没有任何一个技术是普适的。

注意,所谓场景一般分为两个,一个是业务场景,一个是技术场景。

  1. 技术的组成部分和关键点

这是技术的核心思想和核心组件了,也是这个技术的灵魂所在了。学习技术的核心部分是快速掌握关键。

  1. 技术的底层原理和关键实现

任何一个技术都有其底层的关键基础技术,这些关键技术很可能也是其它技术的关键基础技术。所以,学习这些关键的基础底层技术,可以让你未来很快地掌握其它技术。可以参考我在 CoolShell 上写的 Docker 底层技术那一系列文章。

  1. 已有的实现和它们之间的对比

一般来说,任何一个技术都会有不同的实现,不同的实现都会有不同的侧重。学习不同的实现,可以让你得到不同的想法和思路,对于开阔思维,深入细节是非常重要的。

小宇宙

视频号

微信公共号

你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI

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

ARTS是由左耳朵耗子在极客时间专栏《左耳听风》中发起的一个每周学习打卡计划

Algorithm:至少做一个 LeetCode 的算法题。主要为了编程训练和学习。 Review :阅读并点评至少一篇英文技术文章。主要为了学习英文,如果你英文不行,很难成为技术高手。 Tip:学习至少一个技术技巧。主要是为了总结和归纳你日常工作中所遇到的知识点。 Share:分享一篇有观点和思考的技术文章。主要为了输出你的影响力,能够输出你的价值观

https://time.geekbang.org/column/article/14271

学习是为了改变自己的思考方式,改变自己的思维方式,改变自己与生俱来的那些垃圾和低效的算法。总之,学习让我们改变自己,行动和践行,反思和改善,从而获得成长

学习是为了找到通往答案的路径和方法,是为了拥有无师自通的能力。

提问1:分布式文件系统 调度如何实现的(进程,线程,协程)?

类型

本质调度模型

亲和类

固定绑定调度(Static Scheduling)

计算类

全局抢占调度(Work Sharing / Pull Model)

协程类

用户态调度(User-space Scheduling)

线程池类型

核心特征(头文件原文)

本质

竞争情况

负载均衡

典型用途

亲和类

线程运行无竞争,任务投递需指定线程

任务与线程 1:1 绑定,每个线程有独立任务队列

❌ 无锁竞争

❌ 无法自动均衡,闲线程不能帮忙线程

严格顺序执行的任务、线程本地资源独占任务

计算类

线程运行有竞争,任务负载可在线程间均衡

所有线程共享同一个任务队列,谁闲谁取任务

✅ 取任务时有锁竞争

✅ 自动全局负载均衡

CPU 密集型、执行时间不确定的后台任务

协程类

任务按协程方式运行,任务投递需指定线程

用户态轻量级调度,协程绑定到指定线程

❌ 协程间无竞争

❌ 仅线程内协程均衡,跨线程无法均衡

大量轻量 IO 密集型任务

根据业务需求划分1:亲和类线程池

Shared-Nothing 无共享架构

核心特征为:

CPU 核心强绑定、线程私有任务队列、任务显式指定投递、无锁无竞争、极致低延迟

产品1:Ceph Crimson OSD(完全贴合,工业级标杆实现)

核心定位

Ceph 是开源分布式统一存储系统,支持对象、块、文件三种存储接口;

Crimson 是 Ceph 新一代 OSD(对象存储守护进程),完全重构了传统 OSD 的线程模型,是亲和类线程池在分布式存储领域的标杆实现。

亲和类线程池核心实现

  1. CPU 核心强绑定的 Shared-Nothing 架构基于 Seastar 高性能框架构建,为每个 CPU 核心创建唯一的 reactor 线程,通过系统调用强绑定到固定 CPU 核心,线程生命周期内绝不跨核心迁移,彻底避免内核调度带来的上下文切换与缓存失效。完全对应你的代码中 grdsched_worker.bindCoreType 的 CPU 绑定设计。
  2. 完全无锁的私有任务队列每个 reactor 线程拥有独立的私有任务队列,基于future/promise异步模型实现,无任何全局共享队列与全局锁,从设计根源上消除多线程竞争,任务入队出队全程无锁。完全对应你的代码中 “线程运行无竞争” 的核心设计目标。
  3. 任务显式指定投递,无自动负载均衡业务层必须通过 seastar::smp::submit_to(cpu_id, task) 接口,显式指定任务投递到哪个 CPU 核心的 reactor 线程;同时 PG(放置组)分片固定归属到指定核心,客户端 IO 请求按 PG 路由规则直接投递到对应线程,不会出现跨核心任务迁移。完全对应你的代码中 “任务投递需指定线程” 的核心要求。
  4. Polling 轮询模式极致降延迟reactor 事件循环默认支持 Polling 模式,高负载下线程不休眠,持续轮询本地任务队列与 IO 事件,消除内核唤醒延迟,将 IO 请求的端到端延迟压到微秒级。
产品2:BeeGFS(轻量级并行文件系统亲和设计)

核心定位

BeeGFS(原 FHGFS)是弗劳恩霍夫研究所开源的并行分布式文件系统,主打极简部署、极致性能,

专为 HPC、AI 训练场景优化,原生采用 Multi-reactor 亲和线程模型。

亲和类线程池核心实现

  1. Per-Core Reactor

线程绑定元数据服务(MDS)和存储服务(OSS)采用Multi-reactor 架构

每个 CPU 核心绑定一个独立的 reactor 工作线程,线程与核心一一对应,避免线程漂移带来的缓存失效,最大化 CPU 缓存命中率。

  1. 连接 / 请求哈希固定路由

客户端 TCP 连接、IO 请求会按文件 ID / 目录哈希,固定分配到指定的 reactor 线程处理,保证相同文件的所有读写请求始终由同一个线程处理,无需加锁即可保证 IO 顺序性,同时彻底避免线程间的任务竞争。

  1. 线程私有任务队列与资源隔离

每个 reactor 线程拥有独立的任务队列、内存缓冲区、网络连接上下文,线程间无共享状态,仅通过消息传递进行通信,完全符合 Shared-Nothing 的亲和类设计思想。

产品3:DAOS(极致低延迟的亲和类架构实现)

核心定位

DAOS 是 Intel 开源的分布式异步对象存储系统,专为 NVMe SSD、RDMA 高速网络设计,

主打亚微秒级 IO 延迟,是超算、AI 大模型训练场景的新一代存储系统,完全基于亲和类线程池架构构建。

亲和类线程池核心实现

  1. Core-to-Target 1:1

强绑定每个 CPU 核心对应一个唯一的 Target 服务线程,线程强绑定到固定核心,

每个 Target 拥有独立的存储引擎、内存池、任务队列、网络上下文,完全无共享设计,线程间无任何全局竞争

  1. 请求显式路由,无跨线程调度IO
  2. 全链路 Polling 轮询模式

线程全程采用 Polling 模式轮询

RDMA 网络队列、NVMe SSD 硬件队列,完全绕过内核中断,极致降低 IO 处理延迟,

是目前开源存储领域延迟最低的实现之一。

计算类线程

几乎所有主流数据库和分布式存储系统,都把后台 CPU 密集型任务交给计算类线程池处理,下面是最典型的实现:

  1. RocksDB:计算类线程池的教科书级实现

RocksDB 是 LSM 树存储引擎的标杆,它的所有后台核心任务都运行在计算类线程池上:

  • Compaction 线程池:负责合并 SST 文件(CPU 密集型,执行时间从几秒到几小时不等)
  • Flush 线程池:负责将内存表刷盘(CPU + 磁盘混合)
  • Background 线程池:负责垃圾回收、校验和计算等

为什么用计算类?

  • Compaction 任务大小差异极大(小到几 MB,大到几十 GB),如果用亲和类,会出现 "一个线程忙死,其他线程闲死" 的情况
  • 计算类线程池自动负载均衡:空闲线程会主动从共享队列取任务,最大化 CPU 利用率
  • 支持优先级:高优先级的 Level 0 Compaction 会优先执行,避免写停顿
  1. MySQL InnoDB:计算类线程池支撑核心事务

InnoDB 的后台线程池是数据库性能的关键:

  • 刷脏页线程池:将缓冲池中的脏页刷到磁盘
  • Undo Log 回收线程池:清理过期的回滚日志
  • Purge 线程池:删除标记为删除的记录

为什么用计算类?

  • 脏页刷盘的负载随业务波动极大,计算类线程池可以动态调整任务分配
  • 避免亲和类的 "任务堆积" 问题:如果某个线程分配了大量脏页,其他线程可以帮忙处理
  • 支持线程数动态调整:根据系统负载自动增减线程数
  1. Ceph:分布式存储的计算类线程池架构

Ceph OSD(对象存储守护进程)的所有数据处理任务都运行在计算类线程池上:

  • PG 线程池:负责 Placement Group 的数据读写、副本同步
  • Recovery 线程池:负责数据恢复和重平衡
  • Scrub 线程池:负责数据校验和一致性检查

为什么用计算类?

  • 分布式存储的负载是全局波动的,计算类线程池可以在整个 OSD 节点内均衡负载
  • 数据恢复任务的执行时间不确定,计算类线程池可以避免单个线程阻塞导致整个恢复流程变慢
  • 支持多队列优先级:用户读写任务优先级高于后台恢复任务
  1. 通用语言标准库:Java ThreadPoolExecutor

Java 的 ThreadPoolExecutor 是最经典的计算类线程池实现,默认就是「共享任务队列 + 自动负载均衡」模式,被几乎所有 Java 后端项目使用。

协程类线程池解决了什么问题?

它专门解决纯线程池处理 IO 密集型任务的致命缺陷

  • 纯线程池处理 IO 密集型任务时,线程会阻塞在 IO 上,导致 CPU 利用率极低(通常 < 10%)
  • 线程创建和销毁开销大,无法支持万级以上的并发任务
  • 线程上下文切换开销大,大量线程会导致系统卡顿

协程类线程池的解决方案:

  • IO 阻塞时,协程主动让出 CPU,让同一个线程上的其他协程继续运行,CPU 利用率可以提升到 90% 以上
  • 协程创建和销毁开销几乎为 0,可以轻松支持百万级并发
  • 协程上下文切换开销极低,几乎可以忽略不计

协程类线程池

举个真实场景(Ceph / TiKV / RocksDB)

一次写请求:

客户端写入

网络发送(等)

副本复制(等)

磁盘落盘(等)

返回结果

👉 CPU真正干活的时间很少

几乎所有现代高性能分布式系统都在使用协程类线程池:

  1. Go GMP 调度器(最经典)
  • 本质就是协程类线程池:M = 线程,P = 协程调度器,G = 协程
  • 每个 P 绑定一个 M,G 只能在绑定的 P 上运行
  • 实现了工作窃取:空闲的 P 可以从其他 P 的队列偷 G,解决负载不均衡问题
  • M:N 调度模型

✅ GMP 是 M:N 用户态协程调度模型,P 是调度核心,G 是执行单元,M 是被复用的内核线程载体;

✅ 工作窃取是 P 之间偷 G,实现协程级负载均衡

  1. C++ Seastar 框架(share-nothing 标杆)
  • 每个 CPU 核心一个线程,线程内部跑协程
  • 任务必须投递到指定核心,完全无锁
  • 性能极致:单核心可以处理 100 万 + QPS 的网络请求
  • 代表产品:ScyllaDB(替代 Cassandra 的分布式数据库)
  1. C++ brpc(百度开源 RPC 框架)
  • bthread 协程池:每个工作线程有自己的 bthread 调度器
  • 支持任务投递到指定线程,也支持全局投递
  • 代表产品:百度内部所有分布式系统
  1. Rust Tokio 运行时
  • current-thread scheduler:纯协程类线程池,单线程跑所有协程
  • multi-threaded scheduler:计算类 + 协程混合模型,协程可以跨线程运行
  • TiKV(Rust,基于 Tokio)
必然遇到的问题:协程类线程池:为什么要 "任务投递到指定线程" 如何实现
代码语言:javascript
复制
// 构造函数:初始化调度器,绑定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;
}

必然遇到的疑问:Coroutine = 一个带状态的执行流程 如何理解?

模型

队列里放什么

本质

消息队列

数据

传递信息

线程池

task(函数)

要执行的代码

协程池

coroutine

可恢复执行状态

模型

队列里放什么

队列本质

谁来取

核心特点

消息队列 MQ

消息 / 数据包

传递数据、事件

消费者进程 / 线程

只传数据,不携带执行逻辑

线程池

任务函数(Task/Function)

一段要执行的代码

线程

执行一次就销毁,无状态

协程池

协程对象(Coroutine)

可暂停、可恢复的执行现场

线程内的协程调度器

自带栈 + 寄存器 + 上下文,能挂起、能继续跑

Task = 函数 + 数据

int x = 10;

pool.submit([x] {

std::cout << x;

});

队列里其实是:

Task {

code: print

data: x = 10

}

如何理性:

协程池:放「Coroutine」,本质「可恢复执行状态」

Coroutine = 一个带状态的执行流程


为什么说协程是有状态的?

看一个普通函数👇

代码语言:javascript
复制
void f() {
    int x = 1;
    int y = 2;
    int z = x + y;
}

执行完就没了:

代码语言:javascript
复制
调用 → 执行 → 返回 → 状态消失 ❌

现在加一个暂停

代码语言:javascript
复制
void f() {
    int x = 1;
    yield;        // 停在这里
    int y = 2;
    int z = x + y;
}

👉 问题来了:

当你停在 yield 时,系统必须记住:

代码语言:javascript
复制
1️⃣ 执行到哪一行(PC)
2️⃣ x = 1(局部变量)
3️⃣ 栈在哪

👉 这些就是“状态”:

代码语言:javascript
复制
状态 = 能让程序继续执行所需的全部信息

协程 = 执行现场快照

你可以这样理解:

代码语言:javascript
复制
Coroutine =
    函数 + 当前执行位置 + 局部变量 + 栈

更底层一点(CPU视角)

代码语言:javascript
复制
Coroutine =
    RIP(下一条指令)
  + RSP(栈指针)
  + registers(寄存器)
  + stack(栈内存)

👉 所以:

❗ 协程其实是CPU执行现场的保存 + 恢复


为什么叫状态机?

因为它会在不同状态之间切换👇

代码语言:javascript
复制
READY → RUNNING → BLOCKED → READY → FINISHED

举例

代码语言:javascript
复制
async_read()

执行过程:

代码语言:javascript
复制
RUNNING → 发起IO → BLOCKED → IO完成 → READY → RUNNING

👉 所以:

❗ 协程 = “带状态的执行流程(state machine)”


如果没有状态,会发生什么?


没有状态

代码语言:javascript
复制
每次只能从头执行

✅ 有状态(协程)

代码语言:javascript
复制
可以:
    - 中断
    - 保存现场
    - 以后继续

再对比一次

模型

是否有状态

能否暂停

普通函数

❌ 没有

线程

✅ 有(内核保存)

协程

✅ 有(用户态保存)


👉 关键区别:

代码语言:javascript
复制
线程:操作系统帮你保存状态(重)
协程:你自己保存状态(轻)

一句话终极理解

协程之所以叫“带状态的执行流程”,是因为它在任意时刻都可以保存自己的执行现场,并在未来恢复继续执行。


提问2:分布式文件系统 如何保证数据一致性?

这里不考虑 raft 协议 ,只考虑 2 个,4 个节点,

但是同样采取相同的方式 就是版本号,主,从,然后怎么保证 2 个节点数据一致,

并且 如何同步数据呢?

在文件系统场景下,我们只需要保证这 3 点(不追求线性一致的极致性能):

  1. 写后读一致:客户端写完一个文件后,立刻能读到自己写的内容
  2. 最终一致:所有节点最终会拥有相同的文件数据和元数据

这里主要说 高度抽象一个框架,业务只写普通 函数一样,不考虑是否跨线程,跨进程,跨节点。

【操作系统能力】

TCP / epoll / RDMA

【通信框架】

connection / message / buffer

【分布式协议】

raft / primary-backup / log replication

【业务语义】

write / read / commit

所有厂商都采用 分层通信架构:

底层用 TCP/RDMA 做数据传输,

中间用自研 RPC 框架做远程调用,上层用定制化同步协议保证数据一致性

没有任何一家会用通用的 gRPC/HTTP2 做主从数据同步,全部是深度定制的私有协议。

为什么定制 通许框架?
  1. 无法实现真正的零拷贝
  2. 序列化 / 反序列化开销巨大
  3. 不支持存储专属的批量操作
  4. .无法与硬件深度协同

它本质上是为特定场景量身定制的极简协议

它没有 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),牺牲写延迟换取更高读吞吐;如何做到的

为什么适合AI场景?

3FS选择CRAQ,正是因为AI训练场景的典型IO特征是“一次写入、多次读取”:

  • 训练数据(如ImageNet、C4)写入一次后,会被成千上万个训练任务反复读取。
  • 模型Checkpoint写入频率低(通常几小时一次),但保存时对一致性和持久性要求极高。

写入路径:牺牲延迟

在强一致性要求下,一个写操作需要经历以下步骤:

  1. Client → Head:客户端向头节点发送写请求。
  2. Head → Middle → Tail:头节点写入本地后,将更新顺序地沿着链条向下传播。每个节点收到更新后写入本地,再转发给下一个节点,直到尾节点。
  3. Tail → ... → Head → Client:尾节点完成写入后,生成确认(ACK),确认消息逆序地沿链条回传给头节点。
  4. Head → Client:头节点收到尾节点的最终确认后,才向客户端返回写入成功。

延迟代价的来源:

  • 顺序传播:更新必须串行经过N个节点,网络跳数为 N-1。节点越多,延迟线性增加。
  • 尾节点瓶颈:确认消息必须等待最慢的尾节点完成落盘。在跨地域部署时,尾节点可能距离头节点很远,导致写入延迟极高。

写入路径:牺牲延迟

在强一致性要求下,一个写操作需要经历以下步骤:

  1. Client → Head:客户端向头节点发送写请求。
  2. Head → Middle → Tail:头节点写入本地后,将更新顺序地沿着链条向下传播。每个节点收到更新后写入本地,再转发给下一个节点,直到尾节点。
  3. Tail → ... → Head → Client:尾节点完成写入后,生成确认(ACK),确认消息逆序地沿链条回传给头节点。
  4. Head → Client:头节点收到尾节点的最终确认后,才向客户端返回写入成功。

延迟代价的来源:

  • 顺序传播:更新必须串行经过N个节点,网络跳数为 N-1。节点越多,延迟线性增加。
  • 尾节点瓶颈:确认消息必须等待最慢的尾节点完成落盘。在跨地域部署时,尾节点可能距离头节点很远,导致写入延迟极高。

同样的3节点场景(1 Primary + 2 Backup):

操作

Primary-Backup

CRAQ

写延迟

低(并行复制,约2跳)

高(顺序传播,4跳)

3个并发读

全部打到Primary,吞吐受单节点限制

分散到3个节点,吞吐提升3倍

学会提问3:POOL 概念理解

Ceph Pool:逻辑隔离与策略定义单元

在Ceph的RADOS架构中,Pool是一个纯粹的逻辑容器和策略定义单元。

它不是物理资源的集合,而更像是一组配置参数的“标签

策略的载体:Pool的核心作用是承载数据分布策略。关键属性包括:

  • 冗余策略:定义该Pool的数据是使用多副本还是纠删码(EC) 来保护。
  • PG数量:该Pool被划分为多少个放置组(PG)。PG是数据迁移和恢复的最小单位。
  • CRUSH规则:指定该Pool的数据应如何分布到不同的故障域(如不同的主机、机架或机房)

工作机制:

当一个数据对象存入Ceph时,它首先被映射到某个Pool下的某个PG,然后CRUSH算法会根据该Pool的CRUSH规则和PG ID,计算出这个PG(以及其中的数据)应该存储在哪些OSD上。

这个过程完全是动态计算,无需查找中心化元数据

  • 实现机制:客户端(或OSD)输入对象ID和Pool的CRUSH规则,通过一个可控的伪哈希函数,直接计算出一个有序的OSD列表(如 [OSD.3, OSD.15, OSD.27])。
  • 特点:无需查表,无需元数据服务器。只要集群拓扑(Cluster Map)一致,所有节点对“数据在哪”的计算结果是唯一且一致的。这是一种确定性计算抽象。
  1. File/App → Objects(对象):业务数据被切分成一个个固定大小的对象。
  2. Object → PG(放置组):通过哈希算法,对象被映射到某个PG(小包裹)。
  3. PG → OSDs(磁盘):通过CRUSH算法,计算出这个PG应该存储在哪些物理磁盘上。
  4. PG → 数据冗余(副本/EC):根据存储池的冗余策略,PG会对应一个OSD列表(例如,3副本策略下,一个PG会落在3个不同的OSD上),由主OSD负责协调数据复

在ParaStor中,Pool(通常称为“存储池”或“统一存储资源池”)是一个更接近物理实现的、全局性的资源聚合与性能管理单元。

  • 物理资源的聚合与虚拟化:ParaStor的Pool建立在LVM卷之上。首先,每个存储节点(OSS)上的多块物理盘通过LVM组成卷组(VG),再从中划分出逻辑卷(LV) 作为底层存储块。然后,集群管理软件将所有节点的LV逻辑上聚合为一个统一的、全局的存储池。
  • 全局命名空间的实现:这个聚合后的Pool直接构成了ParaStor的全局统一命名空间。所有文件目录都创建在这个Pool之上,并由元数据节点(MDS)统一管理。

总结:

Ceph和ParaStor代表了分布式存储设计的两种不同哲学:

Ceph 的设计哲学是 通用、开放、去中心化 。LVM在此只是其庞大生态中的一个可选模块,用于解决本地的磁盘管理问题。这种设计赋予了Ceph极大的灵活性和扩展性,适合构建大规模、标准化的统一存储资源池

ParaStor 的设计哲学是 专用、深度优化、全局统一 。它将LVM深度整合进其核心架构,作为构筑其高性能、全局统一存储池的基石。这种紧耦合设计使其能进行极致的软硬件协同优化,专攻高性能计算、AI等对I/O有极致要求的场景。

LVM在分布式存储中的角色差异

在Ceph和曙光ParaStor这两种分布式存储架构中,LVM的作用定位截然不同:

  • 在Ceph中:LVM是辅助性的本地设备管理器
    • LVM仅在单个存储节点内部工作,作用是为该节点上的OSD(对象存储守护进程)管理本地磁盘。
    • 它通过给磁盘打上标签(Tags),帮助Ceph在重启后精准识别和挂载属于该OSD的存储设备。
    • LVM的存在对Ceph集群的全局数据分布(CRUSH算法)没有任何影响,只是一个可选的底层工具。
  • 在ParaStor中:LVM是构建全局统一存储池的基石
    • LVM是ParaStor架构的基础组件。首先,每个存储节点通过LVM将本地物理磁盘聚合成逻辑卷(LV)。
    • 随后,集群管理软件将所有节点的这些LV进一步整合,形成一个跨节点的、EB级的全局统一存储池。
    • ParaStor的元数据服务、数据冗余策略(如1~4副本)都构建在这个由LVM支撑的全局池之上,使其成为高性能I/O的物理底座
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-04-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 后端开发成长指南 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 二、学习是为了找到通往答案的路径和方法,是为了拥有无师自通的能力
    • 小宇宙
    • 视频号
      • 微信公共号
  • 三、ARTS
    • 提问1:分布式文件系统 调度如何实现的(进程,线程,协程)?
      • 根据业务需求划分1:亲和类线程池
    • 协程类线程池解决了什么问题?
    • 协程类线程池
      • 举个真实场景(Ceph / TiKV / RocksDB)
      • 必然遇到的疑问:Coroutine = 一个带状态的执行流程 如何理解?
    • 举例
    • 提问2:分布式文件系统 如何保证数据一致性?
      • 为什么适合AI场景?
      • 写入路径:牺牲延迟
      • 写入路径:牺牲延迟
    • 学会提问3:POOL 概念理解
      • Ceph Pool:逻辑隔离与策略定义单元
      • LVM在分布式存储中的角色差异
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档