تجاوز إلى المحتوى
← المدونة

cgroup v1 انتهى. رحّل بشكل صحيح.

أزال الإصدار systemd 258 دعم cgroup v1 نهائيًا، فصارت خوادمك تُقلع على الهرمية الموحدة سواء طلب أحد ذلك أم لا. إليك التدقيق، والتحويل ملفًا بملف، والادعاءات الأربعة الشائعة عن هذا الترحيل والخاطئة ببساطة.

·28 دقيقة قراءة
  • Linux
  • cgroups
  • systemd
  • الحاويات

معظم عمليات الترحيل تبدأ بقرار. هذه تبدأ بواقعة: في سبتمبر 2025 أزال الإصدار systemd 258 دعم cgroup v1 بالكامل، ومعه مخرج الطوارئ SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1 الذي كان الإصدار 256 قد أدخله قبل عام. الهرمية الموحدة تُركَّب الآن عند الإقلاع على كل جهاز يشغّل ذلك الإصدار أو ما بعده، ولا توجد أي طريقة مدعومة لطلب غير ذلك. ومهما كان رأي منصة الحاويات لديك في cgroups، فإن نظام التشغيل تحتها قد أدلى بصوته بالفعل.

صورة غلاف من ثلاث لوحات. اللوحة اليسرى، وعنوانها "cgroup v1 — هرمية منفصلة لكل متحكم"، تعدّد ست أشجار مستقلة لكل متحكم: memory وcpu,cpuacct وblkio وdevices وnet_cls وfreezer، تحت تعليق "العملية موجودة في كل واحدة منها". والعمود الأوسط، وعنوانه "ما الذي يتغيّر معناه"، يعرض ستة أزواج تحويل: memory.limit_in_bytes يصبح memory.max، وcpu.cfs_quota_us مع period يصبحان cpu.max، وblkio.weight يصبح io.weight، وmemory.memsw.limit يصبح التبديل وحده لا الذاكرة مع التبديل، وcpu.shares بقيمة 1024 يصبح cpu.weight بقيمة 100، وnet_cls وdevices يصبحان eBPF بلا متحكم. واللوحة اليمنى، وعنوانها "cgroup v2 — هرمية موحدة واحدة"، تُظهر شجرة واحدة لا توجد فيها العمليات إلا على الأوراق، مع شريط يعدّد طبقات الذاكرة الأربع memory.min وmemory.low وmemory.high وmemory.max.
التغيير البنيوي في صورة واحدة: شجرة منفصلة لكل متحكم تنهار في شجرة واحدة، والعمليات لا يُسمح لها إلا على الأوراق، والذاكرة تتوسع من حد صارم واحد إلى أربع طبقات ذات سلوك مختلف فعلًا.

ما يلي هو الترحيل كما يجري حقًا على أساطيل حقيقية: كيف تعرف الهرمية التي يعمل عليها الخادم دون أن يخدعك التخطيط الهجين، وجدول تحويل ملفًا بملف مع توجيهات systemd المقابلة، والترجمتان اللتان تغيّران السلوك بصمت إذا نسخت الأرقام كما هي، ومعادلة وزن المعالج التي استُبدلت بهدوء في أزمنة تشغيل OCI خلال 2025، ورواية أمينة لما فعله وما لم يفعله Docker وKubernetes — لأن معظم ما يُكتب في هذه النقطة الأخيرة خاطئ، والتصرف بناءً عليه يكلّفك إما انقطاعًا في الخدمة أو سنة من الذعر بلا داعٍ.

أنت لم تختر هذا، وتلك هي النقطة

الأعراض غير مباشرة على نحو غير مفيد، لأن لا شيء يعلن عن نفسه بوصفه مشكلة cgroup. عميل مراقبة يبدأ بالإبلاغ عن أصفار. حد ذاكرة حاوية يتوقف عن التطبيق. حِمل rootless كان يعمل جيدًا على الجهاز القديم يرفض الإقلاع. سكربت ظل ثماني سنوات يقرأ /sys/fs/cgroup/memory/memory.usage_in_bytes يبدأ بتسجيل خطأ "ملف غير موجود" لا ينتبه إليه أحد، لأنه يكتب في سجل لا يقرؤه أحد. كل هذه هي الحدث نفسه من زوايا مختلفة: المسارات انتقلت، والدلالات تغيّرت تحتها، ولم يصرخ أي خطأ بصوت عالٍ بما يكفي لإيقاف عملية نشر.[sd258]

ما تراهما يعنيه عادةًأين يُعالَج
عميل مراقبة يبلّغ عن صفر ذاكرة أو صفر معالج لكل الحاوياتإنه يقرأ /sys/fs/cgroup/memory/… أو cpu,cpuacct، وهي غير موجودة تحت الهرمية الموحدةجدول التحويل
write error: Device or resource busy عند إضافة معرّف عملية إلى مجموعةقيد لا عمليات داخلية: تلك المجموعة تفوّض المتحكمات لأبنائها بالفعلثلاث قواعد
حاوية تستخدم التبديل أكثر بكثير مما اعتادت، بالحدود نفسهاقيمة memory.memsw.limit_in_bytes نُسخت إلى memory.swap.max، وهذا يعني التبديل وحدهدلالات الذاكرة
Podman في وضع rootless يقبل --memory ولا يطبّقهsystemd لم يفوّض متحكم الذاكرة إلى مدير المستخدمالحاويات
الحاويات تحصل على معالج أقل تحت التنازع مما اعتادتالتحويل الخطي من الحصص إلى الوزن وضع طلب معالج واحد عند وزن 39 مقابل افتراضي 100وزن المعالج
kubelet يرفض الإقلاع بعد ترقية صورة العقدةعقدة cgroup v1، وkubelet بإصدار 1.35 أو أحدث، وfailCgroupV1 على قيمته الافتراضية trueKubernetes
--oom-kill-disable بلا أي أثر وبصمتمُهمَل على cgroup v2. لا مكافئ له ولا خطة لإيجاد مكافئالحاويات

هناك عادة واحدة تستحق التخلي عنها قبل أي شيء آخر. إن كانت لديك أداة تكتب مباشرة داخل /sys/fs/cgroup على خادم يعمل بـ systemd، فهي لن تنجو من هذا الترحيل بالشكل الذي تريده. systemd يملك تلك الشجرة ويعيد فرض رؤيته لها كلما تغيّرت وحدة؛ ووثيقة التفويض الرسمية للمشروع صريحة في أن قاعدة "كاتب واحد لكل شجرة فرعية" قاعدة لا اقتراح. تحت v1 كان بإمكانك عادة كسرها والإفلات بذلك. تحت v2، بتفويضها الصارم من الأعلى إلى الأسفل، لا يمكنك.[sddeleg]

على أي هرمية يعمل هذا الخادم فعلًا؟

ابدأ بإثبات الحقيقة، لأن الأوامر المتوارثة تكذب بطريقة محددة ومتسقة. الفحص الذي توثّقه Kubernetes هو الصحيح، وهو أمر واحد: اسأل عن نوع نظام الملفات لـ /sys/fs/cgroup. تحت الهرمية الموحدة تكون نقطة التركيب تلك هي نفسها نظام ملفات cgroup2، فيُبلغ stat بـ cgroup2fs. أما تحت v1 — والأهم، تحت التخطيط الهجين القديم أيضًا — فهي tmpfs بداخلها أدلة المتحكمات مركّبة.[k8scg]

# The only check that cannot lie. /sys/fs/cgroup is a tmpfs under v1 and under
# the old hybrid layout, and a cgroup2 filesystem under the unified hierarchy.
stat -fc %T /sys/fs/cgroup/
# cgroup2fs   -> unified, cgroup v2 only
# tmpfs       -> cgroup v1, or hybrid: v2 mounted under a v1 tmpfs

# Why `mount | grep cgroup2` is not enough: hybrid mounts a cgroup2 hierarchy
# too, at /sys/fs/cgroup/unified, with no controllers attached to it. Grepping
# for the string finds it and tells you the opposite of the truth.
mount | grep -E '^cgroup' | sed 's/ (.*//'
# cgroup2 on /sys/fs/cgroup type cgroup2          <- unified: good
# cgroup2 on /sys/fs/cgroup/unified type cgroup2  <- hybrid:  not good

# What the kernel will actually let you control here. Under the unified
# hierarchy this file exists at the top of the tree and lists the controllers.
# Under hybrid it is not here at all - it is one level down, at
# /sys/fs/cgroup/unified/cgroup.controllers, and it is empty. Its absence from
# the top level is the clearest single tell that you are not on v2.
cat /sys/fs/cgroup/cgroup.controllers 2>/dev/null
# cpuset cpu io memory hugetlb pids rdma misc

# Where a given process ended up. Under v2 there is exactly one line and it
# starts with `0::`. More than one line means controllers are still split.
cat /proc/self/cgroup
# 0::/user.slice/user-1000.slice/session-3.scope

# And the two userspace pieces that have to agree with the kernel:
systemctl --version | head -1
docker info --format 'driver={{.CgroupDriver}} version={{.CgroupVersion}}' 2>/dev/null

الحالة الهجينة هي سبب كون mount | grep cgroup2 مضلِّلًا فعليًا لا ناقصًا فحسب: التخطيط الهجين يركّب هرمية cgroup2 حقيقية على /sys/fs/cgroup/unified دون ربط أي متحكم بها. يطابق الـ grep، فتستنتج أنك على v2، ثم يهبط كل حد تضبطه بعد ذلك على متحكم v1. الدليل الثاني هو /proc/self/cgroup: تحت v2 يحتوي على سطر واحد فقط يبدأ بـ 0::، وتحت v1 أو الهجين يحتوي على سطر لكل متحكم. شغّل التدقيق أدناه على الأسطول كله قبل أن تخطط لأي شيء، لأن الجواب في خبرتي لا يكون موحدًا أبدًا.[cgman]

#!/usr/bin/env bash
# Fleet audit. Read-only: it changes nothing. Run it before you plan anything,
# because the answer is almost never uniform across a real estate of servers.
set -u

host=$(hostname -s)
ver=$(stat -fc %T /sys/fs/cgroup/ 2>/dev/null)
case "$ver" in
  cgroup2fs) mode=unified ;;
  tmpfs)     [ -d /sys/fs/cgroup/unified ] && mode=hybrid || mode=legacy ;;
  *)         mode=unknown ;;
esac

kernel=$(uname -r)
sd=$(systemctl --version 2>/dev/null | awk 'NR==1{print $2}')
printf '%-16s mode=%-8s kernel=%-14s systemd=%s\n' "$host" "$mode" "$kernel" "$sd"

# --- the things that will break, rather than the things that will complain ---

# a) v1-only controllers with no v2 equivalent. If anything you run writes to
#    these, it needs an eBPF replacement, not a path change.
for c in net_cls net_prio devices; do
  [ -d "/sys/fs/cgroup/$c" ] && echo "  uses v1-only controller: $c"
done

# b) anything with a hardcoded v1 path. This is the single most common cause of
#    a migration failing three weeks later in an agent nobody remembered.
grep -rIl --exclude-dir=.git \
     -e '/sys/fs/cgroup/memory/' \
     -e '/sys/fs/cgroup/cpu,cpuacct/' \
     -e 'memory.limit_in_bytes' \
     -e 'cpu.cfs_quota_us' \
     /etc /opt /usr/local 2>/dev/null | sed 's/^/  hardcoded v1 path: /'

# c) kernel command line pinning the old hierarchy. On systemd 258 and later
#    this parameter no longer does anything, which is its own kind of trap:
#    the host silently boots unified and the runbook still says otherwise.
grep -o 'systemd\.unified_cgroup_hierarchy=[01]' /proc/cmdline \
  | sed 's/^/  kernel cmdline: /'
grep -o 'SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1' /proc/cmdline \
  | sed 's/^/  legacy force flag (removed in systemd 258): /'

# d) container runtimes and their drivers, which have to match the kernel
command -v docker  >/dev/null && docker info 2>/dev/null \
  | grep -E 'Cgroup (Driver|Version)' | sed 's/^/ /'
command -v podman  >/dev/null && podman info --format \
  '  podman cgroupVersion={{.Host.CgroupsVersion}} manager={{.Host.CgroupManager}}' 2>/dev/null
[ -f /var/lib/kubelet/config.yaml ] && \
  grep -E '^(cgroupDriver|failCgroupV1):' /var/lib/kubelet/config.yaml | sed 's/^/  kubelet /'

شغّله على كل خادم، لا على خادم يُفترض أنه ممثل للبقية. الأجهزة التي ما تزال على v1 في 2026 هي، بحكم التعريف تقريبًا، الأجهزة التي لم يلمسها أحد — الجهاز المغلق من المورّد، ووكيل البناء الذي أقامه أحدهم يدويًا في 2019، وعقدة قاعدة البيانات المستثناة عمدًا من دورة إدارة الإعدادات. تلك بالضبط هي الخوادم التي ينتظر فيها مسار /sys/fs/cgroup/memory/ مكتوبًا بشكل صريح، وهي بالضبط الخوادم التي لن يلاحظ فيها أحد أن شيئًا قد انكسر.

من أزال ماذا، ومن اكتفى بالإعلان

أربعة مشاريع معنيّة بالأمر، وهي على جداول زمنية مختلفة تمامًا، وهذا أكبر مصدر منفرد للالتباس حول هذا الترحيل. تحرّك systemd أولًا وبأقوى صورة. الإصدار 256، في يونيو 2024، توقف عن الإقلاع على cgroup v1 افتراضيًا مع إبقاء مخرج طوارئ. والإصدار 258، في سبتمبر 2025، أزال الشيفرة نفسها — وملاحظات الإصدار لا تحتمل التأويل: أُزيل دعم cgroup v1 (الهرميتان "legacy" و"hybrid")، وأن cgroup v2 ستُركَّب دائمًا أثناء إقلاع النظام. ورفع ذلك الإصدار أيضًا الحد الأدنى للنواة إلى 5.4، مع التوصية بـ 5.7.[sdnews][sd256]

المشروعما حدث فعلًامتىماذا يعني لك
systemd 256توقف عن الإقلاع على cgroup v1 افتراضيًا؛ وأضاف SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1يونيو 2024الإصدار 257 ما زال يحترم ذلك الخيار، فهو آخر إصدار يمكنك فيه طلب v1 والحصول عليه
systemd 258أزال دعم cgroup v1 بالكامل بما فيه مخرج الطوارئ؛ ورفع أساس النواة إلى 5.4 مع التوصية بـ 5.7سبتمبر 2025هذا هو الموعد النهائي. الوضع الموحد هو الوضع الوحيد في الشيفرة
Kubernetes 1.35أهمل cgroup v1؛ وkubelet يرفض الإقلاع على عقدة v1 افتراضيًاديسمبر 2025قابل للتجاوز بـ failCgroupV1: false. ليس إزالة
Kubernetes 1.38 وما بعدهأقرب إصدار سيزيل فيه KEP-5573 الشيفرةغير مجدوللديك وقت أطول مما توحي به العناوين
Docker Engine 29.0أهمل cgroup v1؛ دون تحديد إصدار للإزالةنوفمبر 2025إهمال فقط. وتنص الوثائق على أن الدعم مستمر حتى مايو 2029
runc / crunاستبدل التحويل الخطي من الحصص إلى الوزن بتحويل لوغاريتميخلال 2025يغيّر أولوية المعالج على عقد رحّلتها بالفعل

أما الثلاثة الباقون فلم يفعلوا ما قرأت على الأرجح أنهم فعلوه، والفرق مهم للتخطيط. Kubernetes أهملت cgroup v1 في الإصدار 1.35 — فصار kubelet يرفض الإقلاع على عقدة v1 افتراضيًا، لكن ذلك الافتراض حقل إعدادات يمكنك قلبه، وينص KEP-5573 حرفيًا على أن حذف الشيفرة لن يتم قبل 1.38، دون أي تاريخ مرتبط بذلك. وDocker أهملت cgroup v1 في Docker Engine 29.0، الصادر في نوفمبر 2025، دون تحديد أي إصدار للإزالة إطلاقًا؛ وتقول صفحة الإهمال في وثائق Docker نفسها إن الدعم مستمر حتى مايو 2029، وهو موعد نهاية عمر إصدارات المؤسسات التي ما تزال تحتاجه. إذن: نظام التشغيل أزاله فعلًا، ومنصات التنسيق اكتفت بالإعلان. خطّط اعتمادًا على نظام التشغيل.[kep5573][dockdep]

التوزيعةالهرمية الافتراضيةملاحظة
Fedora 31 وما بعدهاموحدة (v2)أول توزيعة واسعة الانتشار تتحول؛ وFedora 43 ترث إزالة systemd 258
Debian 11 وما بعدهاموحدة (v2)تشحن Debian 13 الإصدار systemd 257، فهي آخر توزيعة فيها أي مسار قديم أصلًا
Ubuntu 21.10 وما بعدهاموحدة (v2)الإصداران 22.04 LTS وما بعده هما الأرجح أنك ما زلت تشغّلهما
RHEL 9 وما بعدهاموحدة (v2)أهملت RHEL 9.4 نسخة v1 رسميًا؛ وRHEL 10 لن تُقلع على v1 إطلاقًا
SLES 15 SP6 وما بعدهاموحدة (v2)الإصدارات SP3 إلى SP5 كانت هجينة افتراضيًا، وهو التخطيط الذي يخدع grep
أي شيء أقدمv1 أو هجينةوهو أيضًا خارج الدعم الآن. مسألة cgroup ليست العاجلة هنا

ما يعني أن الموعد النهائي الحقيقي على أي جهاز بعينه هو إصدار systemd الذي تشحنه توزيعته، لا خارطة طريق أي منصة حاويات. معظم الأساطيل موحدة أصلًا ومنذ سنوات دون أن ينتبه أحد — Fedora منذ 31، وDebian منذ 11، وUbuntu منذ 21.10، وRHEL منذ 9. العمل متركّز فيما تبقى، وفي الأدوات التي ما تزال تفترض مسارات v1 بغض النظر عما يفعله الخادم.[rhel10][moby51111]

ثلاث قواعد تكسر التخطيطات المبنية يدويًا

ثلاث قواعد بنيوية تميّز v2 عن v1، وكل واحدة منها ستكسر تخطيطًا بُني يدويًا تحت v1. الأولى هي الهرمية الموحدة نفسها: للعملية موضع واحد في شجرة واحدة، وكل متحكم يقرأ الموضع ذاته، ولهذا انكمش /proc/self/cgroup من عشرات الأسطر إلى سطر واحد. الثانية هي التفويض من الأعلى إلى الأسفل: لا يوجد المتحكم في مجموعة ابنة إلا إذا سلّمه الأب صراحةً عبر cgroup.subtree_control. والثالثة هي التي تؤلم فعلًا.[kdoc][knoint]

# DEMONSTRATION ONLY. This writes into the cgroup tree by hand, which is the
# exact thing the rest of this article tells you not to do on a systemd host.
# Read it to understand the rules, then set limits through systemd.

# Rule 1 - one tree. Under v1 a process had a position in each controller's
# hierarchy independently, which is why /proc/PID/cgroup had a dozen lines.
# Under v2 it has one, and every controller reads the same position.
cd /sys/fs/cgroup
mkdir -p demo/worker demo/batch

# Rule 2 - a controller only exists in a child if the parent hands it down.
# cgroup.controllers is what you HAVE; cgroup.subtree_control is what you GIVE.
cat demo/cgroup.controllers        # what the parent has delegated so far
echo '+cpu +memory +io' > cgroup.subtree_control   # root delegates to demo
cat demo/cgroup.controllers        # cpu io memory
echo '+cpu +memory' > demo/cgroup.subtree_control  # demo delegates to its kids
ls demo/worker/ | grep -E '^(cpu|memory)\.'        # the knobs now exist

# Rule 3 - no internal processes. A cgroup may hold processes, or hand
# resources to children, never both. This is the rule that breaks hand-built
# v1 layouts, and it fails at write() time with a very unhelpful error.
echo $$ > demo/cgroup.procs
# bash: echo: write error: Device or resource busy
#
# ... because demo already has subtree_control set. Processes live on leaves:
echo $$ > demo/worker/cgroup.procs   # fine

# The root cgroup is exempt from rule 3, which is why the mistake survives
# testing at the top level and only shows up one directory down.

