大型网站技术架构的演进

摘要 最近我在阅读2本关于大型网站架构的书:《大型网站技术架构——核心原理与案例分析》李智慧、《大型网站系统与Java中间件实践》曾宪杰。 我期望从这些书中学习到大型网站是如何做架构的,这个过程会遇到什么问题。当看完这2本书后,我总结出两个大问题: 1. 网站技术架构为什么会演进?换个说法就是为什么网站会变大? 2. 演进的过程会遇到什么问题?或者说为了演进,会遇到什么问题?


最近我在阅读2本关于大型网站架构的书:《大型网站技术架构——核心原理与案例分析》李智慧、《大型网站系统与Java中间件实践》曾宪杰。

我期望从这些书中学习到大型网站是如何做架构的,这个过程会遇到什么问题。当看完这2本书后,我总结出两个大问题:

1. 网站技术架构为什么会演进?换个说法就是为什么网站会变大?

2. 演进的过程会遇到什么问题?或者说为了演进,会遇到什么问题?

网站技术架构为什么会演进

我个人总结出来我们的技术架构演进的两种驱动力,驱动着我们为什么演进网站的技术架构:

1. 内在驱动力:我们期望把当前的业务做得更好,开发更多新业务

2. 外在驱动力:用户量的上升、用户种类的多样化

这两种驱动力不是独立的,更多时候是并行的。我想淘宝就是两种驱动力并行驱动的结果。

演进的原因很简单。但是在什么时机我们就应该演进网站的技术架构了,以及如何演进?面对这些问题,说实话,我没有任何经验,再说现实中每家企业当时都面临的问题都不一样,所以,我很难从经验中总结出什么是演进的时机。

但是我可以从另一个角度切入这个问题:研究网站内外结构,找到这些结构可能出现的问题点,知道或者预见到问题点了,你当然就知道应该怎么演进了。类似于你了解了PC机的结构,你也就知道什么时候要加内存了,什么时候要加硬盘了。

那么我们先看看网站的外部结构:

Screen Shot 2015-03-12 at 12.20.27 PM

外部结构中,我们可以看由以下几个部分构成:

U:代表用户群。当用户群变了,我们的网站如何演进?用户群的分析,我目前能知道的维度有:数量,种类,地理位置(区域)。

N:代表网络环境。网络环境在每个地区都不同。你可以想像我们为什么需要CDN。当我们期望每个区域的用户都能得到好的体验,我们的网站如何演进?

S:代表安全。就是我们要安全到什么程度?这与网站当前所处阶段及你网站的性质有关。

C:代表我们的网站。属于内部结构

网站的内部结构:

Screen Shot 2015-03-12 at 6.18.20 PM

内部结构的组成:

A:应用服务。

D:数据服务

总结下来就是我们在考虑网站是否应该演进了或者如何演进时,这些组成部分为我们提供了考虑问题的基准。

那么我们为什么不一开始就把网站设计成“大型”的。李智慧在后记里写到:“不要企图去设计一个大型网站”,“原因是互联网发展运行有其自己的规律,短暂的互联网历史已经一再证明这种企图行不通”。还说了:“大型网站不是设计出来的,而是逐步演化出来的”。对于最后这句话,我需要提醒下:“不是设计出来的”并不代表“随意设计”。

对于“大型网站的设计”,我个人的看法是现在我们的有“云”了,计算是可以买的,只要我们的设计能适应“云”,我是不是就可以一开始就设计大型网站了?

演进的过程会遇到什么问题

– 最初

从一个小网站说起。一台服务器也就足够了。

Screen Shot 2015-03-08 at 8.07.03 PM

– 数据服务与应用服务分离

越来越多的用户代表着越来越多的数据,一台服务器已经满足不了。我们将数据服务和应用服务分离,给应用服务器配置更好的CPU,内存。而给数据服务器配置更好更大的硬盘。

Screen Shot 2015-03-08 at 8.44.17 PM

– 使用缓存

因为80%的业务访问都集中在20%的数据上,如果我们能将这部分数据缓存下来,性能一下子就上来了。而缓存又分为两种:本地缓存和远程分布式缓存。具体使用哪种?还是两种都用,我目前不知道。

Screen Shot 2015-03-08 at 8.48.20 PM

这里有一个问题,书没有提到:应该缓存哪些数据?应该有一些原则的吧。

– 使用服务器集群

当这台服务器的处理能力达到上限时,它就会成为瓶颈。虽然你是可以通过购买更强大的硬件,但总会有上限。这时,我们就需要服务器的集群。这时,就必须加个新东西:负载均衡调度服务器。

Screen Shot 2015-03-08 at 9.08.59 PM

但是,使用服务器集群时,需要考虑一个问题:Session的管理问题。Session的管理有以下几种方式:

* Session Sticky:打个比方就是如果我们每次吃饭都要保证我们用的是自己的碗筷,而只要我们在一家饭店里存着我们的碗筷,只要我们每次去这家饭店吃饭就好了。

