تسلسل جافا: السرعة القصوى بدون بنية بيانات جامدة

يعمل فريقنا في Sberbank على تطوير خدمة بيانات الجلسة التي تنظم تبادل سياق جلسة Java واحد بين التطبيقات الموزعة. تحتاج خدمتنا بشكل عاجل إلى إجراء تسلسل سريع جدًا لكائنات Java ، لأن هذا جزء من مهمتنا الحرجة. في البداية ، توصلنا إلى أذهاننا: Google Protocol Buffers و Apache Thrift و Apache Avro و CBORتتطلب المكتبات الثلاثة الأولى من هذه المكتبات إجراء تسلسل للكائنات لوصف مخطط بياناتها. CBOR منخفض المستوى لدرجة أنه لا يمكنه إجراء تسلسل إلا للقيم العددية ومجموعاتها. ما كنا بحاجة إليه هو مكتبة تسلسل جافا "لم تطرح الكثير من الأسئلة" ولم تفرض الفرز اليدوي للأشياء القابلة للتسلسل "إلى ذرات". أردنا إجراء تسلسل لكائنات Java العشوائية دون معرفة أي شيء عنها تقريبًا ، وأردنا القيام بذلك في أسرع وقت ممكن. لذلك ، نظمنا منافسة على الحلول المفتوحة المصدر المتاحة لمشكلة تسلسل Java.

KDPV

المنافسين


بالنسبة للمسابقة ، اخترنا مكتبات تسلسل Java الأكثر شيوعًا ، وذلك باستخدام التنسيق الثنائي بشكل رئيسي ، بالإضافة إلى المكتبات التي عملت جيدًا في مراجعات تسلسل Java الأخرى .
1معيار JavaJava- « »,  Java- .
2Jackson JSON FasterXML/jackson-databind, Java- JSON-.
3Jackson JSON (with types), , , full qualified Java-. JSON- (, ) .
, JSON...
[
  "ru.sbrf.ufs.dto.PersonDto",
  {
    "firstName":"Ivan",
    "lastName":"Ivanov"
  }
]
...
:
public ObjectMapper createMapper() {
    return new ObjectMapper();
}
:
public ObjectMapper createMapper() {
    return new ObjectMapper()
            .enable(
                    ACCEPT_SINGLE_VALUE_AS_ARRAY,
                    ACCEPT_EMPTY_STRING_AS_NULL_OBJECT,
                    ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT,
                    READ_UNKNOWN_ENUM_VALUES_AS_NULL,
                    UNWRAP_SINGLE_VALUE_ARRAYS
            )
            .disable(
                    FAIL_ON_INVALID_SUBTYPE,
                    FAIL_ON_NULL_FOR_PRIMITIVES,
                    FAIL_ON_IGNORED_PROPERTIES,
                    FAIL_ON_UNKNOWN_PROPERTIES,
                    FAIL_ON_NUMBERS_FOR_ENUMS,
                    FAIL_ON_UNRESOLVED_OBJECT_IDS,
                    WRAP_EXCEPTIONS
            )
            .enable(ALLOW_SINGLE_QUOTES)
            .disable(FAIL_ON_EMPTY_BEANS)
            .enable(MapperFeature.PROPAGATE_TRANSIENT_MARKER)
            .setVisibility(FIELD, ANY)
            .setVisibility(ALL, NONE)
            .enableDefaultTyping(NON_FINAL);  // !
}
4Jackson Smile FasterXML/jackson-dataformats-binary/smile, Jackson-, Java- JSON- – Smile.
5Jackson Smile (with types), , «Jackson JSON (with types)» (full qualified Java- ).
6Bson4Jackson michel-kraemer/bson4jackson, Jackson-, Java- JSON- – BSON.
7Bson4Jackson (with types), , «Jackson JSON (with types)» (full qualified Java- ).
8BSON MongoDb mongodb/mongo-java-driver/bson,   Java- BSON-.
9Kryo EsotericSoftware/kryo,  Java- .
10Kryo (unsafe), , sun.misc.Unsafe /.
...
:
com.esotericsoftware.kryo.io.Input
com.esotericsoftware.kryo.io.Output
:
com.esotericsoftware.kryo.io.UnsafeInput
com.esotericsoftware.kryo.io.UnsafeOutput
11FSTمكتبة  RuedigerMoeller / التسلسل السريع التي تحول كائنات Java إلى تنسيق ثنائي خاص بها.
12FST (غير آمن)نفس المكتبة المذكورة أعلاه ، ولكن تم تكوينها لاستخدام فئة sun.misc.Unsafe لتسريع التسلسل / إلغاء التسلسل.
ميزات إعدادات المكتبة ...
:
FSTConfiguration fst = FSTConfiguration.createDefaultConfiguration();
:
FSTConfiguration fst = FSTConfiguration.createUnsafeBinaryConfiguration();
ثلاثة عشرواحد جديدمكتبة  odnoklassniki / one-nio التي تحول كائنات Java إلى تنسيق ثنائي خاص بها.
14One Nio (للإصرار)نفس المكتبة المذكورة أعلاه ، ولكن تم تكوينها بطريقة تتضمن معلومات التعريف التفصيلية حول فئة كائن Java القابل للتسلسل في نتيجة التسلسل. قد يكون هذا مطلوبًا أثناء التخزين طويل المدى byte[](على سبيل المثال ، في قاعدة البيانات) قبل إلغاء التسلسل. أي أن الهدف المنشود هو نفس هدف جاكسون JSON (مع الأنواع).
ميزات إعدادات المكتبة ...
:
byte[] bufWithoutSerializers = new byte[bufferSize];
SerializeStream out = new SerializeStream( bufWithoutSerializers );
out.writeObject(object);
// bufWithoutSerializers is the result
:
byte[] bufWithSerializers = new byte[bufferSize];
PersistStream out = new PersistStream( bufWithSerializers );
out.writeObject(object);
bufWithSerializers = out.toByteArray();
// bufWithSerializers is the result

