Do Apache ao Go: Como melhoramos nossas Landing Pages

Publicado por Gabriel Mazetto no dia dev

Apache

Sempre que fazemos melhorias de performance, antes de qualquer alteração, definimos formas de medição. Medir é importante para identificar qual solução é melhor ou ainda, se as modificações irão de fato melhorar o que já temos.

Recentemente, fizemos uma mudança grande na arquitetura de serviço que hospeda nossas Landing Pages, a qual chamamos internamente de Landing Pages 2.0. Na infraestrutura antiga, tínhamos um serviço simples. Um servidor Apache servia indiscriminadamente conteúdos estáticos.

Arquitetura: Landing Pages 2.0

Na nova arquitetura, criamos um micro-serviço em Go para rotear essas requisições. Ele é responsável por receber as requisições, verificar o mapeamento do domínio para um determinado cliente em uma base de dados Chave/Valor (Redis), e enviar o conteúdo previamente gerado armazenado no S3.

Essa arquitetura nos permitiu escalar facilmente todos os componentes envolvidos:

  • Requisições: Aumentar o número de nodos da aplicação em Go.
  • Armazenamento: S3 escala isso sozinho pra nós.
  • Base de dados: Redis torna as poucas operações de leitura/escrita altamente performáticas e permite expansões futuras quando necessário.

Melhorando a performance

Na nossa primeira iteração, resolvemos o problema da escalabilidade, mas ainda não estávamos satisfeitos com a performance. A solução já era melhor que a antiga, mas ainda existiam pontos de melhoria:

  • Mapeamentos de domínios não mudam com frequência.
  • Conteúdos de Landing Pages possuem um tempo de latência de transferência do S3 para o Go.

Para resolver isso, introduzimos cache nas consultas do Redis, o que diminuiu a necessidade de abrir conexões adicionais com o servidor (que é um recurso escasso). Melhorias de performance são apenas um efeito colateral positivo; o maior ganho é na escalabilidade. Já nas transferências do S3, o ganho maior é na performance (redução da latência e tempo de resposta).

Em algumas situações, essas duas melhorias combinadas ainda nos permitem suportar volumes assimétricos de tráfego, por exemplo, acessos a uma Landing Page logo após o disparo de uma campanha de e-mail para uma base grande de usuários, ou conteúdos temporalmente populares (como relacionados com vestibular, concursos, ou eventos importantes como a Black Friday e liquidações pós-natal).

Nós fizemos as alterações mencionadas passo-a-passo. Inicialmente, introduzindo o cache das consultas do Redis e testando algumas bibliotecas de cache in memory do Go e, posteriormente, expandindo para as chamadas do S3.

Entre cada alteração, foram feitos benchmarks para avaliar ganhos e melhorias. Então elegemos a melhor solução e seguimos adiante.

Metodologia do benchmark

Testes foram disparados por uma máquina m4.xlarge na mesma zona de disponibilidade (availability zone) da Amazon, do micro-serviço de Landing Pages, para evitar que gargalos na conexão influenciassem o resultado.

As medições utilizaram o boom, que é um ótimo substituto ao Apache Benchmark (conhecido popularmente pelo comando ab).

Para cada teste foram feitos 3 disparos com intervalo de pelo menos 1 minuto. Consideramos para comparação o benchmark com melhor resultado, usando os seguintes parâmetros para disparo:

  • Concorrência: 300
  • Numero de requisições: 50000
1
boom -c 300 -n 50000 http://endereco_do_teste/landing_page

O que e como medir

Quando fazemos um benchmark, não queremos saber apenas o número de Requests por segundo que a aplicação aguenta. É muito mais importante conhecer a latência distribuída em todas essas requisições. É comum que um grupo de usuários recebam a resposta em um tempo mais curto, enquanto outros experimentem tempos de resposta mais demorados.

Entender essa distribuição como um todo nos permite verificar qual vai ser a experiência média (a que a maioria dos usuários terá). Isso também permite que otimizemos para melhorar para o conjunto de usuários que terá uma performance pior.

Uma forma comum de visualizar essa distribuição é através de histogramas, algo que o boom faz nativamente para nós.

É importante entender que estamos falando da flutuação de performance que usuários nas mesmas condições (máquina, conexão, latência, entre outros) vão experimentar, simplesmente por características alheias a vontade dele. Em um teste de carga esses gargalos podem ficar bem evidentes. Veja alguns exemplos de agentes que podem influenciar a flutuação de performance:

  • Gargalos na rede
  • Execução de Garbage Colector na aplicação
  • Lock de semáforos
  • Context Switching de threads
  • Latência na resposta de serviços externos
  • Cold Cache/Cache Revalidation

Dados do Benchmark

Para que o artigo não ficasse muito grande, preservamos o melhor resultado de cada bateria de testes, e uma pequena descrição dos parâmetros e estados envolvidos.

Infra-Atual

Ambiente com t1.micro, sem limites no pool do Redis e sem cache interno:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Summary:
  Total:        1228.3118 secs.
  Slowest:      59.9010 secs.
  Fastest:      0.1040 secs.
  Average:      7.2891 secs.
  Requests/sec: 40.7063
  Total Data Received:  15858 bytes.
  Response Size per Request:    0 bytes.

Status code distribution:
  [500] 4 responses
  [200] 49098 responses
  [404] 881 responses
  [504] 17 responses

Response time histogram:
  0.104 [1]     |
  6.084 [31360] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  12.063 [11632]|∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  18.043 [2521] |∎∎∎
  24.023 [1336] |  30.003 [1413] |  35.982 [731]  |
  41.962 [623]  |
  47.942 [304]  |
  53.921 [58]   |
  59.901 [21]   |

Latency distribution:
  10% in 1.5992 secs.
  25% in 3.0501 secs.
  50% in 4.1043 secs.
  75% in 8.3689 secs.
  90% in 15.7139 secs.
  95% in 26.6289 secs.
  99% in 40.5505 secs.

Teste 1: máquina maior

Ambiente com c3.large, sem limites no pool do Redis e sem cache interno:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Summary:
  Total:        302.5603 secs.
  Slowest:      10.1343 secs.
  Fastest:      0.1663 secs.
  Average:      1.8117 secs.
  Requests/sec: 165.2563
  Total Data Received:  1008 bytes.
  Response Size per Request:    0 bytes.

Status code distribution:
  [404] 56 responses
  [200] 49944 responses

Response time histogram:
  0.166 [1]     |
  1.163 [74]    |
  2.160 [48902] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  3.157 [964]   |
  4.153 [32]    |
  5.150 [23]    |
  6.147 [2]     |
  7.144 [1]     |
  8.141 [0]     |
  9.137 [0]     |
  10.134 [1]    |

Latency distribution:
  10% in 1.6216 secs.
  25% in 1.7099 secs.
  50% in 1.8015 secs.
  75% in 1.9050 secs.
  90% in 2.0080 secs.
  95% in 2.0794 secs.
  99% in 2.2223 secs.

As conexões que retornaram 404 foram por causadas por um esgotamento do limite de conexões no Redis:

Rollbar - ERR max number of clients reached

Teste 2: limite no pool

Ambiente com c3.large, com limites no pool do Redis e sem cache interno:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Summary:
  Total:        271.2277 secs.
  Slowest:      6.1899 secs.
  Fastest:      0.2417 secs.
  Average:      1.6247 secs.
  Requests/sec: 184.3469

Status code distribution:
  [200] 50000 responses

Response time histogram:
  0.242 [1]     |
  0.836 [94]    |
  1.431 [8198]  |∎∎∎∎∎∎∎∎
  2.026 [39903] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  2.621 [1667]  |  3.216 [125]   |
  3.811 [9]     |
  4.405 [1]     |
  5.000 [1]     |
  5.595 [0]     |
  6.190 [1]     |

Latency distribution:
  10% in 1.3701 secs.
  25% in 1.4887 secs.
  50% in 1.6132 secs.
  75% in 1.7502 secs.
  90% in 1.8914 secs.
  95% in 1.9813 secs.
  99% in 2.1942 secs.

Teste 3: limite no pool + go-cache

Ambiente com c3.large, com limites no pool do Redis e com cache interno das chamadas do Redis usando pmylund/go-cache:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Summary:
  Total:        262.3821 secs.
  Slowest:      5.5117 secs.
  Fastest:      0.1617 secs.
  Average:      1.5700 secs.
  Requests/sec: 190.5618

Status code distribution:
  [500] 12 responses
  [200] 49988 responses

Response time histogram:
  0.162 [1]     |
  0.697 [51]    |
  1.232 [4406]  |∎∎∎∎∎
  1.767 [35245] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  2.302 [9694]  |∎∎∎∎∎∎∎∎∎∎∎
  2.837 [516]   |
  3.372 [54]    |
  3.907 [8]     |
  4.442 [7]     |
  4.977 [11]    |
  5.512 [7]     |

