重新認識Clientset

重新認識Clientset

1、介紹

Clientset 是調用 Kubernetes 資源對象最常用的客戶端,可以操作所有的資源對象。

那麼在 Clientset 中使如何用這些資源的呢?

因爲在 staging/src/k8s.io/api 下面定義了各種類型資源的規範,然後將這些規範註冊到了全局的 Scheme 中。這樣就可以在Clientset中使用這些資源了。

2、示例

首先我們來看下如何通過 Clientset 來獲取資源對象,我們這裏來創建一個 Clientset 對象,然後通過該對象來獲取默認命名空間之下的 Deployments 列表,代碼如下所示:

package main

import (
	"flag"
	"fmt"
	"os"
	"path/filepath"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
)

func main() {
	var err error
	var config *rest.Config
	var kubeconfig *string

	if home := homeDir(); home != "" {
		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
	} else {
		kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
	}
	flag.Parse()

	// 使用 ServiceAccount 創建集羣配置(InCluster模式)
	if config, err = rest.InClusterConfig(); err != nil {
		// 使用 KubeConfig 文件創建集羣配置
		if config, err = clientcmd.BuildConfigFromFlags("", *kubeconfig); err != nil {
			panic(err.Error())
		}
	}

	// 創建 clientset
	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		panic(err.Error())
	}

	// 使用 clientsent 獲取 Deployments
	deployments, err := clientset.AppsV1().Deployments("default").List(metav1.ListOptions{})
	if err != nil {
		panic(err)
	}
	for idx, deploy := range deployments.Items {
		fmt.Printf("%d -> %s\n", idx+1, deploy.Name)
	}

}

func homeDir() string {
	if h := os.Getenv("HOME"); h != "" {
		return h
	}
	return os.Getenv("USERPROFILE") // windows
}

這是一個非常典型的訪問 Kubernetes 集羣資源的方式,通過 client-go 提供的 Clientset 對象來獲取資源數據,主要有以下三個步驟:

  1. 使用 kubeconfig 文件或者 ServiceAccount(InCluster 模式)來創建訪問 Kubernetes API 的 Restful 配置參數,也就是代碼中的 rest.Config 對象
  2. 使用 rest.Config 參數創建 Clientset 對象,這一步非常簡單,直接調用 kubernetes.NewForConfig(config) 即可初始化
  3. 然後是 Clientset 對象的方法去獲取各個 Group 下面的對應資源對象進行 CRUD 操作

3、Clientset 對象

上面我們瞭解瞭如何使用 Clientset 對象來獲取集羣資源,接下來我們來分析下 Clientset 對象的實現。

上面我們使用的 Clientset 實際上是對各種資源類型的 Clientset 的一次封裝:

// staging/src/k8s.io/client-go/kubernetes/clientset.go

// NewForConfig 通過給定的 config 創建一個新的 Clientset
func NewForConfig(c *rest.Config) (*Clientset, error) {
	configShallowCopy := *c

	if configShallowCopy.UserAgent == "" {
		configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent()
	}

	// share the transport between all clients
	httpClient, err := rest.HTTPClientFor(&configShallowCopy)
	if err != nil {
		return nil, err
	}

	return NewForConfigAndClient(&configShallowCopy, httpClient)
}


func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) {
	configShallowCopy := *c
	if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 {
		if configShallowCopy.Burst <= 0 {
			return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0")
		}
		configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst)
	}

	var cs Clientset
	var err error
  
    	// 將其他 Group 和版本的資源的 RESTClient 封裝到全局的 Clientset 對象中
	cs.admissionregistrationV1, err = admissionregistrationv1.NewForConfigAndClient(&configShallowCopy, httpClient)
	if err != nil {
		return nil, err
	}
 
  cs.appsV1, err = appsv1.NewForConfigAndClient(&configShallowCopy, httpClient)
	if err != nil {
		return nil, err
	}
  ......
  return &cs, nil
}

上面的 NewForConfig 函數最後調用了NewForConfigAndClient函數,NewForConfigAndClient裏面就是將其他的各種資源的 RESTClient 封裝到了全局的 Clientset 中,這樣當我們需要訪問某個資源的時候只需要使用 Clientset 裏面包裝的屬性即可,比如 clientset.CoreV1() 就是訪問 Core 這個 Group 下面 v1 這個版本的 RESTClient。這些局部的 RESTClient 都定義在 staging/src/k8s.io/client-go/typed///_client.go 文件中,比如 staging/src/k8s.io/client-go/kubernetes/typed/apps/v1/apps_client.go 這個文件中就是定義的 apps 這個 Group 下面的 v1 版本的 RESTClient,這裏同樣以 Deployment 爲例:

// staging/src/k8s.io/client-go/kubernetes/typed/apps/v1/apps_client.go

func NewForConfig(c *rest.Config) (*AppsV1Client, error) {
	config := *c
	if err := setConfigDefaults(&config); err != nil {
		return nil, err
	}
	httpClient, err := rest.HTTPClientFor(&config)
	if err != nil {
		return nil, err
	}
  
 	 // 最後還是通過調用 NewForConfigAndClient 函數,返回AppsV1這個資源對象的Clientset
	return NewForConfigAndClient(&config, httpClient)
}

func setConfigDefaults(config *rest.Config) error {
	// 資源對象的GroupVersion
	gv := v1.SchemeGroupVersion
	config.GroupVersion = &gv
	// 資源對象的根目錄
	config.APIPath = "/apis"
	// 使用註冊的資源類型 Scheme 對請求和響應進行編解碼,Scheme 就是資源類型的規範
	config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()

	if config.UserAgent == "" {
		config.UserAgent = rest.DefaultKubernetesUserAgent()
	}

	return nil
}

func (c *AppsV1Client) Deployments(namespace string) DeploymentInterface {
	return newDeployments(c, namespace)
}


// staging/src/k8s.io/client-go/kubernetes/typed/apps/v1/deployment.go

// deployments implements DeploymentInterface
// deployments 實現了 DeploymentInterface 接口
type deployments struct {
	client rest.Interface
	ns     string
}

// newDeployments returns a Deployments
// newDeployments 實例化 deployments 對象
func newDeployments(c *AppsV1Client, namespace string) *deployments {
	return &deployments{
		client: c.RESTClient(),
		ns:     namespace,
	}
}

通過上面代碼我們就可以很清晰的知道可以通過 clientset.AppsV1().Deployments("default")來獲取一個 deployments 對象,然後該對象下面定義了 deployments 對象的 CRUD 操作,比如我們調用的 List() 函數:

// staging/src/k8s.io/client-go/kubernetes/typed/apps/v1/deployment.go

// List takes label and field selectors, and returns the list of Deployments that match those selectors.
func (c *deployments) List(ctx context.Context, opts metav1.ListOptions) (result *v1.DeploymentList, err error) {
	var timeout time.Duration
	if opts.TimeoutSeconds != nil {
		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
	}
	result = &v1.DeploymentList{}
	err = c.client.Get().
		Namespace(c.ns).
		Resource("deployments").
		VersionedParams(&opts, scheme.ParameterCodec).
		Timeout(timeout).
		Do(ctx).
		Into(result)
	return
}

從上面代碼可以看出最終是通過 c.client 去發起的請求,也就是局部的 restClient 初始化的函數中通過 rest.RESTClientFor(&config) 創建的對象,也就是將 rest.Config 對象轉換成一個 Restful 的 Client 對象用於網絡操作:

// staging/src/k8s.io/client-go/rest/config.go


// RESTClientFor返回一個滿足客戶端Config對象上所要求屬性的RESTClient。
// 請注意,在初始化客戶端時,一個RESTClient可能需要一些可選的字段。
// 由該方法創建的RESTClient是通用的
// 它期望在遵循Kubernetes慣例的API上操作,但可能不是Kubernetes的API。
// RESTClientFor等同於調用RESTClientForConfigAndClient(config, httpClient)
// 其中httpClient是用HTTPClientFor(config)生成的。
func RESTClientFor(config *Config) (*RESTClient, error) {
	if config.GroupVersion == nil {
		return nil, fmt.Errorf("GroupVersion is required when initializing a RESTClient")
	}
	if config.NegotiatedSerializer == nil {
		return nil, fmt.Errorf("NegotiatedSerializer is required when initializing a RESTClient")
	}

	// Validate config.Host before constructing the transport/client so we can fail fast.
	// ServerURL will be obtained later in RESTClientForConfigAndClient()
	_, _, err := defaultServerUrlFor(config)
	if err != nil {
		return nil, err
	}

	httpClient, err := HTTPClientFor(config)
	if err != nil {
		return nil, err
	}

	return RESTClientForConfigAndClient(config, httpClient)
}

