《软件架构设计》笔记

文章目录

  • 架构的分类
  • 内功
  • 语言
  • 操作系统
    • IO
      • IO涉及的概念
      • IO读写对比
      • 网络IO模型
        • 同步阻塞IO
        • 同步非阻塞IO
        • 多路复用
        • 异步IO
      • Reactor模式与Preactor模式
      • 服务器编程1+N+M
    • 进程、线程、协程
    • 无锁
      • 内存屏障
      • CAS
  • 网络
    • HTTP1.0
    • HTTP1.1
    • HTTP/2
    • SSL/TLS
    • HTTPS
    • TCP(Transmission Control Protocol)
    • UDP(User Datagram Protocol)
    • QUIC(Quick UDP Internet Connection)
  • 数据库
    • 数据库范式与反范式
    • 分库分表
    • 分布式ID生成
    • 拆分维度选择
    • join查询问题
    • 分布式事务
    • B+树
      • 逻辑结构
      • 物理结构
      • 非主键索引
    • 事务和锁
      • 事务隔离级别
      • 事务实现原理
        • Write-ahead Log(WAL)
        • redoLog
        • undoLog
        • mysql的锁
    • BinLog
      • 2PC
  • 技术架构之道
    • 高并发
      • 高并发读
      • 高并发写
      • 高并发读写
      • 容量规划
    • 高可用
      • 多副本
      • 隔离
      • 限流
      • 熔断
      • 降级
      • 灰度发布
      • 回滚
      • 监控体系
      • 日志报警
  • 事务一致性
    • 强一致性
      • 2PC
      • 3PC
    • 最终一致性
      • 不支持事务消息
      • 支持事务消息
      • TCC
      • SAGA
    • 非标准方案(不完善、最终一致性)
      • 事务状态表+调用方重试+接收方幂
      • 对账
  • 多副本一致性
    • Paxos
      • Basic-Paxos
      • Mutil-Paxos
      • 精髓
    • Raft
      • 选举
      • 日志复制
      • 恢复阶段
    • ZAB
      • 选举
      • 日志复制(2PC)
      • 恢复阶段
  • CAP
  • 业务架构
    • 业务意识
      • 需求分析、产品经理
      • 业务闭环
      • 组织架构、业务架构、技术架构
    • 业务架构思维
      • 分层思维
      • 边界思维
      • 系统化思维
      • 利益相关者分析
      • 非功能性需求
      • 视角
      • 抽象
      • 建模
      • 正交分解
    • 技术架构与业务架构融合
    • 个人素质
    • 不确定性
    • 业务价值的层次
    • 团队培养

架构的分类

  • 基础架构

  • 中间件与大数据平台

  • 业务系统架构

内功

操作系统、网络、数据库

什么功能已经有了,什么还没有
熟系原理,更容易理解实现,有什么潜在问题
思维借鉴

TCP如何在一个“不可靠”的通信网络上实现一个“可靠”的通道,比如数据库如何利用Write-ahead Log解决I/O问题,利用Checksum保证日志完整性,利用 MVCC(CopyOnWrite)解决高并发问题。

语言

  • 都有一个基本数据类型的集合(比如Java是8种基本数据类型);
  • 都有类型转换、类型推断、类型安全方面的机制;
  • 都是顺序、选择、循环三种语句类型;
  • 都有类、对象、封装、继承、多态(如果是面向对象的);·
  • 都有一个常用数据结构的库(数组、栈、队列、链表、Hash);
  • 都有一个常用的I/O库;
  • 都有一个常用的线程库(协程库);

操作系统

IO

  • 缓存IO c语言库函数,f打头
  • 直接IO liunx系统API

IO涉及的概念

  • 应用程序内存

    malloc/free 分配的内存 对象?

  • 用户缓冲区

    buffer Stream?

  • 内核缓冲区

    Page Cache

  • 磁盘

  • socket缓冲区

    send Buffer/recv Buffer

  • 网络

IO读写对比

  • 缓存IO的读写:3次copy

读:磁盘→内存缓冲区→用户缓冲区→应用程序内存

写:应用程序内存→用户缓冲区→内存缓冲区→磁盘

函数:fflush、fsync

  • 直接IO读写:2次copy

读:磁盘→内核缓冲区→应用程序内存;

写:应用程序内存→内核缓冲区→磁盘。

函数:read/write、pread/pwrite

  • 内存映射

读:磁盘→应用程序内存映射的内核缓冲区;

写:应用程序内存映射的内核缓冲区→磁盘。

函数:mmap java:MappedByteBuffer

  • 零拷贝(网络的概念)

读:socket映射内核→网络

函数:sendFile java:FileChannel.transferTo

拷贝是把数据从一块内存中复制到另外一块内存里;映射相当于只是持有了数据的一个引用(或者叫地址),数据本身只有1份。

把文件数据发送到网络的这个场景,直接I/O、内存映射文件、零拷贝对应的数据拷贝次数分别是4次、3次、2次,内存拷贝次数分别是2次、1次、0次。

直接IO:磁盘→内核缓冲区→应用程序内存→socket缓冲区→网络

