Integre o Envoy com nossa biblioteca RPC usando ganchos assíncronos (2)

O artigo anterior continua, e o livro continua do capítulo anterior. Da última vez , falamos sobre como podemos usar async_hooko contexto de solicitação (Request) (Context) para passar em nosso programa de negócios e como usar o Monkey Patch para definir o contexto. Monkey Patch é uma boa abordagem até certo ponto, mas não é adequado para todas as situações. Por exemplo, no processo de aplicação do Tubi, descobrimos node-grpcque não existe um local particularmente adequado para aplicar um patch. Agora, vamos dar uma olhada em uma solução sem o Monkey Patch .

Com base em truques na pilha de chamadas

Primeiro, vamos ver a estrutura da pilha de chamadas:

A árvore de dependência da pilha de chamadas

Cada nó no diagrama acima representa uma chamada de função. As funções são chamadas uma a uma, de cima para baixo, da esquerda para a direita (percurso em ordem). Na figura, você pode ver que existem duas subárvores (dois tons de azul) no nó raiz. Middleware (Middleware) sempre é executado antes da lógica principal, então você os verá no canto inferior esquerdo.

Se escrevermos um middleware para ser colocado no topo da lista de middleware do serviço HTTP, sua chamada é geralmente semelhante à posição indicada na figura: colocada no canto inferior esquerdo, antes de outras chamadas à direita.

Então, podemos adotar a seguinte estratégia para gerenciar o contexto:

construir uma árvore

Primeiro, adicione um async_hookretorno de chamada. Na implementação do callback, construímos uma árvore das nossas dependências de chamada do programa: uma asyncIdaciona a outra asyncId, assim como na figura acima.

definir contexto

Implemente um novo middleware garantindo que seja o primeiro de todos os middlewares. Sua lógica de implementação é:

  1. asyncIdInformações de contexto necessárias para configurar a execução do middleware .

  2. Para asyncIdtodos os nós ancestrais do middleware, retroceda por sua vez, se não definir nenhuma informação de contexto, copie as informações de contexto definidas pelo middleware.

Fica mais fácil de entender olhando uma foto:

definir contexto

A parte amarela na figura é o nó marcado com informações de contexto. Como você pode ver, a primeira requisição (à esquerda), vai marcar alguns nós que não pertencem à requisição propriamente dita. Isso é aceitável, porque uma vez que uma solicitação marca esses nós, outras solicitações subsequentes irão "respeitar" sua "decisão" e, assim, ver os limites entre si. É fácil provar que apenas o primeiro pedido fará isso.

contexto de consulta

  1. Seja currentIdigual a asyncId(atual asyncId).

  2. Descubra currentIdse o nó pai de possui informações de contexto:

    1. Sim : copie as informações de contexto do nó pai para currentIde retorne as informações de contexto.

    2. Nenhum : deixe currentIdigual ao do nó pai asyncId, continue na etapa 2 até que as informações de contexto sejam encontradas e retorne.

A parte ciano na imagem abaixo é como a consulta funciona:

Como as consultas funcionam

Cada consulta começa com a invocação da consulta e, em seguida, prossegue para seu nó pai e todos os nós ancestrais por sua vez. Depois que a consulta é concluída, todos os nós percorridos são marcados com as informações de contexto corretas. É fácil descobrir que a complexidade de tempo média desse algoritmo é O(1)(o número de consultas recursivas não será maior que a profundidade de chamada do programa).

Vantagem

  • Não há necessidade de se preocupar com qual biblioteca o programa de negócios usa e como aplicar o Monkey Patch.

  • A complexidade computacional é consistente com Monkey Patch.

desvantagem

  • Esta é obviamente uma implementação mais complexa.

  • Essa abordagem não funcionará se a implementação de uma biblioteca executar duas solicitações separadas no mesmo recurso assíncrono. Em nosso caso de uso, nenhum desses casos foi encontrado até agora.

  • O armazenamento de contexto precisa ser gerenciado com cuidado para evitar vazamentos de memória.

  • Se você precisa evitar confusão de contexto, faça-o estritamente:

    • A configuração do contexto deve preceder a consulta.

    • Para um contexto de solicitação, ele só pode ser definido uma vez (caso contrário, as informações originalmente pertencentes à mesma solicitação serão divididas em várias).

