Yandex.Diskをどのようにしたのか:サーバー側、WebDAV、Erlang

先週、Yandex.Diskは1年前になり、今年は8,000,000人以上のユーザーがこのサービスを使用することができました。



そして今、私たちはこれをすべて可能にするためにどれだけの労力がかかったかについて話し続けています。 最近、 Yandex.Diskチームがデスクトップクライアントをサーバーと同期するためにWebDAV選択した方法と理由について書き、プロトタイプのYandex.Diskクライアントの作業を開始しました。 今日、約束されたように、それはすべてがサーバー側でどのように機能するかについてです。



ディスクはファイルを保存します-Shoiguではありません



適切に同期するには、ファイルをアップロードできるだけでなく、接続が中断された場合に塗りつぶしを復元し、クライアントにファイルの変更を考慮するように教える必要もあります。



サーバーへの接続が中断されてから復元される場合、クライアントはファイルをアップロードできる必要があることは明らかです。 この場合に考慮する必要がある2つのパラメーターがあります:ファイル名とそのサイズです。 しかし、私たちにとっては十分ではありません。複数のクライアントが同時にリポジトリを操作でき、ファイルを競争的に更新できます。 したがって、もう1つのパラメーターを追加する必要がありました。



その時点で、同期モジュールの開発をすでに開始していました。このプロセスでは、ファイルの内容のmd5ハッシュを読み取ります。 そして、それを明確化パラメータとして使用することにしました。 まず、クライアントには常にこの情報があり、md5ハッシュを使用して、その負荷を増加させませんでした。 第二に、ファイルの内容に依存しないパラメータよりも優れています-送受信されたファイルの身元を確認することができます。



ファイルをサーバーに送信する前に、クライアントはハッシュを考慮します。 次に、PUTメソッドを使用してファイルをアップロードし、Etag HTTPヘッダーでこのハッシュをサーバーに伝えます。 このようなリクエストを受信すると、サーバーはアップロードされたファイルのサイズとそのmd5を不完全な塗りつぶしの特別なテーブルに保存します。 サーバー上のすべてのコンテンツが正常に満たされている場合、受信したファイルのmd5が計算され、クライアントから受信したファイルと比較されます。一致する場合、ファイルは正しく受信され、保存できます。



接続で問題が発生した場合-閉じられた場合や長いタイムアウトの後-サーバー上で、実際に受け入れられたサイズをテーブルに保存し、失敗したリクエストをaccess.logで保護する必要がありました。 mochiwebをWebサーバーフレームワークとして使用し、切断された接続の問題を処理する過程で、その機能に出会いました。 ライブラリは、「終了(通常)」を呼び出すことでエラーに反応しました。これは、プロセスの「サイレント」終了を意味します。 これは、リクエストを記録するためにnginxに直面している場合、およびこの種の接続終了で何もする必要がない場合は正常です。 もちろん、この例外をキャッチできます。 ただし、この場合、スタックトレースに既知の関数が存在することによってのみ、どの問題が発生したのかを理解することができます。 このメソッドを通常と呼ぶことはできないため、ライブラリを編集してより健全なエラーを発行する必要がありました。



接続が切断されると、クライアントは送信されたファイルの何バイトが実際にサーバーに到達したかに関する情報に依存できません。 そのため、プロトコルをもう1つ改良する必要がありました。クライアントがこの情報を要求するHEADメソッドを拡張し、ファイルがアップロードされたパス、サイズ、およびmd5をサーバーに渡します。 サーバーは、同じパラメーターで不完全なユーザーダウンロードを検索し、実際にダウンロードされたクライアントの数に応答します。 その後、クライアントは特別な要求(PUTメソッドの新しい拡張機能)を使用して、サーバーによって示された場所からダウンロードを再開する必要があります。



ファイルをダウンロードするだけでなく、rsyncで行われるように、バイナリファイルパッチ-デルタ更新-をオーバーレイしますが、サーバー上のこれらの操作による負荷を最小限に抑えたいと考えました。 ファイルは、高速で永続的な署名が考慮されるブロックに従って分割されます。 高速署名を計算する方法-ローリングチェックサム-rsyncから借用しました。 ブロック署名は、ネットワーク上で送信する必要のないファイルの一致部分を検索するために使用されます。 ブロックサイズ、署名、md5ファイルの組み合わせをファイルダイジェストと呼びます。 クライアントが更新されたファイルのどの部分をダウンロードまたはサーバーに送信する必要があるかを判断できるようにするには、サーバーに保存されているファイルのダイジェストを受信する必要があります。 これを行うには、プロトコルを拡張する必要がありました-今回はダイジェスト方式を使用しています。



サーバーから受信したダイジェスト自体については、オンデマンドで計算することで同期プロセスを遅くしたくないため、既に計算されたサーバーに保存することにしました。



まず始めに、Erlangでファイルをストリーミングしているときにダイジェストを読み取ってみました。 オーバーヘッドを削減するように見えました。データの一部は既にメモリにあり、ダイジェスト計算モジュールに転送することは安価なソリューションのように思えました。 残念ながら、Erlangでのメモリ操作の詳細のため、これは事実ではありませんでした:データはハッシュを読み取るドライバーにコピーされ、中間結果はプロセッサープロセスにコピーされ、その後すべてがドライバーに送信されました。 リソース集約型であることが判明しました。 すべての中間状態を内部に保存し、Erlangに返さない特殊なドライバーを開発したくありませんでした。 別の解決策は、通常どおりファイルをディスクに配置し、ファイルがCで記述され、Erlangからポートとして起動された別個のプログラムとして完全に受信された後、ダイジェストを考慮することでした。 このアプローチを使用して、ダイジェストを計算する時間を10倍短縮しました。



サーバー上のデルタ更新のために、標準のPUTメソッドが拡張されました。これはバイナリdiffを取得し、ソースファイルに重ね合わせます。 この差分では、2つのコマンドのみが定義されています。ソースファイルの一部をコピーし、クライアントから送信された部分を貼り付けます。 サーバーは単純な操作のみを実行し、ファイルの変更の重い分析はすべてクライアント側にあります。



実際、緑の人はファイルをディスクにアップロードします

ファイルがサーバー上で更新された場合、同一のパーツを検索するために同じアルゴリズムが使用されます。 クライアントは同じファイルのいくつかの部分を必要とする可能性があるため、ファイルメタデータの呼び出し回数を減らすために、応答がマルチパート/バイト範囲の形式である場合、多くの範囲を示すリクエストをサポートしました。



同期に必要な別の方法は、ファイルツリーの差分を取得して、クライアントがサーバー上で更新されたファイルを判別できるようにすることです。 このタスクは通常のバージョン管理とは異なるため、標準で提案された方法は私たちに適さず、プロトコルを再度拡張する必要がありました。 クライアントがファイルを更新する場合、この新しいメソッドを呼び出して、同期されたバージョンの識別子を示します。 また、サーバーは、最新バージョンの識別子と、最後の更新以降にファイル構造では(ファイル自体ではなく)発生した変更のリストで応答します。 これを行うために、各ユーザーのファイル構造のすべての変更の履歴を保持します。



おそらく、いくつかの小さなことを除いて、Yandex.DiskのWebDAVサーバーが行うことはこれだけです。 このプロトコルを選択したことを嬉しく思います。 一方では、実質的に「箱から出して」すぐに私たちのニーズに応え、大幅な改善を必要としませんでしたが、他方では、多くのユーティリティとアプリケーションをYandex.Diskと簡単に統合できます。



All Articles