Back-End

16 mai, 2016

PHP Session Locking: como evitar bloqueio de sessões nas solicitações PHP

348 visualizações
Publicidade

O que descobri quando se fala sobre o bloqueio de sessão no PHP é que metade das minhas conversas gira em torno de “oh, eu me lembro da sessão de bloqueio do PHP, que custou alguns dias da minha vida, mas melhorou seriamente o desempenho das minhas aplicações quando eu as evito”, ou a outra ponta “o que diabos é bloqueio de sessão PHP?”.

Especificamente, os mecanismos de bloqueio envolvidos em sessões do PHP não são claros para todos e podem causar lentidão nos aplicativos se você não levá-los em conta.

Isso não tem que ser um problema. Se você está ciente do que se passa nos bastidores, você pode antecipar esse comportamento em suas aplicações PHP e evitá-lo completamente.

O que acontece quando você chama session_start()

Vamos pegar uma configuração básica PHP como exemplo: sempre que você iniciar uma sessão PHP, o PHP irá criar um arquivo simples no caminho session.save_path, que é padrão para /var/lib/php/session. Todos os dados da sessão são armazenados lá.

Se o usuário ainda não tiver um cookie de sessão, uma nova ID será gerada e o usuário receberá um cookie configurado em sua máquina. Se é um usuário que está retornando, ele vai enviar sua ID do cookie para o servidor, o PHP irá analisá-lo e carregar os dados correspondentes de session.save_path.

Simplificando, isso é session_start().

Bloqueios de sessão e concorrência

Em um exemplo um pouco mais completo, aqui está o que acontece nos bastidores quando uma sessão é iniciada no PHP.

Cronometragem Código PHP Linux/Servidor
0ms session_start(); Bloqueio de arquivo criado em /var/lib/php/session/sess_ $identifier
15ms Consultas SQL, para loops, chamadas de API de terceiros Bloqueio de arquivo no resto da sessão
350ms Término do script PHP Bloqueio de arquivo é removido do arquivo de sessão em /var/lib/php/sess_$identifier

Sempre que iniciar session_start() (ou quando session.auto_start do PHP é definido como verdadeiro, ele irá fazer isso automaticamente em cada script PHP), o sistema operacional irá bloquear o arquivo da sessão. A maioria das implementações usa flock, que também é usado para evitar o overlap de cronjobs ou outros bloqueios de arquivos no Linux.

De uma máquina Linux, o bloqueio na sessão se parece com isto:

$ fuser /var/lib/php/session/sess_cdmtgg3noi8fb6j2vqkaai9ff5
/var/lib/php/session/sess_cdmtgg3noi8fb6j2vqkaai9ff5:  2768  2769  2770

fuser relata os 3 PIDs de processos que tem esse arquivo bloqueado ou está aguardando liberação.

$ lsof /var/lib/php/session/sess_cdmtgg3noi8fb6j2vqkaai9ff5
COMMAND  PID      USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
php-fpm 2769 http_demo    5uW  REG  253,1        0 655415 sess_cdmtgg3noi8fb6j2vqkaai9ff5

lsof relata o PID e o comando que mantém o bloqueio atual.

Esse bloqueio é mantido no arquivo até que o script termine ou que o bloqueio seja  propositadamente removido (mais sobre isso abaixo). Isso age como um bloqueio de leitura e escrita: cada tentativa de ler a sessão vai ter que esperar até que o bloqueio seja liberado.

O bloqueio em si não é um problema. É uma salvaguarda para evitar gravações no arquivo da sessão de vários locais diferentes, possivelmente corrompendo dados ou removendo dados anteriores.

Isso se torna um problema quando um segundo script PHP, concorrente, tenta acessar a mesma sessão PHP.

Cronometragem script 1 Linux/Servidor script 2
0ms session_start(); Bloqueio de arquivo (flock) em /var/lib/php/session/sess_$identifier definido pelo script 1 session_start(); é chamado, mas está bloqueando o bloqueio de arquivo existente. Este é o lugar onde o PHP apenas “espera” que o bloqueio seja removido
15ms Consultas SQL, para loops, chamadas de API de terceiros Bloqueio de arquivo no resto da sessão Este script ainda está esperando, sem fazer nada
350ms Término do script 1 Bloqueio de arquivo do script 1 é removido Script 2 ainda está esperando…
360ms Script 2 recebe o novo bloqueio de arquivo Script 2 agora pode fazer as suas consultas SQL, loops
700ms Bloqueio de arquivo removido do script 2 Término do script 2

No caso de a tabela acima não ser muito clara:

  • Quando dois arquivos PHP tentam iniciar uma sessão ao mesmo tempo, apenas um “ganha” e obtém o bloqueio. O outro tem de esperar.
  • Enquanto espera, ele não está fazendo nada: session_start() está bloqueando todas as execuções futuras.
  • Assim que o bloqueio do primeiro script é removido, o segundo script pode continuar, recebendo o bloqueio.

Na maioria dos cenários, isso faz o PHP se comportar como um conjunto de scripts síncronos para o mesmo usuário: um é executado após o outro, não há solicitações paralelas, mesmo se você tentar chamar esses arquivos PHP pelo AJAX.

