Você já se deparou com situações onde o tempo de resposta de sua API é impactado pelo tempo de resposta de outras API’s consultadas? Ou de operações independentes realizadas de forma síncrona e sequencial? Se sim você já sofreu com um problema conhecido como Tail latency amplification.
Deixo a explicação formal para livros como Designing Data Intensive Applications mas o parágrafo anterior deixa transparecer um pouco o que é o problema.
Tail Latency Amplification na prática
Ao realizar de forma síncrona e sequencial diversas requisições independentes o tempo de resposta de sua API será, no mínimo, a soma dos tempos de resposta das requisições realizadas.
Portanto se você realizar 3 requisições e cada uma delas responde em 150ms a sua resposta levará no mínimo 450ms (150×3). Agora imagine que um dos serviços em questão está sofrendo instabilidade, tendo tempo de resposta de 400ms. Nesse caso sua API levará no mínimo 700ms (150×2 + 400) para responder.
Considerando que não é possível interferir nos serviços de terceiros, o que podemos fazer para minimizar o problema é mudar a forma como nossa aplicação realiza tais requisições.
Não seria ótimo se pudéssemos buscar um tempo de resposta mais próximo do maior tempo de resposta entre as requisições realizadas? No cenário 1 (3 requisições de 150ms) nossa API responderia em torno de 150ms. No cenário 2 (2 requisições de 150ms e 1 de 400ms) nosso tempo de resposta seria em torno de 400ms.
Como faríamos isso? Paralelizando as requisições.
Java 8 e CompletableFuture
A classe CompletableFuture foi introduzida no Java 8 junto com melhorias na API de concorrência. Através dela é possível executar tarefas independentes de forma assíncrona e paralela.
Para simular requisições custosas iremos criar 3 métodos que serão executados, em um primeiro momento, de forma síncrona e sequencial. Cada método terá um Thread.sleep(x) visando simular o tempo de resposta do servidor. Caso deseje altere os valores para observar o comportamento da aplicação.
public class ThirdPartyService {
public String getHello() {
try {
Thread.sleep(150); // suspende por 150ms
} catch (InterruptedException e) {
e.printStackTrace(); // ignora exception
}
return "Hello";
}
public String getWorld() {
try {
Thread.sleep(150); // suspende por 150ms
} catch (InterruptedException e) {
e.printStackTrace(); // ignora exception
}
return " World";
}
public String getMyFriend() {
try {
Thread.sleep(400); // suspende por 400ms
} catch (InterruptedException e) {
e.printStackTrace(); // ignora exception
}
return " My Friend";
}
}
Enter fullscreen mode Exit fullscreen mode
Para manter a simplicidade estamos ignorando as exceções. Isso não deve ser realizado em código que for utilizado em produção!
A primeira versão do nosso client realiza as requisições de forma síncrona e sequencial:
class SyncClient {
public static void main(String[] args) {
ThirdPartyService service = new ThirdPartyService(); //instancia do serviço
StringBuilder builder = new StringBuilder(); //utilizado para consolidar o resultado
Long curTime = System.nanoTime();
//Realizando requisições ao serviço
builder.append(service.getHello());
builder.append(service.getWorld());
builder.append(service.getMyFriend());
System.out.println(builder.toString()); //impressão do resultado "Hello World My Friend"
System.out.println( (double) (System.nanoTime() - curTime) / 1_000_000_000 );
}
}
Enter fullscreen mode Exit fullscreen mode
Ao executar o código acima será possível observar que o tempo de execução nunca será menor que a soma dos tempos de cada requisição. Ou seja, o tempo de execução será no mínimo 150 + 150 + 400 = 700 ms. A imagem abaixo detalha a sequência de eventos:
Vamos agora criar a segunda versão do nosso client que executará as requisições de forma assíncrona e em paralelo conforme abaixo:
class AsyncClient {
public static void main(String[] args) {
ThirdPartyService service = new ThirdPartyService(); // instancia do serviço
ExecutorService executor = Executors.newCachedThreadPool(); // 1 - Obtém um pool de threads para execução de tarefas assíncronas
Long curTime = System.nanoTime();
CompletableFuture<String> hello = CompletableFuture.supplyAsync(() -> service.getHello(), executor); // 2 - Executa a tarefa de forma assíncrona no pool
CompletableFuture<String> world = CompletableFuture.supplyAsync(() -> service.getWorld(), executor);
CompletableFuture<String> myFriend = CompletableFuture.supplyAsync(() -> service.getMyFriend(), executor);
CompletableFuture.allOf(hello, world, myFriend).join(); // 3 - Aguarda a finalização da execução de todas as requisições
StringBuilder builder = new StringBuilder();
builder.append(hello.join()); // 4 - Obtem o valor retornado
builder.append(world.join());
builder.append(myFriend.join());
System.out.println(builder.toString()); // impressão do resultado "Hello World My Friend"
System.out.println( (double) (System.nanoTime() - curTime) / 1_000_000_000 );
executor.shutdownNow();
}
}
Enter fullscreen mode Exit fullscreen mode
Ao executar o novo client será possível observar que o tempo de execução será próximo ao da requisição mais longa, que no caso é 400ms do método getMyFriend(). Abaixo a nova sequência de eventos:
Pontos de atenção
1 – Entender os diferentes tipos de Thread Pools, seus funcionamentos e impactos é importante para usar o mais adequado ao seu cenário. Aqui usamos Cache Thread Pool que é útil para tarefas assíncronas curtas (short-lived asynchronous tasks). A classe Executors possui diversos métodos estáticos que fornecem thread pools pré-definidos para diversas situações.
2 – CompletableFuture.supplyAsync Retorna um CompletableFuture que será executado de forma assíncrona e fornecerá o resultado da tarefa quando solicitado (ver item 4).
3 – CompletableFuture.allOf Retorna um CompletableFuture que é completado quando todos os CompletableFutures passados também forem.
4 – CompletableFuture.get() e CompletableFuture.join() são operações “blocantes”, ou seja, interrompem a execução do programa até obterem o retorno desejado. A diferença principal entre elas é que join() lança unchecked exceptions. Para operações assíncronas que retornam valor você terá que em algum momento usar get() ou join().
暂无评论内容