Kubernetes و کانتینرمهندسانسطح پیشرفته

پر شدن فضای etcd؛ خطای mvcc: database space exceeded

وقتی فایل backend در etcd از فضای تعیین‌شده عبور می‌کند، کل کلاستر Kubernetes به حالت فقط‌خواندنی می‌رود. در این مقاله علت fragmentation را بررسی می‌کنیم و با defrag، disarm و تنظیم quota کلاستر را برمی‌گردانیم.

تحریریه کلودیکپ ۷ دقیقه مطالعه
پر شدن فضای etcd؛ خطای mvcc: database space exceeded

اگر یک روز صبح kubectl apply با خطای عجیبی برگردد، هیچ پاد جدیدی ساخته نشود، اما kubectl get همچنان بی‌مشکل کار کند، احتمال زیادی وجود دارد که با کلاسیک‌ترین حالت خرابی کنترل‌پلین روبه‌رو شده باشید: پر شدن فضای ذخیره‌سازی etcd. این مقاله دقیقاً همان سناریو را بررسی می‌کند.

شرح مسئله

نشانه اصلی، پیامی است که هم از سمت کلاینت و هم در لاگ kube-apiserver دیده می‌شود:

Error from server: etcdserver: mvcc: database space exceeded

در لاگ kube-apiserver هم خطوطی شبیه این تکرار می‌شود:

{"level":"warn","msg":"etcdserver: mvcc: database space exceeded","logger":"etcd-client"}
{"level":"error","msg":"Updating endpoint failed","logger":"apiserver"}

رفتاری که در عمل می‌بینید:

  • عملیات خواندن (kubectl get، kubectl describe) سالم است؛ حتی پادهای در حال اجرا به کار خود ادامه می‌دهند.

  • هر عملیات نوشتن شکست می‌خورد: apply، create، scale، ساخت Secret، به‌روزرسانی Deployment.

  • kube-controller-manager نمی‌تواند leader election را تمدید کند و مدام restart می‌شود؛ در نتیجه garbage collection و HPA هم از کار می‌افتند.

  • رویدادهای جدید ثبت نمی‌شوند؛ kubectl get events عملاً منجمد می‌شود.

  • خروجی etcdctl endpoint status نشان می‌دهد حجم دیتابیس به سقف نزدیک شده است.

نکته مهم این است که این وضعیت «خرابی» نیست؛ etcd عمداً برای حفظ یکپارچگی داده، نوشتن را رد می‌کند. کلاستر زنده است، ولی عملاً قفل شده.

علت ریشه‌ای

etcd از مدل MVCC استفاده می‌کند. هر نوشتن یک revision جدید می‌سازد و کلید قدیمی پاک نمی‌شود؛ فقط نسخه جدید اضافه می‌شود. کلیدهای حذف‌شده هم به‌صورت tombstone باقی می‌مانند. تا وقتی این نسخه‌های قدیمی compact نشوند، فضای منطقی آن‌ها آزاد نمی‌شود.

مسئله دوم اما جایی است که بیشتر مهندسان را گمراه می‌کند: compact کردن، فایل را کوچک نمی‌کند. فایل backend یک فایل bbolt است و پس از compact شدن، صفحات آزاد داخل همان فایل می‌مانند. برای برگرداندن این فضا به فایل‌سیستم، باید defrag انجام شود. به همین دلیل معمولاً این الگو را می‌بینید: حجم فایل دیتابیس بالا می‌رود، اما حجم in-use پایین است و رشد نمی‌کند.

وقتی حجم فایل backend از مقدار --quota-backend-bytes (پیش‌فرض ۲ گیبی‌بایت) عبور کند، etcd یک آلارم از نوع NOSPACE فعال می‌کند و تا زمانی که این آلارم برداشته نشود، همه نوشتن‌ها رد می‌شوند. همین است که کلاستر فقط‌خواندنی می‌شود.

منابع رایج این رشد در کلاسترهای Kubernetes:

  • رویدادها (Events) با نرخ بالا، به‌خصوص وقتی کسی چرخه‌ای از restart پادها دارد.

  • Jobs و CronJobs بدون ttlSecondsAfterFinished که تاریخچه انبوه تولید می‌کنند.

  • Secretهای بزرگ (مثل kubeconfig یا کلیدهای TLS حجیم) که مرتب به‌روزرسانی می‌شوند.

  • churn در Endpoints و Leaseها در کلاسترهای پرترافیک.

  • به‌روزرسانی‌های پرتکرار Helm/GitOps روی Deploymentهای بزرگ با manifestهای چندصد کیلوبایتی.

