SSD Nodes Learn 🎉 VPS از $5.50/ماه
راهنماها Matt Connorتوسط Matt Connor

مدیریت لاگ روی یک VPS: راهنمای بهینه و کم‌هزینه

برای مدیریت لاگ روی یک VPS، از journald شروع کنید یا Loki را جایگزین OpenSearch کنید. این راهنما حداقل رم مورد نیاز و استراتژی نگهداری داده برای سرورهای کوچک را بررسی می‌کند.

هزینه واقعی مدیریت لاگ به صورت self-hosted روی یک VPS

مدیریت لاگ به صورت self-hosted روی یک VPS (سرور مجازی) به یک پرسش اساسی ختم می‌شود: آیا به یک کلاستر جستجو نیاز دارید، یا به چرخش لاگ (rotation) و دستور grep؟ اکثر راهنماهای ارائه‌دهندگان، پاسخ را با پیشنهاد سه گره (node) و 12 گیگابایت رم، پیش از ارسال حتی یک خط لاگ، شروع می‌کنند. این پاسخ برای یک سرور واحد بی‌فایده است؛ بنابراین مقایسه زیر بر اساس منابعی است که هر گزینه پیش از ذخیره هرگونه داده، از یک سرور کوچک اشغال می‌کند.

اگر یک یا دو سرور دارید و می‌خواهید بدانید سه‌شنبه گذشته چه اتفاقی افتاده است، systemd-journald و logrotate همین حالا این کار را انجام می‌دهند و می‌توانید پس از بخش بعدی مطالعه را متوقف کنید. اگر چندین ماشین باید لاگ‌های خود را در یک مکان متمرکز کنند و امکان جستجو در بازه‌های چند هفته‌ای نیاز است، Grafana Loki برای یک سرور کوچک مناسب است، زیرا به جای متن خطوط، فقط برچسب‌ها (labels) را ایندکس می‌کند. Elasticsearch و OpenSearch قابلیت جستجوی تمام‌متن واقعی را به شما می‌دهند، اما هزینه آن را از حافظه رم دریافت می‌کنند؛ چرا که heap در JVM (ماشین مجازی جاوا) دارای حداقل مقداری است که نمی‌توان از آن کمتر رفت.

با journald شروع کنید، چون اکثر افراد در همین مرحله متوقف می‌شوند

سرویس systemd-journald روی هر سرور فعلی Ubuntu یا Debian در حال اجرا است. این سرویس خروجی استاندارد هر واحد سرویس، پیام‌های هسته (kernel) و هر چیزی که به syslog ارسال می‌شود را ثبت می‌کند. چهار دستور، اکثر حوادث را پوشش می‌دهند.

journalctl -u nginx.service --since "2026-08-14 09:00" --until "2026-08-14 10:00"
journalctl -p err -b
journalctl -f -u ssh.service
journalctl --disk-usage

آخرین دستور، خطی مانند Archived and active journals take up 1.1G in the file system. را چاپ می‌کند. این عددی است که تعیین می‌کند آیا به چیز دیگری نیاز دارید یا خیر. اگر حجم آن چند صد مگابایت باشد و بتوانید آنچه نیاز دارید را با -u و --since پیدا کنید، کار تمام است.

اینکه آیا journal پس از reboot باقی می‌ماند یا خیر، به Storage= و وجود /var/log/journal بستگی دارد. با تنظیم معمول Storage=auto، اگر دایرکتوری مذکور موجود باشد، journald در /var/log/journal می‌نویسد و در غیر این صورت در /run/log/journal می‌نویسد. /run در حافظه (RAM) قرار دارد، بنابراین در سیستمی که فاقد آن دایرکتوری باشد، تمام لاگ‌ها در زمان reboot پاک می‌شوند؛ یعنی دقیقاً در لحظه‌ای که می‌خواهید آن‌ها را بخوانید. ایمیج‌های Ubuntu این دایرکتوری را دارند. ایمیج‌های مینیمال و مبتنی بر container اغلب فاقد آن هستند.

ls -d /var/log/journal
sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journald
journalctl --disk-usage

پس از راه‌اندازی مجدد، journalctl --disk-usage باید حجمی کمتر از /var/log/journal را گزارش کند، نه /run. مقادیر پیش‌فرض از قبل محدود شده‌اند، که دلیل اصلی جدی بودن journald به عنوان یک راهکار اصلی (و نه جایگزین) است. صفحه راهنمای journald.conf مقدار SystemMaxUse= را روی 10 درصد از حجم فایل‌سیستم و SystemKeepFree= را روی 15 درصد تنظیم می‌کند و هر مقدار پیش‌فرض محاسبه‌شده را در 4G محدود می‌کند. SystemMaxFileSize= به‌طور پیش‌فرض یک‌هشتم SystemMaxUse= است که در 128M محدود شده، بنابراین معمولاً هفت فایل چرخانده‌شده (rotated) نگه می‌دارید. MaxRetentionSec= به‌طور پیش‌فرض 0 است که حذف بر اساس سن را غیرفعال می‌کند. آن مقدار پیش‌فرض آخر را دوباره بخوانید: به‌صورت پیش‌فرض، journal فقط بر اساس حجم محدود می‌شود و نه بر اساس سن.