object- result  -:
1) full qualified object,
2) ,
3) full qualified ,
4) .
- , One Nio , .
ها نحن ذا!

سباق


السرعة هي المعيار الرئيسي لتقييم مكتبات تسلسل جافا التي تشارك في منافستنا المرتجلة. من أجل التقييم الموضوعي لأي من مكتبات التسلسل أسرع ، أخذنا بيانات حقيقية من سجلات نظامنا وقمنا بتكوين بيانات جلسات اصطناعية منها بأطوال مختلفة : من 0 إلى 1 ميجابايت. كان تنسيق البيانات سلاسل وسلاسل البايت.
ملاحظة: بالنظر إلى المستقبل ، يجب القول أن الفائزين والخاسرين قد ظهروا بالفعل على أحجام الكائنات القابلة للتسلسل من 0 إلى 10 كيلوبايت. زيادة أخرى في حجم العناصر إلى 1 ميغابايت لم تغير نتيجة المسابقة.
في هذا الصدد ، من أجل وضوح أفضل ، فإن الرسوم البيانية التالية لأداء متسلقي Java محدودة بحجم الكائنات 10 كيلوبايت.
, :
IntelR CoreTM i7-6700 CPU, 3.4GHz, 8 cores
16 GB
Microsoft Windows 10 (64-bit)
JREIBM J9 VM 1.7.0
: , IBM JRE One Nio ( 13 14). sun.reflect.MagicAccessorImpl private final ( ) , . , IBM JRE  sun.reflect.MagicAccessorImpl, , runtime .

(, Serialization-FAQ, One Nio ), fork ,  sun.reflect.MagicAccessorImpl  .  sun.reflect.MagicAccessorImpl  fork- sun.misc.Unsafe .
بالإضافة إلى ذلك ، في شوكة لدينا ، تم تحسين تسلسل السلاسل - بدأت السلاسل في إجراء تسلسل 30-40 ٪ أسرع عند العمل على IBM JRE.

