Articleblog.cloudflare.com·2026년 6월 12일·0

Scaling Security Insights: how we achieved a 10x increase in global scanning capacity

Quick Summary

Cloudflare는 Security Insights의 스캔 병목을 Kafka 처리 방식, 데이터베이스 쓰기, API 배치, 스케줄러 설계 전반에서 개선해 초당 약 10회였던 스캔 처리량을 피크 기준 120회 이상으로 끌어올렸다.

Scaling Security Insights: how we achieved a 10x increase in global scanning capacity 관련 대표 이미지

🖼️ 인포그래픽

Scaling Security Insights: how we achieved a 10x increase in global scanning capacity 내용을 설명하는 본문 이미지

🖼️ 4컷 인포그래픽

Scaling Security Insights: how we achieved a 10x increase in global scanning capacity 내용을 설명하는 본문 이미지

💡 한 줄 요약

Cloudflare는 Security Insights의 스캔 병목을 Kafka 처리 방식, 데이터베이스 쓰기, API 배치, 스케줄러 설계 전반에서 개선해 초당 약 10회였던 스캔 처리량을 피크 기준 120회 이상으로 끌어올렸다.

📌 핵심 요약

  • Security Insights는 Cloudflare 계정, zone, DNS 레코드를 정기적으로 스캔해 보안 위험과 설정 오류를 찾아내는 기능이지만, 기존에는 스캔 주기가 1~2주로 길고 많은 무료 플랜 계정은 자동 스캔이 opt-in 방식이라 누락이 발생했다.
  • Cloudflare는 더 자주, 더 넓게 스캔하려면 평균 처리량을 초당 10회에서 100회로 약 10배 늘려야 한다고 계산했지만, 당시 시스템은 Kafka backlog, API timeout, 프로세스 crash 등으로 이미 부하를 견디기 어려운 상태였다.
  • 개선은 Kafka partition 특성을 고려한 batch 소비와 goroutine 병렬 처리, 느린 메시지와 빠른 메시지를 분리하는 slow lane/fast lane 구조, Postgres bulk write 최적화로 진행됐다.
  • API timeout의 핵심 원인은 Portland의 primary database와 Amsterdam API 인스턴스 사이의 왕복 지연이었다. Cloudflare는 API를 active-active에서 primary database를 따르는 active-passive 구조로 바꿔 latency 문제를 제거했다.
  • 마지막으로 스케줄러를 재설계해 zone을 계정과 독립적으로 스케줄링하고 기존 last_scheduled_at 값을 무작위화했으며, 계정·zone 수와 스캔 주기에 따라 30분마다 다시 계산되는 adaptive rate limiting을 도입해 균등한 스캔 분산을 달성했다.

🧩 주요 포인트

  1. Security Insights는 Cloudflare 계정, zone, DNS 레코드를 정기적으로 스캔해 보안 위험과 설정 오류를 찾아내는 기능이지만, 기존에는 스캔 주기가 1~2주로 길고 많은 무료 플랜 계정은 자동 스캔이 opt-in 방식이라 누락이 발생했다.
  2. Cloudflare는 더 자주, 더 넓게 스캔하려면 평균 처리량을 초당 10회에서 100회로 약 10배 늘려야 한다고 계산했지만, 당시 시스템은 Kafka backlog, API timeout, 프로세스 crash 등으로 이미 부하를 견디기 어려운 상태였다.
  3. 개선은 Kafka partition 특성을 고려한 batch 소비와 goroutine 병렬 처리, 느린 메시지와 빠른 메시지를 분리하는 slow lane/fast lane 구조, Postgres bulk write 최적화로 진행됐다.
  4. API timeout의 핵심 원인은 Portland의 primary database와 Amsterdam API 인스턴스 사이의 왕복 지연이었다. Cloudflare는 API를 active-active에서 primary database를 따르는 active-passive 구조로 바꿔 latency 문제를 제거했다.
  5. 마지막으로 스케줄러를 재설계해 zone을 계정과 독립적으로 스케줄링하고 기존 last_scheduled_at 값을 무작위화했으며, 계정·zone 수와 스캔 주기에 따라 30분마다 다시 계산되는 adaptive rate limiting을 도입해 균등한 스캔 분산을 달성했다.

