Serialização Java: velocidade máxima sem uma estrutura de dados rígida

Nossa equipe do Sberbank está desenvolvendo um serviço de dados de sessão que organiza o intercâmbio de um único contexto de sessão Java entre aplicativos distribuídos. Nosso serviço precisa urgentemente de serialização muito rápida de objetos Java, pois isso faz parte de nossa tarefa de missão crítica. Inicialmente, eles vieram à nossa mente: Buffers de Protocolo do Google , Apache Thrift , Apache Avro , CBORAs três primeiras bibliotecas requerem serialização de objetos para descrever o esquema de seus dados. O CBOR é tão baixo que só pode serializar valores escalares e seus conjuntos. O que precisávamos era de uma biblioteca de serialização Java que "não fizesse muitas perguntas" e não forçasse a classificação manual de objetos serializáveis ​​"em átomos". Queríamos serializar objetos Java arbitrários sem saber praticamente nada sobre eles, e queríamos fazer isso o mais rápido possível. Portanto, organizamos uma competição pelas soluções Open Source disponíveis para o problema de serialização do Java.

KDPV

Concorrentes


Para a competição, selecionamos as bibliotecas de serialização Java mais populares, principalmente usando o formato binário, bem como as bibliotecas que funcionaram bem em outras revisões de serialização Java.
1 1Padrão 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
11FSTA biblioteca  RuedigerMoeller / serialização rápida que converte objetos Java em seu próprio formato binário.
12FST (inseguro)A mesma biblioteca acima, mas configurada para usar a classe sun.misc.Unsafe para acelerar a serialização / desserialização.
Recursos das configurações da biblioteca ...
:
FSTConfiguration fst = FSTConfiguration.createDefaultConfiguration();
:
FSTConfiguration fst = FSTConfiguration.createUnsafeBinaryConfiguration();
trezeUm niobiblioteca odnoklassniki / one-nio que converte objetos Java em seu próprio formato binário.
14Um Nio (para persistir)A mesma biblioteca acima, mas configurada de forma a incluir meta informações detalhadas sobre a classe do objeto Java serializável no resultado da serialização. Isso pode estar em demanda durante o armazenamento de longo prazo byte[](por exemplo, no banco de dados) antes da desserialização. Ou seja, o objetivo perseguido é o mesmo de Jackson JSON (com tipos).
Recursos das configurações da biblioteca ...
:
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 , .
Aqui vamos nós!

Corrida


A velocidade é o principal critério para avaliar as bibliotecas de serialização Java participantes de nossa competição improvisada. Para avaliar objetivamente qual das bibliotecas de serialização é mais rápida, extraímos dados reais dos logs de nosso sistema e compusemos dados de sessão sintéticos com diferentes comprimentos : de 0 a 1 MB. O formato dos dados foram cadeias de caracteres e matrizes de bytes.
Nota: Olhando para o futuro, deve-se dizer que vencedores e perdedores já apareceram no tamanho de objetos serializáveis ​​de 0 a 10 KB. Um aumento adicional no tamanho dos objetos para 1 MB não mudou o resultado da competição.
Nesse sentido, para melhor clareza, os gráficos a seguir do desempenho dos serializadores Java são limitados pelo tamanho dos objetos de 10 KB.
, :
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 .
Além disso, em nossa bifurcação, a serialização de cadeias foi otimizada - as cadeias começaram a ser serializadas 30 a 40% mais rapidamente ao trabalhar no IBM JRE.

Nesse sentido, nesta publicação, todos os resultados da biblioteca One Nio foram obtidos em nosso próprio fork, e não na biblioteca original.
A medição direta da velocidade de serialização / desserialização foi realizada usando o Java Microbenchmark Harness (JMH) - uma ferramenta do OpenJDK para construção e execução de benchmark-s. Para cada medição (um ponto no gráfico), foram utilizados 5 segundos para “aquecer” a JVM e outros 5 segundos para as próprias medições de tempo, seguidos da média.
UPD:
Código de referência JMH sem alguns detalhes
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() );
    }
}

