听说你解决了长达半年的网关内存泄漏问题?

1,460 阅读5分钟

文章将占用您宝贵的五分钟

背景

所谓微服务网关的含义其它文章介绍的也相对齐全,我这次着重讲讲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的引用计数器只有一份引用存在,也就可以完美的传递给下一个链路继续使用了! image.png

看到此处相信还是有一些萌萌的听油们,我后边会针对这个在做一个补充试文章,主要是演示如何将应用的堆外内存搞起来以及如果排查堆外内存!先在这里预告一波!

    启动参数: -Dio.netty.leakDetection.level=paranoid -XX:NativeMemoryTracking=detail -Dspring.profiles.active=local -XX:MaxDirectMemorySize=8M

好了,感谢听油们看到这里,辛苦!

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