前言
在面试的时候,尝尝会被问到分布式一些相关的问题,比如如何确保在分布式环境下 session 一致,分布式 ID 等等。
一致性Hash 算法
解决hash 冲突的方法:
开放寻址法:1放进去了,6再来的时候,向前或者向后找空闲位置存放,不好的地⽅,如果数组⻓度定
义好了⽐如10,⻓度不能扩展,来了11个数据,不管Hash冲突不冲突,肯定存不下这么多数据
拉链法:数据⻓度定义好了,怎么存储更多内容呢,算好Hash值,在数组元素存储位置放了⼀个链表.也就是出现重读的,我们就通过链表存储起来,也叫链地址法。hashmap 就是采用在这种结构哒。
hash 算法应用场景
nginx 的负载均衡
我们知道 nginx 实现负载均衡有三种方式。轮寻、设置权重、配置 ip_hash 。‘’
其中配置 ip_hash 就使用到了 hash 算法。Nginx的 IP_hash策略可以在客户端ip不变的情况下,将其发出的请求始终路由到同⼀个⽬标服务器上,实现会话粘滞,避免处理session共享问题 。对ip地址或者sessionid进⾏计算哈希值,哈希值与服务器数量进⾏取模运算,得到的值就是当前请求应该被路由到的服务器编号,如此,同⼀个客户端ip发送过来的请求就可以路由到同⼀个⽬标服务器,实现会话粘滞。
分布式存储
以分布式内存数据库Redis为例,集群中有redis1,redis2,redis3 三台Redis服务器那么,在进⾏数据存储时,<key1,value1>数据存储到哪个服务器当中呢?针对key进⾏hash处理hash(key1)%3=index, 使⽤余数index锁定存储的具体服务器节点。
普通hash 简单实现
我们写一个普通hash 的简单实现。
public class GeneralHash {
public static void main(String[] args) {
// 定义客户端IP
String[] clients = new String[]{
"192.168.1.61",
"192.168.1.48",
"192.168.1.44",
"192.168.1.42",
"192.168.1.43",
"192.168.1.73",
"192.168.1.83",
"192.168.1.23"};
//服务器数量
int count =5;
for (String client : clients) {
int hash = Math.abs(client.hashCode());
int index=hash%count;
System.out.println("IP 为:"+client+ "服务器编号为:"+index);
}
}
}
结果:
IP 为:192.168.1.61服务器编号为:2
IP 为:192.168.1.48服务器编号为:2
IP 为:192.168.1.44服务器编号为:3
IP 为:192.168.1.42服务器编号为:1
IP 为:192.168.1.43服务器编号为:2
IP 为:192.168.1.73服务器编号为:0
IP 为:192.168.1.83服务器编号为:1
IP 为:192.168.1.23服务器编号为:0
普通 hash 算法存在的问题
比如上面的 ip_hash 是利用取模运算,但是如果出现一个服务宕机或者出现扩容和缩容的情况,就会导致重新hash .那么原来的会话就会丢失。
一致性 hash 算法
首先有一条一条直线。为 0 到 2 的 32 次方 -1。然后将首尾相连,形成一个闭环,也就是 hash 环。
如下图所示:我们的服务器节点散落在这个环上。当请求的 ip 通过一致性 hash 可以找到离他最近的节点,从而进行访问。
当出现服务宕机,或者缩容时。只会影响部分的 IP 进行重新指向。
同理,增加服务器的时候,也只会影响部分 IP 重新指定。
demo 实现
我们使用 代码来实现一个一致性 hash 算法 借助 SortedMap 的 tailMap(K fromKey)获取一个子集。其所有对象的 key 的值大于等于 fromKey 。然后 firstKey() 获取最小 key
/**
* 无虚拟节点
*/
public class ConsistentHashNoVirtual {
public static void main(String[] args) {
//step 1:将服务器的节点存到 hash 环中。
// 定义服务器节点
String[] servers = new String[]{
"192.168.1.10",
"192.168.1.30",
"192.168.1.50",
"192.168.1.70",
"192.168.1.90",
"192.168.2.10",
"192.168.1.30",
"192.168.2.80"};
//定义一个hash 环
SortedMap<Integer, String> hashServerMap = new TreeMap<>();
for (String server : servers) {
int hash = Math.abs(server.hashCode());
hashServerMap.put(hash,server);
}
//step 2:将客户端的IP映射到 hash 环中。
String[] clients = new String[]{
"10.168.1.10",
"10.168.2.10",
"10.168.3.10",
"10.168.4.10",
"10.168.5.10",
"10.168.6.10",
"10.168.7.10",
"192.168.1.40",
"192.168.1.60",
"192.168.1.80",
"192.168.2.00",
"192.168.2.30",
"192.168.2.50",
"192.168.3.50",
"192.168.4.50",
"192.168.2.90"};
for (String client : clients) {
//step3 针对客户端,找到能够处理当前客户端请求的服务器(哈希环上顺时针最近)
// 根据客户端ip的哈希值去找出哪一个服务器节点能够处理()
int clienthash = Math.abs(client.hashCode());
//tailMap(K fromKey)获取一个子集。其所有对象的 key 的值大于等于 fromKey
SortedMap<Integer, String> tailMap = hashServerMap.tailMap(clienthash);
Integer firstKey = hashServerMap.firstKey();
if(!tailMap.isEmpty()){
firstKey = tailMap.firstKey();
}
System.out.println("客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
}
}
}
结果:
问题
从上面的结果来看,这种会存在一问题,也就是可能出现数据倾斜。⼀致性哈希算法在服务节点太少时,容易因为节点分部不均匀⽽造成数据倾斜问题。例如系统中只有两台服务器,其环分布如下,节点2只能负责⾮常⼩的⼀段,⼤量的客户端请求落在了节点1上,这就是数据(请求)倾斜问题。
解决方案
一致性hash 算法引入了虚拟节点机制。为每个服务节点计算多个hash,每个hash 放置一个服务器几点,称为虚拟节点。
具体做法可以在服务器ip或主机名的后⾯增加编号来实现。⽐如,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “节点1的ip#1”、“节点1的ip#2”、“节点1的ip#3”、“节点2的ip#1”、“节点2的ip#2”、“节点2的ip#3”的哈希值,于是形成六个虚拟节点,当客户端被路由到虚拟节点的时候其实是被路由到该虚拟节点所对应的真实节点
demo 实现
我们在上面代码的基础上增加,虚拟节点。这里的 rehash 算法需要实际调整,我这里只是随便写的。
就在原来的基础上加了这部分代码,我们再运行看下。可以看到原来后面这部分就重新分配了。
集群时钟同步问题
当我们的服务部署在多台服务器时,如果这些服务器的时间不一致必定会导致各种问题。
所以需要保证集群所在的服务器时间保持一致。
可以联网
#使⽤ ntpdate ⽹络时间同步命令
ntpdate -u ntp.api.bz #从⼀个时间服务器同步时间
不能联网
1、如果有 restrict default ignore,注释掉它
2、添加如下⼏⾏内容
restrict 172.17.0.0 mask 255.255.255.0 nomodify notrap # 放开局
域⽹同步功能,172.17.0.0是你的局域⽹⽹段
server 127.127.1.0 # local clock
fudge 127.127.1.0 stratum 10
3、重启⽣效并配置ntpd服务开机⾃启动
service ntpd restart
chkconfig ntpd on
分布式 ID 解决方案
解决方法:
- UUID 使用uuid 最为主键,每次都会随机生成保证不一样。
- 独⽴数据库的⾃增ID
- SnowFlake 雪花算法
雪花算法是⼀个算法,基于这个算法可以⽣成ID,⽣成的ID是⼀个long型,那么在Java中⼀个long 型是8个字节,算下来是64bit,如下是使⽤雪花算法⽣成的⼀个ID的⼆进制形式示意
- 借助Redis的Incr命令获取全局唯⼀ID
分布式调度问题
什么是分布式调度
- 运⾏在分布式集群环境下的调度任务(同⼀个定时任务程序部署多份,只应该有⼀个定时任务在执⾏)
- 分布式调度—>定时任务的分布式—>定时任务的拆分(即为把⼀个⼤的作业任务拆分为多个⼩的作业任务,同时执⾏)
定时任务与消息队列的区别
共同点:
- 异步处理。⽐如注册、下单事件
- 应⽤解耦。不管定时任务作业还是MQ都可以作为两个应⽤之间的⻮轮实现应⽤解耦,这个⻮轮可以中转数据,当然单体服务不需要考虑这些,服务拆分的时候往往都会考虑
- 流量削峰。双⼗⼀的时候,任务作业和MQ都可以⽤来扛流量,后端系统根据服务能⼒定时处理订单或者从MQ抓取订单抓取到⼀个订单到来事件的话触发处理,对于前端⽤户来说看到的结果是已经下单成功了,下单是不受任何影响的
不同点:
定时任务作业是时间驱动,⽽MQ是事件驱动;
时间驱动是不可代替的,⽐如⾦融系统每⽇的利息结算,不是说利息来⼀条(利息到来事件)就算⼀下,⽽往往是通过定时任务批量计算;所以,定时任务作业更倾向于批处理,MQ倾向于逐条处理;
分布式调度框架 Elastic-Job
Elastic-Job是当当⽹开源的⼀个分布式调度解决⽅案,基于Quartz⼆次开发的,由两个相互独⽴的⼦项⽬Elastic-Job-Lite和Elastic-Job-Cloud组成。我们要学习的是 Elastic-Job-Lite,它定位为轻量级⽆中⼼化解决⽅案,使⽤Jar包的形式提供分布式任务的协调服务,⽽Elastic-Job-Cloud⼦项⽬需要结合Mesos以及Docker在云环境下使⽤。
Elastic-Job的github地址:
https://github.com/elasticjob
主要功能介绍
- 分布式调度协调。在分布式环境中,任务能够按指定的调度策略执⾏,并且能够避免同⼀任务多实例重复执⾏
- 丰富的调度策略 基于成熟的定时任务作业框架Quartz cron表达式执⾏定时任务
- 弹性扩容缩容 当集群中增加某⼀个实例,它应当也能够被选举并执⾏任务;当集群减少⼀个实例时,它所执⾏的任务能被转移到别的实例来执⾏。
- 失效转移 某实例在任务执⾏失败后,会被转移到其他实例执⾏错过执⾏作业重触发 若因某种原因导致作业错过执⾏,⾃动记录错过执⾏的作业,并在上次作业完成后⾃动触发。
- ⽀持并⾏调度 ⽀持任务分⽚,任务分⽚是指将⼀个任务分为多个⼩任务项在多个实例同时执⾏。
- 作业分⽚⼀致性 当任务被分⽚后,保证同⼀分⽚在分布式环境中仅⼀个执⾏实例。
引用
<!-- https://mvnrepository.com/artifact/com.dangdang/elastic-job-lite-core
-->
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>elastic-job-lite-core</artifactId>
<version>2.1.5</version>
</dependency>
Session 共享问题
session 丢失问题
从根本上来说是因为Http协议是⽆状态的协议。客户端和服务端在某次会话中产⽣的数据不会被保留下来,所以第⼆次请求服务端⽆法认识到你曾经来过, Http为什么要设计为⽆状态协议?早期都是静态⻚⾯⽆所谓有⽆状态,后来有动态的内容更丰富,就需要有状态,出现了两种⽤于保持Http状态的技术,那就是Cookie和Session。⽽出现上述不停让登录的问题,分析如下图:
解决 Session ⼀致性的⽅案
- Nginx的 IP_Hash 策略(可以使⽤)
同⼀个客户端IP的请求都会被路由到同⼀个⽬标服务器,也叫做会话粘滞
优点:
配置简单,不⼊侵应⽤,不需要额外修改代码
缺点:
服务器重启Session丢失
存在单点负载⾼的⻛险
单点故障问题
- Session复制(不推荐)
多个tomcat之间通过修改配置⽂件,达到Session之间的复制
优点:
不⼊侵应⽤
便于服务器⽔平扩展
能适应各种负载均衡策略
服务器重启或者宕机不会造成Session丢失
缺点:
性能低
内存消耗
不能存储太多数据,否则数据越多越影响性能
延迟性
- Session共享,Session集中存储(推荐)
优点:
能适应各种负载均衡策略
服务器重启或者宕机不会造成Session丢失
扩展能⼒强
适合⼤集群数量使⽤
缺点:
对应⽤有⼊侵,引⼊了和Redis的交互代码
总结
这些问题都是面试会别问到的高频问题,所以赶紧 get 吧