大家好,我叫謝偉,是一名程序員。
今天的主題:kafka 上手指南--集羣版
前提回顧:kafka 單節點
1. 基本概念
在消息系統中,涉及的概念都比較類似,初學消息系統,概念有時候理解不到位,需要讀者反覆的根據自己的學習進度回過頭把基本概念捋清楚。
下面採用問答式陳述基本概念:
- 什麼是 broker ?
簡單的說,一個 kafka server 就是一個 broker。
- 什麼是 生產者 producer ?
簡單的說,提供消息的系統稱爲生產者
- 什麼是 消費者 consumer ?
簡單的說,對消息進行處理的系統稱爲消費者
- 什麼是 topic ?
簡單的說,區分消息的不同類型,人爲的起個名字,所以 topic 是個邏輯概念。
- 什麼是分區 partition ?
簡單的說,是存儲消息的實體,即將 topic 劃分爲不同的分區。物理層面看就是以 topic-N 命名的文件夾,文件夾下存儲消息日誌。當然分區可以在同一個 broker 上,也可以在不同 broker 上,如果你用上了集羣版的 kafka。
topic-go-0
topic-go-1
topic-go-2
...
- 什麼是 offset ?
簡單的說,是一個表示位移的數字。用來給消費者做標記的。比如給你發了100 條消息,我怎麼知道你消費到了第幾個呢,offset 就是用來標記的。
- 什麼是 消費者組 ?
簡單的說,是一組消費者共同消費一個或者多個topic, 當然某個消費者消費的是一個或者多個分區內的消息。爲什麼有消費者,又要消費者組?消費者消費消息,需要訂閱某個 topic, 消費者組共同消費一個或者多個 topic,這樣可以的效果是:可拓展、容錯。可拓展指,新加入一個消費者,可以承擔部分任務,減輕其他消費者負擔;同理,減少一個消費者,再重新給消費者分配消息。這種分配機制,在 kafka 系統中稱之爲:Rebalance,動態的調整。
那麼什麼時候會 Rebalance ?
- 消費者數目的變化
- topic 的變化
- 分區的變化
其中消費者數目的變化,是最常見的場景。Rebalance 有利有弊,利:可拓展,容錯;弊:Rebalance 比較耗性能,某一個時刻會停止消費消息。
- 什麼是 kafka 集羣?
簡單的說,集羣是一羣服務的集合,一個典型的特徵是:多機器,多服務。這種特徵能夠保障系統的高可用,高併發。系統內部之間可以通過 zookeeper 、 Metadata 等發現彼此;對外,就像使用單服務一樣。
- “能力”的大小怎麼控制 ?
配置文件,比如我怎麼保障生產者準確的發送消息呢,比如多個分區,我按什麼分區策略呢,比如生產者的消息要不要壓縮,採用什麼壓縮方式;比如消費者是從最新的消費,還是最老的消息消費;比如消費者組的 Rebalance 策略是什麼?
這些特性,我把它稱之爲能力的大小,這些能力的大小,需要使用者足夠熟悉才能發揮其能力,或者說能具體問題具體分析。
- broker “能力”的配置
- 生產者“能力”的配置
- 消費者“能力”的配置
- 消費者組“能力”的配置
2. 配置
啓動服務時的配置文件,這也是絕大多少服務啓動的一般方式,比如 MySQL 數據庫服務,比如 Redis 服務等,都是啓動時進行配置文件,賦予其能力。
broker
# 目錄
config/server.properties
- log.dirs 消息存儲目錄,可以多個
log.dirs=/kafka/kafka-logs-kfk1
- zookeeper.connect ,可以多個,用於集羣方式
zookeeper.connect=zookeeper-1:2181
- advertised.listeners 對外地址
advertised.listeners=PLAINTEXT://kfk1:9092
- listener.security.protocol.map 安全協議
listener.security.protocol.map=CONTROLLER:PLAINTEXT
一般這些配置就可以,其他默認,其中 log.dirs , zookeeper.connect 最爲重要
topic
- auto.create.topics.enable 是否允許自動創建 topic
auto.create.topics.enable=false
啓動服務之後,一般通過客戶端工具,編寫代碼完成相應的設置。
就 go 中,kafka 客戶端使用:sarama
type config struct {
Producer struct {
...
}
Consumer struct {
...
Group struct {
...
}
}
}
- 配置針對消費者,配置config.Consumer
- 配置針對生產者,配置config.Producer
- 配置針對消費者組,配置config.Consumer.Group
消費者:
c.Consumer.Fetch.Min = 1
c.Consumer.Fetch.Default = 1024 * 1024
c.Consumer.Retry.Backoff = 2 * time.Second
c.Consumer.MaxWaitTime = 250 * time.Millisecond
c.Consumer.MaxProcessingTime = 100 * time.Millisecond
c.Consumer.Return.Errors = false
c.Consumer.Offsets.CommitInterval = 1 * time.Second
c.Consumer.Offsets.Initial = OffsetNewest
c.Consumer.Offsets.Retry.Max = 3
其中,一般默認,否則配置:
- 是否返回錯誤:c.Consumer.Return.Errors
- 消費起始值:c.Consumer.Offsets.Initial
- 重試機制:Retry
生產者:
// 消息的最大值大概 1MB
c.Producer.MaxMessageBytes = 1000000
// 消息是否應答:0: 不應答,禁用;1: leader 收到即可 ; -1: 所有的副本都收到
c.Producer.RequiredAcks = WaitForLocal
c.Producer.Timeout = 10 * time.Second
// 分區策略:隨機、輪詢、hash 等
c.Producer.Partitioner = NewHashPartitioner
// 重試機制
c.Producer.Retry.Max = 3
c.Producer.Retry.Backoff = 100 * time.Millisecond
c.Producer.Return.Errors = true
// 壓縮算法:gzip, zstd, lz4, snappy
c.Producer.CompressionLevel = CompressionLevelDefault
消費者組:
// 間隔
c.Consumer.Group.Session.Timeout = 10 * time.Second
// 心跳
c.Consumer.Group.Heartbeat.Interval = 3 * time.Second
// Rebalance 策略
c.Consumer.Group.Rebalance.Strategy = BalanceStrategyRange
c.Consumer.Group.Rebalance.Timeout = 60 * time.Second
c.Consumer.Group.Rebalance.Retry.Max = 4
c.Consumer.Group.Rebalance.Retry.Backoff = 2 * time.Second
3. 消費者組
普通的消費者,一般需要指定 topic, offset 指定消費:
比如:
config := sarama.NewConfig()
config.Consumer.Return.Errors = true
brokers := []string{"127.0.0.1:9092"}
master, err := sarama.NewConsumer(brokers, config)
consumer, err := master.ConsumePartition("topic-python", 0, sarama.OffsetNewest)
其中:
ConsumePartition(topic string, partition int32, offset int64) (PartitionConsumer, error)
- topic
- partition
- offset
但一般這種形式,需要指定 offset 這種,不方便使用。所以一般使用消費者組的形式。
type KafkaConsumerGroupAction struct {
group sarama.ConsumerGroup
}
func NewKafkaConsumerGroupAction(brokers []string, groupId string) *KafkaConsumerGroupAction {
config := sarama.NewConfig()
sarama.Logger = log.New(os.Stdout, "[consumer_group]", log.Lshortfile)
// 重平衡策略
config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategySticky
config.Consumer.Group.Session.Timeout = 20 * time.Second
config.Consumer.Group.Heartbeat.Interval = 6 * time.Second
config.Consumer.IsolationLevel = sarama.ReadCommitted
config.Consumer.Offsets.Initial = sarama.OffsetNewest
config.Version = sarama.V2_3_0_0
consumerGroup, e := sarama.NewConsumerGroup(brokers, groupId, config)
if e != nil {
log.Println(e)
return nil
}
return &KafkaConsumerGroupAction{group: consumerGroup}
}
func (K *KafkaConsumerGroupAction) Consume(topics []string, wg sync.WaitGroup, ctx context.Context) {
var consumer = KafkaConsumerGroupHandler{ready: make(chan bool)}
go func() {
defer wg.Done()
for {
if err := K.group.Consume(ctx, topics, &consumer); err != nil {
log.Panicf("Error from consumer: %v", err)
}
if ctx.Err() != nil {
return
}
consumer.ready = make(chan bool)
}
}()
<-consumer.ready
log.Println("Sarama consumer up and running!...")
sigterm := make(chan os.Signal, 1)
signal.Notify(sigterm, syscall.SIGINT, syscall.SIGTERM)
select {
case <-ctx.Done():
log.Println("terminating: context cancelled")
case <-sigterm:
log.Println("terminating: via signal")
}
wg.Wait()
if err := K.group.Close(); err != nil {
log.Panicf("Error closing client: %v", err)
}
}
type KafkaConsumerGroupHandler struct {
ready chan bool
}
func (K *KafkaConsumerGroupHandler) Setup(sarama.ConsumerGroupSession) error {
return nil
}
func (K *KafkaConsumerGroupHandler) Cleanup(sarama.ConsumerGroupSession) error {
return nil
}
func (K *KafkaConsumerGroupHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
for message := range claim.Messages() {
log.Printf("Message claimed: value = %s, timestamp = %v, topic = %s, partions = %d, offset = %d", string(message.Value), message.Timestamp, message.Topic, message.Partition, message.Offset)
lag := claim.HighWaterMarkOffset() - message.Offset
fmt.Println(lag)
session.MarkMessage(message, "")
}
return nil
}
消費者組:
type ConsumerGroup interface {
Consume(ctx context.Context, topics []string, handler ConsumerGroupHandler) error
Errors() <-chan error
Close() error
}
其中:
type ConsumerGroupHandler interface {
Setup(ConsumerGroupSession) error
Cleanup(ConsumerGroupSession) error
ConsumeClaim(ConsumerGroupSession, ConsumerGroupClaim) error
}
真實的消息處理,需要實現 ConsumerGroupHandler 接口。
4. 生產者的一般處理流程
如果這些概念你都清楚,那麼整體來說,使用 kafka 的難點在哪呢?
- 如何確保消息準確無誤地發送
- 如何確保不重複消費消息
- 如何確保消息不滯後,最好是生產者發往消息系統,消費者立馬消費掉,沒有延長
- 如何確保系統高可用
- 生產者配置
- 實例化生產者
- 構建消息
- 發送消息
- 關閉生產者實例
func NewAsyncProducer(addrs []string, conf *Config) (AsyncProducer, error) {
client, err := NewClient(addrs, conf)
if err != nil {
return nil, err
}
return newAsyncProducer(client)
}
//異步生產者
type AsyncProducer interface {
AsyncClose()
Close() error
Input() chan<- *ProducerMessage // 發送消息
Successes() <-chan *ProducerMessage
Errors() <-chan *ProducerError
}
5. 消費者的一般處理流程
消費者的一般處理流程:
- 消費者配置
- 實例化消費者
- 訂閱主題
- 提交位移
- 關閉消費者
func NewConsumer(addrs []string, config *Config) (Consumer, error) {
client, err := NewClient(addrs, config)
if err != nil {
return nil, err
}
return newConsumer(client)
}
type Consumer interface {
Topics() ([]string, error) // 消息
Partitions(topic string) ([]int32, error) // 分區
ConsumePartition(topic string, partition int32, offset int64) (PartitionConsumer, error) // 消費消息
HighWaterMarks() map[string]map[int32]int64 // 高水位
Close() error
}
6. 消費者組的一般處理流程
普通的消費者,需要指定分區和位移,進行消費,不常用。一般選擇消費者組。
那麼消費者組一般的處理流程是?
- 配置消費者組
- 實例話消費者組,指定 topic, 指定消費者組 GroupID
- 消費消息
- 關閉消費者組
type ConsumerGroup interface {
Consume(ctx context.Context, topics []string, handler ConsumerGroupHandler) error
Errors() <-chan error
Close() error
}
消費者組處理器:
type ConsumerGroupHandler interface {
Setup(ConsumerGroupSession) error
Cleanup(ConsumerGroupSession) error
ConsumeClaim(ConsumerGroupSession, ConsumerGroupClaim) error
}
7. 集羣
上文說到,集羣一個特徵是:多機器,多服務。
真實的線上環境,zookeeper 部署在不同機器,kafka server 部署在不同機器,組成的系統,共同服務於線上系統。
個人學習,爲了達到集羣的效果,即:使用不同的端口區分即可。
當然你可以本地配置 zookeeper, kafka。但我一般喜歡用容器的方式,部署起來方便。
- 多節點 zookeeper
zookeeper-1:
image: zookeeper
restart: always
hostname: zookeeper-1
ports:
- 2181:2181
environment:
ZOO_MY_ID: 1
ZOO_SERVERS: server.1=0.0.0.0:2888:3888;2181 server.2=zookeeper-2:2888:3888;2181 server.3=zookeeper-3:2888:3888;2181
volumes:
- /local/volumn/zookeeper1/data:/data
- /local/volumn/zookeeper1/datalog:/datalog
zookeeper-2:
image: zookeeper
restart: always
hostname: zookeeper-2
ports:
- 2182:2181
environment:
ZOO_MY_ID: 2
ZOO_SERVERS: server.1=zookeeper-1:2888:3888;2181 server.2=0.0.0.0:2888:3888;2181 server.3=zookeeper-3:2888:3888;2181
volumes:
- /local/volumn/zookeeper2/data:/data
- /local/volumn/zookeeper2/datalog:/datalog
zookeeper-3:
image: zookeeper
restart: always
hostname: zookeeper-3
ports:
- 2183:2181
environment:
ZOO_MY_ID: 3
ZOO_SERVERS: server.1=zookeeper-1:2888:3888;2181 server.2=zookeeper-2:2888:3888;2181 server.3=0.0.0.0:2888:3888;2181
volumes:
- /local/volumn/zookeeper3/data:/data
- /local/volumn/zookeeper3/datalog:/datalog
其中最重要的是環境變量:
ZOO_MY_ID 一般用一個數字表示 myid
ZOO_SERVERS
抽象出一個公式:server.A=B:C:D
- A 表示 myid,表示服務器的編號
- B 表示代表服務器的 ip 地址
- C 表示服務器與集羣中的 leader 服務器交換信息的端口
- D 表示選舉時服務器相互通信的端口
有人會說,我不知道這些環境變量怎麼辦,我也不知道具體的環境變量名呢?
看 Docker hub 上的具體文檔啊:
zookeeper docker hub 文檔:https://hub.docker.com/_/zookeeper
- 多節點 kafka:(kafka docker hub 地址:https://hub.docker.com/r/wurstmeister/kafka)
kfk1:
image: index.docker.io/wurstmeister/kafka:latest
container_name: kfk1
hostname: kfk1
restart: always
ports:
- 9092:9092
- 19999:9999
expose:
- 19092
links:
- zookeeper-1
- zookeeper-2
- zookeeper-3
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kfk1:9092
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
JMX_PORT: 9999
volumes:
- /local/volumn/kfk1:/kafka/kafka-logs-kfk1
kfk2:
image: index.docker.io/wurstmeister/kafka:latest
container_name: kfk2
hostname: kfk2
restart: always
ports:
- 29092:29092
- 29999:9999
expose:
- 29092
links:
- zookeeper-1
- zookeeper-2
- zookeeper-3
environment:
KAFKA_BROKER_ID: 2
KAFKA_ZOOKEEPER_CONNECT: zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kfk2:29092
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:29092
JMX_PORT: 9999
volumes:
- /local/volumn/kfk2:/kafka/kafka-logs-kfk2
kfk3:
image: index.docker.io/wurstmeister/kafka:latest
container_name: kfk3
hostname: kfk3
restart: always
ports:
- 39092:39092
- 39999:9999
expose:
- 39092
links:
- zookeeper-1
- zookeeper-2
- zookeeper-3
environment:
KAFKA_BROKER_ID: 3
KAFKA_ZOOKEEPER_CONNECT: zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kfk3:39092
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:39092
JMX_PORT: 9999
volumes:
- /local/volumn/kfk3:/kafka/kafka-logs-kfk3
其中最重要的是以下幾個環境變量:
KAFKA_BROKER_ID broker.id 單節點時,默認值爲-1
KAFKA_ZOOKEEPER_CONNECT kafka zookeeper 連接地址,對應上文 zookeeper 對外地址
KAFKA_ADVERTISED_LISTENERS 該節點對外公佈的訪問地址和端口
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:39092 該節點對外訪問地址和端口
- 監控節點(kafka-manager docker hub 地址:https://hub.docker.com/r/sheepkiller/kafka-manager)
ui:
image: index.docker.io/sheepkiller/kafka-manager:latest
restart: always
links:
- zookeeper-1
- zookeeper-2
- zookeeper-3
- kfk1
- kfk2
- kfk3
ports:
- 9000:9000
environment:
ZK_HOSTS: zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181
KAFKA_BROKERS: kfk1:19092,kfk2:29092,kfk3:39092
其中環境變量:
ZK_HOSTS zookeeper 節點地址
KAFKA_BROKERS kafa 節點地址
啓動:
docker-compose -f docker-compose.yml up -d
集羣版本的kafka 服務,基本上和單節點的 kafka 服務使用方式一致,集羣版本的系統更穩健,高可用,比如冗餘備份,一旦一個節點失效了並不影響服務,除非全部節點失效。
- 備份:
創建 topic,備份的數目小於等於 kafka 節點數目。比如三個節點,備份2份,可能在 三個節點上任意兩個。
- 分區
單節點,topic 的分區,都在同一個文件夾下;集羣版本,分區的大致可以均勻的分佈在集羣節點上
對外服務,和單節點完全一致。
topic-go 10個分區,備份 2 份:三個節點分別存儲:6,7,7 個分區
集羣版本可能會出現的問題?
設置過 不自動創建 topic,記得先手動創建 topic
集羣訪問地址不通。1 設置 /etc/hosts ;2 開放端口,特別是雲服務器,記得開放端口
消費滯後 Lag, 怎麼辦?增加消費者實例
參考:
代碼地址: