大家好,我叫谢伟,是一名程序员。
今天的主题:kafka 使用指南,单节点版本。
如果你是一名后端工程师,设计的应用正常的线上运行,某次秒杀活动,突然间把系统搞崩了,排查系统发现很多的流量没有处理,导致系统挂了,这个时候有两种思路: 1. nginx 反向代理,把更多的请求转发给内部网络的服务器上进行处理,达到一个负载均衡的目的 2. 使用消息系统,将更多的请求使用中间件“缓存”起来,再从这个系统中不断的取到缓存的请求,进行进一步的处理。
后者使用到的消息系统,就是kafka 的一个使用场景。
那么什么是 kafka?
kafka 是一个分布式消息系统,目前已定位为分布式流式处理平台。
简单的说一个系统A 将消息发给消息系统,一个系统B 再从消息系统中取到消息,进行后续的处理。
常见的用来描述 kafka 应用场景的一个词是:削峰填谷,削减波峰流量,填充波谷流量,使系统尽量的平滑。
由此得处:kafka 的三个典型应用场景
消息系统是目前最广泛的应用;消息传输需要存储起来,供后续系统拉取,故也可以当作存储系统;拉取消息之后,其实也是供后续系统处理,那么为什么不把数据处理也包含再kafka 系统中?分布式流式处理平台,大概就是这个意思。
下文陈述最核心的应用:消息系统
一条消息由系统A 产生,发往消息系统,系统B 从消息系统中拉取,这其中涉及到很多的概念。
针对生产者、消费者有不同的设置参数,决定了生产者、消费者的不同行为。
生产者要发送消息,首先要知道发往何处,即要知道 broker 的地址,知道 broker 的地址,broker(kafka server) 的设置约束了持久化存储的地址及其他行为,除此之外,如何区分发的消息的类型不同呢?kafka 系统给这个区分消息的概念取了个逻辑概念:Topic , 即生产者指定的 Topic 不同,存储的地址就不同。
针对 Topic,简单的场景是,不断的往里面发内容,持久化存储就不断以追加的模式存储,简单场景没什么问题,问题是消息数据过多的话,不利于系统消费,很简单的想法,分不同的“文件”追加存储,把整体规模缩小,这个概念在 kafka 中称之为分区:partition. 消息可以不断的以追加的模式不断的发往分区内,分区有编号,起始位 0 ,消息追加模式存储在分区内,会给一个编号 offset
消费者从 broker 系统中拉取消息,首先要知道broker 地址,其次需要知道 Topic,更细化的还可以设置哪个分区,哪个偏移量 offset 开始,消费消息。
那消息万一丢了咋整?一个简单的做法就是冗余备份:Replication,多份备份,其中有一个是 Leader , 其他的是 follower, leader 的作用是和消息对接,follower 不直接和消息对接,只负责和 leader 对接,不断的同步数据。
多个 broker 构成 kafka 集群,万一一个挂了 kafka 系统依靠 zookeeper 进行重新选举产生新leader。
kafka cluster:
image
kafka topic: 分区概念
image
kafka 集群:
image
基于上述概念:那么如何构建一个Kafka 服务,完成消息系统呢?
伪代码:
type Broker struct{
Addr
Config
...
}
伪代码:
type Producer struct{
Config
Message
...
}
伪代码
type Consumer strcut{
Config
Topic
Partitions
Offset
...
}
基本的思路:
结合官网的示例:如何完成最基本的消息收发。
下载安装包:kafka_2.12-2.3.0.tgz
解压之后,最重要的有两目录:
zookeeper-server-start.sh
zookeeper-server-stop.sh
kafka-configs.sh
kafka-console-consumer.sh
kafka-console-producer.sh
kafka-consumer-groups.sh
kafka-topics.sh
kafka-server-start.sh
kafka-server-stop.sh
...
zookeeper.properties
server.properties
producer.properties
consumer.properties
...
大概200多个参数吧,不好意思,我记不住。那怎么办?不学了吗,那挣不了钱,涨不了工资啊。
基本默认设置,部分按分类设置:
kafka 依赖于 zookeeper 分布式协调
dataDir=/tmp/zookeeper
clientPort=2181
记住这个默认的 clientPort=2181
kafka server 服务
log.dirs=/tmp/kafka-logs //日志存储目录
log.retention.hours=168 // 日志存储时长
broker.id=0 // 默认 broker id,集群方式的 kafka 设置,给每个 broker 编号
listeners=PLAINTEXT://:9092 // 对外提供的服务入口地址
zookeeper.connect=localhost:2181 // ZooKeeper集群地址
...
约定消息等的内容
约定消费消息等的内容
配置好配置参数后:
> bin/zookeeper-server-start.sh config/zookeeper.properties
> bin/kafka-server-start.sh config/server.properties
创建topic, 查询 topic 等可以使用:kafka-topics.sh
生产者生产消息可以使用:kafka-console-producer.sh
消费者消费消息可以使用:kafka-console-consumer.sh
当然,这些操作,一般只供测试使用,实际的使用是使用对应变成语言的客户端。
kafka go版本客户端:
下载安装:
go get -u -v github.com/Shopify/sarama
系统 A
type KafkaAction struct {
DataSyncProducer sarama.SyncProducer
DataAsyncProducer sarama.AsyncProducer
}
// 同步方式
func newDataSyncProducer(brokerList []string) sarama.SyncProducer {
config := sarama.NewConfig()
config.Producer.RequiredAcks = sarama.WaitForAll // Wait for all in-sync replicas to ack the message
config.Producer.Retry.Max = 5 // Retry up to 10 times to produce the message
config.Producer.Return.Successes = true
config.Producer.Partitioner = sarama.NewRoundRobinPartitioner
producer, err := sarama.NewSyncProducer(brokerList, config)
if err != nil {
log.Fatalln("Failed to start Sarama producer1:", err)
}
return producer
}
// 异步方式
func newDataAsyncProducer(brokerList []string) sarama.AsyncProducer {
config := sarama.NewConfig()
sarama.Logger = log.New(os.Stdout, "[KAFKA] ", log.LstdFlags)
config.Producer.RequiredAcks = sarama.WaitForLocal // Only wait for the leader to ack
config.Producer.Compression = sarama.CompressionSnappy // Compress messages
config.Producer.Flush.Frequency = 500 * time.Millisecond // Flush batches every 500ms
config.Producer.Partitioner = sarama.NewRoundRobinPartitioner
producer, err := sarama.NewAsyncProducer(brokerList, config)
if err != nil {
log.Fatalln("Failed to start Sarama producer2:", err)
}
go func() {
for err := range producer.Errors() {
log.Println("Failed to write access log entry:", err)
}
}()
return producer
}
还记得生产者有一系列配置参数吗?config 就这这个作用,有默认值,可以自己设置对应的值。
比如:压缩算法
config.Producer.Compression = sarama.CompressionSnappy
常用的压缩算法有:
不同的压缩算法主要在压缩比和吞吐量不同。
比如分区规则
config.Producer.Partitioner = sarama.NewRoundRobinPartitioner
常用的分区规则:
比如:发送消息是否返回成功与否
onfig.Producer.RequiredAcks = sarama.WaitForLocal
接口
type Encoder interface {
Encode() ([]byte, error)
Length() int
}
发送的消息需要实现Encoder 接口,即定义的消息结构体需要实现 Encode 和 Length 方法。
type SendMessage struct {
Method string `json:"method"`
URL string `json:"url"`
Value string `json:"value"`
Date string `json:"date"`
encoded []byte
err error
}
func (S *SendMessage) Length() int {
b, e := json.Marshal(S)
S.encoded = b
S.err = e
return len(string(b))
}
func (S *SendMessage) Encode() ([]byte, error) {
return S.encoded, S.err
}
func (K *KafkaAction) Do(v interface{}) {
message := v.(SendMessage)
// 发送的消息返回分区和偏移量
partition, offset, err := K.DataSyncProducer.SendMessage(&sarama.ProducerMessage{
Topic: TOPIC,
Value: &message,
})
if err != nil {
log.Println(err)
return
}
value := map[string]string{
"method": message.Method,
"url": message.URL,
"value": message.Value,
"date": message.Date,
}
fmt.Println(fmt.Sprintf("/%d/%d/%+v", partition, offset, value))
}
比如我们按照上面的配置发送消息:topic: topic-golang partition/offset/value
/0/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/0/1/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/0/2/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/0/3/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
上文只有一个 partition , offset值不断增加。
创建另外一个 topic, 分10个区。topic: topic-python
在日志中显示成咋样的呢?
// cd log.dirs ; server.properties 中的设置
topic-golang-0
topic-python-0
topic-python-1
topic-python-2
topic-python-3
topic-python-4
topic-python-5
topic-python-6
topic-python-7
topic-python-8
topic-python-9
往 topic-python 中发送日志,分区规则轮询:
/0/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/1/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/2/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/3/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/4/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/5/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/6/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/7/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/8/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/9/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/0/1/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/1/1/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
轮询,不断的往分区内存消息。
系统 B
func main() {
config := sarama.NewConfig()
config.Consumer.Return.Errors = true
brokers := []string{"127.0.0.1:9092"}
master, err := sarama.NewConsumer(brokers, config)
if err != nil {
panic(err)
}
defer func() {
if err := master.Close(); err != nil {
panic(err)
}
}()
_, e := master.Partitions("topic-python")
if e != nil {
log.Println(e)
}
consumer, err := master.ConsumePartition("topic-python", 0, sarama.OffsetOldest)
if err != nil {
panic(err)
}
signals := make(chan os.Signal, 1)
signal.Notify(signals, os.Interrupt)
doneCh := make(chan struct{})
go func() {
for {
select {
case err := <-consumer.Errors():
fmt.Println(err)
case msg := <-consumer.Messages():
fmt.Println("Received messages", string(msg.Key), string(msg.Value), msg.Topic)
case <-signals:
fmt.Println("Interrupt is detected")
doneCh <- struct{}{}
}
}
}()
<-doneCh
}
还记得生产者向 topic-python 内发送的消息吗? partition/offset/value
/0/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/1/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/2/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/3/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/4/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/5/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/6/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/7/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/8/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/9/0/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/0/1/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
/1/1/map[date:12344 method:get5 url:www.baidu.com4 value:da4]
可以看出:partition: 0 中有两条消息。那么消费者指定了分区,只能消费这两条消息。
Received messages {"method":"get5","url":"www.baidu.com4","value":"da4","date":"12344"} topic-python
Received messages {"method":"get5","url":"www.baidu.com4","value":"da4","date":"12344"} topic-python
使用 kafka 客户端 ,那么我们还需要哪些功能?
type ClusterAdmin interface {
CreateTopic(topic string, detail *TopicDetail, validateOnly bool) error
ListTopics() (map[string]TopicDetail, error)
DescribeTopics(topics []string) (metadata []*TopicMetadata, err error)
DeleteTopic(topic string) error
CreatePartitions(topic string, count int32, assignment [][]int32, validateOnly bool) error
DeleteRecords(topic string, partitionOffsets map[int32]int64) error
DescribeConfig(resource ConfigResource) ([]ConfigEntry, error)
AlterConfig(resourceType ConfigResourceType, name string, entries map[string]*string, validateOnly bool) error
CreateACL(resource Resource, acl Acl) error
ListAcls(filter AclFilter) ([]ResourceAcls, error)
DeleteACL(filter AclFilter, validateOnly bool) ([]MatchingAcl, error)
ListConsumerGroups() (map[string]string, error)
DescribeConsumerGroups(groups []string) ([]*GroupDescription, error)
ListConsumerGroupOffsets(group string, topicPartitions map[string][]int32) (*OffsetFetchResponse, error)
DeleteConsumerGroup(group string) error
DescribeCluster() (brokers []*Broker, controllerID int32, err error)
Close() error
}
关于单节点 kafka 的基本应用就这些。
任何提供服务的系统,都可以使用容器版本,kafka 也可以使用容器版本。配置可以使用环境变量的形式设置。
docker-compose.yml
version: '2'
services:
ui:
image: index.docker.io/sheepkiller/kafka-manager:latest
depends_on:
- zookeeper
ports:
- 9000:9000
environment:
ZK_HOSTS: zookeeper:2181
zookeeper:
image: index.docker.io/wurstmeister/zookeeper:latest
ports:
- 2181:2181
server:
image: index.docker.io/wurstmeister/kafka:latest
depends_on:
- zookeeper
ports:
- 9092:9092
environment:
KAFKA_OFFSETS_TOPIC_REPLIATION_FACTOR: 1
KAFKA_ADVERTISED_HOST_NAME: 127.0.0.1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
后续集群版本。
<完>