0%

Prometheus 的客户端库中提供了四种核心的指标类型:Counter(计数器)、Guage(仪表盘)、Histogram(直方图)、Summary(摘要)。但这些类型只是在客户端库中存在,实际在 Prometheus 上并不对指标类型进行区分,而是简单地把这些指标类型统一视为无类型的时间序列。

Counter(计数器)

Counter 类型代表一种样本数据单调递增的指标,即只增不减,除非监控系统发生了重置。例如,你可以使用 counter 类型的指标来表示服务的请求数、已完成的任务数、错误发生的次数等。counter 主要有两个方法:

1
2
3
4
// 将 counter 值加1
Inc()
// 将指定的值加到 counter 值上,如果指定值 < 0 会 panic
Add(float64)

Counter 类型数据可以让用户方便的了解事件发生的速率的变化,在 PromQL 内置的相关操作函数可以提供相应的分析,比如以 HTTP 应用请求量来进行说明:

1
2
3
4
// 通过 rate() 函数获取 HTTP 请求量的增长率
rate(http_requests_total[5m])
// 查询当前系统中,访问量前 10 的 HTTP 地址
topk(10, http_requests_total)

不要将 counter 类型应用于样本数据非单调递增的指标,例如:当前运行的进程数量(应该用 Guage 类型)。

Guage(仪表盘)

Guage 类型代表一种样本数据可以任意变化的指标,即可增可减。guage 通常用于像温度或者内存使用率这种指标数据,也可以表示能随时增加或减少的 "总数",例如:当前并发请求的数量。

对于 Guage 类型的监控指标,通过 PromQL 内置函数 delta() 可以获取样本在一段时间内的变化情况,例如,计算 CPU 温度在两小时内的差异:

1
delta(cpu_temp_celsius{host="zeus"}[2h])

你还可以通过 PromQL 内置函数 predict_linear() 基于简单线性回归的方式,对样本数据的变化趋势做出预测。例如,基于 2 小时的样本数据,来预测主机可用磁盘空间在 4 个小时之后的剩余情况:

1
predict_linear(node_filesystem_free{job="node"}[2h], 4 * 3600) < 0

Histogram(直方图)

在大多数情况下人们都倾向于使用某些量化指标的平均值,例如 CPU 的平均使用率、页面的平均响应时间。这种方式的问题很明显,以系统 API 调用的平均响应时间为例:如果大多数 API 请求都维持在 100ms 的响应时间范围内, 而个别请求的响应时间需要 5s,那么就会导致某些 web 页面的响应时间落到中位数的情况,而这种现象被称为长尾问题。

为了区分是平均的慢还是长尾的慢,最简单的方式就是按照请求延迟的范围进行分组。例如,统计延迟在 0~10ms 之间的请求数有多少而 10~20ms 之间的请求数又有多少,通过这种方式可以快速分析系统慢的原因。 Histogram 和 Summary 都是为了能够解决这样问题的存在,通过 Histogram 和 Summary 类型的监控指标,我们可以快速了解监控样本的分布情况。

Histogram 在一段时间范围内对数据进行采样(通常是请求持续时间或响应大小等),并将其计入可配置的存储桶(bucket)中,后续可通过指定区间筛选样本,也可以统计样本总数,最后一般将数据展示为直方图。

Histogram 类型的样本会提供三种指标(假设指标名称为 ):

  • 样本的值分布在 bucket 中的数量,命名为 _bucket{le="<上边界>"}。解释得更通俗易懂一点,这个值表示指标值小于等于上边界的所有样本数量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 在总共2次请求当中。http 请求响应时间 <=0.005 秒 的请求次数为0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.005",} 0.0
// 在总共2次请求当中。http 请求响应时间 <=0.01 秒 的请求次数为0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.01",} 0.0
// 在总共2次请求当中。http 请求响应时间 <=0.025 秒 的请求次数为0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.025",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.05",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.075",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.1",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.25",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.5",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.75",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="1.0",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="2.5",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="5.0",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="7.5",} 2.0
// 在总共2次请求当中。http 请求响应时间 <=10 秒 的请求次数为 2
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="10.0",} 2.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="+Inf",} 2.0
  • 所有样本值的大小总和,命名为 _sum
