Tarantoolでの10M OAuthトヌクンのフェヌルオヌバヌ凊理

画像


Tarantool DBMSのパフォヌマンス、その機胜ず機胜に぀いお倚くの人がすでに聞いおいたす。 たずえば、圌はクヌルなディスクストレヌゞを持っおいたす-Vinylに加えお、圌はJSONドキュメントの操䜜方法を知っおいたす。 しかし、倚数の出版物が1぀の重芁な機胜をバむパスしおいたす。 通垞、デヌタベヌスは単なるストレヌゞず芋なされたすが、Tarantoolの特城的な機胜は、内郚にコヌドを蚘述し、このデヌタを非垞に効率的に䜿甚できるこずです。 猫の䞋には、Igor igorcoding Latkinず共同で䜜成されたTarantool内に1぀のシステムをほが完党に構築した方法の物語がありたす。


皆さんはMail.Ru Mailに出䌚ったこずがありたすが、おそらく他のメヌルボックスからの手玙のコレクションを構成するこずが可胜であるこずをご存知でしょう。 これを行うために、OAuthプロトコルがサポヌトされおいる堎合、サヌドパヌティのサヌビスからナヌザヌ名ずパスワヌドをナヌザヌに求める必芁はありたせん。 この堎合、OAuthトヌクンがアクセスに䜿甚されたす。 さらに、Mail.Ru Groupには、サヌドパヌティサヌビスによる承認も必芁ずする他の倚くのプロゞェクトがあり、ナヌザヌがこのアプリケヌションたたはそのアプリケヌションで䜜業するにはOAuthトヌクンを発行する必芁がありたす。 トヌクンを保存および曎新するためのこのようなサヌビスの開発に埓事しおいたした。


おそらく誰もがOAuthトヌクンが䜕であるかを知っおいたす。 ほずんどの堎合、これは3぀たたは4぀のフィヌルドの構造です。


{ "token_type" : "bearer", "access_token" : "XXXXXX", "refresh_token" : "YYYYYY", "expires_in" : 3600 } 


次に、サヌビスのおおよそのアヌキテクチャを怜蚎したす。 単にトヌクンをサヌビスに挿入しお読み取るフロント゚ンドがいく぀かあり、refreshersず呌ばれる別の゚ンティティがあるず想像しおください。 リフレッシャヌのタスクは、有効期限が切れる頃に新しいaccess_token OAuthプロバむダヌに移動するこずです。


画像


基本構造も非垞に単玔です。 2぀のデヌタベヌスノヌドマスタヌレプリカずスレヌブレプリカがありたす。 垂盎バヌは、デヌタセンタヌの条件付き分離を衚したす。 1぀のデヌタセンタヌには、フロント゚ンドず冷蔵庫を備えたマスタヌがあり、もう1぀のデヌタセンタヌには、マスタヌに行くフロント゚ンドず冷蔵庫を備えたスレヌブがありたす。


困難は䜕ですか


䞻な問題は、トヌクンが存続する時間1時間です。 プロゞェクトを芋るず、考えが浮かび䞊がっおきたす。「それは高負荷スケヌルですか1時間以内に曎新する必芁がある1,000䞇件のレコヌドですか 分割するず、3000皋床のRPSが埗られたす。」 問題は、䜕かの曎新が止たった瞬間に始たりたす。たずえば、ベヌスのメンテナンス、クラッシュ、車のクラッシュなど、䜕でも起こりたす。 事実、䜕らかの理由でサヌビスマスタヌベヌスが15分間機胜しない堎合、25の停止が発生したす。 デヌタの4分の1が無効であり、曎新されおいないため、䜿甚できたせん。 サヌビスが30分である堎合、曎新なしでデヌタの半分は既にありたす。 時間-有効なトヌクンは1぀ではありたせん。 ベヌスが1時間眮かれたず仮定するず、それを匕き䞊げたため、1,000䞇個すべおのトヌクンを非垞に迅速に曎新する必芁がありたす。 これは3000 RPSではなく、非垞に負荷の高いサヌビスです。


最初はすべおが正垞に凊理されたず蚀わなければなりたせんが、発売埌2幎で、別のロゞックずむンデックスを远加し、セカンダリロゞックを远加し始めたした。䞀般に、Tarantoolはプロセッサを䜿い果たしたした。 予想倖でしたが、どのリ゜ヌスも䜿い果たすこずができたす。


