如何估算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事务最大吞吐量,我们大概可以得出一个结论:
而我们知道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"/
{
= count();
}
重新跑一下之前的测试用例,用bpftrace脚本收集一下数据,我们发现大约有8,000次的fsync操作
$ sudo bpftrace fsync_count.d
Attaching 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 mysqld
Attaching 5 probes...
fd 5 -> /var/lib/mysql/ib_logfile0 # redo log, or write-ahead-log
fd 9 -> /var/lib/mysql/ibdata1 # shared mysql tablespace
fd 11 -> /var/lib/mysql/#ib_16384_0.dblwr # innodb doublewrite-buffer
fd 13 -> /var/lib/mysql/undo_001 # undo log, to rollback transactions
fd 15 -> /var/lib/mysql/undo_002 # undo log, to rollback transactions
fd 27 -> /var/lib/mysql/mysql.ibd # tablespace
fd 34 -> /var/lib/mysql/napkin/products.ibd # innodb storage for our products table
fd 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"/
{
[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_fsync,tracepoint:syscalls:sys_exit_fdatasync
/comm == "mysqld"/
{
1000, 0, 1500, 100); = lhist((nsecs - [tid]) /
delete( [tid]);
}
我们会得到下面的时延分布图histogram,我们从中看到了一些速度非常快的fsync系统调用
personal@napkin:~$ sudo bpftrace fsync_latency.d
Attaching 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]的总结。
2 中间件
比如Redis / Kafka/ ES等中间件系统的吞吐量和时延是怎样的。
3 业务系统
如何估算业务系统单机的吞吐量和时延。有了前面的性能数据之后,我们是否可以评估现在负责的业务系统单机的性能如何?
4 系统容量评估
有了单机的性能数据(预估和实际压测相结合),可以根据现有业务量和增长速率,做一些系统容量评估了。
附录
1 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