6 histórias práticas de nossos dias da semana do SRE



A infraestrutura da web moderna consiste em muitos componentes para diversos fins, com óbvia e não muito interconectada. Isso se torna especialmente evidente quando aplicativos operacionais que usam pilhas de software diferentes, que com o advento dos microsserviços, começaram a ocorrer literalmente a cada passo. Fatores externos (APIs de terceiros, serviços etc.) são adicionados à "diversão" geral, o que complica uma imagem já difícil.

Em geral, mesmo que essas aplicações sejam unidas por idéias e soluções arquitetônicas comuns, para eliminar problemas incomuns, muitas vezes precisam percorrer as próximas áreas desconhecidas. Se esses problemas ocorrem é apenas uma questão de tempo. Estes são os exemplos de nossas práticas mais recentes às quais este artigo é dedicado. Elenco: Golang, Sentry, RabbitMQ, nginx, PostgreSQL e outros.

História nº 1. Golang e HTTP / 2


A execução de um benchmark que executa muitas solicitações HTTP para um aplicativo Web levou a resultados inesperados. Um aplicativo Go simples no processo de benchmark vai para outro aplicativo Go localizado atrás de ingress / openresty. Quando o HTTP / 2 está ativado, obtemos erros com o código 400 para algumas solicitações. Para entender o motivo desse comportamento, removemos o aplicativo Go do outro lado da cadeia e fizemos um local simples no Ingress, que sempre retorna 200. O comportamento não mudou!

Decidiu-se então reproduzir o script fora do ambiente Kubernetes - em outro pedaço de ferro. O resultado foi um Makefile, com a ajuda do qual são lançados dois contêineres: em um - benchmarks que vão para o nginx, no outro - no Apache. Ambos ouvem o HTTP / 2 com um certificado autoassinado. O tempo final de operação, veja neste repositório .

Execute os benchmarks com concurrency=200 :

1.1 Nginx:

 Completed 0 requests Completed 1000 requests Completed 2000 requests Completed 3000 requests Completed 4000 requests Completed 5000 requests Completed 6000 requests Completed 7000 requests Completed 8000 requests Completed 9000 requests ----- Bench results begin ----- Requests per second: 10336.09 Failed requests: 1623 ----- Bench results end ----- 

1.2 Apache:

 … ----- Bench results begin ----- Requests per second: 11427.60 Failed requests: 0 ----- Bench results end ----- 

Assumimos que o ponto aqui é uma implementação menos rigorosa do HTTP / 2 no Apache.

Vamos tentar com concurrency=1000 :

2.1 Nginx:

 … ----- Bench results begin ----- Requests per second: 11274.92 Failed requests: 4205 ----- Bench results end ----- 

2.2 Apache:

 … ----- Bench results begin ----- Requests per second: 11211.48 Failed requests: 5 ----- Bench results end ----- 

Ao mesmo tempo, observamos que os resultados nem sempre são reproduzidos : alguns dos lançamentos passam sem problemas.

Uma busca por problemas no github do projeto Golang levou aos números 25009 e 32441 . Através deles, fomos para o PR 903 : desativando o HTTP / 2 no Go por padrão!

Interpretar resultados de benchmark sem mergulhar profundamente na arquitetura dos servidores Web acima é bastante difícil. Em um caso específico, foi suficiente desativar o HTTP / 2 para o serviço especificado.

História No. 2. Symfony e sentinela antigos


Em um dos projetos, uma versão muito antiga do framework PHP symfony (v2.3) ainda está funcionando. Um cliente Raven antigo e uma classe auto-escrita em PHP são anexados a ele "no kit", o que complica um pouco a depuração.

Depois de transferir um dos serviços no Kubernetes para o Sentry, usado para rastrear erros na aplicação deste projeto, os eventos pararam subitamente. Para reproduzir esse comportamento, usamos exemplos do site Sentry, pegando duas opções e copiando o DSN das configurações do Sentry. Visualmente, tudo funcionou: mensagens de erro (supostamente) foram enviadas uma após a outra.

