Рубрики
Без рубрики

Переопределение сериализации Kryo по умолчанию для объектов связанных данных

На днях мы обнаружили интересную проблему при использовании Kryo. У одного из наших клиентов был большой набор d… Помечен как kotlin, java.

На днях мы обнаружили интересную проблему при использовании Kryo.

У одного из наших клиентов был большой набор данных, и они отправляли каждый элемент в другой процесс примерно так:

// some large set full of elements we want to send off
set.forEach {
    send(it)  // <-- (de)Serialization happens here
}

Все это было хорошо и хорошо, пока набор не вырос до пары тысяч предметов. Затем они начали сталкиваться с ошибками переполнения стека (!).

exception="java.lang.StackOverflowError
at com.esotericsoftware.kryo.io.Output.flush(Output.java:185)
at com.esotericsoftware.kryo.io.OutputChunked.flush(OutputChunked.java:59)
at com.esotericsoftware.kryo.io.Output.require(Output.java:164)
at com.esotericsoftware.kryo.io.Output.writeBytes(Output.java:251)
at com.esotericsoftware.kryo.io.Output.write(Output.java:219)
at com.esotericsoftware.kryo.io.Output.flush(Output.java:185)
at com.esotericsoftware.kryo.io.OutputChunked.flush(OutputChunked.java:59)
at com.esotericsoftware.kryo.io.Output.require(Output.java:164)
at com.esotericsoftware.kryo.io.Output.writeBytes(Output.java:251)
at com.esotericsoftware.kryo.io.Output.write(Output.java:219)
at com.esotericsoftware.kryo.io.Output.flush(Output.java:185)
at com.esotericsoftware.kryo.io.OutputChunked.flush(OutputChunked.java:59)

И когда они увеличили размер стека, вместо этого у них закончилась память (!!) .

exception="java.lang.OutOfMemoryError
at java.io.ByteArrayOutputStream.hugeCapacity(ByteArrayOutputStream.java:123)
at java.io.ByteArrayOutputStream.grow(ByteArrayOutputStream.java:117)
at java.io.ByteArrayOutputStream.ensureCapacity(ByteArrayOutputStream.java:93)
at java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.java:153)
at com.esotericsoftware.kryo.io.Output.flush(Output.java:185)
at com.esotericsoftware.kryo.io.Output.close(Output.java:196)

Мы посмотрели, что происходило во время сериализации, и хитрость заключалась в том, что наш клиент использовал (по нашему предложению) LinkedHashSet . Поскольку LinkedHashSet фактически представляет собой двусвязный список под капотом (да, это нечто большее, но давайте сделаем это проще), когда Kryo пошел сериализовать текущую запись в наборе, он также сериализует предыдущую и следующие элементы. Затем для этих элементов он будет продолжаться и продолжаться, эффективно сериализуя каждый элемент в наборе каждый раз!

Интересно, что это было связано с сериализацией самого набора, который отлично работает. Но раскрытие итератора множеств и соответствующей записи приводит к этой проблеме.

Чтобы решить эту проблему, мы решили, что нам придется переопределить реализацию сериализатора записей по умолчанию и пропустить предыдущие/следующие ссылки.

object LinkedHashMapEntrySerializer : Serializer>() {
    // Create a dummy map so that we can get the LinkedHashMap$Entry from it
    // The element type of the map doesn't matter.  The entry is all we want
    private val DUMMY_MAP = linkedMapOf(1L to 1)
    fun getEntry(): Any = DUMMY_MAP.entries.first()
    private val constr: Constructor<*> = getEntry()::class.java.declaredConstructors.single().apply { isAccessible = true }

    /**
     * Kryo would end up serialising "this" entry, then serialise "this.after" recursively, leading to a very large stack.
     * we'll skip that and just write out the key/value
     */
    override fun write(kryo: Kryo, output: Output, obj: Map.Entry<*, *>) {
        val e: Map.Entry<*, *> = obj
        kryo.writeClassAndObject(output, e.key)
        kryo.writeClassAndObject(output, e.value)
    }

    override fun read(kryo: Kryo, input: Input, type: Class>): Map.Entry<*, *> {
        val key = kryo.readClassAndObject(input)
        val value = kryo.readClassAndObject(input)
        return constr.newInstance(0, key, value, null) as Map.Entry<*, *>
    }
}

Несколько вещей, которые следует отметить в этой строке:

object LinkedHashMapEntrySerializer : Serializer>() {

Возможно, вы думаете: “Разве этот пост не был о наборе ?”

И это правильно – так оно и есть. Но оказывается, что Хэш-наборы реализованы в виде LinkedHashMap со всеми значениями, указывающими на синглтон (называемый ПРИСУТСТВУЕТ ). Таким образом, при обработке дела Карта мы в конечном итоге получаем Набор бесплатно.

Также обратите внимание, что переопределенный сериализатор предназначен для типов Карта. Запись<*, *> . Однако то, что мы на самом деле хотим запечатлеть, – это LinkedHashMap. Запись которая является частной. Для этого нам пришлось немного испачкаться и поиграть с отражением.

    // Create a dummy map so that we can get the LinkedHashMap$Entry from it
    // The element type of the map doesn't matter.  The entry is all we want
    private val DUMMY_MAP = linkedMapOf(1L to 1)
    fun getEntry(): Any = DUMMY_MAP.entries.first()

И еще раз, чтобы получить конструктор для LinkedHashMap. Запись .

    private val constr: Constructor<*> = getEntry()::class.java.declaredConstructors.single().apply { isAccessible = true }

Наконец нам пришлось зарегистрировать наш новый сериализатор в Kryo.

    register(LinkedHashMapEntrySerializer.getEntry()::class.java, LinkedHashMapEntrySerializer)

Вот так! Мы закончили…

Ну, не совсем так. Нам также пришлось переопределить сериализацию для итератора. Это был просто вопрос записи ключа элемента, на который ссылался итератор при сериализации, и возврата карты при десериализации, пока мы не окажемся в нужном месте. Я оставлю эту реализацию в качестве домашнего задания. Или, возможно, другой пост.

Конечным результатом является то, что теперь мы можем (де) сериализовать многие тысячи объектов, не беспокоясь об ошибках стекового потока.

P.S. Быстрым и грязным обходным путем было вызвать .ToList () перед .forEach . Эта проблема не существует в несвязанных списках.

Оригинал: “https://dev.to/r3/overriding-default-kryo-serialization-for-linked-data-objects-47l4”