a vida não segue o seu caminho

Dissemos que era uma implementação mais complicada, certo? Embora já tenhamos uma implementação em funcionamento, existem alguns problemas que precisam ser resolvidos.

Pergunta 1: O ciclo de vida de async_hook

Quando você vê a API async_hook pela primeira vez, pode ter algumas suposições preconcebidas:

  1. init//   before//   afterserá   destoryexecutado apenas uma vez.

  2. afterO / de um ancestral   destorysempre será executado após o método correspondente do descendente.

No entanto, não é. Menos intuitivamente, é possível não implementar   before/   after. Ao mesmo tempo, o   after/ ancestral destorypode ocorrer antes do descendente.

Para demonstrar isso, veja este código:


o init / antes / execStart do ancestral é estritamente anterior ao do sucessor */ 
const asyncHooks = require('async_hooks'); 
const treeify = require('treeify'); 
const fs = require('fs'); 
deixe timeId = 0; 
deixe initAndNotDestroyed = 0; 
const timeInfo = {};



árvore const = {}; 
const nodeMap = {}; 


função viagem(árvore) { 
  const ret = {}; 
  Object.keys(tree).forEach(key => { 
      const subTree = tree[key]; 
      const info = timeInfo[key]; 
      let newName = key; 
      if (info) { 
          const order = Object.keys(info) 
              .sort ((a, b) => info[a] - info[b]) 
              // .filter(key => key === "init") 
              .map(key => `${key}(${info[ key]})`) 
              .join(', ') 
          newName = `${key}: ${order}`; 
      } 
      ret[newName] = travel(subTree); 
  }); 
  retorno ret; 
}

function init(asyncId, type, triggerAsyncId, resource) { 
function after(asyncId) {
  timeInfo[asyncId] = { 
      init: timeId++ 
  }; 
  const atual = {}; 
  nodeMap[asyncId] = atual; 
  if (!nodeMap[triggerAsyncId]) { 
      const tmp = {}; 
      nodeMap[triggerAsyncId] = tmp; 
      árvore[triggerAsyncId] = tmp; 
  } 
  nodeMap[triggerAsyncId][asyncId] = atual; 
  initAndNotDestroyed++; 
} 
function before (asyncId) { 
  if (!timeInfo[asyncId]) { 
      fs.writeSync(1, `antes, não iniciado: ${asyncId}\n`); 
      retornar; 
  } 
  timeInfo[asyncId].before = timeId++; 
} 
      fs.writeSync(1, `depois, não iniciado: ${asyncId}\n`);
  if (!timeInfo[asyncId]) { 
      return; 
  } 
  timeInfo[asyncId].after = timeId++; 
} 
function destroy(asyncId) { 
  timeInfo[asyncId].destroy = timeId++; 
  initAndNotDestroyed--; 
} 
function setExeStartTime() { 
  timeInfo[asyncHooks.executionAsyncId()].execStart = timeId++; 
} 

function setExecEndTime() { 
  timeInfo[asyncHooks.executionAsyncId()].execEnd = timeId++; 
} 


const asyncHook = asyncHooks.createHook({ init, antes, depois, destruir }); 
asyncHook.enable(); 


const ASYNC = 0; 
const SET_TIMEOUT = 1; 
const PROMESSA = 2; 

const TIPOS = [ASYNC, SET_TIMEOUT, PROMISE];

const sleep = (time) => new Promise(resolve => { 
  setTimeout(() => { 
      resolve(); 
      setExecEndTime(); 
  }, time); 
  setExecEndTime(); 
}); 

função chain(ctx, tipos, primeiro, idx = 0) { 
  if (idx > tipos.comprimento) { 
      return; 
  } 
  const time = (first ? 50 : 0) + 50 * idx; 

  switch(types[idx]) { 
      case ASYNC: 
          (async function foo () { 
              await sleep(time); 
              setExeStartTime(); 
              chain(ctx, types, first, idx + 1); 
              setExecEndTime(); 
          })(); 
          quebrar; 
      caso SET_TIMEOUT:
          setTimeout(function() { 
              setExeStartTime(); 
              chain(ctx, tipos, primeiro, idx + 1); 
              setExecEndTime(); 
          }, time); 
          quebrar; 
      case PROMISE: 
          sleep(time) 
              .then(() => { 
                  setExeStartTime(); 
                  chain(ctx, types, first, idx + 1); 
                  setExecEndTime(); 
              }) 
          break; 
  } 
  setExecEndTime(); 
} 