توجه داشته باشید که kube-apiserver به‌طور پیش‌فرض هر ۵ دقیقه compaction انجام می‌دهد (--etcd-compaction-interval)، پس در محیط Kubernetes معمولاً مشکل «کمپکت نشدن» نیست؛ مشکل fragmentation است.

راه‌حل گام‌به‌گام

گام ۰: بکاپ بگیرید

قبل از هر کاری، یک snapshot سالم بگیرید. بدون این گام سراغ defrag نروید.

export ETCDCTL_API=3
export ETCDCTL_CACERT=/etc/kubernetes/pki/etcd/ca.crt
export ETCDCTL_CERT=/etc/kubernetes/pki/etcd/server.crt
export ETCDCTL_KEY=/etc/kubernetes/pki/etcd/server.key

etcdctl --endpoints=https://127.0.0.1:2379 \
  snapshot save /var/backups/etcd-$(date +%F-%H%M).db

etcdctl snapshot status /var/backups/etcd-*.db --write-out=table

گام ۱: وضعیت و آلارم‌ها را تأیید کنید

etcdctl --endpoints=https://10.0.0.11:2379,https://10.0.0.12:2379,https://10.0.0.13:2379 \
  endpoint status --write-out=table

etcdctl --endpoints=https://10.0.0.11:2379,https://10.0.0.12:2379,https://10.0.0.13:2379 \
  alarm list

خروجی جدول ستون‌های DB SIZE، IS LEADER، RAFT INDEX و ERRORS را نشان می‌دهد. اگر DB SIZE نزدیک فضای آن است و alarm list عضوی با آلارم NOSPACE برمی‌گرداند، تشخیص تأیید است. توجه کنید که DB SIZE باید در همه اعضا نزدیک به هم باشد؛ اختلاف فاحش یعنی یک عضو از کلاستر عقب مانده و مشکل جداگانه‌ای دارید.

گام ۲: defrag اعضا، یکی‌یکی و از followerها شروع کنید

defrag یک عملیات blocking است؛ در طول اجرا آن عضو نمی‌تواند به خواندن و نوشتن پاسخ دهد. در کلاستر سه‌عضوی، تا وقتی quorum حفظ شود سرویس‌دهی ادامه دارد، اما ترتیب کار مهم است: اول followerها، آخر leader.

for ep in https://10.0.0.12:2379 https://10.0.0.13:2379 https://10.0.0.11:2379; do
  echo "### defrag $ep"
  etcdctl --endpoints=$ep defrag --command-timeout=600s
done

مقدار --command-timeout را بزرگ‌تر از زمان مورد انتظار defrag بگذارید؛ در دیتابیس‌های چند گیگی، defrag می‌تواند چند دقیقه طول بکشد و تایم‌اوت پیش‌فرض ۵ ثانیه‌ای etcdctl باعث می‌شود فکر کنید عملیات شکست خورده، در حالی که در سرور ادامه دارد.

اگر etcd به‌صورت static pod در کلاستر kubeadm اجرا می‌شود، همین کار را داخل پاد انجام دهید:

kubectl -n kube-system exec -it etcd-cp1 -- sh -c '
  ETCDCTL_API=3 etcdctl \
    --endpoints=https://127.0.0.1:2379 \
    --cacert=/etc/kubernetes/pki/etcd/ca.crt \
    --cert=/etc/kubernetes/pki/etcd/server.crt \
    --key=/etc/kubernetes/pki/etcd/server.key \
    defrag --command-timeout=600s'

در محیط external etcd، همین دستور را روی خود ماشین‌ها با آدرس‌های واقعی هر عضو اجرا کنید.

گام ۳: آلارم را بردارید

تا زمانی که آلارم فعال است، حتی اگر فایل کوچک شده باشد، نوشتن‌ها رد می‌شوند. بعد از defrag موفق روی همه اعضا:

etcdctl --endpoints=https://10.0.0.11:2379,https://10.0.0.12:2379,https://10.0.0.13:2379 \
  alarm disarm

etcdctl --endpoints=https://10.0.0.11:2379,https://10.0.0.12:2379,https://10.0.0.13:2379 \
  alarm list

خروجی alarm list باید خالی باشد (یا memberID:0 alarm:NONE). اگر بعد از disarm دوباره بلافاصله آلارم برگشت، یعنی حجم فایل هنوز بالای فضای آن است و defrag انجام نشده یا ناقص مانده.

