Desenvolvendo com o Java de tempo real, Parte 3: Escreva, valide e analise com um aplicativo Java de tempo real

Explore as ferramentas e técnicas para validação e aperfeiçoamento da qualidade de serviço determinística

Embasado nos dois artigos anteriores em uma série de três partes e na série Real-time Java , este artigo mostra como projetar, validar e analisar um aplicativo básico de tempo real. A ênfase nos aspectos práticos de validação na obtenção da qualidade de serviço determinística do aplicativo.

Andrew Hall, Software Engineer, IBM

Andrew HallAndrew Hall entrou para o Centro de Tecnologia Java da IBM em 2004, iniciando na equipe de teste do sistema onde trabalhou por dois anos. Ele ficou 18 meses na equipe de serviço Java, onde depurou dezenas de problemas de memória nativa em várias plataformas. Atualmente ele é o desenvolvedor da equipe de Confiabilidade, Disponibilidade e Capacidade de Manutenção Java. Nas horas vagas ele se dedica à leitura, fotografia e diversões.



Alan Stevens, Software Engineer, IBM Hursley Lab

Alan StevensAlan Stevens joined IBM in 1988 and has worked on a variety of software performance projects, including CICS, and Java since 1997. He is currently assigned to the IBM Real Time project.



11/Nov/2009

Este artigo, o terceiro e último da série Developing with real time Java ilustra:

  • Os requisitos de desempenho e temporal do aplicativo.
  • Por que o Java de tempo não-real não é adequado para o aplicativo?
  • Quais técnicas de programação Java de tempo real são selecionadas?
  • Considerações para atingir o determinismo.
  • Testando e validando o determinismo do aplicativo.
  • Ferramentas e técnicas para resolver os problemas de determinismo de tempo real.
  • Opções para aumentar a previsibilidade.

Mantivemos o design e o código do aplicativo simples para que nos concentremos nestes objetivos. O código fonte completo está disponível para download.

O aplicativo demo

Nossa tarefa é criar um aplicativo Java para ser executado em um sistema operacional Linux® que fornece leituras de temperatura a baixa latência de um dispositvo industrial. Para se beneficiar com a produtividade e portabilidade da linguagem Java, o código deve ser mantido o mais próximo possível do padrão Java em tempo não-real. A fim de manter a eficiência do dispositivo, as leituras devem ser feitas e entregues aos programas do consumidor que, por sua vez, controlam as taxas de alimentação e de resfriamento para o dispositivo. O requisito é que as leituras sejam disponibilizadas no prazo de 5 milissegundos. Até mesmo pequenos atrasos podem fazer com que as taxas de alimentação e de resfriamento se tornem subótimas, diminuindo a eficiência do dispositivo.

O paradigma de fornecer dados para os consumidores a intervalos regulares a latências muito baixas é comum em sistemas de tempo real; os dados podem facilmente ser os preços das ações no mercado ou sinais de radar. A Parte 2 descreve as muitas razões pelas quais um aplicativo não pode ser escrito em código convencional Java e ainda atender estes requisitos temporais. A Tabela 1 sintetiza essas razões e relaciona as soluções disponíveis correspondentes em Java de tempo real. (Nem todas as implementações fornecem o mesmo conjunto de soluçoes.)

Tabela 1. Java convencional versus de tempo real.
Emisão em Java convencionalSolução em Java de tempo-real
Atrasos de carregamento de classesOs atrasos podem ser evitados por meio do precarregamento das classes requisitadas.
Atrasos da compilação JIT (Just-in-time)Os atrasos podem ser evitados pela compilação AOT (Ahead-of-time) ou compilação JIT assíncrona.
Suporte muito limitado para prioridade de encadeamento a nível do sistema operacionalA RTSJ (Real Time Specification for Java) garante que, pelo menos, vinte e oito níveis e prioridade estão disponíveis.
Nenhum suporte para o controle da política de processamento não-padronizado.A RTSJ fornece ao programador a opção de escolher — por exemplo, o FIFO (first-in-first-out - primeiro que entra, primeiro que sai).
Grandes atrasos do GC (garbage-collection - coletor de lixo)

O RTSJ fornece áreas não-heap de memória (escopos e imortais) para aplicativos NoHeapRealtimeThread (NHRT) que não estão sujeitos às pausas do GC.

Os coletores de lixo de tempo real não estão disponíveis para reduzir os atrasos do GC para os encadeamentos do aplicativo.

Os atrasos dos encadeamentos kernel do sistema operacional e a inversão de prioridadeKernels em tempo real como as fornecidas em distribuições Linux em tempo real foram projetadas para evitar ameaças kernel de longo prazo, para ser totalmente preemptível e evitar as inversões de prioridade (descritas na Parte 1).

Qual o modelo de programação de Java de tempo real?

O requisito de que o código do aplicativo demo deve ser mantido o mais próximo possível do padrão Java de tempo não-real tem uma consequência imediata para a nossa escolha do modelo de programação RTSJ: a utilização dos NHRTs e dos escopos da memória para evitar os atrasos do GC não é uma opção.

