هندسة الفوضى والمرونة

تجارب الفوضى: البنية التحتية

18 دقيقة الدرس 4 من 27

تجارب الفوضى: البنية التحتية

تختبر تجارب فوضى البنية التحتية أنماط الفشل الأكثر أهمية على نطاق واسع: الاختفاء المفاجئ للحوسبة، وإشباع مورد ما، وقطع مسار شبكي. هذه ليست حالات حافة نظرية — بل هي السيناريوهات بالضبط التي تسببت في انقطاعات كبرى في AWS وGoogle وكل نظام موزع ضخم. يعلمك هذا الدرس كيف تصمم هذه التجارب وتنفذها وتتعلم منها في الإنتاج دون أن تسبب الحوادث التي تسعى إلى منعها.

التجربة الأولى: إيقاف النسخ والمناطق

أكثر تجارب فوضى البنية التحتية جوهرية هي إنهاء تشغيل نسخة جارية أو إيقاف منطقة توافرية كاملة. هذا يتحقق من افتراض أساسي: يمكن لنظامك أن يفقد وحدة حوسبة دون التأثير على المستخدمين. إن لم يستطع، فأنت اكتشفت نقطة فشل وحيدة قبل أن يفعل ذلك مزود السحابة نيابةً عنك.

في كبرى شركات التقنية، تُجرى عمليات إنهاء النسخ بشكل مستمر. يفعل Chaos Monkey الأصلي لـ Netflix هذا تحديدًا: يُنهي نسخ EC2 عشوائيًا خلال ساعات العمل. المبرر هو أنك إذا أجريت الفوضى فقط في الليل، ستصبح متساهلًا — تحتاج إلى مهندسي الاستجابة لحالات الطوارئ حاضرين عندما يكون النظام تحت الضغط.

تصميم التجربة:

  1. الحالة الثابتة: زمن الاستجابة p99 أقل من 200 مللي ثانية، معدل الخطأ أقل من 0.1%، جميع فحوصات الصحة خضراء.
  2. الفرضية: إنهاء نسخة واحدة في مجموعة Auto Scaling لا يُدهور زمن الاستجابة للمستخدمين أكثر من 250 مللي ثانية p99 لأكثر من 60 ثانية.
  3. نطاق الانفجار: نسخة واحدة (1 من N في المجموعة). لا تُوقف أكثر من 33% من طبقة ما في آنٍ واحد في التشغيل الأول.
  4. التراجع: إعادة تفعيل الطاقة المُنهاة فورًا؛ الاستعادة من لقطة إذا لزم الأمر.

باستخدام AWS Fault Injection Service (FIS)، يمكنك إنهاء نسخة عبر وثيقة تجربة منظمة بدلًا من استدعاء API مباشر. هذا يمنحك مسارات تدقيق وإجراءات تراجع وشروط إيقاف:

# fis-instance-kill.yaml — AWS FIS experiment template description: "Terminate one instance in the api-service ASG" stopConditions: - source: "aws:cloudwatch:alarm" value: "arn:aws:cloudwatch:us-east-1:123456789:alarm/ApiErrorRateHigh" targets: ApiInstance: resourceType: "aws:ec2:instance" resourceTags: Service: "api-service" Env: "production" filters: - path: "State.Name" values: ["running"] selectionMode: "COUNT(1)" actions: TerminateInstance: actionId: "aws:ec2:terminate-instances" targets: Instances: "ApiInstance" roleArn: "arn:aws:iam::123456789:role/FISRole"

كتلة stopConditions حيوية: إذا اشتعل إنذار CloudWatch الخاص بك (بمعنى أن النظام يعاني فعلًا من تدهور)، يوقف FIS التجربة تلقائيًا. هذا هو قاطع الدائرة الآلي — لا تُشغِّل تجارب فوضى بنية تحتية أبدًا بدون شرط إيقاف مرتبط بإنذار SLO حقيقي.

