تصلك رسالة أن الموقع بطيء، فتدخل إلى السيرفر عبر SSH وأمامك شاشة سوداء ومؤشر ينتظر، والسؤال الذي يحدد ما سيحدث في الساعة التالية هو: ما أول أمر تكتبه؟ والإجابة التي يصل إليها أغلبنا بالتجربة هي top، ثم نظرة سريعة على أعلى عملية، ثم إعادة تشغيل الخدمة على أمل أن تختفي المشكلة. وفي سنة 2015 نشر فريق هندسة الأداء في Netflix، بقيادة Brendan Gregg، مقالاً بعنوان Linux Performance Analysis in 60,000 Milliseconds يقترح عشرة أوامر بترتيب محدد تعطيك خلال دقيقة صورة عن كل مورد في السيرفر، فتعرف أين تبحث قبل أن تغير أي شيء.
وهذا المقال مبني على فكرة ذلك المقال وترتيبه، ولكن النص والأمثلة من عندنا، وكل مخرج فيه من تجربة حقيقية صنعنا فيها المشكلة بأنفسنا ثم أمسكناها بالأمر المناسب. أما طريقة التحقيق نفسها، أي كيف تصف العرض وتضع فرضية وتقيسها، وقوائم USE وRED، والحالات الشائعة من أولها إلى إصلاحها، فمكانها دليل استكشاف الأخطاء وتحليل الأداء، وهذا المقال هو الجزء الذي يفحص سيرفر Linux واحداً مورداً مورداً. وسوف نناقش في هذا المقال ما يلي:
- الأوامر العشرة بالترتيب، وما يقوله كل واحد منها، مع مخرج حقيقي من سيرفر تحت الحمل.
- المعالج CPU: متوسط الحمل الذي يرتفع والمعالج فارغ، والفرق بين وقت التطبيق ووقت النواة وانتظار القرص والوقت المسروق.
- الذاكرة Memory: لماذا لا تعد ال Page Cache ذاكرة مستهلكة، وكيف تعرف أن السيرفر يستخدم Swap، وكيف تقرأ سجل OOM Killer.
- القرص Disk: امتلاء المساحة، وامتلاء ال inodes، والملف المحذوف الذي ما زال يحجز المساحة، وسجلات Docker وjournald، وضغط الإدخال والإخراج I/O.
- الشبكة Network: حالات الاتصال في
ss، والحالة TIME_WAIT، ونفاد المنافذ، وإعادة الإرسال، وجدول conntrack، وفحص سريع لبطء DNS. - ال Containers: لماذا تعرض الأدوات داخل ال Container أرقام السيرفر كله، وأين تجد الحد الحقيقي.
- ورقة من صفحة واحدة: العرض، والأمر، وما تبحث عنه، ثم الأخطاء الشائعة وقائمة تحقق.
تجهيز المختبر
سوف نصنع كل مشكلة في هذا المقال بأنفسنا داخل Containers صغيرة، والسبب أن قراءة مخرج vmstat في سيرفر سليم لا تعلمك شيئاً، أما أن ترى العمود يتغير لأنك أنت من ضغط القرص فهذا ما يبقى في الذاكرة. ونحتاج Docker Image واحدة فيها أدوات القياس من الحزمة sysstat (أي iostat وmpstat وpidstat وsar)، وأداة توليد الحمل stress-ng، وأداة اختبار القرص fio، وبقية الأدوات التي سنستخدمها، عبر ملف Dockerfile التالي:
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends sysstat stress-ng fio procps iproute2 lsof ncdu htop python3 dnsutils kmod && rm -rf /var/lib/apt/lists/*docker build -t lab-perf-tools:1 .
docker volume create lab-perf-dataوعلى سيرفرك الحقيقي سوف تشغل الأوامر مباشرة بعد تثبيت الحزم بالأمر sudo apt install sysstat، أما في المختبر فنحتاج Container خاصاً نقيس منه السيرفر كله، ونسميه المراقب:
docker run -d --name lab-perf-obs --privileged --pid=host lab-perf-tools:1 sleep infinity
docker exec -it lab-perf-obs bashفي الإعداد أعلاه لاحظ التالي:
- الخيار
--pid=hostيجعل ال Container يرى عمليات السيرفر كلها، فيعرضpidstatوtopالعمليات التي تعمل في ال Containers الأخرى. - الخيار
--privilegedيعطيه صلاحية قراءة سجل النواة بالأمرdmesg، وهذه صلاحيات واسعة جداً، لذلك لا تشغل Container بهذا الشكل على سيرفر إنتاجي، وإنما هو للمختبر فقط. - سيرفر المختبر فيه ثمانية معالجات منطقية و32 GiB من الذاكرة، ويعمل بالإصدار الأول من cgroup، وسوف نذكر أسماء ملفات الإصدار الثاني cgroup v2 حيث تختلف.
والأرقام في كل المخرجات التالية خاصة بهذا السيرفر في وقت التجربة، فهي توضيحية Illustrative، والذي يهم فيها هو الشكل والعلاقة بين الأعمدة وليس القيمة نفسها.
الدقيقة الأولى: عشرة أوامر بالترتيب
الفكرة في الترتيب أن تبدأ بما هو أسهل في القراءة، وهو الأخطاء Errors والتشبع Saturation، أي مورد عليه طابور انتظار، ثم تنظر في نسبة الاستخدام Utilization، وهذا تطبيق مباشر لطريقة USE التي شرحناها في دليل استكشاف الأخطاء. والصورة التالية تبين الأوامر العشرة بالترتيب، وكل لون يمثل المورد الذي ينظر إليه الأمر:

ولكي يكون عند الأوامر ما تكشفه، شغلنا في المختبر عمليتين: الأولى خيط حسابي واحد يعمل باستمرار، والثانية كتابة عشوائية على القرص بأحجام صغيرة:
docker run -d --name lab-perf-cpu lab-perf-tools:1 stress-ng --cpu 1 --timeout 300s
docker run -d --name lab-perf-io -v lab-perf-data:/data lab-perf-tools:1 fio --name=randwrite --filename=/data/test.bin --rw=randwrite --bs=4k --direct=1 --ioengine=libaio --iodepth=32 --size=512m --runtime=300 --time_basedثم نفذنا الأوامر العشرة من داخل المراقب بعد دقيقة واحدة، وحذفنا من مخرجات mpstat وpidstat وsar أسطر Average لأنها تكرر التقرير الوحيد.
1. هل الحمل يزيد أم يقل؟ الأمر uptime
uptimeوالمخرج سوف يكون كما يلي:
05:09:19 up 5:50, 0 user, load average: 3.42, 3.36, 2.89الأرقام الثلاثة هي متوسط الحمل Load Average في آخر دقيقة وخمس دقائق وخمس عشرة دقيقة، وفائدتها الأولى ليست في قيمتها وإنما في اتجاهها: إذا كان رقم الدقيقة أكبر بكثير من رقم الخمس عشرة دقيقة فالمشكلة بدأت للتو، وإذا كان العكس فالمشكلة كانت وانتهت، وربما وصلت متأخراً. ولاحظ أن هذه الأرقام تشمل نهاية تجربة سابقة على السيرفر نفسه، وهذا بالضبط ما يحدث في الواقع، فالمتوسط يحمل ذاكرة ما حدث قبل دخولك. وما يحسبه Linux في هذا المتوسط بالضبط موضوع قسم المعالج.
2. هل اشتكت النواة من شيء؟ الأمر dmesg
dmesg -T | tailوالمخرج سوف يكون كما يلي:
[Tue Oct 6 05:08:17 2026] device veth824b4e9 entered promiscuous mode
[Tue Oct 6 05:08:17 2026] docker0: port 3(veth824b4e9) entered blocking state
[Tue Oct 6 05:08:17 2026] docker0: port 3(veth824b4e9) entered forwarding state
[Tue Oct 6 05:08:17 2026] eth0: renamed from veth21dc0e8
[Tue Oct 6 05:08:17 2026] IPv6: ADDRCONF(NETDEV_CHANGE): veth824b4e9: link becomes readyسجل النواة Kernel Log هو المكان الذي تظهر فيه أخطر الرسائل قبل أن تظهر في أي مكان آخر: قتل عملية بسبب نفاد الذاكرة، وأخطاء القرص I/O Error، وامتلاء جدول تتبع الاتصالات. والخيار -T يحول الوقت إلى تاريخ يقرؤه الإنسان، مع أن صفحة dmesg تنبه إلى أنه قد يكون غير دقيق بعد إيقاف الجهاز مؤقتاً Suspend. والرسائل هنا عادية، فهي ربط واجهات شبكة لل Containers التي شغلناها للتو. وعلى أغلب التوزيعات الحديثة تحتاج sudo لقراءة السجل، والسبب الإعداد dmesg_restrict الذي يمنع المستخدم العادي من قراءته.
3. الطابور والذاكرة والمعالج في سطر واحد مع vmstat 1
vmstat 1 5والمخرج سوف يكون كما يلي:
procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu-------
r b swpd free buff cache si so bi bo in cs us sy id wa st gu
3 0 2764 26276732 426436 4400628 43 62 827 4019 1768 7 4 3 91 2 0 0
2 0 2764 26276732 426436 4400628 0 0 0 121752 9742 49433 14 7 80 0 0 0
1 0 2764 26276732 426436 4400628 0 0 0 57820 4732 23794 13 4 83 0 0 0
2 0 2764 26282956 426436 4400628 0 0 0 99156 8419 41506 14 6 80 0 0 0
2 0 2764 26282956 426444 4400620 0 0 0 103376 8941 43310 14 6 80 0 0 0في المخرج أعلاه لاحظ التالي:
- السطر الأول متوسطات منذ تشغيل السيرفر كما تذكر صفحة vmstat، لذلك تجاهله واقرأ الأسطر التي بعده.
- العمود
rعدد العمليات الجاهزة للعمل أو التي تعمل، فإذا تجاوز عدد المعالجات باستمرار فهناك طابور على المعالج، والعمودbعدد العمليات المحجوزة في انتظار الإدخال والإخراج. - العمودان
siوsoهما الذاكرة التي تقرأ من Swap وتكتب إليه في الثانية، والأصل أن يكونا صفراً، وأي رقم ثابت فيهما يعني أن الذاكرة لا تكفي. - العمود
boيعرض كتابة على القرص بعشرات الآلاف من الكتل في الثانية، وهذه كتابة fio. - أعمدة المعالج
usوsyوidوwaوstنشرحها في قسم المعالج، وهنا نرى 14% تقريباً للتطبيقات، وهو خيط واحد من ثمانية معالجات.
4. كل نواة على حدة مع mpstat -P ALL 1
mpstat -P ALL 1 1والمخرج سوف يكون كما يلي:
Linux 5.15.167.4-microsoft-standard-WSL2 (655034d4b192) 10/06/26 _x86_64_ (8 CPU)
05:09:23 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
05:09:24 all 13.07 0.00 2.84 0.00 0.00 5.67 0.00 0.00 0.00 78.42
05:09:24 0 69.23 0.00 0.00 0.00 0.00 4.81 0.00 0.00 0.00 25.96
05:09:24 1 0.00 0.00 4.39 0.00 0.00 35.09 0.00 0.00 0.00 60.53
05:09:24 2 0.00 0.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 99.00
05:09:24 3 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
05:09:24 4 28.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 72.00
05:09:24 5 6.45 0.00 18.28 0.00 0.00 0.00 0.00 0.00 0.00 75.27
05:09:24 6 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 99.01
05:09:24 7 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00السطر all يقول إن المعالج مشغول 13% فقط، ومن يكتفي به يظن أن السيرفر مرتاح، أما الأسطر التالية فتبين أن النواة 0 عملت 69% والنواة 4 عملت 28% في الثانية نفسها، ومجموعهما قرابة 100%، وهذا خيط واحد ينقله المجدول Scheduler بين نواتين، أي أن عملية واحدة تستهلك نواة كاملة، ولو كان تطبيقك هو هذه العملية لكان بطيئاً والسيرفر في نظر الرسم البياني فارغ. وهذا العرض يكشف التطبيق المحصور في خيط واحد، وشرحه موجود في مقال المعالج والخيوط.
5. من صاحب هذا الاستهلاك؟ الأمر pidstat 1
pidstat 1 1والمخرج سوف يكون كما يلي:
Linux 5.15.167.4-microsoft-standard-WSL2 (655034d4b192) 10/06/26 _x86_64_ (8 CPU)
05:09:24 UID PID %usr %system %guest %wait %CPU CPU Command
05:09:25 0 90965 100.00 0.00 0.00 0.00 100.00 0 stress-ng-cpu
05:09:25 0 91015 10.00 13.00 0.00 0.00 23.00 5 fioالأمر pidstat يشبه top ولكنه يطبع سطراً جديداً كل فترة بدل أن يمسح الشاشة، فتستطيع أن ترى التغير مع الوقت وأن تنسخ المخرج إلى ملاحظاتك. وهنا نرى العملية stress-ng-cpu تستهلك نواة كاملة 100% في وقت التطبيق %usr، والعملية fio تستهلك 23% أغلبها وقت النواة %system لأنها تطلب الكتابة من النظام باستمرار. والعمود %wait هو الوقت الذي كانت فيه العملية جاهزة وتنتظر دورها على المعالج، كما في صفحة pidstat، فإذا ارتفع فالمعالج أقل من حاجة العمليات الموجودة.
6. هل القرص يتأخر؟ الأمر iostat -xz 1
iostat -xz 1 2التقرير الأول من iostat متوسط منذ تشغيل السيرفر، والتقرير الثاني هو الثانية الأخيرة فقط، لذلك نعرض التقرير الثاني:
avg-cpu: %user %nice %system %iowait %steal %idle
13.15 0.00 3.23 0.00 0.00 83.62
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz d/s dkB/s drqm/s %drqm d_await dareq-sz f/s f_await aqu-sz %util
sdd 0.00 0.00 0.00 0.00 0.00 0.00 14903.00 59612.00 0.00 0.00 2.16 4.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 32.23 102.00في المخرج أعلاه لاحظ التالي:
- الخيار
-xيعرض الأعمدة الموسعة، والخيار-zيخفي الأقراص التي لم يحدث عليها شيء، ولذلك ظهر قرص واحد. - العمود
w/sقرابة 15 ألف كتابة في الثانية، وwareq-szمتوسط حجم الطلب 4 KB، وهو ما طلبناه من fio. - العمود
w_awaitمتوسط زمن الكتابة الواحدة بالمللي ثانية، ويشمل وقت الانتظار في الطابور ووقت التنفيذ، وفي الإصدارات الحالية من sysstat ينقسم الزمن إلىr_awaitللقراءة وw_awaitللكتابة وd_awaitللحذف Discard وf_awaitلطلبات Flush بدل العمود القديمawait. - العمود
aqu-szمتوسط طول الطابور على القرص، وكان اسمهavgqu-szفي الإصدارات القديمة كما تذكر صفحة iostat، وهنا 32 طلباً، وهو عمق الطابور الذي طلبناه--iodepth=32. - العمود
%utilيقول 102%، ومع ذلك فالقرص ليس مختنقاً، فكل كتابة أخذت 2 ms فقط. والسبب أن هذا العمود هو نسبة الوقت الذي كان فيه طلب واحد على الأقل عند القرص، والصفحة نفسها تقول إنه لا يعكس حدود الأقراص التي تخدم طلبات كثيرة بالتوازي مثل SSD وRAID، وهذا يشمل أغلب أقراص السيرفرات الافتراضية.
7. كم بقي من الذاكرة فعلاً؟ الأمر free -m
free -mوالمخرج سوف يكون كما يلي:
total used free shared buff/cache available
Mem: 32036 2141 25666 36 4713 29894
Swap: 8192 2 8189العمود الذي يهمك هو available وليس free، والسبب نشرحه بتجربة في قسم الذاكرة. وهنا السيرفر مرتاح: قرابة 29 GiB متاحة، وSwap شبه فارغ.
8. حركة الشبكة على كل واجهة مع sar -n DEV 1
sar -n DEV 1 1والمخرج سوف يكون كما يلي:
Linux 5.15.167.4-microsoft-standard-WSL2 (655034d4b192) 10/06/26 _x86_64_ (8 CPU)
05:09:26 IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s %ifutil
05:09:27 lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
05:09:27 eth0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00الأعمدة rxkB/s وtxkB/s هي الكيلوبايت المستلمة والمرسلة في الثانية على كل واجهة، فتقارنها بسرعة منفذ السيرفر أو بحد المزود، والعمود %ifutil نسبة استخدام الواجهة إذا عرف النظام سرعتها كما في صفحة sar. وتجربتنا هنا لا تستخدم الشبكة، لذلك كل القيم صفر، وهذا بحد ذاته معلومة: الشبكة ليست المشكلة.
9. الاتصالات وإعادة الإرسال مع sar -n TCP,ETCP 1
sar -n TCP,ETCP 1 1والمخرج سوف يكون كما يلي:
05:09:27 active/s passive/s iseg/s oseg/s
05:09:28 0.00 0.00 0.00 0.00
05:09:27 atmptf/s estres/s retrans/s isegerr/s orsts/s
05:09:28 0.00 0.00 0.00 0.00 0.00العمود active/s عدد الاتصالات التي يفتحها السيرفر إلى غيره في الثانية، مثل اتصال تطبيقك بقاعدة البيانات، والعمود passive/s عدد الاتصالات التي يستقبلها من غيره، مثل الزوار. والعمود retrans/s عدد المقاطع التي أعيد إرسالها في الثانية، وارتفاعه يعني فقد حزم في الطريق أو سيرفراً لا يرد في الوقت، وسوف نصنع ذلك عمداً في قسم الشبكة.
10. الصورة الكاملة للتأكد مع top
top -b -n1 | head -15والمخرج سوف يكون كما يلي:
top - 05:09:29 up 5:50, 0 user, load average: 3.12, 3.29, 2.87
Tasks: 59 total, 3 running, 56 sleeping, 0 stopped, 0 zombie
%Cpu(s): 12.6 us, 2.3 sy, 0.0 ni, 78.2 id, 0.0 wa, 0.0 hi, 6.9 si, 0.0 st
MiB Mem : 32036.0 total, 25666.4 free, 2142.5 used, 4713.9 buff/cache
MiB Swap: 8192.0 total, 8189.3 free, 2.7 used. 29893.5 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
90965 root 20 0 52588 6088 2396 R 100.0 0.0 1:10.48 stress-ng+
91015 root 20 0 233600 6652 1360 R 20.0 0.0 0:14.32 fio
1 root 20 0 2064592 71468 37936 S 0.0 0.2 0:02.82 initd
21 root 20 0 3100520 150996 44004 S 0.0 0.5 17:22.14 initd
41 root 20 0 3932 1752 1436 S 0.0 0.0 0:00.00 sh
42 root 20 0 4264 2712 2324 S 0.0 0.0 0:00.02 login
49 root 20 0 4196 3332 2848 S 0.0 0.0 0:00.03 bash
66 100 20 0 16084 3780 3320 S 0.0 0.0 0:00.04 rpcbindوقد يتساءل البعض: إذا كان top يعرض كل هذا، فلماذا نؤخره إلى آخر القائمة؟ والإجابة أن top يعرض لقطة تتجدد كل بضع ثوان وتمسح ما قبلها، فيسهل أن تفوتك قفزة قصيرة، بينما الأوامر السابقة تطبع أسطراً تبقى أمامك وتقارن بينها، لذلك تأتي top في النهاية لتتأكد مما وجدته، وليس لتبحث به من الصفر. والخيار -b يجعله يطبع مرة واحدة دون شاشة تفاعلية، وهي مفيدة عندما تريد حفظ اللقطة. أما htop فنسخة أوضح تعرض كل نواة في شريط، وهي اختيار جيد للنظر السريع.
وبعد هذه الأوامر العشرة تعرف أي مورد يستحق أن تتعمق فيه، والأقسام التالية تأخذ كل مورد وحده، وتصنع فيه المشكلة، وتريك الأمر الذي يمسكها. وقبل أن تكمل احذف الحملين بالأمر docker rm -f lab-perf-cpu lab-perf-io.
المعالج: من يستهلك الوقت، وما نوع هذا الوقت؟
شرحنا في مقال المعالج والخيوط والتزامن الأنوية والخيوط والتبديل بين السياقات Context Switching وحدود المعالج في ال Containers، ولن نكرر ذلك هنا، وإنما نأخذ سؤالين يتكرران عند التحقيق: هل متوسط الحمل المرتفع يعني أن المعالج مشغول؟ وإذا كان المعالج مشغولاً، فمن يشغله؟
متوسط الحمل يرتفع والمعالج فارغ
في Linux لا يعد متوسط الحمل العمليات التي تعمل أو تنتظر المعالج فقط، وإنما يضيف إليها العمليات المحجوزة في انتظار القرص، وهي التي تظهر بالحرف D في ps، أي الانتظار غير القابل للمقاطعة Uninterruptible Sleep، كما في صفحة /proc/loadavg ومقال Brendan Gregg عن متوسط الحمل في Linux. ولكي نرى ذلك شغلنا ثمانية عمال يكتبون على القرص ويطلبون من النظام أن يحفظ كل كتابة فوراً بالخيار --fsync=1، فتبقى كل عملية محجوزة حتى يؤكد القرص الحفظ:
docker run -d --name lab-perf-sync -v lab-perf-data:/data lab-perf-tools:1 fio --name=syncwrite --directory=/data --rw=randwrite --bs=4k --ioengine=psync --direct=1 --fsync=1 --numjobs=8 --size=128m --runtime=240 --time_based --group_reportingوبعد دقيقتين تقريباً نفذنا من المراقب:
nproc
uptime
top -b -n1 | head -4 | tail -2والمخرج سوف يكون كما يلي:
8
05:08:00 up 5:48, 0 user, load average: 8.07, 3.91, 3.02
%Cpu(s): 1.2 us, 1.2 sy, 0.0 ni, 81.2 id, 12.9 wa, 0.0 hi, 3.5 si, 0.0 st
MiB Mem : 32036.0 total, 25699.9 free, 2106.9 used, 4707.9 buff/cacheمتوسط الحمل 8 على ثمانية معالجات، وهذا في التفسير الشائع سيرفر ممتلئ تماماً، بينما المعالج فارغ 81% من الوقت. وقبل ذلك بدقيقة أخذنا لقطة من vmstat وقائمة العمليات في الحالة D:
vmstat 1 5
ps -eo state,pid,comm | awk '$1=="D"'والمخرج سوف يكون كما يلي:
procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu-------
r b swpd free buff cache si so bi bo in cs us sy id wa st gu
4 1 2764 26323176 396284 4393600 43 62 832 3864 1761 7 4 3 91 2 0 0
0 1 2764 26321160 398968 4393848 0 0 0 20232 6238 15751 0 8 77 15 0 0
1 5 2764 26319144 400748 4393744 0 0 0 12512 4401 11197 1 14 69 17 0 0
0 1 2764 26314200 403568 4394224 0 0 0 17620 6797 17441 1 11 73 15 0 0
1 3 2764 26312688 405320 4394072 0 0 0 12960 4541 11671 0 3 76 21 0 0
D 90075 fio
D 90076 fio
D 90077 fio
D 90078 fio
D 90079 fio
D 90080 fio
D 90081 fio
D 90082 fioالعمود us قرابة الصفر، والعمود b فيه عمليات محجوزة، والعمود wa بين 15% و21%، والعمال الثمانية كلهم في الحالة D، أي أن الحمل كله قرص. والقاعدة العملية أن لا تحكم على متوسط الحمل قبل أن تقارنه بعدد المعالجات من nproc، ثم تنظر في العمودين r وb، والسبب أن الرقم الواحد يجمع مشكلتين علاجهما مختلف تماماً، فالطابور على المعالج يحتاج معالجاً أكثر أو كوداً أخف، والطابور على القرص يحتاج قرصاً أسرع أو كتابة أقل. احذف هذا الحمل بالأمر docker rm -f lab-perf-sync قبل أن تكمل.
وقت التطبيق ووقت النواة والانتظار والوقت المسروق
الصورة التالية تبين كيف يقسم Linux كل ثانية من وقت النواة الواحدة، وهي الأعمدة نفسها في vmstat وtop وmpstat بأسماء مختلفة قليلاً:

- وقت المستخدم User (
usأو%usr): كود تطبيقك يحسب، فابحث عن العملية فيpidstatثم عن الدالة التي تستهلك الوقت داخلها، وتتبع الطلب الواحد داخل التطبيق موضوع مقال تتبع الطلب البطيء. - وقت النظام System (
syأو%sys): النواة تعمل نيابة عن التطبيقات، مثل استدعاءات النظام System Calls وحزم الشبكة وإعداد عمليات القرص، فإذا ارتفع وحده فابحث عن تطبيق يقرأ ويكتب أجزاء صغيرة جداً، أو يفتح ويغلق اتصالات كثيرة. وقد شرحنا ما يحدث عند استدعاء النظام في مقال Hello World من الداخل. - انتظار القرص I/O Wait (
waأو%iowait): المعالج لا يجد ما يعمله وهناك عمليات تنتظر القرص، فهو وقت فراغ في الحقيقة، ولكنه علامة على أن القرص هو الذي يؤخر العمل، والخطوة التاليةiostat. - الوقت المسروق Steal (
stأو%steal): في السيرفر الافتراضي VPS أراد سيرفرك أن يعمل، ولكن المضيف Hypervisor أعطى النواة الفعلية لجهاز افتراضي آخر، أي لجيرانك على السيرفر الفعلي نفسه. وإذا بقي هذا الرقم مرتفعاً باستمرار فأنت تدفع لمعالج لا تحصل عليه كاملاً، وقد شرحنا كيف تتعامل مع ذلك في دليل اختيار VPS. - الفراغ Idle (
id): لا يوجد ما يعمل، والمعالج ليس مشكلتك.
والجزء الثاني من الصورة هو ما رأيناه في مخرج mpstat: مجموع 12.5% على ثمانية معالجات قد يكون خيطاً واحداً ممتلئاً، والسيرفر الأكبر لا يسرعه، والذي يسرعه أن يعمل التطبيق في أكثر من خيط أو عملية، أما إذا كانت الأنوية كلها ممتلئة فالنقص حقيقي.
الذاكرة: Page Cache وSwap وOOM Killer
أكثر سؤال يصلنا عن الذاكرة هو: السيرفر فيه 4 GB والأمر free يقول إن المتبقي 200 MB، فهل أحتاج ذاكرة أكبر؟ وغالب الظن أن الإجابة لا، والسبب في الصورة التالية:

الذاكرة المؤقتة للملفات ليست ذاكرة مستهلكة
كل ملف يقرؤه السيرفر يبقى في الذاكرة بعد قراءته، في ما يسمى ذاكرة الصفحات المؤقتة Page Cache، حتى إذا طلبه أحد مرة ثانية جاء من الذاكرة بدل القرص، كما في وثيقة مفاهيم الذاكرة في النواة. وهذه الذاكرة تتخلى عنها النواة فوراً عندما يحتاج برنامج إليها، فالذاكرة الفارغة في Linux ذاكرة ضائعة. والتجربة التالية تكتب ملفاً حجمه 2 GB دون أن يمر بال Cache، ثم تقرؤه:
docker run --rm --name lab-perf-cache -v lab-perf-data:/data lab-perf-tools:1 sh -c 'dd if=/dev/urandom of=/data/big.bin bs=1M count=2048 oflag=direct status=none; free -m; cat /data/big.bin > /dev/null; free -m; grep -E "^(MemTotal|MemFree|MemAvailable|Buffers|Cached|SwapCached|Active\(file\)|Inactive\(file\)|Shmem):" /proc/meminfo; rm /data/big.bin'والمخرج سوف يكون كما يلي:
total used free shared buff/cache available
Mem: 32036 2509 25724 149 4401 29526
Swap: 8192 2 8189
total used free shared buff/cache available
Mem: 32036 2487 23692 149 6455 29548
Swap: 8192 2 8189
MemTotal: 32804872 kB
MemFree: 24261160 kB
MemAvailable: 30257304 kB
Buffers: 146464 kB
Cached: 5405312 kB
SwapCached: 164 kB
Active(file): 838000 kB
Inactive(file): 4560060 kB
Shmem: 153384 kBفي المخرج أعلاه لاحظ التالي:
- بعد قراءة الملف نقص العمود
freeقرابة 2 GB وزادbuff/cacheبالقدر نفسه، أماavailableفلم يتغير تقريباً، والسبب أن هذه الذاكرة ما زالت متاحة لأي برنامج يطلبها. - العمود
usedفي الإصدارات الحالية منfreeيحسب على أنهtotalناقصavailableكما في صفحة free، وavailableنفسه هو القيمةMemAvailableمن /proc/meminfo، وهي تقدير النواة لما يمكن أن يأخذه برنامج جديد دون أن يبدأ النظام ب Swap. - الأمر
freeهنا يعمل داخل Container، ومع ذلك عرض ذاكرة السيرفر كله، وهذا موضوع قسم ال Containers.
والقاعدة أن تراقب available، ولا تقم بتفريغ ال Cache يدوياً بالكتابة في /proc/sys/vm/drop_caches لكي ترتاح للرقم، والسبب أن النظام سوف يعيد قراءة الملفات نفسها من القرص بعد ذلك، فتبطئ السيرفر لتحسن شكل رقم لا قيمة له في التشخيص.
متى يصبح Swap مشكلة؟
عندما لا تكفي الذاكرة الحقيقية تنقل النواة صفحات من ذاكرة البرامج إلى مساحة Swap على القرص، ثم تعيدها عندما يحتاجها البرنامج، والقرص أبطأ من الذاكرة بأضعاف كثيرة، لذلك يبدأ السيرفر بالبطء قبل أن يتوقف أي شيء. ولكي نرى ذلك شغلنا برنامجاً يحجز 640 MB ويلمسها باستمرار داخل Container حده 256 MB من الذاكرة، مع السماح له ب Swap حتى 1 GB كما في توثيق قيود الموارد في Docker:
docker run -d --name lab-perf-swap --memory=256m --memory-swap=1g lab-perf-tools:1 python3 -c "
import time
a=[]
for i in range(40):
a.append(bytearray(16*1024*1024)); time.sleep(0.3)
while True:
for b in a:
b[::4096]=b'x'*len(b[::4096])
time.sleep(0.2)
"ثم نفذنا من المراقب:
docker stats --no-stream lab-perf-swap
docker exec lab-perf-swap sh -c 'grep -E "^(rss|swap|total_rss|total_swap) " /sys/fs/cgroup/memory/memory.stat'
free -mوالمخرج سوف يكون كما يلي:
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
e873538aa20b lab-perf-swap 11.71% 255.9MiB / 256MiB 99.96% 1.08kB / 126B 0B / 0B 1
rss 261066752
swap 410632192
total_rss 261066752
total_swap 410632192
total used free shared buff/cache available
Mem: 32036 2579 26396 59 3570 29456
Swap: 8192 397 7794ال Container عند حده تماماً 99.96%، وdocker stats لا يخبرك بأن هناك 390 MB تقريباً خارج الذاكرة، أما ملف memory.stat فيقول ذلك صراحة في السطر swap. وبعد ذلك بعشر ثوان نفذنا vmstat 1 5:
procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu-------
r b swpd free buff cache si so bi bo in cs us sy id wa st gu
14 1 404040 26443992 123952 4036836 43 64 793 3396 1658 6 3 2 93 2 0 0
9 0 412444 26402900 124104 4069976 25204 24900 25220 67360 7283 29160 39 39 16 6 0 0
12 1 410856 26361220 124972 4130608 31060 30836 31064 75828 12543 49161 32 44 18 6 0 0
9 1 417508 26337936 125236 4155692 15992 22968 15992 137136 6748 29848 41 38 15 7 0 0
13 1 405452 26283740 125284 4179640 10812 4228 10812 36948 3429 20509 38 42 17 4 0 0هذا هو الشكل الذي تبحث عنه: العمودان si وso بعشرات الآلاف من الكيلوبايت في الثانية معاً، أي أن الصفحات تخرج إلى القرص وتعود منه في كل ثانية، وهذا ما يسمى Thrashing، والسيرفر في هذه الحالة يبدو حياً ولكنه بطيء جداً. ولا تحكم من العمود swpd وحده، فقد يكون فيه رقم كبير من صفحات خرجت قبل أيام ولم يحتجها أحد، والمهم هو الحركة في si وso. وأسماء الملفات أعلاه من الإصدار الأول cgroup v1، وفي الإصدار الثاني تجد الذاكرة في memory.current وSwap في memory.swap.current كما في وثيقة cgroup v2. احذف التجربة بالأمر docker rm -f lab-perf-swap.
قاتل العمليات OOM Killer وكيف تقرأ سجله
عندما تنفد الذاكرة وال Swap معاً، أو يصل Container إلى حده، لا يبقى أمام النواة إلا أن تختار عملية وتقتلها لتحرر ذاكرتها، وهذا عمل OOM Killer، واختصار OOM هو Out Of Memory. وهي تختار العملية بحسب درجة تقريبية من 0 إلى 1000 تعتمد على نسبة ما تستخدمه العملية من الذاكرة المسموحة، ويمكن تعديلها لكل عملية بالملف oom_score_adj كما في صفحة oom_score_adj. والتجربة التالية تشغل برنامجاً يحجز 16 MB كل نصف ثانية داخل Container حده 256 MB دون Swap، لأن --memory-swap يساوي --memory:
docker run -d --name lab-perf-oom --memory=256m --memory-swap=256m lab-perf-tools:1 python3 -c "
import time
a=[]
while True:
a.append(bytearray(16*1024*1024)); print(len(a)*16, 'MiB', flush=True); time.sleep(0.5)
"وبعد أربع ثوان، ثم بعد ثمان ثوان أخرى، نفذنا:
docker stats --no-stream lab-perf-oom
docker ps -a --filter name=^lab-perf-oom --format '{{.Names}} {{.Status}}'
docker inspect lab-perf-oom --format 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}'
docker logs lab-perf-oom 2>&1 | tail -3والمخرج سوف يكون كما يلي:
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
93f09b6d22b7 lab-perf-oom 2.15% 195.6MiB / 256MiB 76.39% 872B / 126B 0B / 0B 1
lab-perf-oom Exited (137) 6 seconds ago
OOMKilled=true ExitCode=137
208 MiB
224 MiB
240 MiBآخر ما كتبه البرنامج في سجله 240 MiB، ثم لا شيء، فلا يوجد خطأ ولا رسالة وداع، والسبب أن العملية قتلت بالإشارة SIGKILL التي لا يستطيع البرنامج أن يلتقطها. ورمز الخروج 137 هو 128 مضافاً إليه رقم الإشارة 9، وهذه قاعدة الطرفية في صفحة bash: العملية التي تنتهي بإشارة رقمها N يكون رمز خروجها 128+N. لذلك عندما ترى Container يعيد التشغيل ورمزه 137 فأول ما تفحصه هو OOMKilled، ولا تحكم من الرقم 137 وحده، والسبب أن الأمر docker kill يرسل الإشارة نفسها. أما سجل النواة فيحكي القصة كاملة:
dmesg -T | grep -E "memory: usage|memory\+swap: usage|Memory cgroup out|invoked oom-killer" | tail -4والمخرج سوف يكون كما يلي:
[Tue Oct 6 04:33:23 2026] python3 invoked oom-killer: gfp_mask=0xcc0(GFP_KERNEL), order=0, oom_score_adj=0
[Tue Oct 6 04:33:23 2026] memory: usage 262144kB, limit 262144kB, failcnt 0
[Tue Oct 6 04:33:23 2026] memory+swap: usage 262144kB, limit 262144kB, failcnt 40
[Tue Oct 6 04:33:23 2026] Memory cgroup out of memory: Killed process 42469 (python3) total-vm:277312kB, anon-rss:261040kB, file-rss:6224kB, shmem-rss:0kB, UID:0 pgtables:584kB oom_score_adj:0في المخرج أعلاه لاحظ التالي:
- السطر الأول يذكر العملية التي طلبت الذاكرة فلم تجدها، وهي ليست بالضرورة العملية التي قتلت، فالتي تقتل هي التي في السطر الأخير.
- السطران الأوسطان يقولان إن الاستهلاك وصل إلى الحد تماماً 262144 kB، أي 256 MiB، وهذا يعني أن السبب حد ال Container وليس ذاكرة السيرفر.
- السطر الأخير يبدأ ب
Memory cgroup out of memory، أما إذا نفدت ذاكرة السيرفر كله فيبدأ بOut of memory، وهما النصان اللذان يكتبهما الملف mm/oom_kill.c في النواة. والقيمةanon-rssهي الذاكرة التي كانت العملية تحجزها فعلاً لحظة قتلها، وهي أول رقم تقارنه بما تتوقعه من تطبيقك.
وإذا كانت العملية التي قتلت ليست العملية الرئيسية في ال Container، مثل عامل Worker واحد من عدة عمال، فقد يبقى ال Container يعمل وتظهر المشكلة فقط كطلبات تفشل، لذلك ابحث في dmesg دائماً ولا تعتمد على حالة ال Container وحدها. وعلى سيرفر فيه systemd تجد السجل نفسه بالأمر journalctl -k. احذف التجربة بالأمر docker rm lab-perf-oom.
القرص: المساحة وال inodes والملفات المحذوفة والضغط
عندما يقول أحد إن القرص فيه مشكلة فهو يقصد واحدة من أربع مشكلات مختلفة تماماً، ولكل واحدة منها أمر يكشفها، والصورة التالية تبين الأربع:

ولكي نصنع الحالات الثلاث الأولى دون أن نملأ قرص السيرفر نستخدم قرصاً في الذاكرة من نوع tmpfs حجمه 20 MB وفيه 2000 inode فقط:
docker run --rm --name lab-perf-disk --tmpfs /data:size=20m,nr_inodes=2000 lab-perf-tools:1 bash -c '
echo "### full"; dd if=/dev/zero of=/data/app.log bs=1M count=30 status=none; echo "exit=$?"; df -h /data; rm /data/app.log
echo "### inodes"; mkdir /data/sessions; for i in $(seq 1 2100); do : > /data/sessions/sess_$i || { echo "failed at $i"; break; }; done; df -h /data; df -i /data; echo "### du"; du -sh /data/* ; echo "### find"; for d in /data/*; do echo "$(find $d -xdev | wc -l) $d"; done; rm -rf /data/sessions
echo "### deleted"; python3 -c "
import os,time
f=open(\"/data/worker.log\",\"wb\"); f.write(b\"x\"*15*1024*1024); f.flush()
os.remove(\"/data/worker.log\"); time.sleep(30)" & sleep 2; df -h /data; du -sh /data; lsof +L1 2>/dev/null; ls -l /proc/$!/fd | grep deleted
'والمخرج سوف يكون كما يلي:
### full
dd: error writing '/data/app.log': No space left on device
exit=1
Filesystem Size Used Avail Use% Mounted on
tmpfs 20M 20M 0 100% /data
### inodes
failed at 1999
bash: line 3: /data/sessions/sess_1999: No space left on device
Filesystem Size Used Avail Use% Mounted on
tmpfs 20M 0 20M 0% /data
Filesystem Inodes IUsed IFree IUse% Mounted on
tmpfs 2000 2000 0 100% /data
### du
0 /data/sessions
### find
1999 /data/sessions
### deleted
Filesystem Size Used Avail Use% Mounted on
tmpfs 20M 15M 5.0M 75% /data
0 /data
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
python3 19 root 3w REG 0,218 15728640 0 2002 /data/worker.log (deleted)
l-wx------ 1 root root 64 Oct 6 04:36 3 -> /data/worker.log (deleted)المساحة ممتلئة
الحالة الأولى هي المعروفة: df -h يقول 100% والكتابة تفشل بالرسالة No space left on device. والخطوة التالية أن تعرف أين ذهبت المساحة، والأمر du -xh --max-depth=1 / | sort -h يعرض حجم كل مجلد في المستوى الأول مرتباً، والخيار -x يمنعه من الدخول في أقراص أخرى مركبة تحته كما في صفحة du، ثم تنزل مستوى بعد مستوى. وإذا أردت أن تتنقل بين المجلدات بالأسهم فالأداة ncdu تفعل ذلك بالأمر ncdu -x /. أما إذا كان القرص نفسه صغيراً على ما تحتاجه فقد شرحنا زيادته في دليل توسيع قرص النظام LVM.
ال inodes ممتلئة والمساحة فارغة
كل ملف في نظام الملفات يحتاج سجلاً واحداً يصف مكانه وصلاحياته، وهذا السجل اسمه inode، وفي ext4، وهو نظام الملفات الافتراضي في Ubuntu، يحدد عدد هذه السجلات عند إنشاء القرص ولا يزيد بعد ذلك. لذلك يمكن أن تصل إلى الرسالة نفسها No space left on device والأمر df -h يقول 0%، كما في المخرج أعلاه، والذي يكشف الحقيقة هو df -i الذي يقول IUse% 100%. ولاحظ أن du لم يساعد، فالمجلد حجمه صفر لأن الملفات فارغة، أما عد الملفات في كل مجلد بالأمر find فهو الذي أمسك المجلد المسؤول. والأسباب المعتادة ملفات الجلسات Sessions في تطبيقات PHP، وملفات ال Cache الصغيرة، وطوابير البريد، وكلها ملايين من الملفات الصغيرة لا يلاحظها أحد حتى يتوقف كل شيء.
ملف محذوف ما زال يحجز المساحة
هذه الحالة تحير الجميع في المرة الأولى: حذفت ملف سجل كبيراً، وdf ما زال يقول إن المساحة مستخدمة. والسبب أن الحذف في Linux يزيل اسم الملف فقط، أما محتواه فيبقى ما دامت هناك عملية تفتحه، ولا تعود المساحة إلا عندما تغلقه. وفي المخرج أعلاه يقول df إن 15M مستخدمة، ويقول du إن المجلد فارغ، والذي يكشف السبب هو lsof +L1، أي الملفات المفتوحة التي عدد أسمائها أقل من واحد، كما في صفحة lsof، فيظهر الملف مع كلمة (deleted) واسم العملية التي تمسكه.
والحل الأنظف أن تعيد تشغيل الخدمة التي تمسك الملف، أو أن تطلب منها إعادة فتح ملف السجل إذا كانت تدعم ذلك. وإذا لم تستطع إعادة تشغيلها الآن فيمكنك تفريغ الملف من خلال واصف الملف File Descriptor في /proc، والتجربة التالية تبين ذلك:
docker run --rm --name lab-perf-disk2 --tmpfs /data:size=20m lab-perf-tools:1 bash -c '
python3 -c "
import os,time
f=open(\"/data/worker.log\",\"wb\"); f.write(b\"x\"*15*1024*1024); f.flush()
os.remove(\"/data/worker.log\"); time.sleep(30)" & sleep 2; P=$!; df -h /data | tail -1; : > /proc/$P/fd/3; df -h /data | tail -1'والمخرج سوف يكون كما يلي:
tmpfs 20M 15M 5.0M 75% /data
tmpfs 20M 0 20M 0% /dataالمساحة عادت دون إيقاف العملية. ولكن لا تقم بذلك إلا مع ملف تعرف أنه سجل يمكن أن تخسر محتواه، والسبب أن التفريغ يمسح ما فيه نهائياً، والعملية ما زالت تظن أنه موجود وقد تكتب بعد ذلك في موضع بعيد داخله.
احذف ما تعرفه فقط من سجلات Docker وjournald
أكثر ما يملأ أقراص السيرفرات الصغيرة بصمت هو السجلات، فمشغل السجلات الافتراضي في Docker هو json-file، والقيمة الافتراضية لحجم الملف max-size هي -1، أي بلا حد، كما في توثيق json-file. والتجربة التالية تشغل Containerين يكتب كل منهما ثلاثة ملايين سطر، الأول بالإعداد الافتراضي، والثاني مع تدوير السجلات Log Rotation:
docker run -d --name lab-perf-chatty lab-perf-tools:1 sh -c 'yes "2026-10-06 GET /api/orders 200 12ms user=42 trace=abc" | head -n 3000000; sleep 600'
docker run -d --name lab-perf-chatty-rot --log-opt max-size=5m --log-opt max-file=2 lab-perf-tools:1 sh -c 'yes "2026-10-06 GET /api/orders 200 12ms user=42 trace=abc" | head -n 3000000; sleep 600'وبعد خمس وعشرين ثانية نقيس ملفات السجل على السيرفر:
for c in lab-perf-chatty lab-perf-chatty-rot; do sudo sh -c "du -h $(dirname "$(docker inspect --format '{{.LogPath}}' $c)")/*-json.log*"; doneوالمخرج سوف يكون كما يلي:
355M /var/lib/docker/containers/959d294ef2d762564fb491a5d479521d8c57a872740a20e24db70650f71065ba/959d294ef2d762564fb491a5d479521d8c57a872740a20e24db70650f71065ba-json.log
816K /var/lib/docker/containers/08b913471df10bb667957ecb354995fa56f643caf6610fca1b7781ff7fc20a42/08b913471df10bb667957ecb354995fa56f643caf6610fca1b7781ff7fc20a42-json.log
4.8M /var/lib/docker/containers/08b913471df10bb667957ecb354995fa56f643caf6610fca1b7781ff7fc20a42/08b913471df10bb667957ecb354995fa56f643caf6610fca1b7781ff7fc20a42-json.log.1ال Container الأول كتب 355 MB في أقل من نصف دقيقة وما زال الملف ينمو بلا حد، والثاني لم يتجاوز ملفين مجموعهما أقل من 6 MB. والحل الدائم أن تضبط التدوير لكل ال Containers في الملف daemon.json كما شرحنا في دليل تثبيت Docker، مع ملاحظة أن الإعداد الجديد يطبق على ال Containers التي تنشأ بعده فقط. احذف التجربة بالأمر docker rm -f lab-perf-chatty lab-perf-chatty-rot.
وسجلات النظام في journald لها حد افتراضي، فالقيمة SystemMaxUse افتراضياً 10% من حجم نظام الملفات بحد أقصى 4G كما في صفحة journald.conf، وتعرف حجمها الحالي بالأمر journalctl --disk-usage، وتقلصها إلى حجم محدد بالأمر sudo journalctl --vacuum-size=500M الذي يحذف أقدم الملفات المؤرشفة كما في صفحة journalctl.
docker system prune أو docker volume prune لتحرير المساحة على سيرفر إنتاجي، والسبب أنهما يحذفان كل ما لا يستخدمه Container يعمل في هذه اللحظة، وقد يكون من بينها Volume فيه قاعدة بيانات خدمة متوقفة مؤقتاً، أو Docker Image يحتاجها زميلك. احذف ما تعرفه فقط وبالاسم: ملف سجل تعرف صاحبه، أو Docker Image قديمة لإصدار تعرف أنك لن تعود إليه، أو نسخة احتياطية محلية نقلت إلى مكان آخر.القرص مشغول: متى يكون ضغط الإدخال والإخراج حقيقياً؟
الحالة الرابعة لا علاقة لها بالمساحة، فالقرص فيه مكان كاف ولكن كل طلب ينتظر دوره. وقد رأينا في مخرج iostat أن %util قد يقول 100% والقرص يرد في 2 ms، لذلك لا تحكم بهذا العمود وحده على الأقراص الحديثة، وإنما انظر في ثلاثة أشياء معاً:
- زمن الطلب في
r_awaitوw_await: قارنه بما يعطيه القرص عادة، وأفضل مرجع هو قياس fio الذي تأخذه في اليوم الأول كما في دليل اختيار VPS، فإذا تضاعف أضعافاً فالقرص متأخر. - طول الطابور في
aqu-sz: طابور يكبر مع زمن يكبر هو علامة التشبع Saturation. - من ينتظر: العمود
bفيvmstat، والعمليات في الحالة D، والأمرpidstat -d 1الذي يعرض لكل عملية ما تقرؤه وتكتبه في الثانية.
وفي تجربة الكتابة مع --fsync=1 التي استخدمناها في قسم متوسط الحمل كان التقرير الثاني من iostat -xz 1 2 كما يلي:
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz d/s dkB/s drqm/s %drqm d_await dareq-sz f/s f_await aqu-sz %util
sdd 0.00 0.00 0.00 0.00 0.00 0.00 250.00 1896.00 223.00 47.15 5.92 7.58 0.00 0.00 0.00 0.00 0.00 0.00 83.00 12.88 2.55 110.00وفي الثانية نفسها كان pidstat -d 1 1 يعرض العمال الثمانية، وهذا الجزء الأخير من مخرجه:
Average: UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
Average: 0 90075 0.00 114.85 0.00 0 fio
Average: 0 90076 0.00 114.85 0.00 0 fio
Average: 0 90077 0.00 114.85 0.00 0 fio
Average: 0 90078 0.00 114.85 0.00 0 fio
Average: 0 90079 0.00 114.85 0.00 0 fio
Average: 0 90080 0.00 114.85 0.00 0 fio
Average: 0 90081 0.00 114.85 0.00 0 fio
Average: 0 90082 0.00 114.85 0.00 0 fioالكتابة هنا 250 في الثانية فقط بعد أن كانت قرابة 15 ألفاً في التجربة الأولى على القرص نفسه، والسبب ظاهر في العمودين f/s وf_await: كل كتابة يتبعها طلب Flush يطلب من القرص أن يحفظ ما عنده نهائياً، وكل Flush يأخذ قرابة 13 ms، والنواة تنفذ طلب Flush واحداً في كل مرة كما في صفحة iostat. وهذا هو شكل قاعدة البيانات التي تؤكد كل معاملة على القرص، فالبطء فيها يأتي من عدد مرات الحفظ وليس من حجم البيانات، والقياس داخل قاعدة البيانات نفسها شرحناه في مقال PostgreSQL للجداول الضخمة. احذف ملفات التجربة لاحقاً مع ال Volume في قسم حذف المختبر.
الشبكة: حالات الاتصال والمنافذ وإعادة الإرسال
مشكلات الشبكة على السيرفر الواحد نادراً ما تكون في سرعة المنفذ، وأغلبها في الاتصالات نفسها: اتصالات كثيرة جداً، أو اتصالات لا تغلق، أو حزم تضيع في الطريق. والأداة الأساسية هنا هي ss، وهي بديل netstat في التوزيعات الحديثة. والصورة التالية تبين حالات الاتصال TCP التي ستراها في مخرجها، وأين تتراكم:

الاتصالات وحالاتها مع ss
نحتاج في هذا القسم برنامجاً صغيراً فيه سيرفر وكلاينت معاً، يفتح الكلاينت عدداً من الاتصالات القصيرة واحداً بعد الآخر ويغلق كل اتصال بعد الرد، والسيرفر ينتظر حتى يغلق الكلاينت أولاً، وهذا الملف net.py:
import socket, sys, threading, time
RESP = b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok"
def handle(c):
try:
c.recv(1024); c.sendall(RESP); c.recv(1024) # wait for the client to close first
except OSError: pass
c.close()
def server():
s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", 8080)); s.listen(1024)
while True:
c, _ = s.accept(); threading.Thread(target=handle, args=(c,), daemon=True).start()
threading.Thread(target=server, daemon=True).start(); time.sleep(0.5)
n = int(sys.argv[1]); ok = 0
for i in range(n):
try:
c = socket.create_connection(("127.0.0.1", 8080), timeout=5)
c.sendall(b"GET / HTTP/1.1\r\nHost: x\r\n\r\n"); c.recv(1024); c.close(); ok += 1
except OSError as e:
print(f"request {i+1}: {e}"); break
print(f"{ok} connections ok")
time.sleep(float(sys.argv[2]) if len(sys.argv) > 2 else 0)ثم نشغله بعشرين ألف اتصال، وبعد ثانيتين نقيس معدل الاتصالات وحالاتها:
docker run --rm --name lab-perf-net -v "$PWD/net.py:/net.py:ro" lab-perf-tools:1 bash -c 'python3 /net.py 20000 20 & sleep 2; sar -n TCP,ETCP 1 3; echo "### ss -s"; ss -s | head -3; echo "### count by state"; ss -tan | awk "NR>1 {print \$1}" | sort | uniq -c | sort -rn'والمخرج سوف يكون كما يلي، بعد حذف أسطر Average:
04:40:24 active/s passive/s iseg/s oseg/s
04:40:25 700.00 700.00 6998.02 6998.02
04:40:24 atmptf/s estres/s retrans/s isegerr/s orsts/s
04:40:25 0.00 0.00 0.00 0.00 0.00
04:40:25 active/s passive/s iseg/s oseg/s
04:40:26 691.00 691.00 6912.00 6912.00
04:40:25 atmptf/s estres/s retrans/s isegerr/s orsts/s
04:40:26 0.00 0.00 0.00 0.00 0.00
04:40:26 active/s passive/s iseg/s oseg/s
04:40:27 398.00 398.00 3978.00 3978.00
04:40:26 atmptf/s estres/s retrans/s isegerr/s orsts/s
04:40:27 0.00 0.00 0.00 0.00 0.00
### ss -s
Total: 4
TCP: 2437 (estab 1, closed 2434, orphaned 3, timewait 2285)
### count by state
2286 TIME-WAIT
2 ESTAB
1 LISTENفي المخرج أعلاه لاحظ التالي:
- العمودان
active/sوpassive/sمتساويان لأن الكلاينت والسيرفر في الجهاز نفسه، فكل اتصال يفتح من طرف ويستقبل في الطرف الآخر، وعلى سيرفر حقيقي سوف ترىpassive/sللزوار وactive/sلاتصالات تطبيقك بغيره. - الأمر
ss -sيعطيك الملخص في سطر واحد، وهنا 2285 اتصالاً في الحالة TIME_WAIT واتصال واحد قائم. - الحالة TIME-WAIT تبقى عند الطرف الذي أغلق الاتصال أولاً، وفي Linux مدتها 60 ثانية ثابتة في الكود، وهي القيمة
TCP_TIMEWAIT_LENفي الملف include/net/tcp.h، والغرض منها أن لا تختلط حزمة متأخرة من اتصال قديم باتصال جديد على المنفذ نفسه. والإعدادtcp_fin_timeoutالذي يذكر كثيراً في هذا السياق لا يغير هذه المدة، وإنما يخص الحالة FIN_WAIT_2 كما في وثيقة ip-sysctl.
وآلاف الاتصالات في TIME_WAIT ليست مشكلة بحد ذاتها، أما الحالة التي تستحق القلق فهي CLOSE-WAIT إذا كانت تزيد مع الوقت، والسبب أنها تعني أن الطرف الآخر أغلق الاتصال وتطبيقك لم يغلق جهته بعد، أي أن هناك اتصالات تسربت في الكود، وتعرف صاحبها بالأمر ss -tanp state close-wait الذي يعرض اسم العملية مع كل اتصال.
نفاد المنافذ المحلية
كل اتصال يفتحه السيرفر إلى عنوان ومنفذ محددين يأخذ منفذاً محلياً من مجال اسمه ip_local_port_range، والقيمة الافتراضية من 32768 إلى 60999 كما في وثيقة ip-sysctl، أي قرابة 28 ألف منفذ لكل وجهة. فإذا فتح تطبيقك اتصالاً جديداً لكل طلب إلى قاعدة البيانات أو إلى API، وكل اتصال يبقى 60 ثانية في TIME_WAIT، فسوف تنفد المنافذ عند قرابة 470 اتصالاً جديداً في الثانية. ولكي نرى الخطأ دون انتظار نضيق المجال داخل Container إلى مئة منفذ:
docker run --rm --name lab-perf-ports --sysctl net.ipv4.ip_local_port_range="40000 40099" --sysctl net.ipv4.tcp_tw_reuse=0 -v "$PWD/net.py:/net.py:ro" lab-perf-tools:1 bash -c 'python3 /net.py 3000 0; ss -s | sed -n 2p; ss -tan state time-wait | tail -n +2 | wc -l; ss -tan state time-wait | head -4'والمخرج سوف يكون كما يلي:
request 101: [Errno 99] Cannot assign requested address
100 connections ok
TCP: 243 (estab 0, closed 243, orphaned 0, timewait 100)
100
Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
0 0 127.0.0.1:40096 127.0.0.1:8080
0 0 127.0.0.1:40036 127.0.0.1:8080
0 0 127.0.0.1:40094 127.0.0.1:8080الاتصال رقم 101 فشل بالخطأ Cannot assign requested address، والمنافذ المئة كلها في TIME_WAIT. ولاحظ أننا أوقفنا الإعداد tcp_tw_reuse في هذه التجربة، والسبب أن قيمته الافتراضية 2 تسمح بإعادة استخدام منافذ TIME_WAIT لاتصالات Loopback فقط كما في الوثيقة نفسها، فأوقفناه لنحاكي اتصالات إلى سيرفر آخر. والحل الصحيح ليس توسيع المجال ولا تقصير المدة، وإنما أن لا تفتح اتصالاً جديداً لكل طلب، أي أن تعيد استخدام الاتصالات عبر مجمع اتصالات Connection Pool أو keepalive، وقد شرحنا ذلك مع مختبر بين Nginx والتطبيق في مقال تجميع الاتصالات.
إعادة الإرسال: حزم تضيع في الطريق
عندما تضيع حزمة TCP في الطريق ينتظر المرسل مدة ثم يعيد إرسالها، وهذا الانتظار يظهر للمستخدم كبطء متقطع لا تفسير له في التطبيق. ولكي نصنعه أسقطنا 5% من الحزم الواردة بقاعدة iptables عشوائية داخل Container، ثم شغلنا البرنامج نفسه:
docker run --rm --name lab-perf-loss --cap-add NET_ADMIN -v "$PWD/net.py:/net.py:ro" lab-perf-tools:1 bash -c 'apt-get update -qq >/dev/null && apt-get install -y -qq iptables >/dev/null 2>&1; iptables -A INPUT -i lo -p tcp -m statistic --mode random --probability 0.05 -j DROP; python3 /net.py 1500 25 & sleep 3; sar -n TCP,ETCP 1 3 | grep -v "^$"'والمخرج سوف يكون كما يلي، بعد حذف أسطر Average:
04:42:18 active/s passive/s iseg/s oseg/s
04:42:19 27.00 27.00 278.00 277.00
04:42:18 atmptf/s estres/s retrans/s isegerr/s orsts/s
04:42:19 0.00 0.00 13.00 1.00 0.00
04:42:19 active/s passive/s iseg/s oseg/s
04:42:20 20.00 20.00 193.00 197.00
04:42:19 atmptf/s estres/s retrans/s isegerr/s orsts/s
04:42:20 0.00 0.00 4.00 0.00 0.00
04:42:20 active/s passive/s iseg/s oseg/s
04:42:21 0.00 0.00 8.00 5.00
04:42:20 atmptf/s estres/s retrans/s isegerr/s orsts/s
04:42:21 0.00 0.00 3.00 0.00 0.00البرنامج نفسه الذي فتح 700 اتصال في الثانية في التجربة السابقة صار يفتح 20 إلى 27 فقط، وفي الثانية الثالثة لم يفتح أي اتصال لأنه كان ينتظر إعادة إرسال، والسبب خسارة 5% فقط من الحزم، والعمود retrans/s هو الذي يكشف ذلك. وعلى سيرفر حقيقي يرتفع هذا العمود بسبب شبكة مزدحمة عند المزود، أو بطاقة شبكة فيها مشكلة، أو سيرفر في الطرف الآخر لا يلحق بالرد، والخطوة التالية أن تعرف مع أي عنوان تحدث إعادة الإرسال بالأمر ss -ti الذي يعرض لكل اتصال عداد retrans الخاص به.
جدول تتبع الاتصالات ممتلئ
إذا كان على السيرفر جدار حماية أو Docker، فالنواة تتتبع كل اتصال في جدول اسمه conntrack، وللجدول حد أقصى هو nf_conntrack_max، فإذا امتلأ أسقطت النواة الحزم الجديدة وكتبت في سجلها الرسالة nf_conntrack: table full, dropping packet، وهي النص الذي تجده في الملف nf_conntrack_core.c. وتعرف العدد الحالي والحد بالأمر sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max، أما كيف يعمل هذا الجدول ولماذا يمتلئ فقد شرحناه في مقال NAT، والإعدادات في وثيقة nf_conntrack-sysctl.
فحص سريع لبطء DNS
أحياناً يكون كل طلب يخرج من تطبيقك بطيئاً بالقدر نفسه تقريباً، مثل ثانيتين أو خمس ثوان، والسبب أن السيرفر يسأل عن اسم الوجهة كل مرة وأول سيرفر DNS في قائمته لا يرد. والفحص السريع أن تسأل عن اسم وتقرأ الوقت، ثم تسأل سيرفراً لا يرد لترى كيف يبدو ذلك:
docker run --rm --name lab-perf-dns lab-perf-tools:1 bash -c 'echo "## local resolver"; dig arabroot.io +noall +stats | grep -E "Query time|SERVER"; echo "## dead resolver"; time dig @192.0.2.53 arabroot.io +tries=1 +timeout=2 | tail -2'والمخرج سوف يكون كما يلي:
## local resolver
;; Query time: 0 msec
;; SERVER: 192.168.65.7#53(192.168.65.7) (UDP)
## dead resolver
;; global options: +cmd
;; no servers could be reached
real 0m2.048s
user 0m0.027s
sys 0m0.010sالسطر Query time يعطيك زمن السؤال، والسطر SERVER يقول لك أي سيرفر أجاب، فإذا كان الزمن مئات المللي ثانية أو كان السؤال ينتهي بمهلة Timeout فالمشكلة في سيرفر DNS المذكور في /etc/resolv.conf، وكل طلب يدفع هذه المهلة قبل أن يبدأ. والعنوان 192.0.2.53 من مجال مخصص للتوثيق لا يرد عليه أحد، لذلك انتظر dig المهلة كاملة. أما كيف يحفظ ال Resolver الإجابات ومدة بقائها فقد شرحناه في دليل DNS.
لماذا ترى أرقام السيرفر من داخل ال Container؟
ال Container ليس جهازاً افتراضياً، وإنما عمليات عادية على نواة السيرفر نفسها، تفصلها عن غيرها مساحات الأسماء Namespaces، وتحدد مواردها مجموعات التحكم cgroups كما في صفحة cgroups، وقد شرحنا الفرق بينهما وبين الجهاز الافتراضي في مقال مصطلحات الاستضافة الذاتية. والنتيجة العملية أن ملفات مثل /proc/meminfo و/proc/stat و/proc/loadavg تعرض أرقام السيرفر كله حتى من داخل ال Container، والتجربة التالية تبين ذلك:
docker run --rm --name lab-perf-view --memory=256m --cpus=1 lab-perf-tools:1 sh -c 'nproc; free -m; cat /sys/fs/cgroup/memory/memory.limit_in_bytes; cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us; uptime'والمخرج سوف يكون كما يلي:
8
total used free shared buff/cache available
Mem: 32036 2196 26954 26 3360 29839
Swap: 8192 1 8190
268435456
100000
04:33:06 up 5:13, 0 user, load average: 5.71, 2.91, 1.26ال Container حده 256 MB ومعالج واحد، ومع ذلك يقول nproc إن عنده 8 معالجات، ويقول free إن عنده 32 GB، ويعرض uptime متوسط حمل السيرفر كله. أما الحد الحقيقي ففي ملفات cgroup: 268435456 بايت أي 256 MiB، وحصة 100000 ميكروثانية في كل فترة، أي معالج واحد. والصورة التالية تجمع ما يراه ال Container عن نفسه وما تراه أنت من السيرفر:

وهذا يعني ثلاثة أشياء عند التحقيق:
- عندما تشك في Container فابدأ من السيرفر بالأمر
docker stats، فهو يعرض لكل Container الاستهلاك مقابل الحد، مع ملاحظة أن Docker يطرح ذاكرة ال Cache من الاستهلاك الذي يعرضه كما في توثيق docker stats، ولا يذكر Swap كما رأينا في تجربة Swap. - داخل ال Container اقرأ ملفات cgroup مباشرة: في الإصدار الثاني
memory.maxوmemory.currentوmemory.eventsالذي يعد مرات الوصول إلى الحد ومرات القتل، وcpu.maxوcpu.statالذي يعد مرات الخنق Throttling، وتعرف الإصدار بالأمرstat -fc %T /sys/fs/cgroup/، وقد شرحنا خنق المعالج بتجربة في مقال المعالج والخيوط. - التطبيقات التي تحسب عدد عمالها من عدد المعالجات أو من حجم الذاكرة قد ترى أرقام السيرفر فتبدأ عمالاً أكثر مما يتحمله الحد، فتخنق أو تقتل. وهناك أداة اسمها LXCFS تعرض ملفات
/procبأرقام ال Container، ولكن الحل الأبسط أن تحدد عدد العمال بنفسك في إعدادات التطبيق.
ورقة الدقيقة الواحدة: العرض والأمر وما تبحث عنه
الجدول التالي يجمع المقال في صفحة واحدة، وفكرته أن تبدأ من العرض الذي أمامك:
| العرض | الأمر | ما تبحث عنه |
|---|---|---|
| متوسط الحمل مرتفع | nproc ثم vmstat 1 | الحمل مقارنة بعدد المعالجات، والعمود r للطابور على المعالج، والعمود b والحالة D لطابور القرص |
| المعالج مشغول | mpstat -P ALL 1 ثم pidstat 1 | نواة واحدة عند 100% أم كل الأنوية، ومن العملية، وهل الوقت us أم sy أم st |
| VPS بطيء والاستهلاك منخفض | vmstat 1 | العمود st فوق الصفر باستمرار |
| الذاكرة تبدو ممتلئة | free -m | العمود available وليس free |
| السيرفر بطيء جداً ولا يتوقف | vmstat 1 | العمودان si وso بأرقام ثابتة |
| عملية أو Container يموت فجأة | dmesg -T | grep -i -E 'killed process|out of memory' | Out of memory للسيرفر كله أو Memory cgroup out of memory للحد، ثم OOMKilled ورمز الخروج 137 |
No space left on device | df -h ثم df -i | المساحة أم ال inodes، ثم du -xh --max-depth=1 أو عدد الملفات بالأمر find |
| حذفت ملفات والمساحة لم تعد | lsof +L1 | ملف (deleted) واسم العملية التي تمسكه |
| القرص يمتلئ مع الوقت | docker system df وjournalctl --disk-usage | سجلات ال Containers دون تدوير، وسجلات النظام، وDocker Images قديمة |
| القرص بطيء | iostat -xz 1 وpidstat -d 1 | r_await وw_await مقارنة بالمعتاد، وaqu-sz يكبر، وf_await مع Flush كثير، ولا تعتمد على %util وحده |
اتصالات تفشل بخطأ Cannot assign requested address | ss -s وss -tan state time-wait | wc -l | آلاف الاتصالات في TIME_WAIT إلى الوجهة نفسها، والحل مجمع اتصالات |
| اتصالات معلقة تزيد | ss -tanp state close-wait | العملية التي لا تغلق اتصالاتها |
| بطء متقطع في الشبكة | sar -n TCP,ETCP 1 وss -ti | retrans/s فوق الصفر، ومع أي عنوان |
| اتصالات جديدة تسقط تحت الضغط | dmesg -T | grep conntrack | nf_conntrack: table full, dropping packet |
| كل طلب خارجي يتأخر بالقدر نفسه | dig example.com +noall +stats | Query time مرتفع أو مهلة، وSERVER الذي لا يرد |
| Container بطيء والسيرفر فارغ | docker stats وملفات cgroup | الاستهلاك عند الحد، وnr_throttled في cpu.stat |
أخطاء شائعة
- إعادة التشغيل قبل القياس: إعادة تشغيل الخدمة تمسح الدليل، فالعمليات العالقة والاتصالات والطوابير تختفي معها، لذلك خذ دقيقة الأوامر العشرة أولاً واحفظ مخرجها، ثم قرر.
- الحكم من رقم واحد: متوسط الحمل وحده، أو
%utilوحده، أوfreeوحده، كل منها يخدعك كما رأينا، والسبب أن كل رقم منها يجمع حالات مختلفة. - قراءة السطر الأول من vmstat وiostat: هو متوسط منذ تشغيل السيرفر، وقد يكون قبل أسابيع.
- الوثوق بأرقام الأدوات داخل ال Container:
freeوtopوnprocداخل ال Container تعرض السيرفر، والحد في cgroup وفيdocker stats. - تفريغ ال Page Cache لكي تبدو الذاكرة فارغة: تبطئ السيرفر ولا تحل شيئاً.
- حذف ملف سجل مفتوح بالأمر rm: المساحة لا تعود حتى تغلقه العملية، فإما أن تعيد تشغيلها، أو تفرغ الملف بدل حذفه.
- أوامر الحذف الشاملة في Docker:
docker system pruneوdocker volume pruneتحذفان ما لا تعرفه، فاحذف بالاسم. - توسيع مجال المنافذ بدل إصلاح السبب: نفاد المنافذ علامة على اتصالات لا يعاد استخدامها، والمجال الأوسع يؤخر المشكلة فقط.
قائمة تحقق
- الحزمة
sysstatمثبتة على كل سيرفر قبل أن تحتاجها، فلا وقت للتثبيت أثناء المشكلة. - تعرف عدد المعالجات وحجم الذاكرة وزمن القرص المعتاد لكل سيرفر، لكي تقارن به.
- تدوير سجلات Docker مضبوط في
daemon.json، وحد journald معروف. - كل Container له حد ذاكرة، والتطبيق يعرف عدد عماله صراحة.
- تعرف كيف تقرأ
dmesgعلى سيرفرك، بsudoأوjournalctl -k. - تحفظ مخرج الأوامر العشرة في ملاحظات التحقيق بوقتها، كما في دليل استكشاف الأخطاء.
- عندك مراقبة ترسم هذه الأرقام نفسها مع الوقت، لأن الأوامر تريك اللحظة الحالية فقط.
حذف المختبر
احذف ما أنشأته في هذا المقال بالاسم فقط، ولا تستخدم أوامر الحذف الشاملة لما ذكرناه في قسم السجلات:
docker rm -f lab-perf-obs lab-perf-cpu lab-perf-io lab-perf-sync lab-perf-swap lab-perf-oom lab-perf-chatty lab-perf-chatty-rot
docker volume rm lab-perf-data
docker image rm lab-perf-tools:1الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- الأوامر العشرة بالترتيب:
uptimeوdmesg -T | tailوvmstat 1وmpstat -P ALL 1وpidstat 1وiostat -xz 1وfree -mوsar -n DEV 1وsar -n TCP,ETCP 1وtop، تعطيك في دقيقة صورة كل مورد قبل أن تغير أي شيء. - متوسط الحمل في Linux يشمل العمليات المحجوزة في انتظار القرص، فقارنه بعدد المعالجات وانظر في
rوbوwaقبل أن تحكم. - ال Page Cache ليست ذاكرة مستهلكة، والرقم الذي تراقبه هو
available، وSwap يصبح مشكلة عندما تتحركsiوsoباستمرار. - OOM Killer يقتل العملية بالإشارة SIGKILL دون رسالة في سجلها، والدليل في
dmesg، ورمز الخروج 137 معOOMKilled=trueفي Docker. - القرص له أربع مشكلات: المساحة بالأمر
df -h، وال inodes بالأمرdf -i، والملف المحذوف المفتوح بالأمرlsof +L1، والضغط بالأمرiostatمعawaitوaqu-szوليس%utilوحده. - في الشبكة: TIME_WAIT طبيعية وتحجز منفذاً 60 ثانية، وCLOSE-WAIT المتزايدة خطأ في التطبيق، و
retrans/sيكشف الحزم الضائعة. - الأدوات داخل ال Container تعرض أرقام السيرفر، والحد الحقيقي في cgroup وفي
docker stats.
وفي هذه السلسلة يبدأ التحقيق من الطريقة في دليل استكشاف الأخطاء، ثم يأتي هذا المقال لفحص السيرفر، ثم مقال تتبع الطلب البطيء للتعمق داخل التطبيق:
دليلاستكشاف الأخطاء وتحليل الأداء للمبتدئين: من العرض إلى السببعندما يبطئ الموقع أو يعطي 502، هذه طريقة منظمة توصلك من العرض إلى السبب قبل أن تخمن أو تشتري سيرفراً أكبر.
دليلتتبع الطلب البطيء من المتصفح إلى قاعدة البياناتكيف تأخذ طلباً واحداً بطيئاً أو فاشلاً وتعرف في أي طبقة ضاع وقته، بقياسات حقيقية في كل طبقة بدل التخمين.
دليلالتزامن Concurrency والخيوط: ماذا يعني لسيرفرك وتطبيقاتكلماذا يكون المعالج عند 100% والتطبيق بطيئاً، ولا تسرع الخيوط كود Python، وكيف تضبط عدد العمال وحدود ال Containers.
دليلتثبيت Docker Engine وCompose على Ubuntuكيف تثبت Docker على Ubuntu من المستودع الرسمي، وتضبطه من البداية حتى لا يمتلئ القرص أو تنكشف منافذك للإنترنت.
دليلPostgreSQL للجداول الضخمة: عشرات الملايين من الصفوفماذا تفعل عندما يصل جدول الطلبات إلى عشرات الملايين من الصفوف وتبطئ لوحة التحكم ويمتلئ القرص، وكيف تقيس قبل أن تصلح.سجل التحديثات
- أكتوبر 2026: كتابة المقال مستوحى من مقال Netflix وBrendan Gregg سنة 2015، وتنفيذ كل التجارب على ubuntu:24.04 مع sysstat 12.6.1 وstress-ng 0.17.06 وfio 3.36 وDocker Engine 29.8.0.