Skip to main content

Distributed System 分布式

https://github.com/fenixsoft/awesome-fenix 如何构建一套可靠的大型分布式系统

CAP是什么

https://people.eecs.berkeley.edu/~brewer/cs262b-2004/PODC-keynote.pdf

http://www.royans.net/wp/2010/02/14/brewers-cap-theorem-on-distributed-systems/

https://github.com/qiurunze123 淘宝分布式大牛

书籍 https://book.douban.com/subject/26702824/ https://book.douban.com/subject/35492898/ https://book.douban.com/subject/27091029/

1. 什么是分布式系统and为什么需要它

  • 为什么需要?

    普通的单机无法完成计算/存储量大的任务, 于是就将这个任务分解为多个子任务, 交给多台机器计算/存储, 最后汇总结果, 这样多台廉价的机器可以处理大计算量的任务了, 这就是分布式计算系统

    分布式系统要做的任务就是把多台机器连接起来,通过网络通信, 让其协同完成任务, 可以是计算任务,也可以是存储任务

  • 区分两个概念:

    • 分布式:一个业务分拆多个子业务,部署在不同的服务器上; (强调更多的机器提供更强的数据处理能力, 保量)

    • 集群:同一个业务,重复部署在多个服务器上, 通过一个单独服务器在最前端进行负载均衡 (强调更多的机器提供更稳定的服务, 保质)

  • 分类:

    • 分布式存储系统

    • 分布式计算系统

只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为分布式系统多节点、通过网络通信的结构,会引入很多单机系统没有的问题,所以不到万不得已不要引入分布式

1.1. 分布式存储系统

将数据分片, 存储到多个节点

  1. 结构化存储
  2. 非结构化存储
  3. 半结构化存储
  4. In-memory 存储

分布式存储系统有一系列的理论、算法、技术作为支撑:例如 Paxos, CAP, Consistent Hash, Timing (时钟), 2PC, 3PC 等等(参考)

  • 结构化存储(structured storage systems)

    • 典型的场景就是事务处理系统或者关系型数据库(RDBMS, 如MySQL, PostgreSQL ...)

    • 传统的结构化存储系统强调的是: (正是由于这些性质和限制,结构化存储系统的可扩展性通常都不是很好,这在一定程度上限制了结构化存储在大数据环境下的表现)

      • 结构化的数据(例如关系表)

      • 强一致性 (例如,银行系统,电商系统等场景)

      • 随机访问(索引,增删查改,SQL 语言)

  • 非结构化存储 (no-structed storage systems)

    • GFS (google file system) 则是做出了里程碑的一步,其开源实现对应为 HDFS.

    • 非结构化存储强调的是高可扩展性,典型的系统就是分布式文件系统; 相应的, 几乎无法支持随机访问(如随机 update, read)操作,通常只能进行文件进行追加(append)操作。而这样的限制使得非结构化存储系统很难面对那些实时性较强的应用。

  • 半结构化存储 (semi-structure storage systems)

    • 是为了解决结非构化存储系统随机访问性能差的问题

    • NoSQL, Key-Value Store, 甚至包括对象存储,例如 protobuf,thrift 都属于半结构化存储

    • NoSQL 系统既有分布式文件系统所具有的可扩展性,又有结构化存储系统的随机访问能力 (例如随机update, read 操作)

  • In-memory 存储(将数据存储在内存中, 从而获得读写的高性能)

    • 比较有名的系统包括 memcahed ,以及 Redis

1.2. 分布式计算系统

传统的并行计算要的是:投入更多机器,数据大小不变,计算速度更快。------强调更高的性能

分布式计算要求:投入更多的机器,能处理更大的数据。-------------------强调处理更大规模的数据

  1. 传统基于msg的系统
  2. MapReduce-like 系统
  3. 图计算系统
  4. 基于状态(state)的系统
  5. Streaming 系统

对于Java来说, 分布式系统就是多个子系统分布在一台/多台机器的一个/多个 jvm 中;

2. 分布式系统的特性and如何衡量

可扩展性 - 通过增加机器来应对数据量的增长,同时,当任务规模缩减的时候,可以撤掉一些多余的机器,达到动态伸缩的效果

可用性 - 任何时候都能对外提供服务

高性能 - 高并发,低延迟

一致性 - 为了提高可用性, 分布式系统会引入 replication (数据冗余, 即提供多个数据完全一致的节点);一致性就是保证这些 重复节点的状态的一致; 一致性越强,对用户越友好,但会制约系统的可用性;一致性等级越低,用户就需要兼容数据不一致的情况,但系统的可用性、并发性很高很多。

3. 涉及哪些技术规范

负载均衡: nginx

分布式缓存: redis

网络通信:每个应用手写 socket --> 通过 http(应用层协议) 提供 http 接口 --> rpc - 典型如 dubbo, grpc

分布式事务

服务注册发现 : zookeeper

消息队列: kafka、rabbitMQ、rocketMQ

分布式计算平台: Hadoop, storm, akka

分布式存储

日志:elk

4. CAP 理论

4.1. cap 介绍

https://mwhittaker.github.io/blog/an_illustrated_proof_of_the_cap_theorem/

