Kafka: Connect
转自:http://www.cnblogs.com/f1194361820/p/6108025.html
Kafka Connect 简介
Kafka Connect 是一个可以在Kafka与其他系统之间提供可靠的、易于扩展的数据流处理工具。使用它能够使得数据进出Kafka变得很简单。Kafka Connect有如下特性:
·是一个通用的构造kafka connector的框架
·有单机、分布式两种模式。开发时建议使用单机模式,生产环境下使用分布式模式。
·提供restful的管理connector的API。
·自动化的offset管理。Kafka Connect自动的管理offset提交。
·分布式、可扩展。采用与concumer group中对partition rebalance同样的机制来管理在worker group中的connector、task。
·流/批处理的集成。
接下来会对Kafka Connect做一个全面的分析,来帮助了解上述特性。
1、基本组件:
1.1 Worker
Worker用于调度source task、sink task 来处理数据的进出Kafka。一个Worker中包括多个Connector。一个Connector与多个task关联。当启动(或者加入)一个Connector时,与之关联的Tasks会被创建并提交到executor去执行;停止(或移除)一个Connector时,与之关联的Tasks会被停止并移除。也就是说Worker管理Connector插件的插、拔,Task的启、停。
1.2 Connector
Connector,可以看做是Kafka Connect的插件,用来与其他系统进行集成。有两类:SourceConnector、SinkConnector。如果要实现一个Connector,那么必须继承这两个类中的一个,这是硬性要求。Connector 与Task关联,Worker对Connector的启、停、插、拔,其实最终反映在对Task启、停、插、拔上。
Connector主要为创建Task实例提供配置,可以提供connector本身的配置,也可以提供task的配置。
1.3 Task
Task是由Worker中的executor来执行的。在Worker中包括了所有的WorkerTask。WorkerTask有两类:WorkerSourceTask、WorkSinkTask。
在运行过程中,WorkerSourceTask不断的做如下调用:
1)调用SourceTask.poll()方法取得数据SourceRecords,
2)使用相应的Converter将SourceRecords转换为ProducerRecords,
3)并由KafkaProducer#send推给Kafka Broker
在运行过程中,WorkerSinkTask不断的做如下调用:
1)调用KafkaConsumer#poll方法从Kafka Broker拉取ConsumerRecords,
2)使用相应的Converter将ConsumerRecords转换为SinkRecords,
3)调用SinkTask#flush方法来执行自定义的处理
4)提交 offset到Kafka Broker。
不难看出,只有系统运行正常,SinkTask、SourceTask都会被一次又一次的调用。即一个Task实例会占用一个线程。如果一个为Connector配置了task数量大于1,就会有多个tasks实例(这多个Task实例是同一个Java类的多个实例)与该Connector关联,那么这多个tasks在运行时必然是并发执行的。这就要求SourceTask#poll()、SinkTask#flush()在运行时能够保证线程安全性。
另外从WorkerSinkTask#flush如果出现异常,是不会提交offset的。此外,还会调用consumer#seek找到上一次提交的offset,seek的作用是:让下一次使用consumer#poll方法时,从上一次提交的offset开始poll。这样就可以保证数据在consume过程中不会丢失。
1.3.1 Kafka Connect与Kafka Producer、Kafka Consumer关系
Kafka Connect必然是要依赖于KafkaProducer、KafkaConsumer的。这种依赖关系到底是怎样的呢?从上面WorkerSinkTask、WorkerSourceTask的执行过程来看,他们是直接与KafkaConsumer、KafkaProducer直接关联的。经过源码的查看可以了解到:
在一个Worker内,所有的WorkerSourceTask共享同一个KafkaProducer对象来发送record到Kafka,这种设计也符合KafkaProducer的使用原则;每一个WorkerSinkTask拥有一个私有的KafkaConsumer,也就是说不存在KafkaConsumer共享使用的情况。
既然一个WorkerSinkTask独自使用一个KafkaConsumer,那么一个WorkerSinkTask就可以认为是一个独立的KafkaConsumer。那么如果一个WorkerSinkTask因某种原因启动、加入、重启、结束、中断、停止,就等于是说一个KafkaConsumer加入、退出consumer group,那么该consumer group 的Coordinator Broker就会发起rebalance,使得该KafkaConsumer被分配到的partiion分配到group 内其他的KafkaConsumer上。
在Kafka-Connect中,如何对KafkaConsumer进行分组呢?
从WorkerSinkTask的相关源码来看:
private KafkaConsumer<byte[], byte[]> createConsumer() { // Include any unknown worker configs so consumer configs can be set globally on the worker // and through to the task Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.GROUP_ID_CONFIG, "connect-" + id.connector()); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, Utils.join(workerConfig.getList(WorkerConfig.BOOTSTRAP_SERVERS_CONFIG), ",")); props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArrayDeserializer"); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArrayDeserializer");
props.putAll(workerConfig.originalsWithPrefix("consumer."));
KafkaConsumer<byte[], byte[]> newConsumer; try { newConsumer = new KafkaConsumer<>(props); } catch (Throwable t) { throw new ConnectException("Failed to create consumer", t); }
return newConsumer; } |
是把与同一个Connector关联的Task会分到一个Consumer Group中。如果要自定义并且 组名默认是connector的名字。如果要自定义Consumer的属性,可以在worker 的配置文件中使用consumer.前缀的方式来完成。
通过上述代码也可以看出,默认情况下,关闭了KafkaConsumer的自动提交offset,改为由Kafka Connect来完成offset的提交。
1.4 Converter
WorkerSinkTask、WorkerSourceTask的执行过程中,会使用Key的Converter、Value的Converter对Key、Value进行转换。
这样做的原因:
作为一个Connector框架,在进行producer#send、consumer#pool时,框架本身并不知道用户的程序中,如何进行序列化、反序列化操作。并且进入Kafka的数据类别不同,所需要的序列化、反序列化工具也不同。
为了解决这一问题,Kafka Connect中引入了Converter、Schema。
WorkerSoureTask中,将SourceRecord中的key、value取出来,经过Converter转换(序列化)成byte[],转成ProducerRecord。
for (final SourceRecord record : toSend) { byte[] key = keyConverter.fromConnectData(record.topic(), record.keySchema(), record.key()); byte[] value = valueConverter.fromConnectData(record.topic(), record.valueSchema(), record.value()); final ProducerRecord<byte[], byte[]> producerRecord = newProducerRecord<>(record.topic(), record.kafkaPartition(), key, value); … } |
WorkerSinkTask中,将consumer#poll到的ConsumerRecord转换(反序列化)成SinkRecord:
private void convertMessages(ConsumerRecords<byte[], byte[]> msgs) { for (ConsumerRecord<byte[], byte[]> msg : msgs) { log.trace("Consuming message with key {}, value {}", msg.key(), msg.value()); SchemaAndValue keyAndSchema = keyConverter.toConnectData(msg.topic(), msg.key()); SchemaAndValue valueAndSchema = valueConverter.toConnectData(msg.topic(), msg.value()); messageBatch.add( new SinkRecord(msg.topic(), msg.partition(), keyAndSchema.schema(), keyAndSchema.value(), valueAndSchema.schema(), valueAndSchema.value(), msg.offset()) ); } } |
2、Rebalance
Kafka Connect支持单机、分布式(集群)两种模式。在生产环境下,通常会使用分布式模式。
通常情况下业务系统集群的数据进入到kafka,然后有Consumer集群进行数据消费处理。如果使用Kafka Connect,集群方案就变得很简单并且扩展性很好。
根据前面所了解的内容来看,一个Connector关联到一类Task,可以指定该类Task的数目,也就是Consumer的数目。也就是说一个Connector关联到一个Consumer Group上。如果Worker是单机模式,那么这个Consumer Group内的多个Consumer就是在同一进程内。如果进程挂掉,也就是整个Consumer Group全部挂掉。
在使用集群模式(Worker Cluster)时,这个问题如果不解决,所谓的集群就没有任何意义。所以在Worker Cluster模式时,重点要解决上述问题。
通过之前的Consumer、Broker的学习,知道Broker会为每一个Consumer Group提供一个协处理器。在Broker Coordinator、 Consumer Leader的配合下,当有Consumer 加入或退出Consumer Group时,会对partition进行rebalance。使得partition相对均匀的分配给各个Consumer上,以此来避免出现热点(partition分配给少数的Consumer上)问题。
Kafka Connect在解决这个问题时,也采用了同样的方案:即使用Broker Coordinator 配合 Worker Leader的方式,对connector、task进行rebalance。这样一来,使connector、task相对均匀的分配到同一个Worker Group中的不同的Worker上。如此便可以解决上述问题了。
一个Worker Leader分配connector、task时的处理是:
private Map<String, ByteBuffer> performTaskAssignment(String leaderId,long maxOffset, Map<String, ConnectProtocol.WorkerState> allConfigs) { Map<String, List<String>> connectorAssignments = new HashMap<>(); Map<String, List<ConnectorTaskId>> taskAssignments = newHashMap<>();
// Perform round-robin task assignment CircularIterator<String> memberIt = new CircularIterator<>(sorted(allConfigs.keySet())); for (String connectorId : sorted(configSnapshot.connectors())) { String connectorAssignedTo = memberIt.next(); log.trace("Assigning connector {} to {}", connectorId, connectorAssignedTo); List<String> memberConnectors = connectorAssignments.get(connectorAssignedTo); if (memberConnectors == null) { memberConnectors = new ArrayList<>(); connectorAssignments.put(connectorAssignedTo, memberConnectors); } memberConnectors.add(connectorId);
for (ConnectorTaskId taskId :sorted(configSnapshot.tasks(connectorId))) { String taskAssignedTo = memberIt.next(); log.trace("Assigning task {} to {}", taskId, taskAssignedTo); List<ConnectorTaskId> memberTasks = taskAssignments.get(taskAssignedTo); if (memberTasks == null) { memberTasks = new ArrayList<>(); taskAssignments.put(taskAssignedTo, memberTasks); } memberTasks.add(taskId); } }
this.leaderState = new LeaderState(allConfigs, connectorAssignments, taskAssignments);
return fillAssignmentsAndSerialize(allConfigs.keySet(), ConnectProtocol.Assignment.NO_ERROR, leaderId, allConfigs.get(leaderId).url(), maxOffset, connectorAssignments, taskAssignments); } |
下面这张图展示了某个Worker失败情况下,对Task Rebalance的情况:
一个Connector 1设置了5个task:
第一列表示所有的Worker都正常工作情况下 Task的分配。
第二列表示Worker 2进程出现故障, task2、task3也就挂掉了。
第三列表示对Task进行rebalance后,task2、task3被rebalance到其他的Worker名下了。
3、Storage
3.1 内置Storage
在Kafka Connect中,内置了3种Storages。这三种配置都会(不论是Standalone,还是Distributed)有一份基于内存的Storage。下面只是针对内存Storage以外的方式进行说明:
·status’s storage
存储各个connector、task的状态信息。在Worker分布式的情况下,存储在Kafka 的一个Topic中,topic的名字由worker配置项status.storage.topic来指定。
Status 有5种:
1) UNASSIGNED:connector或者task是否被分配到了个Worker上。
2) RUNNING: connector或者task是否正在运行。
3) PAUSED: connector或者task是否已暂停运行。
4) FAILED: connector或者task是否失败(通常是抛出异常)。
5) DESTROYED:connector或者task是否已被删除。
在运行时,会定时更新状态信息。
·offset’s storage
在Kafka Connect中,WorkerSourceTask 用于从某个源拉数据,然后使用kafkaProducer#send推送到Kafka中。如果一个Connector 配置了多个Task,那么这多个Task应该是从不同的源中取数据。
其中这里说的源可以是多个File,一个或者多个Topic,可以是RDBMS中的多张表,可以是多个Queue等等。
为了记录从哪个源读取数据时读取到了哪个位置,当WorkerSourceTask中断重启时,能够继续从这个位置读取。Kafka Connect 为 SourceRecord设计了 两个字段:sourcePartition, sourceOffset:
public SourceRecord(Map<String, ?> sourcePartition, Map<String, ?> sourceOffset, String topic, Integer partition, Schema valueSchema, Object value) { this(sourcePartition, sourceOffset, topic, partition, null, null, valueSchema, value); }
sourcePartition :用于表示从哪个Source中读取数据。所以它可以是一个File名、某个topic的partition、一个table名 等等。
sourceOffset:用于表示读取到哪个位置。可以它可以是一个File中哪一行、某个topic的partition的哪个offset、table中的第几行 等等。
因为sourcePartition, sourceOffset是可以自定义的,所以它具体的数据类型,并不知道。为了能够方便的进行序列化、反序列化,可以配置internal.key.converter (用于序列化、反序列化sourcePartition), interval.value.converter (用于序列化、反序列化sourceOffset)。
Kafka Connect会 会把sourcePartition, sourceOffset 以 一个key-value 方式(key 是 sourcePartition, value 是sourceOffset)存储起来。单机模式、分布式模式下,都会存储这个offset的。
只是在Standalone模式下,是以本地File方式存储。分布式模式下,是把sourcePartition, sourceOffset做为一条 record存储在Kafka中的一个topic中。Topic的名称由worker配置项offset.storage.topic来指定。
这个offset与Broker中的Log的offset 没有任何的关系。
·config’s storage
在Kafka connect中,每一个Connector,以及与之关联的Task都会有一些配置信息。在rebalance后,还是需要用到这些配置的。为了使得Worker Group内共享配置,也需要对connector、task的配置进行存储。
分布式模式下,会在一个topic中存储。Topic的名称由worker配置项 config.storage.topic来指定。
4、Connector 管理
4.1 Herder
在前面的基本组件中,可以了解到,Worker可以对Connector进行启、停、插、拔。为了更加方便的管理Connector,Kafka-Connect中在Worker之外提供了一个工具Herder(翻译为汉语:牧人,其实也就是管理者的意思)。
使用Herder可以插、拔Connector,可以启、停Task,可以修改Connector的配置,获取connector、task的状态。
针对Standalone、Distributed模式,实现的Herder也是不尽相同的。一些修改性质的操作都是只有Worker Leader能够进行的。所以一个Worker Group中的某个Worker的管理者Herder接收到一个修改的请求时,会将该请求路由(通过Restful请求)到Worker Leader。
在使用Kafka Connect时,如果是将Kafka Connect内嵌到业务系统中时,我们是可以通过Herder 对象来对Kafka Connect中的组件进行管理操作的。
但是很多情况下,并不是将Kafka Connect内嵌到我们的系统中的,而是使用它的CLI独立的启动一个或者多个进程的。这种情况下,如何来管理Kafka Connect的组件呢?
4.2 Restful
为了方便管理,另外也专门提供了一种基于Restful的方式来管理connector、task。不论是Standalone还是Distributed模式都支持Restful方式管理。
Restful管理方式,会在Kafka-Connect进程内部内嵌一个基于Jetty的Web Server。默认的端口是8083。
下面会列出当前Kafka-Connector 版本0.10.1.0支持的Restful请求:
- GET /connectors – 获取当前活动的 connectors列表
- POST /connectors – 创建一个新的Connector。请求体是一个JSON对象,包括connector的名称,和配置。
{ name:”conn-2”, config:{ // connector的配置项。 } } |
- GET /connectors/{name} – 获取指定的connector的信息。
- GET /connectors/{name}/config - 获取指定的connector的配置项。
- PUT /connectors/{name}/config – 修改指定的connector的配置项。
- GET /connectors/{name}/status – 获取指定的connector的当前状态。包括: 1)connector的status, 2)分配在哪个worker上, 3)如果失败的话,错误信息,4)与之关联的所有任务的状态。
- GET /connectors/{name}/tasks – 获取指定connector的任务列表。
- GET /connectors/{name}/tasks/{taskid}/status – 获取指定的connector的指定task的状态信息。包括:1)该任务的status,2)该任务分配在哪个worker上,3)如果有失败,错误信息。
- PUT /connectors/{name}/pause – 暂停某个Connector。暂停某个connector时,与之关联的task都会暂停。
- PUT /connectors/{name}/resume – 继续某个Connector。如果这个connector根本就没有暂停,那就什么也不用做。
- POST /connectors/{name}/restart – 重启一个Connector。通常在一Connector失败时。
- POST /connectors/{name}/tasks/{taskId}/restart – 重启一个task。
- DELETE /connectors/{name} –删除一个Connector。删除Connector时,会停止它关联的task,并删除相关的配置信息。
Kafka Connect 也为获取Connector Plugin的信息提供了 REST API :
- GET /connector-plugins- 获取在改Kafka-Connect集群中安装的Connector Plugin列表。需要注意的是,该API只能获取到正在Worker中正常运行状态的connector。某些connector是看不到的,特别是在进行升级时,例如add一个connector jar包。
- PUT/connector-plugins/{connector-type}/config/validate 验证connector的配置。
4.3 通用Connector升级
目前已提供了很多通用的Connector插件。
地址:https://www.confluent.io/product/connectors/
在使用这些插件时,如果需要升级connector,需要按照下面步骤进行:
1) 下载新版的connector
2) 停止所有的Kafka connect workers。
3)根据connector plugin 的安装说明来安装。
4)启动workers
5)如果采用分布式模式,启动connector。
5、Configuration
5.1 Worker
Worker 可以在Standalone模式运行,也可以在Distributed模式下运行,两种情况下有不同的配置,所以这里需要对他们加以区分:
5.1.1 Common Configuration
·bootstrap.server
因为Worker必然要有至少一个KafkaProducer实例(分布式时有两个),所以需要至少配置一个Kafka broker的host:port对。如果是Broker集群模式,也没有必要将所有的broker配置上。格式是:host1:port1,host2:port2,host3:port3...
·key.converter, value.converter
配置record中的key、value 所采用的converter类名。比较流行的Converter有:JSON、Avro。
·internal.key.converter, internal.value.converter
配置connect-offset topic (也即 offset storage’s topic)中的record的key、value所采用的converter类名。比较流行的Converter有JSON、Avro。
·rest.host.name, rest.port
Rest Server的要绑定的IP和port。其中port模式是8083。
·session.time.out
每一个worker会周期性的发送一个heartbeat到Broker,以告诉Broker自己还活着。如果超过session.timeout.ms 还没有发送heartbeat给Broker,Worker就会断开相关的连接。那么Broker就不能收到heartbeat,就会导致Broker 认为该Worker已经挂掉了。然后Broker就会触发Rebalance,来重新分配connector、task。这个值必须配置的 group.min.session.timeout.ms 与 group.max.session.timeout.ms之间。
默认值:10000,即10s。
·heartbeat.interval.ms
心跳间隔。心跳用于keep-alive Worker与Broker之间的连接。这个值必须低于session.timeout.ms。建议设置的值不要高于session.timeout.ms的1/3。
默认值:3000,即3s 。
. rebalance.timeout.ms
一旦一个rebalance开始了,一个Worker加入到group的时间。在此期间,运行在该Worker上的所有的任务需要flush掉那些还没有处理完毕的数据,并且要提交offset。
如果超过了这个时间,offset commit就会失败。
默认值 60000,即60s 。
·connection.max.idle.ms
连接最大空闲时间。模式值:540000,即9 min。
·client.id
指定一个认为可读的worker id,只是用于跟踪。
5.1.2 Standalone Worker Configuration
·offset.storage.file.filename
存储connector offset 的文件。
5.1.3 Distributed Worker Configuration
·group.id
指定worker属于哪个worker group。
·config.storage.topic, offset.storage.topic, status.storage.topic
在一个Worker Group内, Workers 能够发现彼此并共享connector、Task相关的config、offset、status信息。这些共享信息分别存储在一个topic内。这3个配置项就是用来指定topic名称的。在同一个Worker Group下的每一个worker的配置文件中,这三项必须以一致的。
创建这三个topic时的注意事项:
1)Offset存储的topic 需要有很多个partation并且每一个partition至少三个replicas。
2)config存储的topic 只创建一个partition,并且至少3个replicas。
3)status存储的topic需要创建多个partition并且每一个partition至少3个replicas。
要了解更多信息,可以参考前面Storage部分。
如果想要了解更多关于Kafka-Connect的知识,可以参考:http://docs.confluent.io/current
作者:明翼(XGogo)
-------------
公众号:TSparks
微信:shinelife
扫描关注我的微信公众号感谢
-------------