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.ConnectTimeoutribbon.ReadTimeout。 如果 zuul 使用的是简单路由(通过配置 url 进行路由转发),则将采用 socket-timeout-millisconnect-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-millisconnect-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(entry, false);
} else {
break;
}
}
if (entry != null) {
this.available.remove(entry);
this.leased.add(entry);
onReuse(entry);
return entry;
} // New connection is needed
final int maxPerRoute = getMax(route);
// Shrink the pool prior to allocating a new connection
final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute);
if (excess > 0) {
for (int i = 0; i < excess; i++) {
final E lastUsed = pool.getLastUsed();
if (lastUsed == null) {
break;
}
lastUsed.close();
this.available.remove(lastUsed);
pool.remove(lastUsed);
}
} if (pool.getAllocatedCount() < maxPerRoute) {
final int totalUsed = this.leased.size();
final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0);
if (freeCapacity > 0) {
final int totalAvailable = this.available.size();
if (totalAvailable > freeCapacity - 1) {
if (!this.available.isEmpty()) {
final E lastUsed = this.available.removeLast();
lastUsed.close();
final RouteSpecificPool<T, C, E> otherpool = getPool(lastUsed.getRoute());
otherpool.remove(lastUsed);
}
}
final C conn = this.connFactory.create(route);
entry = pool.add(conn);
this.leased.add(entry);
return entry;
}
} boolean success = false;
try {
pool.queue(future);
this.pending.add(future);
if (deadline != null) {
success = this.condition.awaitUntil(deadline);
} else {
this.condition.await();
success = true;
}
if (future.isCancelled()) {
throw new ExecutionException(operationAborted());
}
} finally {
// In case of 'success', we were woken up by the
// connection pool and should now have a connection
// waiting for us, or else we're shutting down.
// Just continue in the loop, both cases are checked.
pool.unqueue(future);
this.pending.remove(future);
}
// check for spurious wakeup vs. timeout
if (!success && (deadline != null && deadline.getTime() <= System.currentTimeMillis())) {
break;
}
}
throw new TimeoutException("Timeout waiting for connection");
} finally {
this.lock.unlock();
}
}
  1. 代码已建立有一个deadline ,然后判断timeout ,这个timeout要注意。如果大于零才会赋值deadline, 如果为0 则不会赋值deadline 也就是说deadline始终为null
        Date deadline = null;
if (timeout > 0) {
//如果超时时间有效,则设定deadline
deadline = new Date (System.currentTimeMillis() + tunit.toMillis(timeout));
}
  1. 进入锁代码。pool.getFree 获取池资源。如果获取到了,并且Connect的检验并没有被关闭,则直接return entry
                Asserts.check(!this.isShutDown, "Connection pool shut down");
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(entry, false);
} else {
break;
}
}
if (entry != null) {
this.available.remove(entry);
this.leased.add(entry);
onReuse(entry);
return entry;
}
  1. 如果没有获取到 进行接下来的代码。
  2. 判断是否达到了host配置的最大池数量,是否需要增加, 如果需要增加,则会在增加新连接之前缩小池,然后再分配返回entry
                // New connection is needed  获取是否需要创建新的连接
final int maxPerRoute = getMax(route);
// Shrink the pool prior to allocating a new connection
final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute);
if (excess > 0) {
for (int i = 0; i < excess; i++) {
final E lastUsed = pool.getLastUsed();
if (lastUsed == null) {
break;
}
lastUsed.close();
this.available.remove(lastUsed);
pool.remove(lastUsed);
}
} if (pool.getAllocatedCount() < maxPerRoute) {
final int totalUsed = this.leased.size();
final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0);
if (freeCapacity > 0) {
final int totalAvailable = this.available.size();
if (totalAvailable > freeCapacity - 1) {
if (!this.available.isEmpty()) {
final E lastUsed = this.available.removeLast();
lastUsed.close();
final RouteSpecificPool<T, C, E> otherpool = getPool(lastUsed.getRoute());
otherpool.remove(lastUsed);
}
}
final C conn = this.connFactory.create(route);
entry = pool.add(conn);
this.leased.add(entry);
return entry;
}
}
  1. 如果并不是上面的情况,实际情况就是池子被用光了,而且还达到了最大。就不能从池子中获取资源了。只能等了……
  2. 等待的时候会判断deadline , 如果deadline不为null 就会await一个时间。如果为null,那么等待就会无限等待,直到有资源。
                boolean success = false;
try {
if (future.isCancelled()) {
throw new InterruptedException("Operation interrupted");
}
pool.queue(future);
this.pending.add(future);
//判断deadline是否有效
if (deadline != null) {
//如果有效就等待至deadline
success = this.condition.awaitUntil(deadline);
} else {
//如果无效就一直等待,没有超时时间
this.condition.await();
success = true;
}
if (future.isCancelled()) {
throw new InterruptedException("Operation interrupted");
}
} finally {
// In case of 'success', we were woken up by the
// connection pool and should now have a connection
// waiting for us, or else we're shutting down.
// Just continue in the loop, both cases are checked.
pool.unqueue(future);
this.pending.remove(future);
}