分布式系统一定要满足 P, 也就是在 网络分区容错的情况下, 剩下的两者无法同时满足, 这就是 cap定理

  • Consistency 一致性 - 所有节点在同一时间具有相同的数据 (任何节点上的read 操作每次都能读取到最新数据)

    如果要保证 "一致性", 势必无法保证 "可用性", 因为 数据在节点间的复制也是要耗时的, 这段时间内系统无法对外提供服务

    比如: 为了保证其他server的 "一致性", 当前server在完成写操作时候, 必须锁定 其他server的读写, 只有数据同步后, 才可放开, 这段时间内其他server不具有"可用性"

    强一致性: 就是说, 节点间数据是实时同步的 (分布式系统一般会舍弃强一致性, 保证弱一致性), 访问任何节点得到的都是最新的数据

    最终一致性: 在一段时间后,节点间的数据会最终达到一致状态

    弱一致性: 即使在一段时间后, 节点间数据也不保证是一致的

  • Availability 可用性 - 总是能够正常对外提供服务, 但是不保证响应的数据是最新的

    假设一个节点挂,另一个备份节点要顶上, 也就是总能找到一个可用的数据副本

    放弃可用性的话,则在遇到网络分区或其他故障时,受影响的服务需要等待一定的时间,再此期间无法对外提供政策的服务,即不可用

  • Partition tolerance 分区容错性 - 网络分区之间的通信可能失败, 即使故障时整个系统仍然能够对外提供服务

    大多数分布式系统都分布在多个子网络。每个子网络就叫做一个网络分区(partition)

    放弃分区容错性的话,则放弃了分布式,就是一个单体 app, 分布式系统不许满足 P

以"write/read " 为例子🌰, 一个"非一致性"系统:

一个 "一致性"系统:

4.2. 保证一致性的算法

4.2.1. Raft 算法

用于替代 paxos 算法, paxos 太复杂了

http://thesecretlivesofdata.com/raft/ 动画

https://www.cnblogs.com/xybaby/p/10124083.html

动图演示: https://raft.github.io/raftscope/index.html

Raft集群包含多个服务器,5个服务器是比较典型的,允许系统容忍两个故障。在任何给定时间,每个服务器都处于以下三种状态之一,领导者(Leader),追随者(Follower)或候选人(Candidate)。 这几个状态见可以相互转换

  • Leader:处理所有客户端交互,日志复制等,一般一次只有一个Leader

  • Follower:类似选民,完全被动

  • Candidate:类似 follower 的 升级版,可以发起选举投票, 可能被选为一个新的 leader

================选举Leader 心跳机制

Leader向所有Follower发送定期心跳以保持其leader权限

所有 node 以Follower的身份开始。 只要某个 node 从Leader或Candidate接收到有效的RPC请求,该node就会保持Follower状态

如果一个Follower在一段时间内 (称为选举超时时间) 没有接到来自 leader 任何通信, 从follower切换到candidate, 并发起选举. 若收到大多数赞成票, 升级为 leader 状态. 如果发现其他节点比自己更新 (更早成为 leader), 主动切换回 follower

================ 数据复制

4.2.2. Paxos 算法

5. BASE 理论

基于CAP定理演化而来, 核心观点是: 优先满足可用性, 根据自身业务特点, 允许一段时间内的数据不一致, 保证最终一致性

  • ba : Basically Available(基本可用)

    系统基本可用, 指响应时间可以适当延长, 或者 给部分用户返回一个降级页面

  • s: Soft state(软/柔性状态)

    即允许各个节点数据短时间内不同步

  • e: Eventually consistent(最终一致性),

    数据最终是一致的,但是实时是不一致的

base 和 acid 区别:

  • acid - 刚性事务, 是 关系型数据库中的事务特性, 属于单机系统, "一致性" > "可用性", 这里的一致性 指的是 "主外键约束的完整性, 不被破坏"

  • base - 柔性事务, 属于分布式系统, "可用性" > "一致性", 并且 "一致性" 实际上是 "最终一致性";

分布式系统若遵循严格的一致性, 那就和单机系统一样 , 那就遵循单机事务的 acid

若遵循柔性事务, 那就是遵循 base 理论

6. 分布式事务

6.1. 开源方案 分布式事务



https://github.com/changmingxie/tcc-transaction 开源的TCC补偿性分布式事务框架

Seata 从两段提交演变而来, 提供了 AT、TCC、SAGA 和 XA 等事务模式

sharding jdbc 分库分表/读写分离/分布式事务
https://github.com/apache/shardingsphere
https://dbaplus.cn/news-11-1854-1.html
https://www.jianshu.com/p/bd61417ac63c
mycat 与之对比: 一个是jdbc proxy, 一个是db proxy; Mycat 是基于 Proxy,它复写了 MySQL 协议,将 Mycat Server 伪装成一个 MySQL 数据库,而 Sharding-JDBC 是基于 JDBC 接口的扩展,是以 jar 包的形式提供轻量级服务的


6.2. 事务概念and选型


本地事务: 单体范围内的事务管理.

分布式事务: 跨了多个节点的事务管理, 在分布式系统下, 将不同节点下的事务操作绑定在一起, 作为一个整体事务处理

刚性事务是指严格遵循ACID原则的事务, 例如单机环境下的数据库事务.

柔性事务是指遵循BASE理论的事务(保证最终一致性)





在实际应用中,比较常见的分布式事务实现有 2PC(Two-phase Commit,也叫二阶段提交)、TCC(Try-Confirm-Cancel) 和事务消息。每一种实现都有其特定的使用场景,也有各自的问题,都不是完美的解决方案。

- 基于 XA 协议的 2PC、3PC (依赖于数据库,提供强一致性, 但相对来说延迟比较高, 不适合高并发和高性能要求的场景)
XA 协议中分为两部分:事务管理器和本地资源管理器
其中本地资源管理器往往由数据库实现,比如 Oracle、MYSQL 这些数据库都实现了 XA 接口,而事务管理器则作为一个全局的调度者。

