clllll  

很多数据库和系统可以用来存储数据,缺一个可以帮助处理持续数据流的组件。
不只是一个能够存储数据的系统。比如 关系型数据库、键值存储引擎、搜索引擎、缓存系统。
把数据看成是持续变化和不断增长的流。
Kafka是一个流平台:可以发布和订阅数据流。并可以保存起来、进行处理。
Kafka有点像消息系统,允许发布和订阅消息流。类似于ActiveMQ、RabbitMQ。但有很多重要的不同点。
Kafka是个分布式系统,以集群的方式运行,可以自由伸缩。
Kafka集群并不是一组独立运行的broker,而是一个可以灵活伸缩的中心平台。
kafka可以按照你的要求存储数据,保存多久都可以。作为数据连接层,Kafka提供了数据传递保证---可复制、持久化。
流式处理将数据处理的层次提升到了新高度。
消息系统只会传递消息,而Kafka的流式处理能力让你只用很少的代码就能够动态的处理派生流和数据集。

另一个角度看Kafka,看成实时版的Hadoop.
Hadoop可以存储和定期处理大量的数据文件。而Kafka可以存储和持续处理大型的数据流。
从技术的角度来看,有着惊人的相似之处。它们之间最大不同体现在持续的低延迟处理和批处理之间的差异上。
Hadoop和大数据的主要应用在数据分析上,而Kafka因其低延迟的特点更适合用在核心的业务上
业务事件时刻在发生,Kafka能及时对这些事件做出响应。基于Kafka构建的服务直接为业务运营提供支撑,提升用户体验。

Kafka与ETL工具或者其他数据集成工具比较,Kafka和这些工具都擅长移动数据、
Kafka并非只是把数据从一个系统拆解出来再塞进另一个系统。是一个面向实时数据流的平台。
不仅可以将现有的应用程序和数据系统连接起来。还能用于加强这些触发相同数据流的应用。
以数据流为中心的架构是非常重要的。

初识Kafka

发布与订阅消息系统

数据的发送者不会直接把消息发送给接收者,这是发布与订阅消息系统的一个特点。
发布者以某种方式对消息进行分类。接收者订阅它们,以便接收特定类型的消息。
发布与订阅系统一般会有一个broker.也就是发布消息的中心点。

如何开始

这样子,还是有太多重复的地方。就需要Kafka了。

Kafka登场

一般被称为分布式提交日志 或者 分布式流平台。
文件系统或数据库提交日志用来提供所有事务的持久记录。通过重放这些日志可以重建系统的状态。
同样地,Kafka的数据是按照一定顺序持久化保存的,可以按需读取。
此外,Kafka的数据分布在整个系统里,具备数据故障保护和性能伸缩能力。

消息和批次

Kafka的数据单元被称为消息。
可以把消息看成是数据库里的一个数据行或一条记录
消息由字节数组组成,所以对于Kafka来说,消息里的数据没有特别的格式和含义。
消息可以有一个可选的元数据,也就是键。键也是一个字节数组,与消息一样,对于Kafka来说也没有特殊的含义。
当消息以一种可控的方式写入不同的分区时,会用到键。最简单的例子就是为键生成一个一致性散列值。然后使用散列值对主题分区数进行取模,为消息选择分区。
这样可以保证具有相同键的消息总是被写到相同的分区上。

为了提高效率,消息被分批次写入Kafka。批次就是一组消息。这些消息属于同一个主题和分区。
如果每一个消息都单独穿行与网络,会导致大量的网络开销。把 消息分批次传输可以减少网络开销。
不过,这要在时间延迟和吞吐量之间做出权衡: 批次月底,单位时间内处理的消息就越多,单个消息的传输时间就越长。
批次数据会被压缩。这样可以提升数据的传输和存储能力。但要做更多的计算处理。

模式

对于Kafka来说,消息不过是晦涩难懂的字节数组,所以有人建议用一些额外的结构来定义消息内容,让它们更易于理解。
根据应用程序的需求,消息模式 schema有许多可用的选项。
像json和xml这些简单的系统,不仅易用,而且读性好。不过,它们缺乏强类型处理能力,不同版本之间的兼容性不好。
Kafka的许多开发者喜欢使用Apache Avro. 是Hadoop开发的一款序列化框架。
Avro提供了一种紧凑的序列化模式,模式和消息体是分开的,当模式发生变化时,不需要重新生成代码。
它还支持强类型和模式进化,其版本既向前兼容,也向后兼容。

数据格式的一致性对于Kafka来说很重要。消除了消息读写操作之间的耦合性。
如果读写操作紧密地耦合在一起,消息订阅者需要升级应用程序才能同时处理新旧俩种数据格式。
在消息订阅者升级了之后,消息发布者才能跟着升级。以便使用新的数据格式。
新的应用程序如果需要使用数据,就咬与消息发布者发生耦合,导致开发者需要做很多繁杂的工作。
定义良好的模式,并把它们存放在公共仓库,可以方便我们理解Kafka的消息结构。

主题和分区

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

我们通常会使用流这个词来描述这类系统的数据,很多时候,人们把一个主题的数据看成一个流,不管它有多少个分区,流是一组从生产者移动到消费者的数据。
当我们讨论流式处理时,一般都是这样描述消息的。

生产者和消费者

Kafka的客户端就是Kafka系统的用户,它们被分为俩种基本类型:生产者和消费者。
除此之外,还有其他高级客户端API---用于数据集成的Kafka Connect API和用于流式处理的Kafka Streams。 这些高级客户端API使用生产者和消费者作为内部组件。提供了高级的功能。

生产者创建消息。一般情况下,一个消息会被发布到一个特定的主题上。
生产者在默认情况下把消息均衡地发布到主题的所有分区上。而并不关心特定消息会被写到哪个分区。
不过,在某些情况下,生成者会直接写到指定的分区。这通常是通过消息键和分区器来实现的。
分区器为键生成一个散列值,并将其映射到指定的分区上。这样可以保证包含同一个键的消息会被写到同一个分区上。
生产者也可以使用自定义的分区器。根据不同的业务规则将消息映射到分区。

消费者读取消息。消费者定于一个或多个主题,并按照消息生成的顺序读取它们。消费者通过检查消息的偏移量来区分已经读取过的消息。
偏移量是另一种元数据,它是一个不断递增的整数值,在创建消息时,Kafka会把他添加到消息里。
在给定的分区里,每个消息的偏移量都是唯一的。消费者会把每个分区最后读取的消息偏移量保存在Zookeeper或Kafka上。如果消费者关闭或重启,它的读取状态不会丢失。

消费者是消费者群组的一部分,也就是说,会有一个或多个消费者共同读取一个主题,群组保证每个分区只能被一个消费者使用。
消费者与分区之间的映射通常被称为消费者对分区的 所有权关系。
通过这种方式,消费者可以消费包含大量消息的主题,而且,如果一个消费者失效,群组里的其他消费者可以接管失效消费者的工作。

broker和集群

一个独立的Kafka服务器被称为broker。broker接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存.
broker为消费者提供服务,对读取分区的请求做出响应,返回已经提交到磁盘上的消息。
根据特定的硬件及其性能特征,单个broker可以轻松处理数千个分区以及每秒百万级的消息量。

broker是集群的组成部分,每个集群都有一个broker同时充当了集群控制器的角色(自动从集群的活跃成员选举出来)。
控制器负责管理工作,包括将分区分配给broker和监控broker.
在集群中,一个分区从属于一个borker.该broker被称为分区的首领。
一个分区可以分配给多个broker.这个时候会发生分区复制。这种复制机制为分区提供了消息冗余,如果有一个broker失效。其他broker可以接管领导权。
不过相关的消费者和生产者都要重新连接到新的首领。

保留消息时Kafka的一个重要特性。Kafka broker默认的消息保留策略是这样的: 要么保留一段时间(比如7天), 要么保留到消息达到一定大小的字节数(比如1GB)
当消息数量达到这些上限是,旧消息就会过期并被删除,所以在任何时K,可用消息的总量都不会超过配置参数所指定的大小。
主题可以配置自己的保留策略,可以将消息保留到不再使用它们为止。例如,用于跟踪用户活动的数据可能需要保留几天,而应用程序的度量指标可能只需要保留几个小时。
可以通过配置把主题当做紧凑型日志,只有最后一个带有特定键的消息会被保留下来。

多集群

随着Kafka部署数量的增加,最好使用多个集群。

  • 数据类型分离
  • 安全需求隔离
  • 多数据中序
    如果使用多个数据中心,就需要它们之间复制消息,这样,在线应用程序才可以访问到多个站点的用户活动消息。
    例如,如果一个用户修改了他们的资料信息,不管从哪个数据中心都应该能看到这些改动。
    不过Kafka的消息复制机制只能在单个集群里进行,不能在多个集群之间进行。

Kafka提供了一个叫做MirroMaker的工具,可以用它来实现集群间的消息复制。
MirrorMaker的核心组件包含了一个生产者和一个消费者。俩者之间通过一个队列相连,
消费者从一个集群读取消息,生产者把消息发送到另一个集群上。

为什么选择Kafka.

多个生产者

Kafka可以无缝地支持多个生产者,不管客户端在使用单个主题还是多个主题。多以它很适合用来从多个前端系统收集数据,并以统一的格式对外提供数据。

多个消费者

除了支持多个生产者外,Kafka也支持多个消费者从一个单独的消息流上读取数据,而且消费者之间互不影响,这与其他队列系统不同。其他队列系统的消息一旦被一个客户端读取,其他客户端就无法再读取它。
另外,多个消费者可以组成一个群组,共享一个消息流,并保证整个群组对每个给定的消息只处理一次。

基于磁盘的数据存储

Kafka不仅支持多个消费者,还允许消费者非实时地读取消息。消息被提交到磁盘,根据设置的保留规则进行保存。
每个主题可以设置单独的保留规则,

伸缩性

为了能够轻松处理大量数据,Kafka从一开始就被设计成一个具有灵活伸缩性的系统。

高性能

通过横向扩展生产者、消费者和broker,Kafka可以轻松处理巨大的消息流。在处理大量数据的同时,还能保证亚秒级的消息延迟。

数据生态系统

