对于服务间RPC请求遇到网络抖动的情况,最简单的解决办法就是重试。重试可以提高RPC请求的最终成功率,增强服务应对网络抖动情况时的可用性。
3.2.1 幂等接口当执行RPC请求调用下游服务接口遇到网络超时的情况时,我们并不知道RPC请求是否已经被下游服务成功处理,因为超时可能出现在请求处理的多个阶段。例如:
RPC请求发送超时,此时下游服务并未收到RPC请求。
RPC请求处理超时,下游服务已经收到RPC请求,但是处理时间过长。
RPC响应报文超时,下游服务已经处理完RPC请求,但是响应报文超时未回复。
我们的服务无法准确判断RPC请求是否被下游服务成功处理,所以只能假定最坏的情况:下游服务已经成功处理请求,但是我们的服务没有收到响应信息。此时,如果我们的服务要进行重试,那么下游服务必须保证再次处理同一请求的结果与用户预期相符。
怎样才算与用户预期相符呢?举一个电商产品下单服务的例子。用户选择购买价格为100元的产品时,下单服务会调用用户账户服务的扣款接口,从用户的余额中扣除100元。假如用户账户服务已经成功从用户的余额中扣除100元,但是对下单服务请求的响应超时,这时下单服务将重试 ...
当某个业务从单体服务架构转变为微服务架构后,多个服务之间会通过网络调用形式形成错综复杂的依赖关系。以负责展示用户主页的个人页服务为例,为了拼装出完整的用户主页,个人页服务需要对多个其他微服务发起RPC请求,如图3-1所示。
从用户信息服务中获取用户昵称、头像、个性签名等用户基础信息。
从地理位置服务中获取用户活跃IP地址的归属国家和省市信息。
从内容列表服务中获取用户已发布内容的列表。而内容列表服务要进一步从计数服务中获取用户发布的内容数、点赞总数,并从内容服务中获取每条内容的具体信息,如文本、图片、发布时间、点赞数、评论数、转发数等,其中后三者又需要内容服务继续从计数服务中获取。
从关系服务中获取用户与请求发起者之间的关系,并进一步从计数服务中获取用户的关注数和粉丝数。
我们可以看到,在微服务架构中,一个微服务正常工作依赖它与其他微服务之间的多级网络调用,这是微服务架构与单体服务架构最典型的区别。
但网络是脆弱的,RPC请求有较大的概率会遇到超时、抖动、断开连接等各种异常情况,这些都会直接影响微服务的可用性。比如个人页服务在调用用户信息服务时发生网络超时,由于无法获取到用户基 ...
数据分片本质上是通过提高系统的可扩展性来支撑高并发写请求的,每当写请求量达到一个新高度时,系统就需要数据分片扩容。从产品发展的角度来讲,这本无可厚非,但是扩容就意味着需要更多昂贵的服务器资源,经济成本较高;况且扩容不是一个实时操作,对临时的突增流量很难及时应对。实际上,我们还可以从业务的角度和数据特点的角度来思考高并发写场景的应对之道,本节就来介绍两种常见的方案:
异步写
写聚合
2.7.1 异步写异步写是一个泛化的概念,并不局限于实现形式。异步写把写请求的交互流程从“用户发起写请求并同步等待结果返回”转变为“用户提交写请求后,异步查询结果”的两阶段交互。一般而言,异步写的技术实现有如下特点。
将用户写请求先以适当的方式快速暂存到一个数据池中,然后立刻响应用户,告知其请求提交成功,以便缩短写请求的响应时间。
真正的写操作由后台任务不断地从数据池中读取请求并真正执行。
写操作结果依靠用户主动查询,有的业务场景为了提高实时性,也会在写操作执行完成后王动将结果通知给用户。
异步写非常适合写请求量大,但是被请求方的系统吞吐量跟不上的场景——写请求先排队,被请求方以正常的速度处理请求, ...
数据分片是指将待处理的数据或者请求分成多份并行处理。在现实生活中,有很多与数据分片思想一致的场景,例如:
为了减少患者与家属的排队时间,医院会开通多个挂号/收费窗口;
为了提高乘客进站的速度,人流量大的火车站、地铁站会设置多个闸机口,同时为乘客检票。
互联网应用在应对高并发写请求的架构设计中,数据分片也是一种常用方案。数据分片有多种表现形式,其中被广泛提及的是数据库分库分表。由于数据库分库分表已经可以充分体现数据分片的主要技术要素,所以本节会以数据库分库分表形式为主、其他数据分片形式为辅展开介绍。
2.6.1 分库和分表数据库的分库和分表其实是两个概念:
分库指的是将数据库拆分为多个小数据库,原来存储在单个数据库中的数据被分开存储到各个小数据库中;
分表指的是将单个数据表拆分为多个结构完全一致的表,原来存储在单个数据表中的数据被分开存储到各个表中。
由于数据库的分库操作和分表操作一般会同时进行,所以通常将它们合并在一起称为“数据库分库分表”。
大部分互联网应用都绕不开数据库分库分表,因为随着业务的不断发展和用户活跃度的提高,数据库会面临诸多挑战。
数据量大:当业 ...
无论是数据库读/写分离(见2.2节)、本地缓存(见2.3节)还是分布式缓存(见2.4节),其本质上都是读/写分离,这也是在微服务架构中经常被提及的CQRS模式。
CQRS(Command Query Responsibility Segregation,命令查询职责分离)是一种将数据的读取操作与更新操作分离的模式。query指的是读取操作,而command是对会引起数据变化的操作的总称,新增、删除、修改这些操作都是命令。
2.5.1 CQRS的简要架构与实现为了避免引入微服务领域驱动设计的相关概念,图2-8给出了CQRS的简要架构。
当业务服务收到客户端发起的command请求(即写请求)时,会将此请求交给写数据存储来处理。
写数据存储完成数据变更后,将数据变更消息发送到消息队列。
读数据存储负责监听消息队列,当它收到数据变更消息后,将数据写入自身。
当业务服务收到客户端发起的query请求(即读请求)时,将此请求交给读数据存储来处理。
读数据存储将此请求希望访问的数据返回。
写数据存储、读数据存储、数据传输通道均是较为宽泛的代称,其中写数据存储和读数据存储在 ...
由于本地缓存把数据缓存在服务进程的内存中,不需要网络开销,故而性能非常高。但是把数据缓存到内存中也有较多限制,举例如下。
无法共享:多个服务进程之间无法共享本地缓存。
编程语言限制:本地缓存与程序绑定,用Golang语言开发的本地缓存组件不可以直接为用Java语言开发的服务器所使用。
可扩展性差:由于服务进程携带了数据,因此服务是有状态的。有状态的服务不具备较好的可扩展性。
内存易失性:服务进程重启,缓存数据全部丢失。
我们需要一种支持多进程共享、与编程语言无关、可扩展、数据可持久化的缓存,这种缓存就是分布式缓存。
2.4.1 分布式缓存选型主流的分布式缓存开源项目有Memcached和Redis,两者都是优秀的缓存产品,并且都具有缓存数据共享、与编程语言无关的能力。不过,相对于Memcached而言,Redis更为流行,主要体现如下。
数据类型丰富:Memcached仅支持字符串数据类型缓存,而Redis支持字符串、列表、集合、哈希、有序集合等数据类型缓存。
数据可持久化:Redis通过RDB机制和AOF机制支持数据持久化,而Memcached没有数据持久化能力。
高可用性: ...
在计算机世界中,缓存(Cache)无处不在,如CPU缓存、DNS缓存、浏览器缓存等。值得一提的是,Cache在我国台湾地区被译为“快取”,更直接地体现了它的用途:快速读取。缓存的本质是通过空间换时间的思路来保证数据的快速读取。
业务服务一般需要通过网络调用向其他服务或数据库发送读数据请求。为了提高数据的读取效率,业务服务进程可以将已经获取到的数据缓存到本地内存中,之后业务服务进程收到相同的数据请求时就可以直接从本地内存中获取数据返回,将网络请求转化为高效的内存存取逻辑。这就是本地缓存的主要用途。在本书后面的核心服务设计篇中会大量应用本地缓存,本节先重点介绍本地缓存的技术原理。
2.3.1 基本的缓存淘汰策略虽然缓存使用空间换时间可以提高数据的读取效率,但是内存资源的珍贵决定了本地缓存不可无限扩张,需要在占用空间和节约时间之间进行权衡。这就要求本地缓存能自动淘汰一些缓存的数据,淘汰策略应该尽量保证淘汰不再被使用的数据,保证有较高的缓存命中率。基本的缓存淘汰策略如下。
FIFO(First In First Out)策略:优先淘汰最早进入缓存的数据。这是最简单的淘汰策略,可以基于队列实现 ...
大部分互联网应用都是读多写少的,比如刷帖的请求永远比发帖的请求多,浏览商品的请求永远比下单购买商品的请求多。数据库承受的高并发请求压力,主要来自读请求。
我们可以把数据库按照读/写请求分成两类:
专门负责处理写请求的数据库(写库)
专门负责处理读请求的数据库(读库)
让所有的写请求都落到写库,写库将写请求处理后的最新数据同步到读库,所有的读请求都从读库中读取数据。这就是数据库读/写分离的思路。
数据库读/写分离使大量的读请求从数据库中分离出来,减少了数据库访问压力,缩短了请求响应时间。
2.2.1 读/写分离架构我们通常使用数据库主从复制技术实现读/写分离架构,将数据库主节点Master作为“写库”,将数据库从节点Slave作为“读库”,一个Master可以与多个Slave连接,如图2-2所示。
市面上各主流数据库都实现了主从复制技术,参见1.7.3节介绍的MySQL数据库的主从复制原理。
2.2.2 读/写请求路由方式在数据库读/写分离架构下,把写请求交给Master处理,而把读请求交给Slave处理。那么由 ...
高并发意味着系统要应对海量请求。从笔者多年的面试经验来看,很多面试者在面对“什么是高并发架构”的问题时,往往会粗略地认为一个系统的设计是否满足高并发架构,就是看这个系统是否可以应对海量请求。再细问具体的细节时,回答往往显得模棱两可,比如每秒多少个请求才是高并发请求、系统的性能表现如何、系统的可用性表现如何,等等。为了可以清晰地评判一个系统的设计是否满足高并发架构,在正式给出通用的高并发架构设计方案前,我们先要厘清以下三点:
形成高并发系统的必要条件
高并发系统的衡量指标
高并发场景分类
2.1.1 形成高并发系统的必要条件形成高并发系统主要有三大必要条件。
高性能:性能代表一个系统的并行处理能力,在同样的硬件设备条件下,性能越高,越能节约硬件资源;同时性能关乎用户体验,如果系统响应时间过长,用户就会产生抱怨。
高可用性:系统可以长期稳定、正常地对外提供服务,而不是经常出故障、宕机、崩溃。
可扩展性:系统可以通过水平扩容的方式,从容应对请求量的日渐递增乃至突发的请求量激增。
我们可以将形成高并发系统的必要条件类比为一个篮球运动员的各项属性:
高性能:相当于这个球员在赛场上的表 ...
大部分互联网应用使用“同城双活”架构就可以承担海量用户请求与保障后台高可用了,但是如果你的应用不是仅面向一个国家,而是面向全球(如Facebook、Instagram等), 那么“同城双活”架构就会带来一些问题。
用户访问延迟问题。比如我们在泰国曼谷建设了“同城双活”机房,泰国、日本、 韩国、马来西亚等附近国家用户的访问请求能被快速响应,而欧洲用户的访问请求只能“跨越山河大海”才能接入机房(因为欧洲距离曼谷物理位置太远),这就会造成访问延迟大大增加,用户会明显感觉到应用卡顿。
数据合规问题。很多国家非常注重互联网用户隐私数据安全,它们通常要求应用将本国用户的数据独立存储到本国机房。“同城双活”架构最多只能满足一个国家的数据合规要求。
灾难问题。如果部署机房的国家发生了战争、暴乱、自然灾害等,则可能导致机房被破坏,进而导致整个应用在全球范围内不可用。
全球级互联网应用后台一般采用多国部署机房的架构:在全球范围内筛选几个国家和城市部署机房并负责接入附近国家用户的访问请求,各个机房之间通过数据复制保证它们都有全球全量数据,这就是“异地多活”架构。
1.14.1 架构要点假设我们在全球 ...