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

978-7-115-48055-2
作者: 胡世杰
译者:
编辑: 陈冀康
分类: 存储

图书目录:

详情

本书首先从一个最简单的对象存储服务原型开始,讨论在原型中存在的问题并介绍对象存储服务中一些常见的概念以及设计理念,然后通过改变架构或添加功能的方式解决这些问题。这一迭代步骤将发生多次,最终我们会收获一个足够完善的对象存储服务。

图书摘要

版权信息

书名:分布式对象存储——原理、架构及Go语言实现

ISBN:978-7-115-48055-2

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    胡世杰

责任编辑 陈冀康

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


本书从云存储的需求出发讲述对象存储的原理,循序渐进地建立起一个分布式对象存储的架构,并且将软件实现出来。全书共8章,分别涉及对象存储简介、可扩展的分布式系统、元数据服务、数据校验和去重、数据冗余和即时修复、断点续传、数据压缩和数据维护等。本书选择用来实现分布式对象存储软件的编程语言是当前流行的Go语言。

本书适合从事云存储方面工作的工程师或架构师,也适合想要学习和实现分布式对象存储的读者。


早在几年前,云存储还只是存在于业界大佬们口中的一个概念,其应用场景仅供大公司使用。突飞猛进的网络技术似乎在一瞬间就把这个概念普及到千家万户,现在云存储已经是大家司空见惯的一个网络服务了。比如大家用的百度云盘、已经成为实质上的业界标准的亚马逊S3、微软的OneDrive、苹果公司的iCloud和谷歌的Google Cloud等。

现在市面上和云存储相关的图书本来就不多,而专门讲述对象存储实现的书就更是难得一见,且内容大多以OpenStack、Swift和Ceph这些已经较为成熟的开源软件的架构和实现为例。读者一开始就知道怎么做,然后解释为什么要这么做。

本书则另辟蹊径,完全从云存储的需求出发讲述对象存储的原理,循序渐进、从无到有地建立起一个分布式对象存储的架构,并且将软件实现出来。换句话说,本书首先介绍为什么要这么做,然后解释怎么做。

本书选择用来实现分布式对象存储软件的编程语言是Go,但并不是非它不可的。读者也可以在了解对象存储的原理之后选用其他的语言来实现。

在读完本书之后,每一位读者都将对对象存储服务这一概念有一个较为深入的理解,部分读者甚至能够实现自己的对象存储服务。

对象存储是云存储的一部分,它提供了云存储后端的存储服务。云存储是建立在对象存储之上的一个整体的解决方案,除了后端的存储服务之外,它还需要包括各种操作系统和平台上运行的客户端、身份认证、多种管理和监控功能等。

本书主要集中在对象存储的原理架构和实现上,对云存储其他组件也会有一定的介绍,但不会是本书的主要内容。

传统的高端服务器性能强劲、成本高昂,以前只有大公司用来搭建自己的私有存储。互联网生态下的云存储则用数量弥补质量,以大量低成本的普通PC服务器组成网络集群来提供服务。相比传统的高端服务器来说,同样价格下分布式存储提供的服务更好、性价比更高,且新节点的扩展以及坏旧节点的替换更为方便。

如果你是云存储方面的学者、工程师或架构师,那么本书适合你。

如果你是一位对云存储感兴趣的人或者是云存储的用户,那么光凭看这本书你可能无法实现一个自己的对象存储服务,但是在读完本书之后,你依然能够学到很多相关的知识。

对Go语言和HTTP/REST协议的了解可以帮助你实现并执行本书中的代码,但这不是必需的。本书对每一段代码都会有详细的解释来帮助读者了解其内容。即使对语言和协议一无所知的读者也能了解代码的含义和执行效果。

如果你是一位对云存储比较熟悉的读者,你可能已经了解对象存储服务的架构以及一些常见问题的成因,但这也不是阅读本书所必需的条件。本书会以提出问题并解决问题的方式介绍对象存储服务的架构设计。即便是对对象存储一窍不通的读者也可以在这个过程中亲眼见证对象存储服务的架构是如何一步步丰满起来的。

第1章,我们提出了一个单机版的对象存储原型系统,介绍了最简单的对象REST接口。

第2章,我们将这个原型系统进行了扩展,将它分拆成接口服务和数据服务,使得这些服务可以互相独立地提供服务功能,让我们的系统得以自由扩展。

