모든 태그

# dbt

7개의 글

사람 손이 붙어 있던 다섯 곳을 끊다: 설정 드리프트·변경 리뷰·인덱스 판정·인시던트 리포트·월간 점검

이기종 DBMS 운영 관리 플랫폼 DBTower 9편. 현업 DBA의 병목이 어디에 남는지를 렌즈로, 사람 손이 선형으로 붙던 다섯 지점을 기능으로 끊었습니다. (1) 설정 드리프트 이력: 파라미터 diff의 공간축("A와 B가 다른가")에 시간축("언제부터 무엇이 바뀌었나")을 붙였습니다. 거울 테이블과 변경 로그로 무변경 주기엔 스냅샷 한 줄만 쌓이게 했고, work_mem 4096→8192 실변경을 감지해 카드를 쐈습니다. 검증 중 MongoDB parameters()가 $clusterTime 같은 응답 gossip 필드를 흘려 매번 오탐이 나던 기존 버그도 잡았습니다. (2) 스키마 변경 리뷰 게이트: 배포 전 DDL을 규칙으로 판정(락 위험·DEFAULT 없는 NOT NULL·DROP·WHERE 없는 대량 변경)하고 실제 행수로 락 위험을 확정한 뒤 AI 1차 소견을 붙여 ADMIN 승인·자동 감사까지. 실행은 하지 않고 gh-ost 경로만 안내합니다. (3) 인덱스 사용 통계 주기 영속: "이 인덱스 지워도 되나"는 재시작 누적 카운터의 순간값으론 못 답합니다. 5기종 스캔 통계를 6시간 주기로 영속하고(Oracle은 미지원 정직), lakehouse가 first-vs-last 델타·리셋 클램프로 분기 창 판정 마트를 짓습니다. (4) 인시던트 리포트: 장애 구간을 주면 시점 비교·설정 변경·플랜 플립·대기·가용성을 한 장으로 재구성하고 AI가 재료 내 사실만으로 요약합니다. (5) 월간 점검 리포트: 헬스·백업·Advisor·용량·낭비·설정 변경을 매월 자동 발행합니다. 다섯 개 전부 읽고 판정·기록까지가 몫이고 대상 DB는 바꾸지 않습니다. 신규 모듈 하나(review)는 이벤트로 alert에 카드를 위임하고, 공개 파사드 둘(score·finops)로 Modulith 경계를 순환 없이 유지했습니다. 테스트 514건, VERIFICATION 110개 절.

7일이면 버려지는 스냅샷을 장기 이력으로 살려낸 dbtower-lakehouse 실측 총정리

DBTower가 7일 뒤 버리는 쿼리 스냅샷을 컬럼형 저장소로 내려 장기 이력을 만드는 ELT 파이프라인의 전체 기록을 한 편에 정리합니다. 오케스트레이션은 Airflow, 적재는 MinIO의 Parquet입니다. 변환은 dbt가 맡고 질의는 DuckLake로 합니다. 문제 정의(7일 시야로는 '지난달 대비 느려진 쿼리'에 못 답함)에서 시작해, 원천·적재·조회 3자 일치로 검증한 멱등 추출(닫힌 창 07-05=149,259·07-06=79,894), 누적 카운터를 일간 델타로 접는 변환, 조용한 오답을 막는 4축 fail-closed 게이트, lake를 house로 올리는 DuckLake 타임트래블, 아카이브가 자신을 지우던 치명 결함의 차단, 1년치를 합성해 407.62초 재빌드를 4초로 줄인 규모 실측, Kafka를 넣지 않은 근거를 담았습니다. 여기에 남이 그대로 띄우는 셀프호스트 어플라이언스, 두 저장소가 손잡는 되쓰기, 그리고 이 창고만 할 수 있는 여섯 가지 판정(용량 D-day·플랜 회귀·백업 공백·미사용 인덱스·설정 변경 상관·가용성 SLO)까지, 파이프라인이 답을 만드는 공정에서 판정을 내리는 창고로 자란 과정을 이었습니다. 라이브에서 MSSQL 두 인스턴스가 63퍼센트대 가용성에 평균 ping 2에서 7초로 목표 미달, 나머지는 99.9퍼센트로 목표를 지킵니다. 모든 수치는 직접 측정했고 재현 기록이 저장소에 있습니다.

창고가 데이터를 내리는 데서 판정을 내리는 데까지, 여섯 가지 판정을 채우다

