soobook
INFRA

Harbor Proxy Cache

Harbor proxy cache는 외부 registry의 이미지를 요청 시점에 당겨와 cache하는 pull-through proxy다.

Intro

Docker Hub에서 미인증 pull은 IP당 6시간에 100회, 무료 인증 계정은 6시간에 200회로 제한된다.

문제는 이 제한이 IP 기준이라는 데 있다. Kubernetes cluster의 node 수십 대가 하나의 NAT gateway로 나가면, Docker Hub 눈에는 그 전부가 하나의 IP다.

배포가 몰리는 순간 cluster 전체가 한도를 먼저 소진하고, 그다음부터 모든 node에서 429 Too Many Requests가 터진다.

ImagePullBackOff가 무더기로 뜬다

Harbor proxy cache 를 사용하면 외부로 나가는 pull을 cluster 밖 한 지점으로 모아서 같은 이미지는 한 번만 당기고 나머지는 local에서 돌려줄 수 있다.

Why a Proxy Cache

핵심은 Registry Image 를 내부로 들여오는건데, 미리 전부 복사해 두는 replication(mirror)도 있고, 요청 시점에 당기는 proxy cache도 있다.

Replication은 사전에 복사하는 방법이다. “이 저장소의 이 tag들”을 규칙으로 정해 미리 통째로 당겨 둔다. 이 방법은 무엇을 쓸지 미리 알아야 하고 안 쓰는 이미지까지 복사한다.

Proxy cache는 지연 복사하는 방법이다. client가 요청하는 순간 상위 registry에서 당겨 서빙하고, 그 사본을 cache해 다음 요청부터 local로 돌려준다.

rate limit 우회에는 proxy cache면 충분하다.

목록을 유지할 필요 없이, “무엇을 당기든 두 번째부터는 바깥에 안 나간다”가 단순하면서도 문제를 정확하게 해결할 수 있다.

How It Works

cluster의 pull이 하나의 IP로 묶여 Harbor proxy cache로 들어오면, 이미지가 그대로면 HEAD 확인만으로 cache된 사본을 서빙해 Docker Hub 한도를 안 쓰고, cache에 없거나 갱신됐을 때만 상위에서 새로 당겨 한도에 잡힌다

첫 요청 때 이미지가 cache에 없으면, Harbor가 상위 registry에서 당겨와 local 이미지처럼 서빙하고 그 사본을 저장한다.

이후 같은 이미지 요청이 오면 Harbor는 상위 registry의 최신 manifest를 확인해 네 갈래로 갈린다.

  • 상위에서 이미지가 안 바뀌었으면 → cache된 사본을 서빙한다.
  • 상위에서 바뀌었으면 → 새 이미지를 당겨 서빙하고 다시 cache한다.
  • 상위가 접속 불가면 → cache된 사본을 서빙한다. (upstream 장애 내성)
  • 상위에서 이미지가 사라졌지만 cache엔 남아 있으면 → cache된 사본을 서빙한다.

rate limit 우회의 핵심은 “안 바뀌었는지 확인”하는 방식에 있는데, Harbor proxy cache는 layer가 갱신됐는지 확인할 때 HEAD 요청을 쏜다.

Docker의 rate limiter는 이 HEAD manifest 조회를 카운트하지 않는다. 즉 cache hit인 경우 Docker Hub 한도를 전혀 소비하지 않는다.

Configuration

설정은 꽤나 간편한데, endpoint를 만들고, 그 endpoint를 쓰는 proxy cache project를 만든다.

Registry endpoint 생성

Administration → Registries에서 New Endpoint를 만든다.

Provider(e.g. Docker Hub)와 이름을 넣고 Test Connection으로 확인한다.

Proxy cache project 생성

New Project에서 Proxy Cache slider를 켜고 방금 만든 endpoint를 고른다.

Bandwidth는 상위로 나가는 pull 속도 제한이며 -1은 무제한이다.

Pulling Through the Cache

cache를 경유하려면 이미지 경로 앞에 Harbor와 proxy cache project를 붙인다. project 이름이 dockerhub-proxy라면 이렇게 된다.

# 원래 경로
docker pull nginx:latest
docker pull docker.io/library/nginx:latest

# proxy cache 경유
docker pull harbor.example.com/dockerhub-proxy/library/nginx:latest

library/는 Docker Hub의 공식 이미지 namespace다. nginx처럼 짧게 쓰던 것도 proxy 경로에서는 library/nginx로 풀어 써야 한다.

Wiring It Into Kubernetes

그럼 cluster의 모든 manifest를 이 긴 경로로 바꿔야 하느냐? 그건 좀 귀찮다.

containerd registry mirror 방식을 쓰면 된다.

node의 container runtime에게 “docker.io로 가는 pull은 Harbor proxy로 우회하라”고 알려주는 방식이다.

manifest는 원래 이미지 이름을 그대로 두고, runtime level에서 투명하게 redirect된다. /etc/containerd/certs.d/docker.io/hosts.toml을 이렇게 둔다.

server = "https://registry-1.docker.io"

[host."https://harbor.example.com/v2/dockerhub-proxy"]
  capabilities = ["pull", "resolve"]
  override_path = true

Retention and Garbage Collection

cache는 무한히 쌓이면 storage를 잡아먹는다. Harbor는 새 proxy cache project마다 기본 7일 retention 정책을 자동으로 건다. 7일간 당겨지지 않은 cache 이미지는 정리 대상이 된다.

다만 retention은 “무엇을 지울지 표시”까지고, 실제 disk 공간 회수는 garbage collection이 별도로 돌아야 이뤄진다.

tag가 지워져도 참조 없는 layer가 남아 있으면 GC가 이를 수거한다. proxy cache는 성격상 cache 회전이 빠르므로, GC schedule을 적절히 잡지 않으면 storage가 예상보다 빨리 찬다.

References