涛思(TDengine)类型的Flink Source实现及与Kafka的对比分析

1. 涛思(TDengine v3.1)和Kafka订阅的不同之处

  1. 涛思不支持指定分区订阅,只能订阅一个或多个主题。涛思中的vGroup对应Kafka中的Partition概念。对于涛思中的一个消费者组,哪些分区由哪个消费者消费已经平衡再分配由服务端决定。
  2. TaosConsumer没有主动的暂停和恢复订阅的能力。
  3. TaosConsumer没有提供数据抓取和消费相关的度量数据接口。

基于上述的不同,可以发现实现TDengine源节点,应该通过SourceFunction实现,而不应该使用Source来实现。

2. Source理解

要理解Flink的Source,主要看它的方法,通过方法了解它的作用和定位。

接口:org.apache.flink.api.connector.source.Source

public interface Source<T, SplitT extends SourceSplit, EnumChkT>
        extends SourceReaderFactory<T, SplitT> {

    /**
     * 有界数据,还是持续数据流
     */
    Boundedness getBoundedness();

    /**
     * 重点在Split,即源数据是可拆分,并发读取的。
     */
    SplitEnumerator<SplitT, EnumChkT> createEnumerator(SplitEnumeratorContext<SplitT> enumContext)
            throws Exception;

    /**
     * 重新装载数据分拆读取器
     */
    SplitEnumerator<SplitT, EnumChkT> restoreEnumerator(
            SplitEnumeratorContext<SplitT> enumContext, EnumChkT checkpoint) throws Exception;

    // ------------------------------------------------------------------------
    //  序列化源数据
    // ------------------------------------------------------------------------

    /**
     * 客户端这边有分派到哪些任务的数据,需要存储时,凭此方法可以实现个性化的状态数据序列化和反序列化。
     */
    SimpleVersionedSerializer<SplitT> getSplitSerializer();

    /**
     * 客户端这边有状态数据,需要存储时,凭此方法可以实现个性化的状态数据序列化和反序列化。
     */
    SimpleVersionedSerializer<EnumChkT> getEnumeratorCheckpointSerializer();
}

从上述接口,可以总结出使用Source来实现源应该具备以下特征:

  1. 源是可拆分且可并发读取的。
  2. 拆分后的数据读取任务是客户端这边可控的,并且可以伴有任务分派和状态信息以支持任务的重分派。

涛思数据订阅功能,分区和订阅分派是有涛思服务端管理的,各个分区的进度是在涛思服务端存储的,且客户端这边只能以主题级别发起订阅。所以客户端这边缺少任务的调节能力,也没有状态信息需要记录,使用RichParallelSourceFunction来实现是最合适的。

3. TDengine源节点在XSailboat中的实现

类:com.cimstech.sailboat.flink.run.source.taos.TDengineSourceFunction

import java.sql.SQLException;
import java.time.Duration;
import java.util.Collection;

import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.source.RichParallelSourceFunction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.cimstech.xfront.common.collection.PropertiesEx;
import com.cimstech.xfront.common.excep.ExceptionAssist;
import com.taosdata.jdbc.tmq.ConsumerRecord;
import com.taosdata.jdbc.tmq.ConsumerRecords;
import com.taosdata.jdbc.tmq.TaosConsumer;

public class TDengineSourceFunction<O> extends RichParallelSourceFunction<O>
{

	private static final long serialVersionUID = 1L;
	
	private static final long POLL_TIMEOUT = 10_000L;
	
	static final Logger sLogger = LoggerFactory.getLogger(TDengineSourceFunction.class) ;
	
	TaosConsumer<O> mConsumer ;
	
	PropertiesEx mProps ;
	Collection<String> mTopics ;
	
	boolean mCanceled = false ;
	
	public TDengineSourceFunction()
	{
	}
	
	public TDengineSourceFunction(PropertiesEx aProps , Collection<String> aTopics)
	{
		mProps = aProps ;
		mTopics = aTopics ;
	}
	
