Kafkaを使用して、データベースのリアルタイムBinlogをサブスクライブします

「JavaArt」をフォローして、一緒に成長しましょう!

サブスクリプションBinlogの目的は、リアルタイムのキャッシュ更新、複雑なロジックデータの処理Elasticsearchと他のデータベーステーブルおよび他のビジネスシナリオとのリアルタイム同期を実現することです。

この記事の内容は次のとおりです。

  • アプリケーション層で監視SQL実装する方法

  • 予備知識:Mysql2段階のコミットとBinlog

  • 予備知識:についてKafka

  • アリババクラウドデータ送信サービスDTS-データサブスクリプション

  • 公式DEMO消費モデル:生産->消費モデル

  • 公式にDEMO提供されMetaStoreCheckpoint機能

  • DEMO注意を払うために公式の場所を使用してください

  • avroシリアル化と逆シリアル化について

アプリケーション層でSQLを監視する方法

著者は以前に、MybatisプラグインとSQL解析ツールを使用してアプリケーションのSQLデータを監視および更新することについて記述し、この機能を個人のオープンソースプロジェクトに統合し(easymulti-datasource-spring-boot-starter)、トランザクションの監視をサポートし、リアルタイムの消費をサポートしていますSQL。トランザクションリスナーのコールバックインターフェイスメソッドを登録し、トランザクションがコミットされたときにリスナーの消費を開始しますSQL

easymulti-datasource-spring-boot-starterアプリケーションレベルの簡単なSQLサブスクリプションでリアルタイムに使用することもできますが、この方法ではSQL、コンシューマーのリッスンは非同期ですが、傍受SQL、分析SQL、それ自体もパフォーマンスが少し低下するという欠点もあります。また、同じテーブルを変更するアプリケーションが複数ある場合は、各アプリケーションで消費コードを記述する必要があります。

より低いレベルで直接サブスクライブできる場合MysqlBinlog効率はアプリケーション層で達成される効率よりもはるかに高くなります。

予備知識:Mysqlトランザクションに関する2フェーズコミットとBinlog

Binlogデータベースによって実行された書き込み操作情報を記録するために使用され、バイナリ形式でディスクに保存されます。

BinlogはいMysql、論理ログServerはレイヤーによって記録さます。使用されているストレージエンジンに関係なく、MysqlデータベースはBinlogログを記録します。

Mysqlトランザクションがコミットされたときにのみ記録され、BiglogトランザクションがコミットBiglogされたときにのみメモリに記録され、構成されたフラッシュ戦略によってファイルに書き込まれます。

Mysqlsync_binlogパラメータによって制御されるBiglogディスクのフラッシュタイミング。値の範囲は0-N次のとおりです

  • 0:システムは、ディスクにいつ書き込むかを自分で決定します。

  • 1:毎回ディスクcommitBinlog書き込まれます;

  • N:各NトランザクションcommitBinlogディスクに書き込まれることになっていた。

sync_binlog最も安全な設定は1、これがMySQL 5.7.7後のバージョンのデフォルト値でもあること間違いありません

通常Mysql前述のトランザクションの2フェーズコミットInnoDBストレージエンジンに関連しています。

Mysqlトランザクションは2つの段階でコミットされます。最初の段階はストレージエンジンによって事前に書き込まれます。たとえば、InnoDBストレージエンジンが書き込む場合、Redologこの段階Binlogでは操作は実行されません。2番目の段階は最初に書き込むことBinlogであり、次にストレージエンジンはcommit日記への書き込みなどのトランザクションコミット作業を完了します。 、ロックを解除するなど。

2番目のフェーズでの書き込みがBinlog成功MySQLすると、トランザクションがコミットされて永続化されたと見なされるため、このステップでBinlogサブスクライバー送信できます。書き込み後Binlog、ストレージエンジンがコミットされたトランザクションを完了しておらず、この時点でデータベースがクラッシュした場合でも、再起動後もBinlogトランザクション正しく復元できます。Binlog操作が失敗する前に書き込みステップが完了すると、トランザクションがロールバックされます。

したがって、直接サブスクリプションの場合Binlog、トランザクションが最終的にコミットされるかロールバックされるかを気にする必要はありません。トランザクションがコミットされる前に、トランザクションで実行されるSQL日記をサブスクライブすることはできません

詳細を知りたい場合は、次の記事を読むことをお勧めします:「MySQL ・原則の紹介・再考されたMySQL障害回復」http://mysql.taobao.org/monthly/2018/12/04/

