重新認識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 對象來獲取資源數據,主要有以下三個步驟:
- 使用 kubeconfig 文件或者 ServiceAccount(InCluster 模式)來創建訪問 Kubernetes API 的 Restful 配置參數,也就是代碼中的
rest.Config
對象 - 使用 rest.Config 參數創建 Clientset 對象,這一步非常簡單,直接調用
kubernetes.NewForConfig(config)
即可初始化 - 然後是 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/
// 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 庫來執行一個正常的網絡請求而已。