超大流量分布式系统架构解决方案:人人都是架构师2.0
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.1 分布式系统的架构演变过程

互联网悄然改变了世界,改变了人们对事务的认知,缩短了人与人之间的距离。无论你是否愿意承认,互联网已经完全影响并融入我们的生活中。笔者的母亲从来就不是一个喜欢追赶潮流的人,但她早已智能设备从不离身,每天早上起床的第一件事情就是拿起智能手机,刷刷朋友圈、看看时事政治、做回“吃瓜群众”八卦下娱乐新闻,甚至衣食住行也几乎都是通过互联网这个载体一键搞定的,如图1-1所示。既然互联网能够使我们的生活变得更美好,那就请张开双臂紧紧拥抱它。

图1-1 丰富的互联网生活

拥有互联网的夜晚是明亮的,当然为这寂静夜空点燃光明的正是聚集在各个互联网企业内部的技术团队,正是这些家伙昼夜颠倒的辛勤付出(无数的通宵、会议、功能迭代、体验优化、架构升级),才换来今日互联网的光彩夺目。很多电商网站为了吸引用户流量,往往都会以极低的价格作为诱饵,但是当促销活动正式开始后,峰值流量却远远超出了系统所能够承受的处理能力,这必然只会产生一种结果——宕机。如何帮助企业顺利走出困境,打造真正具备高性能、高可用、易扩展、可伸缩,以及安全的网站架构才是架构师、技术大牛们应该重点思考的问题和责任,哪怕是系统宕机,也要尽最大努力做到“死而不僵”,用技术支撑业务的野蛮生长,活下去才会有更美好的明天。

通常来说,网站由小变大的过程,几乎都需要经历单机架构、集群架构、分布式架构、分布式多活数据中心架构。伴随着业务系统架构一同演变的还有各种外围系统和存储系统,比如关系数据库的分库分表改造、从本地缓存过渡到分布式缓存等。当系统架构演变到一定阶段且逐渐趋向于稳定和成熟后,架构师们需要对技术细节追本溯源,如果现有的技术或者框架不能有效满足业务需要,就需要从“拿来主义”的消费者角色转变为自行研发的生产者角色。当然,在这条技术之路离你和你所在的企业还很遥远的时候,尽管未雨绸缪利大于弊,但这却并不是你现阶段的工作重点。在此大家需要注意,对技术由衷的热爱和痴迷本身并没有什么不对,但是千万不能过于沉醉而选择刻意忽略掉业务,任何技术的初衷都是更好地服务业务,一旦脱离业务,技术必然会失去原有的价值和意义,切勿舍本逐末。

1.1.1 单机架构

任何一个网站在上线初期几乎都不可能立马就拥有庞大的用户流量和海量数据,都是在不停的试错过程中一步一步演变其自身架构,满足其自身业务。所以我们常说,互联网领域几乎没有哪一个网站天生就是大型网站,因为往往系统做大与业务做大是呈正比的,大型网站都是从小型网站逐渐演变过来的,而不是被刻意设计出来的。试想一下,如果业务不见起色,一味地追求大型网站架构又有何意义呢?

对于一个刚上线的项目,我们往往会将WebServer、文件服务器和数据库全都部署在同一台物理服务器上,并将所有的业务逻辑全都耦合在同一个单体应用中,这样做的好处只有一个——省钱,如图1-2所示。

图1-2 单机架构

资金紧张且用户流量相对较小的网站,采用这样的架构进行部署确实非常实惠,毕竟用户流量上不去,自然就没有必要考虑更多的问题,哪怕是系统宕机,影响范围也不大。当然,一旦业务开始加速发展,用户逐渐增多,系统瓶颈便会开始暴露,这时架构师们就可以考虑对现有网站架构做出以下四点调整:

● 独立部署,避免不同的系统相互之间争夺共享资源(比如CPU、内存、磁盘等);

● WebServer集群,提高容错性;

● 部署分布式缓存系统,使查询操作尽可能在缓存命中;

● 数据库实施读/写分离改造,实现HA(High Availability,高可用性)架构。

1.1.2 集群架构

一旦用户体量增加,并发流量上来后,为了让用户拥有更好的操作体验,我们不得不对单机系统架构做出调整和优化,因此在这个阶段主要需要解决的问题就是提升业务系统的并行处理能力,降低单机系统负载,以便支撑更多用户访问。

集群(Cluster)技术可以将多台独立的服务器通过网络相互连接组合起来,形成一个有效的整体对外提供服务,使用集群的意义就在于其目标收益远高于所付出的实际成本和代价。互联网领域存在一个共识,那就是当一台服务器的处理能力接近或已超出其容量上限时,不要企图更换一台性能更强劲的服务器,通常的做法是采用集群技术,通过增加新的服务器来分散并发访问流量,1台不够就扩到2台,2台不够就扩到4台,只要业务系统能够随意支持服务器的横向扩容,那么从理论上来说就应该无惧任何挑战,从而实现可伸缩性和高可用架构。

如图1-3所示,对于无状态的WebServer节点来说,通常我们会使用Nginx来实现负载均衡调度,但是在线上环境中,Nginx也应该具备高可用性,这可以依靠DNS轮询来实现,或者如果你所在企业使用的是云主机,则可以使用云服务商提供的SLB服务。总之,在集群环境中,WebServer节点的数量越多,并行处理能力和容错性就越强,哪怕其中某些节点因为种种原因宕机,也不会导致系统不可用。

图1-3 集群架构

伴随着WebServer集群改造的还有分布式缓存和数据库,对于查询操作我们应当尽可能在缓存中命中,从而降低数据库的负载压力。尽管缓存技术可以分担数据库的大部分查询压力,但是写入操作和无法在缓存命中的数据仍然需要频繁地对数据库进行读/写操作,因此对数据库实施读/写分离改造也迫在眉睫。

业务发展到一定阶段后必然会变得更加复杂,用户规模也会线性上升,这时架构师就可以考虑对现有网站架构做出以下两点调整:

● 利用CDN加速系统响应;

● 业务垂直化,降低耦合,从而实现分而治之的管理。

由于中国的网络环境相对比较复杂,跨网络、跨地域的用户访问网站时,速度有较大差别,因此,当用户流量增大之后,我们需要考虑将系统中的一些静态资源数据(如图片、音频、视频、脚本文件及HTML网页等)缓存在CDN节点上,因为CDN正在变得越来越廉价,如图1-4所示。由于用户的请求并不会直接落到企业的数据中心,而是请求到离用户最近的ISP(Internet Service Provider,互联网服务提供商)上,因此可以大幅提升系统整体的响应速度。

图1-4 利用CDN加速网站响应

1.1.3 垂直拆分业务子系统

尽管业务系统可以通过集群技术来提升并行处理能力和实现高可用架构,但是一般到了这个阶段,不仅用户规模暴增,业务需求也变得更加复杂。在单体应用中,业务逻辑全部耦合在一起,并且部署在同一个WebServer中,这必然会导致系统中不同业务之间的耦合过于紧密,除了扩展和维护困难,在生产环境中极有可能会因为某个业务功能不可用而影响系统整体服务不可用,因此在这个阶段主要需要解决的问题就是降低业务耦合,实现高内聚低耦合,提升系统容错性,避免牵一发而动全身的风险。

下面为大家分享笔者亲身经历过的一次真实生产事故。

在一次限时抢购活动中,我们预计零点开抢时的那一拨峰值流量肯定会远高于平时,因此在几天前运维部门的同学就提前扩容了上百台服务器来应对“双11”。在活动开始前,大家摩拳擦掌,屏住呼吸紧盯着电脑屏幕“观赏”流量监控曲线图时,“悲剧”发生了,随着倒数结束,活动正式开始,流量曲线居然没有太大的起伏,这让当时在场的小伙伴们瞬间懵掉了,紧急排查故障后发现,前端App在每一次业务请求提交之前都会先将客户端埋点数据上报到专门用于数据采集的WebServer上(后期需要利用大数据平台对埋点数据进行一些用户行为的实时/离线分析),然后再请求到业务系统上执行逻辑处理。但因为我们的疏忽,负责数据采集的WebServer和核心业务系统使用的是同一个Nginx服务器进行请求转发,由于没有对数据采集的机器做扩容,导致Nginx日志中出现大量的请求超时异常,从而严重影响了核心业务的正常运转。据不完全统计,这次活动至少导致了2/3的用户无法正常访问,而那些“侥幸”正常访问并下单的用户,也因为在指定时间内无法顺利完成支付而导致大量的订单回流。这次惨痛的教训说明,细节决定成败的重要性不言而喻。当然,既然踩坑了,吸取经验教训避免悲剧重现才是最重要的。

