问题描述
某日告警群突发OOM日志告警:
| 1
 | java.lang.OutOfMemoryError: unable to create new native thread
 | 
开发、运维、运营相关人员立马被召集了起来,首先确认了业务未受到明显的影响。
定位问题
我首先回想最近该服务更新的内容,但改动并不多,而且距离最近一次发版也有一定的时间了。
运维人员则确认告警的服务后,打算登入对应的服务器一探究竟,但无奈发现服务器也登录不进去,一时范了难。
回想告警的内容,是因为无法创建线程,合理猜测会不会是服务器的线程已经爆满了,导致物理机也受到影响。于是建议运维首先将部署在该物理机上的其他一个服务杀掉(考虑到保留线程信息),以释放掉部分线程出来,尝试后终于登进去了。
登录进去后,查看该服务器总线程数,达到了近3w,确认了问题是因为线程爆满导致,再针对告警的服务,使用top指定对应的进程:
但发现该服务占用线程数并不高,只有不到200个;于是针对部署在该物理机上的服务,逐个进行排查,最终发现是服务网关gateway导致,其线程数达到了2w多个。
知道是gateway导致后,则立马进入到该gateway所在pod,使用jstack输出该服务进程的堆栈信息:
导出堆栈信息后,重启gateway服务。
这边开始分析堆栈信息,发现有大量的命名为boundedElastic-evictor-xxx的线程,而且都处理TIMED_WAITING状态。
于是打开gateway服务的代码工程(spring-cloud-starter-gateway为3.0.0版本):
尝试全局搜索boundedElastic关键字的类,发现了BoundedElasticScheduler这个类,查看其源代码,发现里面有这么一个线程工厂静态对象,其创建的线程命名与堆栈信息输出的一致:
| 12
 3
 4
 5
 
 | static final ThreadFactory EVICTOR_FACTORY = r -> {Thread t = new Thread(r, Schedulers.BOUNDED_ELASTIC + "-evictor-" + EVICTOR_COUNTER.incrementAndGet());
 t.setDaemon(true);
 return t;
 };
 
 | 
于是逐一查看其调用的位置,一路找一路排除,来到了DefaultPartHttpMessageReader:
| 12
 
 | private Scheduler blockingOperationScheduler = Schedulers.newBoundedElastic(Schedulers.DEFAULT_BOUNDED_ELASTIC_SIZE,Schedulers.DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, IDENTIFIER, 60, true);
 
 | 
又一路找,来到了创建DefaultPartHttpMessageReader的来源ServerDefaultCodecsImpl#extendTypedReaders:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | @Overrideprotected void extendTypedReaders(List<HttpMessageReader<?>> typedReaders) {
 if (this.multipartReader != null) {
 addCodec(typedReaders, this.multipartReader);
 return;
 }
 DefaultPartHttpMessageReader partReader = new DefaultPartHttpMessageReader();
 addCodec(typedReaders, partReader);
 addCodec(typedReaders, new MultipartHttpMessageReader(partReader));
 }
 
 | 
继续排查代码,发现来到DefaultServerWebExchange这里:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 
 | private static Mono<MultiValueMap<String, String>> initFormData(ServerHttpRequest request,ServerCodecConfigurer configurer, String logPrefix) {
 
 try {
 MediaType contentType = request.getHeaders().getContentType();
 if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType)) {
 return ((HttpMessageReader<MultiValueMap<String, String>>) configurer.getReaders().stream()
 .filter(reader -> reader.canRead(FORM_DATA_TYPE, MediaType.APPLICATION_FORM_URLENCODED))
 .findFirst()
 .orElseThrow(() -> new IllegalStateException("No form data HttpMessageReader.")))
 .readMono(FORM_DATA_TYPE, request, Hints.from(Hints.LOG_PREFIX_HINT, logPrefix))
 .switchIfEmpty(EMPTY_FORM_DATA)
 .cache();
 }
 }
 catch (InvalidMediaTypeException ex) {
 // Ignore
 }
 return EMPTY_FORM_DATA;
 }
 
 @SuppressWarnings("unchecked")
 private static Mono<MultiValueMap<String, Part>> initMultipartData(ServerHttpRequest request,
 ServerCodecConfigurer configurer, String logPrefix) {
 
 try {
 MediaType contentType = request.getHeaders().getContentType();
 if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType)) {
 return ((HttpMessageReader<MultiValueMap<String, Part>>) configurer.getReaders().stream()
 .filter(reader -> reader.canRead(MULTIPART_DATA_TYPE, MediaType.MULTIPART_FORM_DATA))
 .findFirst()
 .orElseThrow(() -> new IllegalStateException("No multipart HttpMessageReader.")))
 .readMono(MULTIPART_DATA_TYPE, request, Hints.from(Hints.LOG_PREFIX_HINT, logPrefix))
 .switchIfEmpty(EMPTY_MULTIPART_DATA)
 .cache();
 }
 }
 catch (InvalidMediaTypeException ex) {
 // Ignore
 }
 return EMPTY_MULTIPART_DATA;
 }
 
 | 
看了代码后,初步确认是在处理application/x-www-form-urlencoded或multipart/form-data请求时发生的。
于是在测试环境,找了个这种请求,使用jmeter简单跑了下,果然会导致线程爆满的问题,至此问题元凶终于确认。
解决方法
解决方法也比较简单,就是使用WebFlux的自定义接口,配置上默认的multipart解析器:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | @Configurationpublic class WebConfig implements WebFluxConfigurer {
 
 @Override
 public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
 DefaultPartHttpMessageReader partReader = new DefaultPartHttpMessageReader();
 MultipartHttpMessageReader multipartReader = new MultipartHttpMessageReader(partReader);
 configurer.defaultCodecs().multipartReader(multipartReader);
 }
 
 }
 
 | 
针对该类问题,后续可以针对每个pod单独限制线程的上限,避免服务之间的相互影响。
对于服务创建的线程数,可以设定阈值,并添加到告警项,避免业务受损问题才暴露出来。