MYSQL Binlog Cache详解

最近在线上遇到一个突发情况:某客户出现了超大事务,该事务运行时占据的磁盘空间超过800GB,但du -sh时未发现任何线索。于是刨根溯源,找到了最终的原因并紧急处理了该问题。本文便是对该问题涉及的binlog cache知识进行整理,希望也能造福更多的朋友。本文会涉及到如下几个概念:

  • binlog cache:它是用于缓存binlog event的内存,大小由binlog_cache_size控制
  • binlog cache 临时文件:是一个临时磁盘文件,存储由于binlog cache不足溢出的binlog event,该文件名字由”ML”打头,由参数max_binlog_cache_size控制该文件大小
  • binlog file:代表binglog 文件,由max_binlog_size指定大小
  • binlog event:代表binlog中的记录,如MAP_EVENT/QUERY EVENT/XID EVENT/WRITE EVENT等

事务binlog event写入流程

binlog cache和binlog临时文件都是在事务运行过程中写入,一旦事务提交,binlog cache和binlog临时文件都会释放掉。而且如果事务中包含多个DML语句,他们共享binlog cache和binlog 临时文件。整个binlog写入流程:

  1. 事务开启
  2. 执行dml语句,在dml语句第一次执行的时候会分配内存空间binlog cache
  3. 执行dml语句期间生成的event不断写入到binlog cache
  4. 如果binlog cache的空间已经满了,则将binlog cache的数据写入到binlog临时文件,同时清空binlog cache。如果binlog临时文件的大小大于了max_binlog_cache_size的设置则抛错ERROR 1197
  5. 事务提交,整个binlog cache和binlog临时文件数据全部写入到binlog file中,同时释放binlog cache和binlog临时文件。但是注意此时binlog cache的内存空间会被保留以供THD上的下一个事务使用,但是binlog临时文件被截断为0,保留文件描述符。其实也就是IO_CACHE(参考后文)保留,并且保留IO_CACHE中的分配的内存空间,和物理文件描述符
  6. 客户端断开连接,这个过程会释放IO_CACHE同时释放其持有的binlog cache内存空间以及持有的binlog 临时文件。 本文主要关注步骤3和4过程中对binlog cache以及binlog 临时文件的写入细节。

数据结构

binlog_cache_mngr

这个类中包含了两个cache:binlog cache和binlog stmt cache。同时包含了将binlog event flush到binlog file的方法。

binlog_trx_cache_data

暂时不表

Binlog_cache_storage

暂时不表

IO_CACHE_binlog_cache_storage

暂时不表

IO_CACHE

将binlog event写入到binlog cache 或者 binlog临时文件都是由 IO_CACHE子系统实现的。IO_CACHE子系统实现了写缓存以及在缓存不足时写入物理文件的功能。它包含读缓存,写缓存以及访问物理文件等信息。其维护的核心成员有:

  • 读缓存 uchar *buffer;
  • 写缓存 uchar *write_buffer;
  • 物理文件 File file;

同时IO_CACHE也支持多种访问模式如READ_CACHE/WRITE_CACHE/SEQ_READ_APPEND,这里就暂时不表。

binlog_cache_size & max_binlog_cache_size

如果开启binlog,那么binlog_cache_size用来在事务运行期间在内存中缓存binlog event。如果经常使用大事务应该加大这个缓存,避免过多的磁盘使用影响性能。

当binlog_cache_size不足以容纳所有的binlog event时,便转而使用临时文件来缓存binlog event。从Binlog_cache_use和Binlog_cache_disk_use可以看出是否使用了binlog cache或binlog 临时文件用于保存binlog event。

binlog cache创建

