2005年,我开始和朋友们开始拉活儿做网站,当时第一个网站是在linux上用jsp搭建的,到后来逐步的引入了多种框架,如webwork、hibernate等。在到后来,进入公司,开始用c/c++,做分布式计算和存储。(到那时才解开了我的一个疑惑:C语言除了用来写HelloWorld,还能干嘛?^_^)。

总而言之,网站根据不同的需求,不同的请求压力,不同的业务模型,需要不同的架构来给予支持。我从我的一些经历和感受出发,大体上总结了一下的一些阶段。详情容我慢慢道来。

【第一阶段:搭建属于自己的网站】

我们最先开始的网站可能是长成这个样子的:

拿Java做例子,我们可能会引入struts、spring、hibernate等框架,用来做URL分流,C、V、M隔离,数据的ORM等。这样,我们的系统中,数据访问层可以抽取出很多公用的类,业务逻辑层也可以抽取出很多公用的业务类,同一个业务逻辑可以对应多个展示页面,可复用性得到极大的增强。

不过,从性能上看,引入框架后,效率并不见得比第一种架构高,有可能还有降低。因为框架可能会大量引入“反射”的机制,来创建对应的业务对象;同时,也可能增加额外的框架逻辑,来增强隔离性。从而使得整体服务能力下降。幸好,在这个阶段,业务请求量不大,性能不是我们太care的事情。J

【第三阶段:降低磁盘压力 】

可能随着业务的持续发展,或者是网站关注度逐步提升(也有可能是搜索引擎的爬虫关注度逐步提升。我之前有一个网站,每天有超过1/3的访问量,就是各种爬虫贡献的),我们的请求量逐步变大,这个时候,往往出现瓶颈的就是磁盘性能。在linux下,用vmstat、iostat等命令,可以看到磁盘的bi、bo、wait、util等值持续高位运行。怎么办呢?

其实,在我们刚刚踏进大学校门的时候,第一门计算机课程——《计算机导论》里面就给出了解决方案。依稀记得下面这个图:

在我们的存储体系里面,磁盘一般是机械的(现在Flash、SSD等开始逐步大规模使用了),读取速度最慢,而内存访问速度较快(读取一个字节约10μs,速度较磁盘能高几百倍),速度最快的是CPU的cache。不过价格和存储空间却递减。

话题切换回来,当我们的磁盘出现性能瓶颈的时候,我们这个时候,就要考虑其他的存储介质,那么我们是用cpu cache还是内存呢,或是其他形态的磁盘?综合性价比来看,到这个阶段,我个人还是推荐使用内存。现在内存真是白菜价,而且容量持续增长(我现在就看到64G内存的机器[截止2012-4-3])。

但是问题来了,磁盘是持久化存储的,断电后。数据不会丢失,而内存却是易失性存储介质,断电后内容会丢失。因此,内存只能用来保存临时性数据,持久性数据还是需要放到磁盘等持久化介质上。因此,内存可以有多种设计,其中最常见的就是cache(其他的设计方式会在后面提及)。这种数据结构通常利用LRU算法(现在还有结合队列、集合等多种数据结构,以及排序等多种算法的cache),用于记录一段时间的临时性数据,在必要的时候可以淘汰或定期删除,以保证数据的有效性。cache通常以Key-Value形式来存储数据(也有Key-SubKey-Value,或者是Key-List,以及Key-Set等形式的)。因为数据存放在内存,所以访问速度会提高上百倍,并且极大的减少磁盘IO压力。

Cache有多种架构设计,最常见的就是穿透式和旁路式。穿透式通常是程序本身使用对应的cache代码库,将cache编译进程序,通过函数直接访问。旁路式则是以服务的方式提供查询和更新。在此阶段,我们通常使用旁路式cache,这种cache往往利用开源的服务程序直接搭建就可以使用(如MemCache)。旁路式结构如下图:

请求来临的时候,我们的程序先从cache里面取数据,如果数据存在并且有效,就直接返回结果;如果数据不存在,则从数据库里面获取,经过逻辑处理后,先写入到cache,然后再返回给用户数据。这样,我们下次再访问的时候,就可以从cache中获取数据。

Cache引入以后,最重要的就是调整内存的大小,以保证有足够的命中率。根据经验,好的内存设置,可以极大的提升命中率,从而提升服务的响应速度。如果原来IO有瓶颈的网站,经过引入内存cache以后,性能提升10倍应该是没有问题的。

不过,有一个问题就是:cache依赖。如果cache出问题(比如挂了,或是命中率下降),那就杯具了L。这个时候,服务就会直接将大的压力压向数据库,造成服务响应慢,或者是直接500。

另外,服务如果重新启动时,也会出现慢启动,即:给cache充数据的阶段。对于这种情况,可以采取回放日志,或是从数据库抽取最新数据等方式,在服务启动前,提前将一部分数据放入到cache中,保证有一定命中率。

通过这样的拆分后,我们就迈出了多机的第一步。虽然看起来比较简单和容易,但是这也是非常具有里程碑意义的。这样的优化,可能会提升20-30%左右的一个CPU idle。能够使得我们的网站能够经受更大的压力。

【第五阶段:逻辑程序的多机化】

当我们的访问量持续增加的时候,我们承受这成长的快乐和痛苦。流量刷刷往上涨,高兴!但是呢,服务器叫苦不绝,我们的程序已经快到不能服务的边缘了。怎么办?

“快使用分布式,哼哼哈嘿”J

这个时候,我们就需要针对CPU瓶颈,将我们的程序分别放在多台服务器上,让他们同时提供服务,将用户请求分摊到多个提供服务的机器。

好,如果提供这样的服务,我们会遇到什么样的问题?怎么样来解决?

我们一个个的来分析:

一、WebServer怎么样来分流用户请求?That’s a good question!

在考虑这个问题的时候,我们常见的WebServer早已给我们想好了解决方案。现在主流的WebServer几乎都提供一个叫“Load Balance”的功能,翻译过来就是负载均衡。我们可以在WebServer上配置一组机器列表,当请求来临的时候,WebServer会根据一定的规则选取某一台机器,将请求转发到对应的逻辑处理程序上。

有同学马上就会问了,“一定的规则”是怎么样的规则?他能解决什么样的问题?如果机器宕了怎么办?……

哈哈,这里的一定规则就是“Load Balance”。负载均衡其实是分布式计算和存储中最基础的算法,他的好坏,直接决定了服务的稳定性。我曾经设计和开发了一个负载均衡算法(现在正在大规模使用),有一次就因为一个很小的case,导致服务大面积出现问题。

负载均衡要解决的就是,上游程序如何在我们提供的一堆机器列表中,找到合适的机器来提供下游的服务。因此,我们可以将负载均衡分成两个方向来看:第一,根据怎样的规则来选机器;第二,符合规则的机器中,哪些是能提供服务的。对于第一个问题,我们通常使用随机、轮询、一致Hash等算法;对于第二个问题,我们要使用心跳、服务响应判定等方法检测机器的健康状态。关于负载均衡,要谈的话点其实很多,我之前也写过专门的一篇文章来介绍,后续有空了,我再详细的描述。

