1. 概述
前段时间,线上的服务不知道为啥,突然全部的服务都超时,所有的请求经过网关都超时,后来进行链路追踪排查,发现有一个服务链接 RDS 数据库,一个查询花费了 20S 的查询时间,导致后续调用该服务的应用都超时。然后超时的连接占满了 zuul 的转发池,最终导致了所有经过 gateway 的服务都在等待,导致全体服务全部超时。
原因找出来之后,我就在纳闷,为何一个服务超时会导致所有服务都延时,作为一个高可用的网关, zuul 的设计应该不会这么差吧,并且,当时系统的 QPS 也不是很高,所以,必须找出这次超时的问题所在,于是,我就开始了此次网关超时的排查和优化!
下图是出现问题是的相关监控
因为公司的服务是 java 和 node 应用都存在,所以使用的 zuul 是简单路由转发,未使用 服务注册中心之类的,所以,熔断器和
2. 问题分析
首先,因为网关的相关配置都是之前填的,有些参数不是特别的清楚,下面的配置文件是我当时系统的相关配置。
zuul:
host:
connect-timeout-millis: 30000
socket-timeout-millis: 60000
ribbon:
ConnectTimeout: 300000
ReadTimeout: 60000
eureka:
enabled: false
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 70000
2.1 zuul 参数解释
connect-timeout-millis
此参数为 zuul 网关连接服务的时间,单位为毫秒,我这里配置的是 30S,如果 30S 之内未连接到待转发的下一服务,则转发将报错,此请求也就将结束。这段时间为下图的第 1 步的时间
如下图所示:
socket-timeout-millis
zuul 网关连接到服务并且服务返回结果这一部分时间,单位为毫秒,我这里配置的是 60S,如果 60S 之内下一服务还没有返回, gateway 将报转发超时。
此时间为上图的 1+2+3 三部分时间
ribbon.ConnectTimeout
此为 ribbon 转发的连接时间,如果 zuul 使用的服务调用,则将采用此时间
ribbon.ReadTimeout
此为 ribbon 转发到返回的时间,如果 zuul 使用的是服务调用,则将采用此时间
超时时间采用哪个?
以上四个都是 zuul 的超时时间,但是问题来了,四个时间,到底采用哪两个呢?
我通过阅读 zuul 的相关文档,了解到,如果 zuul 使用的服务发现,则将会使用 rebbon进行负载均衡,即 ribbon.ConnectTimeout
和 ribbon.ReadTimeout
。 如果 zuul 使用的是简单路由(通过配置 url 进行路由转发),则将采用 socket-timeout-millis
和 connect-timeout-millis
。
If you want to configure the socket timeouts and read timeouts for requests proxied through Zuul, you have two options, based on your configuration:
- If Zuul uses service discovery, you need to configure these timeouts with the ribbon.ReadTimeout and ribbon.SocketTimeout Ribbon properties.
- If you have configured Zuul routes by specifying URLs, you need to use zuul.host.connect-timeout-millis and zuul.host.socket-timeout-millis.
2.2 问题分析
因为我的服务未使用服务注册中心,所以,很明显, 配置的 ribbon 并未生效,zuul 超时时间使用的 socket-timeout-millis
和 connect-timeout-millis
。
但是问题又来了,我 zuul 的超时时间为 60S,为何我的服务相应时间平均达到了 3 分钟,远远超过了我设置的 60S,所以肯定是 zuul 还出现了相应的问题。
后续通过了解到 zuul 源码和架构,明白 zuul 是 NIO 架构,即一个请求进来,经过 zuul 拦截器 Filter,最终交给 HttpClient 进行请求,zuul 为了高性能,使用 HttpClient 连接池。
获取连接源码
下面是 zuul 获取 HttpClient 连接池的代码
AbstractConnPool.getPoolEntryBlocking,看这个名字就知道。这是一个阻塞获取池资源的方法
private E getPoolEntryBlocking(
final T route, final Object state,
final long timeout, final TimeUnit timeUnit,
final Future<E> future) throws IOException, InterruptedException, ExecutionException, TimeoutException {
Date deadline = null;
if (timeout > 0) {
deadline = new Date (System.currentTimeMillis() + timeUnit.toMillis(timeout));
}
this.lock.lock();
try {
final RouteSpecificPool<T, C, E> pool = getPool(route);
E entry;
for (;;) {
Asserts.check(!this.isShutDown, "Connection pool shut down");
if (future.isCancelled()) {
throw new ExecutionException(operationAborted());
}
for (;;) {
entry = pool.getFree(state);
if (entry == null) {
break;
}
if (entry.isExpired(System.currentTimeMillis())) {
entry.close();
}
if (entry.isClosed()) {
this.available.remove(entry);
pool.free