- 2pc: 两阶段提交
- 3pc

- 基于业务层的 TCC补偿型事务 (依赖于应用层的Commit与cancel, 强一致性, 相比于 XA, 需要丛更多的业务开发工作)

- saga

- AT

- 基于异步消息的 / 本地消息表+异步消息 (都适用于事务中参与方支持操作幂等,对一致性要求不高的场景, 基于异步消息的方案仅仅保证最终一致性)


https://heapdump.cn/article/6140580

6.2.1. 2PC

两阶段提交 (Two-phase Commit,2PC): 将分布式事务分为两阶段, 1. 准备: master 问各个 node , 是否准备好提交事务, 2. 提交: 若阶段 1 都返回 yes, 则 master 发送指令要所有 node 提交, 整体事务成功, 若不是都返回 yes , 则 整体事务失败, 成功的节点回滚

  • 准备阶段 - coordinator 询问 node 准备好你的事务提交了吗, node 回答 yes/no

    这段参与者执行了事务,但是还未提交, 并且记录 undo, redo 日志 (undo 用来记录数据被修改前的值, redo 用来记录数据将会被修改的值, 数据恢复依靠 redo和 undo 日志)

  • 提交阶段 - 如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务;

    参与者接收到协调者发来的通知后,才进行事务提交或者回滚

优点:

  • 强一致性 (只要有一个节点失败, 整体都失败)

  • 数据库原生支持, 对业务侵⼊很小,它最⼤的优势就是对使⽤⽅透明,用户可以像使⽤本地事务⼀样使⽤基于 XA 协议的分布式事务

但是问题不少:

  • 同步阻塞 - 所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作,

    当调用链很长的时候,2PC的可用性是非常低的

  • 单点问题 - 协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。

    特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。

  • 数据不一致 - 在阶段二,如果协调者只发送了部分 Commit 通知消息,此时网络发生异常

    那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致

6.2.2. 3PC

三阶段提交, 从原来的两个阶段扩展为三个阶段,并且增加了超时机制: coordinator 和 node 相互发送消息后 若长时间没收到回应, 则做出某种处理;

主要是为了解决两阶段提交协议的阻塞问题

这样三阶段提交就有 CanCommit 、 PreCommit 、 DoCommit 三个阶段

  • CanCommit: coordinator 向所有节点发送 commit 请求信息, 询问是否可以提交事务, 等待节点反馈 yes/no

  • PreCommit:

    • 若所有节点返回 yes, coordinator 向所有节点发送 preCommit 请求, 各个节点接收到请求后, 执行事务, 记录 redo/undo 日志, 但是不提交事务, 执行成功各个几点返回 ack 响应

    • 若存在节点返回 no, coordinator 发送中断abort请求, 各个节点接收到后执行事务中断

  • DoCommit: (进入阶段 3 后, 无论出现任何问题, 参与者都会在等待超时之后,继续执行事务提交)

优缺点

  • 相比二阶段提交,三阶段提交降低了阻塞范围

  • 数据不一致问题依然存在

6.2.3. TCC 失败补偿模式

补偿事务, TCC是Try、Commit、Cancel的缩写, 对于每个事务, 都有 三个方法. 其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作

本质就是在 应用层, 使用业务编码的方式, 保证一致性,

对分布式事务中的各个资源进行分别锁定, 分别提交与释放, 例如, 假设有AB两个操作, 假设A操作耗时短, 那么A就能较快的完成自身的try-confirm-cancel流程, 释放资源. 无需等待B操作. 如果事后出现问题, 追加执行补偿性事务即可

分为三个阶段:

  • try 阶段 - 对业务系统做检测及资源检查

    如: 转账, commit 前要首先冻结 双方账户

  • commit 阶段 - 对业务系统做确认提交

    Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。

  • cancel 阶段 - 在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放

    满足幂等, 可以支持重试

优点:

  • 提升性能: Try操作可以灵活选择业务资源的锁定粒度,而不是锁住整个资源,提高了并发度

  • 满足严格的一致性, 适用于 红包, 收付款业务

缺点:

  • 代码很繁琐, 对应用的侵入性强。业务逻辑的每个分支都需要实现try、confirm、cancel三个操作

    confirm和cancel接口必须实现幂等

  • 还是有数据一致性问题 - 第三步有可能失败, 造成回滚不成功

6.2.4. 基于异步消息

6.2.4.1. 只使用消息中间件

遵循 base 理论, 也就是 柔性事务, 通过将一系列同步的事务操作变为基于消息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响. 最终一致性

  1. node1发送远程事务消息到MQ Server;

    保证本地事务的提交与消息发送这两个操作的原子性

  2. MQ Server给予响应, 表明事务消息已成功到达MQ Server.

  3. node1 Commit本地事务.

  4. 若本地事务Commit成功, 则通知MQ Server允许对应事务消息被 node 2 消费; 若本地事务失败, 则通知MQ Server对应事务消息应被丢弃.

    若 node1 超时未对MQ Server作出本地事务执行状态的反馈, 那么需要MQ Server向 node1 主动回查事务状态, 以决定事务消息是否能被消费.

  5. node2 从 mq server 消费被允许的消息

    如果MQ订阅方 node 2 执行远程事务成功, 则给予消费成功的ack, 那么MQ Server可以安全将事务消息移除;

    如果执行失败, MQ Server需要对消息重新投递, 直至node2消费成功.

消息中间件也需要支持 HAC 来确保事务消息不丢失. 还可能需要对消息中间件增加消息不重复, 不乱序等其它要求

