高可用高性能系统

转自 http://blog.csdn.net/romandion/article/details/2697238 

         && http://blog.csdn.net/romandion/article/details/2698067

         && http://blog.csdn.net/romandion/article/details/2705629

         && http://blog.csdn.net/romandion/article/details/2731703

         && http://blog.csdn.net/romandion/article/details/2772241

         && http://blog.csdn.net/romandion/article/details/2772246

         && http://blog.csdn.net/romandion/article/details/2772259

         && http://blog.csdn.net/romandion/article/details/2772329

         && http://blog.csdn.net/romandion/article/details/2772334

         && http://blog.csdn.net/romandion/article/details/2772345

         && http://blog.csdn.net/romandion/article/details/1800025

         &&http://blog.csdn.net/romandion/article/details/3929738

         &&http://blog.csdn.net/romandion/article/details/3953353




高可用高性能系统(一)系统应用场景

      建设一个高可用高性能的系统是我最近几年的努力目标,但其中涉及的内容颇多,都是些零星的经验,缺乏系统性架构总结。写这个系列的文章其实是我一直以来的想法,不过题材和内容过多,所以一直搁置。前些日子到一家公司面试,备受打击,觉得有必要把它总结一下。
      我假定要为期货交易建设一个高可用高性能的系统,那么期货交易就是系统应用场景。期货交易的诸多规则我们没有必要理会,我们只需要简化出一个交易场景,需要使用高可用高性能的系统。之所以选择期货交易,是因为现在正供职于一家期货交易系统软件提供商,而且期货交易也需要这样的系统。
      我们假设的场景是这样的。有家期货经纪公司,需要为他的很多客户提供期货交易界面,这个界面需要连接到多个交易所。每个客户都希望以最短的时间从交易所那里获取行情,并提交交易合约。由于期货交易是T+0原则,所以延时和中断,都可能导致交易失败和大量的经济损失。每笔交易涉及金额可能十分巨大,千万甚至亿级别,所以不允许任何数据上的差错。
      上面的场景对全年不一定要求无中断,但在交易时间内对可用性要求同样苛刻。交易的实时性,对性能要求也是很高的。于是,为期货交易的场景建立高可用高性能的系统从应用需求角度来看,是完全足备的。
      当客户来到交易终端前,用自己的账户登录系统,查询下自己的资金,如果不够,那么需要转入更多的资金到经纪公司账户。界面上不断刷新各种合约以及新的行情,在合适的行情和合约出现时,这个客户期待以最快的速度吃下他,并期望能够获得马上的成交。
      可是,当客户提交交易请求时,经纪公司需要知道这个客户是否有足够的资金,否则经纪公司必然承当巨大的经纪损失。对于客户来说,当然希望这个过程足够短,否则就无法成交。
      经纪公司的管理人员需要知道现在有多少人在交易,成交了多少笔。每个客户是否把风险控制在可接受范围内。在行情失控的时候,我们需要让经纪公司在最短的时候内作出反映,以减少损失。


高可用高性能系统(二)系统异常场景

一、网络故障
      当客户正在交易时,突然网络发生异常,导致无法继续连接到网络上。这个场景是最可能发生的。我们需要在网络发生的时候,让客户能够继续进行这个交易。
二、性能故障
      由于机器性能或者软件性能的缘故,可能会导致我们在某个业务处理过程中耗费大量时间,比如数据库查询。但是这也不应该影响我们进行继续交易。
三、系统崩溃
      一个畸形的请求,让我们的系统在某个不完善的地方发生了崩溃,这个很可怕,因为系统可能会退出,而不再提供服务。当然,系统崩溃的原因有很多,内存访问异常是最有可能发生的。当然,还有许多未知的原因,但是,我们只需要知道我们的系统已经死掉,并希望在这个情况,客户依然能获得相关的服务,比如交易。
四、系统拥塞
      数据库发生了死锁,或者网络流量过大,导致我们系统被阻塞住了。或者CPU和IO等操作让机器负荷过大,无法对请求做出响应,这时我们依然需要让客户继续交易。
      硬件、软件、网络,任何构成系统的单元都可能发生故障,而我们首先要做的是,在发生故障发生之前去,去分析、总结、预测哪些故障可能发生。

高可用高性能系统(三)故障管理

      高可用系统必须对故障具备很高的容错能力,而这核心问题就是对故障的管理。从系统组成来看,故障可能是整个系统的,或者某个组件、某个服务甚至某个进程中发生。对故障的管理可以分为几个过程:检测、定位、隔离、恢复、报告。如果继续强化的话,还应该加入预测功能。
      故障的检测主要工作是检测系统中在某个单元是有故障发生。故障的定位主要是判断这个故障发生在哪个单元以及这个单元具体在哪个位置。故障隔离是将发生故障单元从系统中摘除,使得故障不会在系统中继续传播。故障恢复是故障发生单元在处理故障之后,继续恢复服务。故障报告是将发生故障的类型、位置、结果报告给监测人员。
      故障预测是比较高级的功能,他需要对一个系统有比较深入地认识,对一些非通用的系统需要建立专家规则。但依然存在许多可以被通用化的预测规则,比如网络拥塞在一个单元上被监测,那么相关单元发生网络拥塞就可以被预测的。

