따로 돌던 수집기 22종을 공통 Airflow 경로로 병합
수동 실행과 상주 컨테이너로 흩어져 있던 수집기를 모노레포 컴포넌트와 Airflow DAG로 옮기고, 옮기면서 드러난 결함을 고쳤습니다.
작업 요약
수집기 22종 중 16종 전환 · 라이브러리 통계 스캔 163GB → 11GB
스케줄러 밖에서 돌던 수집기가 22종이었습니다
2026년 9월 기준으로 공통 실행 환경인 labrador-scrapers 밖에서 도는 수집기가 22종이었습니다. OS 배포판 취약점 수집기 10종은 별도 서버에서 docker compose로 손으로 띄웠고, Go·Maven·NuGet·Packagist·RubyGems·CocoaPods 라이브러리 크롤러 6종은 컨테이너 안에서 4시간 루프로 스스로 돌았습니다. 주기를 바꾸거나 멈추려면 서버에 들어가야 했고, 실패해도 알림이 오지 않았습니다.
고객사 내부에 수집 체계를 그대로 세워야 하는 과제가 생기면서 실행 방식을 하나로 맞추는 일이 먼저가 됐습니다. 그래서 데이터 이동이 없는 수집기부터 옮기고, 이관마다 원본 테이블을 동결한 뒤 행 수와 워터마크를 대조하는 순서로 진행했습니다.
배경
수집 결과는 gatheringdb 생태계 테이블과 labradordb 공용 테이블에 나뉘어 쌓였습니다. 통계 툴은 공용 테이블만 세고 있어서, 이관이 끝난 언어의 숫자가 이관 시점에 멈춘 채 석 달 동안 보고되는 일도 있었습니다.
- 실행 방식이 세 가지였습니다. 수동 docker compose, 자체 루프로 도는 상주 컨테이너, 그리고
schedule=None으로 만들어 두고 손으로 트리거하는 DAG였습니다. execution_timeout이 없는 DAG 하나는 97일 동안 초록색인 채 죽어 있었습니다. 실패가 화면에 드러나지 않는 구조였습니다.- 레거시 코드는 의존성 버전에 민감했습니다. xmltodict 0.13이
OrderedDict대신dict를 돌려주자 Oracle OVAL 파서가 0행을 냈습니다. - 로그만 보고 성공을 판정하면 속았습니다. except 절이 오류를 warning으로 삼켜서, 컨테이너는 정상 종료하고 테이블은 비어 있는 경우가 있었습니다.
이관 순서와 판단
- 현황표
22종의 위치·실행 방식·상태·이관 여부를 한 표로 정리
- 컴포넌트화
상주 루프와 자체 스케줄을 빼고 1회 실행 유닛으로, 커서는 상태 테이블로
- 데이터 이관
원본 동결 후 전용 테이블에 복사하고 행 수·워터마크·행 단위 diff 대조
- 컷오버
레거시 컨테이너 중지, DAG 활성화, 통계·취약점 매퍼·배포 동기화 설정 재지정
- RubyGems를 첫 이관으로 잡아 하루 안에 컷오버까지 마치고, 그 절차를 나머지 언어의 기준으로 썼습니다. 버전 1,914,331행·제품 228,461행을 복사한 뒤 레거시를 멈추고 남은 차이 238행·26행만 다시 upsert했습니다.
- 라이브러리 컴포넌트 9개를 한 디렉터리 아래로 모으되 이미지 이름은 디렉터리 이름과 같아서 그대로 두었습니다. DAG 16개는
dag_id를 유지하고 표시명만 바꿨습니다. 이 클러스터는 DAG를 만들 때 정지 상태로 두기 때문에 이름을 바꾸면 전부 멈춘 채 시작하고 실행 이력도 끊깁니다. - Packagist 크롤러의 버전 정렬 jar는 Maven 비교기였습니다. 개발 브랜치
1.x-dev를 정식 릴리스 위에 올려 제품의 최신 버전을 잘못 가리키고 있었습니다. Composer 규칙으로 파이썬에 다시 구현해 표본 3,000개 중 237개(7.9%)의 최신 버전이 바뀌는 것을 확인했고, 전체 539,900개로 환산하면 약 43,000개입니다. 이 재정렬은 이관 때 한 번만 실행했습니다. - Maven은 인덱서와 워커 2단 구조를 그대로 두었습니다. 이미 운영 구조였고, 워커는
CRC32(PRODUCT_KEY) % 4로 나눠 공유 락 없이 돌기 때문입니다. 제품 958,816행을 동결된 원본에서 25초에 복사하고 PK 전체로 조인해 9개 컬럼을 비교한 결과 차이는 0건이었습니다. - Go는 이미 gatheringdb 전용 테이블에 쓰고 있어 데이터 이동이 없었습니다. 대신 7월부터 워커의 상태 기록이 무시되던 버그를 찾았습니다. 완료 처리와 재시도 복원이 인덱서용 no-op upsert를 거쳐 420,673건이 처리 중 상태에 멈춰 있었고, 컷오버 때 reclaim으로 되돌렸습니다.
병합 후 실행 구조
컴포넌트 이미지는 Jenkins가 빌드해 레지스트리에 올리고, Airflow DAG가 KubernetesPodOperator로 실행합니다. 실행 시간이 주기보다 긴 크롤러는 매시 스케줄에 주기 가드를 두어 마지막 실행 기록을 보고 건너뜁니다. 적재는 gatheringdb 생태계 테이블로 모으고, 배포 DB 동기화와 통계는 그 테이블을 읽도록 다시 지정했습니다.
옮기면서 드러난 결함
이관은 오래 돌던 수집기를 처음으로 한 화면에서 보게 만드는 일이기도 했습니다. 아래는 그 과정에서 잡은 것들입니다.
- AlmaLinux 수집기는 CVE 한 건을 처리할 때마다 그때까지 쌓인 전체 목록을 다시 upsert했습니다. 분당 988건이던 적재 속도가 20시간 뒤 22건으로 떨어졌고 한 회차가 21시간을 넘겼습니다. 5,000건마다 넣고 비우도록 고치자 시뮬레이션에서 총 쓰기가 54억 건에서 18만 건으로 줄었습니다.
- Fedora 42가 repodata 압축을 gzip에서 zstd로 바꾸면서 매 회차 7~11분 만에 죽었습니다. 확장자 대신 매직 넘버로 gzip·zstd·xz·bz2를 판별하게 했습니다.
- GitHub 토큰이 전부 401이 되면 60초마다 같은 실패를 반복하는 무한 루프가 있었습니다. 파드는 25시간 살아 있었고 아무것도 진행하지 않았습니다. 전부 무효면 즉시 예외를 내고 종료 코드에도 반영하게 했습니다.
- 이미지 48개를 두 플랫폼으로 빌드하는 96건 중 한 건이 튀면 그 컴포넌트만 조용히 배포에서 빠졌습니다. 재시도 3회와 실패 목록 출력을 넣었습니다.
- DAG 파일을 지워도 Airflow가 비활성화하지 않았습니다. git-sync worktree 경로가 커밋마다 바뀌어 삭제 판정이 어긋났기 때문입니다. 고스트 DAG 20건을 지워 활성 DAG 92개와 파일 92개를 맞췄습니다.
- 라이브러리 통계 쿼리가 몇 시간씩 걸리다 재시도 5회를 다 쓰고 죽었습니다. 언어별로 쪼갠 테이블에
LANGUAGE조건이 남아 있어 옵티마이저가 163.1GB PRIMARY를 읽었고, 조건을 빼자 11.2GB 커버링 인덱스를 타고 122초에 끝났습니다. 같은 통계가 이관 전 공용 테이블을 세고 있어 GOLANG 제품 수가 6월 30일 값 1,662,387에 멈춰 있던 것도 고쳤습니다. 실제는 2,943,744건이었습니다. - DAG 실패 알림을 카카오워크에서 Slack으로 옮기면서 원인 줄을 마지막 줄 대신 첫 예외 줄에서 뽑게 했습니다. Airflow가 원래 예외를 감싸 다시 던지기 때문에 실패 6건 중 4건에서 두 줄이 달랐습니다.
결과
9월 25일 기준입니다. 이관 수는 9월 9일 현황표의 22종을 기준으로 셌습니다.
남은 일과 한계
- Maven DAG는 정지 상태로 이미지 재빌드 확인을 기다립니다. npm·pypi 제품 테이블의 gatheringdb 이관은 브랜치에만 있습니다.
- 커밋 URL·코드 취약점·GitHub Advisory 3종은 착수하지 않았고, Snyk·SourceClear는 삭제 대상입니다.
- Go 컷오버는 9월 23일에 실행했지만 이슈 상태는 아직 진행 중으로 남아 있습니다. 통계의 SWIFT·C_CPP 버전 집계 누락은 별건으로 남겼습니다.
- 위 수치는 이슈 본문과 작업 기록에 적힌 실측이며, 이관 뒤 수집량이 늘었다는 주장은 하지 않습니다.
역할
병합 프로그램의 하위 작업은 전부 제가 맡았습니다. 현황표 작성, 이관 순서와 테이블 위치 결정, 컴포넌트 이식과 DAG 작성, 컷오버 실행과 소비자 재지정까지 했고, 컨테이너 중지·DDL·복사처럼 운영에 손대는 단계는 런북을 쓰고 직접 실행했습니다. 고객사 내부 수집 체계 구축 과제는 동료와 함께 맡았고, 수집 모니터링 화면은 다른 담당자가 만들었습니다.
관련 항목: 12개 라이브러리 크롤러 누락 점검과 Python 전환 · 수집처별 법적·운영 경계 정리 · K8s · Airflow 데이터 수집 플랫폼 운영·장애 대응
근거 자료
아래 기록에 근거해 정리했습니다. 내부 원문 경로를 표시하며, 비공개 원문 파일은 이 페이지에서 제공하지 않습니다.
ai/wiki/projects/legacy-crawler-consolidation.mdai/sources/confluence/2026-09-25-portfolio-jira-confluence-refresh.mdai/workspace/crawler-migration-handoff.mdai/repo-notes/labrador-scrapers.mdai/repo-notes/labrador-data-platform.mdai/worklog/2026/2026-W37.mdai/worklog/2026/2026-W38.mdai/worklog/2026/2026-W39.md