大数据存储组件TiDB原理+实战篇

这篇具有很好参考价值的文章主要介绍了大数据存储组件TiDB原理+实战篇。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1.TiDB引入

1.1.数据库技术发展简史

​ 数据库技术产生于20世纪60年代末70年代初,其主要主要研究如何存储,使用和管理数据。随着计算机硬件和软件的发展,数据库技术也不断地发展。数据库技术在理论研究和系统开发上都取得了辉煌的成就。

  • 从数据管理的角度看,数据库技术到目前共经历了如下三个阶段:
    • 人工管理阶段-数据量小独立,用户直接管理
    • 文件系统阶段-使用文件存取数据,冗余度高,管理维护难
    • 数据库系统阶段-专门的数据库软件系统管理数据,高效方便,易于共享维护
  • 按照数据模型发展的主线,数据库技术的形成过程和发展可分为如下三个阶段:
    • 层次和网状数据库管理系统-可以理解为使用指针来表示数据之间的联系
    • 关系数据库管理系统(RDBMS)-可以理解为理解为使用二维表来表示维护数据间的关系
    • 新一代数据库技术的研究和发展-针对关系型数据库存在数据模型,性能,扩展性,伸缩性等方面的缺点,出现了:
    • ORDBMS:面向对象数据库技术。如:PostGreSQL
    • NoSQL:非结构化数据库技术。如:
      • 键值存储数据库:Redis
      • 列式储数数据库:HBase
      • 文档型数据库:MongoDB
      • 图形数据库:Neo4J
    • NewSQL:这类数据库不仅具有NoSQL对海量数据的存储管理能力,还保持了传统数据库支持ACID和SQL等特性。如:TiDB
1.2.从MySQL到TiDB

(1)场景引入

​ 假设现在有一个高速发展的互联网公司,核心业务库MySQL的数据量已经近亿行,且还在不断增长中,公司对于数据资产较为重视,所有数据要求多副本保存至少5年,且除了有对历史数据进行统计分析的离线报表业务外,还有一些针对用户数据实时查询的需求,如用户历史订单实时查询。

(2)问题分析

  • MySQL能否满足上述场景需求?

​ 根据以往的MySQL使用经验,MySQL单表在 5000 万行以内时,性能较好,单表超过5000万行后,数据库性能、可维护性都会极剧下降。当然这时候可以做MySQL分库分表,如使用Mycat或Sharding-jdbc。

  • 分库分表的能否解决问题?

​ 将大表拆分成小表,单表数据量控制在 5000 万行以内,使 MySQL 性能稳定可控。将单张大表拆分成小表后,能水平扩展,通过部署到多台服务器,提升整个集群的 QPS、TPS、Latency 等数据库服务指标。

​ 但是,此方案的缺点也非常明显:分表跨实例后,产生分布式事务管理难题,一旦数据库服务器宕机,有事务不一致风险。分表后,对 SQL 语句有一定限制,对业务方功能需求大打折扣。尤其对于实时报表统计类需求,限制非常之大。事实上,报表大多都是提供给高层领导使用的,其重要性不言而喻。分表后,需要维护的对象呈指数增长(MySQL实例数、需要执行的 SQL 变更数量等)。

(3)解决问题

​ 基于以上核心痛点,我们需要探索新的数据库技术方案来应对业务爆发式增长所带来的挑战,为业务提供更好的数据库服务支撑。

调研市场上的各大数据库,我们可以考虑选用NewSQL技术来解决,因为NewSQL技术有如下显著特点:

  • 无限水平扩展能力
  • 分布式强一致性,确保数据 100% 安全
  • 完整的分布式事务处理能力与 ACID 特性

​ 而TiDB数据库 GitHub的活跃度及社区贡献者方面都可以算得上是国际化的开源项目,是NewSQL技术中的代表性产品,所以我们可以选择使用TiDB数据库!

1.3.TiDB概述

(1)TiDB简介

  • 官网:https://pingcap.com/index.html
    大数据存储组件TiDB原理+实战篇

    ​ TiDB 是 PingCAP 公司设计的开源分布式 HTAP (Hybrid Transactional and Analytical Processing) 数据库,结合了传统的 RDBMS 和 NoSQL 的最佳特性。TiDB 兼容 MySQL,支持无限的水平扩展,具备强一致性和高可用性。TiDB 的目标是为 OLTP (Online Transactional Processing) 和 OLAP (Online Analytical Processing) 场景提供一站式的解决方案。

​ TiDB 的设计目标是 100% 的 OLTP 场景和 80% 的 OLAP 场景,更复杂的 OLAP 分析可以通过 TiSpark 项目来完成。

​ TiDB 对业务没有任何侵入性,能优雅的替换传统的数据库中间件、数据库分库分表等 Sharding 方案。同时它也让开发运维人员不用关注数据库 Scale 的细节问题,专注于业务开发,极大的提升研发的生产力。

(2)OLAP与OLTP的区别

OLTP:强调支持短时间内大量并发的事务操作(增删改查)能力,每个操作涉及的数据量都很小(比如几十到几百字节)

OLAP:偏向于复杂的只读查询,读取海量数据进行分析计算,查询时间往往很长。

1.4.数据库种类简介

(1)关系型数据库(RDBMS,即SQL数据库)

  • 商业软件: Oracle,DB2
  • 开源软件:MySQL,PostgreSQL
  • 单机版本已经很难满足海量数据的需求

(2)NoSQL非关系型的数据存储

  • 键值(Key-Value)数据库:如 MemcacheDB,Redis
  • 文档存储:如 MongoDB
  • 列存储:方便存储结构化和半结构化数据,并做数据压缩,对某几列的查询有非常大的IO优势: 如 HBase,Cassandra
  • 图数据库:存储图关系(注意:不是图片)。如 Neo4J

(3)NewSQL既保持NoSQL的高可扩展和高性能,并且保持关系模型

  • Google Spanner:Google最近公开的新一代分布式数据库。
  • OceanBase:蚂蚁集团完全自主研发的国产原生分布式数据库。
  • TiDB:PingCAP公司自主设计、研发的开源分布式关系型数据库。

2.TiDB架构特性

2.1.TiDB整体架构

