编辑
2023-07-16
服务端
0
请注意,本文编写于 459 天前,最后修改于 70 天前,其中某些信息可能已经过时。

目录

前言
挑战
要求
演进中的架构
单体结构
应用服务和数据服务分离
分布式架构
性能优化
微服务化
架构间的区别
SOA
微服务
差别
核心模块
基本理论
CAP理论
BASE定理
服务治理
路由控制
服务发现
负载均衡
流量调度
稳定性
超时
熔断
过载
监控与日志
日志
数据存储
数据库
事务
缓存
Redis
localCache WIP
消息队列
消息队列应用场景
异步处理
应用解耦
流量削锋
日志处理
消息通讯
编程范式和设计模式
云技术

前言

分布式系统涵盖的知识面非常广,all in one本篇我会介绍下我所了解的知识点,预计完稿后的知识密度较高,本篇只点到为止,每个点都有很多值得深挖和学习的内容,后续逐渐会有相关系列文章来具体阐述。

挑战

1990年,浏览器诞生。1994年底,万维网联盟(World Wide Web Consortium,简称3W)成立,这标志着万维网的正式诞生。此时的网页以HTML为主,是纯静态的网页,信息流只能通过服务器到客户端单向流通,由此世界进入Web1.0时代,至今三十年过去了,互联网已经进入web3.0。随之而来遇到的挑战非常多,包括但不局限于下面几点

  • 高并发,大流量:用户常用的应用往往集中在头部的那几个,参考Google日均PV35亿,日IP访问数3亿。另一方面,特殊时期节点,用户对平台访问和使用集中度非常高,导致的瞬时qps也非常高。典型的如12306节假日抢票,淘宝双11,春节红包会场等等,流量的波峰波谷明显,非常考验整体的系统架构设计,同时对系统的可升缩性也提出了要求。
  • 全球化,用户分布广:全球化的公司比如谷歌,meta,国内典型的如字节跳动等在全球化发展过程中遇到非常多的挑战,包括网络、语言、文化等,技术上也会带来诸多挑战,比如,如何让不同区域用户的使用体验趋同,不同机房之间的流量调度和数据隔离问题,用户隐私数据的合规问题等等。
  • 大数据:需要存储、管理海量数据,需要使用大量服务器。Facebook每周上传的照片数量接近10亿,百度收录的网页数目有数百亿,Google有近百万台服务器为全球用户提供服务。
  • 需求迭代快:和传统软件的版本发布频率不同,互联网产品为快速适应市场,满足用户需求,其产品发布频率极高。一般大型网站的产品每周都有新版本发布上线,中小型网站的发布更频繁,有时候一天会发布多次。

要求

上述一系列挑战对系统架构设计产生了非常要的要求,比如三高

  • 高并发:在尽量不降低用户体验的情况下,系统能够同时并行处理很高的请求量,常用的一些指标有响应时间(RT),吞吐量(Throughput),每秒查询率(QPS)。一方面,我们可以通过堆硬件的方式,比如服务器数量,CPU核数等等;另一方面,我们也需要努力提高单机的性能,在代码的常规设计中,尽量使用缓存来减少IO次数,合理使用异步调用,使用无锁数据结构,控制GC频率,对于一些边界问题或者常规测试中无法发现的问题,我们可以通过压测的方式找出性能瓶颈。
  • 高可用:可以理解为系统正常提供服务的时间,云服务提供商都会用服务SLA指标去定义产品的稳定性。这是个基础指标,在公司内部,我们可以经常看到几分钟的事故就能造成上千万的损失(比如广告、电商),严重点的问题会非常影响用户体验,从而影响产品的声誉和品牌形象,前阵子阿里云宕机事件对阿里的影响非常大,甚至集团CEO张勇也亲自下场跟进,相关高管被问责。

