数据库连接池之c3p0-0.9.1.2,16年的古董,发生连接泄露怎么查(二)

数据库,连接池,c3p0,古董,发生,连接,泄露,怎么 · 浏览次数 : 131

小编点评

**代码分析** * `PooledConnectionPool`中定义了`checkoutPooledConnection()`方法,该方法用于获取连接。 * `checkoutPooledConnection()`方法获取连接后,检查是否检测超时、过期的异常。 * 如果检测异常,就会调用`destroyResource()`方法关闭连接。 * `destroyResource()`方法关闭连接时,打印借出者的堆栈。 * 在`removeResource()`方法中,如果检测异常,就会调用`destroyResource()`方法关闭连接。 **问题** * `removeResource()`方法在异常情况下关闭连接时,打印的堆栈很误导人。 * 因为打印的堆栈来自异常,导致本地是会正常关闭连接的。 * 因此,在代码中无法获得正常关闭连接的堆栈信息。 **建议** * 在异常处理中,打印异常信息的堆栈。 * 考虑使用日志记录异常信息的堆栈信息。 * 在异常处理中,确保正常关闭连接的关闭过程。 * 尽量使用其他方法获取连接和关闭连接。 * 仔细分析异常信息的堆栈,才能获得正常关闭连接的信息。

正文

背景

本篇是c3p0连接泄露问题的第二篇,在前面一篇里面,大体介绍了问题,问题就是,我们发现线上服务不响应的原因是拿不到连接。而为啥拿不到连接呢,因为空闲链表为空,那么为什么空闲链表为空呢?

这个我一开始的猜测就是,估计是某处代码从连接池里获取了连接,用完了没有归还,那么,怎么才能找到这些罪恶的代码呢?

结合简单的源码分析、文档、搜索引擎,发现了两个配置项。

<property name="unreturnedConnectionTimeout">50</property>
<property name="debugUnreturnedConnectionStackTraces">true</property>

配置项的官方解释

首先说下,官方文档去哪里看呢,由于这个版本07年发布的,而官网一般只有近期版本的文档,所以我也是费了一些周折才找到。

https://sourceforge.net/projects/c3p0/files/c3p0-bin/c3p0-0.9.1.2/

另外,这里也额外补充下源码的地址:源码有两种方式获得,一是通过mvn仓库,二是https://sourceforge.net/projects/c3p0/files/c3p0-src/c3p0-0.9.1.2/,两种方式获得的源码我对比过,是一致的。

在下载的zip包中,doc/index.html即是该版本的离线文档。

其中的Configuring to Debug and Workaround Broken Client Applications章节,讲述了这两个配置项的意思。喜欢看原文的看下图即可:

image-20230715135714677

我简单翻译下意思:

有时候,应用程序比较大意,它们从连接池中获取的连接可能未调用close进行关闭。最终,池子膨胀到maxSize,然后因为这些大意的应用程序耗尽连接池。

解决这个问题的正确方式是修复程序。c3p0能帮你debug,让你知道是哪里发生了借出连接不归还的情况。也有比较少见的情况下,程序的开发周期已经结束,即使你明知其有bug也无法修复。在这种情况下,c3p0能帮你绕过这个问题,阻止其耗尽连接池。

unreturnedConnectionTimeout 定义了一个连接被借出后可以多久不归还的时间(单位秒),如果设置为非0的值,未归还的被借出的连接,超过这个时间后会被销毁,然后在连接池中重新生成新的连接。显然,你必须设置这个参数在一个合理的值,以确保程序在拿到连接后有时间能去完成自己的所有潜在操作(增删改查)。你能使用这个参数绕过那些有问题的借了连接不还的程序代码。

比绕过问题更好的办法是修复代码。除了设置上述参数外,还需要设置debugUnreturnedConnectionStackTraces为true,那么,在连接被借出时,会生成一个借用线程的线程堆栈快照。等到这类未归还的连接超过unreturnedConnectionTimeout 的时候,就可以打印出对应的线程堆栈,从而揭示出问题所在。这个参数主要用于找出异常代码,修复后可以关掉,因为借出连接时生成线程堆栈也是一个较为耗时的行为。

