MySQL 慢 concurrent/parallel 查询 Python

MySQL slow concurrent/parallel queries in Python

我们正在使用非常简单的 SELECT … WHERE 查询(1000 行,来自一个非常小的 table,有 18000 个条目)来加载时间序列数据以进行绘图(为了测试,我们是 运行 相同的查询 X 次)。虽然对 MySQL 数据库的单个查询如预期的那样快,但多个并行查询非常慢(下面的并行查询的简化 python 代码)。 当将并行查询的数量增加 10 倍时,mysql.slow_log 显示的查询时间是原来的两倍,但在 python 中,单个查询的时间是原来的五倍到十倍(0.04 秒对 0.4 秒) .

此外,我们可以看到第一次查询比最后一次查询快得多(最后一次查询为 0.5 秒,第一次查询为 0.06 秒)

显然 table 已完全索引。

我们也仅通过 SLEEP(2) 查询进行了尝试,得到了一个更小但相似的效果:对于其中的 20 个,每个在大约 2.03 秒后返回,对于 200 个,它们大约需要 2.1 秒。

innodb 缓冲区似乎工作正常,因为 MySQL 显示 0 Disk/IO 在 运行 查询时读取(所有数据似乎都在内存中)。这意味着 storage/IO 可能不是问题。

CPU 使用率可能不是 AWS RDS 中 CPU 使用率图表所指示的瓶颈。 网络使用似乎也不是问题,因为我们也看到了对较小查询的影响。

我们正在数据库上使用 AWS RDS MySQL 服务。t3.large 实例(2 vCPU,8GB RAM,5GBit/s 网络)。

重现问题的代码:

from concurrent.futures import ThreadPoolExecutor 
import pandas as pd 
import time 

executor = ThreadPoolExecutor(max_workers=200) 
futures = [] 

def read_db(query): 
    con = engine.connect() 
    start_single_query = time.time() 
    print(start_single) 
    result = pd.read_sql(query, con) 
    print(f"query finished: {time.time() - start_single}s")     
    con.close() 
    return result 

start_complete = time.time() 
query = "SELECT * FROM table WHERE id_security = 1000;" 
# query = "SELECT SLEEP(2)" 

for i in range(100):  
    future = executor.submit(read_db, query) 
    futures.append(future) 

for future in futures: # wait for all results 
    f=future.result() 
 
print(f"time total: {time.time() - start_complete}s") 

所以主要问题是:

更新:

创建 Table 语句:

CREATE TABLE `table` (
  `id_security` int(11) NOT NULL,
  `id_field` int(11) NOT NULL,
  `currency` varchar(3) DEFAULT NULL,
  `date` date NOT NULL,
  `timestamp` datetime DEFAULT CURRENT_TIMESTAMP,
  `value` double DEFAULT NULL,
  `source` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id_security`,`id_field`,`date`,`source`),
  KEY `Dateindex` (`date`),
  KEY `id_security_id_field` (`id_security`,`id_field`),
  KEY `id_security` (`id_security`),
  KEY `id_field` (`id_field`),
  KEY `id_source` (`source`),
  KEY `PRIMARY_WITHOUT_SOURCE` (`id_security`,`id_field`,`date`,`value`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

这些是尝试过的查询及其解释语句(不要介意生产中的不同 table 名称): SELECT * FROM series_security WHERE id_security = 1002 AND id_field = 100 AND date>'2021-01-01';

SELECT * 来自 series_security 其中 id_security = 1002 AND id_field = 100;

SELECT * 来自 series_security 其中 id_security = 1002

真正的 table 是 18.000.000 行长,但这并没有显着降低请求速度。所以指数似乎有效。尽管如此,我们还是在较小的 18.000 行上尝试了它 table 以确保。

更新二: 那只是一个虚拟 table 来测试这个问题。许多索引都没有在生产中,否则 Insert 语句可能会太慢。

产品table是这样创建的:

CREATE TABLE `table` (
  `id_security` int(11) NOT NULL,
  `id_field` int(11) NOT NULL,
  `currency` varchar(3) DEFAULT NULL,
  `date` date NOT NULL,
  `timestamp` datetime DEFAULT CURRENT_TIMESTAMP,
  `value` double DEFAULT NULL,
  `source` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id_security`,`id_field`,`date`,`source`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

我们尝试使用索引来了解问题所在。但是只要 PRIMARY KEY 在那里,就没有什么区别。

WHERE 子句中的 id 是一个整数。

innodb_buffer_pool_size 是内存的 3/4,所以 6GB(默认 RDS 设置)

由于我们在这个虚拟示例中对所有请求使用相同的查询,innodb_buffer_pool 达到 100%,我们的 IO 读取为零。

此外,即使我们对一个小虚拟对象进行更小的查询 table,我们也会看到类似的效果。

在生产环境中,一次查询通常会获取 500 到 20,000 行(时间序列数据)。

在这个虚拟示例中,我们尝试了多达 200 个线程(s.max_worker)

关于生产环境:

是否有更好的方法来测试 Python 中的 Futures/Threads 开销是问题还是 MySQL 数据库本身的并发请求?假设 MySQL 应该能够轻松处理完全在 innodb_buffer_pool 中的 100 个并发查询,而不会减慢 5 到 10 倍,这样的假设是否正确?

更新 3:

根据要求:

SELECT COUNT(*) FROM information_schema.tables;: https://pastebin.com/SRmKupdC -- 392 行

显示全局状态; https://pastebin.com/6F8kjCX6

显示全局变量; https://pastebin.com/r5tmFX7d

显示完整的流程列表; (空闲时) https://pastebin.com/Q3DhEZxV -- 没什么精彩的

显示完整的流程列表; (在网络服务器上有一些负载) https://pastebin.com/4FK1FjEC -- 一次查询,运行 只用了 0 秒:

SELECT  date as d,
    value  as v
    FROM  datalake.series_security
    WHERE  id_security = 1008
      AND  date >= '1962-02-09 00:00:00'
      AND  date <= '2099-01-01 00:00:00'
      AND  id_field=100
    ORDER BY  date ASC |

状态 https://pastebin.com/X5SL64XK -- 主要是 latin1

显示引擎 INNODB 状态; (虚度) https://pastebin.com/JPiqF76s --哈欠。

显示引擎 INNODB 状态; (在网络服务器上有一些负载) https://pastebin.com/GGsmzsXz -- 本质上是空闲的

删除冗余索引;他们妨碍了。 (或者至少这是我对其中两个 EXPLAIN 的看法。)

  -- good
  PRIMARY KEY                 (`id_security`,`id_field`,`date`,`source`),
  -- toss as redundant:
  KEY `id_security_id_field`  (`id_security`,`id_field`),
  KEY `id_security`           (`id_security`),
  -- not useful in the current queries:
  KEY `PRIMARY_WITHOUT_SOURCE` (`id_security`,`id_field`,`date`,`value`)
  KEY `id_field` (`id_field`),
  KEY `id_source` (`source`),
  KEY `Dateindex` (`date`),

您真的会获取所有列吗?在某些情况下,这会给查询带来不必要的负担。

您希望获取 1800 万行中的多少行?

您会使用不同的 id_security 值吗?

innodb_buffer_pool_size 的值是多少? (我想考虑数据的可缓存性,尤其是在做并行查询的时候。)

我怀疑超过 10 个并行查询是否有效;有一点“递减returns”。

Analysis of GLOBAL STATUS and VARIABLES:
 

观察:

  • 版本:5.7.33-log
  • 8 GB 内存
  • 正常运行时间 = 14 天 05:11:54
  • 5.98 Queries/sec : 3.39 Questions/sec

更重要的问题:

发生了什么事会导致 Max_used_connections 增长到数百个?当它达到大约几百时,MySQL 绊倒了自己,似乎“冻结了”。最好避免同时允许这么多连接运行。

max_allowed_packet 的值较大可能会存在交换风险。

使用 long_query_time = 0,每个查询都“慢”。然而,对于 slow_query_log = OFF,您无法获得有关处理的任何信息。 Re-think 在这里做什么。

一些具体的建议:

binlog_cache_size = 64M
max_connections = 300
max_allowed_packet = 50M

为什么要删除这么多行?看看 TRUNCATE 是否更有用。

除非数据会增长,否则 8GB 的​​实例是多余的。

检查是否所有Prepared statements都是“Closed”。

细节和其他观察:

( Binlog_cache_disk_use / Binlog_cache_use ) = 8,557 / 13019 = 65.7% -- 溢出到磁盘 -- 增加 binlog_cache_size(现在是 32768)

( innodb_lru_scan_depth * innodb_page_cleaners ) = 1,024 * 4 = 4,096 -- 页面清理器每秒的工作量。 -- "InnoDB: page_cleaner: 1000ms intended loop take ..." 可以通过降低 lru_scan_depth 来修复:考虑 1000 / innodb_page_cleaners(现在是 4)。还要检查交换。

( innodb_lru_scan_depth ) = 1,024 -- innodb_lru_scan_depth 是一个非常糟糕的命名变量。更好的名称是 innodb_free_page_target_per_buffer_pool。这是 InnoDB 尝试在每个缓冲池实例中保持空闲以加速读取和页面创建操作的页面数。 -- "InnoDB: page_cleaner: 1000ms intended loop take ..." 可以通过降低 lru_scan_depth

来修复

( innodb_io_capacity ) = 200 -- 刷新时,使用这么多 IOP。 -- 读取可能缓慢或不稳定。如果使用 SSD 驱动器,请使用 2000。

( Innodb_buffer_pool_pages_free / Innodb_buffer_pool_pages_total ) = 65,999 / 327680 = 20.1% -- 当前未使用的 buffer_pool 的百分比 -- innodb_buffer_pool_size(现在是 5368709120)是否比需要的大?

( innodb_flush_neighbors ) = innodb_flush_neighbors = 1 -- 将块写入磁盘时的一个小优化。 -- SSD 驱动器使用 0; 1 个用于硬盘。

( innodb_io_capacity ) = 200 - I/O 磁盘上的每秒操作数。 100 用于慢速驱动器; 200 用于旋转驱动器; SSD 1000-2000;乘以 RAID 系数。限制每秒写入 IO 请求 (IOPS)。 -- 对于初学者:HDD:200;固态硬盘:2000.

( innodb_adaptive_hash_index ) = innodb_adaptive_hash_index = ON -- 是否使用自适应哈希(AHI)。 -- ON 表示大部分只读; DDL-heavy

关闭

( innodb_flush_log_at_trx_commit ) = 1 -- 1 = 安全; 2 = 更快 --(您决定)使用 1 以及 sync_binlog(现在为 1)=1 以获得最大级别的容错。 0 最适合速度。 2 是 0 和 1 之间的折衷。

( innodb_print_all_deadlocks ) = innodb_print_all_deadlocks = OFF -- 是否记录所有死锁。 -- 如果您为死锁所困扰,请打开它。注意:如果有很多死锁,这可能会向磁盘写入大量内容。

( innodb_purge_threads ) = 1 -- 清理历史列表的线程数。 -- 如果写的比较多,推荐5.6和10.0以上版本4个。

( max_connections ) = 2,000 -- 最大连接数(线程)。影响各种分配。 -- 如果 max_connections(现在是 2000)太高并且各种内存设置都很高,您可能 运行 内存不足。

( 176000 * max_connections ) = (176000 * 2000) / 8192M = 4.1% -- 由于 max_connections 的大小,估计 ram 使用量。 -- max_connections(现在是2000)有点高

( max_allowed_packet ) = 999,999,488 / 8192M = 11.6% -- 如果您没有要加载的大 blob(等),则减小该值。否则减少 innodb_buffer_pool_size(现在是 5368709120)来腾出空间。交换对性能来说很糟糕。

( innodb_ft_result_cache_limit ) = 2,000,000,000 / 8192M = 23.3% -- FULLTEXT 结果集的字节限制。 (它会根据需要增长。) -- 降低设置。

( character_set_client ) = character_set_client = latin1--

( character_set_connection ) = character_set_connection = latin1--

( character_set_results ) = character_set_results = latin1--

( (Com_show_create_table + Com_show_fields) / Questions ) = (306 + 226110) / 4161091 = 5.4% -- 顽皮的框架 -- 花费大量精力重新发现模式。 -- 向第 3 方供应商投诉。

( local_infile ) = local_infile = ON -- local_infile(现在开启)= 开启是一个潜在的安全问题

( Created_tmp_disk_tables / Questions ) = 1,527,791 / 4161091 = 36.7% -- 需要 on-disk tmp table 的查询的 Pct。 -- 更好的索引/没有斑点/等等

( Created_tmp_disk_tables / Created_tmp_tables ) = 1,527,791 / 1840986 = 83.0% -- 溢出到磁盘的临时 table 的百分比 -- 可能增加tmp_table_size(现在16777216)和max_heap_table_size(现在16777216);改善指标;避免斑点等

( Com_rollback / (Com_commit + Com_rollback) ) = 490,964 / (100382 + 490964) = 83.0% -- 回滚:提交比率 -- 回滚代价高昂;更改应用程序逻辑

( (Com_insert + Com_update + Com_delete + Com_replace) / Com_commit ) = (168913 + 3650 + 23593 + 1066) / 100382 = 1.96 -- 每次提交的语句(假设所有 InnoDB) -- 低:可能有助于在事务中将查询分组;高:长期交易会带来各种压力。

( ( Com_stmt_prepare - Com_stmt_close ) / ( Com_stmt_prepare + Com_stmt_close ) ) = ( 118 - 102 ) / ( 118 + 102 ) = 7.3% -- 您要关闭准备好的报表吗? -- 添加关闭。

( Com_stmt_close / Com_stmt_prepare ) = 102 / 118 = 86.4% -- 准备好的语句应该关闭。 -- 检查是否所有的Prepared statements都是“Closed”。

( Com_admin_commands / Queries ) = 3,167,117 / 7344901 = 43.1% -- “管理”命令查询的百分比。 -- 怎么回事?

( Com__biggest ) = Com__biggest = Com_admin_commands -- 哪个“Com_”指标最大。 -- 通常是 Com_select(现在是 2792924)。如果是其他的,那么它可能是一个草率的平台,也可能是其他的。

( binlog_format ) = binlog_format = MIXED -- STATEMENT/ROW/MIXED。 -- ROW 优先于 5.7 (10.3)

( slow_query_log ) = slow_query_log = OFF -- 是否记录慢查询。 (5.1.12)

( Slow_queries ) = (Syncs) / 1228314 = 5.1 /sec -- 频率每秒慢查询) -- 返工慢人;改善指标;观察磁盘 space 是否有慢速日志文件

