في إعداد المقال الأخير ، قام أحد الأمثلة الغريبة التي تم اكتشافها في أحد تطبيقاتنا بفت انتباهي. لقد صممته كمقال منفصل ، الذي تقرأه حاليًا.
الجوهر بسيط للغاية: عند إنشاء تقرير وكتابته إلى قاعدة البيانات ، من وقت لآخر بدأنا في استلام 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 الذي نستخدمه ، تم ذلك على النحو التالي:
تعليقبعد حقن تغييرين ( 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 ، قد يكون هذا النوع من الحلول في متناول اليد. راجع وثائق التعليقات التوضيحية أعلاه لمزيد من المعلومات.
هذا كل شيء لهذا اليوم :)