MySQL 2038 问题分析

数据库关系型数据库技术服务知识库
前言

在 MySQL 中,timestamp 采用 4 字节进行存储,取值范围是 '1970-01-01 00:00:01' UTC to '2038-01-19 03:14:07' UTC [1]。如果时间来到 2038 年,而官方没有采取行动,那么 MySQL 将无法正常使用。

问题复现

查看 MySQL PID

(base) [root@ip-10-0-0-22 logs]# pidof mysqld
8763

查看版本并运行些 SQL 语句

mysql> select version();
+------------+
| version()  |
+------------+
| 5.7.19-log |
+------------+
1 row in set (0.00 sec)

mysql> select now();
+---------------------+
| now()               |
+---------------------+
| 2022-10-10 00:00:42 | 
+---------------------+
1 row in set (0.00 sec)

mysql> insert into student_score select * from student_score limit 1;
Query OK, 1 row affected (0.00 sec)
Records: 1  Duplicates: 0  Warnings: 0

查看当前连接的processlist id 和 对应的 OS_ID

mysql> show full processlist;
+----+------+-----------+--------+---------+------+----------+-----------------------+
| Id | User | Host      | db     | Command | Time | State    | Info                  |
+----+------+-----------+--------+---------+------+----------+-----------------------+
|  3 | root | localhost | rudonx | Query   |    0 | starting | show full processlist |
+----+------+-----------+--------+---------+------+----------+-----------------------+
1 row in set (0.00 sec)

mysql> select * from performance_schema.threads where processlist_id=3\G;
*************************** 1. row ***************************
          THREAD_ID: 28
               NAME: thread/sql/one_connection
               TYPE: FOREGROUND
     PROCESSLIST_ID: 3
   PROCESSLIST_USER: root
   PROCESSLIST_HOST: localhost
     PROCESSLIST_DB: rudonx
PROCESSLIST_COMMAND: Query
   PROCESSLIST_TIME: 0
  PROCESSLIST_STATE: Sending data
   PROCESSLIST_INFO: select * from performance_schema.threads where processlist_id=3
   PARENT_THREAD_ID: 1
               ROLE: NULL
       INSTRUMENTED: YES
            HISTORY: YES
    CONNECTION_TYPE: Socket
       THREAD_OS_ID: 8797
1 row in set (0.00 sec)

ERROR: 
No query specified

修改系统时间

mysql> system date -s '2038-10-10';
Sun Oct 10 00:00:00 UTC 2038

查看错误日志,发现 MySQL会立刻挂掉,理由也很简单,当前不支持 2038 年以后的日期

2022-10-10T00:00:06.002600Z 0 [Note] InnoDB: Buffer pool(s) load completed at 221010  0:00:06
2038-10-10T00:00:00.000799Z 0 [Note] InnoDB: page_cleaner: 1000ms intended loop took 504921488258ms. The settings might not be optimal. (flushed=0 and evicted=0, during the time.)
2038-10-10T00:00:08.371723Z 3 [Warning] Current time has got past year 2038. Validating current time with 5 iterations before initiating the normal server shutdown process.
2038-10-10T00:00:08.371952Z 3 [Warning] Iteration 1: Current time obtained from system is greater than 2038
2038-10-10T00:00:08.372164Z 3 [Warning] Iteration 2: Current time obtained from system is greater than 2038
2038-10-10T00:00:08.372375Z 3 [Warning] Iteration 3: Current time obtained from system is greater than 2038
2038-10-10T00:00:08.372585Z 3 [Warning] Iteration 4: Current time obtained from system is greater than 2038
2038-10-10T00:00:08.372804Z 3 [Warning] Iteration 5: Current time obtained from system is greater than 2038
2038-10-10T00:00:08.372949Z 3 [ERROR] This MySQL server doesn't support dates later then 2038
……
2038-10-10T00:00:09.544779Z mysqld_safe mysqld from pid file /home/mysql3306/mysql.pid ended
问题分析

使用 strace 进行分析,在每次运行语句时,都会调用 gettimeofday(),其中的 tv_sec 表示自 1970 年到语句执行时经过的秒数,如下:

8797  00:00:42.645223 recvfrom(45, "\3select now()", 13, MSG_DONTWAIT, NULL, NULL) = 13 <0.000032>
8797  00:00:42.645339 gettimeofday({tv_sec=1665360042, tv_usec=645375}, NULL) = 0 <0.000036>

