浅谈集群的分类

本文主要介绍集群部署相关的知识,介绍集群部署的基础,集群的分类、集群的负载均衡技术,集群的可用性以及集群的容错机制。随后介绍Redis-Cluster以及Mysql的架构以及主从复制原理。

集群介绍

单台服务器本身会受到带宽、内存、处理器等多方面因素的影响,当通过垂直扩展已经无法提升系统性能时,我们就需要通过水平扩展,即以集群的方 式来部署服务,从而提升系统整体性能。集群在提升性能的同时,也可以减轻单机器的压力,同时提高系统的可用性。这就是我们常说的加机器,简单粗暴,但非常有效。

提到集群,一定会想到分布式,不过要注意这两者的区别。集群强调的是服务的多个副本冗余部署;分布式强调的是将整个系统拆分成多个子系统独立开发、维护和运行,每个子系统仍然以集群的方式部署。Redis-Cluster就是典型的分布式系统,每个节点采用一主多从的集群方式部署。

Redis-Cliuster架构图

对于集群的分类,网上说最多的就是分成高性能集群、高可用集群、负载均衡集群,但我却不敢苟同。我认为三者之间是分不开的,你中有我,我中有你,比如通过负载均衡就可以实现高性能、高可用,或者说负载均衡是手段,高性能高可用是结果。所以我更倾向于将集群分成两种:即无状态服务的水平扩展集群和有状态的主从集群。

无状态服务的水平扩展集群

该方式实现的前提是服务本身是无状态的,举例来说,如果服务是一个Web应用,访问界面需要用户登录态,如果session是保留在本地服务器上,那么这个服务就是有状态的。如果用户的多次请求打到了不同的机器上,就会出现重复登录的问题。但如果session存储在中间件中,比如Redis,那么就是无状态的。

通过机器的水平扩容,就构建了服务的集群,随后通过负载均衡的算法,将请求分散到不同的机器上。

负载均衡几个比较重要的指标:负载均衡算法、容错机制以及健康检测。

负载均衡算法

1、随机算法

思想:服务集群中的每个服务都配置一个权重weight,默认是100。如果所有权重相等,则随机选择一个;如果权重不等,即为加权随机算法。比如现在有两个服务A,B,权重分别是100,200,那么选取比例就是 1/3,2/3。

Dubbo实现算法:

复制代码
@Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        // Number of invokers
        int length = invokers.size();
        // Every invoker has the same weight?
        boolean sameWeight = true;
        // the weight of every invokers
        int[] weights = new int[length];
        // the first invoker's weight
        int firstWeight = getWeight(invokers.get(0), invocation);
        weights[0] = firstWeight;
        // The sum of weights
        int totalWeight = firstWeight;
        for (int i = 1; i < length; i++) {
            int weight = getWeight(invokers.get(i), invocation);
            // save for later use
            weights[i] = weight;
            // Sum
            totalWeight += weight;
            if (sameWeight && weight != firstWeight) {
                sameWeight = false;
            }
        }
        if (totalWeight > 0 && !sameWeight) {
          随机选取小于总权重的整数
            int offset = ThreadLocalRandom.current().nextInt(totalWeight);
            //这个是关键,如果减去总权重是负数,即为选中的Invoker
            for (int i = 0; i < length; i++) {
                offset -= weights[i];
                if (offset < 0) {
                    return invokers.get(i);
                }
            }
        }
      //完全随机
        return invokers.get(ThreadLocalRandom.current().nextInt(length));
    }

2、RoundRobin轮询

最简单的轮询就是依次轮询,比如有N台服务器,编号从0到N-1依次轮询执行,没有任何的复杂算法。

下面是一个简单的实现:

复制代码
public class RoundRobinBalance {

    public static final List<Integer> selectList = Lists.newArrayList(1,2,3);

    private int getWeight(Integer integer){
        return integer;
    }

    private AtomicInteger atomicInteger = new AtomicInteger(-1);

