Agora que estou com o Slackware sambando nas nuves, ninguém me segura!

Coloquei o Slackware para rodar na menor instância disponível do AWS, uma t1.micro. Ela tem um processadorzinho com poder computacional variável e 615MB de RAM. E olha, com o Slackware tá voando! O máximo que essa instância consumiu de memória até agora foi 113MB, já com o MexApi rodando nela.

Eu não teria conseguido consumo comparável se estivesse rodando o Apache. Mas não me entenda mal. O Apache é ótimo, poderoso, flexível... e grande. Tamanho é documento. E custa caro! Esse preço é cobrado em ciclos de processamento, memória e I/O.

Essas coisas se convertem em dinheiro do seguinte modo: a necessidade de mais recursos para servir aplicações requer o uso de mais infraestrutura para atender uma dada quantidade de pessoas. A quantidade de infraestrutura contratada é diretamente proporcional ao dinheiro necessário para pagá-la. Mais infra, mais dinheiro. Menos infra, menos dinheiro. Quem usa AWS sabe bem como essa conta fecha (ou não) no fim do mês.

Já há tempos eu vinha com um olho no peixe, outro no gato: servia tudo com o Apache e ficava de olho no queridinho do momento, o nginx. Até já usei o lighttpd em produção por longos anos, mas por força do ambiente que encontrei pronto na empresa que me envolvi, voltei ao Apache e com ele fiquei. Até agora.

Esse momento Slackware/AWS me deu a oportunidade ideal pra ver qual é. E é demais! Mas por que é demais? Explico agora.

Só pra ficar claro, é disso que vou falar:

Quesito, PageSpeed. Acadêmicos do MexApi. Nota, 'nuoventa e
nuóve'.

Speed, speed, speed

No nginx é tudo sobre velocidade: alta concorrência e alta performance com baixo uso de memória. Isso é música para a infra! Claro, você tem que saber fazer, mas sabendo terá benefícios mais rápidos e com menor complexidade do que com o Apache.

Por quê menor complexidade? Porque toda a configuração do nginx é centralizada em uns poucos scripts e o conceito de .htaccess simplesmente não existe nele. Ou as configurações de um servidor virtual são parte da configuração do nginx ou não são.

Claro, isso também pode ser obtido com o Apache, mas como ele permite o .htaccess, a exceção virou regra, a banana comeu o macaco. Basta olhar para um Wordpress da vida e ver que ele próprio confia na existência de um .htaccess, sem contar na quantidade de plugins para ele que também o fazem. Se o pai faz, os filinhos imitam.

Manter configurações do webserver espalhadas? Não bom, complexo, morde, dói, feio.

Outro ponto onde a complexidade do nginx é diminuída: módulos. O Apache nos dá o webserver e nós vamos atrás dos módulos. Quer PHP? Módulo. SSL, rewrite, LDAP, tudo módulo. É certo que muitos são parte do Apache, mas na real, depois de compilados, eles são programinhas carregados durante a inicialização do Apache.

No nginx é assim: ou você já o compila com os módulos que quer, ou você recompila para ter os módulos que precisa. Não tem programinha separado. É tudo parte do executável do nginx. Na configuração você apenas habilita ou desabilita um módulo, mas não poderá jamais incluir ou tirar um módulo sem recompilar o nginx.

E daí vem a velocidade. E como é rápido! A explicação em alto nível para isso é muito simples: ao invés de fazer como o Apache, que ao usar um módulo o carrega em memória e troca informações com esse pedaço de programa separado, o nginx fala com ele mesmo pois ele tem tudo o que precisa.

