Spring Cloud Gateway 彻底解决Exceeded limit on max bytes to buffer : 262144报错问题

一、问题描述

使用Spring Cloud Gateway开发内部API网关时,当业务的Http请求体大小超过256K时,会出现如下报错:Exceeded limit on max bytes to buffer : 262144。

Spring Boot框架给了两种方式来修改这个大小的方式:
方式一:使用修改配置参数值,spring.max-in-memory-size: 1024 * 1024 的方式

spring:
  application:
    name: gateway
  codec:
    max-in-memory-size: 1024 * 1024

方式二:使用WebFluxConfigurer,通过set方法设置max-in-memory-size

	@Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        configurer.defaultCodecs().maxInMemorySize((int) maxInMemorySize.toBytes());
    }

但在实际上线以后发现并为解决256K的限制,这是为什么呢?

二、问题定位

首先排查了SpringBoot是如何使用spring.max-in-memory-size这个参数的,当业务在启动时候设置的参数到底是怎么执行的。

第一步:SpringBoot首先会自动加载autoconfigure包下面的CodecsAutoConfiguration类:

在这里插入图片描述这个类主要是加载Encoders和Decoders,如果我们使用的application/json的请求格式,并且项目里面也存在jackson2的包,则SpringBoot会加载,Jackson2JsonDecoder 和Jackson2JsonEncoder:

org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(ObjectMapper.class)
	static class JacksonCodecConfiguration {

		@Bean
		@Order(0)
		@ConditionalOnBean(ObjectMapper.class)
		CodecCustomizer jacksonCodecCustomizer(ObjectMapper objectMapper) {
			return (configurer) -> {
				CodecConfigurer.DefaultCodecs defaults = configurer.defaultCodecs();
				// 这里会new一个Jackson2JsonDecoder
				defaults.jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper, EMPTY_MIME_TYPES));
				defaults.jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper, EMPTY_MIME_TYPES));
			};
		}

	}

第二步:将设置的Jackson2JsonDecoder交给org.springframework.http.codec.support.BaseDefaultCodecs类去做初始化的工作:

在这里插入图片描述
在这里插入图片描述通过追踪到这里,我们可以看出来是执行了setMaxInMemorySize方法的,那么我们只需要知道这个set之前的值是从哪里来的。

第三步:setMaxInMemorySize修改参数

如果通过上述的两个方式来修改setMaxInMemorySize,会调用BaseDefaultCodecs的maxInMemorySize方法来设置的:

org.springframework.http.codec.support.BaseDefaultCodecs

 @Override
	public void maxInMemorySize(int byteCount) {
		if (!ObjectUtils.nullSafeEquals(this.maxInMemorySize, byteCount)) {
			this.maxInMemorySize = byteCount;
			initReaders();
		}
	}

到这里可以确定的逻辑是,不管是通过哪种set maxInMemorySize的方式,都会执行Jackson2JsonDecoder父类AbstractJackson2Decoder的setMaxInMemorySize方法来修改maxInMemorySize这个私有属性:在这里插入图片描述
而这个maxInMemorySize默认的256 * 1024 就是控制256K请求体的大小。具体可以查看org.springframework.http.codec.json.AbstractJackson2Decoder的decodeToMono方法,这个方法负责将http请求的body转换成json格式:

@Override
	public Mono<Object> decodeToMono(Publisher<DataBuffer> input, ResolvableType elementType,
			@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {

		return DataBufferUtils.join(input, this.maxInMemorySize)
				.flatMap(dataBuffer -> Mono.justOrEmpty(decode(dataBuffer, elementType, mimeType, hints)));
	}

DataBufferUtils.join(input, this.maxInMemorySize)方法里面使用了LimitedDataBufferList:
在这里插入图片描述

如果超过了maxByteCount则会抛出raiseLimitException,这里就是异常具体抛出的地方:
在这里插入图片描述

第四步:通过工具排查执行setMaxInMemorySize以后maxInMemorySize

既然代码流程里面执行了setMaxInMemorySize,但是却没生效,那么这里到底执行以后maxInMemorySize的值是多少呢?

通过arthas工具查看了AbstractJackson2Decoder值:

// sc 获取classLoadHash:
sc -d org.springframework.http.codec.json.AbstractJackson2Decoder

// 获取value
[arthas@48]$ vmtool -c 18b4aac2 -a getInstances --className org.springframework.http.codec.json.AbstractJackson2Decoder --express '#val=instances[0].maxInMemorySize'
@Integer[1048576]
[arthas@48]$ vmtool -c 18b4aac2 -a getInstances --className org.springframework.http.codec.json.AbstractJackson2Decoder --express '#val=instances[1].maxInMemorySize'
@Integer[262144]
[arthas@48]$ vmtool -c 18b4aac2 -a getInstances --className org.springframework.http.codec.json.AbstractJackson2Decoder --express '#val=instances[2].maxInMemorySize'
@Integer[262144]
[arthas@48]$ vmtool -c 18b4aac2 -a getInstances --className org.springframework.http.codec.json.AbstractJackson2Decoder --express '#val=instances[3].maxInMemorySize'
@Integer[262144]
[arthas@48]$ vmtool -c 18b4aac2 -a getInstances --className org.springframework.http.codec.json.AbstractJackson2Decoder --express '#val=instances[4].maxInMemorySize'
@Integer[262144]
[arthas@48]$ vmtool -c 18b4aac2 -a getInstances --className org.springframework.http.codec.json.AbstractJackson2Decoder --express '#val=instances[5].maxInMemorySize'

通过获取多个AbstractJackson2Decoder实例的maxInMemorySize属性发现只有一个设置了1048576=1M,其他都是262144=256K,看到这里已经可以得出结论,不是setMaxInMemorySize没有生效,而是只生效了一个实例,因为初始化的时候只new了一个Jackson2JsonDecoder。

那其他的Jackson2JsonDecoder是从哪里来的?

第五步:查看Spring Cloud Gateway源码:源码地址

查看了Spring Cloud Gateway源码,发现了它给的代码中将Http Request Body 转换成Json的方式如下:

org.springframework.cloud.gateway.filter.factory.JsonToGrpcGatewayFilterFactory

在这里插入图片描述
在witeWith方法里面定义一个将Reques转换成Json的方法deserializeJSONRequest:
在这里插入图片描述
这里我们会发现如果你按照官方给的这种写法,每次请求进来会new Jackson2JsonDecoder,每次new出来的新对象是没有执行setMaxInMemorySize操作的,所以后期new的对象都默认使用了256 * 1024。

到此问题已经得到结论,不是SpringBoot框架setMaxInMemorySize没有生效,而是后期new的对象没有重新setMaxInMemorySize。

三、解决方案

如果要解决上述问题,我们需要保证每个新new的Jackson2JsonDecoder都能够使用设置的spring.max-in-memory-size,方法可以参考如下几种解决方案:

方案一:重写org.springframework.http.codec.json.AbstractJackson2Decoder

这种方法简单粗暴,直接将AbstractJackson2Decoder的maxInMemorySize改成spring.max-in-memory-size,通过自己写的AbstractJackson2Decoder覆盖SpringBoot框架里的AbstractJackson2Decoder,亲测有效。

在这里插入图片描述

方案二:将maxInMemorySize修改成static的私有属性

将maxInMemorySize改成static以后,则SpringBoot框架在初始化的时候执行一次setMaxInMemorySize则也能解决其他new的Jackson2JsonDecoder 对象maxInMemorySize不生效问题:
在这里插入图片描述

方案三:每次new Jackson2JsonDecoder对像时执行setMaxInMemorySize

private Flux<JsonNode> deserializeJSONRequest(ServerWebExchange exchange) {
        return exchange.getRequest().getBody().map(dataBufferBody -> {
            ResolvableType targetType = ResolvableType.forType(JsonNode.class);
            Jackson2JsonDecoder Jackson2JsonDecoder = new Jackson2JsonDecoder();
            // 每次创建对象以后需要设置setMaxInMemorySize
            Jackson2JsonDecoder.setMaxInMemorySize(1024 * 1024);
            return Jackson2JsonDecoder.decode(dataBufferBody, targetType, null, null);
        }).cast(JsonNode.class);
    }
当你在使用 IDEA 编译项目时遇到 "GC overhead limit exceeded" 错误时,这通常意味着 JVM(Java 虚拟机)花费了过多的时间进行垃圾回收,但仍然无法释放足够的内存。 以下是几种可能的解决方法: 1. 增加 JVM 内存限制:在 IDEA 的启动配置文件(idea.vmoptions 或 idea64.vmoptions)中增加内存限制参数,例如 `-Xmx4g`,将最大堆内存限制增加到 4GB。你可以根据你的项目需求和可用内存来调整这个值。 2. 优化代码和资源使用:检查你的代码和资源使用情况,尤其是内存密集型操作。确保你及时释放不再需要的对象,避免无限循环、内存泄漏等问题。 3. 分析内存使用情况:使用 JVM 监视工具(如 VisualVM、jstat 等)来分析内存使用情况,找出可能导致内存问题的代码部分。你可以检查是否有过多的对象创建、频繁的垃圾回收等情况。 4. 调整垃圾回收器参数:尝试调整垃圾回收器参数以改善垃圾回收性能。例如,可以尝试使用不同的垃圾回收器,或调整垃圾回收器的参数,如堆大小、新生代和老年代的比例等。 5. 检查依赖库和插件:某些依赖库或插件可能存在内存泄漏或其他问题,导致 JVM 内存消耗过高。尝试更新或删除可能引起问题的依赖库或插件。 6. 升级 IDEA 版本:如果你使用的是较旧的 IDEA 版本,尝试升级到最新版本,其中一些性能问题可能已经得到改进。 如果以上方法无法解决问题,你可以进一步搜索特定于你的项目和环境的解决方案,或者寻求专业人士的帮助。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值