FlinkはDorisのリアルタイムアプリケーションを作成します

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
  }

}

おすすめ

転載: blog.csdn.net/jklcl/article/details/112851685