FinOps وتحسين تكاليف السحابة

إدارة تكاليف Kubernetes

18 دقيقة الدرس 7 من 26

إدارة تكاليف Kubernetes

حلّ Kubernetes مشكلة جدولة الموارد بأناقة، لكنه لم يوفر أدوات محاسبة للتكاليف تقريبًا. المجموعة (cluster) هي تجمّع مشترك من الحوسبة والذاكرة والشبكة، تستهلكه مئات الـ pods عبر عشرات الـ namespaces، في حين يصل الفاتورة السحابية كبند واحد فقط: node pool. استرداد تكلفة كل فريق وكل خدمة من هذا التجمّع المشترك هو التحدي الأساسي في FinOps على Kubernetes، وعلى نطاق المنظمات الكبيرة يحدد ما إذا كانت القيادة الهندسية قادرة على اتخاذ قرارات استثمارية رشيدة أم تعمل في الظلام.

لماذا تكاليف Kubernetes صعبة القياس؟

ثلاث خصائص هيكلية تجعل تكاليف Kubernetes أصعب قياسًا مقارنةً بالبنية القائمة على الأجهزة الافتراضية.

  • تجميع الـ pods يُخفي الملكية. يضع المجدول (scheduler) pods من فرق مختلفة على نفس الـ node. تكلفة الـ node حقيقية، لكن إسنادها لعبء عمل معين يتطلب معرفة كل من الموارد المطلوبة (requested) من كل pod والطاقة الخاملة في الـ node، ثم تحديد من يدفع تلك الطاقة الخاملة.
  • Requests مقابل limits مقابل الاستخدام الفعلي. يمكن لـ pod أن يطلب 2 CPU و4 GiB، ويُحدَّد له سقف 4 CPU و8 GiB، بينما يستهلك فعليًا 0.3 CPU و900 MiB فقط. يجب على أدوات التكلفة أن تقرر أي رقم تستخدم. الفوترة على أساس الـ requests محافظة وقابلة للتنبؤ، أما الفوترة على الاستخدام الفعلي فهي أدق لكن أكثر تقلبًا وأصعب لوضع الميزانية.
  • البنية المشتركة ليس لها مالك واحد. الإضافات المشتركة كـ CoreDNS وkube-proxy وmetrics-server ووحدة التحكم في الدخول (ingress controller) وداخلي الشبكة (CNI daemonset) تستهلك موارد حقيقية لكنها لا يمكن إسنادها لأي فريق. يستنزف هذا العبء عادةً 8–15% من إجمالي طاقة المجموعة ويجب توزيعه على جميع المستخدمين.
معيار الصناعة: تُفيد معظم منصات Kubernetes الناضجة على نطاق 500+ node بنسبة طاقة خاملة تتراوح بين 20–35% في الأحوال العادية، لأن التطبيقات تطلب موارد زائدة تجنبًا لـ OOMKills والتقييد. استرداد نصف هذه الهدر فقط من خلال ضبط الـ requests أفضل ماليًا من أي برنامج خصومات Reserved Instances.

نموذج Namespace كمركز تكلفة للفريق

النهج المعتمد في كبرى شركات التقنية هو تعيين حدود التكلفة على Kubernetes namespaces وتطبيقها بالتسميات (labels). كل فريق يمتلك namespace واحدًا أو أكثر، وكل namespace مُعلَّم بـ team وَenv وَcost-centre عبر MutatingAdmissionWebhook أو سياسات OPA/Kyverno. تُجمَّع التكاليف بعد ذلك حسب namespace وتُرسل أسبوعيًا لقادة الفرق كـ showback، وشهريًا للمالية كـ chargeback.

تجعل حصص الموارد هذا النموذج قابلًا للتشغيل: بدونها، يمكن لـ deployment واحد خاطئ في namespace ما أن يستنزف node pool بالكامل ويحرم الفرق الأخرى من الموارد. كل namespace يشارك في الـ chargeback يجب أن يمتلك ResourceQuota وLimitRange.

# Namespace مع تسميات التكلفة الإلزامية apiVersion: v1 kind: Namespace metadata: name: payments-prod labels: team: payments env: production cost-centre: cc-1042 --- # ResourceQuota: سقف صارم لكل namespace apiVersion: v1 kind: ResourceQuota metadata: name: payments-prod-quota namespace: payments-prod spec: hard: requests.cpu: "40" requests.memory: 80Gi limits.cpu: "80" limits.memory: 160Gi count/pods: "200" --- # LimitRange: قيم requests/limits افتراضية # حتى لا تُجدوَل pods بدون requests واضحة apiVersion: v1 kind: LimitRange metadata: name: payments-prod-defaults namespace: payments-prod spec: limits: - type: Container default: cpu: "500m" memory: 512Mi defaultRequest: cpu: "100m" memory: 128Mi max: cpu: "8" memory: 16Gi

تحسين التعبئة (Bin Packing): تحويل الطاقة الخاملة إلى مدخرات

