分布式设计介绍

举报
SHQ5785 发表于 2023/12/07 08:52:45 2023/12/07
【摘要】 ​​一、前言            分布式设计与开发在IDF05(Intel Developer Forum 2005)上,Intel首席执行官Craig Barrett就取消4GHz芯片计划一事,半开玩笑当众单膝下跪致歉,给广大软件开发者一个明显的信号,单纯依靠垂直提升硬件性能来提高系统性能的时代已结束,分布式开发的时代实际上早已悄悄地成为了时代的主流,吵得很热的云计算实际上只是包装在分布...

​一、前言      

      分布式设计与开发在IDF05(Intel Developer Forum 2005)上,Intel首席执行官Craig Barrett就取消4GHz芯片计划一事,半开玩笑当众单膝下跪致歉,给广大软件开发者一个明显的信号,单纯依靠垂直提升硬件性能来提高系统性能的时代已结束,分布式开发的时代实际上早已悄悄地成为了时代的主流,吵得很热的云计算实际上只是包装在分布式之外的商业概念,很多开发者(包括我)都想加入研究云计算这个潮流,在google上通过“云计算”这个关键词来查询资料,查到的都是些概念性或商业性的宣传资料,其实真正需要深入的还是那个早以被人熟知的概念------分布式。


      分布式可繁也可以简,最简单的分布式就是大家最常用的,在负载均衡服务器后加一堆web服务器,然后在上面搞一个缓存服务器来保存临时状态,后面共享一个数据库,其实一致性Hash算法本身比较简单,不过可以根据实际情况有很多改进的版本,其目的无非是两点:

  1. 节点变动后其他节点受影响尽可能小;

  2. 节点变动后数据重新分配尽可能均衡;


      实现这个算法就技术本身来说没多少难度和工作量,需要做的是建立起你所设计的映射关系,无需借助什么框架或工具,sourceforge上倒是有个项目libconhash,可以参考一下。


      以上两个算法在我看来就算从不涉及算法的开发人员也需要了解的,算法其实就是一个策略,而在分布式环境常常需要我们设计一个策略来解决很多无法通过单纯的技术搞定的难题,学习这些算法可以提供我们一些思路。分布式环境中大多数服务是允许部分失败,也允许数据不一致,但有些最基础的服务是需要高可靠性,高一致性的,这些服务是其他分布式服务运转的基础,比如naming service、分布式lock等,这些分布式的基础服务有以下要求:

      高可用性  高一致性  高性能

      对于这种有些挑战CAP原则的服务该如何设计,是一个挑战,也是一个不错的研究课题,Apache的ZooKeeper给了我们一个不错的答案。ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,它暴露了一个简单的原语集,分布式应用程序可以基于它实现同步服务,配置维护和命名服务等。关于ZooKeeper更多信息可以参见官方文档。

二、ZooKeeper的基本使用

      搭一个分布式的ZooKeeper环境比较简单,基本步骤如下:

      1)在各服务器安装ZooKeeper。下载ZooKeeper后在各服务器上进行解压即可:

         tar -xzf zookeeper-3.2.2.tar.gz


      2)配置集群环境

      分别在各服务器的zookeeper安装目录下创建名为zoo.cfg的配置文件,内容填写如下:

# The number of milliseconds of each tick

tickTime=2000

# The number of ticks that the initial

# synchronization phase can take

initLimit=10

# The number of ticks that can pass between

# sending a request and getting an acknowledgement

syncLimit=5

# the directory where the snapshot is stored.

dataDir=/home/admin/zookeeper-3.2.2/data

# the port at which the clients will connect

clientPort=2181

server.1=zoo1:2888:3888

server.2=zoo2:2888:3888


      其中zoo1和zoo2分别对应集群中各服务器的机器名或ip,server.1和server.2中1和2分别对应各服务器的zookeeper id,id的设置方法为在dataDir配置的目录下创建名为myid的文

