为何需要数据校验
在上一篇博客中,我们加入了元数据服务作为存储对象的全局唯一标识,并在测试时留下了一个伏笔,查看 hello 文件的版本时可以看到这两个版本的 size 一致,都是 14 。其实是上传了同一个 hello 文件但每次请求的散列值不同,我们在服务节点和数据节点都没有做数据散列值校验,因此不知道这个两次上传的对象其实是相同的。
这样的话很容易被攻击,黑客只需要一直不断上传各种散列值,用户真正获取数据时就只能获取到黑客恶意上传的文件。 就算排除黑客攻击,正常的客户端也有可能因为 bug 出现计算出错的情况,又或者说是在网络传输的时候数据发生损坏,这就导致最终在数据服务节点存储的数据是错误的,客户请求获取数据时只能拿到一个错误的数据。
因此,在数据服务节点保存数据之前,我们还需要做数据校验,只有正确的数据才会被保存。这个时候才能根据散列值对数据进行查重,保证数据服务节点上仅有一份正确的数据,到达节省存储空间提高存储效率的目标。
如何实现数据校验
平时下载文件时,浏览器是先生成一个临时文件,下载完毕后才将临时文件变成完整的文件。我们模仿这个思路去实现数据校验:
客户端上传文件时,在数据服务节点新建一个临时文件来保存客户端上传的文件,当客户端上传完毕后,数据服务节点计算这个临时文件的散列值是否与请求的散列值一致,若一致则认为数据是正确的,将这个临时文件转换成正式文件保存,若散列值不一致则将临时文件删除,响应文件传输错误,提示客户端重新上传文件。
那为何不选择在服务接口做缓存?考虑高并发与服务器性能,接口服务器主要负责提供 restful api 接口,在硬件上的考虑是不会预留太多的磁盘空间,而数据服务器是负责存储的功能,在硬件上回预留较宽的磁盘空间,在高并发时,接口服务接收数据可能将磁盘空间耗尽,若使用内存空间也是如此,因此有溢出的安全隐患。每一层架构都有各自的考虑,成本也因此不同:
实现接口服务缓存功能
PUT 流程
(流程图源于参考书籍)
新增缓存结构体和方法
/*缓存结构体*/
type TempPutStream struct {
Server string
Uuid string
}
/*构建一个新缓存结构体,返回指针*/
func NewTempPutStream(server, object string, size int64) (*TempPutStream, error) {
// 请求数据服务节点构建一个临时文件
request, e := http.NewRequest(http.MethodPost, "http://"+server+"/temp/"+object, nil)
if e != nil {
return nil, e
}
request.Header.Set("size", fmt.Sprintf("%d", size))
client := http.Client{}
response, e := client.Do(request)
if e != nil {
return nil, e
}
// 处理返回结果
uuid, e := ioutil.ReadAll(response.Body)
if e != nil {
return nil, e
}
// 返回缓存对象指针
return &TempPutStream{server, string(uuid)}, nil
}
/*通过 PATCH 请求向临时文件 uuid 写入数据*/
func (w *TempPutStream) Write(p []byte) (n int, err error) {
request, e := http.NewRequest(http.MethodPatch, "http://"+w.Server+"/temp/"+w.Uuid, strings.NewReader(string(p)))
if e != nil {
return 0, e
}
client := http.Client{}
r, e := client.Do(request)
if e != nil {
return 0, e
}
if r.StatusCode != http.StatusOK {
return 0, fmt.Errorf(sys.DataServerError, r.StatusCode)
}
return len(p), nil
}
/*若散列值一致则由 PUT 请求将临时文件转成正式文件,若不一致则由 DELETE 请求删除临时文件,参数 ok 为散列值校对结果*/
func (w *TempPutStream) Commit(ok bool) {
method := http.MethodDelete
if ok {
method = http.MethodPut
}
request, _ := http.NewRequest(method, "http://"+w.Server+"/temp/"+w.Uuid, nil)
client := http.Client{}
client.Do(request)
}
/*获取临时文件*/
func NewTempGetStream(server, uuid string) (*GetStream, error) {
return newGetStream("http://" + server + "/temp/" + uuid)
}
修改接口服务的 PUT 请求流程
/*处理接口服务 PUT 请求*/
func put(w http.ResponseWriter, r *http.Request) {
// 按以前的步骤,这里应该获取存储对象名字,不过从 header 中取对象的散列值作为名字
hash := utils.GetHashFromHeader(r.Header)
if hash == "" {
log.Println(sys.MissingObjectHash)
w.WriteHeader(http.StatusBadRequest)
return
}
// 从 header 中获取 size ,创建临时文件时指定文件大小
size := utils.GetSizeFromHeader(r.Header)
// 存储请求数据,散列值要作转义
httpStatus, e := storeObject(r.Body, url.PathEscape(hash), size)
if e != nil {
log.Println(e)
w.WriteHeader(httpStatus)
return
}
if httpStatus != http.StatusOK {
w.WriteHeader(httpStatus)
return
}
// 获取名字和大小,新增一个对象版本
name := strings.Split(r.URL.EscapedPath(), "/")[2]
e = es.AddVersion(name, hash, size)
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusInternalServerError)
}
// 返回结果
w.WriteHeader(httpStatus)
}
/*参数 r 请求存储的数据*/
func storeObject(r io.Reader, obj string,size int64) (int, error) {
// 如果已存在一份数据,直接返回
if locate.Exist(obj) {
return http.StatusOK, nil
}
// 获取接口服务节点存储对象的流
stream, e := putStream(obj,size)
if e != nil {
return http.StatusServiceUnavailable, e
}
// stream 属于 TempPutStream 结构体的一个实例化,同时实现了 io.Writer 的 write 方法,作为 io.Writer
// r 是请求存储的数据,作为 io.Reader
// TeeReader 方法类似 Linux tee 命令,返回一个 reader ,当 reader 读取数据时,会从 r 中读取内容,并且写入到 stream 中
reader := io.TeeReader(r, stream)
hash := utils.CalculateHash(reader)
if url.PathEscape(hash) != obj {
// 散列值不一致,删除临时文件
stream.Commit(false)
return http.StatusBadRequest, fmt.Errorf(sys.HashMismatch, hash, obj)
}
// 散列值一致,将临时文件转成正式文件
stream.Commit(true)
// 返回成功状态码
return http.StatusOK, nil
}
func putStream(obj string,size int64) (*objectStream.TempPutStream, error) {
// 随机选择一个数据服务节点
server := heartbeat.ChooseRandomDataServer()
// 若没有可用的数据服务节点则返回错误信息
if server == "" {
return nil, fmt.Errorf(sys.DataServerNotFound)
}
// 返回数据服务节点存储对象的流
return objectStream.NewTempPutStream(server, obj, size)
}
实现数据服务缓存功能
优化定位方法
直接使用内存缓存
package locate
import (
"demo/rabbitmq"
"demo/sys"
"log"
"os"
"path/filepath"
"strconv"
"sync"
)
// 缓存全部文件
var objects = make(map[string]int)
// 多读写锁
var mutex sync.RWMutex
// 定位对象
func Locate(hash string) bool {
mutex.Lock()
_, ok := objects[hash]
mutex.Unlock()
return ok
}
func Add(hash string) {
mutex.Lock()
objects[hash] = 1
mutex.Unlock()
}
func Del(hash string) {
mutex.Lock()
delete(objects, hash)
mutex.Unlock()
}
// 监听定位信息
func StartLocate() {
q := rabbitmq.New(os.Getenv(sys.RabbitmqServer))
defer q.Close()
// 绑定 data 网络层
q.Bind(sys.DataServersExchange)
// 获取信息管道
c := q.Consume()
// 从管道中遍历信息,msg 为需要定位的存储对象名字
for msg := range c {
// 去掉 json 序列化的双引号
obj, e := strconv.Unquote(string(msg.Body))
if e != nil {
log.Fatalln(e)
}
// 定位存储对象,名字需要 URL 转义 url.PathEscape(obj) ,如果由前端转义则无需处理
if Locate(obj) {
// 如果存储对象存在,则回送本节点监听地址,已告知存储对象在该节点
q.Send(msg.ReplyTo, os.Getenv(sys.ListenAddress))
}
}
}
// 扫描全磁盘,将所有的文件缓存到内存
func CollectObjects() {
// pattern /home/sam/files/1/objects/*
files, _ := filepath.Glob(os.Getenv(sys.StorageRoot) + "/objects/*")
for i := range files {
hash := filepath.Base(files[i])
objects[hash] = 1
}
}
使用 go-redis 中间件
下载安装:
go get -u github.com/go-redis/redis
封装参考:
package locate
import (
"demo/rabbitmq"
myredis "demo/redis"
"demo/sys"
"log"
"os"
"path/filepath"
"strconv"
"sync"
)
// 多读写锁
var mutex sync.RWMutex
// 定位对象
func Locate(hash string) bool {
mutex.Lock()
ok := myredis.Hexists(sys.RedisHashTableName, hash)
mutex.Unlock()
return ok
}
func Add(hash string) {
mutex.Lock()
myredis.Hset(sys.RedisHashTableName,hash, string(rune(1)))
mutex.Unlock()
}
func Del(hash string) {
mutex.Lock()
myredis.Hdel(sys.RedisHashTableName,hash)
mutex.Unlock()
}
// 监听定位信息
func StartLocate() {
q := rabbitmq.New(os.Getenv(sys.RabbitmqServer))
defer q.Close()
// 绑定 data 网络层
q.Bind(sys.DataServersExchange)
// 获取信息管道
c := q.Consume()
// 从管道中遍历信息,msg 为需要定位的存储对象名字
for msg := range c {
// 去掉 json 序列化的双引号
obj, e := strconv.Unquote(string(msg.Body))
if e != nil {
log.Fatalln(e)
}
// 定位存储对象,名字需要 URL 转义 url.PathEscape(obj) ,如果由前端转义则无需处理
if Locate(obj) {
// 如果存储对象存在,则回送本节点监听地址,已告知存储对象在该节点
q.Send(msg.ReplyTo, os.Getenv(sys.ListenAddress))
}
}
}
// 扫描全磁盘,将所有的文件缓存到 redis table
func CollectObjects() {
objects := make(map[string]interface{})
// pattern /home/sam/files/1/objects/*
files, _ := filepath.Glob(os.Getenv(sys.StorageRoot) + "/objects/*")
for i := range files {
hash := filepath.Base(files[i])
objects[hash] = 1
}
// 将 map 缓存到 redis
if len(objects) > 0 {
myredis.Hmset(sys.RedisHashTableName, objects)
}
}
实现 tmp 缓存功能
缓存临时对象时,其实是创建了两个临时文件,一个用于保存 json 格式的对象信息文件,一个用于保存请求数据的对象内容文件。
package tmp
import (
"demo/dataServer/locate"
"demo/sys"
"demo/utils"
"encoding/json"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"strings"
)
// 临时文件结构体
type tempInfo struct {
Uuid string
Name string
Size int64
}
func Handler(w http.ResponseWriter, r *http.Request) {
m := r.Method
// PUT 将临时文件转换成正式文件
if m == http.MethodPut {
put(w, r)
return
}
// PATCH 写入数据到临时文件
if m == http.MethodPatch {
patch(w, r)
return
}
// POST 创建临时文件
if m == http.MethodPost {
post(w, r)
return
}
// DELETE 删除临时文件
if m == http.MethodDelete {
del(w, r)
return
}
// 其他方法响应 405
w.WriteHeader(http.StatusMethodNotAllowed)
}
/*将临时文件转换成正式文件*/
func put(w http.ResponseWriter, r *http.Request) {
// 获取临时文件的 uuid
uuid := strings.Split(r.URL.EscapedPath(), "/")[2]
// 读取临时文件信息反序列化为结构体
tempinfo, e := readFromFile(uuid)
if e != nil {
log.Println(e)
// 信息文件未找到 404
w.WriteHeader(http.StatusNotFound)
return
}
// 临时文件路径拼接
infoFile := os.Getenv(sys.StorageRoot) + "/temp/" + uuid
datFile := infoFile + ".dat"
// 打开临时数据文件
f, e := os.Open(datFile)
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusInternalServerError)
return
}
// 最终关闭文件流
defer f.Close()
// 调用 f.Stat() 获取当前临时文件信息
info, e := f.Stat()
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusInternalServerError)
return
}
// 查看临时文件当前大小
actual := info.Size()
// 删除临时文件信息
os.Remove(infoFile)
// 如果临时文件当前大小不等于指定的大小,则认为出错,删除临时数据文件,返回 500
if actual != tempinfo.Size {
// 删除临时数据文件
os.Remove(datFile)
log.Printf(sys.SizeMismatch, tempinfo.Size, actual)
w.WriteHeader(http.StatusInternalServerError)
return
}
// 临时文件转正式文件
commitTempObject(datFile, tempinfo)
}
/*创建临时文件,uuid 存储 json 格式临时文件信息,uuid.dat 存储临时文件内容数据*/
func post(w http.ResponseWriter, r *http.Request) {
/*
// Linux 下 uuidgen 命令生成 uuid
// [sam@instance-7w9gig0k ~]$ uuidgen
// 130fa51f-9f68-45f4-a588-84b2c4e28695
output, _ := exec.Command("uuidgen").Output()
// 滤掉换行符
uuid := strings.TrimSuffix(string(output), "\n")
*/
// 使用命令生成 uuid 具有跨平台的局限性,这里使用一个 uuid 包生成
uuid := utils.GenerateUUID()
// 获取文件散列值
name := strings.Split(r.URL.EscapedPath(), "/")[2]
// 获取文件大小
size, e := strconv.ParseInt(r.Header.Get("size"), 0, 64)
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusInternalServerError)
return
}
// 构建一个临时文件结构体
t := tempInfo{uuid, name, size}
// 创建一个临时文件 /home/sam/files/tmp/uuid 保存该对象的信息
e = t.writeToFile()
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusInternalServerError)
return
}
// 创建一个临时文件 /home/sam/files/tmp/uuid.dat 保存该对象的内容数据
os.Create(os.Getenv(sys.StorageRoot) + "/temp/" + t.Uuid + ".dat")
// 响应返回临时文件 uuid
w.Write([]byte(uuid))
}
/*创建临时文件以 json 格式保存文件信息*/
func (t *tempInfo) writeToFile() error {
f, e := os.Create(os.Getenv(sys.StorageRoot) + "/temp/" + t.Uuid)
if e != nil {
return e
}
defer f.Close()
b, _ := json.Marshal(t)
f.Write(b)
return nil
}
/*将临时文件转成正式文件,并且加入到全局文件缓存中*/
func commitTempObject(datFile string, tempinfo *tempInfo) {
os.Rename(datFile, os.Getenv(sys.StorageRoot)+"/objects/"+tempinfo.Name)
locate.Add(tempinfo.Name)
}
/*删除临时文件信息和临时文件数据*/
func del(w http.ResponseWriter, r *http.Request) {
uuid := strings.Split(r.URL.EscapedPath(), "/")[2]
infoFile := os.Getenv(sys.StorageRoot) + "/temp/" + uuid
datFile := infoFile + ".dat"
os.Remove(infoFile)
os.Remove(datFile)
}
/*将数据写入临时文件*/
func patch(w http.ResponseWriter, r *http.Request) {
// 获取对象的散列值
uuid := strings.Split(r.URL.EscapedPath(), "/")[2]
// 获取对象信息
tempinfo, e := readFromFile(uuid)
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusNotFound)
return
}
// 拼接临时文件路径
infoFile := os.Getenv(sys.StorageRoot) + "/temp/" + uuid
datFile := infoFile + ".dat"
// 打开临时文件,以 os.O_WRONLY|os.O_APPEND 模式打开,只写入,追加数据,不设置权限掩码
f, e := os.OpenFile(datFile, os.O_WRONLY|os.O_APPEND, 0)
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusInternalServerError)
return
}
// 最后关闭文件流
defer f.Close()
// 将请求的数据 r.Body 拷贝到临时文件 f 中
_, e = io.Copy(f, r.Body)
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusInternalServerError)
return
}
/* 用 f.Stat() 方法获取临时文件信息
// A FileInfo describes a file and is returned by Stat.
type FileInfo interface {
Name() string // base name of the file
Size() int64 // length in bytes for regular files; system-dependent for others
Mode() FileMode // file mode bits
ModTime() time.Time // modification time
IsDir() bool // abbreviation for Mode().IsDir()
Sys() interface{} // underlying data source (can return nil)
}
*/
info, e := f.Stat()
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusInternalServerError)
return
}
// 获取临时文件当前大小
actual := info.Size()
// 如果当前大小已经超过了设定的大小,则认为出现错误,删除临时文件信息和临时文件内容,返回 500
if actual > tempinfo.Size {
os.Remove(datFile)
os.Remove(infoFile)
log.Println(sys.SizeMismatch, tempinfo.Size, actual)
w.WriteHeader(http.StatusInternalServerError)
}
}
/*读取文件信息,反序列化 json 格式为临时文件结构体*/
func readFromFile(uuid string) (*tempInfo, error) {
f, e := os.Open(os.Getenv(sys.StorageRoot) + "/temp/" + uuid)
if e != nil {
return nil, e
}
defer f.Close()
b, _ := ioutil.ReadAll(f)
var info tempInfo
json.Unmarshal(b, &info)
return &info, nil
}
GET 请求新增数据校验
package objects
import (
"demo/dataServer/locate"
"demo/sys"
"demo/utils"
"io"
"log"
"net/http"
"net/url"
"os"
"strings"
)
/*对象上传依靠 tmp 接口的临时文件转正,无需 put 方法*/
func Handler(w http.ResponseWriter, r *http.Request) {
m := r.Method
// GET 方法时,获取资源
if m == http.MethodGet {
get(w, r)
return
}
// 其他方式时,返回状态码,方法不允许
w.WriteHeader(http.StatusMethodNotAllowed)
}
/*处理客户端请求数据*/
func get(w http.ResponseWriter, r *http.Request) {
// 文件是否存在
file := getFile(strings.Split(r.URL.EscapedPath(), "/")[2])
if file == "" {
// 不存在则返回 404
w.WriteHeader(http.StatusNotFound)
return
}
// 存在则返回数据
sendFile(w, file)
}
/*检查文件数据是否存在*/
func getFile(hash string) string {
// 打开磁盘中的文件,检查该文件哈希值是否与请求的哈希值一致
file := os.Getenv(sys.StorageRoot) + "/objects/" + hash
f, _ := os.Open(file)
// 计算哈希值并进行 url 转义
d := url.PathEscape(utils.CalculateHash(f))
// 最终关闭文件流
f.Close()
// 如果磁盘文件计算的哈希值与请求的哈希值不一致,说明磁盘的文件数据被降解,需要删除该错误的数据
if d != hash {
log.Printf(sys.HashMismatchThenRemove, file)
// 从全局缓存中移除该文件
locate.Del(hash)
// 磁盘删除该文件
os.Remove(file)
return ""
}
// 哈希值一致则返回文件路径
return file
}
/*返回请求的数据*/
func sendFile(w io.Writer, file string) {
// 打开请求的文件
f, _ := os.Open(file)
defer f.Close()
// 将文件写入响应流 w
io.Copy(w, f)
}
main 函数
package main
import (
"demo/dataServer/heartbeat"
"demo/dataServer/locate"
"demo/dataServer/objects"
"demo/dataServer/tmp"
"demo/sys"
"net/http"
"os"
)
func main() {
// 扫描全文件缓存到 redis 服务器
locate.CollectObjects()
// 向 apiServers exchange 发送心跳
go heartbeat.StartHeartbeat()
// 监听定位信息
go locate.StartLocate()
// 注册URL与逻辑处理函数
// 仅处理 GET 请求,对象上传依靠 tmp 接口的临时文件转正,无需 PUT 方法
http.HandleFunc("/handleObjs/", objects.Handler)
http.HandleFunc("/tmp/", tmp.Handler)
// 启动并监听服务
http.ListenAndServe(os.Getenv(sys.ListenAddress), nil)
}
定时任务
定时任务用于清理 /tmp/ 路径下的临时文件。
使用 Linux 定时任务
使用 cron 包
这里仅做一个简单的清理,后续再完善。
package schedule
import (
"github.com/robfig/cron"
"os/exec"
)
/*
定时清理临时目录下的文件
cron 表达式
字段名 是否必须 允许的值 允许的特定字符
秒(Seconds) 是 0-59 * / , -
分(Minute) 是 0-59 * / , -
时(Hours) 是 0-23 * / , -
日(Day of month) 是 1-31 * / , - ?
月(Month) 是 1-12 或 JAN-DEC * / , -
星期(Day of week) 否 0-6 或 SUM-SAT * / , - ?
*/
func CleanTmpFiles(){
c := cron.New()
// cron 表达式,每天零点执行
spec := "0 0 0 * * ?"
c.AddFunc(spec, func() {
exec.Command("rm -rf /home/sam/tmp/*")
})
c.Start()
}
测试
模拟分布式网络
# 查看本机网络接口
ip a
# 数据服务节点 eth0:1~6
# IP范围 192.168.1.1 ~ 192.168.1.1.6
# 接口服务节点 eth0:7~8
# IP范围 192.168.2.1 ~ 192.168.2.2
# 网络接口绑定多个 IP
ifconfig eth0:1 192.168.1.1/24
ifconfig eth0:2 192.168.1.2/24
ifconfig eth0:3 192.168.1.3/24
ifconfig eth0:4 192.168.1.4/24
ifconfig eth0:5 192.168.1.5/24
ifconfig eth0:6 192.168.1.6/24
ifconfig eth0:7 192.168.2.1/24
ifconfig eth0:8 192.168.2.2/24
导入环境变量
# rabbitmq-server 变量
export RABBITMQ_SERVER=amqp://yushanma:passwd@192.168.0.55:5672
# es-server 变量
export ES_SERVER=192.168.0.55:9200
创建临时文件夹
for i in `seq 1 6`; do mkdir -p files/$i/temp; done
启动服务
# 启动数据服务节点
LISTEN_ADDRESS=192.168.1.1:12345 STORAGE_ROOT=/home/sam/files/1 go run dataServer/cmd/main.go &
LISTEN_ADDRESS=192.168.1.2:12345 STORAGE_ROOT=/home/sam/files/2 go run dataServer/cmd/main.go &
LISTEN_ADDRESS=192.168.1.3:12345 STORAGE_ROOT=/home/sam/files/3 go run dataServer/cmd/main.go &
LISTEN_ADDRESS=192.168.1.4:12345 STORAGE_ROOT=/home/sam/files/4 go run dataServer/cmd/main.go &
LISTEN_ADDRESS=192.168.1.5:12345 STORAGE_ROOT=/home/sam/files/5 go run dataServer/cmd/main.go &
LISTEN_ADDRESS=192.168.1.6:12345 STORAGE_ROOT=/home/sam/files/6 go run dataServer/cmd/main.go &
# 启动接口服务节点
LISTEN_ADDRESS=192.168.2.1:12346 go run apiServer/cmd/main.go &
LISTEN_ADDRESS=192.168.2.2:12346 go run apiServer/cmd/main.go &
模拟客户端
访问服务接口节点 192.168.2.1:12346 连续 PUT 请求内容相同但名字不同的对象:
$ echo -n "余衫马" | openssl dgst -sha256 -binary | base64
O/Ik3r/4kVvblcX1Q7xmVpPI3v/nGRdWAQLk6xknYfs=
curl -v 192.168.2.1:12346/handleObjs/test1 -XPUT -d "余衫马" -H "digest:SHA-256=O/Ik3r/4kVvblcX1Q7xmVpPI3v/nGRdWAQLk6xknYfs="
curl -v 192.168.2.1:12346/handleObjs/test2 -XPUT -d "余衫马" -H "digest:SHA-256=O/Ik3r/4kVvblcX1Q7xmVpPI3v/nGRdWAQLk6xknYfs="
curl -v 192.168.2.1:12346/handleObjs/test3 -XPUT -d "余衫马" -H "digest:SHA-256=O/Ik3r/4kVvblcX1Q7xmVpPI3v/nGRdWAQLk6xknYfs="
定位该对象被保存到哪一个数据服务节点:
curl 192.168.2.1:12346/locateObj/O%2FIk3r%2F4kVvblcX1Q7xmVpPI3v%2FnGRdWAQLk6xknYfs=
其实这里的 192.168.1.1 跟 192.168.1.5 都未必是保存该对象的数据节点,因为数据服务节点是从 redis-server 中查询文件名然后作出响应的,我使用的是同一个 index 数据库,全部数据服务节点的对象都被保存到同一个 index 数据库中,因此会有不确定性。解决方法也很简单,只需要给每个服务节点分配一个唯一的 index 数据库,查询时也不会相互干涉,结果也相应是准确的。
不过我们可以通过 ls 指令查看该对象真正保存到哪里:
[sam@hecs-37464 ~]$ ls files/?/objects/O%2FIk3r%2F4kVvblcX1Q7xmVpPI3v%2FnGRdWAQLk6xknYfs=
files/2/objects/O%2FIk3r%2F4kVvblcX1Q7xmVpPI3v%2FnGRdWAQLk6xknYfs=
可见,数据服务节点 2 才是真正保存对象的节点。
尝试 PUT 一个散列值不一致的对象:
curl -v 192.168.2.1:12346/handleObjs/test1 -XPUT -d "余衫马" -H "digest: SHA-256=O/Ik3r/4kVvblcX1Q7xmVpPI3v/nGRdWAQLk6xknYfs=22"
可以看到响应了预期 400 的错误码。
获取 test1 对象:
curl -v 192.168.2.1:12346/handleObjs/test1
注意,只有每个数据服务节点使用的是同一个 redis-server 不同 index 数据库的情况下,或者说使用同一个 index 数据库的不同 hashTable 的情况下,才能请求成功,否则会报 404 ,使用同一个 index 数据或者同一个 hashTable 时就算节点响应了,也不一定有该对象的数据。
我们修改代码从环境中获取 hashTable 变量:
package locate
import (
"demo/rabbitmq"
myredis "demo/redis"
"demo/sys"
"log"
"os"
"path/filepath"
"strconv"
"sync"
)
// 多读写锁
var mutex sync.RWMutex
// 定位对象
func Locate(hash string) bool {
mutex.Lock()
ok := myredis.Hexists(os.Getenv(sys.RedisHashTableName), hash)
mutex.Unlock()
return ok
}
func Add(hash string) {
mutex.Lock()
myredis.Hset(os.Getenv(sys.RedisHashTableName), hash, string(rune(1)))
mutex.Unlock()
}
func Del(hash string) {
mutex.Lock()
myredis.Hdel(os.Getenv(sys.RedisHashTableName), hash)
mutex.Unlock()
}
// 监听定位信息
func StartLocate() {
q := rabbitmq.New(os.Getenv(sys.RabbitmqServer))
defer q.Close()
// 绑定 data 网络层
q.Bind(sys.DataServersExchange)
// 获取信息管道
c := q.Consume()
// 从管道中遍历信息,msg 为需要定位的存储对象名字
for msg := range c {
// 去掉 json 序列化的双引号
obj, e := strconv.Unquote(string(msg.Body))
if e != nil {
log.Fatalln(e)
}
// 定位存储对象,名字需要 URL 转义 url.PathEscape(obj) ,如果由前端转义则无需处理
if Locate(obj) {
// 如果存储对象存在,则回送本节点监听地址,已告知存储对象在该节点
q.Send(msg.ReplyTo, os.Getenv(sys.ListenAddress))
}
}
}
// 扫描全磁盘,将所有的文件缓存到 redis table
func CollectObjects() {
objects := make(map[string]interface{})
// pattern /home/sam/files/1/objects/*
files, _ := filepath.Glob(os.Getenv(sys.StorageRoot) + "/objects/*")
for i := range files {
hash := filepath.Base(files[i])
objects[hash] = 1
}
// 将 map 缓存到 redis
if len(objects) > 0 {
myredis.Hmset(os.Getenv(sys.RedisHashTableName), objects)
}
}
启动时指定 hashTable 变量:
LISTEN_ADDRESS=192.168.1.1:12345 STORAGE_ROOT=/home/sam/files/1 OBJ_CACHE=cache_1 go run dataServer/cmd/main.go &
至此,数据校验和去重的功能实现完毕。