使用场景

  • 1.活动跟踪
    Kafka最初的使用场景是跟踪用户的活动。网站用户与前端应用程序发生交互,前端应用程序生成用户活动相关的消息。
    这些消息可以是一些静态的信息,比如页面访问次数和点击量,也可以是一些复杂的操作。比如添加用户资料。
    这些消息被发布到一个或多个主题上,由后端应用程序负责读取。可以生成报告,为机器学习系统提供数据,更新搜索结果。
  • 2.传递消息
    应用程序向用户发送通知,就是通过消息来实现的。这些应用程序组件可以生成消息 ,而不需要关系消息的格式,也不需要关系消息时如何被发送的。一个公共应用程序会读取这些消息。对它们进行处理:
    1)格式化消息 装饰
    2)将多个消息放在同一个通知里发
    3)根据用户配置的首选项来发送数据
    使用公共组件的好处在于,不需要在多个应用程序上开发重复的功能。而且可以在公共组件上做一些有趣的转换。
  • 3.度量指标和日志记录
    Kafka也可以用于收集应用程序和系统度量指标以及日志。Kafka支持多个生产者的特性在这个时候就可以派上用场。
    应用程序定期把度量指标发布到Kafka主题上,监控系统或告警系统读取这些信息。Kafka也可以用在像Hadoop这样的离线系统上,进行较长时间片段的数据分析,比如年度增长走势预测。
    日志消息也可以发布到Kafka主题上。然后被路由到专门的日志搜索系统(Elasticsearch)或安全分析应用程序。
  • 4.提交日志
    Kafka的基本概念来源于提交日志。所以使用Kafka作为提交日志是件顺理成章的事。
    我们可以把数据库的更新发布到Kafka上。应用程序通过监控事件流来接收数据库的实时更新。
    这种变更日志流也可以用于把数据库的更新复制到远程系统上。或者合并多个应用程序的更新到一个单独的数据库视图上。
  • 5.流处理
    流处理时又一个能提供多种类型应用程序的领域。
    可以说,它们提供的功能与Hadoop里的map和reduce有点类似,只不过它们操作的是实时数据流,而Hadoop处理更长时间片段的数据,可能是几个小时或者几天,Hadoop会对这些数据进行批处理。
    通过使用流式处理框架,用户可以编写小型应用程序来操作Kafka消息,比如计算度量指标,为其他应用程序有效地处理消息分区,或者对来自多个数据源的消息进行转换。

安装Kafka

Kafka是java开发的应用程序。

安装java.

kaillinux自带java 11.

不行,得安装个java 8 .
配置PATH: /etc/profile

export JAVA_HOME=/usr/local/jdk8
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export PATH=$PATH:$JAVA_HOME/bin

查看java的路径
whereis java 1 ⨯
java: /usr/bin/java /usr/share/java /usr/share/man/man1/java.1.gz

安装Zookeeper

Kafka使用zookeeper保存集群的元数据信息和消费者信息。
Kafka发行版自带了Zookeeper.可以直接从脚本启动。
1.单机服务
安装目录为/usr/local/zookeeper,数据目录为zookeeper



启动zookeeper

测试安装是否正确

  1. Zookeeper群组 Ensemble

Zookeeper群组被称为群组。 Zookeeper使用的是一致性协议。所以建议每个群组里应该包含奇数个节点。因为只有当群组里的大多数节点处于可用状态,Zookeeper才能处理外部请求。
也就是说,如果你又一个包含3个节点的群组,那么它允许一个节点失效。如果群组包含5个节点,那么它允许2个节点失效。
群组节点个数的选择。
假设有一个包含5个节点的群组,如果要对群组做一些包含更换节点在内的配置更改,需要依次重启每一个节点。
如果你的群组无法容忍多个节点失效,那么在进行群组维护时就会存在风险。
不过,也不建议一个群组包含超过7个节点。因为Zookeeper使用了一致性协议,节点过多会降低整个群组的性能。

群组需要一些公共配置。并且每个服务器还要再数据目录中创建一个myid文件,用于指明自己的ID.
如果群组里服务器的机器名是zoo1.example.com zoo2.example.com zoo3.example.com.那么配置文件可能如下:

initLimt表示用于在从节点与主节点之间建立初始化连接的时间上限
syncLimit表示允许从节点与主节点处于不同步状态的时间上限。
这俩个值都是tickTime的倍数,所以initLimit = 20 * 2000 ms,也就是40s.
服务器地址遵循 server.X=hostname:peerPort:leaderPort 格式 , X是服务器ID,必须是一个整数,不过不一定要从0开始,也不要求是连续的。
hostname:服务器的机器名或IP地址
peerPort:用于节点键通信的TCP端口
leaderPort:用于首领选举的TCP端口

客户端只需要通过clientPort就能连接到群组,而群组节点间的通信则同时需要用到这三个端口: peerPort、leaderPort、clientPort

myid要与ID保持一致。

安装Kafka Broker

验证安装是否正确
报错:zookeeper is not a recognized option

安装对应的版本把
然后就可以了。

查看主题

往测试主题上发布消息

读取消息

broker配置

自带的配置样本可以用来安装单机服务,但并不能满足大多数安装场景的要求。
Kafka有多很多配置选项,涉及安装和调优的方方面面。
不过大多数调优选项可以使用默认配置,

常规配置

配置文件路径:安装目录/config/server.properties

############################# Server Basics #############################
# The id of the broker. This must be set to a unique integer for each broker.
broker.id=0

############################# Socket Server Settings #############################

listeners=PLAINTEXT://:9092

# The port the socket server listens on
#port=9092

# Hostname the broker will bind to. If not set, the server will bind to all interfaces
#host.name=localhost

# Hostname the broker will advertise to producers and consumers. If not set, it uses the
# value for "host.name" if configured.  Otherwise, it will use the value returned from
# java.net.InetAddress.getCanonicalHostName().
#advertised.host.name=<hostname routable by clients>

# The port to publish to ZooKeeper for clients to use. If this is not set,
# it will publish the same port that the broker binds to.
#advertised.port=<port accessible by clients>

# The number of threads handling network requests
num.network.threads=3

# The number of threads doing disk I/O
num.io.threads=8

# The send buffer (SO_SNDBUF) used by the socket server
socket.send.buffer.bytes=102400

# The receive buffer (SO_RCVBUF) used by the socket server
socket.receive.buffer.bytes=102400

# The maximum size of a request that the socket server will accept (protection against OOM)
socket.request.max.bytes=104857600


############################# Log Basics #############################

# A comma seperated list of directories under which to store log files
log.dirs=/tmp/kafka-logs

# The default number of log partitions per topic. More partitions allow greater
# parallelism for consumption, but this will also result in more files across
# the brokers.
num.partitions=1

# The number of threads per data directory to be used for log recovery at startup and flushing at shutdown.
# This value is recommended to be increased for installations with data dirs located in RAID array.
num.recovery.threads.per.data.dir=1

############################# Log Flush Policy #############################

# Messages are immediately written to the filesystem but by default we only fsync() to sync
# the OS cache lazily. The following configurations control the flush of data to disk.
# There are a few important trade-offs here:
#    1. Durability: Unflushed data may be lost if you are not using replication.
#    2. Latency: Very large flush intervals may lead to latency spikes when the flush does occur as there will be a lot of data to flush.
#    3. Throughput: The flush is generally the most expensive operation, and a small flush interval may lead to exceessive seeks.
# The settings below allow one to configure the flush policy to flush data after a period of time or
# every N messages (or both). This can be done globally and overridden on a per-topic basis.

# The number of messages to accept before forcing a flush of data to disk
#log.flush.interval.messages=10000

# The maximum amount of time a message can sit in a log before we force a flush
#log.flush.interval.ms=1000

############################# Log Retention Policy #############################

# The following configurations control the disposal of log segments. The policy can
# be set to delete segments after a period of time, or after a given size has accumulated.
# A segment will be deleted whenever *either* of these criteria are met. Deletion always happens
# from the end of the log.

# The minimum age of a log file to be eligible for deletion
log.retention.hours=168

# A size-based retention policy for logs. Segments are pruned from the log as long as the remaining
# segments don't drop below log.retention.bytes.
#log.retention.bytes=1073741824

# The maximum size of a log segment file. When this size is reached a new log segment will be created.
log.segment.bytes=1073741824

# The interval at which log segments are checked to see if they can be deleted according
# to the retention policies
log.retention.check.interval.ms=300000

############################# Zookeeper #############################

# Zookeeper connection string (see zookeeper docs for details).
# This is a comma separated host:port pairs, each corresponding to a zk
# server. e.g. "127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002".
# You can also append an optional chroot string to the urls to specify the
# root directory for all kafka znodes.
zookeeper.connect=localhost:2181

# Timeout in ms for connecting to zookeeper
zookeeper.connection.timeout.ms=6000

  1. broker.id
    每个broker.id都需要一个标识符,使用broker.id表示,默认值是0.可以被设置为其他任意整数。
    这个值在整个kafka集群里必须是唯一的。这个值可以任意选定。如果处于维护的需要。可以在服务器节点间交换使用这些ID.
    建议设置成与机器名具有相关性的整数。

  2. port
    如果使用配置样本来启动kafka,会监听9092端口。

  3. zookeeper.connect
    用于保存broker元数据的Zookeeper地址是通过zookeeper.connect来指定的。
    localhost:2181表示运行在本地的2181端口上。 hostname:port/path
    /path 是可选,作为Kafka集群的chroot环境,如果不指定,默认使用根路径。
    如果不存在,broker会在启动的时候创建它。
    4.log.dirs
    Kafka把所有消息都保存在磁盘上,存放这些日志片段的目录是通过log.dirs指定的。
    它是一组用逗号分割的本地文件系统路径。
    如果指定了多个路径,那么broker会根据最小使用原则,把同一个分区的日志片段保存到同一个路径下。

  4. num.recovery.threads.per.data.dir
    对于如下三种情况,Kafka会使用可配置的线程池来处理日志片段。

  • 服务器正常启动,用于打开每个分区的日志片段。
  • 服务器崩溃后重启,用于检查和戒短每个分区的日志片段
  • 服务器正常关闭,用于关闭日志片段
    默认情况下,每个日志目录只用一个线程。因为这些线程只是在服务器启动和关闭时会用到。所以完全可以设置大量的线程来达到并行操作的目的。
    特别是对于包含大量分区的服务器来说,一旦发生崩溃,在进行恢复时使用并行操作可能会省下数小时的时间。
    设置此参数时需要注意,所配置的数据对应的是log.dirs指定的单个日志目录。
    也就是说,如果num.recovery.threads.per.data.dir 设定为8 。并且log.dir指定了3个路径,那么总共需要24个线程。