( Slow_queries / Questions ) = 6,269,663 / 4161091 = 150.7% -- 频率(占所有查询的百分比) -- 查找慢查询;检查索引。

( log_slow_slave_statements ) = log_slow_slave_statements = OFF -- (5.6.11, 5.7.1) 默认情况下,复制的语句不会出现在慢日志中;这使他们表现出来。 -- 在慢速日志中查看可能干扰副本读取的写入很有帮助。

( Max_used_connections ) = 626 -- High-water 连接标记 -- 大量非活动连接是可以的;超过 100 个活动连接可能会出现问题。 Max_used_connections(现在626)不区分它们; Threads_running(现在为 1)是瞬时的。

( connect_timeout ) = 86,400 -- DOS攻击漏洞太大

( thread_stack * max_connections ) = (262144 * 2000) / 8192M = 6.1% -- max_connections 的最小内存分配。 -- 更低 max_connections(现为 2000)

异常小:

Innodb_log_writes / Innodb_log_write_requests = 0.11%

异常大:

Com_purge_before_date = 12 /HR
Com_rename_table = 0.018 /HR
Com_replace_select = 1.5 /HR
Com_show_create_event = 0.0059 /HR
Com_show_create_trigger = 0.059 /HR
Com_show_profile = 0.0088 /HR
Com_show_profiles = 0.0029 /HR
Handler_delete = 114 /sec
Innodb_buffer_pool_pages_flushed / max(Questions, Queries) = 2.39
Innodb_buffer_pool_write_requests / Innodb_buffer_pool_pages_flushed = 213
Innodb_rows_deleted = 114 /sec
Max_execution_time_set = 74
Max_execution_time_set / Com_select = 0.00%
Performance_schema_file_instances_lost = 1
Prepared_stmt_count = 4
Ssl_accepts = 10,073
Ssl_default_timeout = 7,200
Ssl_finished_accepts = 10,073
Ssl_session_cache_misses = 4,432
Ssl_session_cache_overflows = 3,512
Ssl_used_session_cache_entries = 110
Ssl_verify_depth = 1.84e+19
Ssl_verify_mode = 5
Threads_connected = 339
Uptime - Uptime_since_flush_status = 1.12e+6
net_read_timeout = 86,400
net_write_timeout = 86,400
wait_timeout = 86,400

