如何提升系统架构能力?

系统设计与架构,与系统的业务类型关联很大,比如,传统的业务系统主要关注的是领域建模设计,高并发、高可用、数据一致性等系统,在设计的时候会与业务系统有较大的差别,所以这里针对不同类型的系统,来简单介绍一下设计的时候面临的一些难点与解决方案。

背景常规业务系统设计关键 —— 领域模型

业务系统设计的关键是在于如何定义系统的模型以及模型之间的关系,其中主要是领域模型的定义,当我们在模型确定之后,模型之间的关系也会随之明确。

模型设计可以参考领域模型的经典书籍《Domain-Driven Design》一书,通过这个基本可以对领域定义、防腐层、贫血模型等概念有一个较为清晰的认识了。

单个应用内的领域模型系统也需要注意领域分层,作为开发大家是不是见过、重构过很多 Controller-Service-DAO 样式的代码分层设计?往往在在做重构的时候会令人吐血。

设计较好的领域设计这里给一个分层建议:

接口层 Interface

主要负责与外部系统进行交互 & 通信,比如一些 dubbo 服务、Restful API、RMI 等,这一层主要包括 Facade、DTO 还有一些 Assembler。

应用层 Application

这一层包含的主要组件就是 Service 服务,但是要特别注意,这一层的 Service 不是简单的 DAO 层的包装,在领域驱动设计的架构里面,Service 层只是一层很 “薄” 的一层,它内部并不实现任何逻辑,只是负责协调和转发、委派业务动作给更下层的领域层。

领域层 Domain

Domain 层是领域模型系统的核心,负责维护面向对象的领域模型,几乎全部的业务逻辑都会在这一层实现。内部主要包含 Entity(实体)、ValueObject(值对象)、Domain Event(领域事件)和 Repository(仓储)等多种重要的领域组件。

基础设施层 Infrastructure

它主要为 Interfaces、Application 和 Domain 三层提供支撑。所有与具体平台、框架相关的实现会在 Infrastructure 中提供,避免三层特别是 Domain 层掺杂进这些实现,从而 “污染” 领域模型。Infrastructure 中最常见的一类设施是对象持久化的具体实现。

高并发系统设计

在面试中是不是经常被问到一个问题:如果你系统的流量增加 N 倍你要怎么重新设计你的系统?这个高并发的问题可以从各个层面去解,比如:

代码层面

  • 锁优化(采用无锁数据结构),主要是 concurrent 包下面的关于 AQS 锁的一些内容
  • 数据库缓存设计(降低数据库并发争抢压力),这里又会有缓存、DB 数据不一致的问题,在实际使用中,高并发系统和数据一致性系统采用的策略会截然相反。
  • 数据更新时采用合并更新,可以在应用层去做更新合并,同一个 Container 在同一时间只会有一个 DB 更新请求。
  • 其他的比如基于 BloomFilter 的空间换时间、通过异步化降低处理时间、通过多线程并发执行等等。

数据库层面

  • 根据不同的存储诉求来进行不同的存储选型,从早期的 RDBMS,再到 NoSql(KV 存储、文档数据库、全文索引引擎等等),再到最新的 NewSql(TiDB、Google spanner/F1 DB)等等。
  • 表数据结构的设计,字段类型选择与区别。
  • 索引设计,需要关注聚簇索引原理与覆盖索引消除排序等,至于最左匹配原则都是烂大街的常识了,高级一点索引消除排序的一些机制等等,B + 树与 B 树的区别。
  • 最后的常规手段:分库分表、读写分离、数据分片、热点数据拆分等等,高并发往往会做数据分桶,这里面往深了去说又有很多,比如分桶如何初始化、路由规则、最后阶段怎么把数据合并等等,比较经典的方式就是把桶分成一个主桶 + N 个分桶。

架构设计层面

  • 分布式系统为服务化
  • 无状态化支持水平弹性扩缩容
  • 业务逻辑层面 failfast 快速失败
  • 调用链路热点数据前置
  • 多级缓存设计
  • 提前容量规划等等

高可用系统设计

对于可用性要求非常高的系统,一般我们都说几个 9 的可用率,比如 99.999% 等。