قيد "لا عمليات داخلية" يقول إن المجموعة غير الجذرية يجوز أن تحتوي عمليات، أو أن توزّع الموارد على أبنائها، لكن لا الاثنين معًا. وُجد هذا القيد لإزالة غموض حقيقي في v1، حيث كانت عمليات الأب تتنافس مع أبنائه دون قاعدة محددة. عمليًا يعني ذلك أن تخطيط v1 الذي يضع حدودًا في كل مستوى من الشجرة لا يُترجم: عليك دفع العمليات إلى الأوراق وإبقاء العقد الداخلية فارغة. وهو يفشل بأقل الطرق فائدة على الإطلاق — رسالة عارية write error: Device or resource busy ناتجة عن echo داخل cgroup.procs، دون ما يشير إلى أي القاعدتين كسرت. المجموعة الجذرية مستثناة، وهذا بالضبط سبب نجاة الخطأ من اختبار سريع على المستوى الأعلى.[kdeleg]

جدول التحويل، ملفًا بملف

هذا هو التخطيط، مع توجيه systemd بجانب كل زوج، لأن التوجيه على أي خادم يعمل بـ systemd هو ما يجب أن تضبطه فعلًا. الأسماء تتغير أكثر مما تتغير القيم، لكن ثلاثة صفوف تغيّر المعنى لا الهجاء، وهي مُعلَّمة. لاحظ خصوصًا أن cpu.max يدمج ملفَّي v1 في ملف واحد بقيمتين، وأن مقياس وزن المعالج ليس مقياس حصص المعالج — فالقيم الافتراضية وحدها تختلف بعامل عشرة.[sdresctl]

cgroup v1cgroup v2توجيه systemdملاحظة
memory.limit_in_bytesmemory.maxMemoryMax=إعادة تسمية. المعنى نفسه: حد صارم، وقتل OOM عند تجاوزه
memory.soft_limit_in_bytesmemory.highMemoryHigh=ترقية. الحد الليّن كان مُتجاهلًا غالبًا؛ أما memory.high فيخنق فعلًا
memory.low / memory.minMemoryLow= / MemoryMin=جديد. أرضيات حماية، لا مكافئ لها في v1
memory.memsw.limit_in_bytesmemory.swap.maxMemorySwapMax=معنى مختلف. memsw كان ذاكرة+تبديل؛ وهذا تبديل وحده
memory.usage_in_bytesmemory.currentإعادة تسمية
memory.failcntmemory.eventsأفضل: عدّادات منفصلة لـ low وhigh وmax وoom وoom_kill
cpu.sharescpu.weightCPUWeight=مقياس مختلف. الافتراضي 1024 يصبح افتراضيًا 100؛ راجع التحويل
cpu.cfs_quota_us + cpu.cfs_period_uscpu.maxCPUQuota= + CPUQuotaPeriodSec=ملفان يصبحان واحدًا، يُكتب على هيئة "$MAX $PERIOD". وCPUQuota= يضبط شق الحصة وحده
cpuacct.usagecpu.statصار يتضمن nr_throttled وthrottled_usec
blkio.weightio.weightIOWeight=إعادة تسمية، لكن الحساب تحتها صار صحيحًا أخيرًا
blkio.throttle.*_bps_deviceio.maxIOReadBandwidthMax= وغيرهملف واحد بمفاتيح متداخلة بدل أربعة؛ ويغطي الآن الكتابات المؤقتة
pids.maxpids.maxTasksMax=دون تغيير
freezer.statecgroup.freezeإعادة تسمية؛ اكتب 1 أو 0
devices.allow / devices.deny— (eBPF)DeviceAllow=لا متحكم. استُبدل بـ BPF_PROG_TYPE_CGROUP_DEVICE
net_cls.classid / net_prio.*— (eBPF)لا متحكم ولا ملف بديل. استخدم eBPF على مسارات cgroup
cpu.pressure / memory.pressure / io.pressureجديد. PSI: سبب للترحيل لا كلفة له

ثلاثة من متحكمات v1 ليس لها أي مقابل في v2، وهذا هو الصف الذي يحوّل ترحيلًا آليًا إلى مهمة هندسية. net_cls وnet_prio أُزيلا تمامًا بدل إعادة تنفيذهما؛ وتصنيف حركة الشبكة وتشكيلها لكل مجموعة يجري الآن ببرامج eBPF مرتبطة بمسارات cgroup v2، مع دعم مقابل في iptables وnftables. ومتحكم devices سلك الطريق نفسه: بدل ملف قائمة سماح، يتلقى برنامج eBPF من النوع BPF_PROG_TYPE_CGROUP_DEVICE الرقمين الكبير والصغير، ونوع الجهاز، ونوع الوصول، ثم يعيد السماح أو -EPERM. إن كانت منصتك تستخدم أيًا من هذه مباشرة، فخصّص ميزانية لعمل حقيقي لا لاستبدال مسارات.[cgman][bpfdev]

الذاكرة هي موضع التغيّر الحقيقي في الدلالات

الذاكرة هي حيث يُحدث الترحيل المتعجّل ضرره الصامت، لأن الأرقام تظل مقبولة بينما يتغير السلوك. تحت v1 كان لديك حد صارم واحد وحد ليّن تجاهلته معظم النوى عمليًا. وتحت v2 لديك أربع طبقات، واحدة منها فقط تستطيع قتل شيء: memory.max هو الحد الصارم ويطلق قتل OOM داخل المجموعة؛ وmemory.high خانق يضع المجموعة تحت استرداد مكثّف، وبعبارة النواة نفسها لا يستدعي قاتل OOM أبدًا؛ وmemory.low حماية بأفضل جهد؛ وmemory.min حماية صارمة لا يُسترد منها شيء إطلاقًا.[kmem][kv1mem]

# --- cgroup v1: two numbers, and the second one is not what people think ---
#   memory.limit_in_bytes        = 2G   -> hard limit on memory
#   memory.memsw.limit_in_bytes  = 3G   -> hard limit on memory PLUS swap
#                                          (so: 2G RAM + up to 1G of swap)
#   memory.soft_limit_in_bytes   = 1G   -> best-effort, and widely ignored
#
# --- cgroup v2: four memory tiers, plus a separate swap cap ---------------
cd /sys/fs/cgroup/demo/worker

echo 2G       > memory.max        # hard limit. Over this, OOM kill inside the cgroup.
echo 1800M    > memory.high       # throttle. Over this, heavy reclaim - never an OOM kill.
echo 512M     > memory.low        # best-effort protection. Reclaimed only as a last resort.
echo 256M     > memory.min        # hard protection. Never reclaimed, at all.
echo 1G       > memory.swap.max   # SWAP ONLY. Not memory+swap. Read that twice.

# The migration trap, stated as arithmetic:
#   v1: memsw.limit=3G with limit=2G  ->  2G RAM, 1G swap
#   v2: memory.swap.max=3G            ->  memory.max RAM, 3G swap
# Copying 3G across gives the workload three times the swap it used to have.
# The correct translation is (memsw.limit - limit), and if that is zero you
# want memory.swap.max=0, not "unset".

# What is actually happening, rather than what you configured:
cat memory.current                 # bytes in use right now
cat memory.events                  # low high max oom oom_kill oom_group_kill
# low 0
# high 148          <- throttled 148 times: memory.high is doing work
# max 0
# oom 0
# oom_kill 0        <- and it never had to kill anything
cat memory.pressure                # PSI: how much time was lost waiting on memory
# some avg10=0.42 avg60=0.31 avg300=0.11 total=9214430
# full avg10=0.00 avg60=0.00 avg300=0.00 total=118221

# memory.high plus memory.events is the pair that turns "the box OOMs at 3am"
# into a number you can alert on before it happens. v1 could not do this.
ملف v2ما الذي يفعلههل يستطيع القتل؟أقرب مكافئ في v1
memory.minحماية صارمة: الذاكرة دون هذا الحد لا تُسترد أبدًا مهما بلغ الضغطبشكل غير مباشرلا يوجد
memory.lowحماية بأفضل جهد: لا تُسترد إلا حين لا يبقى شيء غير محميلالا يوجد
memory.highخنق: فوق هذا الحد تُوضع المجموعة تحت استرداد مكثّف وتبطؤ عملياتهالاmemory.soft_limit_in_bytes، بتقريب
memory.maxحد صارم: فوقه ومع تعذّر الاسترداد يعمل قاتل OOM داخل المجموعةنعمmemory.limit_in_bytes
memory.swap.maxسقف على استهلاك التبديل وحده، مستقل عن memory.maxبشكل غير مباشرmemory.memsw.limit_in_bytes ناقص حد الذاكرة
memory.eventsعدّادات: كم مرة بلغ كل مما سبق، بما في ذلك oom_killmemory.failcnt، دون أي تفصيل

الفخ هو memory.swap.max، ويستحق الوضوح التام لأن نسخ الرقم كما هو هو التصرف الطبيعي. في v1 كان memory.memsw.limit_in_bytes يحدّ الذاكرة مضافًا إليها التبديل معًا؛ وفي v2 يحدّ memory.swap.max التبديل وحده. حاوية كانت لها limit=2G, memsw=3G كان مسموحًا لها بـ 2 غيغابايت من الذاكرة و1 غيغابايت من التبديل. اضبط memory.swap.max=3G وتكون قد منحتها ثلاثة أضعاف التبديل الذي كان لها، ونمط الفشل ليس انهيارًا بل جهازًا يزداد بطئًا تحت الحمل بطريقة لا تظهر في أي رسم بياني للذاكرة. الترجمة الصحيحة هي الفرق بين رقمَي v1، وإن كان ذلك الفرق صفرًا فأنت تريد 0 صريحًا لا ملفًا غير مضبوط. وفي المقابل تحصل على memory.events، الذي يفصّل العدّادات التي كان v1 يجمعها كلها في failcnt واحد، وعلى memory.pressure، الذي لا مكافئ له في v1 على الإطلاق: قياس تعثّر يتيح لك التنبيه على حِمل يعاني قبل أن يعمل قاتل OOM بوقت طويل.[psi]

حصة المعالج، ومتحكم إدخال/إخراج يحسب أخيرًا

