写点什么

InfoQ 极客传媒 15 周年庆征文|分布式设计介绍

  • 2022 年 6 月 21 日
  • 本文字数:6620 字

    阅读完需:约 22 分钟

InfoQ 极客传媒 15 周年庆征文|分布式设计介绍

​一、前言      

      分布式设计与开发在 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 的代理产品可以在数据库端做路由。这种方式的最大问题就是拆分规则配置的灵活性不好,不一定能满足应用端的多种划分需求。

发布于: 9 小时前阅读数: 10
用户头像

No Silver Bullet 2021.07.09 加入

岂曰无衣 与子同袍

评论

发布
暂无评论
InfoQ 极客传媒 15 周年庆征文|分布式设计介绍_6月月更_No Silver Bullet_InfoQ写作社区