站在巨人的肩膀上-像kubernetes一樣用etcd存儲自定義對象

背景

衆所周知,kubernetes利用etcd存儲API對象,例如Pod、Deployment、StatefulSet等等。筆者認爲kubernetes這種API對象的設計方案當前來看非常先進,基於etcd實現對象存儲是這個設計方案的關鍵基礎。筆者和很多讀者都有這樣一些需求:

  1. 自己設計的系統也希望採用etcd;
  2. 系統也想採用kubernetes設計的API對象思想,這些對象是爲系統設計的,與kubernetes無關;

像kubernetes一樣實現一套自己的代碼工作量是驚人的,即便是照抄一份難度也是非常大的,畢竟這裏面涉及到的大量的代碼依賴。所以筆者認爲能夠複用kubernetes已經實現的代碼,並在此基礎上擴展自定義的API對象,這會是一個比較有意思的事情。所以本文標題爲站在巨人的肩膀上,kubernetes是一個巨人,有大量的優秀設計和代碼實現可以借鑑和引用。需要注意的是,本文的示例代碼筆者不會做過多詳細的原理解釋,如果讀者需要了解具體實現原理,可以閱讀筆者的《深入剖析kubernetes的API對象類型定義》、《etcd在kubernetes中的應用》以及《深入剖析kubernetes apiserver的存儲實現》。

本文代碼依賴如下:

k8s.io/apimachinery v0.0.0-20191017185446-6e68a40eebf9
k8s.io/apiserver v0.0.0-20191018030144-550b75f0da71

實現

首先,先建一個項目,筆者姑且把這個項目命名爲customeapi,並且在這個項目根目錄創建cmd和pkg兩個目錄。然後就是自定義API對象的了,筆者把api對象定義在customeapi/pkg/api/test/v1包中(讀者需要注意kubernetes中api都是定義在一個單獨的項目k8s.io/api中,本文爲了演示方便定義在了自己的項目中),如下代碼所示:

// 代碼源自customeapi/pkg/api/test/v1/types.go
package v1

import (
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/runtime"
)
// 定義自定義API對象Custom.
type Custom struct {
    // 繼承metav1.TypeMeta和metav1.ObjectMeta才實現了runtime.Object,這樣Custom對象
    // 的yaml的格式就像如下:
    // kind: Custom
    // apiVersion: test/v1
    // metadata: 
    //   labels: 
    //     name: custom
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
    // 爲了演示方便,Custom對象的規格和狀態都定義爲空類型,讀者根據自己的業務進行設計
    Spec              CustomSpec
    Status            CustomStatus
}
type CustomSpec struct{}
type CustomStatus struct{}

// DeepCopyObject()是必須要實現的,這是runtime.Objec定義的接口,否則編譯就會報錯。讀者需要注意,
// kubernetes的API對象的DeepCopyObject()函數是代碼生成工具生成的,本文的示例是筆者自己寫的。
func (in *Custom) DeepCopyObject() runtime.Object {
    if in == nil {
        return nil
    }
    out := new(Custom)
    *out = *in
    return out
}
var _ runtime.Object = &Custom{}

好了,自定義對象已經定義完了,現在需要讓codec能夠識別Custom這個類型,否則Custom在寫入etcd前序列化會失敗,這部分代碼實現如下:

// 代碼源自customeapi/pkg/api/test/v1/register.go
package v1

import (
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/runtime/schema"
)

// 定義自定義類型組名,因爲是測試例子,所以筆者把組名定義爲test,這樣test/Custom纔是類型全稱
const GroupName = "test"

// 定義自定義類型的組名+版本,即test v1
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"}

// 程序初始化部分需要調用AddToScheme()來實現自定義類型的註冊,具體的實現在addKnownTypes()
var (
    SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
    AddToScheme   = SchemeBuilder.AddToScheme
)

// 把筆者上面定義的Custom添加到scheme中就完成了類型的註冊,就是這麼簡單。讀者需要注意,類型註冊
// 其實是一個比較複雜的過程,kubernetes把這部分實現全部交給了scheme,把簡單的接口留給了使用者。
func addKnownTypes(scheme *runtime.Scheme) error {
    scheme.AddKnownTypes(SchemeGroupVersion,
        &Custom{},
    )

    return nil
}

