Vou contar um pouco sobre o que é o PHP Swoole e como foi utiliza-lo para desenvolver um microserviço de websocket escalável e totalmente integrado com os serviços da Amazon Web Services (AWS).
Por que Websocket em PHP?
Escutei este pergunta em diversos momentos, até mesmo pela própria equipe onde trabalho. Já haviamos desenvolvido um websocket uitilizando Ratchet, mas era um escopo muito menor e ficou claro alguns problemas que teríamos em um escopo maior. Neste novo desafio precisavamos de uma biblioteca muito mais robusta, escalável e com mais ferramentas a disposição.
Na fase de planejamento do projeto testei diversos websockets em diferentes linguagens, mas acabei escolhendo o PHP Swoole, pois ele fornecia coroutine, velocidade, escalabilidade e mais um pouco.
A confiança em utilizar o Websocket em PHP, algo que dificilmente a comunidade recomendaria, foram os testes de carga, o conhecimento da linguagem na empresa e a facilidade de outras pessoas assumirem ou fornecerem suporte ao projeto.
Benchmark
No final do artigo Introdução ao swoole, podemos ver um benchmark por números de requisições entre os seguintes cenários: PHP puro, NodeJs, Go e PHP Swoole.
PHP Puro (882 requisições por segundo)
Running 10s test @ http://127.0.0.1:8101
4 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 3.46ms 9.56ms 141.27ms 98.92%
Req/Sec 1.41k 1.70k 9.47k 85.45%
8871 requests in 10.05s, 1.47MB read
Socket errors: connect 0, read 9275, write 0, timeout 0
Requests/sec: 882.46
Transfer/sec: 149.95KB
NodeJS (49.720 requisições por segundo)
Running 10s test @ http://127.0.0.1:8101
4 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 4.05ms 1.09ms 43.57ms 98.31%
Req/Sec 12.49k 1.46k 13.49k 97.03%
502227 requests in 10.10s, 53.16MB read
Socket errors: connect 0, read 110, write 0, timeout 0
Requests/sec: 49720.54
Transfer/sec: 5.26MB
GO (187.280 requisições por segundo)
Running 10s test @ http://127.0.0.1:8101
4 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.05ms 624.03us 42.09ms 92.46%
Req/Sec 47.05k 2.31k 50.59k 95.50%
1873010 requests in 10.00s, 228.64MB read
Socket errors: connect 0, read 48, write 0, timeout 0
Requests/sec: 187280.40
Transfer/sec: 22.86MB
PHP Swoole (193.149 requisições por segundo)
Running 10s test @ http://127.0.0.1:8101
4 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 0.87ms 660.42us 42.95ms 98.60%
Req/Sec 48.55k 3.12k 53.67k 88.75%
1933132 requests in 10.01s, 304.19MB read
Socket errors: connect 0, read 41, write 0, timeout 0
Requests/sec: 193149.91
Transfer/sec: 30.39MB
Mas o que é o PHP Swoole
O Swoole é um Event-Driven, assíncrono e baseado em corotina, com alto desempenho escrito em C e C++ para PHP.
Uma extensão PHP, que permite escrever serviços de alto desempenho, escaláveis e simultâneos, TCP, UDP, Unix socket, HTTP, WebSocket , sem muito conhecimento sobre non-blocking I/O e Linux Kernel de baixo nível.
Como funciona o PHP Swoole?
O Swoole funciona diferente do modelo tradicional do PHP, ele é executado no modo CLI.
Master: É processo principal, ele forka o Main Reactor e o Manager, é o processo raiz de toda a aplicação.
Main Reactor: Thread principal, gerencia e faz o balanceamento entre os reactors auxiliares.
Reactor: Multi-thread e totalmente assíncrono, responsável por receber solicitações e entregar ao Manager.
Manager: Processo gerenciador, forka e gerencia os workers.
Worker: É aqui que você realmente deve se preocupar. Onde realmente as tarefas são executadas.
Task Worker: São auxiliáres dos Workers, bastante utilizado para tarefas paralelas e não bloqueantes ao worker.
Algumas diferenças entre o Swoole e o PHP-FPM são:
O Swoole forka um determinado número de workers baseado na quantidade de núcleos da CPU, para utilizar todos os núcleos da CPU.
O Swoole suporta conexões de longa duração para servidor websocket ou servidor TCP / UDP.
Swoole suporta várias requisições ao mesmo tempo (não bloqueantes).
Swoole pode gerenciar e reutilizar o status na memória.
Para um entendimento mais avançado de como o PHP Swoole funciona, mesmo não tratando de Websocket, eu recomendo a leitura do artigo disponibilizado em Benchmark.
Instalando PHP Swoole
Instalação básica
pecl install swoole
Instalação Recomendada
git clone https://github.com/swoole/swoole-src.git
cd swoole-src
git checkout v4.4.12
phpize
./configure
make && make install
Criando Websocket Server
Exemplo básico da construção de um websocket server com 3 eventos (open, mensage, close)
<?php
$server = new swoole_websocket_server("0.0.0.0", 9502, SWOOLE_PROCESS);
$server->set(array(
'task_worker_num' => 10,
'log_file' => '/var/log/supervisor/swoole.log',
'open_tcp_keepalive' => true
));
$server->on('open', function($server, $request)
{
echo "connection open: {$req->fd}\n";
});
$server->on('message', function($server, $frame)
{
echo "received message: {$frame->data}\n";
foreach ($server->connections as $fd) {
$server->push($fd, $frame->data);
}
});
$server->on('close', function($server, $fd)
{
echo "connection close: {$fd}\n";
});
$server->on('task', function($server, $fd)
{
echo "Task";
});
$server->start();
A variável $server é usada para construir o próprio servidor websocket.
Nela podemos definir algumas configurações de inicialização com o método ->set().
No exemplo acima inicializamos o servidor com 10 workers para task, o caminho do log e habilitamos a conexão persistente do websocket.
Utilizamos o método ->on() para mapear os eventos de callback dentro do server, no exemplo acima configuramos evento de abertura de conexão, mensagem e desconexão.
Como testar?
Você pode utilizar a extensão do Chrome chamada WS Client:
https://chrome.google.com/webstore/detail/web-socket-client/lifhekgaodigcpmnakfhaaaboididbdn
Basta colocar a URL como ws://localhost:9501 e enviar qualquer mensagem de texto. No código descrito acima será feito um broadcast, todos que estiverem conectados receberão uma cópia da mensagem.
A mensagem pode ser de qualquer tipo, mas normalmente usamos padrão Json entre os sistemas.
Memória
A memória entre os Workers não é compartilhada, já que cada worker recebe uma cópia da classe.
Mas para trabalhar com memória compartilhada entre workers existem 3 funcionalidades nativas no Swoole:
swoole_buffer, swoole_channel, swoole_table.
Ou criar uma memória compartilhada via Unix Socket, um bom exemplo é o repositório abaixo:
No caso do nosso projeto, utilizamos a memória compartilhada via Unix Socket para as listas de FDs associando a uma hash e o Redis (Utilizando o Type Hash com Lua) para gerenciamento de salas.
Outra dica importante é utilizar Ec2 focada em memória, pela maneira que o Swoole funciona.
Variáveis importantes do Callback
A variável $frame no escopo do callback possui 4 atributos importantes:
-
$frame->fd: Descritor de arquivo, é o id único de cada conexão no sistema.
- Cada aba dos clientes é um FD único associado a um hash.
- Por padrão o swoole usa um inteiro incremental iniciando de 1. Os FDs são resetados a cada vez que o server inicializa.
$frame->data: A mensagem do cliente que chegou no websocket, na imagem acima seria o “1234567”.
-
$frame->opcode: Tipo da mensagem, default é TEXT.
-
Exemplos de opcode:
- WEBSOCKET_OPCODE_TEXT = 0x1, utf8 text data;
- WEBSOCKET_OPCODE_BINARY = 0x2, binary data;
- WEBSOCKET_OPCODE_PING = 0x9, ping data;
-
$frame->finish: Retorna um boolean se o frame está completo.
A variável $server no escopo do callback possui 4 métodos/atributos importantes:
- $server->isEstablished($fd): Retorna boolean se o FD está conectado.
- $server->push($fd, $message): Envia a mensagem para o FD.
- $server->connection: Retorna um array numérico com a lista de todos os FDs conectados.
- $server->getClientInfo($fd): Retorna um array com as informações do FD fornecido.
AWS Elastic Load Balance (timeout)
Por padrão, o Elastic Load Balancing define o valor do tempo limite de inatividade para 60 segundos. Portanto, se o destino não enviar dados pelo menos a cada 60 segundos enquanto a solicitação estiver em trânsito, o load balancer poderá fechar a conexão front-end. Para garantir que operações demoradas, como uploads de arquivo, tenham tempo para serem concluídas, envie pelo menos 1 byte de dados antes de decorrer cada período de tempo limite de inatividade e aumente a duração do período do tempo limite de inatividade conforme o necessário.
Para resolver este problema, enviamos um pacote a cada 30 segundos utilizando o opcode de PING.
Este pacote não ativa o evento onMessage e mantém todas as conexões ativas.
Sem este keep_alive via ping, todos nossos clientes desconectariam e reconectariam a cada 60 segundos.
$server->on('managerStart', function($server, $fd)
{
$server->tick(30000, function () use ($server) {
foreach ($server->connections as $id) {
if ($server->isEstablished($id)) {
$server->push($id, 'ping', WEBSOCKET_OPCODE_PING);
}
}
});
}
Explicando o código acima, adicionamos no server um mapeamento para evento de callback managerStart que será executado somente 1 vez por inicialização do servidor.
Com isso adicionamos um timer de 30 segundos que vai correr a lista de todos FDs conectados no websocket, conferindo se o mesmo ainda está conectado e enviando um pacote de PING para manter a conexão aberta.
Task
Utilizamos a task por exemplo para publicar no SNS (Simple Notification Service) da AWS uma cópia de cada mensagem que é trafegada no Websocket, mas você pode usa-la para qualquer tarefa não bloqueante dentro de todos os eventos.
$server->on('managerStart', function( $server , $task_id , $from_id , $data )
{
$this->awsService->publish($data['topic'], $data['subject'], $data['payload']);
}
// Disparando manualmente uma task paralela
$server->task([
'topic' => 'message',
'subject' => 'Message',
'payload' => $frame->data
]);
Configurar a máquina para suportar 2 milhões de conexões
https://gist.github.com/mustafaturan/47268d8ad6d56cadda357e4c438f51ca
Lista de callbacks
https://www.swoole.co.uk/docs/modules/swoole-server/callback-functions
Conclusão
Podemos concluir que realmente é possível desenvolver um servidor Websocket em PHP Swoole, escalável e sem muita complexidade, batendo de frente com as linguagens mais rápidas do mercado.
O único cuidado é ao utilizar a memória compartilhada entre workers, não esqueça de remover o que não estiver sendo usado, pois o Swoole vai manter tudo em memória.
Bom, é isso. Acabei resumindo muita coisa, mas caso precisem, eu posso explicar como funciona todo o sistema de handshake e gerenciamento entre workers em outro post, ou até mesmo desenvolver um chat de exemplo para a comunidade.
Top comments (2)
hi Ronie, nice article, but i do not speak Brasil or spanish, Care to translate to english? Really appreciate it if you do!
I guess the chrome translation is your best option at the moment.