Uitilizando o RealtimeThread, obtemos acesso às extensões de programação em Java de tempo real sem a complexidade da programação NHRT. A utilização dos NHRTs força o programador a tomar o controle da memória em vez de confiar no GC automático normal. Na prática, devem ser utilizados os escopos da memória com NHRTs em vez de memória imortal porque a memória imortal é finita e, fatalmente, se exaurirá. Também há restrições sobre quais classes são "à prova de NHRT", — isto é, que não utilizam ou referenciam objetos no heap principal, que as NHRTs não podem.

Além de uma programação mais simples, podemos esperar:

  • Melhor desempenho. Objetos heap são mais rápidos de criar e referenciar que os objetos imortais ou escopos por causa da barreira da memória e outros excessos de processamento requeridos por objetos não-heap.
  • Depuração facilitada. Os NHRTs devem ser impedidos de fazer referência à memória heap, o que requer que as excessões de memória de acesso sejam depuradas. Falhas fora da momória para os NHRTs também podem ser desencadeadas a partir de um heap imortal ou escopos de memória.

Qual JVM em tempo real?

Dois pacotes IBM® JVM Java 6 estão disponíveis para programas em tempo real para Linux com nomes similares mas com diferentes capacidades e conflitos performance/determinismo (consulte Recursos). O recurso necessário para executar os programas demo neste artigo é o IBM WebSphere® Real Time para RT Linux. Ele fornece as capacidades RTSJ capabilities como RealtimeThread e a classe de temporizador periódico que necessitaremos para implementar o encadeamento da leitura de temperatura para as pausas mais curtas do GC. Ele é executado em um kernel Linux em tempo real e um hardware específico para entregar o determinismo necessário para os aplicativos pesados de tempo real. (O outro pacote — IBM WebSphere Real Time para Linux — suporta aplicativos leves de tempo real, fornecendo um maior rendimento e escalabilidade. Não inclui as bibliotecas de programação RTSJ. Inclui o coletor de lixo Metronome com objetivo de limitar as pausas do GC em 3 milissegundos).


O design do aplicativo demo

A principal desvantagem de utilizar RealtimeThreads em vez de NHRTs serão as (pequenas) pausas do coletor de lixo de tempo real ao qual estarão sujeitos os encadeamentos. Com o coletor de lixo Metronome do WebSphere Real Time, podemos confiar no código do aplicativo sendo interrompido por pausas do coletor apenas cerca de 500 microssegundos. E sabemos que durante um ciclo de GC nosso aplicativo será certamente executado em 70% do tempo. Em outras palavras, sobre uma janela de 10 milissegundos, não esperamos mais de seis quanta GC com cerca de 500 microssegundos cada intercalados com o tempo de aplicativo. Este modelo será explicado posteriormente neste artigo.

Um requisito para este aplicativo é fornecer dados de temperatura em intervalos menores de 5 milissegundos. O design foi concebido para ler a temperatura a cada 2 milissegundos, criando uma contingência suficiente para atender os requisitos mesmo se os ciclos de GC forem executados.

Definição da qualidade do serviço de tempo real

O conceito da qualidade de serviço de tempo real destaca o contraste entre os requisitos determinísticos e os de puro desempenho. Para um aplicativo de tempo real, estamos principalmente interessados em garantir a precisão e a pontualidade dos comportamentos do sistema em vez da taxa de rendimento. De fato, uma implementação Java de tempo real será mais lenta em comparação com a implementação Java padrão porque os excessos de processamento necessários para suportar o RTSJ (como as barreiras de memória) e a necessidade de desabilitar as otimizações durante a execução que criam o comportamento não-determinístico. Dependendo se estamos definindo um sistema complexo de tempo real ou um sistema simples de tempo real, o requisito seria formulado diferentemente.

Em um sistema complexo de tempo real, o sistema precisa tornar disponíveis todos os dados de temperatura para os encadeamentos do consumidor no período de 5 milissegundos. Todos os atrasos de mais de 5 milissegundos constituem uma falha do sistema. Em um sistema simples de tempo real, o sistema precisa tornar disponíveis todos os dados de temperatura para os encadeamentos do consumidor no período de 5 milissegundos. Foornecendo, pelo menos, 99,9% de leituras disponíveis em 5 milissegundos e 99,999% em 200 milissegundos, o sistema é aceitável.

Validando sistemas de tempo real.

Testes simples funcional ou de desempenho de aplicativos de tempo real não são suficientes para provar que atendem aos requisitos deles. Um sistema que funciona corretamente em alguns testes pode decair ou mesmo alterar seu rendimento ao longo do tempo. A gama de possíveis valores para o tempo de execução do agendamento periódico não será exibido a menos que uma amostra muito maior seja tomada em longas execuções.