最後就是要把這個對象存入etcd中,筆者把它實現自cmd/main.go文件中,如下代碼所示:

// 代碼源自customeapi/cmd/main.go
package main

import (
    "fmt"
    "context"

    testv1 "customapi/pkg/api/test/v1"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/runtime/serializer"
    "k8s.io/apiserver/pkg/storage/storagebackend"
    "k8s.io/apiserver/pkg/storage/storagebackend/factory"
)

func main() {
    // 構造scheme,然後調用剛剛實現的註冊函數實現自定義API對象的註冊。
    scheme := runtime.NewScheme()
    testv1.AddToScheme(scheme)
    
    // 註冊了自定義API對象,就可以構造codec工廠了,在通過codec工廠構造codec。所謂的codec就是
    // 自定義API對象的序列化與反序列化的實現。
    cf := serializer.NewCodecFactory(scheme)
    codec := cf.LegacyCodec(testv1.SchemeGroupVersion)

    // 有了codec就可以創建storagebackend.Config,他是構造storage.Interfaces的必要參數
    config := storagebackend.NewDefaultConfig("", codec)
    // 筆者在本機裝了etcd,所以把etcd的地址填寫爲本機地址,這樣方便測試
    config.Transport.ServerList = append(config.Transport.ServerList, "127.0.0.1:2379")
    // 創建storage.Interfaces對象,storage.Interfaces是apiserver對存儲的抽象,這樣我們
    // 就可以像apiserver一樣在etcd上操作自己定義的對象了。
    storage, destroy, err := factory.Create(*config)
    if nil != err {
        fmt.Printf("%v\n", err)
    }
    // 構造Custom對象
    custom := testv1.Custom{}
    // 把Custom對象寫入etcd
    if err = storage.Create(context.Background(), "test", &custom, &custom, 0); nil != err {
        fmt.Println(err)
    }
    // 把寫入的Custom對象打印出來看看結果
    if data, err := runtime.Encode(codec, &custom); nil == err {
        fmt.Printf("%s\n", string(data))
    }
    // 必要的析構函數
    destroy()
}

好了,至此筆者就完成了利用apiserver已經實現的storage.Interface接口存儲自定義對象。編譯運行後的結果如下:

I1018 11:10:21.782324   25634 client.go:357] parsed scheme: "endpoint"
I1018 11:10:21.782665   25634 endpoint.go:68] ccResolverWrapper: sending new addresses to cc: [{127.0.0.1:2379 0  <nil>}]
I1018 11:10:21.784459   25634 once.go:66] CPU time info is unavailable on non-linux or appengine environment.
I1018 11:10:21.787267   25634 client.go:357] parsed scheme: "endpoint"
I1018 11:10:21.787306   25634 endpoint.go:68] ccResolverWrapper: sending new addresses to cc: [{127.0.0.1:2379 0  <nil>}]
{"kind":"Custom","apiVersion":"test/v1","metadata":{"creationTimestamp":null},"Spec":{},"Status":{}}

再通過etcdctl --endpoints=http://localhost:2379 get /test 驗證結果如下:

{"kind":"Custom","apiVersion":"test/v1","metadata":{"creationTimestamp":null},"Spec":{},"Status":{}}

大功告成,驚不驚喜?意不意外?只編寫少量代碼就可以寫出跟kubernetes一樣操作API對象的程序,關鍵在於這個對象類型我們是自定義的,並且存儲在了我們自己部署的etcd上。

至於對象的get、watch筆者就不一一演示了,讀者可以自己動手試試

總結

上面的內容只是證明了利用kubernetes現成的代碼實現一個能像kubernetes一樣在etcd上操作對象的程序是可行的。但是使用上多少還是有一些限制,比如:

  1. 對象類型必須繼承metav1.TypeMeta和metav1.ObjectMeta,metav1.TypeMeta還算是非常通用,但是metav1.ObjectMeta就顯得有些冗餘或者可能不夠;
  2. 從例子上看對象序列化是json格式,如果和對象需要通過rpc(比如grpc)與其他系統交互,對象需要protobuf序列化該怎麼辦?

其實這部分在筆者的《深入剖析kubernetes的API對象類型定義》以及《深入剖析kubernetes apiserver的存儲實現》可以找到答案,感興趣的讀者不妨看看。此時此刻,筆者有一種到站在kubernetes這個巨人肩膀上的感覺。

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