MySQLの写真または別のキューの移行

最近、データセンター間でのユーザーデータの大規模な移行のタスクにどのように直面したか、そしてそれをどのように解決したについて書きました。

今回は、ユーザーの写真がどのように移行され、どのデータ構造が写真を使用してサーバーの負荷を制限するために使用されたのかについて詳しく説明します。

Badooユーザーは、毎日約300万枚の写真をアップロードしています。 それらを保存するために、サイズ変更、透かし入れ、他のソーシャルネットワークからの写真のインポート、および他のファイル操作にも関与するサーバーの特別なクラスターを割り当てました。

このクラスター内のすべてのマシンは、3つのグループに分けることができます。 1つ目は、ユーザーへの写真の迅速な配信を担当するサーバーです(CDNの独自の実装と言えます)。 移行の文脈では、これらのサーバーは私たちの興味を引くものではありません。 2番目のグループはディスクを備えたストレージで、実際にはすべての写真があります。 そして、3番目のグループは、2番目のグループへのインターフェースを提供するサーバーです。通常、これらを写真サーバーと呼びます。 ストレージのディスクアレイが光ファイバー上にマウントされ、写真が同じマシンにダウンロードされ、ファイル操作を実行するすべてのスクリプトがここで機能します。

したがって、PHPコードの場合、写真がどのストレージのどのディスクにあるかは関係ありません。 必要なのは、ユーザーの写真をあるフォトサーバーから別のフォトサーバーに転送し、データベースといくつかのデーモンでこの情報を更新することです。 ここで、すべてのユーザーの写真は常に同じフォトサーバー上にあることに注意することが重要です。



問題の声明



ユーザーがこれまでにアップロードしたすべての写真の合計容量は約600 TBです。 この数には、特定のケースでの表示に必要な、元の写真とサイズ変更された写真のセットの両方が含まれます。

概算では、1億900万人のユーザーが600 TBのデータをダウンロードした場合、タイ(データセンター間で転送された最大の国)の150万人のユーザーが4.7 TBをダウンロードしました。 移行時のデータセンター間のチャネルの帯域幅は200 Mbpsでした。 簡単な計算により、タイのユーザーの写真をすべて55時間転送できます。 当然、このチャネルはすでにデータセンター間で常に循環している他のデータによって部分的に占有されており、実際には55時間以上かかります。 そして、私たちの目標は8時間でそれを達成することです。

元の写真のみを転送して、新しい写真サーバーで「サイズ変更」することも可能ですが、これにより、プロセッサの負荷が望ましくないほど増加します。 そのため、ユーザーが全国の写真を転送するのに必要な時間内に多くの新しい写真をアップロードする時間がないことを期待して、写真を事前に転送することにしました。

つまり、最初は既存の写真をコピーするだけで、すべてのユーザーデータの移行中(サイトがユーザーに利用できず、写真の一部を変更できない場合)、写真の転送後に変更があったかどうかを確認します。 変更があった場合は、写真を再度コピーし(またはrsyncを作成します)、その後のみデータベースとデーモンのデータを更新して、ユーザーの写真が表示され、新しいフォトサーバーにアップロードされるようにします。

実践が示しているように、私たちの期待は満たされ、ごく一部のユーザーに対して2回目のrsyncが行われました。

もう1つの制限は、ストレージディスクのパフォーマンスです。 数百テラビットのチャンネルがあっても、写真は常に写真サーバーにアップロードされ、さまざまな操作が実行されるため、フルキャパシティで使用することはできません。 さらに、CDNはこれらの写真をディスクから「読み取り」ます。読み取りの負荷が増えると、日常の操作が大幅に遅くなる可能性があります。 つまり、写真の移行の強度を人為的に制限する必要があります。



実装



最初に思いついたのは、データベース内のキュー(MySQLを使用)で、これには写真の転送が必要なすべてのユーザーが含まれます。 キューはいくつかのプロセスで処理されます。 フォトサーバーあたりのプロセス数を制限することで、ディスクの負荷を制限する問題を解決します。 プロセスの総数を制限することで、データセンター間のチャネルの負荷を制御できます。

次の構造を持つMigrationPhotoテーブルがあるとします。



CREATE TABLE MigrationPhoto ( user_id INT PRIMARY KEY, updated TIMESTAMP, photoserver VARCHAR(255), #  c   script_name VARCHAR(255), #  ,   done TINYINT(1), # 1    ,  0 KEY photoserver (photoserver), KEY script_name (script_name) )
      
      





まず、写真をテーブルに転送するすべてのユーザーを追加します。



 INSERT INTO MigrationPhoto (user_id,photoserver) VALUES (00000000, 'photoserver1')
      
      





プロセスの総数の制限はpcntl拡張機能を使用して簡単に達成でき、あまり重要ではないため、転送を処理する1つのプロセスをさらに検討します。

写真サーバーごとに特定の数のプロセスを提供する必要があります。 まず、どの写真サーバーのどのユーザーがキューに入っているのかを把握しましょう。 SELECT photoserver、COUNT(*)FROM MigrationPhotoを毎回行わないために、個別のテーブルを作成します。



 CREATE TABLE MigrationPhotoCounters ( photoserver VARCHAR(255) PRIMARY KEY, users INT )
      
      





各ユーザーがMigrationPhotoテーブルに挿入するときにデータを入力します。



 INSERT INTO MigrationPhotoCounters (photoserver, users) VALUES ('photoserver1', 1) ON DUPLICATE KEY UPDATE users = users + VALUES(users)
      
      





または、MigrationPhotoに記入した後、これを行います。



 INSERT INTO MigrationPhotoCounters (bphotos_server, users) VALUES (SELECT photoserver, COUNT(*) AS users FROM MigrationPhoto)
      
      





このようなテーブルを用意して、各プロセスの開始時に行います



 SELECT photoserver FROM MigrationPhotoCounters WHERE users>0 ORDER BY RAND()
      
      





すべての写真サーバーのリストを受け取った後、制限を超えないようにプロセスを開始できる写真サーバーを決定します。 これを行うには、フォトサーバーの名前とこのサーバー内のプロセスのシリアル番号で構成される名前のロックをデータベースに入れます。



 $processNumber = null; foreach ($photoservers as $serverName) { for ($i = 1; $i <= PROCESSES_PER_SERVER; $i++) { $lock = executeQuery("SELECT GET_LOCK('migration" . $serverName . '_' . $processNumber . "', 0)"); if ($lock === '1') { $processNumber = $i; break; } } if ($processNumber) { $serverName = $serverName; $scriptName = 'migration' . $serverName . '_' . $processNumber; break; } }
      
      





したがって、2つのネストされたサイクルですべての写真サーバーとそれらのプロセス番号をソートします。 内部ループのすべての反復が完了し、 $ processNumber変数が定義されていない場合、この写真サーバーのプロセス数の制限に達しました。 外側のループのすべての反復が完了すると、移行するユーザーがまだいるすべての写真サーバーでこの制限に達しました。

photoserver1を選択し、これが2番目のプロセスであるとします。つまり、プロセス識別子は$ scriptName = 'migration_photoserver1_2'になります。

先に進む前に、前回の起動時に何らかの理由で、選択したプロセス識別子($ scriptName)でタグ付けされたユーザーの一般的なキューに戻りましょう。



 UPDATE MIgrationPhoto SET script_name = NULL WHERE done = 0 AND script_name = 'migration_photoserver1_2'
      
      





ユーザーの一部をこのプロセスで処理済みとしてマークします。



 UPDATE SET MigrationPhoto script_name='migration_photoserver1_2' WHERE photoserver='photoserver1' AND done = 0 AND script_name IS NULL LIMIT 100;
      
      





次に、選択した写真サーバーに対応し、まだ別のプロセスで処理されていない複数のユーザーをキューから取得します。



 SELECT * FROM MigrationPhoto WHERE script_name='migration_photoserver1_2' AND done=0;
      
      





その後、選択したレコードを処理するプロセスが他にないことを確認できます。

ユーザーの写真をいつ転送したかを覚えておいてください。



 UPDATE MigrationPhoto SET updated=NOW() WHERE user_id = 00000000
      
      





必要なすべての操作を、rsyncとして表すことができる単純化された方法で実行します。 写真を正常に転送した後、データベースに(選択したユーザーごとに)これを記録する必要があります。



 BEGIN; UPDATE MigrationPhotoCounters SET users = users - 1 WHERE photoserver = 'photoserver1'; UPDATE MigrationPhoto SET done = 1 WHERE user_id = 00000000; COMMIT;
      
      





処理のために100人のユーザーが使用されている場合、さまざまな理由で転送できないユーザーもいます。 これらのユーザーは、後で転送するためにキューに戻す必要があります。



 UPDATE MigrationPhoto SET script_name = NULL WHERE user_id IN (<failed_ids>)
      
      





プロセスを完了します。



 SELECT RELEASE_LOCK('migration_photoserver1_2')
      
      





これで完了できるように思えます。 しかし、このスキームには根本的な欠陥があります。

PROCESSES_PER_SERVER = 10の間に$ scriptName = 'migration_photoserver1_10'を持つプロセスが開始されたとします。 そして、このプロセスは、キューにかかったユーザーを返さずに落ちました。 これらのユーザーを再度選択するには、同じ$ scriptNameを持つプロセスを再度開始するか、誰かがこれらのユーザーをデータベースscript_name = NULLに設定する必要があります 。 同じ$ scriptNameでプロセスを開始することはできなくなります。

たとえば、MigrationPhotoCountersには100のフォトサーバーがあり、プロセスの合計数の制限は50、プロセスの数の制限は1〜10であり、ある時点で1つのフォトサーバーに10のプロセスがあった場合、将来このフォトサーバーが受信できることは明らかです1つのプロセスのみ。 したがって、たとえば、プロセスが現在実行されていないユーザーに対して1分ごとにscript_name = NULLを設定する別のスクリプトを作成します



 foreach ($photoservers as $serverName) { for ($i = 1; $i <= PROCESSES_PER_SERVER; $i++) { $lock = executeQuery("SELECT GET_LOCK('migration" . $serverName . '_' . $processNumber . "', 0)"); if ($lock === '1') { executeQuery("UPDATE MigrationPhoto SET script_name = NULL WHERE done = 0 AND script_name = 'migration" . $serverName . '_' . $processNumber . "'"); } } }
      
      





これで、プロセスがクラッシュした場合でも、そのユーザーは他のプロセスで処理できるようになります。 さらに、これにより、1つの写真サーバーあたりのプロセス数の制限を「オンザフライ」で変更できます。

プロセスが完了し、他のすべてのユーザーデータの移行が開始されたら、MigrationPhotoテーブルの更新されたフィールドで指定された時刻以降、ユーザーの写真が変更されていないことを確認するだけで十分です。 そして、変更された場合は、rsyncを繰り返します。 ほとんどのユーザーは2〜3日ですべての写真を変更しないため、これにはそれほど時間がかかりません。

その結果、写真を読み取る63台の写真サーバーと、書き込み先の30台のサーバーがありました。 これはすべて、80個のプロセスで発生し、写真サーバーごとに最大3つのプロセスの制限があります。 これらの制限により、トラフィックは150 Mbpsでした。 タイのユーザーに写真を転送するのに3日弱かかりました。 データ量を考えると、素晴らしい結果が得られました。



おわりに



もちろん、このスキームでは拡張と改善が可能です。 たとえば、各フォトサーバーの処理プロセスの数を個別に制限できます。 この数は、特定のフォトサーバーのユーザーの数に応じて調整できます。 優先順位、次のレコード処理のためのプログレッシブタイムアウト(現在の処理が失敗した場合)、失敗したプロセスの最大数、ログ、グラフィックスなどを追加できます。

しかし、与えられた制限付きの並列キュー処理のアイデアを伝えたかったのです。もちろん、サーバー間でファイルを転送するためだけに使用することはできません。

今日は、さまざまなエラーや例外的な状況を処理するプロセス、特定の数のプロセスを作成および維持するメカニズム、およびファイル転送の実装については説明しませんでした。 しかし、これがあなたにとって興味深い場合-質問してください、そしてコメントで答えます。



PHP開発者、チームリード、アントンステパネンコ



All Articles