Zuul1.x 高并发下阻塞分析以及解决方案

背景

由于最近博主在压测接口的时候发现我接口出现卡死状态,最开始以为是我自己接口出现问题,单独压测我自己的服务(不经过网关)200/qps/10 次循环 是没问题,但是加上网关(zuul 1.x) 去发现 经过两次循环基本就不能访问,同时其他接口也不能访问,由此问题出现在zuul ,接着开始排查之路。

确认问题

在刚才背景当时只是怀疑zuul 有问题,因为zuul 没有加降级熔断。是否是它需要排查去确认,我当时(测试环境)通过arthas 查看了内存、线程,发现大量waiting 线程,查询具体waiting 线程详细信息后发现有大量http 请求连接没有唤醒,处于watting 根本原因是连接没有关闭。后来又在本地压测通过jconsole 定位如图
在这里插入图片描述
发现和测试环境一样的大量阻塞线程,为啥阻塞就需要看看Zuul 和Ribbon 他们交互逻辑。下面是分析过程。

代码分析

基于之前对zuul 1.x了解执行流程图
在这里插入图片描述
可以看到整个流程也就是route 会建立http连接请求。根据源码之后流程只有两种情况一种是成功执行post 另一种是出现异常执行error。

请求进来代码逻辑

在这里插入图片描述
在这里插入图片描述

异常后执行代码

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
核心出现也就是SendErrorFilter-run在这里插入图片描述
正常不抛异常的话SendResponseFilter 理论是最后一个filter 他会执行关闭操作

