O Diferencial do PHP

Artigo sobre as diferenças da linguagem de programação PHP em relação às demais linguagens e como isso deve ser levado em consideração ao se planejar uma arquitetura de microserviços.

Introdução

Já faz um bom tempo que eu não escrevo nada neste blog e eu resolvi voltar com um artigo que não tem o mesmo intuito de ensinar algo extremamente prático sobre o mundo do PHP, mas apresentar um artigo mais reflexivo sobre o que o PHP é em essência e o que o distingue das demais linguagens utilizadas para web hoje em dia. Por fim, fazer algumas análises sobre o momento atual, em que tanto se fala de microserviços e serverless, e como o PHP está (ou não) inserido neste cenário.

O cíclo de vida de uma requisição HTTP no PHP

Bom, sendo bastante direto, o principal diferencial do PHP em relação às outras linguagens de progração (para Web) é como ele lida com o cíclo de vida de uma requisição HTTP. Mas o que isso quer dizer?

Primeiramente, é preciso destacar que, embora a linguagem PHP possa ser utilizada para outros propósitos que não seja exatamente o desenvolvimento Web (por exemplo, criação de scripts de execução no shell para realização de operações diversas), a linguagem nasceu para atender uma demanda bem específica, que é o desenvolvimento Web. Portanto, a linguagem tem um modo de lidar com requisições HTTP (base de qualquer aplicação Web) de modo muito peculiar.

O papel do Servidor HTTP

Esta peculiaridade começa com o fato de que a linguagem depende de um servidor HTTP externo (podemos citar o Nginx e o Apache HTTP Server como os mais comuns). No momento, a linguagem até oferece um servidor HTTP nativo, mas ele é oferecido puramente com o propósito de conveniência para desenvolvedores, e não é recomendado para utilização em ambientes de produção, já que possui limitações importantes.

A forma como o servidor HTTP é "ligado" ao PHP, ou seja, como eles são configurados para utilizar a linguagem PHP para realizar o processamento de requisições HTTP e devolver conteúdos dinâmicos, pode variar. Uma das forma é a instalação do PHP no servidor Apache na forma de "módulo do Apache", em que cada instância do servidor Apache que está preparada para receber/processar requisições HTTP possui o interpretador de PHP embutido. Embora esta forma de utilização seja relativamente comum em sistemas pequenos, ela costuma ser ineficáz para sistemas maiores. Isso porque o servidor HTTP deixa N instâncias disponíveis para responder requisições web e todas elas "carregam" o interpretador de PHP dentro de si, portanto, ocupando memória RAM. O problema disso é que quando estamos falando de uma aplicação Web, nem sempre a requisição recebida é para um arquivo que precisa ser executado pelo PHP para produzir uma resposta dinâmica. Por exemplo, quando o servidor HTTP recebe uma requisição para devolver uma imagem PNG, a instância do servidor HTTP estará "colada" ao módulo do PHP, mesmo que ele não seja utilizado, já que, normalmente, uma imagem PNG é estática e só precisa ser devolvida como qualquer outro arquivo estático. Com isso, durante a resposta das requisições por arquivos estáticos, uma instância que poderia estar processando algum script PHP foi, de certo modo, desperdiçada para resolver algo trivial, e que não utiliza o PHP. Como o número de instâncias do servidor HTTP é limitada, isso pode se tornar um problema quando o servidor está com um número de acessos extremamente alto, pois acaba utilizando instâncias que respondem por arquivos estáticos ao invés de utilizá-los para processar arquivos PHP. Para exemplificar, vamos dizer que cada instância do servidor HTTP com o módulo do PHP ocupe 500MB de memória e, com isso, o servidor físico só consiga manter 10 instâncias do serviço de HTTP disponíveis por vez (ou seja, serão 5000MB ou 5GB de memória utilizada apenas pelo serviço de HTTP). Se cada instância só processa uma única requisição por vez, o servidor físico só conseguiria atender 10 requisições HTTP simultâneas (incluindo arquivos estáticos e dinâmicos).

A outra forma comum de configuração do PHP é como "FastCGI Process Manager" (FPM). Neste caso, o servidor HTTP carrega apenas os módulos que necessita para responder pelas requisições HTTP por arquivos estáticos, mas é configurado para delegar a requisição a um outro serviço (o FPM do PHP), apenas se a requisição for identificada com alguma característica que exija o uso do PHP. Por exemplo, configurar o servidor HTTP para que, quando receber uma requisição para um arquivo com a extensão .php, então a requisição é delegada ao FPM. Com isso, o servidor HTTP fica com um número de instâncias X a disposição para responder pelas requisições HTTP, e o servidor FPM fica com um número de instâncias Y a disposição para processar requisições que exigem o PHP. Logo, se uma imagem estática for requisitada ao servidor HTTP, ele só irá devolver a imagem para o solicitante, sem necessidade de incomodar o FPM, já que não envolve nenhum processamento dinâmico com PHP. Além disso, a instância do servidor HTTP ocupa muito menos memória, já que não possui o interpretador do PHP embutido nela. Neste sentido, a utilização dos recursos computacionais fica mais eficiente na medida em que utiliza o interpretador do PHP apenas quando realmente é necessário. Num exemplo mais prático, poderiamos ter um servidor físico com 20 instâncias que respondem pelo serviço de HTTP e cada uma requer, por exemplo, 200MB e também ter 10 instâncias do serviço FPM e cada uma requer, por exemplo, 100MB. Com isso, teríamos um total de 5GB (200MB × 20 + 100MB × 10) de uso de memória total para serviço de HTTP + FPM. É um valor exatamente igual ao do exemplo anterior, mas agora teríamos 20 instâncias disponíveis para requisições HTTP. Aquelas que precisam do PHP iriam utilizar o FPM para processar, mas aquelas que não precisam só iriam devolver os arquivos estáticos.

O script PHP

Agora que já comentamos sobre as duas principais formas de se preparar o interpretador do PHP para processar requisições HTTP, que são recebidas por um servidor HTTP externo, podemos comentar mais a fundo sobre o principal diferencial da linguagem PHP para responder requisições HTTP, que está diretamente relacionada à forma como as aplicações PHP são construidas/organizadas em arquivos.

Cada arquivo PHP pode incluir outros arquivos, seja explicitamente com include, require, include_once ou require_once), seja de forma semi-automática, através da configuração de um autoloader via função __autoload ou spl_autoload_register. Porém, toda requisição HTTP que é delegada pelo servidor HTTP para ser interpretada pelo PHP inicia o processamento através de um único arquivo inicial e este arquivo inicial é que pode (ou não) incluir vários outros arquivos PHP em cascata.

Portanto, o principal diferencial do PHP em relação às outras linguagens pra Web é que o PHP só executa os script sob demanda. Isso significa que uma requisição HTTP é encaminhada para processamento por um script PHP, ele é interpretado (carregado na memória e executado), produz uma resposta HTTP e em seguida morre (é desalocado da memória). Portanto, a aplicação PHP não fica toda alocada na memória, assim como um programa convencional fica na memória enquanto é executado (na forma de um processo). Quem fica na memória é: ou a instância do servidor HTTP, que tem o PHP embutido como módulo, ou a instância do servidor FPM (que é só um interpretador de PHP). Quando digo que a aplicação não fica em memória, é óbvio que não quero dizer que ela nunca vai pra memória. Quero dizer que as instruções da aplicação só são carregadas e executadas em memória durante o ciclo de vida de uma requisição HTTP e que somente serão executadas as instruções que estiverem no script inicial ou que foram carregados sob demanda. Isso quer dizer que se você tem uma aplicação PHP composta por 1000 arquivos PHP: (1) você não terá os 1000 arquivos PHP alocados em memória e prontos para serem executados e (2) você também não irá executar os 1000 arquivos toda vez que uma requisição HTTP for delegada para execução pelo interpretador de PHP. O que acontece é que um conjunto destes 1000 arquivos serão interpretados (conjunto este definido pelos includes explícitos ou implicitos), de acordo com dados de uma requisição HTTP, e que estas instruções que foram executadas em memória saem da memória assim que a requisição é completamente processada.

Por isso que quando estamos na linguagem PHP e um script acessa $_POST ou $_GET, esta variável contém dados de uma requisição específica. Além disso, os valores das variáveis são destruídos quando o script encerra o processamento. A única forma de se recuperar um valor através de várias requisições diferentes é utilizando algum armazenamento externo, como por exemplo sessões, em que o cliente envia um ID de sessão nas requisições subsequêntes à requisição inicial em que criou os dados em sessão.

Outras linguagens/ambientes como Go, Python, Java e Node.JS normalmente tem uma abordagem em que uma aplicação é completamente carregada em memória, na forma de um processo, e este processo é responsável tanto pelo serviço de HTTP (receber requisições/despachar respostas) quanto pelas regras de negócio da aplicação. E, normalmente, se você tem 1000 arquivos de código-fonte nestas linguagens, todos eles serão compilados ou interpretados, e formarão o processo que é composto de um loop infinito que fica esperando receber requisições HTTP para serem processados. Este loop pode ser capaz de processar requisições em sequência ou múltiplas requisições paralelamente conforme as capacidades da linguagem ou da forma como elas foram utilizadas. Existe também um abordagem híbrida em que parte do código-fonte é interpretado e fica em memória, mas parte só é interpretado sob demanda.

