0Pricing
DevOps Bootcamp · درس

السكربتات القابلة للتكرار ومنطق إعادة المحاولة بالتراجع التدريجي

صمّم عمليات آمنة لإعادة تشغيلها وأضف تراجعًا أُسّيًا عند التعامل مع الاستدعاءات الخارجية غير المستقرة

السكربتات القابلة للتكرار ومنطق إعادة المحاولة بالتراجع التدريجي درس مجاني في DevOps Bootcamp على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DevOps Bootcamp، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DevOps Bootcamp 4 دروس في المجموع.

ما هي قابلية التكرار ولماذا تهم

تعني قابلية التكرار أن يؤدي تشغيل العملية نفسها عدة مرات إلى النتيجة نفسها التي يحققها تشغيلها مرة واحدة. وهذا أمر بالغ الأهمية في برامج Bash النصية، لأن البرامج قد تتعطل، وقد تنقطع الشبكات، وقد يعيد الأشخاص تشغيل الأوامر عن طريق الخطأ.

  • قد يفشل البرنامج النصي غير القابل للتكرار الذي ينشئ مستخدمًا مرتين أو ينشئ بيانات مكررة.
  • يتحقق البرنامج النصي القابل للتكرار أولًا: هل هذا موجود بالفعل؟
  • من الآمن استخدام البرامج النصية القابلة للتكرار في مهام Cron، ومسارات CI، وحلقات إعادة المحاولة.

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

حماية إنشاء الملفات والأدلة

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

  • [ -d dir ] — تكون صحيحة إذا كان الدليل موجودًا
  • [ -f file ] — تكون صحيحة إذا كان الملف العادي موجودًا
  • mkdir -p — ينشئ الدليل فقط إذا لم يكن موجودًا (قابلية تكرار مدمجة)

فضّل الخيارات المدمجة مثل -p و--no-clobber على عمليات التحقق اليدوية عندما تكون متاحة، لأنها ذرية وآمنة من حالات السباق.

#!/usr/bin/env bash
set -euo pipefail

CONFIG_DIR="$HOME/.myapp"
CONFIG_FILE="$CONFIG_DIR/config.ini"

# Idempotent: mkdir -p never fails if dir already exists
mkdir -p "$CONFIG_DIR"

# Idempotent: only write config if it doesn't exist yet
if [ ! -f "$CONFIG_FILE" ]; then
  echo '[defaults]' > "$CONFIG_FILE"
  echo 'timeout=30' >> "$CONFIG_FILE"
  echo "Created $CONFIG_FILE"
else
  echo "Config already exists, skipping."
fi

إدارة المستخدمين والمجموعات القابلة للتكرار

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

  • يعيد id username القيمة 0 إذا كان المستخدم موجودًا
  • يتحقق getent group groupname من وجود مجموعة
  • غلّف كل عملية بشرط حارس حتى يكون البرنامج النصي آمنًا لإعادة التشغيل

هذا النمط هو أساس أدوات إدارة الإعدادات مثل Ansible — فكل مهمة عملية قابلة للتكرار محمية بشرط.

#!/usr/bin/env bash
set -euo pipefail

APP_USER="apprunner"
APP_GROUP="appgroup"

# Idempotent group creation
if ! getent group "$APP_GROUP" &>/dev/null; then
  groupadd "$APP_GROUP"
  echo "Group '$APP_GROUP' created."
else
  echo "Group '$APP_GROUP' already exists."
fi

# Idempotent user creation
if ! id "$APP_USER" &>/dev/null; then
  useradd -m -g "$APP_GROUP" -s /bin/bash "$APP_USER"
  echo "User '$APP_USER' created."
else
  echo "User '$APP_USER' already exists."
fi

استخدام ملفات القفل لمنع التشغيل المتزامن

حتى البرنامج النصي القابل للتكرار قد يسبب مشكلات إذا شُغِّل مثيلان منه في الوقت نفسه. يضمن ملف القفل تشغيل مثيل واحد فقط في كل مرة.

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

