数据压缩

数据压缩

对象存储服务端并不是最适合做数据压缩的地方。最适合做数据压缩的地方是客户端。一个高性能的客户端不仅可以将大量小对象打包成大对象提高存储和传输的效率,也可以在客户机本地进行数据压缩,进一步节省网络带宽和存储空间。如果云存储系统在设计最初就包含了专门的客户端,那么一定要将数据压缩功能放在客户端,而不是服务端。

数据压缩的效率和使用的压缩算法以及待压缩数据的特征密切相关,存放随机数据的二进制文件的压缩比惨不忍睹,文本文件的压缩比会好很多。如果云存储系统中没有一个专门的客户端,或者用户更倾向使用通用的客户端比如浏览器,且用户上传的对象大多数都是一些适合数据压缩的文档,那么可以考虑在服务端实现数据压缩功能,将客户上传的对象压缩起来再进行存储。

可以应用数据压缩功能的不仅仅在数据存储这一块,数据的传输也一样可以进行压缩。对于对象的上传来说,由于没有一个专门的客户端,没办法限定客户上传的数据。但是对于对象的下载,服务端可以提供一种选择,只要客户端支持,接口服务就可以传输压缩后的数据给客户端。

Go语言原生支持的压缩算法包有bzip2、flate、gzip、lzw 和zlib。

本项目采用的压缩算法是 gzip,它不是压缩速度最快的也不是压缩比最高的压缩算法,但是对于功能的实现来说,gzip足够好且足够简单。

用gzip实现对象存储和下载时的数据压缩

存储时的数据压缩

在本版本之前,数据服务节点把分片临时对象转正时使用的是os.Rename 操作,将$STORAGE_ROOT/temp/<uuid>.dat重命名为$STORAGE_ROOT/objects/<hash>.X.<hash>of shard X>。而本版本的实现则需要读取$STORAGE_ROOT/temp/<uid>.dat文件的内容,并使用gzip压缩后写入$STORAGE_ROOT/objects/<hash>.X.<hash of shard X>

在临时对象转正时进行数据压缩

在读取对象分片时,数据服务节点需要在读取$STORAGE_ROOT/objects/<hash>.X.<hash of shard X>文件的内容后先进行 gzip解压,然后才作为HTTP响应的正文输出。

get对象时进行数据解压

下载时进行数据压缩

客户端在下载对象时可以设置Accept-Encoding头部为gzip。接口服务在检查到这个头部后会将对象数据流经过gzip压缩后写入HTTP响应的正文。

对象下载时进行数据压缩

接口服务的REST接口
1
2
3
4
5
6
7
GET /objects/<object_name>
请求头部
Accept-Encoding:gzip
响应头部
Content-Encoding:gzip
响应正文
gzip压缩后的对象内容

具体实现

接口服务

接口服务的objects.get函数发生改变:多了一个对Accpet-Encoding请求头部的检查。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
func get(w http.ResponseWriter, r *http.Request) {
name := strings.Split(r.URL.EscapedPath(), "/")[2]
versionId := r.URL.Query()["version"]
version := 0
var e error
if len(versionId) != 0 {
version, e = strconv.Atoi(versionId[0])
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusBadRequest)
return
}
}
meta, e := es.GetMetadata(name, version)
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusInternalServerError)
return
}
if meta.Hash == "" {
w.WriteHeader(http.StatusNotFound)
return
}
hash := url.PathEscape(meta.Hash)
stream, e := GetStream(hash, meta.Size)
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusNotFound)
return
}
offset := utils.GetOffsetFromHeader(r.Header)
if offset != 0 {
stream.Seek(offset, io.SeekCurrent)
w.Header().Set("content-range", fmt.Sprintf("bytes %d-%d/%d", offset, meta.Size-1, meta.Size))
w.WriteHeader(http.StatusPartialContent)
}
// 增加对Accept-Encoding请求头部的检查
acceptGzip := false
encoding := r.Header["Accept-Encoding"]
for i := range encoding {
if encoding[i] == "gzip" {
acceptGzip = true
break
}
}
// 如果头部中含有gzip,说明客户端可以接受gzip压缩数据
if acceptGzip {
// 设置Content-Encoding响应头部为gzip
w.Header().Set("content-encoding", "gzip")
// 以w为参数调用gzip.NewWriter创建一个指向gzip.Writer结构体的指针w2
w2 := gzip.NewWriter(w)
// 用io.Copy将对象数据流stream的内容用io.Copy写入w2,数据会被自动压缩,然后写入w
io.Copy(w2, stream)
w2.Close()
} else {
io.Copy(w, stream)
}
stream.Close()
}
数据服务

用于将临时对象转正的commitTempObject函数发生改变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func commitTempObject(datFile string, tempinfo *tempInfo) {
f, _ := os.Open(datFile)
defer f.Close()
d := url.PathEscape(utils.CalculateHash(f))
f.Seek(0, io.SeekStart)
// 使用os.Create创建正式对象文件w
w, _ := os.Create(os.Getenv("STORAGE_ROOT") + "/objects/" + tempinfo.Name + "." + d)
// 然后以w为参数调用gzip.NewWriter创建w2
w2 := gzip.NewWriter(w)
// 将临时对象文件f中的数据复制进w2
io.Copy(w2, f)
w2.Close()
// 删除临时对象文件
os.Remove(datFile)
// 添加对象定位缓存
locate.Add(tempinfo.hash(), tempinfo.id())
}

用于读取对象的objects.SendFile函数发生改变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func sendFile(w io.Writer, file string) {
f, e := os.Open(file)
if e != nil {
log.Println(e)
return
}
defer f.Close()
// 在对象文件上用gzip.NewReader创建一个指向gzip.Reader结构体的指针gzipStream
gzipStream, e := gzip.NewReader(f)
if e != nil {
log.Println(e)
return
}
// 读出gzipStream中的数据
io.Copy(w, gzipStream)
gzipStream.Close()
}

测试

本版本所有代码及测试用例可见增加数据压缩版本全部代码

参考

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