Ás veces máis é menos. Cando se reduce a carga, aumenta a latencia

Como dentro a maioría das publicacións, hai un problema cun servizo distribuído, chamemos a este servizo Alvin. Esta vez non descubrín eu o problema, informáronme os mozos do lado do cliente.

Un día espertei cun correo electrónico descontento debido aos longos atrasos con Alvin, que planeabamos lanzar nun futuro próximo. En concreto, o cliente experimentou unha latencia do percentil 99 na rexión de 50 ms, moi por riba do noso orzamento de latencia. Isto foi sorprendente xa que probei o servizo extensamente, especialmente na latencia, que é unha queixa común.

Antes de probar a Alvin, realicei moitos experimentos con 40 consultas por segundo (QPS), todos mostrando unha latencia inferior a 10 ms. Estaba preparado para declarar que non estaba de acordo cos seus resultados. Pero botando unha nova ollada á carta, notei algo novo: non probara exactamente as condicións que mencionaban, o seu QPS era moito máis baixo que o meu. Probei a 40k QPS, pero só a 1k. Fixen outro experimento, esta vez cun QPS máis baixo, só para calmalos.

Xa que estou blogueando sobre isto, probablemente xa te decataches de que os seus números eran correctos. Probei o meu cliente virtual unha e outra vez, co mesmo resultado: un número baixo de solicitudes non só aumenta a latencia, senón que aumenta o número de solicitudes cunha latencia superior a 10 ms. Noutras palabras, se a 40k QPS unhas 50 solicitudes por segundo superaban os 50 ms, entón a 1k QPS había 100 solicitudes superiores a 50 ms cada segundo. Paradoxo!

Ás veces máis é menos. Cando se reduce a carga, aumenta a latencia

Reducindo a busca

Cando se enfronta a un problema de latencia nun sistema distribuído con moitos compoñentes, o primeiro paso é crear unha pequena lista de sospeitosos. Afondemos un pouco máis na arquitectura de Alvin:

Ás veces máis é menos. Cando se reduce a carga, aumenta a latencia

Un bo punto de partida é unha lista de transicións de E/S completadas (chamadas de rede/buscas de discos, etc.). Imos tentar descubrir onde está o atraso. Ademais da evidente E/S co cliente, Alvin dá un paso extra: accede ao almacén de datos. Non obstante, este almacenamento funciona no mesmo clúster que Alvin, polo que a latencia debería ser inferior á do cliente. Entón, a lista de sospeitosos:

  1. Chamada de rede do cliente a Alvin.
  2. Chamada de rede de Alvin ao almacén de datos.
  3. Busca no disco no almacén de datos.
  4. Chamada de rede do almacén de datos a Alvin.
  5. Chamada de rede de Alvin a un cliente.

Intentemos tachar algúns puntos.

O almacenamento de datos non ten nada que ver con iso

O primeiro que fixen foi converter Alvin nun servidor de ping-ping que non procesa solicitudes. Cando recibe unha solicitude, devolve unha resposta baleira. Se a latencia diminúe, entón un erro na implementación de Alvin ou do almacén de datos non é nada inaudito. No primeiro experimento obtemos a seguinte gráfica:

Ás veces máis é menos. Cando se reduce a carga, aumenta a latencia

Como podes ver, non hai ningunha mellora ao usar o servidor ping-ping. Isto significa que o almacén de datos non aumenta a latencia e a lista de sospeitosos redúcese á metade:

  1. Chamada de rede do cliente a Alvin.
  2. Chamada de rede de Alvin a un cliente.

Genial! A lista vaise reducindo rapidamente. Pensei que case descubrira o motivo.

gRPC

Agora é o momento de presentarche un novo xogador: gRPC. Esta é unha biblioteca de código aberto de Google para a comunicación en proceso CPR... Aínda que gRPC ben optimizado e moi utilizado, esta era a primeira vez que o usaba nun sistema deste tamaño e esperaba que a miña implementación fose subóptima, cando menos.

dispoñibilidade gRPC na pila deu lugar a unha nova pregunta: quizais sexa a miña implementación ou eu mesmo gRPC está causando un problema de latencia? Engadindo un novo sospeitoso á lista:

  1. O cliente chama á biblioteca gRPC
  2. biblioteca gRPC fai unha chamada de rede á biblioteca do cliente gRPC no servidor
  3. biblioteca gRPC contactos con Alvin (sen operación en caso de servidor de ping-pong)

Para que che fagas unha idea de como é o código, a miña implementación cliente/Alvin non é moi diferente á cliente-servidor exemplos asíncronos.

Nota: a lista anterior está un pouco simplificada porque gRPC fai posible utilizar o seu propio modelo de threading (modelo?), no que a pila de execución está entrelazada gRPC e implementación de usuarios. En aras da sinxeleza, seguiremos este modelo.

A creación de perfiles arranxará todo

Despois de tachar os almacéns de datos, pensei que estaba case rematado: "Agora é fácil! Apliquemos o perfil e descubramos onde se produce o atraso". eu gran fan do perfilado de precisión, porque as CPU son moi rápidas e a maioría das veces non son o pescozo de botella. A maioría dos atrasos ocorren cando o procesador debe deixar de procesar para facer outra cousa. O perfil preciso da CPU fai precisamente iso: rexistra todo con precisión cambios de contexto e deixa claro onde se producen atrasos.

