1 背景
在微服务项目中,我们通常需要监测客户请求的耗时,进而掌握系统整体的性能情况。
若发现某些请求耗时非常高,那肯定会对客户体验造成影响。
并且高耗时的服务非常容易成为整个服务的瓶颈,在高并发下很可能引发微服务雪崩效应,进而导致整个服务不可用。
2 微服务项目中如何监测请求耗时呢?
例如常见的监测手段是:
- 某个请求的最大耗时。(木桶效应里的最短的那块板)
- 某个请求的耗时百分位。(请求耗时的整体分布情况)
例如:
请求:http://127.0.0.1/hello
最大耗时:300ms [需要重点关注,什么情况下产生这么大的耗时,必须被优化掉]
耗时百分位:
- 50分位,50%:100ms(有50%的请求,耗时低于100ms)[性能很好,耗时较低]
- 90分位,90%:230ms(有90%的请求,耗时低于230ms)[230ms,性能可接受]
- 95分位,95%:260ms(有95%的请求,耗时低于260ms)[260ms,需要优化性能]
- 99分位,99%:270ms(有99%的请求,耗时低于270ms)[270ms,影响客户体验]
3 使用Prometheus的Summary类型来统计HTTP请求耗时
3.1 实践:如何使用Summary类型Metric?
示例代码:
代码语言:javascript复制// 统计http请求耗时
var httpRequestDuration = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Name: "http_request_duration",
Help: "http request duration",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.95: 0.005, 0.99: 0.001},
},
[]string{"endpoint"},
)
采样结果:
代码语言:javascript复制http_request_duration{endpoint="/hello/2",quantile="0.5"} 35
http_request_duration{endpoint="/hello/2",quantile="0.9"} 94
http_request_duration{endpoint="/hello/2",quantile="0.95"} 97
http_request_duration{endpoint="/hello/2",quantile="0.99"} 98
http_request_duration_sum{endpoint="/hello/2"} 1172
http_request_duration_count{endpoint="/hello/2"} 28
Summary类型的Metric会生成三种类型的值:
- xxx_sum:表示“/hello/2”这个请求,耗时的总和。
- xxx_count:表示“/hello/2”这个请求,请求的次数。
- xxx{xxxx, quantile="0.5"}:表示“/hello/2”这个请求,50分位的值,例如上述示例中,50分位值是35,意思是这个url 50%的请求耗时都小于35ms
3.2 源码分析:Summary是如何计算分位数的?
首先看Summary的定义
代码语言:javascript复制type Summary interface {
Metric
Collector
// Observe adds a single observation to the summary.
// 新增一个观测值
Observe(float64)
}
Summary很核心的一个方法是Observe(),在本地增加一个观测值。
再看Summary的实现
代码语言:javascript复制// Summary接口的实现类
type summary struct {
selfCollector
// 省略
objectives map[float64]float64 // 分位数,告诉Summary要统计哪些分位的值
sortedObjectives []float64 // 对分位数进行排序,升序,防止用户输入的分位数是乱序的
labelPairs []*dto.LabelPair
sum float64 // 观测到的数据值的总和
cnt uint64 // 观测的次数
// 省略
streams []*quantile.Stream
streamDuration time.Duration
headStream *quantile.Stream // 存储当前观测数据的地方
headStreamIdx int
// 省略
}
系统观测到的数据放在quantile.Stream里:
代码语言:javascript复制type Stream struct {
*stream // 历史所有观测数据保存在这里,会在一定条件下将b里的观测值merge到stream中
b Samples // Samples类型本质就是[]Sample,保存当前观测到的数据
sorted bool // 是否已排序
}
// stream结构
type stream struct {
n float64 // 数量
l []Sample // 所有观测到的数据
ƒ invariant
}
// 一次观测获取的数据
type Sample struct {
Value float64 `json:",string"`
Width float64 `json:",string"`
Delta float64 `json:",string"`
}
由此可见,每一次观测到的值被包装成一个Sample,然后所有观测到的值放在一个list里。
如何计算分位数?
我们搞清楚了最终的存储结构是List,那计算分位数就明确了。
先对list进行排序,升序。
根据list的长度length,例如取50分位,index=Ceil( length * 0.5 ),list[index]就是最终分位数对应的值。
4 Summary就这么简单?
- 如何并发写list?
- 若通过lock保证写入安全,那怎样保证lock的竞争不会消耗太多时间?
- 高并发写入时,如何保证写入性能?
- 写入的数据量太大时如何存储list?
- 如何保证一个超大的list的数据是有序的?
- 如果一直往list里写数据,内存会不会被干爆?
上述这些问题,会在下一篇文章中详细地介绍。
推荐阅读:
- Prometheus核心概念:一图了解瞬时向量Instant vector和区间向量Range vector的区别
- Prometheus源码分析:基于Go Client自定义的Exporter,是如何在Local存储Metrics的?
- Prometheus核心概念:一图了解Counter和Gauge两种数据指标类型的区别