码迷,mamicode.com
首页 > 其他好文 > 详细

构建高性能的读服务

时间:2021-05-24 02:00:02      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:假设   范式   xxxx   维度   mysql   href   src   after   pre   

构建高性能的读服务

1、如何使用简洁的架构实现高性能的读服务?

读服务在实现流程上,基本上是纯粹的从存储中一次或多次获取原始数据,进行简单的逻辑加工,或直接返回给用户/前端业务系统,它是无状态或者无副作用的,也就是说每一次执行都不会在存储中记录或修改数据,每一次请求都和上一次无关。

1.1、存储的选型和架构

读服务最主要依赖的中间件是存储,因此存储的性能很大程度上决定了读服务的性能。对于MySQL数据库,即使使用了分库分表,读写分离,索引优化等手段,在并发量大时,性能也很难达到200ms以内。

为了提升性能,通常会选用基于内存的,性能更高的Redis作为主存储,MySQL作为兜底来构建。

技术图片

此架构称为懒加载,在初始的时候,所有的数据都存储在数据库中,当读服务接受请求时,会先去缓存中查询数据,如果没有查询到数据,如果没有查询到数据,就会降级到数据库中查询,并将查询结构保存在Redis中。保存在Redis中的数据会设置一个过期时间,防止数据库的数据变更了,请求还一直读取缓存中的脏数据。

1.2、存在问题

  • 存在缓存穿透的风险

    如果恶意请求不断使用缓存中没有的数据发送请求,就会导致该请求每次都会降级到数据库。针对数据库中没有的数据,可以在缓存中设置一个占位符,在第二次请求处理时,读取缓存中的占位符就可以识别数据库中没有次数据,但是使用占位符虽然解决了穿透问题,但同时带来另一个问题,恶意请求不但变换请求条件,同时这些请求条件对应的数据在数据库中均没有,那么缓存中存储的表示无数据的占位符就会把整个缓存撑爆,导致有效数据被缓存清理策略清除。

  • 缓存集中过期导致雪崩

    对存储在缓存中的数据设置过期时间是为了定期获取数据库中的变更,但是设置不合理,可能会导致缓存集体过期,进而所有的读请求都会因缓存未命中,而直接请求到数据库。

    对于缓存的过期时间,可以设置进行加盐操作。做到各个缓存的过期时间不一致,同时过期缓存的数量可控。

  • 懒加载无法感知实时变更

    在缓存中设置过期时间,虽然可以让用户感知到数据的变更,但感知并不是实时的,会有一定延迟。

    如果想要做到实时看到数据的变更,可以将架构升级。

技术图片

在每次修改完数据之后,主动将数据更新至缓存,此种方案下,缓存中的数据和数据库保持一致。但是在细节上还存在一些问题。如果修改完数据库再更新缓存,异常情况下,可能出现数据库更新成功了,但是缓存更新失败情况。

数据库和缓存是两个存储,如果没有分布式事务的机制,缓存更新失败了,数据库的数据是不会回滚的。同样如果先更新缓存,在更新数据库,没有分布式事务的保障,出现缓存中存在脏数据的问题。

  • 懒加载无法摆脱的毛刺困扰

    当缓存过期时,读服务的请求都会穿透到数据库,对于穿透请求的性能和使用缓存的性能差距非常大, 时常是毫秒和秒级别的差异。

2、如何利用全量缓存打造毫秒级的读服务?

全量缓存是指将数据库中的所有数据都存储在缓存中,同时在缓存中不设置过期时间的一种实现方式。

因为所有数据都存储在缓存里,读服务在查询时不会再降级到数据库,所有的请求都完全依赖于缓存,此时因降级到数据库导致的毛刺问题就解决了。

但全量缓存并没有解决更新时的分布式事务问题,反而把问题放大了,全量缓存对数据更新要求更加严格,要求所有数据库已有的数据和实时更新的数据必须完全同步至缓存,不能有遗漏。

2.1、基于Binlog的全量缓存

Binlog是MySQL及大部分主流数据库的主从数据同步方案,主数据库会将所有的变更按一定格式写入塔本地的Binlog文件中,在主从同步时,从数据库会和主数据库建立连接,通过特定的协议串行地读取主数据库Binlog文件,并在从库进行Binlog的回放,进而完成主从复制。