ينقسم المعالج بوضوح إلى فكرتين كان v1 يخلط بينهما. cpu.weight حصة نسبية من وقت المعالج المتنازع عليه — لا يفعل شيئًا البتة على جهاز خامل، وهو الأداة الصحيحة للأولوية. وcpu.max سقف مطلق يُكتب على هيئة "$MAX $PERIOD" بالميكروثانية، ويحل محل ملفَّي v1 المنفصلين اللذين كان الناس يعكسون العلاقة بينهما باستمرار. أما ملف cpu.stat فهو الجدير بالربط بالمراقبة: nr_throttled وthrottled_usec يجيبان عن سؤال "هل هذه الخدمة بطيئة لأننا وضعنا لها سقفًا؟"، وهو سؤال يكاد يستحيل إثباته من الخارج بغير ذلك.[kio]

# --- CPU: two knobs, and only one of them is a limit ---------------------
cd /sys/fs/cgroup/demo/worker

# Weight: relative share of contended CPU. Default 100, range 1-10000.
# It does nothing at all while the machine is idle.
echo 200 > cpu.weight

# Quota: an absolute ceiling, written as "$MAX $PERIOD" in microseconds.
# 150000 out of every 100000us = 1.5 CPUs. "max" removes the ceiling.
echo '150000 100000' > cpu.max
echo 20000 > cpu.max.burst      # allow short bursts above quota (v2 only)

# The v1 equivalents were three files, and the period was easy to forget:
#   cpu.shares = 200            -> but the scale was different: default 1024
#   cpu.cfs_quota_us  = 150000
#   cpu.cfs_period_us = 100000

# Throttling, which is the number people actually need and rarely find:
cat cpu.stat
# usage_usec 918422311
# nr_periods 41822
# nr_throttled 219       <- how often the quota was hit
# throttled_usec 411920  <- and how much time was lost to it

# --- IO: blkio became io, and the numbers finally mean something ----------
# v1's blkio.throttle.* only saw direct IO; buffered writes were charged to
# whatever kernel thread flushed them, so the accounting was fiction.
echo '259:0 rbps=104857600 wbps=52428800 riops=max wiops=2000' > io.max
echo 'default 100' > io.weight
cat io.stat
# 259:0 rbytes=2841579520 wbytes=1120043008 rios=48211 wios=22103 dbytes=0 dios=0
cat io.pressure
# some avg10=1.94 avg60=0.88 avg300=0.31 total=41822193

# Device numbers, because io.max will not take a path:
lsblk -no MAJ:MIN,NAME /dev/nvme0n1
ملف v2النوعالافتراضيالغرض منه
cpu.weightنسبي100 (المجال 1–10000)الأولوية تحت التنازع. لا أثر له على جهاز خامل
cpu.maxمطلقmax 100000سقف. القيمة 150000 100000 تعني 1.5 معالج
cpu.max.burstمطلق0يسمح بتجاوزات قصيرة للحصة بدل الخنق الفوري
cpu.statللقراءة فقطnr_throttled وthrottled_usec: دليل على أن السقف يضرّ
io.weightنسبيdefault 100حصة نسبية من وقت القرص، لكل جهاز أو إجمالًا
io.maxمطلقغير مضبوطrbps وwbps وriops وwiops لكل MAJ:MIN
io.latencyهدفغير مضبوطيحمي هدف زمن استجابة بدل رقم عرض نطاق

متحكم الإدخال/الإخراج هو الجزء الذي يمثل تحسينًا حقيقيًا في v2 لا مجرد إعادة تسمية. خنق blkio في v1 لم يكن يرى سوى الإدخال/الإخراج المباشر؛ أما الكتابات المؤقتة فكانت تُحمَّل على أي خيط نواة تولّى إفراغها لاحقًا، فكان حساب الكتابة لكل مجموعة، بلغة مهذبة، خيالًا. أما متحكم io في v2 فيفهم الكتابة المؤجلة وينسبها إلى المجموعة التي أوسخت الصفحات. هذا التغيير وحده يجعل io.max وio.weight يستحقان الضبط على خادم قاعدة بيانات، حيث لم تكن إعدادات v1 المكافئة تستحق ذلك غالبًا. ويذهب io.latency وio.cost أبعد من ذلك، إذ يحميان هدف زمن استجابة الحِمل لا عرض نطاقه — وإن كنت غير واثق من أين تبدأ، فـ io.latency أقل الاثنين تدخلًا.[kdoc]

تغيير وزن المعالج الذي لم يُعلن لك أحد عنه

هذا التغيير يستحق قسمًا خاصًا به لأنه أزاح أولوية المعالج لكل حِمل يعمل في حاوية، وفعل ذلك في مكوّن لا يقرأ معظم الناس سجل تغييراته. اشتقّت Kubernetes دائمًا حصص المعالج من الطلب بالصيغة milliCPU × 1024 / 1000، فحاوية تطلب معالجًا واحدًا تحصل على 1024 حصة. ثم يحوّل زمن تشغيل OCI الحصص إلى وزن v2، وكان التحويل الأصلي خطيًا عبر مجال الحصص في النواة [2, 262144]. طبّق تلك الحسبة على 1024 حصة فتحصل على وزن 39 — مقابل قيمة افتراضية في cgroup v2 تساوي 100.[k8scpu][runcissue]

# A quiet change that moved every containerised workload's CPU priority, with
# no release note in most people's changelog because it happened in the OCI
# runtime rather than in the orchestrator.

# Kubernetes derives shares from the CPU request, and always has:
#   cpu.shares = milliCPU * 1024 / 1000
#   request 1000m -> 1024 shares      request 100m -> 102 shares

# runc then converted shares to a v2 weight. The original conversion was
# linear over the kernel's [2, 262144] share range:
#   weight = 1 + ((shares - 2) * 9999) / 262142
python3 -c 'print(1 + ((1024 - 2) * 9999) // 262142)'
# 39
#
# 39. Against a cgroup v2 default of 100. Every container asking for a full CPU
# was scheduled at roughly a third of the weight of anything not in a
# container - including the kubelet and the runtime themselves.

# The replacement is log-based, and is chosen so that one CPU lands on the
# default rather than well below it:
python3 - <<'PY'
import math
def weight(shares):
    if shares == 0: return 0
    if shares <= 2: return 1
    if shares >= 262144: return 10000
    l = math.log2(shares)
    return math.floor(10 ** ((l*l + 125*l) / 612.0 - 7/34) + 0.99)
for req, sh in (("100m",102), ("500m",512), ("1",1024), ("4",4096), ("16",16384)):
    print("%-6s shares=%-6d weight=%d" % (req, sh, weight(sh)))
PY
# 100m   shares=102    weight=17
# 500m   shares=512    weight=59
# 1      shares=1024   weight=100
# 4      shares=4096   weight=303
# 16     shares=16384  weight=942
#
# 1024 lands on exactly 100 because the curve is fitted through three fixed
# points: 2 -> 1, 1024 -> 100, and 262144 -> 10000.

# Check what your nodes are doing, because this depends on the runtime version
# and not on the Kubernetes version. The new conversion ships in runc 1.3.2 and
# later, and in crun 1.23 and later:
runc --version; crun --version 2>/dev/null
cat /sys/fs/cgroup/kubepods.slice/*/*/cpu.weight 2>/dev/null | sort -n | uniq -c

والنتيجة أن حاوية تطلب معالجًا كاملًا كانت تتنافس بثلث أولوية أي شيء خارج الحاويات تقريبًا، على العقدة نفسها، بما في ذلك خدمات النظام وkubelet ذاته. الإصلاح يستبدل الخريطة الخطية بمنحنى لوغاريتمي مُركَّب على ثلاث نقاط ثابتة — 2 حصة عند وزن 1، و1024 عند 100، و262144 عند 10000 — فصار طلب معالج واحد يهبط عند القيمة الافتراضية في cgroup v2 بالضبط. وهو يُشحن في runc 1.3.2 وما بعده وفي crun 1.23 وما بعده. ويترتب على ذلك أمران يسهل إغفالهما. الأول أن هذا تغيير في زمن التشغيل لا في Kubernetes: يصلك عند ترقية runc أو crun، وقد يتزامن ذلك مع ترقية العنقود وقد لا يتزامن، فافحص إصدار زمن التشغيل لا إصدار العنقود. والثاني أنه يغيّر الأولويات النسبية بين الأحمال على عقد رحّلتها بالفعل، فإن كنت قد قِست سلوك المعالج على v2 قبل التغيير، فذلك القياس صار قديمًا.[runcpr]

نفّذها عبر systemd، لا عبر /sys

على أي خادم يعمل بـ systemd، الواجهة الصحيحة هي systemd لا نظام الملفات. وهذه ليست مسألة ذوق. systemd ينشئ شجرة cgroup، ويعيد تطبيق رؤيته لإعدادات موارد الوحدة كلما أُعيد تحميلها أو تشغيلها أو ضبطها — فقيمة كتبتها بـ echo داخل memory.max تعيش تمامًا حتى التغيير التالي غير ذي الصلة، ثم تختفي دون سطر واحد في السجل. وتنص وثيقة التفويض الرسمية على القاعدة بوضوح: كاتب واحد لكل شجرة فرعية. والمرور عبر systemd يمنحك أيضًا البقاء عبر إعادات الإقلاع مجانًا، وهو ما لا يمنحه التحرير اليدوي أبدًا.[sddeleg][sdresctl]

# Writing into /sys/fs/cgroup by hand works exactly until systemd next touches
# that unit, at which point your values are overwritten without warning.
# systemd owns the tree; ask it, and the setting also survives a reboot.

# Try a limit on something already running, for this boot only:
systemctl set-property --runtime nginx.service MemoryHigh=1G IOWeight=50

# Make it permanent. This writes a drop-in for you - under
# /etc/systemd/system.control/nginx.service.d/, not /etc/systemd/system/, which
# is why hand-searching for your setting in the obvious place turns up nothing.
# No daemon-reload needed.
systemctl set-property nginx.service MemoryMax=2G MemoryHigh=1800M CPUWeight=200

# Or write the drop-in yourself, which is what you want in configuration
# management: /etc/systemd/system/nginx.service.d/50-resources.conf
#
#   [Service]
#   MemoryMax=2G            # -> memory.max
#   MemoryHigh=1800M        # -> memory.high
#   MemoryMin=256M          # -> memory.min
#   MemorySwapMax=0         # -> memory.swap.max
#   CPUWeight=200           # -> cpu.weight
#   CPUQuota=150%           # -> cpu.max  (150% of one CPU)
#   IOWeight=50             # -> io.weight
#   IOReadBandwidthMax=/dev/nvme0n1 100M
#   TasksMax=512            # -> pids.max
#
# Note CPUQuota is a percentage of ONE CPU, not of the machine: 150% is 1.5
# cores. This is the systemd unit that trips people most often.