予備知識:カフカについて

データストレージの問題

Kafkaクラスターは、消費されたかどうかに関係なく、公開されたすべてのレコードを保持します。メッセージの保持期間は、保持期間パラメーターを構成することで制御できます。保持ポリシーが2日数に設定されている場合、レコードはリリースされてから2日以内であればいつでも消費できます。2日後、レコードは破棄され、ディスク領域が解放されます。

オフセット消費オフセット

オフセットは消費者によって制御されます。消費者commitが新しいオフセットを記録し後、オフセットkafkaは消費者のために保存され、その後の消費を容易にします。オフセットkafkagroup + topic + partition保存されます。もちろん、自分で保存することもできます。オフセットを自分で保存する際に注意が必要な問題については、後で説明します。

起因kafkaするgroup + topic + partitionオフセット記憶、これはまた別の問題に対応する:「同じグループで、topicそれぞれのそれぞれがpartition唯一消費する一つの消費者を有することができるが、1つの消費者は、同時に複数の消費することができますpartition」。

offset消費者によって制御されるため、消費者は任意の順序でレコードを消費できます。つまりtopic、消費者の消費者は古いオフセットにリセットして、過去のデータを再処理したり、スキップしたりできます。最新のレコードは、現在の場所から消費を開始します。

消費者

KafkaConsumerインスタンスは、必ずしも消費者に等しくありません。

ではsubscribeモデル、1つのKafkaConsumerインスタンスは、1つのコンシューマに等しいです。パーティションが1つだけで、複数が開いていると仮定するとKafkaConsumer、アイドル状態のコンシューマーが存在します。つまり、このスレッドがKafkaConsumerインスタンスpollメソッドを呼び出すたび常に空が返され、現在消費している永続的なKafkaConsumer接続が切断されるまでメッセージはプルされません。リバランス後、アイドル状態の消費者はレコードをプルできるようになります。

これは、この文も確認します。Kafka消費を実現する方法は、ログ内のパーティションを各コンシューマーインスタンスに分割することです。これにより、いつでも、各コンシューマーが特定のパーティションの唯一のコンシューマーになります。

subscribeモデルKafkaConsumer、(一つの接続Socket)は、一人の消費者に等しくなく、1

ただし、このassignモードでは、複数のKafkaConsumerサブスクリプションが指定されtopicてパーティション化されている場合(および同じグループ内)、これらのKafkaConsumerプルレコードはすべて同じパーティションになります。これは単なる例です。このように使用しないでください。使用すると、レコードが繰り返し消費され、2つのスレッドがコミット(commit)とオフセット(offset)をクロスするときに問題が発生ます。

消費者グループ

通常の状況では、それぞれtopicにいくつかのコンシューマーグループがあり、コンシューマーグループは論理サブスクライバーです。

例えば:

topic:用户注册
group 1:短信推送服务订阅者
group 2:邮件推送服务订阅者

group1合計group2は論理サブスクライバーですが、各論理サブスクライバーは複数のコンシューマーを持つことができます。

同じグループ内の消費者の数はtopic、そのpartition数を超えてはpartitionなりませんpartition。これは、そのを超える消費者割り当てられない、つまりアイドル状態になるためです(「消費者」の説明を参照)。

消費者グループ内の消費者関係の維持は、Kafka契約によって動的に処理されます。新しい消費者がグループに参加すると、新しく追加された消費者は、グループの他のメンバーから一部のpartitionパーティションを引き継ぎます。消費者が消えると、消費者は所有します。パーティションは、他の残りのコンシューマーに再割り当てされます。

もう1つのポイントは、同じグループで、topic各地区で現在消費している消費者がいる場合、新しく追加された消費者が消費している消費者を置き換え、置き換えられた消費者が消費する地区を引き継ぐことです。

アリババクラウドデータ送信サービスDTS-データサブスクリプション

アリクラウドデータ伝送サービスのDTSサポートMySQLリアルタイムのサブスクリプション。DRDSBinlog

公式SDKサブスクリプションを使用する必要はありませんがサブスクリプション実装BinlogするためにKafkaクライアントKafka使用するだけで済みます。APIBinlog

公式ドキュメント:Kafkaクライアントを使用してサブスクリプションデータを使用するhttps://help.aliyun.com/document_detail/121239.html?spm=a2c4g.11186623.6.785.6d4d6d2aIOqQQm

