通過prometheus實現k8s hpa自定義指標 (三)
在本系列文章的上一節通過prometheus實現k8s hpa自定義指標 (二),我們從開發者角度理解k8s hpa工作的流程。這節我們將通過一個最基礎的custom metrics API server介紹開發一個自定義metrics-apiserver需要完成哪些方面的工作,作為後繼分析prometheus-adapter的基礎。
開發Custom Metrics API Server
介面卡有兩個部分: setup code和providers。setup code用於初始化API server,providers處理對metrics的API請求。
1. 編寫provider
目前provider有兩個介面,對應於兩個不同的API:custom metrics API(用於描述k8s物件的metrics),另外一個是external metrics API(不用描述k8s物件的metrics,或者不用於附加到特定物件的metrics)。為了簡潔起見,本文只展示custom metrics API。
將你的provider放在你專案的pkg/provider目錄。
首先得引入以下包
package provider
import (
"fmt"
"time"
"github.com/golang/glog"
apierr "k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/metrics/pkg/apis/custom_metrics"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider"
"k8s.io/metrics/pkg/apis/external_metrics"
)
所要實現的custom metrics provider介面成為CustomMetricsProvider,說下所示:
type CustomMetricsProvider interface {
ListAllMetrics() []CustomMetricInfo
GetMetricByName(name types.NamespacedName, info CustomMetricInfo) (*custom_metrics.MetricValue, error)
GetMetricBySelector(namespace string, selector labels.Selector, info CustomMetricInfo) (*custom_metrics.MetricValueList, error)
}
通過上述介面可以列出所有的可用metrics,用於API中的資訊發現,以便客戶端可以知道哪些metrics是可以用的。同時不允許失敗(它不會返回任何錯誤),並且請求應該快速返回,因此在程式碼中最好是要非同步更新。
如下是返回幾個固定命名的metrics,其中兩個metrics是具備名稱空間的,其中一個本身就是namespaces,屬於root-scoped:
func (p *yourProvider) ListAllMetrics() []provider.CustomMetricInfo {
return []provider.CustomMetricInfo{
// these are mostly arbitrary examples
{
GroupResource: schema.GroupResource{Group: "", Resource: "pods"},
Metric: "packets-per-second",
Namespaced: true,
},
{
GroupResource: schema.GroupResource{Group: "", Resource: "services"},
Metric: "connections-per-second",
Namespaced: true,
},
{
GroupResource: schema.GroupResource{Group: "", Resource: "namespaces"},
Metric: "work-queue-length",
Namespaced: false,
},
}
}
接下來,需要實現獲取metrics的實際的方法。這些方法獲取的metrics可以描述任意k8s資源,包括root-scoped和namespaced-scoped。當然,這些metrics也可以以單個物件形式獲取,也可以通過selector方式獲取一組物件的metrics。
這裡需要一個RESTMapper(用於對映resources和kinds)和動態客戶端來獲取叢集中的物件列表。
type yourProvider struct {
client dynamic.Interface
mapper apimeta.RESTMapper
// just increment values when they're requested
values map[provider.CustomMetricInfo]int64
}
接著可以實現獲取metrics的方法。這裡,這些方法只是在獲取metrics時遞增metric值,在真正的介面卡中,這些metrics需要從真正的後端獲取。
這裡提供假的"獲取"操作,並構造一個結果物件返回。
// valueFor fetches a value from the fake list and increments it.
func (p *yourProvider) valueFor(info provider.CustomMetricInfo) (int64, error) {
// normalize the value so that you treat plural resources and singular
// resources the same (e.g. pods vs pod)
info, _, err := info.Normalized(p.mapper)
if err != nil {
return 0, err
}
value := p.values[info]
value += 1
p.values[info] = value
return value, nil
}
// metricFor constructs a result for a single metric value.
func (p *testingProvider) metricFor(value int64, name types.NamespacedName, info provider.CustomMetricInfo) (*custom_metrics.MetricValue, error) {
// construct a reference referring to the described object
objRef, err := helpers.ReferenceFor(p.mapper, name, info)
if err != nil {
return nil, err
}
return &custom_metrics.MetricValue{
DescribedObject: objRef,
MetricName: info.Metric,
// you'll want to use the actual timestamp in a real adapter
Timestamp: metav1.Time{time.Now()},
Value: *resource.NewMilliQuantity(value*100, resource.DecimalSI),
}, nil
}
接著要實現連個主要的方法,第一個是獲取一個物件的單個metric值(例如,HorizontalPodAutoScaler中的物件metric型別):
func (p *yourProvider) GetMetricByName(name types.NamespacedName, info provider.CustomMetricInfo) (*custom_metrics.MetricValue, error) {
value, err := p.valueFor(info)
if err != nil {
return nil, err
}
return p.metricFor(value, name, info)
}
第二個是獲取多個metric值,用於集合中的每個物件(例如HorizontalPodAutoScaler中的Pods metric型別)。
func (p *yourProvider) GetMetricBySelector(namespace string, selector labels.Selector, info provider.CustomMetricInfo) (*custom_metrics.MetricValueList, error) {
totalValue, err := p.valueFor(info)
if err != nil {
return nil, err
}
names, err := helpers.ListObjectNames(p.mapper, p.client, namespace, selector, info)
if err != nil {
return nil, err
}
res := make([]custom_metrics.MetricValue, len(names))
for i, name := range names {
// in a real adapter, you might want to consider pre-computing the
// object reference created in metricFor, instead of recomputing it
// for each object.
value, err := p.metricFor(100*totalValue/int64(len(res)), types.NamespacedName{Namespace: namespace, Name: name}, info)
if err != nil {
return nil, err
}
res[i] = *value
}
return &custom_metrics.MetricValueList{
Items: res,
}, nil
}
最後,將provider當作API server的外掛即可。
2. 編寫setup code
首先,引入下列包
package main
import (
"flag"
"os"
"github.com/golang/glog"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/util/logs"
basecmd "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/cmd"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider"
// make this the path to the provider that you just wrote
yourprov "github.com/user/repo/pkg/provider"
)
接著利用basecmd.AdapterBase結構幫助設定API server:
type YourAdapter struct {
basecmd.AdapterBase
// the message printed on startup
Message string
}
func main() {
logs.InitLogs()
defer logs.FlushLogs()
// initialize the flags, with one custom flag for the message
cmd := &YourAdapter{}
cmd.Flags().StringVar(&cmd.Message, "msg", "starting adapter...", "startup message")
cmd.Flags().AddGoFlagSet(flag.CommandLine) // make sure you get the glog flags
cmd.Flags().Parse(os.Args)
provider := cmd.makeProviderOrDie()
cmd.WithCustomMetrics(provider)
// you could also set up external metrics support,
// if your provider supported it:
// cmd.WithExternalMetrics(provider)
glog.Infof(cmd.Message)
if err := cmd.Run(wait.NeverStop); err != nil {
glog.Fatalf("unable to run custom metrics adapter: %v", err)
}
}
最後,只要給我們的provider新增一點設定程式碼即可。當然你的provider可能會有其它選項,比如你需要與後端metrics解決方案的連線配置,證書或者其它高階配置。對於上面編寫的provider,安裝程式碼如下所示:
func (a *SampleAdapter) makeProviderOrDie() provider.CustomMetricsProvider {
client, err := a.DynamicClient()
if err != nil {
glog.Fatalf("unable to construct dynamic client: %v", err)
}
mapper, err := a.RESTMapper()
if err != nil {
glog.Fatalf("unable to construct discovery REST mapper: %v", err)
}
return yourprov.NewProvider(client, mapper)
}
總結
本節介紹瞭如何去開發自己的custom Metrics API Server,主要是編寫provider和setup code,對於我們後繼即將分析的prometheus adapter具有一定的幫助作用,剝離底層實現,我們在第四節將主要關注adapter外掛的metric相關邏輯。