总之,分流的问题,我们可以通过负载均衡来比较轻松的解决了。

二、用户的session如何来同步?That’s a good question,TOO!

虽然HTTP协议是一个无状态的服务协议,但是,用户的基本信息是要求能够保证的。比如:登录信息。原来在单机的时候,我们可以很简单的使用类似setSession(“user”, ”XXX”)的函数来解决。当使用多机的时候,该怎么样来解决呢?

其实这个问题也是当年困扰我很久的一个问题。如果用setSession,用户在某一台机器上登录了,当下次请求来的时候,到其他机器了,就变成未登录了。Oh,My God!

Ok,让我们一个个的来看:

1、一台机器登录,其他机器不知道;

2、用户请求可能到多台机器。

对于第一个问题,如果我们在一台机器的登录信息让其他机器知道,不就OK了嘛。或者,大家都在一台机器上登录,不就可以了嘛。 对于第二个问题,如果我们让同一个用户的请求,只落在同一台机器上,不就OK了嘛。因此,我们可以提出三种解决方案:

1、提供session同步机制;

2、提供统一session服务;

3、将同一用户分流到同一机器。

嗯,这三种方式,你会选哪个呢?如果是我,我就选最后一个。因为我是一个懒虫,我会选最简单的一个,我信奉的一个原则既是:简单粗暴有效!哈哈。在WebServer层使用一致Hash算法,按session_id进行分流(如果WebServer没有提供该功能,可以简单写一个扩展,或者干脆在WebServer后面做一个代理即可)。但是这种方案有一个致命的问题,当一台机器宕机了以后,该机器上的所有用户的session信息即会丢失,即使是做了磁盘备份,也会有一段时间出现session失效。

好,那看看第一种方案。其实现在有一些框架已经提供了这样的服务机制。比如Tomcat就提供session同步机制。利用自有的协议,将一台机器上的session数据同步到其他的机器上。这样就有一个问题,我需要在所有的机器上配置需要同步的机器,机器的耦合度瞬间就增加了,烦啊!而且,如果session量比较大的话,同步的实效性还是一个问题。

那再来看看第二种方案,提供统一session服务。这个就是单独再写一个逻辑程序,来管理session,并且以网络服务的方式提供查询和更新。对于这样的一个阶段的服务来讲,显得重了一些。因为,我们如果这样做,又会面临一堆其他的问题,比如:这个服务是否存在单点(一台服务器,如果宕机服务就停止),使用什么样的协议来进行交互等等。这些问题在我们这个阶段都还得不到解决。所以,看起来这个方案也不是很完美。

好吧,三种方案选其一,如果是你,你会选哪一种呢?或者还有更好的方案?如果我没钱没实力(传说中的“屌丝”,哈哈),我就可能牺牲一下服务的稳定性,采用代价最低的。

三、数据访问同步问题。

当多个请求同时到达,并且竞争同一资源的时候(比如:秒杀,或是定火车票),我们怎么来解决呢?

这个时候,因为我们用到了单机数据库,可以很好的利用数据库的“锁”功能来解决这个问题。一般的数据库都提供事务的功能。事务的级别分多种,比如可重复读、串行化等,根据不同的业务需求,可能会选择不同的事务级别。我们可以在需要竞争的资源上加上锁,用于同步资源的请求。但是,这个东东也不是万能的,锁会极大的影响效率,所以尽量的减少锁的使用,并且已经使用锁的地方尽量的优化,并检查是否可能出现死锁。

cache也有对应的解决方案,比如延迟删除或者冻结时间等技术,就是让资源在一段时间处于不可读状态,用户直接从数据库查询,这样保证数据的有效性。

好了,上述三个问题,应该涵盖了我们在这个阶段遇到的大部分问题。那么,我们现在可以把整体的架构图画出来看看。

样的结构,足够我们撑一段时间了,并且因为逻辑程序的无状态性,可以通过增加机器来扩展。而接下来我们要面对的,就是提交增长和查询量增加带来的存储性能的瓶颈。

【第六阶段:写分离,提升IO性能】

好了,到现在这个阶段,我们的单机数据库可能已经逐步成为瓶颈,数据库出现比较严重的读写冲突(即:多个线程或进程因为读写需要,争抢磁盘,使得磁盘的磁头不断变换磁道或盘片,造成读写都很缓慢)。

那我们针对这样的问题,看看有哪些方法来解决。

一、减少读取量。我们所有的问题来源就是因为读写量增加,所以看起来这个是最直接最根源的解决办法。不过,用户有那么大请求量我们怎么可能减少呢?其实,对于越后端的系统,这是越可能的事情。我们可以在每一层都减少一部分往后传输的请求。具体到数据库的话,我们可以考虑通过增加cache命中率,减少数据库压力。增加cache命中率有很多中方法,比如对业务访问模式进行优化、多级cache模式、增加内存容量等等。业务模式的修改不是太好通用,因此这里我们考虑如何通过增加内存容量来解决问题。

对于单机,现在通用的cache服务一般都可以配置内存大小,这个只需要很简单的配置即可。另外,我们也可以考虑多机cache的方案,通过增加机器来扩充内存容量。因此,我们就引入了分布式cache,现在常用的cache(如:memcache),都带有这样的功能,支持多机cache服务,可以通过负载均衡算法,将请求分散到多台不同的机器上,从而扩充内存容量。

这里要强调一点。在我们选择均衡算法的时候,是有考虑的。这个时候,常常选贼一致Hash算法,将某一系列ID分配到固定的机器,这样的话,能放的KV对基本等于所有机器相加。否则,如果不做这样的分配,所有机器内存里面的内容会有大量重复,内存并没有很好的利用。另外,因为采用一致Hash,即使一台机器宕掉,也会比较均匀的分散到其他机器,不会造成瞬间其他机器cache大量失效或不命中的问题。

二、减少写入量。要减少用户的提交,这个看起来是不太现实的。确实,我们要减少写入的量似乎是很难的一件事。不过也不是完全不可能。这里我们会提到一个思想:合并写入。就是将有可能的写入在内存里进行合并,到一定时间或是一定条件后,再一起写入。其实,在mysql等存储引擎内部,都是有这样的机制的。打个比方,比如有一个逻辑是修改用户购买物品的数量,每次用户购买物品后,计数都加一。如果我们现在不是每次都去实时写磁盘,而是到一定的时间或一定次数后,再写入,这样就可以减少大量的写入操作。但是,这里需要考虑,如果服务器宕掉以后,内存数据的恢复问题(这一部分会在后面来描述)。因此,如果想简单的使用数据合并,最好是针对数据重要性不是很强的业务,即使丢掉一部分数据,也没有关系。

三、多机承担请求,分散压力。如果我们能将原来单机的服务,扩充成多机,这样我们就能很好的将处理能力在一定限度内很好的扩展。那怎么来做呢?其实有多种方法,我们常用的有数据同步和数据订阅。

数据同步,我们将所有的更新数据发送到一台固定的数据服务器上,由数据服务程序处理后,通过日志等方式,同步到其他机器的数据服务程序上。如下图:

这种结构的好处就是,我们的数据基本能保证最终一致性(即:数据可能在短暂时间内出现不一致,但最后的数据能达到一致),而且结构比较简单,扩展性较好。另外,如果我们需要实时数据,我们可以通过查询Master就行。但是,问题也比较明显,如果负责处理和分发的机器挂掉了,我们就需要考虑单点备份和切换方案。

数据订阅,我们也可以通过这样的方式来解决数据多机更新的问题。这种模式既是在存储逻辑和数据系统前,增加一个叫做Message Queue(消息队列,简称MQ)的东东,前端业务逻辑将数据直接提交到MQ,MQ将数据做排队等操作,各个存储系统订阅自己想要的数据,然后让MQ推送或自己拉取需要的数据。

MQ不带任何业务处理逻辑,他的作用就是数据转发,将数据转发给需要的系统。其他系统拿到数据后,自行处理。

这样的结构,好处是扩展比较方便,数据分发效率很高。但是问题也比较明显,因为处理逻辑分散在各个机器,所以数据的一致性难以得到保证。另外,因为这种模式看起来就是一个异步提交的模式,如果想得到同步的更新结果,要做很多附加的工作,成本很高且耦合度很大。还有,需要考虑MQ的单点备份和切换问题。

因为现在数据库(如Mysql)基本带有数据同步功能,因此我们在这个阶段比较推荐数据同步的方法。至于第二种方式,其实是很好的一种思想,后续我们会有着重的提及。那再来看我们的架构,就应该演变成这样的结构。

到目前这个阶段,我们基本上就实现了从单机到多机的转变。数据的多机化,必然带来的问题:一致性!这个是否有解决方案?这个时候我们需要引入一个著名的理论:CAP原理。

CAP原理包含了三个要素:一致性(Consistency)、可用性(Availability)、分区容忍性(Partition tolerance)。三个要素中,最多只能保证两个要素同时满足,不能三者兼顾。架构设计时,要根据业务需要进行取舍。比如,我们为了保证可用性和分区容忍性,可能会舍去一致性。

我们将数据分成多机,提高了系统的可用性,因此,一致性的保证很难做到强一致性。有可能做到最终一致性。这也是分布式引入以后的烦恼。

这样的一个系统,也是后续我们分布式架构的一个雏形,虽然比较粗糙,但是他还是比较简单实用,对于一般中型网站,已经能很好的解决问题。

【第七阶段:拆分】

到上面一个阶段,我们初步接触到了逻辑、存储等的多机模式。这样的结构,对于逻辑不是特别复杂的网站,足以撑起千万级的压力。所以大多数网站,只要能够用好上面的结构就可以很好的应对服务压力了。只不过还有很多细节的工作需要精细化,比如:多机的运维、稳定性的监控、日志的管理、请求的分析与挖掘等。

如果流量持续增长,或者是业务持续的扩展,上述的架构可能又将面临挑战。比如,多人开发常常出现版本冲突;对于数据库的更新量变大;一个表里面的记录数已经超过千万甚至过亿等等。

怎么解决呢?还记得我们之前介绍过一个CAP理论嘛?三要素里面有一个东东叫:分区容忍性(Partition tolerance)。其实,这个就是我们接下来解决问题的基础:切分!

一、从数据流向来看,切分包括:请求的切分、逻辑的切分、数据的切分。

数据的切分:将不同的数据放到不同的库中,将原来的单一的一个库,切分成多个库。

逻辑的切分:将不同的业务逻辑拆分成多份代码,用不同的代码管理路径来管理(如svn目录)。

请求的切分:将不同的逻辑请求分流到不同的机器上。比如:图片请求、视频请求、注册请求等。

二、从数据组织来看,切分包括:水平切分、垂直切分。

数据库的变大通常是朝着两个方向来进行的,一个是功能增加,导致表结构横向扩展;一个是提交数据持续增多,导致数据库表里的数据量持续纵向增加。

数据量变大以后,单机性能会下降很明显,因此我们需要在合适的时候对数据进行切分(这个我没有太深入的研究过相关数据库的最合适的切分点,只是从经验上来讲,单表的字段数控制在20个以内,记录数控制在5千万以内会比较好些)。

垂直切分和水平切分,其实是挺纠结的两个词。我之前对这两个词经常搞混。后来自己画了个图,就很直接明了了。

水平切分:

水平切分就是因为记录数太多了,需要横着来一刀,将原来一张表里面的数据存入到多张表中,用于减少单张表里的数据量。

垂直切分:

垂直切分就是因为业务逻辑需要的字段太多,需要竖着来一刀,将原来放在一张表里的所有字段,拆分成多张表,通过某一个Key来做关联(如关系数据库中的外键),从而避免大表的产生。

好了,有了上述的基础以后,我们再来看实际问题如何来解决。

假设,现在我们有一个博客网站,这个网站拥有多个功能,如:图片、博客、用户信息等的插查删改操作。而现在博客数据膨胀比较厉害。

首先,我们从数据流向来看,用户访问博客、图片、用户信息等这几个逻辑没有直接的耦合,对应的业务逻辑关联也很少。

因此,我们第一步从入口上就可以把三者分开。最简单的方式就是通过域名来切分,比如:img.XXX.com、blog.XXX.com、user.XXX.com。然后通过不同的WebServer来接收这些请求。

第二步,我们的业务逻辑代码,很明显可以将这些逻辑分开(从部署上分开)。一部分专门处理图片的请求,如ImageUploadAction/ImageDisplayAction/ImageDeleteAction,一部分专门处理博客请求,如:BlogDisplayAction/BlogDeleteAction,一部分专门处理用户相关请求,如:UserModifyAction/UserDisplayAction等等。

第三步,从数据库存储上,将三者剥离开。简单的就是分成三个不同的库。

这样,从数据流向上,我们就按不同的功能,将请求进行了拆分。

其次,从数据存储上来看,由于博客数据量增长比较快,我们可以将博客的数据进行水平的拆分。拆分方法很多,比如常用的:

1、按区间拆分。假定我们用blog_id作为Key,那么我们可以每1千万,做一次切分。比如[1,1kw)、[1kw,2kw)等等。这样做的好处就是可以不断的增长。但访问可能会因为博客新旧的原因,集中到最新的几个库或表中。另外,要根据数据的增长动态的建表。

2、按取模拆分。比如我们预估我们的blog_id最多不超过10亿,如果每张表里面我们预估存入1千万的数据,那么我们就需要100张表(或库)。我们就可以按照blog_id % 100 这样来做切分。这样做的好处是简单,表在一开始就全部建立好了。且每个表或者库的访问都比较均匀。但问题就是,如果数据持续扩张,超出预期,那么扩展性就成为最主要的问题。

3、其他还有一些衍生的方式,比如按Hash值切分等等,大多大同小异。

这样一来,我们通过访问模式、数据组织等多个维度的拆分以后,我们单机能够提供服务的能力就变的比较强悍了。具体的架构如下图。

上述结构看似比较完美,但是在实际的使用中可能会遇到以下几个问题:

1、业务关联问题。多个Service之间不可能没有任何关联,如果出现关联,怎么办?特别是如果是提交的信息要修改多个业务的数据的时候,这个会比较头疼。

2、服务运维问题。这样拆分以后,随着机器数量的膨胀,对于机器的管理将会变的愈发的困难。这个问题直接会影响到整体架构的设计。面向运维的设计是架构设计中必须要考虑的重要因素。

3、还有一个问题是我们WebServer始终是单机的,如果出现宕机等问题,那影响将是致命的。这个我们还没有解决。

这些问题都会在接下来的部分详细来解决。

【第八阶段:WebServer多机化】

上面说了这么多,我们的业务都基本上运转在只有一个WebServer的条件下。如果出现宕机,所有服务就停掉了;如果压力大了,单机不能承载了,怎么办?

说到这个话题,我们需要来回顾一下在大学时学习的关于网络的基本知识。^_^

抛开复杂的网络,我们简化我们的模型。我们的电脑通过光纤直接连入互联网。当我们在浏览器地址栏里面输入http://www.XXX.com时,到我们的浏览器展现出页面为止,中间出现了怎么样的数据变化?(注意:为了不那么麻烦,我简化了很多东西,比如:NAT、CDN、数据包切片、TCP超时重传等等)

上面的图我们应该比较熟悉,同时也应该比较清晰的表达了我们简化后,从输入网址到页面展现的一个过程。中间有两个东西我们比较关注,也是解决我们WebServer多机化的关键。

1、DNS服务是否能帮我们解决多机化?

2、www.XXX.com服务器的WebServer如何多机化?

首先,如果DNS解析能够根据我们的请求来区分,对于同一域名,将不同的用户请求,绑定到不同的ip上,这样,我们就(友情提示:word统计此处已经达到10000字)能部署多个WebServer,对应不同的ip,剩下的无非就是多申请几个ip地址而已。

当我们网站比较小的时候,我们都是在代理商处购买域名并由代理商的服务域名解析服务器帮我们做域名解析。但是,对于许多大型的网站,都需要对类似于www.XXX.com、blog.XXX.com、img.XXX.com等在XXX.com根下的所有服务的进行域名解析,这样便于对服务进行控制和管理。而域名的解析往往有专门的策略来处理,比如根据IP地域、根据不同请求IP的运营商等返回不同的服务器IP地址。(大家可能以前也有过这样的经验:在不同的地方,ping几个大的网站,看到的ip是不一样的)。

DNS策略分析和处理服务是对请求IP进行分析和判断的系统,判断请求来自哪个地域、哪个运营商,然后根据内部的一些库的判断,决定应该返回哪个WebServer的IP。这样,就能尽量保证用户以最快的速度访问到对应的服务。

但是,如果我们有大量的WebServer,那每个Server都要有一个IP,另外,我们要增加一个新机器,又要申请一个IP地址,好像很麻烦,且不可接受。怎么办呢?

第二点,我们需要考虑对于服务器的WebServer的多机化方式。

我们为什么要WebServer多机化?原因就是因为单机的处理性能不行了,我们要提升处理能力。

那WebServer要做哪些事情?Hold住大量用户请求连接;根据URL将请求分流到不同逻辑处理的服务器上;有可能还有一些防攻击策略等。其实这些都是消耗CPU的。

如果我们在WebServer前端增加一层,什么逻辑都不处理,就是利用一定的负载均衡策略将数据包转发给WebServer(比如:工作在IP层,而非TCP层)。那这一层的处理能力跟WebServer比是否是要强悍很多?!这样的话,这一层后面就可以挂载很多的WebServer,而无需增加外网IP。我们暂且叫这一层叫VS(Virtual Server)。这一层服务要求稳定性较高,且处理逻辑要极为简单,同时最好工作在网络模型中较低的层次上。

这样的话,我们就只需要几个这样的VS服务器组,就可以组建大量的WebServer集群。当一个群组出现问题,直接可以通过改变IP绑定,就可以切换到其他服务器组上。

现在这样的VS实现有多种。有靠硬件方式实现的,也有靠软件方式实现的。硬件方式实现的话,成本较高,但稳定性和效率较好。软件方式实现的,则成本较低,但稳定性和效率较硬件方式要低一些。

现在用的比较多的有开源的LVS(Linux Virtual Server),是由我国的一个博士写的,NB!以及根据LVS改写后的一些变种。

另外还有F5 Networks公司出的收费的F5-BIG-IP-GTM等。(注:这个确实没用过,以前在网上看过,写到此处记不清,在百度上搜的。如有错误,敬请雅正)。

好了,通过上述的方式,我们基本实现了WebServer的多机化。

【第九阶段:逻辑关联和层次划分】

在第七阶段的时候,我们提到了几个问题,其中有一个就是业务关联问题。当我们将业务拆分以后,多个业务之间没有了耦合(或者是极弱的耦合),能够独立的运转。这个看起来是多么美妙的事情。但是实际情况真是如此嘛?

这样的业务还真是存在的。比如我们有两个业务blog和image。blog可以上传和展示图片。那image.XXX.com就提供两个HTTP服务,一个是上传的,一个是显示的。这样,blog业务就可以通过简单的URL耦合来实现了图片的这些功能。

但真是所有的情况都是如此的嘛?

我们再看一个例子。比如blog和用户相关的业务。用户可以在blog登录、注销等,blog需要实时判断某一个用户是否登录等。登录和注销两个操作似乎可以通过类似account.XXX.com提供的login和logout这样的URL接口实现。但是每次页面浏览要判断用户是否已经登录了,出于安全性等多方面的考虑,就不好通过URL来提供这样的服务。

那看起来,我们在第七阶段提出的按业务切分的理想情况,在实施的时候,并不是那样的完美。在实际的运行中,耦合是不可避免的。

有了耦合,我的第一反应基本上就是看看是否能够借助设计模式来解决这些的问题。其实呢,设计模式早已经给我们比较好的解决方案(但绝对不是完美的解决方案。俗话说的:没有最好,只有更好!)。在这篇文章的最初已经提到过了,为了增强网站代码的可重用性,我们引入了一些框架,比如:struts、spring、hibernate等等。其实这些框架,基本上是围绕着MVC的原则来设计的。struts、webwork等框架,将视图和逻辑控制分离;spring负责组织业务逻辑的数据;hibernate很好的做了数据访问层的工作,实现了ORM。

那现在我们采用多机分布式的时候,是否可以借鉴这些思想呢?其实也是可以的。

我们来分析一下我们的业务。

其实我们的业务大多可以分为两类:

一、与实际的产品相关的业务,比如:blog、news等等。这些业务之间的耦合度不是很高,往往可以通过提供HTTP的接口即可实现业务需要的互通。因此,从这个层面上来看,是可以基本做到业务垂直拆分的。

二、基础服务,比如:用户帐号管理、消息通知等等。这些服务往往被多个业务所依赖。他们需要提供更通用的、更安全的、更稳定的接口和服务。但是,关于基础业务的理解和划分,是没有一个特定的规则的。比如,image图片服务,他有可能刚开始是一个业务服务,到一定阶段以后,多个系统需要对他有强的依赖,自然也就成为了一个基础服务。

