前言
kafka官网:http://kafka.apachecn.org/
消息队列
消息队列在如今的软件架构中,地位非比寻常。优点如下:
1)、解耦。2)、冗余。3)、扩展性。4)、灵活性and峰值处理能力。5)、可恢复性。6)、顺序保证。(ps:kafka保证一个partition内的数据是有序的)7)、缓冲。8)、异步通信。
kafka
Kafka是一个分布式的、可分区的、可复制的消息系统。
其基本术语:
- 1)无论是kafka集群还是consumer,都依赖zookeeper集群来保存一些meta信息。
- Producer:消息生产者,向kafka broker发消息的客户端。
- consumer:消息消费者,向kafka broker取消息的客户端。
- Topic:可以理解是一个队列,一个topic里有很多个partition。
- consumer group:这是kafka用来实现topic广播的和单播的手段。topic的消息会复制(不是真正的复制)到所有的CG,但是每个partition只会把消息发给该CG中的一个consumer。广播的实现方法是:只要每个consumer有一个独立的CG就行了。单播的实现方法是:只要所有的consumer在同一个CG。
- Broker:一台kafka服务器就是一个broker,一个集群由多个broker。一个broker可以容纳多个topic。
- partition:一个非常大恶的topic可以分布在多个broker上,一个topic可以分成多个partition,每个partition是一个有序队列。partition中的每条消息都会被分配一个有序的ID,kafka只保证按一个partition中的顺序将消息发给consumer,不保证一个topic的整体(多个partition)的顺序。
安装
这里使用docker安装,首先安装zookeeper
安装zookeeper
docker run -itd --name zookeeper -p 2181:2181 -v /etc/localtime:/etc/localtime zookeeper:3.6
- 指定端口 2181
- /etc/localtime:同步本地时间和容器时间
- 这里指定版本3.6,也可以不指定
安装docker
docker run -itd --name kafka -p 9092:9092 -e KAFKA_BROKER_ID=0 \
-e KAFKA_ZOOKEEPER_CONNECT=192.168.254.172:2181/kafka \
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.254.172:9092 \
-e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 \
-v /etc/localtime:/etc/localtime wurstmeister/kafka
- -e KAFKA_BROKER_ID=0 在kafka集群中,每个kafka都有一个BROKER_ID来区分自己
- -e KAFKA_ZOOKEEPER_CONNECT=192.168.254.172:2181/kafka
配置zookeeper管理kafka的路径192.168.254.172:2181/kafka - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.254.172:9092
把kafka的地址端口注册给zookeeper - -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 配置kafka的监听端口
- -v /etc/localtime:/etc/localtime 容器时间同步虚拟机的时间
验证
略。。。
go简单栗子
下载包
文档地址:https://godoc.org/github.com/Shopify/sarama#Broker
go get github.com/Shopify/sarama
//集群也可以用下面这个
go get github.com/bsm/sarama-cluster
异步生产者
func InitKafkaProducer()(sarama.AsyncProducer,error){
config := sarama.NewConfig()
config.Producer.RequiredAcks = sarama.WaitForAll //等待服务器所有副本都保存成功后的响应
config.Producer.Partitioner = sarama.NewRandomPartitioner //随机的分区类型
//是否等待成功和失败后的响应,只有上面的RequireAcks设置不是NoReponse这里才有用
config.Producer.Return.Successes = true
config.Producer.Return.Errors = true
config.Version = sarama.V2_5_0_0 //设置使用的kafka版本,如果低于V0_10_0_0版本,消息中的timestrap没有作用.需要消费和生产同时配置
return sarama.NewAsyncProducer([]string{"192.168.254.172:9092"},config)
}
//ProducerMsg 调用该函数生产消息
func ProducerMsg(){
prod,err := InitKafkaProducer()
if err!=nil{
panic(err)
}
defer prod.Close()
msg := &sarama.ProducerMessage{
Topic: "log",
Key: sarama.StringEncoder("test") ,
Partition :1,
}
msgchan := prod.Input()
for i:= 0;i<100;i++{
msg.Value = sarama.StringEncoder("msg id is :" + strconv.Itoa(rand.Intn(100)))
msgchan <- msg
select {
case suc := <-prod.Successes():
fmt.Println(suc)
case err := <-prod.Errors():
fmt.Println(err)
}
}
}
消费者
func InitKafkaConsumer()(sarama.Consumer,error) {
config := sarama.NewConfig()
config.Consumer.Return.Errors = true
config.Version = sarama.V0_11_0_0
return sarama.NewConsumer([]string{"192.168.254.172:9092"}, config)
}
//ConsumerMsg 调用该函数消费消息
func ConsumerMsg(){
cons,err := InitKafkaConsumer()
if err!=nil{
panic(err)
}
defer cons.Close()
//获取分区数
res,_ := cons.Partitions("log")
fmt.Println(res)
pc,err := cons.ConsumePartition("log",0,sarama.OffsetNewest)
if err!=nil{
panic(err)
}
msgchan := pc.Messages()
errchan := pc.Errors()
for {
select {
case msg :=<- msgchan :
fmt.Println("msg:",msg.Partition,string(msg.Key),string(msg.Value),msg.Timestamp.String())
case err =<- errchan :
fmt.Println(err)
}
}
}
cluster管理
需求:为一个主题添加两个分区,并指定副本
更多管理方法参考 https://godoc.org/github.com/Shopify/sarama#Broker
func ClusterMge(){
config := sarama.NewConfig()
config.Version = sarama.V2_5_0_0 //版本要高于2.4
admin,err := sarama.NewClusterAdmin([]string{"192.168.254.172:9092"},config)
if err!=nil{
panic(err)
}
//这里添加两个分区,也可以做其他操作
err = admin.CreatePartitions ("log",3,[][]int32{{0},{0}},false)
if err!=nil{
panic(err)
}
admin.Close()
}
消费者组
上面的消费者例子中,如何同时启用两个消费者,就会发现,每一条消息会被两个接受者接收到。
如果想让一条消息只被一个消费者接收到,就要设置消费者组,同组消费者对于一条消息,只有一个人能够接收到。例子如下:
//先实现接口
type exampleConsumerGroupHandler struct{}
func (exampleConsumerGroupHandler) Setup(_ sarama.ConsumerGroupSession) error { return nil }
func (exampleConsumerGroupHandler) Cleanup(_ sarama.ConsumerGroupSession) error { return nil }
func (h exampleConsumerGroupHandler) ConsumeClaim(sess sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
for msg := range claim.Messages() {
fmt.Printf("Message topic:%q partition:%d offset:%d value=%s\n", msg.Topic,msg.Partition, msg.Offset,string(msg.Value))
sess.MarkMessage(msg, "")
}
return nil
}
//启用消费者组,将程序打包后,运行同样的两个实例,会发现接收到的消息不相同
func StartConsumeGroup(){
config := sarama.NewConfig()
config.Version = sarama.V2_0_0_0 // specify appropriate version
config.Consumer.Return.Errors = true
group, err := sarama.NewConsumerGroup([]string{"192.168.254.172:9092"}, "my-group", config)
if err != nil {
panic(err)
}
defer func() { _ = group.Close() }()
go func() {
for err := range group.Errors() {
fmt.Println("ERROR", err)
}
}()
ctx := context.Background()
for {
topics := []string{"log"}
handler := exampleConsumerGroupHandler{}
err := group.Consume(ctx, topics, handler)
if err != nil {
panic(err)
}
}
}
未完待续