第3章,我们又往系统中加入了元数据服务,用于保存描述对象的元数据,包括对象的名字、版本、大小、散列值等。有了元数据服务,我们就可以使得对象的名字和对象的内容解耦合。

第4章,我们实现对象数据的校验和去重,使得名字不同但内容相同的对象可以共享同一份存储实体,这样做可以降低对存储空间的要求。

第5章,为了增强数据的可靠性,我们提出了数据冗余的概念并实现了RS纠删码。我们在对象数据存取的过程中以流的形式进行编解码,可以在一定程度上修正对象数据的损坏。

第6章,为了战胜现实世界不良的网络环境,我们实现了断点续传。客户端在下载对象时自由指定下载数据的偏移量,也可以通过特殊的接口以分批的方式上传对象的数据。

第7章,我们介绍数据压缩。在大多数情况下,数据压缩都应该在客户端实现。但如果你需要设计一个通过浏览器就可以使用的对象存储系统,且你的大多数对象的数据都适合进行压缩,那么可以参考我们在本章实现的gzip数据压缩,进一步降低对存储空间和下载带宽的要求。

第8章,我们讨论了对象存储系统的数据维护,并实现了3个工具,它们可用于清理过期的对象数据和元数据,检查和修复当期的数据。

我们没有实现一个专门的客户端来配合对象存储系统,只是在部分章节中提到一个配套的客户端可以起到的作用。本书使用Linux下的curl命令作为我们的客户端进行功能测试,可以帮助我们更好地了解客户端和服务端之间发生的交互行为。但是一个美观UI的专门的客户端对用户来说会更加友好。

我们没有涉及用户管理,虽然用户管理是云存储系统的一个基本组成部分,但是这部分和其他系统的用户管理没什么区别,一个用户信息数据库就可以满足大多数要求,有兴趣的用户可以自行查阅相关书籍。

我们没有提到信息安全方面的内容,本书为了方便起见,使用的通信协议都是HTTP,而事实上一个云存储系统对外一定是使用HTTPS协议,服务端和客户端之间需要建立SSL的双向认证。除此之外,用户合法身份的授权和验证等功能通常都会有一个专门的身份认证系统来进行管理,而服务端客户端则可以通过JWT和身份认证系统打交道。

我们没有实现对象存储系统的监控。系统监控包括对日志的实时收集和分析,对系统KPI的收集和可视化等。我们在这里推荐的做法是使用Logstash收集和分析系统日志和KPI,记录在ElasticSearch中并用Kibana进行可视化。这些功能不涉及Go语言实现,而是通过各种工具的配置来进行。本书不展开讨论。

本书的代码使用Go语言实现,使用的Go编译器的版本是1.8.1,开发和运行环境是Ubuntu 16.04。

本书中所有Go语言代码实现都可以在github上找到,在Linux环境可以用git命令下载:

git clone https://github.com/stuarthu/go-implement-your-object- storage.git

github.com是一个在线的软件项目管理仓库,Ubuntu下的git客户端可以用apt-get下载安装:

sudo apt-get install git

编译Go代码需要运行Go编译器,读者可以在Go语言官网下载最新的Go编译器。

胡世杰,上海交通大学硕士,目前任七牛云技术专家,是私有云存储服务的负责人。他是分布式对象存储系统专家,在该领域拥有多年的架构、开发和部署经验,精通C/C++/Perl/Python/Ruby/Go等多种编程语言,熟悉ElasticSearch/RabbitMQ等开源软件。

除了自己写作,他还致力于技术书籍的翻译,是《JavaScript面向对象精要》《Python和HDF5大数据应用》《Python高性能编程》等著作的译者。

感谢我的妻子黄静和岳父黄雪春在我写书的日子里对我的支持,让我能够没有后顾之忧地写作。感谢人民邮电出版社的陈冀康编辑在本书写作和出版过程中的大力协助,感谢好友高博启发了我自己写作的念头,以及对本书推广所做的一切工作。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书提供如下资源:

本书所有示例源代码;

本书作者针对书中内容的配套视频讲解。

读者请通过本书封底的刮刮卡观看。也可通过异步社区“课程”频道订阅。

要获得以上配套资源,请在异步社区本书页面中点击 ,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。

如果您是学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


要理解对象存储,我们首先要来谈谈传统的网络存储。传统的网络存储主要有两类,分别是NAS和SAN。

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

