Para falar sobre como escalar em PHP, primeiro quero trazer um dado sobre a PHP. De acordo com o Stack Overflow Survey 2021, o PHP é nona linguagem mais odiada pelos desenvolvedores. E, se os desenvolvedores odeiam tanto essa linguagem, questionamos: por que ela ainda é tão utilizada? Ou, por que o EBANX, uma empresa inovadora, escolheu criar uma plataforma de pagamentos inteiramente escrita em PHP?

Bem, para a segunda pergunta a resposta é simplesmente porque nosso cofundador, João Del Valle, era programador PHP. Ele escolheu usar a linguagem que ele mais se sentia à vontade para começar o código da sua nova startup. E isso provavelmente é verdade para todas as tecnologias em todas as grandes empresas no mercado.

Porém, nós já tivemos a conversa de trocar de stack no passado, e escolhemos manter nosso codebase em PHP. Fizemos essa decisão pois acreditamos que o PHP é uma linguagem que nos permite escrever código robusto, manutenível e escalável. E é sobre este último ponto que a gente vai comentar neste artigo. Vamos comentar um pouco sobre quais as maiores dificuldades que nós encontramos em escalar nossa plataforma para acompanhar o crescimento da empresa. E vamos mostrar algumas soluções que nós encontramos.

PHP é serverless desde 1996

E é o nosso VP de Software Engineering, Kalecser Kurtz, quem costuma dizer isso.

O modelo do PHP é tão parecido com o serveless, que é quase trivial converter uma aplicação PHP para serveless. No EBANX, migramos algumas partes da aplicação para lambdas. Como por exemplo, nosso gerador de boletos. Isso nos ajuda a suportar promoções gigantes como a 11/11 e black friday onde pode haver milhares de customers tentando gerar boletos ao mesmo tempo. Rapidamente a AWS cria novos lambdas e, o melhor de tudo, todo este processamento pode ocorrer separado do resto da plataforma que está preocupada em receber os pagamentos direto dos merchants.

E sobre escalar em PHP?

O primeiro ponto é o quão fácil é escalar a maior parte das aplicações em PHP. Isso pois o modelo padrão de execução do PHP te incentiva a escrever software escalável.

A maior parte das aplicações em PHP começa com um servidor web (Apache, NGinx), que invoca um script em PHP. Este script processa a request, retorna um resultado e pode gerar algum side effect em alguma camada de persistência. Cada chamada a aplicação, é independente das outras vivendo dentro de um novo processo isolado. Este modelo é muito parecido com o novo modelo serveless. E o mais importante, é muito fácil de escalar. O desenvolvedor já programa sabendo que cada execução é independente das outras executadas. Então, não importa se as outras request estão rodando na mesma máquina, ou em outras.

No que consiste escalar em PHP?

Por isso, na maioria dos casos, escalar uma aplicação em PHP consiste em adicionar mais máquinas em baixo de um load balancer e pronto. Muito diferente de outras linguagens como Java, C# ou Go onde existe este conceito de share entre as threads. É normal existir certa dependência entre as threads e processos da mesma aplicação de modo que nestas aplicações, é normal que o melhor jeito de escalar sem precisar de reaver a arquitetura de software é aumentar o tamanho da máquina. E este approach não escala tão bem. E acredito que isso é ainda mais importante pensando que geralmente, no embrião de uma empresa, a maior preocupação não é se a minha aplicação vai escalar, mas sim se ele vai funcionar. O fundador está querendo ter o MVP dele o mais rápido possível. Escala tende a ser algo a ser pensado no futuro do negócio.

E o meu banco de dados?

Você deve ter pensado sobre isso quando eu te falei para apenas adicionar novas máquinas. Mas e o meu banco de dados? Se você está usando um banco de dados relacional tradicional, como o EBANX usa, tu não pode simplesmente adicionar mais máquinas no banco também. Claro, você pode e deve usar réplicas e masters. Separe sua aplicação de um modo que tudo aquilo que não precise fazer queries direto na master, faça na réplica. Mas existem limites a isso, principalmente falando em plataformas financeiras como a do EBANX onde não podemos nos dar ao luxo de usar consistência eventual.

Para resolver isso nós no EBANX usamos várias soluções diferentes. Mas basicamente tudo se resumiu a tentar remover ao máximo hits desnecessários da nossa base de dados transacional. Fizemos isso tanto criando outras bases de dados como modificando a aplicação para fazer mais operações de IO em bulk. O resto deste artigo vai basicamente se concentrar em nossas estratégias para conseguir aliviar nossa base de dados.

Gregor

Auditabilidade é muito importante em qualquer empresa. É importante ter ferramentas para poder te ajudar a entender o que está acontecendo com sua aplicação e se o processo está correto. Por isso, no EBANX temos umas séries de tabelas que servem puramente para guardar a história de um pagamento. Por exemplo, durante o processo de captura do pagamento, uma provedora de risco negou uma transação, pois o customer é suspeito. A aplicação pega essa informação e grava numa tabela dizendo que o pagamento foi negado por motivos de risco e é por isso que o pagamento foi cancelado.

Conforme o EBANX foi crescendo e a gente começou a trabalhar em diminuir acessos ao banco, surgiu a vontade de invez de cada request de pagamento escrever múltiplas vezes nessa tabela, como seria legal se a gente conseguisse escrever apenas uma vez múltiplas linhas para vários pagamentos.Foi assim que surgiu a ideia do Gregor.

Ferramenta in-house

O Gregor é uma ferramenta in-house e faz parte da nossa aplicação e a ideia dele é fazer escrita em bulk na base de dados. Como ele funciona? Basicamente invés de durante o request a aplicação escrever direto na base de dados a história do pagamento, ela escreve num arquivo no disco. Depois, temos um daemon escrito em PHP que pega um conjunto destes arquivos e faz um insert/update em bulk na base de dados. Com isso, a gente consegue transformar 100 inserts em apenas um. Claro que a gente fez isso criando junto uma API bem escrita, em que abstrai todo esses detalhes e no final o processo é tão simples como implementar uma interface e dar um push neste objeto para ser executado depois pelo gregor.

Uma das vantagens de usar o disco para isso é que em praticamente todos os sistemas operacionais modernos, há uma série de ferramentas que permite tratar arquivos no disco de forma atômica. Além disso, a latência de se escrever no disco é muito pequena se comparado a escrever algo pela rede, como num Redis por exemplo.

Mais bases de dados sem afetar o reliability

Conforme a gente foi mudando mais e mais processos para utilizar o Gregor, a gente percebeu que praticamente todas essas informações não eram necessárias para existir dentro da nossa base transacional. A gente poderia facilmente criar uma outra base de dados e salvar os dados lá. E isso nos permitiu retirar tabelas e acessos desnecessários da nossa base de dados principal.

Agora, tu podes fazer essa transformação sem usar algo como o Gregor. Nada impede de durante um requisição tu escrever em duas bases de dados diferentes e colher os benefícios de dividir a carga nas sua camada de persistência. Mas, um aviso, lembre-se que aplicações falham. Nós já tivemos casos de nossa base de dados falhando por causa de erro no disco no RDS. Se a tua API principal depende de duas bases de dados, se qualquer uma delas falhar, a sua aplicação vai ficar degradada ou off. Sem falar de que qualquer manutenção necessária em alguma delas pode afetar sua aplicação. Por isso valorizamos o Gregor por nos permitir fazer dividir a carga entre várias bases mas sem ficar refém de erros de todas elas. Ficamos apenas a cometer erros no disco da máquina mas que provavelmente já iriam afetar a aplicação como todo naquela maquina de qualquer forma.

E como lidar com processos data-intensive?

Nem tudo no EBANX são request de pagamentos. Temos uma série de processos financeiros que acontecem no nosso backoffice. Estes fluxos geralmente envolvem lidar com milhões de entidades e muitos deles têm limites de tempo regidos devido ao fluxo financeiro do mercado. E, um dos problemas que a gente teve que lidar assim que começamos a separar nossos dados em múltiplas bases relacionais foi como processar milhões de entidades em todas essas bases de uma forma performática e mantendo nosso código limpo.

Duas coisas que devemos sempre manter em mente quando vamos trabalhar com estes tipos de processos: bulk e chunks. Se tu precisas fechar uma transferência bancária com 2 milhões de pagamentos, tu não pode fazer um select para cada entidade. Principalmente se pra cada entidade tu precisa fazer este select em duas bases diferentes. O mesmo vale para seus inserts e updates. Valorize operações em lotes pois elas vão ser a diferença entre demorar 5 horas ou 5 minutos. E por fim, use chunks de informações para evitar consumir toda a memória da tua máquina com uma lista de entidades gigantes.

No EBANX, a gente criou uma biblioteca chamada de Stream (pois foi inspirada no Stream do Elixir) para nos ajudar a fazer transformações de grandes quantidade de dados de forma lazy e ergonômica. Com ela fica fácil fazer uma query numa base de dados, separar em chunks, fazer a query destes chunks em outra base de dados, juntar as informações e gerar um relatório, por exemplo. Ela se provou tão útil e tão utilizada dentro do nosso codebase que resolvemos abrir o código dela e hoje tu podes encontrá-la no GitHub.

Concluindo o uso de PHP

Estes foram apenas alguns dos problemas e soluções que a gente teve que lidar aqui no EBANX. Tivemos que deixar várias coisas de fora para não transformar este artigo num livro. Todavia, espero que tenhamos te convencido que é possível erguer uma unicórnio de alto crescimento usando o PHP como principal linguagem na stack. E, se quiser saber mais sobre estes e outros desafios, temos vagas abertas para engenheiros ;).

Mais sobre escalar em PHP

Você pode ouvir no episódio de novembro do Code Your Way.
Dê o play e aproveite!