في هذا الصدد ، في هذا المنشور ، تم الحصول على جميع نتائج مكتبة One Nio على مفترقنا ، وليس في المكتبة الأصلية.
تم إجراء القياس المباشر لسرعة التسلسل / إلغاء التسلسل باستخدام Java Microbenchmark Harness (JMH) - وهي أداة من OpenJDK لبناء وتشغيل معايير الأداء. لكل قياس (نقطة واحدة على الرسم البياني) ، تم استخدام 5 ثوان "لتسخين" JVM و 5 ثوانٍ أخرى لقياسات الوقت نفسها ، متبوعة بالمتوسط.
تحديث:
كود JMH المرجعي بدون بعض التفاصيل
public class SerializationPerformanceBenchmark {

    @State( Scope.Benchmark )
    public static class Parameters {

        @Param( {
            "Java standard",
            "Jackson default",
            "Jackson system",
            "JacksonSmile default",
            "JacksonSmile system",
            "Bson4Jackson default",
            "Bson4Jackson system",
            "Bson MongoDb",
            "Kryo default",
            "Kryo unsafe",
            "FST default",
            "FST unsafe",
            "One-Nio default",
            "One-Nio for persist"
        } )
        public String serializer;
        public Serializer serializerInstance;

        @Param( { "0", "100", "200", "300", /*... */ "1000000" } )  // Toward 1 MB
        public int sizeOfDto;
        public Object dtoInstance;
        public byte[] serializedDto;

        @Setup( Level.Trial )
        public void setup() throws IOException {
            serializerInstance = Serializers.getMap().get( serializer );
            dtoInstance = DtoFactory.createWorkflowDto( sizeOfDto );
            serializedDto = serializerInstance.serialize( dtoInstance );
        }

        @TearDown( Level.Trial )
        public void tearDown() {
            serializerInstance = null;
            dtoInstance = null;
            serializedDto = null;
        }
    }

    @Benchmark
    public byte[] serialization( Parameters parameters ) throws IOException {
        return parameters.serializerInstance.serialize(
                parameters.dtoInstance );
    }

    @Benchmark
    public Object unserialization( Parameters parameters ) throws IOException, ClassNotFoundException {
        return parameters.serializerInstance.deserialize(
                parameters.serializedDto,
                parameters.dtoInstance.getClass() );
    }
}

إليك ما حدث: أولاً ، لاحظ أن خيارات المكتبة التي تضيف بيانات تعريف إضافية إلى نتيجة التسلسل أبطأ من التكوينات الافتراضية للمكتبات نفسها (انظر التكوينات "مع الأنواع" و "للاستمرار"). بشكل عام ، بغض النظر عن التكوين ، يصبح جاكسون JSON و Bson4Jackson ، الذين هم خارج السباق ، غرباء وفقًا لنتائج التسلسل . بالإضافة إلى ذلك ، يخرج Java Standard عن السباق بناءً على نتائج إلغاء التسلسل ، مثل بالنسبة لأي حجم من البيانات القابلة للتسلسل ، تكون إزالة التسلسل أبطأ بكثير من المنافسين. ألق نظرة عن كثب على المشاركين المتبقين: وفقًا لنتائج التسلسل ، فإن مكتبة FST في قادة واثقين

السباق - جميع المشاركين







السباق - باستثناء الغرباء

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

من أجل وضع جميع النقاط أكثر من AND ، دعونا نلقي نظرة على النتيجة الإجمالية للتسلسل وإلغاء التسلسل: أصبح من الواضح أن هناك قائدين لا لبس فيهما: FST (غير آمن) و One Nio . إذا كانت الأجسام الصغيرة FST (غير آمنة)

السباق - باستثناء الغرباء (التصنيف العام)


 يقود بثقة ، ثم مع زيادة حجم الأشياء القابلة للتسلسل ، يبدأ في التنازل ، وفي النهاية ، أدنى من One Nio .

المركز الثالث مع الزيادة في حجم الأشياء القابلة للتسلسل يتخذ بثقة من قبل BSON MongoDb ، على الرغم من أنه يتقدم مرتين تقريبًا على القادة.

وزن


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

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

وكانت نتائج "الترجيح" كما يلي:

وزن

من المتوقع أن تكون النتائج الأكثر إحكاما هي التسلسل من أحد قادة السباق: One Nio .
حصل المركز الثاني في التعاقد على BSON MongoDb  (الذي احتل المركز الثالث في السباق).
في المركز الثالث من حيث التعاقد ، "هربت" مكتبة كريو ، التي فشلت سابقًا في إثبات نفسها في السباق.

يتم أيضًا ضغط نتائج التسلسل لهؤلاء القادة الثلاثة "للوزن" بشكل مثالي (تقريبًا اثنين). اتضح أنه الأكثر قابلية للضغط: المعادل الثنائي لـ JSON هو Smile و JSON نفسه.

حقيقة غريبة - كل الفائزين في "الوزن" أثناء التسلسل يضيفون نفس كمية بيانات الخدمة إلى كائنات صغيرة وكبيرة قابلة للتسلسل.

المرونة


قبل اتخاذ قرار مسؤول بشأن اختيار فائز ، قررنا التحقق بدقة من مرونة كل مُسلسل ومدى قابليته للاستخدام.
لهذا ، قمنا بتجميع 20 معيارًا لتقييم المتسللين المشاركين في المسابقة حتى لا يمر "ماوس واحد" عبر أعيننا.

المرونة
الحواشي مع التفسيرات
1    LinkedHashMap.
2    — , — .
3    — , — .
4    sun.reflect.MagicAccessorImpl — : boxing/unboxing, BigInteger/BigDecimal/String. MagicAccessorImpl ( ' fork One Nio) — .
5    ArrayList.
6    ArrayList HashSet .
7    HashMap.
8    — , , /Map-, ( HashMap).
9    -.
10  One Nio — , ' fork- — .
11 .
UPD: وفقًا للمعيار الثالث عشر ، حصل One Nio (للإصرار) على نقطة أخرى (19).

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

كان من العار أن ندرك ، لكن قادتنا وفقًا لنتائج السباقات والوزن - FST (غير آمن) و One Nio- اتضح أنهم غرباء من حيث المرونة ... ومع ذلك ، كنا مهتمين بحقيقة غريبة: واحد Nio في التكوين "للإصرار" (ليس الأسرع وليس الأصغر) سجل أكبر عدد من النقاط من حيث المرونة - 19/20. كما أن فرصة جعل التهيئة الافتراضية (السريعة والمدمجة) One Nio تعمل بشكل مرن تبدو جذابة للغاية - وكانت هناك طريقة.

في البداية ، عندما قدمنا ​​المشاركين في المسابقة ، قيل أن One Nio (للإصرار) تم تضمينه في نتيجة التسلسل معلومات وصفية تفصيلية حول فئة كائن Java القابل للتسلسل(*). باستخدام هذه المعلومات الوصفية لإلغاء التسلسل ، تعرف مكتبة One Nio بالضبط ما تبدو عليه فئة الكائن القابل للتسلسل في وقت التسلسل. على أساس هذه المعرفة أن خوارزمية One Nio deserialization مرنة للغاية بحيث توفر أقصى قدر من التوافق الناتج عن التسلسل byte[].

اتضح أنه يمكن الحصول على المعلومات الوصفية (*) بشكل منفصل للفئة المحددة ، وتسلسلها  byte[] وإرسالها إلى الجانب حيث سيتم إلغاء تسلسل كائنات Java من هذه الفئة:
مع التعليمات البرمجية في الخطوات ...
//  №1:  -   SomeDto
one.nio.serial.Serializer<SomeDto> dtoSerializerWithMeta = Repository.get( SomeDto.class );
byte[] dtoMeta = serializeByDefaultOneNioAlgorithm( dtoSerializerWithMeta );
//  №1:  dtoMeta  №2

//  №2:  -    SomeDto      One Nio
one.nio.serial.Serializer<SomeDto> dtoSerializerWithMeta = deserializeByOneNio( dtoMeta );
Repository.provideSerializer( dtoSerializerWithMeta );

//  №1:    SomeDto
byte[] bytes1 = serializeByDefaultOneNioAlgorithm( object1 );
byte[] bytes2 = serializeByDefaultOneNioAlgorithm( object2 );
...
//  №1:    №2

//  №2:      SomeDto
SomeDto object1 = deserializeByOneNio( bytes1 );
SomeDto object2 = deserializeByOneNio( bytes2 );
...

إذا قمت بتنفيذ هذا الإجراء الصريح لتبادل المعلومات الوصفية حول الفئات بين الخدمات الموزعة ، فستكون هذه الخدمات قادرة على إرسال كائنات Java المتسلسلة إلى بعضها البعض باستخدام التكوين الافتراضي (السريع والمضغوط) One Nio. بعد كل شيء ، أثناء تشغيل الخدمات ، فإن إصدارات الفئات على جانبيها لم تتغير ، مما يعني أنه لا يوجد سبب "لسحب ذهابًا وإيابًا" للمعلومات الوصفية الثابتة داخل كل نتيجة تسلسل خلال كل تفاعل. وبالتالي ، بعد أن قمت بعمل أكثر قليلاً في البداية ، يمكنك استخدام سرعة وانضغاط One Nio في وقت واحد مع مرونة One Nio (للاستمرار) . بالضبط ما هو مطلوب!

نتيجة لذلك ، لنقل كائنات Java بين الخدمات الموزعة في شكل تسلسلي (هذا هو ما نظمناه هذه المسابقة) كان One Nio هو الفائز في المرونة  (19/20).
من بين متسلقي جافا الذين ميزوا أنفسهم سابقًا في السباق والوزن ، لم تظهر المرونة السيئة:

  • BSON MongoDb  (14.5 / 20) ،
  • كريو (13/20).

الركيزة


تذكر نتائج مسابقات تسلسل جافا السابقة:

  • في السباقات ، تم تقسيم أول سطرين من التصنيف على FST (غير آمن) و One Nio ، و BSON MongoDb في المركز الثالث ،
  • هزم واحد Nio الوزن ، يليه BSON MongoDb و Kryo ،
  • من حيث المرونة ، فقط لمهمتنا لتبادل سياق الجلسة بين التطبيقات الموزعة ، ذهب One Nio مرة أخرى أولاً  ، وفاز BSON MongoDb و Kryo .

وبالتالي ، من حيث مجموع النتائج المحققة ، فإن القاعدة التي حصلنا عليها هي كما يلي:

  1. One Nio
    في المسابقة الرئيسية - السباقات - شارك المركز الأول مع FST (غير آمن) ، لكنه وزن المنافس بشكل كبير في الوزن واختبار المرونة.
  2. FST (غير آمن)
    أيضًا مكتبة تسلسل Java سريعة جدًا ، ومع ذلك ، فإنها تفتقر إلى التوافق المباشر والخلفي لصفيف البايت الناتج عن التسلسل.
  3. BSON MongoDB + Kryo
    2 3- Java-, . 2- , . Collection Map, BSON MongoDB custom- / (Externalizable ..).

في Sberbank ، في خدمة بيانات الجلسة ، استخدمنا مكتبة One Nio ، التي فازت بالمركز الأول في منافسينا. باستخدام هذه المكتبة ، تم إجراء تسلسل لبيانات سياق جلسة Java ونقلها بين التطبيقات. بفضل هذه المراجعة ، تسارعت سرعة نقل الجلسة عدة مرات. أظهر اختبار الحمل أنه في السيناريوهات القريبة من سلوك المستخدم الفعلي في Sberbank Online ، تم الحصول على تسريع يصل إلى 40٪ فقط بسبب هذا التحسين وحده. هذه النتيجة تعني تقليل وقت استجابة النظام لإجراءات المستخدم ، مما يزيد من درجة رضا عملائنا.

في المقالة التالية سأحاول أن أثبت عمليًا التسارع الإضافي لـ One Nioمستمدة من استخدام الفصل sun.reflect.MagicAccessorImpl. لسوء الحظ ، لا يدعم IBM JRE أهم خصائص هذه الفئة ، مما يعني أن الإمكانات الكاملة لـ One Nio في هذا الإصدار من JRE لم يتم الكشف عنها بعد. يتبع.

All Articles