728x90

Infra 112

[운영] 모니터링을 붙였더니 결함이 나왔다, 그런데 바로 안 고쳤다

부제: 측정이 드러낸 숫자를 곧바로 결함으로 단정하지 않은 이야기모니터링을 붙이는 작업을 했다.목적은 단순했다. 안 보이던 걸 보이게 하는 것.그런데 실측을 시작하자, 모니터링과 무관한 원래부터 있던 것들이 드러났다.PostgreSQL 트랜잭션 롤백율 18.4%정렬·해시가 메모리가 모자라 디스크로 새면서 6일간 임시 파일 19GB, 1,439개로그용 Redis가 메모리 무제한 + 축출 안 함 → 호스트 램이 유일한 한계, 즉 OOM 위험. 게다가 append 로그가 원본 대비 47배로 부풀어 있음숫자만 보면 다 나쁘다.그런데 바로 안 고쳤다.18%는 나쁜 숫자인가롤백율 18%는 언뜻 앱이 에러투성이로 보인다.그런데 롤백은 에러일 때만 생기는 게 아니다.커넥션 풀은 연결을 반납할 때 상태를 리셋하며 롤백한..

Infra 2026.07.16

[관측성] 트레이스 원본 바이너리 마스킹 base64 PII

부제: 관측 도구에 14MB base64를 통째로 보냈다가 멈춘 이야기LLM 서비스엔 관측 도구를 붙여 둔다.어떤 요청이 들어와 어떤 프롬프트로 무슨 답을 냈는지, 흐름을 트레이스로 남긴다.문제를 디버깅할 때 이 기록이 없으면 깜깜하다.그런데 이 기록에 넣지 말아야 할 게 있었다.원본을 통째로 실었다PDF를 분석하는 요청이 있었다.그 파일을 14MB짜리 base64 문자열로 바꿔 트레이스에 통째로 실어 보냈다."입력이 무엇이었나"를 남긴다는 게, 파일 원본을 그대로 담아 버린 것이다.트레이스 하나가 수십 MB로 부풀었다.이게 트레이스 저장소(ClickHouse)의 다른 버그와 맞물리면서 크게 번졌다.쿼리가 12시간 멈추고, 헬스체크까지 막혀서 컨테이너가 재시작을 반복했다.교훈은 분명했다.관측 도구는 "무..

Infra 2026.07.12

[복구] 서버 재시작 유실 백그라운드 작업 10분 타임스탬프 가드

부제: 재시작하면 사라지는 fire-and-forget 작업을 되살리기서버엔 요청과 별도로 뒤에서 도는 작업이 있다.문서를 업로드하면 바로 "접수됐다"고 응답하고, 분석·텍스트 추출·임베딩은 백그라운드에서 처리한다.사용자를 수십 초씩 기다리게 하지 않으려는 구조다.여기에 구멍이 있었다.재시작하면 증발한다이 백그라운드 작업은 메모리에서만 돈다.서버가 재시작되면(배포, 메모리 부족으로 강제 종료, 컨테이너 재생성) 그 작업이 통째로 사라진다.작업 안에 예외 처리를 해 뒀어도 소용없다.프로세스 자체가 죽으면 예외를 잡을 코드도 같이 죽기 때문이다.결과가 좋지 않았다.문서가 "처리 중" 상태로 영원히 멈춘다.검색도 조회도 안 되는 채로. 실제로 며칠에서 열흘 넘게 고착된 게 여러 건 있었다.켤 때 되살린다방향은..

Infra 2026.07.11

[모니터링] 저트래픽 서비스 4계층 관측 비율 대신 절대건수

부제: 하루 몇십 요청짜리 서비스에서 비율 임계값이 무의미한 이유모니터링을 계층으로 쌓았다.호스트(자원이 한계인가), 컨테이너, 컴포넌트(DB·redis 내부), 그리고 서비스(앱이 요청을 제대로 처리하나).계층을 나눈 데는 이유가 있고, 알림 임계값을 정하는 데는 더 큰 고민이 있었다.계층마다 다른 질문각 계층은 서로 다른 질문에 답한다.호스트는 "서버 자원이 한계인가"를 본다. CPU·메모리·디스크의 사용률과 포화다.컨테이너는 그 자원이 어느 컨테이너로 갔는지를 나눈다.컴포넌트는 DB·redis 같은 내부 부품이 건강한지를 본다.그런데 여기까지로는 정작 중요한 게 안 보인다."앱이 사용자 요청을 실제로 성공시키고 있나."호스트가 멀쩡해도 앱은 500을 뱉고 있을 수 있다.그래서 마지막 계층을 서비스로..

Infra 2026.07.11

[비동기] SSE heartbeat 컨텍스트 누수 OTel 트레이스 깨짐

부제: 하트비트를 넣었더니 트레이스가 깨진 이야기챗봇은 답을 스트리밍으로 흘려보낸다.답이 길어지면 중간에 연결이 끊기지 않게, 주기적으로 빈 신호(하트비트)를 함께 보낸다.문제는 그 하트비트를 어떻게 끼워 넣었느냐였다.하트비트가 필요했다원래 구현은 이랬다.청크가 하나 올 때마다, 제너레이터의 다음 단계를 별도의 비동기 task로 띄웠다.하트비트 타이머와 다음 청크를 동시에 기다리려고 그렇게 쪼갠 것이다.겉보기엔 잘 돌았다.그런데 관측 도구에서 이상한 에러가 쏟아지기 시작했다."다른 컨텍스트에서 만들어진 토큰"트레이스를 남기는 라이브러리(Langfuse, 내부적으로 OpenTelemetry)가계속 "Token was created in a different Context"라는 에러를 냈다.원인은 컨텍스트였..

