vlambda博客
学习文章列表

如何估算MySQL每秒的事务吞吐量


最近在看Simon Hørup Eskildsen写的Napkin系列文章,这个系列展示了如何对系统性能做估算的方法,文章生动有趣,而又不失深度。Simon在Shopfiy工作了8年,经历了系统调用量从100/s到1million/s的架构演化历程,对应的公司人员从10到1000的过程,是Shopfiy的Principal Engineer[1],现在已离开公司,做架构咨询相关的工作。其主要的评估思路和方法可以通过下面视频来学习:



我们今天翻译的是其评估系列的第十个问题,分析评估了MySQL事务性能,原文参考链接[10],或者直接点击文末的原文链接。



第十个问题:MySQL的每秒事务的峰值和支持的fsync峰值相等吗?


MySQL每秒可以完成多少事务操作?

像MySQL这种能提供ACID功能的数据库,其一次数据库写操作(insert/update/delete)大概涉及流程(也适用于Postgres等关系型数据库):

1 客户端通过TCP链接向服务端发送一条语句

INSERT INTO products (name, price) VALUES ('Sneaker', 100)


2 MySQL将这条记录写入到WAL(write ahead log)日志中,然后调用fsync(2)接口,确保数据已经刷到了磁盘(OS ->File System ->Disk)。这是最复杂的一个步骤,后面会详细介绍。

3 MySQL再将记录写入到Innodb存储引擎在内存的某个page中,这样可以满足后续的查询请求。为什么要同时写入内存和WAL日志中呢?存储引擎是用来应对后续查询数据请求的,而WAL模块是用来确保写入数据安全不丢失的----但是WAL日志显然不适合处理查询请求。

4 MySQL返回应答信息给客户端

5 MySQL再次调用fsync(2)接口,确保Innodb引擎将page内的数据刷新到磁盘

整个流程如下图所示:


在这几个步骤的任意一个节点,如果出现断电等系统异常,数据库状态也不会出现超出认知的异常情况,也能达到ACID的要求。


我们已经对关系型数据库的写入流程构建了一个简单的模型。我们现在评估一下单条写入语句的时延是多少?我们根据Napkin-Math[3]

上面的估算数据,能够得知步骤2的fsync操作是整个链条中最耗时的操作,大概在1ms左右。


因为步骤1中的网络传输耗时为10 μs (microsecond,10^-6 s);

fsync(2)操作之前的write(2)调用耗时也大约为10 μs (是一个in-memory

buffer内存操作),也可以忽略掉,但是单纯的write操作不能保证修改后的

数据写到了disk上---在断点异常情况下有丢失数据的风险,不能满足

ACID的要求。所以需要调用fsync(2)将数据刷新到disk上,这样才能处理断电

等系统异常,但是其代价是系统性能降低了100倍。


通过上面的分析,对于MySQL事务最大吞吐量,我们大概可以得出一个结论:

MySQL理论上事务的吞吐量等于fsync(2)接口每秒的调用次数。

而我们知道fsync(2)耗时大约为1ms,由此可以推知MySQL每秒的写入数量大约为1S/1ms/fsync  = 1000 fsyncs/s  = 1000 transaction /s 。


我们按照Napkin Math的几个评估步骤得到了我们的预估结果

1 对系统建模 :model the system

2 确认系统各节点的时延 :identify the relevant latencies

3 做系统估算:do the napkin math

4 把自己的估算结果与实际情况进行比对,进一步校正我们的预估流程和数据:verify the napkin calculations against reality


我们写了一段Rust程序来验证一下我们预估的数据:开启16个线程, 每个线程往MySQL写入1000条数据:

for i in 0..16 { handles.push(thread::spawn({ let pool = pool.clone(); move || { let mut conn = pool.get_conn().unwrap(); // TODO: we should ideally be popping these off a queue in case of a stall // in a thread, but this is likely good enough. for _ in 0..1000 { conn.exec_drop( r"INSERT INTO products (shop_id, title) VALUES (:shop_id, :title)", params! { "shop_id" => 123, "title" => "aerodynamic chair" }, ) .unwrap(); } } }));
for handle in handles { handle.join().unwrap(); } // 3 seconds, 16,000 insertions}


这16,000次写入操作花费了3s左右的时间,由此我们得知每秒事务写入量大约为5300次:这个比我们之前预估的1000要大5倍。


通常来说,我们估算结果与实际情况的误差在一个数量级之内都是可以接受的。但是,我们的模型给出的是系统的下界(low-bound):从第一性原理进行推导,在理想情况下这个系统到底能跑多快?(我还没有搞懂这个下界的意思,感觉应该是上界upper-bound,不应该是low-bound)


很少会出现比预算值快5倍的情况。当我们收集的实际数据和预估的数据有比显著差异的时候,我们需要进行更深一步的探讨和分析,我给这个差异起了一个名字:第一性差距(first-principle gap)。这一般意味着下面几个点

1 系统有提升的空间:an opportunity to improve the system

2 我们系统建模有缺陷:a flaw in our model of the system

而刚才的case只能用2来解释,因为系统数据比我们预估的要快很多:我们的系统建模有问题。


我们之前的系统模型和流程哪里有问题?为什么fsync的次数不等于事务提交数量呢?....经过一番MySQL配置确认(每次写操作都会调用fsync),数据库记录写入量确认(16,000条记录),实际的测量数据是没有问题的。


我坐下来重新思考我们模型中可能有问题的地方。难道MySQL并不是每个write操作都会调用fsync来刷盘?测试结果是每秒能支持5000个左右的写入操作,那有可能在步骤2有batch优化机制,把多个写操作合并成一个WAL写操作?因为每条事务的处理时间是比较短的,而单次fsync操作又很耗时,那MySQL在这个地方做batch操作是很合适的:调用耗时的fsync操作之前,可以多等几个毫秒,积累多个事务操作,然后批量写入这些操作。


我们可以通过下面的bpftrace[4]脚本来验证我们的想法

tracepoint:syscalls:sys_enter_fsync,tracepoint:syscalls:sys_enter_fdatasync/comm == "mysqld"/{ @fsyncs = count();}


重新跑一下之前的测试用例,用bpftrace脚本收集一下数据,我们发现大约有8,000次的fsync操作

$ sudo bpftrace fsync_count.dAttaching 2 probes...^C
@fsyncs: 8037


这是一个很神奇的数字。如果MySQL有批量优化的流程的话,数字应该远低于这个数字的。这个数字意味着我们每秒会做8037/3=2500个fsync调用,每个调用的时延在0.4ms左右。这个时延比我们的经验值1ms要快两倍。为了确保我们的经验值没有问题,我又绕开MySQL做了单独的fsync测试,结果也是1ms左右。



这个到底是哪里出问题了呢?理论上来说MySQL虽然每次写入操作都会有一次fsync调用,但是有没有可能MySQL的其他模块也会调用fsync接口呢?有没有可能是其他模块触发的fsync调用打乱了我们的数据?


我们需要更精准的,按照文件描述符来分类统计的fsync数据,进而分析这些fsync操作都是由谁来触发的。我们根据下面的bpftrace脚本来做进一步的数据分类收集操作(需要借助于readlink和proc文件系统来做文件操作符和文件名字的映射):

tracepoint:syscalls:sys_enter_fsync,tracepoint:syscalls:sys_enter_fdatasync/comm == str($1)/{ @fsyncs[args->fd] = count(); if (@fd_to_filename[args->fd]) { } else { @fd_to_filename[args->fd] = 1; system("echo -n 'fd %d -> ' &1>&2 | readlink /proc/%d/fd/%d", args->fd, pid, args->fd); }}
END { clear(@fd_to_filename);}


重新跑一下测试数据,得到下面的结果

personal@napkin:~$ sudo bpftrace --unsafe fsync_count_by_fd.d mysqldAttaching 5 probes...fd 5 -> /var/lib/mysql/ib_logfile0 # redo log, or write-ahead-logfd 9 -> /var/lib/mysql/ibdata1 # shared mysql tablespacefd 11 -> /var/lib/mysql/#ib_16384_0.dblwr # innodb doublewrite-bufferfd 13 -> /var/lib/mysql/undo_001 # undo log, to rollback transactionsfd 15 -> /var/lib/mysql/undo_002 # undo log, to rollback transactionsfd 27 -> /var/lib/mysql/mysql.ibd # tablespace fd 34 -> /var/lib/mysql/napkin/products.ibd # innodb storage for our products tablefd 99 -> /var/lib/mysql/binlog.000019 # binlog for replication^C
@fsyncs[9]: 2@fsyncs[12]: 2@fsyncs[27]: 12@fsyncs[34]: 47@fsyncs[13]: 86@fsyncs[15]: 93@fsyncs[11]: 103@fsyncs[99]: 2962@fsyncs[5]: 4887


我们从这个结果我们可以得出一些结论:大部分的fsync都是由于redo log(WAL功能)写入引起的;有少量的调用是由Innodb表空间的写入触发的。


但我们有另一个重大发现:有许多的fsync调用是用来刷新BinLog文件的,这是我们之前模型里没有考虑到的地方。BinLog就像是一个“replication stream”流,用来在主从节点之间复制数据。


我们再深入想一想:Binlog和WAL中的数据也是要保持一致的。不能说数据已经在WAL中commit了,但是在Binlog文件中却找不到它。如果它们之间的数据存在不一致,那从节点的数据会出现数据丢失:比如主节点数据已经写入WAL日志,然后系统断电,并没有把数据写入到BinLog中。


而fsync接口是用来操作单个文件的,那在一个事务中,如何保证数据在WAL和BinLog多个文件的写入能保持一致呢? 


我们想到的一个解决方案是:把BinLog和WAL文件合并成一个文件来写入。至于MySQL为什么没有采取这种方案,可能是由于历史原因。希望知晓的朋友告知一下。


MySQL的解决方案是两阶段提交(two-phase commit):要完成一个事务的提交操作,需要3次fsync调用,具体写入流程细节可以参考文档[6,7]。而在两阶段提交中,WAL log文件需要写入两次,这就解释了我们bpftrace脚本收集到的数据:WAL文件的fsync调用量是BinLog文件fsync调用量的两倍。同时MySQL也会把多个待提交的事务进行分组,同一个分组内的事务会通过单次两阶段提交来完成commit操作,这种优化机制叫”group commit“.


我们从之前的数据中可以看到:虽然有16,000次写操作,但是由于group commit机制的存在,这些事务操作最后被减少到2885次。平均来看,一次commit提交可以合并写入5.5个事务。


但是还有另一个疑问,为什么我们统计的latency数据比经验值要快两倍以上?我们再次通过bpftrace脚本来收集一下时延分布:

tracepoint:syscalls:sys_enter_fsync,tracepoint:syscalls:sys_enter_fdatasync/comm == "mysqld"/{ @start[tid] = nsecs;}
tracepoint:syscalls:sys_exit_fsync,tracepoint:syscalls:sys_exit_fdatasync/comm == "mysqld"/{ @bytes = lhist((nsecs - @start[tid]) / 1000, 0, 1500, 100); delete(@start[tid]);}


我们会得到下面的时延分布图histogram,我们从中看到了一些速度非常快的fsync系统调用

personal@napkin:~$ sudo bpftrace fsync_latency.dAttaching 4 probes...^C
@bytes:[0, 100) 439 |@@@@@@@@@@@@@@@ |[100, 200) 8 | |[200, 300) 2 | |[300, 400) 242 |@@@@@@@@ |[400, 500) 1495 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|[500, 600) 768 |@@@@@@@@@@@@@@@@@@@@@@@@@@ |[600, 700) 376 |@@@@@@@@@@@@@ |[700, 800) 375 |@@@@@@@@@@@@@ |[800, 900) 379 |@@@@@@@@@@@@@ |[900, 1000) 322 |@@@@@@@@@@@ |[1000, 1100) 256 |@@@@@@@@ |[1100, 1200) 406 |@@@@@@@@@@@@@@ |[1200, 1300) 690 |@@@@@@@@@@@@@@@@@@@@@@@@ |[1300, 1400) 803 |@@@@@@@@@@@@@@@@@@@@@@@@@@@ |[1400, 1500) 582 |@@@@@@@@@@@@@@@@@@@@ |[1500, ...) 1402 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |


要搞明白这个问题,我们需要深入到到文件系统的范畴。这个需要了解的背景知识就更多了,在这里不做深入的探讨,先给一个大概的思路:简单来说,ext4文件系统也有类似”group commit“机制,会把多次写入操作优化一次journal文件的batch写入(和MySQL的WAL机制类似)。用ext4配置[8]中,有个配置项max_batch_time,可以配置批量写入合并的等待时间。同样在disk层也可以采用类似优化机制。


我们现在能够得到基线数据(bottom-line):在真实的业务流程中,fsync经过优化后会比1ms更快(1ms的数据是通过简单的单机单个文件的write和fsync的benchmark出来的)。这个收益主要是通过ext4文件系统的batch (group commit)操作达到的,而这个性能数据在我们串行的benchmark测试中是收集不到的,这个需要有parallel的fsync测试流程来测试。


经过上面的分析流程,回到我们最初的疑问:预估的MySQL每秒最大的事务数量和实际情况为什么有5倍的差异?现在我们能够回答,这5倍的差异主要是通过下面机制提升的:

1 MySQL层的group commit机制,把多个事务合并成一个commit操作,从而减少fsync的调用量

2 同时在文件系统(filesystem)和磁盘(disk)层,也会有类似的batch机制,来提高写入的性能。


本质上来说,为了提高写入的效率,在写入路径的各个层级(MySQL->File System -> Disk)都做了batch优化。


在这篇文章里,我们虽然没有对每个疑问点都进行深入的分析,但是从部分疑问的分析过程中我们还是学到了许多。而同时我们也有了这样的认识:通过group commit[9]的配置,我们可以在吞吐量(throughput)和时延(latency)之间做trade-off,可以牺牲一定的延迟来换取更多的吞吐量。这个不仅仅在MySQL层可以做,在文件系统层也可以做。



衍生问题


能否把这种性能预估的方式应用到其他模块

1 基础物理资源的性能测试评估

CPU / Memory / Disk  /Network这些通用物理资源每秒可以支撑的计算量,数据处理量,数据传输量,数据时延等数据。这个可以参考[2,10]总结。

中间件

比如Redis / Kafka/ ES等中间件系统的吞吐量和时延是怎样的。

3 业务系统

如何估算业务系统单机的吞吐量和时延。有了前面的性能数据之后,我们是否可以评估现在负责的业务系统单机的性能如何?

4 系统容量评估

有了单机的性能数据(预估和实际压测相结合),可以根据现有业务量和增长速率,做一些系统容量评估了


附录

https://sirupsen.com/about

https://sirupsen.com/hire

2 Napkin Math

https://sirupsen.com/napkin

3 https://github.com/sirupsen/napkin-math

4 https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_flush_log_at_trx_commit

5 https://bpftrace.org/

https://github.com/iovisor/bpftrace

6 Fun MySQL fact of the day: everything is two-phase

https://www.burnison.ca/notes/fun-mysql-fact-of-the-day-everything-is-two-phase

7 Fixing MySQL group commit (part 1)

https://knielsen-hq.org/w/fixing-mysql-group-commit-part-1/

https://kristiannielsen.livejournal.com/12254.html

8 Ext4 Filesystem

https://www.kernel.org/doc/Documentation/filesystems/ext4.txt

9 Changing Group Commit Frequency

https://mariadb.com/kb/en/group-commit-for-the-binary-log/#changing-group-commit-frequency

10 MySQL transactions per second vs fsyncs per second

https://sirupsen.com/napkin/problem-10-mysql-transactions-per-second

11 LATENCY NUMBERS EVERY PROGRAMMER SHOULD KNOW

http://stereobooster.github.io/latency-numbers-every-programmer-should-know