تصحيح زمن انتقال الشبكة في Kubernetes

تصحيح زمن انتقال الشبكة في Kubernetes

قبل عامين، Kubernetes تمت مناقشتها بالفعل على مدونة جيثب الرسمية. ومنذ ذلك الحين، أصبحت التكنولوجيا القياسية لنشر الخدمات. يدير Kubernetes الآن جزءًا كبيرًا من الخدمات الداخلية والعامة. مع نمو مجموعاتنا وأصبحت متطلبات الأداء أكثر صرامة، بدأنا نلاحظ أن بعض الخدمات على Kubernetes كانت تعاني بشكل متقطع من زمن الوصول الذي لا يمكن تفسيره من خلال تحميل التطبيق نفسه.

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

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

القضاء على التعقيد غير الضروري في السلسلة التي تؤدي إلى الفشل

ومن خلال إعادة إنتاج نفس المثال، أردنا تضييق نطاق تركيز المشكلة وإزالة طبقات التعقيد غير الضرورية. في البداية، كان هناك الكثير من العناصر في التدفق بين كبسولات Vegeta وKubernetes. لتحديد مشكلة أعمق في الشبكة، تحتاج إلى استبعاد بعضها.

تصحيح زمن انتقال الشبكة في Kubernetes

يقوم العميل (Vegeta) بإنشاء اتصال TCP مع أي عقدة في المجموعة. تعمل Kubernetes كشبكة متراكبة (فوق شبكة مركز البيانات الحالية) التي تستخدم IPIPأي أنه يقوم بتغليف حزم IP الخاصة بشبكة التراكب داخل حزم IP الخاصة بمركز البيانات. عند الاتصال بالعقدة الأولى، يتم تنفيذ ترجمة عنوان الشبكة ترجمة عنوان الشبكة (NAT) مناسب لترجمة عنوان IP ومنفذ عقدة Kubernetes إلى عنوان IP والمنفذ في شبكة التراكب (على وجه التحديد، جراب التطبيق). بالنسبة للحزم الواردة، يتم تنفيذ التسلسل العكسي للإجراءات. إنه نظام معقد به الكثير من الحالات والعديد من العناصر التي يتم تحديثها وتغييرها باستمرار مع نشر الخدمات ونقلها.

فائدة tcpdump في اختبار Vegeta، هناك تأخير أثناء مصافحة TCP (بين SYN وSYN-ACK). لإزالة هذا التعقيد غير الضروري، يمكنك استخدام hping3 لإجراء "أصوات" بسيطة مع حزم SYN. نتحقق مما إذا كان هناك تأخير في حزمة الاستجابة، ثم نقوم بإعادة تعيين الاتصال. يمكننا تصفية البيانات لتشمل فقط الحزم التي يزيد حجمها عن 100 مللي ثانية والحصول على طريقة أسهل لإعادة إنتاج المشكلة من اختبار Vegeta للطبقة السابعة للشبكة الكاملة. فيما يلي "أصوات" عقدة Kubernetes باستخدام TCP SYN/SYN-ACK على "منفذ العقدة" للخدمة (7) بفواصل زمنية تبلغ 30927 مللي ثانية، تمت تصفيتها بواسطة الاستجابات الأبطأ:

theojulienne@shell ~ $ sudo hping3 172.16.47.27 -S -p 30927 -i u10000 | egrep --line-buffered 'rtt=[0-9]{3}.'

len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=1485 win=29200 rtt=127.1 ms

len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=1486 win=29200 rtt=117.0 ms

len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=1487 win=29200 rtt=106.2 ms

len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=1488 win=29200 rtt=104.1 ms

len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=5024 win=29200 rtt=109.2 ms

len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=5231 win=29200 rtt=109.2 ms

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

بعد ذلك، نريد معرفة المكونات التي قد تكون متورطة في حدوث الازدحام. ربما هذه بعض من مئات قواعد iptables في NAT؟ أم أن هناك أي مشاكل مع نفق IPIP على الشبكة؟ إحدى الطرق للتحقق من ذلك هي اختبار كل خطوة من خطوات النظام عن طريق إزالتها. ماذا يحدث إذا قمت بإزالة NAT ومنطق جدار الحماية، ولم يتبق سوى جزء IPIP:

تصحيح زمن انتقال الشبكة في Kubernetes

ولحسن الحظ، يسهل Linux الوصول إلى طبقة تراكب IP مباشرة إذا كان الجهاز على نفس الشبكة:

theojulienne@kube-node-client ~ $ sudo hping3 10.125.20.64 -S -i u10000 | egrep --line-buffered 'rtt=[0-9]{3}.'

len=40 ip=10.125.20.64 ttl=64 DF id=0 sport=0 flags=RA seq=7346 win=0 rtt=127.3 ms

len=40 ip=10.125.20.64 ttl=64 DF id=0 sport=0 flags=RA seq=7347 win=0 rtt=117.3 ms

len=40 ip=10.125.20.64 ttl=64 DF id=0 sport=0 flags=RA seq=7348 win=0 rtt=107.2 ms

وبالنظر إلى النتائج، لا تزال المشكلة قائمة! وهذا يستثني iptables وNAT. إذن المشكلة هي TCP؟ دعونا نرى كيف تسير عملية اختبار اتصال ICMP العادية:

theojulienne@kube-node-client ~ $ sudo hping3 10.125.20.64 --icmp -i u10000 | egrep --line-buffered 'rtt=[0-9]{3}.'

len=28 ip=10.125.20.64 ttl=64 id=42594 icmp_seq=104 rtt=110.0 ms

len=28 ip=10.125.20.64 ttl=64 id=49448 icmp_seq=4022 rtt=141.3 ms

len=28 ip=10.125.20.64 ttl=64 id=49449 icmp_seq=4023 rtt=131.3 ms

len=28 ip=10.125.20.64 ttl=64 id=49450 icmp_seq=4024 rtt=121.2 ms

len=28 ip=10.125.20.64 ttl=64 id=49451 icmp_seq=4025 rtt=111.2 ms

len=28 ip=10.125.20.64 ttl=64 id=49452 icmp_seq=4026 rtt=101.1 ms

len=28 ip=10.125.20.64 ttl=64 id=50023 icmp_seq=4343 rtt=126.8 ms

len=28 ip=10.125.20.64 ttl=64 id=50024 icmp_seq=4344 rtt=116.8 ms

len=28 ip=10.125.20.64 ttl=64 id=50025 icmp_seq=4345 rtt=106.8 ms

len=28 ip=10.125.20.64 ttl=64 id=59727 icmp_seq=9836 rtt=106.1 ms

وتظهر النتائج أن المشكلة لم تختف. ربما هذا هو نفق IPIP؟ دعونا نبسط الاختبار أكثر:

تصحيح زمن انتقال الشبكة في Kubernetes

هل يتم إرسال جميع الحزم بين هذين المضيفين؟

theojulienne@kube-node-client ~ $ sudo hping3 172.16.47.27 --icmp -i u10000 | egrep --line-buffered 'rtt=[0-9]{3}.'

len=46 ip=172.16.47.27 ttl=61 id=41127 icmp_seq=12564 rtt=140.9 ms

len=46 ip=172.16.47.27 ttl=61 id=41128 icmp_seq=12565 rtt=130.9 ms

len=46 ip=172.16.47.27 ttl=61 id=41129 icmp_seq=12566 rtt=120.8 ms

len=46 ip=172.16.47.27 ttl=61 id=41130 icmp_seq=12567 rtt=110.8 ms

len=46 ip=172.16.47.27 ttl=61 id=41131 icmp_seq=12568 rtt=100.7 ms

len=46 ip=172.16.47.27 ttl=61 id=9062 icmp_seq=31443 rtt=134.2 ms

len=46 ip=172.16.47.27 ttl=61 id=9063 icmp_seq=31444 rtt=124.2 ms

len=46 ip=172.16.47.27 ttl=61 id=9064 icmp_seq=31445 rtt=114.2 ms

len=46 ip=172.16.47.27 ttl=61 id=9065 icmp_seq=31446 rtt=104.2 ms

لقد قمنا بتبسيط الموقف إلى عقدتين من عقد Kubernetes ترسلان لبعضهما البعض أي حزمة، حتى لو كان الأمر ping لـ ICMP. ما زالوا يرون زمن الوصول إذا كان المضيف المستهدف "سيئًا" (بعضه أسوأ من البعض الآخر).

الآن السؤال الأخير: لماذا يحدث التأخير فقط على خوادم kube-node؟ وهل يحدث ذلك عندما تكون عقدة kube هي المرسل أو المتلقي؟ لحسن الحظ، من السهل أيضًا معرفة ذلك عن طريق إرسال حزمة من مضيف خارج Kubernetes، ولكن مع نفس المستلم "السيئ المعروف". وكما ترون فإن المشكلة لم تختف:

theojulienne@shell ~ $ sudo hping3 172.16.47.27 -p 9876 -S -i u10000 | egrep --line-buffered 'rtt=[0-9]{3}.'

len=46 ip=172.16.47.27 ttl=61 DF id=0 sport=9876 flags=RA seq=312 win=0 rtt=108.5 ms

len=46 ip=172.16.47.27 ttl=61 DF id=0 sport=9876 flags=RA seq=5903 win=0 rtt=119.4 ms

len=46 ip=172.16.47.27 ttl=61 DF id=0 sport=9876 flags=RA seq=6227 win=0 rtt=139.9 ms

len=46 ip=172.16.47.27 ttl=61 DF id=0 sport=9876 flags=RA seq=7929 win=0 rtt=131.2 ms

سنقوم بعد ذلك بتشغيل نفس الطلبات من عقدة kube المصدر السابقة إلى المضيف الخارجي (الذي يستثني المضيف المصدر نظرًا لأن اختبار الاتصال يتضمن مكون RX وTX):

theojulienne@kube-node-client ~ $ sudo hping3 172.16.33.44 -p 9876 -S -i u10000 | egrep --line-buffered 'rtt=[0-9]{3}.'
^C
--- 172.16.33.44 hping statistic ---
22352 packets transmitted, 22350 packets received, 1% packet loss
round-trip min/avg/max = 0.2/7.6/1010.6 ms

ومن خلال فحص التقاط حزم زمن الوصول، حصلنا على بعض المعلومات الإضافية. على وجه التحديد، يرى المرسل (أسفل) هذه المهلة، لكن المستلم (أعلى) لا يرى ذلك - راجع عمود دلتا (بالثواني):

تصحيح زمن انتقال الشبكة في Kubernetes

بالإضافة إلى ذلك، إذا نظرت إلى الاختلاف في ترتيب حزم TCP وICMP (حسب الأرقام التسلسلية) على جانب المستلم، فإن حزم ICMP تصل دائمًا بنفس التسلسل الذي تم إرسالها به، ولكن بتوقيت مختلف. وفي الوقت نفسه، تتداخل حزم TCP أحيانًا، ويتعطل بعضها. على وجه الخصوص، إذا قمت بفحص منافذ حزم SYN، فستجد أنها مرتبة من جانب المرسل، ولكن ليس من جانب جهاز الاستقبال.

هناك فرق دقيق في كيفية ذلك بطاقات الشبكة تقوم الخوادم الحديثة (مثل تلك الموجودة في مركز البيانات لدينا) بمعالجة الحزم التي تحتوي على TCP أو ICMP. عند وصول حزمة، يقوم محول الشبكة "بتجزئتها لكل اتصال"، أي أنه يحاول تقسيم الاتصالات إلى قوائم انتظار وإرسال كل قائمة انتظار إلى مركز معالج منفصل. بالنسبة لـ TCP، تتضمن هذه التجزئة كلاً من عنوان IP المصدر والوجهة والمنفذ. بمعنى آخر، يتم تجزئة كل اتصال (من المحتمل) بشكل مختلف. بالنسبة لـ ICMP، تتم تجزئة عناوين IP فقط، نظرًا لعدم وجود منافذ.

ملاحظة جديدة أخرى: خلال هذه الفترة نرى تأخيرات ICMP في جميع الاتصالات بين مضيفين، لكن TCP لا يحدث ذلك. يخبرنا هذا أن السبب على الأرجح مرتبط بتجزئة قائمة انتظار RX: من المؤكد تقريبًا أن الازدحام يحدث في معالجة حزم RX، وليس في إرسال الاستجابات.

يؤدي هذا إلى إزالة إرسال الحزم من قائمة الأسباب المحتملة. نحن نعلم الآن أن مشكلة معالجة الحزم تكمن في جانب الاستلام في بعض خوادم عقدة kube.

فهم معالجة الحزم في نواة لينكس

لفهم سبب حدوث المشكلة عند جهاز الاستقبال على بعض خوادم عقدة kube، دعونا نلقي نظرة على كيفية معالجة نواة Linux للحزم.

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

تصحيح زمن انتقال الشبكة في Kubernetes

يعد تبديل السياق هذا بطيئًا: ربما لم يكن زمن الوصول ملحوظًا على بطاقات الشبكة بسرعة 10 ميجابت في الثانية في التسعينيات، ولكن على بطاقات 90G الحديثة ذات الإنتاجية القصوى البالغة 10 مليون حزمة في الثانية، يمكن مقاطعة كل نواة لخادم صغير ثماني النواة بملايين مرات في الثانية الواحدة.

