早上运维说线上出错了,发了如下截图:
错误截图unable to create new native thread,看到这里,首先想到的是让运维搞一份线上的线程堆栈(可能通过jstack命令搞定的)。发现线上的堆栈竟然有5M多大,打开文件后线程数量居然达到了8000多个。有大量的线程堆栈如下图所示: 线程堆栈 。看到有大量的线程是以scheduler-开头的,可以确定的是程序里有使用spring的定时任务功能,定时的处理一些任务。会不会是有长时间的定时任务造成线程没有关闭。于是翻看了下spring 的配置文件。代码如下:
<task:executor id="executor" pool-size="5" />
<task:scheduler id="scheduler" pool-size="10" />
<task:annotation-driven executor="executor" scheduler="scheduler" />
可以肯定的是这里有配置线程池,但是线程池的大小也就10个啊,怎么会有8000多个线程。看到这里,我差点去翻spring的源码,想看看是不是spring的bug。检查看发现通过task:scheduler启动的线程是以scheduler-命名的,并不会有后面的SendThread与EventThread。上面的堆栈还能看出线程与zookeeper有关。于是还是回到自已的代码中,(必竟自已的代码出问题的可能性还是比框架代码出问题的概率要高出一个量级的)。
服务是分布式部署的,各部署的机器能够提供对等的服务。对于有需要一台机器执行的任务便会用到分布式锁,而zookeeper是能够用来实现分布式锁的,代码如下:
/**
* 基于zookeep实现的分布式锁
*/
public class ZookeeperLock implements DistributedLock,Watcher{
private String basePath;
private ZooKeeper zk ;
private String host;
Logger logger = LoggerFactory.getLogger(ZookeeperLock.class);
private final static String realIP = IpUtils.getRealIp();
public ZookeeperLock(String host,String basePath) {
this.basePath = basePath;
this.host = host;
}
@Override
public boolean getDistributedLock(String businessname) {
if(StringUtils.isBlank(businessname)){
throw new IllegalArgumentException("businessname can not be empty!!!");
}
try {
zk = new ZooKeeper(host, 3000, this);
logger.info("success get zk:"+zk +"and host is:" + host);
} catch (IOException e) {
logger.error("error is IOException:" + e.getMessage());
}
int retries=0;
while(retries++ < 5){
if(zk.getState() == States.CONNECTED ){
String key =basePath + "/" + businessname + "_" +DateFormatUtils.format(new Date(), "yyMMdd") ;
try {
if(zk.exists(key, false) == null){
zk.create(key, realIP.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
logger.info("get distributed lock success! ip is " + realIP);
return true;
}
} catch (KeeperException e) {
logger.error("error is KeeperException: " + e.getMessage());
releaseLock();
} catch (InterruptedException e) {
logger.error("error is InterruptedException 1: " + e.getMessage());
}
logger.debug("fail to get lock , interrupt the loop!");
break;
}
//仅在State状态不少Connected时才会循环多次等待zookeeper连接成功
try {
Thread.sleep(1000);
logger.info("zookeeper client has not connected." + zk.getState());
} catch (InterruptedException e) {
logger.error("error is InterruptedException 2: " + e.getMessage());
}
}
return false;
}
@Override
public void releaseLock() {
if(zk != null){
try {
zk.close();
zk = null;
} catch (InterruptedException e) {
}
}
}
public void process(WatchedEvent event) {
logger.debug( " Zookeeper client connect success! The clent ip is " + realIP);
}
}
分布式锁原理:
1):往指定的目录创建临时节点,创建成功的成获取锁。
2):业务执行完成后,需要删除相应的临时节点,相当于释放锁。
代码提供了加锁与解锁的逻辑,通过ZookeeperLock 对象,便能够实现分布式锁了。ZookeeperLock通过xml文件的配置来进行初使化。
为了方便对ZookeeperLock 的使用,提供了相应的模版类,代码如下:
@Component
public class ZookeeperLockTemplate {
private static final Logger logger = LoggerFactory.getLogger(ZookeeperLockTemplate.class);
@Autowired //容器会自动注入
private ZookeeperLock distributedLock;
public void execute(String businessname, ZookeeperLockCallBack callBack){
try {
if (distributedLock.getDistributedLock(businessname)) {
callBack.call();
}
} catch (Exception e) {
logger.error("ZookeeperLockTemplate error! ", e);
} finally {
distributedLock.releaseLock();
}
}
}
代码看上去并没有什么问题,但是问题就出在ZookeeperLockTemplate这个类上。通过componet注解注释的类在Spring 容器里是单例的,里面的属性即使scope类型是prototype,每次使用的时候也是用的同一个实例。上面的ZookeeperLockTemplate如果在多线程的情况下使用,便会有大的问题,一个线程可能关闭另一个线程开启的锁,从而造成另一个线程的锁无法关闭,导制zk = new ZooKeeper(host, 3000, this)这个对象的线程一直在内存中修改的方法也很简单,在execute方法里,每次都从spring IOC的容器里取一次就好了。
上面的代码如果有问题,那在线上应该能够频繁的重现,而不至于上线好久了才发现这样的问题。这里主要是因为线上就三个定时任务,而且定时任务执行的并不频繁,但是他们的执行时间是有重叠的(也就是有可能有多线程的情况)。从日子累计到8000多条线程也可以看出只有运行了一定时间后,才可能出现问题。
网友评论