关于 ideawu | 吴祖洋

高性能 NoSQL 数据库 SSDB 的作者,曾就职于业界多家知名的互联网企业,擅长分布式数据库,高性能高并发互联网技术架构设计。

RSS 地址: https://www.ideawu.net/blog/feed

请复制 RSS 到你的阅读器,或快速订阅到 :

ideawu | 吴祖洋 RSS 预览

关于多写入点数据库集群的一些想法

2021-09-06 20:51:04

在分布式数据库系统领域, 多主(多写入点, Leader-less)是一个非常诱人的特性, 因为客户端可以随机请求任何一个节点. 这种可随机选择访问点(写入点)的特性, 使得系统的高可用唾手可得, 因为当客户端发现某个节点出故障时, 更换另一个节点重试就可以了, 只要系统没有完全宕机, 几次重试之后一定成功, 也就可以达到百分之百高可用.

传统的 Basic Paxos 常常被误认为是 Leader-less 的, 也即多主, 但 Basic Paxos 只能用于确定一个实例的共识, 真正落地还需要结合日志复制状态机, 如果复制组(多节点)不指定 Leader 的话, 那么就会出现争取同一个位置的日志的情况, 也就是在尝试达成这个位置的日志的共识时出现活锁. 这种多节点争取同一个位置的情况, 在实践上将导致系统不可用, 因为, 通常自称采用 Paxos 的多副本数据库系统, 依然要显式指定 Leader, 并不是真正 Leader-less 的.

借鉴 vector 时钟和多复制组的思路(如 Multi Raft Group), 是可以避免多主冲突的, 因为, 不同的节点作为各自组的 Leader, 分别写入不同的日志序列, 也就完全没有争取同一个位置的日志而导致冲突了.

以一个3节点的集群来举例, 通过预先配置强制指定各为其主, 结构为:

G1 A B C
G2 B A C
G3 C B A

有复制组 G1, 成员节点是 {A, B, C}, 其中节点 A 是主. 类似的, 复制组 G2 和 G3 的主是 B, C. 当客户端请求不同节点时, 将日志写入不同复制组的日志序列, 因此不会产生冲突.

某一时刻, 所有节点的状态是:

G1 G2 G3
A 6 1 2
B 6 1 2
C 6 1 2

这个状态表明, 复制组 G1 累计有 6 条日志, 复制组 G2 是 1 条, 复制组 G3 是 2 条.

到目前为此, 似乎一切顺利, 写入没有冲突, 还能通过复制组形成多副本. 但是, 现实不是这样的, 一个只能写入的数据库几乎没有任何用处, 数据库必须支持读取, 所以, 日志复制状态机架构必须有状态机, 也即这 3 条日志序列必须在所有节点 Apply 到各节点的状态机实例中.

最简单的方法是在各个节点上把 3 条日志序列合并成(Merge)一条日志序列, 通过某种算法保证所有节点上的合并结果一定是相同的, 例如按时间戳排序, 这样才能保证状态机一致(相同).

但是, 节点不能只依赖自己本地的 3 条日志序列合并, 在每一次合并时, 它需要获取其它复制组的最新信息, 判断自己本地的日志序列是不是足够新. 自己作为 Leader 的那一条序列, 当然自己能确定, 但其它两条不能, 需要询问其它节点, 不能定死询问 Leader, 因为需要容忍某个复制组的 Leader 宕机的情况.

所以, 针对其它复制组, 采取 Read Index 逻辑, 可以判断是否最新. 并且在其它组 Leader 宕机的情况下, 将对应的日志序列从其它 Follower 那里补齐.

日志序列中可能包含非幂等的指令, 通过加入时间戳之后, 非幂等的指令可以变成幂等的.(需要文章论证). 以一个 key 为例, 状态机中的 key-value 带有初始化时间戳和最新更新时间戳 {reset_time, modify_time}. 当收到一条 incr 指令时, 指令中带有时间戳, 与 key-value 中的 meta 信息比对之后, 就能知道 Apply 算法.

if key.reset_time < op.time {
	value += 1
} else {
	// 忽略在初始化之前的指令
}

例如, 在 A 节点上针对 key 执行了 set 操作, 便会复位其 reset_time, 之后, 再收到其它节点的更早的 incr 指令时, 这个 incr 指令就不能修改状态机了, 因为这个指令发生在复位之前.

