Aproximadamente. latência / tempo.
Provavelmente, todos enfrentam a tarefa de criar perfis de código na produção. O xhprof do Facebook faz isso bem. Você perfila, por exemplo, 1/1000 solicitações e vê a foto no momento. Após cada lançamento, o produto vem rodando e diz "era melhor e mais rápido antes do lançamento". Você não tem dados históricos e não pode provar nada. E se você pudesse?
Não faz muito tempo, reescrevemos uma parte problemática do código e esperávamos um forte ganho de desempenho. Nós escrevemos testes de unidade, fizemos testes de carga, mas como o código se comportará sob carga ativa? Afinal, sabemos que o teste de carga nem sempre exibe dados reais e, após a implantação, você precisa obter feedback rapidamente de seu código. Se você está coletando dados, então, após o lançamento, 10-15 minutos são suficientes para você entender a situação no ambiente de combate.
Aproximadamente. latência / tempo. (1) implantação, (2) reversão
Pilha
Para nossa tarefa, pegamos um banco de dados ClickHouse colunar (abreviado como kx). Velocidade, escalabilidade linear, compressão de dados e nenhum deadlock foram os principais motivos para essa escolha. Agora é uma das principais bases do projeto.
Na primeira versão, escrevemos mensagens para a fila e, já pelos consumidores, as escrevemos para ClickHouse. O atraso chegou a 3-4 horas (sim, ClickHouse é lento para inserir um por umregistros). O tempo passou e foi preciso mudar alguma coisa. Não adiantava responder às notificações com tanto atraso. Em seguida, escrevemos um comando de coroa que selecionou o número necessário de mensagens da fila e enviou um lote para o banco de dados, em seguida, marcou o processamento na fila. Nos primeiros meses estava tudo bem, até que os problemas começaram aqui. Havia muitos eventos, dados duplicados começaram a aparecer no banco de dados, as filas não eram usadas para o propósito pretendido (elas se tornaram um banco de dados) e o comando crown não mais lidava com a gravação no ClickHouse. Durante esse tempo, algumas dezenas de tabelas foram adicionadas ao projeto, que tiveram que ser gravadas em lotes em kx. A velocidade de processamento caiu. A solução foi a mais simples e rápida possível. Isso nos levou a escrever código com listas no redis. A ideia é esta: escrevemos mensagens no final da lista,Com o comando coroa, formamos um pacote e enviamos para a fila. Em seguida, os consumidores analisam a fila e gravam um monte de mensagens em kx.
Temos : ClickHouse, Redis e uma fila (qualquer - rabbitmq, kafka, beanstalkd ...)
Redis e listas
Até um certo tempo, o Redis era usado como cache, mas isso está mudando. A base tem uma grande funcionalidade e para nossa tarefa apenas 3 comandos são necessários : rpush , lrange e ltrim .
Usaremos o comando rpush para gravar dados no final da lista. No comando crown, leia os dados usando lrange e envie para a fila, se conseguimos enviar para a fila, então precisamos deletar os dados selecionados usando ltrim.
Da teoria à prática. Vamos criar uma lista simples.
Temos uma lista de três mensagens, vamos adicionar um pouco mais ...
Novas mensagens são adicionadas ao final da lista. Usando o comando lrange, selecione o lote (que seja = 5 mensagens).
Em seguida, enviamos o pacote para a fila. Agora você precisa remover este pacote do Redis para não enviá-lo novamente.
Existe um algoritmo, vamos começar a implementação.
Implementação
Vamos começar com a tabela ClickHouse. Não me preocupei muito e defini tudo no tipo String .
create table profile_logs
(
hostname String, // ,
project String, //
version String, //
userId Nullable(String),
sessionId Nullable(String),
requestId String, //
requestIp String, // ip
eventName String, //
target String, // URL
latency Float32, // (latency=endTime - beginTime)
memoryPeak Int32,
date Date,
created DateTime
)
engine = MergeTree(date, (date, project, eventName), 8192);
O evento será assim:
{
"hostname": "debian-fsn1-2",
"project": "habr",
"version": "7.19.1",
"userId": null,
"sessionId": "Vv6ahLm0ZMrpOIMCZeJKEU0CTukTGM3bz0XVrM70",
"requestId": "9c73b19b973ca460",
"requestIp": "46.229.168.146",
"eventName": "app:init",
"target": "/",
"latency": 0.01384348869323730,
"memoryPeak": 2097152,
"date": "2020-07-13",
"created": "2020-07-13 13:59:02"
}
A estrutura está definida. Para calcular a latência , precisamos de um período de tempo. Nós beliscamos usando a função microtime :
$beginTime = microtime(true);
//
$latency = microtime(true) - $beginTime;
Para simplificar a implementação, usaremos o framework laravel e a biblioteca laravel-entry . Adicione um modelo (tabela profile_logs):
class ProfileLog extends \Bavix\Entry\Models\Entry
{
protected $fillable = [
'hostname',
'project',
'version',
'userId',
'sessionId',
'requestId',
'requestIp',
'eventName',
'target',
'latency',
'memoryPeak',
'date',
'created',
];
protected $casts = [
'date' => 'date:Y-m-d',
'created' => 'datetime:Y-m-d H:i:s',
];
}
Vamos escrever um método tick ( criei um serviço ProfileLogService ) que escreverá mensagens no Redis. Pegamos a hora atual (nosso beginTime) e gravamos na variável $ currentTime:
$currentTime = \microtime(true);
Se o tick para um evento for chamado pela primeira vez, grave-o na matriz tick e finalize o método:
if (empty($this->ticks[$eventName])) {
$this->ticks[$eventName] = $currentTime;
return;
}
Se o tick for chamado novamente, escreveremos a mensagem para o Redis usando o método rpush:
$tickTime = $this->ticks[$eventName];
unset($this->ticks[$eventName]);
Redis::rpush('events:profile_logs', \json_encode([
'hostname' => \gethostname(),
'project' => 'habr',
'version' => \app()->version(),
'userId' => Auth::id(),
'sessionId' => \session()->getId(),
'requestId' => \bin2hex(\random_bytes(8)),
'requestIp' => \request()->getClientIp(),
'eventName' => $eventName,
'target' => \request()->getRequestUri(),
'latency' => $currentTime - $tickTime,
'memoryPeak' => \memory_get_usage(true),
'date' => $tickTime,
'created' => $tickTime,
]));
A variável $ this-> ticks não é estática. Você precisa registrar o serviço como um singleton.
$this->app->singleton(ProfileLogService::class);
O tamanho do lote ( $ batchSize ) é configurável, é recomendável especificar um valor pequeno (por exemplo, 10.000 itens). Se surgirem problemas (por exemplo, ClickHouse não está disponível), a fila começará a falhar e você precisará depurar os dados.
Vamos escrever um comando de coroa:
$batchSize = 10000;
$key = 'events:profile_logs'
do {
$bulkData = Redis::lrange($key, 0, \max($batchSize - 1, 0));
$count = \count($bulkData);
if ($count) {
// json, decode
foreach ($bulkData as $itemKey => $itemValue) {
$bulkData[$itemKey] = \json_decode($itemValue, true);
}
// ch
\dispatch(new BulkWriter($bulkData));
// redis
Redis::ltrim($key, $count, -1);
}
} while ($count >= $batchSize);
Você pode gravar dados imediatamente no ClickHouse, mas o problema reside no fato de que o kronor funciona no modo de thread único. Portanto, iremos por outro caminho - com o comando iremos formar pacotes e enviá-los para a fila para subsequente gravação multithread no ClickHouse. O número de consumidores pode ser regulado - isso vai agilizar o envio de mensagens.
Vamos continuar escrevendo para um consumidor:
class BulkWriter implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $bulkData;
public function __construct(array $bulkData)
{
$this->bulkData = $bulkData;
}
public function handle(): void
{
ProfileLog::insert($this->bulkData);
}
}
}
Assim, desenvolve-se a formação dos packs, o envio para a fila e o consumidor - pode começar a traçar:
app(ProfileLogService::class)->tick('post::paginate');
$posts = Post::query()->paginate();
$response = view('posts', \compact('posts'));
app(ProfileLogService::class)->tick('post::paginate');
return $response;
Se tudo for feito corretamente, os dados devem estar no Redis. Iremos confundir o comando coroa e enviar os pacotes para a fila, e o consumidor irá inseri-los no banco de dados.
Dados no banco de dados. Você pode construir gráficos.
Grafana
Agora, vamos passar para a apresentação gráfica dos dados, que é um elemento-chave deste artigo. Você precisa instalar o grafana . Vamos pular o processo de instalação para assemblies do tipo debain, você pode usar o link para a documentação . Normalmente, a etapa de instalação resume-se ao apt install grafana .
No ArchLinux, a instalação se parece com isto:
yaourt -S grafana
sudo systemctl start grafana
O serviço foi iniciado. URL: http: // localhost: 3000
Agora você precisa instalar o plug - in da fonte de dados ClickHouse :
sudo grafana-cli plugins install vertamedia-clickhouse-datasource
Se você instalou o grafana 7+, o ClickHouse não funcionará. Você precisa fazer alterações na configuração:
sudo vi /etc/grafana.ini
Vamos encontrar a linha:
;allow_loading_unsigned_plugins =
Vamos substituí-lo por este:
allow_loading_unsigned_plugins=vertamedia-clickhouse-datasource
Vamos salvar e reiniciar o serviço:
sudo systemctl restart grafana
Feito. Agora podemos ir para a grafana .
Login: admin / senha: admin por padrão.
Após a autorização bem-sucedida, clique na engrenagem. Na janela pop-up que se abre, selecione Fontes de dados, adicione uma conexão ao ClickHouse.
Preenchemos a configuração kx. Clique no botão "Salvar e Testar", recebemos uma mensagem sobre uma conexão bem-sucedida.
Agora vamos adicionar um novo painel:
Adicionar um painel:
Selecione a base e as colunas correspondentes para trabalhar com datas:
Vamos passar para a consulta:
Temos um gráfico, mas quero detalhes. Vamos imprimir a latência média arredondando a data com a hora até o início do intervalo de cinco minutos :
Agora os dados selecionados são exibidos no gráfico, podemos nos concentrar neles. Para alertas, configure gatilhos, agrupe por eventos e muito mais.
O profiler não é de forma alguma um substituto para as ferramentas: xhprof (facebook) , xhprof (tideways) , liveprof de (Badoo) . E apenas os complementa.
Todo o código-fonte está no github - modelo de perfil , serviço , BulkWriteCommand , BulkWriterJob e middleware ( 1 , 2 ).
Instalando o pacote:
composer req bavix/laravel-prof
Configurando conexões (config / database.php), adicione clickhouse:
'bavix::clickhouse' => [
'driver' => 'bavix::clickhouse',
'host' => env('CH_HOST'),
'port' => env('CH_PORT'),
'database' => env('CH_DATABASE'),
'username' => env('CH_USERNAME'),
'password' => env('CH_PASSWORD'),
],
Início do trabalho:
use Bavix\Prof\Services\ProfileLogService;
// ...
app(ProfileLogService::class)->tick('event-name');
//
app(ProfileLogService::class)->tick('event-name');
Para enviar um lote à fila, você precisa adicionar um comando ao cron:
* * * * * php /var/www/site.com/artisan entry:bulk
Você também precisa executar um consumidor:
php artisan queue:work --sleep=3 --tries=3
Recomenda-se configurar o supervisor . Config (5 consumidores):
[program:bulk_write]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/site.com/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
user=www-data
numprocs=5
redirect_stderr=true
stopwaitsecs=3600
UPD:
1. ClickHouse pode extrair dados nativamente da fila kafka . Obrigado,sdm