الفجوة بين ما توفره الـ nodes وما تطلبه الـ pods تُسمى cluster slack. على 1,000 node بتكلفة $0.50/ساعة، تكلّف نسبة 25% خمولًا $3,000 يوميًا. ثلاثة رافعات لتقليلها.

1. Vertical Pod Autoscaler (VPA) في وضع التوصية. شغّل VPA في وضع Off أولًا — يُصدر توصيات دون تطبيقها — لمراجعة دقة الـ requests قبل تفعيل التحديثات التلقائية. بعد 7 أيام من البيانات، رتّب الأعباء حسب نسبة CPU المطلوبة إلى CPU الموصى بها؛ الأعباء التي تتجاوز نسبتها 4× هي أعلى أولويات للضبط.

# VPA في وضع التوصية فقط (آمن للإنتاج) apiVersion: autoscaling.k8s.io/v1 kind: VerticalPodAutoscaler metadata: name: api-gateway-vpa namespace: platform-prod spec: targetRef: apiVersion: apps/v1 kind: Deployment name: api-gateway updatePolicy: updateMode: "Off" # توصية فقط؛ لا إخلاء للـ pods # لقراءة التوصيات بعد أسبوع: # kubectl get vpa api-gateway-vpa -n platform-prod -o json \ # | jq '.status.recommendation.containerRecommendations[] # | {container: .containerName, # lowerBound: .lowerBound, # target: .target, # upperBound: .upperBound}'

2. دمج الـ nodes عبر Karpenter. سياسة disruption.consolidationPolicy: WhenUnderutilized في Karpenter تُفرّغ الـ nodes قليلة الاستخدام وتُعيد تعبئة الأعباء على عدد أقل من الـ nodes الأكثر امتلاءً. تحسّن هذه الآلية كفاءة التعبئة عادةً من 55–65% إلى 75–85%.

3. اختيار عائلة الـ instances. تتيح NodePool في Karpenter تحديد قائمة بعائلات الـ instances بترتيب الأولوية. دمج m7i وَm7g (Graviton) وَc7g يسمح للمجدول باختيار الـ instance الأرخص المناسبة. عادةً ما تكون Graviton أرخص بـ 20% لكل vCPU، لكن تحقق من معاياراتك الخاصة.

قابلية الرؤية على نمط Kubecost

يعمل كل من Kubecost وتقنيته مفتوحة المصدر OpenCost داخل المجموعة، ويُحدّثان باستمرار نموذج تكلفة كل pod وَdeployment وَnamespace عبر دمج مقاييس Prometheus مع واجهات برمجة أسعار مزودي السحابة. يمنحك ذلك إسنادًا للتكلفة بدقة دون الساعة دون الحاجة لكتابة أي أدوات مخصصة.

Kubecost / OpenCost cost attribution pipeline Prometheus CPU / Mem metrics kube-state Pod / Node labels Cloud Pricing API On-demand / Spot rates CUR / Billing Actual spend OpenCost Engine Cost model: requests × node rate + idle allocation Namespace View per-team cost Workload View per-deployment Efficiency Report idle / waste % API / Export Grafana / Slack alerts
خط أنابيب إسناد التكلفة في OpenCost: تغذّي مقاييس Prometheus وواجهات أسعار السحابة نموذج التكلفة الذي يكشف عن عرض الـ namespaces والأعباء وتقارير الكفاءة.

نموذج التكلفة الأساسي هو: تكلفة الـ pod = (CPU المطلوبة / CPU الـ node) × السعر الساعي للـ node × ساعات التشغيل، مضافًا إليها نفس الحساب للذاكرة. يتوزع الخمول في الـ node على جميع الـ pods بنسبة requests كل منها، لذا الفرق الذي يطلب موارد زائدة يدفع لهدره بدلًا من توزيعه على الجيران.

# تثبيت OpenCost (Helm) بجانب Prometheus helm repo add opencost https://opencost.github.io/opencost-helm-chart helm repo update helm install opencost opencost/opencost \ --namespace opencost \ --create-namespace \ --set opencost.exporter.cloudProviderApiKey="$(cat /path/to/aws-billing-key)" \ --set opencost.prometheus.external.enabled=true \ --set opencost.prometheus.external.url="http://prometheus-operated.monitoring:9090" # استعلام التكلفة حسب namespace للأيام السبعة الماضية عبر OpenCost API kubectl port-forward svc/opencost 9003 -n opencost & curl -s "http://localhost:9003/allocation/compute?window=7d&aggregate=namespace&accumulate=false" \ | jq '.data[0] | to_entries[] | {namespace: .key, cpuCost: (.value.cpuCost | . * 100 | round / 100), memoryCost: (.value.memoryCost | . * 100 | round / 100), totalCost: (.value.totalCost | . * 100 | round / 100), efficiency: (.value.totalEfficiency | . * 100 | round / 100)}'

الضبط التلقائي على نطاق واسع: حلقة التغذية الراجعة الآلية

