从入门到入土 04-1 kafka

这篇文章是实操篇,如果看理论篇请看这里

kafka apache开发的一个开源的流处理平台

rabbitmq和kafka的对比吞吐量测试

rabbit mq 36MB 轻量级
kafka 600MB 重量级 最新版本出了轻量级

理论或者面试题请参见 从入门到入土 04 kafka理论篇

环境:

环境

zookeeper(理解成数据库,也会检测心跳) 强一致性,选举Leader
PS:建议分区数量最好的Broker的数量一致
C# 包:confluent.kafka

安装

1、下载zookeeper镜像(kafka的运行是基于zookeeper的所以要先安装这个)下载zookeeper镜像 没啥好说的直接下就行了

docker pull wurstmeister/zookeeper

2、创建容器 然后映射下时间, 要不docker 是utc时间 就是0时区的

docker run -d --restart=always --log-driver json-file --log-opt max-size=10m --log-opt max-file=2  --name zookeeper -p 2181:2181  -v /etc/localtime:/etc/localtime wurstmeister/zookeeper

3、下载kafka镜像 没啥好说的直接下就行了

docker pull wurstmeister/kafka

4、启动kafka镜像生成容器 里面有两个ip zk 得改成你自己服务器zk的地址,kafka也得改成你kafka的安装地址, 不改不行。 你的ip肯定跟我的不一样

docker run -d --restart=always --log-driver json-file --log-opt max-size=1000m --log-opt max-file=2 --name kafka    -p 9092:9092 -e KAFKA_BROKER_ID=0 -e KAFKA_ZOOKEEPER_CONNECT=192.168.1.51:2181/kafka   -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.1.51:9092 -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092  -v /etc/localtime:/etc/localtime wurstmeister/kafka
docker run -d --restart=always --log-driver json-file --log-opt max-size=1000m --log-opt max-file=2 --name kafka    -p 9092:9092 -e KAFKA_BROKER_ID=0 -e KAFKA_ZOOKEEPER_CONNECT=192.168.1.51:2181   -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.1.51:9092 -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092  -v /etc/localtime:/etc/localtime wurstmeister/kafka

上面的两个不一样 第一个在连接的时候需要指定 properties里的 chroot path 要不连接不上。但是也许能连接上。反正我一开始就没连上 后来+了这个就ok; 如下图, 后面的 0.0.0.0的可以不改 也可以改都没影响;

参数说明:

-e KAFKA_BROKER_ID=0 在kafka集群中,每个kafka都有一个BROKER_ID来区分自己 集群的时候就有用了
-e KAFKA_ZOOKEEPER_CONNECT=192.168.1.51:2181/kafka 配置zookeeper管理kafka的路径192.168.1.51:2181/kafka
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.1.51:9092 把kafka的地址端口注册给zookeeper,如果是远程访问要改成外网IP,类如Java程序访问出现无法连接。
-e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 配置kafka的监听端口
-v /etc/localtime:/etc/localtime 容器时间同步虚拟机的时间

5、验证kafka是否可以使用
5.1、进入容器

docker exec -it kafka bash

5.2、进入 /opt/kafka_2.12-2.3.0/bin/ 目录下

cd /opt/kafka_2.12-2.3.0/bin/

5.3、运行kafka生产者发送消息

./kafka-console-producer.sh --broker-list localhost:9092 --topic test

发送消息

hello word!

5.4、运行kafka消费者接收消息

./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test--from-beginning

5.5 防火墙放行两个端口 否则只能本地访问;

文字术语:

Producer
Consumer
Consumer
Group/Consumers
Broker
Topic
Partition
Replica
Offset

C# 代码

依赖 Confluent.Kafka 1.4.3
依赖 protobuf-net 2.4.6

net5代码(生产者):