Latency distribution:
  10% in 1.2471 secs.
  25% in 1.3933 secs.
  50% in 1.5462 secs.
  75% in 1.7243 secs.
  90% in 1.9146 secs.
  95% in 2.0473 secs.
  99% in 2.3329 secs.

Teste 4: limite no pool + go-ttl-cache

Ambiente com c3.large, com limites no pool do Redis e com cache interno das chamadas do Redis usando koofr/go-ttl-cache:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Summary:
  Total:        262.1352 secs.
  Slowest:      5.5964 secs.
  Fastest:      0.1945 secs.
  Average:      1.5676 secs.
  Requests/sec: 190.7413

Status code distribution:
  [200] 50000 responses

Response time histogram:
  0.194 [1]     |
  0.735 [30]    |
  1.275 [5647]  |∎∎∎∎∎∎
  1.815 [36570] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  2.355 [7336]  |∎∎∎∎∎∎∎∎
  2.895 [338]   |
  3.436 [60]    |
  3.976 [12]    |
  4.516 [2]     |
  5.056 [1]     |
  5.596 [3]     |

Latency distribution:
  10% in 1.2593 secs.
  25% in 1.3971 secs.
  50% in 1.5453 secs.
  75% in 1.7142 secs.
  90% in 1.8991 secs.
  95% in 2.0210 secs.
  99% in 2.3233 secs.

Teste 5: limite no pool + congomap

Ambiente com c3.large, com limites no pool do Redis e com cache interno das chamadas do Redis usando karrick/congomap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Summary:
  Total:        262.7559 secs.
  Slowest:      4.7946 secs.
  Fastest:      0.2050 secs.
  Average:      1.5736 secs.
  Requests/sec: 190.2907

Status code distribution:
  [500] 12 responses
  [200] 49988 responses

Response time histogram:
  0.205 [1]     |
  0.664 [34]    |
  1.123 [2379]  |∎∎∎
  1.582 [26218] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  2.041 [17540] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  2.500 [3267]  |∎∎∎∎
  2.959 [442]   |
  3.418 [68]    |
  3.877 [17]    |
  4.336 [11]    |
  4.795 [23]    |

Latency distribution:
  10% in 1.2200 secs.
  25% in 1.3726 secs.
  50% in 1.5365 secs.
  75% in 1.7261 secs.
  90% in 1.9748 secs.
  95% in 2.1572 secs.
  99% in 2.5275 secs.

Teste 6: limite no pool + congomap cacheando tudo

Ambiente com c3.large, com limites no pool do Redis e com cache interno das chamadas do Redis e S3 usando karrick/congomap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Summary:
  Total:        84.2632 secs.
  Slowest:      8.3139 secs.
  Fastest:      0.0029 secs.
  Average:      0.5027 secs.
  Requests/sec: 593.3790

Status code distribution:
  [200] 50000 responses

Response time histogram:
  0.003 [1]     |
  0.834 [46225] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  1.665 [1141]  |
  2.496 [532]   |
  3.327 [1]     |
  4.158 [1]     |
  4.989 [21]    |
  5.821 [1368]  |  6.652 [632]   |
  7.483 [70]    |
  8.314 [8]     |

Latency distribution:
  10% in 0.1176 secs.
  25% in 0.1691 secs.
  50% in 0.2237 secs.
  75% in 0.3168 secs.
  90% in 0.5130 secs.
  95% in 1.7127 secs.
  99% in 6.0562 secs.

Resultado final

A primeira melhoria, e a mais simples, veio com a técnica de scale up, quando substituímos uma máquina simples por uma com mais recursos.

As demais melhorias relacionadas a introdução dos caches, nos deixou mais preparados para picos de acesso repentinos, ao mesmo tempo que temos um caminho bem definido de crescimento horizontal, e mais previsível em um longterm.

Conseguimos com essas modificações, em situações de pico (por nodo), passar de 40reqs/seg com uma latência média de 7.2s para ~590reqs/seg, com latência média de 0.5s. Melhoria de 14x em throughput e 14.4x em latência.

Uma outra lição que pudemos tirar dessa situação é que não existe “tecnologia mágica”. Escolhas corretas para situações específicas ajudam, mas você ainda precisa usar corretamente os conhecimentos dos vários anos da Ciência da Computação e mensurar sempre.

Gabriel Mazetto

Gabriel Mazetto

Full Stack Developer

Comentários