内存映射:磁盘→应用程序映射的内核缓冲区→socket缓冲区→网络

零拷贝:磁盘→socket映射的内核缓冲区→网络

网络IO模型

同步和异步

  • 同步:读写由应用程序完成。等待或者轮询都是应用程序做的。
  • 异步:读写由操作系统完成,完成之后,回调或者事件通知应用程序。

阻塞和非阻塞

  • 阻塞:如果读写没有就绪或者读写没有完成,则该函数一直等待。
  • 非阻塞:函数立即返回,然后让应用程序轮询。
同步阻塞IO

read/write函数,用户线程等I/O完成,磁盘到应用程序的过程都得block等待。

同步非阻塞IO

O_NONBLOCK/O_NDELAY,打开fd的时候带有O_NONBLOCK参数。于是,当调用read和write函数的时候,如果没有准备好数据,会立即返回,不会阻塞。然后让应用程序不断地去轮询很多fd,应用程序的线程是一直被占用轮询的。

多路复用

select、poll、epoll

epoll:

(1)事件注册。通过函数epoll_ctl实现。对于服务器而言,是accept、read、write 三种事件;对于客户端而言,是connect、read、write 三种事件。

(2)轮询这三个事件是否就绪。通过函数epoll_wait实现。有事件发生,该函数返回。

(3)事件就绪,执行实际的I/O操作。通过函数accept/read/write实现。

异步IO

windows iocp,aio(不成熟),读写由底层完成(操作系统或者框架),读写完成之后,以某种方式通知应用程序。

Reactor模式与Preactor模式

网络框架的两种设计模式,无论操作系统的网络 I/O 模型的设计,还是上层网络框架的网络I/O模型的设计,用的都是这两种设计模式之一。

Reactor模式

主动模式。所谓主动,是指应用程序不断地轮询,询问操作系统或者网络框架、I/O是否就绪。Linux系统下的select、poll、epoll就属于主动模式,需要应用程序中有一个循环一直轮询;Java中的NIO也属于这种模式。在这种模式下,实际的I/O操作还是应用程序执行的。

Proactor模式

被动模式。应用程序把read和write函数操作全部交给操作系统或者网络框架,实际的 I/O 操作由操作系统或网络框架完成,之后再回调应用程序。asio 库就是典型的Proactor模式。

服务器编程1+N+M

1+N+M个线程,一个监听线程,N个I/O线程,M个Worker线程。N的个数通常等于CPU核数,M的个数根据上层决定,通常有几百个。

  1. 监听线程:负责accept事件的注册和处理。和每一个新进来的客户端建立socket连接,然后把socket连接移交给I/O线程,完成任务,继续监听新的客户端;
  2. I/O线程:负责每个socket连接上面read/write事件的注册和实际的socket的读写。把读到的Reqeust放入Request队列,交由Worker线程处理。
  3. Worker线程:纯粹的业务线程,没有socket读写操作。对Request队列进行处理,生成Response队列,然后写入Response队列,由I/O线程再回复给客户端。

进程、线程、协程

  • 进程

    优势:通过ipc通信,不需要锁;进程独立运行,安全性高,并发效率高。nginx的worker。

    劣势 :需要系统提供IPC机制;数量级少,和cpu数差不多;通信需要异步IO支持。

  • 线程

    优势:方法成熟,功能强大;线程数能到几百;

    劣势 :锁;线程切换开销

  • 协程

    优势:上万个;堆栈不固定,内存利用率高

    劣势 :go、rust支持,java不成熟

无锁

内存屏障

RingBuffer,允许一个线程写、一个线程读。示例:Disruptor,其核心就是“一写多读,完全无锁”。

CAS

CPU层面提供的一个硬件原子指令,实现对同一个值的Compare和Set 两个操作的原子化。示例:实现乐观锁、无锁队列、无锁链表

网络

HTTP1.0

请求一来一回,不支持服务推送

连接建立、关闭性能差?

keep-alive,请求和返回加keepalive 复用tcp连接;通过timeout关连接。

连接复用了,客户端怎么确定消息传完了?

Content-Length

HTTP1.1

1.0的Response Content-Length要计算?

Chuck机制:将分块 25-1C-…0

连接复用后,请求串行,并发度不够?

pipline,多请求并行发送,返回按发送顺序返回。请求1如果响应时间长,2、3不能先返回,这个叫队头阻塞(Head-of-Line Blocking)。所以很多浏览器就关掉了这个功能。

pipline关掉后,性能提升的一些奇巧淫技

图片拼接、js压缩、多域名(浏览器单一域名下只能发6到8个连接)

断点下载

客户端从服务器下载文件时,如果下载到一半连接中断了,再新建连接之后,客户端可以从上次断的地方继续下载。具体实现也很简单,客户端一边下载一边记录下载的数据量大小,一旦连接中断了,重新建立连接之后,在请求的头部加上Range:firstoffset-last offset 字段,指定从某个offset下载到某个offset,服务器就可以只返回(first offset,last offset)之间的数据。