大家一定要具备大系统小做的意识,所谓拆系统其实指的就是业务垂直化,简而言之,架构师可以根据系统业务功能的不同拆分出多个业务模块(一般大型电商网站都会拆分出首页、用户、搜索、广告、购物、订单、商品、收益结算等子系统),再由不同的业务团队负责承建,分而治之,独立部署,如图1-5所示。

图1-5 业务垂直化改造

在此大家需要注意,拆分粒度越细,耦合越小,容错性越好,每个业务子系统的职责就越清晰。但是如果拆分粒度过细,维护成本将是一个不小的挑战。对业务系统完成拆分后,就意味着系统已经过渡到分布式系统了,这也为企业实施服务化改造和数据库分库分表改造提前做好了准备。关于数据库分库分表的相关知识点,大家可以直接阅读本书的第5章。

1.1.4 服务化架构演进

在大部分互联网企业的系统架构演变过程中,不得不提到的就是服务化改造,那么究竟为什么要实现服务化架构呢?

随着用户规模逐渐庞大,需求更加复杂,我们一定会对耦合在一个Web容器中的单体应用进行垂直化改造,以业务功能为维度拆分出多个子系统,这样做就是为了能够更清晰地规划和体现出每个子系统的职责,降低业务耦合,以及提升容错性。但是在多元化的业务需求下,子系统中一定会存在较多的共享业务,这些共享业务肯定会被重复建设,产生较多的冗余业务代码。而且,业务系统中数据库连接之类的底层资源必然会限制业务系统所允许横扩的节点数量,因为在横扩过程中,连接数是机器数的平方。除了共享业务重复建设和资源连接受限,还有一个不容忽视的问题,当业务做大时,技术团队的人员规模也开始膨胀,太多人共同维护一个系统肯定会坏事,尤其是那些“手潮”的同学,经常会导致版本冲突。因此为了避免这些问题,服务化架构(Service-Oriented Architecture,SOA)改造刻不容缓,如图1-6所示。

图1-6 服务化架构

当然,如果你现阶段并不满足上述任何一个条件,笔者其实是不建议实施服务化改造的,因为这显然会提升系统的复杂度和维护成本,甚至还需要提前规划一些服务化场景下才需要考虑的技术难题,比如服务治理、分布式事务等。

1.1.5 服务化与微服务架构的区别

实施服务化改造后,原本耦合在WebServer中的业务逻辑被剥离,因此这些子系统从某种意义上来说仅仅只是充当着控制层(Controller)的角色,由于无须处理任何复杂的业务逻辑,因此吞吐量相对会有所提升,但是业务的逻辑处理由独立部署的服务负责,所以架构师需要认真思考如何有效提升整体的服务质量。近几年,微服务这个概念被炒得火热,就连Spring也借机推出了Spring Cloud服务治理平台。那么究竟什么是微服务?它和服务化之间的区别在哪?

其实,当笔者第一次听说微服务这个概念的时候,尽管查阅了非常多的文献资料,却仍然不清楚它和服务化架构之间究竟有何区别,但从本质上来说,微服务和服务化这2个概念完全就是一回事,所谓的微服务架构,从宏观上来看,无非就是细化了服务拆分过程中的粒度,粒度越细,业务耦合越小,容错性就越好,并且后期扩展也会越容易。

那么服务到底应该如何拆分才称得上“微服务”呢?通常来说,笔者建议大家以子系统为维度来进行拆分,这样的拆分粒度适中,所付出的成本与实际收益会相对等价,否则粒度过细不仅会提升维护成本,还会影响排查问题时的效率,更重要的是,业务开发同学很难梳理清楚服务之间的依赖关系。

1.1.6 集群与分布式的区别

截至目前,笔者已经对单机架构、集群架构,以及分布式架构进行了深入介绍,相信大家对此已经有了一定的认识。在本书的第一版上市后,笔者曾收到许多读者来信,其中不乏有一些初入职场的同学容易混淆集群和分布式二者概念,在此笔者就简单对其进行阐述,以便让大家能够更好地理解。

