На днях мы обнаружили интересную проблему при использовании 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”