关于mysql:故障分析-从一则错误日志到-MySQL-认证机制与-bug-的深入分析

1次阅读

共计 8384 个字符,预计需要花费 21 分钟才能阅读完成。

作者:李锡超

一个爱笑的江苏苏宁银行 数据库工程师,次要负责数据库日常运维、自动化建设、DMP 平台运维。善于 MySQL、Python、Oracle,喜好骑行、钻研技术。
本文起源:原创投稿

* 爱可生开源社区出品,原创内容未经受权不得随便应用,转载请分割小编并注明起源。


研发同学反馈某零碎性能测试环境 MySQL 数据库相干的业务零碎运行失常,但存在大量正告日志,需配合剖析起因。

一、异常现象

mysql 谬误日志文件中存在大量如下信息:

2023-01-10T01:07:23.035479Z 13 [Warning] [MY-013360] [Server] Plugin sha256_password reported: ''sha256_password' is deprecated and will be removed in a future release. Please use caching_sha2_password instead'

要害环境信息

二、初步剖析

当看到如上正告日志时,依据经验主义,第一反馈应该是客户端的版本过低,其受权认证插件是服务端将要废除的版本,所以产生了以上告警信息。特地是一些常见的客户端的工具,可能会因为更新频率,会很容易触发该问题。

尝试复现

依据初步剖析倡议,将初步剖析倡议与研发同学沟通后,通过常见的数据库工具拜访数据库,看是否是否复现该谬误。但通过数据库外面常见的数据库用户,通过不同的工具拜访数据库,均未在拜访时刻触发该异样。
由此,第一次尝试复现失败。难道是因为其它起因?

再第一次尝试拜访的过程,通过实时察看数据库谬误日志。在用客户端尝试拜访的过程中,没有复现该谬误。然而依然看到对应的正告日志在继续输入到谬误日志文件。且频率较高、间隔时间固定,由此也证实在谬误不是数据库工具人工拜访的。

利用零碎运行失常,又不是客户端导致的!作为 DBA 的你,应该如何进一步剖析呢?

初放小招

因为所处测试环境,针对该谬误,能够执行如下操作启用 MySQL 的个别日志:

-- 开明个别日志:show variables like 'general_log';
set global general_log=on;
show variables like 'general_log';
-- 查看个别日志门路:show variables like 'general_log_file';

启用日志后,察看谬误日志,发现个别日志中如下记录:

提醒:发现异常后,立刻敞开个别日志,防止产生过多日志耗尽磁盘空间:

-- 开明个别日志:show variables like 'general_log';
set global general_log=off;
show variables like 'general_log';

即用户 dbuser2 在问题时刻从 10.x.y.43 服务器发动了拜访数据库的申请。确认异样拜访的用户和服务器后,查看数据库 mysql.user 表、skip-grant-tables 等配置,发现数据库并不存在该用户,且没有跳过受权表等配置。应用该用户将无奈登录到数据库。

将信息反馈至研发同学,很快就确认了是因为局部利用配置不合理,应用了不存在的数据库用户,并定时连贯数据库执行工作。于是研发同学批改配置后,正告日志不再产生。

那么该问题剖析到此,能够完结了么?

批改配置后,正告日志不在产生!但既然是不存在的用户,拜访时为什么还提醒认证插件将废除呢?

三、源码剖析

带着问题,首先想到的是:既然数据库用户为存在于 mysql.user 表,登录也会产生正告,难道这个用户是 mysql 的外部用户,被硬编码了么!于是取到对应版本源码,通过如下命令进行确认:

cd mysql-8.0.27/
grep -rwi "dbuser2" *

其拜访后果为空,即不存在猜测的“外部用户”。

失常登录认证逻辑

既然没有硬编码,那就只能是外部逻辑导致。于是首先对失常状况下 mysql 用户登录过程,源码剖析后果如下:

|—> handle_connection
  |—> thd_prepare_connection
    |—> login_connection
      |—> check_connection
        // 判断客户端的主机名是否能够登录 (mysql.user.host),如果 mysql.user.host 有 '%' 那么将 allow_all_hosts,容许所有主机。|—> acl_check_host   
        |—> acl_authenticate
          |—> server_mpvio_initialize // 初始化 mpvio 对象,包含赋值 mpvio->ip / mpvio->host
          |—> auth_plugin_name="caching_sha2_password"
          |—> do_auth_once
            |—> caching_sha2_password_authenticate // auth->authenticate_user
              |—> server_mpvio_read_packet // vio->read_packet(vio, &pkt) // pkt=passwd
                |—> parse_client_handshake_packet
                  |—> char *user = get_string(&end, &bytes_remaining_in_packet, &user_len);
                  |—> passwd = get_length_encoded_string(&end, &bytes_remaining_in_packet, &passwd_len);
                  |—> mpvio->auth_info.user_name = my_strndup(key_memory_MPVIO_EXT_auth_info, user, user_len, MYF(MY_WME))
                  // 依据 user 搜寻 mysql.user.host , 并与客户端的 hostname/ip 进行比拟:匹配记录后,赋值 mpvio->acl_user
                  |—> find_mpvio_user(thd, mpvio) 
                    |—> list = cached_acl_users_for_name(mpvio->auth_info.user_name); // 依据 user 搜寻 mysql.user.host 
                    |—> acl_user_tmp->host.compare_hostname(mpvio->host, mpvio->ip) //  与客户端的 hostname/ip 进行比拟
                    |—> mpvio->acl_user_plugin = mpvio->acl_user->plugin; // 赋值 acl_user_plugin 属性为用户的 plugin 名
                    |—> mpvio->auth_info.multi_factor_auth_info[0].auth_string = mpvio->acl_user->credentials[PRIMARY_CRED].m_auth_string.str; 
                    |—> mpvio->auth_info.auth_string = mpvio->auth_info.multi_factor_auth_info[0].auth_string; 
                  |—> if (my_strcasecmp(system_charset_info, mpvio->acl_user_plugin.str,plugin_name(mpvio->plugin)->str) != 0)
                  |—> my_strcasecmp(system_charset_info, client_plugin,user_client_plugin_name) // 查看客户端的认证插件与用户插件是否雷同
              |—> make_hash_key(info->authenticated_as, hostname ? hostname : nullptr, authorization_id);  // 生成 authorization_id = user1\000% 
              |—> g_caching_sha2_password->fast_authenticate(authorization_id,*scramble,20,pkt,false) // 进行疾速受权操作
                |—> m_cache.search(authorization_id, digest) // 依据 user、host 搜寻明码,赋值到 digest
                |—> Validate_scramble validate_scramble_first(scramble, digest.digest_buffer[0], random, random_length);
                |—> validate_scramble_first.validate(); // 校验 scramble
              // 如验证胜利
              |—> vio->write_packet(vio, (uchar *)&fast_auth_success, 1)
              |—> return CR_OK;
              // 否则进行进行慢受权操作
              |—> g_caching_sha2_password->authenticate(authorization_id, serialized_string, plaintext_password);
          |—> server_mpvio_update_thd(thd, &mpvio);
          |—> check_and_update_password_lock_state(mpvio, thd, res);
          // 持续其它受权操作 

即外围的认证操作在函数 caching_sha2_password_authenticate() 中执行,先调用函数 find_mpvio_user(),通过 user、hostname 找到曾经配置的用户,而后调用函数 fast_authenticate() 对明码进行疾速验证。

应用不存在用户认证逻辑

当用户不存在时,mysql 用户登录过程,源码剖析后果如下:

|—> handle_connection
  |—> thd_prepare_connection
    |—> login_connection
      |—> check_connection
        // 判断客户端的主机名是否能够登录 (mysql.user.host),如果 mysql.user.host 有 '%' 那么将 allow_all_hosts,容许所有主机。|—> acl_check_host   
        |—> acl_authenticate
          |—> server_mpvio_initialize // 初始化 mpvio 对象,包含赋值 mpvio->ip / mpvio->host
          |—> auth_plugin_name="caching_sha2_password"
          |—> do_auth_once
            |—> caching_sha2_password_authenticate // auth->authenticate_user
              |—> server_mpvio_read_packet // vio->read_packet(vio, &pkt) // pkt=passwd
                |—> parse_client_handshake_packet
                  |—> char *user = get_string(&end, &bytes_remaining_in_packet, &user_len);
                  |—> passwd = get_length_encoded_string(&end, &bytes_remaining_in_packet, &passwd_len);
                  |—> mpvio->auth_info.user_name = my_strndup(key_memory_MPVIO_EXT_auth_info, user, user_len, MYF(MY_WME))
                  |—> find_mpvio_user(thd, mpvio) 
                    |—> list = cached_acl_users_for_name(mpvio->auth_info.user_name); // 依据 user 搜寻 mysql.user.host, 因为用户不存在,搜寻不到记录
                    |—> mpvio->acl_user = decoy_user(usr, hst, mpvio->mem_root, mpvio->rand, initialized); // 
                      |—> Auth_id key(user);
                      // 判断是否用户存在于 unknown_accounts
                      |—> unknown_accounts->find(key, value)
                      // 如存在:|—> user->plugin = Cached_authentication_plugins::cached_plugins_names[value];
                      // 如不存在:|—> const int DECIMAL_SHIFT = 1000;
                      |—> const int random_number = static_cast<int>(my_rnd(rand) * DECIMAL_SHIFT);
                      |—> uint plugin_num = (uint)(random_number % ((uint)PLUGIN_LAST));
                      |—> user->plugin = Cached_authentication_plugins::cached_plugins_names[plugin_num];
                      |—> unknown_accounts->insert(key, plugin_num)
                    |—> mpvio->acl_user_plugin = mpvio->acl_user->plugin; // 赋值 acl_user_plugin 属性为用户的 plugin 名
                    |—> mpvio->auth_info.multi_factor_auth_info[0].auth_string = mpvio->acl_user->credentials[PRIMARY_CRED].m_auth_string.str; // ""|—> mpvio->auth_info.auth_string = mpvio->auth_info.multi_factor_auth_info[0].auth_string; //""
                    |—> mpvio->auth_info.additional_auth_string_length = 0; // 0
                    |—> mpvio->auth_info.auth_string_length = mpvio->auth_info.multi_factor_auth_info[0].auth_string_length; // 0
                  |—> if (my_strcasecmp(system_charset_info, mpvio->acl_user_plugin.str,plugin_name(mpvio->plugin)->str) != 0)
                  |—> return packet_error;
                |—> if (pkt_len == packet_error) goto err;
                |—> return -1;
          |—> auth_plugin_name = mpvio.acl_user->plugin;
          |—> res = do_auth_once(thd, auth_plugin_name, &mpvio);
            |—> sha256_password_authenticate() //auth->authenticate_user(mpvio, &mpvio->auth_info);
              |—> LogPluginErr // Deprecate message for SHA-256 authentication plugin.
              // 打印: 2023-01-10T01:07:23.035479Z 13 [Warning] [MY-013360] [Server] Plugin sha256_password reported: ''sha256_password' is deprecated and will be removed in a future release. Please use caching_sha2_password instead'
              |—> server_mpvio_read_packet() // vio->read_packet(vio, &pkt)
              |—> if (info->auth_string_length == 0 && info->additional_auth_string_length == 0) // info -> auth_info
              |—>   return CR_ERROR;
            |—> return res; // 0
          |—> server_mpvio_update_thd(thd, &mpvio);
          |—> check_and_update_password_lock_state(mpvio, thd, res); // 间接返回
          ...
          |—> login_failed_error // 打印登录报错信息
          // 2023-01-10T02:02:44.659796Z 19 [Note] [MY-010926] [Server] Access denied for user 'user2'@'localhost' (using password: YES)
      |—> thd->send_statement_status();  // 客户端终止 