# Put a limit on a command you are about to run, without writing a unit:
systemd-run --scope --user -p MemoryMax=4G -p CPUQuota=200% -- ./import-job.sh

# And look at the tree systemd actually built, not the one you configured:
systemd-cgls --unit nginx.service
systemd-cgtop --order=memory --iterations=1

# Reading the values back. Note that there is no CPUQuota property to query:
# the unit-file setting CPUQuota= is exposed as CPUQuotaPerSecUSec, and asking
# for the name you wrote is the usual reason this returns nothing.
systemctl show nginx.service -p MemoryMax -p MemoryHigh -p CPUQuotaPerSecUSec

ثلاثة أوامر تنجز معظم العمل اليومي. systemctl set-property يطبّق الحد فورًا ويكتبه على القرص لإعادات الإقلاع القادمة، ما لم تمرر --runtime لجعله مؤقتًا. وsystemd-run --scope -p … يضع حدًا حول أمر أنت على وشك تشغيله، وهي الطريقة الأمينة لتقييد عملية استيراد أو نسخ احتياطي عابرة بدل الاتكال على الأمل. وsystemd-cgtop يعرض استهلاك الموارد لكل مجموعة لا لكل عملية، وهي الرؤية التي تريدها فعلًا حين ينشغل خادم حاويات ويعرض لك top مئتَي عملية بلا أي بنية. وهناك توجيه واحد يوقع الجميع: CPUQuota= نسبة من معالج واحد، فـ 150% تعني نواة ونصفًا لا 150% من الجهاز.[sdctl][sdcgtop]

Docker وPodman وحدود rootless التي صارت تعمل

بالنسبة للحاويات، الأخبار جيدة في معظمها، لأن زمن التشغيل يتولى الترجمة. --memory و--cpus و--memory-reservation و--pids-limit كلها ما زالت تعني ما كانت تعنيه؛ غاية الأمر أنها تهبط الآن على memory.max وcpu.max وmemory.low وpids.max. وعلى خادم موحد يعتمد Docker افتراضيًا سائق cgroup من نوع systemd ومجال أسماء cgroup خاصًا، وكلاهما الافتراض الصحيح. الاستثناء الجدير بالمعرفة هو --oom-kill-disable، الذي تقول وثائق Docker نفسها إنه يُهمَل على v2 — لا يُترجم، ولا يُنبَّه عليه، بل يُهمَل. ولا يوجد مكافئ له في v2 عن قصد، فأي شيء يعتمد عليه يحتاج إلى إعادة تفكير لا إلى نقل.[dockrun]

# --- Docker ---------------------------------------------------------------
docker info --format 'version={{.CgroupVersion}} driver={{.CgroupDriver}}'
# version=2 driver=systemd     <- the defaults on a unified host

# Most flags are unchanged, because the daemon translates them for you:
docker run --memory 2g --memory-reservation 1g --cpus 1.5 --pids-limit 512 nginx
#          -> memory.max        memory.low       cpu.max      pids.max

# Two that are not:
#   --oom-kill-disable   is discarded on cgroup v2. Not translated - discarded.
#                        There is no v2 equivalent, by design.
#   --kernel-memory      removed from the Engine in v23.0. It is gone, not moved.

# Setting the driver explicitly, in /etc/docker/daemon.json. Use systemd unless
# something specific stops you: it is the default on v2 and it is the only
# option that keeps one writer per subtree.
#   { "exec-opts": ["native.cgroupdriver=systemd"] }

# --- Podman rootless: this is the part that only works on v2 --------------
# Under v1, an unprivileged user could not be given controllers at all, so
# rootless resource limits silently did nothing. Under v2 they work, but only
# once systemd delegates the controllers to the user manager:
#
#   /etc/systemd/system/user@.service.d/delegate.conf
#   [Service]
#   Delegate=cpu cpuset io memory pids
#
sudo systemctl daemon-reload    # then log out and back in

# Verify from inside the user session, before blaming the container:
cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/cgroup.controllers
# cpuset cpu io memory pids     <- if memory is missing, --memory does nothing
podman info --format '{{.Host.CgroupsVersion}} {{.Host.CgroupManager}} {{.Host.OCIRuntime.Name}}'
# v2 systemd crun

الحاويات بلا صلاحيات جذر هي الموضع الوحيد الذي لا يكون فيه v2 ضريبة بل ميزة. فتفويض متحكمات v1 إلى مستخدم غير مميّز لم يكن يُعدّ آمنًا يومًا، ولذلك لم تكن معظم تطبيقات rootless تدعم حدود الموارد أصلًا على خادم يعمل بـ v1. وتحت v2، يجعلها التفويض الآمن للأشجار الفرعية تعمل كما ينبغي — لكن فقط بعد أن يفوّض systemd المتحكمات إلى مدير المستخدم، وهو ملف drop-in لـ user@.service وإعادة تسجيل دخول، وهو الخطوة الغائبة وراء كل تقرير تقريبًا عن أن "Podman في وضع rootless يتجاهل ‎--memory". افحص cgroup.controllers داخل شريحة المستخدم الخاصة بك قبل أن تلوم الحاوية: إن لم يكن memory مدرجًا هناك، فلن يُطبَّق أي خيار تمرره. وتفويض cpuset يحتاج إضافةً إلى systemd 244 أو أحدث.[podman][crun]

Kubernetes: ما هو صحيح، وما تقرأه باستمرار

والآن الجزء الذي يُنقل خطأً أكثر من غيره، مصوغًا بدقة. Kubernetes لم تُزل cgroup v1. الوثائق تصفه بأنه مهمَل اعتبارًا من الإصدار 1.35، والنتيجة العملية أن kubelet يرفض الإقلاع على عقدة cgroup v1 افتراضيًا. وذلك الافتراض حقل في KubeletConfiguration اسمه failCgroupV1، وضبطه على false يعيد السلوك القديم. ويقول KEP-5573 — وهو التحسين الذي سينفّذ الإزالة في النهاية — إن الإزالة لن تتم قبل 1.38. إن أخبرتك تدوينة أن 1.36 حذفت cgroup v1، فقد كانت مخطئة، والفرق بين "مهمَل مع إمكانية التجاوز" و"محذوف" هو الفرق بين ترحيل مخطط له وعطلة نهاية أسبوع ضائعة.[k8scg][kep5573]

# What the cluster thinks it is standing on. Run this first; mixed node pools
# are the normal case, not the exception.
kubectl get nodes -o custom-columns=\
'NODE:.metadata.name,KERNEL:.status.nodeInfo.kernelVersion,'\
'RUNTIME:.status.nodeInfo.containerRuntimeVersion,OS:.status.nodeInfo.osImage'

# The kernel version alone does not tell you the hierarchy. Ask each node.
# Note the -it: without it, kubectl debug does not attach, the output goes to
# the debug pod's log instead of your terminal, and you are left with one
# orphaned pod per node.
kubectl get nodes -o name | while read -r n; do
  printf '%-40s ' "${n#node/}"
  kubectl debug "$n" -it --image=busybox --profile=general -- \
    stat -fc %T /host/sys/fs/cgroup/ 2>/dev/null || echo '(debug unavailable)'
done
# Clean up afterwards - the debug pods are not removed for you:
kubectl delete pod -l app.kubernetes.io/managed-by=kubectl-debug 2>/dev/null

# On the node itself - the three files that have to agree:
stat -fc %T /sys/fs/cgroup/                       # cgroup2fs
grep -E '^(cgroupDriver|failCgroupV1):' /var/lib/kubelet/config.yaml
grep -A2 'runc.options' /etc/containerd/config.toml   # SystemdCgroup = true

# --- what is actually true about Kubernetes and cgroup v1 -----------------
# cgroup v1 is DEPRECATED as of v1.35, not removed. The kubelet refuses to
# start on a v1 node by default, and that default is overridable:
#
#   /var/lib/kubelet/config.yaml
#   apiVersion: kubelet.config.k8s.io/v1beta1
#   kind: KubeletConfiguration
#   cgroupDriver: systemd
#   failCgroupV1: false        # <- the escape hatch. Buys time, not a fix.
#
# KEP-5573 states the code removal will happen no earlier than 1.38.

# The v2-only features you get in exchange, and how to see them:
NODE=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
kubectl get --raw "/api/v1/nodes/$NODE/proxy/metrics/cadvisor" \
  | grep -E '^container_pressure_(cpu|memory|io)_' | head
# container_pressure_memory_stalled_seconds_total{...}
# container_pressure_memory_waiting_seconds_total{...}

متطلبات جانب العقدة متواضعة وتستحق الفحص لا الافتراض: نواة 5.8 أو أحدث، وcontainerd v1.4+ أو CRI-O v1.20+، وأن يُضبط kubelet وزمن التشغيل على سائق cgroup من نوع systemd تحديدًا — لا مجرد سائق متطابق بينهما. كان هذا الشرط الأخير مصدرًا متكررًا لعقد تعمل نصف عمل، لأنه كان يعتمد على توافق ملفَّي إعدادات؛ ومنذ الإصدار 1.34 صار kubelet يسأل زمن تشغيل CRI مباشرة عن السائق الذي يستخدمه، وهو ما يُنهي هذا الصنف من المشاكل كله لمن يستخدم زمن تشغيل حديثًا بما يكفي. وما تستردّه مقابل الترحيل مجموعة من الميزات لا توجد إلا على v2:[k8sdriver][k8spsi]

  • مقاييس PSI، وصلت إلى GA في 1.36. يقرأ kubelet ملفات cpu.pressure وmemory.pressure وio.pressure لكل مجموعة ويعرضها عبر Summary API ونقطة مقاييس cAdvisor. هذه الملفات غير موجودة تحت v1، فهي ليست ميزة يمكنك نقلها إلى الخلف — بل هي سبب للترحيل.
  • Memory QoS. يستطيع kubelet ضبط memory.high انطلاقًا من memoryThrottlingFactor بحيث تُسترد ذاكرة الحاوية بقوة قبل أن تُقتل، ويستطيع بشكل منفصل، تحت memoryReservationPolicy: TieredReservation، ضبط memory.min لبودات Guaranteed وmemory.low لبودات Burstable. واعتبارًا من الإصدار 1.36 لا يزال في مرحلة ألفا ومعطّلًا افتراضيًا، فتعامل معه كشيء تختبره لا كشيء تعتمد عليه — لكن شكله هذا هو الغاية من نموذج الذاكرة رباعي الطبقات.
  • أحمال rootless ومجالات أسماء المستخدمين التي تطبّق الحدود فعلًا. كل ما ورد في قسم Podman ينطبق على عقدة Kubernetes أيضًا، وهو سبب كون وصول مجالات أسماء المستخدمين إلى GA وcgroup v2 قصتين مترابطتين لا متزامنتين مصادفة.
  • حساب أمين للإدخال/الإخراج لكل بود. الكتابات المؤقتة تُنسب إلى المجموعة التي تسببت بها، ما يجعل استهلاك القرص لكل بود رقمًا تستطيع التصرف بناءً عليه لا رقمًا تعتذر عنه.

