分布式架构场景化解决方案

1. 分布式和集群的区别

分布式是把一个系统拆分成多个子系统,每个系统负责各自的那部分功能,独立部署各司其职。分布式⼀定是集群,但是集群不⼀定是分布式(因为集群就是多个实例⼀起⼯作,分布式将⼀个系统拆分之后那就是多个实例;集群并不⼀定是分布式,因为复制型的集群不是拆分⽽是复制)

2.⼀致性Hash算法

Hash算法较多的应⽤在数据存储和查找领域,最经典的就是Hash表,它的查询效率⾮常之⾼,其中的哈希算法如果设计的⽐较ok的话,那么Hash表的数据查询时间复杂度可以接近于O(1),示例:

需求:提供⼀组数据 1,5,7,6,3,4,8,对这组数据进⾏存储,然后随便给定⼀个数n,请你判断n是否存在于刚才的数据集中?

1.顺序查找法 :这种⽅式我们是通过循环来完成,⽐较原始,效率也不⾼

List:List[1,5,7,6,3,4,8]
// 通过循环判断来实现
for(int element: list) {
    if(element == n) {
        //如果相等,说明n存在于数据集中
    }
}

2.⼆分查找:排序之后折半查找,相对于顺序查找法会提⾼⼀些效率,但是效率也并不是特别好.

3.直接寻址法:定义⼀个数组,数组⻓度⼤于等于数据集⻓度,此处⻓度为9,数据1就存储在下标为1的位置,3就存储在下标为3的元素位置,,,依次类推。优点:速度快,⼀次查找得到结果   缺点:数据大会浪费空间。

4.现在,换⼀种设计,如果数据是3,5,7,12306,⼀共4个数据,我们开辟任意个空间,⽐如5个,我们可以对数据进⾏取模(对空间位置数5),根据取模余数确定存储位置的下标,⽐如3%5=3,就可以把3这个数据放到下标为3的位置上,12306%5=1,就把12306这个数据存储到下标为1的位置上。这就是⼀个hash算法,只不过这是⼀种⽐较普通⼜简单的hash算法,这种构造Hash算法的⽅式叫做除留余数法。

5. 拉链法:数据⻓度定义好了,怎么存储更多内容呢,算好Hash值,在数组元素存储位置放了⼀个链表。如果Hash算法设计的⽐较好的话,那么查询效率会更接近于O(1),如果Hash算法设计的⽐较low,那么

查询效率就会很低了。

2.1 Hash算法应⽤场景

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

请求的负载均衡(⽐如nginx的ip_hash策略): 对ip地址或者sessionid进⾏计算哈希值,哈希值与服务器数量进⾏取模运算,得到的值就是当前请求应该被路由到的服务器编号,如此,同⼀个客户端ip发送过来的请求就可以路由到同⼀个⽬标服务器,实现会话粘滞。

分布式存储:以分布式内存数据库Redis为例,集群中有redis1,redis2,redis3 三台Redis服务器。那么在进⾏数据存储时<key1,value1>数据存储到哪个服务器当中呢?针对key进⾏hash处理hash(key1)%3=index, 使⽤余数index锁定存储的具体服务器节点。

2.2 ⼀致性Hash算法

普通Hash算法存在⼀个问题,以ip_hash为例,假定下载⽤户ip固定没有发⽣改变,现在tomcat3出现了问题,down机了,服务器数量由3个变为了2个,之前所有的求模都需要重新计算。如此,⼤量⽤户的请求会被路由到其他的⽬标服务器处理,⽤户在原来服务器中的会话都会丢失。

⼀致性哈希算法思路如下:⾸先有⼀条直线,直线开头和结尾分别定为为1和2的32次⽅减1,这相当于⼀个地址。弯过来构成⼀个圆环形成一个hash环。我们把服务器的ip或者主机名求hash值然后对应到hash环上,那么针对客户端⽤户,也根据它的ip进⾏hash求值,对应到环上某个位置,按照顺时针⽅向找最近的服务器节点。

 

但是,⼀致性哈希算法在服务节点太少时,容易因为节点分部不均匀⽽造成数据倾斜问题。例如系统中只有两台服务器,其环分布如下,节点2只能负责⾮常⼩的⼀段,⼤量的客户端请求落在了节点1上,这就是数据(请求)倾斜问题。为了解决这种数据倾斜问题,⼀致性哈希算法引⼊了虚拟节点机制,即对每⼀个服务节点计算多个哈希,每个计算结果位置都放置⼀个此服务节点,称为虚拟节点。

 2.3 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⽂件中配置

