Quantcast
Channel: 数据库内核月报
Viewing all 689 articles
Browse latest View live

PgSQL· 引擎特性 · 多版本并发控制介绍及实例分析

$
0
0

前言

PostgreSQL 内部通过多版本并发控制MVCC模型来维护。这意味着每个SQL语句看到的可能都是一小段时间以前某本版本的数据快照,而非当前数据最新的状态。这样可以避免并发写操作而造成的数据不一致问题。每个数据库都提供事务隔离机制,MVCC避免了强锁定方法,通过锁征用最小化来保障多用户环境下查询的性能。使用这一套模型最主要的优点是MVCC中读写请求并不冲突,读写互相不会阻塞。

PostgreSQL 元组

PostgreSQL中表以元组(堆)的形式存储,元组的头结点中包含了一些重要的节点信息。介绍MVCC前只针对需要重点阐述的信息进行介绍。

  • t_xmin 插入这条元组务的事务 txid
  • t_xmax 保存的是更新或删除这条元组事务的 txid,如果这条元素没有被删除或更新,那么 t_xmax 字段被设置为0, 即valid
  • t_cid 保存的是插入这条元组命令的编号,一个事务中可能会有多个命令,那么事务中的这些命令会从0开始依次被编号
  • t_ctid 中保存元组的标识符,他指向元组本身或者该元组最新的版本。由于PostgreSQL对于记录的修改不会直接修改元组中的用户数据,而是重新生成一个元组,旧的元组通过t_cid 指向新的元组。如果一条记录被修改多次,那么该记录会存在多个版本。各个版本通过t_cid 串联,形成一个版本链。通过这个版本链,可以找到最新的版本。t_cid 是一个二元组(x,y)其中x(从0开始编号)代表元组所在page, y(从1开始编号)表示在该page的第几个位置上

元组的 INSERT/DELETE/UPDATE 操作

上文提到PostgreSQL中对记录的修改不是直接修改tuple结构,而是重新生成一个tuple,旧的tuple通过t_cid指向新的tuple。下面针对数据库的三种典型操作,增加、删除、修改进行分析

INSERT

INSERT操作最简单,直接将一条记录放到page中即可

插入操作的过程和结果分析:

  • t_xmin 被设置成了 1184 即事务ID
  • t_xmax 被设置成了 0 因为该元组还未被更新或删除
  • t_cid 被设置为了0 这条命令是该事务的第一条命令
  • t_ctid 被设置为(0,1)表示这是一个元组,被放置在0page的第一个位置上

DELETE

PostgreSQL中的删除并不是真正的删除,实际上该元组依然存在于数据库的存储页面,只是因为其对元组进行处理使得查询不可见。

删除操作使得tuple 的t_xmax字段被修改了,即被改成了1187,删除这个tuple数据的事务txid

当txid为1187的事务提交时,tuple就成了无效元组,称为“dead tuple”

但是这个tuple依然残留在页面上, 随着数据库的运行,这种 “dead tuple” 越来越多,它们会在 VACUUM时最终被清理掉。

UPDATE

相比于前面的操作,update操作复杂一些。同样按照准则:PostgreSQL对记录的修改不会修改数据,而是先删除旧数据,后新增加数据。(将旧数据标记为删除状态,再插入一条新数据)

这里涉及的操作较多,依次理一下思路

1 . 首先第一条语句 是一次普通的更新操作

第一元组, 原本的ID是 1184 - 0 - 0 - (0,1) 升级操作第一步,将t_max 与 t_cid 更新, 新的ID 为

1184 - 1187 - 0 - (0, 2) 。

  • 对于 t_max 来说,其更新为了新的事务ID,也标识该tuple为“dead tuple”
  • 对于 t_tcid 来说, 其更新了新的指向ID,即从1号元组 更新成了 2号元组

第二元组,新插入一条 元组,这条元组就像新插入的数据一样,t_cid 指向其本身

2 . 第二条升级语句是同一事务中,多次更新的例子

第一元组 已经过期,且为“dead tuple” 不更新

第二元组为当前的活跃元组,其被更新,方式如同上文,指向第三元组

第三元组新插入一条数据,这里不同的是,由于在同一事务中,其事务号不变,而是事务次序+1(t_cid)

事务快照

事务快照是一个很形象的词,所谓快照就像是相机按下快门,记录当前瞬间的信息。 观察者通过快照只能获取当前时间点之前的信息,而按下快门以后的信息便无法察觉,即 INVISIBLE

类比于快照,事务快照就是当一个事务执行期间,那些事务active、那些非active。即这个事务要么在执行中,要么还没开始。

postgres=# SELECT txid_current_snapshot();
 txid_current_snapshot 
-----------------------
 1197:1197:
(1 row)

快照由这样一个序列构成 xmin:xmax:xip_list

  • xmin : 最早的active的 tid,所有小于该值的事务状态为visible(commit)或dead(abort)
  • xmax: 第一个还未分配的xid,大于等于该值的事务在快照生成时都不可见
  • xip_list 快照生成时所有active事务的txid

事务快照举例

在PostgreSQL中,当一个事务的隔离级别是 已提交读 时, 在该事务的每个命令执行之前都会重新获取一次 snapshot, 如果事务的隔离级别是 可重复度 和 可串行化时,事务只在第一条命令获取 snapshot。

现在假设一个多事务的场景,其中事务 A 事务 B 的隔离级别是 已提交读,而 事务 C 的隔离级别是 可重复读。这些阶段我们分别来讨论

  • T1

Transaction_A 开始并执行第一条命令,获取 txid 和 snapshot ,事务系统分配给 Transaction_A 分配的 txid 为 200, 并获取当前快照为 200:200

  • T2

Transaction_B 开始执行第一条命令,获取txid 和 snapshot, 事务系统给 Transaction_B 分配 txid 为 201并获取当前快照为 200:200 ,因为Transaction_A正在运行,所以Transaction_B 无法看到 A中的修改

  • T3

Transaction_C 开始执行第一条命令,获取txid和snapshot, 事务给Transaction_B 分配xid为 202,并获取当前快照为 200:200, Transaction_C 无法看到 A、B 中的修改

  • T4

Transaction_A 进行了commit,事务管理系统删除了 Transaction_A 信息

  • T5

Transaction_B Transaction_C 分别执行其 SELECT 命令,此时 Transaction_B 获取一个新的 snapshot(其隔离级别是已提交读),该snapshot为 201:201 。因为Transaction_A 已提交,所以Transaction_A 对 Transaction_B 可见。

同时,Transaction_C 隔离级别是 已提交读,所以它不会更新自己的 snapshot,因此 Transaction_A和Transaction_B仍然对Transaction_C不可见。

元组可见性规则

元组可见性利用以下几个点确定:

  1. tuple 中的 t_xmin 和 t_max
  2. clog
  3. 当前的snapshot

判断一个tuple对当前执行的事务是否可见,既事务是否会处理该tuple。下面只针对最简单的情形进行讨论

1 .t_xmin 的状态为 ABORTED

定则1:如果t_xmin事务废弃其不可见

2 .t_xmin 的状态为 IN_PROGRESS

定则2:如果t_xmin事务正在运行且非当前事务,事务中的tuple不可见(不然就是读脏数据)

定则3:如果t_xmin事务运行中,且tuple状态为死亡(t_xmax!=0)不可见

定则4:如果t_xmin事务运行中,且tuple状态为活跃(t_xmax=0)可见

3. t_xmin的状态为 COMMITTED

定则5:如果t_xmin事务已提交,当前快照中t_xmin活跃,则不可见

定则6:如果t_xmin事务已提交,t_xmax事务在运行中且t_max事务为当前事务,则不可见

定则7:如果t_xmin事务已提交,t_xmax事务在运行中且t_max事务不为当前事务,则可见

定则8:如果t_xmin事务已提交,t_xmax事务状态为已提交,当前快照中t_xmax活跃,则可见

定则9: 如果t_xmin事务已提交,t_xmax事务状态为已提交,当前快照t_xmax不活跃,不可见

PostgreSQL 中的 MVCC

PostgreSQL 多版本并发控制是种乐观锁的体现,解决了读多写少场景下的大规模读并发问题。一个事务在读取数据时应该读取哪个版本的数据,取决于该数据对于该事务的可见性。这种元组可见性检测可以帮助数据库找到”正确“版本的tuple,而且实现了ANSI SQL-92 标准中定义的异常:脏读、不可重复读、幻读


AliSQL · 引擎特性 · Recycle Bin

$
0
0

背景

MySQL 在生产环境使用过程中,会伴随着开发和运维人员的误操作,比如 DROP TABLE / DATABASE,这类 DDL 语句不具有可操作的回滚特性,而导致数据丢失,AliSQL 8.0 新特性支持回收站功能(Recycle Bin),临时把删除清理的表转移到回收站,并保留可设置的时间,方便用户找回数据。为了方便,提供了 DBMS_RECYCLE package 作为管理接口

Recycle Bin 管理接口

Recycle Bin 提供了两个管理接口,分别是:

1.DBMS_RECYCLE.show_tables()

展示回收站中所有临时保存的表:

  mysql> call dbms_recycle.show_tables();
  +-----------------+---------------+---------------+--------------+---------------------+---------------------+
  | SCHEMA          | TABLE         | ORIGIN_SCHEMA | ORIGIN_TABLE | RECYCLED_TIME       | PURGE_TIME          |
  +-----------------+---------------+---------------+--------------+---------------------+---------------------+
  | __recycle_bin__ | __innodb_1063 | product_db    | t1           | 2019-08-08 11:01:46 | 2019-08-15 11:01:46 |
  | __recycle_bin__ | __innodb_1064 | product_db    | t2           | 2019-08-08 11:01:46 | 2019-08-15 11:01:46 |
  | __recycle_bin__ | __innodb_1065 | product_db    | parent       | 2019-08-08 11:01:46 | 2019-08-15 11:01:46 |
  | __recycle_bin__ | __innodb_1066 | product_db    | child        | 2019-08-08 11:01:46 | 2019-08-15 11:01:46 |
  +-----------------+---------------+---------------+--------------+---------------------+---------------------+
  4 rows in set (0.00 sec)

– Columns 解释:

SCHEMA 
回收站的 schema
TABLE 
进入回收站后的表名
ORIGIN_SCHEMA 
原始表的 schema
ORIGIN_TABLE 
原始表的表名
RECYCLED_TIME 
回收时间
PURGE_TIME 
未来被清理掉的时间

2.DBMS_RECYCLE.purge_table(table_name=>)

手动清理回收站中的某张表

mysql> call dbms_recycle.purge_table("__innodb_1063");
Query OK, 0 rows affected (0.01 sec)
清理掉回收站中的"__innodb_1063"表

Recycle Bin 参数

Recycle Bin 一共设计了 5 个参数,分别是:

  1. recycle_bin
      recycle_bin
      -- 是否打开回收功能, session + global 级别。
    
  2. recycle_bin_retention
      recycle_bin_retention
      -- 回收站保留最长时间是多少,单位是seconds,默认是一周。
    
  3. recycle_scheduler
      recycle_scheduler
      -- 是否打开回收站的异步清理任务线程
    
  4. recycle_scheduler_interval
      recycle_scheduler_interval
      -- 回收站异步清理线程的轮询间隔,单位是seconds, 默认是30s。
    
  5. recycle_scheduler_purge_table_print
      recycle_scheduler_purge_table_print
      -- 是否打印异步清理现场工作的详细日志
    

Recycle Bin 设计

Recycle Bin 总览

1. 回收机制

当操作 DROP TABLE / DATABASE 语句的时候, 只保留相关的表对象,并移动到专门的 recycle bin 目录中,
其它对象的删除策略是:

  1. 与表无关的对象,比如 procedure,根据操作语句决定是否保留,不做回收。
  2. 表的附属对象,比如 trigger,Foreign key,column statistics等,只要存在可能修改表数据的,做删除,

比如 trigger,Foreign key。 但columns statistics不做清理,随表进入回收站。

2. 清理机制

回收站会启动一个background 线程,来异步清理超过 recycle_bin_retention 时间的表对象, 在清理回收站表的时候,如果遇到是大表的清理,会再启动一个background 来做异步大文件删除。

Recycle schema 和权限控制

1. recycle schema  MySQL 系统启动的时候,会初始化一个 recycle bin 的schema, 命名为 “recycle_bin“, 作为回收站使用的专有 database。

  mysql> show databases;
  +--------------------+
  | Database           |
  +--------------------+
  | __recycle_bin__    |
  | information_schema |
  | mysql              |
  | performance_schema |
  | sys                |
  +--------------------+
  6 rows in set (0.00 sec)

2. 权限控制 

1.Database 权限:

recycle_bin  作为回收站的 schema,是系统级 database,没有权限做修改和删除。
用户无法使用drop table / database 来操作回收站。
比如:

  mysql> drop table __recycle_bin__.__innodb_1064;
  ERROR 1044 (42000): Access denied for user 'b1'@'%' to database '__recycle_bin__'

2.recycled table 权限:

– recycle scheduler 后台线程具有所有权限,可以做清理工作;
– 用户虽然无法直接 drop table,可以使用 dbms_recycle.purge_table(),
但仍然需要原表和回收站表都具有 DROP_ACL 权限: 

比如:

mysql> call dbms_recycle.purge_table("__innodb_1064");
ERROR 1142 (42000): DROP command denied to user 'b1'@'localhost' for table '__innodb_1064'

-- Grant 回收站权限
mysql> grant drop on __recycle_bin__.__innodb_1064 to b1@'%';
Query OK, 0 rows affected (0.00 sec)
-- Grant 原表权限
mysql> grant drop on product_db.t2 to b1@'%';
Query OK, 0 rows affected (0.00 sec)
mysql> call dbms_recycle.purge_table("__innodb_1064");
Query OK, 0 rows affected (0.01 sec)

Recycled table 命名规则

Recycled table 会从不同的 schema,回收到统一的 recycle bin 回收站中,所以需要保证目标表表名唯一,所以
这里定义了一个命名格式:

"__" + Storge Engine + SE private id

Storge Engine:代表存储引擎名称,比如 innodb。
SE private id:是存储引擎为每一个表生成的唯一值,比如 InnoDB 中,就是 table id,
以此来唯一表示一个表名称。
 

Recycled table 关联对象

在回收表的过程中,需要处理表的相关对象,其处理的原则是:

  1. 如果是表附属对象,可能会存在修改表数据的可能性,就做删除,比如 trigger 和 FK。
  2. 如果是表相关对象,不会修改数据,就不做清理,比如相关的 view,统计信息等。

下面通过一个例子来看下:

 原始结构 

  CREATE TABLE parent (
      id INT NOT NULL,
      PRIMARY KEY (id)
      ) ENGINE=INNODB;

  CREATE TABLE child (
      id INT,
      parent_id INT,
      self_id INT,
      INDEX id_ind (id),
      INDEX par_ind (parent_id),
      INDEX sel_ind (self_id),
      FOREIGN KEY (self_id) REFERENCES child(id),
      FOREIGN KEY (parent_id) REFERENCES parent(id) ON DELETE CASCADE
      ) ENGINE=INNODB;

  CREATE TABLE log(id INT);

  delimiter //
  CREATE TRIGGER trigger_child
  before INSERT ON child FOR EACH ROW
  BEGIN
  INSERT INTO log value(1);
  END//
  delimiter ;

  CREATE VIEW view_child AS SELECT * FROM child;

