"검증 중"이라고 출력되었지만, 아무것도 검증하지 못했습니다.
요약
Ollama를 사용하여 로컬에서 AI 에이전트 프레임워크를 운영하던 중 발생한 시스템 프리징과 모델 파일 손상 문제를 다룹니다. 도구가 체크섬 검증에 성공했다고 보고했음에도 실제로는 0바이트 파일이 생성되는 디버깅 과정을 상세히 기록했습니다.
핵심 포인트
- Ollama의 콘텐츠 주소 지정 저장소(CAS) 방식 이해
- 도구의 검증 성공 메시지가 실제 파일 무결성을 보장하지 않을 수 있음
- 시스템 프리징(Hanging) 현상과 파일 시스템 불일치 문제
- AI 에이전트 실행 환경에서의 보안 및 안정성 확보의 중요성
"sha256 digest 검증 중"이라고 출력되었습니다. 그다음에는 "성공"이라고 나왔습니다.
방금 검증을 마친 파일은 561바이트의 null(0) 값으로 채워져 있었고, 파일명 자체도 이를 나타내고 있었습니다.
저는 이 문제로 이틀을 허비했고, 그 시간의 대부분은 완전히 잘못된 소프트웨어를 살펴보는 데 사용했습니다.
실제로 일어나고 있었던 일
저는 AI 에이전트 프레임워크 (AI agent frameworks)를 보안이 강화된 VM (hardened VM)에서 실행합니다. 왜냐하면 해당 프레임워크들은 언어 모델 (language model)이 작성한 코드를 실행하며, 프레임워크 자체 문서에서도 실제 파일 시스템 근처에서는 실행하지 말라고 권고하기 때문입니다. 모델 또한 VM 내부에 존재하며, Ollama를 통해 로컬에서 서비스되므로 API 키가 필요하지 않고 네트워크를 차단할 수 있습니다.
그러던 중 VM이 멈추기 시작했습니다. 충돌(crashing)이 아니라, 멈춤(hanging) 현상이었습니다. 콘솔은 얼어붙었고, SSH는 비밀번호를 받아들인 뒤 영원히 대기 상태로 머물렀으며, 탈출할 수 있는 유일한 방법은 강제 전원 종료뿐이었습니다.
이틀 동안 이런 일을 네다섯 번 반복했습니다.
제가 실제로 쫓았던 증상
재부팅을 한 번 하고 나니, 모델 하나가 작동을 멈췄습니다. 모델을 사용하려는 모든 시도는 동일한 결과를 반환했습니다:
invalid character '\x00' looking for beginning of value
이 에러는 Python traceback의 세 개 층에 싸여 있고, HTTP 클라이언트 라이브러리의 5개 프레임 깊이에서 나타나며 제 에이전트 프레임워크를 통해 표출되었습니다. 그래서 저는 그 부분을 살펴보았습니다. 프레임워크의 재시도 로직 (retry logic)을 읽었습니다. HTTP 핸들러 (HTTP handler)를 읽었습니다. 요청 페이로드 (request payload)가 잘못되었는지 확인했습니다.
그동안 저는 모델을 계속 재설치하려고 시도했습니다. 매번 다운로드 바가 100%까지 찼고, 도구는 "sha256 digest 검증 중"이라고 출력한 뒤, "manifest 작성 중"을 거쳐 "성공"이라고 출력했습니다.
하지만 매번, 모델은 설치된 모델 목록에서 여전히 사라진 상태였습니다.
도구는 수행하지도 않은 작업에 대해 네 번이나 성공했다고 저에게 말했습니다. 저는 네 번이나 그것을 믿었습니다. 왜냐하면 도구가 "검증 중"이라고 말했고, 실패 현상이 다른 곳에서 나타나고 있었기 때문입니다.
잘못된 방향들, 순서대로
이 이야기의 깔끔한 버전은 거짓말이기 때문에, 이 기록을 남겨두고 싶습니다.
디스크가 가득 찼다고 생각했습니다. 디스크 사용량은 42%였습니다.
모델 파일 자체가 손상되었다고 생각하여, 파일을 삭제하고 다시 설치했습니다. 결과는 같았습니다.
도구의 인덱스(index)가 디스크에 있는 내용과 어긋났다고 생각하여 서비스를 재시작했습니다. 결과는 같았습니다.
그다음 서버가 보고한 내용과 폴더에 실제로 들어 있는 내용을 비교해 보았는데, 서로 일치하지 않았습니다. 실행 중인 프로세스가 인지하기를 거부하는 무언가가 디스크에 존재하고 있었습니다.
내내 그 자리에 있었던 것
Ollama는 모든 파일의 이름이 해당 파일 내용의 SHA256 해시(hash) 값으로 지정된 폴더에 모델 데이터를 저장합니다. 이것이 콘텐츠 주소 지정 저장소 (content-addressed store)의 전체 설계 방식입니다. 이름이 곧 체크섬 (checksum)입니다. 파일이 온전한지 알기 위해 매니페스트 (manifest), 데이터베이스, 또는 네트워크 호출이 필요하지 않습니다. 파일을 해싱 (hashing)한 뒤 이미 지정된 이름과 비교하기만 하면 됩니다.
그래서 저도 그렇게 했습니다:
$ sudo sha256sum .../blobs/sha256-34bb5ab01051a11372a91f95f3fbbc51173eed8e7f13ec395b9ae9b8bd0e242b
d69d411b56d3a2b64ee95edb489d83fd913e8dfa12a620656ba2ace261723f6e .../sha256-34bb5ab0...
파일 이름은 34bb5ab0였습니다. 하지만 그 내용은 d69d411b로 해싱되었습니다.
명령어 하나였습니다. 첫 번째 실패가 발생했을 때부터 이미 확인할 수 있었던 것이었습니다.
그 후 저장소에 있는 13개의 모든 파일을 같은 방식으로 확인했고, 똑같이 망가진 두 번째 파일을 찾아냈습니다.
"검증 (verifying)"의 의미
출력 결과에는 그 단어가 있었지만, 실제 검사(check)는 없었습니다.
파일이 예상된 경로에 존재하기만 하면, 도구는 이를 신뢰합니다. 파일을 다시 해싱하지 않습니다. 따라서 손상된 파일은 "이미 가지고 있는 것"으로 간주되어 건너뛰어지고, 진행 표시줄은 즉시 채워지며, "성공 (success)"이 출력됩니다.
나중에 저는 이를 의도적으로 확인했습니다. 쓰기 작업이 중단되었을 때 남겨지는 상태인, 파일의 크기는 그대로 유지한 채 내용만 0으로 채웠습니다 (zeroed). 그런 다음 서비스를 재시작하고 모델을 다시 설치했습니다.
시작 루틴에는 다음과 같이 기록되었습니다:
msg="total blobs: 12"
msg="total unused blobs removed: 0"
도구는 해당 파일을 카운트하고 그대로 두었습니다. 모델 목록은 failures=0으로 로드되었습니다. 재설치 과정에서는 "verifying sha256 digest"와 "success"가 출력되었습니다. 하지만 파일은 여전히 잘못된 값으로 해싱되었습니다.
모든 레이어(layer)는 모든 것이 정상이라고 보고했습니다. 유일하게 의견이 일치하지 않았던 것은 파일 이름뿐이었고, 그 무엇도 이를 읽지 못하고 있었습니다.
상황을 더 악화시킨 해결책
같은 이틀 동안 묻혀 있었던 두 번째 교훈이 있으며, 이는 첫 번째와는 다른 종류의 실패입니다.
VM(가상 머신)이 멈추는 현상은 제 잘못으로 밝혀졌습니다. 8개의 물리 코어(physical cores)를 가진 머신에 VM에 8개의 가상 CPU(vCPUs)를 할당했기 때문에, 모델 서버가 이들을 모두 점유했을 때 게스트 커널(guest kernel) 자체가 스케줄링(scheduling)될 수 없었습니다. 콘솔은 결국 제가 미처 보지 못했던 메시지를 통해 정확히 그 사실을 알려주었습니다:
watchdog: BUG: soft lockup - CPU#3 stuck for 371s! [llama-server:2626]
5개의 CPU가 잠겼습니다. SSH는 인증을 시도하다가 쉘(shell)을 넘겨줄 CPU가 남아 있지 않아 멈춰버렸습니다.
그래서 저는 서비스의 CPU 할당량(quota)을 제한했고, 이는 즉시 락업(lockup) 현상을 멈추게 했습니다. 하지만 이는 보호하려던 대상마저 파괴해 버렸습니다. 제한을 적용한 상태에서 프롬프트(prompt)를 로드하는 것은 여전히 정상 속도로 실행되었지만, 응답을 생성하는 속도는 25분당 6단어로 느려졌습니다.
이 제한은 버스트(bursts) 방식으로 작동합니다. 프로세스가 할당된 예산을 사용하고 나면, 다음 윈도우(window)가 올 때까지 동결됩니다. 이는 병렬로 실행되는 작업에는 견딜 만하지만, 모든 단계가 이전 단계를 기다려야 하는 작업에는 재앙적입니다.
해결책은 프로세스에 모든 CPU를 간헐적으로 주는 대신, 더 적은 수의 CPU를 지속적으로 할당하는 것이었습니다. 의도는 같았으나 결과는 정반대였습니다.
저는 명시된 목표를 달성하면서도 어쨌든 시스템을 망가뜨리는 제어 장치를 만들었습니다. 이는 아무것도 하지 못하는 제어 장치의 실패와는 다르지만, 두 가지 모두 같은 방식으로 발견하게 됩니다. 즉, 보호하려는 대상으로부터 보호하려는 것이 아니라, 보호하려는 대상 자체를 측정함으로써 말입니다.
내가 실제로 얻은 교훈
4일 전, 저는 제 VM이 격리되었는지 확인하기 위해 만든 스크립트에 대해 썼습니다. 그 스크립트는 구조적으로 출력하지 않을 수 없는 초록색 'OK'를 출력했습니다. 저는 그것이 제 부주의에 관한 이야기라고 생각했습니다.
그러다 저는 검증이 무료이며 로컬에서 이루어진다는 것이 전제 조건인 스토어에서, 동일한 행위를 더 강력하게 수행하는 프로덕션 도구를 발견했습니다.
확인(checking) 없이 성공을 보고하는 시스템은 당신에게 거짓말을 하는 것이 아닙니다. 그것은 다른 무언가에 대한 진실을 말하고 있는 것입니다. 바로 시스템이 '실행되었다'는 사실 말입니다.
이런 방식으로 실패하는 체크들은 결코 당신이 지켜보고 있는 것들이 아닙니다. 그것들은 체크처럼 들리는 단어를 출력하는 것들입니다.
제 것은 [OK]라고 표시되었습니다. 이 도구는 verifying이라고 표시했습니다.
둘 다 확인하지 않았습니다.
보고 (The report)
저는 누구나 약 30초 안에 실행할 수 있는 재현 스크립트와 함께 이를 ollama/ollama#17520으로 제출했습니다.
해당 트래커에 이미 무엇이 있었는지 주목할 가치가 있습니다. 매니페스트(manifest)가 누락된 파일을 가리키고 있다는 내용의 메인테이너(maintainer)로부터 제기된 오픈 이슈가 있는데, 이는 인접해 있지만 동일한 문제는 아닙니다. 제 파일은 올바른 경로에 존재했고, 매니페스트가 선언한 정확한 크기로 존재했습니다. 존재 여부와 크기는 모두 통과되었습니다. 오직 해싱(hashing)만이 이를 잡아낼 수 있습니다.
그리고 정확히 이런 종류의 무결성 검사(integrity checking)를 요청하는 종료된 기능 요청(feature request)이 있었으나, 서버가 시작 시 손상된 다운로드를 정리하기 때문에 서버를 재시작하라는 조언과 함께 6월에 거절되었습니다.
저는 서버가 손상된 파일을 확인하고, 개수를 세었지만, 아무것도 삭제하지 않은 시작 로그(startup log)를 가지고 있습니다.
궁금한 분들을 위해
증상이 변한 이유. 스토어는 모델당 여러 개의 파일을 보유합니다. 작은 설정 파일(config file)이 손상되면 모델이 설치 목록에서 완전히 사라집니다. 레이어(layer) 중 하나가 손상되면 모델은 정상인 것으로 표시되다가, 로드 시 동일한 난해한 오류와 함께 죽어버립니다. 근본 원인은 같지만 두 가지 완전히 다른 증상이 나타나며, 오류 메시지는 두 경우 모두 모델이나 파일의 이름을 명시하지 않습니다.
Null(널) 값이 어디서 왔는가. 거의 확실하게 하드 리셋 (hard resets) 때문입니다. 파일이 할당되었으나 데이터가 플러시 (flush) 되기 전에 머신이 죽어버리면, 파일 시스템은 정확한 개수의 제로 블록 (zeroed blocks)을 남겨둡니다. 즉, 문제 2를 해결하기 위해 제가 적용했던 잘못된 수정 사항이 문제 1을 만들어낸 것이며, 저는 이 두 문제가 서로 관련이 없다고 생각하며 이틀을 허비했습니다.
샌드박스 (sandbox)와 검증 스크립트는 ai-security-lab에서 확인할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기