配置项在源码中如何初始化

1、datasource的初始化

首先是配置,我们这边是框架从一个xml文件读配置:

<datasource id="test" desc=""> 
    <property name="driver-name">oracle.jdbc.driver.OracleDriver</property>  
    <property name="url">jdbc:oracle:thin:@1.1.1.1:1521:orcl</property>
    <property name="user">111</property>
    <property name="password">111</property>  
    <property name="initialPoolSize">1</property>  
    <property name="minPoolSize">1</property>  
    <property name="maxPoolSize">50</property>  
    <property name="maxIdleTime">0</property>
    <property name="checkoutTimeout">10000</property>  
    <property name="maxStatements">0</property>  
    <property name="idleConnectionTestPeriod">0</property>
    <property name="acquireRetryAttempts">30</property>  
    <property name="acquireIncrement">2</property>  
    <property name="unreturnedConnectionTimeout">50</property>
    <property name="debugUnreturnedConnectionStackTraces">true</property>
  </datasource>

初始化代码如下:

image-20230715145814317

核心的代码是上图红框,一方面接收一个未池化的只封装了url、用户名密码的datasource,一方面接收配置项map。

进入函数内部后,会生成一个WrapperConnectionPoolDataSource对象,内部会把用户的配置项map设置进去,这也包含了前面我们要设置进去的unreturnedConnectionTimeout 和 debugUnreturnedConnectionStackTraces。

WrapperConnectionPoolDataSource wcpds = new WrapperConnectionPoolDataSource(configName);
// 设置原始的未池化datasource
wcpds.setNestedDataSource( unpooledDataSource );
// 设置用户配置项
BeansUtils.overwriteAccessiblePropertiesFromMap( overrideProps);

接下来呢,返回给用户的datasource的实际类型为PoolBackedDataSource

PoolBackedDataSource nascent_pbds = new PoolBackedDataSource(configName);
nascent_pbds.setConnectionPoolDataSource( wcpds );

其实呢,这个PoolBackedDataSource,有个关键字段poolManager还没初始化,也就是说,连接池此时还没生成(懒加载),看图:

image-20230715150543023

什么时候生成连接池呢?要等到第一次调用的时候,如获取连接。所以,我们框架是启动时,先去调用一次,确保连接池初始化。

private static void connectToDB(DataSource dataSource)
{
    // datasource:  com.mchange.v2.c3p0.PoolBackedDataSource
    Connection conn =  dataSource.getConnection();
}

此时,就会进入如下方法:

com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource#getConnection()    
public Connection getConnection() throws SQLException
{
    PooledConnection pc = getPoolManager().getPool().checkoutPooledConnection();
    return pc.getConnection();
}

首先进入getPoolManager:

private synchronized C3P0PooledConnectionPoolManager getPoolManager() throws SQLException
{
    if (poolManager == null)
    {
        ConnectionPoolDataSource cpds = assertCpds();
        poolManager = new C3P0PooledConnectionPoolManager(cpds, null, null, this.getNumHelperThreads(), this.getIdentityToken());
    }
    return poolManager;	    
}

可以看到,是个懒加载方法,首次进入时才开始new:

2、poolManager的初始化

com.mchange.v2.c3p0.impl.C3P0PooledConnectionPoolManager#poolsInit    

首先是生成一个定时任务执行线程:

this.timer = new Timer( true );

再下来,是生成一个线程池,当然,我们知道,线程池的线程,如果执行一些阻塞还不带超时的方法(如网络请求,而对方完全不返回或者很久才返回的时候),就会导致线程完全hang死,所以,这个线程池支持设置一个时间阈值,超过这个阈值,就会把这个线程给中断。

至于要不要生成这么一个带中断功能的线程池,是根据配置项:maxAdministrativeTaskTime。如果值大于0,就开启该功能。

int matt = this.getMaxAdministrativeTaskTime( null );
if ( matt > 0 )
{
    int matt_ms = matt * 1000;
    this.taskRunner = new ThreadPoolAsynchronousRunner( num_task_threads, 
                                                       true,
                                                       matt_ms,    
                                                       matt_ms * 3, 
                                                       matt_ms * 6, 
                                                       timer );
}

