چرا اجرای کانتینر به معنی آماده‌بودن نیست؟

در یک پروژهٔ چندسرویسی، ممکن است برنامه پیش از آماده‌شدن پایگاه داده یا سرویس داخلی شروع به پاسخ‌گویی کند. نتیجه گاهی خطای اتصال در نخستین درخواست است و با راه‌اندازی دوباره ظاهراً ناپدید می‌شود. در این آموزش، دو سرویس کوچک می‌سازیم تا تفاوت «فرایند اجرا شده» و «وابستگی آماده است» قابل مشاهده باشد. هدف، پیدا کردن قرارداد آمادگی است؛ صرفاً اضافه‌کردن یک مکث ثابت مسئله را حل نمی‌کند.

در فایل کامپوز، سرویس برنامه به وضعیت سالم سرویس وابسته منتظر می‌ماند. این شرط برای ترتیب شروع مفید است، اما تضمین نمی‌کند وابستگی تا پایان عمر برنامه سالم بماند. برنامه همچنان باید قطع ارتباط، خطای پاسخ و زمان انتظار را مدیریت کند. بررسی سلامت نیز به‌تنهایی کانتینر ناسالم را دوباره راه‌اندازی نمی‌کند؛ سیاست اجرای مجدد فرایند و تصمیم سامانهٔ استقرار موضوع‌های جداگانه‌اند.

طراحی آزمایش وابستگی و مرزهای بررسی سلامت

مسیر زنده‌بودن نشان می‌دهد فرایند می‌تواند پاسخ بدهد؛ مسیر آمادگی علاوه بر آن یک درخواست محدود به وابستگی می‌فرستد. اگر وابستگی در دسترس نباشد، برنامه پاسخ ۵۰۳ می‌دهد. در پروژهٔ واقعی، نباید بررسی آمادگی تراکنش سنگین یا تغییر داده انجام دهد. هزینه و تناوب این درخواست را حساب کنید تا پایش سلامت خودش به منبع فشار روی سرویس تبدیل نشود.

چهار فایل زیر را در یک پوشه ذخیره کنید. با docker compose config ساختار تنظیمات را بررسی و سپس با docker compose up --build --wait آزمایش را اجرا کنید. ابزار بررسی با python check_stack.py از بیرون کانتینر درخواست می‌فرستد و در صورت آماده‌بودن خروجی صفر دارد. درگاه نمونه فقط روی نشانی محلی میزبان منتشر می‌شود؛ این نمونه تنظیمات کامل یک سرویس عمومی اینترنتی نیست.

پس از پاسخ موفق، در محیط آزمایش docker compose stop dependency را اجرا و دوباره ابزار بررسی را فراخوانی کنید. انتظار می‌رود پاسخ آمادگی ناموفق شود، هرچند فرایند برنامه هنوز زنده است. سپس وابستگی را با docker compose start dependency برگردانید. این تمرین نشان می‌دهد شرط شروع، جای رسیدگی به خرابی حین کار را نمی‌گیرد و شکست وابستگی باید در سطح درخواست هم قابل مشاهده باشد.

کدهای کامل و قابل ذخیره

چهار فایل زیر را در یک پوشه ذخیره کنید. دستورهای اجرا و بررسی در متن آمده‌اند؛ هیچ کد با سه‌نقطه جایگزین نشده است.

1. مسیرهای زنده‌بودن و آمادگی

نام فایل: app.py؛ همهٔ سطرها را با همین نام ذخیره کنید.

import json
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        ready = True
        if self.path == '/ready':
            try:
                with urllib.request.urlopen('http://dependency:8000/', timeout=2) as response:
                    ready = response.status == 200
            except OSError:
                ready = False
        elif self.path != '/live':
            self.send_error(404)
            return
        body = json.dumps({'ready': ready}).encode()
        self.send_response(200 if ready else 503)
        self.send_header('Content-Type', 'application/json')
        self.send_header('Content-Length', str(len(body)))
        self.end_headers()
        self.wfile.write(body)


if __name__ == '__main__':
    ThreadingHTTPServer(('0.0.0.0', 8000), Handler).serve_forever()

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

2. تصویر برنامه با کاربر محدود

نام فایل: Dockerfile؛ همهٔ سطرها را با همین نام ذخیره کنید.

FROM python:3.14-slim
WORKDIR /app
COPY app.py /app/app.py
USER 10001:10001
EXPOSE 8000
CMD ["python", "app.py"]

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

3. وابستگی سالم پیش از شروع برنامه

نام فایل: compose.yaml؛ همهٔ سطرها را با همین نام ذخیره کنید.

services:
  dependency:
    image: python:3.14-slim
    user: "10001:10001"
    working_dir: /tmp
    command: ["python", "-m", "http.server", "8000"]
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/', timeout=2).read(1)"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 5s
  app:
    build: .
    ports: ["127.0.0.1:8088:8000"]
    depends_on:
      dependency:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/ready', timeout=3).read(1)"]
      interval: 5s
      timeout: 4s
      retries: 3
      start_period: 5s

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

4. بررسی آمادگی از میزبان

نام فایل: check_stack.py؛ همهٔ سطرها را با همین نام ذخیره کنید.

"""Run after docker compose up --build --wait; does not alter containers."""
import json
import sys
import urllib.error
import urllib.request


def check(url):
    try:
        with urllib.request.urlopen(url, timeout=5) as response:
            return response.status == 200 and json.load(response)['ready'] is True
    except (OSError, ValueError, KeyError):
        return False


if __name__ == '__main__':
    passed = check('http://127.0.0.1:8088/ready')
    print(json.dumps({'ready': passed}))
    sys.exit(0 if passed else 1)

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

اجرای نمونه و مشاهدهٔ خرابی واقعی

برای استقرار واقعی، نسخه یا چکیدهٔ تصویر پایه را در فرایند انتشار خود ثابت و دوره‌ای بازبینی کنید. زمان‌های بررسی را با زمان شروع واقعی برنامه تنظیم کنید و گزارش خروجی بررسی سلامت را در کنار لاگ برنامه ببینید. پایان آزمایش را با docker compose down انجام دهید. پیش از انتقال الگو به سرویس اصلی، قطع شبکه، کندی وابستگی و بازگشت آن را در محیط جداگانه آزمایش کنید.

چک‌لیست اجرای عملی

  • ساختار کامپوز و آماده‌شدن هر دو سرویس بررسی شود.
  • پس از توقف وابستگی، آمادگی ناموفق و زنده‌بودن مستقل باشد.
  • برای تماس با وابستگی زمان انتظار محدود تعیین شود.
  • سالم‌بودن را با راه‌اندازی مجدد خودکار یکسان نگیرید.

منابع

تدوین و راهنمای عملی: دانشنامه لیان.