2022年2月17日

MySQL+Percona高性能初探,挑战10万并发量

作者 admin

在这篇文章中,我想探索一种与MySQL建立100,000个连接的方法。不仅仅是空闲连接,而是执行查询。

100,000 个连接。你可能会问,MySQL真的需要吗?虽然这似乎有些过分,但我在客户部署中看到了很多不同的设置。有些部署一个应用程序连接池,每个池中有 100 个应用程序服务器和 1,000 个连接。某些应用程序使用”如果查询太慢,则重新连接并重试”技术,这是一种可怕的做法。它可能导致滚雪球效应,并且可以在几秒钟内与MySQL建立数千个连接,导致系统崩溃。

所以现在我想设定一个超额完成的目标,看看我们是否能实现它。

设置

为此,我将使用以下硬件:

裸机服务器,实例大小:c2.medium.x86
物理内核 @ 2.2 GHz
(1 X AMD EPYC 7401P)
内存: 64 GB ECC RAM
存储: 英特尔®固态盘 DC S4500, 480GB

这是服务器级 SATA 固态硬盘。

我将使用其中五个Server,原因如下所述。一个Server用于MySQL服务器,四个Server用于客户端连接。

对于服务器,我将使用Percona Server for MySQL 8.0.13-4作为线程池插件。该插件将需要支持数千个连接。

初始服务器设置

网络设置(Ansible 格式):

Shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
– { name: ‘net.core.somaxconn’, value: 32768 }
– { name: ‘net.core.rmem_max’, value: 134217728 }
– { name: ‘net.core.wmem_max’, value: 134217728 }
– { name: ‘net.ipv4.tcp_rmem’, value: ‘4096 87380 134217728’ }
– { name: ‘net.ipv4.tcp_wmem’, value: ‘4096 87380 134217728’ }
– { name: ‘net.core.netdev_max_backlog’, value: 300000 }
– { name: ‘net.ipv4.tcp_moderate_rcvbuf’, value: 1 }
– { name: ‘net.ipv4.tcp_no_metrics_save’, value: 1 }
– { name: ‘net.ipv4.tcp_congestion_control’, value: ‘htcp’ }
– { name: ‘net.ipv4.tcp_mtu_probing’, value: 1 }
– { name: ‘net.ipv4.tcp_timestamps’, value: 0 }
– { name: ‘net.ipv4.tcp_sack’, value: 0 }
– { name: ‘net.ipv4.tcp_syncookies’, value: 1 }
– { name: ‘net.ipv4.tcp_max_syn_backlog’, value: 4096 }
– { name: ‘net.ipv4.tcp_mem’, value: ‘50576   64768 98152’ }
– { name: ‘net.ipv4.ip_local_port_range’, value: ‘4000 65000’ }
– { name: ‘net.ipv4.netdev_max_backlog’, value: 2500 }
– { name: ‘net.ipv4.tcp_tw_reuse’, value: 1 }
– { name: ‘net.ipv4.tcp_fin_timeout’, value: 5 }

这些是建议用于 10Gb 网络和高并发工作负载的典型设置。

systemd 的限制设置:

Shell

1
2
3
[Service]
LimitNOFILE=1000000
LimitNPROC=500000

以及 my.cnf 中 MySQL 的相关设置:

Shell

1
2
back_log=3500
max_connections=110000

对于客户端,我将使用 sysbench 版本 0.5 而不是 1.0.x,原因如下所述。

工作负载为sysbench –test=sysbench/tests/db/select.lua –mysql-host=139.178.82.47 –mysql-user=sbtest –mysql-password=sbtest –oltp-tables-count=10–report-interval=1 –num-threads=10000 –max-time=300 –max-requests=0 –oltp-table-size=10000000 –rand-type=uniform — rand-init=on run

步骤 1.  10,000 个连接

这个很容易,因为没有太多的事情要做。我们只能用一个客户端来做到这一点。但是,您可能会在客户端遇到以下错误:

错误 2004年:无法创建 TCP/IP 套接字 (24)

这是由打开的文件限制引起的,这也是 TCP/IP 套接字的限制。这可以通过设置来修复ulimit -n 100000在客户端上。

我们观察到的性能:

Shell

1
2
[  26s] threads: 10000, tps: 0.00, reads: 33367.48, writes: 0.00, response time: 3681.42ms (95%), errors: 0.00, reconnects:  0.00
[  27s] threads: 10000, tps: 0.00, reads: 33289.74, writes: 0.00, response time: 3690.25ms (95%), errors: 0.00, reconnects:  0.00

步骤 2.  25,000 个连接

对于25,000个连接,我们在MySQL端遇到了一个错误:

无法创建新 线程 (errno 11); 如果 可用内存不足,可以 查阅手册,了解 可能与操作系统相关的错误

但是,在我们的情况下,这无济于事,因为我们的所有限制都设置得足够高:

Shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cat /proc/`pidof mysqld`/limitsLimit                     Soft Limit Hard Limit           UnitsMax cpu time              unlimited  unlimited            secondsMax file size             unlimited  unlimited            bytesMax data size             unlimited  unlimited            bytesMax stack size            8388608    unlimited            bytesMax core file size        0          unlimited            bytesMax resident set          unlimited  unlimited            bytesMax processes             500000     500000               processesMax open files            1000000    1000000              filesMax locked memory         16777216   16777216             bytesMax address space         unlimited  unlimited            bytesMax file locks            unlimited  unlimited            locksMax pending signals       255051     255051               signalsMax msgqueue size         819200     819200               bytesMax nice priority         0          0Max realtime priority     0          0Max realtime timeout      unlimited unlimited            us

加:

thread_handling = pool-of-threads

到 my.cnf 并重新启动 Percona 服务器

结果:

Shell

1
2
[   7s] threads: 25000, tps: 0.00, reads: 33332.57, writes: 0.00, response time: 974.56ms (95%), errors: 0.00, reconnects:  0.00[   8s] threads: 25000, tps: 0.00, reads: 33187.01, writes: 0.00, response time: 979.24ms (95%), errors: 0.00, reconnects:  0.00

我们的吞吐量相同,但实际上 95% 的响应时间已从 3690 毫秒提高到 979 毫秒(这要归功于线程池)。

步骤 3. 50,000 个连接

这是我们遇到最大挑战的地方。首先,尝试在性能测试工具sysbench上获取50,000个连接,我们遇到了以下错误:

FATAL: error 2003: Can’t connect to MySQL server on ‘139.178.82.47’ (99)

Error (99) 是隐晦的,这意味着:无法分配请求的地址。

它来自应用程序可以打开的端口限制。默认情况下,在我的系统上,它是

cat /proc/sys/net/ipv4/ip_local_port_range : 32768 60999

这表示只有28,231个可用端口 – 60999减去32768 – 或者您可以从给定的IP地址建立或建立的TCP连接的限制。

您可以在客户端和服务器上使用更广泛的范围来扩展它:

echo 4000 65000 > /proc/sys/net/ipv4/ip_local_port_range

这将为我们提供61,000个连接,但这非常接近一个IP地址的限制(最大端口为65535)。从这里得到的关键结论是,如果我们想要更多的连接,我们需要为MySQL服务器分配更多的IP地址。为了实现100,000个连接,我将使用运行MySQL的服务器上的两个IP地址。

在整理出端口范围后,我们在sysbench上遇到了以下问题:

Shell

1
2
3
4
5
6
sysbench 0.5:  multi-threaded system evaluation benchmark Running the test with following options:Number of threads: 50000 FATAL: pthread_create() for thread #32352 failed. errno = 12 (Cannot allocate memory)

在这种情况下,这是系统台内存分配(即lua子系统)的问题。Sysbench 只能为 32,351 个连接分配内存。这个问题在systench 1.0.x中更为严重。

Sysbench 1.0.x 限制

Sysbench 1.0.x使用不同的Lua JIT,即使有4000个连接,它也会遇到内存问题,因此在Sysbench1.0.x中不可能超过4000个连接

因此,似乎我们使用sysbench比使用Percona Server更快达到极限。为了使用更多的连接,我们需要使用多个系统服务器客户端,如果 32,351 个连接是系统工作站的限制,我们必须使用至少四个系统服务器客户端才能获得多达 100,000 个连接。