一、故障区域单元划分

      检测、定位、隔离故障,首先将整个系统划分为相互独立的单元,这个单元可以是一台机器,一个进程,或者一段代码。这些依赖于控制粒度。我们将整个系统视为一棵树的根,每个单元可以包含更小粒度的子单元,每深入一个层次,都要提供更小的粒度。对于故障检测系统来说,可能处于这颗树的某个层,负责监控这个层某些兄弟单元的故障情况。对于某个单元的子单元如果发生故障,最好由该单元负责检测,再报告给更高层次的监控系统,或者该单元中负责检测的子单元来负责。我们将整个系统树化为不同粒度的单元,对检测故障、定位、隔离故障是有效的。

二、故障检测的方法

      在故障检测的方法中,心跳是最常用的方法。如果在一定时间内,一个单元没有和监控单元交互心跳,则认定该单元已经失效。但这个方法由一个很大的问题,就是心跳时间无法把握。如果太长了,就可能无法及时发生问题;如果心跳时间太短,对系统是个负担,而且对大型处理任务可能无法作出有效判断。
      比如,有个查询需要耗费DBMS时间,而这个期间,单元必须继续等待,直到查询结果。这个过程可能导致无法回应心跳,而可能被系统判定为失效,而将查询转发到其他单元,让DBMS性能继续恶化,而导致最终崩溃。阻塞可能导致心跳失效,但系统本身并没有失效,但处理结束后,系统会自动恢复正常,这时候就可能发生冲突问题。但实际上,现有的解决方案中,都是以心跳为基础,或者加入多路心跳线。
      心跳是最核心的方法,但他无法应对阻塞的情况,包括网络、机器、进程。我们并不否认心跳的有效性,我们需要在心跳的基础上,加入拥塞的检测办法来作为补充,比如判断cpu,IO,网络通讯等辅助手段。我们通过心跳来检验单元失效问题,但我们同样必须预防假失效的发生,因为阻塞的存在,所以假失效发生的可能性还是相当大的。避免假失效问题,很难使用统一的办法,必须针对不同情况,采用不同的策略。但心跳检测依然是检测单元失效最重要的方法。对于比较常见的网络失效问题,我们认为通过ping等更低层次的方法来检测比较合适。因为如果继续通过套接字来检测,依然会阻塞在套接字发送和接收过程中。
      故障类型不单是失效,而且还包含其他类型的故障,比如网络故障、数据库故障、存储故障等,是无法通过心跳检测来完成。我们会通过另外的文档来更详细的讨论这个问题。
      心跳的作用等价于轮询,她不停地询问对方地近况,在对方没有应答的时候推断对方失效。在正常情况下,这个没有问题,但在故障发生的时候,就显得延时过大。我们需要一些更加主动的措施来触发检测,比如,当对方网络断开连接的时候,这时发送和接收报文显然会发生问题,这样可以马上触发检测,而不是继续等待到心跳周期的到来。主动和被动两种检测触发方式,可以互相补充。另外,我们可以将大型的任务划分为更加细小的子任务,每个子任务的执行时间比较短,这时,我们可以在每个子任务执行间隔进行心跳检测,这样可以避免大型任务无法及时检测造成的假失效。

三、心跳检测服务

      心跳检测如果在多个单元之间互相守护,给心跳检测的管理带来巨大的复杂度。一个服务单元需要管理多个其他的服务单元,这个对系统没有什么好处。我们可以设计一个心跳检测服务,将这个功能独立出来。服务单元之间的网状心跳检测就转化为服务单元和心跳检测单元之间的线性过程。不幸的是,心跳检测服务之间依然需要互相守护,这样可能构成一个无限递归的过程。但是我们只需要对心跳服务设计互相守护的双机检测机制,就可以终止这个无限递归过程。
      有个思路解决多个单元互相守护问题比较有趣,类似于选举机制。构成一个系统的多个服务单元通过选举算法推举出一个单元作为主服务,当该单元时,新一轮的选举再次发生。这是个动态的过程,和建立一个特定心跳检测服务的静态方法不同,具备更大的灵活性,当然复杂度也更高了。我还没有仔细研究他有什么好和不好,显然他们的内部自治度更高,不过向哪个单元查询他们内部状态是个头疼的问题。

四、故障定位

      故障定位是通过故障检测发现某个单元发生了故障。我们已经将整个系统分割成互相独立的若干单元,并且每个单元还可以继续细化为更小的单元。故障定位实际上仍然可以被看成为另外一个故障检测的过程。如果故障在网络单元之间发生,我们通常可以利用ping来确认和定位,这已经是比较成熟的技术了。在进程中的定位,同样需要依赖于软件方式,参考《返回码的设计》一文中,提供了这样的一个思路。树型结构比网络拓扑要简单地多,我们在故障定位时,主要借助树型区域来逐渐细化故障区域。

五、故障隔离

      故障隔离就是将失效或者发生故障的单元从服务单元列表中摘除。软件和硬件有所不同,硬件可能是需要更换,但软件只需要做状态重置就可以了,速度要快多了,硬件更换的处理在逻辑上却简单。隔离的另外一个需要注意的是,必须控制导致故障的请求不能继续传播,需要采用一定的规则来限制畸形请求在系统中的传播。故障隔离原则可以参考硬件采用“失效即停”的规则。
      故障检测、故障定位、故障隔离是一个连续的过程。隔离依赖于定位、定位依赖于检测。隔离需要把握一个单元粒度问题,如果是一个进程发生了故障,应该没有必要将整个机器隔离掉,我们只要简单地将这个进程从服务列表中摘除就可以,这台机器上的其他服务进程依然继续提供服务。

