문제
JMeter를 사용하여 테스트 중 NoHttpResponseException
가 발생했다.
org.apache.http.NoHttpResponseException: localhost:80 failed to respond
at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:141)
at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:56)
at org.apache.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:259)
at org.apache.http.impl.DefaultBHttpClientConnection.receiveResponseHeader(DefaultBHttpClientConnection.java:163)
at org.apache.http.impl.conn.CPoolProxy.receiveResponseHeader(CPoolProxy.java:157)
at org.apache.http.protocol.HttpRequestExecutor.doReceiveResponse(HttpRequestExecutor.java:273)
at org.apache.http.protocol.HttpRequestExecutor.execute(HttpRequestExecutor.java:125)
at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:272)
at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186)
at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89)
at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110)
at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83)
at org.apache.jmeter.protocol.http.sampler.HTTPHC4Impl.executeRequest(HTTPHC4Impl.java:940)
at org.apache.jmeter.protocol.http.sampler.HTTPHC4Impl.sample(HTTPHC4Impl.java:651)
at org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy.sample(HTTPSamplerProxy.java:66)
at org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase.sample(HTTPSamplerBase.java:1311)
at org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase.sample(HTTPSamplerBase.java:1300)
at org.apache.jmeter.threads.JMeterThread.doSampling(JMeterThread.java:651)
at org.apache.jmeter.threads.JMeterThread.executeSamplePackage(JMeterThread.java:570)
at org.apache.jmeter.threads.JMeterThread.processSampler(JMeterThread.java:501)
at org.apache.jmeter.threads.JMeterThread.run(JMeterThread.java:268)
at java.base/java.lang.Thread.run(Thread.java:840)
M1 Mac에서 테스트를 수행했으며 Thread Group은 다음과 같이 설정했다.
5000번 요청하는데 그 중 2.15%에서 에러가 발생했다.
원인
원인을 찾아보았을 때 HTTP/1.1 의 Keep-Alive 때문이라는 내용을 볼 수 있었다. 서버에서 통신이 완료되어 연결을 Close하더라도 Client에서는 여전히 Open된 상태인 상황이 될 수 있는데, 이를 half-closed connection
이라 표현한다고 한다. 이런 상황이 되면 JVM상 Connection은 살아있지만 내부 소켓은 CLOSE_WAIT
상태가 된다. 그리고 CLOSE-WAIT
상태가된 Connection을 다시 사용하려고 할 때 서버에서 이미 연결을 Close 해버렸기 때문에 NoHttpResponseException
이 발생하는 것이라고 한다.
해결 시도
Nginx
현재 WebSocket과 SSE 통신을 위해 Nginx에 HTTP/1.1과 keep-alive가 적용된 상황이다. 그리고 지속 연결이 필요없는 요청도 keep-alive가 적용되어있다.
위에서 알아본 원인을 통해 keep-alive를 유지할 필요가 없는 요청을 따로 분리하여 timeout을 짧게 설정했다. (편의를 위해 다른 부분은 생략한다.)
upstream backend {
server host.docker.internal:8080;
keepalive_timeout 0;
}
location ~ /(api|imgs) {
...
proxy_pass http://backend/$1$is_args$args;
proxy_set_header Connection '';
proxy_http_version 1.1;
...
}
하지만 결과는 달라지지 않았다. 에러가 더 많이 발생하거나 비슷한 비중으로 발생했다.
그럼에도 불필요한 keep-alive 연결을 다시 설정할 수 있는 기회가 되어서 의미있는 작업이라고 생각한다.
properties
jmeter의 properties 설정을 해주면 해결이 된다는 글을 보고 다음과 같이 파일을 수정했다. (각 파일에 추가/수정 해주면 된다.)
- bin/hc.parameters
- HttpClient는 stale check를 해줌으로써 서버 쪽에서 닫혀서 더이상 유효하지 않은 connection을 확인한다고 한다. 하지만 100% 신뢰성이 있지 않기 때문에 eviction policy를 적용하여 명시적으로 expired connection을 모두 닫아버리는 솔루션을 제안한다.
http.connection.stalecheck$Boolean=true
- bin/user.properties
- retrycount를 1로 설정해주면 1번 재시도해준다.
hc.parameters.file=hc.parameters
httpclient4.retrycount=1
이 설정 또한 결과에 큰 차이가 없다. 에러 비중이 1.96%가 확인되었지만 거의 같은 수준이다.
이후 retry는 테스트를 수행하는데 적당해보이지 않아 삭제했다.
number of threads
number of thread를 줄이면서 테스트해보았을 때 3,500에서 에러 없이 모두 정상적으로 돌아가는 것을 확인할 수 있었다. 즉, 동시 요청은 최대 약 3,500건이 정상적으로 처리된다는 사실을 알게되었다. (조금씩 조정한다면 더 많은 요청을 처리할 수 있겠지만 불필요한 작업이라 생각한다.)
ramp-up period
Wireshark로 확인해보았을 때 아래처럼 4-way handshake가 약 1.1 ~ 1.2ms 정도 걸리는 것을 볼 수 있다.
이 시간에 맞추어서 요청 당 실행 간격을 ramp-up period로 조정해보기로 했다. 현재 설정은 1이므로 요청당 1/4000(0.25ms) 간격으로 수행되며, 이를 2로 늘리고 다시 number of threads 4000으로 설정하고 테스트했다. 그러면 0.5ms 간격으로 수행되길 기대해야한다.
여러번 테스트해보았을때 가끔 에러가 발생하지만 대부분 위처럼 정상적으로 모든 요청이 완료된 모습을 확인할 수 있었다.
4000으로 설정해서 그런지 ramp-up period를 1로 설정해도 에러가 0.5%미만으로 적게 나오는 것을 확인할 수 있었다.
Connection eviction policy
앞서 apache httpcomponents 문서에 따르면 stale check로는 유효하지 않은 connection을 확인하는 것에 대해 100% 신뢰할 수 없다고 했다. 그래서 Connecion eviction policy
를 제안했는데 이 방법을 적용해보려고 한다.
우선 dependency를 추가해준다.
implementation 'org.apache.httpcomponents:httpclient:4.5.13'
이후 다음과 같이 빈을 추가해주면 된다.
@Bean
public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
// 허용 최대 connection 수
poolingHttpClientConnectionManager.setMaxTotal(200);
// 라우팅 경로에 대한 최대 connection 수
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(20);
return poolingHttpClientConnectionManager;
}
@Bean
public Runnable idleConnectionMonitor(PoolingHttpClientConnectionManager connectionManager) {
return new Runnable() {
@Override
@Scheduled(fixedDelay = 1000)
public void run() {
try {
if (connectionManager != null) {
// 만료된 모든 connection close
connectionManager.closeExpiredConnections();
// 주어진 시간(1000ms)이 지난 idle connection close
connectionManager.closeIdleConnections(1000, TimeUnit.MILLISECONDS);
} else {
log.trace("run IdleConnectionMonitor - Http Client Connection manager is not initialised");
}
} catch (Exception e) {
log.error("run IdleConnectionMonitor - Exception occurred. msg={}, e={}", e.getMessage(), e);
}
}
};
}
Schedule을 사용하여 1초 간격으로 실행하도록 했다. 그리고 number of threads=4000, ramp-up periods=1로 설정하고 다시 테스트해보았다.
에러가 0.03%, 개수로 보면 1개만 실패한 것을 확인할 수 있다. 여러번 테스트해보아도 확실히 개선된 결과가 보인다.
하지만 number of threads를 5000으로 늘려버리면 똑같이 Error가 비례해서 늘어나버린다는 사실...
결론
- ramp-up period를 증가시켜 에러율을 감소시킬 수 있다.
- Connection eviction policy를 적용하여 에러율을 감소시킬 수 있지만 number of threads가 증가함에따라 에러율이 비례해서 증가한다. 적용해도 에러가 발생하는 것은 마찬가지이다.
- stale check를 설정해주어도 에러율에 큰 차이를 확인할 수 없었다.
확실한 해결책은 thread가 순차적으로 실행되게 설정하는 것이라고 생각한다.
Update
Mac 기준으로 4-way handshake의 클라이언트 측의TIME_WAIT 상태가 지속되는 시간을 다음과 같이 설정해줄 수 있다.
sudo sysctl net.inet.tcp.msl=1
기본 값은 15000(15초)로 설정되어있고, 극단적으로 1으로 바꾸어주었다. 그리고 Jmeter로 number of threads=4000, ramp-up period=1 설정 후 결과 에러가 나오지 않았다.
NOTE
msl = Maximum Segment Lifetime
NOTE
sysctl -a로 모든 값을 확인할 수 있다.
[reference]
- https://www.yunsobi.com/blog/i/entry/660
- https://stackoverflow.com/questions/10558791/apache-httpclient-interim-error-nohttpresponseexception
- https://hc.apache.org/httpcomponents-client-4.5.x/current/tutorial/html/connmgmt.html#d5e418
- https://haon.blog/infra/nginx/keep-alive/
- https://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive
- https://stackoverflow.com/questions/58930872/org-apache-http-nohttpresponseexception-server-address80-failed-to-respond-j
- https://tweety1121.tistory.com/entry/Spring-restTemplate-Connection-pool-%EC%82%AC%EC%9A%A9