编者注:本文为历史博文归档;涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效,阅读时请注意时效性。

引言

服务切分是最基本且最多变的架构思路之一。说它基本,是因为在学习程序设计的第一天就应该知晓;说它多变,是因为在未来的职业生涯中,你会不停地应用这个方法来解决问题。不幸的是,「切分」这个思路并没有得到应有的重视。

或许是因为这个词听起来比较朴实,远不如「并行」、「集群」、「负载均衡」这些术语显得高端。因此,当碰到问题时,往往被拿出来的解决方案会是以上三个大词之一,很少有人去认真考虑切分问题。但事实上,这三个大词所需的技术,其实也是建立在良好可切分的系统之上的。

最近碰到了两个项目,都是典型案例。

典型案例分析

案例一:盲目追求负载均衡

某小公司发展不错,一台服务器眼看不够用了,于是就买了第二台,希望能做一个「负载均衡」系统。很多人大概认为负载均衡是类似自来水一样的技术,只要打开水龙头,清水就汩汩涌出,往往忘记了水龙头后面的水管和自来水公司

一个服务器上放了很多个服务,是很难应用负载均衡这种技术的。必须先要把服务拆开,找到性能薄弱的点,对这个点进行负载均衡,才能得到比较好的效果。否则很可能用了 80% 的力气,但是只得到 20% 的结果。

案例二:全镜像集群的隐患

某外企,之前我们给他们做过咨询,解决了一次性能问题。上次我们找到了系统最慢的地方,去掉了老系统中性能消耗多的地方,用通用的缓存(Squid/Memcached)系统来代替他们用 PHP 写的硬盘文件缓存。上次的改动让性能困扰远离了他们一年,但随着业务发展,性能又显得不够了。

比起一年前,他们的服务器数量增长了一倍,其中大量服务器用于做 MySQL 集群和 Web 集群,服务器是复杂的网状关系。前面说过,这种不分主次的架构往往只能解决 20% 的问题,而且很容易到达瓶颈。

套用回那三个大词,这个系统确实是集群的、负载平衡的。因为所有服务器的功能是完全一样的,任何一台单独拿出来,都是完整的系统镜像。看上去很美,但实际用起来可没有这么理想。在实际应用中,一台服务器出了问题,几分钟之后,所有的服务器都出问题了,并没能如预期想象的那样——损坏一两台不影响正常工作。

这个案例中,实际使用了 6 台服务器做 MySQL 集群,每台服务器的数据是完全镜像。可以想象,如果 6 台服务器中有一台出了故障,所有的压力就会被平均分到其他 5 台上。在集群负载大的情况下,增加 1/5 压力,很可能让另外的 5 台直接被压垮。这时候发生的情况,就好像最近株洲塌了的大桥一样,一个桥墩一个桥墩,好像多米诺骨牌一样,一路塌了下去。

从任何角度看来,这都不是一个好架构,别说稳定了,连基本的服务都保证不了。

解决方案:先切分,后优化

对于这两个案例,我给出的解决方案都是先做切分,然后再分别进行优化。首先可以按照业务切分,把现有服务器分成多个组。 对于 Web 前端和 MySQL 集群,每组 2~3 台足够应付大多数情况。每组服务器数量太多,不仅造成单点故障的概率增加,也容易在自身的数据同步中消耗大量的资源。

比较以下两张图:

改造前:
改造前

改造后:
改造后

之前的结构是所有 Web 服务器放着同样的应用,所有数据库服务器放着同样的数据。改进后的结构是将整个系统按照服务内容和性质拆成了几组。 从 ServiceA 到 Service n 采用了不同的配置方式,简单的只用一组机器,复杂的用多组。在实践中,其实还可以分得更细致,甚至几个 Service 公用一组机器。

后者的好处很明显,最主要的包括:

  1. 可以有效使用资源:比如服务 A 是低负载型的,并非核心业务,可以给予较少的资源(较少的机器、较差的机器);而服务 B 是核心服务,需要全力支持。
  2. 可以简单监控性能:所有服务都混在一起的时候,碰到性能问题很难下手分析。很有可能是最不常用的服务 A 中有 Bug,导致了整个集群性能低下。寻找这种 Bug 要耗费大量的人力和时间。
  3. 易于备份,升级时切换服务平滑
  4. 为下一步优化做好准备:综合以上几点,下一步可以集中资源,对重要服务专门进行优化,保证稳定和性能。

在以上的例子中,我们可以看到,专门对重要服务 B 进行负载均衡,无论在成本上还是性能受益上,都比对整个应用进行处理划算得多。

切分的多维度应用

除了根据明显的业务边界进行切分,还有很多种方式,比如按照时间、按照读写频率等。

这种思路也不仅仅应用于 Web Server/Database。比如,设计缓存的时候,也应该进行一定的切分。以 Squid 为例,应该把文件大小差不多的文件放在一个 Squid 中,把更新频率接近的文件放在一个 Squid 中。这些都是让架构清晰、性能提高的办法。

设计原则:降低耦合度

从系统设计的初期,就应该贯彻这个思路。用子域名来标记不同的服务(img.yourdomain.com, bbs.yourdomain.com...),比用虚拟目录的方式(www.yourdomain.com/img, www.yourdomain.com/bbs)更便于切分。

很多人咨询过我这个问题,不过他们都是从 SEO 和产品方面考虑,从来没想过这是和技术相关的。如果所有的应用都放在 www 之下,未来一旦想把一个服务挪到另外一台服务器上,就很困难——不是没有办法,只是难度比较高。子域名只需要设置一下域名解析,把要挪走的服务指向新的 IP,几分钟就搞定了。

这个办法的实质,仍然是降低系统耦合度。必要的关联数据,宁可采用 HTTP 请求的 API 形式,也不要跨数据库进行查询。 HTTP 请求可以很方便地跨服务器,可以设置页面缓存等;跨数据库请求则只能在数据库上花力气,最后又退回了对全部数据做数据库集群、Master/Slave 的老路上(当然,这不是一条原则,还是要根据应用的具体情况而定)。诸如此类,尽量要将思路放远,不要贪图眼前的小得失。(常见的说法:数据库怎么也比 HTTP 快吧...这没错,不过要看应用场合

结语

能在一个单一的服务中承载尽量多的数据和请求,这当然是所有技术工作者不懈的追求。在此之前,如果先进行合理的切分,再令每个服务的性能最大化,一般都能得到更大的好处。简单是美,简单就是力量。

说明:本文提及的部分技术栈(如 Squid、PHP 文件缓存、特定 MySQL 集群方案)具有历史背景,具体实施请参考当前主流架构与官方文档。文中外链图片可能因年代久远失效,仅供架构思路参考。