实现一个可扩展的,简易的,分布式对象存储系统

存储系统介绍

先谈谈传统的网络存储,传统的网络存储主要分为两类:

NAS,即Newtwork Attached Storage,是一个提供了存储功能和文件系统的网络服务器,客户端可以访问NAS上的文件系统,可以上传和下载文件,NAS客户端和服务端之间使用的协议有SMB、NFS以及AFS等网络文件系统协议。

对于客户端来说,NAS是一个网络上的文件服务器。SAN即Storage Area Network,SAN只提供了块存储,而把文件系统的抽象交给客户端来管理。对于客户端来说,SAN就是一块磁盘,可以对其格式化、创建文件系统并挂载。

数据管理方式

对象存储对数据管理方式不同于传统网络存储,对于网络文件系统,数据是以一个个文件的形似来管理的,对于块存储,数据是以数据块的形式来管理,每个数据块都有它自己的地址,但是没有额外的背景信息,对象存储则是以对象的方式来管理数据。

一个对象通常包含3个部分:对象的数据、对象的元数据以及一个唯一的标识符ID,对象的数据就是该对象中存储的数据本身,一个对象可以用来保存大量无结构的数据,比如一张图片或者一个在线文档。对象的元数据是对象的描述信息,为了和对象的数据本身区分开来,称其为元数据。对象的标识符具有全局唯一性,一般用对象的散列值。

数据访问方式

网络文件系统的客户端通过NFS等网络协议访问某个远程服务器上存储的文件,块存储的客户端通过数据块的地址访问SAN上数据块,对象存储则通过REST网络服务访问对象。REST即Representational Sate Transfer,REST网络服务通过标准HTTP服务对网络资源提供一套预先定义的无状态操作。客户端向REST网络服务发起请求并接收响应,以确认网络资源发生了某种变化。

HTTP预定义的请求方法通常包括GET、POST、PUT、DELETE等,分别对应不同的处理方式: GET方法在REST标准中通常用来获取某个网络资源,PUT通常用于创建或替换某个网络资源,POST通常用于创建某个网络资源,DELETE通常用于删除某个网络资源。

对象存储优势

对象存储提升了存储系统的扩展性。当一个存储系统中保存的数据越来越多时,存储系统也需要同步扩展,然而由于存储架构的硬性限制,传统网络存储系统的管理开销会不断上升,而对象存储架构只要添加新的存储节点即可。

另一大优势在于以更低的代价提供了数据冗余的能力,在分布式对象存储系统中一个或多个节点失效的情况下,对象依然可用,且大多数情况下客户都不会意识到有节点出了问题

单点对象存储

先实现一个在单机的对象存储系统,基于HTTP提供的REST接口,目录结构:

  1. .
  2. ├── go.mod
  3. ├── main
  4.    └── main.go
  5. └── objects
  6. └── server.go

main包作为程序的入口,objects包来实现主要的逻辑

main方法的实现:

  1. package main
  2. import (
  3. "log"
  4. "net/http"
  5. "os"
  6. "storage/objects"
  7. )
  8. func main() {
  9. http.HandleFunc("/objects/", objects.Handler)
  10. addr := os.Getenv("LISTEN_ADDRESS")
  11. err := http.ListenAndServe(addr, nil)
  12. if err != nil {
  13. log.Fatalln(err)
  14. }
  15. }

main函数中使用HandleFunc注册一个handler,并调用ListenAndServe开始监听端口,监听的地址将在环境变量中定义,handler方法将在objects包中实现

正常情况下ListenAndServe是不会返回的,将会一直监听在指定端口,除非外部将其中断,非正常情况下会返回错误信息

objects包的实现:

  1. package objects
  2. import (
  3. "io"
  4. "log"
  5. "net/http"
  6. "os"
  7. "strings"
  8. )
  9. func Handler(w http.ResponseWriter, r *http.Request) {
  10. method := r.Method
  11. if method == http.MethodPut {
  12. if err := put(w, r); err != nil {
  13. log.Println(err)
  14. }
  15. return
  16. }
  17. if method == http.MethodGet {
  18. if err := get(w, r); err != nil {
  19. log.Println(err)
  20. }
  21. return
  22. }
  23. w.WriteHeader(http.StatusMethodNotAllowed)
  24. }
  25. func put(w http.ResponseWriter, r *http.Request) error {
  26. f, err := os.Create(os.Getenv("STORAGE_ROOT") +
  27. "/objects/" + strings.Split(r.URL.EscapedPath(), "/")[2])
  28. defer f.Close()
  29. if err != nil {
  30. w.WriteHeader(http.StatusInternalServerError)
  31. return err
  32. }
  33. _, err = io.Copy(f, r.Body)
  34. if err != nil {
  35. w.WriteHeader(http.StatusInternalServerError)
  36. }
  37. return err
  38. }
  39. func get(w http.ResponseWriter, r *http.Request) error {
  40. f, err := os.Open(os.Getenv("STORAGE_ROOT") +
  41. "/objects/" + strings.Split(r.URL.EscapedPath(), "/")[2])
  42. defer f.Close()
  43. if err != nil {
  44. w.WriteHeader(http.StatusNotFound)
  45. return err
  46. }
  47. _, err = io.Copy(w, f)
  48. if err != nil {
  49. w.WriteHeader(http.StatusInternalServerError)
  50. }
  51. return err
  52. }

