前言
前两天接到用户反馈,某个业务的列表出不来了。这个业务的链路上是前端调用我们的服务,我们再调用第三方的服务,并对结果进行包装后返回。
查看了一下相应请求的日志,发现错误的来源是DataBufferLimitException
从直觉上感觉是返回的报文太大了,便让前端同事尝试把分页的列表数量调小点,调整后接口返回正常。
不过,用过蛮多的Http请求工具,确实还没遇到过因为返回报文过大而报错的问题,便看了看这次的代码,准备研究一番。
异常定位
查看了一下本次出问题的接口使用的Http请求工具,是用的Spring WebFlux封装的WebClient,代码如下
1 | Mono<String> resp = WebClient.create().post() |
乍一看是默认配置,难道默认配置的大小就只有262144?这个值是从哪儿来的呢?
通过堆栈,可以定位到这个错误是从org.springframework.core.io.buffer.LimitedDataBufferList#raiseLimitException中产生。
1 | private void raiseLimitException() { |
maxByteCount是LimitedDataBufferList的属性,值声明为final类型,因此他一定是初始化的时候产生的,查看其构造方法,值由外部传入。
1 | public class LimitedDataBufferList extends ArrayList<DataBuffer> { |
查看构造方法的调用方,一共有两处,一个是从org.springframework.core.codec.StringDecoder#decode(org.reactivestreams.Publisher<org.springframework.core.io.buffer.DataBuffer>, org.springframework.core.ResolvableType, org.springframework.util.MimeType, java.util.Map<java.lang.String,java.lang.Object>)中构建,一个是从org.springframework.core.io.buffer.DataBufferUtils#join(org.reactivestreams.Publisher<? extends org.springframework.core.io.buffer.DataBuffer>, int)中构建
StringDecoder#decode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Flux<String> decode(Publisher<DataBuffer> input, ResolvableType elementType,
MimeType mimeType, Map<String, Object> hints) {
byte[][] delimiterBytes = getDelimiterBytes(mimeType);
LimitedDataBufferList chunks = new LimitedDataBufferList(getMaxInMemorySize());
DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(delimiterBytes);
return Flux.from(input)
.concatMapIterable(buffer -> processDataBuffer(buffer, matcher, chunks))
.concatWith(Mono.defer(() -> {
if (chunks.isEmpty()) {
return Mono.empty();
}
DataBuffer lastBuffer = chunks.get(0).factory().join(chunks);
chunks.clear();
return Mono.just(lastBuffer);
}))
.doOnTerminate(chunks::releaseAndClear)
.doOnDiscard(PooledDataBuffer.class, PooledDataBuffer::release)
.map(buffer -> decode(buffer, elementType, mimeType, hints));
}这里的代码比较简单,主要关注getMaxInMemorySize()的方法,这个方法是其父类
org.springframework.core.codec.AbstractDataBufferDecoder提供的,获取的是maxInMemorySize属性的值,这个值初始化为256*1024,即262144
DataBufferUtils#join
这个方法的代码比较复杂,主要关注
maxByateCount是从调用者传参进来的,继续跟踪1
2
3
4
5
6
7
8
9
10
11
12
13public static Mono<DataBuffer> join(Publisher<? extends DataBuffer> buffers, int maxByteCount) {
Assert.notNull(buffers, "'dataBuffers' must not be null");
if (buffers instanceof Mono) {
return (Mono<DataBuffer>) buffers;
}
return Flux.from(buffers)
.collect(() -> new LimitedDataBufferList(maxByteCount), LimitedDataBufferList::add)
.filter(list -> !list.isEmpty())
.map(list -> list.get(0).factory().join(list))
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release);
}
AbstractDataBufferDecoder是刚才StringDecoder的父类,因此大小跟追踪StringDecoder的情况一致,其余的调用方均传输了类定义的属性maxInMemorySize,大小均为256*1024
由此可确认,默认值就是256*1024,即262144
怎么改?
定位到了数据的来源,就比较好完成修改了。查看这些maxInMemorySize属性,基本上都配有setMaxInMemorySize的方法,追踪这个方法,均可以到达org.springframework.http.codec.support.BaseDefaultCodecs#initCodec的方法。
可以看到,当org.springframework.http.codec.support.BaseDefaultCodecs#maxInMemorySize不为null时,codec无论是什么类型,都能完成setMaxMessageSize的操作。因此接下来查看如何修改BaseDefaultCodecs#maxInMemorySize。
除去对象复制类,BaseDefaultCodecs#maxInMemorySize值的修改只能通过org.springframework.http.codec.support.BaseDefaultCodecs#maxInMemorySize(int)完成。查看这个方法的注释
默认值是256k,如果不限制的话是-1。跟我们刚才追踪代码的结果一致,但怎么修改,没有明确的注释。
查看org.springframework.http.codec.CodecConfigurer.DefaultCodecs#maxInMemorySize的引用,也没有找到合适的调用方。因此猜测,这里就是顶层的入口,如果需要修改,我们需要调用这个方法。
但怎么调用呢,查看``org.springframework.http.codec.CodecConfigurer`的注释
注释里说,创建一个实例,可以通过ClientCodecConfigurer. create()或者 ServerCodecConfigurer.create()方法,我们是客户端方,从命名上看,应该是ClientCodecConfigurer.create()。
查看这个方法的引用关系,发现只有一个入口(其余都是注释),来自org.springframework.web.reactive.function.client.DefaultExchangeStrategiesBuilder的构造方法。
再查看这个构造方法的引用,可以看到有个DEFAULT_EXCHANGE_STRATEGIES的静态变量,看看这个静态变量的引用关系,定位到org.springframework.web.reactive.function.client.ExchangeStrategies#withDefaults
再查看这个方法的引用,可以看到一个Builder结尾的类org.springframework.web.reactive.function.client.DefaultWebClientBuilder,方法initExchangeStrategies()
1 | private ExchangeStrategies initExchangeStrategies() { |
这里的逻辑跟DefaultWebClientBuilder中strategies和strategiesConfigurers属性相关,查看这两个变量的引用方法及其注释
可以推断出,org.springframework.web.reactive.function.client.WebClient.Builder#codecs是更推荐的设置方法,当然org.springframework.web.reactive.function.client.WebClient.Builder#exchangeStrategies()也是可选择的。
总结刚才的代码定位,也就是如果我们需要修改maxInMemorySize的大小,可以通过WebClient构造时调用codecs方法实现,即
1 | Mono<String> resp = WebClient.builder().codecs(codecs->codecs.defaultCodecs().maxInMemorySize(-1)).build().post() |
碎碎念
解决这个问题需要这么复杂吗,其实并不用。打开搜索引擎,输入DataBufferLimitException,随手翻翻就能找到解决方法,例如Baeldung的How to Resolve Spring Webflux DataBufferLimitException,基本上从是什么为什么怎么办都做了详细的解释。如果你用上了大模型的代码助手,只要点点报错的堆栈,大模型也会告诉你怎么办。
实际上,并不需要花时间去翻代码,善于利用工具,1分钟不到就解决完事儿了。
这是好事,也是坏事。
当下的我们在快节奏的生活下,总是在追求怎么办,而忽略了是什么和为什么。这会导致问题没有被根本性的解决——都不知道问题从哪儿来,怎么能根治问题呢?
我身边不少项目充斥着从搜索引擎复制粘贴过来的代码,细问他们为啥会拷这份代码,他们表达的观点大体都是: 遇到了一个问题,不知道怎么解决,然后搜索引擎一查说可以这么做,就拷过来了,发现诶好像确实能解决。但至于为什么这样可以解决,他们并不知道。
而这份代码存在的问题,埋下的大雷,他们更不知道。
当这个大雷爆发时,他们再去求助搜索引擎,发现找不到他们需要的答案时,便陷入了僵局。
程序员能力的高低,分水岭在我看来就是是否有独立思考和独立解决问题的能力。如果遇到需求遇到问题只能借助搜索引擎来得到答案,那是得不到成长的。搜索引擎得到的结果也都是其他程序员总结的,但案例确是不尽相同的。当有一天搜索引擎得到的案例方案跟实际遇到的有所出入,当有一天遇到了一个别人都没有遇到过的问题,难道我们只能束手无策吗?
我很喜欢的一句话是
源码之下无秘密
当我们对一个东西十分了解之后,当我们能知道是什么之后,对于它可能出现的问题,对于产生的为什么,也就不会那么束手无策了。
AI这几年兴起后,我对象曾问我:”你们程序员会被AI取代掉吗?”
实话说,我曾经为这个问题陷入思考,隐隐觉得自己马上就是个失业人口。
后来接触了一些生成式AI的产品,以及厂商们推出的代码助手后,我有了一些不一样的想法。
这些助手们,在当下的能力,还是只能做一个助手——你可以告诉他问题,也可以让他帮忙解释和修正,但你让他完完整整的对一个业务问题从设计到编码再到测试形成一个完整的方案,他还做不到。
对于一些简单的问题进行编码,这些助手还是绰绰有余的。但他们的思考,也好像是停留在面向过程的方式思考。让他用面对对象的编码思路去思考问题,去定义接口和抽象类,去运用模板模式做逻辑拆分,他们就开始有点吃力了。
忘了是谁跟我提过一句说要把当下的大模型当做实习生去对待,我觉得这句话很在理。你可以告诉他你需要什么得怎么做,也可以让他去做汇总做报告,他都能圆满的完成。但如果你让他自己去从头到尾操心一个事情,,站在更高的角度去思考一些问题,大模型就无法完全胜任了。
曾经跟一个AI厂商的售前交流,他说他们公司的人基本上把它们的产品当做搜索引擎用了,有什么问题通过自然语言进行询问,大模型联网总结结果,不用再通过人工去对结果进行筛选了。
想到十几年前搜索东西还得按搜索引擎的思考方式给到关键词,想到刚提到的面向搜索引擎编程,我觉得大模型真的不是昙花一现,他是技术进步到一定阶段的成果。但让他一蹴而就完成对一个职业的取代,在我看来是比较困难的。
不过,趋势来看,是有可能的,只是他会从最简单最底层最重复的工作开始。例如,面向搜索引擎编程的我们。