`
yde986
  • 浏览: 97535 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

分布式设计与开发

阅读更多

 

分布式设计与开发(一)------宏观概述

 

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

分布式可繁也可以简,最简单的分布式就是大家最常用的,在负载均衡服务器后加一堆web服务器,然后在上面搞一个缓存服务器来保存临时状态,后面共享一个数据库,其实很多号称分布式专家的人也就停留于此,大致结构如下图所示:



 

这种环境下真正进行分布式的只是web server而已,并且web server之间没有任何联系,所以结构和实现都非常简单。

有些情况下,对分布式的需求就没这么简单,在每个环节上都有分布式的需求,比如Load BalanceDBCache和文件等等,并且当分布式节点之间有关联时,还得考虑之间的通讯,另外,节点非常多的时候,得有监控和管理来支撑。这样看起来,分布式是一个非常庞大的体系,只不过你可以根据具体需求进行适当地裁剪。按照最完备的分布式体系来看,可以由以下模块组成:



 

分布式任务处理服务:负责具体的业务逻辑处理

分布式节点注册和查询:负责管理所有分布式节点的命名和物理信息的注册与查询,是节点之间联系的桥梁

分布式DB:分布式结构化数据存取

分布式Cache:分布式缓存数据(非持久化)存取

分布式文件:分布式文件存取

网络通信:节点之间的网络数据通信

监控管理:搜集、监控和诊断所有节点运行状态

分布式编程语言:用于分布式环境下的专有编程语言,比如ElangScala

分布式算法:为解决分布式环境下一些特有问题的算法,比如解决一致性问题的Paxos算法

因此,若要深入研究云计算和分布式,就得深入研究以上领域,而这些领域每一块的水都很深,都需要很底层的知识和技术来支撑,所以说,对于想提升技术的开发者来说,以分布式来作为切入点是非常好的,可以以此为线索,探索计算机世界的各个角落。

 

 

 



 

 

 

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

ZooKeeper的应用领域

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

1)分布式Barrier

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



  

 

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

2 分布式 Queue

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

/**

 * 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, new byte[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就会根据前面所讲的拿锁规则来竞争锁。

 



 

 

其实,相对于垂直切分更进一步的是服务化改造,说得简单就是要把原来强耦合的系统拆分成多个弱耦合的服务,通过服务间的调用来满足业务需求看,因此表拆出来后要通过服务的形式暴露出去,而不是直接调用不同模块的表,淘宝在架构不断演变过程,最重要的一环就是服务化改造,把用户、交易、店铺、宝贝这些核心的概念抽取成独立的服务,也非常有利于进行局部的优化和治理,保障核心模块的稳定性。这样一种拆分方式也是有代价的:

表关联无法在数据库层面做

单表大数据量依然存在性能瓶颈

事务保证比较复杂

应用端的复杂性增加

上面这些问题是显而易见的,处理这些的关键在于如何解除不同模块间的耦合性,这说是技术问题,其实更是业务的设计问题,只有在业务上是松耦合的,才可能在技术设计上隔离开来。没有耦合性,也就不存在表关联和事务的需求。另外,大数据瓶颈问题可以参见下面要将的水平切分。

2)水平切分

上面谈到垂直切分只是把表按模块划分到不同数据库,但没有解决单表大数据量的问题,而水平切分就是要把一个表按照某种规则把数据划分到不同表或数据库里。例如像计费系统,通过按时间来划分表就比较合适,因为系统都是处理某一时间段的数据。而像SaaS应用,通过按用户维度来划分数据比较合适,因为用户与用户之间的隔离的,一般不存在处理多个用户数据的情况,下面是一个比较简单的按user_id来水平切分的例子:



 

 

水平切分没有破坏表之间的联系,完全可以把有关系的表放在一个库里,这样就不影响应用端的业务需求,并且这样的切分能从根本上解决大数据量的问题。它的问题也是很明显的:

当切分规则复杂时,增加了应用端调用的难度

数据维护难度比较大,当拆分规则有变化时,需要对数据进行迁移

对于第一个问题,可以参考后面要讲的如何整合应用端和数据库端。对于第二个问题可以参考一致性hash的算法,通过某些映射策略来降低数据维护的成本,可参见以前的博文分布式设计与开发(二)------几种必须了解的分布式算法

3)垂直与水平联合切分

由上面可知垂直切分能更清晰化模块划分,区分治理,水平切分能解决大数据量性能瓶颈问题,因此常常就会把两者结合使用,这在大型网站里是种常见的策略,这可以结合两者的优点,当然缺点就是比较复杂,成本较高,不太适合小型网站,下面是结合前面两个例子的情况:



 

 

与应用程序端的整合策略

数据切出来还只是第一步,关键在于应用端如何方便地存取数据,不能因为数据拆分导致应用端存取数据错误或者异常复杂。按照从前往后一般说来有以下三种策略:

应用端做数据库路由

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

数据库端自行做路由

1 应用端做数据库路由

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

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

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

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

数据库请求路由可配置性

对调用命令和SQL的解析

调用结果的过滤和合并

现在有些开源框架提供了类似功能,比如ameoba,在以前博文设计与开发应用服务器(一)------常见模式 中介绍过ameoba的大致结构,在构建高性能web之路------mysql读写分离实战 介绍过如何实战ameoba,有兴趣的朋友可以参考一下。

3)数据库端自行做路由

例如MySQL就提供了MySQL Proxy的代理产品可以在数据库端做路由,结构如下所示:



 

 

这种方式的最大问题就是拆分规则配置的灵活性不好,不一定能满足应用端的多种划分需求。

以上介绍了些数据拆分的策略和相关支撑策略,随后会研究一下前面谈到的数据库高可用架构。

 

 

 

分布式设计与开发(五)------数据库高可用架构

 

数据库高可用架构对于我们这些应用端开发的人来说是一个比较陌生的领域,是在具体的数据库产品之上搭建的环境,需要像DBA这样对数据库产品有足够的了解才能有所涉及,虽然不能深入其中,但可以通过一些经典的高可用架构学习其中的思想。就我所了解到的有以下几种:

MySQL Replication

MySQL Cluster

Oracle RAC

IBM HACMP

Oracle ASM

MySQL Replication

MySQL Replication就是通过异步复制多个copy以达到提高可用性的目的,常规的复制架构有以下几种:

Master-Slaves

Master-Master

Master-Master-Salves

1Master-Slaves

Master-Slaves是最常用的提高可用的方法,特别是在互联网应用中,读远远大于写,因此提高读的可用性是首当其中的,Master-Slaves就是让写的操作集中在一台数据库Master上,然后这个Master会把更新的操作复制到其他数据库Slaves上,读的操作都发生在Slaves上,架构图如下所示:



 

如上图在SlaveC不可用时,读和写都不会中断,等SlaveC恢复后会自动同步丢失的数据,又能重新投入运转,可维护性非常好。但如果Master有问题就麻烦了,因此它只解决了读的高可用性,但不保证写的高可用性。关于Master-Slaves的实战可参考以前的一篇博文构建高性能web之路------mysql读写分离实战

2Master-Master

为解决上面谈的写的高可用性,MySQL提供了Master-Master的复制架构,如下所示:



 

一般说来都向MasterA写,MasterA同步数据到MasterB,当MasterA有问题时,会自动切换到MasterB,等MasterA恢复时,MasterB同步数据到MasterA

3Master-Master-Salves

Master-Master-Salves是结合上面两种方案,是一种同时提供读和写高可用的复制架构,如下图所示:



 

MySQL Cluster

MySQL Cluster主要由三个部分组成:

SQL服务器节点

NDB数据存储节点

监控和管理节点

三个部门的组成结构如下图所示:



 

这样的分层也是由MySQL本身把SQL处理和存储分开的架构相关系的,关于MySQL的架构可见以前的博文设计与开发应用服务器(一)------常见模式

这样一来MySQL Cluster就可以分别在SQL处理和存储两个层次上做高可用的复制策略。在SQL处理层次上,比较容易做集群,因为这些SQL处理是无状态性的,完全可以通过增加机器的方式增强可用性。在存储层次上,通过对每个节点进行备份的形式增加存储的可用性,这类似与MySQL Replication,结构图如下所示:



 

Oracle RAC

Oracle RACMySQL Cluster有些相似,但主要集中在SQL处理层的高可用性,而在存储上体现不多,结构图如下所示:



 

它的主要优点就是对应用透明,并且通过Heartbeat检测可用性非常高,主要缺点就是存储是共享的,存储上可扩展能力不足。

IBM HACMP

IBM HACMPOracle RAC也是类似,主要用于双机互备,运行流程如下所示:

1)作为双机系统的两台服务器(主机AB)同时运行在Hacmp环境中;

2)服务器除正常运行自机的应用外,同时又作为对方的备份主机;

3)两台主机系统(AB)在整个运行过程中,通过 “心跳线”相互监测对方的运行情况(包括系统的软硬件运行、网络通讯和应用运行情况等);

4)一旦发现对方主机的运行不正常(出故障)时,故障机上的应用就会立即停止运行,本机(故障机的备份机)就会立即在自己的机器上启动故障机上的应用,把故障机的应用及其资源(包括用到的IP地址和磁盘空间等)接管过来,使故障机上的应用在本机继续运行;

5)应用和资源的接管过程由Ha软件自动完成,无需人工干预;

6)当两台主机正常工作时,也可以根据需要将其中一台机上的应用人为切换到另一台机(备份机)上运行。

Oracle ASM

Oracle ASM主要提供存储的可扩展性,通过自动化的存储管理加上后端可扩展性的存储阵列达到高可用性,结构图如下所示:



 

因此,可以尝试把Oracle RACASM组合起来使用,同时提供SQL处理和存储的高可用性,这也是MySQL Cluster想达到的效果

 

 

分布式设计与开发(六)------memcached分布式

 

memcached是应用最广的开源cache产品,它本身不提供分布式的解决方案,我猜想一方面它想尽量保持产品简单高效,另一方面cachekey-value的特性使得让memcached分布式起来比较简单。memcached的分布式主要在于客户端,通过客户端的路由处理来搭建memcached集群环境,因此在服务端,memcached集群环境实际上就是一个个memcached服务器的堆积品,环境的搭建比较简单。下面从客户端做路由和服务端集群环境搭建两方面来谈如何让memcached分布式

客户端做路由

客户端做路由的原理非常简单,应用服务器在每次存取某keyvalue时,通过某种算法把key映射到某台memcached服务器nodeA上,因此这个key所有操作都在nodeA上,结构图如下所示:

存储某个key-value

 

 

取某个key-value



 

因此关键在于算法的选择,最基本的要求就是能让数据平均到所有服务器上。这自然而然让我想到了hash算法,spymemcached是一个用得比较广的java客户端,它就提供了一种简单的hash算法,实现类为ArrayModNodeLocator,从key映射到node的源码如下:

public MemcachedNode getPrimary(String k) { 

    return nodes[getServerForKey(k)]; 

} 

private int getServerForKey(String key) { 

    int rv=(int)(hashAlg.hash(key) % nodes.length); 

    assert rv >= 0 : "Returned negative key for key " + key; 

    assert rv < nodes.length 

        : "Invalid server number " + rv + " for key " + key; 

    return rv; 

} 

         public MemcachedNode getPrimary(String k) {

                   return nodes[getServerForKey(k)];

         }

         private int getServerForKey(String key) {

                   int rv=(int)(hashAlg.hash(key) % nodes.length);

                   assert rv >= 0 : "Returned negative key for key " + key;

                   assert rv < nodes.length

                            : "Invalid server number " + rv + " for key " + key;

                   return rv;

         }

从上面可知它是把所有node放在数组里,通过hash算法把key映射到某index,然后通过这个index在数组里取node

再则需要考虑如何容错,比如当某个node当掉了,如何自动地转到其他node上,上面的简单hash路由策略采用的方法是在数据组里顺序向下轮询node,找第一个工作正常的node即可。

最后要考虑当需要移除node或添加node的时候,如何有效地调整映射关系,这自然又让我们想到一致性hash算法,关于一致性hash算法就不多说,博文分布式设计与开发(二)------几种必须了解的分布式算法 有所涉及,这里可以看看spymemcached是如何利用这个算法来做路由的,实现类为KetamaNodeLocator,从key映射到node的源码如下:

public MemcachedNode getPrimary(final String k) { 

    MemcachedNode rv=getNodeForKey(hashAlg.hash(k)); 

    assert rv != null : "Found no node for key " + k; 

    return rv; 

} 

MemcachedNode getNodeForKey(long hash) { 

    final MemcachedNode rv; 

    if(!ketamaNodes.containsKey(hash)) { 

        // Java 1.6 adds a ceilingKey method, but I'm still stuck in 1.5  

        // in a lot of places, so I'm doing this myself.  

        SortedMap<Long, MemcachedNode> tailMap=ketamaNodes.tailMap(hash); 

        if(tailMap.isEmpty()) { 

            hash=ketamaNodes.firstKey(); 

        } else { 

            hash=tailMap.firstKey(); 

        } 

    } 

    rv=ketamaNodes.get(hash); 

    return rv; 

} 

         public MemcachedNode getPrimary(final String k) {

                   MemcachedNode rv=getNodeForKey(hashAlg.hash(k));

                   assert rv != null : "Found no node for key " + k;

                   return rv;

         }

         MemcachedNode getNodeForKey(long hash) {

                   final MemcachedNode rv;

                   if(!ketamaNodes.containsKey(hash)) {

                            // Java 1.6 adds a ceilingKey method, but I'm still stuck in 1.5

                            // in a lot of places, so I'm doing this myself.

                            SortedMap<Long, MemcachedNode> tailMap=ketamaNodes.tailMap(hash);

                            if(tailMap.isEmpty()) {

                                     hash=ketamaNodes.firstKey();

                            } else {

                                     hash=tailMap.firstKey();

                            }

                   }

                   rv=ketamaNodes.get(hash);

                   return rv;

         }

这段代码非常清晰,就是通过ketamaNodes这个数据结构按照一致性hash算法把node分区,每次都把映射到一个分区的key对于到负责这个分区的node上。

从上面几段代码和图示,我们大致能弄明白在客户端如何做路由来让memcached分布式,其实在大多数的项目中,以上这些简单的处理办法就足够了

memcached服务端集群

由上面可知一般的应用中memcached服务端集群不用做太多工作,部署一堆memcached服务器就可以了,大不了就是要做好监控的工作,但像facebook这样的大型互联网应用,并且又是那么依赖memcached,集群的工作就很有学问了。今年Qcom的会议上facebook就介绍了是如何通过扩展memcached来应付这么多数据量的,ppt可见Facebook的扩展Memcached实战。这个PPT比较抽象,我没看得太懂,并且facebook也只是蜻蜓点水,没透露太多的细节,但公司的资深架构师陈大峰同学做了些解析,才多多少少有点眉目。facebook所有数据的存取都基本上是在memcached上完成,后端的数据库mysql仅仅只是做持久化的作用,由于数据量巨大,做了类似与mysql的读写分离的结构,结构图如下所示:



 

其中非常重要的一点,当Westmemcached要向East同步数据的时候,它没有采取memcached之间的同步,而是走MySQL replication,如下图所示:



 

这么做的原因我没法搞得太清楚,大概是比较信赖MySQL replication的简单稳定吧,并且像sns这种应用本身就不需要即时一致性,只要最终一致就行了。

另外在数据分布上是很有讲究的,facebook上面有很多很热的数据,比如LadyGaGa发布一条消息,将会有千万的人收到这个消息,如何把LadyGaGa和普通的用户同等对待就很可能会把这个memcached节点搞垮,甚至访问冲向后面的数据库后会把数据搞垮,如下图所示:



 

因此就需要一些策略来控制这些热点数据和热点访问,这些策略细节是什么facebook没说太清楚,一般说来可以把热点数据分布到其他节点,另外对于数据库可以加锁控制流量,只有拿到锁的访问才能直接访问数据库,没拿到的需要等候和竞争。

另外一个数据分布的难题是每个用户可能会有成百上千的好友,而这些好友的数据分布在成百上千台的memcached的节点,这样一个客户端就需要连接成千上万的memcached的节点,如下图所示:



 

这种问题一般说来可以采取数据重组,把有关联的数据重组在一起,而不是分布在n台机器上。

以上的这些facebook的实践只能说是走马观花地看看了,从我们可以看到一个简简单单memcached也能完成这么多玩样来,可以猜想到facebook的那些天才工程师们在亿万数据压力下被逼出了多少创新的设计,这些设计不一定适用于我们,不了解情景也没办法深究里面的细节,我们要做的是围绕我们自己的应用,让memcached玩出点味道来。

 

 

 

 

 

 

 

 

 

 

分布式设计与开发(四)------数据拆分

 

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

数据拆分

数据库高可用架构

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

切分策略

与应用程序端的整合策略

切分策略

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

1)垂直切分

垂直切分就是要把表按模块划分到不同数据库中,这种拆分在大型网站的演变过程中是很常见的。当一个网站还在很小的时候,只有小量的人来开发和维护,各模块和表都在一起,当网站不断丰富和壮大的时候,也会变成多个子系统来支撑,这时就有按模块和功能把表划分出来的需求。如下图所示:

分布式设计与开发(二)------几种必须了解的分布式算法

 

分布式设计与开发中有些疑难问题必须借助一些算法才能解决,比如分布式环境一致性问题,感觉以下分布式算法是必须了解的(随着学习深入有待添加):

Paxos算法

一致性Hash算法

Paxos算法

1)问题描述

分布式中有这么一个疑难问题,客户端向一个分布式集群的服务端发出一系列更新数据的消息,由于分布式集群中的各个服务端节点是互为同步数据的,所以运行完客户端这系列消息指令后各服务端节点的数据应该是一致的,但由于网络或其他原因,各个服务端节点接收到消息的序列可能不一致,最后导致各节点的数据不一致。举一个实例来说明这个问题,下面是客户端与服务端的结构图:

 



 

 

client1client2client3分别发出消息指令ABC时,Server1~4由于网络问题,接收到的消息序列就可能各不相同,这样就可能由于消息序列的不同导致Server1~4上的数据不一致。对于这么一个问题,在分布式环境中很难通过像单机里处理同步问题那么简单,而Paxos算法就是一种处理类似于以上数据不一致问题的方案。

2)算法本身

算法本身我就不进行完整的描述和推导,网上有大量的资料做了这个事情,但我学习以后感觉莱斯利·兰伯特(Leslie Lamportpaxos算法的奠基人,此人现在在微软研究院)的Paxos Made Simple 是学习paxos最好的文档,它并没有像大多数算法文档那样搞一堆公式和数学符号在那里吓唬人,而是用人类语言让你搞清楚Paxos要解决什么问题,是如何解决的。这里也借机抨击一下那些学院派的研究者,要想让别人认可你的成果,首先要学会怎样让大多数人乐于阅读你的成果,而这个描述Paxos算法的文档就是我们学习的榜样。

言归正传,透过Paxos算法的各个步骤和约束,其实它就是一个分布式的选举算法,其目的就是要在一堆消息中通过选举,使得消息的接收者或者执行者能达成一致,按照一致的消息顺序来执行。其实,以最简单的想法来看,为了达到大伙执行相同序列的指令,完全可以通过串行来做,比如在分布式环境前加上一个FIFO队列来接收所有指令,然后所有服务节点按照队列里的顺序来执行。这个方法当然可以解决一致性问题,但它不符合分布式特性,如果这个队列down掉或是不堪重负这么办?而Paxos的高明之处就在于允许各个client互不影响地向服务端发指令,大伙按照选举的方式达成一致,这种方式具有分布式特性,容错性更好。

说到这个选举算法本身,可以联想一下现实社会中的选举,一般说来都是得票者最多者获胜,而Paxos算法是序列号更高者获胜,并且当尝试提交指令者被拒绝时(说明它的指令所占有的序列号不是最高),它会重新以一个更好的序列参与再次选举,通过各个提交者不断参与选举的方式,达到选出大伙公认的一个序列的目的。也正是因为有这个不断参与选举的过程,所以Paxos规定了三种角色(proposeracceptor,和 learner)和两个阶段(acceptlearn),三种角色的具体职责和两个阶段的具体过程就见Paxos Made Simple ,另外一个国内的哥们写了个不错的PPT ,还通过动画描述了paxos运行的过程。不过还是那句话不要一开始就陷入算法的细节中,一定要多想想设计这些游戏规则的初衷是什么。

Paxos算法的最大优点在于它的限制比较少,它允许各个角色在各个阶段的失败和重复执行,这也是分布式环境下常有的事情,只要大伙按照规矩办事即可,算法的本身保障了在错误发生时仍然得到一致的结果。

3)算法的实现

Paxos的实现有很多版本,最有名的就是google chubby ,不过看不了源码。开源的实现可见libpaxos 。另外,ZooKeeper 也基于paxos解决数据一致性问题,也可以看看它是如果实现paxos的。

4)适用场景

弄清楚paxos的来龙去脉后,会发现它的适用场景非常多,Tim有篇blogPaxos在大型系统中常见的应用场景》 专门谈这个问题。我所见到的项目里,naming service是运用Paxos最广的领域,具体应用可参考ZooKeeper

一致性Hash算法

1)问题描述

分布式常常用Hash算法来分布数据,当数据节点不变化时是非常好的,但当数据节点有增加或减少时,由于需要调整Hash算法里的模,导致所有数据得重新按照新的模分布到各个节点中去。如果数据量庞大,这样的工作常常是很难完成的。一致性Hash算法是基于Hash算法的优化,通过一些映射规则解决以上问题

2)算法本身

对于一致性Hash算法本身我也不做完整的阐述,有篇blog《一致性hash算法 - consistent hashing 描述这个算法非常到位,我就不重复造轮子了。

实际上,在其他设计和开发领域我们也可以借鉴一致性Hash的思路,当一个映射或规则导致有难以维护的问题时,可以考虑更一步抽象这些映射或规则,通过规则的变化使得最终数据的不变。一致性hash实际就是把以前点映射改为区段映射,使得数据节点变更后其他数据节点变动尽可能小。这个思路在操作系统对于存储问题上体现很多,比如操作系统为了更优化地利用存储空间,区分了段、页等不同纬度,加了很多映射规则,目的就是要通过灵活的规则避免物理变动的代价

3)算法实现

一致性Hash算法本身比较简单,不过可以根据实际情况有很多改进的版本,其目的无非是两点:

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

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

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

以上两个算法在我看来就算从不涉及算法的开发人员也需要了解的,算法其实就是一个策略,而在分布式环境常常需要我们设计一个策略来解决很多无法通过单纯的技术搞定的难题,学习这些算法可以提供我们一些思路。

 

 

 

分布式设计与开发(三)------高一致性服务ZooKeeper

 

分布式环境中大多数服务是允许部分失败,也允许数据不一致,但有些最基础的服务是需要高可靠性,高一致性的,这些服务是其他分布式服务运转的基础,比如naming service、分布式lock等,这些分布式的基础服务有以下要求:

高可用性

高一致性

高性能

对于这种有些挑战CAP原则 的服务该如何设计,是一个挑战,也是一个不错的研究课题,ApacheZooKeeper也许给了我们一个不错的答案。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 

其中zoo1zoo2分别对应集群中各服务器的机器名或ipserver.1server.212分别对应各服务器的zookeeper idid的设置方法为在dataDir配置的目录下创建名为myid的文件,并把id作为其文件内容即可,在本例中就分为设置为12。其他配置具体含义可见官方文档。

3)启动集群环境

分别在各服务器下运行zookeeper启动脚本

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

4)应用zookeeper

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

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

bin/zkCli.sh -server 10.20.147.35:2181

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

java中通过调用程序接口来应用zookeeper较为复杂一点,需要了解watchcallback等概念,不过试验最简单的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(); 

    } 

} 

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

ZooKeeper的实现机理

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

1ZooKeeper的基本原理

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

2ZooKeeper的基本运转流程

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

选举Leader

同步数据

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

Leader要具有最高的zxid 

集群中大多数的机器得到响应并follow选出的Leader

同步数据这个流程是ZooKeeper的精髓所在,并且就是Fast Paxos算法的具体实现。一个牛人画了一个ZooKeeper数据流动图,比较直观地描述了ZooKeeper是如何同步数据的。

  • 大小: 29 KB
  • 大小: 41.8 KB
  • 大小: 41 KB
  • 大小: 29.5 KB
  • 大小: 27.8 KB
  • 大小: 47.4 KB
  • 大小: 55.2 KB
  • 大小: 61.7 KB
  • 大小: 39.9 KB
  • 大小: 37.3 KB
  • 大小: 29.9 KB
  • 大小: 44.5 KB
  • 大小: 59.9 KB
  • 大小: 54.7 KB
  • 大小: 81.2 KB
  • 大小: 48.2 KB
  • 大小: 42.3 KB
  • 大小: 44.7 KB
  • 大小: 39.8 KB
  • 大小: 46.6 KB
  • 大小: 32.7 KB
  • 大小: 38.1 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics