消息中间件能做什么
主要解决分布式系统间消息传递的问题,它能屏蔽不同平台和协议之间的特性,实现程序之间的协同。
例如将一个流程中相互独立的子操作拆开来,实现异步化,类似于多线程并行处理。
如何实现这一功能,多线程也是可以,最好的方法是用第三方消息中间件:分布式消息队列。
总之,三个字:解耦;异步;削峰
maven依赖
org.apache.kafka
kafka-clients
2.0.0
发送端代码
public class GPkafkaProduce extends Thread
{
private KafkaProducer<Integer,String> kafkaProducer;
private String topic;
//初始化kafka信息
public GPkafkaProduce(String topic)
{
Properties properties=new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.116.137:9092");
properties.put(ProducerConfig.CLIENT_ID_CONFIG,"practice-producer");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
IntegerSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
StringSerializer.class.getName());
kafkaProducer = new KafkaProducer<Integer, String>(properties);
this.topic=topic;
}
@Override
public void run()
{
//发送50个消息
int index = 0;
while(index<50)
{
String msg="pratice test message:"+index;
try
{
//同步获得返回结果
//这里的offset是该partition下一个被消费消息的指向地址
RecordMetadata recordMetadata = kafkaProducer.send(new ProducerRecord<Integer,String>(topic,msg)).get();
System.out.println(recordMetadata.offset()+"->"+recordMetadata.partition()+"->"+recordMetadata.topic());
//kafkaProducer.send(new ProducerRecord(topic,msg)).get();
//异步获取返回结果
// kafkaProducer.send(new ProducerRecord(topic, msg), new Callback()
// {
// @Override
// public void onCompletion(RecordMetadata recordMetadata, Exception e)
// {
// System.out.println("这是异步返回消息"+recordMetadata.offset()+"->"+recordMetadata.partition()+"->"+recordMetadata.topic());
// }
// });
TimeUnit.SECONDS.sleep(2);
index++;
} catch (InterruptedException e)
{
e.printStackTrace();
} catch (ExecutionException e)
{
e.printStackTrace();
}
}
}
public static void main(String[] args)
{
new GPkafkaProduce("test").start();
}
}
kafka对消息的发送支持同步和异步,本质上来说kafka用的是异步的方式发送到broker的,实际上kafka把消息是发送在一个队列中,由后台的线程不断的取消息发送,发送成功后调用callback。
kafka客户端会积累一定量的消息组装成一个批量消息发送出去,触发条件是
batch.size和linger.ms
batch.size 批量发送大小
生产者批量发送消息,为了减少性能消耗,会等待消息达到一定数量才发送消息,默认的字节数大小是16k,一批消息大小达到16k时会立即发送
linger.ms 发送的间隔时间
当消息迟迟不满16k时,也不能总是等着不发送,所以达到间隔时间时还是会发送的
二者都配置了的情况下,触发一个就发送消息
消费端代码
public class GPkafkaConsumer extends Thread
{
//消费端也要初始化
private KafkaConsumer<Integer,String> consumer;
private String topic;
//初始化kafka信息
public GPkafkaConsumer(String topic)
{
Properties properties=new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.116.137:9092");
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "practice-consumer");
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");//设置offset自动提交
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");//自动提交间隔时间
properties.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "30000");
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.IntegerDeserializer");
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringDeserializer");
properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");//对于当前groupid来说,消息的offset从最早的消息开始消费
consumer= new KafkaConsumer<Integer,String>(properties);
this.topic=topic;
;
}
@Override
public void run()
{
//消费端写个死循环去处理
while(true)
{
//阻塞
consumer.subscribe(Collections.singleton(topic));
ConsumerRecords<Integer, String> records = consumer.poll(Duration.ofSeconds(1));
records.forEach(record -> {
System.out.println(record.key() + " " + record.value() + " -> offset:" + record.offset());
});
}
}
public static void main(String[] args)
{
new GPkafkaConsumer("test").start();
}
}
group.id
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "practice-consumer");
consumer group是消费者的一个分类,一个group下有一个多个consumer,group.id是相同的
一个分区只能有一个group内的一个consumer来消费
enable.auto.commit
//设置offset自动提交
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
前面说了消息的消费状态是consumer控制的,消费者得保证自己不重复消费一条消息
消费者消费一条消息后将offset指向该分区内的下一条消息
消费者消费消息以后自动提交,只有当消息提交以后,该消息才不会被再次接收到。
注意:一般都是用手动提交的多
还可以配合
auto.commit.interval.ms控制自动提交的频率。
//自动提交间隔时间
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
auto.offset.reset
//对于当前groupid来说,消息的offset从最早的消息开始消费
properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");
这个配置有三个参数
latest、earliest、none
都是针对于新的groupid的消费者而言的
earliest 是最早的topic消息都消费
latest 新的消费者从其他消费者最后的offset开始往下消费
none 新的消费者加入以后,由于之前不存在offset,则会直接抛出异常
max.poll.records
此设置限制每次调用poll返回的消息数,这样可以更容易的预测每次poll间隔要处理的最大值。通过调整此值,可以减少poll间隔
依赖
org.springframework.kafka
spring-kafka
2.2.0.RELEASE
发送端
@Component
public class Producer
{
@Autowired
private KafkaTemplate kafkaTemplate;
public void send()
{
kafkaTemplate.send("test","msgKey","msgData");
}
}
消费端
@Component
public class Consumer
{
@KafkaListener(topics = {"test"})
public void listener(ConsumerRecord record)
{
Optional<?> msg=Optional.ofNullable(record.value());
if(msg.isPresent()){
System.out.println(msg.get());
}
}
}
测试类
public class MainKafka
{
public static void main(String[] args)
{
//以springboot方式启动
ConfigurableApplicationContext context=SpringApplication.run
(KafkaPracticeApplication.class, args);
Producer kafkaProducer=context.getBean(Producer.class);
for(int i=0;i<3;i++){
kafkaProducer.send();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
topic是kafka存储消息的,可以认为是一个消息集合。每条消息发送到kafka都有一个类别
从物理上来说,不同的topic消息是分开存储的。
partition
每个topic分为一个或多个partition,同一topic下不同partition下的消息是不同
每个消息分配到partition时都会分配一个offset,是消息在该分区内的唯一标识,kafka通过offset保证下次再分区内的顺序性,offset的顺序不跨分区,kafka只保证在同一个分区内是有顺序的。
partition是以文件的形式存储在文件系统中
sh kafka-topics.sh --create --zookeeper 192.168.116.137:2181 --replication-factor 1 --partitions 3 --topic firstTopic
在kafka中,一条消息是由key和value组成的,producer会根据key和partition的分发机制来决定把消息发到哪个partition中,可以自定义partition机制
默认的消息分发机制
kafka默认采用hash取模的分区算法,如果key为null,则随机分配分区。
metadata,是topic/partition和broker的映射关系,每一个topic每一个分区
都需要知道对应的broker列表是什么,leader是谁,foller是谁。都存在这里
自定义partition分发规则
public class MyPartitioner implements Partitioner
{
@Override
public int partition(String topic, Object key, byte[] bytes, Object o1, byte[] bytes1, Cluster cluster)
{
System.out.println("enter MyPartitioner");
List<PartitionInfo> list = cluster.partitionsForTopic(topic);
int length = list.size();
if(key==null)
{
Random random=new Random();
return random.nextInt(length);
}
return Math.abs(key.hashCode())%length;
}
@Override
public void close()
{
}
@Override
public void configure(Map<String, ?> map)
{
}
}
发送端配置自动应以partitioner
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.gupaoedu.kafka.MyPartitioner");
消费端如何消费指定分区
代码控制
//消费指定分区的时候,不需要再订阅
//kafkaConsumer.subscribe(Collections.singletonList(topic));
//消费指定的分区
TopicPartition topicPartition=new TopicPartition(topic,0);
kafkaConsumer.assign(Arrays.asList(topicPartition));
同一组消费者只有一个消费者能消费一个分区
borker上的数据分片提升了io性能,如何合理负载均衡呢
需要合理设置消费者和分区的数量
1、consumer数不要大于分区数
2、分区数最好是consumer的整数倍
3、kafka只保证在一个分区上数据有序,从多个分区读取数据,顺序不同
4、consumer、broker、partition数量变化回导致rebalance。
消费组新加了消费者
消费者挂了
topic新增了分区
消费者消费分区规则
1、RangeAssignor(范围分区)
n=分区数/消费者数 取模
m=分区数%消费者数 取余数
前m个消费者是n+1个分区,后(消费者数-m)分配n个分区
比如,10个分区,三个消费者,c1,c2,c3
n=3
m=1
前m个消费者(c1)消费4个(n+1),后面俩(3-1)消费3(n)
11个分区
n=3
m=2
c1 消费 4个
c2 消费 4个
c3 消费 3个
如果有两个主题,分别10个分区,c1就消费了 8个,其他的都是6个,好惨
2、RoundRobinAssignor(轮询分区)
按照hashcode排序,最后轮询分配分区给消费者
3、StrickyAssignor (粘滞策略)
简单的来说就俩
1、尽可能分配均匀
2、尽可能维持上次的分配相同
2个条件冲突时,1优先于2。
coordinator管理消费者组。第一个consumer启动后向kafka确定谁是
coordinator,后续组员都跟这个coordinnator协调。
kafka服务端返回负载最小的broker节点作为coordinator
rebalance 过程
1、join
选一个consumer作为leader,一般是第一个来的cnsumer,如果leader
没了,随机算法选举一个leader
coordinatro接收各消费者分分区消费策略
统计所有消费者的消费策略票数,用票数多的消费策略
2、sync
一般是由leader将消费者对应的分区分配方案同步给所有的consumer
总结
rebalance过程
coordinator 管理消费组
coordinator 在zk上增加watcher,消费者变化时,触发rebalance
开始rebalance
消费者向coordinator发起join请求,coordinator选一个leader。所有消费者
收到coordinator的返回消息,知道谁是leader
消费者向coordinator发sync请求,leader去分区分配,把结果童子所有comsumer
消息保存格式
生产者发送的消息可以永久保存在broker上,是以文件的形式保存的
默认路径是:/tmp/kafka-logs/topic_partition
eg: /tmp/kafka-logs/test_0
消息是分段保存的,n条记录后就生成新的文件;
index文件记录索引;log文件记录真实数据
如果第一个log最后一个offset是多少,下一个分段文件的命名就从这个值开始。
sh kafka-run-class.sh kafka.tools.DumpLogSegments --files /tmp/kafka-logs/mytopic-0/00000000000000000000.log --print-data-log
1、根据offset查找index索引文件,文件是以上一个文件最后一个offset命名的,所以使用二分查找快速丁文索引文件
2、找到索引文件,根据offset,定位,找出索引位置
3、得到position后,到log表中查找offset对应的消息
log文件格式:
createTime表示创建时间、keysize和valuesize表示key和 value的大小
compresscodec表示压缩编码、payload:表示消息的具体内容
日志清除或者压缩
清除策略:
1、根据保留时间,定期清除
2、根据topic存储数据大小,超过了,就开始清除旧的日志。kafka有后台线程,定期检查
通过log.retention.bytes和log.retention.hours设置
压缩策略:
kafka开启日志压缩功能;将key值相同的value合并,新value替换老的value。
磁盘存储的性能优化
零拷贝
页缓存
有时间可以了解下
offset
每个topic有一个多个分区,每个分区数据不同。
每个消息分配到分区时,贴一个offset,是这个消息在此分区中的唯一表示
kafka通过offset保证在该分区内有序,
offset在哪里维护
老版本kafka中是保存在zk上,但是zk不适合做大量的读写操作,于是kafka自己来维护了
kafka提供一个topic,把offset写到这个topic中
这个topic保存了每个消费组的某个时段的提交的offset
根据自带计算公式推断出在哪个分区