카프카 클러스터 사이의 데이터 복제. 클러스터 안의 노드끼리 주고받는 것은 복제(replication)라고 부르고, 클러스터를 넘어가는 것을 미러링(mirroring)이라고 구분해 부른다. 복제 메커니즘이 하나의 클러스터 안에서만 작동하도록 설계되었기 때문에 클러스터를 넘으려면 별도의 도구가 필요하다.

클러스터를 여럿 두는 이유

클러스터를 여럿 두는 이유는 여러 갈래다. 도시나 대륙마다 IDC가 있고 여러 IDC의 데이터를 함께 봐야 하는 애플리케이션이 있을 때, 고가용성과 재해 복구가 필요할 때, 나라마다 데이터 보존 기간 같은 정책이 달라야 할 때, 온프레미스 DB의 변경을 로컬 카프카로 내보낸 뒤 클라우드 클러스터로 옮겨야 할 때, 연결이 불안정한 IoT 기기의 데이터를 가용성 높은 집적용 클러스터로 모아야 할 때다.

데이터센터 간 통신의 제약

어느 경우든 지키는 원칙이 셋이다. 하나의 데이터센터당 한 개 이상의 클러스터를 설치한다. 데이터센터 간에 각 이벤트를 에러로 인한 재시도를 제외하면 정확히 한 번씩 복제한다. 원격 데이터센터에 쓰는 것보다 원격 데이터센터에서 읽어오는 것이 낫다.

셋째가 이 주제의 중심이다. 아파치 카프카의 브로커와 클라이언트는 하나의 IDC 안에서 실행되도록 설계되고 개발되고 테스트되고 조정되었다. 개발자들이 낮은 지연과 높은 대역폭을 상정했고 타임아웃 기본값과 각종 버퍼 크기가 전부 그 가정에 맞춰져 있다. 그래서 브로커를 서로 다른 데이터센터에 나눠 설치하는 것은 권장되지 않는다.

IDC 간 통신에는 고질적인 문제가 있다. 지연은 거리와 네트워크 홉 개수가 늘수록 커진다. 대역폭은 WAN이 단일 IDC 내부보다 훨씬 낮고 시시각각 변하는데, 지연이 높아지면 있는 대역폭조차 활용하기 어려워진다. 비용은 서로 다른 IDC나 리전, 클라우드 사이의 데이터 전송에 과금이 붙는 형태로 나온다.

원격 브로커 간 통신과 원격 프로듀서에서 브로커로의 통신을 배제하고 나면 원격 브로커에서 컨슈머로의 통신만 남는다. 그리고 여기가 안전하다. 네트워크가 끊어지면 컨슈머는 읽지 못하지만 레코드 자체는 연결이 복구될 때까지 브로커 안에 안전하게 저장되어 있어서, 네트워크 단절로 인한 데이터 유실 위험이 없다.

미러메이커 2.0

미러메이커 2.0은 카프카 커넥트 프레임워크 위에 만들어진 차세대 미러링 솔루션이다. DB가 아니라 다른 카프카 클러스터에서 데이터를 읽어오기 위해 소스 커넥터를 쓴다.

이전 버전의 단점을 여러 군데 해결했다. 태스크를 필요에 따라 서로 다른 워커 노드에 자동 할당하므로 인스턴스별 스트림 수와 서버별 인스턴스 수를 직접 정할 필요가 없다. 컨슈머 그룹 관리 프로토콜을 쓰지 않고 태스크에 파티션을 균등 배분하므로 새 토픽이나 파티션이 추가될 때 리밸런스가 발생하지 않는다. 대상 클러스터의 동일한 파티션으로 미러링해서 파티션의 의미 구조와 각 파티션 안의 이벤트 순서를 그대로 유지한다. 데이터뿐 아니라 컨슈머 오프셋, 토픽 설정, 토픽 ACL까지 마이그레이션한다.

설정은 클러스터 별칭을 접두어로 쓰는 방식이다.

clusters = NYC, LON
NYC.bootstrap.servers = kafka.nyc.example.com:9092
LON.bootstrap.servers = kafka.lon.example.com:9092
 
NYC->LON.enabled = true
NYC->LON.topics = .*

몇 가지 세부사항이 있다. tasks.max는 기본값이 1인데 최소 2 이상을 권장하고, 복제할 토픽 파티션이 많으면 더 크게 잡는다. ACL은 Literal 타입만 이전되므로 자원 이름에 접두어나 와일드카드를 썼다면 대상 클러스터에 직접 잡아줘야 한다. Topic:Write ACL은 미러메이커만 대상 토픽에 쓸 수 있게 하려고 일부러 이전하지 않는다. 대상 클러스터에서 이미 쓰이고 있는 컨슈머 그룹의 오프셋은 덮어쓰지 않아서, 작동 중인 컨슈머의 오프셋과 원본에서 마이그레이션된 오프셋이 충돌하지 않는다.