件,并把id作为其文件内容即可,在本例中就分为设置为1和2。其他配置具体含义可见官方文档。


       3)启动集群环境分别在各服务器下运行zookeeper启动脚本

/home/admin/zookeeper-3.2.2/bin/zkServer.sh start

     

4)应用zookeeper

      应用zookeeper可以在是shell中执行命令,也可以在java或c中调用程序接口。

      在shell中执行命令,可运行以下命令:

 bin/zkCli.sh -server 10.20.147.35:2181

      其中10.20.147.35为集群中任一台机器的ip或机器名。执行后可进入zookeeper的操作面板,具体如何操作可见官方文档。

      在java中通过调用程序接口来应用zookeeper较为复杂一点,需要了解watch和callback等概念,不过试验最简单的CURD倒不需要这些,只需要使用ZooKeeper这个类即可,具体测试代码如下:

Public static void main(String[] args) {

    try {

        ZooKeeper zk = new ZooKeeper("10.20.147.35:2181", 30000, null);

        String name = zk.create("/company", "alibaba".getBytes(),

                Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);

        Stat stat = new Stat();

        System.out.println(new String(zk.getData(name, null, stat)));

        zk.setData(name, "taobao".getBytes(), stat.getVersion(), null, null);

        System.out.println(new String(zk.getData(name, null, stat)));

        stat = zk.exists(name, null);

        zk.delete(name, stat.getVersion(), null, null);

        System.out.println(new String(zk.getData(name, null, stat)));

    } catch (Exception e) {

        e.printStackTrace();

    }

}

      以上代码比较简单,查看一下ZooKeeper的api doc就知道如何使用了。

三、ZooKeeper的实现机理

      ZooKeeper的实现机理是我看过的开源框架中最复杂的,它解决的是分布式环境中的一致性问题,这个场景也决定了其实现的复杂性。看了两三天的源码还是有些摸不着头脑,有些超出了我的能力,不过通过看文档和其他高人写的文章大致清楚它的原理和基本结构。


      1)ZooKeeper的基本原理

      ZooKeeper是以Fast Paxos算法为基础的,在前一篇blog中大致介绍了一下paxos,而没有提到的是paxos存在活锁的问题,也就是当有多个proposer交错提交时,有可能互相排斥导致没有一个proposer能提交成功,而Fast Paxos作了一些优化,通过选举产生一个leader,只有leader才能提交propose,具体算法可见Fast Paxos。因此要想弄懂ZooKeeper首先得对Fast Paxos有所了解。


      2)ZooKeeper的基本运转流程

      ZooKeeper主要存在以下两个流程:

  1. 选举Leader

  2. 同步数据


      选举Leader过程中算法有很多,但要达到的选举标准是一致的:

  1. Leader要具有最高的zxid

  2. 集群中大多数的机器得到响应并follow选出的Leader同步数据这个流程是ZooKeeper的精髓所在,并且就是Fast Paxos算法的具体实现。一个牛人画了一个ZooKeeper数据流动图,比较直观地描述了ZooKeeper是如何同步数据的。


      以上两个核心流程我暂时还不能悟透其中的精髓,这也和我还没有完全理解Fast Paxos算法有关,有待后续深入学习。

四、ZooKeeper应用领域

      Tim在blog中提到了Paxos所能应用的几个主要场景,包括database replication、naming service、config配置管理、access control list等等,这也是ZooKeeper可以应用的几个主要场景。此外,ZooKeeper官方文档中提到了几个更为基础的分布式应用,这也算是ZooKeeper的妙用吧。

      1)分布式Barrier

      Barrier是一种控制和协调多个任务触发次序的机制,简单说来就是搞个闸门把欲执行的任务给拦住,等所有任务都处于可以执行的状态时,才放开闸门。

      在单机上JDK提供了CyclicBarrier这个类来实现这个机制,但在分布式环境中JDK就无能为力了。在分布式里实现Barrer需要高一致性做保障,因此ZooKeeper可以派上用场,所采取的方案就是用一个Node作为Barrer的实体,需要被Barrer的任务通过调用exists()检测这个Node的存在,当需要打开Barrier的时候,删掉这个Node,ZooKeeper的watch机制会通知到各个任务可以开始执行。


      2)分布式Queue

      与Barrier类似,分布式环境中实现Queue也需要高一致性做保障,ZooKeeper提供了一个种简单的方式,ZooKeeper通过一个Node来维护Queue的实体,用其children来存储Queue的内容,并且ZooKeeper的create方法中提供了顺序递增的模式,会自动地在name后面加上一个递增的数字来插入新元素。可以用其children来构建一个queue的数据结构,offer的时候使用create,take的时候按照children的顺序删除第一个即可。ZooKeeper保障了各个server上数据是一致的,因此也就实现了一个分布式Queue。take和offer的实例代码如下所示:

/**

* Removes the head of the queue and returns it, blocks until it succeeds.

* @return The former head of the queue

* @throws NoSuchElementException

* @throws KeeperException

* @throws InterruptedException

*/

Public byte[] take() throws KeeperException, InterruptedException {

    TreeMap<Long,String> orderedChildren;

    // Same as for element.  Should refactor this.

    while(true){

        LatchChildWatcher childWatcher = new LatchChildWatcher();

        try{

            orderedChildren = orderedChildren(childWatcher);

        }catch(KeeperException.NoNodeException e){

            zookeeper.create(dir, new byte[0], acl, CreateMode.PERSISTENT);

            continue;

        }

        if(orderedChildren.size() == 0){

            childWatcher.await();

            continue;

        }

        for(String headNode : orderedChildren.values()){

            String path = dir +"/"+headNode;

            try{

                byte[] data = zookeeper.getData(path, false, null);

                zookeeper.delete(path, -1);

                return data;

            }catch(KeeperException.NoNodeException e){

                // Another client deleted the node first.

            }

        }

    }

}

/**

* Inserts data into queue.

* @param data

* @return true if data was successfully added

*/

public boolean offer(byte[] data) throws KeeperException, InterruptedException{

    for(;;){

        try{

         zookeeper.create(dir+"/"+prefix,data,acl,CreateMode.PERSISTENT_SEQUENTIAL);

            return true;

        }catch(KeeperException.NoNodeException e){

            zookeeper.create(dir, newbyte[0], acl, CreateMode.PERSISTENT);

        }

    }

}

      3)分布式lock

      利用ZooKeeper实现分布式lock,主要是通过一个Node来代表一个Lock,当一个client去拿锁的时候,会在这个Node下创建一个自增序列的child,然后通过getChildren()方式来check创建的child是不是最靠前的,如果是则拿到锁,否则就调用exist()来check第二靠前的child,并加上watch来监视。当拿到锁的child执行完后归还锁,归还锁仅仅需要删除自己创建的child,这时watch机制会通知到所有没有拿到锁的client,这些child就会根据前面所讲的拿锁规则来竞争锁。


      一个大型系统里各个环节中最容易出性能和可用性问题的往往是数据库,因此分布式设计与开发的一个重要领域就是如何让数据层具有可扩展性,数据库的扩展分为Scale Up 和Scale Out,而Scale Up说白了是通过升级服务器配置来完成,因此不在分布式设计的考虑之内。Scale Out是通过增加机器的方式来提升处理能力,一般需要考虑以下两个问题:

  1. 数据拆分

  2. 数据库高可用架构

      数据拆分是最先会被想到的,原理很简单,当一个表的数据达到无法处理的时候,就需要把它拆成多个表,说起来简单,真正在项目里运用的时候有很多点是需要深入研究的,一般分为:切分策略与应用程序端的整合策略。

