الألغام تحت الأداء تنتظر في الأجنحة: الجزء 2

في إعداد المقال الأخير ، قام أحد الأمثلة الغريبة التي تم اكتشافها في أحد تطبيقاتنا بفت انتباهي. لقد صممته كمقال منفصل ، الذي تقرأه حاليًا.


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


في دراسة هذه الانحرافات ، تسلسل الإجراءات واضح:


  • نطلق التطبيق في بيئة معزولة بإعدادات تشبه prod ، دون أن ننسى العلم -XX:+HeapDumpOnOutOfMemoryError ، بحيث يقوم VM بإنشاء كومة من الكومة عندما تكون ممتلئة
  • أداء الإجراءات التي تؤدي إلى سقوط
  • خذ المدلى بها والبدء في فحصها

قدم النهج الأول المواد اللازمة للدراسة. الصورة التالية فتحت

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



في التقريب الأول ، تظهر قطعتان متساويتان بحجم 71 ميغابايت بوضوح ، والأكبر أكبر 6 مرات.


ساعد التدخين القصير لسلسلة المكالمات والرموز المصدرية على تحديد نقاط "".


أول 10 خطوط تكفي
 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.base/java.util.Arrays.copyOf(Arrays.java:3745) at java.base/java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:172) at java.base/java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:538) at java.base/java.lang.StringBuilder.append(StringBuilder.java:174) at com.p6spy.engine.common.Value.quoteIfNeeded(Value.java:167) at com.p6spy.engine.common.Value.convertToString(Value.java:116) at com.p6spy.engine.common.Value.toString(Value.java:63) at com.p6spy.engine.common.PreparedStatementInformation.getSqlWithValues(PreparedStatementInformation.java:56) at com.p6spy.engine.common.P6LogQuery.logElapsed(P6LogQuery.java:203) at com.p6spy.engine.logging.LoggingEventListener.logElapsed(LoggingEventListener.java:107) at com.p6spy.engine.logging.LoggingEventListener.onAfterAnyExecute(LoggingEventListener.java:44) at com.p6spy.engine.event.SimpleJdbcEventListener.onAfterExecuteUpdate(SimpleJdbcEventListener.java:121) at com.p6spy.engine.event.CompoundJdbcEventListener.onAfterExecuteUpdate(CompoundJdbcEventListener.java:157) at com.p6spy.engine.wrapper.PreparedStatementWrapper.executeUpdate(PreparedStatementWrapper.java:100) at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:175) at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3176) at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3690) at org.hibernate.action.internal.EntityInsertAction.execute(EntityInsertAction.java:90) at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:604) at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:478) at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:356) at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:39) at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1454) at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:511) at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:3290) at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:2486) at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:473) at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:178) at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.access$300(JdbcResourceLocalTransactionCoordinatorImpl.java:39) at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:271) at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:104) at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:532) 

استخدم المشروع مجموعة Spring + Hibernate المعتادة لمثل هذه التطبيقات. في مرحلة ما ، لدراسة ارتفاع التطبيق في قاعدة البيانات (وهو ما يفعله معظم الوقت) ، تم التفاف DataSource في p6spy . هذه مكتبة بسيطة ومفيدة للغاية مصممة لاعتراض وتسجيل استعلامات قاعدة البيانات ، وكذلك قياس وقت تنفيذها. أهم ما يميزه هو تسجيل استعلام ينتقل إلى قاعدة البيانات بكل الوسائط ، أي أخذ الاستعلام من السجل يمكن تنفيذه على الفور في وحدة التحكم دون التركيز على تحويل الوسائط (هل يكتب Hibernate بدلاً من ذلك ? ) ، أيهما مناسب عند استخدام @Convert أو في وجود حقول من النوع Date / LocalDate / LocalTime ومشتقاتها. IMHO ، شيء مفيد للغاية في اقتصاد المطور دوي E.


هذا ما يبدو عليه الكيان الذي يحتوي على التقرير:


 @Entity public class ReportEntity { @Id @GeneratedValue private long id; @Lob private byte[] reportContent; } 

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


