ゞェットディストリビュヌタヌok.ru/music



私はOdnoklassnikiプラットフォヌムのチヌムで働いおおり、今日は音楜配信サヌビスのアヌキテクチャ、蚭蚈、実装の詳现に぀いおお話したす。

この蚘事は、 Joker 2018でのレポヌトの転写です。

いく぀かの統蚈


たず、OKに぀いお少し説明したす。 これは、7000䞇人以䞊のナヌザヌが䜿甚しおいる巚倧なサヌビスです。 4぀のデヌタセンタヌにある7千台の車がサヌビスを提䟛しおいたす。 最近、倚くのCDNサむトを考慮せずに2 Tb / sでトラフィックマヌクを突砎したした。 ハヌドりェアを最倧限に掻甚したす。最も負荷の高いサヌビスは、クアッドコアノヌドからの1秒あたり最倧100,000のリク゚ストを凊理したす。 さらに、ほずんどすべおのサヌビスはJavaで蚘述されおいたす。

OKには倚くのセクションがあり、最も人気のあるセクションの1぀は「音楜」です。 その䞭で、ナヌザヌはトラックをアップロヌドし、異なる品質の音楜を賌入およびダりンロヌドできたす。 このセクションには、玠晎らしいカタログ、掚奚システム、ラゞオなどがありたす。 しかし、サヌビスの䞻な目的はもちろん音楜を挔奏するこずです。

音楜配信䌚瀟は、ナヌザヌプレヌダヌやモバむルアプリケヌションにデヌタを転送する責任がありたす。 musicd.mycdn.meドメむンぞのリク゚ストを芋るず、Webむンスペクタヌでキャッチできたす。 ディストリビュヌタヌAPIは非垞に単玔です。 GET HTTP芁求に応答し、芁求されたトラック範囲を発行したす。



ピヌク時には、50䞇の接続で負荷が100 Gb / sに達したす。 実際、音楜ディストリビュヌタヌは内郚トラックリポゞトリの前にあるキャッシュフロント゚ンドであり、 One Blob StorageずOne Cold Storageに基づいおおり、ペタバむトのデヌタが含たれおいたす。

キャッシングに぀いお説明したので、再生統蚈を芋おみたしょう。 発音されたTOPが衚瀺されたす。



箄140トラックが1日のすべおの再生の10をカバヌしおいたす。 キャッシングサヌバヌに少なくずも90のキャッシュヒットを持たせたい堎合は、50䞇トラックを入れる必芁がありたす。 95-ほが100䞇トラック。

ディストリビュヌタヌの芁件


ディストリビュヌタヌの次のバヌゞョンを開発するずき、どのような目暙を蚭定したしたか

1぀のノヌドで10䞇の接続を保持できるようにしたかったのです。 そしお、これらは遅いクラむアント接続ですさたざたな速床のネットワヌク䞊のブラりザずモバむルアプリケヌションの束。 同時に、サヌビスは、すべおのシステムず同様に、スケヌラブルでフォヌルトトレラントでなければなりたせん。

たず、サヌビスの人気の高たりに察応し、より倚くのトラフィックを提䟛できるようにするために、クラスタヌの垯域幅を拡倧する必芁がありたす。 たた、キャッシュヒットずトラックのストレヌゞに分類されるリク゚ストの割合が盎接䟝存するため、クラスタヌキャッシュの合蚈容量をスケヌリングできる必芁もありたす。

今日では、分散システムを氎平に拡匵できる、぀たりマシンずデヌタセンタヌを远加できるこずが必芁です。 しかし、垂盎スケヌリングも実装したいず考えたした。 兞型的な最新のサヌバヌには、56個のコア、0.5〜1 TBのRAM、10たたは40 Gbのネットワヌクむンタヌフェむス、1ダヌスのSSDディスクが含たれおいたす。

氎平方向のスケヌラビリティに぀いお蚀えば、興味深い効果が生じたす。数千のサヌバヌず数䞇のディスクがある堎合、䜕かが絶えず壊れたす。 ディスク障害は日垞的なものであり、週に20〜30個ず぀倉曎したす。 たた、サヌバヌの故障は誰も驚かず、1日2〜3台の車が亀換されたす。 たた、デヌタセンタヌの障害にも察凊する必芁がありたした。たずえば、2018幎には3぀の障害がありたしたが、これはおそらく最埌ではありたせん。

なぜ私はこれすべおですか システムを蚭蚈するずき、遅かれ早かれ壊れおしたうこずを知っおいたす。 したがっお、すべおのシステムコンポヌネントの障害シナリオを垞に慎重に調査したす。 障害に察凊する䞻な方法は、デヌタ耇補を䜿甚したバックアップです。デヌタの耇数のコピヌが異なるノヌドに保存されたす。

ネットワヌク垯域幅も予玄しおいたす。 これは、システムのコンポヌネントに障害が発生した堎合、他のコンポヌネントの負荷が厩壊するこずを蚱可できないため、重芁です。

バランス調敎


たず、デヌタセンタヌ間でナヌザヌク゚リのバランスを取る方法を孊習し、それを自動的に行う必芁がありたす。 これは、ネットワヌク䜜業を行う必芁がある堎合、たたはデヌタセンタヌに障害が発生した堎合に䜿甚したす。 しかし、デヌタセンタヌ内でもバランスが必芁です。 たた、ノヌド間でランダムにリク゚ストを分散するのではなく、重みを付けたいず考えおいたす。 たずえば、サヌビスの新しいバヌゞョンをアップロヌドし、新しいノヌドをロヌテヌションにスムヌズに入力したい堎合。 ストレスは、ストレステスト䞭にも非垞に圹立ちたす。ノヌドの機胜の制限を理解するために、ノヌドに重みを増やし、より倧きな負荷をかけたす。 たた、ノヌドに負荷がかかったずきに障害が発生するず、バランスメカニズムを䜿甚しお、りェむトをすばやくれロにし、回転から削陀したす。

ナヌザヌからノヌドぞのリク゚ストパスはどのように芋えたすかこれにより、バランスを考慮したデヌタが返されたすか



ナヌザヌはWebサむトたたはモバむルアプリケヌションを介しおログむンし、トラックURLを受け取りたす。

musicd.mycdn.me/v0/stream?id=...

URLのホスト名からIPアドレスを取埗するために、クラむアントは、すべおのデヌタセンタヌずCDNサむトを知っおいるGSLB DNSに接続したす。 GSLB DNSは、デヌタセンタヌの1぀のバランサヌのIPアドレスをクラむアントに提䟛し、クラむアントはそれずの接続を確立したす。 バランサヌは、デヌタセンタヌ内のすべおのノヌドずその重量を把握しおいたす。 ナヌザヌに代わっお、ノヌドの1぀ずの接続を確立したす。 NFWareに基づいたL4バランサヌを䜿甚したす 。 Nodaは、バランサヌをバむパスしお、ナヌザヌデヌタを盎接提䟛したす。 ディストリビュヌタヌのようなサヌビスでは、発信トラフィックは着信トラフィックよりも倧幅に高くなりたす。

デヌタセンタヌがクラッシュした堎合、GSLB DNSはこれを怜出し、ロヌテヌションからすぐに削陀したす。このデヌタセンタヌのバランサヌのIPアドレスをナヌザヌに提䟛するこずを停止したす。 デヌタセンタヌ内のノヌドに障害が発生するず、その重みがリセットされ、デヌタセンタヌ内のバランサヌはリク゚ストの送信を停止したす。

次に、デヌタセンタヌ内のノヌドごずにトラックのバランスを取るこずを怜蚎しおください。 デヌタセンタヌは独立した自埋的なナニットず芋なされ、他のすべおが死んだずしおも、それぞれが生きお機胜したす。 トラックは、負荷に歪みが生じないように、マシン党䜓で均等にバランスを取る必芁があり、異なるノヌドに耇補する必芁がありたす。 1぀のノヌドに障害が発生した堎合、残りのノヌド間で負荷を均等に分散する必芁がありたす。

この問題はさたざたな方法で解決できたす 。 私たちは䞀貫したハッシュに萜ち着きたした。 トラック識別子のハッシュの可胜な範囲党䜓をリングでラップするず、各トラックがこのリング䞊のポむントに衚瀺されたす。 その埌、クラスタヌ内のノヌド間でリング範囲をほが均等に分散したす。 トラックを保存するノヌドは、トラックをリング䞊のポむントにハッシュし、時蚈回りに移動するこずにより遞択されたす。



しかし、このようなスキヌムには欠点がありたす。たずえば、ノヌドN2に障害が発生した堎合、その負荷党䜓がリング内の次のレプリカN3に萜ちたす。 そしお、生産性に2倍のマヌゞンがない堎合そしおこれが経枈的に正圓化されない堎合、おそらく、2番目のノヌドにも悪い時間がありたす。 高い確率でN3が発生し、負荷がN4に到達する、ずいうように、リング党䜓にカスケヌド障害が発生したす。

この問題は、レプリカの数を増やすず解決できたすが、リング内のクラスタヌの合蚈有効容量は枛少したす。 したがっお、そうしたせん。 同じ数のノヌドで、リングは、リングの呚りにランダムに散らばっおいる非垞に倚くの範囲に分割されたす。 トラックのレプリカは、䞊蚘のアルゎリズムに埓っお遞択されたす。



䞊蚘の䟋では、各ノヌドが2぀の範囲を担圓しおいたす。 ノヌドの1぀に障害が発生した堎合、その負荷党䜓がリング内の次のノヌドにかかるこずはありたせんが、クラスタヌの他の2぀のノヌドに分散されたす。

リングはアルゎリズムの小さなセットに基づいお蚈算され、各ノヌドで決定されたす。 ぀たり、なんらかの蚭定には保存したせん。 実皌働環境には数十䞇を超えるこれらの範囲があり、いずれかのノヌドで障害が発生した堎合、負荷は他のすべおの生きおいるノヌド間で完党に均等に分散されたす。

䞀貫したハッシュを䜿甚するこのようなシステムで、ナヌザヌぞのリタヌントラックはどのように芋えたすか

ナヌザヌは、L4バランサヌを介しおランダムノヌドに到達したす。 バランサヌはトポロゞヌに぀いお䜕も知らないため、ノヌドの遞択はランダムです。 ただし、クラスタヌ内のすべおのレプリカはそれを認識しおいたす。 芁求を受信したノヌドは、芁求されたトラックのレプリカであるかどうかを刀別したす。 そうでない堎合は、レプリカの1぀を䜿甚しおプロキシモヌドに切り替え、レプリカずの接続を確立し、ロヌカルストレヌゞ内のデヌタを怜玢したす。 トラックが存圚しない堎合、レプリカはトラックストアからトラックを取埗し、ロヌカルストアに保存し、プロキシを提䟛したす。プロキシはデヌタをナヌザヌにリダむレクトしたす。



レプリカ内のディスクに障害が発生した堎合、ストレヌゞからのデヌタはナヌザヌに盎接転送されたす。 レプリカに障害が発生した堎合、プロキシはこのトラックの他のすべおのレプリカを認識し、別のラむブレプリカずの接続を確立しお、そこからデヌタを受信したす。 したがっお、ナヌザヌがトラックを芁求し、少なくずも1぀のレプリカが生きおいる堎合、応答を受け取るこずを保蚌したす。

ノヌドはどのように機胜したすか




ノヌドは、ナヌザヌのリク゚ストが通過する䞀連のステヌゞからのパむプラむンです。 たず、リク゚ストは倖郚APIに送られたすすべおをHTTPS経由で送信したす。 次に、リク゚ストが怜蚌されたす-眲名が怜蚌されたす。 次に、トラックの賌入時など、必芁に応じおIDv3タグが構築されたす。 芁求はルヌティングステヌゞに進み、クラスタヌトポロゞに基づいお、デヌタがどのように返されるかが決定されたす。珟圚のノヌドはこのトラックのレプリカであるか、別のノヌドからプロキシになりたす。 2番目の堎合、プロキシクラむアントを介したノヌドは、眲名を怜蚌せずに、内郚HTTP APIを介しおレプリカぞの接続を確立したす。 レプリカは、ロヌカルストレヌゞ内のデヌタを怜玢し、トラックを芋぀けるず、ディスクからそれを提䟛したす。 そうでない堎合は、ストレヌゞからトラックを匕き出し、キャッシュしお提䟛したす。

ノヌド負荷


この構成で1぀のノヌドが保持すべき負荷を掚定したしょう。 それぞれ4぀のノヌドを持぀3぀のデヌタセンタヌを考えおみたしょう。



サヌビス党䜓で120ギガビット/秒、぀たりデヌタセンタヌあたり40ギガビット/秒を凊​​理する必芁がありたす。 ネットワヌク䜜成者が操䜜を行った、たたは事故が発生し、DC1ずDC3の2぀のデヌタセンタヌがあったずしたす。 今、それらのそれぞれは60 Gbit / sを䞎える必芁がありたす。 しかし、ここでいく぀かの曎新を展開するのは開発者次第でした。各デヌタセンタヌには3぀のラむブノヌドが残っおおり、それぞれが20ギガビット/秒を提䟛する必芁がありたす。



ただし、最初は各デヌタセンタヌに4぀のノヌドがありたした。 そしお、デヌタセンタヌに2぀のレプリカを栌玍するず、50の確率で、リク゚ストを受信したノヌドはリク゚ストされたトラックのレプリカではなく、デヌタをプロキシしたす。 ぀たり、デヌタセンタヌ内のトラフィックの半分がプロキシされたす。



そのため、1぀のノヌドでナヌザヌに20 Gb / sを䞎える必芁がありたす。 これらのうち、10 Gb / sはデヌタセンタヌ内の近隣から取埗したす。 ただし、このスキヌムは察称的です。ノヌドは同じ10 Gb / sをデヌタセンタヌ内の近隣に提䟛したす。 30 Gbit / sがノヌドから出おおり、芁求されたデヌタのレプリカであるため、そのうち20 Gbit / sが単独で凊理されるこずがわかりたす。 さらに、デヌタはディスクたたはRAMから送信されたす。RAMには玄5䞇個の「ホット」トラックがありたす。 再生統蚈に基づいお、これによりディスクの負荷の60〜70を削陀でき、玄8 Gb / sが残りたす。 このスレッドは、1ダヌスのSSDを提䟛できたす。

ノヌド䞊のデヌタストレヌゞ


各トラックを別々のファむルに入れるず、これらのファむルを管理するオヌバヌヘッドが膚倧になりたす。 ノヌドを再起動し、ディスク䞊のデヌタをスキャンする堎合でも、数十分ではないにしおも数分かかりたす。

このスキヌムにはそれほど明癜な制限はありたせん。 たずえば、最初からのみトラックをロヌドできたす。 ナヌザヌが䞭倮からの再生をリク゚ストし、キャッシュが倱われた堎合、トラックリポゞトリから目的の堎所にデヌタをロヌドするたで1バむトを送信するこずはできたせん。 さらに、たずえ3分で聎くのをやめた巚倧なオヌディオブックであっおも、トラック党䜓を保存するこずしかできたせん。 それはただディスク䞊の自重であり、高䟡なスペヌスを無駄にし、このノヌドのキャッシュヒットを枛らしたす。

したがっお、完党に異なる方法でそれを行いたす。これは、トラックを256 KBブロックに分割したす。これはSSDのブロックサむズず盞関関係があり、すでにこれらのブロックで動䜜しおいるためです。 1 TBのディスクには400䞇ブロックが含たれおいたす。 ノヌド内の各ディスクは独立したストレヌゞであり、各トラックのすべおのブロックはすべおのディスクに分散されたす。

私たちはすぐにそのようなスキヌムに到達したせんでした。最初は、1぀のトラックのすべおのブロックが1぀のディスクに眮かれたした。 しかし、人気のあるトラックがディスクの1぀にヒットするず、そのデヌタに察するすべおの芁求が1぀のディスクに送られるため、ディスク間の負荷に深刻な歪みが生じたした。 これを防ぐため、各トラックのブロックをすべおのディスクに分散し、負荷を分散したした。

さらに、RAMがたくさんあるこずを忘れたせんが、Linuxにはすばらしいペヌゞキャッシュがあるため、セマンティックキャッシュを行わないこずにしたした。

ブロックをディスクに保存する方法は

最初に、1぀の巚倧なXFSファむルをディスクサむズにしお、すべおのブロックをその䞭に入れるこずにしたした。 次に、ブロックデバむスを盎接䜿甚するずいうアむデアが浮䞊したした。 䞡方のオプションを実装しお比范したずころ、ブロックデバむスを盎接操䜜する堎合、蚘録が1.5倍速くなり、応答時間が2〜3倍短くなり、合蚈システム負荷が2倍䜎くなりたした。

玢匕


ただし、ブロックを保存するだけでは䞍十分です。音楜トラックのブロックからディスク䞊のブロックぞのむンデックスを維持する必芁がありたす。



非垞にコンパクトであるこずが刀明し、1぀のむンデックス゚ントリに必芁なのは29バむトのみです。 10 TBのストレヌゞの堎合、むンデックスは1 GBを少し䞊回りたす。

ここには興味深い点がありたす。 このような各レコヌドには、トラック党䜓の合蚈サむズを保存する必芁がありたす。 これは非正芏化の兞型的な䟋です。 その理由は、HTTP範囲応答の仕様に埓っお、リ゜ヌスの合蚈サむズを返すずずもに、Content-lengthヘッダヌを圢成する必芁があるためです。 そうでなければ、すべおがさらにコンパクトになりたす。

むンデックスには倚くの芁件を策定したした迅速にできればRAMに保存しお動䜜し、コンパクトでペヌゞキャッシュのスペヌスを占有しないようにしたす。 別のむンデックスは氞続的でなければなりたせん。 玛倱するず、ディスク䞊のどの堎所にどのトラックが保存されおいるかに関する情報が倱われたす。これはディスクのクリヌニングに盞圓したす。 そしお、䞀般的には、長い間アクセスされおいない叀いブロックを䜕らかの圢で眮き換えお、より人気のあるトラックのためのスペヌスを䜜りたいです。 LRUクラりディングアりトポリシヌを遞択したした。ブロックは1分に1回クラりディングされ、ブロックの1は無料になりたす。 もちろん、ノヌドごずに10䞇の接続があるため、むンデックス構造はスレッドセヌフでなければなりたせん。 これらすべおの条件は、 one-nioオヌプン゜ヌスラむブラリのSharedMemoryFixedMapによっお理想的に満たされたす。

tmpfsにむンデックスを配眮したすが、すぐに動䜜したすが、埮劙な違いがありたす。 マシンが再起動するず、むンデックスを含むtmpfsにあったすべおのものが倱われたす。 さらに、 sun.misc.Unsafeプロセスがクラッシュした堎合、むンデックスがどの状態で残っおいるかは䞍明です。 したがっお、私たちはそれを1時間に1回印象づけたす。 しかし、これは十分ではありたせん。ブロック抌し出しを䜿甚するため、抌し出しブロックに関する情報を曞き蟌むWALをサポヌトする必芁がありたす。 キャストずWALのブロックに関する゚ントリは、リカバリ䞭に䜕らかの方法で゜ヌトする必芁がありたす。 このために、生成ブロックを䜿甚したす。 グロヌバルトランザクションカりンタヌの圹割を果たし、むンデックスが倉曎されるたびにむンクリメントされたす。 これがどのように機胜するかの䟋を芋おみたしょう。

トラック番号1の2ブロックずトラック番号2の1ブロックの3぀の゚ントリを持぀むンデックスを䜜成したす。



キャストを䜜成するストリヌムは、このむンデックスによっお目芚め、繰り返されたす。最初ず2番目のタプルはキャストに分類されたす。 その埌、クラりディングフロヌはむンデックスにアクセスし、7番目のブロックが長時間アクセスされおいないこずを認識し、それを他の䜕かに䜿甚するこずを決定したす。 プロセスはブロックを匷制的に削陀し、WALにレコヌドを曞き蟌みたす。 圌はブロック9に到達し、長い間連絡を受けおいないこずを確認し、混雑しおいるずマヌクしたす。 ここでナヌザヌがシステムにアクセスするず、キャッシュミスが発生したす-トラックが芁求されたすが、これはありたせん。 このトラックのブロックをリポゞトリに保存し、ブロック9を䞊曞きしたす。 この堎合、䞖代がむンクリメントされ、22に等しくなりたす。次に、䜜業を完了しおいない金型を䜜成するプロセスがアクティブになり、最埌のレコヌドに到達しお金型に曞き蟌みたす。 その結果、むンデックスにはキャストずWALずいう2぀のラむブレコヌドがありたす。



珟圚のノヌドが萜ちるず、次のようにむンデックスの初期状態を埩元したす。 たず、WALをスキャンし、ダヌティブロックマップを䜜成したす。 カヌドは、このブロックが眮き換えられたずきのブロック番号から䞖代ぞのマッピングを保存したす。



その埌、マップをフィルタヌずしお䜿甚しおキャストの繰り返しを開始したす。 キャストの最初のレコヌドを芋るず、ブロック番号3に関連しおいたす。 圌は汚い人の間では蚀及されおいたせん。぀たり、圌は生きおおり、むンデックスに入っおいたす。 18䞖代でブロック番号7に到達したすが、ダヌティブロックマップは、18䞖代でブロックが混み合っおいるこずを瀺しおいたす。 したがっお、むンデックスには含たれたせん。 22䞖代のブロック9の内容を蚘述する最埌のレコヌドに到達したす。 このブロックはダヌティブロックマップで蚀及されおいたすが、以前に眮き換えられたした。 そのため、新しいデヌタに再利甚され、むンデックスに入りたす。 目暙が達成されたした。

最適化


しかし、それだけではありたせん。深く掘り䞋げたす。

ペヌゞキャッシュから始めたしょう。 最初はそれを頌りにしおいたしたが、最初のバヌゞョンの負荷テストの実行を開始したずき、ペヌゞキャッシュのヒット率が20に達しおいないこずがわかりたした。 圌らは、問題を先読みするこずを提案したしたファむルを保存するのではなく、ブロックしたすが、倚くの接続を提䟛し、この構成では、ディスクの操䜜はランダムです。 順番に䜕も読むこずはほずんどありたせん。 幞いなこずに、Linuxにはposix_fadvise呌び出しがあり、ファむル蚘述子をどのように䜿甚するかをカヌネルに䌝えるこずができたす。特に、 POSIX_FADV_RANDOMフラグを枡すこずで先読みする必芁がないず蚀えたす。 このシステムコヌルはone-nioを介しお利甚可胜です。 動䜜䞭、キャッシュヒットは70〜80です。ディスクからの物理的な読み取りの数は2倍以䞊枛少し、HTTP応答の遅延は20枛少したした。

さらに進みたしょう。サヌビスのヒヌプサむズはかなり倧きくなりたす。プロセッサのTLBキャッシュの寿呜を延ばすために、JavaプロセスにHuge Pagesを含めるこずにしたした。その結果、ガベヌゞコレクション時間で顕著な利益を埗たしたGC時間/セヌフポむント合蚈時間は20〜30短瞮されたした。カヌネルの読み蟌みはより均䞀になりたしたが、HTTPレむテンシグラフぞの圱響はありたせんでした。

事件


サヌビスの開始埌すぐに、唯䞀のこれたでのむンシデントが発生したした。

営業日の終わりのある倕方、音楜の挔奏に関する苊情が支持を集めたした。ナヌザヌはお気に入りのトラックを含めるず曞きたしたが、数秒ごずに他の人や他の人から奇劙な音楜を聞いたので、プレむダヌはお気に入りのトラックを再生したず蚀いたした。かなり迅速に怜玢サヌクルを1台の車に絞り蟌みたした。ログから、最近再起動されたこずがわかりたした。簡単にするために、ブロックの内容を蚘述する2぀のディスクずむンデックスがありたした。ある指暙では、Daft Punkトラックの4番目のブロックはsdcディスクのブロック番号2にあり、Stas Mikhailovトラックのれロブロックはsddディスクのれロブロックにありたす。



マシンを再起動するず、ドラむブ名が堎所を倉曎し、その埌の結果がすべお倉わるこずがわかりたした。この問題はLinuxでよく知られおいたす。サヌバヌに耇数のディスクコントロヌラヌがある堎合、ディスクの呜名順序は保蚌されたせん。



修正は簡単であるこずが刀明したした。ディスクには、いく぀かの異なるタむプの氞続IDがありたす。ディスクのシリアル番号に基づいおWWNを䜿甚し、それらを䜿甚しおむンデックス、スナップショット、およびWALを識別したす。これはディスク自䜓のシャッフルを陀倖するものではありたせんが、シャッフル方法に関係なく、ディスク䞊のむンデックスマッピングに違反するこずはなく、垞に正しいデヌタを提䟛したす。

むンシデント分析


ナヌザヌ芁求は倚くの段階を経おノヌドの境界を越えるため、このような分散システムの問題の分析は困難です。 CDNの堎合、アップストリヌムがホヌムデヌタセンタヌであるため、すべおがさらに耇雑になりたす。そのような垌望は非垞に倚くありたす。さらに、システムは数十䞇のナヌザヌ接続に察応しおいたす。特定のナヌザヌからのリク゚ストの凊理に問題がある段階を理解するこずは非垞に困難です。

私たちは生掻を楜にしたす。ログむン時に、Open TracingおよびZipkinに䌌たタグですべおのリク゚ストをマヌクしたす。タグには、ナヌザヌの識別子、リク゚スト、リク゚ストされたトラックが含たれたす。このタグは、珟圚の接続に関連するすべおのデヌタず芁求ずずもにパむプラむン内で送信され、ノヌド間ではHTTPヘッダヌずしお送信され、受信偎によっお埩元されたす。問題に察凊する必芁がある堎合は、デバッグを有効にし、タグをログに蚘録し、特定のナヌザヌたたはトラックに関連するすべおのレコヌドを芋぀け、集玄し、クラスタヌ党䜓でリク゚ストが凊理された方法を芋぀けたす。

デヌタを送信する


ディスクから゜ケットにデヌタを送信するための䞀般的なスキヌムを怜蚎しおください。耇雑なこずはないようです。バッファを遞択し、ディスクからバッファに読み蟌み、バッファを゜ケットに送信したす。

 ByteBuffer buffer = ByteBuffer.allocate(size); int count = fileChannel.read(buffer, position); if (count <= 0) { // ... } buffer.flip(); socketChannel.write(buffer); 

このアプロヌチの問題の1぀は、2぀の隠されたデヌタコピヌがここに隠されおいるこずです。


幞いなこずに、Linuxには、sendfile()ナヌザヌスペヌスぞのコピヌをバむパスしお、ファむルから特定のオフセットから゜ケットにデヌタを盎接送信するようカヌネルに芁求できる呌び出しがありたす。そしおもちろん、この呌び出しはone-nioを通じお利甚できたす。負荷テストでは、1぀のノヌドでナヌザヌトラフィックを開始し、デヌタのみを送信する隣接ノヌドからプロキシを匷制sendfile()したした-10 Gb / sのプロセッサ負荷は䜿甚時sendfile()に0 に近かった

が、ナヌザヌスペヌスSSL゜ケットの堎合は、できたせん掻甚sendfile()しお、ファむルからデヌタをバッファ経由で送信する以倖に遞択肢はありたせん。そしお、ここで別の驚きがありたす。゜ヌスSocketChannelずを掘り䞋げる堎合FileChannel、たたは非同期プロファむラヌを䜿甚する堎合そしおこのように、戻りデヌタの過皋でpoprofilirovatシステムは、遅かれ早かれ、あなたがクラスに取埗sun.nio.ch.IOUtil沞隰するすべおのコヌルダりンするread()ず、write()これらのチャネル䞊。そのようなコヌドはそこに隠されおいたす。

 ByteBuffer bb = Util.getTemporaryDirectBuffer(dst.remaining()); try { int n = readIntoNativeBuffer(fd, bb, position, nd); bb.flip(); if (n > 0) dst.put(bb); return n; } finally { Util.offerFirstTemporaryDirectBuffer(bb); } 

これはネむティブバッファのプヌルです。heapのファむルから読み取る堎合ByteBuffer、暙準ラむブラリは最初にこのプヌルからバッファヌを取埗し、そこにデヌタを読み取り、それをヒヌプにコピヌしByteBuffer、ネむティブバッファヌをプヌルに返したす。゜ケットに曞き蟌むずき、同じこずが起こりたす。

物議を醞すスキヌム。ここでone-nioが再び助けになりたす。アロケヌタヌを䜜成したすMallocMT-実際、これはメモリヌプヌルです。SSLがあり、バッファを介しおデヌタを送信する必芁がある堎合は、Javaヒヌプの倖偎のバッファを遞択しおラップしByteBuffer、FileChannelこのバッファから䜙分なコピヌなしで読み取り、゜ケットに曞き蟌みたす。そしお、バッファをアロケヌタに返したす。

 final Allocator allocator = new MallocMT(size, concurrency); int write(Socket socket) { if (socket.getSslContext() != null) { long address = allocator.malloc(size); ByteBuffer buf = DirectMemory.wrap(address, size); int available = channel.read(buf, offset); socket.writeRaw(address, available, flags); 

ノヌドあたり100,000接続


しかし、システムの成功は、䜎レベルでの合理的な実装によっお保蚌されたせん。ここには別の問題がありたす。各ノヌドのコンベアは、最倧10䞇の同時接続に察応したす。そのようなシステムで蚈算を敎理する方法は

最初に頭に浮かぶのは、クラむアントたたは接続ごずに実行スレッドを䜜成するこずです。その䞭で、パむプラむンステヌゞを次々に実行したす。必芁に応じおブロックし、次に進みたす。しかし、このようなスキヌムでは、ディストリビュヌタヌに぀いお話しおいるため、倚くのフロヌがあるため、コンテキストの切り替えずフロヌのスタックのコストは過剰になりたす。したがっお、私たちは他の方法で行った。



各接続に察しお論理パむプラむンが䜜成されたす。これは、非同期的に盞互䜜甚するステヌゞで構成されたす。各ステヌゞには、着信リク゚ストを保存する順番がありたす。ステヌゞの実行には、小さな共通スレッドプヌルが䜿甚されたす。芁求キュヌからのメッセヌゞを凊理する必芁がある堎合、プヌルからストリヌムを取埗し、メッセヌゞを凊理しお、ストリヌムをプヌルに返したす。このスキヌムでは、デヌタがストレヌゞからクラむアントにプッシュされたす。

しかし、そのようなスキヌムには欠陥がないわけではありたせん。バック゚ンドはナヌザヌ接続よりもはるかに高速です。デヌタがパむプラむンを通過するずき、最も遅い段階で蓄積されたす。クラむアント接続゜ケットにブロックを曞き蟌む段階で。遅かれ早かれ、これはシステムの厩壊に぀ながりたす。これらの段階でキュヌを制限しようずするず、チェヌン内のナヌザヌの゜ケットぞのパむプラむンがブロックされるため、すべおが即座に停止したす。たた、共有スレッドプヌルを䜿甚しおいるため、その䞭のすべおのスレッドをブロックしたす。背圧が必芁です。

これを行うために、ゞェットストリヌムを䜿甚したした。このアプロヌチの本質は、サブスクラむバヌがパブリッシャヌからのデヌタレヌトをデマンドを䜿甚しお制埡するこずです。需芁ずは、サブスクラむバヌが凊理する準備ができおいるデヌタの量ず、既に通知されおいる以前の需芁を意味したす。パブリッシャヌにはデヌタを送信する暩利がありたすが、珟時点で环積された総需芁から送信枈みのデヌタを差し匕いたものを超えるこずはできたせん。

したがっお、システムはプッシュモヌドずプルモヌドを動的に切り替えたす。プッシュモヌドでは、サブスクラむバヌはパブリッシャヌよりも高速です。぀たり、パブリッシャヌはサブスクラむバヌからの満足されない芁求を垞に持っおいたすが、デヌタはありたせん。デヌタが衚瀺されるずすぐに、圌はすぐにそれらをサブスクラむバヌに送信したす。プルモヌドは、パブリッシャヌがサブスクラむバヌより速い堎合に発生したす。぀たり、出版瀟はデヌタを送信しおも問題ありたせん。需芁のみがれロです。サブスクラむバヌがもう少し凊理する準備ができたず蚀ったらすぐに、パブリッシャヌはすぐにデヌタの䞀郚を芁求の䞀郚ずしお送信したす。

コンベアはゞェットストリヌムに倉わりたす。各ステヌゞは、前のステヌゞではパブリッシャヌになり、次のステヌゞではサブスクラむバヌになりたす。

ゞェットストリヌムのむンタヌフェむスは非垞にシンプルに芋えたす。Publisher眲名したしょうSubscriber 、および圌は4぀のハンドラヌのみを実装する必芁がありたす。

 interface Publisher<T> { void subscribe(Subscriber<? super T> s); } interface Subscriber<T> { void onSubscribe(Subscription s); void onNext(T t); void onError(Throwable t); void onComplete(); } interface Subscription { void request(long n); void cancel(); } 

Subscription需芁を通知し、賌読を解陀できたす。どこも簡単です。

デヌタの芁玠ずしお、バむト配列は枡さず、チャンクなどの抜象化を枡したす。可胜であれば、デヌタをヒヌプにドラッグしないためにこれを行いたす。チャンクは、デヌタの読み取りByteBuffer、゜ケットぞの曞き蟌み、たたはファむルぞの曞き蟌みのみを蚱可する、非垞に限られたむンタヌフェむスを持぀デヌタリンクです。

 interface Chunk { int read(ByteBuffer dst); int write(Socket socket); void write(FileChannel channel, long offset); } 

チャンクには倚くの実装がありたす。



このAPIは単玔であるにもかかわらず、仕様ではスレッドセヌフであり、ほずんどのメ゜ッドは非ブロッキングである必芁がありたす。私たちは、觊発され粟神型付き俳優モデル、パスを遞択した䟋から公匏リポゞトリ のゞェットストリヌム。メ゜ッド呌び出しを非ブロッキングにするには、メ゜ッドを呌び出すずきに、すべおのパラメヌタヌを取埗し、メッセヌゞにラップし、実行のためにキュヌに入れお、制埡を返したす。キュヌからのメッセヌゞは厳密に順番に凊理されたす。

同期は行われたせん。コヌドはシンプルで簡単です。
状態は3぀のフィヌルドでのみ説明されおいたす。各パブリッシャヌたたはサブスクラむバヌには、着信メッセヌゞが収集されるメヌルボックスず、このタむプのすべおのステヌゞ間で分割される゚グれキュヌタヌがありたす。AtomicBoolean連続した目芚めの前に起こる。

 // Incoming messages final Queue<M> mailbox; // Message processing works here final Executor executor; // To ensure HB relationship between runs final AtomicBoolean on = new AtomicBoolean(); 

呌び出しはメッセヌゞに倉わりたす

 @Override void request(final long n) { enqueue(new Request(n)); } void enqueue(final M message) { mailbox.offer(message); tryScheduleToExecute(); } 

方法tryScheduleToExecute()

 if (on.compareAndSet(false, true)) { try { executor.execute(this); } catch (Exception e) { ... } } 

方法run()

 if (on.get()) try { dequeueAndProcess(); } finally { on.set(false); if (!messages.isEmpty()) { tryScheduleToExecute(); } } } 

方法dequeueAndProcess()

 M message; while ((message = mailbox.poll()) != null) { // Pattern match if (message instanceof Request) { doRequest(((Request) message).n); } else { 
 } } 

完党にノンブロッキングの実装ができたした。コヌドシンプルか぀䞀貫性のある、なしにvolatile、Atomic*競合など。システム党䜓では、100,000の接続を凊理するために合蚈200のスレッドがありたす。

最埌に


実皌働では、12台のマシンがありたすが、垯域幅には2倍以䞊のマヌゞンがありたす。通垞モヌドの各マシンは、数十䞇の接続を通じお最倧10ギガビット/秒を提䟛したす。スケヌラビリティず埩元力を提䟛したした。すべおがJavaずone-nioで曞かれおいたす。



これは、サヌバヌ偎からナヌザヌに䞎えられる最初のバむトたでのグラフです。 20ミリ秒未満の99パヌセンタむル。青いグラフは、ナヌザヌぞのHTTPSデヌタの戻り倀です。赀いグラフは、sendfile()HTTP を介したレプリカからプロキシぞのデヌタの戻り倀です。

実際、実皌働環境でのキャッシュヒットは97であるため、グラフはトラックリポゞトリのレむテンシを瀺しおいたす。キャッシュミスの堎合はそこからデヌタを取埗したすが、これもペタバむトのデヌタを考慮するず悪くありたせん。



ディスクから戻るずきに75パヌセンタむルを芋るず、1ミリ秒埌に最初のバむトがナヌザヌに飛んでいたす。クラスタヌ内のレプリカはさらに高速で通信したす-300ÎŒsの責任がありたす。぀たり0.7ミリ秒はプロキシのコストです。

この蚘事では、高速性ず優れたフォヌルトトレランスの䞡方を備えたスケヌラブルで負荷の高いシステムを構築する方法を瀺したした。成功したこずを願っおいたす。

Source: https://habr.com/ru/post/J434206/


All Articles