创建连接池源码

问题通过以上的源码就发现了,关键问题是线程池的等待时间,设置一个连接的等待时间即可解决,使得线程不会一直等待 HttpCllient 连接,我找到相应的创建 CloseableHttpClient 卫士,位于 SimpleHostRoutingFilter#newClient#newClient(),源码如下

	protected CloseableHttpClient newClient() {
final RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(
// 设置超时链接时间
this.hostProperties.getConnectionRequestTimeoutMillis())
.setSocketTimeout(this.hostProperties.getSocketTimeoutMillis())
.setConnectTimeout(this.hostProperties.getConnectTimeoutMillis())
.setCookieSpec(CookieSpecs.IGNORE_COOKIES).build();
return httpClientFactory.createBuilder().setDefaultRequestConfig(requestConfig)
.setConnectionManager(this.connectionManager).disableRedirectHandling()
.build();
}

此处可看到,主要是从 this.hostProperties.getConnectionRequestTimeoutMillis(),拿到超时时间,最终我找到了 Springboot 配置 connectionRequestTimeoutMillis 位置,即 ZuulProperties.Host#connectionRequestTimeoutMillis,代码如下

	/**
* Represents a host.
*/
public static class Host { /**
* The maximum number of total connections the proxy can hold open to backends.
*/
private int maxTotalConnections = 200; /**
* The maximum number of connections that can be used by a single route.
*/
private int maxPerRouteConnections = 20; /**
* The socket timeout in millis. Defaults to 10000.
*/
private int socketTimeoutMillis = 10000; /**
* The connection timeout in millis. Defaults to 2000.
*/
private int connectTimeoutMillis = 2000; /**
* The timeout in milliseconds used when requesting a connection from the
* connection manager. Defaults to -1, undefined use the system default.
* 此处时间为 -1,即永久等待
*/
private int connectionRequestTimeoutMillis = -1; /**
* The lifetime for the connection pool.
*/
private long timeToLive = -1; /**
* The time unit for timeToLive.
*/
private TimeUnit timeUnit = TimeUnit.MILLISECONDS; public Host() {
} // get set and toString }

通过源码可看到,zuul 有如下的相关配置:

  • maxTotalConnections:HttpClient 总连接数,默认值为200
  • maxPerRouteConnections:HttpClient 单个服务(即服务发现中的每个服务)连接数,默认为 20
  • socketTimeoutMillis:连接服务时间,单位为毫秒,默认为10秒
  • connectTimeoutMillis:服务返回时间,单位为毫秒,默认时间为20秒
  • connectionRequestTimeoutMillis:连接 HttpClient 等待时间,默认为-1,即永久!

下面来对为何超时进行一个复现:

假设 100 个请求进来,HttpClient 连接池大小为 20,其中 20 个请求一直在等待远程服务返回,其余 80 个请求一直在等待连接池的空闲连接,所以连接一直在等待,最终导致其它服务无法进入,最终导致所有服务都瘫痪了。

因为我的 gateway 未使用路由发现,所以,微服务中的熔断器和负载均衡,均使用不上。所以只能采用如下方法:

  • 设置 HttpClient 连接时间,即 connectionRequestTimeoutMillis 设置为 10S
  • 增大 HttpClient 连接池大小,使得有足够多的连接数,来增大并发量,即设置 maxTotalConnections 和 maxPerRouteConnections,这里我设置成了 500 和 250

下面是我调优后的参数:

zuul:
host:
connect-timeout-millis: 30000
socket-timeout-millis: 60000
max-total-connections: 500
max-per-route-connections: 250
connection-request-timeout-millis: 10000

3. 后续

虽然增大了 HttpClient 连接池大小,修改了连接 HttpClient 的时间,但是进行压测时,依旧会出现某些请求,时间超过了 60S,我们以上的设置为 60S 再加上抢占连接池,总时间也不过 70S,如果超过70S,则会报抢占 HttpClient 连接池异常。但是我压测后的结果,并不如此,以下是我压测的结果

以上压测显示,95% 请求时间为 164 S,远远超过了我们预测的 70S,并且压测的 QPS 很低,才 5.1,所以 zuul 的效率还不是很高,还待优化!

预知后事如何,请期待接下来的博客

zuul 网关超时优化的更多相关文章

  1. SpringCloud Zuul网关超时

    最近在使用SpringCloudZuul网关时,报错"NUMBEROF_RETRIES_NEXTSERVER_EXCEEDED", 查询资料后,发现: ribbon.Connect ...

  2. 最全面的改造Zuul网关为Spring Cloud Gateway(包含Zuul核心实现和Spring Cloud Gateway核心实现)

    前言: 最近开发了Zuul网关的实现和Spring Cloud Gateway实现,对比Spring Cloud Gateway发现后者性能好支持场景也丰富.在高并发或者复杂的分布式下,后者限流和自定 ...

  3. zuul网关

