الأدلة التقنية

تحليل أداء سيرفر Linux في 60 ثانية: ماذا تكتب أولاً؟

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

تحليل أداء سيرفر Linux في 60 ثانية: ماذا تكتب أولاً؟

تصلك رسالة أن الموقع بطيء، فتدخل إلى السيرفر عبر 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 التي شرحناها في دليل استكشاف الأخطاء. والصورة التالية تبين الأوامر العشرة بالترتيب، وكل لون يمثل المورد الذي ينظر إليه الأمر:

عشر بطاقات مرقمة من اليمين إلى اليسار في صفين: uptime لمتوسط الحمل، وdmesg لأخطاء النواة، وvmstat للطابور والذاكرة المبدلة، وmpstat لكل نواة، وpidstat لكل عملية، وiostat لزمن القرص، وfree للذاكرة، وsar -n DEV للواجهات، وsar -n TCP,ETCP لاتصالات TCP، وtop للصورة الكاملة، وكل بطاقة ملونة بلون المورد
الأوامر العشرة بالترتيب، ولون كل بطاقة هو المورد الذي يفحصه الأمر

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

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 بأسماء مختلفة قليلاً:

شريط يمثل ثانية واحدة من نواة مقسم إلى خمسة أجزاء: us لكود التطبيق، وsy لنواة النظام، وwa لانتظار القرص، وst لوقت أخذه جهاز افتراضي آخر، وid للفراغ، وتحته لوحتان من ثماني أنوية: في الأولى كل الأنوية ممتلئة، وفي الثانية نواة واحدة عند 100% والباقي فارغ
وقت المعالج في خمسة أجزاء، والفرق بين خيط واحد ساخن ونقص حقيقي في المعالج
  • وقت المستخدم 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، فهل أحتاج ذاكرة أكبر؟ وغالب الظن أن الإجابة لا، والسبب في الصورة التالية:

شريط ذاكرة فيه ثلاثة أجزاء: used لذاكرة التطبيقات، وbuff/cache لملفات قرئت حديثاً، وfree، وتحته قوس أخضر يجمع free وجزءاً من cache باسم available، وبجانبه قرص Swap بسهمين so وsi، وفي الأسفل ثلاث مراحل: طبيعي، ثم ضغط تتخلى فيه النواة عن ال Cache وتبدأ Swap، ثم نفاد الذاكرة وتدخل OOM Killer
العمود available هو ما يهمك، وال Page Cache تتراجع عندما يحتاج برنامج الذاكرة

الذاكرة المؤقتة للملفات ليست ذاكرة مستهلكة

كل ملف يقرؤه السيرفر يبقى في الذاكرة بعد قراءته، في ما يسمى ذاكرة الصفحات المؤقتة 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 والملفات المحذوفة والضغط

عندما يقول أحد إن القرص فيه مشكلة فهو يقصد واحدة من أربع مشكلات مختلفة تماماً، ولكل واحدة منها أمر يكشفها، والصورة التالية تبين الأربع:

أربع لوحات: الأولى قرص مساحته ممتلئة 100% مع الرسالة No space left on device والأمر df -h، والثانية جدول ملفات صغيرة ممتلئ مع Use 0% وIUse 100% والأمر df -i، والثالثة تطبيق يمسك ملفاً محذوفاً فيقول df إن 15M مستخدمة وdu يقول صفر والأمر lsof +L1، والرابعة طابور طلبات أمام القرص مع util 96 وaqu-sz 50 والأمر iostat -xz 1
المساحة، وال 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 التي ستراها في مخرجها، وأين تتراكم:

سلسلتان من حالات TCP: عند الكلاينت الذي يغلق أولاً SYN-SENT ثم ESTAB ثم FIN-WAIT-1 ثم FIN-WAIT-2 ثم TIME-WAIT الطويلة لمدة 60 ثانية، وعند السيرفر LISTEN ثم SYN-RECV ثم ESTAB ثم CLOSE-WAIT الحمراء ثم LAST-ACK، وتحتهما شبكة مربعات تمثل المنافذ المحلية أغلبها محجوز في TIME-WAIT، وقائمة أوامر ss وsar وconntrack
الحالة TIME-WAIT تحجز منفذاً محلياً 60 ثانية، والحالة CLOSE-WAIT المتزايدة خطأ في التطبيق

الاتصالات وحالاتها مع 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 حده 256m ومعالج واحد: لوحة حمراء فيها free وnproc وuptime وtop تعرض أرقام السيرفر مثل total 32036 و8 معالجات، ولوحة خضراء فيها ملفات cgroup مثل memory.limit_in_bytes بقيمة 268435456 وcpu.max بقيمة 100000 100000، وخارج ال Container لوحة سوداء فيها docker stats بقيمة 195.6MiB / 256MiB وdocker inspect مع OOMKilled=true وExitCode=137 وسطر dmesg، وثلاث قواعد مرقمة في الأسفل
الأدوات المعتادة تقرأ /proc فترى السيرفر، والحد الحقيقي في ملفات cgroup وفي docker stats

وهذا يعني ثلاثة أشياء عند التحقيق:

  1. عندما تشك في Container فابدأ من السيرفر بالأمر docker stats، فهو يعرض لكل Container الاستهلاك مقابل الحد، مع ملاحظة أن Docker يطرح ذاكرة ال Cache من الاستهلاك الذي يعرضه كما في توثيق docker stats، ولا يذكر Swap كما رأينا في تجربة Swap.
  2. داخل ال Container اقرأ ملفات cgroup مباشرة: في الإصدار الثاني memory.max وmemory.current وmemory.events الذي يعد مرات الوصول إلى الحد ومرات القتل، وcpu.max وcpu.stat الذي يعد مرات الخنق Throttling، وتعرف الإصدار بالأمر stat -fc %T /sys/fs/cgroup/، وقد شرحنا خنق المعالج بتجربة في مقال المعالج والخيوط.
  3. التطبيقات التي تحسب عدد عمالها من عدد المعالجات أو من حجم الذاكرة قد ترى أرقام السيرفر فتبدأ عمالاً أكثر مما يتحمله الحد، فتخنق أو تقتل. وهناك أداة اسمها 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 devicedf -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 1r_await وw_await مقارنة بالمعتاد، وaqu-sz يكبر، وf_await مع Flush كثير، ولا تعتمد على %util وحده
اتصالات تفشل بخطأ Cannot assign requested addressss -s وss -tan state time-wait | wc -lآلاف الاتصالات في TIME_WAIT إلى الوجهة نفسها، والحل مجمع اتصالات
اتصالات معلقة تزيدss -tanp state close-waitالعملية التي لا تغلق اتصالاتها
بطء متقطع في الشبكةsar -n TCP,ETCP 1 وss -tiretrans/s فوق الصفر، ومع أي عنوان
اتصالات جديدة تسقط تحت الضغطdmesg -T | grep conntracknf_conntrack: table full, dropping packet
كل طلب خارجي يتأخر بالقدر نفسهdig example.com +noall +statsQuery 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.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.