即当应用不存在的用户登录数据库时,通过函数 decoy_user() 创立的 acl_user 对象。在创立这个对象时,其 plugin 属性采纳随机形式从 cached_plugins_enum 抉择。从而有可能抉择到 PLUGIN_SHA256_PASSWORD 插件。但在函数 sha256_password_authenticate() 的入口,就会生成 Warning 级别的提醒,以提醒该验证 PLUGIN_SHA256_PASSWORD 将被废除。随后,因为在 decoy_user() 创立的 acl_user 对象 auth_string_length 长度未 0,在后续的认证逻辑中会间接返回 CR_ERROR,即认证失败。

根因总结

依据以上认证过的剖析,导致谬误日志存在 PLUGIN_SHA256_PASSWORD 将被废除的根本原因为:在以后版本,当应用不存在的用户登录数据库时,mysql 会随机抉择用户的明码认证插件,在以后的版本版本中,有 1 / 3 的概率会抉择到 PLUGIN_SHA256_PASSWORD 插件。抉择该插件后,在后续的认证逻辑将会触发正告日志生成。

四、问题解决

综合以上剖析过程,导致该问题的间接起因是利用配置了不存在的数据库用户,根本原因为数据库登录认证逻辑存在肯定缺点。那么解决该问题可参考如下几种计划:

1. 参考初步剖析中的计划,将利用的连贯配置批改为正确的用户信息;

2. 能够在 mysql 数据库中通过参数将该告警过滤,防止该告警信息输出到谬误日志文件。相干配置如下:

show variables like 'log_error_suppression_list';
set global log_error_suppression_list='MY-013360;
show variables like 'log_error_suppression_list';

留神,应用该计划也会导致某个存在且应用 SHA256_PASSWORD 认证插件产生的告警。能够作为长期计划;

3. 批改 mysql 代码,防止在应用不存在用户登录数据库时,抉择 SHA256_PASSWORD 认证插件。目前针对该计划已提交 Bug #109635。

附:要害函数地位

find_mpvio_user() (./sql/auth/sql_authentication.cc:2084)
parse_client_handshake_packet() (./sql/auth/sql_authentication.cc:2990)
server_mpvio_read_packet() (./sql/auth/sql_authentication.cc:3282)
caching_sha2_password_authenticate() (./sql/auth/sha2_password.cc:955)
do_auth_once() (./sql/auth/sql_authentication.cc:3327)
acl_authenticate() (./sql/auth/sql_authentication.cc:3799)
check_connection() (./sql/sql_connect.cc:651)
login_connection() (./sql/sql_connect.cc:716)
thd_prepare_connection() (./sql/sql_connect.cc:889)
handle_connection() (./sql/conn_handler/connection_handler_per_thread.cc:298)
正文完
 0