Opção de verificação de JavaScript:

 <!DOCTYPE html> <html> <body> <script src="https://browser.sentry-cdn.com/5.6.3/bundle.min.js" integrity="sha384-/Cqa/8kaWn7emdqIBLk3AkFMAHBk0LObErtMhO+hr52CntkaurEnihPmqYj3uJho" crossorigin="anonymous"> </script> <h2>JavaScript in Body</h2> <p id="demo">A Paragraph.</p> <button type="button" onclick="myFunction()">Try it</button> <script> Sentry.init({ dsn: 'http://33dddd76e9f0c4ddcdb51@sentry.kube-dev.test//12' }); try { throw new Error('Caught'); } catch (err) { Sentry.captureException(err); } </script> </body> </html> 

Da mesma forma em Python:

  from sentry_sdk import init, capture_message init("http://33dddd76e9f0c4ddcdb51@sentry.kube-dev.test//12") capture_message("Hello World") # Will create an event. raise ValueError() 

No entanto, eles não entraram no Sentry. Ao enviar uma mensagem, é criada a ilusão de que a mensagem foi enviada, porque os clientes geram imediatamente um hash para o problema.

Como resultado, o problema foi resolvido com muita simplicidade: o envio de eventos foi para HTTP, e o serviço Sentry ouviu apenas HTTPS. Foi fornecido um redirecionamento de HTTP para HTTPS, mas o cliente antigo (o código no lado do symfony) não conseguiu seguir os redirecionamentos, o que você não espera por padrão atualmente.

História nº 3. RabbitMQ e proxy de terceiros


Em um projeto, a nuvem Evotor é usada para conectar caixas registradoras. De fato, funciona como um proxy: as solicitações POST do Evotor vão diretamente para o RabbitMQ - através do plug - in STOMP implementado nas conexões WebSocket.

Um dos desenvolvedores fez solicitações de teste usando o Postman e recebeu as respostas esperadas de 200 OK , no entanto, solicitações através da nuvem levaram a um inesperado 405 Method Not Allowed .

200 OK
 source: kubernetes namespace: kube-nginx-ingress host: kube-node-2 pod_name: nginx-2bpt7 container_name: nginx stream: stdout app: nginx controller-revision-hash: 5bdbfd564 pod-template-generation: 25 time: 2019-09-10T09:42:50+00:00 request_id: 1271dba228f0943ab2df0196ff0d7f67 user: client address: 100.200.300.400 protocol: HTTP/1.1 scheme: http method: POST host: rmq-review.kube-dev.client.domain path: /api/queues/vhost/queue.gen.eeeeffff111:1.onlinecassa:55556666/get request_query: referrer: user_agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36 content_kind: cacheable namespace: review ingress: stomp-ws service: rabbitmq service_port: stats vhost: rmq-review.kube-dev.client.domain location: / nginx_upstream_addr: 10.127.1.1:15672 nginx_upstream_bytes_received: 2538 nginx_upstream_response_time: 0.008 nginx_upstream_status: 200 bytes_received: 757 bytes_sent: 1254 request_time: 0 status: 200 upstream_response_time: 0 upstream_retries: 0 

405 Método não permitido
 source: kubernetes namespace: kube-nginx-ingress host: kube-node-1 pod_name: nginx-4xx6h container_name: nginx stream: stdout app: nginx controller-revision-hash: 5bdbfd564 pod-template-generation: 25 time: 2019-09-10T09:46:26+00:00 request_id: b8dd789604864c95b4af499ed6805630 user: client address: 200.100.300.400 protocol: HTTP/1.1 scheme: http method: POST host: rmq-review.kube-dev.client.domain path: /api/queues/vhost/queue.gen.ef7fb93387ca9b544fc1ecd581cad4a9:1.onlinecassa:55556666/get request_query: referrer: user_agent: ru.evotor.proxy/37 content_kind: cache-headers-not-present namespace: review ingress: stomp-ws service: rabbitmq service_port: stats vhost: rmq-review.kube-dev.client.domain location: / nginx_upstream_addr: 10.127.1.1:15672 nginx_upstream_bytes_received: 134 nginx_upstream_response_time: 0.004 nginx_upstream_status: 405 bytes_received: 878 bytes_sent: 137 request_time: 0 status: 405 upstream_response_time: 0 upstream_retries: 0 