六、故障恢复

      将故障隔离后,需要尽快的恢复系统。故障恢复需要将故障恢复到可以继续服务的状态。故障恢复需要解决几个问题,从什么地方开始恢复,需要恢复什么内容,这在设计时是需要注意的。为了便于设置恢复点,我们需要将一个连续的过程分解成多个子过程。以便耗费最小的代价来恢复。恢复内容一般都从外存中读入内存。
      日志型文件系统的设计为我们提供很好的思路,DBMS的事务也是为故障恢复提供很好的借鉴。一般来说,我们将一个操作划分2个步骤,一个是实际业务操作,一个是状态设置。实际业务操作由于需要大量的时间和资源,而状态设置相对简单,发生错误的概率要小得多,耗费资源也少的多。当实际业务操作完毕,那么状态设置为成功。如果状态没有设置成功,就可以将故障恢复到之前。故障恢复主要涉及的问题就是恢复点,恢复的内容和业务相关。

七、故障报告

      故障报告没啥好说的,只要将故障的内容、位置报告给合适的人就行了。
      故障管理是高可用的一个核心方案。我们现在主要讨论的是通过心跳来对失效进行管理,但问题远不止于此,我们需要更复杂的机制来讨论这个问题。在以后的章节中继续论述。

高可用高性能系统(四)分布和集群

      分布和集群不完全一样,但有很多相似的地方,也有很多不同的地方。我们主要从他的基础特性上来分析,分布的计算单元可能分布在广大的地理空间,比如中国的 一台机器和美国的一台机器;而集群的计算单元主要集中在一个相对较近的距离,比如一个机房,对于集群的使用者来说,集群是个整体,他的构成是透明。不论他 们是为了完成同一个任务还是分担不同的任务,我觉得都不是关键问题。他们之间最大差别就是计算单元之间通讯代价引起的,导致不同处理方案。
      P2P类型的文件下载是分布一个典型的应用,每个文件被分割成若干部分,同时从不同地方下载,然后在本地组合成一个文件。这个方式比从单个服务器下载要快 得多,不过显然依然无法快过本地下载的速度。分布计算的鼓吹者认为分布计算将一个任务分解成若干子任务,由不同计算单元承担计算任务,然后对结果合成,他 所耗费的时间将比单个计算单元要少得多。但我们依然要清醒认识到他的限制条件,计算单元的通讯代价以及由此引起的通讯量和计算时间问题,还有任务分配的额 外时间。将分布计算应用于计算耗时长、计算单元通讯量小的计算任务中,是合适的,比如分布式文件下载。
      集群对外提供的是单个入口,对访问者来说,他是一个整体。一般来说,负载均衡是集群的主要任务。不管一个任务是被多个集群单元分担,还是被选中一个集群单 元承担,都不是关键。关键的是他利用集群的扩展性,通过增加计算单元来降低计算时间。由于集群的计算单元之间的通讯代价要远小于分布,有些实现利用光通讯 来连接计算单元,使得不同计算单元之间的计算代价和一个计算单元内部通讯代价相差无几。
      分布和集群各有自己优势,针对不同的环境我们需要采用不同的方案,不能一概而论。比如为全世界各地提供期货交易服务,我们不可能为分布在各大洲的客户提供 一个集群,如果这样的话,集群所提高的效率将被通讯代价所严重抵消。一个好的方案是为美国交易所提供一个集群为美国客户服务,为中国交易所提供一个集群为 中国客户服务。当有个中国客户想异地在美国交易所做交易,那么我们可以通过中国的集群和美国的集群通讯。这样的好处是可以通过降低中国集群和美国集群之间 的通讯代价而提高所有中国客户在美国交易所的交易效率。当然,我们还应该允许中国客户可以直接连接到美国集群,毕竟当中国集群发生故障时,依然可以使用美 国集群。这是分布的好处,可不能浪费了。
      从一个更宏观的角度来看,由于集群被视为一个计算单元,所以可以被视为分布的一个计算单元。集群本身由多个机器构成,比单个机器构成的计算单元提供更高的 可用性和性能,降低响应速度。但是,当通往集群的访问通道被切断时,集群将同样也会失效,除非同时提供其他通道。而分布同样由多个机器构成的,他的高可用 性是由数据冗余量和网络的连通性决定的,比如文件下载,当A计算单元失效,同样的文件片段可以从B计算单元获取;高性能的获得是通过缩短网络通讯代价,由 多个计算单元分担计算任务。
      我们要建立高可用高性能系统,可以结合分布和集群的优势。一般来说,这个系统以分布计算为主、集群为辅。对于期货交易系统来说,这个假设是客观的,因为不 同营业部可能分布在不同的省市地区。就是推而广之,也是合理的假设。我们将计算量较大的任务交给集群负责,比如数据库管理,是比较适合于集群的,将这些集 群系统挂接到系统网络中,作为一个高可用的计算单元。对于一个复杂的交易系统来说,可能有大量的功能需要计算,这些功能多数具有地域性,比如某个营业部的 客户信息,将这种功能作为集群为整个系统服务其实没有太大必要。我们将功能分解成不同部分,分布在系统各个区域,为各个区域服务,反而能获取更短的响应时 间。
      但有些任务,比如交易,需要大量的计算,响应大量客户请求,同时和交易所只能有单一的通讯通路,用集群来承担这个任务才是合适的。
      分布的连通性是高可用的重要保证,分布的区域自治和集群的负载均衡是缩短响应时间,提高性能的重要保证。我们通过设计服务单元的部署方案来更好的利用分布和集群的优势。

