分布式与集群场景

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;
      }
      
  • 分布式存储:如 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 值对应到环上的某个位置,然后顺时针找到最近的服务器节点。

    image-20201121230930956

  • 缩容时,原来路由到服务器3的客户端重新路由到服务器4,不影响其他客户端,避免大量迁移请求

    image-20201121230948027

  • 扩容时,原路由到服务器3的客户端重新路由到服务器5,不影响其他客户端,避免大量迁移请求

    image-20201121231653345

  • 虚拟节点机制

    采用一致性哈希算法,如果服务器节点太少时,节点分布不均,会造成某个节点处理大量的请求,产生数据倾斜问题,可以使用虚拟节点,哈希环上多个节点对应一个真实服务器节点

    image-20201121232345179

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

img

  • 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 的本质功能是存储和通知

  1. 引入jar

    <dependency>
    <groupId>com.dangdang</groupId>
    <artifactId>elastic-job-lite-core</artifactId>
    <version>2.1.5</version>
    </dependency>
    
  2. 编写定时任务类,实现com.dangdang.ddframe.job.api.simple.SimpleJob

  3. 配置分布式协调服务(注册中心)Zookeeper

    ZookeeperConfiguration zookeeperConfiguration = new ZookeeperConfiguration("localhost:2181","data-archive-job");
    CoordinatorRegistryCenter coordinatorRegistryCenter = new ZookeeperRegistryCenter(zookeeperConfiguration);
    coordinatorRegistryCenter.init();
    
  4. 配置任务:时间事件(包括任务分片)、定时任务业务逻辑、调度器

    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 用法

  1. 引入 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>
    
  2. 配置 Redis

    spring.redis.database=0
    spring.redis.host=127.0.0.1
    spring.redis.port=6379
    
  3. 添加注解 @EnableRedisHttpSession 到启动类

5.4 Spring Session 源码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

火车站卖橘子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值