原文地址:https://www.tony-yin.site/201...
本文分享一次数据库连接调优的经历,并对pgpool
一系列生命周期配置参数进行解读。
背景
测试发现数据库读写有时候会短时间卡顿,经过定位,发现数据库VIP
未发生漂移,服务端口正常,数据库(pgpool
)连接数已到达上限,所以判断是数据库连接数过多导致之后的短时间访问阻塞。
Django数据库连接处理
由于建立数据库连接最多的客户端用的是Django
框架,所以先分析一下Django
对数据库连接的处理流程。
Django
程序接受到请求之后,在第一次访问数据库的时候会创建一个连接,在请求结束时,关闭连接。Django1.6
后的版本,提供了数据库持久连接的配置,通过DATABASE
配置上添加CONN_MAX_AGE
来控制每个连接的最大存活时间。参数的原理就是在每次创建完数据库连接之后,把连接放到一个Theard.local
的实例中。在request
请求结束时会判断是否超过CONN_MAX_AGE
设置这个有效期。每次进行数据库请求的时候其实只是判断local
中有没有已存在的连接,有则复用。
class BaseDatabaseWrapper(object):
def connect(self):
"""Connects to the database. Assumes that the connection is closed.""" # Reset parameters defining when to close the connection
max_age = self.settings_dict['CONN_MAX_AGE']
self.close_at = None if max_age is None else time.time() + max_age
self.connection = self.get_new_connection(conn_params)
......
......
self.run_on_commit = []
def close_if_unusable_or_obsolete(self):
"""
Closes the current connection if unrecoverable errors have occurred,
or if it outlived its maximum age.
"""
if self.connection is not None:
# If the application didn't restore the original autocommit setting,
# don't take chances, drop the connection.
if self.get_autocommit() != self.settings_dict['AUTOCOMMIT']:
self.close()
return
# If an exception other than DataError or IntegrityError occurred
# since the last commit / rollback, check if the connection works.
if self.errors_occurred:
if self.is_usable():
self.errors_occurred = False
else:
self.close()
return
if self.close_at is not None and time.time() >= self.close_at:
self.close()
return
Django
是通过HttpResponse
来作为请求结束的标准,关闭连接的代码也是在HttpResponseBase
这个class
中。
class HttpResponseBase(six.Iterator):
def close(self):
for closable in self._closable_objects:
try:
closable.close()
except Exception:
pass
self.closed = True
# 请求结束时触发request_finished这个触发器
signals.request_finished.send(sender=self._handler_class)
定位
通过上面可以大致了解Django
对于数据库连接的处理流程,客户端中并没有设置长连接时间,所以理论上来说每个请求都会在结束时关闭数据库连接,那么为什么数据库端(pgpool
)还显示有很多没有关闭的连接呢?
那无非两种场景:
- 程序没有走
Response
; - 程序走了
Response
,但是没能关闭掉连接;
场景1
- 正常的
restful
接口最终是返回Response
,但是中间代码出错或者抛异常会导致最后没能运行到Response
。 - 项目中有很多程序并不是对外暴露的接口,比如后端的一些定时任务,这类程序不会涉及到请求的开始和结束,如果这些任务中涉及
DB
的操作,Django
是不会帮助程序关闭连接的。
场景2
再谈正常走Response
的程序,什么原因会导致连接没能被正常关闭?
通过上面Django
数据库连接处理流程分析,可以知道Django
里的数据库连接是放在线程的local()
实例中,当本线程需要一个数据库连接,判断local
中是否存在已有连接,有则复用;但是如果采用多线程的方式访问数据库,Django
则会创建一条属于当前线程的数据库连接,即每个线程都会创建属于自己的数据库连接,每个线程也只能关闭当前线程的数据库连接。
所以如果一段程序中,采用了多线程的方式访问DB
,最后通过主线程返回Response
,主线程是不会关闭其他线程中的数据库连接。
解决方案
客户端
- 针对代码异常没有运行到
Response
的情况,需要添加try-catch
,在最终的finally
加上返回Response
的代码; - 针对非
web
接口,即最后不走Response
的情况,需要在程序最后额外添加关闭数据库连接的代码; - 针对多线程使用数据库的场景,解决方案就是除了主线程的每次工作线程完成一个任务后,就把它相关的数据库连接关掉。
from django.db import connections
# 每一个线程都有专属的connections,把本线程名下的所有连接关闭。
connections.close_all()
数据库端
Pgpool
配置文件中配置客户端连接空闲最大时间为300
秒:
client_idle_limit = 300 # Client is disconnected after being idle for that many seconds
# (even inside an explicit transactions!)
# 0 means no disconnection
该参数表示当一个客户端在执行最后一条查询后如果空闲到了client_idle_limit
秒数, 到这个客户端的连接将被断开。这里配置为300
秒,防止客户端存在长时间的空闲连接占用连接数。
Pgpool 子进程状态解读
查看pgpool
连接数和状态:
[root@host1 ~]# systemctl status pgpool
● pgpool.service - Pgpool-II
Loaded: loaded (/usr/lib/systemd/system/pgpool.service; enabled; vendor preset: disabled)
Active: active (running) since Wed 2019-10-23 16:14:42 CST; 5 days ago
Main PID: 3195664 (pgpool)
Memory: 163.8M
CGroup: /system.slice/pgpool.service
├─ 2500232 pgpool: test_user test_db 192.168.1.1(39436) idle
├─ 2561154 pgpool: wait for connection request
├─ 794851 pgpool: test_user test_db 192.168.225.1(17831) idle in transaction
......
连接一般分为以下几种状态:
客户端连接处于空闲状态
客户端位于192.168.1.1
上,通过test_user
用户连接的数据库test_db
,并且该连接当前处于idle
状态。
2500232 pgpool: test_user test_db 192.168.1.1(39436) idle
客户端连接处于事务状态
客户端位于192.168.1.1
上,通过test_user
用户连接的数据库test_db
,并且该连接当前处于transaction
状态。
794851 pgpool: test_user test_db 192.168.1.1(17831) idle in transaction
等待连接请求
当客户端断开连接后,原子进程便会切换成wait
状态,等待下一次客户端连接,如果child_life_time
配置的时间范围内,没有客户端向这个子进程建立连接,该子进程便会kill
掉并重新生成新的子进程。
Pgpool 生命周期参数解读
child_life_time
该参数表示最后一次客户端断开连接后,子进程空闲持续的最大时间,避免长时间不退出导致内存泄露。客户端断开数据库连接后,子进程切换成wait request
状态,如果child_life_time
配置的时间范围内,没有客户端与该子进程建立连接,该子进程便会被kill
掉并重新生成新的子进程。0
表示永远不会退出子进程。
client_idle_limit
该参数表示最后一次客户在连接中的数据库操作后,该连接持续的最大时间。当一个客户端在执行最后一条查询后如果空闲到了client_idle_limit
秒数,这个客户端的连接将被pgpool
断开。该参数可以防止客户端长时间的空闲连接占用连接数,本文也是通过该参数进行数据库优化。0
表示永远不会断开连接。
child_max_connections
该参数表示pgpool
允许的最大连接数,当pgpool
建立的连接数到达child_max_connections
配置的数目后,则会优先退出最早的子进程。该参数主要用于并发量大的场景,短时间内触发不到child_life_time
和client_idle_limit
参数,0
表示永远不会退出子进程。
connection_life_time
该参数表示pgpool
与数据库后端建立连接的最大时间,主要用于缓存,提高数据库性能。0
表示永远不会关闭连接。
总结
Django
在大部分情况下会帮助关闭数据库连接,但部分场景,如无Response
、程序抛异常、多线程等场景会导致数据库连接无法自动关闭。客户端需要在以上场景下采取对应的解决方案,防止数据库连接堆积。此外,数据库端也需要配置客户端空闲连接最大时间,保证在客户端不关闭连接的情况下能够关闭长时间空闲的数据库连接。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。