من دون قفل، قد تتصادم مهمة Cron بطيئة مع إعادة تشغيل يدوية، مما يؤدي إلى إفساد الحالة المشتركة.

#!/usr/bin/env bash
set -euo pipefail

LOCKFILE="/tmp/myapp_deploy.lock"

# Atomic lock acquisition using mkdir
if ! mkdir "$LOCKFILE" 2>/dev/null; then
  echo "ERROR: Another instance is running (lock: $LOCKFILE). Exiting." >&2
  exit 1
fi

# Guarantee lock removal on any exit
trap 'rmdir "$LOCKFILE"; echo "Lock released."' EXIT

echo "Lock acquired. Running deployment..."
sleep 2   # simulate work
echo "Deployment complete."

تتبع الخطوات المكتملة باستخدام ملف حالة

بالنسبة إلى البرامج النصية متعددة الخطوات (مثل عمليات الترحيل والتثبيت والتجهيز)، يمكنك تتبع الخطوات التي اكتملت بالفعل باستخدام ملف حالة. تتحقق كل خطوة من ملف الحالة قبل التنفيذ، وتكتب فيه عند الانتهاء.

  • حل قليل التكلفة وقابل للنقل — ولا يتطلب قاعدة بيانات.
  • يتيح للبرنامج النصي الفاشل المتابعة من الموضع الذي توقف فيه.
  • خزّن الحالة في موقع يمكن توقعه مثل /var/lib/myapp/ أو ~/.myapp/state/.

يُستخدم هذا النمط في أدوات رئيسية مثل apt وcloud-init وأطر عمل ترحيل قواعد البيانات.

#!/usr/bin/env bash
set -euo pipefail

STATE_DIR="/tmp/myapp_state"
mkdir -p "$STATE_DIR"

run_step() {
  local step_name="$1"
  local step_cmd="$2"
  local marker="$STATE_DIR/${step_name}.done"

  if [ -f "$marker" ]; then
    echo "[SKIP] $step_name already completed."
    return 0
  fi

  echo "[RUN]  $step_name ..."
  eval "$step_cmd"
  touch "$marker"
  echo "[DONE] $step_name"
}

run_step "install_deps"   "echo 'Installing dependencies...'"
run_step "configure_db"   "echo 'Configuring database...'"
run_step "start_service"  "echo 'Starting service...'"

مقدمة إلى منطق إعادة المحاولة

تتسم الاستدعاءات الخارجية — مثل طلبات HTTP واستعلامات DNS واستدعاءات واجهات cloud API — بعدم الموثوقية بطبيعتها. ولا ينبغي لفشل واحد أن ينهي البرنامج النصي بالكامل. يعيد منطق إعادة المحاولة محاولة تنفيذ الأمر الفاشل تلقائيًا.

  • إعادة المحاولة الساذجة: تكرار التنفيذ N مرة حتى ينجح الأمر.
  • حدّد دائمًا عددًا أقصى للمحاولات لتجنب الحلقات اللانهائية.
  • سجّل كل محاولة حتى يمكن تشخيص حالات الفشل.

تغلف أبسط دالة لإعادة المحاولة أي أمر، وتعيد محاولة تنفيذه عددًا محددًا من المرات مع تأخير ثابت. وهذا كافٍ للعديد من حالات الاستخدام، لكنه يعاني خللًا خطيرًا تحت الحمل — وسنتناوله في القسم التالي.

#!/usr/bin/env bash
set -euo pipefail

# Simple fixed-delay retry (3 attempts, 2s apart)
retry() {
  local max_attempts=3
  local delay=2
  local attempt=1

  until "$@"; do
    if (( attempt >= max_attempts )); then
      echo "ERROR: Command failed after $max_attempts attempts: $*" >&2
      return 1
    fi
    echo "Attempt $attempt failed. Retrying in ${delay}s..." >&2
    sleep "$delay"
    (( attempt++ ))
  done
}