[Journal]
Storage=persistent
SystemMaxUse=2G
MaxRetentionSec=30day

آن را در /etc/systemd/journald.conf.d/99-size.conf بنویسید، journald را restart کنید و سپس بررسی کنید که journalctl --disk-usage به سمت سقف جدید شما حرکت کرده باشد. برای بازپس‌گیری فضا در همین لحظه و بدون انتظار برای چرخش بعدی، sudo journalctl --vacuum-size=500M یا sudo journalctl --vacuum-time=14d را اجرا کنید. هر دو دستور، تمام فایل‌هایی که حذف می‌کنند را چاپ می‌کنند، بنابراین اجرای بی‌صدا به این معنی است که چیزی برای حذف وجود نداشته است.

هر چیزی خارج از journal، مانند /var/log/nginx/access.log، وظیفه logrotate است که روزانه توسط یک تایمر systemd اجرا می‌شود. یک خطا وجود دارد که دانستن آن مفید است، زیرا شبیه به یک باگ در df به نظر می‌رسد. پس از چرخش (rotation)، فایل قدیمی از لیست دایرکتوری حذف می‌شود در حالی که daemon همچنان آن را باز نگه داشته است؛ بنابراین df -h گزارش می‌دهد که دیسک پر است، در حالی که du -sh /var/log فضای بسیار کمتری را نشان می‌دهد. فضا تنها زمانی بازمی‌گردد که پردازش، لاگ خود را دوباره باز کند، که این دقیقاً هدف خط reload در تنظیمات postrotate است. sudo lsof -nP +L1 فایل‌های حذف‌شده‌ای که هنوز باز نگه داشته شده‌اند را لیست می‌کند و نام پردازشی که هر کدام را نگه داشته است، نمایش می‌دهد. برای تست یک قانون بدون تغییر در هیچ فایلی، از sudo logrotate -d /etc/logrotate.d/nginx استفاده کنید.

ارسال لاگ‌ها از چندین سرور به یک جمع‌کننده (collector)

هنگامی که تعداد سرورها از یک عدد فراتر می‌رود، مدیریت همزمان چندین سرور لینوکسی زمانی آسان‌تر می‌شود که لاگ‌های آن‌ها در یک مکان متمرکز جمع‌آوری شوند. از آنجا که rsyslog به‌صورت پیش‌فرض در اکثر توزیع‌ها نصب است، ارزان‌ترین راه برای راه‌اندازی یک جمع‌کننده مرکزی، استفاده از یک فایل روی هر فرستنده (sender) است.

*.* action(type="omfwd" target="logs.example.com" port="514" protocol="tcp")

آن را با نام /etc/rsyslog.d/50-forward.conf ذخیره کنید، سپس با استفاده از sudo rsyslogd -N1 آن را بررسی کنید؛ این دستور فایل پیکربندی را اعتبارسنجی کرده و بدون اجرای هیچ سرویسی خارج می‌شود. پس از آن، rsyslog را restart کنید. در سمت جمع‌کننده (collector)، ورودی TCP را فعال کنید.

module(load="imtcp")
input(type="imtcp" port="514")

دو هشدار فنی وجود دارد که باید به آن‌ها توجه کنید. پروتکل syslog ساده فاقد رمزنگاری و احراز هویت است؛ بنابراین هر چیزی که به پورت 514 دسترسی داشته باشد، می‌تواند خطوط لاگی را تزریق کند که دقیقاً مشابه لاگ‌های شما به نظر برسند. آن را به یک شبکه خصوصی یا VPN محدود کنید و پورت مربوطه را با فایروال ببندید. دوم اینکه، صف عملیات پیش‌فرض در حافظه (RAM) قرار دارد؛ بنابراین اگر جمع‌کننده در دسترس نباشد، صف پر شده و پیام‌ها بدون نگهداری هیچ نسخه‌ای از آن‌ها، حذف (drop) می‌شوند. rsyslog برای این حالت، استفاده از صف‌های مبتنی بر دیسک (disk-assisted queue) را در آموزش ارسال مطمئن خود مستند کرده است.

چرا پشته ELK برای یک VPS کوچک مناسب نیست

ELK مخفف Elasticsearch برای ذخیره‌سازی و جستجو، Logstash برای خط لوله دریافت داده و Kibana برای رابط کاربری است. کف حافظه مورد نیاز، همان JVM heap است که پیش از رسیدن هرگونه لاگ تعیین می‌شود.

مستندات Elastic توصیه می‌کند که heap را حداکثر روی 50 درصد از کل حافظه موجود برای هر نود Elasticsearch تنظیم کنید، زیرا این پردازش از بافرهای خارج از heap نیز استفاده می‌کند و برای خواندن سریع فایل‌های ایندکس به کش فایل سیستم‌عامل وابسته است. بنابراین، یک heap با حجم 2 GB به معنای نیاز به یک ماشین 4 GB است؛ آن هم پیش از در نظر گرفتن Kibana و هر سرویس دیگری که سرور برای اجرای آن خریداری شده است. Elastic همچنین بیان می‌کند که Elasticsearch حجم heap را به‌طور خودکار بر اساس نقش‌های نود و کل حافظه تعیین می‌کند؛ این یعنی یک سرور کوچک، heap کوچکی دریافت می‌کند و سپس تمام توان خود را صرف garbage collection می‌کند.