const tipoUm = []; 
const tipoDois = []; 

for (let i = 0; i < TYPES.length; i++) { 
  for (let j = 0; j < TYPES.length; j++) {
      tipoUm.push(TIPOS[i]); 
      typeTwo.push(TIPOS[j]); 
  }  
}

clearTimeout(setTimeout(() => { 
  console.log("eu nunca logo"); 
}, 100)) 

setTimeout(function() { 
  const ctx = '>>>' 
  // set(ctx); 
  chain(ctx , tipoUm, verdadeiro); 
}, 0); 

setTimeout(function() { 
  const ctx = '<<<' 
  // set(ctx); 
  chain(ctx, typeTwo, false); 
}, 0); 

setTimeout(function() { 
  asyncHook.disable(); 
  console.log(treeify.asTree(travel(tree))); 
}, 10000);

Sua saída:

$ node async_hook_life_cycle_test.js 
└─ 1 
 ├─ 5: init(0), destroy(7) 
 ├─ 6: init(1), destroy(8) 
 ├─ 7: init(2), before(10), execEnd( 18), after(19), destroy(32) 
 │ ├─ 12: init(11) 
 │ │ └─ 16: init(16) 
 │ │ └─ 17: init(17), before(66), execStart(67 ), execEnd(75), after(76) 
 │ │ ├─ 30: init(68) 
 │ │ │ └─ 33: init(72) 
 │ │ │ └─ 34: init(73), antes(104), execStart (105), execEnd(113), after(114) 
 │ │ │ ├─ 43: init(106) 
 │ │ │ │ └─ 46: init(110) 
 │ │ │ │ └─ 47: init(111), antes (138), execStart(139), execEnd(142), after(143)
 │ │ │ │ └─ 51: init(140), before(158), execStart(159), execEnd(162), after(163), destroy(166) │ │ │ │ └─ 56: init(160) 
 , antes(172), execIniciar(173), execEnd(177), depois(178), destruir(193) 
 │ │ │ │ ├─ 57: init(174), antes(196), execIniciar(197), execEnd(204 ), after(205), destroy(218) 
 │ │ │ │ │ ├─ 65: init(198) 
 │ │ │ │ │ │ └─ 68: init(202), before(236), execStart(237), execEnd (243), after(244) 
 │ │ │ │ │ │ ├─ 74: init(238) 
 │ │ │ │ │ │ │ └─ 76: init(241), before(261), execStart(262), execEnd( 269), depois (270)
 │ │ │ │ │ │ │ ├─ 77: init(263)  
 │ │ │ │ │ │ │ │ └─ 80: init(267), antes(279), execStart(280), execEnd(282), depois(283)
 │ │ │ │ │ │ │ ├─ 78: init(264), antes(275), execEnd(276), depois(277), destruir(284) 
 │ │ │ │ │ │ │ └─ 79: init(265), antes(274), depois(278), destruir(285) │ │ │ │ │ │ └─ 75: init( 
 239), antes(252), execEnd(253), after(254), destroy(272) 
 │ │ │ │ │ ├─ 66: init(199), before(222), execEnd(223), after(224), destroy(245) │ │ 
 │ │ │ └─ 67: init(200), antes(221), depois(235), destruir(247) 
 │ │ │ │ └─ 58: init(175), antes(195), depois(209), destruir( 220) 
 │ │ │ ├─ 44: init(107) 
 │ └─ 48: init(115), antes(129), depois(130)
 │ │ │ └─ 45: init(108), antes(123), execEnd(124), depois(125), destruir(145) │ │ ├─ 31: 
 init(69) 
 │ │ │ └─ 35: init( 77), antes(101), depois(102) 
 │ │ └─ 32: init(70), antes(85), execEnd(86), depois(87), destruir(117) │ ├─ 13: init( 
 12 ) 
 │ │ └─ 23: init(30), antes(64), depois(65) 
 │ ├─ 14: init(13), antes(50), execEnd(51), depois(52), destruir(78) 
 │ └─ 15: init(14), antes(49), depois(63), destruir(80) ├─ 8 
 : init(3), antes(34), depois(38), destruir(48) ├─ 
 9 : init(4), antes(20), execEnd(27), depois(28), destruir(33) 
 │ ├─ 18: init(21)  
 │ │ └─ 21: init(25)
 │ │ └─ 22: init(26), antes(41), execStart(42), execEnd(45), after(46)
 │ │ └─ 25: init(43), before(53), execStart(54), execEnd(61), after(62), destroy(79) │ │ ├─ 26: init(55) │ │ 
 │ 
 └─ 29: init(59), antes(89), execStart(90), execEnd(99), depois(100) 
 │ │ │ ├─ 36: init(91) 
 │ │ │ │ └─ 40: init(96) 
 │ │ │ │ └─ 41: init(97), antes(131), execStart(132), execEnd(136), depois(137) 
 │ │ │ │ ├─ 49: init(133), antes(148), execStart (149), execEnd(156), after(157), destroy(165)  
 │ │ │ │ ├─ 52: init(150)
 │ │ │ │ │ │ └─ 55: init(154), before(180), execStart(181), execEnd(189), after( 190) 
 │ │ │ │ │ │ ├─ 59: init(182)
 │ │ │ │ │ │ │ └ └ 62: init (186) 
 │ │ │ │ │ │ │ └ └ 63: init (187), antes (212), ExecStart (213), Execend (216), após (217) 
 │ │ │ │ │ │ │ └─ 69: init(214), before(225), execStart(226), execEnd(233), after(234), destroy(246) │ │ │ │ │ │ │ ├─ 
 70 : init(227) 
 │ │ │ │ │ │ │ │ └─ 73: init(231), before(256), execStart(257), execEnd(259), after(260) │ │ │ │ │ │ │ └ 
 │ │ │ │ │ │ ├─ 60: init(183) 
 │ │ │ │ │ │ │ ├─ 71: init(228), before(249), execEnd(250) , após(251), destruir(271)
 │ │ │ │ │ │ │ └─ 72: init(229), antes(248), depois(255), destruir(273) │ │ │ │ │ │ │ └─ 64: init( 
 │ │ │ │ └─ 42: init(103) , antes(127), depois(128)
 │ │ │ │ │ │ │ └─ 64: init(191), antes(210), depois(211) 
 │ │ │ │ │ │ └─ 61: init(184), antes(206), execEnd(207), after(208), destroy(219) 
 │ │ │ │ │ ├─ 53: init(151), before(169), execEnd(170), after(171), destroy(192) │ │ │ │ │ └─ 
 54 : init(152), antes(168), depois(179), destruir(194) 
 │ │ │ │ └─ 50: init(134), antes(147), depois(164), destruir(167) │ │ 
 │ ├─ 37: init(92) 
 │ │ │ ├─ 38: init(93), antes(120), execEnd(121), depois(122), destruir(144) │ │ │ └─ 39: init(94 
 ) , antes(119), depois(126), destruir(146) 
 83 ), depois(84), destruir(116) 
 │ │ └─ 28: init(57), antes(81), depois(88), destruir(118) 
 │ ├─ 19: init(22) 
 │ │ └─ 24 : init(31), antes(39), depois(40) 
 │ └─ 20: init(23), antes(35), execEnd(36), depois(37), destruir(47) ├─ 10: 
 init( 5), antes(287) 
 └─ 11: init(6), antes(286)