Pedido tcpdump da Evotor
 200.100.300.400.21519 > 100.200.400.300: Flags [P.], cksum 0x8e29 (correct), seq 1:879, ack 1, win 221, options [nop,nop,TS val 2313007107 ecr 79097074], length 878: HTTP, length: 878 POST /api/queues//vhost/queue.gen.ef7fb93387ca9b544fc1ecd581cad4a9:1.onlinecassa:55556666/get HTTP/1.1 device-model: ST-5 device-os: android Accept-Encoding: gzip content-type: application/json; charset=utf-8 connection: close accept: application/json x-original-forwarded-for: 10.11.12.13 originhost: rmq-review.kube-dev.client.domain x-original-uri: /api/v2/apps/e114-aaef-bbbb-beee-abadada44ae/requests x-scheme: https accept-encoding: gzip user-agent: ru.evotor.proxy/37 Authorization: Basic X-Evotor-Store-Uuid: 20180417-73DC-40C9-80B7-00E990B77D2D X-Evotor-Device-Uuid: 20190909-A47B-40EA-806A-F7BC33833270 X-Evotor-User-Id: 01-000000000147888 Content-Length: 58 Host: rmq-review.kube-dev.client.domain {"count":1,"encoding":"auto","ackmode":"ack_requeue_true"}[!http] 12:53:30.095385 IP (tos 0x0, ttl 64, id 5512, offset 0, flags [DF], proto TCP (6), length 52) 100.200.400.300:80 > 200.100.300.400.21519: Flags [.], cksum 0xfa81 (incorrect -> 0x3c87), seq 1, ack 879, win 60, options [nop,nop,TS val 79097122 ecr 2313007107], length 0 12:53:30.096876 IP (tos 0x0, ttl 64, id 5513, offset 0, flags [DF], proto TCP (6), length 189) 100.200.400.300:80 > 200.100.300.400.21519: Flags [P.], cksum 0xfb0a (incorrect -> 0x03b9), seq 1:138, ack 879, win 60, options [nop,nop,TS val 79097123 ecr 2313007107], length 137: HTTP, length: 137 HTTP/1.1 405 Method Not Allowed Date: Tue, 10 Sep 2019 10:53:30 GMT Content-Length: 0 Connection: close allow: HEAD, GET, OPTIONS 

Pedido tcpdump feito por curl
 777.10.74.11.61211 > 100.200.400.300:80: Flags [P.], cksum 0x32a8 (correct), seq 1:397, ack 1, win 2052, options [nop,nop,TS val 734012594 ecr 4012360530], length 396: HTTP, length: 396 POST /api/queues/%2Fvhost/queue.gen.ef7fb93387ca9b544fc1ecd581cad4a9:1.onlinecassa:55556666/get HTTP/1.1 Host: rmq-review.kube-dev.client.domain User-Agent: curl/7.54.0 Authorization: Basic = Content-Type: application/json Accept: application/json Content-Length: 58 {"count":1,"ackmode":"ack_requeue_true","encoding":"auto"}[!http] 12:40:11.001442 IP (tos 0x0, ttl 64, id 50844, offset 0, flags [DF], proto TCP (6), length 52) 100.200.400.300:80 > 777.10.74.11.61211: Flags [.], cksum 0x2d01 (incorrect -> 0xfa25), seq 1, ack 397, win 59, options [nop,nop,TS val 4012360590 ecr 734012594], length 0 12:40:11.017065 IP (tos 0x0, ttl 64, id 50845, offset 0, flags [DF], proto TCP (6), length 2621) 100.200.400.300:80 > 777.10.74.11.61211: Flags [P.], cksum 0x370a (incorrect -> 0x6872), seq 1:2570, ack 397, win 59, options [nop,nop,TS val 4012360605 ecr 734012594], length 2569: HTTP, length: 2569 HTTP/1.1 200 OK Date: Tue, 10 Sep 2019 10:40:11 GMT Content-Type: application/json Content-Length: 2348 Connection: keep-alive Vary: Accept-Encoding cache-control: no-cache vary: accept, accept-encoding, origin 