生成的线程池赋值给taskRunner字段。

如果maxAdministrativeTaskTime为0,则调用如下方法生成:

this.taskRunner = new ThreadPoolAsynchronousRunner( num_task_threads, true, timer );

线程池生成后,还要生成一个工厂,一个用来创建资源池(即连接池)的工厂:

ResourcePoolFactory          rpfact;

this.rpfact = BasicResourcePoolFactory.createNoEventSupportInstance( taskRunner, timer );

3、线程池的生成

// 设置线程池大小,它这个线程池是不能扩缩容的,是固定大小
this.num_threads = num_threads;
// 后台运行
this.daemon = daemon;
// maxAdministrativeTaskTime的值,一个任务最多运行多久
this.max_individual_task_time = max_individual_task_time;
// 死锁检测相关配置
this.deadlock_detector_interval = deadlock_detector_interval;
this.interrupt_delay_after_apparent_deadlock = interrupt_delay_after_apparent_deadlock;
this.myTimer = myTimer;
this.should_cancel_timer = should_cancel_timer;
// 根据配置创建线程
recreateThreadsAndTasks();
// 周期运行死锁检测任务
myTimer.schedule( deadlockDetector, deadlock_detector_interval, deadlock_detector_interval );

从上我们看到,这里面有个timer,而且给这个timer传递了一个周期调度的job,这个job呢,是进行死锁检测的,为啥叫这个名字,我估计,作者也他么知道自己用了太多多线程的东西了,太多syn关键字,容易导致出问题,所以搞个死锁检测的job,看看是不是有问题,有问题的话,就会打日志、重建线程池等。

而创建线程的部分如下:

private void recreateThreadsAndTasks()
{
    this.managed = new HashSet();
    this.available = new HashSet();
    this.pendingTasks = new LinkedList();
    for (int i = 0; i < num_threads; ++i)
    {
        Thread t = new PoolThread(i, daemon);
        managed.add( t );
        available.add( t );
        t.start();
    }
}

这里就是并没有线程池,有的只是n个线程,线程继承了jdk自带的线程:

class PoolThread extends Thread

运行逻辑如下:

HashSet    managed; // 所有的线程
HashSet    available; // 空闲线程
LinkedList pendingTasks; // 待执行的任务链表

while (true)
{
	Runnable myTask;
	synchronized ( ThreadPoolAsynchronousRunner.this )
	{	
        // 1 没有任务,睡眠
		while ( pendingTasks.size() == 0 )
			ThreadPoolAsynchronousRunner.this.wait( POLL_FOR_STOP_INTERVAL );
		// 2 有任务,准备执行,则先把自己从空闲线程中摘掉	
		if (! available.remove( this ) )
			throw new InternalError("An unavailable PoolThread tried to check itself out!!!");
        // 3 获取要执行的任务,队头获取
		myTask = (Runnable) pendingTasks.remove(0);
		currentTask = myTask;
	}
	// 4 检查是否设置了task的最长运行时间,设置了的话,要给timer调度一个n秒后执行的task,task届时会打断我们
	if (max_individual_task_time > 0)
		setMaxIndividualTaskTimeEnforcer();
    // 5 执行任务
	myTask.run(); 

	finally
	{   // 6 走到这,说明执行完成了,取消给timer调度的task
		if ( maxIndividualTaskTimeEnforcer != null )
			cancelMaxIndividualTaskTimeEnforcer();

		synchronized ( ThreadPoolAsynchronousRunner.this )
		{	// 7 把自己放回空闲线程列表
            available.add( this )
			currentTask = null;
		}
	}
}

4、连接池factory的创建

首先是连接池工厂的创建,工厂的创建并不复杂,工厂在构造器中,只是接收了从外部传递进来的timer和线程池:

BasicResourcePoolFactory( AsynchronousRunner taskRunner, 
                         RunnableQueue asyncEventQueue,  
                         Timer timer,
                         int default_num_task_threads)
{  
    this.taskRunner = taskRunner;
    this.timer = timer;
    this.default_num_task_threads = default_num_task_threads;
}