到這裏我們就知道了 Clientset 是基於 RESTClient 的,RESTClient 是底層的用於網絡請求的對象,可以直接通過 RESTClient 提供的 RESTful 方法如 Get()、Put()、Post()、Delete() 等和 APIServer 進行交互。

同時支持JSON和protobuf兩種序列化方式,支持所有原生資源。

當主食化RESTClient過後,就可以發起網絡請求了,比如對於Deployment的List操作:

// staging/src/k8s.io/client-go/kubernetes/typed/apps/v1/deployment.go

func (c *deployments) List(ctx context.Context, opts metav1.ListOptions) (result *v1.DeploymentList, err error) {
	var timeout time.Duration
	if opts.TimeoutSeconds != nil {
		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
	}
	result = &v1.DeploymentList{}
	err = c.client.Get().
		Namespace(c.ns).
		Resource("deployments").
		VersionedParams(&opts, scheme.ParameterCodec).
		Timeout(timeout).
		Do(ctx).
		Into(result)
	return
}

上面通過調用 RestClient 發起網絡請求,真正發起網絡請求的代碼如下所示:

// staging/src/k8s.io/client-go/rest/request.go

// request connects to the server and invokes the provided function when a server response is
// received. It handles retry behavior and up front validation of requests. It will invoke
// fn at most once. It will return an error if a problem occurred prior to connecting to the
// server - the provided function is responsible for handling server errors.
func (r *Request) request(ctx context.Context, fn func(*http.Request, *http.Response)) error {
	//Metrics for total request latency
	start := time.Now()
	defer func() {
		metrics.RequestLatency.Observe(ctx, r.verb, *r.URL(), time.Since(start))
	}()

	if r.err != nil {
		klog.V(4).Infof("Error in request: %v", r.err)
		return r.err
	}

	if err := r.requestPreflightCheck(); err != nil {
		return err
	}

  	// 初始化網絡客戶端
	client := r.c.Client
	if client == nil {
		client = http.DefaultClient
	}

	// Throttle the first try before setting up the timeout configured on the
	// client. We don't want a throttled client to return timeouts to callers
	// before it makes a single request.
	if err := r.tryThrottle(ctx); err != nil {
		return err
	}

 	 // 超時處理
	if r.timeout > 0 {
		var cancel context.CancelFunc
		ctx, cancel = context.WithTimeout(ctx, r.timeout)
		defer cancel()
	}

	isErrRetryableFunc := func(req *http.Request, err error) bool {
		// "Connection reset by peer" or "apiserver is shutting down" are usually a transient errors.
		// Thus in case of "GET" operations, we simply retry it.
		// We are not automatically retrying "write" operations, as they are not idempotent.
		if req.Method != "GET" {
			return false
		}
		// For connection errors and apiserver shutdown errors retry.
		if net.IsConnectionReset(err) || net.IsProbableEOF(err) {
			return true
		}
		return false
	}

	// Right now we make about ten retry attempts if we get a Retry-After response.
  	// 重試機制
	retry := r.retryFn(r.maxRetries)
	for {
		if err := retry.Before(ctx, r); err != nil {
			return retry.WrapPreviousError(err)
		}
    
   		 // 構造請求對象
		req, err := r.newHTTPRequest(ctx)
		if err != nil {
			return err
		}
    
    		// 發起網絡請求
		resp, err := client.Do(req)
		updateURLMetrics(ctx, r, resp, err)
		// The value -1 or a value of 0 with a non-nil Body indicates that the length is unknown.
		// https://pkg.go.dev/net/http#Request
		if req.ContentLength >= 0 && !(req.Body != nil && req.ContentLength == 0) {
			metrics.RequestSize.Observe(ctx, r.verb, r.URL().Host, float64(req.ContentLength))
		}
		retry.After(ctx, r, resp, err)

		done := func() bool {
			defer readAndCloseResponseBody(resp)

			// if the the server returns an error in err, the response will be nil.
			f := func(req *http.Request, resp *http.Response) {
				if resp == nil {
					return
				}
				fn(req, resp)
			}

			if retry.IsNextRetry(ctx, r, req, resp, err, isErrRetryableFunc) {
				return false
			}

			f(req, resp)
			return true
		}()
		if done {
			return retry.WrapPreviousError(err)
		}
	}
}

到這裏就完成了一次完整的網絡請求。

其實 Clientset 對象也就是將 rest.Config 封裝成了一個 http.Client 對象而已,最終還是利用 golang 中的 http 庫來執行一個正常的網絡請求而已。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章