Screen Shot 2015-03-08 at 9.12.37 PM

这种方式的问题:

1. 一台服务器重启,上面的session都没了

2. 负载均衡器成了有状态的机器,要实现容灾会有麻烦

* Session复制:就像我们在所有的饭店里都存一份自己的碗筷。不适合做大规模集群,适合机器不多的情况

Screen Shot 2015-03-08 at 9.22.34 PM

这种方案的问题:

1. 应用服务器间带宽问题

2. 大量用户在线时,占用内存过多

 

* 基于Cookie:类似于每次吃饭都把自己的碗筷带上

Screen Shot 2015-03-08 at 9.28.16 PM

这种方案的问题:

1. Cookie的长度限制

2. 安全性

3. 数据中心外部带宽的消耗

4. 性能影响,服务器处理每次的请求的内容又多了

* Session服务器:同样可以是集群的。这种方式适用于session数量及web服务器数量大的情况

Screen Shot 2015-03-08 at 9.35.35 PM

这种方案需要考虑的是:

1. 保证session服务器的可用性

2. 我们在写应用时需要做调整,我目前不知道应用服务器能否将这部分逻辑透明化

– 数据库读写分离

数据库的一部分读(未缓存、缓存过期)及所有的写操作都还需要经过数据库。当用户量达到一定量,数据库将会成为瓶颈。这边我们使用数据库提供的热备功能,将所有的读操作引入slave服务器。注意:读写分离解决的是读压力大的问题。

Screen Shot 2015-03-08 at 9.46.05 PM

因为数据库的读写分离了,所以,我们的应用程序也得做相应的变化。我们实现一个数据访问模块使上层写代码的人不知道读写分离的存在。这里,我很想知道如果我使用ORM模型时,如何实现读写的分离?

数据库读写分离会遇到如下问题:

* 数据复制问题: 考虑时延、数据库的支持、复制条件支持。不要忘了,分机房后,这个更是问题。

* 应用对于数据源的路由问题

– 使用反向代理和CDN加速网站响应

使用CDN可以很好的解决不同的地区的访问速度问题,反向代理则在服务器机房中缓存用户资源:

Screen Shot 2015-03-08 at 9.51.49 PM

– 使用分布式文件系统

Screen Shot 2015-03-08 at 10.18.57 PM

– 数据库专库专用:数据垂直拆分。这样可以解决部分数据写的问题

Screen Shot 2015-03-08 at 10.25.34 PM

垂直拆分数据库时,会遇到的问题:

* 跨业务的事务

* 应用的配置项多了

关于事务的问题,有两种办法:

* 使用分布式事务

* 去掉事务或不追求强事务

– 某个业务的数据表的数据量或者更新量达到了单个数据库的瓶颈:数据水平拆分

将同一个表的数据拆分到两个数据库中

Screen Shot 2015-03-08 at 10.34.06 PM

数据水平拆分会遇到的问题:

* SQL的路由问题,需要知道某个User在哪个数据库上。

* 主键的策略会有不同。

* 查询时的性能问题,如分页问题

– 使用搜索引擎:解决数据查询问题

– 部分场景可使用NoSQL提高性能

– 开发数据统一访问模块:解决上层应用开发的数据源问题

Screen Shot 2015-03-08 at 10.49.26 PM

– 业务拆分及应用拆分

网站的业务日益复杂,建立一个独立的大型应用来完成这所有的业务变得不实际。从管理角度来,也不方便管理。然而,业务的拆分很难找到一种通用的模式,这是一个企业管理问题和技术问题的混合问题。同时和每个企业的具体情况有关。

但是从这两本书来看,最终架构都走向服务化,也就是SOA。而如何实现SOA,是另一个很大的话题,不是本篇文章的范畴。

我从程立08年的演讲中截个图来说明SOA后的架构大概是怎样的:

Screen Shot 2015-03-09 at 8.55.28 AM

– 非功能性问题

     – 安全性问题、监控问题

     – 发布问题:新的架构意味着新的发布方式

     – 分机房

这两本书都没有说分机房的问题。我没有经验,可是也可以猜到如果要分机房了,所有上面的问题都可能要重新考虑。

 

     – 组织架构的变化 

我们的技术架构的变化,势必会引起我们的组织架构的变化,反之亦然。

这部分看似不应该由我们来管,但是,我觉得,我们技术人员也要参与一部分的组织架构的设计。举个例子,组织架构的设计会涉及绩效,而绩效有时很像一个国家的法律。如果一个国家的法律不健全,会发生什么?你懂的。

同时,我们还必须考虑人员对新架构的学习成本。

这部分我目前在看相关的书籍,还没有一个系统的认识。

总结:

– 关于演进的顺序

在现实中,技术架构的演进不一定就是按文章从头到尾这样列下来的,所以,要视具体情况来下决定。

– 关于传统演进与现代有“云”环境下的演进