الضبط اليدوي لا يتوسع بعد 50 خدمة. النمط الإنتاجي هو خط أنابيب أسبوعي آلي: تُجمَع توصيات VPA وتُصفَّى للتأكد من أهميتها الإحصائية (عبء العمل يحتاج 7 أيام من البيانات وتباين أقل من 40%)، ثم تُقدَّم كـ pull requests على ملف Helm values الخاص بالفريق تُظهر القيم الحالية والموصى بها جنبًا إلى جنب. يُوافَق على الـ PR تلقائيًا إذا خفّض الـ requests بأكثر من 20% وكانت الخدمة تملك HPA (لكي تتوسع أفقيًا عند الحاجة).

النسب الذهبية لـ pods الإنتاج: اضبط CPU requests عند p95 الاستخدام الفعلي، وCPU limits عند 2–3× الـ requests (CPU قابل للضغط — التقييد آمن). اضبط memory requests عند p99 الاستخدام الفعلي مضافًا إليه 20% هامش، وmemory limits مساوية لـ requests (الذاكرة غير قابلة للضغط — OOMKill أفضل من إخلاء الـ node). هذا التوليف يقلل الهدر مع الحفاظ على أنماط فشل يمكن التنبؤ بها.

مجموعة مشتركة أم مجموعات مخصصة: الحسابات المعمارية

المجموعة المشتركة متعددة المستأجرين تحقق أفضل كفاءة تعبئة — العبء العام يتوزع، الـ nodes أكثر امتلاءً، ويمكن لـ Karpenter إعادة التعبئة عبر جميع الأعباء. أما مجموعة مخصصة لكل فريق فتقضي على مشكلة الجار الصاخب وتبسّط إسناد التكلفة (فاتورة واحدة = فريق واحد) وتتيح جداول ترقية مستقلة، لكنها تضاعف العبء العام: كل مجموعة تحتاج control plane ومضافات وفريق تشغيل.

الإجماع الصناعي في معظم الشركات هو نموذج متدرج: مجموعة منصة مشتركة واحدة لكل بيئة (dev/staging/prod) لغالبية الأعباء، مع مجموعات مخصصة اختيارية للأعباء ذات متطلبات الامتثال الصارمة أو العزل الشبكي أو الأجهزة المتخصصة. يحقق هذا النهج نحو 80% من فائدة التعبئة مع احتواء مخاطر الـ 20% الباقية.

فخ Namespace كمركز تكلفة: تتماشى الـ namespaces مع الفرق بشكل نظري، لكن في الواقع تنتهي كثير من المنظمات بمئات الـ namespaces — واحد لكل microservice أو بيئة PR أو تجربة. يتطلب تقرير التكلفة حينها تجميعًا ثانيًا (namespace → فريق → مركز تكلفة)، ويجب الحفاظ على هذا التعيين. ابنِه بشكل صريح من اليوم الأول: ConfigMap أو إدخال CMDB يربط الـ namespace بالفريق هو بنية تحتية إلزامية لأي برنامج FinOps على نطاق واسع.

إبراز التكلفة في سير عمل المطور

التغيير السلوكي الأكثر فاعلية هو جعل التكلفة مرئية في لحظة اتخاذ المطور لقرار الحجم، لا بعد وصول الفاتورة. ثلاث نقاط دمج جوهرية:

  • تقديرات تكلفة في CI. خطوة في خط نشر (بعد helm-diff) تستدعي OpenCost API لتوقع التكلفة الشهرية لمواصفات deployment الجديدة وتنشرها كتعليق على الـ PR: "سيكلف هذا الـ deployment حوالي $1,240/شهر (+18% مقارنةً بالحالي). زاد طلب CPU من 500m إلى 800m عبر 10 replicas." المهندسون لا يتصرفون حيال ما لا يرونه.
  • ملخص أسبوعي على Slack. رسالة تلقائية كل إثنين لـ channel كل فريق: أعلى 5 أعباء من حيث التكلفة، نسبة الكفاءة، التغيير أسبوعًا على أسبوع، ورابط لوحة Kubecost. الفرق التي ترى تكاليفها ترتفع دون مبرر تجاري تحقق؛ التي لا ترى لا تحقق.
  • لوحة تكلفة Grafana على كل dashboard خدمة. لوحة Grafana قياسية (باستخدام مقاييس Prometheus من OpenCost) تعرض التكلفة اليومية ونسبة الكفاءة بجانب زمن الاستجابة ومعدل الأخطاء. تصبح التكلفة إشارة تشغيلية من الدرجة الأولى، لا مخرجًا ماليًا.

إدارة تكاليف Kubernetes ليست شراء أداة — بل هي تغيير في نموذج التشغيل. الأدوات (OpenCost، VPA، Karpenter) ناضجة ومجانية في معظمها. العمل الحقيقي هو تصنيف التسميات، وتطبيق الحصص، والتحول الثقافي الذي يجعل كل فريق مسؤولًا عن إنفاقه السحابي.