面对高可用系统设计也可以从代码层面和软件层面 2 方面来进行分析:

  • 代码层面:需要关注分布式事务问题,CAP 理论是面试的常规套路
  • 软件层面:应用支持无状态化,部署的多个模块完全对等,请求在任意模块处理结果完全一致 => 模块不存储上下文信息,只根据请求携带的参数进行处理。目的是为了快速伸缩,服务冗余。常见的比如 session 问题等。

负载均衡问题

软件部署多份之后,如何保证系统负载?如何选择调用机器?也就是负载均衡问题。

狭义上的负载均衡按照类型可以分为这几种:

  • 硬件负载:比如 F5 等
  • 软件负载:比如 LVS、Ngnix、HaProxy、DNS 等。
  • 当然,还有代码算法上的负载均衡,比如 Random、RoundRobin、ConsistentHash、加权轮训等等算法

广义上的负载均衡可以理解为负载均衡的能力,比如一个负载均衡系统需要如下 4 个能力:

  • 故障机器自动发现
  • 故障服务自动摘除(服务熔断)
  • 请求自动重试
  • 服务恢复自动发现

幂等设计问题

上面提负载均衡的时候,广义负载均衡需要完成自动重试机制,那么在业务上,我们就必须保证幂等设计。

这里可以从 2 个层面来进行考虑:

  • 请求层面
    由于请求会重试所以必须做幂等,需要保证请求重复执行和执行一次的结果完全相同。请求层面的幂等设计需要在数据修改的层做幂等,也就是数据访问层读请求天然幂等,写请求需要做幂等。读请求一般是天然幂等的,无论查询多少次返回的结果都是一致。这其中的本质实际上是分布式事务问题,这里下面再详细介绍。
  • 业务层面
    不幂等会造成诸如奖励多发、重复下单等非常严重的问题。业务层面的幂等本质上是分布式锁的问题,后面会介绍。如何保证不重复下单?这里比如 token 机制等等。如何保证商品不超卖?比如乐观锁等。MQ 消费方如何保证幂等等都是面试的常见题。

分布缩式

业务层面的幂等设计本质上是分布式锁问题,什么是分布式锁?分布式环境下锁的全局唯一资源,使请求串行化,实际表现互斥锁,解决业务层幂等问题。

常见的解决方式是基于 Redis 缓存的 setnx 方法,但作为技术人员应该清楚这其中还存在单点问题、基于超时时间无法续租问题、异步主从同步问题等等,更深一点,CAP 理论,一个 AP 系统本质上无法实现一个 AP 需求,即使是 RedLock 也不行。

那我们如何去设计一个分布式锁呢?强一致性、服务本身要高可用是最基本的需求,其他的比如支持自动续期,自动释放机制,高度抽象接入简单,可视化、可管理等。

基于存储层的可靠的解决方案比如:

  • zookeeper
    CP/ZAB/N+1 可用:基于临时节点实现和 Watch 机制。
  • ETCD
    CP or AP/Raft/N+1 可用:基于 restful API;KV 存储,强一致性,高可用,数据可靠:持久化;Client TTL 模式,需要心跳 CAS 唯一凭证 uuid。

服务的熔断

微服务化之后,系统分布式部署,系统之间通过 RPC 通讯,整个系统发生故障的概率随着系统规模的增长而增长,一个小的故障经过链路传导放大,有可能造成更大的故障。希望在调用服务的时,在一些非关键路径服务发生服务质量下降的情况下,选择尽可能地屏蔽所造成的影响。

大部分熔断返回默认值 null,也可以定制,RPCClient 原生支持最好,业务方少改代码(熔断放的地方),进入熔断时,打印熔断日志,同时返回 Exception(业务方定制熔断方法),需要有服务治理平台,可以看到服务的状态、是否降级、是否熔断、可以实时下发阀值配置等。

服务降级

服务整体负载超出预设的上限,或者即将到来的流量预计将会超过阀值,为了保证重要或者基本的服务能够正常运行,拒绝部分请求或者将一些不重要的不紧急的服务或任务进行服务的延迟使用或暂停使用。

