一个JedisConnectionException的异常跟踪调查

图片来自pixabay.com的spencerlikestorun会员

1. 问题缘起

有一个web服务应用一直在稳定运行,直到最近一个月,陆续三次在晚上出现了JedisConnectionException的异常,服务集群几乎在同一时间段都报同一错误,无法正常运行,重启后一切正常。

redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
   at redis.clients.util.Pool.getResource(Pool.java:50)
   at redis.clients.jedis.JedisPool.getResource(JedisPool.java:86)
    …
Caused by: java.util.NoSuchElementException: Timeout waiting for idle object
    at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:449)

2. 现场处理和初步调查结论

  • JedisConnectionException异常所报告的问题是,无法正常获取连接,通过查看redis连接池配置(maxTotal=200)和当时的并发流量(并发数小于5),没有发现不对和特别的地方。
  • 通过netstat查看网络TCP连接,redis连接数也不多(小于20),正常连接中,在redis服务器端可以观察到有连接正在陆续正常地新建和关闭,网络没有问题。
  • 查看zabbix和pinpoint监控,主站应用的CPU使用率有升高,不过在正常范围内,但响应时间有飙高(长达6秒),预计是获取redis连接阻塞的原因(在redis客户端设置阻塞时间为6秒)。
  • 为了能够保证主站恢复使用,调查一段时间后,决定重启应用,然后检查所有服务,一切恢复正常使用。

3. 调查方向

根据现象,确定如下几个调查方向,

  1. 查看redis的访问代码实现,是否有redis连接泄漏?并尝试是否可以本地开发环境重现同样问题。
  2. 查看线上配置,是否配置不正确或者未加载的情况?
  3. 查看网络原因,是否由于因为防火墙导致单机连接有限制?
    • 查看iptables
    • 使用tcpdump查看TCP流量
  4. 查看主站应用的请求访问日志和运行日志,查看异常前后有无特别的请求访问?
    • 请求访问日志 localhost_access_log.2018-12-28
    • 运行日志 catalina.out-20181228

4. 定位问题

通过查看代码时,发现有段代码在获取redis连接后,没有在方法退出时执行close()方法,

public void leakRedisConn() {
  Jedis jedis = null;
    try {
        jedis = jedisPool.getResource();
        jedis.ping();
    } catch (Exception ignored) {
    } finally {
        // 没有归还redis连接到空闲资源池
        // if (jedis != null) jedis.close();
    }
}

查看服务集群的请求访问日志和运行日志,发现在JedisConnectionException异常出现前,都有对该方法的调用,印证此方法和异常的出现有强关联性,因此上述方法应该就是异常发生的罪魁祸首。

该方法在获取连接后,并没有执行close()方法,使得redis连接无法回收到空闲资源池,每一次调用,都会泄漏一个连接,直到整个连接池队列达到上限,从而出现异常。具体原因见对JedisPool的代码分析。

5. 异常出现的原因和JedisPool代码分析

通过分析JedisPool的实现代码,可以看到里面有两个内部对象allObjects和idleObjects,一个是保存着所有连接对象,一个是保存着所有空闲连接对象,

public class JedisPool extends Pool<Jedis> {
  protected GenericObjectPool<T> internalPool;
}

// JedisPool里通过GenericObjectPool维护着所创建的连接池对象,实现Jedis连接的创建、获取和回收,
class GenericObjectPool{
    private final Map<T, PooledObject<T>> allObjects; // 保存着所有连接对象,上限通过maxTotal控制
    private final LinkedBlockingDeque<PooledObject<T>> idleObjects; // 保存着所有空闲连接对象,上限通过maxIdle控制
}

每次创建时,新建的连接会放入allObjects列表中,在close()方法调用时,空闲的连接会存放入idleObjects,以待后续重复利用。

方法jedisPool.getResource()会先尝试从空闲队列中获取连接,若获取不到,则尝试新建一个连接。一旦获取到的连接没有执行close()方法,则无法被回收到空闲队列,于是每一次方法调用,allObjects连接池中会增加一个,一旦数量超过maxTotal,则不再允许创建,请见下面的代码,

class GenericObjectPool{

  public T borrowObject(long borrowMaxWaitMillis) throws Exception {
     ...
     boolean create;
     p = (PooledObject)this.idleObjects.pollFirst();
     if(p == null) {
         p = this.create();
         if(p != null) {
            create = true;
         }
     }
     ...
  }

  private PooledObject<T> create() throws Exception {
        int localMaxTotal = this.getMaxTotal();
        long newCreateCount = this.createCount.incrementAndGet();

        // 当创建的个数大于maxTotal时,则不再允许创建,直接返回null
        if((localMaxTotal <= -1 || newCreateCount <= (long)localMaxTotal) && newCreateCount <= 2147483647L) {
            PooledObject p;
            try {
                p = this.factory.makeObject();
            } catch (Exception var6) {
            }
            ...
            return p;
        } else {
            this.createCount.decrementAndGet();
            return null;
        }
  }

}