集群是指将多台服务器集中在一起,目的是实现同一业务;而分布式是指将不同的业务分布在不同的地方,目的是实现不同的业务;前者是串联工作,而后者是并联工作。在此大家需要注意,分布式架构中的每一个子节点都允许构成一个集群,但集群却并不一定就是分布式的。

再举一个贴近生活的例子。假设你厨艺高超,声名远播,周末盛情邀约了几个小伙伴来你家聚餐,你一个人负责买菜、切菜、炒菜、上菜,这便是单机架构;而某一天更多朋友来你家做客时,你发现似乎有些力不从心,这时你需要几个人一起来协作帮忙,以便提升效率,这就是集群架构;假设你家大业大,有上百位朋友都相约你家吃饭时,你会需要更多的人来协作帮忙,并且相互之间需要明确职责分工,A组负责买菜,B组负责洗菜,C组负责炒菜,D组负责上菜,这就是分布式+集群架构。

1.1.7 前后端分离架构演进

前后端分离架构近两年也是被社区炒得热火朝天,无论是否是互联网企业,几乎都在大刀阔斧地实施前后端分离改造,包括一些大型技术峰会上也开设有诸多关于前后端分离架构的专题分享,因此,笔者将所在企业实施前后端分离改造的一些实践经验分享给大家。

我们的业务现状包含H5、APP,PC-WEB 3个端。根据端的不同请求会落到不同的域名上,再由对应的接入层来负责请求的接入,WebServer充当了聚合层,负责调用下游服务执行具体的业务逻辑处理后响应请求结果,同时还耦合有前端代码。

如图1-7所示,早期WebServer我们是以域名为维度来进行划分的,尽管这样的设计并不合理,但这确实属于历史遗留问题,最初我们只有H5端,后来为了支撑业务的快速发展,PC-WEB和APP端是由相应的开发人员直接拷贝现有H5的WebServer代码,并在此基础之上快速适配修改上线的。随着时间的逐步推移,业务需求开始变得越发复杂,所积压的问题也越来越多,最终产生了如下4个主要痛点:

图1-7 未前后端分离的整体架构

● 下游RPC接口发生变化时,上游依赖这个接口的各个端的WebServer都需要修改代码;

● 各个端的WebServer中存在大量的代码冗余;

● 前后端职责模糊,前端需要关心后端语法,后端需要维护模板代码;

● 前后端联调成本高。

其实各个端WebServer中的逻辑大致都是相似的,只存在极少数的不同(比如:数据的返回格式存在区别、入参存在区别、展示交互存在区别,以及调用下游服务的数量存在区别等),因此我们开始尝试使用Node.js技术实施前后端分离改造来解决上述提出的诸多痛点,如图1-8所示。

图1-8 前后端分离改造后整体架构

首先需要改造的是WebServer的拆分问题,不再继续以域名为维度进行拆分,而是以子系统维度来进行拆分,这样便可以有效解决大量代码拷贝和冗余等问题,最终只需保留一套代码即可。然后将耦合在WebServer中的前端模板代码前移至Node.js中,由Node.js充当中间层来负责具体的服务端渲染和数据二次加工等任务,这样不仅减少了耦合,同时还降低了前后端联调成本(前端开发人员与后端开发人员约定好数据的返回格式后,不再需要等待后端一起联调,可以自行调用Mock服务来进行验证),以及维护成本。总之前后端分离架构在当下是趋势,但是否有必要进行改造则还需要结合自身业务而定。

1.1.8 API网关服务

如果没有Spring Cloud的出现,可能大部分互联网企业至今都只能够选择基于Dubbo来落地服务化架构,但如今我们的武器库中确实又多了一个非常不错的选择,而且基于Spring Cloud的强大生态,开发人员能够更加专注于自身业务逻辑。关于Dubbo和Spring Cloud孰强孰弱的问题,则不在本书的讨论范围之内,不过笔者还是要阐述一个观点,在大部分情况下,框架之间细微的性能差异并不是影响系统性能的关键,花心思去优化我们的业务代码似乎更有意义,比如业务上存在一些耗时较长的操作,那么无论是使用Dubbo还是Spring Cloud结果都是一样的。