미러메이커의 설치 위치

배치는 가능한 한 대상 데이터센터에서 실행하는 것이 좋다. 이유가 앞의 원칙 셋째와 같다. 네트워크가 끊어졌을 때 컨슈머가 연결하지 못하는 편이 프로듀서가 연결하지 못하는 편보다 안전하다. 예외가 둘 있다. 지역 트래픽은 암호화가 필요 없고 데이터센터 간만 암호화해야 한다면 원본 쪽에 두고 평문으로 읽어 SSL로 쓰는 편이 낫다. 프로듀서만 SSL을 타므로 성능 영향이 작다. 하이브리드 환경에서 온프레미스가 방화벽 뒤에 있어 클라우드에서의 연결을 허용하지 않는 경우에도 미러메이커를 온프레미스에 두어 모든 연결이 온프레미스에서 클라우드 방향으로 맺어지게 한다. 어느 쪽이든 errors.tolerance=none을 잡아 빠르게 실패하게 한다. 유실 위험을 안고 계속하는 것보다 에러가 나는 편이 낫다.

모니터링과 랙 추적

모니터링의 중심은 replication-latency-ms다. 레코드의 타임스탬프와 대상 클러스터에 성공적으로 쓰인 시각 사이의 간격이고, 피크 시간대에 치솟았다가 따라잡는 것은 괜찮지만 지속적으로 증가하면 처리 용량이 부족하다는 뜻이다. 그 외에 복제 시점의 레코드 나이인 record-age-ms, 복제 처리량인 byte-rate, 오프셋 마이그레이션 지연인 checkpoint-latency-ms를 본다. 1분에 한 번 소스에 이벤트를 보내고 대상에서 읽어 복제 시간이 일정 수준을 넘으면 알림을 받는 카나리아 테스트도 유용하다.

랙 추적에는 두 가지 방법이 있는데 둘 다 100% 정확하지는 않다. kafka-consumer-groups 툴은 미러메이커가 항상 커밋하지는 않으므로(기본값이 1분에 한 번) 1분 동안 오르다가 갑자기 낮아지는 모양을 보인다. JMX의 최대 컨슈머 랙은 컨슈머가 읽어온 것만 반영하고 프로듀서가 대상 클러스터에 썼는지, 응답을 받았는지는 고려하지 않는다.

처리량 튜닝

튜닝은 랙을 얼마나 허용하느냐에서 시작한다. 허용할 수 없다면 최고 수준의 처리량을 유지하도록 크기를 키우고, 어느 정도 허용된다면 전체 시간의 9599%에 대해 7580% 사용률을 목표로 하고 피크에 랙을 감수한다. 압축된 이벤트를 다루면 압축 해제와 재압축이 일어나 CPU를 매우 많이 쓰므로 태스크 수를 늘릴 때 CPU 사용률을 봐야 한다. 민감한 토픽은 별도 미러메이커 클러스터로 분리해서, 통제할 수 없는 프로듀서 때문에 중요한 파이프라인이 밀리는 것을 막는다. 병목은 jstack으로 스레드 덤프를 떠서 찾는다. poll에 시간을 쓰고 있으면 컨슈머가, send에 쓰고 있으면 프로듀서가 병목이다.

다중 클러스터 토폴로지

토폴로지가 넷으로 갈린다.

허브-앤-스포크

여러 개의 로컬 클러스터와 하나의 중앙 클러스터를 두는 형태다. 클러스터를 둘만 쓰면 리더와 팔로워 형태가 된다. 데이터가 여러 데이터센터에서 생성되는데 일부 컨슈머가 전체 데이터를 봐야 할 때 쓴다.

데이터는 항상 로컬에서 생성되고 중앙으로 단 한 번만 미러링된다. 미러링이 한 방향이고 각 컨슈머가 언제나 같은 클러스터에서 읽으므로 배포와 설정, 모니터링이 간편하다. 대신 지역 데이터센터의 애플리케이션은 다른 데이터센터의 데이터를 쓸 수 없다.

액티브-액티브

두 개 이상의 데이터센터가 전체 데이터의 일부나 전부를 공유하면서 각각 읽기와 쓰기를 모두 하는 형태다. 인근 데이터센터에서 사용자 요청을 처리하므로 성능이 좋고, 모든 데이터센터가 동일한 기능을 가지므로 하나가 죽어도 다른 곳에서 처리된다. 장애 복구가 네트워크 리다이렉션만으로 끝난다. 대신 여러 위치에서 비동기적으로 읽고 쓰므로 충돌을 피하기 어렵고 데이터 일관성 유지도 어렵다.