Logstash بخشی است که بودجه منابع یک سرور کوچک را کاملاً از بین می‌برد. صفحه تنظیمات JVM خودِ Elastic، برای دریافت داده‌های معمول، heapای بین 4GB تا 8GB را توصیه می‌کند. این مقدار، کل حافظه یک VPS با 4 GB رم است، آن هم فقط برای یک پردازش در میانه خط لوله.

ChartDocumented JVM heap settings, from each project's own docs
The data behind this chart
[
  {
    "label": "Loki plus Alloy (no JVM)",
    "documented_heap_mb": 0
  },
  {
    "label": "OpenSearch demo compose",
    "documented_heap_mb": 512
  },
  {
    "label": "OpenSearch production example",
    "documented_heap_mb": 2048
  },
  {
    "label": "Logstash recommended minimum",
    "documented_heap_mb": 4096
  }
]

این‌ها مقادیری هستند که هر پروژه در مستندات رسمی خود منتشر کرده است. این‌ها اندازه‌گیری‌های انجام‌شده روی یک سیستم تست نیستند و حجم کاری شما آن‌ها را تغییر خواهد داد. فایل compose نمونه OpenSearch مقدار 512 MB برای هر نود در حالت دمو و 2048 MB در مثال عملیاتی (production) تنظیم می‌کند، در حالی که حداقل مقدار توصیه شده برای Logstash برابر با 4096 MB است. ستون heap برای Loki و Alloy مقدار 0 را نشان می‌دهد، زیرا آن‌ها برنامه‌هایی با زبان Go هستند و هیچ JVM heapای برای رزرو کردن ندارند. تفاوت اصلی در همین یک عدد نهفته است: یک مؤلفه JVM، چه لاگی دریافت شود و چه نشود، مقدار رزرو شده خود را اشغال می‌کند.

اگر با این وجود قصد دارید پشته Elastic را روی یک سرور کوچک اجرا کنید، Logstash را حذف کنید و با استفاده از یک جمع‌آوری‌کننده (collector) سبک، داده‌ها را مستقیماً به Elasticsearch بفرستید. Logstash برای تجزیه و تبدیل داده‌ها در حجم بالا طراحی شده است و روی یک سرور واحد، می‌توانید این کار را در مبدأ انجام دهید یا از آن صرف‌نظر کنید.

هر دو نرم‌افزار Elasticsearch و OpenSearch همچنین نیاز دارند که مقدار vm.max_map_count به 262144 افزایش یابد، زیرا آن‌ها فایل‌های ایندکس را در حافظه نگاشت (memory map) می‌کنند و محدودیت پیش‌فرض لینوکس برای آن‌ها بسیار کم است. کانتینری که چند ثانیه پس از شروع در یک سرور تازه متوقف می‌شود، معمولاً به همین دلیل است و نه چیز دیگر.

OpenSearch یا Elasticsearch: کدام را می‌توانید مستقر کنید؟

تاریخچه کوتاه مجوزها، تعیین‌کننده این است که چه چیزی را مجاز به اجرا هستید. در ژانویه 2021، شرکت Elastic مجوز Elasticsearch و Kibana را از Apache 2.0 به مدل دوگانه SSPL (مجوز عمومی سمت سرور) و Elastic License 2.0 تغییر داد. AWS آخرین نسخه کد Apache 2.0 را با نام OpenSearch فورک کرد که همچنان تحت مجوز Apache 2.0 باقی مانده است. در سپتامبر 2024، Elastic مجوز AGPLv3 (مجوز عمومی همگانی آفرو گنو نسخه 3) را به عنوان گزینه دیگری برای کد منبع آزاد اضافه کرد. برای یک شخص که سرویسی را روی یک VPS شخصی میزبانی می‌کند، تمام این مجوزها فعالیت شما را مجاز می‌دانند. محدودیت‌های مجوز زمانی اعمال می‌شوند که شما نرم‌افزار را به عنوان یک سرویس مدیریت‌شده به دیگران ارائه دهید.

تفاوت عملی در یک سرور کوچک، کمتر از آن چیزی است که تاریخچه نشان می‌دهد، زیرا هر دو در زیرساخت از یک موتور مشابه استفاده می‌کنند. نام‌گذاری‌ها متفاوت است: چرخه حیات ایندکس در OpenSearch با نام ISM (مدیریت وضعیت ایندکس) و در Elasticsearch با نام ILM (مدیریت چرخه حیات ایندکس) شناخته می‌شود. از اوت 2026، نسخه‌های OpenSearch 2.12 و بالاتر بدون تنظیم رمز عبور مدیر در اولین اجرا، از شروع به کار خودداری می‌کنند.

sudo sysctl -w vm.max_map_count=262144
printf 'vm.max_map_count = 262144\n' | sudo tee /etc/sysctl.d/99-opensearch.conf
docker run -d -p 9200:9200 -p 9600:9600 -e "discovery.type=single-node" \
  -e "OPENSEARCH_INITIAL_ADMIN_PASSWORD=<custom-admin-password>" \
  opensearchproject/opensearch:latest

