分布式系统常见问题

对于分布式高并发系统需要考虑的问题:容量规划架构设计数据库设计缓存设计框架选型数据迁移方案性能压测监控报警领域模型回滚方案高并发分库分表

一致性Hash

一致性哈希算法通过一致性哈希环数据结构实现,该环的起点0终点2^32 - 1,且起点与终点连接,故该环的整数分布范围[0, 2^32-1]

将对象和服务器都放置到同一个哈希环后,在哈希环上顺时针查找距离这个对象的hash值最近的机器,即是该对象所属的机器。引入虚拟节点来解决负载不均衡的问题。将每台物理服务器虚拟为一组虚拟服务器,将虚拟服务器放置到哈希环上,如果要确定对象的服务器,需先确定对象的虚拟服务器,再由虚拟服务器确定物理服务器。

分布式ID

UUID

优点生成简单本地生成无网络成本

缺点:无序的字符串,不具备趋势自增特性没有具体的业务含义、长度过长16字节128位,36位长度的字符串,存储以及查询对MySQL的性能消耗较大,作为数据库主键UUID的无序性会导致数据位置频繁变动,严重影响性能

数据库自增ID

需要一个单独的MySQL实例用来生成ID,要一个ID时,向表中插入一条记录返回主键ID,但该方式在访问量激增时MySQL本身就是系统的瓶颈,用它来实现分布式服务风险比较大。

优点:实现简单,ID单调自增,数值类型查询速度快

缺点:DB单点存在宕机风险,无法扛住高并发场景

数据库多主模式

主从模式多主模式集群,设置起始值自增步长

优点:解决DB单点问题

缺点:不利于后续扩容,且实际上单个数据库自身压力还是大,依旧无法满足高并发场景

号段模式

一次获取一批号段,可以基于MySQL也可以局域Redis或Zookeeper等

Redis

RDB模式:若连续自增但redis没及时持久化,但Redis挂掉了,重启Redis后会出现ID重复的情况

AOF会对每条写命令进行持久化,即使Redis挂掉了也不会出现ID重复的情况,但由于incr命令的特殊性,会导致Redis重启恢复的数据时间过长

雪花算法SnowFlake

雪花算法能保证不同进程主键不重复相同进程主键有序,二进制形式包含4部分,从高位到低位分表为:1bit符号位41bit时间戳位10bit工作进程位12bit序列号位毫秒数在高位自增序列在低位,整个ID趋势递增;不依赖第三方组件稳定性高,生成ID性能也非常高可根据自身业务特性分配bit非常灵活;但强依赖机器时钟,若机器上时钟回拨会导致发号重复

  • 1bit符号位:预留的符号位恒为零
  • 41bit时间戳位:41位时间戳可容纳毫秒数为2的41次幂,一年所使用的毫秒数为365 * 24 * 60 * 60 * 1000 Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L) = 69.73年不重复
  • 10bit工作进程位:该标志Java进程内唯一,若分布式应用部署应保证每个工作进程的id不同,默认为0可通过属性设置
  • 12bit序列号位:该序列用来在同一个毫秒内生成不同的ID,若在该毫秒内生成数量超过4096即2的12次幂,则生成器会等待到下个毫秒继续生成。

滴滴出品TinyID

Tinyid是基于号段模式原理实现的

百度uid-generator

uid-generator是基于Snowflake算法实现,与原始的snowflake算法不同在于,uid-generator支持自定义时间戳工作机器ID和序列号等各部分的位数,而且uid-generator中采用用户自定义workId的生成策略。

uid-generator需要与数据库配合使用,需要新增一个WORKER_NODE表。当应用启动时会向数据库表中去插入一条数据,插入成功后返回的自增ID就是该机器的workId数据由host,port组成。

美团Leaf

Leaf同时支持号段模式snowflake算法模式,可以切换使用;Leaf的snowflake模式依赖于ZooKeeper,不同于原始snowflake算法也主要是在workId的生成上,LeafworkId是基于ZooKeeper的顺序Id来生成的,每个应用在使用Leaf-snowflake时,启动时都会都在Zookeeper中生成一个顺序Id,相当于一台机器对应一个顺序节点,也就是一个workId