    public int simpleRoundRobin(){
        int index = atomicInteger.addAndGet(1);
        int length = selectList.size();
        if (index >= length){
           index = atomicInteger.addAndGet(-length);
        }
        return selectList.get(index);
    }

}

但实际中,每台服务器可能配置不同,我们需要为不同的服务器增加不同的权重,部分服务器需要大概率被选中,权重就设置大一些,否则就设置小一些。带权重的轮询俗称WRR算法。

WRR目前有两种实现方式,一种是Nginx实现的平滑最大权重,dubbo也是参考这种模式。二是LVS实现的算法,其优先选择权重最大的服务器。

Nginx & Dubbo:不同权重的服务器,每次选择权重最大的,选中后,该权重会减去总权重,随后会加上其初始值。

类似下面的过程:

复制代码
In case of { 5, 1, 1 } weights this gives the following sequence of
current_weight's:

     a  b  c
     0  0  0  (initial state)

     5  1  1  (a selected)
    -2  1  1

     3  2  2  (a selected)
    -4  2  2

     1  3  3  (b selected)
     1 -4  3

     6 -3  4  (a selected)
    -1 -3  4

     4 -2  5  (c selected)
     4 -2 -2

     9 -1 -1  (a selected)
     2 -1 -1

     7  0  0  (a selected)
     0  0  0

看一下dubbo的实现:

复制代码
public class RoundRobinLoadBalance extends AbstractLoadBalance {
    public static final String NAME = "roundrobin";
    
    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
        ConcurrentMap<String, WeightedRoundRobin> map = methodWeightMap.get(key);
        if (map == null) {
            methodWeightMap.putIfAbsent(key, new ConcurrentHashMap<String, WeightedRoundRobin>());
            map = methodWeightMap.get(key);
        }
        int totalWeight = 0;
        long maxCurrent = Long.MIN_VALUE;
        long now = System.currentTimeMillis();
        Invoker<T> selectedInvoker = null;
        WeightedRoundRobin selectedWRR = null;
        //找出当前权重最大的,并计算总权重。
        for (Invoker<T> invoker : invokers) {
            String identifyString = invoker.getUrl().toIdentityString();
            WeightedRoundRobin weightedRoundRobin = map.get(identifyString);
            int weight = getWeight(invoker, invocation);
            long cur = weightedRoundRobin.increaseCurrent();
            weightedRoundRobin.setLastUpdate(now);
            //找出权重最大的
            if (cur > maxCurrent) {
                maxCurrent = cur;
                selectedInvoker = invoker;
                selectedWRR = weightedRoundRobin;
            }
            totalWeight += weight;
        }
       
        }
        if (selectedInvoker != null) {
            //对于选中的,会对其权重进行抵减,值为totalWeight。
            selectedWRR.sel(totalWeight);
            return selectedInvoker;
        }
    }

}

LVS的加权轮询每次都会选择权重最大的,的wiki说明:

复制代码
Supposing that there is a server set S = {S0, S1, ..., Sn-1};
W(Si) indicates the weight of Si;
i indicates the server selected last time, and i is initialized with -1;
cw is the current weight in scheduling, and cw is initialized with zero; 
max(S) is the maximum weight of all the servers in S;
gcd(S) is the greatest common divisor of all server weights in S;

while (true) {
    i = (i + 1) mod n;
    if (i == 0) {
        cw = cw - gcd(S); 
        if (cw <= 0) {
            cw = max(S);
            if (cw == 0)
            return NULL;
        }
    } 
    if (W(Si) >= cw) 
        return Si;
}

Python版的实现:

复制代码
class Store:
    __slots__ = ('index', 'weight')

    def __init__(self, index, weight):
        self.index = index
        self.weight = weight


def weighted(dataset):
    current = Store(index=-1, weight=0)

    dataset_length = len(dataset)
    dataset_max_weight = 0
    dataset_gcd_weight = 0

    for _, weight in dataset:
        if dataset_max_weight < weight:
            dataset_max_weight = weight
        dataset_gcd_weight = gcd(dataset_gcd_weight, weight)

    def get_next():
        while True:
            current.index = (current.index + 1) % dataset_length
            if current.index == 0:
                current.weight = current.weight - dataset_gcd_weight
                if current.weight <= 0:
                    current.weight = dataset_max_weight
                    if current.weight == 0:
                        return None
            if dataset[current.index][1] >= current.weight:
                return dataset[current.index][0]

    return get_next

