从0到1手写分布式对象存储系统-04数据校验与去重

为何需要数据校验

上一篇博客中,我们加入了元数据服务作为存储对象的全局唯一标识,并在测试时留下了一个伏笔,查看 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

封装参考:

Golang之redis中间件框架——redigo的使用

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 定时任务

参考 Linux/UNIX 定时任务 cron 详解 。

使用 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 &

至此,数据校验和去重的功能实现完毕。 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

余衫马

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值