# Example: retry a curl call
retry curl --silent --fail --max-time 5 https://httpbin.org/get -o /dev/null
echo "Request succeeded."

مشكلة القطيع الهادر

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

  • يعيد 100 برنامج نصي المحاولة كل 5 ثوانٍ ← 100 طلب متزامن كل 5 ثوانٍ.
  • يكون الخادم مثقلًا بالفعل؛ ويزيد الحمل المتزامن الوضع سوءًا.
  • الحل هو التراجع الأسي: مضاعفة مدة الانتظار بعد كل فشل.
  • أضف التشويش العشوائي (ضوضاء عشوائية) لإلغاء تزامن عمليات إعادة المحاولة بين العملاء.

يُعد التراجع الأسي مع التشويش العشوائي المعيار المتبع في الصناعة، وتستخدمه حزم AWS SDK وعملاء Google Cloud وكل نظام موزع رئيسي.

تنفيذ التراجع الأسي

يزيد التراجع الأسي مدة الانتظار أُسّيًا بعد كل فشل: 1s، 2s، 4s، 8s، 16s... وهذا يمنح النظام البعيد وقتًا للتعافي، مع تقليل الحمل الإجمالي.

الصيغة: delay = base * (2 ^ attempt)

  • حدّد سقفًا (أقصى مدة انتظار) حتى لا تزداد فترات الانتظار بلا حدود.
  • المعلمات التي يمكن ضبطها: base_delay وmax_delay وmax_attempts.

هذه الدالة قابلة لإعادة الاستخدام — مرّر إليها أي أمر باعتباره وسيطًا.

#!/usr/bin/env bash
set -euo pipefail

retry_with_backoff() {
  local max_attempts="${RETRY_MAX_ATTEMPTS:-5}"
  local base_delay="${RETRY_BASE_DELAY:-1}"
  local max_delay="${RETRY_MAX_DELAY:-30}"
  local attempt=0
  local delay="$base_delay"

  until "$@"; do
    (( attempt++ ))
    if (( attempt >= max_attempts )); then
      echo "ERROR: '$*' failed after $max_attempts attempts." >&2
      return 1
    fi
    echo "Attempt $attempt failed. Backing off for ${delay}s..." >&2
    sleep "$delay"
    # Double the delay, but cap it
    delay=$(( delay * 2 ))
    (( delay > max_delay )) && delay=$max_delay
  done

  echo "Command succeeded on attempt $(( attempt + 1 ))."
}

retry_with_backoff echo "Simulated success"

إضافة Jitter إلى التراجع

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

استراتيجيتان شائعتان لـ Jitter:

  • Full jitter: sleep random(0, cap) — أكبر قدر من التوزيع وأقل حمل ذروي.
  • Equal jitter: sleep cap/2 + random(0, cap/2) — يضمن حدًا أدنى للانتظار، ويمنع إعادة المحاولة المكثفة فورًا.

في Bash، استخدم $RANDOM (من 0 إلى 32767) لتوليد أرقام عشوائية. واضبطها لتناسب نطاق مدة الانتظار باستخدام العمليات الحسابية مع باقي القسمة.

#!/usr/bin/env bash
set -euo pipefail

retry_with_jitter() {
  local max_attempts="${1:-5}"; shift
  local base_delay=1
  local max_delay=32
  local attempt=0
  local cap=$base_delay

  until "$@"; do
    (( attempt++ ))
    if (( attempt >= max_attempts )); then
      echo "ERROR: Giving up after $max_attempts attempts." >&2
      return 1
    fi

    # Full jitter: random value in [0, cap]
    local jitter=$(( RANDOM % (cap + 1) ))
    echo "Attempt $attempt failed. Sleeping ${jitter}s (cap=${cap}s)..." >&2
    sleep "$jitter"

    # Grow cap exponentially, bounded by max_delay
    cap=$(( cap * 2 ))
    (( cap > max_delay )) && cap=$max_delay
  done
}