3、最少连接数均衡(Least Connection)

思想:对每一台服务器都会记录当前正在处理的连接数量,当有新的服务连接请求时,将把当前请求分配给连接数最少的服务器。如果有多台服务器都满足最小连接数,就变成随机算法。此种均衡算法适合长时间处理的请求服务。感兴趣的可以看下Dubbo的实现:LeastActiveLoadBalance。

4、处理能力均衡

思想:把服务请求分配当前系统负载最小的服务器,由于考虑到了内部服务器的处理能力及当前网络运行状况,所以此种均衡算法相对来说更加精确。

5、目标地址散列:

思想:根据请求的目标IP地址,作为散列键(Hash Key)从散列表找出对应的服务器。

6、源地址散列:

思想:根据请求的源IP地址,作为散列键(Hash Key)散列表找出对应的服务器。

7、一致性Hash

相比较普通hash算法,一致性hash算法在扩缩容时,影响的数据范围比较小,方便客户端做路由缓存。针对该算法,在之前写的文章小米分库分表的实践 中有过介绍。在使用时为了避免数据倾斜,如果服务器数量较少的情况,要使用虚拟节点。Dubbo的实现方式是默认增加160个虚拟节点。并对每个ip+port后增加数字 1,2,3..... 。一致性Hash的存储结构最好的是使用红黑树TreeMap,因为其实现完美满足一致性hash算法。

容错机制

dubbo客户端通过watch注册中心的节点实现动态变化,此外支持不同方式的集群容错:

1)failover

失败自动切换,自动重试其他机器,常用于 操作。注意这里强调的是读请求,对于写请求,最好不要使用该模式。举例,如果某个接口是扣减金额接口,因为出现死锁或其他问题,导致接口超时了,此时会继续调用其他的服务器,这就导致接口被调用了两次或者多次。如果接口没有设计幂等(哪位大牛会不考虑幂等呢),那问题就比较严重了。

(2)failfast

快速失败,一次调用失败就立即失败,常用于非幂等的 操作或者是对时延要求较高的场景。

(3)failsafe

调用异常时忽略异常,不报错。该模式多用于一些不是特别重要的接口调用,比如邮件通知、数据统计等等。

(4)failback

失败后自动记录请求,然后定时重发,比较适合于一些异步操作,比如发送消息。

(5)forking

并行调用多个provider,只要一个成功就立即返回。常用于实时性要求比较高的读操作,但是会浪费更多的服务资源,可通过forks="2"来设置最大的并行数

(6)broadcacst

逐个调用所有的provider,任何一个provider出错则报错。常用于通知所有提供者更新缓存或日志等本地资源信息。

健康检测

对于负载均衡的服务器,应该具备某种健康检测机制,确保如果某个服务器异常下线,需要将其从负载均衡列表中摘除。健康检测可以是被动检测,也可以是主动检测。

被动检测思想类似于熔断,当多次调用超时或者失败,会在一定时间内不再访问对应服务器。下面是参考资料中的一个示意图,阐述得比较清晰。

被动检测的最大问题就是对于异常的机器不能及时摘除,导致仍然会有请求分发到异常机器上。因此如Dubbo+nacos实现的负载均衡,nacos通过和客户端和服务端建立连接,每几秒都会检测目标机器是否异常,如果异常会从当前注册列表中摘除。不过在实际使用中要注意,摘除异常并不是实时的,是定时发生的,也就是说存在客户端依然会调用异常Ip的可能。

负载均衡分类

目前负载均衡的实现有很多种,从TCP/IP协议栈的角度划分,包括四层负载均衡、七层负载均衡。

四层负载均衡:即在传输层实现的负载均衡,如LVS,Nginx都支持四层负载均衡。