O olho treinado de um engenheiro vê imediatamente a diferença:

  • curl: POST /api/queues/%2Fclient…
  • Evotor: POST /api/queues//client…

O fato é que, em um caso, um //vhost incompreensível (para RabbitMQ) //vhost e no outro - %2Fvhost , que é o comportamento esperado quando:

 # rabbitmqctl list_vhosts Listing vhosts ... /vhost 

Na edição do projeto RabbitMQ sobre este tópico, o desenvolvedor explica:

Não iremos substituir%-codificação. É uma maneira padrão de codificação de caminho de URL e existe há séculos. Assumindo que a codificação% em ferramentas baseadas em HTTP desaparecerá, até mesmo a estrutura mais popular assumindo que esses caminhos de URL são "maliciosos" é míope e ingênuo. O nome do host virtual padrão pode ser alterado para qualquer valor (como um que não use barras ou outros caracteres que exijam codificação%) e, pelo menos com a versão Pivotal BOSH do RabbitMQ, o host virtual padrão é excluído no momento da implantação de qualquer maneira .

O problema foi resolvido sem o envolvimento de nossos engenheiros (no lado do Evotor após entrar em contato com eles).

História nº 4. Gene no PostgreSQL


O PostgreSQL possui um índice muito útil, que é frequentemente esquecido. Esta história começou com reclamações sobre os freios no aplicativo. Em um artigo recente, já demos um exemplo de um fluxo de trabalho aproximado ao analisar essas situações. E aqui nosso APM - Atatus - mostrou a seguinte imagem:



Às 10h, há um aumento no tempo que o aplicativo gasta trabalhando com o banco de dados. Como esperado, o motivo está nas respostas lentas do DBMS. Para nós, analisar consultas, identificar áreas problemáticas e índices "paralisados" é uma rotina compreensível. O okmeter que usamos ajuda muito nele : existem dois painéis padrão para monitorar o status dos servidores e a capacidade de criar rapidamente os nossos - com a saída de métricas problemáticas:



Os gráficos de carregamento da CPU indicam que um dos bancos de dados é 100% carregado. Porque Os novos painéis do PostgreSQL solicitarão:



A causa dos problemas é imediatamente aparente - o principal consumidor da CPU:

 SELECT u.* FROM users u WHERE u.id = ? & u.field_1 = ? AND u.field_2 LIKE '%somestring%' ORDER BY u.id DESC LIMIT ? 

Considerando o plano de trabalho da consulta problemática, descobrimos que a filtragem por campos indexados de uma tabela oferece uma seleção muito grande: o banco de dados recebe mais de 70 mil linhas por id e field_1 e, em seguida, procura uma substring entre elas. Acontece que LIKE em uma substring interage com uma grande quantidade de dados de texto, o que leva a um sério abrandamento na execução da consulta e a um aumento na carga da CPU.

Aqui, você pode notar com razão que um problema de arquitetura não está descartado (é necessária uma correção lógica do aplicativo ou até mesmo um mecanismo de texto completo ...), mas não há tempo para retrabalho, e ele deveria ter funcionado rapidamente 15 minutos atrás. Ao mesmo tempo, a palavra de pesquisa é realmente um identificador (e por que não em um campo separado? ..), que produz unidades de linhas. De fato, se pudéssemos compor um índice nesse campo de texto, todos os outros se tornariam desnecessários.

