欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【转】Zookeeper的前世今生

程序员文章站 2024-03-23 09:01:58
...

原文链接:https://www.imooc.com/article/45213

 

到底发生了什么?

  在电商架构中,早期是单体架构,可以很快的解决交互问题和产品初期的迭代。但是随着架构的发展,后端无法支撑大流量。一开始的解决办法是增加服务器等垂直解决方法,但是这样的效率太低并且成本太高。因此开发者开始考虑水平伸缩来提高整体的性能。

  首先是对产品的拆分:按照类型拆分成不同的模块,那么模块之间的交互就需要实现远程调用,比如webservice。这样其实就简单的形成了一个分布式架构。服务越来越多,我们就拆分的越来越细,随着流量不断提升,后端规模越来越大。举个例子:用户调用订单服务,它是通过http协议来调用,那么首先订单服务那必须给用户一个地址。如果说订单系统是一个大的集群,那么可能我们就需要维护多个这样的地址。那么应该如何解决大规模地址管理?集群里的地址如何去转发?如果其中一个节点down机了怎么办,怎么去管理服务动态上下线感知?

  那我们就通过解决以上三个问题来引出本文的主角---Zookeeper

  我们可以考虑设置一个中间件,它的存在可以让我们的服务发布的时候注册上去,充当一个电话本,记住你所有的地址,并且及时了解你是不是断开。用户服务只需要拿到中间件的地址,就可以获得对应的相关的调用的目标服务的信息,拿到这个信息,根据负载均衡算法,就可以做一个转发。

  Zookeeper是一个什么东西呢?它是一个文件存储类似的树型结构,entry是key-value。每个子节点由父节点管理,子节点是父节点详细的分类。比如说,对于订单服务系统,它的子节点存放着各种地址。Zookeeper适不适合作为一个注册中心?很多人说不是很合适,但是目前大部分企业仍然用它来做注册中心的功能。Zookeeper的学术名称为分布式协调服务,它的本意是解决分布式锁的,比如说几个服务访问共享资源,就会出现资源竞争的问题,这时候就会需要一个协调者来解决这个问题,Zookeeper就是用来解决这个问题的,可以看作一个交警。这样一来共享资源就变成了一个单点访问资源,你先来我中间件里来,我再判断让不让你去访问。当然为了保持单点的特点,Zookeeper一般是以集群出现,在满足单点的功能,提高其可用性。集群的出现带来的问题众所周知,那就是数据同步。Zookeeper内部角色分为Leader、Follower、observer,数据提交方式基于二阶提交,写数据写在follower上,其他的follower去同步数据。请求命令放在leader上,然后让其他的节点知道,这里满足一个CAP原则。

  Zookeeper作为一个分布式协调服务,目标是为了解决分布式架构中一致性问题。

  Zookeeper客户端可以提供增删改查节点的功能,删除的时候必须一层一层的删除。而且节点具有唯一性,可以参考电脑文件结构。同时节点还分为临时节点 -e和持久化节点、有序节点 -s和无序节点。

Zookeeper应用场景

  注册中心、配置中心(和注册中心大同小异,类似于application.properties,用来统一维护配置信息)、负载均衡(知道机器的状态以及选举leader)和分布式锁。

[zk: localhost:2181(CONNECTED) 0] create /userservice 0Created /userservice
[zk: localhost:2181(CONNECTED) 2] ls /[zookeeper, userservice]
[zk: localhost:2181(CONNECTED) 3] ls /userservice
[]

   配置zoo.cfg文件,将三个服务器的ip、访问Zookeeper的端口以及选举的端口配置好

server.1=10.10.101.7:2888:3888server.2=10.10.101.104:2888:3888server.3=10.10.101.108:2888:3888
server.n=ip:prot:prot

其中n在 /tmp/zookeeper/myid里配置

  除了Leader和Follower之外,还有一个Observer的节点,它的作用是用来监控整个集群的状态。

Zookeeper特性分析

ACL

  ACL属于一种权限控制,控制你创建的文件夹(节点)的0访问权限。它提供了create、write、read、delete和admin五种权限。

角色

  leader用来处理事务请求的,就是所有的添加修改删除都会去leader那,非事务请求(查询)可能会落到任一节点上。

数据模型

  数据模型是Zookeeper里较为核心的东西,它的结构类似于树,也类似于电脑的文件管理系统。节点特性分为持久化和有序性,每个znode可以保存少量数据。

会话

  客户端和服务端连接会建立会话

进阶

Zookeeper的技术层面由来

  假设分布式系统中有三个节点作为一个集群,在这个集群里运行一个任务,所以每个节点都有权限去执行这个任务。那我我有几个问题想问:

  (1)怎么保证各个节点数据一致?

  (2)怎么保证任务只在一个节点执行?

  (3)如果在执行任务的1节点挂了,那么其他的节点如何发现并接替任务?

  (4)它们对于共享资源是怎么处理的?

   我们可以先将节点注册到Zookeeper中,然后因为节点有顺序性,所以说我们第一个看到的节点就认为他是最具优先权的,那么它就可以去做这个操作,这就是Zookeeper起到的能够给集群节点进行一个协调的作用。

  那么按照上面的几个问题,如果我们想设计一个中间件,那么应该注意哪些事情呢?

  (1)单点故障 

  存在leader、follower节点。同时也会分担请求(高可用、高性能)。

  (2)为啥集群要有master

  (3)如果集群中的maser挂了怎么办?数据如何恢复?如何选举?

    Zookeeper选举使用了ZAB协议

  (4)如何保证数据一致性?(分布式事务)

  2PC(二阶提交):当一个事务涉及多个节点提交,为了保证进行,引入一个协调者,通过协调者控制整个集群工作的顺利进行。当一个事务开始时,由协调者将请求发给所有节点,然后节点若能执行,则向协调者发起可以执行请求。如果一个参与者失败,不能进行事务执行,那么其他节点都将发起回滚提交。否则,所有节点顺利提交,这个事务顺利完成。

Zookeeper集群内部成员介绍:

【转】Zookeeper的前世今生

为什么Zookeeper集群是2*n+1的数量?

  因为Zookeeper集群如果正常对外服务,必须有投票机制,集群内部有过半节点正常服务,保证投票能够有结果。而且能够保证对n个服务器的容灾处理。

ZAB协议

  它是Zookeeper里面专门用来处理崩溃恢复的原子广播协议,依赖ZAB协议实现分布式数据一致性。如果集群中leader出现了问题,ZAB协议就会进行恢复模式并选举产生新的leader,选举产生之后,并且集群中有过半节点与leader数据同步之后,ZAB就会退出恢复模式。

  消息广播:属于一个简化的二阶提交机制,leader收到请求后,会给事务请求赋予一个zxid,可以通过zxid的大小去比较生成因果有序这个特性。leader会给每个follower给一个FIFO队列,然后将带有zxid的消息作为一个提案分发给所有的leader,follower收到请求后,将提案写入磁盘,并给leader发送ack。如果leader收到半数以上的ack,就确定这个消息要执行,然后给所有的follower发送commit指令,同时也会在本地执行这个请求。如果没有通过,所有节点执行回滚。

  崩溃恢复:leader挂掉之后,那么就需要恢复选举和数据。当leader失去了过半节点联系、leader挂了这两种情况发生,集群就会进入崩溃恢复阶段。对于数据恢复来说:(1)已经被处理的消息不能丢失,也就是来个栗子:当follower收到commit之前,leader挂掉了,怎么办?这时部分节点收到commit,部分节点没有收到,这时ZAB协议保证已经处理的消息不能被丢失,被丢弃的消息不能再次出现(当leader节点收到事务请求之后,在生成提案时挂了,那么新选举的leader节点要跳过这个消息)。

  ZAB协议需要满足这两个要求,必须设计出算法。

  ZAB协议中保证选举出来的leader有着整个集群zxid最大的提案,这样第一是保证新的leader之前是正常工作的,第二是因为zxid是64位的,高32为epoch编号,每当leader选举产生一个新的leader,新的leader的epoch号就+1,低32位是消息计数器,每当接受一条消息,就+1。新的leader被选举之后就会清空。这样可以保证老的leader挂掉之后,不可能被再次选举。可以把epoch看做成皇帝的年号,现在统治的事哪个皇帝。

  zxid在上面已经简单介绍了,下面说一下它的简单特性:Zookeeper中所有提议在被提出时都会加上zxid。

leader选举

  基于fast leader选举,基于几个方面:zxid最大会设置成leader,epoch;myid(服务器id),myid越大,在leader选举权重中越大;事务id,事务id越大,表示事务越新;epoch(逻辑时钟)每一次投票,epoch都会递增;选举状态:LOOKING->LEADING(FOLLOWING、OBSERVING)

  启动的时候:每个Server都会发起一个投票,每个节点都会先将自己作为一个leader,并将自己的zxid、myid等信息发给其他节点。其他节点会进行比较:zxid相同就检查myid,myid大的会作为leader,之后开始进行统计投票,最后选出leader。

  前一个leader挂掉:所有节点编程looking状态,然后会查看其他节点的信息,来做出投票。

看一下源码理解一下