Infra 2026.07.10

[디버깅] user thread 완전복제 재현 원본 오염 방지

부제: 운영 버그를 원본을 건드리지 않고 재현하기특정 유저, 특정 대화에서만 나는 버그가 있다.재현하려고 그 유저에게 채팅을 한 번 쏘면, 그 순간 메모리·요약·체크포인트가 바뀐다.상태가 오염돼서 두 번째 디버깅은 이미 다른 조건이 된다.원본에 쏘면 오염된다손으로 재현 데이터를 만들 수도 없었다.수백 개의 대화 스레드에 분석 기록까지 누적된 상태를,픽스처 코드로 흉내 내는 건 사실상 불가능하다.그래서 방향은 하나였다.원본과 똑같은 상태의 사본을 만들고, 실험은 사본에서 한다.복제의 정의부터 못 박았다.모든 기본 키(PK)와 외래 키(FK)만 새로 생성하고,나머지 컬럼은 JSONB든 임베딩이든 파일이든 타임스탬프든 바이트 그대로 복사한다.즉 ID만 빼면 원본과 구별이 안 되는 사본이다.복제 범위는 손으로 ..

Infra 2026.07.09

[관측성] 실패 거부 경로 로깅 중앙 예외 핸들러 Fluent Bit

부제: 성공만 로깅되던 서버에서 실패를 조사할 수 있게 만들기성공 경로는 촘촘히 로깅되는데, 실패·거부 경로에서는 서버가 조용했다.HEIF 이미지가 왜 안 올라갔는지, 채팅이 어디서 타임아웃 났는지, 403이 왜 났는지 —조사할 때마다 로그가 없어서 원인 특정에 막혔다.문제를 두 축으로 나눴다. 무엇을 로깅할지, 그리고 그걸 어디로 모아 얼마나 보존할지.성공만 남던 서버실패를 성공만큼 남기기로 했다.다만 원칙을 세웠다.판정에 쓴 입력값과 함께 남기되, 본문 내용은 넣지 않고(개인정보 0),4xx는 경고, 5xx는 에러 수준으로 남긴다.여기서 갈림길이 있었다.실패가 나는 자리마다 로그를 한 줄씩 심을 수도 있었다.그런데 그러면 자리마다 남기는 방식이 제각각이 되고, 새 에러 코드가 생기면 로그를 또 빠뜨린..

Infra 2026.07.09

[백업] restic DB 백업 용량 최소화 복원 자동검증

부제: 6GB 백업을 월 0.9GB로 줄이고 매일 복원까지 확인한 이야기DB를 백업하려고 보니 덩치가 컸다.그런데 뜯어보니 대부분은 굳이 백업할 필요가 없는 데이터였다. 그 이야기다.문제이 서비스는 LangGraph로 대화 상태를 checkpoint에 저장한다.agent가 추론 턴을 한 번 돌 때마다 그 시점의 전체 상태를 스냅샷으로 남긴다. 과거 step으로 되돌아가는 time-travel을 위해서다.그러다 보니 한 스레드에서 대화가 100번 오가면 step 버전이 100개 쌓인다.checkpoint 테이블만 약 6GB였는데, 그중 98%가 이 step 이력이었다.재해 복구 관점에서 진짜 필요한 건 각 스레드의 최신 상태 하나뿐이다. 나머지 옛 step은 되감기 같은 고급 기능에나 쓰인다.결정 — 최신..

Infra 2026.07.08

[ClickHouse] Langfuse 쿼리 스톨 오진단 24.10 타임아웃 슬롯 버그

부제: 그럴듯한 우회로 때우다 진짜 원인을 놓칠 뻔한 이야기관측용으로 Langfuse를 쓰고, 그 뒤에서 ClickHouse가 트레이스를 저장한다. 어느 날 이 ClickHouse가 12시간 동안 먹통이 됐다. 사고를 어떻게 잘못 짚었다가 바로잡았는지 적어둔다.증상프로세스는 살아 있었다. 크래시도 OOM도 아니었다. 그런데 모든 쿼리가 멈췄다. 헬스체크로 날리는 SELECT 1조차 응답을 못 했다. 타임아웃을 60초로 걸어 뒀는데, 그 한 줄짜리 쿼리가 로그상 75분 넘게 슬롯을 붙들고 있었다. 설정한 타임아웃이 그냥 무시된 거다. host swap도 9 GiB 가까이 올라 있었다.그럴듯했던 오답사고 직전에 서버가 PDF를 잔뜩 처리하던 참이었다. 그 과정에서 14MB짜리 base64 데이터가 트레이스에..

Infra 2026.07.01

[nginx] docker-compose command override envsubst 미실행 ${VAR}

부제: command를 덮어썼더니 ${VAR}가 그대로 남았다nginx 설정을 템플릿으로 두고, 환경변수를 채워 렌더하는 방식을 쓰고 있었다. TLS 인증서 경로에 도메인을 변수로 넣어 뒀다.ssl_certificate /etc/letsencrypt/live/${SSL_DOMAIN}/fullchain.pem;그런데 어느 순간 ${SSL_DOMAIN}이 치환되지 않고 글자 그대로 남았다. nginx는 ${SSL_DOMAIN}이라는 이름의 디렉터리에서 인증서를 찾으려다 실패했고, 443(HTTPS)이 뜨지 못했다.증상디스크의 템플릿은 멀쩡했다. ${SSL_DOMAIN}을 채우는 단계가 그냥 안 돌았다. 변수 치환(envsubst)이 통째로 건너뛰어진 것이다.근본 원인 — command를 덮어쓰면 치환 단계도..

Infra 2026.06.25
728x90