不同服务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分钟

  • 高性能:主要关注系统处理速度,非常快,所占内存少、CPU 占用率低。高性能的指标经常和高并发的指标紧密相关,想要提高性能,那么就要提高系统高并发能力,两者互相捆绑在一起。应用性能优化的时候,对于计算密集型和 IO 密集型还是有很大差别,需要分开来考虑。还有可以增加服务器的数量、内存、IO 等参数提升系统的并发能力和性能,但不要浪费资源,要考虑硬件的使用率最高才能发挥到极致。怎么样提高性能呢?避免因为 IO 阻塞让 CPU 闲置,导致 CPU 的浪费。避免多线程间增加锁来保证同步,导致并行系统串行化。免创建、销毁、维护太多进程、线程,导致操作系统浪费资源在调度上。

演进中的架构

上述挑战并不是一天发生的,而是随着互联网的日益发展逐渐诞生,相应地我们只需要渐进式地对系统进行重构和优化。在我看来,好的产品和架构设计都是不断随着业务变动,不同发展阶段都有自身特点,切忌过度设计。

单体结构

最早的时候,平台的用户访问量低,只需要单台或者少数几台服务器即可。将文件、数据库与应用程序一起部署在单物理机上。

image.png

应用服务和数据服务分离

随着网站业务的发展,一台服务器逐渐不能满足需求:越来越多的用户访问导致性能越来越差,越来越多的数据导致存储空间不足。这时就需要将应用和数据分离。应用和数据分离后整个网站使用3台服务器:应用服务器、文件服务器和数据库服务器。这3台服务器对硬件资源的要求各不相同:

  • 应用服务器需要处理大量的业务逻辑,因此需要更快更强大的CPU;
  • 数据库服务器需要快速磁盘检索和数据缓存,因此需要更快的磁盘和更大的内存;
  • 文件服务器需要存储大量用户上传的文件,因此需要更大的硬盘。

image.png

分布式架构

随着系统规模越来越大,我们需要考虑分布式系统的架构设计。

  • 开启缓存:流量带来最直接的影响是用户数据的读写压力,这成了系统瓶颈,于是新增了缓存,这包括本地缓存(localcache)和缓存服务,比如redis等。当然为了更好得提高性能和扩展性,我们也把缓存单独部署在了缓存服务器(要求内存大),方便伸缩和灾备。
  • 数据库的读写分离:网站在使用缓存后,使对大部分数据读操作访问都可以不通过数据库就能完成,但是仍有一部分读操作(缓存访问不命中、缓存过期)和全部的写操作都需要访问数据库,在网站的用户达到一定规模后,数据库因为负载压力过高而成为网站的瓶颈。目前大部分的主流数据库都提供主从热备功能,通过配置两台数据库主从关系,可以将一台数据库服务器的数据更新同步到另一台服务器上。网站利用数据库的这一功能,实现数据库读写分离,从而改善数据库负载压力。
  • 集群化:使用集群部署是网站解决高并发问题的常用手段。对于应用服务器,我们通过横向增加服务器数量的方式来减轻每台服务器的负载,通过扩容缩容的形式非常方便得抵御流量压力,实现了系统的可伸缩性,另一方面通过集群也能隔离不同上游调用间的互相影响。

image.png

性能优化

随着网站业务不断发展,用户规模越来越大,由于中国复杂的网络环境,不同地区的用户访问网站时,速度差别也极大。有研究表明,网站访问延迟和用户流失率正相关,网站访问越慢,用户越容易失去耐心而离开。为了提供更好的用户体验,留住用户,网站需要加速网站访问速度。主要手段有使用 CDN 和反向代理。如下图所示:

image.png CDN 和反向代理的基本原理都是缓存。 CDN 部署在网络提供商的机房,使用户在请求网站服务时,可以从距离自己最近的网络提供商机房获取数据 反向代理则部署在网站的中心机房,当用户请求到达中心机房后,首先访问的服务器是反向代理服务器,如果反向代理服务器中缓存着用户请求的资源,就将其直接返回给用户 使用 CDN 和反向代理的目的都是尽早返回数据给用户,一方面加快用户访问速度,另一方面也减轻后端服务器的负载压力。

微服务化

大型网站为了应对日益复杂的业务场景,通过使用分而治之的手段将整个网站业务分成不同的产品线。如大型购物交易网站都会将首页、商铺、订单、买家、卖家等拆分成不同的产品线,分归不同的业务团队负责。