🧠 상세 정리

1. Security Insights가 해결하려던 문제

Security Insights는 Cloudflare의 모든 계정에 실행 가능한 보안 권고를 제공하기 위해 계정, zone, DNS 레코드를 정기적으로 검사하는 시스템이다. 원문은 이 기능이 잠재적인 보안 위험과 잘못된 설정을 찾아내는 데 목적이 있다고 설명한다. 그러나 기존 스캔은 너무 드물게 수행되어 새로 생긴 위험이 최대 2주 동안 감지되지 않을 수 있었다. 또 많은 무료 플랜 계정에서는 자동 스캔이 opt-in 방식이었기 때문에 상당수 계정이 아예 스캔되지 않았다. 자동화된 공격이 빨라지는 상황에서 이런 지연과 누락은 보안 위험을 발견할 수 있는 시간을 줄이는 문제로 제시된다.

2. 10배 처리량 확대가 필요했던 배경

Cloudflare는 스캔 빈도를 높이고 모든 계정에 자동 스캔을 적용하려면 평균 스캔 처리량을 초당 10회에서 초당 100회로 늘려야 한다고 계산했다. 하지만 기존 시스템은 이미 한계에 가까웠다. 수백만 개의 이벤트가 backlog에 쌓여 처리를 기다렸고, 내부 API는 자주 timeout이 발생했으며, 일부 프로세스는 crash를 일으켰다. 따라서 단순히 스캔 대상을 늘리는 문제가 아니라, 시스템 자체가 안정적으로 확장되도록 구조를 손보는 작업이 필요했다. 글은 이 과정을 통해 Security Insights의 처리량을 10배 이상 높이고 수백만 고객에게 보안 인사이트를 제공할 수 있게 된 사례를 설명한다.

3. 기존 스캔 파이프라인의 구조

자동 보안 스캔은 scheduler가 계정이나 zone의 스캔 시점을 판단하는 방식으로 시작된다. 스캔이 필요하다고 판단되면 scheduler는 Apache Kafka에 하나 이상의 메시지를 발행한다. 이후 메시지는 특정 asset이나 configuration을 검사하는 여러 checker로 fan-out되며, 이 checker들은 Go로 작성된 특화 microservice로 설명된다. 각 checker는 메시지를 처리한 뒤 발견한 Security Insights 결과를 내부 API로 전송한다. 내부 API는 이 결과를 Postgres 데이터베이스에 저장하므로, 전체 병목은 Kafka 소비, checker 처리, API 호출, 데이터베이스 쓰기 중 어디에서든 발생할 수 있었다.

4. Kafka partition 제약과 병렬 처리 도입

원문은 Kafka가 엄밀히 말해 일반적인 queue가 아니라 partitioned event stream이라는 점을 강조한다. 같은 partition 안에서는 메시지가 순서대로 소비되고 처리되어야 하며, 하나의 consumer group 안에서는 partition당 활성 consumer가 하나만 존재할 수 있다. 이 때문에 느리게 처리되는 메시지는 다음 메시지 처리를 막고, checker별 consumer 수 역시 partition 수에 의해 제한된다. Cloudflare는 partition을 늘리는 방안도 고려했지만, Kafka broker는 다른 서비스와 공유되고 있어 리소스 사용 증가를 피하고자 마지막 수단으로 남겼다. 대신 checker가 메시지를 batch로 소비하고 각 메시지를 별도의 goroutine에서 처리하도록 바꾸어, 순서 제약을 유지하면서도 실제 처리 병렬성을 높였다.

5. Head-of-line blocking을 줄인 slow lane과 fast lane