不存在数据过滤

使用布隆过滤器

接口幂等性解决方案

需要判断哪些操作是需要回滚的,以及这次成功下次不需要再处理了,数据积累问题怎么解决

  • 全局唯一ID
  • 去重表:可以在redis中做去重表
  • 状态机:将状态从0改成1

分布式Session解决方案

Session StickyIP进行hash分发到固定的机器上,但是会存在单点故障问题,

Session Relication即Session复制,可解决单点复制,但耗内存、耗宽带

Session Center将Session存入Redis应用与Redis通信

数据库优化

  • 换数据库:MySQL -> Redis;读多写少使用Redis做缓存,ES搜索(全量更新、增量更新)
  • 分库分表,读写分离;
    • JDBC应用层:shardingspheretddl;性能更高、不支持夸语言
    • Proxy代理层:mycatmysql-proxy;性能相对较差、支持夸语言、业务侵入性低

jdbc应用层分片

Proxy代理层分片

图片处理

可以存OSS等

后端优化

一般商品详情页都是读多写少,通过多级缓存

缓存应用场景

  • 访问量大、QPS高、更新频率不是很高的业务
  • 数据一致性要求不高

缓存一致性问题

提高请求的吞吐量

  • 减少磁盘IO
  • 减少网络IO

若商品详情页数据过大,Redis会存在网络瓶颈,需要对数据进行压缩后再缓存到Redis,提升通讯性能。

  • 最终一致性
    • 设置超时时间
  • 实时一致性
    • canal binlog日志实时同步

二级缓存

L1缓存失效时间L2缓存失效时间。请求优先从L1缓存获取数据,如果未命中则加锁,保证只有一个线程去数据库中读取数据然后再更新到L1和L2中。然后其他线程依然在L2缓存获取数据。

多级缓存设计

对于高并发系统来说,网络IO磁盘IO对系统的影响比较大,引入Redis缓存的目的是提高网站的性能,本质是不走磁盘走内存减少磁盘IO来提高性能,但是增加了网络的操作,若是本地缓存的既可以减少磁盘IO也可以减少网络IO。引入Guava缓存,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LocalCache {
private Cache<String, ProductParam> localCache = null;
@PostConstruct
private void init() {
localCache = CacheBuilder.newBuilder()
.initialCapacity(10) //设置本地缓存容器的初始容量
.maximumSize(500) //设置本地缓存的最大容量
.expireAfterWrite(60, TimeUnit.SECONDS) //设置写缓存后多少秒过期
.build();
}
public void setLocalCache(String key, PmsProductParam object) {
localCache.put(key, object);
}
public PmsProductParam get(String key) {
return localCache.getIfPresent(key);
}
}

前端页面静态化处理

可通过FreeMarker模板引擎,基于模板和数据源生成前端静态页面,适用于小流量商品详情页缓存架构,缺点是每个商品都需要生成一个静态页面,且若有多个机房每个产品得生成多个静态页面或生成一个静态页面同步多个机房。且若插入、修改、数据调整导致模板变化所有产品得重新生成。

架构问题:数据新增分增量更新或全量更新,不同应用部署在不同服务器甚至不同的机房或国家,存在数据同步问题

  • 通过网络方式同步:其中一台服务器静态化,把文件同步到其他应用服务器上,如scp命令
  • 定时任务:在每个应用使用一个定时任务,分别执行数据库需要静态话的数据,解决了同步问题,但产生了数据重复执行问题
  • 消息中间件:通过消息中间件,订阅Topic生成当前服务器静态化的页面

后天数据有变更如何及时更新同步到其他服务端,页面静态化后,搜索打开一个商品详细页,怎么确定需要访问的静态页面,或若模板修改等问题

前端缓存

小流量架构

可通过FreeMarker模板引擎,基于模板和数据源生成前端静态页面,适用于小流量商品详情页缓存架构,缺点是每个商品都需要生成一个静态页面,且若有多个机房每个产品得生成多个静态页面或生成一个静态页面同步多个机房。且若插入、修改、数据调整导致模板变化所有产品得重新生成。

