背景
衆所周知,kubernetes利用etcd存儲API對象,例如Pod、Deployment、StatefulSet等等。筆者認爲kubernetes這種API對象的設計方案當前來看非常先進,基於etcd實現對象存儲是這個設計方案的關鍵基礎。筆者和很多讀者都有這樣一些需求:
- 自己設計的系統也希望採用etcd;
- 系統也想採用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上操作對象的程序是可行的。但是使用上多少還是有一些限制,比如:
- 對象類型必須繼承metav1.TypeMeta和metav1.ObjectMeta,metav1.TypeMeta還算是非常通用,但是metav1.ObjectMeta就顯得有些冗餘或者可能不夠;
- 從例子上看對象序列化是json格式,如果和對象需要通過rpc(比如grpc)與其他系統交互,對象需要protobuf序列化該怎麼辦?
其實這部分在筆者的《深入剖析kubernetes的API對象類型定義》以及《深入剖析kubernetes apiserver的存儲實現》可以找到答案,感興趣的讀者不妨看看。此時此刻,筆者有一種到站在kubernetes這個巨人肩膀上的感覺。