6.2.4.2. 本地消息表 + mq

结合消息中间件和数据库表, 各系统本地的事务来实现分布式事务, 思路是来源于ebay

创建 本地消息表/操作流水表, 保证业务操作和流水插入在同一个事务中. 并且使用消息队列来保证最终一致性

流程:

  • 在某节点 node1 完成 写库后(子事务), 向本地消息表保存一条记录, 状态为 "发送中", 代表这个操作, 使用本地事务保证这条记录一定被写入

  • node 1 同时向消息队列中投递一条消息数据, 同消息表中的数据

    消息恢复系统: 引入一个定时任务, 每隔一段时间去本地消息表中捞取状态为“发送中”的消息,然后重新投递到mq中间件中

  • node2 订阅了消息, 开始消费 (幂等), 成功后, 反向向mq投递一条消费成功的消息到另外的 ack_queue 队列

  • node 1 订阅 ack_queue, 开始消费这个反馈消息, 将本地消息表的消息状态改为“已发送”

  • 新开一个服务, 定时轮询将本地消息表中的消息转发到 Kafka 等消息队列中,如果转发成功则将消息从本地消息表中删除 (or 不删除而使用 status 字段标注)

  • node2 从 消息队列中读取一个消息,并执行消息中的操作, 回调通知 node1 成功则 删除 消息表记录 or 更新 消息表 status 字段; 失败则 node1 回滚

优点:

  • 避免了分布式事务,实现了最终一致性

缺点:

  • 消息表会耦合到业务系统中, 开发量大

7. 分布式锁

分布式系统下加锁,

  • 需要独立的外部存储空间

    保证两个节点都可以访问到

  • 需要唯一标识

  • 至少需要 2 种状态 (锁定 / 释放 状态)

有如下的解决方案:

  • 借助数据库加锁 (行锁、version乐观锁)

    数据库表,字段为锁的ID(唯一标识),锁的状态(0表示没有被锁,1表示被锁)

    quartz集群架构中就有所使用

    问题太多, 一般不用

  • 系统外缓存 如redis , 借助 setnx 指令

    问题: 为了防止主机宕机或网络断开之后的死锁,Redis没有ZK那种天然的实现方式,只能依赖设置超时时间来规避

    推荐 Redisson 这个 redis client, 提供许多分布式功能

  • Zookeeper

    ZK中还有一种名为临时有序节点, 创建后会有一个编号依次增加

    而且由于是临时节点, client 断开就删除, 天然防止了死锁

    还用到了 watch机制 (唤醒等待的线程)

    过程是这样:

    1. client create 临时有序 node (比如在目录 '/lock' 下创建 'node1')

    2. client 获取 /lock 下的所有 children node

    3. 比较 node1 和其他 node 的序号, 如果是最小的, 则获取 锁, 如果不是最小的, 则睡眠, zookeeper监测比 node1 小的前一个 node0, 若 node0 删除则唤醒睡眠线程, 即获取锁

    4. 释放锁, 即删除 node1

8. 分布式 id 生成

使用数据库的自增ID,虽然简单,会带来ID重复的问题,并且单机版的ID自增,并且每次生成一个ID都会访问数据库一次,DB的压力也很大

  • 基于 UUID, 不是自增, 不利于检索

  • 为水平拓展的节点设置不同的起始 id 和相同的步长(步长为 节点数), 如共有三个节点, node1 的 id 会是 1, 4, 7..., node2 会是 2, 4, 8..., node3 是 3, 6, 9...

    通过 sequence 设置

    这种方式缺点是 不好拓展.如果确定后期会扩容, 需要将步长设置大一点, 预留一些初始值给后续扩容使用

  • 批量申请自增 id (号段模式): 一次性申请一批 可用 id, 快用完了再次申请, 始终保证内存中有可用的 id https://github.com/didi/tinyid

    需要数据表配合: primary_key_table(id, biz_type, max_id, step, version),

    biz_type 可选, 表示业务类型, 为不同业务做id隔离, max_id 表示当前可用 id 最大到多少,step 代表每次申请多少 id数量, 可以固定设置一个值, 一般为 1000 够了, 越大 qps 越高可根据业务类型决定大小; version 为乐观锁, 每次更新都加一, 保证并发更新的正确性

    流程: 查询 max_id 信息, 计算新的最大可用 id 为 new_max_id, 更新 new_max_id 到数据库, 同时 version + 1, 若更新成功, 则获取 id 成功, 为 (max_id, new_max_id], 若更新失败, 则号段可能被其他线程获取, 重试

  • 中间件: redis 方式

    因为 redis 单线程的排他性, 保证的生成id 的唯一, 每次先+1再获取 (incr和increby )

    改进: 每次先+1000再获取, 拿到本地后, 0~1000 慢慢用, 用完了再获取 1001~2000, 一个用户服务宕机了,也顶多一小段userId没有用到

    手写工具类代码 TODO

  • 雪花算法

    snowflake (雪花) 算法: 同时使用了时间戳、机器号、进程号以及随机数来保证唯一性。

9. 并发请求数据冲突-超卖问题

比如电商的 "超卖问题": 并发的请求几乎同时到达, 因此页面展示可能来不及反应商品数量变化, 导致两个请求购买到了同一个商品

通过redis解决:

  • 下单成功后, 立即向 redis 中存储 商品编号 (可以设置过期时间)

  • 每次下单前, 先到 redis 检查, 看看当前 want buy 的 商品是否存在于 redis, 若存在, 则下单失败, 否则下单成功

