Kafka 3.7.0 集群进行水平扩容 (Kraft模式) - 扩容 broker、controller 节点

背景

近期业务环境压力爬升,Kafka 作为后端业务的核心组件,存在业务压力警告状态,资源利用率一直很高,打算将原有 3 节点的kafka集群进行节点水平扩容,提升消息中间件的业务吞吐量。

  • Kafka 版本: 3.7.0
  • Kafka 节点角色:controller,broker
  • Zk 节点:none (采用 Kraft 模式)

计划扩容两个节点,(为了演示,这里分别扩容一个 broker 和一个 controller ),先进行本地环境的水平扩容预演。


1. 开始搭建初始集群

本地模拟环境采用 Docker 搭建,拓扑如下:

名称 ip role
controller-1 172.23.1.11 controller
controller-2 172.23.1.12 controller
controller-3 172.23.1.13 controller
broker-1 172.23.1.14 broker
broker-2 172.23.1.15 broker
broker-3 172.23.1.16 broker
kafka-ui 172.23.1.20 web-ui

1.1 docker-compose 定义

docmer-compose.yaml 定义如下:

services:
  controller-1:
    image: apache/kafka:3.7.0
    container_name: controller-1
    privileged: true
    user: root
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: controller
      KAFKA_LISTENERS: CONTROLLER://:9093
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@controller-1:9093,2@controller-2:9093,3@controller-3:9093
      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
      CLUSTER_ID: gQ3YBuwyQsiCIbN3Ilq1cQ
    volumes:
      - ./volumes/controller-1:/tmp/kafka-logs
    networks:
      kafka_horizontal_upscale_network:
        ipv4_address: 172.23.1.11

  controller-2:
    image: apache/kafka:3.7.0
    container_name: controller-2
    privileged: true
    user: root
    environment:
      KAFKA_NODE_ID: 2
      KAFKA_PROCESS_ROLES: controller
      KAFKA_LISTENERS: CONTROLLER://:9093
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@controller-1:9093,2@controller-2:9093,3@controller-3:9093
      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
      CLUSTER_ID: gQ3YBuwyQsiCIbN3Ilq1cQ
    volumes:
      - ./volumes/controller-2:/tmp/kafka-logs
    networks:
      kafka_horizontal_upscale_network:
        ipv4_address: 172.23.1.12

  controller-3:
    image: apache/kafka:3.7.0
    container_name: controller-3
    privileged: true
    user: root
    environment:
      KAFKA_NODE_ID: 3
      KAFKA_PROCESS_ROLES: controller
      KAFKA_LISTENERS: CONTROLLER://:9093
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@controller-1:9093,2@controller-2:9093,3@controller-3:9093
      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
      CLUSTER_ID: gQ3YBuwyQsiCIbN3Ilq1cQ
    volumes:
      - ./volumes/controller-3:/tmp/kafka-logs
    networks:
      kafka_horizontal_upscale_network:
        ipv4_address: 172.23.1.13

  broker-1:
    image: apache/kafka:3.7.0
    container_name: broker-1
    ports:
      - 29092:9092
    environment:
      KAFKA_NODE_ID: 4
      KAFKA_PROCESS_ROLES: broker
      KAFKA_LISTENERS: 'PLAINTEXT://:19092,PLAINTEXT_HOST://:9092'
      KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://broker-1:19092,PLAINTEXT_HOST://localhost:29092'
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@controller-1:9093,2@controller-2:9093,3@controller-3:9093
      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
      KAFKA_NUM_PARTITIONS: 3
      CLUSTER_ID: gQ3YBuwyQsiCIbN3Ilq1cQ
    volumes:
      - ./volumes/broker-1:/tmp/kafka-logs
    networks:
      kafka_horizontal_upscale_network:
        ipv4_address: 172.23.1.14
    depends_on:
      - controller-1
      - controller-2
      - controller-3

  broker-2:
    image: apache/kafka:3.7.0
    container_name: broker-2
    ports:
      - 39092:9092
    environment:
      KAFKA_NODE_ID: 5
      KAFKA_PROCESS_ROLES: broker
      KAFKA_LISTENERS: 'PLAINTEXT://:19092,PLAINTEXT_HOST://:9092'
      KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://broker-2:19092,PLAINTEXT_HOST://localhost:39092'
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@controller-1:9093,2@controller-2:9093,3@controller-3:9093
      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
      KAFKA_NUM_PARTITIONS: 3
      CLUSTER_ID: gQ3YBuwyQsiCIbN3Ilq1cQ
    volumes:
      - ./volumes/broker-2:/tmp/kafka-logs
    networks:
      kafka_horizontal_upscale_network:
        ipv4_address: 172.23.1.15
    depends_on:
      - controller-1
      - controller-2
      - controller-3

  broker-3:
    image: apache/kafka:3.7.0
    container_name: broker-3
    ports:
      - 49092:9092
    environment:
      KAFKA_NODE_ID: 6
      KAFKA_PROCESS_ROLES: broker
      KAFKA_LISTENERS: 'PLAINTEXT://:19092,PLAINTEXT_HOST://:9092'
      KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://broker-3:19092,PLAINTEXT_HOST://localhost:49092'
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@controller-1:9093,2@controller-2:9093,3@controller-3:9093
      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
      KAFKA_NUM_PARTITIONS: 3
      CLUSTER_ID: gQ3YBuwyQsiCIbN3Ilq1cQ
    volumes:
      - ./volumes/broker-3:/tmp/kafka-logs
    networks:
      kafka_horizontal_upscale_network:
        ipv4_address: 172.23.1.16
    depends_on:
      - controller-1
      - controller-2
      - controller-3

  kafka-ui:
    image: docker.io/provectuslabs/kafka-ui:v0.7.2
    container_name: kafka-ui
    ports:
      - 8080:8080
    environment:
      DYNAMIC_CONFIG_ENABLED: true
      SPRING_CONFIG_ADDITIONAL-LOCATION: /kafka-ui/config.yaml
    volumes:
      - /root/kafka/kafka-ui/config.yaml:/kafka-ui/config.yaml
    networks:
      kafka_horizontal_upscale_network:
        ipv4_address: 172.23.1.20
    depends_on:
      - broker-1
      - broker-2
      - broker-3