protected void initializeAndRun(String[] args)        throws ConfigException, IOException
    {
        org.apache.zookeeper.server.quorum.QuorumPeerConfig config = new org.apache.zookeeper.server.quorum.QuorumPeerConfig();        if (args.length == 1) {
            config.parse(args[0]);
        }        // Start and schedule the the purge task
        DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config
                .getDataDir(), config.getDataLogDir(), config
                .getSnapRetainCount(), config.getPurgeInterval());
        purgeMgr.start();        //判断是单机还是集群模式
        if (args.length == 1 && config.servers.size() > 0) {
            runFromConfig(config);
        } else {
            LOG.warn("Either no config or no quorum defined in config, running "
                    + " in standalone mode");            // there is only server in the quorum -- run as standalone            ZooKeeperServerMain.main(args);
        }
    }

 

  因为只有集群模式才会有选举,这时候我们会进入到runFromConfig方法中:

public void runFromConfig(org.apache.zookeeper.server.quorum.QuorumPeerConfig config) throws IOException {      try {
          ManagedUtil.registerLog4jMBeans();
      } catch (JMException e) {
          LOG.warn("Unable to register log4j JMX control", e);
      }
  
      LOG.info("Starting quorum peer");      try {
          ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
          cnxnFactory.configure(config.getClientPortAddress(),
                                config.getMaxClientCnxns());
  
          quorumPeer = new org.apache.zookeeper.server.quorum.QuorumPeer();
          quorumPeer.setClientPortAddress(config.getClientPortAddress());
          quorumPeer.setTxnFactory(new FileTxnSnapLog(                      new File(config.getDataLogDir()),                      new File(config.getDataDir())));
          quorumPeer.setQuorumPeers(config.getServers());
          quorumPeer.setElectionType(config.getElectionAlg());
          quorumPeer.setMyid(config.getServerId());
          quorumPeer.setTickTime(config.getTickTime());
          quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout());
          quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout());
          quorumPeer.setInitLimit(config.getInitLimit());
          quorumPeer.setSyncLimit(config.getSyncLimit());
          quorumPeer.setQuorumVerifier(config.getQuorumVerifier());
          quorumPeer.setCnxnFactory(cnxnFactory);
          quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory()));
          quorumPeer.setLearnerType(config.getPeerType());
          quorumPeer.setSyncEnabled(config.getSyncEnabled());
          quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs());
  
          quorumPeer.start();
          quorumPeer.join();
      } catch (InterruptedException e) {          // warn, but generally this is ok
          LOG.warn("Quorum Peer interrupted", e);
      }
    }

 

  可以看到,它会从配置文件中加载一些信息,最后启动start来开始进行选举。

 

@Override    
public synchronized void start() {
        loadDataBase();
        cnxnFactory.start(); 
        
        //开始进行选举Leader        startLeaderElection();        super.start();
    }

 

 

    
synchronized public void startLeaderElection() {        try {
            currentVote = new org.apache.zookeeper.server.quorum.Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
        } catch(IOException e) {
            RuntimeException re = new RuntimeException(e.getMessage());
            re.setStackTrace(e.getStackTrace());            throw re;
        }        for (QuorumServer p : getView().values()) {            if (p.id == myid) {
                myQuorumAddr = p.addr;                break;
            }
        }        if (myQuorumAddr == null) {            throw new RuntimeException("My id " + myid + " not in the peer list");
        }        if (electionType == 0) {            try {
                udpSocket = new DatagramSocket(myQuorumAddr.getPort());
                responder = new ResponderThread();
                responder.start();
            } catch (SocketException e) {                throw new RuntimeException(e);
            }
        }        this.electionAlg = createElectionAlgorithm(electionType);
    }

 

  从上段代码的一开始,表示它会存储三个信息:myid、zxid和epoch,然后配置选举类型来使用选举算法,

 

protected org.apache.zookeeper.server.quorum.Election createElectionAlgorithm(int electionAlgorithm){
        org.apache.zookeeper.server.quorum.Election le=null;                
        //TODO: use a factory rather than a switch
        switch (electionAlgorithm) {        case 0:
            le = new org.apache.zookeeper.server.quorum.LeaderElection(this);            break;        case 1:
            le = new org.apache.zookeeper.server.quorum.AuthFastLeaderElection(this);            break;        case 2:
            le = new org.apache.zookeeper.server.quorum.AuthFastLeaderElection(this, true);            break;        case 3:
            qcm = new org.apache.zookeeper.server.quorum.QuorumCnxManager(this);
            org.apache.zookeeper.server.quorum.QuorumCnxManager.Listener listener = qcm.listener;            if(listener != null){
                listener.start();
                le = new org.apache.zookeeper.server.quorum.FastLeaderElection(this, qcm);
            } else {
                LOG.error("Null listener when initializing cnx manager");
            }            break;        default:            assert false;
        }        return le;
    }

 

  这里面提供着一些选举算法,最后会设置一个负责选举的IO类,然后启动来进行选举。

 

 本文思路来源于《从Paxos到Zookeeper:分布式一致性原理与实践》一书


作者:慕仙森
链接:https://www.imooc.com/article/45213
来源:慕课网