3. 集群时钟同步问题

都可以访问互联网时:

#使⽤ ntpdate ⽹络时间同步命令

ntpdate -u ntp.api.bz #从⼀个时间服务器同步时间

如果不能访问互联网:

选取集群中的⼀个服务器节点A(172.17.0.17)作为时间服务器(整个集群时间从这台服务器同步,如果这台服务器能够访问互联⽹,可以让这台服务器和⽹络时间保持同步,如果不能就⼿动设置⼀个时间)。

把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

4. 分布式ID解决⽅案

方案一:使⽤java.util包提供的⽅法创建UUID,UUID 产⽣重复并造成错误的情况⾮常低,是故⼤可不必考虑此问题。

public class MyTest {
    public static void main(String[] args) {
        System.out.println(java.util.UUID.randomUUID().toString());
    }
}

方案二:SnowFlake 雪花算法

雪花算法是⼀个算法,基于这个算法可以⽣成ID,⽣成的ID是⼀个long型,那么在Java中⼀个long型是8个字节,算下来是64bit,如下是使⽤雪花算法⽣成的⼀个ID的⼆进制形式示意:

方案三:借助Redis的Incr命令获取全局唯⼀ID

Redis Incr 命令将 key 中储存的数字值增⼀。如果 key 不存在,那么 key 的值会先被初始化为 0,然后再执⾏ INCR 操作。

Redis安装

1)上传到linux服务器解压 tar -zxvf redis-3.2.10.tar.gz    2)cd 解压⽂件⽬录,对解压的redis进⾏编译执行命令:make

3)然后cd 进⼊src⽬录,执⾏make install  4) 修改解压⽬录中的配置⽂件redis.conf,关掉保护模式。在src⽬录下执⾏ ./redis-server ../redis.conf 启动redis服务。

Java代码中使⽤Jedis客户端调⽤Reids的incr命令获得⼀个全局的id。

引⼊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();
  }
}

4. 分布式调度问题

什么是分布式任务调度?有两层含义。1)运⾏在分布式集群环境下的调度任务(同⼀个定时任务程序部署多份,只应该有⼀个定时任务在执⾏)

2)分布式调度—>定时任务的分布式—>定时任务的拆分(即为把⼀个⼤的作业任务拆分为多个⼩的作业任务,同时执⾏)

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

共同点:异步处理,应⽤解耦(不管定时任务作业还是MQ都可以作为两个应⽤之间的⻮轮实现应⽤解耦,这个⻮轮可以中转数据,当然单体服务不需要考虑这些,服务拆分的时候往往都会考虑),

流量削峰(双⼗⼀的时候,任务作业和MQ都可以⽤来扛流量,后端系统根据服务能⼒定时处理订单或者从MQ抓取订单抓取到⼀个订单到来事件的话触发处理,对于前端⽤户来说看到的结果是已经下单成功了,下单是不受任何影响的)

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

4.1 分布式调度框架Elastic-Job

功能及介绍:在分布式环境中,任务能够按指定的调度策略执⾏,并且能够避免同⼀任务多实例重复执⾏。丰富的调度策略 基于成熟的定时任务作业框架Quartz cron表达式执⾏定时任务。弹性扩容缩容 当集群中增加某⼀个实例,它应当也能够被选举并执⾏任务;当集群减少⼀个实例时,它所执⾏的任务能被转移到别的实例来执⾏。某实例在任务执⾏失败后,会被转移到其他实例执⾏。错过执⾏作业重触发 若因某种原因导致作业错过执⾏,⾃动记录错过执⾏的作业,并在上次作业完成后⾃动触发。⽀持并⾏调度 ⽀持任务分⽚,任务分⽚是指将⼀个任务分为多个⼩任务项在多个实例同时执⾏。作业分⽚⼀致性 当任务被分⽚后,保证同⼀分⽚在分布式环境中仅⼀个执⾏实例。

Elastic-Job-Lite应⽤:jar包(API) + 安装zk软件。

安装zookeeper:1)在linux平台解压下载的zookeeper-3.4.10.tar.gz  2)进⼊conf⽬录,cp zoo_sample.cfg zoo.cfg  3)进⼊bin⽬录,启动zk服务。启动 ./zkServer.sh start  停⽌ ./zkServer.sh stop  查看状态 ./zkServer.sh status。

