• Feeds

  • 某分布式应用实践一致性哈希的一些问题

    最近项目中一个分布式应用碰到一些设计问题,听过上次技术沙龙key value store漫谈的同学可能会比较容易理解以下说明。

    场景
    假定一个有状态的服务,可以理解成web或者socket服务器,每个用户在这个服务上登录后是有状态的,我们把它的状态连同其他加载到内存的用户数据统称用户session。由于session数据实时会变化,加上程序访问session频率大,几乎所有的操作都跟session数据相关,因此不适合放在远程memcached中

    第一阶段
    考虑到单服务器不能承载,因此使用了分布式架构,最初的算法为 hash() mod n, hash()通常取用户ID,n为节点数。此方法容易实现且能够满足运营要求。缺点是当单点发生故障时,系统无法自动恢复。

    figure1

    第二阶段
    为了解决单点故障,使用 hash() mod (n/2), 这样任意一个用户都有2个服务器备选,可由client随机选取。由于不同服务器之间的用户需要彼此交互,所以所有的服务器需要确切的知道用户所在的位置。因此用户位置被保存到memcached中。

    当一台发生故障,client可以自动切换到对应backup,由于切换前另外1台没有用户的session,因此需要client自行重新登录。

    figure2

    这个阶段的设计存在以下问题

    • 负载不均衡,尤其是单台发生故障后剩下一台会压力过大。
    • 不能动态增删节点
    • 节点发生故障时需要client重新登录

    第三阶段
    打算去掉硬编码的hash() mod n 算法,改用一致性哈希(consistent hashing)分布
    假如采用Dynamo中的strategy 1(可参看Dynamo: Amazon’s Highly Available Key-value Store, PDF, P216)
    我们把每台server分成v个虚拟节点,再把所有虚拟节点(n*v)随机分配到一致性哈希的圆环上,这样所有的用户从自己圆环上的位置顺时针往下取到第一个vnode就是自己所属节点。当此节点存在故障时,再顺时针取下一个作为替代节点。

    figure3

    优点:发生单点故障时负载会均衡分散到其他所有节点,程序实现也比较优雅。

    应用一致性哈希分布后若干问题
    1.如何解决单点故障时候的session迁移?是否所有session都像Dynamo那样写入到多个节点(或双写)?如果双写所有的服务器需要消耗2倍的内存及更多CPU资源,所以优先不考虑双写方案。

    2.如果不双写,则发生故障切换时,即使服务器内部自动帮用户切换节点不重新登录,都需要牵涉到大量session重建,会引起集群震荡。当然这里可以稍微优化,比如session按需建立,IDLE的用户可以先不重建。

    3.当故障节点恢复时候如何处理?Dynamo的策略是故障期间所有的数据都属于hinted handoff, 就是备用机起业务代理作用,一旦故障机恢复就立即把所有临时数据从备用机拉回去,然后整个集群恢复正常流程。但由于本场景session数据比较笨重,而且牵涉到复制时存在并发变更,如果直接借鉴Dynamo的话则感觉切换成本过高,大部分开发人员倾向于继续用备用机处理该用户业务。如果恢复正常后不切换,则存在用户位置的不确定性,使用一致性哈希算出来的结果和用户实际所在的节点不同。需要顺着圆环往下找用户,效率很低。因此就有提议把所有用户所在的当前节点位置写入memcached。

    5. 假如需要将位置写入memcached,那似乎一致性哈希算法又成了花瓶,完全可以由client在create session时候随机选取一个没有故障的节点, 然后把位置写入memcached, 某个节点发生故障时,client再另外选一个随机的,并把新的位置写入memcached, 所有用户所在节点的位置都通过memcached来存储,服务器之间实时的通讯也通过查询memcached来寻址。从实用的角度来看,这样似乎程序更简单。

    因此,一致性哈希分布对于这个场景来说是无用的?

    谈技术人员研究方向

    为了更清楚的看清自己,拿一个成熟工业领域用设计汽车的例子来类比软件设计与开发。

    技术人员的学习与实践有三个层次

    第一层次 了解专业知识与原理

    作为一名汽车设计师,在正式上岗之前,首先要了解汽车的各种原理。如引擎、燃料、悬挂,制动、碟鼓,ABS,风阻,油耗,安全防撞等知识,设计汽车目标并不仅是制造一个漂亮的外壳或者让轮子转起来。相反它一个有机的整体,一个系统的工程,你必须有综合的了解才能进行下一步。

    软件技术也是如此,在入行之前,我们要了解计算机基础知识、操作系统、内存、网络、协议、TCP/IP、数据库/SQL、存储、数据结构、Web,HTML等专业知识。对于每一种知识你要知其然并知其所以然。比如HTML你不能只象一般的IT人员那样简单的知道它是一种描述网页的语言,而是要知其所覆盖范围,其所长和不所长,为什么有了HTML还要用JavaScript/Flash。

    这个层次主要主要目标是知其所用。大部分技术人员应该不局限于长久停留在这一阶段。

    第二层次 掌握工具、搭造环境

    在汽车设计领域就是你的汽车模型建造工具,验证环境,测试工具,分析工具。你要能清晰的知道它们的特性,了解它们的限制及如何去规避。在软件领域,工具主要是指计算机语言,它是你制造原型及最终产品的工具。不同的场景适合不同的工具,合适的工具能帮助你如虎添翼,但语言也并不是多多益善,通常精通2-3种足矣。了解多种不如精通一种。除了工具还要建立自己得心应手的环境,就像比亚迪要建造自己的的汽车试验场一样,有了合适的环境,才能让你能高效的设计,开发,测试及验证。Jon Bentley,世界著名计算机科学家,被誉为影响算法发展的十位大师之一,他在《编程珠玑》中提到贝尔实验室的环境对他成就的巨大帮助

    I came to the Labs because I enjoy balancing the theoretical and the applied, because I want to build products and write books. The pendulum has swung back and forth during my years at the Labs, but my management has always encouraged a wide range of activities.

    能熟练使用工具只是对开发人员最低的要求,代表你有能力开发软件产品。但是你是满足一辈子都是重复造一种QQ车还是有更高的追求。比如在国内,某些行业几乎和10年前没什么区别,比如在企业管理应用领域,10年前用VB/Delphi做企业应用,大家都在谈人脉和关系在项目中的关键作用。10年后不用Delphi了,改用Java/SSH,其它一切如旧。这样的领域,开发人员和打一辈子铁的铁匠没什么区别,大家都是把活干得更熟了,除此之外,所获寥寥。

    第三层次 学会设计
    这时应跳出语言之争与语言迷恋,语言的细节了解得再多,也只是一名工匠,大部分优秀的应用只用到语言及框架不到1/3的特性。你应该寻找有价值的领域深入研究, 就如乔布斯所说

    你的工作将填满你的一大块人生,唯一获得真正满足的方法就是做你相信是伟大的工作,而唯一做伟大工作的方法是爱你所做的事。如果你还没找到这些事,继续找,别停顿。尽你全心全力,你知道你一定会找到。而且,如同任何伟大的关系,事情只会随着时间愈来愈好。
    所以,在你找到之前,继续找,别停顿。

    首先应达到在单个专业领域能够游刃有余,比如如何设计一个简单的网站爬虫。注意有兴趣的设计与干活完成任务的重大区别,你设计的每个产品,它不单是个工作任务,而应该把它看作一个艺术品,这样才能保证你能不断的进步。注意不单要完成功能,否则永远无法达到更高境界。

    下一步设计相对更全面领域的产品,比如考虑一个游戏服务器的方方面面或可以设计一个类twitter系统。慢慢的,你才会有自己积累的东西。

    最后, 什么是开发人员有意义的方向?
    从汽车行业来看,主要任务是学习国外先进公司的经验,在关键领域缩小与其差距,软件开发领域看来也是如此。有些领域虽然高深和有趣,但如果行业暂时需求不大,专注这方面研究难免敝帚自珍。研究业界有需求的领域并寻找有应用需求的场合方可达到开放人员价值的最大化。比如在热门的云存储云计算,虚拟化到不太热门的数据挖掘等都存在一定的空白去填补。最好是在有需求有环境的公司内开展。国内科研机构做表面文章的太多,因此感觉不是合适的场地。

    只有找到你感兴趣的方向,才会达到王国维说的第二境界,“衣带渐宽终不悔,为伊消得人憔悴”。至于更高境界,自然是水到渠成。

    在这个社会里,你只有做出令人尊敬的产品,才能赢得认可。就像Mini设计在汽车行业流芳百世的榜样,在技术行业里,学识,名望,人脉,金钱带来的价值都是可估量的,不可估量的是对行业留下的创新设计,让你无愧于工程师这个称号。

    2009/9/3 on blackberry
    (第一次写说教类的东西,由于视界及眼光高度有限,难免思路局限,仅供自勉)

    广告:广州技术沙龙已经创建Google Group讨论组,主要关注广州及华南地区技术讨论及线下活动组织,欢迎加入

    http://groups.google.com/group/guangzhou-tech-party

    利用Gearman来实现远程监控与管理

    Gearman是一个分发任务的程序框架,可以用在各种场合,与Hadoop相比,Gearman更偏向于任务分发功能。它的任务分布非常简单,简单得可以只需要用脚本即可完成。Gearman最初用于LiveJournal的图片resize功能,由于图片resize需要消耗大量计算资源,因此需要调度到后端多台服务器执行,完成任务之后返回前端再呈现到界面。

    gearman

    Gearman分布式任务实现原理上只用到2个字段,function name和data。function name即任务名称,由client传给job server, job server根据function name选择合适的worker节点来执行。data通常为执行任务所需的自定义的内容,比如简单的做法可以把需要执行的脚本当成data即可(当然要注意其中的安全防范)。如果有多个worker可以处理同一个function name, 则job server会自动分配一个。当用于远程监控场景时,我们可以让每个worker注册成不同的业务名称,以达到方便控制每台worker节点的目的。

    下面介绍Gearman实践中可用于多服务器管理的一些小功能,如果你的服务器有多台,重复执行命令过于繁琐,那可以借助下面一些脚本来完成一些有趣的功能。比如要实时看到所有服务器上的 netstat -nat | grep 80, 按下面步骤搭好环境之后,只需要几行脚本代码即可实现。

    1. 安装gearman
    最早的gearman是perl版的,由于国内开发人员对perl熟悉的不多,所以推荐安装C版本
    http://gearman.org/index.php?id=download
    解开后 ./configure; make; make install 即可

    2. 启动 job server
    cd sbin; ./gearmand -d

    3. 到 worker 节点启动 worker

    先按步骤1在worker机上安装 gearman, 然后创建一个执行worker的python脚本。

    #!/usr/bin/env python
    import os
    
    def main():
        cmd = raw_input()
        print os.popen(cmd).read()
    
    if __name__ == "__main__":
        main()

    cd bin; ./gearman -w -h <job_server_ip> -f server1 — ./monitor.py

    4. 通过远程 web 管理
    JSP示例:

    <%@ page language="java" import="java.util.*,java.util.concurrent.*,
    org.gearman.client.*,org.gearman.common.*"%><%!
            private static String host = "192.168.1.1";
            private static int port = 4730;
            public String testGearman(String func, String data) {
                    byte[] input = data.getBytes();
                    String uniqueId = null;
    
                    GearmanJobServerConnection conn =
                        new GearmanNIOJobServerConnection(host, port);
                    GearmanClient client = new GearmanClientImpl();
                    client.addJobServer(conn);
                    GearmanJob job = GearmanJobImpl.createJob(func, input, uniqueId);
                    Future<GearmanJobResult> f = client.submit(job);
                    GearmanJobResult jr = null;
                    String result = null;
                    try {
                            jr = f.get(3, TimeUnit.SECONDS);
                            result = new String(jr.getResults());
                    } catch (Exception e) {
                            result = e.getMessage();
                    }
    
                    client.shutdown();
                    return result;
            }
    %><%
    out.println(testGearman("test", "netstat -nat | grep 80");
    %>

    PHP示例:

    # Create our client object.
    $client= new GearmanClient();
    
    # Add default server (localhost).
    $client->addServer();
    
    echo "Sending job\n";
    
    # Send reverse job
    $result = $client->do("server1", "netstat -nat | grep 80");
    if ($result)
      echo "Success: $result\n";