10. 幂等问题

就是对接口的多次调用所产生的结果和调用一次是一致的

为什么需要: 分布式系统因为网络原因, 造成重试. 或者 用户重复点击提交

=============分析 and 解决方案:

  • 在最前端控制

    比如: 页面进行防止重复点击

  • 在系统设计层面控制, 每次请求过来, 先进行校验

    • 涉及到金钱的进行强校验. 如: 本地数据库增加一个流水表 包括 (id, 订单号, 业务场景id), 借助本地事务保证校验一定完成.

      过程: 处理请求后, 在同个事务中增加一条流水, 以后再处理每个请求, 先到 通过 订单号, 业务场景id 到 流水表中查找, 查到则拦截, 查不到放行.

      另外, 硬盘中有持久化的数据心里有底:)

    • 其他不是很重要的场景做弱校验.如: 通过 redis + sign 进行弱校验:

      sign 是 通过请求参数+url 经过加密生成的字符串, 第一次请求完成后就保存到 redis, 后续若有第二次请求, 首先拿到 sign 和 redis 中的的比对, 若相同则证明是第二次访问了, 拒绝;

      是不是每个请求都需要在 redis 中缓存一个 sign, 那 redis 爆炸了怎么办? 不会, 我们会给每个 sign 设置一个过期时间, 这个时间和 请求认证拿到的 token 过期时间一致, 若 redis 中的签名过期了, 第二次请求也不会通过, 因为 认证 token 也过期了, 也会被拦截.

      如果不是分布式系统, 就不需要 redis ,token 直接放在jvm 即可, 如生成后放在 session 中

  • 在业务层面控制, 比如 将增量修改变为普通等量修改

    就是将涉及数字加减的操作改为操作布尔类型的状态字段

    譬如说需求是:当用户点击赞同时,将答案的赞同数量+1。改为:当用户点击赞同时,确保答案赞同表中存在一条记录,用户、答案。赞同数量由答案赞同表统计出来

  • 在数据库层面控制, 比如 唯一索引.

    对于新增操作, 当然是对 业务id (比如邮箱) 设置唯一索引

11. 限流

  • Semaphore 控制并发数量

  • 漏桶算法, 令牌桶算法控制访问速率

    Guava提供的限流工具类RateLimiter来实现控制速率,该类基于令牌桶算法来完成限流

  • 分布式限流 redis (注解+aop)

  • nginx 限流

12. 负载均衡

ref: https://www.cnblogs.com/xybaby/p/7867735.html#_label_14

12.1. 负载均衡算法

12.1.1. 轮询

round-robin

各个节点逐个对外提供服务

没有考虑不同节点 的机器性能不同.比如, 也许同样数目的请求,高配的机器CPU才20%,低配的机器CPU已经80%了

12.1.2. 加权轮询

weight round-robin

在轮训算法的基础上,考虑到机器的差异性,分配给机器不同的权重,能者多劳

这个权重的分配依赖于请求的类型,比如计算密集型,那就考虑CPU、内存;如果是IO密集型,那就考虑磁盘性能

12.1.3. 随机算法

random

随机选择一个节点服务,按照概率,只要请求数量足够多,那么也能达到绝对均衡的效果。而且实现简单很多

12.1.4. 加权随机算法

weight random

在随机的时候引入不同节点的权重

12.1.5. 哈希法hash

根据客户端的IP,或者请求的“Key”,计算出一个hash值,然后对节点数目取模。好处就是,同一个请求能够分配到同样的服务节点,这对于“有状态”的服务很有必要

只要hash结果足够分散,也是能做到绝对均衡的

哈希算法的缺陷也很明显,当节点的数目发生变化的时候,请求会大概率分配到其他的节点,引发一系列问题,比如sticky session。而且在某些情况,比如分布式存储,是绝对的不允许的

12.1.6. 一致性哈希hash

http://www.zsythink.net/archives/1182

为了解决取余hash伸缩性差的问题, 一致性hash则利用hash环对其进行了改进。

每个节点 hash(ip), 按照顺序均匀分布在环形上作为虚拟节点(环形有刻度, 0 ~ 正无穷(0)), 接受到请求, hash(req_ip或者user_id)肯定会落到环形上的某个刻度, 顺时针找到最近的 node 处理这个请求

调整每个物理节点对应的虚拟节点数目,也就相当于每个物理节点有不同的权重

12.1.7. 最少连接算法(least connection)

一般其他算法, 要么没有考虑到节点间的差异(轮询、随机、哈希),要么节点间的权重是静态分配的(加权轮训、加权随机、一致性hash)

13. 分布式 session 一致性问题 sso (single sign on)

13.1. 单系统中的登陆是怎样的

web 应用采用 B/S 架构, http 作为通信协议, 是 stateless 的

client 首次请求 server, server 会创建一个 session, 将这个 session 的 id 返回给 client, 后续的 请求, client 都会带上这个 id, 这样后续的请求和第一个请求 就关联了

浏览器端使用 cookie 机制 来保存 session id, (cookie是浏览器用来存储少量数据的一种机制,数据以”key/value“形式存储,浏览器发送http请求时自动附带cookie信息)

如果 web 容器使用 tomcat, 浏览器中可以看到一个名为 "JSESSIONID" 的 cookie, 这就是 tomcat 中的会话机制维护的会话id

//浏览器第一次请求服务器需要输入用户名与密码验证身份,服务器拿到用户名密码去数据库比对,正确的话说明当前持有这个会话的用户是合法用户,应该将这个会话标记为“已授权”
HttpSession session = request.getSession();
session.setAttribute("isLogin", true);

//再次访问时,tomcat在会话对象中查看登录状态
HttpSession session = request.getSession();
session.getAttribute("isLogin");

13.2. 多系统中的登陆 sso

单系统登陆核心是 cookie , 原样照搬到多系统登陆会碰到一个限制: cookie 的域(通常对应网站的域名)--> --> 浏览器发送http请求时会自动携带与该域匹配的cookie,而不是所有cookie

这对这个限制, 有个粗暴的解决方法: 将多个子系统的域名统一在一个顶级域名下, 如 "*.baidu.com", 然后将所有 cookie 的域设置为 "baidu.com"; 这样做理论可行, 但是会进一步带来更多限制 ==> 💊 子系统必须统一域名, 💊 子系统使用的技术需要统一, 不然 cookie 的 key 无法统一(比如 tomcat 的 为 JSESSIONID), 无法维持会话, 💊 cookie本身不安全.

单点登陆 ==> 单点登录全称Single Sign On(以下简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分

早先的架构是单一系统, 登陆只需要在唯一的系统中保存 session 即可. 随后架构演进为多系统架构, 在某一个子系统中保存 session 行不通了, 因为登陆进一个子系统, 然后进入其他子系统仍然需要登陆(因为其他子系统并没有保存 session ), 这明显很傻.

单点登陆(SSO)由此而生: SSO 提供一个单独的认证中心, 其他子系统没有登陆入口, 只接受认证中心的授权(即令牌), 验证中心验证用户密码没问题则创建全局会话 , 然后创建授权令牌, 跳转到各个子系统, 子系统拿到令牌即拿到授权, 借此创建局部会话(这一步就和单系统登陆相同了).

单点登陆

在线预览

有这样几个角色 : client/浏览器, sys1, sys2, sso认证中心

  1. 用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
  2. sso认证中心发现用户未登录,将用户引导至登录页面
  3. 用户输入用户名密码提交登录申请
  4. sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌
  5. sso认证中心带着令牌跳转回最初的请求地址(系统1)
  6. 系统1拿到令牌,去sso认证中心校验令牌是否有效
  7. sso认证中心校验令牌,返回有效,注册系统1
  8. 系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
  9. 用户访问系统2的受保护资源
  10. 系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
  11. sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌
  12. 系统2拿到令牌,去sso认证中心校验令牌是否有效
  13. sso认证中心校验令牌,返回有效,注册系统2
  14. 系统2使用该令牌创建与用户的局部会话,返回受保护资源

用户登录成功之后,会与sso认证中心及各个子系统建立会话,用户与sso认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过sso认证中心

单点注销

在线预览(包括登陆和注销)

sso认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知所有注册系统执行注销操作

  1. 用户向系统1发起注销请求
  2. 系统1根据用户与系统1建立的会话id拿到令牌,向sso认证中心发起注销请求
  3. sso认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址
  4. sso认证中心向所有注册系统发起注销请求
  5. 各注册系统接收sso认证中心的注销请求,销毁局部会话
  6. sso认证中心引导用户至登录页面

13.3. 其他解决方案

  • hazelcast + spring-session 外部session解决方案
  • redis + spring-session 方案
  • nginx 做 ip_hash, 将请求路由到特定的 server, 请求的 session 位于集群的那个 节点 , 就将请求路由到那个节点, (容易产生单点故障问题)

13.3.1. session 复制同步

利:

  • Tomcat 原生支持, 只需要修改配置文件, 无需改动代码

  • 复制操作会消耗机器额外性能

13.3.2. 客户端存储 session

将 session 加密后存储在 cookie中, 每个 client 都存储自己的 session

  • 服务端无需存储

  • cookie 有大小限制
  • 每次 http 请求都得带上 session

13.3.3. NGINX 中对 ip 进行hash 路由

反向代理层使用用户ip来做hash,以保证同一个ip的请求落在同一个web-server上

即Nginx 四层负载均衡方式,其实 Nginx 还可以做到七层负载均衡方式,也就是使用 Http 协议中的一些业务属性来做 Hash,常见的有 userId,loginId等等

13.3.4. 后端集中 session

比如 session 集中存储到 redis, 可以借助 spring-session 集成

  • 应用水平拓展方便

  • 需要对相应的代码做出修改,这样复杂度就变高
  • 每次请求都需要调用一次 Redis ,这就增加一次网络的开销

14. 怎么实现子系统间的通信 进程通信

  • springcloud 支持 restful api 调用
  • dubbo可以支持多种通讯协议
  • 自己封装
    • socket 编程
    • netty 框架
  • 使用rpc 协议规范, 具体实现如 grpc, thrift
  • WebService 协议规范 - 语言无关的系统通信标准; server端提供的 http 接口 发布生成 wsdl 文件, 和应用一起放入 http 服务器中发布, client 根据 wsdl 文件 生成辅助代码, 这个代码作用是将请求信息封装为标准的 soap 格式数据, 然后发送到服务端, server端反射调用相关类; jdk6 集成了 webservice (@WebService 注解)

15. 灰度发布

将服务器集群分成若干部分,每天只发布一部分机器,观察运行稳定没有故障,第二天继续发布一部分机器,持续几天才把整个集群全部发布完毕,期间如果发现问题,只需要回滚已发布的一部分服务器即可

16. 分布式架构之soa

soa: service oriented architecture 面向服务架构. 各个子系统 (子系统可以各不相同, 架构, 语言...) 间以统一的 "服务方式" 进行交互.

soa 并没有给出具体的实现方式, 目前业界有两套实现方式:去中心化 (spring cloud, dubbox) and 中心化 (esb)的方式

引入 soa 会带来新的问题:

  • 网络延时, 尤其是多级服务调用.

    在每一级服务执行完需要检测是否超时, 如果超时, 就直接抛出 超时异常而不要继续下一级服务调用

  • 各个服务负责人不同, 调试跟踪困难

  • 系统的安全/监测, 流量控制

  • 老系统改造移植

16.1. 基于 sca 实现 soa

sca (service component architecture) 是几大公司 指定的 实现 soa 架构的具体实施规范

sca 涉及内容:

  • 如何发布服务
  • 服务符合调用
  • 支持的通信方式

Tuscany 框架 基于 sca 规范, 可用来实现 soa 架构

16.2. 基于 esb 实现中心化的 soa

和 sca 不同 (sca 是多个厂家联合指定的 soa 实现标准), esb 只是一个概念 (check : https://www.slideshare.net/MuraliMalli/mule-esb-demo)

esb核心内容如下: 主要思想就是基于消息中间件 (总线) 实现系统间交互

  • 要有统一的消息格式
  • 消息路由 - 总线根据传进来的消息决定调用哪个子系统
  • 支持多种消息交互类型 - 比如 请求/响应 (适用于 同步请求), 发布/订阅 (适用于 异步的消息广播)
  • 支持多种网络协议 - 比如 tcp/ip, udp/ip, http
  • 支持多种数据格式 - 消息中的数据格式可能不一致, 总线要能够在不同的数据格式间转换

Mule 是 esb 实现框架之一, 可以方便的连接各个系统 , 基本概念check http://www.cnblogs.com/liangqihui/p/7905310.html

16.3. 基于spring cloud 实现去中心化的soa

也就是微服务

17. 微服务 micro service

17.1. 为什么需要微服务

传统的web开发方式:所有的功能打包在一个war包,部署在一个容器

  • 优点:开发简单,集中式管理不会重复开发,没有分布式的管理开销调用开销

  • 缺点

    • 开发效率低:所有的开发在一个项目改代码,递交代码相互等待,代码冲突不断
    • 技术选型单一: 比如只能使用Java, 无法在一个系统中使用多种语言
    • 部署不灵活:任何小修改必须重新构建整个项目,这个过程往往很长
    • 扩展不灵活: 水平扩展(即前端加一个 load balancer)会带来资源浪费(如: moduleA, B 资源占用小, C占用多, 无法单独扩展C)

17.2. 什么是微服务架构

  • 将原来的单体app 根据业务划分为多个服务
  • 每个服务可单独部署
  • 通过暴露出来的api互相调用(http, rpc)
  • 每个服务需要保证高可用(每个服务需要有候补节点)

优缺点:

  • 优点:开发简单、服务独立无依赖、拓展方便
  • 缺点:多服务运维难、通信成本、数据一致性、系统集成测试

一般架构是这样:

  1. 负载均衡层
  2. 网关(内部gw, 第三方gw)
  3. 业务服务层 (如订单服务, 库存服务, 用户服务)
  4. 支撑服务 (注册发现, 配置中心, 日志聚合, 监控告警)
  5. 平台服务 PaaS( 发布系统, 镜像治理, 资源治理)
  6. 基础设施 IaaS (计算, 网络 , 存储...)

17.3. 如何搭建微服务架构

17.3.1. 整体选型

https://www.infoq.cn/article/micro-service-technology-stack https://zhuanlan.zhihu.com/p/94488453 https://blog.csdn.net/hardworking0323/article/details/81170961

https://blog.csdn.net/mulinsen77/article/details/84583716 https://www.cnblogs.com/waterystone/p/4920797.html https://www.cnblogs.com/shijiaqi1066/p/3412346.html

17.3.2. 微服务系统要素

  1. 服务注册、发现、负载均衡,容错(服务注册中心->兼配置中心)
  2. 服务间的通信(消息模式,RPC模式)
  3. api网关(路由,监控,安全认证,日志,限流)
  4. 微服务周边设施:日志中心,监控中心 ,文档自动生成

17.3.2.1. 客户端访问服务 API Gateway

网关选型

  • 提供统一服务入口,让微服务对前台透明
  • 聚合后台的服务,节省流量,提升性能
  • 提供安全,过滤,流控等API管理功能

17.3.2.2. 服务间通信

  • 同步调用

一致性强,但是性能稍差,如:
REST(JAX-RS,Spring Boot):容易实现、使用范围广
RPC(Thrift, Dubbo):传输协议更高效,安全可控

  • 异步消息调用

数据一致性减弱,后台服务需要实现幂等性;
(Kafka, Notify, MetaQ)

17.3.2.3. 服务注册、发现

  • 在微服务架构中,一般每一个服务都是有多个拷贝,来做负载均衡
  • 服务发现基本都是通过zookeeper等类似技术做服务注册信息的分布式管理。

17.3.2.4. 服务挂了怎么办-熔断降级

  • 重试机制
  • 限流
  • 熔断机制
  • 负载均衡
  • 降级(本地缓存)

17.3.2.5. 运维的压力怎么解决

docker

17.4. 相关开源项目

  • Spring Cloud:提供一组工具,构建分布式系统中的common patterns,如:配置管理,服务发现,网关

  • jhipster:代码生成工具。可以生成整套微服务架构项目基础代码,后端使用spring(spring cloud ,spring boot等),前端使用AngularJS。

    • jhipster-registry:基于 Spring Cloud Netflix Eureka 和 Spring Cloud Config,实现服务注册、发现,配置管理等

    • jhipster-console:基于ELK,实现日志中心和监控中心。

  • spring cloud alibaba + dubbo

备注:

注册中心-JHipster Registry

网关--JhipsterSampleGateway

分布式日志和全文检索--jhipster console

Jenkins持续集成

sonarqube代码质量控制

mongoDB

分布式文件系统CEPH API:http://docs.ceph.org.cn/radosgw/s3/java/

jhipster各组件启动顺序:

  1. 启动jhipster-registry --服务和网关依赖注册中心,如果不先启动注册中心,服务和网关启动不了。
  2. 启动jhipster-console--如果使用这个控制台的话需要优先于具体服务启动
  3. 启动具体服务--例如订单服务,商品服务。服务启动时会把自己注册到注册中心,方便别的服务或网关调用
  4. 启动网关--这里的网关指jhipster生成的gateway类型的项目,是一个基于angular的前端

18. zookeeper

18.1. zk是什么

分布式系统协调组件

核心原理: 功能主要是它的树形节点来实现的。当有数据变化的时候或者节点过期的时候,会通过事件触发通知对应的客户端数据变化了,然后客户端再请求zk获取最新数据,

zab 协议:

  • 消息广播: 集群中zk在数据更新的时候,通过leader节点将将消息广播给其他follower节点,采用简单的两阶段提交模式,先request->ack->commit,当超过一半的follower节点响应ack, 就可以更新内部数据

  • 崩溃恢复: 当leader挂了,或者超半数follower投票得出leader不可用,那么会重新选举,这段期间zk服务是不可用的; 选举出来后需要将新的leader中的数据更新给超过半数的follower节点才能对外提供服务

18.2. 为什么使用zk

18.2.1. 解决什么问题

  • 注册中心
  • 配置中心

18.2.2. 选型对比 使用场景

18.3. 怎么用zk

18.3.1. 命令行使用 zk client

# 进入命令行客户端
docker run --link zookeeper-x:zk -it --rm zookeeper zkCli.sh -server zk
ls /
get /
create -e /k1 v1 # 创建临时节点

18.3.2. 作为微服务注册中心

spring cloud 提供了 zookeeper client, 配置 connect-string (格式 host:port )即可. 会在 zk 中创建临时节点, 位于 /services/(application-name)/(uuid)

相比 eureka 保证 ap, zk 优先保证 cp, 只要 client 断开和 server的心跳, zk 马上就会删除对应的临时节点

docker run -d --name zookeeper-x -p 2181:2181 zookeeper
docker logs -f --tail=10 zookeeper-x

19. etcd

19.1. etcd 介绍

分布式 key-value 存储系统, 可用于配置共享, 服务注册/发现, 类似 zookeeper, consul

  • 完全复制: 集群中,每个节点都可以使用完整的存档。
  • 高可用, 强一致性: 使用 raft 算法保证
  • 用户友好的 api (grpc)
  • 类似 zk, 数据以树结构存储
  • 监控key 以及下属数据的变化, 主动通知订阅者

19.2. 为什么使用etcd

19.2.1. etcd使用场景

- 配置中心

将配置数据放在etcd, 每个节点从 etcd 订阅获取这些配置, 同时etcd 注册一个 watcher, 监控配置是否修改, 一旦修改, 实时通知订阅者

- 分布式锁 (由强一致性保证)

锁服务有两种使用方式:
- 保持独占, 即多个 client 同时获取锁, 只有一个最终能够获取到
- 保证获取顺序, 即会给多个 client 排序, 这个排序会固定

19.2.2. 为什么不使用 zookeeper 而使用 etcd

zk 缺点:

  • zookeeper 部署维护复杂 , 使用的算法 Paxos 也比 Raft 复杂

  • 跑在 jvm, 体积大, 重量级, 更消耗资源

etcd 优点:

  • 简单, 部署维护简单, 提供 http 接口
  • 持久化简单, 默认就是一更新就持久化
  • 安全, 支持 ssl 客户端安全认证

19.2.3. 和 redis 对比

  • redis 没有版本的概念,历史版本数据在大规模微服务中非常有必要,对于状态回滚和故障排查,甚至定锅都很重要
  • redis 的注册和发现目前只能通过 pub 和 sub 来实现,这两个命令的性能在低延迟和吞吐量方面完全不能满足生产环境的要求
  • redis 和 etcd 的集群实现方案是不一致的,etcd 采用的是 raft 协议,一主多从,只能写主,底层采用 boltdb 作为 k/v 存储,直接落盘; redis 的持久化方案有 aof 和 rdb,这两种方案在宕机的时候都或多或少的会丢失数据

19.3. etcd如何使用

19.3.1. 集群部署 etcd

一般部署奇数台节点(3, 5, 7)集群

高可用方案 HA


核心是 监控集群系统中各个服务节点的状态:

Keepalived 轻量级别的高可用解决方案
依据一个VRRP协议,利用VRRP维持主备节点的心跳[1],部署方式比较简单,一般用于Web应用的高可用,最常见和Nginx一起使用, 也有和做mysql的高可用

Keepailived有一台主服务器和多台备份服务器,在主服务器和备份服务器上面部署相同的服务配置,使用一个虚拟IP地址对外提供服务,当主服务器出现故障时,虚拟IP地址会自动漂移到备份服务器。

https://www.jianshu.com/p/ac538fa4c175

https://github.com/galtjay/keepalived-docker-compose 基于docker

HeartBeat 功能强大, 部署和使用相对比较麻烦