各个节点的系统时钟不同步没关系, 因为一致性与系统时钟没有必然联系(需要文章论证). 只要确保所有节点最终的结果是一致的(相同)的就行. 不要让 A 节点先执行 set 再执行 incr, 而 B 节点先执行 incr 再执行 set 这种情况出现. 在这个例子中, 从上帝视角知道 incr 在 set 之前, 所以, A 节点先执行 set, 然后遇到 incr 指令时会忽略.

上面的时间戳比较要求不同的节点产生的时间戳不能相同, 可以把节点ID当作时间戳的最低位, 避免两个时间戳相同.

对于 incr 操作, 初始化比较简单, 但是, 对于 pop 这种非幂等性的操作, 并不是初始化, 很难解决. 本质上, 是一种回滚操作, 能实现, 但成本太大, 要讨论起来真是够多的.

不同的节点收到不是自己负责的日志序列的延迟不一定, 但是, 我们又不能等确保全部日志序列都收到之后再 Apply, 虽然等全部日志序列都到齐之后再 Apply 可以不需要回滚操作, 但等待行为不能容忍宕机(CAP 理论).

总结起来, 就是没有银弹. vector 时钟看起来很美好, 但日志序列之间的相互依赖问题, 要么通过停止等待来解决, 要么通过回滚操作来解决, 否则无法保证多副本一致(相同). 但是, 某些操作的回滚成本太大.

注: 有一种技术叫 CRDT, 思想类似, 但是上面讨论的问题依然存在.

考虑过这个问题:

C 宕机, 但是它上面有一条日志还没有复制到其它节点.

A 和 B 各自合并日志序列之后 Apply 了, 两者的状态机一致, 没问题.

这时, C 恢复, 那条日志得以复制出去, 这时, A 和 B 需要回滚, 然后重新合并, 再 Apply.

如果 C 在恢复之后, 给那条日志赋予新的时间戳, 那么也会有一种场景(C 宕机前日志已经复制出去了, 但它还没知道结果), 同样需要回滚, 因为别的节点可能已经 Apply 过那条日志了.

Related posts:

  1. Paxos和Raft读优化 – Quorum Read 和 Read Index
  2. 什么是 Paxos 的日志空洞?
  3. Paxos 算法实现和工程落地: 选主与复制状态机
  4. 为什么极少有开源的Paxos库?
  5. 为什么 Leader Based 的分布式协议 Raft 是更好的

你现在看的文章是: 关于多写入点数据库集群的一些想法

Linode VPS - 美国虚拟主机 | IT牛人博客聚合网站

什么是分布式一致性

2021-09-05 10:49:53

在工程实践上, 分布式一致性和多副本有关系, 如果没有多副本, 就没有分布式一致性的问题.

多副本的定义: 多副本可以放在多台机器上, 也可以放在同一个进程内的不同内存地址内, 或者一个副本在内存, 一个副本在硬盘. 只要同一个对象出现在多处, 或者在多处被引用, 就是多副本.

各个副本的写入操作序列必须先经过共识, 按同样的顺序写入, 因此所有副本的状态将是最终一致的(相同). 但是, 有可能单独地读取某个副本, 这就导致读操作在不同副本上发生的顺序并不相同, 这显然会导致最终结果不一致(符合预期), 因为我们本能地知道, 顺序决定结果.

例如, 先写后读与先读后写, 显然读出来的结果不一样, 这个很显然. 因为日志序列的复制和执行必然是异步的, 绝对不可能所有副本在同一个时间点同时写入, 必然有一个时间差, 这也是很显然的. 因此, 如果轻率地去读取不同副本, 将可能导致读取的结果不同, 因为某个写入操作可能只在某个副本上执行了, 而在另一个副本上还没有执行, 所以读取的结果不同, 这是很显然的.

所以, 分布式一致性的本质在这里就显而易见了 - 那就是让操作在某个副本上发生的顺序符合我们的期望.

例如, 我们可能读取副本A, 也可能读取副本B, 或者同时读取副本A和B, 如果我们让所有的操作, 包括读和写在两个副本上发生的顺序相同, 那么, 结果必然是一致的, 这是很显然的.

这也是为什么有一些系统, 会把读请求放入 Raft 日志序列中进行同步的原因, 因为不同副本上的 Raft 的日志序列是相同排序的. 一条 Raft 日志序列是全序的, 但不相关的操作之间可能没有相互依赖和影响, 所以全序没有必要. 例如, 某些日志操作对象 a, 某些日志操作对象 b, 这两类操作之间没有必要排序, 让它们单独排序就可以了, 即所谓的偏序.