Por exemplo, a partir dos requisitos de tempo real anterior, sabemos que necessitamos testar, pelo menos, 100.000 vezes antes de começarmos a mostrar que foi atingido o objetivo de 99,999% em 200 milissegundos. Para os requisitos de tempo real complexo, o ônus fica para os desenvolvedores do sistema para fazerem testes suficientes que os permitam se certificarem de que o sistema atenderá os requisitos de tornar disponíveis todos os dados de temperatura aos encadeamentos do consumidor em 5 milissegundos. Isso será geralmente feito por testes prolongados combinados com uma análise estatística da distribuição dos dados de desempenho. As ferramentas existem para criar curvas de distribuição bem ajustadas às amostras de dados e os dados modelados podem ser utilizados para prever o número e a probabilidade de valores atípicos extremos. Em função do fato de geralmente não termos tempos para testar os sistemas por tanto tempo quanto forem utilizados, a aplicação de uma contingência além do valor atípico observado no pior cenário é um procedimento pragmático para definir um sistema certificado.


A implementação do aplicativo demo

Por uma questão de simplicidade, somente consideramos o código utilizado pelos encadeamentos que entregam os dados de temperatura. Os encadeamentos de dados do consumidor não estão descritos em detalhe. Admite-se que sejam executados em processos diferentes no mesmo computador,

Para a implementaçãp inicial, não tentaremos maximizar o determinismo. Ao contrário, utilizaremos a ferramenta de compilação WebSphere Real Time AOT, admincache. A ênfase está nas técnicas e nas ferramentas para identificar as áreas remanescentes de não-determinismo e as formas para resolvê-las.

Utilizamos um processo Reader para verificar o sensor de temperatura do dispositivo (simulado aqui por um gerador aleatório de números) a cada 2 milissegundos. A classe javax.realtime.PeriodicTimer é a solução do RTSJ para realizar regularmente uma ação e Reader utiliza um PeriodicTimer para verificar o sensor de temperatura. Isso foi implementado como um BoundAsyncEventHandler para a latência mais baixa. O Reader constrói um fragmento de XML para cada leitura e o escreve em um soquete de rede conectado a um processo Writer. Se o tempo necessário para o ciclo de leitura do sensor e escrita dos dados excederem 2 milissegundos é relatado um erro.

O processo Writer é executado em um JVM separado no mesmo computador à medida que o Reader processa e grava em um soquete de rede para as leituras de temperatura. O Writer utiliza um javax.realtime.RealtimeThread para se conectar ao soquete de rede para tirar vantagem do modelo de agendamento FIFO e controle de prioridade de ajuste fino. Ele descompacta o fragmento XML, extrai as leituras de temperatura e as escreve em um arquivo de log. Com o objetivo de tornar os dados disponíveis rapidamente aos consumidores, o Writer possui também um tempo limite de 3 milissegundos para escrever todas as medições fora do disco. Se o tempo total entre a leitura de temperatura e a escrita de dados exceder 5 milissegundos será relatado um erro.

A Figura 1 mostra o fluxo de dados no aplicativo.

Figura 1. Fluxo de Dados
Fluxo de Dados

Lembre-se que utilizar XML para uma pequena quantidade de dados em um cenário sensível ao tempo não é uma boa prática de design. O aplicativo demo foi projetado para fornecer um exemplo útil para explorar o desempenho do WebSphere Real Time. Não deve ser tomado como um exemplo de um bom aplicativo de monitoramento remoto de temperatura.

Executando o demo

Se você possuir um ambiente WebSphere Real Time, poderá executar o demo por sua conta.

  1. Descompacte o demo em algum local em sua máquina WebSphere Real Time.
  2. Compile o demo com o comando:
    PATH to WRT/bin/javac -Xrealtime *.java
  3. Inicie o processo, informando o número da porta e o nome do arquivo de log. Por exemplo:
    sdk/jre/bin/java -Xrealtime Writer 8080 readings.txt
  4. Inicie o processo Reader, informando o nome do hospedeiro e o número da porta para o processo Writer. Por exemplo:
    PATH to WRT/jre/bin/java -Xrealtime Reader localhost 8080

O rendimento do demo

O código relata os atrasos em todos os encadeamentos Reader e Writer. O Writer também relata o tempo total de transferência entre a leitura de temperatura e a gravação no disco. Para atender o objetivo complexo de tempo real, o tempo de transferência não deve (nunca) exceder 5 milissegundos. Para tempo real simples, 99,9% em 5 milissegundos atende a primeira parte do requisito e 99,999% em 200 milissegundos atende ao segundo.

No sistema de teste IBM em que o aplicativo estava sendo executado, os objetivos de tempo real simples foram atendidos, mas houve falhas em relação ao objetivo de tempo real complexo. A próxima seção discutirá algumas ferramentas e técnicas que podem ser utilizadas para pesquisar essas falhas.


Analisando os programas Java de tempo real

Os limites de tempo utilizados na programação de tempo real são geralmente mais curtos que aqueles utilizados na análise de Java normal. Java de tempo real geralmente atua no campo do nanossegundo a milissegundo diferentemente das dezenas de milissegundos e acima para a programação Java normal. Para entender as falhas do aplicativo demo, precisamos trabalhar nos limites entre o microssegundo e poucos milissegundos.

