找回密码
 用户注册

QQ登录

只需一步,快速开始

查看: 5621|回复: 2

应用中的分布式用法思考和实践

[复制链接]
发表于 2011-11-25 20:01:50 | 显示全部楼层 |阅读模式
最近一直比较忙,每天加班到12点,一周7天,身体实在有点扛不住,0.83版本正在断断续续的开发,争取年前发布出来吧。
ICE的Grid在实际应用中是有一些技巧的,有时候系统性能的下降并不是ICE造成的,而是用法不对。把最近遇到的一些问题和解决方法贴出来,供自己进一步琢磨和一些经验组织,同时,也希望如果你有兴趣的话,慢慢的读以下的文字,或许会激发你的一些想法。在我的理想模式中,逻辑可以被分解在任意一台空闲的服务器中执行,这就带来了几个问题,下面详细说明一下。
如果逻辑服务器我可以启动N台,将来自网关的大量请求压力分压在不同的服务器上:


如何组织逻辑使用的数据?最简单的想法,我找一种支持分布式的缓冲池工具,将我运算中需要的对象存在这些缓冲池中。需要逻辑处理的时候,我从缓冲池中索取数据,这样,实际分布式节点只是负载了我的计算压力。但是仔细想想,这样的做法真的可行吗?这个方法可行性依赖几个条件,第一,你的分布式缓冲池是否是值得信任的。第二,你的分布式缓冲池在大量请求下,可否分布IO不压在同一台机器上。
因为分布式的逻辑系统,实际上解决了CPU的负载,你可以让更多的CPU去运行你的逻辑代码,从而起到并行的作用,但是这个并行是有前提的,那就是首先你的逻辑必须具备可拆分性。也就是逻辑可以独立计算,并不过多依赖更多的方法和不同的数据源。因为,分布式逻辑看上去减少了CPU计算的负荷,但是实际上,性能却是极端依赖IO的,为什么这么说?因为你可以想象一下。简单的逻辑,不需要和别的数据交互的方法,其实实用分布式逻辑是再合适不过的,比如,我买了显卡内存主板,DIY一台自己想要的PC,这个活动不牵扯到别人,自己无论成功和失败都和别人无关。但是,如果再稍微复杂点,我需要给你一个邮包,你签收后我要收到一张回执。这样的需求下,就强调了我需要和别的不属于我的数据或者数据源进行互动。那么,我分布式计算的效果,就取决于我获得别人数据的速度,而这个速度,你不可能只依赖一台机器。(如果有10个分布式逻辑服务器,而只有一台数据源,最后你会发现所有性能都会被这台数据源拖累)。那么,我们来讨论一下,目前就我所知的几种分布式缓冲池对分布式逻辑的支持。
memcahced:一般大家想到的都是这个,不过,在我的实际运用中,却发现这个和分布式逻辑搭配的话,并不是最好的选择,或者说不能是主要选择。为什么这么说?理由如下。首先,memcahced的对象有自己的生命周期,这一点非常好,但是,要命的是,我却不能获得它的对象生命周期终止的事件。因为memcached并不是有IO的,这就意味着,如果我的缓冲服务器内存完蛋了,所有的数据都会烟消云散,这还不是最恐怖的,最恐怖的是它的"触点"(请允许我擅自命名这个名词)。所谓"触点",和memcached的机制有关,memcached的每个数据块是有一个大小的,如果你不配置,会造成无法预知的内存占满的事件发生,而这个事件一旦发生,memcached可不会问你"我满了,可以清除你的数据吗?",它会直接找到快要到期的对象强行从内存清除,腾挪出空间来,当然,此后你就杯具了。当然,你可以说,我可以设置这个参数,但是我想,很多使用memcached的人,并不是设置这个的行家吧。所以,你会发现,你费力设置的触点,依旧有可能在你意料之外被触发。
redis:这个分布式池对于我而言,其实并没有应用在我的实际项目中,不过自己也测试了一下,这个东东非常适合做对象索引,而不适合做大对象的缓冲池,为什么这么说?redis首先不能分布式,至少现在还不行。那么就意味着,所有的请求IO会涌向一个点,这个比较可怕。单点故障姑且不论,数据压力的要求,就会要求IO尽快的返回。所以我觉得这个东西非常适合做一个索引,索引数据对象的位置,让IO去某一个IO上去取得。但是这样的模式,一样存在问题,那就是,我要获取一个对象,我必须至少有几次IO。而你知道,我一般宁可在本地运行1万次空循环,也不想进行一次IO。那个第一是耗时,而这个耗时有时候你是无法预计的。如果那天网卡不给力给你来个罢工,就够你焦头烂额了。
mongoDB:其实这种东西我一直觉得它是一类,为什么这么说,在我看来,NoSQL分为两种,一种是内存式,内存完蛋了它就完蛋了,还有一种是IO式,在内存的基础上,把符合条件的数据存在IO上,就算我内存挂了,只要硬盘没坏,我还是可以找到我的一部分数据。这类东西在我看来,依旧不能达成我的完美方案。理由是,因为现实应用中,对数据对象的生存周期是不可预知的,这不是程序设计的问题,更类似一个哲学问题,To be or not to be,就算有些NoSQL说支持冷热数据的区分,但是这种区分实际上很原始,大多根据访问IO计数,一段时间内访问多的,我就算"热数据",反之就存在IO上。但是现实真的如此吗?你可以想象一下,网站应用,微博应用,我怎么知道下一次,肯定会有用户来访问我定义的"热数据"呢?同样,"冷数据"就肯定不会在下一秒钟被访问吗?所以,现在所谓的冷热数据算法,更多的是一种概率论下的算法,"我猜"我缓冲了尽量多的你想要的数据。
那么,回过头来看看,如果设计一个好的分布式系统(网格),应该具备哪几个因素?
1.我的逻辑可以放在N台服务器去并行运行。
2.我的逻辑服务器在遇到崩溃的时候,客户感觉不出来。
3.我的数据缓冲池必须能支持分布式IO的命中。(N台)
4.我的数据库缓冲池必须是可信任的,至少要有灾难恢复的方案。