1
2
// 实际含义:发生的2次 http 请求总的响应时间为 13.107670803000001 秒
io_namespace_http_requests_latency_seconds_histogram_sum{path="/",method="GET",code="200",} 13.107670803000001
  • 样本总数,命名为 _count。值和 _bucket{le="+inf"} 相同。
1
2
// 实际含义:当前一共发生了 2 次 http 请求
io_namespace_http_requests_latency_seconds_histogram_count{path="/",method="GET",code="200",} 2.0

可以通过 histogram_quantile() 函数来计算 Histogram 类型样本的分位数。分位数可能不太好理解,我举个例子,假设你要计算样本的 9 分位数(quantile=0.9), 即 90% 的样本的值。Histogram 还可以用来计算应用性能指标值(Apdex score)。

Summary(摘要)

与 Histogram 类型类似,用于表示一段时间内的数据采样结果(通常是请求持续时间或响应大小等),但它直接存储了分位数(通过客户端计算,然后展示出来),而不是通过区间来计算。

Summary 类型的样本也会提供三种指标(假设指标名称为):

  • 样本值的分位数分布情况,命名为 {quantile="<φ>"}
1
2
3
4
// 含义:这 12 次 http 请求中有 50% 的请求响应时间是 3.052404983s
io_namespace_http_requests_latency_seconds_summary{path="/",method="GET",code="200",quantile="0.5",} 3.052404983
// 含义:这 12 次 http 请求中有 90% 的请求响应时间是 8.003261666s
io_namespace_http_requests_latency_seconds_summary{path="/",method="GET",code="200",quantile="0.9",} 8.003261666
  • 所有样本值的大小总和,命名为 _sum
1
2
// 含义:这12次 http 请求的总响应时间为 51.029495508s
io_namespace_http_requests_latency_seconds_summary_sum{path="/",method="GET",code="200",} 51.029495508
  • 样本总数,命名为 _count
1
2
// 含义:当前一共发生了 12 次 http 请求
io_namespace_http_requests_latency_seconds_summary_count{path="/",method="GET",code="200",} 12.0

现在可以总结一下 Histogram 与 Summary 的异同:

  • 它们都包含了 _sum 和 _count 指标。

  • Histogram 需要通过 _bucket 来计算分位数,而 Summary 则直接存储了分位数的值。

在 web 应用中,我们有很大几率会遇到一个请求无响应的情况,这种情况往往是某些系统调用阻塞导致的(比如 MySQL、MongoDB 查询一直没有返回),我们该怎么来知道是什么原因导致的呢?

在 linux 下,我们可以使用 ltrace 来查看进程当前的状态。

使用方法:

script
1
ltrace -p 1234

这里的 1234 是进程 id。

但是有个需要注意的是,对于已经阻塞的进程,通过 ltrace 无法知道当前阻塞在什么系统调用上。不过如果可以在进程处理请求之前拿到进程 id 的话,就可以在请求到来之前先调用 ltrace,然后就可以看到请求过程中的系统调用。

另外有个类似的命令 strace。

具体情况大概是这样的,我们有一个 MySQL 的模型,有一个 Mongo 的模型,然后在 MySQL 的模型中 hasOne Mongo 的模型,结果:

MySQL with 的时候可以查询出数据,但是使用 whereHas 的时候查询不到数据了。

环境

  • laravel/lumen-framework: 5.5.2
  • jenssegers/mongodb: 3.3.2

重现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A extends \Illuminate\Database\Eloquent\Model
{
// 可以让我们关联 MongoDB 的一个 trait
use \Jenssegers\Mongodb\Eloquent\HybridRelations;

public function b()
{
return $this->hasOne(B::class, '_id', 'b_id');
}
}

class B extends \Jenssegers\Mongodb\Eloquent\Model
{

}

Laravel 的关联查询如下:

1
2
3
4
5
6
7
$a = A::query()->with('b')->whereKey(10)->first();
// $a 可以查询出来
dump($a);

$a = A::query()->whereHas('b')->whereKey(10)->first();
// $a 为 null
dump($a);

正常情况下,其实第二个查询我们也应该查询出来才对,可是我们查不到,然后尝试打印一下查询:

1
2
3
4
[2021-06-08 09:54:30] lumen.INFO: time: 75.18 select * from `as` where `as`.`id` = 10 limit 1  
[2021-06-08 09:54:30] lumen.INFO: time: 48.25, connection: mongo_core, db.bs.find({"_id":{"$in":["60becd2306bce821e55ffce3"]}})
[2021-06-08 09:54:30] lumen.INFO: time: 2.52, connection: mongo_core, db.bs.find({"projection":{"_id":true}})
[2021-06-08 09:54:30] lumen.INFO: time: 18.81 select * from `as` where `id` in ("60becd2306bce821e55ffce3") and `as`.`id` = 10 limit 1

我们发现最后一条语句并不是如我们期待的那样,where b_id in 而是 where id in,这是一个不正常的行为,因为根据我们的定义,关联 A 模型的字段应该是 b_id 而不是 id

原因

因为想找到查询使用 id 而不是 b_id,对关联部分的代码 debug 了一下,最后发现,位于 \Jenssegers\Mongodb\Helpers\QueriesRelationships 的方法 getRelatedConstraintKey 返回了一个错误的关联键:

QueriesRelationships.php v3.3.2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Returns key we are constraining this parent model's query with
* @param $relation
* @return string
* @throws \Exception
*/
protected function getRelatedConstraintKey($relation)
{
if ($relation instanceof HasOneOrMany) {
// 这里返回了关联模型的主键(id),但是实际上应该返回的是关联里面指定的那个键(b_id)
return $this->model->getKeyName();
}

if ($relation instanceof BelongsTo) {
return $relation->getForeignKey();
}

throw new \Exception(class_basename($relation) . ' Is Not supported for hybrid query constraints!');
}

感觉这是一个 bug,然后去新版本的库里面看了一下,发现新版本里面已经修改过来了:

QueriesRelationships.php v3.7.3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Returns key we are constraining this parent model's query with
* @param Relation $relation
* @return string
* @throws Exception
*/
protected function getRelatedConstraintKey(Relation $relation)
{
if ($relation instanceof HasOneOrMany) {
// 返回了正确的关联键
return $relation->getLocalKeyName();
}

if ($relation instanceof BelongsTo) {
return $relation->getForeignKeyName();
}

if ($relation instanceof BelongsToMany && !$this->isAcrossConnections($relation)) {
return $this->model->getKeyName();
}

throw new Exception(class_basename($relation) . ' is not supported for hybrid query constraints.');
}

结论

由于修复的版本与当前使用的版本相差太多,而且依赖于 Laravel 的版本,所以无法升级其版本,只有换一种写法了。(whereHas 拆分出来,先查询到关联数据的 _id,然后作为 whereIn 条件传递)。

有时候需要根据需要对 MongoDB 的集合进行分集合存储(类似 MySQL 的分表),这样一来 db 下就产生了很多相同前缀的集合,这样一来,我们想知道这一批集合的一些关键状态信息的时候就比较不便,比如想知道某一个前缀所有集合的总大小等等。

一个可行的方法是,通过找到一个模式前缀的所有集合,然后逐个调用 MongoDB 的 collStats 命令来获取单个集合的状态,然后做一个聚合,下面是基于 jenssegers mongodb 的一个实现:

统计 product 库下 xx_ 前缀的所有集合的大小等

输出的字段说明:

  • collection_count: xx_ 前缀的集合总数量
  • total_document_count: xx_ 前缀文档总数量
  • total_size: xx_ 前缀集合总大小
  • avg_obj_size: xx_ 前缀集合平均单个文档的大小
  • total_storage_size: xx_ 前缀集合占的总存储大小
  • total_index_size: xx_ 前缀集合索引总大小
  • total_bytes_in_cache: xx_ 前缀集合数据当前占用的缓存大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
