如何理解高性能数据库连接池

50次阅读
没有评论

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

本篇文章为大家展示了如何理解高性能数据库连接池,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。

为什么要有连接池

先看一下连接池所处的位置:

应用框架的业务实现一般都会访问数据库,缓存或者 HTTP 服务。为什么要在访问的地方加上一个连接池呢?

下面以访问 MySQL 为例,执行一个 SQL 命令,如果不使用连接池,需要经过哪些流程。

1:TCP 建立连接的三次握手

2:MySQL 认证的三次握手

3:真正的 SQL 执行

4:MySQL 的关闭

5:TCP 的四次握手关闭

可以看到,为了执行一条 SQL,却多了非常多我们不关心的网络交互。

优点:实现简单。

缺点:

1:网络 IO 较多

2:数据库的负载较高

3:响应时间较长及 QPS 较低

4:应用频繁的创建连接和关闭连接,导致临时对象较多,GC 频繁

5:在关闭连接后,会出现大量 TIME_WAIT 的 TCP 状态(在 2 个 MSL 之后关闭)

使用连接池流程

第一次访问的时候,需要建立连接。但是之后的访问,均会复用之前创建的连接。

优点:

1:较少了网络开销

2:系统的性能会有一个实质的提升

3:没了麻烦的 TIME_WAIT 状态

当然,现实往往是残酷的,当我们解决了一个问题的时候,同时伴随着另外一个问题的产生。

使用连接池面临的 *** 挑战:连接池的性能

连接数和线程数性能优化

分库 DB 部署结构:

假设有 128 个分库:32 个服务器,每个服务器有 4 个 schema。按照 128 个分库的设计,便会新建 128 个独立数据库连接池。

数据库连接池的模型

特点:

1:128 个连接池完全独立,不同的 schema 也对应不同的连接池

2:先通过拆库,读写等策略选择对应的连接池,再从连接池获取一个连接进行操作

3:操作完后,再将连接归还到对应的连接池中。

优点:

结构简单,分散竞争

面临的问题:

1:线程数过多

先看一下新建一个连接池,需要新建的线程数的个数。

连接池

线程数

描述

128 个分库需要的线程数

C3P0

4

3 个 helperThread (pollerThread),1 个定时任务 AdminTaskTimer(DeadlockDetector)

4*128=512

DBCP

1

负责心跳,最小连接数维持,*** 空闲时间和防连接泄露

1*128=128

Druid

2

一个异步创建连接。一个异步关闭连接。

2*128=256

可以看到随着分库的增加,不管选用哪个连接池,线程的个数均会线性增长。线程数过多将会导致内存占用较大:   默认 1 个线程会占用 1M 的空间,如果是 512 个线程,则会占用 1M*512=512M 上下文切换开销。

Tips:由于 stack 和 heap 申请为虚地址空间,但是一旦使用就不会释放。(线程也不一定会占用 1M 的空间)

2:连接数过多

数据库的连接资源比较重,并且随着连接的增加,数据库的性能会有明显的下降。DBA 一般会限制每个 DB 建立连接的个数,比如限制为 3K  。假设数据库单台限制 3K,32 台则容量为 3K*32=96K。如果应用 ***,最小连接数均为 10,则每个应用总计需要 128*10=1.28K 个连接。那么数据库理论上支持的应用个数为 96K/1.28K=  80 台

3:不能连接复用

同一个物理机下面不同的 schema 完全独立,连接不能复用

优化后的数据库连接池模型

特点:

1:只有一个连接池, 所有节点共享线程 (解决了线程数过多的问题)

2:每个物理机对应一个 host, host 里面维护多个 schema,schema 存放连接。

3:同一个 host 下面的不同 schema 可以进行连接复用(解决连接数过多的问题)

获取连接流程:

1:获取连接需要带上 ip,port 和 schema 信息:比如获取的是 host31 的 schema1

2:先到 host31 的 schema1 中获取空闲连接,但是 schema1 无空闲连接,便会从 schema2 中获取空闲连接。

