• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
夣築
不该坚持的,就要坚决放弃;内心追求的,就要勇敢去追;不要做语言上的巨人,行动上的矮子。
博客园    首页    新随笔    联系   管理    订阅  订阅

消息队列 —— Kafka

目录
  • 一. Kafka 简介
    • 1. 概述
    • 2. Kafka 的主要设计目标
    • 3. 消息系统介绍
        • 3.1 点对点的消息传递模式
        • 3.2 发布-订阅消息传递模式
    • 4. Kafka 的优势
    • 5. Kafka 的应用场景
  • 二. Kafka 架构
    • 1. Kafka 基本架构
        • (1)消息和批次
        • (2)消息模式
        • (3)主题和分区
        • (4)生产者和消费者
        • (5)broker和集群
        • (6)多集群
    • 2. Kafka 核心概念
        • 1. broker
        • 2. Topic
        • 3. Partition
        • 4. Producer(生产者)
        • 5. Consumer(消费者)
        • 6. Consumer Group
        • 7. Replicas
        • 8. HW
        • 9. LEO
        • 10. Offset 消息偏移量
  • 三. Kafka 的安装与配置
    • 1. Java 环境的安装与配置(JDK 版本为 1.8)
    • 2. Zookeeper 的安装与配置(版本为 3.4.14)
    • 3. Kafka 的安装与配置
  • 四. Kafka 的应用
    • 1. 脚本应用-主题管理
    • 2. 脚本应用-生产者操作
    • 3. 脚本应用-消费者操作
    • 4.客户端应用-生产者
      • (1)生产者发送消息的过程
      • (2)创建生产者
      • (3)发送消息
    • (4)自定义分区器
    • 5.客户端应用-消费者
      • (1)消费者和消费者组
      • (2)创建Kafka消费者、订阅主题、轮询
  • 五. Kafka 高级特性
    • 1. 生产者
      • (1)数据生产流程
      • (2)自定义序列化器
      • (3)分区器
      • (4)拦截器
    • 2. 消费者
      • (1)订阅主题
      • (2)反序列化
      • (3)消费者位移
      • (4)心跳机制
      • (5)再均衡
      • (6)拦截器
    • 3. 主题管理
      • (1)副本分割机制
      • (2)分区重新分配
    • 4. 物理存储
      • (1)日志索引文件
      • (2)日志清理机制
    • 5. 磁盘存储
      • (1)零拷贝
      • (2)页缓存
      • (3)顺序写入
  • 六. Kafka 集群配置
    • 1.集群配置规划
    • 2. 集群配置
      • (1)配置 java 环境变量
      • (2)配置 zookeeper 集群环境
      • (3)配置 kafka 集群环境

一. Kafka 简介

1. 概述

    Kafka是最初由Linkedin公司开发,是一个分布式、分区的、多副本的、多订阅者,基于zookeeper协调的分布式日志系统(也可以当做MQ系统),常见可以用于web/nginx日志、访问日志,消息服务等等,Linkedin于2010年贡献给了Apache基金会并成为顶级开源项目。
    主要的应用场景是:日志手机系统和消息系统

2. Kafka 的主要设计目标

  • 以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能保证常数时间的访问性能
  • 高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒100K条消息的传输
  • 支持Kafka Server间的消息分区,及分布式消费,同时保证每个partition内的消息顺序传输
  • 支持离线数据处理和实时数据处理
  • 支持在线水平扩展

3. 消息系统介绍

    一个消息系统负责将数据从一个用用传递到另一个应用,应用只需关注于数据,无需关注在两个或多个应用间是如何传递的。分布式消息传递基于可靠的消息队列,在客户端应用和消息系统之间异步传递消息。有两种主要的消息传递模式:

  • 点对点传递模式
  • 发布订阅模式
    大部分的消息系统选用发布订阅模式,Kafka 就是一种发布-订阅模式

3.1 点对点的消息传递模式

    在点对点消息系统中,消息持久化到一个队列中。此时,将有一个或多个消费者消费队列中的数据。但是一条消息只能被消费一次。当一个消费者消费了队列中的某条数据之后,该条数据则从消息队列中删除。该模式即使有多个消费者同时消费数据,也能保证数据处理的顺序。这种结构描述示意图如下:
image
    生产者发送一条消息到 queue 队列中,只有一个消费者可以收到。

3.2 发布-订阅消息传递模式

    在发布-订阅消息系统中,消息被持久化到一个 topic 中。与点对点消息系统不同的是,消费者可以订阅一个或多个 topic,消费者可以消费该 topic 中所有的数据,同一条数据可以被多个消费者消费,数据被消费后不会立马删除。在发布-订阅消息系统中,消息的生产者被称为发布者,消费者成为订阅者。该模式的示例图如下:
image
    发布者发送到 topic 的消息,只有订阅了 topic 的订阅者才会收到消息。

4. Kafka 的优势

  • 高吞吐量
  • 高性能
  • 持久化顺序存储
    • 零拷贝
    • 顺序读写
    • 页缓存
  • 分布式系统
  • 可靠性
  • 支持 online 和 offline 的场景
  • 支持多种客户端语言

