예전에 SQLite의 블로킹 문제를 겪었는데, 원인은 디스크 조각화(fragmentation) 였음
오래된 안드로이드 태블릿에서 앱을 몇 년간 매일 8시간씩 사용하던 사용자들이 느려짐과 락 오류를 호소했음
데이터를 복사해 받아보면 재현이 안 됐는데, 결국 기기를 직접 받아 확인해보니 DB 파일을 새 위치로 복사했다가 원래 이름으로 되돌리는 식으로 ‘디프래그’하니 문제가 완전히 사라졌음
Jellyfin DB에서도 같은 방법으로 성능 향상을 경험했음
그건 조각화보다는 플래시 메모리 열화일 가능성이 높음. 혹시 eMMC 저장소가 들어간 저가형 태블릿이었는지 궁금함
SQLite의 VACUUM 함수로도 같은 효과를 낼 수 있는지 궁금함
흥미로운 사례임. 하지만 사용자에게 직접 디프래그를 시킬 수는 없으니, 현실적인 해결책이 필요함
SQLite의 트랜잭션은 기본적으로 “deferred” 모드로 시작됨
즉, 실제 쓰기 작업을 시도하기 전까지는 write lock을 잡지 않음 SQLITE_BUSY 오류는 읽기 트랜잭션이 쓰기로 전환하려 할 때 이미 다른 트랜잭션이 write lock을 잡고 있을 때 발생함
해결책은 busy_timeout을 설정하고, 쓰기를 포함하는 트랜잭션은 “immediate” 모드로 시작하는 것임
관련 설명은 이 블로그 글에 잘 정리되어 있음
나도 처음에 이건 SQLITE_BUSY 문제라고 생각했음. 관련 사례들을 여기에 모아두고 있음
SQLITE_BUSY는 일종의 아키텍처 냄새라고 생각함. WAL 모드에서는 읽기 전용 커넥션 풀과 단일 쓰기 커넥션 풀을 분리해 설계함. 이렇게 하면 락 보유 상태를 명확히 파악하고, 경합 상황을 사전에 설계할 수 있음
busy_timeout은 이 경우 적용되지 않음. WAL 모드에서는 페이지가 단일 로그 파일에 추가되므로, 읽기 중에 쓰기로 전환하려 하면 직렬화 보장을 위해 SQLite가 즉시 실패시킴. “immediate” 모드가 이를 방지함
결국 SQLite를 쓰는 사람이라면 언젠가 이 문제에 한 번쯤은 데이고 원인을 추적하느라 시간을 쓸 것임
블로그 글에서 SQLITE_BUSY 언급이 없던데, 아마 설정이 빠졌던 것 같음
글의 일부 설명이 잘못된 듯함
SQLite는 자체적으로 락 관리를 하기 때문에, 애플리케이션이 직접 파일 접근을 제어할 필요는 없음
또한 WAL은 여러 병렬 쓰기를 허용하지 않음. 단지 읽기와 단일 쓰기를 동시에 수행할 수 있게 해줌
나도 SQLite를 정말 좋아하지만, 이 글은 기본적인 동시성 개념부터 틀려 있어서 추천을 못 하겠음
SQLite는 훌륭한 데이터베이스지만, 기본 설정값(defaults) 이 너무 보수적이라 아쉬움
실제 서비스용으로 쓰려면 여러 PRAGMA 설정을 조정해야 함
어떤 PRAGMA를 기본으로 켜두면 좋은지 알고 싶음
이런 상황이면 차라리 포크(fork) 해서 새 기본값을 만드는 게 낫지 않을까 싶음
SQLite의 새로운 hctree 기능이 안정화되면, 그때부터는 SQLite만 쓸 생각임
이름의 hc는 아마 High Concurrency의 약자일 것 같음 공식 문서 링크
이런 글을 보면 문제의 근본 원인 분석보다는 임시방편적인 해결책에 그친다는 느낌을 받음
더 깊은 디버깅과 연구로 정확한 원인을 밝혀내는 게 진짜 가치 있는 공유일 것 같음
아마 글쓴이는 일부만 조사하고 불완전한 해결책을 공유한 듯함. HN에서 더 나은 답변을 유도하려는 의도였을 수도 있음. “틀린 답을 올리면 더 빨리 정답을 얻는다”는 말처럼 말임
WAL 모드도 결국 단일 쓰기, 다중 읽기 구조임을 이해하지 못한 듯함
병렬 쓰기는 불가능하고, 단지 읽기 트랜잭션이 쓰기에 의해 블록되지 않게 해줄 뿐임
완전한 MVCC가 있으면 좋겠지만, 현재 구조도 원리를 이해하면 충분히 잘 동작함
나도 Jellyfin에서 비슷한 문제를 겪었음
평소엔 잘 돌아가지만, 특정 상황에서 DB가 락된 채 멈추는 현상이 생김
로그에는 “database is locked”만 남고, 결국 Docker 컨테이너를 재시작해야 해결됨
주로 TV UI에서 여러 버튼을 빠르게 조작할 때 발생함
약간 다른 이야기지만, SQLite in-memory DB를 대량의 insert/delete 작업에 쓰면 메모리 사용량이 점점 늘어남
예를 들어 5분마다 10만 행을 넣고 지우는 작업을 며칠간 반복하면, macOS에서 메모리가 1GB까지 올라감
이런 경우 조정할 만한 설정이 있을지 궁금함
혹시 VACUUM을 주기적으로 돌리고 있는지, auto_vacuum이 켜져 있는지 확인해보길 권함 VACUUM 문서
버퍼가 사용 패턴에 맞게 동적으로 조정되는 정상 동작일 수도 있음
모든 행을 지우는 경우라면, 그냥 테이블을 드롭하고 다시 생성하는 게 더 효율적임
SQLite는 훌륭하지만, 이런 문제를 보면 차라리 Postgres를 쓰는 게 낫지 않을까 싶을 때도 있음
단일 파일 이동성이나 임베디드 용도가 아니라면 Postgres가 더 간단하게 동시성 문제를 해결함
하지만 Jellyfin은 셀프호스팅 미디어 서버라서, Postgres를 요구하면 설치와 유지보수가 복잡해짐. SQLite가 더 적합함
Jellyfin은 대부분 가정용 단일 사용자 환경이므로 SQLite로 충분함. 다만 현재 설정이 최적은 아닌 듯함
SQLite의 장점을 무시하고 Postgres로 가자는 건, “캠핑하려는데 오두막을 지으라”는 말과 같음
Postgres를 쓰면 설치뿐 아니라 버전 업그레이드 시 마이그레이션도 신경 써야 함. SQLite는 이런 부담이 없음
Jellyfin은 최근 DB 코드를 Entity Framework로 리라이트해서, 앞으로는 DB 선택을 유연하게 할 수 있게 준비 중임
Hacker News 의견
예전에 SQLite의 블로킹 문제를 겪었는데, 원인은 디스크 조각화(fragmentation) 였음
오래된 안드로이드 태블릿에서 앱을 몇 년간 매일 8시간씩 사용하던 사용자들이 느려짐과 락 오류를 호소했음
데이터를 복사해 받아보면 재현이 안 됐는데, 결국 기기를 직접 받아 확인해보니 DB 파일을 새 위치로 복사했다가 원래 이름으로 되돌리는 식으로 ‘디프래그’하니 문제가 완전히 사라졌음
Jellyfin DB에서도 같은 방법으로 성능 향상을 경험했음
SQLite의 트랜잭션은 기본적으로 “deferred” 모드로 시작됨
즉, 실제 쓰기 작업을 시도하기 전까지는 write lock을 잡지 않음
SQLITE_BUSY오류는 읽기 트랜잭션이 쓰기로 전환하려 할 때 이미 다른 트랜잭션이 write lock을 잡고 있을 때 발생함해결책은
busy_timeout을 설정하고, 쓰기를 포함하는 트랜잭션은 “immediate” 모드로 시작하는 것임관련 설명은 이 블로그 글에 잘 정리되어 있음
SQLITE_BUSY문제라고 생각했음. 관련 사례들을 여기에 모아두고 있음SQLITE_BUSY는 일종의 아키텍처 냄새라고 생각함. WAL 모드에서는 읽기 전용 커넥션 풀과 단일 쓰기 커넥션 풀을 분리해 설계함. 이렇게 하면 락 보유 상태를 명확히 파악하고, 경합 상황을 사전에 설계할 수 있음busy_timeout은 이 경우 적용되지 않음. WAL 모드에서는 페이지가 단일 로그 파일에 추가되므로, 읽기 중에 쓰기로 전환하려 하면 직렬화 보장을 위해 SQLite가 즉시 실패시킴. “immediate” 모드가 이를 방지함SQLITE_BUSY언급이 없던데, 아마 설정이 빠졌던 것 같음글의 일부 설명이 잘못된 듯함
SQLite는 자체적으로 락 관리를 하기 때문에, 애플리케이션이 직접 파일 접근을 제어할 필요는 없음
또한 WAL은 여러 병렬 쓰기를 허용하지 않음. 단지 읽기와 단일 쓰기를 동시에 수행할 수 있게 해줌
SQLite는 훌륭한 데이터베이스지만, 기본 설정값(defaults) 이 너무 보수적이라 아쉬움
실제 서비스용으로 쓰려면 여러 PRAGMA 설정을 조정해야 함
SQLite의 새로운 hctree 기능이 안정화되면, 그때부터는 SQLite만 쓸 생각임
이름의
hc는 아마 High Concurrency의 약자일 것 같음공식 문서 링크
이런 글을 보면 문제의 근본 원인 분석보다는 임시방편적인 해결책에 그친다는 느낌을 받음
더 깊은 디버깅과 연구로 정확한 원인을 밝혀내는 게 진짜 가치 있는 공유일 것 같음
WAL 모드도 결국 단일 쓰기, 다중 읽기 구조임을 이해하지 못한 듯함
병렬 쓰기는 불가능하고, 단지 읽기 트랜잭션이 쓰기에 의해 블록되지 않게 해줄 뿐임
완전한 MVCC가 있으면 좋겠지만, 현재 구조도 원리를 이해하면 충분히 잘 동작함
나도 Jellyfin에서 비슷한 문제를 겪었음
평소엔 잘 돌아가지만, 특정 상황에서 DB가 락된 채 멈추는 현상이 생김
로그에는 “database is locked”만 남고, 결국 Docker 컨테이너를 재시작해야 해결됨
주로 TV UI에서 여러 버튼을 빠르게 조작할 때 발생함
약간 다른 이야기지만, SQLite in-memory DB를 대량의 insert/delete 작업에 쓰면 메모리 사용량이 점점 늘어남
예를 들어 5분마다 10만 행을 넣고 지우는 작업을 며칠간 반복하면, macOS에서 메모리가 1GB까지 올라감
이런 경우 조정할 만한 설정이 있을지 궁금함
auto_vacuum이 켜져 있는지 확인해보길 권함VACUUM 문서
SQLite는 훌륭하지만, 이런 문제를 보면 차라리 Postgres를 쓰는 게 낫지 않을까 싶을 때도 있음
단일 파일 이동성이나 임베디드 용도가 아니라면 Postgres가 더 간단하게 동시성 문제를 해결함