نام کامل مقاله 110 میلیون کامنت از Hacker News :تست medium data full-text / analytics بود که به علت محدودیت کاراکتر در عنوان اصلی، به نام فوق تغییر کرده است.
در این آزمایش ما از یک مجموعه داده 1.1 میلیونی از کامنتهای Curate شده Hacker News با فیلدهای عددی ضرب شده در 100 از https://zenodo.org/record/45901 استفاده میکنیم. در دنیای مدرن، 110 میلیون document را میتوان یک مجموعه داده با اندازه متوسط در نظر گرفت. شما میتوانید مجموعه دادههایی با اندازه مشابه را در وبلاگها و سایتهای خبری بزرگ، فروشگاههای آنلاین بزرگ، سایتهای پخشکننده آگهی و غیره پیدا کنید. برای چنین اپلکیشنهایی رایج است که موارد زیر را داشته باشید:
- دادههای متنی نه چندان طولانی در یک یا چند فیلد
- تعدادی از attribute ها
- Data collection
منبع گردآوری دادهها https://zenodo.org/record/45901 است.
ساختار رکورد عبارت است از:
"properties": {
"story_id": {"type": "integer"},
"story_text": {"type": "text"},
"story_author": {"type": "text", "fields": {"raw": {"type":"keyword"}}},
"comment_id": {"type": "integer"},
"comment_text": {"type": "text"},
"comment_author": {"type": "text", "fields": {"raw": {"type":"keyword"}}},
"comment_ranking": {"type": "integer"},
"author_comment_count": {"type": "integer"},
"story_comment_count": {"type": "integer"}
}
# دیتابیسها
ما تا کنون این تست را برای 3 دیتابیس در دسترس قرار دادهایم:
Clickhouse – دیتابیس قدرتمند OLAP
Elasticsearch – موتور جستجو و تجزیه و تحلیل با اهداف عمومی
Manticore Search – دیتابیسی برای جستجو (آلترناتیو Elasticsearch )
در این تست ما تا حد امکان تغییرات کمی در تنظیمات پیشفرض دیتابیس ایجاد میکنیم تا به هیچ یک از آنها مزیت ناعادلانهای تعلق نگیرد. تست در tuning حداکثری از اهمیت کمتری برخوردار نیست، اما این موضوعی برای یک معیار دیگر است. در اینجا میخواهیم بفهمیم که یک کاربر معمولی بیتجربه پس از نصب یک دیتابیس و اجرای آن با تنظیمات پیشفرض، میتواند تا چه مقدار latency داشته باشد. اما برای منصفانه بودن این مقایسه، مجبور شدیم چند چیز را در تنظیمات تغییر دهیم:
- Clickhouse – بدون tuning ، تنها با CREATE TABLE … ENGINE = MergeTree() ORDER BY id and standard clickhouse-server docker image.
- Elasticsearch – همانطور که در آزمایش دیگر دیدیم، sharding میتواند کمک قابل توجهی به Elasticsearch بکند. بنابراین با توجه به وجود بیش از 100 میلیون سند، و اینکه این مجموعه داده کوچکی نیست، تصمیم گرفتیم که کمی منصفانهتر باشد:
- اجازه دهید که Elasticsearch به اندازه 32 shard بسازد: (“number_of_shards”: 32) در غیر این صورت نمیتواند از CPU ای که 32 هسته بر روی سرور دارد استفاده کنید. همانطور که در راهنمای رسمی Elasticsearch گفته شده است، «هر shard جستجو را بر روی یک CPU thread انجام میدهد.
- ما همچنین آن را به صورت memory_lock=true تنظیم کردیم، زیرا همانطور که در https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#_disable_swapping گفته شده است، این کار برای عملکرد کلی میبایست انجام شود.
- docker image استاندارد است.
- Manticore Search نیز در یک فرم از docker image رسمی آنها + columnar library که آنها ارائه میدهند استفاده میشود. آپدیتهای زیر برای پیشفرضهای آنها انجام شده است:
- min_infix_len = 2 زیرا در Elasticsearch میتوانید به صورت پیشفرض یک جستجوی infix full-text انجام دهید و منصفانه نیست که Manticore در حالت سبکتر (w/o infixes) ران شود. متأسفانه این کار در Clickhouse به هیچ وجه امکانپذیر نیست. بنابراین این نقص وجود دارد.
- secondary_indexes = 1 که index های ثانویه را در حین فیلترینگ فعال میکند. (هنگامی که دادههای ساخته شده بارگیری میشوند.) از آنجایی که Elasticsearch به طور پیشفرض از index های ثانویه استفاده میکند و فعالکردن آن در Manticore نسبتاً آسان است، انجام دادن آن منطقی است. متأسفانه در Clickhouse کاربر برای انجام همین کار، باید تلاش زیادی انجام دهد. از این رو این کار انجام نمیشود، چرا که پس از آن یک tuning سنگین در نظر گرفته میشود که این امر مستلزم tuning اکثریت دیتابیسهای دیگر در آینده است. که همه چیز را بسیار پیچیده و ناعادلانه میکند.
- ما Manticore را در دو حالت تست کردیم:
- فضای ذخیرهسازی row-wise که یک فضای ذخیرهسازی پیشفرض است، بنابراین ارزش تست کردن را دارد.
- columnar storage : مجموعه دادهها اندازه متوسطی دارد، بنابراین اگر Elasticsearch و Clickhouse به صورت داخلی از ساختارهای column-oriented استفاده کنند، مقایسه آنها با ذخیرهسازی columnar متعلق به Manticore نیز منصفانه به نظر میرسد.
# درباره کَشها
ما همچنین دیتابیسها را طوری پیکربندی کردهایم که از هیچ کش داخلی استفاده نکنند. اما چرا؟
- در این معیار، ما یک اندازهگیری دقیق از latency انجام میدهیم تا بفهمیم کاربران در صورتی که یکی از query های آزمایششده را در یک لحظه رندوم اجرا کنند، و در صورتی که این اجرا به صورت ران کردن همان query به تعداد زیاد و پشت سر هم نباشد، چه زمانی را برای دریافت پاسخ میتوانند توقع داشته باشند.
- هر کش میانبری برای latency پایین است. همانطور که در ویکیپدیا نوشته شده است، «کش دادهها را ذخیره میکند تا درخواستهای آتی برای آن دادهها سریعتر پاسخ داده شود.» اما کشها متفاوت هستند. آنها را میتوان به دو گروه اصلی تقسیم کرد:
- آنهایی که فقط دادههای خام ذخیره شده روی دیسک را کش میکنند. به عنوان مثال، بسیاری از دیتابیسها برای نوشتن دادهای ذخیرهشده ازروی دیسک به مموری، از mmap() استفاده میکنند. به راحتی به آن دسترسی پیدا میکنند و به سیستم عامل اجازه میدهند بقیه موارد را بخواند. (خواندن آن از روی دیسک در زمانی که در مموری حافظه آزاد وجود دارد، حذف آن از از مموری زمانی که نیاز به فضا برای چیز مهمتری وجود دارد. و غیره). این برای تست عملکرد خوب است، زیرا ما به هر دیتابیس اجازه میدهیم از مزایای استفاده از کش پیج سیستم عامل (یا کش داخلی مشابه آن که فقط دادهها را از روی دیسک میخواند) استفاده کند. این دقیقا همان کاری است که در این معیار انجام میدهیم.
- مواردی که برای ذخیره نتایج محاسبات قبلی استفاده میشوند، در بسیاری از موارد خوباند. اما از نظر این معیار، اجازه دادن به دیتابیس برای فعال کردن چنین کشی، ایده بدی است. چرا که:
- اندازهگیری صحیح را خراب میکند: به جای اندازهگیری زمان محاسبه، شروع به اندازهگیری این میکنید که چقدر طول میکشد تا یک value را با یک کلید در مموری پیدا کنید. این چیزی نیست که بخواهیم در این تست انجام دهیم. (اما به طور کلی جالب است و شاید در آینده این کار را انجام دهیم و مقاله «معیار کشها» را منتشر کنیم).
- حتی اگر آنها نه یک نتیجه کامل از یک query خاص، بلکه نتایج محاسبات فرعی آن را ذخیره کنند، این خوب نیست. زیرا این کار، ایده اصلی آزمایش را زیر پا میگذارد. – «کاربران چه زمانی را میتوانند برای پاسخ توقع داشته باشند، اگر آنها یکی از query های تستشده را در یک لحظه رندوم ران کنند.»
- برخی دیتابیسها دارای چنین کشی هستند. (معمولاً به آن “query cache” میگویند). برخی دیگر هم ندارند. بنابراین اگر کشهای داخلی دیتابیس را غیرفعال نکنیم، مزیت غیر منصفانهای به کسانی که آن را دارند میدهیم.
بنابراین ما هر کاری میکنیم تا مطمئن شویم هیچ یک از دیتابیسها، این نوع کش کردن را انجام نمیدهند.
برای رسیدن به آن دقیقاً چه کاری انجام میدهیم:
- Clickhouse :
- SYSTEM DROP MARK CACHE ، SYSTEM DROP UNCOMPRESSED CACHE ، SYSTEM DROP COMPILED EXPRESSION CACHE پیش از آزمایش هر query جدید (نه هر تلاش برای همان query )
- Elasticsearch :
- “queries.cache.enabled” : false در این پیکربندی.
- /_cache/clear?request=true&query=true&fielddata=true پیش از آزمایش هر query جدید (نه هر تلاش برای همان query )
- Manticore Search (در فایل پیکربندی) :
- qcache_max_bytes = 0
- docstore_cache_size = 0
- Operating System :
- ما echo 3 > /proc/sys/vm/drop_caches; sync را پیش از هر query جدید (نه هر تلاش برای همان query ) انجام میدهیم. یعنی برای هر درخواست جدید، موارد زیر را انجام میدهیم:
- استاپ کردن دیتابیس
- Drop کردن کش سیستم عامل
- شروع دوباره
- اولین cold query را انجام دهید و زمان آن را اندازه بگیرید.
- و دهها تلاش دیگر انجام دهید. (تا 100 بار یا تا زمانی که ضریب تغییرات به اندازه کافی پایین باشد. به این دلیل که نتایج تست با کیفیت بالایی در نظر گرفته شود).
- ما echo 3 > /proc/sys/vm/drop_caches; sync را پیش از هر query جدید (نه هر تلاش برای همان query ) انجام میدهیم. یعنی برای هر درخواست جدید، موارد زیر را انجام میدهیم:
# Query ها
مجموعه query شامل query های full-text و analytical (فیلترینگ، سورتینگ، گروهبندی، تجمیع) است:
[
"select count(*) from hn",
"select count(*) from hn where comment_ranking=100",
"select count(*) from hn where comment_ranking=500",
"select count(*) from hn where comment_ranking > 300 and comment_ranking < 500",
"select story_author, count(*) from hn group by story_author order by count(*) desc limit 20",
"select story_author, avg(comment_ranking) avg from hn group by story_author order by avg desc limit 20",
"select comment_ranking, count(*) from hn group by comment_ranking order by count(*) desc limit 20",
"select comment_ranking, avg(author_comment_count) avg from hn group by comment_ranking order by avg desc, comment_ranking desc limit 20",
"select comment_ranking, avg(author_comment_count+story_comment_count) avg from hn group by comment_ranking order by avg desc, comment_ranking desc limit 20",
"select comment_ranking, avg(author_comment_count+story_comment_count) avg from hn where comment_ranking < 10 group by comment_ranking order by avg desc, comment_ranking desc limit 20",
{
"manticoresearch": "select comment_ranking, avg(author_comment_count) avg from hn where match('google') group by comment_ranking order by avg desc, comment_ranking desc limit 20",
"clickhouse": "select comment_ranking, avg(author_comment_count) avg from hn where (match(story_text, '(?i)\\Wgoogle\\W') or match(story_author,'(?i)\\Wgoogle\\W') or match(comment_text, '(?i)\\Wgoogle\\W') or match(comment_author, '(?i)\\Wgoogle\\W')) group by comment_ranking order by avg desc, comment_ranking desc limit 20",
"elasticsearch": "select comment_ranking, avg(author_comment_count) avg from hn where query('google') group by comment_ranking order by avg desc, comment_ranking desc limit 20",
"mysql": "select comment_ranking, avg(author_comment_count) avg from hn where match(story_text,story_author,comment_text,comment_author) against ('google') group by comment_ranking order by avg desc, comment_ranking desc limit 20"
},
{
"manticoresearch": "select comment_ranking, avg(author_comment_count) avg from hn where match('google') and comment_ranking > 200 group by comment_ranking order by avg desc, comment_ranking desc limit 20",
"clickhouse":"select comment_ranking, avg(author_comment_count) avg from hn where (match(story_text, '(?i)\\Wgoogle\\W') or match(story_author,'(?i)\\Wgoogle\\W') or match(comment_text, '(?i)\\Wgoogle\\W') or match(comment_author, '(?i)\\Wgoogle\\W')) and comment_ranking > 200 group by comment_ranking order by avg desc, comment_ranking desc limit 20",
"elasticsearch":"select comment_ranking, avg(author_comment_count) avg from hn where query('google') and comment_ranking > 200 group by comment_ranking order by avg desc, comment_ranking desc limit 20",
"mysql":"select comment_ranking, avg(author_comment_count) avg from hn where match(story_text,story_author,comment_text,comment_author) against ('google') and comment_ranking > 200 group by comment_ranking order by avg desc, comment_ranking desc limit 20"
},
{
"manticoresearch": "select comment_ranking, avg(author_comment_count+story_comment_count) avg from hn where match('google') and comment_ranking > 200 group by comment_ranking order by avg desc, comment_ranking desc limit 20",
"clickhouse": "select comment_ranking, avg(author_comment_count+story_comment_count) avg from hn where (match(story_text, '(?i)\\Wgoogle\\W') or match(story_author,'(?i)\\Wgoogle\\W') or match(comment_text, '(?i)\\Wgoogle\\W') or match(comment_author, '(?i)\\Wgoogle\\W')) and comment_ranking > 200 group by comment_ranking order by avg desc, comment_ranking desc limit 20",
"elasticsearch": "select comment_ranking, avg(author_comment_count+story_comment_count) avg from hn where query('google') and comment_ranking > 200 group by comment_ranking order by avg desc, comment_ranking desc limit 20",
"mysql": "select comment_ranking, avg(author_comment_count+story_comment_count) avg from hn where match(story_text,story_author,comment_text,comment_author) against ('google') and comment_ranking > 200 group by comment_ranking order by avg desc, comment_ranking desc limit 20"
},
{
"manticoresearch": "select * from hn where match('abc') limit 20",
"clickhouse": "select * from hn where (match(story_text, '(?i)\\Wabc\\W') or match(story_author,'(?i)\\Wabc\\W') or match(comment_text, '(?i)\\Wabc\\W') or match(comment_author, '(?i)\\Wabc\\W')) limit 20",
"elasticsearch": "select * from hn where query('abc') limit 20",
"mysql": "select * from hn where match(story_text,story_author,comment_text,comment_author) against ('google') limit 20"
},
{
"manticoresearch": "select * from hn where match('abc -google') limit 20",
"clickhouse": "select * from hn where (match(story_text, '(?i)\\Wabc\\W') or match(story_author,'(?i)\\Wabc\\W') or match(comment_text, '(?i)\\Wabc\\W') or match(comment_author, '(?i)\\Wabc\\W')) and not (match(story_text, '(?i)\\Wgoogle\\W') or match(story_author,'(?i)\\Wgoogle\\W') or match(comment_text, '(?i)\\Wgoogle\\W') or match(comment_author, '(?i)\\Wgoogle\\W')) limit 20",
"elasticsearch": "select * from hn where query('abc !google') limit 20",
"mysql": "select * from hn where match(story_text,story_author,comment_text,comment_author) against ('abc -google') limit 20"
},
{
"manticoresearch": "select * from hn where match('\"elon musk\"') limit 20",
"clickhouse": "select * from hn where (match(story_text, '(?i)\\Welon\\Wmusk\\W') or match(story_author,'(?i)\\Welon\\Wmusk\\W') or match(comment_text, '(?i)\\Welon\\Wmusk\\W') or match(comment_author, '(?i)\\Welon\\Wmusk\\W')) limit 20",
"elasticsearch": "select * from hn where query('\\\"elon musk\\\"') limit 20",
"mysql": "select * from hn where match(story_text,story_author,comment_text,comment_author) against ('\"elon musk\"') limit 20"
},
{
"manticoresearch": "select * from hn where match('abc') order by comment_ranking asc limit 20",
"clickhouse": "select * from hn where (match(story_text, '(?i)\\Wabc\\W') or match(story_author,'(?i)\\Wabc\\W') or match(comment_text, '(?i)\\Wabc\\W') or match(comment_author, '(?i)\\Wabc\\W')) order by comment_ranking asc limit 20",
"elasticsearch": "select * from hn where query('abc') order by comment_ranking asc limit 20",
"mysql": "select * from hn where match(story_text,story_author,comment_text,comment_author) against ('abc') order by comment_ranking asc limit 20"
},
{
"manticoresearch": "select * from hn where match('abc') order by comment_ranking asc, story_id desc limit 20",
"clickhouse": "select * from hn where (match(story_text, '(?i)\\Wabc\\W') or match(story_author,'(?i)\\Wabc\\W') or match(comment_text, '(?i)\\Wabc\\W') or match(comment_author, '(?i)\\Wabc\\W')) order by comment_ranking asc, story_id desc limit 20",
"elasticsearch": "select * from hn where query('abc') order by comment_ranking asc, story_id desc limit 20",
"mysql": "select * from hn where match(story_text,story_author,comment_text,comment_author) against ('abc') order by comment_ranking asc, story_id desc limit 20"
},
{
"manticoresearch": "select count(*) from hn where match('google') and comment_ranking > 200",
"clickhouse": "select count(*) from hn where (match(story_text, '(?i)\\Wgoogle\\W') or match(story_author,'(?i)\\Wgoogle\\W') or match(comment_text, '(?i)\\Wgoogle\\W') or match(comment_author, '(?i)\\Wgoogle\\W')) and comment_ranking > 200",
"elasticsearch": "select count(*) from hn where query('google') and comment_ranking > 200",
"mysql": "select count(*) from hn where match(story_text,story_author,comment_text,comment_author) against ('google') and comment_ranking > 200"
},
{
"manticoresearch": "select story_id from hn where match('me') order by comment_ranking asc limit 20",
"clickhouse": "select story_id from hn where (match(story_text, '(?i)\\Wme\\W') or match(story_author,'(?i)\\Wme\\W') or match(comment_text, '(?i)\\Wme\\W') or match(comment_author, '(?i)\\Wme\\W')) order by comment_ranking asc limit 20",
"elasticsearch": "select story_id from hn where query('me') order by comment_ranking asc limit 20",
"mysql": "select story_id from hn where match(story_text,story_author,comment_text,comment_author) against ('me') order by comment_ranking asc limit 20"
},
{
"manticoresearch": "select story_id, comment_id, comment_ranking, author_comment_count, story_comment_count, story_author, comment_author from hn where match('abc') limit 20",
"clickhouse": "select story_id, comment_id, comment_ranking, author_comment_count, story_comment_count, story_author, comment_author from hn where (match(story_text, '(?i)\\Wabc\\W') or match(story_author,'(?i)\\Wabc\\W') or match(comment_text, '(?i)\\Wabc\\W') or match(comment_author, '(?i)\\Wabc\\W')) limit 20",
"elasticsearch": "select story_id, comment_id, comment_ranking, author_comment_count, story_comment_count, story_author, comment_author from hn where query('abc') limit 20",
"mysql": "select story_id, comment_id, comment_ranking, author_comment_count, story_comment_count, story_author, comment_author from hn where match(story_text,story_author,comment_text,comment_author) against ('abc') limit 20"
},
"select * from hn order by comment_ranking asc limit 20",
"select * from hn order by comment_ranking desc limit 20",
"select * from hn order by comment_ranking asc, story_id asc limit 20",
"select comment_ranking from hn order by comment_ranking asc limit 20",
"select comment_ranking, story_text from hn order by comment_ranking asc limit 20",
"select count(*) from hn where comment_ranking in (100,200)",
"select story_id from hn order by comment_ranking asc, author_comment_count asc, story_comment_count asc, comment_id asc limit 20"
]
# نتایج
با انتخاب “Test: hn” میتوانید تمام نتایج را در صفحه نتایج پیدا کنید.
به یاد داشته باشید که تنها معیار با کیفیت بالا، “Fast avg” است. زیرا ضریب تغییرات پایین و تعداد جستجوهای بالا را برای هر query تضمین میکند. 2 مورد دیگر (“Fastest” و “Slowest” ) بدون هیچ ضمانتی ارائه میشوند، زیرا:
- Slowest – نتیجه حاصل از یک تلاش است. در اکثر موارد، اولین coldest query است. حتی اگر از قبل کش سیستم عامل را از هر cold query پاک کنیم، نمیتوان آن را پایدار در نظر گرفت. بنابراین میتوان از آن فقط برای اهداف informational استفاده کرد. (حتی اگر بسیاری از نویسندگانِ معیار، چنین نتایجی را بدون هیچگونه سلب مسئولیتی منتشر میکنند).
- Fastest – تنها سریعترین نتیجه. در بیشتر موارد، باید مشابه معیار “Fast avg” باشد، اما میتواند اجرا به اجرا بیثباتتر شود.
به یاد داشته باشید که آزمایشات، شامل نتایج آنها، و همچنین همه چیز در این پروژه، 100% شفاف هستند. بنابراین:
- میتوانید از framework تست برای یادگیری نحوه ساخت آنها استفاده کنید.
- و نتایج خام آزمایش را در دایرکتوری نتایج پیدا کنید.
بر خلاف سایر معیارهای کمتر شفاف و کمتر هدفمند، ما هیچ نتیجهگیری نمیکنیم. فقط تصاویری از نتایج را در اینجا میگذاریم.
4 رقیب همزمان

Clickhouse vs Elasticsearch

Manticore Search (columnar storage) vs Elasticsearch

Manticore Search (columnar storage) vs Clickhouse

Manticore Search row-wise storage vs columnar storage

در مورد MySQL چطور؟
همانطور که در اسکرینشاتها مشاهده میکنید، MySQL نیز تست شده است. اما ما آن را با سایر موارد این پروژه مقایسه نمیکنیم. زیرا به شدت tune شده است. و کلیدها بر اساس query ها اضافه شدهاند.
سلب مسئولیت
نویسنده این آزمایش و framework تست یکی از اعضای تیم اصلی Manticore Search است و آزمایش در ابتدا برای مقایسه Manticore Search و Elasticsearch انجام شده بود. اما همانطور که در بالا نشان دادهشده است،و میتوان آن را در کد open source و با اجرای همان تست خودتان تأیید کرد، جستجوی Manticore هیچ مزیت غیرمنصفانهای ندارد. بنابراین میتوان آزمایش را بدون پیشداوری در نظر گرفت. با این حال، اگر چیزی در این آزمایش جا افتاده یا اشتباه است، (غیر هدفمند است) میتوانید در Github یک pull request ثبت کنید. برداشت شما برای ما ارزشمند است! از اینکه وقت خود را صرف خواندن این مطلب کردید، متشکرم!
منبع: db-benchmarks نویسنده: سرگی نیکولایِف
Leave feedback about this