6.auto.create.topics.enable
默认情况下,Kafka会在如下集中情形自动创建主题:

  • 当一个生产者开始往主题写入消息时
  • 当一个消费者开始从主题读取消息时
  • 当任意一个客户端向主题发送元数据请求时

主题的默认设置

  1. num.partitions
    num.partitions参数指定了新创建的主题将包含多少个分区。如果启用了主题自动创建功能(默认启动)。
    主题的分区的个数就是该参数指定的值,默认值为1。 我们可以增加主题分区的个数,不能减少分区的个数。
    Kafka集群通过分区对主题进行横向扩展,所以当有新的broker加入集群时,可以通过分区个数来实现集群的负载均衡。
    当然,并不是说,在存在多个主题的情况下,分布在多个broker上。为了能让分区分布到所有的 broker上。
    主题分区的个数必须大于broker的个数。不过,拥有大量消息的主题如果要进行负载分散,就需要大量的分区。

如何选定分区数量?
为主题选定分区数量并不是一件可有可无的事情,在进行数量选择时,需要考虑如下几个因素。

  • 主题需要达到多大的吞吐量?是每秒钟写入100KB还是1GB?
  • 从单个分区读取数据的最大吞吐量是多少?每个分区一般都会有一个消费者,如果你知道消费者将数据写入数据库的速度不会超过每秒50MB.那么你也应该知道,从一个分区读取数据的吞吐量不需要超过每秒50MB.
  • 可以通过类似的方法估算生产者向单个分区写入数据的吞吐量,不过生产者的速度一般比消费者快得多,所以最好为生产者多估算一些吞吐量
  • 每个broker包含的分区个数、可用的磁盘空间和网络带宽
  • 如果消息是按照不同的键来写入分区的,那么为已有的主题新增分区就会很困难
  • 单个broker对分区个数是有限制的,因为分区越多,占用的内存就越多,完成首领选举需要的时间也越长。
    综合考虑以上因素,你需要很多分区,但不能太多,
    如果你估算出主题的吞吐量和消费者吞吐量,可以用主题吞吐量除以消费者吞吐量算出分区的个数,也就是说,如果每秒钟要从主题上写入和读取1GB的数据,并且每个消费者每秒钟可以处理50MB的数据,
    那么至少需要20个分区。这样就可以让20个消费者同时读取这些分区,从而达到每秒钟1GB的吞吐量。

2.log.retention.ms
Kafka通常根据时间来决定数据可以被保留多久,默认使用log.retention.hours参数来配置时间,默认值为168小时。一个星期。
还有其他俩个参数log.retention。minutes和log.retention.ms.这三个参数的作用是一样的,都是决定消息多久以后被删除。
如果指定了不止一个参数,Kafka会优先使用具有最小值的那个参数。

  1. log.retention.bytes
    另一种方式是通过保留的消息字节数来判断消息是否过期,log.retention.bytes作用在每一个分区上,也就是说,如果有一个包含8个分区的主题。
    log.retention.bytes被设为1GB,那么这个主题最多可以保留8GB的数据,所以,当主题的分区个数增加时,整个主题可以保留的数据也随之增加。

  2. log.segment.bytes
    以上的设置都作用在日志片段上,而不是作用在单个消息上,当消息到达broker时,它们被追加到分区的当前日志片段上,当日志片段大小达到log.segment.bytes指定的上限(默认是1GB)
    当前日志片段就会被关闭,一个新的日志片段被打开,如果一个日志片段被关闭,就开始等待过期,这个参数的值越小,就会越频繁地关闭和分配新文件。从而降低磁盘写入的整体效率。

如果主题的消息量不大,那么如何调整这个参数的大小就变得尤为重要。

  1. log.segment.ms
    另一个可以控制日志片段关闭的时间的参数是log.segment.ms。指定了多长时间之后日志片段会被关闭。

6.message.max.bytes
broker通过设置message.max.bytes参数来限制单个消息的大小,默认值是1000000.也就是1MB.
如果生产者尝试发送的消息超过这个大小,不仅消息不会被接收,还会收到broker返回的错误消息。

在服务端和客户端之间协调消息大小的配置,
消费者客户端设置的fetch.message.max.bytes必须与服务端设置的消息大小协调,如果这个值比message.max.bytes小,那么消费者就无法读取比较大的消息。导致出现消费者被阻塞的情况。

硬件的选择

如果比较关注性能:需要考虑 磁盘吞吐量和容量、内存、网络、CPU.

磁盘吞吐量

生产者客户端的性能直接受到服务端磁盘吞吐量的影响。生产者生成的消息必须被提交到服务器保存,大多数客户端在发送消息后会一直等待,直到至少有一个服务器确认消息已经成功提交为止。
也就是说,磁盘写入速度越快,生成消息的延迟就越低。

机械硬盘 HDD 固态硬盘SSD .

磁盘容量

需要多大的磁盘容量取决于需要保留的消息数量。
如果服务器每天会受到1TB的消息,并且保留7天,那么就需要7TB的存储空间。

内存

磁盘性能影响生产者, 内存影响消费者。
消费者一般从分区尾部读取消息,如果有生产者存在,就紧跟在生产者后面,在这种情况下,消费者读取的消息会直接存放在系统的页面缓存里。

运行Kafka的JVM不需要太大的内存,剩余的系统内存可以用作页面缓存。或者用来缓存正在使用的日志片段。这也就是为什么不建议把Kafka同其他重要的应用程序部署在一起的原因。它们需要共享页面缓存,最终会降低Kafka消费者的性能。

网络

网络吞吐量决定了Kafka能处理的最大数据流量,网络和磁盘存储是制约Kafka扩展规模的主要因素。
Kafka支持多个消费者,造成流入和流出的网络流量不平衡。从而让情况变得更加复杂。
对于给定的主题,一个生产者可能每秒写入1MB数据,但可能同时有多个消费者瓜分网络流量,

CPU

与磁盘和内存相比,Kafka对计算处理能力的要求相对较低,不过也会影响整体的性能。
客户端为了优化网络和磁盘空间,会对消息进行压缩,服务器需要对消息进行批量解压,设置偏移量,然后重新进行批量压缩。再保存到磁盘上。

云端的Kafka

Kafka一般被安装在云端。

Kafka集群

使用集群最大的好处是可以跨服务器进行负载均衡,再就是可以使用复制功能来避免因单点故障造成的数据丢失。

需要多少个broker

一个Kafka集群需要多少个broker取决于以下几个因素。
首先需要多少磁盘空间来保留数据,以及单个broker有多少空间可用。
如果整个集群需要保留10TB的数据,每个broker可以存储2TB。那么至少需要5个broker.
如果启用了数据复制,那么至少还需要一倍的空间。不过要取决于配置的复制系数是多少。

第二个要考虑的因素是集群处理请求的能力,这通常与网络接口处理客户端流量的能力有关。如果单个broker的网络接口在高峰时段可以达到80%的使用量。并且有俩个消费者,那么消费者就无法保持峰值,除非有俩个broker.

broker配置

要把一个broker加入到集群里,只需要修改俩个参数,首先,所有broker都必须配置相同的zookeeper.connect.该参数指定了用于保存元数据的Zookeeper群组和路径。
其次,每个broker.id参数设置唯一的值。

操作系统调优

大部分Linux发行版默认的内核调优参数配置已经能够满足大多数应用程序的运行需求。不过还是可以通过调整一些参数来经一步提升Kafka性能。
主要与虚拟内存、网络子系统、用来存储日志片段的磁盘挂载点有关。
这些参数一般配置在/etc/sysctl.conf文件里。

  1. 虚拟内存
    一般来说,Linux的虚拟内存会根据系统的工作负荷进行自动调整,我们可以对交换分区的处理方式和内存脏页进行调整。

对于大多数依赖吞吐量的应用程序来说,要尽量避免内存交换。内存页和磁盘之间的交换对Kafka各方面性能都有重大影响。
Kafka大量地使用系统页面缓存,如果虚拟内存被交换到磁盘,说明已经没有多余内存可以分配给页面缓存了。

一种避免内存交换的方法是不设置任何交换分区。 内存交换不是必需的。不过它确实能够在系统发生灾难性错误时提供一些帮助。
进行内存交换可以防止操作系统由于内存不足而突然终止进程。
基于上述原因,建议把vm.swappiness参数的值设置的小一点。比如1

脏页会被刷新到磁盘上,调整内核对脏页的处理方式可以让我们从中获益。
Kafka依赖IO性能为生产者提供快速响应。
这就是为什么日志片段一般要保存在快速磁盘上。
这样依赖,在后台刷新进程将脏页写入磁盘之前,可以减少脏页的数量,这个可以铜鼓瓯江vm.dirty_background_ratio设定为小于10的值来实现。

vm.dirty_ratio参数可以增加被内核进程刷新到磁盘之前的脏页数量。

在Kafka运行期间检查脏页的数量。可以在/proc/vmstat文件里查看当前脏页数量

  1. 磁盘
    除了选择合适的磁盘硬件和使用RAID外,文件系统是影响性能的另一个重要因素。
    不管使用哪一种文件系统来存储日志片段,最好要对挂载点的noatime参数进行合理的设置。
    文件元数据包含三个时间戳:创建时间ctime、最后修改时间mtime、最后访问时间atime.
    默认情况下,每次文件被读取后都会更新atime。这会导致大量的磁盘写操作,而且atime属性的用处不大,Kafka用不到该属性,所以完全可以把它禁用掉。
    为挂载点设置noatime参数可以防止更新atime.但不会影响ctime\mtime.

  2. 网络
    默认情况下,系统内核没有针对快速的大流量网络传输进行优化,所以对于应用程序来说,一般需要对linux网络栈进行调优。

