唯一ID生成器本身也是一个服务,为了生成单调递增的唯一ID,这个服务需要使用某种存储系统记录可分配的唯一ID。Redis和其他数据库都可以达到这个目的。
4.2.1 Redis INCRBY 命令Redis提供的INCRBY命令可以为键(Key)的数字值加上指定的增量(increment)。如果键不存在,则其数字值被初始化为0,然后执行增量操作。使用INCRBY命令限制的值类型为64位有符号整数,此命令的特性与单调递增的唯一ID的诉求非常契合。基于Redis INCRBY命令实现的唯一ID生成器的Go语言代码非常简单:
12345678func GenID() (int64, error) { // 执行 Redis 命令:INCRBY seq_id 1 cmd := rdb.IncrBy(context.TODO(), "seq_id", 1) if cmd.Err() != nil { return 0, cmd.Err() } return cmd.Val(), nil}
每 ...
在复杂的系统中,每个业务实体都需要使用ID做唯一标识,以方便进行数据操作。例如,每个用户都有唯一的用户ID,每条内容都有唯一的内容ID,甚至每条内容下的每条评论都有唯一的评论ID。
4.1.1 全局唯一与UUID在互联网还未普及的年代,由于用户量少、网络交互形式单调,互联网产品后台数据库使用单体架构就可以满足日常服务的需求。当时每个业务实体都对应数据库中的一个数据表,每条数据都简单地使用数据库的自增主键作为唯一ID。
近年来,随着互联网用户的爆发式增长,数据库从单体架构演进到分库分表的分布式架构,同一个业务实体的数据被分散到多个数据库中。由于数据表之间相互独立,在插入数据时会生成相同的自增主键。此时,如果还使用自增主键作为唯一ID,就会导致大量数据的标识相同,造成严重事故。我们应该保证无论一个业务实体的数据被分散到多少个数据库中,每条数据的唯一ID都是全局的,这个全局唯一ID就是分布式唯一ID。
RFC 4122 规范中定义了通用唯一识别码(Universally Unique Identifier, UUID),它是计算机体系中用于识别信息的一个128位标识符。UUID按照标准方法生 ...
在3.4节中,我们曾列举著名景区在节假日期间限制游客数量的例子来表述限流,而景区在节假日期间将不重要的、安全风险较大的或难以管理的游玩项目暂时关闭叫作“降级”,其目的是保障游客的游玩核心体验。与此类似,服务降级的目的是重点保障用户的核心体验和服务的可用性。在异常、高并发的情况下可以忽略非核心场景或换一种简单处理方式,以便释放资源给核心场景,保证核心场景的正常处理与高性能执行。服务降级的实施方案灵活性较大,一般与业务场景息息相关,接下来我们介绍几种思路。
3.6.1 服务依赖度降级一个服务虽然会有多个下游服务,但是每个下游服务的重要程度对它来说都是不一样的。例如,3.1节中提到的用户信息服务、内容列表服务对于个人页服务来说很重要,而地址位置服务和关系服务就不是很重要。
如果B服务是A服务的下游服务,那么B服务对A服务的重要性被称为“依赖度”。依赖度越高,表明下游服务越重要。依赖度可以用如下两种(但不限于)表示形式来反映下游服务对上游服务的重要程度。
二元:强依赖(出现故障时业务不可接受)和弱依赖(出现故障时业务可暂时接受)
三元:一级依赖(故障导致服务完全不可用)、二级依赖(故障基本 ...
无论是时间窗口、漏桶算法、令牌桶算法,还是全局限流方案,限流阈值都是人为设置的,这就意味着这些限流策略的实际效果很被动,依赖限流阈值的设置是否足够合理。
为了设置合理的限流阈值,在一个服务正式上线前,我们一般会事先对它进行一段时间的全链路压测,再根据压测期间服务节点的各项性能指标,选择服务负载接近临界值前的QPS作为限流阈值。
基于服务的压测数据得出的限流阈值看似是合理的,但是服务的性能会随着服务的不断迭代而变化,例如:
某服务的单个实例可承受的最大QPS为100,研发工程师不满意此服务的性能表现,于是对服务进行了系统性重构,性能得到大幅提升,单个实例可承受的最大QPS变为300。
某服务原本是一个轻量级服务,单个实例可承受的最大QPS为500。但是在某次产品需求变更中,为此服务增加了对多个下游服务的调用,于是服务性能下降到单个实例可承受的最大QPS为100。
可以看到,服务的每次迭代都有可能影响服务的性能,性能可能提升,也可能劣化。
如果服务的性能提升了,则原限流阈值会限制服务发挥性能,浪费服务资源;
如果服务的性能劣化了,则原限流阈值无法有效保护服务不被打垮。
理论上,服 ...
3.2节和3.3节介绍的重试、熔断、资源隔离,都是上游服务为了提高自身服务质量和适当保护下游服务而采用的策略。本节将介绍作为下游服务,为了应对多个上游服务的请求访问,以防被上游服务打垮应做好的预防机制,这个预防机制就是老生常谈的“限流”。
在现实生活中有大量应用限流的场景,比如某些著名景区在劳动节、国庆节等节假日期间往往人满为患,不仅容易破坏景区环境,而且容易发生踩踏事故,游客的体验也非常差,于是景区管理部门就会通过一系列手段对景区限流,如限定每日票量、限制游玩项目同时参与的人数等。在互联网场景中,这样的例子也随处可见,比如“双十一”电商秒杀抢购、火车票抢票等场景,都通过限流策略来防止服务被海量请求打垮。限流的表现形式主要包括如下几种。
频控:控制用户在N秒内只可执行M次操作,比如限制用户在30s内只能下载1次文件、在1h内最多只能发布5条动态。
单机限流+固定阈值:某服务的每台服务器在1s内最多可处理M个请求,M值是预先设置好的。
全局限流+固定阈值:某服务在1s内总共可处理M个请求,与前者的主要区别在于限流范围是某服务的全部服务实例。
单机自适应限流:某服务的每台服务器根据自身 ...
熔断和隔离都是上游服务可以采取的流量控制策略。
熔断可以有效防止我们的服务被下游服务拖垮,同时可以在一定程度上保护下游服务。
隔离可以防止一个服务内各个接口之间因质量问题而相互影响。
3.3.1 服务雪崩由于网络原因或服务自身设计问题,每个微服务一般都难以保证100%对外可用。如果某服务出现了质量问题,那么与其相关的上游服务网络调用就容易出现线程阻塞的情况;如果有大量的线程发生阻塞,则会导致上游服务承受较大的负载压力而发生宕机故障。在微服务架构中,由于在服务间建立了依赖关系,所以一个服务的故障会不断向上传播,最终导致整个服务链路发生宕机故障。这就是服务雪崩现象。
假设有3个服务形成如图3-7所示的依赖关系,最上游的Server-1服务直接负责与用户请求交互。
如图3-8所示,某一天,Server-3服务因请求量暴增或设计不合理而宕机。由于Server-2是其上游服务,所以Server-2服务还会有源源不断的请求继续调用Server-3服务。
如图3-9所示,随着Server-2服务内大量的请求调用线程被阻塞在对Server-3服务的调用上,Server-2服务最终也由于大量的线 ...
对于服务间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 分库和分表数据库的分库和分表其实是两个概念:
分库指的是将数据库拆分为多个小数据库,原来存储在单个数据库中的数据被分开存储到各个小数据库中;
分表指的是将单个数据表拆分为多个结构完全一致的表,原来存储在单个数据表中的数据被分开存储到各个表中。
由于数据库的分库操作和分表操作一般会同时进行,所以通常将它们合并在一起称为“数据库分库分表”。
大部分互联网应用都绕不开数据库分库分表,因为随着业务的不断发展和用户活跃度的提高,数据库会面临诸多挑战。
数据量大:当业 ...