hyperf| hyperf/metric 上手指南

date: 2019-11-01 16:25:45
title: hyperf| hyperf/metric 上手指南

這期又開始聊微服務的基礎設施之一: 實時監控. 更準確的說法, 基於 prometheus 的實時監控. 關於技術選型這裏就不多囉嗦啦, 很多時候「從衆」或者「用腳投票」往往是最有效的

真的猛士, 敢於走少有人走的路. 我們選擇 prometheus, 這條猛士已經走過的路.

prometheus 實戰

之所以先把實戰放上來, 在於「看得見」往往不如「摸得着」來得 刺激 深刻. 有了一套環境在那放着, 變學變實踐, 纔不會停留在 花了時間學, 用的時候還是不會 這種低效的循環裏. 至於環境問題怎麼解決, 又到了我們的老朋友 docker 了.

version: '3.1'
services:
    hyperf:
        image: hyperf/hyperf
        volumes:
            - ./:/data
        ports:
            - "9501:9501"
        tty: true
    # https://prometheus.io/docs/prometheus/latest/installation/
    prometheus:
        image: prom/prometheus
        ports:
            - "9090:9090"
        volumes:
            - ./config/prometheus.yml:/etc/prometheus/prometheus.yml
    # https://grafana.com/docs/installation/docker/
    grafana:
        image: grafana/grafana
        ports:
            - "3000:3000"

docker 知識不熟悉的小夥伴, 可以先學着把自己本地使用的環境 docker 化.

把 docker 當做一個工具, 就會發現這傢伙是的真的簡單. 原理可能很複雜, 但是我只是使用呀, 這和 CRUD 能有多大差別?

hyperf/metric 配置

首先是 hyperf 裏面使用 prometheus, 這裏參考官網文檔 hyperf/metric 即可. 當然官網的 metric 包比較強大(這不是吹水, 官方通常要考慮更 通用, 監控選項的支持會更多一些), 這裏我們只關注 prometheus:

  • 安裝
# 安裝
composer require hyperf/metric

# 添加配置
php bin/hyperf vendor:publish hyperf/metric
  • 配置