字符串异常:

gtid_mode = OFF_PERMISSIVE
innodb_data_home_dir = /rdsdbdata/db/innodb
innodb_fast_shutdown = 1
log_output = TABLE
log_statements_unsafe_for_binlog = OFF
optimizer_trace = enabled=off,one_line=off
optimizer_trace_features = greedy_search=on, range_optimizer=on, dynamic_range=on, repeated_subselect=on
relay_log_recovery = ON
slave_rows_search_algorithms = TABLE_SCAN,INDEX_SCAN
sql_slave_skip_counter = 0
time_zone = Europe/Amsterdam

每秒速率 = RPS

关于 AWS RDS 性能组更新的建议

table_open_cache_instances=1  # from 16 - your db.t3.large has ONLY 1 Core
innodb_lru_scan_depth=100  # from 1024 to conserve 90% of CPU cycles used for function
innodb_change_buffer_max_size=50  # from 25 percent to accomodate high volume INSERTS
read_rnd_buffer_size=96*1024  # from 512K to reduce handler_read_rnd_next RPS of 734
net_buffer_length=96*1024  # from 16K to reduce number packets in/out

观察: A) 根据 86400 的 WAIT_TIMEOUT 值,完成 24 小时保持开放的连接后,CLOSE 似乎丢失了。 B) 您的实例每 3-4 秒就会受到 com_rollback 和 handler_rollback 的困扰。 innodb_print_all_deadlocks=ON 会在您的错误日志中记录一些问题。

如需更多帮助,请查看个人资料以获取联系信息并联系我们以获取免费的可下载实用程序脚本以协助性能调整。您有更多的 AWS RDS 数据库调优机会。t3.large 实例。

我们发现,它与MySQL无关,而与Python的全局解释器锁(GIL)有关。

由于函数read_db()/pd.read_sql(query, con)是CPU-bound,Python有GIL,查询结果是按顺序接收和处理的

一种解决方案是使用多处理而不是多线程。可以轻松地将 ThreadPoolExecutor 与 concurrent.futures 中的 ProcessPoolExecutor 交换。