上一讲 提到了 mysql 的线程模型,本节我们就来看看。
One-Thread-Per-Connection模型与 Pool-Threads模型
MySQL每个连接使用一个线程,另外还有内部处理线程、特殊用途的线程、以及所有存储引擎创建的线程。-- 《高性能MySQL》
站在客户端视角来看,也就是下面的 conn
对象就可以对应到 server 端的线程A:
// 从DriverManager处获取数据库连接
Connection conn = DriverManager.getConnection(
"jdbc:mysql://数据库ip/数据库名称",
"账号",
"密码" );
客户端对这个 conn 对象执行SQL语句时,server端的这个线程A就会处理SQL语句,也就是走 上一讲 提到的整体流程。下图中的 work 线程就是包含解析器、优化器等的那部分模块。
当我们在代码中执行SQL时,如前文的:
Statement st = conn.createStatement();
// 只查找一行数据
ResultSet rs = st.executeQuery("SELECT id, username, password FROM sys_user LIMIT 1");
这部分代码对应的 server 端的执行过程就是在 work 线程中执行的。由 work 线程利用 解析器、优化器等模块方法来执行。这种模型叫做:One-Thread-Per-Connection
。
那么,当连接数过多的时候,比如1万个连接,就要创建1万个 work 线程。又由于我们在客户端缓存了 conn 对象,也就是保持了这个长连接。那么在这种模型下(每个连接使用一个线程),server 端也需要保持1万个线程。
虽然每个 work 线程可以复用来处理同一个 conn 对象的多个 SQL 语句,但是维护这么多的线程意味着内存占用开销大,意味着CPU调度开销大,性能肯定上不来。
所以呢?所以 mysql server 的线程处理模型是不是应该换一换呢?
我们先说结论,不是的。原因如下:
- 数据库的长连接一般不会很多,属于常量连接;但是数据库的请求是非常多的,也就是一个长连接中,可能来回收发请求数达到成百上千。由于客户端的连接池将连接缓存并做了最大限制,实际应用中数据库长连接不是很多。
- 我们可以看下 mysql5.7 官方文档是怎么说的:
The default thread-handling model in MySQL Server executes statements using one thread per client connection
首先,官方说是默认的线程处理模型是对每个客户端连接都创建一个线程,这个和我们认知一致。但是呢,还有其他线程处理模型,这就是 Pool-Threads
处理模型,官方说明如下:
to limit the number of concurrently executing statements/queries and transactions to ensure that each has sufficient CPU and memory resources to fulfill its task
主要的目的是限流以确保有足够的资源来完成已经接受的任务。那么原先的 One-Thread-Per-Connection
缺陷为:
As more clients connect to the server and execute statements, overall performance degrades.
就是大量客户端连接到服务器并执行各种语句,会导致服务端的整体性能下降。那么使用了 Pool-Threads
模型后,会怎么样呢?
The Thread Pool plugin increases server performance by efficiently managing statement execution threads for large numbers of client connections, especially on modern multi-CPU/Core systems
通过有效的线程管理会提升服务器性能。但是我们想用也难,因为:
For MySQL 5.7, the Thread Pool plugin is included in MySQL Enterprise Edition, a commercial product.
企业版才配置这个功能,也就是才支持这个线程处理模型。
所以,结论就是 mysql 的线程模型是 One-Thread-Per-Connection
,即每个连接都创建一个新的处理线程。
实际上,完整的线程模型如下图:
因为我们是基于 TCP 长连接的,所以前级有一个 Dispatcher线程
(实际上叫什么都行),它就是来监听TCP的连接请求,然后创建一个 Work线程
并将这个连接绑定到上面。
接着,work线程执行读取请求信息操作,也就是 read 操作,这样子就收到了客户端发来的信息,比如查询请求。然后调用业务处理,这里的业务处理就是上一讲中的解析器等一堆业务操作。
最后,将查询到的数据返回给客户端。
并发处理
既然讲到了线程模型,就需要提及一下并发处理模型。此处不区分线程和进程,进行简单的解析,后续会深入。
一个线程对应一个连接
这就是 One-Thread-Per-Connection
模型,不再赘述。这种思路下,资源消耗巨大,能否换种思路,让资源消耗降低,从而多余出去的资源给到真正需要用的地方。也就是 一个线程同时对应多条连接
。
一个线程对应多条连接
这就是大名鼎鼎的 I/O多路复用技术
。可以大大降低线程的数量,从而解决一个线程对应一个连接的调度问题。
当连接建立后,我们需要对连接做什么:
- 监听连接上的请求信息
- 监听到了,就去读取信息内容,并执行后续处理。即 read --> 业务处理 --> send
那么当我们的线程持有比如1万个连接时,如何处理第一步呢?即如何监听哪个或者哪些长连接目前发来了请求信息呢?
方法1:轮询
线程A一次轮询每个长连接,看是否有请求发出,有请求则执行第二步。
很容易想到,当第二步阻塞时,即该连接的数据还没有发送读取完成,没法立即返回结果,则排在后面的长连接就算有信息到来也无法立刻被线程A读取。
方法2:select
线程A利用 select 系统调用向操作系统内核注册长连接的文件句柄,当某个长连接有数据发来并且数据可以读取时,线程A就可以再次检查是哪个长连接的文件句柄发生了变化,就可以去依次读取发生变化的数据信息。由于此时数据已经准备好了,所以 read 操作几乎瞬间就可以完成。
但是文件句柄数量有限而且还需要再次检查是哪个句柄发生了变化,浪费性能。
方法3:epoll
线程A利用 epoll 系统调用向系统内核同样注册文件句柄,但是操作系统只返回可读的文件句柄。这样,就避免了重复轮询大量没有准备好数据的文件句柄了。
不过,虽然是 epoll非阻塞,但是 read、send这些仍然是同步读取数据的,这里还是会慢。
方法4:异步IO
异步IO是将 read 和 send 操作异步化,实现数据从网卡拷贝到内存中也是非阻塞的。但是目前 linux 层面对于异步IO的支持不完善,而且真正的异步化不好实现,所以一般也不采用。
这些实际上是IO模型,也就是下图:
图中右侧的两个阶段,就对应上文的两个步骤。等待数据对应的是监听连接上的请求信息,将数据从内核拷贝到用户空间,就对应的是第二步 read 过程。
方法1 对应 非阻塞I/O,方法2 对应I/O复用,方法3 对应 信号驱动I/O,方法4 对应 异步I/O。
在第一个阶段等待数据处,图中的后四种都是非阻塞的,因为他们不会阻塞在同一个长连接上;然而只有最后一种是异步的,因为在第二个阶段拷贝数据时,异步I O将数据拷贝到用户空间后再通知程序数据可读,不像其他四个,都需要主动去读取数据,所以他们又叫做同步IO。
到这里,等待数据、拷贝数据并读取都理清了。那么业务处理怎么办?对于处理速度较快的业务,可以直接在同一个线程中进行操作,完了再处理其他准备好的连接。对于业务处理慢的,比如有I/O操作的业务,则可以另起线程池进行操作。
其实,I/O复用模型 + 线程池 的组合就是非常重要的 Reactor 并发模式,后续文章会继续涉及。
再看One-Thread-Per-Connection模型
对于每个连接创建一个“线程“,这个线程是否就是操作系统内核管理的线程呢?答案是否定的。这个”线程“实际上是 mysql thread
,其只是一个对象,需要和操作系统的真实线程 os thread
关联起来。
每个连接对应一个 mysql thread
,每个 mysql thread
对应一个 os thread
。当连接关闭时,mysql thread
也随之消失,但是 os thread
可以继续被其他的 mysql thread
继续使用。
下一个mysql文章中,我们会对关联查询的底层原理进行分析。在此之前,会先补充一个短篇,关于线程和操作系统的关系介绍。