    Zuul路由网关简介及基本使用 简介 Zuul API路由网关服务简介 请看上图,这里的API 路由网关服务 由Zuul实现,主要就是对外提供服务接口的时候,起到了请求的路由和过滤作用,也因此能够隐藏 ...

  4. SpringCloud之Zuul网关原理及其配置

    Zuul是spring cloud中的微服务网关.网关: 是一个网络整体系统中的前置门户入口.请求首先通过网关,进行路径的路由,定位到具体的服务节点上. Zuul是一个微服务网关,首先是一个微服务.也 ...

  5. Web API幂等、超时优化

    幂等 当涉及业务数据的变更,不是简单的数据查询时, 在调用方相同条件有效重复请求时,就需要保持业务系统数据之间的一致性,不管请求多少次都会返回相同的结果. 比如一个订单支付接口,第一次请求返回支付成功 ...

  6. zuul网关配置

    静态路由:通过url匹配映射地址进行静态路由(只会把到达zuul网关的请求按照发送,并把匹配请求地址 /common-service/ ->http://localhost:9001/) zuu ...

  7. 14 微服务电商【黑马乐优商城】:day03-springcloud(Zuul网关)

    本项目的笔记和资料的Download,请点击这一句话自行获取. day01-springboot(理论篇) :day01-springboot(实践篇) day02-springcloud(理论篇一) ...

  8. Zuul请求超时

    最近在弄springcloud的时候发现在发送短信的时候zuul总是报错,错误信息如下 com.netflix.zuul.exception.ZuulException: at org.springf ...

  9. 非常全面的讲解SpringCloud中Zuul网关原理及其配置,看它就够了!

    Zuul是spring cloud中的微服务网关.网关:是一个网络整体系统中的前置门户入口.请求首先通过网关,进行路径的路由,定位到具体的服务节点上. Zuul是一个微服务网关,首先是一个微服务.也是 ...

  10. Spring Cloud Zuul网关 Filter、熔断、重试、高可用的使用方式。

    时间过的很快,写springcloud(十):服务网关zuul初级篇还在半年前,现在已经是2018年了,我们继续探讨Zuul更高级的使用方式. 上篇文章主要介绍了Zuul网关使用模式,以及自动转发机制 ...

随机推荐

  1. JAVA 24 环境安装与配置

    JAVA 24 环境安装与配置 一.Java Downloads Java 下载(Windows x64) https://www.oracle.com/java/technologies/downl ...

  2. 关于#pragma comment的小认识

    #pragma 指令 #pragma为预处理指令,作用是设定编译器的状态或者是指示编译器完成一些特定的动作.#pragma指令对每个编译器给出了一个方法,在保持与C和C++语言完全兼容的情况下,给出主 ...

  3. codeup之有序插入

    Description 有一个已排好序的数组,要求输入一个数后,按原来排序的规律将它插入到数组中. 假设数组长度为10,数组中前9个数(这9个数要求从键盘上输入,输入时要满足自小到大的输入顺序)已经按 ...

  4. 从传统搜索到智能问答:自研 RAG 系统的技术实践与工程落地

    一.引言 在数字化转型浪潮下,企业知识服务体系正经历着深刻变革.如何让用户高效获取所需信息,成为提升产品竞争力和用户满意度的关键.葡萄城作为企业级开发工具与解决方案提供商,长期致力于知识服务体系的建设 ...

  5. 使用 frp 进行内网穿透

    frp 是一个开源的内网穿透工具,可以使外网设备访问内网防火墙后的设备/服务器. 比如可以买一个 99元/年 的阿里云低配 ECS,然后把自己相对高配的旧电脑放在家里. 家里的旧电脑通过 frp 连上 ...

  6. VUE前端,小程序,APP签名盖章

    签名盖章功能就是把图片(签名或盖章) 与背景图片(需要签名的文件,如PDF) 合并起来,生成一个新的图片 此功能的开发流程顺序: 显示背景PDF 操作PDF:手势缩放,回到顶部,上下翻页 手绘签字功能 ...

  7. SQL Server 链接服务器"XXXXXXX"的 OLE DB 访问接口 "SQLNCLI11" 返回了消息 "没有活动事务。"。

    一.确保互联双方服务器MS DTC服务已启动并正确配置 打开"服务"管理控制台(services.msc) 找到"Distributed Transaction Coor ...

  8. Web前端入门第 70 问:JavaScript DOM 节点查找常用方法

    虽然目前的开发场景基本都是使用 React/Vue/Angular 等框架,但是对于一些基础的 DOM 操作,还是需要了解学习. 曾经我们讨论过这样一个问题:Vue 这些开发框架,用它们渲染页面,真的 ...

  9. Rust 修仙之道

    静修Rust三年多,有所感悟.今借道家修仙之法展一斑之管窥,只为博大家一乐.进入正文

  10. Milvus 使用

    Milvus记录:1.安装Python: $ pip install -U pymilvus #pymilvus 中包含的一个 python 库,可以嵌入到客户端应用程序中 $ pip install ...