disk I/O 에러가 사실은 DB 잠금 경합이었던 날

26년 05월 25일

disk I/O error의 진짜 원인

이날은 도구 인프라를 고친 날이었다.

context-mode 플러그인에서 “disk I/O error”가 나서 작업이 막혀 있었다.

원인을 추적해보니, 디스크 문제가 아니라 DB 잠금 경합이었다.

에러 메시지가 거짓말을 하고 있었던 거다.

context-mode는 컨텍스트 윈도우를 약 98% 절약해주는 플러그인이다.

웹 페이지를 가져와서 인덱싱하는 도구가 있었는데, 이게 “disk I/O error”를 냈다.

HTTP 요청은 200으로 성공하는데, 인덱스 쓰기에서 실패하는 거다.

에러 메시지를 보면 디스크 문제 같다.

“disk I/O error”니까. 근데 원인을 추적해보니 전혀 달랐다.

DB 잠금 경합이었다.

node
대기 시간 설정을 무시한다

원인은 node

대기 시간 설정 설정을 무시하고 있었다는 거다.

context-mode는 SQLite를 쓴다.

여러 프로세스가 동시에 DB에 쓸 수 있게 설계되어 있다.

동시 쓰기 충돌을 막기 위해 대기 시간 설정을 30초로 잡아놨다.

충돌이 나면 30초 동안 기다렸다가 재시도하는 거다.

근데 node

이 설정을 무시했다.

생성자에 timeout 옵션을 넘겨도 무시하고 대기 시간 설정을 0으로 둔다.

0이면 기다리지 않고 바로 실패한다.

이게 왜 문제인가. 동시 쓰기가 발생하면 첫 번째 충돌에서 바로 실패한다.

30초를 기다리지 않고 즉시 실패하는 거다.

그리고 이 실패가 “disk I/O error”로 표시된다.

SQLite가 WAL 모드에서 잠금 실패를 디스크 에러처럼 보고하기 때문이다.

왜 node
마이그레이션했나

원래는 better-sqlite3라는 네이티브 애드온을 썼다.

근데 이게 크래시를 일으켰다.

리눅스에서 SIGSEGV가 나고 Node 26에서 V8 ABI가 바뀌어서 컴파일이 안 됐다.

그래서 Node에 내장된 node

마이그레이션했다.

근데 이 마이그레이션이 문제를 만들었다.

better-sqlite3는 생성자에서 timeout을 받아들인다.

근데 node

같은 이름의 옵션을 무시한다.

같은 옵션 이름인데 동작이 다른 거다.

마이그레이션의 전형적인 함정이다.

복합 원인

이 문제는 단일 원인이 아니었다.

세 가지가 겹쳐 있었다.

첫째, 대기 시간 설정이 0이었다.

이게 핵심이다.

두 번째, 크래시된 프로세스가 남긴 고아 임시 디렉토리가 90개 넘게 있었다.

이들이 오래된 WAL 잠금을 들고 있었다.

세 번째, 디스크가 95% 찼다.

WAL 모드는 추가 공간이 필요한데, 여유가 없었다.

이 세 가지가 합쳐서 disk I/O error를 만들고 있었다.

하나만 고쳐서는 안 된다.

전부 잡아야 했다.

복구 과정

복구는 두 단계로 했다.

첫 번째는 플러그인을 업그레이드하는 거다.

버전을 올리니까 대기 시간 설정 전파 수정이 포함되어 있었다.

node

경로에서도 대기 시간 설정이 제대로 설정된다.

두 번째는 지식 기반을 갈아엎는 거다.

purge 명령으로 기존 인덱스를 날리고 다시 구축했다.

63개 섹션, 95.7킬로바이트를 깨끗하게 다시 인덱싱했다.

고아 임시 디렉토리가 들고 있던 오래된 잠금도 이 과정에서 정리됐다.

에러 메시지가 거짓말을 한다

이 사건의 교훈은 에러 메시지를 맹신하면 안 된다는 거다.

“disk I/O error”를 보면 디스크를 의심하게 된다.

디스크 용량, 디스크 건강, 파일 시스템을 점검하게 된다.

근데 진짜 원인은 DB 잠금이었다.

에러 메시지는 증상을 보고할 뿐이다.

원인을 보고하는 게 아니다.

SQLite는 WAL 모드에서 잠금 실패를 디스크 에러로 보고한다.

이걸 그대로 믿으면 디스크 쪽에서 삽질하게 된다.

원인을 찾으려면 에러 메시지 너머를 봐야 한다.

“왜 디스크 에러가 나는가”를 파고들면 결국 잠금 경합에 도달한다.

에러 메시지를 출발점으로 쓰되 거기서 멈추지 말고 원인까지 파고들어야 한다.

마무리

이날은 인프라 신뢰성을 고친 날이었다.

지식 기반이 안 돌아가면 전체 작업 흐름이 막힌다.

context-mode가 컨텍스트를 절약해주는데, 이게 안 돌면 에이전트 작업이 비효율적이 된다.

근본 원인은 마이그레이션의 함정이었다.

better-sqlite3에서 node

옮기면서 같은 이름의 옵션이 다르게 동작하는 걸 놓쳤다.

마이그레이션할 때는 “같은 인터페이스”라고 가정하면 안 된다.

같은 이름이라도 동작이 다를 수 있다.

이걸 검증하지 않으면 이런 버그가 생긴다.