FlinkはDorisのリアルタイムアプリケーションを作成します
ドリスのプライベートチャットを私と深く交換したい場合は、WeChatを追加してください
前書き
リアルタイムのデータウェアハウスを行う学生は、現在人気のあるKFC(Kafka \ Flink \ ClickHouse)パッケージに精通していますが、KFDも優れています。
ビッグデータコンポーネントはますます豊富になっていますが、OLAPおよびOLTPと互換性のあるツール、つまり、DBとログのリアルタイムストレージと複雑なクエリに対応し、の構築に対応するツールはまだありません。これに基づいてデータウェアハウスを試してみました。ClickHouseを試しました。短所保守が難しく、リアルタイムの書き込み効率が低い。内部の断片化とデータ移行を伴う大量のデータのリアルタイムストレージを実現するのが難しい。impala+を使用した後kuduの欠点は、impalaがメモリを大量に消費し、2つの組み合わせが面倒になることです。DBをリアルタイムで同期するためのツールが開発され、メンテナンスコストが高すぎたため、あきらめました。最後に、 Baiduのドリスとジョブヘルプ情報を参照して、私は正式にドリスの使用を開始し、準リアルタイムのログとDB(サブテーブルのマージを含む)の同期、およびドリスに基づくデータウェアハウスモデリングを実現しました。
次に、Dorisのリアルタイム書き込み部分の実装方法、主にコードとコメントについて簡単に説明します。
テーブルデザイン
フィールドを「null以外」として設計しないでください。後でテーブルを変更(フィールドを追加)しても通常のデータに影響がないという利点があります。他の人は一時的に開示するのが不便です。後で説明します。
JSONStreamLoad
StreamLoadを選択する理由 初めに、使用された挿入。挿入をFEがビジーであることを、以降のデータ量に問題があるだろう引き起こし使用FE資源へ。しかし、のStreamloadはこの問題はありません。関係者は(0.12ドキュメント)::
Stream load 中,Doris 会选定一个节点作为 Coordinator 节点。该节点负责接数据并分发数据到其他数据节点。
用户通过 HTTP 协议提交导入命令。如果提交到 FE,则 FE 会通过 HTTP redirect 指令将请求转发给某一个 BE。用户也可以直接提交导入命令给某一指定 BE。
导入的最终结果由 Coordinator BE 返回给用户。
^ +
| |
| | 1A. User submit load to FE
| |
| +--v-----------+
| | FE |
- Return result to user | +--+-----------+
| |
| | 2. Redirect to BE
| |
| +--v-----------+
+---+Coordinator BE| 1B. User submit load to BE
+-+-----+----+-+
| | |
+-----+ | +-----+
| | | 3. Distrbute data
| | |
+-v-+ +-v-+ +-v-+
|BE | |BE | |BE |
+---+ +---+ +---+
その後、Jingdongの慣例を参考にして、小さなファイルの継続的な読み込みとリアルタイムのデータ挿入を実現します。
ピットを踏む:
- 複数の送信を避けるために大量のデータを用意するようにしてください。そうすると、スレッド占有の問題が発生します。
- ロードはDBに基づいており、DBのデフォルトは100スレッドであり、ロードスレッドの数を制御します
- 負荷は非常にメモリを消費します。1つはスレッドで、もう1つはデータのマージです。
- Streaming_load_max_batch_size_mbのデフォルトは100で、ビジネスに応じて変更できます。
- DBデータを同期する場合は、curlのマルチスレッド実行に注意してください。
実装は比較的単純で、flinkSinkコードにcurlを実行するためのコードを埋め込むだけです。
## 原curl
curl --location-trusted -u 用户名:密码 -T /xxx/test -H "format: json" -H "strip_outer_array: true" http://doris_fe:8030/api/{
database}/{
table}/_stream_load
## -u 不用解释了,用户名和密码
## -T json文件的地址,内容为[json,json,json],就是jsonlist
## -H 指定参数
## http 指定库名和表名
手順:一時ファイルの生成、一時ファイルcreateFile
へのデータの書き込みmappedFile
、実行execCurl
、一時ファイルの削除deleteFile
(簡易バージョン)
/**
* 创建临时内存文件
* @param fileName
* @throws IOException
*/
public static void createFile(String fileName) throws IOException {
File testFile = new File(fileName);
File fileParent = testFile.getParentFile();
if (!fileParent.exists()) {
fileParent.mkdirs();
}
if (!testFile.exists())
testFile.createNewFile();
}
/**
* 删除临时内存文件
* @param fileName
* @return
*/
public static boolean deleteFile(String fileName) {
boolean flag = false;
File file = new File(fileName);
// 路径为文件且不为空则进行删除
if (file.isFile() && file.exists()) {
file.delete();
flag = true;
}
return flag;
}
/**
* 写入内存文件
* @param data
* @param path
*/
public static void mappedFile(String data, String path) {
CharBuffer charBuffer = CharBuffer.wrap(data);
try {
FileChannel fileChannel = FileChannel.open(Paths.get(path), StandardOpenOption.READ, StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING);
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, data.getBytes().length*4);
if (mappedByteBuffer != null) {
mappedByteBuffer.clear();
mappedByteBuffer.put(Charset.forName("UTF-8").encode(charBuffer));
}
fileChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 执行curl
* @param curl
* @return
*/
public static String execCurl(String[] curl) {
ProcessBuilder process = new ProcessBuilder(curl);
Process p;
try {
p = process.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
StringBuilder builder = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
builder.append(line);
builder.append(System.getProperty("line.separator"));
}
return builder.toString();
} catch (IOException e) {
System.out.print("error");
e.printStackTrace();
}
return null;
}
/**
* 生成Culr
* @param filePath
* @param databases
* @param table
* @return
*/
public static String[] createCurl(String filePath, String databases, String table){
String[] curl = {
"curl","--location-trusted", "-u", "用户名:密码", "-T",filePath, "-H","format: json", "-H", "strip_outer_array: true", "http://doris_fe:8030/api/"+databases+"/"+table+"/_stream_load"};
return curl;
}
実質的
カスタムシンクを実装するのは比較的簡単です。これは私がそれをどのように書いたかの簡単な共有です(簡略版)。
class LogCurlSink(insertTimenterval:Long,
insertBatchSize:Int) extends RichSinkFunction[(String, Int, Long, String)] with Serializable{
private val Logger = LoggerFactory.getLogger(this.getClass)
private val mesList = new java.util.ArrayList[String]()
private var lastInsertTime = 0L
override def open(parameters: Configuration): Unit ={
val path = s"/tmp/doris/{databases}/{table}/{ThreadId}"
CurlUtils.createFile(path)
Logger.warn(s"init and create $topic filePath!!!")
}
// (topic,partition,offset,jsonstr)
override def invoke(value: (String, Int, Long, String), context: SinkFunction.Context[_]): Unit = {
if(mesList.size >= this.insertBatchSize || isTimeToDoInsert){
//存入
insertData(mesList)
//此处可以进行受到维护offset
mesList.clear()
this.lastInsertTime = System.currentTimeMillis()
}
mesList.add(value._4)
}
override def close(): Unit = {
val path = s"/tmp/doris/{databases}/{table}/{ThreadId}"
CurlUtils.deleteFile(path)
Logger.warn("close and delete filePath!!!")
}
/**
* 执行插入操作
* @param dataList
*/
private def insertData(dataList: java.util.ArrayList[String]): Unit ={
略
}
/**
* 根据时间判断是否插入数据
*
* @return
*/
private def isTimeToDoInsert = {
val currTime = System.currentTimeMillis
currTime - this.lastInsertTime >= this.insertCkTimenterval
}
}