5. Kafka 的应用场景

  • 日志收集:一个公司可以用 kafka 收集各个服务的 log,通过 kafka 以统一接口服务的方式,开放给各个消费者(consumer)
  • 消息系统:解耦生产者和消费者、缓存消息等
  • 用户活动跟踪:kafka 经常被用来记录 web 用户或者 app 用户的各种活动,入浏览网页、搜索、点击等活动,这些活动信息呗各个服务器发不到 kafka 的 topic 中,然后消费者通过订阅这些 topic 来做实时的监控分析,也可以保存到数据库
  • 运营指标:kafka 也经常用来记录运营监控数据。包括搜集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告

二. Kafka 架构

1. Kafka 基本架构

(1)消息和批次

    Kafka 的数据单元被称为“消息”,相当于数据库表的一行记录。消息可以拥有一个可选的元数据,也就是键。通过键可以将消息写入不同的分区。
    为了提高效率,消息可以分批次写入 kafka,批次就是一组消息,这些消息属于同一个主题和分区。

(2)消息模式

    对于Kafka来说,消息不过是晦涩难懂的字节数组,所以有人建议用一些额外的结构来定义消息的内容,让它们更易于理解。根据应用程序的需求,消息模式(schema)有许多可用的选项,像JSON和XML这些简单的系统不仅易用,而且可读性好。

(3)主题和分区

    Kafka的消息通过主题进行分类。主题就好比数据库的表,或者文件系统里面的文件夹。主题可以被分为弱哦干个分区,一个分区就是一个提交日志。消息以追加的方式写入分区,然后以先入先出的顺序读取。一般一个主题包含几个分区,因此无法整个主题范围内保证消息的顺序,但可以保证消息在单个分区内的顺序。如下图所示的主题有四个分区,消息被追加写入每个分区的尾部。kafka通过分区来实现数据冗余和伸缩性。分区可以分布在不同的服务器上,也就是一个主题可以横跨多个服务器,以此来提供比单个服务器更大的性能
image

(4)生产者和消费者

    Kafka的客户端就是Kafka的系统的用户,它们被分为两种基本类型:生产者和消费者。除此之外,还有其他高级客户端API---用于数据继承的Kafka Connect API和用于流式处理的Kafka Streams。这些高级客户端API使用生产者和消费者作为内部组件,通过了高级功能
    生产者创建消息。在其他发布和订阅系统中,生产者可能被称为发布者或写入者。一般情况下,一个消息被发布到一个特定的主题上。生产者默认情况下把消息均衡地分布到主题的所有分区上,而并不关心特定消息会被写到哪个分区。不过,在某些情况下,生产者会把消息直接写到指定的分区。通常是通过消息键和分区器来实现的,分区器为键生成一个散列值,并将其映射到指定的分区上,这样可以保证包含同一个键的消息会被写到同一个分区上。生产者也可以使用自定义的分区器,根据不同的业务规则将消息映射到分区。
    消费者读取消息。消费者订阅一个或多个主题,并按照消息生成的顺序读取它们。消费者通过检查消息的偏移量来区分已经读过的消息。偏移量是另一种元数据,它是一个不断递增的整数值,在创建消息时,Kafka会把它添加到消息里面,在给定的分区里,每个消息的偏移量都是唯一的。消费者把每个分区最后读取的消息偏移量保存在Zookeeper或Kafka上,如果消费者关闭或重启,它的读取状态不会丢失。 消费者是消费者群组的一部分,一个或多个消费者共同读取一个主题。群组保证每个分区只能被一个消费者使用
image

(5)broker和集群

    一个独立的Kafka服务器称为broker,broker接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。broker为消费者提供服务,对读取分区的请求作出响应,返回已经提交到磁盘上的消息。根据特定的硬件及其性能特征,单个broker可以轻松处理数千个分区以及每秒百万级的消息量。
    broker是集群的组成部分。每个集群都有一个broker同时充当了集群控制器的角色(自动从集群的活跃成员中选举出来)。控制器负责管理工作,包括将分区分配给broker和监控broker。在集群中,一个分区从属于一个broker,该broker被称为分区的首领。一个分区可以分配多个broker,这个时候会发生分区复制。这种复制机制为分区提供了消息冗余,如果一个broker失效,其他broker可以接管领导权。不过,相关的消费者和生产者都要重新连接到新的首领。
image
    保留消息是Kafka的一个重要特性。Kafka broker默认的消息保留策略有两种。

  • 保留一段固定的时间。比如7天
  • 保留到消息达到一定大小的字节数,如1GB 当达到上限后,旧的消息会被过期删除。所以在任何时刻,可用消息的总量不会超过配置参数所指定的大小。

(6)多集群

    Kafka的消息复制机制只能在单个集群里面进行,不能在多个集群之间进行。Kafka提供了一个叫做MirrorMaker的工具,可以用它来实现集群间的消息复制。MirrorMaker的核心组件包含了一个生产者和一个消费者,两者通过一个队列相连。消费者从一个集群读取消息,生产者把消息发送到另一个集群。
image

2. Kafka 核心概念

image