公式に提供されたものDEMO:[subscribe_example] https://github.com/LioRoger/subscribe_example、これは上司DEMOによって龙玄提供されるべきです。

ホイールを繰り返すのではなく、公式のDEMO[subscribe_example / javaimpl]Mysql Binlog基づいてリアルタイムサブスクリプションサービス(トライアルフェーズ)構築することを選択しましたただし、メッセージの逆シリアル化MetaStoreCheckpoint機能を維持したまま、ソースコードにいくつかの変更を加えましたどこMetaStoreCheckpointDEMO最も勉強する価値があります。

公式デモの消費モデル:生産->消費モデル

DEMO開かれるコンシューマーは1つだけです。このコンシューマーは、メッセージのサブスクライブを担当し、サブスクライブされたメッセージをブロッキングキュー(LinkedBlockingQueue)に入れます。このブロッキングキューのデフォルトサイズはに設定されてい512ます。

また、ブロックキューからメッセージを読み、呼び出し、実際にメッセージを消費することスレッドを開始し、後に消費する方法を 、メッセージを消費し、ラップメッセージを(チェックポイントに)、および最新のチェックにチェックポイントを設定しますポイントに加えて、送信された最新のチェックポイントが送信秒ごとに時間指定されたタスクがありますRecordListenerconsumeRecordListeneroffsetCheckpoint5offset

kafka消費者は毎回メッセージのバッチをプルすることができ、これらのメッセージはリリース順にソートされます。そのためtopic、パーティションが一つだけのコンシューマによって消費することができ、パーティション内のメッセージは、それらが公開された順序でソートされています。

ではDEMO、コンシューマーはサブスクライブされたメッセージを順番にブロックキューに入れ、キューがいっぱいになるとブロックして待機するため、ブロックキュー内のメッセージが順番に消費されて送信されることを確認するだけで済みますoffset

ブロッキングキュー内のメッセージが順不同で消費された場合はどうなりますか?

複数のスレッドがプルされたメッセージを並行して順序どおりに消費しないと仮定すると、それらoffsetが正しく送信されたことを確認することは不可能であり、その結果、一部のメッセージが繰り返し消費される可能性があります。

各メッセージを正しく消費する必要があるという厳密な要件がなく、例外なく、マルチスレッド消費を使用してメッセージ消費の速度を上げることができます。

たとえば、ブロッキングキュー内のメッセージを消費するスレッドは、ブロッキングキューからのメッセージの取得と解析のみを担当します。キャッシュの更新などの他のアクションは、実行のために非同期スレッドプールに配置されます。非同期スレッドプールに正常に配置されている限り、update Checkpointoffset)、続行します。消費者ニュース。

公式デモが提供するメタストアとチェックポイントの機能

Checkpointこれは、グループ内のtopic特定のパーティション現在の実際の消費位置を記録するために使用されます(オフセット:) offset

/**
 * 安全检查点(即:记录消费位置)
 */
public class Checkpoint {
    // 分区信息
    private final TopicPartition topicPartition;
    private final long timeStamp;
    private final long offset;
    public Checkpoint(TopicPartition topicPartition, long timeStamp, long offset) {
        this.topicPartition = topicPartition;
        this.timeStamp = timeStamp;
        this.offset = offset;
    }
}

MetaStoreストレージCheckpoint、または送信オフセットに使用されます

public interface MetaStore<V> {
    Future<V> serializeTo(TopicPartition topicPartition, String group, V value);
    V deserializeFrom(TopicPartition topicPartition, String group);
}

DEMO2つの実装クラスが提供されています:KafkaMetaStoreLocalFileMetaStore何されてLocalFileMetaStore達成することは、消費パーティションのオフセット、および保存するために、ローカルファイルを使用することですメソッドKafkaMetaStoreと呼ばれるがKafkaConsumerしているcommitAsyncことを、非同期的にオフセットされ手段提出kafkaオフセットが格納されているが。

このsubscribeモードでは、を使用しないでくださいLocalFileMetaStore

コンシューマーがクラスターにデプロイされると、ノードの再起動後kafkaのリバランスにより、ノードによって消費されるパーティションが再起動前のパーティションと異なる場合があるため、ローカルファイルストレージの消費オフセットが使用されず、再起動が発生します(構成済み)消費場所の初期化)消費記録を開始します。