下面,我要说的是,逻辑分布网格,我已经找到了一个比较好用的中间件(ICE和TAO都很优秀)
但是,中间件有它的界限,这个界限如果你跨越了,你会发现你的系统变的弱不禁风。甚至到处是BUG。


最近项目中遇到了一些问题,我并没有急着着手去做,而是花了大量的时间,在思考。为什么我会沦落到如此田地?是的,这样的思考,对于我而言,是非常有用的。而且我深刻理解了,没有完美的系统,你一定要知道它擅长做什么,千万不要认为它全能,在工作上是同理的,如果一个黑心老板找一个程序员,说,我知道你是全才,你去给我开发一个windows吧,多半的结果是,程序员会累的接近吐血而死,而实际的产品往往差强人意。软件也一样,如果你想做的完美,你就要学会,给它找帮手,而不是死命的用它,它有帮手了,你也就成功了一大半,这绝对是一个真理。
我犯的错误如下:
1.我看到ICE的文档里,对多对象的一种表现方法,拿唱片来说,我可以在ICE里面建立若干个唱片的对象,而这些对象分布在我的整个ICE集群中,我想要谁就直接获取。于是,我把每个用户数据做了这样的设计,如果我的ICE里面没有找到这个对象,我就会从数据库里面把这个数据对象取出来,放在ICE对象池里面。再在对象池里面做一个LRU,避免对象池过大。
想的很美,是吧,结果教训是惨痛的。当有大量用户涌入我的系统后,我惊奇的发现,我的ICE某一个对象File IO急剧增加,几乎不减,很快就达到了我设置的linux file io上限。结果系统就拒绝服务了。
在用户的大量反馈中,我尝试启动多个逻辑节点,心想,就算我一个ice节点撑不住,我起10个,总应该可以了吧,结果出现了我更意想不到的变化,用户的数据开始出现了大量回滚的现象,本来买入的订单,在节点处理完成后,莫名其妙的消失了。IO继续急剧增加,以至于我的linux服务器都快被IO访问撑爆了。看着屏幕上不断闪烁的数据,我陷入了茫然。
确实,在看到数据file IO增加的第一秒,我已经知道我犯了什么样不可饶恕的错误(因为这个模块当初不是我写的,我也确实没有怀疑过这样的模式会产生什么样的结果,这是我的失误,我只是当初见到这样的设计提醒了同事一句,并没有深究本质,以至于这个错误在实际公测中爆发)。而且,我知道我采用的方式是饮鸩止渴。但是,我想改造的方法,里面有两个问题,还没有想清楚,我必须花时间想明白,在此之前,我的当务之急是先让系统能运行。
于是我写了一个脚本,当一个节点进程fileIO达到1000的时候,我就重启这个节点。谢天谢地,ICE最后时刻还是帮了我,它的Grid模式能让用户在无察觉的情况下重启节点,这个我真要感谢它。
那么,解决思路也很明确,我应该建立一个高速公路的收费站,不在ICE下创建对象节点,而是将所有的数据都放在缓存中,ICE的对象是固定的,只提供interface的功能。不再动态生成对象,所有的数据管理依赖memcached和DB数据源来完成。实际上我的不少模块是如此做的,性能表现目前看来非常不错。
2.我启动了多个ICE进程,结果我错了,ICE缓冲池里出现了大量重复的用户数据,理由是,我在每个ICE节点所创建的对象,都是基于当前节点的上下文完成的。也就是说,最极端的情况下,我的每个节点都可能会有一份对象数据。杯具的是,ICE的驱逐器似乎我用的不对,在用户达到上限的时候,数据并备有被成功的驱逐出去。产生多个对象的本质原因是我使用了多个上下文。或许,我用一个上下文,或者用主节点的上下文是否会好些?结果这样的改造结果直接让我的主节点崩溃了。塞的慢慢的IO,ICE不用干别的了。
对于这个,我依旧可以用上述的方案解决。不过,如果用户继续增加,逻辑节点的能力会越来越压在访问缓存的IO上。这个IO将会面临两种方式的压力,一种是对单点的压力,一种是对群访问的压力。后一种可以通过添加物理服务器分压。不过第一种就比较麻烦了,综合考量,我决定采用以下的方法,在所有的对象调用的时候,在缓存上增加一个引用计数标记和时间戳,如果某数据已经被频繁加载达到我的上限,我会自动将这些数据在memcached上生成多份拷贝,此后的对此用户请求,都会随机分到一个拷贝上。对于这个拷贝的对象修改,我只要同步修改这些对象即可,那这里还有一个问题,就是怎么处理数据同步的问题。因为,我很可能正在修改这个对象的拷贝,而另一个对象没有修改。这样就会造成数据不一致的问题,时间戳就是派这个用场的,这个时间戳存在另一个地方,对应对象类型,用户通过访问随机复制对象的时候,先从这个结构里找到最新的时间戳(无论这里谁修改了这个对象,我都会更新这个时间戳),然后拿着这个时间戳,去找当前对象时间戳相等的对象,随机返回一份。这样,我就解决了单一热点的问题。
图示如下:
objectIndexPool
key1<数据类型>   Value<最后一个写入时间的秒值><objKey List 对象所在的key命名数组>
key2<数据类型>   Value<最后一个写入时间的秒值><objKey List 对象所在的key命名数组>
。。。。。。
这是一个索引:
下面是这个对象的数据结构
objectPoool
objkey1<数据类型>    Value<IO计数器><最后一个写入时间的秒值><对象内容>
objkey2<数据类型>    Value<IO计数器><最后一个写入时间的秒值><对象内容>