事务开启时,如果开启binlog功能,便会创建binlog cache。

  1. void *handler_create_thd(
  2. bool enable_binlog) /*!< in: whether to enable binlog */
  3. {
  4. ...
  5. if (enable_binlog) {
  6. thd->binlog_setup_trx_data();
  7. }
  8. return (thd);
  9. }
  10. int THD::binlog_setup_trx_data() {
  11. binlog_cache_mngr *cache_mngr = thd_get_cache_mngr(this);
  12. cache_mngr = (binlog_cache_mngr *)my_malloc(key_memory_binlog_cache_mngr,
  13. sizeof(binlog_cache_mngr),
  14. MYF(MY_ZEROFILL));
  15. cache_mngr = new (cache_mngr)
  16. binlog_cache_mngr(&binlog_stmt_cache_use, &binlog_stmt_cache_disk_use,
  17. &binlog_cache_use, &binlog_cache_disk_use);
  18. if (cache_mngr->init()) {
  19. ...
  20. }
  21. }
  22. class binlog_cache_mngr {
  23. public:
  24. bool init() {
  25. return stmt_cache.open(binlog_stmt_cache_size,
  26. max_binlog_stmt_cache_size) ||
  27. trx_cache.open(binlog_cache_size, max_binlog_cache_size);
  28. }
  29. }
  30. class binlog_cache_data {
  31. public:
  32. bool open(my_off_t cache_size, my_off_t max_cache_size) {
  33. return m_cache.open(cache_size, max_cache_size);
  34. }
  35. }
  36. // 分配binlog cache内存缓存空间以及创建临时文件
  37. // 最终是进入了函数init_io_cache_ext处理,暂且不表
  38. bool Binlog_cache_storage::open(my_off_t cache_size, my_off_t max_cache_size) {
  39. const char *LOG_PREFIX = "ML";
  40. if (m_file.open(mysql_tmpdir, LOG_PREFIX, cache_size, max_cache_size))
  41. return true;
  42. m_pipeline_head = &m_file;
  43. return false;
  44. }

binlog临时文件会被存放到tmpdir的目录下,并以”ML”作为文件名开头。但该文件无法用ls命令看到,因为使用了LINUX创建临时API(mkstemp),以避免其他进程破坏文件内容。也就是说,这个文件是mysqld进程内部专用的,我们在后面会给出访问该文件的方法。

binlog写入cache和临时文件

binlog event写入binlog cache和临时文件是通过函数_my_b_write进行的:

  1. bool IO_CACHE_binlog_cache_storage::write(const unsigned char *buffer,
  2. my_off_t length) {
  3. return my_b_safe_write(&m_io_cache, buffer, length);
  4. }
  5. int my_b_safe_write(IO_CACHE *info, const uchar *Buffer, size_t Count) {
  6. if (info->type == SEQ_READ_APPEND) return my_b_append(info, Buffer, Count);
  7. return my_b_write(info, Buffer, Count);
  8. }
  9. // 如果binlog cache缓存当前写入的位置加上本次写入的总量大于了binlog cache的内存地址的边界
  10. // 则我们需要进行通过*(info)->write_function将binlog cache的内容写到磁盘了
  11. // 这样才能腾出空间给新的binlog event存放。这个回调函数就是_my_b_write。
  12. #define my_b_write(info, Buffer, Count) \
  13. ((info)->write_pos + (Count) <= (info)->write_end \
  14. ? (memcpy((info)->write_pos, (Buffer), (size_t)(Count)), \
  15. ((info)->write_pos += (Count)), 0) \
  16. : (*(info)->write_function)((info), (uchar *)(Buffer), (Count)))
  17. int _my_b_write(IO_CACHE *info, const uchar *Buffer, size_t Count) {
  18. size_t rest_length, length;
  19. my_off_t pos_in_file = info->pos_in_file;
  20. // 如果超过临时文件大小设置,则报错
  21. if (pos_in_file + info->buffer_length > info->end_of_file) {
  22. errno = EFBIG;
  23. set_my_errno(EFBIG);
  24. return info->error = -1;
  25. }
  26. // 首先将binlog内容拷贝至内存cache,将cache填满
  27. rest_length = (size_t)(info->write_end - info->write_pos);
  28. memcpy(info->write_pos, Buffer, (size_t)rest_length);
  29. Buffer += rest_length;
  30. Count -= rest_length;
  31. info->write_pos += rest_length;
  32. if (my_b_flush_io_cache(info, 1)) return 1;
  33. if (Count >= IO_SIZE) { /* Fill first intern buffer */
  34. length = Count & (size_t) ~(IO_SIZE - 1);
  35. ...
  36. if (mysql_file_write(info->file, Buffer, length, info->myflags | MY_NABP))
  37. return info->error = -1;
  38. ...
  39. Count -= length;
  40. Buffer += length;
  41. info->pos_in_file += length;
  42. }
  43. memcpy(info->write_pos, Buffer, (size_t)Count);
  44. info->write_pos += Count;
  45. return 0;
  46. }

运维技巧:查看binlog 临时文件

因为没法直接通过ls来查看binlog临时缓存文件,但可以使用lsof|grep delete来观察到这种文件

  1. [root@test ~]# lsof|grep delete|grep ML
  2. mysqld 21414 root 77u REG 252,3 65536 1856092 /var/tmp/mysqld.1/MLUFzokf