服务器推送

  • 客户端定期轮询

  • WebSocket(TCP)

  • 长轮询

    客户端发请求,有消息返回,然后再发请求,如果没消息服务端夯住(wait),约定时间未返回,就返回空,客户端再连。目前最常用的服务端推送。

  • HTTP Streaming(基于1.1)

    服务器端利用Transfer-Encodeing:chunked机制,发送一个“没完没了”的chunk流,就一个连接,但其Response永远接收不完。与长轮询的差异在于,这里只有一个HTTP请求,不存在HTTP Header不断重复的问题,但实现时没有长轮询简单直接。

HTTP/2

相当于在HTTP 1.1和TCP之间多了一个转换层

解决pipline问题?

二进制分帧,流ID,请求和响应被打散,队头阻塞细化到帧,可以对流制定优先级。

其他调优措施

头部压缩等

SSL/TLS

SSL(Secure Sockets Layer)的中文名称为安全套接层,TLS(Transport Layer Security)的中文名称为传输层安全协议。TLS是SSL标准化的结果,TLS 1.0相当于SSL 3.1;TLS 1.1、TLS 1.2 相当于SSL 3.2、SSL3.3。TCP层面的协议,所以可以支撑应用层的协议。在应用层里,习惯将两者并称为SSL/TLS。

  • 对称加密

  • 非对称加密

    分双向和单向,双向只能固定密钥,或者定期线下更换;单向可能会有中间人攻击的风险

  • 数字证书与证书认证中心(CA,Certification Authority)

    CA公钥服务器和客户端都知道

    服务器拿公钥找CA换证书,然后发给客户端,客户端可以拿CA的公钥验证证书是不是真的

  • 根证书与CA信任链

    如果CA是假的,怎么办?

    Root CA机构都是一些世界上公认的机构,在用户的操作系统、浏览器发布的时候,里面就已经嵌入了这些机构的Root证书。你信任这个操作系统,信任这个浏览器,也就信任了这些Root证书。

    认证时,ca4→ca3→ca2→ca1→ca0(ROOT,无条件信任)

    下发时,ca0→ca1→ca2→ca3→ca4

HTTPS

tcp 3次握手→SSL四次握手→http对称加密

  • TCP三次握手

    syn j→ack k,syn j+1→ack k+1

  • SSL四次握手

    客户端询问证书→服务器证书下发→客户端验证证书和公钥后,将通信使用的对称密钥使用公钥加密给服务器→服务器明文说收到

  • 升级协议,使用HTTP对称加密通信(可以复用连接)

TCP(Transmission Control Protocol)

不丢、不重、不乱

  • 不丢

    接收方ack包的序号,比如1,2,3,ack3;接收反馈超时重发;

  • 不重

    接收到6,此时又有1,2,3被重发,不接收。

  • 不乱

    比如1,2,3,5,6,7,ack3,此时4到达,ack7;

UDP(User Datagram Protocol)

无连接,不检查数据报,不等对方应答

UDP 在传输数据前不建立连接,不对数据报进行检查与修改,无须等待对方的应答,所以会出现分组丢失、重复、乱序,应用程序需要负责传输可靠性方面的所有工作;

UDP 具有较好的实时性,工作效率较 TCP 协议高;UDP 段结构比 TCP 的段结构简单,因此网络开销也小。

QUIC(Quick UDP Internet Connection)

  • 不丢包

    Raid5,每发送5个数据包,就发送一个冗余包。冗余包是对5个数据包做异或运算得到的。这样一来,服务器收到6个包,如果5个当中,有一个丢失了,可以通过其他几个包计算出来。这就好比做最简单的数学运算:A+B+C+D+E=R,假设数据包D丢失了,可以通过D=R-A-B-C-E计算出来。

  • 更少的RTT(Round-Trip Time往返时延)

    建立一个HTTPS连接需要七次握手,TCP的三次握手加上SSL/TLS的四次握手,是三个RTT。而造成网络延迟的原因,一个是带宽,另一个是RTT。因为RTT有个特点是同步阻塞。在数据包发出去之后,必须要等对方的确认回来,接着再发下一个。基于QUIC协议,可以把前面的七次握手(三个RTT),减为0次。

  • 连接迁移

    TCP 的连接本来就是“假”的,一个逻辑上的概念而已,QUIC 协议也可以创造一个逻辑上的连接。具体做法是,不再以4元组(客户端IP,客户端Port,服务器IP,服务器Port)来标识连接,而是让客户端生成一个64位的数字标识连接,虽然客户端的IP和Port在漂移,但64位的数字没有变化,这条连接就会存在。这样,对于上层应用来说,就感觉连接一直存在,没有中断过。

数据库

数据库范式与反范式

  • 第一范式:一个字段不能再拆分,反例:json、数组

  • 第二范式:表中必须有主键,或者组合主键;非主键必须完全依赖主键,不能和主键无关

  • 第三范式:非主属性必须直接依赖主键,不能间接依赖

    为性能或者开发,做反范式设计

