من الـ OOM Killer الذي يأكل الذاكرة إلى الـ Log Storm الذي يغرق السيرفر، هذه هي الأخطاء الحقيقية التي واجهتها مع Docker في بيئات الإنتاج وكيف حللتها دون أن أفقد شعرة أخرى من رأسي.
في أحد أيام الجمعة، تلقيت مكالمة طوارئ من فريق العمليات: "السيرفرات كلها بيعلق، الـ CPU عند ١٠٠٪، والعملاء بيتصلوا". فتحت الـ Dashboard بتوتر، وشاهدت ٣٠ حاوية Docker تتنافس على نفس الـ CPU core، وكل واحدة تحاول كتابة ٥٠٠ ميجابايت من الـ Logs في الثانية. المشكلة؟ لم أكن أول مطور يقع في هذا الفخ، لكنني أول واحد قرر أن يكتب عنه قبل أن يفقد وظيفته.
Docker يبدو وكأنه حل سحري: "اكتب مرة، شغل في أي مكان". لكن الحقيقة التي لا يخبرك بها أحد هي أن بيئات الإنتاج ليست مجرد بيئات تطوير مكبّرة. هي بيئات قاسية، مليئة بالـ Edge Cases التي لا تظهر إلا عندما يكون هناك ١٠ آلاف مستخدم متزامن، و٥٠٠ حاوية تعمل في نفس الوقت، والـ Disk I/O عند الحد الأقصى. في هذا المقال، سأفكك لك الأخطاء التي واجهتها في الإنتاج، ليس من منظور نظري، بل من منظور الـ Kernel والـ Memory والـ CPU نفسه.
في بيئات التطوير، نكتب عادة docker run بدون أي حدود للذاكرة. "الجهاز عندي ٣٢ جيجا، ما في مشكلة". لكن في الإنتاج، عندما يكون لديك ٢٠ حاوية تعمل على سيرفر بـ ١٦ جيجا، تبدأ المشاكل. الـ Linux Kernel لديه آلية اسمها Out Of Memory Killer (OOM Killer)، وهي آلية ذكية جداً ولكنها قاسية: عندما تصل الذاكرة إلى حد معين، تبدأ تقتل العمليات عشوائياً تقريباً لتحرير الذاكرة.
المشكلة الأكبر هي أن الـ OOM Killer لا ينظر إلى اسم العملية أو أهميتها. هو ينظر إلى الـ "OOM Score"، وهو رقم يحسب بناءً على كمية الذاكرة التي تستخدمها العملية ومدى استخدامها للـ CPU. في إحدى المرات، قتل الـ OOM Killer قاعدة البيانات PostgreSQL لأن الـ Worker Processes كانت تستخدم ذاكرة أكثر من الـ Web Server، رغم أن الـ Web Server هو الذي كان يسبب المشكلة أصلاً. الحل؟ يجب أن تضع حدوداً صارمة للذاكرة لكل حاوية، وتستخدم --memory-swap لمنع الحاوية من استخدام الـ Swap لأن ذلك يبطئ الأداء بشكل كبير.
# مثال على تشغيل حاوية مع حدود ذاكرة صارمة
# --memory: الحد الأقصى للذاكرة المادية
# --memory-swap: يجب أن يكون مساوياً لـ --memory لمنع استخدام الـ Swap
# --oom-kill-disable: لا تستخدم هذا أبداً في الإنتاج! (سيجعل الـ Kernel يقتل العملية الأم بدلاً من الحاوية)
docker run -d --name postgres \
--memory=4g \
--memory-swap=4g \
-e POSTGRES_PASSWORD=secret \
postgres:13
# لمراقبة استخدام الذاكرة في الوقت الفعلي
docker stats --format "table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}"هناك خدعة أخرى مهمة: استخدم --oom-score-adj لضبط أولوية الـ OOM Killer. مثلاً، إذا كانت قاعدة البيانات لديك أكثر أهمية من الـ Cache، يمكنك تقليل الـ OOM Score الخاص بها بحيث لا تكون أول عملية تُقتل. لكن احذر، هذه الخدعة تحتاج فهم عميق لكيفية عمل الـ Kernel، وإلا ستجد نفسك تقتل العمليات الخطأ في اللحظات الحرجة.
في أحد المشاريع، استخدمنا ELK Stack لتحليل الـ Logs. كل شيء كان يعمل بشكل جيد في التطوير، لكن في الإنتاج، عندما وصل عدد المستخدمين إلى ٥ آلاف متزامن، بدأ الـ Disk I/O يرتفع بشكل جنوني. فتحنا الـ Logs ووجدنا أن كل حاوية كانت تنتج ١٠ ميجابايت من الـ Logs في الثانية، ومع ٥٠ حاوية، أصبح لدينا ٥٠٠ ميجابايت في الثانية تُكتب على الـ Disk. الـ Disk لم يستطع التعامل مع هذا الحمل، وبدأ الـ Latency يرتفع، مما تسبب في فشل الـ Health Checks وحدوث إعادة تشغيل للحاويات بشكل متكرر.
الحل لم يكن مجرد تقليل كمية الـ Logs، بل إعادة التفكير في كيفية التعامل معها. أولاً، استخدمنا --log-driver=json-file مع خيارات لضغط الـ Logs وتقسيمها إلى ملفات صغيرة. ثانياً، أضفنا فلتر في الـ Application نفسه لتقليل الـ Verbosity في الـ Logs عندما يكون الـ Traffic عالياً. ثالثاً، استخدمنا الـ Log Rotation مع خيارات صارمة لمنع تراكم الملفات القديمة. وأخيراً، نقلنا الـ Logs إلى نظام مركزي مثل Fluentd بدلاً من الاعتماد على الـ Disk المحلي.
# تشغيل حاوية مع إعدادات متقدمة للـ Logs
# --log-opt max-size: حجم الملف قبل التدوير
# --log-opt max-file: عدد الملفات قبل الحذف
# --log-opt compress: ضغط الملفات القديمة
# --log-opt labels: إضافة علامات للفلترة
docker run -d --name nginx \
--log-driver=json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
--log-opt compress=true \
--log-opt labels=app,env \
-p 80:80 \
nginx:alpine
# مثال على فلترة الـ Logs في التطبيق (Node.js)
if (process.env.NODE_ENV === 'production' && highTraffic) {
winston.level = 'warn';
} else {
winston.level = 'info';
}هناك نقطة مهمة جداً: لا تعتمد على الـ Logs المحلية أبداً في بيئات الإنتاج. استخدم أدوات مثل Fluentd أو Filebeat لنقل الـ Logs إلى نظام مركزي مثل Elasticsearch أو Loki. هذه الأدوات مصممة للتعامل مع كميات كبيرة من البيانات دون التأثير على أداء السيرفر الأساسي.
في أحد المشاريع، استخدمنا Docker Swarm لتشغيل ١٠٠ حاوية على ٥ سيرفرات. كل شيء كان يعمل بشكل جيد في البداية، لكن بعد أسبوعين، بدأنا نلاحظ أن بعض الـ Requests تستغرق ١٠ ثوانٍ بدلاً من ٢٠٠ مللي ثانية. فحصنا الـ Metrics ووجدنا أن الـ CPU Usage كان عند ١٠٠٪، لكن الغريب أن الـ CPU Throttling كان عند ٥٠٪. هذا يعني أن الـ Kernel كان يخنق الـ CPU للحاويات رغم أن هناك موارد متاحة.
المشكلة كانت في كيفية ضبط الـ CPU Limits. عندما تستخدم --cpus في Docker، أنت تخبر الـ Kernel بأن هذه الحاوية يمكنها استخدام عدد معين من الـ CPU Cores. لكن الـ Kernel لا يوزع الـ CPU بشكل متساوٍ دائماً. بدلاً من ذلك، يستخدم آلية اسمها CFS (Completely Fair Scheduler) التي تحاول توزيع الوقت بين العمليات. المشكلة هي أن CFS لا يفهم أن بعض العمليات تحتاج إلى وقت مستمر بدلاً من وقت متقطع.
# تشغيل حاوية مع ضبط الـ CPU بشكل صحيح
# --cpus: عدد الأنوية التي يمكن استخدامها
# --cpu-shares: الأولوية النسبية (افتراضي 1024)
# --cpu-quota: الحد الأقصى للوقت في الميكروثانية
# --cpu-period: الفترة الزمنية للـ CFS (افتراضي 100000)
docker run -d --name redis \
--cpus=2 \
--cpu-shares=2048 \
--cpu-quota=200000 \
--cpu-period=100000 \
redis:6
# لمراقبة الـ CPU Throttling
cat /sys/fs/cgroup/cpu,cpuacct/docker/<container-id>/cpu.statالحل الذي وجدناه هو استخدام --cpu-quota و--cpu-period بدلاً من الاعتماد فقط على --cpus. بهذه الطريقة، يمكنك التحكم بشكل أدق في كيفية توزيع الـ CPU بين الحاويات. مثلاً، إذا كان لديك حاوية تحتاج إلى وقت مستمر، يمكنك زيادة --cpu-quota بحيث تحصل على وقت أطول في كل فترة. لكن احذر، هذا يتطلب فهم عميق لكيفية عمل الـ CFS في الـ Kernel، وإلا ستجد نفسك تخنق الحاويات الأخرى دون قصد.
في أحد المشاريع، استخدمنا Docker مع Kubernetes لتشغيل تطبيق يعتمد على الـ Microservices. كل شيء كان يعمل بشكل جيد في التطوير، لكن في الإنتاج، بدأنا نلاحظ أن بعض الـ API Calls تستغرق ٥ ثوانٍ بدلاً من ١٠٠ مللي ثانية. فحصنا الـ Network ووجدنا أن الـ Latency بين الـ Pods كان مرتفعاً جداً. المشكلة؟ كنا نستخدم الـ Default Network Driver في Docker، وهو bridge، الذي يضيف طبقة إضافية من الـ Overhead.
الحل كان استخدام network mode آخر. في Kubernetes، استخدمنا Calico بدلاً من الـ Default Network Plugin. Calico يستخدم الـ eBPF لتسريع الـ Networking وتقليل الـ Latency. في Docker Swarm، استخدمنا Overlay Network مع خيارات متقدمة لتقليل الـ Overhead. أيضاً، قمنا بتفعيل الـ TCP Keepalive لضمان عدم انقطاع الاتصالات بسبب الـ Timeouts.
# مثال على تكوين Calico في Kubernetes
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
name: default-ipv4-ippool
spec:
cidr: 192.168.0.0/16
natOutgoing: true
disabled: false
nodeSelector: all()
---
apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
name: default
spec:
logSeverityScreen: Info
nodeToNodeMeshEnabled: true
asNumber: 64512هناك نقطة مهمة جداً: لا تعتمد على الـ Default Network Settings في بيئات الإنتاج. دائماً استخدم أدوات مثل iperf3 لقياس الـ Network Performance قبل وبعد تطبيق أي تغييرات. أيضاً، استخدم أدوات مثل Wireshark لتحليل الـ Packets ومعرفة أين بالضبط يحدث الـ Bottleneck.
في أحد المشاريع، استخدمنا Docker مع قاعدة بيانات PostgreSQL. كل شيء كان يعمل بشكل جيد في البداية، لكن بعد شهرين، بدأنا نلاحظ أن الـ Disk I/O يرتفع بشكل جنوني، والـ Latency في قاعدة البيانات يصل إلى ١٠ ثوانٍ. فتحنا الـ Metrics ووجدنا أن الـ Disk Usage كان عند ١٠٠٪، رغم أن الـ Storage المتاح كان ٥٠٠ جيجابايت. المشكلة؟ كنا نستخدم الـ Default Storage Driver في Docker، وهو overlay2، الذي يضيف طبقة إضافية من الـ Overhead عند التعامل مع الملفات الكبيرة.
الحل كان استخدام storage driver آخر. في بيئات الإنتاج، نوصي باستخدام devicemapper في الوضع direct-lvm أو استخدام ZFS إذا كان لديك دعم له. أيضاً، استخدمنا bind mounts بدلاً من volumes عندما كنا بحاجة إلى أداء عالي، مثل قواعد البيانات. لكن احذر، bind mounts لها مشاكلها الخاصة، مثل عدم دعم الـ Backup والـ Restore بسهولة.
# تهيئة devicemapper في الوضع direct-lvm
# أولاً، قم بإنشاء logical volume
lvcreate --size 100G --name docker-thinpool docker
lvcreate --size 10G --name docker-thinpoolmeta docker
lvconvert -y --zero n -c 512K --thinpool docker/docker-thinpool --poolmetadata docker/docker-thinpoolmeta
# ثم قم بتعديل daemon.json
cat /etc/docker/daemon.json
{
"storage-driver": "devicemapper",
"storage-opts": [
"dm.thinpooldev=/dev/mapper/docker-thinpool",
"dm.use_deferred_removal=true",
"dm.use_deferred_deletion=true"
]
}
# إعادة تشغيل Docker
systemctl restart dockerهناك نقطة مهمة جداً: لا تعتمد على الـ Default Storage Settings في بيئات الإنتاج. دائماً استخدم أدوات مثل iostat وiotop لمراقبة الـ Disk I/O ومعرفة أين بالضبط يحدث الـ Bottleneck. أيضاً، استخدم أدوات مثل fio لاختبار أداء الـ Storage قبل وبعد تطبيق أي تغييرات.
Docker في الإنتاج ليس مجرد docker run. هو علم كامل يتطلب فهم عميق للـ Kernel والـ Memory والـ CPU والـ Network والـ Storage. الأخطاء التي ذكرتها ليست مجرد أخطاء تقنية، بل هي أخطاء في التفكير والتصميم. عندما تستخدم Docker في الإنتاج، يجب أن تفكر في كل طبقة من الطبقات، بدءاً من الـ Application وصولاً إلى الـ Hardware نفسه.
نصيحة ذهبية: ابدأ دائماً بـ docker stats وhtop قبل أن تبدأ في البحث عن المشاكل. في ٩٠٪ من الحالات، المشكلة تكون واضحة في هذه الأدوات البسيطة. أيضاً، استخدم أدوات مثل Prometheus وGrafana لمراقبة الأداء في الوقت الفعلي، ولا تنتظر حتى تحدث الكارثة. وأخيراً، لا تخف من تجربة الحلول المختلفة، لكن دائماً اختبرها في بيئة تشبه الإنتاج قبل تطبيقها على السيرفرات الحقيقية.
Docker ليس حلاً سحرياً، بل هو أداة قوية تتطلب فهم عميق للعمل خلف الكواليس. إذا لم تفهم كيف يعمل الـ Kernel والـ Memory والـ CPU، فستجد نفسك دائماً في دوامة من المشاكل التي لا تعرف كيف تحلها.
— مهندس DevOps في شركة ناشئة