public class Producer
	{
		public async Task Show()
		{

			string brokerList = "IP:PORT,IP:PORT,IP:PORT"; //如果是单节点 那就写一个
			string topicName = "test";

			var config = new ProducerConfig
			{
				BootstrapServers = brokerList,
				//幂等性
				EnableIdempotence = true,
				Acks = Acks.All,
				//LingerMs = 10000,
				BatchNumMessages = 1,
				//MessageSendMaxRetries=3, 
				//Partitioner = Partitioner.Murmur2Random 			};
			NewInfo newInfo = new NewInfo()
			{
				P_ID = 1,
				P_Title = "tltle",
				P_Content = "P_Content",
				P_CreateTime = DateTime.Now.ToString("yyyyMMdd")
			};
			using (var producer = new ProducerBuilder<string, NewInfo>(config)
				.SetValueSerializer(new CustomStringSerializer<NewInfo>())
				.SetStatisticsHandler((o, json) =>
				{
					Console.WriteLine("json");
					Console.WriteLine(json);
				})
				.Build())
			{

				Console.WriteLine("\n-----------------------------------------------------------------------");
				Console.WriteLine($"Producer {producer.Name} producing on topic {topicName}.");
				Console.WriteLine("-----------------------------------------------------------------------");
				try
				{
					var deliveryReport = await producer.ProduceAsync(
						topicName, new Message<string, NewInfo> { Key = "newinfo", Value = newInfo });

					Console.WriteLine($"delivered to: {deliveryReport.TopicPartitionOffset}");
				}
				catch (ProduceException<string, string> e)
				{
					Console.WriteLine($"failed to deliver message: {e.Message} [{e.Error.Code}]");
				}
			}
		}
	}

消费者代码:

	{
		public void Show()
		{
			string brokerList = "39.96.82.51:9093,47.95.2.2:9092,39.96.34.52:9092";
			string topicName = "test";
			var config = new ConsumerConfig
			{
				BootstrapServers = brokerList,
				GroupId = "mm",
				EnableAutoCommit = false,
				StatisticsIntervalMs = 5000,
				AutoOffsetReset = AutoOffsetReset.Earliest,
				EnablePartitionEof = true
			};
			const int commitPeriod = 1;
			using (var consumer = new ConsumerBuilder<Ignore, NewInfo>(config)
				.SetErrorHandler((_, e) => Console.WriteLine($"Error: {e.Reason}"))

				.SetPartitionsAssignedHandler((c, partitions) =>
				{
					Console.WriteLine($"Assigned partitions: [{string.Join(", ", partitions)}]");
				})
				.SetPartitionsRevokedHandler((c, partitions) =>
				{
					Console.WriteLine($"Revoking assignment: [{string.Join(", ", partitions)}]");
				})
					.SetValueDeserializer(new CustomStringIDeserializer<NewInfo>())
				.Build())
			{
				consumer.Subscribe(topicName);
				try
				{
					while (true)
					{
						try
						{
							var consumeResult = consumer.Consume();

							if (consumeResult.IsPartitionEOF)
							{
								//Console.WriteLine(
								//	$"Reached end of topic {consumeResult.Topic}, partition {consumeResult.Partition}, offset {consumeResult.Offset}.");

								continue;
							}

							Console.WriteLine($"Received message at {consumeResult.TopicPartitionOffset}: {consumeResult.Message.Value}");
							Console.WriteLine($": {consumeResult.TopicPartitionOffset}::{consumeResult.Message.Value.P_Title}:{consumeResult.Message.Value.P_Content}");

							if (consumeResult.Offset % commitPeriod == 0)
							{
								try
								{
									//consumer.Commit(consumeResult);
								}
								catch (KafkaException e)
								{
									Console.WriteLine($"Commit error: {e.Error.Reason}");
								}
							}
						}
						catch (ConsumeException e)
						{
							Console.WriteLine($"Consume error: {e.Error.Reason}");
						}
					}
				}
				catch (OperationCanceledException)
				{
					Console.WriteLine("Closing consumer.");
					consumer.Close();
				}
			}
		}
	}
	```
把代码用委托做一个 死循环代码隐藏是最好的;

```csharp
if (Kafka_Switch)
            {
                KafkaConsumer KafkaConsumer = new KafkaConsumer(Kafka_Broker_Address);
                foreach (var OrgId in OrgIds)
                {
                    var TopicList = new List<string>() { XianChao_WaterFlow_Kafka_Topic.Replace("{OrgId}", OrgId.ToString()) };

                    //死循环外(Kafka封装类里有while死循环) 不开新线程服务会被阻塞无法停止
                    var KafkaThread = new Thread(() =>
                    {
                        try
                        {
                            KafkaConsumer.Run_Consume(TopicList, XianChao_WaterFlow_Kafka_GroupId, (key, msg) =>
                            {
                                //委托处理数据  不知道为什么实际拿到的key是null 这里不管key了
                                var XianChao_Kafka_Data = JsonConvert.DeserializeObject<XianChao_Kafka_Model>(msg);
                                //插入数据库
                                WaterMeter_Data_Insert_To_DB(XianChao_Kafka_Data);

                            });
                        }
                        catch (Exception ex)
                        {
                            LogHelper.Error(typeof(XianChao_WaterMeter_Data_Transmit_Task), ex.Message, ex);
                            CommonMethod.CreateTXTLog($"先超水流获取kafka获取 异常 \r 错误:\t{ex.Message} \r 错误源:\t{ex.StackTrace}", "_WaterFlow");
                        }
               
                    });

                    KafkaThread.Start();
                }
            }

处理接收数据用的委托方法

public void Run_Consume(List<string> Topics, string Group,CallBackFunctionDelegate CallBackFunction)
        {
            _consumerConfig.GroupId = Group;
            const int commitPeriod = 1;
            using (var consumer = new ConsumerBuilder<Ignore, string>(_consumerConfig).Build())
            {
                //消费者会影响在平衡分区,当同一个组新加入消费者时,分区会在分配
                consumer.Subscribe(Topics);
                    while (true)
                    {
                        try
                        {
                            var consumeResult = consumer.Consume();
                            if (consumeResult.IsPartitionEOF)
                            {
                                continue;
                            }
                            //使用委托来处理数据  
                            CallBackFunction?.Invoke(consumeResult.Message.Key != null ? consumeResult.Message.Key.ToString() : null, consumeResult.Message.Value);
                            if (consumeResult.Offset % commitPeriod == 0)
                            {
                                try
                                {
                                    //程序员来控制提交 ack?
                                    consumer.Commit(consumeResult);
                                }
                                catch (KafkaException e)
                                {
                                    throw new Exception($"Kafka Consume Error: {e.Message};error code: {e.Error.Code}");
                                }
                            }
                        }
                        catch (ConsumeException e)
                        {
                            throw new Exception($"Kafka Consume Error: {e.Message};error code: {e.Error.Code}");
                        }
                    } 
            }
        }

集群的连接方式:

string brokerList = "39.96.8.*:端口,IP:端口,IP:端口";

这篇文章是实操篇,如果看理论篇请看这里

首先简单介绍一下Kafka的架构和涉及到的名词:
Topic:这个是消息的主题用于划分Message的逻辑概念,一个Topic可以分布在多个Broker上。
Partition:分区:是Kafka中横向扩展和一切并行化的基础,每个topic可以有多个分区,分区的作用是做负载,提高kafka的吞吐量。 每个Topic都至少被切分为1个Partition。
Offset:消息在Partition中的编号,编号顺序不跨Partition。如果想不重复 那就只能在一个Partition里了
Consumer:用于从Broker中取出/消费Message。
Producer:用于往Broker中发送/生产Message。
Replication:Kafka支持以Partition为单位对Message进行冗余备份,每个Partition都可以配置至少1个Replication(当仅1个Replication时即仅该Partition本身)。
Leader:每个Replication集合中的Partition都会选出一个唯一的Leader,所有的读写请求都由Leader处理。其他Replicas从Leader处把数据更新同步到本地,过程类似大家熟悉的MySQL中的Binlog同步。
Broker:Kafka中使用Broker来接受Producer和Consumer的请求,并把Message持久化到本地磁盘。每个Cluster当中会选举出一个Broker来担任Controller,负责处理Partition的Leader选举,协调Partition迁移等工作。
Consumer Group:我们可以将多个消费组组成一个消费者组,在kafka的设计中同一个分区的数据只能被消费者组中的某一个消费者消费。同一个消费者组的消费者可以消费同一个topic的不同分区的数据,这也是为了提高kafka的吞吐量!
**ISR(In-Sync Replica):是Replicas的一个子集,表示目前Alive且与Leader能够“Catch-up”的Replicas集合。**由于读写都是首先落到Leader上,所以一般来说通过同步机制从Leader上拉取数据的Replica都会和Leader有一些延迟(包括了延迟时间和延迟条数两个维度),任意一个超过阈值都会把该Replica踢出ISR。每个Partition都有它自己独立的ISR。

生产快
producer ——> broker
分布式broker
简单说就是通过buffer控制数据的发送,有时间阀值与消息数量阀值来控制
也可以更简单的说是批量发送
存储快
broker ——>disk
顺序读写(Sequence I/O ) 要比随机读写快很多很多。Kafka 官方给出的测试数据(Raid-5,7200rpm):
Sequence I/O: 600MB/s (实验室)
Sequence I/O: 400MB-500MB/s (工作场景)
Random I/O: 100KB/s

3.Consumer的负载均衡
当一个group中,有consumer加入或者离开时,会触发partitions均衡.均衡的最终目的,是提升topic的并发消费能力,步骤如下:
1、 假如topic1,具有如下partitions: P0,P1,P2,P3
2、 加入group中,有如下consumer: C1,C2
3、 首先根据partition索引号对partitions排序: P0,P1,P2,P3
4、 根据consumer.id排序: C0,C1
5、 计算倍数: M = [P0,P1,P2,P3].size / [C0,C1].size,本例值M=2(向上取整)
6、 然后依次分配partitions: C0 = [P0,P1],C1=[P2,P3],即Ci = [P(i * M),P((i + 1) * M -1)]

==================================================
如果上面的都没看懂那么请看下面的吧:
写入的时候 需要有topic 这个是必须的,为啥写的快。 那就是他可以按时间或者数量来搞批量的写入,然后是传说中的顺序写入;
存储的时候呢 他是有多个broker和多个Partition做负载,同时又有多个Replication副本做备胎或者是备份;
读取的时候 先从 PageCache 中查找 类似于布隆过滤器,有就直接在这读 没有就找下一个,然后还有0拷贝这么个牛逼的技术;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值