Hyperscanで耇数の正芏衚珟を芋぀ける

この蚘事では、 ハむパヌスキャンシステムを䜿甚しお倚くの正芏衚珟の実行を最適化した経隓に぀いおお話したいず思いたす。 rspamdスパムフィルタヌを開発する際、spamassassin向けに曞かれた倧量の叀いルヌルを数幎の䜜業で移怍する必芁に盎面したした。 私の最初の決定は、これらのルヌルを読み取り、それらから構文ツリヌを構築するプラグむンを曞くこずでした。 次に、このツリヌでさたざたな最適化を実行しお、党䜓の実行時間を短瞮したしたこれに぀いお簡単に説明したした。



残念ながら、操䜜䞭にpcreが䟝然ずしおボトルネックであるこずが刀明し、倧きな文字ではこのルヌルセットは遅すぎたす。 たずえば、メガバむトサむズの文字で、pcreは玄1ギガバむトのテキストをチェックするこずが刀明したした。 正芏衚珟のテキストの量を制限するなどのさたざたなトリックは、ルヌルに悪圱響を及がし、pcre_jit_execを介したjit高速パスを集䞭的に䜿甚しおpcreを最適化するこずは非垞に危険であるこずが刀明したした-いく぀かの叀い衚珟は、たずえばUTF-8の文字を「傷぀けた」ため、プログラムスタックが砎損する再珟可胜なバグが発生したした。 しかし、高負荷の䌚議で、Vyacheslav Olkhovchenkovず話をし、圌はハむパヌスキャンを芋るようにアドバむスしたした。 次に、芁点を説明し、それが䜕をもたらしたのかを説明したす。





ハむパヌスキャンに぀いお簡単に





Hyperscanはかなり長い歎史を持぀プロゞェクトであり、Deep Packet IntrospectionDPI゜リュヌションを販売するために䜜成されたした。 しかし、昚幎10月にIntelは゜ヌスコヌドをオヌプンするこずを決定し、さらにBSDラむセンスの䞋でさえ決定したした。 このプロゞェクトはC ++で曞かれおおり、ブヌストをかなり集䞭的に䜿甚しおいたす。これにより、移怍時に特定の問題が発生したす埌で詳しく説明したす。 内郚では、ハむパヌスキャンは非決定性有限状態マシンNFAに基づく非バックトラッキング正芏衚珟゚ンゞンです。 原則ずしお、すべおの最新のパフォヌマンス指向の正芏衚珟゚ンゞンは同じ理論を䜿甚しお蚘述され、バックトラッキングを完党に攟棄したすもちろん、線圢怜玢速床を確保したい堎合は非垞に合理的です。 私にずっお最も重芁なハむパヌスキャン機胜は、いく぀かのテキストで倚くの正芏衚珟を同時に実行できるこずでした。 pcreで同じこずを行うための単玔なアプロヌチは、耇数の匏を|ず組み合わせお詊すこずです。 䞀皮の「゜ヌセヌゞ」で
  re1|re2| ... |reN 
。 残念ながら、このアプロヌチは機胜したせん。タスクは、以前に機胜した匏だけでなく、指定された匏のセットからすべおのオカレンスを芋぀けるこずであるためです。 Hyperscanはそのようには機胜したせん。怜出された各匏に察しお、テキスト内の䜍眮ただし、゚ントリの最埌の文字のみでコヌルバック関数を呌び出したす。これは次の図に瀺すこずができたす。







ハむパヌスキャンアヌキテクチャ





Hyperscanは、コンパむラず実際にぱンゞンの2぀の郚分で構成されおいたす。 コンパむラヌは、ラむブラリヌの非垞に倧きな郚分であり、匏の束を取り、そのためのNFAを構成し、たずえばAVX2などのベクトル呜什を䜿甚しお、このNFAをアセンブラヌコヌドに倉換できたす。 このタスクは難しく、リ゜ヌスを消費するため、コンパむラヌは結果コヌドをシリアル化しお埌で䜿甚するこずもできたす。 怜玢゚ンゞンは小さなラむブラリであり、実際、特定のテキストでNFAを実行するように蚭蚈されおいたす。 プリコンパむルされた匏セットのみを䜿甚するアプリケヌションの堎合、別個のlibhs_runtimeラむブラリを䜿甚できたす。これは、コンパむラ+゚ンゞンlibhsを組み合わせたラむブラリよりもはるかに小さくなりたす。 たず、ハむパヌスキャンを構築しようずしたした。これは、システムにかなり新しいブヌストがあれば、非垞に簡単なタスクであるこずがわかりたした最小バヌゞョン1.57。 おそらく唯䞀の発蚀は、デバッグシンボルを䜿甚しおビルドする堎合、ラむブラリは巚倧であるずいうこずです-私のMacbookでは玄200MBです。 たた、デフォルトでは静的ラむブラリのみが収集されるため、接続時に、バむナリのサむズも200Mbの領域で取埗されたす。 デバッグシンボルが必芁ない堎合は、指定せずにハむパヌスキャンを収集するこずをお勧めしたす
  -DCMAKE_BUILD_TYPE = MinSizeRel 
蚭定段階でcmakeを䜿甚したす。



テストコヌド



それから、ハむパヌスキャンずpcreを比范しお、ネタバレの䞋で芋るこずができる非垞に粗雑なプロトタむプを曞いおみたした私はあなたに譊告したす-これはプロトタむプコヌドで、品質を䞻匵せずに急いで曞かれたした。



Pcreずハむパヌスキャンの比范コヌド
#include <iostream> #include <string> #include <fstream> #include <vector> #include <stdexcept> #include <algorithm> #include <set> #include "pcre.h" #include "hs.h" #include <time.h> #ifdef __APPLE__ #include <mach/mach_time.h> #endif using namespace std; double get_ticks(void) { double res; #if defined(__APPLE__) res = mach_absolute_time() / 1000000000.; #else struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); res = (double)ts.tv_sec + ts.tv_nsec / 1000000000.; #endif return res; } struct pcre_regexp { pcre* re; pcre_extra* extra; pcre_regexp(const string& pattern) { const char* err; int err_off; re = pcre_compile(pattern.c_str(), PCRE_NEWLINE_ANYCRLF, &err, &err_off, NULL); if (re == NULL) { throw invalid_argument(string("cannot compile: '") + pattern + "' error: " + err + " at offset: " + to_string(err_off)); } extra = pcre_study(re, PCRE_STUDY_JIT_COMPILE, &err); if (extra == NULL) { throw invalid_argument(string("cannot study: '") + pattern + "' error: " + err + " at offset: " + to_string(err_off)); } } }; struct cb_context { set<int> approx_re; vector<pcre_regexp> pcre_vec; }; struct cb_data { struct cb_context* ctx; vector<int> matched; const std::string* str; }; bool remove_uncompileable(const string& s, int id, struct cb_context* ctx) { hs_compile_error_t* hs_errors; hs_database_t* hs_db; if (hs_compile(s.c_str(), HS_FLAG_ALLOWEMPTY, HS_MODE_BLOCK, NULL, &hs_db, &hs_errors) != HS_SUCCESS) { cout << "pattern: '" << s << "', error: " << hs_errors->message << endl; if (hs_compile(s.c_str(), HS_FLAG_ALLOWEMPTY | HS_FLAG_PREFILTER, HS_MODE_BLOCK, NULL, &hs_db, &hs_errors) != HS_SUCCESS) { cout << "completely bad pattern: '" << s << "', error: " << hs_errors->message << endl; return true; } else { ctx->approx_re.insert(id); } } else { hs_free_database(hs_db); } return false; } int match_cb(unsigned int id, unsigned long long from, unsigned long long to, unsigned int flags, void* context) { auto cbdata = (struct cb_data*)context; auto& matched = cbdata->matched; if (cbdata->ctx->approx_re.find(id) != cbdata->ctx->approx_re.end()) { int ovec[3]; auto re = cbdata->ctx->pcre_vec[id]; auto* begin = cbdata->str->data(); auto* p = begin; auto sz = cbdata->str->size(); while (pcre_exec(re.re, re.extra, p, sz - (p - begin), 0, 0, ovec, 3) > 0) { p = p + ovec[1]; matched[id]++; } } else { matched[id]++; } return 0; } int main(int argc, char** argv) { ifstream refile(argv[1]); vector<string> re_vec; double t1, t2, total_ticks = 0; struct cb_context ctx; int ls; pcre_config(PCRE_CONFIG_LINK_SIZE, &ls); cout << ls << endl; for (std::string line; std::getline(refile, line);) { re_vec.push_back(line); } string re_pipe; const char** pats = new const char*[re_vec.size()]; unsigned int i = 0, *ids = new unsigned int[re_vec.size()]; //re_vec.erase(remove_if(re_vec.begin(), re_vec.end(), remove_uncompileable), re_vec.end()); for (i = 0; i < re_vec.size(); i++) { const auto& r = re_vec[i]; remove_uncompileable(r, i, &ctx); pats[i] = r.c_str(); ids[i] = i; re_pipe = re_pipe + string("(") + r + string(")|"); } // Last | re_pipe.erase(re_pipe.size() - 1); total_ticks = 0; for (const auto& r : re_vec) { t1 = get_ticks(); ctx.pcre_vec.emplace_back(r); t2 = get_ticks(); total_ticks += t2 - t1; } cout << "PCRE compile time: " << total_ticks << endl; ifstream input(argv[2]); std::string in_str((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>()); hs_compile_error_t* hs_errors; hs_database_t* hs_db; hs_platform_info_t plt; hs_populate_platform(&plt); unsigned int* flags = new unsigned int[re_vec.size()]; for (i = 0; i < re_vec.size(); i++) { if (ctx.approx_re.find(i) != ctx.approx_re.end()) { flags[i] = HS_FLAG_PREFILTER; } else { flags[i] = 0; } } t1 = get_ticks(); if (hs_compile_multi(pats, flags, ids, re_vec.size(), HS_MODE_BLOCK, &plt, &hs_db, &hs_errors) != HS_SUCCESS) { cout << "BAD pattern: '" << re_vec[hs_errors->expression] << "', error: " << hs_errors->message << endl; return -101; } t2 = get_ticks(); cout << "Hyperscan compile time: " << (t2 - t1) << "; approx re: " << ctx.approx_re.size() << "; total re: " << re_vec.size() << endl; char* bytes = NULL; size_t bytes_len; t1 = get_ticks(); if (hs_serialize_database(hs_db, &bytes, &bytes_len) != HS_SUCCESS) { cout << "BAD" << endl; return -101; } t2 = get_ticks(); cout << "Hyperscan serialize time: " << (t2 - t1) << "; size: " << bytes_len << " bytes" << endl; hs_database_t* hs_db1 = NULL; t1 = get_ticks(); if (hs_deserialize_database(bytes, bytes_len, &hs_db1) != HS_SUCCESS) { cout << "BAD1" << endl; return -101; } t2 = get_ticks(); cout << "Hyperscan deserialize time: " << (t2 - t1) << "; size: " << bytes_len << " bytes" << endl; auto matches = 0; total_ticks = 0; for (const auto& re : ctx.pcre_vec) { int ovec[3]; auto* begin = in_str.data(); auto* p = begin; auto sz = in_str.size(); t1 = get_ticks(); while (pcre_exec(re.re, re.extra, p, sz - (p - begin), 0, 0, ovec, 3) > 0) { p = p + ovec[1]; matches++; } t2 = get_ticks(); total_ticks += t2 - t1; } //cout << re_pipe << endl; cout << "Time for individual re: " << total_ticks << "; matches: " << matches << endl; //cout << "Time for piped re: " << (t2 - t1) << endl; hs_scratch_t* scratch = NULL; int rc; if ((rc = hs_alloc_scratch(hs_db1, &scratch)) != HS_SUCCESS) { cout << "bad scratch: " << rc << endl; return -102; } struct cb_data cbdata; cbdata.ctx = &ctx; cbdata.matched = vector<int>(re_vec.size(), 0); cbdata.str = &in_str; t1 = get_ticks(); if ((rc = hs_scan(hs_db1, in_str.data(), in_str.size(), 0, scratch, match_cb, &cbdata)) != HS_SUCCESS) { cout << "bad scan: " << rc << endl; return -103; } t2 = get_ticks(); matches = 0; for_each(cbdata.matched.begin(), cbdata.matched.end(), [&matches](int elt) { matches += elt; }); cout << "Time for hyperscan re: " << (t2 - t1) << "; matches: " << matches << endl; return 0; }
      
      









結果は非垞に印象的でした。1メガバむトのスパムメヌルず1000個たでの正芏衚珟のセットで、次の結果が埗られたした。



 PCREコンパむル時間0.0138553
ハむパヌスキャンのコンパむル時間4.94309; 箄re191; 合蚈日時971
ハむパヌスキャンのシリアル化時間0.00312218; サむズ5242956バむト
ハむパヌスキャンのデシリアラむズ時間0.00359501; サむズ5242956バむト
個々の日時0.440707; マッチ7
ハむパヌスキャンの時間0.0770988; マッチ7




プレフィルタヌ





ハむパヌスキャンの最も印象的な機胜の1぀は、プレフィルタヌずしお機胜するこずです。 このモヌドは、匏にサポヌトされおいないコンストラクトがある堎合、たずえば同じバックトラッキングがある堎合に䟿利です。 このモヌドでは、ハむパヌスキャンは、指定されたサポヌトされおいない匏の保蚌されたスヌパヌセットである匏を䜜成したす。 ぀たり、新しい匏は最初の操䜜のすべおのケヌスで機胜するこずが保蚌されおいたすが、他のケヌスでも機胜し、誀怜知操䜜が発生する可胜性がありたす。 そのため、pcreなどの埓来の正芏衚珟゚ンゞンで結果を確認する必芁がありたすただし、この堎合はテキスト党䜓を実行する必芁はありたせんが、事前衚珟の最初から発生箇所たで確認する必芁がありたす。 これは、次の図に明確に瀺されおいたす。







コンパむルの問題





残念ながら、2぀の䞍快な瞬間が明らかになりたした。 これらの最初はコンパむル時間です— pcreず比范しお非珟実的に長い時間がかかりたす。 2番目のポむントは、䞀郚の匏がコンパむル䞭に単玔にプリフィルタヌにコンパむルされるずいう事実に関連しおいたす。 最も単玔な「悪質な」衚珟は、たずえば次のずおりです。



 <a \ s [^>] {0,2048} \ bhref =3D。https ?: [^> "'\] {8,29} [^>"' \\ /=][^>] {0,2048}>[^ <] {0,1024} <?! \ / A[^>] {1,1024}>{0、 99} \ s {0.10}\ 1https[^ \ W <] {1,3} [^ <] {5}




結果ずしお、匏をコンパむルする前に、すべおの事前フィルタヌ匏が最初にプロセスの別のフォヌクでチェックされたした。 匏のコンパむルが長すぎる堎合、このプロセスは砎られ、匏は絶望的ずマヌクされたす。぀たり、垞にpcreが䜿甚されたす。 これらの衚珟の玄4000が芋぀かりたした。 それらはすべお私の倧奜きなspamassassin'aから来おおり、「脳のperl」ず呌ばれる病気の非垞に特城的な産物です。 Intel゚ンゞニアずのある皋床のコミュニケヌションの埌、圌らは無限のコンパむルを修正したしたが、䞊蚘の正芏衚珟はただ1分皋床でコンパむルされたすが、実際の目的には受け入れられたせん。



ハむパヌスキャンが機胜するためには、匏のセットをいわゆる「クラス」に分解する必芁があるこずも刀明したした。これは、セットからの正芏衚珟を介しおチェックされる入力テキストのタむプです。 そのようなクラスを次の図に瀺したす。







コンパむルには、次のアプロヌチを䜿甚したした。





このアプロヌチにより、高䟡で長いハむパヌスキャンコンパむルを埅぀のではなく、pcreを䜿甚しお開始盎埌にメヌルのチェックを開始し、コンパむルが完了したらすぐにpcreからハむパヌスキャンに切り替えたすキャッシュデスクを離れずに呌び出されたす。 たた、チェックおよびトリガヌされた正芏衚珟にビットセットを䜿甚するこずにより、スキャンで既に実行されおいる文字の凊理を切り替えるずきに邪魔されないようにするこずもできたす。



結論





rspamd + hyperscanの過皋で、次のようになりたした。



それは

 len610591、time2492.457ms real、882.251ms virtual
正芏衚珟の統蚈4095 pcre正芏衚珟がスキャンされ、18個の正芏衚珟が䞀臎し、pcreを䜿甚しお694Mバむトがスキャンされたした




次のようになりたした

 len610591、time654.596msリアルタむム、309.785msバヌチャル
正芏衚珟の統蚈34 pcre正芏衚珟がスキャンされ、41個の正芏衚珟が䞀臎、8.41Mバむトがpcreを䜿甚しおスキャンされ、 
スキャンされた合蚈9.56Mバむト




ハむパヌスキャンバヌゞョンで䞀臎する正芏衚珟の数が倚いのは、pcreに察しお実行される構文ツリヌ最適化が原因ですが、ハむパヌスキャンの堎合は圹に立ちたせんすべおの匏が同時にチェックされるため。



Hyperscanバヌゞョンはすでに生産されおおり、rspamdの新しいバヌゞョンに含たれおいたす。 正芏衚珟DPI、プロキシなどをチェックするためのパフォヌマンスクリティカルなプロゞェクト、およびテキスト内の静的な行を芋぀けるためのアプリケヌションに぀いお、ハむパヌスキャンを自信を持っおアドバむスできたす。



最埌のタスクでは、ハむパヌスキャンず、埓来そのような目的で䜿甚されおいたaho-corasickアルゎリズムを比范したした。 Mischa Sandbergで知っおいる最速の実装を比范したした。



そのような行が倚数あるメガバむト文字の1䞇行の静的行の比范結果぀たり、耇雑床OM + Nのaho-corrasicの最悪の条件、ここでMは芋぀かった行の数



動䜜コンパむル時間0.0743811
ハむパヌスキャンのコンパむル時間0.1547; 箄re0; 合蚈日時7400
ハむパヌスキャンのシリアル化時間0.000178297; サむズ1250856バむト
ハむパヌスキャンデシリアラむズ時間0.000312379; サむズ1250856バむト
ハむパヌスキャンの再時間0.117938; 䞀臎3001024
行動の時間0.100427; マッチ3000144




残念ながら、ac-trieコヌドの゚ラヌが原因でヒット数が収束しなかったのに察し、ハむパヌスキャンはミスを犯しおいたせんでした。



たた、この蚘事の資料はプレれンテヌションで入手できたす。 コヌドは、githubの rspamdプロゞェクトで芋るこずができたす



All Articles