Learning a few things about running SQLite

Julia Evans

SQLite 운영하며 배운 몇 가지

원문은 Julia Evans님이 에 게재했습니다. 이 블로그 구독하기

안녕하세요! 최근에 Django 사이트를 하나 만들고 있는데, 데이터베이스로 SQLite를 사용하고 있습니다. 웹사이트에 SQLite를 쓰기 시작할 때, 작은 사이트라면 프로덕션에서도 SQLite를 써도 전혀 문제없다는 여러 글을 읽었고, 저도 충분히 괜찮다고 생각합니다. 다만 제가 미처 제대로 깨닫지 못했던 점은 SQLite도 결국 데이터베이스라는 것, 데이터베이스는 복잡하다는 것, 그리고 저는 데이터베이스 운영에 대해 아는 게 별로 없다는 것이었습니다.

그래서 SQLite를 운영하면서 배우고 있는 소소한 것들을 몇 가지 적어보려고 합니다. SQLite를 쓴 웹사이트는 이번이 네 번째인데, 이번 프로젝트가 더 어렵게 느껴지는 이유는 Django ORM 덕분에 예전처럼 Django 없이 쓸 때보다 데이터베이스에 더 많은 일을 시키고 있기 때문인 것 같습니다.

일단은 모든 블로그 글에서 하라는 대로 WAL 모드를 켜고 잘 되길 바라며 시작했습니다.

ANALYZE가 생각보다 중요하다

오늘 4000개 행이 있는 테이블에서 쿼리 하나를 실행했는데(SQLite의 FTS5를 이용한 전문 검색 쿼리였습니다) 5초가 걸렸습니다. 뭔가 잘못됐다고 느꼈습니다. 컴퓨터는 빠르니까요!

알고 보니 제가 해야 할 일은 ANALYZE를 실행하는 것이었습니다! 실행하자마자 문제가 되던 쿼리가 5초에서 0.05초 정도로 줄었습니다(정확히는 더 파고들 필요 없을 만큼 충분히 작은 수치로요). 쿼리 플랜에서 정확히 무엇이 잘못됐는지는 아직도 모르지만, 제 추측으로는 일종의 accidentally quadratic 문제였던 것 같습니다.

ANALYZE는 쿼리 플래너가 더 나은 선택을 할 수 있도록 “통계”(아마 각 테이블의 행 개수 같은 것, 그리고 아마 다른 것들도?)를 생성합니다.

언젠가는 쿼리 플랜 읽는 법도 배우게 되겠죠.

데이터베이스 정리는 까다롭다

가끔 데이터베이스에 원치 않는 행이 잔뜩 쌓이는 경우가 있습니다(예를 들어 django-tasks-db의 완료된 작업 같은 것). 그럴 때 정리를 하고 싶어집니다.

이럴 때 제게 몇 번 일어났던 일은 다음과 같습니다.

  1. 행을 정리하는 명령을 실행한다
  2. 행이 많아서 명령 실행에 5초 이상이 걸린다(솔직히 왜 이 DELETE 문이 이렇게 느린지는 아직 의문입니다. 아마 트랜잭션 안에서 파이썬 코드가 많이 실행되고 있는지도 모르겠습니다. 잘 모르겠습니다)
  3. 그 사이에 다른 워커 중 하나가 데이터베이스에 쓰기를 시도하다가 5초 후에 타임아웃이 발생한다(타임아웃을 5초로 설정해 두었습니다)
  4. 데이터베이스에 쓰지 못한 워커가 크래시되면서 VM이 종료된다

지금까지는 이런 정리 작업을 작은 배치로 나눠서 5초 이상 걸리는 데이터베이스 쿼리를 실행하지 않도록 하고 있습니다. 다만 이런 경험을 겪고 나니 왜 사람들이 Postgres 같은 “진짜” 데이터베이스, 즉 동시에 여러 writer를 가질 수 있는 데이터베이스를 쓰고 싶어 하는지 좀 더 이해하게 됐습니다.