简单的实现方法就是做 Sharding, 把针对不同对象的操作拆分到不同的日志序列中, 也即所谓的 Multi Raft Group.

还有一种实现是不把读操作放到日志序列中, 而是通过停止等待, 等待某一个写操作在某个副本发生之后, 再执行读操作, 便保证了顺序. 这就是 Read Index 的本质, Read Index 先找出全部副本的最新的共识, 然后等最新的那条共识执行之后, 再执行读操作, 也就必然保证读操作发生在我们期望的那一次写操作之后.

Related posts:

  1. Paxos和Raft读优化 – Quorum Read 和 Read Index
  2. Raft 为什么不能直接 commit 前任的日志?
  3. Paxos 与分布式强一致性
  4. 什么是 Paxos 的日志空洞?
  5. 分布式存储名词解析 – 一致性

你现在看的文章是: 什么是分布式一致性

Linode VPS - 美国虚拟主机 | IT牛人博客聚合网站

记一次关于系统性能的有趣讨论

2021-09-04 10:29:04

有个同事问我:"你开发的分布式数据库系统, 如何避免 scan 扫描操作返回了 pending 事务状态的数据?"

我说:"把数据扫描出来, 然后逐个判断过滤掉 pending 状态的数据."

我感到奇怪, 对于他的问题, 解决方案非常显然啊. 解决方案非常直观, 兵来将挡水来土掩, 我相信他也能想到, 不想要的数据当然要剔除掉, 否则呢? 那么, 他的问题的点在哪?

没错, 他接着问了:"我 scan 的时候只想返回 key, 但是, 要判断状态, 是不是还得去读取 value 解析出状态信息?"

我当时是黑人问号脸:"当然要知道状态信息才能根据状态过滤呀, 不然呢?"

他终于祭出了那个经典的让人哭笑不得的问题:"读取 value, 是不是性能会下降啊?"

且不说无缘无故在没有任何场景支撑的情况下就提出性能问题让人莫名其妙, 这有什么性能问题? 我当然知道, 一次内存拷贝当然比两次内存拷贝性能高, 一次 IO 当然比两次 IO 性能高, 所以呢?

这种凭空想出的所谓"性能问题"有什么可讨论的? 你觉得一次 IO 性能高, 那只能在写入的时候把需要的数据聚簇到一起, 以便将来能一次读取. 也就是在写入的时候多付出成本, 以换取读取的时候节省成本, 道理不是很简单吗?

所以我说:"那就把状态信息放一起, scan 的时候就一起读出来了. 或者把 pending 状态的数据放到别的地方, scan 时自然不返回."

他应该是悟了, 说:"哦...", 然后走了.

我直到现在还是黑人问号脸... 感觉就像经历了一次蜘蛛蚂蚁, 打雷下雨一样的显而易见的因果关系, 感觉他是在寻找银弹.

其实, 系统层面的方案, 并不像冒泡排序和快速排序的算法性能差异那么明显, 系统层面的算法(方案, 策略, 流程)都是跟着需求走的, 功能需求要求怎么做就怎么做, 一就是一, 一之后就是二, 按部就班照着做, 非常直白, 没有一丁点可以偷工减料的地方, 朝三暮四和朝四暮三的障眼法更不可取.

不想要就过滤掉, 需要的就找出来, 不想多次IO就聚簇存储, 硬盘慢就放内存中, 内存不够就加内存或者淘汰数据, 锁粒度太大了那就增加锁数量减小锁粒度, 临界区太长了那就减少临界区长度, 操作次数太多了那就合并操作, 串行化吞吐量不满足需求那就并发多线程, 如果认为多线程调度成本太大那就换多路复用接口... 软件系统工程的事, 一切都是那么地自然和直观.

Related posts:

  1. Raft Read Index 的实现
  2. SSDB 采用里程碑式版本发布机制
  3. SSDB 1.6.6 稳定版发布, 支持 hclear/zclear
  4. Paxos 所谓的”幽灵复现”
  5. SSDB增加hlist, zlist命令

你现在看的文章是: 记一次关于系统性能的有趣讨论

Linode VPS - 美国虚拟主机 | IT牛人博客聚合网站