1. broker

    Kafka 集群包含一个或多个服务器,服务器节点称为 broker
    broker存储topic的数据。如果某topic有N个partition,集群有N个broker,那么每个broker存储该topic的一个partition
    如果某topic有N个partition,集群有(N+M)个broker,那么其中有N个broker存储该topic的一个partition,剩下的M个broker不存储该topic的partition数据
    如果某topic有N个partition,集群中broker数目少于N个,那么一个broker存储该topic的一个或多个partition。在实际生产环境中,尽量避免这种情况的发生,这种情况容易导致Kafka集群数据不均衡

2. Topic

    每条发布到 Kafka 集群的消息都有一个类别,这个类别被称为 Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处)

3. Partition

    topic中的数据分割为一个或多个partition。每个topic至少有一个partition。每个partition中的数据使用多个segment文件存储。partition中的数据是有序的,不同partition间的数据丢失了数据的顺序。如果topic有多个partition,消费数据时就不能保证数据的顺序。在需要严格保证消息的消费顺序的场景下,需要将partition数目设为1

4. Producer(生产者)

    生产者创建消息。
    该角色将消息发布到Kafka的topic中。broker接收到生产者发送的消息后,broker将该消息追加到当前用于追加数据的 segment 文件中。一般情况下,一个消息会被发布到一个特定的主题上。
Ⅰ. 默认情况下通过轮询吧消息均衡地分布到主题的所有分区上。
Ⅱ. 在某些情况下,生产者会吧消息直接写到指定的分区。这通常是通过消息键和分区器来实现的,分区器为键生成一个散裂值,并将其映射到指定的分区上。这样可以保证包含同一个键的消息会被写到同一个分区上。
Ⅲ. 生产者也可以使用自定义的分区器,根据不同的业务规则将消息映射到分区。

5. Consumer(消费者)

    消费者读取消息。
A. 消费者订阅一个或多个主题,并按照消息生成的顺序读取它们。
B. 消费者通过检查消息的偏移量来区分已经读取过的消息。偏移量是另一种元数据,它是一个不断递增的整数值,在创建消息时,Kafka 会把它添加到消息里。在给定的分区里,每个消息的偏移量都是唯一的。消费者把分每个分区最后读取的消息偏移量保存在 Zookeeper 或 Kafka 上,如果消费者关闭或重启,它的读取状态不会丢失。
C. 消费者是消费组的一部分。群组保证每个分区只能被一个消费者使用。
D. 如果一个消费者失效,消费组里其它消费者可以接管失效消费者的工作,再平衡,分区重新分配。

6. Consumer Group

    Kafka 和其他消息系统有一个不一样的设计,在 consumer 之上加了一层 group。同一个group的consumer可以并行消费同一个topic的消息,但是同group的consumer,不会重复消费。这就好比多个consumer组成了一个团队,一起干活,当然干活的速度就上来了。group中的consumer是如何配合协调的,其实和topic的分区相关联。
    如果同一个topic需要被多次消费,可以通过设立多个consumer group来实现。每个group分别消费,互不影响。

7. Replicas

    Kafka 使用主题来组织数据,每个主题被分为若干个分区,每个分区有多个副本。那些副本被保存 在broker 上,每个broker 可以保存成百上千个属于不同主题和分区的副本。副本有以下两种类型:

  • Leader(首领副本)
        每个分区都有一个首领副本,为了保证一致性,所有生产者请求和消费者请求都会经过这个副本。
  • Follower(跟随者副本)
        首领以外的副本都是跟随者副本。跟随者副本不处理来自客户端的请求,他们唯一的任务就是从首领副本那里复制消息,保持与首领副本一致的状态。如果首领发生崩溃,其中一个跟随者会被提升为新首领。
        跟随者副本包括同步副本和不同步副本,在发生首领副本切换的过程中,只有同步副本可以切换为首领副本。
    A. AR
        分区中的所有副本统称为AR(Assigned Replicas)。AR=ISR+OSR
    B. ISR
        所有与 leader 副本保持一定程度同步的副本(包括leader)组成ISR(In-Sync Replicas),ISR 集合是 AR 集合中的一个子集。消息会先发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步,同步期间 follower 副本相对于 leader 副本而言会有一定程度的之后。前面所说的“一定程度”是指可以忍受的之后范围,这个范围可以通过参数进行配置。
    C. OSR
        与 leader 副本同步滞后过多的副本(不包括 leader)副本,组成 OSR(Out-Sync Replicas)。在正常情况下,所有的follower 副本都应该与 leader 副本保持一定程度的同步,即 AR=ISR,OSR 集合为空。

8. HW

    HW 是 High Watermak 的缩写,缩成高水位,它表示了一个特定消息的偏移量(offset),消费者只能拉取到这个 offset 之前的消息。

9. LEO

    LEO 是 Log End Offset 的缩写,它表示了当前日志文件中下一条待写入消息的 offset。
image

10. Offset 消息偏移量

  • 生产者 Offset
        消息写入的时候,每一个分区都有一个 offset,这个 offset 就是生产者 offset,同时也是这个分区的最新最大的 offset。有些时候没有指定某一个分区的 offset,这个工作 kafka 帮我们完成。
    image
  • 消费者 Offset
    image
        这是一个分区的 offset 情况,生产者写入的 offset 是最新最大的值是 12,而当 Consumer A 进行消费时,从 0 开始消费,一直消费到了 9,消费者的 offset 就记录到 9,Consumer B 就记录在了 11,。等一次他们在来消费时,他们可以选择接着上一次的位置消费,当然也可以选择从头消费,或者跳到最近的记录并从“现在”开始消费。