Percebemos que as falhas do demo eram mais comuns durante as primeiras execuções. Esse é um resultado comum nos sistemas de tempo real e em outros sistemas: o mais lento é executado para ser um dos primeiros. Já tratamos de algumas das razões para isso: carregamento de classe e compilação JIT.

Monitorando o carregamento de classe

No nível mais simples, podemos executar o aplicativo com a opção da linha de comando -verbose:class para produzir informação em todos os eventos de carregamento de classe. No entanto, para correlacionar todos os valores atípicos com outras atividades — como o carregamento de classe — precisamos de registros de data e horário precisos nos eventos atípicos e causas suspeitas. Para objetivos gerais, nós podemos escrever uma ferramenta utilizando a um evento de carregamento de classe JVMTI (Java Virtual Machine Tool Interface) (consulte Recursos), mas ainda precisaríamos instrumentar nosso código de aplicativo e correlacionar os eventos de tempo.

Monitorando a atividade de complação JIT

Os compiladores JIT modernos oferecem muitos benefícios de desempenho. Eles são software tecnicamente mais complexos que a maioria dos desenvolvedores os veem como caixas pretas. Com certeza, há uma interação menos explícita nas opções de configuração e menos visibilidade da atividade JIT do que no colector de lixo, por exemplo. No entanto, podemos habilitar um detalhamento JIT por meio das opções de linha de comando e eventos JVMTI relacionados ao JIT estão disponíveis para ferramentas que monitoram a geração de código JIT.

A partir da linha de comando, podemos iniciar o JVM com a flag -Xjit:compiling para ser notificada da compilação do método (e recompilação para níveis de otimização maiores).

Instrumentando o código demo

Vamos supor que habilitamos o verbose:class e o -Xjit:compiling, mas vemos as falhas de tempo do demo bem depois do encerramento do carregamento de classe e depois que o código JIT gerado se estabilizou. Nesse caso, precisamos saber com mais detalhes o que exatamente nosso aplicativo está fazendo em relação à atividade JVM.

Um procedimento é instrumentar o código com registros de ata e horário para identificar os principais atrasos que estão ocorrendo. Uma vantagem da plataforma Java de tempo real é que temos acesso à clocks de alta precisão e de alto desempenho. Podemos adicionar código como o seguinte em nossas áreas suspeitas no código Reader:

AbsoluteTime startTime1 = clock.getTime();
xmlSnippet.append("<reading><sensor id=\"");
AbsoluteTime startTime2 = clock.getTime();
RelativeTime timeTaken = startTime2.subtract(startTime1);
System.err.println("Time taken: " + timeTaken);

Isso pode identificar linhas lentas do código, mas é um processo trabalhoso e iterativo e os dados gerados não estão correlacionados com nenhum outro evento. Se contarmos com a ordem dos eventos na tela a partir dessa saída e escrevermos verbose:gc ou verbose:classloading, podemos ter algum progresso, mas há uma solução muito melhor: Rastreamento Tuning Fork

Rastreamento Tuning Fork

A Plataforma de Visualização Tuning Fork (consulte Recursos) foi originalmente desenvolvida para ajudar no desenvolvimento e na depuração do coletor de lixo Metronome. (É também um plug-in Eclipse extensível com uma aplicação mais ampla, por exemplo, no IBM Toolkit for Data Collection and Visual Analysis for Multi-Core Systems; consulte Recursos).

Vantagens de utilizar o Tuning Fork:

  • Visualização poderosa e recursos de análise
  • Disponibilidade de dados de atividade JVM: do GC, carregamento de classe, compilação JIT e outros componentes
  • Combinação de pontos de monitoramento do aplicativo com dados de rastreio JVM

As mudanças no código necessitam adicionar os pontos de monitoramento Tuning Fork no código fonte fornecido (Reader.java.instrumented e Writer.java.instrumented; consulte Download).

O código é um pouco mais complexo do que o exemplo anterior de escrita de dados de tempo para o fluxo de saída de erro padrão, mas mostraremos os grandes benefícios desse esforço extra. Os pontos de monitoramento Tuning Fork possuem duas variedades: aqueles que registram eventos simples de registro de data e horário e aqueles que registram dados em nome do programador. Ambos os eventos são registrados utilizando os mesmos componentes que dos pontos de rastreio interno JVM para o carregamento de classe, JIT e atividade do GC. Essencialmente, isso assegura que o aplicativo e os dados de rastreio JVM utilizem o mesmo dispositivo de registro de data e hora, — o que significa que podemos correlacionar de forma segura todos os eventos para verificar o que está sendo executado em qualquer ponto. Tentativas de intercalar dados de rastreio de diferentes fontes são geralmente repletas de dificuldade e erros. Os únicos eventos de rastreio Tuning Fork que necessitamos são os eventos de registro de data e hora, que adicionaremos no início e no final das áreas de interesse do código do aplicativo.

O código adicional na fonte é marcado com delimitadores, como neste exemplo:

/*---------------------TF INSTRUMENTATION START ------------------------- */
		writerTimer.start();
/* ---------------------TF INSTRUMENTATION END ------------------------- */

A fonte instrumentada gera um arquivo de rastreio para o JVM Reader (Reader.trace) e um para o JVM Writer (Writer.trace). Esses arquivos binários contém os eventos de início e interrupção para o processamento de todas as mensagens de leitura de temperatura para as análises posteriores com o visualizador Tunning Fork.

O código adicionado no Tuning Fork instrumentou as versões nas seguintes áreas:

  • Declarações de importação para métodos no arquivo de geração de rastreio Tuning Fork, tuningForkTraceGeneration.jar
  • O código de inicialização para um criador de log para escrever e criar um timer e feedlet (uma alimentação de dados entre o timer e o criador de log)
  • Executando a inicialização da instrumentação
  • Um método para vincular o feedlet ao encadeamento atual

O único código adicional necessário é aquele que faz a sincronização:

/*---------------------TF INSTRUMENTATION START ------------------------- */
                    writerTimer.start();
/* ---------------------TF INSTRUMENTATION END ------------------------- */

                    AbsoluteTime startTime = clock.getTime();

                    Code to be timed

                    RelativeTime timeTaken = stopTime.subtract(startTime);

/* ---------------------TF INSTRUMENTATION START ------------------------- */
                    writerTimer.stop();
