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

# تحليل أداء سيرفر Linux في 60 ثانية: ماذا تكتب أولاً؟
- URL: https://arabroot.io/articles/تحليل-اداء-سيرفر-linux-في-60-ثانية/
- Published: 2026-10-05T18:15:00.000Z
- Updated: 2026-10-09T09:42:44.000Z
- Description: ماذا تكتب أول ما تدخل إلى سيرفر بطيء، وكيف تعرف خلال دقيقة هل المشكلة في المعالج أم الذاكرة أم القرص أم الشبكة.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, DevOps والتشغيل, لينكس والسيرفرات, DevOps وCI/CD, الاستضافة الذاتية

تصلك رسالة أن الموقع بطيء، فتدخل إلى السيرفر عبر SSH وأمامك شاشة سوداء ومؤشر ينتظر، والسؤال الذي يحدد ما سيحدث في الساعة التالية هو: ما أول أمر تكتبه؟ والإجابة التي يصل إليها أغلبنا بالتجربة هي `top`، ثم نظرة سريعة على أعلى عملية، ثم إعادة تشغيل الخدمة على أمل أن تختفي المشكلة. وفي سنة 2015 نشر فريق هندسة الأداء في Netflix، بقيادة Brendan Gregg، مقالاً بعنوان [Linux Performance Analysis in 60,000 Milliseconds](https://netflixtechblog.com/linux-performance-analysis-in-60-000-milliseconds-accc10403c55?ref=arabroot.io) يقترح عشرة أوامر بترتيب محدد تعطيك خلال دقيقة صورة عن كل مورد في السيرفر، فتعرف أين تبحث قبل أن تغير أي شيء.

وهذا المقال مبني على فكرة ذلك المقال وترتيبه، ولكن النص والأمثلة من عندنا، وكل مخرج فيه من تجربة حقيقية صنعنا فيها المشكلة بأنفسنا ثم أمسكناها بالأمر المناسب. أما طريقة التحقيق نفسها، أي كيف تصف العرض وتضع فرضية وتقيسها، وقوائم USE وRED، والحالات الشائعة من أولها إلى إصلاحها، فمكانها [دليل استكشاف الأخطاء وتحليل الأداء](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، وهذا المقال هو الجزء الذي يفحص سيرفر 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](https://sysstat.github.io/?ref=arabroot.io) (أي `iostat` و`mpstat` و`pidstat` و`sar`)، وأداة توليد الحمل [stress-ng](https://github.com/ColinIanKing/stress-ng?ref=arabroot.io)، وأداة اختبار القرص [fio](https://fio.readthedocs.io/en/latest/?ref=arabroot.io)، وبقية الأدوات التي سنستخدمها، عبر ملف Dockerfile التالي:

```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/*
```

```bash
docker build -t lab-perf-tools:1 .
docker volume create lab-perf-data
```

وعلى سيرفرك الحقيقي سوف تشغل الأوامر مباشرة بعد تثبيت الحزم بالأمر `sudo apt install sysstat`، أما في المختبر فنحتاج Container خاصاً نقيس منه السيرفر كله، ونسميه المراقب:

```bash
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 التي شرحناها في [دليل استكشاف الأخطاء](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#use-method). والصورة التالية تبين الأوامر العشرة بالترتيب، وكل لون يمثل المورد الذي ينظر إليه الأمر:

![عشر بطاقات مرقمة من اليمين إلى اليسار في صفين: uptime لمتوسط الحمل، وdmesg لأخطاء النواة، وvmstat للطابور والذاكرة المبدلة، وmpstat لكل نواة، وpidstat لكل عملية، وiostat لزمن القرص، وfree للذاكرة، وsar -n DEV للواجهات، وsar -n TCP,ETCP لاتصالات TCP، وtop للصورة الكاملة، وكل بطاقة ملونة بلون المورد](https://arabroot.io/content/images/2026/10/linux-performance-01-sequence.webp)

الأوامر العشرة بالترتيب، ولون كل بطاقة هو المورد الذي يفحصه الأمر

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

```bash
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

```bash
uptime
```

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

```text
 05:09:19 up  5:50,  0 user,  load average: 3.42, 3.36, 2.89
```

الأرقام الثلاثة هي متوسط الحمل Load Average في آخر دقيقة وخمس دقائق وخمس عشرة دقيقة، وفائدتها الأولى ليست في قيمتها وإنما في اتجاهها: إذا كان رقم الدقيقة أكبر بكثير من رقم الخمس عشرة دقيقة فالمشكلة بدأت للتو، وإذا كان العكس فالمشكلة كانت وانتهت، وربما وصلت متأخراً. ولاحظ أن هذه الأرقام تشمل نهاية تجربة سابقة على السيرفر نفسه، وهذا بالضبط ما يحدث في الواقع، فالمتوسط يحمل ذاكرة ما حدث قبل دخولك. وما يحسبه Linux في هذا المتوسط بالضبط موضوع [قسم المعالج](#load-average).

### 2\. هل اشتكت النواة من شيء؟ الأمر dmesg

```bash
dmesg -T | tail
```

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

```text
[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](https://man7.org/linux/man-pages/man1/dmesg.1.html?ref=arabroot.io) تنبه إلى أنه قد يكون غير دقيق بعد إيقاف الجهاز مؤقتاً Suspend. والرسائل هنا عادية، فهي ربط واجهات شبكة لل Containers التي شغلناها للتو. وعلى أغلب التوزيعات الحديثة تحتاج `sudo` لقراءة السجل، والسبب الإعداد [dmesg\_restrict](https://docs.kernel.org/admin-guide/sysctl/kernel.html?ref=arabroot.io#dmesg-restrict) الذي يمنع المستخدم العادي من قراءته.

### 3\. الطابور والذاكرة والمعالج في سطر واحد مع vmstat 1

```bash
vmstat 1 5
```

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

```text
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](https://man7.org/linux/man-pages/man8/vmstat.8.html?ref=arabroot.io)، لذلك تجاهله واقرأ الأسطر التي بعده.
- العمود `r` عدد العمليات الجاهزة للعمل أو التي تعمل، فإذا تجاوز عدد المعالجات باستمرار فهناك طابور على المعالج، والعمود `b` عدد العمليات المحجوزة في انتظار الإدخال والإخراج.
- العمودان `si` و`so` هما الذاكرة التي تقرأ من Swap وتكتب إليه في الثانية، والأصل أن يكونا صفراً، وأي رقم ثابت فيهما يعني أن الذاكرة لا تكفي.
- العمود `bo` يعرض كتابة على القرص بعشرات الآلاف من الكتل في الثانية، وهذه كتابة fio.
- أعمدة المعالج `us` و`sy` و`id` و`wa` و`st` نشرحها في [قسم المعالج](#cpu-time)، وهنا نرى 14% تقريباً للتطبيقات، وهو خيط واحد من ثمانية معالجات.

### 4\. كل نواة على حدة مع mpstat -P ALL 1

```bash
mpstat -P ALL 1 1
```

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

```text
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 بين نواتين، أي أن عملية واحدة تستهلك نواة كاملة، ولو كان تطبيقك هو هذه العملية لكان بطيئاً والسيرفر في نظر الرسم البياني فارغ. وهذا العرض يكشف التطبيق المحصور في خيط واحد، وشرحه موجود في [مقال المعالج والخيوط](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%84%D8%AC-%D9%88%D8%A7%D9%84%D8%AE%D9%8A%D9%88%D8%B7-%D9%88%D8%A7%D9%84%D8%AA%D8%B2%D8%A7%D9%85%D9%86-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#measure).

### 5\. من صاحب هذا الاستهلاك؟ الأمر pidstat 1

```bash
pidstat 1 1
```

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

```text
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](https://man7.org/linux/man-pages/man1/pidstat.1.html?ref=arabroot.io)، فإذا ارتفع فالمعالج أقل من حاجة العمليات الموجودة.

### 6\. هل القرص يتأخر؟ الأمر iostat -xz 1

```bash
iostat -xz 1 2
```

التقرير الأول من `iostat` متوسط منذ تشغيل السيرفر، والتقرير الثاني هو الثانية الأخيرة فقط، لذلك نعرض التقرير الثاني:

```text
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](https://man7.org/linux/man-pages/man1/iostat.1.html?ref=arabroot.io)، وهنا 32 طلباً، وهو عمق الطابور الذي طلبناه `--iodepth=32`.
- العمود `%util` يقول 102%، ومع ذلك فالقرص ليس مختنقاً، فكل كتابة أخذت 2 ms فقط. والسبب أن هذا العمود هو نسبة الوقت الذي كان فيه طلب واحد على الأقل عند القرص، والصفحة نفسها تقول إنه لا يعكس حدود الأقراص التي تخدم طلبات كثيرة بالتوازي مثل SSD وRAID، وهذا يشمل أغلب أقراص السيرفرات الافتراضية.

### 7\. كم بقي من الذاكرة فعلاً؟ الأمر free -m

```bash
free -m
```

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

```text
               total        used        free      shared  buff/cache   available
Mem:           32036        2141       25666          36        4713       29894
Swap:           8192           2        8189
```

العمود الذي يهمك هو `available` وليس `free`، والسبب نشرحه بتجربة في [قسم الذاكرة](#page-cache). وهنا السيرفر مرتاح: قرابة 29 GiB متاحة، وSwap شبه فارغ.

### 8\. حركة الشبكة على كل واجهة مع sar -n DEV 1

```bash
sar -n DEV 1 1
```

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

```text
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](https://man7.org/linux/man-pages/man1/sar.1.html?ref=arabroot.io). وتجربتنا هنا لا تستخدم الشبكة، لذلك كل القيم صفر، وهذا بحد ذاته معلومة: الشبكة ليست المشكلة.

### 9\. الاتصالات وإعادة الإرسال مع sar -n TCP,ETCP 1

```bash
sar -n TCP,ETCP 1 1
```

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

```text
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` عدد المقاطع التي أعيد إرسالها في الثانية، وارتفاعه يعني فقد حزم في الطريق أو سيرفراً لا يرد في الوقت، وسوف نصنع ذلك عمداً في [قسم الشبكة](#retransmits).

### 10\. الصورة الكاملة للتأكد مع top

```bash
top -b -n1 | head -15
```

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

```text
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](https://htop.dev/?ref=arabroot.io) فنسخة أوضح تعرض كل نواة في شريط، وهي اختيار جيد للنظر السريع.

وبعد هذه الأوامر العشرة تعرف أي مورد يستحق أن تتعمق فيه، والأقسام التالية تأخذ كل مورد وحده، وتصنع فيه المشكلة، وتريك الأمر الذي يمسكها. وقبل أن تكمل احذف الحملين بالأمر `docker rm -f lab-perf-cpu lab-perf-io`.

## المعالج: من يستهلك الوقت، وما نوع هذا الوقت؟

شرحنا في [مقال المعالج والخيوط والتزامن](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%84%D8%AC-%D9%88%D8%A7%D9%84%D8%AE%D9%8A%D9%88%D8%B7-%D9%88%D8%A7%D9%84%D8%AA%D8%B2%D8%A7%D9%85%D9%86-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/) الأنوية والخيوط والتبديل بين السياقات Context Switching وحدود المعالج في ال Containers، ولن نكرر ذلك هنا، وإنما نأخذ سؤالين يتكرران عند التحقيق: هل متوسط الحمل المرتفع يعني أن المعالج مشغول؟ وإذا كان المعالج مشغولاً، فمن يشغله؟

### متوسط الحمل يرتفع والمعالج فارغ

في Linux لا يعد متوسط الحمل العمليات التي تعمل أو تنتظر المعالج فقط، وإنما يضيف إليها العمليات المحجوزة في انتظار القرص، وهي التي تظهر بالحرف D في `ps`، أي الانتظار غير القابل للمقاطعة Uninterruptible Sleep، كما في [صفحة /proc/loadavg](https://man7.org/linux/man-pages/man5/proc%5Floadavg.5.html?ref=arabroot.io) ومقال Brendan Gregg عن [متوسط الحمل في Linux](https://www.brendangregg.com/blog/2017-08-08/linux-load-averages.html?ref=arabroot.io). ولكي نرى ذلك شغلنا ثمانية عمال يكتبون على القرص ويطلبون من النظام أن يحفظ كل كتابة فوراً بالخيار `--fsync=1`، فتبقى كل عملية محجوزة حتى يؤكد القرص الحفظ:

```bash
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
```

وبعد دقيقتين تقريباً نفذنا من المراقب:

```bash
nproc
uptime
top -b -n1 | head -4 | tail -2
```

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

```text
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:

```bash
vmstat 1 5
ps -eo state,pid,comm | awk '$1=="D"'
```

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

```text
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% والباقي فارغ](https://arabroot.io/content/images/2026/10/linux-performance-02-cpu-time.webp)

وقت المعالج في خمسة أجزاء، والفرق بين خيط واحد ساخن ونقص حقيقي في المعالج

- **وقت المستخدم User** (`us` أو `%usr`): كود تطبيقك يحسب، فابحث عن العملية في `pidstat` ثم عن الدالة التي تستهلك الوقت داخلها، وتتبع الطلب الواحد داخل التطبيق موضوع [مقال تتبع الطلب البطيء](https://arabroot.io/articles/%D8%AA%D8%AA%D8%A8%D8%B9-%D8%A7%D9%84%D8%B7%D9%84%D8%A8-%D8%A7%D9%84%D8%A8%D8%B7%D9%8A%D8%A1-%D9%85%D9%86-%D8%A7%D9%84%D9%85%D8%AA%D8%B5%D9%81%D8%AD-%D8%A7%D9%84%D9%89-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA/).
- **وقت النظام System** (`sy` أو `%sys`): النواة تعمل نيابة عن التطبيقات، مثل استدعاءات النظام System Calls وحزم الشبكة وإعداد عمليات القرص، فإذا ارتفع وحده فابحث عن تطبيق يقرأ ويكتب أجزاء صغيرة جداً، أو يفتح ويغلق اتصالات كثيرة. وقد شرحنا ما يحدث عند استدعاء النظام في [مقال Hello World من الداخل](https://arabroot.io/articles/%D8%B1%D8%AD%D9%84%D8%A9-hello-world-%D9%85%D9%86-%D9%84%D9%88%D8%AD%D8%A9-%D8%A7%D9%84%D9%85%D9%81%D8%A7%D8%AA%D9%8A%D8%AD-%D8%A7%D9%84%D9%89-%D8%A7%D9%84%D8%B4%D8%A7%D8%B4%D8%A9/#hardware).
- **انتظار القرص I/O Wait** (`wa` أو `%iowait`): المعالج لا يجد ما يعمله وهناك عمليات تنتظر القرص، فهو وقت فراغ في الحقيقة، ولكنه علامة على أن القرص هو الذي يؤخر العمل، والخطوة التالية `iostat`.
- **الوقت المسروق Steal** (`st` أو `%steal`): في السيرفر الافتراضي VPS أراد سيرفرك أن يعمل، ولكن المضيف Hypervisor أعطى النواة الفعلية لجهاز افتراضي آخر، أي لجيرانك على السيرفر الفعلي نفسه. وإذا بقي هذا الرقم مرتفعاً باستمرار فأنت تدفع لمعالج لا تحصل عليه كاملاً، وقد شرحنا كيف تتعامل مع ذلك في [دليل اختيار VPS](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D8%AE%D8%A7%D8%AF%D9%85%D8%A7-%D8%A7%D9%81%D8%AA%D8%B1%D8%A7%D8%B6%D9%8A%D8%A7-vps/#high-steal).
- **الفراغ Idle** (`id`): لا يوجد ما يعمل، والمعالج ليس مشكلتك.

والجزء الثاني من الصورة هو ما رأيناه في [مخرج mpstat](#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](https://arabroot.io/content/images/2026/10/linux-performance-03-memory.webp)

العمود available هو ما يهمك، وال Page Cache تتراجع عندما يحتاج برنامج الذاكرة

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

كل ملف يقرؤه السيرفر يبقى في الذاكرة بعد قراءته، في ما يسمى ذاكرة الصفحات المؤقتة Page Cache، حتى إذا طلبه أحد مرة ثانية جاء من الذاكرة بدل القرص، كما في [وثيقة مفاهيم الذاكرة في النواة](https://docs.kernel.org/admin-guide/mm/concepts.html?ref=arabroot.io). وهذه الذاكرة تتخلى عنها النواة فوراً عندما يحتاج برنامج إليها، فالذاكرة الفارغة في Linux ذاكرة ضائعة. والتجربة التالية تكتب ملفاً حجمه 2 GB دون أن يمر بال Cache، ثم تقرؤه:

```bash
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'
```

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

```text
               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](https://man7.org/linux/man-pages/man1/free.1.html?ref=arabroot.io)، و`available` نفسه هو القيمة `MemAvailable` من [/proc/meminfo](https://man7.org/linux/man-pages/man5/proc%5Fmeminfo.5.html?ref=arabroot.io)، وهي تقدير النواة لما يمكن أن يأخذه برنامج جديد دون أن يبدأ النظام ب Swap.
- الأمر `free` هنا يعمل داخل Container، ومع ذلك عرض ذاكرة السيرفر كله، وهذا موضوع [قسم ال Containers](#containers).

والقاعدة أن تراقب `available`، ولا تقم بتفريغ ال Cache يدوياً بالكتابة في `/proc/sys/vm/drop_caches` لكي ترتاح للرقم، والسبب أن النظام سوف يعيد قراءة الملفات نفسها من القرص بعد ذلك، فتبطئ السيرفر لتحسن شكل رقم لا قيمة له في التشخيص.

### متى يصبح Swap مشكلة؟

عندما لا تكفي الذاكرة الحقيقية تنقل النواة صفحات من ذاكرة البرامج إلى مساحة Swap على القرص، ثم تعيدها عندما يحتاجها البرنامج، والقرص أبطأ من الذاكرة بأضعاف كثيرة، لذلك يبدأ السيرفر بالبطء قبل أن يتوقف أي شيء. ولكي نرى ذلك شغلنا برنامجاً يحجز 640 MB ويلمسها باستمرار داخل Container حده 256 MB من الذاكرة، مع السماح له ب Swap حتى 1 GB كما في [توثيق قيود الموارد في Docker](https://docs.docker.com/engine/containers/resource%5Fconstraints/?ref=arabroot.io):

```bash
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)
"
```

ثم نفذنا من المراقب:

```bash
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
```

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

```text
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`:

```text
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](https://docs.kernel.org/admin-guide/cgroup-v2.html?ref=arabroot.io). احذف التجربة بالأمر `docker rm -f lab-perf-swap`.

### قاتل العمليات OOM Killer وكيف تقرأ سجله

عندما تنفد الذاكرة وال Swap معاً، أو يصل Container إلى حده، لا يبقى أمام النواة إلا أن تختار عملية وتقتلها لتحرر ذاكرتها، وهذا عمل OOM Killer، واختصار OOM هو Out Of Memory. وهي تختار العملية بحسب درجة تقريبية من 0 إلى 1000 تعتمد على نسبة ما تستخدمه العملية من الذاكرة المسموحة، ويمكن تعديلها لكل عملية بالملف `oom_score_adj` كما في [صفحة oom\_score\_adj](https://man7.org/linux/man-pages/man5/proc%5Fpid%5Foom%5Fscore%5Fadj.5.html?ref=arabroot.io). والتجربة التالية تشغل برنامجاً يحجز 16 MB كل نصف ثانية داخل Container حده 256 MB دون Swap، لأن `--memory-swap` يساوي `--memory`:

```bash
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)
"
```

وبعد أربع ثوان، ثم بعد ثمان ثوان أخرى، نفذنا:

```bash
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
```

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

```text
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](https://man7.org/linux/man-pages/man1/bash.1.html?ref=arabroot.io): العملية التي تنتهي بإشارة رقمها N يكون رمز خروجها 128+N. لذلك عندما ترى Container يعيد التشغيل ورمزه 137 فأول ما تفحصه هو `OOMKilled`، ولا تحكم من الرقم 137 وحده، والسبب أن الأمر `docker kill` يرسل الإشارة نفسها. أما سجل النواة فيحكي القصة كاملة:

```bash
dmesg -T | grep -E "memory: usage|memory\+swap: usage|Memory cgroup out|invoked oom-killer" | tail -4
```

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

```text
[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](https://github.com/torvalds/linux/blob/master/mm/oom%5Fkill.c?ref=arabroot.io) في النواة. والقيمة `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](https://arabroot.io/content/images/2026/10/linux-performance-04-disk.webp)

المساحة، وال inodes، والملف المحذوف المفتوح، وضغط الإدخال والإخراج، ولكل منها أمر

ولكي نصنع الحالات الثلاث الأولى دون أن نملأ قرص السيرفر نستخدم قرصاً في الذاكرة من نوع tmpfs حجمه 20 MB وفيه 2000 inode فقط:

```bash
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
'
```

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

```text
### 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](https://man7.org/linux/man-pages/man1/du.1.html?ref=arabroot.io)، ثم تنزل مستوى بعد مستوى. وإذا أردت أن تتنقل بين المجلدات بالأسهم فالأداة [ncdu](https://dev.yorhel.nl/ncdu?ref=arabroot.io) تفعل ذلك بالأمر `ncdu -x /`. أما إذا كان القرص نفسه صغيراً على ما تحتاجه فقد شرحنا زيادته في [دليل توسيع قرص النظام LVM](https://arabroot.io/articles/%D8%AA%D9%88%D8%B3%D9%8A%D8%B9-%D9%82%D8%B1%D8%B5-%D8%A7%D9%84%D9%86%D8%B8%D8%A7%D9%85-lvm-%D9%81%D9%8A-ubuntu/).

### ال 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](https://man7.org/linux/man-pages/man8/lsof.8.html?ref=arabroot.io)، فيظهر الملف مع كلمة `(deleted)` واسم العملية التي تمسكه.

والحل الأنظف أن تعيد تشغيل الخدمة التي تمسك الملف، أو أن تطلب منها إعادة فتح ملف السجل إذا كانت تدعم ذلك. وإذا لم تستطع إعادة تشغيلها الآن فيمكنك تفريغ الملف من خلال واصف الملف File Descriptor في `/proc`، والتجربة التالية تبين ذلك:

```bash
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'
```

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

```text
tmpfs            20M   15M  5.0M  75% /data
tmpfs            20M     0   20M   0% /data
```

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

### احذف ما تعرفه فقط من سجلات Docker وjournald

أكثر ما يملأ أقراص السيرفرات الصغيرة بصمت هو السجلات، فمشغل السجلات الافتراضي في Docker هو `json-file`، والقيمة الافتراضية لحجم الملف `max-size` هي `-1`، أي بلا حد، كما في [توثيق json-file](https://docs.docker.com/engine/logging/drivers/json-file/?ref=arabroot.io). والتجربة التالية تشغل Containerين يكتب كل منهما ثلاثة ملايين سطر، الأول بالإعداد الافتراضي، والثاني مع تدوير السجلات Log Rotation:

```bash
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'
```

وبعد خمس وعشرين ثانية نقيس ملفات السجل على السيرفر:

```bash
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
```

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

```text
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](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-docker-%D8%B9%D9%84%D9%89-ubuntu/#daemon-json)، مع ملاحظة أن الإعداد الجديد يطبق على ال Containers التي تنشأ بعده فقط. احذف التجربة بالأمر `docker rm -f lab-perf-chatty lab-perf-chatty-rot`.

وسجلات النظام في journald لها حد افتراضي، فالقيمة `SystemMaxUse` افتراضياً 10% من حجم نظام الملفات بحد أقصى 4G كما في [صفحة journald.conf](https://man7.org/linux/man-pages/man5/journald.conf.5.html?ref=arabroot.io)، وتعرف حجمها الحالي بالأمر `journalctl --disk-usage`، وتقلصها إلى حجم محدد بالأمر `sudo journalctl --vacuum-size=500M` الذي يحذف أقدم الملفات المؤرشفة كما في [صفحة journalctl](https://man7.org/linux/man-pages/man1/journalctl.1.html?ref=arabroot.io).

🛑

لا تقم بتشغيل `docker system prune` أو `docker volume prune` لتحرير المساحة على سيرفر إنتاجي، والسبب أنهما يحذفان كل ما لا يستخدمه Container يعمل في هذه اللحظة، وقد يكون من بينها Volume فيه قاعدة بيانات خدمة متوقفة مؤقتاً، أو Docker Image يحتاجها زميلك. احذف ما تعرفه فقط وبالاسم: ملف سجل تعرف صاحبه، أو Docker Image قديمة لإصدار تعرف أنك لن تعود إليه، أو نسخة احتياطية محلية نقلت إلى مكان آخر.

### القرص مشغول: متى يكون ضغط الإدخال والإخراج حقيقياً؟

الحالة الرابعة لا علاقة لها بالمساحة، فالقرص فيه مكان كاف ولكن كل طلب ينتظر دوره. وقد رأينا في [مخرج iostat](#iostat) أن `%util` قد يقول 100% والقرص يرد في 2 ms، لذلك لا تحكم بهذا العمود وحده على الأقراص الحديثة، وإنما انظر في ثلاثة أشياء معاً:

- **زمن الطلب** في `r_await` و`w_await`: قارنه بما يعطيه القرص عادة، وأفضل مرجع هو قياس fio الذي تأخذه في اليوم الأول كما في [دليل اختيار VPS](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D8%AE%D8%A7%D8%AF%D9%85%D8%A7-%D8%A7%D9%81%D8%AA%D8%B1%D8%A7%D8%B6%D9%8A%D8%A7-vps/#sizing-disk)، فإذا تضاعف أضعافاً فالقرص متأخر.
- **طول الطابور** في `aqu-sz`: طابور يكبر مع زمن يكبر هو علامة التشبع Saturation.
- **من ينتظر**: العمود `b` في `vmstat`، والعمليات في الحالة D، والأمر `pidstat -d 1` الذي يعرض لكل عملية ما تقرؤه وتكتبه في الثانية.

وفي تجربة الكتابة مع `--fsync=1` التي استخدمناها في [قسم متوسط الحمل](#load-average) كان التقرير الثاني من `iostat -xz 1 2` كما يلي:

```text
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` يعرض العمال الثمانية، وهذا الجزء الأخير من مخرجه:

```text
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 للجداول الضخمة](https://arabroot.io/articles/postgresql-%D9%84%D9%84%D8%AC%D8%AF%D8%A7%D9%88%D9%84-%D8%A7%D9%84%D8%B6%D8%AE%D9%85%D8%A9/#measure). احذف ملفات التجربة لاحقاً مع ال Volume في [قسم حذف المختبر](#cleanup).

## الشبكة: حالات الاتصال والمنافذ وإعادة الإرسال

مشكلات الشبكة على السيرفر الواحد نادراً ما تكون في سرعة المنفذ، وأغلبها في الاتصالات نفسها: اتصالات كثيرة جداً، أو اتصالات لا تغلق، أو حزم تضيع في الطريق. والأداة الأساسية هنا هي [ss](https://man7.org/linux/man-pages/man8/ss.8.html?ref=arabroot.io)، وهي بديل `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](https://arabroot.io/content/images/2026/10/linux-performance-05-sockets.webp)

الحالة TIME-WAIT تحجز منفذاً محلياً 60 ثانية، والحالة CLOSE-WAIT المتزايدة خطأ في التطبيق

### الاتصالات وحالاتها مع ss

نحتاج في هذا القسم برنامجاً صغيراً فيه سيرفر وكلاينت معاً، يفتح الكلاينت عدداً من الاتصالات القصيرة واحداً بعد الآخر ويغلق كل اتصال بعد الرد، والسيرفر ينتظر حتى يغلق الكلاينت أولاً، وهذا الملف `net.py`:

```python
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)
```

ثم نشغله بعشرين ألف اتصال، وبعد ثانيتين نقيس معدل الاتصالات وحالاتها:

```bash
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`:

```text
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](https://github.com/torvalds/linux/blob/master/include/net/tcp.h?ref=arabroot.io)، والغرض منها أن لا تختلط حزمة متأخرة من اتصال قديم باتصال جديد على المنفذ نفسه. والإعداد `tcp_fin_timeout` الذي يذكر كثيراً في هذا السياق لا يغير هذه المدة، وإنما يخص الحالة FIN\_WAIT\_2 كما في [وثيقة ip-sysctl](https://docs.kernel.org/networking/ip-sysctl.html?ref=arabroot.io).

وآلاف الاتصالات في TIME\_WAIT ليست مشكلة بحد ذاتها، أما الحالة التي تستحق القلق فهي CLOSE-WAIT إذا كانت تزيد مع الوقت، والسبب أنها تعني أن الطرف الآخر أغلق الاتصال وتطبيقك لم يغلق جهته بعد، أي أن هناك اتصالات تسربت في الكود، وتعرف صاحبها بالأمر `ss -tanp state close-wait` الذي يعرض اسم العملية مع كل اتصال.

### نفاد المنافذ المحلية

كل اتصال يفتحه السيرفر إلى عنوان ومنفذ محددين يأخذ منفذاً محلياً من مجال اسمه `ip_local_port_range`، والقيمة الافتراضية من 32768 إلى 60999 كما في [وثيقة ip-sysctl](https://docs.kernel.org/networking/ip-sysctl.html?ref=arabroot.io)، أي قرابة 28 ألف منفذ لكل وجهة. فإذا فتح تطبيقك اتصالاً جديداً لكل طلب إلى قاعدة البيانات أو إلى API، وكل اتصال يبقى 60 ثانية في TIME\_WAIT، فسوف تنفد المنافذ عند قرابة 470 اتصالاً جديداً في الثانية. ولكي نرى الخطأ دون انتظار نضيق المجال داخل Container إلى مئة منفذ:

```bash
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'
```

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

```text
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 والتطبيق في [مقال تجميع الاتصالات](https://arabroot.io/articles/%D8%AA%D8%AC%D9%85%D9%8A%D8%B9-%D8%A7%D9%84%D8%A7%D8%AA%D8%B5%D8%A7%D9%84%D8%A7%D8%AA-connection-pooling-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#nginx-why).

### إعادة الإرسال: حزم تضيع في الطريق

عندما تضيع حزمة TCP في الطريق ينتظر المرسل مدة ثم يعيد إرسالها، وهذا الانتظار يظهر للمستخدم كبطء متقطع لا تفسير له في التطبيق. ولكي نصنعه أسقطنا 5% من الحزم الواردة بقاعدة iptables عشوائية داخل Container، ثم شغلنا البرنامج نفسه:

```bash
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`:

```text
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](https://github.com/torvalds/linux/blob/master/net/netfilter/nf%5Fconntrack%5Fcore.c?ref=arabroot.io). وتعرف العدد الحالي والحد بالأمر `sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max`، أما كيف يعمل هذا الجدول ولماذا يمتلئ فقد شرحناه في [مقال NAT](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%88-nat-%D8%B4%D8%B1%D8%AD-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#conntrack-full)، والإعدادات في [وثيقة nf\_conntrack-sysctl](https://docs.kernel.org/networking/nf%5Fconntrack-sysctl.html?ref=arabroot.io).

### فحص سريع لبطء DNS

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

```bash
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'
```

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

```text
## 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](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-dns-%D9%88%D8%B3%D8%AC%D9%84%D8%A7%D8%AA%D9%87-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#cache-ttl).

## لماذا ترى أرقام السيرفر من داخل ال Container؟

ال Container ليس جهازاً افتراضياً، وإنما عمليات عادية على نواة السيرفر نفسها، تفصلها عن غيرها مساحات الأسماء Namespaces، وتحدد مواردها مجموعات التحكم cgroups كما في [صفحة cgroups](https://man7.org/linux/man-pages/man7/cgroups.7.html?ref=arabroot.io)، وقد شرحنا الفرق بينهما وبين الجهاز الافتراضي في [مقال مصطلحات الاستضافة الذاتية](https://arabroot.io/articles/%D9%85%D8%B5%D8%B7%D9%84%D8%AD%D8%A7%D8%AA-%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B6%D8%A7%D9%81%D8%A9-%D8%A7%D9%84%D8%B0%D8%A7%D8%AA%D9%8A%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#virtualization). والنتيجة العملية أن ملفات مثل `/proc/meminfo` و`/proc/stat` و`/proc/loadavg` تعرض أرقام السيرفر كله حتى من داخل ال Container، والتجربة التالية تبين ذلك:

```bash
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'
```

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

```text
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، وثلاث قواعد مرقمة في الأسفل](https://arabroot.io/content/images/2026/10/linux-performance-06-container-view.webp)

الأدوات المعتادة تقرأ /proc فترى السيرفر، والحد الحقيقي في ملفات cgroup وفي docker stats

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

1. عندما تشك في Container فابدأ من السيرفر بالأمر `docker stats`، فهو يعرض لكل Container الاستهلاك مقابل الحد، مع ملاحظة أن Docker يطرح ذاكرة ال Cache من الاستهلاك الذي يعرضه كما في [توثيق docker stats](https://docs.docker.com/reference/cli/docker/container/stats/?ref=arabroot.io)، ولا يذكر Swap كما رأينا في [تجربة Swap](#swap).
2. داخل ال Container اقرأ ملفات cgroup مباشرة: في الإصدار الثاني `memory.max` و`memory.current` و`memory.events` الذي يعد مرات الوصول إلى الحد ومرات القتل، و`cpu.max` و`cpu.stat` الذي يعد مرات الخنق Throttling، وتعرف الإصدار بالأمر `stat -fc %T /sys/fs/cgroup/`، وقد شرحنا خنق المعالج بتجربة في [مقال المعالج والخيوط](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%84%D8%AC-%D9%88%D8%A7%D9%84%D8%AE%D9%8A%D9%88%D8%B7-%D9%88%D8%A7%D9%84%D8%AA%D8%B2%D8%A7%D9%85%D9%86-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#containers).
3. التطبيقات التي تحسب عدد عمالها من عدد المعالجات أو من حجم الذاكرة قد ترى أرقام السيرفر فتبدأ عمالاً أكثر مما يتحمله الحد، فتخنق أو تقتل. وهناك أداة اسمها [LXCFS](https://github.com/lxc/lxcfs?ref=arabroot.io) تعرض ملفات `/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`.
- تحفظ مخرج الأوامر العشرة في ملاحظات التحقيق بوقتها، كما في [دليل استكشاف الأخطاء](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#notes).
- عندك مراقبة ترسم هذه الأرقام نفسها مع الوقت، لأن الأوامر تريك اللحظة الحالية فقط.

## حذف المختبر

احذف ما أنشأته في هذا المقال بالاسم فقط، ولا تستخدم أوامر الحذف الشاملة لما ذكرناه في [قسم السجلات](#logs):

```bash
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`.

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

[![](https://arabroot.io/content/images/2026/10/troubleshooting-guide-cover-cover.webp)دليلاستكشاف الأخطاء وتحليل الأداء للمبتدئين: من العرض إلى السببعندما يبطئ الموقع أو يعطي 502، هذه طريقة منظمة توصلك من العرض إلى السبب قبل أن تخمن أو تشتري سيرفراً أكبر.](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/slow-requests-cover-cover.webp)دليلتتبع الطلب البطيء من المتصفح إلى قاعدة البياناتكيف تأخذ طلباً واحداً بطيئاً أو فاشلاً وتعرف في أي طبقة ضاع وقته، بقياسات حقيقية في كل طبقة بدل التخمين.](https://arabroot.io/articles/%D8%AA%D8%AA%D8%A8%D8%B9-%D8%A7%D9%84%D8%B7%D9%84%D8%A8-%D8%A7%D9%84%D8%A8%D8%B7%D9%8A%D8%A1-%D9%85%D9%86-%D8%A7%D9%84%D9%85%D8%AA%D8%B5%D9%81%D8%AD-%D8%A7%D9%84%D9%89-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/cpu-concurrency-cover-cover.webp)دليلالتزامن Concurrency والخيوط: ماذا يعني لسيرفرك وتطبيقاتكلماذا يكون المعالج عند 100% والتطبيق بطيئاً، ولا تسرع الخيوط كود Python، وكيف تضبط عدد العمال وحدود ال Containers.](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%84%D8%AC-%D9%88%D8%A7%D9%84%D8%AE%D9%8A%D9%88%D8%B7-%D9%88%D8%A7%D9%84%D8%AA%D8%B2%D8%A7%D9%85%D9%86-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/docker-cover-cover-1.webp)دليلتثبيت Docker Engine وCompose على Ubuntuكيف تثبت Docker على Ubuntu من المستودع الرسمي، وتضبطه من البداية حتى لا يمتلئ القرص أو تنكشف منافذك للإنترنت.](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-docker-%D8%B9%D9%84%D9%89-ubuntu/)[![](https://arabroot.io/content/images/2026/10/postgres-large-tables-cover-cover.webp)دليلPostgreSQL للجداول الضخمة: عشرات الملايين من الصفوفماذا تفعل عندما يصل جدول الطلبات إلى عشرات الملايين من الصفوف وتبطئ لوحة التحكم ويمتلئ القرص، وكيف تقيس قبل أن تصلح.](https://arabroot.io/articles/postgresql-%D9%84%D9%84%D8%AC%D8%AF%D8%A7%D9%88%D9%84-%D8%A7%D9%84%D8%B6%D8%AE%D9%85%D8%A9/)

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

- أكتوبر 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.