تحت الغطاء ، يحتوي hh.ru على عدد كبير من خدمات Java التي تعمل في حاويات الإرساء. خلال عملها ، واجهنا الكثير من المشاكل غير التافهة. في كثير من الحالات ، من أجل الوصول إلى أسفل الحل ، اضطررت إلى google لفترة طويلة ، وقراءة مصادر OpenJDK وحتى التعريف بالخدمات على الإنتاج. سأحاول في هذه المقالة أن أنقل جوهر المعرفة المكتسبة في هذه العملية.
حدود وحدة المعالجة المركزية
اعتدنا أن نعيش في أجهزة افتراضية kvm مع قيود وحدة المعالجة المركزية والذاكرة ، والانتقال إلى Docker ، وضع قيود مماثلة في cgroups. والمشكلة الأولى التي واجهناها هي بالتحديد حدود وحدة المعالجة المركزية. يجب أن أقول على الفور أن هذه المشكلة لم تعد ذات صلة بالإصدارات الأخيرة من Java 8 و Java ≥ 10. إذا كنت مواكبة العصر ، فيمكنك تخطي هذا القسم بأمان.
لذلك ، نبدأ خدمة صغيرة في الحاوية ونرى أنها تنتج عددًا كبيرًا من الخيوط. أو وحدة المعالجة المركزية تستهلك أكثر بكثير مما كان متوقعا ، مهلة كم عبثا. أو فيما يلي موقف حقيقي آخر: على جهاز ما ، تبدأ الخدمة بشكل طبيعي ، وعلى جهاز آخر ، وبنفس الإعدادات ، تعطل ، مسمر من قاتل OOM.
يتضح أن الحل بسيط للغاية - فقط Java لا ترى حدود -
--cpus
المحددة في جهاز
--cpus
وتعتقد أن جميع
--cpus
الجهاز المضيف يمكن الوصول إليها. ويمكن أن يكون هناك الكثير منهم (في الإعداد القياسي لدينا - 80).
تقوم المكتبات بضبط حجم تجمعات مؤشرات الترابط إلى عدد المعالجات المتوفرة - ومن ثم العدد الهائل من مؤشرات الترابط.
تقوم Java نفسها بقياس عدد مؤشرات ترابط GC بالطريقة نفسها ، وبالتالي استهلاك وحدة المعالجة المركزية والمهلة الزمنية - تبدأ الخدمة في إنفاق الكثير من الموارد على جمع القمامة ، وذلك باستخدام حصة الأسد من الحصة المخصصة لها.
أيضًا ، يمكن للمكتبات (Netty على وجه الخصوص) ، في بعض الحالات ، ضبط حجم ذاكرة إيقاف التشغيل على عدد وحدات المعالجة المركزية (CPU) ، مما يؤدي إلى احتمال كبير بتجاوز الحدود المحددة للحاوية عند التشغيل على أجهزة أكثر قوة.
في البداية ، كما أظهرت هذه المشكلة نفسها ، حاولنا استخدام جولات العمل التالية:
- حاول استخدام اثنين من الخدمات
libnumcpus - مكتبة تتيح لك "خداع" Java عن طريق تعيين عدد مختلف من المعالجات المتاحة ؛
- أشير صراحة إلى عدد مؤشرات الترابط GC ،
- صراحة وضع قيود على استخدام المخازن المؤقتة للبايت المباشر.
لكن ، بالطبع ، التنقل مع مثل هذه العكازات ليس مريحًا جدًا ، وكان الانتقال إلى Java 10 (ثم Java 11) ، حيث
تغيب كل هذه المشكلات ، حلاً حقيقيًا. في الإنصاف ، تجدر الإشارة إلى أنه في الثمانية ، أيضًا ، كان كل شيء على ما يرام مع
التحديث 191 ، الذي صدر في أكتوبر 2018. بحلول ذلك الوقت ، كان الأمر غير ذي صلة بالفعل بالنسبة لنا ، وهو ما أتمناه لك أيضًا.
هذا مثال على ذلك حيث لا يؤدي تحديث إصدار Java إلى الرضا الأخلاقي فحسب ، بل إلى ربح ملموس حقيقي في شكل عملية مبسطة وأداء خدمة أعلى.
عامل الميناء وخادم الجهاز
لذلك ، في Java 10 ، ظهرت خيارات
-XX:ActiveProcessorCount
و
-XX:+UseContainerSupport
(وتم نقلها إلى Java 8) ، مع مراعاة حدود مجموعات cgroups الافتراضية. الآن كل شيء كان رائعا. أم لا؟
بعد مرور بعض الوقت على انتقالنا إلى Java 10/11 ، بدأنا نلاحظ بعض الشذوذ. لسبب ما ، في بعض الخدمات ، بدت رسومات GC وكأنها لم تستخدم G1:
كان هذا ، بعبارة ملطفة ، غير متوقع إلى حد ما ، لأننا عرفنا بالتأكيد أن G1 هو المجمع الافتراضي ، بدءًا من Java 9. وفي الوقت نفسه ، لا توجد مشكلة من هذا القبيل في بعض الخدمات - يتم تشغيل G1 ، كما هو متوقع.
نبدأ في فهم وتعثر
شيء مثير للاهتمام . اتضح أنه إذا كان Java يعمل على أقل من 3 معالجات وبحد ذاكرة أقل من 2 جيجابايت ، فإنه يعتبر نفسه عميلًا ولا يسمح باستخدام أي شيء آخر غير SerialGC.
بالمناسبة ، هذا يؤثر فقط على
اختيار GC وليس له علاقة بخيارات تجميع -client / -server و JIT.
من الواضح ، عندما استخدمنا Java 8 ، فإنه لم يأخذ في الاعتبار حدود عامل الميناء واعتقد أنه يحتوي على الكثير من المعالجات والذاكرة. بعد الترقية إلى Java 10 ، بدأت العديد من الخدمات ذات الحدود الموضوعة أقل فجأة باستخدام SerialGC. لحسن الحظ ، يتم التعامل مع هذا بكل بساطة - عن طريق تحديد الخيار
-XX:+AlwaysActAsServerClassMachine
.
حدود وحدة المعالجة المركزية (نعم ، مرة أخرى) وتفتيت الذاكرة
بالنظر إلى الرسوم البيانية في المراقبة ، لاحظنا بطريقة ما أن حجم مجموعة المقيم للحاوية كبير جدًا - بقدر ثلاثة أضعاف حجم الحد الأقصى لحجم الفخذ. يمكن أن يكون هذا هو الحال في بعض آلية صعبة القادمة التي تحجيم وفقا لعدد المعالجات في النظام ولا يعرف عن حدود عامل الميناء؟
اتضح أن الآلية ليست صعبة على الإطلاق - بل هي malloc المعروفة من glibc. باختصار ، يستخدم glibc ما يسمى بالساحات لتخصيص الذاكرة. عند إنشاء ، يتم تعيين كل موضوع واحد من الساحات. عندما يريد مؤشر ترابط يستخدم glibc تخصيص مقدار معين من الذاكرة في الكومة الأصلية لاحتياجاته ويدعو malloc ، يتم تخصيص الذاكرة في الساحة المخصصة لها. إذا كانت الساحة تقدم عدة خيوط ، فسوف تتنافس هذه المواضيع عليها. لمزيد من الساحات ، تنافس أقل ، ولكن تجزئة أكثر ، لأن كل ساحة لديها قائمة المناطق الحرة الخاصة بها.
في أنظمة 64 بت ، يتم ضبط الرقم الافتراضي للساحات على 8 * عدد وحدات المعالجة المركزية (CPU). من الواضح أن هذا يمثل عبءًا كبيرًا بالنسبة لنا ، لأنه لا تتوفر جميع وحدات المعالجة المركزية في الحاوية. علاوة على ذلك ، بالنسبة للتطبيقات المستندة إلى Java ، فإن التنافس على الساحات ليس وثيق الصلة بالموضوع ، نظرًا لأن معظم التخصيصات تتم في Java-heap ، حيث يمكن تخصيص الذاكرة بالكامل عند بدء التشغيل.
تُعرف ميزة malloc هذه لفترة
طويلة جدًا ، وكذلك حلها - لاستخدام متغير البيئة
MALLOC_ARENA_MAX
للإشارة صراحة إلى عدد الساحات. من السهل جدًا القيام بأي حاوية. إليك تأثير تحديد
MALLOC_ARENA_MAX = 4
الرئيسية لدينا:
هناك حالتان على مخطط RSS: في واحدة (زرقاء) نقوم بتشغيل
MALLOC_ARENA_MAX
، والآخر (أحمر) نعيد تشغيله للتو. الفرق واضح.
ولكن بعد ذلك ، هناك رغبة معقولة لمعرفة ما الذي تنفقه Java بشكل عام على الذاكرة. هل من الممكن تشغيل خدمة microservice على Java مع حد ذاكرة 300-400 ميغا بايت ولا تخشى أن تسقط من Java-OOM أو لا يقتلها قاتل نظام OOM؟
نحن نعالج Java-OOM
بادئ ذي بدء ، تحتاج إلى التحضير لحقيقة أن OOMs أمر لا مفر منه ، وتحتاج إلى التعامل معها بشكل صحيح - على الأقل حفظ مقالب الورك. الغريب ، حتى هذا التعهد البسيط له فروقه الخاصة. على سبيل المثال ، لا يتم الكتابة فوق مقالب الورك - إذا تم بالفعل حفظ ملف تفريغ الورك الذي يحمل نفس الاسم ، فلن يتم إنشاء ملف تفريغ جديد.
يمكن لـ Java
إضافة الرقم التسلسلي وتفريغ العملية
تلقائيًا إلى اسم الملف ، لكن هذا لن يساعدنا. الرقم التسلسلي غير مفيد ، لأن هذا هو OOM ، وليس تفريغ الورك المطلوب بانتظام - يتم إعادة تشغيل التطبيق بعده ، وإعادة ضبط العداد. ومعرف العملية غير مناسب ، لأنه دائمًا ما يكون نفسه في عامل النقل (1).
لذلك ، توصلنا إلى هذا الخيار:
-XX:+HeapDumpOnOutOfMemoryError
-XX:+ExitOnOutOfMemoryError
-XX:HeapDumpPath=/var/crash/java.hprof
-XX:OnOutOfMemoryError="mv /var/crash/java.hprof /var/crash/heapdump.hprof"
إنه أمر بسيط للغاية ، ومع بعض التحسينات ، يمكنك تعليم تخزينه ليس فقط أحدث تفريغ للوركين ، ولكن لاحتياجاتنا هذا أكثر من كافٍ.
Java OOM ليس الشيء الوحيد الذي يتعين علينا مواجهته. كل حاوية لها حدود على الذاكرة التي تحتلها ، ويمكن تجاوزها. في حالة حدوث ذلك ، يتم قتل الحاوية بواسطة قاتل OOM وإعادة تشغيل النظام (نستخدم
restart_policy: always
). بطبيعة الحال ، هذا أمر غير مرغوب فيه ، ونريد أن نتعلم كيفية وضع قيود بشكل صحيح على الموارد المستخدمة من قبل JVM.
تحسين استهلاك الذاكرة
ولكن قبل وضع الحدود ، تحتاج إلى التأكد من أن JVM لا تهدر الموارد. لقد نجحنا بالفعل في تقليل استهلاك الذاكرة باستخدام حد أقصى لعدد وحدات المعالجة المركزية (CPU) والمتغير
MALLOC_ARENA_MAX
. هل هناك أي طرق أخرى "مجانية تقريبا" للقيام بذلك؟
اتضح أن هناك بعض الحيل التي ستوفر القليل من الذاكرة.
الأول هو استخدام الخيار
-Xss
(أو
-XX:ThreadStackSize
) ، والذي يتحكم في حجم مكدس مؤشرات الترابط. الافتراضي لـ JVM 64 بت هو 1 ميغابايت. اكتشفنا أن 512 كيلوبايت كافية لنا. لهذا السبب ، لم يتم اكتشاف StackOverflowException من قبل ، لكنني أعترف أن هذا غير مناسب للجميع. والربح من هذا هو صغير جدا.
والثاني هو علامة
-XX:+UseStringDeduplication
(مع تمكين G1 GC). إنها تتيح لك الحفظ على الذاكرة عن طريق طي الصفوف المكررة بسبب تحميل المعالج الإضافي. تعتمد المفاضلة بين الذاكرة ووحدة المعالجة المركزية فقط على التطبيق المحدد وإعدادات آلية إلغاء البيانات المكررة نفسها. قراءة
قفص الاتهام والاختبار في خدماتك ، لدينا هذا الخيار لم يتم العثور على التطبيق بعد.
وأخيرًا ، تتمثل الطريقة التي لا تناسب الجميع (ولكنها تناسبنا) في استخدام
jemalloc بدلاً من malloc الأصلي. هذا التطبيق موجه نحو تقليل تجزئة الذاكرة ودعم دعم تعدد مؤشرات ترابط مقارنة مع malloc من glibc. فيما يتعلق بخدماتنا ، أعطى jemalloc مكسبًا للذاكرة أكثر قليلاً من malloc مع
MALLOC_ARENA_MAX=4
، دون التأثير على الأداء بشكل كبير.
الخيارات الأخرى ، بما في ذلك تلك التي وصفها Alexei Shipilev في
JVM Anatomy Quark # 12: Native Memory Tracking ، بدت خطيرة إلى حد ما أو أدت إلى تدهور ملحوظ في الأداء. ومع ذلك ، لأغراض تعليمية ، أوصي بقراءة هذا المقال.
في غضون ذلك ، دعنا ننتقل إلى الموضوع التالي ، وأخيرا ، حاول أن تتعلم كيفية الحد من استهلاك الذاكرة وتحديد الحدود الصحيحة.
الحد من استهلاك الذاكرة: كومة الذاكرة المؤقتة ، غير كومة الذاكرة المؤقتة
لفعل كل شيء بشكل صحيح ، تحتاج إلى تذكر الذاكرة التي تتكون بشكل عام في Java. أولاً ، دعونا ننظر إلى المجمعات التي يمكن رصد حالتها من خلال JMX.
الأول ، بالطبع ، هو
الورك . الأمر بسيط:
-Xmx
، لكن كيف نفعل ذلك بشكل صحيح؟ لسوء الحظ ، لا توجد وصفة عالمية هنا ، كل هذا يتوقف على التطبيق وملف تعريف التحميل. بالنسبة للخدمات الجديدة ، نبدأ بحجم كومة معقول نسبياً (128 ميجابايت) ، وإذا لزم الأمر ، نقوم بزيادة أو تقليله. لدعم تلك القائمة ، هناك مراقبة مع الرسوم البيانية لاستهلاك الذاكرة ومقاييس GC.
في نفس الوقت مثل
-Xmx
قمنا بتعيين
-Xms == -Xmx
. ليس لدينا زيادة في الذاكرة ، لذلك من مصلحتنا أن تستخدم الخدمة الموارد التي قدمناها إلى الحد الأقصى. بالإضافة إلى ذلك ، في الخدمات العادية ، نقوم بتضمين
-XX:+AlwaysPreTouch
وآلية صفحات Huge الشفافة:
-XX:+UseTransparentHugePages -XX:+UseLargePagesInMetaspace
. ومع ذلك ، قبل تمكين THP ، اقرأ
الوثائق بعناية واختبر كيف تتصرف الخدمات مع هذا الخيار لفترة طويلة. لا يتم استبعاد المفاجآت على أجهزة ذات ذاكرة RAM غير كافية (على سبيل المثال ، كان علينا إيقاف تشغيل THP على مقاعد الاختبار).
التالي هو
غير كومة . تتضمن ذاكرة غير كومة الذاكرة المؤقتة:
- Metaspace وفضاء الطبقة المضغوطة ،
- كود ذاكرة التخزين المؤقت.
النظر في هذه التجمعات بالترتيب.
بالطبع ، لقد سمع الجميع عن
Metaspace ، لن أتحدث عن ذلك بالتفصيل. يقوم بتخزين بيانات تعريف الفئة ، ورمز الطريقة ، وما إلى ذلك. في الواقع ، يعتمد استخدام Metaspace بشكل مباشر على عدد وحجم الفئات المحملة ، ويمكنك تحديده ، مثل hip ، فقط عن طريق تشغيل التطبيق وإزالة المقاييس عبر JMX. بشكل افتراضي ، لا يقتصر Metaspace على أي شيء ، ولكن من السهل جدًا القيام بذلك باستخدام
-XX:MaxMetaspaceSize
.
تعتبر
مساحة الفصل المضغوطة جزءًا من Metaspace وتظهر عندما يكون الخيار
-XX:+UseCompressedClassPointers
ممكّنًا (يتم تمكينه افتراضيًا لأكوام
-XX:+UseCompressedClassPointers
التي تقل عن 32 جيجابايت ، أي عندما يمكن أن يوفر مكسبًا حقيقيًا للذاكرة). يمكن أن يكون حجم هذا التجمع محدودًا بواسطة الخيار
-XX:CompressedClassSpaceSize
، ولكن ليس هناك معنى كبير ، نظرًا لأن مساحة الفئة المضغوطة مضمَّنة في Metaspace وتقتصر المساحة الإجمالية للذاكرة المؤمَّنة لـ Metaspace و Space Compressed Class Space في نهاية المطاف على
-XX:MaxMetaspaceSize
واحد -
-XX:MaxMetaspaceSize
.
بالمناسبة ، إذا نظرت إلى قراءات JMX ، فسيتم دائمًا احتساب مقدار الذاكرة غير الكومة
كمجموع لـ Metaspace و Class Space Compress و Cache Code. في الواقع ، تحتاج فقط إلى تلخيص Metaspace و CodeCache.
لذلك ، في
ذاكرة التخزين المؤقت فقط ، ظل
كود Cache - مستودع الكود الذي تم تجميعه بواسطة برنامج التحويل البرمجي JIT. بشكل افتراضي ، يتم تعيين الحد الأقصى لحجمه على 240 ميجابايت ، وبالنسبة للخدمات الصغيرة ، يكون حجمه أكبر عدة مرات من اللازم. يمكن تعيين حجم ذاكرة التخزين المؤقت
-XX:ReservedCodeCacheSize
الخيار
-XX:ReservedCodeCacheSize
. لا يمكن تحديد الحجم الصحيح إلا عن طريق تشغيل التطبيق ومتابعته ضمن ملف تعريف تحميل نموذجي.
من المهم عدم ارتكاب خطأ هنا ، نظرًا لعدم كفاية Code Cache يحذف الكود البارد والقديم من ذاكرة التخزين المؤقت (-
-XX:+UseCodeCacheFlushing
بشكل افتراضي) ، وهذا بدوره قد يؤدي إلى ارتفاع استهلاك وحدة المعالجة المركزية وتدهور الأداء . سيكون من الرائع إذا أمكنك رمي OOM عندما تفيض كود ذاكرة التخزين المؤقت ، لذلك هناك علامة
-XX:+ExitOnFullCodeCache
، ولكن لسوء الحظ ، لا يتوفر هذا
الإصدار إلا في
إصدار تطوير JVM.
المجموعة الأخيرة التي توجد عنها معلومات في JMX هي
الذاكرة المباشرة . بشكل افتراضي ، حجمها غير محدود ، لذلك من المهم تعيين نوع من القيود عليه - على الأقل سيتم توجيه المكتبات مثل Netty ، التي تستخدم بنشاط المخازن المؤقتة للبايت المباشر. ليس من الصعب تعيين حد باستخدام علامة
-XX:MaxDirectMemorySize
، ومرة أخرى ، سيساعدنا الرصد فقط على تحديد القيمة الصحيحة.
إذن ما الذي وصلنا إليه حتى الآن؟
ذاكرة عملية جافا =
كومة + Metaspace + رمز ذاكرة التخزين المؤقت + ذاكرة مباشرة =
-Xmx +
-XX: MaxMetaspaceSize +
-XX: ReservedCodeCacheSize +
-XX: MaxDirectMemorySize
دعنا نحاول رسم كل شيء على الرسم البياني ومقارنته مع حاوية عامل ميناء RSS.
السطر أعلاه هو RSS للحاوية وهو أكثر من مرة ونصف من استهلاك ذاكرة JVM ، والذي يمكننا مراقبته من خلال JMX.
مزيد من الحفر!
الحد من استهلاك الذاكرة: تتبع الذاكرة الأصلية
بالطبع ، بالإضافة إلى الكومة وغير الكومة والذاكرة المباشرة ، يستخدم JVM مجموعة كاملة من تجمعات الذاكرة الأخرى. سوف
-XX:NativeMemoryTracking=summary
العلامة
-XX:NativeMemoryTracking=summary
معهم
-XX:NativeMemoryTracking=summary
. بتمكين هذا الخيار ، سنكون قادرين على الحصول على معلومات حول التجمعات المعروفة لدى JVM ، ولكن لن تكون متاحة في JMX. يمكنك قراءة المزيد حول استخدام هذا الخيار في
الوثائق .
لنبدأ الأكثر وضوحا - الذاكرة التي تحتلها
أكوام الخيوط . NMT تنتج شيئا مثل ما يلي لخدمتنا:
موضوع (محجوز = 32166 كيلو بايت ، ملتزم = 5358 كيلو بايت)
(الموضوع رقم 52)
(مكدس: محجوز = 31920 كيلو بايت ، ملتزم = 5112 كيلو بايت)
(malloc = 185 كيلو بايت # 270)
(الساحة = 61 كيلو بايت # 102)
بالمناسبة ، يمكن أيضًا العثور على حجمها دون تتبع الذاكرة الأصلية ، باستخدام jstack وحفر قليلاً في
/proc/<pid>/smaps
. وضع أندريه بانجين
أداة خاصة لهذا الغرض.
حجم
مساحة الفصل المشتركة أسهل في التقييم:
مساحة صف مشتركة (محجوزة = 17084 كيلو بايت ، ملتزمة = 17084 كيلو بايت)
(ملف mmap: محجوز = 17084 كيلو بايت ، ملتزم = 17084 كيلو بايت)
هذه هي آلية Class Data Sharing ،
-Xshare
و
-XX:+UseAppCDS
. في Java 11 ، يتم تعيين الخيار
-Xshare
على تلقائي بشكل افتراضي ، مما يعني أنه إذا كان لديك
$JAVA_HOME/lib/server/classes.jsa
(في صورة عامل تحميل OpenJDK الرسمي) ، فسيتم تحميل خريطة الذاكرة- أوم عند بدء تشغيل JVM ، تسريع وقت بدء التشغيل. وفقًا لذلك ، من السهل تحديد حجم مساحة الفصل المشتركة إذا كنت تعرف حجم أرشيفات jsa.
فيما يلي بنيات
أداة تجميع مجمعي البيانات المهملة الأصلية:
GC (محفوظة = 42137 كيلو بايت ، ملتزمة = 41801 كيلو بايت)
(malloc = 5705KB # 9460)
(ملف mmap: محجوز = 36432 كيلو بايت ، ملتزم = 36096 كيلو بايت)
يقول Alexey Shipilev في الدليل المذكور بالفعل حول تتبع الذاكرة الأصلية أنهم يشغلون حوالي 4-5٪ من حجم الكومة ، ولكن في إعدادنا للكومب الصغير (يصل إلى عدة مئات من الميغابايت) ، بلغت النفقات العامة 50٪ من حجم الكومة.
يمكن شغل مساحة كبيرة بواسطة
جداول الرموز :
الرمز (محجوز = 16421 كيلو بايت ، ملتزم = 16421 كيلو بايت)
(malloc = 15261KB # 203089)
(الساحة = 1159 كيلو بايت # 1)
يقومون بتخزين أسماء الطرق والتوقيعات بالإضافة إلى روابط إلى سلاسل متدرجة. لسوء الحظ ، يبدو من الممكن تقدير حجم جدول الرموز فقط بعد الحادثة باستخدام تتبع الذاكرة الأصلية.
ما تبقى؟ وفقًا لتتبع الذاكرة الأصلي ، هناك الكثير من الأشياء:
مترجم (محجوز = 509 كيلو بايت ، ملتزم = 509 كيلو بايت)
داخلي (محجوز = 1647 كيلو بايت ، ملتزم = 1647 كيلو بايت)
أخرى (محفوظة = 2110 كيلو بايت ، ملتزمة = 2110 كيلو بايت)
قطعة الساحة (محفوظة = 1712 كيلو بايت ، ملتزمة = 1712 كيلو بايت)
التسجيل (محجوز = 6 كيلو بايت ، ملتزم = 6 كيلو بايت)
الوسيطات (محفوظة = 19 كيلو بايت ، ارتكبت = 19 كيلو بايت)
الوحدة النمطية (محفوظة = 227 كيلو بايت ، ارتكبت = 227 كيلو بايت)
غير معروف (محجوز = 32 كيلو بايت ، ملتزم = 32 كيلو بايت)
ولكن كل هذا يستغرق قدرا كبيرا من الفضاء.
لسوء الحظ ، لا يمكن تقييد أو التحكم في العديد من مناطق الذاكرة المذكورة ، وإذا كان يمكن أن يكون ، فإن التكوين سيتحول إلى جحيم. حتى مراقبة حالتها مهمة غير تافهة ، حيث أن تضمين "تعقب الذاكرة الأصلية" يستنزف قليلاً أداء التطبيق وتمكينه من الإنتاج في خدمة هامة ليست فكرة جيدة.
ومع ذلك ، من أجل الاهتمام ، دعونا نحاول أن نفكر في الرسم البياني بكل ما يشير إليه تتبع الذاكرة الأصلية:
ليس سيئا! الفارق الباقي هو حمل عام لتجزئة / تخصيص الذاكرة (إنه صغير جدًا ، نظرًا لأننا نستخدم jemalloc) أو الذاكرة التي خصصتها libs الأصلية. نحن فقط نستخدم واحدة من هذه التخزين الفعال لشجرة البادئة.
لذلك ، لتلبية احتياجاتنا ، يكفي تقييد ما يمكننا: كومة الذاكرة المؤقتة ، Metaspace ، كود ذاكرة التخزين المؤقت ، الذاكرة المباشرة. بالنسبة لكل شيء آخر ، نترك بعض الأسس المعقولة ، التي تحددها نتائج القياسات العملية.
بعد التعامل مع وحدة المعالجة المركزية والذاكرة ، ننتقل إلى المورد التالي الذي يمكن أن تتنافس التطبيقات عليه - على الأقراص.
جافا ومحركات الأقراص
ومعهم ، كل شيء سيء للغاية: فهي بطيئة ويمكن أن تؤدي إلى بلادة ملموسة للتطبيق. لذلك ، نقوم بفك ارتباط جافا عن الأقراص قدر الإمكان:
- نكتب جميع سجلات التطبيق إلى syslog المحلي عبر UDP. هذا يترك بعض الفرصة لفقدان السجلات اللازمة في مكان ما على طول الطريق ، ولكن كما أظهرت الممارسة ، فإن مثل هذه الحالات نادرة جدًا.
- سوف نكتب سجلات JVM في tmpfs ، لهذا نحن فقط بحاجة إلى تحميل عامل ميناء إلى الموقع المطلوب مع حجم
/dev/shm
.
إذا كتبنا سجلات في syslog أو في tmpfs ، والتطبيق نفسه لا يكتب أي شيء على القرص باستثناء مقالب الورك ، ثم اتضح أن القصة ذات الأقراص يمكن اعتبارها مغلقة في هذا؟
بالطبع لا.
نلاحظ الرسم البياني الخاص بمدة توقف التوقف مؤقتًا في العالم ، ونرى صورة حزينة - توقف التوقف مؤقتًا على المضيفين بمئات الميلي ثانية ، وعلى مضيف واحد يمكنهم الوصول إلى ثانية واحدة:
وغني عن القول أن هذا يؤثر سلبا على التطبيق؟ هنا ، على سبيل المثال ، رسم بياني يعكس وقت استجابة الخدمة وفقًا للعملاء:
هذه خدمة بسيطة للغاية ، حيث تقدم في معظمها ردودًا مخبأة ، لذلك من أين تأتي هذه المواعيد الباهظة ، بدءًا من النسبة المئوية 95؟ الخدمات الأخرى لها صورة مماثلة ، علاوة على ذلك ، تمطر المهلات بثبات محسوس عند نقل الاتصال من تجمع الاتصال بقاعدة البيانات ، وعند تنفيذ الطلبات ، وهلم جرا.
ما علاقة محرك الأقراص به؟ - أنت تسأل. اتضح الكثير لتفعله حيال ذلك.
أظهر تحليل مفصل للمشكلة أن فترات توقف STW الطويلة تنشأ بسبب حقيقة أن الخيوط تنتقل إلى نقطة الأمان لفترة طويلة. بعد قراءة كود JVM ، أدركنا أنه خلال تزامن الخيوط على نقطة الأمان ، يمكن لـ JVM كتابة الملف
/tmp/hsperfdata*
خلال خريطة الذاكرة ، التي تصدر إليها بعض الإحصاءات. تستخدم الأدوات المساعدة مثل
jstat
و
jps
jstat
jps
.
-XX:+PerfDisableSharedMem
على نفس الجهاز مع الخيار
-XX:+PerfDisableSharedMem
و ...
جيتي treadpool مقاييس استقرار:
(, ):
, , , .
?
Java- , , , .
Nuts and Bolts , . , . , , JMX.
, . .
statsd JVM, (heap, non-heap ):
, , .
— , , , , ? . () -, , RPS .
: , . . ammo-
. . . :
.
, . , , - , , .
في الختام
, Java Docker — , . .