注意的是,泄漏的连接在redis服务器端在超时后会被关闭,但在客户端则一直占着位置不会被释放,从而报JedisConnectionException异常,直到重启。

5. 后记

对这个JedisConnectionException的异常泄漏问题解决后,后来想了想,还有两个事情可以总结下,以待未来改进。

5.1 保留问题第一现场

在发生问题时,可以通过如下jmap命令保存下Java内存快照,保留第一现场,这样能够为后续查看发生问题,分析JedisPool对象的内部数据提供方便。

jmap -dump:format=b,file=jmap.dump <pid>

下图是通过Eclipse的MemoryAnalyzer工具分析的JedisPool对象,

jedis连接池

若当时有上面的内存快照,可以看到minIdle/maxIdle的配置情况,也可以看到createCount的变量值,这样会更加容易发现redis泄漏问题。

5.2 一颗老鼠屎,坏了一锅粥

对于Jedis连接池这种全局有限资源,必须严控管理,规范使用,不然一个地方出现泄漏,就会导致整个应用崩溃,本案例是一个活生生的例子。

一个统计Git代码仓库签入代码行数的简单方法

本文介绍一个使用简单的脚本方法来统计各个开发者对一个git代码项目的提交代码行数,用于衡量各个开发者对于该项目的贡献量。

查看签入的提交代码行数

使用git命令行工具git log可以用于查看签入的各种信息,其中有个参数可以查看到每次签入的提交文件及其相关代码行数增减数目

git log --numstat --author="test"

一个输出的日志样例为,

Author: test <test@gmail.com>
Date: Tue Jan 5 01:20:49 2016 +0800

 add lombok feature.

10 1 pom.xml
5 16 src/main/java/com/peipeihh/spring/po/UserPO.java

在上述日志中,可以看到每次代码签入的改动文件,及其每个文件的代码改动量,其中第一个数字为代码增加量,第二个数字为代码减少量。有了这些日志,我们就可以使用awk工具统计下当前开发者总体增减行数量,脚本如下,

git log --numstat --author="test" | awk 'BEGIN{add=0;subs=0;loc=0} {if($1~/^[0-9]+/){add += $1; subs += $2; loc += $1 + $2 }} END {printf "%s\t%s\t%s\n", add, subs, loc }'

其中:变量add统计的为代码增减量,subs统计的为代码减少量,然后将add和subs相加得到代码改动行数loc,这样就可以获取到一个开发者在某个代码项目中改动的代码行数。

统计每人签入的代码行数

在统计每人签入的代码行数之前,还需要获取所有开发者列表,

git log --format='%aN' | sort -u

接下来要做的事情,就是逐一统计每个开发者的签入代码行数,一个简单的脚本如下,

git log --format='%aN' | sort -u | while read name; do echo -en "$name\t"; git log --numstat --author="$name" | awk 'BEGIN{add=0;subs=0;loc=0} {if($1~/^[0-9]+/){add += $1; subs += $2; loc += $1 + $2 }} END {printf "%s\t%s\t%s\n", add, subs, loc }'; done;

一个输出结果样例为,

test 32469 22 32491

优化建议

为了让统计更加准确和有意义,可以做如下改进,

过滤合并的代码

一般情况下需要对合并的代码量过滤掉,不计入统计,这可以通过加上--no-merges参数,

git log --numstat --author="test" --no-merges
过滤提交代码量大于10000行的提交

有时候提交的代码中不免含有引入第三方代码库的时候,这个时候可以在统计的时候过滤掉代码量过于大的代码提交(>10000),

git log --numstat --author="test" | awk 'BEGIN{add=0;subs=0;loc=0} {if(($1~/^[0-9]+/) && ($1<10000)){add += $1; subs += $2; loc += $1 + $2 }} END {printf "%s\t%s\t%s\n", add, subs, loc }';
按时间区段

如果需要统计某个时间区段,比如统计9月份的代码签入情况,可以加上--since/--until参数,

git log --numstat --author="test" --since=2016-9-01 --until=2016-9-30

后记

上面的统计方法可以简单的统计一个git代码仓库中每个开发者提交的代码行数,依次类推还可以统计文件数目、统计签入次数等等,还可以根据情况深入优化统计数据,这个就让读者自己根据上述资料来写个简单脚本了。

参考资料

Git log: https://git-scm.com/docs/git-log

java多线程编程简介

1. Java多线程的实现方式

Java中提供如下三种方式的多线程实现方式,

a) 继承Thread类:实现run()方法。
b) 继承Runnable接口:实现run()方法,比起Thread类更加灵活,可以实现多个线程共享数据。
c) 继承Callable接口:实现call()方法,比起Thread和Runnable,可以获取各个线程的返回值。