首先可以对分配给socket读写缓冲区的内存大小作出调整。这样可以显著提升网络点的传输性能。
socket读写缓冲区对应的参数分别是net.core.wmem_default和net.core.rmem_default。 合理的值是131072 , 128KB。最大值是2097152,2MB.

除了设置socket外,还需要设置TCP socket的读写缓冲区,分别是net.ipv4.tcp_wmem和net.ipv4.tcp_rmem.
这些参数的值由三个整数组成,使用空格分割,分别表示最小值、默认值、最大值。

生产环境的注意事项

垃圾回收器选项

为应用程序调整Java垃圾回收参数就像是一门一束。我们需要直到应用程序时如何使用内存的,还需要大量的观察和试错。

数据中心布局

共享Zookeeper

Kafka使用Zookeeper来保存broker、主题和分区的元数据信息。
对于一个包含多个节点的Zookeeper群组来说,Kafka集群的这些流量并不算多,那些写操作只是用于构造消费者群组或者集群本身。
实际上,在很多部署环境里,会让多个Kafka集群共享一个Zookeeper群组。

Kafka生产者---向Kafka写入数据

生产者概览

从创建一个ProducerRecord对象开始,
ProducerRecord对象需要包含目标主题和要发送的内容。可以指定键或分区。
在发送ProducerRecord对象,生产者要先把键和值对象序列化成字节数组

接下来,数据被传给分区器,如果ProducerRecord对象里指定了分区,那么分区器就不会做任何事情,直接把指定的分区返回。
如果没有指定分区,就根据ProducerRecord对象的键来选择一个分区。选好分区以后,生产者就知道该往哪个主题和分区发送这条记录了。
这条记录被添加到一个记录批次里,这个批次里的所有消息会被发送到相同的主题和分区上。
有一个独立的线程负责把这些记录批次发送到相应的broker上。

服务器收到这些消息时会返回一个响应,如果消息成功写入Kafka,就返回一个RecordMetaData对象,包含了主题和分区信息,以及记录在分区里的偏移量。
如果写入失败,就会返回一个错误。生产者在收到错误之后会尝试重新发送消息。几次之后如果还是失败,就返回错误。

创建Kafka生产者

Kafka生产者有3个必选的属性。

  • bootstrap.servers
    该属性指定broker的地址清单。地址的格式为host:port.清单里不需要包含所有的broker地址,生产者会从给定的broker里查找其他broker信息。
    不过建议提供俩个broker。
  • key.serializer
    broker希望接收到的消息的键和值都是字节数组,生产者接口允许使用参数化类型,因此可以把Java对象作为键和值发送给broker。
    生产者需要知道如何把这些Java对象转换成字节数组,key.serializer必须被设置为一个实现了org.apache.kafka.common.serialization.Serializer接口的类,
    生产者会使用这个类把键对象序列化成字节数组,
    Kafka客户端默认提供了ByteArraySerializer、StringSerializer和IntergerSerializer.
    因此,如果只使用常见的几种java对象类型,没必要实现自己的序列化器。
    key.serializer是必须设置的,就算不设置键。
  • value.serializer
    与key.serializer一样。value.serialiezer指定的类会将值序列化。如果键和值都是字符串,可以使用与key.serializer一样的序列化器。
    如果键是整数类型而值是字符串,那么需要不同的序列化器。
private Properties kafkaProps = new Properties();
kafkaProps.put("bootstrap.servers", "broker1:9092,broker2:9092");
kafkaProps.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
kafkaProps.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
producer = new KafkaProducer<String, String>(kafkaProps);

1.新建一个Properties对象
2.使用内置的StringSerializer.
3.为键和值设置恰当的类型。然后传入Properties对象。

实例化生产者对象之后,就可以发送消息了。
发送消息主要有三种方式。

  1. 发送并忘记 fire-and-forget
    把消息发送给服务器,并不关心它是否正常到达,
  2. 同步发送
    使用send()方法发送消息。返回一个Future对象,调用get()方法进行等待,就可以知道消息是否发送成功
  3. 异步发送
    调用send()方法,并指定一个回调函数,服务器在返回响应时调用该函数。

生产者可以使用多线程来发送消息。
开始的时候可以使用单个消费者和单个线程,
如果需要更高的吞吐量,可以在生产者数量不变的前提下增加线程数量
还可以增加生产者数量

发送消息到Kafka

ProducerRecord<String,String> record = new ProducerRecord<>("CustomerCountry", "precision Products", "France");
try{
	producer.send(record);
} catch(Exception e) {
    e.printStackTrace();
}
  1. 生产者的send()方法将ProducerRecord对象作为参数。需要先创建一个ProducerRecord对象。有多个构造函数。
  2. 使用生产者的send()方法发送ProducerRecord对象。消息先是被放进缓冲区,然后使用单独的线程发送到服务器。
    send()方法会返回一个包含RecordMetadata的Future对象。
  3. 我们可以忽略发送消息时可能发生的错误或在服务器端可能发生的错误。但在消息发送之前,生产者还可能有其他异常。这种异常可能是SerializationException 序列化消息失败、BufferExhaustedException 、TimeoutException 缓冲区已满
    、 InterruptException 发送线程被终端

同步发送消息

producer.send(record).get();

send()方法返回一个Future对象,然后调用get()方法等待Kafka的响应。
如果服务器返回错误,get()方法会抛出异常。如果没有发生错误,我们会得到一个RecordMetadata对象。可以用它获取消息的偏移量。
如果在发送数据之前或者在发送过程中发生了任何错误,比如broker返回来一个不允许重发消息的异常或者已经超过了重发的次数。那么就会抛出异常

KafkaProducer一般会发生俩类错误,其中一类是可重试错误,这类错误可以通过重发消息来解决。
比如:对于连接错误,可以通过再次建立连接来解决, 无主 no leader错误则可通过重新为分区选举首领来解决。
kafka可以被配置为自动重试,如果在多次重试后仍无法解决问题,应用程序会收到一个重试异常。
另一类错误无法通过重试解决,比如消息太大,对于这类错误,KafkaProducer不会进行任何重试,直接抛出异常。

异步发送消息

假设消息在应用程序和Kafka集群之间一个来回需要10ms.如果在发送每个消息之后都等待回应。那么发送100个消息需要1秒。
但如果只发送消息不等待响应,那么就会少很多。
不过在遇到消息发送失败时,我们需要抛出异常、记录错误日志。

private class DemoProducerCallback implements Callback {

@Override
public vodi onCompletion(RecordMetadata recordMetadata, Exception e) {
	if (e != null){
		e.printStackTrace();
	}
}

producer.send(record, newDemoProducerCallback());

为了使用回调,需要一个实现Callback接口的类,这个接口只有一个onCompletion方法。
如果Kafka返回一个错误,onCompletion方法会抛出一个非空 non null异常。

生产者配置

生产者还有很多可配置的参数,大部分是合理的默认值,不需要修改。不过有介个参数在内存使用、性能和可靠性方面对生产者影响比较大。
1.acks
acks参数指定了必须要有多少个分区副本收到消息。生产者才会认为消息写入是成功的。
如果acks=0.生产者在成功写入消息之前不会等待任何来自服务器的响应。
如果acks=1,只要集群的首领节点收到消息,生产者就会收到一个来自服务器的成功响应。如果消息无法到达首领节点,生产者会收到一个错误响应。为了避免数据丢失,生产者会重复消息。
不过一个没有收到消息的节点成为新首领,消息还会丢失。
如果acks=all。只有当所有同步副本全部收到消息时,生产者才会收到一个来自服务器的成功响应。这种模式是最安全的。

  1. buffer.memory
    设置生产者内存缓冲区的大小。生产者用它缓冲要发送到服务器的消息。
    如果应用程序发送消息的速度超过发送到服务器的速度,会导致生产者空间不足,这个时候,send()方法要么阻塞,要么抛出异常,
  2. compression.type
    默认情况下 ,消息发送时不会被压缩,该参数可以设置为snappy、gzip或lz4.指定了消息被发送给broker之前使用哪一种压缩算法进行压缩。
  3. retries
    生产者从服务器收到的错误有可能是临时性的错误,这种情况下,retries参数的值决定了生产者可以重复消息的次数,如果达到这个次数,生产者会放弃重试并返回错误,
    默认情况下,生产者会在每次重试之间等待100ms. 不过可以通过retry.backoff.ms参数来改变这个时间间隔。

5.batch.size
当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里,该参数指定了一个批次可以使用的内存大小。安装字节数计算,当批次被填满,批次里的消息会被发送出去。
不过生产者不一定会等到批次被填满才发送。半满的批次,甚至只包含一个消息的批次也有可能发送。

6.linger.ms
指定了生产者发送批次之前等待更多消息加入批次的时间。KafkaProducer会在批次填满或linger.ms达到上限时把批次发送出去。默认情况下,只要有可用的线程,就算批次里只有一个消息,生产者也会把消息发送出去。

  1. client.id

可以是任意的字符串,服务器会用它来识别消息的来源,

  1. max.in.flight.requests.per.connection
    指定了生产者在收到服务器响应之前可以发送多少个消息。值越高,就会占用越多的内存。也会提升吞吐量,设为1可以保证消息是按照发送的顺序写入服务器的,即使发生了重试。

  2. timeout.ms、 request.timeout.ms 和metadata.fetch.timeout.ms
    request.time.ms 指定了生产者在发送数据时等待服务器返回响应的时间
    metadata.fetch.timeout.ms 指定了生产者在获取元数据(比如目标分区的首领是谁)时等待服务器返回响应的时间,
    如果等待响应超时,那么生产者要么重试发送数据,要么返回一个错误。
    timeout.ms指定了broker等待同步副本返回消息确认的时间,与asks的配置相匹配, 如果指定时间内没有收到同步副本的确认,broker就会返回一个错误。

10.max.block.ms
该参数指定了在调用send()方法或使用partitionsFor()方法获取元数据时生产者的阻塞时间,当生产者的发送缓冲区已满,或者没有可用的元数据,这些方法就会阻塞。阻塞时间到达max.block.ms时,生产者会抛出超时异常

11.max.request.size
控制生产者发送的请求大小。可以指 能发送单个消息的最大值,也可以指 单个请求里所有消息的总大小。

