分布式系统涵盖的知识面非常广,all in one本篇我会介绍下我所了解的知识点,预计完稿后的知识密度较高,本篇只点到为止,每个点都有很多值得深挖和学习的内容,后续逐渐会有相关系列文章来具体阐述。
1990年,浏览器诞生。1994年底,万维网联盟(World Wide Web Consortium,简称3W)成立,这标志着万维网的正式诞生。此时的网页以HTML为主,是纯静态的网页,信息流只能通过服务器到客户端单向流通,由此世界进入Web1.0时代,至今三十年过去了,互联网已经进入web3.0。随之而来遇到的挑战非常多,包括但不局限于下面几点
上述一系列挑战对系统架构设计产生了非常要的要求,比如三高
不同服务SLA指标对应一年内故障时间不超过 3个9:(1-99.9%)36524=8.76小时 4个9:(1-99.99%)36524=0.876小时=52.6分钟 5个9:(1-99.999%)36524*60=5.26分钟
上述挑战并不是一天发生的,而是随着互联网的日益发展逐渐诞生,相应地我们只需要渐进式地对系统进行重构和优化。在我看来,好的产品和架构设计都是不断随着业务变动,不同发展阶段都有自身特点,切忌过度设计。
最早的时候,平台的用户访问量低,只需要单台或者少数几台服务器即可。将文件、数据库与应用程序一起部署在单物理机上。
随着网站业务的发展,一台服务器逐渐不能满足需求:越来越多的用户访问导致性能越来越差,越来越多的数据导致存储空间不足。这时就需要将应用和数据分离。应用和数据分离后整个网站使用3台服务器:应用服务器、文件服务器和数据库服务器。这3台服务器对硬件资源的要求各不相同:
随着系统规模越来越大,我们需要考虑分布式系统的架构设计。
随着网站业务不断发展,用户规模越来越大,由于中国复杂的网络环境,不同地区的用户访问网站时,速度差别也极大。有研究表明,网站访问延迟和用户流失率正相关,网站访问越慢,用户越容易失去耐心而离开。为了提供更好的用户体验,留住用户,网站需要加速网站访问速度。主要手段有使用 CDN 和反向代理。如下图所示:
CDN 和反向代理的基本原理都是缓存。 CDN 部署在网络提供商的机房,使用户在请求网站服务时,可以从距离自己最近的网络提供商机房获取数据 反向代理则部署在网站的中心机房,当用户请求到达中心机房后,首先访问的服务器是反向代理服务器,如果反向代理服务器中缓存着用户请求的资源,就将其直接返回给用户 使用 CDN 和反向代理的目的都是尽早返回数据给用户,一方面加快用户访问速度,另一方面也减轻后端服务器的负载压力。
大型网站为了应对日益复杂的业务场景,通过使用分而治之的手段将整个网站业务分成不同的产品线。如大型购物交易网站都会将首页、商铺、订单、买家、卖家等拆分成不同的产品线,分归不同的业务团队负责。
具体到技术上,也会根据产品线划分,将一个网站拆分成许多不同的应用,每个应用独立部署。应用之间可以通过一个超链接建立关系(在首页上的导航链接每个都指向不同的应用地址),也可以通过消息队列进行数据分发,当然最多的还是通过访问同一个数据存储系统来构成一个关联的完整系统,如下图所示: 随着业务拆分越来越小,存储系统越来越庞大,应用系统的整体复杂度呈指数级增加,部署维护越来越困难。由于所有应用要和所有数据库系统连接,在数万台服务器规模的网站中,这些连接的数目是服务器规模的平方,导致数据库连接资源不足,拒绝服务。
既然每一个应用系统都需要执行许多相同的业务操作,比如用户管理、商品管理等,那么可以将这些共用的业务提取出来,独立部署。由这些可复用的业务连接数据库,提供共用业务服务,而应用系统只需要管理用户界面,通过分布式服务调用共用业务服务完成具体业务操作。如下图所示:
SOA(全称Service Oriented Architecture),中文意思为 “面向服务的架构”,你可以将它理解为一个架构模型或者一种设计方法,而并不是服务解决方案。其中包含多个服务, 服务之间通过相互依赖或者通过通信机制,来完成相互通信的,最终提供一系列的功能。一个服务通常以独立的形式存在与操作系统进程中。各个服务之间通过网络调用 。跟SOA相提并论的还有一个ESB(企业服务总线),简单来说ESB就是一根管道,用来连接各个服务节点。为了集成不同系统,不同协议的服务,ESB可以简单理解为:它做了消息的转化解释和路由工作,让不同的服务互联互通;我们将各个应用之间彼此的通信全部去掉,在中间引入一个ESB企业总线,各个服务之间,只需要和ESB进行通信,这个时候,各个应用之间的交互就会变得更加的清晰,业务架构/逻辑等,也会变得很清楚。原本杂乱没有规划的系统,梳理成了一个有规划可治理的系统,在这个过程中,最大的变化,就是引入了ESB企业总线。
好处
微服务架构其实和SOA架构类似,微服务是在SOA上做的升华。微服务架构重点强调的一个是"业务需要彻底的组件化和服务化",原有的单个业务系统会拆分为多个可以独立开发、设计、运行的小应用。这样的小应用和其他各个应用之间,相互去协作通信,来完成一个交互和集成,这就是微服务架构。
组件化:组件表示一个可以独立更换和升级的单元,就以PC机为例,PC中的 CPU、内存、显卡、硬盘一样,独立更换升级而不影响其他单元。如果我们把PC作为组件以服务的方式构建,那么这台PC只需要维护主板和一些必要的外部设备。CPU、内存、硬盘都是以组件方式提供服务,PC需要调用CPU做计算处理,只需要知道CPU这个组件的地址即可。
特征
分布式架构带来一些的诸多问题和技术难点,比如分布式事务问题,数据强弱一致性问题,负载均衡。 正如我们前面所说的,构建分布式系统的目的是增加系统容量,提高系统的可用性,一方面,通过集群技术把大规模并发请求的负载分散到不同的机器上,另一方面,提高服务的可用性,把故障隔离起来阻止多米诺骨牌效应(雪崩效应)。如果流量过大,需要对业务降级,以保护关键业务流转。说白了就是干两件事。一是提高整体架构的吞吐量,服务更多的并发和流量,二是为了提高系统的稳定性,让系统的可用性更高。
提高系统性能 提高系统稳定性
分布式系统中存在CAP理论,即Consistency(强一致性)、Availability(可用性)、Partition tolerance(分区容错性 。CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求。它存在几种组合
CAP的证明基于异步网络,异步网络也是反映了真实网络中情况的模型。真实的网络系统中,节点之间不可能保持同步,即便是时钟也不可能保持同步,所有的节点依靠获得的消息来进行本地计算和通讯。这个概念其实是相当强的,意味着任何超时判断也是不可能的,因为没有共同的时间标准。之后我们会扩展CAP的证明到弱一点的异步网络中,这个网络中时钟不完全一致,但是时钟运行的步调是一致的,这种系统是允许节点做超时判断的。 CAP理论应用在分布式存储系统中,最多只能实现上面的两点。由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们必须需要实现的。所以我们只能在一致性和可用性之间进行权衡,没有任何分布式系统能同时保证这三点。一致性和可用性之间取一个平衡。多余大多数web应用,其实并不需要强一致性。因此牺牲C换取P,这是目前分布式数据库产品的方向。
BASE就是为了解决关系数据库强一致性引起的问题而引起的可用性降低而提出的解决方案。 有三个特性
它的思想是通过让系统放松对某一时刻数据一致性的要求来换取系统整体伸缩性和性能上改观。为什么这么说呢,缘由就在于大型系统往往由于地域分布和极高性能的要求,不可能采用分布式事务来完成这些指标,要想获得这些指标,我们必须采用另外一种方式来完成,这里BASE就是解决这个问题的办法
在微服务架构中,整个系统会按职责能力划分为多个服务,通过服务之间协作来实现业务目标。这样在我们的代码中免不了要进行服务间的远程调用,服务的消费方要调用服务的生产方,为了完成一次请求,消费方需要知道服务生产方的网络位置(IP地址和端口号)。云原生的环境下,存在海量的独立部署服务,服务的生命周期缩短,频繁的更新发布,服务部署资源的可伸缩性使得实例经常变更,因此需要一个注册中心提供名字服务,以便请求方进行服务方的寻址。 某种意义上,服务发现是云原生的基础。没有灵活的服务发现,就没有灵活的部署、迁移、扩缩容的能力。服务注册中心需要提供一个专门用于存储服务地址信息的服务,实例启动的时候,将自身的名字和地址信息注册到该服务中,需要调用该服务的时候,从该服务中根据提供的名字查询对应服务的地址列表。当服务实例退出的时候,会自动删掉曾经注册的地址信息。存储服务注册信息的往往是一个能够提供高可用存储服务,因为所有的RPC调用都会通过查询该服务来获取地址。可以作为该服务的开源组件有zookeeper, etcd等,它们都依赖分布式一致性算法来保证被存储的数据的可靠性
由于在云原生场景下,几乎所有的组件都会分布式部署,拥有一定的冗余,那么如何在这些分布式部署的组件之间分配流量就成为了负载均衡的主要考虑点。实现负载均衡的组件包括Ngix、网关、mesh
集群调度:灵活的集群流量调度,方便用用集群维度对不同流量进行隔离。如下游希望隔离不同业务线上游的流量、读写请求的隔离等 机房调度:由于资源&部署等原因限制,部分业务上下游的不同机房的部署情况或服务能力可能不一样,同机房调用无法服务上游的请求,需要将负载高的机房&未部署实例的机房 的流量 调度到 负载低的机房
微服务体系是由复杂的调用链组成的,当某一个环节出现问题的时候,为了不影响上层服务的时延和用户的体验,因此不应该一直等下去。超时控制用于动态控制服务间调用的超时配置,例如:控制 A -> B(A 服务调用 B 服务)之间的调用超时。超时主要是调用发起方用来保护自身不被下游拖累。 比如,手机App用户想要发布一条评论。请求发送到数据中心的LB层,再转发到API服务层。API服务调用评论服务,尝试写入数据。但是评论服务出现异常,请求1000ms后依然没有任何回应。此时我们不能让手机App的用户永远卡死在“评论发送中”的界面上,API服务的client发现超时1000ms已经到来还没有收到回复,因此向手机App侧回应“服务异常,评论发布失败”的返回值。
服务方有可能因为处于过载状态,造成错误率的上升。这时如果请求方维持原有的请求发送频率,有可能加剧问题,并可能造成雪崩。熔断主要是被调用发起方用来保护自身不被上游打崩。 在微服务中服务间依赖非常常见,比如评论服务依赖审核服务而审核服务又依赖反垃圾服务,当评论服务调用审核服务时,审核服务又调用反垃圾服务,而这时反垃圾服务超时了,由于审核服务依赖反垃圾服务,反垃圾服务超时导致审核服务逻辑一直等待,而这个时候评论服务又在一直调用审核服务,审核服务就有可能因为堆积了大量请求而导致服务宕机。 由此可见,在整个调用链中,中间的某一个环节出现异常就会引起上游调用服务出现一系列的问题,甚至导致整个调用链的服务都宕机,这是非常可怕的。因此一个服务作为调用方调用另一个服务时,为了防止被调用服务出现问题进而导致调用服务出现问题,所以调用服务需要进行自我保护,而保护的常用手段就是熔断。
熔断器原理
熔断机制其实是参考了我们日常生活中的保险丝的保护机制,当电路超负荷运行时,保险丝会自动的断开,从而保证电路中的电器不受损害。而服务治理中的熔断机制,指的是在发起服务调用的时候,如果被调用方返回的错误率超过一定的阈值,那么后续将不会真正地发起请求,而是在调用方直接返回错误 在这种模式下,服务调用方为每一个调用服务(调用路径)维护一个状态机,在这个状态机中有三个状态:
服务治理中引入熔断机制,使得系统更加稳定和有弹性,在系统从错误中恢复的时候提供稳定性,并且减少了错误对系统性能的影响,可以快速拒绝可能导致错误的服务调用,而不需要等待真正的错误返回。
「动态过载控制」是维护服务稳定性的一种手段,由服务端承载方主动发起。通过动态衡量自身的承载能力,对超过负载的请求进行快速返回(尽可能减少开销),以保证服务可以在预期负载范围内保有预期内的服务能力。 「动态过载控制」可以分为「过载检测」、「过载处理」和「过载恢复」三部分:
ELK是一套针对日志数据做解决方案的框架,分别代表了三款产品
这里主要介绍提高mysql数据库性能的关键技术。下图是mysql的架构图
池化技术
正常来说,从数据库获取数据都需要与数据库先建立连接,其中就会经过tcp建立三次握手(客户端与mysql服务器的连接基于tcp协议)、mysql认证,在sql执行完毕后需要释放连接,又会经过mysql的关闭、tcp四次握手关闭。如果每次执行sql操作都需要经过这个流程,就会造成连接频繁创建,缺点就是响应时间慢、连接关闭的临时对象较多,造成较多内存碎片等。所以我们的解决方案是使用数据库连接池。 数据库连接池(Connection pooling)是程序启动时建立足够的数据库连接,并将这些连接组成一个连接池,由程序动态地对池中的连接进行申请、使用、释放。所以,在sql执行完毕后,我们只需要将连接返回连接池,在系统关闭前断开所有连接并释放占用的系统资源即可。
这是一种常见的软件设计思想,叫做池化技术,它的核心思想是空间换时间,期望使用预先创建好的对象来减少频繁创建对象的性能开销,同时还可以对对象进行统一的管理,降低了对象的使用的成本,类似的还有http连接池、redis连接池。当然,这也存在一些缺陷,比如,池子中的连接对象肯定需要消耗多余的内存,如果对象没有被频繁使用,就会造成内存上的浪费。
主从分离
在一般的应用内,数据库的读流量一般都比写流量高,比如,刷抖音的请求量肯定远远大于发布抖音视频的请求量,而现在读写流量都集中在一个数据库上让数据库不堪重负。因此,我们优先考虑数据库如何抵抗更高的查询请求,那么你需要把读写流量区分开,这样才方便针对读流量做独立的扩展,这就是读写分离。读写分离机制中,我们将一个数据库的数据拷贝为一份或者多份,并且写入到其它的数据库服务器中,原始的数据库我们称为主库,主要负责数据的写入,拷贝的目标数据库称为从库,主要负责支持数据查询。所以,主库主要承载写流量,从库主要承载读流量,这样当读流量涨的时候就不会影响主库的负载,从而不影响数据写入,dba只需要扩容从库即可。
关键技术-主从复制
主从复制主要依赖binlog,主库上的所有变化会以二进制形式保存在磁盘上二进制日志文件。主从复制就是将 binlog 中的数据从主库传输到从库上,一般这个过程是异步的,即主库上的操作不会等待 binlog 同步的完成。整个过程是这样的,首先从库在连接到主节点时会创建一个 IO 线程,用以请求主库更新的 binlog,并且把接收到的 binlog 信息写入一个叫做 relay log 的日志文件中,而主库也会创建一个 log dump 线程来发送 binlog 给从库;同时,从库还会创建一个 SQL 线程读取 relay log 中的内容,并且在从库中做回放,最终实现主从的一致性。这是一种比较常见的主从复制方式。当然主从复制存在一定的延迟,所以当应用对强一致性有比较高的要求的时候,就需要考虑写完以后从主库读取数据等方式解决。
分库分表
当数据量激增,分库分表是一种常见的将数据分片的方式,它的基本思想是依照某一种策略将数据尽量平均地分配到多个数据库节点或者多个表中。不同于主从复制时数据是全量地被拷贝到多个节点,分库分表后,每个节点只保存部分的数据,这样可以有效地减少单个数据库节点和单个数据表中存储的数据量,在解决了数据存储瓶颈的同时也能有效地提升数据查询的性能。同时,因为数据被分配到多个数据库节点上,那么数据的写入请求也从请求单一主库变成了请求多个数据分片节点,在一定程度上也会提升并发写入的性能。
拆分方式
垂直拆分:按照业务类型将数据库的表拆分到多个不同的数据库中,核心思想是专库专用,将业务耦合度比较高的表拆分到单独的库中。比如,在做饭的时候,将肉、蔬菜、水果放在不同的盘子里。好处是风险最小化,实现了数据层面的故障隔离。
事务将多个读写操作组合成一个逻辑单元(类似原子操作),这个逻辑单元要么成功(提交commit)要么失败(回滚rollback。 另外需要注意的是,事务的实现是在引擎层,所以需要mysql使用的引擎支持事务。mysql是一个支持多引擎的系统,但并不是所有的引擎都支持事务,比如 MyISAM引擎就不支持事务,这也是MyISAM被 InnoDB 取代的重要原因之一。
mysql有四种隔离级别,在并发操作时每种隔离级别都有一些优缺点,总体而言,隔离级别越高,事务并发性越差
下面介绍事务的ACID特性
分布式系统中远程调用也会消耗很多资源,因为网络开销会导致整体的响应时间下降,而在数据库层面,分布式系统中最耗性能的地方就是后端的数据库了。对于一般的应用来说,用户层面的写流量不会太高,所以数据库中的三个写操作( insert、update 和 delete )不太会出现性能问题(insert 一般不会有性能问题,update 和 delete 一般会有主键,所以也不会太慢),除非索引建得太多(建索引的性能损耗)且数据库里的数据太多,这三个操作才会变慢。绝大多数情况下,select是出现性能问题最大的地方。一方面,select 会有很多像 join、group、order、like 等这样丰富的语义,而这些语义是非常耗性能的;另一方面,大多数应用都是读多写少,所以加剧了慢查询的问题。所以,我们引入了缓存,这里的缓存主要介绍redis。
Redis是完全开源免费的高性能的KV分布式NoSQL数据库,内存运行也支持持久化,使用C语言编写的,遵守BSD协议。
优点如下
redis这么快的原因主要是以下三点
**redis的过期策略以及内存淘汰机制 **
redis的内存容量是有限的,比如,redis只能存1G数据,当写入量大于1G时,就会触发内存淘汰策略。redis采用的是定期删除+惰性删除策略。
为什么不用定时删除策略
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.
定期删除+惰性删除的工作模式
定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。 于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
定期删除+惰性删除存在的问题
如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效,redis的内存也会越来越高。那么就应该采用内存淘汰机制。下面介绍常见的淘汰策略。
其它常见问题
缓存穿透
概念访问一个不存在的key,缓存不起作用,请求会穿透到DB,流量大时DB会挂掉。
解决方案:
**缓存雪崩 **
大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。
解决方案
缓存和数据库数据一致性
在高并发场景和分布式微服务交互场景中,消息队列是重要的中间件,用于实现高性能、高可用、可伸缩和最终一致性架构 。它主要解决应用耦合,异步消息,流量削锋等问题。使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ
以下介绍消息队列在实际应用中常用的使用场景。异步处理,应用解耦,流量削锋和消息通讯四个场景
场景说明:用户注册后,需要发注册邮件和注册短信。传统的做法有两种 1.串行的方式;2.并行方式
假设三个业务节点每个使用50毫秒钟,不考虑网络等其他开销,则串行方式的时间是150毫秒,并行的时间可能是100毫秒。 因为CPU在单位时间内处理的请求数是一定的,假设CPU1秒内吞吐量是100次。则串行方式1秒内CPU可处理的请求量是7次(1000/150)。并行方式处理的请求量是10次(1000/100)。
如以上案例描述,传统的方式系统的性能(并发量,吞吐量,响应时间)会有瓶颈。如何解决这个问题呢? 引入消息队列,将不是必须的业务逻辑,异步处理。改造后的架构如下
按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间,也就是50毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此架构改变后,系统的吞吐量提高到每秒20 QPS。比串行提高了3倍,比并行提高了两倍。
场景说明:用户下单后,订单系统需要通知库存系统。传统的做法是,订单系统调用库存系统的接口。如下图 传统模式的缺点:
如何解决以上问题呢?引入应用消息队列后的方案,如下图:
即使在下单时库存系统不能正常使用也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦。
流量削锋也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛 应用场景:秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列。
日志处理是指将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题。架构简化如下
日志采集客户端,负责日志数据采集,定时写受写入Kafka队列
下图是是新浪kafka日志处理应用案例
消息通讯是指,消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等 点对点通讯
客户端A和客户端B使用同一队列,进行消息通讯。
聊天室通讯
客户端A,客户端B,客户端N订阅同一主题,进行消息发布和接收。实现类似聊天室效果。 以上实际是消息队列的两种消息模式,点对点或发布订阅模式。模型为示意图,供参考。
本文作者:sora
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!