Kafka Schema Registry — 비즈니스 이벤트 계약
주문이 하나 생기면 정산 쪽도 알아야 하고 알림도 나가야 하는데, 서비스끼리 직접 API 로 부르게 하면 부르는 쪽이 받는 쪽을 전부 알고 있어야 하고 받는 쪽이 하나 늘 때마다 발행하는 코드를 고쳐야 했습니다. 그래서 사이에 Kafka 를 뒀고, 발행하는 쪽은 토픽에 이벤트를 넣기만 할 뿐 누가 읽는지는 모릅니다.
같은 Kafka 클러스터로 로그도 들어오고, 노드마다 뜬 Fluent Bit 가 컨테이너 로그를 읽어 토픽에 넣고 뒤에서 그걸 받아 저장소로 옮기는 경로입니다. 서비스 간 이벤트와 로그가 한 클러스터 안에 같이 있는 상태.
여기에 Schema Registry 가 같이 나왔습니다. 이름만 보면 Kafka 로 들어가는 모든 메시지를 검사하는 문지기처럼 읽힙니다. 실제 역할은 그보다 좁았고, 범위를 잘못 잡으면 로그 파이프라인까지 여기 걸리는 줄 알게 됩니다.
로그 아닌 비즈니스 이벤트 보호
Fluent Bit 에서 Kafka 로 가는 로그는 Schema Registry 를 안 거칩니다. 로그 수집 파이프라인과 Schema Registry 는 아예 별개로 돌아서, 로그는 스키마 검증 없이 그대로 토픽에 들어갑니다.
Schema Registry 가 지키는 건 서비스 간 비즈니스 이벤트 쪽입니다. 주문 생성이나 결제 완료 같은 이벤트를 서비스 A 가 발행하고 서비스 B 가 구독할 때, 그 구조(주로 Avro 스키마)를 등록해 두고 맞는지 보니, 같은 Kafka 를 쓰더라도 두 흐름은 구분해야 합니다.
발행자와 구독자 사이의 계약
이벤트 기반 구조에서 발행자와 구독자는 서로를 직접 모른 채 토픽을 사이에 두고 붙어 있을 뿐입니다. 그래서 발행자가 필드 이름을 바꾸거나 타입을 바꿔도 구독자 쪽에는 아무 신호가 안 가고, 구독자는 그걸 모른 채 파싱하다 깨집니다.
Schema Registry 는 그 이벤트 구조를 양쪽이 합의한 계약으로 만들어 등록해 둡니다. 주문 생성 이벤트라면 이런 모양.
{
"type": "record",
"name": "OrderCreated",
"fields": [
{ "name": "orderId", "type": "string" },
{ "name": "userId", "type": "string" },
{ "name": "amount", "type": "int" },
{ "name": "createdAt", "type": "long" }
]
}
발행자가 무엇을 보내기로 했는지가 코드 밖에 명시적으로 남으니, 구독자는 이 정의를 보고 파싱하면 되고 필드가 바뀌었을 때도 코드를 뒤질 게 아니라 여기 등록된 버전을 비교하면 그만입니다.
호환성 모드 — 런타임 장애를 배포 실패로
계약을 등록해 두는 것만으로는 누가 그 계약을 바꾸는 걸 막지 못하고, 실무에서 효과가 난 건
그다음이었습니다. Schema Registry 에 BACKWARD 같은 호환성 모드를 걸어 두면 호환되지 않는
스키마를 등록하는 시점에 거부해 버립니다.
BACKWARD— 새 스키마로 옛 데이터를 읽을 수 있어야 함(구독자 먼저 업그레이드)FORWARD— 옛 스키마로 새 데이터를 읽을 수 있어야 함FULL— 양방향 다
예를 들어 BACKWARD 를 걸어 둔 상태에서 발행자가 필수 필드를 하나 더 넣는다고 해 봅니다.
{ "name": "createdAt", "type": "long" },
{ "name": "couponId", "type": "string" }
기본값이 없는 필수 필드라 이 스키마로는 couponId 가 없던 옛 데이터를 못 읽고, 등록하려 하면
거기서 막힙니다.
409 Conflict
Schema being registered is incompatible with an earlier schema
이게 없으면 발행자가 바뀐 이벤트를 그대로 내보내고 구독자가 운영 중에 파싱하다 NPE 나 KeyError 로 죽습니다. 호환성 모드를 걸어 두면 그 사고가 배포 단계에서 등록 거부로 먼저 나타나니, 운영 중에 터질 장애가 배포 실패로 앞당겨집니다.
필드를 더하고 싶으면 기본값을 주면 됩니다.
{ "name": "couponId", "type": ["null", "string"], "default": null }
default 가 있으면 옛 데이터에 이 필드가 없어도 그 값으로 채워 읽으니 BACKWARD 를 통과합니다.
운영 장애를 배포 시점에 미리 막기
Schema Registry 도입을 정할 때는 Kafka 를 쓰느냐가 아니라 서비스끼리 이벤트로 얘기하고 있느냐를 봅니다. 로그를 모으는 용도로만 Kafka 를 쓰고 있으면 여기서 얻을 게 없고 서비스 A 가 발행한 이벤트를 서비스 B 가 파싱하는 구조가 있으면 그 이벤트가 Schema Registry 가 지켜야 할 계약입니다. 등록만 해 두지 말고 호환성 모드까지 걸어야 계약이 실제로 강제됩니다.
댓글 0