七层负载均衡:即在应用层实现的负载均衡,如HTTP协议、DNS协议等,HTTP协议实现的负载均衡如Nginx,Dubbo等。

从软硬件角度可划分出DNS负载均衡、软件负载均衡、硬件负载均衡。

硬件负载均衡需要购买专门的硬件产品实现,而且都很昂贵,基本上是淘汰的,过时的。

软件负载均衡是通过软件来实现的,价格比较便宜,性能也比较优越。如nginx,lvs都是优秀的负载均衡软件。

在实际应用中,通常情况会结合着使用不同的负载均衡技术。如下图所示:

七层负载均衡:

  • Nginx HTTP协议
  • DNS DNS协议
  • Dubbo HTTP协议

四层负载均衡:

Nginx

LVS

Nginx负载均衡

nginx支持七层和四层负载均衡两种,不过四层负载均衡需要第三方插件的支持。Nginx七层负载均衡的配置非常简单。

复制代码
upstream account_backend {
    server 127.0.0.1:9090 max_fails=3 fail_timeout=10s weight=2;
    server 127.0.0.2:9090 max_fails=3 fail_timeout=10s;
    server 127.0.0.3:9090 max_fails=3 fail_timeout=10s;

}

当请求过来时,会根据某种算法选择某一个ip进行访问。Nginx默认使用加权轮询,权重默认是1。此外,Nginx还内置了ip hash算法,通过对客户端ip进行hash选择一个服务器,通过该方法可保证同一用户的可只打到一个后端机器上。

复制代码
upstream account_backend {
   ip_hash;
    server 127.0.0.1:9090 max_fails=3 fail_timeout=10s ;
    server 127.0.0.2:9090 max_fails=3 fail_timeout=10s;
    server 127.0.0.3:9090 max_fails=3 fail_timeout=10s;

}

当后端服务器集群中某个挂掉了,Nginx会自动将请求转到其他节点上。但是它默认不会将该异常节点摘除,后续的请求还是可能打到异常机器上的。至于这点,可以看这篇文章,该文章详细介绍了该如何进行后端健康检测的: Nginx负载均衡中后端节点服务器健康检查 - 运维笔记 - 散尽浮华 - 博客园,主要介绍了淘宝团队开发的nginx继承模块,通过主动检测实现自动剔除异常的server。

DNS负载均衡

DNS负载均衡实现的是广域网的全局负载均衡,通过为域名配置不同的A记录,实现将请求按照权重分散到不同的ip。这里的ip并不一定是实际的服务器ip,可以是nginx集群,也可以是lvs的vip。

下面是腾讯云DNSPod的一个实操:

优点

  • 近乎零成本,因为域名注册商的这种解析都是免费的;
  • 部署方便,除了网络拓扑的简单扩增,加服务器只需要在配置里加上相应ip。

缺点

  • 健康检查,如果某台服务器宕机,DNS服务器是无法知晓的,仍旧会将访问分配到此服务器。修改DNS记录全部生效时间需要很久。
  • 缓存问题。一般低级DNS都会缓存域名IP映射关系,如果某个出现问题或者发生变化,不会及时更新。

LVS负载均衡

四层负载均衡是针对七层负载均衡的一个补充,当Nginx的并发超过上线时,就需要通过LVS+Nginx集群实现高并发。

主要模式主要有以下几种:

  • LVS-NAT 主要通过网络地址转换,修改目的IP实现。Network Address Translation
  • LVS-TUN 主要封装一层IP头 IP Tunneling
  • LVS-DR 主要是修改目的MAC Direct Routing
  • LVS-FULLNAT 这个是阿里研发的一种模式,主要是解决多vlan的场景,它会修改请求报文的(源/目的)地址、(源/目的)端口。

先看几个LVS的名词定义:

CIP:客户端ip

Director:负载调度集群的主机,简称DR

VIP:Virtual IP,向外提供服务的IP

RIP:Real Server IP,内部真正提供服务的IP

DIP:DR主机用于内部通信的IP