三. Kafka 的安装与配置

1. Java 环境的安装与配置(JDK 版本为 1.8)

2. Zookeeper 的安装与配置(版本为 3.4.14)

3. Kafka 的安装与配置

(1)下载 Kafka

wget https://archive.apache.org/dist/kafka/1.0.2/kafka_2.12-1.0.2.tgz

(2)解压下载的文件到指定的安装目录

tar -zxvf kafka_2.12-1.0.2.tgz -C 安装目录

(3)配置 Kafka 的环境变量以及其他相关配置

# 配置 kafka 的环境变量
vim /etc.profile
# 在 etc.profile 的文件末尾增加如下配置:
export KAFKA_HOME=kafka 的安装目录
export PATH=$PATH:$KAFKA_HOME/bin
# 使得环境变量生效
source /etc/profile

# 修改 kafka 的其他配置
# 进入到 kafka 的配置文件的目录
cd kafka 的安装目录/config
# 编辑 server.properties 文件
vim server.properties
# 修改 zookeeper 的连接(指定连接的zookeeper)
zookeeper.connect=linux121:2181/myKafka
# 修改日志存储位置
log.dirs=指定 log 文件存储位置
# 修改控制器
broker.id=0
# 修改监听信息
listeners=PLAINTEXT://:9092

(4)启动 kafka

  • 前台启动(不推荐)
kafka-server-start.sh ../config/server.properties

image

  • 后台启动
kafka-server-start.sh -daemon /opt/lagou/servers/kafka_2.12-1.0.2/config/server.properties

image

四. Kafka 的应用

1. 脚本应用-主题管理

(1)创建主题

kafka-topics.sh --create --zookeeper linux121:2181 --replication-factor 2 --partitions 3 --topic kafka-action
  • --zookeeper 表示 zookeeper 所在的ip,--zookeeper 为必传参数,多个 zookeeper 用 ‘,’分开
    --zookeeper 参数是之前版本的用法,从kafka 2.2 版本开始,社区推荐使用 --bootstrap-server 参数替换 --zoookeeper ,并且显式地将后者标记为 “已过期”,因此,如果你已经在使用 2.2 版本了,那么创建主题请指定 --bootstrap-server 参数。
    推荐使用 --bootstrap-server 而非 --zookeeper 的原因主要有两个。
  • 使用 --zookeeper 会绕过 Kafka 的安全体系。这就是说,即使你为 Kafka 集群设置了安全认证,限制了主题的创建,如果你使用 --zookeeper 的命令,依然能成功创建任意主题,不受认证体系的约束。这显然是 Kafka 集群的运维人员不希望看到的。
  • 使用 --bootstrap-server 与集群进行交互,越来越成为使用 Kafka 的标准姿势。换句话说,以后会有越来越少的命令和 API 需要与 ZooKeeper 进行连接。这样,我们只需要一套连接信息,就能与 Kafka 进行全方位的交互,不用像以前一样,必须同时维护 ZooKeeper 和 Broker 的连接信息。
    --partitions 用于设置主题分区数,每个线程处理一个分区数据
    --replication-factor 用于设置主题副本数,每个副本分布在不同的节点,不能超过总结点数。如你只有一个节点,但是创建时指定副本数为2,就会报错。
    (2)查询主题
  • 查看所有主题
kafka-topics.sh --zookeeper linux121:2181/clusterKafka --list

image

  • 查看某个特定主题信息,不指定 topic 则查询所有
 kafka-topics.sh --zookeeper linux121:2181/clusterKafka --describe --topic 主题名称

image
(3)修改主题

  • 修改主题分区(增加分区)
kafka-topics.sh --zookeeper zookeeper的 ip:port --alter --topic <topic_name> --partitions < 新分区数 >

image

注意:指定的分区数一定要比原有的分区数大,否则 Kafka 会抛出 InvalidPartitionsException 异常
image

  • 修改主题级别参数
        在主题创建之后,我们可以使用 kafka-configs 脚本修改对应的参数。
kafka-configs.sh --zookeeper zookeeper_host:port --entity-type topics --entity-name <topic_name> --alter --add-config max.message.bytes=10485760
  • 变更副本数
        使用自带的kafka-reassign-partitions脚本,帮助我们增加主题的副本数。
        假设kafka的内部主题 __consumer_offsets 只有 1 个副本,现在我们想要增加至 3 个副本。下面是操作:
    • 1.创建一个 json 文件,显示提供 50 个分区对应的副本数。注意:replicas 中的 3 台 Broker 排列顺序不同,目的是将 Leader 副本均匀地分散在 Broker 上。该文件具体格式如下:
{"version":1, "partitions":[
 {"topic":"__consumer_offsets","partition":0,"replicas":[0,1,2]}, 
  {"topic":"__consumer_offsets","partition":1,"replicas":[0,2,1]},
  {"topic":"__consumer_offsets","partition":2,"replicas":[1,0,2]},
  {"topic":"__consumer_offsets","partition":3,"replicas":[1,2,0]},
  ...
  {"topic":"__consumer_offsets","partition":49,"replicas":[0,1,2]}
]}
    • 2.执行 kafka-reassign-partitions脚本,命令如下:
kafka-reassign-partitions.sh --zookeeper zookeeper_host:port --reassignment-json-file reassign.json --execute
  • 查看消费者提交的位移数据
kafka-console-consumer.sh --bootstrap-server kafka_host:port --topic __consumer_offsets --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter" --from-beginning

image

  • 查看消费者组的状态信息
kafka-console-consumer.sh --bootstrap-server kafka_host:port --topic __consumer_offsets --formatter "kafka.coordinator.group.GroupMetadataManager\$GroupMetadataMessageFormatter" --from-beginning

image

2. 脚本应用-生产者操作

kafka-console-producer.sh --broker-list kafka_host:port --topic <topic_name>

image

3. 脚本应用-消费者操作

kafka-console-consumer.sh --bootstrap-server kafka_host:port --topic <topic_name>

image

kafka-console-consumer.sh --bootstrap-server kafka_host:port --topic <topic_name> --from-beginning

--from-beginning 添加后可以接收到所有的消息,包括中断前的消息

4.客户端应用-生产者

(1)生产者发送消息的过程

  • Kafka 会将发送消息包装为ProducerRecord对象,ProducerRecord对象包含了目标主题和要发送的内容,同时还可以指定键和分区。在发送ProducerRecord对象前,生产者会先把键和值对象序列化成字节数组,这样它们才能够在网络上传输。
  • 接下来,数据被传给分区器。如果之前已经在ProducerRecord对象里指定了分区,那么分区器就不会再做任何事情。如果没有指定分区,那么分区器会根据ProducerRecord对象的键来选择一个分区,紧接着,这条记录被添加到一个记录批次里,这个批次里的所有消息会被发送到相同的主题和分区上。有一个独立的线程负责把这些记录批次发送到相应的 broker 上。
  • 服务器在收到这些消息时会返回一个响应。如果消息成功写入 Kafka,就返回一个RecordMetaData对象,它包含了主题和分区信息,以及记录在分区里的偏移量。如果写入失败,则会返回一个错误。生产者在收到错误之后会尝试重新发送消息,如果达到指定的重试次数还没有成功,则直接抛出异常,不在重试。
    image

(2)创建生产者

  • 项目依赖
 <dependency>
	 <groupId>org.apache.kafka</groupId>
	 <artifactId>kafka-clients</artifactId>
	 <version>1.0.2</version>
 </dependency>
  • 创建生产者
        创建Kafka生产者时,以下三个属性是必须指定的:
    • bootstrap.servers:指定 broker 的地址清单,清单里不需要包含所有的 broker 地址,生产者会从给定的 broker 里查找 broker 的信息。不过建议至少要提供两个 broker 的信息作为容错
    • key.serializer:指定键的序列化器
    • value.serializer:指定值的序列化器
package com.lagou.kafka.demo;

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Properties;


public class SimpleProducer {
    public static void main(String[] args) {
        String topicName = "Hello-Kafka";
        // 创建属性类
        Properties props = new Properties();
        // 指定 broker 的地址清单
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"47.95.26.48:9092");
        // 指定键的序列化器
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        // 指定值的序列化器
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class);
        // 创建生产者
        Producer<String, String> producer = new KafkaProducer<>(props);
        for (int i = 0; i < 10; i++) {
            ProducerRecord<String, String> producerRecord = new ProducerRecord<>(topicName, "hello" + i, "world" + i);
            // 发送消息
            producer.send(producerRecord);
        }
        // 关闭生产者
        producer.close();
    }
}

(3)发送消息

    上面的示例程序调用了 send 方法发送消息后没有做任何操作,在这种情况下,我们没有办法知道消息发送的结果。想要知道消息发送的结果,可以使用同步发送或者异步发送来实现。

  • 同步发送消息
        在调用 send 方法后可以接着调用 get() 方法,send 方法的返回值是一个Future<RecordMetadata>对象,RecordMetadata 里面包含了发送消息的主题、分区、偏移量等信息。该写后的代码如下:
package com.lagou.kafka.demo;

import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Properties;
import java.util.concurrent.ExecutionException;


public class SimpleProducer {
    public static void main(String[] args) {
        String topicName = "Hello-Kafka";
        // 创建属性类
        Properties props = new Properties();
        // 指定 broker 的地址清单
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"47.95.26.48:9092");
        // 指定键的序列化器
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        // 指定值的序列化器
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class);
        // 创建生产者
        Producer<String, String> producer = new KafkaProducer<>(props);

        for (int i = 0; i < 10; i++) {
            try {
                ProducerRecord<String, String> producerRecord = new ProducerRecord<>(topicName, "hello" + i, "world" + i);
                // 同步发送消息
                RecordMetadata metadata = producer.send(producerRecord).get();
                System.out.printf("topic=%s, partition=%d, offset=%s \n",
                        metadata.topic(), metadata.partition(), metadata.offset());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }

        }
        // 关闭生产者
        producer.close();
    }
}
  • 异步发送消息
        通常我们并不关心发送成功的情况,更多关注的是失败的情况,因此 Kafka 提供了异步发送和回调函数。代码如下:
package com.lagou.kafka.demo;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
import java.util.concurrent.ExecutionException;


public class SimpleProducer {
    public static void main(String[] args) {
        String topicName = "Hello-Kafka";
        // 创建属性类
        Properties props = new Properties();
        // 指定 broker 的地址清单
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"47.95.26.48:9092");
        // 指定键的序列化器
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        // 指定值的序列化器
     props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class);
        // 创建生产者
        Producer<String, String> producer = new KafkaProducer<>(props);
        // 异步发送消息
        for (int i = 0; i < 10; i++) {
            ProducerRecord<String, String> producerRecord = new ProducerRecord<>(topicName, "异步" + i, "world" + i);
            producer.send(producerRecord, new Callback() {
                @Override
                public void onCompletion(RecordMetadata metadata, Exception e) {
                    if (e != null) {
                        System.out.println("进行异常处理");
                    } else {
                        System.out.printf("topic=%s, partition=%d, offset=%s \n",
                                metadata.topic(), metadata.partition(), metadata.offset());
                    }
                }
            });
        }
        // 关闭生产者
        producer.close();
    }
}

(4)自定义分区器

    Kafka 有默认的分区机制:

  • 如果键值为 null,则是用轮询(Round Robin)算法将消息均衡地分布到各个分区上;
  • 如果键值不为 null,那么 Kafka 会使用内置的散列算法对键进行散列,然后分不到各个分区上。
  • 但在某些情况下,我们需要按照自己的需求进行分区,这时候可以采用自定义分区器实现。
  • 创建自定义分区器
package com.lagou.kafka.demo;

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.PartitionInfo;
import org.apache.kafka.common.utils.Utils;

import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;

public class CustomPartitioner implements Partitioner {

    private final ConcurrentMap<String, AtomicInteger> topicCounterMap = new ConcurrentHashMap();

    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        List partitions = cluster.partitionsForTopic(topic);
        int numPartitions = partitions.size();
        if(keyBytes == null) {
            int nextValue = this.nextValue(topic);
            List availablePartitions = cluster.availablePartitionsForTopic(topic);
            if(availablePartitions.size() > 0) {
                int part = Utils.toPositive(nextValue) % availablePartitions.size();
                return ((PartitionInfo)availablePartitions.get(part)).partition();
            } else {
                return Utils.toPositive(nextValue) % numPartitions;
            }
        } else {
            return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
        }
    }

    @Override
    public void close() {
        System.out.println("分区器关闭");
    }

    @Override
    public void configure(Map<String, ?> map) {
    }
    private int nextValue(String topic) {
        AtomicInteger counter = (AtomicInteger)this.topicCounterMap.get(topic);
        if(null == counter) {
            counter = new AtomicInteger((new Random()).nextInt());
            AtomicInteger currentCounter = (AtomicInteger)this.topicCounterMap.putIfAbsent(topic, counter);
            if(currentCounter != null) {
                counter = currentCounter;
            }
        }

        return counter.getAndIncrement();
    }
}
  • 在创建生产者时指定分区器
// 指定自定义分区器    props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.lagou.kafka.demo.CustomPartitioner");

5.客户端应用-消费者

(1)消费者和消费者组

    Kafka为这两种模型提供了单一的消费者抽象模型: 消费者组 (consumer group)。 消费者用一个消费者组名标记自己。 一个发布在Topic上消息被分发给此消费者组中的一个消费者。 假如所有的消费者都在一个组中,那么这就变成了队列模型。 假如所有的消费者都在不同的组中,那么就完全变成了发布-订阅模型。 一个消费者组中消费者订阅同一个Topic,每个消费者接受Topic的一部分分区的消息,从而实现对消费者的横向扩展,对消息进行分流。
    注意:当单个消费者无法跟上数据生成的速度,就可以增加更多的消费者分担负载,每个消费者只处理部分partition的消息,从而实现单个应用程序的横向伸缩。但是不要让消费者的数量多于partition的数量,此时多余的消费者会空闲。此外,Kafka还允许多个应用程序从同一个Topic读取所有的消息,此时只要保证每个应用程序有自己的消费者组即可。
    消费者组的概念就是:当有多个应用程序都需要从Kafka获取消息时,让每个app对应一个消费者组,从而使每个应用程序都能获取一个或多个Topic的全部消息;在每个消费者组中,往消费者组中添加消费者来伸缩读取能力和处理能力,消费者组中的每个消费者只处理每个Topic的一部分的消息,每个消费者对应一个线程。

image

    在同一个群组中,无法让一个线程运行多个消费者,也无法让多线线程安全地共享一个消费者。按照规则,一个消费者使用一个线程,如果要在同一个消费者组中运行多个消费者,需要让每个消费者运行在自己的线程中。最好把消费者的逻辑封装在自己的对象中,然后使用java的ExecutorService启动多个线程,使每个消费者运行在自己的线程上,可参考https://www.confluent.io/blog

(2)创建Kafka消费者、订阅主题、轮询

package com.lagou.demo.consumer;

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;

import java.util.Collections;
import java.util.Properties;