تجارب المنطقة الكاملة هي خطوة أبعد من إيقاف النسخ. محاكاة انقطاع منطقة توافرية تعني حجب كل حركة مرور بين طبقة تطبيقك وشبكات المنطقة المستهدفة. في Kubernetes يتم ذلك بإضافة سياسة شبكة تحظر الخروج إلى نطاقات CIDR للمنطقة الفاشلة:

# kubectl — cordon all nodes in us-east-1b to simulate AZ failure kubectl get nodes -l topology.kubernetes.io/zone=us-east-1b \ -o jsonpath='{.items[*].metadata.name}' \ | tr ' ' '\n' \ | xargs -I{} kubectl cordon {} # Verify pods are rescheduled to healthy zones kubectl get pods -n api-service -o wide --watch # Rollback: uncordon the zone kubectl get nodes -l topology.kubernetes.io/zone=us-east-1b \ -o jsonpath='{.items[*].metadata.name}' \ | tr ' ' '\n' \ | xargs -I{} kubectl uncordon {}
Zone Kill Experiment — AZ failure and traffic redistribution AZ-1 (us-east-1a) api-pod-1 api-pod-2 AZ-2 (us-east-1b) Cordoned / Blackholed (chaos target) AZ-3 (us-east-1c) api-pod-3 api-pod-4 (new) Load Balancer Traffic reroutes to AZ-1 and AZ-3 when AZ-2 is taken offline.
تجربة إيقاف المنطقة — يُعزل AZ-2، يُوجّه موازن الحمل كل الحركة إلى AZ-1 وAZ-3، ويضع الجدوَل بودًا جديدًا في AZ-3.
قيود انتشار الطوبولوجيا هي دفاعك. بدون topologySpreadConstraints في مواصفة البود، يمكن لجدوَل Kubernetes وضع جميع النسخ في منطقة توافرية واحدة، مما يجعل إيقاف المنطقة كارثيًا. وزّع الأحمال دائمًا عبر المناطق قبل تشغيل تجارب إيقاف المنطقة — ستعلمك التجربة ما إذا كان التوزيع يحدث فعلًا.

التجربة الثانية: استنفاد الموارد

تجارب استنفاد الموارد تجيب على السؤال: ماذا يحدث عندما تنفد موارد المضيف من CPU أو ذاكرة أو قرص؟ هذه الإخفاقات خفية لأنها تتدهور تدريجيًا، وتتسبب في تأثيرات متتالية عبر الخدمات على نفس المضيف، وغالبًا ما تُطلق أنماط فشل (قتل OOM، والتخبط، وانزياح الساعة) يستحيل إعادة إنتاجها بأي طريقة أخرى.

استنفاد CPU — الأداة stress-ng هي المعيار الإنتاجي لتوليد حمل CPU محكوم. اقرنها بحد زمني حتى لا تعمل إلى ما لا نهاية:

# Saturate 4 CPU cores for 120 seconds, then stop # --cpu N: spawn N workers each spinning on sqrt() # --timeout: hard stop after this duration stress-ng --cpu 4 --timeout 120s --metrics-brief # In a container: run stress-ng as a sidecar or via kubectl exec kubectl exec -it api-pod-abc123 -n api-service -- \ stress-ng --cpu 2 --timeout 60s # Watch what happens to CPU throttling at the cgroup level kubectl top pod -n api-service --containers cat /sys/fs/cgroup/cpu/cpu.stat # throttled_time field

ضغط الذاكرة — إجبار عملية ما نحو حدود OOM يكشف كيف يتصرف تطبيقك في ظل منافسة الذاكرة. على Linux، يُنهي قاتل OOM في النواة العملية ذات أعلى درجة OOM. فهم أي عملية تُقتل أولًا أمر حيوي للأنظمة التي تحتوي على حاويات متعددة على عقدة:

# Allocate and hold 2 GB of memory for 90 seconds stress-ng --vm 1 --vm-bytes 2G --timeout 90s # Monitor OOM events in real time dmesg -w | grep -i "oom\|killed" # Check OOM score for your process (lower = less likely killed) cat /proc/$(pgrep -n python)/oom_score cat /proc/$(pgrep -n python)/oom_score_adj # In Kubernetes: set oom_score_adj via quality-of-service class # Guaranteed QoS (requests == limits) gets score -997 # BestEffort QoS gets score 1000 — killed first

استنفاد القرص — ملء نظام ملفات هو أبسط طريقة لإحداث تلف صامت في البيانات وإخفاق الكتابة وإخفاقات متتالية في خطوط أنابيب السجلات. شغّل دائمًا تجارب استنفاد القرص في نقطة تحميل tmpfs، لا على وحدة التخزين الجذرية أو وحدة البيانات:

# Create a 5 GB sparse file to fill a test filesystem # NEVER run this on / or a data volume — use a dedicated test mount fallocate -l 5G /mnt/chaos-test/fill.dat # Monitor disk usage during the experiment watch -n 2 df -h /mnt/chaos-test # Verify application behavior: does it log the error and degrade gracefully, # or does it crash silently? tail -f /var/log/app/app.log | grep -i "disk\|space\|write\|enospc" # Cleanup rm /mnt/chaos-test/fill.dat
استنفاد القرص في الحاويات خادع. يستخدم Docker وcontainerd نظام overlayfs. الحاوية التي تكتب على نظام ملفاتها الخاص تملأ قسم الجذر في المضيف، لا وحدة تخزين معزولة. دائمًا احرص على تحميل tmpfs أو PVC مخصص لتجارب فوضى القرص. ملء قسم الجذر لعقدة إنتاجية سيُعطل kubelet ويتسبب في عمليات إخلاء متتالية عبر العقدة.

التجربة الثالثة: تقسيمات الشبكة

تقسيمات الشبكة هي أخطر تجارب البنية التحتية وأكثرها كشفًا. تكشف حالات الدماغ المنقسم، والحالة غير المتسقة، وعواصف إعادة المحاولة، وأخطاء ضبط المهل الزمنية التي لا يمكن لأي اختبار وحدة أو تكامل اكتشافها. يصبح مبرهنة CAP حقيقيًا بشكل ملموس عندما تُقسّم مجموعة قاعدة بيانات في الإنتاج وتلاحظ ما إذا كان تطبيقك يختار الاتساق أم التوافرية.

الأداة المعيارية لحقن أعطال الشبكة على Linux هي tc (التحكم في حركة المرور) مع نظام netem. يمكن لـ tc netem حقن الكمون وفقدان الحزم والتكرار والتلف والثقوب السوداء الكاملة بدقة نانو ثانية:

# Add 200ms latency + 50ms jitter to all outbound traffic on eth0 tc qdisc add dev eth0 root netem delay 200ms 50ms distribution normal # Simulate 15% packet loss (models lossy link behavior) tc qdisc add dev eth0 root netem loss 15% # Simulate a complete network blackhole to a specific service IP # (simulates a database becoming unreachable) tc qdisc add dev eth0 root handle 1: prio tc filter add dev eth0 parent 1:0 protocol ip u32 \ match ip dst 10.0.1.45/32 \ action drop # Show current qdisc rules tc qdisc show dev eth0 # ALWAYS clean up when done tc qdisc del dev eth0 root

على مستوى Kubernetes، استخدم NetworkPolicy لعزل بود أو نطاق اسم كامل عن اعتماده الأعلى. هذا أأمن من tc لأنه تصريحي وقابل للتراجع الكامل بمجرد حذف كائن السياسة:

# Partition: deny all ingress to the cache layer from api-service # Simulates "api-service cannot reach Redis" apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: chaos-partition-cache namespace: production spec: podSelector: matchLabels: app: redis-cache policyTypes: - Ingress ingress: - from: - podSelector: matchLabels: chaos-exempt: "true" # Only chaos-exempt pods (e.g., monitoring) can reach cache # api-service pods are now partitioned from Redis
راقب عواصف إعادة المحاولة خلال التعافي من التقسيم. عندما يُشفى تقسيم الشبكة، يُعيد كل عميل كان ينتهي مهلته المحاولة في آنٍ واحد. بدون تراجع أسي وتشويش، يخلق هذا قطيعًا مدويًا يُغرق الخدمة المتعافية. تجارب الفوضى هي الطريقة الوحيدة الموثوقة للتحقق من أن منطق إعادة المحاولة لديك يحتوي فعلًا على تشويش مناسب. استخدم kubectl logs -f وراقب مقاييسك بحثًا عن "ارتفاع التعافي" — إذا ارتفع p99 فوق الحالة الثابتة أثناء التعافي، فإن منطق إعادة المحاولة يحتاج إلى إصلاح.

قياس نتائج التجارب

يجب أن ترتبط كل تجربة فوضى بنية تحتية بإشارة حالة ثابتة قابلة للقياس. بدون القياس، لا تُجري تجربة — بل تكسر الأشياء فحسب. المجموعة الدنيا من المقاييس اللازمة لأي تجربة بنية تحتية:

  • معدل الخطأ: نسبة الطلبات التي تُرجع 5xx. يجب تحديد خط الأساس قبل التجربة.
  • مئينيات زمن الاستجابة: p50 وp95 وp99. غالبًا تُظهر إيقافات النسخ ارتفاعات مقبولة في زمن الاستجابة خلال الإخفاق؛ يجب أن تُظهر إيقافات المنطقة تعافيًا كاملًا ضمن نافذة SLO.
  • الإشباع: وقت تقييد CPU، معدل أخطاء صفحات الذاكرة، انتظار إدخال/إخراج القرص. هذه تكشف ما إذا كان استنفاد الموارد قد أثّر على طبقة التطبيق.
  • معدلات إعادة المحاولة والمهل: ارتفاع أعداد إعادة المحاولة دون ارتفاع معدلات الخطأ يعني أن سياسة إعادة المحاولة تعمل. ارتفاع إعادة المحاولات والأخطاء معًا يعني إخفاقًا متتاليًا.
وثّق كل تجربة كـ RFC قبل تشغيلها. وثيقة من صفحة واحدة تذكر الفرضية ونطاق الانفجار وشروط الإيقاف وإجراء التراجع والمدة المتوقعة ليست بيروقراطية — بل هي الحد الأدنى لمعيار إجراء فوضى الإنتاج بمسؤولية. التجارب غير الموثقة التي تتسبب في انقطاعات هي حوادث. التجارب الموثقة التي تتسبب في انقطاعات هي دروس مستفادة.

الخلاصة

تجارب فوضى البنية التحتية — إيقاف النسخ، وإخفاق المناطق، واستنفاد الموارد، وتقسيمات الشبكة — تكشف كل منها فئة مميزة من أنماط الفشل في الإنتاج. تتحقق إيقافات النسخ من تكرارك وضبط الاسترداد التلقائي. تتحقق إيقافات المنطقة من أن توزيع حركة المرور متعدد المناطق فعلًا. تكشف تجارب استنفاد الموارد ما إذا كانت تطبيقاتك تتدهور بأناقة أم تفشل بصمت. تكشف تقسيمات الشبكة عن سلوك المهلة وإعادة المحاولة وقاطع الدائرة الذي لا يمكن لأي اختبار اصطناعي تكراره. شغّلها بترتيب تصاعدي من نطاق الانفجار، دائمًا مع شرط إيقاف، دائمًا مع فرضية موثقة، ودائمًا مع مقاييس مفتوحة على شاشتك الثانية.