获取流程是:无论怎么取得数据对象,都从key中获得
先获得数据类型,从key1数据类型获得objKey数组,然后从中去命中时间戳匹配的objkey对象,这样就可以匹配的数据。
不过你可能会问,我每获得一个对象,我岂不要至少支付4次IO?两次一来一回。是的,我是用通过建立中间层来分布数据命中,如果某对象单点访问过多,用这种方法,可以把key分布出去。比如类似明星微博的访问。
对于一般的数据,直接去获取objkey1即可。
第二个改进,就是我如何处理数据源的IO和memcached的匹配。
我专门启动了一个ICE的数据库节点。其实,这个节点可以是多个。借助ICE的特性,我每次需要进行数据库动作的时候,都往这个节点中发送一个信息,把要更新的SQL放进去。然后这个节点的所有实例,都是一个队列,慢慢的去往数据库里面加就好了。
其实这么做还必须有一个前提,就是一定要确保,你的memcached对象生命周期尽量要高于数据库队列数据吞吐时间。
举个例子来说
我有一个数据对象,比如生命周期是30秒,那么,一定要确保,我的数据库队列30秒之内必须能处理完这个对象的存储,否则,就会出现数据不一致的现象。也就是数据回滚现象。这个可以通过设计上去弥补。
恩,先写到这里,以上部分实现出来,部署在我的ICE节点上过两周看看。
再说说ICE单节点多实例的应用,我觉得这个是一个非常实用的功能,我之前的看法是,如果一个节点压力过大,我可以开两个相同的节点,这是可以做到的,单实际上最好用的是一个节点多个实例。而这些实例,是可以在多个服务器上部署的。这样就可以实现,一个节点,只要不是所有节点都同时宕机,服务就会正常提供。非常好的功能。
那么,怎么在ICE怎么操作呢?
首先,写一个XML,具体有兴趣的朋友,可以看我之前写过的一些ICE基础文章。
  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <!-- This file was written by IceGrid Admin -->
  3. <icegrid>
  4.    <application name="TestServer">
  5.                 <server-template id="TestTemplate">
  6.       <parameter name="index"/>
  7.       <server id="TestTemplate-${index}" exe="python" activation="on-demand">
  8.         <option>TextServer.py</option>
  9.         <adapter name="TestAdapter" endpoints="tcp" replica-group="ReplicatedMapAdapter"/>
  10.         <property name="Identity" value="TestCenter/Center"/>
  11.                                 <property name="Ice.ThreadPool.Server.SizeMax" value="1000"/>
  12.                                 <log path="${server}.log" property="LogFile"/>
  13.                                 <description>测试服务器</description>
  14.       </server>
  15.     </server-template>
  16.    
  17.           <replica-group id="ReplicatedMapAdapter">
  18.       <load-balancing type="round-robin"/>
  19.       <object identity="TestCenter/Center" type="::TestCenter::Center"/>
  20.     </replica-group>
  21.    
  22.                 <node name="node-Test[1]">
  23.                         <server-instance template="TestTemplate" index="1"/>
  24.                         <server-instance template="TestTemplate" index="2"/>
  25.                 </node>
  26.                 <node name="node-Test[2]">
  27.                         <server-instance template="TestTemplate" index="3"/>
  28.                 </node>
  29.    </application>
  30. </icegrid>