1、LVS-NAT

流程示意图:

其请求处理流程:

1.客户端发送请求到LVS, 目标IP地址为VIP

  1. LVS根据某种负载均衡算法选择一个Real-server,并记录连接信息到hash表中,然后修改客户端的request的目的IP地址为选择的RS(这个RIP只是内部通信用的),随后将请求发给RS,此时源IP为CIP,目的IP为RIP;

  2. RS收到request包后,发现目的IP是自己的IP,于是处理请求,然后发送reply给LVS,此时源IP为RIP,目的IP是CIP;

  3. LVS收到reply包后,修改reply包的的源地址为VIP,原端口为VIP端口;

  4. LVS将reply发送给客户端;

  5. 客户端来的属于本次连接的包,查hash表,然后发给对应的RS;

  6. 客户端发送完毕,此次连接结束时,LVS自动从hash表中删除此条记录;

上面流程的特点是:

1、DR和RS必须是在同一个网段内,RS的网关配置成DIP,RIP和DIP都是用于内网机器间通信的IP。我感觉说的直白点,DR就是局域网网关的角色;

2、所有请求响应都要经过DR,这必然会导致DR会成为整个网络的瓶颈。

2、LVS-TUN

1.客户端Client 发送request包到LVS服务器, 目标地址VIP;

  1. LVS按照算法选择后端的一个Real-server,并将记录一条消息到hash表中,然后将客户端的request包封装到一个新的IP包里,新IP包的目的IP是RIP,源IP是DIP,然后转发给RS;

  2. RS收到包后,解封装,取出客户端的request包,发现还有一个IP包,目的地址是VIP,而RS发现在自己的虚拟网卡tunl0上有这个IP地址,于是处理客户端的请求,处理完成通过虚拟网卡发送给eth0网口发送出去,此时源IP为VIP,目的地址是CIP;

  3. 该客户端的后面的request包,LVS直接按照hash表中的记录直接转发给Real-server,当传输完毕或者连接超时,那么将删除hash表中的记录。

该模式中:

RIP,VIP,DIP都可以是公网地址,可以跨网段;

请求报文都走DR,响应不走,直接由RS发送给Client。这个特点使得TUN的性能要相比NAT提升了几倍,且解决了跨网段问题,问题就在于其维护成本挺高的;

3、LVS-DR

1、客户端Client 发送request包到LVS服务器, 目标地址VIP;

2、LVS根据负载均衡算法选择一台Realserver,将此RIP所在网卡的mac地址作为目标mac地址,发送到局域网里,因为是在数据链路层,所以必须在同一局域网;

3、RS在局域网中收到这个帧,拆开后发现目标IP不是自己的IP,正常来讲RS会抛弃的,但由于我们在loopback接口上配置了VIP,所以RS会接收该请求包并进行处理。

4、处理后,RS直接经过网络发送给客户端,源IP为VIP,目的IP为CIP。

LVS-DR技术解决了NAT的DR瓶颈问题,提高性能,但其最大限制就是DR和RIP必须是同一网段的,不能解决跨网段的问题。

4、LVS-FULLNAT

这个是阿里做的,LVS的DR和NAT模式要求RS和DR在同一个vlan中,导致部署成本过高;TUN模式虽然可以跨vlan,但RealServer上需要部署ipip隧道模块等,网络拓扑上需要连通外网,较复杂,不易运维。

为了解决上述问题,开发出FULLNAT,该模式和NAT模式的区别是:数据包进入时,除了做DNAT,还做SNAT(用户ip->内网ip),从而实现LVS-RealServer间可以跨vlan通讯,RealServer只需要连接到内网。

当客户端访问VIP后,DR会将源IP改成DIP,目的IP改成RIP。剩下的就是内网调用的事情了,可以是同网段,也可以是不同网段,至于不同Vlan如何通信,这个就不多说了,就是路由策略配置的问题了。

FULLNAT相对于之前那三种方式,配置维护都很简单,可以跨网段。现在也是LVS主流的使用模式。

LVS本身的部署有以下几种模式:

1、单机模式,一个LVS对应多个Nginx server;

2、一主一备+KeepAlived,提高可用性;

3、集群模式。通过FullNAT+OSPF-ECMP,并使用一致性hash算法实现分流。

目前最多的就是集群部署方式,其特点为:

  • LVS 和上联的三层交换机间运行 OSPF 协议。 OSPF 开放最短路径优先,是一种常用的路由协议。
  • 上联交换机通过 ECMP 等价路由,将数据流分发给 LVS 集群。ECMP充分应用在各大路由协议中,如OSPF,ISIS、EIGRP、BGP等。它可保证如果存在多条相等路径,会充分利用多条路由,从而实现基于流的负载均衡(如果不开ECMP,那么当选择一条路由后,或许都会一直使用该路由,其他相等路径都不会走)。
  • LVS 集群再转发给业务服务器。

目前公司的大部分服务几乎都用了LVS。即便是我之前公司所我开发的结算系统,虽然是ToB和内部财务使用,也同样用了LVS,用了两台Real Server,1台DIP,利用权重轮询。不过现在还把一台给停了,另外一台权重配置成100%了,哈哈。

有状态的需要存储数据的主从集群

有些服务器为了实现高可用性,会冗余部署多台服务器,并设置主从关系。可以是一主多从,也可以是多主多从。对于这种类型的集群部署,重要的是完成数据的复制以及当主服务器down时,可以实现自动或者手动切换,某台slave可以自动切换成主服务器。其中最典型的例子就是Mysql主从以及Redis中的主从部署,本文主要是介绍这两种。

Mysql高可用架构

Mysql的高可用架构有很多中,有一主一从,一主多从,双主+KeepAlive等。其中最常用的就是一主多从。通过水平扩展的方式可以实现Mysql数据库的高可用,同时实现流量分摊,并能够在某台master异常时,能够通过一定手段完成切换。

引入集群的部署模式后,在带来高可用、高性能的同时也引入了新问题,比如如何保证主从数据库的数据一致性、如何解决主从延迟、如何完成流量的切换等问题。

数据需要从主库同步到从数据库,从而保证数据的一致性,并通过不同的复制模式来实现强一致性、弱一致性、最终一致性等。先看一下Mysql主从复制的流程:

  1. 主库将数据更改记录到binlog中;
  2. 从库开启一个IO线程,并在主库开启一个特殊的binlog dump线程,将binlog数据 拷贝到从库的relay log中;从库并不是不断轮询去做同步操作。而是当主库有数据更改时,会给主库发送一个信号量。主库返回的信息不仅仅有binlog内容,还有新的binlog文件名,它以及下一个更新的binlog文件的位置。
  3. 从库使用SQL线程读取relay log中的数据,并将其写入到从库数据表。

可以看一下slave的processlist:

复制代码
ysql> show processlist\G;
*************************** 1. row ***************************
     Id: 4
   User: system user
   Host: 
     db: NULL
Command: Connect
   Time: 5205
  State: Waiting for master to send event
   Info: NULL
*************************** 2. row ***************************
     Id: 5
   User: system user
   Host: 
     db: NULL
Command: Connect
   Time: 5113
  State: Slave has read all relay log; waiting for more updates
   Info: NULL
*************************** 3. row ***************************

上面就是从库的两个线程,一个IO线程,一个SQL线程。实际上在Mysql5.6之后,SQL线程不仅仅只有一个,可以是多SQL线程并发执行。

传统的复制是根据文件位置开始进行的,但自从5.6之后引入了一个叫GTID的概念。GTID,global transaction identifieds全局事务标识。这个标识是唯一的,它可保证一个事务只能被执行一次。

在主从复制过程中,Master在更新数据时会生成一个GTID,并写在binlog中。从库的SQL线程在执行中继日志的过程中,会检查从库的binlog是否已经有了某个GTID记录,如果有就略过,如果没有就执行。

我们可以查看当前Mysql是否已经开启了GTID。

