什么是 Apache Kafka(以下简称 Kafka)
Kafka 是一个 分布式 事件流平台, 生产者(Producer) 可以 发布(publish) 事件到 Kafka, 消费者(Consumer) 可以通过 订阅(subscribe) 的方式来消费这些事件. Kafka 会将这些事件存储到磁盘上, 并且存放的时长是可以配置的. 所以消费者可以立即去获取事件来处理, 也可以等有空的时候再来处理。
事件也被叫做 记录(record) 或者 消息(message)
作用
- 解耦: 通过 pub/sub 的方式实现业务的解耦
- 削峰: 当大量 event 发送给 Kafka 时, 由于 Kafka 会先存储事件, 消费者可以根据消费能力能行消费
特性
核心概念
Broker
一个 broker 可以理解成一台 Kafka 服务器(server), 用于存放生产者发送过来的事件. 一般 Kafka 会以分布式的方式部署多个 broker, 多个 broker 通过 ZooKeeper 来统一协调, 它们一起组成了一个集群(Cluster).
Controller Broker
Kafka 集群 为了减少对 ZooKeeper 的依赖, 会选举集群内的一个 broker 作为 Controller Broker, Controller Broker 配合 ZooKeeper 一起管理和协调整个集群.
Topic
由于许多不同的生产者(比如: 数据库、传感器、其他系统应用等)实时地向 Kafka 发送大量不同类型的事件. 为了让消费者只处理自己关心的事件, Kafka 将同一类型的事件定义为一个 主题(topic). 生产者在发送事件的时候需要指定事件的主题. 消费者按照主题进行订阅.
Partition
为了提高 Kafka 的吞吐量, Kafka 将同一主题下的事件进行分区(partition)存储, Kafka 会将同一主题下的多个分区均匀的分布到集群内的各个 broker 上. 一个分区可以理解成 broker 里的一个文件夹, 一个分区内的事件会按发送顺序存储. 这样可以用多个消费者同时消费一个主题下的多个分区.
Replication
为了提高 Kafka 的可靠性, 可以为一个分区生成多个副本(replication), 副本数是可配的. 下图的 Kafka 集群内有 3 个 broker, 并且假设当前只存储了一个主题的事件, 并且这个主题下有 3 个分区. 从图中可以看出, 一个分区有 3 个副本, 并且同一分区的不同副本要分布在不同的 broker 上.
从上图可以看到 相同分区的多个副本中有一个被选举为 leader 副本, 其他的为 follower 副本. 因为一个分区的副本的同步不是绝对实时的, 会有一定的延迟(可以通过 replica.lag.time.max.ms 和 zookeeper.session.timeout.ms 来配置), 为了保证事件的一致性所以必须从这多个副本中选举一个副本作为 leader, 由它来处理事件的存储和消费, follower 副本则只会从 leader 副本里同步数据.
ISR(In-Sync Replicas)
副本同步队列: 一个分区的多个副本组成的队列, 如果副本在这个队列中, 那说明这个副本和 leader 是保持同步的, 如果某个副本长时间未和 leader 同步, Controller Broker 会将 这个副本移除 ISR, 把它放到 OSR(Outof-Sync Replicas) 中. 当 leader 出现问题, 需用重新选举 leader 时, Controller Broker 会优先从 ISR 中选举.
Segment
为了进一步加快事件的读写速度, Kafka 会将同一个分区的事件按照分段(segment)来存储, 一个分段包含数据(log)文件和索引(index)文件. 一个分段的大小可以通过 log.segment.bytes 来配置。
当一个 event 被转发到某个分区存储时, Kafka 会给这个 event 分配一个序号(offset)(序号是递增的), 然后将这个 event 添加到最后一个 segment 的末尾.
每个分段将本分段内的第一个 event 的序号作为分段的名称, 所以第一个分段包含 00000000000000000000.log 和 00000000000000000000.index. 其中 index 文件中存储了 offset 和 offset 对应 event 在 log 文件中的位置(position). 为了减少 index 文件的大小, index 文件中只会存储部分 event 的 offset 和 positon 的映射关系, 具体可以通过 index.interval.bytes 来配置.
Producer
生产者, 发送 event 的一方, 可以同时有多个生产者向 Kafka 发送 event. 一个生产者可以向一个主题同时发送多个 event.
可以通过配置 linger.ms 和 batch.size 来实现批量发送 event。它们分别是指 缓冲多久后 或者 缓冲大小到达多大时 一次性发送多个 event.
生产者在发送 event 时, 可以指定 event 的确认(acks)策略:
- acks 为 0: 生产者直接将 event 放到缓冲区, 不需要确认
- acks 为 1: 当 leader 收到 event 并存储后才认为这个 event 发送成功
- acks 为 all: 当 ISR 所有副本都收到 event 并存储后才认为 evnet 发送成功
Consumer
消费者, 消费 event 的一方, 通过 pull 的方式来获取 event. 在 Kafka 中可以将多个消费者组成一个消费者组(Consumer Group). 一个主题下的一个分区不能被一个组下的多个消费者消费, 通常会将一个组下的消费者分别连接到一个主题下的一个或多个分区上.
消费者在消费 event 后需要提交 event 的 offset, 这样能记录消费的进度, 方便下次能接着上次消费的地方继续。 也可以重置 offset 到某个值来实现重新消费. offset 的提交可以分为自动提交和手动提交, 可以通过 enable_auto_commit 和 auto_commit_interval_ms 来控制.
Enhancer 应用如何接入 Kafka
- 安装 Kafka kafka_2.13-2.8.0.tgz
tar -xzf kafka_2.13-2.8.0.tgz
cd kafka_2.13-2.8.0
bin/zookeeper-server-start.sh config/zookeeper.properties
bin/kafka-server-start.sh config/server.properties
-
添加依赖 在 Enhancer 工作台 -> 全局配置 -> 系统 -> 依赖 里添加
"kafka-node": "^5.0.0" -
创建生产者
3.1 在 Enhancer 工作台 -> 自定义模块里添加一个叫 producer 的模块, 然后写如下代码:
const kafka = require('kafka-node');
const client = new kafka.KafkaClient({
kafkaHost:'127.0.0.1:9092' //如果是集群: '127.0.0.1:9091,127.0.0.1:9092,127.0.0.1:9093'
});
const producer = new kafka.Producer(client);
producer.on('ready', () => {
const topicName = 'enhancer-test';
// 主题也可以通过 kafka 的命令提前创建好
producer.createTopics([{
topic: topicName,
// partitions: 3,
// replicationFactor: 2
}], (err, result) => {
if (err) {
console.log(err.message)
return
}
producer.emit('topic-created', topicName);
})
});
producer.on('error', (err) => {
console.error('producer error ', err.stack);
});
module.exports = producer;
3.2 在需要发送 event 的地方(一般是 执行->服务器过程 里面的 执行 -> SQL执行后后台脚本)像下面这样使用:
const producer = require('@custom/producer');
const event = JSON.stringify({cmd: 'x', value: 'y'});
producer.send([{
topic: 'enhancer-test',
messages: [event],
// partition: 0
}], (err, data) => {
if (err) {
console.log(err.message);
}
})
- 创建消费者 4.1 在 Enhancer 工作台 -> 自定义模块里添加一个叫 consumer 的模块, 然后写如下代码:
function init (Enhancer, topic) {
const kafka = require('kafka-node');
const ConsumerGroup = kafka.ConsumerGroup;
const options = {
kafkaHost: '127.0.0.1:9092', //如果是集群: '127.0.0.1:9091,127.0.0.1:9092,127.0.0.1:9093',
groupId: 'enhancer-test',
sessionTimeout: 15000,
protocol: ['roundrobin']
};
const consumer = new ConsumerGroup(options, topic);
consumer.on('message', (message) => {
console.log(`process ${process.pid} receive `, message);
});
consumer.on('error', (err) => {
console.error('consumer error ', err.message);
});
consumer.on('offsetOutOfRange', (topic) => {
console.log('offsetOutOfRange', topic);
})
}
module.exports = {
init
}
4.2 在 Enhancer 工作台 -> 全局配置 -> 系统 -> 启动函数 点击 启用, 并填入下面的代码:
const producer = require('@custom/producer');
const consumer = require('@custom/consumer');
producer.on('topic-created', (topic) => {
consumer.init(Enhancer, topic);
});
测试环境: 在点击 应用 后, 需要再点击 测试并模拟执行系统启动函数
- 如果需要用 Docker 配置集群, 可以参考下面的 docker-compose.yml
version: '3'
services:
zookeeper:
image: confluentinc/cp-zookeeper:latest
hostname: zookeeper
ports:
- "2181:2181"
environment:
ZOOKEEPER_SERVER_ID: 1
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_SERVERS: zookeeper:2888:3888
volumes:
- ./data/zookeeper/data:/data
- ./data/zookeeper/datalog:/datalog
kafka1:
image: confluentinc/cp-kafka:latest
hostname: kafka1
ports:
- "9091:9091"
environment:
KAFKA_ADVERTISED_LISTENERS: LISTENER_DOCKER_INTERNAL://kafka1:19091,LISTENER_DOCKER_EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9091
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: LISTENER_DOCKER_INTERNAL:PLAINTEXT,LISTENER_DOCKER_EXTERNAL:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: LISTENER_DOCKER_INTERNAL
KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181"
KAFKA_BROKER_ID: 1
volumes:
- ./data/kafka1/data:/var/lib/kafka/data
depends_on:
- zookeeper
kafka2:
image: confluentinc/cp-kafka:latest
hostname: kafka2
ports:
- "9092:9092"
environment:
KAFKA_ADVERTISED_LISTENERS: LISTENER_DOCKER_INTERNAL://kafka2:29091,LISTENER_DOCKER_EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: LISTENER_DOCKER_INTERNAL:PLAINTEXT,LISTENER_DOCKER_EXTERNAL:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: LISTENER_DOCKER_INTERNAL
KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181"
KAFKA_BROKER_ID: 2
volumes:
- ./data/kafka2/data:/var/lib/kafka/data
depends_on:
- zookeeper
kafka3:
image: confluentinc/cp-kafka:latest
hostname: kafka3
ports:
- "9093:9093"
environment:
KAFKA_ADVERTISED_LISTENERS: LISTENER_DOCKER_INTERNAL://kafka3:39091,LISTENER_DOCKER_EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9093
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: LISTENER_DOCKER_INTERNAL:PLAINTEXT,LISTENER_DOCKER_EXTERNAL:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: LISTENER_DOCKER_INTERNAL
KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181"
KAFKA_BROKER_ID: 3
volumes:
- ./data/kafka3/data:/var/lib/kafka/data
depends_on:
- zookeeper