가장 큰 기술적 문제는 순환 미러링이다. 같은 데이터가 클러스터 사이를 끝없이 오가는 것을 막아야 한다. 해법은 논리적 토픽마다 데이터센터별 토픽을 따로 두고 원격 데이터센터에서 생성된 토픽은 복제하지 않는 것이다. 미러메이커는 원격 토픽 앞에 클러스터 별칭을 붙이는 네이밍 컨벤션으로 이것을 구현하고, 그래서 양방향 복제 흐름을 켜는 것만으로 이 토폴로지가 설정된다. 카프카 0.11.0에 추가된 레코드 헤더로 데이터가 생성된 데이터센터를 태그할 수도 있다.

액티브-스탠바이

첫 번째 클러스터를 모든 애플리케이션이 쓰고, 두 번째 클러스터는 거의 같은 데이터를 들고 대기하는 형태다. 가장 간단해서 클러스터를 하나 더 설치하고 미러링 프로세스를 걸기만 하면 되고, 데이터 접근이나 충돌 처리 같은 문제를 걱정할 필요가 없다. 대신 멀쩡한 클러스터를 놀리게 되고 장애 복구가 보기보다 훨씬 어렵다. DR 클러스터를 작게 만들어 낭비를 줄이려는 조직도 있지만, 최소한으로 설정한 클러스터가 비상시에 제대로 작동할지 확신할 수 없다.

스트레치 클러스터

하나의 카프카 클러스터를 여러 데이터센터에 걸쳐 설치하는 형태다. 다중 클러스터가 아니라 하나의 클러스터이므로 두 클러스터를 동기화해주는 미러링 프로세스 자체가 없다. 카프카의 복제 메커니즘이 평소대로 브로커들을 동기화하고, 그것이 곧 동기적 복제다. 랙 설정으로 각 파티션의 레플리카를 여러 데이터센터에 분산시키고 min.insync.replicasacks=all을 잡으면 각 쓰기 작업이 최소 두 개의 데이터센터에서 성공한 뒤에야 응답이 간다.

DR 클러스터가 주 클러스터와 언제나 100% 동기화되어야 하는 경우에 맞고, 양쪽 데이터센터의 모든 브로커가 쓰이므로 자원 낭비가 없다. 대신 커버하는 장애의 종류가 데이터센터 전체 장애로 한정된다. 애플리케이션이나 카프카 자체의 장애는 커버하지 않고, 물리적 인프라도 필요하다.

데이터센터가 셋이어야 한다는 조건이 붙는데 주키퍼 클러스터 때문이다. 홀수 노드로 과반이 살아야 하므로, 셋이면 어느 하나에도 과반이 몰리지 않도록 배치하기 쉽다. 클라우드 제공자의 한 리전 안 세 개 가용 영역이 전형적인 경우다. 2.4.0부터는 컨슈머가 정의된 랙 기준으로 가장 가까운 레플리카에서 읽도록 브로커를 설정할 수 있다. 로컬 데이터센터의 팔로워에서 읽으면 처리율이 오르고 지연과 데이터센터 간 통신 비용이 줄어든다.

다른 미러링 솔루션

미러메이커 말고도 솔루션이 있다.

우버 uReplicator는 인스턴스에 할당된 토픽 목록과 파티션을 관리하는 중앙집중화된 컨트롤러를 Apache Helix로 구현했다. Helix 컨슈머는 리밸런스를 회피하고 대신 Helix가 주는 파티션 할당 변경을 받아서 처리한다.

링크드인 브루클린은 카프카를 포함한 서로 다른 저장소 사이에 데이터를 스트리밍하는 범용 수집 프레임워크다. 하루에 조 단위의 메시지를 미러링하는 데 쓰이고 안정성과 성능, 운영성 모두에 최적화되어 있다.

컨플루언트에는 셋이 있다. 리프리케이터는 커넥트 기반이고 레코드 헤더에 출처를 기입하는 방식으로 순환 복제를 막는다. 멀티 리전 클러스터(MRC)는 ISR 목록에 추가되지 않은 채 비동기적으로 복제하는 옵저버 개념을 도입해서, 같은 리전 안에서는 동기적으로 서로 다른 리전 사이에서는 비동기적으로 복제하게 한다. 지연이 50밀리초 미만인 구간에 적절하다. 클러스터 링킹은 클러스터 간 복제를 컨플루언트 서버에 아예 탑재한 것으로, 클러스터 안의 브로커 간 복제와 동일한 프로토콜을 써서 오프셋을 보존한 채 복제하므로 오프셋 변환 없이 클라이언트를 마이그레이션할 수 있다.

