原文地址:https://www.tony-yin.site/201...

pgpool

本文分享一次数据库连接调优的经历,并对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)还显示有很多没有关闭的连接呢?

那无非两种场景:

  1. 程序没有走Response
  2. 程序走了Response,但是没能关闭掉连接;

场景1

  • 正常的restful接口最终是返回Response,但是中间代码出错或者抛异常会导致最后没能运行到Response
  • 项目中有很多程序并不是对外暴露的接口,比如后端的一些定时任务,这类程序不会涉及到请求的开始和结束,如果这些任务中涉及DB的操作,Django是不会帮助程序关闭连接的。

场景2

再谈正常走Response的程序,什么原因会导致连接没能被正常关闭?

通过上面Django数据库连接处理流程分析,可以知道Django里的数据库连接是放在线程的local() 实例中,当本线程需要一个数据库连接,判断local中是否存在已有连接,有则复用;但是如果采用多线程的方式访问数据库,Django则会创建一条属于当前线程的数据库连接,即每个线程都会创建属于自己的数据库连接,每个线程也只能关闭当前线程的数据库连接。

所以如果一段程序中,采用了多线程的方式访问DB,最后通过主线程返回Response,主线程是不会关闭其他线程中的数据库连接。

解决方案

客户端

  1. 针对代码异常没有运行到Response的情况,需要添加try-catch,在最终的finally加上返回Response的代码;
  2. 针对非web接口,即最后不走Response的情况,需要在程序最后额外添加关闭数据库连接的代码;
  3. 针对多线程使用数据库的场景,解决方案就是除了主线程的每次工作线程完成一个任务后,就把它相关的数据库连接关掉。
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_timeclient_idle_limit参数,0表示永远不会退出子进程。

connection_life_time

该参数表示pgpool与数据库后端建立连接的最大时间,主要用于缓存,提高数据库性能。0表示永远不会关闭连接。

总结

Django在大部分情况下会帮助关闭数据库连接,但部分场景,如无Response、程序抛异常、多线程等场景会导致数据库连接无法自动关闭。客户端需要在以上场景下采取对应的解决方案,防止数据库连接堆积。此外,数据库端也需要配置客户端空闲连接最大时间,保证在客户端不关闭连接的情况下能够关闭长时间空闲的数据库连接。

Refer

  1. 通过CONN_MAX_AGE优化Django的数据库连接
  2. 多线程Django程序耗尽数据库连接的问题
  3. django数据库连接
  4. A good PgPool II configuration
  5. pgpool配置文件详解
  6. 关于PgPool客户端阻塞

Tony_Zby
7.1k 声望154 粉丝

世界太大,没事瞄一瞄