1 分布式和集群

  分布式:把一个系统拆分为多个子系统,每个子系统负责各自的那部分功能,独立部署,各司其职。

  集群:多个实例共同工作,最简单的集群是把一个应用复制多份部署。

  分布式一定是集群,但是集群不一定是分布式(集群就是多个实例一起工作,分布式将一个系统拆分之后那就是多个实例;集群并不一定是分布式,因为复制型的集群不是拆分而是复制)。

 

2 一致性hash算法

  首先了解下为什么使用hash?hash算法较多的应用在数据存储和查找领域,最经典的是hash表,查询效率非常之高,数据查询时间复杂度可以接近0(1)。

2.1 hash算法在分布式集群中的应用场景

  hash算法在很多分布式集群产品中都有应用,比如分布式集群架构Redis、Hadoop、elasticSearch,MySQL分库分表、Nginx负载均衡等。

  主要的应用场景:

  1)请求的负载均衡(比如Nginx的ip_hash策略)

  Nginx的hash策略可以在客户端IP不变的情况下,将其发出的请求始终路由到同一个目标服务器上,实现回话粘滞,避免处理session共享问题。

  如果没有ip_hash策略,如何实现回话粘滞?可以维护一个映射表,存储客户端IP或者sessionid与具体目标服务器的映射关系。但是在客户端很多的情况下,映射表非常大,浪费内存空间,另一方面,客户端上下线,目标服务器上下线都会导致重新维护映射表,映射表维护成本很大。

  使用哈希算法,可以对IP地址或者sessionid进行计算哈希值,哈希值与服务器数量进行取模运算,得到的值就是当前请求应该被路由到的服务器编号,如此,同一个客户端IP发送过来的请求就可以路由到同一个目标服务器,实现回话粘滞。

  2)分布式存储

  以分布式内存数据库Redis为例,集群中有redis1,redis2,redis3三台Redis服务器,进行数据存储时存储到那台服务器呢,针对key进行hash处理hash(key1)% 3 = index,使用余数index锁定存储的具体服务器节点。

2.2 普通hash算法存在的问题

  以ip_hash为例,假定下载用户IP固定没有发生改变,其中一个tomcat出现了问题,宕机了,服务器数量减少,之前的求模都需要重新计算。

  在后台服务器很多台,客户端也有很多,那么影响非常大,缩容和扩容都会存在这样的问题,大量用户的请求会被路由到其他的目标服务器处理,用户在原来服务器中的回话都会丢失。

2.3 一致性hash算法

  首先有一条直线,直线开头和结分别定位1和2的32次方减一,相当于一个地址,对于这样一条线,弯过来构成一个圆环形成闭环,这样的一个圆环称为hash环,把服务器IP或者主机名求hash值然后对应到hash环上,针对客户端用户,可以根据它的IP进行hash求值,对应到某个位置,然后如何确定一个客户端路由到哪个服务器处理呢?按照顺时针方向找最近的服务器节点。

 

 

假如将服务器3下线,原来路由到3的客户端重新路由到4,对于其他客户端没有影响,只是小部分受影响(请求的迁移达到了最小,对于分布式集群来说非常合适,避免了大量请求迁移)。

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

但是容易因为节点分布不均造成数据倾斜问题,例如有两台服务器,可能一个节点负责小的一段,大量的客户端请求落在了另一个节点,这就是数据(请求)倾斜问题。

解决数据倾斜问题,引入虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。

具体做法可以在服务器IP或主机名的后面增加编号来实现,比如,可以为每台服务器计算三个虚拟节点,于是可以分别计算节点1的IP#1、节点1的IP#2,节点1的IP#3等等,于是形成六个虚拟节点,当客户端被路由到虚拟节点的时候其实是被路由到该虚拟节点所对应的真实节点。

 如下面代码为简单的模拟含虚拟节点的一致性hash算法实现。

