التنميط. تتبع حالة بيئة القتال باستخدام Redis و ClickHouse و Grafana



تقريبا. الوقت الكمون.



ربما يواجه الجميع مهمة تحديد الكود في الإنتاج. xhprof من Facebook يقوم بهذه المهمة بشكل جيد. يمكنك ملف التعريف ، على سبيل المثال ، 1/1000 طلب وترى الصورة في الوقت الحالي. بعد كل إصدار ، يأتي المنتج قيد التشغيل ويقول "كان أفضل وأسرع قبل الإصدار." ليس لديك بيانات تاريخية ولا يمكنك إثبات أي شيء. ماذا لو استطعت؟



منذ وقت ليس ببعيد ، أعدنا كتابة جزء إشكالي من الكود وتوقعنا مكاسب قوية في الأداء. كتبنا اختبارات الوحدة ، وقمنا باختبار التحميل ، ولكن كيف سيتصرف الكود في ظل التحميل المباشر؟ بعد كل شيء ، نحن نعلم أن اختبار التحميل لا يعرض دائمًا بيانات حقيقية ، وبعد النشر ، تحتاج إلى الحصول بسرعة على تعليقات من التعليمات البرمجية الخاصة بك. إذا قمت بجمع البيانات ، فبعد الإصدار ، لن تحتاج إلا إلى 10-15 دقيقة لفهم الوضع في بيئة القتال.





تقريبا. الوقت الكمون. (1) نشر ، (2) تراجع



كومة



لمهمتنا ، أخذنا قاعدة بيانات ClickHouse العمودية (اختصار kx). كانت السرعة وقابلية التوسع الخطي وضغط البيانات وعدم وجود توقف تام هي الأسباب الرئيسية لهذا الاختيار. وهي الآن إحدى القواعد الرئيسية في المشروع.



في الإصدار الأول ، كتبنا رسائل إلى قائمة الانتظار ، وكتبناها بالفعل من قبل المستهلكين إلى ClickHouse. وصل التأخير إلى 3-4 ساعات (نعم ، ClickHouse بطيء في إدراج واحدة تلو الأخرىالسجلات). مر الوقت وكان من الضروري تغيير شيء ما. لا فائدة من الرد على التنبيهات بمثل هذا التأخير. ثم كتبنا أمر التاج الذي حدد العدد المطلوب من الرسائل من قائمة الانتظار وأرسل دفعة إلى قاعدة البيانات ، ثم وضعنا علامة على معالجتها في قائمة الانتظار. في الشهرين الأولين كان كل شيء على ما يرام ، حتى بدأت المشاكل هنا. كان هناك عدد كبير جدًا من الأحداث ، وبدأت البيانات المكررة في الظهور في قاعدة البيانات ، ولم يتم استخدام قوائم الانتظار للغرض المقصود منها (أصبحت قاعدة بيانات) ، وتوقف أمر التاج عن التعامل مع التسجيل في ClickHouse. خلال هذا الوقت ، تمت إضافة بضع عشرات من الجداول الأخرى إلى المشروع ، والتي كان لا بد من كتابتها على دفعات في kx. انخفضت سرعة المعالجة. كان الحل بسيطًا وسريعًا قدر الإمكان. دفعنا هذا إلى كتابة رمز مع قوائم في redis. الفكرة هي: نكتب رسائل في نهاية القائمة ،باستخدام أمر التاج ، نشكل حزمة ونرسلها إلى قائمة الانتظار. ثم يقوم المستهلكون بتحليل قائمة الانتظار وكتابة مجموعة من الرسائل في kx.



لدينا : ClickHouse و Redis وقائمة انتظار (أي - rabbitmq ، kafka ، beanstalkd ...)



Redis والقوائم



حتى وقت معين ، تم استخدام Redis كذاكرة تخزين مؤقت ، لكن هذا يتغير. تتمتع القاعدة بوظائف ضخمة ، ولا نحتاج إلا إلى 3 أوامر لمهمتنا : rpush و lrange و ltrim .



سنكتب البيانات إلى نهاية القائمة باستخدام الأمر rpush. في أمر التاج ، اقرأ البيانات باستخدام 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',
    ];

}


دعنا نكتب طريقة التجزئة (لقد أنشأت خدمة ProfileLogService ) والتي ستكتب رسائل إلى Redis. نحصل على الوقت الحالي (وقتنا الحالي) ونكتبه إلى المتغير الحالي $ currentTime:



$currentTime = \microtime(true);


إذا تم استدعاء علامة التجزئة لحدث ما لأول مرة ، فقم بكتابتها في مصفوفة التجزئة وإنهاء العملية:



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


إذا تم استدعاء العلامة مرة أخرى ، فسنكتب الرسالة إلى Redis باستخدام طريقة 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,
]));


المتغير $ this-> ticks غير ثابت. تحتاج إلى تسجيل الخدمة كمفرد.



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


حجم الدفعة ( $ batchSize ) قابل للتكوين ، يوصى بتحديد قيمة صغيرة (على سبيل المثال ، 10000 عنصر). إذا ظهرت مشاكل (على سبيل المثال ، ClickHouse غير متوفر) ، فستبدأ قائمة الانتظار في الانتقال إلى الفشل ، وتحتاج إلى تصحيح البيانات.



لنكتب أمر تاج:



$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. سنخلط بين أمر التاج ونرسل الحزم إلى قائمة الانتظار ، وسيقوم المستهلك بإدراجها في قاعدة البيانات.







البيانات في قاعدة البيانات. يمكنك بناء الرسوم البيانية.



جرافانا



الآن دعنا ننتقل إلى العرض الرسومي للبيانات ، وهو عنصر أساسي في هذه المقالة. تحتاج إلى تثبيت برنامج grafana . دعنا نتخطى عملية التثبيت للتجمعات التي تشبه debain ، يمكنك استخدام رابط التوثيق . عادةً ما تتلخص خطوة التثبيت في apt install grafana .



في ArchLinux ، يبدو التثبيت كما يلي:



yaourt -S grafana
sudo systemctl start grafana


بدأت الخدمة. URL: http: // localhost: 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 / password: admin افتراضيًا.







بعد الحصول على إذن ناجح ، انقر فوق الترس. في النافذة المنبثقة التي تفتح ، حدد مصادر البيانات ، وأضف اتصال ClickHouse.







نقوم بملء التكوين kx. انقر فوق الزر "حفظ واختبار" ، وستتلقى رسالة حول اتصال ناجح.



الآن دعنا نضيف لوحة معلومات جديدة:







أضف لوحة:







حدد القاعدة والأعمدة المقابلة للعمل مع التواريخ:







دعنا ننتقل إلى الاستعلام:







لقد حصلنا على رسم بياني ، لكنني أريد تفاصيل. دعنا نطبع متوسط زمن الوصول لتقريب التاريخ بالوقت إلى بداية الفاصل الزمني المكون من خمس دقائق :







الآن يتم عرض البيانات المحددة على الرسم البياني ، يمكننا التركيز عليها. بالنسبة إلى التنبيهات ، يمكنك تهيئة المشغلات والتجميع حسب الأحداث والمزيد.







لا يعد ملف التعريف بأي حال من الأحوال بديلاً للأدوات: xhprof (facebook) ، xhprof (tideways) ، liveprof من (Badoo) . ويكملهم فقط.



كل رمز المصدر على جيثب - نموذج التعريف ، خدمة ، BulkWriteCommand ، BulkWriterJob والوسيطة ( 1 ، 2 ).



تثبيت الحزمة:



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