retry_with_jitter 4 curl --silent --fail --max-time 3 https://httpbin.org/get -o /dev/null
echo "Done."

دمج Idempotency وإعادة المحاولة في سير عمل فعلي

في النصوص البرمجية المستخدمة في بيئة الإنتاج، تعمل خاصية idempotency ومنطق إعادة المحاولة معًا. قد يتضمن سير عمل نموذجي للنشر ما يلي:

  1. الحصول على قفل (لمنع عمليات التشغيل المتزامنة)
  2. فحص ملفات الحالة (لتخطي الخطوات المكتملة)
  3. استخدام إعادة المحاولة مع التراجع للاستدعاءات الخارجية (التنزيل وواجهة API وDNS)
  4. وضع علامة على الخطوات كمكتملة فقط بعد تأكيد نجاحها
  5. تحرير القفل عبر trap

يجعل هذا الدمج النصوص البرمجية آمنة لإعادة التشغيل في أي وقت — بعد تعطل أو انتهاء مهلة أو إيقاف يدوي — من دون ترك النظام في حالة معطلة.

#!/usr/bin/env bash
set -euo pipefail

STATE_DIR="/tmp/deploy_state" && mkdir -p "$STATE_DIR"
LOCK="/tmp/deploy.lock"

mkdir "$LOCK" 2>/dev/null || { echo "Already running."; exit 1; }
trap 'rmdir "$LOCK"' EXIT

step_done() { [ -f "$STATE_DIR/$1.done" ]; }
mark_done() { touch "$STATE_DIR/$1.done"; }

retry_backoff() {
  local attempt=0 delay=1
  until "${@:2}"; do
    (( ++attempt >= $1 )) && { echo "Failed after $1 attempts."; return 1; }
    echo "Retry $attempt in ${delay}s..."; sleep $delay; delay=$(( delay * 2 ))
  done
}

if ! step_done "download_artifact"; then
  retry_backoff 4 curl -fsSL https://httpbin.org/get -o /tmp/artifact.json
  mark_done "download_artifact"
  echo "[DONE] download_artifact"
else
  echo "[SKIP] download_artifact"
fi

echo "Deployment finished successfully."

التعامل مع الأخطاء غير القابلة لإعادة المحاولة

لا ينبغي إعادة محاولة جميع الأخطاء. فإعادة محاولة 404 Not Found أو 403 Forbidden تهدر الموارد، إذ لن تنجح هذه الطلبات من دون تدخل بشري. ينبغي لمنطق إعادة المحاولة التمييز بين:

  • الأخطاء المؤقتة — انتهاء مهلة الشبكة، و503 Service Unavailable، وفشل DNS ← إعادة المحاولة
  • الأخطاء الدائمة — 401 Unauthorized، و404 Not Found، والمدخلات غير الصالحة ← الفشل فورًا

مع curl، افحص رمز حالة HTTP وتخطَّ إعادة المحاولة لاستجابات 4xx. استخدم --write-out '%{http_code}' لالتقاط الحالة بصورة منفصلة عن نص الاستجابة.

#!/usr/bin/env bash
set -euo pipefail

fetch_with_retry() {
  local url="$1"
  local max_attempts=4
  local delay=1
  local attempt=0
  local http_code

  until http_code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$url"); do
    : # curl itself failed (network error)
    (( ++attempt >= max_attempts )) && { echo "Network error, giving up."; return 1; }
    sleep $delay; delay=$(( delay * 2 ))
  done

  # Permanent client errors — do not retry
  if [[ "$http_code" =~ ^4 ]]; then
    echo "ERROR: HTTP $http_code for $url — not retrying." >&2
    return 1
  fi

  # Transient server errors — retry
  if [[ "$http_code" =~ ^5 ]]; then
    (( ++attempt >= max_attempts )) && { echo "Server error, giving up."; return 1; }
    echo "HTTP $http_code — backing off ${delay}s..."
    sleep $delay; delay=$(( delay * 2 ))
    fetch_with_retry "$url"
    return
  fi

  echo "HTTP $http_code — success."
}