public class SimpleConsumer {
    public static void main(String[] args) {
		// 配置消费者的参数
        Properties properties = new Properties();
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"linux121:9092");
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"consumer_kafka");
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class);
        // 创建消费者
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
        // 订阅主题
        consumer.subscribe(Collections.singleton("Hello-Kafka"));
        // 轮询
        try {
            while (true) {
                ConsumerRecords<String, String> records = consumer.poll(1000);
                for (ConsumerRecord<String, String> record : records) {
                    System.out.println(record.topic() + record.partition() + record.offset() + record.key() + record.value());
                    //统计各个地区的客户数量,即模拟对消息的处理
//                    int updatedCount = 1;
//                    updatedCount += custCountryMap.getOrDefault(record.value(), 0) + 1;
//                    custCountryMap.put(record.value(), updatedCount);
//
//                    //真实场景中,结果一般会被保存到数据存储系统中
//                    JSONObject json = new JSONObject(custCountryMap);
//                    System.out.println(json.toString(4));
                }
            }
        } finally {
            // 关闭消费者
            consumer.close();
        }
    }
}

五. Kafka 高级特性

1. 生产者

(1)数据生产流程

image

  • Producer创建时,会创建一个Sender线程并设置为守护线程
  • 生产消息时,内部其实是异步流程;生产的消息先经过拦截器->序列化器->分区器,然后将消息缓存在缓冲区(该缓冲区也是在Producer创建时创建)
  • 批次发送的条件为:缓冲区数据大小达到batch.size或者linger.ms达到上限
  • 批次发送后,发往指定分区,然后落盘到broker;如果生产者配置了retrires参数大于0并且失败原因允许重试,那么客户端内部会对该消息进行重试
  • 落盘到broker成功,返回生产元数据给生产者
    元数据返回有两种方式:一种是通过阻塞直接返回,另一种是通过回调返回

(2)自定义序列化器

  • 创建自定义序列化器
package com.lagou.demo.serializer;

import org.apache.kafka.common.serialization.Serializer;
import org.codehaus.jackson.map.ObjectMapper;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;

public class UserSerializer implements Serializer {
    private ObjectMapper objectMapper;
    @Override
    public void configure(Map configs, boolean isKey) {
        objectMapper = new ObjectMapper();
    }

    @Override
    public byte[] serialize(String s, Object o) {
        byte[] ret = null;
        try {
            ret = objectMapper.writeValueAsString(o).getBytes(StandardCharsets.UTF_8);
        } catch (IOException e) {
            System.out.println("序列化失败");
            e.printStackTrace();
        }
        return ret;
    }

    @Override
    public void close() {

    }
}
  • 构建生产者属性时指定使用自定义序列化器
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "com.lagou.demo.serializer.UserSerializer");

(3)分区器

  • 分区的原因
        方便在集群中扩展,每个Partition可以通过调整以适应它所在的机器,而一个topic又可以有多个Partition组成,因此整个集群就可以适应任意大小的数据了
        可以提高并发,因为可以以Partition为单位读写了
  • 分区的原则
    • 指定了 partition,则直接使用
    • 未指定 partition 但指定 key,通过对 key 的 value 进行 hash 出一个 partition
    • partition 和 key 都未指定,使用轮询选出一个 partition

(4)拦截器

  • 简介
        kafka生产者拦截器主要用于在消息发送前对消息内容进行定制化修改,以便满足相应的业务需求,也可用于在消息发送后获取消息的发送状态,所在分区和偏移量等信息。同时,用户可以在生产者中指定多个拦截器形成一个拦截器链,生产者会根据指定顺序先后调用。

  • kafka 生产者拦截器的访问流程
    image

  • 生产者拦截器的实现

    • 自定义拦截器类,实现 org.apache.kafka.clients.producer.ProducerInterceptor 接口
      • 时间拦截器
		package com.lagou.demo.interceptors;

import com.lagou.demo.entity.User;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;

import java.util.Map;

/**
 * 时间拦截器,发送消息之前,在消息内容前面加入时间戳
 */
public class TimeInterceptor implements ProducerInterceptor<String, User> {
    /**
     * 该方法在消息发送之前调用
     * 对原消息记录进行修改,在消息内容最前边添加时间戳
     * @param producerRecord    生产者发送的消息记录,并自动传入
     * @return  修改后的消息记录
     */
    @Override
    public ProducerRecord<String, User> onSend(ProducerRecord<String, User> producerRecord) {
        System.out.println("TimeInterceptor--------------->onSend方法被调用");
        // 创建一个新的 producerRecord,把时间戳写到消息体的最前面
        User user = producerRecord.value();
        user.setFirstName(System.currentTimeMillis() + user.getFirstName());
        ProducerRecord record = new ProducerRecord<String,User>(producerRecord.topic(), producerRecord.key(),producerRecord.value() );
        return record;
    }

    /**
     * 该方法在消息发送完毕后调用
     * 当发送到服务器的记录已被确认,或者记录发送失败时,将调用此方法
     * @param recordMetadata
     * @param e
     */
    @Override
    public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
        System.out.println("TimeInterceptor-------onAcknowledgement方法被调用");
    }

    @Override
    public void close() {

    }

    /**
     * 获取生产者配置信息
     * @param map
     */
    @Override
    public void configure(Map<String, ?> map) {
        System.out.println(map.get(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG));
    }
}
      • 状态拦截器
		package com.lagou.demo.interceptors;