من أجل عدم التعامل باستمرار مع المقاطعات، تمت إضافة Linux منذ سنوات عديدة نابي: Network API التي تستخدمها جميع برامج التشغيل الحديثة لتحسين الأداء بسرعات عالية. عند السرعات المنخفضة، تستمر النواة في تلقي المقاطعات من بطاقة الشبكة بالطريقة القديمة. بمجرد وصول ما يكفي من الحزم التي تتجاوز العتبة، تقوم النواة بتعطيل المقاطعات وتبدأ بدلاً من ذلك في استقصاء محول الشبكة والتقاط الحزم في أجزاء. تتم المعالجة في softirq، أي في سياق المقاطعات البرمجية بعد استدعاءات النظام ومقاطعات الأجهزة، عندما تكون النواة (على عكس مساحة المستخدم) قيد التشغيل بالفعل.

تصحيح زمن انتقال الشبكة في Kubernetes

وهذا أسرع بكثير، ولكنه يسبب مشكلة مختلفة. إذا كان هناك عدد كبير جدًا من الحزم، فسيتم قضاء الوقت كله في معالجة الحزم من بطاقة الشبكة، ولن يكون لدى عمليات مساحة المستخدم الوقت الكافي لإفراغ قوائم الانتظار هذه فعليًا (القراءة من اتصالات TCP، وما إلى ذلك). في نهاية المطاف، تمتلئ قوائم الانتظار ونبدأ في إسقاط الحزم. في محاولة للعثور على توازن، تقوم النواة بتعيين ميزانية للحد الأقصى لعدد الحزم التي تتم معالجتها في سياق softirq. بمجرد تجاوز هذه الميزانية، يتم تنشيط موضوع منفصل ksoftirqd (سترى واحدًا منهم ps لكل نواة) الذي يتعامل مع هذه softirqs خارج مسار syscall/المقاطعة العادي. تمت جدولة هذا الموضوع باستخدام برنامج جدولة العمليات القياسي، الذي يحاول تخصيص الموارد بشكل عادل.

تصحيح زمن انتقال الشبكة في Kubernetes

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

تضييق المعالجة وصولاً إلى جوهر أو طريقة

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

دعنا نعود إلى حزمنا البطيئة:

len=46 ip=172.16.53.32 ttl=61 id=29573 icmp_seq=1953 rtt=99.3 ms

len=46 ip=172.16.53.32 ttl=61 id=29574 icmp_seq=1954 rtt=89.3 ms

len=46 ip=172.16.53.32 ttl=61 id=29575 icmp_seq=1955 rtt=79.2 ms

len=46 ip=172.16.53.32 ttl=61 id=29576 icmp_seq=1956 rtt=69.1 ms

len=46 ip=172.16.53.32 ttl=61 id=29577 icmp_seq=1957 rtt=59.1 ms

len=46 ip=172.16.53.32 ttl=61 id=29790 icmp_seq=2070 rtt=75.7 ms

len=46 ip=172.16.53.32 ttl=61 id=29791 icmp_seq=2071 rtt=65.6 ms

len=46 ip=172.16.53.32 ttl=61 id=29792 icmp_seq=2072 rtt=55.5 ms

كما تمت مناقشته سابقًا، يتم تجزئة حزم ICMP هذه إلى قائمة انتظار RX NIC واحدة وتتم معالجتها بواسطة نواة وحدة المعالجة المركزية (CPU) واحدة. إذا أردنا أن نفهم كيفية عمل Linux، فمن المفيد أن نعرف أين (على أي نواة وحدة المعالجة المركزية) وكيف تتم معالجة هذه الحزم (softirq، ksoftirqd) من أجل تتبع العملية.

حان الوقت الآن لاستخدام الأدوات التي تتيح لك مراقبة نواة Linux في الوقت الفعلي. هنا استخدمنا مخفية. تسمح لك مجموعة الأدوات هذه بكتابة برامج C صغيرة تربط وظائف عشوائية في النواة وتخزن الأحداث مؤقتًا في برنامج Python لمساحة المستخدم الذي يمكنه معالجتها وإرجاع النتيجة إليك. يعد ربط الوظائف العشوائية في النواة أمرًا معقدًا، ولكن الأداة المساعدة مصممة لتحقيق أقصى قدر من الأمان وهي مصممة لتتبع نوع مشكلات الإنتاج التي لا يمكن إعادة إنتاجها بسهولة في بيئة الاختبار أو التطوير.

الخطة هنا بسيطة: نحن نعلم أن النواة تعالج إشارات ICMP هذه، لذا سنضع خطافًا على وظيفة النواة icmp_echo، الذي يقبل حزمة طلب صدى ICMP الواردة ويبدأ في إرسال استجابة صدى ICMP. يمكننا التعرف على الحزمة عن طريق زيادة رقم icmp_seq الذي يظهر hping3 أعلاه.

قانون البرنامج النصي مخفية الوجهة تبدو معقدة، ولكنها ليست مخيفة كما يبدو. وظيفة icmp_echo ينقل struct sk_buff *skb: هذه حزمة تحتوي على "طلب صدى". يمكننا تتبعه، وسحب التسلسل echo.sequence (الذي يقارن مع icmp_seq بواسطة hping3 выше)، وإرساله إلى مساحة المستخدم. من السهل أيضًا التقاط اسم/معرف العملية الحالية. فيما يلي النتائج التي نراها مباشرة أثناء معالجة النواة للحزم:

TGID PID اسم العملية ICMP_SEQ 0 0 مبادلة/11 770 0 0 مبادلة/11 771 0 0 مبادلة/11 772 0 0 مبادلة/11 773 0 0 مبادلة/11 774 20041 20086 بروميثيوس 775 0 0 مبادلة/11 776 0 0 مبادلة/11 777 0 0 مبادلة/11 778 4512 4542 تقرير المتحدث 779

وتجدر الإشارة هنا إلى أنه في السياق softirq ستظهر العمليات التي قامت باستدعاءات النظام على أنها "عمليات" بينما في الواقع فإن النواة هي التي تعالج الحزم بأمان في سياق النواة.

باستخدام هذه الأداة، يمكننا ربط عمليات محددة بحزم محددة تظهر تأخيرًا hping3. دعونا نجعل الأمر بسيطا grep على هذا الالتقاط لقيم معينة icmp_seq. تمت الإشارة إلى الحزم المطابقة لقيم icmp_seq أعلاه مع RTT الخاصة بها التي لاحظناها أعلاه (بين قوسين قيم RTT المتوقعة للحزم التي قمنا بتصفيتها بسبب قيم RTT أقل من 50 مللي ثانية):

اسم عملية TGID PID ICMP_SEQ ** RTT - 10137 10436 cadvisor 1951 10137 10436 cadvisor 1952 76 76 ksoftirqd/11 1953 ** 99ms 76 76 ksoftirqd/11 1954 ** 89ms 76 76 ksoftirqd /11 1955 ** 79ms 76 76 ksoftirqd/11 1956 ** 69 مللي ثانية 76 76 ksoftirqd/11 1957 ** 59 مللي ثانية 76 76 ksoftirqd/11 1958 ** (49 مللي ثانية) 76 76 ksoftirqd/11 1959 ** (39 مللي ثانية) 76 76 ksoftirqd/11 1960 ** (29 مللي ثانية) 76 76 ksoftirq d / 11 1961 ** (19 مللي ثانية) 76 76 ksoftirqd/11 1962 ** (9 مللي ثانية) - 10137 10436 cadvisor 2068 10137 10436 cadvisor 2069 76 76 ksoftirqd/11 2070 ** 75 مللي ثانية 76 76 ksoftirqd/11 2071 65 76 ** 76 م.ث 11 2072 ksoftirqd/ 55 76 ** 76 مللي ثانية 11 2073 ksoftirqd/45 76 ** (76 مللي ثانية) 11 2074 ksoftirqd/35 76 ** (76 مللي ثانية) 11 2075 ksoftirqd/25 76 ** (76 مللي ثانية) 11 2076 ksoftirqd/15 76 ** (76 مللي ثانية) ) 11 2077 ksoftirqd/5 XNUMX ** (XNUMX مللي ثانية)

النتائج تخبرنا بعدة أشياء. أولاً، تتم معالجة كل هذه الحزم حسب السياق ksoftirqd/11. وهذا يعني أنه بالنسبة لهذا الزوج من الأجهزة، تم تجزئة حزم ICMP إلى النواة 11 عند الطرف المتلقي. نرى أيضًا أنه عندما يكون هناك ازدحام، هناك حزم تتم معالجتها في سياق استدعاء النظام cadvisor. ثم ksoftirqd يتولى المهمة ويعالج قائمة الانتظار المتراكمة: بالضبط عدد الحزم التي تراكمت بعد ذلك cadvisor.

