幂等设计

消息语义

At most once:消息可能会丢失,但不会重复。
At least once:消息不会丢失,但有可能重复。
Exactly once:正好一次。消息不会重复也不会丢失。
在0.11.0.0之前,如果生产者未能收到已提交消息的响应,除了重新发送消息外别无选择,但提供了至少一次的语义保证。即原始请求实际上已经成功,但是遇上某些意外情况,则会在重新发送时将消息再次写入日志。
如:网络抖动、超时等问题,导致Producer没有收到Broker返回的税局Ack,则Producer会继续重试发送消息,从而导致消息重复发送。
从0.11.0.0开始,Kafka生产者支持幂等选项,该选项保证重新发送不会导致日志中出现重复条目
从0.11.0.0开始,Kafka支持类似于将消息发送到多个分区的事务能力:即,所有消息要么全部成功写入,要么都不成功。

  • 幂等: 精确一次,按顺序,投递到每个分区。
  • 事务:跨分区原子写操作。
  • 跨读写任务的流数据正好一次处理。

Kafka幂等实现原理

为了实现Producer的幂等语义,Kafka引入了ProducerID和Sequence Number

  • ProducerID:初始化过程中,Broker会为每个生产者分配一个PID,该PID对用户完全透明的
  • Sequence Number:每个对应PID的Producer发送数据时每条消息都对应一个从0开始单调递增的序号

而Broker 端也会为每个<pid, topic, partitionid>维护一个序号,并且每次提交一条消息都会将序号递增。对于接收的消息,如果其序号比Broker维护的序号(最后一次提交的消息编号)大1,则Broker会接受它,否则将其丢弃。

  • 如果消息序号比 Broker 维护的序号大于等于2,说明中间有数据尚未写入,也即乱序,此时 Broker 拒绝该消息,Producer 抛出OutofSequenceNumber
  • 如果消息序号小于等于 Broker 维护的序号,说明该消息已被保存,即为重复消息,Broker 直接丢弃该消息,Producer 抛出DuplicateSequenceException

注意:幂等设计只能保证单个 Producer 对于同一个Partition的Exactly Once语义

事务保证

Kafka的事务处理,主要是允许应用可以把消费和生产的batch处理(涉及多个partition)在一个原子单元内完成,操作要么全部完成、要么全部失败。例如:从某个Topic消费数据,经过一系列转换后写回另一个Topic,这个过程中要保证从源Topic读取与向目的Topic写入的原子性,这将有助于从故障中恢复。
为了实现这种机制,我们需要应用能提供一个唯一id,即使故障恢复后也不会改变,这个id就是Transactionnal.id(也叫txn.id)。Transactionnal.id跟内部的PID可能(可能是因为Producer也许会发生故障)一一对应,不同之处在于Transactionnal.id由用户提供,而PID是Producer内部生成的。
Kafka事务性语义提供的保证主要有以下三个:

  • 跨多个分区的原子写操作
  • 事务中的所有消息一起可见,或者一起不可见
  • 消费者被设置为跳过未提交的消息
# Producer支持的事务接口
public interface Producer<K, V> extends Closeable {
    // 初始化事务
    // 确保前一个生产者实例发起的具有相同的transactional.id的事务已完成
    // 如果前一个实例事务在执行中失败,则中止。如果最后一笔交易已开始但尚未完成,则等待其完成
    // 获取生产者ID和epoch,用于后续事务中生产者发出的消息
    void initTransactions();
    
    // 开启事务
    void beginTransaction() throws ProducerFencedException;

    // 将指定偏移量的列表发送给Consumer Group Coordinator
    // 并将这些偏移量标记为当前事务的一部分
    // 仅当事务成功提交后,这些偏移量才被视为已提交
    // 提交的偏移量是应用程序将使用的下一条消息,即lastProcessedMessageOffset +1
    void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets, String consumerGroupId) throws ProducerFencedException;
    void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets, ConsumerGroupMetadata groupMetadata)
         throws ProducerFencedException;

    // 提交事务
    void commitTransaction() throws ProducerFencedException;

    // 回滚事务
    void abortTransaction() throws ProducerFencedException;
}

