Operações de E/S Escaláveis: Acesso a Webservices
Durante o processamento de uma requisição HTTP ou de uma mensagem Websocket a lógica do algoritmo pode implicar que dados sejam enviados para ou obtidos de um webservice. Em ambos os casos, o programa servidor passa a atuar como um programa cliente do webservice.
O programa no lado servidor da aplicação deve então usar o protocolo HTTP para enviar ou solicitar dados ao webservice. O problema de escalabilidade ocorrerá caso a execução do algoritmo tenha que ser suspensa enquanto o ciclo requisição-processamento-resposta não for completado pelo webservice.
Considere, como exemplo, o seguinte código JavaScript que é executado pelo programa servidor para atender uma requisição.
function processaRequisicao (req, res) {
const id = req.param.id
const url = 'http://funcionarios.ws.com?id=' + id
const respWS = ws.get(url) // invocação do webservice
const jsonFuncionario = obtenhaFuncionario(respWS)
res.json(jsonFuncionario)
}
A função processaRequisicao
tem por objetivo enviar uma resposta HTTP contendo os dados de um funcionário cujo id
foi enviado como parâmetro na requisiço HTTP.
O problema de escalabilidade do algoritmo implementado na função processaRequisicao
está na forma como o webservice é invocado. O comando de atribuição presente na linha
const respWS = ws.get(url)
demorará muito tempo para ser executado. Na lógica do algoritmo, é necessário aguardar a execução da linha de código para se poder obter os dados do funcionário.
Assim, se, por exemplo, a execução do algoritmo contido na função processaRequisicao
demorar 500 ms é razoável afirmar que quase na totalidade deste tempo a CPU não está executando nenhuma instrução. Literalmente, a execução do algoritmo está suspensa (até que a resposta HTTP enviada pelo webservice chegue).
A execução do método get
do objeto ws
está bloqueando a execução do algoritmo contido na função processaRequisicao
.
A solução para o problema é utilizar uma implementação não bloqueante do método get
.
Uma versão não bloqueante poderia ser uma que retornasse um Promise que, em algum momento futuro, conteria a resposta HTTP enviada pelo webservice.
Por exemplo, a versão que utiliza Promise poderia ser
function processaRequisicao (req, res) {
const id = req.param.id
const url = 'http://funcionarios.ws.com?id=' + id
const promRespWS = ws.getAsync(url) // invocação do webservice retornando um objeto da classe Promise
promRespWS
.then(respWS => obtenhaFuncionarioAsync(respWS))
.then(jsonFuncionario => res.json(jsonFuncionario))
}
Nesta versão o tempo de execução do algoritmo contido na função processaRequisicao
é de poucos milisegundos. Com isso, a thread que foi alocada para executar a função ficará rapidamente (poucos milisegundos) liberada para ser usada no processamento de outra requisição.
Como consequência, não é necessário definir um pool muito grande de threads para atender as requisições. Alguns frameworks para desenvolvimento web definem que o tamanho do pool seja igual ao número de CPUs do servidor. Outros, como é o caso do Node, define uma única thread para atender todas as requisições. Em ambos os casos, o mais importante é não utilizar bibliotecas que façam com que a execução de um algoritmo fique suspensa aguardando a operação de E/S.