分布式集群架构场景化解决⽅案

·  阅读 776

一、一致性Hash算法

  • 简单介绍下Hash算法,⽐如说在安全加密领域MD5、SHA等加密算法,在数据存储和查找⽅⾯有Hash表等, 以上 都应⽤到了Hash算法。

  • 查找效率(时间&空间,时间优先) : 顺序查找法(for遍历) < ⼆分查找(折半查找) < 直接寻址法(数组) < 开放寻址法(简单hash) < 拉链法(hash+链表)。

  • 如果Hash算法设计的⽐较好的话,那么查询效率会更接近于O(1),如果Hash算法设计的⽐较low,那么 查询效率就会很低了 (hash冲突造成链表过长

1. Hash算法应⽤场景

Hash算法在很多分布式集群产品中都有应⽤,⽐如分布式集群架构Redis、Hadoop、ElasticSearch, Mysql分库分表,Nginx负载均衡等

主要的应⽤场景归纳起来两个:

  • 请求的负载均衡(⽐如nginx的ip_hash策略)

    • 如果没有IP_hash策略,那么如何实现会话粘滞?可以维护⼀张映射表,存储客户端IP或者sessionid与具体⽬标服务器的映射关系<ip,tomcat1>。不过缺点明显 ,用户多浪费空间,需要维护,成本大。
    • 使用hash 算法实现得以解决。
  • 分布式存储

    例子 :集群中有redis1,redis2,redis3 三台Redis服务器,查找时候使用hash(key1)%3=index,用余数定位服务器。(mysql分表同理)

2. 普通Hash存在的问题

以ip_hash为例,如果一个服务宕机了,需要重新求模重新计算。服务器多了,缩容和扩容都会出现问题,比如⽤户在原来服务器中的会话都会丢失。

3. ⼀致性Hash算法

3.1 设计思路

思路如下:

1->2的32次⽅减1,把它想成一个闭环(hash环)我们把服务器的ip或者主机名求 hash值然后对应到hash环上,那么针对客户端⽤户,也根据它的ip进⾏hash求值,对应到环上某个位 置,然后如何确定⼀个客户端路由到哪个服务器处理呢?按照顺时针⽅向找最近的服务器节点。(简单理解为双方同时hash,找最近的节点)

假如 3下线了,原来打到3的请求重新路由到4,对其他客户端没有影响。避免大量请求迁移,如下图:

假如 增加了服务器5,原来到3 的路由到5,也是迁移了一小部分,避免了大量请求迁移。如下图:

综上,,每⼀台服务器负责⼀段,⼀致性哈希算法对于节点的增减都只需重定位环空间中的⼀⼩ 部分数据,具有较好的容错性和可扩展性。

3.2 数据倾斜问题

但是节点太少的情况下,会产生数据倾斜问题。例如只有2台服务器,根据一致性hash,大量请求会打到节点1,节点2 负责的非常少。这就是数据倾斜问题

3.3 虚拟节点解决方案

此时,虚拟节点解决方案就来了:将每一个节点计算多个hash,每个hash计算结果都设置成当前节点,成为虚拟节点

具体做法:在服务器ip后面增加编号来实现,比如,可以为每台服务器计算三个虚拟节点, 如下图:

4. 手写一致性hash 帮助理解

github地址-> github.com/znbsmd/hand…

5. 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模块是⼀个第三⽅模块,需要下载安装后使⽤->github.com/replay/ngx_… 使用步骤如下:

  • 解压到nginx 源码目录
  • ./configure —add-module=/root/ngx_http_consistent_hash-master
  • make
  • make install
  • 在nginx.conf⽂件中配置
    upstream zjnServer {
        consistent_hash $request_uri;// 根据客户端请求的uri映射
        server 127.0.0.1:8080;
        server 127.0.0.1:8081;
    }
    复制代码

二、集群时钟同步问题

1. 时钟不同步导致的问题

下单分发到不同的服务器,由于创建时间都是取系统时间,由于时间不同,导致数据错乱。

2. 集群时钟同步配置

  • 集群时钟同步思路
    • 分布式集群中各个服务器节点都可以连接互联⽹

      #使⽤ ntpdate ⽹络时间同步命令
      ntpdate -u ntp.api.bz #从⼀个时间服务器同步时间
      复制代码

      最后使用crontab 定时任务去同步

    • 分布式集群中某⼀个服务器节点可以访问互联⽹或者所有节点都不能够访问互联⽹

      选node1 作为主时间服务器,(如果这台服务器能够访问互联⽹,可以让这台服务器和⽹络时间保持同步,如果不能就⼿动设置⼀个时间)

      • 设置好node1 时间
      • 把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不能重复

  • UUID(可以⽤)
    public class MyTest {
        public static void main(String[] args) {
             System.out.println(java.util.UUID.randomUUID().toString());
        }
    }
    复制代码
  • 独⽴数据库的⾃增ID,设置单独的数据库,在插入前优先插入,单独维护(不推荐)
  • SnowFlake 雪花算法(可以⽤,推荐)
  • 雪花算法是⼀个算法,基于这个算法可以⽣成ID,⽣成的ID是⼀个long型,那么在Java中⼀个long 型是8个字节,算下来是64bit,如下是使⽤雪花算法⽣成的⼀个ID的⼆进制形式示意:

⼀些互联⽹公司也基于上述的⽅案封装了⼀些分布式ID⽣成器,⽐如滴滴的tinyid(基于数据库实现)、百度的uidgenerator(基于SnowFlake)和美团的leaf(基于数据库和SnowFlake)等

  • redis.incr (推荐)

    • 引入jedis jar
      <dependency>
          <groupId>redis.clients</groupId>
          <artifactId>jedis</artifactId>
          <version>2.9.0</version>
      </dependency>
      复制代码
    • Jedis jedis = new Jedis("127.0.0.1",6379);
          try {
              long id = jedis.incr("id");
              System.out.println("从redis中获取的分布式id为:" + id);
              } 
          finally {
              if (null != jedis) {
              jedis.close();
          }
      }
      复制代码

四、分布式调度问题

1. 分布式调度有两层含义

  • 同一个定时任务部署多份,只有一个运行
  • 定时任务拆分,同时执行

2. 定时任务与消息队列的区别

本质不同,定时任务是时间驱动,⽽MQ是事件驱动;定时任务作业更倾向于批处理,MQ倾向于逐条处理;

3. 定时任务的实现⽅式

任务调度框架Quartz

4. 分布式调度框架Elastic-Job

Elastic-Job是当当⽹开源的⼀个分布式调度解决⽅案,基于Quartz⼆次开发的,由两个相互独⽴的⼦项 ⽬Elastic-Job-Lite和Elastic-Job-Cloud组成。地址-> :github.com/elasticjob

主要功能:

  • 分布式避免同一任务重复执行
  • 基于cron表达式
  • 弹性扩容,增加减少,能转移到别的实例执行
  • 失效转移,执行失败后,转移其他实例执行
  • 错过执⾏作业重触发 若因某种原因导致作业错过执⾏,⾃动记录错过执⾏的作业,并在上次作业完成后⾃动触发。
  • 支持并行调度 支持任务分片,任务分⽚是指将⼀个任务分为多个⼩任务项在多个实例同时执⾏。
  • 作业分⽚⼀致性 当任务被分⽚后,保证同⼀分⽚在分布式环境中仅⼀个执⾏实例。

4.1 Elastic-Job-Lite应⽤

jar包(API) + 安装zk软件

Elastic-Job依赖于Zookeeper进⾏分布式协调,所以需要安装Zookeeper软件(3.4.6版本以上)

  • 安装配置zk

  • 引入jar

    <dependency>
        <groupId>com.dangdang</groupId>
        <artifactId>elastic-job-lite-core</artifactId>
        <version>2.1.5</version>
    </dependency>
    复制代码
  • 定时任务实例...

  • Leader节点选举机制 每个Elastic-Job的任务执⾏实例App作为Zookeeper的客户端来操作ZooKeeper的znode

    • 多个实例同时创建/leader节点
    • /leader节点只能创建⼀个,后创建的会失败,创建成功的实例会被选为leader节点,执⾏任务

4.2 Elastic-Job-Lite轻量级去中⼼化的特点

4.2 任务分⽚

ElasticJob可以把作业分为多个的task(每⼀个task就是⼀个任务分⽚),每 ⼀个task交给具体的⼀个机器实例去处理(⼀个机器实例是可以处理多个task的),但是具体每个task 执⾏什么逻辑由我们⾃⼰来指定。

Strategy策略定义这些分⽚项怎么去分配到各个机器上去,默认是平均分配,可以定制,⽐如某⼀个机 器负载 ⽐较⾼或者预配置⽐较⾼,那么就可以写策略。分⽚和作业本身是通过⼀个注册中⼼协调的,因 为在分布式环境下,状态数据肯定集中到⼀点,才可以在分布式中沟通。

4.3 弹性扩容

新增加⼀个运⾏实例app3,它会⾃动注册到注册中⼼,注册中⼼发现新的服务上线,注册中⼼会通知 ElasticJob 进⾏重新分⽚,那么总得分⽚项有多少,那么就可以搞多少个实例机器,⽐如完全可以分1000⽚ 最多就可以有多少app实例,完全可以分1000⽚,那么就可以搞1000台机器⼀起执⾏作业

注意:

  1. 分⽚项也是⼀个JOB配置,修改配置,重新分⽚,在下⼀次定时运⾏之前会重新调⽤分⽚算法,那么 这个分⽚算法的结果就是:哪台机器运⾏哪⼀个⼀⽚,这个结果存储到zk中的,主节点会把分⽚给分好 放到注册中⼼去,然后执⾏节点从注册中⼼获取信息(执⾏节点在定时任务开启的时候获取相应的分⽚)。
  2. 如果所有的节点挂掉值剩下⼀个节点,所有分⽚都会指向剩下的⼀个节点,这也是ElasticJob的⾼可⽤。

五、Session共享问题

1. Session问题原因分析

因为Http协议是⽆状态,Http为什么要设计为⽆状态协议?早期都是静态⻚⾯⽆所谓有⽆状态,后来有动态的内容更丰富,就需要有状态,出现了两种⽤于保持Http状态的技术,那就是Cookie和Session。

使用nginx默认轮询策略,会出现Session打到不同的服务器问题。

2. 解决Session⼀致性的⽅案

  • Nginx的 IP_Hash 策略(可以使⽤)

    • 优点:配置简单,不⼊侵应⽤,不需要额外修改代码
    • 缺点:
      • 服务器重启Session丢失
      • 存在单点负载⾼的⻛险(恶意攻击)
      • 单点故障问题(重新hash,导致会话丢失)
  • Session复制(不推荐)

    • 大数据时代缺点一堆,就不细说了
  • redis,Session共享,Session集中存储(推荐)

    • 唯一缺点:对应⽤有⼊侵,引⼊了和Redis的交互代码(有专门jar包,暂无缺点)
    • 引入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
    复制代码
    • 主run方法添加注解
    @EnableRedisHttpSession
    复制代码
    • 原理:主要是在过滤器进行请求拦截

《lagouedu》分布式解决方案总结

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改