很可惜,只有李智慧谈到云,而且只点了一下——“现在越来越多人的网站从建立之初就是搭建在大型网站提供的云计算服务基础之上,所需的一切资源:计算、存储、网络都可以按需购买线性伸缩,不需要自己一点一点地拼凑各种资源,综合使用各种技术方案逐步去完善自己的网站架构”。

因为我用“云”的时间也不长,还不能总结出有云架构与传统的无云架构在演进的时候有什么不同。

说回传统的架构演进,我自己总结和思考的结果是:

在对网站进行架构调整时,可以从两大的维度考虑:数据服务和应用服务。而这个调整的过程中,需要分清当前哪个点是瓶颈,需要知道哪个点优化的优先级最高。同时,最重要的一点:我们虽然作为技术人员,也应该去学习业务知识,这样我们在考虑问题时分清哪些是业务问题,哪些是技术问题,分清后才能对症下药。你要知道有些问题用技术手段并不比用业务手段更有效。12306的分时卖票就是一个典型例子。

以上总结及思考有不对,欢迎斧正。非常感谢。

Share

软件系统的稳定性

软件系统的稳定性,主要决定于整体的系统架构设计,然而也不可忽略编程的细节,正所谓“千里之堤,溃于蚁穴”,一旦考虑不周,看似无关紧要的代码片段可能会带来整体软件系统的崩溃。这正是我阅读Release It!的直接感受。究其原因,一方面是程序员对代码质量的追求不够,在项目进度的压力下,只考虑了功能实现,而不用过多的追求质量属性;第二则是对编程语言的正确编码方式不够了解,不知如何有效而正确的编码;第三则是知识量的不足,在编程时没有意识到实现会对哪些因素造成影响。

例如在Release It!一书中,给出了如下的Java代码片段:

package com.example.cf.flightsearch; 
//... 
public class FlightSearch implements SessionBean {
    private MonitoredDataSource connectionPool;
    public List lookupByCity(. . .) throws SQLException, RemoteException { 
        Connection conn = null; 
        Statement stmt = null;
        try { 
            conn = connectionPool.getConnection(); 
            stmt = conn.createStatement();

            // Do the lookup logic
            // return a list of results
        } finally { 
            if (stmt != null) {
                stmt.close();
            }
            if (conn != null) { 
                conn.close();
            }
        }
    }
}

正是这一小段代码,是造成Airline系统崩溃的罪魁祸首。程序员充分地考虑了资源的释放,但在这段代码中他却没有对多个资源的释放给予足够的重视,而是以释放单资源的做法去处理多资源。在finally语句块中,如果释放Statement资源的操作失败了,就可能抛出异常,因为在finally中并没有捕获这种异常,就会导致后面的conn.close()语句没有执行,从而导致Connection资源未能及时释放。最终导致连接池中存放了大量未能及时释放的Connection资源,却不能得到使用,直到连接池满。当后续请求lookupByCity()时,就会在调用connectionPool.getConnection()方法时被阻塞。这些被阻塞的请求会越来越多,最后导致资源耗尽,整个系统崩溃。

Release It!的作者对Java中同步方法的使用也提出了警告。同步方法虽然可以较好地解决并发问题,在一定程度上可以避免出现资源抢占、竟态条件和死锁的情况。但它的一个副作用同步锁可能导致线程阻塞。这就要求同步方法的执行时间不能太长。此外,Java的接口方法是不能标记synchronized关键字。当我们在调用封装好的第三方API时,基于“面向接口设计”的原理,可能调用者只知道公开的接口方法,却不知道实现类事实上将其实现为同步方法,这种未知性就可能存在隐患。

假设有这样的一个接口:

public interface GlobalObjectCache {
    public Object get(String id);
}

如果接口方法get()的实现如下:

public synchronized Object get(String id){
    Object obj = items.get(id); 
    if(obj == null) {
        obj = create(id); 
        items.put(id, obj);
    } 
    return obj;
}

protected Object create(String id) {
    //...
}

这段代码很简单,当调用者试图根据id获得目标对象时,首先会在Cache中寻找,如果有就直接返回;否则通过create()方法获得目标对象,然后再将它存储到Cache中。create()方法是该类定义的一个非final方法,它执行了DB的查询功能。现在,假设使用该类的用户对它进行了扩展,例如定义RemoteAvailabilityCache类派生该类,并重写create()方法,将原来的本地调用改为远程调用。问题出现了。由于采用create()方法是远程调用,当服务端比较繁忙时,发出的远程调用请求可能会被阻塞。由于get()方法是同步方法,在方法体内,每次只能有一个线程访问它,直到方法执行完毕释放锁。现在create()方法被阻塞,就会导致其他试图调用RemoteAvailabilityCache对象的get()方法的线程随之而被阻塞。进而可能导致系统崩溃。

当然,我们可以认为这种扩展本身是不合理的。但从设计的角度来看,它并没有违背Liskove替换原则。从接口的角度看,它的行为也没有发生任何改变,仅仅是实现发生了变化。如果不是同步方法,则一个调用线程的阻塞并不会影响到其他调用线程,问题就可以避免了。当然,这里的同步方法本身是合理的,因为只有采取同步的方式才能保证对Cache的读取是支持并发的。书中给出这个例子,无非是要说明同步方法潜在的危险,提示我们在编写代码时,需要考虑周全。