fetch_with_retry "https://httpbin.org/status/200"

اختبار المعرفة: اختيار استراتيجية التراجع

ينزّل برنامج نصي للنشر ملف إصدار من حاوية S3. أثناء حادثة وقعت مؤخرًا، فشلت 80 نسخة من خط أنابيب التنفيذ في الوقت نفسه بسبب انقطاع قصير في S3. وعندما تعافت S3 بعد 10 ثوانٍ، أعادت النسخ الثمانون المحاولة في اللحظة نفسها، ما تسبب في حمل زائد آخر ومدّد الانقطاع 3 دقائق.

أي استراتيجية لإعادة المحاولة ستمنع على أفضل نحو هذا التسلسل المتتابع من حمل القطيع الهائج في الحوادث المستقبلية؟

مراجعة: خاصية Idempotency وإعادة المحاولة مع التراجع

تعلّمتم في هذا الدرس كيفية تصميم نصوص Bash البرمجية بحيث تكون آمنة لإعادة التشغيل وقادرة على الصمود أمام الأعطال المؤقتة.

أنماط Idempotency:

  • أحط كل عملية بفحص وجود ([ -f ] و[ -d ] وid وgetent).
  • استخدم mkdir -p وأعلامًا مدمجة أخرى توفر idempotency متى توفرت.
  • استخدم ملفات القفل (مع mkdir ذري) لمنع عمليات التشغيل المتزامنة.
  • استخدم ملفات الحالة (ملفات علامات لكل خطوة) للسماح بالاستئناف بعد الفشل.

أنماط إعادة المحاولة مع التراجع:

  • حدّد دائمًا عددًا أقصى للمحاولات — ولا تعِد المحاولة إلى ما لا نهاية.
  • استخدم التراجع الأسي: ضاعف مدة الانتظار بعد كل فشل.
  • أضف Jitter (العشوائية) لمنع التزامن الذي يسبب حمل القطيع الهائج.
  • ميّز بين الأخطاء المؤقتة (إعادة المحاولة) والدائمة (الفشل السريع).

ينتج عن دمج هذه الأنماط نصوص برمجية بمستوى الإنتاج: آمنة وقابلة للرصد وقادرة على إصلاح نفسها.

الأسئلة الشائعة

هل درس «السكربتات القابلة للتكرار ومنطق إعادة المحاولة بالتراجع التدريجي» مجاني؟

نعم — نص درس «السكربتات القابلة للتكرار ومنطق إعادة المحاولة بالتراجع التدريجي» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DevOps Bootcamp، انتقل إلى CoddyKit PRO. تتضمن دورة DevOps Bootcamp 4 دروس في المجموع.

ماذا ستتعلم في «السكربتات القابلة للتكرار ومنطق إعادة المحاولة بالتراجع التدريجي»؟

صمّم عمليات آمنة لإعادة تشغيلها وأضف تراجعًا أُسّيًا عند التعامل مع الاستدعاءات الخارجية غير المستقرة تتمرن على DevOps Bootcamp مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ DevOps Bootcamp؟

لا تُشترط خبرة سابقة. DevOps Bootcamp على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.

كم من الوقت يستغرق درس «السكربتات القابلة للتكرار ومنطق إعادة المحاولة بالتراجع التدريجي»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس DevOps Bootcamp هذا؟

نعم. كل درس في DevOps Bootcamp يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. الوضع الصارم باستخدام set -euo pipefail
  2. معالجات trap للتنظيف والإشارات
  3. الملفات المؤقتة الآمنة ومجلدات القفل
  4. السكربتات القابلة للتكرار ومنطق إعادة المحاولة بالتراجع التدريجي
← العودة إلى DevOps Bootcamp