高可用高性能系统(五)基于规则的请求路由 

      首先必须申明,这不是个路由器,而是借用路由器的特性。一般说来,传统的路由器,将IP包转发到目标机器上,那么他的路由规则就是IP地址。但对服务请求来说,可能是SQL语句或者其他具有规则特性的请求。我们可以建立一个服务,按照一定的规则,将这些请求分发到其他服务上去,就像路由器转发IP包那样。这个路由服务主要的功能就是按规则分发,但他可以也可以不需要处理请求的结果数据。对于接收到请求的服务来说完全可以直接将数据返回给客户端。
      可能会很奇怪,要这个功能有什么用呢?其实这个功能非常有用,特别是在高可用高性能的系统中。以DBMS为例,如果一个数据按时间段分布在12个数据库中,每个数据库存储每天中2个小时的数据。那么如果有一个请求需要计算1天的数据,就需要将这个请求分发到12个数据库中,这个例子多见于海量数据的情况。当然,也有的时候,每个数据库的数据是完全相同,当遇到写操作的时候,需要同时向多个数据库写入数据。而读请求只由一个数据库来响应。这样一读多写的请求同样需要一个服务来处理这个分发功能。上面2个例子是典型的高可用高性能系统DBMS的方案。但在传统的C/S架构中,客户端和服务器直接交互,缺乏这种中间环节,也失去了这种灵活性。
      从单个请求的总体计算时间看,可能会稍大点,因为需要增加路由的计算,但由于计算负荷被多个机器分担,用户响应时间反而要减少很多。当某个服务器失效时,由于路由服务的存在,完全可以将这个请求重新定位到一个可用的服务。在传统的C/S架构中,插入路由服务,并不是单纯意义上的三层或者四层。对于路由服务来说,同样可以插入到应用服务和客户端之间,关键问题是他需要基于什么样的规则来实现请求的分发功能。
      同时,我们还必须关注一个问题,当请求被分发到多个服务器之后,其中一个处理失败,我们应该采取什么策略来处理这个结果呢?如果我们采用鸵鸟策略,将它置之不理,那么这种情况可能会破坏了数据的完整性。但如果我们将它视为一个事务,全部回滚的话,也可能导致错误。比如期货交易系统中的成交回报,成交回报是交易所返回的报告,说明某笔委托已经完成交易,不论我们在存储这个信息的时候是否发生错误,那么交易所不可能去回滚。所以它必须完成。我们必须不断的强制发送这个存储请求直到全部成功。但是这种方案的后果,可能导致系统的性能严重下降。我们应该认识到,如果路由服务管理着越多的节点,那么发生错误的概率越大。这个问题和UDP多播的情况很相似。
      我们知道,就像TCP和UDP那样,一个请求是否需要被复制,是否需要保证完整性,是否必须具备实时性,都是因情况而异的。针对不同的情况,我们可以采取不同的策略,而不是一概而论。现在我们讨论3种比较通用的情景:
      1、需要高可靠性:毫无疑问,我们必须同时保证所有的请求得到同样的结果,当然一个分发出来的请求失败后,我们需要不断的重启,当最终无法解决时,我们必须将负责处理这个请求的点标志为失效。
      2、需要高可靠性,但容忍较大的延时:和上面一样,我们必须保证全部分发的请求处理成功,不过有点比较特殊,如果遇到失败的处理,我们可以将这个请求缓存下来,等待机会继续重发,而不是花大量的资源来不断重试。
      3、只要发送一次就可以了:这种情况比较简单,我们把它当作UDP那样就可以了。

      对于使用路由服务的系统来说,时序也是很重要的考虑因素。我们打个比方,一个客户的保证金为零,但他先卖出合约,然后再买入合约,这个交易过程是可以成功的。但如果这个过程交换了次序却是失败,而这种情况在我们的系统中,以及分布式系统中完全可能发生的。当多个请求被多个路由服务所分发,那么在时序上是很难保证的,除非被严格的排序。我们可以使用序号或者时间戳等方式来控制这种时序性,而不是去排序。

