问题
最近碰到一个 case,一台主机上,部署了多个实例。之前使用的是 MySQL 8.0,启动时没有任何问题。但升级到 MySQL 8.4 后,部分实例在启动时出现了以下错误。
下面我们来分析下这个报错的具体原因及解决方法。
定位过程
首先搜索下这个报错是在哪个文件产生的。
接着分析该文件中产生报错的具体函数。
可以看到,错误信息主要是在执行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
的值即可。
注意这种只是临时修改,系统重启就会失效。如果要永久修改,需调整 /etc/sysctl.conf。
问题解决了,接下来我们分析下同一台主机,为什么之前的 MySQL 8.0 没问题,升级到 MySQL 8.4 就报错了呢?
这个时候,就需要分析函数中 max_events 的生成逻辑了。
堆栈信息
下面是AIO::linux_create_io_ctx
函数被调用的堆栈信息。
堆栈中的重点是 #6 的srv_start
函数,这个函数会调用os_aio_init
来初始化异步 I/O 系统。
调用 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
中定义的。
所以,错误日志中看到的[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。
这里只会初始化 1 个异步 I/O 请求。
场景2:AIO::init_linux_native_aio
该函数是用来初始化 Linux 原生异步 I/O 的。
函数中的 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
中调用的。
函数中的 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::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 是判断系统是否支持 AIO。
验证
下面通过一个具体的案例来验证下上面的计算公式是否正确。
首先通过/proc/sys/fs/aio-nr
查看当前系统中已分配的异步 I/O 请求的数量。
接着,启动一个 MySQL 8.4 实例,启动命令中显式设置 innodb_read_io_threads 和 innodb_write_io_threads。
实例启动后,再次查看/proc/sys/fs/aio-nr
。
两个数之间的差值是 17665。
按照之前的公式计算,也是 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。
不巧,问题 case 主机的逻辑 CPU 是 128 核,所以就导致了 innodb_read_io_threads 等于 64。
这就意味着,在/proc/sys/fs/aio-max-nr
等于 65536(默认值)的情况下,该主机上只能启动 3(65536/17665) 个 MySQL 8.4 实例。
结论
- MySQL 在启动时,如果出现
io_setup() failed with EAGAIN
错误,可适当增加/proc/sys/fs/aio-max-nr
的值。 - MySQL 在启动过程中需要初始化的异步 I/O 请求数等于
(1 + innodb_read_io_threads + innodb_write_io_threads) * 256 + 1
- 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