复制代码
mysql> show variables like "gtid_mode";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| gtid_mode     | ON   |
+---------------+-------+

Mysq复制方式主要包括异步复制、同步复制和半同步复制。

异步复制:当master执行了相应的写操作,执行成功后直接返回客户端。随后异步得将数据同步到slave。这种好处就是在完成复制的同时,不会牺牲系统的响应速率,但最大的问题就是不能保证数据一致性;

同步复制:当master在执行写操作时,必须要等到所有slave库的relay log写完,才能够返回给客户端。该模式可以较好低保证数据一致性,但却严重影响系统性能,从库越多,性能越差;

半同步复制:该复制模式是异步方式和同步复制的折中方案,只要slave中有一个成功写入relay log,master就可直接返回,如果在一定时间内,没有slave写入成功,则复制模式变为异步复制。

Mysql默认是采用异步复制的,可能其更看重的是系统性能,系统崩溃的概率还是比较低的,在实际中,我们可以根据自身使用场景来选择复制模式。

目前binlog的记录模式主要有三种,基于语句(statment),基于行(Row)模式,混合(mixed)模式。

基于语句的模式就是binlog记录的是所有可能影响数据变化的sql语句。然后在从库中再完全相同地执行一遍。这种模式比较好的地方是只记录语句,不用记录每一行的变化,数据量相对不大,节省带宽。比如一条批量update,只需要传一条语句,不用传每个更改的数据。但这种模式的缺点就是虽然主从库执行的sql完全相同。但主从库的上下文环境是不同的,而sql可能是严格依赖于环境的,比如当前时间戳等等。

基于行的模式,是记录被修改的数据,而不是sql语句。这种模式使得从库并不会因为环境不同,导致数据不能完全一致。但基于行的模式,可能会导致binlog的数据量特别大,就像上面说到的update,可能会在binlog产生更多的数据记录。这给复制造成了更大的开销。

mix模式,该模式是上述两种模式的组合。Mysql会根据实际的sql来决定选择哪个模式进行写入。

Redis-Cluster

Redis的系统架构经过了多次的演进,最开始的架构模式是一主多从,但由于其无法实现故障自动切换,随后增加了sentinel哨兵机制,即在主从的基础上增加一个哨兵进行监控,从而实现master出现故障时可自动切换。

从上面示意图可以看出,哨兵机制需要额外的机器节点来实现,完全和Redis无关,不存储数据,只用来监控,这在一定程度上造成了资源的浪费。基于此,Redis官方又开发出一套新的Redis架构,即Redis-Cluster,一个纯分布式的系统架构,示意图在文章最开始已经画出来了,这里再粘贴一下。

多个master节点通过Gossip协议实现元信息的交互和通信,实现命令包括Meet,Ping,Pong,Fail等,本文对此不做过多阐述,只讲述每个master节点和其slave所构成的集群模式。

Redis-Cluster的master-slave主要有以下几个作用:

1、保持高可用;

当某个master节点下线时,会自动切换到某个slave,slave会升级为新的master,从而实现故障的自动切换。具体流程是:

1、通过Ping消息,master节点在一定的超时时间内没有响应,就被标记为疑似下线,如果有多个节点认为该master节点疑似下线,那么该master就标记为下线。

2、master下的slave发起选举,其他的master节点会为相应的slave进行投票,根据Raft协议,选择出新的master节点;

这里要注意两点。一是slave发起选举,二是master投票。

Slave在认为master进入FAIL之后会delay一段时间才会发起选举,其原因是为了让master的FAIL状态在集群内广播。不同的slave的delay时间是不同的,从而避免一个master的多个slave在同一时间发起election。

master投票的条件:

  • Slave所属的master的状态为FAIL
  • Slave的currentEpoch大于等于master的currentEpoch
  • Slave的configEpoch大于等于master认为该slave所属master的configEpoch
  • Master维护一个lastVoteEpoch字段,针对每一个epoch只会投票一次,一旦投票后就会拒绝所有更小的epoch
  • 投票就会回复ACK,否则就忽略