Binlog 和 Redolog 的区别

2021-09-02 21:29:51

在开发分布式数据库的过程中, Binlog 和 Redolog 是非常重要的两个概念, 两者的作用似乎相同, 但实际上各有各的使用场景. 从多副本复制一致性的角度看, Binlog 用于强一致性, Redolog 用于最终一致性.

Binlog 可包含非幂等的指令, 例如 incr 指令. Redolog 只能包含幂等的指令, 例如 set 指令.

全球跨地域同步最终一致, 能不能复制 Binlog 呢? 绝对不行! 使用 incr 和 set 指令的组合, 在不同的地域写入数据, 很容易就能发现可造成数据不一致(相同)的场景, 而且几乎无法避免(除非副本带有回滚功能). 而如果同步的是 Redolog 的话, 通过复合时间戳, 是可以实现多副本的最终一致的.

对于强一致的多副本, 能不能复制 Redolog 呢? 似乎是可行的. 例如, 收到 incr 指令, 可以先转换成对应的 set 指令. 但是, 共识过程可能耗费较长时间, 如果这时再来一个 incr 指令, 则必须将这个指令阻塞(因为两个指令有依赖), 否则生成的 set 指令将是错误的. 而如果复制的是 Binlog, 则没有这个问题, 两个 incr 指令可以并发地进行共识流程.

如果你还是不死心, 还是想在强一致性的多副本之间复制 Redolog, 行不行? 刚才得到的问题也有解决方案, 不过, 复杂度太大, 可能的解决方案也许还要引入更多的问题, 你最好放弃.

串行化是简化问题的最高效方案, 可以简化依赖的处理, 避免不必要的引用. 所以, 越早串行化越好, 对 Binlog 达成共识, 可以避免对 Redolog 达成共识可能造成的回滚问题. 一条 Binlog 如果共识不了, 换个位置尝试共识即可, 计算成本很小. 如果生成 Redolog 再共识, 一旦共识不了, 就要回滚重新生成 Redolog 再尝试共识.

Related posts:

  1. Binlog, Redolog 在分布式数据库系统中的应用
  2. MySQL binlog查看和清理
  3. 关于多写入点数据库集群的一些想法
  4. 数据库内核的并发控制
  5. SSDB源码分析 – 主从和多主同步原理解析

你现在看的文章是: Binlog 和 Redolog 的区别

Linode VPS - 美国虚拟主机 | IT牛人博客聚合网站

企业级SSD硬盘fsync速度

2021-08-27 20:42:53

小数据测试, 以便对硬盘 fsync 的速度有一个大概的了解. 结果:

rate latency 备注
4044/s 0.247ms Intel SATA SSD
19720/s 0.051ms Intel NVMe SSD

结论: SATA 盘的 QPS 是 4000, NVMe 的 QPS 是 20000.

如果要开发一个分布式 KV 数据库, 那么对于每一个客户端请求, 至少进行 1 次日志 fsync. 为了提高吞吐量(QPS), 日志模块必须进行 batch 持久化.

如果 batch 大小是 25 的话, 普通 SATA SSD 盘能达到 10w qps, 而 NVMe SSD 只需要 batch 是 5 即可达到 10w qps.

Related posts:

  1. Linux 核心编程 – fsync, write
  2. Redis被bgsave和bgrewriteaof阻塞的解决方法
  3. 接口与实现分离
  4. 数据库的持久化等级
  5. C++ Latch 实现

你现在看的文章是: 企业级SSD硬盘fsync速度

Linode VPS - 美国虚拟主机 | IT牛人博客聚合网站

C++ Latch 实现

2021-08-26 21:10:33

Latch(Binary Semaphore) 不同于信号量(Counting Semaphore), 也不同于条件变量, 它是一种合并信号成一个标记的通信方式, 可用于实现 Batch 操作. 例如, 两个线程围绕一个标记, 一个设置(生产者), 一个复位(消费者). 如果标记已设置, 则消费者立即复位然后返回. 如果标记未设置, 则消费者等待标记被设置.

在生产者消费者编程模式中, 生产者产生任务, 任务被加入队列中, 同时通过 Latch 告知消费者. 使用 Latch 的话, 可能多个任务产生之后, 消费者才会获知消息, 于是, 便可以将多个任务合并处理, 也即所谓的 Batch(批量)操作. 同时, 只要有任务, 标记就一定会被设置, 消费者不会漏掉任务.

