Hyperf nativo da nuvem
Publicado originalmente por @Reasno em guxi.me/posts/cloudnative-hyperf O Hyperf fornece oficialmente...

Publicado originalmente por @Reasno em guxi.me/posts/cloudnative-hyperf
O Hyperf fornece oficialmente imagens de contêiner, e as opções de configuração são bem abertas. Fazer o deploy do Hyperf na nuvem em si não é complicado. Vamos usar Kubernetes como exemplo e fazer algumas modificações no pacote esqueleto padrão do Hyperf para que ele rode de forma elegante no Kubernetes. Este artigo não é uma introdução ao Kubernetes, então quem lê precisa ter algum conhecimento sobre Kubernetes.
Ciclo de vida
Depois que um contêiner é iniciado no Kubernetes, ele executa duas verificações no contêiner: Liveness Probe e Readiness Probe. Se a Liveness Probe falhar, o contêiner será reiniciado; se a Readiness Probe falhar, o serviço será removido temporariamente da lista de descoberta. Quando o Hyperf é iniciado como um servidor web HTTP, só precisamos adicionar duas rotas.
<?php
namespace App\Controller;
class HealthCheckController extends AbstractController
{
public function liveness()
{
return 'ok';
}
public function readiness()
{
return 'ok';
}
}
<?php
// in config/Routes.php
Router::addRoute(['GET', 'HEAD'], '/liveness', 'App\Controller\HealthCheckController@liveness');
Router::addRoute(['GET', 'HEAD'], '/readiness', 'App\Controller\HealthCheckController@readiness');
Configure no deployment do Kubernetes:
livenessProbe:
httpGet:
path: /liveness
port: 9501
failureThreshold: 1
periodSeconds: 10
readinessProbe:
httpGet:
path: /readiness
port: 9501
failureThreshold: 1
periodSeconds: 10
É claro que aqui simplesmente retornamos ‘ok’, o que obviamente não verifica a saúde de verdade. A inspeção real deve considerar os cenários de negócio específicos e os recursos dos quais o negócio depende. Por exemplo, para serviços que usam banco de dados intensivamente, podemos verificar o pool de conexões do banco e, se o pool estiver cheio, retornar temporariamente o status code 503 na Readiness Probe.
Quando o serviço é destruído pelo Kubernetes, ele envia primeiro o sinal SIGTERM. O processo tem esse tempo definido por terminationGracePeriodSeconds (60 segundos por padrão) para se encerrar. Se o tempo acabar, o Kubernetes enviará um sinal SIGINT para matar o processo à força. O próprio Swoole consegue responder corretamente ao SIGTERM para encerrar o serviço e, em circunstâncias normais, não perderá nenhuma conexão em andamento. Em produção, se o Swoole não responder ao SIGTERM e sair, é provável que algum timer registrado pelo servidor não tenha sido limpo. Podemos limpar o timer em OnWorkerExit para garantir um encerramento suave.
<?php
// config/autoload/server.php
// ...
'callbacks' => [
SwooleEvent::ON_BEFORE_START => [Hyperf\Framework\Bootstrap\ServerStartCallback::class, 'beforeStart'],
SwooleEvent::ON_WORKER_START => [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'],
SwooleEvent::ON_PIPE_MESSAGE => [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'],
SwooleEvent::ON_WORKER_EXIT => function () {
Swoole\Timer::clearAll();
},
],
// ...
Modo de operação
O Swoole Server inclui dois modos de operação: modo single-threaded (SWOOLE_BASE) e modo de processo (SWOOLE_PROCESS).
O pacote esqueleto oficial do Hyperf usa o modo Process por padrão. Em deployments tradicionais de serviços, o modo Process ajuda a gerenciar processos. Em um deployment no Kubernetes, o Kubernetes e o Ingress ou Sidecar do Kubernetes já assumem algumas funções, como pull, balanceamento e manutenção de conexões. Usar Process acaba sendo um pouco redundante.
O Docker incentiva oficialmente a abordagem “um processo por contêiner”. Aqui usamos o modo Base e iniciamos apenas um processo (worker_num=1).
O site oficial do Swoole define as vantagens do modo Base como:
- O modo Base não tem overhead de IPC e oferece melhor desempenho.
- O código no modo BASE é mais simples e menos propenso a erros.
<?php
// config/autoload/server.php
// ...
'mode' => SWOOLE_BASE,
// ...
'settings' => [
'enable_coroutine' => true,
'worker_num' => 1,
'pid_file' => BASE_PATH . '/runtime/hyperf.pid',
'open_tcp_nodelay' => true,
'max_coroutine' => 100000,
'open_http2_protocol' => true,
'max_request' => 100000,
'socket_buffer_size' => 2 * 1024 * 1024,
],
// ...
Depois de configurar um processo por contêiner, nossa expansão e contração podem ser mais granulares. Imagine que existam 16 processos em um contêiner; nesse caso, o número de processos depois da expansão só poderia ser múltiplo de 16. Com um processo por contêiner, podemos definir o número total de processos como qualquer número natural.
Como existe apenas um processo por contêiner, aqui limitamos cada contêiner a usar no máximo um core.
resources:
requests:
cpu: "1"
limits:
cpu: "1"
Depois configuramos o Horizontal Pod Autoscaler para escalar automaticamente de acordo com a pressão do serviço.
# Minimum 1 process, maximum 100 processes, target CPU usage 50%
kubectl autoscale deployment hyperf-demo --cpu-percent=50 --min=1 --max=100
Processamento de logs
A melhor prática para contêineres Docker é imprimir logs em stdout e stderr. Os logs do Hyperf são divididos em logs de sistema e logs de aplicação. Os logs de sistema já são impressos na saída padrão, e os logs de aplicação são impressos por padrão na pasta runtime. Isso obviamente não é flexível o suficiente em um ambiente de contêiner. Vamos imprimir ambos na saída padrão.
<?php
// config/autoload/logger.php
return [
'default' => [
'handler' => [
'class' => Monolog\Handler\ErrorLogHandler::class,
'constructor' => [
'messageType' => Monolog\Handler\ErrorLogHandler::OPERATING_SYSTEM,
'level' => env('APP_ENV') === 'prod'
? Monolog\Logger::WARNING
: Monolog\Logger::DEBUG,
],
],
'formatter' => [
'class' => env('APP_ENV') === 'prod'
? Monolog\Formatter\JsonFormatter::class
: Monolog\Formatter\LineFormatter::class,
],
'PsrLogMessageProcessor' => [
'class' => Monolog\Processor\PsrLogMessageProcessor::class,
],
],
];
Um olhar mais atento para a configuração acima mostra que fizemos processamentos diferentes para variáveis de ambiente diferentes.
Primeiro, geramos logs estruturados em JSON no ambiente de produção, porque ferramentas de coleta de logs como FluentBit e Filebeat conseguem fazer parse nativo de logs JSON, distribuí-los, filtrá-los e modificá-los sem depender de combinações complexas com grok. Em um ambiente de desenvolvimento, logs JSON não são tão amigáveis, e a legibilidade despenca quando há escapes envolvidos. Por isso, no ambiente de desenvolvimento ainda usamos LineFormatter para gerar logs.
Segundo, no ambiente de desenvolvimento geramos muitos logs; no ambiente de produção, precisamos controlar a quantidade de logs para evitar congestionar a ferramenta de coleta. Se o log eventualmente for gravado no Elasticsearch, controlar a velocidade de escrita é ainda mais importante. No ambiente de produção, recomendamos habilitar por padrão apenas logs acima de WARNING.
De acordo com a introdução da documentação oficial, também entregamos ao Monolog o processamento dos logs impressos pelo framework.
<?php
namespace App\Provider;
use Hyperf\Logger\LoggerFactory;
use Psr\Container\ContainerInterface;
class StdoutLoggerFactory
{
public function __invoke(ContainerInterface $container)
{
$factory = $container->get(LoggerFactory::class);
return $factory->get('Sys', 'default');
}
}
<?php
// config/autoload/dependencies.php
return [
Hyperf\Contract\StdoutLoggerInterface::class => App\Provider\StdoutLoggerFactory::class,
];
Tratamento de arquivos
Aplicações stateful não podem ser escaladas arbitrariamente. O estado comum de uma aplicação PHP geralmente se resume a Session, logs, upload de arquivos etc. Session pode ser armazenada no Redis. Os logs foram apresentados na seção anterior. Esta seção apresenta o processamento de arquivos.
Recomenda-se fazer upload de arquivos para a nuvem na forma de armazenamento de objetos. Alibaba Cloud, Qiniu Cloud etc. são fornecedores comuns. Soluções de deployment privado também incluem MinIO, Ceph etc. Para evitar vendor lock-in, recomenda-se usar uma camada de abstração unificada em vez de depender diretamente dos SDKs fornecidos pelos vendors. league/flysystem é uma escolha comum em muitos frameworks populares, incluindo Laravel. Aqui introduzimos o pacote League\Flysystem e conectamos o armazenamento MinIO por meio da API aws S3.
composer require league/flysystem
composer require league/flysystem-aws-s3-v3
Crie uma classe factory e vincule a relação de acordo com a documentação oficial de DI do Hyperf.
<?php
namespace App\Provider;
use Aws\S3\S3Client;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Guzzle\CoroutineHandler;
use League\Flysystem\Adapter\Local;
use League\Flysystem\AwsS3v3\AwsS3Adapter;
use League\Flysystem\Config;
use League\Flysystem\Filesystem;
use Psr\Container\ContainerInterface;
class FileSystemFactory
{
public function __invoke(ContainerInterface $container)
{
$config = $container->get(ConfigInterface::class);
if ($config->get('app_env') === 'dev') {
return new Filesystem(new Local(__DIR__ . '/../../runtime'));
}
$options = $container->get(ConfigInterface::class)->get('file');
$adapter = $this->adapterFromArray($options);
return new Filesystem($adapter, new Config($options));
}
private function adapterFromArray(array $options): AwsS3Adapter
{
// 协程化S3客户端
$options = array_merge($options, ['http_handler' => new CoroutineHandler()]);
$client = new S3Client($options);
return new AwsS3Adapter($client, $options['bucket_name'], '', ['override_visibility_on_copy' => true]);
}
}
<?php
// config/autoload/dependencies.php
return [
Hyperf\Contract\StdoutLoggerInterface::class => App\Provider\StdoutLoggerFactory::class,
League\Flysystem\Filesystem::class => App\Provider\FileSystemFactory::class,
];
Vamos criar um novo config/autoload/file.php do jeito que o Hyperf costuma usar e configurar a chave do S3 e outras informações:
<?php
// config/autoload/file.php
return [
'credentials' => [
'key' => env('S3_KEY'),
'secret' => env('S3_SECRET'),
],
'region' => env('S3_REGION'),
'version' => 'latest',
'bucket_endpoint' => false,
'use_path_style_endpoint' => true,
'endpoint' => env('S3_ENDPOINT'),
'bucket_name' => env('S3_BUCKET'),
];
Assim como nos logs, ao desenvolver e depurar usamos a pasta Runtime para uploads e, no ambiente de produção, fazemos upload das imagens para o MinIO. Para fazer upload para a Alibaba Cloud no futuro, basta instalar o adaptador Alibaba Cloud do league/flysystem:
composer require aliyuncs/aliyun-oss-flysystem
E reescrever FileSystemFactory conforme necessário.
Rastreamento e monitoramento
O rastreamento de chamadas e o monitoramento de serviços em si não são funções fornecidas pelo Kubernetes, mas, como as pilhas de tecnologia no panorama cloud native conseguem cooperar muito bem entre si, geralmente é recomendado usá-las em conjunto.
Documentação de rastreamento do Hyperf: hyperf.wiki/2.2/#/zh-cn/tracer
Documentação de monitoramento de serviços do Hyperf: hyperf.wiki/2.2/#/zh-cn/metric
Se você configurou o modo base e usa um processo, não precisa iniciar um processo de monitoramento separado ao monitorar o serviço. Adicione as rotas abaixo ao Controller:
<?php
// Bind the /metrics route here
public function metrics(CollectorRegistry $registry)
{
$renderer = new RenderTextFormat();
return $renderer->render($registry->getMetricFamilySamples());
}
Se o Prometheus que você usa oferece suporte à descoberta de targets de coleta por anotações de serviço, basta adicionar as anotações do Prometheus ao Service.
kind: Service
metadata:
annotations:
prometheus.io/port: "9501"
prometheus.io/scrape: "true"
prometheus.io/path: "/metrics"
Se você usa Nginx Ingress, pode configurar a ativação do Opentracing. (documentação do nginx ingress)
Primeiro configure o Tracer usado no Configmap do Nginx Ingress.
zipkin-collector-host: zipkin.default.svc.cluster.local
jaeger-collector-host: jaeger-agent.default.svc.cluster.local
datadog-collector-host: datadog-agent.default.svc.cluster.local
Depois habilite opentracing na annotation do Ingress.
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/enable-opentracing: "true"
Dessa forma, a ligação entre o Nginx Ingress e o Hyperf pode ser aberta.
Exemplo completo
Um pacote esqueleto completo pode ser encontrado no meu GitHub: https://github.com/Reasno/cloudnative-hyperf
Na prática, existem muitas formas de fazer deploy no Kubernetes. É impossível que qualquer pacote esqueleto seja adequado para todas as situações.