$patterns = [
'product.xx_'
];

function format($bytes): string
{
if ($bytes < 1024) {
return "{$bytes} bytes";
}

if ($bytes < 1024 * 1024) {
$bytes = round($bytes / 1024, 2);

return "{$bytes} Kb";
}

if ($bytes < 1024 * 1024 * 1024) {
$bytes = round($bytes / 1024 / 1024, 2);

return "{$bytes} Mb";
}

$bytes = round($bytes / 1024 / 1024 / 1024, 2);

return "{$bytes} Gb";
}

class CollectionStats
{
private $stats;

public function __construct(array $stats)
{
$this->stats = $stats;
}

public function toArray(): array
{
return [
'collection' => $this->stats['ns'],
'size' => format($this->stats['size']),
'count' => $this->stats['count'],
'avgObjSize' => format($this->stats['avgObjSize'] ?? 0),
'storageSize' => format($this->stats['storageSize']),
'totalIndexSize' => format($this->stats['totalIndexSize']),

'raw' => [
'count' => $this->stats['count'],
'size' => $this->stats['size'],
'avgObjSize' => $this->stats['avgObjSize'] ?? 0,
'storageSize' => $this->stats['storageSize'],
'totalIndexSize' => $this->stats['totalIndexSize'],
'bytes_in_cache' => $this->stats['wiredTiger']['cache']['bytes currently in the cache'],
],
];
}
}

foreach ($patterns as $pattern) {
// $connectionName 连接名
// $collectionPrefix Collection 前缀
[$connectionName, $collectionPrefix] = explode('.', $pattern);

dump(compact('connectionName', 'collectionPrefix'));

$collections = get_collection_names($connectionName);
$collections = collect($collections)
->filter(function ($collection) use ($collectionPrefix) {
return Str::startsWith($collection, $collectionPrefix);
})
->values()
->toArray();

// 查看 Collection 的状态:
// https://docs.mongodb.com/manual/reference/command/
// https://docs.mongodb.com/manual/reference/command/collStats/#mongodb-dbcommand-dbcmd.collStats

$result = [];

foreach ($collections as $collection) {
$cursor = DB::connection($connectionName)->getMongoDB()->command(['collStats' => $collection]);
$res = json_decode(json_encode($cursor->toArray()[0]), true);
$reply = json_encode($res, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

$result[] = (new CollectionStats($res))->toArray();
}

$results = collect($result);

dump('collection_count: ' . $results->count());
dump('total_document_count: ' . $results->sum('raw.count'));
dump('total_size: ' . format($results->sum('raw.size')));
dump('avg_obj_size: ' . format($results->avg('raw.avgObjSize')));
dump('total_storage_size: ' . format($results->sum('raw.storageSize')));
dump('total_index_size: ' . format($results->sum('raw.totalIndexSize')));
dump('total_bytes_in_cache: ' . format($results->sum('raw.bytes_in_cache')));
echo PHP_EOL;
}

输出:

1
2
3
4
5
6
7
8
9
10
11
array:2 [
"connectionName" => "product"
"collectionPrefix" => "xx_"
]
"collection_count: 222"
"total_document_count: 1664601"
"total_size: 2.24 Gb"
"avg_obj_size: 1.4 Kb"
"total_storage_size: 996.66 Mb"
"total_index_size: 156.34 Mb"
"total_bytes_in_cache: 3.17 Gb"

其他

这个例子是基于 Laravel 的一个库来实现的,不过只是简单地调用了一个 command 方法,其他语言利用 MongoDB 提供的库也比较容易可以实现。

是这样的,在服务器上的 php 需要 zip 库,但我自己手动编译了一个 zip 库,安装在了 /usr/local/lib64 下。但 php 编译 zip 扩展的时候提示找不到 libzip。

解决方法

/usr/local/lib64 也加入到系统共享库的搜索路径,我们可以通过 ldconfig

script
1
2
echo "/usr/local/lib64" > /etc/ld.so.conf.d/local.conf
ldconfig

然后重新 configure。