高可用高性能系统(六)虚拟网

      虚拟网在这个系统中作用是为了能更好控制请求和响应的处理。象DNS等标准的互联网组件来说,我们没有办法控制,但如果我们建立自己的虚拟网,那么就完全可以实现自己的DNS。象很多以DNS来实现负载均衡的系统来说,在这里都是很容易实现,当然这不是主要的考虑因素。
      我们假设有一个服务失效了,那么我们的传统做法如何处理呢?如果是通过DNS来实现负载均衡的话,实际只能删除改服务的IP,防止新的请求指向这个服务。如果是在网关之后,带集群的话,这个失效的管理可以由网关来切换。然而,如果是网关的话,同样将问题转移到网关失效的情况上。总之,在传统的网络服务中,和IP切换相关的操作十分困难。
      如果我们为系统建立一个虚拟网络,那么一切都在控制之中。很简单,IP策略,路由控制,请求分发都可以按照自己的规则来完成。我们继续以期货交易系统为例,如何利用虚拟网络来解决IP相关的问题。
      1、我们建立多个路由服务,这些路由服务通过IP地址和端口实现互联,从而构成了一个虚拟网络。
      2、每个路由服务本身指定一个虚拟IP地址。
      3、每个客户端可以自己指定一个IP地址,或者申请一个IP地址,当然可以是多个。这个和多网卡的概念是一样。
      4、服务被设定为路由服务的客户端,只是他们可以自己指定IP地址。
      5、当一个服务失效后,可以服务可以使用相同IP向路由服务注册,因此,请求能容易被接管。
      6、是否允许多个服务注册相同的IP,这个可以考虑,但后续的问题也比较多,个人不建议。

      一个期货经纪公司可以有很多营业部,这些营业部在物理上可能分布在很多地方,甚至跨越多个省市。但一个经纪公司在交易所中的帐号很有效,可能只有一个。每个营业部的交易请求都需要集中到总部。我们在营业部部署一个或者多个路由服务,将这些交易请求转发到总部,将一些营业部相关处理留在营业部。对营业部来说,相当于在局域网进行操作,他只需要知道提供服务节点的虚拟地址就可以了,这些服务究竟被部署在总部还是部署在本地,由路由服务来控制就可以了。
      我们可以在虚拟网中实现一个DNS服务,利用DNS来实现负载均衡在虚拟网中很容易被实现。如果我们有一个集群,那么可以在这个集群之前带多个路由服务,路由服务可以作为虚拟网的组件,也可以是真实存在的路由。当一个路由服务失效时,在虚拟网中很容易被其他路由服务接管,只要重新由另外的路由服务注册相同的IP就可以。如果我们是在多个节点部署相同的服务来实现高可用,那么在虚拟网中完全可以通过相同的方式来切换服务。
      有一种方案,由同一网关来接收请求,但由具体完成处理的服务来返回结果来进行负载均衡。在传统的方案,需要建立2个或者多个TCP通道来处理请求接收和结果返回。但在虚拟网中,这个也不需要做这个处理,这是因为路由服务也是虚拟网的一个组件,路由选择可以由他自动完成,不需要另外建立。
      我们知道P2P网络在处理节点失效的情况十分有效,因为存在一个tracker服务跟踪所有的连接情况。我们在虚拟网中,不一定有tracker,但路由服务是知道客户端的存活情况,这个可以实时回馈到特定服务器中,这样来降低失效响应时间是有效的。   

高可用高性能系统(七)状态和无状态

      关于状态还是无状态这2种服务器架构,我在以前的一篇文章:《状态和无状态--2种服务器架构之间的比较》 做了论述,也涉及到高可用高性能方面,现在想做一些补充。

一、核心区别

    每个服务器的架构,通常可以简化为请求和应答的过程,状态化和无状态化的最核心区别在于,服务器应答过程是否基于上次请求所构筑的上下文环境。一般说来, 无状态化架构基于请求所携带的信息,如果请求所携带的信息比较简单,象HTTP中表单,其实也可以将这部分状态化架构归于无状态化架构。而状态化架构的响 应则依赖于服务器中的上下文环境,比如期货交易系统中,要发起委托,则必须先登陆,否则资金、持仓等信息都无法获取。无状态化架构也可以模拟状态化结构, 比如在HTTP中,可以携带COOKIE,或者是服务端的会话ID,这种方式其实也是为服务端指定了上下文环境,实际上可以归结到状态化架构中。

二、请求迁移

      无状态化架构的响应条件完全基于请求所携带的内容,所以比较容易迁移,在发生错误的时候,由于发生错误节点并没有保存请求的上下文环境,所以由另外一个节点来处理完全可能获得相同的结果【注意:是可能】, 而状态化架构在发生错误时,其他节点可能无法构筑相同的上下文环境,或者构筑相同的环境代价巨大。我们可以看到无状态摒弃了上下文依赖,“无官一身轻”的 自由,不过在请求携带会话ID的应用中,并没有这个自由。但我们也必须注意到,如果请求中包含资源的申请,那就不一定能够获取正确的结果了,比如文件传 输。
      状态化架构如果不是保存上下文信息的节点失效,那么实际上并不会造成影响。或者上下文环境能够比较容易地被重新构建出来,那么状态架构完全可以获取和无状 态同样的自由。这个情况在多数情况是比较困难的,比如在网络游戏中,一个角色的上下文环境可能包含很多角色信息,装备信息以及其他杂七杂八的数据。不过, 我们可以采取几种策略来达到这种效果。
      1、存储分割:一个请求所需要的上下文环境即使再复杂,他在一次处理中不会同时用到。我们可以按照某种策略,将这个环境分割成多个单元,放在不同的地方。将不太容易发生变化的数据存储到数据库中,而将容易变化的数据和另外一个节点同步。
      2、增量日志:将数据的变化过程转化成增量日志,当节点失效后,我们可以从失效点开始重构上下文。
      3、状态前置:在该节点之前,有个地方保存可以重现上下文的状态,该状态可以移植到其他节点,重构上下文。
      这种请求迁移不一定要在节点失效的情况下发生,而且很有可能是在负载过大的情况下发生。请求是否可以迁移是可用高性能系统的重要标记。状态化架构在这样处理之后,比较容易实现迁移,在负荷均衡方面作用就很大了。

三、实时响应

      实时响应是服务器对客户端的响应时间小。在客户端主动请求时,并不能看出这2种架构对响应时间的影响,因为同样依赖于服务器的处理能力。但当服务器需要主 动推动客户端处理时就不一样了,比如期货交易的风险类别的提示,这类计算多数是在服务端完成的,我们需要实时监控行情的变化,当合约的价格达到我们期望的 时候,需要马上作出处理。这时候需要服务器主动向监控者发送信息。
      在状态化架构中,我们可以从服务器中保存的上下文环境知道监控者在哪里,然后向他发送消息。但是在无状态化架构中,服务器中没有任何监控者的消息,只能等 监控者发送请求时,向他发送消息,这是被动的处理方式。监控者在这种架构中只能采取定时的方式,如果在时间极度敏感的环境中,这个显然是无法实现的,因为 时间间隔是无法跨越的鸿沟。
      这种情况同样会出现失效检测中。无状态化架构中,只能由客户端发送心跳请求,服务端被动地判断。而在状态化架构,服务器把握更大的主动性,因为他可以主动地去监控客户端。

