Prometheus سیستم مانیتورینگ فوق‌العاده‌ای است، اما یکی از مشکلات آزاردهنده‌اش مصرف بالای منابع است. اگر این موضوع برای شما هم مثل من آزاردهنده است، بیایید عوامل این مصرف را بررسی کنیم و راهکارهای عملی برای کاهش آن را ببینیم 😊

قبل از هر چیز یک مرور کوتاه روی Prometheus، طراحی‌اش، اجزای درگیر و نحوه مصرف منابع توسط هر کدام داشته باشیم تا بعد دنبال مقصر اصلی بگردیم.

تاریخچه و معماری Prometheus

ایده Prometheus از 2012 شروع شد؛ تیم SoundCloud با الهام از Borgmon (سیستم مانیتورینگ Borg در Google) تصمیم گرفت یک نمونه متن‌باز بسازد و اولین نسخه عمومی در 2015 منتشر شد. Prometheus با زبان Go نوشته شده و به‌صورت یک باینری واحد اجرا می‌شود، اما درون آن چند فرایند مرتبط با اهداف مختلف فعال‌اند که قابل تمییز هستند.

اجزای اصلی Prometheus و کاری که انجام می‌دهند:

اول از همه باید بدانیم از کجا metricها را می‌گیریم. Service discovery این کار را انجام می‌دهد: با APIهای خارجی تعامل می‌کند، metadata می‌گیرد و لیستی از منابع داده (targets) می‌سازد.

مرحله بعدی جمع‌آوری داده‌هاست که Scraping نام دارد. Prometheus با مدل pull و پروتکل HTTP از targets داده می‌گیرد. این داده‌ها معمولاً از exporters یا خودِ اپلیکیشن‌ها برمی‌خیزند و با client libraries قابل انتشار به فرمت سازگار با Prometheus هستند.

پس از جمع‌آوری، داده‌ها باید جایی ذخیره شوند؛ Prometheus از TSDB (Time Series Database) برای ذخیره استفاده می‌کند.

برای مشاهده داده‌ها یک UI دارد، اما اغلب برای داشبورد و تحلیل از Grafana استفاده می‌شود چون UI داخلی Prometheus در مواردی ناکافی است.

Prometheus دو نوع rule دارد: recording و alerting.

Recording rules اجازه می‌دهند یک query اجرا شود و نتیجه‌اش به‌عنوان متریک جدید ذخیره شود (برای کاهش بار queryها و آماده‌سازی aggregationها).

Alerting rules شرایطی را بررسی می‌کنند و در صورت وقوع به سیستم‌های خارجی مثل Alertmanager هشدار می‌فرستند.

حالا که اجزای معماری را مرور کردیم، فهمیدن اینکه کدام بخش بیشترین منابع را مصرف می‌کند ساده است — یک نمودار فشرده کافی است تا ببینیم مصرف روی چه بخش‌هایی متمرکز است.

از آنجا که Prometheus با Go نوشته شده، می‌توانید با ابزار داخلی pprof بفهمید هر جزء چقدر حافظه مصرف می‌کند. در نمونه‌هایی که من بررسی کردم توزیع تقریبی مصرف حافظه چنین بود:

نتایج کلی نشان داد که:

Service discovery هزینه کمی دارد و تقریباً منابع زیادی مصرف نمی‌کند.

Rules معمولاً هر ساعت اجرا می‌شوند، پس نیاز به داده و منابع زیاد ندارند.

درخواست‌های UI هم تنها حدود 10٪ منابع را مصرف می‌کنند که جای تعجب دارد.

حدود 30٪ مصرف برای scraping معقول است چون داده‌ها به‌صورت متن خام دریافت و به ساختارهای داخلی تبدیل می‌شوند؛ عملیات رشته‌ای در Go هزینه‌بر است.

اما بیشترین سهم مصرف حافظه به Storage (TSDB) مربوط می‌شود — اینجا است که باید دقیق‌تر بگردیم 🔎

نگاهی دقیق‌تر به TSDB

در مقاله قبلی من که روی TSDBها با تمرکز بر Prometheus بود، چند نکته کلیدی مطرح شد که اینجا خلاصه می‌کنم.

تصور کنید مجموعه‌ای از دماسنج‌ها داریم که هر 30 یا 60 ثانیه مقدارشان گرفته می‌شود و همراه timestamp ذخیره می‌شوند. برای شناسایی هر دماسنج از label set استفاده می‌شود — مجموعه کلید-مقدارها. مثالی از label set:

{_name_="http_requests_total", job="ingress", method="GET"}

در Prometheus، label setها معمولاً در آکولاد نمایش داده می‌شوند و چون رشته‌های طولانی‌اند، به‌جای کار مستقیم با آن‌ها به هر label set یک ID اختصاص می‌یابد. سپس مقدارها همراه با ID ذخیره می‌شوند که همان time series است.

فرض کنید یک میلیون منبع داریم و یک هارد و محدوده‌ای از حافظه. نکته مهم اینکه TSDB یک active block دارد که در Prometheus معمولاً دو ساعت است و این block در حافظه نگهداری شده و بعداً روی دیسک flush می‌شود.

فرایند کلی ذخیره‌سازی به این ترتیب است:

1) مقدار از منبع scrape می‌شود و نگاشت label set به ID در حافظه ذخیره می‌شود.

2) همان نگاشت در log هم ذخیره می‌شود تا در صورت رخداد (ری‌بوت یا کمبود حافظه) بتوان داده‌ها را بازسازی کرد.

3) مقدار و timestamp در log نوشته می‌شود.

4) مقدار و timestamp در حافظه هم نوشته می‌شود.

این عملیات برای منابع بعدی تکرار می‌شود تا داده‌ای جمع شود که دوره‌ای به‌صورت بلاک به دیسک منتقل می‌گردد؛ هر بلاک مانند یک mini-database شامل مقادیر و مجموعه کامل labelها برای بازه زمانی خودش است.

در حافظه دو بخش مهم وجود دارد: داده‌ها و ساختارهای مربوط به label setها. بیایید به داده‌ها برگردیم و ببینیم چه چیزی مصرف حافظه را بالا می‌برد.

پردازش داده‌ها و encoding

داده‌ها در حافظه رمزگذاری و فشرده می‌شوند:

- مقادیر با الگوریتم GORILLA encoding ذخیره می‌شوند که فضای زیادی صرفه‌جویی می‌کند. نکته مهم این است که برای خواندن یک نقطه باید کل داده‌های chunk مربوطه دیکد شوند — یعنی معمولاً کل بازه دو ساعته باید پردازش شود.

- timestampها با Delta-Delta encoding فشرده می‌شوند؛ اگر intervalها منظم باشند، فضای ذخیره‌سازی timestamp بسیار کم خواهد بود.

یک دنباله از timestampها و مقدارها را chunk می‌نامیم. chunk پیش‌فرض در Prometheus دو ساعت داده را پوشش می‌دهد و بعد از پر شدن به دیسک flush می‌شود.

اگر یک میلیون منبع داشته باشید، به ازای هر منبع یک chunk خواهید داشت؛ در دو ساعت یک میلیون فایل خواهیم داشت که به‌ظاهر زیاد نیست، اما در 24 ساعت به 12 میلیون می‌رسیم. پراکندگی زیاد فایل‌ها روی دیسک خواندن را کند می‌کند.

برای دور زدن این مشکل، Prometheus chunkها را به‌صورت ترتیبی در یک فایل واحد می‌نویسد. این کار تعداد فایل‌ها را کم، داده را منسجم و عملیات خواندن را سریع می‌کند. همچنین آفست هر chunk مشخص است و می‌توان بدون خواندن کل فایل به محل موردنظر رفت. اندازه هر فایل تا حدود 128 MB محدود می‌شود.

روش نوشتن به این شکل است:

ابتدا اولین chunkها از همه منابع در فایل نوشته می‌شود؛ سپس اگر فضا مانده، دومین chunkها اضافه می‌شوند و الی آخر. وقتی فایل پر شد فایل جدید باز می‌شود و نوشتن ادامه می‌یابد.

مشکل اصلی این است که اندازه chunk فقط بر اساس بازه زمانی تعیین می‌شود، نه بر اساس تعداد نقاط داده. برای مثال اگر interval استاندارد 60s باشد، chunk دو ساعته 120 نقطه خواهد داشت؛ اما با interval=15s همان بازه دو ساعته 480 نقطه خواهد داشت.

دو چالش ایجاد می‌شود:

1) پیش‌بینی مقدار حافظه لازم برای نگه‌داشتن یک chunk مشکل است.

2) چون برای خواندن هر نقطه باید کل chunk دیکد شود، مصرف کلی حافظه افزایش می‌یابد.

برای مقابله با رشد حجم داده‌ها، Prometheus محدودیت 120 نمونه در هر chunk را اعمال می‌کند. وقتی chunk به این حد رسید، آن را فوراً به دیسک می‌نویسد.

برای حفظ سرعت خواندن، از memory-mapping استفاده می‌شود؛ این شبیه swap عمل می‌کند: ساختارها در RAM تعریف می‌شوند و وقتی دسترسی لازم شود، OS داده را از دیسک به حافظه می‌آورد و از دید اپلیکیشن انگار همیشه در RAM بوده است — این کار مصرف واقعی RAM را کاهش می‌دهد زیرا داده‌ها فیزیکی روی دیسک ذخیره‌اند و فقط در صورت نیاز بارگذاری می‌شوند.

بعد از حدود سه ساعت (یک بلوک دو ساعته کامل و نصف بلوک بعدی)، سیستم chunkهای متعلق به همان بازه زمانی را گروه‌بندی می‌کند و با افزودن ایندکس به آن‌ها یک بلاک معتبر ایجاد می‌شود و فرایند تکرار می‌گردد.

خلاصه تا اینجا:

- هر منبع داده در chunk مجزایی نوشته می‌شود. سایز chunk بر مبنای 120 نقطه است.

- chunkها به‌صورت ترتیبی در فایل‌هایی تا اندازه 128 MB ذخیره می‌شوند و وقتی فایل پر شد فایل جدیدی باز می‌شود.

- هر سه ساعت داده‌ها بسته‌بندی و به‌عنوان بلاک ذخیره می‌شوند.

- داده‌ها فشرده و encode می‌شوند: GORILLA برای مقادیر و Delta-Delta برای timestampها. نکته کلیدی GORILLA این است که برای دسترسی به هر نقطه باید chunk مربوطه دیکد شود که می‌تواند هزینه حافظه و CPU زیادی داشته باشد.

نتیجه‌گیری و راهکارهای عملی برای کاهش مصرف منابع 🛠️

اگر Prometheus شما حافظه‌گیر یا IO-bound است، اغلب مقصر افزایش cardinality و حجم داده‌ها در TSDB است. گزینه‌هایی که می‌توانید در نظر بگیرید (متخصصان DevOps/SRE آن‌ها را معمولاً پیاده می‌کنند):

- کاهش cardinality: با relabel_configs در scrape یا در service discovery، برچسب‌های پرکاربرد یا متغیر که باعث انفجار series می‌شوند را حذف یا نرمالایز کنید. در بسیاری از موارد حذف یک label غیرضروری مصرف را به‌طرز چشمگیری کم می‌کند.

- کاهش فرکانس Scrape: افزایش scrape_interval برای targets با تغییرات کند یا استفاده از exporters که aggregation محلی انجام می‌دهند می‌تواند فشار TSDB را کاهش دهد.

- استفاده از recording rules: محاسبات سنگین یا aggregationها را به recording rules منتقل کنید تا queryهای زمان اجرا کم‌هزینه‌تر شوند و نیاز به خواندن حجم زیادی از داده خام نباشد.

- sharding و federation: به‌جای یک instance‌ی بزرگ، داده‌ها را بین چندین Prometheus تقسیم کنید یا از federation برای تجمع امن و محدود کردن حجم داده‌ها در هر سرور استفاده کنید.

- remote_write و سیستم‌های مقیاس‌پذیر: برای نگهداری بلندمدت و مقیاس بالا از Thanos/Cortex/Mimir یا سیستم‌های مشابه استفاده کنید و نگهداری محلی را کوتاه نگه دارید؛ این باعث می‌شود TSDB محلی کوچک و کم‌هزینه بماند.

- حذف یا نمونه‌برداری متریک‌های پرتکرار: برای متریک‌هایی که حجم زیادی تولید می‌کنند، از techniques مثل downsampling یا کاهش نرخ نمونه‌برداری استفاده کنید.

- مانیتور و پروفایل: با pprof و ابزارهای OS-level (vmstat, iostat) مصرف حافظه و کارایی disk را مانیتور کنید تا دقیقاً بدانید گلوگاه کجاست و اقدامات دقیق‌تری انجام دهید.

- تنظیم retention و block duration: زمان نگهداری (retention) را بر اساس نیازهای تجاری تنظیم کنید و طول بلوک‌ها و پارامترهای TSDB را طوری تنظیم کنید که با الگوی scrape شما همخوانی داشته باشد.

در انتها، اگر دنبال یک راه حل کوتاه‌مدت هستید: به سرعت cardinality را بررسی کنید، لوپ‌های relabel را برای drop کردن labelهای غیرضروری فعال کنید، و برای داده‌های سرد یا بلندمدت از remote_write بهره ببرید. برای راه‌حل بلندمدت، معماری‌تان را به سمت sharding یا استفاده از راه‌حل‌های مقیاس‌پذیر تر مثل Thanos/Cortex ببرید تا هزینه‌های TSDB مدیریت‌پذیر شوند 🚀

اگر دوست دارید می‌توانم روی پایش cardinality یا یک playbook برای relabeling و recording rules در محیط شما کار کنم — بگو از کجا شروع کنیم تا دقیق‌تر پیشنهاد بدهم.

Prometheus prometheus: کاهش مصرف منابع و بهبود کارایی سیستم

این مطلب با تمرکز بر prometheus prometheus و جنبه‌های مدیریت مصرف منابع مانند مصرف حافظه، کارایی سیستم و مقیاس‌پذیری، نکات کلیدی و راهکارهای عملی را بررسی می‌کند.