升级到 MySQL 8.4,MySQL 启动报错:io_setup() failed with EAGAIN

问题

最近碰到一个 case,一台主机上,部署了多个实例。之前使用的是 MySQL 8.0,启动时没有任何问题。但升级到 MySQL 8.4 后,部分实例在启动时出现了以下错误。

[Warning] [MY-012582] [InnoDB] io_setup() failed with EAGAIN. Will make 5 attempts before giving up.
[Warning] [MY-012583] [InnoDB] io_setup() attempt 1.
[Warning] [MY-012583] [InnoDB] io_setup() attempt 2.
[Warning] [MY-012583] [InnoDB] io_setup() attempt 3.
[Warning] [MY-012583] [InnoDB] io_setup() attempt 4.
[Warning] [MY-012583] [InnoDB] io_setup() attempt 5.
[ERROR] [MY-012584] [InnoDB] io_setup() failed with EAGAIN after 5 attempts.
[ERROR] [MY-012954] [InnoDB] Cannot initialize AIO sub-system
[ERROR] [MY-012930] [InnoDB] Plugin initialization aborted with error Generic error.
[ERROR] [MY-010334] [Server] Failed to initialize DD Storage Engine
[ERROR] [MY-010020] [Server] Data Dictionary initialization failed.
[ERROR] [MY-010119] [Server] Aborting
[System] [MY-010910] [Server] /usr/local/mysql/bin/mysqld: Shutdown complete (mysqld 8.4.0)  MySQL Community Server - GPL.
[System] [MY-015016] [Server] MySQL Server - end.

下面我们来分析下这个报错的具体原因及解决方法。

定位过程

首先搜索下这个报错是在哪个文件产生的。

# grep "io_setup() failed" -r /usr/src/mysql-8.4.0
/usr/src/mysql-8.4.0/storage/innobase/os/os0file.cc:          ib::warn(ER_IB_MSG_757) << "io_setup() failed with EAGAIN."
/usr/src/mysql-8.4.0/storage/innobase/os/os0file.cc:            << "io_setup() failed with EAGAIN after "

接着分析该文件中产生报错的具体函数。

// storage/innobase/os/os0file.cc
bool AIO::linux_create_io_ctx(ulint max_events, io_context_t *io_ctx) {
  ssize_t n_retries = 0;
  for (;;) {
    memset(io_ctx, 0x0, sizeof(*io_ctx));
    int ret = io_setup(max_events, io_ctx);

    if (ret == 0) {
      /* Success. Return now. */
      return (true);
    }
    switch (ret) {
      case -EAGAIN:
        if (n_retries == 0) {
          /* First time around. */
          ib::warn(ER_IB_MSG_757) << "io_setup() failed with EAGAIN."
                                     " Will make "
                                  << OS_AIO_IO_SETUP_RETRY_ATTEMPTS
                                  << " attempts before giving up.";
        }
        if (n_retries < OS_AIO_IO_SETUP_RETRY_ATTEMPTS) {
          ++n_retries;
          ib::warn(ER_IB_MSG_758) << "io_setup() attempt " << n_retries << ".";
          std::this_thread::sleep_for(OS_AIO_IO_SETUP_RETRY_SLEEP);
          continue;
        }

        /* Have tried enough. Better call it a day. */
        ib::error(ER_IB_MSG_759)
            << "io_setup() failed with EAGAIN after "
            << OS_AIO_IO_SETUP_RETRY_ATTEMPTS << " attempts.";
        break;
        ...
    }
    ib::info(ER_IB_MSG_762) << "You can disable Linux Native AIO by"
                               " setting innodb_use_native_aio = 0 in my.cnf";

    break;
  }
  return (false);
}

可以看到,错误信息主要是在执行io_setup,产生 EAGAIN 错误时打印的。

函数中的io_setup是一个 Linux 系统调用,用于初始化一个异步 I/O (AIO) 上下文(context)。异步 I/O(AIO)允许程序在发出 I/O 操作请求后继续执行其他工作,而不是等待操作完成。io_setup是 Linux 内核提供的异步 I/O 接口,通常用于高性能应用程序和数据库系统,以实现非阻塞 I/O 操作。max_events 指定了这个异步 I/O 上下文可以处理的最大并发 I/O 请求数。io_setup执行成功时会返回 0,失败时则返回 -1,并通过 errno 表示具体错误。

当返回的错误是 EAGAIN 时,则意味着指定的 max_events 超过了系统允许的最大异步 I/O (AIO) 事件数。