4.1 切分策略

      切分策略一般分为垂直切分横向切分和两者的混搭


    1)垂直切分

      垂直切分就是要把表按模块划分到不同数据库中,这种拆分在大型网站的演变过程中是很常见的。当一个网站还在很小的时候,只有小量的人来开发和维护,各模块和表都在一起,当网站不断丰富和壮大的时候,也会变成多个子系统来支撑,这时就有按模块和功能把表划分出来的需求。其实,相对于垂直切分更进一步的是服务化改造,说得简单就是要把原来强耦合的系统拆分成多个弱耦合的服务,通过服务间的调用来满足业务需求看,因此表拆出来后要通过服务的形式暴露出去,而不是直接调用不同模块的表,淘宝在架构不断演变过程中,最重要的一环就是服务化改造,把用户、交易、店铺、宝贝这些核心概念抽取成独立的服务,也非常有利于进行局部的优化和治理,保障核心模块的稳定性。


这样一种拆分方式也是有代价的:表关联无法在数据库层面做单表大数据量依然存在性能瓶颈,事务保证比较复杂应用端的复杂性增加上面这些问题是显而易见的,处理这些的关键在于如何解除不同模块间的耦合性,这说是技术问题,其实更是业务的设计问题,只有在业务上是松耦合的,才可能在技术设计上隔离开来。没有耦合性,也就不存在表关联和事务的需求。另外,大数据瓶颈问题可以参见下面要讲的水平切分。


      2)水平切分

      上面谈到垂直切分只是把表按模块划分到不同数据库,但没有解决单表大数据量的问题,而水平切分就是要把一个表按照某种规则把数据划分到不同表或数据库里。例如像计费系统,通过按时间来划分表就比较合适,因为系统都是处理某一时间段的数据。而像SaaS(software as a service)应用,通过按用户维度来划分数据比较合适,因为用户与用户之间的隔离的,一般不存在处理多个用户数据的情况。水平切分没有破坏表之间的联系,完全可以把有关系的表放在一个库里,这样就不影响应用端的业务需求,并且这样的切分能从根本上解决大数据量的问题。它的问题也是很明显的:当切分规则复杂时,增加了应用端调用的难度数据维护难度比较大,当拆分规则有变化时,需要对数据进行迁移。对于第一个问题,可以参考后面要讲的如何整合应用端和数据库端。对于第二个问题可以参考一致性hash的算法,通过某些映射策略来降低数据维护的成本。


      3)垂直与水平联合切分

      由上面可知垂直切分能更清晰化模块划分,区分治理,水平切分能解决大数据量性能瓶颈问题,因此常常就会把两者结合使用,这在大型网站里是常见的策略,这可以结合两者的优点,当然缺点就是比较复杂,成本较高,不太适合小型网站,下面是结合前面两个例子的情况:与应用程序端的整合策略数据切出来还只是第一步,关键在于应用端如何方便地存取数据,不能因为数据拆分导致应用端存取数据错误或者异常复杂。按照从前往后一般说来有以下三种策略:

  • 应用端做数据库路由

  • 在应用端和服务器端加一个代理服务器做路由

  • 数据库端自行做路由


      1)应用端做数据库路由

      应用端做数据库路由实现起来比较简单,也就是在数据库调用的点通过工具包的处理,给每次调用数据库加上路由信息,也就是分析每次调用,路由到正确的库。这种方式多多少少没有对应用端透明,如果路由策略有更改还需要修改应用端,并且这种更改很难做到动态更改。最关键的是应用端的连接池设计会比较复杂,池里的连接就不是无状态了,不利于管理和扩展。


      2)在应用端和服务器端加一个代理服务器做路由

      通过代理服务器来做服务器做路由可以对客户端屏蔽后端数据库拆分细节,增强了拆分规则的可维护性,一般而言proxy需要提供以下features:

  • 对客户端和数据库服务端的连接管理和安全认证

  • 数据库请求路由可配置性

  • 对调用命令和SQL的解析

  • 调用结果的过滤和合并


      3)数据库端自行做路由

      例如MySQL就提供了MySQL Proxy的代理产品可以在数据库端做路由。这种方式的最大问题就是拆分规则配置的灵活性不好,不一定能满足应用端的多种划分需求。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。