import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;

import java.util.Map;

public class CounterInterceptor implements ProducerInterceptor<String,String> {
    // 发送成功的消息数量
    private int successCounter = 0;
    // 发送失败的消息数量
    private int errCounter = 0;

    /**
     * 该方法在消息发送之前调用
     * 对原消息记录进行修改,在消息内容最前边添加时间戳
     * @param producerRecord    生产者发送的消息记录,并自动传入
     * @return  修改后的消息记录
     */
    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> producerRecord) {
        System.out.println("CounterInterceptor------------------->onSend方法被调用了");
        return producerRecord;
    }

    /**
     * 该方法在消息发送完毕后调用
     * 当发送到服务器的记录已被确认,或者记录发送失败时,将调用此方法
     * @param recordMetadata
     * @param exception
     */
    @Override
    public void onAcknowledgement(RecordMetadata recordMetadata, Exception exception) {
        System.out.println("CounterInterceptor-------onAcknowledgement方法被调用");
        // 统计成功和失败的次数
        if (exception == null) {
            successCounter ++;
        }else{
            errCounter ++;
        }
    }

    /**
     * 当生产者关闭时调用该方法,可以在此将结果进行持久化保存
     */
    @Override
    public void close() {
        System.out.println("CounterInterceptor------->close方法被调用");
        // 打印统计结果
        System.out.println("发送成功的消息数量: " + successCounter);
        System.out.println("发送失败的消息数量: " + errCounter);
    }

    /**
     * 获取生产者消息的配置信息
     * @param map
     */
    @Override
    public void configure(Map<String, ?> map) {
        System.out.println(map.get(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG));
    }
}
    • 在定义生产者属性的时候指定拦截器
	List<String> interceptors = new ArrayList<>();
        interceptors.add("com.lagou.demo.interceptors.CounterInterceptor");
        interceptors.add("com.lagou.demo.interceptors.TimeInterceptor");
        properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,interceptors);
    • 启动消费者
kafka-console-consumer.sh --bootstrap-server linux121:9092 --topic Hello-Kafka-Interceptor
    • 运行生产者程序,观察 kafka 消费端和 IDEA 控制台输出信息

image
image

2. 消费者

(1)订阅主题

(2)反序列化

(3)消费者位移

(4)心跳机制

(5)再均衡

(6)拦截器

3. 主题管理

(1)副本分割机制

(2)分区重新分配

4. 物理存储

(1)日志索引文件

(2)日志清理机制

  • 日志压缩
  • 日志删除

5. 磁盘存储

(1)零拷贝

(2)页缓存

(3)顺序写入

六. Kafka 集群配置

1.集群配置规划

linux121 linux122 linux123
zookeeper1 zookeeper2 zookeeper3
broker0 broker1 broker2

2. 集群配置

(1)配置 java 环境变量

(2)配置 zookeeper 集群环境

(3)配置 kafka 集群环境

  • 配置 kafka 环境变量
# 修改 环境变量的配置文件
vim /etc/profile
# 在文件的末端添加如下内容
# KAFKA_HOME
export KAFKA_HOME=/opt/lagou/servers/kafka_2.12-1.0.2
export PATH=$PATH:${KAFKA_HOME}/bin
  • 修改 kafka 配置文件
# linux121
# 进入到 kafka 的安装目录的配置文件目录下
cd /opt/lagou/servers/kafka_2.12-1.0.2/config
# 编辑 server.properties
vim server.properties
broker.id=0
listeners=PLAINTEXT://:9092
advertised.listeners=PLAINTEXT://linux121:9092
log.dirs=/var/lagou/kafka/kafka-cluster-logs
zookeeper.connect=linux121:2181,linux122:2181,linux123:2181/clusterKafka
- 发送消息
# linux122
# 进入到 kafka 的安装目录的配置文件目录下
cd /opt/lagou/servers/kafka_2.12-1.0.2/config
# 编辑 server.properties
vim server.properties
broker.id=1
listeners=PLAINTEXT://:9092
advertised.listeners=PLAINTEXT://linux122:9092
log.dirs=/var/lagou/kafka/kafka-cluster-logs
zookeeper.connect=linux121:2181,linux122:2181,linux123:2181/clusterKafka

# linux123
# 进入到 kafka 的安装目录的配置文件目录下
cd /opt/lagou/servers/kafka_2.12-1.0.2/config
# 编辑 server.properties
vim server.properties
broker.id=2
listeners=PLAINTEXT://:9092
advertised.listeners=PLAINTEXT://linux123:9092
log.dirs=/var/lagou/kafka/kafka-cluster-logs
zookeeper.connect=linux121:2181,linux122:2181,linux123:2181/clusterKafka

# 分别启动三台服务器上的 kafka 服务
kafka-server-start.sh /opt/lagou/servers/kafka_2.12-1.0.2/config/server.properties

如何确认三个 KAFKA 服务在一个集群中?

image

posted @ 2021-07-15 15:36  快乐生活浪漫人生  阅读(210)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3