【技術乾貨】程式碼示例:使用 Apache Flink 連線 TDengine
小 T 導讀:想用 Flink 對接 TDengine?保姆級教程來了。
0、前言
TDengine 是由濤思資料開發並開源的一款高效能、分散式、支援 SQL 的時序資料庫(Time-Series Database)。
除了核心的時序資料庫功能外,TDengine 還提供快取、資料訂閱、流式計算等大資料平臺所需要的系列功能。但是很多小夥伴出於架構的考慮,還是需要將資料匯出到 Apache Flink、Apache Spark 等平臺進行計算分析。
為了幫助大家對接,我們特別推出了保姆級課程,包學包會。
1、技術實現
Apache Flink 提供了 SourceFunction 和 SinkFunction,用來提供 Flink 和外部資料來源的連線,其中 SouceFunction 為從資料來源讀取資料,SinkFunction 為將資料寫入資料來源。 與此同時,Flink 提供了 RichSourceFunction 和 RichSinkFunction 這兩個類(繼承自open(Configuration)
)和銷燬方法(close()
)。 通過重寫這兩個方法,可以避免每次讀寫資料時都重新建立連線。
2、程式碼實現
完整原始碼:https://github.com/liuyq-617/TD-Flink
程式碼邏輯:
1) 自定義類 SourceFromTDengine
用途:資料來源連線,資料讀取
package com.taosdata.flink;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.source.RichSourceFunction;
import com.taosdata.model.Sensor;
import java.sql.*;
import java.util.Properties;
public class SourceFromTDengine extends RichSourceFunction<Sensor> {
Statement statement;
private Connection connection;
private String property;
public SourceFromTDengine(){
super();
}
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
String driver = "com.taosdata.jdbc.rs.RestfulDriver";
String host = "u05";
String username = "root";
String password = "taosdata";
String prop = System.getProperty("java.library.path");
Logger LOG = LoggerFactory.getLogger(SourceFromTDengine.class);
LOG.info("java.library.path:{}", prop);
System.out.println(prop);
Class.forName( driver );
Properties properties = new Properties();
connection = DriverManager.getConnection("jdbc:TAOS-RS://" + host + ":6041/tt" + "?user=root&password=taosdata"
, properties);
statement = connection.createStatement();
}
@Override
public void close() throws Exception {
super.close();
if (connection != null) {
connection.close();
}
if (statement != null) {
statement.close();
}
}
@Override
public void run(SourceContext<Sensor> sourceContext) throws Exception {
try {
String sql = "select * from tt.meters";
ResultSet resultSet = statement.executeQuery(sql);
while (resultSet.next()) {
Sensor sensor = new Sensor( resultSet.getLong(1),
resultSet.getInt( "vol" ),
resultSet.getFloat( "current" ),
resultSet.getString( "location" ).trim());
sourceContext.collect( sensor );
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void cancel() {
}
}
2) 自定義類 SinkToTDengine
用途:資料來源連線,資料寫入
SinkToTDengine
package com.taosdata.flink;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;
import com.taosdata.model.Sensor;
import java.sql.*;
import java.util.Properties;
public class SinkToTDengine extends RichSinkFunction<Sensor> {
Statement statement;
private Connection connection;
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
String driver = "com.taosdata.jdbc.rs.RestfulDriver";
String host = "TAOS-FQDN";
String username = "root";
String password = "taosdata";
String prop = System.getProperty("java.library.path");
System.out.println(prop);
Class.forName( driver );
Properties properties = new Properties();
connection = DriverManager.getConnection("jdbc:TAOS-RS://" + host + ":6041/tt" + "?user=root&password=taosdata"
, properties);
statement = connection.createStatement();
}
@Override
public void close() throws Exception {
super.close();
if (connection != null) {
connection.close();
}
if (statement != null) {
statement.close();
}
}
@Override
public void invoke(Sensor sensor, Context context) throws Exception {
try {
String sql = String.format("insert into sinktest.%s using sinktest.meters tags('%s') values(%d,%d,%f)",
sensor.getLocation(),
sensor.getLocation(),
sensor.getTs(),
sensor.getVal(),
sensor.getCurrent()
);
statement.executeUpdate(sql);
} catch (Exception e) {
e.printStackTrace();
}
}
}
3) 自定義類 Sensor
用途:定義資料結構,用來接受資料
package com.taosdata.model;
public class Sensor {
public long ts;
public int val;
public float current;
public String location;
public Sensor() {
}
public Sensor(long ts, int val, float current, String location) {
this.ts = ts;
this.val = val;
this.current = current;
this.location = location;
}
public long getTs() {
return ts;
}
public void setTs(long ts) {
this.ts = ts;
}
public int getVal() {
return val;
}
public void setVal(int val) {
this.val = val;
}
public float getCurrent() {
return current;
}
public void setCurrent(float current) {
this.current = current;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
@Override
public String toString() {
return "Sensor{" +
"ts=" + ts +
", val=" + val +
", current=" + current +
", location='" + location + '\'' +
'}';
}
}
4) 主程式類 ReadFromTDengine
用途:呼叫 Flink 進行讀取和寫入資料
package com.taosdata;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.streaming.api.datastream.DataStream;
import com.taosdata.model.Sensor;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
public class ReadFromTDengine {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<Sensor> SensorList = env.addSource( new com.taosdata.flink.SourceFromTDengine() );
SensorList.print();
SensorList.addSink( new com.taosdata.flink.SinkToTDengine() );
env.execute();
}
}
3、簡單測試 RESTful 介面
1) 環境準備:
a) Flink 安裝&啟動:
- wget https://dlcdn.apache.org/flink/flink-1.14.3/flink-1.14.3-bin-scala_2.12.tgz
- tar zxf flink-1.14.3-bin-scala_2.12.tgz -C /usr/local
- /usr/local/flink-1.14.3/bin/start-cluster.sh
b) TDengine Database 環境準備:
- 建立原始資料:
- create database tt;
- create table `meters` (`ts` TIMESTAMP,`vol` INT,`current` FLOAT) TAGS (`location` BINARY(20));
- insert into beijing using meters tags(‘beijing’) values(now,220,30.2);
- 建立目標資料庫表:
- create database sinktest;
- create table `meters` (`ts` TIMESTAMP,`vol` INT,`current` FLOAT) TAGS (`location` BINARY(20));
2) 打包編譯:
原始碼位置: https://github.com/liuyq-617/TD-Flink
mvn clean package
3) 程式啟動:
flink run target/test-flink-1.0-SNAPSHOT-dist.jar
- 讀取資料
- vi log/flink-root-taskexecutor-0-xxxxx.out
- 檢視到資料列印:Sensor{ts=1645166073101, val=220, current=5.7, location=’beijing’}
- 寫入資料
- show sinktest.tables;
- 已經建立了beijing 子表
- select * from sinktest.beijing;
- 可以查詢到剛插入的資料
- show sinktest.tables;
4、使用 JNI 方式
舉一反三的小夥伴此時已經猜到,只要把 JDBC URL 修改一下就可以了。
但是 Flink 每次分派作業時都在使用一個新的 ClassLoader,而我們在計算節點上就會得到“Native library already loaded in another classloader”錯誤。
為了避免此問題,可以將 JDBC 的 jar 包放到 Flink 的 lib 目錄下,不去呼叫 dist 包就可以了。
- cp taos-jdbcdriver-2.0.37-dist.jar /usr/local/flink-1.14.3/lib
- flink run target/test-flink-1.0-SNAPSHOT.jar
5、小結
通過在專案中引入 SourceFromTDengine 和 SinkToTDengine 兩個類,即可完成在 Flink 中對 TDengine 的讀寫操作。後面我們會有文章介紹 Spark 和 TDengine 的對接。
注:文中使用的是 JDBC 的 RESTful 介面,這樣就不用在 Flink 的節點安裝 TDengine,JNI 方式需要在 Flink 節點安裝 TDengine Database 的客戶端。
想了解更多 TDengine Database的具體細節,歡迎大家在GitHub上檢視相關原始碼。