数据压缩
对象存储服务端并不是最适合做数据压缩的地方。最适合做数据压缩的地方是客户端。一个高性能的客户端不仅可以将大量小对象打包成大对象提高存储和传输的效率,也可以在客户机本地进行数据压缩,进一步节省网络带宽和存储空间。如果云存储系统在设计最初就包含了专门的客户端,那么一定要将数据压缩功能放在客户端,而不是服务端。
数据压缩的效率和使用的压缩算法以及待压缩数据的特征密切相关,存放随机数据的二进制文件的压缩比惨不忍睹,文本文件的压缩比会好很多。如果云存储系统中没有一个专门的客户端,或者用户更倾向使用通用的客户端比如浏览器,且用户上传的对象大多数都是一些适合数据压缩的文档,那么可以考虑在服务端实现数据压缩功能,将客户上传的对象压缩起来再进行存储。
可以应用数据压缩功能的不仅仅在数据存储这一块,数据的传输也一样可以进行压缩。对于对象的上传来说,由于没有一个专门的客户端,没办法限定客户上传的数据。但是对于对象的下载,服务端可以提供一种选择,只要客户端支持,接口服务就可以传输压缩后的数据给客户端。
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>
。
![在临时对象转正时进行数据压缩](https://api2.mubu.com/v3/document_image/3735ce78-ff8c-4a71-a8e1-26642ba4f577-11197877.jpg)
在读取对象分片时,数据服务节点需要在读取$STORAGE_ROOT/objects/<hash>.X.<hash of shard X>
文件的内容后先进行 gzip解压,然后才作为HTTP响应的正文输出。
![get对象时进行数据解压](https://api2.mubu.com/v3/document_image/eebc4a0e-9bbd-4e6b-9c9e-9427957d4798-11197877.jpg)
下载时进行数据压缩
客户端在下载对象时可以设置Accept-Encoding
头部为gzip。接口服务在检查到这个头部后会将对象数据流经过gzip压缩后写入HTTP响应的正文。
![对象下载时进行数据压缩](https://api2.mubu.com/v3/document_image/eec8cf39-0a4c-4834-ab08-c0f52923735d-11197877.jpg)
接口服务的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) } acceptGzip := false encoding := r.Header["Accept-Encoding"] for i := range encoding { if encoding[i] == "gzip" { acceptGzip = true break } } if acceptGzip { w.Header().Set("content-encoding", "gzip") w2 := gzip.NewWriter(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) w, _ := os.Create(os.Getenv("STORAGE_ROOT") + "/objects/" + tempinfo.Name + "." + d) w2 := gzip.NewWriter(w) 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() gzipStream, e := gzip.NewReader(f) if e != nil { log.Println(e) return } io.Copy(w, gzipStream) gzipStream.Close() }
|
测试
本版本所有代码及测试用例可见增加数据压缩版本全部代码
参考
《分布式对象存储—原理、架构及Go语言实现》