화스트 페이스 경보 발생...
문제상황
프로젝트 진행 중 이번달 말에 계산을 돌려야하는데 시간이 오래 걸린다고 성능을 개선해달라는 요청을 받았다.
Slow Query가 있겠거니 하고 소스를 뜯어봤더니 왠 걸,, 프로시저로 되어있다.
메인 프로시저 하나에 수십개의 프로시저가 딸려있는 구조였다.
정리하자면, java단에서 반복문으로 한명 씩 해당 프로시저를 호출해서 계산하는 구조인데 해당 프로시저는 수십개의 서브 프로시저를 달고있는 구조였으며, 1명당 약 3.6초가 소요됐다.
100명을 돌리면 360초. 1000명을 돌리면 3600초...... 그..그만!!
해결방안 모색
여러가지 해결방안을 모색했고 적합한 방식을 찾아나갔다.
Query 튜닝
처음에는 Slow Query가 있을 줄 알고 까보니 수십개의 프로시저로 구성되어 있었다.
딱히 시간이 오래 걸릴만한 Query도 안보였고 수천줄 짜리 프로시저가 여러개 있다보니 혼자 일일이 Query 실행계획을 까보기에도 무리가 있었고 이정도 버퍼면 3.6초 걸릴만한 것 같은데..? 라는 납득과 함께 이 방법은 일단 패스했다.
DB 튜닝
구글링을 통해 기본적 셋팅을 확인해봤는데, 메모리도 적당한 것 같고 딱히 문제가 될 부분이 보이지 않았다.
좀 더 깊게 들어가기엔 전문적인 지식이 부족했고 스케일 아웃 해서 클러스터링을 하는 부분들은 전문 DBA가 필요해보인다. 추후 DBA인력이 있다면 셋팅이나 개선방법의 조언을 구해보기로 하고 내가 할 수 있는 부분만 확인하고 일단은 패스했다.
구조변경
프로시저를 걷어내거나 구조를 변경하는데 일단 시간이 부족했다. 기존 로직 설계자들이 모두 바쁜 상태며 나혼자 모든 프로시저를 분석하고 수정하고 테스트하는데 시간이 너무 부족해서 일단 패스하였다. 근데 추후 분명히 구조를 변경하긴 해야한다. 미래의 나에게 맡긴다.
멀티스레드 처리 ✔️
멀티스레드로 프로시저를 호출해서 처리하는 방법이 성능을 개선하는데 가장 좋아 보였다.
DB Lock 부분을 걱정했지만, MariaDB의 기본 스토리지 엔진인 InnoDB 스토리지 엔진은 기본적으로 행 수준에서 락(row lock)을 관리하기 때문에 pk를 통한 동시 액세스에 문제가 없어 보인다고 판단했다.
따라서 멀티스레드로 구현하고 Race Condition 문제를 유발할 수 있는 code나 프로시저의 DDL같은 부분을 수정해주고 바로 테스트를 진행해봤다.
-- default setting 확인 -> row-level locking check
SELECT *
FROM information_schema.engines
WHERE engine='InnoDB';
멀티스레드 구현
아래는 실코드와 다른 예제 코드이니, 참고할 부분만 참고하길 바란다.
Java에서 기본 제공해주는ThreadPool인 ExcutorService보다 Spring에서 제공해주는 ThreadPool을 사용하였다.
왜냐하면, Spring에서 제공해주는 ThreadPool을 사용하면 요청이 동시에 들어와도 Spring에 설정된 ThreadPool에 요청을 쌓아놓고 멀티스레딩을 하는 반면, ExcutorService를 직접적으로 사용할 경우 사용자의 요청마다 ThreadPool을 생성하기때문에 요청이 몰릴 경우 Overflow가 발생할 수 밖어 없어보였다.
AsyncConfig.class
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "payworkExecutor")
public ThreadPoolTaskExecutor payworkExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setQueueCapacity(10);
executor.setThreadNamePrefix("payworkExecutor-");
executor.initialize();
return executor;
}
}
Calc.class
@Qualifier("payworkExecutor")
private final ThreadPoolTaskExecutor payworkExecutor;
public void calculate(List<Dto> dtoList) {
for (Dto dto : dtoList) {
payworkExecutor.execute(() -> {
mapper.calc(dto);
throwIfCalculationFailed(dto);
});
}
}
여기서 고민에 빠졌다.
스레드를 몇개로 처리해야 할까?
몇개로 호출해야 효율이 가장 뛰어날까?
물론 서버의 코어와 메모리에 따라 달라지겠지만 현재 8코어를 가진 DB에서 몇개가 효율성이 좋은지 확인해야했다.
먼저, DB의 maximum connection수와 현재 몇개의 connection을 맺고 있는지 확인했다.
SHOW VARIABLES LIKE 'max_connections';
SHOW STATUS LIKE 'Threads_connected';
그리고 max_connections - Threads_connected 한 값에서 천천히 스레드를 늘려가면서 서버 메모리를 확인하며 테스트를 실행했다.
이제 멀티스레드로 프로시저를 호출하였다.
그리고 아래의 명령어를 통해 DB에서 현재 connection 맺은 스레드의 상태를 확인했다.
SHOW processlist;
분명 5개의 스레드로 프로시저를 호출했는데...
오잉? 3개의 connection밖에 동시에 맺고 있지 않았다.
설마 Spring config에서 maximumPoolSize에 제한이 걸려있나?
application.yml
hikari:
maximumPoolSize: 3
역시는 역시 역시군. local환경에서는 maximumPoolSize가 3개로 제한되어 있었다.
해당 값을 20으로 늘려준 후 다시 테스트를 진행하였다.
5개의 프로시저 호출이 동시에 제대로 진행되고 있다.
검증결과
DB서버 모니터링을 진행하였고 아래와 같은 결과가 나타났다.
스레드 수에 따른 퍼포먼스
스레드 수 | CPU 사용량 | 건당 소요시간(초) |
3 | 30% | 1.5 |
4 | 50% | 1.4 |
5 | 60% | 1.2 |
6 | 70% | 1.3~ |
7 | 80~90% | 1.3~ |
CPU 사용량과 퍼포먼스를 봤을 때, 4~6개의 스레드로 돌리는게 효율적으로 보인다.
그런데 컨텍스트 스위칭 비용 때문인지 5개의 스레드를 초과할때부터는 소요시간이 약간 더 걸리게 되고 CPU 사용률만 늘어나게 된다.
따라서 5개의 스레드로 돌리는게 최적의 상태인걸로 유추했다.
성능 개선 결과
기존 건당 소요시간(초) | 개선 후 건당 소요시간(초) |
3.6 | 1.2 |
검증결과 값도 제대로 출력되었고 성능도 3배 이상 향상 시켰다.
건당 1.2초가 소요되지만, 아직도 느려보인다.
이제 프로시저 로직을 분석하면 개선포인트가 있다면 개선해보는 시도를 해봐야 할 것 같다.
일단 오늘은 여기까지!
'Language > ☕️Java' 카테고리의 다른 글
Java - bucket4j를 통해 트래픽 제한 및 IP 차단으로 Rate Limit 구현 (1) | 2022.12.21 |
---|---|
EnumMap을 써야하는 이유 (0) | 2022.11.20 |
JVM 아키텍처 2탄 - 런타임 데이터 영역(Run-time Data Area) (0) | 2022.08.31 |
StringBuffer에서 Thread safe의 원리 및 Example Sample Code를 통한 고찰 (0) | 2022.08.16 |
JAVA로 카카오 메시지 API연동 (4) | 2022.02.25 |
댓글