分布式对象存储介绍及单机实现

对象存储

网络存储

  • NAS: Network Attached Storage的简称,是一个提供了存储功能文件系统网络服务器。客户端可以访问NAS上的文件系统,还可以上传和下载文件。NAS客户端和服务端之间使用的协议有SMB、NFS 以及AFS等网络文件系统协议。对于客户端来说,NAS就是一个网络上的文件服务器。
  • SAN: Storage Area Network 的简称。和NAS的区别是SAN只提供了块存储,而把文件系统的抽象交给客户端来管理。SAN 的客户端和服务端之间的协议有FibreChannel、iSCSI、ATA over Ethernet(AoE)和 HyperSCSI。对于客户端来说,SAN就是一块磁盘,可以对其格式化、创建文件系统并挂载。

现代的网络存储通常混合使用NAS和SAN,同时提供文件级别的协议和块级别的协议。

网络文件系统、块存储与对象存储的区别

数据管理
  • 网络文件系统:数据是以一个个文件的形式来管理
  • 块存储:数据是以数据块的形式来管理的,每个数据块有它自己的地址,但是没有额外的背景信息
  • 对象存储:以对象的方式来管理数据的,一个对象通常包含了3个部分:对象的数据、对象的元数据以及一个全局唯一的标识符(即对象的ID)。其中
    • 对象的数据就是该对象中存储的数据本身。一个对象可以用来保存大量无结构的数据(例如:音乐的具体内容)。
    • 对象的元数据是对象的描述信息,为了和对象的数据本身区分开来,称其为元数据(例如:音乐的名字、大小等)。
    • 对象的标识符用于引用该对象。和对象的名字不同,标识符具有全局唯一性。名字不具有这个特性。通常用对象的散列值来做其标识符
数据访问
  • 网络文件系统的客户端通过NFS等网络协议访问某个远程服务器上存储的文件。
  • 块存储的客户端通过数据块的地址访问SAN上的数据块。
  • 对象存储则通过REST网络服务访问对象。

REST为Representational State Transfer的简称。REST网络服务通过标准HTTP服务对网络资源提供一套预先定义的无状态操作。网络资源被定义为可以通过URL 访问的文档或文件。更广发的:网络上一切可以通过任何方式被标识、命名、引用或处理的东西都是一种网络资源。

对于对象存储来说,对象就是一种网络资源,但除了对象本身以外,还需要提供一些其他的网络资源用来访问对象存储的各种功能。客户端向 REST网络服务发起请求并接收响应,以确认网络资源发生了某种变化。HTTP预定义的请求方法(Request Method)通常包括且不限于GET、POST、PUT、DELETE等。它们分别对应不同的处理方式:GET方法在REST 标准中通常用来获取某个网络资源,PUT通常用于创建或替换某个网络资源(注意,它跟PUT的区别是POST一般不同于替换网络资源,如果该资源已经存在,POST通常会返回一个错误而不是覆盖它,POST通常用于创建某个网络资源,DELETE通常用于删除某个网络资源。

对象存储的优势
  • 扩展方便:扩展只需要添加新的存储节点就可以
  • 低代价的数据冗余能力

单机版对象存储系统

通过在一台服务器上运行一个HTTP服务提供的REST接口,并通过接口实现本地服务器上的对象存取。

单机版分布式存储系统

REST接口

实现PUT方法和GET方法

对象PUT
1
2
PUT /object/<object_name>
请求正文(Request Body)

单机版put操作

客户端通过PUT方法将一个对象上传至服务器,服务器则将该对象保存在本地磁盘上。/objects/<object_name>是标识该对象网络资源的URL。URL是Uniform Resource Locator 的简称,也就是一个网络地址,用于引用某个网络资源在网络上的位置。

在对象存储中,通常使用PUT方法来上传一个对象。客户端的PUT请求提供了对象的名字<object_name>和对象的数据<content of object>,它们最终被保存在本地磁盘上的文件STORAGE_ROOT/objects/<object_name>中。$STORAGE_ROOT环境变量保存着在本地磁盘上的存储根目录的名字。

对象GET
1
2
GET /objects/<object_name>
响应正文 (Response Body)

单机版get操作

客户端通过GET方法从服务器上下载一个对象,服务器在本地磁盘上查找并读取该对象,如果该对象不存在,则服务器返回HTTP错误代码404 Not Found

在对象存储中,总是使用GET方法来下载一个对象。客户端的GET请求提供了<object_name>,服务进程从本地磁盘上的文件$STORAGE_ROOT/objects/<object_name>读取对象并将其写入HTTP响应正文。

具体实现

main函数

1
2
3
4
5
6
7
8
9
func main() {
//注册HTTP处理函数objects.Handler,若有客户端访问该服务器的HTTP服务且URL以"objects/"开头,
//则请求将由objects.Handler负责处理。除此之外的HTTP请求会默认返回HTTP错误代码404 Not Found.
http.HandleFunc("/objects/", objects.Handler)
//监听端口
//正常情况下没有返回,程序运行后开始监听端口上的请求
//非正常情况下,该函数将错误返回,log.Fatal打印错误信息并退出程序
log.Fatal(http.ListenAndServe(os.Getenv("LISTEN_ADDRESS"), nil))
}

objects包下的Handler函数、get、put函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package objects

import "net/http"

// 检查HTTP请求方法:PUT则调用put函数,GET则调用共get函数。其余则返回405 Method Not Allowed错误代码
func Handler(w http.ResponseWriter, r *http.Request) {
m := r.Method //Method记录该HTTP请求的方法
if m == http.MethodPut {
put(w, r)
return
}
if m == http.MethodGet {
get(w, r)
return
}
//写HTTP响应的代码
w.WriteHeader(http.StatusMethodNotAllowed)
}

put方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package objects

import (
"io"
"log"
"net/http"
"os"
"strings"
"fmt"
)

func put(w http.ResponseWriter, r *http.Request) {
fmt.Println(r.URL.EscapedPath())
//r.URL.EsccapedPath得到request的路径,此处为/objects/xxx
f, e := os.Create(os.Getenv("STORAGE_ROOT") + "/objects/" +
strings.Split(r.URL.EscapedPath(), "/")[2]) //得到文件名
if e != nil {
//创建文件失败
log.Println(e)
//写入HTTP响应的代码
w.WriteHeader(http.StatusInternalServerError)
return
}
defer f.Close()
io.Copy(f, r.Body) //将r.Body写入文件
}

get方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package objects

import (
"io"
"log"
"net/http"
"os"
"strings"
)

func get(w http.ResponseWriter, r *http.Request) {
f, e := os.Open(os.Getenv("STORAGE_ROOT") + "/objects/" +
strings.Split(r.URL.EscapedPath(), "/")[2])
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusNotFound)
return
}
defer f.Close()
io.Copy(w, f)
//f本身的类型是*os.File,同时实现了io.Writer和io.Reader两个接口,即实现了Write和Read方法
//http.ResponseWriter也是接口,该接口实现了Write方法,也是一个io.Write接口
}

linux下功能测试

运行服务器:

1
2
mkdir /tmp/objects
LISTEN_ADDRESS=:12345 STORAGE_ROOT=/tmp go run server.go

curl 进行http访问:

1
2
3
4
5
curl -v 10.29.102.172:12345/objects/test #默认get操作,此时没有数据,返回404 Not Found

curl -v 10.29.102.172:12345/objects/test -XPUT -d"this is a test object" # put一个对象

curl -v 10.29.102.172:12345/objects/test #默认get操作,成功返回200 OK

参考

《分布式对象存储—原理、架构及Go语言实现》