	@Override
	public void open(Configuration aConf) throws Exception
	{
		RuntimeContext rc = getRuntimeContext() ;
		mProps.setProperty(TMQConstants.CLIENT_ID, mProps.getProperty(TMQConstants.CLIENT_ID) +"_"+rc.getIndexOfThisSubtask()) ;
		mConsumer = new TaosConsumer<>(mProps) ;
		mConsumer.subscribe(mTopics);
	}

	@Override
	public void run(SourceContext<O> aCtx) throws Exception
	{
		try
		{
			while(!mCanceled)
			{
				ConsumerRecords<O> rcds = mConsumer.poll(Duration.ofMillis(POLL_TIMEOUT)) ;
				if(!rcds.isEmpty())
				{
					for(ConsumerRecord<O> rcd : rcds)
					{
						aCtx.collect(rcd.value());
					}
				}
			}
		}
		finally
		{
			try
			{
				mConsumer.close() ;
			}
			catch (SQLException e)
			{
				sLogger.error(ExceptionAssist.getClearMessage(getClass(), e)) ;
			}
		}
	}

	@Override
	public void cancel()
	{
		mCanceled = true ;
	}

}

类:com.cimstech.sailboat.flink.run.source.taos.RowDeserializer

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.flink.types.Row;
import org.apache.flink.types.RowKind;
import org.apache.flink.types.RowUtils;

import com.cimstech.xfront.common.collection.CS;
import com.cimstech.xfront.common.collection.PropertiesEx;
import com.cimstech.xfront.common.log.Assert;
import com.cimstech.xfront.common.reflect.XClassUtil;
import com.taosdata.jdbc.TaosGlobalConfig;
import com.taosdata.jdbc.tmq.Deserializer;
import com.taosdata.jdbc.tmq.DeserializerException;
import com.taosdata.jdbc.tmq.TMQConstants;

public class RowDeserializer implements Deserializer<Row>
{    
    final LinkedHashMap<String, Integer> mFieldIndexMap = CS.linkedHashMap() ;
    String[] mOutFieldNames ;
    Class<?>[] mOutFieldClasses ;

    @Override
    public void configure(Map<?, ?> configs)
    {
        Object encodingValue = configs.get(TMQConstants.VALUE_DESERIALIZER_ENCODING) ;
        if (encodingValue instanceof String)
            TaosGlobalConfig.setCharset(((String) encodingValue).trim());
        
        String outFieldsStr = (String)configs.get("outFields") ;
        mOutFieldNames = PropertiesEx.split(outFieldsStr) ;
        Assert.notEmpty(mOutFieldNames , "没有指定输出列(outFields)!") ;
        
        mOutFieldClasses = new Class[mOutFieldNames.length] ;
        for(int i=0 ; i<mOutFieldNames.length ; i++)
        {
        	String[] segs = mOutFieldNames[i].split(":") ;
        	mOutFieldNames[i] = segs[0] ;		// 字段名
        	mOutFieldClasses[i] = XClassUtil.getClassOfCSN(segs[1]) ;
        	mFieldIndexMap.put(mOutFieldNames[i] , i) ;
        }
    }

    @Override
    public Row deserialize(ResultSet data, String topic, String dbName) throws DeserializerException, SQLException
    {
    	Row row = RowUtils.createRowWithNamedPositions(RowKind.INSERT , new Object[mOutFieldNames.length]
				, mFieldIndexMap) ;
    	for(int i=0 ; i<mOutFieldNames.length ; i++)
    	{
    		row.setField(i, XClassUtil.typeAdapt(data.getObject(mOutFieldNames[i]), mOutFieldClasses[i])) ;
    	}
        return row ;
    }
}