networks:
  kafka_horizontal_upscale_network:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 172.23.1.0/24

其中,定义了一个 kafka-ui 的管理工具容器,可以通过页面的方式对 topic 和 consumer 进行直观的管理和查看。
其所需要的配置文件如下 config.yaml

auth:
  type: LOGIN_FORM
kafka:
  clusters:
    - bootstrapServers: broker-1:19092,broker-2:19092,broker-3:19092
      name: local
management:
  health:
    ldap:
      enabled: false
metrics:
  port: 9081
  type: JMX
spring:
  security:
    oauth2: null
    user:
      name: admin
      password: admin

1.2 启动初始集群

我们先在本地目录下,批量创建容器需要挂载的目录:

>_ mkdir -pv ./volumes/{controller,broker}-{1..4}
mkdir: created directory './volumes'
mkdir: created directory './volumes/broker-1'
mkdir: created directory './volumes/broker-2'
mkdir: created directory './volumes/broker-3'
mkdir: created directory './volumes/broker-4'
mkdir: created directory './volumes/controller-1'
mkdir: created directory './volumes/controller-2'
mkdir: created directory './volumes/controller-3'
mkdir: created directory './volumes/controller-4'

然后,启动 docker-compose 容器组:

>_ docker-compose up -d

等待两分钟过后,查看容器日志是否运行正常。

1.3 检查集群是否正常运行

# docker exec -it broker-1 bash
fd7bdb2e5ab1:/$ /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:19092 --list

fd7bdb2e5ab1:/$

OK,这一步我们进到 broker-1 容器中,使用附带的工具查看 topics 信息。

可以看到,由于没有任何一个 topic,所以这里显示为空。

1.4 对kafka集群进行消息写入和消费测试

我们通过自带脚本创建一个测试 topic:

#  docker exec -it broker-1 bash   
fd7bdb2e5ab1:/$ /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server broker-1:19092 --topic test123
>abcd
[2025-03-05 06:40:44,170] WARN [Producer clientId=console-producer] Error while fetching metadata with correlation id 7 : {test123=UNKNOWN_TOPIC_OR_PARTITION} (org.apache.kafka.clients.NetworkClient)                                                     
[2025-03-05 06:40:44,284] WARN [Producer clientId=console-producer] Error while fetching metadata with correlation id 8 : {test123=UNKNOWN_TOPIC_OR_PARTITION} (org.apache.kafka.clients.NetworkClient)                                                     
[2025-03-05 06:40:44,519] WARN [Producer clientId=console-producer] Error while fetching metadata with correlation id 9 : {test123=UNKNOWN_TOPIC_OR_PARTITION} (org.apache.kafka.clients.NetworkClient)

由于 topic 默认是允许自动创建的,所以写入一条消息后,按 ctrl-D 进行退出,该 topic 根据默认配置创建了 3 个主分区。

#  docker exec -it broker-1 bash
fd7bdb2e5ab1:/$ /opt/kafka/bin/kafka-topics.sh --bootstrap-server broker-1:19092 --list
test123

我们查看 topics 列表后,可以看到刚创建的 topic test123

我们利用 kafka-console-consumer.sh 脚本对这个 topic 进行从头消费:

#  docker exec -it broker-1 bash 
fd7bdb2e5ab1:/$ /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server broker-2:19092 --topic test123 --from-beginning
abcd

我们在通过映射到本机 8080 端口的 kafka-ui 工具查看 kafka 集群的情况:

可以看到,由于已经有了 consumer 所以,kafka 自动创建了 __consumer_offsets topic,并且默认为 50 个分区。

1.5 在外部宿主机节点进行消息读写

我们在宿主机上安装 kafka 工具,在宿主机上进程读写测试:

# cd ~/kafka
# wget https://archive.apache.org/dist/kafka/3.7.0/kafka_2.13-3.7.0.tgz
# tar -xf kafka_2.13-3.7.0.tgz
# ln -n kafka_2.13-3.7.0 kafka
# yum install java-17-openjdk-devel java-17-openjdk -y
# cd ~/kafka/kafka/bin
# ./bin/kafka-console-producer.sh --bootstrap-server localhost:29092,localhost:39092,localhost:49092 --topic test123
>123456
>7890

同样,使用 ctrl-D 进行退出。


可以看到,该 test123 topic 已经有了 3 条消息。
证明,从外部写入 kafka 集群是正常的。


kafka-ui 可以对 topic 的消息进行浏览。

2. 对 kafka 集群的 broker 节点进行扩容

查阅 kafka 镜像的官方文档得知,apache/kafka 镜像封装的脚本中,已经配置有默认的 ["CLUSTER_ID"]="5L6g3nShT-eMCtK--X86sw",初始情况下,可以利用环境变量 CLUSTER_ID 进行覆盖此默认值。
文档见:https://github.com/apache/kafka/blob/18eca0229dc9f33c3253c705170a25b7f78eac40/docker/resources/common-scripts/configureDefaults#L20

由于我们初始集群已经单独设定了 CLUSTER_ID 环境变量,准备新增的节点也需要同步设置该环境变量。

新“节点”的启动脚本中会利用该 CLUSTER_ID=gQ3YBuwyQsiCIbN3Ilq1cQ 对数据目录下的 meta.properties 文件进行配置。

Tips: 如果实际环境中,新节点要加入旧集群,需要从旧集群的任一节点数据目录中拿到 cluster.id,然后利用 ./bin/kafka-storage.sh format -t "${CLUSTER_ID}" -c config/server.properties 工具生成新节点数据目录下的 meta.properties 文件。

扩容的节点,我们采用手动 docker run 的方式,新启动一个容器(节点),并在启动容器命令中指明 --network 的方式,让新节点与 docker-compose 方式启动的旧集群处于同一网络环境内。

# docker run --name broker-4 -d \
  -e KAFKA_NODE_ID=7 \
  -e KAFKA_PROCESS_ROLES=broker \
  -e KAFKA_LISTENERS="PLAINTEXT://:19092,PLAINTEXT_HOST://:9092" \
  -e KAFKA_ADVERTISED_LISTENERS='PLAINTEXT://broker-4:19092,PLAINTEXT_HOST://localhost:59092' \
  -e KAFKA_INTER_BROKER_LISTENER_NAME=PLAINTEXT \
  -e KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER \
  -e KAFKA_LISTENER_SECURITY_PROTOCOL_MAP='CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT' \
  -e KAFKA_CONTROLLER_QUORUM_VOTERS='1@controller-1:9093,2@controller-2:9093,3@controller-3:9093' \
  -e KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS=0 \
  -e CLUSTER_ID=gQ3YBuwyQsiCIbN3Ilq1cQ \
  -p 59092:9092 \
  -u root \
  --network kafka_kafka_horizontal_upscale_network \
  -v ./volumes/broker-4:/tmp/kafka-logs \
  apache/kafka:3.7.0

查看新增节点的 meta.properties 集群元数据文件。

>_ cat /tmp/kafka-logs/meta.properties
#
#Thu Mar 06 03:22:03 GMT 2025
cluster.id=Some(gQ3YBuwyQsiCIbN3Ilq1cQ)
directory.id=AGAMAiENnqOPETmsPTv6tw
node.id=7
version=1

查看新增节点 broker-4 的日志:

# docker logs -f broker-4
....
        zookeeper.ssl.truststore.location = null
        zookeeper.ssl.truststore.password = null
        zookeeper.ssl.truststore.type = null
 (kafka.server.KafkaConfig)