RTO와 RPO

재해 복구 계획은 두 숫자로 잡는다. RTO(복구 시간 목표)는 장애 후 모든 서비스가 작동을 재개할 때까지의 최대 시간이고, 낮은 RTO는 자동화된 장애 복구에서만 가능하므로 수동 작업과 애플리케이션 재시작을 최소화해야 한다. RPO(복구 지점 목표)는 장애로 유실될 수 있는 데이터의 최대 시간이고, 0으로 만들려면 동기적 미러링이 필요하다.

계획된 장애와 계획에 없던 장애

계획된 장애 복구는 유실을 막을 수 있다. 주 클러스터를 먼저 멈추고 미러링 프로세스가 남은 메시지를 다 옮길 때까지 기다린 뒤 애플리케이션을 옮기면 된다. 계획에 없던 장애는 다르다. DR 클러스터는 주 클러스터의 가장 최신 메시지를 가지고 있지 못하고, 미러링 솔루션들이 트랜잭션을 지원하지 않으므로 유실을 완전히 없앨 수 없다.

복구 후 읽기 시작 지점

가장 어려운 문제는 장애 복구 후 어디서부터 읽을 것인가이고, 흔히 쓰는 방법 넷이 전부 유실이나 중복을 초래할 수 있다.

자동 오프셋 재설정은 맨 처음부터 읽거나 맨 끝에서 시작한다. 앞에서 시작하면 많은 양을 다시 처리하고 끝에서 시작하면 알 수 없는 개수의 이벤트를 건너뛴다. 단순해서 끝에서 시작하는 쪽이 많이 쓰인다.

오프셋 토픽 복제는 __consumer_offsets를 DR 클러스터로 미러링하는 것인데 주의사항이 많다. 양쪽 오프셋이 일치한다는 보장이 없다. 토픽이 처음 생성된 시점부터 미러링해서 둘 다 0에서 시작했더라도 프로듀서 재시도 때문에 어긋난다. 랙이 있으므로 커밋된 오프셋이 해당 레코드보다 먼저 또는 늦게 도착할 수 있고, 커밋된 오프셋은 있는데 해당 레코드가 없는 상황을 어떻게 처리할지도 정해야 한다.

시간 기반 복구는 카프카 0.10.0부터 메시지가 타임스탬프를 갖고 브로커가 타임스탬프로 오프셋을 검색하는 인덱스와 API를 제공하는 것을 쓴다. kafka-consumer-groups 툴로 특정 컨슈머 그룹의 모든 토픽 오프셋을 특정 시점으로 초기화할 수 있어서, 어느 정도의 확실성을 보장해야 할 때 권장된다.

오프셋 변환은 미러링 툴이 서로 매핑되는 오프셋 값을 카프카 토픽에 저장해두는 방식이다. 복구할 때 타임스탬프 대신 매핑에서 대응 오프셋을 찾아 작업을 재개한다. 커밋된 오프셋이 레코드보다 먼저 미러링되거나 제때 미러링되지 않는 문제는 여전히 남는다.

복구 뒤에 남는 일

복구가 끝난 뒤에도 일이 남는다. 장애가 났던 주 클러스터를 DR 역할로 바꿔야 하는데, 이전 주 클러스터는 DR이 가지고 있지 않은 이벤트를 가지고 있을 가능성이 높다. 반대 방향으로 미러링을 시작하면 그 여분의 이벤트가 남아서 두 클러스터의 내용물이 서로 달라진다. 일관성과 순서가 극도로 중요하다면 가장 간단한 해법은 원래 주 클러스터에 저장된 데이터와 커밋된 오프셋을 완전히 삭제하고 새 주 클러스터에서 처음부터 미러링을 시작하는 것이다.

애플리케이션이 장애 복구용 클러스터를 찾을 방법도 미리 정해둬야 한다. 호스트 이름을 하드코딩하지 않고 이름을 최대한 단순하게 정한 뒤 DNS로 주 클러스터를 가리키다가, 비상시에 DNS 이름을 스탠바이 클러스터로 돌린다. 디스커버리 서비스가 모든 브로커의 IP를 가질 필요는 없다. 카프카 클라이언트는 하나의 브로커에만 성공적으로 접근하면 클러스터 메타데이터를 얻어 나머지를 찾으므로 대체로 세 개면 된다.

관련

출처