所以,从上面的描述,我们可以发现服务的类型,并不是固定的。要很好的解决服务耦合的问题,也并没有一个十分完美的解决方案。我们能做的就是尽量降低耦合,通过某种方式,能够很好的达到耦合和可维护性的平衡。

总的来看,MVC模式其实给我们提供了一个比较好的方案。

我们把系统从两个维度上进行划分:垂直(业务)和水平(逻辑)。

我们做了这样的划分以后,似乎看起来没有实质性的改变。但是,我们可以明确了我们设计的原则,并强化了代码的可复用性。另外,最关键的是,服务之间的依赖和耦合关系,有了明确的地方来做。同时,我们还可以将业务内部的结构进行拆分,更好的增强复用。

数据访问和组织层、数据存储层,这两个位于下游的层次,应该是属于系统内部的层次,原则上是最好不要对外开放接口的,否则,系统间的耦合就会非常的大。并且可维护性会非常的难。而逻辑控制和视图层,实际上是提供对外(对用户或者是外系统)最好的访问的入口。当然,这个入口可以是HTTP协议的,也可以是非HTTP协议的。

比如,对于account服务,可以提供基于HTTPS对用户开放的login和logout服务,也提供基于XXX Protocol数据交换的协议的给内部的get_session服务。从简单的设计上来看,只是根据服务不同,提供不同的数据交换格式、以及不同的安全控制。这样也是秉承了一个高内聚低耦合的原则。

这里还有几个及其重要的问题没有详细的提及:系统内外的数据传输协议、接口API、服务访问定位。这几个问题实际上还跟运维问题紧密相关,都会放到后面来详细讨论。

【第十阶段:数据存储优化】

在前面的阶段中,我们都使用数据库作为默认的存储引擎,很少谈论关于关于数据存储的话题。但是,数据的存储却是我们现在众多大型网站面临的最核心的问题。现在众多网络巨头纷纷推出自己的“高端”存储引擎,也吸引了众多的眼球。比如:google的BigTable、facebook的cassandra、以及开源的Hadoop等等。国内众多IT巨头也纷纷推出自己的“云”存储引擎。

其实这些存储引擎用的一些关键技术有许多的共性,比如:Meta信息管理、分片、冗余备份、数据自动恢复等。因为之前我也做过一些工作和研究,但是不是特别深入,不敢在此指手画脚、高谈阔论。相关的资料网上比比皆是,大家有兴趣有search吧。^_^

关系型数据库常用的有几个,比如:MySQL、PostgreSQL、SQLServer、DB2等,当然还有NB的Oracle(唉,作为DB科班出生的屌丝,没有真正意义上使用过这样的高帅富数据库,惭愧啊)。

互联网使用频率最高的应该要算是MySQL。最重要的是开源;其次是他提供的一些特性,比如:多种存储引擎、主从同步机制等,使用起来非常的方便;再次,就是一个单词:LAMP,几乎成为搭建网站的必备利器;还有,较高服务的稳定性。

关系型数据使得建立网站变得及其轻松,几乎是个网站都会有一个数据库。试想一下,如果没有这种通用的关系型数据库,我们的生活会是怎么样的?

关系型数据库在95%的场景下是工作的非常好的。而且只要配置得当、数据切分合理、架构设计符合要求,性能上是绝对能够承受业务的需求。现在很多大型网站的后台,几乎都是数据库作为标准的存储引擎。

另外,最近炒的比较热的一个概念就是NoSQL。说起来,其实就是放弃关系型数据库中许多的特性,比如:事务、外键等等。简化设计,将视线更关注于存储本身。比较有名的,比如:BDB、MongoDB、Redis等等。这些存储引擎提供更为直接的Key-Value存储,以满足互联网高效快速的业务需求。其实,从另外一个角度来看,关系型数据库(比如MySQL),如果不使用那么多的关系型数据库特性,也可以简化成KV模式,提升效率。

不过,有些时候,为了节省机器资源,提升存储引擎的效率,就不得不开发针对业务需要的专用存储引擎。这些存储引擎的效率,往往较关系数据库效率高10-100倍。比如,当一个图片服务,存储的图片量从1亿到10亿,甚至100亿;现在流行的微薄,假如发布总量达到10亿或者100亿。这样级别的数据量,如果用数据库来存储固然可以,但是有可能需要耗费相当多的机器,且维护成本和代价不小。

其实分析我们通常的业务,我们对数据的操作无非就是四个:查、插、删、改。对应数据库的操作就是select、insert、delete、update。那自己设计的存储引擎无非就是对这四个操作中的某几个做针对性的优化,让其中几个根据不同的业务特点,使其变的更加的高效。比如:对于微薄而言,可能就需要插入和查询具有很高的效率,而删除和修改的需求不是那么高。这个时候,就可以牺牲一部分删除和修改的性能,而重点放在插入和查询上。

在业务上,我们的提交通常可以看作在某一个维度上是有序的。比如,每一个微薄或者博客可能都会有一个id,这些id可能是按照序列递增、或是时间递增等。而查询的时候,则是按照另外一个维度的顺序组织的,比如:按关注的人组织微薄的信息。这样就造成了一个冲突,就是提交的组织顺序和查询的组织顺序不一致。

再来看看我们的磁盘。我们现在常规磁盘还是机械方式运转的:有盘片、有磁头等等。当需要写入或者读取的时候,磁头定位到不同的盘片的不同扇区上,然后找到或修改对应的信息。如果信息分散在不同的盘片、扇区上,那么磁头寻道的时间就会比较长。

反过来,我们再来看看我们的业务和磁盘的组织。A、如果我们按照写入有序的方式存储数据,那么磁盘会以很高的效率,将数据连续的写入到盘片中,无需多次寻道。那么读取的时候,可能就会出现按照另外的维度来组织数据,这样就有可能需要在多个地方来读取。从而造成我们磁盘来回寻道定位,使得查询效率低下。B、如果我们按照某一种查询维度来存储数据(因为同一业务往往有多种查询模式),那读取的时候,就让磁盘顺序读取即可。但带来的麻烦就是,写入的时候有可能需要反复的寻道定位,将提交的数据一条条写入。这样就会给写入带来麻烦。

其实矛盾的主体就是:僵化的磁盘存储方式不能满足网站日益增长的提交和查询需求

是否有解决方案呢?那必须有!

要解决这个问题,可以从两方面入手。

1、改变现有的磁盘存储方式。随着硬件快速的发展,磁盘本身的效率得到了极大提升。磁盘的转速,盘片的个数等都大幅增加,本身寻道的速度提升很快。加上缓存等的加强,效率提升还是很明显。另外,Flash Disk、Solid State Disk等新技术的引入,改变了原来的随机读取的低效(没有数据,根据经验,Flash或SSD的效率可以达到10-100倍普通硬盘的随机访问的效率)。

