架构设计
# 系统架构设计
# 前言
一个好的软件架构,应该遵循高性能、高可用、易扩展 3 大原则,其中 高可用
在系统规模变得越来越大时,变得尤为重要。
# 1 高性能
# 1.1 服务拆分
按功能纬度、读写维度(如提供一个专门的查询服务给外部调用,数据库配置读库)
# 1.2 缓存
浏览器缓存、cdn缓存、应用层缓存(redis、内存缓存等)
# 1.3 消息队列解耦削峰
运单收发到派签消息通过RabbitMQ发送
# 1.4 负载均衡
常见的负载均衡系统包括3种:
DNS负载均衡:一般用来实现地理级别的均衡。
硬件负载均衡:通过独立的硬件设备,比如F5,实现负载均衡功能(硬件价格一般较贵)。
软件负载均衡:通过软件的方式,比如Nginx,实现负载均衡功能。
# 1.5 读写分离&分库分表
# 1.5.1 读写分离会带来的问题
主库和从库数据同步存在延迟,写完数据马上读取时可能读不到最新数据。
解决方法:
1.强制将读请求路由到主库处理
比如 Sharding-JDBC,可以通过 HintManager 分片键值管理器强制使用主库。
HintManager hitManager = HintManager.getInstance();
hintManager.setMasterRouteOnly();
// 继续JDBC操作
2
3
2.延迟读取
这种方式不太合适 ...
# 1.5.2 主从复制原理
1.主库将数据变化写入binlog。
2.从库创建一个I/O线程向主库请求更新的binlog。
3.主库创建一个线程发送binlog,从库I/O线程负责接收。
4.从库I/O线程接收的binlog写入到relay log中。
5.从库的SQL线程读取relay log同步数据到本地(也就是再执行一遍SQL)。
# 1.5.3 分库分表带来的问题
1.join操作:表分布到不同数据库,导致无法进行连表操作。
2.事务问题:操作不同数据库的表,自带的事务无法支持。
3.分布式id:自增主键等方式无法使用,需引入分布式ID。
# 1.5.4 分库分表后,数据怎么迁移?
使用数据库同步工具 Canal 做增量数据迁移(还是依赖 binlog,开发和维护成本较低)。
# 2 高可用
# 2.1 衡量高可用
高可用通常通常用两个指标来衡量。
平均故障时间间隔:表示两次故障的时间间隔,也就是系统正常运行平均时间。这个时间越长越稳定。
故障恢复时间:系统发生故障后的恢复时间。这个时间越短,对用户影响越小。
可用性跟这两个指标之间的关系为:
可用性 = 平均故障时间间隔 / (平均故障时间间隔 + 故障恢复时间) * 100%
这个公式得出的结果是一个 比例
,通常我们会用 N 个 9
来描述一个系统的可用性。
系统可用性 | 年故障时间 | 日故障时间 |
---|---|---|
90%(1个9) | 36.5天 | 2.4小时 |
99%(2个9) | 3.65天 | 14分钟 |
99.9%(3个9) | 8小时 | 86秒 |
99.99%(4个9) | 52分钟 | 8.6秒 |
99.999%(5个9) | 5分钟 | 0.86秒 |
99.9999%(6个9) | 32秒 | 86毫秒 |
# 2.2 硬件层面保证高可用
# 2.2.1 灾备设计
灾备设计
= 容灾
+ 备份
容灾:建立两个相同的系统。当其中一个系统出问题时,可以直接切换另一个系统使用。
备份:将系统产生的重要数据进行备份。
# 2.2.2 异地多活
将服务部署到异地,并且多地服务能同时对外提供服务。
异地多活主要应对突发情况,如火灾、地震、人为灾害等。
# 2.2.3 灾备到异地多活的演变过程
# 2.2.3.1 同城灾备
同城灾备
分为 冷备
和 热备
, 冷备
只备份数据,不提供服务。 热备
实时同步数据,做好随时切换的准备。
# 2.2.3.2 同城双活
同城双活
比同城灾备的优势在于,两个机房都可以接入 读写
流量。提高可用性的同时也提高系统性能。(由于机房部署同一城市,可不考虑网络延迟问题。光纤传输的速度大概为 300km/ms
)
# 2.2.3.3 两地三中心
两地三中心
是在同城双活基础上再部署一个异地机房做 灾备
,用来抵御 城市
级别的灾害。但启用灾备机房需要耗费一定时间。(两地是指两个城市,三中心是指三个机房)
# 2.2.3.4 异地双活
异地双活
才是抵御 城市
级别灾害的更好方案。异地两个机房同时提供服务,有故障随时切换,可用性高。但是实现也很复杂。异地双活要两个机房都可以读写(不同城市的两个机房如果只有一个机房数据库做主库,会导致另一个只读的机房查数据延迟很高)。MySQL本身提供了双主架构,支持双向复制数据,但是像redis、mq等都不支持双向同步数据,需要另外开发。
此外,还需要在业务上将数据区分开,保证指定数据操作指定机房,避免各种脏数据的产生。这样,需要在接入层之上再部署一个 路由层
(通常部署在云服务器上),自己可以配置不同路由规则,将用户分流到不同的机房内。
# 2.2.3.5 异地多活
异地多活
是在异地双活的基础上扩展多个机房,这样不仅保证了高可用,还保证了高性能,可以应对更大规模的流量压力。是实现高可用的最终方案。
这种星状的方案必须要设立一个 中心机房
,任意机房写入数据后要先同步到中心机房,再由中心机房同步到其他机房。中心机房的稳定性要求比较高,不过中心机房如果发生故障的话,可以把任意一个机房提升为中心机房,继续按照之前的架构提供服务。
# 2.3 系统及代码层面保证高可用
# 2.3.1 集群
使用集群,减少单点故障。
# 2.3.2 版本可回滚
应用部署支持版本回滚。
数据库脚本也需要有回滚脚本。
# 2.3.3 超时重试
重试次数一般为3次。
# 2.3.4 降级
同步改异步(如同步导出通过配置调整成异步导出)。
直接读缓存(关键功能本来查库的调整成临时查缓存)。
# 2.3.5 熔断
熔断和降级是两个容易混淆的概念,这两者的含义并不一样。
降级针对的是自身系统的故障,而熔断是要应对其他系统的故障。
# 2.3.6 限流
# 2.3.6.1 常见限流方案
# 1.计数器法
原理:在单位时间段内,对请求数进行计数,如果数量超过了单位时间的限制,则执行限流策略,当单位时间结束后,计数器清零,这个过程周而复始,就是计数器法。
缺点:不能均衡限流,在一个单位时间的末尾和下一个单位时间的开始,很可能会有两个访问的峰值,导致系统崩溃。
改进方式:可以通过减小单位时间来提高精度。
# 2.漏桶算法
原理:假设有一个水桶,水桶有一定的容量,所有请求不论速度都会注入到水桶中,然后水桶以一个恒定的速度向外将请求放出,当水桶满了的时候,新的请求被丢弃。
优点:可以平滑请求,削减峰值。
缺点:瓶颈会在漏出的速度,可能会拖慢整个系统,且不能有效地利用系统的资源。
# 3.令牌桶算法(推荐)
原理:有一个令牌桶,单位时间内令牌会以恒定的数量(即令牌的加入速度)加入到令牌桶中,所有请求都需要获取令牌才可正常访问。当令牌桶中没有令牌可取的时候,则拒绝请求。
优点:相比漏桶算法,令牌桶算法允许一定的突发流量,但是又不会让突发流量超过我们给定的限制(单位时间窗口内的令牌数)。即限制了我们所说的 QPS(每秒查询率)。
# 2.3.6.2 Guava限流工具类
# 1.说明
Google开源工具包Guava提供了限流工具类RateLimiter,基于令牌桶算法实现。
常用方法:
create(Double permitsPerSecond) 方法根据参数(令牌:单位时间(1s))比例为令牌生成速率。
tryAcquire() 方法尝试获取一个令牌,立即返回true/false,不阻塞,重载方法具备设置获取令牌个数、获取最大等待时间等参数。
acquire() 方法与tryAcquire类似,但是会阻塞,尝试获取一个令牌,没有时则阻塞直到获取成功。
可能有人在想既然是令牌桶算法,应该有个类似定时器的东东来持续往桶放令牌才对啊,我刚开始也是这么想的,看了代码觉得自己还是太嫩了,如果开启一个定时器无可厚非,但如果系统需要N个不同速率的桶来针对不同的场景或用户,就会极大的消耗系统资源。
RateLimiter用了一种类似于延迟计算的方法,把桶里令牌数量的计算放在下一个请求中计算,即桶里的令牌数 storedPermits 不是实时更新的,而是等到下一个请求过来时才更新。
# 2.代码示例
@Component
@Aspect
public class RateLimitAspect {
private static final double permitsPerSecond = 10.0;
private static RateLimiter rateLimiter = RateLimiter.create(permitsPerSecond);
@Pointcut("@annotation(com.xxx.common.base.config.RateLimitAnno)")
public void aspectService() {
}
@Around("aspectService()")
public Object aroundMsg(ProceedingJoinPoint joinPoint) throws Throwable {
Object obj = null;
boolean flag = rateLimiter.tryAcquire();
if (flag) {
obj = joinPoint.proceed();
} else {
throw new RateLimitException(ResultCodeEnum.RATELIMIT_ERROR);
}
return obj;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public @interface RateLimitAnno {
}
2
@Override
@RateLimitAnno
public Bill getPostal(String billNo, List<String> fields) {
if (StringUtils.isEmpty(billNo)) {
return null;
}
Bill bill = this.detail(billNo, fields);
return bill;
}
2
3
4
5
6
7
8
9
需关注好服务监控指标,如qps,响应时间,tomcat线程信息等(acceptcount,maxConnections)