SAN是Storage Area Network的简称。它和NAS的区别是SAN只提供了块存储,而把文件系统的抽象交给客户端来管理。SAN的客户端和服务端之间的协议有Fibre Channel、iSCSI、ATA over Ethernet(AoE)和HyperSCSI。对于客户端来说,SAN就是一块磁盘,可以对其格式化、创建文件系统并挂载。

NAS和SAN并不是完全对立的,现代的网络存储通常都是两者混合使用,可以同时提供文件级别的协议和块级别的协议。

介绍完传统的网络存储,那么对象存储跟它们又有什么区别呢?首先是对数据的管理方式不同。

对于网络文件系统来说,数据是以一个个文件的形式来管理的;对于块存储来说,数据是以数据块的形式来管理的,每个数据块有它自己的地址,但是没有额外的背景信息;对象存储则是以对象的方式来管理数据的,一个对象通常包含了3个部分:对象的数据、对象的元数据以及一个全局唯一的标识符(即对象的ID)。

对象的数据就是该对象中存储的数据本身。一个对象可以用来保存大量无结构的数据,比如一首歌、一张照片或是一个在线文档。

对象的元数据是对象的描述信息,为了和对象的数据本身区分开来,我们称其为元数据。比如某首歌的歌名、某张照片拍摄的时间、某个文档的大小等都属于描述信息,也就是元数据。对于对象的元数据,我们在第3章会详细介绍,这里不多展开。

对象的标识符用于引用该对象。和对象的名字不同,标识符具有全局唯一性。名字不具有这个特性,例如张三家的猫名字叫阿黄,李四家的狗名字也可以叫阿黄,名字为阿黄的对象可以有很多个。但若是用标识符来引用就只可能有一个。通常我们会用对象的散列值来做其标识符,关于散列值的详细介绍见第3章。

除了对数据的管理方式不同以外,对象存储跟网络存储访问数据的方式也不同。

网络文件系统的客户端通过NFS等网络协议访问某个远程服务器上存储的文件。块存储的客户端通过数据块的地址访问SAN上的数据块。对象存储则通过REST网络服务访问对象。

REST是Representational State Transfer的简称。REST网络服务通过标准HTTP服务对网络资源提供一套预先定义的无状态操作。在万维网刚兴起的时候,网络资源被定义为可以通过URL访问的文档或文件。现如今对于它的定义已经更为宽泛和抽象:网络上一切可以通过任何方式被标识、命名、引用或处理的东西都是一种网络资源。对于对象存储来说,对象当然就是一种网络资源,但除了对象本身以外,我们也需要提供一些其他的网络资源用来访问对象存储的各种功能,本书后续会一一介绍。

客户端向REST网络服务发起请求并接收响应,以确认网络资源发生了某种变化。HTTP预定义的请求方法(Request Method)通常包括且不限于GET、POST、PUT、DELETE等,它们分别对应不同的处理方式:GET方法在REST标准中通常用来获取某个网络资源;PUT通常用于创建或替换某个网络资源(注意,它跟POST的区别是POST一般不同于替换网络资源,如果该资源已经存在,POST通常会返回一个错误而不是覆盖它);POST通常用于创建某个网络资源,DELETE通常用于删除某个网络资源。

我们会在本书的后续章节看到对象存储的接口是如何使用这些HTTP请求方法的。

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

对象存储的另一大优势在于以更低的代价提供了数据冗余的能力。在分布式对象存储系统中一个或多个节点失效的情况下,对象依然可用,且大多数情况下客户都不会意识到有节点出了问题。传统网络存储对于数据冗余通常采用的方式是保留多个副本(一般至少3份,这样当其中一个副本出了错,我们还能用少数服从多数的方式解决争议),而对象存储的冗余效率则更高。我们会在第5章讨论数据冗余的问题。

本章将要实现的是一个单机版的对象存储原型,目的是让读者对我们讨论的对象存储有一个直观的了解。一个单机版的服务程序还称不上分布式服务,但是我们可以借此了解对象存储的接口,也就是说我们将了解客户端是如何通过REST接口上传和下载一个对象的,以及这个对象又是以什么样的形式被保存在服务器端的。从下一章开始,我们还将不断丰富架构和功能来适应各种新的需求。

在一台服务器上运行了一个HTTP服务提供的REST接口,该服务通过访问本地磁盘来进行对象的存取,见图1-1。

图1-1 单机版对象存储的架构

单机版的REST接口极其简单,只实现了对象的PUT和GET方法。