分库分表

  • 业务拆分、易扩展

  • 应对高并发,读多写少,读从库,加缓存;写多读少,分库分表

  • 数据隔离,优先级

分布式ID生成

  • snowflake、redis预生成
  • 思考题:部分数据库为什么不建议用uuid?

拆分维度选择

  • 建立映射表,比如征信分表时做了字段指向;
  • 业务双写,同一订单,两份数据。一份用户ID切,一份商户ID切;
  • 异步双写,依然是同一份数据,通过binlog异步写。
  • 两个维度合并到一起,比如订单id已用户ID开头。

join查询问题

  • 单表查询,应用侧做拼接,效率高;
  • 做宽表,重读轻写;
  • 搜索引擎

分布式事务

  • 业务规避,通过回查机制来做;

  • 业务可以异步,事务消息;

  • 业务不能异步,saga;

    思考题:为什么TCC的落地不友好?

B+树

mysql innodb为例

逻辑结构

(1)在叶子节点一层,所有记录的主键按照从小到大的顺序排列,并且形成了一个双向链表。叶子节点的每一个Key指向一条记录。(2)非叶子节点取的是叶子节点里面Key的最小值。这意味着所有非叶子节点的Key都是冗余的叶子节点。同一层的非叶子节点也互相串联,形成了一个双向链表。

优势:双向链表,范围查找方便;主键前缀模糊匹配;天然有序,排序和分页很方便

物理结构

Page,默认16k,设一个page可存1000个key,非叶子节点能存1000,第3层+数据(设100字节),3层大概能存2亿条,16G左右。

id自增插入,顺序写,追加即可,如果随机,可能会造成页分裂,影响插入效率;删除的时候软删除,这样不用页合并(表优化)。

非主键索引

非主键索引结构也是b+树,但叶子节点存储的是主键的值。另外非叶子节点上,存储着最小的主键值。

事务和锁

事务隔离级别

RU、RC、RR、Serialization

ACID

  • 单条语句原子性
  • 悲观锁,for update
  • 乐观锁,version
  • 分布式锁
  • 死锁检测:有向图环

事务实现原理

Write-ahead Log(WAL)

先在内存中提交事务,然后写日志(所谓的Write-ahead Log),然后后台任务把内存中的数据异步刷到磁盘。日志是顺序地在尾部Append,从而也就避免了一个事务发生多次磁盘随机 I/O 的问题。明明是先在内存中提交事务,后写的日志,为什么叫作Write-Ahead呢?这里的Ahead,其实是指相对于真正的数据刷到磁盘,因为是先写的日志,后把内存数据刷到磁盘,所以叫Write-Ahead Log。

redoLog

ARIES恢复算法(Algorithms for Recovery And Isolation Expoliting Semantics)

  • 从宕机点找最后一次的checkpoint,当时有t1,t2,t3,t4;开始循环,发现t1,t3已提交,则未提交列表为t2,t4;

  • 然后开始从t1刷盘,db的page里有个字段为pageLsn,如果log里的lsn<=pageLsn,则表示已经刷盘,不处理;否则就刷入page,用这个方法实现幂等;此时t2,t4未提交的记录也刷盘了;

  • 对t2,t4生成Undo log,进行回滚。回滚时从后向前滚,如t2的lsn为2,t4的lsn为4,回滚4的时候,会记录UndoNxtLsn为2,这样回滚过程如果再次宕机,也不会继续回滚4,避免回滚嵌套。

undoLog

MVCC:解决快照读写的问题

CopyOnWrite:写的时候拷贝一份对象,再这个对象上做修改;写完后把对象指针指向新的对象。读读并发,读写并发,写写并发。

undoLog是CopyOnWrite的实现,生成了一个数据快照。快照读就是最常用的select语句,当前读包括了加锁的select语句和insert/update/delete语句。MVCC解决了快照读写的问题。

mysql的锁

写写问题要靠锁来解决。

互斥锁(X锁):对象上只有一把锁,只要有一个线程就互斥,读读互斥,读写互斥,写写互斥

共享锁(S锁):对象上只有一把锁,但是有两个视图,读写互斥,写写互斥,读读可以并发

表和行的共享锁、互斥锁好理解。

如果线程A锁了某行,线程B要锁全表是否要遍历?不需要,意向锁(IS锁、IX锁),线程A会对行加锁,对表加意向锁。

自增锁AI(Auto-inc Locks)、间隙锁(Gap Lock)、临键锁(Next-Key Lock)和插入意向锁(InsertIntension Lock)

BinLog

BinLog是Server层面的Log,Redo、Undo是引擎层面的Log。

串行化的log

  • 主从复制
  • 伪装成Slave,做消息

2PC

Prepare: InnoDB ok;BinLog ok;

Commit:先刷BinLog,再刷InnoDB。

RedoLog向BinLog对齐:

Prepare阶段宕机,BinLog在内存:消失;RedoLog没提交,回滚;

Commit阶段BinLog写一半宕机,BinLog没提交标记,回退;RedoLog没提交,回滚;

Commit阶段BinLog写完,RedoLog没写完宕机:遍历BinLog,存在BinLog有RedoLog没有的事务,提交。

技术架构之道

高并发

高并发读

举例:搜索

策略:

  • 缓存:加缓存(Redis、Guava);动静分离(CDN加速);读写分离(Mysql的Master,Slave)
  • 并行读:异步RPC
  • 重读轻写:微博关注数少的直接推所有粉丝;关注数多的推到在线粉丝;宽表、搜索引擎

高并发写

举例:点击流

策略:

  • 数据分片:分库分表;1.7的concurrentHashMap的槽;Kafka的partition;
  • 任务分片:Map/Reduce;Tomcat 1+N+M;
  • 异步:Mq(验证码短信);RedoLog(内存+WAL顺序写)
  • 批量:合并计费、扣库存
  • 串行化+多进程单线程+异步I/O

高并发读写

举例:库存

策略:按业务使用上面的方法来搞

容量规划

吞吐量、响应时间与并发数:吞吐量(QPS)*响应时间=并发数。对一个单机单线程的系统,假设处理每个请求的时间是 1ms,也就是响应时间是1ms,意味着1s可以处理1000个请求,QPS为1000。则并发数=1000qps乘以0.001s=1

  • 机器数=预估流量/单机容量
  • 压力测试:线上压测,通过打标签,让数据库中间件分流到影子库

高可用

多副本

无状态,加机器;有状态(缓存、数据库),考虑数据同步和一致性问题;

举例:应用多副本、mysql的主从(半同步写)、redis主从

隔离

数据隔离、机器隔离、线程池隔离、信号量隔离

限流

技术层面的限流:限制并发数、限制速率

业务层面的限流:秒杀100,2万人抢购,只放进来500,其他直接屏蔽掉

算法:滑动窗口计数、令牌桶、漏桶

熔断

根据请求失败率做熔断

根据请求响应时间做熔断

降级

兜底,mq存数据;主动关闭部分功能;做默认展示页

灰度发布

  • 新功能上线,按userId灰度,网关路由
  • 代码重构,按比例给量

回滚

  • 安装包回滚
  • 功能开关

监控体系

  • 资源监控

    cpu、磁盘、内存、带宽

  • 系统监控

    接口失败率、平均响应时间、最大响应时间、TP(top percentile )50,90,95,99、慢sql、GC时间等

  • 业务监控

    支付成功率、异常响应时间

日志报警

  • 日志等级
  • 异常分类

事务一致性

强一致性

2PC

角色:事务协调者、参与者

过程:

  • prepare:协调者先询问所有参与者,各参与者ack ok
  • commit:ok则进入提交阶段,如果有参与者超时或者不ok,则通知所有参与者回滚。

缺点:

  • 性能问题:所有节点锁资源,一起等待,不适合高并发。
  • 协调者单机风险,如果协调者有问题,所有参与者被挂起。
  • 提交阶段如果有参与者超时但实际已做业务处理,其他参与者被迫回滚,可能造成数据不一致。

3PC

角色:事务协调者、参与者

过程:

  • canCommit:协调者先询问所有参与者是否可以提交事务,做各种检查,不锁定资源。各参与者ack ok。canCommit参与者不ok或者响应超时,协调者发起abort,事务终止。
  • preCommit:canCommit ok则发起preCommit,各参与者锁定资源,不提交,然后ack ok。preCommit所有参与者ok则发起下一阶段。
  • commit:所有参与者提交后ack ok,则事务完成;如果不ok,则回滚。如果此时协调者超时,即没收到commit指令,参与者继续提交事务。

缺点:

  • 阶段2在等待超时后协调者或参与者会中断事务,降低了阻塞范围
  • 避免了协调者单点问题,阶段 3 中协调者出现问题时,参与者会继续提交事务
  • 数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 do commit 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。

最终一致性

不支持事务消息

角色:系统A、系统B、kafka

过程:

  • 系统A增加一张消息表,把消息和业务操作放到一个事务里,同时操作业务表和消息表。
  • 系统A准备一个后台定时,不断将消息表的中的数据发送到消息中间件。如果发送失败了,就重发。这样消息可能会重复,但不会丢。如果需要业务有顺序要求,定时需要考虑顺序的事。
  • 系统B从消息队列里取消息,做业务处理,处理成功再发送ack;没成功,消息中间件来重新发送。避免ack失败,业务处理成功的情况,每次接收消息时,一定要先判重。如业务数据本身能判重,则不需要判重表。如果不能判重,就增加一张判重表,记录处理成功的消息ID和中间件的offset,并且将业务操作和判重表的写入也要放到一个事务里。

缺点:

系统A需要增加消息表和定时,增加了业务方代码的复杂性。不过可以抽象成共有组件来做这个事情。

支持事务消息

角色:系统A、系统B、rocketmq

