水平有限整理自己的学习笔记,如果有误请谅解。
最近老是看到有朋友报错如下:
ERROR 1197 (HY000): Multi-statement transaction required more than 'max_binlog_cache_size' bytes of storage; increase this mysqld variable and try again
虽然错误提示非常明确,但是我还是对binlog cache的使用方式进行了学习整理下来,这部分也是我一直疑惑的地方,也希望通过本文接触读者的疑惑。
本文约定:
本文源码版本:
这里首先给出写入过程,让大家有一个大概的理解。
注意这个流程都是对于一个事物来讲的。一旦事物提交,binlog cache和binlog 临时文件都会释放掉,因为binlog已经固化到了binlog file。同时如果事物中包含多个DML语句,那么他们共享的一套的binlog cache和binlog 临时文件。
本文研究就是步骤3和步骤4的写入过程,和binlog cache以及binlog 临时文件的实现方式。同时本文不描述binlog_stmt_cache_size和max_binlog_stmt_cache_size的作用,因为他们和binlog_cache_size、max_binlog_cache_size参数的函数一致,只是存储是非事物语句的binlog event,但是在讨论数据结构的时候还是给出部分解释。
binlog_cache_mngr类:
这个类中包含了两个cache实际上就是binlog cache和binlog stmt cache,如下:
#0 binlog_cache_mngr::flush (this=0x7fff58ca17c0, thd=0x7fff58012930, bytes_written=0x7ffff0358918, wrote_xid=0x7ffff0358917)
at /root/mysql5.7.14/percona-server-5.7.14-7/sql/binlog.cc:739
#1 0x0000000001857152 in MYSQL_BIN_LOG::flush_thread_caches (this=0x2e02c80, thd=0x7fff58012930) at /root/mysql5.7.14/percona-server-5.7.14-7/sql/binlog.cc:8484
#2 0x0000000001857383 in MYSQL_BIN_LOG::process_flush_stage_queue (this=0x2e02c80, total_bytes_var=0x7ffff0358a88, rotate_var=0x7ffff0358a87,
out_queue_var=0x7ffff0358a78) at /root/mysql5.7.14/percona-server-5.7.14-7/sql/binlog.cc:8546
#3 0x000000000185899f in MYSQL_BIN_LOG::ordered_commit (this=0x2e02c80, thd=0x7fff58012930, all=false, skip_commit=false)
at /root/mysql5.7.14/percona-server-5.7.14-7/sql/binlog.cc:9189
#4 0x000000000185700c in MYSQL_BIN_LOG::commit (this=0x2e02c80, thd=0x7fff58012930, all=false) at /root/mysql5.7.14/percona-server-5.7.14-7/sql/binlog.cc:8440
可以说他是一个访问binlog cache和binlog stmt cache的统一的对外接口。
他们都继承来自统一的父类binlog_cache_data。因为binlog_trx_cache_data要实现更多的功能,因为要支持事物,支持事物的回滚等功能,所以必须增加额外的功能呢但是。binlog_stmt_cache_data则不需要。
实现了主要成员包含:
实际上binlog event的写入binlog 或者 binlog临时文件都是由 IO_CACHE子系统实现的,本子系统实现了类似写缓存如果缓存不够写入物理文件的功能,从而降低了物理写入的次数,提升了性能。它封装了包含读缓存,写缓存以及物理文件等信息。如下:
其使用方式也分多种模式如:
当然IO_CACHE还用到其他地方,比如我们熟悉的filesort,进行内存和磁盘排序也是通过这个子系统作为存储方式完成的。栈帧参考结束附录,对于这个子系统更加详细的参考信息可以自行百度我发现有朋友写得很清楚,本文主要说明是binlog cache的使用方式而不是这个子系统。但是在涉及到源码部分的时候以binlog cache和binlog临时文件的写入为列子。
最后我们画一张这几个类的关系图,同时给出binlog cache(binlog stmt cache)和binlog临时文件(binlog stmt临时文件)的表示,如下:
这张图解释了他们的关系。
本节使用的binlog_cache_size大小为32678及默认值。
binlog_cache_size参数在官方文档的描述大概为如果开启了binlog功能,则在事物期间用于保存binlog event的缓存。如果经常使用大事物应该加大这个缓存,避免过多的磁盘使用,从Binlog_cache_use和Binlog_cache_disk_use可以看出是否使用了binlog cache或binlog 临时文件用于保存binlog event。
当然如我开头所说这部分是完全内存的。
实际上当我们的事物需要记录事物的binlog event的时候我们就会使用到binlog cache,其初始化栈帧如下:
#0 init_io_cache_ext (info=0x7fff58ca17c8, file=-1, cachesize=32768, type=WRITE_CACHE, seek_offset=0, use_async_io=0 '\000', cache_myflags=20, file_key=10)
at /root/mysql5.7.14/percona-server-5.7.14-7/mysys/mf_iocache.c:154
#1 0x00000000018c8b48 in init_io_cache (info=0x7fff58ca17c8, file=-1, cachesize=32768, type=WRITE_CACHE, seek_offset=0, use_async_io=0 '\000', cache_myflags=20)
at /root/mysql5.7.14/percona-server-5.7.14-7/mysys/mf_iocache.c:299
#2 0x00000000018c6b96 in open_cached_file (cache=0x7fff58ca17c8, dir=0x2fa1d70 "/root/mysql5.7.14/percona-server-5.7.14-7/mysql-test/var/tmp/mysqld.1",
prefix=0x2277d4e "ML", cache_size=32768, cache_myflags=16) at /root/mysql5.7.14/percona-server-5.7.14-7/mysys/mf_cache.c:60
#3 0x00000000018599b2 in THD::binlog_setup_trx_data (this=0x7fff58012930) at /root/mysql5.7.14/percona-server-5.7.14-7/sql/binlog.cc:9641
#4 0x0000000001859cb3 in binlog_start_trans_and_stmt (thd=0x7fff58012930, start_event=0x7ffff0359350) at /root/mysql5.7.14/percona-server-5.7.14-7/sql/binlog.cc:9742
#5 0x000000000185a0a6 in THD::binlog_write_table_map (this=0x7fff58012930, table=0x7fff58d98260, is_transactional=false, binlog_rows_query=false)
at /root/mysql5.7.14/percona-server-5.7.14-7/sql/binlog.cc:9835
#6 0x0000000000f7299f in write_locked_table_maps (thd=0x7fff58012930) at /root/mysql5.7.14/percona-server-5.7.14-7/sql/handler.cc:8019
#7 0x0000000000f72bf6 in binlog_log_row (table=0x7fff58d98260, before_record=0x7fff58d99120 "\375\001", after_record=0x0,
log_func=0xf77f7d )
at /root/mysql5.7.14/percona-server-5.7.14-7/sql/handler.cc:8089
我们可以在THD::binlog_setup_trx_data找到binlog_cache_size的作如下:
open_cached_file(&cache_mngr->trx_cache.cache_log, mysql_tmpdir,
LOG_PREFIX, binlog_cache_size, MYF(MY_WME)))
大概解释一下部分参数的含义。
最终参数binlog_cache_size的值会通过init_io_cache函数传入到init_io_cache_ext中。我们关注一下以下几个传入到init_io_cache_ext的参数。
下面是init_io_cache_ext函数关于binlog cache分配的重点代码部分:
info->file= file;(-1)
info->pos_in_file= seek_offset;(0)
info->pre_close = info->pre_read = info->post_read = 0;
info->arg = 0;
info->alloced_buffer = 0;
info->buffer=0;
info->seek_not_done= 0;
buffer_block= cachesize; //他们和binlog_cache_size参数指定相等(有对其操作)
if ((info->buffer= (uchar*) my_malloc(key_memory_IO_CACHE,
buffer_block, flags)) != 0)//为binlog cache分配实际的内存其大小就是binlog_cache_size指定的大小(有对其操作)
{
info->write_buffer=info->buffer;//write_buffer和buffer指向了同一片内存区域,及我们的binlog cache缓存区域
info->alloced_buffer=1;
}
info->read_length=info->buffer_length=cachesize; //此处将缓存的长度记录为binlog_cache_size指定的大小(有对其操作)
info->request_pos= info->read_pos= info->write_pos = info->buffer;//初始化各种位置为info->buffer指针指向的位置
if (type == WRITE_CACHE)
info->write_end= info->buffer+info->buffer_length- (seek_offset & (IO_SIZE-1));//将缓存的边界设置为(info->buffer指针指向位置+binlog_cache_size参数指定的大小)
此后IO_CACHE对象已经分配了一块binlog_cache_size参数指定相等大小的内存,则使我们说的binlog cache已经初始化分配完成。
及当我们事物需要存储的binlog event已经不能在binlog cache中存下的时候,我们需要开启我们binlong临时文件来存储。其文件存放到tmpdir的定义的目录下,文件名为”ML”开头。但是这个文件不能用ls看到,因为使用了LINUX临时文件建立的方法,以避免其他进程使用这个文件而破坏这个文件的内容。也就是说,这个文件是mysqld进程内部专用的,在LINUX中MySQL使用mkstemp系统调用完成这个功能。对于这个文件可以使用lsof|grep delete看到最后我会给出一个例子。
分配栈帧如下:
#0 create_temp_file (to=0x7ffff0359040 "/root/mysql5.7.14/percona-server-5.7.14-7/mysql-test/var/tmp/mysqld.1/MLIdFGCH",
dir=0x7fff580167c0 "/root/mysql5.7.14/percona-server-5.7.14-7/mysql-test/var/tmp/mysqld.1", prefix=0x7fff58016830 "ML", mode=514, MyFlags=16)
at /root/mysql5.7.14/percona-server-5.7.14-7/mysys/mf_tempfile.c:141
#1 0x00000000018c693d in inline_mysql_file_create_temp (key=10, src_file=0x2296d78 "/root/mysql5.7.14/percona-server-5.7.14-7/mysys/mf_cache.c", src_line=80,
to=0x7ffff0359040 "/root/mysql5.7.14/percona-server-5.7.14-7/mysql-test/var/tmp/mysqld.1/MLIdFGCH",
dir=0x7fff580167c0 "/root/mysql5.7.14/percona-server-5.7.14-7/mysql-test/var/tmp/mysqld.1", pfx=0x7fff58016830 "ML", mode=514, myFlags=16)
at /root/mysql5.7.14/percona-server-5.7.14-7/include/mysql/psi/mysql_file.h:1048
#2 0x00000000018c6c87 in real_open_cached_file (cache=0x7fff58ca17c8) at /root/mysql5.7.14/percona-server-5.7.14-7/mysys/mf_cache.c:77
#3 0x00000000018cb4a9 in my_b_flush_io_cache (info=0x7fff58ca17c8, need_append_buffer_lock=0) at /root/mysql5.7.14/percona-server-5.7.14-7/mysys/mf_iocache.c:1528
#4 0x00000000018cad03 in _my_b_write (info=0x7fff58ca17c8, Buffer=0x7fff58de751a "", Count=186) at /root/mysql5.7.14/percona-server-5.7.14-7/mysys/mf_iocache.c:1330
#5 0x00000000018cb156 in my_b_safe_write (info=0x7fff58ca17c8, Buffer=0x7fff58de55e0 "\376\001", Count=8180)
at /root/mysql5.7.14/percona-server-5.7.14-7/mysys/mf_iocache.c:1437
#6 0x000000000180e7b2 in Log_event::wrapper_my_b_safe_write (this=0x7fff58972bb0, file=0x7fff58ca17c8, buf=0x7fff58de55e0 "\376\001", size=8180)
at /root/mysql5.7.14/percona-server-5.7.14-7/sql/log_event.cc:977
#7 0x000000000182b392 in Rows_log_event::write_data_body (this=0x7fff58972bb0, file=0x7fff58ca17c8)
at /root/mysql5.7.14/percona-server-5.7.14-7/sql/log_event.cc:11356
#8 0x0000000001835e63 in Log_event::write (this=0x7fff58972bb0, file=0x7fff58ca17c8) at /root/mysql5.7.14/percona-server-5.7.14-7/sql/log_event.h:825
#9 0x0000000001845899 in binlog_cache_data::write_event (this=0x7fff58ca17c0, thd=0x7fff58012930, ev=0x7fff58972bb0)
at /root/mysql5.7.14/percona-server-5.7.14-7/sql/binlog.cc:1120
#10 0x0000000001853625 in MYSQL_BIN_LOG::flush_and_set_pending_rows_event (this=0x2e02c80, thd=0x7fff58012930, event=0x7fff58954fd0, is_transactional=false)
at /root/mysql5.7.14/percona-server-5.7.14-7/sql/binlog.cc:7075
当我们不断的写入binlog event的情况下,我们最终会将binlog cache写满(对于小事物肯定不会写满)。我们来梳理一下binlog 临时文件是怎么打开的,我们从my_b_safe_write函数开始:
注意这里实际对于IO_CACHE来讲是通用的方法我只是以binlog 临时文件建立的流程为例子,因为不同的模式走的逻辑不一样
这个函数在每次binlog event写入到binlog cache的时候都会调用。
在WRITE_CACHE的模式下会调用宏my_b_write,这是一个关键点宏的定义如下:
#define my_b_write(info,Buffer,Count) \
((info)->write_pos + (Count) <=(info)->write_end ?\
(memcpy((info)->write_pos, (Buffer), (size_t)(Count)),\
((info)->write_pos+=(Count)),0) : \
(*(info)->write_function)((info),(uchar *)(Buffer),(Count)))
很简单如果binlog cache缓存当前写入的位置加上本次写入的总量大于了binlog cache的内存地址的边界则我们需要进行通过*(info)->write_function这个回调函数将binlog cache的内容写到磁盘了,这样才能腾出空间给新的binlog event存放。这里这个回调函数应该就是_my_b_write。
这个函数只会在binlog cache不够用的时候需要进行临时物理文件写入的时候调用。
大概逻辑如下:
rest_length= (size_t) (info->write_end - info->write_pos); //统计出此时binlog cache剩余的空间
memcpy(info->write_pos,Buffer,(size_t) rest_length); //写满这些剩余的空间
Buffer+=rest_length;//既然已经写了一部分那么待写入的数据应该减去写入的部分
Count-=rest_length;//写入的总量也应该的减去已经写入的部分
info->write_pos+=rest_length;//binlog cache的写入位置应该增加
if (my_b_flush_io_cache(info,1))//创建临时文件(如果没有创建的话)并且写入数据到临时文件
memcpy(info->write_pos,Buffer,(size_t) Count);//刷新完成后。binlog cache已经腾空可以继续使用了
info->write_pos+=Count;//简单的增加binlog cache指针的位置
这个函数主要负责了当binlog cache 写满后将binlog cache的数据全部写入到binlog 临时文件和清空binlog cache,当然最后会增加Binlog_cache_disk_use。
大概逻辑如下:
if (info->file == -1)//如果是第一次建立临时文件,这里的文件描述为-1,前面已经分析过了
{
if (real_open_cached_file(info))//打开临时文件返回文件描述符 !!!!!
length=(size_t) (info->write_pos - info->write_buffer)//统计本次写入临时文件数据的大小
pos_in_file=info->pos_in_file;
info->pos_in_file+=length;//将当前文件写入位置加上本次写入数据的大小及为下次文件写入的位置
if (mysql_file_write(info->file,info->write_buffer,length,info->myflags | MY_NABP))//将数据写入到临时文件中。
info->append_read_pos=info->write_pos=info->write_buffer; //此处将info->write_pos指针指向到binlog cache缓冲开始的位置及binlog cache缓冲全部释放
++info->disk_writes;//Binlog_cache_disk_use增加
注意对于filesort的临时文件也是通过这个函数建立,前缀为MY。
到这里我们的binlog cache内存空间已经分配,并且由于事物写入的binlog event大于了binlog cache设置的大小,binlog 临时文件已经分配并且进行了一次binlog cache数据写入到binlog 临时文件的操作。这就是整个流程。
报错如下:
(root@localhost)[06:19:12] [test ;]> insert into t1 select * from t1;
ERROR 1197 (HY000): Multi-statement transaction required more than 'max_binlog_cache_size' bytes of storage; increase this mysqld variable and try again
当然报错也很清晰告诉你原因,但是在整个流程中到底是哪里抛错出来的呢?实际上就是函数_my_b_write中进行的判断。先来看看这个错误的错误码如下:
{ "ER_TRANS_CACHE_FULL", 1197, "Multi-statement transaction required more than \'max_binlog_cache_size\' bytes of storage; increase this mysqld variable and try again" },
在函数_my_b_write中可以看到如下代码:
if (pos_in_file+info->buffer_length > info->end_of_file) //判断binlog临时文件的位置加上本次需要写盘的数据大于info->end_of_file的大小则抛错
{
errno=EFBIG;
set_my_errno(EFBIG);
return info->error = -1;
}
而抛错的代码在MYSQL_BIN_LOG::check_write_error函数中如下:
if (is_transactional)
{
my_message(ER_TRANS_CACHE_FULL, ER(ER_TRANS_CACHE_FULL), MYF(MY_WME));
}
其中info->end_of_file的大小正是来自于我们的参数max_binlog_cache_size,不再给出代码。
前文已经说过可以使用lsof|grep delete来观察到这种文件,据我观察这个文件会以64k的倍数上涨如下:
[root@test ~]# lsof|grep delete|grep ML
mysqld 21414 root 77u REG 252,3 65536 1856092 /root/mysql5.7.14/percona-server-5.7.14-7/mysql-test/var/tmp/mysqld.1/MLUFzokf (deleted)
[root@test ~]# lsof|grep delete|grep ML
mysqld 21414 root 77u REG 252,3 131072 1856092 /root/mysql5.7.14/percona-server-5.7.14-7/mysql-test/var/tmp/mysqld.1/MLUFzokf (deleted)
[root@test ~]# lsof|grep delete|grep ML
mysqld 21414 root 77u REG 252,3 163840 1856092 /root/mysql5.7.14/percona-server-5.7.14-7/mysql-test/var/tmp/mysqld.1/MLUFzokf (deleted)
[root@test ~]# lsof|grep delete|grep ML
mysqld 21414 root 77u REG 252,3 229376 1856092 /root/mysql5.7.14/percona-server-5.7.14-7/mysql-test/var/tmp/mysqld.1/MLUFzokf (deleted)
写了这么多,这里需要总结一下参数的具体含义,以及注意点。
ERROR 1197 (HY000): Multi-statement transaction required more than 'max_binlog_cache_size' bytes of storage; increase this mysqld variable and try again
断点 MYSQL_BIN_LOG::write_event函数,代码片段如下:
bool is_trans_cache= event_info->is_using_trans_cache();//DDL这里返回的false
binlog_cache_mngr *cache_mngr= thd_get_cache_mngr(thd);//得到cache_mngr指针地址,有了它不管是stmt缓存还是trx缓存都能找到如前文所述
binlog_cache_data *cache_data= cache_mngr->get_binlog_cache_data(is_trans_cache);//这里如果是非事物级别使用stmt缓存区
最后留下一些有用的接口信息:
File file=cache->file;
cache->file= -1; /* Don't flush data */
(void) end_io_cache(cache);//释放整个IO_CACHE
if (file >= 0)
{
(void) mysql_file_close(file, MYF(0));//关闭临时文件
}