如今API网关已经成为微服务架构体系中的标准,简单来说,网关层作为整个系统的入口,所有的前端请求都需要通过它来访问后端服务,并由它统一负责处理一些公共逻辑,比如:鉴权、流控、日志记录、安全防护、负载均衡、灰度发布等。如果是基于Spring Cloud的,那么开箱即用的Zuul组件则用于支持开发人员快速构建一个健壮的、高性能的,且具备良好伸缩性的API网关服务。遗憾的是,由于存在架构上的差异,Dubbo目前并没有为开发人员提供成熟、易用的网关服务,因此我们通常会在服务上游构建一层WebServer用以满足如下3个需求:

● 将外部的HTTP协议适配为内部的二进制协议;

● 聚合操作;

● 集成一些公共逻辑。

大家思考一个问题,在不引入API网关的情况下,WebServer其实也能够实现API网关能做的所有事情,那么我们为何还需要大费周章地引入网关层呢?事实上,是否需要引入网关层是由业务复杂度来决定的,如果业务不多,且相对简单的情况下,那么网关层就不是刚需,但如果业务众多和复杂,在没有引入网关层的情况下,那些耦合在WebServer中的公共逻辑的维护成本就会变得非常高,甚至一个小功能的升级,都将耗费大量的时间和人力成本,但如果引入了API网关,事情就会变得简单起来,因为网关逻辑和业务逻辑是完全独立的,架构团队只需要统一对网关层进行升级即可。

笔者所在企业也是使用Dubbo来落地服务化架构的,业务初期我们也并未引入API网关,由WebServer来负责处理网关业务,但随着业务发展到一定阶段后,WebServer变得越来越重,架构团队不得不考虑引入网关层来解决现有痛点。我们最早的解决方案是将接入层Nginx改造为网关层,但发现Lua脚本的维护成本实和实现难度较大,因此,后续我们选择了将一些流控、灰度发布、安全防护,以及日志记录等一些相对简单的网关逻辑放在接入层,尽可能将流量挡在系统上游,而对于一些复杂的网关逻辑,则单独引入了一层API网关,替换掉原本的WebServer,将聚合操作下潜至服务层,API网关通过优化后的泛化调用方式调用下游目标服务,如图1-9所示。

图1-9 API网关层架构

关于如何在API网关中实现流控逻辑,大家可以直接阅读本书的3.2.3小节。而本小节将会为大家介绍如何在API网关中实施灰度发布方案。在正式开始为大家讲解什么是灰度发布之前,我先为大家讲个故事。据史书记载,在中国古代的东汉末年三国时期,曹操为了弥补军饷的不足,专门设立了发丘中郎将,摸金校尉等军衔,专司盗墓取财、以贴补军饷之用,由此开启了倒斗的先例。据隋代医家巢元方撰写的《诸病源候论》一书记载“入井冢墓毒瓦斯候”,因此后来的盗墓者们在每次下墓前,都会先将几只金丝雀放至鸟笼中,然后将鸟笼系上绳子后投放至墓中。由于金丝雀具备特殊的呼吸系统结构,使得它们天生就对空气中含有的有害气体十分敏感,因此,如果金丝雀被投放至墓中因为晕倒而不再鸣叫,则表示墓中含有较高浓度的有害气体,不宜下墓,反之,就表示相对安全。这就是灰度发布(也被称之为金丝雀发布)的起源。

随着敏捷开发的日益普及,互联网企业的项目发布也随之变得越发频繁,由于每次新版本的发布都可能伴随着诸多不可控风险,因此我们需要一种发布机制,能够帮助企业降低风险的影响范围。灰度发布,简单来说,就是通过一系列的规则和策略,先将一小部分的用户作为“金丝雀”,让其请求路由到新版本应用上进行观察,待运行正常后,再逐步导流更多的用户到灰度环境中。

当然,如果初期灰度环境的机器数量不多,为了避免系统容量被持续递增的用户流量撑爆而产生宕机,我们还需要实时扩容灰度环境的机器数量,以便支撑更多用户的访问。当所有的用户都顺利切换到新版本应用上后,再停机之前的老版本应用,即可完成灰度发布。