四、资源存储

      不论哪种架构,只要他们涉及到对资源的访问,那么他们就面临同样的制约:资源存储。在WEB/P2P中,主要是对文件等静态资源的访问,这些资源没有变 化,因此当请求被迁移时,可以获取同样的结果,这个过程对状态化架构是同样适用的。不过在资源存储节点失效时,我们必须看到如果资源存储不是在多个节点具 有相同的备份,哪种架构都没招。
      如果是访问变化频繁的动态资源时,状态化架构比无状态化架构更具优势。因为状态化架构对变化具有更强地感知能力,可以实现增量更新的操作。无状态化架构只能读取全部的数据,或者由其他服务来提供需要的数据。
      曾经在期货交易系统中实现风险控制级别的计算,这个算法涉及到行情数据,客户的资金、持仓。行情数据还好说,可是客户的资金和持仓是在数据库中的,对于风 控服务器来说,没法感知到他们的变化,一个折中的办法就是发个请求给数据库,让他从操作日志中,解析出变化的数据再来更新本地的数据。其实这种方法对数据 库来说是很为难的事情,但是对于无状态架构来说,完全没有办法。
    资源存储和实时响应其实都涉及到变化的感知能力。当资源存储失效时,重定向到正确的资源存储点是最关键的问题。

五、长短连接

      长短连接指的是连接的保持时间,一般说来,无状态化架构倾向于短接连【如:HTTP】或者无连接,而状态化架构倾向于使用长连接。这是因为无状态化结构没有上下文关系,所以保持连接对他来说是没有必要的;而状态化架构往往需要连接来判断上下文关系,同属于一个连接的前后2个请求就属于同一个上下文的请求。
      这种关系的判断主要应用于TCP/IP的时候,如果在虚拟网中,这种关系会被模糊掉。 因为在虚拟网中,客户端极有可能是长连接在路由服务中的,但他发送的请求被路由服务转发到不同的服务节点,这时候,极有可能是无状态化架构。不过长连接对状态化架构支持得更好,毕竟长连接容易标识上下文环境。

高可用高性能系统(八)进程管理

      进程是系统的一个处理单元。系统的处理单元按不同的粒度来划分的话,可以是函数、线程、进程、计算机、集群。当然,进程仍然是最基础的操作系统的处理单元。当系统发生一个无法恢复的错误时,操作系统一般都是以进程为单位,将其杀死,同时释放该进程的资源。函数,线程在处理致命性错误传播时,多数是无能为力的,即使有C++异常捕获系统这类东西。
      我们讨论进程如何在实现高可用高性能,对其他粒度的讨论是有作用的。对于一个处理过程来说,如果要高可用的话,就要保证该处理过程即使在某个进程崩溃时,也能由其他进程来继续完成。当然是否从崩溃点开始,还是从头开始涉及到不同的策略。从头开始,对进程来说比较简单。而从崩溃点开始的话,要涉及到很复杂的事情,比如崩溃点的上下文环境、资源情况,总之就是还原崩溃之前的环境,他的好处很明显,就是避免重复执行,减少执行的代价。
      很显然,一个进程如果被操作系统杀死的话,他自己是不会拉起的,所以一般都需要一个独立的监控进程,不停地检查目标进程是否存在内存中,如果不存在,那么重新启动这个进程。不论LINUX或者是WIN32进程,本身是没法从进程崩溃的点开始重新执行的,所以我们一般都是让他重新执行。稍好点的做法,就是在进程执行体的实现中,做些扩展,自己记录一些应用层的断点数据,然后再重新执行时,减少前面的执行过程。
      Erlang的进程和通常意义上的进程不太一样,是他虚拟机内部实现的,所以能够以很小的代价启动大量的进程。但我不能确定他是如何实现的,从我推测上来看,既然进程是他自己实现的,那么保存上下文应该不是问题。但从传统的角度来看,LINUX要做到这个地方还是蛮困难的。LVS是LINUX下的一个重要集群软件,他就是修改了部分内核,才能获取进程的上下文。
      单从进程管理本身来看,没啥好说的,就是如何恢复进程,保证业务处理的顺利进行。但如果涉及到跨机器,就要考虑进程迁移的问题。进程迁移同样也涉及到进程的上下文,进程迁移必须考虑到迁移的代价。
      进程由于是是操作系统支持的最小的逻辑处理单元,他具有独立空间,所以使用进程来实现故障隔离是很好的方式。比线程要好很多。我很欣赏Erlang自己管理进程的做法,这个理由上面已经提到了很充分。