Nesta abordagem de único processo respondendo por qualquer requisição HTTP, existem vantagens de desvantagens. Uma vantagem é que o processo fica vivo em memória, então a performance tende a ser melhor, já que não é como um interpretador do PHP que precisa interpretar o código-fonte, executar as instruções e desalocar as instruções para cada requisição. Além disso, o processo pode tirar vantagem de estar vivo em memória para deixar algumas informações pré-carregadas na memória, como por exemplo algum cache utilizando um array (que guarde os dados em memória). E uma possível desvantagem é que o processo pode ficar muito grande e, consequentemente, ocupar muita memória para responder pelas requisições HTTP. Por isso, a abordagem de microserviços parece fazer bastante sentido nestas linguagens, já que a idéia é que os microserviços não fiquem tão grandes, logo, os processos também não ficam tão grandes. Assim, cada microserviço fica escalável individualmente, fazendo com que alqueles que demandem mais uso tenham mais instâncias (ou instâncias mais robustas para atender o serviço).

Conclusão

Com tudo isso, o que quero destacar é que o grande diferencial do PHP é a sua capacidade de ter aplicações gigantes e, mesmo assim, só utilizar os recursos computacionais sob demanda, ou seja, carregar em memória um conjunto de scripts PHP para cada requisição, depois desalocá-los quando a requisição já foi processada. Infelizmente, também é notável que a maioria dos frameworks PHP parecem não ter feito uma grande reflexão sobre esta característica do PHP e acabam carregando vários recursos (classes, interfaces e funções), mesmo eles não sendo utilizados para todas requisições que chegam à aplicação. Depois de anos trabalhando com PHP pude perceber o quanto a organização do código com o Paradigma Orientado a Objetos no PHP pode, inclusive, ser prejudicial no PHP. Isso porque o custo para se carregar novos arquivos PHP, instanciar classes e definir restrições com interfaces pode resultar em um custo de processamento no PHP que não justifica tamanha complexidade. Além disso, normalmente os frameworks lêem arquivos de configurações enormes no início do processamento de todas as requisições, mesmo que nem todas elas acessem todas estas configurações. Enfim, a linguagem foi moldada para ser simples e direta, mas o equilíbrio para manter o código saudável (testável e de fácil manutenção) precisa ser avaliado com cuidado, pois também pode causar uma perda de performance desnecessária por conta desta característica tão elementar da linguagem PHP. Acredito, portanto, que os frameworks de PHP deveriam ter uma estrutura muito diferente das estruturas dos frameworks de linguagens que seguem a abordagem do processo único com toda aplicação em memória.

Além disso, nos dias de hoje em que escrevo este artigo (2019, praticamente 2020) há uma forte tendência/foco para essas duas abordagens: microserviços e serverless. Por um lado, e de forma bem resumida, a arquitetura de microserviços é a organização de aplicações de modo que conteúdos relacionados sejam agrupados em microserviços e que estes podem se comunicar entre si através de requisições ou mensageria, e os principais benefícios disso é que cada microserviço está desacoplado do outro, podendo ser publicado/testado de forma independente, além de poder ser instalado em instâncias na nuvem para que escalem sob demanda, de forma eficiênte. Já o serverless diz respeito exclusivamente ao aproveitamento eficiênte de recursos na nuvem, através da inicialização e uso de recursos sob demanda. Por exemplo, configurar um serviço de Lambda na AWS, nada mais é que utilizar uma linguagem de programação (como Go, Python, Node.JS) para se comportar de forma similar ao que o PHP já é nativamente. Ou seja, através de algum gatilho, que pode ser uma requisição HTTP detectada pelo serviço da AWS API Gateway, executar uma função com escopo bem delimitado, não incluindo, portanto, vários arquivos de código-fonte, mas apenas um conjunto estritamente necessário para responder a requisição.

Portanto, quando se fala de microserviços e serverless, deve-se tomar muito cuidado ao utilizar PHP pois, eventualmente, você estará utilizando estas abordagens com a linguagem PHP apenas por modinha ou por desconhecimento. Não quero dizer que não faça sentido seguir uma abordagem de microserviços quando se usa PHP, mas que é preciso fazer uma reflexão profunda sobre o que se espera ganhar com a abordagem de microserviços e que, talvez, a pura organização do código PHP (e configuração de como eles são publicados) seria suficiente atingir estes objetivos.

2 comentários

Anônimo disse...

Nossa muito BOA as INFORMAÇÕES sobre o COMPORTAMENTO do PHP em Relação do FUNIONAMENTO das REQUISIÇÕES.Obrigado por COMPARTILHAR as INFORMAÇÕES.