过程:

  • 系统A调用Prepare接口,预发送消息。此时消息保存在消息中间件里,但消息中间件不会把消息给消费方消费,消息只是暂存在那。
  • 系统A更新数据库,进行业务操作。之后调用Comfirm接口,确认发送消息。此时消息中间件才会把消息给消费方进行消费。
  • RocketMQ会定期(默认是1min)扫描所有的预发送但还没有确认的消息,回调给发送方,询问这条消息是要发出去,还是取消。发送方根据自己的业务数据,知道这条消息是应该发出去(DB更新成功了),还是应该取消(DB更新失败)。
  • 系统B从消息队列里取消息,做业务处理,处理成功再发送ack;没成功,消息中间件来重新发送。这个过程和不支持事务消息的处理一致,也是需要做判重和幂等的操作

缺点:

RocketMQ 最大的改变其实是把“扫描消息表”这件事不让业务方做,而是让消息中间件完成。至于消息表,其实还是没有省掉。因为消息中间件要询问发送方事物是否执行成功,还需要一个“变相的本地消息表”,记录事务执行状态和消息发送状态。同时对于消费方,还是没有解决系统重启可能导致的重复消费问题,这只能由消费方解决。需要设计判重机制,实现消息消费的幂等。以上两种方式,如果一直消费失败,则需要人工介入来解决。

TCC

角色:调用方、服务方系统A、服务方系统B

过程:

  • Try:调用方调用各服务方系统try接口做业务校验、资源锁定。举例:系统A要扣款,系统B加上款项;先做业务校验,比如A余额是否足够,如果够,则锁定余额;
  • Comfirm:系统A、系统B Try之后都返回Ok,则调用方调用各服务方系统confirm,如果失败,重复提交。
  • Cancel:系统A、系统B Try不Ok,则调用方调用各服务方系统cancel,如果失败,重复调用提交接口。

缺点:

TCC实质上是业务上的2PC,需要TCC框架做支持,因为需要涉及的相关方都实现三个接口,一般业界少有实践;需要判重和幂等。

SAGA

事件方式(Choreography)

角色:多服务方,向前补偿、向后补偿

过程:

  • 各服务方都有一个回退方法
  • 各服务方完成事务后发送一个事件消息SUC_A,并且订阅回退事件。
  • 其他服务方接到消息后进行事务操作并发送对应的事件消息SUC_B,失败后发送FAIL_B,则服务A订阅FAIL_B调用自己的回退方法

缺点:

  • 需要确保事件不丢失

  • 仅适用于参与者比较少的情况,参与者多了订阅关系就不清楚了;而且还有可能出现环状事件

命令方式(Orchestration)

角色:协调中心、多服务方,向前补偿、向后补偿

过程:

  • 各服务方都有一个回退方法
  • 第一个服务方发起,并向协调中心申请开启事务。
  • 协调中心按状态机发送命令给多服务方,多服务方返回成功或者失败。如成功则继续,直到事务成功;如失败则的调用之前服务的回退方法。

缺点:

  • 需要维护协调中心,但这个中心无业务方负责

  • 依然要保证命令不丢失

非标准方案(不完善、最终一致性)

事务状态表+调用方重试+接收方幂

角色:调用方、服务方

过程:

  • 各服务方做幂等。调用方先做一张事务状态表,每次事务开启落一个状态记录,状态为0。然后调用各服务方,成功则自增。如果4个服务方,状态为4则表示成功。
  • 如果状态表状态未达4,固定时间内扫描,然后重新发送。

缺点:

只适应于内部业务,可控性很差,不能用于不支持幂等的三方服务调用等。

对账

基于数据关系的补偿。

  • 全量对账。比如每天晚上运作一个定时任务,比对两个数据库。
  • 增量对账。可以是一个定时任务,基于数据库的更新时间;也可以基于消息中间件,每一次业务操作都抛出一个消息到消息中间件,然后由一个消费者消费这条消息,对两个数据库中的数据进行比对(当然,消息可能丢失,无法百分之百地保证,还是需要全量对账来兜底)。

多副本一致性

Paxos

Basic-Paxos

角色:Proposer、Acceptor、Learner(不参与协商过程,根据Quorum读取协商完成的值)

过程:

  • Prepare

    1. Proposer选择一个新的提案编号n,然后向某个acceptor集合(至少需满足majority)广播prepare(n),要求该集合中的Acceptor做出如下回应:
      如果n>当前的最小提案值,则接收并保证不再批准任何编号小于n的提案,当前最小提案号赋值为n,返回yes;否则返回no。
      如果acceptor已经批准过任何提案,那么其就向proposer反馈当前该acceptor已经批准的编号小于n但为最大编号的那个提案的值(已经接受的值不能覆盖)。将该请求成为编号n的提案的prepare请求。
    2. 如果proposer收到了来自半数以上(majority)的acceptor的反馈,那么有两种情况:
      可以产生编号为n、value值为Vn的提案,其中Vn是所有响应中编号最大的提案的value值。
      返回的所有反馈中,都没有批准过任何提案,即响应中不包含任何的提案,那么此时Vn值就可以由proposer任意选择。
  • Accept

    1. Proposer广播accept(n,v)。这里的n就是Prepare阶段的n,v可能是自己的值,也可能是Prepare阶段的acceptValue。

    2. Acceptor收到accept(n,v),做如下决策:

      n>=当前最小提案号,当前最小提案号赋值为n,值赋值为acceptValue,返回yes。否则no。

    3. Proposer如果收到半数以上的yes,并且minProposalId=n,则算法结束。否则,n自增,重复P1a。

