كن حقيقيًا مع المواضيع الافتراضية

بقلم فاديم فيلانوفسكي ومايك هوانغ وداني توماس ومارتن تشالوبا
تتمتع Netflix بتاريخ حافل في استخدام Java باعتبارها لغة البرمجة الأساسية لدينا عبر أسطولنا الضخم من الخدمات الصغيرة. بينما نلتقط إصدارات أحدث من Java، يبحث فريق JVM Ecosystem عن ميزات لغوية جديدة يمكنها تحسين بيئة العمل وأداء أنظمتنا. في مقال حديث، قمنا بتفصيل كيف استفادت أعباء العمل لدينا من التحول إلى أجيال ZGC باعتبارها أداة تجميع البيانات المهملة الافتراضية لدينا عندما قمنا بالترحيل إلى Java 21. تعد سلاسل العمليات الافتراضية ميزة أخرى يسعدنا اعتمادها كجزء من هذا الترحيل.
بالنسبة لأولئك الجدد على الخيوط الافتراضية، يتم وصفها بأنها “خيوط خفيفة الوزن تقلل بشكل كبير من جهد الكتابة والصيانة ومراقبة التطبيقات المتزامنة عالية الإنتاجية”. وتأتي قوتها من قدرتها على التعليق والاستئناف تلقائيًا عبر الاستمرارات عندما تحدث عمليات الحظر، وبالتالي يتم تحرير مؤشرات ترابط نظام التشغيل الأساسي لإعادة استخدامها في عمليات أخرى. يمكن أن تؤدي الاستفادة من الخيوط الافتراضية إلى فتح أداء أعلى عند استخدامها في السياق المناسب.
نناقش في هذه المقالة إحدى الحالات الغريبة التي واجهناها خلال طريقنا لنشر سلاسل المحادثات الافتراضية على Java 21.
رفع مهندسو Netflix العديد من التقارير المستقلة عن المهلات المتقطعة وقاموا بتعليق المثيلات على فريقي هندسة الأداء والنظام البيئي JVM. وبعد الفحص الدقيق، لاحظنا مجموعة من السمات والأعراض الشائعة. في جميع الحالات، تم تشغيل التطبيقات المتأثرة على Java 21 مع SpringBoot 3 وTomcat المضمن الذي يخدم حركة المرور على نقاط نهاية REST. توقفت المثيلات التي واجهت المشكلة عن خدمة حركة المرور على الرغم من أن JVM في تلك المثيلات ظل قيد التشغيل. أحد الأعراض الواضحة التي تميز بداية هذه المشكلة هو الزيادة المستمرة في عدد المقابس الموجودة closeWait
الدولة كما هو موضح في الرسم البياني أدناه:
المقابس المتبقية في closeWait
تشير الحالة إلى أن النظير البعيد أغلق المقبس، ولكن لم يتم إغلاقه مطلقًا في المثيل المحلي، ربما بسبب فشل التطبيق في القيام بذلك. يمكن أن يشير هذا غالبًا إلى أن التطبيق معلق في حالة غير طبيعية، وفي هذه الحالة قد تكشف عمليات تفريغ مؤشر ترابط التطبيق عن معلومات إضافية.
من أجل استكشاف هذه المشكلة وإصلاحها، قمنا أولاً بالاستفادة من نظام التنبيهات الخاص بنا لاكتشاف مثيل في هذه الحالة. نظرًا لأننا نجمع ونستمر بشكل دوري في عمليات تفريغ سلاسل الرسائل لجميع أحمال عمل JVM، فيمكننا غالبًا تجميع السلوك معًا بأثر رجعي من خلال فحص عمليات تفريغ سلاسل المحادثات هذه من مثيل. ومع ذلك، فقد فوجئنا عندما اكتشفنا أن جميع عمليات تفريغ سلاسل المحادثات الخاصة بنا تُظهر JVM خاملاً تمامًا دون أي نشاط واضح. كشفت مراجعة التغييرات الأخيرة أن هذه الخدمات المتأثرة مكنت سلاسل المحادثات الافتراضية، وكنا نعلم أن مكدسات استدعاءات سلسلة المحادثات الافتراضية لا تظهر في jstack
مقالب الموضوع التي تم إنشاؤها. للحصول على تفريغ مؤشر ترابط أكثر اكتمالاً يحتوي على حالة سلاسل الرسائل الافتراضية، استخدمنا الملف “jcmd Thread.dump_to_file
“الأمر بدلاً من ذلك. كمحاولة أخيرة لاستبطان حالة JVM، قمنا أيضًا بجمع ملف تفريغ الكومة من المثيل.
كشفت عمليات تفريغ المواضيع عن آلاف سلاسل الرسائل الافتراضية “الفارغة”:
#119821 "" virtual#119820 "" virtual
#119823 "" virtual
#120847 "" virtual
#119822 "" virtual
...
هذه هي VTs (مؤشرات الترابط الافتراضية) التي تم إنشاء كائن مؤشر ترابط لها، ولكن لم يبدأ تشغيلها، وبالتالي، لا يوجد بها تتبع مكدس. في الواقع، كان عدد VTs الفارغة تقريبًا نفس عدد المقابس في حالة CloseWait. لفهم ما كنا نراه، نحتاج أولاً إلى فهم كيفية عمل VTs.
لم يتم تعيين مؤشر ترابط افتراضي بنسبة 1:1 إلى مؤشر ترابط مخصص على مستوى نظام التشغيل. بدلاً من ذلك، يمكننا أن نفكر في الأمر كمهمة تمت جدولتها لتجمع سلاسل الرسائل المتشعبة. عندما يدخل مؤشر ترابط افتراضي في مكالمة محظورة، مثل انتظار مكالمة Future
، فهو يتخلى عن مؤشر ترابط نظام التشغيل الذي يشغله ويظل ببساطة في الذاكرة حتى يصبح جاهزًا للاستئناف. في هذه الأثناء، يمكن إعادة تعيين مؤشر ترابط نظام التشغيل لتنفيذ VTs أخرى في نفس تجمع الوصلة المتفرعة. يتيح لنا ذلك مضاعفة عدد كبير من VTs لعدد قليل من مؤشرات ترابط نظام التشغيل الأساسية. في مصطلحات JVM، يُشار إلى مؤشر ترابط نظام التشغيل الأساسي باسم “مؤشر الترابط الناقل” الذي يمكن “تركيب” الخيط الافتراضي عليه أثناء التنفيذ و”إلغاء تثبيته” أثناء انتظاره. يتوفر وصف متعمق رائع للخيط الافتراضي في JEP 444.
في بيئتنا، نستخدم نموذج حظر لـ Tomcat، والذي يحتفظ في الواقع بمؤشر ترابط عامل طوال عمر الطلب. من خلال تمكين المواضيع الافتراضية، يتحول Tomcat إلى التنفيذ الافتراضي. يقوم كل طلب وارد بإنشاء سلسلة رسائل افتراضية جديدة يتم جدولتها ببساطة كمهمة على منفذ سلسلة المحادثات الافتراضية. يمكننا أن نرى Tomcat يقوم بإنشاء ملف VirtualThreadExecutor
هنا.
بربط هذه المعلومات بمشكلتنا، تتوافق الأعراض مع الحالة التي يستمر فيها Tomcat في إنشاء عامل ويب جديد VT لكل طلب وارد، ولكن لا توجد سلاسل عمليات نظام تشغيل متاحة لتثبيتها عليها.
ماذا حدث لسلاسل نظام التشغيل لدينا وما الذي يشغلهم؟ كما هو موضح هنا، سيتم تثبيت VT على مؤشر ترابط نظام التشغيل الأساسي إذا قام بإجراء عملية حظر أثناء وجوده داخل ملف synchronized
كتلة أو طريقة. هذا هو بالضبط ما يحدث هنا. فيما يلي مقتطف ذو صلة من ملف تفريغ سلسلة المحادثات الذي تم الحصول عليه من المثيل المتوقف:
#119515 "" virtual
java.base/jdk.internal.misc.Unsafe.park(Native Method)
java.base/java.lang.VirtualThread.parkOnCarrierThread(VirtualThread.java:661)
java.base/java.lang.VirtualThread.park(VirtualThread.java:593)
java.base/java.lang.System$2.parkVirtualThread(System.java:2643)
java.base/jdk.internal.misc.VirtualThreads.park(VirtualThreads.java:54)
java.base/java.util.concurrent.locks.LockSupport.park(LockSupport.java:219)
java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:754)
java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:990)
java.base/java.util.concurrent.locks.ReentrantLock$Sync.lock(ReentrantLock.java:153)
java.base/java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:322)
zipkin2.reporter.internal.CountBoundedQueue.offer(CountBoundedQueue.java:54)
zipkin2.reporter.internal.AsyncReporter$BoundedAsyncReporter.report(AsyncReporter.java:230)
zipkin2.reporter.brave.AsyncZipkinSpanHandler.end(AsyncZipkinSpanHandler.java:214)
brave.internal.handler.NoopAwareSpanHandler$CompositeSpanHandler.end(NoopAwareSpanHandler.java:98)
brave.internal.handler.NoopAwareSpanHandler.end(NoopAwareSpanHandler.java:48)
brave.internal.recorder.PendingSpans.finish(PendingSpans.java:116)
brave.RealSpan.finish(RealSpan.java:134)
brave.RealSpan.finish(RealSpan.java:129)
io.micrometer.tracing.brave.bridge.BraveSpan.end(BraveSpan.java:117)
io.micrometer.tracing.annotation.AbstractMethodInvocationProcessor.after(AbstractMethodInvocationProcessor.java:67)
io.micrometer.tracing.annotation.ImperativeMethodInvocationProcessor.proceedUnderSynchronousSpan(ImperativeMethodInvocationProcessor.java:98)
io.micrometer.tracing.annotation.ImperativeMethodInvocationProcessor.process(ImperativeMethodInvocationProcessor.java:73)
io.micrometer.tracing.annotation.SpanAspect.newSpanMethod(SpanAspect.java:59)
java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
java.base/java.lang.reflect.Method.invoke(Method.java:580)
org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:637)
...
في تتبع المكدس هذا، ندخل المزامنة brave.RealSpan.finish(RealSpan.java:134)
. تم تثبيت هذا الخيط الافتراضي بشكل فعال – حيث تم تثبيته على خيط نظام التشغيل الفعلي حتى أثناء انتظاره للحصول على قفل إعادة الدخول. هناك 3 VTs في هذه الحالة بالضبط وتم تحديد VT آخر باسم “<redacted> @DefaultExecutor - 46542
“والذي يتبع أيضًا نفس مسار الكود. يتم تثبيت هذه المواضيع الافتراضية الأربعة أثناء انتظار الحصول على القفل. نظرًا لأنه تم نشر التطبيق على مثيل يحتوي على 4 وحدات معالجة مركزية افتراضية، فإن تجمع الروابط المتشعبة الذي يدعم تنفيذ VT يحتوي أيضًا على 4 سلاسل عمليات لنظام التشغيل. والآن بعد أن استنفذنا كل هذه العناصر، لا يمكن لأي مؤشر ترابط افتراضي آخر أن يحقق أي تقدم. وهذا ما يفسر سبب توقف Tomcat عن معالجة الطلبات وسبب عدد المقابس الموجودة closeWait
الدولة تواصل الصعود. في الواقع، يقبل Tomcat الاتصال على مأخذ توصيل، وينشئ طلبًا مع سلسلة رسائل افتراضية، ويمرر هذا الطلب/سلسلة الرسائل إلى المنفذ للمعالجة. ومع ذلك، لا يمكن جدولة VT الذي تم إنشاؤه حديثًا نظرًا لأن كافة سلاسل عمليات نظام التشغيل الموجودة في تجمع fork-join مثبتة ولا يتم تحريرها مطلقًا. لذا فإن VTs التي تم إنشاؤها حديثًا عالقة في قائمة الانتظار، بينما لا تزال تمسك بالمقبس.
والآن بعد أن علمنا أن VTs تنتظر الحصول على القفل، فإن السؤال التالي هو: من يحمل القفل؟ تعد الإجابة على هذا السؤال أمرًا أساسيًا لفهم سبب حدوث هذه الحالة في المقام الأول. عادةً ما يشير تفريغ الخيط إلى من يحمل القفل إما بـ “- locked <0xâ¦> (at â¦)
” أو “.”Locked ownable synchronizers
“، لكن لم يظهر أي منهما في مقالب سلاسل المحادثات لدينا. في واقع الأمر، لا يتم تضمين معلومات القفل/وقوف السيارات/الانتظار في jcmd
مقالب الموضوع التي تم إنشاؤها. يعد هذا أحد القيود في Java 21 وسيتم تناوله في الإصدارات المستقبلية. يكشف التمشيط الدقيق لملف تفريغ الخيوط أن هناك إجمالي 6 خيوط تتنافس على نفس الشيء ReentrantLock
وما يرتبط بها Condition
. وقد تم تفصيل أربعة من هذه المواضيع الستة في القسم السابق. وهنا موضوع آخر:
#119516 "" virtual
java.base/java.lang.VirtualThread.park(VirtualThread.java:582)
java.base/java.lang.System$2.parkVirtualThread(System.java:2643)
java.base/jdk.internal.misc.VirtualThreads.park(VirtualThreads.java:54)
java.base/java.util.concurrent.locks.LockSupport.park(LockSupport.java:219)
java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:754)
java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:990)
java.base/java.util.concurrent.locks.ReentrantLock$Sync.lock(ReentrantLock.java:153)
java.base/java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:322)
zipkin2.reporter.internal.CountBoundedQueue.offer(CountBoundedQueue.java:54)
zipkin2.reporter.internal.AsyncReporter$BoundedAsyncReporter.report(AsyncReporter.java:230)
zipkin2.reporter.brave.AsyncZipkinSpanHandler.end(AsyncZipkinSpanHandler.java:214)
brave.internal.handler.NoopAwareSpanHandler$CompositeSpanHandler.end(NoopAwareSpanHandler.java:98)
brave.internal.handler.NoopAwareSpanHandler.end(NoopAwareSpanHandler.java:48)
brave.internal.recorder.PendingSpans.finish(PendingSpans.java:116)
brave.RealScopedSpan.finish(RealScopedSpan.java:64)
...
لاحظ أنه بينما يبدو أن هذا الموضوع يمر عبر نفس مسار التعليمات البرمجية لإنهاء فترة، فإنه لا يمر عبر synchronized
حاجز. وأخيراً هذا هو الموضوع السادس:
#107 "AsyncReporter <redacted>"
java.base/jdk.internal.misc.Unsafe.park(Native Method)
java.base/java.util.concurrent.locks.LockSupport.park(LockSupport.java:221)
java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:754)
java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:1761)
zipkin2.reporter.internal.CountBoundedQueue.drainTo(CountBoundedQueue.java:81)
zipkin2.reporter.internal.AsyncReporter$BoundedAsyncReporter.flush(AsyncReporter.java:241)
zipkin2.reporter.internal.AsyncReporter$Flusher.run(AsyncReporter.java:352)
java.base/java.lang.Thread.run(Thread.java:1583)
هذا في الواقع مؤشر ترابط عادي للنظام الأساسي، وليس مؤشر ترابط افتراضي. مع إيلاء اهتمام خاص لأرقام الأسطر في تتبع المكدس هذا، من الغريب أن يبدو أن الخيط محظور داخل الجزء الداخلي acquire()
طريقة بعد استكمال الانتظار. بمعنى آخر، كان مؤشر ترابط الاتصال هذا يمتلك القفل عند الدخول awaitNanos()
. نحن نعلم أن القفل قد تم الحصول عليه صراحةً هنا. ومع ذلك، بحلول الوقت الذي اكتمل فيه الانتظار، لم يتمكن من استعادة القفل. تلخيص تحليل تفريغ الخيط الخاص بنا:
هناك 5 سلاسل افتراضية وخيط واحد عادي في انتظار القفل. من بين 5 VTs، تم تثبيت 4 منها في سلاسل عمليات نظام التشغيل في تجمع fork-join. لا توجد حتى الآن معلومات حول من يملك القفل. نظرًا لعدم وجود أي شيء آخر يمكننا استخلاصه من تفريغ مؤشر الترابط، فإن خطوتنا المنطقية التالية هي إلقاء نظرة خاطفة على تفريغ الكومة واستبطان حالة القفل.
كان العثور على القفل في تفريغ الكومة أمرًا بسيطًا نسبيًا. باستخدام أداة Eclipse MAT الممتازة، قمنا بفحص الكائنات الموجودة على حزمة الملف AsyncReporter
خيط غير افتراضي لتحديد كائن القفل. ربما كان التفكير في الحالة الحالية للقفل هو الجزء الأكثر صعوبة في تحقيقنا. يمكن العثور على معظم التعليمات البرمجية ذات الصلة في AbstractQueuedSynchronizer.java. على الرغم من أننا لا ندعي أننا نفهم بشكل كامل الأعمال الداخلية له، فقد قمنا بإجراء هندسة عكسية كافية منه لتتناسب مع ما نراه في تفريغ الكومة. يوضح هذا الرسم البياني النتائج التي توصلنا إليها:
اكتشاف المزيد من هيدب فيديو
اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.