Olhe atentamente e você encontrará:

  1. before/   afteré possível sem (ver 5, 6).

  2. O antepassado destoryserá anterior ao do descendente (ver 7 vs 51, anterior destroy(32)a destroy(166)).

Portanto, se inicializarmos inito contexto no tempo e destorylimpá-lo no tempo, pode não haver maneira de encontrar as informações de contexto correspondentes para a chamada no topo da pilha (o código atualmente em execução, para a árvore na figura acima, é o nó folha).

No entanto, se você não limpar essas informações de contexto, haverá um vazamento de memória.

Mas também descobrimos que duas coisas são certas:

  1. Para a   mesma chamada   init//   before///   é   sequencial.execEndafterdestory

  2. Ancestral   init//   beforeé   execStartanterior ao descendente.

Então, aqui apresentamos o uso da contagem de referência para resolver este problema:

iniciar

As informações de contexto atuais são referenciadas pela execução atual e a contagem de referência do nó pai é aumentada em um.

destruição

  1. Referências claras ao próprio contexto.

  2. Se as informações de contexto atuais não tiverem referências a outros nós descendentes, elas podem ser excluídas; ao mesmo tempo, a referência ao nó pai é excluída.

  3. Veja se o nó pai também precisa desse processamento.

Problema 2: Fora de contexto