また、1つのコンシューマーサービスのみをデプロイする場合、または複数のコンシューマーがプロセス中または使用assignモードにある場合は、それを使用できますがLocalFileMetaStore、サービスを再起動するたびにオフセットファイルがあることを確認する必要があります。サーバーデプロイメントを切り替える場合は、オフセットファイルを新しいサーバーに同期する必要があります。

不要な手間を省くため、直接捨てLocalFileMetaStoreて使用していKafkaMetaStoreます。

public class KafkaMetaStore implements MetaStore<Checkpoint> {

    private volatile KafkaConsumer kafkaConsumer;
    //.....
    // 异步提交offset
    @Override
    public Future<Checkpoint> serializeTo(TopicPartition topicPartition, String group, Checkpoint value) {
        KafkaFutureImpl ret = new KafkaFutureImpl();
        if (null != kafkaConsumer) {
            OffsetAndMetadata offsetAndMetadata = new OffsetAndMetadata(value.getOffset(), String.valueOf(value.getTimeStamp()));
            // 异步提交(不能同步提交,否则影响RecordGenerator#run())
            // Notice: commitAsync is only put commit offset request to sending queue, the future  result will be driven by KafkaConsumer.poll() function
            // So if you only call this method but not poll, you may not wait offset commit call back
            kafkaConsumer.commitAsync(Collections.singletonMap(topicPartition, offsetAndMetadata), new OffsetCommitCallback() {
                @Override
                public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
                    if (null != exception) {
                        log.warn("KafkaMetaStore: Commit offset for group[" + group + "] topicPartition[" + topicPartition.toString() + "] " +
                                value.toString() + " failed cause " + exception.getMessage(), exception);
                        ret.completeExceptionally(exception);
                    } else {
                        log.debug("KafkaMetaStore:Commit offset success for group[{}] topicPartition [{}] {}", group, topicPartition, value);
                        ret.complete(value);
                    }
                }
            });
        } else {
            log.warn("KafkaMetaStore: kafka consumer not set, ignore report");
            ret.complete(value);
        }
        return ret;
    }
    // 从kafka获取当前分区的offset和时间戳
    @Override
    public Checkpoint deserializeFrom(TopicPartition topicPartition, String group) {
        if (null != kafkaConsumer) {
            OffsetAndMetadata offsetAndMetadata = kafkaConsumer.committed(topicPartition);
            if (null != offsetAndMetadata) {
                return new Checkpoint(topicPartition, Long.valueOf(offsetAndMetadata.metadata()), offsetAndMetadata.offset(), offsetAndMetadata.metadata());
            } else {
                return null;
            }
        } else {
            log.warn("KafkaMetaStore: kafka consumer not set, ignore fetch offset");
            throw new KafkaException("KafkaMetaStore: kafka consumer not set, ignore fetch offset for group[" + group + "] and tp [" + topicPartition + "]");
        }
    }
}

公式デモを使用する際の注意点

topic特定のパーティションが一度も消費されていない場合は、コンシューマーを初めて起動するときに、最初の消費場所を構成する必要があります。タイムスタンプを使用するかoffset、消費する場所を見つけることができます

パーティションが消費されている場合、コンシューマーが開始/再起動すると、最後の消費の場所が最初に取得され、次に最後の消費の場所から消費が開始されます。ただし、offset5送信のタイミングは1秒に1回であるためoffset、実際の消費量のオフセットを表すものではなく、記録を再開するたびに再消費量が発生します。これは、独自の電力消費量やその他のメッセージを確認する必要があります。

DEMOデフォルトLocalFileMetaStore公式に使用され、置換MetaStoreRecordGenerator#getConsumerWrapメソッドを変更するだけで済みます。コードは次のとおりです。

public class RecordGenerator{
    private ConsumerWrap getConsumerWrap(String message) {
        // KafkaConsumer包装器
        ConsumerWrap kafkaConsumerWrap = getConsumerWrap();
        // 不建议使用LocalFileMetaStore存储(特别是部署到k8s上),否则将消费者部署到其它服务器后,需要将localCheckpointStore文件也要同步过去才可以
        // metaStoreCenter.registerStore(LOCAL_FILE_STORE_NAME, new LocalFileMetaStore(LOCAL_FILE_STORE_NAME));
        // 使用KafkaMetaStore
        metaStoreCenter.registerStore(KAFKA_STORE_NAME, new KafkaMetaStore(kafkaConsumerWrap.getRawConsumer()));
        // 从检查点存储器获取检查点(由于是每5秒提交一次,所以每次重起都会有小部分记录被重新消费)
        Checkpoint checkpoint = getCheckpoint();
        // 没有找到检查点,则使用配置的初始化检查点
        if (null == checkpoint || Checkpoint.INVALID_STREAM_CHECKPOINT == checkpoint) {
            checkpoint = initialCheckpoint; // 在配置文件中配置
            log.info("RecordGenerator: use initial checkpoint [{}] to start", checkpoint);
        } else {
            log.info("RecordGenerator: load checkpoint from checkpoint store success, current checkpoint [{}]", checkpoint);
        }
        //.......
    }
}