PUT /objects/<object_name>

请求正文(Request Body)

客户端通过PUT方法将一个对象上传至服务器,服务器则将该对象保存在本地磁盘上。

这里/objects/<object_name>是标识该对象网络资源的URL。URL是Uniform Resource Locator的简称,也就是一个网络地址,用于引用某个网络资源在网络上的位置。

在对象存储中,通常使用PUT方法来上传一个对象。

GET /objects/<object_name>

响应正文(Response Body)

客户端通过GET方法从服务器上下载一个对象,服务器在本地磁盘上查找并读取该对象,如果该对象不存在,则服务器返回HTTP错误代码404 Not Found。

在对象存储中,总是使用GET方法来下载一个对象。

我们可以用一张简单的图来概括PUT流程,见图1-2。

图1-2 单机版对象PUT流程

客户端的PUT请求提供了对象的名字<object_name>和对象的数据<content of object>,它们最终被保存在本地磁盘上的文件$STORAGE_ROOT/objects/<object_name>中。$STORAGE_ROOT环境变量保存着我们在本地磁盘上的存储根目录的名字。

对象GET流程见图1-3。

图1-3 单机版对象GET流程

客户端的GET请求提供了<objectname>,我们的服务进程从本地磁盘上的文件$STORAGE ROOT/objects/<object_name>中读取对象并将其写入HTTP响应正文。

流程介绍完了,接下来让我们去看一下如何用Go语言实现这样一个服务。

首先让我们来看一下main函数的实现。和大多数语言一样,Go语言也有一个main函数作为系统的入口点。在main函数中我们需要做的只是注册一个HTTP处理函数并开始监听端口,见例1-1。

例1-1 main函数

func main() {
       http.HandleFunc("/objects/", objects.Handler)
       log.Fatal(http.ListenAndServe(os.Getenv("LISTEN_ADDRESS"), nil))
}

http.HandleFunc的作用是注册HTTP处理函数objects.Handler,如果有客户端访问本机的HTTP服务且URL以“/objects/”开头,那么该请求将由objects.Handler负责处理。除此以外的HTTP请求会默认返回HTTP错误代码404 Not Found。

处理函数注册成功后,我们调用http.ListenAndServe正式开始监听端口,该端口由系统环境变量LISTEN_ADDRESS定义。正常情况下该函数永远不会返回,程序运行后会始终监听端口上的请求,除非我们发送信号中断进程。非正常情况下,该函数会将错误返回,此时log.Fatal会打印错误的信息并退出程序。

objects.Handler函数属于objects包,该包一共有3个函数,除了Handler以外还有put和get函数。在Go语言中,某个变量或函数名的首字母大写,则意味着该变量或函数可在包外部被引用;如果是首字母小写,则意味着该变量或函数名仅可在包内部被引用。Handler函数的首字母H大写就表明该函数可以在objects包外部被调用(被main包的main函数调用),而put和get函数则仅在objects包内部可见。Handler函数见例1-2。

例1-2 objects.Handler函数

func Handler(w http.ResponseWriter, r *http.Request) {
        m := r.Method
        if m == http.MethodPut {
                put(w, r)
                return
        }
        if m == http.MethodGet {
                get(w, r)
                return
        }
        w.WriteHeader(http.StatusMethodNotAllowed)
}

Handler有两个参数。w的类型是http.ResponseWriter,用于写入HTTP的响应。它的WriteHeader方法用于写HTTP响应的错误代码,它的Write方法则用于写HTTP响应的正文。r的类型是*http.Request,是一个指向http.Request结构体的指针,代表当前处理的HTTP的请求。它的Method成员变量记录了该HTTP请求的方法。

Handler函数会首先检查HTTP请求的方法:如果是PUT,则调用put函数;如果是GET,则调用get函数;两者皆否,则返回HTTP错误代码405 Method Not Allowed。

put函数负责处理HTTP的PUT请求,将PUT上来的对象存储在本地硬盘上,见例1-3。

例1-3 objects.put函数

func put(w http.ResponseWriter, r *http.Request) {
        f, e := os.Create(os.Getenv("STORAGE_ROOT") + "/objects/" +
                strings.Split(r.URL.EscapedPath(), "/")[2])
        if e != nil {
                log.Println(e)
                w.WriteHeader(http.StatusInternalServerError)
                return
        }
        defer f.Close()
        io.Copy(f, r.Body)
}