ثم حدث شيء فظيع وغير متوقع: مزيج من السبات ، مجموعة من البايتات و p6spy تحولت إلى قنبلة موقوتة ، كانت تدق بهدوء في الوقت الحاضر ، وعندما كان هناك الكثير من البيانات ، كانت هناك انفجارات.


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


 // class com.p6spy.engine.common.Value private String toHexString(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { int temp = (int) b & 0xFF; sb.append(HEX_CHARS[temp / 16]); sb.append(HEX_CHARS[temp % 16]); } return sb.toString(); } 

تعليق

بعد حقن تغييرين ( tyts و tyts ) ، يبدو الرمز كما يلي (الإصدار الحالي 3.8.2):


 private String toHexString(byte[] bytes) { char[] result = new char[bytes.length * 2]; int idx = 0; for (byte b : bytes) { int temp = (int) b & 0xFF; result[idx++] = HEX_CHARS[temp / 16]; result[idx++] = HEX_CHARS[temp % 16]; } return new String(result); } 

في المستقبل سوف نسترشد بهذه الطبعة ، لأنها تستخدم في تطبيق العرض التوضيحي.


نتيجة لذلك ، تمت كتابة شيء مثل هذا في السجل


 insert into report_entity (report_content, id) values ('6C6F..........7565', 1); 

انت ترى نعم في حالة وجود مجموعة من الحالات غير الناجحة ، قد يظهر ما يلي في ذاكرة التطبيق:


  • التقرير ، كمجموعة من بايت
  • مجموعة الأحرف المشتقة من مجموعة البايت
  • سلسلة تم الحصول عليها من مجموعة الأحرف التي تم الحصول عليها من مجموعة بايت
  • StringBuilder ، والذي يتضمن نسخة من السلسلة التي تم الحصول عليها من صفيف الأحرف التي تم الحصول عليها من صفيف البايت
  • سلسلة تتضمن نسخة من الصفيف داخل StringBuilder ، والتي تتضمن نسخة من السلسلة التي تم الحصول عليها من صفيف الأحرف التي تم الحصول عليها من صفيف البايت.

في ظل هذه الظروف ، تطبيق تجريبي يتكون من فئتين ، بعد تجميعه وتشغيله على Java 11 (أي بخطوط مضغوطة) مع 1 غيغابايت من الكومة ، يمكنك وضع تقرير يزن 71 ميغابايت فقط!


هناك طريقتان لحل هذه المشكلة دون التخلص من p6spy:


  • استبدل byte[] java.sql.Clob (الحل java.sql.Clob ، لأنه لم يتم تحميل البيانات على الفور OutputStream الضجة مع InputStream / OutputStream )
  • أضف excludebinary=true خاصية excludebinary=true إلى ملف excludebinary=true (تمت إضافته بالفعل في تطبيق الاختبار ، تحتاج فقط إلى فتحه)

في هذه الحالة ، يكون سجل الاستعلام خفيفًا وجميلًا:


 insert into report_entity (report_content, id) values ('[binary]', 1); 

دليل التشغيل انظر README.MD


الاستنتاجات:


  • ثبات (في صفوف معينة) يستحق غاليا EXPENSIVE عزيزي جدا
  • إذا كان لديك جداول بها بيانات حساسة (المظاهر وكلمات المرور وما إلى ذلك) ، فإنك تستخدم p6spy ، والسجلات سيئة ، ثم ... حسنًا ، أنت تفهم
  • إذا كان لديك p6spy وكنت متأكدًا من أنه سيكون دائمًا / دائمًا ، فمن المنطقي بالنسبة للكيانات الكبيرة النظر إلى @DynamicInsert / @DynamicUpdate . النقطة المهمة هي تقليل حجم السجلات عن طريق إنشاء طلب لكل تحديث / إدخال فردي. نعم ، سيتم إنشاء هذه الاستعلامات على الفور في كل مرة ، ولكن في الحالات التي يقوم فيها أحد الكيانات بتحديث حقل واحد من أصل 20 ، قد يكون هذا النوع من الحلول في متناول اليد. راجع وثائق التعليقات التوضيحية أعلاه لمزيد من المعلومات.

هذا كل شيء لهذا اليوم :)

Source: https://habr.com/ru/post/ar457776/


All Articles