本文原文出自:http://zhangyi.farbox.com/post/stable-software-system

Share

架构腐化之谜

前言

新技术层出不穷。过去十年时间里,我们经历了许多激动人心的新技术,包括那些新的框架、语言、平台、编程模型等等。这些新技术极大地改善了开发人员的工作环境,缩短了产品和项目的面世时间。然而作为在软件行业第一线工作多年的从业者,我们却不得不面对一个现实,那就是当初采用新技术的乐趣随着项目周期的增长而迅速减少。无论当初的选择多么光鲜,半年、一年之后,只要这个项目依然活跃,业务在扩张——越来越多的功能需要加入,一些公共的问题就会逐渐显露出来。构建过慢,完成新功能让你痛不欲生,团队成员无法很快融入,文档无法及时更新等等。

在长期运转的项目中,架构的腐化是怎么产生的?为什么常见的面向对象技术无法解决这类问题?如何延缓架构的腐化?

本文将尝试解释这一切,并提出相应的解决方案。读者需要具备相当的开发经验——至少在同一个项目的开发上一年以上;公司负责架构演进、产品演进的角色会从本文找到灵感。

架构

架构这个词在各种场合不断地以各种面目表现出来。从维基百科的词条看来,我们经常听到的有插件架构(Plugin),以数据库为中心的架构(Database Centric),模型-视图-控制器架构(MVC),面向服务的架构(SOA),三层模型(Three-Tier model),模型驱动架构(MDA)等等等等。奇妙的是,这些词越大,实际的开发者就越痛苦。SOA很好——但在它提出的那个年代,带给开发者的只是面向厂商虚无缥缈的“公共数据类型”;MDA甚至都没有机会沦为新一轮令人笑话的CASE工具。

在继续阅读之前,读者不妨问自己一个问题:在长期的项目中,这些大词是否真的切实给你带来过好处?更为功利的问题是:你,作为战斗在一线的开发者,在长期项目中可曾有过美好的体验?

技术的演变与挥之不去的痛

企业应用的发展似乎从十年前开始腾飞。从Microsoft ASP/LAMP(Linux、Apache、MySQL、PHP)年代开始,各种企业应用纷纷向浏览器迁移。经过十年的发展,目前阵营已经百花齐放。与过去不同,现在的技术不仅仅在编程语言方面,常见的编程套路、最佳实践、方法学、社区,都是各种技术独特拥有的。目前占据主流的阵营有:

 

  • Rails
  • Java EE平台。值得一提的是Java VM已经成为一种新的宿主平台,Scala、JRuby更为活跃并引人瞩目
  • LAMP平台。Linux/MySQL/Apache并没有多少变化,PHP社区从Rails社区获得了不少养分,出现了许多更加优秀的开发框架
  • Microsoft .NET平台
  • Django

没有理由对这些新技术不感到振奋。它们解决了许多它们出现之前的问题。在它们的网站上都宣称各种生产效率如何之高的广告语,类似于15分钟创建一个博客应用;2分钟快速教程等等。比起过去21天才能学会XXX,现在它们在上手难度上早已大幅度降低。

