升级MySQL 8.4后启动报错io_setup()失败EAGAIN,原因是什么?

摘要:问题 最近碰到一个 case,一台主机上,部署了多个实例。之前使用的是 MySQL 8.0,启动时没有任何问题。但升级到 MySQL 8.4 后,部分实例在启动时出现了以下错误。 [Warning] [MY-012
问题 最近碰到一个 case,一台主机上,部署了多个实例。之前使用的是 MySQL 8.0,启动时没有任何问题。但升级到 MySQL 8.4 后,部分实例在启动时出现了以下错误。 [Warning][MY-012582][InnoDB]io_setup()failedwithEAGAIN.Willmake5attemptsbeforegivingup. [Warning][MY-012583][InnoDB]io_setup()attempt1. [Warning][MY-012583][InnoDB]io_setup()attempt2. [Warning][MY-012583][InnoDB]io_setup()attempt3. [Warning][MY-012583][InnoDB]io_setup()attempt4. [Warning][MY-012583][InnoDB]io_setup()attempt5. [ERROR][MY-012584][InnoDB]io_setup()failedwithEAGAINafter5attempts. [ERROR][MY-012954][InnoDB]CannotinitializeAIOsub-system [ERROR][MY-012930][InnoDB]PlugininitializationabortedwitherrorGenericerror. [ERROR][MY-010334][Server]FailedtoinitializeDDStorageEngine [ERROR][MY-010020][Server]DataDictionaryinitializationfailed. [ERROR][MY-010119][Server]Aborting [System][MY-010910][Server]/usr/local/mysql/bin/mysqld:Shutdowncomplete(mysqld8.4.0)MySQLCommunityServer-GPL. [System][MY-015016][Server]MySQLServer-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()failedwithEAGAIN." /usr/src/mysql-8.4.0/storage/innobase/os/os0file.cc:<<"io_setup()failedwithEAGAINafter" 接着分析该文件中产生报错的具体函数。 //storage/innobase/os/os0file.cc boolAIO::linux_create_io_ctx(ulintmax_events,io_context_t*io_ctx){ ssize_tn_retries=0; for(;;){ memset(io_ctx,0x0,sizeof(*io_ctx)); intret=io_setup(max_events,io_ctx); if(ret==0){ /*Success.Returnnow.*/ return(true); } switch(ret){ case-EAGAIN: if(n_retries==0){ /*Firsttimearound.*/ ib::warn(ER_IB_MSG_757)<<"io_setup()failedwithEAGAIN." "Willmake" <<OS_AIO_IO_SETUP_RETRY_ATTEMPTS <<"attemptsbeforegivingup."; } 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; } /*Havetriedenough.Bettercallitaday.*/ ib::error(ER_IB_MSG_759) <<"io_setup()failedwithEAGAINafter" <<OS_AIO_IO_SETUP_RETRY_ATTEMPTS<<"attempts."; break; ... } ib::info(ER_IB_MSG_762)<<"YoucandisableLinuxNativeAIOby" "settinginnodb_use_native_aio=0inmy.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的值即可。 #echo1048576>/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函数被调用的堆栈信息。 #0AIO::linux_create_io_ctx(max_events=256,io_ctx=0x7fffe02d1500) at/usr/src/mysql-8.4.0/storage/innobase/os/os0file.cc:2559 #10x0000000004db1649inAIO::init_linux_native_aio(this=0x7fffe02d1d70) at/usr/src/mysql-8.4.0/storage/innobase/os/os0file.cc:6139 #20x0000000004db16edinAIO::init(this=0x7fffe02d1d70) at/usr/src/mysql-8.4.0/storage/innobase/os/os0file.cc:6167 #30x0000000004db1826inAIO::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 #40x0000000004db1a2binAIO::start(n_per_seg=256,n_readers=64,n_writers=4) at/usr/src/mysql-8.4.0/storage/innobase/os/os0file.cc:6254 #50x0000000004db261ainos_aio_init(n_readers=64,n_writers=4) at/usr/src/mysql-8.4.0/storage/innobase/os/os0file.cc:6514 #60x0000000004ee6b4dinsrv_start(create_new_db=false) at/usr/src/mysql-8.4.0/storage/innobase/srv/srv0start.cc:1743 #70x0000000004bdfc92ininnobase_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 #80x0000000004bf0e22ininnobase_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 #90x00000000049153b8indd::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 #100x0000000004916195indd::bootstrap::restart_dictionary(thd=0xb7ce6b0) at/usr/src/mysql-8.4.0/sql/dd/impl/bootstrap/bootstrapper.cc:907 #110x0000000003814e54inbootstrap::handle_bootstrap(arg=0x7fffffffcf20) at/usr/src/mysql-8.4.0/sql/bootstrap.cc:340 #120x0000000005792a92inpfs_spawn_thread(arg=0xb7ece50)at/usr/src/mysql-8.4.0/storage/perfschema/pfs.cc:3051 #130x00007ffff7bc6ea5instart_thread()from/lib64/libpthread.so.0 #140x00007ffff5ff0b0dinclone()from/lib64/libc.so.6 堆栈中的重点是 #6 的srv_start函数,这个函数会调用os_aio_init来初始化异步 I/O 系统。 //storage/innobase/srv/srv0start.cc dberr_tsrv_start(boolcreate_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"CannotinitializeAIOsub-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。 boolAIO::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_tAIO::init_linux_native_aio(){ ... ulintmax_events=slots_per_segment(); for(ulinti=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 boolAIO::start(ulintn_per_seg,ulintn_readers,ulintn_writers){ #ifdefined(LINUX_NATIVE_AIO) /*Checkifnativeaioissupportedonthissystemandtmpfs*/ if(srv_use_native_aio&&!is_linux_native_aio_supported()){ ib::warn(ER_IB_MSG_829)<<"LinuxNativeAIOdisabled."; 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); ... returntrue; } 函数中的 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_tid,ulintn,ulintn_segments){ ... AIO*array= ut::new_withkey<AIO>(UT_NEW_THIS_FILE_PSI_KEY,id,n,n_segments); ... } AIO::AIO(latch_id_tid,ulintn,ulintsegments) :m_slots(n), m_n_segments(segments), ... [[nodiscard]]ulintslots_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。 staticMYSQL_SYSVAR_ULONG( read_io_threads,srv_n_read_io_threads, PLUGIN_VAR_RQCMDARG|PLUGIN_VAR_READONLY, "NumberofbackgroundreadI/OthreadsinInnoDB.",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 实例。 结论 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