问题

最近碰到一个 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

升级到 MySQL 8.4,MySQL 启动报错:io_setup() failed with EAGAIN的更多相关文章

  1. springboot启动报错:Failed to configure a DataSource

    一.背景 springboot的出现,让项目搭建变得更方便快捷,同时简化掉很多的样板化配置代码,提高开发效率. 通过idea生成springboot项目,启动报错:Failed to configur ...

  2. VM虚拟机启动报错Reason Failed to lock the file怎么办

    VMware启动报错Reason: Failed to lock the file的解决方法 症状:  启动VMware虚拟机的时候出现了Cannot open the disk '*.vmdk' o ...

  3. SSM项目启动报错:Failed to read candidate component class

    SSM项目启动报错:Failed to read candidate component class 换成3.1又没有问题,换成3.2又不行,查看编译环境用的是1.8,将1.8降为1.7,问题解决,服 ...

  4. docker启动报错iptables failed: -重建docker0网络恢复

    # docker启动报错 [root@localhost mysqlconf]# docker run -d -p 8080:8080 --link zookeeper:zookeeper -e du ...

  5. 解决Eclipse启动报错【Failed to create the Java Virtual Machine】

    电脑:2G内存,WIN7 32位. 启动adt-bundle-windows-x86-20140702\eclipse\eclipse.exe时,报错[Failed to create the Jav ...

  6. Hbase master启动报错:Failed construction of Master: class org.apache.hadoop.hbase.master.HMaster Caused by: java.net.UnknownHostException:

    Hbase master启动报错: java.lang.RuntimeException: Failed construction of Master: class org.apache.hadoop ...

  7. mysql启动报错:Failed to start LSB: start and stop MySQL

    报错信息: [root@youxx- bin]# service mysql status Redirecting to /bin/systemctl status mysql.service ¡ñ ...

  8. Tomcat启动报错:Failed to initialize end point associated with ProtocolHandler ["http-apr-8080"]

    在用MyEclipse做开发,启动Tomcat的时候,控制台老是报错Failed to initialize end point associated with ProtocolHandler [&q ...

  9. Tomcat启动报错:[Failed to start component]的解决方案

    在MyEclipse中启动Tomcat,该Tomcat仅部署了一个报错项目,启动Tomcat Server的全部信息如下: usage: java org.apache.catalina.startu ...

  10. 【tomcat】启动报错:Failed to initialize end point associated with ProtocolHandler ["http-apr-8080"] java.lang.Exception: Socket bind failed 和java.net.BindException: Address already in use: JVM_Bind错误解决

    背景:[新手] 将开发机子上的Tomcat连同其中的项目,一起拷贝到服务器上,启动tomcat的start.bat,然后报错如下: 问题1: Failed to initialize end poin ...

随机推荐

  1. 老外为了在MacBook上玩原神,让M1支持了所有iOS应用 | Github每周精彩分享第一期

    大家好,这里是每周更新的Github有趣项目分享,我是每周都在吃瓜的蛮三刀酱. 我会从Github热门榜里选出 高质量.有趣,牛B 的开源项目进行分享. 废话不多说,看看最近有什么有意思的Github ...

  2. Docker部署Scrapy-redis分布式爬虫框架(整合Selenium+Headless Chrome网页渲染)

    前言 我的京东价格监控网站需要不间断爬取京东商品页面,爬虫模块我采用了Scrapy+selenium+Headless Chrome的方式进行商品信息的采集. 由于最近爬虫用的服务器到期,需要换到新服 ...

  3. centos 7 开启 httpd 服务和 80 端口

    centos 7 开启 httpd 服务和 80 端口 yum install -y httpd systemctl start httpd firewall-cmd --add-service=ht ...

  4. su与sudo用法详解

    su与sudo用法详解 目录 su与sudo用法详解 1. su和sudo详解:切换用户身份 1.1 shell登录类型和环境配置文件 1.2 su进行身份切换 1.3 sudo命令详解 1.3.1 ...

  5. 7月 Splashtop上线了这些新功能 快来看鸭

    经过我们的攻城狮天天努力,我们的软件又得到了升级和完善,上线了一些有用的新功能和增强功能,快来看看吧. Splashtop已为Splashtop Business Access,Splashtop远程 ...

  6. OpenAirInterface,开源的 4G EPS 实现

    目录 文章目录 目录 前文列表 OSA OpenAirInterface OAI 的仿真 物理信道仿真 系统级仿真 OAI 的 SDR LTE 参考文档 前文列表 <USRP B210 软件定义 ...

  7. 定了!航天科技AIRIOT 物联网平台新品发布会,6月6日北京见!

    AIRIOT新品发布会预告 航天科技定档6月6日举办AIRIOT新品发布会,诚邀大家共同见证4.0版本的创新与赋能! 活动地点:北京雍和航星科技园. 现场参会请通过下方长图二维码进行报名! 亦可预约直 ...

  8. 我开源的H5商城2.0版本发布,强烈推荐

    简介 waynboot-mall 是一套全部开源的 H5 商城项目,包含运营后台.H5 商城前台和后端接口三个项目 .实现了一套完整的商城业务,有首页展示.商品分类.商品详情.sku 详情.商品搜索. ...

  9. Kubernetes:kubelet 源码分析之探针

    0. 前言 kubernetes 提供三种探针,配置探针(Liveness),就绪探针(Readiness)和启动(Startup)探针判断容器健康状态.其中,存活探针确定什么时候重启容器,就绪探针确 ...

  10. 推荐几款卓越的 .NET 开源搜索组件

    前言 在当今日益数据化的世界中,信息的检索和搜索功能对于各种应用来说变得至关重要. 无论是电子商务网站.企业资源规划系统.还是内容管理系统,高效的搜索功能都是提升用户体验.促进业务发展的关键. 因此, ...