代码实现:

引入jar包

<!-- 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>

定时任务实例:需求:每隔两秒钟执⾏⼀次定时任务(resume表中未归档的数据归档到resume_bak表中,每次归档1条记录)

1)resume_bak和resume表结构完全⼀样

2)resume表中数据归档之后不删除,只将state置为"已归档"

DROP TABLE IF EXISTS `resume`;

CREATE TABLE `resume` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `sex` varchar(255) DEFAULT NULL,
  `phone` varchar(255) DEFAULT NULL,
  `address` varchar(255) DEFAULT NULL,
  `education` varchar(255) DEFAULT NULL,
  `state` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS
= 1;

定时任务类

public class BackupJob implements SimpleJob {
  // 定时任务每执⾏⼀次都会执⾏如下的逻辑
  @Override
  public void execute(ShardingContext shardingContext) {
    /*从resume数据表查找1条未归档的数据,将其归档到resume_bak表,并更新状态为已归档(不删除原数据)*/
    // 查询出⼀条数据
    String selectSql = "select * from resume where state='未归档' limit 1";
    List<Map<String, Object>> list = JdbcUtil.executeQuery(selectSql);
    if(list == null || list.size() == 0) {
      return;
    }

    Map<String, Object> stringObjectMap = list.get(0);
    long id = (long) stringObjectMap.get("id");
    String name = (String) stringObjectMap.get("name");
    String education = (String)
    stringObjectMap.get("education");
    // 打印出这条记录
    System.out.println("======>>>id:" + id + " name:" + name + " education:" + education);
    // 更改状态
    String updateSql = "update resume set state='已归档' where id=?";
    JdbcUtil.executeUpdate(updateSql,id);
    // 归档这条记录
    String insertSql = "insert into resume_bak select *from resume where id=?";
    JdbcUtil.executeUpdate(insertSql,id);
  }
}

主类

public class ElasticJobMain {
  public static void main(String[] args) {
    // 配置注册中⼼zookeeper,zookeeper协调调度,不能让任务重复执⾏,
    ZookeeperConfiguration zookeeperConfiguration = new ZookeeperConfiguration("localhost:2181","data-archive-job");
    CoordinatorRegistryCenter coordinatorRegistryCenter = new ZookeeperRegistryCenter(zookeeperConfiguration);
    coordinatorRegistryCenter.init();

    // 配置任务
    JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration.newBuilder("archive-job","*/2 * * * *?",1).build();
    SimpleJobConfiguration simpleJobConfiguration = new SimpleJobConfiguration(jobCoreConfiguration,BackupJob.class.getName());
    // 启动任务
    new JobScheduler(coordinatorRegistryCenter, LiteJobConfiguration.newBuilder(simpleJobConfiguration).build()).init();
  }
}

 

测试:1)可先启动⼀个进程,然后再启动⼀个进程(两个进程模拟分布式环境下,通⼀个定时任务部署了两份在⼯作)

     2)两个进程逐个启动,观察现象

   3)关闭其中执⾏的进程,观察现象

任务分片:⼀个⼤的⾮常耗时的作业Job,⽐如:⼀次要处理⼀亿的数据,那这⼀亿的数据存储在数据库中,如果⽤⼀个作业节点处理⼀亿数据要很久,在互联⽹领域是不太能接受的,互联⽹领域更希望机器的增加去

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

 

 

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

2)如果所有的节点挂掉值剩下⼀个节点,所有分⽚都会指向剩下的⼀个节点,这也是ElasticJob的⾼可⽤。

5. 解决Session⼀致性的⽅案

方案一:Nginx的 IP_Hash 策略(可以使⽤)

同⼀个客户端IP的请求都会被路由到同⼀个⽬标服务器,也叫做会话粘滞。优点:配置简单,不⼊侵应⽤,不需要额外修改代码。

缺点:服务器重启Session丢失,存在单点负载⾼的⻛险,单点故障问题。

方案二:使用Redis集中存储session。优点:能适应各种负载均衡策略,服务器重启或者宕机不会造成Session丢失,扩展能⼒强,适合⼤集群数量使⽤。缺点:对应⽤有⼊侵,引⼊了和Redis的交互代码。

Spring Session使得基于Redis的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)添加注解 

 

posted on 2020-10-12 17:34  lvguoliang(学无止境)  阅读(456)  评论(0编辑  收藏  举报