در مواردی که compaction غیرفعال بوده (مثلاً etcd مستقل خارج از Kubernetes)، قبل از defrag باید compact کنید:

EP=https://127.0.0.1:2379
REV=$(etcdctl --endpoints=$EP endpoint status -w json | jq -r '.[0].Status.header.revision')
etcdctl --endpoints=$EP compact "$REV"

گام ۴: فضای etcd و compaction را درست تنظیم کنید

اگر بعد از پاک‌سازی، رشد دوباره سریع است، فضای آن را بالا ببرید. سقف توصیه‌شده ۸ گیبی‌بایت است؛ بیشتر از این، هم defrag را طولانی می‌کند و هم بازیابی snapshot را.

در کلاستر kubeadm، مانیفست /etc/kubernetes/manifests/etcd.yaml را ویرایش کنید و فلگ‌ها را به command اضافه کنید:

spec:
  containers:
  - name: etcd
    command:
    - etcd
    - --quota-backend-bytes=4294967296
    - --auto-compaction-mode=periodic
    - --auto-compaction-retention=1h
    # ... سایر فلگ‌های موجود را دست نزنید

برای محیط‌هایی که با kubeadm از فایل پیکربندی نصب شده‌اند، معادل آن در ClusterConfiguration این‌طور است:

kind: ClusterConfiguration
etcd:
  local:
    extraArgs:
      quota-backend-bytes: '4294967296'
      auto-compaction-mode: periodic
      auto-compaction-retention: '1h'

تغییر مانیفست static pod باعث restart خودکار etcd می‌شود؛ اعضا را یکی‌یکی تغییر دهید، نه همه با هم. مقدار auto-compaction-retention در نسخه‌های قدیمی‌تر پیش‌فرض صفر (غیرفعال) است، پس صریح تنظیمش کنید.

بررسی اینکه مشکل واقعاً حل شده است

صرفاً دیدن پیام خالی در ترمینال کافی نیست. این چهار بررسی را انجام دهید:

  1. تست نوشتن واقعی: kubectl create configmap etcd-write-test --from-literal=a=b -n default و بعد حذفش. اگر بدون خطا انجام شد، مسیر نوشتن باز است.

  2. حجم دیتابیس: دوباره endpoint status --write-out=table بگیرید؛ DB SIZE باید به‌طور محسوس (معمولاً ۳۰ تا ۷۰ درصد) کاهش یافته باشد. اگر کاهش نداشت، defrag روی آن عضو اجرا نشده است.

  3. لاگ apiserver: شمارش خطا باید صفر شود: kubectl -n kube-system logs kube-apiserver-cp1 | grep -c "database space exceeded". اگر عدد بزرگ است، لاگ قدیمی است؛ از --since=10m استفاده کنید تا فقط بازه بعد از رفع مشکل دیده شود.

  4. سلامت کنترل‌پلین: kubectl get --raw='/readyz?verbose' باید همه چک‌ها را ok نشان دهد و kubectl -n kube-system get lease kube-controller-manager -o yaml باید مرتب به‌روزرسانی شود.

برای مقایسه، وضعیت مورد انتظار بعد از رفع مشکل:

نشانه

قبل از رفع

بعد از رفع

alarm list

NOSPACE

خالی

DB SIZE

~ فضا

کاهش محسوس

kubectl apply

database space exceeded

موفق

leader election

restart مکرر

پایدار

پیشگیری و بهترین روش‌ها

  • آلارم روی نسبت مصرف فضای آن بگذارید، نه روی حجم مطلق. آستانه ۷۰ درصد نقطه خوبی است:

    (etcd_mvcc_db_total_size_in_bytes / etcd_server_quota_backend_bytes) > 0.7

    در etcd 3.5 به بعد، متریک حجم دیتابیس با نام etcd_db_total_size_in_bytes هم منتشر می‌شود؛ در کلاستر خودتان چک کنید کدام نام فعال است.

  • نسبت fragmentation را رصد کنید: اگر حجم فایل چند برابر حجم in-use شد (نسبت بزرگ‌تر از ۳)، زمان defrag دوره‌ای رسیده است.

  • churn را کم کنید: برای همه Jobها ttlSecondsAfterFinished بگذارید، از ثبت رویدادهای پرحجم پرهیز کنید، و Secret/ConfigMapهای بزرگ را در جای لازم نگه ندارید.

  • defrag را زمان‌بندی کنید: در کلاسترهای پرترافیک، یک CronJob یا playbook ماهانه که اعضا را یکی‌یکی defrag می‌کند، از وقوع دوباره این حادثه جلوگیری می‌کند. هیچ‌وقت همه اعضا را همزمان defrag نکنید.

  • روی دیسک و fsync سرمایه‌گذاری کنید: etcd به تأخیر دیسک حساس است؛ NVMe با IOPS پایدار، هم defrag را سریع می‌کند هم احتمال از دست دادن leader را کاهش می‌دهد.

  • سقف ۸ گیگابایت را جدی بگیرید: بالا بردن quota راه‌حل موقت است، نه جایگزین کنترل churn.