나중에는 이런 작업이 필요할 때 그냥 사이트를 내려서 정기 점검 시간에 처리할지도 모르지만, 아직 그럴 만한 워크플로우는 찾지 못했습니다.

ORM 쿼리 성능에 대해서는 아직 할 말이 없다

지금까지는 Django ORM으로 쿼리 성능을 전혀 신경 쓰지 않고 원하는 쿼리라면 뭐든 만들어 써 왔는데, ANALYZE 건을 제외하면 대부분 괜찮게 돌아가고 있습니다. 데이터베이스 자체가 꽤 작고(아마 10000행 정도?) 앞으로도 계속 작게 유지될 것 같아서, 이대로도 계속 괜찮기를 바라고 있습니다.

sqlite 백업하기

SQLite 백업은 몇 가지 방법으로 해봤는데, 사실 백업으로부터 복구하는 걸 직접 테스트해 본 적은 없는 것 같습니다. 다만 보통은 dead man’s switch로 모니터링을 하려고 합니다.

방법 1: restic

sqlite3 /data/calendar.db "VACUUM INTO '/tmp/calendar.sqlite'"
gzip /tmp/calendar.sqlite

# Upload backup to S3
# Sometimes the backup gets OOM killed and so it stays locked, do an unlock
restic -r s3://s3.amazonaws.com/some_bucket/ unlock
# Do the backup & prune old backups
restic -r s3://s3.amazonaws.com/some_bucket/ backup /tmp/calendar.sqlite.gz
restic -r s3://s3.amazonaws.com/some_bucket/ snapshots
restic -r s3://s3.amazonaws.com/some_bucket/ forget -l 1 -H 6 -d 2 -w 2 -m 2 -y 2
restic -r s3://s3.amazonaws.com/some_bucket/ prune

방법 2: litestream

최근에 Litestream을 써보기 시작했습니다. 증분 백업을 하는 게 더 효율적일 것 같아서였습니다. restic 백업이 가끔 OOM kill을 당하곤 했고, 좀 지치기도 했거든요. 기본적으로는 설정 파일을 작성하고 다음을 실행하기만 하면 됩니다.

litestream replicate -config litestream.yml

설정 파일에 retention: 400h를 넣어 어느 정도 데이터베이스 히스토리를 유지하려고 해봤는데, 제대로 동작하는지는 잘 모르겠습니다.

AWS에 백업하고 있는데, 자격 증명을 만들려고 AWS 콘솔을 헤매는 게 항상 번거롭습니다. 언젠가는 다른 S3 호환 서비스로 옮길지도 모르겠습니다.

여러 개의 데이터베이스를 쓸 수도 있다

현재 프로젝트는 데이터베이스가 하나뿐이지만, Mess with DNS에서 썼던 한 가지 트릭은 테이블들을 세 개의 별도 데이터베이스 파일로 나눈 것이었습니다. 테이블들이 굳이 같은 DB에 있을 필요가 없었기 때문입니다. 도움이 됐던 것 같습니다.

Mess with DNS는 이제 4년째(2022년부터) SQLite로 운영되고 있는데 정말 잘 돌아가고 있습니다. Postgres에서 옮긴 것은 그 프로젝트에 있어 훌륭한 선택이었다고 생각합니다.

여기까지!

제가 쓰는 기술의 아주 기본적인 것들을 배우는 데 얼마나 오래 걸리는지 보는 건 항상 은근히 재미있습니다. 웹 프로젝트에 SQLite를 처음 쓴 게 2022년인데, ANALYZE가 있다는 걸 오늘에서야 알았습니다! 아마 1~2년 뒤에는 또 다른 아주 기본적인 기능을 배우고 있겠죠.

참고 자료

공식 문서 외에 참고한 블로그 글들입니다.

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.

댓글