ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] ### **Verb与Scope** API调用延时是指kube-apiserver的API调用延时。kube-apiserver提供的对API延时的metrics如下: ``` apiserver_request_duration_seconds_bucket{component="apiserver",dry_run="",group="",resource="pods",scope="namespace",subresource="",verb="DELETE",version="v1",le="0.2"} 1289 ``` 在label中有 resource、subresource、scope、verb等关键key。其中scope的取值有cluster、namespace、resource(表示对单个资源对象的操作);verb的取值有GET、LIST、POST、PUT、PATCH、DELETE、WATCH。在apiserver的REST API中,HTTP方法有GET、POST、PUT、PATCH。 我们来举个例子,看一下metrics与REST API的对应关系: ``` # scope GET /api/v1/namespaces/{namespace}/pods/{name} -> resource="pods", subresource="", scope="resource", verb="GET" GET /api/v1/namespaces/{namespace}/pods -> resource="pods", subresource="", scope="namespace", verb="LIST" GET /api/v1/pods -> resource="pods", subresource="", scope="cluster", verb="LIST" # verb - mutating POST /api/v1/namespaces/{namespace}/pods -> resource="pods", subresource="", scope="resource", verb="POST" PATCH /api/v1/namespaces/{namespace}/pods/{name} -> resource="pods", subresource="", scope="resource", verb="PATCH" PUT /api/v1/namespaces/{namespace}/pods/{name} -> resource="pods", subresource="", scope="resource", verb="PUT" DELETE /api/v1/namespaces/{namespace}/pods -> resource="pods", subresource="", scope="namespace", verb="DELETE" # verb - watch GET /api/v1/watch/namespaces/{namespace}/pods/{name} -> resource="pods", subresource="", scope="resource", verb="WATCH" GET /api/v1/watch/namespaces/{namespace}/pods -> resource="pods", subresource="", scope="namespace", verb="WATCH" # subresource POST /api/v1/namespaces/{namespace}/services/{name}/proxy -> resource="services", subresource="proxy", scope="resource", verb="POST" ``` ### **SLI-SLO详解** (1)Mutating API mutating API对应的verb就是POST、PUT、PATCH、DELETE。 > 对于每个(resource, verb)对,过去5分钟内对该类资源单个对象的写操作(mutating) API,第99分位数上的API调用延时,小于1秒。这里的resource不包含虚拟资源、聚合资源、以及CRD。 如何理解?我们举个例子:假设在5分钟内(service, POST)这个API被调用了N次,那么这N个API调用,99%的调用延时要小于1秒;假设在这5分钟内(configmap,DELETE)这个API被调用了M次,那么这M个API调用,99%的调用延时也要小于1秒。在这5分钟内,每个(resource, verb)对的API调用,都要满足99%的调用延时要小于1秒。 还有一个问题, 就是统计哪5分钟?实际上,SLO的理论要求是,对于任意一个时间点,其过去5分钟内的API调用延时都要满足要求。 (2)Non-Streaming Read-Only API Non-Streaming Read-Only API对应的verb为GET、LIST。对某类资源的读操作中,我们可能只读取某一个特定的对象(比如kubectl get pod xxx),也有可能读取命名空间下的所有对象(比如kubectl get pod -n default),也有可能读取集群下的对象(比如kubectl get pod --all-namespaces)。 > 对于每个(resource, scope)对,过去5分钟内对该类资源对象的Non-Streaming读操作API,第99分位数上的API调用延时:(1)如果scope=resource,小于1秒(2)如果scope=namespace或cluster,则小于30秒 举个例子:如果是`kubectl get pod xxx`这个API(scope=resource),那么过去5分钟内这个API的调用中,99%的延时要小于1秒;如果是`kubectl get pod -n default`(scope=namespace),那么过去5分钟内这个API的调用中,99%的延时要小于30秒。 ### **Clusterloader2中的实现** ##### **1、统计方法** API调用延时的两个SLI中,时间窗口为5m,而且理论上要求所有时间点都要满足要求。但要实现测试过程中,不可能获取所有的时间点,clusterloader2是如何判断的呢? 在clusterloader2的源码中,promql查询语句如下: ``` quantile_over_time(0.99, apiserver:apiserver_request_latency_1m:histogram_quantile{%v}[%v]) ``` 上面的第一个参数为label,第二个参数为查询的时间段。 这里我们介绍一下quantile_over_time这个函数。`quantile_over_time(0.99, xxx{..}[1h])`表示:`xxx{...}`指标在1小时内的采样值,按照采样值升序排序,第99分位上的采样值。可以简单理解为这1小时内的采样值,99%的值在哪个范围内。那么这个1小时,是从什么时候开始的呢?其实,这1小时是指Prometheus接收到该查询请求时的前一个小时。 那么,clusterloader2在测试时,这个时间段是如何确定的呢?源码如下: ``` // Latency measurement is based on 5m window aggregation, // therefore first 5 minutes of the test should be skipped. latencyMeasurementDuration := measurementDuration - latencyWindowSize if latencyMeasurementDuration < time.Minute { latencyMeasurementDuration = time.Minute } ``` 从这里可以看出,clusterloader会去掉测试的前5分钟,统计时间段为`[startTime+5m, endTime]`。 接下来,我们再来看一下上面的`apiserver:apiserver_request_latency_1m:histogram_quantile`这个指标是什么意思。在prometheus的manifest中,有一个prometheus.rules文件,有如下内容: ``` - name: apiserver.1m.rules rules: - expr: | histogram_quantile(0.99, sum(rate(apiserver_request_duration_seconds_bucket[1m])) by (resource, subresource, verb, scope, le)) record: apiserver:apiserver_request_latency_1m:histogram_quantile labels: quantile: "0.99" ``` record表示把一个指标,先计算出来,保存为另一个指标,这样在查询的时候就不需要再计算,可以加快查询效率。可以看出,`apiserver:apiserver_request_latency_1m:histogram_quantile`就是各类(resource, subresource, verb, scope)API请求的第99分位值。但需要注意的是,这里的窗口为1m,而不是SLI中的5m。 > 到现在,我们基本了解了clusterloader2的统计方法。首先,它先以1m为时间窗口,统计这1m内每类API请求的99%的响应时间,得到一个新的序列值,然后,对于这个新的序列值,计算某段时间内所有样本点中99%的样本的最大值。也就是说,clusterloader2统计时,实际上有两个0.99。 ##### **2、其他差异点** 在SLO中,scope=namespace与scope=cluster的阈值都是30秒,但是在clusterloader2测试时,scope=namespace的阈值为5秒,如下: ``` resourceThreshold time.Duration = 1 * time.Second namespaceThreshold time.Duration = 5 * time.Second clusterThreshold time.Duration = 30 * time.Second ``` ### **Grafana展示** 在Grafana的SLO这个dashboard中,展示了各类API的调用延时(这里我们截图的是5m窗口,不是1m) ![](https://img.kancloud.cn/77/92/779242fb0ee2a43ea00104933c4a37ab_3840x1491.png) ![](https://img.kancloud.cn/fd/80/fd80f8df959035961431742faf03c48d_3840x1237.png) ##### **quantile_over_time** 首先,我们看一下这些图表的promql语句,以第一个 Read-only API call latency(scope=resource, threshold=1s)为例,它的查询语句如下: ``` quantile_over_time(0.99, apiserver:apiserver_request_latency:histogram_quantile{quantile="0.99", verb=~"GET", scope=~"resource"}[12h]) ``` 可以看出它使用的是quantile_over_time,即与clusterloader2中的统计方法是一样的。不一样的地方在于,这里它的时间段为12h。也就说是,图中的每一个点(比如时间点为T),都要对过去12个小时内`[T-12h, T]`的数据做一次运算。所以这个结果可能会和clusterloader2有较大的出入。即可以简单认为,假设clusterloader2的运行时长为12h+5m,那么clusterloader2的结果只是图中的某一个点。 上面的展示感觉比较复杂,为了能够更直观地判断API调用延时,建议查询语句换成: ``` apiserver:apiserver_request_latency:histogram_quantile{quantile="0.99", verb=~"GET", scope=~"resource"} ``` 此时,每个点的值就表示该点在过去的5分钟内,99%的API请求的延时。那么,在测试的过程中,我们只需要看一下测试时间段内,每个点是否都在threshold内(clusterloader2是99%的点在threhold内)。 ##### **filter(subresource)** 在clusterloader2中的查询语句中: ``` quantile_over_time(0.99, apiserver:apiserver_request_latency_1m:histogram_quantile{%v}[%v]) ``` 第一个参数为filter,它的定义如下: ``` filters = `verb!="WATCH", subresource!~"log|exec|portforward|attach|proxy"` ``` 也就是说,subresource不能为上述几个。而在Grafana的语句中,却没有做这样的限制,所以有时间Grafana中经常出现`{resource="service", subresource="proxy"}`的API超过threshold。 因此,我们需要对Grafana中的语句做更进一步的调整,以第一个 Read-only API call latency(scope=resource, threshold=1s)为例,应该调整为: ``` apiserver:apiserver_request_latency:histogram_quantile{quantile="0.99", verb=~"GET", scope=~"resource", subresource!~"log|exec|portforward|attach|proxy"} ``` ##### **Mutating API call latency (threshold=1s)** 这个pannel中查询语句如下: ``` quantile_over_time(0.99, apiserver:apiserver_request_latency:histogram_quantile{quantile="0.99", verb=~"CREATE|DELETE|PATCH|POST|PUT", scope=~"namespace|cluster"}[12h]) ``` 它的scope为namespace或cluster,很明显在SLI中没有对namespace或cluster级别的mutating API有定义。这里我们应该改成SLO中的对单个资源对象mutating操作的API,如下: ``` apiserver:apiserver_request_latency:histogram_quantile{quantile="0.99", verb=~"CREATE|DELETE|PATCH|POST|PUT", scope=~"resource"} ``` 当然,如果要和clusterloader2保持一致,应该还要加上subresource的限制。 ### **APIResponsivenessPrometheusSimple** 在密度测试中会有如下的measurement ``` - name: Collecting measurements measurements: - Identifier: APIResponsivenessPrometheusSimple Method: APIResponsivenessPrometheus Params: action: gather enableViolations: true useSimpleLatencyQuery: true summaryName: APIResponsivenessPrometheus_simple ``` 也就是Params中有`useSimpleLatencyQuery: true`,这是什么意思呢?我们来看一下源码: ``` // simpleLatencyQuery measures 99th percentile of API call latency over given period of time // it doesn't match SLI, but is useful in shorter tests, where we don't have enough number of windows to use latencyQuery meaningfully. // // simpleLatencyQuery: placeholders should be replaced with (1) quantile (2) filters and (3) query window size. simpleLatencyQuery = "histogram_quantile(%.2f, sum(rate(apiserver_request_duration_seconds_bucket{%v}[%v])) by (resource, subresource, verb, scope, le))" ``` 也就是说,在某些情况,如果测试在短时间内就运行结束了,比如还没有5分钟(当然也可以超过5分钟),那么此时就直接统计这段时间内的API调用延时。比如说,测试运行了N分钟,那么直接统计这N分钟内99%的API的调度延时,不再以5分钟做为时间窗口。 ### **其它源码** APIResponsivenessPrometheus是一个基于PrometheusMeasurement的Measurement。APIResponsivenessPrometheus没有结构体定义,当我们在config.yaml文件中使用它时,start与gather实际上调用的是PrometheusMeasurement的Execute函数。 我们先来看一下该Measurement的Register函数,如下: ``` func init() { create := func() measurement.Measurement { return common.CreatePrometheusMeasurement(&apiResponsivenessGatherer{}) } if err := measurement.Register(apiResponsivenessPrometheusMeasurementName, create); err != nil { klog.Fatalf("Cannot register %s: %v", apiResponsivenessPrometheusMeasurementName, err) } } ``` 它实际调用的函数为common.CreatePrometheusMeasurement(&apiResponsivenessGatherer{}),该函数如下: ``` func CreatePrometheusMeasurement(gatherer Gatherer) measurement.Measurement { return &prometheusMeasurement{ gatherer: gatherer, } } ``` 也就是说,APIResponsivenessPrometheus它的Measurement对象实际上是一个PrometheusMeasurement对象,而PrometheusMeasurement对象中的gather变量的值为`&apiResponsivenessGatherer{}`。 根据 [PrometheusMeasurement]() 一文的介绍,每个基于PrometheusMeasurement的Measurement都需要实现Gather接口,而这里就是apiResponsivenessGatherer。 to be continued ### **FAQ** Q:API调用延时的两个SLI中,要求在过去5分钟内99%的API调用延时要小于阈值,而如果测试时间持续N分钟,取哪个时间点的值做为测试通过的依据?还是要取这N分钟内每个采样点的值? A:在源码中,使用的Promql语句为 ``` quantile_over_time(0.99, apiserver:apiserver_request_latency_1m:histogram_quantile{%v}[%v] ``` 上面第一个参数为label,第二个参数持续时间,实际的查询例子如下: ``` I0908 16:12:04.582486 23317 prometheus.go:109] Executing "quantile_over_time(0.99, apiserver:apiserver_request_latency_1m:histogram_quantile{verb!=\"WATCH\", subresource!~\"log|exec|portforward|attach|proxy\"}[95m])" at 2022-09-08T16:12:04+08:00 ``` 那么这个查询语句代表什么意思呢? 在prometheus的manifest中,有一个prometheus.rules文件,有如下内容: ``` - name: apiserver.1m.rules rules: - expr: | histogram_quantile(0.99, sum(rate(apiserver_request_duration_seconds_bucket[1m])) by (resource, subresource, verb, scope, le)) record: apiserver:apiserver_request_latency_1m:histogram_quantile labels: quantile: "0.99" ``` 也就是说,`apiserver:apiserver_request_latency_1m:histogram_quantile`其实是`histogram_quantile(0.99, sum(rate(apiserver_request_duration_seconds_bucket[1m]))`的一个别名。 那么上面的`quantile_over_time`查询语句表示是的,在查询的这95min时间内,每个采样点值。是从什么开始的95min呢?实际上,调用prometheus执行这个语句时,还会传过去一个endtime,即从endtime向前的95min。 所以,API调用延时那两个SLI,clusterloader2中的判断条件实际上就是在