2、根据不同的业务,有效的组织数据。比如微薄(我没有写过微薄,但是做过微薄类似的东东),因为读取业务组织的维度是按人,而提交组织的维度则是时间。所以,我们可以将某个人一段时间提交的数据,在内存里面进行合并,然后再一次性的刷入到磁盘之中。这样,某个人一段时间发布的数据就在磁盘上连续存储。当读取的时候,原来需要多次读取的数据,现在可以一次性的读取出来。

第一点提到的东东现在也逐步的开始普及,其实他给我们的改变是比较大的。不用花太多的精力和时间,去精心设计和优化一个系统,而只需要花一些钱就能使得性能大幅提升,而且这样的成本还在降低。

但是,资源永远是不够的。多年前,当内存还是64K的时候,我们畅想如果内存有个32M该多美妙啊。但是,随着数据的膨胀,即使现在64G内存,也很快就不能满足我们的需求。

所以,在一些特殊的应用下面,我们还是需要更多的关注第二个点。

对于存储优化,一直是一个持久的话题,也有很多成熟的方案。我这里可能提几个点。

【阶段性小结】

经过了上述的架构扩展和优化以后,我们的系统无论是从前端接入,还是后端存储都较最初的阶段有了质的变化。这样的架构足以支撑起10亿级别的流量和10亿级别的数据量。我们具体的来看一下整体的架构。

上述的模型是我个人觉得的一个比较理想的模型。Virtual Server Cluster接收数据包,转发给Web Server Cluster或者Private Protocol Server Cluster(如果有的话)。然后视图和逻辑层server负责调用cache或者数据访问组织层的接口,返回处理后的数据。定制存储系统、通用存储系统和数据库集群,提供基本的数据。

每一个层次通过负载均衡和一定的协议来获取下一层提供的数据,或者提交数据。在存储系统内部,通过Meta信息管理、主从同步、消息订阅等方式,实现数据的同步。

如果我们再要扩大规模,比如:机器数扩展到上千台、万台。对于我们来说,管理机器就成为了机器头等的大问题。

同时,我们之前还有几个问题没有很详尽的描述,比如:数据传输协议、远程系统调用、系统的异构性等等,这些都是会影响到我们系统可维护性的大问题。

我7年前就开始使用Java,到现在,总算能看懂一些东西了。J2EE我个人觉得确实是一个比较伟大的东东。里面其实早已经提出了一套比较完善的解决大型或者超大型网站的整体解决方案。

比如:

1、JNDI(Java Naming and Directory Interface):描述了如何使用命名和目录等的规范,使得我们能够将服务作为资源挂接到命名服务器上,并且通过标准的接口进行访问。这对我们管理巨大的机器资源和服务提供了很好的方案。

2、RMI(Remote Method Invoke):远程过程调用。即,通过标准的RMI接口,可以轻松实现跨系统的远程调用。

3、IDL(Interface Definition Language):接口定义语言。这个本来是CORBA中用来访问异构系统对象的统一语言,其实也给我们提供了跨系统调用中,对外接口的定义方案。

4、JDBC(Java Database Connectivity):数据库访问接口。屏蔽了不同数据库访问的实现细节,使得数据库开发变得轻松。

5、JSP(Java Server Page):实现将视图和控制层很好分离的方式。

6、JMS(Java Message Service):用于和面向消息的中间件相互通信的应用程序接口。提供了很好的消息推送和订阅的机制。

以上这些组件其实很好的协助构建了J2EE整体架构。我的很多想法都来源于这些东东。后续会结合这些,详细来分析诸如资源命名位置服务、数据传输协议、异构系统接口定义等解决大规模机器运维问题的方案。

【第十一阶段:命名位置服务】

在前面我们不止一次提到了命名位置服务(Naming & Location Service)。在不同的架构或者公司里面,这个名字往往不一样,比如,在java里面叫JNDI(Java Naming & Directory Interface),在有些地方可能会叫做资源位置系统(Resource Location System)。

总之,不管叫什么名字,我们要知道的就是为什么要有这样的系统?他能做哪些事情?他有哪些实现方式?等等。

在我们之前的章节中,我们的服务从一台单机扩展到十台左右的多机,到成百上千台机器。我们的服务从单一的一个服务扩展到成百上千的服务。这么多的机器、服务,如果不好好管理,我们就崩溃了。比如,我们的服务A要连接服务B,如果现在采用配置IP的方式,可能需要配置几十台机器的IP,如果其中某些机器出现了变更,那所有服务A连接服务B的IP都要改变。如果所有的服务都是这样,这将是多么痛苦的一件事情 Orz。

如果我们只是简单的将我们的服务看成是一个个的资源(Resource),这些资源可以是数据库,可以是cache,可以是我们自己写的服务。他们都有一个共同的特点,就是在某一个IP上,打开一个PORT,遵循一定的协议,提供服务。

我们先简单的来构建这样一个模型。我们这里先抽象一个接口,叫做Interface Resource。这个接口下面有多个实现,比如:DBResource、CacheResource、ImageResource等等。具体类图如下:

有了这样的一个层次结构以后,我们为了得到某一个实例,有多种方式,比如:

1、直接生成的方式:

Resource r = new ImageResource();

2、间接生成的方式:

A、比如我们在设计模式中经常使用到的工厂模式:

Resource r = ResourceFactory.get("Image");

B、IoC(Inversion of Control)方式:

Resource r = (Resource)Container.getInstance("ImageResource");

我们打一个不是很完全匹配的比方。我们如果直接在服务中采用IP配置的方式,就类似于直接生成实例一样,如果实例发生变化,或者要调整生成的对象,就需要修改调用者的代码。也就是说,如果其他服务的IP发生变化,我们调用者就需要修改配置,重启程序等等。而且对于如果有很多很多这样的服务,我们就崩溃了。

那么,我们觉得更好的一种方式呢,就是,如果有一个工厂,或者一个容器,来帮我们管理这一堆的服务IP、端口、协议等等,我们要用的时候,就只需要叫一声:“给我XXX服务的实例”,那是多么美妙的事情啊!

其实呢,我们的命名位置服务要做的就是这样的事情。他类似于一个Meta Server,记录所有服务的IP、port、protocol等基础信息,以及检查这些服务的健康状态,提供给调用者最基础的信息服务。同时,再结合调用时的负载均衡策略,就可以帮我们提供很好的资源管理方式。

这个服务,提供注册、注销、获取列表等接口。他的存在,就将直接关联的两个服务给很好的解耦了。我们看看对比:

在没有Naming Location Service的时候,我们的服务相互直接依赖,到最后,关联关系及其复杂,可能完全没有办法维护。

如果我们增加Naming Location Service以后,这个状态就可以得到极大的改善。

这个时候,我们所有的服务都在NLS上注册,同时向NLS获取其他服务的信息,所有的信息都汇聚到NLS上管理。

有了这个服务,就好类比成我们生成一个类的时候,采用间接的方式生成。

Service s = (Service) NamingService.getService("Image");

好,有了这样一个架构以后,我们可能会关注这个NLS如何来实现。

实现这个服务有简单的方式,也有复杂的方式。关键是要考虑以下几个方面:

1、如何找到这个NLS。NLS是所有服务的入口,他应该是有一个不变的地址来保证我们的服务。因此,我们可以使用我们之前提到过的Virtual Server的方案,通过一个(或多个)固定的域名或者IP来绑定这个服务。

2、可用性(Availability)和数据一致性(Consistency)。因为这个服务是一个最基础的服务,如果这个服务挂掉了,其他服务就没有办法来定位了。那么这个服务的稳定和可靠性就是及其重要的。解决方案有如下几种:

A、单机实现服务,本地增加备份。我们用单机来实现这样一个服务,这样可以保证绝对的数据一致性。同时,每次请求数据后,每个服务本地保留一份备份数据。当这个服务挂掉了,就使用最近的一次备份。这个方案对于大多数情况是足以应付的,而且具备简单粗暴有效的特点。

B、多机服务,数据同步。采用多机提供服务,信息更新时进行数据同步。这种方式的优点就是服务可以保证7*24小时服务,服务稳定性高。但是,问题就是维护成败会比上面一种方式高。

3、功能。实现服务名称到IP、Port、Protocol等信息的一个对应。如果要设计的更通用,比如可以注册任何信息,就只需要实现Key-Value的通用数据格式。其中,Value部分需要支持更多更丰富的结构,比如List、Set等。

有了这样的一个系统,我们就可以很方便的扩展我们的服务,并且能很好的规范我们服务的获取、访问接口。

【第十二阶段:传输协议、接口、远程调用】

这一部分主要谈谈关于协议、接口和远程调用相关的内容。本来这一部分应该在之前就有比较详细的讨论,不过我放到后面来,足见其重要性。特别是在系统越来越多的时候,这几个东东直接决定了我们的开发速度和运维成本。

好,接下来我们一个个的看。

1、传输协议

到目前为止,在不同系统之间获取数据的时候,你是采用那种方式呢?

我们简单看一个例子:

以上这个可能是我们最(|两万字的分隔线|)初学习网络编程的时候,最常使用的一种C-S交互方式。其实这里面我们已经定义了一种交互协议,只是这种方式显得比较山寨,没有规范。扩充性等等都没有充分考虑。

我们在学习网络编程的时候,老师就给我们讲过,网络分层的概念,经典的有5层和7层模型。在每一层里面,都有自己的协议。比如:IP协议、TCP协议、HTTP协议等等。这些协议基本上都由两部分组成:头+数据。

【头信息】

我们来看看TCP协议:

(注:以上是我从百度百科上截取的)

头信息中,一般可能会包含几个重要的元素:协议标识符、版本号、串号(或是本次交互的id)、数据包长度、数据校验等信息,可能有些协议还会带一些其他数据,比如数据发出方名称、接收方名称、时间、保留字段等等信息。

这些信息的目的,就是为了清晰的表达,我是怎么样的一个协议,我有哪些特征,我带的数据有多大。方便接收方能够清晰的辨认出来。

【数据部分】

数据部分是为了让应用层更好的通讯和表达数据。要达到的目标就是简洁高效、清晰明了。说起来很容易,但是实现起来要考虑的东西就比较多,比如:数据压缩、字符转移、二进制数据表达等等。

我们通常有多种格式来作为数据部分的协议。大体上可以分为:

A、二进制流。比如:C里面的结构体、JAVA里面的Serializable,以及像Google的protobuf等。将内存里面实体的数据,按字节序列化到缓冲区。这种方式的好处就是数据非常紧凑,几乎没有什么浪费。但是,问题也比较明显。双方必须很清楚协议,面向的语言基本上是要求一样的,很难做兼容,跨平台差。且扩展性比较差。另外,还有网络大小端字节序(Big-Endian、Little-Endian)的问题需要考虑。

B、文本传输协议。就是以字符串的方式来组织信息。常见的有XML、JSON等等。这种方式的好处就是扩展性强,跨平台兼容能力好,接口标准且规范。问题就是传输量比较大,需要考虑做压缩或者优化。

XML方式:

JSON方式:

因此,我们可以按照我们实际的需求,来定制我们想要的数据格式,从而达到高效和易于表达和扩展的效果。

经过上述分析,我们基本上对传输协议有了一个比较大致的了解。有了传输协议之后,我们的跨机器间的数据交互,才显得比较规范和具有扩展性。如果我们还不想自己来定义协议,我们可以用现有的协议进行组合,比如:HTTP+XML、HTTPS+JSON等等。只要在一个平台上,大家都遵守这样的规范,后续开发起来就变得轻松容易。

2、接口(或者API)

接口是我们的服务对外表达的窗口。接口的好坏直接决定了我们服务的可表达性。因此,接口是一个承上启下的作用。对外,很好的表达提供的服务名称、参数、功能、返回的数据等;对内,能够自动生成描述所对应的代码函数框架,让开发者编写实现。

我们描述我们接口的方式有很多,可以利用描述性语言来表达。比如:XML、JAVA里提供的IDL(Interface Definition Language)等等。我们把这种描述接口的语言统一称为IDL(Interface Definition Language)。

我们可以自己开发一些工具,将IDL进行翻译,转换成方便阅读的HTML格式、DOC、CHM等等。方便其他开发者查阅。

同时,另外一方面,我们可以将IDL转换成我们的接口代码,让服务接口的开发者和调用方的开发者按规范和标准来实现。

对于下层代码如何来实现数据的解析、函数的调用、参数的传递、数据的转换和压缩、数据的交换等等工作,则由工具来生成。对上完全屏蔽。详细的内容,我们将在接下来的远程调用中来分析。

3、远程调用

我们最初写代码的时候,就被教授了函数的概念。我们可以将一些公用的代码,或者实现一定含义或逻辑的代码,做成一个函数,方便重复的使用。

最开始,这些函数往往在同一个文件里,我们只要先申明,即可使用。

后来,我们开始使用库里面的函数,或是将函数封装成一个个的库(比如C里面的静态库.a或者动态库.so,或者是Java里面的.jar)。

以上对于函数的调用,以及函数自身的处理都是在本地。假如,当我们单机不能满足需求的时候,我们就需要将函数的处理放到其他机器上,让机器做到并行的计算。这个时候,我们就需要远程的函数调用,或者叫做远程过程调用(Remote Procedure Call 或者 Remote Method Invoke,简称RPC或者RMI)。

我们在这之前讲过几个东东:负载均衡、命名位置服务、协议、接口。其实前面讲这几个东东都是为了给远程过程调用做铺垫。RPC都是建立在以上部分的基础上。

还是按照我们之前分析问题的思路:为什么要这个东东?这个东西解决什么问题?如何实现?有哪些问题?等等来分析RPC吧。

RPC的目的,就是使得从不同服务上获取数据如同本地调用一样方便和自然。让程序调用者不需要了解网络细节,不用了解协议细节,不用了解服务的机器状态细节等等。如果没有RPC,其实也是可以的,就是我们写程序的时候难受点而已,哈哈。

接下来,我们看看如何来实现。

以上是整个的一个大体静态逻辑。最先编写调用的IDL,完成后由工具生成接口说明文档(doc);同时,生成客户端调用代码(stub,我们叫做存根);另外,需要生成server端接口框架(skeleton),接口开发者实现具体的代码逻辑。