return [
    'default' => env('METRIC_DRIVER', 'prometheus'),
    'use_standalone_process' => env('METRIC_USE_STANDALONE_PROCESS', true),
    'enable_default_metric' => env('METRIC_ENABLE_DEFAULT_METRIC', true),
    'default_metric_interval' => env('DEFAULT_METRIC_INTERVAL', 5),
    'metric' => [
        'prometheus' => [
            'driver' => Hyperf\Metric\Adapter\Prometheus\MetricFactory::class,
            'mode' => Constants::SCRAPE_MODE,
            'namespace' => env('APP_NAME', 'skeleton'),
            'scrape_host' => env('PROMETHEUS_SCRAPE_HOST', '0.0.0.0'),
            'scrape_port' => env('PROMETHEUS_SCRAPE_PORT', '9502'),
            'scrape_path' => env('PROMETHEUS_SCRAPE_PATH', '/metrics'),
            'push_host' => env('PROMETHEUS_PUSH_HOST', '0.0.0.0'),
            'push_port' => env('PROMETHEUS_PUSH_PORT', '9091'),
            'push_interval' => env('PROMETHEUS_PUSH_INTERVAL', 5),
        ],
        ...

使用默認配置就好, 默認配置就是 prometheus, 之後訪問 http://localhost:9502/metrics 就可以查看組件默認配置的 metric

prometheus 配置

重複一句, 你只是使用 prometheus, 根據「學習思維三部曲: what->how->why」, 只是 what 其實很簡單(簡單不代表低級, 命名很簡單卻還沒有完成的問題比比皆是), 這裏都使用默認配置運行起來即可

參考文件, 即上面 docker-compose 中配置的:

    prometheus:
        image: prom/prometheus
        ports:
            - "9090:9090"
        volumes:
            - ./config/prometheus.yml:/etc/prometheus/prometheus.yml

對應其實只需要修改:

# my global config
global:
  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      # - alertmanager:9093

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
  # - "first_rules.yml"
  # - "second_rules.yml"

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: 'prometheus'

    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.

    static_configs:
    - targets: ['localhost:9090']
  - job_name: 'hyperf'
    static_configs:
    - targets: ['ms:9502']
  - job_name: 'grafana'
    static_configs:
    - targets: ['grafana:3000']

只需要添加需要監控的 job 即可, 這裏我添加 grafanahyperf 作爲示例

grafana 配置

grafana 純 webUI 交互, 啓動後按照頁面提示, 就可以添加 prometheus 作爲數據源, 然後可以選擇一個 grafana 的 UI 模板, 就可以看到採集的效果了

同理, hyperf 官方團隊也提供了 UI 模板, 在 grafana theme 中搜索即可, 替換相應的參數, 就可以看到 hyperf 默認提供的 metric 參數

實踐小結

實踐是檢驗真理的唯一標準, 這句話在面對新技術, 面對未知時尤其有用, 學習一個新知識, 通常面對的知識 what 一級的內容, 很多時候天然具備 簡單 屬性, 這個時候的摸爬滾打幾乎是 最公平 的 -- 只需要像小孩子一樣, 保持好奇心, 多試幾次.

prometheus 的典型生產實踐

項目中使用 prometheus 監控最常見的 2 個場景:

  • api 監控 -> 擴展在所有的業務服務監控
  • db 監控 -> 擴展到所有基礎服務監控

到了這裏, 我們需要補充一點 prometheus 的基礎知識:

prometheus docs: https://prometheus.io/docs/introduction/overview/

prometheus 文檔並不長, 閱讀大概需要 15min

PS: 閱讀英文有障礙, 其實主要是心理作用, 安裝一個網頁翻譯查詞插件, 哪裏不會點哪裏, 常用的計算機詞彙熟悉後, 幾乎就不會影響閱讀體驗了

摘抄一下需要掌握的基礎知識:

  • 基礎概念
    • metric: 監控指標, 比如 pv_total
    • label: 相當於 tag, 附在 metric 上, 可以做細化的查詢分析
  • 指標類型
    • count: 只增計數器, 比如 pv
    • gauge: 可增減計數器, 比如當前 qps
    • histogrm: 直方圖, 比如 95值
  • query: PromQL

熟悉 MySQL 記憶有一些代碼量的讀者, 我相信 show me the code 絕對是絕佳的學習方式:

# api 訪問次數
sum(rate(app_api_query_time_count{api_name="self"}[1m]))
# service 對應 request_uri
sum(rate(app_api_query_time_count{api_name="self"}[1m])) by (service)

# api 訪問耗時95值
# https://prometheus.io/docs/concepts/metric_types/#histogram
histogram_quantile(0.95, sum(rate(app_api_query_time_bucket{api_name="self"}[1m])) by (le))
histogram_quantile(0.95, sum(rate(app_api_query_time_bucket{api_name="self"}[1m])) by (le, service))

# 表查詢次數
sum(irate(app_db_query_time_count{db=~"mysql:.*"}[1m])) by (db,table)
# 表訪問次數
# type: curd
sum(rate(app_db_query_time_count{db=~"mysql:.*"}[30s])) by (type)
# db 響應時間95值
histogram_quantile(0.95, sum(irate(app_db_query_time_bucket{db=~"mysql:.*"}[1m])) by (le))
# db 平均查詢時間
sum(rate(app_db_query_time_sum{db=~"mysql:.*"}[30s]))/sum(rate(app_db_query_time_count{db=~"mysql:.*"}[30s]))

# api qps
sum(rate(app_api_query_time_count{api_name!="self"}[1m])) by (api_name)
# api 平均響應時間
sum(rate(app_api_query_time_sum{api_name!="self"}[1m])) by (api_name)/sum(rate(app_api_query_time_count{api_name!="self"}[1m])) by (api_name)

PS: 關於95值, 熟悉 web 性能指標對 95值 就不陌生了, 直譯是 95%的請求在多久時間內返回, 95值 在監控服務指標上使用廣泛, 如果需要更好的性能, 可以使用 97值 甚至 99值, 作爲衡量標準

一次 PHP微服務 prometheus 落地實踐之旅

監控由微服務基礎設施團隊下的服務保障組來統一維護, 落實好 技術執行(業務開發) / 技術決策(架構師/服務 owner) / 技術支撐(基礎設施/服務保障) 3大角色的分工合作, 上線 prometheus 選擇了對業務方几乎零打擾的方案:

  • prometheus 使用的單獨的 redis 進行存儲
<?php

namespace Mt\Metric;

use Hyperf\Redis\RedisFactory;
use Hyperf\Metric\Adapter\Prometheus\Redis;

class RedisStorageFactory
{
    public function __invoke()
    {
        $redis = container(RedisFactory::class)->get('metric');
        Redis::setPrefix(config('app_name'));
        return Redis::fromExistingConnection($redis);
    }
}
  • 添加默認 metric 配置
'metric' => [
        'enable' => true,
        'default' => 'prometheus',
        // 不適用單獨進程
        'use_standalone_process' => false,
        // 不使用默認 metric
        'enable_default_metric' => false,
        // 5s 統計週期
        'default_metric_interval' => 5,
        'metric' => [
            'prometheus' => [
                'driver' => \Hyperf\Metric\Adapter\Prometheus\MetricFactory::class,
                // 自定義模式
                'mode' => \Hyperf\Metric\Adapter\Prometheus\Constants::CUSTOM_MODE,
                // hyperf/metric 已修復這個問題, 使用 _ 作爲分隔符
                'namespace' => \Hyperf\Utils\Str::camel(env('APP_NAME', 'app')),
            ],
        ],
    ],
  • 統一監控 api/db
<?php

namespace Mt\Metric;

use Hyperf\Metric\Metric;

class Prometheus
{
    public static function isEnable()
    {
        return config('metric.enable') === true;
    }

    public static function apiQuery($api, $http_code, $use_time, $method = '')
    {
        if (!self::isEnable()) {
            return;
        }
        $name = 'api_query_time';
        $labels = [
            'hostname' => php_uname('n'),
            'service' => config('app_name', 'app'),
            'api_name' => $api,
            'code' => (string)$http_code,
            'method' => $method,
        ];
        Metric::put($name, (float)($use_time * 1000), $labels);
    }

    public static function dbQuery($type, $db, $table, $use_time, $result = 'ok')
    {
        if (!self::isEnable()) {
            return;
        }
        $name = 'db_query_time';
        $labels = [
            'hostname' => php_uname('n'),
            'service' => config('app_name', 'app'),
            'type' => $type,
            'db' => $db,
            'table' => $table,
            'result' => $result,
        ];
        Metric::put($name, (float)$use_time, $labels);
    }

    public static function responseCode($api, $response_code)
    {
        if (!self::isEnable()) {
            return;
        }
        $name = 'api_response_code';
        $labels = [
            'hostname' => php_uname('n'),
            'service' => config('app_name', 'app'),
            'api' => $api,
            'response_code' => (string)$response_code,
        ];
        Metric::count($name, 1, $labels);
    }
}
  • 最後一步, 由一個基礎設施的 tools 服務中暴露路由給 prometheus 服務即可
// metrics
$renderer = new RenderTextFormat();
$prometheus_collector = new PrometheusCollector();
$micro_services = ['app1', 'app2', 'app3'];
foreach ($micro_services as $service_name) {
    Router::get('/' . $service_name . '/metrics', function() use($service_name, $prometheus_collector, $renderer) {
        $prometheus_collector->setPrefix($service_name);
        return $renderer->render($prometheus_collector->collect());
    }, ['middleware' => [MetricAuthMiddleware::class]]);
}

寫在最後

prometheus 的監控之旅還沒有走完, 不斷暴露問題, 不斷提升監控的數量和質量, 對了, 用一句經典的話語:

用發展的眼光來看問題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章