  1. receive.buffer.bytes 和 send.buffer.bytes

指定了TCP socket 接收和发送数据包的缓冲区大小。
如果设定为-1.就使用操作系统的默认值。

  • 顺序保证

Kafka可以保证同一个分区里的消息时有序的。
如果生产者按照一定的顺序发送消息,broker就会按照这个顺序把它们写入分区,
消费者也按照童颜的顺序读取它们。

序列化器

自定义序列化器

如果发送到Kafka的对象不是简单的字符串或整型,那么可以使用序列化框架来创建消息记录。
如Avro、Thrift或Protobuf.或使用自定义序列化器。
强烈建议使用通用的序列化框架。
简单的一个类

public class Customer {
	private int customerID;
	private String customerName;
	
	public Customer(int ID, String name) {
		this.customerID = ID;
		this.customerName = name;
	}
	
	public int getID() {
		return customerID;
	}
	
	public String getName() {
		return customerName
	}
}

为这个类创建一个序列化器:

import org.apache.kafka.common.errors.SerializationException;

import java.nio.ByteBuffer;
import java.util.Map 

public class CustomerSerializer implements Serializer<Customer> {
	
	@Override 
	public void configure(Map configs, boolean isKey) {
	
	
	}
	
	@Override
	/**
	Customer对象被序列化成:
	表示customerID的四字节整数
	表示customerName长度的4字节整数。如果customerName为空,则长度为0
	表示customerName的N个字节
	*/
	public byte[] serialize(String topic, Customer data) {
		try {
			byte[] serializedName; 
			int stringSize;
			if(data == null) {
				return null;
			} else {
				if (data.getName() != null ) {
					serializedName = data.getName().getBytes("UTF-8");
					stringSize = serializedName.length;
				
				} else {
					serializedName = new byte[0];
					stringSize = 0
				
				}
			
			}
			ByteBuffer buffer = ByteBuffer.allocate(4 + 4 + stringSize);
			buffer.putInt(data.getID());
			buffer.putInt(stringSize);
			buffer.put(serializedName);
		
			return buffer.array();
		}catch (Exception e){
			throw new SerializationException("Error when serializing Customer to byte[] " + e);
		}
	
	
	}
	
	@Override
	public void close(){
		// 
	}
}

只要使用CustomerSerializer.就可以把消息记录定义成ProducerRecord<String, Customer>. 并且可以直接把Cutomer对象传递给生产者。
但是代码比较脆弱:如果我们有多种类型的消费者,可能需要把customerID字段变成长整型,或者为Customer添加startDate字段。
这样会出现新旧消息的兼容性问题。
在不同版本的序列化器和反序列化器之间调试兼容性问题着实是个挑战。
如果同一个公司的不同团队都需要往Kafka写入Customer数据,那么他们就需要使用相同的序列化器。如果序列化器发生改动,他们几乎要在同一时间修改代码。

基于以上几点原因。我们不建议使用自定义序列化器,而是使用已有的序列化器和反序列化器。。 比如json.Avro.Thrift.Protobuf.

使用Avro序列化

Avro是一种与编程语言无关的序列化格式,Doug Cutting创建了这个项目,目的是提供一种共享数据文件的方式。

Avro数据通过与语言无关的schema来定义。schema通过json来描述,数据被序列化成二进制或json文件。一般会使用二进制。
Avro在读写文件时需要用到schema。 schema一般会被内嵌在数据文件里。

Avro有一个很有意思的特性,当负责写消息的应用程序使用了新的schema. 负责读消息的应用程序可以继续处理消息而无需做任何改动。

假如schema 第一版 是这样的

{
	"namespace": "customerManagement.avro",
	"type": "record",
	"name": "Customer",
	"fields": [
		{"name": "id", "type": "int"},
		{"name": "name", "type": "string"},
		{"name": "faxNumber", "type": ["null", "string"], "default": "null"},
	
	]
}

id 和name是必须的,faxNumber是可选的。

schema第二版:

{
	"namespace": "customerManagement.avro",
	"type": "record",
	"name": "Customer",
	"fields": [
		{"name": "id", "type": "int"},
		{"name": "name", "type": "string"},
		{"name": "email", "type": ["null", "string"], "default": "null"},
	
	]
}

更新到新版schema后,旧记录仍然包含faxNumber字段,而新纪录则包含email字段。
部分负责读取数据的应用程序进行了升级,是如何处理这些变化的呢。

在应用程序升级之前,会调用类似getName()、getId()、getFaxNumber()这样的方法。
如果碰到使用新的schema构建的消息。getName() 和 getId()正常返回,但getFaxNumber()方法会返回null.因为消息里不包含传真号码。

在应用程序升级之后,getEmail()方法取代了getFaxNumber()方法。如果碰到一个使用旧schema构建的消息。那么getEmail()就会返回null.因为旧消息不包含邮件地址。

Avro的好处:修改了消息的schema。但并没有更新所有负责读取数据的应用程序,而这样仍然不会出现异常或阻断性错误,也不需要对现有数据进行大幅更新。

  • 用于写入数据的读取数据的schema必须是相互兼容的。有兼容性原则
  • 反序列化器需要用到用于写入数据的schema.即使它可能与用于读取数据的schema不一样。 Avro数据文件里就包含了用于写入数据的schema.不过在kafka里有一种更好的处理方式。

在Kafka里使用Avro

Avro的数据文件里包含了整个schema。不过这样的开销是可接受的。
但是如果每条记录都嵌入schema.会让记录的大小成倍地增加。
不过不管咋样,在读取记录时仍然需要用到整个schema.所以要先找到schema.我们遵循通用的结构模式,并使用schema注册表来达到目的。

schema注册表并不属于kafka.现在已经有一些开源的schema注册表实现。

我们使用Confluent Schema Registry.

我们把所有写入数据需要用到的schema保存在注册表里,
然后在记录里引用schema的标识符。
负责读取数据的应用程序从注册表里拉去schema来反序列化记录。
序列化器和反序列化器分别负责处理schema的注册和拉取。
Avro序列号器的使用方法和其他序列化器是一样的。

Properties props = new Properties();

props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "io.confluent.kafka.serializers.KafkaAvroSerializer");
props.put("value.serializer", "io.confluent.kafka.serializers.KafkaAvroSerializer");
props.put("schema.registry.url", schemaUrl);

String topic = "customerContacts";

Producer<String, Customer> producer = new KafkaProducer<String, Customer>(props);

while (true) {
	Customer customer = CustomerGenerator.getNext();
	System.out.println("Generated customer " + customer.toString());
	ProducerRecord<String, Customer> record = new ProducerRecord<>(topic, customer.getId(), customer);
	producer.send(record);
}

分区

ProducerRecord对象包含了主题,键和值,
Kafka消息是一个个键值对。
ProducerRecord对象可以只包含主题和值,键可以设置为默认的null.
不过大多数应用程序会用到键。
键有俩个用途:可以作为消息的附加消息,也可以决定消息该被写到主题的哪个分区。
拥有相同键的所有记录都会被写到同一个分区。
如果一个进程只从一个主题的分区读取数据,那么具有相同键的所有记录都会被该进程读取。

如果键值为null,并且使用了默认的分区器,那么记录将被随机地发送到主题内各个可用的分区上。
分区器使用轮询Round Robin算法将消息均衡地发布到各个分区上。

如果键不为空,并且使用了默认的分区器,那么Kafka会对键进行散列。然后根据散列值把消息映射到特定的分区上。
这里的关键之处在于,同一个键总是被映射到同一个分区上。
所以在进行映射时,我们会使用主题所有的分区,而不仅仅是可用的分区。
这也意味着,如果写入的分区是不可用的,那么就会发生错误,这种情况很少发生。

只有在不改变主题分区数量的情况下,键与分区之间的映射才能保持不变。
如果要使用键来映射分区,那么最好在创建主题的时候就把分区规划好。

  • 实现自定义分区策略
import org.apache.kafka.client.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.PartitionInfo;
import org.apache.kafka.common.record.InvalidRecordException;
import org.apache.kafka.common.utils.Utils;

public class BananaPartitioner implements Partitioner {
	public void configure(Map<String, ?> configs) {};
	public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
	
		List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
		int numPartitions = partitions.size();
		
		if ( (keyBytes == null) || (!(key. instanceOf String))) {
			throw new InvalidRecordException("We expect all messages to have customer name as key");
			
		}
		if (((String) key).equalse("Banana"){
			return numPartitions; // Banana总是被分配到最后一个分区。
		}
		
		//其他记录被散列到其他分区
		return (Math.abs(Utils.murmur2(keyBytes)) % (numPartitions-1))
	}

}

Kafka消费者---从Kafka读取数据

应用程序使用KafkaConsumer向Kafka订阅主题,并从订阅的主题上接收消息

KafkaConsumer概念

消费者和消费者群组

我们有一个应用程序需要从一个Kafka主题读取消息并验证这些消息,然后再保存起来。
需要创建一个消费者对象,订阅主题并开始接收消息,然后验证消息并保存结果。

生产者往主题写入消息的速度超过了应用程序验证数据的速度。
此时,很有必要对消费者进行横向伸缩。
我们可以使用多个消费者从同一个主题读取消息。对消息进行分流。

Kafka消费者从属于消费者群组。 一个群组里的消费者订阅的是同一个主题。每个消费者接收主题 一部分分区的消息。
假设主题T1有4个分区,消费者有1个C1. 是群组G1里的唯一的消费之二,我们用它订阅主题T1.

一个消费者收到4个分区的消息

俩个消费者收到4个分区的消息

4个消费者收到4个分区的消息

消费者超过主题的分区数量,那么就有一部分消费者就会闲置。

往群组里增加消费者 是横向伸缩消费能力的主要方式。
Kafka消费者经常会做一些高延迟的操作。比如把数据写到数据库或者HDFS。
或者使用数据进行比较耗时的计算。
单个消费者无法跟上数据生成的速度,所以可以增加更多的消费者,让它们分担负载。每个消费者只处理部分分区的消息。
我们有必要为主题创建大量的分区,在负载增长时可以加入更多的消费者。