问题:

  • 不断循环的2PC,并发冲突高,有可能“活锁”;
  • 每确定一个值,2次RTT+2次写盘,性能是个问题。

Mutil-Paxos

角色:Proposer(转为Leader)、Acceptor、Learner

过程:

  • 基本思路是当一个节点被确认为Leader之后,它先广播一次Prepare(n,index),index为已accept的日志index。一旦超过半数同意,之后对于收到的每条日志直接执行Accept操作。在这里,Perpare不再是对一条日志的控制了,而是相对于拿到了整个日志的控制权。一旦这个Leader拿到了整个日志的控制权,后面就直接略过Prepare,直接执行Accept。Leader解决了活锁问题,然后通过拿到所有日志控制权,相当于除了首次prepare,之后每次只有一次RTT,性能有提高。允许出现多个Leader,新的Leader肯定会先发起Prepare,导致minProposalId变大。这时旧的 Leader 的广播 Accept 肯定会失败,旧的 Leader会自己转变成一个普通的Acceptor,新的Leader把旧的顶替掉了。
  • Proposer(也就是Leader) 接收到多数派对Accept请求的同意后,就知道这条日志被“choose”了,也就是被确认了,不能再更改!但只有Proposer知道这条日志被确认了,其他的Acceptor并不知道这条日志被确认了。如何把这个信息传递给其他Accepotor呢?Accept(n,v,index,firstUnchooseIndex)。

精髓

  • 一个强一致的“P2P网络”
  • 时序(paxos不能保证顺序,因为多leader问题;raft和zab是可以保证顺序的)

Raft

单点写入,只允许有一个Leader。

过程:和ZAB相同

  • 阶段1:选举阶段。选举出Leader,其他机器为Follower。
  • 阶段2:正常阶段。Leader接收写请求,然后复制给其他Followers。
  • 阶段3:恢复阶段。旧Leader宕机,新Leader上任,其他Follower切换到新的Leader,开始同步数据。

概念

  • term(epoch):Leader的任期。

    作用1:可以让Follower感知到Leader过期了。

    作用2:分区发生并且合并后,最后只允许有一个Leader。

    作用3:term是分区存储的,不会发生有延迟的节点被选成Leade,选举要半数同意,表示大部分节点存储的是新的term,选举的时候要选自己同步的最大最新的term。

  • index:日志的顺序。

  • commitIndex:已提交日志的index

    类tcp的设计,commitIndex=7表示7之前的日志都确认了。

    作用1:不需要为每条日志都维护一个 commit 或 uncommit 状态,而只需要维护一个全局变量commitIndex即可。

    作用2:Follower不需要逐条日志地反馈Leader,哪一条commit了,则哪一条uncommit。

  • lastApplied:记录哪些日志已经被回放到了状态机。

  • 节点的State变量

选举

  1. 任何一个节点也是有三种状态:Leader、Follower 和Candidate。初始时,所有机器处于Follower状态,等待Leader的心跳消息(一个机器成为Leader之后,会周期性地给其他Follower发心跳)。很显然,此时没有Leader,所以收不到心跳消息。

  2. 当Follower在给定的时间(比如2000ms)内收不到Leader的消息,就会认为Leader宕机,也就是选举超时。然后,随机睡眠0~1000ms之间的一个值(为了避免大家同时发起选举),把自己切换成Candidate状态,发起选举。

  3. 选举结束,自己变成Leader或者Follower。

    两条日志a和b,日志a比日志b新:

    term>b.term

    term=b.term 且 a.index>b.index。

    日志比自己新,对选举返回yes,且一个term只能选一次;否则就no。

  4. 对于Leader,发现有更大term的Leader存在,自己主动退位,变成Follower。这里有一个关键点:心跳是单向的,只存在Leader周期性地往Follower发送心跳,Follower不会反向往Leader发送心跳。后面要讲的Zab算法是双向心跳,很显然,单向心跳比双向心跳简单很多。

日志复制

  • 在Leader成功选举出来后,Leader会并发地向所有的Follower发送AppendEntries RPC请求,只要超过半数的Follower复制成功,就返回给客户端日志写入成功。

  • Follower接到请求后处理逻辑如下:

    如果term

    如果发现自己的日志中没有(prevLogIndex,pevLogTerm)日志,则拒绝接收当前的复制;

    如果发现自己的日志中,某个index位置和Leader发过来的不一样,则删除index之后的所有日志,然后从index的位置同步接下来的日志。

恢复阶段

当Leader宕机之后,选出了新的Leader,Follower是被动的,其并不会主动发现有新的Leader上台了;而是新的Leader上台之后,会马上给所有的 Follower 发一个心跳消息,也就是一个空的AppendEntries 消息,这样每个Follower都会将自己的term更新到最新的term。这样旧的Leader即使活过来了,也没有机会再写入日志。

