文章目录
- 一致性Hash算法
- Hash算法应用场景
- 普通Hash算法存在的问题
- 一致性Hash算法
- 手写一致性Hash算法
- Nginx配置一致性Hash负载均衡策略
- 集群时钟同步
- 时钟不同步导致的问题
- 集群时钟同步策略
- 分布式ID解决方案
- 分布式调度问题
- 定时任务的场景
- 什么是分布式调度
- 定时任务与消息队列的区别
- 定时任务的实现方式
- 分布式调度框架Elastic-Job
- Session共享问题
- Session问题原因分析
- 解决Session一致性的方案
主要内容
- 第一部分:一致性hash算法
- 第二部分:集群时钟同步问题
- 第三部分:分布式ID解决方案
- 第四部分:分布式调度问题
- 第五部分:Session共享
分布式和集群
分布式和集群是不⼀样的,分布式⼀定是集群,但是集群不⼀定是分布式(因为集群就是多个实例⼀起⼯作,分布式将⼀个系统拆分之后那就是多个实例;集群并不⼀定是分布式,因为复制型的集群不是拆分⽽是复制
分布式: 把一个系统拆分成多个子系统,每个子系统负责把各自的部分功能,独立部署,各司其职
集群: 多个实例公共工作,一个应用复制多份部署
一致性Hash算法
Hash算法,⽐如说在安全加密领域MD5、SHA等加密算法,在数据存储和查找⽅⾯有Hash表等, 以上都应⽤到了Hash算法。
为什么需要使⽤Hash? Hash算法较多的应⽤在数据存储和查找领域,最经典的就是Hash表,它的查询效率⾮常之⾼,其中的哈希算法如果设计的⽐较ok的话,那么Hash表的数据查询时间复杂度可以接近于O(1)
需求:提供⼀组数据 1,5,7,6,3,4,8,对这组数据进⾏存储,然后随便给定⼀个数n,请你判断n是否存在于刚才的数据集中?
传统方法:顺序查找法和二分法
直接寻址法:直接把数据和数组的下标绑定到⼀起,查找的时候,直接array[n]就取出了数据
优点:速度快,⼀次查找得到结果
缺点:1)浪费空间,⽐如 1,5,7,6,3,4,8,12306 ,最⼤值12306 ,按照上述⽅式需要定义⼀个⽐如⻓度为12307的数组,但是只存储零星的⼏个数据,其他位置空间都浪费着 2)数据如:1,5,7,6,3,4,8,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2最⼤值12,⽐如开辟13个空间,存储不了这么多内容
优化: Hash取模法
带来hash冲突的问题,使用开放寻址法和拉链法
Hash算法应用场景
Hash算法在分布式集群架构中的应⽤场景
Hash算法在很多分布式集群产品中都有应⽤,⽐如分布式集群架构Redis、Hadoop、ElasticSearch,Mysql分库分表,Nginx负载均衡等
主要的应⽤场景归纳起来两个
- 请求的负载均衡(⽐如nginx的ip_hash策略)
Nginx的IP_hash策略可以在客户端ip不变的情况下,将其发出的请求始终路由到同⼀个⽬标服务器上,实现会话粘滞,避免处理session共享问题
如果没有IP_hash策略,那么如何实现会话粘滞?可以维护⼀张映射表,存储客户端IP或者sessionid与具体⽬标服务器的映射关系<ip,tomcat1>
缺点
- 1)那么,在客户端很多的情况下,映射表⾮常⼤,浪费内存空间
- 2)客户端上下线,⽬标服务器上下线,都会导致重新维护映射表,映射表维护成本很⼤
如果使⽤哈希算法,事情就简单很多,我们可以对ip地址或者sessionid进⾏计算哈希值,哈希值与服务器数量进⾏取模运算,得到的值就是当前请求应该被路由到的服务器编号,如此,同⼀个客户端ip发送过来的请求就可以路由到同⼀个⽬标服务器,实现会话粘滞。
- 分布式存储
以分布式内存数据库Redis为例,集群中有redis1,redis2,redis3 三台Redis服务器
那么,在进⾏数据存储时,<key1,value1>数据存储到哪个服务器当中呢?针对key进⾏hash处理hash(key1)%3=index, 使⽤余数index锁定存储的具体服务器节点
普通Hash算法存在的问题
普通Hash算法存在⼀个问题,以ip_hash为例,假定下载⽤户ip固定没有发⽣改变,现在tomcat3出现了问题,down机了,服务器数量由3个变为了2个,之前所有的求模都需要重新计算。
如果在真实⽣产情况下,后台服务器很多台,客户端也有很多,那么影响是很⼤的,缩容和扩容都会存在这样的问题,⼤量⽤户的请求会被路由到其他的⽬标服务器处理,⽤户在原来服务器中的会话都会丢失。
一致性Hash算法
⼀致性哈希算法思路如下:
⾸先有⼀条直线,直线开头和结尾分别定为为1和2的32次⽅减1,这相当于⼀个地址,对于这样⼀条线,弯过来构成⼀个圆环形成闭环,这样的⼀个圆环称为hash环。我们把服务器的ip或者主机名求hash值然后对应到hash环上,那么针对客户端⽤户,也根据它的ip进⾏hash求值,对应到环上某个位置,然后如何确定⼀个客户端路由到哪个服务器处理呢?按照顺时针⽅向找最近的服务器节点
假如将服务器3下线,服务器3下线后,原来路由到3的客户端重新路由到服务器4,对于其他客户端没有影响只是这⼀⼩部分受影响(请求的迁移达到了最⼩,这样的算法对分布式集群来说⾮常合适的,避免了⼤量请求迁移 )
缩容-减少服务器
增加服务器5之后,原来路由到3的部分客户端路由到新增服务器5上,对于其他客户端没有影响只是这⼀⼩部分受影响(请求的迁移达到了最⼩,这样的算法对分布式集群来说⾮常合适的,避免了⼤量请求迁移 )
扩容-增加服务器
1)如前所述,每⼀台服务器负责⼀段,⼀致性哈希算法对于节点的增减都只需重定位环空间中的⼀⼩部分数据,具有较好的容错性和可扩展性。
但是,⼀致性哈希算法在服务节点太少时,容易因为节点分部不均匀⽽造成数据倾斜问题。例如系统中只有两台服务器,其环分布如下,节点2只能负责⾮常⼩的⼀段,⼤量的客户端请求落在了节点1上,这就是数据(请求)倾斜问题
2)为了解决这种数据倾斜问题,⼀致性哈希算法引⼊了虚拟节点机制,即对每⼀个服务节点计算多个哈希,每个计算结果位置都放置⼀个此服务节点,称为虚拟节点。
具体做法可以在服务器ip或主机名的后⾯增加编号来实现。⽐如,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “节点1的ip#1”、“节点1的ip#2”、“节点1的ip#3”、“节点2的ip#1”、“节点2的ip#2”、“节点2的ip#3”的哈希值,于是形成六个虚拟节点,当客户端被路由到虚拟节点的时候其实是被路由到该虚拟节点所对应的真实节点
手写一致性Hash算法
Nginx配置一致性Hash负载均衡策略
ngx_http_upstream_consistent_hash
模块是⼀个负载均衡器,使⽤⼀个内部⼀致性hash算法来选择合适的后端节点。
该模块可以根据配置参数采取不同的⽅式将请求均匀映射到后端机器,
consistent_hash $remote_addr:可以根据客户端ip映射
consistent_hash $request_uri:根据客户端请求的uri映射
consistent_hash $args:根据客户端携带的参数进⾏映
ngx_http_upstream_consistent_hash
模块是⼀个第三⽅模块,需要我们下载安装后使⽤
1)github下载nginx⼀致性hash负载均衡模块 【https://github.com/replay/ngx_http_consistent_hash】 2)将下载的压缩包上传到nginx服务器,并解压
3)我们已经编译安装过nginx,此时进⼊当时nginx的源码⽬录,执⾏如下命令
./configure —add-module=/root/ngx_http_consistent_hash-master
make
make install
4)Nginx就可以使⽤啦,在nginx.conf⽂件中配置
集群时钟同步
时钟不同步导致的问题
时钟此处指服务器时间,如果集群中各个服务器时钟不⼀致势必导致⼀系列问题,试想 “集群是各个服务器⼀起团队化作战,⼤家⼯作都不在⼀个点上,岂不乱了套!”
举⼀个例⼦,电商⽹站业务中,新增⼀条订单,那么势必会在订单表中增加了⼀条记录,该条记录中应该会有“下单时间”这样的字段,往往我们会在程序中获取当前系统时间插⼊到数据库或者直接从数据库服务器获取时间。那我们的订单⼦系统是集群化部署,或者我们的数据库也是分库分表的集群化部署,然⽽他们的系统时钟缺不⼀致,⽐如有⼀台服务器的时间是昨天,那么这个时候下单时间就成了昨天,那我们的数据将会混乱!如下
集群时钟同步策略
集群时钟同步思路
- 分布式集群中各个服务器节点都可以访问互联网
思路:
操作⽅式:
#使⽤ ntpdate ⽹络时间同步命令
ntpdate -u ntp.api.bz #从⼀个时间服务器同步时间
windows有计划任务
Linux也有定时任务,crond,可以使⽤linux的定时任务,每隔10分钟执⾏⼀次ntpdate命令
- 分布式集群中某⼀个服务器节点可以访问互联⽹或者所有节点都不能够访问互联⽹
思路
操作⽅式:
- 1)选取集群中的⼀个服务器节点A(172.17.0.17)作为时间服务器(整个集群时间从这台服务器同步,如果这台服务器能够访问互联⽹,可以让这台服务器和⽹络时间保持同步,如果不能就⼿动设置⼀个时间)
- ⾸先设置好A的时间
- 把A配置为时间服务器(修改/etc/ntp.conf⽂件)
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
- 集群中其他节点就可以从A服务器同步时间了
ntpdate 172.17.0.17
分布式ID解决方案
为什么需要分布式ID(分布式集群环境下的全局唯⼀ID)
UUID
UUID 是指Universally Unique Identifier
,翻译为中⽂是通⽤唯⼀识别码产⽣重复 UUID 并造成错误的情况⾮常低,是故⼤可不必考虑此问题。Java中得到⼀个UUID,可以使⽤java.util包提供的⽅法
public class MyTest {
public static void main(String[] args) {
System.out.println(java.util.UUID.randomUUID().toString());
}
}
独⽴数据库的⾃增ID
⽐如A表分表为A1表和A2表,那么肯定不能让A1表和A2表的ID⾃增,那么ID怎么获取呢?我们可以单独的创建⼀个Mysql数据库,在这个数据库中创建⼀张表,这张表的ID设置为⾃增,其他地⽅需要全局唯⼀ID的时候,就模拟向这个Mysql数据库的这张表中模拟插⼊⼀条记录,此时ID会⾃增,然后我们可以通过Mysql的select last_insert_id() 获取到刚刚这张表中⾃增⽣成的ID.
⽐如,我们创建了⼀个数据库实例global_id_generator,在其中创建了⼀个数据表,表结构如下:
-- ----------------------------
-- Table structure for DISTRIBUTE_ID
-- ----------------------------
DROP TABLE IF EXISTS `DISTRIBUTE_ID`;
CREATE TABLE `DISTRIBUTE_ID` (
`id` bigint(32) NOT NULL AUTO_INCREMENT COMMENT '主键',
`createtime` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
当分布式集群环境中哪个应⽤需要获取⼀个全局唯⼀的分布式ID的时候,就可以使⽤代码连接这个数据库实例,执⾏如下sql语句即可。
insert into DISTRIBUTE_ID(createtime) values(NOW());
select LAST_INSERT_ID();
注意:
1)这⾥的createtime字段⽆实际意义,是为了随便插⼊⼀条数据以⾄于能够⾃增id。
2)使⽤独⽴的Mysql实例⽣成分布式id,虽然可⾏,但是性能和可靠性都不够好,因为你需要代码连接到数据库才能获取到id,性能⽆法保障,另外mysql数据库实例挂掉了,那么就⽆法获取分布式id了。
3)有⼀些开发者⼜针对上述的情况将⽤于⽣成分布式id的mysql数据库设计成了⼀个集群架构,那么其实这种⽅式现在基本不⽤,因为过于麻烦了。
SnowFlake 雪花算法(可以⽤,推荐)
雪花算法是Twitter推出的⼀个⽤于⽣成分布式ID的策略。雪花算法是⼀个算法,基于这个算法可以⽣成ID,⽣成的ID是⼀个long型,那么在Java中⼀个long型是8个字节,算下来是64bit,如下是使⽤雪花算法⽣成的⼀个ID的⼆进制形式示意:
另外,⼀切互联⽹公司也基于上述的⽅案封装了⼀些分布式ID⽣成器,⽐如滴滴的tinyid(基于数据库实现)、百度的uidgenerator(基于SnowFlake)和美团的leaf(基于数据库和SnowFlake)等,他们在。借助Redis的Incr命令获取全局唯⼀ID(推荐)
Redis Incr 命令将 key 中储存的数字值增⼀。如果 key 不存在,那么 key 的值会先被初始化为 0,然后再执⾏ INCR 操作
分布式调度问题
调度—>定时任务,分布式调度—>在分布式集群环境下定时任务这件事
Elastic-job(当当⽹开源的分布式调度框架)
定时任务的场景
定时任务形式:每隔⼀定时间/特定某⼀时刻执⾏
例如:
- 订单审核、出库
- 订单超时⾃动取消、⽀付退款
- 礼券同步、⽣成、发放作业
- 物流信息推送、抓取作业、退换货处理作业
- 数据积压监控、⽇志监控、服务可⽤性探测作业
- 定时备份数据
- ⾦融系统每天的定时结算
- 数据归档、清理作业
- 报表、离线数据分析作业
什么是分布式调度
什么是分布式任务调度?有两层含义
- 1)运⾏在分布式集群环境下的调度任务(同⼀个定时任务程序部署多份,只应该有⼀个定时任务在执⾏)
- 2)分布式调度—>定时任务的分布式—>定时任务的拆分(即为把⼀个⼤的作业任务拆分为多个⼩的作业任务,同时执⾏)
定时任务与消息队列的区别
- 共同点
- 异步处理
⽐如注册、下单事件 - 应⽤解耦
不管定时任务作业还是MQ都可以作为两个应⽤之间的⻮轮实现应⽤解耦,这个⻮轮可以中转数据,当然单体服务不需要考虑这些,服务拆分的时候往往都会考虑 - 流量削峰
双⼗⼀的时候,任务作业和MQ都可以⽤来扛流量,后端系统根据服务能⼒定时处理订单或者从MQ抓取订单抓取到⼀个订单到来事件的话触发处理,对于前端⽤户来说看到的结果是已经下单成功了,下单是不受任何影响的
- 本质不同
定时任务作业是时间驱动,⽽MQ是事件驱动;时间驱动是不可代替的,⽐如⾦融系统每⽇的利息结算,不是说利息来⼀条(利息到来事件)就算⼀下,⽽往往是通过定时任务批量计算;所以,定时任务作业更倾向于批处理,MQ倾向于逐条处理;
定时任务的实现方式
分布式调度框架Elastic-Job
Session共享问题
Session问题原因分析
出现这个问题的原因,从根本上来说是因为Http协议是⽆状态的协议。客户端和服务端在某次会话中产⽣的数据不会被保留下来,所以第⼆次请求服务端⽆法认识到你曾经来过, Http为什么要设计为⽆状态协议?早期都是静态⻚⾯⽆所谓有⽆状态,后来有动态的内容更丰富,就需要有状态,出现了两种⽤于保持Http状态的技术,那就是Cookie和Session。⽽出现上述不停让登录的问题,分析如下图:
场景:nginx默认轮询策略
解决Session一致性的方案
Nginx的 IP_Hash 策略(可以使⽤)
同⼀个客户端IP的请求都会被路由到同⼀个⽬标服务器,也叫做会话粘滞
- 优点:
配置简单,不⼊侵应⽤,不需要额外修改代码 - 缺点:
服务器重启Session丢失
存在单点负载⾼的⻛险
单点故障问题
Session复制(不推荐)
也即,多个tomcat之间通过修改配置⽂件,达到Session之间的复制
- 优点:
不⼊侵应⽤
便于服务器⽔平扩展
能适应各种负载均衡策略
服务器重启或者宕机不会造成Session丢失 - 缺点:
性能低
内存消耗
不能存储太多数据,否则数据越多越影响性能
延迟性
Session共享,Session集中存储(推荐)
Session的本质就是缓存,那Session数据为什么不交给专业的缓存中间件呢?⽐如Redis
- 优点:
能适应各种负载均衡策略
服务器重启或者宕机不会造成Session丢失
扩展能⼒强
适合⼤集群数量使⽤ - 缺点:
对应⽤有⼊侵,引⼊了和Redis的交互代码