일부 checker에서는 특정 메시지가 평균보다 훨씬 오래 걸리는 문제가 있었다. 어떤 계정이나 zone은 다른 대상보다 훨씬 많은 asset을 보유할 수 있고, 이런 경우 처리 시간이 평균적인 초 또는 밀리초 단위가 아니라 분 또는 시간 단위로 늘어날 수 있었다. 이런 느린 메시지가 같은 흐름 안에 있으면 뒤따르는 빠른 메시지까지 지연되는 head-of-line blocking이 생긴다. Cloudflare는 복잡한 방식 대신 consumer group과 checker를 slow lane과 fast lane으로 나누는 단순한 해결책을 선택했다. 빠르게 느린 메시지 여부를 판단할 수 있었기 때문에 fast lane checker가 느린 메시지를 만나면 건너뛰고, 느린 메시지는 전용 리소스에서 처리되도록 했다.

6. Postgres 쓰기 최적화

각 스캔에서 발견된 insight는 모두 Postgres 데이터베이스에 기록된다. 기존 구현은 checker가 insight 목록을 내부 API에 전달하면 API가 반복문 안에서 각 issue마다 INSERT ... ON CONFLICT DO UPDATE 쿼리를 실행하는 형태였다. insight 수가 큰 경우 이 방식은 issue마다 데이터베이스 왕복을 발생시켰고, 관측된 최대 크기인 50만 건에서는 한 번의 API 호출 안에 50만 번의 왕복, query, transaction이 발생하는 셈이었다. Cloudflare는 처음에 Postgres bulk insert의 대표적 방식인 임시 테이블 COPY를 시도했지만 system table bloat 문제가 있었다고 설명한다. 최종적으로는 건수가 threshold보다 작을 때 UNNEST를 쓰고, threshold를 넘는 큰 작업에는 COPY를 쓰는 hybrid 방식을 채택해 작은 insert는 밀리초 단위, 큰 insert도 초 단위로 처리하도록 개선했다.

7. API timeout의 실제 원인: 지역 간 latency

확장 과정에서 내부 API에는 여러 이상 현상이 관찰됐다. 많은 요청이 client-side timeout을 유발했고, 여러 checker는 처리 시간의 20~90%를 단일 API 호출에 쓰고 있었으며, 대량 스캔을 시작하면 초반 처리량은 높다가 점점 악화됐다. 원문은 이 모든 문제의 공통 원인을 latency라고 설명한다. primary database는 Portland, Oregon에 있었지만 API는 Portland와 Amsterdam에서 active-active로 동작하고 있었다. Amsterdam API 인스턴스에서 database query를 수행하면 물리적 거리 때문에 지연이 커졌고, 이 지연이 client-side connection pool의 connection을 오래 점유해 timeout을 만들었다.

8. Active-passive 전환으로 latency 병목 제거

Portland API에서는 평균 API 호출이 약 10밀리초에 끝났지만, Amsterdam에서는 거의 3초가 걸렸다. checker 프로세스는 Kafka stream의 partition들을 배정받아 소비하고, API는 load-balanced되어 있었기 때문에 어떤 프로세스는 Amsterdam API에 연결되고 어떤 프로세스는 Portland API에 연결됐다. 그 결과 Portland로 연결된 partition은 빠르게 처리되는 반면 Amsterdam으로 연결된 partition은 뒤처졌고, Kafka lag 그래프에서도 partition의 절반 정도가 늦게 따라오는 현상이 나타났다. Cloudflare는 API를 active-active에서 active-passive로 전환해 active API가 primary database를 따르도록 했다. 이 변경으로 latency 문제는 빠르게 사라졌다고 원문은 설명한다.

9. 스케줄러 재설계와 균등 분산