具体到技术上,也会根据产品线划分,将一个网站拆分成许多不同的应用,每个应用独立部署。应用之间可以通过一个超链接建立关系(在首页上的导航链接每个都指向不同的应用地址),也可以通过消息队列进行数据分发,当然最多的还是通过访问同一个数据存储系统来构成一个关联的完整系统,如下图所示: 随着业务拆分越来越小,存储系统越来越庞大,应用系统的整体复杂度呈指数级增加,部署维护越来越困难。由于所有应用要和所有数据库系统连接,在数万台服务器规模的网站中,这些连接的数目是服务器规模的平方,导致数据库连接资源不足,拒绝服务。

既然每一个应用系统都需要执行许多相同的业务操作,比如用户管理、商品管理等,那么可以将这些共用的业务提取出来,独立部署。由这些可复用的业务连接数据库,提供共用业务服务,而应用系统只需要管理用户界面,通过分布式服务调用共用业务服务完成具体业务操作。如下图所示: image.png

架构间的区别

image.png

SOA

SOA(全称Service Oriented Architecture),中文意思为 “面向服务的架构”,你可以将它理解为一个架构模型或者一种设计方法,而并不是服务解决方案。其中包含多个服务, 服务之间通过相互依赖或者通过通信机制,来完成相互通信的,最终提供一系列的功能。一个服务通常以独立的形式存在与操作系统进程中。各个服务之间通过网络调用 。跟SOA相提并论的还有一个ESB(企业服务总线),简单来说ESB就是一根管道,用来连接各个服务节点。为了集成不同系统,不同协议的服务,ESB可以简单理解为:它做了消息的转化解释和路由工作,让不同的服务互联互通;我们将各个应用之间彼此的通信全部去掉,在中间引入一个ESB企业总线,各个服务之间,只需要和ESB进行通信,这个时候,各个应用之间的交互就会变得更加的清晰,业务架构/逻辑等,也会变得很清楚。原本杂乱没有规划的系统,梳理成了一个有规划可治理的系统,在这个过程中,最大的变化,就是引入了ESB企业总线。

好处

  • 系统集成:站在系统的角度,解决企业系统间的通信问题,把原先散乱、无规划的系统间的网状结构,梳理成规整、可治理的系统间星形结构,这一步往往需要引入一些产品,比如ESB、以及技术规范、服务管理规范;这一步解决的核心问题是有序
  • 系统的服务化:站在功能的角度,把业务逻辑抽象成可复用、可组装的服务,通过服务的编排实现业务的快速再生。目的:把原先固有的业务功能转变为通用的业务服务,实现业务逻辑的快速复用;这一步解决的核心问题是复用
  • 业务的服务化:站在企业的角度,把企业职能抽象成可复用、可组装的服务;把原先职能化的企业架构转变为服务化的企业架构,进一步提升企业的对外服务能力;前面两步都是从技术层面来解决系统调用、系统功能复用的问题。第三步,则是以业务驱动把一个 业务单元封装成一项服务。这一步解决的核心问题是高效

微服务

微服务架构其实和SOA架构类似,微服务是在SOA上做的升华。微服务架构重点强调的一个是"业务需要彻底的组件化和服务化",原有的单个业务系统会拆分为多个可以独立开发、设计、运行的小应用。这样的小应用和其他各个应用之间,相互去协作通信,来完成一个交互和集成,这就是微服务架构。

组件化:组件表示一个可以独立更换和升级的单元,就以PC机为例,PC中的 CPU、内存、显卡、硬盘一样,独立更换升级而不影响其他单元。如果我们把PC作为组件以服务的方式构建,那么这台PC只需要维护主板和一些必要的外部设备。CPU、内存、硬盘都是以组件方式提供服务,PC需要调用CPU做计算处理,只需要知道CPU这个组件的地址即可。

特征

  • 通过服务实现组件化,去中心化
  • 按业务能力来划分服务和开发团队
  • 基础设施自动化(devops、自动化部署)