A solução final atual é adicionar um índice GIN para o field_2 . Esse é o herói do dia - esse mesmo "gênio". Em resumo, o GIN é um tipo de índice que tem um desempenho muito bom na pesquisa de texto completo, acelerando-o qualitativamente. Você pode ler mais sobre isso, por exemplo, neste material maravilhoso .



Como você pode ver, essa operação simples permitiu remover a carga extra e com ela - e economizar dinheiro para o cliente.

História nº 5. Armazenando em cache s3 no nginx


O armazenamento em nuvem compatível com S3 está há muito tempo na lista de tecnologias usadas por muitos projetos. Se você precisar de armazenamento de imagem confiável para o seu site ou para dados de rede neural, o Amazon S3 é uma ótima opção. A confiabilidade do armazenamento e a alta disponibilidade de dados (e a falta da necessidade de "cercar o jardim") são cativantes.

No entanto, às vezes, para economizar dinheiro - porque geralmente o pagamento do S3 é feito para solicitações e tráfego - uma boa solução é instalar um servidor proxy em cache na frente do armazenamento. Esse método reduzirá os custos quando se trata, por exemplo, de avatares de usuários, dos quais existem muitos em cada página.

Parece que é mais fácil do que pegar o nginx e configurar proxies com cache, revalidação, atualização em segundo plano e outro blackjack? No entanto, como em outros lugares, existem algumas nuances ...

A configuração aproximada desse proxy com armazenamento em cache era assim:

 proxy_cache_key $uri; proxy_cache_methods GET HEAD; proxy_cache_lock on; proxy_cache_revalidate on; proxy_cache_background_update on; proxy_cache_use_stale updating error timeout invalid_header http_500 http_502 http_503 http_504; proxy_cache_valid 200 1h; location ~ ^/(?<bucket>avatars|images)/(?<filename>.+)$ { set $upstream $bucket.s3.amazonaws.com; proxy_pass http://$upstream/$filename; proxy_set_header Host $upstream; proxy_cache aws; proxy_cache_valid 200 1h; proxy_cache_valid 404 60s; } 

E, em geral, funcionou: as imagens foram exibidas, tudo estava bem com o cache ... no entanto, surgiram problemas com os clientes do AWS S3. Em particular, o cliente do aws-sdk-php parou de funcionar. A análise dos logs nginx mostrou que o upstream retornou o código 403 para solicitações HEAD, e a resposta continha um erro específico: SignatureDoesNotMatch . Quando vimos que o nginx faz uma solicitação GET para upstream, tudo se encaixou.

O fato é que o cliente S3 assina cada solicitação e o servidor verifica essa assinatura. No caso de proxy simples, tudo funciona bem: a solicitação atinge o servidor inalterada. No entanto, quando o cache está ativado, o nginx começa a otimizar o trabalho com o back-end e substitui as solicitações HEAD pelo GET. A lógica é simples: é melhor recuperar e salvar o objeto inteiro e, em seguida, todos os pedidos HEAD do cache também. No entanto, no nosso caso, a solicitação não pode ser modificada porque está assinada.

Existem essencialmente duas soluções:

  1. Não conduza clientes S3 através de proxies;
  2. se for "necessário", desative a opção proxy_cache_convert_head e adicione $request_method à chave de cache. Nesse caso, o nginx envia solicitações HEAD "como estão" e armazena em cache as respostas separadamente.

História nº 6. DDoS e conteúdo do usuário do Google


A noite de domingo não pressagiou problemas até - de repente! - A fila de invalidação de cache nos servidores de borda não aumentou, o que fornece tráfego para usuários reais. Esse é um sintoma muito estranho: afinal, o cache é implementado na memória e não está vinculado aos discos rígidos. A descarga do cache na arquitetura usada é uma operação barata, portanto esse erro pode aparecer apenas no caso de uma carga realmente alta. Isso foi confirmado pelo fato de que os mesmos servidores começaram a notificar o aparecimento de 500 erros (picos de linha vermelha no gráfico abaixo) .



Um aumento tão acentuado levou a superações de CPU:



Uma análise rápida mostrou que os pedidos não chegaram aos domínios principais, mas a partir dos logs ficou claro que eles estavam no vhost padrão. Ao longo do caminho, muitos usuários americanos procuraram o recurso russo. Tais circunstâncias sempre levantam questões imediatamente.

Depois de coletar dados dos logs do nginx, revelamos que estamos lidando com uma certa botnet:

 35.222.30.127 US [15/Sep/2019:21:40:00 +0300] GET "http://example.ru/?ITPDH=XHJI" HTTP/1.1 301 178 "http://example.ru/ORQHYGJES" "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729)" "-" "upcache=-" "upaddr=-" "upstatus=-" "uplen=-" "uptime=-" spdy="" "loc=wide-closed.example.ru.undef" "rewrited=/?ITPDH=XHJI" "redirect=http://www.example.ru/?ITPDH=XHJI" ancient=1 cipher=- "LM=-;EXP=-;CC=-" 107.178.215.0 US [15/Sep/2019:21:40:00 +0300] GET "http://example.ru/?REVQSD=VQPYFLAJZ" HTTP/1.1 301 178 "http://www.usatoday.com/search/results?q=MLAJSBZAK" "Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)" "-" "upcache=-" "upaddr=-" "upstatus=-" "uplen=-" "uptime=-" spdy="" "loc=wide-closed.example.ru.undef" "rewrited=/?REVQSD=VQPYFLAJZ" "redirect=http://www.example.ru/?REVQSD=VQPYFLAJZ" ancient=1 cipher=- "LM=-;EXP=-;CC=-" 107.178.215.0 US [15/Sep/2019:21:40:00 +0300] GET "http://example.ru/?MPYGEXB=OMJ" HTTP/1.1 301 178 "http://engadget.search.aol.com/search?q=MIWTYEDX" "Mozilla/5.0 (Windows; U; Windows NT 6.1; en; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729)" "-" "upcache=-" "upaddr=-" "upstatus=-" "uplen=-" "uptime=-" spdy="" "loc=wide-closed.example.ru.undef" "rewrited=/?MPYGEXB=OMJ" "redirect=http://www.example.ru/?MPYGEXB=OMJ" ancient=1 cipher=- "LM=-;EXP=-;CC=-" 

Um padrão compreensível é rastreado nos logs:

  • verdadeiro agente do usuário;
  • uma solicitação para o URL raiz com um argumento GET aleatório para evitar entrar no cache;
  • referenciador indica que a solicitação veio de um mecanismo de pesquisa.

Coletamos os endereços e verificamos sua afiliação - todos eles pertencem ao googleusercontent.com , com duas sub-redes (107.178.192.0/18 e 34.64.0.0/10). Essas sub-redes contêm máquinas virtuais GCE e vários serviços, como tradução de página.

Felizmente, o ataque não durou tanto tempo (cerca de uma hora) e diminuiu gradualmente. Parece que os algoritmos de proteção dentro do Google funcionaram, então o problema foi resolvido "por si só".

Esse ataque não foi destrutivo, mas levantou questões úteis para o futuro:

  • Por que os anti-ddos não funcionam? Um serviço externo é usado, para o qual enviamos uma solicitação correspondente. No entanto, havia muitos endereços ...
  • Como se proteger disso no futuro? No nosso caso, até opções para fechar o acesso em uma base geográfica são possíveis.

PS


Leia também em nosso blog:

Source: https://habr.com/ru/post/pt471892/


All Articles