复制代码

ICE有好几种实例命中方式,round-robin是平均分配。具体可以参看ICE文档。
其他写法和正常的ICE实例一致。
这样的对象创建方法,在每个实例里,实际上是多份,在这里也谨记一下,除特殊需求,这样用会造成对象叠加。
  1. #!/usr/bin/env python
  2. # -*- coding:utf-8 -*-#
  3. '''
  4. Created on 2011-11-21
  5. @author: freeeyes
  6. '''
  7. import Ice
  8. import sys
  9. import traceback
  10. Ice.loadSlice('-I. --all ../slice/TestCenter.ice')
  11. import TestCenter
  12. #City的实现类
  13. class CityI(TestCenter.City):
  14.     def __init__(self, nID):
  15.         self.mnID    = nID
  16.         self.mnVisit = 0
  17.         
  18.     def GetID(self, current=None):
  19.         print "[GetID]Begin mnVisit=" + str(self.mnVisit)
  20.         self.mnVisit = self.mnVisit + 1
  21.         print "[GetID]End mnVisit=" + str(self.mnVisit)
  22.         return self.mnVisit
  23.    
  24. #
  25. class CenterI(TestCenter.Center):
  26.     def __init__(self):
  27.         self.m_cityPrxList = {}
  28.    
  29.     def getCityList(self, current=None):
  30.         for res in range(1, 11):
  31.             name = current.adapter.getCommunicator().stringToIdentity("CITY_TEST_%s" % (res))
  32.             cityPrx = current.adapter.find(name)
  33.             if cityPrx is None:
  34.                 print "[getCityList]Create Object"
  35.                 print current.adapter.getName()
  36.                 objcity = CityI(res)
  37.                 cityPrx = TestCenter.CityPrx.uncheckedCast(current.adapter.add(objcity, name))
  38.             else:
  39.                 print "[getCityList]Get object"
  40.                 cityPrx = TestCenter.CityPrx.uncheckedCast(current.adapter.createProxy(name))
  41.             print "[getCityList]key=" + str(res)
  42.             self.m_cityPrxList[res] = cityPrx
  43.         return True   
  44.             
  45.     def GetCity(self, nID, current=None):
  46.         objcity = self.m_cityPrxList.get(nID, None)
  47.         if objcity is None:
  48.             print "[GetCity] is None"
  49.             return -1;
  50.         else:
  51.             nData = objcity.GetID()
  52.             print "[GetCity]nData=" + str(nData)
  53.             return nData
  54.         
  55. class Server(Ice.Application):
  56.     def run(self, args):
  57.         if len(args) > 1:
  58.             print self.appName() + ": too many arguments"
  59.             return 1
  60.         properties = self.communicator().getProperties()
  61.         adapter = self.communicator().createObjectAdapter("TestAdapter")
  62.         id = self.communicator().stringToIdentity("TestCenter/Center")
  63.         adapter.add(CenterI(), id)
  64.         adapter.activate()
  65.         self.communicator().waitForShutdown()
  66.         return 0
  67. app = Server()
  68. sys.exit(app.main(sys.argv))
复制代码
发表于 2011-11-26 11:21:54 | 显示全部楼层
眼总确实有写书的天赋
发表于 2012-1-12 14:32:28 | 显示全部楼层
围观围观~~~~
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

Archiver|手机版|小黑屋|ACE Developer ( 京ICP备06055248号 )

GMT+8, 2024-4-28 15:31 , Processed in 0.020079 second(s), 9 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表