خط sysctl -w تنظیمات را بلافاصله اعمال می‌کند و فایلی که در /etc/sysctl.d/ قرار دارد، بخشی است که پس از reboot باقی می‌ماند. با استفاده از curl -k -u admin:<password> https://localhost:9200 بررسی کنید که container بالا آمده باشد. این سرویس از طریق https و با استفاده از یک گواهی آزمایشی پاسخ می‌دهد، بنابراین -k از بررسی اعتبار گواهی صرف‌نظر می‌کند و پاسخ سالم، یک بلوک JSON کوچک است که نام کلاستر و نسخه آن را نمایش می‌دهد. صفحه نصب OpenSearch همچنین به کاربران Docker Desktop توصیه می‌کند که حداقل 4 GB حافظه به میزبان اختصاص دهند، که نشان‌دهنده میزان منابع مورد انتظار این پردازش است.

چگونه Loki کم‌حجم باقی می‌ماند: استفاده از برچسب‌ها به‌جای ایندکس متن کامل

Loki یک ایندکس روی برچسب‌ها (labels) نگه می‌دارد و خطوط لاگ را به‌صورت تکه‌های فشرده ذخیره می‌کند. در یک کوئری، ابتدا جریان‌ها (streams) انتخاب شده و سپس متن فیلتر می‌شود. {unit="ssh.service"} |= "Failed password" جریان را بر اساس برچسب آن انتخاب می‌کند و سپس آن تکه‌ها را برای یافتن رشته موردنظر اسکن می‌کند. هیچ ایندکسی برای بدنهٔ خطوط ساخته نمی‌شود، بنابراین هزینهٔ دریافت داده (ingestion) پایین می‌ماند و نیازی به نگهداری ایندکس معکوس در حافظه نیست. هزینه به زمان اجرای کوئری منتقل می‌شود؛ این معامله‌ای منطقی است، چرا که معمولاً می‌دانید در حال بررسی کدام سرویس هستید.

مستندات Grafana حالت monolithic، یعنی اجرای تمام اجزای Loki در یک پردازش واحد با -target=all، را برای حجم خواندن و نوشتن پایین تا حدود 20GB در روز مناسب می‌داند. یک VPS به‌راحتی در این محدوده قرار می‌گیرد.

دام اصلی، کاردینالیتی (cardinality) برچسب‌ها است. هر ترکیب متمایز از مقادیر برچسب، یک جریان محسوب می‌شود و تعداد جریان‌ها، حافظه و اندازهٔ ایندکس Loki را تعیین می‌کند. برچسبی که حاوی آدرس IP کلاینت یا شناسهٔ درخواست باشد، به ازای هر مقدار یک جریان جدید ایجاد می‌کند؛ بنابراین یک وب‌سرور پرکار می‌تواند در روز ده‌ها هزار جریان تولید کند و پردازش آن‌قدر رشد می‌کند تا هستهٔ سیستم‌عامل (kernel) آن را متوقف کند. برچسب‌ها را به مقادیری محدود کنید که بتوانید روی کاغذ بشمارید: unit، host، job، level. جزئیات متغیر را در خودِ خط لاگ قرار دهید، جایی که یک عبارت فیلتر در زمان اجرای کوئری آن را پیدا می‌کند.

نصب Loki و Alloy روی یک VPS

دو پردازش این کار را انجام می‌دهند. Loki لاگ‌ها را ذخیره کرده و به پرس‌وجوها پاسخ می‌دهد. Grafana Alloy لاگ‌ها را می‌خواند و به مقصد ارسال می‌کند. در گذشته Promtail وظیفه ارسال را بر عهده داشت، اما در تاریخ 2 March 2026 به پایان عمر خود رسید؛ بنابراین در نصب‌های جدید از Alloy استفاده می‌شود و نمونه Docker خودِ Loki اکنون شامل یک پیکربندی Alloy است.

wget https://raw.githubusercontent.com/grafana/loki/v3.7.0/cmd/loki/loki-local-config.yaml -O loki-config.yaml

پیش از استفاده از آن فایل، آن را مطالعه کنید. این فایل path_prefix: /tmp/loki را با chunkها در مسیر /tmp/loki/chunks تنظیم می‌کند که برای یک دمو مناسب است اما برای سرور اشتباه است: هیچ داده‌ای در مسیر /tmp کانتینر پس از بازسازی کانتینر باقی نمی‌ماند، بنابراین با به‌روزرسانی بعدی image، تاریخچه شما از بین می‌رود. آن را به مسیری که mount کرده‌اید اشاره دهید.

common:
  instance_addr: 127.0.0.1
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory
docker volume create loki-data
docker run --name loki -d \
  -v $(pwd):/mnt/config -v loki-data:/loki \
  -p 127.0.0.1:3100:3100 \
  grafana/loki:3.7.0 -config.file=/mnt/config/loki-config.yaml
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3100/ready

دستور آخر باید 200 را چاپ کند، زیرا /ready به محض اینکه Loki آماده پذیرش ترافیک باشد، کد HTTP 200 برمی‌گرداند. هر خروجی دیگری به این معنی است که پردازش هنوز در حال شروع است یا پیکربندی رد شده است، و docker logs loki دلیل آن را مشخص می‌کند. دو جزئیات در دستور اجرا عمدی هستند. پورت فقط روی 127.0.0.1 منتشر می‌شود، زیرا پیکربندی نمونه شامل auth_enabled: false است و Loki به‌صورت پیش‌فرض احراز هویت کاربری ندارد؛ بنابراین هر کسی که به پورت 3100 دسترسی داشته باشد می‌تواند تمام لاگ‌ها را بخواند یا لاگ‌های جعلی بنویسد. آن را روی loopback نگه دارید یا پشت یک VPN یا یک reverse proxy دارای احراز هویت قرار دهید. استفاده از volume نام‌گذاری‌شده اهمیت دارد، زیرا image با کاربر loki و UID 10001 اجرا می‌شود، بنابراین دایرکتوری host که با bind mount متصل شده و متعلق به root است، توسط کانتینر قابل نوشتن نخواهد بود.

sudo apt-get update && sudo apt-get install -y gpg wget
sudo mkdir -p /etc/apt/keyrings/
sudo wget -O /etc/apt/keyrings/grafana.asc https://apt.grafana.com/gpg-full.key
echo "deb [signed-by=/etc/apt/keyrings/grafana.asc] https://apt.grafana.com stable main" \
  | sudo tee /etc/apt/sources.list.d/grafana.list
sudo apt-get update
sudo apt-get install -y alloy

Alloy فایل /etc/alloy/config.alloy را می‌خواند. این فایل journal سیستم و یک مجموعه از فایل‌ها را می‌گیرد و هر دو را به Loki محلی ارسال می‌کند.

loki.write "local" {
  endpoint {
    url = "http://127.0.0.1:3100/loki/api/v1/push"
  }
}

loki.relabel "journal" {
  forward_to = []
  rule {
    source_labels = ["__journal__systemd_unit"]
    target_label  = "unit"
  }
}

loki.source.journal "read" {
  forward_to    = [loki.write.local.receiver]
  relabel_rules = loki.relabel.journal.rules
  labels        = {job = "systemd-journal", host = "app-01"}
}

local.file_match "nginx" {
  path_targets = [{"__path__" = "/var/log/nginx/*.log", "job" = "nginx", "host" = "app-01"}]
}

loki.source.file "nginx" {
  targets    = local.file_match.nginx.targets
  forward_to = [loki.write.local.receiver]
}

قانون relabel، فیلد __journal__systemd_unit از journal را در برچسبی به نام unit کپی می‌کند که باعث می‌شود {unit="ssh.service"} بعداً به‌درستی کار کند. بدون این قانون، نام unit درون ورودی قرار می‌گیرد و نه در یک برچسب، بنابراین نمی‌توانید بر اساس آن فیلتر کنید و هر پرس‌وجو مجبور است همه چیز را اسکن کند.

sudo systemctl reload alloy
systemctl show -p User alloy
sudo journalctl -n 5
sudo -u alloy journalctl -n 5

اینجاست که اکثر تنظیمات متوقف می‌شوند. Alloy به عنوان یک سرویس‌حساب (service account) اختصاصی اجرا می‌شود، نه به عنوان root، و خواندن journal سیستم نیازمند عضویت در گروه systemd-journal است، در حالی که فایل‌های تحت /var/log/nginx در Debian و Ubuntu متعلق به گروه adm هستند. حسابی که systemctl show در دستور آخر چاپ کرده است را جایگزین کنید. اگر تعداد ورودی‌های بازگشتی بسیار کمتر از اجرای root باشد، آن حساب نمی‌تواند journal سیستم را بخواند و Loki با وجود صحیح بودن پیکربندی، خالی می‌ماند. گروه‌ها را اضافه کنید و با sudo usermod -aG systemd-journal,adm alloy و سپس sudo systemctl restart alloy سرویس را مجدداً راه‌اندازی کنید.

curl -G -s "http://127.0.0.1:3100/loki/api/v1/query_range" \
  --data-urlencode 'query={job="systemd-journal"}' \
  --data-urlencode 'limit=5' | jq '.data.result | length'

عددی بزرگتر از 0 به این معنی است که streamهایی با آن برچسب وجود دارند و حاوی ورودی هستند. عدد 0 به این معنی است که هنوز چیزی با آن برچسب دریافت نشده است. یک مقدار پیش‌فرض، یک هشدار اشتباه رایج را توضیح می‌دهد: loki.source.journal مقدار max_age را روی 7h تنظیم می‌کند، بنابراین یک شروع تازه، هفت ساعت اخیر journal را می‌خواند و نه قدیمی‌تر از آن. برای رابط کاربری انسانی، Grafana را روی همان سیستم اجرا کنید و منبع داده Loki را به http://127.0.0.1:3100. اشاره دهید. لاگ‌های کانتینر به منبع متفاوتی نیاز دارند: Alloy کانتینرهای Docker در حال اجرا را شناسایی کرده و آن‌ها را دنبال (tail) می‌کند، که همان کاری است که نمونه شروع به کار خودِ Loki انجام می‌دهد، و در یک کلاستر k3s تک‌گره روی یک VPS، آن job به دایرکتوری لاگ pod که توسط kubelet نوشته می‌شود، منتقل می‌گردد.

نگهداری: روزی را انتخاب کنید که لاگ‌هایتان پاک می‌شوند

تقریباً هیچ‌کس تا زمانی که دیسک پر نشود، دوره نگهداری (retention) را انتخاب نمی‌کند؛ و وقتی هم که این کار را می‌کنند، ساعت 3 صبح است و سرویس از دسترس خارج شده. این تصمیم را از همان روز اول و با پاسخ به دو پرسش بگیرید: واقعاً تا چه زمانی به عقب برمی‌گردید و بررسی می‌کنید، و در طول بررسی یک حادثه در ماه آینده، به چه داده‌هایی نیاز دارید. برای یک سرور تکی، بازه 14 تا 30 روز پاسخگوی هر دو مورد است.

Loki تا زمانی که compactor را فعال نکنید، هیچ چیزی را حذف نمی‌کند. نگهداری به‌صورت پیش‌فرض غیرفعال است، که این موضوع باعث غافلگیری کسانی می‌شود که حجم دیسک‌شان پر شده، در حالی که retention_period در فایل پیکربندی بدون هیچ عملکردی قرار داشته است.

limits_config:
  retention_period: 744h

compactor:
  working_directory: /loki/retention
  compaction_interval: 10m
  retention_enabled: true
  retention_delete_delay: 2h
  retention_delete_worker_count: 150
  delete_request_store: filesystem

مقدار 744h معادل 31 روز است. چهار قانون مستند بر این بلوک حاکم است:

  • نگهداری توسط compactor اعمال می‌شود و مستندات Grafana توصیه می‌کنند که compactor را به صورت یک instance تکی اجرا کنید. در یک VPS، این اتفاق به‌طور خودکار می‌افتد.
  • حداقل دوره نگهداری 24h است و نگهداری تنها زمانی کار می‌کند که دوره ایندکس 24h باشد. نمونه schema_config از قبل از period: 24h استفاده می‌کند، پس آن را تغییر ندهید.
  • وقتی retention_enabled برابر با true باشد، delete_request_store الزامی است. این گزینه محل ذخیره‌سازی درخواست‌های حذف را مشخص می‌کند، بنابراین در یک گره تکی که از فایل‌سیستم استفاده می‌کند، باید با object_store: filesystem که در schema تعریف شده مطابقت داشته باشد.
  • چانک‌ها (chunks) ابتدا علامت‌گذاری شده و پس از retention_delete_delay حذف می‌شوند که در اینجا 2h است؛ بنابراین فضای آزاد دیرتر از آنچه سیاست تعیین کرده، بازمی‌گردد. تنظیمات را با df پنج دقیقه پس از reload قضاوت نکنید.

OpenSearch به جای خطوط تکی، کل ایندکس‌ها را حذف می‌کند؛ به همین دلیل است که ایندکس‌های لاگ به‌صورت روزانه ایجاد می‌شوند. یک سیاست ISM، ایندکس را از وضعیت‌های مختلف عبور می‌دهد و پس از اینکه به اندازه کافی قدیمی شد، آن را حذف می‌کند. یک ism_template نیز این سیاست را به ایندکس‌های جدید متصل می‌کند تا نیازی به یادآوری دستی نداشته باشید.

سیاست ISM که ایندکس‌های لاگ را پس از 14 روز حذف می‌کند
{
  "policy": {
    "description": "delete log indexes after 14 days",
    "default_state": "hot",
    "states": [
      {
        "name": "hot",
        "actions": [],
        "transitions": [
          { "state_name": "delete", "conditions": { "min_index_age": "14d" } }
        ]
      },
      {
        "name": "delete",
        "actions": [ { "delete": {} } ],
        "transitions": []
      }
    ],
    "ism_template": { "index_patterns": ["logs-*"], "priority": 100 }
  }
}

آن را با یک درخواست PUT به _plugins/_ism/policies/logs-retention ایجاد کنید. این الگو (template) برای ایندکس‌هایی اعمال می‌شود که پس از ایجاد سیاست ساخته شده‌اند، بنابراین هر چیزی که از قبل روی دیسک وجود دارد، باید به‌صورت دستی به این سیاست متصل شود.

هر سیستمی که اجرا می‌کنید، یک عدد برای نگهداری تنها زمانی کارآمد است که یک بررسی فضای آزاد در پشت آن باشد. اگر 10 روز لاگ، دیسک را پر کرده باشد، حذف در 14 روز کمکی به شما نمی‌کند؛ بنابراین این سیاست را با مانیتورینگ سلامت دیسک در VPS و یک هشدار در 80 درصد فضای مصرفی همراه کنید.

میزان فضای دیسک مورد نیاز برای هر گیگابایت لاگ

پاسخ صادقانه به تعداد خطوط و فیلدهای شما بستگی دارد، بنابراین به‌جای اعتماد به نسبت‌های منتشرشده، آن را روی داده‌های خودتان اندازه‌گیری کنید. مکانیزم‌ها به اندازه‌ای متفاوت هستند که جهت تغییرات را پیش‌بینی کنند. OpenSearch و Elasticsearch در کنار سند ذخیره‌شده، یک inverted index برای هر فیلد نمایه‌گذاری‌شده می‌نویسند، بنابراین آنچه روی دیسک قرار می‌گیرد از متن خام بزرگ‌تر است و هر replica آن را چند برابر می‌کند. در یک node تکی، تعداد replica را روی 0 تنظیم کنید، زیرا یک shard از نوع replica روی همان node نمی‌تواند در صورت خرابی آن node زنده بماند: باقی گذاشتن آن روی 1، فضای دیسک را دو برابر کرده و وضعیت سلامت کلاستر را برای همیشه روی yellow قفل می‌کند. Loki قطعات فشرده‌شده (compressed chunks) را به همراه یک ایندکس کوچک از برچسب‌ها می‌نویسد، بنابراین حجم مصرفی آن با اندازه فشرده‌شده خطوط همخوانی دارد.

sudo du -sh /var/lib/docker/volumes/loki-data/_data
curl -k -u admin:<password> "https://localhost:9200/_cat/indices?v&h=index,docs.count,store.size"

هر کدام که برای شما صدق می‌کند را در دو روز متوالی اجرا کنید. اختلاف حاصل، رشد روزانه شماست. آن را در تعداد روزهای نگهداری (retention) ضرب کنید، حدود 30 درصد فضای اضافه برای compaction و merge در نظر بگیرید و سپس آن را با حجم دیسک مقایسه کنید. اگر در دیسک جا نمی‌شود، پیش از خرید دیسک جدید، مدت زمان نگهداری را کاهش دهید، زیرا حجم بیشتر فقط همان مشکل را چند هفته به تعویق می‌اندازد.

چه چیزی در یک سرور کوچک زودتر از کار می‌افتد

حافظه اولین منبعی است که تمام می‌شود. مکانیزم OOM (مخفف out of memory) در هسته، یک پردازش بزرگ را انتخاب می‌کند و بزرگ‌ترین پردازش در یک سرور لاگ‌گیری، معمولاً JVM است. journalctl -k | grep -i "killed process" این توقف پردازش را با نام آن در پرانتز نشان می‌دهد. قربانی همیشه پشتهٔ لاگ نیست: ممکن است sshd یا دیتابیس شما انتخاب شود؛ این همان دلیلی است که باعث می‌شود یک آزمایش لاگ‌گیری، برنامه‌ای را که قصد داشتید لاگ‌هایش را بگیرید، از کار بیندازد. برای کانتینرها سقف‌های مشخصی تعیین کنید تا خرابی در جایی رخ دهد که شما انتخاب کرده‌اید؛ این دقیقاً همان کاری است که محدودیت‌های حافظه در Docker Compose برای آن طراحی شده‌اند.

دیسک دومین منبعی است که تمام می‌شود و موتورهای جستجو به روشی خاص و قابل‌تشخیص از کار می‌افتند. Elasticsearch و OpenSearch میزان استفاده از دیسک را در سطوح مختلف پایش می‌کنند. حد پایین (low watermark) روی 85 درصد و حد بالا (high watermark) روی 90 درصد تنظیم شده است. در مرحلهٔ بحرانی 95 درصد، هر ایندکسی که شارد (shard) روی آن نود داشته باشد، بلاک index.blocks.read_only_allow_delete را دریافت می‌کند و عملیات نوشتن با خطای blocked by: [FORBIDDEN/12/index read-only / allow delete (api)] مواجه می‌شود. این بلاک پس از کاهش استفاده از دیسک به زیر حد بالا، آزاد می‌شود. ابتدا فضای دیسک را خالی کنید و تنها اگر وضعیت بلاک باقی ماند، آن را به‌صورت دستی آزاد کنید.

curl -k -u admin:<password> -X PUT "https://localhost:9200/_all/_settings" \
  -H 'Content-Type: application/json' \
  -d '{"index.blocks.read_only_allow_delete": null}'

Loki با صدای کمتری از کار می‌افتد. این سرویس حالت read-only ندارد، بنابراین پر شدن حجم دیسک به شکل شکست در ارسال لاگ‌ها (failed pushes) در سمت فرستنده و ایجاد شکاف در نتایج جستجو ظاهر می‌شود. همچنین مشکل cardinality به جای نمایش خطا، به صورت افزایش تدریجی مصرف حافظه بروز می‌کند. حجم دایرکتوری chunks را طبق یک برنامهٔ زمانی پایش کنید، نه پس از وقوع حادثه.

آخرین مورد از خرابی‌ها، وارد کردن داده‌های اشتباه است. سیستم لاگ، سیستم متریک نیست: بار CPU که هر 10 ثانیه نمونه‌برداری و به صورت متن ذخیره شود، نگهداری‌اش پرهزینه و رسم نمودار آن دشوار است؛ این وظیفه بر عهدهٔ ابزاری مانند یک سرور مانیتورینگ Zabbix روی Ubuntu 24.04 است. استثناهای برنامه (exceptions) نیاز به گروه‌بندی، حذف موارد تکراری و نمایش stack trace دارند که وظیفهٔ یک ردیاب خطای self-hosted است. اطلاع از اینکه سایت اصلاً بالا است یا خیر، وظیفهٔ جداگانه‌ای است که توسط یک صفحه وضعیت و آپ‌تایم مانند Uptime Kuma پاسخ داده می‌شود. سیستم لاگ را برای خطوط متنی که قرار است توسط انسان خوانده شوند، حفظ کنید.

FAQ

آیا برای جستجوی لاگ‌های سرور به Elasticsearch نیاز دارم؟

برای یک یا دو سرور، خیر. journalctl در حال حاضر امکان فیلتر کردن بر اساس واحد (unit)، اولویت، بوت و بازه زمانی را فراهم می‌کند و فایل‌های چرخش‌یافته (rotated) نیز با grep و zgrep قابل بررسی هستند. استفاده از یک کلاستر جستجو زمانی توجیه‌پذیر است که تعداد زیادی ماشین داشته باشید، نیاز به جستجوی متن آزاد (free text) در تمام آن‌ها به‌صورت هم‌زمان داشته باشید، یا چندین نفر نیاز به یک رابط کاربری مشترک داشته باشند. در مقیاس‌های کوچک‌تر، journald با تنظیم محدودیت حجم و زمان نگهداری، همان کار را بدون مصرف RAM اضافه انجام می‌دهد.

برای مدیریت لاگ در حالت self-hosted به چه مقدار RAM نیاز دارم؟

به‌جای استفاده از یک قاعده کلی، به ارقام منتشرشده برای هر پروژه استناد کنید. Loki و Alloy برنامه‌هایی به زبان Go هستند که heap از پیش رزرو نمی‌کنند و Grafana برای Loki به‌صورت monolithic، پردازش تا حدود 20GB در روز را مستند کرده است. نمونه compose برای OpenSearch، مقدار 512 MB حافظه heap را برای نسخه نمایشی و 2 GB را در مثال عملیاتی پیشنهاد می‌دهد؛ Elastic نیز تأکید دارد که heap باید در حد 50% یا کمتر از کل حافظه باشد، بنابراین یک heap به حجم 2 GB به معنای نیاز به یک ماشین 4 GB پیش از احتساب Kibana است. مستندات Logstash توصیه می‌کند که کمتر از 4GB حافظه heap برای آن در نظر نگیرید. این‌ها تنظیمات مستندشده هستند، نه بنچمارک؛ بنابراین پیش از تعیین ابعاد سرور، بار کاری خود را اندازه‌گیری کنید.

تفاوت واقعی بین Loki و OpenSearch برای لاگ‌ها چیست؟

مدل ایندکس‌گذاری. Loki فقط برچسب‌ها (labels) را ایندکس می‌کند و بدنه لاگ را به‌صورت تکه‌های فشرده نگه می‌دارد که در زمان کوئری اسکن می‌شوند؛ بنابراین عملیات نوشتن کم‌هزینه است اما کوئری‌های گسترده هزینه بیشتری دارند. OpenSearch محتوای فیلدها را ایندکس می‌کند، بنابراین جستجوی متنی کامل (full text search) بسیار سریع است، اما حافظه و دیسک هزینه ایندکس را می‌پردازند. اگر می‌دانید به کدام سرویس و بازه زمانی نیاز دارید، Loki را انتخاب کنید. اگر نیاز دارید متنی را جستجو کنید که از قبل قابل پیش‌بینی نیست، OpenSearch را انتخاب کنید.

لاگ‌ها را تا چه مدت باید روی VPS نگه دارم؟

پیش از آنکه دیسک شما را مجبور به حذف کند، خودتان عددی را انتخاب کنید. این مقدار را در هر سیستم فقط در یک نقطه تنظیم کنید: MaxRetentionSec= و SystemMaxUse= برای journald، retention_period با فعال‌سازی compactor برای Loki، و یک سیاست ISM با min_index_age برای OpenSearch. برای اکثر تنظیمات تک‌سرور، بازه 14 تا 30 روز برای دیباگ و بررسی حوادث کافی است. هر چیزی که باید طولانی‌تر نگهداری شود، باید در نسخه‌ای خارج از سرور ذخیره شود، زیرا لاگی که فقط روی سرورِ دچار مشکل باقی بماند، یک رکورد قابل اتکا نیست.

آیا Promtail همچنان روش مناسب برای ارسال لاگ به Loki است؟

خیر. عمر Promtail در تاریخ 2 March 2026 به پایان رسید و Grafana Alloy جایگزین آن شده است. نمونه نصب Docker برای Loki اکنون شامل پیکربندی Alloy است و Grafana ابزاری برای تبدیل پیکربندی فعلی Promtail به سینتکس Alloy ارائه می‌دهد. نصب فعلی Promtail همچنان کار می‌کند، اما هیچ به‌روزرسانی امنیتی دریافت نمی‌کند؛ بنابراین مهاجرت به آن را به‌عنوان یک وظیفه نگهداری در نظر بگیرید، نه ارتقایی که بتوان آن را تا بی‌نهایت به تعویق انداخت.

#logging#loki#opensearch#journald#monitoring