Ao implementar o back-end de aplicativos da Web e aplicativos móveis, mesmo os mais simples, tornou-se habitual usar ferramentas como: bancos de dados, servidor de correio (smtp), servidor redis. O conjunto de ferramentas utilizadas está em constante expansão. Por exemplo, filas de mensagens, a julgar pelo número de instalações do pacote
amqplib (650 mil instalações por semana), são usadas junto com os bancos de dados relacionais (pacote mysql 460 mil instalações por semana e pág. 800 mil instalações por semana).
Hoje eu quero falar sobre filas de empregos, que até agora são usadas em uma ordem de magnitude menor, embora a necessidade delas surja, em quase todos os projetos reais
Portanto, as filas de tarefas permitem que você execute alguma tarefa de forma assíncrona; de fato, execute uma função com os parâmetros de entrada fornecidos e no tempo definido.
Dependendo dos parâmetros, a tarefa pode ser executada:
- imediatamente após adicionar à fila de trabalhos;
- uma vez em um horário definido;
- muitas vezes dentro do cronograma.
As filas de tarefas permitem transferir parâmetros para uma tarefa que está sendo executada, rastrear e reexecutar tarefas que falharam e definir um limite para o número de tarefas que estão sendo executadas simultaneamente.
A grande maioria dos aplicativos no Node.js está associada ao desenvolvimento de uma API REST para aplicativos da web e móveis. Reduzir o tempo de execução da API REST é importante para um trabalho confortável do usuário com o aplicativo. Ao mesmo tempo, uma chamada para a API REST pode iniciar operações demoradas e / ou intensivas em recursos. Por exemplo, depois de fazer uma compra, você deve enviar ao usuário uma mensagem push para o aplicativo móvel ou enviar uma solicitação para fazer uma compra na API REST do CRM. Essas consultas podem ser executadas de forma assíncrona. Como fazer isso corretamente se você não possui uma ferramenta para trabalhar com filas de trabalhos? Por exemplo, você pode enviar uma mensagem para a fila de mensagens, iniciar um trabalhador que leia essas mensagens e realize o trabalho necessário com base nessas mensagens.
De fato, é isso que as filas de trabalho fazem. No entanto, se você observar atentamente, as filas de tarefas têm várias diferenças fundamentais da fila de mensagens. Em primeiro lugar, as mensagens (estáticas) são colocadas na fila de mensagens e as filas de trabalhos envolvem algum tipo de trabalho (chamada de função). Em segundo lugar, a fila de tarefas implica a presença de algum processador (trabalhador) que executará o trabalho especificado. Nesse caso, é necessária funcionalidade adicional. O número de processadores do processador deve ser dimensionado de forma transparente em caso de aumento de carga. Por outro lado, é necessário limitar o número de tarefas em execução simultâneas em um trabalhador do processador, a fim de suavizar as cargas de pico e impedir a negação de serviço. Isso mostra que é necessária uma ferramenta que possa executar tarefas assíncronas definindo vários parâmetros, tão fácil quanto fazer uma solicitação usando a API REST (ou melhor, se for ainda mais fácil).
Usando filas de mensagens, é relativamente simples implementar uma fila de tarefas que é executada imediatamente após a fila de tarefas. Mas muitas vezes é necessário concluir a tarefa uma vez em um horário definido ou de acordo com um cronograma. Para essas tarefas, vários pacotes são amplamente utilizados que implementam a lógica cron no linux. Para não ser infundado, direi que o pacote node-cron possui 480 mil instalações por semana, agendamento de nós - 170 mil instalações por semana.
Usar o node-cron é, é claro, mais conveniente que o ascetic setInterval (), mas pessoalmente, encontrei vários problemas ao usá-lo. Se para expressar uma desvantagem geral, essa é a falta de controle sobre o número de tarefas executadas simultaneamente (isso estimula o pico de cargas: aumentar a carga diminui o trabalho das tarefas, diminuir as tarefas aumenta o número de tarefas executadas simultaneamente, que por sua vez carrega o sistema ainda mais), a incapacidade de executar o nó para aumentar a produtividade -cron em vários núcleos (nesse caso, todas as tarefas são executadas independentemente em cada núcleo) e a falta de ferramentas para rastrear e reiniciar tarefas que foram concluídas Xia com um erro.
Espero ter demonstrado que a necessidade de uma ferramenta como a fila de tarefas está em pé de igualdade com ferramentas como bancos de dados. E esses fundos apareceram, embora ainda não sejam amplamente utilizados. Vou listar os mais populares deles:
Hoje vou considerar o uso do pacote bull, com o qual trabalho comigo. Por que eu escolhi esse pacote específico (embora eu não imponha minha escolha a outras pessoas). Nesse momento, quando comecei a procurar uma implementação conveniente da fila de mensagens, o projeto da fila de abelhas já estava parado. A implementação do kue, de acordo com os parâmetros de referência fornecidos no repositório de filas de abelhas, ficou muito atrás de outras implementações e, além disso, não continha os meios para executar tarefas executadas periodicamente. O projeto da agenda implementa filas com armazenamento no banco de dados mongodb. Essa é uma grande vantagem para alguns casos, se você precisar de super confiabilidade ao colocar tarefas na fila. No entanto, isso não é apenas um fator decisivo. Naturalmente, testei todas as opções de resistência da biblioteca, gerando um grande número de tarefas na fila e ainda não consegui obter trabalho ininterrupto da agenda. Ao exceder um certo número de tarefas, a agenda parou e parou de colocar as tarefas em funcionamento.
Portanto, decidi pelo bull que implementa uma API conveniente, com velocidade e escalabilidade suficientes, pois o pacote bull usa um servidor redis como back-end. Em particular, você pode usar um cluster de servidores redis.
Ao criar uma fila, é muito importante selecionar os parâmetros ideais para a fila de trabalhos. Existem muitos parâmetros, e o valor de alguns deles não me alcançou imediatamente. Após inúmeras experiências, decidi pelos seguintes parâmetros:
const Bull = require('bull'); const redis = { host: 'localhost', port: 6379, maxRetriesPerRequest: null, connectTimeout: 180000 }; const defaultJobOptions = { removeOnComplete: true, removeOnFail: false, }; const limiter = { max: 10000, duration: 1000, bounceBack: false, }; const settings = { lockDuration: 600000,
Em casos triviais, não há necessidade de criar muitas filas, pois em cada fila você pode especificar nomes para tarefas diferentes e associar um trabalhador do processador a cada nome:
const { bull } = require('../bull'); bull.process('push:news', 1, `${__dirname}/push-news.js`); bull.process('push:status', 2, `${__dirname}/push-status.js`); ... bull.process('some:job', function(...args) { ... });
Eu uso a oportunidade que está na ponta dos dedos - fora da caixa - para paralelizar os trabalhadores do processador em vários núcleos. Para fazer isso, o segundo parâmetro define o número de núcleos nos quais o processador-trabalhador será iniciado e, no terceiro parâmetro, o nome do arquivo com a definição da função de processamento de tarefas. Se esse recurso não for necessário, você pode simplesmente passar uma função de retorno de chamada como o segundo parâmetro.
A tarefa é enfileirada por uma chamada ao método add (), para a qual o nome e o objeto da fila são transmitidos nos parâmetros, que serão posteriormente transmitidos ao manipulador de tarefas. Por exemplo, em um gancho ORM, depois de criar uma entrada com novas notícias, posso enviar assincronamente uma mensagem push para todos os clientes:
afterCreate(instance) { bull.add('push:news', _.pick(instance, 'id', 'title', 'message'), options); }
O manipulador de eventos aceita nos parâmetros o objeto de tarefa com os parâmetros passados para o método add () e a função done (), que devem ser chamados para confirmar que a tarefa está concluída ou para informar que a tarefa terminou com um erro:
const { firebase: { admin } } = require('../firebase'); const { makePayload } = require('./makePayload'); module.exports = (job, done) => { const { id, title, message } = job.data; const data = { id: String(id), type: 'news', }; const payloadRu = makePayload(title.ru, message.ru, data); const payloadEn = makePayload(title.en, message.en, data); return Promise.all([ admin.messaging().send({ ...payloadRu, condition: "'news' in topics && 'ru' in topics" }), admin.messaging().send({ ...payloadEn, condition: "'news' in topics && 'en' in topics" }), ]) .then(response => done(null, response)) .catch(done); };
Para visualizar o status da fila de trabalhos, você pode usar a ferramenta arena-bull:
const Arena = require('bull-arena'); const redis = { host: 'localhost', port: 6379, maxRetriesPerRequest: null, connectTimeout: 180000 }; const arena = Arena({ queues: [ { name: 'my_gueue', hostId: 'My Queue', redis, }, ], }, { basePath: '/', disableListen: true, }); module.exports = { arena };
E, finalmente, um pequeno truque de vida. Como eu disse, o bull usa um servidor redis como back-end. Quando o servidor redis é reiniciado, a probabilidade de o trabalho desaparecer é muito pequena. Mas, sabendo que os administradores de sistema às vezes podem simplesmente "limpar o cache de rabanete", enquanto excluí todas as tarefas em particular, eu me preocupava principalmente com a execução periódica de tarefas, que nesse caso paravam para sempre. A esse respeito, encontrei a oportunidade de retomar essas tarefas periódicas:
const cron = '*/10 * * * * *'; const { bull } = require('./app/services/bull'); bull.getRepeatableJobs() .then(jobs => Promise.all(_.map(jobs, (job) => { const [name, cron] = job.key.split(/:{2,}/); return bull.removeRepeatable(name, { cron }); }))) .then(() => bull.add('check:status', {}, { priority: 1, repeat: { cron } })); setInterval(() => bull.add('check:status', {}, { priority: 1, repeat: { cron } }), 60000);
Ou seja, a tarefa é excluída primeiro da fila e, em seguida, configurada novamente, e tudo isso (infelizmente) por setInterval (). Na verdade, sem esse truque de vida, eu provavelmente não teria decidido usar tarefas periódicas no bull.
apapacy@gmail.com
3 de julho de 2019