ملاحظة تخطيطية واحدة يسهل الخطأ فيها في بيئة مختلطة. لأن العامل الدافع هو systemd لا Kubernetes، تميل مجمّعات العقد إلى ترحيل نفسها مع تقدّم صور الأساس، قبل أي قرار على مستوى العنقود بوقت طويل. لا بأس بذلك، لكنه يعني أنك قد تنتهي إلى عنقود نصف عقده على v2 ونصفها ليس كذلك، تشغّل الأحمال نفسها بسلوك ذاكرة ومعالج مختلف اختلافًا جوهريًا — ولن يخبرك أي تنبيه في أي مكان بذلك. دقّق العقد، ولا تستنتجها من إصدار العنقود.[k8sqos]

تفعيل v2 حيث لم يُفعَّل بعد

إن كانت لديك خوادم ما تزال على v1 أو الهجين، فهذا هو الجزء الآلي، وهو قصير. لاحظ أولًا أنه على systemd 258 وما بعده لا يوجد ما يُفعَّل ولا ما يُعطَّل: الوضع الموحد هو الوضع الوحيد الذي تدعمه الشيفرة. ومع ذلك، لا تترك معامل النواة القديم على تلك الخوادم. فـ systemd 258 لم يعد يتصرف بناءً عليه، لكن إن كان initrd ما يزال يتصرف بناءً عليه ويركّب هرمية v1، فإن العملية رقم 1 ترفض الإقلاع وتطالبك بإزالة خيار سطر الأوامر القديم — وهو فشل أفضل بكثير من فشل صامت، لكنه يظل إقلاعًا عليك إصلاحه من طرفية. كل ما يلي ينطبق فقط على الخوادم القديمة بما يكفي ليبقى لها خيار — وعلى تلك، ينبغي أن يُتخذ القرار في نافذة صيانة تنقل أيضًا أزمنة تشغيل الحاويات، لأن عقدة تُقلع إلى v2 بسائق cgroupfs هي عقدة تعود في حالة مثيرة للاهتمام.[sd258]

# Only needed on hosts old enough to still default to v1 or hybrid. On
# systemd 258 and later there is nothing to enable: unified is the only mode.

# 1. Check you can. Kubernetes wants kernel 5.8+; systemd 258 needs 5.4 as an
#    absolute floor and recommends 5.7. Below that, upgrade the OS instead.
uname -r

# 2. Set the kernel parameter. Debian and Ubuntu. The grep guard matters:
#    without it, running this twice adds the parameter twice.
grep -q 'systemd.unified_cgroup_hierarchy' /etc/default/grub || \
  sudo sed -i 's/^GRUB_CMDLINE_LINUX="/&systemd.unified_cgroup_hierarchy=1 /' \
       /etc/default/grub
sudo update-grub

#    RHEL, Fedora, Rocky, Alma - grubby, and note ALL rather than the running
#    kernel, or the setting vanishes at the next kernel update:
sudo grubby --update-kernel=ALL --args="systemd.unified_cgroup_hierarchy=1"

# 3. Line up the container runtimes in the SAME maintenance window. A node
#    that reboots into v2 with a cgroupfs driver is a node that does not come
#    back cleanly.
#    /etc/docker/daemon.json      -> "exec-opts": ["native.cgroupdriver=systemd"]
#    /etc/containerd/config.toml  -> SystemdCgroup = true
#    /var/lib/kubelet/config.yaml -> cgroupDriver: systemd

sudo reboot

# 4. Verify, in this order. If step one disagrees with step three, stop.
stat -fc %T /sys/fs/cgroup/            # cgroup2fs
cat /sys/fs/cgroup/cgroup.controllers  # non-empty
systemctl --failed
docker info --format '{{.CgroupVersion}}/{{.CgroupDriver}}'   # 2/systemd

# Rolling back is removing the parameter and rebooting - but only while your
# systemd is older than 258. After that the parameter is inert and the only
# way back is downgrading the OS, which is not a rollback plan.
ما الذي ينكسرلماذاماذا تفعل بدلًا منه
سكربتات تقرأ /sys/fs/cgroup/memory/…أدلة المتحكمات المنفصلة غير موجودة تحت الهرمية الموحدةاقرأ مسارات v2 المسطحة، أو اسأل systemctl show عن القيمة
أدوات تكتب داخل مجموعات يملكها systemdsystemd يعيد فرض إعداداته عند أي تغيير في الوحدة، وبصمتsystemctl set-property، أو ملف drop-in
تعليم حركة الشبكة المعتمد على net_clsأُزيل المتحكم دون ملف بديلeBPF مرتبط بمسار المجموعة، ويُطابَق من nftables
قوائم السماح في devices.allowاستُبدل المتحكم بنوع برنامج eBPFDeviceAllow= في وحدة، أو برنامج eBPF للأجهزة
تخطيطات فيها حدود في كل مستوى من الشجرةقيد لا عمليات داخليةادفع العمليات إلى الأوراق؛ وأبقِ المجموعات الداخلية فارغة
--oom-kill-disableمُهمَل على v2، عن قصداضبط memory.max بحجم مناسب، واستخدم memory.high لتحصل على إنذار أولًا
حدود تبديل منسوخة حرفيًاmemory.swap.max هو التبديل وحده، لا الذاكرة مع التبديلاضبط الفرق بين رقمَي v1، أو 0 صريحًا

ملاحظتان عن التراجع، إذ لا أحد يخطط لترحيل من دونه. ما دام systemd لديك بالإصدار 257 أو أقدم، فالتراجع هو إزالة معامل النواة وإعادة الإقلاع، وهو رخيص فعلًا. وحالما تصل إلى 258 أو ما بعده، لا يوجد أي طريق مدعوم للعودة سوى تخفيض إصدار نظام التشغيل، وهذه ليست خطة تراجع — بل إعادة تثبيت. رتّب أسطولك تبعًا لذلك: ابدأ بالخوادم التي ما زال لها مخرج، وتعلّم منها، ثم انقل بعد ذلك فقط تلك التي لا مخرج لها.[dockrun]

التحقّق، بدلًا من التمنّي

التحقّق ليس مسألة ذوق. الغاية من السكربت أدناه أن كل سطر فيه إما يطبع OK أو يشرح نفسه، وأن رمز الخروج هو عدد الإخفاقات، بحيث يمكن وضعه مباشرة فيما يعمل بعد إعادة الإقلاع. وفيه جزآن يستحقان مكانهما. الأول يطبع جردًا بكل وحدة تعمل ولها MemoryMax مضبوط فعلًا، تقارنه بالقائمة التي قصدت ضبطها — فالوحدة الغائبة عن ذلك المخرَج لديها ملف drop-in في الدليل الخطأ، ولا سبيل لاكتشاف ذلك دون معرفة الحالة المقصودة. والثاني يمسح الشجرة كلها بحثًا عن الخنق، لأن تسلّق nr_throttled على خدمة لا يشتكي منها أحد هو العلامة الكلاسيكية على حد تُرجم بضيق مفرط.[sdcgls]

#!/usr/bin/env bash
# Post-migration verification. Every check either prints OK or explains
# itself; nothing here is judged by eye. Exit code is the number of failures.
fail=0
chk() { if eval "$2" >/dev/null 2>&1; then printf 'OK   %s\n' "$1";
        else printf 'FAIL %s\n' "$1"; fail=$((fail+1)); fi; }

chk 'unified hierarchy'        '[ "$(stat -fc %T /sys/fs/cgroup/)" = cgroup2fs ]'
chk 'controllers available'    '[ -s /sys/fs/cgroup/cgroup.controllers ]'
chk 'memory controller'        'grep -qw memory /sys/fs/cgroup/cgroup.controllers'
chk 'io controller'            'grep -qw io     /sys/fs/cgroup/cgroup.controllers'
chk 'single cgroup line'       '[ "$(wc -l < /proc/self/cgroup)" -eq 1 ]'
chk 'no v1 leftovers mounted'  '! mount | grep -q "type cgroup "'
chk 'no failed units'          '[ -z "$(systemctl list-units --state=failed --no-legend)" ]'
chk 'PSI available'            '[ -r /sys/fs/cgroup/cpu.pressure ]'

# Limits are actually applied, rather than merely configured. A unit whose
# MemoryMax reads "infinity" after you set it is a unit whose drop-in is in
# the wrong place - a very common outcome of hand-editing.
for u in $(systemctl list-units --type=service --state=running \
           --no-legend --plain | awk '{print $1}'); do
  m=$(systemctl show "$u" -p MemoryMax --value)
  [ "$m" != "infinity" ] && printf '     %-34s MemoryMax=%s\n' "$u" "$m"
done

# Nothing is being silently throttled. nr_throttled climbing on a service that
# is not busy means cpu.max is too tight, and it will not appear in load
# average. Search the whole tree, not just the top-level slices: throttling
# happens on the leaf that holds the process.
find /sys/fs/cgroup -name cpu.stat -exec \
  awk '/^nr_throttled/ && $2>0 {print FILENAME": "$0}' {} + 2>/dev/null

# Containers agree with the host.
command -v docker >/dev/null && \
  chk 'docker on v2/systemd' '[ "$(docker info -f "{{.CgroupVersion}}/{{.CgroupDriver}}")" = 2/systemd ]'

printf '\n%d failure(s)\n' "$fail"; exit "$fail" 

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

الترتيب الذي تنفّذ به هذا