差别

  • 微服务去中心化,去掉ESB企业总线。微服务不再强调传统SOA架构里面比较重的ESB企业服务总线,同时SOA的思想进入到单个业务系统内部实现真正的组件化
  • Docker容器技术的出现,为微服务提供了更便利的条件,比如更小的部署单元,每个服务可以通过类似Node或者Spring Boot等技术跑在自己的进程中。
  • SOA注重的是系统集成方面,而微服务关注的是完全分离

核心模块

分布式架构带来一些的诸多问题和技术难点,比如分布式事务问题,数据强弱一致性问题,负载均衡。 正如我们前面所说的,构建分布式系统的目的是增加系统容量,提高系统的可用性,一方面,通过集群技术把大规模并发请求的负载分散到不同的机器上,另一方面,提高服务的可用性,把故障隔离起来阻止多米诺骨牌效应(雪崩效应)。如果流量过大,需要对业务降级,以保护关键业务流转。说白了就是干两件事。一是提高整体架构的吞吐量,服务更多的并发和流量,二是为了提高系统的稳定性,让系统的可用性更高。

提高系统性能 image.png 提高系统稳定性 image.png

基本理论

CAP理论

分布式系统中存在CAP理论,即Consistency(强一致性)、Availability(可用性)、Partition tolerance(分区容错性 。CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求。它存在几种组合

  • CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
  • CP - 满足一致性,分区容忍必的系统,通常性能不是特别高。
  • AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。 CAP的证明很简单,假设两个节点集{G1, G2},由于网络分片导致G1和G2之间所有的通讯都断开了,如果不满足P,则整个网络不可用,如果在G1中写,在G2中读刚写的数据, G2中返回的值不可能G1中的写值。由于A的要求,G2一定要返回这次读请求,由于P的存在,导致C一定是不可满足的。

CAP的证明基于异步网络,异步网络也是反映了真实网络中情况的模型。真实的网络系统中,节点之间不可能保持同步,即便是时钟也不可能保持同步,所有的节点依靠获得的消息来进行本地计算和通讯。这个概念其实是相当强的,意味着任何超时判断也是不可能的,因为没有共同的时间标准。之后我们会扩展CAP的证明到弱一点的异步网络中,这个网络中时钟不完全一致,但是时钟运行的步调是一致的,这种系统是允许节点做超时判断的。 CAP理论应用在分布式存储系统中,最多只能实现上面的两点。由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们必须需要实现的。所以我们只能在一致性和可用性之间进行权衡,没有任何分布式系统能同时保证这三点。一致性和可用性之间取一个平衡。多余大多数web应用,其实并不需要强一致性。因此牺牲C换取P,这是目前分布式数据库产品的方向。

BASE定理

BASE就是为了解决关系数据库强一致性引起的问题而引起的可用性降低而提出的解决方案。 有三个特性

  • 基本可用(Basically Available)
  • 软状态(Soft state)
  • 最终一致(Eventually consistent)

它的思想是通过让系统放松对某一时刻数据一致性的要求来换取系统整体伸缩性和性能上改观。为什么这么说呢,缘由就在于大型系统往往由于地域分布和极高性能的要求,不可能采用分布式事务来完成这些指标,要想获得这些指标,我们必须采用另外一种方式来完成,这里BASE就是解决这个问题的办法

服务治理

路由控制

服务发现

在微服务架构中,整个系统会按职责能力划分为多个服务,通过服务之间协作来实现业务目标。这样在我们的代码中免不了要进行服务间的远程调用,服务的消费方要调用服务的生产方,为了完成一次请求,消费方需要知道服务生产方的网络位置(IP地址和端口号)。云原生的环境下,存在海量的独立部署服务,服务的生命周期缩短,频繁的更新发布,服务部署资源的可伸缩性使得实例经常变更,因此需要一个注册中心提供名字服务,以便请求方进行服务方的寻址。 某种意义上,服务发现是云原生的基础。没有灵活的服务发现,就没有灵活的部署、迁移、扩缩容的能力。服务注册中心需要提供一个专门用于存储服务地址信息的服务,实例启动的时候,将自身的名字和地址信息注册到该服务中,需要调用该服务的时候,从该服务中根据提供的名字查询对应服务的地址列表。当服务实例退出的时候,会自动删掉曾经注册的地址信息。存储服务注册信息的往往是一个能够提供高可用存储服务,因为所有的RPC调用都会通过查询该服务来获取地址。可以作为该服务的开源组件有zookeeper, etcd等,它们都依赖分布式一致性算法来保证被存储的数据的可靠性

负载均衡

由于在云原生场景下,几乎所有的组件都会分布式部署,拥有一定的冗余,那么如何在这些分布式部署的组件之间分配流量就成为了负载均衡的主要考虑点。实现负载均衡的组件包括Ngix、网关、mesh

流量调度

集群调度:灵活的集群流量调度,方便用用集群维度对不同流量进行隔离。如下游希望隔离不同业务线上游的流量、读写请求的隔离等 机房调度:由于资源&部署等原因限制,部分业务上下游的不同机房的部署情况或服务能力可能不一样,同机房调用无法服务上游的请求,需要将负载高的机房&未部署实例的机房 的流量 调度到 负载低的机房

稳定性

超时

微服务体系是由复杂的调用链组成的,当某一个环节出现问题的时候,为了不影响上层服务的时延和用户的体验,因此不应该一直等下去。超时控制用于动态控制服务间调用的超时配置,例如:控制 A -> B(A 服务调用 B 服务)之间的调用超时。超时主要是调用发起方用来保护自身不被下游拖累。 比如,手机App用户想要发布一条评论。请求发送到数据中心的LB层,再转发到API服务层。API服务调用评论服务,尝试写入数据。但是评论服务出现异常,请求1000ms后依然没有任何回应。此时我们不能让手机App的用户永远卡死在“评论发送中”的界面上,API服务的client发现超时1000ms已经到来还没有收到回复,因此向手机App侧回应“服务异常,评论发布失败”的返回值。

熔断

服务方有可能因为处于过载状态,造成错误率的上升。这时如果请求方维持原有的请求发送频率,有可能加剧问题,并可能造成雪崩。熔断主要是被调用发起方用来保护自身不被上游打崩。 在微服务中服务间依赖非常常见,比如评论服务依赖审核服务而审核服务又依赖反垃圾服务,当评论服务调用审核服务时,审核服务又调用反垃圾服务,而这时反垃圾服务超时了,由于审核服务依赖反垃圾服务,反垃圾服务超时导致审核服务逻辑一直等待,而这个时候评论服务又在一直调用审核服务,审核服务就有可能因为堆积了大量请求而导致服务宕机。 image.png 由此可见,在整个调用链中,中间的某一个环节出现异常就会引起上游调用服务出现一系列的问题,甚至导致整个调用链的服务都宕机,这是非常可怕的。因此一个服务作为调用方调用另一个服务时,为了防止被调用服务出现问题进而导致调用服务出现问题,所以调用服务需要进行自我保护,而保护的常用手段就是熔断。

熔断器原理

熔断机制其实是参考了我们日常生活中的保险丝的保护机制,当电路超负荷运行时,保险丝会自动的断开,从而保证电路中的电器不受损害。而服务治理中的熔断机制,指的是在发起服务调用的时候,如果被调用方返回的错误率超过一定的阈值,那么后续将不会真正地发起请求,而是在调用方直接返回错误 在这种模式下,服务调用方为每一个调用服务(调用路径)维护一个状态机,在这个状态机中有三个状态:

  • 关闭(Closed):在这种状态下,我们需要一个计数器来记录调用失败的次数和总的请求次数,如果在某个时间窗口内,失败的失败率达到预设的阈值,则切换到断开状态,此时开启一个超时时间,当到达该时间则切换到半关闭状态,该超时时间是给了系统一次机会来修正导致调用失败的错误,以回到正常的工作状态。在关闭状态下,调用错误是基于时间的,在特定的时间间隔内会重置,这能够防止偶然错误导致熔断器进入断开状态
  • 打开(Open):在该状态下,发起请求时会立即返回错误,一般会启动一个超时计时器,当计时器超时后,状态切换到半打开状态,也可以设置一个定时器,定期的探测服务是否恢复
  • 半打开(Half-Open):在该状态下,允许应用程序一定数量的请求发往被调用服务,如果这些调用正常,那么可以认为被调用服务已经恢复正常,此时熔断器切换到关闭状态,同时需要重置计数。如果这部分仍有调用失败的情况,则认为被调用方仍然没有恢复,熔断器会切换到打开状态,然后重置计数器,半打开状态能够有效防止正在恢复中的服务被突然大量请求再次打垮

image.png 服务治理中引入熔断机制,使得系统更加稳定和有弹性,在系统从错误中恢复的时候提供稳定性,并且减少了错误对系统性能的影响,可以快速拒绝可能导致错误的服务调用,而不需要等待真正的错误返回。

过载

  • 保证系统不被过量请求拖垮
  • 在保证系统稳定的前提下,尽可能提供更高的吞吐量

「动态过载控制」是维护服务稳定性的一种手段,由服务端承载方主动发起。通过动态衡量自身的承载能力,对超过负载的请求进行快速返回(尽可能减少开销),以保证服务可以在预期负载范围内保有预期内的服务能力。 「动态过载控制」可以分为「过载检测」、「过载处理」和「过载恢复」三部分:

  • 过载检测:判断服务现在状态过载与否的过程。
  • 过载处理:服务过载后的处理方案。
  • 过载恢复:服务从过载状态回归常态的处理方案。

监控与日志

日志

ELK是一套针对日志数据做解决方案的框架,分别代表了三款产品

  • E: ElasticSearch(ES),负责日志的存储和检索
  • L:Logstash,负责日志的收集,过滤和格式化
  • K:Kibana,负责日志的展示统计和数据可视化

数据存储

数据库

这里主要介绍提高mysql数据库性能的关键技术。下图是mysql的架构图

image.png

池化技术

正常来说,从数据库获取数据都需要与数据库先建立连接,其中就会经过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特性

  • A (Atomicity) 原子性 :一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态。原子性很容易理解,也就是说事务里的所有操作要么全部做完,要么都不做,事务成功的条件是事务里的所有操作都成功,只要有一个操作失败,整个事务就失败,需要回滚。比如,银行转账操作,从A账户转100元至B账户,分为两个步骤:从A账户取100元,存入100元至B账户。这两步要么一起完成,要么一起不完成,如果只完成第一步,第二步失败,钱会莫名其妙少了100元。
  • C (Consistency) 一致性:在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。这表示写入的数据必须完全符合所有的预设约束、触发器、级联回滚等。
  • I (Isolation) 独立性 :数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。比如,现有有个交易是从A账户转100元至B账户,在这个交易还未完成的情况下,如果此时B查询自己的账户,是看不到新增加的100元
  • D (Durability) 持久性 :事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。在分布式环境中,持久性意味着数据已成功复制到一些节点,为了提供持久性保证,数据库必须等到这些写入或复制完成后,才能报告事务成功提交。比如,交易完成后B账户上的100元,永久保留在了B账户上。

缓存

分布式系统中远程调用也会消耗很多资源,因为网络开销会导致整体的响应时间下降,而在数据库层面,分布式系统中最耗性能的地方就是后端的数据库了。对于一般的应用来说,用户层面的写流量不会太高,所以数据库中的三个写操作( insert、update 和 delete )不太会出现性能问题(insert 一般不会有性能问题,update 和 delete 一般会有主键,所以也不会太慢),除非索引建得太多(建索引的性能损耗)且数据库里的数据太多,这三个操作才会变慢。绝大多数情况下,select是出现性能问题最大的地方。一方面,select 会有很多像 join、group、order、like 等这样丰富的语义,而这些语义是非常耗性能的;另一方面,大多数应用都是读多写少,所以加剧了慢查询的问题。所以,我们引入了缓存,这里的缓存主要介绍redis。

Redis

Redis是完全开源免费的高性能的KV分布式NoSQL数据库,内存运行也支持持久化,使用C语言编写的,遵守BSD协议。

优点如下

  • Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用
  • Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储
  • Redis支持数据的备份,即master-slave模式的数据备份

redis这么快的原因主要是以下三点

  • 纯内存操作
  • 单线程操作,避免了频繁的上下文切换
  • 采用了非阻塞I/O多路复用机制

**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的内存也会越来越高。那么就应该采用内存淘汰机制。下面介绍常见的淘汰策略。

  • noeviction:不删除key。当内存不足以新写入数据时,新写入操作会报错。不推荐使用
  • allkeys-lru:lru算法淘汰(页面置换算法,最近最少使用淘汰)。当内存不足以容纳新写入数据时,移除最近最少使用的key。推荐使用
  • volatile-lru:lru算法淘汰(页面置换算法,最近最少使用淘汰)。当内存不足以容纳新写入数据时,在设置了过期时间的key中移除最近最少使用的key。不推荐
  • allkeys-lfu:lfu算法淘汰(页面置换算法,最近最不经常使用淘汰)。当内存不足以容纳新写入数据时,移除最近使用频率最低的key。推荐使用
  • volatile-lfu:lfu算法淘汰(页面置换算法,最近最不经常使用淘汰)。当内存不足以容纳新写入数据时,在设置了过期时间的key中移除最近使用频率最低的key。不推荐
  • allkeys-random:随机淘汰。当内存不足以容纳新写入数据时,随机移除某个key。不推荐
  • volatile-random:随机淘汰。当内存不足以容纳新写入数据时,在设置了过期时间的key中随机移除某个key。不推荐
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的key中有更早过期时间的key优先移除。不推荐

其它常见问题

缓存穿透

概念访问一个不存在的key,缓存不起作用,请求会穿透到DB,流量大时DB会挂掉。

解决方案

  • 采用布隆过滤器,使用一个足够大的bitmap,用于存储可能访问的key,不存在的key直接被过滤
  • 访问key未在DB查询到值,也将空值写进缓存,但可以设置较短过期时间

**缓存雪崩 **

大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。

解决方案

  • 可以给缓存设置过期时间时加上一个随机值时间,使得每个key的过期时间分布开来,不会集中在同一时刻失效;
  • 采用限流算法,限制流量;
  • 采用分布式锁,加锁访问。

缓存和数据库数据一致性

  • Cache Aside:旁路缓存。应用程序直接与缓存和数据库交互,由调用方负责把数据加载入缓存。

image.png

  • Read/Write Through:读/写穿透。应用程序不需要关心从哪里读取数据或者写入数据到哪里,只需要与缓存交互。如下图,虚线框内的操作不再由应用程序来处理,而是由缓存自己来处理,对应用透明。

image.png

  • Write Behind Caching:回写模式。应用程序会把数据立即写入数据库中,而Write-Behind会在一段时间之后(或是被其他方式触发)把数据一起写入数据库

localCache WIP

消息队列

在高并发场景和分布式微服务交互场景中,消息队列是重要的中间件,用于实现高性能、高可用、可伸缩和最终一致性架构 。它主要解决应用耦合,异步消息,流量削锋等问题。使用较多的消息队列有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消息队列,负责日志数据的接收,存储和转发
  • 日志处理应用:订阅并消费kafka队列中的日志数据

下图是是新浪kafka日志处理应用案例

  • Kafka:接收用户日志的消息队列
  • Logstash:做日志解析,统一成JSON输出给Elasticsearch
  • Elasticsearch:实时日志分析服务的核心技术,一个schemaless,实时的数据存储服务,通过index组织数据,兼具强大的搜索和统计功能
  • Kibana:基于Elasticsearch的数据可视化组件,超强的数据可视化能力是众多公司选择ELK stack的重要原因

消息通讯

消息通讯是指,消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等 点对点通讯

客户端A和客户端B使用同一队列,进行消息通讯。

聊天室通讯

客户端A,客户端B,客户端N订阅同一主题,进行消息发布和接收。实现类似聊天室效果。 以上实际是消息队列的两种消息模式,点对点或发布订阅模式。模型为示意图,供参考。

编程范式和设计模式

云技术

  • 容器化技术
  • 边缘计算
  • 低代码
  • laas、saas、paas
  • serverless
  • devops
  • AI

本文作者:sora

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!