除了通过增加消费者来横向伸缩单个应用程序外,还经常出现多个应用程序从同一个主题读取数据的情况。
在这些场景里,每个应用程序可以获取到所有的消息,而不只是其中的一部分。
只要保证每个应用有自己的消费者群组,就可以让它们获取到主题的所有消息。
横向伸缩Kafka消费者和消费者群组并不会对性能造成负面影响。

俩个消费者群组对应一个主题

消费者群组和分区再均衡

群组里的消费者共同读取主题的分区。
一个新的消费者加入群组时,它读取的是原本由其他消费者读取的消息。
当一个消费者被关闭或发生崩溃时,它就离开群组,原本由它读取的分区将由群组里的其它消费者来读取。
在主题发生变化时,比如添加了新的分区,会发生分区重分配

分区的所有权从一个消费者转移到另一个消费者,这样的行为称为再均衡。
再均衡非常重要,为消费者群组带来了高可用性和伸缩性。 我们可以放心地添加或移除消费者。

在再均衡期间,消费者无法读取消息,造成整个群组一小段时间的不可用
当分区被重新分配给另一个消费者是,消费者当前的读取状态会丢失,有可能还需要刷新缓存。

消费者通过 向 被指派 为 群组协调器 的broker(不同的群组可以有不同的协调器) 发送 心跳来维持它们和群组的从属关系以及它们对分区的所有权关系。
只要消费者以正常的时间间隔发送心跳,就被认为是活跃的,说明它还在读取分区里的消息。
消费者会在轮询消息或提交偏移量时发送心跳。
如果消费者停止发送心跳的时间足够长,会话就会过期,群组协调器认为它已经死亡,会触发一次再均衡。

如果一个消费者发生崩溃,并停止读取消息,群组协调器会等待几秒钟,确认它死亡率才会触发再均衡。
在这几秒钟的时间里,死掉的消费者不会读取分区里的消息。
在清理消费者时,消费者会通知协调器 它将要离开群组。 协调器会立即触发一次再均衡。

  • 心跳行为在最近版本中的变化
    0.10.1版本里,Kafka社区引入了一个独立的心跳线程,可以在轮询消息的空档发送心跳。
    发送心跳的频率与消息轮询的频率之间是相互独立的。
    新版本的Kafka里,可以指定消费者在离开群组并触发再均衡之前可以由多长时间不进行消息轮询。这样可以避免出现活锁 livelock.

  • 分配分区时咋样的一个过程
    当消费者要加入群组时,它会向群组协调器发送一个JoinGroup请求,第一个加入群组的消费者将成为 群主。
    群主从协调器那里获得群组的成员列表, 并负责给每一个消费者分配分区,

Kafka内置了俩种分配策略。
分配完毕之后,群主把分配情况列表发送给群组协调器,协调器再把这些信息发送给所有的消费者。每个消费者只能看到自己的分配信息。只有群主知道所有消费者的分配信息。

创建Kafka消费者。

读取消息之前,需要先创建一个KafkaCustomer对象。
把想要传个消费者的属性放在Properties对象里。
三个必要的属性。 bootstrap.servers、key.deserializer、value.deserializer.

bootstrap.servers指定了Kafka集群的连接字符串。 key.deserializer和value.deserializer把指定的字节数组转成Java对象。

group.id不是必需的,指定了KafkaConsumer属于哪一个消费者群组。

Properties props = new Properties();
props.put("bootstrap.servers", "borker1:9092,broker2:9092");
props.put("group.id", "CountryConter");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);

订阅主题

创建好消费者之后,可以开始订阅主题了。 subscribe()方法接受一个主题列表作为参数,
consumer.subscribe(Collections.singletonList("customerCountries"))

也可以传入一个正则表达式,可以匹配多个主题。
如果应用程序需要读取多个主题,并且可以处理不同类型的数据,那么这种订阅方式就很管用。
consumer.subscribe(Patter.compile("test.*"))

轮询

消费轮询是消费者API的核心。通过一个简单的轮询向服务器请求数据,一旦消费者订阅了主题,轮询就会处理所有的细节。包括群组协调、分区再均衡、发送心跳和获取数据。

开发者只需要使用一组简单的API来处理从分区返回的数据。

try {
	while(true) {
		ConsumerRecords<String, String> records = consumer.poll(100);  // 持续对Kafka进行轮询,否则会被认为死亡。它的分区会被移到其他消费者。返回一个列表。
		for (ConsumserRecord<String, String> record: records) {
                        //循环处理数据
			log.debug("topic = %s, partition = %s, offset = %d, customer = %s, country = %s\n",
			record.topic(),record.partition(), record.offset(), record.key(), record.value());
			
			int updateCount = 1;
			
			if (custCountryMap.containkey(record.value()) {
				updatedCount = custCountryMap.get(record.value()) + 1;
			
			}
			custCountryMap.put(record.value(), updatedCount);
			JSONObject json = new JSONObject(custCountryMap);
			System.out.println(json.toString());
		
		
		}
	
	
	}

} finally{
	consumer.close();
}

轮询不只是获取数据,在第一次调用新消费者的poll()方法是,会法则查找Groupcoordinator.然后加入群组,接受分配的分区,
如果发生了再均衡,整个过程也是在轮询期间进行的。
心跳也是轮询里发送出去的。

  • 线程安全
    在同一个群组里。我们无法让一个线程运行多个消费者,也无法让多个线程安全地共享一个消费者。
    按照规则,一个消费者使用一个线程。如果要在同一个消费者群组里运行多个消费者,需要让每个消费者运行在自己的线程里。
    最好是把消费者的逻辑封装在自己的对象里,然后使用Java的ExecutorService启动多个线程。

消费者的配置

  1. fetch.min.bytes
    消費者从服务器获取记录的最小字节数。broker在收到消费者的数据请求时,如果可用的数据量小于fetch.min.bytes。那么就会等到有足够的可用数据时才返回给消费者。
    这样可以降低消费者和broker的负载。

  2. fetch.max.wait.ms
    指定broker的等待时间,默认是500ms.如果没有足够的数据流入kafka.导致500ms的延迟。

  3. max.partition.fetch.bytes
    服务器从每个分区里返回给消费者的最大字节数。默认1MB.

  4. session.timeout.ms
    消费者在被认为死亡之前可以与服务器断开连接的时间。默认是3s.

  5. auto.offset.reset
    消费者在读取一个没有偏移量的分区或者偏移量无效的情况下(因消费者长时间失效,包含偏移量的记录已经过时并被删除)该如何处理。默认值是latest.
    在偏移量无效的情况下,消费者将从最新的记录开始读取数据。另一个是earliest.从起始位置读取分区的记录。

  6. enable.auto.commit
    有几种不同的提交偏移量的方式,该属性指定了消费者是否自动提交偏移量。默认值 true. 为了避免出现重复数据和数据丢失,可以设为false.由自己控制何时提交偏移量。

  7. partition.assignment.strategy
    分区会被分配给群组里的消费者,PartitionAssignor根据给定的消费者和主题,决定哪些分区应该被分配给哪个消费者,Kafkayou俩个默认的分配策略。
    Range.
    该策略会把主题的若干个连续的分区分配给消费者,
    C1 和 C2 同时订阅了主题 T1 T2. 每个主题有3个分区。那么C1有可能被分配到这俩个主题的分区0 和 分区 1. C2分配到俩个主题IDE分区2.
    因为每个主题有奇数个分区,而分配是在主题内独立完成的。C1分配到比C2更多的分区。
    RoundRobin:
    该策略把主题的所有分区逐个分配给消费者,

  8. client.id
    任意的字符串,broker用来标识从客户端发送过来的消息

  9. max.poll.records
    单词调用call()方法能够返回的记录数量

  10. receive.buffer.bytes和send.buffer.bytes
    用到的tcp缓冲去可以设置大小。设为-1,就使用操作系统的默认值。

提交和偏移量

Kafka不会像其他JMS队列那样需要得到消费者的确认,这是Kafka的一个独特之处,
消费者可以使用Kafka来追踪消息在分区里的位置(偏移量)。

我们把更新分区当前位置的操作叫做提交。
消费者是如何提交偏移量的呢? 消费者往一个叫做 _consumer_offset的特殊主题发送消息,消息里包含每个分区的偏移量。
如果消费者一直处于运行状态,那么偏移量就没有什么用处,不过,如果消费者发生崩溃或者有新的消费者加入群组,就会出发再均衡。
完成再均衡之后,每个消费者可能分配到新的分区,而不是之前处理的分区,为了能够继续之前的工作,消费者需要读取每个分区最后一次提交的偏移量。
然后从偏移量指定的地方继续处理。

如果提交的偏移量小于客户端处理的最后一个消息的偏移量。那么处于俩个偏移量之间的消息就会被重复处理。
如果提交的偏移量大于客户端处理的最后一个消息的偏移量。那么处于俩个偏移量之间的消息就会丢失。

自动提交

最简单的方式是让消费者自动提交偏移量。如果enable.auto.commit=true.那么每过5s.消费者会自动把从poll()方法接收到最大偏移量提交上去。
提交时间间隔由auto.commit.interval.ms控制。
自动提交也是在poll()方法里进行的。消费者在每次进行轮询会检查是否提交偏移量了。如果是,那么就会提交从上一次轮询返回的偏移量。

自动提交虽然方便,但是没有为开发者留有余地来避免重复处理消息。

提交当前偏移量

大部分开发者通过控制偏移量提交时间来消除丢失消息的可能性。
并在发生再均衡时减少重复消息的数量。
消费者API提供了另一种提交偏移量的方式。开发者可以在必要的时候提交当前偏移量,而不是基于时间间隔。

把enable.auto.commin设为false. 让应用程序决定何时提交偏移量。
使用commitSync()提交偏移量最简单也最可靠。 这个API会提交由poll()方法返回的最新偏移量,提交成功后马上返回,提交失败就抛出异常。

commitSync()会提交由poll()方法返回的最新偏移量。所以在处理完所有记录后要确保调用了commitSync().
如果发生了再均衡,从最近一批消息到发生再均衡之间的所有消息都将被重复处理。

异步提交

手动提交有一个不足之处,在broker对提交请求作出回应之前。应用程序会一直阻塞。这样会限制应用程序的吞吐量。
使用异步 commitAsync().

同步和异步组合提交

偶尔出现的提交失败,不进行重试不会由太大问题。临时失败,后续的提交总会有成功的。
一般会组合使用。

提交特定的偏移量

提交偏移量的频率与处理消息批次的频率是一致的。
如果想要更频繁的提交该怎么办?
如果poll()返回一大批数据,为了避免因再均衡引起的重复处理整批消息。想要在批次中间提交偏移量该怎么办?
这种情况无法调用commitSync()和commitAsync()来实现,因为它们只会提交最后一个偏移量。而此时该批次里的消息还没有处理完。
消费者API允许在调用commitSync()和commitAsync()方法时传进去希望提交的分区和偏移量的map.
因为消费者可能不只读一个分区,需要跟踪所有分区的偏移量。

再均衡监听器

消费者在退出和进行分区再均衡之前,会做一些清理工作。

你会在消费者失去对一个分区的所有权之前提交最后一个已处理记录的偏移量。
如果消费者准备了一个缓冲区用于处理偶发的事件,那么在失去分区所有权之前,需要处理在缓冲区累积下来的记录。

在为消费者分配新分区或移除旧分区时,可以通过消费者API执行一些应用程序代码,在调用subscribe()方法时传进去一个ConsumerRebalanceListener实例就可以了。

ConsumerRebalanceListener有俩个需要实现的方法

private class HandleRebalance implements ConsumerRebalanceListener {
	public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
	
	}
	
	public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
		consumer.commitSync(currentOffSets);
		
	}

}