Como mencionado anteriormente, fora de contexto só acontecerá quando:

  1. Consultar o contexto acontece antes de configurá-lo.

  2. O contexto foi definido várias vezes para a mesma solicitação.

Pior ainda, não há como detectar essa insanidade. No entanto, se a primeira solicitação for feita corretamente e marcada, as solicitações subsequentes podem detectar os problemas acima das seguintes maneiras:

  • Para Consulta: Se constatar que encontra o contexto definido pela primeira requisição (conforme marcação), então pode inferir que o contexto da requisição atual não está configurado.

  • For Set: Se ele encontrar um contexto definido pela primeira solicitação, deduza que o contexto não foi definido duas vezes na mesma solicitação.

Portanto, para evitar confusão, podemos definir as informações de contexto apenas uma vez, e isso acontece antes da consulta. Isso não é problema para nosso cenário de aplicação. Em outras palavras, desde que seja usado corretamente, não teremos problemas com confusão.

Questão 3: Destory não chama

Esse problema deve ser resolvido atualmente (dezembro de 2018), consulte Github/node#19859 [1] para obter detalhes.

Quando implantamos o código em produção, encontramos alguns vazamentos de memória:

Para comparação, é assim que parecia uma semana atrás:

Para investigar o problema, fizemos um contador:

  • init+1 quando você está  

  • Quando em   destroy-1

Aqui está uma foto de um dia e meio:

Os recursos assíncronos não destruídos aumentam com o tempo (queda devido ao reinício automático do pm2)

Podemos ver que destroynão é initchamado na mesma quantidade que . Quando nos aprofundamos nesse problema, descobrimos que usamos o cliente HTTP configurado com Keep-alive para destroynão causar chamadas. Só será chamado quando a conexão for desconectada   destroy, mas o ambiente de produção tem requisições contínuas, então a conexão normalmente não é desconectada.

Para resolver esse problema, usamos um algoritmo de coleta de lixo geracional simples:

  1. Dois mapas: antigo vs novo.

  2. Para definir, sempre escreva para o novo mapa.

  3. Para obter, verifique se há algum no novo, se não, vá para o antigo para encontrar, se encontrado, copie para o novo.

  4. Dentro de um determinado período, como 5 minutos, exclua pontos antigos, antigos para novos e novos pontos para um mapa vazio.

Como a grande maioria de nossas solicitações termina em segundos, remover dados não utilizados a cada 5 minutos é uma aposta segura. Ao mesmo tempo, esta implementação não resultou em uma diminuição no desempenho.

para concluir

Ao usá-lo   async_hook, é muito mais fácil contar e rastrear. Para mais detalhes, você pode dar uma olhada em nosso projeto open source envoy-node [2] baseado neste algoritmo. Sinta-se à vontade para nos deixar uma mensagem ou fornecer feedback!

  • [1] Problema do nó do Github nº 19859 A reutilização da conexão HTTP leva à não destruição acionada: https://github.com/nodejs/node/issues/19859

  • [2] envoy-node: https://github.com/Tubitv/envoy-node

Sobre o autor

Cheng Yingyu, engenheiro de back-end sênior da equipe Tubi China. Após a formatura, ele trabalhou na Microsoft por três anos, sendo responsável pela pesquisa e desenvolvimento da API do Bing Ad Insights e da plataforma Middle Tier. Juntou-se à recém-criada equipe Tubi China em 2016 e foi responsável por vários projetos importantes, como Adbreak Finder e Clip Pipeline. Agora ele está envolvido principalmente na pesquisa e desenvolvimento de infraestrutura de microsserviços e Ad Server.

O 8º Scala Meetup está com inscrições quentes, seja bem-vindo a participar: Garanta sua vaga|Scala Meetup retorna online! Marcação de Marcação-Tubi TV Eventos-Linha de Eventos 

Acho que você gosta

Origin blog.csdn.net/weixin_49193714/article/details/128410495
Recomendado
Clasificación