1. 程式人生 > >通過prometheus實現k8s hpa自定義指標 (三)

通過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相關邏輯。