Eis o que aconteceu: Primeiro, observamos que as opções de biblioteca que adicionam metadados adicionais ao resultado da serialização são mais lentas que as configurações padrão das mesmas bibliotecas (consulte as configurações “com tipos” e “para persistir”). Em geral, independentemente da configuração, Jackson JSON e Bson4Jackson, que estão fora da corrida, tornam-se estranhos de acordo com os resultados da serialização . Além disso, o Java Standard sai da corrida com base nos resultados de desserialização , como para qualquer tamanho de dados serializáveis, a desserialização é muito mais lenta que a dos concorrentes. Dê uma olhada nos participantes restantes: de acordo com os resultados da serialização, a biblioteca do FST está em líderes confiantes

Corrida - todos os participantes







Corrida - exceto para forasteiros

, e com um aumento no tamanho dos objetos, o One Nio "pisa nos calcanhares" . Observe que, para o One Nio, a opção "para persistir" é muito mais lenta que a configuração padrão para a velocidade de serialização.
Se você observar a desserialização, vemos que o One Nio conseguiu superar o FST com o aumento do tamanho dos dados . Neste último, pelo contrário, a configuração não padrão “insegura” realiza a desserialização muito mais rapidamente.

Para colocar todos os pontos acima de E, vamos ver o resultado total da serialização e desserialização: ficou óbvio que existem dois líderes inequívocos: FST (inseguro) e One Nio . Se em objetos pequenos FST (inseguro)

Corrida - exceto para forasteiros (classificação geral)


 lidera com confiança e, em seguida, com o aumento do tamanho dos objetos serializáveis, ele começa a conceder e, finalmente, inferior a One Nio .

A terceira posição com o aumento no tamanho dos objetos serializáveis ​​é assumida com confiança pelo BSON MongoDb , embora esteja quase duas vezes à frente dos líderes.

Pesagem


O tamanho do resultado da serialização é o segundo critério mais importante para avaliar as bibliotecas de serialização Java. De certa forma, a velocidade da serialização / desserialização depende do tamanho do resultado: é mais rápido formar e processar um resultado compacto do que o volume. Para "ponderar" os resultados da serialização, todos os mesmos objetos Java foram usados, formados a partir de dados reais extraídos dos logs do sistema (cadeias de caracteres e matrizes de bytes).

Além disso, uma propriedade importante do resultado da serialização também é o tamanho da compactação (por exemplo, para salvá-la no banco de dados ou em outros armazenamentos). Em nossa competição, usamos o algoritmo de compactação Deflate , que é a base para ZIP e gzip.

Os resultados da "pesagem" foram os seguintes:

Pesagem

Espera-se que os resultados mais compactos tenham sido serializados por um dos líderes da corrida: um Nio .
O segundo lugar no compacto foi para o BSON MongoDb  (que ficou em terceiro lugar na corrida).
Em terceiro lugar, em termos de compacidade, a biblioteca Kryo "escapou" , que antes não havia se provado na corrida.

Os resultados de serialização desses três líderes de "pesagem" também são perfeitamente compactados (quase dois). Acabou sendo o mais descompactável: o equivalente binário do JSON é o Smile e o próprio JSON.

Um fato curioso - todos os vencedores da "pesagem" durante a serialização adicionam a mesma quantidade de dados de serviço a objetos serializáveis ​​pequenos e grandes.

Flexibilidade


Antes de tomar uma decisão responsável sobre a escolha de um vencedor, decidimos verificar minuciosamente a flexibilidade de cada serializador e sua usabilidade.
Para isso, compilamos 20 critérios para avaliar nossos serializadores participantes da competição, de modo que “nem um único mouse escapasse” de nossos olhos.

Flexibilidade
Notas de rodapé com explicações
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: De acordo com o 13º critério, um Nio (para persistir) recebeu outro ponto (19).

Esse meticuloso "exame dos candidatos" foi talvez o estágio mais demorado do nosso "casting". Mas esses resultados de comparação abrem bem a conveniência de usar bibliotecas de serialização. Como conseqüência, você pode usar esses resultados como referência.

Foi uma pena perceber, mas nossos líderes de acordo com os resultados de corridas e pesagem - FST (inseguro) e One Nio- acabamos sendo estranhos em termos de flexibilidade ... No entanto, estávamos interessados ​​em um fato curioso: um Nio na configuração "para persistir" (não o mais rápido nem o mais compacto) obteve o maior número de pontos em termos de flexibilidade - 19/20. A oportunidade de fazer com que a configuração padrão do One Nio (rápida e compacta) funcionasse com flexibilidade também parecia muito atraente - e havia uma maneira.