3、slave选举为新的master节点后,会被分配旧master的哈希槽。

2、实现读写分离;

Redis-Cluster中的master节点对外提供写服务,slave可以对外提供读服务。

如果要实现高可用和读写分离,前提是要实现主从复制,即保证主从节点的数据的一致性。Redis的主从复制主要包括两种 全量复制和部分复制。其中全量复制通常发生在第一次同步过程或者在部分复制出现异常时。

当在slave中使用slaveOf(Redi5.0之后使用replicaof),确定slave和master的关系。

复制代码
 slave of ip  port

具体复制流程可见下图,来自小林coding:

1、执行slaveof之后,主从服务器会建立一个长连接。随后从服务器向主服务器发送psync命令(早期版本是syncc),psync命令有两个参数,一个是runid,表示的是主服务器id;一个是offset,表示的是偏移量。第一次执行时,从服务器不知道主服务器的runId,所以参数是"?",且没有读取任何数据,偏移量是-1;

2、主服务器收到命令后,会开始执行全量复制。首先会执行bgsave命令,生成RDB文件,随后将其发送给从服务器,slave收到RDB文件之后,会载入RDB文件数据。

3、在上一步执行期间,有可能还会有写命令进入,而这些写命令执行结果并没有在RDB文件中,因此主服务器为了处理这部分数据,在执行间隙会将写命令写入到一个叫做replication buffer缓冲区中。当slave加载完RDB文件之后,主服务器会将缓冲区中的写命令发送给slave,随后slave继续执行这些写命令;

4、完成第一次全量同步之后,后续只要主服务器有写命令产生,就会将写命令异步地同步给从服务器。

5、如果某个slave网络断开之后恢复了。从服务器会给master发送psync命令,其中会传入主服务器的runid以及读偏移量。

6、主服务器会判断从服务器要读取的偏移量数据是否在repl_backlog_buffer中,如果在就执行增量复制;如果不在,则执行全量复制。

总结

本文主要讲述了两种不同的集群模式,一是无状态的集群;二是有状态,需做数据存储的集群两种。无状态集群介绍了负载均衡分类以及负载均衡算法;有状态集群介绍了主从复制的原理和流程。

参考资料:

深入浅出负载均衡 - SegmentFault 思否

[万字长文] 吃透负载均衡 - 高性能架构探索

Dubbo 一致性Hash负载均衡实现剖析 | Apache DubboDUBBO LOGO

Nginx负载均衡(仅学习) - 知乎

千与千寻-Mysql复制

详解nginx的原生被动健康检查机制&灾备使用(含测试)_无影V随风的博客-CSDN博客_被动健康检查

高并发场景 LVS 安装及高可用实现 - 惨绿少年 - 博客园

Redis哨兵模式(sentinel)学习总结及部署记录(主从复制、读写分离、主从切换) - 散尽浮华 - 博客园

主从复制是怎么实现的? | 小林coding

深度探索MySQL主从复制原理 - 知乎

相关推荐
你都会上树?37 分钟前
MySQL MVCC 详解
数据库·mysql
大春儿的试验田42 分钟前
高并发收藏功能设计:Redis异步同步与定时补偿机制详解
java·数据库·redis·学习·缓存
hqxstudying1 小时前
Redis为什么是单线程
java·redis
长征coder1 小时前
AWS MySQL 读写分离配置指南
mysql·云计算·aws
ladymorgana2 小时前
【docker】修改 MySQL 密码后 Navicat 仍能用原密码连接
mysql·adb·docker
PanZonghui2 小时前
Centos项目部署之安装数据库MySQL8
linux·后端·mysql
GreatSQL社区2 小时前
用systemd管理GreatSQL服务详解
数据库·mysql·greatsql
掘根2 小时前
【MySQL进阶】错误日志,二进制日志,mysql系统库
数据库·mysql
weixin_438335402 小时前
基础知识:mysql-connector-j依赖
数据库·mysql
小明铭同学3 小时前
MySQL 八股文【持续更新ing】
数据库·mysql