如果使用条件变量, 那么消费者可能漏消息. 因为, 如果消费者在忙于处理任务时, 生产者的通知将会被丢弃.

如果使用信号量, 那么消费者进行 Batch 操作之后, 后续的堆积的信号会导致空操作, 因为任务已经处理完了, 但信号还在.

所以, Latch 是一种非常有用的数据结构.

class Latch
{
private:
    std::mutex mux;
    std::condition_variable cv;
    bool flag = false;

public:
    void wait(){
        std::unique_lock<std::mutex> lock(this->mux);
        while(!this->flag){
            this->cv.wait(lock);
        }
        this->flag = false;
    }

    void notify(){
        {
            std::unique_lock<std::mutex> lock(this->mux);
            this->flag = true;
        }
        this->cv.notify_one();
    }
};

Golang 版本: https://github.com/ssdb-cluster/base/blob/master/util/latch.go

Related posts:

  1. 生产者消费者模型中生产者的速度快于消费者时所产生的问题及其解决方法讨论
  2. 必须放在循环中的pthread_cond_wait
  3. C#环形缓冲
  4. C# 中实现 FIFO 缓冲区–ArrayBuffer
  5. CSS 样式规则的匹配算法实现

你现在看的文章是: C++ Latch 实现

Linode VPS - 美国虚拟主机 | IT牛人博客聚合网站

Mac 看图软件 Tovi 免费下载

2021-08-22 10:53:00

我开发的 Mac 看图软件 Tovi, 支持播放 GIF 动画, 以及用箭头按键浏览上一张下一张. 支持缩放, 旋转, 导出成 mp4 视频等等. Tovi 曾经作为收费软件在苹果 App Store 上售卖, 现在, Tovi 已经免费了.

下载地址: http://tovi.ideawu.com/

Tovi 运行截图:

注意: 启动后, 请将根目录 / 添加到权限列表中(点击"+", Shift + Command + G, 然后输入 '/').

Related posts:

  1. Mac 下最好用的看图软件 Tovi 免费下载了!
  2. Mac OSX下的看图软件Tovi
  3. Mac下改变文件关联(打开方式)的方法
  4. “打开方式”里无法选择程序的解决方法

你现在看的文章是: Mac 看图软件 Tovi 免费下载

Linode VPS - 美国虚拟主机 | IT牛人博客聚合网站

生产者消费者模式的系统性能分析方法

2021-08-21 12:04:42

前一篇文章介绍了生产者消费者编程模式, 一种非常流行且强大的编程模式. 本文将分析采用这种模式的系统的性能分析方法, 以做性能优化.

系统性能分析主要关注这几个指标:

其中, qps 和 latency 在通俗语义在常常统称为"性能", 这会给科学分析带来歧义, 所以我们要特别注意弄清楚并区分. qps 和 latency 的关系公式为:

qps = 1 / avg_latency    // 时间单位为秒
avg_latency = 1 / qps    // 时间单位为秒

等式的右边为观测值(实证), 左边为计算结果.

对于采用生产者消费者模式的系统, 针对其性能指标的分析方法是非常清晰的. 我们将系统划分成3个部分:

生产者 -> 队列 -> 消费者

那么, 一个任务被生产者生产之后, 将加入到队列中排队. 任务在队列中可能会等待一些时间, 然后被消费者消费. 一个任务一般对应了一个客户端请求, 因此, 请求在系统内部的处理耗时可分成3个部分:

对应的, 生产者和消费者的处理速度为:

队列作为一个通信管道, 不关注其处理速度(qps), 而主要关注其延迟, 也即任务排队耗时.

采用生产者消费者模式的系统, 其各个模块分隔得比较清晰, 易于单独测试, 因此这种系统易于排查问题和优化性能.

理想模型

假设排队耗时为零, 如果能实证 q1 和 q2以及 t1 和 t2, 那么系统的 qps (能力)和 latency 可由公式计算得出:

qps = MIN(q1, q3)
latency = t1 + t3

在理想情况下, 不仅排队没有成本, 而且各个模块的处理也完全独立, 例如不争抢 CPU 或者磁盘 IO 资源. 在硬件资源比较富裕的前提下, 这个分析公式是合理的.

任务排队等待

