배경
현재 진행중인 나르지지 서비스는 스케줄링으로 적재되는 데이터 이외에는 유저의 데이터가 들어갈 일이 없습니다.
하지만 서비스를 확장하면서 몇 명이 조회했는지, 리뷰 등록과 같은 상황이 발생할 수 있습니다.
이러한 상황에서는 현재 DB인 SQLite의 한계가 발생할 수 있기에 먼저 테스트 환경에서 측정해보고자 합니다.
내용
기본적으로 SQLite도 MySQL과 같이 트랜잭션을 지원하지만 단위에서 차이가 있습니다.
SQLite는 단일 파일기반으로 임베디드 형식이라 파일에 락이 걸리게 됩니다.
반면 MySQL은 Row Lock기반이기에 동시성 처리에서 처리량이 높다는 강점이 있습니다.
스레드 개수와 요청 수의 개수를 조절하면서 SQLite와 MySQL을 비교해보겠습니다.
가상 시나리오
: 특정 게시글에 대해 조회수 카운트 하는 상황을 가정하겠습니다.
카운트 필드 하나를 Post 엔티티에 넣는 방식이 있을 수 있고
따로 Post엔티티에 대한 PostViewLog로 로우 하나씩 Insert 하는 방식을 떠올렸습니다.
Case1. 스레드 10개, 동시 요청 수 1만 개
초기 커넥션 풀 개수 10개이기에 스레드도 10개로 세팅하고 진행하였습니다.
SQLite
=========================================
총 요청(Requests): 10000
동시 스레드(Threads): 10
총 수행 시간: 5095ms
성공: 10000
실패: 0
초당 처리 건수(TPS): 1962.71
=========================================
MySQL
=========================================
총 요청(Requests): 10000
동시 스레드(Threads): 100
총 수행 시간: 8080ms
성공: 10000
실패: 0
초당 처리 건수(TPS): 1237.62
=========================================
오히려 1만 건 정도에서는 SQLite가 실패 없이 더욱 빠르게 성공하는 모습을 보였습니다.
Case2. 스레드 10개, 동시 요청 수 10만 개
SQLite
=========================================
총 요청(Requests): 100000
동시 스레드(Threads): 10
총 수행 시간: 35128ms
성공: 99952
실패: 48
초당 처리 건수(TPS): 2845.37
=========================================
2025-12-05T12:15:08.972+09:00 WARN 77865 --- [ool-2-thread-69] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 5, SQLState: null
2025-12-05T12:15:08.972+09:00 ERROR 77865 --- [ool-2-thread-69] o.h.engine.jdbc.spi.SqlExceptionHelper : [SQLITE_BUSY] The database file is locked (database is locked)
....
MySQL
CPU 사용률: 52%
=========================================
총 요청(Requests): 100000
동시 스레드(Threads): 10
총 수행 시간: 55972ms
성공: 100000
실패: 0
초당 처리 건수(TPS): 1786.61
=========================================
10만건이 되면서 SQLite에서는 Timeout으로 인한 실패값이 생겼습니다.
Case3. 스레드 10개, 동시 요청 수 100만 개
SQLite
=========================================
총 요청(Requests): 1000000
동시 스레드(Threads): 10
총 수행 시간: 360225ms
성공: 999440
실패: 560
초당 처리 건수(TPS): 2774.49
=========================================
MySQL
=========================================
총 요청(Requests): 1000000
동시 스레드(Threads): 10
총 수행 시간: 630941ms
성공: 1000000
실패: 0
초당 처리 건수(TPS): 1584.93
=========================================
여기까지 테스트를 해봤을 때 처리시간 자체는 SQLite가 대용량으로 갈 수록 상대적으로 더 빨라집니다.
하지만 Timeout으로 인한 실패 횟수가 늘어나며 실패값이 누적됩니다.
제 생각에는 100만건 정도에서는 MySQL이 더 빠를 것이라고 생각했지만 오히려 더 느린 모습이었습니다.
이 이유를 생각해봤을 때 Connection Pool과 연관이 있을 것 같다고 생각했습니다.
MySQL에서는 CP 개수에 따른 네트워크 통신 비용이 영향이 있을 것 같았습니다.
CP개수를 조절하며 다시 테스트해보았습니다.
Case4. Case2에서 CP개수와 스레드 10-> 100개로 증가
SQLite
=========================================
총 요청(Requests): 100000
동시 스레드(Threads): 100
총 수행 시간: 41332ms
성공: 99179
실패: 821
초당 처리 건수(TPS): 2399.57
=========================================
MySQL
=========================================
총 요청(Requests): 100000
동시 스레드(Threads): 100
총 수행 시간: 30711ms
성공: 100000
실패: 0
초당 처리 건수(TPS): 3256.16
=========================================
Case5. Case2에서 CP개수와 스레드 10-> 100개로 증가
SQLite
=========================================
총 요청(Requests): 1000000
동시 스레드(Threads): 100
총 수행 시간: 365684ms
성공: 991986
실패: 8014
초당 처리 건수(TPS): 2712.69
=========================================
MySQL
CPU : 74.41%
=========================================
총 요청(Requests): 1000000
동시 스레드(Threads): 100
총 수행 시간: 296462ms
성공: 1000000
실패: 0
초당 처리 건수(TPS): 3373.11
=========================================

Docker MySQL은 vCPU : 2 환경에서 테스트하였습니다.
CP의 개수를 늘리며 다시 테스트해봤을 때 MySQL은 처리량이 대폭 증가하고 처리시간도 단축되었습니다.
이는 Row Insert로 인해 Lock경합이 발생하지 않았기 때문입니다.
범위 조건 쿼리문에서는 Next Gap Lock으로 인해 경합상황이 발생할 수 있습니다.
그리고 무조건 CP를 늘리면 좋냐고 했을 때 그건 아닙니다.
https://bugoverdose.github.io/docs/database-connection-pool-sizing/
[번역] 커넥션 풀 사이즈에 대하여
HikariCP 공식 위키의 'About Pool Sizing' 페이지에 대한 번역입니다.
bugoverdose.github.io
CP가 너무 클수록 DB의 제한된 CPU로 인해 오히려 빈번한 Context Switching 로 인해 Thrashing 상황이 생길 수 있습니다.
저같은 경우는 단순한 단일 Row Insert작업이었고 I/O대기 시간이 존재했습니다. 덕분에 vCPU가 2개인 환경에서도 70%대의 CPU 사용률을 보이며 긍정적인 결과를 얻을 수 있었습니다.
정리
SQLite와 MySQL의 매커니즘의 차이를 직접 테스트 하면서 몰랐던 부분도 알 수 있었습니다.
Connection Pool이 늘어날 수록 Timeout의 실패값 개수가 더욱 커질 수 있으니 이러한 트레이드오프도 고려해야겠습니다.
저는 오히려 아직까지는 SQLite로 충분하다라고 판단하였습니다.
1만 동시요청까지는 충분히 실패 없이 처리할 수 있었기에 대략적으로 언제 DB를 바꾸는게 좋을 지 대비 할 수 있게 되었습니다.
'나르지지' 카테고리의 다른 글
| MySQL에서 SQLite로 마이그레이션 한 이유 (1) | 2025.11.15 |
|---|---|
| 반복되는 일정 서비스 API콜에 대한 캐시 적용 및 전략 (3) | 2025.08.15 |
| DTO 프로젝션을 활용한 일정 서비스 성능 개선 (2) | 2025.08.15 |
| 8만 건 데이터 DB 마이그레이션 자동화 구축 (4) | 2025.08.06 |
| OOM 원인 API, DB 최적화로 해결하기 (4) | 2025.07.31 |