现在有很多开源工具(如阿里的 Canal、MySQL_Streamer、Maxwell、Linkedin 的 Databus 等)可以模拟主从复制的协议,通过模拟协议读取主数据库的Binlog文件,从而获取主库的所有变更。对于这些变更,它们开房了各种借口供业务服务获取数据。

基于Binlog的全量缓存架构正是依赖此类中间件完成数据同步。

技术图片

2.2、Binlog全量缓存的优点

  • 降低了延迟

    缓存基本上是准实时的,数据库的主从同步保持在毫秒级别。

  • 解决了分布式事务问题

    Binlog的主从复制基于ACK响应机制,如果同步缓存失败了,被消费的Binlog不会被确认,下一次会重复消费,数据最终会写入缓存中。

2.3、Binlog存在的问题

  • 提升了系统的整体复杂度

  • 缓存的容量会成本上升,相应的资源成本也大幅上升

    优化:存储在缓存中的数据经过筛选,有业务含义且会被查询的才进行存储。

    ? 存储在缓存中的数据可以进行压缩。

3、如何做到异构数据的同步一致性?

3.1、基于Binlog的全量缓存架构问题分析

技术图片

  • 问题一:Binlog延迟低是指纯MySQL的主从同步

    基于Binlog的缓存数据同步和MySQL的主从同步在架构上是存在区别的

    1、MySQL的主从同步时纯数据同步,格式和协议完全适配,因此性能损耗极低,而使用Binlog同步时经过协议转换的,有一定的性能损耗。

    2、基于Binlog的同步比MySQL的主从同步多了两个模块,因此整体链路比较长。

    3、在实际场景中,为了保持稳定性,同步的是从库的Binlog,这也导致延迟进一步加大。

    4、Binlog是串行的,这会导致同步的吞吐量太低,进一步加大同步的延迟。

  • 问题二:如何做Binlog格式解析

    抽象来看,程序其实就是数据和逻辑的组合,所有的程序都要按照一定的业务规则对某种数据处理才能产生价值。

    Binlog的同步程序也是一样,Binlog同步转换程序处理的是Binlog数据,那Binlog的格式是什么样的?是每次变更的SQL还是其他维度的数据?这关系到同步程序的设计方案,以及对应的实现复杂度。

  • 问题三:如何保证数据不丢失或错误

    MySQL的主从逻辑和业务数据无关,正式版本发布后,修改的频率比较低。而基于Binlog实现的业务数据同步程序是易变的。如何保证同步的数据不丢失、不出错呢?

  • 问题四:如何设计缓存数据格式?

    现在主流的数据库(如Memcache、Redis等)不知提供Key-Value的数据结构,还提供了其他丰富的数据结构类型,如何利用和设计这些数据结构,来提升数据查询和写入时的性能,同时降低代码的复杂度?

3.2、Binlog如何高效消费

  • 全串行的方式进行消费

    以MySQL为例,不管是表还是SQL维度的数据,都需要将整个实例的所有数据变更写入一个Binlog文件,在消费时,对此Binlog文件使用ACK机制进行串行消费,每消费一条确认一条,然后再消费一条,以此重复。

技术图片

此模式消费存在两个问题:

  • 问题一:串行消费效率低,延迟大。

  • 单线程无法水平扩展,架构有缺陷。

  • 采用并行的方式提升吞吐量及扩展性

    Binlog的单文件及ACK机制,导致我们必须去串行消费,但实际上可以通过一些技术手段能够对Binlog文件里的不同库,不同表的数据进行并行消费,因为不同库之间的数据是不相关的,为了在Binlog原有的串行机制下完成按库的并行消费,整体架构需要进行一定的升级。

    技术图片

    上述架构里,借用了MQ进行拆分,在Binlog处仍然进行串行消费,但只是ACK数据。现在大部分的MQ中间件都支持数据并行消费,数据转换模块在消费数据时,开启并行乱序消费即可。此时虽然完成了从串行消费到并行消费的升级,提升了吞吐量和扩展性,但也带来了数据乱序的问题。

    此时我们可以依赖MQ中间件的串行通道特性进行支持,在Binlog订阅及转发Binlog数据前,按照业务规则判断转发的Binlog数据是否在并发后仍需要串行消费。在同步模块进行同步时,MQ中间件里的串行通道的数据均会串行执行,而多个串行通道间则可以并发执行。

    技术图片

3.3、如何发送Binlog

MySQL的Binlog氛围三种数据格式:statement,row及mixed