/* ---------------------TF INSTRUMENTATION END ------------------------- *

Em nosso exemplo, o código de sincronização do Tuning Fork engloba o código de sincronização existente dos programas demo padrão. Dessa forma, temos uma boa combinação entre as sincronizações de:

AbsoluteTime startTime = clock.getTime();
               Code to be timed
      RelativeTime timeTaken = stopTime.subtract(startTime);

Para montar e executar o código com os pontos de rastreio Tuning Fork adicionados, simplesmente necessitamos nos certificar que tuningForkTraceGeneration.jar esteja adicionado ao classpath.

Dados de rastreio JVM Tuning Fork

Instalando o plug-in Eclipse para o WebSphere Real Time

Após o download do Tuning Fork do SourceForge (consulte Recursos) e sua instalação em seu sistema:

  1. Inicie o visualizador.
  2. Selecione Help > Software Upgrades > Search for new features to install > Tuning Fork extensions for WebSphere Real Time > New Remote Site.
  3. Digite um nome e a URL http://awwebx04.alphaworks.ibm.com/
    ettktechnologies/updates
    .
  4. Quando o Search Results retorna uma lista de recursos para instalar, aumente a caixa do WebSphere Real Time e selecione TuningFork Real Time JVM Feature 2.0.0.

Para permitir o log dos dados internos JVM, adicionamos a flag -XXgc:perfTraceLog=filename.trace à linha de comando.

A ferramenta de visualização Tuning Fork é um plug-in Eclipse que pode ser executado em Windows® ou Linux. Para permitir os números prontos para o IBM WebSphere Real Time JVM, necessitamos adicionar um plug-in separado. (A infraestrutura The Tuning Fork é uma proposta geral que pode ser também utilizada em outros aplicativos Java, C e C++).

A visualização mais útil no Tuning Fork para a execução do programa de compreensão é simplesmente um retrato dos eventos em sequência ao longo do tempo. Uma quantidade de números predefinidos estão disponíveis para utilização com o WebSphere Real Time (consulte Recursos). Eles fornecem visualizações de combinações úteis de dados JVM — por exemplo, o GC Performance Summary.

Adicionamos ao aplicativo demo algumas instrumentações de sincronização Tuning Fork simples. Esse código define simplesmente um temporizador e inicia e interrompe imediatamente antes e depois do código de sincronização existente, que verifica o excesso de processamento dos encadeamentos: 2 milissegundos para o Reader e 3 milissegundos para o Writer. Uma visualização do Tuning Fork de uma pequena seção deste dado é mostrada na Figura 2, confirmando que o código está sendo executando de acordo com o planejado:

Figura 2. Rastreio do Tuning Fork — código do aplicativo demo
Rastreio do Tuning Fork código do aplicativo demo

A Figura 2 mostra que o Reader é executado em cerca de 130 microssegundos e que os dados enviados no soquete ativa o encadeamento do Writer para ser executado em cerca 900 microssegundos (o que é esperado, porque esse segmento tem mais trabalho a fazer). A transferência completa de dados da leitura de temperatura para escrever o arquivo concluída em pouco mais de 1 milissegundo, bem dentro de nosso limite de 5 milissegundos. Podemos também ver que o encadeamento do Reader está regularmente ativo em seu período de 2 milissegundos.

O visualizador Tuning Fork alinha automaticamente a data e hora das duas fontes de dados, de modo que o eixo X de Tempo aplique-se a ambos os encadeamentos.

O que acontece com esse padrão durante um ciclo de GC? Sem o Tuning Fork, tudo o que podemos ver é o ponto de vista do aplicativo, mas agora podemos ver de forma muito mais clara como a pausa de GC afeta nosso aplicativo. A Figura 3 mostra uma visualização da atividade de GC no JVM Writer:

Figure 4. Rastreio Tuning Fork — seções de GC
Figure 4. Rastreio Tuning Fork seções de GC

Aqui o efeito de uma GC pode ser vista na duração do encadeamento do Writer. Cada parte incremental do trabalho feito pelo Metronome — um quantum (ou seção no Tuning Fork) — é executado em cerca de 500 microssegundos. Durante um quantum, todos os encadeamentos do aplicativo no JVM Writer estão pausados de forma que o prazo de execução (geralmente) aumente de 900 microssegundos para 1,4 milissegundos se o quantum ocorreu enquanto o encadeamento do Writer estava sendo executado.

Note que a perturbação total para o encadeamento do Writer será um pouco mais de 500 microssegundos da quantum; um contexto de alternância de excessos de processamento e efeitos potenciais da poluição do processador de cache certamente ocorrerão. Se o encadeamento do Writer foi concluído em um núcleo diferente daquele em que foi executada antes do quantum da GC, ocorrerão maiores custos em caches de núcleos específicos.

O encadeamento do Reader foi executado em um JVM separado e não foi afetado pela atividade de GC na execução do Writer porque o computador tinha quatro núcleos que executou os encadeamentos de GC do JVM Writer e do Reader simultaneamente. (O WebSphere Real Time utiliza um encadeamento de GC por JVM por definição.)

Inspecionando os outros ciclos de GC no JVM Writer, percebemos que uma sincronização atípica excedeu 2 milissegundos. Olhando mais de perto a Figura 4, verifica-se que esses tiveram o azar de serem interrompidos por dois quanta de GC:

Figure 4. Rastreio Tuning Fork — duas seções de GC
Figure 4. Rastreio Tuning Fork duas seções de GC

Essas ocorrências de duplicidade são raras, mas podemos evitá-las juntamente?

Para não ser atingido por dois quanta, a duração do código do aplicativo mais um quantum precisa se ajustar no espaço entre duas quanta de GC, que é de normalmente cerca de um milissegundo. Dessa forma, o Writer teria de reduzir o seu tempo de execução de cerca de 900 microssegundos para menos de 500 microssegundos. No entanto, mesmo essa estratégia nem sempre garante que se evitarão as colisões com os quanta de GC, por várias razões:

  • Há uma ligeira variabilidade quando as pausas de GC estão programadas, devido à forma como o contrato para garantir a manutenção de 70 por cento de utilização do mutador (encadeamentos do aplicativo) é gerenciado.
  • Todos os núcleos de processadores geralmente possuem encadeamentos kernel de alta prioridade conectados a eles, como os as interrupções e os temporizações. Embora eles sejam executados em intervalos muito curtos, podem ter uma prioridade mais elevada do que a do aplicativo ou dos encadeamentos JVM e podem perturbar a sincronização dessas execuções.
  • O JVM tem um encadeamento com mais prioridade do que o usuário ou outros encadeamentos de GC — ou encadeamento de alarme de coleta de lixo, que é executado por poucos microssegundos a cada 450 microssegundos, a fim de gerir frações de tempo de GC. Se o agendamento do sistema operacional conclui no mesmo núcleo que o nosso aplicativo ou encadeamento de GC, ocorrerão pequenos atrasos.

O exame da execução de aplicativo no nível do microssegundo começa a revelar (raras vezes) as interações entre encadeamentos, núcleos e agendamentos. Às vezes, pode ser necessário entendê-los mais plenamente incorporando os dados do sistema operacional. O Tuning Fork também pode importar dados da ferramenta Linux System Tap, mas não vamos discutir isso neste artigo. A IBM Toolkit for Data Collection e Visual Analysis for Multi-Core Systems também podem visualizar esses dados (consulte Recursos).

Mais valores atípicos do Writer

Se você estiver executando o aplicativo demo, provavelmente terá visto uma enxurrada de mensagens do console do Writer, imediatamente após o início do JVM Reader. Iniciaremos com esta mensagem:

       Prazo final do Writer perdido após 0 boas escritas.
  O prazo final era de (3 ms, 0 ns), o período medido foi de (48 ms, 858000 ns)

Esta mensagem relata que na primeira vez em que o Writer foi executado, demorou cerca de 49 milissegundos para gravar um registro; — muito mais tempo do que na última execução que levou menos de 1 milissegundo. Sabemos que a causa do atraso não está relacionada com a atividade JIT de conversão dos bytecodes dos métodos em código nativo, pois executamos o código compilado AOT e o JIT foi desabilitado no momento da execução. A outra área suspeita a ser considerada é o carregamento de classe, porque esse problema acontece na primeira chamada. Podemos confirmar isso com o Tuning Fork? A Figura 5 mostra um gráfico do Tuning Fork da primeira execução do Writer:

Figura 5. Rastreio Tuning Fork — primeira execução e carregamento de classe do Writer
Rastreio Tuning Fork primeira execução e carregamento de classe do Writer

Como suspeitávamos, vemos uma atividade considerável de carregamento de classe imediatamente antes e durante a primeira execução do Writer, uma clara confirmação de que o carregamento de classe (e a inicialização da classe de execução) é a causa da lentidão. As subsequentes execuções do Writer ocorrem na faixa de 1 milissegundo ou menos.

A Parte 2 nesta série discutiu as técnicas para se evitar esses atrasos. Uma das outras formas que as visualizações do Tuning Fork podem nos ajudar a identificar as classes a serem precarregadas é mostrada quando usamos fatores maiores de tempo. Na Figura 6, vemos o que org/apache/xerces/util/XMLChar leva mais de 3 milissegundos para carregar:

Figura 6. Rastreio Tuning Fork — identificando o carregamento lento de classe
Rastreio Tuning Fork identificando o carregamento lento de classe

Embora o nosso aplicativo seja bastante simples, utilizar o processamento XML requer muito mais classes para ser carregado. Por isso, seria importante precarregá-las ou usar uma simulação de execução inicial, antes que o aplicativo inicie sua fase de sincronização crítica.

Valores atípicos de ponta a ponta

Até agora, temos estudado os valores atípicos somente dentro do JVM Writer, mas nosso requisito é para que o processamento de ponta a ponta seja concluído no prazo de 5 milissegundos. Não vemos os relatórios do JVM Reader em que os prazos não foram cumpridos e mesmo a sua primeira iteração é executada em menos de 1 milissegundo antes de estabilizar em cerca de 140 microssegundos. Adicionar a instrumentação Tuning Fork também nos fornece as estatísticas para o evento na Figura 7. (O valor atípico de 3 milissegundos ocorreu quando o JVM foi encerrado com o Control-C e quando ocorreu o carregamento tardio de classes associados com a manipulação de exceção. A Parte 2 discutiu os problemas desses caminhos e técnicas raras para identificar e precarregar as classes necessárias — o simples aquecimento não é suficiente.)

Figure 4. Rastreio Tuning Fork — estatísticas Reader
Figure 4. Rastreio Tuning Fork estatísticas Reader

A questão é que no início do JVM Reader, o JVM Writer relatou grande volume de prazos não cumpridos para a transferência completa dos dados a partir do ponto de leitura do termômetro no JVM Reader para gravá-lo no JVM Writer:

 Prazo final de dados vencido após 0 boas transferências.
 O prazo final era de (5 ms, 0 ns), o tempo de trânsito foi de(122 ms, 93000 ns) 
 prazo final do Writer após 0 boas gravações. O prazo final era de (3 ms, 0 ns), 
 o tempo de trânsito foi de(48 ms, 858000 ns)
 prazo final vencido dos dados após 0 boas transferências. 
 O prazo final era de (5 ms, 0 ns), 
 o tempo de trânsito foi de (122 ms, 517000 ns) 
 prazo final vencido dos dados após 0 boas transferências. 
 O prazo final era de (5 ms, 0 ns), o tempo de trânsito foi de (121 ms, 567000 ns) 
 prazo final vencido dos dados após 0 boas transferências. 
 O prazo final era de (5 ms, 0 ns), o tempo de trânsito foi de (120 ms, 541000 ns)
 prazo final vencido dos dados após 0 boas transferências. 
 O prazo final era de (5 ms, 0 ns), o tempo de trânsito foi de (119 ms, 525000 ns)

Este padrão continuou diminuindo gradualmente com algumas exceções até:

Prazo final de dados vencido após 0 boas transferências. 
O prazo final era de (5 ms, 0 ns), 
o tempo de trânsito foi de (10 ms, 585000 ns) 
prazo final vencido dos dados após 0 boas transferências.
O prazo final era de (5 ms, 0 ns), o tempo de trânsito foi de (9 ms, 588000 ns) 
prazo final vencido dos dados após 0 boas transferências. 
O prazo final era de (5 ms, 0 ns), o tempo de trânsito foi de (8 ms, 531000 ns) 
prazo final vencido dos dados após 0 boas transferências. 
O prazo final era de (5 ms, 0 ns), o tempo de trânsito foi de (7 ms, 469000 ns) 
prazo final vencido dos dados após 0 boas transferências. 
O prazo final era de (5 ms, 0 ns), o tempo de trânsito foi de (6 ms, 398000 ns) 
prazo final vencido dos dados após 0 boas transferências. 
O prazo final era de (5 ms, 0 ns), o tempo de trânsito foi de(5 ms, 518000 ns) 
prazo final do Writer após 3087 boas gravações.
O prazo final era de (3 ms, 0 ns), o período medido foi de (3 ms, 316000 ns)

Portanto, temos um grande atraso inicial de 122 milissegundos que diminui progressivamente, chegando finalmente a um estado onde os prazos finais do Writer ou do Reader são apenas ocasionalmente perdidos. A Figura 8 mostra um gráfico da transferência de dados em momento de inicialização:

Figura 8. Inicialização de transferência de dados
Inicialização de transferência de dados

Além dos 48 primeiros milissegundos de duração do Writer, não houve relatos de superação do Reader ou do Writer durante as primeiras ~120 transferência lentas de dados — então, onde está o atraso? Mais uma vez, nossos rastreios Tuning Fork podem ajudar, desta vez, combinando os dados de encadeamentos JVMs e do aplicativo como foi mostrado na Figura 9. Podemos mostrar que nenhuma atividade de GC foi iniciada em qualquer JVM pela acréscimo de -verbose:gc à linha de comando, mas o carregamento de classe poderia ser o responsável novamente?

Figura 9. Rastreio Tuning Fork — em encadeamentos JVMs e de aplicativo
Rastreio Tuning Fork em encadeamentos JVMs e de aplicativo

Na Figura 9, o carregamento de classe no JVM Reader está na terceira linha. Como era esperado, ele encerrou na primeira execução do Reader. A linha inferior é o carregamento de classe no Writer. Ao passar o ponteiro do mouse sobre as barras na seção entre 20 e 70 milissegundos sobre o eixo X, vemos que quase todos as barras têm ligação com o XML. Mais uma vez, os atrasos do carregamento de classe estão contribuindo com uma parcela considerável do nosso tempo de transferência de dados. O nosso atraso mais longo de 122 milissegundos é a diferença entre a primeira barra vermelha para o temporizador do Reader e o final da primeira barra verde do Writer (marcado com 49,88 ms). A segunda transferência é ligeiramente mais rápida e, assim, a tendência continua à medida que o Writer executa plano de trabalho por meio da lista não-processada de pedidos de entrada, até que o acúmulo seja resolvido e as transferências de dados ocorram dentro de 5 milissegundos. Isso explica o padrão de prazos não cumpridos para transferência de dados na inicialização. Mas o carregamento de classe é o único fator? A transmissão de dados entre os JVMs do Reader e do Writer pelo soquete poderia estar contribuindo?

Latência do soquete

A utilização de um soquete para conectar os JVMs, que permite que o aplicativo se divida entre dois computadores, pode causar atrasos. Fizemos as seguintes alterações no código do aplicativo do Reader e do Writer para ver qual teve qualquer efeito:

  • Desativando o Nagle: O algoritmo Nagle (consulte Recursos) é bastante conhecido por causar atrasos em sistemas de tempo real porque armazena pacotes na rede antes de enviá-los. A configuração do Nagle pode ser testada por aplicativos Java com socket.getTcpNoDelay() e, se for encontrado, desabilitado pela configuração setTcpNoDelay(true). No entanto, desabilitar o Nagle não afeta as lentas transferências de inicialização.
  • Utilizando PerformancePreferences: O soquete padrão pode ser modificado para se comportar de forma mais semelhante aos nossos requisitos utilizando setPerformancePreferences(1, 2, 0). Isso dá uma maior importância à baixa latência, seguida pelo tempo curto de conexão e menos importância à alta largura de banda. O acréscimo desse código demo pode reduzir significativamente o atraso de inicialização — consulte a Figura 10:
    Figura 10. Rastreio Tuning Fork — com soquete PerformancePreferences
    Rastreio Tuning Fork com soquete PerformancePreferences
    O principal atraso está agora reduzido a ~40 milissegundos causado pelo carregamento de classe que poderia ser eliminado pelo precarregamento daquelas classes.

Conclusão

Este artigo conclui a série Developing with real-time Java. Como toda a série tem enfatizado, a previsibilidade é a primeira prioridade para um aplicativo de tempo real. Neste artigo, concebemos e escrevemos um aplicativo Java de tempo real e mostramos como utilizar uma combinação de temporizadores e ferramentas para analisar sua execução e validar o quanto previsível é sua execução. Ao longo do caminho, explicamos a interação das pausas de GC (coleta de lixo) e um aplicativo em execução; mostramos como o carregamento de classe pode causar atrasos e observamos o impacto da utilização de XML em um aplicativo simples. E identificamos a importância da análise de ponta a ponta dos sistemas conectados e detectamos e reduzimos as latências causadas pelos soquetes de rede.

Os dados de desempenho que relatamos neste artigo foram determinados em um ambiente controlado. Os resultados obtidos em outros ambientes operacionais podem variar significativamente. Por isso, você deve verificar os dados aplicáveis para o seu ambiente específico. Observe também que os aplicativos distribuídos fisicamente estão sujeitos a uma vasta gama de fontes de variabilidade (consulte Recursos). Reduzimos ou evitamos algumas delas, executando o aplicativo demo em um único computador. Você pode modificar alguns dos parâmetros Java de ajuste de rede que apresentamos como sugestões.


Download

DescriçãoNomeTamanho
Source codej-rtjdev3.tar.gz5KB

Recursos

Aprender

Obter produtos e tecnologias

Discutir

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Tecnologia Java
ArticleID=446230
ArticleTitle=Desenvolvendo com o Java de tempo real, Parte 3: Escreva, valide e analise com um aplicativo Java de tempo real
publish-date=11112009