Tomei catro perfís: con QPS alto (baixa latencia) e cun servidor de ping-pong con QPS baixo (alta latencia), tanto no lado do cliente como no do servidor. E por se acaso, tamén tomei un perfil de procesador de mostra. Ao comparar perfís, adoito buscar unha pila de chamadas anómala. Por exemplo, no lado malo con alta latencia hai moitos máis cambios de contexto (10 veces ou máis). Pero no meu caso, o número de cambios de contexto era case o mesmo. Para o meu horror, non había nada significativo alí.

Depuración adicional

Estaba desesperado. Non sabía que outras ferramentas podía usar, e o meu seguinte plan era esencialmente repetir os experimentos con diferentes variacións en lugar de diagnosticar claramente o problema.

E se

Desde o principio, preocupoume a latencia específica de 50 ms. Este é un momento moi grande. Decidín cortar anacos do código ata que puidese descubrir exactamente que parte estaba a causar este erro. Despois veu un experimento que funcionou.

Como é habitual, coa retrospectiva parece que todo era obvio. Coloquei o cliente na mesma máquina que Alvin e enviei unha solicitude a localhost. E o aumento da latencia desapareceu!

Ás veces máis é menos. Cando se reduce a carga, aumenta a latencia

Produciuse un erro coa rede.

Adquirir habilidades de enxeñeiro de redes

Debo recoñecer: o meu coñecemento das tecnoloxías de rede é terrible, sobre todo tendo en conta o feito de que traballo con elas todos os días. Pero a rede era o principal sospeitoso e necesitaba aprender a depurala.

Afortunadamente, Internet adora os que queren aprender. A combinación de ping e tracert parecía un bo comezo para depurar problemas de transporte de rede.

En primeiro lugar, lancei PsPing ao porto TCP de Alvin. Usei a configuración predeterminada, nada especial. De máis de mil pings, ningún superou os 10 ms, agás o primeiro de quecemento. Isto é contrario ao aumento observado da latencia de 50 ms no percentil 99: alí, por cada 100 solicitudes, deberíamos ter visto preto dunha solicitude cunha latencia de 50 ms.

Despois tenteino trazo: pode haber un problema nalgún dos nodos da ruta entre Alvin e o cliente. Pero o rastreador tamén regresou coas mans baleiras.

Polo tanto, non foi o meu código, a implementación de gRPC ou a rede o que estaba a causar o atraso. Comezaba a preocuparme de que nunca entendería isto.

Agora en que sistema operativo estamos

gRPC moi usado en Linux, pero exótico en Windows. Decidín probar un experimento, que funcionou: creei unha máquina virtual Linux, compilei Alvin para Linux e despregueino.

Ás veces máis é menos. Cando se reduce a carga, aumenta a latencia

E aquí está o que pasou: o servidor de ping-pong Linux non tivo os mesmos atrasos que un servidor similar de Windows, aínda que a fonte de datos non era diferente. Resulta que o problema está na implementación de gRPC para Windows.

Algoritmo de Nagle

Durante todo este tempo pensei que me faltaba unha bandeira gRPC. Agora entendo o que é realmente gRPC Falta a bandeira de Windows. Atopei unha biblioteca RPC interna na que estaba seguro de que funcionaría ben para todas as marcas establecidas Winsock. Despois engadín todas estas bandeiras a gRPC e despreguei Alvin en Windows, nun servidor de ping-pong de Windows parcheado.

Ás veces máis é menos. Cando se reduce a carga, aumenta a latencia

Case Feito: comecei a eliminar as bandeiras engadidas unha a unha ata que volveu a regresión para poder identificar a causa. Foi infame TCP_NODELAY, interruptor de algoritmo de Nagle.

Algoritmo de Nagle tenta reducir o número de paquetes enviados a través dunha rede atrasando a transmisión de mensaxes ata que o tamaño do paquete supere un determinado número de bytes. Aínda que isto pode ser bo para o usuario medio, é destrutivo para os servidores en tempo real xa que o sistema operativo atrasará algunhas mensaxes, causando atrasos en QPS baixo. U gRPC esta marca estableceuse na implementación de Linux para sockets TCP, pero non en Windows. Eu son isto corrixido.

Conclusión

A maior latencia a baixo QPS foi causada pola optimización do SO. En retrospectiva, a creación de perfiles non detectou a latencia porque se fixo no modo de núcleo en lugar de en modo de usuario. Non sei se o algoritmo de Nagle se pode observar mediante capturas ETW, pero sería interesante.

En canto ao experimento localhost, probablemente non tocou o código de rede real e o algoritmo de Nagle non se executou, polo que os problemas de latencia desapareceron cando o cliente chegou a Alvin a través de localhost.

A próxima vez que vexas un aumento da latencia a medida que diminúe o número de solicitudes por segundo, o algoritmo de Nagle debería estar na túa lista de sospeitosos.

Fonte: www.habr.com

Engadir un comentario