实践中, 模块由工作线程组成, 但是工作线程数量(并发数)会被限制, 如果任务的到来不是均匀的且超过并发数, 那么便会造成任务排队.

例如, 假设生产者的并发数是 1, 平均处理一个任务的耗时是 1 秒钟. 如果生产者平均每秒生产一个任务的话, 那么任务的排队时间是零, 也即没有任务排队. 但是, 如果生产者集中每秒生产 2 个任务, 那么, 将造成其中一个任务额外排队等待 1 秒钟, 导致最终处理耗时是 2 秒钟.

因此, 平均地生产任务, 确保同一时刻同时生产的任务数量不要超过消费者的并发数, 对于保证每个任务的处理耗时是有帮助的.

实践中, 限时抢购类活动对系统产生突发冲击, 从而导致请求排队, 最终导致请求处理耗时增加(部分请求的耗时远远超过平均值).

所以, 一个处理能力是 qps=10 的系统, 不代表每个请求的处理耗时都会是平均的 100ms, 如果集中冲击(并发)系统, 那么最长的请求的耗时可能将是 900ms, 就是因为客户端的并发数超过了系统的工作线程数量导致请求排队等待.

Related posts:

  1. Redis被bgsave和bgrewriteaof阻塞的解决方法
  2. 企业级SSD硬盘fsync速度
  3. LevelDB 写操作出现停顿的问题分析
  4. Paxos 算法实现和工程落地: 选主与复制状态机
  5. 海量游戏数据的即时分析和挖掘

你现在看的文章是: 生产者消费者模式的系统性能分析方法

Linode VPS - 美国虚拟主机 | IT牛人博客聚合网站

生产者消费者编程模式

2021-08-15 09:42:47

相信很多人都知道"生产者消费者"编程模式, 也使用过这种模式, 但是, 可能只是本能不自觉地使用过, 未必对这种模式有清晰和深刻的理解. 特别是级联生产者消费者模式, 更是强大无比. 很多人可能没有意识到, Golang 语言的核心思想正是生产者消费者模式, 也即 go routine + channel.

假设有一个功能, 处理某个任务需要进行3个步骤, 那么代码可以这样写:

func worker(t *Task) {
    step1(t)
    step2(t)
    step3(t)
}

for t := recv_task(); t != nil {
    worker(t)
}

这样的代码非常直观, 也是我所推崇的程序设计核心原则之一. 不过, 这段代码毕竟是串行化的, 中间某个步骤会阻塞后面的步骤.

对于串行化阻塞问题的最直接解决方案就是多线程并发. 例如, 每收到一个任务, 就启动一个线程去处理:

for t := recv_task(); t != nil {
    go worker() // 启动线程/协程
}

这种并发模式有一定的缺陷, 一是并发数量不受控制, 二是如果做了并发数量控制, 这样的控制也不高效. 例如限制最多只有 N 个 worker 线程在同时运行, 如果刚好这 N 个线程都被阻塞在某一个步骤时, 那么整个系统就都没有在工作, 是被闲置的.

但是, 如果我们改成流水线并发, 也即分段并发, 针对上面的代码逻辑启动 3 组工作线程池, 每一组是 N/3 个线程, 代码结构就变成:

q1, q2, q3 channel // 3 个任务队列, 分别给 3 组工作线程提供任务

// 启动所有工作线程组
for i := 0; i < N/3; i ++ {
    go worker1()
    go worker2()
    go worker3()
}

func worker1() {
    for t := q1.recv_task(); t != nil {
        step1(t)
        q2.push_task(t) // 将任务传给下一组工作线程
    }
}

func worker2() {
    for t := q2.recv_task(); t != nil {
        step2(t)
        q3.push_task(t) // 将任务传给下一组工作线程
    }
}

func worker3() {
    for t := q3.recv_task(); t != nil {
        step3(t)
        // 处理完毕
    }
}

这种编程模式, 不仅高效, 而且模块解耦. 即使 worker2 的所有线程都阻塞了, worker1 和 worker3 依然在工作, 计算机的算力就不会被闲置.

这样的系统如果遇到性能瓶颈, 要进行优化的话, 思路也很直观: 就是统计各种工作线程的工作速度, 然后根据阿姆达尔定律去优化最慢的那个模块. 职责划分非常清晰, 系统结构简洁.

补充: CPU 指令流水线

CPU 指令流水线技术是一种将指令分解为多步, 并让不同指令的各步操作重叠(并发), 从而实现几条指令并行处理, 以加速程序运行过程的技术.