القرار، مضغوطًا. الخلاصة الأمينة أن معظم الناس رحّلوا بالفعل دون مشروع، وأن ما تبقى ليس الخوادم بل الأدوات: السكربتات والوكلاء ولوحات المتابعة التي ما تزال تقرأ مسارات v1 وستظل تعيد أصفارًا بهدوء حتى يفحصها أحد. تمنحك Kubernetes مهلة حتى 1.38 على أقرب تقدير، وDocker مهلة أطول من ذلك، لكن أيًا من هذين التاريخين ليس موعدك النهائي. موعدك النهائي هو اللحظة التي تنقل فيها ترقية صورة الأساس التالية إصدار systemd إلى ما بعد 258 — وعلى معظم البيئات، حدث ذلك بالفعل.[kep5573]

إن كان وضعك…فالموعد النهائي هو…والعمل المطلوب هو…
كل شيء يُبلّغ أصلًا بـ cgroup2fsمضى بالفعل، بهدوءالأدوات فقط: ابحث عن مسارات v1 التي ما تزال تُقرأ وأصلحها
حفنة خوادم قديمة على v1 أو الهجينعند وصول ترقية نظام التشغيل التالية لهامعامل النواة مع سائقي أزمنة التشغيل، ونافذة صيانة واحدة لكل منها
عقد Kubernetes على صور أساس مختلطةمع تقدّم صور العقد، لا عند ترقية العنقوددقّق كل عقدة؛ ولا تستنتج الهرمية من إصدار العنقود
خوادم Docker بلا منصة تنسيقأهملها Engine 29.0؛ والإزالة بعد سنواتإلحاح منخفض من جهة Docker، لكن systemd سينقل الخادم أولًا على أي حال
أدوات خاصة تكتب ملفات cgroup مباشرةالآن، وهو المشروع الحقيقيأعد كتابتها مقابل واجهات systemd قبل أن تتحرك الخوادم من تحتك
جهاز مغلق أو وكيل مورّد لا تستطيع تغييرهجدول المورّد، وهو ليس جدولكاحصل عليه كتابةً، واعزل الخادم إن كان الجواب غير مُرضٍ
  1. دقّق قبل أن تخطط. شغّل سكربت الأسطول على كل خادم واحتفظ بالمخرجات. أنت تبحث عن شيئين: خوادم ما تزال على v1 أو الهجين، ومسارات v1 مكتوبة صراحةً في /etc و/opt و/usr/local. القائمة الثانية أطول من الأولى دائمًا تقريبًا، وهي العمل الحقيقي.
  2. أصلح الأدوات أولًا، ما دامت الهرميتان موجودتين. أي شيء يقرأ /sys/fs/cgroup ينبغي أن يتعامل مع التخطيطين، أو أن يقرأ عبر systemctl show بدلًا من ذلك. فعل هذا قبل نقل الخوادم يعني أنك تستطيع اختبار الإصلاح على الشيء الذي يُفترض أن يصلحه.
  3. ترجم الحدود بالمعنى، لا بالاسم. صفّان في جدول التحويل يغيّران السلوك: التبديل، حيث memory.swap.max هو التبديل وحده، ووزن المعالج، الذي يختلف مقياسه عن الحصص بأكثر من عشرة أضعاف. وكل صف آخر مجرد إعادة تسمية.
  4. انقل أزمنة التشغيل والخوادم في النافذة نفسها. معامل النواة، وسائق Docker، وSystemdCgroup في containerd، وcgroupDriver في kubelet — الأربعة معًا، وإعادة إقلاع واحدة، ثم تحقّق قبل الانتقال إلى الدفعة التالية.
  5. اقبض العائد. اربط memory.high وmemory.events، وضع PSI على لوحة متابعة، وفعّل حدود الإدخال/الإخراج لكل مجموعة التي لم تكن تستحق الضبط تحت v1. هذا الترحيل لا ميزة في نهايته ما لم تطالب بواحدة.

يقع هذا إلى جانب ثلاث قطع أخرى من التحوّل نفسه، وهي تتراكم: ترحيل سكربتات SysV init وrc.local إلى وحدات systemd، لأن التغييرين يصلان في إصدارات systemd نفسها وعلى الخوادم نفسها؛ وترقية خادم Ubuntu من 24.04 إلى 26.04، وهو الموضع الذي ستعبر عنده معظم الأساطيل الخط فعلًا؛ والتغييرات الكاسرة في Docker Engine 29، وإهماله لـ cgroup v1 هو النصف الخاص بالحاويات من هذه القصة. وإن كنت توازن أصلًا كم تحتاج من هذا التعقيد، فإن متى لا تستخدم Kubernetes هو الوجه الآخر لذلك النقاش.

الأسئلة الشائعة

كيف أعرف إن كنت على cgroup v1 أم v2؟

شغّل stat -fc %T /sys/fs/cgroup/. إن طبع cgroup2fs فأنت على الهرمية الموحدة؛ وإن طبع tmpfs فأنت على v1 أو على التخطيط الهجين. لا تستخدم mount | grep cgroup2: فالتخطيط الهجين يركّب هرمية cgroup2 على /sys/fs/cgroup/unified دون ربط أي متحكم بها، فيطابق الـ grep ويخبرك بعكس الحقيقة. والتأكيد الثاني هو /proc/self/cgroup، الذي يحتوي تحت v2 على سطر واحد فقط يبدأ بـ 0::.

هل أزال Kubernetes 1.36 دعم cgroup v1؟

لا. cgroup v1 مهمَل اعتبارًا من Kubernetes 1.35، والأثر العملي أن kubelet يرفض الإقلاع على عقدة cgroup v1 افتراضيًا. وذلك الافتراض حقل في KubeletConfiguration اسمه failCgroupV1، وضبطه على false يعيد السلوك السابق. وينص KEP-5573، وهو التحسين الذي سيزيل الشيفرة في النهاية، على أن الإزالة لن تحدث قبل الإصدار 1.38. عدة تدوينات واسعة الانتشار تقول غير ذلك؛ والمرجع هو الـ KEP.

هل memory.swap.max هو نفسه memory.memsw.limit_in_bytes؟

لا، وهذا أشد سوء فهم ضررًا في الترحيل كله. في cgroup v1 كان memory.memsw.limit_in_bytes يحدّ الذاكرة مع التبديل معًا، فمجموعة لها limit=2G وmemsw=3G كان بإمكانها استخدام 2 غيغابايت من الذاكرة إضافةً إلى 1 غيغابايت من التبديل. أما في cgroup v2 فيحدّ memory.swap.max التبديل وحده. ونسخ القيمة 3G كما هي يمنح ثلاثة أضعاف مخصّص التبديل السابق. الترجمة الصحيحة هي الفرق بين قيمتَي v1، وحيث يكون ذلك الفرق صفرًا اضبط 0 صريحًا.

ما الفرق بين memory.high و memory.max؟

memory.max حد صارم: حين تبلغه المجموعة ولا تستطيع الاسترداد، يعمل قاتل OOM داخل تلك المجموعة. وmemory.high خانق: تجاوزه يضع المجموعة تحت ضغط استرداد مكثّف ويبطئ عملياتها، ووثائق النواة صريحة في أن تجاوزه لا يستدعي قاتل OOM أبدًا. عمليًا تضبط memory.high دون memory.max بقليل وتنبّه على عدّاد high في memory.events، فتحصل على إنذار قبل أن يُقتل أي شيء. ولم يكن لدى cgroup v1 أي آلية مكافئة.

لماذا حصلت حاوياتي على معالج أقل بعد الانتقال إلى cgroup v2؟

بسبب التحويل من الحصص إلى الوزن، لا بسبب v2 نفسه. تشتق Kubernetes قيمة cpu.shares من طلب المعالج بالصيغة milliCPU × 1024 / 1000، فطلب معالج واحد ينتج 1024 حصة. والتحويل الأصلي في أزمنة تشغيل OCI كان يسقط ذلك خطيًا على مجال أوزان v2 فينتج 39، مقابل قيمة افتراضية في cgroup v2 تساوي 100 — فكانت الحاويات تتنافس بثلث أولوية العمليات خارج أي حاوية تقريبًا. أما البديل اللوغاريتمي فيُسقط 1024 حصة على 100 بالضبط، وهو يُشحن في runc 1.3.2 وما بعده وفي crun 1.23 وما بعده. افحص إصدار زمن التشغيل لديك، لا إصدار Kubernetes: فهذا يصلك مع ترقية صورة العقدة أو زمن التشغيل، لا مع ترقية مستوى التحكم.

هل ما زال بإمكاني فرض cgroup v1 عبر systemd.unified_cgroup_hierarchy=0؟

على systemd 257 وما قبله فقط، وهناك يحتاج الأمر إلى systemd.unified_cgroup_hierarchy=0 وSYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1 معًا على سطر أوامر النواة. توقف systemd 256 عن الإقلاع على cgroup v1 افتراضيًا وأدخل مخرج الطوارئ ذاك؛ والإصدار 257 ما زال يحترمه؛ ثم أزال systemd 258 دعم cgroup v1 بالكامل بما فيه مخرج الطوارئ. وعلى 258 وما بعده لم يعد systemd يتصرف بناءً على الخيار — لكن أزله بدل تركه في مكانه، لأن initrd إن كان ما يزال يتصرف بناءً عليه ويركّب هرمية v1، فإن العملية رقم 1 ترفض العمل وتطالبك بمسح خيار سطر الأوامر القديم.

ما البديل عن متحكمات devices و net_cls و net_prio؟

eBPF، في الحالات الثلاث. صار التحكم في الوصول إلى الأجهزة برنامج eBPF من النوع BPF_PROG_TYPE_CGROUP_DEVICE، يتلقى الرقمين الكبير والصغير ونوع الجهاز ونوع الوصول ثم يعيد السماح أو -EPERM؛ وعلى خادم يعمل بـ systemd يقود توجيه الوحدة DeviceAllow= هذا نيابةً عنك. أما تصنيف الشبكة وترتيب أولوياتها فلا متحكم لهما في v2 ولا ملف واجهة بديل: تربط برنامج eBPF بمسار المجموعة وتطابق عليه من iptables أو nftables. وهذه هي الصفوف الثلاثة في جدول التحويل التي تتطلب هندسة لا استبدال مسارات.

لماذا يتجاهل Podman في وضع rootless حد الذاكرة على cgroup v2؟