以上就是客户端调用的整个逻辑。

【第十三阶段:分布式计算和存储的运维设计与考虑】

以上的部分已经从前到后的将系统架构进行了描述,同时针对我们会遇到的问题进行了分析和处理,提出了一些解决方案,以保证我们的系统在不断增长的压力之下,如何的良好运转。

不过,我们很少描述运维相关的工作,以及设计如何和运维相关联。系统运维的成败,直接决定了系统设计的成败。所以系统的运维问题,是设计中必须考虑的问题。特别是当我们有成千上万的(tens of thousands)台机器的时候,运维越发显得重要。因此,在系统设计初期,就应当把运维问题纳入其中来进行综合的考虑。

如果我们用人来管机器,在几台、几十台机器的时候是比较可行的。出了问题,人直接上,搞定!不过,当我们有几百台、几千台、几万台、几十万台机器的时候,我们如果要让人去搞定,那就未见得可行了。

首先,人是不一定靠谱的。即使再聪明可靠的人,也有犯错误的时候。按照一定概率计算,如果我们机器数量变多,那么出错的绝对数量也是很大的。同时,人和人之间的协作也可能会出现问题。另外,每个人的素质也是不一样的。

其次,随着机器数量的膨胀,需要投入更多的人力来管理机器。人的精力是有限的,机器增多以后,需要增加人力来管理机器。这样的膨胀是难以承受的。

再次,人工恢复速度慢。如果出现了故障,人工来恢复的速度是比较慢的,一般至少是分钟级别。这对于要提供7×24小时的服务来说,系统稳定运行的指标是存在问题的。同时,随着机器的增多,机器出问题的概率一定的条件下,绝对数量会变多,这也导致我们的服务会经常处于出错的情况之下。

还有,如果涉及到多地机房,如何来管理还是一个比较麻烦的事情。

如果我们能转换思维,在设计系统的时候,如果能有一套自动化管理的模式,借助电脑的计算能力和运算速度,让机器来管理机器,那我们的工作就轻松了。

比如,我们可以设计一套系统,集成了健康检查、负载均衡、任务调度、自动数据切片、自动数据恢复等等功能,让这套系统来管理我们的程序,一旦出现问题,系统可以自动的发现有问题的机器,并自动修复或处理。

当然以上都是比较理想的情况。凡事没有绝对之说,只是需要尽可能达到一个平衡。

好了,说了这么多的问题,要表述的一点就是:人来管理机器是不靠谱的,我们需要尽量用机器来管理机器!

接下来,我们比较简单的描述一下一个比较理想的自动化管理模型。

我们将我们的系统层次进行初步的划分。

我们将系统粗略的划分为三个层次:访问接入层、逻辑处理层和数据存储层。上一次对下一层进行调用,获取数据,并返回。

因此,如果我们能够做到,说下一层提供给上一层足够可靠的服务,我们就可以简化我们的设计模型了。

好,那如果要提供足够可靠的服务,方便调用的话,应该如何来做呢?

我们可以针对每一层来看。

首先,看看访问接入层:

所有的Web Server和 Private Protocol Server将服务注册到命名和定位服务上。一旦注册后,NLS会定期去检查服务的存活,如果服务宕掉,会自动摘除,并发出报警信息,供运维人员查阅。等服务恢复后,再自动注册。

Virtual Server从NLS获取服务信息,并利用负载均衡策略去访问对应的后继服务。而对外,只看到有一台Virtual Server。

接下来,我们看看逻辑层:

由于HTTP的无状态性,我们将逻辑代码按标准接口写成一个个的逻辑处理单元,放入到我们的逻辑处理容器之中,进行统一的运行。并通过容器,到NLS中注册。Virtual Server通过NLS获取到对应的信息,并通过负载均衡策略将数据转发到下游。

这里比较关键的数据处理单元,实际就是我们要写的业务逻辑。业务逻辑的编写,我们需要严格按照容器提供的规范(如IDL的标准等),并从容器获取资源(如存储服务、日志服务等)。这样上线也变的简单,只需要将我们的处理单元发布到对应的容器目录下,容器就可以自动的加载服务,并在NLS上注册。如果某一个服务出现异常,就从NLS上将其摘掉。

容器做的工作就相对比较多。需要提供基本的服务注册功能、服务分发功能等。同时,还需要提供各种资源,如:存储服务资源(通过NLS、API等,提供存储层的访问接口)、日志服务资源等,给处理单元,让其能够方便的计算和处理。

总体来说,因为HTTP的无状态特性,以及不存在数据的存储,逻辑层要做到同构化是相对比较容易的,并且同构化以后的运维也就非常容易了。

最后,看看数据存储层。

存储层是运维设计中最难的一部分。因为根据不同的业务需要,可能提供不同的存储引擎,而不同的存储引擎实现的机理和方式可能完全不一样。比如,为了保证数据的有效性和一致性,有些存储系统需要使用事务;而有些业务,可能为了追求高效,可能会牺牲一些数据的一致性,而提供快速的KV查询等等。

因此,数据存储层的异构性就是运维设计中亟待解决的问题。

一种比较理想的方式,就是让各个存储系统,隐藏内部的实现,对外提供简单的访问接口。而在系统内部,通过meta server、data assemble server、status manager、message queue等管理单元来管理数据存储单元。当然,这只是其中一种方式。也可以利用mysql类似的主从级联方式来管理,这种方式也是可行的。

数据存储系统的设计,也没有一个固定的规范(比如:Big Table、Cassandra、Oracle数据库集群等),所以运维的设计需要在系统中来充分考虑。上述图示只是提供了一种简单的设计方案。

好了,有了多个层次的详细分析以后,上一层次调用下一层次,就直接通过固定的地址进行访问即可。

不过有一个问题就是,如果我们的Virtual Server是一个单点,出现故障后,该层就不能提供服务。这个是我们不可接受的。怎么办呢?

要解决这样的问题,就是利用冗余。我们可以将我们的服务划分为多个组,分别由多个VS来管理。上层调用下层的时候,通过一定的选择策略来选择即可。这样,如果服务出现问题,我们就能通过冗余策略,将请求冗余到其他组上。

我们来看一个实例:

用户通过域名访问我们的服务,DNS通过访问IP解析,返回对应访问层的IP地址。访问层将请求转发到对应的逻辑处理组,逻辑处理组从不同的存储系统里获取数据,并返回处理结果。

通过以上的分析,我们通过原有的一些技术手段,可以做到比较好的自动化运维的方式。不过这种运维方式也不是完全智能的。有些时候也需要人工的参与。最难的一点就是存储系统的设计和实现。如果要完全的自动化的话,是一件比较难的事情。

说明:以上的描述是一个比较理想化的模型,要真正实现这种模型,需要很多的辅助手段,并且需要搭建很多基础设施。可能会遇到我们没有提到的很多的实际问题,比如:跨机房网络传输延迟、服务间隔离性、网卡带宽限制、服务的存活监控等等。因此,在具体实施的时候,需要仔细分析和考虑。

文章来源:老王的技术博客