时间轮

时间轮,是一个高效的延时队列,或者说定时器。Kafka中的时间轮(TimingWheel)是一个存储定时任务的环形队列,底层采用数组实现,数组中的每个元素可以存放一个定时任务列表(TimerTaskList)。TimerTaskList是一个环形的双向链表,链表中的每一项表示的都是定时任务项(TimerTaskEntry),其中封装了真正的定时任务TimerTask。

Connector

Connect是Kafka的一部分,是一种用于在Kafka和其他系统之间可扩展的、可靠的流式传输数据的工具。Kafka Connect可以提取整个数据库或将所有应用服务器中的指标收集到Kafka主题中,使数据可用于低延迟的流处理。它为在Kafka和外部数据存储系统之间移动数据提供了一种可靠且可伸缩的方案。

它使得能够快速定义将大量数据集合移入和移出Kafka的连接器变得简单。导出作业可以将数据从Kafka Topic传输到二次存储和查询系统,或者传递到批处理系统以进行离线分析。

Skywalking agent kafka 启动时间长 kafka at most once_字段


Kafka Connect功能包括:

  • Kafka连接器通用框架:规范化其他数据系统与Kafka的集成,简化了连接器开发,部署和管理
  • 分布式和单机模式:上至大型的集中式管理服务,下至开发、测试和小规模的生产部署
  • REST接口:使用REST API提交和管理Connector
  • 自动offset管理:只需从Connector获取一些信息,Kafka Connect就可以自动管理offset提交,因此连接器开发人员不需要担心这个容易出错的部分
  • 分布式和可扩展性:Kafka Connect基于现有的组管理协议。可以添加更多的worker来 扩展Kafka Connect群集
  • 流/批量集成:利用Kafka的现有功能,Kafka Connect是桥接流媒体和批数据系统的理想解决方案

什么时候用

如果你想把数据从MySQL移动到ElasticSearch中,你会使用什么方案?
如果你想把MySQL中的数据或者XML数据转成JSON写入到ElasticSearch中,或者转换成Parquet写入到Hadoop HDFS中,或者转成CSV写入到S3中,你会使用什么方案?
在很多公司的应用中,经常需要使用Logstash把日志导入到ElasticSearch中,也需要使用Flume把日志导入到Hadoop HDFS中,很多时候也需要使用工具把Oracle中的数据导入的Hadoop HDFS中,也需要使用工具把MySQL中的数据或者把XML导入到Oracle。但是这几种场景有一个共性:

  • 每个场景都有自己的数据管道
  • 每个数据管道与各自的端点耦合
  • 数据管道不统一
  • 非统一部署、维护和监控需要额外成本

基本概念

Connect的几个基本概念:

  • Source:负责导入数据到Kafka
  • Sink:负责从Kafka导出数据
  • Connectors:通过管理任务来协调数据流的高级抽象
  • Tasks:数据写入Kafka和从Kafka中读出的具体实现
  • Workers:运行connectors和tasks的进程
  • Converters:Kafka connect和其他存储系统直接发送或者接受数据之间转换数据
  • Transforms:一种轻量级数据调整的工具

两种工作模式

  • standalone(单机模式):在standalone模式中,所有的worker都在一个独立的进程中完成
  • distributed(分布式模式):distributed模式具有高扩展性,以及提供自动容错机制。你可以使用一个group.id来启动很多worker进程,在有效的worker进程中它们会自动的去协调执行connector和task,如果你新加了一个worker或者挂了一个worker,其他的worker会检测到然后在重新分配connector和task

内置Topic

connect-status

存储各个connector、task的状态信息。在Worker分布式的情况下,存储在Kafka 的一个Topic中,topic的名字由worker配置项status.storage.topic来指定。
Status 有5种:

  • UNASSIGNED:connector或者task是否被分配到了个Worker上
  • RUNNING: connector或者task是否正在运行
  • PAUSED: connector或者task是否已暂停运行
  • FAILED: connector或者task是否失败(通常是抛出异常)
  • DESTROYED:connector或者task是否已被删除

在运行时,会定时更新状态信息。

connect-offset