create table demo_table{
  id bigint not null auto_increment comment ‘主键‘,
  message varchar(100) not null comment ‘消息‘,
  status tinyint not null comment ‘状态‘,
  created datetime not null ‘创建时间‘,
  modified datetime not null ‘修改时间‘,
  primary key (‘id‘) using btree
}
  • statement格式

    statement格式是把每次执行的SQL语句记录到Binlog文件中,在主从复制时,基于Binlog里的SQL语句进行回放来完成主从复制。比如执行如下SQL成功后:

    update demo_table set status=1 where id = 1;
    

    Binlog中记录的便是上述的这条具体SQL,采用SQL格式的Binlog好处是内容少,传输速度快,但在基于Binlog进行数据同步时,需要解析SQL获取变更的字段,存在一定的开发成本。

  • row格式

    row格式的Binlog会把当次执行的SQL命中的那条数据行变更前和变更后的内容都记录到Binlog文件中,以上述statement格式里的SQL作为示例,该SQL在row格式下执行后会产生如下数据:

    {
      "before":{
        "id":1,
        "message":"文本",
        "status":0,
        "created":"xxxx-xx-xx",
        "modified":"xxxx-xx-xx"
      },
      "after":{
        "id":1,
        "message":"文本",
        "status":1,
        "created":"xxxx-xx-xx",
        "modified":"xxxx-xx-xx"
      },
      "change_fields":["status"]
    }
    

    上述案例记录的 Binlog 数据非常全面,包含了 demo_table 中所有字段对应的变更和未变更的数据,同时标记了具体哪些字段发生了变更。在数据同步时,可以完全以它为准。基于上述格式的数据同步的实现代码会非常简单,但缺点是,上述格式产生的数据量较大。

  • mixed模式

    mixed是上述两种模式的动态结合,采用mixed模式的Binlog会根据每一条执行的SQL动态判断是记录为row格式还是statement格式。比如一些DDL语句,如新增字段的SQL,就没必要记录为row,记录为statement即可,因为它本省并没有涉及数据的变更。

3.4、数据对比发现错误

数据对比以数据据的数据为基准,定期轮训对比缓存和数据库的数据,如果发现不一致后,可以增加延迟重试,再次对比,如果多次对比不一致后,可以增加报警并保留当时数据,之后以数据库的数据为准刷新缓存。延迟重试是为了防止因同步的视察,出现短暂的数据不一致但最终数据一致的情况。其次保留出现现场的数据是为了排查定位问题。

技术图片

3.5、缓存数据结构设计及写入

数据库表是按技术的范式来设计的,会将数据按一对一或一对多拆分成多张表,而缓存中则是面向业务设计的,会尽可能地将业务上一次查询的数据存储为缓存中的一个 Value 值。

比如订单至少要包含订单基本信息和用户的购买商品列表。在数据库中会设计订单基本信息表和商品表。而在缓存中,会直接将订单基本信息和商品信息存储为一个 Value 值,方便直接满足用户查询订单详情的需求,减少和 Redis 的交互次数。

这种在数据库中多张表存储,而在缓存中只用 K-V 结构进行冗余存储的数据结构,需要我们在数据同步的时候进行并发控制,防止因为多张表的变更导致并发写入,从而产生数据错乱。

此时我们可以采用Redis的Hash结构来进行局部更新,参考数据库的多张表设计,缓存中也可以进行多部分存储。在Redis中,可以采用Hash结构,对于一个订单下不同表的数据,在Redis中存储至各个field下即可,同时Redis支持对单个field的局部更新。

4、如何应对热点数据的查询

4.1、为什么扛不住相同用户百万的流量

当百万的QPS属于不同用户时,因缓存是集群化的,所有到达业务后台的请求会根据一定的路由规则,分散到请求缓存集群中的某一个节点。假设一个节点最大能之城10万QPS,我们只需要在集群中部署10台节点即可支持百万流量。

但当百万QPS都属于同一个用户时,即使缓存是集群花的,同一个用户的请求都会被路由至集群中某一个节点。

4.2、主从复制进行垂直扩容

虽然单机的机器配置和程序的性能是有上限的,但我们可以利用节点间的主从复制功能来进行节点间的扩容,主从复制开启后,一个节点可以挂一至多个从。

技术图片

在查询时,将应用内的缓存客户端开启主从随机读,此时包含一个从的分片并发能力可以提升至原来的一倍,随着从节点的增加,但分片的并发心更会不断翻倍。这对于所有请求只会命中某一个固定单分片的热点查询能够很好地应对。

