مسیر Remote Read پرومتئوس در GreptimeDB ۱۰ برابر شد
خلاصهٔ کاملتر
گرپتایمدیبی میتونه بهعنوان بکاند remote read برای پرومتئوس کار کنه. حلقهٔ آخر این مسیر تابع recordbatches_to_timeseries هست که RecordBatch ستونیِ موتور کوئری رو به TimeSeries سطریِ پروتکل پرومتئوس تبدیل میکنه: سطرها رو بر اساس مجموعهٔ لیبل گروه میکنه و نمونههای هر سری رو کنار هم میذاره. این تابع مدتها دستنخورده تو مخزن مونده بود تا اینکه lyang24، یکی از committerهای پروژه، تو PR شمارهٔ ۸۵۸۷ بازنویسیاش کرد؛ تغییر دقیقاً یک فایل رو لمس میکنه: src/servers/src/prom_store.rs.
به گفتهٔ نویسنده، چیزی که توجهش رو جلب کرد یه پروفایل CPU گوشهٔ توضیح همون PR بود: با ۳۰۰ هزار سری و ۴ خوانش همزمان، مرحلهٔ تبدیل RecordBatch به TimeSeries معادل ۳۶.۲ درصد CPU میگرفت و باز شدن dictionary بهتنهایی ۸.۹ درصد. تابعی که نه چیزی رو از حالت فشرده در میاره نه به دیسک دست میزنه و فقط داده رو تو حافظه جابهجا میکنه. چون کد بنچمارک همراه PR نیومده بود، نویسنده خودش یکی نوشت و دو طرف تغییر رو اندازه گرفت: قبلش ۱۰ هزار سطر ۳.۷ میلیثانیه و ۱۰۰ هزار سطر ۴۴ میلیثانیه طول میکشید، یعنی زیر ۳ میلیون سطر در ثانیه.
دلیلش تو کد قدیمی روشنه: برای هر سطر یه بردار لیبلِ مالکیتدار ساخته میشد و همهٔ ستونها هم از قبل بهصورت کامل به رشته تبدیل میشدن. اگه R تعداد سطر، S تعداد سری و L میانگین تعداد لیبل باشه، هر پاس R·L بار to_string()، R تخصیص هیپ، R·L کپی رشته و یه جستوجوی BTreeMap بهازای هر سطر خرج میکرد. یعنی همهٔ هزینه با تعداد سطر بالا میره، در حالی که اطلاعات واقعیِ نتیجه فقط S سریه — و یه درخواست remote read معمولاً دهها نقطه بهازای هر سری داره.
نکتهٔ عجیبتر این بود که ستونهای dictionary حدود ۳۵ درصد کندتر از ستونهای رشتهٔ ساده در میاومدن، با اینکه dictionary اصلاً برای صرفهجویی تو حافظه و کپی وجود داره. مسیر خوانش PromQL لیبلها رو به شکل Dictionary برمیگردونه، ولی iter_column_as_string این نوع رو نمیشناسه و میافته رو مسیر عمومی و سطربهسطر to_string() صدا میزنه. یعنی ۳ رشتهٔ ذخیرهشده دوباره میشن ۱۰۰۰ رشته و هر تخصیصی که بالادست صرفهجویی شده بود، همین پاییندست دوباره انجام میشه.
جهت PR از همینجا معلومه: گروهبندی فقط لازمه مقدار هر سطر رو بخونه و مقایسه کنه، پس نیازی به کپی نیست — مستقیم از آرایهٔ Arrow قرض بگیر و فقط وقتی مطمئن شدی سریِ تازهای دیدی، تخصیص بده. برای این کار ستونهای لیبل تو یه enum قرضی بستهبندی میشن که هر نوع آرایه رو جدا هندل میکنه:
enum LabelValues<'a> {
Utf8(&'a StringArray),
LargeUtf8(&'a LargeStringArray),
Utf8View(&'a StringViewArray),
DictionaryUtf8 {
dictionary: &'a DictionaryArray<UInt32Type>,
values: &'a StringArray,
},
}نتیجه به گفتهٔ نویسنده در بهترین حالتها بیش از ۱۰ برابر سریعتره، و این بهینهسازی تو نسخهٔ GreptimeDB v1.2.0-beta.1 منتشر شده.
نکات کلیدی:
- تبدیل RecordBatch ستونی به TimeSeries پرومتئوس تو پروفایل ۳۶.۲ درصد CPU میگرفت
- هزینهٔ پیادهسازی قدیمی با تعداد سطر بالا میرفت، نه با تعداد سری — و بیشترش صرف ساختن دوبارهٔ همون مجموعه لیبل میشد
- ستونهای dictionary بهخاطر مسیر عمومی iter_column_as_string حدود ۳۵ درصد کندتر بودن
- راهحل: بهجای materialize کردن ستونها، قرضگرفتن مستقیم از آرایههای Arrow و تخصیص فقط برای سری تازه
- بیش از ۱۰ برابر بهبود در بهترین حالتها؛ منتشرشده در v1.2.0-beta.1