Então, em vez de ter ambos os scripts terminando em torno de 350ms, o primeiro script termina em 350ms, mas o segundo leva o dobro do tempo (700ms) porque teve que esperar o primeiro ser concluído.

Manipuladores de sessão alternativos: Redis, memcached, mysql

Se você está procurando uma solução rápida e pensar “Eu vou guardar minhas sessões em memcached”, vai ficar desapontado. A configuração padrão do memcached usa a mesma lógica de segurança descrita acima: sessões serão bloqueadas assim que uma chamada PHP usá-las.

Se você estiver usando a extensão Memcached do PHP, pode definir o memcached.sess_locking para ‘off’ para evitar bloqueios de sessão. O valor padrão é ‘on’, o que o faz agir como o manipulador de sessão normal.

Se você estiver usando Redis, você está com “sorte” (*), pois o manipulador de sessão do Redis ainda não suporta bloqueio. Com Redis como um backend de armazenamento de sessão, não haverá bloqueios.

Se você estiver usando o MySQL como backend de sessão (como o Drupal faz), você tem uma implementação personalizada: não há nenhuma extensão padrão do PHP que permita usar o MySQL como armazenamento de sessão. Em algum lugar do seu código PHP, há uma chamada de função para session_set_save_handler() que descreve qual classe ou método é responsável por ler e escrever seus dados de sessão. Isso significa que a sua aplicação decide se as sessões serão bloqueadas ou não.

(*)ver abaixo

Bloqueio de sessão PHP: o problema que está tentando corrigir

Pode parecer que estou sendo excessivamente negativo sobre esse comportamento, mas eu não estou. Você deve apenas estar ciente do comportamento. Também é uma coisa boa ele existir.

Imaginem o seguinte cenário. Isso mostra onde isso pode dar errado. Se não existisse tal coisa como um “bloqueio de sessão”, isto que aconteceria se dois scripts fossem executados ao mesmo tempo nos mesmos dados da sessão:

Cronometragem script 1 script 2
0ms session_start();
Os dados da sessão estão agora em $_SESSION
session_start(); Os dados da sessão estão agora em $_SESSION
15ms Script 1 grava dados na sessão: $_SESSION [ ‘Payment_id’] = 1; Script 2 também grava dados na sessão: $_SESSION [ ‘Payment_id’] = 5;
350ms sleep(1); Script termina, salvando os dados em $_SESSION.
450ms Script termina, salvando seus dados em $_SESSION.
Qual valor está na sessão?  O valor do script 1. Os dados armazenados pelo script 2 são substituídos pela última gravação, realizada pelo script 1.

Esse é um bug de concorrência muito estranho e difícil de solucionar. A sessão de bloqueio impede isso.

Mas você pode não querer isso. É principalmente um problema quando se grava dados na sessão. Se você tem um script PHP que só lê os dados da sessão (como um monte de chamadas de AJAX faria), você pode ler os dados com segurança várias vezes.

Por outro lado, se você tiver um script de longa duração que lê os dados da sessão e altera os valores, mas um segundo script se inicia e lê os dados velhos e obsoletos, isso também pode causar problemas na sua aplicação.

Fechando o bloqueio de sessão: PHP 5.x e PHP 7

No PHP, há uma função chamada session_write_close(). Ela faz o que seu nome sugere: grava os dados da sessão e fecha o arquivo, removendo assim o bloqueio. Em seu código PHP, você pode usar assim.

<?php
...
// Isso funciona no PHP 5.x e PHP 7
session_start();

$_SESSION['something'] = 'foo';
$_SESSION['yolo'] = 'swag';

session_write_close();

// Faça o resto da sua execução PHP abaixo

O exemplo acima abre a sessão (o que lê os dados da sessão em $_SESSION), escreve os dados nela e fecha o bloqueio. De agora em diante, ele não pode gravar mais no mesmo arquivo de sessão. Se mais manipulações em $_SESSION acontecerem na sequência do script, essas alterações não serão salvas.

A partir do PHP 7, contudo, você pode definir opções adicionais quando chamar session_start();.

<?php
session_start([
    'read_and_close' => true,
]);
?>

Isso é o equivalente aproximado de:

<?php
session_start();

session_write_close();
?>

Ele lê os dados da sessão e imediatamente libera o bloqueio para que outros scripts não fiquem bloqueados de usá-lo.

Demonstração: quão lento é lento?

Nada bate uma boa demonstração para mostrar esse comportamento. Eu configurei um repositório GitHub bem simples que mostra esse comportamento.

É simples e feio, mas mostra o que se passa: demo.ma.ttias.be/demo-php-blocking-sessions.

blocking_nonblocking_php_session_calls

Se você tiver sido pego por esses bloqueios de sessão PHP, deixa-me saber. Se você encontrou uma forma original ou solução alternativa para resolver o seu problema em particular, eu ficaria ainda mais interessado!

***

Mattias Geniar faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://ma.ttias.be/php-session-locking-prevent-sessions-blocking-in-requests/