في الغالب الأعم لأن systemd لم يفوّض متحكم الذاكرة إلى مدير المستخدم لديك. أنشئ ملف drop-in لـ user@.service يحتوي Delegate=cpu cpuset io memory pids، ثم أعد تحميل systemd، ثم سجّل الخروج والدخول من جديد. تحقّق بـ cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/cgroup.controllers: إن لم يكن memory في تلك القائمة، فلن يُطبَّق أي خيار تمرره إلى Podman. وتفويض cpuset تحديدًا يتطلب systemd 244 أو أحدث. وتحت cgroup v1 لم يكن تفويض المتحكمات إلى مستخدم غير جذري يُعدّ آمنًا، ولم تكن معظم تطبيقات rootless تدعمه، ولهذا فحدود الموارد في وضع rootless قدرة يضيفها v2 لا خللًا أدخله.

هل عليّ تغيير أوامر Docker لديّ؟

في معظمها لا. --memory و--cpus و--memory-reservation و--pids-limit تحتفظ كلها بمعناها وتُترجَم إلى memory.max وcpu.max وmemory.low وpids.max. وهناك استثناءان مهمان: --oom-kill-disable يُهمَل على cgroup v2 دون مكافئ، و--kernel-memory أُزيل من المحرك منذ الإصدار 23.0. وعلى خادم موحد يعتمد Docker افتراضيًا سائق cgroup من نوع systemd ومجال أسماء cgroup خاصًا، ويجدر بك ترك الاثنين كما هما ما لم يفرض شيء محدد غير ذلك.

هل هناك مكسب فعلي في الأداء، أم أن هذا كلفة ترحيل خالصة؟

هناك مكسب حقيقي، متركّز في الإدخال/الإخراج وفي إمكانية الرصد. متحكم blkio في v1 كان يحسب الإدخال/الإخراج المباشر فقط، فكانت الكتابات المؤقتة تُحمَّل على أي خيط نواة أفرغها وكانت حدود الكتابة لكل مجموعة زخرفية إلى حد بعيد؛ أما متحكم io في v2 فيفهم الكتابة المؤجلة وينسبها بشكل صحيح، ما يجعل io.max وio.latency يستحقان الضبط على خوادم قواعد البيانات والبناء. وفوق ذلك، فإن PSI — cpu.pressure وmemory.pressure وio.pressure — لا يوجد إلا تحت v2، وهو الفرق بين أن تعرف أن الجهاز تحت ضغط وأن تكتشف ذلك حين يموت شيء.

زمن التشغيل الذي تحته يسير على الساعة نفسها: يغادر containerd 1.7 الدعم الممتد في سبتمبر 2026، ولأجل Kubernetes 1.36 لا تُدرِج مصفوفة دعم المشروع سوى 2.3.0+ و2.2.0+ — بلا أي مدخل لـ 1.x. الترحيل من containerd 1.7 إلى 2.x يغطي إعادة كتابة الإعداد إلى الإصدار 3، وتحويل السجلّات الذي يمنع تحميل إضافة CRI، ولماذا 2.3 هو الفرع الوحيد الذي يستحق الاستهداف.

مستوى بيانات الـ Service على العُقد نفسها يسير على ساعته الخاصة: أعلن Kubernetes 1.37 إهمال وضع ipvs في kube-proxy خلف feature gate، ويُعطّله 1.40 افتراضيًا، ويحذف 1.43 الشيفرة. نقل kube-proxy من IPVS إلى nftables يغطي حد النواة 5.13، وسلوك NodePort الذي يتغيّر من تحتك، وواجهة kube-ipvs0 المتبقية التي تبتلع حركة المرور إن لم ينظّفها أحد.

ملاحظة على مستوى الإصدار، لأن دفتر حساب 1.37 ليس كما تصفه معظم التغطية: التغيير الذي قد يترك الحاويات فعليًا في ContainerCreating هو وصول SELinuxMount إلى GA، بينما جاء فشل cgroup v1 في 1.35، وقيد الـ static pods في 1.34، أما حافة containerd فما زالت أمامك في 1.38. ما ينكسر فعليًا عند الترقية إلى Kubernetes 1.37 يفصل الأعمدة الثلاثة ويقدّم التدقيق الذي يجب تنفيذه قبل الترقية لا بعدها.

المصادر

المصادر الأولية أولًا: وثائق النواة تعرّف كل ملف واجهة اقتُبس هنا، وملاحظات إصدارات المشاريع ومقترحات التحسين الخاصة بها هي البيان الموثوق الوحيد لما أُزيل ومتى. وحيث يناقض هذا المقال التغطية الثانوية — في شأن Kubernetes وDocker خصوصًا — فالخلاف مع العنوان لا مع المصدر الأولي.

  1. Linux kernel — Control Group v2: the normative document. Every interface file, default value and range quoted in this article was checked here, including the fact that memory.max defaults to "max" and cpu.max defaults to "max 100000"
  2. Control Group v2 — Memory interface files: memory.min, memory.low, memory.high and memory.max, and the sentence that going over memory.high never invokes the OOM killer. This is the four-tier model cgroup v1 did not have
  3. Control Group v2 — IO interface files: io.weight, io.max with its rbps/wbps/riops/wiops keys, io.latency and io.cost. The v2 io controller is also the first one that accounts for writeback correctly
  4. Control Group v2 — No Internal Process Constraint: non-root cgroups can only distribute resources to children when they hold no processes of their own. This single rule is what breaks hand-rolled v1 layouts on contact
  5. Control Group v2 — Delegation: the model that makes rootless containers possible, and the reason a delegated subtree must not be allowed to write its own resource-control files
  6. Linux kernel — Memory Resource Controller (cgroup v1): the source for what memory.limit_in_bytes and memory.memsw.limit_in_bytes actually meant, which is the only way to see how different memory.swap.max is
  7. cgroups(7) — the manual page, including the statement that there is no direct equivalent of the net_cls and net_prio controllers, and that iptables gained support for eBPF filters hooking on cgroup v2 pathnames instead
  8. BPF_PROG_TYPE_CGROUP_DEVICE — the eBPF program type that replaced the v1 devices controller: it receives major, minor, device type and access type, and returns allow or -EPERM
  9. systemd — NEWS: the upstream changelog and the authoritative statement of what happened in which release. The v258 section carries both the cgroup v1 removal and the kernel baseline bump quoted here
  10. systemd v258 release notes — "Support for cgroup v1 ('legacy' and 'hybrid' hierarchies) has been removed", and the bump of the minimum kernel baseline to v5.4 with v5.7 recommended
  11. systemd v256 release notes — the release that stopped booting cgroup v1 by default and introduced the SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1 escape hatch that v258 then took away
  12. systemd.resource-control(5) — MemoryMax=, MemoryHigh=, MemoryLow=, MemoryMin=, MemorySwapMax=, CPUWeight=, CPUQuota=, IOWeight=, IOReadBandwidthMax= and TasksMax=: the directive names for every interface file in the conversion table
  13. systemctl(1) — set-property, and the fact that it applies changes immediately and stores them on disk for future boots unless --runtime is passed
  14. systemd-run(1) — --scope and --property=, the pair that lets you put a limit on a command you are about to run without writing a unit file first
  15. systemd-cgls(1) — recursively show control group contents: the fastest way to see the tree systemd actually built, as opposed to the one you think you configured
  16. systemd-cgtop(1) — top control groups by resource usage, which is the per-cgroup view that plain top cannot give you
  17. systemd — Control Group APIs and Delegation: upstream's own rules for who owns which part of the tree, and why writing into systemd's cgroups from outside systemd is a bug rather than a technique
  18. Kubernetes — About cgroup v2: the requirements (kernel 5.8 or later, containerd v1.4+, cri-o v1.20+, systemd cgroup driver), the stat -fc %T check, and the deprecation notice marking cgroup v1 deprecated as of v1.35
  19. KEP-5573, Remove cgroup v1 support — the document that says removal "will be done no earlier than 1.38". Worth reading before believing any headline that says Kubernetes has already removed it
  20. Kubernetes blog — New Conversion from cgroup v1 CPU Shares to v2 CPU Weight: why a container requesting 1 CPU ended up below the default weight on v2, and the replacement formula
  21. runc pull request 4785 — the dependency bump that pulls the new shares-to-weight conversion into runc. The conversion itself lives in the opencontainers/cgroups library, which is where the change reaches everyone regardless of orchestrator
  22. runc issue 4772 — the report behind that change: the linear conversion gave 1024 shares a weight of 39 against a default of 100, so containers lost CPU to everything not in a container
  23. Kubernetes blog — Autoconfiguration for Node Cgroup Driver Goes GA: the kubelet now asks the CRI runtime which cgroup driver it uses instead of trusting two files to agree
  24. Kubernetes — Understand PSI metrics: pressure stall information read from cpu.pressure, memory.pressure and io.pressure, which exist only under cgroup v2
  25. Kubernetes blog — Tiered Memory Protection with Memory QoS: the kubelet writing memory.high and, under memoryReservationPolicy, memory.min and memory.low. None of this has a cgroup v1 equivalent
  26. Docker — Runtime metrics: the cgroup v2 requirements (containerd v1.4+, kernel v4.15+ with v5.2+ recommended), the default driver being systemd on v2 and cgroupfs on v1, and the sentence that --oom-kill-disable is discarded on v2
  27. Docker Engine — Deprecated features: the table row recording that support for cgroup v1 was deprecated in Engine v29.0, with no removal version set, and that the kernel memory limit was removed back in v23.0
  28. moby issue 51111 — the proposal to deprecate cgroup v1 while maintaining it until the enterprise distributions that still need it reach end of life. This is why Docker's deadline is much later than systemd's
  29. Rootless Containers — cgroup v2: the systemd user-manager Delegate= drop-in that gives an unprivileged user real cpu, memory, io and pids limits, and the note that delegating cpuset needs systemd 244 or newer
  30. crun — the OCI runtime with native cgroup v2 support and the default on current Podman installations, which matters because runc reached v2 later and older builds handle it badly
  31. Red Hat Enterprise Linux 10 release notes — the release where systemd no longer supports booting in cgroup v1 mode at all, for readers whose deadline is an enterprise distribution rather than upstream
  32. Linux kernel — PSI, Pressure Stall Information: what the numbers in cpu.pressure, memory.pressure and io.pressure mean, and why "some" and "full" are different questions

Was this useful?