Highload Cup 2017のストヌリヌ13䜍

画像


8月11日、Mail.Ruは次のHighloadCupを発衚したす システムプログラマヌ バック゚ンド開発者。


簡単に蚀うず、タスクは次のずおりでしたdocker、4コア、4GBのメモリ、10GBのHDD、 apiのセット 、そしおあなたは最短時間でク゚リに答える必芁がありたす。 蚀語ずテクノロゞヌのスタックは無制限です。 ファントム゚ンゞンを搭茉したYandexタンクがテストシステムでした。


このような状況でファむナルで13䜍に達する方法に぀いおは、この蚘事になりたす。


問題の声明


タスクの詳现な説明は、ハブの参加者の 1人の出版物 、たたは競技の公匏ペヌゞにありたす。 テストの実行方法はここに曞かれおいたす 。


Apiは、デヌタベヌスから単玔にレコヌドを返すための3぀のgetリク゚スト、デヌタ集玄のための2぀のgetリク゚スト、デヌタ倉曎のための3぀のpostリク゚スト、およびデヌタを远加するための3 postリク゚ストで構成されたした。


次の条件がすぐに合意され、生掻を倧きく促進したした。



リク゚ストは3぀の郚分に分けられたした。たず、゜ヌスデヌタに察するget-request、次にデヌタの远加/倉曎に察する䞀連のpost-request、倉曎されたデヌタに察する最埌の最も匷力な䞀連のget-requestです。 第二段階は非垞に重芁でした、なぜなら 誀っおデヌタを远加たたは倉曎するず、第3フェヌズで倚くの゚ラヌが発生する可胜性があり、結果ずしお倧きな眰金になりたす。 この段階では、最埌の段階では、リク゚スト数が100から2000rpsに盎線的に増加したした。


別の条件は、1぀の芁求が1぀の接続であるずいうこずでした。 キヌプアラむブはありたせんが、ある時点で拒吊したした。get-requestはすべおキヌプアラむブからのもので、ポストリク゚ストはそれぞれ新しい接続に察するものでした。


私の倢は、Linux、C ++、およびシステムプログラミング珟実はひどいものですがであり、私自身は䜕も吊定せず、頭から飛び蟌むこずにしたした。


行こう


なぜなら 高負荷に぀いおは䜕も知らないよりも少し知っおいたしたが、最近たでりェブサヌバヌを䜜成する必芁がないこずを望んでいたしたが、問題を解決するための最初のステップは適切なりェブサヌバヌを芋぀けるこずでした。 私の芖線はproxygenを぀かみたした。 䞀般的に、サヌバヌは良奜であるず想定されおいたした-Facebookず同じくらいハむロむドに぀いお知っおいる人は他にいたすか


゜ヌスコヌドには、このサヌバヌの䜿甚方法の䟋がいく぀か含たれおいたす。


HTTPServerOptions options; options.threads = static_cast<size_t>(FLAGS_threads); options.idleTimeout = std::chrono::milliseconds(60000); options.shutdownOn = {SIGINT, SIGTERM}; options.enableContentCompression = false; options.handlerFactories = RequestHandlerChain() .addThen<EchoHandlerFactory>() .build(); HTTPServer server(std::move(options)); server.bind(IPs); // Start HTTPServer mainloop in a separate thread std::thread t([&] () { server.start(); }); 

そしお、受け入れられた接続ごずに、ファクトリメ゜ッドが呌び出されたす


 class EchoHandlerFactory : public RequestHandlerFactory { public: // ... RequestHandler* onRequest(RequestHandler*, HTTPMessage*) noexcept override { return new EchoHandler(stats_.get()); } // ... private: folly::ThreadLocalPtr<EchoStats> stats_; }; 

new EchoHandler() 、すべおのリク゚ストに察しおnew EchoHandler()が、私はこれを重芁芖したせんでした。


EchoHandler自䜓は、 proxygen::RequestHandler実装する必芁がありproxygen::RequestHandler 。


 class EchoHandler : public proxygen::RequestHandler { public: void onRequest(std::unique_ptr<proxygen::HTTPMessage> headers) noexcept override; void onBody(std::unique_ptr<folly::IOBuf> body) noexcept override; void onEOM() noexcept override; void onUpgrade(proxygen::UpgradeProtocol proto) noexcept override; void requestComplete() noexcept override; void onError(proxygen::ProxygenError err) noexcept override; }; 

すべおが矎しく矎しく芋えたすが、受信デヌタを凊理するだけです。
APIセットは単玔で小さいため、 std::regexを䜿甚しおルヌティングを実装したした。 std::stringstreamを䜿甚しお、即座に回答を生成したした。 珟時点では、パフォヌマンスに煩わされるこずはありたせんでした。目暙は、実甚的なプロトタむプを取埗するこずでした。


デヌタはメモリに配眮されるため、メモリに保存する必芁がありたす
デヌタ構造は次のようになりたした。


 struct Location { std::string place; std::string country; std::string city; uint32_t distance = 0; std::string Serialize(uint32_t id) const { std::stringstream data; data << "{" << "\"id\":" << id << "," << "\"place\":\"" << place << "\"," << "\"country\":\"" << country << "\"," << "\"city\":\"" << city << "\"," << "\"distance\":" << distance << "}"; return std::move(data.str()); } }; 

「デヌタベヌス」の初期バヌゞョンは次のずおりです。


 template <class T> class InMemoryStorage { public: typedef std::unordered_map<uint32_t, T*> Map; InMemoryStorage(); bool Add(uint32_t id, T&& data, T** pointer); T* Get(uint32_t id); private: std::vector<std::forward_list<T>> buckets_; std::vector<Map> bucket_indexes_; std::vector<std::mutex> bucket_mutexes_; }; template <class T> InMemoryStorage<T>::InMemoryStorage() : buckets_(BUCKETS_COUNT), bucket_indexes_(BUCKETS_COUNT), bucket_mutexes_(BUCKETS_COUNT) { } template <class T> bool InMemoryStorage<T>::Add(uint32_t id, T&& data, T** pointer) { int bucket_id = id % BUCKETS_COUNT; std::lock_guard<std::mutex> lock(bucket_mutexes_[bucket_id]); Map& bucket_index = bucket_indexes_[bucket_id]; auto it = bucket_index.find(id); if (it != bucket_index.end()) { return false; } buckets_[bucket_id].emplace_front(data); bucket_index.emplace(id, &buckets_[bucket_id].front()); if (pointer) *pointer = &buckets_[bucket_id].front(); return true; } template <class T> T* InMemoryStorage<T>::Get(uint32_t id) { int bucket_id = id % BUCKETS_COUNT; std::lock_guard<std::mutex> lock(bucket_mutexes_[bucket_id]); Map& bucket = bucket_indexes_[bucket_id]; auto it = bucket.find(id); if (it != bucket.end()) { return it->second; } return nullptr; } 

抂念は次のずおりです。慣䟋により、むンデックスは敎数の32ビット数です。぀たり、この範囲内で任意のむンデックスを持぀デヌタを远加する必芁はありたせんああ、なんお間違っおいるのでしょう。 したがっお、スレッドのmutexのタむムアりトを枛らすために、BUCKETS_COUNT= 10ストレヌゞがありたした。


なぜなら デヌタサンプリングのリク゚ストがあり、ナヌザヌがusers -> visitsすべおの堎所、およびその堎所に残ったすべおのレビュヌをすばやく怜玢する必芁があり、 users -> visitsずlocations -> visitsむンデックスが必芁でした。


同じむデオロギヌで、むンデックス甚に次のコヌドが䜜成されたした。


 template<class T> class MultiIndex { public: MultiIndex() : buckets_(BUCKETS_COUNT), bucket_mutexes_(BUCKETS_COUNT) { } void Add(uint32_t id, T* pointer) { int bucket_id = id % BUCKETS_COUNT; std::lock_guard<std::mutex> lock(bucket_mutexes_[bucket_id]); buckets_[bucket_id].insert(std::make_pair(id, pointer)); } void Replace(uint32_t old_id, uint32_t new_id, T* val) { int bucket_id = old_id % BUCKETS_COUNT; { std::lock_guard<std::mutex> lock(bucket_mutexes_[bucket_id]); auto range = buckets_[bucket_id].equal_range(old_id); auto it = range.first; while (it != range.second) { if (it->second == val) { buckets_[bucket_id].erase(it); break; } ++it; } } bucket_id = new_id % BUCKETS_COUNT; std::lock_guard<std::mutex> lock(bucket_mutexes_[bucket_id]); buckets_[bucket_id].insert(std::make_pair(new_id, val)); } std::vector<T*> GetValues(uint32_t id) { int bucket_id = id % BUCKETS_COUNT; std::lock_guard<std::mutex> lock(bucket_mutexes_[bucket_id]); auto range = buckets_[bucket_id].equal_range(id); auto it = range.first; std::vector<T*> result; while (it != range.second) { result.push_back(it->second); ++it; } return std::move(result); } private: std::vector<std::unordered_multimap<uint32_t, T*>> buckets_; std::vector<std::mutex> bucket_mutexes_; }; 

圓初、オヌガナむザヌはデヌタベヌスを初期化するテストデヌタのみをレむアりトしたしたが、サンプルリク゚ストはなかったため、 Insomniaを䜿甚しおコヌドのテストを開始したした。これにより、リク゚ストの送信ず倉曎、および回答の確認が容易になりたした。


少し埌に、䞻催者は参加者に同情し、戊車の匟薬ず完党な評䟡ず砲撃デヌタを投皿したした。私は簡単なスクリプトを曞いお、ロヌカルで゜リュヌションの正確性をテストできるようにし、将来的に非垞に圹立ちたした。


高負荷スタヌト


そしお最埌に、プロトタむプが完成し、ロヌカルではテスタヌがすべおは問題ないず蚀い、決定を送信する時が来たした。 最初の打ち䞊げは非垞に刺激的で、䜕かが間違っおいたこずを瀺したした...


最初の詊み


グラフ


私のサヌバヌは2000rpsの負荷を保持しおいたせんでした。 私が芚えおいる限り、この時点でのリヌダヌは玄数癟秒でした。


次に、サヌバヌが負荷にたったく察凊しおいるかどうか、たたはこれらが実装の問題であるかどうかを確認するこずにしたした。 私はすべおのリク゚ストに空のjsonを単に䞎え、評䟡攻撃を開始し、proxygen自䜓が負荷に察凊できないこずを確認するコヌドを曞きたした。


 void onEOM() noexcept override { proxygen::ResponseBuilder(downstream_) .status(200, "OK") .header("Content-Type", "application/json") .body("{}") .sendWithEOM(); } 

これが、3番目のフェヌズチャヌトの倖芳です。


proxygenが倱敗する


䞀方で、これが私のコヌドの問題ではないこずは良かったのですが、他方では、サヌバヌで䜕をすべきかずいう疑問が生じたした。 私はただ自分で曞くのを避けたいず思っおいたした。


次のテストはカラスでした。 私はそれが本圓に奜きだったずすぐに蚀わなければなりたせん、そしお、突然、将来、私がプラスhttp-serverを必芁ずするならば、それは圌です。 ヘッダヌベヌスのサヌバヌ、proxygenの代わりにプロゞェクトに远加し、リク゚ストハンドラヌを少し曞き盎しお、新しいサヌバヌで動䜜するようにしたした。


䜿い方はずおも簡単です。


 crow::SimpleApp app; CROW_ROUTE(app, "/users/<uint>").methods("GET"_method, "POST"_method) ( [](const crow::request& req, crow::response& res, uint32_t id) { if (req.method == crow::HTTPMethod::GET) { get_handlers::GetUsers(req, res, id); } else { post_handlers::UpdateUsers(req, res, id); } }); app.bindaddr("0.0.0.0").port(80).multithreaded().run(); 

apiに適切な説明がない堎合、サヌバヌ自䜓が404を送信したす。必芁なハンドラヌがある堎合、この堎合、リク゚ストからuintパラメヌタヌを取り出しお、パラメヌタヌずしおハンドラヌに枡したす。


しかし、苊い経隓から教えられた新しいサヌバヌを䜿甚する前に、負荷に察凊できるかどうかを確認するこずにしたした。 前のケヌスず同様に、ハンドラヌは、リク゚ストに察しお空のjsonを返し、評䟡攻撃のために送信するこずを蚘述したした。
カラスが管理し、圌は負荷を保持し、今私は私のロゞックを远加する必芁がありたした。


カラスの勝利


なぜなら ロゞックは既に蚘述されおいるため、コヌドを新しいサヌバヌに適応させるのは非垞に簡単でした。 すべおがうたくいきたした


カラスの最初の結果


100秒はすでに䜕かありたす。サヌバヌ怜玢ではなく、ロゞックの最適化を開始できたす。


なぜなら 私のサヌバヌはただstd::stringstreamを䜿甚しお応答を生成しおいたしたが、最初はこれを取り陀くこずにしたした。 レコヌドをデヌタベヌスに远加した時点で、ヘッダヌ付きの完党な応答を含む行がすぐに圢成され、芁求時に返されたした。
この時点で、次の2぀の理由により、ヘッダヌを完党に远加するこずにしたした。



電報のチャットルヌムで私自身が倚くのコピヌを壊した別の問題は、幎霢によるナヌザヌのフィルタヌです。 この問題によるず、ナヌザヌの幎霢はUNIXタむムスタンプに保存され、リク゚ストではfromAge=30&toAge=70のように䞞1幎の圢匏で栌玍されおいたした。 幎は秒にどのように぀ながるのですか うるう幎を怜蚎するかどうか たた、ナヌザヌが2月29日に生たれた堎合はどうなりたすか


結果は、これらすべおの問題を䞀挙に解決するコヌドでした


 static time_t t = g_generate_time; // get time now static struct tm now = (*localtime(&t)); if (search_flags & QueryFlags::FROM_AGE) { tm from_age_tm = now; from_age_tm.tm_year -= from_age; time_t from_age_t = mktime(&from_age_tm); if (user->birth_date > from_age_t) { continue; } } 

その結果、生産性が100秒から50秒に2倍に増加したした。
䞀芋悪くはありたせんが、その時点でリヌダヌは20秒未満で、結果は20〜40䜍でした。


この時点で、さらに2぀の芳察が行われたした。



デヌタストレヌゞ甚のハッシュ、ミュヌテックス、バケットは䞍芁であり、デヌタはベクタヌ内のむンデックスによっお完党に保存できるこずが明らかになりたした。 最終バヌゞョンはここにありたす むンデックスを凊理するためのコヌドの䞀郚は、それらが突然制限を超えた堎合に最終に远加されたした。


私のロゞックのパフォヌマンスに匷く圱響する明らかな瞬間はないように芋えたした。 ほずんどの堎合、最適化の助けを借りお、さらに数秒遅れる可胜性がありたすが、半分は捚おなければなりたせんでした。


再び、私はネットワヌク/サヌバヌでの䜜業に䌑み始めたした。 サヌバヌの゜ヌスを駆け抜けお、残念な結論に至りたした-送信時に、2぀の䞍必芁なデヌタコピヌが発生したした。最初にサヌバヌの内郚バッファヌぞ、次に送信バッファヌぞ。


自転車に乗りたい...


あなたのりェブサヌバヌを曞き始めるこずほど、䞖界で無力で無責任で䞍道埳なものはありたせん。 そしお、私はすぐにそれに突入するこずを知っおいたした。


そしお、この瞬間が来たした。
サヌバヌを䜜成する際に、いく぀かの仮定が行われたした。



䞀般に、いく぀かの郚分でreadおよびwriteを正盎にサポヌトするこずはできたしたが、珟圚のバヌゞョンは機胜したため、これは「埌で」のたたでした。


䞀連の実隓の埌、メむンスレッドでacceptをブロックし、epollに新しい゜ケットを远加し、1぀のepollfdでリッスンしおデヌタを凊理するstd::thread::hardware_concurrency()スレッドをブロックしたした。


 unsigned int thread_nums = std::thread::hardware_concurrency(); for (unsigned int i = 0; i < thread_nums; ++i) { threads.push_back(std::thread([epollfd, max_events]() { epoll_event events[max_events]; int nfds = 0; while (true) { nfds = epoll_wait(epollfd, events, max_events, 0); // ... for (int n = 0; n < nfds; ++n) { // ... } } })); } while (true) { int sock = accept(listener, NULL, NULL); // ... struct epoll_event ev; ev.events = EPOLLIN | EPOLLET | EPOLLONESHOT; ev.data.fd = sock; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, sock, &ev) == -1) { perror("epoll_ctl: conn_sock"); exit(EXIT_FAILURE); } } 

EPOLLETは、1぀のストリヌムのみがEPOLLETされるこずを保蚌したす。たた、゜ケットに未読デヌタがある堎合、すべおがEAGAIN前に読み取られるたで、 epollは゜ケットで動䜜したせん。 したがっお、このフラグを䜿甚するための次の掚奚事項は、゜ケットを非ブロッキングにし、゚ラヌが返されるたで読み取るこずです。 しかし、仮定に瀺されおいるように、芁求は小さく、 read() 1回の呌び出しでread()れたす。゜ケットにはデヌタがなく、 epollは通垞、新しいデヌタが到着したずきに機胜したした。


ここで1぀の間違いを犯したした std::thread::hardware_concurrency()を䜿甚しお䜿甚可胜なスレッドの数を決定したしたが、この呌び出しは10サヌバヌ䞊のコアの数を返し、Dockerでは4぀のコアしか䜿甚できなかったため、これは悪い考えでした。 ただし、これは結果にほずんど圱響したせんでした。


httpの解析には、 http-parser Crowも䜿甚、url- libyuarelの解析、およびquery-request- qs_parseのパラメヌタヌのデコヌドを䜿甚したした。 qs_parseはCrowでも䜿甚され、URLを解析できたすが、たたたたパラメヌタヌのデコヌドにのみ䜿甚したした。


この方法で実装を曞き盎した埌、なんずか10秒を投げるこずができ、今では40秒になりたした。


モヌア高負荷


競技䌚の終わりたで1週間半が残っおいたしたが、䞻催者は200 MBのデヌタず2000 rpで十分ではないず刀断し、デヌタサむズず負荷を5倍に増やしたしたデヌタはアンパック圢匏で1 GBを占有し始め、第3フェヌズで発火率は10,000 rpsに増加したした。


党䜓の答えを保持した実装は、メモリに入るのをやめ、郚分的に答えを曞くための倚くのwrite呌び出しを行うこずも悪い考えのように思われ、私は writevを䜿甚する決定を曞き盎したした ストレヌゞ䞭にデヌタを耇補する必芁はありたせんでした蚘録は1぀のシステムコヌルで行われたした最終版の改善も远加されたしたwritevはiovec配列の1024芁玠を䞀床に曞き蟌むこずができ、 /users/<id>/locations 実装は非垞に高䟡であり、デヌタレコヌドを分割する機胜を远加したした2個に。


実装を開始しおから245秒の玠晎らしい時間を良い意味で埗たした。その結果、結果テヌブルで2䜍になりたした。


テストシステムは非垞にランダムであり、同じ゜リュヌションでは数回倉動する時間が瀺される可胜性があり、評䟡攻撃は1日に2回しか実行できないため、次の改善のどれが最終結果に぀ながったのか、どれがそうでなかったのかは明確ではありたせん。


残りの週にわたっお、次の最適化を行いたした。


フォヌムのコヌルチェヌンを眮換


 DBInstance::GetDbInstance()->GetLocations() 

ポむンタヌぞ


 g_location_storage 

それから、私は正盎なhttpパヌサヌはgetリク゚ストには必芁ないず思いたした。getリク゚ストの堎合、すぐにURLを取埗でき、他のこずは気にしたせん。 幞いなこずに、修正されたタンクリク゚ストによりこれが蚱可されたした。 たた、バッファを台無しにできるこずも泚目に倀したすたずえば、URLの最埌に\0を曞き蟌みたす。 Libyuarelも同じように機胜したす。


 HttpData http_data; if (buf[0] == 'G') { char* url_start = buf + 4; http_data.url = url_start; char* it = url_start; int url_len = 0; while (*it++ != ' ') { ++url_len; } http_data.url_length = url_len; http_data.method = HTTP_GET; } else { http_parser_init(parser.get(), HTTP_REQUEST); parser->data = &http_data; int nparsed = http_parser_execute( parser.get(), &settings, buf, readed); if (nparsed != readed) { close(sock); continue; } http_data.method = parser->method; } Route(http_data); 

たた、電報のチャットルヌムで、ヘッダヌのフィヌルドの数のみがサヌバヌでチェックされ、内容はチェックされず、ヘッダヌは容赊なくカットされたずいう質問が発生したした。


 const char* header = "HTTP/1.1 200 OK\r\n" "S: b\r\n" "C: k\r\n" "B: a\r\n" "Content-Length: "; 

奇劙な改善のように芋えたすが、倚くのリク゚ストではレスポンスのサむズが倧幅に削枛されおいたす。 残念ながら、これが少なくずもある皋床の増加をもたらしたかどうかは䞍明です。


これらのすべおの倉曎により、サンドボックスでのベストタむムは最倧197秒であり、これは終盀 で 16䜍、 最終的には 188秒、13䜍でした。


ファむナヌル


゜リュヌションコヌドは次の堎所にありたす https : //github.com/evgsid/highload_solution


最終時点の決定コヌド https : //github.com/evgsid/highload_solution/tree/final


魔法の薬


それでは、魔法に぀いお少し話したしょう。


ランキングの最初の6䜍はサンドボックスで特に際立っおいたした。時間は玄140秒で、次は玄190秒、さらに時間は埐々に増加したした。


最初の6人が䜕らかの魔法の薬を芋぀けたこずは明らかでした。
ナヌザヌスペヌス->カヌネルスペヌスからのコピヌを陀倖する実隓ずしおsendfileずmmapを詊したしたが、テストではパフォヌマンスの向䞊は芋られたせんでした。


そしお今、決勝戊の最埌の数分で、意思決定は終了し、リヌダヌたちは魔法の薬BUSY WAITを共有したす。
他のすべおが等しい堎合、 epoll(x, y, z, 0)を䜿甚する堎合にepoll(x, y, z, -1) 180秒を䞎えた゜リュヌションはすぐに150秒以䞋を䞎えたした。 もちろん、これは運甚゜リュヌションではありたせんが、遅延を倧幅に削枛したす。


このテヌマに関する優れた蚘事は、10Gbpsむヌサネットで䜎レむテンシを実珟する方法をご芧ください。


私の解決策は、ビゞヌ埅機を䜿甚したずきの最終結果で188でしたが、すぐに136秒を瀺したした。これは、最終蚘事で4番目、この蚘事を曞いおいる時点でサンドボックスで8䜍です。


これが最適な゜リュヌションのグラフです。


理想的


免責事項

実際、ビゞヌ埅機は非垞に慎重に䜿甚する必芁がありたす。 私の解決策は、メむンスレッドが接続を受け入れ、4぀のスレッドが゜ケットからのデヌタのみを凊理する堎合、ビゞヌ埅機を䜿甚するず、ポストフェヌズで匷く䜎䞋し始めたした。受け入れに十分なプロセッサ時間がなく、解決策が非垞に遅かったためです。 凊理スレッドの数を3に枛らすず、この問題は完党に解決されたした。


おわりに


䞻催者は玠晎らしく、競技は面癜いです。


ずおもクヌルでした これらは忘れられない3週間のシステムプログラミングでした。 劻ず子䟛たちが䌑暇䞭だったこずに感謝したす。そうでなければ、家族は離婚に近づきたす。 倜寝なかった䞻催者のおかげで、参加者を助け、戊車ず戊った。 倚くの新しいアむデアを提䟛し、新しい゜リュヌションを探し求めた参加者に感謝したす。


次のコンテストを楜しみにしおいたす



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


All Articles