从特定偏移量处开始处理记录

如果想从分区的起始位置开始读取消息,或者分区的末尾。
可以使用seekTobeginning(Collection tp) 和 seekToEnd(Collection tp)俩个方法。

在使用Kafka以外的系统来存储偏移量,它将给我们带来更大的惊喜。

每处理一条记录就提交一次偏移量。
在记录被保存到数据库之后,偏移量提交之前,应用程序仍然有可能发生崩溃,导致重复处理数据。数据库里就会出现重复记录。

如果保存记录和偏移量可以在一个原子里操作完成,就可以避免出现上述情况。
记录和偏移量要么都被成功提交,要么都不提交。
如果记录是保存在数据库里,而偏移量是提交到Kafka上,那么就无法实现原子操作。

在同一个事务里把记录和偏移量都写到数据库里,而不是Kafka里。
那么消费者在得到新分区时怎么知道从哪读取,
可以使用seek()方法,在消费者启动或分配到新分区时,可以使用seek()方法查找保存在数据库里的偏移量。

如何退出

如果确定要退出循环,需要通过另一个线程调用consumer.wakeup()方法,如果循环运行在主线程里,可以在ShutdownHook里调用该方法。
consumer.wakeup()是消费者唯一一个可以从其他线程里安全调用的方法。
调用consumer.wakeup()可以退出poll().并抛出WakeupException异常。
如果调用consumer.wakeup()时线程没有等待轮询,那么异常将在下一轮调用poll()时抛出。

反序列化器

消费者需要用反序列化器 把从Kafka接收到的字节数组转成Java对象。
生成消息使用的序列化器和读取消息使用的反序列化器应该是一一对应的。
使用Avro 和 schema注册表进行序列化和反序列化的优势在于:AvroSerializer可以保证写入主题的数据与主题的Schema是兼容的。

  1. 自定义反序列化器。
import org.apache.kafka.common.errors.SerializationException;

import java.nio.ByteBuffer;
import java.util.Map;

public class CustomerDeserializer implements Deserializer<Customer> {

	@Override
	public void configure(Map configs, boolean isKey) {
	
	}
	
	@Override
	public Customer deserializer(String topic, byte[] data){
		int id;
		int nameSize;
		String name;
		try {
			if (data == null) {
				return null;
			}
			
			if(data.length < 8 ){
				throw new SerializationException("Size of data received by IntegerDeserializer is shorter than expected");
			
			}
			
			ByteBuffer buffer = ByteBuffer.wrap(data);
			id = buffer.getInt();
			nameSize = buffer.getInt();
			byte[] nameBytes = new byte[nameSize];
			name = new String(nameBytes, "UTF-8");
			
			return new Customer(id, name);
			
			
		} catch(Exception e) {
		
			throw new SerializationException("error when serializing Customer " + e);
		
		}
		
	}
}
  1. 在消费者里进行Avro反序列化

schema.registry.url 是一个新的参数,指向shcema的存放位置。消费者可以使用由生产者注册的schema来反序列化消息。
将生成的类Customer作为值的类型。
record.value()返回的是一个Customer实例。

独立消费者 ---为什么以及咋样使用没有群组的消费者。

一个消费者从一个主题的所有分区或者某个特定的分区读取数据,不需要消费者群组和再均衡了。
只需要把主题或者分区分配给消费者,然后开始读取消息并提交偏移量。

深入Kafka

  • Kafka如何进行复制
  • Kafka如何处理来自生产者和消费者的请求
  • Kafka的存储细节,比如文件格式和索引

集群成员关系

Kafka使用Zookeeper来维护集群成员的信息
每个broker都有一个唯一的标识符,可以在配置文件里指定,也可以自动生成。
在broker启动的时候,它通过创建临时节点把自己的ID注册到broker.
Kafka组件订阅zookeeper的/brokers/ids路径。
当有broker加入集群或者退出集群时,这些组件就可以获得通知。

在broker停机、出现网络分区或长时间垃圾回收停顿时,broker会从Zookeeper上断开连接,
此时,broker在启动时创建的临时节点自动从Zookeeper上移除。监听broker列表的Kafka组件会被告知该broker 已移除。

控制器

控制器其实就是一个broker.除了具有一般broker的功能之外,还负责分区首领的选择。
集群里第一个启动的broker通过Zookeeper创建一个临时节点/controller让自己称为控制器。
其他broker在启动时也会尝试创建这个节点,会收到一个“节点已存在”的异常。
其他broker在控制器节点上创建Zookeeper watch对象。这样它们就可以收到这个节点变更的通知。

如果控制器被关闭或者与Zookeeper断开连接,Zookeeper上的临时节点就会小时。其他broker会收到控制器节点消失的通知。
它们会尝试让自己成为新的控制器,第一个创建成功之后,其他节点再新的控制器节点上再次创建watch对象。

当控制器发现一个broker离开集群,通过观察相关的zookeeper路径,它就知道,那些失去首领的分区需要一个新首领,控制器遍历这些分区,并确定谁应该成为新首领。

当控制器发现一个broker加入集群,它会使用broker ID来检查新加入的broker是否包含现有分区的副本,如果有,控制器就把变更通知发送给新加入的broker和其他broker.

Kafka使用Zookeeper的临时节点来选举控制器。并在节点加入集群或退出集群时通知控制器。
控制器负责在节点加入或离开集群时进行分区首领选举。
控制器使用epoch来避免脑裂。
脑裂是指俩个节点同时认为自己是当前的控制器。

复制

复制功能是Kafka的核心,
Kafka把自己描述成:一个分布式、可分区的、可复制的提交日志服务。
复制之所以郑愕关键,是因为它可以在个别节点失效时仍能保证Kafka的可用性和持久性。

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

副本有以下俩种类型。

  • 首领副本
    每个分区都有一个首领副本,为了保证一致性,所有生产者请求和消费者请求都会经过这个副本
  • 跟随者副本
    首领以外的副本都是跟随者副本。跟随者副本不处理来自客户端的请求,它们唯一的任务就是从首领那里复制消息。保持与首领一致的状态。
    如果首领发生崩溃,其中的一个跟随者会被提升为首领。

首领的另一个任务就是搞清楚哪个跟随者的状态与自己是一致的。
跟随者为了保持与首领状态一致,在有新消息到达时尝试从首领那里复制消息,不过有各种原因会导致同步失败。
例如:网络拥塞导致复制变慢,broker发生崩溃导致复制之后,直到重启broker后复制才会继续。

为了与首领保持同步,跟随者向首领发送获取数据的请求,这种请求与消费者为了读取消息而发送的请求是一样的。
首领将响应消息发给跟随者。
请求消息里包含了跟随者想要获取消息的偏移量,而且这些偏移量总是有序的。

一个跟随者副本先请求消息1,接着请求消息2,然后请求消息3,在收到这3个请求的响应之前,它是不会发生第4个请求消息的。
如果跟随者发送了请求消息4,那么首领就知道它已经收到了前面3个请求的响应。
通过查看每个跟随者请求的最新偏移量,首领就会知道每个跟随者的复制进度。
如果跟随者在10s内没有请求任何消息,或者虽然在请求消息,但在10s内没有请求最新的数据,那么它就会被认为是不同步的。
如果一个副本无法与首领保持一致,在首领失效时,它就不可能成为新首领,没有包含全部的消息。

持续请求得到的最新消息副本被称为同步的副本。
在首领发生失效时,只有同步副本才有可能被选为新首领。

跟随者的正常不活跃时间或在称为不同步副本之前的时间 是通过 replica.lag.time.max.ms参数来配置的。

除了当前首领之外,每个分区都有一个首选首领--创建主题时选定的首领就是分区的首选首领。
之所以把它叫为首选首领,是因为在创建分区时,需要在broker之间均衡首领。
我们希望首选首领在成为真正的首领时,broker 间的负载最终会得到均衡。
默认情况下。auto.leader.rebalance.enable = true. 它会检查首选首领是不是当前首领,如果不是,并且该副本是同步的,那么就会出发首领选举,让首选首领成为当前首领。

处理请求

broker的大部分工作是处理客户端、分区副本、控制器发送给分区首领的请求。
Kafka提供了一个二进制协议(基于TCP),指定了请求消息的格式,以及broker如何对请求作出响应--包括成功处理请求或在处理请求过程中遇到错误。
客户端发起连接并发送请求,broker处理请求并作出响应。
broker按照请求到达的顺序来处理它们--这种顺序保证让kafka具有了消息队列的特性,同时保证保存的消息也是有序的。

所有的请求消息都包含一个标准消息头:

  • Request type API key
  • Request version broker可以处理不同版本的客户端请求,并根据客户端版本作出不同的响应。
  • Correlation ID -- 一个具有唯一性的数字,用于标识请求消息,同时也会出现在响应消息和错误日志里。
  • Client ID -- 用于标识发送请求的客户端