والحقيقة أنه قبل أن يعمل دائما cadvisor، يدل على تورطه في المشكلة. ومن المفارقات أن الغرض سيوف - "تحليل استخدام الموارد وخصائص الأداء للحاويات قيد التشغيل" بدلاً من التسبب في مشكلة الأداء هذه.

كما هو الحال مع الجوانب الأخرى للحاويات، تعد هذه كلها أدوات متقدمة للغاية ومن المتوقع أن تواجه مشكلات في الأداء في ظل بعض الظروف غير المتوقعة.

ما الذي يفعله cadvisor لإبطاء قائمة انتظار الحزم؟

لدينا الآن فهم جيد لكيفية حدوث العطل، والعملية التي تسببه، وعلى أي وحدة معالجة مركزية. نرى أنه بسبب الحظر الصارم، ليس لدى Linux kernel الوقت الكافي للجدولة ksoftirqd. ونحن نرى أن الحزم تتم معالجتها في السياق cadvisor. ومن المنطقي أن نفترض ذلك cadvisor يطلق استدعاء نظام بطيئًا، وبعد ذلك تتم معالجة جميع الحزم المتراكمة في ذلك الوقت:

تصحيح زمن انتقال الشبكة في Kubernetes

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

تصحيح زمن انتقال الشبكة في Kubernetes

ومن الملائم أن يتم كل هذا باستخدام الأدوات الموجودة. على سبيل المثال، سجل الأداء يتحقق من نواة وحدة المعالجة المركزية المحددة بتردد محدد ويمكنه إنشاء جدول للمكالمات إلى النظام قيد التشغيل، بما في ذلك مساحة المستخدم ونواة Linux. يمكنك أخذ هذا السجل ومعالجته باستخدام شوكة صغيرة من البرنامج FlameGraph من Brendan Gregg، الذي يحافظ على ترتيب تتبع المكدس. يمكننا حفظ تتبعات مكدس سطر واحد كل 1 مللي ثانية، ثم تحديد العينة وحفظها قبل 100 مللي ثانية من وصول التتبع ksoftirqd:

# record 999 times a second, or every 1ms with some offset so not to align exactly with timers
sudo perf record -C 11 -g -F 999
# take that recording and make a simpler stack trace.
sudo perf script 2>/dev/null | ./FlameGraph/stackcollapse-perf-ordered.pl | grep ksoftir -B 100

ها هي النتائج:

(сотни следов, которые выглядят похожими)

cadvisor;[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];entry_SYSCALL_64_after_swapgs;do_syscall_64;sys_read;vfs_read;seq_read;memcg_stat_show;mem_cgroup_nr_lru_pages;mem_cgroup_node_nr_lru_pages cadvisor;[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];entry_SYSCALL_64_after_swapgs;do_syscall_64;sys_read;vfs_read;seq_read;memcg_stat_show;mem_cgroup_nr_lru_pages;mem_cgroup_node_nr_lru_pages cadvisor;[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];entry_SYSCALL_64_after_swapgs;do_syscall_64;sys_read;vfs_read;seq_read;memcg_stat_show;mem_cgroup_iter cadvisor;[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];entry_SYSCALL_64_after_swapgs;do_syscall_64;sys_read;vfs_read;seq_read;memcg_stat_show;mem_cgroup_nr_lru_pages;mem_cgroup_node_nr_lru_pages cadvisor;[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];entry_SYSCALL_64_after_swapgs;do_syscall_64;sys_read;vfs_read;seq_read;memcg_stat_show;mem_cgroup_nr_lru_pages;mem_cgroup_node_nr_lru_pages ksoftirqd/11;ret_from_fork;kthread;kthread;smpboot_thread_fn;smpboot_thread_fn;run_ksoftirqd;__do_softirq;net_rx_action;ixgbe_poll;ixgbe_clean_rx_irq;napi_gro_receive;netif_receive_skb_internal;inet_gro_receive;bond_handle_frame;__netif_receive_skb_core;ip_rcv_finish;ip_rcv;ip_forward_finish;ip_forward;ip_finish_output;nf_iterate;ip_output;ip_finish_output2;__dev_queue_xmit;dev_hard_start_xmit;ipip_tunnel_xmit;ip_tunnel_xmit;iptunnel_xmit;ip_local_out;dst_output;__ip_local_out;nf_hook_slow;nf_iterate;nf_conntrack_in;generic_packet;ipt_do_table;set_match_v4;ip_set_test;hash_net4_kadt;ixgbe_xmit_frame_ring;swiotlb_dma_mapping_error;hash_net4_test ksoftirqd/11;ret_from_fork;kthread;kthread;smpboot_thread_fn;smpboot_thread_fn;run_ksoftirqd;__do_softirq;net_rx_action;gro_cell_poll;napi_gro_receive;netif_receive_skb_internal;inet_gro_receive;__netif_receive_skb_core;ip_rcv_finish;ip_rcv;ip_forward_finish;ip_forward;ip_finish_output;nf_iterate;ip_output;ip_finish_output2;__dev_queue_xmit;dev_hard_start_xmit;dev_queue_xmit_nit;packet_rcv;tpacket_rcv;sch_direct_xmit;validate_xmit_skb_list;validate_xmit_skb;netif_skb_features;ixgbe_xmit_frame_ring;swiotlb_dma_mapping_error;__dev_queue_xmit;dev_hard_start_xmit;__bpf_prog_run;__bpf_prog_run