高可用高性能系统(九)UDP的应用

      UDP对实现高可能高性能系统有啥好处呢?其实UDP的好处是间接的。我们知道UDP和TCP不同的是,UDP不保证可靠性,他只管发,不管收没收到。也就是说一个报文,他只会发一次而已。而TCP就必须靠复杂的机制保证可靠性。而且,UDP还具有广播,可以一次让多台机器收到报文,如果靠TCP的话,就必须发多次,性能上,当然差别很大了。
       以上只是简单介绍下UDP的特性,下面我们来引入实际的例子。我假设有N个DMBS,有个数据必须同时写入到这些数据库中,同时还保证成功。这个案例在第五章基于规则的路由请求中有提到过。这个案例看似简单,其实十分复杂难办。如果我将N个DBMS的写操作做为一个事务,那么这个写操作的成功将受到N个因素影响,任何一个失败,都将导致整个事务失败。而且,如果是以TCP来传输这个写操作的话,还必须发送N次,在所有的写操作成功后才被认为是成功了。
      如果是UDP的话,可以用多播或者广播的方式,发送一次就可以了。即使不是以这种方式,那么我们用UDP报文发送的话,也不用等待太长的时间。在很多情况下,N个DBMS的写操作并不需要构成一个完整的事务。可能有一个核心的DMBS需要保证他的完整性,而其他DBMS相对来说,并不需要那么苛刻的要求,比如查询。查询是个很费资源的操作,将查询请求放置在一个单独的DBMS是很明智的做法,将这么耗资源的操作独立起来,可以避免核心操作被干扰。在这种情况下,我们只要保证核心数据库的写操作是成功的就行了。即使查询数据库的写操作失败,我们依然可以将他缓冲起来,在合适的时候在发送。UDP正好符合这个需求,而且在性能上要优于TCP的传输。
      UDP是不可靠的传输,这个我们必须要记住。但是我们可以在UDP上附加一个简单的机制保证他的可靠性,过程可以象TCP那样。TCP的可靠性我们是无法选择,但是UDP的可靠性,我们可以选择。我们可以根据需要解除这种附加的机制,来提高性能或者实现可用性。

高可用高性能系统(十)基于状态反馈的故障检测

      故障检测是高可用系统中的一个重要内容,它是系统高可用的基础,只有故障能够被及时准确的检测出来,才谈得上高可用。心跳检测是常用的检测方法,它被广泛 地使用于各种系统,虽然心跳检测有很高的适用范围,但是它有很大的局限性,包括滞后性以及其他因素的影响,比如CPU忙或者网络阻塞的因素。
      其实我们知道,心跳的目的只是想知道故障的结果,而不需要知道故障的原因和现象,但是故障的发生并没有这么简单。打个比方,当OUT OF MEMORY发生之前,一定会有内存不断被分配却极少释放的现象,导致最后可用内存不断减少,而最终发生这个错误。很多故障的发生实际上是一个渐变的过 程,我们可以监控这个过程来避免最终结果的发生,或者及时采取合适的措施。但也有不少故障的发生是突发的,没有任何预兆。当这些故障发生之前我们是无法预 测的,但是故障发生之后就会有相应的现象。比如网络故障,当这个故障发生时,任何在网络中传输数据的企图都会失败。因此,在心跳之前,仍然有足够的信息表 明网络发生了异常。
      我们在上面主要论证除了心跳之外,有更多的手段来检测故障的存在,主要是根据各种状态来判断,就像中医诊断病情一样。假设存在A/B两台机器,他们之间的 网络状况可以有各种协议,比如ICMP/SNMP等。他们内部进程之间的也可以存在发送和接收数据时返回的错误信息能够表明网络状态和对方进程的状态。基 于状态反馈的故障检测理论认为,任何故障的发生之前或者发生之后存在与之相关的状态,能够甄别这个故障即将发生、可能发生或者曾经发生。
      状态的传播主要还是要以推送为主。首先由关注的节点向服务器注册,然后由服务器将状态推送给各个节点。当然,也可以采用轮询的方式。如果节点数比较多的 话,可以采用级联的方式,这样,得到状态推送的节点可以级数增长。毕竟推送的时间很短,即使多级级联,时间消耗也只是t*n。
      在进程中得到状态信息的代码位置可能在函数调用栈很深的地方,也可能出现在进程任意位置,但发送状态信息的目的地可能出现在另外一个进程。所以发现状态的代码本身并不能得到服务器的帮助,只能根据自身的设计进行处理,关键是故障的状态发现。