代码样例如下(具体的演示样例请参见multithread项目中的demo1),

 // BaseThread继承自Thread类
 BaseThread t1 = new BaseThread("Thread-1");
 BaseThread t2 = new BaseThread("Thread-2");
 t1.start();
 t2.start();
 
 // BaseRunable继承自Runnable接口,线程r1/r2共享run对象的数据
 BaseRunable run = new BaseRunable();
 Thread r1 = new Thread(run, "Thread-run-1");
 Thread r2 = new Thread(run, "Thread-run-2");
 r1.start();
 r2.start();
 
 // BaseCallback继承自Callable接口,在执行完毕后可以获取执行结果
 ExecutorService exec = Executors.newCachedThreadPool(); 
 Future<Integer> result = exec.submit(new BaseCallback(100));
 
 while(!result.isDone()){
 App.sleep(1);
 }
 
 try {
 App.logMessage("result: " + result.get());
 } catch (Exception e) {
 e.printStackTrace();
 }
 exec.shutdown();

2. Java多线程的同步Synchronized

为了控制多个线程对同一代码区的访问,Java语言提供了Synchronized关键字给对象、方法或者代码块进行加锁,加了锁的代码块在同一时间只能允许一个线程执行。

在如下的类中run()方法中加了Synchronized关键字,这将会使得第一个进入run()方法的线程打印完所有的count数字(从10到1),而后进入的其它线程将不会输出任何信息(因为count为0),注:假设所有线程使用同一个run对象初始化。

public class BaseRunableSync implements Runnable {

 private int count = 10;
 
 public synchronized void run() {
 for( ; count>0; count--) {
 App.logMessage("count=" + count);
 }
 }

}

具体的演示样例请参见multithread项目中的demo2

3. Java多线程的阻塞(Sleep/Wait)

在多线程编程中,很多时候不仅需要利用多线程充分使用资源,也要需要释放资源。Thread.Sleep和Object.wait就是在线程运行时主动释放CPU资源或者同步锁资源。

Thread.Sleep:当前线程进入阻塞模式,释放CPU资源,但是不释放同步锁。

Object.Wait:当前线程进入阻塞模式,释放CPU资源并释放同步锁。注意Object.wait必须和Synchronized同步配合使用,否则因为没有同步锁释放而导致抛异常。

具体的演示样例请参见multithread项目中的demo3,分别有如下的演示,

a)  runnable + synchronized :同步区没有任何阻塞
b) runnable + synchronized + sleep:同步区加sleep,线程被阻塞到等待池
c)  runnable + synchronized + wait:同步区加wait,线程被阻塞到等待池

4. Java多线程的合并(join)

Java程序起来后,有一个main的主线程,其它线程都是从该主线程诞生出子线程,如果子线程运行时间比较长,很有可能的情况是,主线程已经结束但子线程还在运行。

为了能够让主线程能够等待子线程的结束,可以使用thread.join()方法,使得当前线程阻塞到等待池,一直等待子线程执行完毕,

public class App {

 public static void main(String[] args) throws InterruptedException {
 
 App.logMessage("start...");
 
 BaseRunable run = new BaseRunable();
 Thread r1 = new Thread(run);
 Thread r2 = new Thread(run);
 r1.start();
 r2.start();
 
 // add the join method, which wait for r1/r2 to be finished
 r1.join();
 r2.join();
 
 App.logMessage("the end.");
 
 }
 
}

上述代码中,如果没有r1.join()和r2.join(),主线程很有可能在r1和r2执行完之前就已经结束。

具体代码样例请参见multithread项目中的demo4

5. Java多线程的死锁

多线程中,如果执行代码有多个资源锁,如果执行顺序不当,就会形成死锁。比如两个子线程,互相握有对方需要的对象锁并不释放,则会形成死锁。

public class DeadlockRunable implements Runnable {

 private Object lock1 = new Object();
 private Object lock2 = new Object();
 private Boolean bLockFlag = Boolean.FALSE;
 
 public void run() {
   bLockFlag = !bLockFlag;
   if(bLockFlag){

     synchronized(lock1){
       App.logMessage("lock1 is locked...");
       App.sleep(2);
       synchronized(lock2){
         App.logMessage("lock2 is locked...");
       }
     }
 
   }
   else {
 
       synchronized(lock2){
         App.logMessage("lock2 is locked...");
         App.sleep(2);
         synchronized(lock1){
          App.logMessage("lock1 is locked...");
         }
       }
 
   }
 }

}

一个死锁代码样例请参见multithread项目中的demo5

6. Java多线程的状态

Java多线程有如下几种状态,

新建(NEW)
就绪/可运行/在运行(RUNNABLE)
阻塞(BLOCKED)
等待(WAITING/TIMED_WAITING)
结束(TERMINATED)

线程在各个状态的流转图见如下,

thread_status

代码样例

代码仓库地址:http://git.oschina.net/pphh/tools,可以通过如下git clone命令获取仓库代码,

git clone git@git.oschina.net:pphh/tools.git

上述代码样例在文件路径tools\java\multithread中。