Kafka, 데이터베이스, API를 개선한 뒤에도 스캔이 시간적으로 고르게 분산되어야 한다는 문제가 남아 있었다. Kafka topic은 time-based retention policy를 사용하기 때문에 모든 스캔을 한꺼번에 queue에 넣으면 처리 전에 메시지가 삭제될 위험이 있었다. 기존 scheduler는 last_scheduled_at과 scanning frequency를 기준으로 계정 단위 스캔을 트리거하고, 해당 계정의 모든 zone도 함께 스캔했다. 그런데 많은 계정의 last_scheduled_at 값이 비슷했고, 스캔 주기를 줄이면 갑자기 대량 계정이 동시에 due 상태가 되는 문제가 있었다. 여기에 많은 zone을 가진 계정이 스케줄되면 관련 zone 스캔이 cascade처럼 발생해 Kafka partition을 포화시키고 작은 계정의 스캔까지 지연시켰다.

10. adaptive rate limiting과 결과

Cloudflare는 scheduler 문제를 해결하기 위해 세 가지 변경을 적용했다. zone을 계정과 독립적으로 스케줄링해 각 zone이 자체 last_scheduled_at 값을 갖도록 했고, 기존 계정과 zone의 last_scheduled_at 시간을 무작위화해 데이터베이스에 쌓여 있던 불균등성을 완화했다. 또한 스캔 빈도를 바꿀 때 생기는 spike를 흡수하기 위해 adaptive rate limiting을 도입했다. 이 rate limit은 계정과 zone의 총량, 플랜별 스캔 주기를 바탕으로 30분마다 비동기적으로 다시 계산되며, 계정 수가 늘어나도 정해진 주기 안에 스캔을 끝낼 수 있도록 조정된다. 결과적으로 Security Insights는 피크 스케줄링 시 초당 120회 이상의 스캔을 지속해 10배 개선 목표를 넘어섰고, 내부 API timeout과 Kafka lag 상태도 개선됐다.

🧾 핵심 주장 / 시사점

  • 처리량 문제는 단일 병목 하나가 아니라 Kafka 소비 방식, 느린 메시지의 영향, 데이터베이스 왕복, 지역 간 API latency, 스케줄링 spike가 함께 만든 복합 문제였다.
  • Cloudflare가 먼저 partition이나 리소스를 무작정 늘리지 않고 코드와 아키텍처를 개선한 점은, 분산 시스템 확장에서 비용 증가보다 병목의 성격을 정확히 파악하는 것이 중요함을 보여준다.
  • 스캔 빈도를 높이는 작업은 처리 worker만 빠르게 만드는 것으로 끝나지 않으며, 스케줄러가 작업을 시간적으로 균등하게 배치하지 못하면 downstream 시스템 전체에 새로운 병목을 만들 수 있다.

✅ 액션 아이템

  • 초당 10회에서 100회 목표로 Kafka partition 기반 batch 소비와 고루틴 병렬화를 병행해 피크 120회 달성 여부를 검증한다.
  • 느린 메시지와 빠른 메시지를 slow lane/fast lane으로 분리하고 Postgres bulk write 최적화로 쓰기 병목 감소 효과를 계정·zone 단위로 추적한다.
  • 스케줄러를 zone 독립 기준으로 재설계하고 last_scheduled_at 무작위화 후 30분 adaptive rate limiting이 스캔 분산 균형을 안정적으로 만드는지 점검한다.

❓ 열린 질문

  • active-passive로 전환한 API 구조에서 timeout 개선을 판단할 핵심 지표는 어떤 지연 구간까지 모니터링해야 하는가?
  • Kafka backlog 증가 시 slow lane/fast lane의 임계 비율은 어떤 방식으로 재조정해야 크래시 복원을 최소화할 수 있는가?
  • 계정·zone 수와 스캔 주기 변동이 커질 때 30분 주기 adaptive rate limiting이 과부하를 충분히 분산하는지 판단할 기준은 무엇인가?

관련 문서

공통 태그와 주제 흐름을 기준으로 같이 보면 좋은 문서를 이어서 제안합니다.