小流量商品详情页缓存架构

架构问题:数据新增分增量更新或全量更新,不同应用部署在不同服务器甚至不同的机房或国家,存在数据同步问题

  • 通过网络方式同步:其中一台服务器静态化,把文件同步到其他应用服务器上,如scp命令
  • 定时任务:在每个应用使用一个定时任务,分别执行数据库需要静态话的数据,解决了同步问题,但产生了数据重复执行问题
  • 消息中间件:通过消息中间件,订阅Topic生成当前服务器静态化的页面

数据有变更如何及时更新同步到其他服务端,页面静态化后,搜索打开一个商品详细页,怎么确定需要访问的静态页面,或若模板修改等问题

大型网站架构

可使用OpenResty来搭建能够处理超高并发、扩展性极高的动态Web应用、Web服务和动态网关。OpenResty是基于Nginx和Lua脚本的高性能Web平台,内部集成了大量精良的Lua库、第三方模块以及大多数的依赖项。通过汇聚各种设计精良的Nginx模块,从而将Nginx有效的变成一个强大的通用Web应用平台。

高并发整体架构

高并发架构v1

使用静态化技术,按照商品维度生成静态化HTML

  • 通过MQ得到变更通知
  • 通过Java Worker调用多个依赖系统生成详情页HTML
  • 通过rsync同步到其他机器
  • 通过Nginx直接输出静态页
  • 接入层负责负载均衡

随着商品数量的增加这种架构的存储容量到达了瓶颈,且按照商品维度生成整个页面会存在如分类维度变更就要全部刷一遍该分类下所有信息的问题

  • 若只有分类、模板变更,所有相关的商品都需要重新静态化
  • 随着商品数量的增加,rsync会成为瓶颈
  • 无法迅速响应一些页面需求变更,大部分都是通过JavaScript动态改页面元素。

方案改进

  • 容量问题通过按照商品尾号做路由分散到多台机器,按照自营商品单独一台,第三方商品分散到11台
  • 按维度生成HTML片段(框架、商品介绍、规格参数、面包屑、相关分类、店铺信息),而不是一个大HTML
  • 通过Nginx SSI合并片段输出
  • 接入层负责负载均衡
  • 多机房部署也无法通过rsync同步,而是使用部署多套相同架构来实现

该方案主要缺点,随着业务的发展,无法满足迅速变化、还有一些变态的需求

  • 碎片文件太多,导致无法rsync
  • 机械盘SSI合并时,高并发时性能差
  • 模板如果要变更,数亿商品需要数天才能刷完
  • 到达容量瓶颈时,会删除一部分静态化商品,然后通过动态渲染输出,动态渲染系统在高峰时会导致依赖系统压力大,抗不住
  • 还是无法迅速响应一些业务需求

高并发架构v2

存在需要解决的问题

  • 能迅速响瞬变的需求,和各种变态需求
  • 支持各种垂直化页面改版
  • 页面模块化
  • AB测试
  • 高性能、水平扩容
  • 多机房多活、异地多活

高并发架构v2

目前架构的目标不仅仅是为商品详情页提供数据,只要是Key-Value获取的而非关系的都可以提供服务,叫做动态服务系统

  • 数据变更还是通过MQ通知
  • 数据异构Worker得到通知,然后按照一些维度进行数据存储,存储到数据异构JIMDB集群Redis+持久化引擎,存储的数据都是未加工的原子化数据,如商品基本信息、商品扩展属性、商品其他一些相关信息、商品规格参数、分类、商家信息等
  • 数据异构Worker存储成功后,会发送一个MQ给数据同步Worker,数据同步Worker也可叫做数据聚合Worker,按照相应的维度聚合数据存储到相应的JIMDB集群;三个维度:基本信息(基本信息+扩展属性等的一个聚合)、商品介绍(PC版、移动版)、其他信息(分类、商家等维度,数据量小,直接Redis存储)
  • 前端展示分为两个:商品详情页和商品介绍,使用Nginx+Lua技术获取数据并渲染模板输出