창고에 데이터를 내리기만 하고 판정을 안 하고 있었습니다. plan_snapshot과 fct_query_daily를 둘 다 갖고도 상관시키지 않아 신고마다 30분씩 플랜 이력을 뒤졌고, 백업 공백은 판정 컬럼이 없어 복구하다 발견했습니다. 셋 다 신규 수집 없이 이미 내린 데이터를 판정 컬럼까지 잇는 일이었습니다. 플랜 회귀는 일 단위 대표 플랜의 뒤집힘을 전후 N일 지연과 겹치되 관측이 덜 차면 PENDING, 창이 오염되면 AMBIGUOUS로 지어내지 않고, 백업 공백은 유니버스를 query 팩트에서 잡아 기록 없는 인스턴스도 행으로 드러내며 기준일을 벽시계가 아닌 창고 최신 dt로 잡습니다. 이 셋을 주간 보고 한 장으로 접었습니다. 그리고 판정에 "왜"가 빠졌음을 깨달아 설정 드리프트를 원인 후보로 붙이고, 상관을 플랜 뒤집힘 한 축에서 지연·볼륨까지 넓히고, change_review는 저빈도라 자리만 열어 두었습니다. 여러 마트가 달던 "기종 축이 없다"는 각주는 이미 읽던 database_instance의 name·type로 회수해 instance_id 1이 local-mysql (MYSQL)로 읽히게 했고, 마지막으로 DBTower가 35일 뒤 지우는 up 여부를 장기 가용성 SLO로 만들었습니다. 라이브에서 MSSQL 두 인스턴스가 63퍼센트대 가용성에 평균 ping 2에서 7초로 목표 미달, 나머지는 99.9퍼센트로 목표를 지킵니다. 이걸로 관리 대상 DB에 관해 창고가 답할 판정 여섯이 한 바퀴 찼습니다. 발화는 여전히 남에게 맡기고 판정 컬럼까지만 정직하게 계산합니다.

남이 그대로 띄우게 만들고, 두 저장소가 되쓰기로 손잡다

여기까지 "전부 로컬에서 e2e 재현 가능"이라고 여러 번 써 왔는데, 남이 clone하면 아무것도 안 떴습니다. 원천이 없어 인스턴스 0개로 조용히 빈 결과가 나기 때문입니다. 그래서 데모 위성을 떼어내 셀프호스트 어플라이언스로 만들었습니다. docker-compose.standalone.yml과 demo 프로필로 DBTower 없이도 offload부터 게이트, dbt, 발행까지 e2e가 돌고, 격리 실측에서 외부 의존은 0이었습니다. 범용 도구로 넓히지는 않았습니다. DBTower를 셀프호스트하는 사람이 자기 관제 옆에 이 창고를 같이 띄우는 것까지가 목표라 초점을 좁힌 결정입니다. 그다음 두 저장소가 손을 잡았습니다. 원천을 query_snapshot 하나만 내리던 걸 테이블 스펙 레지스트리로 일반화해 백업·플랜·대기 이력까지 편입하는데, 전제가 셋 다 깨졌습니다. wait event는 원천에 영속 테이블이 없고, plan_snapshot은 카운트 기반 보존이라 하루가 닫히기 전 지워질 수 있고, backup_run은 verify가 나중에 UPDATE하는 사후 변이 테이블입니다. 그래서 워터마크·불변성·게이트 프로필을 스펙의 일부로 넣었고, 게이트를 그대로 재면 백업 안 도는 인스턴스가 정상인데도 completeness FAIL로 정상이 차단되는 걸 확인해 프로필로 그 축을 SKIP으로 남겼습니다. 방향이 반대인 일도 하나 했습니다. 장기 dow×hour 베이스라인을 계산해 원천 쪽으로 되쓰는 경로입니다. readonly 봉인을 깨지 않으려 별도 역할에 해당 테이블만 권한을 주고 단일 트랜잭션으로 32,498행 왕복을 실측했으며, 그 역할로 query_snapshot을 읽으면 permission denied가 나는 것까지 확인했습니다.

며칠치로는 규모를 증명 못 해서 1년치를 만들었고, 축을 돌려 다시 쟀습니다

지금까지 모든 실측은 닫힌 dt 세 개, 수십만 행에서 돌았습니다. 그 규모에선 전부 초 단위라 "규모에서도 버틴다"고 말하고 싶어지는데, 며칠치로는 증명이 안 됩니다. 마트(fct)는 매일 전체 이력을 다시 계산하는데 이력이 3일이라 안 아팠을 뿐입니다. 그래서 닫힌 dt를 날짜 시프트로 복제해 365dt×6인스턴스=2,190파일(54.5M행)을 격리 프리픽스에 만들어 어디가 먼저 무너지는지 쟀습니다. 병목은 fct 전체 재빌드 407.62초 하나였고, 나머지는 규모에서도 초 단위였습니다. 그 수치가 정당화하니 dt 단위 독립성을 살려 delete+insert로 증분화했고, 컴파일 타임 워터마크 리터럴로 파티션 프루닝을 걸어 407.62초를 4초로 줄였습니다. 그런데 규모의 축은 시간 하나가 아닙니다. 셀프호스터가 몇백 대를 관제하는 사람이라면 늘어나는 축은 dt가 아니라 인스턴스 수입니다. 로드맵에 "소파일이 먼저 무너지고 그다음 추출, 컴퓨트는 마지막"이라고 외삽해 뒀었는데 재보지 않은 문장이었습니다. 총량을 고정하고 축만 돌려 300대(52.2M행)로 다시 재보니, 증분 fct는 8.03초로 건재했고 급소는 소파일이 아니라 full-refresh였습니다. 제 외삽 두 개가 틀렸고, 그걸 수치가 바로잡았습니다. 최적화는 병목 수치가 정당화한 곳만 건드렸습니다.

코드보다 데이터 계약을 먼저 쓰고, 조용한 오답을 게이트로 막으며 파이프라인을 지었습니다

DBTower가 7일 뒤 버리는 쿼리 스냅샷을 컬럼형 저장소로 내려 장기 이력을 만드는 파이프라인을, 짓는 순서대로 담았습니다. DAG를 짜기 전에 무엇을 어떤 계약으로 옮길지부터 못박고, 원천 인덱스 선두를 타는 인스턴스별 등치 질의와 파티션 통째 덮어쓰기로 어제치를 안전하고 멱등하게 내립니다(닫힌 창 재실행에 79,894행 불변). 누적 카운터 raw로는 "느려진 쿼리"에 답할 수 없어, 지문 충돌 12,743키를 SUM으로 접고 하루 양 끝 차분에 리셋 클램프를 걸어 일간 델타로 바꿉니다. 반쪽 파티션 위 랭킹이 조용히 오답을 내는 걸 막으려 reconciliation·completeness·freshness·드리프트 4축 게이트를 다운스트림 앞에 세워 fail-closed로 dbt를 아예 실행하지 않게 했습니다(장애 주입 시 exit 2). 덮어쓰기만으로는 ACID도 타임트래블도 없어 lake일 뿐이라, 카탈로그를 이미 쓰던 PostgreSQL에 두고 데이터는 S3 parquet에 두는 DuckLake를 얹어 과거 버전 복원과 롤백을 실증했습니다. 막았다는 사실을 아무도 모르면 마트가 조용히 낡으니 컨테이너 안 dbt e2e·webhook·retry·CHECKPOINT로 운영을 경화했고, 마지막으로 Metabase가 DuckLake를 read-only로 읽게 해 처음 던진 "지난달 대비 느려진 쿼리"에 화면이 답하게 했습니다. 모든 수치는 직접 측정했습니다.

아카이브가 자기 자신을 지우던 치명 경로를 막고, CI와 deadman과 dbt contracts로 신뢰를 마저 묶은 이야기

코드 감사에서 받은 결함 목록의 1번이 치명이었습니다. offload의 멱등 재적재는 '파티션을 통째로 지우고 다시 쓴다'인데 삭제가 원천 0행 체크보다 먼저라, 원천 보존(7일) 밖의 dt를 backfill이나 Clear로 재실행하면 아카이브 유일본 parquet를 지운 뒤 아무것도 안 쓰고 exit 0으로 '성공'합니다. 1부에서 이 경로를 실제로 재현하고 fail-closed 가드(ArchiveSelfDestructError, exit 1 → 재시도·webhook 경로 탑승)로 막았습니다. 같은 감사에서 나온 나머지 셋, 곧 게이트의 원천 Seq Scan(332ms/31k버퍼 → 인스턴스별 인덱스 루프 20ms/76버퍼), publish 혼합 버전(개별 커밋 → 단일 트랜잭션), 유지보수 DAG의 데모 테이블 하드 참조도 걷어내고 pytest 35개로 고정했습니다. 2부는 그 신뢰를 커밋·침묵·계약 세 축으로 마저 묶습니다. CI(GitHub Actions 3관문: ruff·pytest·dbt)가 임베디드 DuckDB 덕에 MinIO도 PG도 없는 러너에서 tiny 픽스처 parquet로 dbt build를 e2e로 돌리고(PASS=25), dbt unit test로 델타 로직 엣지 4개를 정적 입력→기대 출력으로 못박고, deadman heartbeat가 30h 침묵을 실제 경보 발화로 잡고(기한 26h), dbt contracts가 마트 컬럼 타입을 DB 레벨로 강제해 latency_increase_ms를 VARCHAR로 바꾸자 빌드가 'data type mismatch'로 막혔습니다. 회귀는 없었습니다. verify는 ALL MATCH(149,259/79,894행), pytest는 53개 통과입니다.