​ TiDB集群主要包括三个核心组件:TiDB ServerPD ServerTiKV Server。此外,还有用于解决用户复杂OLAP需求的TiSpark组件和简化云上部署管理的TiDB Operator组件。
大数据存储组件TiDB原理+实战篇

PD Server

​ Placement Driver (简称 PD) 是整个集群的管理模块,其主要工作有三个:一是存储集群的元信息(某个 Key 存储在哪个 TiKV 节点);二是对 TiKV 集群进行调度和负载均衡(如数据的迁移、Raft group leader 的迁移等);三是分配全局唯一且递增的事务 ID。

TiDB Server

​ TiDB Server 负责接收 SQL 请求,处理 SQL 相关的逻辑,并通过 PD 找到存储计算所需数据的 TiKV 地址,与 TiKV 交互获取数据,最终返回结果。TiDB Server 是无状态的,其本身并不存储数据,只负责计算,可以无限水平扩展,可以通过负载均衡组件(如LVS、HAProxy 或 F5)对外提供统一的接入地址。

TiKV

​ TiKV Server 负责存储数据,从外部看 TiKV 是一个分布式的提供事务的 Key-Value 存储引擎。存储数据的基本单位是 Region,每个 Region 负责存储一个 Key Range(从 StartKey 到 EndKey 的左闭右开区间)的数据,每个 TiKV 节点会负责多个 Region。TiKV 使用 Raft 协议做复制,保持数据的一致性和容灾。副本以 Region 为单位进行管理,不同节点上的多个 Region 构成一个 Raft Group,互为副本。数据在多个 TiKV 之间的负载均衡由 PD 调度,这里也是以 Region 为单位进行调度。

TiSpark

​ TiSpark 作为 TiDB 中解决用户复杂 OLAP 需求的主要组件,将 Spark SQL 直接运行在 TiDB 存储层上,同时融合 TiKV 分布式集群的优势,并融入大数据社区生态。至此,TiDB 可以通过一套系统,同时支持 OLTP 与 OLAP,免除用户数据同步的烦恼。

TiDB Operator

​ TiDB Operator 提供在主流云基础设施(Kubernetes)上部署管理 TiDB 集群的能力。它结合云原生社区的容器编排最佳实践与 TiDB 的专业运维知识,集成一键部署、多集群混部、自动运维、故障自愈等能力,极大地降低了用户使用和管理 TiDB 的门槛与成本。

2.2.TiDB核心特性

TiDB 具备如下众多特性,其中两大核心特性为:水平扩展与高可用

(1)高度兼容MySQL

​ 大多数情况下,无需修改代码即可从 MySQL 轻松迁移至 TiDB,分库分表后的 MySQL 集群亦可通过 TiDB 工具进行实时迁移。

​ 对于用户使用的时候,可以透明地从MySQL切换到TiDB 中,只是“新MySQL”的后端是存储“无限的”,不再受制于Local的磁盘容量。在运维使用时也可以将TiDB当做一个从库挂到MySQL主从架构中。

(2)分布式事务

​ TiDB 100% 支持标准的 ACID 事务。

(3)一站式 HTAP 解决方案

​ TiDB 作为典型的 OLTP 行存数据库,同时兼具强大的 OLAP 性能,配合 TiSpark,可提供一站式 HTAP 解决方案,一份存储同时处理 OLTP & OLAP,无需传统繁琐的 ETL 过程。

(4)云原生SQL数据库

​ TiDB 是为云而设计的数据库,支持公有云、私有云和混合云,配合 TiDB Operator 项目 可实现自动化运维,使部署、配置和维护变得十分简单。

(5)水平弹性扩展

​ 通过简单地增加新节点即可实现 TiDB 的水平扩展,按需扩展吞吐或存储,轻松应对高并发、海量数据场景。

(6)真正的金融级高可用

​ 相比于传统主从 (M-S) 复制方案,基于 Raft 的多数派选举协议可以提供金融级的 100% 数据强一致性保证,且在不丢失大多数副本的前提下,可以实现故障的自动恢复 (auto-failover),无需人工介入。

水平扩展

  • 无限水平扩展是 TiDB 的一大特点,这里说的水平扩展包括两方面:计算能力(TiDB)和存储能力(TiKV)。
  • TiDB Server 负责处理 SQL 请求,随着业务的增长,可以简单的添加 TiDB Server 节点,提高整体的处理能力,提供更高的吞吐。
  • TiKV 负责存储数据,随着数据量的增长,可以部署更多的 TiKV Server 节点解决数据 Scale 的问题。
  • PD 会在 TiKV 节点之间以 Region 为单位做调度,将部分数据迁移到新加的节点上。
  • 所以在业务的早期,可以只部署少量的服务实例(推荐至少部署 3 个 TiKV, 3 个 PD,2 个 TiDB),随着业务量的增长,按照需求添加 TiKV 或者 TiDB 实例。

高可用

大数据存储组件TiDB原理+实战篇

​ 高可用是 TiDB 的另一大特点,TiDB/TiKV/PD 这三个组件都能容忍部分实例失效,不影响整个集群的可用性。下面分别说明这三个组件的可用性、单个实例失效后的后果以及如何恢复。

TiDB

​ TiDB 是无状态的,推荐至少部署两个实例,前端通过负载均衡组件对外提供服务。当单个实例失效时,会影响正在这个实例上进行的 Session,从应用的角度看,会出现单次请求失败的情况,重新连接后即可继续获得服务。单个实例失效后,可以重启这个实例或者部署一个新的实例。

PD

​ PD 是一个集群,通过 Raft 协议保持数据的一致性,单个实例失效时,如果这个实例不是 Raft 的 leader,那么服务完全不受影响;如果这个实例是 Raft 的 leader,会重新选出新的 Raft leader,自动恢复服务。PD 在选举的过程中无法对外提供服务,这个时间大约是3秒钟。推荐至少部署三个 PD 实例,单个实例失效后,重启这个实例或者添加新的实例。

TiKV

​ TiKV 是一个集群,通过 Raft 协议保持数据的一致性(副本数量可配置,默认保存三副本),并通过 PD 做负载均衡调度。单个节点失效时,会影响这个节点上存储的所有 Region。对于 Region 中的 Leader 节点,会中断服务,等待重新选举;对于 Region 中的 Follower 节点,不会影响服务。当某个 TiKV 节点失效,并且在一段时间内(默认 30 分钟)无法恢复,PD 会将其上的数据迁移到其他的 TiKV 节点上。