8797  00:01:23.828475 recvfrom(45, "\3insert into student_score selec"..., 62, MSG_DONTWAIT, NULL, NULL) = 62 <0.000030>
8797  00:01:23.828556 gettimeofday({tv_sec=1665360083, tv_usec=828575}, NULL) = 0 <0.000025>

8797  00:01:33.596279 recvfrom(45, "\3show full processlist", 22, MSG_DONTWAIT, NULL, NULL) = 22 <0.000029>
8797  00:01:33.596356 gettimeofday({tv_sec=1665360093, tv_usec=596375}, NULL) = 0 <0.000024>

8797  00:01:38.749061 recvfrom(45, "\3select * from performance_schem"..., 64, MSG_DONTWAIT, NULL, NULL) = 64 <0.000030>
8797  00:01:38.749141 gettimeofday({tv_sec=1665360098, tv_usec=749161}, NULL) = 0 <0.000025>

```
`
查看 pstack,会发现每次运行 SQL 语句都走到了/sql/sql_parse.cc 的逻辑中
```shell
#9  0x0000000000bf885b in Protocol_classic::get_command (this=0x7f290c001d90, com_data=0x7f2976c7cde0, cmd=0x7f2976c7cdd0) at /usr/local/mysql-5.7.19/sql/protocol_classic.cc:965
#10 0x0000000000c9523c in do_command (thd=thd@entry=0x7f290c000d40) at /usr/local/mysql-5.7.19/sql/sql_parse.cc:938
#11 0x0000000000d4f680 in handle_connection (arg=arg@entry=0x7f2918000b20) at /usr/local/mysql-5.7.19/sql/conn_handler/connection_handler_per_thread.cc:300
```

查看源码发现,sql_parse.cc 中的 dispatch_command 有如下注释,其中说明,在语句执行性会调用 gettimeofday() 系统函数,用于检查时间是否大于 2038,并进行重试 5 次,然后接着就会 shutdown MySQL [2]。
````undefined
if (is_time_t_valid_for_timestamp(thd->query_start_in_secs()) == false) {
    /*
      If the time has gone past end of epoch we need to shutdown the server. But
      there is possibility of getting invalid time value on some platforms.
      For example, gettimeofday() might return incorrect value on solaris
      platform. Hence validating the current time with 5 iterations before
      initiating the normal server shutdown process because of time getting
      past 2038.
    */

if (tries > max_tries) {
      /*
        If the time has got past epoch, we need to shut this server down.
        We do this by making sure every command is a shutdown and we
        have enough privileges to shut the server down
        TODO: remove this when we have full 64 bit my_time_t support
      */
      LogErr(ERROR_LEVEL, ER_UNSUPPORTED_DATE);
      ulong master_access = thd->security_context()->master_access();
      thd->security_context()->set_master_access(master_access | SHUTDOWN_ACL);
      error = true;
      kill_mysql();
    }
  }
    
```
`
从 strace 中也可以看到 调用了 tgkill 来 shutdown mysqld
```shell
8797  00:00:08.372856 write(2, "2038-10-10T00:00:08.372804Z 3 [W"..., 108) = 108 <0.000028>
8797  00:00:08.372929 gettimeofday({tv_sec=2170281608, tv_usec=372949}, NULL) = 0 <0.000027>
8797  00:00:08.373000 write(2, "2038-10-10T00:00:08.372949Z 3 [E"..., 94) = 94 <0.000029>
8797  00:00:08.373080 getpid()          = 8763 <0.000027>
8797  00:00:08.373148 tgkill(8763, 8792, SIGTERM) = 0 <0.000035>
```

# 解决方案
从 TODO 来看,MySQL 官方后续会支持 my_time_t 64 位的版本,截止到目前,还有 16 年之久,留给 MySQL 的时间还有很多。
# 参考文档
[1] [https://dev.mysql.com/doc/refman/5.7/en/datetime.html](https://dev.mysql.com/doc/refman/5.7/en/datetime.html)
[2] [Source Code /sql/sql_parse.cc](https://github.com/mysql/mysql-server/blob/6846e6b2f72931991cc9fd589dc9946ea2ab58c9/sql/sql_parse.cc)
**如果您有其他问题,欢迎您联系火山引擎**[技术支持服务](https://console.volcengine.com/ticket/createTicketV2/)

45
0
0
0
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论