جمع‌بندی کوتاه: «database space exceeded» یعنی فایل backend از فضای آن گذشته و آلارم NOSPACE فعال است. مسیر درست، بکاپ → defrag عضو به عضو → disarm آلارم → تنظیم quota و compaction است؛ و مسیر اشتباه، فقط بالا بردن quota بدون کوچک کردن فایل.

اشتراک‌گذاری:

نویسنده

تحریریه کلودیکپ

تیم محتوای کلودیکپ

تیم فنی و محتوای کلودیکپ؛ مهندسانی که هر روز با DevOps، Kubernetes و زیرساخت ابری کار می‌کنند و تجربه‌هایشان را اینجا می‌نویسند.

سوالات متداول

سوالات متداول این مقاله

compact نسخه‌های قدیمی کلیدها را از نظر منطقی حذف می‌کند و فضای in-use را کم می‌کند، اما فایل backend کوچک نمی‌شود. defrag فایل را بازنویسی می‌کند و صفحات آزاد را به فایل‌سیستم برمی‌گرداند. برای عبور از فضای آن ، defrag الزامی است.

در کلاستر سه‌عضوی لازم نیست، به شرطی که اعضا را یکی‌یکی و از followerها شروع کنید تا quorum حفظ شود. در کلاستر تک‌عضوی یا عضوهای دو‌تایی، قطعی کوتاه اجتناب‌ناپذیر است، چون defrag آن عضو را blocking می‌کند.

احتمالاً آلارم NOSPACE را disarm نکرده‌اید. تا وقتی آلارم فعال است، etcd همه نوشتن‌ها را رد می‌کند حتی اگر فایل کوچک شده باشد. اگر بعد از disarm هم بلافاصله آلارم برگشت، defrag روی همه اعضا کامل نشده یا حجم فایل هنوز بالای فضای آن است.

ادامه مطالعه

مقالات مرتبط

خطای fsnotify watcher در Kubernetes؛ سقف inotify نود
Kubernetes و کانتینرمتوسط

خطای fsnotify watcher در Kubernetes؛ سقف inotify نود

اگر پادهای Grafana، Prometheus یا هر اپلیکیشنی که از inotify استفاده می‌کند با خطای «failed to create fsnotify watcher: too many open files» کرش می‌کنند، مقصر سقف پیش‌فرض هسته لینوکس روی نود است.

تحریریه کلودیکپ ۷ دقیقه مطالعه
تأخیر ۵ ثانیه‌ای DNS در Kubernetes؛ پای ndots:5 در میان است
Kubernetes و کانتینرمتوسط

تأخیر ۵ ثانیه‌ای DNS در Kubernetes؛ پای ndots:5 در میان است

هرچند وقت یک‌بار یک درخواست خروجی دقیقاً ۵ ثانیه طول می‌کشد و بعد موفق می‌شود؟ ریشه معمولاً گزینه ndots:5 در resolv.conf پادها و لیست search است. این مقاله علت، تشخیص و راه‌حل کامل را بررسی می‌کند.

تحریریه کلودیکپ ۷ دقیقه مطالعه
پاد Kubernetes در وضعیت Terminating گیر کرده؛ آزادسازی امن
Kubernetes و کانتینرمتوسط

پاد Kubernetes در وضعیت Terminating گیر کرده؛ آزادسازی امن

پاد در حالت Terminating گیر کرده و با kubectl delete پاک نمی‌شود؟ این مقاله علت واقعی (finalizer باقی‌مانده، نود NotReady، ولوم گیرکرده) و راه‌حل گام‌به‌گام امن را نشان می‌دهد.

تحریریه کلودیکپ ۶ دقیقه مطالعه

در پیاده‌سازی به کمک نیاز دارید؟

تیم کلودیکپ همین کار را هر روز برای تیم‌های دیگر انجام می‌دهد. اگر جایی گیر کرده‌اید، با ما صحبت کنید.