なぜ読み書きを分ける必要があるのでしょうか? 個人的には、ビジネスがある程度の規模に発展すると、技術アーキテクチャの改革が促進されると感じています。読み取りと書き込みの分離により、単一サーバーの負荷が軽減され、読み取りリクエストと書き込みリクエストが別のサーバーにオフロードされ、サーバーの負荷が分散されます。単一のサービスにより、可用性が向上し、読み取りリクエストのパフォーマンスが向上します。
上の図は、基本的な Mysql マスター/スレーブ アーキテクチャです。1 つのマスター、1 つのスタンバイ、および 3 つのスレーブです。このアーキテクチャは、クライアントによって能動的に実行されるロード バランシングであり、データベースの接続情報は通常、クライアントの接続層に配置されます。つまり、クライアントが読み取りおよび書き込み用のデータベースを選択します。
上図はプロキシを使用したマスター/スレーブ アーキテクチャを示しており、クライアントはプロキシに接続するだけで、プロキシはリクエストの種類とコンテキストに基づいてリクエストの配布ルートを決定します。
2 つのアーキテクチャ ソリューションのそれぞれの特徴は次のとおりです。
クライアント直接接続アーキテクチャでは、プロキシ転送の層が 1 つ少ないため、クエリのパフォーマンスが向上し、アーキテクチャがシンプルで問題のトラブルシューティングが簡単です。ただし、このアーキテクチャでは、バックエンド展開の詳細を理解する必要があるため、クライアントはライブラリの移行時にアクティブ/スタンバイの切り替えを認識し、ライブラリの接続情報を調整する必要があります。
プロキシを使用したアーキテクチャはクライアントにとってよりフレンドリーです。クライアントはバックエンド展開の詳細を知る必要はありません。接続維持とバックエンド情報維持はすべてプロキシによって完了します。このようなアーキテクチャでは、バックエンドの運用および保守チームに対する要件が比較的高く、プロキシ自体も高可用性を必要とするため、アーキテクチャ全体が比較的複雑になります。
ただし、どのアーキテクチャが使用されていても、マスターとスレーブ間の遅延により、トランザクションの更新が完了した直後に読み取りリクエストが開始されます。スレーブ ライブラリからの読み取りを選択した場合、スレーブ ライブラリからの読み取りが行われる可能性が非常に高くなります。トランザクション更新前の状態 この種の読み取りを参照します このリクエストは期限切れ読み取りと呼ばれます。マスタースレーブ遅延が発生する状況は数多くあります。興味のある学生は自分で学ぶことができます。マスタースレーブ遅延に対処する戦略もありますが、100% 回避することはできません。これらは私たちの議論の範囲ではありません。主にマスターとスレーブの遅延が発生した場合の対処方法について説明します。マスターとスレーブの遅延があり、読み取る内容はたまたまスレーブ データベースからのものです。これにどのように対処する必要がありますか?
まず、対処法をまとめておきます。
メインライブラリーを強制的に占拠される
睡眠計画
マスターとスレーブの判断に遅れはありません
メイン倉庫の場所を待っています
GTIDスキームを待っています
次に、上記の解決策を基に、それらをどのように実装するか、どのような問題があるのかを 1 つずつ説明します。
マスター/スレーブ遅延ソリューションの紹介を始める前に、マスター/スレーブ同期について簡単に確認してみましょう。
上の図は、ノード A からノード B に更新ステートメントを同期する完全なプロセスを示しています。
スタンバイ データベース B とメイン データベース A は長時間の接続を維持します。メイン データベース A 内には、スタンバイ データベース B の接続を提供するために特別に使用されるスレッドがあります。トランザクション ログ同期の完全なプロセスは次のとおりです。
-
スタンバイデータベース B で change master コマンドを使用して、プライマリ データベース A の IP、ポート、ユーザー名、パスワード、およびバイナリ ログの要求元の場所を設定します。この場所には、ファイル名とログ オフセットが含まれます。 -
スタンバイ データベース B でスレーブ開始コマンドを実行します。このとき、スタンバイ データベースは 2 つのスレッド (図の io_thread と sql_thread) を開始します。 -
このうち、io_thread はメイン ライブラリとの接続を確立する役割を果たします。 -
メイン データベース A は、ユーザー名とパスワードを検証した後、スタンバイ データベース B から渡された場所に従ってローカルでバイナリ ログの読み取りを開始し、それを Bに送信します。スタンバイ データベース B はバイナリ ログを取得した後、それをリレー ログと呼ばれるローカル ファイルに書き込みます。 -
sql_thread は転送ログを読み取り、ログ内のコマンドを解析して実行します。
上図の赤い矢印は、同時実行性を色深度で表すと、色が濃いほど同時実行性が高いことを示しているため、マスタ・スレーブ遅延時間の長さはスタンバイデータベース同期スレッドの速度に依存します。リレーログ(図中のリレーログ)を実行します。マスター/スレーブ遅延の考えられる理由を要約すると、次のようになります。
メイン データベースは、同時実行性、TPS が高く、スタンバイ データベースに対する負荷が高く、ログの実行が遅いです。
大規模なトランザクションの場合、トランザクションはメイン データベースで 5 秒間実行され、その後同じトランザクションがスタンバイ データベースで 5 秒間実行されます (たとえば、一度に大量のデータを削除する場合や、大規模なテーブル DDL など)。すべて大規模なトランザクションです。
ライブラリからの並列コピー機能Msyql5.6 より前のバージョンは、上の図のモデルである並列コピーをサポートしていません。並列レプリケーションもより複雑なので、ここでは詳しく説明しません。ご自身で確認してください。
1. メインライブラリを強制的に使用する
この解決策は、リクエストを分類することです。リクエストは通常、次の 2 つのカテゴリに分類できます。
最新の結果を取得する必要があるリクエストの場合は、メイン ライブラリの使用を強制できます。
古いデータを読み取ることができるリクエストの場合は、スレーブ ライブラリに割り当てることができます。
この解決策は最も単純な解決策ですが、この解決策の欠点の 1 つは、すべてのリクエストを期限切れの読み取りリクエストにすることができないため、すべてのプレッシャーが再びメイン ライブラリに加わり、読み取りと書き込みの分離と拡張を諦めなければならないことです。セックス
2.睡眠計画
スリープの解決策は、スレーブ データベースにクエリを実行する前に、これと同様のコマンドである select sleep(1) を毎回実行することですが、この方法には 2 つの問題があります。
マスターとスレーブの遅延が 1 秒より大きい場合でも、期限切れステータスが読み取られます。
このリクエストがライブラリから結果を 0.5 秒で取得できる場合でも、1 秒待つ必要があります。
このソリューションは非常に信頼性が低く、専門的ではないように見えますが、使用シナリオはあります。
以前プロジェクトに取り組んでいたとき、最初にメイン ライブラリを作成し、作成後に MQ メッセージを送信し、コンシューマがメッセージを受信した後、クエリ インターフェイスを呼び出してデータを確認するというシナリオがありました。 , 私たちもそれを読みました。書き込み分離モードでは、データが見つかりません。このとき、コンシューマーは 30 ミリ秒遅らせるなど、メッセージの消費を遅らせることをお勧めします。その後、問題は解決されます。この方法は同様です呼び出し元にスリープが設定されることを除いて、スリープ ソリューションに適用されます。
3. マスター/スレーブの遅延なし計画を決定する
1) コマンド判定
show slide status では、このコマンドはスレーブ データベース上で実行されます。実行結果には、 Seconds_behind_master フィールドがあります。このフィールドは、マスターとスレーブの遅延を秒単位で示します。単位は秒であることに注意してください。したがって、現在の値が 0 であるかどうかを判断し、0 である場合は直接クエリを実行して結果を取得し、0 でない場合はマスターとスレーブの遅延が 0 になるまで待ちます。
この値は第 2 レベルですが、一部のシナリオはミリ秒レベルのリクエストであるため、この方法による判断は特に正確ではありません。
2) マスターとスレーブを決定するための位置の比較に遅延はありません
上の図は、show smile status を 1 回実行した結果の一部です。
Master_Log_File および Read_Master_Log_Pos は、読み取られたマスター ライブラリの最新の位置を表します。
Relay_Master_Log_File および Exec_Master_Log_Pos は、スタンバイ データベース実行の最新の位置を表します。
Master_Log_File と Relay_Master_Log_File、Read_Master_Log_Pos と Exec_Master_Log_Pos の 2 つの値セットが完全に一致している場合、マスターとスレーブの間に遅延がないことを意味します。
3) GTID を比較して、マスターとスレーブの間に遅延がないことを確認します。
Auto_Position: GTID プロトコルがマスターとスレーブのペア間で有効であることを示します。
Retrieved_Gtid_Set: ライブラリから受信したすべての GTID のセットを表します
Executed_Gtid_Set:ライブラリから完成したすべての GTID のセットを示します
Retrieved_Gtid_Set セットと Executed_Gtid_Set セットが一貫しているかどうかを比較して、マスターとスレーブの間に遅延があるかどうかを判断します。
スリープよりもサイトの比較や GTID セットの比較の方が精度が高いことがわかります。クエリを実行する前に、受信したログが実行されたかどうかを判断できます。精度は向上しましたが、まだ正確ではありません。なぜそうなるのですか。何?
まずモノの下の binlog のステータスを確認します
メイン ライブラリの実行が完了すると、それがバイナリログに書き込まれ、クライアントにフィードバックされます。
バイナリ ログはメイン データベースからスタンバイ データベースに送信され、スタンバイ データベースはログを受信します。
スタンバイデータベース実行のバイナリログ
上記のアクティブ データベースとスタンバイ データベース間の遅延なしソリューションを判断する場合、スタンバイ データベースが受信したログは実行されたと判断しますが、アクティブ データベースとスタンバイ データベース間のバイナリ ログの状態分析から、次のことがわかります。クライアントによって送信されたログがまだいくつかあることが確認されましたが、スタンバイ データベースはまだログのステータスを受信していません。
このとき、メインライブラリは trx1、trx2、trx3 の 3 つを実行しました。
trx1、trx2がスレーブライブラリに転送され、スレーブライブラリの実行が完了しました。
trx3主库已经执行完成,并且已经给客户端回复,但是还没有传给从库
这个时候如果在从库B执行查询,按照上面我们判断位点的方式,这个时候主从是没有延迟的,但是还查不到trx3,严格说就是出现了"过期读"。那么这个问题有什么方法可以解决么?
要解决这个问题,可以引入半同步复制,也就是semi-sync repliacation(参考:https://dev.mysql.com/doc/refman/8.0/en/replication-semisync.html)。
可以通过
show variables like '%rpl_semi_sync_master_enabled%'
show variables like '%rpl_semi_sync_slave_enabled%'
这两个命令来查看主从是否都开启了半同步复制。
semi-sync做了这样的设计:
事务提交的时候,主库把binlog发给从库
从库接收到主库发过来的binlog,给主库一个ack确认,表示收到了
主库收到这个ack确认后,才给客户端返回一个事务完成的确认
也就是启用了semi-sync,表示所有返回给客户端已经确认完成的事务,从库都收到了binlog日志,这样通过semi-sync配合判断位点的方式,就可以确定在从库上的查询,避免了过期读的出现。
但是semi-sync配合判断位点的方式,只适用一主一备的情况,在一主多从的情况下,主库只要收到一个从库的ack确认,就给客户端返回事物执行完成的确认,这个时候在从库上执行查询就有两种情况。
如果查询刚好是在给主库响应ack确认的从库上,那么可以查询到正确的数据
但是如果请求落到其他的从库上,他们可能还没收到日志,所以依然可能存在过期读
其实通过判断同步位点或者GTID集合的方案,还存在一个潜在的问题,就是业务高峰期,主库的位点或者GITD集合更新的非常快,那么两个位点的判断一直不相等,很可能出现从库一直无法响应查询请求的情况。
上面的两种方案在靠谱程度和精确性上都差了一点儿,接下来介绍两种相对靠谱和精确一点儿的方案。
4.等主库位点
要理解等主库位点,先介绍一条命令
select master_pos_wait(file, pos[, timeout]);
这条命令执行的逻辑是:
首先是在从库执行的
参数file和pos是主库的binlog文件名和执行到的位置
timeout参数是非必须,设置为正整数N,表示这个函数最多等到N秒
这个命令执行结果M可能存在的情况:
M>0表示从命令执行开始,到应用完file和pos表示的binlog位置,一共执行了M个事务
如果执行期间,备库的同步线程发生异常,则返回null
如果等待超过N秒,返回-1
如果刚开始执行的时候,发现已经执行了过了这个pos,则返回0
当一个事务执行完成后,我们要马上发起一个查询请求,可以通过下面的步骤实现:
1.当一个事务执行完成后,马上执行show master status,获取主库的File和Position
2.选择一个从库执行查询
3.在从库上执行 select master_pos_wait(File,Poistion,1)
4.如果返回的值>=0,则在这个从库上执行
5.否则回主库查询
这里我们假设,这条查询请求在从库上最多等待1s,那么如果1s内master_pos_wait返回一个大于等于0的数,那么就能保证在这个从库上能查到刚执行完的事务的最新的数据。
上述的步骤5是这类方案的兜底方案,因为从库的延迟时间不可控,不能无限等待,所以如果超时,就应该放弃,到主库查询。
可能有同学会觉得,如果所有的延迟都超过1s,那么所有的压力都到了主库,确实是这样的,但是按照我们设定的不允许出现过期读,那么就只有两种选择,要么超时放弃,要么转到主库,具体选择哪种,需要我们根据业务进行具体的分析。
5.等GTID方案
如果数据库开启的GTID模式,那么相应的也有等GTID的方案
select wait_for_executed_gtid_set(gtid_set, 1);
这条命令的逻辑是:
等待,直到这个库执行的事务中包含传入的giid_set集合,返回0
超时返回1
在前面等待主库位点的方案中,执行完事务后,需要到主库执行show master status。从mysql5.7.6开始,允许事务执行完成后,把这个事务执行的GTID返回给客户端,这样等待GTIID的方案就减少了一次查询。
这时等GTID方案的流程就变成这样:
事务执行完成后,从返回包解析获取这个事务的GTID,记为gtid1
选定一个从库执行查询
在从库上执行select wait_for_executed_gtid_set(gtid1,1)
如果返回0,则在这个从库上执行查询
否则回到主库查询
和等待主库位点方案一样,最后的兜底方案都是转到主库查询了,需要综合业务考虑确定方案
上面的事务执行完成后,从返回的包中解析GTID,mysql其实没有提供对应的命令,可以参考Mysql提供的api(https://dev.mysql.com/doc/c-api/8.0/en/mysql-session-track-get-first.html),在我们的客户端可以调用这个函数获取GTID。
以上简单介绍了读写分离架构,和出现主从延迟后,如果我们用的读写分离的架构,那么我们应该怎么处理这种情况,相信在日常我们的主从还是或多或少的存在延迟。上面介绍的几种方案,有些方案看上去十分不靠谱,有些方案做了一些妥协,但是都有实际的应用场景,需要我们根据自身的业务情况,合理选择对应的方案。
但话说回来,导致过期读的本质还是一写多读导致的,在实际的应用中,可能有别的不用等待就可以水平扩展的数据库方案,但这往往都是通过牺牲写性能获得的,也就是需要我们在读性能和写性能之间做个权衡。
文中有不太严谨或者错误的地方还望大家多多指正。
-end-
本文分享自微信公众号 - 京东云开发者(JDT_Developers)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。