系统允许创建的最大异步 I/O 事件数是在/proc/sys/fs/aio-max-nr中定义的,默认值跟系统有关,通常是 65536。

所以,解决方法找到了,直接调整/proc/sys/fs/aio-max-nr的值即可。

# echo 1048576 > /proc/sys/fs/aio-max-nr

注意这种只是临时修改,系统重启就会失效。如果要永久修改,需调整 /etc/sysctl.conf。

# vim /etc/sysctl.conf
fs.aio-max-nr=1048576
# sysctl -p

问题解决了,接下来我们分析下同一台主机,为什么之前的 MySQL 8.0 没问题,升级到 MySQL 8.4 就报错了呢?

这个时候,就需要分析函数中 max_events 的生成逻辑了。

堆栈信息

下面是AIO::linux_create_io_ctx函数被调用的堆栈信息。

#0  AIO::linux_create_io_ctx (max_events=256, io_ctx=0x7fffe02d1500)
    at /usr/src/mysql-8.4.0/storage/innobase/os/os0file.cc:2559
#1  0x0000000004db1649 in AIO::init_linux_native_aio (this=0x7fffe02d1d70)
    at /usr/src/mysql-8.4.0/storage/innobase/os/os0file.cc:6139
#2  0x0000000004db16ed in AIO::init (this=0x7fffe02d1d70)
    at /usr/src/mysql-8.4.0/storage/innobase/os/os0file.cc:6167
#3  0x0000000004db1826 in AIO::create (id=LATCH_ID_OS_AIO_IBUF_MUTEX, n=256, n_segments=1)
    at /usr/src/mysql-8.4.0/storage/innobase/os/os0file.cc:6200
#4  0x0000000004db1a2b in AIO::start (n_per_seg=256, n_readers=64, n_writers=4)
    at /usr/src/mysql-8.4.0/storage/innobase/os/os0file.cc:6254
#5  0x0000000004db261a in os_aio_init (n_readers=64, n_writers=4)
    at /usr/src/mysql-8.4.0/storage/innobase/os/os0file.cc:6514
#6  0x0000000004ee6b4d in srv_start (create_new_db=false)
    at /usr/src/mysql-8.4.0/storage/innobase/srv/srv0start.cc:1743
#7  0x0000000004bdfc92 in innobase_init_files (dict_init_mode=DICT_INIT_CHECK_FILES, tablespaces=0x7fffe77bf720)
    at /usr/src/mysql-8.4.0/storage/innobase/handler/ha_innodb.cc:5744
#8  0x0000000004bf0e22 in innobase_ddse_dict_init (dict_init_mode=DICT_INIT_CHECK_FILES, tables=0x7fffe77bf740, 
    tablespaces=0x7fffe77bf720) at /usr/src/mysql-8.4.0/storage/innobase/handler/ha_innodb.cc:13133
#9  0x00000000049153b8 in dd::bootstrap::DDSE_dict_init (thd=0xb7ce6b0, dict_init_mode=DICT_INIT_CHECK_FILES, 
    version=80300) at /usr/src/mysql-8.4.0/sql/dd/impl/bootstrap/bootstrapper.cc:746
#10 0x0000000004916195 in dd::bootstrap::restart_dictionary (thd=0xb7ce6b0)
    at /usr/src/mysql-8.4.0/sql/dd/impl/bootstrap/bootstrapper.cc:907
#11 0x0000000003814e54 in bootstrap::handle_bootstrap (arg=0x7fffffffcf20)
    at /usr/src/mysql-8.4.0/sql/bootstrap.cc:340
#12 0x0000000005792a92 in pfs_spawn_thread (arg=0xb7ece50) at /usr/src/mysql-8.4.0/storage/perfschema/pfs.cc:3051
#13 0x00007ffff7bc6ea5 in start_thread () from /lib64/libpthread.so.0
#14 0x00007ffff5ff0b0d in clone () from /lib64/libc.so.6

堆栈中的重点是 #6 的srv_start函数,这个函数会调用os_aio_init来初始化异步 I/O 系统。

