服务器出现故障,分析日志:
最先开始的是danga.memcached报"too many open files",句柄数达到上限(ulimit上限已经是加大的),
导致danga.memcached无法创建新的套接字,连接缓存服务器失败,
紧接着压力全部转移到数据库,数据库连接池用完,很快就撑不住,
因为数据库响应慢,线程占用率变高,接收请求的线程池也达到上限,很多请求被拒绝,
页面出现时而可以访问,时而访问不了,重启后恢复正常,
lsof查看Java进程持有的句柄,大部分为pipe和event poll,
初步判断是danga.memcached中使用的nio管道未正确关闭。
为了进一步了解问题,分析源码:
首先来看danga是怎么获取套接字的:
SocketIO.getSocket()
再来看应用调用API发生了什么:
MemCachedClient.get()
内部调用关系等价于:
再看看它是怎么关闭的:
SockIO.trueClose()
这种把非阻塞式管道当作阻塞式套接字用法,我们来看看SunJDK是怎么处理的:
SocketChannelImpl.socket().connect()最终调用的是:
SocketAdaptor.connect()
再看看Util.getTemporarySelector()怎么实现的:
通过上面的代码,基本结论是:
danga.memcached2.0.1使用nio非阻塞式管道,
但却把非阻塞式管道当作阻塞式套接字用,没有注册Selector,
JDK会注册一个临时的Selector,使用Soft引用,并使用ThreadLocal做缓存,
当线程池剧烈收缩和扩张时,就会出现很多垃圾Soft引用的Selector,
如果内存够用,这些Soft引用都不会释放,句柄数就有可能达到上限。
而danga.memcached1.6.0使用的是常规阻塞式套接字,性能稍慢,但资源管理都比较正确,
降级使用1.6.0版本后,服务器恢复正常,
但1.6.0版本存在memcached服务器重启后,客户端无法自动恢复连接的问题,需将连接超时设置足够短(50ms以内)。
另外,将接收请求的线程池最大值和最小值设为一样,固定大小,避免收缩。
最先开始的是danga.memcached报"too many open files",句柄数达到上限(ulimit上限已经是加大的),
导致danga.memcached无法创建新的套接字,连接缓存服务器失败,
紧接着压力全部转移到数据库,数据库连接池用完,很快就撑不住,
因为数据库响应慢,线程占用率变高,接收请求的线程池也达到上限,很多请求被拒绝,
页面出现时而可以访问,时而访问不了,重启后恢复正常,
lsof查看Java进程持有的句柄,大部分为pipe和event poll,
初步判断是danga.memcached中使用的nio管道未正确关闭。
为了进一步了解问题,分析源码:
首先来看danga是怎么获取套接字的:
SocketIO.getSocket()
SocketChannel sock = SocketChannel.open();
sock.socket().connect( new InetSocketAddress( host, port ), timeout );
return sock.socket(); // 直接拿到Socket使用,把SocketChannel的引用丢了,并且没有注册Selector
再来看应用调用API发生了什么:
MemCachedClient.get()
// 省略了不相关代码
sock.write( cmd.getBytes() );
String line = sock.readLine();
内部调用关系等价于:
// sock.write( cmd.getBytes() ); 等价于
out = new BufferedOutputStream( sock.getOutputStream() );
out.write( b );
// String line = sock.readLine(); 等价于
in = new DataInputStream( sock.getInputStream() );
in.read( b );
再看看它是怎么关闭的:
SockIO.trueClose()
// 省略了错误检查代码
in.close();
out.close();
sock.close(); // Soket内部会同时关闭Channel,但不会关闭Selector
这种把非阻塞式管道当作阻塞式套接字用法,我们来看看SunJDK是怎么处理的:
SocketChannelImpl.socket().connect()最终调用的是:
SocketAdaptor.connect()
// 省略了部分代码
sel = Util.getTemporarySelector(sc);
sk = sc.register(sel, SelectionKey.OP_CONNECT);
再看看Util.getTemporarySelector()怎么实现的:
// 省略了部分代码
private static ThreadLocal localSelector = new ThreadLocal();
SoftReference ref = (SoftReference)localSelector.get();
sel = sc.provider().openSelector();
localSelector.set(new SoftReference(new SelectorWrapper(sel)));
return sel;
通过上面的代码,基本结论是:
danga.memcached2.0.1使用nio非阻塞式管道,
但却把非阻塞式管道当作阻塞式套接字用,没有注册Selector,
JDK会注册一个临时的Selector,使用Soft引用,并使用ThreadLocal做缓存,
当线程池剧烈收缩和扩张时,就会出现很多垃圾Soft引用的Selector,
如果内存够用,这些Soft引用都不会释放,句柄数就有可能达到上限。
而danga.memcached1.6.0使用的是常规阻塞式套接字,性能稍慢,但资源管理都比较正确,
降级使用1.6.0版本后,服务器恢复正常,
但1.6.0版本存在memcached服务器重启后,客户端无法自动恢复连接的问题,需将连接超时设置足够短(50ms以内)。
另外,将接收请求的线程池最大值和最小值设为一样,固定大小,避免收缩。
能力不够,别人找到了这个帖子,而自己找不到,不过当时已经怀疑是mmcached的问题的了,呵呵。