Drop 并回收(相关关联对象删除或失效)

  1. 删除表 child;
  mysql> drop table child;
  Query OK, 0 rows affected (0.01 sec)

  2. 查看回收站,及 child 表在回收站的结构
  mysql> call dbms_recycle.show_tables();
  +-----------------+---------------+---------------+--------------+---------------------+---------------------+
  | SCHEMA          | TABLE         | ORIGIN_SCHEMA | ORIGIN_TABLE | RECYCLED_TIME       | PURGE_TIME          |
  +-----------------+---------------+---------------+--------------+---------------------+---------------------+
  | __recycle_bin__ | __innodb_1068 | test          | child        | 2019-08-08 12:32:48 | 2019-08-15 12:32:48 |
  +-----------------+---------------+---------------+--------------+---------------------+---------------------+

  mysql> show create table __recycle_bin__.__innodb_1068\G
  *************************** 1. row ***************************
  Table: __innodb_1068
  Create Table: CREATE TABLE `__innodb_1068` (
      `id` int(11) DEFAULT NULL,
      `parent_id` int(11) DEFAULT NULL,
      `self_id` int(11) DEFAULT NULL,
      KEY `id_ind` (`id`),
      KEY `par_ind` (`parent_id`),
      KEY `sel_ind` (`self_id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8

  -- 相关的 Foreign key 已经全部删除。

  3. 查看相关trigger。
  mysql> show create trigger trigger_child;
  ERROR 1360 (HY000): Trigger does not exist

  -- 相关的trigger已经全部删除。

  4. 查看相关view。
  mysql> show create view view_child\G
  *************************** 1. row ***************************
  View: view_child
  Create View: CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `view_child` AS select `child`.`id` AS `id`,`child`.`parent_id` AS `parent_id`,`child`.`self_id` AS `self_id` from `child`
  character_set_client: utf8mb4
  collation_connection: utf8mb4_0900_ai_ci
  1 row in set, 1 warning (0.01 sec)

  mysql> show warnings;
  +---------+------+-----------------------------------------------------------------------------------------------------------------------------------+
  | Level   | Code | Message                                                                                                                           |
  +---------+------+-----------------------------------------------------------------------------------------------------------------------------------+
  | Warning | 1356 | View 'test.view_child' references invalid table(s) or column(s) or function(s) or definer/invoker of view lack rights to use them |
  +---------+------+-----------------------------------------------------------------------------------------------------------------------------------+
  1 row in set (0.00 sec)

  -- 相关的view 已经失效。

Master-slave 独立回收

在 master - slave 结构中, 是否回收,或回收站保留的周期,都是实例本身的设置,不会影响到 binlog 复制到的节点上,所以,我们可以在 master 节点上设置回收,保留 7 天周期,在slave 节点上,设置回收,保留14天周期。
比如
master: 

  --recycle_bin = on
  --recycle_bin_retention = 7 * 24 * 60 * 60

  master节点上,回收站保留 7 天

slave: 

  --recycle_bin = on
  --recycle_bin_retention = 14 * 24 * 60 * 60

  slave 节点上,回收站保留 14 天

要注意的点就是,回收站保留周期不同,将导致 master - slave 节点之间的空间占用差别比较大。

异步表清理和大文件删除

当 recycle scheduler 异步线程 purge  回收站的表时候,如果遇到大表,那么将会启动大表异步删除逻辑,相关参数如下:

  INNODB_DATA_FILE_PURGE: Whether enable the async purge strategy
  INNODB_DATA_FILE_PURGE_IMMEDIATE: Unlink data file rather than truncate
  INNODB_DATA_FILE_PURGE_ALL_AT_SHUTDOWN: Cleanup all when normal shutdown
  INNODB_DATA_FILE_PURGE_DIR: Temporary file directory
  INNODB_DATA_FILE_PURGE_INTERVAL: Purge time interval (by milliseconds)
INNODB_DATA_FILE_PURGE_MAX_SIZE: Purge max size every time (by MB)
  INNODB_PRINT_DATA_FILE_PURGE_PROCESS: Print the process of file purge worker

比如设置:

  set global INNODB_DATA_FILE_PURGE = on;
  set global INNODB_DATA_FILE_PURGE_INTERVAL = 100;
  set global INNODB_DATA_FILE_PURGE_MAX_SIZE = 128;

  每 100ms,删除 128MB 大小。

可以通过如下视图,查看大表异步删除的进展情况:

  mysql> select * from information_schema.innodb_purge_files;
  +--------+---------------------+--------------------------------------+---------------+------------------------+--------------+
  | log_id | start_time          |          original_path               | original_size | temporary_path         | current_size |
  +--------+---------------------+--------------------------------------+---------------+------------------------+--------------+
  |     36 | 2019-08-08 12:06:38 | ./__recycle_bin__/__innodb_1064.ibd  |      37748736 | purge/#FP_1557846107_1 |     20971520 |
  +--------+---------------------+--------------------------------------+---------------+------------------------+--------------+

注意事项

1. 回收站跨文件系统如果你的回收站目录 “_recycle__bin“_  和回收的表跨了文件系统,那么drop table,将会搬迁表空间文件,耗时较长。

2. General tablespace  general tablespace 会存在多个表共享同一个表空间的情况, 当回收其中一张表的时候,不会搬迁相关的表空间文件,如果master 和 slave 设置的回收保留时间不同,那么就会存在在某一个时间点,主备间的这个general tablespace中的表数量不相等的情况。

MySQL · 引擎特性 · 8.0 Innodb redo log record 源码分析

$
0
0

Introduction

redo log对于innodb高效实现事务有至关重要的作用,关于redo log的介绍目前已有许多资料,但大都针对MySQL 5.6、MySQL 5.7版本,内容大都聚集在redo log与事务、redo log与恢复、checkpoint技术等特性上,对于redo log record本身却很少有(甚至几乎没有)资料介绍。目前8.0.16版本,redo log record一共有64种类型,对每种类型都进行详细分析是困难的。本文针对insert语句的redo log类型进行分析,重点分析B+树分裂产生新页时,redo log作为物理日志是如何准确地描述该过程。由于涉及到B+树,本文也会对innodb的数据页进行简单总结。

Index Page

关于索引页,可以查看这篇文章,此处总结索引页的一些关键设计

  • innodb page的大小由innodb_page_size确定,默认为16 KB
  • index page的结构图如下所示

FIL Header / Trailer

  • Offset(Page Number):每个表空间从0开始,该值乘以数据页的大小得到数据页在文件中的起始偏移量。在redo log通过记录该值指示操作修改了哪个页面
  • Previous/Next Page:两个指针,按照逻辑顺序(一般是主键顺序)组织成双向链表。这也可以看到,聚集索引指的是逻辑上的聚集,而物理上实际不一定是连续的。通过双向链表可以很方便进行范围查找
  • FIL_PAGE_LSN:最新被修改的LSN,用于实现幂等特性
  • FIL_PAGE_TYPE:可能的page typeindex、undo、blob等十多种
  • FIL_PAGE_ARCH_LOG_NO_OR_SPACE_IDspace id,redo log通过该值与page no唯一标识一个page

Page Header

  • Number of Directory Slots:目录槽的个数。
    • index page overview图中看到,记录从上往下涨,而目录槽从下往上堆。
    • Page Directory是一个稀疏目录,按照key排序。里面的每个slot指向了本slot中的第一个记录。
    • 当定位到一个page时,先通过page directory找到对应的slot,然后找到slot中的第一个记录,通过遍历slot内的所有记录,最终找到指定的记录
    • slot上的记录数不能太多也不能太少,对于普通记录在[4,8]区间内,如果超过则要重新整理,方法是将slot按照中间点拆分成两个,后续的目录进行平移(为了给新的slot腾出空间)
  • PAGE_HEAP_TOP:空闲空间的起始地址,
  • PAGE_N_HEAP:最高位用来标记格式(compact还是redundant),不会减少,只要有空闲空间记录就会增加
  • PAGE_FREE:删除记录的链表。
    • 当一个记录被删除时,会先被标记为delete状态,随后被purge线程彻底删除,最后用头插法加入这个链表。
    • 当一个记录被插入时,会先去page_free找,不行再用空闲空间(heap
  • PAGE_LAST_INSERT, PAGE_DIRECTION, PAGE_N_DIRECTION:上一次被插入的记录、最后一个记录插入的防线、同一个方法插入的记录数,这些都是为了加速连续插入操作
  • PAGE_N_RECS:用户的记录(不包括最大和最小记录)
  • PAGE_MAX_TRX_ID:修改此数据页的当前最大事务id
  • PAGE_LEVEL:索引页索引id
  • PAGE_INDEX_ID:索引页的索引ID
  • PAGE_BTR_SEG_LEAF,PAGE_BTR_SEG_TOP:叶子节点和非叶子节点的段头页地址

Infimum and Supremum Records

数据页上逻辑最小和最大的记录,数据页被创建时创建,不能被删除,目的是方便页内操作。

Compact Format

变长字段长度列表NULL字段标记记录头字段1数据字段2数据
0~n0~m5 bytes字段1长度字段2长度

上表是一个典型的compact格式的行记录,关于compact记录已有许多资料,具体可参考引用。 以下将补充许多资料中讲得相对模糊的地方:

  • 变长字段长度列表只记录变长字段的长度,比如VARCHAR,值得注意的是:
    • CHAR类型也会被记录,因为从mysql 4.1开始,CHR(N)中的N指的的字符个数,而不是字节个数,所以CHAR本身也需要存储其变长长度
  • 变长字段长度列表可以只有0个字节,只要行记录中没有变长字段
  • 对于一个变长字段,其字段长度最多只有2个字节,当长度小于256字节,用1字节表示,否则用2字节
  • NULL字段标记可以也只有0个字节,只要行记录中没有nullable字段
  • non-nullable字段是不会在NULL字段标记中被标记的 -NULL字段标记的标记方法:
    • 每一个nullable列都在NULL字段标记中有1 bit对应的标记位
    • 从右往左,即第一个nullable列在最低位
    • 如果nullable列为NULL,则对应的标记位为1,否则为0
  • NULL字段标记可以不止1个字节(网上许多资料均标注这里固定采用1个字节,其实是不对的),具体可以参考这篇文章。此处简单总结下结论:
    • NULL字段标记的长度不是固定的,每8nullable列占用1个字节
    • nullable列数不足8的整数倍时,最后一个字节高位补0
    • 基于上述原因,在设计表时,并非nullalbe字段越多越节省空间
  • 记录头总是固定的5个字节,其格式如下图所示

  • 记录头其中有两位是预留位,GCS格式中用来实现instantly add column,具体原理可参考这篇文章
  • 记录头后就是各个字段的具体数据,值得注意的有:
    • 第一个字段是主键,如果没有显示指定,则innodb会隐式增加一个6字节的字段作为主键
    • 接下来是两个隐藏的字段:事务ID列以及回滚指针列,分别占6 byte 7 bytes
    • 接下来是个非主键字段,如果nullable字段且其值为NULL,则不占用空间

insert && redo log

Flow Chart

insert redo record

上表是一条insert语句对应的redo log记录。值得注意的是,这条insert语句没有涉及到instant列,也不是临时表上的插入,并且上一条记录与本记录的extra_lendata_len都相同(见compact format, 同一张表的不同记录,这两部分的长度可能不同)。

即使是同一种类型的redo record,其解析格式也可能不同,如果上一条记录与本记录的extra_lendata_len都不同,则会额外记录本条记录在page中的偏移,以及本条记录发生mismatch的位置。

关于redo log record的资料很少,本次分析是最为简单的一种,下面简单阐述该表中较为难以理解的部分:

  • compressed:一种压缩格式,从最高位开始,第几位开始为0则意味着它的字节长度,如第1位为0(0nnnnnnn,则它是1个字节的数据;如第2位为0(10nnnnnn nnnnnnnn则它为2个字节的数据;以此类推
  • mismatch len:插入的记录与逻辑顺序上的上一条记录比对中,不一致的长度。
    • recovery发生时,通过上一条记录与redo log中记录结合,可以恢复出插入的记录。
    • 不同记录的记录头部分不一样,但在寻找mismatch点时,会跳过该部分,在恢复过程中,新insert的记录的记录头会被重新计算填充

Notes

  • offset:在构造index page record以及redo log record过程中,会使用到offset数组
    • offset数组用于标记一条记录中各个列相对于记录头的偏移
    • 该数组默认情况下从栈上分配,当表的列数超过100时,则从innodb中获取
    • 其结构如下所示:

  • rec_t*: 由于一条记录的extra以及data部分都是变长的,所以传入的指针rec_t*一般指向中间:即它的左侧是extra部分,包括变长字段长度列表、NULL标志、记录头;右侧则是各列的数据
  • LSN的值为当前已经写入的redo log长度,它没有在redo log record中记录,其原因在于:redo log block的头部中记录了本block的第一个redo log recrodLSN。在解析过程中,结合redo log record的长度可以获知该recordLSN
  • redo log的刷写不需要double write保护,原因在于redo log block的长度为512个字节,与硬盘扇区的长度一致。读写一个扇区是原子操作。

create page && redo log

可以看到,当插入一条记录时,edo log记录了对对应数据页的修改过程。但当当前page不足以放下新插入的记录时,且邻居page也没有空间时,会触发B+树的分裂操作。具体过程:

// 持有page的X-latch
btr_cur_optimistic_insert--> 
// 当前page空间不足,乐观插入失败, 
// 进行悲观插入之前,btr_pcur_open对父亲子树进行上锁
btr_cur_pessimistic_insert--> 
// 分裂,并将记录插入指定page
btr_page_split_and_insert-->  
...

create page redo log record

btr_page_split_and_insert函数中,调用btr_page_alloc分配新的page,然后调用btr_page_create创建新页,期间调用page_create_write_log生成一条类型为MLOG_COMP_PAGE_CREATEredo log记录,其典型format如下:

typespace_idpage_no
MLOG_COMP_PAGE_CREATE, 1 byte1~5 bytes1~5 bytes

接着在btr_page_set_level方法中,生成一条类型为MLOG_2BYTESrecord,记录pageB+树上的level(叶子节点的level0,根节点level为深度),其format如下所示:

typespace_idpage_nopage_offsetval
MLOG_2BYTES, 1 byte1~5 bytes1~5 bytes2 bytescompressed, 1~3 bytes

接下来btr_page_set_index_id方法中,生成一条类型为MLOG_8BYTES,该record表示将对应的page(space_id + page_no )中的PAGE_INDEX_ID字段设置为val值,其format与上述MLOG_2BYTES类似

接下来btr_insert_on_non_leaf_level_func会调用btr_cur_optimistic_insert,或者btr_cur_pessimistic_insert来将聚集索引的非叶子节点的记录插入。聚集索引的非叶子节点的记录形如下:

可以看到非叶子节点的记录与用户表记录实际并没有本质区别,该过程可以看成是往page中插入一条“用户记录”。

由于B+树的分裂是个递归过程,btr_insert_on_non_leaf_level_func函数也会被递归调用,直到调整好B+树。期间会继续不断产生相应的redo record,包括但不限于类型为MLOG_COMP_REC_INSERTMLOG_COMP_PAGE_CREATEMLOG_8BYTES等的redo log record

Reference

Database · 内存管理 · JeMalloc-5.1.0 实现分析

$
0
0

JeMalloc 是一款内存分配器,与其它内存分配器相比,它最大的优势在于多线程情况下的高性能以及内存碎片的减少。

这篇文章介绍 JeMalloc-5.1.0版本(release 日期:2018年5月9日)的实现细节。

对于对老版本比较熟悉的人来说,有几点需要说明:

  1. chunk 这一概念被替换成了 extent
  2. dirty page 的 decay(或者说 gc) 变成了两阶段,dirty -> muzzy -> retained
  3. huge class 这一概念不再存在
  4. 红黑树不再使用,取而代之的是 pairing heap

基础知识

以下内容介绍 JeMalloc 中比较重要的概念以及数据结构。

size_class

每个 size_class代表 jemalloc 分配的内存大小,共有 NSIZES(232)个小类(如果用户申请的大小位于两个小类之间,会取较大的,比如申请14字节,位于8和16字节之间,按16字节分配),分为2大类:

  • small_class小内存) : 对于64位机器来说,通常区间是 [8, 14kb],常见的有 8, 16, 32, 48, 64, …, 2kb, 4kb, 8kb,注意为了减少内存碎片并不都是2的次幂,比如如果没有48字节,那当申请33字节时,分配64字节显然会造成约50%的内存碎片
  • large_class大内存): 对于64位机器来说,通常区间是 [16kb, 7EiB],从 4 * page_size 开始,常见的比如 16kb, 32kb, …, 1mb, 2mb, 4mb,最大是 $2^{62}+3^{60}$
  • size_index : size 位于 size_class中的索引号,区间为 [0,231],比如8字节则为0,14字节(按16计算)为1,4kb字节为28,当 size 是 small_class时,size_index也称作 binind

base

用于分配 jemalloc 元数据内存的结构,通常一个 base大小为 2mb, 所有 base组成一个链表。

  • base.extents[NSIZES] : 存放每个 size_classextent元数据

bin

管理正在使用中的 slab(即用于小内存分配的 extent) 的集合,每个 bin对应一个 size_class

  • bin.slabcur : 当前使用中的 slab

  • bin.slabs_nonfull : 有空闲内存块的 slab

extent

管理 jemalloc 内存块(即用于用户分配的内存)的结构,每一个内存块大小可以是 N * page_size(4kb)(N >= 1)。每个 extent 有一个序列号(serial number)。

一个 extent可以用来分配一次 large_class的内存申请,但可以用来分配多次 small_class的内存申请。

  • extent.e_bits : 8字节长,记录多种信息
  • extent.e_addr : 管理的内存块的起始地址
  • extent.e_slab_data : 位图,当此 extent用于分配 small_class内存时,用来记录这个 extent的分配情况,此时每个 extent的内的小内存称为 region

slab

当 extent 用于分配 small_class内存时,称其为 slab。一个 extent可以被用来处理多个同一 size_class的内存申请。

extents

管理 extent的集合。

  • extents.heaps[NPSIZES+1] : 各种 page(4kb)倍数大小的 extent
  • extents.lru : 存放所有 extent的双向链表
  • extents.delay_coalesce : 是否延迟 extent的合并

arena

用于分配&回收 extent的结构,每个用户线程会被绑定到一个 arena上,默认每个逻辑 CPU 会有 4 个 arena来减少锁的竞争,各个 arena 所管理的内存相互独立。

  • arena.extents_dirty : 刚被释放后空闲 extent位于的地方
  • arena.extents_muzzy : extents_dirty进行 lazy purge 后位于的地方,dirty -> muzzy
  • arena.extents_retained : extents_muzzy进行 decommit 或 force purge 后 extent位于的地方,muzzy -> retained
  • arena.large : 存放 large extentextents
  • arena.extent_avail : heap,存放可用的 extent元数据
  • arena.bins[NBINS] : 所以用于分配小内存的 bin
  • arena.base : 用于分配元数据的 base
内存状态备注
clean分配给用户或 tcache
dirty用户调用 free 或 tcache进行了 gc
muzzyextents_dirtyextent进行 lazy purge
retainedextents_muzzyextent进行了 decommit 或 force purge

purge 及 decommit 在内存 gc 模块介绍

rtree

全局唯一的存放每个 extent信息的 Radix Tree,以 extent->e_addruintptr_t为 key,以我的机器为例,uintptr_t为64位(8字节), rtree的高度为3,由于 extent->e_addrpage(1 << 12)对齐的,也就是说需要 64 - 12 = 52 位即可确定在树中的位置,每一层分别通过第0-16位,17-33位,34-51位来进行访问。

cache_bin

每个线程独有的用于分配小内存的缓存

  • low_water : 上一次 gc 后剩余的缓存数量
  • cache_bin.ncached : 当前 cache_bin存放的缓存数量
  • cache_bin.avail : 可直接用于分配的内存,从左往右依次分配(注意这里的寻址方式)

tcache

每个线程独有的缓存(Thread Cache),大多数内存申请都可以在 tcache中直接得到,从而避免加锁

  • tcache.bins_small[NBINS] : 小内存的 cache_bin

tsd

Thread Specific Data,每个线程独有,用于存放与这个线程相关的结构

  • tsd.rtree_ctx : 当前线程的 rtree context,用于快速访问 extent信息
  • tsd.arena : 当前线程绑定的 arena
  • tsd.tcache : 当前线程的 tcache

内存分配(malloc)

小内存(small_class)分配

首先从 tsd->tcache->bins_small[binind]中获取对应 size_class的内存,有的话将内存直接返回给用户,如果 bins_small[binind]中没有的话,需要通过 slab(extent)tsd->tcache->bins_small[binind]进行填充,一次填充多个以备后续分配,填充方式如下(当前步骤无法成功则进行下一步):

  1. 通过 bin->slabcur分配
  2. bin->slabs_nonfull中获取可使用的 extent
  3. arena->extents_dirty中回收 extent,回收方式为 best-fit,即满足大小要求的最小extent,在 arena->extents_dirty->bitmap中找到满足大小要求并且第一个非空 heap 的索引 i,然后从 extents->heaps[i]中获取第一个 extent。由于 extent可能较大,为了防止产生内存碎片,需要对 extent进行分裂(伙伴算法),然后将分裂后不使用的 extent放回 extents_dirty
  4. arena->extents_muzzy中回收 extent,回收方式为 first-fit,即满足大小要求且序列号最小地址最低(最旧)extent,遍历每个满足大小要求并且非空的 arena->extents_dirty->bitmap,获取其对应 extents->heaps中第一个 extent,然后进行比较,找到最旧的 extent,之后仍然需要分裂
  5. arena->extents_retained中回收 extent,回收方式与 extents_muzzy相同
  6. 尝试通过 mmap向内核获取所需的 extent内存,并且在 rtree中注册新 extent的信息
  7. 再次尝试从 bin->slabs_nonfull中获取可使用的 extent

简单来说,这个流程是这样的,cache_bin -> slab -> slabs_nonfull -> extents_dirty -> extents_muzzy -> extents_retained -> kernal

大内存(large_class)分配

大内存不存放在 tsd->tcache中,因为这样可能会浪费内存,所以每次申请都需要重新分配一个 extent,申请的流程和小内存申请 extent流程中的3, 4, 5, 6是一样的。

内存释放(free)

小内存释放

rtree中找到需要被释放内存所属的 extent信息,将要被释放的内存还给 tsd->tcache->bins_small[binind],如果 tsd->tcache->bins_small[binind]已满,需要对其进行 flush,流程如下:

  1. 将这块内存返还给所属 extent,如果这个 extent中空闲的内存块变成了最大(即没有一份内存被分配),跳到2;如果这个 extent中的空闲块变成了1并且这个 extent不是 arena->bins[binind]->slabcur,跳到3
  2. 将这个 extent释放,即插入 arena->extents_dirty
  3. arena->bins[binind]->slabcur切换为这个 extent,前提是这个 extent“更旧”(序列号更小地址更低),并且将替换后的 extent移入 arena->bins[binind]->slabs_nonfull

大内存释放

因为大内存不存放在 tsd->tcache中,所以大内存释放只进行小内存释放的步骤2,即将 extent插入 arena->extents_dirty中。

内存再分配(realloc)

小内存再分配

  1. 尝试进行 no move分配,如果两次申请位于同一 size class的话就可以不做任何事情,直接返回。比如第一次申请了12字节,但实际上 jemalloc 会实际分配16字节,然后第二次申请将12扩大到15字节或者缩小到9字节,那这时候16字节就已经满足需求了,所以不做任何事情,如果无法满足,跳到2
  2. 重新分配,申请新内存大小(参考内存分配),然后将旧内存内容拷贝到新地址,之后释放旧内存(参考内存释放),最后返回新内存

大内存再分配

  1. 尝试进行 no move分配,如果两次申请位于同一 size class的话就可以不做任何事情,直接返回。
  2. 尝试进行 no move resize分配,如果第二次申请的大小大于第一次,则尝试对当前地址所属 extent的下一地址查看是否可以分配,比如当前 extent地址是 0x1000,大小是 0x1000,那么我们查看地址 0x2000 开始的 extent是否存在(通过 rtree)并且是否满足要求,如果满足要求那两个 extent可以进行合并,成为一个新的 extent而不需要重新分配;如果第二次申请的大小小于第一次,那么尝试对当前 extent进行 split,移除不需要的后半部分,以减少内存碎片;如果无法进行 no move resize分配,跳到3
  3. 重新分配,申请新内存大小(参考内存分配),然后将旧内存内容拷贝到新地址,之后释放旧内存(参考内存释放),最后返回新内存

内存 GC

分为2种, tcacheextent GC。其实更准确来说是 decay,为了方便还是用 gc 吧。

tcache GC

针对 small_class,防止某个线程预先分配了内存但是却没有实际分配给用户,会定期将缓存 flush 到 extent

GC 策略

每次对于 tcache进行 malloc 或者 free 操作都会执行一次计数,默认情况下达到228次就会触发 gc,每次 gc 一个 cache_bin

如何 GC

  1. cache_bin.low_water > 0 : gc 掉 low_water的 3/4,同时,将 cache_bin能缓存的最大数量缩小一倍
  2. cache_bin.low_water < 0 : 将 cache_bin能缓存的最大数量增大一倍

总的来说保证当前 cache_bin分配越频繁,则会缓存更多的内存,否则则会减少。

extent GC

调用 free 时,内存并没有归还给内核。 jemalloc 内部会不定期地将没有用于分配的 extent逐步 GC,流程和内存申请是反向的, free -> extents_dirty -> extents_muzzy -> extents_retained -> kernal

GC 策略

默认10s为 extents_dirtyextents_muzzy的一个 gc 周期,每次对于 arena进行 malloc 或者 free 操作都会执行一次计数,达到1000次会检测有没有达到 gc 的 deadline,如果是的话进行 gc。

注意并不是每隔10s一次性 gc,实际上 jemalloc 会将10s划分成200份,即每隔0.05s进行一次 gc,这样一来如果 t 时刻有 N 个 page需要 gc,那么 jemalloc 尽力保证在 t+10 时刻这 N 个 page会被 gc 完成。

严格来说,对于两次 gc 时刻 $t_{1}$ 和 $t_{2}$,在 $t_{2}-t_{1}$ 时间段内产生的所有 page(dirty page 或 muzzy page) 会在 ($t_{2}$, $ t_{2}+10$] 被 gc 完成。

对于 N 个需要 gc 的 page来说,并不是简单地每0.05s处理 N/200 个 page,jemalloc 引入了 Smoothstep(主要用于计算机图形学)来获得更加平滑的 gc 机制,这是 jemalloc 非常有意思的一个点。

jemalloc 内部维护了一个长度为200的数组,用来计算在10s的 gc 周期内每个时间点应该对多少 page进行 gc。这样保证两次 gc 的时间段内产生的需要 gc 的 page都会以图中绿色线条(默认使用 smootherstep)的变化曲线在10s的周期内从 N 减为 0(从右往左)。

如何 GC

先进行 extents_dirty的 gc,后进行 extents_muzzy

  • extents_dirty中的 extent移入 extents_muzzy
    1. extents_dirty中的 LRU 链表中,获得要进行 gc 的 extent,尝试对 extent进行前后合并(前提是两个 extent位于同一 arena并且位于同一 extents中),获得新的 extent,然后将其移除

    2. 对当前 extent管理的地址进行 lazy purge,即通过 madvise使用 MADV_FREE参数告诉内核当前 extent管理的内存可能不会再被访问

    3. extents_muzzy中尝试对当前 extent进行前后合并,获得新的 extent,最后将其插入 extents_muzzy

  • extents_muzzy中的 extent移入 extents_retained :
    1. extents_muzzy中的 LRU 链表中,获得要进行 gc 的 extent,尝试对 extent进行前后合并,获得新的 extent,然后将其移除

    2. 对当前 extent管理的地址进行 decommit,即调用 mmap带上 PROT_NONE告诉内核当前 extent管理的地址可能不会再被访问,如果 decommit 失败,会进行 force purge,即通过 madvise使用 MADV_DONTNEED参数告诉内核当前 extent管理的内存可能不会再被访问

    3. extents_retained中尝试对当前 extent进行前后合并,获得新的 extent,最后将其插入 extents_retained

  • jemalloc 默认不会将内存归还给内核,只有进程结束时,所有内存才会 munmap,从而归还给内核。不过可以手动进行 arena的销毁,从而将 extents_retained中的内存进行 munmap

内存碎片

JeMalloc 保证内部碎片在20%左右。对于绝大多数 size_class来说,都属于 $2^{x}$ 的 group $y$,比如 160,192,224,256都属于 $2^{8-1}$ 的 group 7。对于一个 group 来说,会有4个 size_class,每个 size 的大小计算是这样的,$(1 « y) + (i « (y-2))$,其中 i 为在这个 group 中的索引(1,2,3,4),比如 160 为 $(1 « 7) + (1 « 5)$,即 $5 * 2^{7-2}$。

对于两组 group 来说:

GroupSize
y$5 * 2^{y-2}$, $6 * 2^{y-2}$, $7 * 2^{y-2}$, $8 * 2^{y-2}$
y+1$5 * 2^{y-1}$, $6 * 2^{y-1}$, $7 * 2^{y-1}$, $8 * 2^{y-1}$

取相差最大的第一组的最后一个和第二组的第一个,内存碎片约为 $\frac{5 * 2^{y-1} - 8 * 2^{y-2} + 1}{5 * 2^{y-1}}$ 约等于 20%。

JeMalloc 实现上的优缺点

优点

  1. 采用多个 arena来避免线程同步
  2. 细粒度的锁,比如每一个 bin以及每一个 extents都有自己的锁
  3. Memory Order 的使用,比如 rtree的读写访问有不同的原子语义(relaxed, acquire, release)
  4. 结构体以及内存分配时保证对齐,以获得更好的 cache locality
  5. cache_bin分配内存时会通过栈变量来判断是否成功以避免 cache miss
  6. dirty extent的 delay coalesce 来获得更好的 cache locality;extent的 lazy purge 来保证更平滑的 gc 机制
  7. 紧凑的结构体内存布局来减少占用空间,比如 extent.e_bits
  8. rtree引入 rtree_ctx的两级 cache机制,提升 extent信息获取速度的同时减少 cache miss
  9. tcache gc 时对缓存容量的动态调整

缺点

  1. arena之间的内存不可见
    • 某个线程在这个 arena使用了很多内存,之后这个 arena并没有其他线程使用,导致这个 arena的内存无法被 gc,占用过多
    • 两个位于不同 arena的线程频繁进行内存申请,导致两个 arena的内存出现大量交叉,但是连续的内存由于在不同 arena而无法进行合并
  2. 目前只想到了一个

总结

文章开头说 JeMalloc 的优点在于多线程下的性能以及内存碎片的减少,对于保证多线程性能,有不同 arena、降低锁的粒度、使用原子语义等等;对于内存碎片的减少,有经过设计的多种 size_class、伙伴算法、gc 等等。

阅读 JeMalloc 源码的意义不光在于能够精确描述每次 malloc 和 free 会发生什么,还在于学习内存分配器如何管理内存。malloc 和 free 是静态的释放和分配,而 tcacheextent的 gc 则是动态的管理,熟悉后者同样非常重要。

除此以外还能够帮助自己在编程时根据相应的内存使用特征去选择合适的内存分配方法,甚至使用自己实现的特定内存分配器。

最后个人觉得 jemalloc 最有意思的地方就在于 extent的曲线 gc 了。

参考

JeMalloc

JeMalloc-4.0.3

JeMalloc-Purge

图解 TCMalloc

TCMalloc分析 - 如何减少内存碎片

TCMalloc

MySQL · 引擎特性 · clone_plugin

$
0
0

背景

mysql官方在8.0.17 release了克隆实例功能,它能让用户很方便的在空实例上通过简单的sql命令把远端实例拷贝到本地并替换后重新提供服务,该功能由一系列的worklog实现:

worklog9209实现本地clone,它完成clone核心功能开发。

worklog9210在本地克隆的基础上实现远程克隆,通过新加协议,以流的方式把实例克隆到其他服务器上。

worklog9211完成获取,传输和保存克隆位点的功能,方便克隆实例能够正确的加入到被克隆集群中。

后续还有worklog基于克隆功能实现其他功能:比如备库重搭,group replication新建节点等。

基本原理

克隆最基本的要求就是要保证把克隆源的一个一致性的状态拷贝到另一个数据目录中,那么插件是如何保证拷贝完成时是一个一致性的点呢,这涉及到snapshot的概念,源库的一个snapshot就是一个一致性的状态点,拷贝源库的snapshot到目的数据目录就保证了目的数据目录具有源库的历史一致性状态。

clone snapshot

克隆snapshot是如何实现的呢?总的来说分为3步: snapshot

每一步的分界点都以lsn来区分 基线拷贝所有的文件(clone start lsn -> clone file end lsn) 增量1拷贝 clone start lsn到clone file end lsn之间搜集的脏页 增量2拷贝 clone file end lsn到clone lsn归档的redo

一致性原理: clone开始的时候所有已经flush的数据都通过文件拷贝了,而未flush的将被记录下来了 clone结束的时候:  到最近的checkpoint的脏页都被记录下来了,这些脏页应用到全量文件上就等价于最近的checkpoint,而checkpoint以后的增量通过拷贝归档redo来实现。这个截止点clone lsn(对应的binlog位点)就被完整的拷贝到了目的实例

snapshot因此被分成了如下几种状态:

snapshot_states

实现snapshot必须实现脏页收集和redo归档 脏页收集: 脏页收集可以有两种方式:1. 在mtr 提交脏页时,2. i/o线程刷脏时。目前为了不影响mtr的并发选择了后者

一些关键点:

  1. 通过内存的队列去缓存修改的脏页和spaceid,page_id
  2. 不重复记录相同的spaceid,pageid
  3. 通过后台不停的追加写文件,防止内存撑爆
  4. 元信息不单独维护,文件名和头包括必要的信息
  5. 文件头中记录了开始和结束的lsn.
  6. 如果缓存满了会导致flushpage被阻塞,但是这种情况应该很少,
  7. 内存不足时会告警和停止收集脏页,同时会重启clone的流程

redo归档:

  1. 后台归档线程从checkpoint LSN开始归档redo 这个后台线程就是之前的脏页搜集线程
  2. 每次都按块从上次copy的lsn到最新的flush lsn把日志从redo file拷贝到archive file
  3. 拷贝的速度基本上要比redo生成的速度快,为了防止归档日志被覆盖,mtr在归档日志将要被覆盖时会柱塞mtr
  4. 归档日志通过lsn命名
  5. 提供如用户接口实现归档

抽象接口

clone_copy 和 clone_apply,plugin通过调用这两组接口在源和目的之间拷贝文件和内存数据,从而实现拷贝完整的snapshot:

  1. copy_data:
    clone_copy(locator[IN], callback_context[IN], ...)
    callback_context
    clone_file_cbk(from_file_descriptor[IN], copy_length[IN], ...) // 拷贝文件的回调
    clone_buffer_cbk(from_data_buffer[IN], copy_length[IN], ...) // 拷贝脏页的回调
    
  2. apply_data:
clone_apply(locator[IN], callback_context[IN], ...)
clone_apply_file_cbk(to_file_descriptor[IN], copy_length[IN], ...) // 把copy的数据写到目的数据目录

接口调用示意图 call_interfaces

远程克隆

语法:

CLONE INSTANCE FROM USER@HOST:PORT 
     IDENTIFIED BY ''
     DATA DIRECTORY [=]''
     [REQUIRE [NO] SSL];

克隆步骤:

  1. 创建空实例[mysqld–initialize]
  2. 启动目的实例
  3. 连接源实例
  4. 从源实例clone数据到目的实例
       SQL > INSTALL PLUGIN CLONE
       SQL > CLONE REMOTE INSTANCE
    
  5. 用在clone的数据目录上重启

具体实现: 远程克隆可以理解为将本地克隆的数据以流的方式发送到远端从而写入远端的目的数据目录完成克隆,具体流程如下图所示:                                           clone源和目的交互示意图 clone_sequence

CLONE PROTOCOL说明:

  1. COM_INIT: 协商版本号,存储引擎发起clone操作,源端(DONER)的locater会返回给目的端(RECIPIENT),locater是一个innodb存储引擎内部表示snapshot状态的逻辑指针,协商版本号未来可以支持不同版本间的clone;
  2. COM_ATTACH: 新的slave线程和当前clone线程相关联,用于并发处理;
  3. COM_REINIT: 用于当出现类似网络错误时重启clone,clone主线程等待所有辅助线程退出后,会把stage/chunk/block信息发送给源端重新clone;
  4. COM_EXECUTE: 开始传输数据到客户端,源端流式的将snapshot通过网络发送到目的端,数据通过这个com的回包连续不断的发送;   A. COM_RES_LOCS : 目的端发送给源端的locater信息;   B. COM_RES_DATA_DESC :用于描述接下来数据包的描述符,第一部分是存储引擎在cloneplugin的位置,用于  clone plugin调用正确的存储引擎,第二部分就和具体的存储引擎相关,innodb有如下的内容:      1. State information      2. Task information      3. File metadata      4. Location next data block - file index and offset    C. COM_RES_DATA :clone的原始数据;    D. COM_RES_COMPLETE : 克隆成功完成;    E. COM_RES_ERROR : 克隆报错退出,源端通过多次DESC+DATA直到snapshot发送完毕,然后发送一个 CLONE_COM_END表明结束;
  5. COM_ACK:用于目的端通知源端可以安全的切换snapshot状态了,它同时也可以用于目的端反馈给源端错误信息,因为COM_EXECUTE一直是源端发送数据到目的端;
  6. COM_EXIT: 退出plugin返回到普通的服务器协议;

关键类

这儿主要涉及remote clone: clone_classes

sql层:

  1. Sql_cmd_clone: 客户端处理sql(clone instance)和服务端处理COM_XXX命令
  2. Clone_handler和Mysql_clone:调用plugin的具体实现响应sql层处理
  3. myclone::Client: clone接收端的处理逻辑
  4. myclone::Server: 被clone端的处理逻辑
  5. clone_protocol: 定义的一组接口用于client和server rpc通信 6.  Clone_interface_t: plugin调用的引擎层接口
  6. ha_innobase(clone_interface): innodb引擎实现的Clone_interface_t

innodb层:

  1. Clone_Sys:管理所有的Clone Handle
  2. Clone_Handle: 处理一次innodb clone请求(客户端和服务器端调用不同的接口)
  3. Clone_Task_Manager: 管理一个innodb clone请求的所有任务(多个task可以并行处理)
  4. Clone_Task: 标识一个任务
  5. Clone_Snapshot: 管理doner的一个一致性状态(见前文注释)
  6. Page_Arch_Client_Ctx, Arch_Page_sys, Arch_Block: 提供clone脏页搜集功能 7.Log_Arch_Client_Ctx, Arch_Log_Sys: 提供clone归档redo功能

主要逻辑

1. 发起克隆端(RECIPIENT/CLIENT)

用户发起clone instance 语法解析为SQLCOM_CLONE命令,同时构造出Sql_cmd_clone对象,在mysql_exeucte_command中执行,然后通过plugin_clone_init初始化和mysql_declare_plugin中的clone_descriptor设置的Mysql_clone对象调用plugin_clone_remote_client发起远端clone请求。

int mysql_execute_command(THD *thd, bool first_level) { 
    ... ...
    case SQLCOM_CLONE:
      ... ...
      DBUG_ASSERT(lex->m_sql_cmd != nullptr);
      res = lex->m_sql_cmd->execute(thd); // 调用Sql_cmd_clone::execute
      break;
    ... ...
}

bool Sql_cmd_clone::execute(THD *thd) {
  ... ...
  auto err = m_clone->clone_remote_client(// 调用Clone_handler::clone_remote_client
      thd, m_host.str, static_cast<uint>(m_port), m_user.str, m_passwd.str,
      m_data_dir.str, ssl_mode);
  clone_plugin_unlock(thd, m_plugin);
  m_clone = nullptr;
  ... ...
 }
 
 int Clone_handler::clone_remote_client(THD *thd, const char *remote_host,
                                       uint remote_port,
                                       const char *remote_user,
                                       const char *remote_passwd,
                                       const char *data_dir,
                                       enum mysql_ssl_mode ssl_mode) {
  
   ...  ...
  error = m_plugin_handle->clone_client( // 调用plugin_clone_remote_client
      thd, remote_host, remote_port, remote_user, remote_passwd, dir_ptr, mode);
   ... ...
}

plugin_clone_remote_client主要构造一个myclone::Client对象,然后调用它的clone方法,客户端协议主要就在这个方法中实现,它首先调用connect_remote 发送COM_CLONE命令给服务端表明要进行一次clone操作,然后通过remote_command执行其他的协议命令和处理服务器返回包。

static int plugin_clone_remote_client(THD *thd, const char *remote_host,
                                      uint remote_port, const char *remote_user,
                                      const char *remote_passwd,
                                      const char *data_dir, int ssl_mode) {
     ...  ...

  myclone::Client clone_inst(thd, &client_share, 0, true);

  error = clone_inst.clone(); // 调用 myclone::Client::clone

  return (error);
}
// 客户端协议主流程 可以参见前文的说明
int Client::clone() { 
  ... ...
  do {
    ... ... 
    err = connect_remote(restart, false); // 连接服务端执行COM_CLONE命令
    ... ...

    /* Make another auxiliary connection for ACK */
    err = connect_remote(restart, true);

    ... ...
    auto rpc_com = is_master() ? COM_INIT : COM_ATTACH;
    ... ...

    /* Negotiate clone protocol and SE versions */
    err = remote_command(rpc_com, false);// RPC主线程执行COM_INIT, 并发线程执行COM_ATTACH
    ... ...

    /* Execute clone command */
    if (err == 0) {
      ... ...
      err = remote_command(COM_EXECUTE, false);// RPC执行拷贝命令
      ... ...
}
int Client::connect_remote(bool is_restart, bool use_aux) {
  ... ...
  while (true) {
    /* Connect to remote server and load clone protocol. */
    m_conn = mysql_service_clone_protocol->mysql_clone_connect(// 调用协议实现mysql_clone_connect
        m_server_thd, m_share->m_host, m_share->m_port, m_share->m_user,
        m_share->m_passwd, &ssl_context, &conn_socket);

    if (m_conn != nullptr) {
      break;
    }
  ... ...
  }
  
 DEFINE_METHOD(MYSQL *, mysql_clone_connect,
              (THD * thd, const char *host, uint32_t port, const char *user,
               const char *passwd, mysql_clone_ssl_context *ssl_ctx,
               MYSQL_SOCKET *socket)) {
  ... ...
  /* Load clone plugin in remote */
  auto result = simple_command(mysql, COM_CLONE, nullptr, 0, 0); // 发送COM_CLONE给服务端
  ... ...
  }

remote_command实际调用定义的一组协议接口clone_protocol发送命令等,协议服务的定义见宏BEGIN_SERVICE_IMPLEMENTATION(mysql_server, clone_protocol), 然后处理服务器返回的协议包, 比如处理COM_RES_DATA_DESC和COM_RES_DATA时调用引擎的clone_apply接口把数据写入。innodb引擎对应innodb_clone_apply,后面会详细介绍:

// 客户端RPC实现
int Client::remote_command(Command_RPC com, bool use_aux) {
  ... ...
  /* Send remote command */
  err = mysql_service_clone_protocol->mysql_clone_send_command(//调用协议发送命令
      get_thd(), conn, !use_aux, command, m_cmd_buff.m_buffer, cmd_buff_len);
  if (err != 0) {
    return (err);
  }
  /* Receive response from remote server */
  err = receive_response(com, use_aux); // 处理回包
  ... ..
  }
  
  int Client::receive_response(Command_RPC com, bool use_aux) {
   ... ...
   err = handle_response(packet, length, saved_err, skip_apply, last_packet);
   ... ...
  }
  
  int Client::handle_response(const uchar *packet, size_t length, int in_err,
                            bool skip_loc, bool &is_last) {
  switch (res_com) { //每个包的含义可以参见前文的说明
    ... ... 
    case COM_RES_DATA_DESC:
      /* Skip processing data in case of an error till last */
      if (in_err == 0) {
        err = set_descriptor(packet, length); // 处理元信息包
      }
      break;
     ... ... 
    case COM_RES_DATA: // 数据包交给apply处理
      /* Allow data packet to skip */
      if (in_err != 0) {
        break;
      }
     ... ...
 }
 
 int Client::set_descriptor(const uchar *buffer, size_t length) {
   ... ... 
   Ha_clone_cbk *clone_callback = new Client_Cbk(this);
   ... ...
   // 调用引擎层clone_apply把clone数据写到文件
   err = hton->clone_interface.clone_apply(loc->m_hton, get_thd(), loc->m_loc,
                                          loc->m_loc_len, m_tasks[loc_index], 0,
                                          clone_callback); 
   ... ...
 }

2. 被克隆端(DONER/SERVER)

服务器端在收到COM_CLONE请求后首先构造一个Sql_cmd_clone, 同时执行它的execute_server。和客户端类似它最终会调用clone plugin初始化决定的plugin_clone_remote_server处理服务器端的逻辑。

bool dispatch_command(THD *thd, const COM_DATA *com_data,
                      enum enum_server_command command) {
  ... ...
    case COM_CLONE: {// 执行COM_CLONE命令初始化 clone_cmd
      thd->status_var.com_other++;

      /* Try loading clone plugin */
      clone_cmd = new (thd->mem_root) Sql_cmd_clone();
  ... ...
    /* After sending response, switch to clone protocol */
  if (clone_cmd != nullptr) {
    DBUG_ASSERT(command == COM_CLONE);
    error = clone_cmd->execute_server(thd); // 调用Sql_cmd_clone::execute_server
  }
}

bool Sql_cmd_clone::execute_server(THD *thd) {
  ... ...
  auto err = m_clone->clone_remote_server(thd, sock);// 调用Clone_handler::clone_remote_server
  ... ...
}

int Clone_handler::clone_remote_server(THD *thd, MYSQL_SOCKET socket) {
  auto err = m_plugin_handle->clone_server(thd, socket); //调用plugin_clone_remote_server
  return err;
}

plugin_clone_remote_server首先构造一个myclone::Server的对象,服务端的主要逻辑就在它的clone接口中实现,clone接口同样调用协议服务接收命令然后根据命令类型做相应的处理,比如是客户端发送的COM_EXECUTE命令,它就找到对应的locater然后调用locater关联的引擎clone_copy接口拷贝数据,innodb引擎就调用innodb_clone_copy,具体逻辑见后面的介绍。

tatic int plugin_clone_remote_server(THD *thd, MYSQL_SOCKET socket) {
  myclone::Server clone_inst(thd, socket);

  auto err = clone_inst.clone(); // myclone::Server::clone 服务器端主逻辑

  return (err);
}

int Server::clone() {
  int err = 0;

  while (true) {
    ... ...
    // 协议层接收命令,定义DEFINE_METHOD(MYSQL *, mysql_clone_get_command ...
    err = mysql_service_clone_protocol->mysql_clone_get_command(
        get_thd(), &command, &com_buf, &com_len);

    ... ...

    if (err == 0) {
      err = parse_command_buffer(command, com_buf, com_len, done);// 处理命令
    }
    ... ...
 }
 // 服务器端处理COM_XXX逻辑,见前文具体说明
 int Server::parse_command_buffer(uchar command, uchar *com_buf, size_t com_len,
                                 bool &done) {
   ... ...
   case COM_EXECUTE: {
      ... ...

      Server_Cbk clone_callback(this);
      // 调用引擎层clone_copy拷贝snapshot
      err = hton_clone_copy(get_thd(), get_storage_vector(), m_tasks,
      ... ...
    }
    ... ...
 }
 
 int hton_clone_copy(THD *thd, Storage_Vector &clone_loc_vec,
                    Task_Vector &task_vec, Ha_clone_cbk *clone_cbk) {
  uint index = 0;

  for (auto &loc_iter : clone_loc_vec) {
    DBUG_ASSERT(index < task_vec.size());
    clone_cbk->set_loc_index(index);
    // 如果是innodb, 调用innodb_clone_copy接口进行数据拷贝
    auto err = loc_iter.m_hton->clone_interface.clone_copy(
        loc_iter.m_hton, thd, loc_iter.m_loc, loc_iter.m_loc_len,
        task_vec[index], clone_cbk);

    if (err != 0) {
      return (err);
    }
    index++;
  }

  return (0);
}

3.innodb层copy

innodb层copy先在Clone_Sys中找到对应的任务Clone_Handle(可能不止一个clone任务), 然后调用Clone_Handle的copy接口进行具体的拷贝,拷贝与它绑定的Clone_Snapshot直到CLONE_SNAPSHOT_DONE,通过move_to_next_state驱动Clone_Snapshot切换状态拷贝不同的数据。

int innodb_clone_copy(handlerton *hton, THD *thd, const byte *loc, uint loc_len,
                      uint task_id, Ha_clone_cbk *cbk) {
  cbk->set_hton(hton);

  /* Get clone handle by locator index. */
  auto clone_hdl = clone_sys->get_clone_by_index(loc, loc_len);

  auto err = clone_hdl->check_error(thd);
  if (err != 0) {
    return (err);
  }

  /* Start data copy. */
  err = clone_hdl->copy(thd, task_id, cbk); // 调用 Clone_Handle::copy进行拷贝
  clone_hdl->save_error(err);

  return (err);
}
// copy snapshot的几种状态直到DONE
int Clone_Handle::copy(THD *thd, uint task_id, Ha_clone_cbk *callback) {
  ... ...
  /* Adjust block size based on client buffer size. */
  auto snapshot = m_clone_task_manager.get_snapshot(); // 获取snapshot
  while (m_clone_task_manager.get_state() != CLONE_SNAPSHOT_DONE) {
  ... ...
      /* Send blocks from the reserved chunk. */
      err = process_chunk(task, current_chunk, current_block, callback);
  ... ...
      /* Next state is decided by snapshot for Copy. */
      err = move_to_next_state(task, callback, nullptr); // 切换snapshot状态
  ... ...
  }
}

状态切换实际在Clone_Snapshot::change_state中进行,根据不同的目标状态Clone_Snapshot做相应的初始化: 比如CLONE_SNAPSHOT_FILE_COPY阶段要打开脏页收集,查找要copy的文件,具体文件拷贝的细节请查看源码,这儿就不在赘述。CLONE_SNAPSHOT_PAGE_COPY需要开始redo归档和搜集要发送的脏页等。

int Clone_Handle::move_to_next_state(Clone_Task *task, Ha_clone_cbk *callback,
                                     Clone_Desc_State *state_desc) {
  ... ...
  // 调用 Clone_Task_Manager::change_state
  auto err = m_clone_task_manager.change_state(task, state_desc, next_state,
                                               alert_callback, num_wait);
  ... ...
}

nt Clone_Task_Manager::change_state(Clone_Task *task,
                                     Clone_Desc_State *state_desc,
                                     Snapshot_State new_state,
                                     Clone_Alert_Func cbk, uint &num_wait) {
  ... ...
  // 调用 Clone_Snapshot::change_state
  err = m_clone_snapshot->change_state(
      state_desc, m_next_state, task->m_current_buffer,
      task->m_buffer_alloc_len, cbk, num_pending);
  ... ...
}

int Clone_Snapshot::change_state(Clone_Desc_State *state_desc,
                                 Snapshot_State new_state, byte *temp_buffer,
                                 uint temp_buffer_len, Clone_Alert_Func cbk,
                                 uint &pending_clones) {
   ... ...
   /* Initialize the new state. */
  auto err = init_state(state_desc, temp_buffer, temp_buffer_len, cbk);
  ... ...
}

int Clone_Snapshot::init_state(Clone_Desc_State *state_desc, byte *temp_buffer,
                               uint temp_buffer_len, Clone_Alert_Func cbk) {
   ... ...
   // snapshot的切换状态,file_copy page_copy redo_copy
   switch (m_snapshot_state) {
     ... ...
     case CLONE_SNAPSHOT_FILE_COPY:
       err = init_file_copy();
       m_monitor.change_phase();
       ... ...
     case CLONE_SNAPSHOT_PAGE_COPY:
       err = init_page_copy(temp_buffer, temp_buffer_len);
       m_monitor.change_phase();
       ... ...
     case CLONE_SNAPSHOT_REDO_COPY:
       err = init_redo_copy(cbk);
       m_monitor.change_phase();
       ... ...
}

int Clone_Snapshot::init_file_copy() {
  ... ...
  /* Start modified Page ID Archiving */
    err = m_page_ctx.start(false, nullptr); // 开启脏页收集
  /* Iterate all tablespace files and add persistent data files. */
  auto error = Fil_iterator::for_each_file( // 要copy的文件
      include_log, [&](fil_node_t *file) { return (add_node(file)); });
   ... ...
}

int Clone_Snapshot::init_page_copy(byte *page_buffer, uint page_buffer_len) {
   ... ...
   /* Start Redo Archiving */
    err = m_redo_ctx.start(m_redo_header, m_redo_header_size); // 开始归档redolog
    ... ...
   /* Stop modified page archiving. */
   err = m_page_ctx.stop(nullptr);
   ... ...
   // 获取要发送的pages
   err = m_page_ctx.get_pages(add_page_callback, context, page_buffer,
                             page_buffer_len);
    ... ...
}

int Clone_Snapshot::init_redo_copy(Clone_Alert_Func cbk) {
  ... ...
  /* Stop redo archiving even on error. */
  auto redo_error = m_redo_ctx.stop(m_redo_trailer, m_redo_trailer_size,
                                    m_redo_trailer_offset); // 停止归档redo
  ... ...
  redo_error = m_redo_ctx.get_files(add_redo_file_callback, context);
  ... ...
}

4.innodb层apply

apply的主要工作就是接收服务器端发送的数据写到对应的文件里,它同样也是先根据index找到对应的Clone_Handle, 然后Clone_Handle根据具体的服务器回包类型做相应的处理,根据meta信息做好写数据的准备, 把CLONE_DESC_DATA数据接收然后写入文件

int innodb_clone_apply(handlerton *hton, THD *thd, const byte *loc,
                       uint loc_len, uint task_id, int in_err,
                       Ha_clone_cbk *cbk) {
  ... ...
  /* Apply data received from callback. */
  err = clone_hdl->apply(thd, task_id, cbk);
  ... ...
}

int Clone_Handle::apply(THD *thd, uint task_id, Ha_clone_cbk *callback) {
  ... ...
  switch (header.m_type) {
    case CLONE_DESC_TASK_METADATA:
      err = apply_task_metadata(task, callback);
      break;

    case CLONE_DESC_STATE:
      err = apply_state_metadata(task, callback);
      break;

    case CLONE_DESC_FILE_METADATA:
      err = apply_file_metadata(task, callback);
      break;

    case CLONE_DESC_DATA:
      err = apply_data(task, callback); // apply具体数据
      break;

    default:
      ut_ad(false);
      break;
  }
}

int Clone_Handle::apply_data(Clone_Task *task, Ha_clone_cbk *callback) {
  ... ...
  /* Receive data from callback and apply. */
  err = receive_data(task, data_desc.m_file_offset, data_desc.m_file_size,
                     data_desc.m_data_len, callback);
  ... ...
}

5. 脏页收集

在Clone_Snapshot FILE_COPY状态的准备阶段会调用Page_Arch_Client_Ctx::start开启脏页收集,主要获取当前在线redo日志的lsn然后告诉buffer pool可以开始进行脏页的收集了,同时还要开启一个后台线程把内存收集的脏页在写满的情况下append到文件。

// Clone_Snapshot调用脏页收集客户端接口
int Page_Arch_Client_Ctx::start(bool recovery, uint64_t *start_id) {
  ... ...
  /* Start archiving. */
  err = arch_page_sys->start(&m_group, &m_last_reset_lsn, &m_start_pos,
                             m_is_durable, reset, recovery);
  ... ...
}

int Arch_Page_Sys::start(Arch_Group **group, lsn_t *start_lsn,
                         Arch_Page_Pos *start_pos, bool is_durable,
                         bool restart, bool recovery) {
   ... ...
   // 收集开始lsn时在线日志当前最新分配的lsn
   log_sys_lsn = (recovery ? m_last_lsn : log_get_lsn(*log_sys));
   /* Enable/Reset buffer pool page tracking. */
        set_tracking_buf_pool(log_sys_lsn); // 告诉刷脏开始统计sp_id,page_id
   ... ...
   auto err = start_page_archiver_background(); // 开启后台线程归档收集的sp_id, page_id
   ... ...
   if (!recovery) {
    /* Request checkpoint */
    log_request_checkpoint(*log_sys, true); // 保证归档结束一定有个checkpoint
  }
}

后台刷脏在判断track_page_lsn设置的情况下就会调用Arch_Page_Sys::track_page接口进行脏页收集,记录脏页的space_id和page_id,结束的lsn为最近的一次checkpoint的LSN,归档redo从这个LSN开始。

// 在刷脏过程中收集
ibool buf_flush_page(buf_pool_t *buf_pool, buf_page_t *bpage,
                     buf_flush_t flush_type, bool sync) {
  ... ...
  if (!fsp_is_system_temporary(bpage->id.space()) &&
        buf_pool->track_page_lsn != LSN_MAX) { // start 设置了start_lsn
      ... ...
      frame_lsn = mach_read_from_8(frame + FIL_PAGE_LSN); // 为了过滤重复的id
      // 调用Arch_Page_Sys::track_page
      arch_page_sys->track_page(bpage, buf_pool->track_page_lsn, frame_lsn,
                                false);
    }
    ... ....
}

void Arch_Page_Sys::track_page(buf_page_t *bpage, lsn_t track_lsn,
                               lsn_t frame_lsn, bool force) {
   ... ...
   if (!force) { // 去重逻辑
    /* If the frame LSN is bigger than track LSN, it
    is already added to tracking list. */
    if (frame_lsn > track_lsn) {
      return;
    }
  }
  ... ...
   // 调用Arch_Block::add_page搜集 收集space_id, page_id
  if (!cur_blk->add_page(bpage, &m_write_pos)) {
        /* Should always succeed. */
        ut_ad(false);
      }
  ... ...
}
// 由start start_page_archiver_background 开启的后台线程
/** Archiver background thread */
void page_archiver_thread() {
  ... ...
  while (true) {
    ... ... 
      /* Archive in memory data blocks to disk. */
      page_abort = arch_page_sys->archive(&page_wait); // 归档内存中的ids
    ... ...
}

bool Arch_Page_Sys::archive(bool *wait) {
  ... ...
  db_err = flush_blocks(wait);
  ... ...
}

dberr_t Arch_Page_Sys::flush_blocks(bool *wait) {
  ... ...
  err = flush_inactive_blocks(cur_pos, end_pos);
  ... ...
}

dberr_t Arch_Page_Sys::flush_inactive_blocks(Arch_Page_Pos &cur_pos,
                                             Arch_Page_Pos end_pos) {
   ... ...
   while (cur_pos.m_block_num < end_pos.m_block_num) {
   ... ...
   // 调用 Arch_Block::flush
   err = cur_blk->flush(m_current_group, ARCH_FLUSH_NORMAL);
   ... ...
   }
}
// 结束收集
int Arch_Page_Sys::stop(Arch_Group *group, lsn_t *stop_lsn,
                        Arch_Page_Pos *stop_pos, bool is_durable) {
  ... ...
  *stop_lsn = m_latest_stop_lsn; // 最近一次checkpoint的LSN
  ... ...
}

6.归档redo

Clone_Snapshot在拷贝脏页的准备阶段开启redo归档,它主要的工作就是开启一个后台线程,从最近的一个Checkpoint的LSN开始拷贝线上redo到归档文件,一直到脏页拷贝完。

// Clone_Snapshot 归档REDO时调用
int Log_Arch_Client_Ctx::start(byte *header, uint len) {
  ... ...
  auto err = arch_log_sys->start(m_group, m_begin_lsn, header, false);
  ... ...
}

int Arch_Log_Sys::start(Arch_Group *&group, lsn_t &start_lsn, byte *header,
                        bool is_durable) {
  ... ...
  auto err = start_log_archiver_background(); // 开启归档后台线程
  start_lsn = log_sys->last_checkpoint_lsn; // 开始为上一个Checkpoint lsn
  ... ...
}

/** Archiver background thread */
void log_archiver_thread() {
  ... ...
  while (true) {
    /* Archive available redo log data. */
    log_abort = arch_log_sys->archive(log_init, &log_file_ctx, &log_arch_lsn,
                                      &log_wait);
    ... ...
  }
}

bool Arch_Log_Sys::archive(bool init, Arch_File_Ctx *curr_ctx, lsn_t *arch_lsn,
                           bool *wait) {
    ... ...
    /* Copy data from system redo log files to archiver files */
    err = copy_log(curr_ctx, arch_len);
    ... ...
}

dberr_t Arch_Log_Sys::copy_log(Arch_File_Ctx *file_ctx, uint length) {
   ... ...
   /* Copy log data into one or more files in archiver group. */
  while (length > 0) {
    ... ...
     err =
        curr_group->write_to_file(file_ctx, nullptr, write_size, false, false);
    ... ...
}

int Arch_Log_Sys::stop(Arch_Group *group, lsn_t &stop_lsn, byte *log_blk,
                       uint32_t &blk_len) {
    ... ...
    // 最新的lsn
    if (log_blk == nullptr) {
      ... ...
      stop_lsn = m_archived_lsn.load();
    } else {
      /* Get the current LSN and trailer block. */
      log_buffer_get_last_block(*log_sys, stop_lsn, log_blk, blk_len);
    ... ...
    }
    ... ...
 }

参考

https://dev.mysql.com/worklog/task/?id=9209https://dev.mysql.com/worklog/task/?id=9210https://dev.mysql.com/worklog/task/?id=9211https://dev.mysql.com/worklog/task/?id=11636https://dev.mysql.com/worklog/task/?id=12827https://mysqlserverteam.com/clone-create-mysql-instance-replica/

MSSQL · 最佳实践 · 启用即时文件初始化

$
0
0

问题引入

某天,假设您所在公司的生产订单库大库(假设超过1TB)Crash掉了,此时,做为DBA的您,不得不从备份文件中快速将数据库还原出来。但是您发现,这个数据库还原操作初期长达小时时间始化在做数据文件零填充初始操作。于是,您想能否跳过这个数据库文件的初始化操作,快速的还原数据库呢?答案是肯定的,就是我们今天要分享的SQL Server即时文件初始化技术。

什么是即时文件初始化

在即时文件初始化技术面世之前(或者未开启即时文件初始化)的场景中,SQL Server数据库数据文件和日志文件的初始化操作目的是覆盖之前删除的文件遗留在磁盘上的现有数据,操作的方法是通过零填充(用零填充)数据和日志文件来达到目的的,如果数据库较大(几十上百GB)的话,会导致这个初始化动作耗时很长。

因此,从Windows XP(Windows Server 2003)开始,NTFS文件系统加入了新特性,允许跳过用零填充文件的初始化步骤,叫即时文件初始化。SQL Server 2005引入了即时文件初始化的新特性,可以在瞬间对数据文件进行初始化,以避免零填充操作。即时文件初始化可以快速执行文件创建操作,无论数据库文件的大小有多大。

为什么需要即时文件初始化

一般情况下,操作系统中文件创建的请求都是小文件创建操作,比如:新建一个Word文档或者Excel表格。对于这种小文件创建初始化操作来说,性能提升几乎无感知。但是,对于像数据库这种大文件的频繁操作来说,即时文件初始化在以下场景就显得尤为重要了:

 创建数据库。

 现有数据库添加数据或日志文件。

 增大数据库现有文件的大小。

 数据库文件自动增长操作。

 还原数据库或文件组。

这些数据库(或者说数据库文件)的操作,无论数据库文件大小多大,都可以即时完成,而无需等待零填充操作。

假设,某一天您的生产环境的订单库Crash掉了,而不幸的是这个库恰好是一个超大库(假设超过1TB)。这时候,您做为DBA,不得不选择从备份文件中争分夺秒的将数据库还原出来,如果在没有开启即时文件初始化的情况下,要将1TB的文件以零填充,这个初始化操作耗时可能长达小时级别。而在即时文件初始化开启的情况下,这个动作瞬间完成,大大提速了您数据库还原的动作,最大限度减少损失。由此可见,即时文件初始化可以大大提高数据库文件操作的效率,好处是显而易见的。

注意:

只有数据库数据文件才支持即时文件初始化功能。而创建或修改数据库日志文件大小时,不支持即时文件初始化功能,还是将始终零填充该文件。

如何开启即时文件初始化

即时文件初始化功能仅在向SQL Server 服务启动帐户授予了SE_MANAGE_VOLUME_NAME 权限之后才可用 。因此,我们只需要授予SQL Server启动账号相应的权限即可。方法如下:

 找到SQL Server启动账号

 授予SE_MANAGE_VOLUME_NAME

 重启SQL Server

找到SQL Server启动账号

打开SQL Server管理配置工具 -> SQL Server Services -> 右侧找到 SQL Server (MSSQLSERVER) -> 在Log On As列中的用户名就是SQL Server的启动账号。

01.png

授予SE_MANAGE_VOLUME_NAME

secpol.msc -> Security Settings -> Local Policies -> User Rights Assignment -> 双击“Perform volume maintenance tasks” -> Add User or Group -> NETWORK SERVICES -> OK

02.png

重启SQL Server

开启即时文件初始化的最后一步是重启SQL Server Service。

测试即时文件初始化

即时文件初始化是操作系统层面对数据库文件的行为,确实不太好观察和验证,好在SQL Server中有未公开的Trace Flag 3004,我们只需要打开3004和3006,将其输出到SQL Server错误日志文件中即可验证。

DBCC TRACEON (3004 ,3605 ,-1)
GO

CREATE DATABASE TestInstallInit
GO

EXEC sys.sp_readerrorlog
GO

DBCC TRACEOFF (3004 ,3605 ,-1)
GO

如下截图所示:

03.png

从SQL Server错误日志中,我们已经观察到Zeroing动作只在事务日志文件上发生,数据文件未见Zeroing。因此,即时文件初始化已经生效。

最后总结

即时文件初始化可以大大加速SQL Server数据库数据文件操作,包括创建数、修改和还原数据库等操作,是SQL Server数据库管理与运维需要重点考虑的优化点。

PgSQL · 特性分析 · 浅析PostgreSQL 的JIT

$
0
0

背景

估计很多同学看过之前的月报PgSQL · 特性分析· JIT 在数据仓库中的应用价值,对JIT(just in time)和LLVM(Low Level Virtual Machine)有了一定的了解。概括地来说:

  • JIT 指的是即时编译,即程序在运行过程中即时进行编译,其中可以把编译的中间代码缓存或者优化。相对于静态编译代码,即时编译的代码可以处理延迟绑定并增强安全性。
  • LLVM 就提供了一种在程序运行时编译执行代码的程序框架,它对外提供API,使实现JIT 变得更加简单。

PostgreSQL 社区从2016年就开始对JIT 的实现进行了讨论,详见邮件列表

该邮件中解释了PostgreSQL 需要JIT 技术的原因。因为PostgreSQL 代码中实现的都是通用的逻辑,这就导致在执行过程中可能造成大量不必要的跳转和代码分支执行,继而造成大量不必要的指令执行,造成CPU 的压力。而使用JIT 技术可以将代码扁平化(inline)执行,直接调用对应的函数,而且如果已经知道具体输入,可以直接删除掉很多间接代码的执行。

此外,邮件中也说明了在PostgreSQL 中实现JIT 选择LLVM 的理由,概括起来就是LLVM 成熟度更高,更稳定,license 更友好,支持C 语言。

在PostgreSQL 11 的版本中实现了基于LLVM 的JIT,本文主要是浅析JIT 在PostgreSQL 11 中的使用。

PostgreSQL 中JIT 的实现概述

PostgreSQL 11 中实现的JIT,是把对应的JIT 的提供者封装成了一个外部依赖库。这避免了JIT 对主体代码的侵入,用户可以按需开启/关闭JIT 功能,而且还能通过进一步的抽象支持后期扩展不同的JIT 解决方案(目前使用的是LLVM)。不过这样带来的问题就是各个部分使用JIT 技术编译的代码必须和原来的代码位置分开,这样代码易读性可能有所降低。

作为支持JIT 的第一个正式版本,PostgreSQL 11 只实现了一部分功能,下文将简单讲解下各个功能。

PostgreSQL 中支持的JIT 功能

在之前的月报PgSQL · 特性分析· JIT 在数据仓库中的应用价值中提出了在数据库实现中LLVM 可优化的点,其中包括:

  • 优化频繁调用的存取层
  • 表达式计算
  • 优化执行器流程

PostgreSQL 11 中基本上也实现了这几方面的优化,但是略有不同,目前包含JIT accelerated operations,inlining,optimization。

JIT accelerated operations

利用LLVM 的特性,PostgreSQL 定制化地实现了两方面的加速操作,包括表达式计算优化(expression evaluation)和元组变形优化(tuple deforming)。

表达式计算优化可以针对WHERE 条件,agg 运算等实时将表达式的路径编译为具体的代码执行,在此过程中大量的不必要的调用和跳转会被优化掉。

元组变形优化可以将具体元组转化为其在内存中运行的状态,然后根据元组每列的具体类型和元组中列的个数实时编译为具体的代码执行,在此过程中不必要的代码分支会被优化掉。

表达式和元组操作经常会造成分析型场景下的CPU 性能瓶颈,加速这两方面可以提高PostgreSQL 的分析能力。但是除了这两方面,其他的场景也可以进行JIT 的优化,例如元组排序,COPY 解析/输出以及查询中其他部分等等,这些目前没有实现,社区计划将在后续版本中实现。

inlining

PostgreSQL 源码中含有大量通用的代码,执行时会经过很多不必要的函数调用和操作。为了提高执行效率,将通用代码重写或维护两份很明显是不可取的。而JIT 技术带来的好处之一就是执行的时候将代码扁平化,去掉不必要的函数调用和操作。以LLVM 为例,Clang 编译器可以生成LLVM IR(中间表示代码)并优化,这在一定意义上就代表了两份代码。在PostgreSQL 中LLVM IR 使用的是bitcode(二进制格式),对应安装在$pkglibdir/bitcode/postgres/ 中,而对应插件的bitcode 会安装在$pkglibdir/bitcode/[extension]/ 中,其中extension 为插件名。

optimization

LLVM 中实现了对产生的中间表示代码的优化,这一定程度上也会提升数据库查询的执行速度。但是该过程本身是有相应的代价的,有些优化可能代价比较低,可以很好地提高性能,而有些可能只有在大的查询中才会体现其提高性能的作用。所以,在PostgreSQL 中定制了一些GUC 参数来限制JIT 功能的开启,详见下文。

与JIT 相关的GUC 参数

在使用JIT 的过程中,有以下几个GUC 参数与之相关,分别是:

  • jit,该参数为on 的时候代表打开JIT,反之则是不打开JIT。非常棒的一点是这个参数可以在session 中设置,这就给用户更大的主动权。目前PostgreSQL 11 中默认为off,对此社区也有相关的讨论(参见邮件列表),一方面希望jit 参数能够默认打开,让用户快速的使用起来,可以更多地发现问题,另一方面这样对直接升级上来的用户可能会带来一定的困扰,因为JIT 在某些场景下会带来额外的时间开销。所以最后讨论的结果是PostgreSQL 11 中该参数默认为off,而master 分支上该参数默认为on。
  • jit_provider,该参数表示提供JIT 的依赖库,默认为llvmjit。其实目前PostgreSQL 11 也只实现了llvmjit 一种方式。如果填写了不存在的依赖库,JIT 不会生效,也没有error 产生。
  • jit_above_cost,表示超过多少cost 的查询才会使用JIT 功能,其中不包含开销比较大的optimization。因为JIT 会增加一定的开销,所以这个参数可以使得满足要求的查询使用JIT,这样更大概率会起到加速的效果。默认为100000,如果设置为-1 则关闭JIT。
  • jit_inline_above_cost,表示超过多少cost 的查询使用JIT 的inline 功能。默认为500000,-1则关闭inline 功能。如果把这个值设置的比jit_above_cost 小,则达到了该cost,JIT 还是不会触发,没有意义。
  • jit_optimize_above_cost,表示超过多少cost 的查询使用JIT 的optimization 功能。默认为500000,-1则关闭优化功能。和jit_inline_above_cost 一样,如果把这个值设置的比jit_above_cost 小,没有意义。建议该值设置的比jit_inline_above_cost 大,这样可以在触发inline 功能后,开启optimization 功能。

可以看出,因为目前JIT 功能开启所需要的代价没有很好的办法进行建模,也没有很好的方法来估计,所以导致JIT 功能无法作为代价估计模型中一种可量化的代价。目前实现的策略是按照查询的代价来一刀切是否使用JIT 相应功能,还算是比较简单有效。但是,这并不是特别的优雅。很有可能只有某个部分的查询计划更适合使用JIT 功能。不过要想实现查询的某个部分使用JIT 功能需要一些额外的信息输入和判断,这带来的代价是否足够小也是存疑的。

直到这里,我们基本对PostgreSQL 中的JIT 功能有所了解。接下来,我们会讲如何启用JIT,并且以两个例子看下JIT 的效果。

如何启用JIT

如果你是使用RPM 安装的PostgreSQL 11,则需要另行安装postgresql11-llvmjit 包。如果你使用的是源码编译,则需要在configure 阶段增加–with-llvm 选项,同时指定LLVM_CONFIG 变量,即LLVM 包的llvm-config 位置,还需要指定CLANG 变量,即Clang 的路径,举例如下:

./configure --with-llvm LLVM_CONFIG=/opt/rh/llvm-toolset-7/root/usr/bin/llvm-config CLANG=/opt/rh/llvm-toolset-7/root/usr/bin/clang

简单的测试

我们针对JIT 做了下两组简单的测试,加深对其的理解。

先来一组社区邮件中给出的经典测试:

postgres=# select version();
     version
-----------------
 PostgreSQL 11.4
(1 row)

postgres=# create table t1(id integer primary key,c1 integer,c2 integer,c3 nteger,c4 integer,c5 integer,c6 integer,c7 integer,c8 integer,c9 integer);
CREATE TABLE
postgres=# create table t2(id integer primary key,c1 integer,c2 integer,c3 integer,c4 integer,c5 integer,c6 integer,c7 integer,c8 integer,c9 integer);
CREATE TABLE
postgres=# create table t3(id integer primary key,c1 integer not null,c2 integer not null,c3 integer not null,c4 integer not null,c5 integer not null,c6 integer not null,c7 integer not null,c8 integer not null,c9 integer not null);
CREATE TABLE
postgres=# insert into t1 (id,c1,c2,c3,c4,c5,c6,c7,c8) values (generate_series(1,10000000),0,0,0,0,0,0,0,0);
INSERT 0 10000000
postgres=# insert into t2 (id,c2,c3,c4,c5,c6,c7,c8,c9) values (generate_series(1,10000000),0,0,0,0,0,0,0,0);
INSERT 0 10000000
postgres=# insert into t3 (id,c1,c2,c3,c4,c5,c6,c7,c8,c9) values (generate_series(1,10000000),0,0,0,0,0,0,0,0,0);
INSERT 0 10000000
postgres=# vacuum analyze t1;
VACUUM
postgres=# vacuum analyze t2;
VACUUM
postgres=# vacuum analyze t3;
VACUUM
postgres=# set jit=off;
SET
postgres=# explain analyze select sum(c8) from t1;
                                                      QUERY PLAN
----------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=218457.84..218457.85 rows=1 width=8) (actual time=1762.126..1762.126 rows=1 loops=1)
   ->  Seq Scan on t1  (cost=0.00..193457.87 rows=9999987 width=4) (actual time=0.010..820.756 rows=10000000 loops=1)
 Planning Time: 0.242 ms
 Execution Time: 1762.159 ms
(4 rows)

postgres=# explain analyze select sum(c8) from t2;
                                                      QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=218458.08..218458.09 rows=1 width=8) (actual time=1820.825..1820.825 rows=1 loops=1)
   ->  Seq Scan on t2  (cost=0.00..193458.06 rows=10000006 width=4) (actual time=0.011..820.387 rows=10000000 loops=1)
 Planning Time: 0.102 ms
 Execution Time: 1820.855 ms
(4 rows)

postgres=# explain analyze select sum(c8) from t3;
                                                      QUERY PLAN
----------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=208332.23..208332.24 rows=1 width=8) (actual time=1640.345..1640.345 rows=1 loops=1)
   ->  Seq Scan on t3  (cost=0.00..183332.58 rows=9999858 width=4) (actual time=0.011..767.184 rows=10000000 loops=1)
 Planning Time: 0.101 ms
 Execution Time: 1640.374 ms
(4 rows)

postgres=# explain analyze select sum(c2), sum(c3), sum(c4), sum(c5), sum(c6), sum(c7), sum(c8)
from t1;
                                                      QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=368457.64..368457.65 rows=1 width=56) (actual time=2416.711..2416.711 rows=1 loops=1)
   ->  Seq Scan on t1  (cost=0.00..193457.87 rows=9999987 width=28) (actual time=0.022..833.951 rows=10000000 loops=1)
 Planning Time: 0.069 ms
 Execution Time: 2416.755 ms
(4 rows)

postgres=# explain analyze select sum(c2), sum(c3), sum(c4), sum(c5), sum(c6), sum(c7), sum(c8)
from t2;
                                                       QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=368458.17..368458.18 rows=1 width=56) (actual time=2451.844..2451.844 rows=1 loops=1)
   ->  Seq Scan on t2  (cost=0.00..193458.06 rows=10000006 width=28) (actual time=0.019..842.359 rows=10000000 loops=1)
 Planning Time: 0.113 ms
 Execution Time: 2451.890 ms
(4 rows)

postgres=# explain analyze select sum(c2), sum(c3), sum(c4), sum(c5), sum(c6), sum(c7), sum(c8)
from t3;
                                                      QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=358330.10..358330.11 rows=1 width=56) (actual time=2273.825..2273.825 rows=1 loops=1)
   ->  Seq Scan on t3  (cost=0.00..183332.58 rows=9999858 width=28) (actual time=0.017..792.839 rows=10000000 loops=1)
 Planning Time: 0.114 ms
 Execution Time: 2273.865 ms
(4 rows)

postgres=# set jit=on;
SET
postgres=# explain analyze select sum(c8) from t1;
                                                      QUERY PLAN
----------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=218457.84..218457.85 rows=1 width=8) (actual time=1421.869..1421.869 rows=1 loops=1)
   ->  Seq Scan on t1  (cost=0.00..193457.87 rows=9999987 width=4) (actual time=0.024..820.463 rows=10000000 loops=1)
 Planning Time: 0.058 ms
 JIT:
   Functions: 3
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 0.551 ms, Inlining 2.166 ms, Optimization 20.364 ms, Emission 13.673 ms, Total 36.755 ms
 Execution Time: 1422.491 ms
(8 rows)

postgres=# explain analyze select sum(c8) from t2;
                                                      QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=218458.08..218458.09 rows=1 width=8) (actual time=1414.109..1414.109 rows=1 loops=1)
   ->  Seq Scan on t2  (cost=0.00..193458.06 rows=10000006 width=4) (actual time=0.022..818.406 rows=10000000 loops=1)
 Planning Time: 0.058 ms
 JIT:
   Functions: 3
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 0.557 ms, Inlining 2.231 ms, Optimization 20.261 ms, Emission 13.313 ms, Total 36.363 ms
 Execution Time: 1414.733 ms
(8 rows)

postgres=# explain analyze select sum(c8) from t3;
                                                      QUERY PLAN
----------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=208332.23..208332.24 rows=1 width=8) (actual time=1388.406..1388.406 rows=1 loops=1)
   ->  Seq Scan on t3  (cost=0.00..183332.58 rows=9999858 width=4) (actual time=0.023..768.711 rows=10000000 loops=1)
 Planning Time: 0.058 ms
 JIT:
   Functions: 3
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 0.549 ms, Inlining 2.177 ms, Optimization 20.383 ms, Emission 13.440 ms, Total 36.550 ms
 Execution Time: 1389.025 ms
(8 rows)

postgres=# explain analyze select sum(c2), sum(c3), sum(c4), sum(c5), sum(c6), sum(c7), sum(c8) from t1;
                                                      QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=368457.64..368457.65 rows=1 width=56) (actual time=1687.375..1687.375 rows=1 loops=1)
   ->  Seq Scan on t1  (cost=0.00..193457.87 rows=9999987 width=28) (actual time=0.025..830.483 rows=10000000 loops=1)
 Planning Time: 0.072 ms
 JIT:
   Functions: 3
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 0.756 ms, Inlining 2.180 ms, Optimization 26.391 ms, Emission 19.554 ms, Total 48.881 ms
 Execution Time: 1688.213 ms
(8 rows)

postgres=# explain analyze select sum(c2), sum(c3), sum(c4), sum(c5), sum(c6), sum(c7), sum(c8) from t2;
                                                       QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=368458.17..368458.18 rows=1 width=56) (actual time=1682.681..1682.681 rows=1 loops=1)
   ->  Seq Scan on t2  (cost=0.00..193458.06 rows=10000006 width=28) (actual time=0.023..828.408 rows=10000000 loops=1)
 Planning Time: 0.071 ms
 JIT:
   Functions: 3
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 0.726 ms, Inlining 2.176 ms, Optimization 26.306 ms, Emission 19.807 ms, Total 49.015 ms
 Execution Time: 1683.482 ms
(8 rows)

postgres=# explain analyze select sum(c2), sum(c3), sum(c4), sum(c5), sum(c6), sum(c7), sum(c8) from t3;
                                                      QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=358330.10..358330.11 rows=1 width=56) (actual time=1613.259..1613.259 rows=1 loops=1)
   ->  Seq Scan on t3  (cost=0.00..183332.58 rows=9999858 width=28) (actual time=0.020..773.426 rows=10000000 loops=1)
 Planning Time: 0.069 ms
 JIT:
   Functions: 3
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 0.784 ms, Inlining 2.265 ms, Optimization 26.395 ms, Emission 19.780 ms, Total 49.224 ms
 Execution Time: 1614.121 ms
(8 rows)

可以看出:

  • select sum(c8) from t*; 在JIT 开启下大约有25% 左右的性能提升。
  • select sum(c2), sum(c3), sum(c4), sum(c5), sum(c6), sum(c7), sum(c8) from t*; 在JIT 开启下大约有29% 左右的性能提升。

再来一组简单查询开启JIT 后的测试:

postgres=# create table test (id serial);
CREATE TABLE
postgres=# insert INTO test (id) select * from generate_series(1, 10000000);
INSERT 0 10000000
postgres=# set jit=off;
SET
postgres=# explain  select count(*) from test;
                                       QUERY PLAN
-----------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=97331.43..97331.44 rows=1 width=8)
   ->  Gather  (cost=97331.21..97331.42 rows=2 width=8)
         Workers Planned: 2
         ->  Partial Aggregate  (cost=96331.21..96331.22 rows=1 width=8)
               ->  Parallel Seq Scan on test  (cost=0.00..85914.57 rows=4166657 width=0)
(5 rows)

postgres=# set jit = 'on';
SET
postgres=# show jit_above_cost;
 jit_above_cost
----------------
 100000
(1 row)

postgres=# show jit_inline_above_cost;
 jit_inline_above_cost
-----------------------
 500000
(1 row)

postgres=# show jit_optimize_above_cost;
 jit_optimize_above_cost
-------------------------
 500000
(1 row)

postgres=# explain  select count(*) from test;
                                       QUERY PLAN
-----------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=97331.43..97331.44 rows=1 width=8)
   ->  Gather  (cost=97331.21..97331.42 rows=2 width=8)
         Workers Planned: 2
         ->  Partial Aggregate  (cost=96331.21..96331.22 rows=1 width=8)
               ->  Parallel Seq Scan on test  (cost=0.00..85914.57 rows=4166657 width=0)
(5 rows)

postgres=# explain  analyze select count(*) from test;
                                                                QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=97331.43..97331.44 rows=1 width=8) (actual time=415.747..415.748 rows=1 loops=1)
   ->  Gather  (cost=97331.21..97331.42 rows=2 width=8) (actual time=415.658..418.129 rows=3 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Partial Aggregate  (cost=96331.21..96331.22 rows=1 width=8) (actual time=409.043..409.044 rows=1 loops=3)
               ->  Parallel Seq Scan on test  (cost=0.00..85914.57 rows=4166657 width=0) (actual time=0.148..250.496 rows=3333333 loops=3)
 Planning Time: 0.054 ms
 Execution Time: 418.175 ms
(8 rows)

postgres=# set jit_above_cost = 10; set jit_inline_above_cost = 10; set jit_optimize_above_cost = 10;
SET
SET
SET
postgres=# show max_parallel_workers_per_gather;
 max_parallel_workers_per_gather
---------------------------------
 2
(1 row)

postgres=# explain  analyze select count(*) from test;
                                                                QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=97331.43..97331.44 rows=1 width=8) (actual time=441.672..441.672 rows=1 loops=1)
   ->  Gather  (cost=97331.21..97331.42 rows=2 width=8) (actual time=441.547..446.028 rows=3 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Partial Aggregate  (cost=96331.21..96331.22 rows=1 width=8) (actual time=434.128..434.129 rows=1 loops=3)
               ->  Parallel Seq Scan on test  (cost=0.00..85914.57 rows=4166657 width=0) (actual time=0.161..251.158 rows=3333333 loops=3)
 Planning Time: 0.057 ms
 JIT:
   Functions: 9
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 1.096 ms, Inlining 109.551 ms, Optimization 22.201 ms, Emission 19.127 ms, Total 151.974 ms
 Execution Time: 446.673 ms
(12 rows)

postgres=# set max_parallel_workers_per_gather = 0;
SET
postgres=# explain analyze select count(*) from test;
                                                       QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=169247.71..169247.72 rows=1 width=8) (actual time=1172.932..1172.933 rows=1 loops=1)
   ->  Seq Scan on test  (cost=0.00..144247.77 rows=9999977 width=0) (actual time=0.028..745.134 rows=10000000 loops=1)
 Planning Time: 0.046 ms
 JIT:
   Functions: 2
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 0.298 ms, Inlining 0.881 ms, Optimization 3.986 ms, Emission 3.788 ms, Total 8.952 ms
 Execution Time: 1173.292 ms
(8 rows)

可以看出:

  • 没有达到对应GUC 参数规定的cost,即使jit=on,JIT 也不会起作用。
  • JIT 对应功能的开启是有一定代价的,对于一些SQL 语句可能会有性能的下降,如上面的例子select count(*) from test。
  • JIT 会继承并行查询带来的性能提升。

总结

JIT 技术对数据库操作系统来说是提高AP 能力的有效手段,但是在工程化的道路上要考虑很多实现的问题。PostgreSQL 社区目前是采用的外部依赖按需加载的方式,将JIT 作为一种外挂手段在一定场景下提高了性能,但是由于没有有效手段评估JIT 开启的代价,需要经验和具体业务场景的测试来判断JIT 功能开启是否能够提高性能。另外,目前实现的JIT 功能相对来说比较单一,只是初期版本,尚未成熟,还需要很长的开发周期来稳定和迭代。

参考文献

[1] PgSQL · 特性分析· JIT 在数据仓库中的应用价值

[2] Hello, JIT World: The Joy of Simple JITs

[3] LLVM’s Analysis and Transform Passes

MySQL · 引擎特性 · ROLLUP 功能用法和实现

$
0
0

在数据库查询语句中,在 GROUP BY 表达式之后加上 WITH ROLLUP 语句,可以在查询结果中包含更多高层级的统计输出。ROLLUP 功能使得可以通过单个查询语句来实现对数据进行不同层级上的分析与统计。因此,ROLLUP 功能能够很好得为 OLAP(Online Analytical Processing) 任务提供支持。

在本篇文章中,将会对 ROLLUP 的功能、用法、使用场景进行介绍并给出示例。也会从内核层面对 ROLLUP 的实现原理和方式进行阐述,包括逻辑过程与数据结构。

1 功能介绍

假如有一个 sales 表有 year, country, product 和 profit 四列,其中 profit 列为某年份某个国家的某种产品的一条收益。数据表的创建语句如下:

CREATE TABLE sales
(
    year    INT,
    country VARCHAR(20),
    product VARCHAR(32),
    profit  INT
);

为了方便演示,在数据表中插入如下数据:

INSERT INTO sales (year, country, product, profit) VALUES
    (2000, 'Finland', 'Computer', 500),
    (2000, 'Finland', 'Computer', 1000),
    (2000, 'India', 'Calculator', 150),
    (2000, 'India', 'Computer', 400),
    (2000, 'Finland', 'Phone', 100),
    (2001, 'USA', 'Calculator', 50),
    (2001, 'USA', 'Computer', 2700),
    (2001, 'USA', 'TV', 1),
    (2000, 'India', 'Computer', 300),
    (2000, 'India', 'Computer', 500),
    (2000, 'USA', 'Calculator', 75),
    (2000, 'USA', 'Computer', 1500),
    (2001, 'USA', 'TV', 249),
    (2001, 'Finland', 'Phone', 10);

我们经常需要使用如下查询语句对某年份某个国家的某种产品的总收益进行汇总:

SELECT year, country, product, SUM(profit) AS profit
       FROM sales
       GROUP BY year, country, product;

查询结果为:

+------+---------+------------+--------+
| year | country | product    | profit |
+------+---------+------------+--------+
| 2000 | Finland | Computer   |   1500 |
| 2000 | India   | Calculator |    150 |
| 2000 | India   | Computer   |   1200 |
| 2000 | Finland | Phone      |    100 |
| 2001 | USA     | Calculator |     50 |
| 2001 | USA     | Computer   |   2700 |
| 2001 | USA     | TV         |    250 |
| 2000 | USA     | Calculator |     75 |
| 2000 | USA     | Computer   |   1500 |
| 2001 | Finland | Phone      |     10 |
+------+---------+------------+--------+

通常情况下,我们不光需要这种最高层次的统计结果,也需要在更低的层次进行分析。比如说,某年份某个国家在所有产品的收益总和,或者某年份所有国家的收益总和。为了达到这样的效果,我们可能需要对 Group By List 中的属性列进行调整,并重新执行查询语句得到我们需要的结果。但是 ROLLUP 功能使得我们可以仅通过一条查询语句实现上述效果:

SELECT year, country, product, SUM(profit) AS profit
       FROM sales
       GROUP BY year, country, product WITH ROLLUP;

查询结果为:

+------+---------+------------+--------+
| year | country | product    | profit |
+------+---------+------------+--------+
| 2000 | Finland | Computer   |   1500 |
| 2000 | Finland | Phone      |    100 |
| 2000 | Finland | NULL       |   1600 |
| 2000 | India   | Calculator |    150 |
| 2000 | India   | Computer   |   1200 |
| 2000 | India   | NULL       |   1350 |
| 2000 | USA     | Calculator |     75 |
| 2000 | USA     | Computer   |   1500 |
| 2000 | USA     | NULL       |   1575 |
| 2000 | NULL    | NULL       |   4525 |
| 2001 | Finland | Phone      |     10 |
| 2001 | Finland | NULL       |     10 |
| 2001 | USA     | Calculator |     50 |
| 2001 | USA     | Computer   |   2700 |
| 2001 | USA     | TV         |    250 |
| 2001 | USA     | NULL       |   3000 |
| 2001 | NULL    | NULL       |   3010 |
| NULL | NULL    | NULL       |   7535 |
+------+---------+------------+--------+

查询结果中的 NULL 值表示该行输出为更低层次上的聚合结果,在带 WITH ROLLUP 的聚合时,每当 GROUP BY 的属性列(非最后一列)的值发生变化时,查询结果中都会产生额外的聚合行。

因此,借助 ROLLUP,我们通过一条查询语句就能够得到 GROUP BY 的属性列在不同层次上的聚合结果。适用于需要在不同层次上对数据进行统计分析的场景,不仅省去了写多条查询语句重复查询的麻烦,而且提升了执行效果。

以上是 ROLLUP 的功能、用法、使用场景介绍的部分,接下来将会对 ROLLUP 的内核实现进行介绍,分为优化器和执行器两部分。

2 内核实现

2.1 优化器

2.1.1 开辟内存空间

优化器在优化阶段针对 ROLLUP 做的操作首先是为 ROLLUP 所需要的数据结构开辟内存空间(JOIN::optimize_rollup)。

由于 ROLLUP 需要对 GROUP BY 的属性列,按照不同层级进行聚合,那么假设有一条语句是 GROUP BY year, country, product WITH ROLLUP,那么输出的 ROLLUP 结果行应包含以下3种:

yearcountryproductSum_func
NULLNULLNULL
2000NULLNULL
2000FinlandNULL

因此,为了方便在每读入一条数据时,能直接在不同层级上进行聚合,优化器会提前分配所有层级所需要的内存空间(Item List)。Item List 的条数与不同层级数、GROUP BY 的属性列数相同(send_group_parts)。

2.1.2 初始化数据结构

优化器对 ROLLUP 第二个阶段的操作是对数据结构进行初始化(JOIN::rollup_make_fields),对 ROLLUP 输出的聚合列指向用于表示 ROLLUP 聚合的 Item(Item_null_result),非聚合列对应的 Item 进行拷贝。

rollup_memory

同时也对聚合函数的 Item(Item::SUM_FUNC_ITEM)进行拷贝,通过 sum_funcs 和 sum_funcs_end 的指向,来判断每读入一条数据时需要在哪些 Item_sum 上进行累积。

item_sum_memory

这样的内存设计可以方便在执行阶段,通过一条数据在 GROUP BY 列表中发生变化的最小层级列对应的下标来判断哪些 Item_sum 需要重置,剩下的 Item_sum 需要累积。也可以判断哪些 Level 已经统计完成,可以返回结果。

2.2 执行器

MySQL 中对 ROLLUP 的实现依赖于 Filesort,因此执行器依次读入的数据在 GROUP BY 列表上的属性是严格有序的。通过 List<Cached_item> group_fields来缓存上一组的数据结果,新读入的数据与缓存数据进行比较,判断新读入的数据与缓存数据在 GROUP BY 属性列表上发生变化的最小层级,用 idx表示。

如果 idx = -1,说明当前数据与缓存数据属于同一组,那么直接将当前组和所有 ROLLUP 层级的聚合函数进行累积(update_sum_func)。

如果 idx >= 0,说明当前数据与前一组数据在某些 GROUP BY 属性列的属性值发生了变化,idx 的具体值表示发生变化的分组最高属性列。比如有一条语句是:

SELECT year, country, product, SUM(profit) AS profit
       FROM sales
       GROUP BY year, country, product WITH ROLLUP;

rollup_process

如果新的一条数据仅在 product 属性上发生变化,那么 idx = 2;如果在 country 属性上发生变化,那么 idx = 1。

在这种情况下,前一个组的聚合信息已经统计完成,执行器会更新缓存值(update_item_cache_if_changed),同时将这个组的结果输出。然后根据 idx 的值判断哪些 ROLLUP 层级的统计完成,将所有层级高于当前行的结果返回(JOIN::rollup_send_data)或者写入临时表(JOIN::rollup_write_data)。

然后对新的组拷贝 Item(copy_fields),对新的组和 ROLLUP 中高于当前层级的 Item_sum 进行重置和累积,对低于当前层级的 Item_sum 进行累积(init_sum_functions)。依此类推,直到读入全部数据。

总的来说,ROLLUP 的逻辑过程比较清楚,是通过顺序遍历排好序的数据,依次将其与之前缓存的上一组的属性列进行比较,判断之前组和 ROLLUP 层级的统计数据是否可以返回,并对新的组和低于当前层级的 ROLLUP 进行累积。

3 相关函数

  • JOIN::alloc_func_list 分配一组指向 sum_func 的指针来加速 sum_func 的计算过程。

  • JOIN::make_sum_func_list 使用 item_sum 对象初始化 sum_func 的数组。

  • JOIN::rollup_process_const_fields 将 group by 列表中的常数 item 进行封装。

  • JOIN::rollup_make_fields 用指向 field 的指针来填充 rollup 的数据结构。

  • JOIN::switch_slice_for_rollup_fields 为 rollup 结构切换 ref_items 的片。

  • JOIN::optimize_rollup 优化 rollup 过程,分配 rollup 处理过程中所需的对象。

  • ROLLUP rollup 基本数据结构。

  • JOIN::rollup_send_data 将 rollup 级别高于当前的发送到客户端。

  • JOIN::rollup_write_data 将 rollup 级别高于当前的写入临时表。

  • has_rollup_result 检查一个 item 是否包含 rollup 的 NULL 值,需要被写入临时表。

  • SELECT_LEX::resolve_rollup 解析 rollup 过程中的 items。

4 参考资料

MySQL 官方文档

MySQL 导读 ROLLUP

注:以上测试结果和内核介绍基于 MySQL 8.0.16。


Redis · 最佳实践 · 混合存储实践指南

$
0
0

Redis 混合存储实例是阿里云自主研发的兼容Redis协议和特性的云数据库产品,混合存储实例突破 Redis 数据必须全部存储到内存的限制,使用磁盘存储全量数据,并将热数据缓存到内存,实现访问性能与存储成本的完美平衡。

架构及特性

_1

命令兼容

混合存储兼容绝大多数 Redis 命令,与原生 Redis 相比,如下命令不支持或受限制;不支持的主要原因是考虑到性能,如业务中有使用到,请提交工单。

Keys(键)List(链表)Scripting(Lua脚本)
RENAMELINSERTSCRIPT 不支持LOAD和DEBUG子命令
RENAMENXLREM 
MOVE  
SWAPDB  
SORT 不支持STORE选项  

选型指南 - 场景

_2

选型指南 - 规格

选择混合存储实例时,需要选择合适的【内存配置 + 磁盘配置】;磁盘决定能存储的数据总量,内存决定能存储的热数据总量,实例生产时会根据存储的规格配置选择合适的CPU资源配置,目前暂不支持自定义CPU核数。

比如【64GB内存 + 256GB磁盘】实例,意思是实例最多能存储 256GB 的数据(以KV存储引擎的物理文件总大小为准),其中 64GB 数据可以缓存在内存。

案例1: 用户A 使用 Redis Cluster 存储了 100GB 的数据,总的访问QPS不到2W,其中80%的数据都很少访问到。用户A 可以使用 【32GB内存 + 128GB磁盘】 混合存储实例,节省了近 70GB 的内存存储,存储成本下降50%+。

  案例2:用户B 在IDC自建 Pika/SSDB 实例,解决Redis存储成本高的问题,存储了约 400GB 的数据,其中活跃访问的在10%左右,集群运维负担很重,想迁移至云数据库;用户B 可以使用 【64GB内存 + 512GB磁盘】混合存储实例,来保证免运维的同时,服务质量不下降。

因 Redis 数据存储到 KV 存储引擎,每个key都会额外元数据信息,存储空间占用会有一定的放大,建议在磁盘空间选择上,留有适当余量,按实际存储需求的 1.2 - 1.5倍预估。

性能指标

Redis 混合存储的性能与内存磁盘配比,以及业务的访问高度相关;根据规格配置及业务访问模式的不同,简单 set/get 的性能可在几千到数万之间波动。最好情况所有的访问都内存命中,性能与 Redis 内存版基本一致;最差情况所有的访问都需要从磁盘读取。

测试场景:2000w key,value大小为1KB,25%的热key能存储在内存,get 请求测试数据如下

测试集内存版(100%数据在内存)混合存储版(25%数据在内存)
随机访问12.3(万)1.5
高斯分布80%的概率访问20%的key12.05.4
高斯分布99%的概率访问1%的key13.511.4

应用场景

视频直播类

视频直播类业务往往存在大量热点数据,大部分的请求都来自于热门的直播间。使用 Redis 混合存储型实例,内存中保留热门直播间的数据,不活跃的直播间数据被自动存储到磁盘上,可以达到对有限内存的最佳利用效果。

电商类

电商类应用有大量的商品数据,新上架的商品会被频繁访问,而较老的商品访问热度不高;使用 Redis 混合存储型实例,可以轻松突破内存容量限制,将大量的商品数据存储到磁盘,在正常业务请求中,活跃的商品数据会逐步缓存在内存中,以最低的成本满足业务需求。

在线教育类

在线教育类的场景,有大量的课程、题库、师生交流信息等数据,通常只有热门课程、最新题库题库会被频繁访问; 使用 Redis 混合存储型,将大量的课程信息存储到磁盘,活跃的课程、题库信息会换入到内存并常驻内存,保证高频访问数据的性能,实现性能与存储成本的平衡。

其他场景

其他数据访问有明显冷热特性,对性能要求不高的场景均可使用Redis混合存储来降低存储成本。

PgSQL · 应用案例 · pgbench client_id 变量用途

$
0
0

背景

pgbench是 PG内置的一款压测工具,效率非常高。内置tpcb测试模型,并且支持自定义压测模型(内置了非常丰富的变量生成函数,操作符,函数,变量。同时支持shell 调用结果作为变量传输。支持多个压测文件,文件权重设置等)。

详见

https://www.postgresql.org/docs/current/pgbench.html

由于pgbench支持客户端并行,可以开启多个链接进行测试。每个链接有一个唯一的标示:

client_id :

unique number identifying the client session (starts from zero)

采用client_id,可以模拟数据隔离的更新操作(防止多个链接相互更新到相同记录,导致锁问题,与真实场景不符,或影响更新测试性能)

或者将client_id作为动态identify的suffix组成,实现不同线程操作不同表的需求。(pgbench暂时还不支持这个功能, 可以参考这里说明 《PostgreSQL 使用 pgbench 测试 sysbench 相关case - pg_oltp_bench》需要修改pgbench代码parseQuery)

vi test.sql  
\set id1 random(1, 10000000)  
SELECT pad FROM "sbtest:client_id" WHERE id = :id1;  
  
$ pgbench -M prepared -n -r -P 1 -f ./test.sql -h xxx.xxx.xxx.xxx -p 1924 -U postgres postgres -c 3 -j 3 -T 100   
  
目前会报错,只支持simple模式。  如果要让prepared模式支持,建议改pgbench代码来支持. 例如使用:::varname时,拼接identifid。     
ERROR:  relation "sbtest$1" does not exist  
LINE 1: SELECT pad FROM "sbtest$1" WHERE id = $2;  
                        ^  
client 0 aborted in state 1: ERROR:  prepared statement "P0_1" does not exist  
ERROR:  relation "sbtest$1" does not exist  
LINE 1: SELECT pad FROM "sbtest$1" WHERE id = $2;  
                        ^  
client 1 aborted in state 1: ERROR:  prepared statement "P0_1" does not exist  
ERROR:  relation "sbtest$1" does not exist  
LINE 1: SELECT pad FROM "sbtest$1" WHERE id = $2;  
                        ^  
client 2 aborted in state 1: ERROR:  prepared statement "P0_1" does not exist  

例子

upsert,确保不同的会话一定相互不会出现行级锁冲突干扰。

create table t(id int primary key, info text, crt_time timestamp);  

数据ID范围1亿,64个并发操作。确保不同并发操作的ID相互绝对不会重叠

vi test.sql  
  
\set id random(1,100000000)/64+:client_id  
insert into t values (:id, md5(random()::text), now()) on conflict (id) do update set info=excluded.info , crt_time=excluded.crt_time;  

说明:

\set id random(1,100000000)/64+:client_id  
  
random(1,100000000) 返回1到1亿之间的随机int  
/64除以64得到trunc int  
+:client_id , 加每个线程的number,  
  
得到的值,赋予给id, 从而不同的线程绝对不会有重复的id出现  
postgres=# select * from t limit 10;  
   id    |               info               |          crt_time            
---------+----------------------------------+----------------------------  
  259017 | 1d55b352a6d0505bd9f5f7d4c445233b | 2019-08-28 22:27:38.123068  
 1472003 | 493446240b69fd241c135a238d70eab4 | 2019-08-28 22:27:35.934951  
 1001450 | 74d8334822be81483bffe7da3b5f0253 | 2019-08-28 22:27:26.06475  
  985969 | f9790129e9f4fe2da6f0d887abc5bb1c | 2019-08-28 22:27:37.722908  
 1140661 | d8214e5c1994549b612b7e4194c63bcb | 2019-08-28 22:27:37.205729  
 1252023 | 6d8fbeb3d039749e6594b8913955cde1 | 2019-08-28 22:27:29.841494  
  727159 | 68fa51af79d7c01502a79b8873aeb8fa | 2019-08-28 22:27:31.892077  
  687989 | 00b95072f38ffc73fb9e5b0ace009b5d | 2019-08-28 22:27:15.524358  
 1029162 | 113a44124e08be8105690a24b456863e | 2019-08-28 22:27:23.566686  
 1204224 | c6ea8e3c66790ccf3dd22b9feab2f4a6 | 2019-08-28 22:27:29.696627  
(10 rows)  

性能杠杠的

pgbench -M prepared -n -r -P 1 -f ./test.sql -c 64 -j 64 -T 120  
  
transaction type: ./test.sql  
scaling factor: 1  
query mode: prepared  
number of clients: 64  
number of threads: 64  
duration: 120 s  
number of transactions actually processed: 25803874  
latency average = 0.297 ms  
latency stddev = 0.251 ms  
tps = 215005.904762 (including connections establishing)  
tps = 215027.544261 (excluding connections establishing)  
statement latencies in milliseconds:  
         0.001  \set id random(1,100000000)/64+:client_id  
         0.296  insert into t ...........................  

即使id取值范围就是0-31,性能也是杠杠的。

vi test.sql  
insert into t values (:client_id, md5(random()::text), now()) on conflict (id) do update set info=excluded.info , crt_time=excluded.crt_time;  
  
pgbench -M prepared -n -r -P 1 -f ./test.sql -c 32 -j 32 -T 120  
  
transaction type: ./test.sql  
scaling factor: 1  
query mode: prepared  
number of clients: 32  
number of threads: 32  
duration: 120 s  
number of transactions actually processed: 30045516  
latency average = 0.127 ms  
latency stddev = 0.073 ms  
tps = 250377.400798 (including connections establishing)  
tps = 250393.055448 (excluding connections establishing)  
statement latencies in milliseconds:  
         0.127  insert into t values (:client_id, ...........  

如果没有使用client_id,那锁冲突就会比较严重,造成等待影响性能。从25万qps下降到了18万qps。

vi test.sql  
\set id random(0,31)  
insert into t values (:id, md5(random()::text), now()) on conflict (id) do update set info=excluded.info , crt_time=excluded.crt_time;  
  
pgbench -M prepared -n -r -P 1 -f ./test.sql -c 32 -j 32 -T 120  
  
transaction type: ./test.sql  
scaling factor: 1  
query mode: prepared  
number of clients: 32  
number of threads: 32  
duration: 120 s  
number of transactions actually processed: 21619627  
latency average = 0.177 ms  
latency stddev = 0.138 ms  
tps = 180162.287114 (including connections establishing)  
tps = 180174.521514 (excluding connections establishing)  
statement latencies in milliseconds:  
         0.000  \set id random(0,31)  
         0.177  insert into t values (:id, ............  

小结

期待pgbench支持在identify字段中支持变量,而不仅仅是非identify内容中支持变量。

参考

《PostgreSQL 使用 pgbench 测试 sysbench 相关case - pg_oltp_bench》

《PostgreSQL 10.0 preview 性能增强 - 2PC事务恢复阶段性能提升》

《PostgreSQL native partition 分区表性能优化之 - 动态SQL+服务端绑定变量》

《PostgreSQL Oracle 兼容性之 - DBMS_SQL(存储过程动态SQL中使用绑定变量-DB端prepare statement)》

https://www.postgresql.org/docs/current/pgbench.html

MySQL · 引擎特性 · 临时表改进

$
0
0

最新release的MySQL 8.0.16中包含的临时表改动:

  • WL#11974, 不再支持myisam作为内部临时表转储磁盘时使用,参数internal_tmp_disk_storage_engine被移除掉了, 直接使用innodb作为内部内存表的持久化引擎
  • 新增参数temptable_use_mmap, 默认打开,表示当使用TempTable的临时表的内存占用超过temptable_max_ram之后,就使用memory map的方式去扩展临时文件到内存。如果为off,则使用innodb引擎来存储temptable数据

temptable engine

我们知道UNION, DERIVED TABLE, CTE, 子查询或者distinct order by之类的查询都可能用到临时表来存储中间结果,官方文档中列举了几种场景。内存引擎可以通过参数 internal_tmp_mem_storage_engine来选择: temptable(default) 或者memory引擎。本文只讨论temptable引擎

当内存超出temptable引擎限制( temptable_max_ram, 默认1GB)时,将转换成磁盘数据,这里也可以选择是存储成innodb还是myisam(参数). 但COMMON TABLE EXPRESSION(CTE)不允许使用myisam引擎

Note: 由于innodb有行长度限制,可能报row size too large 或者too many columns之类的错误,可以通过设置internal_tmp_disk_storage_engine来绕过限制。

MySQL8.0.16引入了新的参数temptable_use_mmap,用来控制temptable引擎是否磁盘数据转换成Innodb存储,还是内存映射文件。

temptable引擎和memory引擎本质上类似,但最大的不同时可以支持变长类型(例如blob, text, json, geometry等),例如varchar(100)的数据”abcd”应该只占用4个字节而非100个字节。

在之前的版本中当存在Lob类型时,数据会直接转换成磁盘存储。而WL#11613对此做了修改:在内存中使用数组来维护大字段,每个字段包含数据长度和数据指针。在数组之后连续的存储列值,没有padding(如果使用memory引擎,则会padding)。官方博客的评测中由于无需在遇到lob时转换成磁盘存储,相比之前的版本可能获得数倍的性能提升。

从设计上temptable引擎支持hash Index和tree index,允许一个inserter和多个reader, 插入不影响reader的cursor。

笔者的主要关注点在innodb,由于从5.7开始MySQL对Innodb做了大量的优化(cursor优化,无redo log, 去除代码路径上的各种锁),因此默认情况下使用innodb作为内部临时表的磁盘存储.

可以通过查询performance schema表来监控内存和磁盘上的临时表占用空间:

mysql> SELECT * FROM performance_schema.memory_summary_global_by_event_name WHERE event_name like '%temptable%'\G
*************************** 1. row ***************************
EVENT_NAME: memory/temptable/physical_disk
COUNT_ALLOC: 0
COUNT_FREE: 0
SUM_NUMBER_OF_BYTES_ALLOC: 0
SUM_NUMBER_OF_BYTES_FREE: 0
LOW_COUNT_USED: 0
CURRENT_COUNT_USED: 0
HIGH_COUNT_USED: 0
LOW_NUMBER_OF_BYTES_USED: 0
CURRENT_NUMBER_OF_BYTES_USED: 0
HIGH_NUMBER_OF_BYTES_USED: 0
*************************** 2. row ***************************
EVENT_NAME: memory/temptable/physical_ram
COUNT_ALLOC: 2
COUNT_FREE: 0
SUM_NUMBER_OF_BYTES_ALLOC: 2097152
SUM_NUMBER_OF_BYTES_FREE: 0
LOW_COUNT_USED: 0
CURRENT_COUNT_USED: 2
HIGH_COUNT_USED: 2
LOW_NUMBER_OF_BYTES_USED: 0
CURRENT_NUMBER_OF_BYTES_USED: 2097152
  HIGH_NUMBER_OF_BYTES_USED: 2097152
2 rows in set (0.03 sec)

temptable引擎实现了自己的内存分配器来减少对系统的内存分配释放调用,封装从磁盘上通过mmap进行分配的策略。先从系统分配大块的内存,然后通过这些内存块来提供malloc/free请求. 每个block包含一个header以及一系列的chunk:

  • 每个block的结构如下:(quoted from worklog)

    - bytes [0, 3]: 4 bytes for the block size (set at block creation and never
        changed later).
    - bytes [4, 7]: 4 bytes for the number of used/allocated chunks from this
    block (set to 0 at block creation).
    - bytes [8, 11]: 4 bytes for the offset of the first byte from the block
    start that is free and can be used by the next allocation request (set
        to 12 at block creation (3 * 4 bytes)). We call this first pristine offset.
    - bytes [12, block size) a sequence of chunks appended to each other.
    
  • 每个chunk的结构

    - bytes [0, 3]: 4 bytes that designate the offset of the chunk from
    the start of the block. This is used in order to be able to deduce
    the block start from a given chunk. The offset of the first chunk is
    12 (appended after the block size (4), number of allocated chunks (4)
    and the first pristine offset (4)).
    - bytes [4, chunk size): user data, pointer to this is returned to the
    user after a successfull allocation request.
    
  • 分配内存:

    - if the current block does not have enough space:
    create a new block and make it the current (lose the pointer to the
    previous current block).
    - increment the number of allocated chunks by 1.
    - in the first pristine location - write its offset from the block
    start (4 bytes).
    - increment the first pristine offset with 4 + requested bytes by the user.
    - return a pointer to the previous first pristine + 4 to the user.
    
  • 释放内存

    - read 4 bytes before the provided pointer and derive the block start.
    - decrement the number of used chunks by 1.
    - if this was the last chunk in the block and this is not the last block:
    destroy the block, returning the memory to the OS.
    - keep the last block for reuse even if all chunks from it are removed, it
    will be destroyed when the thread terminates. When the last chunk from
    the last block is removed, instead of destroying the block reset its first
    pristine byte offset to 12.
    

内存分配器的定义和实现在文件storage/temptable/include/temptable/allocator.h

其他模块的定义都在目录storage/temptable/include/下,如果想深入了解该引擎的实现,可以阅读这些头文件代码,有比较详细的注释

InnoDB临时表

在innodb的代码里有大量使用dict_table_t::is_intrinsic()来判定执行路径,对于内部临时表而言,会去消除不必要的开销,例如表锁和事务开销等等。这里简单介绍下相关的代码。

插入操作

当插入临时表时,直接使用cursor进行操作,跳过事务和锁相关操作:

row_insert_for_mysql 
    |--> row_insert_for_mysql_using_cursor

对于临时表记录:

  • 其row_id取自表上递增计数器dict_table_t::sess_row_id, 事务id取自dict_table_t::sess_trx_id而非全局计数器(trx_sys->max_trx_id). 事务Id写入到记录中。

为什么还需要trx id ? 代码中的解释:

Intrinsic table are not shared so don't need a central trx-id
but just need a increased counter to track consistent view while
proceeding SELECT as part of UPDATE
  • 插入操作无需记录undo log, 因此需要通过插入的记录显式回滚(row_explicit_rollback),实际上就是将插入的记录进行标记删除
  • 索引上dict_index_t::last_ins_cur维护了上次插入位点的cursor, 这样对于顺序插入操作,无需每次都commit mtr,并能快速定位到btre上的插入点(row_ins_sorted_clust_index_entry)

    delete/update操作会自动把cursor提交掉 当存在blob/text类型时,不能cache cursor

查询操作

函数:

row_search_for_mysql
     |--> row_search_no_mvcc

由于表只对当前session可见,因此无需走mvcc判断。 查询在满足一定条件时也使用了缓存策略cursor的策略, 上次查询的cursor存储在dict_index_t::last_sel_cur中,无需频繁提交mini transaction, 该特性仅限于auto-generated clust index

临时表空间

在当前版本(8.0.15)的MySQL中,有两类临时表空间:

ibtmp1

在data目录下,具有固定的space id(s_temp_space_id = 0xFFFFFFFD)

Note: 在之前的版本中(例如5.7),使用ibtmp1来存储临时表数据和undo信息等,在每次重启时重新创建并使用新的space id.

在内存中对应的对象为srv_tmp_space,目前用于存储临时表的Undo log:

  • 正常shutdown(innodb_space_shutdown())或者重启时(srv_open_tmp_tablespace())重建文件
  • 回滚段初始化(trx_rseg_adjust_rollback_segments())
  • 回滚段内存对象在trx_sys_t::tmp_rsegs中,默认128个回滚段,与正常回滚段在事务开始时分配不同,临时表回滚段是在使用时才分配(trx_undo_report_row_operation() –> trx_assign_rseg_temp() –>get_next_temp_rseg)
Note: 通常查询产生的内部中间表只有插入和查询,因此无需记录undo log。但对于用户显式创建的临时表依然需要

innodb_temp目录下的临时表空间文件

这些文件以temp_{id}.ibt命名,主要是避免所有文件都存储在ibtmp1中,而ibtmp1是在重启时才会重置,就算表被删除了也不会缩减空间。

  • 在实例启动时,这些文件在目录innodb_temp_tablespaces_dir或者#innodb_temp(如果未显式指定)下被创建(ibt::open_or_create), 初始化创建10个文件.
  • 每个session在第一次请求创建临时表时,会从池中分配一个tablespace. 当这个tablespace被attach到该session时,所有临时表都创建在其中. 每个session最多可以有两个独立的tablespace,一个用于显式创建临时表,一个用于优化器创建的临时表。需要两个独立表空间的原因是未来可以在链接断开之前就单独回收优化器表的空间
dict_build_tablespace_for_table 
    |--> innodb_session->get_instrinsic_temp_tblsp()
    |--> innodb_session->get_usr_temp_tblsp()
  • 当pool中space不够用时,会自动进行扩展,每次扩展单位为10个文件
  • 在session断开时,将tablespace truncate并放回到pool中。所以如果临时表空间占用过大,可以通过中断链接的方式来释放
  • 可以通过is表查询tablespace占用的session id
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_SESSION_TEMP_TABLESPACES;
+----+------------+----------------------------+-------+----------+-----------+
| ID | SPACE      | PATH                       | SIZE  | STATE    | PURPOSE   |
+----+------------+----------------------------+-------+----------+-----------+
| 72 | 4294566162 | ./#innodb_temp/temp_10.ibt | 81920 | ACTIVE   | INTRINSIC |
|  0 | 4294566153 | ./#innodb_temp/temp_1.ibt  | 81920 | INACTIVE | NONE      |
|  0 | 4294566154 | ./#innodb_temp/temp_2.ibt  | 81920 | INACTIVE | NONE      |
|  0 | 4294566155 | ./#innodb_temp/temp_3.ibt  | 81920 | INACTIVE | NONE      |
|  0 | 4294566156 | ./#innodb_temp/temp_4.ibt  | 81920 | INACTIVE | NONE      |
|  0 | 4294566157 | ./#innodb_temp/temp_5.ibt  | 81920 | INACTIVE | NONE      |
|  0 | 4294566158 | ./#innodb_temp/temp_6.ibt  | 81920 | INACTIVE | NONE      |
|  0 | 4294566159 | ./#innodb_temp/temp_7.ibt  | 81920 | INACTIVE | NONE      |
|  0 | 4294566160 | ./#innodb_temp/temp_8.ibt  | 81920 | INACTIVE | NONE      |
|  0 | 4294566161 | ./#innodb_temp/temp_9.ibt  | 81920 | INACTIVE | NONE      |
+----+------------+----------------------------+-------+----------+-----------+
10 rows in set (0.00 sec)
  • temp tablespace有单独space id 段,内部预留了400k的space id 给temporary tablespace (s_min_temp_space_id , s_max_temp_space_id),足够使用.
  • space pool中大小不会缩小,也就是说只会扩展,不会收缩!

Reference

WL#8117: Compact In-Memory Temporary Tables

WL#11452 Support for BLOBs in temptable engine

WL#11613: InnoDB: Reclaim disk space occupied by temporary tables online

MySQL 8.0: Support for BLOBs in TempTable engine

Internal Temporary Table Use in MySQL

InnoDB Temporary Tablespaces

MySQL · 引擎特性 · 初探 Clone Plugin

$
0
0

MySQL8.0.17推出了一个重量级的功能:clone plugin。允许用户可以将当前实例进行本地或者远程的clone。这在某些场景尤其想快速搭建复制备份或者在group replication里加入新成员时非常有用。本文主要试玩下该功能,并试图阐述下其实现的机制是什么。

我们以本地clone为例,因为去除网络部分,理解起来会相对简单点。 也不会过度接触代码部分,仅仅做简单的原理性阐述

示例

本地 clone

本地clone无需启动额外mysqld, 只要在实例上执行一条sql语句,指定下目标目录即可:

CLONE LOCAL DATA DIRECTORY [=] 'clone_dir';


root@test 03:49:43>SELECT STAGE, STATE, END_TIME FROM performance_schema.clone_progress;
+-----------+-------------+----------------------------+
| STAGE     | STATE       | END_TIME                   |
+-----------+-------------+----------------------------+
| DROP DATA | Completed   | 2019-07-26 12:07:12.285611 |
| FILE COPY | Completed   | 2019-07-26 12:07:18.270998 |
| PAGE COPY | Completed   | 2019-07-26 12:07:18.472560 |
| REDO COPY | Completed   | 2019-07-26 12:07:18.673061 |
| FILE SYNC | Completed   | 2019-07-26 12:07:32.090219 |
| RESTART   | Not Started | NULL                       |
| RECOVERY  | Not Started | NULL                       |
+-----------+-------------+----------------------------+
7 rows in set (0.00 sec)

需要BACKUP_ADMIN权限

远程 clone

CLONE INSTANCE FROM USER@HOST:PORT
IDENTIFIED BY 'password'
[DATA DIRECTORY [=] 'clone_dir']
[REQUIRE [NO] SSL];

mysql> SET GLOBAL clone_valid_donor_list = 'example.donor.host.com:3306';
mysql> CLONE INSTANCE FROM clone_user@example.donor.host.com:3306 IDENTIFIED BY 'password';
mysql> CLONE INSTANCE FROM user_name@example.donor.host.com:3306 IDENTIFIED BY 'password' DATA DIRECTORY = '/path/to/clone_dir';
  • 需要指定绝对路径,并且路径目录必须不存在
  • 在接受机器上启动mysqld,执行上述语句连接到目标机器,就能从目标机器上clone数据到本地,注意如果没有指定data directory的话,就默认配置的目录,已有的文件会被清理掉,并在clone完成后重启
  • 两个实例上都需要安装clone plugin
  • 必须有相同的字符集设置

官方文档列出的一些限制:

  1. ddl包括truncate table在clone期间不允许执行 //被block住
  2. An instance cannot be cloned from a different MySQL server version. The donor and recipient must have the same MySQL server version.
  3. the X Protocol port specified by mysqlx_port is not supported for remote cloning operations
  4. The clone plugin does not support cloning of MySQL server configurations
  5. 不支持clone binlog
  6. The clone plugin only clones data stored in InnoDB. Other storage engine data is not cloned
  7. Connecting to the donor MySQL server instance through MySQL Router is not supported.
  8. Local cloning operations do not support cloning of general tablespaces that were created with an absolute path. A cloned tablespace file with the same path as the source tablespace file would cause a conflict.

主要流程

主要流程包含如下几个过程: [INIT] —> [FILE COPY] —> [PAGE COPY] —> [REDO COPY] -> [Done]

INIT 阶段

需要持有backup lock, 阻止ddl进行

FILE COPY 阶段

按照文件进行拷贝,同时开启page tracking功能,记录在拷贝过程中修改的page, 此时会设置buf_pool->track_page_lsn为当前lsn,track_page_lsn在flush page阶段用到:

buf_flush_page:

if (!fsp_is_system_temporary(bpage->id.space()) &&
    buf_pool->track_page_lsn != LSN_MAX) {
  page_t *frame;
  lsn_t frame_lsn;

  frame = bpage->zip.data;

  if (!frame) {
    frame = ((buf_block_t *)bpage)->frame;
  }
  frame_lsn = mach_read_from_8(frame + FIL_PAGE_LSN); //对于在track_page_lsn之后的page, 如果frame_Lsn大于track_page_lsn, 表示已经记录下page id了,无需重复记录

  arch_page_sys->track_page(bpage, buf_pool->track_page_lsn, frame_lsn,
      false);  // 将page id记录下来,表示在track_page_lsn后修改过的page
}

会创建一个后套线程page_archiver_thread(),将内存记录的page id flush到disk上

PAGE COPY

这里有两个动作

  • 开启redo archiving功能,从当前点开始存储新增的redo log,这样从当前点开始所有的增量修改都不会丢失
  • 同时上一步在page track的page被发送到目标端。确保当前点之前所做的变更一定发送到目标端

关于redo archiving,实际上这是官方早就存在的功能,主要用于官方的企业级备份工具,但这里clone利用了该特性来维持增量修改产生的redo。 在开始前会做一次checkpoint, 开启一个后台线程log_archiver_thread()来做日志归档。当有新的写入时(notify_about_advanced_write_lsn)也会通知他去archive

当arch_log_sys处于活跃状态时,他会控制日志写入以避免未归档的日志被覆盖(log_writer_wait_on_archiver), 注意如果log_writer等待时间过长的话, archive任务会被中断掉

Redo Copy

停止Redo Archiving”, 所有归档的日志被发送到目标端,这些日志包含了从page copy阶段开始到现在的所有日志,另外可能还需要记下当前的复制点,例如最后一个事务提交时的binlog位点或者gtid信息,在系统页中可以找到

Done

目标端重启实例,通过crash recovery将redo log应用上去。

参考文档

官方博客:Clone: Create MySQL instance replica

The Clone Plugin

WL#9209: InnoDB: Clone local replica

WL#9210: InnoDB: Clone remote replica

WL#9682: InnoDB: Support cloning encrypted and compressed database

WL#9211: InnoDB: Clone Replication Coordinates

WL#11636: InnoDB: Clone Remote provisioning

MySQL · 引擎特性 · 网络模块优化

$
0
0

本文主要描述下MySQL8.0在网络模块的几个小优化, 由于本人对server层代码不熟悉,所以只是列出自己的理解和相关的patch以及worklog,不做深入详细实现的解释,感兴趣的可自行从连接中找到对应的代码

admin Port

运维大并发负载数据库的同学经常会碰到的情况是,max_connection被占满,甚至root账户都无法登陆上去,kill掉这些链接来让实例恢复正常。

Alibaba RDS MySQL的做法是把connection的个数拆分成不同的使用目的,例如系统维护账户占用一部分,用户账户占用一部分,两者不互相影响。

另外一种方式是比较高危的,通过gdb的方式直接进入进程去修改max_connection的值,但注意符号表要编译到mysqld里面,不然无法识别。

此外在mariadb/percona server的线程池实现里,也引入了extra port,当线程池用满无法登陆时,可以使用extra port来连上实例。

在MySQL8.0里,则引入了admin port的概念,顾名思义,就是单独开一个端口给管理员用,该特性从8.0.14开始引入。可以说这是个对运维非常有用,关键时候可以救命的特性。这个feature由facebook贡献给上游

主要包含几个配置参数: admin_address: 用于指定管理员发起tcp连接的主机地址,可以是ipv4,ipv6, 或者Host name等等,他类似bind-address,但不同的是只能接受一个ip地址

admin_port: 顾名思义,就是管理员用来连接的端口号,注意如果admin_address没有设置的话,这个端口号是无效的

create_admin_listener_thread: 是否创建一个单独的listener线程来监听admin的链接请求,默认值是关闭的,facebook的建议是打开,否则其会使用已有的监听线程去监听admin连接。该参数同样需要admin_address打开, 否则没有任何影响

注意必须要有权限SERVICE_CONNECTION_ADMIN才能登陆该端口,否则会报错

根据文档描述2,admin port的连接个数不受max_connection或者Max_user_connection的限制。

参考文档

官方文档

Administrative Connection Management

WL#12138: Add Admin Port

相关代码

Multiple addresses for the –bind-address

通常在大规模允许的实例上我们不会去设置bind-address, 但在特定场景下还是有用的。从MySQL8.0.13开始,可以通过bind-address设置多个网络地址,对应release note:

To enable the server to listen on a set of addresses, the bind_address system variable now permits a list of comma-separated IP addresses or host names, not just a single address or name. For details, see Server System Variables.

也就是说如果你想通过bind-address绑定多个地址,需要使用8.0.13及之后的版本, 当然在之前的版本你也可以指定为使用 * 来匹配多个地址。

可以混合指定Ipv4和ipv6的地址,例如:

bind_address=198.51.100.20,2001:db8:0:f101::1

参考文档

bind address参数说明

WL#11652: Support multiple addresses for the –bind-address command option

相关代码

Performance for connect/disconnect

这是一个性能优化,尤其是针对频繁断开链接的短连接。这是因为MySQL里是使用一个全局大锁来保护LOCK_thd_list和LOCK_thd_remove来保护链接链表的。

优化的思路其实很简单直接:就是分区。所有的包括锁,链接链表,COND_thd_list都被分成8个分区(hardcode, 无法配置)来减少冲突, 根据thread id来分区。唯一的负面影响就是出于监控目的,可能performance schema需要获取全部分区来获得线程信息,但通常这是可以容忍的。

参考文档

WL#9250: Split LOCK_thd_list and LOCK_thd_remove mutexes

相关代码

Remove metadata from resultset

这是个老话题了,我们知道在mysql返回的结果集了除了用户的数据外,还包含了库,表名,列名,甚至表列的别名等信息,这些信息占据了返回值的很大一部分网络包开销,特别的,当你需要是点查询时,可能你的返回包的元数据要远远大于你需要的数据,而多数情况下,你并不需要这些元数据

例如当你返回n个列时,元数据包含:

- column count (n);
- n * column definitions

而每个column definition包含:

- catalog
- schema
- table alias
- table
- column alias
- column name
etc.

8.0版本里,你可以选择的移除resultset的metadata,通过参数resultset_metadata来控制,不过当我登陆终端,想设置这个参数时 却报错:

root@(none) 10:15:27>set session resultset_metadata = 'none';
ERROR 3640 (HY000): The client doesn't support optional metadata transfer

这是因为标准客户端的连接没有打开选项CLIENT_OPTIONAL_RESULTSET_METADATA,如果您使用C API,可以通在调用mysql_real_connect时把该flag设置到参数client_flag中,这样你就可以可选的设置这个session级别参数来关闭metadata了.

实际上在大概2012年左右,twitter mysql也做过类似的尝试,我在14年也做过类似的尝试,当时的测试结果如下:

After porting twitter’s patch ( Great thanks to Davi Arnaut) to MySQL5.6.16, I slightly changed it to make protocol_mode support more options:

0/METADATA_FULL: return all metadata, default value.
1/METADATA_REAL_COLUMN: only column name;
2/METADATA_FAKE_COLUMN: fake column name ,use 1,2...N instead of real column name
3/METADATA_NULL_COLUMN: use NULL to express the metadata information
4/METADATA_IGNORE: ignore metadata information, just for test..

CREATE TABLE `test_meta_impact` (
    `abcdefg1` int(11) NOT NULL AUTO_INCREMENT,
    `abcdefg2` int(11) DEFAULT NULL,
    `abcdefg3` int(11) DEFAULT NULL,
    `abcdefg4` int(11) DEFAULT NULL,
    ……
    ……
    `abcdefg40` int(11) DEFAULT NULL,
    PRIMARY KEY (`abcdefg1`)
    ) ENGINE=InnoDB AUTO_INCREMENT=229361 DEFAULT CHARSET=utf8

mysqlslap --no-defaults -uxx --create-schema=test -h$host -P $port --number-of-queries=1000000000 --concurrency=100 --query='SELECT * FROM test.test_meta_impact where abcdefg1 = 2'

METADATA_FULL : 3.48w TPS, Net send 113M
METADATA_REAL_COLUMN: 7.2W TPS, Net send 111M
METADATA_FAKE_COLUMN: 9.2W TPS , Net send 116M
METADATA_NULL_COLUMN: 9.6w TPS , Net send 115M
METADATA_IGNORE: 13.8w TPS, Net send 30M

可以看到去掉元数据后,不但网络传输少了至少三倍多, tps也上升了不少.

参考文档

WL#8134: Make metadata information transfer optional

resultset_metadata

C API

相关代码

异步query

从最新的8.0.16版本开始,新的C API开始支持异步的无阻塞的提交查询,相关的API包括:

  mysql_real_connect_nonblocking()

  mysql_real_query_nonblocking()

  mysql_store_result_nonblocking()

  mysql_next_result_nonblocking()

  mysql_fetch_row_nonblocking()

  mysql_free_result_nonblocking()

函数的名字就是原有阻塞性api加上后缀_nonblocking,比如说如果query的执行时间比较长,你可以先去干别的事情,然后再回来查询结果集。当然啦你必须要使用8.0.16或之后的client api

参考文档

WL#11381: Add asynchronous support into the mysql protocol

C API Asynchronous Interface

相关代码

MySQL · 引擎特性 · Multi-Valued Indexes 简述

$
0
0

本文主要简单介绍下8.0.17新引入的功能multi-valued index, 顾名思义,索引上对于同一个Primary key, 可以建立多个二级索引项,实际上已经对array类型的基础功能做了支持 (感觉官方未来一定会推出类似pg的array 列类型), 并基于array来构建二级索引,这意味着该二级索引的记录数可以是多于聚集索引记录数的,因而该索引不可以用于通常意义的查询,只能通过特定的接口函数来使用,下面的例子里会说明。

本文不对代码做深入了解,仅仅记录下相关的入口函数,便于以后工作遇到时能快速查阅。在最后附上了对应worklog的连接,感兴趣的朋友可以直接阅读worklog去了解他是如何实现的。

范例

摘录自官方文档

root@test 04:08:50>show create table customers\G
*************************** 1. row ***************************
Table: customers
Create Table: CREATE TABLE `customers` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    `custinfo` json DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY `zips` ((cast(json_extract(`custinfo`,_latin1'$.zip') as unsigned array)))
    ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

  root@test 04:08:53>select * from customers;
  +----+---------------------+-------------------------------------------------------------------+
  | id | modified            | custinfo                                                          |
  +----+---------------------+-------------------------------------------------------------------+
  |  1 | 2019-08-14 16:08:50 | {"user": "Jack", "user_id": 37, "zipcode": [94582, 94536]}        |
  |  2 | 2019-08-14 16:08:50 | {"user": "Jill", "user_id": 22, "zipcode": [94568, 94507, 94582]} |
  |  3 | 2019-08-14 16:08:50 | {"user": "Bob", "user_id": 31, "zipcode": [94477, 94536]}         |
  |  4 | 2019-08-14 16:08:50 | {"user": "Mary", "user_id": 72, "zipcode": [94536]}               |
  |  5 | 2019-08-14 16:08:50 | {"user": "Ted", "user_id": 56, "zipcode": [94507, 94582]}         |
  +----+---------------------+-------------------------------------------------------------------+
  5 rows in set (0.00 sec)

通过如下三个函数member of, json_contains, json_overlaps可以使用到该索引

root@test 04:09:00>SELECT * FROM customers WHERE 94507 MEMBER OF(custinfo->'$.zipcode');
+----+---------------------+-------------------------------------------------------------------+
| id | modified            | custinfo                                                          |
+----+---------------------+-------------------------------------------------------------------+
|  2 | 2019-08-14 16:08:50 | {"user": "Jill", "user_id": 22, "zipcode": [94568, 94507, 94582]} |
|  5 | 2019-08-14 16:08:50 | {"user": "Ted", "user_id": 56, "zipcode": [94507, 94582]}         |
+----+---------------------+-------------------------------------------------------------------+
2 rows in set (0.00 sec)

  root@test 04:09:41>SELECT * FROM customers  WHERE JSON_CONTAINS(custinfo->'$.zipcode', CAST('[94507,94582]' AS JSON));
  +----+---------------------+-------------------------------------------------------------------+
  | id | modified            | custinfo                                                          |
  +----+---------------------+-------------------------------------------------------------------+
  |  2 | 2019-08-14 16:08:50 | {"user": "Jill", "user_id": 22, "zipcode": [94568, 94507, 94582]} |
  |  5 | 2019-08-14 16:08:50 | {"user": "Ted", "user_id": 56, "zipcode": [94507, 94582]}         |
  +----+---------------------+-------------------------------------------------------------------+
  2 rows in set (0.00 sec)

  root@test 04:09:54>SELECT * FROM customers   WHERE JSON_OVERLAPS(custinfo->'$.zipcode', CAST('[94507,94582]' AS JSON));
  +----+---------------------+-------------------------------------------------------------------+
  | id | modified            | custinfo                                                          |
  +----+---------------------+-------------------------------------------------------------------+
  |  1 | 2019-08-14 16:08:50 | {"user": "Jack", "user_id": 37, "zipcode": [94582, 94536]}        |
  |  2 | 2019-08-14 16:08:50 | {"user": "Jill", "user_id": 22, "zipcode": [94568, 94507, 94582]} |
  |  5 | 2019-08-14 16:08:50 | {"user": "Ted", "user_id": 56, "zipcode": [94507, 94582]}         |
  +----+---------------------+-------------------------------------------------------------------+
  3 rows in set (0.00 sec)

接口函数

multi-value index是functional index的一种实现,列的定义是一个虚拟列,值是从json column上取出来的数组 数组上存在相同值的话,会只存储一个到索引上。支持的类型:DECIMAL, INTEGER, DATETIME,VARCHAR/CHAR。另外index上只能有一个multi-value column。

下面简单介绍下相关的接口函数

数组最大容量: 入口函数: ha_innobase::mv_key_capacity

插入记录: 入口函数 row_ins_sec_index_multi_value_entry 通过类Multi_value_entry_builder_insert来构建tuple, 然后调用正常的接口函数row_ins_sec_index_entry插入到二级索引中. 已经解析好,排序并去重的数据存储在结构struct multi_value_data , 指针在dfield_t::data中. multi_value_data结构也是multi-value具体值的内存表现

删除记录: 入口函数: row_upd_del_multi_sec_index_entry 基于类Multi_value_entry_builder_normal构建tuple, 并依次从索引中删除

更新记录 入口函数:row_upd_multi_sec_index_entry 由于可能不是所有的二级索引记录都需要更新,需要计算出diff,找出要更新的记录calc_row_difference –> innobase_get_multi_value_and_diff, 设置一个需要更新的bitmap

事务回滚 相关函数:

row_undo_ins_remove_multi_sec
row_undo_mod_upd_del_multi_sec
row_undo_mod_del_mark_multi_sec

回滚的时候通过trx_undo_rec_get_multi_value从undo log中获取multi-value column的值,通过接口Multi_value_logger::read来构建并存储到field data中

记录undo log 函数: trx_undo_store_multi_value

通过Multi_value_logger::log将multi-value的信息存储到Undo log中. ‘Multi_value_logger’是一个辅助类,用于记录multi-value column的值以及如何读出来

purge 二级索引记录 入口函数:

row_purge_del_mark
row_purge_upd_exist_or_extern_func
    |--> row_purge_remove_multi_sec_if_poss

参考文档

WL#10604: Create multi-value index

WL#8763: support multi-value functional index for InnoDB

WL#8955: Add support for multi-valued indexes

官方文档

AliSQL · 引擎特性 · Statement Queue

$
0
0

背景

MySQL 的 server 层和引擎层在 statement 并发执行过程中,有很多串行化的点,在 DML 语句中,事务锁冲突比较常见,InnoDB 中事务锁的最细粒度是行级锁,如果语句针对相同行进行并发操作,会导致冲突比较严重,系统吞吐量会随着并发的增加而递减。
AliSQL 设计了针对语句的排队机制,相同的行或者不同的语句进行分桶排队,尽可能的把具有相同冲突可能的在一个桶内排队,减少 conflict 的开销。 

语法

变量

系统提供了两个变量来定义 ccl queue 的 bucket 数量和大小。

1.ccl_queue_bucket_count

表示:一共有多少个bucket, 默认值:4,取值范围:[1, 64]

2.ccl_queue_bucket_size

表示:一个bucket 允许并发数是多少, 默认值:64, 取值范围:[1, 4096]

Hint

系统支持两个hint语法:

1.ccl_queue_value

根据 value 的值进行 hash 分桶

语法:
/*+ CCL_QUEUE_VALUE([INT | STRING)] */

例子:
update /*+ ccl_queue_value(1) */ t set c=c+1 where id = 1;

update /*+ ccl_queue_value('xpchild') */ t set c=c+1 
where name = 'xpchild';

2.ccl_queue_field

根据 where 条件中的 field 指定的值进行 hash 分桶

语法:
/*+ CCL_QUEUE_FIELD(STRING) */

例如:
update /*+ ccl_queue_field("id") */ t set c=c+1
where id = 1 and name = 'xpchild';

在where条件中查找id字段指定的条件常量值来进行分桶

注意: ccl_queue_field 填入的字段名字, 在 where 条件的查找过程中:

  1. 只支持对裸字段的二元运算符的条件
  2. 二元运算的右值必须是数字或者字符串

接口

系统支持两个接口进行查询当前的状态:

1.dbms_ccl.show_ccl_queue()

  mysql> call dbms_ccl.show_ccl_queue();   
  +------+-------+-------------------+---------+---------+----------+
  | ID   | TYPE  | CONCURRENCY_COUNT | MATCHED | RUNNING | WAITTING |
  +------+-------+-------------------+---------+---------+----------+
  |    1 | QUEUE |                64 |       1 |       0 |        0 |
  |    2 | QUEUE |                64 |   40744 |      65 |        6 |
  |    3 | QUEUE |                64 |       0 |       0 |        0 |
  |    4 | QUEUE |                64 |       0 |       0 |        0 |
  +------+-------+-------------------+---------+---------+----------+
  4 rows in set (0.01 sec)

CONCURRENCY_COUNT: 最大并发数
MATCHED: 命中规则的累积数量
RUNNING:当前并发的数量
WAITTING: 当前等待的数量

2.dbms_ccl.flush_ccl_queue()

清理内存中的状态, 重新加载

  mysql> call dbms_ccl.flush_ccl_queue();                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       Query OK, 0 rows affected (0.00 sec)

  mysql> call dbms_ccl.show_ccl_queue();
  +------+-------+-------------------+---------+---------+----------+
  | ID   | TYPE  | CONCURRENCY_COUNT | MATCHED | RUNNING | WAITTING |
  +------+-------+-------------------+---------+---------+----------+
  |    1 | QUEUE |                64 |       0 |       0 |        0 |
  |    2 | QUEUE |                64 |       0 |       0 |        0 |
  |    3 | QUEUE |                64 |       0 |       0 |        0 |
  |    4 | QUEUE |                64 |       0 |       0 |        0 |
  +------+-------+-------------------+---------+---------+----------+
  4 rows in set (0.00 sec)

效果

针对单行进行并发 update 的场景下,目前进行的测试,相比较原生的 MySQL, AliSQL 有接近 4 倍的提升。

配合 outline 在线修改

为了能够快速在线修改 SQL statement 的并发控制,而不介入冗长的应用业务代码的修改,这里可以使用AliSQL 提供的 Outline 来配合, 下面使用 sysbench 的 update_non_index 作为一个例子:

测试环境:

测试表结构:

  CREATE TABLE `sbtest1` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `k` int(10) unsigned NOT NULL DEFAULT '0',
      `c` char(120) NOT NULL DEFAULT '',
      `pad` char(60) NOT NULL DEFAULT '',
      PRIMARY KEY (`id`),
      KEY `k_1` (`k`)
      ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 MAX_ROWS=1000000;

测试语句:

  UPDATE sbtest1 SET c='xpchild' WHERE id=0;

测试脚本:

  ./sysbench 
  --mysql-host= {$ip}
  --mysql-port= {$port}
  --mysql-db=test 
  --test=./sysbench/share/sysbench/update_non_index.lua 
  --oltp-tables-count=1 
  --oltp_table_size=1 
  --num-threads=128
  --mysql-user=u0

测试过程

1. 在线增加 outline 

  mysql> CALL DBMS_OUTLN.add_optimizer_outline('test', '', 1, 
      ' /*+ ccl_queue_field("id") */ ',
      "UPDATE sbtest1 SET c='xpchild' WHERE id=0");
  Query OK, 0 rows affected (0.01 sec)

2. 查看 outline 并验证 

  mysql> call dbms_outln.show_outline();
  +------+--------+------------------------------------------------------------------+-----------+-------+------+--------------------------------+------+----------+---------------------------------------------+
  | ID   | SCHEMA | DIGEST                                                           | TYPE      | SCOPE | POS  | HINT                           | HIT  | OVERFLOW | DIGEST_TEXT                                 |
  +------+--------+------------------------------------------------------------------+-----------+-------+------+--------------------------------+------+----------+---------------------------------------------+
  |    1 | test   | 7b945614749e541e0600753367884acff5df7e7ee2f5fb0af5ea58897910f023 | OPTIMIZER |       |    1 |  /*+ ccl_queue_field("id") */  |    0 |        0 | UPDATE `sbtest1` SET `c` = ? WHERE `id` = ? |
  +------+--------+------------------------------------------------------------------+-----------+-------+------+--------------------------------+------+----------+---------------------------------------------+
  1 row in set (0.00 sec)

3. 验证 outline 生效

  mysql> explain UPDATE sbtest1 SET c='xpchild' WHERE id=0;
  +----+-------------+---------+------------+-------+---------------+---------+---------+-------+------+----------+-------------+
  | id | select_type | table   | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra       |
  +----+-------------+---------+------------+-------+---------------+---------+---------+-------+------+----------+-------------+
  |  1 | UPDATE      | sbtest1 | NULL       | range | PRIMARY       | PRIMARY | 4       | const |    1 |   100.00 | Using where |
  +----+-------------+---------+------------+-------+---------------+---------+---------+-------+------+----------+-------------+
  1 row in set, 1 warning (0.00 sec)

  mysql> show warnings;
  +-------+------+-----------------------------------------------------------------------------------------------------------------------------+
  | Level | Code | Message                                                                                                                     |
  +-------+------+-----------------------------------------------------------------------------------------------------------------------------+
  | Note  | 1003 | update /*+ CCL_QUEUE_FIELD('id') */ `test`.`sbtest1` set `test`.`sbtest1`.`c` = 'xpchild' where (`test`.`sbtest1`.`id` = 0) |
  +-------+------+-----------------------------------------------------------------------------------------------------------------------------+
  1 row in set (0.00 sec)

4. 查看 ccl queue 状态 

  mysql> call dbms_ccl.show_ccl_queue();
  +------+-------+-------------------+---------+---------+----------+
  | ID   | TYPE  | CONCURRENCY_COUNT | MATCHED | RUNNING | WAITTING |
  +------+-------+-------------------+---------+---------+----------+
  |    1 | QUEUE |                64 |       0 |       0 |        0 |
  |    2 | QUEUE |                64 |       0 |       0 |        0 |
  |    3 | QUEUE |                64 |       0 |       0 |        0 |
  |    4 | QUEUE |                64 |       0 |       0 |        0 |
  +------+-------+-------------------+---------+---------+----------+
  4 rows in set (0.00 sec)

5. 开启测试 

  sysbench 
  --mysql-host= {$ip}
  --mysql-port= {$port}
  --mysql-db=test 
  --test=./sysbench/share/sysbench/update_non_index.lua 
  --oltp-tables-count=1 
  --oltp_table_size=1 
  --num-threads=128
  --mysql-user=u0

6. 验证测试效果 

  mysql> call dbms_ccl.show_ccl_queue();
  +------+-------+-------------------+---------+---------+----------+
  | ID   | TYPE  | CONCURRENCY_COUNT | MATCHED | RUNNING | WAITTING |
  +------+-------+-------------------+---------+---------+----------+
  |    1 | QUEUE |                64 |   10996 |      63 |        4 |
  |    2 | QUEUE |                64 |       0 |       0 |        0 |
  |    3 | QUEUE |                64 |       0 |       0 |        0 |
  |    4 | QUEUE |                64 |       0 |       0 |        0 |
  +------+-------+-------------------+---------+---------+----------+
  4 rows in set (0.03 sec)

ccl queue 显示命中了10996 次排队, 当前运行并发63个,排队等待4个。 

  mysql> call dbms_outln.show_outline();
  +------+--------+-----------+-----------+-------+------+--------------------------------+--------+----------+---------------------------------------------+
  | ID   | SCHEMA | DIGEST    | TYPE      | SCOPE | POS  | HINT                           | HIT    | OVERFLOW | DIGEST_TEXT                                 |
  +------+--------+-----------+-----------+-------+------+--------------------------------+--------+----------+---------------------------------------------+
  |    1 | test   | xxxxxxxxx | OPTIMIZER |       |    1 |  /*+ ccl_queue_field("id") */  | 115795 |        0 | UPDATE `sbtest1` SET `c` = ? WHERE `id` = ? |
  +------+--------+-----------+-----------+-------+------+--------------------------------+--------+----------+---------------------------------------------+
  1 row in set (0.00 sec)

outline 显示命中了115795 次。 

Ccl queue 可以配合着 outline 进行在线修改业务,方便快捷。


Database · 理论基础 · Palm Tree

$
0
0

这篇文章介绍 B+ 树的无锁并发算法 Palm Tree

论文链接:Parallel Architecture-Friendly Latch-Free Modifications to B+ Trees on Many-Core Processors

为什么会介绍这个算法?

之前实现过一个有锁并发的 B+ 树算法,Efficient Locking for Concurrent Operations on B-Trees

这是一个很经典的算法,它通过引入节点读写锁和层级指针来实现有锁并发,后续有不少论文都在这个基础上进行了改进。但是这个算法也有缺陷,

  1. 无法在多核情况下保证性能
  2. 对于顺序插入,性能太差
  3. 对删除操作不友好

前两个缺点的根本原因在于线程之间的同步开销太大。所以开始寻找新算法,然后发现了 Palm Tree。

这个算法有什么特别之处?

  1. 真无锁,既没有将节点的某个字段用于加锁,也不用 MVCC
  2. 高可扩展性,性能随着线程数增加而增加
  3. 顺序插入性能良好
  4. 满足序列化(serializability)

常规并发算法

首先我们看一下常规的并发算法,

上图代表了常规并发算法,这类算法主要特点就是每个线程把自己携带的数据处理完,同时通过加锁或者原子操作来避免并发访问的问题。缺点主要有以下几方面:

1. 加锁导致线程阻塞,CPU 利用率下降,(超)高并发情况下问题尤为严重
2. 原子操作导致的过多次重试导致 CPU 的有效利用率下降
3. 差劲的局部数据访问性能,比如顺序插入

有个值得提出的点是 LevelDB/RocksDB 中的 SkipList 使用原子操作但是插入并不需要重试,那是因为只是支持一写多读,如果是多写多读的话还是需要重试,参考 RocksDB 的 InlineSkipList。

Palm Tree 算法

接下来我们介绍 Palm Tree。下图是论文中算法的整体流程:

对于 Palm Tree 来说,维护N个工作线程,N个线程会分工合作来处理数据。

第1行:Partition-Input,把我们要处理的 batch 内的数据平均分配给每个线程。

1

第2行:Search,每个线程对分配到的 key 下降到叶子节点,这个过程是不需要加锁与重试的,因为这个过程中所有节点都是只读的。

2

第3行:Sync,线程间进行同步,保证所有线程都下降到了叶子节点。接下来要解决并发访问的问题。在这里你可以把 sync 理解为一个全局的 barrier。

第4行:Redistribute-Work,可以看到上图中不同的线程落到了同一个叶子节点,这会带来并发访问的问题,所以我们需要进行数据的再分配,保证同一个节点只由一个线程进行读写操作

第5行:Resolve-Hazards,要保证算法满足 serializability 这个特性,我们需要对操作进行重排,使得他们和之前在 Batch 中的顺序是一致的。

3-4-5

当3-4-5行执行完毕后,可以得到上面这张图。

6-7

第6-7行:Modify-Node,每个线程根据自己最终分配到的数据,在相应的叶子节点进行实际的读写操作,不需要加锁,因为这时候每个叶子节点只属于一个线程。

第8行:Sync,所有线程进行完叶子节点的处理后需要进行同步。

9-13

第9-13行,节点可能存在分裂或合并,这时候分裂或者合并信息相当于新的读写操作,我们要把它们反映到相应的上层节点中。这时候对于每一层需要进行(redistribute-work,分裂或合并内部结点,同步)这样的步骤,直到根节点。

15

第15行:Handle-Root,根节点可能存在分裂或合并,只由编号为0的线程处理即可。

以上就是这个算法的整体流程。

Palm Tree 优化

算法自身的优化主要是两个,提前排序(pre-sort)和点对点同步(point-to-point synchronization)。

提前排序

palm tree 算法每次执行一个 batch,提前排序就是把这个 batch 里所有 key 按照顺序排好。

如果不排序,对于一个 batch,每个线程最后落在的叶子节点分布可能是这样的(不同颜色的线代表不同线程):

如果排序,那么每个线程落在的叶子节点的分布可能是这样的:

可以看到同一线程的节点分布是比较紧凑的,这样有两个好处:

  1. 在下降阶段,可以获得更好的缓存局部性
  2. 帮助获得更细粒度的线程同步

对于1,我们假设每个 batch 里有100个 key,然后一共有4个线程,那么每个线程会分配到25 个 key。对于一个线程来说,原来(排序前)key 的分布可能是 [aa, zz),排序完后,key 的分布可能是 [aa, gg),当 key 涉及的区间更紧凑时,它们在下降阶段涉及到的 B+ 树节点就少,所以可以减少 cache miss,获得更好的 cache locality。

点对点同步(point-to-point synchronization)

由上图2可以看到,当提前排序进行完后,每个线程落到的叶子节点范围是一定的。假设我们从左往右对叶子节点层进行标记,如果有100个叶子节点那就是【1,100】的区间。

对于4个线程来说,它们下降到的最终的叶子节点分布可能是这样的:

th1: [1,2,3,3,6,9],区间[1,9]
th2: [9,9,17,23,28],区间[9,28]
th3: [28,31,45,56,78],区间[28,78]
th4: [78,78,82,93,99],区间[78,99]

为了简便处理,之前我们在下降完后会进行一次全局同步(global synchronization),同步完后再进行任务分配并修改叶子节点。

但是现在不需要了,因为每个线程处理的 key 范围是不相交的,对于线程2来说,其实它只需要和线程1、线程3进行同步,此时线程4涉及到的叶子节点和线程2是不冲突的。

所以点对点同步是这样操作的,对于线程 i,通常情况下 i只需要和线程 i-1、线程 i+1进行同步,即相邻的线程之间进行同步即可。这样可以大大减少线程同步的开销。

具体来说,一个线程的一次同步需要确定四个值:

their_last: 前一个线程的最后一个节点
my_first: 我的第一个节点
my_last: 我的最后一个节点
their_first: 后一个线程的第一个节点

当前线程只需要等待前一个线程把它的最后一个节点发过来,同时等待后一个线程把它的第一个节点发过来就行了。

会出现线程 i需要和 i+2进行同步的情况吗?当然。当整个 batch 中的 key 比较紧凑时,这时 their_last 就代表前面某些或者前面所有线程的最后一个叶子节点,their_first 就代表后面某些或者后面所有线程的第一个叶子节点。举个极端的例子,顺序插入,这时候所有线程确定的四个值都是一样的,their_last == my_first == my_last == their_first,那这时候点对点同步就退化为了全局同步。

以上就是提前排序和点对点同步优化的介绍。

总结

整体上而言这个算法分为四个阶段:

1. Divide & Search
2. Redistribute-Work & Resolve-Hazards & Modify Leaf Nodes
3. Redistribute-Work & Modify Internal Nodes
4. Modify Root Node

相较于其他并发算法,Palm Tree 算法的每个线程不再孤立,而是会进行合作,然后通过线程间的同步(比如 Barrier)来保证各个线程整体上处于同一阶段以及写操作的隔离,避免节点加锁和重试,也就是之前提到的特性1。

同时,对于一次读写操作来说,耗时最多部分就是下降过程以及叶子节点的修改这两个部分。对于 Palm Tree 来说,耗时最多的操作是平均分配给各个线程独立进行的,所以理论上可以做到性能的线性扩展,也就是之前提到的这个算法的特性2。

下面这张图比较形象地描述了这个算法。

相同颜色的线代表同一线程

开源

这个算法的实现在这里:UncP/aili,代码在 palm/这个文件夹里。

AliSQL · 引擎特性 · Returning

$
0
0

背景

MySQL 对于 statement 执行结果报文通常分为两类 Resultset 和 OK/ERR,针对 DML 语句则返回OK/ERR 报文,其中包括几个影响记录,扫描记录等属性。但在很多业务场景下,通常 INSERT/UPDATE/DELETE 这样的DML语句后,都会跟随 SELECT 查询当前记录内容,以进行接下来的业务处理, 为了减少一次 Client <-> DB Server 交互,类似 PostgreSQL / Oracle 都提供了 returning clause 支持 DML 返回 Resultset。

AliSQL 为了减少对 MySQL 语法兼容性的侵入,并支持 returning 功能, 采用了 native procedure 的方式,使用DBMS_TRANS package,统一使用 returning procedure 来支持 DML 语句返回 Resultset。

语法

DBMS_TRANS.returning(Field_list=>, Statement=>);

其中: 
Field list : 代表期望的返回字段,以 “,” 进行分割,支持 * 号表达;
Statement :表示要执行的DML 语句, 支持 INSERT / UPDATE / DELETE;

INSERT Returning

针对 insert 语句, returning proc 返回插入到表中的记录内容;

例如:

CREATE TABLE `t` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `col1` int(11) NOT NULL DEFAULT '1',
    `col2` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB;

mysql> call dbms_trans.returning("*", "insert into t(id) values(NULL),(NULL)");
+----+------+---------------------+
| id | col1 | col2                |
+----+------+---------------------+
|  1 |    1 | 2019-09-03 10:39:05 |
|  2 |    1 | 2019-09-03 10:39:05 |
  +----+------+---------------------+
2 rows in set (0.01 sec)

如果没有填入任何 Fields, returning 将退化成 OK/ERR 报文: 

  mysql> call dbms_trans.returning("", "insert into t(id) values(NULL),(NULL)");
  Query OK, 2 rows affected (0.01 sec)
  Records: 2  Duplicates: 0  Warnings: 0

  mysql> select * from t;
  +----+------+---------------------+
  | id | col1 | col2                |
  +----+------+---------------------+
  |  1 |    1 | 2019-09-03 10:40:55 |
  |  2 |    1 | 2019-09-03 10:40:55 |
  |  3 |    1 | 2019-09-03 10:41:06 |
  |  4 |    1 | 2019-09-03 10:41:06 |
  +----+------+---------------------+
  4 rows in set (0.00 sec)

注意:  INSERT returning 只支持 insert values 形式的语法,类似create as, insert select 不支持, 例如:

  mysql> call dbms_trans.returning("", "insert into t select * from t");
  ERROR 7527 (HY000): Statement didn't support RETURNING clause

UPDATE Returning

针对 update 语句, returning 返回更新后的记录:

例如:

  mysql> call dbms_trans.returning("id, col1, col2", "update t set col1 = 2 where id >2");
  +----+------+---------------------+
  | id | col1 | col2                |
  +----+------+---------------------+
  |  3 |    2 | 2019-09-03 10:41:06 |
  |  4 |    2 | 2019-09-03 10:41:06 |
  +----+------+---------------------+
  2 rows in set (0.01 sec)

注意: UPDATE returning 不支持多表 update 语句。

DELETE Returning

针对 delete 语句, returning 返回删除的记录前映像:

例如:

  mysql> call dbms_trans.returning("id, col1, col2", "delete from t where id < 3");
  +----+------+---------------------+
  | id | col1 | col2                |
  +----+------+---------------------+
  |  1 |    1 | 2019-09-03 10:40:55 |
  |  2 |    1 | 2019-09-03 10:40:55 |
  +----+------+---------------------+
2 rows in set (0.00 sec)

注意

1. 事务上下文 DBMS_TRANS.returning() 不是事务性语句,根据 DML statement 来继承 事务上下文,
结束事务需要显式的 COMMIT 或者 ROLLBACK。

2. 字段不支持计算    Field list 中,只支持表中原生的字段,或者 * 号, 不支持进行计算或者聚合等操作。

PgSQL · 最佳实践 · 回归测试探寻

$
0
0

Postgres regress test

每当开发完一个新的功能后,要想让其稳定的上线的前提是必须完成回归测试,回归测试定义为一种软件测试,用于确认最近的程序或代码更改未对现有功能产生负面影响。回归测试只不过是已经执行的测试用例的全部或部分选择,这些测试用例被重新执行以确保现有功能正常工作。对于最先进的开源数据库Postgresql当然也提供了它的回归测试。我们在官方文档中可以找到其具体的使用方法。

make check   			 #  parallel running the Tests Against a Temporary Installation 
make installcheck  			    # serial running the Tests Against an Existing Installation 
make installcheck-paralle   # parallel  running the Tests Against an Existing Installation 

同样的,当我们开发完一个较重要的feature后,也需要添加一些对应新test case,以免以后开发的功能会影响到这个feature。不巧的是官方文档并没有给出添加test case的说明,添加完自己的test case后,我们如何单独的运行这个case,也无法在官网中找到答案,所以我们这次就来对于Postgres regress一探究竟,看一看Postgres的回归测试是如果做的。首先我们进入到Postgres 源码中的src/test/regress中,根据阅读源码的经验,我们首先读一下REDAME是个明智之举。

Documentation concerning how to run these regression tests and interpret the 
results can be found in the PostgreSQL manual, in the chapter "Regression Tests".

令人尴尬的是README将我们又推向了官方文档,所以我们不得不自己到代码中去一探究竟。既然官方让我们运行make check,那么我们先去Makefile是一个不错的选择。

pg_regress

在makefile中,我们很容易看到make installcheck 相当于编译源码后运行了下面的命令:

pg_regress --inputdir=. --bindir='tmp_basedir_polardb_pg_1100_bld/bin'  
--dlpath=. --max-concurrent-tests=20 --user=regress  --schedule=./serial_schedule 

我们可以从Makefile看出src/test/regress下面的 pg_regress.c pg_regress_main.c regress.c被编译成了一个二进制为 pg_regress。执行以下命令:

./pg_regress -h
####################################\
PostgreSQL regression test driver

Usage:
  pg_regress [OPTION]... [EXTRA-TEST]...

Options:
      --bindir=BINPATH          use BINPATH for programs that are run;
                                if empty, use PATH from the environment
      --config-auth=DATADIR     update authentication settings for DATADIR
      --create-role=ROLE        create the specified role before testing
      --dbname=DB               use database DB (default "regression")
      --debug                   turn on debug mode in programs that are run
      --dlpath=DIR              look for dynamic libraries in DIR
      --encoding=ENCODING       use ENCODING as the encoding
  -h, --help                    show this help, then exit
      --inputdir=DIR            take input files from DIR (default ".")
      --launcher=CMD            use CMD as launcher of psql
      --load-extension=EXT      load the named extension before running the
                                tests; can appear multiple times
      --load-language=LANG      load the named language before running the
                                tests; can appear multiple times
      --max-connections=N       maximum number of concurrent connections
                                (default is 0, meaning unlimited)
      --max-concurrent-tests=N  maximum number of concurrent tests in schedule
                                (default is 0, meaning unlimited)
      --outputdir=DIR           place output files in DIR (default ".")
      --schedule=FILE           use test ordering schedule from FILE
                                (can be used multiple times to concatenate)
      --temp-instance=DIR       create a temporary instance in DIR
      --use-existing            use an existing installation
      --no-restrict-auth        disable restricted authentication settings
  -V, --version                 output version information, then exit

Options for "temp-instance" mode:
      --no-locale               use C locale
      --port=PORT               start postmaster on PORT
      --temp-config=FILE        append contents of FILE to temporary config
      --temp-initdb-opts=OPTS   additional flags passed to initdb

Options for using an existing installation:
      --host=HOST               use postmaster running on HOST
      --port=PORT               use postmaster running at PORT
      --user=USER               connect as USER

The exit status is 0 if all tests passed, 1 if some tests failed, and 2
if the tests could not be run for some reason.

Report bugs to <support@enterprisedb.com>.

pg_regress的入口在pg_regress_main.c中:

int
main(int argc, char *argv[])
{
	return regression_main(argc, argv, psql_init, psql_start_test);
}

其中psql_init是初始化regress时连接的数据库,psql_start_test对于每一个test case调用执行。下面我就来简单列出regression_main中的逻辑,如果有兴趣的同学可以自己阅读源码。

  • 1.参数解析
  • 2.如果是在临时实例上运行,构建临时实例。
  • 3.create 测试数据库和测试用户
  • 4.读取schedule文件对于每一个schedule文件做:
    • 4.1. 读取每一行,以test开头,获取后面的每一个case。
    • 4.2. 每一个case启动一个进程 调用psql 执行case sql脚本。
    • 4.3. 将执行sql脚本的输出重定向到./results目录下。
    • 4.4. 将./results下的输出文件和./expected下的下相同文件名进行diff。
    • 4.5. 如果没有差异ok,有差异falied。
  • 5.如果是临时实例,关闭实例。
  • 6.结束。

以上为pg_regress的源码执行流程,其中读取schedule文件是关键的一个参数,我们知道在回归测试时,有并行和串行之分。本质上就是在–schedule 指定/test/regress下的serial_schedule 和parallel_schedule 文件的区别,我们打开这两个文件对比入下:

  • serial_schedule
    test: tablespace
    test: boolean
    test: char
    test: name
    test: varchar
    test: text   
    ......
    
  • parallel_schedule
    # ----------
    # The first group of parallel tests
    # ----------
    test: boolean char name varchar text int2 int4 int8 oid float4 float8 bit numeric txid uuid enum money rangetypes pg_lsn regproc
    ......
    

    很明了的发现串行schedule内是一个test对应一个case,而并行schedule中一个test对应这多个case。在源码中, pg_regress会对去每一行test中,每个case 启动一个进程,这就是串行和并行测试的区别了。

如何添加新的 regress test case。

经过前面的分析,我们可以发现添加新的test case 可以如下步骤:

  1. 在regress/sql中添加新的sql文件,如test1.sql,test2.sql, test3.sql
  2. 使用psql执行添加的sql文件,psql -X -a -q -d databasename > /regress/expected/test1.sql . 将其每个sql文件的执行结果放入expected目录下作为期望文件,用于下次回归进行比对。
  3. 可以在serial_schedule 和parallel_schedule 中添加自己新建的case ,也可以自己单独写一个schedule文件。
  4. 如果自己新建了schedule文件,在Makefile中添加对应的make 标签。serial_schedule 和parallel_schedule 中添加则不需要。

尽管我们已经掌握了regress test的基本原理,就是并行或者串行的执行sql和期望的执行结果进行比对。但是我们有时候会需要这种需求,并不完全并行或者执行一些sql去测试,比如我们需要两个以上的session,相互交互的以固定顺序执行sql,一般用来测试事务和lock以及隔离级别等。然而在regress下我们却无法满足这种schedule。所以下面我们就来介绍以下Postgres的另一个重要的测试工具pg_isolation_regress。

pg_isolation_regress

同样地,我们去了解一下这个工具如何使用,就需要进入它的源码目录一探究竟,目录位于src/test/isolation。幸运的是它提供了一个看起来相当丰富的README。首段翻译如下:

该目录包含一组针对并发行为的PostgreSQL的的测试。这些测试需要运行多个交互事务,
这需要管理多个并发连接,因此无法使用正常的pg_regress程序进行测试。
名字“隔离”来自这个事实,原来的动机是测试可序列化隔离级别;
但测试其他类型的并发行为也被添加了。
  1. 其中介绍了pg_isolation_regress用法如下:
    • make installcheck #可以在已经安装的实例上运行所有case,可以通过PGPORT指定端口。
    • ./pg_isolation_regress case1 case2 #运行指定case1,case2。
  2. 添加一个新的测试,将casename.spec文件放在specs /子目录中,添加预期的输出在expected /子目录中,并将测试的名称添加到isolation_schedule文件,基本用法和pg_regress差不多。
  3. case写法可以参考specs /子目录中.sepc文件。每个case定义的格式如下:
setup { <SQL> }

  The given SQL block is executed once, in one session only, before running
  the test.  Create any test tables or other required objects here.  This
  part is optional.  Multiple setup blocks are allowed if needed; each is
  run separately, in the given order.  (The reason for allowing multiple
  setup blocks is that each block is run as a single PQexec submission,
  and some statements such as VACUUM cannot be combined with others in such
  a block.)

teardown { <SQL> }

  The teardown SQL block is executed once after the test is finished. Use
  this to clean up in preparation for the next permutation, e.g dropping
  any test tables created by setup. This part is optional.

session "<name>"

  There are normally several "session" parts in a spec file. Each
  session is executed in its own connection. A session part consists
  of three parts: setup, teardown and one or more "steps". The per-session
  setup and teardown parts have the same syntax as the per-test setup and
  teardown described above, but they are executed in each session. The
  setup part typically contains a "BEGIN" command to begin a transaction.

  Each step has the syntax

  step "<name>" { <SQL> }

  where <name> is a name identifying this step, and SQL is a SQL statement
  (or statements, separated by semicolons) that is executed in the step.
  Step names must be unique across the whole spec file.

permutation "<step name>" ...

  A permutation line specifies a list of steps that are run in that order.
  Any number of permutation lines can appear.  If no permutation lines are
  given, the test program automatically generates all possible orderings
  of the steps from each session (running the steps of any one session in
  order).  Note that the list of steps in a manually specified
  "permutation" line doesn't actually have to be a permutation of the
  available steps; it could for instance repeat some steps more than once,
  or leave others out.

读完了readme,同时又勾起了我们的好奇心,pg_isolation_regress其内部到底是如何实现的呢?

isolationtester

打开pg_isolation_regress源码我们就会看到isolation_main文件,在Makefile中isolation_main.c, isolationtester.c 被编译了成了一个叫做isolationtester的二进制。我们就从isolation_main 入口开看一看其逻辑。其main函数如下:

int
main(int argc, char *argv[])
{
	return regression_main(argc, argv, isolation_init, isolation_start_test);
}

咋一看,和pg_regress没什么区别呀? 仔细一看原来是,其传奇的的函数指针不同。前面我们了解了pg_regress的参数是psql_init 和psql_start_test ,pg_regress的原理对于给一个case启动以进程用psql来执行case中sql。 但是在pg_isolation_regress中实现了多个session交互的逻辑, 单单使用psql是无法做到的,所以原来,pg_isolation_regress自己实现了一个isolation_start_test来执行每个case,我们进入isolation_start_test 就会发现,其使用的一个和psql不一样的工具就叫做isolationtester。 其实现在isolationtester.c 中,有兴趣的同学可以自己去品尝。其中的主要逻辑如下:

  • 1.参数解析
  • 2.读取将要运行的case即 spec文件。
  • 3.解析spec文件,初始化多个session和对应step。
  • 4.检查step是否有重名。
  • 5.使用libpq建立每一个session连接。
  • 6.按步骤设置会话索引字段。
  • 7.运行规范中指定的排列,如果没有明确指定,则运行所有排列。
  • 8.将./results下的输出文件和./expected下的下相同文件名进行diff。
  • 9.相同ok,有差异这failed
  • 10.关闭所有连接,exit结束。

总结

本文介绍了如何使用Postgres的测试工具进行回归测试。其中主要介绍了两个工具分别为pg_regresss和 pg_isolation_regress。这两个工具都是可以用来回归的,pg_regresss的原理是使用psql来运行每一个case,可以串行的运行,也可以并行的运行。而pg_isolation_regress 是使用libpq来开启多个连接进行交互的运行sql,适用于并发锁的测试。

MongoDB · 最佳实践 · 哈希分片为什么分布不均匀

$
0
0

今天接到一个用户反馈的问题,sharding集群,使用wiredtiger引擎,某个DB下集合全部用的hash分片,show dbs发现其中一个shard里该DB的大小,跟其他的集合差别很大,其他基本在60G左右,而这个shard在200G左右?

_2017_08_02_7_31_54

由于这个DB下有大量的集合及索引,一眼也看不出问题,写了个脚本分析了一下,得到如下结论

  1. somedb 下所有集合都是hash分片,并且chunk的分布是比较均匀的
  2. show dbs 反应的是集合及索引对应的物理文件大小
  3. 集合的数据在各个shard上逻辑总大小是接近的,只有shard0占用的物理空间比其他大很多

从shard0上能找到大量 moveChunk 的记录,猜测应该是集合的数据在没有开启分片的情况下写到shard0了,然后开启分片后,从shard0迁移到其他shard了,跟用户确认的确有一批集合是最开始没有分片。

所以这个问题就转换成了,为什么复制集里集合的逻辑空间与物理空间不一致?即collection stat 里 sizestorageSize的区别。

mymongo:PRIMARY> db.coll.stats()
{
	"ns" : "test.coll",
	"size" : 30526664,
	"count" : 500808,
	"avgObjSize" : 33,
	"storageSize" : 19521536,
	"capped" : false,
	....
}

逻辑存储空间与物理存储空间有差距的主要原因

  1. 存储引擎存储时,需要记录一些额外的元数据信息,这会导致物理空间总和比逻辑空间略大
  2. 存储引擎可能支持数据压缩,逻辑的数据块存储到磁盘时,经过压缩可能比逻辑数据小很多了(具体要看数据的特性,极端情况下压缩后数据变大也是有可能的)
  3. 引擎对删除空间的处理,很多存储引擎在删除数据时,考虑到效率,都不会立即去挪动数据回收删除的存储空间,这样可能导致删除很多文档后,逻辑空间变小,但物理空间并没有变小。如下图所示,灰色的文档删除表示被删除。删除的空间产生很多存储碎片,这些碎片空间不会立即被回收,但有新文档写入时,可以立即被复用。

_2017_08_02_8_03_44

而上述case里,集合数据先分到一个shard,然后启用分片后,迁移一部分到其他shard,就是一个典型的产生大量存储碎片的例子。存储碎片对服务通常影响不大,但如果因为空间不够用了需要回收,如何去强制的回收这些碎片空间?

  • 数据清理掉重新加入复制集同步数据,或者直接执行resync命令 (确保有还有其他的数据备份)
  • 对集合调用 compact 命令

参考资料

PgSQL · 应用案例 · PG有standby的情况下为什么停库可能变慢?

$
0
0

背景

PostgreSQL 有3种停库模式:

“Smart” mode waits for all active clients to disconnect and any online backup to finish.
If the server is in hot standby, recovery and streaming replication will be terminated once all clients have disconnected.

“Fast” mode (the default) does not wait for clients to disconnect and will terminate an online backup in progress.
All active transactions are rolled back and clients are forcibly disconnected, then the server is shut down.

“Immediate” mode will abort all server processes immediately, without a clean shutdown.
This choice will lead to a crash-recovery cycle during the next server start.

smart :等用户进程自然退出。最后做检查点。

fast : 主动断开用户进程。最后做检查点。

immediate : 直接停库(不做检查点,最快)。

除了用户进程,还有归档进程、walsender进程。smart,fast停库时,这些进程又会如何处理呢?

如果数据库开启了归档,smart, fast 停库时会怎么处理pgarch进程

发起最后一次archive周期,将所有.ready的wal进行归档,除非中间archive_command遇到错误,否则要等所有的.ready文件都触发并执行完成archive_command。

如果有walsender进程在,smart, fast 停库时会怎么处理walsender进程

如果有walsender进程存在(例如有standby,有pg_basebackup,有pg_receivewal等利用流复制协议的客户端就有walsender进程),那么要等这个walsender将所有未发送完的wal日志都发送给下游。

src/backend/postmaster/postmaster.c

注释如下

/*  
 * Reaper -- signal handler to cleanup after a child process dies.  
 */  
static void  
reaper(SIGNAL_ARGS)  
{  
  
.....................  
        while ((pid = waitpid(-1, &exitstatus, WNOHANG)) > 0)  
        {  
  
.......................  
  
                /*  
                 * Was it the checkpointer?  
                 */  
                if (pid == CheckpointerPID)  
                {  
                        CheckpointerPID = 0;  
                        if (EXIT_STATUS_0(exitstatus) && pmState == PM_SHUTDOWN)  
                        {  
                                /*  
                                 * OK, we saw normal exit of the checkpointer after it's been  
                                 * told to shut down.  We expect that it wrote a shutdown  
                                 * checkpoint.  (If for some reason it didn't, recovery will  
                                 * occur on next postmaster start.)  
                                 *  
                                 * At this point we should have no normal backend children  
                                 * left (else we'd not be in PM_SHUTDOWN state) but we might  
                                 * have dead_end children to wait for.  
                                 *  
                                 * If we have an archiver subprocess, tell it to do a last  
                                 * archive cycle and quit. Likewise, if we have walsender  
                                 * processes, tell them to send any remaining WAL and quit.  
                                 */  
                                Assert(Shutdown > NoShutdown);  
  
                                /* 唤醒归档进程 进行一轮归档 */  
                                /* Waken archiver for the last time */  
                                if (PgArchPID != 0)  
                                        signal_child(PgArchPID, SIGUSR2);  
  
                                /* wal sender,发送完所有未发送的redo */  
                                /*  
                                 * Waken walsenders for the last time. No regular backends  
                                 * should be around anymore.  
                                 */  
                                SignalChildren(SIGUSR2);  
  
                                pmState = PM_SHUTDOWN_2;  
  
                                /*  
                                 * We can also shut down the stats collector now; there's  
                                 * nothing left for it to do.  
                                 */  
                                if (PgStatPID != 0)  
                                        signal_child(PgStatPID, SIGQUIT);  
                        }  

唤醒归档

src/backend/postmaster/pgarch.c

/* SIGUSR2 signal handler for archiver process */  
static void  
pgarch_waken_stop(SIGNAL_ARGS)  
{  
        int                     save_errno = errno;  
  
        /* set flag to do a final cycle and shut down afterwards */  
        /* 停库,触发最后一轮归档周期 */  
        ready_to_stop = true;  
        SetLatch(MyLatch);  
  
        errno = save_errno;  
}  
/*  
 * pgarch_MainLoop  
 *  
 * Main loop for archiver  
 */  
static void  
pgarch_MainLoop(void)  
{  
        pg_time_t       last_copy_time = 0;  
        bool            time_to_stop;  
  
        /*  
         * We run the copy loop immediately upon entry, in case there are  
         * unarchived files left over from a previous database run (or maybe the  
         * archiver died unexpectedly).  After that we wait for a signal or  
         * timeout before doing more.  
         */  
        wakened = true;  
  
        /*  
         * There shouldn't be anything for the archiver to do except to wait for a  
         * signal ... however, the archiver exists to protect our data, so she  
         * wakes up occasionally to allow herself to be proactive.  
         */  
        do  
        {  
                ResetLatch(MyLatch);  
  
                /* When we get SIGUSR2, we do one more archive cycle, then exit */  
                /* 停库,触发最后一轮归档周期 */  
                time_to_stop = ready_to_stop;  
  
                /* Check for config update */  
                if (got_SIGHUP)  
                {  
                        got_SIGHUP = false;  
                        ProcessConfigFile(PGC_SIGHUP);  
                }  
  
                /*  
                 * If we've gotten SIGTERM, we normally just sit and do nothing until  
                 * SIGUSR2 arrives.  However, that means a random SIGTERM would  
                 * disable archiving indefinitely, which doesn't seem like a good  
                 * idea.  If more than 60 seconds pass since SIGTERM, exit anyway, so  
                 * that the postmaster can start a new archiver if needed.  
                 */  
                if (got_SIGTERM)  
                {  
                        time_t          curtime = time(NULL);  
  
                        if (last_sigterm_time == 0)  
                                last_sigterm_time = curtime;  
                        else if ((unsigned int) (curtime - last_sigterm_time) >=  
                                         (unsigned int) 60)  
                                break;  
                }  
  
                /* Do what we're here for */  
                if (wakened || time_to_stop)  
                {  
                        wakened = false;  
                        pgarch_ArchiverCopyLoop();   // 最后一次循环  
                        last_copy_time = time(NULL);  
                }  
  
                /*  
                 * Sleep until a signal is received, or until a poll is forced by  
                 * PGARCH_AUTOWAKE_INTERVAL having passed since last_copy_time, or  
                 * until postmaster dies.  
                 */  
                if (!time_to_stop)              /* Don't wait during last iteration */  
                {  
                        pg_time_t       curtime = (pg_time_t) time(NULL);  
                        int                     timeout;  
  
                        timeout = PGARCH_AUTOWAKE_INTERVAL - (curtime - last_copy_time);  
                        if (timeout > 0)  
                        {  
                                int                     rc;  
  
                                rc = WaitLatch(MyLatch,  
                                                           WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,  
                                                           timeout * 1000L,  
                                                           WAIT_EVENT_ARCHIVER_MAIN);  
                                if (rc & WL_TIMEOUT)  
                                        wakened = true;  
                                if (rc & WL_POSTMASTER_DEATH)  
                                        time_to_stop = true;  
                        }  
                        else  
                                wakened = true;  
                }  
  
                /*  
                 * The archiver quits either when the postmaster dies (not expected)  
                 * or after completing one more archiving cycle after receiving  
                 * SIGUSR2.  
                 */  
        } while (!time_to_stop);  /* 停库,触发最后一轮归档周期 */  
}  

归档所有未归档日志,直到全部的.ready对应调度wal都归档完成,或者报错

/*  
 * pgarch_ArchiverCopyLoop  
 *  
 * Archives all outstanding xlogs then returns  
 */  
static void  
pgarch_ArchiverCopyLoop(void)  
{  
        char            xlog[MAX_XFN_CHARS + 1];  
  
        /*  
         * loop through all xlogs with archive_status of .ready and archive  
         * them...mostly we expect this to be a single file, though it is possible  
         * some backend will add files onto the list of those that need archiving  
         * while we are still copying earlier archives  
         */  
        while (pgarch_readyXlog(xlog))  
        {  
                int                     failures = 0;  
                int                     failures_orphan = 0;  
  
                for (;;)  
                {  
                        struct stat stat_buf;  
                        char            pathname[MAXPGPATH];  
  
                        /*  
                         * Do not initiate any more archive commands after receiving  
                         * SIGTERM, nor after the postmaster has died unexpectedly. The  
                         * first condition is to try to keep from having init SIGKILL the  
                         * command, and the second is to avoid conflicts with another  
                         * archiver spawned by a newer postmaster.  
                         */  
                        if (got_SIGTERM || !PostmasterIsAlive())  
                                return;  
  
                        /*  
                         * Check for config update.  This is so that we'll adopt a new  
                         * setting for archive_command as soon as possible, even if there  
                         * is a backlog of files to be archived.  
                         */  
                        if (got_SIGHUP)  
                        {  
                                got_SIGHUP = false;  
                                ProcessConfigFile(PGC_SIGHUP);  
                        }  
  
                        /* can't do anything if no command ... */  
                        if (!XLogArchiveCommandSet())  
                        {  
                                ereport(WARNING,  
                                                (errmsg("archive_mode enabled, yet archive_command is not set")));  
                                return;  
                        }  
  
                        /*  
                         * Since archive status files are not removed in a durable manner,  
                         * a system crash could leave behind .ready files for WAL segments  
                         * that have already been recycled or removed.  In this case,  
                         * simply remove the orphan status file and move on.  unlink() is  
                         * used here as even on subsequent crashes the same orphan files  
                         * would get removed, so there is no need to worry about  
                         * durability.  
                         */  
                        snprintf(pathname, MAXPGPATH, XLOGDIR "/%s", xlog);  
                        if (stat(pathname, &stat_buf) != 0 && errno == ENOENT)  
                        {  
                                char            xlogready[MAXPGPATH];  
  
                                StatusFilePath(xlogready, xlog, ".ready");  
                                if (unlink(xlogready) == 0)  
                                {  
                                        ereport(WARNING,  
                                                        (errmsg("removed orphan archive status file \"%s\"",  
                                                                        xlogready)));  
  
                                        /* leave loop and move to the next status file */  
                                        break;  
                                }  
  
                                if (++failures_orphan >= NUM_ORPHAN_CLEANUP_RETRIES)  
                                {  
                                        ereport(WARNING,  
                                                        (errmsg("removal of orphan archive status file \"%s\" failed too many times, will try again later",  
                                                                        xlogready)));  
  
                                        /* give up cleanup of orphan status files */  
                                        return;  
                                }  
  
                                /* wait a bit before retrying */  
                                pg_usleep(1000000L);  
                                continue;  
                        }  
  
                        if (pgarch_archiveXlog(xlog))  
                        {  
                                /* successful */  
                                pgarch_archiveDone(xlog);  
  
                                /*  
                                 * Tell the collector about the WAL file that we successfully  
                                 * archived  
                                 */  
                                pgstat_send_archiver(xlog, false);  
  
                                break;                  /* out of inner retry loop */  
                        }  
                        else  
                        {  
                                /*  
                                 * Tell the collector about the WAL file that we failed to  
                                 * archive  
                                 */  
                                pgstat_send_archiver(xlog, true);  
  
                                if (++failures >= NUM_ARCHIVE_RETRIES)  
                                {  
                                        ereport(WARNING,  
                                                        (errmsg("archiving write-ahead log file \"%s\" failed too many times, will try again later",  
                                                                        xlog)));  
                                        return;         /* give up archiving for now */  
                                }  
                                pg_usleep(1000000L);    /* wait a bit before retrying */  
                        }  
                }  
        }  
}  

那么fast,smart停库时,如果有walsender或归档时到底有什么问题?

1、如果walsender有很多很多的wal没有发送完,则停库可能要很久很久(因为要等walsender发完)

2、同样的道理,如果有很多很多文件没有归档,并且归档过程中没有报错,则一个归档周期会非常漫长,也会导致停库可能要很久很久。

immediate模式停库没有影响,但是immediate停库不写检查点,启动数据库时需要进入recovery模式恢复数据库。

Viewing all 689 articles
Browse latest View live