高可用高性能系统(十一)最后的守护

      在高可用性的要求下,理论上要求所有的数据、载体都必须有备份,这样就可以避免单体故障的影响,这是最基本的要求。如果在简单的系统中,这没有问 题,毕竟2台或者少数的几台基本上没有什么问题。可是我们如果想象下GOOGLE那样的公司,或者大型的券商或者交易所啥的公司。双备份的要求可能会变得 难以承受。
      其实我们研究下双备份的缘由,可以发现,之所以要双备份,主要是希望在其中一个系统失效的情况下,另外一个系统能够及时的提供服务,而最终的目的,依然是提供服务。基于这个最终目的,我们发现,其实只要能够提供正确的服务,是否双备份并不是必然的要求。
      我们考察在哪些情况下,系统失效后,依然能及时提供服务呢?
      1、能很短时间内恢复服务
      2、能很短时间内请求接管
      双备份的系统中,也是为了达到短时间内接管请求,但实现能接管请求的不一样是双备份系统。我们下面来讨论下如何实现非双备份的解决方案来实现双备份的功能。
      首 先,应大多数用程序的重启,实际上可以在几秒钟之内完成。不过很多操作系统和DBMS估计很难做到。但还是有些可以用USB/RAMDISK/LIVE- CD类型的操作系统能够实现很短时间内恢复服务。不管怎么说,只要这个系统能够在很短时间内完成服务的恢复工作,那么双备份的工作实际上没有太的必要。
      其 实,请求接管问题,和恢复服务很类似。通常情况下,客户端发出的请求在网络上传输时,如果接收方失效的话,请求会因无法被接收而丢失了,这种错误只能由双 备份系统来挽救。但是,如果这个请求是放在某台机器的内存中,事实也不需要双备份了。因为请求能容易地被转发到其他服务器或者服务进程中。
      在这样的系统中,我们如果将每个请求以及请求的处理在时间粒度上,能够控制的比较小,这其实也涉及无状态系统的一个特性。那么双备份在大型系统中也不是很 必要的。于是双备份的特性就被演化为服务恢复和请求接管的2个子问题。如果我们解决了这2个问题,那么双备份的问题就不再存在。而这2个子问题的解决方案 在上面已经有了论述了。
      在大型系统中,所有的系统或者机器都能短时间恢复或者所有的请求粒度都受控制,显然是不太现实,总有些系统比较庞大,比如数据库。于是,我们就可以为这些无法解决的案例建立双备份,在这里,我称之为最后的守护。他是高可用性高性能的最小子集。在P2P网络中,实际上就是这种架构。BT种子被提交给服务器,客户端连接到服务器后,得到了种子,那么他们将通过种子中定义的跟踪服务器获取邻居的信息,然后之间互相建立连接,文件的传输过程就和服务器无关了。这种服务器就是扮演最后的守护的角色。我们只要保证这个服务是高可用的,那么后续的过程就能继续。即使传输过程中发生失效,也能重新根据服务器提供的信息继续文件传输的过程。
 

      需要做最后的守护的资源一般是内容庞大,以致于无法迁移的内容,比如数据库,文件。他们大到一定程度时,由于无法被接管,所以对他们做双备份是最后的选择。


高可用高性能系统(十二)处理粒度

      C/S架构中,最常见的就是请求->处理->应答。高可用高性能系统的架构设计中,对这块有一定要求,而且是很重要的要求。如果一个请求很容易被接管转发,那么就能很方便地设计分布式计算,或者负载均衡。所以,我们在这块架构设计中,希望对请求进行一定地设计,减少请求之间的依赖,同时保证请求和应答之间足够快速。这样就能够提高并行度。所以,处理粒度就成了很重要的因素。
      在GOOGLE的一篇MAPREDUCE论文中,通过map分解计算以及reduce归纳计算结果,来实现高性能计算,实际上也是为了降低处理粒度,提高并行度。在apache的hadoop项目中,我们可以看到对这个理论的实现。象搜索引擎这样的系统中,他的存储量极其巨大,但内容却相对固定。系统很容易对目标进行划分成多个区,然后并行处理。这点和P2P还是很相似的。文件被分割成很多小片,然后从不同的对端获取。每次获取小片的时间较小,即使被中断,也很容易得到恢复。所以,对处理粒度的分割是很关键的一个部分,避免处理粒度过于庞大,对于 构建高可用高性能系统是很重要的。
      对于一个动态内容来说,象P2P和搜索引擎那个的结构显然是不够的。我们回到应用场景期货交易所的期货行情,显然无法固定划分,但如果从时间上进行切片,却是可以的。当然,也只能应用在某些应用场景中,不一定具有普适性。
      在很多高性能实现中,进程迁移的时候,往往要迁移很多内容,包括堆栈。事实上他将整个进程拷贝另外一个机器。在纯数值计算时,问题不大,但如果涉及到资源时,很难处理,比如文件。如果我们将这个处理过程细化成更小的粒度,比如文件处理。我们在一个文件处理中,可以将文件某个片段的处理过程分为一个处理粒度,那么迁移这个进程实际上问题就不但,最后复制的内存区稍微大了点,而不需要迁移更多的内容,比如堆栈。这种机制对集群或者分布式计算都是很大改进。

高可用高性能系统(十三)虚拟机

      虚拟机执行速度缓慢,好像和高性能没有什么关系,但事实上,却不是这样的。我们首先探讨下对高可用的作用,这个其实比较好理解。虚拟机一直执行脚本化程序,比如lua/ruby/python/sql等,他们在虚拟机中被解释执行。java其实也可以被认为是这样的,erlang也是。这些情况都反应了这样的事实,当然,我们并不是说他们都是高可用的。我们考察2个重要的语言:SQL和erlang。SQL是一种结构化的查询语言,他的出错概率并不高,因为他基于严格的数学理论。而erlang是广泛应用于电信级系统中的语言,高可用方面是显而易见。
      我们究其原因,可以发现基于同样严格的理论基础,而同时,对系统的运行过程有更高级别的控制。开发者对运行结果和需求施加影响,更多的实现被虚拟机接管。开发者的错误导致的影响被严格地控制在一定的范围内,如死锁,也是有系统进行仲裁。这些特点和C不一样。
      我所设想的虚拟机也是需要完成这样的功能。将用户编写的功能性程序加载到虚拟机上编译,然后在虚拟机上运行,理论基础参考与erlang,但最后设计成如Lua那样轻量级的。
      我们在上面几个章节中做了很多论述,实际上最后的实现可以体现在这个虚拟机上。由于我们对虚拟机是完全控制的,所以划分处理粒度可以通过语言细节来提供,如mapreduce。在进程迁移、调度上都可以做到更好的把握。
      高性能的实现也是这样,我们在虚拟机上的进程如果很容易地被迁移,那么分布式计算/集群实际上就容易地完成了。


阅读更多
个人分类: 互联网架构
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