最後に、Alibaba Cloud Data Transfer Service DTS-Data Subscriptionは日記を1つのパーティションにのみ送信するため、つまりtopic、パーティションは1つだけです。これは、各sql日記を正しい順序で使用できるようにするためです。したがって、subscribeパターンを使用する必要はなく、パターンを使用する必要がありますassign。また、クラスターを展開する必要もありません。これは、公式のDEMOでも推奨されています。

DefaultConsumerWrapカプセル化されKafkaConsumer、使用assignパターンassignTopicこのクラスのメソッドで表現さ、コードは次のとおりです。

public class DefaultConsumerWrap extends ConsumerWrap {
    
    private KafkaConsumer<byte[], byte[]> consumer;
	
    @Override
    public void assignTopic(TopicPartition topicPartition, Checkpoint checkpoint) {
        // KafkaConsumer
        consumer.assign(Collections.singletonList(topicPartition));
        log.info("RecordGenerator:  assigned for {} with checkpoint {}", topicPartition, checkpoint);
        // 设置消费位置
        setFetchOffsetByTimestamp(topicPartition, checkpoint);
    }
    
}

その中で、assignTopicメソッドの2番目のパラメーター(CheckpointMetaStoreは、構成の初期位置から取得されるか、構成の初期位置です。KafkaConsumer#assignメソッドを呼び出した後、メソッドを呼び出してsetFetchOffsetByTimestamp消費場所を設定すると、KafkaConsumer#pollメソッドを呼び出してメッセージをプルできます。

setFetchOffsetByTimestampメソッドは次のように実装されています。DEMOソースコードと比較して、いくつかの変更を加えました。

public class DefaultConsumerWrap extends ConsumerWrap {
 	
    @Override
    public void setFetchOffsetByOffset(TopicPartition topicPartition, Checkpoint checkpoint) {
        // 移动到指定位置继续消费
        consumer.seek(topicPartition, checkpoint.getOffset());
    }

    // recommended
    @Override
    public void setFetchOffsetByTimestamp(TopicPartition topicPartition, Checkpoint checkpoint) {
        // 优先使用偏移量
        if (checkpoint.getOffset() > 0) {
            setFetchOffsetByOffset(topicPartition, checkpoint);
            return;
        }
        long timeStamp = checkpoint.getTimeStamp();
        // 根据时间戳获取偏移量
        Map<TopicPartition, OffsetAndTimestamp> remoteOffset = consumer.offsetsForTimes(Collections.singletonMap(topicPartition, timeStamp));
        OffsetAndTimestamp toSet = remoteOffset.get(topicPartition);
        if (null == toSet) {
            throw new RuntimeException("RecordGenerator:seek timestamp for topic [" + topicPartition + "] with timestamp [" + timeStamp + "] failed");
        }
        // 移动到指定位置继续消费
        consumer.seek(topicPartition, toSet.offset());
    }
 }

avroのシリアル化と逆シリアル化について

公式democom.alibaba.dts.formats.avroこれがpackageある次のように来て、コンパイル、我々はあなた自身の特定の実装をコンパイルすることができますは、次のとおりです。avroshcema

1.コマンドを実行してavscファイルをコンパイルし、javaコードを生成します

java -jar avro/avro-tools-1.8.2.jar compile -string schema avro/Record.avsc .

2、現在のプロジェクトルートディレクトリの生成されたcom.alibaba.dts.formats.avroこのpackageコピーは、もちろん、メインモジュールに組み込まれたモジュールにカプセル化することもできます。

[Javaアート] WeChat ID:javaskill

オリジナルの記事のみをプッシュし、Javaバックエンド関連のテクノロジーを共有するテクニカルパブリックアカウント。

おすすめ

転載: blog.csdn.net/baidu_28523317/article/details/109685442