存储各个source task 的下一次创建的task实例的序号offset。在运行时会更新这些source task的offset信息。单机模式、分布式模式下,都会存储这个offset的。只是在Standalone模式下,是以本地File方式存储。分布式模式下,是存储在Kafka中的一个topic中。Topic的名称由worker配置项offset.storage.topic来指定。

connect-config

在Kafka connect中,每一个Connector,以及与之关联的Task都会有一些配置信息。在rebalance后,还是需要用到这些配置的。为了使得Worker Group内共享配置,也需要对connector、task的配置进行存储。

数据转换

连接器可以配置转换,以进行一次轻量级的消息修改。可以在连接器配置中指定转换链:

# 转换别名列表,按指定的顺序执行转换
transforms=alias1,alias2
# 执行转换的类名称
transforms.$alias.type=
# 转换的配置属性
transforms.$alias.$transformationSpecificConfig=

Kafka Transformations包含一些广泛应用的的类型和路由:

类型或路由

说明

InsertField

增加一个静态字段者元数据

ReplaceField

过滤或者重命名字段

MaskField

将字段替换为类型

ValueToKey

提取的记录值上的字段名称作为key

HoistField

将数据包装为Struct或Map中的单个字段

ExtractField

从Struct和Map中提取特定字段,然后在结果中仅包含此字段

SetSchemaMetadata

修改schema名称或版本

TimestampRouter

根据原主题和时间戳修改一条记录的topic。当使用需要根据时间戳写入不同表或索引时很有用。

RegexRouter

根据原主题,替换字符串和正则表达式修改一条记录的topic

使用示例

standalone

$ connect-standalone.sh config/connect-file.properties config/connect-file-source.properties config/connect-file-sink.properties

distribute

# 启动分布式连接器
$ connect-distributed.sh config/connect-distributed.properties

# 添加连接器
$ curl -XPOST --header "Content-Type:application/json" localhost:8083/connectors -d '
{
    "name":"mysql-product-connector",
    "config": {
        "connector.class":"JdbcSourceConnector", 
        "connection.url":"jdbc:mysql://192.168.1.139:3306/test?user=root&password=123456", 
        "table.whitelist":"product",
        "validate.non.null":false,
        "topic.prefix":"mysql-",
        "mode":"timestamp",
        "timestamp.column.name":"create_time"
    }
}'

Connector开发

要在Kafka和其他系统之间复制数据,用户创建自定义的从系统中pull数据或push数据到系统的Connector(连接器)。Connector有两种形式:

  • SourceConnectors从另一个系统导入数据(例如:JDBCSourceConnector会将关系数据库导入Kafka)
  • SinkConnectors导出数据(如:HDFSSinkConnector会将Kafka主题的内容导出到HDFS文件)

connector不会执行任何复制自己的数据:它们的配置展示了要复制的数据,而Connector是负责将该作业分解成一组可以分配给worker的任务。这些任务也有两种相对应的形式:

  • SourceTask
  • SinkTask

RestAPI接口列表

REST API

说明

GET /connectors

返回所有正在运行的connector

POST /connectors

新建一个connector; 请求体必须是json格式并且需要包含name字段和config字段,name是connector的名字,config是json格式,必须包含你的connector的配置信息

GET /connectors/{name}

获取指定connetor的信息

GET /connectors/{name}/config

获取指定connector的配置信息

PUT /connectors/{name}/config

更新指定connector的配置信息

GET /connectors/{name}/status

获取指定connector的状态,包括它是否在运行、停止、或者失败,如果发生错误,还会列出错误的具体信息。

GET /connectors/{name}/tasks

获取指定connector正在运行的task

GET /connectors/{name}/tasks/{taskid}/status

获取指定connector的task的状态信息

PUT /connectors/{name}/pause

暂停connector和它的task,停止数据处理知道它被恢复。

PUT /connectors/{name}/resume

恢复一个被暂停的connector

POST /connectors/{name}/restart

重启一个connector,尤其是在一个connector运行失败的情况下比较常用

POST /connectors/{name}/tasks/{taskId}/restart

重启一个task,一般是因为它运行失败才这样做

DELETE /connectors/{name}

删除一个connector,停止它的所有task并删除配置