put函数首先获取URL中<object_name>的部分,r.URL成员变量记录了HTTP请求的URL,它的EscapedPath方法用于获取经过转义以后的路径部分,该路径的形式是/objects/<object_name>。

strings.Split函数的功能是用分隔符将一个字符串分割成多个字符串,它有两个输入变量,第一个输入变量是需要分割的字符串,第二个输入变量则是分隔符,分割后的结果以字符串的数组形式返回。在这里,strings.Split会将我们的URL路径拆分成3个字符串,分别是“”“objects”和<object_name>。所以数组的第三个元素就是<object_name>。

然后我们会调用os.Create在本地文件系统的根存储目录的objects子目录下创建一个同名文件f(根存储目录由系统环境变量STORAGE_ROOT定义。除了objects子目录以外,还有一些其他的子目录,我们还会在后续章节看到它们的用处)。如果创建失败则返回HTTP错误代码500;如果创建成功则将r.Body用io.Copy写入文件f。io.Copy接收两个参数,第一个参数是用于写入的io.Writer,在这里我们的文件f就是一个io.Writer,任何写入f的数据都会被写入f所代表的文件;io.Copy接收的第二个参数则是一个用于读取的io.Reader,在这里就是r.Body。http.Request的Body成员变量是一个io.Reader,用来读取HTTP请求的正文内容。

这里需要提醒一点:我们的实现代码默认用户提供的URL的<object_name>部分不能包含“/”,如果包含,则“/”后的部分将被丢弃。真实生产环境中的代码需要对用户的所有输入都进行严格的校验,如果用户输入的URL不符合我们的预期,则服务器必须返回一个错误的信息,而不是忽略。

细心的读者看到这里可能要问:那如果我的对象名字里就需要有一个“/”怎么办呢?别担心,任何对象名字在被放入URL之前都需要在客户端进行URL编码。在编码时,一些不适合放入URL的字符会被转义。比如说用户有一个对象名字是“C:/中文目录/a\b&c.txt”,编码后的名字是“C%3A%2F%E4%B8%AD%E6%96%87%E7%9B% AE%E5%BD%95%2Fa%5Cb%26c.txt”,其中的“/”被转义成“%2F”。然后客户端会以“/objects/ C%3A%2F%E4%B8%AD%E6%96%87%E7%9B%AE%E5%BD%95%2Fa% 5Cb%26c.txt”作为HTTP请求的URL访问我们的服务。我们在获取URL时使用的r.URL.EscapedPath方法得到的就是这个字符串。

get函数负责处理HTTP的GET请求,从本地硬盘上读取内容并将其作为HTTP的响应输出,见例1-4。

例1-4 objects.get函数

func get(w http.ResponseWriter, r *http.Request) {
        f, e := os.Open(os.Getenv("STORAGE_ROOT") + "/objects/" +
                strings.Split(r.URL.EscapedPath(), "/")[2])
        if e != nil {
                log.Println(e)
                w.WriteHeader(http.StatusNotFound)
                return
        }
        defer f.Close()
        io.Copy(w, f)
}

和put函数类似,get函数首先获取<object_name>并调用os.Open尝试打开本地文件系统根存储目录objects子目录中的同名文件f,如果打开失败,则返回HTTP错误代码404;如果打开成功,则用io.Copy将f的内容写入w,此时f作为读取内容的io.Reader,而w则作为写入的io.Writer。

你可能会觉得诧异,刚才我们介绍put的时候还说f是一个io.Writer,现在怎么又变成了io.Reader?事实上f本身的类型是*os.File,一个指向os.File结构体的指针。os.File这个结构体同时实现了io.Writer和io.Reader这两个接口(Go语言中称为interface)。在Go语言中,实现接口只需要实现该接口所要求的全部方法,io.Write接口只要求实现一个Write方法,而io.Read接口则只要求实现一个Read方法。os.File同时实现了Write和Read方法,于是它既是一个io.Writer也是一个io.Reader。http.ResponseWriter也是一个接口,这个接口除了实现了WriteHeader方法以外也实现了Write方法,所以它也是一个io.Write接口。

Go语言的实现介绍完了,接下来我们需要把程序运行起来,并进行功能测试来验证我们的实现。

我们需要先运行服务器程序。

$ LISTEN_ADDRESS=:12345 STORAGE_ROOT=/tmp go run server.go

别忘了在存储根目录/tmp下创建相应的objects子目录。

mkdir /tmp/objects

