├── .DS_Store ├── .github ├── .DS_Store └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── .gitignore ├── .meta ├── ABOUT.md └── SUMMARY.md ├── .nojekyll ├── 01~分布式基础 ├── 01~不可靠的分布式系统 │ ├── .meta │ │ └── 插图.xmind │ ├── README.md │ ├── 不可靠时钟.md │ ├── 不可靠网络.md │ └── 不可靠进程.md ├── 02~节点与集群 │ ├── 主从节点 │ │ ├── README.md │ │ └── 节点选举.md │ └── 分布式互斥.md ├── 03~CAP │ ├── 99~参考资料 │ │ └── 2024~The CAP Theorem. The Bad, the Bad, & the Ugly.md │ ├── BASE.md │ ├── DLS.md │ ├── README.md │ └── 特性与模型.md ├── 04~日志模型 │ ├── WAL │ │ └── README.md │ └── 分割日志 │ │ └── README.md ├── 99~参考资料 │ ├── Distributed Systems for Fun and Profit │ │ ├── English │ │ │ └── .gitkeep │ │ ├── README.md │ │ └── 中文翻译 │ │ │ ├── 00.引言.md │ │ │ ├── 01.Basics.md │ │ │ ├── 02.Abstraction.md │ │ │ ├── 03.TimeAndOrder.md │ │ │ ├── 04.PreventingDivergence.md │ │ │ ├── 05.AcceptingDivergence.md │ │ │ └── 10.Appendix.md │ ├── Patterns of Distributed Systems │ │ ├── README.md │ │ ├── consistent-core.md │ │ ├── follower-reads.md │ │ ├── generation-clock.md │ │ ├── gossip-dissemination.md │ │ ├── heartbeat.md │ │ ├── high-water-mark.md │ │ ├── hybrid-clock.md │ │ ├── idempotent-receiver.md │ │ ├── lamport-clock.md │ │ ├── leader-and-followers.md │ │ ├── lease.md │ │ ├── low-water-mark.md │ │ ├── overview.md │ │ ├── paxos.md │ │ ├── quorum.md │ │ ├── replicated-log.md │ │ ├── request-pipeline.md │ │ ├── segmented-log.md │ │ ├── single-socket-channel.md │ │ ├── singular-update-queue.md │ │ ├── state-watch.md │ │ ├── two-phase-commit.md │ │ ├── version-vector.md │ │ ├── versioned-value.md │ │ └── write-ahead-log.md │ └── 分布式系统的八大谬误.md └── README.md ├── 02~一致性与共识 ├── README.md ├── 一致性模型 │ ├── README.md │ ├── 其他一致性模型.md │ ├── 因果一致性.md │ ├── 最终一致性.md │ ├── 线性一致性.md │ └── 顺序一致性.md ├── 共识算法 │ ├── Paxos │ │ ├── Multiple-Paxos.md │ │ └── README.md │ ├── README.md │ ├── Raft │ │ ├── 99~参考资料 │ │ │ ├── 2016-Raft 原论文-寻找一种易于理解的一致性算法.md │ │ │ └── 2021-多颗糖-条分缕析 Raft.md │ │ ├── README.md │ │ ├── 安全性.md │ │ ├── 日志复制.md │ │ └── 选举与成员变更.md │ ├── ZAB │ │ └── README.md │ └── 算法设计 │ │ ├── README.md │ │ └── 算法对比.md ├── 分布式时钟 │ ├── README.md │ └── 序列号 │ │ ├── 全序广播.md │ │ └── 序列号顺序.md └── 拜占庭问题 │ ├── README.md │ ├── 拜占庭故障.md │ └── 系统模型.md ├── 10~分布式存储 └── README.link ├── 20~分布式计算 └── README.link ├── 88~其他分布式概念 ├── 分布式 ID │ └── README.link ├── 分布式事务 │ └── README.link ├── 分布式操作系统 │ └── README.link ├── 分布式数据库 │ └── README.link └── 分布式锁 │ └── README.link ├── 99~参考资料 └── 2024~Predicting the Future of Distributed Systems.md ├── INTRODUCTION.md ├── LICENSE ├── README.md ├── _sidebar.md ├── header.svg └── index.html /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/DistributedSystem-Notes/d5c81c808d80e3697e264031c4ec5eb29c40f7d3/.DS_Store -------------------------------------------------------------------------------- /.github/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/DistributedSystem-Notes/d5c81c808d80e3697e264031c4ec5eb29c40f7d3/.github/.DS_Store -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all 2 | * 3 | 4 | # Unignore all with extensions 5 | !*.* 6 | 7 | # Unignore all dirs 8 | !*/ 9 | 10 | .DS_Store 11 | 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | 71 | # next.js build output 72 | .next 73 | -------------------------------------------------------------------------------- /.meta/ABOUT.md: -------------------------------------------------------------------------------- 1 | # ABOUT | 关于 2 | 3 | # 规划 4 | 5 | # 致谢 6 | 7 | 由于笔者平日忙于工作,几乎所有线上的文档都是我夫人帮忙整理,在此特别致谢;同时也感谢我家的布丁安静的趴在脚边,不再那么粪发涂墙。 8 | 9 | ![](https://cdn-images-1.medium.com/max/1800/1*BOTGqwpA7mefNBi_muyAJQ.jpeg) 10 | 11 | # Links 12 | -------------------------------------------------------------------------------- /.meta/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | ## [Linux 与操作系统](../Linux 与操作系统/README.md) 4 | 5 | - [Introduction](../Linux 与操作系统/README.md) 6 | 7 | ## [分布式存储](../分布式存储/README.md) 8 | 9 | - [Introduction](../分布式存储/README.md) 10 | 11 | - [HDFS](../分布式存储/HDFS/README.md) 12 | - [HDFS 源代码分析](../分布式存储/HDFS/HDFS 源代码分析.md) 13 | - [HDFS 编程](../分布式存储/HDFS/HDFS 编程.md) 14 | - [HDFS 读取原理](../分布式存储/HDFS/HDFS 读取原理.md) 15 | - [一致性案例](../分布式存储/一致性案例/README.md) 16 | - [因果一致性](../分布式存储/一致性案例/因果一致性.md) 17 | - [一致性算法](../分布式存储/一致性算法/README.md) 18 | - [Multiple-Paxos](../分布式存储/一致性算法/Multiple-Paxos.md) 19 | - [Paxos](../分布式存储/一致性算法/Paxos.md) 20 | - [Raft](../分布式存储/一致性算法/Raft.md) 21 | - [ZAB](../分布式存储/一致性算法/ZAB.md) 22 | - [分布式 ID](../分布式存储/分布式 ID/README.md) 23 | - [Leaf](../分布式存储/分布式 ID/Leaf.md) 24 | - [Snowflake](../分布式存储/分布式 ID/Snowflake.md) 25 | - [UUID](../分布式存储/分布式 ID/UUID.md) 26 | - [库自增 ID](../分布式存储/分布式 ID/库自增 ID.md) 27 | - [分布式事务](../分布式存储/分布式事务/README.md) 28 | - [JDTX](../分布式存储/分布式事务/JDTX/README.md) 29 | - [Seata](../分布式存储/分布式事务/Seata/README.md) 30 | - [事务机制](../分布式存储/分布式事务/事务机制/README.md) 31 | - [Saga](../分布式存储/分布式事务/事务机制/Saga/README.md) 32 | - [Saga](../分布式存储/分布式事务/事务机制/Saga/Saga.md) 33 | - [TCC](../分布式存储/分布式事务/事务机制/TCC/README.md) 34 | - [事务消息](../分布式存储/分布式事务/事务机制/事务消息/README.md) 35 | - [流处理方案](../分布式存储/分布式事务/事务机制/事务消息/流处理方案.md) 36 | - [多阶段提交](../分布式存储/分布式事务/事务机制/多阶段提交/README.md) 37 | - [分布式文件系统](../分布式存储/分布式文件系统/README.md) 38 | - [分布式时钟](../分布式存储/分布式时钟/README.md) 39 | - [分布式系统理论](../分布式存储/分布式系统理论/README.md) 40 | - [BASE](../分布式存储/分布式系统理论/BASE.md) 41 | - [CAP](../分布式存储/分布式系统理论/CAP.md) 42 | - [分布式锁](../分布式存储/分布式锁/README.md) 43 | - [RESTful 分布式锁](../分布式存储/分布式锁/RESTful 分布式锁.md) 44 | - [Redis 分布式锁](../分布式存储/分布式锁/Redis 分布式锁.md) 45 | - [Zookeeper 分布式锁](../分布式存储/分布式锁/Zookeeper 分布式锁.md) 46 | - [副本管理机制](../分布式存储/副本管理机制/README.md) 47 | - [主从复制](../分布式存储/副本管理机制/主从复制.md) 48 | - [存储类型](../分布式存储/存储类型/README.md) 49 | - [分布式存储](../分布式存储/存储类型/分布式存储.md) 50 | - [对象存储](../分布式存储/对象存储/README.md) 51 | - [Haystack](../分布式存储/对象存储/Haystack.md) 52 | - [元数据管理](../分布式存储/对象存储/元数据管理.md) 53 | - [数据一致性](../分布式存储/数据一致性/README.md) 54 | - [分布式一致性](../分布式存储/数据一致性/分布式一致性.md) 55 | - [拜占庭将军问题](../分布式存储/数据一致性/拜占庭将军问题.md) 56 | 57 | ## [分布式计算](../分布式计算/README.md) 58 | 59 | - [Introduction](../分布式计算/README.md) 60 | 61 | - [Beam](../分布式计算/Beam/README.md) 62 | - [快速开始](../分布式计算/Beam/快速开始.md) 63 | - [部署与配置](../分布式计算/Beam/部署与配置.md) 64 | - [DAG](../分布式计算/DAG/README.md) 65 | - [Dryad](../分布式计算/DAG/Dryad.md) 66 | - [Flink](../分布式计算/Flink/README.md) 67 | - [SQL](../分布式计算/Flink/SQL.md) 68 | - [代码开发](../分布式计算/Flink/代码开发.md) 69 | - [环境配置](../分布式计算/Flink/环境配置.md) 70 | - [Blink](../分布式计算/Flink/Blink/README.md) 71 | - [Hadoop](../分布式计算/Hadoop/README.md) 72 | - [MapReduce](../分布式计算/Hadoop/MapReduce/README.md) 73 | - [CRUD](../分布式计算/Hadoop/MapReduce/CRUD.md) 74 | - [聚合计算](../分布式计算/Hadoop/MapReduce/聚合计算.md) 75 | - [Kafka](../分布式计算/Kafka/README.md) 76 | - [消息存储](../分布式计算/Kafka/消息存储.md) 77 | - [消息消费](../分布式计算/Kafka/消息消费.md) 78 | - [消息类型](../分布式计算/Kafka/消息类型.md) 79 | - [部署配置](../分布式计算/Kafka/部署配置.md) 80 | - [集群与高可用](../分布式计算/Kafka/集群与高可用.md) 81 | - [Kafka Streams](../分布式计算/Kafka/Kafka Streams/README.md) 82 | - [Pulsar](../分布式计算/Pulsar/README.md) 83 | - [消息存储](../分布式计算/Pulsar/消息存储.md) 84 | - [消息消费](../分布式计算/Pulsar/消息消费.md) 85 | - [消息类型](../分布式计算/Pulsar/消息类型.md) 86 | - [部署配置](../分布式计算/Pulsar/部署配置.md) 87 | - [RabbitMQ](../分布式计算/RabbitMQ/README.md) 88 | - [Java](../分布式计算/RabbitMQ/Java.md) 89 | - [Python](../分布式计算/RabbitMQ/Python.md) 90 | - [消息存储](../分布式计算/RabbitMQ/消息存储.md) 91 | - [消息消费](../分布式计算/RabbitMQ/消息消费.md) 92 | - [消息类型](../分布式计算/RabbitMQ/消息类型.md) 93 | - [部署配置](../分布式计算/RabbitMQ/部署配置.md) 94 | - [集群与高可用](../分布式计算/RabbitMQ/集群与高可用.md) 95 | - [RocketMQ](../分布式计算/RocketMQ/README.md) 96 | - [消息操作](../分布式计算/RocketMQ/消息操作.md) 97 | - [消息消费](../分布式计算/RocketMQ/消息消费.md) 98 | - [消息类型](../分布式计算/RocketMQ/消息类型.md) 99 | - [集群与高可用](../分布式计算/RocketMQ/集群与高可用.md) 100 | - [Spark](../分布式计算/Spark/README.md) 101 | - [代码开发](../分布式计算/Spark/代码开发.md) 102 | - [环境配置](../分布式计算/Spark/环境配置.md) 103 | - [Waltz](../分布式计算/Waltz/README.md) 104 | - [分布式调度](../分布式计算/分布式调度/README.md) 105 | - [任务调度](../分布式计算/分布式调度/任务调度.md) 106 | - [单机资源管理](../分布式计算/分布式调度/单机资源管理.md) 107 | - [数据调度](../分布式计算/分布式调度/数据调度.md) 108 | - [计算调度](../分布式计算/分布式调度/计算调度.md) 109 | - [流处理](../分布式计算/流处理/README.md) 110 | - [Dataflow 模型](../分布式计算/流处理/Dataflow 模型.md) 111 | - [分布式快照](../分布式计算/流处理/分布式快照.md) 112 | - [流处理框架对比](../分布式计算/流处理/流处理框架对比.md) 113 | - [消息投递](../分布式计算/流处理/消息投递.md) 114 | - [运行与编程模型](../分布式计算/流处理/运行与编程模型.md) 115 | - [消息中间件](../分布式计算/消息中间件/README.md) 116 | - [中间件模型](../分布式计算/消息中间件/中间件模型.md) 117 | - [消息存储](../分布式计算/消息中间件/消息存储.md) 118 | - [消息消费](../分布式计算/消息中间件/消息消费.md) 119 | - [消息类型](../分布式计算/消息中间件/消息类型.md) 120 | - [集群与高可用](../分布式计算/消息中间件/集群与高可用.md) 121 | - [边缘计算](../分布式计算/边缘计算/README.md) 122 | 123 | ## [数据库](../数据库/README.md) 124 | 125 | - [Introduction](../数据库/README.md) 126 | 127 | ## [网络](../网络/README.md) 128 | 129 | - [Introduction](../网络/README.md) 130 | 131 | - [DNS](../网络/DNS/README.md) 132 | - [CDN](../网络/DNS/CDN.md) 133 | - [HTTP](../网络/HTTP/README.md) 134 | - [Get 与 Post](../网络/HTTP/Get 与 Post.md) 135 | - [HTTP 响应](../网络/HTTP/HTTP 响应.md) 136 | - [HTTP 请求](../网络/HTTP/HTTP 请求.md) 137 | - [发展历史](../网络/HTTP/发展历史.md) 138 | - [状态码](../网络/HTTP/状态码.md) 139 | - [缓存](../网络/HTTP/缓存.md) 140 | - [请求与响应体](../网络/HTTP/请求与响应体.md) 141 | - [请求与响应头](../网络/HTTP/请求与响应头.md) 142 | - [HTTP2](../网络/HTTP2/README.md) 143 | - [协议](../网络/HTTP2/协议.md) 144 | - [实战](../网络/HTTP2/实战.md) 145 | - [HTTP3](../网络/HTTP3/README.md) 146 | - [HTTPS](../网络/HTTPS/README.md) 147 | - [加密算法](../网络/HTTPS/加密算法.md) 148 | - [工具与配置](../网络/HTTPS/工具与配置.md) 149 | - [性能优化](../网络/HTTPS/性能优化.md) 150 | - [握手过程](../网络/HTTPS/握手过程.md) 151 | - [Samba](../网络/Samba/README.md) 152 | - [环境配置](../网络/Samba/环境配置.md) 153 | - [Socket](../网络/Socket/README.md) 154 | - [Socket](../网络/Socket/Socket.md) 155 | - [TCPIP](../网络/TCPIP/README.md) 156 | - [OSI 模型](../网络/TCPIP/OSI 模型.md) 157 | - [Socks](../网络/TCPIP/Socks.md) 158 | - [TCP 协议](../网络/TCPIP/TCP 协议.md) 159 | - [WebSocket](../网络/WebSocket/README.md) 160 | 161 | ## [虚拟化与云计算](../虚拟化与云计算/README.md) 162 | 163 | - [Introduction](../虚拟化与云计算/README.md) 164 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/DistributedSystem-Notes/d5c81c808d80e3697e264031c4ec5eb29c40f7d3/.nojekyll -------------------------------------------------------------------------------- /01~分布式基础/01~不可靠的分布式系统/.meta/插图.xmind: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/DistributedSystem-Notes/d5c81c808d80e3697e264031c4ec5eb29c40f7d3/01~分布式基础/01~不可靠的分布式系统/.meta/插图.xmind -------------------------------------------------------------------------------- /01~分布式基础/01~不可靠的分布式系统/README.md: -------------------------------------------------------------------------------- 1 | # 不可靠的分布式系统 2 | 3 | ## 集中式系统 vs 分布式系统 4 | 5 | ### 集中式系统的特点 6 | 7 | - 确定性:相同操作产生相同结果 8 | - 故障模式简单:要么完全正常,要么完全失效 9 | - 稳定性:硬件正常时系统表现可预测 10 | 11 | ### 分布式系统的特点 12 | 13 | - 无共享内存,仅通过不可靠网络通信 14 | - 存在部分失效的可能 15 | - 面临不可靠的时钟和处理暂停问题 16 | 17 | 常见故障示例: 18 | 19 | - 数据中心网络分区 20 | - PDU(配电单元)故障 21 | - 交换机故障 22 | - 机架意外重启 23 | - 数据中心网络或电源故障 24 | - 物理设施损坏(如光缆被挖断) 25 | 26 | ## NPC 问题详解 27 | 28 | ### Network Delay(网络延迟) 29 | 30 | - 数据包可能丢失或任意延迟 31 | - 即使使用 TCP 也无法完全解决延迟问题 32 | - 响应丢失导致状态不确定 33 | 34 | ### Process Pause(进程暂停) 35 | 36 | - 进程可能在任意时刻暂停 37 | - 暂停原因多样: 38 | - 垃圾回收(GC) 39 | - 云服务器迁移 40 | - 暂停时长不可预测(可能持续数分钟) 41 | 42 | ### Clock Drift(时钟漂移) 43 | 44 | - 计算机时钟不完全可靠 45 | - 受硬件限制和环境影响 46 | - NTP 同步可能导致时间跳跃 47 | 48 | ## 分布式系统的抽象 49 | 50 | ### 处理故障的方法 51 | 52 | 1. 简单处理:服务失效并显示错误 53 | 2. 容错处理:通过抽象机制保证服务可用 54 | 55 | ### 关键抽象概念 56 | 57 | 1. 事务 58 | 59 | - 原子性:避免崩溃影响 60 | - 隔离性:处理并发访问 61 | - 持久性:确保数据可靠 62 | 63 | 2. 共识 64 | - 确保节点间数据一致性 65 | - 在不可靠网络中实现困难 66 | 67 | ### 重要理论 68 | 69 | 1. CAP 理论 70 | 71 | - 一致性(Consistency) 72 | - 可用性(Availability) 73 | - 分区容忍性(Partition Tolerance) 74 | - 三者无法同时满足 75 | 76 | 2. FLP 理论 77 | 78 | - 异步环境中共识问题的局限性 79 | - 存在恶意节点时的限制 80 | 81 | 3. DLS 理论 82 | - 同步模型的容错能力 83 | - 部分同步网络的特性 84 | - 异步模型的限制 85 | -------------------------------------------------------------------------------- /01~分布式基础/01~不可靠的分布式系统/不可靠时钟.md: -------------------------------------------------------------------------------- 1 | # 不可靠的时钟 2 | 3 | 在分布式系统中,时间是一件棘手的事情,因为通信不是即时的:消息通过网络从一台机器传送到另一台机器需要时间。收到消息的时间总是晚于发送的时间,但是由于网络中的可变延迟,我们不知道多少时间。这个事实有时很难确定在涉及多台机器时发生事情的顺序。应用程序以各种方式依赖于时钟来回答以下问题: 4 | 5 | - 这个请求是否超时了? 6 | - 这项服务的第 99 百分位响应时间是多少? 7 | - 在过去五分钟内,该服务平均每秒处理多少个查询? 8 | - 用户在我们的网站上花了多长时间? 9 | - 这篇文章在何时发布? 10 | - 在什么时间发送提醒邮件? 11 | - 这个缓存条目何时到期? 12 | - 日志文件中此错误消息的时间戳是什么? 13 | 14 | 而且,网络上的每台机器都有自己的时钟,这是一个实际的硬件设备:通常是石英晶体振荡器。这些设备不是完全准确的,所以每台机器都有自己的时间概念,可能比其他机器稍快或更慢。可以在一定程度上同步时钟:最常用的机制是网络时间协议(NTP),它允许根据一组服务器报告的时间来调整计算机时钟;服务器则从更精确的时间源(如 GPS 接收机)获取时间。 15 | 16 | 在现代计算机中,当我们提到时钟时,其往往指这两种不同的类型:时钟和单调钟;它们都能用于衡量时间,但是它们的目的却相去甚远。 17 | 18 | ## 时钟 19 | 20 | 时钟是您直观地了解时钟的依据:它根据某个日历(也称为挂钟时间(wall-clock time))返回当前日期和时间。例如,Linux 上的 `clock_gettime(CLOCK_REALTIME)` 和 Java 中的 `System.currentTimeMillis()` 返回自 epoch(1970 年 1 月 1 日 午夜 UTC,格里高利历)以来的秒数(或毫秒),根据公历日历,不包括闰秒。有些系统使用其他日期作为参考点。时钟通常与 NTP 同步,这意味着来自一台机器的时间戳(理想情况下)意味着与另一台机器上的时间戳相同。但是如下节所述,时钟也具有各种各样的奇特之处。特别是,如果本地时钟在 NTP 服务器之前太远,则它可能会被强制重置,看上去好像跳回了先前的时间点。这些跳跃以及他们经常忽略闰秒的事实,使时钟不能用于测量经过时间。 21 | 22 | 时钟还具有相当粗略的分辨率,例如,在较早的 Windows 系统上以 10 毫秒为单位前进。 23 | 时钟虽然看起来简单易用,但却具有令人惊讶的缺陷:一天可能不会有精确的 86,400 秒,时钟可能会前后跳跃,而一个节点上的时间可能与另一个节点上的时间完全不同。正如网络丢包与任意延迟包的问题,尽管网络在大多数情况下表现良好,但软件的设计必须假定网络偶尔会出现故障,而软件必须正常处理这些故障。时钟也是如此:尽管大多数时间都工作得很好,但需要准备健壮的软件来处理不正确的时钟。 24 | 25 | ## 单调钟 26 | 27 | 单调钟适用于测量持续时间(时间间隔),例如超时或服务的响应时间:Linux 上的 clock_gettime(CLOCK_MONOTONIC),和 Java 中的 System.nanoTime()都是单调时钟。这个名字来源于他们保证总是前进的事实(而时钟可以及时跳回)。你可以在某个时间点检查单调钟的值,做一些事情,且稍后再次检查它。这两个值之间的差异告诉你两次检查之间经过了多长时间。但单调钟的绝对值是毫无意义的:它可能是计算机启动以来的纳秒数,或类似的任意值。特别是比较来自两台不同计算机的单调钟的值是没有意义的,因为它们并不是一回事。 28 | 29 | 在具有多个 CPU 插槽的服务器上,每个 CPU 可能有一个单独的计时器,但不一定与其他 CPU 同步。操作系统会补偿所有的差异,并尝试向应用线程表现出单调钟的样子,即使这些线程被调度到不同的 CPU 上。当然,明智的做法是不要太把这种单调性保证当回事。。如果 NTP 协议检测到计算机的本地石英钟比 NTP 服务器要更快或更慢,则可以调整单调钟向前走的频率(这称为偏移(skewing)时钟)。 30 | 31 | 默认情况下,NTP 允许时钟速率增加或减慢最高至 0.05%,但 NTP 不能使单调时钟向前或向后跳转。单调时钟的分辨率通常相当好:在大多数系统中,它们能在几微秒或更短的时间内测量时间间隔。在分布式系统中,使用单调钟测量经过时间(elapsed time)(比如超时)通常很好,因为它不假定不同节点的时钟之间存在任何同步,并且对测量的轻微不准确性不敏感。 32 | 33 | # 不准确的时钟同步 34 | 35 | 单调钟不需要同步,但是时钟需要根据 NTP 服务器或其他外部时间源来设置才能有用。不幸的是,我们获取时钟的方法并不像你所希望的那样可靠或准确:硬件时钟和 NTP 可能会变幻莫测。计算机中的石英钟可能会出现所谓的漂移(drifts)(运行速度快于或慢于预期),并且该现象取决于机器的温度。Google 假设其服务器时钟漂移为 200 ppm(百万分之一),相当于每 30 秒与服务器重新同步一次的时钟漂移为 6 毫秒,或者每天重新同步的时钟漂移为 17 秒。即使一切工作正常,此漂移也会限制可以达到的最佳准确度。 36 | 37 | - 如果计算机的时钟与 NTP 服务器的时钟差别太大,可能会拒绝同步,或者本地时钟将被强制重置。任何观察重置前后时间的应用程序都可能会看到时间倒退或突然跳跃。 38 | - 如果某个节点被 NTP 服务器意外阻塞,可能会在一段时间内忽略错误配置。有证据表明,这在实践中确实发生过。 39 | - NTP 同步只能和网络延迟一样好,所以当您在拥有可变数据包延迟的拥塞网络上时,NTP 同步的准确性会受到限制。一个实验表明,当通过互联网同步时,35 毫秒的最小误差是可以实现的,尽管偶尔的网络延迟峰值会导致大约一秒的误差。根据配置,较大的网络延迟会导致 NTP 客户端完全放弃。 40 | - 一些 NTP 服务器错误或配置错误,报告时间已经过去了几个小时。NTP 客户端非常强大,因为他们查询多个服务器并忽略异常值。尽管如此,在互联网上陌生人告诉你的时候,你的系统的正确性还是值得担忧的。 41 | - 闰秒导致 59 分钟或 61 秒长的分钟,这混淆了未设计闰秒的系统中的时序假设。闰秒已经使许多大型系统崩溃的事实说明了,关于时钟的假设是多么容易偷偷溜入系统中。处理闰秒的最佳方法可能是通过在一天中逐渐执行闰秒调整(这被称为拖尾(smearing)),使 NTP 服务器“撒谎”,虽然实际的 NTP 服务器表现各异。 42 | - 在虚拟机中,硬件时钟被虚拟化,这对于需要精确计时的应用程序提出了额外的挑战。当一个 CPU 核心在虚拟机之间共享时,每个虚拟机都会暂停几十毫秒,而另一个虚拟机正在运行。从应用程序的角度来看,这种停顿表现为时钟突然向前跳跃。 43 | - 如果您在未完全控制的设备上运行软件(例如,移动设备或嵌入式设备),则可能完全不信任该设备的硬件时钟。一些用户故意将其硬件时钟设置为不正确的日期和时间,例如,为了规避游戏中的时间限制,时钟可能会被设置到很远的过去或将来。 44 | 45 | 如果你足够关心这件事并投入大量资源,就可以达到非常好的时钟精度。例如,针对金融机构的欧洲法规草案 MiFID II 要求所有高频率交易基金在 UTC 时间 100 微秒内同步时钟,以便调试“闪崩”等市场异常现象,并帮助检测市场操纵。使用 GPS 接收机,精确时间协议(PTP)以及仔细的部署和监测可以实现这种精确度。然而,这需要很多努力和专业知识,而且有很多东西都会导致时钟同步错误。如果你的 NTP 守护进程配置错误,或者防火墙阻止了 NTP 通信,由漂移引起的时钟误差可能很快就会变大。 46 | 47 | ## 置信区间 48 | 49 | 您可能能够以微秒或甚至纳秒的分辨率读取机器的时钟。但即使可以得到如此细致的测量结果,这并不意味着这个值对于这样的精度实际上是准确的。实际上,如前所述,即使您每分钟与本地网络上的 NTP 服务器进行同步,很可能也不会像前面提到的那样,在不精确的石英时钟上漂移几毫秒。使用公共互联网上的 NTP 服务器,最好的准确度可能达到几十毫秒,而且当网络拥塞时,误差可能会超过 100 毫秒。因此,将时钟读数视为一个时间点是没有意义的——它更像是一段时间范围:例如,一个系统可能以 95%的置信度认为当前时间处于本分钟内的第 10.3 秒和 10.5 秒之间,它可能没法比这更精确了。如果我们只知道 ±100 毫秒的时间,那么时间戳中的微秒数字部分基本上是没有意义的。 50 | 51 | 不确定性界限可以根据你的时间源来计算。如果您的 GPS 接收器或原子(铯)时钟直接连接到您的计算机上,预期的错误范围由制造商报告。如果从服务器获得时间,则不确定性取决于自上次与服务器同步以来的石英钟漂移的期望值,加上 NTP 服务器的不确定性,再加上到服务器的网络往返时间(只是获取粗略近似值,并假设服务器是可信的)。 52 | 53 | 不幸的是,大多数系统不公开这种不确定性:例如,当调用 clock_gettime()时,返回值不会告诉你时间戳的预期错误,所以你不知道其置信区间是 5 毫秒还是 5 年。一个有趣的例外是 Spanner 中的 Google TrueTime API,它明确地报告了本地时钟的置信区间。当你询问当前时间时,你会得到两个值:[最早,最晚],这是最早可能的时间戳和最晚可能的时间戳。在不确定性估计的基础上,时钟知道当前的实际时间落在该区间内。间隔的宽度取决于自从本地石英钟最后与更精确的时钟源同步以来已经过了多长时间。 54 | 55 | # 如果依赖同步时钟 56 | 57 | ## 有序事件的时间戳 58 | 59 | 很多时候我们会依赖时钟在多个节点上对事件进行排序。例如,如果两个客户端写入分布式数据库,谁先到达?哪一个更近?下图显示了在具有多领导者复制的数据库中对时钟的危险使用,客户端 A 在节点 1 上写入 x = 1;写入被复制到节点 3;客户端 B 在节点 3 上增加 x(我们现在有 x = 2);最后这两个写入都被复制到节点 2。 60 | 61 | ![客户端B的写入比客户端A的写入要晚,但是B的写入具有较早的时间戳](https://s2.ax1x.com/2020/02/11/1TXeZq.md.png) 62 | 63 | 当一个写入被复制到其他节点时,它会根据发生写入的节点上的时钟时钟标记一个时间戳。在这个例子中,时钟同步是非常好的:节点 1 和节点 3 之间的偏差小于 3ms,这可能比你在实践中预期的更好。尽管如此,上图中的时间戳却无法正确排列事件:写入 x = 1 的时间戳为 42.004 秒,但写入 x = 2 的时间戳为 42.003 秒,即使 x = 2 在稍后出现。当节点 2 接收到这两个事件时,会错误地推断出 x = 1 是最近的值,而丢弃写入 x = 2。效果上表现为,客户端 B 的增量操作会丢失。 64 | 65 | 这种冲突解决策略被称为最后写入为准(LWW),它在多领导者复制和无领导者数据库(如 Cassandra 和 Riak)中被广泛使用。有些实现会在客户端而不是服务器上生成时间戳,但这并不能改变 LWW 的基本问题: 66 | 67 | - 数据库写入可能会神秘地消失:具有滞后时钟的节点无法用快速时钟覆盖之前由节点写入的值,直到节点之间的时钟偏差过去。此方案可能导致一定数量的数据被悄悄丢弃,而未向应用报告任何错误。 68 | - LWW 无法区分高频顺序写入(譬如客户端 B 的增量操作一定发生在客户端 A 的写入之后)和真正并发写入(写入者意识不到其他写入者)。需要额外的因果关系跟踪机制(例如版本向量),以防止因果关系的冲突。 69 | - 两个节点可以独立生成具有相同时间戳的写入,特别是在时钟仅具有毫秒分辨率的情况下。为了解决这样的冲突,还需要一个额外的决胜值(tiebreaker)(可以简单地是一个大随机数),但这种方法也可能会导致违背因果关系。 70 | 71 | 因此,尽管通过保留最“最近”的值并放弃其他值来解决冲突是很诱惑人的,但是要注意,“最近”的定义取决于本地的时钟,这很可能是不正确的。即使用频繁同步的 NTP 时钟,一个数据包也可能在时间戳 100 毫秒(根据发送者的时钟)时发送,并在时间戳 99 毫秒(根据接收者的时钟)处到达——看起来好像数据包在发送之前已经到达,这是不可能的。 72 | 73 | NTP 同步是否能足够准确,以至于这种不正确的排序不会发生?也许不能,因为 NTP 的同步精度本身受到网络往返时间的限制,除了石英钟漂移这类误差源之外。为了进行正确的排序,你需要一个比测量对象(即网络延迟)要精确得多的时钟。所谓的逻辑时钟是基于递增计数器而不是振荡石英晶体,对于排序事件来说是更安全的选择(请参见“检测并发写入”)。逻辑时钟不测量一天中的时间或经过的秒数,而仅测量事件的相对顺序(无论一个事件发生在另一个事件之前还是之后)。相反,用来测量实际经过时间的时钟和单调钟也被称为物理时钟。我们将在“顺序保证”中查看更多订购信息。 74 | 75 | ## 全局快照的同步时钟 76 | 77 | 快照隔离是数据库中非常有用的功能,它允许只读事务看到特定时间点的处于一致状态的数据库,且不会锁定和干扰读写事务。快照隔离最常见的实现需要单调递增的事务 ID。如果写入比快照晚(即,写入具有比快照更大的事务 ID),则该写入对于快照事务是不可见的。在单节点数据库上,一个简单的计数器就足以生成事务 ID。但是当数据库分布在许多机器上,也许可能在多个数据中心中时,由于需要协调,(跨所有分区)全局单调递增的事务 ID 可能很难生成。事务 ID 必须反映因果关系:如果事务 B 读取由事务 A 写入的值,则 B 必须具有比 A 更大的事务 ID,否则快照就无法保持一致。在有大量的小规模、高频率的事务情景下,在分布式系统中创建事务 ID 成为一个站不住脚的瓶颈。 78 | 79 | 我们可以使用同步时钟的时间戳作为事务 ID 吗?如果我们能够获得足够好的同步性,那么这种方法将具有很合适的属性:更晚的事务会有更大的时间戳。当然,问题在于时钟精度的不确定性。Spanner 以这种方式实现跨数据中心的快照隔离。它使用 TrueTime API 报告的时钟置信区间,并基于以下观察结果:如果您有两个置信区间,每个置信区间包含最早和最近可能的时间戳($A = [A_{earliest}, A_{latest}]$,$B=[B_{earliest}, B_{latest}]$),这两个区间不重叠(即:$A_{earliest} < A_{latest} < B_{earliest} < B_{latest}$),那么 B 肯定发生在 A 之后——这是毫无疑问的。只有当区间重叠时,我们才不确定 A 和 B 发生的顺序。 80 | 81 | 为了确保事务时间戳反映因果关系,在提交读写事务之前,Spanner 在提交读写事务时,会故意等待置信区间长度的时间。通过这样,它可以确保任何可能读取数据的事务处于足够晚的时间,因此它们的置信区间不会重叠。为了保持尽可能短的等待时间,Spanner 需要保持尽可能小的时钟不确定性,为此,Google 在每个数据中心都部署了一个 GPS 接收器或原子钟,允许时钟在大约 7 毫秒内同步。 82 | -------------------------------------------------------------------------------- /01~分布式基础/01~不可靠的分布式系统/不可靠网络.md: -------------------------------------------------------------------------------- 1 | # 不可靠网络 2 | 3 | 分布式系统是无共享的系统,即每台机器都有自己的内存和磁盘,一台机器不能访问另一台机器的内存或磁盘,只能通过网络向服务器发出请求,网络是这些机器可以通信的唯一途径。这些机器也就是网络中的节点,网络将节点联接起来,但是网络也带来了一系列的问题。网络消息的传播有先后,消息丢失和延迟是经常发生的事情。 4 | 5 | 典型的网络模式有如下三种:同步网络(节点同步执行,消息延迟有限,高效全局锁)、半同步网络(锁范围放宽)、节点独立执行(消息延迟无上限,无全局锁,部分算法不可行)。互联网和数据中心(通常是以太网)中的大多数内部网络都是异步分组网络(asynchronous packet networks)。在这种网络中,一个节点可以向另一个节点发送一个消息(一个数据包),但是网络不能保证它什么时候到达,或者是否到达。如果您发送请求并期待响应,则很多事情可能会出错: 6 | 7 | - 请求可能已经丢失(可能有人拔掉了网线)。 8 | - 请求可能正在排队,稍后将交付(也许网络或收件人超载)。 9 | - 远程节点可能已经失效(可能是崩溃或关机)。 10 | - 远程节点可能暂时停止了响应(可能会遇到长时间的垃圾回收暂停),但稍后会再次响应。 11 | - 远程节点可能已经处理了请求,但是网络上的响应已经丢失(可能是网络交换机配置错误)。 12 | - 远程节点可能已经处理了请求,但是响应已经被延迟,并且稍后将被传递(可能是网络或者你自己的机器过载)。 13 | 14 | ![如果发送请求并没有得到响应,则无法区分(a)请求是否丢失,(b)远程节点是否关闭,或(c)响应是否丢失](https://s2.ax1x.com/2020/02/11/1TWxht.md.png) 15 | 16 | 发送者甚至不能分辨数据包是否被发送:唯一的选择是让接收者发送响应消息,这可能会丢失或延迟。这些问题在异步网络中难以区分:您所拥有的唯一信息是,您尚未收到响应。如果您向另一个节点发送请求并且没有收到响应,则无法说明原因。处理这个问题的通常方法是超时(Timeout):在一段时间之后放弃等待,并且认为响应不会到达。但是,当发生超时时,你仍然不知道远程节点是否收到了请求(如果请求仍然在某个地方排队,那么即使发件人已经放弃了该请求,仍然可能会将其发送给收件人)。 17 | 18 | ## 真实世界的网络故障 19 | 20 | 有一些系统的研究和大量的轶事证据表明,即使在像一家公司运营的数据中心那样的受控环境中,网络问题也可能出乎意料地普遍。在一家中型数据中心进行的一项研究发现,每个月大约有 12 个网络故障,其中一半断开一台机器,一半断开整个机架。。另一项研究测量了架顶式交换机,汇聚交换机和负载平衡器等组件的故障率。。它发现添加冗余网络设备不会像您所希望的那样减少故障,因为它不能防范人为错误(例如,错误配置的交换机),这是造成中断的主要原因。 21 | 22 | 诸如 EC2 之类的公有云服务因频繁的暂态网络故障而臭名昭着。,管理良好的私有数据中心网络可能是更稳定的环境。尽管如此,没有人不受网络问题的困扰:例如,交换机软件升级过程中的一个问题可能会引发网络拓扑重构,在此期间网络数据包可能会延迟超过一分钟。。鲨鱼可能咬住海底电缆并损坏它们。。其他令人惊讶的故障包括网络接口有时会丢弃所有入站数据包,但是成功发送出站数据包。:仅仅因为网络链接在一个方向上工作,并不能保证它也在相反的方向工作。 23 | 24 | 当网络的一部分由于网络故障而被切断时,有时称为网络分区(network partition)或网络断裂(netsplit)。在本书中,我们通常会坚持使用更一般的术语网络故障(network fault),以避免与存储系统的分区(分片)相混淆。即使网络故障在你的环境中非常罕见,故障可能发生的事实,意味着你的软件需要能够处理它们。无论何时通过网络进行通信,都可能会失败,这是无法避免的。 25 | 26 | 如果网络故障的错误处理没有定义与测试,武断地讲,各种错误可能都会发生:例如,即使网络恢复。,集群可能会发生死锁,永久无法为请求提供服务,甚至可能会删除所有的数据。。如果软件被置于意料之外的情况下,它可能会做出出乎意料的事情。处理网络故障并不意味着容忍它们:如果你的网络通常是相当可靠的,一个有效的方法可能是当你的网络遇到问题时,简单地向用户显示一条错误信息。但是,您确实需要知道您的软件如何应对网络问题,并确保系统能够从中恢复。有意识地触发网络问题并测试系统响应 27 | 28 | # 故障检测 29 | 30 | 许多系统需要自动检测故障节点。例如: 31 | 32 | - 负载平衡器需要停止向已死亡的节点转发请求(即从移出轮询列表(out of rotation))。 33 | - 在单主复制功能的分布式数据库中,如果主库失效,则需要将从库之一升级为新主库。 34 | 35 | 不幸的是,网络的不确定性使得很难判断一个节点是否工作。在某些特定的情况下,您可能会收到一些反馈信息,明确告诉您某些事情没有成功: 36 | 37 | - 如果你可以到达运行节点的机器,但没有进程正在侦听目标端口(例如,因为进程崩溃),操作系统将通过发送 FIN 或 RST 来关闭并重用 TCP 连接。但是,如果节点在处理请求时发生崩溃,则无法知道远程节点实际处理了多少数据。 38 | 39 | - 如果节点进程崩溃(或被管理员杀死),但节点的操作系统仍在运行,则脚本可以通知其他节点有关该崩溃的信息,以便另一个节点可以快速接管,而无需等待超时到期。例如,HBase 做这个。 40 | 41 | - 如果您有权访问数据中心网络交换机的管理界面,则可以查询它们以检测硬件级别的链路故障(例如,远程机器是否关闭电源)。如果您通过互联网连接,或者如果您处于共享数据中心而无法访问交换机,或者由于网络问题而无法访问管理界面,则排除此选项。 42 | 43 | - 如果路由器确认您尝试连接的 IP 地址不可用,则可能会使用 ICMP 目标不可达数据包回复您。但是,路由器不具备神奇的故障检测能力——它受到与网络其他参与者相同的限制。 44 | 45 | 关于远程节点关闭的快速反馈很有用,但是你不能指望它。即使 TCP 确认已经传送了一个数据包,应用程序在处理之前可能已经崩溃。如果你想确保一个请求是成功的,你需要应用程序本身的积极响应。相反,如果出了什么问题,你可能会在堆栈的某个层次上得到一个错误响应,但总的来说,你必须假设你根本就没有得到任何回应。您可以重试几次(TCP 重试是透明的,但是您也可以在应用程序级别重试),等待超时过期,并且如果在超时时间内没有收到响应,则最终声明节点已经死亡。 46 | 47 | ## 合理的超时等待 48 | 49 | 如果超时是检测故障的唯一可靠方法,那么超时应该等待多久?不幸的是没有简单的答案。长时间的超时意味着长时间等待,直到一个节点被宣告死亡(在这段时间内,用户可能不得不等待,或者看到错误信息)。短暂的超时可以更快地检测到故障,但是实际上它只是经历了暂时的减速(例如,由于节点或网络上的负载峰值)而导致错误地宣布节点失效的风险更高。过早地声明一个节点已经死了是有问题的:如果这个节点实际上是活着的,并且正在执行一些动作(例如,发送一封电子邮件),而另一个节点接管,那么这个动作可能会最终执行两次。 50 | 51 | 当一个节点被宣告死亡时,它的职责需要转移到其他节点,这会给其他节点和网络带来额外的负担。如果系统已经处于高负荷状态,则过早宣告节点死亡会使问题更严重。尤其是可能发生,节点实际上并没有死亡,而是由于过载导致响应缓慢;将其负载转移到其他节点可能会导致级联失效(cascading failure)(在极端情况下,所有节点都宣告对方死亡,并且所有节点都停止工作)。 52 | 53 | 设想一个虚构的系统,其网络可以保证数据包的最大延迟——每个数据包要么在一段时间内传送,要么丢失,但是传递永远不会比 $d$ 更长。此外,假设你可以保证一个非故障节点总是在一段时间内处理一个请求$r$。在这种情况下,您可以保证每个成功的请求在 $2d + r$ 时间内都能收到响应,如果您在此时间内没有收到响应,则知道网络或远程节点不工作。如果这是成立的,$2d + r$ 会是一个合理的超时设置。 54 | 55 | 不幸的是,我们所使用的大多数系统都没有这些保证:异步网络具有无限的延迟(即尽可能快地传送数据包,但数据包到达可能需要的时间没有上限),并且大多数服务器实现并不能保证它们可以在一定的最大时间内处理请求。对于故障检测,系统大部分时间快速运行是不够的:如果你的超时时间很短,往返时间只需要一个瞬时尖峰就可以使系统失衡。 56 | 57 | # 网络拥塞和排队 58 | 59 | 在驾驶汽车时,由于交通拥堵,道路交通网络的通行时间往往不尽相同。同样,计算机网络上数据包延迟的可变性通常是由于排队: 60 | 61 | - 如果多个不同的节点同时尝试将数据包发送到同一目的地,则网络交换机必须将它们排队并将它们逐个送入目标网络链路。繁忙的网络链路上,数据包可能需要等待一段时间才能获得一个插槽(这称为网络连接)。如果传入的数据太多,交换机队列填满,数据包将被丢弃,因此需要重新发送数据包 - 即使网络运行良好。 62 | 63 | ![如果有多台机器将网络流量发送到同一目的地,则其交换机队列可能会被填满。在这里,端口1,2和4都试图发送数据包到端口3](https://s2.ax1x.com/2020/02/11/1T5ugP.md.png) 64 | 65 | - 当数据包到达目标机器时,如果所有 CPU 内核当前都处于繁忙状态,则来自网络的传入请求将被操作系统排队,直到应用程序准备好处理它为止。根据机器上的负载,这可能需要一段任意的时间。 66 | 67 | - 在虚拟化环境中,正在运行的操作系统经常暂停几十毫秒,而另一个虚拟机使用 CPU 内核。在这段时间内,虚拟机不能从网络中消耗任何数据,所以传入的数据被虚拟机监视器。排队(缓冲),进一步增加了网络延迟的可变性。 68 | 69 | - TCP 执行流量控制(flow control)(也称为拥塞避免(congestion avoidance)或背压(backpressure)),其中节点限制自己的发送速率以避免网络链路或接收节点过载。。这意味着在数据甚至进入网络之前,在发送者处需要进行额外的排队。 70 | 71 | 而且,如果 TCP 在某个超时时间内没有被确认(这是根据观察的往返时间计算的),则认为数据包丢失,丢失的数据包将自动重新发送。尽管应用程序没有看到数据包丢失和重新传输,但它看到了延迟(等待超时到期,然后等待重新传输的数据包得到确认)。所有这些因素都会造成网络延迟的变化。当系统接近其最大容量时,排队延迟的范围特别广泛:拥有足够备用容量的系统可以轻松排空队列,而在高利用率的系统中,很快就能积累很长的队列。 72 | 73 | 在公共云和多租户数据中心中,资源被许多客户共享:网络链接和交换机,甚至每个机器的网卡和 CPU(在虚拟机上运行时)。批处理工作负载(如 MapReduce)可能很容易使网络链接饱和。由于无法控制或了解其他客户对共享资源的使用情况,如果附近的某个人(嘈杂的邻居)正在使用大量资源,则网络延迟可能会发生剧烈抖动。 74 | 75 | 在这种环境下,您只能通过实验方式选择超时:测量延长的网络往返时间和多台机器的分布,以确定延迟的预期可变性。然后,考虑到应用程序的特性,可以确定故障检测延迟与过早超时风险之间的适当折衷。更好的一种做法是,系统不是使用配置的常量超时时间,而是连续测量响应时间及其变化(抖动),并根据观察到的响应时间分布自动调整超时时间。这可以通过 Phi Accrual 故障检测器。来完成,该检测器在例如 Akka 和 Cassandra。中使用。TCP 超时重传机制也同样起作用。。 76 | 77 | ## TCP 与 UDP 78 | 79 | 一些对延迟敏感的应用程序(如视频会议和 IP 语音(VoIP))使用 UDP 而不是 TCP。这是在可靠性和和延迟可变性之间的折衷:由于 UDP 不执行流量控制并且不重传丢失的分组,所以避免了可变网络延迟的一些原因(尽管它仍然易受切换队列和调度延迟的影响)。 80 | 81 | 在延迟数据毫无价值的情况下,UDP 是一个不错的选择。例如,在 VoIP 电话呼叫中,可能没有足够的时间重新发送丢失的数据包,并在扬声器上播放数据。在这种情况下,重发数据包没有意义——应用程序必须使用静音填充丢失数据包的时隙(导致声音短暂中断),然后在数据流中继续。重试发生在人类层。(“你能再说一遍吗?声音刚刚断了一会儿。“) 82 | -------------------------------------------------------------------------------- /01~分布式基础/01~不可靠的分布式系统/不可靠进程.md: -------------------------------------------------------------------------------- 1 | # 不可靠进程 2 | 3 | ## 休眠的进程问题 4 | 5 | ### 问题引入 6 | 7 | 在分布式系统中,进程可能会出现意外的休眠或暂停,这会导致严重的系统问题。让我们通过一个领导者选举的例子来说明这个问题。 8 | 9 | ### 示例:基于租约的领导者选举 10 | 11 | 在分布式数据库中,每个分区只能有一个领导者节点处理写入请求。节点通过获取租约(带超时的锁)来确认自己的领导者身份。以下是一个简化的处理循环: 12 | 13 | ```java 14 | while(true) { 15 | request = getIncomingRequest(); 16 | // 确保租约还剩下至少10秒 17 | if (lease.expiryTimeMillis - System.currentTimeMillis() < 10000) { 18 | lease = lease.renew(); 19 | } 20 | 21 | if (lease.isValid()) { 22 | process(request); 23 | } 24 | } 25 | ``` 26 | 27 | ### 代码中的问题 28 | 29 | 1. **时钟依赖问题**:代码依赖于同步时钟,如果时钟不同步可能导致异常行为 30 | 2. **执行暂停风险**:代码假设检查时间和处理请求之间的间隔很短,但实际上进程可能在任何时候暂停 31 | 32 | ## 进程暂停的原因 33 | 34 | ### 1. 垃圾收集(GC)暂停 35 | 36 | - 运行时的"停止世界"GC 可能持续数分钟 37 | - 即使是"并行"GC 也需要定期停止所有线程 38 | 39 | ### 2. 虚拟化环境影响 40 | 41 | - 虚拟机可能被挂起和恢复 42 | - 实时迁移过程中的暂停 43 | 44 | ### 3. 硬件和操作系统层面 45 | 46 | - 笔记本电脑合盖等用户操作 47 | - 操作系统上下文切换 48 | - CPU 时间被其他虚拟机窃取 49 | 50 | ### 4. I/O 相关暂停 51 | 52 | - 磁盘 I/O 操作 53 | - 类加载造成的意外 I/O 54 | - 网络文件系统延迟 55 | 56 | ### 5. 内存管理 57 | 58 | - 页面错误和内存交换 59 | - 系统抖动问题 60 | 61 | ### 6. 进程控制 62 | 63 | - SIGSTOP 信号导致的暂停 64 | - 运维操作造成的意外暂停 65 | 66 | ## 缓解措施 67 | 68 | ### 垃圾收集影响的控制 69 | 70 | 1. **计划性 GC** 71 | 72 | - 提前警告应用程序即将进行 GC 73 | - 暂停节点的请求处理 74 | - 等待现有请求处理完成 75 | - 执行 GC 76 | 77 | 2. **对象生命周期管理** 78 | - 只对短命对象进行垃圾收集 79 | - 定期重启进程以避免完整 GC 80 | - 采用滚动重启策略 81 | 82 | ### 最佳实践 83 | 84 | - 在服务器上禁用页面调度 85 | - 合理规划内存使用 86 | - 实施监控和告警机制 87 | -------------------------------------------------------------------------------- /01~分布式基础/02~节点与集群/主从节点/README.md: -------------------------------------------------------------------------------- 1 | # 分布式节点 2 | 3 | 在分布式系统中,我们有多个参与者(有时称为进程,节点或副本)。每个参与者都有其自己的本地状态。参与者通过使用他们之间的通信链接交换消息来进行通信。 4 | 5 | 其实所谓分布式运算,核心的思路就是系统架构无单点,让整个系统可扩展。一般来说,分布式计算环境下的节点会分为有状态存储节点和无状态运算节点。 6 | 7 | 那么针对无状态节点,因为不存储数据,请求分发可以采取很简单的随机算法或者是轮询的算法就可以了,如果需要增加机器,那只需要把对应的运算代码部署到一些机器上,然后启动起来,引导流量到那些机器上就可以实现动态的扩展了,所以一般来说在无状态的节点的扩展是相对的容易的,唯一需要做的事情就是在某个机器承担了某种角色以后,能够快速的广播给需要这个角色提供服务的人说:“我目前可以做这个活儿啦,你们有需要我做事儿的人,可以来找我。” 8 | 9 | 而针对有状态节点,扩容的难度就相对的大一些,因为每台 Server 中都有数据,所以请求分发的算法不能够用随机或者是轮询了,一般来说常见算法就是哈希或者是使用 Tree 来做一层映射,而如果需要增加机器,那么需要一个比较复杂的数据迁移的过程,而迁移数据本身所需要的成本是非常高的,这也就直接导致有状态节点的扩容难度比无状态节点更大。 10 | 11 | 针对有状态节点的难题,我们提供了一套数据自动扩容和迁移的工具来满足用户的自动扩容缩容中所产生的数据迁移类的需求。于是,无论是有状态的数据节点的扩容,还是无状态的数据节点的自动扩容,我们都可以使用自动化工具来完成了。 12 | 13 | Google 在 03-06 年发布了关于 GFS、BigTable、MapReduce 的三篇论文,开启了大数据时代。在发展的早期,就诞生了以 HDFS/HBase/MapReduce 为主的 Hadoop 技术栈,并一直延续到今天。 14 | 15 | 最开始大数据的处理大多是离线处理,MapReduce 理念虽然好,但性能捉急,新出现的 Spark 抓住了这个机会,依靠其强大而高性能的批处理技术,顺利取代了 MapReduce,成为主流的大数据处理引擎。 16 | 随着时代的发展,实时处理的需求越来越多,虽然 Spark 推出了 Spark Streaming 以微批处理来模拟准实时的情况,但在延时上还是不尽如人意。2011 年,Twitter 的 Storm 吹响了真正流处理的号角,而 Flink 则将之发扬光大。 17 | 到现在,Flink 的目光也不再将自己仅仅视为流处理引擎,而是更为通用的处理引擎,开始正面挑战 Spark 的地位。 18 | -------------------------------------------------------------------------------- /01~分布式基础/02~节点与集群/主从节点/节点选举.md: -------------------------------------------------------------------------------- 1 | # 节点选举 2 | 3 | 在实际的系统演化过程中,最初的时候我们只有单节点,或者我们能够人为地去控制仅有的几个节点。但如果该领导者失效,或者如果网络中断导致领导者不可达,这样的系统就无法取得任何进展。应对这种情况可以有三种方法: 4 | 5 | - 等待领导者恢复,接受系统将在这段时间阻塞的事实。许多 XA/JTA 事务协调者选择这个选项。这种方法并不能完全达成共识,因为它不能满足终止属性的要求:如果领导者续命失败,系统可能会永久阻塞。 6 | - 人工故障切换,让人类选择一个新的领导者节点,并重新配置系统使之生效,许多关系型数据库都采用这种方方式。这是一种来自“天意”的共识,由计算机系统之外的运维人员做出决定。故障切换的速度受到人类行动速度的限制,通常要比计算机慢(得多)。 7 | - 使用算法自动选择一个新的领导者。这种方法需要一种共识算法,使用成熟的算法来正确处理恶劣的网络条件是明智之举。 8 | 9 | 尽管单领导者数据库可以提供线性一致性,且无需对每个写操作都执行共识算法,但共识对于保持及变更领导权仍然是必须的。因此从某种意义上说,使用单个领导者不过是“缓兵之计”:共识仍然是需要的,只是在另一个地方,而且没那么频繁。像 ZooKeeper 这样的工具为应用提供了“外包”的共识、故障检测和成员服务。它们扮演了重要的角色,虽说使用不易,但总比自己去开发一个能经受所有问题考验的算法要好得多。如果你发现自己想要解决的问题可以归结为共识,并且希望它能容错,使用一个类似 ZooKeeper 的东西是明智之举。 10 | 11 | ## 领导者与锁定 12 | 13 | 在[《不可靠的分布式系统](https://github.com/wx-chevalier/DistributedSystem-Notes)》中我们讨论过一种进程暂停的情形,想象一个经历了一个长时间 stop-the-world GC Pause 的节点,节点的所有线程被 GC 抢占并暂停一分钟,因此没有请求被处理,也没有响应被发送。其他节点等待,重试,不耐烦,并最终宣布节点死亡,并将其丢到灵车上。最后,GC 完成,节点的线程继续,好像什么也没有发生。其他节点感到惊讶,因为所谓的死亡节点突然从棺材中抬起头来,身体健康,开始和旁观者高兴地聊天。GC 后的节点最初甚至没有意识到已经经过了整整一分钟,而且自己已被宣告死亡。从它自己的角度来看,从最后一次与其他节点交谈以来,几乎没有经过任何时间。 14 | 15 | 通常情况下,一些东西在一个系统中只能有一个。例如: 16 | 17 | - 数据库分区的领导者只能有一个节点,以避免脑裂(split brain)。 18 | - 特定资源的锁或对象只允许一个事务/客户端持有,以防同时写入和损坏。 19 | - 一个特定的用户名只能被一个用户所注册,因为用户名必须唯一标识一个用户。 20 | 21 | 在分布式系统中实现这一点需要注意:即使一个节点认为它是“天选者(the choosen one)”(分区的负责人,锁的持有者,成功获取用户名的用户的请求处理程序),但这并不一定意味着有法定人数的节点同意!一个节点可能以前是领导者,但是如果其他节点在此期间宣布它死亡(例如,由于网络中断或 GC 暂停),则它可能已被降级,且另一个领导者可能已经当选。如果一个节点继续表现为天选者,即使大多数节点已经声明它已经死了,则在考虑不周的系统中可能会导致问题。这样的节点能以自己赋予的权能向其他节点发送消息,如果其他节点相信,整个系统可能会做一些不正确的事情。 22 | 23 | 下图显示了由于不正确的锁实现导致的数据损坏错误,该错误在 HBase 中就真实地存在过。假设你要确保一个存储服务中的文件一次只能被一个客户访问,因为如果多个客户试图写对此,该文件将被损坏。您尝试通过在访问文件之前要求客户端从锁定服务获取租约来实现此目的。 24 | 25 | ![分布式锁的实现不正确:客户端1认为它仍然具有有效的租约,即使它已经过期,从而破坏了存储中的文件](https://s2.ax1x.com/2020/02/12/17fYWt.md.png) 26 | 27 | 如果持有租约的客户端暂停太久,它的租约将到期。另一个客户端可以获得同一文件的租约,并开始写入文件。当暂停的客户端回来时,它认为(不正确)它仍然有一个有效的租约,并继续写入文件。结果,客户的写入冲突和损坏的文件。 28 | 29 | ### 防护令牌 30 | 31 | 当使用锁或租约来保护对某些资源的访问时,需要确保一个被误认为自己是“天选者”的节点不能中断系统的其它部分。实现这一目标的一个相当简单的技术就是防护(fencing)。 32 | 33 | ![只允许以增加屏蔽令牌的顺序进行写操作,从而保证存储安全](https://s2.ax1x.com/2020/02/12/1746MV.png) 34 | 35 | 我们假设每次锁定服务器授予锁或租约时,它还会返回一个防护令牌(fencing token),这个数字在每次授予锁定时都会增加(例如,由锁定服务增加)。然后,我们可以要求客户端每次向存储服务发送写入请求时,都必须包含当前的屏蔽令牌。客户端 1 以 33 的令牌获得租约,但随后进入一个长时间的停顿并且租约到期。客户端 2 以 34 的令牌(该数字总是增加)获取租约,然后将其写入请求发送到存储服务,包括 34 的令牌。稍后,客户端 1 恢复生机并将其写入存储服务,包括其令牌值 33.但是,存储服务器会记住它已经处理了一个具有更高令牌编号(34)的写入,因此它会拒绝带有令牌 33 的请求。 36 | 37 | 如果将 ZooKeeper 用作锁定服务,则可将事务标识 zxid 或节点版本 cversion 用作屏蔽令牌。由于它们保证单调递增,因此它们具有所需的属性。请注意,这种机制要求资源本身在检查令牌方面发挥积极作用,通过拒绝使用旧的令牌,而不是已经被处理的令牌来进行写操作——仅仅依靠客户端检查自己的锁状态是不够的。对于不明确支持屏蔽令牌的资源,可能仍然可以解决此限制(例如,在文件存储服务的情况下,可以将防护令牌包含在文件名中)。但是,为了避免在锁的保护之外处理请求,需要进行某种检查。在服务器端检查一个令牌可能看起来像是一个缺点,但这可以说是一件好事:一个服务假定它的客户总是守规矩并不明智,因为使用客户端的人与运行服务的人优先级非常不一样。因此,任何服务保护自己免受意外客户的滥用是一个好主意。 38 | -------------------------------------------------------------------------------- /01~分布式基础/02~节点与集群/分布式互斥.md: -------------------------------------------------------------------------------- 1 | # 分布式互斥 2 | 3 | 在分布式系统里,这种排他性的资源访问方式,叫作分布式互斥(Distributed Mutual Exclusion),而这种被互斥访问的共享资源就叫作临界资源(Critical Resource)。 4 | 5 | # 集中式算法 6 | 7 | 我们引入一个协调者程序,得到一个分布式互斥算法。每个程序在需要访问临界资源时,先给协调者发送一个请求。如果当前没有程序使用这个资源,协调者直接授权请求程序访问;否则,按照先来后到的顺序为请求程序“排一个号”。如果有程序使用完资源,则通知协调者,协调者从“排号”的队列里取出排在最前面的请求,并给它发送授权消息。拿到授权消息的程序,可以直接去访问临界资源。 8 | 9 | 这个互斥算法,就是我们所说的集中式算法,也可以叫做中央服务器算法。之所以这么称 呼,是因为协调者代表着集中程序或中央服务器。集中式算法的示意图如下所示: 10 | 11 | ![集中式算法示意图](https://pic.imgdb.cn/item/6061cd118322e6675c0b23b7.jpg) 12 | 13 | 如图所示,程序 1、2、3、4 为普通运行程序,另一个程序为协调者。当程序 2 和程序 4 需要使用临界资源时,它们会向协调者发起申请,请求协调者授权。 14 | 不巧的是,程序 3 正在使用临界资源。这时,协调者根据程序 2 和 4 的申请时间顺序,依 次将它们放入等待队列。在这个案例里,程序 4 的申请时间早于程序 2,因此排在程序 2 的前面。程序 3 使用完临界资源后,通知协调者释放授权。此时,协调者从等待队列中取出程序 4,并给它发放授权。这时,程序 4 就可以使用临界资源了。 15 | 16 | 从上述流程可以看出,一个程序完成一次临界资源访问,需要如下几个流程和消息交互: 17 | 18 | - 向协调者发送请求授权信息,1 次消息交互; 19 | - 协调者向程序发放授权信息,1 次消息交互; 20 | - 程序使用完临界资源后,向协调者发送释放授权,1 次消息交互。 21 | 22 | 因此,每个程序完成一次临界资源访问,需要进行 3 次消息交互。不难看出,集中式算法的优点在于直观、简单、信息交互量少、易于实现,并且所有程序只 23 | 需和协调者通信,程序之间无需通信。但是,这个算法的问题也出在了协调者身上。 24 | 25 | - 一方面,协调者会成为系统的性能瓶颈。想象一下,如果有 100 个程序要访问临界资 源,那么协调者要处理 `100*3=300` 条消息。也就是说,协调者处理的消息数量会随着需 要访问临界资源的程序数量线性增加。 26 | - 另一方面,容易引发单点故障问题。协调者故障,会导致所有的程序均无法访问临界资源,导致整个系统不可用。 27 | 28 | 因此,在使用集中式算法的时候,一定要选择性能好、可靠性高的服务器来运行协调者。集中式算法具有简单、易于实现的特点,但可用性、性能易受协调者影响。在可 靠性和性能有一定保障的情况下,比如中央服务器计算能力强、性能高、故障率低,或者中 央服务器进行了主备备份,主故障后备可以立马升为主,且数据可恢复的情况下,集中式算 法可以适用于比较广泛的应用场景。 29 | 30 | # 分布式算法 31 | 32 | 当一个程序要访问临界资源时,先向系统中的 其他程序发送一条请求消息,在接收到所有程序返回的同意消息后,才可以访问临界资源。其中,请求消息需要包含所请求的资源、请求者的 ID,以及发起请求的时间。这就是民主协商法。在分布式领域中,我们称之为分布式算法,或者使用组播和逻辑时钟的算法。如图所示,程序 1、2、3 需要访问共享资源 A。在时间戳为 8 的时刻,程序 1 想要使用资 源 A,于是向程序 2 和 3 发起使用资源 A 的申请,希望得到它们的同意。在时间戳为 12 的时刻,程序 3 想要使用资源 A,于是向程序 1 和 2 发起访问资源 A 的请求。 33 | 34 | ![程序 1 和程序 3 差不多同一时间要访问共享资源 A](https://pic.imgdb.cn/item/6061cf418322e6675c0d6e74.jpg) 35 | 36 | 如图所示,此时程序 2 暂时不访问资源 A,因此同意了程序 1 和 3 的资源访问请求。对于 程序 3 来说,由于程序 1 提出请求的时间更早,因此同意程序 1 先使用资源,并等待程序 1 返回同意消息。 37 | 38 | ![程序 1 的请求时间比程序 3 更早,获得所有授权,访问资源 A](https://pic.imgdb.cn/item/6061cfbe8322e6675c0e3779.jpg) 39 | 40 | 如图所示,程序 1 接收到其他所有程序的同意消息之后,开始使用资源 A。当程序 1 使用 完资源 A 后,释放使用权限,向请求队列中需要使用资源 A 的程序 3 发送同意使用资源的 消息,并将程序 3 从请求队列中删除。此时,程序 3 收到了其他所有程序的同意消息,获 得了使用资源 A 的权限,开始使用临界资源 A 的旅程。 41 | 42 | ![程序 1 释放资源 A,程序 3 获得所有授权,访问资源 A](https://pic.imgdb.cn/item/6061d0048322e6675c0f265a.jpg) 43 | 44 | 从上述流程可以看出,一个程序完成一次临界资源的访问,需要进行如下的信息交互: 45 | 46 | - 向其他 n-1 个程序发送访问临界资源的请求,总共需要 n-1 次消息交互; 47 | - 需要接收到其他 n-1 个程序回复的同意消息,方可访问资源,总共需要 n-1 次消息交互。 48 | 49 | 可以看出,一个程序要成功访问临界资源,至少需要 `2*(n-1)` 次消息交互。假设,现在系统 中的 n 个程序都要访问临界资源,则会同时产生 2n(n-1) 条消息。总结来说,在大型系统 中使用分布式算法,消息数量会随着需要访问临界资源的程序数量呈指数级增加,容易导致 高昂的沟通成本。从上述分析不难看出,分布式算法根据“先到先得”以及“投票全票通过”的机制,让每个程序按时间顺序公平地访问资源,简单粗暴、易于实现。但,这个算法可用性很低,主要包括两个方面的原因: 50 | 51 | - 当系统内需要访问临界资源的程序增多时,容易产生“信令风暴”,也就是程序收到的请求完全超过了自己的处理能力,而导致自己正常的业务无法开展。 52 | - 一旦某一程序发生故障,无法发送同意消息,那么其他程序均处在等待回复的状态中,使得整个系统处于停滞状态,导致整个系统不可用。所以,相对于集中式算法的协调者故障,分布式算法的可用性更低。 53 | 54 | 针对可用性低的一种改进办法是,如果检测到一个程序故障,则直接忽略这个程序,无需再 等待它的同意消息。因此,分布式算法适合节点数目少且变动不频繁的系统,且由于每个程序均需通信交互,因 此适合 P2P 结构的系统。比如,运行在局域网中的分布式文件系统,具有 P2P 结构的系统等。典型的譬如 Hadoop 是我们非常熟悉的分布式系统,其中的分布式文件系统 HDFS 的文件修改就是一个典型的应用分布式算法的场景。 55 | 56 | 如下图所示,处于同一个局域网内的计算机 1、2、3 中都有同一份文件的备份信息,且它 们可以相互通信。这个共享文件,就是临界资源。当计算机 1 想要修改共享的文件时,需要进行如下操作: 57 | 58 | 1. 计算机 1 向计算机 2、3 发送文件修改请求; 59 | 2. 计算机 2、3 发现自己不需要使用资源,因此同意计算机 1 的请求; 60 | 3. 计算机 1 收到其他所有计算机的同意消息后,开始修改该文件; 61 | 4. 计算机 1 修改完成后,向计算机 2、3 发送文件修改完成的消息,并发送修改后的文件数据; 62 | 5. 计算机 2 和 3 收到计算机 1 的新文件数据后,更新本地的备份文件。 63 | 64 | ![HDFS 文件修改流程示意图](https://pic.imgdb.cn/item/6061d1358322e6675c114d76.jpg) 65 | 66 | 分布式算法是一个“先到先得”和“投票全票通过”的公平访问机制,但通信成 本较高,可用性也比集中式算法低,适用于临界资源使用频度较低,且系统规模较小的场 景。 67 | 68 | # 令牌环算法 69 | 70 | 如下图所示,所有程序构 成一个环结构,令牌按照顺时针(或逆时针)方向在程序之间传递,收到令牌的程序有权访 问临界资源,访问完成后将令牌传送到下一个程序;若该程序不需要访问临界资源,则直接 把令牌传送给下一个程序。在分布式领域,这个算法叫作令牌环算法,也可以叫作基于环的算法。 71 | 72 | ![令牌环算法示意图](https://pic.imgdb.cn/item/6061d1a18322e6675c11c01b.jpg) 73 | 74 | 因为在使用临界资源前,不需要像分布式算法那样挨个征求其他程序的意见了,所以相对而言,在令牌环算法里单个程序具有更高的通信效率。同时,在一个周期内,每个程序都能访问到临界资源,因此令牌环算法的公平性很好。但是,不管环中的程序是否想要访问资源,都需要接收并传递令牌,所以也会带来一些无效 通信。假设系统中有 100 个程序,那么程序 1 访问完资源后,即使其它 99 个程序不需要访问,也必须要等令牌在其他 99 个程序传递完后,才能重新访问资源,这就降低了系统的实时性。 75 | 76 | 综上,令牌环算法非常适合通信模式为令牌环方式的分布式系统,例如移动自组织网络系统。一个典型的应用场景就是无人机通信。无人机在通信时,工作原理类似于对讲机,同一时刻只能发送信息或接收信息。因此,通信中的上行链路(即向外发送信息的通信渠道)是临界资源。如下图所示,所有的无人机组成一个环,按照顺时针方向通信。每个无人机只知道其前一个发送信息的无人机,和后一个将要接收信息的无人机。拥有令牌的无人机可以向外发送信息,其他无人机只能接收数据。拥有令牌的无人机通信完成后,会将令牌传送给后一个无人机。 77 | 78 | 所有的无人机轮流通信并传输数据,从而消除了多个无人机对通信资源的争夺,使得每个无人机都能接收到其他无人机的信息,降低了通信碰撞导致的丢包率,保证了网络通信的稳定性,提高了多个无人机之间的协作效率。 79 | 80 | ![无人机通信示意图](https://pic.imgdb.cn/item/6061d1fe8322e6675c122b02.jpg) 81 | 82 | 令牌环算法是一种更加公平的算法,通常会与通信令牌结合,从而取得很好的效果。特别是当系统支持广播或组播通信模式时,该算法更加高效、可行。对于集中式和分布式算法都存在的单点故障问题,在令牌环中,若某一个程序(例如上图的 无人机 2)出现故障,则直接将令牌传递给故障程序的下一个程序(例如,上图中无人机 1 直接将令牌传送给无人机 3),从而很好地解决单点故障问题,提高系统的健壮性,带来更 好的可用性。但,这就要求每个程序都要记住环中的参与者信息,这样才能知道在跳过一个 参与者后令牌应该传递给谁。 83 | 84 | 令牌环算法的公平性高,在改进单点故障后,稳定性也很高,适用于系统规模较 小,并且系统中每个程序使用临界资源的频率高且使用时间比较短的场景。 85 | 86 | ## 两层结构的分布式令牌环算法 87 | 88 | 由于大规模系统的复杂性,我们很自然地想到要用一个相对复杂的互斥算法。时下有一个很 流行的互斥算法,两层结构的分布式令牌环算法,把整个广域网系统中的节点组织成两层结 构,可以用于节点数量较多的系统,或者是广域网系统。我们知道,广域网由多个局域网组成,因此在该算法中,局域网是较低的层次,广域网是较 高的层次。每个局域网中包含若干个局部进程和一个协调进程。局部进程在逻辑上组成一个 环形结构,在每个环形结构上有一个局部令牌 T 在局部进程间传递。局域网与局域网之间 通过各自的协调进程进行通信,这些协调进程同样组成一个环结构,这个环就是广域网中的全局环。在这个全局环上,有一个全局令牌在多个协调进程间传递。 89 | -------------------------------------------------------------------------------- /01~分布式基础/03~CAP/99~参考资料/2024~The CAP Theorem. The Bad, the Bad, & the Ugly.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://blog.dtornow.com/the-cap-theorem.-the-bad-the-bad-the-ugly/) 2 | 3 | # The CAP Theorem. The Bad, the Bad, & the Ugly 4 | -------------------------------------------------------------------------------- /01~分布式基础/03~CAP/BASE.md: -------------------------------------------------------------------------------- 1 | # BASE 2 | 3 | BASE 模型是 eBay 工程师提出大规模分布式系统的实践总结,其理念在于随着时间的迁移,不同节点的数据总是向同一个方向有一个相同的变化。在多数互联网应用情况下,其实我们也并非一定要求强一致性,部分业务可以容忍一定程度的延迟一致,所以为了兼顾效率,发展出来了最终一致性理论 BASE,BASE 是指基本可用(Basically Available)、软状态(Soft State)、最终一致性(Eventual Consistency) 4 | 5 | - 基本可用(Basically Available):基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。譬如音频直播或是做活动时,当业务量非常大的时候可以降级。做游戏也是,在战斗的时候最关心数值的增长,看了多少人都无所谓,缓解核心内容的压力。 6 | - 软状态(Soft State):软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。在写代码、编程业务的设计上,必须容忍有一定的临时数据同步。 7 | - 最终一致性(Eventual Consistency):最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。 8 | 9 | BASE 理论本质上是对 CAP 的延伸和补充,更具体地说,是对 CAP 中 AP 方案的一个补充;CAP 理论是忽略延时的,而实际应用中延时是无法避免的。BASE 方案在分区期间牺牲一致性,但分区故障恢复后,系统应该达到最终一致性。BASE 与 ACID 截然相反,ACID 比较悲观,在每个操作结束时都强制保持一致性;而 BASE 比较乐观,接受数据库的一致性处于一种动荡不定的状态。虽然听起来很难应付,实际上这相当好管理,并且可带来 ACID 无法企及的更高级别的可伸缩性。 10 | 11 | 许多的 NoSQL 是按照 BASE 理论进行设计的,典型的例子包括:Dynamo、Cassandra、CouchDB。 12 | 13 | # 基本可用 14 | 15 | BASE 的可用性是通过支持局部故障而不是系统全局故障来实现的。譬如如果用户分区在 5 个数据库服务器上,则当一个用户数据库的故障只影响这台特定主机那 20% 的用户。 16 | 17 | # 软状态 18 | 19 | 考虑到全局锁和数据多版本的对比,把各个节点的相关数据都上锁,这是一个悲观锁,一旦写任务,其他人都能改我的数据,这是比较悲观的心态。 20 | 21 | 而数据多版本,类似于乐观锁,导致其他人和我方数据冲突的机会并不是那么多,只要在提交的时候发现版本不一样,更新一下,汇总数据就可以了。做好业务上的隔离,多数情况都属于多版本,技术都能解决,不一定要把所有的东西都锁死。允许有一定的临时数据。最终一致性,在临时上的数据不一样,数据同步也是要花时间的。 22 | 23 | # 最终一致性 24 | 25 | 最终一致性,即允许窗口期数据不一致,互相关联的数据要同步。序列一致性,全局按照序列顺序来做。线性一致性,每一个时间的时钟要同步,时间序列是严格的,按顺序的。最后是强一致性,一个时间只能实行一个任务。 26 | 27 | 最终一致性的关键词是`一定时间`和`最终`,一定时间和数据的特性是强关联的,不同业务不同数据能够容忍的不一致时间是不同的。例如支付类业务是要求秒级别内达到一致,因为用户时时关注;用户发的最新微博,可以容忍 30 分钟内达到一致的状态,因为用户短时间看不到明星发的微博是无感知的。而最终的含义就是不管多长时间,最终还是要达到一致性的状态。 28 | -------------------------------------------------------------------------------- /01~分布式基础/03~CAP/DLS.md: -------------------------------------------------------------------------------- 1 | # DLS 2 | -------------------------------------------------------------------------------- /01~分布式基础/03~CAP/README.md: -------------------------------------------------------------------------------- 1 | # CAP 2 | 3 | CAP 定理又被称作布鲁尔定理,是加州大学的计算机科学家布鲁尔在 2000 年提出的一个猜想;2002 年,麻省理工学院的赛斯·吉尔伯特和南希·林奇发表了布鲁尔猜想的证明,使之成为分布式计算领域公认的一个定理。CAP 定理认为,在分布式系统中,系统的一致性(Consistency)、可用性(Availability)、分区容忍性(Partition tolerance)三者不可能同时兼顾。而由于网络通信的不稳定性,分区容忍性是必须要保证的,因此 CAP 理论在实践中往往指明了在设计应用的时候就需要在一致性和可用性之间权衡选择。互联网应用比企业级应用更加偏向保持可用性,因此通常用最终一致性代替传统事务的 ACID 强一致性。 4 | 5 | ![](https://assets.ng-tech.icu/item/20230416205608.png) 6 | 7 | 布鲁尔在提出 CAP 猜想时并没有具体定义 Consistency、Availability、Partition Tolerance 这 3 个词的含义,不同资料的具体定义也有差别: 8 | 9 | - 一致性(Consistency):所有节点都能访问同一份最新的副本数据。 10 | - 可用性(Availability):每个请求都能接收到一个响应,无论响应成功或失败,而不应该是网络超时、连接断开等非服务程序答复。 11 | - 分区容忍性(Partition Tolerance):除了整个网络的故障外,其他的故障(集)都不能导致整个系统无法正确响应。 12 | 13 | CAP 定理同样也是权衡的问题,它最初是作为一个经验法则提出的,没有准确的定义,目的是开始讨论数据库的权衡。CAP 定理鼓励数据库工程师向分布式无共享系统的设计领域深入探索,这类架构更适合实现大规模的网络服务: 14 | 15 | - 如果应用需要线性一致性,且某些副本因为网络问题与其他副本断开连接,那么这些副本掉线时不能处理请求。请求必须等到网络问题解决,或直接返回错误。(无论哪种方式,服务都不可用(unavailable))。 16 | - 如果应用不需要线性一致性,那么某个副本即使与其他副本断开连接,也可以独立处理请求(例如多主复制)。在这种情况下,应用可以在网络问题前保持可用,但其行为不是线性一致的。 17 | 18 | CAP 定理的正式定义仅限于很狭隘的范围,它只考虑了一个一致性模型(即线性一致性)和一种故障(网络分区或活跃但彼此断开的节点)。它没有讨论任何关于网络延迟,死亡节点或其他权衡的事。因此,尽管 CAP 在历史上有一些影响力,但对于设计系统而言并没有实际价值。CAP 定理现在已经被更精确的结果取代,所以它现在基本上成了历史古迹了。 19 | 20 | ## 多主复制的难题 21 | 22 | 譬如在多数据中心中,多主复制通常是理想的选择,但是如果两个数据中心之间发生网络中断会发生什么?我们假设每个数据中心内的网络正在工作,客户端可以访问数据中心,但数据中心之间彼此无法互相连接。 23 | 24 | ![网络中断迫使在线性一致性和可用性之间做出选择](https://s2.ax1x.com/2020/02/16/39BtoT.png) 25 | 26 | 使用多主数据库,每个数据中心都可以继续正常运行:由于在一个数据中心写入的数据是异步复制到另一个数据中心的,所以在恢复网络连接时,写入操作只是简单地排队并交换。另一方面,如果使用单主复制,则主库必须位于其中一个数据中心。任何写入和任何线性一致的读取请求都必须发送给该主库,因此对于连接到从库所在数据中心的客户端,这些读取和写入请求必须通过网络同步发送到主库所在的数据中心。 27 | 28 | 在单主配置的条件下,如果数据中心之间的网络被中断,则连接到从库数据中心的客户端无法联系到主库,因此它们无法对数据库执行任何写入,也不能执行任何线性一致的读取。它们仍能从从库读取,但结果可能是陈旧的(非线性一致)。如果应用需要线性一致的读写,却又位于与主库网络中断的数据中心,则网络中断将导致这些应用不可用。如果客户端可以直接连接到主库所在的数据中心,这就不是问题了,哪些应用可以继续正常工作。但直到网络链接修复之前,只能访问从库数据中心的客户端会中断运行。 29 | -------------------------------------------------------------------------------- /01~分布式基础/03~CAP/特性与模型.md: -------------------------------------------------------------------------------- 1 | # CAP 特性定义 2 | 3 | ## Consistency | 一致性 4 | 5 | 一致性又称为原子性或者事务性,对某个指定的客户端来说,读操作保证能够返回最新的写操作结果。一致性表示一个事务的操作是不可分割的,要不然这个事务完成,要不然这个事务不完成,不会出现这个事务完成了一半这样的情况,也就不会读取到脏数据。传统的 ACID 数据库是很少存在一致性问题的,因为数据的单点原因,数据的存取又具有良好的事务性,不会出现读写的不一致。 6 | 7 | 而在分布式系统中,经常出现的一个数据不具有一致性的情况是读写数据时缺乏一致性。比如两个节点数据冗余,第一个节点有一个写操作,数据更新以后没有有效的使得第二个节点更新数据,在读取第二个节点的时候就会出现不一致的问题出现。这里并不是强调同一时刻拥有相同的数据,对于系统执行事务来说,在事务执行过程中,系统其实处于一个不一致的状态,不同的节点的数据并不完全一致。 8 | 9 | 一致性强调客户端读操作能够获取最新的写操作结果,是因为事务在执行过程中,客户端是无法读取到未提交的数据的。只有等到事务提交后,客户端才能读取到事务写入的数据,而如果事务失败则会进行回滚,客户端也不会读取到事务中间写入的数据。 10 | 11 | ## Availability | 可用性 12 | 13 | 可用性主要是指系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况;所谓非故障的节点在合理的时间内返回合理的响应,而不是错误和超时的响应。这里强调的是合理的响应,不能超时,不能出错。注意并没有说正确的结果,例如,应该返回 Java 但实际上返回了 Go,肯定是不正确的结果,但可以是一个合理的结果。 14 | 15 | 可用性通常情况下可用性和分布式数据冗余,负载均衡等有着很大的关联。 16 | 17 | ## Partition Tolerance | 分区容忍性 18 | 19 | 分区容忍性即指当出现网络分区后,系统能够继续履行职责。这里网络分区是指:一个分布式系统里面,节点组成的网络本来应该是连通的。然而可能因为一些故障,譬如节点间网络连接断开、节点宕机等,使得有些节点之间不连通了,整个网络就分成了几块区域,数据就散布在了这些不连通的区域中。好的分区容忍性要求能够使应用虽然是一个分布式系统,而看上去却好像是在一个可以运转正常的整体。比如现在的分布式系统中有某一个或者几个机器宕掉了,其他剩下的机器还能够正常运转满足系统需求,这样就具有好的分区容忍性。 20 | 21 | 分区容忍性的典型场景,譬如数据库有两个拷贝在两个不同的数据中心,当数据被写到一个数据中心的时候,他也一定要被写到另一个数据中心。那么现在假设网络中断了,这就是我们所说的网络分区的意思: 22 | 23 | ![](https://assets.ng-tech.icu/item/20230416205549.png) 24 | 25 | 我们可以有如下的两种策略: 26 | 27 | - 应用还是被允许写到数据库,所以两边的数据库还是完全可用的。但是一旦两个数据库之间的网络中断了,任何一个数据中心的写操作就不会在另一个数据中心出现,这就违反了一致性。 28 | 29 | - 如果你不想失去一致性,你就必须保证你的读写操作都在同一个数据中心,即 Leader 或者 Master 节点。另一个数据中心,因为网络故障不能被更新,就必须停止接收读写操作,直到网络恢复,两边数据库又同步了之后。所以虽然非 Leader 的数据库在正常运行着,但是他却不能处理请求,这就违反了可用性定义。 30 | 31 | # CAP 模型实践 32 | 33 | 实际应用中的可用性和 CAP 可用性并不相同。你应用的可用性多数是通过 SLA 来衡量的(比如 99.9%的正确的请求一定要在一秒钟之内返回成功),但是一个系统无论是否满足 CAP 可用性其实都可以满足这样的 SLA。实际操作中,跨多个数据中心的系统经常是通过异步备份(Asynchronous Replication)的,所以不是可线性化的。但是做出这个选择的原因经常是因为远距离网络的延迟,而不是仅仅为了处理数据中心的网络故障。 34 | 35 | 互联网行业模型。不同的业务类型要求不同的 CAP 模型,CA 适用于支付、交易、票务等强一致性的行业,宁愿业务不可用,也不能容忍脏数据。互联网业务对于强一致性不高,发个帖子要审核,没人看到无所谓。发一个音频要进行编码审核才能看到。 36 | 37 | ## CA 模型 38 | 39 | CA 模型即保证了可用性与强一致性,牺牲了分区容忍性。比如 MySQL Cluster 集群,提供两阶段提交事务方式,保证各节点数据强一致性。MySQL 的集群无法忍受脱离集群独立工作,一旦和集群脱离了心跳,节点出问题,导致分布式事务操作到那个节点后,整个就会失败,这是分区容忍性的牺牲。 40 | 41 | 当发生分区现象时,为了保证一致性,系统需要禁止写入。当有写入请求时,系统返回 Error(例如,当前系统不允许写入),不过这就又和可用性相冲突,因为可用性要求不可以返回错误或超时。因此分布式系统理论上不可能选择 CA 模型。 42 | 43 | ## CP 模型 44 | 45 | CP 模型即选择了强一致性与分区容忍性,牺牲了可用性。假设系统中存在节点 Node1 与 Node2,因为 Node1 节点和 Node2 节点连接中断导致分区现象,Node1 节点的数据已经更新到 y,但是 Node1 和 Node2 之间的复制通道中断,数据 y 无法同步到 Node2,Node2 节点上的数据还是旧数据 x。这时客户端 C 访问 Node2 时,Node2 需要返回 error,提示客户端系统现在发生了错误,这种处理方式违背了可用性的要求,因此 CAP 三者只能满足 CP。 46 | 47 | CP 模型譬如 Redis 客户端 Hash 和 Twemproxy 集群,各 Redis 节点无共享数据,所以不存在节点间的数据不一致问题。其中节点宕机了,都会影响整个 Redis 集群的工作。当 Redis 某节点失效后,这个节点里的所有数据都无法访问。如果使用 3.0 Redis Cluster,它有中心管理节点负责做数据路由。 48 | 49 | ## AP 模型 50 | 51 | AP 模型牺牲了一致性,譬如同样是 Node2 节点上的数据还是旧数据 x,这时客户端 C 访问 Node2 时,Node2 将当前自己拥有的数据 x 返回给客户端了。而实际上当前最新的数据已经是 y 了,这就不满足一致性(Consistency)的要求了,因此 CAP 三者只能满足 AP。 52 | 53 | 值得一提的是,这里 Node2 节点返回 x,虽然不是一个正确的结果,但是一个合理的结果,因为 x 是旧的数据,并不是一个错乱的值,只是不是最新的数据。 54 | 55 | 譬如在 Cassandra 集群时,数据可以访问,数据能备份到各个节点之间,其中一个节点失效的话,数据还是可以出来的。而分布式事务的各个节点更新了提交了只是其中一部分节点,底层继续同步,这是 AP 模型。 56 | -------------------------------------------------------------------------------- /01~分布式基础/04~日志模型/WAL/README.md: -------------------------------------------------------------------------------- 1 | # Write Ahead Log 2 | -------------------------------------------------------------------------------- /01~分布式基础/04~日志模型/分割日志/README.md: -------------------------------------------------------------------------------- 1 | # 分割日志 2 | 3 | # Links 4 | 5 | - https://cubox.pro/c/lhtMLC 分布式系统设计模式 - 分割日志(Segmented Log) -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Distributed Systems for Fun and Profit/English/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/DistributedSystem-Notes/d5c81c808d80e3697e264031c4ec5eb29c40f7d3/01~分布式基础/99~参考资料/Distributed Systems for Fun and Profit/English/.gitkeep -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Distributed Systems for Fun and Profit/README.md: -------------------------------------------------------------------------------- 1 | > [原文地址](http://book.mixu.net/distsys/single-page.html) 2 | -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Distributed Systems for Fun and Profit/中文翻译/00.引言.md: -------------------------------------------------------------------------------- 1 | # 引言 2 | 3 | 我期望能写一篇文章,它可以将许多最新的分布式系统(诸如 Amazon 的 Dynamo,谷歌的 BigTable 和 MapReduce, 还有 Apache 的 Hadoop 等等)的思想整合在一起。 4 | 5 | 在这篇文章中我试图提供一种更简单的方式来介绍分布式系统。对我来说,这意味着两件事:介绍你将需要的关键性的概念,以便当你需要阅读更高深的文章时能有一个[良好的体验](https://www.google.com/search?q=super+cool+ski+instructor)。同时也必须提供足够详实的叙述让你明白这究竟是怎么一回事,而又不至于陷入细枝末节之中。在 2013 年,你可以通过互联网更有选择性地阅读那些你觉得最有趣的主题。 6 | 7 | 在我看来,大多数分布式编程都是关于处理分布后两个因素所带来的影响: 8 | 9 | - 信息是以光速传播的 10 | - 独立事物,独立失败\* 11 | 12 | 换句话说,分布式编程的核心就是处理距离(废话!)和处理更多的事(废话!)。因此这些约束定义了一个可能的系统设计空间, 我希望在阅读完这篇文章之后,您会对距离、时间和一致性模型之间的相互作用有更好的理解。 13 | 14 | 本文主要讨论分布式编程和系统概念,在此之前你需要了解数据中心里的商业系统是如何运作的。尝试隐藏所有的一切显然是愚蠢的,因此您将学习许多关键的协议和算法(例如,在本学科中被引用次数最多的论文),其中包含一些新的令人兴奋而且还尚未被收录在大学教科书中的方法来研究最终的一致性,例如 CRDTs 和 CALM 定理。 15 | 16 | 我希望你喜欢这篇文章。如果你想表示感谢请在[Github](https://github.com/mixu/)或者[Twitter](http://twitter.com/mikitotakada)上关注我。当然,如果你发现了错误,也可以在[Github 上 pull request](https://github.com/mixu/distsysbook/issues) 17 | 18 | --- 19 | 20 | # 1. 基础 21 | 22 | [第一章](http://book.mixu.net/distsys/single-page.html#intro)通过一些重要的术语和概念来介绍高层次的分布式系统。它包括了一些需要实现的目标,如可扩展性、可用性、性能、延迟和容错能力;同时也介绍了其实现的难点以及抽象和模型,除此之外还介绍了分区和副本(replication)它们是如何发挥作用的。 23 | 24 | # 2. 上下层的抽象 25 | 26 | [第二章](http://book.mixu.net/distsys/single-page.html#abstractions)深入探讨了抽象与不可能性。首先我们将从尼采的引言开始,然后介绍系统模型以及在典型系统模型中所做的诸多假设。接下来我们将讨论 CAP 定理并总结了 FLP 不可能性的结论。然后转向 CAP 定理的含义,其中之一就是应该探索其他的一致性模型。最后讨论一些一致性模型。 27 | 28 | # 3. 时间与序列 29 | 30 | 理解分布式系统的很大一部分就是能充分的理解时间和序列。如果我们不能很好的理解这一点我们的系统将面临失败。[第三章](http://book.mixu.net/distsys/single-page.html#time)集中讨论了时间和序列以及时钟(如向量钟和失效检测器)之间的关系。 31 | 32 | # 4. 复制: 防止差异 33 | 34 | [第四章](http://book.mixu.net/distsys/single-page.html#replication) 介绍了复制问题及其实现的两种基本方式。事实证明,大部分的相关特性都可以用这个简单的描述来进行讨论。然后,从最小容错(2PC)到 Paxos,讨论了维护单副本一致性的复制方法。 35 | 36 | # 5. 复制: 允许差异 37 | 38 | [第五章](http://book.mixu.net/distsys/single-page.html#eventual)讨论了复制在弱一致性时的保证。它引入了一个基本的协调方案,即利用分区副本尝试达成协议。然后以 Amazon 的 Dynamo 为例讨论了系统设计时如何保证弱一致性。最后,讨论了两个关于无序编程的观点: CRDTs 和 CALM 定理。 39 | 40 | # 附录 41 | 42 | [附录](http://book.mixu.net/distsys/single-page.html#appendix) 涵盖了进一步阅读的建议。 43 | 44 | --- 45 | 46 | \*: 这是一个[谎言](http://en.wikipedia.org/wiki/Statistical_independence). [Jay Kreps 的这篇文章详细阐述了这一点](http://blog.empathybox.com/post/19574936361/getting-real-about-distributed-system-reliability)。 47 | -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Distributed Systems for Fun and Profit/中文翻译/01.Basics.md: -------------------------------------------------------------------------------- 1 | # 1. 高级分布式系统 2 | 3 | > 分布式编程是一门能让你在多台计算机上解决问题就如同在一台主机上一样的艺术。 4 | 5 | 任何计算机系统都需要完成以下两个基本任务: 6 | 7 | - 存储 8 | - 计算 9 | 10 | 分布式编程是一门能让你在多台计算机上解决问题就如同在一台主机上一样的艺术 —— 通常情况下是因为单台计算机的性能已经不足以解决当前问题了。 11 | 12 | 事实上并没有什么需求让你必须使用分布式系统。假设拥有无限的开发时间和经费,我们完全不需要使用分布式系统。所有的计算和存储我们都可以在一个小小的魔盒中完成——这是一个*专门为你设计的*快速而且可靠的单机系统。 13 | 14 | 然而,很少有人能够拥有无限的资源。因此,他们不得不在现实世界中的成本—效益曲线上找到一个合适的位置。在小规模的情况下,升级硬件是一个切实可行的方案。然而,随着问题规模的扩大,你将会达到硬件升级的瓶颈——你已经无法再对该结点升级或者升级的成本大大提高难以为继。如果你现在正处于当前这个状态,那么,欢迎你来到分布式的世界。 15 | 16 | 目前的现状是,性价比最高的是中端商用硬件,我们只需要通过容错软件就能使其维护成本大大降低。 17 | 18 | 计算主要受益于高端硬件,以至于其可以用内部存储器访问来取代缓慢的网络访问。但在节点间需要大量通信的任务中,高端硬件的性能优势却受到了限制。 19 | 20 | ![cost-efficiency](./image/barroso_holzle.png) 21 | 22 | 如[Barroso, Clidaras & Hölzle](http://www.morganclaypool.com/doi/abs/10.2200/S00516ED2V01Y201306CAC024)的上图所示,假设所有节点都采用统一的内存访问模式,那么高端硬件和商品级硬件之间的性能差距将随着集群规模的增加而减小 23 | 24 | 在理想情况下,添加一台新机器将使系统的性能和容量呈线性增加。但这显然是不可能的,因为计算机会产生一些额外的开销。数据需要被复制,计算任务需要被协调,诸如此类。这就是为什么研究分布式算法是值得的——它们为具体问题提供了有效的解决方案,除此之外也指导了什么是可行的,正确实现的最小成本是什么以及什么是不可行的。 25 | 26 | 本文的重点是分布式系统与编程,为此我们需要一个普通的,但又与商业密切相关的环境:数据中心。因此,举例来说我们将不会讨论由于某个异乎寻常的网络配置或者因为共享内存设置而产生的某种特殊问题。此外,我们的重点是探索系统架构,而不是针对任何特定的架构进行优化——后者有一个专门的主题来讨论这个。 27 | 28 | ## 我们想要实现的目标: 可扩展性与其他优点 29 | 30 | 依我所见,一切都始于处理规模 _(size)_ 的需要。 31 | 32 | 大多数事物在小尺度时都是微不足道的——但当你的事物一旦超过一定的大小、体积或其他物理约束时,同样的问题就会变得困难得多。这就如同你可以轻而易举的举起一块巧克力,但完全不可能举起一座山。又比如你可以迅速的数清房间里究竟有多少人,但你很难知道这个国家究竟有多少人。诸如此类。 33 | 34 | 所以,一切都要从规模——可扩展性说起。通俗地说,在一个可扩展的系统中随着我们规模的变大,事情并不应该逐渐变差。这有另一个定义: 35 | 36 | - [可扩展性](http://en.wikipedia.org/wiki/Scalability)是一个系统、网络或者进程的能力,使其能够处理越来越多的工作或者其能够被扩展以适应这种增长。 37 | 38 | 那么是什么在增长呢?嗯,你可以用任何术语来衡量增长(人口数量、用电量等)。但是我们一般主要用以下三点来进行衡量: 39 | 40 | - 规模扩展性: 添加更多的结点将使系统响应速度呈线性增快; 同时增长的数据集不应增加延迟。 41 | - 地域扩展性: 应该使用多个数据中心来减少用户查询的响应时间, 同时以某种合理的方式处理跨数据中心的延迟。 42 | - 管理扩展性: 添加更多的结点不应该增加系统的管理成本(例如管理员与机器的比例)。 43 | 44 | 当然, 在一个真实的系统中,增长将同时发生在多个不同的维度上; 每个指标也都只反映了增长的某些方面。 45 | 46 | 一个可扩展的系统应该随着用户规模的增加而持续满足用户的需求。其中有两个特别相关的方面——性能和可用性——可以用不同的方式来衡量。 47 | 48 | ### 性能(与延迟) 49 | 50 | - [性能](http://en.wikipedia.org/wiki/Computer_performance)的特征是计算机系统完成的有效工作量与其使用的时间和资源的比值。 51 | 52 | 根据具体情况,这可能涉及实现以下一项或多项目标: 53 | 54 | - 某项工作的响应时间短/延迟低 55 | - 高吞吐量 (处理工作的速率) 56 | - 计算资源利用率低 57 | 58 | 在对这些目标中的任何一个进行优化时都需要权衡取舍。例如,系统可以通过使用更大的批处理方式来实现更高的吞吐量,从而减少操作开销。但这种折衷方式将会使单个工作的响应时间变长。 59 | 60 | 我发现低延迟——缩短响应时间——是性能中最令人关注的部分,因为它与物理(而非经济)限制密切相关。与性能的其他方面相比,使用财政资源处理延迟更困难。 61 | 62 | 对于延迟有很多非常具体的定义,但我喜欢下面这个依据词源的解释: 63 | 64 | - 延迟 _(Latency)_ 65 | 66 | 是一种潜在 _(Latent)_ 的状态; 即某件事从开始到发生之间的一段时间。 67 | 68 | 那么“潜在”是什么意思呢? 69 | 70 | - 潜在 _(Latent)_ 71 | 72 | 其来源于拉丁语伪装 _(latens)_ 和隐藏 _(latentis)_ ,它的现在分词形式是 _lateo_ (“隐藏”)。指现存的但已经被隐藏或尚不活跃的。 73 | 74 | 这个定义非常的酷,因为它强调了延迟 _(latency)_ 是指从某个事件发生到受该事件影响或使其变得可见之间的时间。 75 | 76 | 例如,假设你感染了一种由空气传播的病毒,它能把人变成僵尸。潜伏期 _(latent)_ 就是你感染后到变成僵尸之间的这段时间。这就是延迟 _(latency)_ :即当事情已经发生但其被隐藏起来而致使不能观察到的这段时间。 77 | 78 | 现在让我们假设我们的分布式系统只执行一项高级任务:给定一个查询,它将获取系统中的所有数据并计算出一个结果。换句话说,可以将分布式系统看作是能够在当前内容上运行单确定性计算(函数)的数据存储。 79 | 80 | `result = query(all data in the system)` 81 | 82 | 那么,延迟的关键不是旧数据的数量,而是新数据在系统中“生效”的速度。例如,延迟可以根据从写入数据到读取可见之间所花费的时间来度量。 83 | 84 | 基于这个定义的另一个关键点是,如果什么事都没发生,那自然就没有“潜伏期 _(latent period)_ ”。数据不改变的系统不会(也不应该)有延迟问题。 85 | 86 | 在分布式系统中,存在一个无法克服的最小延迟:因为光速限制了信息传输的速度;与此同时硬件的每次运行也会有一个最小延迟(比如 RAM 和硬盘,还有 CPU)。 87 | 88 | 最小延迟对查询的影响主要取决于这些查询的性质以及这些信息所需要传输的物理距离。 89 | 90 | ### 可用性(与容错性) 91 | 92 | 可扩展系统的第二个方面是可用性。 93 | 94 | - [可用性](http://en.wikipedia.org/wiki/High_availability) 95 | 96 | 是指系统处于正常运行状态的时间比例。如果一个用户不能访问该系统,则我们称之为不可用。 97 | 98 | 分布式系统使我们能够实现在单个系统上难以实现的理想特性。例如,一台机器不能容忍任何故障,因为它要么失败要么成功。 99 | 100 | 而分布式系统则正是基于一堆不可靠的组件,并在此基础之上试图构建一个可靠的系统。 101 | 102 | 因此没有冗余的系统只能作为其底层组件使用。相反使用冗余构建的系统可以容忍部分故障,从而使其增加可用性。值得注意的是,“冗余”可能意味着不同的东西,比如组件、服务器、数据中心等等,诸如此类。 103 | 104 | 使用公式表示: `Availability = uptime / (uptime + downtime)`. 105 | 106 | 从技术角度来看,可用性主要是指容错性。因为故障出现的概率会随着组件数量的增加而增加,因此系统应该能够提供补偿机制,以使其可靠性不会随着组件数量的增加而降低。 107 | 108 | 例如: 109 | 110 | | 可靠性% | 每年允许多少宕机时间? | 111 | | ------------------- | --------------------- | 112 | | 90% ("一个九") | 超过一个月 | 113 | | 99% ("两个九") | 少于 4 天 | 114 | | 99.9% ("三个九") | 少于 9 小时 | 115 | | 99.99% ("四个九") | 少于 1 小时 | 116 | | 99.999% ("五个九") | ~ 5 分钟 | 117 | | 99.9999% ("六个九") | ~ 31 秒 | 118 | 119 | 可用性在某种意义上来说是比正常运行时间更为宽泛的概念,因为服务的可用性也可能受到网络中断或拥有该服务的公司的业务中断的影响(这将是一个与容错无关的因素,但仍然会影响系统的可用性)。但是,我们并不能了解系统的每一个具体方面,所以我们所能做的最好方式就是设计容错性。 120 | 121 | 什么是容错性? 122 | 123 | - 容错性 124 | 125 | 是指当故障发生时,系统能够以一种明确的方式继续运行的能力。 126 | 127 | 容错性可以归结为:先定义你所期望的故障,然后设计一个针对该故障的容错系统或算法。因此你不能对一个你没有考虑过的错误进行容错。 128 | 129 | ## 是什么阻碍了我们获得美好的事物呢? 130 | 131 | 分布式系统受到两个物理因素的制约: 132 | 133 | - 结点的数量 (随着所需存储和计算能力的增加而增加) 134 | - 两个结点间的距离(信息最多只能以光速传播) 135 | 136 | 因此在这些约束下工作会导致以下三点: 137 | 138 | - 随着独立节点数量的增加会增加系统故障的概率(降低可用性并增加管理成本) 139 | - 随着独立节点数量的增加可能会增加节点间通信的需求(随着规模的增加而降低性能) 140 | - 随着地理距离的增加而增加了远距离节点之间通信的最小延迟(降低某些操作的性能) 141 | 142 | 除了以上这三点之外——这是由物理约束导致的结果——是系统设计方案的世界。 143 | 144 | 性能和可用性都是由系统所提供的外部保证来定义的。从高层次上讲,你可以将这些保证看作是系统的 SLA(服务级别协议):假设我写入数据,那么我可以在其他地方快速访问它吗?数据写入后,我拿什么保证数据的持久性?如果我要求系统运行一个计算,那么它返回结果的速度到底有多快?当组件运行失败或停止运行时,这又会对系统产生什么影响? 145 | 146 | 还有另一个标准,虽然没有明确提到,但隐含在其中:可理解性 _(intelligibility)_ 。所做的这些保证有多容易理解呢?当然,什么是可理解性,没有简单的衡量标准。 147 | 148 | 我很想将“可理解性”归入物理限制之下。毕竟,对于人来说,这是一个硬件限制,我们需要很长的一段的时间来理解它,[因为其并不像移动我们的手指那般简单](http://en.wikipedia.org/wiki/Working_memory#Capacity).。这就是错误和异常的区别——错误是不正确的行为,而异常是意外行为。如果你足够聪明,你应该会预料到异常的发生。 149 | 150 | ## 抽象与模型 151 | 152 | 这就到了抽象和模型发挥作用的地方。抽象通过移除与解决问题无关的现实方面,从而使事情更易于管理。模型则是以一种精确的方式来描述分布式系统的关键属性。下一章我将讨论多种模型,比如: 153 | 154 | - 系统模型(异步/同步) 155 | - 故障模型(崩溃,分区,拜占庭问题) 156 | - 一致性模型(强一致性,最终一致性) 157 | 158 | 因此一个好的抽象可以使系统更加容易理解,同时也可以捕捉到与特定目的相关的因素。 159 | 160 | 现实中多个结点和我们希望系统能“如同一个系统一样工作”的渴望之间往往存在着对立关系。通常,最熟悉的模型(例如,在分布式系统上实现共享内存抽象)往往成本过于昂贵了。 161 | 162 | 而一个保证较弱的系统则会具有更高的行动自由度,因此其可能具有更高的性能——但这也导致其更难以理解。人们更容易理解单系统,而非节点集合的系统。 163 | 164 | 人们通常可以通过公开关于系统内部的更多细节来获得性能的提升。例如,在[列式存储](http://en.wikipedia.org/wiki/Column-oriented_DBMS)中,用户可以(在某种程度上)分析系统内键值对的位置,从而做出影响典型查询性能的决策。因而隐藏这些细节的系统会更容易理解(因为它们更像单个单元,而不需要考虑太多细节),相反暴露更多现实世界细节的系统可能具有更好的性能(因为它们更符合实际)。 165 | 166 | 一些类型的失败使得编写一个如同单一系统一样的分布式系统变得极其困难。网络延迟和网络分区(例如全部网络之中总有一些结点之间会出现故障)意味着系统有时需要做出艰难的选择,即是否保持更好的可用性,但同时失去一些无法执行的重要保证,或当此类故障发生时为了保证它的数据安全性而拒绝客户端。 167 | 168 | 这就是 CAP 定理——我将会在下一章讨论这个——用以描述这些对立关系。最终,理想的系统应该同时满足程序员的需求(简洁的语义 _(clean semantics)_ )和业务需求(可用性/一致性/延迟)。 169 | 170 | ## 设计技巧:分区和复制 171 | 172 | 数据集在多个节点之间分布的方式非常重要。为了使我们能应对任何计算的发生,我们需要找到数据并对其进行处理。 173 | 174 | 有两种基本技术可以应用于我们的数据集。一种方式是拆分到多个节点上,以便其能进行更多的并行处理——分区。另一种方式是在不同的节点上复制或缓存所有数据,以减少客户端和服务器之间的距离并提高容错能力——复制。 175 | 176 | > 分而治之 - 即分区与复制 177 | 178 | 下图说明了这两者之间的区别:使用分区方式处理的数据(下面的 A 和 B)被分成几个独立的数据集,而采用复制方式处理的数据(下面的 C)被复制到多个位置。 179 | 180 | ![Partition and replicate](./image/part-repl.png) 181 | 182 | 这两种方式的组合使用在解决分布式计算问题中扮演着重要的角色。当然,这其中的诀窍在于为你的具体实现选择一种合适的技术; 现在有许多算法可以实现复制与分区,但每种算法也都有着各自不同的优势和局限,这需要根据你的设计目标来进行评估。 183 | 184 | ### 分区技术 185 | 186 | 分区技术是指将原数据集划分为更小的独立数据集;这是用来减少数据集增长所带来的影响,因为每个分区是原数据的一个子集。 187 | 188 | - 分区是通过限制要使用的数据量并通过在相同分区中定位相关数据来提高其性能。 189 | - 分区是通过允许分区独立失败来提高其可用性,因此可以通过增加节点数量来减少失败。 190 | 191 | 分区技术也是依赖于特定的应用程序的,所以在不知道具体细节的情况下很难对其进行说明。这就是为什么大多数文章中都把重点放在复制部分,当然也包括本文。 192 | 193 | 分区技术主要是根据你认为的主要访问模式来定义你的分区,并处理独立分区带来的限制(例如跨分区的低效访问,不同的增长率等)。 194 | 195 | ### 复制技术 196 | 197 | 复制技术是指在多台机器上制作相同数据的副本;从而允许更多的服务器参与计算。 198 | 199 | 让我大致引用一下 [Homer J. Simpson](http://en.wikipedia.org/wiki/Homer_vs._the_Eighteenth_Amendment) 的话: 200 | 201 | > 复制!生活中所有问题的成因和解决方法。 202 | 203 | 复制——拷贝某些内容——是我们对抗延迟的主要方式。 204 | 205 | - 复制通过增加适用于新数据副本的计算能力和带宽来提高性能。 206 | - 复制可以通过创建额外的数据副本来提高可用性,因此可以通过增加节点数量来增加可用性。 207 | 208 | 复制是为了提供额外的带宽,并在重要的地方进行缓存。它会根据某种一致性模型然后以某种方式来保持数据的一致性。 209 | 210 | 复制技术允许我们实现可扩展性、性能和容错性。害怕失去可用性或降低系统性能?复制数据可以避免瓶颈或单点故障。缓慢的计算?我们可以在多个系统上进行复制计算。慢速 I/O?我们可以将数据复制到本地缓存从而减少延迟或复制到到多台机器上以增加吞吐量。 211 | 212 | 复制技术也是许多问题的根源,因为现在独立的数据副本必须在多台机器上保持同步——这意味着必须确保复制遵循一致性模型。 213 | 214 | 一致性模型的选择至关重要:良好的一致性模型为程序员提供了简洁的语义(换句话说,它所保证的属性很容易理解),并满足了诸如高可用性或强一致性等业务/设计目标。 215 | 216 | 只有强一致性模型——它允许你如同底层数据并没有被复制到多个结点一样来进行编程。其他一致性模型都需要向程序员公开一些复制的内部细节。然而,较弱的一致性模型可以提供更低的延迟和更高的可用性,并且不一定更难理解,只是不同而已。 217 | 218 | --- 219 | 220 | ## 扩展阅读 221 | 222 | - [The Datacenter as a Computer - An Introduction to the Design of Warehouse-Scale Machines](http://www.morganclaypool.com/doi/pdf/10.2200/s00193ed1v01y200905cac006) - Barroso & Hölzle, 2008 223 | - [Fallacies of Distributed Computing](http://en.wikipedia.org/wiki/Fallacies_of_Distributed_Computing) 224 | - [Notes on Distributed Systems for Young Bloods](http://www.somethingsimilar.com/2013/01/14/notes-on-distributed-systems-for-young-bloods/) - Hodges, 2013 225 | -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Distributed Systems for Fun and Profit/中文翻译/10.Appendix.md: -------------------------------------------------------------------------------- 1 | # 6. 扩展阅读与附录 2 | 3 | If you've made it this far, thank you. 4 | 5 | 如果你走到了这一步,感谢你的阅读。 6 | 7 | If you liked the book, follow me on [Github](https://github.com/mixu/) (or [Twitter](http://twitter.com/mikitotakada)). I love seeing that I've had some kind of positive impact. "Create more value than you capture" and all that. 8 | 9 | 如果你喜欢这本书,请在[Github](https://github.com/mixu/)(或[Twitter](http://twitter.com/mikitotakada))上关注我。我喜欢看到我产生了某种积极的影响。"创造的价值远超你所能获得的"。 10 | 11 | 在此我需要感谢:logpath、alexras、globalcitizen、graue、frankshearar、roryokane、jpfuentes2、eeror、cmeiklejohn、stevenproctor eos2102 和 stveloughran 所提供的帮助!当然,还有文章一些错误和疏漏,是我的错。当然,剩下的任何错误和遗漏都是我的错! 12 | 13 | 值得注意的是,我关于最终一致性的一章是以伯克利为中心的;我想改变这一点。我还跳过了一个重要的时间用例:一致性快照。还有几个主题我应该扩展一下:即明确讨论安全和可用性,以及更详细地讨论一致性哈希。不过,我要去参加[2013 年的 Strange Loop](https://thestrangeloop.com/),随便吧。 14 | 15 | 如果这本书有第 6 章的话,可能就是关于如何利用和处理大量数据的方法。似乎最常见的"大数据"计算是通过一个[简单的程序来处理大量数据集](http://en.wikipedia.org/wiki/SPMD)。我不知道后续的章节会是什么(也许是高性能计算,因为目前的重点是可行性),但我可能会在几年后知道。 16 | 17 | ## 关于分布式系统的书籍 18 | 19 | #### 分布式算法(Lynch) 20 | 21 | 这可能是最常被推荐的关于分布式算法的书。我也会推荐它,但有一个警告。它非常全面,但它是写给研究生读者的,所以你会花很多时间阅读同步系统和共享内存算法,然后才会读到从业者最感兴趣的东西。 22 | 23 | #### 可靠和安全的分布式编程导论 (Cachin, Guerraoui & Rodrigues) 24 | 25 | 对于一个从业者来说,这是一本很有意思的书。它很短,充满了实际的算法实现。 26 | 27 | #### 复制:理论与实践 28 | 29 | 如果你对复制感兴趣,这本书很了不起。关于复制的一章主要是基于此书有趣部分的综合,再加上最近的阅读。 30 | 31 | #### 分布式系统:算法 (Ghosh) 32 | 33 | #### 分布式算法导论 (Tel) 34 | 35 | #### 事务性信息系统:理论、算法和并发控制与恢复实践 (Weikum & Vossen) 36 | 37 | 本书讲的是传统的事务性信息系统,如本地的 RDBMS。最后有两章是关于分布式事务的,但本书的重点是事务处理。 38 | 39 | #### 事务处理: 概念与技术 ( Gray & Reuter) 40 | 41 | 一部经典之作。我觉得 Weikum & Vossen 更加与时俱进。 42 | 43 | ## 重要论文 44 | 45 | 每年,[Edsger W. Dijkstra 分布式计算奖](http://en.wikipedia.org/wiki/Dijkstra_Prize)都会颁发给关于分布式计算原理的优秀论文。查看完整名单的链接,其中包括以下经典论文: 46 | 47 | - "[Time, Clocks and Ordering of Events in a Distributed System](http://research.microsoft.com/users/lamport/pubs/time-clocks.pdf)" - Leslie Lamport 48 | - "[Impossibility of Distributed Consensus With One Faulty Process](http://theory.lcs.mit.edu/tds/papers/Lynch/jacm85.pdf)" - Fisher, Lynch, Patterson 49 | - "[Unreliable failure detectors and reliable distributed systems](http://scholar.google.com/scholar?q=Unreliable+Failure+Detectors+for+Reliable+Distributed+Systems)" - Chandra and Toueg 50 | 51 | 微软学术搜索有一个[分布式和并行计算领域的顶级出版物按引用次数排序](http://libra.msra.cn/RankList?entitytype=1&topDomainID=2&subDomainID=16&last=0&start=1&end=100)列表,这可能是一个有趣的列表,可以浏览更多经典。 52 | 53 | 以下是另外一些推荐的文件清单: 54 | 55 | - [Nancy Lynch's recommended reading list](http://courses.csail.mit.edu/6.852/08/handouts/handout3.pdf) from her course on Distributed systems. 56 | - [NoSQL Summer paper list](http://nosqlsummer.org/papers) - a curated list of papers related to this buzzword. 57 | - [A Quora question on seminal papers in distributed systems](http://www.quora.com/What-are-the-seminal-papers-in-distributed-systems-Why). 58 | 59 | ### 系统 60 | 61 | - [The Google File System](http://research.google.com/archive/gfs.html) - Ghemawat, Gobioff and Leung 62 | - [MapReduce: Simplified Data Processing on Large Clusters](http://research.google.com/archive/mapreduce.html) - Dean and Ghemawat 63 | - [Dynamo: Amazon’s Highly Available Key-value Store](http://scholar.google.com/scholar?q=Dynamo%3A+Amazon's+Highly+Available+Key-value+Store) - DeCandia et al. 64 | - [Bigtable: A Distributed Storage System for Structured Data](http://research.google.com/archive/bigtable.html) - Chang et al. 65 | - [The Chubby Lock Service for Loosely-Coupled Distributed Systems](http://research.google.com/archive/chubby.html) - Burrows 66 | - [ZooKeeper: Wait-free coordination for Internet-scale systems](http://labs.yahoo.com/publication/zookeeper-wait-free-coordination-for-internet-scale-systems/) - Hunt, Konar, Junqueira, Reed, 2010 67 | -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Patterns of Distributed Systems/README.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://martinfowler.com/articles/patterns-of-distributed-systems/) 2 | 3 | # Patterns of Distributed Systems 4 | 5 | 分布式系统为编程提供了一个特殊的挑战。它们通常要求我们有多个数据副本,这些副本需要保持同步。然而,我们不能依靠处理节点可靠地工作,网络延迟很容易导致不一致的结果。尽管如此,许多组织还是依靠一系列的核心分布式软件来处理数据存储、消息传递、系统管理和计算能力。这些系统面临着共同的问题,他们用类似的解决方案来解决。本文将这些解决方案确认并发展为模式,通过这些模式,我们可以建立起对如何更好地理解、交流和教授分布式系统设计的认识。 6 | 7 | ## 模式 8 | 9 | - 以时钟为限的等待(Clock-Bound Wait) 10 | - [一致性内核(Consistent Core)](content/consistent-core.md) 11 | - 新生领导者(Emergent Leader) 12 | - 固定分区(Fixed Partitions) 13 | - [追随者读取(Follower Reads)](content/follower-reads.md) 14 | - [世代时钟(Generation Clock)](content/generation-clock.md) 15 | - [Gossip 传播(Gossip Dissemination)](content/gossip-dissemination.md) 16 | - [心跳(HeartBeat)](content/heartbeat.md) 17 | - [高水位标记(High-Water Mark)](content/high-water-mark.md) 18 | - [混合时钟(Hybrid Clock)](content/hybrid-clock.md) 19 | - [幂等接收者(Idempotent Receiver)](content/idempotent-receiver.md) 20 | - 键值范围分区(Key-Range Partitions) 21 | - [Lamport 时钟(Lamport Clock)](content/lamport-clock.md) 22 | - [领导者和追随者(Leader and Followers)](content/leader-and-followers.md) 23 | - [租约(Lease)](content/lease.md) 24 | - [低水位标记(Low-Water Mark)](content/low-water-mark.md) 25 | - [Paxos](content/paxos.md) 26 | - [Quorum](content/quorum.md) 27 | - [复制日志(Replicated Log)](content/replicated-log.md) 28 | - 批量请求(Request Batch) 29 | - [请求管道(Request Pipeline)](content/request-pipeline.md) 30 | - 请求等待列表(Request Waiting List) 31 | - [分段日志(Segmented Log)](content/segmented-log.md) 32 | - [单一 Socket 通道(Single Socket Channel)](content/single-socket-channel.md) 33 | - [单一更新队列(Singular Update Queue)](content/singular-update-queue.md) 34 | - [状态监控(State Watch)](content/state-watch.md) 35 | - [两阶段提交(Two Phase Commit)](content/two-phase-commit.md) 36 | - [版本向量(Version Vector)](content/version-vector.md) 37 | - [有版本的值(Versioned Values)](content/versioned-value.md) 38 | - [预写日志(Write-Ahead Log)](content/write-ahead-log.md) 39 | 40 | ## 术语表 41 | 42 | | 英文 | 翻译 | 43 | | --------------------- | -------------- | 44 | | durability | 持久性 | 45 | | Write-Ahead Log | 预写日志 | 46 | | append | 追加 | 47 | | hash | 哈希 | 48 | | replicate | 复制 | 49 | | failure | 失效 | 50 | | partition | 分区 | 51 | | HeartBeat | 心跳 | 52 | | Quorum | Quorum | 53 | | Leader | 领导者 | 54 | | Follower | 追随者 | 55 | | High Water Mark | 高水位标记 | 56 | | Low Water Mark | 低水位标记 | 57 | | entry | 条目 | 58 | | propagate | 传播 | 59 | | disconnect | 失联、断开连接 | 60 | | Generation Clock | 世代时钟 | 61 | | group membership | 分组成员 | 62 | | partitions | 分区 | 63 | | liveness | 活跃情况 | 64 | | round trip | 往返 | 65 | | in-flight | 在途 | 66 | | time to live | 存活时间 | 67 | | head of line blocking | 队首阻塞 | 68 | | coordinator | 协调者 | 69 | | lag | 滞后 | 70 | | fanout | 扇出 | 71 | | incoming | 传入 | 72 | | CommitIndex | 提交索引 | 73 | | candidate | 候选者 | 74 | -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Patterns of Distributed Systems/consistent-core.md: -------------------------------------------------------------------------------- 1 | # 一致性内核(Consistent Core) 2 | 3 | **原文** 4 | 5 | https://martinfowler.com/articles/patterns-of-distributed-systems/consistent-core.html 6 | 7 | 维护一个较小的内核,为大规模数据集群提供更强的一致性,这样,可以在无需实现基于 Quorum 算法的前提下协调服务器行为。 8 | 9 | **2021.1.5** 10 | 11 | ## 问题 12 | 13 | 集群需要处理大规模的数据,就需要越来越多的服务器。对于服务器集群而言,有一些通用性的需求,比如,选择某个特定的服务器成为某个任务的主节点、管理分组成员信息、将数据分区映射到服务器上等等。这些需求都需要强一致性的保证,也就是说,要有线性一致性。实现本身还要有对失效的容忍。一种常见的方式是,使用一种基于 [Quorum](quorum.md) 且支持失效容忍的一致性算法。但是,基于 Quorum 的系统,其吞吐量会随着集群规模的变大而降低。 14 | 15 | ## 解决方案 16 | 17 | 实现一个三五个节点的小集群,提供线性一致性的保证,同时支持失效容忍[1]。一个单独数据集群可以使用小的一致性集群管理元数据,采用类似于[租约(Lease)](lease.md) 之类的原语在集群范围内进行决策。这样一来,数据集群就可以增长到很大的规模,但对于某些需要强一致性保证的动作,可以使用比较小的元数据集群。 18 | 19 | 20 | ![一致性内核](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/ConsistentCore.png) 21 |
图1:一致性内核
22 | 23 | 一个典型一致性内核接口应该是下面这个样子: 24 | 25 | ```java 26 | public interface ConsistentCore { 27 | CompletableFuture put(String key, String value); 28 | List get(String keyPrefix); 29 | CompletableFuture registerLease(String name, long ttl); 30 | void refreshLease(String name); 31 | void watch(String name, Consumer watchCallback); 32 | } 33 | ``` 34 | 以最低的标准看,一致性内核提供了一个简单的键值存储机制,用于存储元数据。 35 | 36 | ### 元数据存储 37 | 38 | 存储可以用诸如 Raft 之类的共识算法实现。它是可复制的预写日志的一个样例实现,其中的复制由[领导者和追随者(Leader and Followers)](leader-and-followers.md) 进行处理,使用 [Quorum](quorum.md) 的话,可以使用[高水位标记(High-Water Mark)](high-water-mark.md)追踪成功的复制。 39 | 40 | #### 支持层级结构的存储 41 | 42 | 一致性内核通常用于存储这样的数据,比如,分组成员、跨服务器的任务分布。一种常见的使用模式是,通过前缀将元数据的类型做一个分类,比如,对于分组成员信息,键值可以存成类似于 `/servers/1`、`servers/2` 等等。对于任务分配给哪些服务器,键值可以是 `/tasks/task1`、`/tasks/task2`。通常来说,要读取这些数据,所有的键值上都要带上特定的前缀。比如,要读取集群中的所有服务器信息,就要读取所有与以 `/servers` 为前缀的键值。 43 | 44 | 下面是一个示例的用法: 45 | 46 | 服务器只要创建一个属于自己的有 `/servers` 前缀的键值,就可以将自身注册到一致性内核中。 47 | 48 | ```java 49 | client1.setValue("/servers/1", "{address:192.168.199.10, port:8000}"); 50 | client2.setValue("/servers/2", "{address:192.168.199.11, port:8000}"); 51 | client3.setValue("/servers/3", "{address:192.168.199.12, port:8000}"); 52 | ``` 53 | 54 | 客户端只要读取以 `/servers` 为前缀的键值,就可以获取所有集群中的服务器信息,像下面这样: 55 | 56 | 57 | ```java 58 | assertEquals(client1.getValue("/servers"), Arrays.asList( 59 | "{address:192.168.199.12, port:8000}", 60 | "{address:192.168.199.11, port:8000}", 61 | "{address:192.168.199.10, port:8000}")); 62 | ``` 63 | 64 | 因为数据存储的层次结构属性,像 [zookeeper](https://zookeeper.apache.org/)、[chubby](https://research.google/pubs/pub27897/) 提供了类似于文件系统的接口,其中,用户可以创建目录和文件,或是节点,有父子节点概念的那种。[etcd3](https://coreos.com/blog/etcd3-a-new-etcd.html) 有扁平的键值空间,这样它就有能力获取更大范围的键值。 65 | 66 | ### 处理客户端交互 67 | 68 | 一致性内核功能的一个关键需求是,客户端如何与内核进行交互。下面是客户端与一致性内核协同工作的关键方面。 69 | 70 | #### 找到领导者 71 | 72 | 所有的操作都要在领导者上执行,这是至关重要的,因此,客户端程序库需要先找到领导者服务器。要做到这一点,有两种可能的方式。 73 | 74 | * 一致性内核的追随者服务器知道当前的领导者,因此,如果客户端连接追随者,它会返回 领导者的地址。客户端可以直接连接应答中给出的领导者。值得注意的是,客户端尝试连接时,服务器可能正处于领导者选举过程中。在这种情况下,服务器无法返回领导者地址,客户端需要等待片刻,再尝试连接另外的服务器。 75 | 76 | * 服务器实现转发机制,将所有的客户端请求转发给领导者。这样就允许客户端连接任意的服务器。同样,如果服务器处于领导者 选举过程中,客户端需要不断重试,直到领导者选举成功,一个合法的领导者获得确认。 77 | 78 | 类似于 zookeeper 和 etcd 这样的产品都实现了这种方式,它们允许追随者服务器处理只读请求,以免领导者面对大量客户端的只读请求时出现瓶颈。这就降低了客户端基于请求类型去连接领导者或追随者的复杂性。 79 | 80 | 一个找到领导者的简单机制是,尝试连接每一台服务器,尝试发送一个请求,如果服务器不是领导者,它给出的应答就是一个重定向的应答。 81 | 82 | ```java 83 | private void establishConnectionToLeader(List servers) { 84 |     for (InetAddressAndPort server : servers) { 85 |         try { 86 |             SingleSocketChannel socketChannel = new SingleSocketChannel(server, 10); 87 |             logger.info("Trying to connect to " + server); 88 |             RequestOrResponse response = sendConnectRequest(socketChannel); 89 |             if (isRedirectResponse(response)) { 90 |                 redirectToLeader(response); 91 |                 break; 92 |             } else if (isLookingForLeader(response)) { 93 |                 logger.info("Server is looking for leader. Trying next server"); 94 |                 continue; 95 |             } else { //we know the leader 96 |                 logger.info("Found leader. Establishing a new connection."); 97 |                 newPipelinedConnection(server); 98 |                 break; 99 |             } 100 |         } catch (IOException e) { 101 |             logger.info("Unable to connect to " + server); 102 |             //try next server 103 |         } 104 |     } 105 | } 106 | 107 | private boolean isLookingForLeader(RequestOrResponse requestOrResponse) { 108 |     return requestOrResponse.getRequestId() == RequestId.LookingForLeader.getId(); 109 | } 110 | 111 | private void redirectToLeader(RequestOrResponse response) { 112 |     RedirectToLeaderResponse redirectResponse = deserialize(response); 113 |     newPipelinedConnection(redirectResponse.leaderAddress); 114 |     logger.info("Connected to the new leader " 115 |             + redirectResponse.leaderServerId 116 |             + " " + redirectResponse.leaderAddress 117 |             + ". Checking connection"); 118 | } 119 | 120 | private boolean isRedirectResponse(RequestOrResponse requestOrResponse) { 121 |     return requestOrResponse.getRequestId() == RequestId.RedirectToLeader.getId(); 122 | } 123 | ``` 124 | 125 | 仅仅建立 TCP 连接还不够,我们还需要知道服务器能否处理我们的请求。因此,客户端会给服务器发送一个特殊的连接请求,服务器需要响应,它是可以处理请求,还是要重定向到领导者服务器上。 126 | 127 | ```java 128 | private RequestOrResponse sendConnectRequest(SingleSocketChannel socketChannel) throws IOException { 129 | RequestOrResponse request 130 | = new RequestOrResponse(RequestId.ConnectRequest.getId(), "CONNECT", 0); 131 | try { 132 | return socketChannel.blockingSend(request); 133 | } catch (IOException e) { 134 | resetConnectionToLeader(); 135 | throw e; 136 | } 137 | } 138 | ``` 139 | 140 | 如果既有的领导者失效了,同样的技术将用于识别集群中新选出的领导者。 141 | 142 | 一旦连接成功,客户端将同领导者服务器间维持一个[单一 Socket 通道(Single Socket Channel)](single-socket-channel.md)。 143 | 144 | #### 处理重复请求 145 | 146 | 在失效的场景下,客户端可以重新连接新的 领导者,重新发送请求。但是,如果这些请求在失效的领导者之前已经处理过了,这就有可能产生重复。因此,至关重要的一点是,服务器需要有一种机制,忽略重复的请求。[幂等接收者(Idempotent Receiver)](idempotent-receiver.md)模式就是用来实现重复检测的。 147 | 148 | 使用[租约(Lease)](lease.md),可以在一组服务器上协调任务。同样的技术也可以用于实现分组成员信息和失效检测机制。 149 | 150 | [状态监控(State Watch)](state-watch.md),可以在元数据发生改变,或是基于时间的租约到期时,获得通知。 151 | 152 | ## 示例 153 | 154 | 众所周知,Google 使用 [chubby](https://research.google/pubs/pub27897/) 锁服务进行协调和元数据管理。 155 | 156 | [kafka](https://kafka.apache.org/) 使用 [zookeeper](https://zookeeper.apache.org/) 管理元数据,以及做一些类似于为集群选举领导者之类的决策。Kafka 中[提议的一个架构调整](https://cwiki.apache.org/confluence/display/KAFKA/KIP-500%3A+Replace+ZooKeeper+with+a+Self-Managed+Metadata+Quorum)是在将来使用自己基于 [raft](https://raft.github.io/) 的控制器集群替换 Zookeeper。 157 | 158 | [bookkeeper](https://bookkeeper.apache.org/) 使用 Zookeeper 管理集群的元数据。 159 | 160 | [kubernetes](https://kubernetes.io/) 使用 [etcd](https://etcd.io/) 进行协调、管理集群的元数据和分组成员信息。 161 | 162 | 所有的大数据存储和处理系统类似于 [hdfs](https://hadoop.apache.org/docs/r3.0.0/hadoop-project-dist/hadoop-hdfs/HDFSHighAvailabilityWithNFS.html)、[spark](http://spark.apache.org/docs/latest/spark-standalone.html#standby-masters-with-zookeeper)、[flink](https://ci.apache.org/projects/flink/flink-docs-release-1.11/ops/jobmanager_high_availability.html) 都使用 [zookeeper](https://zookeeper.apache.org/) 实现高可用以及集群协调。 163 | 164 | -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Patterns of Distributed Systems/generation-clock.md: -------------------------------------------------------------------------------- 1 | # 世代时钟(Generation Clock) 2 | 3 | **原文** 4 | 5 | https://martinfowler.com/articles/patterns-of-distributed-systems/generation.html 6 | 7 | 一个单调递增的数字,表示服务器的世代。 8 | 9 | **2020.8.20** 10 | 11 | 又称:Term、Epoch 或世代(Generation) 12 | 13 | ## 问题 14 | 15 | 在[领导者和追随者(Leader and Followers)](leader-and-followers.md)的构建过程中,有一种可能性,领导者临时同追随者失联了。可能是因为垃圾回收造成而暂停,也可能是临时的网络中断,这些都会让领导者进程与追随者之间失联。在这种情况下,领导者进程依旧在运行,暂停之后或是网络中断停止之后,它还是会尝试发送复制请求给追随者。这么做是有危险的,因为与此同时,集群余下的部分可能已经选出了一个新的领导者,接收来自客户端的请求。有一点非常重要,集群余下的部分要能检测出有的请求是来自原有的领导者。原有的领导者本身也要能检测出,它是临时从集群中断开了,然后,采用必要的修正动作,交出领导权。 16 | 17 | ## 解决方案 18 | 19 | 维护一个单调递增的数字,表示服务器的世代。每次选出新的领导者,这个世代都应该递增。即便服务器重启,这个世代也应该是可用的,因此,它应该存储在[预写日志(Write-Ahead Log)](write-ahead-log.md)每一个条目里。在[高水位标记(High-Water Mark)](high-water-mark.md)里,我们讨论过,追随者会使用这个信息找出日志中冲突的部分。 20 | 21 | 启动时,服务器要从日志中读取最后一个已知的世代。 22 | 23 | ```java 24 | class ReplicationModule… 25 | this.replicationState = new ReplicationState(config, wal.getLastLogEntryGeneration()); 26 | ``` 27 | 28 | 采用[领导者和追随者(Leader and Followers)](leader-and-followers.md)模式,选举新的领导者选举时,服务器对这个世代的值进行递增。 29 | 30 | ```java 31 | class ReplicationModule… 32 |   private void startLeaderElection() { 33 |       replicationState.setGeneration(replicationState.getGeneration() + 1); 34 |       registerSelfVote(); 35 |       requestVoteFrom(followers); 36 |   } 37 | ``` 38 | 39 | 服务器会把世代当做投票请求的一部分发给其它服务器。在这种方式下,经过了成功的领导者选举之后,所有的服务器都有了相同的世代。一旦选出新的领导者,追随者就会被告知新的世代。 40 | 41 | ```java 42 | follower (class ReplicationModule...) 43 |   private void becomeFollower(int leaderId, Long generation) { 44 |       replicationState.setGeneration(generation); 45 |       replicationState.setLeaderId(leaderId); 46 |       transitionTo(ServerRole.FOLLOWING); 47 |   } 48 | ``` 49 | 50 | 自此之后,领导者会在它发给追随者的每个请求中都包含这个世代信息。它也包含在发给追随者的每个[心跳(HeartBeat)](heartbeat.md)消息里,也包含在复制请求中。 51 | 52 | 领导者也会把世代信息持久化到[预写日志(Write-Ahead Log)](write-ahead-log.md)的每一个条目里。 53 | 54 | ```java 55 | leader (class ReplicationModule...) 56 | Long appendToLocalLog(byte[] data) { 57 | var logEntryId = wal.getLastLogEntryId() + 1; 58 | var logEntry = new WALEntry(logEntryId, data, EntryType.DATA, replicationState.getGeneration()); 59 | return wal.writeEntry(logEntry); 60 | } 61 | ``` 62 | 63 | 按照这种做法,它还会持久化在追随者日志中,作为[领导者和追随者(Leader and Followers)](leader-and-followers.md)复制机制的一部分。 64 | 65 | 如果追随者得到了一个来自已罢免领导的消息,追随者就可以告知其世代过低。追随者会给出一个失败的应答。 66 | 67 | ```java 68 | follower (class ReplicationModule...) 69 |   Long currentGeneration = replicationState.getGeneration(); 70 |   if (currentGeneration > replicationRequest.getGeneration()) { 71 |       return new ReplicationResponse(FAILED, serverId(), currentGeneration, wal.getLastLogEntryId()); 72 |   } 73 | ``` 74 | 75 | 当领导者得到了一个失败的应答,它就会变成追随者,期待与新的领导者建立通信。 76 | 77 | ```java 78 | Old leader (class ReplicationModule...) 79 |   if (!response.isSucceeded()) { 80 |       stepDownIfHigherGenerationResponse(response); 81 |       return; 82 |   } 83 | 84 |   private void stepDownIfHigherGenerationResponse(ReplicationResponse replicationResponse) { 85 |       if (replicationResponse.getGeneration() > replicationState.getGeneration()) { 86 |           becomeFollower(-1, replicationResponse.getGeneration()); 87 |       } 88 |   } 89 | ``` 90 | 91 | 考虑一下下面这个例子。在一个服务器集群里,leader1 是既有的领导者。集群里所有服务器的世代都是 1。leader1 持续发送心跳给追随者。leader1 产生了一次长的垃圾收集暂停,比如说,5 秒。追随者没有得到心跳,超时了,然后选举出新的领导者。新的领导者将世代递增到 2。垃圾收集暂停结束之后,leader1 持续发送请求给其它服务器。追随者和新的领导者现在都是世代 2 了,拒绝了其请求,发送一个失败应答,其中的世代是 2。leader1 处理失败的应答,退下来成为一个追随者,将世代更新成 2。 92 | 93 | ![世代时钟1](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/generation1.png) 94 | ![世代时钟2](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/generation2.png) 95 | 96 |
图1:世代
97 | 98 | ## 示例 99 | 100 | ### Raft 101 | 102 | [Raft](https://raft.github.io/) 使用了 Term 的概念标记领导者世代。 103 | 104 | ### Zab 105 | 106 | 在 [Zookeeper](https://zookeeper.apache.org/doc/r3.4.13/zookeeperInternals.html#sc_atomicBroadcast) 里,每个 epoch 数是作为每个事务 ID 的一部分进行维护的。因此,每个持久化在 Zookeeper 里的事务都有一个世代,通过 epoch 表示。 107 | 108 | ### Cassandra 109 | 110 | 在 [Cassandra](http://cassandra.apache.org/) 里,每个服务器都存储了一个世代数字,每次服务器重启时都会递增。世代信息持久化在系统的键值空间里,也作为 Gossip 消息的一部分传给其它服务器。服务器接收到 Gossip 消息之后,将它知道的世代值与 Gossip 消息的世代值进行比较。如果 Gossip 消息中世代更高,它就知道服务器重启了,然后,丢弃它维护的关于这个服务器的所有状态,请求新的状态。 111 | 112 | ### Kafka 中的 Epoch 113 | 114 | [Kafka](https://kafka.apache.org/) 每次为集群选出新的控制器,都会创建一个 epoch 数,将其存在 Zookeeper 里。epoch 会包含在集群里从控制器发到其它服务器的每个请求中。它还维护了另外一个 epoch,称为 [LeaderEpoch](https://cwiki.apache.org/confluence/display/KAFKA/KIP-101+-+Alter+Replication+Protocol+to+use+Leader+Epoch+rather+than+High+Watermark+for+Truncation),以便了解一个分区的追随者是否落后于其[高水位标记(High-Water Mark)](high-water-mark.md)。 115 | -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Patterns of Distributed Systems/heartbeat.md: -------------------------------------------------------------------------------- 1 | # 心跳(HeartBeat) 2 | 3 | **原文** 4 | 5 | https://martinfowler.com/articles/patterns-of-distributed-systems/heartbeat.html 6 | 7 | 通过周期性地发送消息给所有其它服务器,表明一个服务器处于可用状态。 8 | 9 | **2020.8.20** 10 | 11 | ## 问题 12 | 13 | 如果集群里有多个服务器,根据所用的分区和复制的模式,各个服务器都要负责存储一部分数据。及时检测出服务器的失败是很重要的,这样可以确保采用一些修正的行动,让其它服务器负责处理失败服务器对应数据的请求。 14 | 15 | ## 解决方案 16 | 17 | ![心跳](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/Heartbeat.png) 18 | 19 |
图1:心跳
20 | 21 | 一个服务器周期性地发送请求给所有其它的服务器,以此表明它依然活跃。选择的请求间隔应该大于服务器间的网络往返的时间。所有的服务器在检查心跳时,都要等待一个超时间隔,超时间隔应该是多个请求间隔。通常来说, 22 | 23 | 超时间隔 > 请求间隔 > 服务器间的网络往返时间 24 | 25 | 比如,如果服务器间的网络往返时间是 20ms,心跳可以每 100ms 发送一次,服务器检查在 1s 之后执行,这样就给了多个心跳足够的时间,不会产生漏报。如果在这个间隔里没收到心跳,就可以说发送服务器已经失效了。 26 | 27 | 无论是发送心跳的服务器,还是接收心跳的服务器,都有一个调度器,定义如下。调度器会接受一个方法,以固定的间隔执行。启动时,任务就会开始调度,执行给定的方法。 28 | 29 | ```java 30 | class HeartBeatScheduler… 31 |   public class HeartBeatScheduler implements Logging { 32 |       private ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1); 33 |    34 |       private Runnable action; 35 |       private Long heartBeatInterval; 36 | 37 |       public HeartBeatScheduler(Runnable action, Long heartBeatIntervalMs) { 38 |           this.action = action; 39 |           this.heartBeatInterval = heartBeatIntervalMs; 40 |       } 41 |    42 |       private ScheduledFuture scheduledTask; 43 | 44 |       public void start() { 45 |           scheduledTask = executor.scheduleWithFixedDelay(new HeartBeatTask(action), heartBeatInterval, heartBeatInterval, TimeUnit.MILLISECONDS); 46 |       } 47 | ``` 48 | 49 | 在发送端的服务器,调度器会执行方法,发送心跳消息。 50 | 51 | ```java 52 | class SendingServer… 53 |   private void sendHeartbeat() throws IOException { 54 |       socketChannel.blockingSend(newHeartbeatRequest(serverId)); 55 |   } 56 | ``` 57 | 58 | 在接收端的服务器,失效检测机制要启动一个类似的调度器。在固定的时间间隔,检查心跳是否收到。 59 | 60 | ```java 61 | class AbstractFailureDetector… 62 | private HeartBeatScheduler heartbeatScheduler = new HeartBeatScheduler(this::heartBeatCheck, 100l); 63 | 64 | abstract void heartBeatCheck(); 65 | abstract void heartBeatReceived(T serverId); 66 | ``` 67 | 68 | 失效检测器需要有两个方法: 69 | 70 | - 接收服务器接收到心跳调用的方法,告诉失效检测器,心跳收到了。 71 | 72 | ```java 73 | class ReceivingServer… 74 |   private void handleRequest(Message request) { 75 |       RequestOrResponse clientRequest = request.getRequest(); 76 |       if (isHeartbeatRequest(clientRequest)) { 77 |           HeartbeatRequest heartbeatRequest = JsonSerDes.deserialize(clientRequest.getMessageBodyJson(), HeartbeatRequest.class); 78 |           failureDetector.heartBeatReceived(heartbeatRequest.getServerId()); 79 |           sendResponse(request); 80 |       } else { 81 |           //processes other requests 82 |       } 83 |   } 84 | ``` 85 | 86 | - 一个周期性调用的方法,检查心跳状态,检测可能的失效。 87 | 88 | 什么时候将服务器标记为失效,这个实现取决于不同的评判标准。其中是有一些权衡的。总的来说,心跳间隔越小,失效检测得越快,但是,也就更有可能出现失效检测的误报。因此,心跳间隔和心跳丢失的解释是按照集群的需求来的。总的来说,分成下面两大类。 89 | 90 | ### 小集群,比如,像 Raft、Zookeeper 等基于共识的系统 91 | 92 | 在所有的共识实现中,心跳是从领导者服务器发给所有追随者服务器的。每次收到心跳,都要记录心跳到达的时间戳。 93 | 94 | ```java 95 | class TimeoutBasedFailureDetector… 96 |   @Override 97 |   void heartBeatReceived(T serverId) { 98 |       Long currentTime = System.nanoTime(); 99 |       heartbeatReceivedTimes.put(serverId, currentTime); 100 |       markUp(serverId); 101 |   } 102 | ``` 103 | 104 | 如果固定的时间窗口内没有收到心跳,就可以认为领导者崩溃了,需要选出一个新的服务器成为领导者。由于进程或网络缓慢,可能会一些虚报的失效。因此,[世代时钟(Generation Clock)](generation-clock.md)常用来检测过期的领导者。这就给系统提供了更好的可用性,这样很短的时间周期里就能检测出崩溃。对于比较小的集群,这很适用,典型的就是有三五个节点,大多数共识实现比如 Zookeeper 或 Raft 都是这样的。 105 | 106 | ```java 107 | class TimeoutBasedFailureDetector… 108 | @Override 109 | void heartBeatCheck() { 110 | Long now = System.nanoTime(); 111 | Set serverIds = heartbeatReceivedTimes.keySet(); 112 | for (T serverId : serverIds) { 113 | Long lastHeartbeatReceivedTime = heartbeatReceivedTimes.get(serverId); 114 | Long timeSinceLastHeartbeat = now - lastHeartbeatReceivedTime; 115 | if (timeSinceLastHeartbeat >= timeoutNanos) { 116 | markDown(serverId); 117 | } 118 | } 119 | } 120 | ``` 121 | 122 | #### 技术考量 123 | 124 | 采用[单一 Socket 通道(Single Socket Channel)](single-socket-channel.md)在服务器间通信时,有一点需要考虑,就是[队首阻塞(head-of-line-blocking)](https://en.wikipedia.org/wiki/Head-of-line_blocking),这会让心跳消息得不到处理。这样一来,延迟就会非常长,以致于产生虚报,认为发送服务器已经宕机,即便它还在按照固定的间隔发送心跳。使用[请求管道(Request Pipeline)](request-pipeline.md),可以保证服务器在发送心跳之前不必等待之前请求的应答回来。有时,使用[单一更新队列(Singular Update Queue)](singular-update-queue.md),像写磁盘这样的任务,就可能会造成延迟,这可能会延迟定时中断的处理,也会延迟发送心跳。 125 | 126 | 这个问题可以通过在单独的线程中异步发送心跳来解决。类似于 [consul](https://www.consul.io/) 和 [akka](https://akka.io/) 这样的框架都会异步发送心跳。对于接收者服务器同样也是一个问题。接收服务器也要进行磁盘写,检查心跳只能在写完成后才能检查心跳,这就会造成虚报的失效检测。因此接收服务器可以使用[单一更新队列(Singular Update Queue)](singular-update-queue.md),解决心跳检查机制的延迟问题。[raft](https://raft.github.io/) 的参考实现、[log-cabin](https://github.com/logcabin/logcabin) 就是这么做的。 127 | 128 | 有时,一些运行时特定事件,比如垃圾收集,会造成[本地停顿](https://issues.apache.org/jira/browse/CASSANDRA-9183),进而造成心跳处理的延迟。这就需要有一种机制在本地暂停(可能)发生后,检查心跳处理是否发生过。一个简单的机制就是,在一段足够长的时间窗口之后(如,5s),检查是否有心跳。在这种情况下,如果在这个时间窗口内不需要标记为心跳失效,那么就进入到下一个循环。[Cassandra 的实现](https://issues.apache.org/jira/browse/CASSANDRA-9183)就是这种做法的一个很好的示例。 129 | 130 | ### 大集群,基于 Gossip 的协议 131 | 132 | 前面部分描述的心跳机制,并不能扩展到大规模集群,也就是那种有几百到上千台服务器,横跨广域网的集群。在大规模集群中,有两件事要考虑: 133 | 134 | - 每台服务器生成的消息数量要有一个固定的限制。 135 | - 心跳消息消耗的总共的带宽。它不该消耗大量的网络带宽。应该有个几百 K 字节的上限,确保即便有太多的心跳也不会影响到在集群上实际传输的数据。 136 | 137 | 基于这些原因,应该避免所有节点对所有节点的心跳。在这些情况下,通常会使用失效检测器,以及 [Gossip](https://en.wikipedia.org/wiki/Gossip_protocol) 协议,在集群中传播失效信息。在失效的场景下,这些集群会采取一些行动,比如,在节点间搬运数据,因此,集群会倾向于进行正确性的检测,容忍更多的延迟(虽然是有界的)。这里的主要挑战在于,不要因为网络的延迟或进程的缓慢,造成对于节点失效的虚报。那么,一种常用的机制是,给每个进程分配一个怀疑计数,在限定的时间内,如果没有收到该进程的 Gossip 消息,则怀疑计数递增。它可以根据过去的统计信息计算出来,只有在这个怀疑计数到达配置的上限时,才将其标记为失效。 138 | 139 | 有两种主流的实现:1)Phi Accrual 的失效检测器(用于 Akka、Cassandra),2)带 Lifeguard 增强的 SWIM(用于 Hashicop Consul、memberlist)。这种实现可以在有数千台机器的广域网上扩展。据说 Akka 尝试过 [2400](https://www.lightbend.com/blog/running-a-2400-akka-nodes-cluster-on-google-compute-engine) 台服务器。Hashicorp Consul 在一个群组内常规部署了几千台 consul 服务器。有一个可靠的失效检测器,可以有效地用于大规模集群部署,同时,又能提供一些一致性保证,这仍然是一个积极发展中的领域。最近在一些框架的研究看上去非常有希望,比如 [Rapid](https://www.usenix.org/conference/atc18/presentation/suresh)。 140 | 141 | ## 示例 142 | 143 | - 共识实现,诸如 ZAB 或 RAFT,可以在三五个节点的集群中很好的运行,实现了基于固定时间窗口的失效检测。 144 | - Akka Actor 和 Cassandra 采用 [Phi Accrual 的失效检测器](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.80.7427&rep=rep1&type=pdf)。 145 | - Hashicorp consul 采用了基于 Gossip 的失效检测器 [SWIM](https://www.cs.cornell.edu/projects/Quicksilver/public_pdfs/SWIM.pdf)。 146 | -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Patterns of Distributed Systems/high-water-mark.md: -------------------------------------------------------------------------------- 1 | # 高水位标记(High-Water Mark) 2 | 3 | **原文** 4 | 5 | https://martinfowler.com/articles/patterns-of-distributed-systems/high-watermark.html 6 | 7 | 预写日志中的索引,表示最后一次成功的复制。 8 | 9 | **2020.8.5** 10 | 11 | 又称:提交索引 12 | 13 | ## 问题 14 | 15 | [预写日志(Write-Ahead Log)](write-ahead-log.md)模式用于在服务器奔溃重启之后恢复状态。但在服务器失效的情况下,想要保障可用性,仅有预写日志是不够的。如果单个服务器失效了,只有等到服务器重启之后,客户端才能够继续使用其功能。为了得到一个更可用的系统,我们需要将日志复制到多台服务器上。使用[领导者和追随者(Leader and Followers)](leader-and-followers.md)时,领导者会将所有的日志条目都复制到追随者的 [Quorum](quorum.md) 上。如果领导者失效了,集群会选出一个新的领导者,客户端在大部分情况下还是能像从前一样继续在集群中工作。但是,还有几件事可能会有问题: 16 | 17 | - 在向任意的追随者发送日志条目之前,领导者失效了。 18 | - 给一部分追随者发送日志条目之后,领导者失效了,日志条目没有发送给大部分的追随者。 19 | 20 | 在这些错误的场景下,一部分追随者的日志中可能会缺失一些条目,一部分追随者则拥有比其它部分多的日志条目。因此,对于每个追随者来说,有一点变得很重要,了解日志中哪个部分是安全的,对客户端是可用的。 21 | 22 | ## 解决方案 23 | 24 | 高水位标记就是一个日志文件中的索引,记录了在追随者的 [Quorum](quorum.md) 中都成功复制的最后一个日志条目。在复制期间,领导者也会把高水位标记传给追随者。对于集群中的所有服务器而言,只有反映的更新小于高水位标记的数据才能传输给客户端。 25 | 26 | 下面是这个操作的序列图。 27 | 28 | ![高水位标记](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/highwatermark-sequence.png) 29 | 30 |
图1:高水位标记
31 | 32 | 对于每个日志条目而言,领导者将其追加到本地的预写日志中,然后,发送给所有的追随者。 33 | 34 | ```java 35 | leader (class ReplicationModule...) 36 |   private Long appendAndReplicate(byte[] data) { 37 |       Long lastLogEntryIndex = appendToLocalLog(data); 38 |       logger.info("Replicating log entries till index " + lastLogEntryIndex + " on followers"); 39 |       replicateOnFollowers(lastLogEntryIndex); 40 |       return lastLogEntryIndex; 41 |   } 42 | 43 |   private void replicateOnFollowers(Long entryAtIndex) { 44 |       for (final FollowerHandler follower : followers) { 45 |           replicateOn(follower, entryAtIndex); //send replication requests to followers 46 |       } 47 |   } 48 | ``` 49 | 50 | 追随者会处理复制请求,将日志条目追加到本地日志中。在成功地追加日志条目之后,它们会把最新的日志条目索引回给领导者。应答中还包括服务器当前的[时代时钟(Generation Clock)](generation-clock.md)。 51 | 52 | ```java 53 | follower (class ReplicationModule...) 54 |   private ReplicationResponse handleReplicationRequest(ReplicationRequest replicationRequest) { 55 |       List entries = replicationRequest.getEntries(); 56 |       for (WALEntry entry : entries) { 57 |           logger.info("Appending log entry " + entry.getEntryId() + " in " + serverId()); 58 |           if (wal.exists(entry)) { 59 |               logger.info("Entry " + wal.readAt(entry.getEntryId()) + " already exists on " + config.getServerId()); 60 |               continue; 61 |           } 62 |           wal.writeEntry(entry); 63 |       } 64 |       return new ReplicationResponse(SUCCEEDED, serverId(), replicationState.getGeneration(), wal.getLastLogEntryId()); 65 |   } 66 | ``` 67 | 68 | 领导者在收到应答时,会追踪每台服务器上已复制日志的索引。 69 | 70 | ```java 71 | class ReplicationModule… 72 |   recordReplicationConfirmedFor(response.getServerId(), response.getReplicatedLogIndex()); 73 |   long logIndexAtQuorum = computeHighwaterMark(logIndexesAtAllServers(), config.numberOfServers()); 74 |   logger.info("logIndexAtQuorum in " + config.getServerId() + " is " + logIndexAtQuorum + " highWaterMark is " + replicationState.getHighWaterMark()); 75 |   var currentHighWaterMark = replicationState.getHighWaterMark(); 76 |   if (logIndexAtQuorum > currentHighWaterMark) { 77 |       applyLogAt(currentHighWaterMark, logIndexAtQuorum); 78 |       logger.info("Setting highwatermark in " + config.getServerId() + " to " + logIndexAtQuorum); 79 |       replicationState.setHighWaterMark(logIndexAtQuorum); 80 |   } else { 81 |       logger.info("HighWaterMark in " + config.getServerId() + " is " + replicationState.getHighWaterMark() + " >= " + logIndexAtQuorum); 82 |   } 83 | ``` 84 | 85 | 通过查看所有追随者的日志索引和领导者自身的日志,高水位标记是可以计算出来的,选取大多数服务器中可用的索引即可。 86 | 87 | ```java 88 | class ReplicationModule… 89 |   Long computeHighwaterMark(List serverLogIndexes, int noOfServers) { 90 |       serverLogIndexes.sort(Long::compareTo); 91 |       return serverLogIndexes.get(noOfServers / 2); 92 |   } 93 | ``` 94 | 95 | 领导者会将高水位标记传播给追随者,可能是当做常规心跳的一部分,也可能一个单独的请求。追随者随后据此设置自己的高水位标记。 96 | 97 | 客户端只能读取到高水位标记前的日志条目。超出高水位标记的对客户端是不可见的。因为这些条目是否复制还未确认,如果领导者失效了,其它服务器成了领导者,这些条目就是不可用的。 98 | 99 | ```java 100 | class ReplicationModule… 101 | public WALEntry readEntry(long index) { 102 | if (index > replicationState.getHighWaterMark()) { 103 | throw new IllegalArgumentException("Log entry not available"); 104 | } 105 | return wal.readAt(index); 106 | } 107 | ``` 108 | 109 | ### 日志截断 110 | 111 | 一台服务器在崩溃/重启之后,重新加入集群,日志中总有可能出现一些冲突的条目。因此,每当有一台服务器加入集群时,它都会检查集群的领导者,了解日志中哪些条目可能是冲突的。然后,它会做一次日志截断,以便与领导者的条目相匹配,然后用随后的条目更新日志,以确保它的日志与集群的节点相匹配。 112 | 113 | 考虑下面这个例子。客户端发送请求在日志中添加四个条目。领导者成功地复制了三个条目,但在日志中添加了第四项后,失败了。一个新的追随者被选为新的领导者,从客户端接收了更多的项。当失效的领导者再次加入集群时,它的第四项就冲突了。因此,它要把自己的日志截断至第三项,然后,添加第五项,以便于集群的其它节点相匹配。 114 | 115 | ![领导者失效](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/leaderfailure.png) 116 | 117 |
图2:领导者失效
118 | 119 | ![新的领导者](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/newleader.png) 120 | 121 |
图3:新的领导者
122 | 123 | ![日志截断](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/truncation.png) 124 | 125 |
图4:日志截断
126 | 127 | 暂停之后,重新启动或是重新加入集群,服务器都会先去寻找新的领导者。然后,它会显式地查询当前的高水位标记,将日志截断至高水位标记,然后,从领导者那里获取超过高水位标记的所有条目。类似 RAFT 之类的复制算法有一些方式找出冲突项,比如,查看自己日志里的日志条目,对比请求里的日志条目。如果日志条目拥有相同的索引,但[时代时钟(Generation Clock)](generation-clock.md)更低的话,就删除这些条目。 128 | 129 | ```java 130 | class ReplicationModule… 131 |   private void maybeTruncate(ReplicationRequest replicationRequest) throws IOException { 132 |       if (replicationRequest.hasNoEntries() || wal.isEmpty()) { 133 |           return; 134 |       } 135 |       List entries = replicationRequest.getEntries(); 136 |       for (WALEntry entry : entries) { 137 |           if (wal.getLastLogEntryId() >= entry.getEntryId()) { 138 |               if (entry.getGeneration() == wal.readAt(entry.getEntryId()).getGeneration()) { 139 |                   continue; 140 |               } 141 |               wal.truncate(entry.getEntryId()); 142 |           } 143 |       } 144 |   } 145 | ``` 146 | 147 | 要支持日志截断,有一种简单的实现,保存一个日志索引到文件位置的映射。这样,日志就可以按照给定的索引进行截断,如下所示: 148 | 149 | ```java 150 | class WALSegment… 151 | public void truncate(Long logIndex) throws IOException { 152 | var filePosition = entryOffsets.get(logIndex); 153 | if (filePosition == null) throw new IllegalArgumentException("No file position available for logIndex=" + logIndex); 154 | 155 | fileChannel.truncate(filePosition); 156 | readAll(); 157 | } 158 | ``` 159 | 160 | ## 示例 161 | 162 | - 所有共识算法都有高水位标记的概念,以便了解应用状态修改的时机,比如,在 [RAFT](https://raft.github.io/) 共识算法中,高水位标记称为“提交索引”。 163 | - 在 [Kafka 的复制协议](https://www.confluent.io/blog/hands-free-kafka-replication-a-lesson-in-operational-simplicity/)中,维护着一个单独的索引,称为高水位标记。消费者只能看到高水位标记之前的条目。 164 | - [Apache BookKeeper](https://bookkeeper.apache.org/) 有一个概念,叫‘[最后添加确认(last add confirmed)](https://bookkeeper.apache.org/archives/docs/r4.4.0/bookkeeperProtocol.html)’,它表示在 bookie 的 Quorum 上已经成功复制的条目。 165 | -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Patterns of Distributed Systems/idempotent-receiver.md: -------------------------------------------------------------------------------- 1 | # 幂等接收者(Idempotent Receiver) 2 | 3 | **原文** 4 | 5 | https://martinfowler.com/articles/patterns-of-distributed-systems/idempotent-receiver.html 6 | 7 | 识别来自客户端的请求是否唯一,以便在客户端重试时,忽略重复的请求。 8 | 9 | **2021.1.26** 10 | 11 | ## 问题 12 | 13 | 客户端给服务器发请求,可能没有得到应答。客户端不可能知道应答是丢失了,还是服务端在处理请求之前就崩溃了。为了确保请求得到处理,客户端唯有重复发送请求。 14 | 15 | 如果服务器已经处理了请求,然后奔溃了,之后,客户端重试时,服务器端会收到客户端的重复请求。 16 | 17 | ## 解决方案 18 | 19 | 每个客户端都会分配得到一个唯一的 ID,用以对客户端进行识别。发送任何请求之前,客户端需要先向服务器进行注册。 20 | 21 | ```java 22 | class ConsistentCoreClient… 23 |   private void registerWithLeader() { 24 |       RequestOrResponse request 25 |               = new RequestOrResponse(RequestId.RegisterClientRequest.getId(), 26 |               correlationId.incrementAndGet()); 27 | 28 |       //blockingSend will attempt to create a new connection if there is a network error. 29 |       RequestOrResponse response = blockingSend(request); 30 |       RegisterClientResponse registerClientResponse 31 |               = JsonSerDes.deserialize(response.getMessageBodyJson(), 32 |               RegisterClientResponse.class); 33 |       this.clientId = registerClientResponse.getClientId(); 34 |   } 35 | ``` 36 | 37 | 当服务器接收到来自客户端的注册请求,它就给客户端分配一个唯一的 ID,如果服务器是一个[一致性内核(Consistent Core)](content/consistent-core.md),它可以先分配预写日志索引当做客户端标识符。 38 | 39 | ```java 40 | class ReplicatedKVStore… 41 |   private Map clientSessions = new ConcurrentHashMap<>(); 42 | 43 |   private RegisterClientResponse registerClient(WALEntry walEntry) { 44 |       Long clientId = walEntry.getEntryId(); 45 |       //clientId to store client responses. 46 |       clientSessions.put(clientId, new Session(clock.nanoTime())); 47 |       return new RegisterClientResponse(clientId); 48 |   } 49 | ``` 50 | 51 | 服务器会创建一个会话(session),以便为注册客户端的请求存储应答。它还会追踪会话的创建时间,这样,会话不起作用时,就可以把它丢弃了,这会在后面详细讨论。 52 | 53 | ```java 54 | class Session { 55 | long lastAccessTimestamp; 56 | Queue clientResponses = new ArrayDeque<>(); 57 | 58 | public Session(long lastAccessTimestamp) { 59 | this.lastAccessTimestamp = lastAccessTimestamp; 60 | } 61 | 62 | public long getLastAccessTimestamp() { 63 | return lastAccessTimestamp; 64 | } 65 | 66 | public Optional getResponse(int requestNumber) { 67 | return clientResponses.stream(). 68 | filter(r -> requestNumber == r.getRequestNumber()).findFirst(); 69 | } 70 | 71 | private static final int MAX_SAVED_RESPONSES = 5; 72 | 73 | public void addResponse(Response response) { 74 | if (clientResponses.size() == MAX_SAVED_RESPONSES) { 75 | clientResponses.remove(); //remove the oldest request 76 | } 77 | clientResponses.add(response); 78 | } 79 | 80 | public void refresh(long nanoTime) { 81 | this.lastAccessTimestamp = nanoTime; 82 | } 83 | } 84 | ``` 85 | 86 | 对一个一致性内核而言,客户端的注册请求也要作为共识算法的一部分进行复制。如此一来,即便既有的领导者失效了,客户端的注册依然是可用的。对于后续的请求,服务器还要存储发送给客户端的应答。 87 | 88 | >幂等和非幂等请求 89 | >>注意到一些请求的幂等属性是很重要的。比如说,在一个键值存储中,设置键值和值就天然是幂等的。即便同样的键值和值设置了多次,也不会产生什么问题。 90 | >>另一方面,创建[租约(Lease)](lease.md)却并不幂等。如果租约已经创建,再次尝试创建租约 的请求就会失败。这就是问题了。考虑一下这样一个场景。一个客户端发送请求创建租约,服务器成功地创建了租约,然后,崩溃了,或者是,应答发给客户端之前连接断开了。客户端会重新创建连接,重新尝试创建租约;因为服务端已经有了一个指定名称的租约,所以,它会返回错误。因此,客户端就会认为没有这个租约。显然,这并不是我们预期的行为。 91 | >>有了幂等接收者,客户端会用同样的请求号发送租约请求。因为表示“请求已经处理过”的应答已经存在服务器上了,这个应答就可以直接返回。这样一来,如果是客户端在连接断开之前已经成功创建了租约,后续重试相同的请求时,它会得到应有的应答。 92 | 93 | 对于收到的每个非幂等请求(参见边栏),服务端成功执行之后,都会将应答存在客户端会话中。 94 | 95 | ```java 96 | class ReplicatedKVStore… 97 | private Response applyRegisterLeaseCommand(WALEntry walEntry, RegisterLeaseCommand command) { 98 | logger.info("Creating lease with id " + command.getName() 99 | + "with timeout " + command.getTimeout() 100 | + " on server " + getServer().getServerId()); 101 | try { 102 | leaseTracker.addLease(command.getName(), 103 | command.getTimeout()); 104 | Response success = Response.success(walEntry.getEntryId()); 105 | if (command.hasClientId()) { 106 | Session session = clientSessions.get(command.getClientId()); 107 | session.addResponse(success.withRequestNumber(command.getRequestNumber())); 108 | } 109 | return success; 110 | 111 | } catch (DuplicateLeaseException e) { 112 | return Response.error(1, e.getMessage(), walEntry.getEntryId()); 113 | } 114 | } 115 | ``` 116 | 117 | 客户端发送给服务器的每个请求里都包含客户端的标识符。客户端还保持了一个计数器,每个发送给服务器的请求都会分配到一个请求号。 118 | 119 | ```java 120 | class ConsistentCoreClient… 121 |   int nextRequestNumber = 1; 122 | 123 |   public void registerLease(String name, long ttl) { 124 |       RegisterLeaseRequest registerLeaseRequest 125 |               = new RegisterLeaseRequest(clientId, nextRequestNumber, name, ttl); 126 | 127 |       nextRequestNumber++; //increment request number for next request. 128 |       var serializedRequest = serialize(registerLeaseRequest); 129 | 130 |       logger.info("Sending RegisterLeaseRequest for " + name); 131 |       blockingSendWithRetries(serializedRequest); 132 |   } 133 | 134 |   private static final int MAX_RETRIES = 3; 135 | 136 |   private RequestOrResponse blockingSendWithRetries(RequestOrResponse request) { 137 |       for (int i = 0; i <= MAX_RETRIES; i++) { 138 |           try { 139 |               //blockingSend will attempt to create a new connection is there is no connection. 140 |               return blockingSend(request); 141 |           } catch (NetworkException e) { 142 |               resetConnectionToLeader(); 143 |               logger.error("Failed sending request  " + request + ". Try " + i, e); 144 |           } 145 |       } 146 |       throw new NetworkException("Timed out after " + MAX_RETRIES + " retries"); 147 |   } 148 | ``` 149 | 150 | 服务器收到请求时,它会先检查来自同一个客户端给定的请求号是否已经处理过了。如果找到已保存的应答,它就会把相同的应答返回给客户端,而无需重新处理请求。 151 | 152 | ```java 153 | class ReplicatedKVStore… 154 |   private Response applyWalEntry(WALEntry walEntry) { 155 |       Command command = deserialize(walEntry); 156 |       if (command.hasClientId()) { 157 |           Session session = clientSessions.get(command.getClientId()); 158 |           Optional savedResponse = session.getResponse(command.getRequestNumber()); 159 |           if(savedResponse.isPresent()) { 160 |               return savedResponse.get(); 161 |           } //else continue and execute this command. 162 |       } 163 | ``` 164 | 165 | ### 已保存的客户端请求过期处理 166 | 167 | 按客户端存储的请求不可能是永久保存的。有几种方式可以对请求进行过期处理。在 Raft 的[参考实现](https://github.com/logcabin/logcabin)中,客户端会保存一个单独的号码,以便记录成功收到应答的请求号。这个号码稍后会随着每个请求发送给服务器。这样,对于请求号小于这个号码的请求,服务器就可以安全地将其丢弃了。 168 | 169 | 如果客户端能够保证只在接收到上一个请求的应答之后,再发起下一个请求,那么,服务器端一旦接收到来自这个客户端的请求,就可以放心地删除之前所有的请求。使用[请求管道(Request Pipeline)](request-pipeline.md)还会有个问题,可能有在途(in-flight)请求存在,也就是客户端没有收到应答。如果服务器端知道客户端能够接受的在途请求的最大数量,它就可以保留那么多的应答,删除其它的应答。比如说,[kafka](https://kafka.apache.org/) 的 producer 能够接受的最大在途请求数量是 5 个,因此,它最多保存 5 个之前的请求。 170 | 171 | ```java 172 | class Session… 173 | private static final int MAX_SAVED_RESPONSES = 5; 174 | 175 | public void addResponse(Response response) { 176 | if (clientResponses.size() == MAX_SAVED_RESPONSES) { 177 | clientResponses.remove(); //remove the oldest request 178 | } 179 | clientResponses.add(response); 180 | } 181 | ``` 182 | 183 | ### 删除已注册的客户端 184 | 185 | 客户端的会话也不会在服务器上永久保存。一个服务器会对其存储的客户端会话有一个最大保活时间。客户端周期性地发送[心跳(HeartBeat)](heartbeat.md)。如果在保活时间内没有收到心跳,服务器上客户端的状态就会被删除掉。 186 | 187 | 服务器会启动一个定时任务,周期性地检查是否有过期会话,删除已过期的会话。 188 | 189 | ```java 190 | class ReplicatedKVStore… 191 | private long heartBeatIntervalMs = TimeUnit.SECONDS.toMillis(10); 192 | private long sessionTimeoutNanos = TimeUnit.MINUTES.toNanos(5); 193 | 194 | private void startSessionCheckerTask() { 195 | scheduledTask = executor.scheduleWithFixedDelay(()->{ 196 | removeExpiredSession(); 197 | }, heartBeatIntervalMs, heartBeatIntervalMs, TimeUnit.MILLISECONDS); 198 | } 199 | 200 | private void removeExpiredSession() { 201 | long now = System.nanoTime(); 202 | for (Long clientId : clientSessions.keySet()) { 203 | Session session = clientSessions.get(clientId); 204 | long elapsedNanosSinceLastAccess = now - session.getLastAccessTimestamp(); 205 | if (elapsedNanosSinceLastAccess > sessionTimeoutNanos) { 206 | clientSessions.remove(clientId); 207 | } 208 | } 209 | } 210 | ``` 211 | 212 | ## 示例 213 | 214 | [Raft](https://raft.github.io/) 有一个实现了幂等性的参考实现,提供了线性一致性的行为。 215 | 216 | [Kafka](https://kafka.apache.org/) 有一个[幂等 Producer](https://cwiki.apache.org/confluence/display/KAFKA/Idempotent+Producer),允许客户端重试请求,忽略重复的请求。 217 | 218 | [ZooKeeper](https://zookeeper.apache.org/) 有 Session 的概念,还有 zxid,用于客户端恢复。HBase 有一个 [hbase-recoverable-zookeeper](https://docs.cloudera.com/HDPDocuments/HDP2/HDP-2.4.0/bk_hbase_java_api/org/apache/hadoop/hbase/zookeeper/RecoverableZooKeeper.html) 的封装,它实现了遵循 [zookeeper-error-handling](https://cwiki.apache.org/confluence/display/ZOOKEEPER/ErrorHandling) 指导的幂等的行为。 219 | -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Patterns of Distributed Systems/lamport-clock.md: -------------------------------------------------------------------------------- 1 | # Lamport 时钟(Lamport Clock) 2 | 3 | **原文** 4 | 5 | https://martinfowler.com/articles/patterns-of-distributed-systems/lamport-clock.html 6 | 7 | 使用逻辑时间戳作为一个值的版本,以便支持跨服务器的值排序。 8 | 9 | **2021.6.23** 10 | 11 | ## 问题 12 | 13 | 当值要在多个服务器上进行存储时,需要有一种方式知道一个值要在另外一个值之前存储。在这种情况下,不能使用系统时间戳,因为时钟不是单调的,两个服务器的时钟时间不应该进行比较。 14 | 15 | 表示一天中时间的系统时间戳,一般来说是通过晶体振荡器建造的时钟机械测量的。这种机制有一个已知问题,根据晶体震荡的快慢,它可能会偏离一天实际的时间。为了解决这个问题,计算机通常会使用像 NTP 这样的服务,将计算机时钟与互联网上众所周知的时间源进行同步。正因为如此,在一个给定的服务器上连续读取两次系统时间,可能会出现时间倒退的现象。 16 | 17 | 由于服务器之间的时钟漂移没有上限,比较两个不同的服务器的时间戳是不可能的。 18 | 19 | ## 解决方案 20 | 21 | Lamport 时钟维护着一个单独的数字表示时间戳,如下所示: 22 | 23 | ```java 24 | class LamportClock… 25 | 26 | class LamportClock { 27 | int latestTime; 28 | 29 | public LamportClock(int timestamp) { 30 | latestTime = timestamp; 31 | } 32 | ``` 33 | 34 | 每个集群节点都维护着一个 Lamport 时钟的实例。 35 | 36 | ```java 37 | class Server… 38 | 39 | MVCCStore mvccStore; 40 | LamportClock clock; 41 | 42 | public Server(MVCCStore mvccStore) { 43 | this.clock = new LamportClock(1); 44 | this.mvccStore = mvccStore; 45 | } 46 | ``` 47 | 48 | 服务器每当进行任何写操作时,它都应该使用`tick()`方法让 Lamport 时钟前进。 49 | 50 | ```java 51 | class LamportClock… 52 | 53 | public int tick(int requestTime) { 54 | latestTime = Integer.max(latestTime, requestTime); 55 | latestTime++; 56 | return latestTime; 57 | } 58 | ``` 59 | 60 | 如此一来,服务器可以确保写操作的顺序是在这个请求之后,以及客户端发起请求时服务器端已经执行的任何其他动作之后。服务器会返回一个时间戳,用于将值写回给客户端。稍后,请求的客户端会使用这个时间戳向其它的服务器发起进一步的写操作。如此一来,请求的因果链就得到了维持。 61 | 62 | ### 因果性、时间和 Happens-Before 63 | 64 | 在一个系统中,当一个事件 A 发生在事件 B 之前,这其中可能存在因果关系。因果关系意味着,在导致 B 发生的原因中,A 可能扮演了一些角色。这种“A 发生在 B 之前(A happens before B)”的关系是通过在每个事件上附加时间戳达成的。如果 A 发生在 B 之前,附加在 A 的时间戳就会小于附加在 B 上的时间戳。但是,因为我们无法依赖于系统时间,我们需要一些方式确保这种“依赖于附加在事件上的时间戳”的 Happens-Before 关系得到维系。[Leslie Lamport](https://en.wikipedia.org/wiki/Leslie_Lamport) 在其开创性论文[《时间、时钟和事件排序(Time, Clocks and Ordering Of Events)》](https://lamport.azurewebsites.net/pubs/time-clocks.pdf)中提出了一个解决方案,使用逻辑时间戳来跟踪 Happens-Before 的关系。因此,这种使用逻辑时间错追踪因果性的技术就被称为 Lamport 时间戳。 65 | 66 | 值得注意的是,在数据库中,事件是关于存储数据的。因此,Lamport 时间戳会附加到存储的值上。这非常符合有版本的存储机制,这一点我们在[有版本的值(Versioned Value)](versioned-value.md)中讨论过。 67 | 68 | ### 一个样例键值存储 69 | 70 | 考虑一个有多台服务器节点的简单键值存储的例子。它包含两台服务器,蓝色(Blue)和绿色(Green)。每台服务器负责存储一组特定的键值。这是一个典型的场景,数据划分到一组服务器上。值存储为[有版本的值(Versioned Value)](versioned-value.md),其版本号为 Lamport 时间戳。 71 | 72 | ![两台服务器,各自负责特定的键值](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/two-servers-each-with-specific-key-range.png) 73 | 74 |
图1:两台服务器,各自负责特定的键值
75 | 76 | 接收服务器会比较并更新自己的时间戳,然后,用它写入一个有版本的键值和值。 77 | 78 | ```java 79 | class Server… 80 | 81 | public int write(String key, String value, int requestTimestamp) { 82 | //update own clock to reflect causality 83 | int writeAtTimestamp = clock.tick(requestTimestamp); 84 | mvccStore.put(new VersionedKey(key, writeAtTimestamp), value); 85 | return writeAtTimestamp; 86 | } 87 | ``` 88 | 89 | 用于写入值的时间戳会返回给客户端。通过更新自己的时间戳,客户端会跟踪最大的时间戳。它在发出进一步写入请求时会使用这个时间戳。 90 | 91 | ```java 92 | class Client… 93 | 94 | LamportClock clock = new LamportClock(1); 95 | public void write() { 96 | int server1WrittenAt = server1.write("name", "Alice", clock.getLatestTime()); 97 | clock.updateTo(server1WrittenAt); 98 | 99 | int server2WrittenAt = server2.write("title", "Microservices", clock.getLatestTime()); 100 | clock.updateTo(server2WrittenAt); 101 | 102 | assertTrue(server2WrittenAt > server1WrittenAt); 103 | } 104 | ``` 105 | 106 | 请求序列看起来是下面这样: 107 | 108 | ![两台服务器,各自负责特定的键值](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/lamport-clock-request-sequence.png) 109 | 110 |
图2:两台服务器,各自负责特定的键值
111 | 112 | 在[领导者和追随者(Leader and Followers)](leader-and-followers.md)组中,甚至可以用同样的技术在客户端和领导者之间的通信,每组负责一组特定的键值。客户端向该组的领导者发送请求,如上所述。Lamport 时钟的实例由该组的领导者维护,其更新方式与上一节讨论的完全相同。 113 | 114 | ![不同的领导者追随者组存储不同的键值](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/different-keys-different-servers.png) 115 | 116 |
图3:不同的领导者追随者组存储不同的键值
117 | 118 | ### 部分有序 119 | 120 | 使用 Lamport 时钟存储的值只能是[部分有序的](https://en.wikipedia.org/wiki/Partially_ordered_set)。如果两个客户端在两台单独的服务器上存储值,时间戳的值是不能用于跨服务器进行值排序的。在下面这个例子里,Bob 在绿色服务器上存储的标题,其时间戳是 2。但是,这并不能决定 Bob 存储的标题是在 Alice 在蓝色服务器存储名字之前还是之后。 121 | 122 | ![部分有序](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/two-clients-two-separate-servers.png) 123 | 124 |
图4:部分有序
125 | 126 | ### 单一服务器/领导者更新值 127 | 128 | 对一个领导者追随者服务器组而言,领导者总是负责存储值,其基本实现已经在[有版本的值(Versioned Value)](versioned-value.md)中讨论过,它足以维持所需的因果性。 129 | 130 | ![单一领导者追随者组进行键值存储](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/single-servergroup-kvstore.png) 131 | 132 |
图 5:单一领导者追随者组进行键值存储
133 | 134 | 在这种情况下,键值存储会保持一个整数的版本计数器。每次从预写日志中应用了写入命令,版本计数器就要递增。然后,用递增过的版本计数器构建一个新的键值。只有领导者负责递增版本计数器,追随者使用相同的版本号。 135 | 136 | ```java 137 | class ReplicatedKVStore… 138 | 139 | int version = 0; 140 | MVCCStore mvccStore = new MVCCStore(); 141 | 142 | @Override 143 | public CompletableFuture put(String key, String value) { 144 | return server.propose(new SetValueCommand(key, value)); 145 | } 146 | 147 | private Response applySetValueCommand(SetValueCommand setValueCommand) { 148 | getLogger().info("Setting key value " + setValueCommand); 149 | version = version + 1; 150 | mvccStore.put(new VersionedKey(setValueCommand.getKey(), version), setValueCommand.getValue()); 151 | Response response = Response.success(version); 152 | return response; 153 | } 154 | ``` 155 | 156 | ## 示例 157 | 158 | 像 [mongodb](https://www.mongodb.com/) 和 [cockroachdb](https://www.cockroachlabs.com/docs/stable/) 采用了 Lamport 时钟的变体实现了 [mvcc](https://en.wikipedia.org/wiki/Multiversion_concurrency_control) 存储。 159 | 160 | [世代时钟(Generation Clock)](generation-clock.md)是 Lamport 时钟的一个例子。 161 | -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Patterns of Distributed Systems/leader-and-followers.md: -------------------------------------------------------------------------------- 1 | # 领导者和追随者(Leader and Followers) 2 | 3 | **原文** 4 | 5 | https://martinfowler.com/articles/patterns-of-distributed-systems/leader-follower.html 6 | 7 | 有一台服务器协调一组服务器间的复制。 8 | 9 | **2020.8.6** 10 | 11 | ## 问题 12 | 13 | 对于一个管理数据的系统而言,为了在系统内实现容错,需要将数据复制到多台服务器上。 14 | 15 | 有一点也很重要,就是给客户提供一些一致性的保证。当数据在多个服务器上更新时,需要决定何时让客户端看到这些数据。只有写读的 [Quorum](quorum.md) 是不够的,因为一些失效的场景会导致客户端看到不一致的数据。单个的服务器并不知道 Quorum 上其它服务器的数据状态,只有数据是从多台服务器上读取时,才能解决不一致的问题。在某些情况下,这还不够。发送给客户端的数据需要有更强的保证。 16 | 17 | ## 解决方案 18 | 19 | 在集群里选出一台服务器成为领导者。领导者负责根据整个集群的行为作出决策,并将决策传给其它所有的服务器。 20 | 21 | 每台服务器在启动时都会寻找一个既有的领导者。如果没有找到,它会触发领导者选举。只有在领导者选举成功之后,服务器才会接受请求。只有领导者才会处理客户端的请求。如果一个请求发送到一个追随者服务器,追随者会将其转发给领导者服务器。 22 | 23 | ### 领导者选举 24 | 25 | ![选举](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/election.png) 26 | 27 |
图1:选举
28 | 29 | ![投票](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/votes.png) 30 | 31 |
图2:投票
32 | 33 | ![领导者心跳](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/leader-heartbeat.png) 34 | 35 |
图3:领导者心跳
36 | 37 | 对于三五个节点的小集群,比如在实现共识的系统中,领导者选举可以在数据集群内部实现,不依赖于任何外部系统。领袖选举发生在服务器启动时。每台服务器在启动时都会启动领导者选举,尝试选出一个领导者。在选出一个领导者之前,系统不会接收客户端的任何请求。正如在[世代时钟(Generation Clock)](generation-clock.md)模式中所阐释的那样,每次领导者选举都需要更新世代号。服务器总是处于三种状态之一:领导者、追随者或是寻找领导者(或候选者)。 38 | 39 | ```java 40 | public enum ServerRole { 41 | LOOKING_FOR_LEADER, 42 | FOLLOWING, 43 | LEADING; 44 | } 45 | ``` 46 | 47 | [心跳(HeartBeat)](heartbeat.md)机制用以检测既有的领导者是否失效,以便启动新的领导者选举。 48 | 49 | 通过给其它对等的服务器发送消息,启动投票,一次新的选举就开始了。 50 | 51 | ```java 52 | class ReplicationModule… 53 | private void startLeaderElection() { 54 | replicationState.setGeneration(replicationState.getGeneration() + 1); 55 | registerSelfVote(); 56 | requestVoteFrom(followers); 57 | } 58 | ``` 59 | 60 | ### 选举算法 61 | 62 | 选举领导者时,有两个因素要考虑: 63 | 64 | - 因为这个系统主要用于数据复制,哪台服务器可以赢得选举就要做出一些额外的限制。只有“最新”的服务器才能成为合法的领导者。比如说,在典型的基于共识的系统中,“最新”由两件事定义: 65 | - 最新的[世代时钟(Generation Clock)](generation-clock.md) 66 | - [预写日志(Write-Ahead Log)](write-ahead-log.md)的最新日志索引 67 | - 如果所有的服务器都是最新的,领导者可以根据下面的标准来选: 68 | - 一些实现特定的标准,比如,哪个服务器评级为更好或有更高的 ID(比如,Zab) 69 | - 如果要保证注意每台服务器一次只投一票,就看哪台服务器先于其它服务器启动选举。(比如,Raft) 70 | 71 | 在给定的[世代时钟(Generation Clock)](generation-clock.md)内,一旦某台服务器得到投票,在同一个时代内,投票就总是一样的。这就确保了在成功的选举之后,其它服务器再发起同样世代的投票也不会当选。投票请求的处理过程如下: 72 | 73 | ```java 74 | class ReplicationModule… 75 |   VoteResponse handleVoteRequest(VoteRequest voteRequest) { 76 |       VoteTracker voteTracker = replicationState.getVoteTracker(); 77 |       Long requestGeneration = voteRequest.getGeneration(); 78 |       if (replicationState.getGeneration() > requestGeneration) { 79 |           return rejectVote(); 80 | 81 |       } else if (replicationState.getGeneration() < requestGeneration) { 82 |           becomeFollower(-1, requestGeneration); 83 |           voteTracker.registerVote(voteRequest.getServerId()); 84 |           return grantVote(); 85 |       } 86 | 87 |       return handleVoteRequestForSameGeneration(voteRequest); 88 |   } 89 | 90 |   private VoteResponse handleVoteRequestForSameGeneration(VoteRequest voteRequest) { 91 |       Long requestGeneration = voteRequest.getGeneration(); 92 |       VoteTracker voteTracker = replicationState.getVoteTracker(); 93 | 94 |       if (voteTracker.alreadyVoted()) { 95 |           return voteTracker.grantedVoteForSameServer(voteRequest.getServerId()) ? 96 |                   grantVote():rejectVote(); 97 |       } 98 | 99 |       if (voteRequest.getLogIndex() >= (Long) wal.getLastLogEntryId()) { 100 |           becomeFollower(NO_LEADER_ID, requestGeneration); 101 |           voteTracker.registerVote(voteRequest.getServerId()); 102 |           return grantVote(); 103 |       } 104 |       return rejectVote(); 105 |   } 106 | 107 |   private void becomeFollower(int leaderId, Long generation) { 108 |       replicationState.setGeneration(generation); 109 |       replicationState.setLeaderId(leaderId); 110 |       transitionTo(ServerRole.FOLLOWING); 111 |   } 112 | 113 |   private VoteResponse grantVote() { 114 |       return VoteResponse.granted(serverId(), 115 |               replicationState.getGeneration(), 116 |               wal.getLastLogEntryId()); 117 |   } 118 | 119 |   private VoteResponse rejectVote() { 120 |       return VoteResponse.rejected(serverId(), 121 |               replicationState.getGeneration(), 122 |               wal.getLastLogEntryId()); 123 |   } 124 | ``` 125 | 126 | 获得多数服务器投票的服务器将转成领导者状态。大多数的确定是根据 [Quorum](quorum.md) 中所讨论的那样。一旦当选,领导者会持续给所有的追随者发送[心跳(HeartBeat)](heartbeat.md)。如果追随者在特定的时间间隔内没有收到心跳,就会触发新的领导选举。 127 | 128 | # 使用外部[[线性化](https://jepsen.io/consistency/models/linearizable)]的存储进行领导者选举 129 | 130 | 在一个数据集群内运行领导者选举,对小集群来说,效果很好。但对那些有数千个节点的大数据集群来说,使用外部存储会更容易一些,比如 Zookeeper 或 etcd (其内部使用了共识,提供了线性化保证)。这些大规模的集群通常都有一个服务器,标记为主节点或控制器节点,代表整个集群做出所有的决策。实现领导者选举要有三个功能: 131 | 132 | - compareAndSwap 指令,能够原子化地设置一个键值。 133 | - 心跳的实现,如果没有从选举节点收到心跳,将键值做过期处理,以便触发新的选举。 134 | - 通知机制,如果一个键值过期,就通知所有感兴趣的服务器。 135 | 136 | 在选举领导者时,每个服务器都会使用 compareAndSwap 指令尝试在外部存储中创建一个键值,哪个服务器先成功,就当选为领导者。根据所用的外部存储,键值创建后有一小段的存活时间。当选的领导在存活时间之前都会反复更新键值。每台服务器都会监控这个键值,如果键值已经过期,而且没有在设置的存活时间内收到来自既有领导者的更新,服务器会得到通知。比如,[etcd](https://etcd.io/) 允许 compareAndSwap 操作这样做,只在键值之前不存在时设置键值。在 [Zookeeper](https://zookeeper.apache.org/) 里,没有支持显式的 compareAndSwap 这种操作,但可以这样来实现,尝试创建一个节点,如果这个节点已经存在,就抛出一个异常。Zookeeper 也没有存活时间,但它有个临时节点(ephemeral node)的概念。只要服务器同 Zookeeper 间有一个活跃的会话,这个节点就会存在,否则,节点就会删除,每个监控这个节点的人都会得到通知。比如,用 Zookeeper 可以像下面这样选举领导者: 137 | 138 | ```java 139 | class ServerImpl… 140 | public void startup() { 141 | zookeeperClient.subscribeLeaderChangeListener(this); 142 | elect(); 143 | } 144 | 145 | public void elect() { 146 | var leaderId = serverId; 147 | try { 148 | zookeeperClient.tryCreatingLeaderPath(leaderId); 149 | this.currentLeader = serverId; 150 | onBecomingLeader(); 151 | } catch (ZkNodeExistsException e) { 152 | //back off 153 | this.currentLeader = zookeeperClient.getLeaderId(); 154 | } 155 | } 156 | ``` 157 | 158 | 所有其它的服务器都会监控既有领导者的活跃情况。当它检测到既有领导者宕机时,就会触发新的领导者选举。失效检测要使用与领导者选举相同的外部线性化存储。这个外部存储要有一些设施,实现分组成员信息以及失效检测机制。比如,扩展上面基于 Zookeeper 的实现,在 Zookeeper 上配置一个变化监听器,在既有领导者发生改变时,该监听器就会触发。 159 | 160 | ```java 161 | class ZookeeperClient… 162 |   public void subscribeLeaderChangeListener(IZkDataListener listener) { 163 |       zkClient.subscribeDataChanges(LeaderPath, listener); 164 |   } 165 | ``` 166 | 167 | 集群中的每个服务器都会订阅这个变化,当回调触发之后,就会触发一次新选举,方式如上所示。 168 | 169 | ```java 170 | class ServerImpl… 171 |   @Override 172 |   public void handleDataDeleted(String dataPath) throws Exception { 173 |       elect(); 174 |   } 175 | ``` 176 | 177 | ![基于 Zookeeper 的选举](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/zookeeper-leader-election.png) 178 | 179 |
图4:基于 Zookeeper 的选举
180 | 181 | 用同样的方式使用类似于 [etcd](https://etcd.io/) 或 [Consul](https://www.consul.io/) 的系统也可以实现领导者选举。 182 | 183 | ### 为何 Quorum 读/写不足以保证强一致性 184 | 185 | 貌似像 Cassandra 这样的 Dynamo 风格的数据库所提供的 Quorum 读/写,足以在服务器失效的情况下获得强一致性。但事实并非如此。考虑一下下面的例子。假设我们有一个三台服务器的集群。变量 x 存在所有三台服务器上(其复制因子是 3)。启动时,x 的值是 1。 186 | 187 | - 假设 writer1 写入 x=2,复制因子是 3。写的请求发送给所有的三台服务器。server1 写成功了,然而,server2 和 server3 失败了。(可能是小故障,或者只是 writer1 把请求发送给 server1 之后,陷入了长时间的垃圾收集暂停)。 188 | - 客户端 c1 从 server1 和 server2 读取 x 的值。它得到了 x=2 这个最新值,因为 server1 已经有了最新值。 189 | - 客户端 c2 触发去读 x。但是,server1 临时宕机了。因此,c2 要从 server2 和 server 3 去读取,它们拥有的 x 的旧值,x=1。因此,c2 得到的是旧值,即便它们是在 c1 已经得到了最新值之后去读取的。 190 | 191 | 按照这种方式,连续两次的读取,结果是最新的值消失了。一旦 server1 恢复回来,后续的读还会得到最新的值。假设读取修复或是抗熵进程在运行,服务器“最终”还是会得到最新的值。但是,存储集群无法提供任何保证,确保一旦一个特定的值对任何客户端可见之后,所有后续的读取得到都是那个值,即便服务器失效了。 192 | 193 | ## 示例 194 | 195 | - 对于实现共识的系统而言,有一点很重要,就是只有一台服务器协调复制过程的行为。正如[Paxos Made Simple](https://lamport.azurewebsites.net/pubs/paxos-simple.pdf)所指出的,系统的活性很重要。 196 | - 在 [Raft](https://raft.github.io/) 和 [Zab](https://zookeeper.apache.org/doc/r3.4.13/zookeeperInternals.html#sc_atomicBroadcast) 共识算法中,领导者选举是一个显式的阶段,发生在启动时,或是领导者失效时。 197 | - [Viewstamp Replication](http://pmg.csail.mit.edu/papers/vr-revisited.pdf)算法有一个 Primary 概念,类似于其它算法中的领导者。 198 | - [Kafka](https://kafka.apache.org/) 有个 [Controller](https://cwiki.apache.org/confluence/display/KAFKA/Kafka+Controller+Internals),它负责代表集群的其它部分做出所有的决策。它对来自 Zookeeper 的事件做出响应,Kafka 的每个分区都有一个指定的领导者 Broker 以及追随者 Broker。领导者和追随者选举由 Controller Broker 完成。 199 | -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Patterns of Distributed Systems/low-water-mark.md: -------------------------------------------------------------------------------- 1 | # 低水位标记(Low-Water Mark) 2 | 3 | **原文** 4 | 5 | https://martinfowler.com/articles/patterns-of-distributed-systems/low-watermark.html 6 | 7 | 预写日志的一个索引,表示日志中的哪个部分是可以丢弃的。 8 | 9 | **2020.8.18** 10 | 11 | ## 问题 12 | 13 | 预写日志维护着持久化存储的每一次更新。随着时间的推移,它会无限增长。使用[分段日志](segmented-log.md),一次可以处理更小的文件,但如果不检查,磁盘总存储量会无限增长。 14 | 15 | ## 解决方案 16 | 17 | 要有这样一种机制,告诉日志处理部分,哪部分日志可以安全地丢弃了。这种机制要给出最低的偏移或是低水位标记,也就是在这个点之前的日志都可以丢弃了。后台用一个单独的线程执行一个任务,持续检查哪部分日志可以丢弃,然后,从磁盘上删除相应的文件。 18 | 19 | ```java 20 | this.logCleaner = newLogCleaner(config); 21 | this.logCleaner.startup(); 22 | ``` 23 | 24 | 日志清理器可以实现成一个调度任务。 25 | 26 | ```java 27 | public void startup() { 28 |     scheduleLogCleaning(); 29 | } 30 | private void scheduleLogCleaning() { 31 |     singleThreadedExecutor.schedule(() -> { 32 |         cleanLogs(); 33 |     }, config.getCleanTaskIntervalMs(), TimeUnit.MILLISECONDS); 34 | } 35 | ``` 36 | 37 | ### 基于快照的低水位标记 38 | 39 | 大多数共识算法的实现,比如,Zookeeper 或 etcd(如同 RAFT 中所定义的),都实现了快照机制。在这个实现中,存储引擎会周期地打快照。已经成功应用的日志索引也要和快照一起存起来。可以参考[预写日志(Write-Ahead Log)](write-ahead-log.md)模式中的简单键值存储的实现,快照可以像下面这样打: 40 | 41 | ```java 42 | public SnapShot takeSnapshot() { 43 |     Long snapShotTakenAtLogIndex = wal.getLastLogEntryId(); 44 |     return new SnapShot(serializeState(kv), snapShotTakenAtLogIndex); 45 | } 46 | ``` 47 | 48 | 快照一旦持久化到磁盘上,日志管理器就会得到低水位标记,之后,就可以丢弃旧的日志了。 49 | 50 | ```java 51 | List getSegmentsBefore(Long snapshotIndex) { 52 |     List markedForDeletion = new ArrayList<>(); 53 |     List sortedSavedSegments = wal.sortedSavedSegments; 54 |     for (WALSegment sortedSavedSegment : sortedSavedSegments) { 55 |         if (sortedSavedSegment.getLastLogEntryId() < snapshotIndex) { 56 |             markedForDeletion.add(sortedSavedSegment); 57 |         } 58 |     } 59 |     return markedForDeletion; 60 | } 61 | ``` 62 | 63 | ### 基于时间的低水位标记 64 | 65 | 在一些系统中,日志并不是更新系统状态所必需的,在给定的时间窗口后,日志就可以丢弃了,而无需等待其它子系统将可以删除的最低的日志索引共享过来。比如,像 Kafka 这样的系统里,日志维持七周;消息大于七周的日志段都可以丢弃。就这个实现而言,日志条目也包含了其创建的时间戳。这样,日志清理器只要检查每个日志段的最后一项,如果其在配置的时间窗口之前,这个段就可以丢弃了。 66 | 67 | ```java 68 | private List getSegmentsPast(Long logMaxDurationMs) { 69 |     long now = System.currentTimeMillis(); 70 |     List markedForDeletion = new ArrayList<>(); 71 |     List sortedSavedSegments = wal.sortedSavedSegments; 72 |     for (WALSegment sortedSavedSegment : sortedSavedSegments) { 73 |         if (timeElaspedSince(now, sortedSavedSegment.getLastLogEntryTimestamp()) > logMaxDurationMs) { 74 |             markedForDeletion.add(sortedSavedSegment); 75 |         } 76 |     } 77 |     return markedForDeletion; 78 | } 79 | 80 | private long timeElaspedSince(long now, long lastLogEntryTimestamp) { 81 |     return now - lastLogEntryTimestamp; 82 | } 83 | ``` 84 | 85 | ## 示例 86 | 87 | * 所有共识算法的日志实现,比如 [Zookeeper](https://github.com/apache/zookeeper/blob/master/zookeeper-server/src/main/java/org/apache/zookeeper/server/persistence/FileTxnLog.java) 和 [RAFT](https://github.com/etcd-io/etcd/blob/master/wal/wal.go),都实现基于快照的日志清理。 88 | * [Kafka](https://github.com/axbaretto/kafka/blob/master/core/src/main/scala/kafka/log/Log.scala) 的存储实现遵循着基于时间的日志清理。 89 | -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Patterns of Distributed Systems/overview.md: -------------------------------------------------------------------------------- 1 | # 分布式系统模式 2 | 3 | **原文** 4 | 5 | https://martinfowler.com/articles/patterns-of-distributed-systems/ 6 | 7 | 分布式系统给软件开发带来了一些特殊的挑战,要求数据有多个副本,且彼此间要保持同步。然而,我们不能保证所有工作节点都能可靠地工作,网络延迟会轻易地造成不一致。尽管如此,许多组织依然要依赖一系列核心的分布式软件来处理数据存储、消息通信、系统管理以及计算能力。这些系统面临着共同的问题,可以采用类似的方案解决。本文将这些方案进行分类,并进一步提炼成模式。通过模式,我们可以认识到如何更好的理解、交流和传授分布式系统设计。 8 | 9 | **2022.9.7** 10 | 11 | > Unmesh Joshi 12 | > 13 | > Unmesh Joshi 是 ThoughtWorks 的总监级咨询师。他是一个软件架构的爱好者,相信在今天理解分布式系统的原则,同过去十年里理解 Web 架构或面向对象编程一样至关重要。 14 | 15 | ## 这个系列在讨论什么 16 | 17 | 在过去的几个月里,我在 ThoughtWorks 内部组织了许多分布式系统的工作坊。在组织这些工作坊的过程中,我们面临的一个严峻挑战就是,如何将分布式系统的理论映射到诸如 Kafka 或 Cassandra 这样的开源代码库上,同时,还要保持讨论足够通用,覆盖尽可能广泛的解决方案。模式的概念为此提供了一个不错的出路。 18 | 19 | 从模式的本质上说,其结构让我们可以专注在一个特定的问题上,这就很容易说清楚,为什么需要一个特定的解决方案。解决方案的描述让我们有了一个代码结构,对于展示一个实际的解决方案而言,它足够具体,对于涵盖广泛的变体而言,它又足够通用。模式技术还可以将不同的模式联系在一起,构建出一个完整的系统。由此,便有了一个讨论分布式系统实现非常好的词汇表。 20 | 21 | 下面就是从主流开源分布式系统中观察到的第一组模式。希望这组模式对所有的程序员都有用。 22 | 23 | ### 分布式系统:一个实现的视角 24 | 25 | 今天的企业架构充满了各种天生就分布的平台和框架。如果从今天典型的企业应用架构选取典型平台和框架组成列表,我们可能会得到类似于下面这样一个列表: 26 | 27 | | **平台/框架的类型** | **样例** | 28 | | :------------------ | :----------------------------------------- | 29 | | 数据库 | Cassandra、HBase、Riak | 30 | | 消息队列 | Kafka、Pulsar | 31 | | 基础设施 | Kubernetes、Mesos、Zookeeper、etcd、Consul | 32 | | 内存数据/计算网格 | Hazelcast、Pivotal、Gemfire | 33 | | 有状态微服务 | Akka Actors、Axon | 34 | | 文件系统 | HDFS、Ceph | 35 | 36 | 所有这些天生都是“分布式的”。对一个系统而言,分布式意味着什么呢?它包含两个方面: 37 | 38 | - 这个系统运行在多个服务器上。集群中的服务器数量差异极大,少则两三台,多则数千台。 39 | - 这个系统管理着数据。因此,其本质上是一个“有状态”的系统。 40 | 41 | 当多台服务器参与到存储数据中,总有一些地方会出错。上述所有提及的系统都需要解决这些问题。在这些系统的实现中,解决这些问题时总有一些类似的解决方案。以通用的形式理解这些解决方案,有助于在更大的范围内理解这些系统的实现,也可以当做构建新系统的指导原则。 42 | 43 | 好,进入模式。 44 | 45 | #### 模式 46 | 47 | [模式](https://martinfowler.com/articles/writingPatterns.html),这是 Christopher Alexander 引入的一个概念,现在在软件设计社区得到了广泛地接受,用以记录在构建软件系统所用的各种设计构造。模式提供一种“从问题到解决方案”的结构化方式,它可以在许多地方见到,并且得到了证明。使用模式的一种有意思的方式是,采用模式序列或模式语言的形式,将多个模式联系在一起,这为实现“整个”或完整的系统提供了指导方向。将分布式系统视为一系列模式是一种有价值的做法,可以获得关于其实现更多的洞见。 48 | 49 | ## 问题及可复用的解决方案 50 | 51 | 当数据要存储在多台服务器上时,有很多地方可能会出错。 52 | 53 | ### 进程崩溃 54 | 55 | 进程随时都会崩溃,无论是硬件故障,还是软件故障。进程崩溃的方式有许多种: 56 | 57 | - 系统管理员进行常规维护时,进程可能会挂掉 58 | - 因为磁盘已满,异常未能正确处理,做一些文件 IO 时进程可能被杀掉 59 | - 在云环境中,甚至更加诡异,一些无关因素都会让服务器宕机 60 | 61 | 如果进程负责存储数据,底线是其设计要对存储在服务器上的数据给予一定的持久性(durability)保证。即便进程突然崩溃,所有已经通知用户成功存储的数据也要得以保障。依赖于其存储模式,不同的存储引擎有不同的的存储结构,从简单的哈希表到复杂的图存储。因为将数据写入磁盘是最耗时的过程,可能不是每个对存储的插入或更新都来得及写入到磁盘中。因此,大多数数据库都有内存存储结构,其只完成周期性的磁盘写入。这就增加了“进程突然崩溃丢失所有数据”的风险。 62 | 63 | 有一种叫[预写日志(Write-Ahead Log,简称 WAL)](write-ahead-log.md)的技术就是用来对付这种情况的。服务器存储将每个状态改变当做一个命令记录在硬盘上的一个只追加(append-only)的文件上。在文件上附加是一个非常快的操作,因此,它几乎对性能完全没有影响。这样一个可以顺序追加的日志可以存储每次更新。在服务器启动时,日志可以回放,以重建内存状态。 64 | 65 | 这就保证了持久性。即便服务器突然崩溃再重启,数据也不会丢失。但是,客户端在服务器恢复之前是不能获取或存储数据的。因此,在服务器失效时,我们缺乏了可用性。 66 | 67 | 一个显而易见的解决方案是将数据存储在多台服务器上。因此,我们可以将 WAL 复制到多台服务器上。 68 | 69 | 一旦有了多台服务器,就需要考虑更多的失效场景了。 70 | 71 | ### 网络延迟 72 | 73 | 在 TCP/IP 协议栈中,跨网络传输消息的延迟并没有一个上限。由于网络负载,这个值差异会很大。比如,由于触发了一个大数据的任务,一个 1Gbps 的网络也可能会被吞噬,网络缓冲被填满,一些消息到达服务器的延迟可能会变得任意长。 74 | 75 | 在一个典型的数据中心里,服务器并排放在机架上,多个机架通过顶端的机架交换机连接在一起。可能还会有一棵交换机树,将数据中心的一部分同另外的部分连接在一起。在某些情况下,一组服务器可以彼此通信,却与另一组服务器断开连接。这种情况称为网络分区。服务器通过网络进行通信时,一个基本的问题就是,知道一个特定的服务器何时失效。 76 | 77 | 这里有两个问题要解决。 78 | 79 | - 一个特定的服务器不能为了知晓其它服务器是否已崩溃而无限期地等待。 80 | - 不应该出现两组服务器,彼此都认为对方已经失效,因此,继续为不同的客户端提供服务,这种情况称为脑裂。 81 | 82 | 要解决第一个问题,每个服务器都要以固定的间隔像其它服务器发送一个[心跳(HeartBeat)](heartbeat.md)消息。如果心跳丢失,那台服务器就会被认为是崩溃了。心跳间隔要足够小,确保检测到服务器失效并不需要花太长的时间。正如我们下面将要看到的那样,在最糟糕的情况下,服务器可能已经重新启动,集群作为一个整体依然认为这台服务器还在失效中,这样才能确保提供给客户端的服务不会就此中断。 83 | 84 | 第二个问题是脑裂。一旦产生脑裂,两组服务器就会独立接受更新,不同的客户端就会读写不同的数据,脑裂即便解决了,这些冲突也不可能自动得到解决。 85 | 86 | 要解决脑裂问题,必须确保两组失联的服务器不能独立地前进。为了确保这一点,服务器采取的每个动作,只有经过大多数服务器的确认之后,才能认为是成功的。如果服务器无法获得多数确认,就不能提供必要的服务。某些客户端可能无法获得服务,但服务器集群总能保持一致的状态。占多数的服务器的数量称为 [Quorum](quorum.md)。如何确定 Quorum 呢?这取决于集群所容忍的失效数量。如果有一个 5 个节点的集群,Quorum 就应该是 3。总的来说,如果想容忍 f 个失效,集群的规模就应该是 2f + 1。 87 | 88 | Quorum 保证了我们拥有足够的数据副本,以拯救一些服务器的失效。但这不足以给客户端以强一致性保证。比如,一个客户在 Quorum 上发起了一个写操作,但该操作只在一台服务器上成功了。Quorum 上其它服务器依旧是原有的值。当客户端从这个 Quorum 上读取数据时,如果有最新值的服务器可用,它得到的就可能是最新的值。但是,如果客户端开始读取这个值时,有最新值的服务器不可用,它得到的就可能是一个原有的值了。为了避免这种情况,就需要有人追踪是否 Quorum 对于特定的操作达成了一致,只有那些在所有服务器上都可用的值才会发送给客户端。在这种场景下,会使用[领导者和追随者(Leader and Followers)](leader-and-followers.md)。领导者控制和协调在追随者上的复制。由领导者决定什么样的变化对于客户端是可见的。[高水位标记(High Water Mark)](high-water-mark.md)用于追踪在 WAL 上的项是否已经成功复制到 Quorum 的追随者上。所有达到高水位标记的条目就会对客户端可见。领导者还会将高水位标记传播给追随者。因此,当领导者出现失效时,某个追随者就会成为新的领导者,所以,从客户端的角度看,是不会出现不一致的。 89 | 90 | ### 进程暂停 91 | 92 | 但这并非全部,即便有了 Quorum、领导者以及追随者,还有一个诡异的问题需要解决。领导者进程可能会随意地暂停。进程的暂停原因有很多。对于支持垃圾回收的语言来说,可能存在长时间的垃圾回收暂停。如果领导者有一次长时间的垃圾回收暂停,追随者就可能会失联,领导者在暂停结束之后,会不断地给追随者发消息。与此同时,因为追随者无法收到来自领导者的心跳,它们可能会重新选出一个领导者,以便接收来自客户端的更新。如果原有领导者的请求还是正常处理,它们可能会改写掉一些更新。因此,需要有一个机制检测请求是否是来自过期的领导者。[世代时钟(Generation Clock)](generation-clock.md)就是用于标记和检测请求是否来自原有领导者的一种方式。世代,就是一个单调递增的数字。 93 | 94 | ### 不同步的时钟和定序问题 95 | 96 | 检测消息是来自原有的领导者还是新的领导者,这个问题是一个维护消息顺序的问题。一种显而易见的解决方案是,采用系统时间戳为一组消息定序,但是,我们不能这么做。主要原因在于,跨服务器使用系统时钟是无法保证同步的。计算机时钟的时间是由石英晶体管理,并依据晶体震荡来测量时间。 97 | 98 | 这种机制非常容易出错,因为晶体震荡可能会快,也可能会慢,因此,不同的服务器会产生不同的时间。跨服务器同步时钟,可以使用一种称为 NTP 的服务。这个服务周期性地去查看一组全局的时间服务器,然后,据此调整计算时钟。 99 | 100 | 因为这种情况出现在网络通信上,正如我们前面讨论过的那样,网络延迟可能会有很大差异,由于网络原因,时钟同步可能会造成延迟。这就会造成服务器时钟彼此偏移,经过 NTP 同步之后,甚至会出现时间倒退。因为这些计算机时钟存在的问题,时间通常是不能用于对事件定序。取而代之的是,可以使用一种简单的技术,称为 Lamport 时间戳。[世代时钟](generation-clock.md)就是其中一种。 101 | 102 | ## 综合运用——一个分布式系统示例 103 | 104 | 理解这些模式有什么用呢?接下来,我们就从头构建一个完整的系统,看看这些理解会怎样帮助我们。下面我们以一个共识系统为例。 105 | 106 | ### 容错的共识 107 | 108 | 分布式共识,是分布式系统实现的一个特例,它给予了我们强一致性的保证。在常见的企业级系统中,这方面的典型例子是,[Zookeeper](https://zookeeper.apache.org/)、[etcd](https://etcd.io/) 和 [Consul](https://www.consul.io/)。它们实现了诸如 [zab](https://zookeeper.apache.org/doc/r3.4.13/zookeeperInternals.html#sc_atomicBroadcast) 和 [Raft](https://raft.github.io/) 之类的共识算法,提供了复制和强一致性。还有其它一些流行的算法实现共识机制,比如[Paxos](),[Google Chubby](https://research.google/pubs/pub27897/) 把这种算法用在了锁服务、视图戳复制和[虚拟同步(virtual-synchrony)](https://www.cs.cornell.edu/ken/History.pdf)上。简单来说,共识就是指,一组服务器就存储数据达成一致,以决定哪个数据要存储起来,什么时候数据对于客户端可见。 109 | 110 | ### 实现共识的模式序列 111 | 112 | 共识实现使用[状态机复制(state machine replication)](https://en.wikipedia.org/wiki/State_machine_replication),以达到对于失效的容忍。在状态机复制的过程中,类似于键值存储这样的存储服务是在所有服务器上进行复制,用户输入是在每个服务器上以相同的顺序执行。做到这一点,一个关键的实现技术就是在所有服务器上复制[预写日志(Write-Ahead Log)](write-ahead-log.md),这样就有了可复制的 WAL。 113 | 114 | 我们按照下面的方式可以将模式放在一起去实现可复制的 WAL。 115 | 116 | 为了提供持久性的保证,要使用[预写日志(Write-Ahead Log)](write-ahead-log.md)。使用[分段日志(Segmented Log)](segmented-log.md)可以将预写日志分成多个段。这么有助于实现日志的清理,通常这会采用[低水位标记(Low-Water Mark)](low-water-mark.md)进行处理。通过将预写日志复制到多个服务器上,失效容忍性就得到了保障。在服务器间复制由[领导者和追随者(Leader and Followers)](leader-and-followers.md)保障。[Quorum](quorum.md) 用于更新[高水位标记(High Water Mark)](high-water-mark.md),以决定哪些值对客户端可见。所有的请求都严格按照顺序进行处理,这可以通过[单一更新队列(Singular Update Queue)](singular-update-queue.md)实现。领导者发送请求给追随者时,使用[单一 Socket 通道(Single Socket Channel)](single-socket-channel.md)就可以保证顺序。要在单一 Socket 通道上优化吞吐和延迟,可以使用[请求管道(Request Pipeline)](request-pipeline.md)。追随者通过接受来自领导者的[心跳(HeartBeat)](heartbeat.md)以确定领导者的可用性。如果领导者因为网络分区的原因,临时在集群中失联,可以使用[世代时钟(Generation Clock)](generation-clock.md)检测出来。如果只由领导者服务所有的请求,它就可能会过载。当客户端是只读的,而且能够容忍读取到陈旧的值,追随者服务器也可以提供服务。[追随者读取(Follower Reads)](follower-reads.md)就允许由追随者服务器对读取请求提供服务。 117 | 118 | ![实现共识的模式序列](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/pattern-sequence-for-implementing-consensus.png) 119 | 120 | ### Kubernetes 或 Kafka 的控制平面 121 | 122 | 像 [Kubernetes](https://kubernetes.io/) 或 [Kafka](https://kafka.apache.org/) 这样产品的架构是围绕着一个强一致的元数据存储构建起来的。我们可以把它理解成一个模式序列。[一致性内核(Consistent Core)](consistent-core.md)用作一个强一致、可容错的元数据存储。[租约(Lease)](lease.md)用于实现集群节点的分组成员和失效检测。当集群节点失效或更新其元数据时,其它集群节点可以通过[状态监控(State Watch)](state-watch.md)获得通知。在网络失效重试的情况下,[一致性内核(Consistent Core)](consistent-core.md)的实现可以用[幂等接收者(Idempotent Receiver)]忽略集群节点发送的重复请求。[一致性内核(Consistent Core)](consistent-core.md)可以采用可复制的 WAL,上一节已经描述过这个模式序列了。 123 | 124 | ![Kubernetes 或 Kafka 的控制平面](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/kubernetes-or-kafka-control-plane.png) 125 | 126 | ### 逻辑时间戳的使用 127 | 128 | 各种类型逻辑时间戳的使用也可以看作是一个模式序列。各种产品可以使用 [Gossip 传播(Gossip Dissemination)](gossip-dissemination.md)或[一致性内核(Consistent Core)](consistent-core.md)处理群集节点的分组成员和失效检测。数据存储使用[有版本的值(Versioned Value)](versioned-value.md)就能够确定哪个值是最新的。如果有单台服务器负责更新值,或是使用了[领导者和追随者(Leader and Followers)](leader-and-followers.md),可以使用 [Lamport 时钟(Lamport Clock)](lamport-clock.md)当做[有版本的值(Versioned Value)](versioned-value.md) 中的版本。当时间戳需要从一天中时间中推导出来时,可以使用[混合时钟(Hybrid Clock)](hybrid-clock.md),替代简单的 Lamport 时钟(Lamport Clock)。如果允许多台服务器处理客户端请求,更新同样的值,可以使用[版本向量(Version Vector)](version-vector.md),这样能够检测出在不同集群节点上的并发写入。 129 | 130 | ![逻辑时间戳的使用](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/logical-timestamp-usage.png) 131 | 132 | 这样,以通用的形式理解问题以及其可复用的解决方案,有助于理解整个系统的构造块。 133 | 134 | ## 下一步 135 | 136 | 分布式系统是一个巨大的话题。这里涵盖的这套模式只是其中的一小部分,它们覆盖了不同的主题,展示了一个模式能够如何帮助我们理解和设计分布式问题。我将持续在添加更多的模式,包括分布式系统中解决的下列主题: 137 | 138 | - 成员分组以及失效检测 139 | - 分区 140 | - 复制与一致性 141 | - 存储 142 | - 处理 143 | -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Patterns of Distributed Systems/quorum.md: -------------------------------------------------------------------------------- 1 | # Quorum 2 | 3 | **原文** 4 | 5 | https://martinfowler.com/articles/patterns-of-distributed-systems/quorum.html 6 | 7 | 每个决策都需要大多数服务器同意,避免两组服务器各自独立做出决策。 8 | 9 | **2020.8.11** 10 | 11 | ## 问题 12 | 13 | 在一个分布式系统中,无论服务器采取任何的行动,都要确保即便在发生崩溃的情况下,行动的结果都能够对客户端可用。要做到这一点,可以将结果复制到其它的服务器上。但是,这就引出了一个问题:需要有多少服务器确认了这次复制之后,原来的服务器才能确信这次更新已经完全识别。如果原来的服务器要等待过多的复制,它的响应速度就会降低——也就减少了活跃度。但如果没有足够的复制,更新可能会丢失掉——安全性失效。在整体系统性能和系统连续性之间取得平衡,这一点至关重要。 14 | 15 | ## 解决方案 16 | 17 | 集群收到一次更新,在集群中的大多数节点确认了这次更新之后,集群才算是确认了这次更新。我们将这个数量称之为 Quorum。因此,如果我们的集群有 5 个节点,我们需要让 Quorum 为 3(对于 n 个节点的集群而言,quorum 是 n/2 + 1)。 18 | 19 | Quorum 的需求表示,可以容忍多少的失效——这就是集群规模减去 Quorum。5 个节点的集群能够容忍其中的 2 个节点失效。总的来说,如果我们想容忍 “f” 个失效,集群的规模应该是 2f + 1。 20 | 21 | 考虑下面两个需要 Quorum 的例子: 22 | 23 | * **更新服务器集群中的数据**。[高水位标记(High-Water Mark)](high-water-mark.md)保证了一点,只有大多数服务器上确保可用的数据才是对客户端可见的。 24 | * **领导者选举**。在[领导者和追随者(Leader and Followers)](leader-and-followers.md)模式中,领导者只有得到大多数服务器投票才会当选。 25 | 26 | ### 确定集群中服务器的数量 27 | 28 | 只有在大部分服务器都在运行时,集群才能发挥其作用。进行数据复制的系统中,有两点需要考虑: 29 | 30 | * 写操作的吞吐 31 | 32 | 每次数据写入集群时,都需要复制到多台服务器上。每新增一台服务器都会增加完成这次写入的开销。数据写的延迟直接正比于形成 Quorum 的服务器数量。正如我们将在下面看到的,如果集群中的服务器数量翻倍,吞吐值将会降低到原有集群的一半。 33 | 34 | * 能够容忍的失效数量 35 | 36 | 能容忍的失效服务器数量取决于集群的规模。但是,向既有集群增加一台服务器并非总能得到更多的容错率:在一个有三台服务器的集群中,增加一台服务器,并不会增加失效容忍度。 37 | 38 | 考虑到这两个因素,大多数实用的基于 Quorum 的系统集群规模通常是三台或五台。五台服务器集群能够容忍两台服务器失效,其可容忍数据写入的吞吐是每秒几千个请求。 39 | 40 | 下面是一个选择服务器数量的例子,根据可容忍的失效数量,以及在吞吐上近似的影响。吞吐一列展示了近似的相对吞吐量,这样就凸显出吞吐量随着服务器数量的增加而降低。这个数字会因系统而异。作为一个例子,读者可以参考在 [Raft 论文](https://web.stanford.edu/~ouster/cgi-bin/papers/OngaroPhD.pdf)和原始的 [Zookeeper](https://www.usenix.org/legacy/event/atc10/tech/full_papers/Hunt.pdf) 论文中公布的实际的吞吐数据。 41 | 42 | | 服务器的数量 | Quorum | 可容忍的失效数量 | 表现的吞吐量 | 43 | | ------------ | ------ | ---------------- | ------------ | 44 | | 1 | 1 | 0 | 100 | 45 | | 2 | 2 | 0 | 85 | 46 | | 3 | 2 | 1 | 82 | 47 | | 4 | 3 | 1 | 57 | 48 | | 5 | 3 | 2 | 48 | 49 | | 6 | 4 | 2 | 41 | 50 | | 7 | 5 | 3 | 36 | 51 | 52 | 53 | 54 | ## 示例 55 | 56 | * 所有的共识实现都是基于 Quorum 的,比如,[Zab](https://zookeeper.apache.org/doc/r3.4.13/zookeeperInternals.html#sc_atomicBroadcast)、[Raft](https://raft.github.io/)、[Paxos](https://en.wikipedia.org/wiki/Paxos_(computer_science))。 57 | * 即便是不使用共识的系统,也会使用 Quorum,确保在失效或网络分区的情况下,最新的更新也至少在一台服务器上是可用的。比如,在像 [Cassandra](http://cassandra.apache.org/) 这样的数据库里,要配置成只在大多数服务器更新记录成功之后,数据库更新才返回成功。 58 | -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Patterns of Distributed Systems/request-pipeline.md: -------------------------------------------------------------------------------- 1 | # 请求管道(Request Pipeline) 2 | 3 | **原文** 4 | 5 | https://martinfowler.com/articles/patterns-of-distributed-systems/request-pipeline.html 6 | 7 | 在连接上发送多个请求,而无需等待之前请求的应答,以此改善延迟。 8 | 9 | **2020.8.20** 10 | 11 | ## 问题 12 | 13 | 在集群里服务器间使用[单一 Socket 通道(Single Socket Channel)](single-socket-channel.md)进行通信,如果一个请求需要等到之前请求对应应答的返回,这种做法可能会导致性能问题。为了达到更好的吞吐和延迟,服务端的请求队列应该充分填满,确保服务器容量得到完全地利用。比如,当服务器端使用了[单一更新队列(Singular Update Queue)](singular-update-queue.md),只要队列未填满,就可以继续接收更多的请求。如果只是一次只发一个请求,大多数服务器容量就毫无必要地浪费了。 14 | 15 | ## 解决方案 16 | 17 | 节点向另外的节点发送请求,无需等待之前请求的应答。只要创建两个单独的线程就可以做到,一个在网络通道上发送请求,一个从网络通道上接受应答。 18 | 19 | ![请求管道](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/single-socket-channel.png) 20 | 21 |
图1:请求管道
22 | 23 | 发送者节点通过 socket 通道发送请求,无需等待应答。 24 | 25 | ```java 26 | class SingleSocketChannel… 27 |   public void sendOneWay(RequestOrResponse request) throws IOException { 28 |       var dataStream = new DataOutputStream(socketOutputStream); 29 |       byte[] messageBytes = serialize(request); 30 |       dataStream.writeInt(messageBytes.length); 31 |       dataStream.write(messageBytes); 32 |   } 33 | ``` 34 | 35 | 启动一个单独的线程用以读取应答。 36 | 37 | ```java 38 | class ResponseThread… 39 |   class ResponseThread extends Thread implements Logging { 40 |       private volatile boolean isRunning = false; 41 |       private SingleSocketChannel socketChannel; 42 | 43 |       public ResponseThread(SingleSocketChannel socketChannel) { 44 |           this.socketChannel = socketChannel; 45 |       } 46 | 47 |       @Override 48 |       public void run() { 49 |           try { 50 |               isRunning = true; 51 |               logger.info("Starting responder thread = " + isRunning); 52 |               while (isRunning) { 53 |                   doWork(); 54 |               } 55 |           } catch (IOException e) { 56 |               e.printStackTrace(); 57 |               getLogger().error(e); //thread exits if stopped or there is IO error 58 |           } 59 |       } 60 | 61 |       public void doWork() throws IOException { 62 |           RequestOrResponse response = socketChannel.read(); 63 |           logger.info("Read Response = " + response); 64 |           processResponse(response); 65 |       } 66 | ``` 67 | 68 | 应答处理器可以理解处理应答,或是将它提交到[单一更新队列里(Singular Update Queue)](https://martinfowler.com/articles/patterns-of-distributed-systems/singular-update-queue.html)。 69 | 70 | 请求管道有两个问题需要处理。 71 | 72 | 如果无需等待应答,请求持续发送,接收请求的节点就可能会不堪重负。有鉴于此,一般会有一个上限,也就是一次可以有多少在途请求。任何一个节点都可以发送最大数量的请求给其它节点。一旦发出且未收到应答的请求数量达到最大值,再发送请求就不能再接收了,发送者就要阻塞住了。限制最大在途请求,一个非常简单的策略就是,用一个阻塞队列来跟踪请求。队列可以用可接受的最大在途请求数量进行初始化。一旦接收到一个请求的应答,就从队列中把它移除,为更多的请求创造空间。在下面的代码中,每个 socket 连接接收的最大请求数量是 5 个。 73 | 74 | ```java 75 | class RequestLimitingPipelinedConnection… 76 |   private final Map> inflightRequests = new ConcurrentHashMap<>(); 77 |   private int maxInflightRequests = 5; 78 | 79 |   public void send(InetAddressAndPort to, RequestOrResponse request) throws InterruptedException { 80 |       ArrayBlockingQueue requestsForAddress = inflightRequests.get(to); 81 |       if (requestsForAddress == null) { 82 |           requestsForAddress = new ArrayBlockingQueue<>(maxInflightRequests); 83 |           inflightRequests.put(to, requestsForAddress); 84 |       } 85 |       requestsForAddress.put(request); 86 | ``` 87 | 88 | 一旦接收到应答,请求就从在途请求中移除。 89 | 90 | ```java 91 | class RequestLimitingPipelinedConnection… 92 |   private void consume(SocketRequestOrResponse response) { 93 |       Integer correlationId = response.getRequest().getCorrelationId(); 94 |       Queue requestsForAddress = inflightRequests.get(response.getAddress()); 95 |       RequestOrResponse first = requestsForAddress.peek(); 96 |       if (correlationId != first.getCorrelationId()) { 97 |           throw new RuntimeException("First response should be for the first request"); 98 |       } 99 |       requestsForAddress.remove(first); 100 |       responseConsumer.accept(response.getRequest()); 101 |   } 102 | ``` 103 | 104 | 处理失败,以及要维护顺序的保证,这些都会让实现变得比较诡异。比如,有两个在途请求。第一个请求失败,然后,重试了,服务器在重试的第一个请求到达服务器之前,已经把第二个请求处理了。服务器需要有一些机制,确保拒绝掉乱序的请求。否则,如果有失败和重试的情况,就会存在消息重排序的风险。比如,[Raft](https://raft.github.io/) 总是发送之前的日志索引,我们会预期,每个日志条目都会有这么个索引。如果之前的日志索引无法匹配,服务器就会拒绝掉这个请求。Kafka 允许 max.in.flight.requests.per.connection 大于 1,还有[幂等的 Producer 实现](https://cwiki.apache.org/confluence/display/KAFKA/Idempotent+Producer),它会给发送到 Broker 的每个消息批次分配一个唯一标识符。Broker 可以检查进来的请求序列号,如果这边的请求已经乱序,则拒绝掉新请求。 105 | 106 | ## 示例 107 | 108 | 所有像[Zab](https://zookeeper.apache.org/doc/r3.4.13/zookeeperInternals.html#sc_atomicBroadcast)和[Raft](https://raft.github.io/)这样的共识算法都支持请求通道。 109 | 110 | [Kafka](https://kafka.apache.org/protocol)鼓励客户端使用请求通道来改善吞吐。 111 | -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Patterns of Distributed Systems/segmented-log.md: -------------------------------------------------------------------------------- 1 | # 分段日志(Segmented Log) 2 | 3 | **原文** 4 | 5 | https://martinfowler.com/articles/patterns-of-distributed-systems/log-segmentation.html 6 | 7 | 将日志分成多个文件,不能只为了操作简单而使用一个大文件。 8 | 9 | **2020.8.13** 10 | 11 | ## 问题 12 | 13 | 单个日志文件会增大,在启动时读取它,会变成性能瓶颈。旧的日志要定期清理,对一个巨大的文件做清理操作是很难实现的。 14 | 15 | ## 解决方案 16 | 17 | 单个日志分成多个段。到达指定规模的上限之后,日志文件会滚动。 18 | 19 | ```java 20 | public Long writeEntry(WALEntry entry) { 21 |     maybeRoll(); 22 |     return openSegment.writeEntry(entry); 23 | } 24 | 25 | private void maybeRoll() { 26 |     if (openSegment. 27 |             size() >= config.getMaxLogSize()) { 28 |         openSegment.flush(); 29 |         sortedSavedSegments.add(openSegment); 30 |         long lastId = openSegment.getLastLogEntryId(); 31 |         openSegment = WALSegment.open(lastId, config.getWalDir()); 32 |     } 33 | } 34 | ``` 35 | 36 | 有了日志分段,还要有一种简单的方式将逻辑日志偏移(或是日志序列号)同日志分段文件做一个映射。实现这一点可以通过下面两种方式: 37 | 38 | * 每个日志分段名都是生成的,可以采用众所周知的前缀加基本偏移(日志序列号)的方式。 39 | * 每个日志序列分成两个部分,文件名和事务偏移量。 40 | 41 | ```java 42 | public static String createFileName(Long startIndex) { 43 |     return logPrefix + "_" + startIndex + logSuffix; 44 | } 45 | 46 | public static Long getBaseOffsetFromFileName(String fileName) { 47 |     String[] nameAndSuffix = fileName.split(logSuffix); 48 |     String[] prefixAndOffset = nameAndSuffix[0].split("_"); 49 |     if (prefixAndOffset[0].equals(logPrefix)) 50 |         return Long.parseLong(prefixAndOffset[1]); 51 | 52 |     return -1l; 53 | } 54 | ``` 55 | 56 | 有了这些信息,读操作要有两步。对于给定的偏移(或是事务 ID),确定日志分段,从后续的日志段中读取所有的日志记录。 57 | 58 | ```java 59 | public List readFrom(Long startIndex) { 60 |     List segments = getAllSegmentsContainingLogGreaterThan(startIndex); 61 |     return readWalEntriesFrom(startIndex, segments); 62 | } 63 | 64 | private List getAllSegmentsContainingLogGreaterThan(Long startIndex) { 65 |     List segments = new ArrayList<>(); 66 |     //Start from the last segment to the first segment with starting offset less than startIndex 67 |     //This will get all the segments which have log entries more than the startIndex 68 |     for (int i = sortedSavedSegments.size() - 1; i >= 0; i--) { 69 |         WALSegment walSegment = sortedSavedSegments.get(i); 70 |         segments.add(walSegment); 71 |         if (walSegment.getBaseOffset() <= startIndex) { 72 |             break; // break for the first segment with baseoffset less than startIndex 73 |         } 74 |     } 75 | 76 |     if (openSegment.getBaseOffset() <= startIndex) { 77 |         segments.add(openSegment); 78 |     } 79 | 80 |     return segments; 81 | } 82 | ``` 83 | 84 | ## 示例 85 | 86 | * 所有的共识实现都使用了日志分段,比如,[Zookeeper](https://github.com/apache/zookeeper/blob/master/zookeeper-server/src/main/java/org/apache/zookeeper/server/persistence/FileTxnLog.java) 和 [RAFT](https://github.com/etcd-io/etcd/blob/master/wal/wal.go)。 87 | * [Kafka](https://github.com/axbaretto/kafka/blob/master/core/src/main/scala/kafka/log/Log.scala) 的存储实现也遵循日志分段。 88 | * 所有的数据库,包括 NoSQL 数据库,类似于 [Cassandra](https://github.com/facebookarchive/cassandra/blob/master/src/org/apache/cassandra/db/CommitLog.java),都使用基于预先配置日志大小的滚动策略。 89 | -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Patterns of Distributed Systems/single-socket-channel.md: -------------------------------------------------------------------------------- 1 | # 单一 Socket 通道(Single Socket Channel) 2 | 3 | **原文** 4 | 5 | https://martinfowler.com/articles/patterns-of-distributed-systems/single-socket-channel.html 6 | 7 | 通过使用单一的 TCP 连接,维护发送给服务器请求的顺序。 8 | 9 | **2020.8.19** 10 | 11 | ## 问题 12 | 13 | 使用[领导者和追随者(Leader and Followers)](leader-and-followers.md)时,我们需要确保在领导者和各个追随者之间的消息保持有序,如果有消息丢失,需要重试机制。我们需要做到这一点,还要保证保持新连接的成本足够低,开启新连接才不会增加系统的延迟。 14 | 15 | ## 解决方案 16 | 17 | 幸运的是,已经长期广泛使用的 [TCP](https://en.wikipedia.org/wiki/Transmission_Control_Protocol) 机制已经提供了所有这些必要的特征。因此,我们只要确保追随者与其领导者之间都是通过单一的 Socket 通道进行通信,就可以进行我们所需的通信。然后,追随者再对来自领导者的更新进行序列化,将其送入[单一更新队列(Singular Update Queue)](singular-update-queue.md)。 18 | 19 | ![单一 Socket 通道](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/single-socket-channel.png) 20 | 21 |
图1:单一 Socket 通道
22 | 23 | 节点一旦打开连接,就不会关闭,持续从中读取新的请求。节点为每个连接准备一个专用的线程去读取写入请求。如果使用的是[非阻塞 IO](),那就不需要为每个连接准备一个线程。 24 | 25 | 下面是一个基于简单线程的实现: 26 | 27 | ```java 28 | class SocketHandlerThread… 29 |   @Override 30 |   public void run() { 31 |       isRunning = true; 32 |       try { 33 |           //Continues to read/write to the socket connection till it is closed. 34 |           while (isRunning) { 35 |               handleRequest(); 36 |           } 37 |       } catch (Exception e) { 38 |           getLogger().debug(e); 39 |           closeClient(this); 40 |       } 41 |   } 42 | 43 |   private void handleRequest() { 44 |       RequestOrResponse request = clientConnection.readRequest(); 45 |       RequestId requestId = RequestId.valueOf(request.getRequestId()); 46 |       server.accept(new Message<>(request, requestId, clientConnection)); 47 |   } 48 | 49 |   public void closeConnection() { 50 |       clientConnection.close(); 51 |   } 52 | ``` 53 | 54 | 节点读取请求,将它们提交到[单一更新队列(Singular Update Queue)](singular-update-queue.md)中等待处理。一旦节点处理了写入的请求,它就将应答写回到 socket。 55 | 56 | 无论节点什么时候需要建立通信,它都会打开单一 Socket 连接,与对方通信的所有请求都会使用这个连接。 57 | 58 | ```java 59 | class SingleSocketChannel… 60 |   public class SingleSocketChannel implements Closeable { 61 |       final InetAddressAndPort address; 62 |       final int heartbeatIntervalMs; 63 |       private Socket clientSocket; 64 |       private final OutputStream socketOutputStream; 65 |       private final InputStream inputStream; 66 |    67 |       public SingleSocketChannel(InetAddressAndPort address, int heartbeatIntervalMs) throws IOException { 68 |           this.address = address; 69 |           this.heartbeatIntervalMs = heartbeatIntervalMs; 70 |           clientSocket = new Socket(); 71 |           clientSocket.connect(new InetSocketAddress(address.getAddress(), address.getPort()), heartbeatIntervalMs); 72 |           clientSocket.setSoTimeout(heartbeatIntervalMs * 10); //set socket read timeout to be more than heartbeat. 73 |           socketOutputStream = clientSocket.getOutputStream(); 74 |           inputStream = clientSocket.getInputStream(); 75 |       } 76 |    77 |       public synchronized RequestOrResponse blockingSend(RequestOrResponse request) throws IOException { 78 |           writeRequest(request); 79 |           byte[] responseBytes = readResponse(); 80 |           return deserialize(responseBytes); 81 |       } 82 |    83 |       private void writeRequest(RequestOrResponse request) throws IOException { 84 |           var dataStream = new DataOutputStream(socketOutputStream); 85 |           byte[] messageBytes = serialize(request); 86 |           dataStream.writeInt(messageBytes.length); 87 |           dataStream.write(messageBytes); 88 |       } 89 | ``` 90 | 91 | 有一点很重要,就是连接要有超时时间,这样就不会在出错的时候,造成永久阻塞了。我们使用[心跳(HeartBeat)](heartbeat.md)周期性地在 Socket 通道上发送请求,以便保活。超时时间通常都是多个心跳的间隔,这样,网络的往返时间以及可能的一些延迟就不会造成问题了。比方说,将连接超时时间设置成心跳间隔的 10 倍也是合理的。 92 | 93 | ```java 94 | class SocketListener… 95 | private void setReadTimeout(Socket clientSocket) throws SocketException { 96 | clientSocket.setSoTimeout(config.getHeartBeatIntervalMs() * 10); 97 | } 98 | ``` 99 | 100 | 通过单一通道发送请求,可能会带来一个问题,也就是[队首阻塞(Head-of-line blocking,HOL)](https://en.wikipedia.org/wiki/Head-of-line_blocking)问题。为了避免这个问题,我们可以使用[请求管道(Request Pipeline)](request-pipeline.md)。 101 | 102 | ## 示例 103 | 104 | [Zookeeper](https://zookeeper.apache.org/doc/r3.4.13/zookeeperInternals.html) 使用了单一 Socket 通道,每个追随者一个线程,处理所有的通信。 105 | 106 | [Kafka](https://kafka.apache.org/protocol) 在追随者和领导者分区之间使用了单一 Socket 通道,进行消息复制。 107 | 108 | [Raft](https://raft.github.io/) 共识算法的参考实现,[LogCabin](https://github.com/logcabin/logcabin) 使用单一 Socket 通道,在 领导者和追随者之间进行通信。 109 | -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/Patterns of Distributed Systems/write-ahead-log.md: -------------------------------------------------------------------------------- 1 | # 预写日志(Write-Ahead Log) 2 | 3 | **原文** 4 | 5 | https://martinfowler.com/articles/patterns-of-distributed-systems/wal.html 6 | 7 | 将每个状态变化以命令形式持久化至只追加的日志中,提供持久化保证,而无需以存储数据结构存储到磁盘上。 8 | 9 | **2020.8.12** 10 | 11 | 也称:提交日志 12 | 13 | ## 问题 14 | 15 | 对服务器而言,即便在机器存储数据失败的情况下,也要保证强持久化,这一点是必需的。一旦服务器同意执行某个动作,即便服务器失效,重启后丢失了所有内存状态,它也应该做到保证执行。 16 | 17 | ## 解决方案 18 | 19 | ![预写日志](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/book/patterns-of-distributed-systems/wal.png) 20 | 21 |
图1:预写日志
22 | 23 | 将每个状态变化以命令的形式存储在磁盘文件中。一个日志由一个服务端进程维护,其只进行顺序追加。一个顺序追加的日志,简化了重启时以及后续在线操作(新命令追加至日志时)的日志处理。每个日志条目都有一个唯一的标识符。唯一的日志标识符有助于在日志中实现某些其它操作,比如[分段日志(Segmented Log)](segmented-log.md),或是以[低水位标记(Low-Water Mark)](low-water-mark.md)清理日志等等。日志的更新可以通过[单一更新队列(Singular Update Queue)](singular-update-queue.md)实现。 24 | 25 | 典型的日志条目结构如下所示: 26 | 27 | ```java 28 | class WALEntry… 29 |   private final Long entryId; 30 |   private final byte[] data; 31 |   private final EntryType entryType; 32 |   private long timeStamp; 33 | ``` 34 | 35 | 每次重启时,可以读取这个文件,然后,重放所有的日志条目就可以恢复状态。 36 | 37 | 考虑下面这个简单的内存键值存储: 38 | 39 | ```java 40 | class KVStore… 41 |   private Map kv = new HashMap<>(); 42 |   public String get(String key) { 43 |       return kv.get(key); 44 |   } 45 |   public void put(String key, String value) { 46 |       appendLog(key, value); 47 |       kv.put(key, value); 48 |   } 49 |   private Long appendLog(String key, String value) { 50 |       return wal.writeEntry(new SetValueCommand(key, value).serialize()); 51 |   } 52 | ``` 53 | 54 | put 操作表示成了命令(Command),在更新内存的哈希表 之前先把它序列化,然后存储到日志里。 55 | 56 | ```java 57 | class SetValueCommand… 58 | final String key; 59 | final String value; 60 | final String attachLease; 61 | 62 | public SetValueCommand(String key, String value) { 63 | this(key, value, ""); 64 | } 65 | 66 | public SetValueCommand(String key, String value, String attachLease) { 67 | this.key = key; 68 | this.value = value; 69 | this.attachLease = attachLease; 70 | } 71 | 72 | @Override 73 | public void serialize(DataOutputStream os) throws IOException { 74 | os.writeInt(Command.SetValueType); 75 | os.writeUTF(key); 76 | os.writeUTF(value); 77 | os.writeUTF(attachLease); 78 | } 79 | 80 | public static SetValueCommand deserialize(InputStream is) { 81 | try { 82 | DataInputStream dataInputStream = new DataInputStream(is); 83 | return new SetValueCommand(dataInputStream.readUTF(), dataInputStream.readUTF(), dataInputStream.readUTF()); 84 | } catch (IOException e) { 85 | throw new RuntimeException(e); 86 | } 87 | } 88 | ``` 89 | 90 | 这就保证了一旦 put 方法返回成功,即便负责 KVStore 的进程崩溃了,启动时,通过读取日志文件也可以将状态恢复回来。 91 | 92 | ```java 93 | class KVStore… 94 |   public KVStore(Config config) { 95 |       this.config = config; 96 |       this.wal = WriteAheadLog.openWAL(config); 97 |       this.applyLog(); 98 |   } 99 | 100 |   public void applyLog() { 101 |       List walEntries = wal.readAll(); 102 |       applyEntries(walEntries); 103 |   } 104 | 105 |   private void applyEntries(List walEntries) { 106 |       for (WALEntry walEntry : walEntries) { 107 |           Command command = deserialize(walEntry); 108 |           if (command instanceof SetValueCommand) { 109 |               SetValueCommand setValueCommand = (SetValueCommand)command; 110 |               kv.put(setValueCommand.key, setValueCommand.value); 111 |           } 112 |       } 113 |   } 114 | 115 |   public void initialiseFromSnapshot(SnapShot snapShot) { 116 |       kv.putAll(snapShot.deserializeState()); 117 |   } 118 | ``` 119 | 120 | ### 实现考量 121 | 122 | 实现日志还有一些重要的考量。有一点很重要,就是确保写入日志文件的日志条目已经实际地持久化到物理介质上。所有程序设计语言的文件处理程序库都提供了一个机制,强制操作系统将文件的变化“刷(flush)”到物理介质上。然而,使用这种机制有一个需要考量的权衡点。 123 | 124 | 将每个日志的写入都刷到磁盘,这种做法是给了我们一种强持久化的保证(这是使用日志的首要目的),但是,这种做法严重限制了性能,可能很快就会变成瓶颈。将刷的动作延迟,或是采用异步处理,性能就可以提高,但服务器崩溃时,日志条目没有刷到磁盘上,就存在丢失日志条目的风险。大多数采用的技术类似于批处理,以此限制刷操作的影响。 125 | 126 | 还有一个考量,就是如果日志文件受损,读取日志的时候,要保证能够检测出来,日志条目一般来说都会采用 CRC 的方式记录,这样,读取文件时可以对其进行校验。 127 | 128 | 单独的日志文件可能会变得难于管理,可能会快速地消耗掉所有的存储。为了处理这个问题,可以采用像[分段日志(Segmented Log)](segmented-log.md)和[低水位标记(Low-Water Mark)](low-water-mark.md)这样的技术。 129 | 130 | 预写日志是只追加的。因为这种行为,在客户端通信失败重试的时候,日志可能会包含重复的条目。应用这些日志条目时,需要确保重复可以忽略。如果最终状态是类似于 HashMap 的东西,也就是对同一个键值的更新是幂等的,那就不需要特殊的机制。否则,就需要一些机制,对于有唯一标识符的每个请求进行标记,检测重复。 131 | 132 | ## 示例 133 | 134 | - 所有类似于 [Zookeeper](https://github.com/apache/zookeeper/blob/master/zookeeper-server/src/main/java/org/apache/zookeeper/server/persistence/FileTxnLog.java) 和 [RAFT](https://github.com/etcd-io/etcd/blob/master/server/wal/wal.go) 的共识算法中的日志实现都类似于预写日志。 135 | - [Kakfa](https://github.com/axbaretto/kafka/blob/master/core/src/main/scala/kafka/log/Log.scala) 的存储实现遵循着类似于数据库提交日志的结构。 136 | - 所有的数据库,包括类似于 Cassandra 这样的 NoSQL 数据库都使用[预写日志技术](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/db/commitlog/CommitLog.java)保证了持久性。 137 | -------------------------------------------------------------------------------- /01~分布式基础/99~参考资料/分布式系统的八大谬误.md: -------------------------------------------------------------------------------- 1 | > https://ably.com/blog/8-fallacies-of-distributed-computing 2 | 3 | # 分布式系统的八大谬误 4 | -------------------------------------------------------------------------------- /01~分布式基础/README.md: -------------------------------------------------------------------------------- 1 | ![分布式系统题图](https://assets.ng-tech.icu/item/20230503195406.png) 2 | 3 | # 分布式基础 4 | 5 | > A distributed system is one in which the failure of a computer you didn’t even know existed can render your own computer unusable. 6 | > -- Leslie Lamport 7 | 8 | 过去数十年间,信息技术的浪潮深刻地改变了这个社会的通信、交流与协作模式,我们熟知的互联网也经历了基于流量点击赢利的单方面信息发布的 Web 1.0 业务模式,转变为由用户主导而生成内容的 Web 2.0 业务模式;在可见的将来随着 3D 相关技术的落地,互联网应用系统所需处理的访问量和数据量必然会再次爆发性增长。计算机系统早就从单机独立工作过渡到以集群的方式存在的多机器协作工作;按照分布式理论的指导构建出庞大复杂的应用服务,没有分布式系统,我们将无法拨打电话,转账或远距离交换信息。 9 | 10 | # 分布式系统定义 11 | 12 | 典型的集中式系统即某个带多个终端的主机,终端仅负责数据的录入和输出,而没有有数据处理能力,并且运算、存储等全部在主机上进行。传统的银行系统、大型企业、科研单位、军队、政府等,存在着大量的这种集中式的系统。集中式系统的最大的特点就是部署结构非常简单,底层一般采用从 IBM、HP 等厂商购买到的昂贵的大型主机。因此无需考虑如何对服务进行多节点的部署,也就不用考虑各节点之间的分布式协作问题。但是,由于采用单机部署。很可能带来系统大而复杂、难于维护、发生单点故障、扩展性差等问题。不过对于许多现代软件系统而言,垂直扩展(通过在具有更多 CPU,RAM 或更快磁盘的更大,速度更快的计算机上运行同一软件进行扩展)是行不通的。更大的机器更昂贵,更难更换,并且可能需要特殊维护。 13 | 14 | 借鉴凯文.凯利 2016 年在《失控》一书中指出的分布式系统具有四个突出特点:即没有强制性的中心控制、次级单位具有自治的特质、次级单位之间彼此高度链接、点对点之间的影响通过网络形成了非线性因果关系,我们可以看出分布式系统具备的定义: 15 | 16 | - 分布式系统中的多台计算机之间在空间位置上可以随意分布,系统中的多台计算机之间没有主、从之分,即没有控制整个系统的主机,也没有受控的从机。 17 | - 系统资源被所有计算机共享。每台计算机的用户不仅可以使用本机的资源,还可以使用本分布式系统中其他计算机的资源,包括 CPU、文件、打印机等。 18 | - 系统中的若干台计算机可以互相协作来完成一个共同的任务,或者说一个程序可以分布在几台计算机上并行地运行。 19 | - 系统中任意两台计算机都可以通过通信来交换信息。 20 | 21 | 分布式系统则是一个在通过网络连接并作为单个逻辑实体运行的多台计算机上运行软件,它的硬件或软件组件分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统。简单来说就是一群独立计算机集合共同对外提供服务,但是对于系统的用户来说,就像是一台计算机在提供服务一样。分布式意味着可以采用更多的普通计算机(相对于昂贵的大型机)组成分布式集群对外提供服务。计算机越多,CPU、内存、存储资源等也就越多,能够处理的并发访问量也就越大。从分布式系统的概念中我们知道,各个主机之间通信和协调主要通过网络进行,所以,分布式系统中的计算机在空间上几乎没有任何限制,这些计算机可能被放在不同的机柜上,也可能被部署在不同的机房中,还可能在不同的城市中,对于大型的网站甚至可能分布在不同的国家和地区。这种分布性能够有效规避单点故障,即单个点发生故障的时候会波及到整个系统或者网络,从而导致整个系统或者网络的瘫痪。 22 | 23 | 分布式系统的大小可能不同,从少数几台到几百台机器,以及从小型手持设备或传感器设备到高性能计算机的参与者特征。分布式系统中最基础的单元就是节点与网络,节点就是能提供单位服务的逻辑计算资源的集合,网络则将节点聚合起来,形成可协同工作的有机系统。传统的节点也就是一台单体的物理机,所有的服务都揉进去包括服务和数据库;随着虚拟化的发展,单台物理机往往可以分成多台虚拟机,实现资源利用的最大化,节点的概念也变成单台虚拟机上面服务;近几年容器技术逐渐成熟后,服务已经彻底容器化,也就是节点只是轻量级的容器服务。典型的案例是,我们熟悉的数据库系统主要在单个节点上运行的时间早已过去,大多数现代数据库系统都将多个节点连接到群集中,以增加存储容量,提高性能并增强可用性。 24 | 25 | 总结而言,与其说一个分布式、去中心化的网络是一个物体,还不如说它是一个过程。 26 | 27 | # 分布式系统应用 28 | 29 | 今天的企业架构中充满了平台和框架,这些平台和框架都是分布式的,分布式系统的常见应用包括了: 30 | 31 | - 分布式应用和服务:将应用和服务进行分层和分割,然后将应用和服务模块进行分布式部署。这样做不仅可以提高并发访问能力、减少数据库连接和资源消耗,还能使不同应用复用共同的服务,使业务易于扩展。 32 | - 分布式静态资源:对网站的静态资源如 JS、CSS、图片等资源进行分布式部署可以减轻应用服务器的负载压力,提高访问速度。 33 | - 分布式文件系统:单台计算机的存储始终有上限,随着网络的出现,多台计算机协作存储文件的方案也相继被提出来。最早的分布式文件系统其实也称为网络文件系统,现代分布式文件系统则出自由 The Google File System 这篇论文奠定了分布式文件系统的基础。几个常用的文件系统譬如 HDFS, FastDFS, CephmooseFS 等。 34 | - 分布式数据库:大型网站常常需要处理海量数据,单台计算机往往无法提供足够的内存空间,可以对这些数据进行分布式存储。传统关系型数据库为了兼顾事务和性能的特性,在分布式方面的发展有限,非关系型数据库摆脱了事务的强一致性束缚,达到了最终一致性的效果,从而有了飞跃的发展,NoSql(Not Only Sql)也产生了多个架构的数据库类型,包括 KV,列式存储,文档类型等。 35 | - 消息中间件:分布式消息队列系统是消除异步带来一系列的复杂步骤的一大利器,多线程高并发场景先我们常常要谨慎的去设计业务代码,来保证多线程并发情况下不出现资源竞争导致的死锁问题。而消息队列以一种延迟消费的模式将异步任务都存到队列,然后再逐个消化。 36 | - 分布式计算:随着计算技术的发展,有些应用需要非常巨大的计算能力才能完成,如果采用集中式计算,需要耗费相当长的时间来完成。分布式计算将该应用分解成许多小的部分,分配给多台计算机进行处理。这样可以节约整体计算时间,大大提高计算效率。分布式计算系统在场景上分为离线计算,实时计算和流式计算。 37 | 38 | 将这些不同领域的分布式系统映射到实际生产环境中的应用,我们可以得到如下的表格: 39 | 40 | | Type of platform/framework | Example | 41 | | :--------------------------- | :----------------------------------------- | 42 | | Databases | Cassandra, HBase, Riak | 43 | | Message Brokers | Kafka, Pulsar | 44 | | Infrastructure | Kubernetes, Mesos, Zookeeper, etcd, Consul | 45 | | In Memory Data/Compute Grids | Hazelcast, Pivotal Gemfire | 46 | | Stateful Microservices | Akka Actors, Axon | 47 | | File Systems | HDFS, Ceph | 48 | 49 | 和集中式系统相比,分布式系统的性价比更高、处理能力更强、可靠性更高、也有很好的扩展性。提取以上系统的共性需求,可以发现分布式系统能够帮我们解决如下的问题: 50 | 51 | - 单机性能瓶颈导致的成本问题,由于摩尔定律失效,廉价 PC 机性能的瓶颈无法继续突破,小型机和大型机能提高更高的单机性能,但是成本太大高,一般的公司很难承受; 52 | - 用户量和数据量爆炸性的增大导致的成本问题,进入互联网时代,用户量爆炸性的增大,用户产生的数据量也在爆炸性的增大,但是单个用户或者单条数据的价值其实比软件时代(比如银行用户)的价值是只低不高,所以必须寻找更经济的方案; 53 | - 业务高可用的要求,对于互联网的产品来说,都要求 `7 * 24` 小时提供服务,无法容忍停止服务等故障,而要提供高可用的服务,唯一的方式就是增加冗余来完成,这样就算单机系统可以支撑的服务,因为高可用的要求,也会变成一个分布式系统。 54 | 55 | 不过,分布式在解决了网站的高并发问题的同时也带来了一些其他问题。首先,分布式的必要条件就是网络,这可能对性能甚至服务能力造成一定的影响。其次,一个集群中的服务器数量越多,服务器宕机的概率也就越大。另外,由于服务在集群中分布是部署,用户的请求只会落到其中一台机器上,所以,一旦处理不好就很容易产生数据一致性问题。 56 | 57 | # 分布式系统的挑战 58 | 59 | 分布式架构系统使模块重用度更高,开发和发布速度变得更快。但新的架构模式在解决问题的同时,也会带来新的问题。分布式架构首先就面临着高复杂度的问题,这主要体现在以下三个方面: 60 | 61 | - 协作模式。多台机器之间如何协作是非常重要的,选择主从模式、对等模式还是其他协作方式,直接决定了系统最后的运行效率。 62 | - 数据一致性。单体架构中,数据的存储、读取和计算等都在一处,处理起来很方便,但在分布式架构中,会有多处进行数据处理,那如何保证服务器间数据在同一时刻是一致的、客户端读取的都是最新结果? 63 | - 效率问题。限制效率的因素有很多,除了跨机房、跨地域的分布式架构自带的物理限制,还有节点规模越来越大而带来的沟通效率等问题。 64 | -------------------------------------------------------------------------------- /02~一致性与共识/README.md: -------------------------------------------------------------------------------- 1 | # 一致性与共识 2 | 3 | 分布式系统的不可靠性是其内在属性,为了应对这种不可靠性,我们必然会进入到一致性、共识及分布式事务的领域。 4 | 5 | # 一致性 6 | 7 | 在分布式系统中,我们采用多机器进行分布式部署的方式提供服务,而为了保证系统的可用性与性能,我们必须将数据复制到分布式部署的多台机器中,以达到如下的目的: 8 | 9 | - 消除单点故障,防止系统由于某台(些)机器宕机导致的不可用; 10 | - 通过负载均衡技术,能够让分布在不同地方的数据副本全都对外提供服务,从而有效提高系统性能。 11 | 12 | 但是分布式系统引入复制机制后,不同的数据节点之间由于网络延时等原因很容易产生数据不一致的情况,譬如我们常常会面临以下具体的场景: 13 | 14 | - 如果你在同一时刻查看两个数据库节点,则可能在两个节点上看到不同的数据,因为写请求在不同的时间到达不同的节点。无论数据库使用何种复制方法(单主复制,多主复制或无主复制),都会出现这些不一致情况。 15 | - 比如在集中式系统中,有一些关键的配置信息,可以直接保存在服务器的内存中,但是在分布式系统中,如何保存这些配置信息,又如何保证所有机器上的配置信息都保持一致,又如何保证修改一个配置能够把这次修改同步到所有机器中,就是存在的问题。 16 | - 在集中式系统中,进行一个同步操作要写同一个数据的时候,可以直接使用事务与锁来管理保证数据的 ACID。但是,在分布式系统中如何保证多台机器不会同时写同一条数据。 17 | 18 | 数据复制面临的主要难题也是如何保证多个副本之间的分布式一致性,即分布式多个存储节点情况下怎么保证逻辑上相同的副本能够返回相同的数据,保证关联数据之间的逻辑关系是否正确和完整。如何能既保证分布式一致性,又保证系统的性能,是每一个分布式系统都需要重点考虑和权衡的。 19 | 20 | 分布式一致性模型和我们之前讨论的事务隔离级别的层次结构有一些相似之处。尽管两者有一部分内容重叠,但它们大多是无关的问题:事务隔离主要是为了,避免由于同时执行事务而导致的竞争状态,而分布式一致性主要关于,面对延迟和故障时,如何协调副本间的状态。 21 | 22 | 在数据库系统中通常用事务,即访问并可能更新数据库中各种数据项的一个程序执行单元,来保证数据的一致性和完整性。而大多数复制的数据库至少提供了最终一致性,这意味着如果你停止向数据库写入数据并等待一段不确定的时间,那么最终所有的读取请求都会返回相同的值。换句话说,不一致性是暂时的,最终会自行解决(假设网络中的任何故障最终都会被修复)。最终一致性的一个更好的名字可能是收敛(convergence),因为我们预计所有的复本最终会收敛到相同的值。然而,这是一个非常弱的保证,它并没有说什么什么时候副本会收敛。在收敛之前,读操作可能会返回任何东西或什么都没有。例如,如果你写入了一个值,然后立即再次读取,这并不能保证你能看到刚跟写入的值,因为读请求可能会被路由到另外的副本上。 23 | 24 | 在分布式系统与数据库等技术领域中,一致性都会频繁地出现,但是在不同的语境和上下文中,它其实代表着不同的东西: 25 | 26 | - 在事务的上下文中,比如 ACID 里的 C,指的就是通常的一致性(Consistency),即对数据的一组特定陈述必须始终成立,即不变量(invariants)。具体到分布式事务的上下文中这个不变量是:所有参与事务的节点状态保持一致:要么全部成功提交,要么全部失败回滚,不会出现一些节点成功一些节点失败的情况。 27 | - 在分布式系统的上下文中,例如 CAP 里的 C,实际指的是线性一致性(Linearizability),即多副本的系统能够对外表现地像只有单个副本一样(系统保证从任何副本读取到的值都是最新的),且所有操作都以原子的方式生效(一旦某个新值被任一客户端读取到,后续任意读取不会再返回旧值)。 28 | - 一致性哈希、最终一致性这些名词里的一致性也有不同的涵义。 29 | 30 | 在分布式系统中,我们常说的一致性模型有线性一致性、因果一致性、最终一致性等。线性一致性能使多副本数据看起来好像只有一个副本一样,并使其上所有操作都原子性地生效,它使数据库表现的好像单线程程序中的一个变量一样;但它有着速度缓慢的缺点,特别是在网络延迟很大的环境中。因果性对系统中的事件施加了顺序(什么发生在什么之前,基于因与果)。与线性一致不同,线性一致性将所有操作放在单一的全序时间线中,因果一致性为我们提供了一个较弱的一致性模型:某些事件可以是并发的,所以版本历史就像是一条不断分叉与合并的时间线。因果一致性没有线性一致性的协调开销,而且对网络问题的敏感性要低得多。 31 | 32 | # 共识 33 | 34 | 另一方面,共识(Consensus)则是让所有的节点对某件事达成一致,一旦达成共识,应用可以将其用于各种目的。例如,假设你有一个单主复制的数据库。如果主库挂点,并且需要故障切换到另一个节点,剩余的数据库节点可以使用共识来选举新的领导者。在这个过程中重要的是只有一个领导者,且所有的节点都认同其领导。如果两个节点都认为自己是领导者,这种情况被称为脑裂(split brain),且经常导致数据丢失。正确实现共识有助于避免这种问题。 35 | 36 | 但即使捕获到因果顺序(例如使用兰伯特时间戳),我们发现有些事情也不能通过这种方式实现:我们需要确保用户名是唯一的,并拒绝同一用户名的其他并发注册;如果一个节点要通过注册,则需要知道其他的节点没有在并发抢注同一用户名的过程中。这个问题引领我们走向共识。分布式共识问题,简单说,就是在一个或多个进程提议了一个值应当是什么后,采用一种大家都认可的方法,使得系统中所有进程对这个值达成一致意见。 37 | 38 | 达成共识意味着以这样一种方式决定某件事:所有节点一致同意所做决定,且这一决定不可撤销。共识问题通常形式化如下:一个或多个节点可以提议(propose)某些值,而共识算法决定采用其中的某个值。在保证分布式事务一致性的场景中,每个节点可以投票提议,并对谁是新的协调者达成共识。譬如 Raft 算法解决了全序广播问题,维护多副本日志间的一致性,其实就是让所有节点对同全局操作顺序达成一致,也其实就是让日志系统具有线性一致性。因而解决了共识问题。 39 | 40 | 我们可以发现很广泛的一系列问题实际上都可以归结为共识问题,并且彼此等价。比如说选主(Leader election)问题中所有进程对 Leader 达成一致;互斥(Mutual exclusion)问题中对于哪个进程进入临界区达成一致;原子组播(Atomic broadcast)中进程对消息传递(delivery)顺序达成一致。这些等价的问题包括: 41 | 42 | - 线性一致性的 CAS 寄存器:寄存器需要基于当前值是否等于操作给出的参数,原子地决定是否设置新值。 43 | - 原子事务提交:数据库必须决定是否提交或中止分布式事务。 44 | - 全序广播:即保证消息不丢失,且消息以相同的顺序传递给每个节点。 45 | - 锁和租约:当几个客户端争抢锁或租约时,由锁来决定哪个客户端成功获得锁。 46 | - 成员/协调服务:给定某种故障检测器(例如超时),系统必须决定哪些节点活着,哪些节点因为会话超时需要被宣告死亡。 47 | - 唯一性约束:当多个事务同时尝试使用相同的键创建冲突记录时,约束必须决定哪一个被允许,哪些因为违反约束而失败。 48 | 49 | 在分布式系统中,我们常常同时讨论分布式事务与共识,这是因为分布式事务本身的一致性是通过协调者内部的原子操作与多阶段提交协议保证的,不需要共识;但解决分布式事务一致性带来的可用性问题需要用到共识。为了保证分布式事务的一致性,分布式事务通常需要一个协调者(Coordinator)/事务管理器(Transaction Manager)来决定事务的最终提交状态。但无论 2PC 还是 3PC,都无法应对协调者失效的问题,而且具有扩大故障的趋势。这就牺牲了可靠性、可维护性与可扩展性。为了让分布式事务真正可用,就需要在协调者挂点的时候能赶快选举出一个新的协调者来解决分歧,这就需要所有节点对谁是领导者达成共识(Consensus)。 50 | 51 | # Links 52 | 53 | - https://parg.co/MpB 54 | - [分布式系统一致性的发展历史](http://36kr.com/p/5037166.html) 55 | - [关于分布式一致性的探究](http://www.hollischuang.com/archives/663) 56 | - https://mp.weixin.qq.com/s/3odLhBtebF4cm58hl-87JA 条分缕析分布式:浅析强弱一致性 57 | - https://zhuanlan.zhihu.com/p/68743917 分布式系统的一致性与共识 58 | -------------------------------------------------------------------------------- /02~一致性与共识/一致性模型/README.md: -------------------------------------------------------------------------------- 1 | # 一致性模型 2 | 3 | 分布式系统常常通过数据的复制来提高系统的可靠性和容错性,并且将数据的副本存放到不同的机器上,由于多个副本的存在,使得维护副本一致性的代价很高。因此,许多分布式系统都采用弱一致性或者是最终一致性,来提高系统的性能和吞吐能力,这样不同的一致性模型也相继被提出。 4 | 5 | ## 主要的一致性模型 6 | 7 | ### 强一致性(Strong Consistency) 8 | 9 | - 也称为线性一致性(Linearizability) 10 | - 所有节点在同一时间看到的数据完全一致 11 | - 任何读操作都能读到最近一次写操作的结果 12 | - 优点:对用户友好,容易理解 13 | - 缺点:性能较差,可用性降低 14 | 15 | ### 最终一致性(Eventual Consistency) 16 | 17 | - BASE 理论的核心 18 | - 在一段时间后,所有节点的数据最终会达到一致 19 | - 常见变体: 20 | - 读自己写一致性(Read-your-writes Consistency) 21 | - 会话一致性(Session Consistency) 22 | - 因果一致性(Causal Consistency) 23 | 24 | ### 顺序一致性(Sequential Consistency) 25 | 26 | - 所有节点看到的操作顺序是一致的 27 | - 但不要求这个顺序与全局时钟完全对应 28 | - 比强一致性的要求略低,但仍然提供较强的一致性保证 29 | 30 | ### 因果一致性(Causal Consistency) 31 | 32 | - 有因果关系的写操作以相同的顺序被观察到 33 | - 无因果关系的写操作可以以不同顺序被观察到 34 | - 是顺序一致性的进一步放松 35 | 36 | ### 读写一致性(Read-Write Consistency) 37 | 38 | - 读自己写一致性:保证进程可以立即看到自己的写入 39 | - 单调读一致性:如果进程读到某个值,后续不会读到更旧的值 40 | - 单调写一致性:写操作按照顺序执行 41 | 42 | ## 实际应用 43 | 44 | 不同的分布式系统根据其业务需求选择不同的一致性模型: 45 | 46 | - 分布式数据库(如 Google Spanner):通常需要强一致性 47 | - NoSQL 数据库(如 Cassandra):通常采用最终一致性 48 | - 分布式缓存(如 Redis Cluster):可配置的一致性级别 49 | - 社交网络:通常采用最终一致性 50 | 51 | ## 选择考虑因素 52 | 53 | 在选择一致性模型时,需要考虑以下因素: 54 | 55 | 1. 业务需求 56 | 2. 性能要求 57 | 3. 可用性要求 58 | 4. CAP 理论权衡 59 | 5. 实现复杂度 60 | -------------------------------------------------------------------------------- /02~一致性与共识/一致性模型/其他一致性模型.md: -------------------------------------------------------------------------------- 1 | # 其他一致性模型 2 | 3 | # 弱一致性 4 | 5 | 弱一致性指的是系统的某个数据被更新后,后续对该数据的读取操作,取到的可能是更新前的值,也可能是更新后的值,全部用户完全读取到更新后的数据,需要经过一段时间,这段时间称作“不一致性窗口”。 6 | 7 | 系统并不保证续进程或者线程的访问都会返回最新的更新过的值。系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久之后可以读到。但会尽可能保证在某个时间级别(比如秒级别)之后,可以让数据达到一致性状态。 8 | 9 | # 读己所写一致性 10 | 11 | 因果一致性的特定形式。一个进程总可以读到自己更新的数据。 12 | 13 | # 会话一致性 14 | 15 | 读己所写一致性的特定形式。进程在访问存储系统同一个会话内,系统保证该进程读己之所写。 16 | 17 | # 单调读一致性 18 | 19 | 如果一个进程已经读取到一个特定值,那么该进程不会读取到该值以前的任何值。 20 | 21 | # 单调写一致性 22 | 23 | 系统保证对同一个进程的写操作串行化。 24 | -------------------------------------------------------------------------------- /02~一致性与共识/一致性模型/因果一致性.md: -------------------------------------------------------------------------------- 1 | # Casual Consistency | 因果一致性 2 | 3 | 如果 A 进程在更新之后向 B 进程通知更新的完成,那么 B 的访问操作将会返回更新的值。如果没有因果关系的 C 进程将会遵循最终一致性的规则。因果一致性(Casual Consistency)在一致性的要求上,又比顺序一致性降低了:它仅要求有因果关系的操作顺序得到保证,非因果关系的操作顺序则无所谓。 4 | 5 | 因果相关的要求是这样的: 6 | 7 | - 本地顺序:本进程中,事件执行的顺序即为本地因果顺序。 8 | - 异地顺序:如果读操作返回的是写操作的值,那么该写操作在顺序上一定在读操作之前。 9 | - 闭包传递:和时钟向量里面定义的一样,如果 a->b,b->c,那么肯定也有 a->c。 10 | 11 | ![](https://assets.ng-tech.icu/item/20230503195551.png) 12 | 13 | - 图 a 满足顺序一致性,因此也满足因果一致性,因为从这个系统中的四个进程的角度看,它们都有相同的顺序也有相同的因果关系。 14 | 15 | - 图 b 满足因果一致性但是不满足顺序一致性,这是因为从进程 P3、P4 看来,进程 P1、P2 上的操作因果有序,因为 P1、P2 上的写操作不存在因果关系,所以它们可以任意执行。不满足一致性的原因,同上面一样是可以推导出冲突的情况来。 16 | 17 | # 顺序和因果 18 | 19 | 顺序反复出现有几个原因,其中一个原因是,它有助于保持因果关系(causality)。 20 | 21 | - 一个对话的观察者首先看到问题的答案,然后才看到被回答的问题。这是令人困惑的,因为它违背了我们对因(cause)与果(effect)的直觉:如果一个问题被回答,显然问题本身得先在那里,因为给出答案的人必须看到这个问题(假如他们并没有预见未来的超能力)。我们认为在问题和答案之间存在因果依赖(causal dependency)。 22 | 23 | - 三位领导者之间的复制,并注意到由于网络延迟,一些写入可能会“压倒”其他写入。从其中一个副本的角度来看,好像有一个对尚不存在的记录的更新操作。这里的因果意味着,一条记录必须先被创建,然后才能被更新。 24 | 25 | - 在并发写入中,如果有两个操作 A 和 B,则存在三种可能性:A 发生在 B 之前,或 B 发生在 A 之前,或者 A 和 B 并发。这种此前发生(happened before)关系是因果关系的另一种表述:如果 A 在 B 前发生,那么意味着 B 可能已经知道了 A,或者建立在 A 的基础上,或者依赖于 A。如果 A 和 B 是并发的,那么它们之间并没有因果联系;换句话说,我们确信 A 和 B 不知道彼此。 26 | 27 | - 在事务快照隔离的上下文中,我们说事务是从一致性快照中读取的。但此语境中“一致”到底又是什么意思?这意味着与因果关系保持一致(consistent with causality):如果快照包含答案,它也必须包含被回答的问题。在某个时间点观察整个数据库,与因果关系保持一致意味着:因果上在该时间点之前发生的所有操作,其影响都是可见的,但因果上在该时间点之后发生的操作,其影响对观察者不可见。读偏差(read skew)意味着读取的数据处于违反因果关系的状态。 28 | 29 | - 事务之间写偏差(write skew)的例子也说明了因果依赖:爱丽丝被允许离班,因为事务认为鲍勃仍在值班,反之亦然。在这种情况下,离班的动作因果依赖于对当前值班情况的观察。可序列化的快照隔离通过跟踪事务之间的因果依赖来检测写偏差。 30 | 31 | - 在爱丽丝和鲍勃看球的例子中,在听到爱丽丝惊呼比赛结果后,鲍勃从服务器得到陈旧结果的事实违背了因果关系:爱丽丝的惊呼因果依赖于得分宣告,所以鲍勃应该也能在听到爱丽斯惊呼后查询到比分。 32 | 33 | 因果关系对事件施加了一种顺序:因在果之前;消息发送在消息收取之前。而且就像现实生活中一样,一件事会导致另一件事:某个节点读取了一些数据然后写入一些结果,另一个节点读取其写入的内容,并依次写入一些其他内容,等等。这些因果依赖的操作链定义了系统中的因果顺序,即,什么在什么之前发生。如果一个系统服从因果关系所规定的顺序,我们说它是因果一致(causally)的。例如,快照隔离提供了因果一致性:当你从数据库中读取到一些数据时,你一定还能够看到其因果前驱(假设在此期间这些数据还没有被删除)。 34 | 35 | # 因果顺序不是全序的 36 | 37 | 全序(total order)允许任意两个元素进行比较,所以如果有两个元素,你总是可以说出哪个更大,哪个更小。例如,自然数集是全序的:给定两个自然数,比如说 5 和 13,那么你可以告诉我,13 大于 5。然而数学集合并不完全是全序的:{a, b} 比 {b, c} 更大吗?好吧,你没法真正比较它们,因为二者都不是对方的子集。我们说它们是无法比较(incomparable)的,因此数学集合是偏序(partially order)的:在某些情况下,可以说一个集合大于另一个(如果一个集合包含另一个集合的所有元素),但在其他情况下它们是无法比较的。 38 | 39 | 全序和偏序之间的差异反映在不同的数据库一致性模型中: 40 | 41 | - 线性一致性:在线性一致的系统中,操作是全序的:如果系统表现的就好像只有一个数据副本,并且所有操作都是原子性的,这意味着对任何两个操作,我们总是能判定哪个操作先发生。 42 | 43 | - 因果性:如果两个操作都没有在彼此之前发生,那么这两个操作是并发的。换句话说,如果两个事件是因果相关的(一个发生在另一个事件之前),则它们之间是有序的,但如果它们是并发的,则它们之间的顺序是无法比较的。这意味着因果关系定义了一个偏序,而不是一个全序:一些操作相互之间是有顺序的,但有些则是无法比较的。 44 | 45 | 因此,根据这个定义,在线性一致的数据存储中是不存在并发操作的:必须有且仅有一条时间线,所有的操作都在这条时间线上,构成一个全序关系。可能有几个请求在等待处理,但是数据存储确保了每个请求都是在唯一时间线上的某个时间点自动处理的,不存在任何并发。并发意味着时间线会分岔然后合并,在这种情况下,不同分支上的操作是无法比较的(即并发操作)。如果你熟悉像 Git 这样的分布式版本控制系统,那么其版本历史与因果关系图极其相似。通常,一个提交(Commit)发生在另一个提交之后,在一条直线上。但是有时你会遇到分支(当多个人同时在一个项目上工作时),合并(Merge)会在这些并发创建的提交相融合时创建。 46 | 47 | ## 线性一致性强于因果一致性 48 | 49 | 线性一致性隐含着(implies)因果关系:任何线性一致的系统都能正确保持因果性。特别是,如果系统中有多个通信通道,线性一致性可以自动保证因果性,系统无需任何特殊操作(如在不同组件间传递时间戳)。线性一致性确保因果性的事实使线性一致系统变得简单易懂,更有吸引力。不过使系统线性一致可能会损害其性能和可用性,尤其是在系统具有严重的网络延迟的情况下(例如,如果系统在地理上散布)。出于这个原因,一些分布式数据系统已经放弃了线性一致性,从而获得更好的性能,但它们用起来也更为困难。 50 | 51 | 好消息是存在折衷的可能性。线性一致性并不是保持因果性的唯一途径,还有其他方法。一个系统可以是因果一致的,而无需承担线性一致带来的性能折损(尤其对于 CAP 定理不适用的情况)。实际上在所有的不会被网络延迟拖慢的一致性模型中,因果一致性是可行的最强的一致性模型。而且在网络故障时仍能保持可用。 52 | 53 | 在许多情况下,看上去需要线性一致性的系统,实际上需要的只是因果一致性,因果一致性可以更高效地实现。基于这种观察结果,研究人员正在探索新型的数据库,既能保证因果一致性,且性能与可用性与最终一致的系统类似。 54 | 55 | ## 捕获因果关系 56 | 57 | 为了维持因果性,你需要知道哪个操作发生在哪个其他操作之前(happened before)。这是一个偏序:并发操作可以以任意顺序进行,但如果一个操作发生在另一个操作之前,那它们必须在所有副本上以那个顺序被处理。因此,当一个副本处理一个操作时,它必须确保所有因果前驱的操作(之前发生的所有操作)已经被处理;如果前面的某个操作丢失了,后面的操作必须等待,直到前面的操作被处理完毕。 58 | 59 | 为了确定因果依赖,我们需要一些方法来描述系统中节点的“知识”。如果节点在发出写入 Y 的请求时已经看到了 X 的值,则 X 和 Y 可能存在因果关系。这个分析使用了那些在欺诈指控刑事调查中常见的问题:CEO 在做出决定 Y 时是否知道 X ?因果一致性需要跟踪整个数据库中的因果依赖,而不仅仅是一个键,可以推广版本向量以解决此类问题。为了确定因果顺序,数据库需要知道应用读取了哪个版本的数据。在 SSI 的冲突检测中会出现类似的想法,如可序列化的快照隔离(SSI)中所述:当事务要提交时,数据库将检查它所读取的数据版本是否仍然是最新的。为此,数据库跟踪哪些数据被哪些事务所读取。 60 | 61 | # 案例分析 62 | 63 | 在 InfoQ 分享的腾讯朋友圈的设计中,他们在设计数据一致性的时候,使用了因果一致性这个模型。用于保证对同一条朋友圈的回复的一致性,比如这样的情况: 64 | 65 | 1. A 发了朋友圈内容为梅里雪山的图片。 66 | 2. B 针对内容 a 回复了评论:“这里是哪里?” 67 | 3. C 针对 B 的评论进行了回复:”这里是梅里雪山“。 68 | 69 | 那么,这条朋友圈的显示中,显然 C 针对 B 的评论,应该在 B 的评论之后,这是一个因果关系,而其他没有因果关系的数据,可以允许不一致。微信的做法是: 70 | 71 | 1. 每个数据中心,都自己生成唯一的、递增的数据 ID,确保能排重。在下图的示例中,有三个数据中心,数据中心 1 生成的数据 ID 模 1 为 0,数据中心 1 生成的数据 ID 模 2 为 0,数据中心 1 生成的数据 ID 模 3 为 0,这样保证了三个数据中心的数据 ID 不会重复全局唯一。 72 | 73 | 2. 每条评论都比本地看到所有全局 ID 大,这样来确保因果关系,这部分的原理前面提到的向量时钟一样。 74 | 75 | ![](http://mmbiz.qpic.cn/mmbiz/vxCq1iahXotiaFs84SvDRF5U3gefsfA2F8cp2O082gPUZEbkiawXfogQ3DI8ghhhtFZqicbatRvrklGwxe8JlmrlOw/640?wx_fmt=png&wxfrom=5&wx_lazy=1) 76 | 77 | 有了这个模型和原理,就很好处理前面针对评论的评论的顺序问题了。 78 | 79 | 1. 假设 B 在数据中心 1 上,上面的 ID 都满足模 1 为 0,那么当 B 看到 A 的朋友圈时,发表了评论,此时给这个评论分配的 ID 是 1,因此 B 的时钟向量数据是[1]。 80 | 81 | 2. 假设 C 在数据中心 2 上,上面的 ID 都满足模 2 为 0,当 C 看到了 B 的评论时,针对这个评论做了评论,此时需要给这个评论分配的 ID 肯定要满足模 2 为 0 以及大于 1,评论完毕之后 C 上面的时钟向量是[1,2]。 82 | 83 | 3. 假设 A 在数据中心 3 上,上面的 ID 都满足模 3 为 0,当 A 看到 B、C 给自己的评论时,很容易按照 ID 进行排序和合并--即使 A 在收到 C 的数据[1,2]之后再收到 B 的数据[1],也能顺利的完成合并。 84 | -------------------------------------------------------------------------------- /02~一致性与共识/一致性模型/最终一致性.md: -------------------------------------------------------------------------------- 1 | # 最终一致性 2 | 3 | 弱一致性的特定形式,系统保证用户最终能够读取到某个操作对系统的更新。在没有故障发生的前提下,不一致窗口的时间主要受通信延迟,系统负载和复制副本的个数影响。DNS 是一个典型的最终一致性系统。 4 | 5 | 最终一致性同样存在着很多的变种,最终一致性的不同方式可以进行组合,例如单调读一致性和读己之所写一致性就可以组合实现。并且从实践的角度来看,这两者的组合,读取自己更新的数据,和一旦读取到最新的版本不会再读取旧版本,对于此架构上的程序开发来说,会少很多额外的烦恼。 6 | -------------------------------------------------------------------------------- /02~一致性与共识/一致性模型/线性一致性.md: -------------------------------------------------------------------------------- 1 | # 线性一致性 2 | 3 | 所谓的线性一致性(Linearizability),也称为原子一致性(Atomic Consistency)、强一致性(Strict Consistency)、强一致性(strong consistency)、立即一致性(immediate consistency)或外部一致性(external consistency)。在一个线性一致的系统中,只要一个客户端成功完成写操作,所有客户端从数据库中读取数据必须能够看到刚刚写入的值。线性一致性对多副本的系统能够对外表现地像只有单个副本一样,即系统能保障读到的值是最近的、最新的而不是来自过时的缓存或副本。换言之,线性一致性是一个新鲜度保证(recency guarantee)。下图即使某个非线性一致性系统的例子: 4 | 5 | ![这个系统是非线性一致的,导致了球迷的困惑](https://s2.ax1x.com/2020/02/12/1Hvm28.md.png) 6 | 7 | Alice 和 Bob 正坐在同一个房间里,都盯着各自的手机,关注着 2014 年 FIFA 世界杯决赛的结果。在最后得分公布后,Alice 刷新页面,看到宣布了获胜者,并兴奋地告诉 Bob。Bob 难以置信地刷新了自己的手机,但他的请求路由到了一个落后的数据库副本上,手机显示比赛仍在进行。如果 Alice 和 Bob 在同一时间刷新并获得了两个不同的查询结果,也许就没有那么令人惊讶了。因为他们不知道服务器处理他们请求的精确时刻。然而 Bob 是在听到 Alice 惊呼最后得分之后,点击了刷新按钮(启动了他的查询),因此他希望查询结果至少与爱丽丝一样新鲜。但他的查询返回了陈旧结果,这一事实违背了线性一致性的要求。综上所述,线性一致性对一致性的要求两个: 8 | 9 | - 任何一次读都能读到某个数据的最近一次写的数据。 10 | - 系统中的所有进程,看到的操作顺序,都和全局时钟下的顺序一致。 11 | 12 | 强一致性要求无论数据的更新操作是在哪个副本上执行,之后所有的读操作都要能够获取到更新的最新数据。对于单副本的数据来说,读和写都是在同一份数据上执行,容易保证强一致性,但对于多副本数据来说,若想保障强一致性,就需要等待各个副本的写入操作都执行完毕,才能提供数据的读取,否则就有可能数据不一致,这种情况需要通过分布式事务来保证操作的原子性,并且外界无法读到系统的中间状态。显然这强一致性对全局时钟有非常高的要求。强一致性,只是存在理论中的一致性模型,比它要求更弱一些的,就是顺序一致性。 13 | 14 | # 什么使得系统线性一致? 15 | 16 | 线性一致性背后的基本思想很简单:使系统看起来好像只有一个数据副本。然而确切来讲,实际上有更多要操心的地方。为了更好地理解线性一致性,让我们再看几个例子。下图显示了三个客户端在线性一致数据库中同时读写相同的键 x。在分布式系统文献中,x 被称为寄存器(register),例如,它可以是键值存储中的一个键,关系数据库中的一行,或文档数据库中的一个文档。 17 | 18 | ![如果读取请求与写入请求并发,则可能会返回旧值或新值](https://s2.ax1x.com/2020/02/12/1biSnf.md.png) 19 | 20 | 每个柱都是由客户端发出的请求,其中柱头是请求发送的时刻,柱尾是客户端收到响应的时刻。因为网络延迟变化无常,客户端不知道数据库处理其请求的精确时间,只知道它发生在发送请求和接收响应的之间的某个时刻。在这个例子中,寄存器有两种类型的操作: 21 | 22 | - $read(x)⇒v$ 表示客户端请求读取寄存器 x 的值,数据库返回值 v。 23 | 24 | - $write(x,v)⇒r$ 表示客户端请求将寄存器 x 设置为值 v,数据库返回响应 r(可能正确,可能错误)。 25 | 26 | x 的值最初为 0,客户端 C 执行写请求将其设置为 1。发生这种情况时,客户端 A 和 B 反复轮询数据库以读取最新值。A 和 B 的请求可能会收到怎样的响应? 27 | 28 | - 客户端 A 的第一个读操作,完成于写操作开始之前,因此必须返回旧值 `0`。 29 | - 客户端 A 的最后一个读操作,开始于写操作完成之后。如果数据库是线性一致性的,它必然返回新值 `1`:因为读操作和写操作一定是在其各自的起止区间内的某个时刻被处理。如果在写入结束后开始读取,则必须在写入之后处理读取,因此它必须看到写入的新值。 30 | - 与写操作在时间上重叠的任何读操作,可能会返回 `0`或`1`,因为我们不知道读取时,写操作是否已经生效。这些操作是并发(concurrent)的。 31 | 32 | 但是,这还不足以完全描述线性一致性:如果与写入同时发生的读取可以返回旧值或新值,那么读者可能会在写入期间看到数值在旧值和新值之间来回翻转。这不是我们所期望的仿真“单一数据副本”的系统。为了使系统线性一致,我们需要添加另一个约束: 33 | 34 | ![任何一个读取返回新值后,所有后续读取(在相同或其他客户端上)也必须返回新值](https://s2.ax1x.com/2020/02/12/1biWVS.md.png) 35 | 36 | 在一个线性一致的系统中,我们可以想象,在 x 的值从 0 自动翻转到 1 的时候(在写操作的开始和结束之间)必定有一个时间点。因此,如果一个客户端的读取返回新的值 1,即使写操作尚未完成,所有后续读取也必须返回新值。上图中的箭头说明了这个时序依赖关系,客户端 A 是第一个读取新的值 1 的位置。在 A 的读取返回之后,B 开始新的读取。由于 B 的读取严格在发生于 A 的读取之后,因此即使 C 的写入仍在进行中,也必须返回 1。除了读写之外,还增加了第三种类型的操作: 37 | 38 | - $cas(x, v_{old}, v_{new})⇒r$ 表示客户端请求进行原子性的比较与设置操作。如果寄存器 $x$ 的当前值等于 $v_{old}$,则应该原子地设置为 $v_{new}$ 。如果 $x≠v_{old}$,则操作应该保持寄存器不变并返回一个错误。$r$ 是数据库的响应(正确或错误)。 39 | 40 | 下图中每个操作都在我们认为执行操作的时候用竖线标出(在每个操作的条柱之内)。这些标记按顺序连在一起,其结果必须是一个有效的寄存器读写序列(每次读取都必须返回最近一次写入设置的值)。线性一致性的要求是,操作标记的连线总是按时间(从左到右)向前移动,而不是向后移动。这个要求确保了我们之前讨论的新鲜性保证:一旦新的值被写入或读取,所有后续的读都会看到写入的值,直到它被再次覆盖。 41 | 42 | ![可视化读取和写入看起来已经生效的时间点,B 的最后读取不是线性一致性的](https://s2.ax1x.com/2020/02/12/1bFnxI.md.png) 43 | 44 | - 第一个客户端 B 发送一个读取 x 的请求,然后客户端 D 发送一个请求将 x 设置为 0,然后客户端 A 发送请求将 x 设置为 1。尽管如此,返回到 B 的读取值为 1(由 A 写入的值)。这是可以的:这意味着数据库首先处理 D 的写入,然后是 A 的写入,最后是 B 的读取。虽然这不是请求发送的顺序,但这是一个可以接受的顺序,因为这三个请求是并发的。也许 B 的读请求在网络上略有延迟,所以它在两次写入之后才到达数据库。 45 | 46 | - 在客户端 A 从数据库收到响应之前,客户端 B 的读取返回 1,表示写入值 1 已成功。这也是可以的:这并不意味着在写之前读到了值,这只是意味着从数据库到客户端 A 的正确响应在网络中略有延迟。 47 | 48 | - 此模型不假设有任何事务隔离:另一个客户端可能随时更改值。例如,C 首先读取 1,然后读取 2,因为两次读取之间的值由 B 更改。可以使用原子比较并设置(cas)操作来检查该值是否未被另一客户端同时更改:B 和 C 的 cas 请求成功,但是 D 的 cas 请求失败(在数据库处理它时,x 的值不再是 0)。 49 | 50 | - 客户 B 的最后一次读取(阴影条柱中)不是线性一致性的。该操作与 C 的 cas 写操作并发(它将 x 从 2 更新为 4)。在没有其他请求的情况下,B 的读取返回 2 是可以的。然而,在 B 的读取开始之前,客户端 A 已经读取了新的值 4,因此不允许 B 读取比 A 更旧的值。 51 | 52 | 这就是线性一致性背后的直觉。正式的定义更准确地描述了它。通过记录所有请求和响应的时序,并检查它们是否可以排列成有效的顺序,测试一个系统的行为是否线性一致性是可能的(尽管在计算上是昂贵的)。 53 | 54 | # 线性一致性与可序列化 55 | 56 | 线性一致性容易和可序列化相混淆,因为两个词似乎都是类似“可以按顺序排列”的东西。但它们是两种完全不同的保证,区分两者非常重要: 57 | 58 | - 可序列化:可序列化(Serializability)是事务的隔离属性,每个事务可以读写多个对象(行,文档,记录)。它确保事务的行为,与它们按照某种顺序依次执行的结果相同(每个事务在下一个事务开始之前运行完成)。这种执行顺序可以与事务实际执行的顺序不同。 59 | 60 | - 线性一致性:线性一致性(Linearizability)是读取和写入寄存器(单个对象)的新鲜度保证。它不会将操作组合为事务,因此它也不会阻止写偏差等问题,除非采取其他措施(例如物化冲突)。 61 | 62 | 一个数据库可以提供可串行性和线性一致性,这种组合被称为严格的可串行性或强的单副本强可串行性(strong-1SR)。基于两阶段锁定的可串行化实现或实际串行执行通常是线性一致性的。但是,可序列化的快照隔离不是线性一致性的:按照设计,它可以从一致的快照中进行读取,以避免锁定读者和写者之间的争用。一致性快照的要点就在于它不会包括比快照更新的写入,因此从快照读取不是线性一致性的。 63 | 64 | # 线性一致性的案例 65 | 66 | ## 锁定和领导选举 67 | 68 | 一个使用单主复制的系统,需要确保领导真的只有一个,而不是几个(脑裂)。一种选择领导者的方法是使用锁:每个节点在启动时尝试获取锁,成功者成为领导者。不管这个锁是如何实现的,它必须是线性一致的:所有节点必须就哪个节点拥有锁达成一致,否则就没用了。诸如 Apache ZooKeeper 和 etcd 之类的协调服务通常用于实现分布式锁和领导者选举。它们使用一致性算法,以容错的方式实现线性一致的操作。还有许多微妙的细节来正确地实现锁和领导者选择,而像 Apache Curator 这样的库则通过在 ZooKeeper 之上提供更高级别的配方来提供帮助。但是,线性一致性存储服务是这些协调任务的基础。 69 | 70 | 分布式锁也在一些分布式数据库(如 Oracle Real Application Clusters(RAC))中以更细的粒度使用。RAC 对每个磁盘页面使用一个锁,多个节点共享对同一个磁盘存储系统的访问权限。由于这些线性一致的锁处于事务执行的关键路径上,RAC 部署通常具有用于数据库节点之间通信的专用集群互连网络。 71 | 72 | ## 约束和唯一性保证 73 | 74 | 唯一性约束在数据库中很常见:例如,用户名或电子邮件地址必须唯一标识一个用户,而在文件存储服务中,不能有两个具有相同路径和文件名的文件。如果要在写入数据时强制执行此约束(例如,如果两个人试图同时创建一个具有相同名称的用户或文件,其中一个将返回一个错误),则需要线性一致性。这种情况实际上类似于一个锁:当一个用户注册你的服务时,可以认为他们获得了所选用户名的“锁定”。该操作与原子性的比较与设置非常相似:将用户名赋予声明它的用户,前提是用户名尚未被使用。 75 | 76 | 如果想要确保银行账户余额永远不会为负数,或者不会出售比仓库里的库存更多的物品,或者两个人不会都预定了航班或剧院里同一时间的同一个位置。这些约束条件都要求所有节点都同意一个最新的值(账户余额,库存水平,座位占用率)。在实际应用中,处理这些限制有时是可以接受的(例如,如果航班超额预订,你可以将客户转移到不同的航班并为其提供补偿)。在这种情况下,可能不需要线性一致性。然而,一个硬性的唯一性约束(关系型数据库中常见的那种)需要线性一致性。其他类型的约束,如外键或属性约束,可以在不需要线性一致性的情况下实现。 77 | 78 | ## 跨信道的时序依赖 79 | 80 | 例如,假设有一个网站,用户可以上传照片,一个后台进程会调整照片大小,降低分辨率以加快下载速度(缩略图)。图像缩放器需要明确的指令来执行尺寸缩放作业,指令是 Web 服务器通过消息队列发送的。Web 服务器不会将整个照片放在队列中,因为大多数消息代理都是针对较短的消息而设计的,而一张照片的空间占用可能达到几兆字节。取而代之的是,首先将照片写入文件存储服务,写入完成后再将缩放器的指令放入消息队列。该系统的架构和数据流如下图所示。 81 | 82 | ![Web服务器和图像调整器通过文件存储和消息队列进行通信,打开竞争条件的可能性](https://s2.ax1x.com/2020/02/12/1babyn.png) 83 | 84 | 如果文件存储服务是线性一致的,那么这个系统应该可以正常工作。如果它不是线性一致的,则存在竞争条件的风险:消息队列(步骤 3 和 4)可能比存储服务内部的复制更快。在这种情况下,当缩放器读取图像(步骤 5)时,可能会看到图像的旧版本,或者什么都没有。如果它处理的是旧版本的图像,则文件存储中的全尺寸图和略缩图就产生了永久性的不一致。出现这个问题是因为 Web 服务器和缩放器之间存在两个不同的信道:文件存储与消息队列。没有线性一致性的新鲜性保证,这两个信道之间的竞争条件是可能的。线性一致性并不是避免这种竞争条件的唯一方法,但它是最容易理解的。如果你可以控制额外信道,则可以使用在“读己之写”讨论过的备选方法,不过会有额外的复杂度代价。 85 | 86 | # 实现线性一致的系统 87 | 88 | 我们已经见到了几个线性一致性有用的例子,让我们思考一下,如何实现一个提供线性一致语义的系统。由于线性一致性本质上意味着“表现得好像只有一个数据副本,而且所有的操作都是原子的”,所以最简单的答案就是,真的只用一个数据副本。但是这种方法无法容错:如果持有该副本的节点失效,数据将会丢失,或者至少无法访问,直到节点重新启动。 89 | 90 | ## 单主复制(可能线性一致) 91 | 92 | 在具有单主复制功能的系统中,主库具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果从主库或同步更新的从库读取数据,它们可能(protential)是线性一致性的。然而,并不是每个单主数据库都是实际线性一致性的,无论是通过设计(例如,因为使用快照隔离)还是并发错误。对单领域数据库进行分区(分片),以便每个分区有一个单独的领导者,不会影响线性一致性,因为线性一致性只是对单一对象的保证。 93 | 94 | 从主库读取依赖一个假设,你确定领导是谁。正如在“真理在多数人手中”中所讨论的那样,一个节点很可能会认为它是领导者,而事实上并非如此——如果具有错觉的领导者继续为请求提供服务,可能违反线性一致性。使用异步复制,故障切换时甚至可能会丢失已提交的写入,这同时违反了持久性和线性一致性。 95 | 96 | ## 共识算法(线性一致) 97 | 98 | 共识算法与单领导者复制类似。然而,共识协议包含防止脑裂和陈旧副本的措施。由于这些细节,共识算法可以安全地实现线性一致性存储。例如,Zookeeper 和 etcd 就是这样工作的。 99 | 100 | ## 多主复制(非线性一致) 101 | 102 | 具有多主程序复制的系统通常不是线性一致的,因为它们同时在多个节点上处理写入,并将其异步复制到其他节点。因此,它们可能会产生冲突的写入,需要解析。这种冲突是因为缺少单一数据副本人为产生的。 103 | 104 | ## 无主复制(也许不是线性一致的) 105 | 106 | 对于无领导者复制的系统,有时候人们会声称通过要求法定人数读写($w + r> n$)可以获得“强一致性”。这取决于法定人数的具体配置,以及强一致性如何定义(通常不完全正确)。直觉上在 Dynamo 风格的模型中,严格的法定人数读写应该是线性一致性的。但是当我们有可变的网络延迟时,就可能存在竞争条件: 107 | 108 | ![非线性一致的执行,尽管使用了严格的法定人数](https://s2.ax1x.com/2020/02/12/1bwC4S.md.png) 109 | 110 | 上图中 $x$ 的初始值为 0,写入客户端通过向所有三个副本($n = 3, w = 3$)发送写入将 $x$ 更新为 1。客户端 A 并发地从两个节点组成的法定人群($r = 2$)中读取数据,并在其中一个节点上看到新值 1 。客户端 B 也并发地从两个不同的节点组成的法定人数中读取,并从两个节点中取回了旧值 0 。 111 | 112 | 仲裁条件满足($w + r> n$),但是这个执行是非线性一致的:B 的请求在 A 的请求完成后开始,但是 B 返回旧值,而 A 返回新值。有趣的是,通过牺牲性能,可以使 Dynamo 风格的法定人数线性化:读取者必须在将结果返回给应用之前,同步执行读修复,并且写入者必须在发送写入之前,读取法定数量节点的最新状态。然而,由于性能损失,Riak 不执行同步读修复。Cassandra 在进行法定人数读取时,确实在等待读修复完成;但是由于使用了最后写入为准的冲突解决方案,当同一个键有多个并发写入时,将不能保证线性一致性。而且,这种方式只能实现线性一致的读写;不能实现线性一致的比较和设置操作,因为它需要一个共识算法。总而言之,最安全的做法是:假设采用 Dynamo 风格无主复制的系统不能提供线性一致性。 113 | -------------------------------------------------------------------------------- /02~一致性与共识/一致性模型/顺序一致性.md: -------------------------------------------------------------------------------- 1 | # 顺序一致性 2 | 3 | 顺序一致性(Sequential Consistency),也同样有两个条件,其一与前面强一致性的要求一样,也是可以马上读到最近写入的数据,然而它的第二个条件就弱化了很多,它允许系统中的所有进程形成自己合理的统一的一致性,不需要与全局时钟下的顺序都一致。这里的第二个条件的要点在于: 4 | 5 | - 系统的所有进程的顺序一致,而且是合理的,就是说任何一个进程中,这个进程对同一个变量的读写顺序要保持,然后大家形成一致。 6 | - 不需要与全局时钟下的顺序一致。 7 | 8 | 可见,顺序一致性在顺序要求上并没有那么严格,它只要求系统中的所有进程达成自己认为的一致就可以了,即错的话一起错,对的话一起对,同时不违反程序的顺序即可,并不需要个全局顺序保持一致。 9 | 10 | ![](https://assets.ng-tech.icu/item/20230503195429.png) 11 | 12 | - 图 a 是满足顺序一致性,但是不满足强一致性的。原因在于,从全局时钟的观点来看,P2 进程对变量 X 的读操作在 P1 进程对变量 X 的写操作之后,然而读出来的却是旧的数据。但是这个图却是满足顺序一致性的,因为两个进程 P1,P2 的一致性并没有冲突。从这两个进程的角度来看,顺序应该是这样的:Write(y,2), Read(x,0), Write(x,4), Read(y,2),每个进程内部的读写顺序都是合理的,但是显然这个顺序与全局时钟下看到的顺序并不一样。 13 | 14 | - 图 b 满足强一致性,因为每个读操作都读到了该变量的最新写的结果,同时两个进程看到的操作顺序与全局时钟的顺序一样,都是 Write(y,2), Read(x,4), Write(x,4), Read(y,2)。 15 | 16 | - 图 c 不满足顺序一致性,当然也就不满足强一致性了。因为从进程 P1 的角度看,它对变量 Y 的读操作返回了结果 0。那么就是说,P1 进程的对变量 Y 的读操作在 P2 进程对变量 Y 的写操作之前,这意味着它认为的顺序是这样的:write(x,4), Read(y,0), Write(y,2), Read(x,0),显然这个顺序又是不能被满足的,因为最后一个对变量 x 的读操作读出来也是旧的数据。因此这个顺序是有冲突的,不满足顺序一致性。 17 | 18 | # 顺序保证 19 | 20 | 线性一致寄存器的行为就好像只有单个数据副本一样,且每个操作似乎都是在某个时间点以原子性的方式生效的。这个定义意味着操作是按照某种良好定义的顺序执行的。我们通过操作(似乎)执行完毕的顺序来连接操作。顺序这个词也曾反复地出现: 21 | 22 | - 领导者在单主复制中的主要目的就是,在复制日志中确定写入顺序(order of write),也就是从库应用这些写入的顺序。如果不存在一个领导者,则并发操作可能导致冲突。 23 | 24 | - 可序列化,是关于事务表现的像按某种序列顺序(some sequential order)执行的保证。它可以通过字面意义上地序列顺序(serial order)执行事务来实现,或者通过允许并行执行,同时防止序列化冲突来实现(通过锁或中止事务)。 25 | 26 | - 在分布式系统中使用时间戳和时钟是另一种将顺序引入无序世界的尝试,例如,确定两个写入操作哪一个更晚发生。 27 | 28 | 顺序,线性一致性和共识之间有着深刻的联系。 29 | -------------------------------------------------------------------------------- /02~一致性与共识/共识算法/Paxos/Multiple-Paxos.md: -------------------------------------------------------------------------------- 1 | # Multiple Paxos 2 | 3 | # Links 4 | 5 | - [Paxos 选举多次决议的算法实现](http://bingotree.cn/?p=607) 6 | -------------------------------------------------------------------------------- /02~一致性与共识/共识算法/Paxos/README.md: -------------------------------------------------------------------------------- 1 | # Paxos 2 | 3 | Google Chubby 的作者 Mike Burrows 说过这个世界上只有一种一致性算法,那就是 Paxos,其它的算法都是残次品。 4 | 5 | ``` 6 | --- Paxos Proposer --- 7 | 8 | 1 proposer(v): 9 | 2 while not decided: 10 | 2 choose n, unique and higher than any n seen so far 11 | 3 send prepare(n) to all servers including self 12 | 4 if prepare_ok(n, na, va) from majority: 13 | 5 v' = va with highest na; choose own v otherwise 14 | 6 send accept(n, v') to all 15 | 7 if accept_ok(n) from majority: 16 | 8 send decided(v') to all 17 | 18 | 19 | --- Paxos Acceptor --- 20 | 21 | 9 acceptor state on each node (persistent): 22 | 10 np --- highest prepare seen 23 | 11 na, va --- highest accept seen 24 | 25 | 12 acceptor's prepare(n) handler: 26 | 13 if n > np 27 | 14 np = n 28 | 15 reply prepare_ok(n, na, va) 29 | 16 else 30 | 17 reply prepare_reject 31 | 32 | 33 | 18 acceptor's accept(n, v) handler: 34 | 19 if n >= np 35 | 20 np = n 36 | 21 na = n 37 | 22 va = v 38 | 23 reply accept_ok(n) 39 | 24 else 40 | 25 reply accept_reject 41 | 42 | ``` 43 | -------------------------------------------------------------------------------- /02~一致性与共识/共识算法/README.md: -------------------------------------------------------------------------------- 1 | # 分布式共识 2 | 3 | 共识(Consensus)是分布式计算中最重要也是最基本的问题之一,所谓共识,就是让所有的节点对某件事达成一致;例如,如果有几个人同时尝试预订飞机上的最后一个座位,或剧院中的同一个座位,或者尝试使用相同的用户名注册一个帐户,共识算法可以用来确定这些互不相容的操作中,哪一个才是赢家。 4 | 5 | ![简要的一致性描述](https://s1.ax1x.com/2020/08/01/aGlZtK.jpg) 6 | 7 | 因为分布式系统中存在着的网络故障与流程故障,可靠地达成共识是一个令人惊讶的棘手问题。一旦达成共识,应用可以将其用于各种目的。共识的典型场景包括了: 8 | 9 | - 领导选举:在单主复制的数据库中,所有节点需要就哪个节点是领导者达成一致。如果一些节点由于网络故障而无法与其他节点通信,则可能会对领导权的归属引起争议。在这种情况下,共识对于避免错误的故障切换非常重要。错误的故障切换会导致两个节点都认为自己是领导者。如果有两个领导者,它们都会接受写入,它们的数据会发生分歧,从而导致不一致和数据丢失。 10 | - 原子提交:在支持跨多节点或跨多分区事务的数据库中,一个事务可能在某些节点上失败,但在其他节点上成功。如果我们想要维护事务的原子性,我们必须让所有节点对事务的结果达成一致:要么全部中止/回滚(如果出现任何错误),要么它们全部提交(如果没有出错)。这个共识的例子被称为原子提交(atomic commit)问题。 11 | 12 | 注意,原子提交的形式化与共识稍有不同:原子事务只有在所有参与者投票提交的情况下才能提交,如果有任何参与者需要中止,则必须中止。共识则允许就任意一个被参与者提出的候选值达成一致。然而,原子提交和共识可以相互简化为对方,非阻塞原子提交则要比共识更为困难。近年来随着分布式系统的规模越来越大,对可用性和共识的要求越来越高,分布式共识的应用也越来越广泛。纵观分布式共识在工业界的应用,从最开始的鼻祖 Paxos 的一统天下,到横空出世的 Raft 的流行,再到如今 Leaderless 的 EPaxos 开始备受关注。共识算法根据容错能力不同,即在考虑节点故障不响应的情况下,再考虑节点是否会伪造信息进行恶意响应,可以分为 CFT(Crash Fault Tolerance)类和 BFT(Byzantine Fault Tolerance)类共识算法。 13 | 14 | - CFT 共识算法只保证分布式系统中节点发生宕机错误时整个分布式系统的可靠性,而当系统中节点违反共识协议的时候(比如被黑客攻占,数据被恶意篡改等)将无法保障分布式系统的可靠性,因此 CFT 共识算法目前主要应用在企业内部的封闭式分布式系统中,目前流行的 CFT 共识算法主要有 Paxos 算法及其衍生的 Raft 共识算法。 15 | - 采用 BFT 共识算法的分布式系统,即使系统中的节点发生了任意类型的错误,只要发生错误的节点少于一定比例,整个系统的可靠性就可以保证。因此,在开放式分布式系统中,比如区块链网络,必须采用 BFT 共识算法。 16 | 17 | # 共识的局限性 18 | 19 | 共识算法对于分布式系统来说是一个巨大的突破:它为其他充满不确定性的系统带来了基础的安全属性(一致同意,完整性和有效性),然而它们还能保持容错(只要多数节点正常工作且可达,就能取得进展)。它们提供了全序广播,因此它们也可以以一种容错的方式实现线性一致的原子操作。尽管如此,它们并不是在所有地方都用上了,因为好处总是有代价的。节点在做出决定之前对提议进行投票的过程是一种同步复制,但是通常数据库会配置为异步复制模式。在这种配置中发生故障切换时,一些已经提交的数据可能会丢失;但是为了获得更好的性能,许多人选择接受这种风险。 20 | 21 | 共识系统总是需要严格多数来运转。这意味着你至少需要三个节点才能容忍单节点故障(其余两个构成多数),或者至少有五个节点来容忍两个节点发生故障(其余三个构成多数)。如果网络故障切断了某些节点同其他节点的连接,则只有多数节点所在的网络可以继续工作,其余部分将被阻塞。大多数共识算法假定参与投票的节点是固定的集合,这意味着你不能简单的在集群中添加或删除节点。共识算法的**动态成员扩展(dynamic membership extension)**允许集群中的节点集随时间推移而变化,但是它们比静态成员算法要难理解得多。 22 | 23 | 共识系统通常依靠超时来检测失效的节点。在网络延迟高度变化的环境中,特别是在地理上散布的系统中,经常发生一个节点由于暂时的网络问题,错误地认为领导者已经失效。虽然这种错误不会损害安全属性,但频繁的领导者选举会导致糟糕的性能表现,因系统最后可能花在权力倾扎上的时间要比花在建设性工作的多得多。有时共识算法对网络问题特别敏感。例如 Raft 已被证明存在让人不悦的极端情况:如果整个网络工作正常,但只有一条特定的网络连接一直不可靠,Raft 可能会进入领导频繁二人转的局面,或者当前领导者不断被迫辞职以致系统实质上毫无进展。其他一致性算法也存在类似的问题,而设计能健壮应对不可靠网络的算法仍然是一个开放的研究问题。 24 | 25 | # Links 26 | 27 | - 分布式系统的核心:共识问题 https://cubox.pro/c/vhOLF6 28 | -------------------------------------------------------------------------------- /02~一致性与共识/共识算法/Raft/99~参考资料/2021-多颗糖-条分缕析 Raft.md: -------------------------------------------------------------------------------- 1 | # 条分缕析 Raft 算法 2 | 3 | # Links 4 | 5 | - https://mp.weixin.qq.com/s?__biz=MzIwODA2NjIxOA==&mid=2247484140&idx=1&sn=37876b5dda5294ea7f6211f0a3300ea5&chksm=97098129a07e083fe65f8b87c2ec516b630a8f210961038f0091fbcd69468b41edbe193891ee&scene=21#wechat_redirect 6 | 7 | - https://mp.weixin.qq.com/s?__biz=MzIwODA2NjIxOA==&mid=2247484172&idx=1&sn=f4500241002878eb23fdcadbcd8083af&chksm=970980c9a07e09dfd7d6ba064f4ef63bbae438d3e571475ee48073ecb1eb4316b30062a9b758&scene=21#wechat_redirect 8 | -------------------------------------------------------------------------------- /02~一致性与共识/共识算法/Raft/README.md: -------------------------------------------------------------------------------- 1 | # Raft 2 | 3 | Raft 是斯坦福的 Diego Ongaro、John Ousterhout 两个人以易懂(Understandability)为目标设计的一致性算法,在 2013 年发布了论文:《In Search of an Understandable Consensus Algorithm》。Paxos 一致性算法从 90 年提出,但是其流程太过于繁杂实现起来也比较复杂;而 Raft 一致性算法就是比 Paxos 简单又能实现 Paxos 所解决的问题的一致性算法,从 2013 年发布,两年多时间内就有了十多种语言的 Raft 算法实现框架,较为出名的有 etcd,Google 的 Kubernetes 也是用了 etcd 作为他的服务发现框架。 4 | 5 | Raft 是一个用于日志复制,同步的一致性算法,它提供了和 Paxos 一样的功能和性能,只要保证 `n/2+1` 节点正常就能够提供服务;但是不同于 Paxos 算法直接从分布式一致性问题出发推导出来,Raft 算法则是从多副本状态机的角度提出,用于管理多副本状态机的日志复制。为了强调可理解性,Raft 将一致性算法分解为几个关键流程(模块):Leader 选举(Leader election)、日志同步(Log replication)、安全性(Safety)、日志压缩(Log compaction)、成员变更(Membership change)等,通过将分布式一致性这个复杂的问题转化为一系列的小问题进而各个击破的方式来解决问题。同时它通过实施一个更强的一致性来减少一些不必要的状态,进一步降低了复杂性。Raft 还允许线上进行动态的集群扩容,利用有交集的大多数机制来保证安全性。 6 | 7 | ![复制状态机](https://s1.ax1x.com/2020/08/06/agwY9g.png) 8 | 9 | Raft 的主要流程包含以下步骤: 10 | 11 | - Raft 开始时在集群中选举出 Leader 负责日志复制的管理; 12 | - Leader 接受来自客户端的事务请求(日志),并将它们复制给集群的其他节点,然后负责通知集群中其他节点提交日志,Leader 负责保证其他节点与他的日志同步; 13 | - 当 Leader 宕掉后集群其他节点会发起选举选出新的 Leader; 14 | 15 | # 系统角色 16 | 17 | Raft 将系统中的角色分为领导者(Leader)、跟从者(Follower)和候选人(Candidate): 18 | 19 | - Leader:接受客户端请求,并向 Follower 同步请求日志,当日志同步到大多数节点上后告诉 Follower 提交日志。 20 | - Follower:接受并持久化 Leader 同步的日志,在 Leader 告之日志可以提交之后,提交日志。 21 | - Candidate:Leader 选举过程中的临时角色。 22 | 23 | ![Raft 算法角色](https://s1.ax1x.com/2020/08/03/ad3S4H.png) 24 | 25 | Raft 要求系统在任意时刻最多只有一个 Leader,正常工作期间只有 Leader 和 Followers;算法将时间分为一个个的任期(term),每一个 term 的开始都是 Leader 选举。Raft 算法角色状态转换如下: 26 | 27 | ![Raft 算法角色状态转换](https://s1.ax1x.com/2020/08/03/ad3Eb8.png) 28 | 29 | Follower 只响应其他服务器的请求。如果 Follower 超时没有收到 Leader 的消息,它会成为一个 Candidate 并且开始一次 Leader 选举。收到大多数服务器投票的 Candidate 会成为新的 Leader。Leader 在宕机之前会一直保持 Leader 的状态。 30 | 31 | ![选举时序流](https://s1.ax1x.com/2020/08/03/ad3Qvq.png) 32 | 33 | 在成功选举 Leader 之后,Leader 会在整个 term 内管理整个集群。如果 Leader 选举失败,该 term 就会因为没有 Leader 而结束。Splite Vote 是因为如果同时有两个候选人向大家邀票,这时通过类似加时赛来解决,两个候选者在一段 timeout 比如 300ms 互相不服气的等待以后,因为双方得到的票数是一样的,一半对一半,那么在 300ms 以后,再由这两个候选者发出邀票,这时同时的概率大大降低,那么首先发出邀票的的候选者得到了大多数同意,成为领导者 Leader,而另外一个候选者后来发出邀票时,那些 Follower 选民已经投票给第一个候选者,不能再投票给它,它就成为落选者了,最后这个落选者也成为普通 Follower 一员了。 34 | 35 | ## Raft 与 Multi-Paxos 对比 36 | 37 | Raft 与 Multi-Paxos 有着千丝万缕的关系,下面总结了 Raft 与 Multi-Paxos 的异同。Raft 与 Multi-Paxos 中相似的概念: 38 | 39 | ![Raft 与 Multi-Paxos 对比](https://s1.ax1x.com/2020/08/03/adut5n.png) 40 | 41 | - Raft 的 Leader 即 Multi-Paxos 的 Proposer。 42 | - Raft 的 Term 与 Multi-Paxos 的 Proposal ID 本质上是同一个东西。 43 | - Raft 的 Log Entry 即 Multi-Paxos 的 Proposal。 44 | - Raft 的 Log Index 即 Multi-Paxos 的 Instance ID。 45 | - Raft 的 Leader 选举跟 Multi-Paxos 的 Prepare 阶段本质上是相同的。 46 | - Raft 的日志复制即 Multi-Paxos 的 Accept 阶段。 47 | 48 | Raft 与 Multi-Paxos 的不同: 49 | 50 | ![Raft 与 Multi-Paxos 的不同](https://s1.ax1x.com/2020/08/03/adufxK.png) 51 | 52 | Raft 假设系统在任意时刻最多只有一个 Leader,提议只能由 Leader 发出(强 Leader),否则会影响正确性;而 Multi-Paxos 虽然也选举 Leader,但只是为了提高效率,并不限制提议只能由 Leader 发出(弱 Leader)。强 Leader 在工程中一般使用 Leader Lease 和 Leader Stickiness 来保证: 53 | 54 | - Leader Lease:上一任 Leader 的 Lease 过期后,随机等待一段时间再发起 Leader 选举,保证新旧 Leader 的 Lease 不重叠。 55 | - Leader Stickiness:Leader Lease 未过期的 Follower 拒绝新的 Leader 选举请求。 56 | 57 | Raft 限制具有最新已提交的日志的节点才有资格成为 Leader,Multi-Paxos 无此限制。Raft 在确认一条日志之前会检查日志连续性,若检查到日志不连续会拒绝此日志,保证日志连续性,Multi-Paxos 不做此检查,允许日志中有空洞。Raft 在 AppendEntries 中携带 Leader 的 commit index,一旦日志形成多数派,Leader 更新本地的 commit index 即完成提交,下一条 AppendEntries 会携带新的 commit index 通知其它节点;Multi-Paxos 没有日志连接性假设,需要额外的 commit 消息通知其它节点。 58 | 59 | # Links 60 | 61 | - 建议在 [The Secret Lives of Data](http://thesecretlivesofdata.com/) 查看 Raft 算法的动画演示讲解。 62 | -------------------------------------------------------------------------------- /02~一致性与共识/共识算法/Raft/安全性.md: -------------------------------------------------------------------------------- 1 | # 安全性 2 | 3 | 要保证所有的状态机有一样的状态,单凭复制与选举算法还不够。例如有 3 个节点 A、B、C,如果 A 为主节点期间 C 挂了,此时消息被多数节点(A,B)接收,所以 A 会提交这些日志。此时若 A 挂了,而 C 恢复且被选为主节点,则 A 已经提交的日志会被 C 的日志覆盖,从而导致状态机的状态不一致。 4 | 5 | Raft 增加了如下两条限制以保证安全性: 6 | 7 | - 拥有最新的已提交的 log entry 的 Follower 才有资格成为 Leader。 8 | - Leader 只能推进 commit index 来提交当前 term 的已经复制到大多数服务器上的日志,旧 term 日志的提交要等到提交当前 term 的日志来间接提交(log index 小于 commit index 的日志被间接提交)。 9 | 10 | # 选主的限制 11 | 12 | > 拥有最新的已提交的 log entry 的 Follower 才有资格成为 Leader。 13 | 14 | 在所有的主从结构的一致性算法中,主节点最终都必须包含所有提交的日志。有些算法在从节点不包含所有已提交日志的情况下,依旧允许它当选为主节点,之后从节点会将这些日志同步到主节点上。但是 Raft 采用了简单的方式,只允许那些包含所有已提交日志的节点当选为主节点。 15 | 16 | 注意到节点当选主节点要求得到多数票,同时一个日志被提交的前提条件是它被多数节点接收,综合这两点,说明选举要产生结果,则至少有一个节点在场,它是包含了当前已经提交的所有日志的。 17 | 18 | 因此,Raft 算法在处理要求选举的 RequestVote 消息时做了限制:消息中会携带 Candidate 的 log 消息,而在投票时,Follower 会判断 Candidate 的消息是不是比自己“更新”(下文定义),如果不如自己“新”,则拒绝为该 Candidate 投票。 19 | 20 | Raft 会首先判断两个节点最后一个 log entry 的 term,哪个节点的对应的 term 更大则代表该节点的日志“更新”;如果 term 的大小一致,则谁的 log entry 更多谁就“更新”。注意,加了这个限制后,选出的节点不会是“最新的”,即包含所有日志;但会是足够新的,至少比半数节点更新,而这也意味着它所包含的日志都是可以被提交的(但不一定已经提交)。 21 | 22 | 这个保证是在 RequestVote RPC 中做的,Candidate 在发送 RequestVote RPC 时,要带上自己的最后一条日志的 term 和 log index,其他节点收到消息时,如果发现自己的日志比请求中携带的更新,则拒绝投票。日志比较的原则是,如果本地的最后一条 log entry 的 term 更大,则 term 大的更新,如果 term 一样大,则 log index 更大的更新。 23 | 24 | # 提交前一个 term 的日志 25 | 26 | > Leader 只能推进 commit index 来提交当前 term 的已经复制到大多数服务器上的日志,旧 term 日志的提交要等到提交当前 term 的日志来间接提交(log index 小于 commit index 的日志被间接提交)。 27 | 28 | 这里我们要讨论一个特别的情况。我们知道一个主节点如果发现自己任期(term)内的某条日志已经被存储到了多数节点上,主节点就会提交这条日志。但如果主节点在提交之前就挂了,之后的主节点会尝试把前任未提交的这些日志复制到所有子节点上,但与之前不同,仅仅判断这些日志被复制到多数节点,新的主节点并不能立马提交这些日志,下面举一个反例: 29 | 30 | ![已提交的日志被覆盖](https://s1.ax1x.com/2020/08/06/ag9dc6.png) 31 | 32 | - 在 (a) 时,S1 当选并将日志编号为 2 的日志复制到其它节点上。 33 | - 在 (b) 时,S1 宕机,S5 获得来自 S3 与 S4 的投票,当选为 term 3 的主节点,此时收到来自客户端的消息,写入自己编号为 2 的日志。 34 | - (c) 期间,S5 宕机而 S1 重启完毕,它重新当选为主节点并继续将自己的日志复制给 S3,此时编号为 2 且 term 为 2 的日志已经被复制到多数节点,但它还不能被提交。 35 | - 如果此时 S1 宕机,如 (d) 所示,此时 S5 获得来自 S2 S3 S4 的投票,当选新的主节点,此时它将用自己的编号为 2,term 为 3 的日志覆盖其它节点的日志。 36 | - 而如果 S1 继续存活,且在自己的任期内将某条日志复制到多数节点,如 (e) 所示,则此时 S5 已经不可能继续当选为主节点,因此该日志之前的所有日志均可被提交(包括前任创建的,编号 2 的日志)。 37 | 38 | 上例中的 (c) 和 (d) 说明了,即使前任的日志已经被复制到多数节点上,它依然可能被覆盖。因此 Raft 并不通过计算前任日志的复制次数来判断是否提交这些日志,Raft 只对自己任期内的日志计数并在复制到多数节点时进行提交,且在提交这条日志的同时提交之前的所有日志。 39 | 40 | Raft 算法会出现这个额外的问题,是因为它在复制前任的日志时,会保留前任的 term,而其它一致性算法会为这些日志使用新的 term。Raft 算法的优势在于方便推理日志的形成过程,同时新的主节点需要发送的前任日志数目会更少。 41 | 42 | ## 网络分区容忍 43 | 44 | 根据前文介绍的 Raft 的两大安全性保障,我们能知道 Raft 天然兼容网络分区的情况。 45 | 46 | ![Raft 网络分区](https://s1.ax1x.com/2020/08/06/ag18bQ.png) 47 | 48 | 在开始的时候所有节点的 Leader 是 B,而后发生了网络分区情况,独立的三个节点选择了 C 作为新的 Leader。此时有两个客户端分别设置了不同的值 3 和 8,节点 B 因为无法与绝大部分节点通信,因此属于不可提交状态;而新成组的 3 个节点会进行值的设置。 49 | 50 | 在网络分区被修复后,B 接收到了更高的 Electron Term 因此退化为普通的节点,然后根据 Leader 上最新的日志回滚本地未提交的 Entries。 51 | -------------------------------------------------------------------------------- /02~一致性与共识/共识算法/Raft/日志复制.md: -------------------------------------------------------------------------------- 1 | # 日志复制(Log Replication) 2 | 3 | Leader 选出后,就开始接收客户端的请求。Leader 把请求作为日志条目(Log entries)加入到它的日志中,然后并行的向其他服务器发起 AppendEntries RPC 复制日志条目。当这条日志被复制到大多数服务器上,Leader 将这条日志应用到它的状态机并向客户端返回执行结果。 4 | 5 | ## 日志结构 6 | 7 | Log Replication 分为两个主要步骤:复制(Replication)和 提交(Commit)。当一个节点被选为主节点后,它开始对外提供服务,收到客户端的 command 后,主节点会首先将 command 添加到自己的日志队列中,然后并行地将消息发送给其它所有的节点,在确保消息被安全地复制(下文解释)后,主节点会将该消息提交到状态机中,并返回状态机执行的结果。如果 Follower 挂了或因为网络原因消息丢失了,主节点会不断重试直到所有从节点最终成功复制该消息。 8 | 9 | ![Raft 日志结构示例](https://s1.ax1x.com/2020/08/03/ad89dU.md.png) 10 | 11 | 日志由许多条目(log entry)组成,条目顺序编号。条目包含它生成时节点所在的 term(小方格中上方的数字),以及日志的内容。当一个条目被认为安全地被复制,且提交到状态机时,我们认为它处于“已提交(committed)”状态。 12 | 13 | 是否将一个条目提交到状态机是由主节点决定的。Raft 要保证提交的条目会最终被所有的节点执行。当主节点判断一个条目已经被复制到大多数节点时,就会提交 /Commit 该条目,提交一个条目的同时会提交该条目之前的所有条目,包括那些之前由其它主节点创建的条目(还有些特殊情况下面会提)。主节点会记录当前提交的日志编号 (log index),并在发送心跳时带上该信息,这样其它节点最终会同步提交日志。 14 | 15 | ## 日志同步 16 | 17 | 上面说的是“提交”,那么“复制”是如何进行的?在现实情况下,主从节点的日志可能不一致(例如在消息到达从节点前主节点挂了,而从节点被选为了新的主节点,此时主从节点的日志不一致)。Raft 算法中,主节点需要处理不一致的情况,它要求所有的从节点复制自己的所有日志。 18 | 19 | 要复制所有日志,就要先找到日志开始不一致的位置,如何做到呢?Raft 当主节点接收到新的 command 时,会发送 AppendEntries 让从节点复制日志,不一致的情况也会在这时被处理(AppendEntries 消息同时还兼职作为心跳信息)。某些 Followers 可能没有成功的复制日志,Leader 会无限的重试 AppendEntries RPC 直到所有的 Followers 最终存储了所有的日志条目。下面是日志不一致的示例: 20 | 21 | ![Raft 日志](https://s1.ax1x.com/2020/08/05/ase0aD.png) 22 | 23 | 主节点需要为每个从节点记录一个 nextIndex,作为该从节点下一条要发送的日志的编号。当一个节点刚被选为主节点时,为所有从节点的 nextIndex 初始化自己最大日志编号加 1(如上图示例则为 11)。接着主节点发送 AppendEntries 给从节点,此时从节点会进行一致性检查(Consistency Check)。 24 | 25 | 所谓一致性检查,指的是当主节点发送 AppendEntries 消息通知从节点添加条目时,需要将新条目 A 之前的那个条目 B 的 log index 和 term,这样,当从节点收到消息时,就可以判断自己第 log index 条日志的 term 是否与 B 的 term 相同,如果不相同则拒绝该消息,如果相同则添加条目 A。 26 | 27 | 主节点的消息被某个从节点拒绝后,主节点会将该从节点的 nextIndex 减一再重新发送 AppendEntries 消息。不断重试,最终就能找主从节点日志一致的 log index,并用主节点的新日志覆盖从节点的旧日志。当然,如果从节点接收 AppendEntries 消息后,主节点会将 nextIndex 增加一,且如果当前的最新 log index 大于 nextIndex 则会继续发送消息。 28 | 29 | ## 同步保证 30 | 31 | Raft 日志同步保证如下两点: 32 | 33 | - 如果不同日志中的两个条目有着相同的索引和任期号,则它们所存储的命令是相同的。 34 | - 如果不同日志中的两个条目有着相同的索引和任期号,则它们之前的所有条目都是完全一样的。 35 | 36 | 第一条特性源于 Leader 在一个 term 内在给定的一个 log index 最多创建一条日志条目,同时该条目在日志中的位置也从来不会改变。第二条特性源于 AppendEntries 的一个简单的一致性检查。当发送一个 AppendEntries RPC 时,Leader 会把新日志条目紧接着之前的条目的 log index 和 term 都包含在里面。如果 Follower 没有在它的日志中找到 log index 和 term 都相同的日志,它就会拒绝新的日志条目。 37 | 38 | 一般情况下,Leader 和 Followers 的日志保持一致,因此 AppendEntries 一致性检查通常不会失败。然而,Leader 崩溃可能会导致日志不一致:旧的 Leader 可能没有完全复制完日志中的所有条目。 39 | 40 | ![Leader 和 Followers 上日志不一致](https://s1.ax1x.com/2020/08/06/acvqaR.png) 41 | 42 | 上图阐述了一些 Followers 可能和新的 Leader 日志不同的情况。一个 Follower 可能会丢失掉 Leader 上的一些条目,也有可能包含一些 Leader 没有的条目,也有可能两者都会发生。丢失的或者多出来的条目可能会持续多个任期。Leader 通过强制 Followers 复制它的日志来处理日志的不一致,Followers 上的不一致的日志会被 Leader 的日志覆盖。 43 | 44 | Leader 为了使 Followers 的日志同自己的一致,Leader 需要找到 Followers 同它的日志一致的地方,然后覆盖 Followers 在该位置之后的条目。Leader 会从后往前试,每次 AppendEntries 失败后尝试前一个日志条目,直到成功找到每个 Follower 的日志一致位点,然后向后逐条覆盖 Followers 在该位置之后的条目。 45 | 46 | # 日志压缩 47 | 48 | 在实际的系统中,不能让日志无限增长,否则系统重启时需要花很长的时间进行回放,从而影响可用性。Raft 采用对整个系统进行 snapshot 来解决,snapshot 之前的日志都可以丢弃。每个副本独立的对自己的系统状态进行 snapshot,并且只能对已经提交的日志记录进行 snapshot。 49 | 50 | Snapshot 中包含以下内容: 51 | 52 | - 日志元数据。最后一条已提交的 log entry 的 log index 和 term。这两个值在 snapshot 之后的第一条 log entry 的 AppendEntries RPC 的完整性检查的时候会被用上。 53 | - 系统当前状态。 54 | 55 | 当 Leader 要发给某个日志落后太多的 Follower 的 log entry 被丢弃,Leader 会将 snapshot 发给 Follower。或者当新加进一台机器时,也会发送 snapshot 给它。发送 snapshot 使用 InstalledSnapshot RPC。 56 | 57 | 做 snapshot 既不要做的太频繁,否则消耗磁盘带宽,也不要做的太不频繁,否则一旦节点重启需要回放大量日志,影响可用性。推荐当日志达到某个固定的大小做一次 snapshot。做一次 snapshot 可能耗时过长,会影响正常日志同步。可以通过使用 copy-on-write 技术避免 snapshot 过程影响正常日志同步。 58 | -------------------------------------------------------------------------------- /02~一致性与共识/共识算法/Raft/选举与成员变更.md: -------------------------------------------------------------------------------- 1 | # Leader 选举 2 | 3 | 节点启动时,默认处于 Follower 的状态,所以开始时所有节点均是 Follower,那么什么时候触发选主呢?Raft 用“心跳”的方式来保持主从节点的联系,如果长时间没有收到主节点的心跳,则开始选主。这里会涉及到两个时间: 4 | 5 | - 心跳间隔,主节点隔多长时间发送心跳信息 6 | - 等待时间(election timeout),如果超过这个时间仍然没有收到心跳,则认为主节点宕机。一般每个节点各自在 150 ~ 300ms 间随机取值。 7 | 8 | 当一个节点在等待时间内没有收到主节点的心跳信息,它首先将自己保存的 term 增加 1 并进入 Candidate 状态。此时它会先投票给自己,然后并行发送 RequestVote 消息给其它所有节点,请求这些节点投票给自己。然后等待直到以下 3 种情形之一发生: 9 | 10 | - 赢得了多数的选票,成功选举为 Leader;Candidate 节点需要收到集群中与自己 term 相同的所有节点中大于一半的票数(当然如果节点 term 比自己大,是不会理睬自己的选举消息的)。节点投票时会采取先到先得的原则,对于某个 term,最多投出一票(后面还会再对投票加一些限制)。这样能保证某个 term 中,最多只会产生一个 leader。当一个 Candidate 变成主节点后,它会向其它所有节点发送心跳信息,这样其它的 Candidate 也会变成 Follower。 11 | 12 | - 收到了 Leader 的消息,表示有其它服务器已经抢先当选了 Leader;在等待投票的过程中,Candidate 收到其它主节点的心跳信息(只有主节点才会向其它节点发心跳),且信息中包含的 term 大于等于自己的 term,则当前节点放弃竞选,进入 Follower 状态。当然,如前所说,如果心跳中的 term 小于自己,则不予理会。 13 | 14 | - 没有服务器赢得多数的选票,Leader 选举失败,等待选举时间超时后发起下一次选举。一般发生在多个 Follower 同时触发选举,而各节点的投票被分散了,导致没有 Candidate 能得到多数票。超过投票的等待时间后,节点触发新一轮选举。理论上,选举有可能永远平票,Raft 中由于各个节点的超时时间是随机的,实际上平票不太会永远持续下去。 15 | 16 | ![Leader 选举过程](https://s1.ax1x.com/2020/08/03/ad3wx1.png) 17 | 18 | Leader 后,Leader 通过定期向所有 Followers 发送心跳信息维持其统治。若 Follower 一段时间未收到 Leader 的心跳则认为 Leader 可能已经挂了,再次发起 Leader 选举过程。Raft 保证选举出的 Leader 上一定具有最新的已提交的日志。 19 | 20 | # 成员变更 21 | 22 | 成员变更是在集群运行过程中副本发生变化,如增加/减少副本数、节点替换等。成员变更也是一个分布式一致性问题,既所有服务器对新成员达成一致。但是成员变更又有其特殊性,因为在成员变更的一致性达成的过程中,参与投票的进程会发生变化。 23 | 24 | 如果将成员变更当成一般的一致性问题,直接向 Leader 发送成员变更请求,Leader 复制成员变更日志,达成多数派之后提交,各服务器提交成员变更日志后从旧成员配置(Cold)切换到新成员配置(Cnew)。 25 | 26 | ## 双多数派问题 27 | 28 | 一种方法是先停止所有节点,修改配置增加新的节点,再重启所有节点,但是这样服务起停时就会中断服务,同时也可能增加人为操作失误的风险。另一种方法配置好新的节点直接加入集群,这样也会出问题:在某个时刻使用不同配置的两部分节点可能会各自选出一个主节点。因为各个服务器提交成员变更日志的时刻可能不同,造成各个服务器从旧成员配置(Cold)切换到新成员配置(Cnew)的时刻不同。成员变更不能影响服务的可用性,但是成员变更过程的某一时刻,可能出现在 Cold 和 Cnew 中同时存在两个不相交的多数派,进而可能选出两个 Leader,形成不同的决议,破坏安全性。 29 | 30 | 如下图: 31 | 32 | ![双多数派问题](https://s1.ax1x.com/2020/08/06/agy7PP.png) 33 | 34 | 图中绿色为旧的配置,蓝色为新的配置,在中间的某个时刻,Server 1/2/3 可能会选出一个主节点,而 Server 3/4/5 可能会选出另一个,从而破坏了一致性。 35 | 36 | ## 两阶段的成员变更 37 | 38 | 由于成员变更的这一特殊性,成员变更不能当成一般的一致性问题去解决。为了解决这一问题,Raft 提出了两阶段的成员变更方法。集群先从旧成员配置 Cold 切换到一个过渡成员配置,称为共同一致(joint consensus),共同一致是旧成员配置 Cold 和新成员配置 Cnew 的组合 Cold U Cnew,一旦共同一致 Cold U Cnew 被提交,系统再切换到新成员配置 Cnew。 39 | 40 | ![两阶段成员变更](https://s1.ax1x.com/2020/08/06/ag6JZd.md.png) 41 | 42 | Raft 两阶段成员变更过程如下: 43 | 44 | 1. Leader 收到成员变更请求从 Cold 切成 Cnew; 45 | 2. Leader 在本地生成一个新的 log entry,其内容是 Cold∪Cnew,代表当前时刻新旧成员配置共存,写入本地日志,同时将该 log entry 复制至 Cold∪Cnew 中的所有副本。在此之后新的日志同步需要保证得到 Cold 和 Cnew 两个多数派的确认; 46 | 3. Follower 收到 Cold∪Cnew 的 log entry 后更新本地日志,并且此时就以该配置作为自己的成员配置; 47 | 4. 如果 Cold 和 Cnew 中的两个多数派确认了 Cold U Cnew 这条日志,Leader 就提交这条 log entry; 48 | 5. 接下来 Leader 生成一条新的 log entry,其内容是新成员配置 Cnew,同样将该 log entry 写入本地日志,同时复制到 Follower 上; 49 | 6. Follower 收到新成员配置 Cnew 后,将其写入日志,并且从此刻起,就以该配置作为自己的成员配置,并且如果发现自己不在 Cnew 这个成员配置中会自动退出; 50 | 7. Leader 收到 Cnew 的多数派确认后,表示成员变更成功,后续的日志只要得到 Cnew 多数派确认即可。Leader 给客户端回复成员变更执行成功。 51 | 52 | 异常分析: 53 | 54 | - 如果 Leader 的 Cold U Cnew 尚未推送到 Follower,Leader 就挂了,此后选出的新 Leader 并不包含这条日志,此时新 Leader 依然使用 Cold 作为自己的成员配置。 55 | - 如果 Leader 的 Cold U Cnew 推送到大部分的 Follower 后就挂了,此后选出的新 Leader 可能是 Cold 也可能是 Cnew 中的某个 Follower。 56 | - 如果 Leader 在推送 Cnew 配置的过程中挂了,那么同样,新选出来的 Leader 可能是 Cold 也可能是 Cnew 中的某一个,此后客户端继续执行一次改变配置的命令即可。 57 | - 如果大多数的 Follower 确认了 Cnew 这个消息后,那么接下来即使 Leader 挂了,新选出来的 Leader 肯定位于 Cnew 中。 58 | 59 | 两阶段成员变更比较通用且容易理解,但是实现比较复杂,同时两阶段的变更协议也会在一定程度上影响变更过程中的服务可用性,因此我们期望增强成员变更的限制,以简化操作流程。两阶段成员变更,之所以分为两个阶段,是因为对 Cold 与 Cnew 的关系没有做任何假设,为了避免 Cold 和 Cnew 各自形成不相交的多数派选出两个 Leader,才引入了两阶段方案。 60 | 61 | 如果增强成员变更的限制,假设 Cold 与 Cnew 任意的多数派交集不为空,这两个成员配置就无法各自形成多数派,那么成员变更方案就可能简化为一阶段。那么如何限制 Cold 与 Cnew,使之任意的多数派交集不为空呢?方法就是每次成员变更只允许增加或删除一个成员。可从数学上严格证明,只要每次只允许增加或删除一个成员,Cold 与 Cnew 不可能形成两个不相交的多数派。 62 | 63 | 一阶段成员变更: 64 | 65 | - 成员变更限制每次只能增加或删除一个成员(如果要变更多个成员,连续变更多次)。 66 | - 成员变更由 Leader 发起,Cnew 得到多数派确认后,返回客户端成员变更成功。 67 | - 一次成员变更成功前不允许开始下一次成员变更,因此新任 Leader 在开始提供服务前要将自己本地保存的最新成员配置重新投票形成多数派确认。 68 | - Leader 只要开始同步新成员配置,即可开始使用新的成员配置进行日志同步。 69 | -------------------------------------------------------------------------------- /02~一致性与共识/共识算法/ZAB/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/DistributedSystem-Notes/d5c81c808d80e3697e264031c4ec5eb29c40f7d3/02~一致性与共识/共识算法/ZAB/README.md -------------------------------------------------------------------------------- /02~一致性与共识/共识算法/算法设计/README.md: -------------------------------------------------------------------------------- 1 | # 算法模型 2 | 3 | 从系统模型的角度,我们又可以将分布式共识算法(Distributed Consensus Algorithms)看做解决如何在多个节点之间复制 deterministic state machine 的问题,即构建多副本状态机模型(Replicated State Machine),实现高可用和强一致;这里所谓的状态机即代表了任意的服务,譬如数据库、文件服务器、锁服务器等。 4 | 5 | ![多副本状态机](https://s1.ax1x.com/2020/08/01/aGGTqH.png) 6 | 7 | 每个节点都会包含一个状态机实例与日志,但是我们希望呈现给客户端使用者的仿佛只有单个可信赖的、能容错的状态机;提供的服务的可用性与准确性不会受到集群中节点的失败而影响。每个状态机都能够从其自身的日志中读取指令,譬如以 Hash Table 为状态机实例的话,那么存放在日志中的指令就会是 `set x to 3` 这样子的。共识算法也就是为了保证这些节点上的日志内容的一致性,共识算法必须保证如果某个节点上的第 N 条指令是 `set x to 3`,那么其他所有节点上不会出现该序列位不一样的情况。最终,所有的状态机都会以相同顺序执行指令,并得到一致的状态序列。 8 | 9 | ![状态机流程](https://s1.ax1x.com/2020/08/01/aGJtyD.png) 10 | 11 | 上述介绍可以形式化如下:一个或多个节点可以提议(propose)某些值,而共识算法决定(decides)采用其中的某个值。在这种形式下,共识算法必须满足以下性质: 12 | 13 | - 一致同意(Uniform agreement):没有两个节点的决定不同。 14 | - 完整性(Integrity):没有节点决定两次,一致同意和完整性属性定义了共识的核心思想:所有人都决定了相同的结果,一旦决定了,你就不能改变主意。 15 | - 有效性(Validity):如果一个节点决定了值 v,则 v 由某个节点所提议。有效性属性主要是为了排除平凡的解决方案:例如,无论提议了什么值,你都可以有一个始终决定值为 null 的算法;该算法满足一致同意和完整性属性,但不满足有效性属性。 16 | - 终止(Termination):由所有未崩溃的节点来最终决定值。如果你不关心容错,那么满足前三个属性很容易:你可以将一个节点硬编码为“独裁者”,并让该节点做出所有的决定。但如果该节点失效,那么系统就无法再做出任何决定。事实上,这就是我们在两阶段提交的情况中所看到的:如果协调者失效,那么存疑的参与者就无法决定提交还是中止。终止属性正式形成了容错的思想。它实质上说的是,一个共识算法不能简单地永远闲坐着等死;换句话说,它必须取得进展。即使部分节点出现故障,其他节点也必须达成一项决定。 17 | 18 | 共识的系统模型假设,当一个节点“崩溃”时,它会突然消失而且永远不会回来。在这个系统模型中,任何需要等待节点恢复的算法都不能满足终止属性。特别是,2PC 不符合终止属性的要求。当然如果所有的节点都崩溃了,没有一个在运行,那么所有算法都不可能决定任何事情。算法可以容忍的失效数量是有限的:事实上可以证明,任何共识算法都需要至少占总体多数(majority)的节点正确工作,以确保终止属性。多数可以安全地组成法定人数。 19 | 20 | 因此终止属性取决于一个假设,不超过一半的节点崩溃或不可达。然而即使多数节点出现故障或存在严重的网络问题,绝大多数共识的实现都能始终确保安全属性得到满足一致同意,完整性和有效性。因此,大规模的中断可能会阻止系统处理请求,但是它不能通过使系统做出无效的决定来破坏共识系统。大多数共识算法假设不存在拜占庭式错误,正如在“拜占庭式错误”一节中所讨论的那样。也就是说,如果一个节点没有正确地遵循协议(例如,如果它向不同节点发送矛盾的消息),它就可能会破坏协议的安全属性。克服拜占庭故障,稳健地达成共识是可能的,只要少于三分之一的节点存在拜占庭故障。但我们没有地方在本书中详细讨论这些算法了。 21 | 22 | # 共识算法和全序广播 23 | 24 | 最著名的容错共识算法是视图戳复制(VSR, viewstamped replication),Paxos,Raft 以及 Zab,这些算法之间有不少相似之处,但它们并不相同。大多数这些算法实际上并不直接使用这里描述的形式化模型(提议与决定单个值,一致同意,完整性,有效性和终止属性)。取而代之的是,它们决定了值的顺序(sequence),这使它们成为全序广播算法。 25 | 26 | 请记住,全序广播要求将消息按照相同的顺序,恰好传递一次,准确传送到所有节点。如果仔细思考,这相当于进行了几轮共识:在每一轮中,节点提议下一条要发送的消息,然后决定在全序中下一条要发送的消息。所以,全序广播相当于重复进行多轮共识(每次共识决定与一次消息传递相对应): 27 | 28 | - 由于一致同意属性,所有节点决定以相同的顺序传递相同的消息。 29 | - 由于完整性属性,消息仅发送一次而不会重复。 30 | - 由于有效性属性,消息不会被损坏,也不能凭空编造。 31 | - 由于终止属性,消息不会丢失。 32 | 33 | 视图戳复制,Raft 和 Zab 直接实现了全序广播,因为这样做比重复**一次一值(one value a time)**的共识更高效。在 Paxos 的情况下,这种优化被称为 Multi-Paxos。 34 | 35 | # 单领导者复制和共识 36 | 37 | 在单领导复制中,它将所有的写入操作都交给主库,并以相同的顺序将它们应用到从库,从而使副本保持在最新状态。这实际上不就是一个全序广播吗,为何我们并没有导致共识问题呢?答案取决于如何选择领导者。如果主库是由运维人员手动选择和配置的,那么你实际上拥有一种独裁类型的“共识算法”:只有一个节点被允许接受写入(即决定写入复制日志的顺序),如果该节点发生故障,则系统将无法写入,直到运维手动配置其他节点作为主库。这样的系统在实践中可以表现良好,但它无法满足共识的终止属性,因为它需要人为干预才能取得进展。 38 | 39 | # 时代编号和法定人数 40 | 41 | 一些数据库会自动执行领导者选举和故障切换,如果旧主库失效,会提拔一个从库为新主库,这使我们向容错的全序广播更进一步,从而达成共识。但是还有一个问题。我们之前曾经讨论过脑裂的问题,并且说过所有的节点都需要同意是谁领导,否则两个不同的节点都会认为自己是领导者,从而导致数据库进入不一致的状态。因此,选出一位领导者需要共识。但如果这里描述的共识算法实际上是全序广播算法,并且全序广播就像单主复制,而单主复制需要一个领导者,那么这样看来,要选出一个领导者,我们首先需要一个领导者。要解决共识问题,我们首先需要解决共识问题。我们如何跳出这个先有鸡还是先有蛋的问题? 42 | 43 | 迄今为止所讨论的所有共识协议,在内部都以某种形式使用一个领导者,但它们并不能保证领导者是独一无二的。相反,它们可以做出更弱的保证:协议定义了一个时代编号(epoch number)(在 Paxos 中称为投票编号(ballot number),视图戳复制中的视图编号(view number),以及 Raft 中的任期号码(term number)),并确保在每个时代中,领导者都是唯一的。 44 | 45 | 每次当现任领导被认为挂掉的时候,节点间就会开始一场投票,以选出一个新领导。这次选举被赋予一个递增的时代编号,因此时代编号是全序且单调递增的。如果两个不同的时代的领导者之间出现冲突(也许是因为前任领导者实际上并未死亡),那么带有更高时代编号的领导说了算。在任何领导者被允许决定任何事情之前,必须先检查是否存在其他带有更高时代编号的领导者,它们可能会做出相互冲突的决定。领导者如何知道自己没有被另一个节点赶下台?一个节点不一定能相信自己的判断,因为只有节点自己认为自己是领导者,并不一定意味着其他节点接受它作为它们的领导者。 46 | 47 | 相反,它必须从**法定人数(quorum)**的节点中获取选票,对领导者想要做出的每一个决定,都必须将提议值发送给其他节点,并等待法定人数的节点响应并赞成提案。法定人数通常(但不总是)由多数节点组成。只有在没有意识到任何带有更高时代编号的领导者的情况下,一个节点才会投票赞成提议。因此,我们有两轮投票:第一次是为了选出一位领导者,第二次是对领导者的提议进行表决。关键的洞察在于,这两次投票的法定人群必须相互重叠(overlap):如果一个提案的表决通过,则至少得有一个参与投票的节点也必须参加过最近的领导者选举。因此,如果在一个提案的表决过程中没有出现更高的时代编号。那么现任领导者就可以得出这样的结论:没有发生过更高时代的领导选举,因此可以确定自己仍然在领导。然后它就可以安全地对提议值做出决定。 48 | 49 | 这一投票过程表面上看起来很像两阶段提交。最大的区别在于,2PC 中协调者不是由选举产生的,而且 2PC 则要求所有参与者都投赞成票,而容错共识算法只需要多数节点的投票。而且,共识算法还定义了一个恢复过程,节点可以在选举出新的领导者之后进入一个一致的状态,确保始终能满足安全属性。这些区别正是共识算法正确性和容错性的关键。 50 | -------------------------------------------------------------------------------- /02~一致性与共识/共识算法/算法设计/算法对比.md: -------------------------------------------------------------------------------- 1 | # Paxos 2 | 3 | Paxos 达成一个决议至少需要两个阶段(Prepare 阶段和 Accept 阶段)。 4 | 5 | ![Paxos 多阶段描述](https://s1.ax1x.com/2020/08/02/atMCSU.jpg) 6 | 7 | - Prepare 阶段的作用:争取提议权,争取到了提议权才能在 Accept 阶段发起提议,否则需要重新争取;学习之前已经提议的值。 8 | - Accept 阶段使提议形成多数派,提议一旦形成多数派则决议达成,可以开始学习达成的决议。Accept 阶段若被拒绝需要重新走 Prepare 阶段。 9 | 10 | # Multi-Paxos 11 | 12 | Basic Paxos 达成一次决议至少需要两次网络来回,并发情况下可能需要更多,极端情况下甚至可能形成活锁,效率低下,Multi-Paxos 正是为解决此问题而提出。 13 | 14 | ![Multi-Paxos](https://s1.ax1x.com/2020/08/03/aduEHH.md.png) 15 | 16 | Multi-Paxos 选举一个 Leader,提议由 Leader 发起,没有竞争,解决了活锁问题。提议都由 Leader 发起的情况下,Prepare 阶段可以跳过,将两阶段变为一阶段,提高效率。Multi-Paxos 并不假设唯一 Leader,它允许多 Leader 并发提议,不影响安全性,极端情况下退化为 Basic Paxos。 17 | 18 | Multi-Paxos 与 Basic Paxos 的区别并不在于 Multi(Basic Paxos 也可以 Multi),只是在同一 Proposer 连续提议时可以优化跳过 Prepare 直接进入 Accept 阶段,仅此而已。 19 | 20 | # Raft 21 | 22 | 不同于 Paxos 直接从分布式一致性问题出发推导出来,Raft 则是从多副本状态机的角度提出,使用更强的假设来减少需要考虑的状态,使之变的易于理解和实现。复制状态机的想法是将服务器看成一个状态机,而一致性算法的目的是让多台服务器/状态机能够计算得到相同的状态,同时,如果有部分机器宕机,集群作为一个整体依然能继续工作。复制状态机一般通过复制日志(replicated log)来实现,如下图: 23 | 24 | ![复制状态机](https://s1.ax1x.com/2020/08/06/agwY9g.png) 25 | 26 | 服务器会将客户端发来的命令存成日志,日志是有序的。而服务器状态机执行命令的结果是确定的,这样如果每台服务器的状态机执行的命令是相同的,状态机最终的状态也会是相同的,输出的结果也会是相同的。而如何保证不同服务器间的日志是一样的呢?这就是其中的“一致性模块”的工作了。一致性模块(consensus module)在收到客户端的命令时(②),一方面要将命令添加到自己的日志队列中,同时需要与其它服务器的一致性模块沟通,确保所有的服务器将最终拥有相同的日志,即使有些服务器可能挂了。实践中至少需要“大多数(大于一半)”服务器同步了命令才认为同步成功了。 27 | 28 | ## Raft vs Paxos 29 | 30 | Paxos, 我们首先要限制必须是 Basic Paxos, 否则没有争论的意义. Basic Paxos 本身是赤裸裸的, 限制少, 灵活, 因为它是基础中的基础. 也正因为太基础了, 所以脱离群众, 离真正实用太远. 这也是为什么这么多年, 业界没有一个真正意义上的开源的 Paxos 编程语言库。 31 | 32 | Raft 是这么多年, 对 Paxos 工程实践的总结和提炼, 以学术研究(论文)的方式加以证明, 并提供了工程指导. 所以, 这才是为什么有那么多的 Raft 开源库, 而大家的代码结构又大同小异的原因. 因为, 幸福的家庭都是相似的, 不幸的家庭各有各的不同。 33 | 34 | 我总结一下, Paxos 和 Raft 的争议点在有哪些, 这些争议点是职责划分的问题, 你很快就会发现. 35 | 36 | 1. 单主还是多主 37 | 38 | "多主"是很多人不选择 Raft 的原因(没什么所谓选择不选择 Paxos, Paxos 就是基础). 一是多写入点, 客户端可以随机选取任何一台服务器来接收请求, 所以, 客户端的代码非常简单, 配置服务器的 ip:port 列表, 用随机算法或者 round robin 算法选一台创建 socket 连接即可. 二是故障恢复时间, Paxos 把故障恢复隐含到了每一次请求当中, 不像 Raft 那样明确的划分职责, 独立出一个选主过程. 独立的选主过程占用独立的时间片, 阻塞正常请求, 所以理论上要增加故障时间. 39 | 40 | 但是, Raft 当然可以优化成在每一次请求都选主, 工程实践上没问题, 但是, 这不就成了 Basic Paxos 了吗? 所以, 没人这么做. 大多数情况下就是这样的, Paxos 加了限制就成了 Raft, 而 Raft 做了优化就变成了 Paxos. 向谁靠拢的选择而已. 41 | 42 | 2. 顺序提交还是乱序提交 43 | 44 | 这是争论最多的地方. 事实上, 一个系统必然有乱序(并发)的地方, 同时也会存在顺序(串行)的地方, 没有任何一个大型的系统只包含并发或者只包含串行, 不可能, 我在工程上没遇见过这样的系统. 问题就在于, 你想把并发(岔路口)开在哪? 45 | 46 | 举一个例子, 网络编程中, 你可以在 accept 之后就启动线程, 每个线程处理一个 socket, 也就是你把并发的岔路口开在了这里. 你当然也可以用 IO 多路复用(如 epoll), 在一个线程中顺序地(但不阻塞)地读取 socket, 然后在读完请求之后, 启动线程处理请求, 也就是, 你把并发的岔路开在了那里. 47 | 48 | Paxos vs Raft 就是这样的例子, Raft 认为把串行的部分交给我, 然后你(状态机)再并发. 但是用 Paxos 的人认为, 关于是串行还并行, 应该由我(状态机)来决定, 共识算法没必要加这个限制. 孰优孰劣? 任何一个理性和聪明的人都能得出答案. 49 | 50 | 用 Paxos 的人, 希望自己把控更多的东西, 所以 Paxos 非常薄, 薄得几乎不存在, 也就没有所谓的 Paxos 库了, 因为它的职责太少, 以致于根本不值得独立成一个库. 用 Raft 的人相反, 把更多的职责加给 Raft, 不重新发明轮子. 51 | 52 | # EPaxos 53 | 54 | EPaxos(Egalitarian Paxos)于 SOSP'13 提出,比 Raft 还稍早一些,但 Raft 在工业界大行其道的时间里,EPaxos 却长期无人问津,直到最近,EPaxos 开始被工业界所关注。EPaxos 是一个 Leaderless 的一致性算法,任意副本均可提交日志,通常情况下,一次日志提交需要一次或两次网络来回。EPaxos 无 Leader 选举开销,一个副本不可用可立即访问其他副本,具有更高的可用性。各副本负载均衡,无 Leader 瓶颈,具有更高的吞吐量。客户端可选择最近的副本提供服务,在跨 AZ 跨地域场景下具有更小的延迟。 55 | 56 | 不同于 Paxos 和 Raft,事先对所有 Instance 编号排序,然后再对每个 Instance 的值达成一致。EPaxos 不事先规定 Instance 的顺序,而是在运行时动态决定各 Instance 之间的顺序。EPaxos 不仅对每个 Instance 的值达成一致,还对 Instance 之间的相对顺序达成一致。EPaxos 将不同 Instance 之间的相对顺序也做为一致性问题,在各个副本之间达成一致,因此各个副本可并发地在各自的 Instance 中发起提议,在这些 Instance 的值和相对顺序达成一致后,再对它们按照相对顺序重新排序,最后按顺序应用到状态机。 57 | 58 | 从图论的角度看,日志是图的结点,日志之间的顺序是图的边,EPaxos 对结点和边分别达成一致,然后使用拓扑排序,决定日志的顺序。图中也可能形成环路,EPaxos 需要处理循环依赖的问题。EPaxos 引入日志冲突的概念(与 Parallel Raft 类似,与并发冲突不是一个概念),若两条日志之间没有冲突(例如访问不同的 key),则它们的相对顺序无关紧要,因此 EPaxos 只处理有冲突的日志之间的相对顺序。 59 | 60 | 若并发提议的日志之间没有冲突,EPaxos 只需要运行 PreAccept 阶段即可提交(Fast Path),否则需要运行 Accept 阶段才能提交(Slow Path)。 61 | 62 | ![PreAccept](https://s1.ax1x.com/2020/08/03/adKkR0.png) 63 | 64 | PreAccept 阶段尝试将日志以及与其它日志之间的相对顺序达成一致,同时维护该日志与其它日志之间的冲突关系,如果运行完 PreAccept 阶段,没有发现该日志与其它并发提议的日志之间有冲突,则该日志以及与其它日志之间的相对顺序已经达成一致,直接发送异步的 Commit 消息提交;否则如果发现该日志与其它并发提议的日志之间有冲突,则日志之间的相对顺序还未达成一致,需要运行 Accept 阶段将冲突依赖关系达成多数派,再发送 Commit 消息提交。 65 | 66 | ![PreAccept](https://s1.ax1x.com/2020/08/03/adKeLF.png) 67 | 68 | EPaxos 的 Fast Path Quorum 为 2F,可优化至 `F + [ (F + 1) / 2 ]`,在 3 副本和 5 副本时,与 Paxos、Raft 一致。Slow Path 为 Paxos Accept 阶段,Quorum 固定为 F + 1。EPaxos 还有一个主动 Learn 的算法,在恢复的时候可用来追赶日志,这里就不做具体的介绍了,感兴趣的可以看论文。 69 | 70 | # 算法对比 71 | 72 | ## 可理解性 73 | 74 | 众所周知,Paxos 是出了名的晦涩难懂,不仅难以理解,更难以实现。而 Raft 则以可理解性和易于实现为目标,Raft 的提出大大降低了使用分布式一致性的门槛,将分布式一致性变的大众化、平民化,因此当 Raft 提出之后,迅速得到青睐,极大地推动了分布式一致性的工程应用。 75 | 76 | EPaxos 的提出比 Raft 还早,但却长期无人问津,很大一个原因就是 EPaxos 实在是难以理解。EPaxos 基于 Paxos,但却比 Paxos 更难以理解,大大地阻碍了 EPaxos 的工程应用。不过,是金子总会发光的,EPaxos 因着它独特的优势,终于被人们发现,具有广阔的前景。 77 | 78 | ## 效率 79 | 80 | 从 Paxos 到 Raft 再到 EPaxos,效率有没有提升呢?我们主要从负载均衡、消息复杂度、Pipeline 以及并发处理几个方面来对比 Multi-Paxos、Raft 和 EPaxos。 81 | 82 | ### 负载均衡 83 | 84 | Multi-Paxos 和 Raft 的 Leader 负载更高,各副本之间负载不均衡,Leader 容易成为瓶颈,而 EPaxos 无需 Leader,各副本之间负载完全均衡。 85 | 86 | ### 消息复杂度 87 | 88 | Multi-Paxos 和 Raft 选举出 Leader 之后,正常只需要一次网络来回就可以提交一条日志,但 Multi-Paxos 需要额外的异步 Commit 消息提交,Raft 只需要推进本地的 commit index,不使用额外的消息,EPaxos 根据日志冲突情况需要一次或两次网络来回。因此消息复杂度,Raft 最低,Paxos 其次,EPaxos 最高。 89 | 90 | ### Pipeline 91 | 92 | 我们将 Pipeline 分为顺序 Pipeline 和乱序 Pipeline。Multi-Paxos 和 EPaxos 支持乱序 Pipeline,Raft 因为日志连续性假设,只支持顺序 Pipeline。但 Raft 也可以实现乱序 Pipeline,只需要在 Leader 上给每个 Follower 维护一个类似于 TCP 的滑动窗口,对应每个 Follower 上维护一个接收窗口,允许窗口里面的日志不连续,窗口外面是已经连续的日志,日志一旦连续则向前滑动窗口,窗口里面可乱序 Pipeline。 93 | 94 | ### 并发处理 95 | 96 | Multi-Paxos 沿用 Paxos 的策略,一旦发现并发冲突则回退重试,直到成功;Raft 则使用强 Leader 来避免并发冲突,Follwer 不与 Leader 竞争,避免了并发冲突;EPaxos 则直面并发冲突问题,将冲突依赖也做为一致性问题对待,解决并发冲突。Paxos 是冲突回退,Raft 是冲突避免,EPaxos 是冲突解决。Paxos 和 Raft 的日志都是线性的,而 EPaxos 的日志是图状的,因此 EPaxos 的并行性更好,吞吐量也更高。 97 | 98 | ## 可用性 99 | 100 | EPaxos 任意副本均可提供服务,某个副本不可用了可立即切换到其它副本,副本失效对可用性的影响微乎其微;而 Multi-Paxos 和 Raft 均依赖 Leader,Leader 不可用了需要重新选举 Leader,在新 Leader 未选举出来之前服务不可用。显然 EPaxos 的可用性比 Multi-Paxos 和 Raft 更好,但 Multi-Paxos 和 Raft 比谁的可用性更好呢。 101 | 102 | Raft 是强 Leader,Follower 必须等旧 Leader 的 Lease 到期后才能发起选举,Multi-Paxos 是弱 Leader,Follwer 可以随时竞选 Leader,虽然会对效率造成一定影响,但在 Leader 失效的时候能更快的恢复服务,因此 Multi-Paxos 比 Raft 可用性更好。 103 | 104 | ## 适用场景 105 | 106 | EPaxos 更适用于跨 AZ 跨地域场景,对可用性要求极高的场景,Leader 容易形成瓶颈的场景。Multi-Paxos 和 Raft 本身非常相似,适用场景也类似,适用于内网场景,一般的高可用场景,Leader 不容易形成瓶颈的场景。 107 | -------------------------------------------------------------------------------- /02~一致性与共识/分布式时钟/README.md: -------------------------------------------------------------------------------- 1 | # 分布式时钟 2 | 3 | 对于串行的事务来说,很简单的就是跟着时间的脚步走就可以,先来后到的发生。分布式世界里面,我们要协调不同节点之间的先来后到关系,但是不同节点本身承认的时间又各执己见,于是我们创造了网络时间协议(NTP)试图来解决不同节点之间的标准时间,但是 NTP 本身表现并不如人意,所以我们又构造除了逻辑时钟,最后改进为向量时钟。 4 | 5 | # NTP 6 | 7 | NTP 的一些缺点,无法完全满足分布式下并发任务的协调问题,包括节点间时间不同步,硬件时钟漂移,线程可能休眠,操作系统休眠,硬件休眠等。 8 | 9 | ![](https://ww1.sinaimg.cn/large/007rAy9hgy1g29ec73ojnj30cv060js9.jpg) 10 | 11 | # 逻辑时钟 12 | 13 | 定义事件先来后到,`t’ = max(t, t_msg + 1)` 14 | 15 | ![](https://ww1.sinaimg.cn/large/007rAy9hgy1g29ec73ojnj30cv060js9.jpg) 16 | 17 | # 向量时钟 18 | 19 | `t_i’ = max(t_i, t_msg_i)` 20 | 21 | # 原子时钟 22 | -------------------------------------------------------------------------------- /02~一致性与共识/分布式时钟/序列号/全序广播.md: -------------------------------------------------------------------------------- 1 | # 全序广播 2 | 3 | 如果你的程序只运行在单个 CPU 核上,那么定义一个操作全序是很容易的:可以简单地就是 CPU 执行这些操作的顺序。但是在分布式系统中,让所有节点对同一个全局操作顺序达成一致可能相当棘手。我们讨论过按时间戳或序列号进行排序,但发现它还不如单主复制给力(如果你使用时间戳排序来实现唯一性约束,而且不能容忍任何错误)。 4 | 5 | 单主复制通过选择一个节点作为主库来确定操作的全序,并在主库的单个 CPU 核上对所有操作进行排序。接下来的挑战是,如果吞吐量超出单个主库的处理能力,这种情况下如何扩展系统;以及,如果主库失效,如何处理故障切换。在分布式系统文献中,这个问题被称为全序广播(total order broadcast)或原子广播(atomic broadcast)、 6 | 7 | 全序广播通常被描述为在节点间交换消息的协议。非正式地讲,它要满足两个安全属性: 8 | 9 | - 可靠交付(reliable delivery):没有消息丢失:如果消息被传递到一个节点,它将被传递到所有节点。 10 | 11 | - 全序交付(totally ordered delivery):消息以相同的顺序传递给每个节点。 12 | 13 | 正确的全序广播算法必须始终保证可靠性和有序性,即使节点或网络出现故障。当然在网络中断的时候,消息是传不出去的,但是算法可以不断重试,以便在网络最终修复时,消息能及时通过并送达(当然它们必须仍然按照正确的顺序传递)。 14 | 15 | # 使用全序广播 16 | 17 | 像 ZooKeeper 和 etcd 这样的共识服务实际上实现了全序广播,这一事实暗示了全序广播与共识之间有着紧密联系。全序广播正是数据库复制所需的:如果每个消息都代表一次数据库的写入,且每个副本都按相同的顺序处理相同的写入,那么副本间将相互保持一致(除了临时的复制延迟)。这个原理被称为状态机复制(state machine replication)。与之类似,可以使用全序广播来实现可序列化的事务,如果每个消息都表示一个确定性事务,以存储过程的形式来执行,且每个节点都以相同的顺序处理这些消息,那么数据库的分区和副本就可以相互保持一致。 18 | 19 | 全序广播的一个重要表现是,顺序在消息送达时被固化:如果后续的消息已经送达,节点就不允许追溯地将(先前)消息插入顺序中的较早位置。这个事实使得全序广播比时间戳命令更强。考量全序广播的另一种方式是,这是一种创建日志的方式(如在复制日志,事务日志或预写式日志中):传递消息就像附加写入日志。由于所有节点必须以相同的顺序传递相同的消息,因此所有节点都可以读取日志,并看到相同的消息序列。 20 | 21 | 全序广播对于实现提供防护令牌的锁服务也很有用。每个获取锁的请求都作为一条消息追加到日志末尾,并且所有的消息都按它们在日志中出现的顺序依次编号。序列号可以当成防护令牌用,因为它是单调递增的。在 ZooKeeper 中,这个序列号被称为 zxid。 22 | 23 | # 使用全序广播实现线性一致的存储 24 | 25 | 在线性一致的系统中,存在操作的全序。这是否意味着线性一致与全序广播一样?不尽然,但两者之间有者密切的联系。从形式上讲,线性一致读写寄存器是一个“更容易”的问题。全序广播等价于共识,而共识问题在异步的崩溃-停止模型中没有确定性的解决方案,而线性一致的读写寄存器可以在这种模型中实现。然而,支持诸如比较并设置(CAS, compare-and-set),或自增并返回(increment-and-get)的原子操作使它等价于共识问题。因此,共识问题与线性一致寄存器问题密切相关。 26 | 27 | 全序广播是异步的:消息被保证以固定的顺序可靠地传送,但是不能保证消息何时被送达(所以一个接收者可能落后于其他接收者)。相比之下,线性一致性是新鲜性的保证:读取一定能看见最新的写入值。但如果有了全序广播,你就可以在此基础上构建线性一致的存储。例如,你可以确保用户名能唯一标识用户帐户。 28 | 29 | 设想对于每一个可能的用户名,你都可以有一个带有 CAS 原子操作的线性一致寄存器。每个寄存器最初的值为空值(表示不使用用户名)。当用户想要创建一个用户名时,对该用户名的寄存器执行 CAS 操作,在先前寄存器值为空的条件,将其值设置为用户的账号 ID。如果多个用户试图同时获取相同的用户名,则只有一个 CAS 操作会成功,因为其他用户会看到非空的值(由于线性一致性)。你可以通过将全序广播当成仅追加日志的方式来实现这种线性一致的 CAS 操作: 30 | 31 | - 在日志中追加一条消息,试探性地指明你要声明的用户名。 32 | 33 | - 读日志,并等待你所附加的信息被回送。 34 | 35 | - 检查是否有任何消息声称目标用户名的所有权。如果这些消息中的第一条就你自己的消息,那么你就成功了:你可以提交声称的用户名(也许是通过向日志追加另一条消息)并向客户端确认。如果所需用户名的第一条消息来自其他用户,则中止操作。 36 | 37 | 由于日志项是以相同顺序送达至所有节点,因此如果有多个并发写入,则所有节点会对最先到达者达成一致。选择冲突写入中的第一个作为胜利者,并中止后来者,以此确定所有节点对某个写入是提交还是中止达成一致。类似的方法可以在一个日志的基础上实现可序列化的多对象事务。尽管这一过程保证写入是线性一致的,但它并不保证读取也是线性一致的,如果你从与日志异步更新的存储中读取数据,结果可能是陈旧的。(精确地说,这里描述的过程提供了顺序一致性(sequential consistency),有时也称为时间线一致性(timeline consistency),比线性一致性稍微弱一些的保证)。为了使读取也线性一致,有几个选项: 38 | 39 | - 你可以通过追加一条消息,当消息回送时读取日志,执行实际的读取。消息在日志中的位置因此定义了读取发生的时间点。(etcd 的法定人数读取有些类似这种情况。) 40 | 41 | - 如果日志允许以线性一致的方式获取最新日志消息的位置,则可以查询该位置,等待直到该位置前的所有消息都传达到你,然后执行读取。(这是 Zookeeper sync() 操作背后的思想)。 42 | 43 | - 你可以从同步更新的副本中进行读取,因此可以确保结果是最新的。(这种技术用于链式复制;参阅“复制研究”。) 44 | 45 | # 使用线性一致性存储实现全序广播 46 | 47 | 上一节介绍了如何从全序广播构建一个线性一致的 CAS 操作。我们也可以把它反过来,假设我们有线性一致的存储,接下来会展示如何在此基础上构建全序广播。最简单的方法是假设你有一个线性一致的寄存器来存储一个整数,并且有一个原子自增并返回操作。或者原子 CAS 操作也可以完成这项工作。该算法很简单:每个要通过全序广播发送的消息首先对线性一致寄存器执行自增并返回操作。然后将从寄存器获得的值作为序列号附加到消息中。然后你可以将消息发送到所有节点(重新发送任何丢失的消息),而收件人将按序列号连续发送消息。 48 | 49 | 请注意,与兰伯特时间戳不同,通过自增线性一致性寄存器获得的数字形式上是一个没有间隙的序列。因此,如果一个节点已经发送了消息 4 并且接收到序列号为 6 的传入消息,则它知道它在传递消息 6 之前必须等待消息 5 。兰伯特时间戳则与之不同,事实上,这是全序广播和时间戳排序间的关键区别。实现一个带有原子性自增并返回操作的线性一致寄存器有多困难?像往常一样,如果事情从来不出差错,那很容易:你可以简单地把它保存在单个节点内的变量中。问题在于处理当该节点的网络连接中断时的情况,并在该节点失效时能恢复这个值。一般来说,如果你对线性一致性的序列号生成器进行深入过足够深入的思考,你不可避免地会得出一个共识算法。 50 | 51 | 这并非巧合:可以证明,线性一致的 CAS(或自增并返回)寄存器与全序广播都都等价于共识问题。也就是说,如果你能解决其中的一个问题,你可以把它转化成为其他问题的解决方案。这是相当深刻和令人惊讶的洞察! 52 | -------------------------------------------------------------------------------- /02~一致性与共识/分布式时钟/序列号/序列号顺序.md: -------------------------------------------------------------------------------- 1 | # 序列号顺序与全序广播 2 | 3 | # 序列号顺序 4 | 5 | 虽然因果是一个重要的理论概念,但实际上跟踪所有的因果关系是不切实际的。在许多应用中,客户端在写入内容之前会先读取大量数据,我们无法弄清写入因果依赖于先前全部的读取内容,还是仅包括其中一部分。显式跟踪所有已读数据意味着巨大的额外开销。但还有一个更好的方法:我们可以使用序列号(sequence nunber)或时间戳(timestamp)来排序事件。时间戳不一定来自时钟(或物理时钟,存在许多问题)。它可以来自一个逻辑时钟(logical clock),这是一个用来生成标识操作的数字序列的算法,典型实现是使用一个每次操作自增的计数器。 6 | 7 | 这样的序列号或时间戳是紧凑的(只有几个字节大小),它提供了一个全序关系:也就是说每操作都有一个唯一的序列号,而且总是可以比较两个序列号,确定哪一个更大(即哪些操作后发生)。特别是,我们可以使用与因果一致(consistent with causality)的全序来生成序列号:我们保证,如果操作 A 因果后继于操作 B,那么在这个全序中 A 在 B 前(A 具有比 B 更小的序列号)。并行操作之间可以任意排序。这样一个全序关系捕获了所有关于因果的信息,但也施加了一个比因果性要求更为严格的顺序。 8 | 9 | 在单主复制的数据库中,复制日志定义了与因果一致的写操作。主库可以简单地为每个操作自增一个计数器,从而为复制日志中的每个操作分配一个单调递增的序列号。如果一个从库按照它们在复制日志中出现的顺序来应用写操作,那么从库的状态始终是因果一致的(即使它落后于领导者)。 10 | 11 | ## 非因果序列号生成器 12 | 13 | 如果主库不存在(可能因为使用了多主数据库或无主数据库,或者因为使用了分区的数据库),如何为操作生成序列号就没有那么明显了。在实践中有各种各样的方法: 14 | 15 | - 每个节点都可以生成自己独立的一组序列号。例如有两个节点,一个节点只能生成奇数,而另一个节点只能生成偶数。通常,可以在序列号的二进制表示中预留一些位,用于唯一的节点标识符,这样可以确保两个不同的节点永远不会生成相同的序列号。 16 | 17 | - 可以将时钟(物理时钟)时间戳附加到每个操作上。这种时间戳并不连续,但是如果它具有足够高的分辨率,那也许足以提供一个操作的全序关系。这一事实应用于 最后写入为准 的冲突解决方法中。 18 | 19 | - 可以预先分配序列号区块。例如,节点 A 可能要求从序列号 1 到 1,000 区块的所有权,而节点 B 可能要求序列号 1,001 到 2,000 区块的所有权。然后每个节点可以独立分配所属区块中的序列号,并在序列号告急时请求分配一个新的区块。 20 | 21 | 这三个选项都比单一主库的自增计数器表现要好,并且更具可扩展性。它们为每个操作生成一个唯一的,近似自增的序列号。然而它们都有同一个问题:生成的序列号与因果不一致。因为这些序列号生成器不能正确地捕获跨节点的操作顺序,所以会出现因果关系的问题: 22 | 23 | - 每个节点每秒可以处理不同数量的操作。因此,如果一个节点产生偶数序列号而另一个产生奇数序列号,则偶数计数器可能落后于奇数计数器,反之亦然。如果你有一个奇数编号的操作和一个偶数编号的操作,你无法准确地说出哪一个操作在因果上先发生。 24 | 25 | - 来自物理时钟的时间戳会受到时钟偏移的影响,这可能会使其与因果不一致。例如下图这个例子中,其中因果上晚发生的操作,却被分配了一个更早的时间戳。 26 | 27 | ![](https://s2.ax1x.com/2020/02/17/3C4Cdg.md.png) 28 | 29 | - 在分配区块的情况下,某个操作可能会被赋予一个范围在 1,001 到 2,000 内的序列号,然而一个因果上更晚的操作可能被赋予一个范围在 1 到 1,000 之间的数字。这里序列号与因果关系也是不一致的。 30 | 31 | ## 兰伯特时间戳 32 | 33 | 尽管刚才描述的三个序列号生成器与因果不一致,但实际上有一个简单的方法来产生与因果关系一致的序列号。它被称为兰伯特时间戳,莱斯利·兰伯特(Leslie Lamport)于 1978 年提出,现在是分布式系统领域中被引用最多的论文之一。下图说明了兰伯特时间戳的应用。每个节点都有一个唯一标识符,和一个保存自己执行操作数量的计数器。兰伯特时间戳就是两者的简单组合:(计数器,节点 ID)$(counter, node ID)$。两个节点有时可能具有相同的计数器值,但通过在时间戳中包含节点 ID,每个时间戳都是唯一的。兰伯特时间戳与物理时间时钟没有任何关系,但是它提供了一个全序:如果你有两个时间戳,则计数器值大者是更大的时间戳。如果计数器值相同,则节点 ID 越大的,时间戳越大。 34 | 35 | ![Lamport 时间戳提供了与因果关系一致的总排序](https://s2.ax1x.com/2020/02/17/3C596x.md.png) 36 | 37 | 迄今,这个描述与上节所述的奇偶计数器基本类似。使兰伯特时间戳因果一致的关键思想如下所示:每个节点和每个客户端跟踪迄今为止所见到的最大计数器值,并在每个请求中包含这个最大计数器值。当一个节点收到最大计数器值大于自身计数器值的请求或响应时,它立即将自己的计数器设置为这个最大值。还是如上图所示,其中客户端 A 从节点 2 接收计数器值 5,然后将最大值 5 发送到节点 1 。此时,节点 1 的计数器仅为 1,但是它立即前移至 5,所以下一个操作的计数器的值为 6 。只要每一个操作都携带着最大计数器值,这个方案确保兰伯特时间戳的排序与因果一致,因为每个因果依赖都会导致时间戳增长。 38 | 39 | 兰伯特时间戳与版本向量有所相似,但它们有着不同的目的:版本向量可以区分两个操作是并发的,还是一个因果依赖另一个;而兰伯特时间戳总是施行一个全序。从兰伯特时间戳的全序中,你无法分辨两个操作是并发的还是因果依赖的。兰伯特时间戳优于版本向量的地方是,它更加紧凑。 40 | 41 | ## 光有时间戳排序还不够 42 | 43 | 虽然兰伯特时间戳定义了一个与因果一致的全序,但它还不足以解决分布式系统中的许多常见问题。例如,考虑一个需要确保用户名能唯一标识用户帐户的系统。如果两个用户同时尝试使用相同的用户名创建帐户,则其中一个应该成功,另一个应该失败。乍看之下,似乎操作的全序关系足以解决这一问题(例如使用兰伯特时间戳):如果创建了两个具有相同用户名的帐户,选择时间戳较小的那个作为胜者(第一个抓到用户名的人),并让带有更大时间戳者失败。由于时间戳上有全序关系,所以这个比较总是可行的。 44 | 45 | 这种方法适用于事后确定胜利者:一旦你收集了系统中的所有用户名创建操作,就可以比较它们的时间戳。然而当某个节点需要实时处理用户创建用户名的请求时,这样的方法就无法满足了。节点需要马上(right now)决定这个请求是成功还是失败。在那个时刻,节点并不知道是否存其他节点正在并发执行创建同样用户名的操作,罔论其它节点可能分配给那个操作的时间戳。 46 | 47 | 为了确保没有其他节点正在使用相同的用户名和较小的时间戳并发创建同名账户,你必须检查其它每个节点,看看它在做什么。如果其中一个节点由于网络问题出现故障或不可达,则整个系统可能被拖至停机。这不是我们需要的那种容错系统。这里的问题是,只有在所有的操作都被收集之后,操作的全序才会出现。如果另一个节点已经产生了一些操作,但你还不知道那些操作是什么,那就无法构造所有操作最终的全序关系:来自另一个节点的未知操作可能需要被插入到全序中的不同位置。 48 | 49 | 总之:为了实诸如如用户名上的唯一约束这种东西,仅有操作的全序是不够的,你还需要知道这个全序何时会尘埃落定。如果你有一个创建用户名的操作,并且确定在全序中,没有任何其他节点可以在你的操作之前插入对同一用户名的声称,那么你就可以安全地宣告操作执行成功。 50 | -------------------------------------------------------------------------------- /02~一致性与共识/拜占庭问题/README.md: -------------------------------------------------------------------------------- 1 | # 拜占庭问题 2 | 3 | 我们已经探索了分布式系统与运行在单台计算机上的程序的不同之处:没有共享内存,只有通过可变延迟的不可靠网络传递的消息,系统可能遭受部分失效,不可靠的时钟和处理暂停。网络中的一个节点无法确切地知道任何事情——它只能根据它通过网络接收到(或没有接收到)的消息进行猜测。节点只能通过交换消息来找出另一个节点所处的状态(存储了哪些数据,是否正确运行等等)。如果远程节点没有响应,则无法知道它处于什么状态,因为网络中的问题不能可靠地与节点上的问题区分开来。 4 | 5 | 这些系统的讨论与哲学有关:在系统中什么是真什么是假?如果感知和测量的机制都是不可靠的,那么关于这些知识我们又能多么确定呢?软件系统应该遵循我们对物理世界所期望的法则,如因果关系吗? 6 | 7 | # 真理由多数所定义 8 | 9 | 设想一个具有不对称故障的网络:一个节点能够接收发送给它的所有消息,但是来自该节点的任何传出消息被丢弃或延迟。即使该节点运行良好,并且正在接收来自其他节点的请求,其他节点也无法听到其响应。经过一段时间后,其他节点宣布它已经死亡,因为他们没有听到节点的消息。这种情况就像梦魇一样:半断开(semi-disconnected)的节点被拖向墓地,敲打尖叫道“我没死!”,但是由于没有人能听到它的尖叫,葬礼队伍继续以坚忍的决心继续行进。 10 | 11 | 在一个稍微不那么梦魇的场景中,半断开的节点可能会注意到它发送的消息没有被其他节点确认,因此意识到网络中必定存在故障。尽管如此,节点被其他节点错误地宣告为死亡,而半连接的节点对此无能为力。还有一种情况,想象一个经历了一个长时间 stop-the-world GC Pause 的节点,节点的所有线程被 GC 抢占并暂停一分钟,因此没有请求被处理,也没有响应被发送。其他节点等待,重试,不耐烦,并最终宣布节点死亡,并将其丢到灵车上。最后,GC 完成,节点的线程继续,好像什么也没有发生。其他节点感到惊讶,因为所谓的死亡节点突然从棺材中抬起头来,身体健康,开始和旁观者高兴地聊天。GC 后的节点最初甚至没有意识到已经经过了整整一分钟,而且自己已被宣告死亡。从它自己的角度来看,从最后一次与其他节点交谈以来,几乎没有经过任何时间。 12 | 13 | 这些故事的寓意是,节点不一定能相信自己对于情况的判断。分布式系统不能完全依赖单个节点,因为节点可能随时失效,可能会使系统卡死,无法恢复。相反,许多分布式算法都依赖于法定人数,即在节点之间进行投票:决策需要来自多个节点的最小投票数,以减少对于某个特定节点的依赖。这也包括关于宣告节点死亡的决定。如果法定数量的节点宣告另一个节点已经死亡,那么即使该节点仍感觉自己活着,它也必须被认为是死的。个体节点必须遵守法定决定并下台。 14 | 15 | 最常见的法定人数是超过一半的绝对多数(尽管其他类型的法定人数也是可能的)。多数法定人数允许系统继续工作,如果单个节点发生故障(三个节点可以容忍单节点故障;五个节点可以容忍双节点故障)。系统仍然是安全的,因为在这个制度中只能有一个多数——不能同时存在两个相互冲突的多数决定。 16 | 17 | # Links 18 | 19 | - [2021~白话讲解,拜占庭将军问题](https://xie.infoq.cn/article/879cdfde772401f17a42c1025): 作为服务端开发的同学,你可能听说过 Paxos、Raft 这类分布式一致性算法,也在工作中使用过 ZooKeeper、etcd 等工具来解决一致性问题。但你可能不知道,这些算法和工具解决的并不是一致性中最难的问题,要讨论这个最难的问题,这就要追溯到 Leslie Lamport 1982 年发表的著名论文 《拜占庭将军问题》(The Byzantine Generals Problem)上了。 20 | -------------------------------------------------------------------------------- /02~一致性与共识/拜占庭问题/拜占庭故障.md: -------------------------------------------------------------------------------- 1 | # 拜占庭故障 2 | 3 | 在[《分布式锁](https://github.com/wx-chevalier/DistributedSystem-Notes)》中我们讨论过利用屏蔽令牌来解决节点暂停的问题,屏蔽令牌可以检测和阻止无意中发生错误的节点(例如,因为它尚未发现其租约已过期)。但是,如果节点有意破坏系统的保证,则可以通过使用假屏蔽令牌发送消息来轻松完成此操作。大部分情况下,我们假设节点是不可靠但诚实的:它们可能很慢或者从不响应(由于故障),并且它们的状态可能已经过时(由于 GC 暂停或网络延迟),但是我们假设如果节点它做出了回应,它正在说出“真相”:尽其所知,它正在按照协议的规则扮演其角色。 4 | 5 | 如果存在节点可能“撒谎”(发送任意错误或损坏的响应)的风险,则分布式系统的问题变得更困难了——例如,如果节点可能声称其实际上没有收到特定的消息。这种行为被称为拜占庭故障(Byzantine fault),在不信任的环境中达成共识的问题被称为拜占庭将军问题。 6 | 7 | # 拜占庭将军问题 8 | 9 | 拜占庭将军问题是所谓“两将军问题”的概括,它想象两个将军需要就战斗计划达成一致的情况。由于他们在两个不同的地点建立了营地,他们只能通过信使进行沟通,信使有时会被延迟或丢失(就像网络中的信息包一样)。在这个拜占庭式的问题中,有 n 位将军需要同意,他们的努力因为有一些叛徒在他们中间而受到阻碍。大多数的将军都是忠诚的,因而发出了真实的信息,但是叛徒可能会试图通过发送虚假或不真实的信息来欺骗和混淆他人(在试图保持未被发现的同时)。事先并不知道叛徒是谁。 10 | 11 | 拜占庭是后来成为君士坦丁堡的古希腊城市,现在在土耳其的伊斯坦布尔。没有任何历史证据表明拜占庭将军比其他地方更容易出现阴谋和阴谋。相反,这个名字来源于拜占庭式的过度复杂,官僚,迂回等意义,早在计算机之前就已经在政治中被使用了。Lamport 想要选一个不会冒犯任何读者的国家,他被告知将其称为阿尔巴尼亚将军问题并不是一个好主意。 12 | 13 | # 拜占庭容错 14 | 15 | 当一个系统在部分节点发生故障、不遵守协议、甚至恶意攻击、扰乱网络时仍然能继续正确工作,称之为拜占庭容错(Byzantine fault-tolerant)的,在特定场景下,这种担忧在是有意义的: 16 | 17 | - 在航空航天环境中,计算机内存或 CPU 寄存器中的数据可能被辐射破坏,导致其以任意不可预知的方式响应其他节点。由于系统故障将非常昂贵(例如,飞机撞毁和炸死船上所有人员,或火箭与国际空间站相撞),飞行控制系统必须容忍拜占庭故障。 18 | 19 | - 在多个参与组织的系统中,一些参与者可能会试图欺骗或欺骗他人。在这种情况下,节点仅仅信任另一个节点的消息是不安全的,因为它们可能是出于恶意的目的而被发送的。例如,像比特币和其他区块链一样的对等网络可以被认为是让互不信任的各方同意交易是否发生的一种方式,而不依赖于中央当局。 20 | 21 | 在我们常说的服务端基础架构中,我们通常可以安全地假设没有拜占庭式的错误。在你的数据中心里,所有的节点都是由你的组织控制的(所以他们可以信任),辐射水平足够低,内存损坏不是一个大问题。制作拜占庭容错系统的协议相当复杂,而容错嵌入式系统依赖于硬件层面的支持。在大多数服务器端数据系统中,部署拜占庭容错解决方案的成本使其变得不切实际。 22 | 23 | Web 应用程序确实需要预期受终端用户控制的客户端(如 Web 浏览器)的任意和恶意行为。这就是为什么输入验证,清理和输出转义如此重要:例如,防止 SQL 注入和跨站点脚本。但是,我们通常不使用拜占庭容错协议,而只是让服务器决定什么是客户端行为,而不是允许的。在没有这种中心授权的对等网络中,拜占庭容错更为重要。 24 | 25 | 软件中的一个错误可能被认为是拜占庭式的错误,但是如果您将相同的软件部署到所有节点上,那么拜占庭式的容错算法不能为您节省。大多数拜占庭式容错算法要求超过三分之二的节点能够正常工作(即,如果有四个节点,最多只能有一个故障)。要使用这种方法对付 bug,你必须有四个独立的相同软件的实现,并希望一个 bug 只出现在四个实现之一中。 26 | 27 | 同样,如果一个协议可以保护我们免受漏洞,安全妥协和恶意攻击,那么这将是有吸引力的。不幸的是,这也是不现实的:在大多数系统中,如果攻击者可以渗透一个节点,那他们可能会渗透所有这些节点,因为它们可能运行相同的软件。因此传统机制(认证,访问控制,加密,防火墙等)仍然是攻击者的主要保护措施。 28 | 29 | # 弱谎言形式 30 | 31 | 尽管我们假设节点通常是诚实的,但值得向软件中添加防止“撒谎”弱形式的机制——例如,由硬件问题导致的无效消息,软件错误和错误配置。这种保护机制并不是完全的拜占庭容错,因为它们不能抵挡决心坚定的对手,但它们仍然是简单而实用的步骤,以提高可靠性。例如: 32 | 33 | - 由于硬件问题或操作系统,驱动程序,路由器等中的错误,网络数据包有时会受到损坏。通常,内建于 TCP 和 UDP 中的校验和会俘获损坏的数据包,但有时它们会逃避检测。简单的措施通常是采用充分的保护来防止这种破坏,例如应用程序级协议中的校验和。 34 | 35 | - 可公开访问的应用程序必须仔细清理来自用户的任何输入,例如检查值是否在合理的范围内,并限制字符串的大小以防止通过大内存分配拒绝服务。防火墙后面的内部服务可能能够在对输入进行较不严格的检查的情况下逃脱,但是一些基本的理智检查(例如,在协议解析中)是一个好主意。 36 | 37 | - NTP 客户端可以配置多个服务器地址。同步时,客户端联系所有的服务器,估计它们的误差,并检查大多数服务器是否在对某个时间范围内达成一致。只要大多数的服务器没问题,一个配置错误的 NTP 服务器报告的时间会被当成特异值从同步中排除。使用多个服务器使 NTP 更健壮(比起只用单个服务器来)。 38 | -------------------------------------------------------------------------------- /02~一致性与共识/拜占庭问题/系统模型.md: -------------------------------------------------------------------------------- 1 | # 系统模型 2 | 3 | # 系统模型 4 | 5 | 算法的编写方式并不过分依赖于运行的硬件和软件配置的细节。这又要求我们以某种方式将我们期望在系统中发生的错误形式化。我们通过定义一个系统模型来做到这一点,这个模型是一个抽象,描述一个算法可能承担的事情。关于定时假设,三种系统模型是常用的: 6 | 7 | ## 定时假设模型 8 | 9 | ### 同步模型 10 | 11 | 同步模型(synchronous model)假设网络延迟,进程暂停和和时钟误差都是有界限的。这并不意味着完全同步的时钟或零网络延迟;这只意味着你知道网络延迟,暂停和时钟漂移将永远不会超过某个固定的上限。同步模型并不是大多数实际系统的现实模型,因为(如本章所讨论的)无限延迟和暂停确实会发生。 12 | 13 | ### 部分同步模型 14 | 15 | 部分同步(partial synchronous)意味着一个系统在大多数情况下像一个同步系统一样运行,但有时候会超出网络延迟,进程暂停和时钟漂移的界限。这是很多系统的现实模型:大多数情况下,网络和进程表现良好,否则我们永远无法完成任何事情,但是我们必须承认,在任何时刻假设都存在偶然被破坏的事实。发生这种情况时,网络延迟,暂停和时钟错误可能会变得相当大。 16 | 17 | ### 异步模型 18 | 19 | 在这个模型中,一个算法不允许对时机做任何假设——事实上它甚至没有时钟(所以它不能使用超时)。一些算法被设计为可用于异步模型,但非常受限。 20 | 21 | ## 节点失效系统模型 22 | 23 | 除了时间问题,我们还要考虑节点失效。三种最常见的节点系统模型是。 24 | 25 | ### 崩溃-停止故障 26 | 27 | 在崩溃停止(crash-stop)模型中,算法可能会假设一个节点只能以一种方式失效,即通过崩溃。这意味着节点可能在任意时刻突然停止响应,此后该节点永远消失——它永远不会回来。 28 | 29 | ### 崩溃-恢复故障 30 | 31 | 我们假设节点可能会在任何时候崩溃,但也许会在未知的时间之后再次开始响应。在崩溃-恢复(crash-recovery)模型中,假设节点具有稳定的存储(即,非易失性磁盘存储)且会在崩溃中保留,而内存中的状态会丢失。 32 | 33 | ### 拜占庭(任意)故障 34 | 35 | 节点可以做(绝对意义上的)任何事情,包括试图戏弄和欺骗其他节点。 36 | 37 | # 算法的正确性 38 | 39 | 为了定义算法是正确的,我们可以描述它的属性。例如,排序算法的输出具有如下特性:对于输出列表中的任何两个不同的元素,左边的元素比右边的元素小。这只是定义对列表进行排序含义的一种形式方式。同样,我们可以写下我们想要的分布式算法的属性来定义它的正确含义。例如,如果我们正在为一个锁生成屏蔽令牌,我们可能要求算法具有以下属性: 40 | 41 | - 唯一性:没有两个屏蔽令牌请求返回相同的值。 42 | 43 | - 单调序列:如果请求 $x$ 返回了令牌 $t_x$,并且请求$y$返回了令牌$t_y$,并且 $x$ 在 $y$ 开始之前已经完成,那么$t_x 8 |
9 |

10 | 11 | Logo 12 | 13 | 14 |

15 | 在线阅读 >> 16 |
17 |
18 | 代码案例 19 | · 20 | 参考资料 21 | 22 |

23 |

24 | 25 | # Distributed System Series(分布式系统·实践笔记) 26 | 27 | 深入浅出分布式基础架构是笔者归档自己,在学习与实践软件分布式架构过程中的,笔记与代码的仓库;主要包含分布式计算、分布式系统、数据存储、虚拟化、网络、操作系统等几个部分。所谓的分布式系统,其主要由网络、分布式存储与分布式计算等部分构成,分布式存储侧重于数据的读写存取及一致性等方面,而分布式计算则侧重于资源、任务的编排调度。 28 | 29 | ![A Unified Data Infrastructure Architecture](https://s1.ax1x.com/2020/10/18/0XOno9.png) 30 | 31 | ## Nav | 关联导航 32 | 33 | > 如果你想了解微服务/云原生等分布式系统的应用实践,可以参阅;如果你想了解数据库相关,可以参阅 [Database-Notes](https://github.com/wx-chevalier/Database-Notes);如果你想了解虚拟化与云计算相关,可以参阅 [Cloud-Notes](https://github.com/wx-chevalier/Cloud-Notes);如果你想了解 Linux 与操作系统相关,可以参阅 [Linux-Notes](https://github.com/wx-chevalier/Linux-Notes)。 34 | 35 | # About 36 | 37 | ## Copyright & More | 延伸阅读 38 | 39 | 笔者所有文章遵循 [知识共享 署名-非商业性使用-禁止演绎 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh),欢迎转载,尊重版权。您还可以前往 [NGTE Books](https://ng-tech.icu/books-gallery/) 主页浏览包含知识体系、编程语言、软件工程、模式与架构、Web 与大前端、服务端开发实践与工程架构、分布式基础架构、人工智能与深度学习、产品运营与创业等多类目的书籍列表: 40 | 41 | [![NGTE Books](https://s2.ax1x.com/2020/01/18/19uXtI.png)](https://ng-tech.icu/books-gallery/) 42 | 43 | 44 | 45 | 46 | [contributors-shield]: https://img.shields.io/github/contributors/wx-chevalier/DistributedSystem-Notes.svg?style=flat-square 47 | [contributors-url]: https://github.com/wx-chevalier/DistributedSystem-Notes/graphs/contributors 48 | [forks-shield]: https://img.shields.io/github/forks/wx-chevalier/DistributedSystem-Notes.svg?style=flat-square 49 | [forks-url]: https://github.com/wx-chevalier/DistributedSystem-Notes/network/members 50 | [stars-shield]: https://img.shields.io/github/stars/wx-chevalier/DistributedSystem-Notes.svg?style=flat-square 51 | [stars-url]: https://github.com/wx-chevalier/DistributedSystem-Notes/stargazers 52 | [issues-shield]: https://img.shields.io/github/issues/wx-chevalier/DistributedSystem-Notes.svg?style=flat-square 53 | [issues-url]: https://github.com/wx-chevalier/DistributedSystem-Notes/issues 54 | [license-shield]: https://img.shields.io/github/license/wx-chevalier/DistributedSystem-Notes.svg?style=flat-square 55 | [license-url]: https://github.com/wx-chevalier/DistributedSystem-Notes/blob/master/LICENSE.txt 56 | -------------------------------------------------------------------------------- /_sidebar.md: -------------------------------------------------------------------------------- 1 | - [1 INTRODUCTION](/INTRODUCTION.md) 2 | - [2 一致性与共识 [4]](/一致性与共识/README.md) 3 | - [2.1 一致性模型 [5]](/一致性与共识/一致性模型/README.md) 4 | - [2.1.1 其他一致性模型](/一致性与共识/一致性模型/其他一致性模型.md) 5 | - [2.1.2 因果一致性](/一致性与共识/一致性模型/因果一致性.md) 6 | - [2.1.3 最终一致性](/一致性与共识/一致性模型/最终一致性.md) 7 | - [2.1.4 线性一致性](/一致性与共识/一致性模型/线性一致性.md) 8 | - [2.1.5 顺序一致性](/一致性与共识/一致性模型/顺序一致性.md) 9 | - [2.2 共识算法 [4]](/一致性与共识/共识算法/README.md) 10 | - [2.2.1 Paxos [1]](/一致性与共识/共识算法/Paxos/README.md) 11 | - [2.2.1.1 Multiple Paxos](/一致性与共识/共识算法/Paxos/Multiple-Paxos.md) 12 | - [2.2.2 Raft [4]](/一致性与共识/共识算法/Raft/README.md) 13 | - 2.2.2.1 99~参考资料 [2] 14 | - [2.2.2.1.1 Raft 原论文 寻找一种易于理解的一致性算法](/一致性与共识/共识算法/Raft/99~参考资料/2016-Raft%20原论文-寻找一种易于理解的一致性算法.md) 15 | - [2.2.2.1.2 多颗糖 条分缕析 Raft](/一致性与共识/共识算法/Raft/99~参考资料/2021-多颗糖-条分缕析%20Raft.md) 16 | - [2.2.2.2 安全性](/一致性与共识/共识算法/Raft/安全性.md) 17 | - [2.2.2.3 日志复制](/一致性与共识/共识算法/Raft/日志复制.md) 18 | - [2.2.2.4 选举与成员变更](/一致性与共识/共识算法/Raft/选举与成员变更.md) 19 | - [2.2.3 ZAB](/一致性与共识/共识算法/ZAB/README.md) 20 | 21 | - [2.2.4 算法设计 [1]](/一致性与共识/共识算法/算法设计/README.md) 22 | - [2.2.4.1 算法对比](/一致性与共识/共识算法/算法设计/算法对比.md) 23 | - [2.3 分布式时钟 [1]](/一致性与共识/分布式时钟/README.md) 24 | - 2.3.1 序列号 [2] 25 | - [2.3.1.1 全序广播](/一致性与共识/分布式时钟/序列号/全序广播.md) 26 | - [2.3.1.2 序列号顺序](/一致性与共识/分布式时钟/序列号/序列号顺序.md) 27 | - [2.4 拜占庭问题 [2]](/一致性与共识/拜占庭问题/README.md) 28 | - [2.4.1 拜占庭故障](/一致性与共识/拜占庭问题/拜占庭故障.md) 29 | - [2.4.2 系统模型](/一致性与共识/拜占庭问题/系统模型.md) 30 | - [3 分布式事务 [5]](/分布式事务/README.md) 31 | - [3.1 事务方案 [3]](/分布式事务/事务方案/README.md) 32 | - [3.1.1 事务分类 [2]](/分布式事务/事务方案/事务分类/README.md) 33 | - [3.1.1.1 NewSQL 的分布式事务](/分布式事务/事务方案/事务分类/NewSQL%20的分布式事务.md) 34 | - [3.1.1.2 跨服务跨库的分布式事务](/分布式事务/事务方案/事务分类/跨服务跨库的分布式事务.md) 35 | - 3.1.2 事务方案对比 [1] 36 | - [3.1.2.1 事务方案对比](/分布式事务/事务方案/事务方案对比/事务方案对比.md) 37 | - [3.1.3 分布式事务案例](/分布式事务/事务方案/分布式事务案例/README.md) 38 | 39 | - 3.2 事务框架 [3] 40 | - [3.2.1 JDTX](/分布式事务/事务框架/JDTX/README.md) 41 | 42 | - [3.2.2 Seata](/分布式事务/事务框架/Seata/README.md) 43 | 44 | - [3.2.3 dtm](/分布式事务/事务框架/dtm/README.md) 45 | 46 | - [3.3 事务消息 [3]](/分布式事务/事务消息/README.md) 47 | - 3.3.1 实践案例 [1] 48 | - [3.3.1.1 事务消息保障抢购业务的分布式一致性](/分布式事务/事务消息/实践案例/事务消息保障抢购业务的分布式一致性.md) 49 | - [3.3.2 流处理方案](/分布式事务/事务消息/流处理方案.md) 50 | - [3.3.3 消息队列方案](/分布式事务/事务消息/消息队列方案.md) 51 | - [3.4 多阶段提交 [3]](/分布式事务/多阶段提交/README.md) 52 | - [3.4.1 XA 事务](/分布式事务/多阶段提交/XA%20事务.md) 53 | - [3.4.2 三阶段提交](/分布式事务/多阶段提交/三阶段提交.md) 54 | - [3.4.3 二阶段提交](/分布式事务/多阶段提交/二阶段提交.md) 55 | - [3.5 柔性事务 [2]](/分布式事务/柔性事务/README.md) 56 | - 3.5.1 Saga [1] 57 | - [3.5.1.1 Saga](/分布式事务/柔性事务/Saga/Saga.md) 58 | - [3.5.2 TCC](/分布式事务/柔性事务/TCC/README.md) 59 | 60 | - [4 分布式基础 [5]](/分布式基础/README.md) 61 | - [4.1 01~不可靠的分布式系统 [3]](/分布式基础/01~不可靠的分布式系统/README.md) 62 | - [4.1.1 不可靠时钟](/分布式基础/01~不可靠的分布式系统/不可靠时钟.md) 63 | - [4.1.2 不可靠网络](/分布式基础/01~不可靠的分布式系统/不可靠网络.md) 64 | - [4.1.3 不可靠进程](/分布式基础/01~不可靠的分布式系统/不可靠进程.md) 65 | - 4.2 02.节点与集群 [2] 66 | - [4.2.1 主从节点 [1]](/分布式基础/02.节点与集群/主从节点/README.md) 67 | - [4.2.1.1 节点选举](/分布式基础/02.节点与集群/主从节点/节点选举.md) 68 | - [4.2.2 分布式互斥](/分布式基础/02.节点与集群/分布式互斥.md) 69 | - [4.3 03.CAP [3]](/分布式基础/03.CAP/README.md) 70 | - [4.3.1 BASE](/分布式基础/03.CAP/BASE.md) 71 | - [4.3.2 DLS](/分布式基础/03.CAP/DLS.md) 72 | - [4.3.3 特性与模型](/分布式基础/03.CAP/特性与模型.md) 73 | - 4.4 04.日志模型 [2] 74 | - [4.4.1 WAL](/分布式基础/04.日志模型/WAL/README.md) 75 | 76 | - [4.4.2 分割日志](/分布式基础/04.日志模型/分割日志/README.md) 77 | 78 | - 4.5 99~参考资料 [3] 79 | - [4.5.1 Distributed Systems for Fun and Profit [1]](/分布式基础/99~参考资料/Distributed%20Systems%20for%20Fun%20and%20Profit/README.md) 80 | - 4.5.1.1 中文翻译 [7] 81 | - [4.5.1.1.1 00.引言](/分布式基础/99~参考资料/Distributed%20Systems%20for%20Fun%20and%20Profit/中文翻译/00.引言.md) 82 | - [4.5.1.1.2 01.Basics](/分布式基础/99~参考资料/Distributed%20Systems%20for%20Fun%20and%20Profit/中文翻译/01.Basics.md) 83 | - [4.5.1.1.3 02.Abstraction](/分布式基础/99~参考资料/Distributed%20Systems%20for%20Fun%20and%20Profit/中文翻译/02.Abstraction.md) 84 | - [4.5.1.1.4 03.TimeAndOrder](/分布式基础/99~参考资料/Distributed%20Systems%20for%20Fun%20and%20Profit/中文翻译/03.TimeAndOrder.md) 85 | - [4.5.1.1.5 04.PreventingDivergence](/分布式基础/99~参考资料/Distributed%20Systems%20for%20Fun%20and%20Profit/中文翻译/04.PreventingDivergence.md) 86 | - [4.5.1.1.6 05.AcceptingDivergence](/分布式基础/99~参考资料/Distributed%20Systems%20for%20Fun%20and%20Profit/中文翻译/05.AcceptingDivergence.md) 87 | - [4.5.1.1.7 10.Appendix](/分布式基础/99~参考资料/Distributed%20Systems%20for%20Fun%20and%20Profit/中文翻译/10.Appendix.md) 88 | - [4.5.2 Patterns of Distributed Systems [25]](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/README.md) 89 | - [4.5.2.1 consistent core](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/consistent-core.md) 90 | - [4.5.2.2 follower reads](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/follower-reads.md) 91 | - [4.5.2.3 generation clock](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/generation-clock.md) 92 | - [4.5.2.4 gossip dissemination](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/gossip-dissemination.md) 93 | - [4.5.2.5 heartbeat](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/heartbeat.md) 94 | - [4.5.2.6 high water mark](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/high-water-mark.md) 95 | - [4.5.2.7 hybrid clock](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/hybrid-clock.md) 96 | - [4.5.2.8 idempotent receiver](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/idempotent-receiver.md) 97 | - [4.5.2.9 lamport clock](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/lamport-clock.md) 98 | - [4.5.2.10 leader and followers](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/leader-and-followers.md) 99 | - [4.5.2.11 lease](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/lease.md) 100 | - [4.5.2.12 low water mark](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/low-water-mark.md) 101 | - [4.5.2.13 overview](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/overview.md) 102 | - [4.5.2.14 paxos](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/paxos.md) 103 | - [4.5.2.15 quorum](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/quorum.md) 104 | - [4.5.2.16 replicated log](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/replicated-log.md) 105 | - [4.5.2.17 request pipeline](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/request-pipeline.md) 106 | - [4.5.2.18 segmented log](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/segmented-log.md) 107 | - [4.5.2.19 single socket channel](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/single-socket-channel.md) 108 | - [4.5.2.20 singular update queue](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/singular-update-queue.md) 109 | - [4.5.2.21 state watch](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/state-watch.md) 110 | - [4.5.2.22 two phase commit](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/two-phase-commit.md) 111 | - [4.5.2.23 version vector](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/version-vector.md) 112 | - [4.5.2.24 versioned value](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/versioned-value.md) 113 | - [4.5.2.25 write ahead log](/分布式基础/99~参考资料/Patterns%20of%20Distributed%20Systems/write-ahead-log.md) 114 | - [4.5.3 分布式系统的八大谬误](/分布式基础/99~参考资料/分布式系统的八大谬误.md) 115 | - [5 分布式存储](/分布式存储/README.md) 116 | 117 | - [6 分布式计算](/分布式计算/README.md) 118 | 119 | - [7 分布式锁 [5]](/分布式锁/README.md) 120 | - [7.1 分布式锁方案对比](/分布式锁/分布式锁方案对比.md) 121 | - [7.2 基于 Redis 的分布式锁 [2]](/分布式锁/基于%20Redis%20的分布式锁/README.md) 122 | - [7.2.1 Redis 分布式锁](/分布式锁/基于%20Redis%20的分布式锁/Redis%20分布式锁.md) 123 | - [7.2.2 Redisson 锁实现](/分布式锁/基于%20Redis%20的分布式锁/Redisson%20锁实现.md) 124 | - [7.3 基于 ZooKeeper 的分布式锁 [2]](/分布式锁/基于%20ZooKeeper%20的分布式锁/README.md) 125 | - [7.3.1 基于 Apache Curator 的使用](/分布式锁/基于%20ZooKeeper%20的分布式锁/基于%20Apache%20Curator%20的使用.md) 126 | - [7.3.2 实现原理](/分布式锁/基于%20ZooKeeper%20的分布式锁/实现原理.md) 127 | - [7.4 数据库锁](/分布式锁/数据库锁/README.md) 128 | 129 | - 7.5 服务锁 [1] 130 | - [7.5.1 RESTful 分布式锁](/分布式锁/服务锁/RESTful%20分布式锁.md) -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | DistributedSystem-Series 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 34 | 38 | 40 | 45 | 46 |
47 | 64 | 97 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 143 | 144 | 145 | 146 | 155 | 156 | 157 | --------------------------------------------------------------------------------