[2025-03-06 03:22:06,448] INFO [BrokerLifecycleManager id=7] The broker is in RECOVERY. (kafka.server.BrokerLifecycleManager)
[2025-03-06 03:22:06,449] INFO [BrokerServer id=7] Waiting for the broker to be unfenced (kafka.server.BrokerServer)
[2025-03-06 03:22:06,489] INFO [BrokerLifecycleManager id=7] The broker has been unfenced. Transitioning from RECOVERY to RUNNING. (kafka.server.BrokerLifecycleManager)                                                                                    
[2025-03-06 03:22:06,490] INFO [BrokerServer id=7] Finished waiting for the broker to be unfenced (kafka.server.BrokerServer)
[2025-03-06 03:22:06,493] INFO authorizerStart completed for endpoint PLAINTEXT_HOST. Endpoint is now READY. (org.apache.kafka.server.network.EndpointReadyFutures)                                                                                         
[2025-03-06 03:22:06,494] INFO authorizerStart completed for endpoint PLAINTEXT. Endpoint is now READY. (org.apache.kafka.server.network.EndpointReadyFutures)                                                                                              
[2025-03-06 03:22:06,494] INFO [SocketServer listenerType=BROKER, nodeId=7] Enabling request processing. (kafka.network.SocketServer)                                                                                                                       
[2025-03-06 03:22:06,499] INFO Awaiting socket connections on 0.0.0.0:9092. (kafka.network.DataPlaneAcceptor)
[2025-03-06 03:22:06,504] INFO Awaiting socket connections on 0.0.0.0:19092. (kafka.network.DataPlaneAcceptor)
[2025-03-06 03:22:06,508] INFO [BrokerServer id=7] Waiting for all of the authorizer futures to be completed (kafka.server.BrokerServer)                                                                                                                    
[2025-03-06 03:22:06,508] INFO [BrokerServer id=7] Finished waiting for all of the authorizer futures to be completed (kafka.server.BrokerServer)                                                                                                           
[2025-03-06 03:22:06,508] INFO [BrokerServer id=7] Waiting for all of the SocketServer Acceptors to be started (kafka.server.BrokerServer)                                                                                                                  
[2025-03-06 03:22:06,509] INFO [BrokerServer id=7] Finished waiting for all of the SocketServer Acceptors to be started (kafka.server.BrokerServer)                                                                                                         
[2025-03-06 03:22:06,509] INFO [BrokerServer id=7] Transition from STARTING to STARTED (kafka.server.BrokerServer)
[2025-03-06 03:22:06,509] INFO Kafka version: 3.7.0 (org.apache.kafka.common.utils.AppInfoParser)
[2025-03-06 03:22:06,509] INFO Kafka commitId: 2ae524ed625438c5 (org.apache.kafka.common.utils.AppInfoParser)
[2025-03-06 03:22:06,510] INFO Kafka startTimeMs: 1741231326509 (org.apache.kafka.common.utils.AppInfoParser)
[2025-03-06 03:22:06,511] INFO [KafkaRaftServer nodeId=7] Kafka Server started (kafka.server.KafkaRaftServer)
[2025-03-06 03:32:05,608] INFO [RaftManager id=7] Node 2 disconnected. (org.apache.kafka.clients.NetworkClient)

集群拓扑图也发生了变化:

名称 ip role
controller-1 172.23.1.11 controller
controller-2 172.23.1.12 controller
controller-3 172.23.1.13 controller
broker-1 172.23.1.14 broker
broker-2 172.23.1.15 broker
broker-3 172.23.1.16 broker
kafka-ui 172.23.1.20 web-ui
broker-4 172.23.1.17 新增 broker

2.1 利用 kafka-console-consumer.sh 指定在新节点进行消费

在主机上,连接 broker-4 新节点的 adversted 端口进行消费:

# ./bin/kafka-console-consumer.sh --bootstrap-server localhost:59092 --topic test123 --from-beginning
abcd
123456
7890

可以看到,我们指定的 --bootstrap-server localhost:59092 连接到新节点,是能正常路由的,并且正常消费。

2.2 让新节点分配分区

要让新加入的节点分配分区,有如下几种方法:

    1. 手动利用脚本迁移现有的 topic
    1. 扩容现有 topic 分区数
    1. 重新创建 topic

2.2.1 利用自带的 kafka-reassign-partitions.sh 生成迁移计划

首先,需要准备一个 json 文件,其中包含有需要执行重新分配的 topics 列表:

# cat 01-topics.json
{
    "topics":
    [
        {
            "topic": "test123"
        }
    ],
    "version": 1
}

其次,执行脚本生成迁移计划文件:

>_ ./bin/kafka-reassign-partitions.sh --bootstrap-server localhost:29092 --generate --topics-to-move-json-file 01-topics.json --broker-list 4,5,6,7 | tee  02-plan.json
Current partition replica assignment
{"version":1,"partitions":[{"topic":"test123","partition":0,"replicas":[4,7],"log_dirs":["any","any"]},{"topic":"test123","partition":1,"replicas":[5,4],"log_dirs":["any","any"]},{"topic":"test123","partition":2,"replicas":[6,5],"log_dirs":["any","any"]}]}

Proposed partition reassignment configuration
{"version":1,"partitions":[{"topic":"test123","partition":0,"replicas":[5,6],"log_dirs":["any","any"]},{"topic":"test123","partition":1,"replicas":[6,7],"log_dirs":["any","any"]},{"topic":"test123","partition":2,"replicas":[7,4],"log_dirs":["any","any"]}]}

注意:此处在 kafka-ui 页面上对 test123 topic 进行了增加副本的操作,原设定没有副本分区,将 replication-factor 从 1 设置为 2 后,kafka 会对该topic的每个分区新增一份副本分区。

可以看到这里 Proposed partition reassignment configuration 生成的计划中,对现有分区及其副本分区进行了自动分配。

我们对该输出 json 文件进行拆分,分为 02-current-assign.json02-plan-assign.json 两个 json 文件:

>_ sed -n '2p' 02-plan.json > 02-current-assign.json 
>_ sed -n '5p' 02-plan.json > 02-plan-assign.json

02-current-assign.json 可以用于恢复分区迁移操作。
02-plan-assign.json 用于执行分区迁移操作。

最后,根据分区迁移建议,执行迁移操作:

>_ ./bin/kafka-reassign-partitions.sh --bootstrap-server localhost:29092 --execute  --reassignment-json-file 02-plan-assign.json
Current partition replica assignment

{"version":1,"partitions":[{"topic":"test123","partition":0,"replicas":[4,7],"log_dirs":["any","any"]},{"topic":"test123","partition":1,"replicas":[5,4],"log_dirs":["any","any"]},{"topic":"test123","partition":2,"replicas":[6,5],"log_dirs":["any","any"]}]}

Save this to use as the --reassignment-json-file option during rollback
Successfully started partition reassignments for test123-0,test123-1,test123-2

至此,分区迁移结束。


在 kafka-ui 上观察迁移结果的分布。

2.2.2 扩容现有分区数

为了方便,这里就在 kafka-ui 上进行点击操作:


将分区从 3 扩容至 10 个。


扩容成功。

3. 扩容 controller 节点(可选)

仍旧利用 docker run 的方式手动新启一个controller“节点”。

切记,在实际生产业务环境中,controller 节点应保证足够的奇数节点,且 controller 节点数量不宜过大

3.1 启动新的 controller-4 节点

docker run --name controller-4 -d   \
        -e KAFKA_NODE_ID=8   \
        -e KAFKA_PROCESS_ROLES=controller \
        -e KAFKA_LISTENERS="CONTROLLER://:9093"  \
        -e KAFKA_INTER_BROKER_LISTENER_NAME=PLAINTEXT \
        -e KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER  \
        -e KAFKA_CONTROLLER_QUORUM_VOTERS='1@controller-1:9093,2@controller-2:9093,3@controller-3:9093,8@controller-4:9093' \
        -e KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS=0  \
        -e CLUSTER_ID=gQ3YBuwyQsiCIbN3Ilq1cQ \
        -u root  \
        --network kafka_kafka_horizontal_upscale_network  \
        --ip 172.23.1.18 \
        -v ./volumes/controller-4:/tmp/kafka-logs \
        apache/kafka:3.7.0

3.2 查看集群角色状态信息

我们登录任意一个容器内部,利用 kafka-metadata-quorum.sh 工具查看集群中节点的状态

>_ docker exec -it broker-1 bash
e058011f15d8:/# cd /opt/kafka
e058011f15d8:/opt/kafka# ./bin/kafka-metadata-quorum.sh --bootstrap-controller 172.23.1.18:9093 describe --replication
NodeId  LogEndOffset    Lag     LastFetchTimestamp      LastCaughtUpTimestamp   Status
2       4339            0       1741241147141           1741241147141           Leader
1       4339            0       1741241147132           1741241147132           Follower
3       4339            0       1741241147132           1741241147132           Follower
4       4339            0       1741241147119           1741241147119           Observer
5       4339            0       1741241147119           1741241147119           Observer
6       4339            0       1741241147119           1741241147119           Observer
7       4339            0       1741241147119           1741241147119           Observer
8       4339            0       1741241147119           1741241147119           Observer

我们看到,NodeId 为 1,2,3,8 节点的状态应该为 Leader 或者 Follower,但是这里的新增节点 8 却和其他 broker 节点一样显示为 Observer

3.3 更新旧集群中的 controller 节点配置信息

我们更新 docker-compose.yaml 配置中 controller-1 的 KAFKA_CONTROLLER_QUORUM_VOTERS 环境变量。
从旧的配置 KAFKA_CONTROLLER_QUORUM_VOTERS: 1@controller-1:9093,2@controller-2:9093,3@controller-3:9093
更新配置 KAFKA_CONTROLLER_QUORUM_VOTERS: 1@controller-1:9093,2@controller-2:9093,3@controller-3:9093,8@controller-4:9093
即,新增 8@controller-4:9093 新的 controller-4 节点配置。

然后重建 controller-1 节点容器:

>_ docker-compose down controller-1
>_ docker-compose up controller-1 -d

查看 controller-1 的新配置启动情况:

>_ docker logs -f controller-1
....
[2025-03-06 06:02:26,210] INFO Deleted producer state snapshot /tmp/kafka-logs/__cluster_metadata-0/00000000000000003912.snapshot (org.apache.kafka.storage.internals.log.SnapshotFile)                                                                      
[2025-03-06 06:02:26,210] INFO [LogLoader partition=__cluster_metadata-0, dir=/tmp/kafka-logs] Producer state recovery took 3ms for snapshot load and 0ms for segment recovery from offset 0 (kafka.log.UnifiedLog$)                                         
[2025-03-06 06:02:26,733] INFO [ProducerStateManager partition=__cluster_metadata-0] Wrote producer snapshot at offset 3912 with 0 producer ids in 24 ms. (org.apache.kafka.storage.internals.log.ProducerStateManager)                                      
[2025-03-06 06:02:26,758] INFO [LogLoader partition=__cluster_metadata-0, dir=/tmp/kafka-logs] Loading producer state till offset 3912 with message format version 2 (kafka.log.UnifiedLog$)                                                                 
[2025-03-06 06:02:26,758] INFO [LogLoader partition=__cluster_metadata-0, dir=/tmp/kafka-logs] Reloading from producer snapshot and rebuilding producer state from offset 3912 (kafka.log.UnifiedLog$)                                                       
[2025-03-06 06:02:26,758] INFO [ProducerStateManager partition=__cluster_metadata-0] Loading producer state from snapshot file 'SnapshotFile(offset=3912, file=/tmp/kafka-logs/__cluster_metadata-0/00000000000000003912.snapshot)' (org.apache.kafka.storage
.internals.log.ProducerStateManager)                                                                                                                                                                                                                         
[2025-03-06 06:02:26,760] INFO [LogLoader partition=__cluster_metadata-0, dir=/tmp/kafka-logs] Producer state recovery took 2ms for snapshot load and 0ms for segment recovery from offset 3912 (kafka.log.UnifiedLog$)                                      
[2025-03-06 06:02:26,772] INFO Initialized snapshots with IDs SortedSet() from /tmp/kafka-logs/__cluster_metadata-0 (kafka.raft.KafkaMetadataLog$)                                                                                                           
[2025-03-06 06:02:26,788] INFO [raft-expiration-reaper]: Starting (kafka.raft.TimingWheelExpirationService$ExpiredOperationReaper)                                                                                                                           
[2025-03-06 06:02:26,805] ERROR [SharedServer id=1] Got exception while starting SharedServer (kafka.server.SharedServer)                                                                                                                                    
java.lang.IllegalStateException: Configured voter set: [1, 2, 3, 8] is different from the voter set read from the state file: [1, 2, 3]. Check if the quorum configuration is up to date, or wipe out the local state file if necessary                      
        at org.apache.kafka.raft.QuorumState.initialize(QuorumState.java:132)                                                                                                                                                                                
        at org.apache.kafka.raft.KafkaRaftClient.initialize(KafkaRaftClient.java:375)                                                                                                                                                                        
        at kafka.raft.KafkaRaftManager.buildRaftClient(RaftManager.scala:248)                                                                                                                                                                                
        at kafka.raft.KafkaRaftManager.<init>(RaftManager.scala:174)                                                                                                                                                                                         
        at kafka.server.SharedServer.start(SharedServer.scala:266)                                                                                                                                                                                           
        at kafka.server.SharedServer.startForController(SharedServer.scala:138)                                                                                                                                                                              
        at kafka.server.ControllerServer.startup(ControllerServer.scala:206)                                                                                                                                                                                 
        at kafka.server.KafkaRaftServer.$anonfun$startup$1(KafkaRaftServer.scala:98)                                                                                                                                                                         
        at kafka.server.KafkaRaftServer.$anonfun$startup$1$adapted(KafkaRaftServer.scala:98)                                                                                                                                                                 
        at scala.Option.foreach(Option.scala:437)                                                                                                                                                                                                            
        at kafka.server.KafkaRaftServer.startup(KafkaRaftServer.scala:98)                                                                                                                                                                                    
        at kafka.Kafka$.main(Kafka.scala:112)                                                                                                                                                                                                                
        at kafka.Kafka.main(Kafka.scala)                                                                                                                                                                                                                     
[2025-03-06 06:02:26,807] ERROR Encountered fatal fault: caught exception (org.apache.kafka.server.fault.ProcessTerminatingFaultHandler)                                                                                                                     
java.lang.IllegalStateException: Configured voter set: [1, 2, 3, 8] is different from the voter set read from the state file: [1, 2, 3]. Check if the quorum configuration is up to date, or wipe out the local state file if necessary                      
        at org.apache.kafka.raft.QuorumState.initialize(QuorumState.java:132)                                                                                                                                                                                
        at org.apache.kafka.raft.KafkaRaftClient.initialize(KafkaRaftClient.java:375)                                                                                                                                                                        
        at kafka.raft.KafkaRaftManager.buildRaftClient(RaftManager.scala:248)                                                                                                                                                                                
        at kafka.raft.KafkaRaftManager.<init>(RaftManager.scala:174)                                                                                                                                                                                         
        at kafka.server.SharedServer.start(SharedServer.scala:266)                                                                                                                                                                                           
        at kafka.server.SharedServer.startForController(SharedServer.scala:138)                                                                                                                                                                              
        at kafka.server.ControllerServer.startup(ControllerServer.scala:206)                                                                                                                                                                                 
        at kafka.server.KafkaRaftServer.$anonfun$startup$1(KafkaRaftServer.scala:98)                                                                                                                                                                         
        at kafka.server.KafkaRaftServer.$anonfun$startup$1$adapted(KafkaRaftServer.scala:98)                                                                                                                                                                 
        at scala.Option.foreach(Option.scala:437)                                                                                                                                                                                                            
        at kafka.server.KafkaRaftServer.startup(KafkaRaftServer.scala:98)                                                                                                                                                                                    
        at kafka.Kafka$.main(Kafka.scala:112)                                                                                                                                                                                                                
        at kafka.Kafka.main(Kafka.scala)                                                                                             