ZAB

单点,Primary-Backup模型

两种模型比较

  • Replicated State Machine(日志序列,Redis的AOF,mySql BinLog的statement,原语句)

  • Primary-Backup System(状态变化,Redis的RDB,mySql BinLog的raw,变更)

    差异:

    1. 数据同步次数不一样。如果存储的是日志,则客户端的所有写请求都要在节点之间同步,不管状态有无变化。比如客户端连续执行三次X=1、X=1、X=1,如果存储的是三条日志,在节点之间要同步三次数据;如果存储状态变化的话,则只有一条,因为后两次的写请求没有导致数据变化,在节点之间只需要同步一次数据。
    2. 存储状态变化。其天然具有幂等性,比如客户端发送了一个指令X=X+1,如果存储日志X=X+1,Apply多次就会出现问题;但如果存储的是状态变化X=6,即使Apply多次也没有关系。

概念

  • zxid(term+index):4位的整数,高32位表示Leader的任期epoch,低32位是任期内日志的顺序编号
  • 时序:(1)如果日志a小于日志b,则所有节点一定先广播a,后广播b。(2)如果日志a小于日志b,则所有节点一定先Commit a,后Commit b。这里的Commit,指的是Apply到状态机。

选举

  1. 任何一个节点也是有三种状态:Leader、Follower 和Election。Election状态是中间状态,也被称作“Looking”状态。在初始的时候,节点处于Election状态,然后开始发起选举。

  2. 在Zab里面是双向心跳,Follower收不到Leader的心跳,就切换到Election状态发起选举;反过来,Leader收不到超过半数的Follower心跳,也切换到Election状态,重新发起选举。

  3. 选举方式,Raft选取日志最新的节点作为新的Leader,Zab的FLE(FastLeader Election)算法也类似,选取zxid最大的节点作为Leader。如果所有节点的zxid相等,比如整个系统刚初始化的时候,所有节点的zxid都为0。此时,将选取节点编号最大的节点作为Leader(Zookeeper为每个节点配置了一个编号)。

  4. 选举结束,处于Leader或者Follower状态。

    FLE(FastLeader Election)算法,选取zxid最大的节点作为Leader。如果所有节点的zxid相等,比如整个系统刚初始化的时候,所有节点的zxid都为0。此时,将选取节点编号最大的节点作为Leader

日志复制(2PC)

  • 阶段1:Leader收到客户端的请求,先发送Propose消息给所有的Follower,收到超过半数的Follower返回的ACK消息。
  • 阶段2:给所有节点发送Commit消息。

恢复阶段

Leader的日志不会动,Follower上传zxid。Leader拿自己日志和Follower做日志比对,然后发送日志的截断、日志的补齐或全量同步等操作给Follower。

CAP

一致性、可用性、网络分区

分布式场景下的不一致:

  • 消息传播需要时间,有的节点可能接到消息了,有的节点还在传输,各节点不同步。
  • 消息状态的变化,消息发出来时状态是A,结果接收到A的时候,实际状态已变成B,状态不同步。
  • 传输通道不可靠,如超时或者网络错误,不知道对方接到没接到。

业务架构

业务意识

需求分析、产品经理

  • 需求来源?
  • 真需求vs伪需求,信息传播失真
  • 产品手段vs技术手段
  • 需求的优先级

业务闭环

  • 团队闭环:有自己的产品、技术、运营和销售,联合作战。
  • 产品闭环:从内容的生成到消费,整条链路把控。
  • 商业闭环:具备了自负盈亏的能力(即使短期没有,长期也是向这个发展方向)。
  • 纵向闭环:某个垂直领域,涵盖从前到后。
  • 横向闭环:平台模式,横向覆盖某个横切面。

组织架构、业务架构、技术架构

业务架构思维

分层思维

  • 底层调用上层

    是否是设计不合理?能否依赖反转,底层做接口,上层做实现?

  • 同层之间双向调用

    是否可以抽象公共服务,同层做隔离

  • 参数层层传递

    隔离到对应的层做处理,比如app版本,严格放到最外层,隔离出去

  • 聚合层多

边界思维

  • 对象层面、接口层面、产品层面、组织结构
  • 注意不能做什么

系统化思维

  • 第一性原理,追根问底

利益相关者分析

非功能性需求

  • 可用性、并发、可维护、可扩展、一致性、可重用

视角

  • 功能视图、逻辑视图、物理视图、开发视图——》运行视图

抽象

  • 分解:找差异和共性
  • 归纳

建模

  • 重要的东西显性化

正交分解

  • 高维归纳和分解

技术架构与业务架构融合

个人素质

  • 格局
  • 历史观
  • 抽象能力
  • 深入思考能力
  • 落地能力

不确定性

  • 需求、技术、人员、组织不确定性、历史遗留

业务价值的层次

  • 框架、算法
  • 可扩展、高可用、一致性
  • 业务价值、提高运维效率、降低研发成本
  • 开源节流、利润

团队培养

技术能力、独立能力、思维能力

你可能感兴趣的:(架构相关,架构)