> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# تمارين Kata في تشخيص الأعطال بثلاثة مستويات مع الحلول
- URL: https://arabroot.io/articles/تمارين-kata-في-تشخيص-الاعطال/
- Published: 2026-10-06T14:45:00.000Z
- Updated: 2026-10-09T13:02:46.000Z
- Description: حوادث صنعناها في المختبر بأعراض حقيقية، تقرر فيها ما تفحصه أولاً ولماذا، ثم تقارن مسارك بمسار التحقيق الكامل.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, أساسيات الحاسب والبرمجة, DevOps وCI/CD, لينكس والسيرفرات, أساسيات, الاستضافة الذاتية

في مقال [لماذا تتمرن دون ذكاء اصطناعي؟](https://arabroot.io/articles/%D8%AA%D9%85%D8%A7%D8%B1%D9%8A%D9%86-kata-%D8%AD%D8%A7%D9%81%D8%B8-%D8%B9%D9%84%D9%89-%D9%85%D9%87%D8%A7%D8%B1%D8%AA%D9%83-%D8%AF%D9%88%D9%86-%D8%B0%D9%83%D8%A7%D8%A1-%D8%A7%D8%B5%D8%B7%D9%86%D8%A7%D8%B9%D9%8A/) شرحنا لماذا تضعف المهارة التي لا تستخدمها، وهذا المقال يطبق طريقة Kata على أصعب لحظة في عمل أي مطور أو مدير سيرفرات: رسالة تصلك تقول «الموقع لا يعمل» أو «الموقع بطيء»، ومعها دليل قليل، وأنت من يقرر ماذا يفحص أولاً. والسبب في أن هذا المجال يحتاج تمريناً أكثر من غيره أن التشخيص ليس معرفة أوامر، وإنما ترتيب أسئلة: ما الذي أراه بالضبط، وما الذي تغير، وما الأمر الذي يقسم المشكلة إلى نصفين، وهذه المهارة لا تأتي من قراءة الحل، وإنما من أن تكتب الأمر التالي بنفسك ثم تقارنه.

وكل تمرين هنا حادثة صنعناها في المختبر بأعراض حقيقية، فتقرأ العرض والدليل المتاح، ثم تجيب عن سؤال واحد: «ما الأمر التالي الذي تشغله، ولماذا؟»، ثم تكمل المسار حتى السبب والإصلاح. وطريقة الاستخدام كما في بقية السلسلة: أغلق أي مساعد ذكاء اصطناعي، واضبط مؤقتاً على الوقت المقترح، واكتب مسار التحقيق كاملاً في ملف قبل أن تفتح التلميح Hint، وبعدها افتح الحل Answer وقارن الأوامر والترتيب معاً، لأن الأمر الصحيح في الترتيب الخاطئ يضيع عليك نصف ساعة في حادثة حقيقية. والطريقة التي نتبعها في كل الحلول هي طريقة [دليل استكشاف الأخطاء وتحليل الأداء](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#method)، فاقرأه أولاً إن لم تكن قرأته.

وسوف نناقش في هذا المقال ما يلي:

- تجهيز المختبر Lab: تطبيق Python صغير خلف Nginx في Docker، نغير سلوكه بمتغيرات البيئة لنصنع كل حادثة.
- المستوى الأول: 502 بعد النشر مرتين بسببين مختلفين، والقرص الممتلئ بالسجلات، وال Container الذي يخرج بالرمز 137، والشهادة المنتهية، و504 على صفحة تقرير واحدة.
- المستوى الثاني: الموقع بطيء للجميع بسبب فهرس ناقص، وقرص فيه مساحة ويرفض الكتابة، و«too many clients»، و502 والتطبيق يعمل، وطلب يفشل من كل اثنين.
- المستوى الثالث: تغيير DNS لا يراه الزوار، والمعالج 100% بسبب مدخل واحد، وتحديثات معلقة والمعالج خامل، و502 بعد كل إعادة نشر.
- جدول متابعة تنسخه لتسجل تقدمك، وروابط المقالات التي تشرح كل موضوع بالتفصيل.

والصورة التالية تبين الحوادث الخمس عشرة موزعة على المستويات الثلاثة:

![ثلاثة أعمدة ملونة: في اليمين المستوى الأول Beginner بالأخضر من 10 إلى 15 دقيقة، وفيه الحوادث 1 إلى 6: 502 بعد النشر لأن التطبيق توقف، و502 بعد النشر بسبب منفذ خاطئ، وNo space left on device، وContainer يخرج بالرمز 137، وشهادة منتهية، و504 على صفحة تقرير. وفي الوسط المستوى الثاني Intermediate بالأصفر من 15 إلى 20 دقيقة، وفيه الحوادث 7 إلى 11: بطء للجميع، وقرص فيه مساحة والكتابة تفشل، وtoo many clients، و502 والتطبيق يعمل، وفشل طلب من كل اثنين. وفي اليسار المستوى الثالث Advanced بالأحمر من 20 إلى 30 دقيقة، وفيه الحوادث 12 إلى 15: تغيير DNS لا يظهر، ومعالج 100% مع مدخل واحد، وتحديثات معلقة والمعالج خامل، و502 بعد كل إعادة نشر](https://arabroot.io/content/images/2026/10/katas-troubleshooting-01-levels-map.webp)

ابدأ من المستوى الأول، فأغلب الحوادث الحقيقية من هذا النوع، وسرعة حلها هي ما يفرق بين دقيقتين وساعة

## تجهيز المختبر Lab

المختبر تطبيق Python من مكتبة Python القياسية فقط، يرد بالنص `ok from <container> v<version>`، ونتحكم فيه بمتغيرات البيئة: `APP_VERSION` للنسخة، و`PORT` و`BIND` للمنفذ والعنوان الذي يستمع عليه، و`REPORT_SECONDS` لزمن صفحة التقرير، و`BROKEN=1` ليرد بالخطأ 500، ومن النسخة 2.0 يحتاج المتغير `DATABASE_URL`. احفظ هذا الملف باسم `app.py`:

```python
import os, socket, time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

VERSION = os.environ.get("APP_VERSION", "1.0")
if VERSION >= "2.0":
    DATABASE_URL = os.environ["DATABASE_URL"]      # new in 2.0: required setting
BIND = os.environ.get("BIND", "0.0.0.0")
PORT = int(os.environ.get("PORT", "8000"))
BROKEN = os.environ.get("BROKEN") == "1"

class H(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"
    def do_GET(self):
        if self.path.startswith("/report"):
            time.sleep(float(os.environ.get("REPORT_SECONDS", "0")))
        code, body = (500, "internal error\n") if BROKEN else (200, f"ok from {socket.gethostname()} v{VERSION}\n")
        data = body.encode()
        self.send_response(code)
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)

print(f"listening on {BIND}:{PORT} (v{VERSION})", flush=True)
ThreadingHTTPServer((BIND, PORT), H).serve_forever()

```

وهذا إعداد Nginx في الملف `default.conf`، ثم أوامر تشغيل الشبكة والتطبيق وNginx على المنفذ `127.0.0.1:21415`:

```nginx
server {
    listen 80;
    location / {
        proxy_pass http://lab-kata-ops-ts-app:8000;
    }
}

```

```bash
docker network create lab-kata-ops-ts
docker run -d --name lab-kata-ops-ts-app --network lab-kata-ops-ts --restart unless-stopped \
  -v "$PWD/app.py:/app.py:ro" python:3.13-slim python -u /app.py
docker run -d --name lab-kata-ops-ts-nginx --network lab-kata-ops-ts -p 127.0.0.1:21415:80 \
  -v "$PWD/default.conf:/etc/nginx/conf.d/default.conf:ro" nginx:1.29-alpine
curl -s http://127.0.0.1:21415/

```

والمخرج سوف يكون كما يلي:

```text
ok from aa191895c441 v1.0

```

و«النشر» في كل تمرين يعني أن تحذف ال Container الخاص بالتطبيق وتشغله من جديد بنفس الأمر مع متغيرات التمرين، مثل `-e APP_VERSION=2.0`، وبعض التمارين تحتاج Containers إضافية مثل PostgreSQL نذكر أوامرها في التمرين نفسه. وكل الأسماء تبدأ ب `lab-kata-ops-ts-` حتى تحذفها بالاسم في النهاية. ولاحظ أن عناوين IP والأرقام والأوقات في المخرجات تختلف عندك، والمهم هو شكل الدليل وليس قيمته.

## المستوى الأول: مبتدئ Beginner

في هذا المستوى الدليل واضح لمن يعرف أين ينظر، والمطلوب أن تصل إليه بأقل عدد من الأوامر، وأن تميز بين الحالات التي تتشابه أعراضها.

### التمرين 1: 502 بعد نشر النسخة 2.0

نشر الفريق النسخة 2.0 من التطبيق قبل دقائق، والآن كل طلب يعطي هذا:

```text
$ curl -i http://127.0.0.1:21415/
HTTP/1.1 502 Bad Gateway
Server: nginx/1.29.8
Date: Wed, 07 Oct 2026 08:28:23 GMT

```

واقترح أحدهم أن تعيد تشغيل Nginx. ما الأمر التالي الذي تشغله، ولماذا؟ وأكمل حتى السبب والإصلاح.

**الوقت المقترح:** 10 دقائق.

تلميح Hint

502 تعني أن ال Proxy يعمل لكنه لم يحصل على رد صحيح ممن خلفه، فالسؤال الأول ليس عن Nginx وإنما عن التطبيق. وسجل أخطاء Nginx يخبرك أي عنوان حاول وماذا حدث، ثم انظر إلى حالة ال Container وعدد مرات إعادة تشغيله.

الحل Answer 

الأمر التالي هو سجل Nginx، لأنه الطرف الذي أعطى 502 ويعرف لماذا، والمخرج سوف يكون كما يلي:

```text
$ docker logs --tail 2 lab-kata-ops-ts-nginx
2026/10/07 08:28:23 [error] 22#22: *3 connect() failed (111: Connection refused) while connecting to upstream, client: 192.168.96.1, server: , request: "GET / HTTP/1.1", upstream: "http://192.168.96.2:8000/", host: "127.0.0.1:21415"
192.168.96.1 - - [07/Oct/2026:08:28:23 +0000] "GET / HTTP/1.1" 502 157 "-" "curl/8.12.1" "-"

```

الرسالة `Connection refused` تعني أن العنوان موجود ولا يوجد شيء يستمع على المنفذ، فالخطوة التالية حالة التطبيق:

```text
$ docker ps -a --filter name=lab-kata-ops-ts --format 'table {{.Names}}\t{{.Status}}'
NAMES                   STATUS
lab-kata-ops-ts-app     Up 1 second
lab-kata-ops-ts-nginx   Up 52 seconds
$ docker inspect lab-kata-ops-ts-app --format 'RestartCount={{.RestartCount}} ExitCode={{.State.ExitCode}}'
RestartCount=7 ExitCode=1
$ docker logs --tail 6 lab-kata-ops-ts-app
Traceback (most recent call last):
  File "/app.py", line 6, in <module>
    DATABASE_URL = os.environ["DATABASE_URL"]      # new in 2.0: required setting
                   ~~~~~~~~~~^^^^^^^^^^^^^^^^
  File "<frozen os>", line 717, in __getitem__
KeyError: 'DATABASE_URL'

```

في المخرج أعلاه لاحظ التالي:

- `Up 1 second` يبدو حالة سليمة، وهذا فخ، فال Container يعمل لأن سياسة `--restart unless-stopped` تعيده كل مرة، والدليل الحقيقي هو `RestartCount=7` و`ExitCode=1`.
- السبب الجذري Root Cause أن النسخة 2.0 تحتاج إعداداً جديداً `DATABASE_URL` لم يضف إلى بيئة الإنتاج، والإصلاح السريع إما أن تضيف المتغير وتعيد النشر، أو تتراجع إلى 1.0 إذا لم تعرف القيمة الصحيحة الآن.
- إعادة تشغيل Nginx لم تكن لتغير شيئاً، والأسوأ أنها تمسح من ذهنك أن المشكلة بدأت مع النشر.
- للمستقبل: أي إعداد جديد يذكر في ملاحظات النسخة، والتطبيق يفحص إعداداته عند التشغيل ويطبع رسالة واضحة، وفحص صحة Healthcheck في خط النشر يمنع اعتبار النشر ناجحاً.

للتفصيل: [حالة 502 و504 في دليل استكشاف الأخطاء](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#case-502-504)، و[تجربة 502 في دليل تتبع الطلب البطيء](https://arabroot.io/articles/%D8%AA%D8%AA%D8%A8%D8%B9-%D8%A7%D9%84%D8%B7%D9%84%D8%A8-%D8%A7%D9%84%D8%A8%D8%B7%D9%8A%D8%A1-%D9%85%D9%86-%D8%A7%D9%84%D9%85%D8%AA%D8%B5%D9%81%D8%AD-%D8%A7%D9%84%D9%89-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA/#lab-502).

### التمرين 2: 502 مرة أخرى، والتطبيق لا يتوقف هذه المرة

بعد إصلاح التمرين السابق نشر الفريق 2.0 مرة ثانية ومعها `DATABASE_URL`، ونفس المكتبة التي يستخدمها التطبيق غيرت المنفذ الافتراضي في نسختها الجديدة. والعرض:

```text
$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:21415/
502
$ docker ps --filter name=lab-kata-ops-ts-app --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
NAMES                 STATUS         PORTS
lab-kata-ops-ts-app   Up 9 seconds

```

ما الأمر التالي، ولماذا؟

**الوقت المقترح:** 10 دقائق.

تلميح Hint

ال Container لم يعد يتوقف، فاقرأ أول سطر طبعه التطبيق، ثم جرب الاتصال من داخل شبكة Docker نفسها، من Container Nginx، على المنفذ الذي يتوقعه Nginx والمنفذ الذي يقوله التطبيق.

الحل Answer 

سجل Nginx يعطي نفس السطر `connect() failed (111: Connection refused)` إلى `192.168.96.2:8000`، فالأمر التالي سجل التطبيق، ثم الاختبار من مكان Nginx:

```text
$ docker logs lab-kata-ops-ts-app
listening on 0.0.0.0:8080 (v2.0)
$ docker exec lab-kata-ops-ts-nginx wget -qO- http://lab-kata-ops-ts-app:8000/
wget: can't connect to remote host (192.168.96.2): Connection refused
$ docker exec lab-kata-ops-ts-nginx wget -qO- http://lab-kata-ops-ts-app:8080/
ok from 6fcdef9de0fb v2.0
$ docker exec lab-kata-ops-ts-app cat /proc/net/tcp
  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
   0: 00000000:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 899805 1 000000009d9cf5fd 100 0 0 10 0
   1: 0B00007F:AA5B 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 901609 1 000000006df71daf 100 0 0 10 0

```

في المخرج أعلاه لاحظ التالي:

- التطبيق يستمع على 8080 وNginx يرسل إلى 8000، وهذا هو السبب الجذري. والإصلاح أن تثبت المنفذ صراحة في إعداد التطبيق `PORT=8000`، ولا تعتمد على القيمة الافتراضية لمكتبة قد تتغير.
- ال Docker Image الصغير لا يحتوي على `ss` أو `netstat`، فقرأنا الجدول الذي تقرأ منه هذه الأدوات: `1F90` بالست عشري هي 8080، والحالة `0A` تعني LISTEN، و`00000000` تعني كل العناوين، والسطر الثاني `0B00007F` هو `127.0.0.11` أي ال DNS الداخلي في Docker.
- الفرق بين هذا التمرين والذي قبله يظهر في الحالة وعدد مرات إعادة التشغيل، وليس في رسالة Nginx، فرسالة Nginx واحدة في الحالتين.

للتفصيل: [حالة 502 في دليل شبكات Docker](https://arabroot.io/articles/%D8%B4%D8%A8%D9%83%D8%A7%D8%AA-docker-%D9%88%D8%A3%D9%81%D8%B6%D9%84-%D8%A7%D9%84%D9%85%D9%85%D8%A7%D8%B1%D8%B3%D8%A7%D8%AA/#wrong-network-502)، و[قسم ss وحالات الاتصال في دليل تحليل أداء Linux](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D8%AF%D8%A7%D8%A1-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1-linux-%D9%81%D9%8A-60-%D8%AB%D8%A7%D9%86%D9%8A%D8%A9/#ss-states).

### التمرين 3: الطلبات الجديدة لا تحفظ

يشتكي العملاء أن الطلب يفشل في الخطوة الأخيرة، والتطبيق يحفظ كل طلب في ملف داخل `/srv/shop/data`. وهذا ما ظهر في سجل التطبيق، ونفس الخطأ عندما جربت أن تكتب الملف بيدك:

```text
$ echo "{\"id\": 1042}" > /srv/shop/data/order-1042.json
sh: write error: No space left on device

```

ما الأمر التالي، ولماذا؟ وأكمل حتى تجد ما يأخذ المساحة وتصلح المشكلة دون أن تحذف بيانات العملاء.

**الوقت المقترح:** 10 دقائق.

تلميح Hint

تأكد أولاً أن القرص ممتلئ فعلاً وأي قسم Partition بالضبط، ثم انزل في المجلدات من الأكبر إلى الأصغر بالأمر `du` مع الخيار `-x` الذي يبقيك في نفس القسم، أو ابحث مباشرة عن الملفات الكبيرة بالأمر `find -size`.

الحل Answer 

صنعنا الحادثة في Container فيه قسم صغير بحجم 64 ميجابايت (`--tmpfs /srv:size=64m`) حتى لا نملأ قرص جهازك. والأمر التالي `df -h` على المسار نفسه، ثم `du` ثم `find`، والمخرج سوف يكون كما يلي:

```text
$ df -h /srv
Filesystem                Size      Used Available Use% Mounted on
tmpfs                    64.0M     64.0M         0 100% /srv
$ du -xh -d2 /srv | sort -h | tail -4
3.8M	/srv/shop/uploads
60.2M	/srv/shop/logs
64.0M	/srv
64.0M	/srv/shop
$ find /srv -xdev -type f -size +10240k -exec ls -lh {} +
-rw-r--r--    1 root     root       60.2M Oct  7 08:44 /srv/shop/logs/app.log
$ truncate -s 0 /srv/shop/logs/app.log && df -h /srv
Filesystem                Size      Used Available Use% Mounted on
tmpfs                    64.0M      3.8M     60.2M   6% /srv
$ echo "{\"id\": 1042}" > /srv/shop/data/order-1042.json && cat /srv/shop/data/order-1042.json
{"id": 1042}

```

في المخرج أعلاه لاحظ التالي:

- السبب الجذري ملف سجل واحد بلا تدوير Rotation، والإصلاح السريع `truncate` وليس `rm`، لأن التطبيق يبقي الملف مفتوحاً، والحذف لا يعيد المساحة حتى يغلقه، وهذه حالة مستقلة شرحناها في [تمرين الملف المحذوف في تمارين Linux](https://arabroot.io/articles/%D8%AA%D9%85%D8%A7%D8%B1%D9%8A%D9%86-kata-%D9%81%D9%8A-linux/#kata-linux-14).
- الإصلاح الدائم تدوير السجلات بالأداة logrotate، أو حدود حجم لسجلات Docker في `daemon.json`، وتنبيه عندما يتجاوز القرص 80% قبل أن يصل إلى 100%.
- ابدأ دائماً من `df` على المسار الذي فشلت فيه الكتابة، لأن السيرفر قد يكون فيه عدة أقسام، وقد يكون القسم الممتلئ غير الذي تظنه.

للتفصيل: [قسم القرص الممتلئ في دليل تحليل أداء Linux](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D8%AF%D8%A7%D8%A1-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1-linux-%D9%81%D9%8A-60-%D8%AB%D8%A7%D9%86%D9%8A%D8%A9/#disk-full)، و[قسم logrotate في دليل السجلات](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/#files-logrotate).

### التمرين 4: ال Worker يختفي كل بضع دقائق دون أي خطأ

خدمة معالجة في الخلفية Worker تخزن نتائج مؤقتة في الذاكرة، وتتوقف كل فترة، وسجلها ينتهي هكذا دون أي رسالة خطأ:

```text
cached 40 MB
cached 48 MB
cached 56 MB

```

وحالتها في `docker ps -a`:

```text
NAMES                 STATUS
lab-kata-ops-ts-oom   Exited (137) 2 seconds ago

```

ما الأمر التالي، ولماذا؟ وماذا يعني الرقم 137؟

**الوقت المقترح:** 10 دقائق.

تلميح Hint

رمز الخروج الأكبر من 128 يعني أن العملية قتلت بإشارة Signal، والرقم ناقص 128 هو رقم الإشارة. والعملية التي تقتل بالإشارة 9 لا تستطيع أن تطبع شيئاً. ابحث في `docker inspect` عن حالة اسمها OOMKilled وعن حد الذاكرة.

الحل Answer 

137 = 128 + 9، أي أن العملية قتلت بالإشارة SIGKILL، ولهذا لا يوجد خطأ في السجل. والأمر التالي `docker inspect` لمعرفة من قتلها:

```text
$ docker inspect lab-kata-ops-ts-oom --format 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Memory={{.HostConfig.Memory}}'
ExitCode=137 OOMKilled=true Memory=67108864

```

والحادثة صنعناها بهذا الأمر، وفيه حد ذاكرة 64 ميجابايت وكود يضيف 8 ميجابايت إلى الذاكرة كل 0.2 ثانية دون أي حد:

```bash
docker run --name lab-kata-ops-ts-oom -m 64m --memory-swap 64m python:3.13-slim python -c "
import time
cache = []
for i in range(1, 200):
    cache.append(bytearray(8 * 1024 * 1024))
    print(f'cached {i * 8} MB', flush=True)
    time.sleep(0.2)
"; echo "exit=$?"

```

في المخرج أعلاه لاحظ التالي:

- `OOMKilled=true` تعني أن النواة قتلت العملية لأنها تجاوزت حد الذاكرة في ال Container (67108864 بايت = 64 ميجابايت)، وآخر رقم في السجل 56 ميجابايت لأن المحاولة التالية تجاوزت الحد مع ذاكرة Python نفسها.
- السبب الجذري في الكود وليس في الحد: ذاكرة مؤقتة Cache بلا حد أعلى ولا وقت انتهاء، فرفع الحد إلى 512 ميجابايت يؤخر الحادثة فقط. والإصلاح أن تحدد حجم ال Cache، وبعدها تضبط الحد حسب ما تقيسه بالأمر `docker stats`.
- 137 قد تأتي أيضاً من `docker kill` أو من انتهاء مهلة `docker stop`، ولذلك لا تحكم من الرقم وحده، فالحقل OOMKilled هو الذي يفرق.

للتفصيل: [قسم OOM في دليل تحليل أداء Linux](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D8%AF%D8%A7%D8%A1-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1-linux-%D9%81%D9%8A-60-%D8%AB%D8%A7%D9%86%D9%8A%D8%A9/#oom)، و[حدود الموارد في توثيق Docker](https://docs.docker.com/engine/containers/resource%5Fconstraints/?ref=arabroot.io).

### التمرين 5: المتصفح يرفض الموقع منذ صباح اليوم

منذ صباح 7 أكتوبر يرى الزوار صفحة تحذير أمني بدل الموقع، وأمس كان كل شيء يعمل، ولم ينشر أحد شيئاً. وهذا ما يعطيه `curl` من سيرفر Linux:

```text
$ curl -sS --cacert /certs/ca.crt https://shop.example.test:21416/
curl: (60) SSL certificate OpenSSL verify result: certificate has expired (10)
More details here: https://curl.se/docs/sslcerts.html

```

ما الأمر التالي الذي يعطيك التاريخ بالضبط، ولماذا لا تكتفي برسالة curl؟ وماذا تفعل بعد تجديد الشهادة حتى لا تتكرر الحادثة؟

**الوقت المقترح:** 10 دقائق.

تلميح Hint

اطلب الشهادة التي يرسلها السيرفر فعلاً بالأمر `openssl s_client`، ومرر الناتج إلى `openssl x509 -noout` لتقرأ الاسم وتاريخ الانتهاء. وقارن التاريخ بساعة السيرفر نفسه.

الحل Answer 

صنعنا شهادة من جهة إصدار CA خاصة بالمختبر، صلاحيتها من 1 يوليو حتى 5 أكتوبر 2026، وشغلناها على Nginx في المنفذ 21416\. والأمر التالي يقرأ الشهادة التي يرسلها السيرفر الآن، لأن الملف على القرص قد يكون غير الذي حمله السيرفر في الذاكرة:

```bash
echo | openssl s_client -connect shop.example.test:21416 -servername shop.example.test -CAfile /certs/ca.crt 2>/dev/null | grep -E "Verify return code|Verification error"
echo | openssl s_client -connect shop.example.test:21416 -servername shop.example.test 2>/dev/null | openssl x509 -noout -subject -enddate
date -u

```

والمخرج سوف يكون كما يلي:

```text
Verification error: certificate has expired
Verify return code: 10 (certificate has expired)
subject=CN=shop.example.test
notAfter=Oct  5 00:00:00 2026 GMT
Wed Oct  7 08:48:27 UTC 2026

```

وبعد إصدار شهادة جديدة لمدة 90 يوماً وإعادة تحميل Nginx بالأمر `nginx -s reload`:

```text
notAfter=Jan  5 08:49:05 2027 GMT
$ curl -sS --cacert /certs/ca.crt https://shop.example.test:21416/
shop ok

```

في المخرج أعلاه لاحظ التالي:

- الخيار `-servername` يرسل اسم الموقع SNI، وبدونه قد يرد السيرفر بشهادة موقع آخر على نفس العنوان، فتشخص الشهادة الخطأ.
- تحقق من `date -u` على السيرفر وعلى جهاز العميل، فالساعة الخاطئة تعطي نفس الرسالة لشهادة سليمة.
- السبب الجذري ليس الشهادة وإنما أن التجديد التلقائي لم يعمل ولم ينبه أحداً، فبعد التجديد افحص لماذا فشل (مهمة cron متوقفة، أو منفذ 80 مغلق أمام التحقق، أو Reload لم يحدث)، وأضف مراقبة تنبه قبل الانتهاء بأسبوعين على الأقل.

للتفصيل: [تجربة الانتهاء في دليل الشهادات الرقمية](https://arabroot.io/articles/%D8%A7%D9%84%D8%B4%D9%87%D8%A7%D8%AF%D8%A7%D8%AA-%D8%A7%D9%84%D8%B1%D9%82%D9%85%D9%8A%D8%A9-tls-%D9%88-x509-%D9%88%D8%B5%D9%8A%D8%BA-pem-%D9%88-der/#lab-expiry)، و[حالة الشهادة في دليل استكشاف الأخطاء](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#case-certificate).

### التمرين 6: صفحة التقرير السنوي وحدها تعطي 504

كل صفحات الموقع سريعة، وصفحة التقرير السنوي تعطي بعد خمس ثوان تقريباً خطأ 504:

```text
$ curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" http://127.0.0.1:21415/
200 0.077030s
$ curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" "http://127.0.0.1:21415/report?year=2026"
504 5.147367s

```

ما الأمر التالي، ولماذا؟ وما الفرق بين هذه الحالة وحالة 502؟ ولماذا قد يكون رفع المهلة Timeout هو الإصلاح الخاطئ؟

**الوقت المقترح:** 15 دقيقة.

تلميح Hint

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

الحل Answer 

شغلنا التطبيق بالمتغير `REPORT_SECONDS=8`، ووضعنا في إعداد Nginx السطر `proxy_read_timeout 5s;`. والأمر التالي سجل Nginx، ثم قياس التطبيق مباشرة:

```text
$ docker logs --tail 2 lab-kata-ops-ts-nginx
2026/10/07 08:35:40 [error] 60#60: *15 upstream timed out (110: Operation timed out) while reading response header from upstream, client: 192.168.96.1, server: , request: "GET /report?year=2026 HTTP/1.1", upstream: "http://192.168.96.2:8000/report?year=2026", host: "127.0.0.1:21415"
192.168.96.1 - - [07/Oct/2026:08:35:40 +0000] "GET /report?year=2026 HTTP/1.1" 504 167 "-" "curl/8.12.1" "-"
$ docker exec lab-kata-ops-ts-nginx sh -c 'time wget -qO- http://lab-kata-ops-ts-app:8000/report?year=2026'
ok from 1d9c8ff8381e v1.0
real	0m 8.00s
user	0m 0.00s
sys	0m 0.00s

```

في المخرج أعلاه لاحظ التالي:

- 502 تعني أن الاتصال فشل أو جاء رد غير صالح، و504 تعني أن الاتصال نجح والتطبيق لم يرد في المهلة، والرسالة `upstream timed out ... while reading response header` تقول ذلك حرفياً.
- التطبيق يأخذ 8 ثوان والمهلة 5، فالإصلاح السهل رفع المهلة، لكنه يخفي السؤال الحقيقي: لماذا يأخذ التقرير 8 ثوان؟ وكل طلب معلق يحجز اتصالاً وخيطاً Thread في التطبيق، ومع عشرين زائراً يطلبون التقرير تتعطل بقية الصفحات.
- الإصلاح الصحيح للتقارير الثقيلة أن تبنى في الخلفية وترسل عند جهوزها، أو أن تحفظ نتيجتها مسبقاً، وأن تقيس الاستعلام الذي خلفها كما في التمرين 7\. والقيمة الافتراضية للمهلة في Nginx هي 60 ثانية، وأي قيمة أخرى قرار يكتب سببه بجانبه.

للتفصيل: [حالة 504 في دليل تطبيقك خلف Reverse Proxy](https://arabroot.io/articles/%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%83-%D8%AE%D9%84%D9%81-reverse-proxy/#error-504)، و[قسم رموز الحالة في دليل تتبع الطلب البطيء](https://arabroot.io/articles/%D8%AA%D8%AA%D8%A8%D8%B9-%D8%A7%D9%84%D8%B7%D9%84%D8%A8-%D8%A7%D9%84%D8%A8%D8%B7%D9%8A%D8%A1-%D9%85%D9%86-%D8%A7%D9%84%D9%85%D8%AA%D8%B5%D9%81%D8%AD-%D8%A7%D9%84%D9%89-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA/#status-codes).

## المستوى الثاني: متوسط Intermediate

في هذا المستوى الدليل الأول يضللك أحياناً: المعالج مرتفع لكن المشكلة ليست في المعالج، والقرص فيه مساحة لكنه ممتلئ، والتطبيق يعمل لكنه لا يصل. فاكتب فرضيتك قبل كل أمر، واكتب ماذا تتوقع أن ترى.

### التمرين 7: الموقع بطيء للجميع منذ ازدياد العملاء

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

**الوقت المقترح:** 20 دقيقة.

تلميح Hint

اسأل القاعدة ماذا تفعل الآن بالعرض `pg_stat_activity`، فإذا تكرر نفس الاستعلام فاطلب خطته بالأمر `EXPLAIN (ANALYZE, BUFFERS)`، وابحث عن Seq Scan وعن عدد الصفوف المستبعدة Rows Removed by Filter.

الحل Answer 

صنعنا الحادثة في PostgreSQL 18 بجدول `customers` فيه مليونا صف بلا فهرس على البريد، وحمل من عشرة اتصالات بالأداة `pgbench`:

```bash
docker run -d --name lab-kata-ops-ts-pg -e POSTGRES_PASSWORD=example postgres:18-alpine -c max_connections=20
docker exec -i lab-kata-ops-ts-pg psql -U postgres -c "create table customers(id bigserial primary key, email text not null, name text, created_at timestamptz default now()); insert into customers(email,name) select 'user'||g||'@example.com', 'Customer '||g from generate_series(1,2000000) g; analyze customers;"

```

والأمر التالي `pg_stat_activity` أثناء الحمل، لأنه يقول ماذا تفعل القاعدة الآن بدل أن تخمن، والمخرج سوف يكون كما يلي:

```text
$ psql -c "select state, left(query, 70) as query, count(*) from pg_stat_activity where backend_type='client backend' and pid <> pg_backend_pid() group by 1,2 order by 3 desc;"
 state  |                                 query                                  | count
--------+------------------------------------------------------------------------+-------
 active | select id, name from customers where email = 'user' || 1083796 || '@ex |     1
 active | select id, name from customers where email = 'user' || 1807715 || '@ex |     1
 active | select id, name from customers where email = 'user' || 1845771 || '@ex |     1
 ...
(10 rows)
$ docker stats --no-stream lab-kata-ops-ts-pg
NAME                 CPU %     MEM USAGE / LIMIT
lab-kata-ops-ts-pg   847.75%   383.8MiB / 31.29GiB

```

كل الاتصالات العشرة تنفذ نفس الاستعلام، فالخطوة التالية خطته:

```text
$ psql -c "explain (analyze, buffers) select id, name from customers where email = 'user1999999@example.com';"
 Gather  (cost=1000.00..33089.77 rows=1 width=24) (actual time=255.492..256.657 rows=1.00 loops=1)
   Workers Planned: 2
   Workers Launched: 2
   Buffers: shared hit=13365 read=8308 written=166
   ->  Parallel Seq Scan on customers  (cost=0.00..32089.67 rows=1 width=24) (actual time=242.874..242.875 rows=0.33 loops=3)
         Filter: (email = 'user1999999@example.com'::text)
         Rows Removed by Filter: 666666
         Buffers: shared hit=13365 read=8308 written=166
 Execution Time: 256.745 ms

```

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

```text
$ psql -c '\timing on' -c "create index customers_email_idx on customers (email);"
Timing is on.
CREATE INDEX
Time: 27497.086 ms (00:27.497)
$ psql -c "explain (analyze) select id, name from customers where email = 'user1999999@example.com';"
 Index Scan using customers_email_idx on customers  (cost=0.43..8.45 rows=1 width=24) (actual time=0.063..0.064 rows=1.00 loops=1)
   Index Cond: (email = 'user1999999@example.com'::text)
 Execution Time: 0.150 ms

```

ونتيجة `pgbench` بعشرة اتصالات لمدة 20 ثانية قبل الفهرس وبعده على نفس الجهاز (أرقام توضيحية من جهاز المختبر، والمهم النسبة بينها):

```text
$ pgbench -U postgres -n -c 10 -j 2 -T 20 -f /tmp/lookup.sql postgres     # before the index
number of clients: 10
number of transactions actually processed: 181
number of failed transactions: 0 (0.000%)
latency average = 1134.632 ms
tps = 8.813429 (without initial connection time)
$ pgbench -U postgres -n -c 10 -j 2 -T 20 -f /tmp/lookup.sql postgres     # after the index
number of transactions actually processed: 142679
number of failed transactions: 0 (0.000%)
latency average = 1.400 ms
tps = 7143.857459 (without initial connection time)

```

والملف `lookup.sql` يختار عميلاً عشوائياً في كل طلب:

```text
\set n random(1, 2000000)
select id, name from customers where email = 'user' || :n || '@example.com';

```

في المخرج أعلاه لاحظ التالي:

- ترقية المعالج كانت ستضاعف السرعة في أحسن حال، والفهرس جعل الاستعلام أسرع من 250 ميلي ثانية إلى أقل من ميلي ثانية واحدة، لأن المشكلة كانت في كمية العمل وليس في سرعة العامل.
- بناء الفهرس أخذ 27 ثانية، و`CREATE INDEX` العادي يمنع الكتابة في الجدول طوال هذه المدة، وفي الإنتاج استخدم `CREATE INDEX CONCURRENTLY` الذي يبنيه دون أن يوقف الكتابة.
- «لم يتغير التطبيق» لا يعني أن شيئاً لم يتغير، فحجم البيانات تغير، والاستعلام الذي كان سريعاً على عشرة آلاف صف يصبح بطيئاً على مليونين.

للتفصيل: [تجربة الفهرس الناقص في دليل تتبع الطلب البطيء](https://arabroot.io/articles/%D8%AA%D8%AA%D8%A8%D8%B9-%D8%A7%D9%84%D8%B7%D9%84%D8%A8-%D8%A7%D9%84%D8%A8%D8%B7%D9%8A%D8%A1-%D9%85%D9%86-%D8%A7%D9%84%D9%85%D8%AA%D8%B5%D9%81%D8%AD-%D8%A7%D9%84%D9%89-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA/#lab-missing-index)، و[حالة البطء للجميع في دليل استكشاف الأخطاء](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#case-slow-all).

### التمرين 8: القرص فيه مساحة والكتابة تفشل

نفس الخطأ الذي رأيته في التمرين 3، لكن هذه المرة:

```text
$ echo "{\"id\": 1043}" > /srv/shop/data/order-1043.json
sh: can't create /srv/shop/data/order-1043.json: No space left on device
$ df -h /srv
Filesystem                Size      Used Available Use% Mounted on
tmpfs                    64.0M      7.8M     56.2M  12% /srv

```

القرص مستخدم بنسبة 12% فقط. ما الأمر التالي، ولماذا؟

**الوقت المقترح:** 15 دقيقة.

تلميح Hint

كل ملف يحتاج شيئين: مساحة للبيانات، وسجلاً يصفه اسمه Inode، وعدد ال Inodes في نظام الملفات محدود ويحدد عند إنشائه. والأمر `df` له خيار يعرض ال Inodes بدل المساحة.

الحل Answer 

صنعنا الحادثة بقسم فيه 2000 Inode فقط (`--tmpfs /srv:size=64m,nr_inodes=2000`) وتطبيق ينشئ ملف جلسة صغيراً لكل زائر. والأمر التالي `df -i`، ثم البحث عن المجلد الذي فيه أكبر عدد من الملفات، والمخرج سوف يكون كما يلي:

```text
$ df -i /srv
Filesystem              Inodes      Used Available Use% Mounted on
tmpfs                     2000      2000         0 100% /srv
$ for d in /srv/shop/*; do echo "$(find $d | wc -l) $d"; done | sort -n
1 /srv/shop/data
1997 /srv/shop/sessions
$ ls /srv/shop/sessions | head -3
sess_0
sess_1
sess_10
$ find /srv/shop/sessions -type f -delete; df -i /srv
Filesystem              Inodes      Used Available Use% Mounted on
tmpfs                     2000         4      1996   0% /srv

```

في المخرج أعلاه لاحظ التالي:

- رسالة النواة واحدة `No space left on device` لنفاد المساحة ولنفاد ال Inodes، والذي يفرق بينهما هو `df -h` مقابل `df -i`، فاجعل الأمرين معاً عادة.
- ال `du` لا يفيد هنا لأن الملفات صغيرة، والمفيد هو عد الملفات في كل مجلد.
- في الإنتاج لا تحذف كل الجلسات كما فعلنا في المختبر، وإنما القديمة فقط مثل `find /srv/shop/sessions -type f -mmin +1440 -delete`، والإصلاح الدائم أن ينظف التطبيق جلساته المنتهية، أو أن تنقل الجلسات إلى Redis أو قاعدة البيانات.

للتفصيل: [قسم ال Inodes في دليل تحليل أداء Linux](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D8%AF%D8%A7%D8%A1-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1-linux-%D9%81%D9%8A-60-%D8%AB%D8%A7%D9%86%D9%8A%D8%A9/#inodes)، و[صفحة df في دليل Linux](https://man7.org/linux/man-pages/man1/df.1.html?ref=arabroot.io).

### التمرين 9: too many clients بعد زيادة عدد النسخ

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

```text
psql: error: connection to server at "127.0.0.1", port 5432 failed: FATAL:  remaining connection slots are reserved for roles with the SUPERUSER attribute

```

ما الأمر التالي، ولماذا؟ واحسب لماذا يحدث الخطأ مع ثلاث نسخ بالضبط.

**الوقت المقترح:** 15 دقيقة.

تلميح Hint

المستخدم superuser له مقاعد محجوزة، فادخل به واسأل عن ثلاثة أشياء: `max_connections` و`superuser_reserved_connections`، ومن يملك الاتصالات الحالية وما حالتها في `pg_stat_activity`.

الحل Answer 

صنعنا الحادثة في نفس قاعدة التمرين السابق (`max_connections=20`) بمستخدم عادي اسمه `shop`، و17 اتصالاً مفتوحاً منه لا تفعل شيئاً. والأمر التالي الدخول بالمستخدم `postgres` الذي له المقاعد المحجوزة:

```text
$ psql -U postgres -c "show max_connections;" -c "show superuser_reserved_connections;"
 max_connections
-----------------
 20
(1 row)

 superuser_reserved_connections
--------------------------------
 3
(1 row)

$ psql -U postgres -c "select usename, application_name, state, count(*), max(now() - state_change) as longest_in_state from pg_stat_activity where backend_type = 'client backend' group by 1,2,3 order by 4 desc;"
 usename  | application_name | state  | count | longest_in_state
----------+------------------+--------+-------+------------------
 shop     | shop-web         | idle   |    17 | 00:00:10.844264
 postgres | psql             | active |     1 | -00:00:00.000001
(2 rows)

```

في المخرج أعلاه لاحظ التالي:

- المتاح للمستخدمين العاديين هو 20 ناقص 3 = 17 اتصالاً، ونسختان بستة اتصالات تأخذان 12، أما ثلاث نسخ فتطلب 18، فيرفض الاتصال الثامن عشر، وهذا حساب تجريه قبل أن تزيد النسخ وليس بعدها.
- الاتصالات كلها `idle`، أي أن القاعدة لا تعمل بجهد، وإنما ال Pools تحجز اتصالات لا تستخدمها، فرفع `max_connections` إلى 500 ليس الحل، لأن كل اتصال في PostgreSQL عملية كاملة تأخذ ذاكرة.
- الإصلاح أن تحسب حجم ال Pool من حد القاعدة مقسوماً على عدد النسخ مع هامش للصيانة، أو أن تضع PgBouncer بين التطبيق والقاعدة فتشترك كل النسخ في عدد صغير من الاتصالات الحقيقية.

للتفصيل: [قسم حجم ال Pool في دليل تجميع الاتصالات](https://arabroot.io/articles/%D8%AA%D8%AC%D9%85%D9%8A%D8%B9-%D8%A7%D9%84%D8%A7%D8%AA%D8%B5%D8%A7%D9%84%D8%A7%D8%AA-connection-pooling-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#pool-sizing)، و[قسم النسخ المتعددة](https://arabroot.io/articles/%D8%AA%D8%AC%D9%85%D9%8A%D8%B9-%D8%A7%D9%84%D8%A7%D8%AA%D8%B5%D8%A7%D9%84%D8%A7%D8%AA-connection-pooling-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#many-instances) في نفس الدليل، و[إعدادات الاتصال في توثيق PostgreSQL](https://www.postgresql.org/docs/current/runtime-config-connection.html?ref=arabroot.io).

### التمرين 10: 502 والتطبيق يعمل ويرد على نفسه

نسخة جديدة من التطبيق، و502 من Nginx، لكن هذه المرة سجل التطبيق سليم، والمطور يقسم أن التطبيق يعمل لأنه جربه من داخل ال Container:

```text
$ docker logs lab-kata-ops-ts-app
listening on 127.0.0.1:8000 (v1.0)
$ docker exec lab-kata-ops-ts-app python -c "import urllib.request;print(urllib.request.urlopen('http://127.0.0.1:8000/').read().decode().strip())"
ok from bb58b8d9689e v1.0

```

المنفذ صحيح هذه المرة. ما الأمر التالي، ولماذا؟

**الوقت المقترح:** 15 دقيقة.

تلميح Hint

جرب من المكان الذي يأتي منه الطلب الحقيقي، أي من Container Nginx. واقرأ أول سطر في السجل مرة أخرى: على أي عنوان يستمع التطبيق؟ ومن يستطيع الوصول إلى هذا العنوان؟

الحل Answer 

الأمر التالي هو نفس الطلب من Container Nginx، ثم جدول المنافذ في التطبيق:

```text
$ docker exec lab-kata-ops-ts-nginx wget -qO- http://lab-kata-ops-ts-app:8000/
wget: can't connect to remote host (192.168.96.2): Connection refused
$ docker exec lab-kata-ops-ts-app sh -c "awk 'NR==1 || \$4==\"0A\" {print \$2, \$4}' /proc/net/tcp"
local_address st
0100007F:1F40 0A
0B00007F:8235 0A

```

وبعد إعادة التشغيل دون `BIND=127.0.0.1`، أي على `0.0.0.0`:

```text
local_address st
0B00007F:ADB5 0A
00000000:1F40 0A

```

في المخرج أعلاه لاحظ التالي:

- `0100007F:1F40` تعني `127.0.0.1:8000` (العنوان مقلوب البايتات، و`1F40` هي 8000)، أي أن التطبيق يقبل الاتصالات من داخل ال Container فقط، وكل Container له `127.0.0.1` خاص به، فطلب Nginx يصل إلى عنوان الشبكة `192.168.96.2` حيث لا يستمع أحد.
- بعد الإصلاح أصبح السطر `00000000:1F40` أي كل العناوين، وهذا صحيح داخل Container لأن العزل يأتي من الشبكة ومن عدم نشر المنفذ، وليس من عنوان الاستماع.
- كثير من أطر العمل تستمع على `127.0.0.1` افتراضياً في وضع التطوير، مثل خادم التطوير في Flask وVite، فعند نقلها إلى Container تحتاج خياراً مثل `--host 0.0.0.0`.

للتفصيل: [قسم التطبيق داخل Container في قائمة تحقق المطور](https://arabroot.io/articles/%D9%82%D8%A7%D8%A6%D9%85%D8%A9-%D8%AA%D8%AD%D9%82%D9%82-%D8%A7%D9%84%D9%85%D8%B7%D9%88%D8%B1-%D9%82%D8%A8%D9%84-%D9%86%D8%B4%D8%B1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%87/#in-container).

### التمرين 11: نصف الطلبات تفشل ولا أحد يستطيع إعادة الخطأ

يقول الدعم الفني إن بعض العملاء يرون «internal error» وبعضهم لا، والمطور لا يرى الخطأ عندما يجرب. التطبيق يعمل بنسختين خلف Nginx بهذا الإعداد:

```nginx
upstream shop {
    server lab-kata-ops-ts-app1:8000;
    server lab-kata-ops-ts-app2:8000;
}

```

وعشرة طلبات متتالية تعطي:

```text
$ for i in $(seq 1 10); do curl -s -o /dev/null -w "%{http_code} " http://127.0.0.1:21415/; done
500 200 500 200 500 200 500 200 500 200

```

ما الذي يجب أن يكون في السجل حتى تعرف السبب من أول نظرة؟ وما الأمر التالي؟

**الوقت المقترح:** 15 دقيقة.

تلميح Hint

النمط المتناوب تماماً يشير إلى توزيع الحمل Round Robin بين نسختين. وسجل Nginx الافتراضي لا يذكر النسخة التي ردت، فابحث عن المتغيرين `$upstream_addr` و`$upstream_status` والتعليمة `log_format`.

الحل Answer 

السجل يجب أن يذكر النسخة التي ردت على كل طلب، فأضفنا صيغة سجل جديدة:

```nginx
log_format upstreamlog '$remote_addr "$request" $status upstream=$upstream_addr '
                       'upstream_status=$upstream_status rt=$request_time';

server {
    listen 80;
    access_log /var/log/nginx/upstream.log upstreamlog;
    location / {
        proxy_pass http://shop;
    }
}

```

والأمر التالي قراءة السجل وتجميعه حسب النسخة، ثم طلب كل نسخة مباشرة، والمخرج سوف يكون كما يلي:

```text
$ docker exec lab-kata-ops-ts-nginx tail -4 /var/log/nginx/upstream.log
192.168.96.1 "GET / HTTP/1.1" 500 upstream=192.168.96.4:8000 upstream_status=500 rt=0.001
192.168.96.1 "GET / HTTP/1.1" 200 upstream=192.168.96.2:8000 upstream_status=200 rt=0.001
192.168.96.1 "GET / HTTP/1.1" 500 upstream=192.168.96.4:8000 upstream_status=500 rt=0.001
192.168.96.1 "GET / HTTP/1.1" 200 upstream=192.168.96.2:8000 upstream_status=200 rt=0.001
$ docker exec lab-kata-ops-ts-nginx sh -c "awk '{print \$5, \$6}' /var/log/nginx/upstream.log | sort | uniq -c"
      5 200 upstream=192.168.96.2:8000
      5 500 upstream=192.168.96.4:8000
$ docker exec lab-kata-ops-ts-nginx wget -S -qO- http://lab-kata-ops-ts-app1:8000/
  HTTP/1.1 200 OK
$ docker exec lab-kata-ops-ts-nginx wget -S -qO- http://lab-kata-ops-ts-app2:8000/
  HTTP/1.1 500 Internal Server Error
$ docker inspect lab-kata-ops-ts-app1 lab-kata-ops-ts-app2 --format '{{.Name}} {{.Config.Env}}'
/lab-kata-ops-ts-app1 [APP_VERSION=1.4 PYTHON_VERSION=3.13.16 ...]
/lab-kata-ops-ts-app2 [APP_VERSION=1.4 BROKEN=1 PYTHON_VERSION=3.13.16 ...]

```

في المخرج أعلاه لاحظ التالي:

- كل الأخطاء من عنوان واحد، فالسبب في النسخة الثانية وحدها، ومقارنة إعداد النسختين تكشف الفرق، وهو في المختبر متغير `BROKEN=1`، وفي الواقع يكون إعداداً مختلفاً أو نسخة لم تحدث أو قرصاً ممتلئاً على سيرفر واحد.
- الإصلاح السريع أن تخرج النسخة المعطوبة من التوزيع بإضافة `down` بجانبها أو بإيقافها، ثم تصلحها بهدوء.
- نسخة Nginx المفتوحة المصدر لا تعتبر الرد 500 فشلاً في النسخة افتراضياً، فهو يعد أخطاء الاتصال والمهلة فقط حسب `proxy_next_upstream`، ولذلك استمر يرسل نصف الطلبات إلى النسخة المعطوبة، وهذا سبب وجود فحص الصحة Health Check في موزع الحمل.

للتفصيل: [حالة الأعطال المتقطعة في دليل استكشاف الأخطاء](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#case-intermittent)، و[قسم معرف الطلب في دليل السجلات](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/#request-id).

## المستوى الثالث: متقدم Advanced

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

### التمرين 12: نقلنا الموقع إلى سيرفر جديد ونصف الزوار ما زالوا على القديم

غير الفريق سجل A للنطاق `shop.example.test` من السيرفر القديم `203.0.113.10` إلى الجديد `203.0.113.20`، وأطفأ السيرفر القديم بعد خمس دقائق لأن «لوحة DNS تظهر العنوان الجديد». والآن جزء من الزوار لا يصلون. وهذا ما يظهر من شبكة أحد الزوار:

```text
$ dig @10.250.77.20 shop.example.test +noall +answer
shop.example.test.	255	IN	A	203.0.113.10

```

ما الأمر التالي، ولماذا؟ واشرح الرقم 255، وماذا كان يجب أن يفعل الفريق قبل التغيير بيوم.

**الوقت المقترح:** 20 دقيقة.

تلميح Hint

هناك نوعان من سيرفرات DNS: السيرفر الموثوق Authoritative الذي يملك السجل، والسيرفر الذي يسأل نيابة عن الزوار Resolver ويحفظ الجواب. اسأل الاثنين مباشرة وقارن. والرقم في العمود الثاني هو مدة البقاء TTL بالثواني.

الحل Answer 

صنعنا الحادثة بسيرفري dnsmasq على شبكة Docker: سيرفر موثوق على `10.250.77.10` يعطي السجل بمدة بقاء 300 ثانية، وResolver على `10.250.77.20` يحفظ الأجوبة، ثم غيرنا السجل في الموثوق. والأمر التالي أن تسأل السيرفر الموثوق مباشرة، ثم ال Resolver، والمخرج سوف يكون كما يلي:

```text
$ dig @10.250.77.20 shop.example.test +noall +answer     # before the change
shop.example.test.	300	IN	A	203.0.113.10
$ dig @10.250.77.10 shop.example.test +noall +answer     # authoritative, after the change
shop.example.test.	300	IN	A	203.0.113.20
$ dig @10.250.77.20 shop.example.test +noall +answer     # visitor's resolver, after the change
shop.example.test.	255	IN	A	203.0.113.10

```

في المخرج أعلاه لاحظ التالي:

- السيرفر الموثوق يعطي العنوان الجديد، فالتغيير صحيح، وال Resolver يعطي العنوان القديم ومعه 255 أي أنه سوف يبقيه 255 ثانية أخرى قبل أن يسأل من جديد، والرقم ينقص كل ثانية حتى يصل إلى الصفر.
- لوحة DNS تسأل السيرفر الموثوق، والزوار يسألون Resolvers كثيرة (مزود الإنترنت، و1.1.1.1، و8.8.8.8، وشبكات الشركات)، وكل واحد منها حفظ الجواب في وقت مختلف، فلا يوجد «انتشار» لحظي، وإنما ذواكر مؤقتة تنتهي واحدة بعد الأخرى.
- الطريقة الصحيحة أن تخفض TTL إلى 60 أو 300 ثانية قبل التغيير بمدة لا تقل عن TTL القديم (إذا كان يوماً كاملاً فقبل يوم)، ثم تغير السجل، وتبقي السيرفر القديم يعمل أو يحول الطلبات إلى الجديد حتى تنتهي أطول مدة بقاء، ثم ترفع TTL من جديد.
- لا تستطيع أن تمسح ذاكرة Resolver لا تملكه، فإطفاء السيرفر القديم مبكراً هو الخطأ الذي حول تغييراً عادياً إلى حادثة.

للتفصيل: [قسم TTL والانتشار في شرح DNS](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-dns-%D9%88%D8%B3%D8%AC%D9%84%D8%A7%D8%AA%D9%87-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#ttl-propagation)، و[قسم الذاكرة المؤقتة](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-dns-%D9%88%D8%B3%D8%AC%D9%84%D8%A7%D8%AA%D9%87-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#cache-ttl) في نفس الشرح.

### التمرين 13: المعالج 100% بعد تسجيل مستخدم واحد

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

```text
GET /signup len=11 valid=True regex_ms=0.0
GET /signup len=21 valid=False regex_ms=452.0
GET /signup len=23 valid=False regex_ms=2118.6
GET /signup len=25 valid=False regex_ms=8403.9
GET /signup len=27 valid=False regex_ms=31888.7

```

والتعبير في الكود:

```python
NAME = re.compile(r"^([a-zA-Z0-9]+\s?)*$")   # "letters, digits and single spaces"

```

ما الذي تستنتجه من الأرقام قبل أن تقرأ التعبير؟ وما الأمر التالي؟ وأعد كتابة التعبير بطريقة لا يتضاعف فيها الزمن.

**الوقت المقترح:** 25 دقيقة.

تلميح Hint

كل حرفين إضافيين يضربان الزمن في قرابة أربعة، وهذا نمو أسي. وكل المدخلات البطيئة غير صالحة. ابحث عن التراجع الكارثي Catastrophic Backtracking، وانظر إلى المحدد `+` داخل مجموعة عليها `*`.

الحل Answer 

الأرقام تقول إن الزمن يتضاعف مع طول المدخل، وإن المدخلات البطيئة كلها مرفوضة، وهذه علامة التراجع الكارثي. وأرسلنا المدخل `aaaa…a!` بأطوال مختلفة إلى التطبيق على المنفذ 21417، ثم طلباً بطول 30 حرفاً، وقسنا أثناءه:

```text
$ curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" "http://127.0.0.1:21417/signup?name=Ahmad%20Salem"
200 0.231114s
len=21 422 0.510917s
len=23 422 2.232191s
len=25 422 8.472407s
len=27 422 31.968550s
$ docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}' lab-kata-ops-ts-regex
NAME                    CPU %
lab-kata-ops-ts-regex   104.98%

```

والسبب في التعبير `([a-zA-Z0-9]+\s?)*`: المسافة اختيارية، فالنص `aaaa` يمكن تقسيمه إلى مجموعات بطرق كثيرة جداً (`aaaa` أو `a|aaa` أو `aa|aa` أو `a|a|a|a`...)، وعندما تفشل المطابقة عند `!` في النهاية يجرب المحرك كل التقسيمات قبل أن يقول «لا»، وعددها يتضاعف مع كل حرف. والإصلاح أن تجعل لكل نص طريقة تقسيم واحدة، فالمسافة إلزامية بين الكلمات:

```text
SAFE = re.compile(r'^[a-zA-Z0-9]+(?: [a-zA-Z0-9]+)*\Z')

len=11 valid=True regex_ms=0.007
len=27 valid=False regex_ms=0.005
len=10001 valid=False regex_ms=1.068
len=12 valid=False regex_ms=0.007

```

في المخرج أعلاه لاحظ التالي:

- طلب واحد بطول 30 حرفاً يأخذ دقائق ويحجز نواة معالج كاملة، فعشرة طلبات من مخترق تكفي لتعطيل السيرفر، وهذا هجوم معروف باسم ReDoS، وزيادة عدد الطلبات في المراقبة لم تظهر لأنه لا يحتاج عدداً كبيراً.
- المسار الصحيح للتحقيق هنا: المعالج مرتفع ← أي عملية (`docker stats` أو `top`) ← أي طلب (السجل مع الزمن والطول) ← أي سطر في الكود، وأداة تحليل مثل py-spy تعطيك السطر مباشرة لكنها تحتاج صلاحيات إضافية في ال Container.
- التعبير الجديد يرفض المسافتين المتتاليتين (`len=12 valid=False`)، وهذا تغيير في السلوك يجب أن تتأكد أنه مقصود، وأضف حداً لطول المدخل قبل التعبير، فلا يوجد اسم مستخدم بعشرة آلاف حرف.
- بعض المحركات مثل RE2 وRust تضمن زمناً خطياً ولا تتراجع، و.NET يدعم مهلة للتعبير، وكل ذلك في دليل التعبيرات النمطية.

للتفصيل: [قسم ReDoS في دليل التعبيرات النمطية](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B9%D8%A8%D9%8A%D8%B1%D8%A7%D8%AA-%D8%A7%D9%84%D9%86%D9%85%D8%B7%D9%8A%D8%A9-regex-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#redos)، و[قسم المرتكزات Anchors](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B9%D8%A8%D9%8A%D8%B1%D8%A7%D8%AA-%D8%A7%D9%84%D9%86%D9%85%D8%B7%D9%8A%D8%A9-regex-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#anchors) في نفس الدليل، و[حالة المعالج المرتفع في دليل استكشاف الأخطاء](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#case-cpu).

### التمرين 14: تحديث المخزون معلق والمعالج خامل

زر «تأكيد الطلب» يدور ثم يعطي خطأ مهلة، والمعالج في القاعدة قرابة الصفر، والقرص هادئ، ومنتجات أخرى تعمل. وهذا ما رآه المطور عندما جرب نفس التحديث بيده مع مهلة 5 ثوان:

```text
$ time psql -c "set statement_timeout = 5000" -c "update stock set qty = qty - 1 where product_id = 1"
SET
ERROR:  canceling statement due to statement timeout
CONTEXT:  while updating tuple (0,1) in relation "stock"
real	0m 5.03s
$ psql -c "update stock set qty = qty - 1 where product_id = 2"
UPDATE 1

```

ما الأمر التالي، ولماذا؟ وأكمل حتى تحرر التحديث، ثم اذكر الإعداد الذي يمنع تكرار الحادثة.

**الوقت المقترح:** 25 دقيقة.

تلميح Hint

التحديث لا يعمل وإنما ينتظر، والقاعدة تعرف من ينتظر ومن يمنعه: الأعمدة `wait_event_type` و`state` في `pg_stat_activity`، والدالة `pg_blocking_pids()`. وابحث عن الحالة `idle in transaction`.

الحل Answer 

صنعنا الحادثة بجلسة فتحت معاملة Transaction وحدثت المنتج 1 ثم توقفت دون `commit`، كما يحدث عندما يفتح الكود معاملة ثم ينتظر خدمة خارجية أو يرمي استثناء لا يغلقها. والأمر التالي `pg_stat_activity` مع الدالة `pg_blocking_pids` بينما تحديث آخر ينتظر:

```text
$ psql -c "select pid, state, wait_event_type, wait_event, pg_blocking_pids(pid) as blocked_by, now() - xact_start as xact_age, left(query, 50) as query from pg_stat_activity where backend_type = 'client backend' and pid <> pg_backend_pid() order by xact_start;"
 pid |        state        | wait_event_type |  wait_event   | blocked_by |    xact_age     |                       query
-----+---------------------+-----------------+---------------+------------+-----------------+----------------------------------------------------
 136 | idle in transaction | Client          | ClientRead    | {}         | 00:00:33.108682 | update stock set qty = qty - 1 where product_id =
 160 | active              | Lock            | transactionid | {136}      | 00:00:07.554467 | update stock set qty = qty - 1 where product_id =
(2 rows)

```

الجلسة 160 تنتظر قفلاً تملكه الجلسة 136، والجلسة 136 لا تفعل شيئاً منذ 33 ثانية وهي داخل معاملة. والإصلاح الفوري إنهاؤها، ثم إعداد يمنع التكرار:

```text
$ psql -c "select pg_terminate_backend(136);"
 pg_terminate_backend
----------------------
 t
(1 row)

$ psql -c "select * from stock order by 1;"
 product_id | qty
------------+-----
          1 |  49
          2 |   9
(2 rows)

$ psql -c "alter system set idle_in_transaction_session_timeout = '60s';" -c "select pg_reload_conf();"
$ psql -c "show idle_in_transaction_session_timeout;"
 idle_in_transaction_session_timeout
-------------------------------------
 1min
(1 row)

```

في المخرج أعلاه لاحظ التالي:

- الكمية أصبحت 49 وليس 48: تحديث الجلسة 136 ألغي Rollback عند إنهائها، والتحديث المنتظر 160 أكمل فوراً، وهذا ما تريده، لكن تأكد دائماً ماذا كانت الجلسة التي تنهيها تفعل.
- المعالج خامل لأن الانتظار على قفل لا يستهلك شيئاً، ولهذا لا تظهر المشكلة في مراقبة الموارد، وتظهر فقط في الزمن وفي `wait_event_type = Lock`.
- الإعداد `idle_in_transaction_session_timeout` ينهي أي جلسة تبقى خاملة داخل معاملة أكثر من دقيقة، والإصلاح الحقيقي في الكود: لا تنتظر شيئاً خارجياً داخل معاملة، وأغلق المعاملة في `finally` أو بأداة اللغة التي تغلقها تلقائياً.

للتفصيل: [تجربة القفل في دليل تتبع الطلب البطيء](https://arabroot.io/articles/%D8%AA%D8%AA%D8%A8%D8%B9-%D8%A7%D9%84%D8%B7%D9%84%D8%A8-%D8%A7%D9%84%D8%A8%D8%B7%D9%8A%D8%A1-%D9%85%D9%86-%D8%A7%D9%84%D9%85%D8%AA%D8%B5%D9%81%D8%AD-%D8%A7%D9%84%D9%89-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA/#lab-lock)، و[الدالة pg\_blocking\_pids في توثيق PostgreSQL](https://www.postgresql.org/docs/current/functions-info.html?ref=arabroot.io).

### التمرين 15: 502 بعد كل إعادة نشر حتى يعاد تشغيل Nginx

نمط غريب يتكرر: كل نشر للتطبيق ينتهي ب 502 لكل الزوار، والحل الذي تعلمه الفريق هو `nginx -s reload` بعد كل نشر. ونسي أحدهم هذه الخطوة اليوم:

```text
--- before deploy
ok from 1c7d5285fecd v1.0
app IP 192.168.96.2
--- after deploy
502 502 502
$ docker logs --tail 2 lab-kata-ops-ts-nginx
2026/10/07 08:40:27 [error] 22#22: *7 connect() failed (111: Connection refused) while connecting to upstream, client: 192.168.96.1, server: , request: "GET / HTTP/1.1", upstream: "http://192.168.96.2:8000/", host: "127.0.0.1:21415"

```

ال Container الجديد يعمل وسجله سليم. ما الأمر التالي الذي يكشف السبب، ولماذا يصلحه ال Reload؟ وأصلح الإعداد حتى لا يحتاج النشر إلى Reload.

**الوقت المقترح:** 30 دقيقة.

تلميح Hint

قارن العنوان الذي يذكره Nginx في السطر `upstream:` بالعنوان الحالي لاسم التطبيق في DNS الداخلي لشبكة Docker. ومتى يحول Nginx الاسم المكتوب في `proxy_pass` إلى عنوان؟ ابحث في توثيق التعليمة `resolver`.

الحل Answer 

الأمر التالي أن تسأل DNS الشبكة عن عنوان التطبيق الآن، وتقارنه بما يستخدمه Nginx:

```text
$ docker exec lab-kata-ops-ts-nginx getent hosts lab-kata-ops-ts-app
192.168.96.4      lab-kata-ops-ts-app  lab-kata-ops-ts-app
$ docker inspect lab-kata-ops-ts-worker --format '{{(index .NetworkSettings.Networks "lab-kata-ops-ts").IPAddress}}'
192.168.96.2

```

التطبيق الجديد على `192.168.96.4`، وNginx ما زال يرسل إلى `192.168.96.2`، وهذا العنوان أخذه Container آخر (Worker) بدأ أثناء النشر، فالرفض يأتي منه. والسبب أن Nginx يحول الاسم المكتوب في `proxy_pass` إلى عنوان مرة واحدة عند بدء التشغيل أو ال Reload، ولا يسأل بعد ذلك، ولهذا يصلحه ال Reload. وفي المختبر صنعنا الحادثة بحذف التطبيق وتشغيل Container آخر قبله حتى يأخذ العنوان القديم، ثم تشغيل التطبيق من جديد. والإصلاح أن تجعل Nginx يسأل DNS الخاص بDocker على `127.0.0.11` وأن تضع الاسم في متغير، لأن Nginx لا يستخدم ال `resolver` إلا عندما يكون العنوان في `proxy_pass` متغيراً:

```nginx
server {
    listen 80;
    resolver 127.0.0.11 valid=10s ipv6=off;

    location / {
        set $app http://lab-kata-ops-ts-app:8000;
        proxy_pass $app;
    }
}

```

وبعد Reload واحد لتحميل هذا الإعداد، نشرنا نسخة جديدة بنفس الطريقة (Container آخر يأخذ العنوان القديم، والتطبيق على عنوان جديد)، دون أي Reload بعدها:

```text
new app IP 192.168.96.5
200 200 200 200 200 200
ok from 3eb29d8bbdde v1.6

```

في المخرج أعلاه لاحظ التالي:

- العطل لا يحدث في كل نشر في الواقع، فإذا أخذ التطبيق الجديد نفس العنوان القديم يعمل كل شيء، ولذلك يبدو عشوائياً، وهذا أصعب نوع من الأعطال لأن الفريق يتعلم «حلاً» دون أن يعرف السبب.
- `valid=10s` يجعل Nginx يحفظ الجواب عشر ثوان فقط، ف DNS الخاص بDocker لا يعطي مدة بقاء طويلة، وبعض الإعدادات تضع أقل من ذلك.
- مع المتغير في `proxy_pass` يتغير سلوك المسار: يرسل Nginx المسار كما جاء ما لم تكتب URI، فانتبه إذا كان الإعداد القديم يعتمد على استبدال المسار كما في تمرين الشرطة في [تمارين DevOps](https://arabroot.io/articles/%D8%AA%D9%85%D8%A7%D8%B1%D9%8A%D9%86-kata-%D9%81%D9%8A-devops/#kata-devops-11).
- Nginx من النسخة 1.27.3 يدعم أيضاً في النسخة المفتوحة المصدر الخيار `resolve` لكل `server` داخل `upstream` مع التعليمة `zone`، وهو الطريق الأفضل عندما يكون لديك أكثر من نسخة.

للتفصيل: [التعليمة resolver في توثيق Nginx](https://nginx.org/en/docs/http/ngx%5Fhttp%5Fcore%5Fmodule.html?ref=arabroot.io#resolver)، و[DNS الداخلي في توثيق شبكات Docker](https://docs.docker.com/engine/network/?ref=arabroot.io)، و[قسم استكشاف الأخطاء في دليل شبكات Docker](https://arabroot.io/articles/%D8%B4%D8%A8%D9%83%D8%A7%D8%AA-docker-%D9%88%D8%A3%D9%81%D8%B6%D9%84-%D8%A7%D9%84%D9%85%D9%85%D8%A7%D8%B1%D8%B3%D8%A7%D8%AA/#troubleshooting).

## جدول المتابعة

انسخ الجدول التالي إلى ملف عندك، واكتب في كل مرة تحل فيها حادثة التاريخ والوقت وعدد الأوامر التي احتجتها حتى السبب، وهل احتجت التلميح، فعدد الأوامر في التشخيص أهم من الوقت، لأنه يقيس هل تختار الأمر الذي يقسم المشكلة أم تجرب كل شيء:

```text
| #  | المستوى  | الحادثة                                  | التاريخ | الوقت | عدد الأوامر | تلميح؟ |
|----|----------|------------------------------------------|---------|-------|-------------|--------|
| 1  | مبتدئ    | 502 والتطبيق يتوقف                       |         |       |             |        |
| 2  | مبتدئ    | 502 ومنفذ خاطئ                           |         |       |             |        |
| 3  | مبتدئ    | القرص ممتلئ بالسجلات                     |         |       |             |        |
| 4  | مبتدئ    | الخروج بالرمز 137                        |         |       |             |        |
| 5  | مبتدئ    | شهادة منتهية                             |         |       |             |        |
| 6  | مبتدئ    | 504 على صفحة التقرير                     |         |       |             |        |
| 7  | متوسط    | بطء للجميع وفهرس ناقص                    |         |       |             |        |
| 8  | متوسط    | نفاد ال Inodes                           |         |       |             |        |
| 9  | متوسط    | too many clients                         |         |       |             |        |
| 10 | متوسط    | 502 والتطبيق على 127.0.0.1               |         |       |             |        |
| 11 | متوسط    | نسخة معطوبة من اثنتين                    |         |       |             |        |
| 12 | متقدم    | TTL وتغيير DNS                           |         |       |             |        |
| 13 | متقدم    | Regex والمعالج 100%                      |         |       |             |        |
| 14 | متقدم    | قفل من جلسة idle in transaction          |         |       |             |        |
| 15 | متقدم    | Nginx يحفظ العنوان القديم                |         |       |             |        |

```

وعندما تنتهي احذف ما أنشأته بالاسم، ولا تستخدم أوامر `prune` لأنها تحذف ما يخص مشاريع أخرى على نفس الجهاز:

```bash
docker rm -f $(docker ps -aq --filter name=^lab-kata-ops-ts-)
docker network rm lab-kata-ops-ts lab-kata-ops-dnsnet
docker rmi lab-kata-ops-dns

```

## الخلاصة

وصلنا لنهاية تمارين تشخيص الأعطال، وأهم ما تخرج به منها:

- ابدأ من الطرف الذي أعطى الخطأ: 502 و504 يكتب Nginx سببهما في سجل الأخطاء، و`Connection refused` تعني لا أحد يستمع، و`timed out` تعني أن أحداً يستمع ولا يرد.
- الحالة `Up` لا تعني أن التطبيق سليم، فانظر إلى عدد مرات إعادة التشغيل ورمز الخروج والحقل OOMKilled، وجرب الاتصال من المكان الذي يأتي منه الطلب الحقيقي، وليس من داخل التطبيق.
- «No space left on device» لها سببان: المساحة و ال Inodes، فشغل `df -h` و`df -i` معاً، وأفرغ السجل المفتوح بالأمر `truncate` وليس `rm`.
- عندما تكون القاعدة هي المشكلة فاسألها ماذا تفعل: `pg_stat_activity` للاستعلامات والاتصالات والأقفال، و`EXPLAIN ANALYZE` للخطة، والسيرفر الأكبر لا يصلح فهرساً ناقصاً ولا قفلاً معلقاً.
- بعض الأسباب في ذاكرة مؤقتة لا تراها: ال Resolver يحفظ سجل DNS حتى ينتهي TTL، وNginx يحفظ عنوان التطبيق حتى ال Reload، فاعرف من يحفظ ماذا ولكم من الوقت.

وهذه المقالات التي تشرح كل موضوع في التمارين بالتفصيل:

[![](https://arabroot.io/content/images/2026/10/troubleshooting-guide-cover-cover.webp)دليلاستكشاف الأخطاء وتحليل الأداء للمبتدئين: من العرض إلى السببعندما يبطئ الموقع أو يعطي 502، هذه طريقة منظمة توصلك من العرض إلى السبب قبل أن تخمن أو تشتري سيرفراً أكبر.](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/linux-performance-cover-cover.webp)دليلتحليل أداء سيرفر Linux في 60 ثانية: ماذا تكتب أولاً؟ماذا تكتب أول ما تدخل إلى سيرفر بطيء، وكيف تعرف خلال دقيقة هل المشكلة في المعالج أم الذاكرة أم القرص أم الشبكة.](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D8%AF%D8%A7%D8%A1-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1-linux-%D9%81%D9%8A-60-%D8%AB%D8%A7%D9%86%D9%8A%D8%A9/)[![](https://arabroot.io/content/images/2026/10/slow-requests-cover-cover.webp)دليلتتبع الطلب البطيء من المتصفح إلى قاعدة البياناتكيف تأخذ طلباً واحداً بطيئاً أو فاشلاً وتعرف في أي طبقة ضاع وقته، بقياسات حقيقية في كل طبقة بدل التخمين.](https://arabroot.io/articles/%D8%AA%D8%AA%D8%A8%D8%B9-%D8%A7%D9%84%D8%B7%D9%84%D8%A8-%D8%A7%D9%84%D8%A8%D8%B7%D9%8A%D8%A1-%D9%85%D9%86-%D8%A7%D9%84%D9%85%D8%AA%D8%B5%D9%81%D8%AD-%D8%A7%D9%84%D9%89-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/connection-pooling-cover-cover.webp)دليلConnection Pooling: لماذا الاتصال بقاعدة البيانات مكلفلماذا تظهر رسالة too many clients عندما تزيد نسخ تطبيقك، وكيف يحل مجمع الاتصالات وPgBouncer هذه المشكلة.](https://arabroot.io/articles/%D8%AA%D8%AC%D9%85%D9%8A%D8%B9-%D8%A7%D9%84%D8%A7%D8%AA%D8%B5%D8%A7%D9%84%D8%A7%D8%AA-connection-pooling-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/logging-basics-cover-cover.webp)دليلالسجلات Logging للمطورين: ماذا تسجل وماذا لا تسجلكيف تكتب سجلات Logs تساعدك على فهم ما حدث في الإنتاج، وما الذي لا يدخل السجل أبداً، وكيف تجمعها في مكان واحد.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/dns-records-cover-cover.webp)دليلشرح DNS وسجلاته للمبتدئين بمثال حقيقيكيف يتحول اسم نطاقك إلى عنوان سيرفرك، وما وظيفة كل سجل تضيفه في لوحة النطاق، والأخطاء التي تضيع فيها الساعات عادة.](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-dns-%D9%88%D8%B3%D8%AC%D9%84%D8%A7%D8%AA%D9%87-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)

وبهذه الحوادث تكتمل سلسلة [تمارين Kata](https://arabroot.io/start/#stage-katas)، وإذا أردت أن تتمرن على الأوامر التي استخدمناها هنا قبل الحوادث فابدأ من [تمارين Linux](https://arabroot.io/articles/%D8%AA%D9%85%D8%A7%D8%B1%D9%8A%D9%86-kata-%D9%81%D9%8A-linux/) و[تمارين DevOps](https://arabroot.io/articles/%D8%AA%D9%85%D8%A7%D8%B1%D9%8A%D9%86-kata-%D9%81%D9%8A-devops/).

## سجل التحديثات

- أكتوبر 2026: كتابة الحوادث الخمس عشرة، وإعادة إنتاج كل حادثة على Docker Engine 29.8.0 (Docker Desktop) باستخدام python:3.13-slim (Python 3.13.16) وnginx:1.29-alpine (Nginx 1.29.8) وpostgres:18-alpine وalpine:3.22 مع OpenSSL 3.5.9 وdnsmasq وbind-tools من حزم Alpine، وcurl 8.17.0 من nginx:1.29-alpine. أرقام pgbench توضيحية من جهاز المختبر، والمهم فيها النسبة بين القياسين على نفس الجهاز.