发现,进程启动失败,容器退出。
日志中明确告诉了,controller-1 进程在启动时,读取本地持久目录中的数据 /tmp/kafka-logs/__cluster_metadata-0,发现原有数据记录的 voter id[1, 2, 3],但是新配置启动后与旧数据不符,所以启动失败。
解决这个报错的办法有两个:

    1. 确认 quorum 配置是否正确,或者回退老旧配置
    1. 清空本地目录的状态文件,再尝试启动

3.4 处理旧 controller 启动失败的问题

我们采用清理本地数据的方式,让 controller-1 根据 KAFKA_CONTROLLER_QUORUM_VOTERS 配置进行重建本地状态元数据文件。

>_ rm -rf ./volumes/controller-1/*
>_ docker-compose down controller-1
>_ docker-compose up controller-1 -d 
>_ docker logs -f controller-1
....
[2025-03-06 06:03:44,325] INFO [RaftManager id=1] Completed transition to Voted(epoch=191, votedId=2, voters=[1, 2, 3, 8], electionTimeoutMs=1837) from Unattached(epoch=191, voters=[1, 2, 3, 8], electionTimeoutMs=1564) (org.apache.kafka.raft.QuorumState)
[2025-03-06 06:03:44,325] INFO [RaftManager id=1] Vote request VoteRequestData(clusterId='Some(gQ3YBuwyQsiCIbN3Ilq1cQ)', topics=[TopicData(topicName='__cluster_metadata', partitions=[PartitionData(partitionIndex=0, candidateEpoch=191, candidateId=2, lastOffsetEpoch=2, lastOffset=4092)])]) with epoch 191 is granted (org.apache.kafka.raft.KafkaRaftClient)
[2025-03-06 06:03:44,413] INFO [RaftManager id=1] Completed transition to FollowerState(fetchTimeoutMs=2000, epoch=191, leaderId=2, voters=[1, 2, 3, 8], highWatermark=Optional[LogOffsetMetadata(offset=4087, metadata=Optional.empty)], fetchingSnapshot=Optional.empty) from Voted(epoch=191, votedId=2, voters=[1, 2, 3, 8], electionTimeoutMs=1837) (org.apache.kafka.raft.QuorumState)                                                                                                                             
[2025-03-06 06:03:45,992] INFO [RaftManager id=1] Vote request VoteRequestData(clusterId='Some(gQ3YBuwyQsiCIbN3Ilq1cQ)', topics=[TopicData(topicName='__cluster_metadata', partitions=[PartitionData(partitionIndex=0, candidateEpoch=191, candidateId=8, lastOffsetEpoch=1, lastOffset=3912)])]) with epoch 191 is rejected (org.apache.kafka.raft.KafkaRaftClient)
....

可以看到,此处 controller-1 已经正确识别 voters 节点的 ID了。

3.5 如法炮制,将剩下的 controller-2controller-3 节点进行配置更新

首先,更新 docker-compose.yaml 配置文件:

...
  controller-2:
    image: apache/kafka:3.7.0
    container_name: controller-2
    privileged: true
    user: root
    environment:
      KAFKA_NODE_ID: 2
      KAFKA_PROCESS_ROLES: controller
      KAFKA_LISTENERS: CONTROLLER://:9093
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@controller-1:9093,2@controller-2:9093,3@controller-3:9093,8@controller-4:9093
      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
      CLUSTER_ID: gQ3YBuwyQsiCIbN3Ilq1cQ
    volumes:
      - ./volumes/controller-2:/tmp/kafka-logs
    networks:
      kafka_horizontal_upscale_network:
        ipv4_address: 172.23.1.12

  controller-3:
    image: apache/kafka:3.7.0
    container_name: controller-3
    privileged: true
    user: root
    environment:
      KAFKA_NODE_ID: 3
      KAFKA_PROCESS_ROLES: controller
      KAFKA_LISTENERS: CONTROLLER://:9093
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@controller-1:9093,2@controller-2:9093,3@controller-3:9093
      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
      CLUSTER_ID: gQ3YBuwyQsiCIbN3Ilq1cQ
    volumes:
      - ./volumes/controller-3:/tmp/kafka-logs
    networks:
      kafka_horizontal_upscale_network:
        ipv4_address: 172.23.1.13
...

然后,停止容器,删除本地目录文件,启动容器:

>_ docker-compose down controller-2 controller-3
[+] Running 3/3
 ✔ Container controller-2                          Removed
 ✔ Container controller-3                          Removed 
 ! Network kafka_kafka_horizontal_upscale_network  Resource is still in use     

>_ rm -rf ./volumes/controller-{2..3}/*

>_ docker-compose up controller-2 controller-3 -d
[+] Running 2/2
 ✔ Container controller-2  Started
 ✔ Container controller-3  Started

3.6 再次检查集群节点的角色状态

>_ docker exec -it broker-1 bash
e058011f15d8:/# cd /opt/kafka
e058011f15d8:/opt/kafka# ./bin/kafka-metadata-quorum.sh --bootstrap-controller 172.23.1.11:9093 describe --replication
NodeId  LogEndOffset    Lag     LastFetchTimestamp      LastCaughtUpTimestamp   Status
1       7479            0       1741242734997           1741242734997           Leader
2       7479            0       1741242734648           1741242734648           Follower
3       7479            0       1741242734646           1741242734646           Follower
8       7479            0       1741242734646           1741242734646           Follower
4       7479            0       1741242734643           1741242734643           Observer
5       7479            0       1741242734643           1741242734643           Observer
6       7479            0       1741242734643           1741242734643           Observer
7       7479            0       1741242734638           1741242734638           Observer

这里可以看到,当集群中过半数的 controller 节点已经读取到新配置后,新增的 controller-8 节点就会变为 controller 角色,角色状态变为了 Follower 追随者。

最后,我们将旧集群中的 broker 节点配置进行更新,统一新增 KAFKA_CONTROLLER_QUORUM_VOTERS: 1@controller-1:9093,2@controller-2:9093,3@controller-3:9093,8@controller-4:9093 环境变量。
因为 broker 节点中的数据目录保存有 topic 的数据,所以我们就不能将 broker 数据目录进行全部清空。

其实,我们只需要删除一个文件即可:

>_ docker-compose down broker-{1..3}
[+] Running 4/4
 ✔ Container broker-2                              Removed       
 ✔ Container broker-1                              Removed    
 ✔ Container broker-3                              Removed   
 ! Network kafka_kafka_horizontal_upscale_network  Resource is still in use    

## 只需要删除 __cluster_metadata_0/quorum-state 文件即可
>_ rm ./volumes/broker-{1..3}/__cluster_metadata-0/quorum-state
rm: remove regular file './volumes/broker-1/__cluster_metadata-0/quorum-state'? y
rm: remove regular file './volumes/broker-2/__cluster_metadata-0/quorum-state'? y
rm: remove regular file './volumes/broker-3/__cluster_metadata-0/quorum-state'? y

>_ docker-compose up broker-{1..3} -d
[+] Running 6/6
 ✔ Container controller-2  Running              
 ✔ Container controller-3  Running   
 ✔ Container controller-1  Running  
 ✔ Container broker-1      Started          
 ✔ Container broker-2      Started    
 ✔ Container broker-3      Started  

>_ docker logs -f broker-1
[2025-03-06 06:52:28,226] INFO [SocketServer listenerType=BROKER, nodeId=4] Enabling request processing. (kafka.network.SocketServer)                                                                                                                       
[2025-03-06 06:52:28,231] INFO Awaiting socket connections on 0.0.0.0:19092. (kafka.network.DataPlaneAcceptor)
[2025-03-06 06:52:28,240] INFO Awaiting socket connections on 0.0.0.0:9092. (kafka.network.DataPlaneAcceptor)
[2025-03-06 06:52:28,250] INFO [BrokerServer id=4] Waiting for all of the authorizer futures to be completed (kafka.server.BrokerServer)                                                                                                                    
[2025-03-06 06:52:28,250] INFO [BrokerServer id=4] Finished waiting for all of the authorizer futures to be completed (kafka.server.BrokerServer)                                                                                                           
[2025-03-06 06:52:28,251] INFO [BrokerServer id=4] Waiting for all of the SocketServer Acceptors to be started (kafka.server.BrokerServer)                                                                                                                  
[2025-03-06 06:52:28,251] INFO [BrokerServer id=4] Finished waiting for all of the SocketServer Acceptors to be started (kafka.server.BrokerServer)                                                                                                         
[2025-03-06 06:52:28,251] INFO [BrokerServer id=4] Transition from STARTING to STARTED (kafka.server.BrokerServer)
[2025-03-06 06:52:28,252] INFO Kafka version: 3.7.0 (org.apache.kafka.common.utils.AppInfoParser)
[2025-03-06 06:52:28,252] INFO Kafka commitId: 2ae524ed625438c5 (org.apache.kafka.common.utils.AppInfoParser)
[2025-03-06 06:52:28,252] INFO Kafka startTimeMs: 1741243948251 (org.apache.kafka.common.utils.AppInfoParser)
[2025-03-06 06:52:28,256] INFO [KafkaRaftServer nodeId=4] Kafka Server started (kafka.server.KafkaRaftServer)
....

OK, Broker-1 重新上线。

集群拓扑图也发生了变化:

名称 ip role
controller-1 172.23.1.11 controller
controller-2 172.23.1.12 controller
controller-3 172.23.1.13 controller
broker-1 172.23.1.14 broker
broker-2 172.23.1.15 broker
broker-3 172.23.1.16 broker
kafka-ui 172.23.1.20 web-ui
broker-4 172.23.1.17 新增 broker
controller-4 172.23.1.18 新增 controller

4. 小结

至此,kafka kraft 集群的 brokercontroller 节点都已经扩容完毕。

注意:在实际生产环境中,为了控制器节点的可选举性,需要保证 controller 节点的个数为奇数个。

当然,实际生产环境中,更多的是扩容 broker 节点。

posted @ 2025-03-05 16:34  Professor哥  阅读(536)  评论(0)    收藏  举报