Olha, mãe! Sem minificar! {#olha-mãe-sem-minificar}

Minificar! Como eu odeio esse anglicismo! Por isso quando eu falar, vou usar miniaturizar, ok?

Se você é um desenvolvedor web, já deve estar de saco cheio de fazer suas aplicações ou pegar o javascript e css dos outros para otimizar o código. Por miniaturizar entendemos: concatenar múltiplos arquivos do mesmo tipo em apenas um, remover comentários, espaços em branco, quebras de linha e por aí vai.

Também tem as imagens. Otimizar as imagens removendo os metadados desnecessários e comprimindo legal pra carregar rapidinho. São muitos outros detalhezinhos para irem ao ar no que usuário final vai ver e que, justamente por isso, precisa ser carregado num piscar de olhos e funcionar direito, apesar de todo o estupro da miniaturização. No fim das contas, esse processo é chato e demorado, duas coisas muito ruins para se conviver.

Desde que um puto resolveu dizer que cada segundo adicional para carregar as páginas da Amazon custa pra ela US$ 1,6 bilhão (você leu certo, UM VIRGULA SEIS BILHÃO DE DÓLARES) por ano em vendas perdidas, sua vida deve estar bem complicada. Sabe aquela de que tempo é dinheiro? Pois é, agora sabemos o preço: US$ 50,73 por segundo!

O cliente quer um site rápido para os clientes dele, o seu chefe fica fungando no seu cangote com cada milésimo de segundo para o onload, o povo do webdesign surtando com os javascript e CSS cheios das firulas, fontes e tudo o mais, os putos da infra dizendo que a culpa é da aplicação, e você no meio.

Pausa pra musiquinha: 'Vamos dar as mãos, vamos dar as mãos. Vamos lá, e vamos juntos cantar.'

Apresento-lhes o PageSpeed Module. O PageSpeed - aquele mesmo miserento serviço do Google que já te tirou o sono tantas vezes porque o cliente exige uma nota alta para o site dele - possui um módulo para o Apache e o nginx que cuida de quase tudo que é necessário para você passar de ano com louvor e estrelinha no PageSpeed Insights.

Você, como desenvolvedor web, tem a responsabilidade e obrigação de codificar uma aplicação boa, funcional, leve e rápida. O código tem que ser conciso, pode (e deve) conter comentários, usar indentação e todas essas coisas que fazem nós, humanos, entenderem código escrito por outros. Sem isso não há manutenção que preste.

Mas as máquinas não precisam disso. E se as máquinas não precisam disso, o povo da infra também não. Chame-os para uma reunião e discutam a inclusão do PageSpeed Module nos webservers.

Magia negra, pero no mucho

Quando você roda um webserver com o PageSpeed Module devidamente configurado, ele cuida automaticamente de uma série de coisas. Da miniaturização e concatenação de múltiplos CSS (em arquivos ou @imports) e javascripts, passando pela otimização de imagens, até o cache. Sim, ele cacheia!

O modo padrão de operação do PageSpeed Module, chamado de CoreFilters, já vem com configurações bastante razoáveis para começar. A partir do CoreFilters você pode habilitar ou desabilitar os filtros conforme a necessidade. Flexibilidade total.

Mas aí o povo da infra replica: ah, mas isso vai aumentar o processamento requerido em nossos webservers. Concorde com eles, porque é verdade. Vai mesmo. Mas só na primeira vez que a página é acessada. Repetindo: ele cacheia! As otimizações serão TODAS cacheadas. Uma vez otimizado, o webserver vai usar os arquivos estáticos cacheados.

E aí que o nginx entra de sola para detonar o Apache: ele ama arquivos estáticos! Como os objetos da sua aplicação estarão todos cacheados após os primeiros acessos, as cargas de páginas vão voar. Se a sua aplicação não for monstruosa de grande, sua infra pode ser até ser legal com você e cachear em memória. Aí o vôo passa a ser supersônico.

O povo da infra também vai adorar saber que eles terão um console administrativo de onde eles podem controlar e observar todo o comportamento do PageSpeed Module em tempo real. O povo da infra adora coisas assim.

PageSpeed Console. Note a quantidade de opções no
menuzinho.

A única coisa que o PageSpeed Module estranhamente não faz: miniaturizar HTML. Mas para isso há uma solução levíssima para ser embutida no nginx, o HttpStripModule.

Só um aviso: na data em que escrevi isso, o PageSpeed Module está em beta e o HttpStripModule em alpha. Use com cuidado e se você falar que foi eu que recomendou, eu desminto, seu mentiroso! Eu uso em produção, mas eu sou um animal.

Vários continentes, milhares de servidores {#vários-continentes-milhares-de-servidores}

Sua empresa tem 2 funcionários? Foda-se! Você pode contar com uma estrutura com milhares de servidores globais para entregar o seu site. O nome disso é CDN, de Content Delivery Network.

Acredite: isso faz uma diferença enorme no tempo de carga das páginas do seu site. Quando o usuário acessa um site numa CDN, a maior parte do conteúdo estático do site (CSS, javascript, imagens, html) será entregue pelo servidor mais próximo do usuário. Isso muda vidas!

E caso você use hospedagem fora do Brasil por ser ridiculamente mais barato do que aqui, uma CDN ajudará muito a experiência dos usuários que acessarem o site que você fez. Como o conteúdo sempre é entregue pelo servidor da CDN mais próximo do usuário (em termos lógicos, e não físicos), o conteúdo do seu site virá de um servidor no Brasil, o que diminui muito a latência e, por consequência, melhora a experiência do usuário.

O mais famoso e antigo provedor de CDN é a Akamai. Mas corra deles, caros pra cacete e sofrem da síndrome de Apple: cobram pela marca mais do que pelo que entregam. A Akamai inventou o conceito de que CDN é caro.

Há opções muito em conta hoje em dia e que funcionam tão bem quanto a Akamai para a maioria dos casos. Aqui tem uma boa lista de quem provê o serviço e os links levam para uma visão geral que contém os recursos e os preços de cada um.

Como eu uso os serviços do AWS, utilizar o CloudFront foi apenas natural. Poderia ser outra também.

Criar e habilitar uma 'distribuição' não levou mais do que alguns minutos. Configurar o DNS e servidor também foi rápido. Vou mostrar os arquivos de configuração mais abaixo.

Mas aí esbarrei em um problema. Em desenvolvimento, o MexApi tem as URLs dos objetos apontando para o servidor local, mexapi.local. Quando envio para o Slackware no AWS, a URL precisa ser mexapi.macpress.com.br. Mas os arquivos estáticos precisam vir da CDN, cuja URL é dt28kr05zbdwq.cloudfront.net. #comofaz?

Pois é! Essa dúvida queimou minha pestana um pouco. Qual seria o melhor e mais elegante modo de fazer isso? Como no meio desse processo também atualizei o MexApi para o Pelican 3.5, achei que ele seria o lugar certo para isso. Cheguei até a abordar o tema com o pessoal do Pelican, mas enquanto eu escrevia fui tendo os insights.

E o melhor deles foi: o PageSpeed Module também mata essa charada! Propriamente configurado, ele reescreve as URLs dos objetos otimizados em tempo real quando os processa e depois cacheia os resultados. A partir daí é só alegria. O que era mexapi.macpress.com.br virou dt28kr05zbdwq.cloudfront.net. Não precisei alterar aplicação, fazer configurações obscuras ou malabarismo algum.

Show me the code

Certo, certo! Vejamos o código.

Você vai precisar do nginx compilado com o PageSpeed Module e o HttpStripModule. Se você está no Slackware, facilitarei a sua vida. Fiz um SlackBuild pra você compilar tudo num tapa só.

As alterações requeridas no código do MexApi você pode ver no commit log. Procure os commits que contém PageSpeed optimizations e CDN optimizations. Houveram algumas idas e voltas enquanto eu testava, mas no fim é bem pouca coisa que muda mesmo e a maioria é removendo coisas que faziam o Pelican otimizar o código, tarefa que agora é do nginx com PageSpeed Module.

E agora as configurações do nginx e do PageSpeed. Alterei algumas coisas para não expor o meu ambiente de produção, mas nada que comprometa o seu entendimento.

### /etc/nginx/nginx.conf
user  nobody;
worker_processes  1;

events {
    use                epoll;  # Important on Linux for performance.
    worker_connections 256;    # 256 concurrent connections. I won't need more. Default: 1024.
    accept_mutex       off;    # Turn this 'on' if worker_processes > 1.
                              #  No need for locking mutex on single worker, thus servers accepts faster.
}

http {
    ### General options
    charset                   utf-8;
    default_type              application/octet-stream;
    ignore_invalid_headers    on;
    include                   mime.types;
    include                   pagespeed.global;
    keepalive_requests        20;       # Default: 100.
    max_ranges                0;        # Disabled range headers. We only serve very small static files.
    keepalive_timeout         300 300;  # Default 65; made this bigger because creating new TCP connections.
                                        #  is "expensive" and this helps to speed things along because open.
                                        #  TCP connections can be re-used more often.
    output_buffers            1 256k;   # Use output buffer of 256kB, only use for sendfile=off.
    recursive_error_pages     on;       # Allows the use of "error_pages" directive.
    reset_timedout_connection on;       # Close stale connections
    sendfile                  on;       # sendfile(2) allows to transfer data from a file descriptor to another directly in kernel space.
    sendfile_max_chunk        512K;     # Only used when sendfile=on; Set accordingly, see documentation.
    server_tokens             off;      # Disables showing the nginx version number in auto generated error pages.
    tcp_nodelay               on;       # Disable Nagle buffering algoritm. Helps sending small objects quickly.
    tcp_nopush                on;       # Default: off; Cannot be used without sendfile() anyway.

    ### Limit requests
    # Works together with the "limit" in the server{..} directive to limit requests per IP.
    limit_req_zone $binary_remote_addr zone=blog:1m rate=60r/m;

    ### Compression
    gzip                      on;       # On-the-fly compression. Static HTML and CSS compress well.
                                        #  Create static .gz files later to save CPU cycles.
                                        #  Compressing on-the-fly also adds delay!
    gzip_comp_level           5;        # From 1-9, 9 being the maximum compression.
    gzip_http_version         1.1;      # Set this to 1.1 to make it compatible with AWS CloudFront compressed requests.
    gzip_proxied              any;      # Let CloudFront get the compressed assets.
    gzip_min_length           150;      # Compress files larger than 860 bytes. Disable when using gzip_static.
                                        #  Akamai uses 860 bytes as minimum to start compressing.
                                        #  Google recommends starting at 150 bytes.
    gzip_vary                 on;       # Let clients know we can compress data.
    gzip_types application/ecmascript application/javascript application/json application/pdf application/postscript application/x-javascript;
    gzip_types image/svg+xml;
    gzip_types text/css text/csv text/javascript text/plain text/xml;

    ### Log formatting
    log_format  main  '$remote_addr $host $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" $request_time $gzip_ratio';

    ### Servers
    ## Default server
    server {
        listen                80 rcvbuf=1k sndbuf=128k backlog=12;
        server_name           localhost;

        location / {
            root              /var/www/html;
            index             index.html;
        }

        # Include per server pagespeed settings
        include               pagespeed.server;

        # Redirect not found pages to the static page /404.html
        error_page            404 /404.html;

        # Redirect server error pages to the static page /50x.html
        error_page            500 502 503 504 /50x.html;
        location = /50x.html {
            root              /var/www/html;
        }
    }

    include /etc/nginx/conf.d/*.conf;
}
### /etc/nginx/pagespeed.global
# Cache directory and limits
pagespeed FileCachePath /dev/shm/cache;
pagespeed FileCacheSizeKb 51200;
pagespeed FileCacheCleanIntervalMs 2592000;
pagespeed FileCacheInodeLimit 200000;

# In-memory LRU cache limits
pagespeed LRUCacheKbPerProcess     8192;
pagespeed LRUCacheByteLimit        16384;

# Enable statistics per virtual host
pagespeed UsePerVhostStatistics on;

# Set X-Page-Speed header value
pagespeed XHeaderValue "ngx_pagespeed";

# Enable CoreFilters
pagespeed RewriteLevel CoreFilters;

# Enable additional filter(s) selectively
pagespeed EnableFilters collapse_whitespace;
pagespeed EnableFilters lazyload_images;
pagespeed EnableFilters inline_google_font_css;
pagespeed EnableFilters insert_dns_prefetch;
pagespeed EnableFilters rewrite_domains;

# Native URL fetcher
pagespeed UseNativeFetcher on;
resolver 8.8.8.8;

# Authorized rewrite domains
pagespeed Domain *.example.com;
pagespeed Domain d1234.cloudfront.net;

# Fetching Resources using Gzip
pagespeed FetchWithGzip on;

# Admin console
pagespeed AdminPath /pagespeed_admin;

# Admin settings
pagespeed EnableCachePurge on;
pagespeed PurgeMethod PURGE;
pagespeed MessageBufferSize 102400;
pagespeed Statistics on;
pagespeed StatisticsLogging on;
pagespeed LogDir /var/log/pagespeed;
pagespeed StatisticsLoggingIntervalMs 60000;
pagespeed StatisticsLoggingMaxFileSizeKb 1024;
### /etc/nginx/pagespeed.local
# Enable HttpStripModule
strip on;

# Enable ngx_pagespeed
pagespeed on;

# Set basic auth to pagespeed admin
location /pagespeed_admin/ {
    auth_basic "private";
    auth_basic_user_file /etc/nginx/passwords;
}

# Ensure requests for pagespeed optimized resources go to the
# pagespeed handler and no extraneous headers get set.
location ~ "\.pagespeed\.([a-z]\.)?[a-z]{2}\.[^.]{10}\.[^.]+" {
    add_header "" "";
}
location ~ "^/pagespeed_static/" { }
location ~ "^/ngx_pagespeed_beacon$" { }
### /etc/nginx/conf.d/vhost
server {
    server_name www.example.com origin.macpress.com;
    location / {
        root     /var/www/example;                       # root from where nginx will serve files.
        index    index.html;
    }

    access_log  /var/log/nginx/access-example.log main;   # Remove buffer=32k when debugging!
    error_log   /var/log/nginx/error-example.log;

    error_page  404 /404.html;

    limit_req   zone=blog burst=200 nodelay;             # Limit requests per IP.
    add_header  Cache-Control public;                    # Tell clients/proxies it's OK to cache.
    expires     max;                                     # Cache as long as possible.
    include     pagespeed.server;                        # Include PageSpeed settings
    pagespeed   MapRewriteDomain d1234.cloudfront.net *.example.com;
}

Se você estudar um pouquinho a excelente documentação do nginx e do PageSpeed Module, logo verá como cada arquivo de configuração se encaixa no outro e porque as coisas são como são.

No fim, com um pouco de estudo e mudanças mínimas de código, o resultado está naquela imagem lá de cima: 99% no PageSpeed, 98% no YSlow e menos de 1s para o santo graal chamado onload, num site em produção. Não é pra qualquer um. Nem para os grandes! Mas agora é para você e para mim.

Quero só ver US$ 1,6bi na minha conta ano que vem!