类:com.cimstech.sailboat.flink.run.source.taos.SI_TDengine_Builder (部分代码)

	... 省略
	PropertiesEx props = new PropertiesEx() ;
	props.setProperty(TMQConstants.CONNECT_TYPE , "websocket") ;
	props.setProperty(TMQConstants.BOOTSTRAP_SERVERS , servAddr) ;
	props.setProperty(TMQConstants.CONNECT_USER , username) ;
	props.setProperty(TMQConstants.CONNECT_PASS , password) ;
    props.setProperty(TMQConstants.ENABLE_AUTO_COMMIT, "true");
    props.setProperty(TMQConstants.AUTO_COMMIT_INTERVAL, "1000");
    props.setProperty(TMQConstants.VALUE_DESERIALIZER_ENCODING, "UTF-8");
    props.setProperty(TMQConstants.EXPERIMENTAL_SNAPSHOT_ENABLE, "true");
       
    //
    props.setProperty(TMQConstants.CLIENT_ID, nodeId) ;
    props.setProperty(TMQConstants.GROUP_ID, UUID.randomUUID().toString()) ;
	
	String startingOffsets = execConfJo.optString("startingOffset") ;
	if(XString.isNotEmpty(startingOffsets))
	{
		props.setProperty(TMQConstants.AUTO_OFFSET_RESET, startingOffsets.toLowerCase()) ;
	}
	
	//
	props.setProperty(TMQConstants.VALUE_DESERIALIZER , "com.cimstech.sailboat.flink.run.source.taos.RowDeserializer") ;
	
	JSONArray outRowFieldsJa = execConfJo.optJSONArray("outRowFields") ;
	Assert.notNull(outRowFieldsJa , "没有找到outRowFields!%s" , execConfJo);
	ERowTypeInfo rowTypeInfo = JSONKit.toRowTypeInfo(outRowFieldsJa) ;
	final int fieldAmount = outRowFieldsJa.length() ;
	Assert.isTrue(fieldAmount>0 , "没有找到outRowFields!%s" , execConfJo);
	StringBuilder outFieldsStrBld = new StringBuilder() ;
	outRowFieldsJa.forEachJSONObject(jo->{
		if(outFieldsStrBld.length() > 0)
			outFieldsStrBld.append(',') ;
		 outFieldsStrBld.append(jo.optString("name"))
		 	.append(':')
		 	.append(jo.optString("dataType")) ;
	}) ;
	
	props.setProperty("outFields" , outFieldsStrBld.toString()) ;
	...省略
	TDengineSourceFunction<Row> sourceFunc = new TDengineSourceFunction<Row>(props, Arrays.asList(topicsJa.toStringArray())) ;
	SingleOutputStreamOperator<Row> dss = env.addSource(sourceFunc , nodeName , rowTypeInfo)
				.assignTimestampsAndWatermarks(watermarkStrategy)		// 2023-01-08 这一句是必需的,否则不会产生水位线
				.name(nodeName)
				.uid(nodeId)
				;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Flink KafkaSource 可以通过获取 Kafka Topic 的元数据来实现。元数据包括 Topic 的名称、分区数、每个分区的副本数量和分区的 Leader 等信息。 为了获取元数据,我们可以使用 flink-connector-kafka 库提供的 Kafka Consumer API 来连接到 Kafka 集群。具体步骤如下: 1. 创建 Kafka Consumer:使用 flink-connector-kafka 库提供的 Kafka Consumer API 创建一个 Kafka Consumer 实例。在创建实例时,需要配置 Kafka 集群的地址、Topic 的名称以及其他必要参数。 2. 获取 Kafka Topic 的元数据:通过调用 Kafka Consumer 的 `listTopics()` 方法,可以获取到 Kafka 集群中所有的 Topic 和它们的分区信息。该方法返回一个 Map,其中键是 Topic 的名称,值是一个 TopicPartitionInfo 对象,该对象包含了分区的信息。 3. 解析元数据:遍历上一步获取到的 Map,可以获取每个 Topic 的名称和分区数等信息。通过访问 TopicPartitionInfo 对象的方法,可以获取到每个分区的副本数量和 Leader 等元数据。 4. 处理元数据:根据需要,可以将元数据转化为想要的格式或者进行进一步的处理。例如,可以将元数据存储到数据库或者打印到日志中。 通过以上步骤,我们可以使用 Flink KafkaSource 获取 Kafka Topic 的元数据。这些元数据可以帮助我们了解 Topic 的结构以及分区的情况,从而更好地设计和优化 Flink 程序的处理逻辑。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值