3:从 schema2 中获取的连接执行 useschema1,该连接便切换到 schema1 上面。

4:执行对应的 SQL 操作,执行完成后,归还连接到 schema1 的池子里面。

优点:

1:连接复用:有效减少连接数。

2:提升性能:避免频繁的新建连接。新建连接的开销比较大,而使用 use schema 开销非常小

3:有效减少线程数。按现有方案大概只需要 4 个线程即可。而优化前需要 512 个线程

缺点:

1:管理较为复杂

2:不符合 JDBC 接口规范。DataSource 只有简单的 getConnection()接口,没有针对获取对应 schema 的连接的接口。需要继承 DataSouce,实现特定接口。

事务语句性能优化

优化前执行事务的模型

从连接池里面获取到连接,默认是自动提交。为了开启事务,需要执行 setautocommit=false   操作,然后再执行具体的 SQL,归还连接的时候,还需要将连接设置为自动提交(需要执行 set autocommit=true)  。可以看到开启事务,需要额外执行两条事务的语句。

优化后执行事务的模型

每个 schema 里面所有的连接会按照 autocommit 进行分组。分为自动提交(autocommit=true)   和非自动提交(autocommit=false)。获取连接时优先获取相同 autocommit 的分组里的连接,如果没有可用连接则从另外一个分组中获取连接,业务操作执行完后,再归还到对应的分组里面。该种机制避免了开启事务多执行的两条事务语句。

锁性能优化

连接池的通用功能:

连接池主要包含五部分:获取连接,归还连接,定时任务,维护组件及资源池

获取连接:

1:获取超时:如果超过规定时间未获取到连接,则会抛出异常

2:有效性检查:当从资源池里面获取到资源,需要检查该资源的有效性,如果失效,再次获取连接。避免执行业务的时候报错。

3:创建连接:可以同步创建,也可以异步创建。

归还连接:

1:归还连接:比如需要检查 *** 空闲数,确定是物理关闭还是归还到连接池

2:销毁连接: 可同步销毁也可异步销毁

定时任务:

1:空闲检查:主要是检查空闲连接,连接空闲超过一定时间,则会关闭连接。

2:最小连接数控制:一般会设置最小连接数。保证当前系统里面最小的连接数。如果不够,则会新建连接。

组件维护:

1:连接状态控制:空闲,使用,删除等状态控制

2:异常处理:对 JDBC 访问的异常统一处理,如果异常与连接相关,则会将该连接销毁掉。

3:缓存:避免对 SQL 重复解析,PrepareStatement 机制下,会对 SQL 解析的对象进行缓存。

4:JDBC 封装:对 JDBC 进行了实现,真正的实现是底层的 driver, 比如 MySQL-connector-java。

资源池:

1:资源池是存放连接的地方,也是连接池最核心的地方。

2:所有的组件基本上都与资源池进行交互,对连接资源的竞争非常激烈。该处的性能将决定了整个连接池的性能。

3:一般资源池的实现是使用 JDK 提供的 BlockingQueue。那么是否有方案可以进行无锁的设计,来避免竞争。

资源池无锁设计

获取连接大概流程:

1:从 ThreadLocal 里面获取连接,如果没有空闲连接,则从全局连接池 (CopyOnWriteArrayList) 中获取。

2:如果全局连接池中没有空闲连接,则会异步新建连接。

3:判定超时时间是否大于阈值,如果小于阈值,则进行自旋。否则进行 park 休眠。

4:连接建立成功后,会对 park 的线程进行唤醒

主要从四个方面实现了无锁的设计:ThreadLocal,CopyOnWriteArrayList,异步建立连接及自旋。

ThreadLocal

1:每个线程均有一个连接队列。该队列是全局队列的引用。

2:获取连接时先从 ThreadLocal 里面拿连接,如果连接是空闲状态,则使用。否则移除掉,再拿下一个,直到拿不到连接为止。

3:归还连接时,只需要归还到 Threadlocal 的队列里面,同时设置连接为空闲状态

