1. 一致性 Hash 算法
1.0 介绍
- 应用案例:MD5、SHA等加密算法,数据存储和查询用到了 Hash 表
- Hash 的优点:查询效率高,时间复杂度接近于 O(1)
- 直接寻址法:把数据和数组的下标绑定在一起,查找时通过 array[n] 直接获取结果,但是当数据较分散时会浪费空间
- 开放寻址法:如果目标位置不为空,向前或向后找空闲位置存放,但是数组长度固定,存放的数据有上限
- 拉链法:数组位置上存放链表的引用,当 Hash 冲突时,新加入的元素跟在链表后面
1.1 Hash 算法应用场景
归纳为两种
-
请求的负载均衡,如 nginx 的 ip_hash 策略
- 查找 ngx_http_upstream_ip_hash_module.c 文件,搜索 addrlen,可以看到对 ip 的前 3 端(同一个子域)进行hash
for (i = 0; i < (ngx_uint_t) iphp->addrlen; i++) { hash = (hash * 113 + iphp->addr[i]) % 6271; }
- 查找 ngx_http_upstream_ip_hash_module.c 文件,搜索 addrlen,可以看到对 ip 的前 3 端(同一个子域)进行hash
-
分布式存储:如 Redis 集群、MySQL 分库分表
- redis 集群下,对 key 进行 hash 来决定存储到哪个节点,如3台,hash(key) % 3 = index
1.2 普通 Hash 算法存在的问题
-
普通 hash 过来的请求会一直路由到集群的一台服务器
-
缩容时,如果该服务器宕机下线,那么请求就得不到响应,需要对客户端的 ip 重新 hash,迁移全部请求,计算量大
-
扩容时,集群增加服务器后,同样需要对客户端的 ip 重新 hash,迁移全部请求,计算量大
1.3 一致性 Hash 算法
-
把服务器的 ip 或者主机名的 hash 值映射到一个从0到2的32次方-1的圆形闭环,这样的环叫哈希环。接收客户端的请求时根据 ip 的 hash 值对应到环上的某个位置,然后顺时针找到最近的服务器节点。
-
缩容时,原来路由到服务器3的客户端重新路由到服务器4,不影响其他客户端,避免大量迁移请求
-
扩容时,原路由到服务器3的客户端重新路由到服务器5,不影响其他客户端,避免大量迁移请求
-
虚拟节点机制
采用一致性哈希算法,如果服务器节点太少时,节点分布不均,会造成某个节点处理大量的请求,产生数据倾斜问题,可以使用虚拟节点,哈希环上多个节点对应一个真实服务器节点
1.4 手写一致性 Hash 算法
- 普通 hash 算法实现
- 一致性 hash 算法实现(不包括虚拟节点)
- 一致性 hash 算法实现(包括虚拟节点)
1.5 Nginx 配置一致性 Hash 负载均衡策略
使用 ngx_http_upstream_consistent_hash 第三方模块根据参数配置选择不同的策略
- consistent_hash $remote_addr:可以根据客户端ip映射
- consistent_hash $request_uri:根据客户端请求的uri映
- consistent_hash $args:根据客户端携带的参数进⾏映
2. 集群时钟同步问题
场景
- 如果集群中每台服务器都可以访问网络,那么可以各自同步网络时间
- 如果集群中只有一部分可以访问网络,那么某台服务器从网络同步时间,其他服务器以这台服务器的时间为准
- 如果集群的服务器都不能访问网络,那么以某台服务器的时间为准
操作
# 第一种:各自使用 ntpdate 同步 NTP 服务器(上海)的时间
ntpdate -u ntp.api.bz
# 第二种,以某台服务器时间为准
# 修改 /etc/ntp.conf 文件,放开局域⽹同步功能,172.17.0.0是你的局域⽹⽹段
restrict 172.17.0.0 mask 255.255.255.0 nomodify notrap
server 127.127.1.0 # local clock
fudge 127.127.1.0 stratum 10
# 重启⽣效并配置ntpd服务开机⾃启动
service ntpd restart
chkconfig ntpd on
# 集群中其他节点可以从该服务器同步时间
ntpdate 172.17.0.17
# 使用 Linux 的定时任务定期同步时间
* /5 * * * * ntpdate -u ntp.api.bz
3. 分布式 ID 解决方案
分库分表之后如果仍然使用数据库的自增 ID 作为主键,会出现重复,分布式环境下需要全局唯一的主键。
3.1 使用 UUID 作为主键
- 优点:简单,只需要使用 java 的 UUID 工具类即可生成
- 缺点:字符串太长,没有规律(非单调递增)
3.2 SnowFlake 雪花算法
雪花算法可以产生一个 long 型的 ID
- 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0
- 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截) 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
- 10位的数据机器位,可以部署在1024个节点,包括**5位datacenterId和5位workerId。**10-bit机器可以分别表示1024台机器。如果我们对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,可以根据自身需求定义。
- 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号。12个自增序列号可以表示2^12个ID,理论上snowflake方案的QPS约为409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。
优点
- 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
- 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
- 可以根据自身业务特性分配bit位,非常灵活。
缺点
- 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。
- 针对此,美团做出了改进:https://github.com/Meituan-Dianping/Leaf
- 容器环境下,机器id会变,怎么解决?
3.3 使用 Redis 的 Incr 命令
优点
- 高性能,Redis 的 QPS 能够达到 10 w;
- Redis 是单线程内存模型,高并发场景下不会重复发号,
缺点
- 数据需要持久化,避免宕机后重复发号
4. 分布式调度问题
4.1 定时任务场景
- 订单审核、出库
- 订单超时⾃动取消、⽀付退款
- 礼券同步、⽣成、发放作业
- 物流信息推送、抓取作业、退换货处理作业
- 数据积压监控、⽇志监控、服务可⽤性探测作业
- 定时备份数据
- ⾦融系统每天的定时结算
- 数据归档、清理作业
- 报表、离线数据分析作业
4.2 分布式调度
两层含义
- 任务的多实例高可用,某节点下线后任务可以交给其他节点执行
- 任务分片,定时任务可以拆分成小的定时任务,并行执行
4.3 定时任务和消息队列的区别
- 共同点
- 异步处理
- 应用解耦
- 流量消峰
- 本质区别
- 定时任务是时间驱动,而 MQ 是事件驱动
- 定时任务适合批处理,如每日利息结算,而 MQ 适合逐条处理的场景
4.4 分布式调度框架 Elastic-Job
4.4.1 介绍
特点:
- 轻量级
- 使用简便,只需要一个 jar 和 Zookeeper
- 不需要独立部署
- 去中心化
- 执行节点对等(程序和 jar 一样,唯一不同的可能是分片)
- 定时调度自触发(没有中心调度节点分配,主动去竞争创建leader节点)
- 服务自发现(节点上下线不需要额外操作,上线了就竞争创建leader节点)
- 主节点非固定(没有分发任务的节点)
主要功能:
-
分布式调度协调
在分布式环境中,任务能够按指定的调度策略执⾏,并且能够避免同⼀任务多实例重复执⾏
-
丰富的调度策略
基于成熟的定时任务作业框架 Quartz cron 表达式执⾏定时任务
-
弹性扩容缩容
当集群中增加某⼀个实例,它应当也能够被选举并执⾏任务;当集群减少⼀个实例时,它所执⾏的任务能被转移到别的实例来执⾏。
-
失效转移
某实例在任务执⾏失败后,会被转移到其他实例执⾏
-
错过执⾏作业重触发
若因某种原因导致作业错过执⾏,⾃动记录错过执⾏的作业,并在上次作业完成后⾃动触发。
-
⽀持并⾏调度
⽀持任务分⽚,任务分⽚是指将⼀个任务分为多个⼩任务项在多个实例同时执⾏。
-
作业分⽚⼀致性
当任务被分⽚后,保证同⼀分⽚在分布式环境中仅⼀个执⾏实例。
4.4.2 应用
只需一个 jar 和 Zookeeper 即可完成分布式调用, 此处 Zookeeper 的本质功能是存储和通知
-
引入jar
<dependency> <groupId>com.dangdang</groupId> <artifactId>elastic-job-lite-core</artifactId> <version>2.1.5</version> </dependency>
-
编写定时任务类,实现com.dangdang.ddframe.job.api.simple.SimpleJob
-
配置分布式协调服务(注册中心)Zookeeper
ZookeeperConfiguration zookeeperConfiguration = new ZookeeperConfiguration("localhost:2181","data-archive-job"); CoordinatorRegistryCenter coordinatorRegistryCenter = new ZookeeperRegistryCenter(zookeeperConfiguration); coordinatorRegistryCenter.init();
-
配置任务:时间事件(包括任务分片)、定时任务业务逻辑、调度器
JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration.newBuilder("archive-job", "*/2 * * * * ?", 3) .shardingItemParameters("0=bachelor,1=master,2=doctor").build(); SimpleJobConfiguration simpleJobConfiguration = new SimpleJobConfiguration(jobCoreConfiguration,ArchivieJob.class.getName()); JobScheduler jobScheduler = new JobScheduler(coordinatorRegistryCenter, LiteJobConfiguration.newBuilder(simpleJobConfiguration).overwrite(true).build()); jobScheduler.init();
5. Session 共享问题
5.1 问题
集群环境下,客户端第二次请求轮询到另一台服务器,由于 HTTP 是无状态的,因此服务器无法识别是否登录过,导致重复登录, 即 Session 丢失问题。
5.2 解决 Session 一致性的方案
使用 Nginx 的 ip_hash,同⼀个客户端IP的请求都会被路由到同⼀个⽬标服务器(会话粘滞)
-
优点:配置简单,不入侵应用,不需要额外修改代码
-
缺点:
服务器重启 Session 会丢失
存在单点负载高的风险(恶意客户端发出大量请求到同一个目标服务器)
单点故障
使用 Redis 缓存 Session
-
优点
能适应各种负载均衡策略
服务器重启或宕机不会丢失 Session
扩展能力强,集群扩容同样使用 Redis 缓存,可以直接扩容
适合大集群数量使用
-
缺点:对应用有入侵,引入了和 Redis 的交互代码
5.3 Spring Session 用法
-
引入 jar
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
-
配置 Redis
spring.redis.database=0 spring.redis.host=127.0.0.1 spring.redis.port=6379
-
添加注解 @EnableRedisHttpSession 到启动类