文章将占用您宝贵的五分钟
背景
所谓微服务网关的含义其它文章介绍的也相对齐全,我这次着重讲讲API Gateway实现鉴权的时候连带产生了哪些问题,哪些知识点是欠缺需要过后重新学习。
首先我们使用的是ShenYu网关框架,想要完整了解的朋友可以去官网学习下,其中的功能非常强大,值得瞻仰!
其中引用了两个在我看来相对不是很熟悉的框架,SpringCloud&webFlux对于Web编程框架,以及一个组件反应式编程项目,这三个东东作为底层数据传输的主要支撑,一下子就把我给搞懵了!现在回过头来看,主要还是没有不清楚上边三个内容的功能点,以及最主要的Netty通讯。
复现问题
正如标题所描述的,我所负责的网关应用线上出现了多次OOM事件,因为多台机器共同提供服务,而OOM也仅是一台机器无法对外提供服务,未对业务产生实际影响,但是作为富有责任心的开发,并且还是应用的Owner,我一定要将此处的臭虫排查出来,还一个健康稳定的应用!
要说如何复现,还要从如何将用户请求数据从请求体中读取讲起!
SpringMVC || SpringBoot
一般来讲我们对于mvn或者boot框架,可以新建一个类来继承HttpServletRequestWrapper然后重写读取流的方法,然后再将类传递给到拦截器作为下一次请求读取使用,从而达到避免流不可二次读取的问题,具体代码如下:
public class PostServletRequest extends HttpServletRequestWrapper {
private final String body;
public PostServletRequest(HttpServletRequest request) throws IOException {
super(request);
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
try {
InputStream inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
} else {
stringBuilder.append(StringUtils.EMPTY);
}
} catch (IOException ex) {
throw ex;
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException ex) {
throw ex;
}}}
body = stringBuilder.toString();
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
ServletInputStream servletInputStream = new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
return servletInputStream;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
public String getBody() {
return this.body;
}
}
public class CustomInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
PostServletRequest postServletRequest = new PostServletRequest(request);
String data = postServletRequest.getBody();
Map<String, String> params = null;
try {
params = JSONObject.parseObject(data, new TypeReference<HashMap<String, String>>() {});
checkoutSign(params);
} catch (Exception e) {
log.error("签名错误,data:{}", data);
}
return super.preHandle(postServletRequest, response, handler);
}
SpringCloud
由于项目中更多使用的微服务或者网关,此时如何将请求体读取出来呢?
最初的代码片段,大家可以猜下,下面的代码片段是否存在内存泄漏问题
```
public class CacheBodyGlobalFilter implements Ordered, WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if (exchange.getRequest().getHeaders().getContentType() == null) {
return chain.filter(exchange);
} else {
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
DataBufferUtils.retain(dataBuffer);
Flux<DataBuffer> cachedFlux = Flux
.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(
exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
DataBufferUtils.release(dataBuffer);
return chain.filter(exchange.mutate().request(mutatedRequest).build());
});
}
}
@Override
public int getOrder() {
return -1000;
}
}
```
public final class OpenApiRequestFilter implements WebFilter{
@Override
public Mono<Void> filter(@Nullable final ServerWebExchange exchange, @Nullable final WebFilterChain chain) {
String urlPath = Objects.requireNonNull(exchange).getRequest().getURI().getPath();
String bodyStr = RequestUtil.resolveBodyFromRequest(exchange.getRequest());
try {
sign(bodyStr);
} catch (ValidateException | SystemException e) {
return WebFluxResultUtils.result(exchange, Result.buildFail(e.getMessage()));
}
return chain.filter(exchange);
}
```
// 读取请求体的方式
public static String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest){
//获取请求体
Flux<DataBuffer> body = serverHttpRequest.getBody();
StringBuilder sb = new StringBuilder();
body.subscribe(buffer -> {
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
String bodyString = new String(bytes, StandardCharsets.UTF_8);
sb.append(bodyString);
});
return sb.toString();
}
相信看到这里大佬已经发现了问题所在,然后我也将上边的一些工具类方法进行介绍,哪怕上边你没有看出来问题,跟着介绍走下去,一定会转角发现爱!
组件的背景
网关应用集成了webFlux框架,使用的是ServerWebExchange包装的请求体,读取请求体的方式getRequest().getBody()==>Flux,DataBuffer是请求体数据,需要从DataBuffer里将数据读取出来,DataBuffer==>使用的NettyDataBuffer==>持有ByteBuf引用,这个引用的创建删除是基于计数器实现的
dataBuffer.slice(0, dataBuffer.readableByteCount())
这行代码是拷贝出来一份共享底层ByteBuf 字节数组的对象,它同时也是一个新的DataBuffer对象,它的读取写入不会影响原dataBuffer对象的读写指针
DataBufferUtils.retain(dataBuffer);
这段代码是将dataBuffer的引用计数增加2,理论上来讲,retain一次,要跟着release一次,否则会出现dataBuffer无法释放导致内存泄漏
DataBufferUtils.release(dataBuffer);
这段代码是将dataBuffer的引用计数器减少2
emmm,听油们,如果到这里还没有点内存泄漏的意思的话,那说明还得上code
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
// 将引用计数增加2,保证原始的dataBuffer不会被释放掉
DataBufferUtils.retain(dataBuffer);
DataBuffer tmpDataBuffer = dataBuffer.slice(0, dataBuffer.readableByteCount());
byte[] bytes = new byte[tmpDataBuffer.readableByteCount()];
tmpDataBuffer.read(bytes);
// 将复制出来的tmpDataBuffer的计数器减少2,这个的同时也会将原始ataBuffer减少2
DataBufferUtils.release(tmpDataBuffer);
String body = Strings.fromUTF8ByteArray(bytes);
// 这个位置将请求体缓存起来,后续可任意读取使用
exchange.getAttributes().put(ConstantUtil.CACHE_REQUEST_BODY_OBJECT_KEY, body);
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(
exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return Flux.just(dataBuffer);
}
};
return chain.filter(exchange.mutate().request(mutatedRequest).build());
});
结论
看完上边的代码后,我在露出来一张图,这个是slice以后的状态,上边的refCnt是原始dataBuffer retain后的计数器,下面是拷贝出来的一份dataBuffer recCnt;然后在将tmpDataBuffer的计数器减少2的同时也会将原始ataBuffer减少2,这样在我看来就保证了ByteBuf的引用计数器只有一份引用存在,也就可以完美的传递给下一个链路继续使用了!
看到此处相信还是有一些萌萌的听油们,我后边会针对这个在做一个补充试文章,主要是演示如何将应用的堆外内存搞起来以及如果排查堆外内存!先在这里预告一波!
启动参数: -Dio.netty.leakDetection.level=paranoid -XX:NativeMemoryTracking=detail -Dspring.profiles.active=local -XX:MaxDirectMemorySize=8M
好了,感谢听油们看到这里,辛苦!
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。