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.
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.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.
, :: , 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 detalhespublic 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" } )
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
, 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)
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:
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.
Notas de rodapé com explicações1 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 ...
one.nio.serial.Serializer<SomeDto> dtoSerializerWithMeta = Repository.get( SomeDto.class );
byte[] dtoMeta = serializeByDefaultOneNioAlgorithm( dtoSerializerWithMeta );
// №1: dtoMeta №2
one.nio.serial.Serializer<SomeDto> dtoSerializerWithMeta = deserializeByOneNio( dtoMeta );
Repository.provideSerializer( dtoSerializerWithMeta );
byte[] bytes1 = serializeByDefaultOneNioAlgorithm( object1 );
byte[] bytes2 = serializeByDefaultOneNioAlgorithm( object2 );
...
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:- 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. - 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. - 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.