主要的手段如下:

  • 服务层降级,主要手段
    1. 拒绝部分请求(限流),比如缓存请求队列,拒绝部分等待时间长的请求;根据 Head,来拒绝非核心请求;还有其他通用算法上的限流比如令牌桶、漏桶算法等等。
    2. 关闭部分服务:比如双 11 大促 0 点会关闭逆向退款服务等等。
    3. 分级降级:比如自治式服务降级,从网关到业务到 DB 根据拦截、业务规则逐渐降低下游请求量,体现上是从上到下的处理能力逐渐下降。
  • write ahead logging
    比如流量大的时候,更新请求只缓存到 MQ,读请求读缓存,等流量小的时候,进行补齐操作 (一般数据访问层如果做了降级,就没必要在数据层再做了)
  • 柔性可用策略
    比如一些指定最大流量的限流工具,又或是根据 CPU 负载的限流工具等,需要保证自动打开,不依赖于人工。

发布方式引发的可用性问题

发布方式也是影响高可用的一个点,以前还经历过一些线上直接停机发布的案例(银行内部系统),不过作为高大上的互联网,主要会采用这几种发布方式:灰度发布、蓝绿发布、金丝雀发布等等。

数据一致性系统设计

一般一些金融、账务系统对这一块要求会非常严格,下面主要介绍下这里面涉及到的事务一致性、一致性算法等内容。

事务一致性问题

在 DB 层面,一般通过 刚性事务 来实现数据一致性,主要通过 预写日志 WAL 的方式来实现,WAL 预写日志的方式。就是所有对数据文件的修改,必须要先写日志,这样,即使在写数据的时候崩溃了,也能通过日志文件恢复,传统的数据库事务就是基于这一个机制(REDO 已提交事务的数据也求改 UNDO 未提交事务的回滚)。

除了这个方式之外,还有一个就是通过 影子数据块 来进行数据备份,提前记录被修改的数据块的修改前的状态,备份起来,如果需要回滚,直接用这个备份的数据块进行覆盖就好了。

其他的就是基于二阶段提交的 XA 模型 了。

但是目前互联网系统,已经广泛采用分布式部署模式了,传统的刚性事务无法实现,所以,柔性事务成了目前主流的分布式事务解决防范,主要的模式有下面几种:

  • TCC 模式 / 或者叫 2 阶段模式
    在 Try 阶段预扣除资源(但是不锁定资源,提升可用性),在 Confirm 或者 Cancel 阶段进行数据提交或者回滚。一般需要引入协调者,或者叫事务管理器。
  • SAGA 模式
    业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,支持向前或者向后补偿。
  • MQ 的事务消息
    就是先发 halfMsg,在处理完之后,再发送 commit 或者 rollback Msg,然后 MQ 会定期询问 producer ,halfMsg  能不能 commit 或者 rollback,最终实现事务的最终一致性。实际上是把补偿的动作委托给了 RocketMQ。
  • 分段事物(异步确保)
    基于可靠消息 + 本地事务消息表 + 消息队列重试机制。目前这也是一些大厂的主流方案,内部一般称为分段事物。

柔性事务基本都是基于最终一致性去实现,所以肯定会有补偿动作在里面,在达到最终一致性之前,对用户一般展示软状态。

需要注意的一点是,并不是所有的系统都适合引入数据一致性框架,比如用户可以随时修改自己发起的请求的情况,例如,商家设置后台系统,商户会随时修改数据,这里如果涉及到一致性的话,引入一致性框架会导致补偿动作达到最终一致性之前,资源锁会阻塞用户后续的请求,导致体验较差。这种情况下,就需要通过其他手段来保障数据一致性了,比如数据对账等操作。

一致性算法

从早期的 Paxos 算法,再到后面衍生的 zab 协议,提供了当下可靠的分布式锁的解决方案。再到后来的 Raft 算法(In Search of an Understandable Consensus Algorithm),也都是分布式系统设计里面需要了解到的一些知识要点。

总结

这里简单介绍了不同系统设计的时候会面临的一些难点,基本里面每一个点,都是前人在解决各种疑难问题的道路上不断探索,最终才得出的这些业界解决方案,呈现在大家眼前,作为一个技术人员,学会这些技术点只是时间问题,但这种发现问题、直面问题、再到解决问题的能力和精神才是我们最值得学习的地方,也是做为一个系统设计人员或者说是架构师的必要能力。

分享