在此大家需要注意,就算灰度期新版本应用存在问题,我们也能够迅速将流量切换回老版本上。其实,实施灰度发布的目的就在于试错,尽可能控制和缩小问题的影响范围。从产品的角度来看,灰度发布的好处是,能够通过收集用户的使用反馈来更好地完善和改进当前产品。然而从研发的角度来看,从预发布环境到灰度环境,都是在不停地试错,以确保最终上线的稳定,哪怕灰度期出现问题,也能够做到快速止损,缩小问题的影响范围,从而保证绝大多数用户可用。

既然API网关作为整个系统的入口,那么在API网关中嵌入灰度逻辑就显得顺理成章,并且对于业务系统而言也不存在任何侵入性。刚才也强调过,我们的API网关物理上虽然是由Nginx和独立的Gateway服务构成的,但其逻辑上仍然是作为同一个整体对外提供服务,对于灰度发布这种相对简单的网关逻辑,我们选择了在Nginx中嵌入Lua脚本来实现。

简而言之,如嵌入在Nginx中的Lua脚本会首先从Header中获取出Token并解析出用户的UserId,再请求Redis验证当前的UserId是否包含在白名单中,只有那些包含在白名单中的用户,才允许访问灰度环境中的新版本应用,如图1-10所示。果是以白名单作为灰度策略的,那么当用户发起HTTP请求后,

图1-10 基于API网关实现灰度发布

对于网关的技术选型,目前市面上开源的网关实现方案有很多选择,比如基于Nginx的KONG、API Umbrella,基于Node.js的apigee、StrongLoop等。总之,网关层的引入并非是必需的,但是业务越复杂,网关的好处就会越明显。

1.1.9 分布式多活数据中心架构演进

出于对灾备、高可用等方面的考虑,稍微有点实力的企业都会选择构建多个数据中心,由主数据中心负责核心业务的正常运转,其他数据中心负责数据备份,及承担一些边缘业务。假设主数据中心发生重大灾难,灾备数据中心可以有效接管,从而减少对用户的影响。传统企业,比如电信、银行一类,大多会构建“两地三中心”架构,即:生产数据中心、同城灾备中心,以及异地灾备中心,如图1-11所示。

图1-11 “两地三中心”架构

“两地三中心”架构尽管具备较好的容灾和容错性,但存在一个弊端,那就是会产生大量的资源浪费。因为多数据中心之间仅仅只是主/备关系,只有当灾难真正不幸降临时,灾备数据中心才能派上用场和体现出它的价值,平时几乎都是闲置状态,这对于那些资金紧张或是互联网企业来说,几乎是难以接受的,前者出于资金压力,而后者却因为需要面对高并发、大流量,以及海量的数据的洗礼,需要充分地利用资源,所以空闲出这么多机器绝对是不能忍受的。目前越来越多的企业,正在逐步从原有的灾备模式开始过渡到“分布式多活数据中心(Distributed Active/Active Data Centers)”的建设上,如图1-12所示。

图1-12 “分布式多活”架构

分布式多活数据中心听上去挺高大上的,实际上就是将2个或者2个以上的数据中心同时并行对外提供服务,实现了对资源的充分利用,避免了资源利用率低下等诸多问题。当然多活数据中心的建设是非常复杂的,我们在实施分布式多活数据中心的建设时,主要需要面对如下3个难题:

● 多数据中心之间需要打通内网专线通道;

● RPC调用需要做到就近调用;

● 数据同步问题。

关于RPC就近调用的问题,大家可以直接阅读本章的1.2.7小节。笔者所在企业的2个数据中心分布情况还是比较复杂的,一个在华东,一个在华南,首先需要打通内网专线通道,提高网络传输速度尽可能降低延迟,正常情况下,运维同学给出的理论延迟率可以控制在20ms内。存储层的数据同步既是核心同时也是难点,因为数据的实时性、一致性、冲突等问题在某些特定场景下是存在矛盾且无解的,所以分布式多活的建设需要根据实际的业务场景而定,业界几乎并没有完美的异地多活方案,也并非所有的业务都能够实现分布式多活,但我们可以采取多种有效的手段,尽可能保证绝大部分核心业务能够实现分布式多活。在此大家需要注意,就算实现了分布式多活,但因为种种原因仍然无法保证业务能够100%可用,因为极端情况下(比如:网络抖动、光纤被施工队挖断等)可能出现的数据无法同步等风险都会导致业务不可用,因此只能是牺牲小部分用户而保证绝大多数用户可用。