【トラブル対応】WebClientでメモリリーク?

SpringWebFluxのHTTPクライアントのWebClientですが、
使い方次第でメモリリークの可能性があるので知っていることをまとめておきます!

前提

  • SpringBoot:2.7.6
  • actuatorを一緒に利用

build.gradleはこんな感じ

plugins {
	id 'java'
	id 'org.springframework.boot' version '2.7.6'
	id 'io.spring.dependency-management' version '1.1.0'
}

group = 'hirabay'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-webflux'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-actuator'
	implementation 'io.micrometer:micrometer-registry-prometheus'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
}

tasks.named('test') {
	useJUnitPlatform()
}

NG例①

@RequiredArgsConstructor
public class WebClientWrapper {
    private final WebClient.Builder webClientBuilder;

    public Mono<String> ng1() {
        return webClientBuilder
                .baseUrl("http://localhost:8080")
                .build()
                .get()
                .uri("/api/sample?uuid=" + UUID.randomUUID().toString())
                .retrieve()
                .bodyToMono(String.class);
    }
}
$ curl -s http://localhost:8080/actuator/prometheus | grep ^http_client
http_client_requests_seconds_count{client_name="localhost",method="GET",outcome="SUCCESS",status="200",uri="/api/sample?uuid=c54f4ea5-6ebd-4122-bacb-b6212cb7eab6",} 1.0
http_client_requests_seconds_sum{client_name="localhost",method="GET",outcome="SUCCESS",status="200",uri="/api/sample?uuid=c54f4ea5-6ebd-4122-bacb-b6212cb7eab6",} 0.003281917
http_client_requests_seconds_count{client_name="localhost",method="GET",outcome="SUCCESS",status="200",uri="/api/sample?uuid=e03dab0e-88cb-48fa-a520-ef8fa3a4db0f",} 1.0
http_client_requests_seconds_sum{client_name="localhost",method="GET",outcome="SUCCESS",status="200",uri="/api/sample?uuid=e03dab0e-88cb-48fa-a520-ef8fa3a4db0f",} 0.054425584
http_client_requests_seconds_max{client_name="localhost",method="GET",outcome="SUCCESS",status="200",uri="/api/sample?uuid=c54f4ea5-6ebd-4122-bacb-b6212cb7eab6",} 0.003281917
http_client_requests_seconds_max{client_name="localhost",method="GET",outcome="SUCCESS",status="200",uri="/api/sample?uuid=e03dab0e-88cb-48fa-a520-ef8fa3a4db0f",} 0.0

uriにパラメータが埋め込まれており、パラメータごとにメトリクスが増えていきます。
UUIDのように毎回値が変わるような場合だと処理が行われるたびにメモリを食い潰していきます😱😱😱

NG例②

@RequiredArgsConstructor
public class WebClientWrapper {
    private final WebClient.Builder webClientBuilder;

    public Mono<String> ng2() {
        return webClientBuilder
                .baseUrl("http://localhost:8080")
                .build()
                .get()
                .uri(uriBuilder -> uriBuilder.path("/api/sample")
                        .queryParam("uuid", UUID.randomUUID().toString())
                        .build())
                .retrieve()
                .bodyToMono(String.class);
    }
}

builder使えばqueryParamでパラメータ切り出してるしイケるやろ…!

$ curl -s http://localhost:8080/actuator/prometheus | grep ^http_client
http_client_requests_seconds_count{client_name="localhost",method="GET",outcome="SUCCESS",status="200",uri="/api/sample?uuid=0ce9d034-76e3-4bde-9607-1338b12e7aba",} 1.0
http_client_requests_seconds_sum{client_name="localhost",method="GET",outcome="SUCCESS",status="200",uri="/api/sample?uuid=0ce9d034-76e3-4bde-9607-1338b12e7aba",} 0.003851958
http_client_requests_seconds_count{client_name="localhost",method="GET",outcome="SUCCESS",status="200",uri="/api/sample?uuid=253e67c4-4e85-4e5c-b770-d1380a6fbceb",} 1.0
http_client_requests_seconds_sum{client_name="localhost",method="GET",outcome="SUCCESS",status="200",uri="/api/sample?uuid=253e67c4-4e85-4e5c-b770-d1380a6fbceb",} 0.06888125
http_client_requests_seconds_max{client_name="localhost",method="GET",outcome="SUCCESS",status="200",uri="/api/sample?uuid=0ce9d034-76e3-4bde-9607-1338b12e7aba",} 0.003851958
http_client_requests_seconds_max{client_name="localhost",method="GET",outcome="SUCCESS",status="200",uri="/api/sample?uuid=253e67c4-4e85-4e5c-b770-d1380a6fbceb",} 0.06888125

ダメでした。。。

uriBuilderはURLの生成を補助してくれるだけで、NG例①とやっていることは変わらないってことですね。。。

OK例

@RequiredArgsConstructor
public class WebClientWrapper {
    private final WebClient.Builder webClientBuilder;

    public Mono<String> ok() {
        return webClientBuilder
                .baseUrl("http://localhost:8080")
                .build()
                .get()
                .uri("/api/sample?uuid={uuid}", UUID.randomUUID().toString())
                .retrieve()
                .bodyToMono(String.class);
    }
}
$ curl -s http://localhost:8080/actuator/prometheus | grep ^http_client
http_client_requests_seconds_count{client_name="localhost",method="GET",outcome="SUCCESS",status="200",uri="/api/sample?uuid={uuid}",} 2.0
http_client_requests_seconds_sum{client_name="localhost",method="GET",outcome="SUCCESS",status="200",uri="/api/sample?uuid={uuid}",} 0.058212042
http_client_requests_seconds_max{client_name="localhost",method="GET",outcome="SUCCESS",status="200",uri="/api/sample?uuid={uuid}",} 0.055181458

メモ
uriメソッドで、第一引数にURLフォーマットを、第二引数以降に変数を指定しましょう!

UriBuilerを使いたい場合

事前にuriフォーマットを生成するために使用しましょう。

    public Mono<String> ok2() {
        var uri = UriComponentsBuilder.fromPath("/api/sample")
                .queryParam("uuid", "{uuid}")
                .build().toUriString();

        return webClientBuilder
                .baseUrl("http://localhost:8080")
                .build()
                .get()
                .uri(uri, UUID.randomUUID().toString())
                .retrieve()
                .bodyToMono(String.class);
    }

SpringBoot 3.0だと・・・

NG例①だと同じくNGですが、NG例②(uriBuilderのパターン)はメモリリークの対策かuriがnoneに丸め込まれていました。

$ curl -s http://localhost:8080/actuator/prometheus | grep ^http_client
http_client_requests_active_seconds_active_count{exception="none",method="none",outcome="UNKNOWN",status="CLIENT_ERROR",uri="none",} 0.0
http_client_requests_active_seconds_duration_sum{exception="none",method="none",outcome="UNKNOWN",status="CLIENT_ERROR",uri="none",} 0.0
http_client_requests_active_seconds_max{exception="none",method="none",outcome="UNKNOWN",status="CLIENT_ERROR",uri="none",} 0.0
http_client_requests_seconds_count{error="none",exception="none",method="GET",outcome="SUCCESS",status="200",uri="none",} 3.0
http_client_requests_seconds_sum{error="none",exception="none",method="GET",outcome="SUCCESS",status="200",uri="none",} 0.060628958
http_client_requests_seconds_max{error="none",exception="none",method="GET",outcome="SUCCESS",status="200",uri="none",} 0.052780208

ただ、これだとメトリクスとしての価値が下がってしまうので、OK例のような変更をするのがオススメです!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です