使用Go-Ceph库编写一个更简单的RBD HTTP API

很多人看到这个标题会很奇怪,Ceph不是有一个RESTful API么,为什么又要造一遍轮子?

的确,Ceph的官方组件Dashboard,内置了一些非常强大的RESTful API,功能也是比较的全面。为啥又要自己写一个呢?在我们的环境里,有一个自己实现的类似Openstack的虚拟机管理平台。而这个平台对接Ceph RBD时,就是使用的Dashboard模块提供的API。个人觉得啊,官方的API,虽然功能全,但确实对于对接的用户来说,真的不是那么友好。这里举几个简单的点:

  1. 官方API基于Token进行鉴权,而Token又通过用户名和密码进行获取,并且有一个固定的过期时间,这就会有两个选择,一个暴力点的选择是不管发送什么请求,都会获取一个新的Token,这样可以保证基于新Token的请求都可以成功;或者,每次在请求之前请求auth/check接口,确认Token的有效性,如果失效了,那就重新获取;再或者,根据请求的返回值,如果出现401错误等等情况,再重新获取新的Token。但是无论是哪种方法,都会显得冗余和逻辑复杂,特别是在多线程等等环境下,还需要考虑使用单例等等。另外,这多出来的这些Token请求,确实也拖慢了整体的效率,毕竟Python写的API,确实不算快。

  2. 官方API是一个异步API,怎么理解呢?让我们先看下大部分接口的返回值,以创建RBD为例:

    • 201 Created – Resource created.
    • 202 Accepted – Operation is still executing. Please check the task queue.
    • 400 Bad Request – Operation exception. Please check the response body for details.
    • 401 Unauthorized – Unauthenticated access. Please login first.
    • 403 Forbidden – Unauthorized access. Please check your permissions.
    • 500 Internal Server Error – Unexpected error. Please check the response body for the stack trace.

    对于一个创建请求,如果成功,则可能会有两种返回值:201表示RBD Image创建成功,可以直接使用;202表示创建任务已经被接受了,但是还没有创建成功,具体的结果,需要去队列里找结果。怎么理解呢?如果返回201,那么恭喜,这个Image可以直接被使用了。如果返回了202,那此时还不能直接使用这个Image,因为仅仅是添加了任务,必须等任务执行完成之后,Image才真正可用。那怎么去寻找这个任务结果呢?又需要我们去轮询调用Display Tasks API,然后从返回的一个列表里,自己匹配刚刚的请求,来确认什么时间任务被执行完成。这个动作实在是不太优雅,让人难受。

  3. 官方API确实也缺失了一些功能。因为我们是一个VM的环境,依赖Clone功能实现VM的OS卷分发,而Clone功能又依赖某个Image的某个Snapshot。但是翻遍了RBDSNAPSHOT章节的文档,也没有找到如何确认某个Image的某个名字Snapshot是否存在的接口,最后从获取Image详情API的返回结果里找到了Image所拥有的Snapshot列表。但是呢,除了Snapshot,这个接口也会返回所有基于该Snapshot创建的所有Clone的列表。如果像我们现在这样某个Snapshot会有成千上万个Clone(有很多VM的操作系统都是一样的)。那这个接口的返回Body就会变得无比之大,这对于Dashboard、以及客户端的解析,都会是一个不小的成本。

当然了,这些问题,也只是在我们这个特定环境下的痛点,是绝对不可以说Ceph本身的实现问题的,那这些问题,要么忍着,要么,也可以尝试改变一下。

要说到同样是一个虚拟机管理平台,Openstack是怎么面临这些问题的呢?是不是我们也可以参考一下Openstack的实现呢?很遗憾,在Openstack Cinder组件里,是直接通过librbd的Python binding实现的。可惜的是我们并没有使用Python进行开发,相对于Openstack来说,集成方式也有些区别。

不过好在Ceph官方也提供了librbd的Golang Bindinggo-ceph,原理和Python一样,也是直接基于librbd的C接口,那既然这样,我们也可以尝试基于这个库,实现一个我们自己的RBD HTTP API。不需要多么花哨的设计和功能,只需要满足最基本的功能就可以了。

实现之前,还是先整理一下我们的需求。到目前为止,需求并不复杂,当然未来可能会对接K8S或者类似的容器平台,还需要额外的其他接口,但在当前虚拟机这个场景下,我们需要的功能如下:

1. Image相关接口,包括创建Image,获取Image信息,扩容Image,设置Image QOS,删除Image
2. Snapshot相关接口,包括针对Image创建Snapshot,根据Snapshot创建Clone,以及判断Image某个Snapshot是否存在(这个接口在上面提到官方API没有,但是librbd里是有相关接口的)

看起来还是比较简单的,这里举个创建Image接口的例子,顺便也算是提供了一个简单的go-ceph的使用文档,在这之前,go-ceph相关的文档确实不太好找,以至于我只能一遍看他的实现代码,一边看librbd的文档写代码:

package main

import (
	"fmt"
	"log"

	"github.com/ceph/go-ceph/rados"
	"github.com/ceph/go-ceph/rbd"
)

const PoolName = "test_rbd_pool"
const ImageName = "test-image-name"
const ImageSize uint64 = 100 * 1024 * 1024 * 1024 // 100GB

func main() {
	conn, err := rados.NewConn()
	if err != nil {
		log.Fatal(err)
	}

	// 打开默认的配置文件(/etc/ceph/ceph.conf)
	if err := conn.ReadDefaultConfigFile(); err != nil {
		log.Fatal(err)
	}
	if err := conn.Connect(); err != nil {
		log.Fatal(err)
	}
	defer conn.Shutdown()

	ctx, err := conn.OpenIOContext(PoolName)
	if err != nil {
		log.Fatal(err)
	}
	defer ctx.Destroy()

	// 这里使用默认配置创建,也可以根据自己需求,指定image的features
	if err := rbd.CreateImage(ctx, ImageName, ImageSize, rbd.NewRbdImageOptions()); err != nil {
		log.Fatal(err)
	}

	// 获取或者修改Image时,需要先OpenImage,或者OpenImageReadOnly
	rbdImage, err := rbd.OpenImageReadOnly(ctx, ImageName, rbd.NoSnapshot)
	if err != nil {
		if err == rbd.ErrNotFound {
			log.Println("image not found")
		}
		log.Fatal(err)
	} else {
		fmt.Println(rbdImage.GetId())
	}
}

总的来说,开发起来还是挺简单的。最终我也把上面需求的这些功能,封装成了HTTP API,代码也放到了C0reFast/rbd-api。相对官方的API来说,简单、速度快、所有操作全部是同步的,希望有一天在类似的场景下能发挥一些作用。