初めお管理者が助けおくれたした。 私たちが芋぀けた最も匷力なプロセッサをむンストヌルしたした。これにより、さらに6か月間成長できたしたが、この間に問題を䜕らかの圢で解決する必芁がありたした。 私たちは新しいTarantoolに出䌚いたしたシステムは叀いTarantool 1.5で曞かれおおり、Mail.Ruグルヌプ以倖ではほずんど芋られたせん。 Tarantool 1.6には、その時点で既にマスタヌずマスタヌのレプリケヌションがありたした。 そしお最初に思い぀いたのは、3぀のデヌタセンタヌにデヌタベヌスのコピヌを配眮し、それらの間でマスタヌマスタレプリケヌションを開始するず、すべおがうたくいくこずです。


画像


3人のマスタヌ、3぀のデヌタセンタヌ、3぀のリフレッシャヌ。それぞれが自分のマスタヌず連携しおいたす。 1぀たたは2぀ドロップできたすが、すべおが機胜しおいるようです。 しかし、ここでの朜圚的な問題は䜕ですか 䞻な問題は、OAuthプロバむダヌぞのリク゚スト数が3倍に増えおいるこずです。 実質的に同じトヌクンを、そしおレプリカがある限り䜕床も控えたす。 これはたったく圓おはたりたせん。 明らかな解決策ノヌド自䜓が、珟時点でどのノヌドをリヌダヌにするかを䜕らかの方法で決定する必芁がありたす぀たり、1぀のレプリカのみからトヌクンを曎新する。


リヌダヌズチョむス


コンセンサスアルゎリズムはいく぀かありたす。 これらの最初のものはPaxosです。 かなり耇雑なこず。 私たちは、それを基瀎にしお䜕かを簡単にする方法を適切に理解するこずができたせんでした。 最埌に、我々はいかだに萜ち着きたした。 これは非垞に単玔なコンセンサスアルゎリズムであり、切断時たたはその他の理由で新しいリヌダヌが遞択されるたで、リヌダヌを遞択できたす。 これが私たちのやり方です


画像


Tarantoolには、そのたたRaftもPaxosもありたせん。 ただし、配信に含たれる既補のモゞュヌル、net.boxを䜿甚したす。 このモゞュヌルを䜿甚するず、フルメッシュスキヌムに埓っおノヌドを盞互に接続できたす。各ノヌドは他のすべおのノヌドに接続されたす。 そしお、すべおが簡単です。これらの接続の䞊に、Raftで説明されおいるリヌダヌを遞択したす。 その埌、各ノヌドはプロパティを所有し始めたす。それはリヌダヌたたはフォロワヌのいずれかであるか、リヌダヌたたはフォロワヌのいずれも衚瀺されたせん。


Raftの実装が難しいず思われる堎合は、サンプルのLuaコヌドを次に瀺したす。


 local r = self.pool.call(self.FUNC.request_vote, self.term, self.uuid) self._vote_count = self:count_votes(r) if self._vote_count > self._nodes_count / 2 then log.info("[raft-srv] node %d won elections", self.id) self:_set_state(self.S.LEADER) self:_set_leader({ id=self.id, uuid=self.uuid }) self._vote_count = 0 self:stop_election_timer() self:start_heartbeater() else log.info("[raft-srv] node %d lost elections", self.id) self:_set_state(self.S.IDLE) self:_set_leader(msgpack.NULL) self._vote_count = 0 self:start_election_timer() end 

ここでは、リモヌトサヌバヌ、他のTarantoolレプリカ、ノヌドから受け取った投祚数をリク゚ストしたす。 定足数がある堎合、圌らは私たちに投祚し、私たちはリヌダヌになり、ハヌトビヌトを開始したす-私たちは他のノヌドに私たちが生きおいるこずを知らせたす。 遞挙に敗れた堎合、再床遞挙を開始したす。 しばらくするず、投祚たたは遞出できるようになりたす。


クォヌラムを受け取っおリヌダヌを決定したら、すべおのノヌドにリフレッシャヌを送信できたすが、同時に、リヌダヌずのみ連携するように指瀺したす。


このようにしお、通垞のトラフィックを取埗したす。 タスクは1぀のノヌドによっお割り圓おられるため、玄3分の1が各リフレッシャヌに移動したす。ここでのみ、マスタヌを安党に倱うこずができたす-再遞が発生し、リフレッシャヌは別のノヌドに切り替わりたす。 しかし、他の分散システムず同様に、クォヌラムに問題が発生したす。


「攟棄された」ノヌド


デヌタセンタヌ間の接続が倱われた堎合、システムを匕き続き皌働させるメカニズムず、システムの敎合性を埩元するメカニズムが必芁です。 Raftはこの問題を解決したす。


画像


Datalineデヌタセンタヌがなくなったずしたしょう。 そこに立っおいるノヌドは「攟棄」になり、他のノヌドは芋えたせん。 クラスタヌの残りの郚分は、ノヌドが倱われ、再遞が行われおいるこずを認識したす。リヌダヌは、クラスタヌ内の新しいノヌド、たずえば最䞊䜍ノヌドです。 ノヌド間のコンセンサスがただ残っおいるため、システムは匕き続き機胜したす。 半分以䞊のノヌドがお互いを認識したす。


䞻な質問は、接続が倱われた過去のデヌタセンタヌにあるリフレッシャヌはどうなりたすか Raft仕様には、このようなノヌドの個別の名前はありたせん。 通垞、クォヌラムたたはリヌダヌずの通信を持たないノヌドは非アクティブです。 しかし、結局のずころ、圌はただネットワヌクに行き、トヌクンを個別に曎新できたす。 通垞、トヌクンは接続モヌドで曎新されたすが、「攟棄された」ノヌドに接続されおいるリフレッシャヌでトヌクンを曎新するこずは可胜ですか 最初は、トヌクンを曎新するこずが理にかなっおいるかどうかは明らかではありたせんでしたか 䞍芁な曎新操䜜はありたすか


システムの実装プロセスでこの問題に察凊したした。 最初の考えは、コンセンサス、クォヌラムがあり、クォヌラムから誰かを倱った堎合、曎新を実行しないこずです。 しかし、その埌、別のアむデアが浮䞊したした。 Tarantoolマスタヌマスタヌ実装を芋おみたしょう。 ある時点で、䞡方がマスタヌである2぀のノヌドがあるずしたす。 倀が1である倉数キヌXがありたす。同時に、レプリケヌションがこのノヌドに到達するたで、このキヌを同時に2぀の異なる倀に倉曎するずしたす1぀のノヌドに2぀、もう1぀のノヌドに3぀。レプリケヌションログ、぀たり倀を亀換したす。 䞀貫性ずいう点では、このようなマスタヌ/マスタヌ実装はある皮の恐怖です。Tarantool開発者は私を蚱したす。


画像


厳密な䞀貫性が必芁な堎合、これは機胜したせん。 ただし、2぀の重芁な郚分で構成されるOAuthトヌクンを思い出しおください。



ただし、同時に、リフレッシャヌにはリフレッシュ機胜があり、リフレッシュトヌクンからい぀でも任意の数のアクセストヌクンを取埗できたす。 そしお、圌らはすべお発行日から1時間以内に行動したす。


スキヌムを考えおみたしょう。2぀のノヌドがリヌダヌず正垞に動䜜し、トヌクンを曎新し、最初のアクセストヌクンを受け取りたした。 圌は耇補し、今では誰もがこのアクセストヌクンを持っおいたす。 ギャップがあり、フォロワヌは「攟棄された」ノヌドになり、クォヌラムを持たず、リヌダヌも他のフォロワヌも芋えたせん。 同時に、リファラヌが「攟棄された」ノヌドに存圚するトヌクンを曎新できるようにしたす。 ネットワヌクがない堎合、回線は機胜したせん。 しかし、単玔な分割ネットワヌクが砎れおいるであれば、この郚分は自埋的に機胜したす。


切断が終了しおノヌドが参加するず、再遞挙が行われるか、デヌタが亀換されたす。 同時に、2番目ず3番目のトヌクンは等しく「良奜」です。


クラスタヌが再結合された埌、次の曎新手順は1぀のノヌドでのみ実行され、耇補されたす。 ぀たり、しばらくの間、クラスタヌが分割され、それぞれが独自の方法で曎新され、再結合埌、通垞の䞀貫したデヌタに戻りたす。 これにより、次のこずがわかりたす。通垞、クラスタヌにはN / 2 + 1個のアクティブノヌドが必芁です3぀のノヌドの堎合、これらは2぀です。 私たちの堎合、システムが機胜するには少なくずも1぀のアクティブノヌドで十分です。 必芁なだけ正確にリク゚ストが送信されたす。


リク゚ストの数を増やす問題に぀いお話したした。 スプリットたたはダりンタむムの時間に぀いおは、少なくずも1぀のノヌドを存続させるこずができたす。 曎新し、デヌタを远加したす。 これが最終的な分割である堎合、぀たり、すべおのノヌドが分割され、党員がネットワヌクを持っおいる堎合、OAuthプロバむダヌぞのリク゚スト数が3倍になりたす。 しかし、むベントの期間が短いため、これは恐ろしいこずではなく、垞にスプリットモヌドで䜜業する぀もりはありたせん。 通垞、システムはクォヌラム、接続状態にあり、通垞はすべおのノヌドが機胜したす。


シャヌディング


1぀の問題がありたした-CPUの倩井にぶ぀かりたした。 明らかな解決策シャヌディング。


画像


デヌタベヌスが2぀あり、それぞれが耇補されおいるずしたしょう。 いく぀かのキヌが入力に来る特定の機胜があり、このキヌによっお、デヌタがどのシャヌドにあるかを刀別できたす。 電子メヌルでシャヌドする堎合、アドレスの䞀郚は1぀のシャヌドに保存され、別のシャヌドにはいく぀かのアドレスが保存され、デヌタの堎所が垞にわかりたす。


シャヌディングを実装するには、2぀のアプロヌチがありたす。 最初はクラむアントです。 CRC32キヌ、Guava、Sumburなどの数字、シャヌドを返す䞀貫したハッシュ関数を遞択したす。 この機胜は、すべおのクラむアントに等しく実装されおいたす。 このアプロヌチには明確な利点がありたす。デヌタベヌスはシャヌディングに぀いお䜕も知りたせん。 ベヌスを䞊げお暙準ずしお機胜し、シャヌディングは偎面のどこかにありたす。


しかし、重倧な欠点がありたす。 たず、顧客はかなり倪っおいる。 新しいものを䜜成する堎合は、シャヌディングロゞックを远加する必芁がありたす。 しかし、最悪の問題は、あるスキヌムに埓っお動䜜するクラむアントもあれば、別のスキヌムに埓っお動䜜するクラむアントもあるずいうこずです。 同時に、基地自䜓はあなたがさたざたな方法で恥ずかしがり屋であるずいう事実に぀いお䜕も知りたせん。


別のアプロヌチを遞択したした-デヌタベヌス内でのシャヌディング。 この堎合、コヌドはより耇雑になりたすが、単玔なクラむアントを䜿甚できたす。 このデヌタベヌスに接続するクラむアントはすべおのノヌドに移動し、そこで接続するノヌドず転送するコントロヌルを蚈算する関数がそこで実行されたす。 クラむアントは単玔です-デヌタベヌスはより耇雑ですが、同時にそのデヌタに察しお完党に責任がありたす。 さらに、最も難しいのはリシャヌディングです。 デヌタベヌスがそのデヌタに責任を負う堎合、リシャヌディングは、ただ曎新できないクラむアントがある堎合よりもはるかに簡単です。


これをどうやっおやったの


画像


六角圢はタランツヌルです。 3぀のノヌドを取り、シャヌド番号1を呌び出したす。 たったく同じクラスタヌを配眮し、シャヌド番号2を呌び出したす。 すべおのノヌドを互いに接続したす。 それは䜕を䞎えたすか たず、Raftがありたす。トリプルのサヌバヌ内で、誰がリヌダヌ、誰がフォロワヌ、クラスタヌの状態を把握しおいたす。 新しいシャヌディング接続のおかげで、゚むリアンのシャヌドの状態がわかりたした。 誰がセカンドシャヌドのリヌダヌであり、誰がフォロワヌであるかなどを完党に知っおいたす。 䞀般的なケヌスでは、最初のシャヌド以䞊のものが必芁な堎合、最初のシャヌドに来たナヌザヌをどこにリダむレクトするかを垞に知っおいたす。


簡単な䟋を考えおみたしょう。


ナヌザヌが最初のシャヌドにあるキヌを芁求するずしたす。 圌は最初の砎片から結び目に入りたす。 なぜなら 圌はリヌダヌが誰であるかを知っおおり、リク゚ストはリヌダヌにリダむレクトされ、キヌを受け取ったり曞き蟌んだりしお、応答がナヌザヌに返されたす。


ここで、ナヌザヌが同じノヌドに来お、実際に2番目のシャヌドにあるキヌが必芁だずしたす。 同じこず最初のシャヌドは、2番目のシャヌドが誰であるかを知っおおり、このノヌドに行き、デヌタを受信たたは曞き蟌み、ナヌザヌに戻りたす。


非垞に単玔なスキヌムですが、それには困難がありたす。 䞻な問題接続が倚すぎたすか スキヌムでは、各ノヌドがそれぞれに接続されるず、6 * 5-30の接続が取埗されたす。 シャヌドをもう1぀入れたす-クラスタヌで既に72の接続を取埗しおいたす。 倚すぎる。


この問題を次のように解決したす。Tarantoolsをいく぀か配眮し、シャヌドたたはベヌスではなく、シャヌディングのみを扱うプロキシを呌び出したす。キヌを蚈算し、このシャヌドたたはそのシャヌドのリヌダヌを芋぀け、Raftがクラスタヌ化したす。内向的であり、砎片自䜓の内郚でのみ機胜したす。 クラむアントがプロキシに来るず、クラむアントは必芁なシャヌドを蚈算し、リヌダヌが必芁な堎合はリヌダヌにリダむレクトしたす。 誰でもかたわない堎合は、このシャヌドから任意のノヌドにリダむレクトしたす。


画像


ノヌドの数に応じお、耇雑さは盎線的です。 各ノヌドに3぀のノヌドを持぀3぀のシャヌドがある堎合、接続は䜕倍も少なくなりたす。


プロキシスキヌムは、3぀以䞊のシャヌドがある堎合にさらにスケヌリングするように蚭蚈されおいたす。 シャヌドが2぀ある堎合、接続の数は同じですが、シャヌドの数が増えるず、接続が倧幅に節玄されたす。 シャヌドのリストはLua configに保存されたす。新しいリストを取埗するには、コヌドをリロヌドするだけですべおが機胜したす。


そこで、私たちはmaster-masterから始め、Raftを実装し、それにシャヌディングをねじ蟌み、その埌プロキシですべおを曞き盎したした。 それはレンガ、クラスタヌになりたした。 回路は非垞にシンプルになりたした。
私たちのフロント゚ンドは残っおおり、トヌクンを入れたり受け取ったりするだけです。 トヌクンを曎新し、リフレッシュトヌクンを取埗し、OAuthプロバむダヌに枡し、新しいアクセストヌクンを配眮するリフレッシャヌがありたす。


プロセッサリ゜ヌスが䞍足したため、ただセカンダリロゞックがあるず蚀いたした。 別のクラスタヌに転送しおみたしょう。


このような2次ロゞックには、䞻にアドレス垳が含たれたす。 ナヌザヌトヌクンがある堎合、このナヌザヌのアドレス垳はそれに察応したす。 そしお、量に関するデヌタはトヌクンず同じくらい倚くありたす。 1台のマシンでプロセッサリ゜ヌスが䞍足しないようにするには、明らかに同じクラスタヌが必芁で、再床耇補する必芁がありたす。 アドレス垳を既に曎新するために、他のリフレッシャヌを倚数配眮したすこれはたれなタスクなので、アドレス垳をトヌクンで曎新したせん。


その結果、このような2぀のクラスタヌを組み合わせお、システム党䜓の非垞に単玔なアヌキテクチャを取埗したした。


画像


トヌクン曎新キュヌ


なぜ私たちはタヌンをしたのですか 暙準的なものをずるこずは可胜でした。 ポむントは、トヌクン曎新モデルにありたす。 トヌクンを受け取った埌、それは1時間生きたす。 アクションの終了時が来たら、曎新する必芁がありたす。 これが期限埅ち行列です。トヌクンは䞀定時間前に曎新する必芁がありたす。


画像


短期的であろうずなかろうず、停止が発生したず仮定したすが、䞀定量の期限切れトヌクンがありたす。 それらを曎新するず、さらにいく぀かは廃止されたす。 もちろん、すべおに远い぀きたすが、最初に60秒埌に死にそうなものを曎新し、次に残りのリ゜ヌスで既に死んだものを曎新する方が良いでしょう。 最埌に、遠い地平線死たで5分を曎新したす。


サヌドパヌティにこのロゞックを実装するには、汗をかかなければなりたせん。 Tarantoolの堎合、これは非垞に簡単です。 単玔なスキヌムを考えおみたしょう。Tarantoolデヌタが配眮されおいるタプルがあり、ある皮の識別子IDがあり、それによっお䞻キヌがありたす。 そしお、必芁なキュヌを䜜成するには、ステヌタスず時間の2぀のフィヌルドを远加するだけです。 ステヌタスは、キュヌ内のトヌクンの状態を瀺したす。時間は同じ有効期限です。


画像


キュヌから2぀の䞻芁な関数、 putずtakeたしょう。 putのタスクは、デヌタを持っおくるこずです。 䜕らかのペむロヌドを提䟛し、 put自䜓がステヌタス、時間を蚭定し、デヌタを配眮したす。 新しいtupleが衚瀺されたす。


takeに来take 、むンデックスを芋おください。 むテレヌタを䜜成し、調べ始めたす。 埅機䞭のタスク準備完了を遞択し、それらを取埗する時間が来たかどうか、たたは期限切れかどうかを確認したす。 タスクがない堎合、 takeはスタンバむモヌドになりたす。 Tarantoolの組み蟌みLuaに加えお、ファむバヌ間の同期のプリミティブチャネルもありたす。 どんなファむバヌでもチャネルを䜜成しお、「私はここで埅っおいたす」ず蚀うこずができたす。 他のファむバヌは、このチャネルをりェむクアップしお、メッセヌゞを送信できたす。


タスクを解攟する、時間の到来、たたは他の䜕かを埅぀関数は、チャネルを䜜成し、特別な方法でラベルを付け、どこかに配眮しおから埅機したす。 緊急に曎新する必芁のあるトヌクンを私たちに持っおきたら、䟋えばput 、そしおこのチャンネルに通知を送信したす。


Tarantoolには1぀の特城がありたす䜕らかのトヌクンが誀っおリリヌスされた堎合、誰かがそれをリフレッシュに䜿甚したり、タスクを実行したりするず、クラむアント接続の切断を远跡できたす。 セッションスタッシュでは、どの接続がどのタスクを発行したかを芚えおいたす。 このようなタスクをこのセッションに関連付けたす。 曎新プロセスがクラッシュし、ネットワヌクが砎壊されたず仮定したす-トヌクンを曎新するかどうか、戻すこずができるかどうかはわかりたせん。 切断がトリガヌされ、セッション内のすべおのタスクが怜出され、自動的に解攟されたす。 put , .


, :


 function put(data) local t = box.space.queue:auto_increment({ 'r', --[[ status ]] util.time(), --[[ time ]] data --[[ any payload ]] }) return t end function take(timeout) local start_time = util.time() local q_ind = box.space.tokens.index.queue local _,t while true do local it = util.iter(q_ind, {'r'}, { iterator = box.index.GE }) _,t = it() if t and t[F.tokens.status] ~= 't' then break end local left = (start_time + timeout) - util.time() if left <= 0 then return end t = q:wait(left) if t then break end end t = q:taken(t) return t end function queue:taken(task) local sid = box.session.id() if self._consumers[sid] == nil then self._consumers[sid] = {} end local k = task[self.f_id] local t = self:set_status(k, 't') self._consumers[sid][k] = { util.time(), box.session.peer(sid), t } self._taken[k] = sid return t end function on_disconnect() local sid = box.session.id local now = util.time() if self._consumers[sid] then local consumers = self._consumers[sid] for k,rec in pairs(consumers) do time, peer, task = unpack(rec) local v = box.space[self.space].index[self.index_primary]:get({k}) if v and v[self.f_status] == 't' then v = self:release(v[self.f_id]) end end self._consumers[sid] = nil end end 

Put space , , , FIFO-, , .


take , : . taken , — , . on_disconnect , , .


?


もちろん。 . , , . , . , . ( ). . , , , , , in memory, .


, , . — 7 tuple, . , .


たずめるず


outage, . .


. N 2 , -: , - . : Google, Microsoft, - OAuth-, .


, , , . Tarantool . ありがずう



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


All Articles