private void writeResponse() throws Exception {RequestContext context = RequestContext.getCurrentContext();// there is no body to sendif (context.getResponseBody() == null&& context.getResponseDataStream() == null) {return;}HttpServletResponse servletResponse = context.getResponse();if (servletResponse.getCharacterEncoding() == null) { // only set if not setservletResponse.setCharacterEncoding("UTF-8");}String servletResponseContentEncoding = getResponseContentEncoding(context);OutputStream outStream = servletResponse.getOutputStream();InputStream is = null;try {if (context.getResponseBody() != null) {String body = context.getResponseBody();is = new ByteArrayInputStream(body.getBytes(servletResponse.getCharacterEncoding()));}else {is = context.getResponseDataStream();if (is != null && context.getResponseGZipped()) {// if origin response is gzipped, and client has not requested gzip,// decompress stream before sending to client// else, stream gzip directly to clientif (isGzipRequested(context)) {servletResponseContentEncoding = "gzip";}else {servletResponseContentEncoding = null;is = handleGzipStream(is);}}}if (servletResponseContentEncoding != null) {servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING,servletResponseContentEncoding);}if (is != null) {writeResponse(is, outStream);}}finally {/*** We must ensure that the InputStream provided by our upstream pooling* mechanism is ALWAYS closed even in the case of wrapped streams, which are* supplied by pooled sources such as Apache's* PoolingHttpClientConnectionManager. In that particular case, the underlying* HTTP connection will be returned back to the connection pool iif either* close() is explicitly called, a read error occurs, or the end of the* underlying stream is reached. If, however a write error occurs, we will end* up leaking a connection from the pool without an explicit close()** @author Johannes Edmeier*/if (is != null) {try {//关闭流 同时org.apache.http.conn.EofSensorInputStream 也会清除http 连接is.close();}catch (Exception ex) {log.warn("Error while closing upstream input stream", ex);}}// cleanup ThreadLocal when we are all doneif (buffers != null) {buffers.remove();}try {Object zuulResponse = context.get("zuulResponse");if (zuulResponse instanceof Closeable) {((Closeable) zuulResponse).close();}outStream.flush();// The container will close the stream for us}catch (IOException ex) {log.warn("Error while sending response to client: " + ex.getMessage());}}}

EofSensorInputStream 关闭同时也会归还http连接。
通过上面代码分析,压测的时候发生异常,所以代码执行都会去SendErrorFilter run 方法 他会转发

dispatcher.forward(request, ctx.getResponse());

这个又会重新执行到ZuulServlet 中service 再次请求到之前的微服务接口。因此我们压测那个场景出现阻塞的原因就是:当并发线程高于配置资源后 rabbion http 连接池么有可用连接了,拿不到连接也没有熔断降级配置,抛异常最后执行到SendErrorFilter 这里没有对

public InputStream getResponseDataStream() {return (InputStream) get("responseDataStream");}

执行关闭。导致了连接泄露线程阻塞了,从而页面卡死。
不同情况具体分析

  • 异常发生在route 阶段
    像我们那个场景就是这个阶段,由于线程不够,在获取连接抛出异常,第一次执行到SendErrorFilter 由于没有请求成功 所以getResponseDataStream 是null ,但是由于上面说了会转发会继续走一次ZuulServlet service 这个时候假如有连接释放请求成功后 会对responseDataStream 进行设置赋值 代码如下
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    再次回到SendErrorFilter 的时候 getResponseDataStream 就会有值 这个时候没有对他进行关闭,造成连接泄露。
  • 异常发生在post 阶段
    这个阶段发生异常基本getResponseDataStream 已经有值了,所以说只要你自己定义的post 类型的filter 有异常抛出来没有处理必然发生连接泄露,因为他最后还是执行到SendErrorFilter 。

解决方案

第一步增加熔断降级

@Slf4j
public class CustomFallbackProvider implements FallbackProvider {@Overridepublic String getRoute() {return "*";}@Overridepublic ClientHttpResponse fallbackResponse(String route, Throwable cause) {return new ClientHttpResponse() {/***ClientHttpResponse的fallback的状态码,返回的是HttpStatus* @return*/@Overridepublic HttpStatus getStatusCode() throws IOException {return HttpStatus.INTERNAL_SERVER_ERROR;}/***ClientHttpResponse的fallback的状态码,返回的是int* @return*/@Overridepublic int getRawStatusCode() throws IOException {return this.getStatusCode().value();}/***ClientHttpResponse的fallback的状态码,返回的是String* @return*/@Overridepublic String getStatusText() throws IOException {return this.getStatusCode().getReasonPhrase();}@Overridepublic void close() {}/***设置响应体信息* @return*/@Overridepublic InputStream getBody() {String content = "网络异常,请稍后重试!";return new ByteArrayInputStream(content.getBytes());}/***设置响应的头信息* @return*/@Overridepublic HttpHeaders getHeaders() {HttpHeaders headers = new HttpHeaders();MediaType mediaType = new MediaType("application", "json", Charset.forName("utf-8"));headers.setContentType(mediaType);return headers;}};}
}

为啥增加降级会减少(是大大降低但是不是完全解决)线程阻塞问题?通过代码分析
在这里插入图片描述
在这里插入图片描述
我们有自定义的FallbackProvider 返回ClientHttpResponse 这样不会执行到SendErrorFilter 最后走的还是SendResponseFilter run 方法中关闭流归还连接。

重新写SendErrorFilter

继承ZuulFilter 设置Error 类型 Order 设置-1 保证有异常不去执行SendErrorFilter (context.remove(“throwable”); 之后shouldFilter 返回false 也就不会执行了) 核心代码如下:

@Slf4j
@Component
public class ErrorFilter extends ZuulFilter {@Overridepublic String filterType() {return ERROR_TYPE;}@Overridepublic int filterOrder() {return -1;}protected static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran";@Overridepublic boolean shouldFilter() {RequestContext ctx = RequestContext.getCurrentContext();return ctx.getThrowable() != null && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);}@Overridepublic Object run() {RequestContext context = RequestContext.getCurrentContext();PrintWriter writer = null;InputStream is = null;try {context.remove("throwable");context.set(SEND_ERROR_FILTER_RAN, true);ZuulException exception = findZuulException(context.getThrowable());HttpServletResponse response = context.getResponse();response.setContentType("application/json; charset=utf8");response.setStatus(exception.nStatusCode);is = context.getResponseDataStream();writer = response.getWriter();Map<String, Object> map = new HashMap<>();map.put("code", exception.nStatusCode);map.put("msg", exception.errorCause);map.put("detail", exception.getMessage());String retStr = JSON.toJSONString(map);writer.print(retStr);writer.flush();} catch (Exception e) {log.error(e.getMessage());} finally {if (is != null) {try {is.close();} catch (IOException e) {e.printStackTrace();}}if (writer != null) {writer.close();}}return null;}protected ZuulException findZuulException(Throwable throwable) {if (Objects.isNull(throwable)) {return null;}if (throwable.getCause() instanceof ZuulRuntimeException) {Throwable cause = null;if (throwable.getCause().getCause() != null) {cause = throwable.getCause().getCause().getCause();}if (cause instanceof ClientException && cause.getCause() != null&& cause.getCause().getCause() instanceof SocketTimeoutException) {ZuulException zuulException = new ZuulException("", 504,ZuulException.class.getName() + ": Hystrix Readed time out");return zuulException;}if (throwable.getCause().getCause() instanceof ZuulException) {return (ZuulException) throwable.getCause().getCause();}}if (throwable.getCause() instanceof ZuulException) {return (ZuulException) throwable.getCause();}if (throwable instanceof ZuulException) {return (ZuulException) throwable;}return new ZuulException(throwable, HttpStatus.INTERNAL_SERVER_ERROR.value(), null);}
}

总结

目前熔断和重新写Error filter 基本可以保证高并发下不发生连接泄露,但是要是性能追求更高 可以使用Nocos、Zuul2.x 等基于Netty 的网关框架。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/418529.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

React入门 - 08(组件拆分组件传值)

本章内容 目录 父组件向子组件传递数据子组件向父组件传递数据 上一节内容我们补充l了在 React使用 JSX语法的一些细节。本节我们继续使用 ”TodoList“ 案例来讲解一下”组件拆分与组件传值“ 父组件向子组件传递数据 打开一开始我们已经创建好的工程&#xff0c;现在我们用…

极狐GitLab 线下『 DevOps专家训练营』成都站开班在即

成都机器人创新中心联合极狐(GitLab)隆重推出极狐GitLab DevOps系列认证培训课程。该课程主要面向使用极狐GitLab的DevOps工程师、安全审计人员、系统运维工程师、系统管理员、项目经理或项目管理人员&#xff0c;完成该课程后&#xff0c;学员将达到DevOps的专家级水平&#x…

算法训练 day24 | 77. 组合

77. 组合 题目链接:组合 视频讲解:带你学透回溯算法-组合问题 回溯其实和递归是密不可分的&#xff0c;解决回溯问题标准解法也是根据三部曲来进行的。 1、递归函数的返回值和参数 对于本题&#xff0c;我们需要用一个数组保存单个满足条件的组合&#xff0c;还需要另一个结果数…

Bit.Store 加密卡集成主流 BRC20通证,助力 BTC 生态流动性

“Bit.Store 首创性的将包括 ORDI、SATS、以及 RATS 在内的主流 BRC20 资产集成到其加密卡支付中&#xff0c;通过以其推出的加密银行卡为媒介&#xff0c;助力 BTC 生态 Token 的流动性与消费。” 比特币网络在被设计之初&#xff0c;就是以一种去中心化、点对点的现金系统为定…

数据结构之二叉树的性质与存储结构

数据结构之二叉树的性质与存储结构 1、二叉树的性质2、二叉树的存储结构 数据结构是程序设计的重要基础&#xff0c;它所讨论的内容和技术对从事软件项目的开发有重要作用。学习数据结构要达到的目标是学会从问题出发&#xff0c;分析和研究计算机加工的数据的特性&#xff0c;…

python写完程序怎么运行

python有两种运行方式&#xff0c;一种是在python交互式命令行下运行; 另一种是使用文本编辑器直接在命令行上运行。 注&#xff1a;以上两种运行方式均由CPython解释器编译运行。 当然&#xff0c;也可以将python代码写入eclipse中&#xff0c;用JPython解释器运行&#xff0c…

用el-image-viewer实现全局预览图片

背景 在后台管理系统中&#xff0c;一些预览图片的场景&#xff0c;通常都是使用 el-image-viewer 去实现&#xff0c;但是如果多个地方都需要预览图片&#xff0c;又要重复的去写 el-image-viewer 以及一些重复的和预览相关的代码。 可以把预览图片的组件放在根文件&#x…

MySQL主从集群

MySQL主从集群 主从模式、集群模式&#xff0c;都是在一个项目中使用多个mysql节点进行存储和读取数据。 当单机模式部署&#xff0c;不满足安全性、高可用、高并发等需求的时候&#xff0c;就需要考虑主从模式或者集群模式部署。 什么是主从模式&#xff1f; 主从模式&…

Hadoop基础知识

Hadoop基础知识 1、Hadoop简介 广义上来说&#xff0c;Hadoop通常是指一个更广泛的概念——Hadoop生态圈。狭义上说&#xff0c;Hadoop指Apache这款开源框架&#xff0c;它的核心组件有&#xff1a; HDFS&#xff08;分布式文件系统&#xff09;&#xff1a;解决海量数据存储Y…

EasyRecovery2024数据恢复大师最新版本下载

EasyRecovery可以从初始化的磁盘恢复损坏或删除的文件。该软有易于使用&#xff0c;即使是最缺乏经验的用户也可以轻松恢复数据。一款威力非常强大的硬盘数据恢复工具。能够帮你恢复丢失的数据以及重建文件系统。EasyRecovery 不会向你的原始驱动器写入任何东东&#xff0c;它主…

Canny边缘检测 双阈值检测理解

问题引入 我们用一个实际例子来引入问题 import cv2 import numpy as npimgcv2.imread("test.png",cv2.IMREAD_GRAYSCALE) # 修改图像大小 show cv2.resize(img,(500,500))v1cv2.Canny(show,120,250) v2cv2.Canny(show,50,100)# 连接图像 res np.hstack((v1,v2)…

b+树的理解

二叉树&#xff1a; 每个节点支持两个分支的树结构&#xff0c;相比于单向链表&#xff0c;多了一个分支。 二叉查找树&#xff1a; 在二叉树的基础上增加了一个规则&#xff0c;左子树的所有节点都小于它的根节点&#xff0c;右子树的所有节点都大于他的根节点。 二叉查找树…