站在巨人的肩膀上-像kubernetes一样用etcd存储自定义对象

33 篇文章 46 订阅

背景

众所周知,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这个巨人肩膀上的感觉。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值