目录
分布式系统
RPC 的工作原理
分布式数据存储
分布式锁
降级、熔断、限流
链路追踪
系统优化和故障处理
传统单体服务架构代码数量庞大,牵一发而动全身,一个很小的改动都可能影响整个服务。正所谓不要把所有的鸡蛋装在一个篮子里,代码和数据库不在一个项目里,尽可能的避免了冲突和改动带来的风险。随着集群规模大了之后,很有可能出现集群宕机和磁盘损坏,分布式系统就是将大的服务拆分成很多个微小的服务,将数据库(存储介质)和代码(计算能力)分布到不同的服务器上,目的在于解决单台机器计算和IO性能问题,以及单机存储空间不足的问题。微服务是分布式系统的一种具体落地方案。阿里的Dubbo和Spring Cloud都是解决分布式微服务架构的优秀框架。
分布式系统有以下的优点:
分布式系统有以下的缺点:
由于网络通信之间是不可靠的,数据交互很可能会有延迟和数据丢失,因此分区容错性(P)是前提,接下来要么选择一致性,保证数据绝对一致;要么选择可用性,保证服务可用。比如下面所示的,节点A实时同步数据到它的副本节点A1,如果由于网络问题同步失败了,那么客户端从节点A1就读取不到数据了。
RPC (Remote Procedure Call,远程过程调用),指的是通过网络调用另一台计算机上部署服务的技术。而 RPC 框架就封装了网络调用的细节,让你像调用本地服务一样,调用远程部署的服务。RPC的基本原理如下图所示:
【问】如果服务提供者挂了,注册中心怎么知道服务不可用了?
【答】服务掉线分为主动下线和心跳检测。主动下线通知:如果服务要发版,在重启之前先主动通知注册中心:我要重启了,有流量进来先不要分给我,等我重启成功后再放流量进来;
心跳检测:如果服务非正常下线(如断电断网),这个时候注册中心可能不知道该服务已经掉线了,一旦调用就会带来问题。为了避免出现这样的情况,注册中心可以增加一个心跳检测功能,它会对服务提供者(Provider)进行心跳检测,比如每隔 30s 发送一个心跳,如果三次心跳结果都没有返回值,就认为该服务已下线,赶紧更新 Consumer 的服务列表,告诉Consumer 调用别的机器。【追问】服务提供者(Provider)挂了可以依靠注册中心解决,如果注册中心(比如Zookeeper)自己挂了怎么办?
【答】 ZK本身就是集群部署的,如果一台机器挂了,ZK 会选举出集群中的其他机器作为 Master 继续提供服务;如果整个集群都挂了也没问题,因为调用者本地会缓存注册中心获取的服务列表,会省略和注册中心的交互,Consumer 和 Provider 采用直连方式,这些策略都是可配置的。
【问】已经有 http 协议接口,或者说 RestFul 接口,为什么还要使用 RPC 技术?
【答】在接口不多的情况下,使用 http 确实是一个明智的选择。使用 http 协议优点:开发简单、测试直接、部署方便。当业务逐渐变大,并发量上升,RPC 框架的好处就显示出来了:首先 RPC 支持长链接,不需要3次握手,减少了网络开销。其次就是 RPC 框架一般都有注册中心模块,有完善的监控管理功能,服务注册发现、服务下线、服务动态扩展等都方便操作,服务化治理效率大大提高。
一般的关系型数据库(如MySQL),当单表数据在 1000万左右的时候,为了提高查询的性能,就该考虑分表了。如果业务数据量增长的比较快,就要提前预估现有单库单表的数据量读写速度能支撑多久,提前规划时间改造。
常见的拆分策略如下:
分库分表后会带来哪些问题:
分库分表常用中间件:MyCAT、TDDL、Sharding 系列(包括 Sharding JDBC,Sharding-Proxy,Sharding-Sidecar)。
另外,分布式数据存储还会涉及关于Redis和消息队列的使用,我单独整理了两篇文章,点击查看:Redis面试核心技术点和缓存相关问题_浮尘笔记的博客-CSDN博客Redis单线程有一个最大好处就是 节省线程切换的开销,更不用考虑并发读写带来的复杂操作场景,大大节省了线程间切换的时间。单线程模型避免了多线程的频繁上下文切换,这也避免了多线程可能产生的竞争问题。Reids 核心是基于非阻塞的 IO 多路复用机制。优先查询缓存,如果缓存未命中则查询数据库,将结果写入缓存;数据更新时先更新数据库,再删除缓存,然后一段时间后再延迟删缓存(防止并发场景下操作出现问题)。https://blog.csdn.net/rxbook/article/details/131036867
消息队列kafka使用技巧和常见问题_浮尘笔记的博客-CSDN博客消息队列主要解决应用耦合、异步消息、流量削锋等问题,是大型分布式系统不可缺少的中间件。消息生产者 只管把消息发布到 MQ 中而不用管谁来取,消息消费者 只管从 MQ 中取消息而不管是谁生产的,这样生产者和消费者都不用知道对方的存在。对于一些流转步骤较多,或者耗费时间过长的场景,就可以使用消息队列。比如用户下订单成功后,可以通过消息队列异步发送信息,异步处理赠送积分等等。https://blog.csdn.net/rxbook/article/details/131053014
分布式锁是解决分布式系统之间同步访问共享资源的一种方式。在分布式环境下,多个客户端同时更新某个服务端资源的时候,防止出现问题,可以加上分布式锁,在同一时刻只能有一个客户端拿到锁,然后更新数据,其它客户端需要等待之前的客户端释放锁之后才能操作。分布式锁的特点是多进程,多个物理机器上无法共享内存,常见的解决办法是基于内存层的干涉,落地方案就是基于 Redis 的分布式锁 或者 ZooKeeper 分布式锁。
怎么设计分布式锁?需要从以下方面考虑:
可以使用下面几种方式实现分布式锁:
(1)使用关系型数据库(如MySQL)实现分布式锁,性能一般。
先查询数据库是否存在记录,为了防止幻读,通过数据库行锁 select for update 锁住这行数据,然后将查询和插入的SQL在同一个事务中提交。比如下面的SQL语句:
select name from user where user_id = 100 for update
使用SQL行锁可能会出现交叉死锁,比如事务1和事物2分别获取到了记录1和记录2的排他锁,然后事务1又要申请获取记录2的排他锁,事务2要申请获取记录1的排他锁,那么这两个事务就会因为相互锁等待产生死锁。
当然可以通过超时控制解决交叉死锁的问题,但是在高并发场景下会出现大部分请求排队等待的情况,所以这个方案在性能上存在缺陷。关于MySQL锁的更多内容请查看:深入理解MySQL中的事务和锁
(2)使用分布式缓存(如Redis)实现分布式锁,性能不错。
由于Redis是在内存中运行,效率远高于MySQL,使用基于Redis的分布式锁可以避免大量请求直接访问数据库,从而提高系统的响应能力。如下面的命令:
SET my_key my_value NX PX 3000
#my_value 是客户端生成的唯一的标识, NX代表只在 my_key 不存在时才设置 my_value
#PX 3000 表示设置my_key的过期时间为3s,防止客户端发生异常而导致无法释放锁
Redis如何解决集群模式分布式锁的可靠性:假设目前有N个独立的Redis实例,客户端先按顺序依次向N个Redis实例执行加锁操作,这里的加锁操作和在单实例上执行的加锁操作一样,但是Redlock 算法设置了加锁的超时时间,为了避免因为某个Redis实例发生故障而一直等待的情况。客户端完成了和所有Redis实例的加锁操作之后,如果有超过半数的Redis实例成功的获取到了锁,并且总耗时没有超过锁的有效时间,那么就是加锁成功。
Redlock是redis官方实现的分布式锁(Redis Distributed Lock),使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击);这个锁的算法实现了多redis实例的情况,相对于单redis节点来说,优点在于 防止了 单节点故障造成整个服务停止运行的情况;并且在多节点中锁的设计,及多节点同时崩溃等各种意外情况有自己独特的设计方法。Redlock(redis分布式锁)原理分析
(3)使用Zookeeper实现分布式锁。
一台机器接收到了请求之后,先获取 zookeeper 上的一把分布式锁(zk 会创建一个 znode)执行操作;然后另外一台机器也尝试去创建那个 znode,结果发现自己创建不了,因为被别人创建了,那只能等待,等第一个机器执行完了才能拿到锁。更多资料可以参考:zookeeper实现分布式锁
在分布式系统中,很多服务之间有依赖关系,并且可能有一定的调用顺序,如果其中有一个环节出现问题,就会带来一系列连锁反应。比如,突如其来的流量导致部分服务突然宕机,会不会影响到整个服务都不可用?针对这样的情况应该怎么解决?思路就是尽量给每个服务找一个备用方案。
比如有个电商平台中有商品、促销、积分 三个系统,出现流量高峰期时,虽然商品系统很容易扩容,但对于商品系统依赖的其他服务,就不会有实时性的响应。那么促销或积分系统就可能因为无法承担大流量,请求处理缓慢,直到所有线程资源被占满,无法处理后续的请求。
在分布式系统中,当检测到某一个系统或服务响应时长出现异常时,要想办法停止调用该服务,快速返回失败,从而释放此次请求持有的资源,这就是架构设计中经常提到的降级和熔断机制。假设某个系统因为一个热点事件突然涌入了大量的请求,导致QPS剧增,服务无法响应。这个时候可以从以下几个方面考虑:
限流:包括单机限流和集群限流,给核心系统的某一环节加一个开关。比如将系统 QPS 控制在最高 2000,后面的 1000 用户告诉他 “系统繁忙,请稍后再试”,这样一来最起码前面的2000次请求可以正常响应,后面的响应排队处理,而不至于让所有用户都无法使用系统。
降级:当资源和访问量出现矛盾时,在有限的资源下放弃部分非核心功能或者服务,保证整体的可用性。降级的实现手段是:在请求流量突增的情况下,放弃一些非核心流程或非关键业务,释放系统资源,让核心业务正常运行,或者对非核心业务采用备用方案。比如商品列表查询,默认查询的是 Redis 集群,各种故障赶在一起,Redis 所有集群都挂了不能用了,这个时候可以计一个备用方案,比如使用 Elasticsearch来返回查询结果,虽然查询速度可能没 Redis 快,但最起码还能用。
(1)服务降级:取舍非核心服务;以及把强一致性转换为最终一致性。
(2)功能降级:产品功能上的取舍,可以通过开关控制某些非核心功能不可用。
熔断: 熔断也是降级的一种手段。在服务A调用服务B时,如果B返回错误或超时的次数超过一定阈值,服务A的后续请求将不再调用服务B。这种设计方式就是断路器模式。
断路器实现熔断的设计原理:服务调用方为每一个调用的服务维护一个有限状态机,在这个状态机中存在三种状态:(1)关闭:正常调用远程服务; (2)半打开:尝试调用远程服务; (3)打开:直接返回错误,不调用远程服务。
- 关闭 --> 打开: 当服务调用失败的次数累积到一定的阈值时,服务熔断状态从关闭切换到打开;
- 打开 --> 半打开: 当熔断处于打开状态时,会启动一个超时计时器,当计时器超时后,状态切换到半打开;
- 半打开 --> 关闭: 在熔断处于半打开状态时,请求可以达到后端服务,如果累计一定成功次数后,状态切换到关闭;
在秒杀和抢购场景下,或者容易被爬虫的页面,可以使用限流来限制并发请求量,如果没有配置限流,在遇到上游系统频繁调用的情况下可能会导致下游系统被击垮。如果配置了限流,但是限流算法不合理,也有可能导致正常访问被拒绝。所以限流算法不能乱用,要充分评估系统是否需要限流,如果要限流,流量大小如何评估。使用限流的目的就是为了保证服务正常运行,也是为了下游系统不会轻易被拖垮,流量合理放行。
常用的限流算法:
对于核心服务限流的值可以通过以下方法来设置合理的值:
断路器Hystrix:是一个用于处理分布式系统的延迟和容错的一个开源库,能保证在一个依赖出现问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的稳定性。更多用法参考:Hystrix - 简书
对于复杂的分布式系统,一个请求可能调用几十到几百个服务,经过很多业务层,而每个业务又是多个机器集群,一个请求具体被随机到哪台机器上又无法确定,如果最后用户的请求失败,只返回一个错误提示,作为开发人员,该如何定位解决问题?
分布式系统中针对上述问题,需要一套链路追踪系统来解决,这个系统主要的任务就是收集各服务的上报日志,然后分析并存储,在需要的时候方便查询。关键核心在于调用链,为每个请求生成全局唯一的 ID(Traceld),通过 Traceld 将不同系统的调用信息关联在一起。通过这个链路追踪系统,可以清楚的知道服务调用深度、涉及服务个数、每个服务调用的时间及状态,到底是哪个服务出现异常,可以具体到方法名。
成熟的调用链开源工具:
一般情况下会从以下维度去衡量系统的性能优劣:
全链路分析系统性能的指标:
- DNS解析:用户在浏览器输入URL按回车,请求会进行DNS查找,浏览器通过DNS解析查到域名映射的IP地址,查找成功后,浏览器会和该IP地址建立连接。对应的性能指标为: DNS解析时间。
- TCP连接:由于HTTP是应用层协议, TCP是传输层协议,所以HTTP是基于TCP协议基础上进行数据传输的,所以可以用TCP的连接时间来衡量浏览器与Web服务器建立的请求连接时间。
- 服务器响应:服务器端的延迟和吞吐能力,可以细分为基础设施性能指标、数据库性能指标、系统应用性能指标。
- 基础设施性能指标:主要针对 CPU利用率、磁盘IO、网络带宽、内存利用率等;
- 数据库的性能指标:主要有SQL查询时间、并发数、连接数、缓存命中率等;
- 系统应用性能指标:和系统业务有关,因为业务场景影响架构设计。
- 前台渲染:当浏览器与Web服务器建立连接后,就可以进行数据通信了。
- 由于浏览器自上而下显示HTML,同时渲染顺序也是自上而下的,所以当用户在浏览器地址栏输入URL按回车,到他看到网页的第一个视觉标志为止,这段白屏时间可以作为一个性能的衡量指标,优化手段为减少首次文件的加载体积。
- 首屏时间是指:用户在浏览器地址输入URL按回车,然后看到当前窗口的区域显示完整页面的时间。一般情况下,一个页面总的白屏时间在2秒以内,用户会认为系统响应快,2~5秒,用户会觉得响应慢,超过5秒很可能造成用户流失。
系统优化的思路:
系统可用性指标:
如果要设计一个系统可用性达到4个9(99.99%)的监控报警体系,需要从以下三个方面考虑:
(1)基础设施监控,判断系统的基础环境是否为高可用。
监控工具常用的有ZABBIX、Open-Falcon、 Prometheus,这些工具基本都能监控所有系统的CPU、内存、磁盘、网络带宽、网络IO 等基础关键指标,再结合一些运营商提供的监控平台,就可以覆盖整个基础设施监控。
监控报警策略一般由时间维度、报警级别、阈值设定三部分组成,假设系统的监控指标有CPU、内存、磁盘,监控的时间维度是分钟级,监控的阈值设置为占比。那么可以定义出如下的监控报警策略:
(2)系统应用监控,主要关注系统自身状态是否健康。
(3)存储服务监控。
常用的第三方存储有 MySQL、Redis、ES、MQ 等。对于存储服务的监控,除了基础指标监控之外,还有一些比如集群节点、分片信息、存储数据信息等 相关特有存储指标的监控。
如果生产环境出现了以上的报警信息,应该怎么处理?
高性能架构实现需要关注以下几个方面:
更多内容请查看:后端系统高并发解决方案分析