2.3.存储和计算能力

大数据存储组件TiDB原理+实战篇

(1)存储能力-TIKV-LSM

​ TiKV Server通常是3+的,TiDB每份数据缺省为3副本,这一点与HDFS有些相似,但是通过Raft协议进行数据复制,TiKV Server上的数据是以Region为单位进行,由PD Server集群进行统一调度,类似HBASE的Region调度。

​ TiKV集群存储的数据格式是KV的,在TiDB中,并不是将数据直接存储在 HDD/SSD中,而是通过RocksDB实现了TB级别的本地化存储方案,着重提的一点是:RocksDB和HBASE一样,都是通过 LSM树作为存储方案,避免了B+树叶子节点膨胀带来的大量随机读写。从何提升了整体的吞吐量。

(2)计算能力-TiDB Server

​ TiDB Server本身是无状态的,意味着当计算能力成为瓶颈的时候,可以直接扩容机器,对用户是透明的。理论上TiDB Server的数量并没有上限限制。

​ TiDB作为新一代的NewSQL数据库,在数据库领域已经逐渐站稳脚跟,结合了Etcd/MySQL/HDFS/HBase/Spark等技术的突出特点,随着TiDB的大面积推广,会逐渐弱化 OLTP/OLAP的界限,并简化目前冗杂的ETL流程,引起新一轮的技术浪潮。

3.TiDB安装部署

3.1.TiDB-Local单机版

​ 在Centos 6的版本中如果要部署,这个难度还是比较大的,而且会有很多未知的坑,根据官方的建议,是需要在Centos 7以上的版本中,否则glibc的版本问题会很快碰到。我们安装一套Centos7,采用快速的单机部署的方式来尝鲜。

(1)下载安装包

  • wget http://download.pingcap.org/tidb-latest-linux-amd64.tar.gz

(2)解压文件

  • tar -zxvf tidb-latest-linux-amd64.tar.gz
  • cd tidb-latest-linux-amd64

(3)启动

  • 启动PD:./bin/pd-server --data-dir=pd --log-file=pd.log &
  • 启动tikv:./bin/tikv-server --pd=“127.0.0.1:2379” --data-dir=tikv --log-file=tikv.log &
  • 启动tidb-server:./bin/tidb-server --store=tikv --path=“127.0.0.1:2379” --log-file=tidb.log &

(4)登入:mysql -h ip -P 4000 -u root

3.2.TiDB-Docker集群版

(1)准备环境

确保你的机器上已安装:

  • Docker(17.06.0 及以上版本)
  • Docker Compose
  • Git

(2)快速部署

下载 tidb-docker-compose

  • git clone https://github.com/pingcap/tidb-docker-compose.git

(3)创建并启动集群

获取最新 Docker 镜像:cd tidb-docker-compose && docker-compose pull && docker-compose up -d

注意:

  • 得先启动Docker
  • sudo systemctl start docker
  • 再执行上面的docker-compose命令

(4)查看集群启动状态

  • docker-compose ps

大数据存储组件TiDB原理+实战篇

大数据存储组件TiDB原理+实战篇

(5)Navicat测试链接
大数据存储组件TiDB原理+实战篇
大数据存储组件TiDB原理+实战篇

(6)访问集群监控页面

4.TiDB实践案例

4.1.TiDB-SQL操作

(1)创建、查看和删除数据库

  • 要创建一个名为 samp_db 的数据库,可使用以下语句:
    • CREATE DATABASE IF NOT EXISTS samp_db;
  • 使用 SHOW DATABASES 语句查看数据库:
    • SHOW DATABASES;
  • 使用 DROP DATABASE 语句删除数据库,例如:
    • DROP DATABASE samp_db;
  • 再次查看数据库:
    • SHOW DATABASES;

(2)创建、查看和删除表

  • 先创建一个库

    • CREATE DATABASE IF NOT EXISTS samp_db;
    • USE samp_db;
  • 使用 SHOW TABLES 语句查看数据库中的所有表。例如:

    • SHOW TABLES FROM samp_db;
  • 使用 CREATE TABLE 语句创建表。

    • 如果表已存在,添加 IF NOT EXISTS 可防止发生错误:

    • CREATE TABLE IF NOT EXISTS person (

      number INT(11),

      name VARCHAR(255),

      birthday DATE

      );

  • 使用 SHOW CREATE 语句查看建表语句。例如:

    • SHOW CREATE table person;
  • 使用 SHOW FULL COLUMNS 语句查看表的列。 例如:

    • SHOW FULL COLUMNS FROM person;
  • 使用 DROP TABLE 语句删除表。例如:

    • DROP TABLE person或者DROP TABLE IF EXISTS person;

(3)创建、查看和删除索引

  • 先创建一张表:

    • CREATE TABLE IF NOT EXISTS person (

      ​ number INT(11),

      ​ name VARCHAR(255),

      ​ birthday DATE

      );

  • 对于值不唯一的列,可使用 CREATE INDEX 或 ALTER TABLE 语句。例如:

    • CREATE INDEX person_num ON person (number) 或者 ALTER TABLE person ADD INDEX person_num (number);
  • 使用 SHOW INDEX 语句查看表内所有索引:

    • SHOW INDEX from person;
  • 使用 ALTER TABLE 或 DROP INDEX 语句来删除索引。与 CREATE INDEX 语句类似,DROP INDEX 也可以嵌入 ALTER TABLE 语句。例如:

    • DROP INDEX person_num ON person;
    • ALTER TABLE person DROP INDEX person_num;
  • 对于值唯一的列,可以创建唯一索引。例如:

    • CREATE UNIQUE INDEX person_num ON person (number) 或者 ALTER TABLE person ADD UNIQUE person_num (number);

(4)增删改查数据

  • 使用 INSERT 语句向表内插入数据。例如:
    • INSERT INTO person VALUES(“1”,“tom”,“20170912”);
  • 使用 SELECT 语句检索表内数据。例如:
    • SELECT * FROM person;
  • 使用 UPDATE 语句修改表内数据。例如:
    • UPDATE person SET birthday=‘20200202’ WHERE name=‘tom’;
    • SELECT * FROM person;
  • 使用 DELETE 语句删除表内数据:
    • DELETE FROM person WHERE number=1;
    • SELECT * FROM person;

(5)创建、授权和删除用户

  • 使用 CREATE USER 语句创建一个用户 tiuser,密码为 123456:
    • CREATE USER ‘tiuser’@‘localhost’ IDENTIFIED BY ‘123456’;
  • 授权用户 tiuser 可检索数据库 samp_db 内的表:
    • GRANT SELECT ON samp_db.* TO ‘tiuser’@‘localhost’;
  • 查询用户 tiuser 的权限
    • SHOW GRANTS for tiuser@localhost;
  • 删除用户 tiuser:
    • DROP USER ‘tiuser’@‘localhost’;
  • 查看所有权限
    • SHOW GRANTS;
4.2.TiDB-读取历史数据

(1)功能说明

​ TiDB 实现了通过标准 SQL 接口读取历史数据功能,无需特殊的 client 或者 driver。当数据被更新、删除后,依然可以通过 SQL 接口将更新/删除前的数据读取出来。

​ 另外即使在更新数据之后,表结构发生了变化,TiDB 依旧能用旧的表结构将数据读取出来。

(2)操作流程

​ 为支持读取历史版本数据, 引入了一个新的 system variable: tidb_snapshot ,这个变量是 Session 范围有效,可以通过标准的 Set 语句修改其值。其值为文本,能够存储 TSO 和日期时间。TSO 即是全局授时的时间戳,是从 PD 端获取的; 日期时间的格式可以为: “2020-10-08 16:45:26.999”,一般来说可以只写到秒,比如”2020-10-08 16:45:26”。 当这个变量被设置时,TiDB 会用这个时间戳建立 Snapshot(没有开销,只是创建数据结构),随后所有的 Select 操作都会在这个 Snapshot 上读取数据。

注意:

​ TiDB 的事务是通过 PD 进行全局授时,所以存储的数据版本也是以 PD 所授时间戳作为版本号。在生成 Snapshot 时,是以 tidb_snapshot 变量的值作为版本号,如果 TiDB Server 所在机器和 PD Server 所在机器的本地时间相差较大,需要以 PD 的时间为准。

​ 当读取历史版本操作结束后,可以结束当前 Session 或者是通过 Set 语句将 tidb_snapshot 变量的值设为 “",即可读取最新版本的数据。

(3)历史数据保留策略

​ TiDB 使用 MVCC 管理版本,当更新/删除数据时,不会做真正的数据删除,只会添加一个新版本数据,所以可以保留历史数据。历史数据不会全部保留,超过一定时间的历史数据会被彻底删除,以减小空间占用以及避免历史版本过多引入的性能开销。

​ TiDB 使用周期性运行的 GC(Garbage Collection,垃圾回收)来进行清理,关于 GC 的详细介绍参见 TiDB 垃圾回收 (GC)。

​ 这里需要重点关注的是 tikv_gc_life_time 和 tikv_gc_safe_point 这条。tikv_gc_life_time 用于配置历史版本保留时间,可以手动修改;tikv_gc_safe_point 记录了当前的 safePoint,用户可以安全地使用大于 safePoint 的时间戳创建 snapshot 读取历史版本。safePoint 在每次 GC 开始运行时自动更新。

(4)案例演示

  • 查看当前表数据

大数据存储组件TiDB原理+实战篇

  • 记录时间

大数据存储组件TiDB原理+实战篇

  • 修改数据

大数据存储组件TiDB原理+实战篇

  • 设置历史时间戳,读取历史数据

大数据存储组件TiDB原理+实战篇

4.3.TiDB整合Spark-TiSpark

(1)准备数据,向集群中插入样本数据

由于docker-compose安装已经帮我们整合好tispark组件,所以我们无须再次部署。

进入集群内部:docker-compose exec tispark-master bash

进入到tispark目录:cd /opt/spark/data/tispark-sample-data

执行脚本:mysql -h tidb -P 4000 -u root < dss.ddl

大数据存储组件TiDB原理+实战篇

(2)启动Spark shell

先退出容器内部执行命令:docker-compose exec tispark-master /opt/spark/bin/spark-shell

大数据存储组件TiDB原理+实战篇

(3)执行Spark代码

scala> import org.apache.spark.sql.TiContext
...
scala> val ti = new TiContext(spark)
...
scala> ti.tidbMapDatabase("TPCH_001")
...
scala> spark.sql("select count(*) from lineitem").show
+--------+
|count(1)|
+--------+
|   60175|
+--------+
也可以通过 Python 或 R 来访问 Spark:

docker-compose exec tispark-master /opt/spark/bin/pyspark &&
docker-compose exec tispark-master /opt/spark/bin/sparkR
执行另一个复杂一点的 Spark SQL:
scala> spark.sql(
      """select
        |   l_returnflag,
        |   l_linestatus,
        |   sum(l_quantity) as sum_qty,
        |   sum(l_extendedprice) as sum_base_price,
        |   sum(l_extendedprice * (1 - l_discount)) as sum_disc_price,
        |   sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)) as sum_charge,
        |   avg(l_quantity) as avg_qty,
        |   avg(l_extendedprice) as avg_price,
        |   avg(l_discount) as avg_disc,
        |   count(*) as count_order
        |from
        |   lineitem
        |where
        |   l_shipdate <= date '1998-12-01' - interval '90' day
        |group by
        |   l_returnflag,
        |   l_linestatus
        |order by
        |   l_returnflag,
        |   l_linestatus
      """.stripMargin).show
+------------+------------+---------+--------------+--------------+
|l_returnflag|l_linestatus|  sum_qty|sum_base_price|sum_disc_price|
+------------+------------+---------+--------------+--------------+
|           A|           F|380456.00|  532348211.65|505822441.4861|
|           N|           F|  8971.00|   12384801.37| 11798257.2080|
|           N|           O|742802.00| 1041502841.45|989737518.6346|
|           R|           F|381449.00|  534594445.35|507996454.4067|
+------------+------------+---------+--------------+--------------+
-----------------+---------+------------+--------+-----------+
       sum_charge|  avg_qty|   avg_price|avg_disc|count_order|
-----------------+---------+------------+--------+-----------+
 526165934.000839|25.575155|35785.709307|0.050081|      14876|
  12282485.056933|25.778736|35588.509684|0.047759|        348|
1029418531.523350|25.454988|35691.129209|0.049931|      29181|
 528524219.358903|25.597168|35874.006533|0.049828|      14902|
-----------------+---------+------------+--------+-----------+
4.4.数据迁移-TiDB Lightning

(1)TiDB Lightning介绍

​ TiDB Lightning是一个将全量数据高速导入到TIDB集群的工具,目前支持Mydumper或CSV输出格式的数据源。你可以在以下两种场景下使用Lightning:

  • 迅速导入大量新数据
  • 备份恢复所有数据

TiDB Lightning主要包含两个部分:

  • tidb-lightning(“前端”):主要完成适配工作,通过读取数据源,在下游TiDB集群建表、将数据转换成键、值对(KV对)发送到tikv-importer、检查数据完整性等。
  • tikv-importer(“后端”):主要完成对数据导入TiKV集群的工作,把tidb-lightning写入的KV对缓存排序、切分并导入到TiKV集群。

大数据存储组件TiDB原理+实战篇
大数据存储组件TiDB原理+实战篇

(2)下载迁移工具

wget https://download.pingcap.org/tidb-enterprise-tools-latest-linux-amd64.tar.gz

wget https://download.pingcap.org/tidb-toolkit-latest-linux-amd64.tar.gz

(3)准备mysql数据

5.TiDB技术原理

5.1.TiDB存储原理

(1)key-value

​ 作为保存数据的系统,首先要决定的是数据的存储模型,就是数据以什么样的方式保存下来。TiKV的选择是Key-Value模型,并且提供有序遍历方法。简单来说,可以将TiDB看做一个巨大的Map,其中Key和Value都是原始的Byte数组,在这个Map中。Key按照Byte数组总的原始二进制比特位比较顺序排序。

对于TiDB的理解只需要记住两点:

  • 这是一个巨大的Map,也就是存储的是Key-Value pair。
  • 这个Map中的Key-Value pair按照Key的二进制顺序有序,也就是我们可以Seek到某一个Key的位置,然后不断的调用Next方法以递增的顺序获取比这个Key大的Key-Value。

(2)RocksDB

任何持久化的存储引擎,数据终归要保存在磁盘上,TiKV也不例外,但是TiKV没有直接选择向磁盘上写数据,而是把数据保存在RocksDB中,具体的数据落地由RocksDB负责。这个选择的原因是开发一个单机存储引擎工作量很大,高性能的单机存储引擎。这里可以简单的认为RocksDB是一个单机的Key-Value Map。

底层LSM树将对数据的修改增量保存在内存中,达到指定大小后批量把数据flush到磁盘中。

(3)Raft

RocksDB保证单机的高性能,但是怎么保证数据的可靠性呢,Raft是一个一致性协议,提供几个重要功能:

  • Leader选举
  • 成员变更
  • 日志复制

TiKV利用Raft来做数据复制,每个数据变更都会落地为一条Raft日志,通过Raft的日志复制功能,将数据安全可靠的同步到Group的多数节点上。

大数据存储组件TiDB原理+实战篇

RocksDB解决数据快速的存储在磁盘上,通过Raft,我们可以将数据复制到多台机器上,以防止单机失效。数据的写入是通过Raft这一层的接口写入,而不是直接写RocksDB。通过实现Raft,我们拥有一个分

(4)Region

TiKV相当于一个巨大的有序的KV Map,为了实现存储的水平扩展,我们需要将数据分散在多台机器上。Region分散有两种方案:一种是按照Key做Hash,根据Hash值选择对应的存储节点,另一种是分Range,某一段连续的Key都保存在一个存储节点上。TiKV选择了第二种方式,将整个key-value空间分成很多段,每一段是一些列连续的key,我们将每一段叫做一个Region,并且我们会尽量保持每个Region都可以用到StartKey到EndKey这样一个左闭右开的区间来描述。

大数据存储组件TiDB原理+实战篇

将数据划分成Region后,我们将会做两件事:

  • 以Region为单位,将数据分散在集群中所有节点上,并且尽量保证每个节点上服务的Region数量差不多。
  • 以Region为单位做Raft的复制和成员管理。

先看第一点,数据按照Key切分成很多Region,每个Region的数据只会保存在一个节点上面。我们的系统会有一个组件将Region尽可能均匀的散布在及群众所有节点上,这样一方面实现了存储容量的水平扩展,另一方面也实现了负载均衡。通过任意一个key就能查到这个key在哪个Region中,以及这个Region目前在哪个节点上。

第二点,TiKV是以Region为单位做数据的复制,也就是一个Region的数据会保存多个副本,我们将一个副本叫做一个Replica。Replica之间是通过Raft来保持数据的一致,一个Region的多个Replica会保存在不同的节点上,构成一个Raft Group。其中一个Replica回座位这个Group的Leader,其他的Replica作为Follower。所有的读和写都是通过Leader进行,再由Leader复制给Follower。

大数据存储组件TiDB原理+实战篇

我们以Region为单位做数据的分散和复制,就有了一个分布式的具备一定容灾能力的KeyValue系统,不用担心数据存不下,或者磁盘故障丢失数据的问题。
(5)MVCC

很多数据库都会实现多版本控制(MVCC),TiKV也不例外。假设这样的场景,两个Client同事去修改一个Key,如果没有MVCC,就需要对数据上锁,在分布式场景下,可能会带来性能以及死锁的问题。TiKV的MVCC实现是通过在Key后面添加Version来实现。

大数据存储组件TiDB原理+实战篇

注意,对于同一个Key的多个版本,我们把版本号较大的放在前面,版本号小的放在后面,这样当用户通过一个Key+Version来获取value的时候,可以将Key和Version构造出MVCC的Key,也就是Key-Version。然后可以直接Seek(Key-Version),定位到第一个大于等于这个Key-Version的位置。

(6)事务

TiKV的事务采用的是Percolator模型,并且做了大量的优化。TiKV的事务采用乐观锁,事务的执行过程中,不会检测写写冲突,只有在提交过程中,才会做冲突检测,冲突的双方中比较早完成提交的会写入成功,另一方面尝试重新执行整个事务。当业务的写入冲突不严重的情况下,这种模型性能会很好,比如随机更新表中某一行的数据,并且表很大。但是如果业务的写入冲突严重,性能就会很差,举一个极端的例子,多个客户端修改少量行,导致冲突严重,造成大量的无效重试。

5.2.TiDB计算原理

(1)关系模型到 Key-Value 模型的映射

我们将关系模型简单的理解为Table和SQL语句,那么问题变为如何在KV结构上保存Table以及如何在KV结构上运行SQL语句。

SQL和KV结构之间存在巨大的区别,那么如何能够方便高效的进行映射,就成为一个很重要的问题。一个好的映射方案必须有利于对数据操作的需求。

对于一个Table来说,需要存储的数据包括三部分:

  • 表的元数据
  • Table中的Row
  • 索引数据

对于Row可以选择行存或者列存,这两种各有优缺点。TiDB面向的首要目标是OLTP业务,这类业务需要快速地读取、保存、修改、删除一行数据,所以采用行存是比较合适的。

对于Index,TiDB不止需要支持Primary Index,还需要支持Secondary Index。Index的作用的辅助查询,提升查询性能,以及保证某些Constraint。

查询的方式分为两种:点查和范围查询。

一个全局有序的分布式Key-value引擎。全局有序这一点重要,可以帮助我们解决不少问题。比如对于快速获取一行数据,假设我们能够构造出某一个或者几个Key,定位这一行,我们就利用TiKV提供的Seek方法快速定位到这一行数据所在的位置。在比如对于扫描全表的需求,如果能够映射为一个Key的Range,从StartKey扫描到EndKey,那么就可以简单的通过这种方式获取全表数据。操作Index数据也是类似的思路。

TiDB对每一个表分配一个TableID,每一个索引都会分配一个IndexID,每一行分配一个RowID,其中TableID在整个集群内唯一,IndexID/RowID在表内唯一,这些ID都是int64类型。

每行数据按照如下规则进行编码成Key-Value pair:

  • Key:tablePrefix{tableID}_recordPrefixSep{rowID}
  • value:[col1,col2,col3,col4]

其中Key的tablePrefix/recordPrefixSep都是特定的字符串常量,用于在KV空间内区分其他数据。

对于Index数据,会按照如下规则编码成Key-Value pair:

  • Key:tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumsValue
  • Value:rowID

Index数据还需要考虑Unique Index和非Unique Index两种情况,对于Unique Index,可以按照上述编码规则。但是对于非Unique Index,通过这种编码并不能构造出唯一的Key,因为同一个Index的tablePrefix{tableID}_indexPrefixSep{indexID}都一样,可能有很多行数据的ColumnsValue是一样的,所以对于非Unique Index的编码做了一点调整:

  • Key:tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue_rowID
  • Value:null

假设表中有 3 行数据:

  • 1, “TiDB”, “SQL Layer”, 10
  • 2, “TiKV”, “KV Engine”, 20
  • 3, “PD”, “Manager”, 30

那么首先每行数据都会映射为一个 Key-Value pair,注意这个表有一个 Int 类型的 Primary Key,所以 RowID 的值即为这个 Primary Key 的值。假设这个表的 Table ID 为 10,其 Row 的数据为:

  • t10_r1 --> [“TiDB”, “SQL Layer”, 10]
  • t10_r2 --> [“TiKV”, “KV Engine”, 20]
  • t10_r3 --> [“PD”, “Manager”, 30]

除了 Primary Key 之外,这个表还有一个 Index,假设这个 Index 的 ID 为 1,则其数据为:

  • t10_i1_10_1 --> null
  • t10_i1_20_2 --> null
  • t10_i1_30_3 --> null

(2)原信息管理

Database/Table都有元信息,也就是其定义以及各项属性,这些信息也需要持久化,我们也将这些信息存储在TiKV中。每个Database/Table都被分配一个唯一的ID,这个ID作为唯一标识,并且在编码为Key-Value时,这个ID都会编码到Key中,再加上m_前缀。这样就可以构造出一个Key,Value中存储的是序列化后的元信息。

除此之外,还有一个专门的Key-Value存储当前Schema版本信息。TiDB使用Google F1的Online Schema变更算法,有一个后台线程不断地检查TiKV上面存储的Schema版本是否发生变化,并且保证在一定时间内一定能够获取版本的变化。

(3)SQL on KV 架构

大数据存储组件TiDB原理+实战篇

TiKV Cluster主要作用是作为KV引擎存储数据,这里主要介绍SQL层,也就是TiDB Servers这一层,这一层的节点都是无状态的节点,本身并不存储数据,节点之间完全对等。TiDB Server这一层最重要的工作是处理用户请求,执行SQL运算逻辑。

(3)SQL 运算

理解了 SQL 到 KV 的映射方案之后,我们可以理解关系数据是如何保存的,接下来我们要理解如何使用这些数据来满足用户的查询需求,也就是一个查询语句是如何操作底层存储的数据。

能想到的最简单的方案就是通过上一节所述的映射方案,将 SQL 查询映射为对 KV 的查询,再通过 KV 接口获取对应的数据,最后执行各种计算。

比如 Select count(*) from user where name=“TiDB”; 这样一个语句,我们需要读取表中所有的数据,然后检查 Name 字段是否是 TiDB,如果是的话,则返回这一行。这样一个操作流程转换为 KV 操作流程:

  • 构造出 Key Range:一个表中所有的 RowID 都在 [0, MaxInt64) 这个范围内,那么我们用 0 和 MaxInt64 根据 Row 的 Key 编码规则,就能构造出一个 [StartKey, EndKey) 的左闭右开区间

  • 扫描 Key Range:根据上面构造出的 Key Range,读取 TiKV 中的数据

  • 过滤数据:对于读到的每一行数据,计算 name=“TiDB” 这个表达式,如果为真,则向上返回这一行,否则丢弃这一行数据

  • 计算 Count:对符合要求的每一行,累计到 Count 值上面 这个方案肯定是可以 Work 的,但是并不能 Work 的很好,原因是显而易见的:

    • 在扫描数据的时候,每一行都要通过 KV 操作同 TiKV 中读取出来,至少有一次 RPC 开销,如果需要扫描的数据很多,那么这个开销会非常大

    • 并不是所有的行都有用,如果不满足条件,其实可以不读取出来

    • 符合要求的行的值并没有什么意义,实际上这里只需要有几行数据这个信息就行

(4)分布式 SQL 运算

如何避免上述缺陷也是显而易见的,首先我们需要将计算尽量靠近存储节点,以避免大量的 RPC 调用。其次,我们需要将 Filter 也下推到存储节点进行计算,这样只需要返回有效的行,避免无意义的网络传输。最后,我们可以将聚合函数、GroupBy 也下推到存储节点,进行预聚合,每个节点只需要返回一个 Count 值即可,再由 tidb-server 将 Count 值 Sum 起来。

这里有一个数据逐层返回的示意图:

大数据存储组件TiDB原理+实战篇

(5)SQL 层架构

上面几节简要介绍了 SQL 层的一些功能,希望大家对 SQL 语句的处理有一个基本的了解。实际上 TiDB 的 SQL 层要复杂的多,模块以及层次非常多,下面这个图列出了重要的模块以及调用关系:

大数据存储组件TiDB原理+实战篇

用户的 SQL 请求会直接或者通过 Load Balancer 发送到 tidb-server,tidb-server 会解析 MySQL Protocol Packet,获取请求内容,然后做语法解析、查询计划制定和优化、执行查询计划获取和处理数据。

数据全部存储在 TiKV 集群中,所以在这个过程中 tidb-server 需要和 tikv-server 交互,获取数据。

最后 tidb-server 需要将查询结果返回给用户。

5.3.TiDB调度原理

(1)调度的基本操作

调度无非是下面三件事:

  • 增加一个 Replica
  • 删除一个 Replica
  • 将 Leader 角色在一个 Raft Group 的不同 Replica 之间 transfer

刚好 Raft 协议能够满足这三种需求,通过 AddReplica、RemoveReplica、TransferLeader 这三个命令,可以支撑上述三种基本操作。

(2)信息收集

调度依赖于整个集群信息的收集,简单来说,我们需要知道每个 TiKV 节点的状态以及每个 Region 的状态。TiKV 集群会向 PD 汇报两类消息:

每个 TiKV 节点会定期向 PD 汇报节点的整体信息

TiKV 节点(Store)与 PD 之间存在心跳包,一方面 PD 通过心跳包检测每个 Store 是否存活,以及是否有新加入的 Store;另一方面,心跳包中也会携带这个 Store 的状态信息,主要包括:

  • 总磁盘容量
  • 可用磁盘容量
  • 承载的 Region 数量
  • 数据写入速度
  • 发送/接受的 Snapshot 数量(Replica 之间可能会通过 Snapshot 同步数据)
  • 是否过载
  • 标签信息(标签是具备层级关系的一系列 Tag)

每个 Raft Group 的 Leader 会定期向 PD 汇报信息

每个 Raft Group 的 Leader 和 PD 之间存在心跳包,用于汇报这个 Region 的状态,主要包括下面几点信息:

  • Leader 的位置
  • Followers 的位置
  • 掉线 Replica 的个数
  • 数据写入/读取的速度

PD 不断的通过这两类心跳消息收集整个集群的信息,再以这些信息作为决策的依据。除此之外,PD 还可以通过管理接口接受额外的信息,用来做更准确的决策。比如当某个 Store 的心跳包中断的时候,PD 并不能判断这个节点是临时失效还是永久失效,只能经过一段时间的等待(默认是 30 分钟),如果一直没有心跳包,就认为是 Store 已经下线,再决定需要将这个 Store 上面的 Region 都调度走。但是有的时候,是运维人员主动将某台机器下线,这个时候,可以通过 PD 的管理接口通知 PD 该 Store 不可用,PD 就可以马上判断需要将这个 Store 上面的 Region 都调度走。

(3)调度的策略

PD 收集了这些信息后,还需要一些策略来制定具体的调度计划。

1.一个 Region的 Replica数量正确

当 PD 通过某个 Region Leader 的心跳包发现这个 Region 的 Replica 数量不满足要求时,需要通过 Add/Remove Replica 操作调整 Replica 数量。出现这种情况的可能原因是:

  • 某个节点掉线,上面的数据全部丢失,导致一些 Region 的 Replica 数量不足
  • 某个掉线节点又恢复服务,自动接入集群,这样之前已经补足了 Replica 的 Region 的 Replica 数量多过,需要删除某个 Replica
  • 管理员调整了副本策略,修改了 max-replicas 的配置

2.一个 Raft Group中的多个 Replica不在同一个位置

注意第二点,『一个 Raft Group 中的多个 Replica 不在同一个位置』,这里用的是『同一个位置』而不是『同一个节点』。在一般情况下,PD 只会保证多个 Replica 不落在一个节点上,以避免单个节点失效导致多个 Replica 丢失。在实际部署中,还可能出现下面这些需求:

  • 多个节点部署在同一台物理机器上
  • TiKV 节点分布在多个机架上,希望单个机架掉电时,也能保证系统可用性
  • TiKV 节点分布在多个 IDC 中,希望单个机房掉电时,也能保证系统可用

这些需求本质上都是某一个节点具备共同的位置属性,构成一个最小的容错单元,我们希望这个单元内部不会存在一个 Region 的多个 Replica。这个时候,可以给节点配置 lables 并且通过在 PD 上配置 location-labels 来指明哪些 lable 是位置标识,需要在 Replica 分配的时候尽量保证不会有一个 Region 的多个 Replica 所在结点有相同的位置标识。

3.副本在 Store 之间的分布均匀分配

前面说过,每个副本中存储的数据容量上限是固定的,所以我们维持每个节点上面,副本数量的均衡,会使得总体的负载更均衡。

4.Leader数量在 Store之间均匀分配

Raft 协议要读取和写入都通过 Leader 进行,所以计算的负载主要在 Leader 上面,PD 会尽可能将 Leader 在节点间分散开。

5.访问热点数量在 Store之间均匀分配

每个 Store 以及 Region Leader 在上报信息时携带了当前访问负载的信息,比如 Key 的读取/写入速度。PD 会检测出访问热点,且将其在节点之间分散开。

6.各个 Store的存储空间占用大致相等

每个 Store 启动的时候都会指定一个 Capacity 参数,表明这个 Store 的存储空间上限,PD 在做调度的时候,会考虑节点的存储空间剩余量。

7.控制调度速度,避免影响在线服务

调度操作需要耗费 CPU、内存、磁盘 IO 以及网络带宽,我们需要避免对线上服务造成太大影响。PD 会对当前正在进行的操作数量进行控制,默认的速度控制是比较保守的,如果希望加快调度(比如已经停服务升级,增加新节点,希望尽快调度),那么可以通过 pd-ctl 手动加快调度速度。

8.支持手动下线节点

当通过 pd-ctl 手动下线节点后,PD 会在一定的速率控制下,将节点上的数据调度走。当调度完成后,就会将这个节点置为下线状态。

(4)调度的实现

了解了上面这些信息后,接下来我们看一下整个调度的流程。

PD 不断的通过 Store 或者 Leader 的心跳包收集信息,获得整个集群的详细数据,并且根据这些信息以及调度策略生成调度操作序列,每次收到 Region Leader 发来的心跳包时,PD 都会检查是否有对这个 Region 待进行的操作,通过心跳包的回复消息,将需要进行的操作返回给 Region Leader,并在后面的心跳包中监测执行结果。注意这里的操作只是给 Region Leader 的建议,并不保证一定能得到执行,具体是否会执行以及什么时候执行,由 Region Leader 自己根据当前自身状态来定。文章来源地址https://www.toymoban.com/news/detail-430443.html

到了这里,关于大数据存储组件TiDB原理+实战篇的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Android入门到项目实战--4.5】—— SQLite数据库存储实现增删改查

    目录 一、添加数据 二、更新数据 三、删除数据 四、查询数据 使用完整SQL语言操作数据库 1、添加数据  2、更新数据 3、删除数据  4、查询数据 前面学习了创建和升级数据库,本篇文章主要讲解SQLite数据库存储实现增删改查(CRUD)操作。         调用SQLiteOpenHelper的get

    2024年02月04日
    浏览(52)
  • [Lesson 01] TiDB数据库架构概述

    目录 一 章节目标  二 TiDB 体系结构  1 TiDB Server 2.1 TiKV 2.2 TiFlash 3 PD 参考  理解TiDB数据库整体架构 了解TiDB Server ,TiKV ,TiFlash 和 PD的主要功能 了解这些体系结构是如何实现TiDB的核心功能的 TiDB Server 是无状态的,所以可以结合负载均衡的组件LVS。 数据并不是存储在TiDB Ser

    2024年02月16日
    浏览(35)
  • 数据库系统原理及MySQL应用教程实验七存储过程与函数的创建管理

    1. 理解存储过程和函数的概念。 2. 掌握创建存储过程和函数的方法。 3. 掌握执行存储过程和函数的方法。 4. 掌握游标的定义、使用方法。 1.验证性实验:某超市的食品管理的数据库的Food表对其操作。 2.设计性试验:学校教师管理数据库中的teacherInfo表对其操作。 三、实验步

    2024年02月03日
    浏览(53)
  • 我的docker随笔42:TiDB数据库部署

    本文介绍 TiDB 数据库的容器化部署。 前段时间,国产化替换提上了日程,有关部门(这个真的是某部门)作为领导身份介入,因此,需启动相应的事情,但最近似乎没有什么动静。因为身份问题,只好私下做一些力所能及的事。 数据库方面,有同事已经对接上达梦,当前只

    2024年02月12日
    浏览(38)
  • 11.云原生分布式数据库之TIDB

    云原生专栏大纲 从后端视角、运维视角和基础架构视角来看,使用 TiDB 作为数据库系统可以获得分布式架构、高可用性、强一致性、事务支持、水平扩展、高性能、简化运维、灵活的扩展和配置、集成的监控和告警等优势。这些优势使得 TiDB 成为处理大规模数据和高并发请求

    2024年02月01日
    浏览(63)
  • TiDB数据库从入门到精通系列之六:使用 TiCDC 将 TiDB 的数据同步到 Apache Kafka

    快速搭建 TiCDC 集群、Kafka 集群和 Flink 集群 创建 changefeed,将 TiDB 增量数据输出至 Kafka 使用 go-tpc 写入数据到上游 TiDB 使用 Kafka console consumer 观察数据被写入到指定的 Topic (可选)配置 Flink 集群消费 Kafka 内数据 部署包含 TiCDC 的 TiDB 集群 在实验或测试环境中,可以使用 TiU

    2024年02月12日
    浏览(54)
  • 【100天精通python】Day44:python网络爬虫开发_爬虫基础(爬虫数据存储:基本文件存储,MySQL,NoSQL:MongDB,Redis 数据库存储+实战代码)

    目录 1 数据存储 1.1 爬虫存储:基本文件存储 1.2 爬虫存储:使用MySQL 数据库 1.3 爬虫 NoSQL 数据库使用 1.3.1 MongoDB 简介

    2024年02月11日
    浏览(67)
  • 【NewSQL】-- 分布式数据库 TiDB 和 CockroachDB

          国产骄傲。从2015年开始,至今已8年,当前最新版本是7.5.x。TiDB 开源分布式 NewSQL 关系型数据库 是新一代开源分布式 NewSQL 数据库,模型受 Google Spanner / F1 论文的启发,实现了自动的水平伸缩,强一致性的分布式事务,基于 Raft 算法的多副本复制等重要 NewSQL 特性。  

    2024年01月21日
    浏览(57)
  • 【产品兼容认证】WhaleStudio 成功兼容TiDB数据库软件

    北京,2023年12月27日 - 平凯星辰(北京)科技有限公司(以下简称平凯星辰)旗下的 TiDB 产品与白鲸开源的 WhaleStudio 已成功完成产品兼容性认证。这一重要合作旨在为全球客户提供更大的价值。 WhaleStudio 是一款由 Apache DolphinScheduler 和 SeaTunnel 核心团队打造的商业版高性能分布

    2024年01月24日
    浏览(40)
  • 分布式数据库(DorisDB、Clickhouse、TiDB)调研

    B站视频:DorisDB VS ClickHouse OLAP PK 1.1 DorisDB 场量:线上数据应用 访问官方网站 DorisDB企业版文档 单表/多表查询,DorisDB总体时间最短 单表查询:DorisDB最快次数最多,ClickHouse次之 多表查询:DorisDB所有执行均最快 DorisDB多表关联效率好 支持各种主流分布式Join,不仅支持大宽表模

    2024年02月06日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包