브로커 3 대로 운영하던 클러스터를 2 대로 줄인다. 데이터 유실 없이 줄이려면 내리려는 브로커가 갖고 있는 파티션 복제본을 남는 브로커로 옮긴 뒤 내려야 한다. 그냥 서버를 내리면 그 브로커가 리더였던 파티션은 다른 복제본이 리더를 이어받지만, 복제 계수를 채우지 못해 under-replicated 상태로 남는다.
복제 계수는 브로커 수를 넘을 수 없다. 복제 계수가 3 인 토픽이 있는 상태에서 브로커가 2 대가 되면 그 토픽은 영원히 under-replicated 다. min.insync.replicas 가 2 이고 acks=all 인 프로듀서는 계속 동작하지만, 복제본 하나가 더 빠지면 쓰기가 막힌다.
kafka-topics.sh --bootstrap-server broker01.example.com:9092 --describe | \
awk '/ReplicationFactor: 3/ {print $2, $6}'
kafka-topics.sh --bootstrap-server broker01.example.com:9092 --describe --under-replicated-partitions
__consumer_offsets 도 잊지 않는다. 이 내부 토픽은 컨슈머 그룹의 오프셋을 저장하며 기본 복제 계수가 3 인 경우가 많다. 브로커 설정 offsets.topic.replication.factor 는 토픽이 처음 만들어질 때만 쓰이므로, 이미 만들어진 토픽은 재배치로 줄여야 한다.
Option "[replication-factor]" can't be used with option "[alter]"
kafka-topics.sh --alter 로는 파티션 수만 늘릴 수 있다. 복제 계수 변경과 복제본 배치는 kafka-reassign-partitions.sh 의 일이다.
토픽 목록을 JSON 으로 만든다. 파티션을 하나하나 나열할 필요는 없다. 토픽만 적고 --generate 에 남길 브로커 ID 를 주면 배치안을 만들어 준다.
cat > topics-to-move.json <<'EOF'
{
"topics": [
{ "topic": "order-events" },
{ "topic": "__consumer_offsets" }
],
"version": 1
}
EOF
kafka-reassign-partitions.sh --bootstrap-server broker01.example.com:9092 \
--topics-to-move-json-file topics-to-move.json \
--broker-list '1001,1002' \
--generate > plan.txt
출력은 현재 배치(Current partition replica assignment)와 제안 배치(Proposed partition replica assignment) 두 덩어리다. 현재 배치는 되돌릴 때 쓰므로 따로 파일에 저장해 둔다. 제안 배치만 떼어 reassign.json 으로 만든다.
kafka-reassign-partitions.sh --bootstrap-server broker01.example.com:9092 \
--reassignment-json-file reassign.json --execute --throttle 50000000
kafka-reassign-partitions.sh --bootstrap-server broker01.example.com:9092 \
--reassignment-json-file reassign.json --verify
--throttle 은 복제 대역폭(바이트/초)을 제한한다. 운영 중에 대량으로 옮기면 디스크와 네트워크를 다 먹어 지연이 커지므로 반드시 건다. --verify 가 완료를 보고하면서 throttle 도 해제한다.
제안 배치를 손으로 고쳐도 된다. 복제 계수를 3 에서 2 로 줄이려면 각 파티션의 replicas 배열에서 브로커 하나를 빼면 된다.
{
"version": 1,
"partitions": [
{ "topic": "order-events", "partition": 0, "replicas": [1001, 1002] },
{ "topic": "order-events", "partition": 1, "replicas": [1002, 1001] }
]
}
배열의 첫 번째 브로커가 선호 리더 다. 파티션마다 앞자리를 번갈아 두어야 리더가 한쪽에 몰리지 않는다.
재배치가 끝나면 리더를 고르게 다시 맞춘다.
kafka-leader-election.sh --bootstrap-server broker01.example.com:9092 \
--election-type preferred --all-topic-partitions
마지막으로 내리려는 브로커에 남은 파티션이 없는지 확인한 뒤 프로세스를 정지한다.
--describe 출력은 브로커 ID 만 보여 주므로 어느 서버인지 알기 어렵다. KRaft 클러스터에서는 다음으로 확인한다.
kafka-broker-api-versions.sh --bootstrap-server broker01.example.com:9092 | head
kafka-metadata-quorum.sh --bootstrap-server broker01.example.com:9092 describe --status
kafka-cluster.sh cluster-id --bootstrap-server broker01.example.com:9092
kafka-broker-api-versions.sh 의 각 줄 앞머리에 호스트:포트 (id: N rack: ...) 형태로 나오므로 이것이 가장 직접적이다.
ZooKeeper 모드를 쓰는 옛 클러스터라면 znode 에서 볼 수 있다. Kafka 4.x 는 KRaft 전용이라 ZooKeeper 자체가 없고, 모든 CLI 에서 --zookeeper 옵션도 사라졌다.
zookeeper-shell.sh zk01.example.com:2181 get /brokers/ids/1001
각 서버의 ID 는 설정 파일에서도 확인한다. KRaft 는 node.id, ZooKeeper 모드는 broker.id 이며, broker.id=-1 이면 자동 생성이라 meta.properties 에 기록된 값을 봐야 한다.
grep -E '^(node|broker)\.id' /opt/kafka/config/server.properties
cat /data/kafka-logs/meta.properties
--describe 출력의 Isr 은 In-Sync Replicas, 즉 리더를 충분히 따라잡은 복제본 목록이다. Replicas 에는 있으나 Isr 에 없는 브로커는 뒤처졌거나 죽은 것이다.
Topic: order-events Partition: 0 Leader: 1001 Replicas: 1001,1002,1003 Isr: 1001,1002
이 예에서 1003 은 동기화에서 빠져 있다. 이 상태로 1001 이 죽으면 1002 가 리더가 되고, 1002 까지 빠지면 unclean.leader.election.enable 값에 따라 데이터 유실을 감수하고 리더를 뽑거나 파티션이 멈춘다. 기본값은 false 이며 유실을 막기 위해 그대로 두는 것이 좋다.