接下来我们用curl命令作为客户端来访问服务器,试图GET一个名为test的对象。

$ curl -v 10.29.102.172:12345/objects/test
* About to connect() to 10.29.102.172 port 12345 (#0)
*   Trying 10.29.102.172... connected
> GET /objects/test HTTP/1.1
> User-Agent: curl/7.22.0 (x86_64-pc-linux-gnu) libcurl/7.22.0 OpenSSL/1.0.1 zlib/1.2.3.4 libidn/1.23 librtmp/2.3
> Host: 10.29.102.172:12345
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Date: Mon, 26 Jun 2017 14:08:10 GMT
< Content-Length: 0
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host 10.29.102.172 left intact
* Closing connection #0

很好,服务器给出了预期的404错误,因为我们还从来没有PUT过一个叫test的对象。

那么,接下来我们PUT一个test对象。

$ curl -v 10.29.102.172:12345/objects/test -XPUT -d"this is a test object"
* About to connect() to 10.29.102.172 port 12345 (#0)
*   Trying 10.29.102.172... connected
> PUT /objects/test HTTP/1.1
> User-Agent: curl/7.22.0 (x86_64-pc-linux-gnu) libcurl/7.22.0 OpenSSL/1.0.1 zlib/1.2.3.4 libidn/1.23 librtmp/2.3
> Host: 10.29.102.172:12345
> Accept: */*
> Content-Length: 21
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 21out of 21 bytes
< HTTP/1.1 200 OK
< Date: Mon, 26 Jun 2017 14:10:34 GMT
< Content-Length: 0
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host 10.29.102.172 left intact
* Closing connection #0

我们用curl命令PUT了一个名为test的对象,该对象的内容为“this is a test object”。服务器返回“200 OK”表示PUT成功。接下来让我们再次GET这个test对象。

$ curl -v 10.29.102.172:12345/objects/test
* About to connect() to 10.29.102.172 port 12345 (#0)
*   Trying 10.29.102.172... connected
> GET /objects/test HTTP/1.1
> User-Agent: curl/7.22.0 (x86_64-pc-linux-gnu) libcurl/7.22.0 OpenSSL/ 1.0.1 zlib/1.2.3.4 libidn/1.23 librtmp/2.3
> Host: 10.29.102.172:12345
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Mon, 26 Jun 2017 14:12:44 GMT
< Content-Type: text/plain; charset=utf-8
< Transfer-Encoding: chunked
<
* Connection #0 to host 10.29.102.172 left intact
* Closing connection #0
this is a test object

太棒了!我们成功获取了之前PUT上去的那个对象的内容。

我们在本章实现了一个单机版的对象存储服务,它提供了对象的PUT和GET方法。当客户端以PUT方法访问我们的服务时,客户端会提供对象的名字和内容,我们的服务就可以把对象的内容以文件的形式存储在服务器的本地磁盘上;当客户端以GET方法访问我们的服务时,我们就可以从服务器的本地磁盘上读取文件的内容并将其作为HTTP的响应输出。

这样一个单机的对象存储服务离我们最终要实现的版本还很远,它只是一个热身,让我们熟悉一下使用的方式。我们最终的目标是要实现一个云版本的对象存储。那么,相比一个云版本的对象存储来说,目前的单机版最大的问题是什么呢?

答案是可扩展性!

分布式对象存储服务必须是可扩展的,当现有的服务器集群无法满足容量、吞吐量、时延等性能指标时,我们必须能够轻易扩展现有的服务器集群。在单机版的架构中,接口和数据存储被紧紧耦合在一起,服务器只能访问本地磁盘。当一台服务器无法满足日益增长的HTTP客户端请求数量时,我们将无法通过加入一个新的服务器来扩展集群,因为新的服务器无法访问旧服务器的磁盘。

在下一章,我们会把接口和数据存储解耦合,分离成专门的接口服务和数据服务。接口服务器可以自由访问任何一台数据服务器。当HTTP请求增长时,我们可以加入新的接口服务器,保持数据服务器不变。而当数据存储渐满或磁盘IO负载过高时,我们可以加入新的数据服务器,保持接口服务器不变。


相关图书

数据存储架构与技术
数据存储架构与技术
数据存储架构与技术(第2版)
数据存储架构与技术(第2版)
信息存储与管理(第二版):数字信息的存储、管理和保护
信息存储与管理(第二版):数字信息的存储、管理和保护

相关文章

相关课程