分析。使用Redis,ClickHouse和Grafana跟踪战斗环境的状态



大约延迟/时间。



可能每个人都面临着在生产环境中分析代码的任务。 Facebook的xhprof做得很好。例如,您分析了1/1000个请求,然后看到了图片。每次发行后,该产品都会运行,并说“发行前更好,更快”。您没有历史数据,也无法证明任何事情。如果可以的话怎么办?



不久前,我们重写了代码中有问题的部分,并期望性能得到很大提高。我们编写了单元测试,进行了负载测试,但是代码在活动负载下将如何运行?毕竟,我们知道负载测试并不总是显示真实数据,并且在部署之后,您需要快速从代码中获取反馈。如果您收集数据,那么在发布后,您只需10到15分钟即可了解战斗环境中的情况。





大约 延迟/时间。(1)部署,(2)回滚



叠放



对于我们的任务,我们采用了列式ClickHouse数据库(缩写为kx)。速度,线性可伸缩性,数据压缩和无死锁是选择此文件的主要原因。现在,它是该项目的主要基地之一。



在第一个版本中,我们将消息写到队列中,并且已经由消费者将消息写到ClickHouse中。延迟达到3-4个小时(是的,ClickHouse缓慢插入一个一个记录)。时间过去了,有必要改变一些东西。如此延迟地响应警报毫无意义。然后,我们编写了Crown命令,该命令从队列中选择了所需数量的消息,并将一批消息发送到数据库,然后将其标记为在队列中已处理。头几个月,一切都很好,直到出现问题为止。事件太多,重复的数据开始出现在数据库中,队列未达到预期的目的(它们成为数据库),并且Crown命令停止处理ClickHouse中的记录。在此期间,该项目中又增加了几十个表,这些表必须以kx批量编写。处理速度下降。解决方案尽可能简单快捷。这促使我们用redis中的列表编写代码。想法是这样的:我们将消息写到列表的末尾,使用Crown命令,我们形成一个包装并将其发送到队列。然后,使用者解析队列并将一堆消息写入kx。



我们有:ClickHouse,Redis和一个队列(任何-Rabbitmq,kafka,beanstalkd ...)



Redis和列表



直到某个时候,Redis才被用作缓存,但是这种情况正在改变。该库具有强大的功能,对于我们的任务,仅需要3个命令:rpushlrangeltrim



我们将使用rpush命令将数据写入列表的末尾。在Crown命令中,使用lrange读取数据并发送到队列,如果我们设法发送到队列,则需要使用ltrim删除选定的数据。



从理论到实践。让我们创建一个简单的列表。







我们有三个消息的列表,让我们添加更多...







新消息添加到列表的末尾。使用lrange命令,选择批次(将其设为= 5条消息)。







接下来,我们将包发送到队列。现在,您需要从Redis中删除此捆绑包,以免再次发送它。







有一个算法,让我们开始实施。



实作



让我们从ClickHouse表开始。我并没有太在意并用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);




该事件将是这样的:

{
  "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"
}


结构已定义。要计算延迟,我们需要一个时间段。我们使用microtime函数来确定



$beginTime = microtime(true);
//    
$latency = microtime(true) - $beginTime;


为了简化实现,我们将使用laravel框架和laravel-entry添加模型(表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',
    ];

}


让我们编写一个tick方法(我做了一个ProfileLogService服务),它将消息写入Redis。我们获取当前时间(我们的beginTime)并将其写入$ currentTime变量:



$currentTime = \microtime(true);


如果是第一次调用事件滴答声,则将其写入滴答数组并结束方法:



 if (empty($this->ticks[$eventName])) {
    $this->ticks[$eventName] = $currentTime;
    return;
}


如果再次调用了滴答声,那么我们使用rpush方法将消息写入Redis:



$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,
]));


$ this-> ticks 变量不是静态的。您需要将服务注册为单例。



$this->app->singleton(ProfileLogService::class);


批大小($ batchSize)是可配置的,建议指定一个较小的值(例如10,000个项目)。如果出现问题(例如,ClickHouse不可用),队列将开始失败,并且您需要调试数据。



让我们写一个Crown命令:



$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);


您可以立即将数据写入ClickHouse,但是问题在于kronor在单线程模式下工作。因此,我们将采取另一种方式-使用命令将形成数据包,并将其发送到队列中,以便在ClickHouse中进行后续多线程记录。使用者的数量可以调节-这将加快消息的发送速度。



让我们继续写一个消费者:



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);
        }
    }
}


因此,开发了打包,发送到队列和使用者的包-您可以开始分析:



app(ProfileLogService::class)->tick('post::paginate');
$posts = Post::query()->paginate();
$response = view('posts', \compact('posts'));
app(ProfileLogService::class)->tick('post::paginate');
return $response;


如果一切都正确完成,那么数据应该在Redis中。我们将混淆Crown命令并将包发送到队列,然后使用者将它们插入数据库。







数据库中的数据。您可以构建图形。



格拉法纳



现在,让我们继续进行数据的图形表示,这是本文的关键要素。您需要安装grafana让我们跳过类似debain的程序集的安装过程,您可以使用指向文档链接通常,安装步骤归结为易于安装grafana



在ArchLinux上,安装如下所示:



yaourt -S grafana
sudo systemctl start grafana


服务已启动。URL:http://本地主机:3000



现在,您需要安装ClickHouse数据源插件



sudo grafana-cli plugins install vertamedia-clickhouse-datasource


如果您已安装grafana 7+,则ClickHouse将无法正常工作。您需要更改配置:



sudo vi /etc/grafana.ini


让我们找到这行:



;allow_loading_unsigned_plugins =


让我们用这个替换它:



allow_loading_unsigned_plugins=vertamedia-clickhouse-datasource


保存并重新启动服务:



sudo systemctl restart grafana


做完了现在我们可以去格拉法纳

登录名:admin /密码:默认情况下为admin。







成功授权后,单击齿轮。在打开的弹出窗口中,选择“数据源”,添加一个ClickHouse连接。







我们填写配置kx。单击“保存并测试”按钮,我们将收到有关成功连接的消息。



现在,让我们添加一个新的仪表板:







添加一个面板:







选择用于处理日期的基础和相应的列:







让我们继续进行查询:







我们得到了一个图形,但是我想要细节。让我们打印将日期与时间四舍五入到五分钟间隔开始平均延迟时间







现在,选定的数据显示在图表上,我们可以将其重点放在上面。对于警报,请配置触发器,按事件分组等等。







分析器绝不是工具的替代品:(Badoo)的xhprof(facebook)xhprof(tideways)liveprof只是对它们的补充。



所有源代码是关于github上-探查模型服务BulkWriteCommandBulkWriterJob和中间件(12)。



安装软件包:



composer req bavix/laravel-prof


设置连接(config / database.php),添加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'),
],


工作开始:



use Bavix\Prof\Services\ProfileLogService;
// ...
app(ProfileLogService::class)->tick('event-name');
// 
app(ProfileLogService::class)->tick('event-name');


要将批次发送到队列,您需要向cron添加命令:



* * * * * php /var/www/site.com/artisan entry:bulk


您还需要运行使用者:



php artisan queue:work --sleep=3 --tries=3


建议配置主管配置(5个使用者):



[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可以从kafka队列中本地提取数据谢谢,sdm



All Articles