我们不打算在这里描述改协议,因为在Kafka文档里已经有很详细的说明。
不过了解broker如何请求还是有必要的。

broker会在它所监听的每一个端口上运行一个Acceptor线程。这个线程会创建一个连接,并把它交个Processor线程(网络线程)去处理。
Processor线程的数量是可配置的。
网络线程负责从客户端获取请求消息,把它们放进请求队列。然后从响应队列获取响应消息,把它们发送到客户端。

请求消息被放到请求队列后。IO线程会负责处理它们。

几种常见的请求类型

  • 生成请求:生成者发送的请求,
  • 获取请求:在消费者和跟随者副本需要从broker读取消息时发送的请求。

生产请求和获取请求都必须发送给分区的首领副本。
如果broker收到一个针对特定分区的请求,而该分区的首领在另一个broker上。那么发送请求的客户端会收到一个“非分区首领”的错误响应。
Kafka客户端要自己负责把生产请求和获取请求发送到正确broker上。

那么客户端怎么直到该往哪里发送请求呢?客户端使用了另一种请求类型,也就是元数据请求。
这种请求包含了客户端感兴趣的主题列表。服务器端的响应消息里指明了这些主题所包含的分区、每个分区都有哪些副本,以及哪个副本是首领。
元数据请求可以发送给任意一个broker.因为所有broker都缓存了这些消息。

一般情况下,客户端会把这些信息缓存起来,并直接往目标broker上发送生产请求和获取请求。
它们需要时不时地通过发送元数据请求来刷新这些信息。刷新的时间间隔通过metadata.max.age.ms参数来设置。
如果客户端收到“非首领”错误,它会在尝试重发请求之前先刷新元数据。

生产请求

acks这个配置参数,---该参数指定了需要多少个broker确认才可以认为一个消息写入是成功的。
不同的配置对写入成功的界定是不一样的。
会对请求做3个验证。

  • 发送数据的用户是否有主题写入的权限?
  • 请求里包含acks值是否有效,只允许出现0,1,all
  • 如果acks=all.是否有足够多的同步副本保证消息已经被安全写入?

之后,消息被写入到本地磁盘。在Linux系统上,消息会被写到文件系统缓存里。
在消息被写入分区的首领之后,broker开始检查acks配置参数,---如果acks被设为0或1,broker立即返回响应。
如果acks设置为all.那么请求会被保存在一个叫做炼狱的缓冲区里。直到首领发现所有跟随者副本都复制了消息。响应才会被返回到客户端。

获取请求

broker处理获取请求的方式与处理生产请求的方式很相似。

客户端发送请求,向broker请求主题分区里具有特定偏移量的消息。
"请把主题Test 分区0 偏移量从53开始的消息 以及 主题Test 分区3 偏移量从64 开始的消息发给我"
客户端还可以指定broker最多可以从一个分区里返回多少数据。这个限制非常重要,因为客户端需要为broker返回的数据分配足够的内存。
如果没有这个限制,broker返回的大量数据有可能耗尽客户端的内存。

首领在收到请求,会先检查请求是否有效, 比如指定的偏移量在分区上是否存在,如果不存在,将返回一个错误。

如果请求的偏移量存在,broker将按照客户端指定的数量上限从分区里读取消息。再把消息返回给客户端。
Kafka使用零复制技术向客户端发送消息。也就是说Kafka直接把消息从文件(缓存)里发送到网络通道,而不需要经过任何中间缓冲区。
这项技术避免了字节复制,也不需要管理内存缓冲区,从而获得更好的性能。

客户端还可以设置broker返回数据的下限。比如下限10kb,就是告诉broker,等到有10kb数据的时候再把它们发送给我。
在主题消息流量不是很大的情况下,这样可以减少CPU和网络开销。

当然不会让客户端一直等待broker累积是数据,在等待了一段时间之后,就可以把可用的数据拿回处理,而不是一直等待下去。

并不是所有保存在分区首领上的数据都可以被客户端读取。大部分客户端只能读取已经被写入所有同步副本的消息。
分区首领直到每个消息会被复制到哪个副本上。

因为还没有被足够多的副本复制的消息 是 不安全的。
如果首领发生崩溃,那么这些消息就对视了。

如果我们允许消费者读取这些消息,可能破坏一致性。

其他请求

客户端在网络上使用的通用的二进制协议,Kafka内置了由开源社区贡献者实现和维护的Java客户端。同时也有其他语言实现的客户端, c、python、go等。

broker之间也使用同样的通信协议,它们之间请求发生在Kafka内部,客户端不应该使用这些请求。
例如:当一个新首领被选举出来,控制器会发送LeaderAndIsr请求给新首领和跟随者。

之前Kafka消费者使用Zookeeper来跟踪偏移量,在消费者启动的时候,它通过检查保存在Zookeeper上的偏移量就可以直到从哪里开始处理消息。

因为各种原因。把偏移量保存在特定的Kafka主题上。为了达到这个目的,不得不往协议里增加几种请求类型:OffsetCommitRequest、OffsetFetchRequest、ListOffsetsRequest.

主题的创建仍然需要通过命令行工具来完成。命令行工具会直接更新Zookeeper里的主题列表。broker监听这些主题列表,在有新主题加入时,它们会收到通知。
后期,增加了CreateTopicRequest请求类型,这样客户端可以直接向broker请求创建新主题了。

可以通过修改已有的请求类型来给它们新增功能。

物理存储

Kafka的基本存储单元是分区,分区无法再多个broker间再进行细分,也无法在同一个broker的多个磁盘上进行再细分。
所以,分区的大小 受到 单个挂载点可用空间的限制。
一个挂载点有单个磁盘或多个磁盘组成。
如果配置了JBOD,就是单个磁盘。如果配置了RAID,就是多个磁盘。

在配置Kafka的时候,管理员指定了一个用于存储分区的目录清单。---log.dirs参数的值。

分区分配

在创建主题时,Kafka首先会决定如何在broker间分配分区,。
假如有6个broker,打算创建一个包含10个分区的主题,并且复制系数为3.
那么Kafka就会有30个分区副本。 被分配到6个broker。
在进行分区分配时,我们要达到如下的目标:

  1. broker间平均地分布分区副本。 保证每个分区有5个副本。
  2. 确保每个分区的每个副本在不同的broker上。
  3. 如果为broker指定了机架信息,那么尽可能把每个分区的副本分配到不同机架的broker上。

为分区选好合适的broker之后,决定这些分区应该使用哪些目录,我们单独为每个分区分配目录。
规则:计算每个目录里的分区数量,新的分区总是被添加到数量最小的那个目录上。

文件管理

保留数据是Kafka的一个基本特性。
Kafka不会一直保留数据,也不会等到所有消费者都读取了消息之后才删除消息。
Kafka管理员为每个主题配置了数据保留期限,规定数据被删除之前可以保留多长时间。
因为在一个大文件里查找和删除消息是很费时的。
我们把分区分成若干个片段。默认情况下,每个片段包含1GB或一周的数据,以较小的为准。
在broker往分区写入数据时,如果达到片段上限,就关闭当前文件,并打开一个新文件。

当前正在写入数据的片段叫做活跃片段。活跃片段永远不会被删除。所以如果你要保留数据1天。但片段里包含了5天的数据,那么这些数据就会被保留5天。

broker会为分区里的每一个片段打开一个文件句柄,哪怕片段是不活跃的,这样会导致打开过多的文件句柄,所以操作系统必须根据实际情况进行调优。

文件格式

Kafka的消息和偏移量保存在文件里。
保存在磁盘上的数据格式与从生产者发送过来或者发送给消费者的消息格式是一样的。

因为使用了相同的消息格式进行磁盘存储和网络传输,Kafka可以使用零复制技术给消费者发送消息。同时避免了对生产者已经压缩过的消息进行解压和再压缩。

除了键、值、偏移量外。消息里还包含了消息大小、校验和、消息格式版本号、压缩算法Snappy、GZip LZ4 、时间戳。
时间戳可以是生产者发送消息的时间,也可以是消息到达broker的时间。这个可配置。

如果生产者发送的是压缩过的消息,那么同一个批次的消息会被压缩在一起。被当做“包装消息”进行发送。

索引

消费者可以从Kafka的任意可用偏移量位置开始读取消息。
假设消费者要读取从偏移量100开始的1MB消息。那么broker必须立即定位到偏移量100.然后开始从这个位置读取消息。
为了帮助broker更快地定位到指定的偏移量。Kafka为每个分区维护了一个索引。
索引把偏移量映射到片段文件和偏移量在文件里的位置。

索引也被分成片段,所以在删除消息时,也可以删除相应的索引。

清理

kafka可以通过改变主题的保留策略来满足各种不同的场景。

清理的工作原理

每个日志片段可以分为俩个部分

  • 干净的部分
    这些消息之前被清理过,每个键只有一个对应的值,这个值是上一次清理时保留下来的。
  • 污浊的部分
    这些消息时上一次清理之后写入的

如果Kafka启动时启用了清理功能, log.cleaner.enable参数。
每个broker会启动一个清理管理器线程或多个清理线程,它们负责执行清理任务。
这些线程会选择污浊率较高的分区进行清理。

为了清理分区,清理线程会读取分区的污浊部分,并在内存里创建一个map.
map里的每个元素包含了消息键的散列值和消息的偏移量,键的散列值是16B.加上偏移量总共是24B.
如果要清理一个1GB的日志片段,并假设每个消息大小1KB,那么这个片段就包含一百万个消息。
而我们 只需要24MB的map就可以清理这个片段。

管理在配置Kafka时,可以对map使用的内存大小进行配置。每个线程都有自己的map.
而这个参数指的是所有线程可使用 的 内存总大小。

被删除的事件

如果只为每个键保留最近的一个消息,那么当需要删除某个特定键所对应的所有消息时,我们该怎么办。

为了彻底把一个键从系统里删除,应用程序必须发送一个包含该键且值为null的消息。

何时清理主题

delete策略不会删除当前活跃的片段一样。
compact策略也不会对当前片段清理,只有旧片段里的消息才会被清理。

posted on 2022-10-26 21:51  llcl  阅读(72)  评论(0编辑  收藏  举报