هناك الكثير من الأشياء هنا، ولكن الشيء الرئيسي هو أن نجد نمط “cadvisor قبل ksoftirqd” الذي رأيناه سابقًا في متتبع ICMP. ماذا يعني ذلك؟

كل سطر هو تتبع وحدة المعالجة المركزية في وقت معين. يتم فصل كل استدعاء أسفل المكدس على السطر بفاصلة منقوطة. في منتصف السطور نرى استدعاء النظام يتم استدعاؤه: read(): .... ;do_syscall_64;sys_read; .... لذلك يقضي cadvisor الكثير من الوقت في استدعاء النظام read()المتعلقة بالوظائف mem_cgroup_* (أعلى مكدس الاستدعاءات/نهاية السطر).

من غير المناسب أن ترى في تتبع المكالمة ما تتم قراءته بالضبط، لذلك دعونا نجري strace ودعنا نرى ما يفعله برنامج cadvisor ونجد مكالمات النظام التي تزيد مدتها عن 100 مللي ثانية:

theojulienne@kube-node-bad ~ $ sudo strace -p 10137 -T -ff 2>&1 | egrep '<0.[1-9]'
[pid 10436] <... futex resumed> ) = 0 <0.156784>
[pid 10432] <... futex resumed> ) = 0 <0.258285>
[pid 10137] <... futex resumed> ) = 0 <0.678382>
[pid 10384] <... futex resumed> ) = 0 <0.762328>
[pid 10436] <... read resumed> "cache 154234880nrss 507904nrss_h"..., 4096) = 658 <0.179438>
[pid 10384] <... futex resumed> ) = 0 <0.104614>
[pid 10436] <... futex resumed> ) = 0 <0.175936>
[pid 10436] <... read resumed> "cache 0nrss 0nrss_huge 0nmapped_"..., 4096) = 577 <0.228091>
[pid 10427] <... read resumed> "cache 0nrss 0nrss_huge 0nmapped_"..., 4096) = 577 <0.207334>
[pid 10411] <... epoll_ctl resumed> ) = 0 <0.118113>
[pid 10382] <... pselect6 resumed> ) = 0 (Timeout) <0.117717>
[pid 10436] <... read resumed> "cache 154234880nrss 507904nrss_h"..., 4096) = 660 <0.159891>
[pid 10417] <... futex resumed> ) = 0 <0.917495>
[pid 10436] <... futex resumed> ) = 0 <0.208172>
[pid 10417] <... futex resumed> ) = 0 <0.190763>
[pid 10417] <... read resumed> "cache 0nrss 0nrss_huge 0nmapped_"..., 4096) = 576 <0.154442>

كما قد تتوقع، نرى مكالمات بطيئة هنا read(). من محتويات عمليات القراءة والسياق mem_cgroup فمن الواضح أن هذه التحديات read() الرجوع إلى الملف memory.stat، والذي يوضح استخدام الذاكرة وحدود مجموعة cgroup (تقنية عزل موارد Docker). تقوم أداة cadvisor بالاستعلام عن هذا الملف للحصول على معلومات استخدام الموارد للحاويات. دعونا نتحقق مما إذا كان النواة أو cadvisor يقومان بشيء غير متوقع:

theojulienne@kube-node-bad ~ $ time cat /sys/fs/cgroup/memory/memory.stat >/dev/null

real 0m0.153s
user 0m0.000s
sys 0m0.152s
theojulienne@kube-node-bad ~ $

يمكننا الآن إعادة إنتاج الخطأ وفهم أن نواة Linux تواجه مرضًا.

لماذا تكون عملية القراءة بطيئة جدًا؟

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

تكمن المشكلة في أن مجموعات التحكم تأخذ في الاعتبار استخدام الذاكرة داخل مساحة الاسم (الحاوية). عند خروج جميع العمليات في مجموعة التحكم هذه، يقوم Docker بتحرير مجموعة الذاكرة. ومع ذلك، فإن "الذاكرة" ليست مجرد ذاكرة عملية. على الرغم من أن ذاكرة العملية نفسها لم تعد مستخدمة، يبدو أن النواة لا تزال تقوم بتعيين محتويات مخزنة مؤقتًا، مثل dentries وinodes (بيانات تعريف الدليل والملف)، والتي يتم تخزينها مؤقتًا في مجموعة الذاكرة cgroup. من وصف المشكلة:

مجموعات zombie cgroups: مجموعات c التي لا تحتوي على عمليات وتم حذفها، ولكن لا يزال لديها ذاكرة مخصصة (في حالتي، من ذاكرة التخزين المؤقت dentry، ولكن يمكن أيضًا تخصيصها من ذاكرة التخزين المؤقت للصفحة أو tmpfs).

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

ومن وجهة نظر الأداء، فقد ضحوا بالذاكرة من أجل الأداء: حيث قاموا بتسريع عملية التنظيف الأولية من خلال ترك بعض الذاكرة المؤقتة خلفهم. هذا جيد. عندما تستخدم النواة آخر ذاكرة تخزين مؤقت، يتم مسح مجموعة التحكم في النهاية، لذلك لا يمكن أن يطلق عليها "تسرب". لسوء الحظ، فإن التنفيذ المحدد لآلية البحث memory.stat في إصدار kernel هذا (4.9)، بالإضافة إلى الكم الهائل من الذاكرة على خوادمنا، يعني أن الأمر يستغرق وقتًا أطول بكثير لاستعادة أحدث البيانات المخزنة مؤقتًا ومسح زومبي مجموعة التحكم.

اتضح أن بعض العقد لدينا تحتوي على عدد كبير جدًا من مجموعات الزومبي cgroup بحيث تجاوزت القراءة وزمن الوصول ثانية واحدة.

يتمثل الحل البديل لمشكلة cadvisor في تحرير ذاكرة التخزين المؤقت لـ dentries/inodes في جميع أنحاء النظام على الفور، مما يؤدي على الفور إلى التخلص من زمن انتقال القراءة بالإضافة إلى زمن انتقال الشبكة على المضيف، نظرًا لأن مسح ذاكرة التخزين المؤقت يؤدي إلى تشغيل صفحات zombie cgroup المخزنة مؤقتًا ويحررها أيضًا. وهذا ليس حلاً، لكنه يؤكد سبب المشكلة.

اتضح أنه في إصدارات kernel الأحدث (4.19+) تم تحسين أداء الاتصال memory.stat، لذا فإن التبديل إلى هذه النواة أدى إلى حل المشكلة. وفي الوقت نفسه، كانت لدينا أدوات للكشف عن العقد التي بها مشكلات في مجموعات Kubernetes، واستنزافها بأمان وإعادة تشغيلها. لقد قمنا بتمشيط جميع المجموعات، ووجدنا العقد ذات زمن الوصول العالي بما فيه الكفاية وأعدنا تشغيلها. لقد منحنا هذا الوقت لتحديث نظام التشغيل على الخوادم المتبقية.

إجمال

نظرًا لأن هذا الخطأ أدى إلى إيقاف معالجة قائمة انتظار RX NIC لمئات المللي ثانية، فقد تسبب في وقت واحد في حدوث زمن انتقال عالٍ على الاتصالات القصيرة وزمن وصول متوسط ​​الاتصال، مثل بين طلبات MySQL وحزم الاستجابة.

يعد فهم أداء الأنظمة الأساسية والحفاظ عليه، مثل Kubernetes، أمرًا بالغ الأهمية لموثوقية وسرعة جميع الخدمات القائمة عليها. يستفيد كل نظام تقوم بتشغيله من تحسينات أداء Kubernetes.

المصدر: www.habr.com

إضافة تعليق