国内某大型互联网企业经常因为对同行的产品进行微创新,然后推出自己的产品而遭人诟病,不讨论这种做法是否合适,我们分析这些产品,发现大多数都比原创产品有更好的用户体验。这些产品常常后来居上,更速度地推出新功能,吸引用户注意,进而占据市场。微信从发布到拥有1亿用户,仅仅用了一年的时间。而据说摇一摇这个功能是两个实习生用一个星期就开发完成上线的。使用TOP ( Taobao Open API ),一个技术熟练的淘宝客网站开发工程师只需要用几个晚上的业余时间就可以开发部署一个炫目的购物导购网站。如此轻易地就可以开发一个新产品,如此快速地就可以实现一个新功能,他们是如何做到的?为什么有的网站必须规定系统发布日,一到发布日就如临大敌,整个技术部加班通宵达旦;而有的网站就可以随时发布,新功能可以随时快速上线。这些都有赖于网站的扩展性架构设计,就是在对现有系统影响最小的情况下,系统功能可持续扩展及提升的能力。经常听到各种场合中对扩展性和伸缩性的误用,包括许多资深网站架构师也常常混淆两者,用扩展性表示伸缩性。主要采用系统解耦和分布式队列来实现。
扩展性(Extensibility):指对现有系统影响最小的情况下,系统功能可持续扩展或提升的能力。表现在系统基础设施稳定不需要经常变更,应用之间较少依赖和耦合,对需求变更可以敏捷响应。它是系统架构设计层面的开闭原则(对扩展开放,对修改关闭),架构设计考虑未来功能扩展,当系统增加新功能时,不需要对现有系统的结构和代码进行修改。
伸缩性( Scalability):指系统能够通过增加(减少)自身资源规模的方式增强(减少)自己计算处理事务的能力。如果这种增减是成比例的,就被称作线性伸缩性。在网站架构中,通常指利用集群的方式增加服务器数量、提高系统的整体事务吞吐能力。
构建可扩展的网站架构开发低耦合系统是软件设计的终极目标之一,这一目标驱动着软件开发技术的创新与发展,从软件与硬件的第一次分离到操作系统的诞生;从汇编语言到面向过程的开发语言,再到面向对象的编程语言;从各种软件工具集到各种开发框架;无不体现着降低软件系统耦合性这一终极目标。可以说,度量一个开发框架、设计模式、编程语言优劣的重要尺度就是衡量它是不是让软件开发过程和软件产品更加低耦合。
显而易见,低耦合的系统更容易扩展,低耦合的模块更容易复用,一个低耦合的系统设计也会让开发过程和维护变得更加轻松和容易管理。一个复杂度为100的系统,如果能够分解成没有耦合的两个子系统,那么每个子系统的复杂度不是50,而可能是25。当然,完全没有耦合就是没有关系,也就无法组合出一个强大的系统。那么如何分解系统的各个模块、如何定义各个模块的接口、如何复用组合不同的模块构造成一个完整的系统,这是软件设计中最有挑战的部分。
软件架构师最大的价值不在于掌握多少先进的技术,而在于具有将一个大系统切分成N个低耦合的子模块的能力,这些子模块包含横向的业务模块,也包含纵向的基础技术模块。这种能力一部分源自专业的技术和经验,还有一部分源自架构师对业务场景的理解、对人性的把握、甚至对世界的认知。
大型网站也常常意味着功能复杂,产品众多。网站为了在市场竞争中胜出,不断推出各种新产品,为了把握市场机会,这些产品从策划到上线,时间非常短暂,技术团队必须在产品设计和需求分析结束之后,快速地开发完成一个新产品。同时经过长期的演化和发展,这些产品之间的关系错综复杂,维护也变得异常困难。这些问题对网站的可扩展架构提出了挑战和要求。
设计网站可扩展架构的核心思想是模块化,并在此基础之上,降低模块间的耦合性,提高模块的复用性。
网站通过分层和分割的方式进行架构伸缩,分层和分割也是模块化设计的重要手段,利用分层和分割的方式将软件分割为若干个低耦合的独立的组件模块,这些组件模块以消息传递及依赖调用的方式聚合成一个完整的系统。在大型网站中,这些模块通过分布式部署的方式,独立的模块部署在独立的服务器(集群)上,从物理上分离模块之间的耦合关系,进一步降低耦合性提高复用性。
模块分布式部署以后具体聚合方式主要有分布式消息队列和分布式服务。
利用分布式消息队列降低系统耦合性如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响最小,这样系统的可扩展性无疑更好一些。
事件驱动架构:事件驱动架构(Event Driven Architecture ):通过在低耦合的模块之间传输事件消息,以保持模块的松散耦合,并借助事件消息的通信完成模块间合作,典型的EDA架构就是操作系统中常见的生产者消费者模式。在大型网站架构中,具体实现手段有很多,最常用的是分布式消息队列,如图所示。
消息队列利用发布—订阅模式工作
消息发送者发布消息,一个或者多个消息接收者订阅消息。消息发送者是消息源,在对消息进行处理后将消息发送至分布式消息队列,消息接受者从分布式消息队列获取该消息后继续进行处理。可以看到,消息发送者和消息接受者之间没有直接耦合,消息发送者将消息发送至分布式消息队列即结束对消息的处理,而消息接受者只需要从分布式消息队列获取消息后进行处理,不需要知道该消息从何而来。对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展设计。
消息接受者在对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接受者订阅处理该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列的流程。
由于消息发送者不需要等待消息接受者处理数据就可以返回,系统具有更好的响应延迟;同时,在网站访问高峰,消息可以暂时存储在消息队列中等待消息接受者根据自身负载处理能力控制消息处理速度,减轻数据库等后端存储的负载压力。
分布式消息队列
队列是一种先进先出的数据结构,分布式消息队列可以看作将这种数据结构部署到独立的服务器上,应用程序可以通过远程访问接口使用分布式消息队列,进行消息存取操作,进而实现分布式的异步调用,基本原理如图所示。消息生产者应用程序通过远程访问接口将消息推送给消息队列服务器,消息队列服务器将消息写入本地内存队列后立即返回成功响应给消息生产者。消息队列服务器根据消息订阅列表查找订阅该消息的消息消费者应用程序,将消息队列中的消息按照先进先出(FIFO)的原则将消息通过远程通信接口发送给消息消费者程序。
目前开源的和商业的分布式消息队列产品有很多,比较著名的如Apache ActiveMQ等,这些产品除了实现分布式消息队列的一般功能,在可用性、伸缩性、数据一致性、性能和可管理性方面也做了很多改善。
在伸缩性方面,由于消息队列服务器上的数据可以看作是被即时处理的,因此类似于无状态的服务器,伸缩性设计比较简单。将新服务器加入分布式消息队列集群中,通知生产者服务器更改消息队列服务器列表即可。在可用性方面,为了避免消费者进程处理缓慢,分布式消息队列服务器内存空间不足造成的问题,如果内存队列已满,会将消息写入磁盘,消息推送模块在将内存队列消息处理完以后,将磁盘内容加载到内存队列继续处理。
为了避免消息队列服务器宕机造成消息丢失,会将消息成功发送到消息队列的消息存储在消息生产者服务器,等消息真正被消息消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中其他的服务器发布消息。
利用分布式服务打造可复用的业务平台使用分布式服务是降低系统耦合性的另一个重要手段。如果说分布式消息队列通过消息对象分解系统耦合性,不同子系统处理同一个消息;那么分布式服务则通过接口分解系统耦合性,不同子系统通过相同的接口描述进行服务调用。
回顾网站架构发展历程,网站在由小到大的演化过程中,表现为整个网站是由单一应用系统逐步膨胀发展变化而来,随着网站功能的日益复杂,网站应用系统会逐渐成为一个巨无霸,如图所示。一个应用中聚合了大量的应用和服务组件,这个巨无霸给整个网站的开发、维护、部署都带来了巨大的麻烦。
巨无霸应用系统带来如下几点问题。
1.编译、部署困难:对于网站开发工程师而言,打包构建一个巨型应用是一件痛苦的事情,也许只是修改了一行代码,输入build命令后,抽完一支烟,回来一看,还在building;又去喝了一杯水,回来一看,还在building;又去了一次厕所,回来一看,还在building;好不容易build结束,一看编译失败,还得重来……
2.代码分支管理困难:复用的代码模块由多个团队共同维护修改,代码merge 的时候总会发生冲突。代码merge一般发生在网站发布的时候,经常和发布过程中出现的其他问题互相纠结在一起,顾此失彼,导致每次发布都要拖到半夜三更。
3.数据库连接耗尽:巨型的应用、大量的访问,必然需要将这个应用部署在一个大规模的服务器集群上,应用与数据库的连接通常使用数据库连接池,以每个应用10个连接计,一个数百台服务器集群的应用将需要在数据库上创建数千个连接。数据库服务器上,每个连接都会占用一些昂贵的系统资源,以至于数据库缺乏足够的系统资源进行一般的数据操作。
4.新增业务困难:想要在一个已经如乱麻般的系统中增加新业务,维护旧功能,难度可想而知:一脚踩进去,发现全都是雷,什么都不敢碰。许多新工程师来公司半年了,还是不能接手业务,因为不知道水有多深。于是就出现这种怪现象:熟悉网站产品的“老人”忙得要死,加班加点干活;不熟悉网站产品的新人一帮忙就出乱,跟着加班加点;整个公司热火朝天,加班加点,却还是经常出故障,新产品迟迟不能上线。
解决方案就是拆分,将模块独立部署,降低系统耦合性。拆分可以分为纵向拆分和横向拆分两种。
纵向拆分:将一个大应用拆分为多个小应用,如果新增业务较为独立,那么就直接将其设计部署为一个独立的Web应用系统。
横向拆分:将复用的业务拆分出来,独立部署为分布式服务,新增业务只需要调用这些分布式服务,不需要依赖具体的模块代码,即可快速搭建一个应用系统,而模块内业务逻辑变化的时候,只要接口保持一致就不会影响业务程序和其他模块。如图所示。
纵向拆分相对较为简单,通过梳理业务,将较少相关的业务剥离,使其成为独立的Web 应用。而对于横向拆分,不但需要识别可复用的业务,设计服务接口,规范服务依赖关系,还需要一个完善的分布式服务管理框架。
大型网站分布式服务的需求与特点对于大型网站,除了Web Service所提供的服务注册与发现,服务调用等标准功能,还需要分布式服务框架能够支持如下特性。
负载均衡:对热门服务,比如登录服务或者商品服务,访问量非常大,服务需要部署在一个集群上。分布式服务框架要能够支持服务请求者使用可配置的负载均衡算法访问服务,使服务提供者集群实现负载均衡。
失效转移:可复用的服务通常会被多个应用调用,一旦该服务不可用,就会影响到很多应用的可用性。因此对于大型网站的分布式服务而言,即使是很少访问的简单服务,也需要集群部署,分布式服务框架支持服务提供者的失效转移机制,当某个服务实例不可用,就将访问切换到其他服务实例上,以实现服务整体高可用。
高效的远程通信:对于大型网站,核心服务每天的调用次数会达到数以亿计,如果没有高效的远程通信手段,服务调用会成为整个系统性能的瓶颈。
整合异构系统:由于历史发展和组织分割,网站服务可能会使用不同的语言开发并部署于不同的平台,分布式服务框架需要整合这些异构的系统。
对应用最少侵入:网站技术是为业务服务的,是否使用分布式服务需要根据业务发展规划,分布式服务也需要渐进式的演化,甚至会出现反复,即使用了分布式服务后又退回到集中式部署,分布式服务框架需要支持这种渐进式演化和反复。当然服务模块本身需要支持可集中式部署,也可分布式部署。
版本管理:为了应对快速变化的需求,服务升级不可避免,如果仅仅是服务内部实现逻辑升级,那么这种升级对服务请求者而言是透明的,无需关注。但如果服务的访问接口也发生了变化,就需要服务请求者和服务提供者同时升级才不会导致服务调用失败。企业应用系统可以申请停机维护,同时升级接口。但是网站服务不可能中断,因此分布式服务框架需要支持服务多版本发布,服务提供者先升级接口发布新版本的服务,并同时提供旧版本的服务供请求者调用,当请求者调用接口升级后才可以关闭旧版本服务。
实时监控:对于网站应用而言,没有监控的服务是不可能实现高可用的。分布式服务框架还需要监控服务提供者和调用者的各项指标,提供运维和运营支持。
分布式服务框架设计服务消费者程序通过服务接口使用服务,而服务接口通过代理加载具体服务,具体服务可以是本地的代码模块,也可以是远程的服务,因此对应用较少侵入:应用程序只需要调用服务接口,服务框架根据配置自动调用本地或远程实现。
服务框架客户端模块通过服务注册中心加载服务提供者列表(服务提供者启动后自动向服务注册中心注册自己可提供的服务接口列表),查找需要的服务接口,并根据配置的负载均衡策略将服务调用请求发送到某台服务提供者服务器。如果服务调用失败,客户端模块会自动从服务提供者列表选择一个可提供同样服务的另一台服务器重新请求服务,实现服务的自动失效转移,保证服务高可用。Dubbo的远程服务通信模块支持多种通信协议和数据序列化协议,使用NIO通信框架,具有较高的网络通信性能。
网站的价值在于为他的用户创造价值,淘宝的价值在于为人们创造交易的平台;QQ的价值在于为人们创造交流的平台;新浪微博的价值在于为人们创造表达自我的平台。只有用户得到了他们想要的价值,他们才愿意使用网站的服务,网站的存在才有意义。但是淘宝有上千万卖家和数亿买家,光靠淘宝一个公司不可能满足所有用户的需求,同样,腾讯、新浪微博也无法面面俱到照顾好如此庞大的用户群。
另一方面,用户却不需要为网站提供的价值而买单。没有人需要为自己在QQ上聊天,在淘宝上购物,在新浪发微博而付费。网站必须提供更多的增值服务才能赚钱。比如,QQ可以卖各种钻石会员服务,淘宝可以出卖商品排名赚钱,新浪微博靠植入广告也能赚点钱。根据长尾效应,这些增值服务的数量越是庞大,种类越是繁多,盈利也就越多。同样,一个网站自己能够开发出的增值服务也是有限的。
大型网站为了更好地服务自己的用户,开发更多的增值服务,会把网站内部的服务封装成一些调用接口开放出去,供外部的第三方开发者使用,这个提供开放接口的平台被称作开放平台。第三方开发者利用这些开放的接口开发应用程序(APP)或者网站,为更多的用户提供价值。网站、用户、第三方开发者互相依赖,形成一个网站的生态-既为用户提供更多的价值,也提高了网站和第三方开发者的竞争能力和盈利能力。
目前百度、淘宝、腾讯等国内互联网巨头都建设有自己的开放平台,力图利用自己庞大的用户群吸引第三方开发者,打造一个更加庞大的航母战斗群,在市场竞争中呼风唤雨,立于不败之地。
开放平台是网站内部和外部交互的接口,外部需要面对众多的第三方开发者,内部需要面对网站内诸多的业务服务。虽然每个网站的业务场景和需求都各不相同,但是开放平台的架构设计却大同小异。
API接口:是开放平台暴露给开发者使用的一组 API,其形式可以是 RESTful、WebService、RPC等各种形式。
协议转换:将各种 API 输入转换成内部服务可以识别的形式,并将内部服务的返回封装成API的格式。
安全:除了一般应用需要的身份识别、权限控制等安全手段,开放平台还需要分级的访问带宽限制,保证平台资源被第三方应用公平合理使用,也保护网站内部服务不会被外部应用拖垮。
审计:记录第三方应用的访问情况,并进行监控、计费等。
路由:将开放平台的各种访问路由映射到具体的内部服务。
流程:将一组离散的服务组织成一个上下文相关的新服务,隐藏服务细节,提供统一接口供开发者调用。
小结网站通过不断试错,在残酷的市场中寻找自己的竞争优势,持续地推出新功能,发现达不到预期,就立马下线。所以我们看到网站总是不停地推出新功能,发布新产品。打开Google首页的“更多”链接,Google 产品分门别类一大堆,这还只是Google重点推广的产品中的一小部分。这些走马灯般出现的产品背后则是网站工程师辛勤的工作和汗水。
既然我们知道网站不停上新产品是其生存的本能,谁能更快更好地推出更多的新产品,谁就活得更滋润,那么工程师就要做好准备应付这种局面。马克思的劳动价值理论告诉我们,产品的内在价值在于劳动的时间,劳动的时间不在于个体付出的劳动时间,而在于行业一般劳动时间,资本家只会为行业一般劳动时间买单,如果你的效率低于行业一般劳动时间,对不起,请你自愿加班。反之,如果你有一个更具有扩展性的网站架构,可以更快速地开发新产品,也许你也享受不了只上半天班的福利,但是至少在这个全行业加班的互联网领域。