// storage/innobase/srv/srv0start.cc
dberr_t srv_start(bool create_new_db) {
  ...
  if (!os_aio_init(srv_n_read_io_threads, srv_n_write_io_threads)) {
    ib::error(ER_IB_MSG_1129);

    return (srv_init_abort(DB_ERROR));
  }
...

调用 os_aio_init 时,会传递两个参数:srv_n_read_io_threads 和 srv_n_write_io_threads。这两个参数实际上对应的就是 MySQL 中的 innodb_read_io_threads 和 innodb_write_io_threads,这两个参数分别用来表示 InnoDB 中用于读操作、写操作的 I/O 线程数。

如果初始化失败,会打印ER_IB_MSG_1129错误。

ER_IB_MSG_1129是一个预定义的错误代码,对应的错误信息是在share/messages_to_error_log.txt中定义的。

ER_IB_MSG_1129
  eng "Cannot initialize AIO sub-system"

所以,错误日志中看到的[ERROR] [MY-012954] [InnoDB] Cannot initialize AIO sub-system其实就是在这里打印的。

有的童鞋可能猜到了,异步 I/O 系统初始化失败与 innodb_read_io_threads 和 innodb_write_io_threads 的设置有关,事实也确实如此。

下面,我们分析下 MySQL 启动过程中需要初始化多少个异步 I/O 请求。

MySQL 启动过程中需要初始化多少个异步 I/O 请求?

异步 I/O 的初始化主要是在AIO::linux_create_io_ctx中进行的,接下来,我们分析下AIO::linux_create_io_ctx的调用场景:

场景1:AIO::is_linux_native_aio_supported

该函数用来判断系统是否支持 AIO。

bool AIO::is_linux_native_aio_supported() {
  ...
  if (!linux_create_io_ctx(1, &io_ctx)) {
    return (false);
  }
  ...
}

这里只会初始化 1 个异步 I/O 请求。

场景2:AIO::init_linux_native_aio

该函数是用来初始化 Linux 原生异步 I/O 的。

dberr_t AIO::init_linux_native_aio() {
  ...
  ulint max_events = slots_per_segment();
  for (ulint i = 0; i < m_n_segments; ++i, ++ctx) {
    if (!linux_create_io_ctx(max_events, ctx)) {
      return (DB_IO_ERROR);
    }
  }
  return (DB_SUCCESS);
}

函数中的 m_n_segments 是需要创建的异步 I/O (AIO) 上下文的数量,max_events 是每个异步 I/O (AIO) 上下文支持的最大并发 I/O 请求数。所以,这个函数会初始化 m_n_segments * max_events 个异步 I/O 请求。

在 MySQL 的启动过程中,AIO::is_linux_native_aio_supported 只被调用一次,而 AIO::init_linux_native_aio 则会被调用三次,分别用于 insert buffer 线程、读线程和写线程的初始化。

这两个函数都是在AIO::start中调用的。

// storage/innobase/os/os0file.cc
bool AIO::start(ulint n_per_seg, ulint n_readers, ulint n_writers) {
#if defined(LINUX_NATIVE_AIO)
  /* Check if native aio is supported on this system and tmpfs */
  if (srv_use_native_aio && !is_linux_native_aio_supported()) {
    ib::warn(ER_IB_MSG_829) << "Linux Native AIO disabled.";
    srv_use_native_aio = false;
  }
#endif /* LINUX_NATIVE_AIO */
  ...
  if (0 < n_extra) {
    ...
    s_ibuf = create(LATCH_ID_OS_AIO_IBUF_MUTEX, n_per_seg, 1);
   ...
  }
  ...
  s_reads =
      create(LATCH_ID_OS_AIO_READ_MUTEX, n_readers * n_per_seg, n_readers);
  ...
  s_writes =
      create(LATCH_ID_OS_AIO_WRITE_MUTEX, n_writers * n_per_seg, n_writers);

  ...
  return true;
}

函数中的 n_per_seg 实际上就是 max_events。

n_per_seg 等于 8 * OS_AIO_N_PENDING_IOS_PER_THREAD,因为 OS_AIO_N_PENDING_IOS_PER_THREAD 是个常量,值为 32,所以 n_per_seg 等于 256。

AIO::init_linux_native_aio 中的 m_n_segments 实际上表示的是线程的数量:对于 insert buffer 线程,线程数为 1;对于读操作线程,线程数为 n_readers;对于写操作线程,线程数为 n_writers。

怎么知道 m_n_segments 就是线程的数量?

关键是在创建 AIO 对象时,会调用 AIO 的构造函数,而构造函数中的 m_slots 又决定了 max_events 的值。

AIO *AIO::create(latch_id_t id, ulint n, ulint n_segments) {
  ...
  AIO *array =
      ut::new_withkey<AIO>(UT_NEW_THIS_FILE_PSI_KEY, id, n, n_segments);
  ...
}

AIO::AIO(latch_id_t id, ulint n, ulint segments)
    : m_slots(n),
      m_n_segments(segments),
...
  
[[nodiscard]] ulint slots_per_segment() const {
  return (m_slots.size() / m_n_segments);
}

以读线程为例,AIO::create中的 n 等于 n_readers * n_per_seg,n_segments 等于 n_readers。

在初始化 AIO 对象时,n_readers * n_per_seg 将赋值给 m_slots,n_readers 将赋值给 m_n_segments。

所以AIO::init_linux_native_aio中的 max_events = slots_per_segment() = m_slots.size() / m_n_segments = n_readers * n_per_seg / n_readers = n_per_seg。

计算公式

基于上面的分析,我们可以推论出 MySQL 在启动过程中需要初始化的异步 I/O 请求数的计算公式。

(1 + innodb_read_io_threads + innodb_write_io_threads) * 256 + 1

最后一个 1 是判断系统是否支持 AIO。

验证

下面通过一个具体的案例来验证下上面的计算公式是否正确。

首先通过/proc/sys/fs/aio-nr查看当前系统中已分配的异步 I/O 请求的数量。

# cat /proc/sys/fs/aio-nr
4866

接着,启动一个 MySQL 8.4 实例,启动命令中显式设置 innodb_read_io_threads 和 innodb_write_io_threads。

# /usr/local/mysql8.4/bin/mysqld --defaults-file=/etc/my_3308.cnf --innodb-read-io-threads=64 --innodb-write-io-threads=4 &

实例启动后,再次查看/proc/sys/fs/aio-nr

# cat /proc/sys/fs/aio-nr
22531

两个数之间的差值是 17665。

按照之前的公式计算,也是 17665,完全吻合。

(1 + 64 + 4) * 256 + 1 = 17665

为什么 MySQL 8.4 启动会报错呢?

因为 innodb_read_io_threads 的默认值在 MySQL 8.4 中发生了变化。

在 MySQL 8.4 之前,innodb_read_io_threads 默认为 4,而在 MySQL 8.4 中,innodb_read_io_threads 默认等于主机逻辑 CPU 的一半,最小是 4,最大是 64。

static MYSQL_SYSVAR_ULONG(
    read_io_threads, srv_n_read_io_threads,
    PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_READONLY,
    "Number of background read I/O threads in InnoDB.", nullptr, nullptr,
    std::clamp(std::thread::hardware_concurrency() / 2, 4U, 64U), 1, 64, 0);

不巧,问题 case 主机的逻辑 CPU 是 128 核,所以就导致了 innodb_read_io_threads 等于 64。

这就意味着,在/proc/sys/fs/aio-max-nr等于 65536(默认值)的情况下,该主机上只能启动 3(65536/17665) 个 MySQL 8.4 实例。

结论

  1. MySQL 在启动时,如果出现io_setup() failed with EAGAIN错误,可适当增加/proc/sys/fs/aio-max-nr的值。
  2. MySQL 在启动过程中需要初始化的异步 I/O 请求数等于(1 + innodb_read_io_threads + innodb_write_io_threads) * 256 + 1
  3. innodb_read_io_threads 的默认值在 MySQL 8.4 中发生了变化,建议在配置文件中显式指定。

参考资料

io_setup(2) --- Linux manual page: https://www.man7.org/linux/man-pages/man2/io_setup.2.html

相关推荐
胡耀超11 分钟前
CentOS 7.9(linux) 设置 MySQL 8.0.30 开机启动详解
linux·mysql·centos
计算机学姐3 小时前
基于微信小程序的民宿预订管理系统
java·vue.js·spring boot·后端·mysql·微信小程序·小程序
web2u6 小时前
MySQL 中如何进行 SQL 调优?
java·数据库·后端·sql·mysql·缓存
新知图书7 小时前
MySQL用户授权、收回权限与查看权限
数据库·mysql·安全
文城5217 小时前
Mysql存储过程(学习自用)
数据库·学习·mysql
沉默的煎蛋7 小时前
MyBatis 注解开发详解
java·数据库·mysql·算法·mybatis
C语言扫地僧8 小时前
MySQL 事务及MVCC机制详解
数据库·mysql
小镇cxy8 小时前
MySQL事物,MVCC机制
数据库·mysql
雾里看山8 小时前
【MySQL】 库的操作
android·数据库·笔记·mysql
꧁瀟洒辵1恛꧂9 小时前
从新手到高手的蜕变:MySQL 视图进阶全攻略
数据库·mysql