4:如果使用 BlockQueue, 获取连接时调用 poll, 归还连接时调用 offer,存在两次锁的竞争。优化后通过 CAS 避免了两次锁的开销(获取连接时,使用 CAS 置连接为非空闲状态; 归还时,使用 CAS 置连接为空闲状态)

CopyOnWriteArrayList

1:该队列使用场景是:大量读,少量写的操作,并且存储的数据比较有限。而连接池的场景非常适合采用 CopyOnWriteArrayList。

2:在获取连接或者归还连接时,只会通过 CAS 更改连接的状态,不会对连接池进行添加或者删除的操作。

3:一般连接池连接的个数比较可控,CopyOnWriteArrayList 在写操作时会对所有连接进行拷贝,对内存影响不大。

异步建立连接

获取到连接后,判断一下是否有并发正在等待获取连接,如果有,则异步建立连接。避免下一个连接的等待。如果 CopyOnWriteArrayList 没有空闲连接,则异步建立连接。

自旋

该自旋比较类似于 JDK 对 synchronized 的自旋机制。如果发现超时时间大于设定的阈值(比如 10 微秒),则会进行线程挂起。如果小于设定的阈值,则重新获取连接,进行自选,避免线程的上下文切换带来的性能开销。。

优化小技巧

方法内联优化

1:每调用一次方法,线程便会新建一个栈帧,新建栈帧开销相对比较大

2:JIT 在运行时会进行内联优化,多个方法使用一个栈帧,避免栈帧新建过多

3:JIT 方法内联优化默认的字节码个数阈值是 35 个字节,低于 35 个字节,才会进行优化。(可通过 -XX:MaxInlineSize=35 进行设置)

如何理解高性能数据库连接池

通过修改上述代码,编译后字节码修改到 34 个字节,则可以满足内联的条件。

心跳语句选择如何理解高性能数据库连接池PrepareStatement 模式选择如何理解高性能数据库连接池

MySQL driver 默认是 client 模式,如果需要开启 server 模式,需要设置 useServerPrepStmts=true  。PrepareStatement 默认的 client 模式和 Statement 对于 DB 端没有区别。大家普遍理解 PrepareStatement 和 Statement 的区别是 PrepareStatement 可以避免 SQL 注入。但是避免 SQL 注入是如何做到的?

使用 PrepareStatement 设置参数的时候,比如调用 setString(int parameterIndex, String  x),本地会对设置的参数进行转义来避免 SQL 注入。

执行 SQL 的时候,会将 SQL 的? 替换成转义后的字符,发送到数据库执行。

PSCache如何理解高性能数据库连接池

MySQLdriver 默认不开启,可通过设置 cachePrepStmts = true 进行开启

QueryTimeout如何理解高性能数据库连接池

之前也遇到因为开启了 queryTimeout,导致连接泄露的问题。

唯品会自研连接池:Caelus

Caelus 是唯品会自研的高性能的分布式的数据库连接池。

高性能:基于无锁的连接池设计模型来提升连接池性能;

在分库较多的场景下,减少线程数。  假如有 128 个分库,现有连接池模型下则需要使用 128 个独立的连接池,每个连接池都需要线程 (1- 4 个,不同的连接池不同) 处理任务。则总共需要维护 128 到 128* 4 个线程,开销巨大。而 Caelus 连接池会大大减少线程数。

连接复用。对于 一个 MySQL   的 instance 上面有多个 schema 场景下。现有连接池不同的 schema 的连接不可复用。而 Caelus 可以复用不同 schema 的连接,提升性能。

过多的事务指令。如果是事务语句,则从连接池拿到连接后,需要先开启事务(setautocommit=false),归还时需要再设置(set  autocommit=true)。每使用一次连接,均需要额外执行两条事务指令。Caelus 能有效减少事务指令。

配置规范的统一。结合 MySQL 的设置,提供规范统一,合理的配置。 

上述内容就是如何理解高性能数据库连接池,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注丸趣 TV 行业资讯频道。

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-19发表,共计4799字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)