前言:
最近生产上出现一个问题,描述如下:
springcloud分布式环境下,服务B无法处理新进来的请求,且zuul服务一直在走降级逻辑。
服务调用情况:zuul->服务A->服务B
问题分析:
1.首先查看tomcat连接是不是满了,通过netstat -nat|grep -i “服务B端口号”|grep ESTABLISHED|wc -l,连接数为300多,tomcat默认最大线程数配置是200,说明所有tomcat线程可能已经被使用。
2.通过jvisualvm工具,查看服务B的线程数确实在200以上,证明了第一点分析,dump内存线程,发现问题的原因是线程卡在了一段代码执行上。
问题已经改了,但是感觉还是没有完全解决完:
1.是什么原因导致zuul服务走降级逻辑?
2.为什么zuul服务已经走降级逻辑了,但是内部服务B的tomcat连接却还占满着不释放?
针对问题1
1.根据debug了下,发现是报了hystrix超时异常导致走的降级逻辑,查看超时配置是30秒:
hystrix:
command:
A:
execution:
isolation:
thread:
timeoutInMilliseconds: 30000
针对问题2
1.查看ribbon读取超时配置:ribbon.ReadTimeout: 60000
2.当服务之间建立连接之后调用超过了60秒,就会报ribbon的socket read timeout异常,结合问题1分析,发现zuul请求服务A的hystrix超时配置小于ribbon的读取超时配置,在这种情况下,当前请求超过20秒,该请求就会走降级逻辑,但是服务A调用服务B,还没有到达ribbon的读取超时时间,所以连接还保持着。
结论:这种配置是有点不太合理,建议服务之间调用超时配置小于hystrix超时配置。
这个问题的分析过程中涉及到了tomcat连接数及线程数概念,后来我就想:
那什么时候tomcat连接数才算真的满了?如果不能建立新的请求,那这时会报错么?报什么错?
带着疑惑写代码做了下验证:
1.自定义tomcat容器配置:
@Component
public class MyEmbeddedServletContainerFactory extends TomcatEmbeddedServletContainerFactory {
@Override
public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers){
//设置端口
this.setPort(8081);
return super.getEmbeddedServletContainer(initializers);
}
@Override
protected void customizeConnector(Connector connector){
super.customizeConnector(connector);
Http11NioProtocol protocol = (Http11NioProtocol)connector.getProtocolHandler();
//设置最大连接数,默认10000
protocol.setMaxConnections(50);
//设置全连接队列大小,默认100
protocol.setAcceptCount(200);
//设置最大线程数,默认200
protocol.setMaxThreads(99);
//设置连接超时,默认20000ms,tomcat读取超时配置也是使用的这个值
protocol.setConnectionTimeout(100);
}
}
2.编写controller:
@RestController
public class ConnectTest {
private AtomicInteger count = new AtomicInteger();
@GetMapping("/test")
public void test(){
System.out.println(count.decrementAndGet());
try {
//休息较长时间,让线程阻塞挂起
Thread.sleep(1000* 10000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3.编写测试代码:
public class ControllerTest {
/**线程数**/
private static final int THREAD_SIZE = 100;
public static void main(String[] args){
String url = "http://127.0.0.1:8081/test";
for (int i =0; i < THREAD_SIZE; i++){
Thread t = new Thread(()->{
RestTemplate template = new RestTemplate();
template.getForEntity(url , Object.class);
});
t.start();
}
}
}
测试开始:
1.设置THREAD_SIZE=149(tomcat最大连接数+tomcat最大处理线程数)
预期:全部建连成功
结果:所有请求全部建立连接,符合预期
2.设置THREAD_SIZE=150(tomcat最大连接数+tomcat最大处理线程数)+1
预期:最后一个执行线程会建连失败
结果:所有请求全部建立连接,不符合预期,百度了下网上也有提到accpetCount也会影响连接的上限
3.设置THREAD_SIZE=250(tomcat最大连接数+tomcat全连接队列大小)
预期:全部建连成功
结果:所有请求全部建立连接,符合预期
4.设置THREAD_SIZE=251(tomcat最大连接数+tomcat全连接队列大小)+1
预期:最后一个执行线程会建连失败
结果:最后一个线程请求报错,服务端返回connect refused错误,符合预期
经过反复测试,不断修改配置以及测试代码最终得到一些结论:
1.tomcat容器可以接受的最大连接数计算:最大连接数+(全对列大小/最大线程数 其中较大的那个值)
2.当连接满了之后,服务端会拒绝客户端的请求,客户端报:connect refused错误。
扩展学习:
socket各种超时
1.socket连接超时
摘自java.Socket.connect方法的一段源码:
* Connects this socket to the server with a specified timeout value.
* A timeout of zero is interpreted as an infinite timeout. The connection
* will then block until established or an error occurs.
public void connect(SocketAddress endpoint, int timeout) throws IOException
2.socket读超时
摘自java.Socket.setSoTimeout方法的一段源码:
* Enable/disable {@link SocketOptions#SO_TIMEOUT SO_TIMEOUT}
* with the specified timeout, in milliseconds. With this option set
* to a non-zero timeout, a read() call on the InputStream associated with
* this Socket will block for only this amount of time. If the timeout
* expires, a <B>java.net.SocketTimeoutException</B> is raised, though the
* Socket is still valid. The option <B>must</B> be enabled
* prior to entering the blocking operation to have effect. The
* timeout must be {@code > 0}.
* A timeout of zero is interpreted as an infinite timeout.
public synchronized void setSoTimeout(int timeout) throws SocketException
3.socket写超时
socket没有写入超时这个概念,可以自己封装实现,例如tomcat1.8使用Nio2SocketWrapper自己封装了写超时。
留下问题(方便自己回过头看时想一想):
1.tcp长连接和短连接区别?
2.怎么理解无状态性?
3.什么是全连接队列?
4.全连接队列大小取值?
全连接队列的大小未必是backlog的值,它是backlog与somaxconn(一个os级别的系统参数)的较小值