Handler函数只处理GET请求和PUT请求,收到GET请求就调用get方法,收到PUT请求就调用put方法

io.Copy用于传输数据,地一个参数是写入的io.Writer,第二个参数是用于读取的io.Reader,put函数中首先创建指定的文件,如果创建失败则返回500状态码,创建成功则将Body数据写入文件,get函数同理,只不过先打开一个本地的文件,然后将文件数据写入response

运行测试:

设置环境变量

  1. $ export LISTEN_ADDRESS=:8080
  2. $ export STORAGE_ROOT=/tmp

使用curl发起请求:

  1. $ curl -v 127.0.0.1:8080/objects/test
  2. * Uses proxy env variable no_proxy == 'localhost,127.0.0.0/8,::1'
  3. * Uses proxy env variable http_proxy == 'http://127.0.0.1:7890/'
  4. * Trying 127.0.0.1:7890...
  5. * TCP_NODELAY set
  6. * Connected to 127.0.0.1 (127.0.0.1) port 7890 (#0)
  7. > GET http://127.0.0.1:8080/objects/test HTTP/1.1
  8. > Host: 127.0.0.1:8080
  9. > User-Agent: curl/7.68.0
  10. > Accept: */*
  11. > Proxy-Connection: Keep-Alive
  12. >
  13. * Mark bundle as not supporting multiuse
  14. < HTTP/1.1 404 Not Found
  15. < Connection: keep-alive
  16. < Date: Fri, 26 Aug 2022 06:17:29 GMT
  17. < Keep-Alive: timeout=4
  18. < Proxy-Connection: keep-alive
  19. < Content-Length: 0
  20. <
  21. * Connection #0 to host 127.0.0.1 left intact

发送请求,服务器给出了404回复,下面使用PUT添加数据:

  1. $ curl -v 127.0.0.1:8080/objects/test -XPUT -d"this is a test objects"
  2. * Uses proxy env variable no_proxy == 'localhost,127.0.0.0/8,::1'
  3. * Uses proxy env variable http_proxy == 'http://127.0.0.1:7890/'
  4. * Trying 127.0.0.1:7890...
  5. * TCP_NODELAY set
  6. * Connected to 127.0.0.1 (127.0.0.1) port 7890 (#0)
  7. > PUT http://127.0.0.1:8080/objects/test HTTP/1.1
  8. > Host: 127.0.0.1:8080
  9. > User-Agent: curl/7.68.0
  10. > Accept: */*
  11. > Proxy-Connection: Keep-Alive
  12. > Content-Length: 22
  13. > Content-Type: application/x-www-form-urlencoded
  14. >
  15. * upload completely sent off: 22 out of 22 bytes
  16. * Mark bundle as not supporting multiuse
  17. < HTTP/1.1 200 OK
  18. < Content-Length: 0
  19. < Connection: keep-alive
  20. < Date: Fri, 26 Aug 2022 06:37:57 GMT
  21. < Keep-Alive: timeout=4
  22. < Proxy-Connection: keep-alive
  23. <
  24. * Connection #0 to host 127.0.0.1 left intact

用curl命令PUT了一个名为test的对象,该对象的内容为"this is a test object",服务器返回"200 OK",表示PUT成功

GET这个对象:

  1. $ curl -v 127.0.0.1:8080/objects/test
  2. * Uses proxy env variable no_proxy == 'localhost,127.0.0.0/8,::1'
  3. * Uses proxy env variable http_proxy == 'http://127.0.0.1:7890/'
  4. * Trying 127.0.0.1:7890...
  5. * TCP_NODELAY set
  6. * Connected to 127.0.0.1 (127.0.0.1) port 7890 (#0)
  7. > GET http://127.0.0.1:8080/objects/test HTTP/1.1
  8. > Host: 127.0.0.1:8080
  9. > User-Agent: curl/7.68.0
  10. > Accept: */*
  11. > Proxy-Connection: Keep-Alive
  12. >
  13. * Mark bundle as not supporting multiuse
  14. < HTTP/1.1 200 OK
  15. < Content-Length: 22
  16. < Connection: keep-alive
  17. < Content-Type: text/plain; charset=utf-8
  18. < Date: Fri, 26 Aug 2022 06:40:02 GMT
  19. < Keep-Alive: timeout=4
  20. < Proxy-Connection: keep-alive
  21. <
  22. * Connection #0 to host 127.0.0.1 left intact
  23. this is a test objects

获取到了内容

单机的对象存储缺乏可扩展性,接口和数据存储耦合度高,分布式对象存储应该是可扩展的

可扩展分布式系统

一个分布式系统要求各节点分布在网络上,通过消息传递来合作完成一个共同目标,分布式系统的三大关键特征是: 节点之间并发工作,没有全局锁以及某个节点上发生的错误不影响其他节点,只要加入新的节点就可以自由扩展集群的性能。相比单机的对象存储,下面要将接口和数据类型解耦合,让接口和数据存储成为相互独立的服务节点,两者互相合作提供对象存储服务

如上是架构图,接口服务层对外提供了REST接口,而数据服务层则提供数据的存储服务。接口服务处理客户端的请求,然后向数据服务存取对象,数据服务处理来自接口服务的请求并在本地磁盘上存取对象,数据服务处理来自接口服务的请求并在本地磁盘上存取对象

接口服务和数据服务之间的接口有两种,一种是接口实现对象的存取,对象的存取使用REST接口,此时接口服务节点作为HTTP客户端向数据服务请求对象,还有一种接口通过RabbitMQ消息队列进行通信,这里对RabbitMQ的使用分为两种模式,一种模式是向某个exchange进行一对多的消息群发,另一种模式是向某个消息队列进行一对一的消息单发

使用RabbitMQ

为了使用RabbitMQ,须要下载RabbitMQ提供的Go语言包:go get github.com/streadway/amqp

相关文档: https://pkg.go.dev/github.com/streadway/amqp@v1.0.0

创建rabbitmq包:

  1. package rabbitmq
  2. import (
  3. "encoding/json"
  4. "github.com/streadway/amqp"
  5. )
  6. type RabbitMQ struct {
  7. channel *amqp.Channel
  8. Name string
  9. exchange string
  10. }
  11. func New(s string) *RabbitMQ {
  12. conn, err := amqp.Dial(s)
  13. if err != nil {
  14. panic(err)
  15. }
  16. ch, err := conn.Channel()
  17. if err != nil {
  18. panic(err)
  19. }
  20. queue, err := ch.QueueDeclare(
  21. "", false, true,
  22. false, false, nil)
  23. if err != nil {
  24. panic(err)
  25. }
  26. mq := new(RabbitMQ)
  27. mq.channel = ch
  28. mq.Name = queue.Name
  29. return mq
  30. }
  31. func (q *RabbitMQ) Bind(exchange string) {
  32. err := q.channel.QueueBind(
  33. q.Name, "", exchange,
  34. false, nil)
  35. if err != nil {
  36. panic(err)
  37. }
  38. q.exchange = exchange
  39. }
  40. func (q *RabbitMQ) Send(queue string, body interface{}) {
  41. s, err := json.Marshal(body)
  42. if err != nil {
  43. panic(err)
  44. }
  45. err = q.channel.Publish(
  46. "", queue,
  47. false, false,
  48. amqp.Publishing{
  49. ReplyTo: q.Name,
  50. Body: []byte(s),
  51. })
  52. if err != nil {
  53. panic(err)
  54. }
  55. }
  56. func (q *RabbitMQ) Publish(exchange string, body interface{}) {
  57. s, err := json.Marshal(body)
  58. if err != nil {
  59. panic(err)
  60. }
  61. err = q.channel.Publish(
  62. exchange, "", false,
  63. false, amqp.Publishing{
  64. ReplyTo: q.Name, Body: []byte(s),
  65. })
  66. if err != nil {
  67. panic(err)
  68. }
  69. }
  70. func (q *RabbitMQ) Consume() <-chan amqp.Delivery {
  71. c, err := q.channel.Consume(
  72. q.Name, "", true, false,
  73. false, false, nil)
  74. if err != nil {
  75. panic(err)
  76. }
  77. return c
  78. }
  79. func (q *RabbitMQ) Close() {
  80. q.channel.Close()
  81. }

首先是New函数用于创建一个新的结构体:

  1. func New(s string) *RabbitMQ {
  2. conn, err := amqp.Dial(s)
  3. if err != nil {
  4. panic(err)
  5. }
  6. ch, err := conn.Channel()
  7. if err != nil {
  8. panic(err)
  9. }
  10. queue, err := ch.QueueDeclare(
  11. "", false, true,
  12. false, false, nil)
  13. if err != nil {
  14. panic(err)
  15. }
  16. mq := new(RabbitMQ)
  17. mq.channel = ch
  18. mq.Name = queue.Name
  19. return mq
  20. }

调用amqp.Dial创建一个连接,调用Channel方法创建一个通道,调用QueueDeclare方法创建一个队列,赋值后返回RabbitMQ结构体,之后定义的Bind方法:

  1. func (q *RabbitMQ) Bind(exchange string) {
  2. err := q.channel.QueueBind(
  3. q.Name, "", exchange,
  4. false, nil)
  5. if err != nil {
  6. panic(err)
  7. }
  8. q.exchange = exchange
  9. }

该方法可以将消息队列和一个exchange绑定,调用QueueBind方法,传入队列名称和exchange

Send方法可以往某个消息队列发送消息:

  1. func (q *RabbitMQ) Send(queue string, body interface{}) {
  2. s, err := json.Marshal(body)
  3. if err != nil {
  4. panic(err)
  5. }
  6. err = q.channel.Publish(
  7. "", queue,
  8. false, false,
  9. amqp.Publishing{
  10. ReplyTo: q.Name,
  11. Body: []byte(s),
  12. })
  13. if err != nil {
  14. panic(err)
  15. }
  16. }

Publish方法可以往某个exchange发送消息:

  1. func (q *RabbitMQ) Publish(exchange string, body interface{}) {
  2. s, err := json.Marshal(body)
  3. if err != nil {
  4. panic(err)
  5. }
  6. err = q.channel.Publish(
  7. exchange, "", false,
  8. false, amqp.Publishing{
  9. ReplyTo: q.Name, Body: []byte(s),
  10. })
  11. if err != nil {
  12. panic(err)
  13. }
  14. }

Consume方法用于生成一个可接收消息的go channel,使客户程序可以通过Go语言的原生机制接收队列中的消息:

  1. func (q *RabbitMQ) Consume() <-chan amqp.Delivery {
  2. c, err := q.channel.Consume(
  3. q.Name, "", true, false,
  4. false, false, nil)
  5. if err != nil {
  6. panic(err)
  7. }
  8. return c
  9. }

Close方法用于关闭消息队列:

  1. func (q *RabbitMQ) Close() {
  2. q.channel.Close()
  3. }

实现数据服务

创建dataserver文件夹,下面是数据服务的实现,数据服务的REST接口与单击版本一致,但实现上有所变化

数据服务程序入口,main函数:

  1. package main
  2. import (
  3. "distributed-storge/dataserver/heartbeat"
  4. "distributed-storge/dataserver/locate"
  5. "log"
  6. "net/http"
  7. "os"
  8. )
  9. func main() {
  10. go heartbeat.StartHeartbeat()
  11. go locate.StartLocate()
  12. http.HandleFunc("/objects/", objects.Handler)
  13. address := os.Getenv("LISTEN_ADDRESS")
  14. err := http.ListenAndServe(address, nil)
  15. if err != nil {
  16. log.Println(err)
  17. }
  18. }

这里先用了两个goroutine,第一个goroutine执行heartbeat.StartHeartbeat函数,heartbeat还未实现,正如其名,与心跳请求相关,第二个goroutine执行locate.StartLocate函数,用于实际定位对象

实现heartbeat

这个包中只实现一个StartHeartbeat,该函数每5s向apiServers exchange发送一条消息

  1. package heartbeat
  2. import (
  3. "distributed-storge/rabbitmq"
  4. "os"
  5. "time"
  6. )
  7. func StartHeartbeat() {
  8. server := os.Getenv("RABBITMQ_SERVER")
  9. q := rabbitmq.New(server)
  10. defer q.Close()
  11. for {
  12. address := os.Getenv("LISTEN_ADDRESS")
  13. q.Publish("apiServers", address)
  14. time.Sleep(5 * time.Second)
  15. }
  16. }

heartbeat.StartHeartbeat调用rabbitmq.New创建一个rabbitmq.RabbitMQ结构体,并不停循环调用Publish方法,向apiServer exchange发送本节点的监听地址,由于该函数在一个goroutine中执行,所以不返回也不影响功能

实现locate包

有两个函数,分别用于实际定位对象的Locate函数和用于监听定位消息的StartLocate函数

  1. package locate
  2. import (
  3. "distributed-storge/rabbitmq"
  4. "os"
  5. "strconv"
  6. )
  7. func Locate(name string) bool {
  8. _, err := os.Stat(name)
  9. return !os.IsNotExist(err)
  10. }
  11. func StartLocate() {
  12. server := os.Getenv("RABBITMQ_SERVER")
  13. q := rabbitmq.New(server)
  14. defer q.Close()
  15. q.Bind("dataServers")
  16. c := q.Consume()
  17. for msg := range c {
  18. object, err := strconv.Unquote(string(msg.Body))
  19. if err != nil {
  20. panic(err)
  21. }
  22. root := os.Getenv("STORAGE_ROOT") + "/objects/" + object
  23. address := os.Getenv("LISTEN_ADDRESS")
  24. if Locate(root) {
  25. q.Send(msg.ReplyTo, address)
  26. }
  27. }
  28. }

Locate函数用os.Stat访问磁盘上对应的文件名,用os.IsNotExist判断文件名是否存在,如果存在则定位成功true,否则定位失败返回false

StartLocate函数会创建一个rabbitmq.RabbitMQ结构体,并调用其Bind方法绑定dataService exchange,rabbitmq.RabbitMQ结构体的Consume方法会返回一个Go语言的通道,遍历这个通道可以接收消息,消息的正文内容是接受服务发送过来要做定位的对象名字,经过了JSON编码。在对象名字前加上相应的存储目录并以此作为文件名,然后调用locate函数检查文件是否存在,如果存在则调用Send方法向消息的发送方返回本服务节点的监听地址,表示该对象存在于本服务节点上

实现接口服务

创建apiserver文件夹

提供REST接口和locate功能,main函数入口:

  1. package apiserver
  2. import (
  3. "distributed-storge/apiserver/heartbeat"
  4. "log"
  5. "net/http"
  6. "os"
  7. )
  8. func main() {
  9. go heartbeat.ListenHeartbeat()
  10. http.HandleFunc("/objects/", objects.Handler)
  11. http.HandleFunc("/locate/", locate.Handler)
  12. address := os.Getenv("LISTEN_ADDRESS")
  13. err := http.ListenAndServe(address, nil)
  14. if err != nil {
  15. log.Println(err)
  16. }
  17. }

接口服务的main函数启动一个goroutine来执行heartbeat.ListenHeartbeat函数,接口服务除了需要objects.Handler处理URL,以/objects/开头的对象以外,还要有一个locate.Handler函数处理URL以/locate/开头的定位请求

实现heartbeat

接口服务的heartbeat用于接收数据服务节点的心跳消息,定义了4个函数用于接收和处理来自数据服务节点的心跳消息:

  1. package heartbeat
  2. import (
  3. "distributed-storge/rabbitmq"
  4. "math/rand"
  5. "os"
  6. "strconv"
  7. "sync"
  8. "time"
  9. )
  10. var dataServers = make(map[string]time.Time)
  11. var mutex sync.Mutex
  12. func ListenHeartbeat() {
  13. server := os.Getenv("RABBIT_SERVER")
  14. q := rabbitmq.New(server)
  15. defer q.Close()
  16. q.Bind("apiServer")
  17. c := q.Consume()
  18. go removeExpiredDataServer()
  19. for msg := range c {
  20. dataServer, err := strconv.Unquote(string(msg.Body))
  21. if err != nil {
  22. panic(err)
  23. }
  24. mutex.Lock()
  25. dataServers[dataServer] = time.Now()
  26. mutex.Unlock()
  27. }
  28. }
  29. func removeExpiredDataServer() {
  30. for {
  31. time.Sleep(5 * time.Second)
  32. mutex.Lock()
  33. for s, t := range dataServers {
  34. if t.Add(10 * time.Second).Before(time.Now()) {
  35. delete(dataServers, s)
  36. }
  37. }
  38. mutex.Unlock()
  39. }
  40. }
  41. func GetDataServers() []string {
  42. mutex.Lock()
  43. defer mutex.Unlock()
  44. ds := make([]string, 0)
  45. for s, _ := range dataServers {
  46. ds = append(ds, s)
  47. }
  48. return ds
  49. }
  50. func ChooseRandomDataServer() string {
  51. ds := GetDataServers()
  52. n := len(ds)
  53. if n == 0 {
  54. return ""
  55. }
  56. return ds[rand.Intn(n)]
  57. }

开头定义了一个map即dataServers,用于缓存所有的数据服务节点,记录了最近一次心跳消息的时间,这里对dataServers的读写全部都需要mutex保护,以防止多个goroutine并发读写map造成错误,Go语言的map可以支持多个goroutine同时读,但不能支持多个goroutine同时既读又写,所以要使用一个互斥锁保护map的并发读写,mutex的类型是sync.Mutex,无论读写都只允许一个goroutine操作map

ListenHeartbeat函数创建消息队列来绑定apiServers exchange并通过通道监听每一个来自数据服务节点的心跳消息,将该消息的正文内容,即数据服务的监听地址作为map的键,收到消息的时间作为值存入dataServers

  1. func ListenHeartbeat() {
  2. server := os.Getenv("RABBIT_SERVER")
  3. q := rabbitmq.New(server)
  4. defer q.Close()
  5. q.Bind("apiServer")
  6. c := q.Consume()
  7. go removeExpiredDataServer()
  8. for msg := range c {
  9. dataServer, err := strconv.Unquote(string(msg.Body))
  10. if err != nil {
  11. panic(err)
  12. }
  13. mutex.Lock()
  14. dataServers[dataServer] = time.Now()
  15. mutex.Unlock()
  16. }
  17. }

removeExpiredDataServer函数在一个goroutine中运行,每隔5s遍历一遍dataServers,并清除其中超过10s没收到心跳消息的数据服务节点

  1. func removeExpiredDataServer() {
  2. for {
  3. time.Sleep(5 * time.Second)
  4. mutex.Lock()
  5. for s, t := range dataServers {
  6. if t.Add(10 * time.Second).Before(time.Now()) {
  7. delete(dataServers, s)
  8. }
  9. }
  10. mutex.Unlock()
  11. }
  12. }

通过调用Add方法为时间加上10秒,通过Before方法来比较时间

GetDataServers遍历dataServers并返回当前所有的数据服务节点

  1. func GetDataServers() []string {
  2. mutex.Lock()
  3. defer mutex.Unlock()
  4. ds := make([]string, 0)
  5. for s, _ := range dataServers {
  6. ds = append(ds, s)
  7. }
  8. return ds
  9. }

ChooseRandomDataServer函数会在当前所有的数据服务节点随机选出一个节点并返回,如果当前数据服务节点为空,则返回空字符串:

  1. func ChooseRandomDataServer() string {
  2. ds := GetDataServers()
  3. n := len(ds)
  4. if n == 0 {
  5. return ""
  6. }
  7. return ds[rand.Intn(n)]
  8. }

实现locate

向数据服务节点群发定位消息并接收反馈:

  1. package locate
  2. import (
  3. "distributed-storge/rabbitmq"
  4. "encoding/json"
  5. "net/http"
  6. "os"
  7. "strconv"
  8. "strings"
  9. "time"
  10. )
  11. func Handler(w http.ResponseWriter, r *http.Request) {
  12. m := r.Method
  13. if m != http.MethodGet {
  14. w.WriteHeader(http.StatusMethodNotAllowed)
  15. return
  16. }
  17. info := Locate(strings.Split(r.URL.EscapedPath(), "/")[2])
  18. if len(info) == 0 {
  19. w.WriteHeader(http.StatusNotFound)
  20. return
  21. }
  22. b, _ := json.Marshal(info)
  23. w.Write(b)
  24. }
  25. func Locate(name string) string {
  26. server := os.Getenv("RABBITMQ_SERVER")
  27. q := rabbitmq.New(server)
  28. q.Publish("dataServers", name)
  29. c := q.Consume()
  30. go func() {
  31. time.Sleep(time.Second)
  32. q.Close()
  33. }()
  34. msg := <-c
  35. s, _ := strconv.Unquote(string(msg.Body))
  36. return s
  37. }
  38. func Exist(name string) bool {
  39. return Locate(name) != ""
  40. }

Handler函数用于处理HTTP请求,如果请求方法不为GET,则返回405,如果请求方法为GET,截取出Object名称再传给Locate来定位这个对象

Locate接收的是需要定位的对象的名字,会创建一个新的消息队列,并向dataServers exchange群发这个对象名字的定位信息,随后启用一个goroutine调用匿名函数,用于在1s后关闭这个临时消息队列,这是为了设置一个超时机制,避免无限等待

Exist函数通过检查Locate结果是否为空字符串来判定对象是否存在

实现objectstream

这个包是对http包的一个封装,用来把一些http函数的调用转换成读写流的形式,方便处理

  1. package objectstream
  2. import (
  3. "fmt"
  4. "io"
  5. "net/http"
  6. )
  7. type PutStream struct {
  8. writer *io.PipeWriter
  9. c chan error
  10. }
  11. func NewPutStream(server, object string) *PutStream {
  12. reader, writer := io.Pipe()
  13. c := make(chan error)
  14. go func() {
  15. request, _ := http.NewRequest("PUT", "http://"+server+"/objects/"+object, reader)
  16. client := http.Client{}
  17. resp, err := client.Do(request)
  18. if err != nil && resp.StatusCode != http.StatusOK {
  19. err = fmt.Errorf("dataserver return http code %d", resp.StatusCode)
  20. }
  21. c <- err
  22. }()
  23. return &PutStream{writer, c}
  24. }
  25. func (w *PutStream) Write(p []byte) (int, error) {
  26. return w.writer.Write(p)
  27. }
  28. func (w *PutStream) Close() error {
  29. w.writer.Close()
  30. return <-w.c
  31. }
  32. type GetStream struct {
  33. reader io.Reader
  34. }
  35. func newGetStream(url string) (*GetStream, error) {
  36. resp, err := http.Get(url)
  37. if err != nil {
  38. return nil, err
  39. }
  40. if resp.StatusCode != http.StatusOK {
  41. return nil, fmt.Errorf("dataServer return http code %d", resp.StatusCode)
  42. }
  43. return &GetStream{resp.Body}, nil
  44. }
  45. func NewGetStream(server, object string) (*GetStream, error) {
  46. if server == "" || object == "" {
  47. return nil, fmt.Errorf("invalid server %s object %s", server, object)
  48. }
  49. return newGetStream("http://" + server + "/objects" + object)
  50. }
  51. func (r *GetStream) Read(p []byte) (int, error) {
  52. return r.reader.Read(p)
  53. }

如下是关于PutStream的定义:

  1. type PutStream struct {
  2. writer *io.PipeWriter
  3. c chan error
  4. }
  5. func NewPutStream(server, object string) *PutStream {
  6. reader, writer := io.Pipe()
  7. c := make(chan error)
  8. go func() {
  9. request, _ := http.NewRequest("PUT", "http://"+server+"/objects/"+object, reader)
  10. client := http.Client{}
  11. resp, err := client.Do(request)
  12. if err != nil && resp.StatusCode != http.StatusOK {
  13. err = fmt.Errorf("dataserver return http code %d", resp.StatusCode)
  14. }
  15. c <- err
  16. }()
  17. return &PutStream{writer, c}
  18. }
  19. func (w *PutStream) Write(p []byte) (int, error) {
  20. return w.writer.Write(p)
  21. }
  22. func (w *PutStream) Close() error {
  23. w.writer.Close()
  24. return <-w.c
  25. }

首先定义了一个结构体PutStream,内含一个io.PipeWriter的指针writer和一个error的通道,这个通道用于把一个goroutine传输数据的过程中发生的错误传回主线程

NewPutStream函数用于生成一个PutStream结构体,用io.Pipe创建了一对reader和writer,类型分别是*io.PipeReader*io.PipeWriter,他们是管道互联的,写入writer的内容可以从reader中读出来,这一对管道用于以写入数据流的方式操作HTTP的PUT请求。Go语言的HTTP包在生成一个PUT请求时要求提供一个io.Reader作为http.NewRequest的参数,由一个类型为http.Client的client的负责读取要PUT的内容,通过这对管道就可以在满足http.NewRequest的参数要求时用写入writer的方式实现PutStream的Write方法,由于管道是阻塞的,所以要调用一个goroutine来调用client.Do方法

Close方法用于关闭管道,否则Reader将会被一直阻塞

如下是关于GetStream的定义:

  1. type GetStream struct {
  2. reader io.Reader
  3. }
  4. func newGetStream(url string) (*GetStream, error) {
  5. resp, err := http.Get(url)
  6. if err != nil {
  7. return nil, err
  8. }
  9. if resp.StatusCode != http.StatusOK {
  10. return nil, fmt.Errorf("dataServer return http code %d", resp.StatusCode)
  11. }
  12. return &GetStream{resp.Body}, nil
  13. }
  14. func NewGetStream(server, object string) (*GetStream, error) {
  15. if server == "" || object == "" {
  16. return nil, fmt.Errorf("invalid server %s object %s", server, object)
  17. }
  18. return newGetStream("http://" + server + "/objects" + object)
  19. }
  20. func (r *GetStream) Read(p []byte) (int, error) {
  21. return r.reader.Read(p)
  22. }

GetStream只需要一个成员reader用于记录http返回的io.Reader

newGetStream的函数的输入参数URL是一个字符串,表示用于获取数据流的HTTP服务地址,之后发起一个Get请求得到响应Body,传入结构体返回

Read方法用于读取reader成员,只要实现了该方法,就实现了io.Reader接口

实现objects

实现REST接口,负责将HTTP请求转发给数据服务

  1. package objects
  2. import (
  3. "distributed-storge/apiserver/heartbeat"
  4. "distributed-storge/apiserver/locate"
  5. "distributed-storge/apiserver/objectstream"
  6. "fmt"
  7. "io"
  8. "log"
  9. "net/http"
  10. "strings"
  11. )
  12. func Handler(w http.ResponseWriter, r *http.Request) {
  13. method := r.Method
  14. if method == http.MethodPut {
  15. put(w, r)
  16. }
  17. if method == http.MethodGet {
  18. get(w, r)
  19. }
  20. w.WriteHeader(http.StatusMethodNotAllowed)
  21. }
  22. func put(w http.ResponseWriter, r *http.Request) {
  23. object := strings.Split(r.URL.EscapedPath(), "/")[2]
  24. c, err := storeObject(r.Body, object)
  25. if err != nil {
  26. log.Println(err)
  27. }
  28. w.WriteHeader(c)
  29. }
  30. func storeObject(r io.Reader, object string) (int, error) {
  31. stream, err := putStream(object)
  32. if err != nil {
  33. return http.StatusServiceUnavailable, err
  34. }
  35. io.Copy(stream, r)
  36. err = stream.Close()
  37. if err != nil {
  38. return http.StatusInternalServerError, err
  39. }
  40. return http.StatusOK, nil
  41. }
  42. func putStream(object string) (*objectstream.PutStream, error) {
  43. server := heartbeat.ChooseRandomDataServer()
  44. if server == "" {
  45. return nil, fmt.Errorf("cannot find any dataserver")
  46. }
  47. return objectstream.NewPutStream(server, object), nil
  48. }
  49. func get(w http.ResponseWriter, r *http.Request) {
  50. object := strings.Split(r.URL.EscapedPath(), "/")[2]
  51. stream, err := getStream(object)
  52. if err != nil {
  53. log.Println(err)
  54. w.WriteHeader(http.StatusNotFound)
  55. return
  56. }
  57. io.Copy(w, stream)
  58. }
  59. func getStream(object string) (io.Reader, error) {
  60. server := locate.Locate(object)
  61. if server == "" {
  62. return nil, fmt.Errorf("object %s locate fail", object)
  63. }
  64. return objectstream.NewGetStream(server, object)
  65. }

put截取出object名称,并和请求的Body一起传给storeObject函数,storeObject函数中先调用putStream函数生成了stream,此时就已经发起了HTTP请求,随后将请求正文写入这个stream后关闭,putStream函数首先调用heartbeat.ChooseRandomDataServer函数获得一个随机数据服务节点地址server,如果server为空字符串,则意味着当前没有可用的数据服务节点,客户端会收到HTTP错误代码503

get同样获得object名称,然后以之为参数调用getStream生成stream,其参数object是一个字符串,代表对象名称,调用locate定位这个对象

Go语言实现分布式对象存储系统的更多相关文章

  1. go语言实现分布式对象存储系统之单体对象存储

    对象存储 基本概念 主流存储类型分为三种:块存储.文件存储以及对象存储 NAS(文件存储):Network Attached storage,提供了存储功能和文件系统的网络服务器,客户端可以访问NAS ...

  2. 三种分布式对象主流技术——COM、Java和COBRA

    既上一遍,看到还有一遍将关于 对象的, 分布式对象, 故摘抄入下: 目前国际上,分布式对象技术有三大流派——COBRA.COM/DCOM和Java.CORBA技术是最早出现的,1991年OMG颁布了C ...

  3. 用asp.net core结合fastdfs打造分布式文件存储系统

    最近被安排开发文件存储微服务,要求是能够通过配置来无缝切换我们公司内部研发的文件存储系统,FastDFS,MongDb GridFS,阿里云OSS,腾讯云OSS等.根据任务紧急度暂时先完成了通过配置来 ...

  4. 淘宝分布式文件存储系统:TFS

    TFS ——分布式文件存储系统 TFS(Taobao File System)是淘宝针对海量非结构化数据存储设计的分布式系统,构筑在普通的Linux机器集群上,可为外部提供高可靠和高并发的存储访问. ...

  5. 分布式 Key-Value 存储系统:Cassandra 入门

    Apache Cassandra 是一套开源分布式 Key-Value 存储系统.它最初由 Facebook 开发,用于储存特别大的数据. Cassandra 不是一个数据库,它是一个混合型的非关系的 ...

  6. Swift是一个提供RESTful HTTP接口的对象存储系统

    Swift是一个提供RESTful HTTP接口的对象存储系统,最初起源于Rackspace的Cloud Files,目的是为了提供一个和AWS S3竞争的服务. Swift于2010年开源,是Ope ...

  7. 一图看懂hadoop分布式文件存储系统HDFS工作原理

    一图看懂hadoop分布式文件存储系统HDFS工作原理

  8. Swift是一个提供RESTful HTTP接口的对象存储系统,目的是为了提供一个和AWS S3竞争的服务

    Swift是一个提供RESTful HTTP接口的对象存储系统,最初起源于Rackspace的Cloud Files,目的是为了提供一个和AWS S3竞争的服务. Swift于2010年开源,是Ope ...

  9. 腾讯重磅开源分布式NoSQL存储系统DCache

    当你在电商平台秒杀商品或者在社交网络刷热门话题的时候,可以很明显感受到当前网络数据流量的恐怖,几十万商品刚开抢,一秒都不到就售罄:哪个大明星出轨的消息一出现,瞬间阅读与转发次数可以达到上亿.作为终端用 ...

  10. 必须掌握的分布式文件存储系统—HDFS

    HDFS(Hadoop Distributed File System)分布式文件存储系统,主要为各类分布式计算框架如Spark.MapReduce等提供海量数据存储服务,同时HBase.Hive底层 ...

随机推荐

  1. Android Native Code 手动调试

    调试启动过程中的 Android Native Code Crash 记录一下,最后成功使用的工具是 lldb + lldb-server,不需要 root 权限.我最先尝试使用的是,gdb + gd ...

  2. 关于JSP无法使用静态引用的问题案例

    问题描述: 在写项目时,对于头部信息,尾部信息,分页信息等出现频率高,又很雷同的部分进行抽取时,使用到了jsp的静态引用功能,但之前我每次使用,都会导致程序报错,甚至出现tomcat无法正常启动的情况 ...

  3. 持续集成环境(5)-Maven安装和配置

    在Jenkins集成服务器上,我们需要安装Maven来编译和打包项目. 安装Maven 1.下载Maven软件到jenkins服务器上 wget https://mirrors.aliyun.com/ ...

  4. 如何优化if--else

    1.现状代码 public interface IPay { void pay(); } package com.test.zyj.note.service.impl; import com.test ...

  5. docker基本操作 备忘

    docker 基本操作 通过镜像运行容器 - docker run -d -it -p 5555:5555 镜像名 启动容器,并将进入容器中的bash命令行 进入容器 - docker attach ...

  6. clion环境配置

    如果是学生:直接使用学校的邮箱,可以直接注册使用 环境配置:下载:https://sourceforge.net/projects/mingw-w64/

  7. Axios的相关应用

    Axios 的案例应用 要求利用axios实现之前利用AJAX实现的验证用户是否登录的案例 鉴于这两种语法的相似性,只需要在AJAX里面的注册界面里面的script标签里面包含的代码修改为如下代码即可 ...

  8. Python通过ssh登录实现报文监听

    Python自动化ssh登录目标主机,实现报文长度length 0监听,并根据反馈信息弹窗报警: 代码比较简陋,后续记得优化改进. #_*_coding:utf-8 _*_ #!/usr/bin/py ...

  9. IDEA-日志管理神器

    Grep Console-插件的好处就在于能使控制台输出日志时,可以直接修改插件中定义好的规则,也可以根据自己定义的规则,输出不同的颜色.这样就可以将错误信息标记成显眼的颜色,方便查看,提高bug寻找 ...

  10. Teamcenter_NX集成开发:使用NX、SOA连接Teamcenter

    最近工作中经常使用Teamcenter.NX集成开发的情况,因此在这里记录使用NX.SOA连接到Teamcenter的连接方式. 主要操作: 1-初始化UGMGR环境成功后就可以连接到Teamcent ...