需要泼冷水的是,本文开篇提出的问题,在上述任何一种技术下,都如幽灵般挥之不去。采用Ruby on Rails的某高效团队在10人团队工作半年之后,构建时间从当初的2分钟变成2小时;我们之前采用Microsoft .NET 3.5 (C# 3.0)的一个项目,在产生2万行代码的时候,构建时间已经超过半小时;我们的一些客户,工作在10年的Java代码库上——他们竭尽全力,保持技术栈与时俱进:Spring、Hibernate、Struts等,面对的困境是他们需要同时打开72个项目才能在Eclipse中获得编译;由于编译打包时间过长,他们去掉了大部分的单元测试——带来巨大的质量风险。

如果你真的在一个长期的项目工作过,你应该清楚地了解到,这种痛苦,似乎不是任何一种框架能够根本性解决的。这些新时代的框架解决了大部分显而易见的问题,然而在一个长期项目中所面对的问题,它们无能为力。

一步一步:架构是如何腐化的

无论架构师在任何时代以何种绚丽的方式描述架构,开发中的项目不会超出下图所示:

architecture1

基本架构示意

一些基本的准则:

  • 为了降低耦合,系统应当以恰当的方式进行分层。目前最经考验的分层是MVC+Service。
  • 为了提供基础的访问,一些基本的、平台级别的API应该被引入。用Spring之类的框架来做这件事情。
  • 用AOP进行横向切分业务层面共性的操作,例如日志、权限等。
  • 为了保证项目正常构建,你还需要数据库、持续集成服务器,以及对应的与环境无关的构建脚本和数据库迁移脚本。

阶段1

满足这个条件的架构在初期是非常令人愉悦的。上一部分我们描述的框架都符合这种架构。这个阶段开发非常快:IDE打开很快,开发功能完成很快,团队这个时候往往规模较小,交流也没有问题。所有人都很高兴——因为用了新技术,因为这个架构是如此的简单、清晰、有效。

阶段2

好日子不算太长。

很快你的老板(或者客户,随便什么)有一揽子的想法要在这个团队实现。工作有条不紊的展开。更多的功能加入进来,更多的团队成员也加入了进来。新加入的功能也按照之前的架构方式开发着;新加入的团队成员也对清晰的架构表示欣喜,也一丝不苟的遵循着。用不了多久——也许是三个月,或者更短,你会发现代码库变成下面的样子:

architecture2

正常开发之后

你也许很快会意识到这其中有什么问题。但你很难意识到这到底意味着什么。常见的动作往往围绕着重构——将纵向相关的抽取出来,形成一个新的项目;横向相关的抽取出来,形成一个名叫common或者base的项目。

无论你做什么类型的重构,一些变化在悄悄产生(也许只是快慢的不同)。构建过程不可避免的变长。从刚开始的一两分钟变成好几分钟,到十几分钟。通过重构构建脚本,去掉那些不需要的部分,构建时间会降到几分钟,你满意了,于是继续。

阶段3

更多的功能、更多的成员加入了。构建时间又变长了。随着加载代码的增多,IDE也慢了下来;交流也多了起来——不是所有人能够了解所有代码了。在某些时候,一个很有道德的程序员尝试重构一部分重复逻辑,发现牵涉的代码太多了,好多都是他看不懂的业务,于是他放弃了。更多的人这么做了,代码库越来越臃肿,最终没有一个人能够搞清楚系统具体是怎么工作的了。

系统在混乱的状态下继续缓慢地混乱——这个过程远比本文写作的时间要长很多,之间会有反复,但据我观察,在不超过1年的时间内,无论采用何种技术框架,应用何种架构,这个过程似乎是不可抗拒的宿命。

常见的解决方案

我们并非是坐以待毙的。身边优秀的同事们在问题发现之前采取了各种解决方案。常见的解决方案如下:

升级工作环境

没有什么比一台与时俱进的电脑更能激励开发人员了。最多每隔三年,升级一次开发人员的电脑——升级到当时最好的配置,能够大幅度的提升生产效率,激励开发人员。反过来,利用过时的电脑,在慢速的机器上进行开发,带来的不仅仅是客观上开发效率的降低,更大程度上带来的是开发人员心理上的懈怠。

升级的工作环境不仅仅是电脑,还包括工作的空间。良好的,促进沟通的空间(以及工作方式)能够促进问题的发现从而减少问题的产生。隔断不适合开发。

分阶段的构建

一般而言,构建的顺序是:本地构建确保所有的功能运行正常,然后提交等待持续集成工作正常。本地构建超过5分钟的时候就变得难以忍受;大多数情况下你希望这个反馈时间越短越好。项目的初期往往会运行所有的步骤:编译所有代码,运行所有测试。随着项目周期的变长,代码的增多,时间会越来越长。在尝试若干次重构构建脚本再也没办法优化之后,“分阶段构建”成为绝大多数的选择。通过合理的拆分、分层,每次运行特定的步骤,例如只运行特定的测试、只构建必要的部分;然后提交,让持续集成服务器运行所有的步骤。这样开发者能够继续进行后续的工作。

分布式构建

即便本地快了起来,采用分阶段构建的团队很快发现,CI服务器的构建时间也越来越让人不满意。每次提交半小时之后才能得到构建结果太不可接受了。各种各样的分布式技术被创建出来。除了常见的CI服务器本身提供的能力,许多团队也发明了自己的分布式技术,他们往往能够将代码分布到多台机器进行编译和运行测试。这种解决方案能够在比较长的一段时间内生效——当构建变慢的时候,只需要调整分布策略,让构建过程运行在更多的集群机器上,就可以显著的减少构建时间。

采用JRebel或者Spork

一些新的工具能够显著地提速开发人员的工作。JRebel能够将需要编译的Java语言变成修改、保存立即生效,减少了大量的修改、保存、重新编译、部署的时间;Spork能够启动一个Server,将RSpec测试相关的代码缓存于其中,这样在运行RSpec测试的时候就不用重新进行加载,极大提升了效率。

到底是什么问题?

上述的解决方案在特定的时间域内很好地解决了一部分问题。然而,在项目运转一年,两年或者更久,它们最终依然无法避免构建时间变长、开发变慢、代码变得混乱、架构晦涩难懂、新人难以上手等问题。到底问题的症结是什么?

人们喜欢简洁。但这更多的看起来是一个谎言——没有多少团队能够自始至终保持简洁。人们喜欢简洁只是因为这个难以做到。并不是说人们不愿意如此。很多人都知道软件开发不比其他的劳动力密集型的行业——人越多,产量越大。《人月神话》中已经提到,项目增加更多的人,在提升工作产出的同时,也产生了混乱。短期内,这些混乱能够被团队通过各种形式消化;但从长期看来,随着团队人员的变动(新人加入,老人离开),以及人正常自然的遗忘曲线,代码库会逐渐失控,混乱无法被消化,而项目并不会停止,新功能不断的加入,架构就在一天天的过程中被腐蚀。

人的理解总有一个边界,而需求和功能不会——今天的功能总比昨天的多;这个版本的功能总比上个版本的多。而在长时间的开发中,忘记之前的代码是正常的;忘记某些约定也是正常的。形成某些小而不经意的错误是正常的,在巨大的代码库中,这些小错误被忽视也是正常的。这些不断积攒的小小的不一致、错误,随着时间的积累,最终变得难以控制。

很少有人注意到,规模的变大才是导致架构腐化的根源——因果关系在时空上的不连续,使得人们并不能从其中获得经验,只是一再重复这个悲剧的循环。

解决方案

解决方案的终极目标是:在混乱发生之前,在我们的认知出现障碍之前,就将项目的规模控制在一定范围之内。这并不容易。大多数团队都有相当的交付压力。大多数的业务用户并没有意识到,往一个项目/产品毫无节制地增加需求只会导致产品的崩溃。看看Lotus Notes,你就知道产品最终会多么令人费解、难以使用。我们这里主要讨论的是技术方案。业务上你也需要始终对需求的增长保持警惕。

0. 采用新技术

这可能是最廉价的、最容易采用的方案。新技术的产生往往为了解决某些特定的问题,它们往往是经验的集合。学习,理解这些新技术能够极大程度减少过去为了完成某些技术目标而进行的必要的经验积累过程。就像武侠小说中经常有离奇遭遇的主人公突然获得某个世外高人多年的内力一样,这些新技术能够迅速帮助团队从某些特定的痛点中解脱出来。

已经有足够多的例子来证明这一观点。在Spring出现之前,开发者的基本上只能遵循J2EE模式文档中的各种实践,来构建自己的系统。有一些简单的框架能够帮助这一过程,但总体来说,在处理今天看起来很基础的如数据库连接,异常管理,系统分层等等方面,还有很多手工的工作要做。Spring出现之后,你不需要花费很多精力,很快就能得到一个系统分层良好、大部分设施已经准备就绪的基础。这为减少代码库容量以及解决可能出现的低级Bug提供了帮助。

Rails则是另外一个极端的例子。Rails带来的不仅仅是开发的便利,还带来了人们在Linux世界多年的部署经验。数据库Migration, Apache + FastCGI或者nginx+passenger,这些过去看起来复杂异常的技术在Rails中变得无足轻重——稍懂命令行的人即可进行部署。

任何一个组织都无法全部拥有这些新技术。因此作为软件从业者,需要不断地保持对技术社区的关注。闭门造车只能加速架构的腐化——特别是这些自己的发明在开源社区早已有成熟的方案的时候。在那些貌似光鲜的产品背后,实际上有着无数的失败的案例成功的经验在支撑。

我们曾经有一个项目。在意识到需求可能转向类似于key-value的文档数据库之后,团队大胆的尝试采用SQLServer 2008的XML能力,在SQL Server内部实现了类似于No-SQL的数据库。这是一个新的发明,创造者初期很兴奋,终于有机会做不同的事情了。然而随着项目的进行,越来越多的需求出现了:Migration的支持、监控、管理工具的支持、文档、性能等等。随着项目的进展,最终发现这些能力与时下流行的MongoDB是如此的相似 ——MongoDB已经解决了大多数的问题。这个时候,代码库已经有相当的规模了——而这部分的代码,让许多团队成员费解;在一年之后,大约只有2个人能够了解其实现过程。如果在早期采用MongoDB,团队本有机会摒弃大部分相关的工作。

值得一提的是,高傲的开发者往往对新技术不够耐心;或者说对新技术的能力或局限缺乏足够耐心去了解。每一个产品都有其针对的问题域,对于问题域之外,新技术往往没有成熟到能够应对的地步。开发者需要不断地阅读、思考、参与,来验证自己的问题域是否与其匹配。浅尝辄止不是好的态度,也阻碍了新技术在团队内的推广。

新技术的选型往往发生在项目/产品特定的时期,如开始阶段,某个特定的痛点时期。日常阶段,开发者仍然需要保持对代码库的关注。下一条,重构到物理隔离的组件则是对不断增大的代码库另一种解决方案。

1. 重构到物理隔离的组件

显而易见的趋势是,对于同一个产品而言,需求总是不断增多的。去年有100个功能,今年就有200个。去年有10万行代码,今年也许就有20万行。去年2G 内存的机器能够正常开发,今年似乎得加倍才行。去年有15个开发人员,今年就到30个了。去年构建一次最多15–20分钟,今年就得1个小时了,还得整个分布式的。

有人会注意到代码的设计问题,孜孜不倦地进行着重构;有人会注意到构建变慢的问题,不懈地改进着构建时间。然而很少有人注意到代码库的变大才是问题的根源。很多常规的策略往往是针对组织的:例如将代码库按照功能模块划分(例如ABC功能之类)或者按层次划分(例如持久层、表现层),但这些拆分之后的项目依然存在于开发人员的工作空间中。无论项目如何组织,开发者都需要打开所有的项目才能完成编译和运行过程。我曾经见到一个团队需要在Visual Studio中打开120个项目;我自己也经历过需要在Eclipse中打开72个项目才能完成编译。

解决方案是物理隔离这些组件。就像团队在使用Spring/Hibernate/Asp.NET MVC/ActiveRecord这些库的时候,不用将它们对应的源代码放到工作空间进行编译一样,团队也可以将稳定工作的代码单元整理出来形成对应的库,标记版本然后直接引用二进制文件。

在不同的技术平台上有着不同的方案。Java世界有历史悠久的Maven库,能够良好的将不同版本的 JAR以及他们的以来进行管理;.NET比较遗憾,这方面真正成熟的什么也没有——但参考Maven的实现,团队自己造一个也不是难事(可能比较困难的是与MSBuild的集成);Ruby/Rails世界则有著名的gem/bundler系统。将自己整理出来的比较独立的模块不要放到rails/lib /中,整理出来,形成一个新的gem,对其进行依赖引用(团队内需要搭建自己的gems库)。

同时,代码库也需要进行大刀阔斧的整改。之前的代码结构可能如下,(这里以SVN为例,因为SVN有明确的trunk/branches/tags目录结构。git/hg类似)

svn-old1

原来的库结构

改进之后,将会如下图所示:

svn-new1

改进的库结构

每个模块都有属于自己的代码库,拥有自己的独立的升级和发布周期,甚至有自己的文档。

这一方案看起来很容易理解,但在实际操作过程中则困难重重。团队运转很长一段时间之后,很少有人去关心模块之间的依赖。一旦要拆分出来,去分析几十上百个现存项目之间的依赖相当费劲。最简单的处理办法是,检查代码库的提交记录,例如最近3个月之内某个模块就没有人提交过,那么这个模块基本上就可以拿出来形成二进制依赖了。

很多开源产品都是通过这个过程形成的,例如Spring(请参考阅读《J2EE设计开发编程指南》,Rod Johnson基本上阐述了整个Spring的设计思路来源)。一旦团队开始这样去思考,每隔一段时间重新审视代码库,你会发现核心代码库不可能失控,同时也获得了一组设计良好、工作稳定的组件。

2. 将独立的模块放入独立的进程

上面的解决方案核心原则只有一条:始终将核心代码库控制在团队可以理解的范围内。如果运转良好,能够很大程度上解决架构因为代码规模变大而腐化的问题。然而该解决方案只解决了在系统在静态层面的隔离。当隔离出的模块越来越多,系统也因此也需要越来越多的依赖来运行。这部分依赖在运行期分为两类:一类是类似于 Spring/Hibernate/Apache Commons之类的,系统运行的基础,运行期这些必须存在;另外一类是相对独立的业务功能,例如缓存的读取,电子商城的支付模块等。

第二类依赖则可以更进一步:将其放到独立的进程中。现在稍具规模的系统,登录、注销功能已经从应用中脱离而出,要么采用SSO的方案来进行登陆,要么则干脆代理给别的登陆系统。LiveJournal团队在开发过程中,发现缓存的读写实际上可以放到独立的进程中进行(而不是类似EhCache的方案,直接运行于所在的运行环境中),于是发明了现在鼎鼎有名的memcached. 我们之前进行的一个项目中,发现支付模块完全能够独立出来,于是将其进行隔离,形成了一个新的、没有界面的、永远在运行的系统,通过REST处理支付请求。在另外一个出版项目中,我们发现编辑编写报告的过程实际上与报告发行过程虽然存在类级别的重用,但在业务层面是独立的。最终我们将报告发行过程做成了一个常驻服务,系统其他的模块通过MQ消息与其进行交互。

这一解决方案应该不难理解。与解决方案1不同的是,这一方案更多的是要对系统进行面向业务层面的思考。由于系统将会以独立的进程来运行这一模块,在不同的进程中可能存在一定的代码重复。例如Spring同时存在两个不相关的项目中大家觉得没什么大不了的;但如果是自己的某个业务组件同时在同一个项目的两个进程中重复,许多人就有些洁癖不可接受了。(题外话:这种洁癖在OSGi环境中也存在)这里需要提醒的是:当处于不同的进程时,它们在物理上、运行时上已经彻底隔离了。必须以进程的观点去思考整个架构,而不是简单的物理结构。

从单进程模型到多进程模型的架构思维转变也不太容易——需要架构师有意识的加强这方面的练习。流行的.NET和Java世界倾向于把什么都放到一起。而 Linux世界Rails/Django则能更好的平衡优秀产品之间的进程协调。例如memcached的使用。另外,现在多核环境越来越多,与其费尽心思在编程语言层面上不如享受多核的好处,多进程能够简单并且显著地利用多核能力。

3. 形成高度松散耦合的平台+应用

现在将眼光看更远一些。想象一下我们在做一个类似于开心网、Facebook、人人网的系统。它们的共同特点是能够接入几乎无限的第三方应用,无论是买卖朋友这类简单的应用,还是绚丽无比的各种社交游戏。神奇的是,实现这一点并不需要第三方应用的开发者采用跟它们一样的技术平台,也不需要服务端提供无限的运算能力——大部分的架构由开发方来控制。

在企业应用中实现这个并不难。这其中的秘诀在于:当用户通过Facebook访问某个第三方应用的时候,Facebook实际上通过后台去访问了第三方应用,将当前用户的信息(以及好友信息)通过HTTP POST送到第三方应用指定的服务网址,然后将应用的HTML结果渲染到当前页面中。某种意义上说,这种技术本质上是一种服务器端的mashup. (详情参考InfoQ 文章

facebook

Facebook App架构

这种架构的优点在于极度的分布式。从外观上看起来一致的系统,实际由若干个耦合极低、技术架构完全不同的小应用组成。它们不需要被部署在同一台机器上,可以单独地开发、升级、优化。一个应用的瘫痪不影响整个系统的运行;每个应用的自行升级对整个系统也完全没有影响。

这并非是终极的解决方案,只在某些特定的条件下有效。当系统规模上非常庞大,例如由若干个子系统组成;界面基本一致;子系统之间关联较少。针对这个前提,可以考虑采用这种架构。抽象出极少的、真正有效公用的信息,在系统之间通过HTTP POST.。其他的系统完全可以独立开发、部署,甚至针对应用访问的情况进行特定的部署优化。如果不这么做,动辄上百万千万行的代码堆在一个系统中,随着时间的推移,开发者逐渐对代码失控,架构的腐化是迟早的事情。

例如,银行的财务系统,包括了十多个个子系统,包括薪资、资产、报表等等模块,每一部分功能都相对独立并且复杂。整个系统如果按照这种方式拆分,就能够实现单点优化而无需重新启动整个应用。针对每个应用,开发者能够在更小的代码内采用自己熟悉的技术方案,从而减少架构腐化的可能。

结语

没有糟糕的架构,变化使之

我访问过很多团队。在很多项目开始的时候,他们花很多时间在选择用何种技术体系,何种架构,乃至何种IDE。就像小孩子选择自己钟爱的玩具,我相信无论过程如何,团队最终都会欣然选择他们所选择的,并且坚信他们的选择没有错误。事实也确实如此。在项目的开始阶段很难有真正的架构挑战。困难的地方在于,随着时间的增长,人们会忘记;有很多的人加入,他们需要理解旧代码的同时完成新功能;每一次代码量的突破,都会引起架构的不适应;这些不适应包括:新功能引入变得困难,新人难以迅速上手;构建时间变长等等。这些能否引起团队的警觉,并且采取结构性的解决方案而不是临时性的。

关于文档

很多人说敏捷不提倡文档。他们说文档很难写。他们说开发人员写不了文档。于是就没有文档。

奇怪的是我看到的情况却不是这样。程序写得优秀的人,写起文字来也很不错。ThoughtBlogs上绝大多数都是程序员,很多人的文字写得都很赞。

而项目中的文档往往少得可怜。新人来了总是一头雾水。令人奇怪的是,新人能够一天或者两天之内通过阅读RSpec或者JBehave迅速了解这些工具的使用,到了团队里面却没有了文档。

抛开项目持续运转并交付的特性不谈,我认为巨大的、不稳定的代码库是文档迅速失效的根源。如果我们能够按照上述的解决方案,将代码库缩小,那么独立出来的模块或者应用就有机会在更小的范围内具备更独特的价值。想象一下现在的Rails3/Spring框架,他们往往有超过20个第三方依赖,我们却没有觉得理解困难,最重要的原因是依赖隔离之后,这些模块有了独立的文档可以学习。

企业级项目也可以如此。

创建应用程序的生态环境,而非单一的项目

功能总是不断的、不断的加到同一个产品中。这毫不奇怪。然而通过我们前面的分析,我们应当重新思考这个常识。是创建一个日益庞大的、缓慢的、毫无生机的产品,还是将其有机分解,成为一个生机勃勃的具有不同依赖的生态系统?项目的各方人员(包括业务用户、架构师、开发者)应当从短视的眼光中走出来,着眼于创建可持续的应用程序生态系统。

关于作者

陈金洲,Buffalo Ajax Framework作者,ThoughtWorks中国公司首席咨询师,现居西安。目前的工作主要集中在RichClient开发,同时一直对Web可用性进行观察,并对其实现保持兴趣。


本文原文发表于InfoQ:http://www.infoq.com/cn/articles/cjz-architecture-corruption

Share