//1.初始化:把服务器节点IP的哈希值对应到哈希环上
        //定义服务器ip
        String[] tomcatServers = new String[] {"123.111.0.0","123.101.3.1","111.20.35.2","123.98.26.3"};

        SortedMap<Integer,String> hashServerMap = new TreeMap<>();

        //定义针对每个真实服务器虚拟出来几个节点
        int virtaulCount = 3;

        for (String tomcatServer : tomcatServers) {
            //求出每一个ip的hash值,对应到hash环上存储hash值和ip的对应关系
            int serverHash = Math.abs(tomcatServer.hashCode());
            //存储hash值与ip的对应关系
            hashServerMap.put(serverHash, tomcatServer);
            //处理虚拟节点
            for (int i = 0; i < virtaulCount; i++) {
                int virtualHash = Math.abs((tomcatServer + "#" + i).hashCode());
                hashServerMap.put(virtualHash, "---由虚拟节点" + i + "映射过来的请求" + tomcatServer);
            }
        }
        //2.针对客户端ip求出hash值
        //定义客户端ip
        String[] clients = new String[] {"10.78.12.3","113.25.63.1","126.12.3.8"};
        for (String client : clients) {
            int clientHash = Math.abs(client.hashCode());
            //3.针对客户端,找到能够处理当前客户端请求的服务器(哈希环上顺时针最近)
            SortedMap<Integer, String> integerStringSortedMap = hashServerMap.tailMap(clientHash);
            if (integerStringSortedMap.isEmpty()) {
                //取哈希环上的顺时针第一台服务器
                Integer firstkey = hashServerMap.firstKey();
                System.out.println("------>>>客户端:" + client + "被路由到服务器" + hashServerMap.get(firstkey));
            } else {
                Integer firstkey = integerStringSortedMap.firstKey();
                System.out.println("------>>>客户端:" + client + "被路由到服务器" + hashServerMap.get(firstkey));
            }
        }

 

3 时钟不同导致的问题

时钟此处指服务器时间,如果集群中各个服务器时钟不一致势必导致一系列问题,举例电商网站业务中,新增一条订单,那么势必会在订单表中增加一条记录,该条记录中应该会有“下单时间”这样的字段,往往我们会在程序中获取当前系统时间插入到数据库或者直接从数据库服务器获取时间,订单子系统是集群化部署,或者数据库也是分库分表的集群化部署,如果系统时钟不一致,那下单时间就不会准确,数据就会混乱。

3.1 集群时钟同步配置

      1)分布式集群中各个服务器 节点都可以连接互联网

  #使用ntpdate网络时间同步命令

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

  Linux可以使用定时任务crond,每隔10分钟执行一次ntpdate命令。

  2)分布式集群中某一个服务器节点可以访问互联网或者所有节点都不能够访问互联网

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

  首先设置A的时间,把A(172.17.0.17)设置为时间服务器(修改/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解决方案

分布式id:分布式集群环境下的全局唯一id。

  1)UUID

  UUID是指Universally Unique Identifier(通用唯一识别码)

public static void main(String[] args) {
        //b4b7a44a-f855-4e59-8e3c-84321e9c9b37s
        System.out.println(UUID.randomUUID().toString());
    }

  2)独立数据库的自增id

  创建数据库实例global_id_generator,创建了数据库表,表结构如下:

  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();

注意:createtime字段无实际意义,这种方式性能和可靠性都不够好,需要代码连接到数据库才能获取到id,性能无法保障,如果数据库挂掉了,就无法获取分布式id了。

  3)SnowFlake雪花算法

  是Twitter推出的一个用于生分布式id的策略,生成的id是long型,8个字节,有64bit。

  其中符号位固定为0,二进制表示最高位是符号位,时间戳:41个二进制数用来记录时间戳,表示某一个毫秒,机器id:代表当前算法运行机器的id,序列化:12位用来记录某个机器一个毫秒内产生的不同序列号,代表同一个机器同一毫秒可以产生的id序号。

  4)借助Redis的incr命令获取全局唯一id

  将key中储存的数字值增一,如果key不存在,那么key的值会被初始化为0,然后再执行incr操作。

5 session共享问题

  session共享及session保存或者叫做session一致性。会有什么问题呢?http协议是无状态的协议,客户端和服务器在某次回话中产生的数据不会被保留下来,所以第二次请求服务器无法认识到曾经来过。

  5.1 解决session一致性的方案

  1)nginx的ip_hash策略

  同一个客户端ip的请求会被路由到同一个目标服务器,优点:配置简单,不入侵应用,不需要额外修改代码。缺点:服务器重启session丢失,存在单点负载高的风险,单点故障问题。

  2)session复制

  多个tomcat之间通过修改配置文件,达到session之间的复制。优点:不入侵应用,便于服务器水平扩展,能适应各种负载均衡策略,服务器重启或者宕机不会造成session丢失。缺点:性能低,内存消耗,不能存储太多数据,数据越多越影响性能,延迟性。

  3)session共享,session集中存储

  交由Redis,优点:适应各种负载均衡策略,服务器重启或者宕机不会造成session丢失,扩展能力强,适合大集群数量使用,缺点:对应用入侵,引入和Redis交互代码。

 

 

 

 

 

 

  

 

posted on 2021-07-25 00:47  jeolyli  阅读(721)  评论(0编辑  收藏  举报