Learning a few things about running SQLite

Julia Evans

SQLite 운영하면서 배운 몇 가지

안녕하세요! 요즘 Django 사이트를 하나 만들고 있는데, 데이터베이스로 SQLite를 쓰기로 했어요. 웹사이트 데이터베이스로 SQLite를 쓰기 시작할 때 작은 사이트라면 프로덕션에서 SQLite를 써도 전혀 문제없다는 블로그 글들을 여러 개 읽었고, 저도 정말 괜찮다고 생각해요. 하지만 제가 제대로 깨닫지 못했던 건 SQLite도 엄연한 데이터베이스이고, 데이터베이스는 복잡하며, 저는 데이터베이스 운영에 대해 아는 게 별로 없다는 사실이었어요.

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

일단 블로그 글들에서 하라는 대로 WAL 모드를 켜고 잘 되길 바라는 마음으로 시작했어요.

ANALYZE가 생각보다 중요하더라고요

오늘 4000개 행이 있는 테이블에서 쿼리 하나를 실행했는데(SQLite의 FTS5로 전체 텍스트 검색을 하는 쿼리였어요) 5초나 걸렸어요. 뭔가 이상하다고 생각했죠. 컴퓨터는 빠르잖아요!

알고 보니 제가 해야 했던 건 ANALYZE를 실행하는 거였어요! 바로 문제가 되던 쿼리가 5초에서 0.05초 정도로 줄었어요(정확히는 더 알아볼 필요도 없을 만큼 충분히 작아졌어요). 쿼리 플랜에서 정확히 뭐가 잘못됐는지는 아직도 잘 모르겠지만, 아마도 우연히 이차 함수가 된 경우 같은 게 아니었을까 추측하고 있어요.

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

언젠가는 쿼리 플랜 읽는 법도 배워야겠어요.

데이터베이스 정리는 까다로워요

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

이럴 때 저에게 몇 번 일어났던 일은 이래요:

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

그래서 지금까지는 5초 이상 걸리는 쿼리를 실행하지 않도록 이런 정리 작업을 작은 배치로 나눠서 처리하고 있어요. 이런 경험을 하고 나니 동시에 여러 writer를 가질 수 있는 Postgres 같은 “진짜” 데이터베이스를 쓰고 싶어하는 이유를 더 잘 알겠더라고요.

나중에는 이런 작업이 필요할 때 그냥 사이트를 내려서 정기 점검 시간에 처리할지도 모르겠어요. 아직 그 workflow는 못 정했지만요.

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

지금까지는 쿼리 성능에 전혀 신경 쓰지 않고 Django ORM으로 원하는 쿼리는 뭐든 만들어 쓰고 있는데, ANALYZE 건을 제외하면 대부분 괜찮았어요. 데이터베이스도 꽤 작고(아마 1만 행 정도?) 앞으로도 계속 작게 유지될 것 같아서, 이대로도 계속 잘 되지 않을까 기대하고 있어요.

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으로 죽는 경우가 있어서 좀 지치기도 했고요. 기본적으로 설정 파일을 하나 작성하고 이렇게 실행하면 돼요:

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년 뒤에는 또 다른 아주 기초적인 기능을 배우고 있겠죠.

참고 자료

공식 문서 외에 제가 참고한 블로그 글들이에요:

원문은 Julia Evans님이 에 게재했습니다.

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