上面的编程模式和 CPU 指令流水线技术是一样的.

Related posts:

  1. 程序设计核心原则: 直观
  2. 接口与实现分离
  3. 小心递归次数限制
  4. 使用 Channel 进行可靠传输
  5. 消除JavaScript闭包的一般方法

你现在看的文章是: 生产者消费者编程模式

Linode VPS - 美国虚拟主机 | IT牛人博客聚合网站

Paxos 算法难以理解吗?

2021-08-11 21:29:19

Paxos 被冠以"晦涩难懂"的恶名, 一方面来源于它自身的定位不清, 边界模糊, 另一方面来源于它并不直接解决工程上广泛的强烈需求. 工程师们需要一个算法(规则, 协议), 用来开发一个分布式多副本系统, 并让多副本对外表现得像一个单一副本的效果(强一致性, 线性一致性, 外部一致性). 坦率地说, Paxos 距离这个需求有十万八千里. 所以, 广大的工程师便认为 Paxos 算法难以理解.

首先, 我们需要理解 Paxos 的算法的定位. 不幸地是, 在这第一步, 我们就遇到的麻烦! 大多数人接触到 Paxos 都是从 Basic Paxos 的两个步骤(1a, 1b, 2a, 2b)开始的. 人们花费了大量的精力来记忆这当中的操作步骤, 但是, 却看不到为什么要这样做.

第一个问题也出现了, Paxos 是不是等于 Basic Paxos? Paxos 是不是就等于那两个步骤? 我们还没谈到 Multi Paxos, 另一个极不完善的理论.

如果把 Basic Paxos 理解为 Paxos 算法的全部, 那么, Paxos 算法其实非常容易理解, 只需要记住两个步骤即可. 这两个步骤, 从头到尾围绕着 3 个变量在做操作, 没什么难的.

那么接下来的问题就是, Basic Paxos 能解决什么问题? 空泛地讲, Basic Paxos 用于决定多副本的最终状态, 也即共识. 这时候, 工程师们又犯迷糊了:

这些问题都是工程师们的拦路虎绊脚石, 如果不解答的话, 寸步难行. 答案很简单:

Paxos 对这些问题没有直接卵用!

Basic Paxos 从来就没有想去直接解答这些问题, 它把责任推给你了! Paxos 说:"我就是决定实例的值, 其它的我都不管." 到了这里, 你知道为什么 Paxos 被冠以恶名了吧? 工程师面前绕不过去的亟待解决的重要工程问题, 全在它的边界之外... 你说, Paxos "难"不难? 你是否弄懂了 Paxos 的边界?

所以说, Paxos 难就难在, 所谓的 Prepare, Accept, Chosen, 通通没有直接的工程意义, 要解决工程问题, 你可以利用它, 再做 99% 的工作. 如果没有那 99% 的工作, 即使有 Paxos, 你一行代码也写不出来.

Paxos 难并不难在所谓的数学证明, 数学之美, 因为并不是所有人都需要去理解数学证明, 证明不证明与我无关. 有时候, 我们只需要知道科学家已经证明过了即可.

Paxos 难就难在, 它不解决广泛的强烈需求. 如果你先有需求, 再找解决方案的话, 那么, Paxos 确实非常难, 因为它不解决你的问题. 如果你陷入自我怀疑, 尝试去弄懂所谓的优美数学证明, 依然没用, 就算弄懂了数学证明, 你也解决不了任何问题. 这时候, 你只会陷入更深的自我怀疑, 只能找个理由说:"Paxos 太难了..."

所以说, Paxos 很简单, 因为 Basic Paxos 的两个步骤只关注 3 个变量, 完完全全照着做就行了, 没什么难度. Paxos 非常难, 因为它不直接解决问题, 你还要做 99% 的工作, 而那 99% 的工作对你来说是黑洞, 才是最难的地方.

进一步阅读:

Related posts:

  1. Paxos 与分布式强一致性
  2. 关于 Paxos 论文中的迷惑之处
  3. Paxos vs Raft 的争论
  4. 什么是 Paxos 的日志空洞?
  5. 为什么 Leader Based 的分布式协议 Raft 是更好的

你现在看的文章是: Paxos 算法难以理解吗?

Linode VPS - 美国虚拟主机 | IT牛人博客聚合网站