在本系列文章的上一节通过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相关逻辑。

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