对于 50,000 个连接,我将使用 2 台服务器(每台服务器运行单独的系统工作站),每台服务器从系统服务器运行 25,000 个线程。

每个systench的结果如下所示:

Shell

1
2
[  29s] threads: 25000, tps: 0.00, reads: 16794.09, writes: 0.00, response time: 1799.63ms (95%), errors: 0.00, reconnects:  0.00[  30s] threads: 25000, tps: 0.00, reads: 16491.03, writes: 0.00, response time: 1800.70ms (95%), errors: 0.00, reconnects:  0.00

因此,我们的吞吐量大致相同(总共 16794*2 = 33588 tps),但 95% 的响应时间翻了一番。这是可以预料的,因为与 25,000 个连接基准相比,我们使用的连接数是其两倍。

步骤 4. 75,000 个连接

为了实现 75,000 个连接,我们将使用三台带有 sysbench 的服务器,每台服务器运行 25,000 个线程。

每个sysbench的结果:

Shell

1
2
[ 157s] threads: 25000, tps: 0.00, reads: 11633.87, writes: 0.00, response time: 2651.76ms (95%), errors: 0.00, reconnects:  0.00[ 158s] threads: 25000, tps: 0.00, reads: 10783.09, writes: 0.00, response time: 2601.44ms (95%), errors: 0.00, reconnects:  0.00

步骤 5. 100,000 个连接

实现75k和100k连接没有什么大事可说的。我们只需启动一个额外的服务器并启动systench。对于 100,000 个连接,我们需要四台服务器用于系统台,每个服务器显示:

Shell

1
2
[ 101s] threads: 25000, tps: 0.00, reads: 8033.83, writes: 0.00, response time: 3320.21ms (95%), errors: 0.00, reconnects:  0.00[ 102s] threads: 25000, tps: 0.00, reads: 8065.02, writes: 0.00, response time: 3405.77ms (95%), errors: 0.00, reconnects:  0.00

因此,我们拥有相同的吞吐量(总共8065 * 4 = 32260 tps),3405ms 95%响应时间。

从中得出一个非常重要的结论:使用 100k 连接并使用线程池,95% 的响应时间甚至比没有线程池的 10k 连接还要好。线程池允许 Percona Server 更有效地管理资源,并提供更好的响应时间。

结论

对于MySQL来说,100k连接是可以实现的,我相信我们可以走得更远。有三个组件可以实现此目的:

  1. Percona Server 中的线程池
  2. 正确调整网络限制
  3. 在服务器框中使用多个 IP 地址(每一个 IP地址约 60k 个连接)

附录:完整的 my.cnf

Shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
[mysqld]
datadir {{ mysqldir }}
ssl=0 
skip-log-bin
log-error=error.log 
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0
character_set_server=latin1
collation_server=latin1_swedish_ci
skip-character-set-client-handshake 
innodb_undo_log_truncate=off
 # generaltable_open_cache = 200000
table_open_cache_instances=64
back_log=3500
max_connections=110000 
# filesinnodb_file_per_table
innodb_log_file_size=15G
innodb_log_files_in_group=2
innodb_open_files=4000 
# buffersinnodb_buffer_pool_size= 40G
innodb_buffer_pool_instances=8
innodb_log_buffer_size=64M 
# tuneinnodb_doublewrite= 1
innodb_thread_concurrency=0
innodb_flush_log_at_trx_commit= 0
innodb_flush_method=O_DIRECT_NO_FSYNC
innodb_max_dirty_pages_pct=90
innodb_max_dirty_pages_pct_lwm=10
innodb_lru_scan_depth=2048
innodb_page_cleaners=4
join_buffer_size=256K
sort_buffer_size=256K
innodb_use_native_aio=1
innodb_stats_persistent = 1 
#innodb_spin_wait_delay=96
innodb_adaptive_flushing = 1
innodb_flush_neighbors = 0
innodb_read_io_threads = 16
innodb_write_io_threads = 16
innodb_io_capacity=1500
innodb_io_capacity_max=2500
innodb_purge_threads=4
innodb_adaptive_hash_index=0
max_prepared_stmt_count=1000000
innodb_monitor_enable = ‘%’
performance_schema = ON