No início, quando apresentamos os participantes à competição, foi dito que o One Nio (para persistir) incluía no resultado da serialização meta-informações detalhadas sobre a classe do objeto Java serializável(*) Usando essas metainformações para desserialização, a biblioteca do One Nio sabe exatamente como era a classe do objeto serializável no momento da serialização. É com base nesse conhecimento que o algoritmo de desserialização do One Nio é tão flexível que fornece a compatibilidade máxima resultante da serialização byte[].

Descobriu-se que as meta-informações (*) podem ser obtidas separadamente para a classe especificada, serializadas  byte[] e enviadas para o lado onde os objetos Java dessa classe serão desserializados:
Com o código nas etapas ...
//  №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 );
...

Se você executar esse procedimento explícito para a troca de meta-informações sobre classes entre serviços distribuídos, esses serviços poderão enviar objetos Java serializados entre si usando a configuração padrão do One Nio (rápida e compacta). Afinal, enquanto os serviços estão em execução, as versões das classes estão inalteradas, o que significa que não há razão para "arrastar para frente e para trás" as metainformações constantes em cada resultado de serialização durante cada interação. Assim, tendo feito um pouco mais de ação no começo, você pode usar a velocidade e a compactação do One Nio simultaneamente com a flexibilidade do One Nio (para persistir) . Exatamente o que é necessário!

Como resultado, para transferir objetos Java entre serviços distribuídos em formato serializado (foi para isso que organizamos essa competição) Um Nio foi o vencedor em flexibilidade  (19/20).
Entre os serializadores Java que se distinguiram anteriormente nas corridas e pesagens, não foi demonstrada uma flexibilidade ruim:

  • BSON MongoDb  (14,5 / 20),
  • Kryo (13/20).

Pedestal


Lembre-se dos resultados de competições anteriores de serialização Java:

  • nas corridas, as duas primeiras linhas da classificação foram divididas por FST (inseguro) e One Nio , e o BSON MongoDb ficou em terceiro lugar ,
  • Um Nio venceu a pesagem , seguido por BSON MongoDb e Kryo ,
  • em termos de flexibilidade, apenas para nossa tarefa de trocar o contexto da sessão entre aplicativos distribuídos, o One Nio voltou a ser o primeiro  , e o BSON MongoDb e Kryo se destacaram .

Assim, em termos da totalidade dos resultados alcançados, o pedestal obtido é o seguinte:

  1. Um Nio
    Na competição principal - as corridas - dividiram o primeiro lugar com o FST (inseguro) , mas pesaram significativamente o concorrente na flexibilidade de pesagem e teste.
  2. FST (insegura)
    Também uma biblioteca de serialização Java muito rápida, no entanto, carece da compatibilidade direta e reversa das matrizes de bytes resultantes da serialização.
  3. BSON MongoDB + Kryo
    2 3- Java-, . 2- , . Collection Map, BSON MongoDB custom- / (Externalizable ..).

No Sberbank, em nosso serviço de dados de sessão, usamos a biblioteca One Nio , que conquistou o primeiro lugar em nossa competição. Usando esta biblioteca, os dados do contexto da sessão Java foram serializados e transferidos entre aplicativos. Graças a esta revisão, a velocidade do transporte da sessão acelerou várias vezes. O teste de carga mostrou que, em cenários próximos ao comportamento real do usuário no Sberbank Online, a aceleração de até 40% foi obtida apenas devido apenas a essa melhoria. Tal resultado significa uma redução no tempo de resposta do sistema às ações do usuário, o que aumenta o grau de satisfação de nossos clientes.

No próximo artigo, tentarei demonstrar em ação a aceleração adicional do One Nioderivada do uso da classe sun.reflect.MagicAccessorImpl. Infelizmente, o IBM JRE não suporta as propriedades mais importantes dessa classe, o que significa que o potencial total do One Nio nesta versão do JRE ainda não foi revelado. Continua.

All Articles