但是此方案浪费资源。

主从复制除了有应对热点的功能,另外一个重要作用是为了高可用。当集群中某一个主节点发生故障后,集群高可用模块会自动对该节点进行故障迁移,从该节点所属分片里选举一个从节点为主节点。为了高可用模块在故障转移时的逻辑能够简单清晰并做到统一,会将集群的从节点数量设置为相同数量。

4.3、利用应用内的前置缓存

热点查询时对相同的数据进行不断重复查询的一种场景,热点是次数多,但需要存储的数据少,因为数据都是相同的,针对此类业务特性,我们可以将热点数据前置缓存在应用程序内来应对热点查询。

技术图片

应用 的缓存存储的均是热点数据,当应用扩容后,热点缓存的数量也随之增加,在采用了前置缓存后,在面对热查询时只需要扩容应用即可。因为所有应用内均存储了所有的热点数据,且前端负载均衡器(如Nginx)会将所有请求平均分发到各应用中去。

需要关注的问题

  • 应用内缓存需要设置上限

    应用所属宿主机的内存是有限的,且其内存还要支持业务应用的使用。所以在使用应用内的前置缓存时,必须设置容量的上限且设置容量满时逐出策略。此外前置缓存也需要设置过期时间,毕竟太久无访问的缓存也肯定是非热点数据。

  • 根据业务对待延迟问题

    如果业务上可以容忍一定时间的延迟,可以在缓存数据上设置一个刷新时间即可。

    如果想要实时感知变化,可以才用Binlog方式,在变更时主动刷新。但前置缓存的主动感知不能在前置缓存的应用里实现,因为应用代码也运行在此机器上,通过MQ感知变更会消耗非常多的CPU和内存资源。另外,前置缓存里数据很少,很多变更消息都会因不在前置缓存中而被忽略,为了实现前置缓存的更新,可以将前置缓存的数据异构一份出来用作判断。

  • 把控好瞬时的逃逸流量

    应用初始化时,前置缓存是空的,假设在初始化时,瞬间出现热点查询,所有的热点请求都会逃逸到后端缓存里,可能这个瞬间热点就会把后端缓存打挂。

    其次,如果前置缓存采用定期过期,在过期时若将数据清理掉,那么所有请求都会逃逸值后端加载最新的缓存,也有可能把后端缓存打挂。

    对于这两种情况,可以对逃逸流量进行前置等待或使用历史数据的方案,不管是初始化还是数据过期,在从后端加载数据时,只允许一个请求逃逸。这样最大的逃逸流量为部署的应用总数,量级可控。

    对于数据初始化为空时,其他费逃逸的请求可以等待前置缓存的数据并设置一个超时时间,对于数据过期需要更新时,并不主动清理数据,其他非逃逸请求使用历史脏数据,而逃逸的哪一个请求负责把数据取回并刷新前置缓存。

  • 如何发现热点缓存并前置

    • 被动发现

      被动发现是借助前置缓存有容量上限实现的,读服务接受到所有请求都会默认从前置缓存中取数据,如果不存在,则从缓存服务器进行加载,因为前置缓存的容量淘汰策略是LRU,如果数据是热点,它的访问次数一定非常高,因此它一定在前置缓存中。借助前置缓存的容量上限和淘汰策略,即可实现热点发现。

      所有的请求都会优先从前置缓存获取数据,并在未查询到数据时加载服务端数据到本地的前置缓存里,此方式也会把非热点数据存储至前置缓存里,导致非热点数据产生非必要的延迟性。

    • 主动发现

      主动发现则需要借助一些外部计数工具来实现热点的发现,在一个集中的位置对于请求进行计数,并根据配置的阈值判断某请求是否会命中数据,对于判定为热点的数据,主动推送至应用内的前置缓存即可。

      读服务接受到请求后仍然会默认的从前置缓存获取数据,如获取到直接返回。如为获取到,会穿透去查询后端缓存的数据并直接返回,但穿透获取到的数据并不会直接写入本地的前置缓存,数据是否为热点切是否要写入前置缓存,均由计数工具来决定。

注:本文是学习拉勾教育《23讲搞定后台架构实战》整理所得,具体访问:https://t8.lagounews.com/NR64RlRocU042

构建高性能的读服务

标签:假设   范式   xxxx   维度   mysql   href   src   after   pre   

原文地址:https://www.cnblogs.com/huiyichanmian/p/14745786.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!