5、poolManager创建完成,创建pool

public Connection getConnection() throws SQLException
{
    PooledConnection pc = getPoolManager().getPool().checkoutPooledConnection();
    return pc.getConnection();
}

接下来即进入getPool方法:

	com.mchange.v2.c3p0.impl.C3P0PooledConnectionPoolManager

    private C3P0PooledConnectionPool createPooledConnectionPool(DbAuth auth) throws SQLException
    {    
        C3P0PooledConnectionPool out =  new C3P0PooledConnectionPool( cpds,
                        auth,
                        this.getMinPoolSize( userName ),
                        this.getMaxPoolSize( userName ),
                        this.getInitialPoolSize( userName ),
                        this.getAcquireIncrement( userName ),
                        this.getAcquireRetryAttempts( userName ),
                        this.getAcquireRetryDelay( userName ),
                        this.getBreakAfterAcquireFailure( userName ),
                        this.getCheckoutTimeout( userName ),
                        this.getIdleConnectionTestPeriod( userName ),
                        this.getMaxIdleTime( userName ),
                        this.getMaxIdleTimeExcessConnections( userName ),
                        this.getMaxConnectionAge( userName ),
                        this.getPropertyCycle( userName ),
                        this.getUnreturnedConnectionTimeout( userName ),
                        this.getDebugUnreturnedConnectionStackTraces( userName ),
                        this.getTestConnectionOnCheckout( userName ),
                        this.getTestConnectionOnCheckin( userName ),
                        this.getMaxStatements( userName ),
                        this.getMaxStatementsPerConnection( userName ),
                        this.getConnectionTester( userName ),
                        this.getConnectionCustomizer( userName ),
                        realTestQuery,
                        rpfact,
                        taskRunner,
                        parentDataSourceIdentityToken );
	}

从上面可以看到,这里获取很多配置项,以我们关注的getUnreturnedConnectionTimeout为例:

private int getUnreturnedConnectionTimeout(String userName)
{
    return getInt("unreturnedConnectionTimeout", userName ); 
}

实际最终取值呢,就是从如下字段中取值,这个大家看前文就知道,里面存储了我们的用户配置项:

final ConnectionPoolDataSource cpds;

propName = "unreturnedConnectionTimeout";
Method m = (Method) propNamesToReadMethods.get( propName );
if (m != null)
{	
    // cpds中取值
    Object readProp = m.invoke( cpds, null );
    if (readProp != null)
        out = readProp.toString();
}

new C3P0PooledConnectionPool的内部,实现如下:

  • 如果设置了statement的缓存相关参数,则创建缓存:
this.scache = new DoubleMaxStatementCache( taskRunner, maxStatements, maxStatementsPerConnection );
  • 根据传入的参数,设置到ResourcePoolFactory fact,如我们关注的

    fact.setDestroyOverdueResourceTime( unreturnedConnectionTimeout * 1000 );
    fact.setDebugStoreCheckoutStackTrace( debugUnreturnedConnectionStackTraces );
    

    注意,我们传入的unreturnedConnectionTimeout,被换算成毫秒,赋值给:

    long destroy_overdue_resc_time;
    

    而debugUnreturnedConnectionStackTraces,变成了:

    boolean debug_store_checkout_stacktrace = false;
    

    完整的如下:

    fact.setMin( min );
    fact.setMax( max );
    fact.setStart( start );
    fact.setIncrement( inc );
    fact.setIdleResourceTestPeriod( idleConnectionTestPeriod * 1000);
    fact.setResourceMaxIdleTime( maxIdleTime * 1000 );
    fact.setExcessResourceMaxIdleTime( maxIdleTimeExcessConnections * 1000 );
    fact.setResourceMaxAge( maxConnectionAge * 1000 );
    fact.setExpirationEnforcementDelay( propertyCycle * 1000 );
    fact.setDestroyOverdueResourceTime( unreturnedConnectionTimeout * 1000 );
    fact.setDebugStoreCheckoutStackTrace( debugUnreturnedConnectionStackTraces );
    fact.setAcquisitionRetryAttempts( acq_retry_attempts );
    fact.setAcquisitionRetryDelay( acq_retry_delay );
    fact.setBreakOnAcquisitionFailure( break_after_acq_failure );
    
  • 创建资源池

    rp = fact.createPool( manager );
    
  • resourcePool创建

    首先是,把传入的配置保存下来:

    this.start                            = start;
    this.min                              = min;
    this.max                              = max;
    this.inc                              = inc;
    this.num_acq_attempts                 = num_acq_attempts;
    this.acq_attempt_delay                = acq_attempt_delay;
    this.check_idle_resources_delay       = check_idle_resources_delay;
    this.max_resource_age                 = max_resource_age;
    this.max_idle_time                    = max_idle_time;
    this.excess_max_idle_time             = excess_max_idle_time;
    // 我们关注的属性
    this.destroy_unreturned_resc_time     = destroy_unreturned_resc_time;
    this.debug_store_checkout_exceptions  = (debug_store_checkout_exceptions && destroy_unreturned_resc_time > 0
                                             
    this.break_on_acquisition_failure     = break_on_acquisition_failure;                                         
    

    其次,外部传入的timer、线程池的引用也存储下来:

    this.taskRunner                       = taskRunner;
    // 就是最前面那个死锁检测的timer,传进来换了名字
    this.cullAndIdleRefurbishTimer        = cullAndIdleRefurbishTimer;
    

    开始池子的初始化:

    // 计算初始的池子大小
    this.target_pool_size = Math.max(start, min);
    //start acquiring our initial resources
    ensureStartResources();
    
    private void ensureStartResources()
    { recheckResizePool(); }
    
    private void expandPool(int count)
    {
        for (int i = 0; i < count; ++i)
            taskRunner.postRunnable( new AcquireTask() );
    }
    

    注意,这里是异步去创建资源的,这里只负责生成创建任务。为啥c3p0代码难懂,就是动不动整个多线程,最终难以维护也是这个原因。线程池taskRunner,就是前文那个线程池

    资源创建完成后,开始给timer生成一个周期task,该task主要检测有没有连接过期了,或者空闲太长时间,如果找到这种资源,就进行人道毁灭。

    if (mustEnforceExpiration())
    {
        this.cullTask = new CullTask();
        // 给timer整个周期job
        cullAndIdleRefurbishTimer.schedule( cullTask, minExpirationTime(), this.expiration_enforcement_delay );
    }
    

    而注意上面的方法mustEnforceExpiration,我们关注的参数就会参与到这里:

        private boolean mustEnforceExpiration()
        {
            return
                    max_resource_age > 0 ||
                            max_idle_time > 0 ||
                            excess_max_idle_time > 0 ||
                			// 这里
                            destroy_unreturned_resc_time > 0;
        }
    

    如果你其他几个属性都没设置,只设置了 unreturnedConnectionTimeout,它就会进入该分支,否则大家都为0,是开启不了分支的, CullTask就不会被调度。

    至此,我们完成了pool的初始化。

配置项在连接借出时如何生效

再回到下面这个代码:

public Connection getConnection() throws SQLException
{
    PooledConnection pc = getPoolManager().getPool().checkoutPooledConnection();
    return pc.getConnection();
}

上面,我们完成了 getPoolManager().getPool()的源码分析,终于可以去获取连接了。

com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool#checkoutPooledConnection

public PooledConnection checkoutPooledConnection() throws SQLException
{
    // rp: final ResourcePool rp;
	return (PooledConnection) rp.checkoutResource( checkoutTimeout ); 
}

这里找pool获取连接,实际内部会去找rp借连接:

private synchronized Object prelimCheckoutResource( long timeout )
{
	// 检查空闲链表的size
    int available = unused.size();
    if (available == 0)
    {
        // 空闲链表为空,检测是否可以扩容
        int msz = managed.size();
        if (msz < max)
        {
            int desired_target = msz + acquireWaiters.size() + 1;
            if (desired_target >= target_pool_size)
            {
                desired_target = Math.max(desired_target, target_pool_size + inc);
                target_pool_size = Math.max( Math.min( max, desired_target ), min );

                _recheckResizePool();
            }
        }
		// 扩容是异步的,在这里等待
        awaitAvailable(timeout); //throws timeout exception
    }
	// 有连接,取队列头部的
    Object  resc = unused.get(0);
	// 取到的连接有问题,毁灭资源
    if ( shouldExpire( resc ) )
    {
        removeResource( resc );
        ensureMinResources();
        return prelimCheckoutResource( timeout );
    }
    else
    {	
        // 资源ok,正常返回
        unused.remove(0);
        return resc;
    }
}

如果借到的连接ok,则获取该连接对应的卡片,在卡片上记录借出时间、当前线程的堆栈:

PunchCard card = (PunchCard) managed.get( resc );
// 借出时间
card.checkout_time = System.currentTimeMillis();
// 如果开启了这个debugUnreturnedConnectionStackTraces选项,记录当前线程的堆栈
if (debug_store_checkout_exceptions)
    card.checkoutStackTraceException = new Exception("DEBUG ONLY: Overdue resource check-out stack trace.");

这里是new了一个异常,在异常上调用getStackTrace就能获取堆栈。

配置项在连接毁灭时如何生效

查找PunchCard的checkoutStackTraceException的usage,发现在毁灭连接的方法中会使用该字段:

com.mchange.v2.resourcepool.BasicResourcePool#removeResource(java.lang.Object, boolean)
    
private void removeResource(Object resc, boolean synchronous)
{
    PunchCard pc = (PunchCard) managed.remove(resc);

    if (pc != null)
    {
        // 资源的卡片中,借出时间大于0,表示资源当前是被借出的,正常我们是不会去destroy正常的连接,既然被destroy,说明这个连接是有问题的,就是那种没归还的连接,因为归还的话,checkout_time会置为-1
        if ( pc.checkout_time > 0 && !broken) 
        {
            logger.info("A checked-out resource is overdue, and will be destroyed: " + resc);		
            // 这里打印借出者的堆栈
            if (pc.checkoutStackTraceException != null)
            {
                logger.log( MLevel.INFO,
                           "Logging the stack trace by which the overdue resource was checked-out.",
                           pc.checkoutStackTraceException );
            }
        }
    }
	// 从空闲链表删除
    unused.remove(resc);
    // 调用底层方法关闭连接
    destroyResource(resc, synchronous);
    addToFormerResources( resc );
}

那么,destroyResource什么时候触发呢,说实话,实际很多:

image-20230715165801923

我们很多连接池都有这样的机制,借出连接后,可以检测,如select 1,select from dual等,如果检测失败,就可以destroy;归还时,也可以检测,失败就destroy。

也有一些定时任务,检测是否空闲太久、检测能不能正常使用,不能的话,就destroy。

总结

看起来,作者的这套机制也是没啥问题的,借出时打标机。然后靠timer定时调度的job,去检测这些连接,看看是不是超时、过期、不正常、空闲太久等,只要满足这些异常条件,就会destroy这个异常连接,destroy的时候,就打印借出者的堆栈,方便开发者修复bug。

可以这么说,如果只是单纯的代码问题,写的代码太粗心而导致连接未关闭,而不是什么别的问题,看起来这个机制是没啥问题的。

但是,我按照目前这个配置,弄到线上后,我以为可以解决我这边的问题了,但是,打印出来的堆栈却非常误导人,我在按照对应的堆栈发起测试,发现本地是会正常关闭连接的。

那么,我觉得,有可能我这边的问题,不是简单的连接泄露,但是我这边的这个场景,造成的结果却几乎和连接泄露一模一样。

所以,我担心我这边的情况是,会不会是我归还了,但是在执行c3p0的归还代码时,归还失败了呢?

具体情况,等到下一篇我再分析。

与数据库连接池之c3p0-0.9.1.2,16年的古董,发生连接泄露怎么查(二) 相似的内容:

数据库连接池之c3p0-0.9.1.2,16年的古董,发生连接泄露怎么查(一)

# 背景 这篇文章是写给有缘人的,为什么这么说呢,因为本篇主要讲讲数据库连接池之c3p0-0.9.1.2版本。 年轻的朋友,可能没怎么听过c3p0了,或者也仅限于听说,这都很正常,因为c3p0算是200几年时比较流行的技术,后来,作者消失了好几年,12年重新开始维护,这时候已经出现了很多第二代线程池

数据库连接池之c3p0-0.9.1.2,16年的古董,发生连接泄露怎么查(二)

# 背景 本篇是c3p0连接泄露问题的第二篇,在前面一篇里面,大体介绍了问题,问题就是,我们发现线上服务不响应的原因是拿不到连接。而为啥拿不到连接呢,因为空闲链表为空,那么为什么空闲链表为空呢? 这个我一开始的猜测就是,估计是某处代码从连接池里获取了连接,用完了没有归还,那么,怎么才能找到这些罪恶的

数据库连接池之c3p0-0.9.1.2,线上偶发APPARENT DEADLOCK,如何解?

# 前言 本篇其实是承接前面两篇的,都是讲定位线上的c3p0数据库连接池,发生连接泄露的问题。 第二篇讲到,可以配置两个参数,来找出是哪里的代码借了连接后没有归还。但是,在我这边的情况是,对于没有归还的连接,借用者的堆栈确实是打印到日志了,但是我在本地模拟的时候,发现其实这些场景是有归还连接的,所以

线上问题排查--进程重启失败,最后发现是忘了cd

# 背景 我前面写了几篇文章,讲c3p0数据库连接池发生了连接泄露,但是随机出现,难以确定根因,最终呢,为了快速解决问题,我是先写了个shell脚本,脚本主要是检测服务的接口访问日志,看看过去的30s内是不是接口几乎都超时了,如果是的话,咱们就重启服务。然后把这个shell加入到了crontab里,

iptables防火墙调试,想打印个日志就这么难

# 背景 怎么会讲这个话题,这个说来真的长了。但是,长话短说,也是可以的。 我前面的文章提到,线上的服务用了c3p0数据库连接池,会偶发连接泄露问题,而分析到最后,又怀疑是db侧主动关闭连接,或者是服务所在机器和db之间有防火墙,防火墙主动关闭了连接。导致我们这边socket看着还健康,实际在对端已

后端服务之应用预热

一 背景 C端服务应用升级和重启,导致耗时瞬时抖动,业务超时,应用监控报警,上游感知明显,导致用户体验变差。 二 应用升级重启导致抖动的原因 1 C端服务应用升级和重启的冷启动阶段,它需要重新加载和初始化各种资源,例如数据库连接、缓存数据等,导致耗时瞬时飙升。 2 应用重启后,本地缓存失效,应用需要

Python学习之六_同时访问Oracle和Mysql的方法

Python学习之六_同时访问Oracle和Mysql的方法 背景 jaydebeapi 可以访问大部分数据库. 但是他有一个问题是仅能够访问一种类型的数据库. 如果同事连接两种数据库,那么就会出现问题 会有如下的提示: TypeError: Class com.mysql.cj.jdbc.Driv

GaussDB技术解读系列之应用无损透明(ALT)

当数据库集群的某个节点由于故障无法对外提供服务,若此时集群内还存在其它可用节点,则将故障节点上的会话连接自动迁移到目标节点上,客户端无需再次发出连接请求,仍然可以继续执行数据库操作。

以小博大外小内大,Db数据库SQL优化之小数据驱动大数据

SQL优化中,有一条放之四海而皆准的既定方针,那就是:永远以小数据驱动大数据。其本质其实就是以小的数据样本作为驱动查询能够优化查询效率,在SQL中,涉及到不同表数据的连接、转移、或者合并,这些操作必须得有个数据集作为“带头”大哥,即驱动数据,而这个驱动数据最好是数据量最小的那一个。 内大外小 在讨论

网络协议之:redis protocol 详解

简介 redis是一个非常优秀的软件,它可以用作内存数据库或者缓存。因为他的优秀性能,redis被应用在很多场合中。 redis是一个客户端和服务器端的模式,客户端和服务器端是通过TCP协议进行连接的,客户端将请求数据发送到服务器端,服务器端将请求返回给客户端。这样一个请求流程就完成了。 当然在最开