月度归档:2013年12月

Swift Object Storage

分布式存储系统 有几个重要功能 features:

1 集群管理
2 名字空间
3 分布与查找
在详细讨论之前,先引入几个基本概念。
node, drive , device, disk , backend 这几个出现在好多文档里面。它们都是一个意思。简称 dev. 即指一块磁盘,也就是 挂载在 /srv/node 下的目录。因为 这些 目录即挂载点分别对应着一个磁盘。当然也可以将一个物理磁盘划分为多个分区,挂载在/srv/node下的多个目录。或者是 /srv/[num]/node 目录下。注意与物理主机节点相区别,有时也会用Node。
partition ,指一致性哈希中vnode,也就是 虚结点,简称 part。
replica,副本,也就是 一份数据或对象的多份拷贝,但是各个拷贝的副本号不同。
1 集群管理
   先介绍一下,主机架构。swift集群中分为 proxy主机和 storage主机。当然一个主机也可以同时具有这两种角色。
   如下图:
          
注意 图中 的 Node 和上面 提到的不是一个意思,指的是 Host,即物理主机节点。
一个集群 可以 有 多个 proxy ,在 多个 proxy间 负载 ,可以使用 LVS 或 f5 等 各种 软硬件方式。
下面讨论 devices 的管理。
  swift 采用 分层 tier 管理所有的dev。
   在1.8.0 即 H版 中  加入region 概念。也就是 区域,来支持一个全球的swift集群。
   然后是 zone ,是 node 的一个集合,可以是 几台 物理主机,或 一个 机架 或 一个机房,甚至一个数据中心。
   然后是 物理主机 Host。
   最后是 物理设备 即 dev。
   如下所示: 来源 swift/swift/common/ring/utils.py
    Example:

region 1 + zone 1 + 192.168.101.1:6000 + device id 0
               |                     |                                        |
               |                     |                                        + device id 1
               |                     |                                        |
               |                     |                                        + device id 2
               |                     |
               |                     + 192.168.101.2:6000 + device id 3
               |                                                               |
               |                                                               + device id 4
               |                                                               |
               |                                                               + device id 5
               |
               + zone 2 + 192.168.102.1:6000 + device id 6
                                    |                                        |
                                    |                                        + device id 7
                                    |                                        |
                                    |                                        + device id 8
                                    |
                                    + 192.168.102.2:6000 + device id 9
                                                                              |
                                                                              + device id 10

region 2 + zone 1 + 192.168.201.1:6000 + device id 12
                                     |                                        |
                                     |                                        + device id 13
                                     |                                        |
                                     |                                        + device id 14
                                     |
                                     + 192.168.201.2:6000 + device id 15
                                                                               |
                                                                               + device id 16
                                                                               |
                                                                               + device id 17

从上面可以看到, 每一个 dev 都有 一个 全局(整个集群)唯一的ID。在 分配 part 的 replicas 时 会尽可能将 不同的region或zone上,官方文档 原文 (来源 github/CHANGELOG ):
The ring builder now supports as-unique-as-possible partition
      placement, unified balancing methods, and can work on more than one
      device at a time.
该变化 在 1.5.0 即 F版中,在 F版之前,同一个part的各个replica 不能在 一个 zone 中。因为 一个 dev只能在一个zone中,所以 不同的zone肯定意味着不同的device。
刚才 提到了 part 的 replica,part 也就是 vnode 是 虚结点。所有的数据 都存储在虚结点中,每个数据都有一个数据ID(通常是 HASH)。一个数据ID 被 映射到 一个 vnode 上,因为 每个 vnode 有多份,所以 每个数据也就有多份。此处 可以参见 源代码文件 swift/swift/common/ring/builder.py
 在 类 RingBuilder 有一个重要的属性 _replica2part2dev, 申明如下
                  # _replica2part2dev maps from replica number to partition number to

        # device id. So, for a three replica, 2**23 ring, it's an array of
# three 2**23 arrays of device ids (unsigned shorts). This can work a
# bit faster than the 2**23 array of triplet arrays of device ids in
# many circumstances. Making one big 2**23 * 3 array didn't seem to
# have any speed change; though you're welcome to try it again (it was
# a while ago, code-wise, when I last tried it).
self._replica2part2dev = None

  使用时,如下(文件同上):
                 dev_id = self._replica2part2dev[replica][part]
另外,replica 还可以是个大于1的小数。来源如下 类 RingBuilder:
  方法: __init__ 中
                 if replicas < 1:

            raise ValueError("replicas must be at least 1 (was %.6f)"
% (replicas,))

  方法: _adjust_replica2part2dev_size 中
                 “”

        Make sure that the lengths of the arrays in _replica2part2dev
are correct for the current value of self.replicas.

Example:
self.part_power = 8
self.replicas = 2.25

self._replica2part2dev will contain 3 arrays: the first 2 of
length 256 (2**8), and the last of length 64 (0.25 * 2**8).

Returns a 2-tuple: the first element is a list of (partition,
replicas) tuples indicating which replicas need to be
(re)assigned to devices, and the second element is a count of
how many replicas were removed.
"""

也就是 说 在 二维数组 _replica2part2dev 中 首先是 不同的 replica 的 part。然后是 part 与 dev 的映射。因为 replica 可能为小数,所以 每个 part的 replica 数是不一样的。准确的说,part号小的replica多。因为是用 列表保存的映射关系 也就是 数组下标是part号,值是dev的ID。下标从0开始的。
综上: 集群中共有 2^part_power 个 part,标号 为 0 ~ 2^part_power-1 。
            因为 replica的原因 part 与 dev 是一个 多对多的关系。也就是 一个 part 会保存到多个dev上。
            一个对象(数据)对应一个part,一对多的关系。
            part的数目是固定的,也就是 集群一旦建立是不能改变的。
            变化的是 dev,所以要改变part与dev的映射。详细 内容 参考 builder.py 文件。
附:根据 deploy_guide , 虚结点个数 最好为 未来最大物理磁盘设备数的100倍。如下所述:

The first step is to determine the number of partitions that will be in the ring. We recommend that there be a minimum of 100 partitions per drive to insure even distribution across the drives. A good starting point might be to figure out the maximum number of drives the cluster will contain, and then multiply by 100, and then round up to the nearest power of two.

For example, imagine we are building a cluster that will have no more than 5,000 drives. That would mean that we would have a total number of 500,000 partitions, which is pretty close to 2^19, rounded up.

It is also a good idea to keep the number of partitions small (relatively). The more partitions there are, the more work that has to be done by the replicators and other backend jobs and the more memory the rings consume in process. The goal is to find a good balance between small rings and maximum cluster size.

The next step is to determine the number of replicas to store of the data. Currently it is recommended to use 3 (as this is the only value that has been tested). The higher the number, the more storage that is used but the less likely you are to lose data.

It is also important to determine how many zones the cluster should have. It is recommended to start with a minimum of 5 zones. You can start with fewer, but our testing has shown that having at least five zones is optimal when failures occur. We also recommend trying to configure the zones at as high a level as possible to create as much isolation as possible. Some example things to take into consideration can include physical location, power availability, and network connectivity. For example, in a small cluster you might decide to split the zones up by cabinet, with each cabinet having its own power and network connectivity. The zone concept is very abstract, so feel free to use it in whatever way best isolates your data from failure. Zones are referenced by number, beginning with 1.

2 名字空间
    swift上有 account,container,object 三种类型的对象。
    有 记录 account , container,object 的 元数据文件,记录 object 内容的 数据文件。
    元数据文件 为 sqlite格式的db文件,数据文件 为 原始数据流保存的字节文件,.data结尾。
    data 文件 的 保存路径:
        dev_dirpath/dev/type_name/partition/suffix_path/name_hash/filename
       dev_dirpath 是 dev 挂载点的目录,一般 为 /srv/node ,写在配置文件 object-server.conf 中
       dev 挂载目录名,如 sdb1,sdb2 等, 在 用 swift-ring-builder 添加 device时 会用到
        type_name 即 类型名,上面提到的三种 data文件属于 object,故为 object
        partition 即 分区号,也就是 该 文件 被分到的  part。
                    因为 part的数目是2的次方,所以可以直接使用移位来计算。
                    代码 ( 文件 swift/swift/common/ring/ring.py):
                         def get_part(self, account, container=None, obj=None):

        """
Get the partition for an account/container/object.

:param account: account name
:param container: container name
:param obj: object name
:returns: the partition number
"
        Get the partition for an account/container/object.
        :param account: account name        :param container: container name        :param obj: object name        :returns: the partition number        """        key = hash_path(account, container, obj, raw_digest=True)        if time() > self._rtime:            self._reload()        part = struct.unpack_from('>I', key)[0>> self._part_shift        return part

       suffix_path 即 计算出来的 name_hash 的 后 3位。
       name_hash 即 hash_path 计算出来值
       filename 不同的 文件方法 不一样,对于 data文件 使用 时间戳加上后缀即扩展名。
                        对于元数据文件一般是name_hash加上 .db。
      例如:
/srv/node/sdb1/objects/109486/8fd/c7a68547b5aefe34c3e1371bcebbb8fd/1381454264.75078.data
/srv/node/sdb1/accounts/47305/82a/3323e66c399c5f6230030e493e6b082a/3323e66c399c5f6230030e493e6b082a.db
account,container,object的名字:
  account 长度 对于 UTF8 编码和URL Encode,不操过 256 字节,最好不要包含冒号。
  container 长度同上,不能包括 斜杠 即 /
  object 长度 对于 UTF8编码和 URL Encode 不操作 1024 字节,可以包含 斜杠 /
另外:系统在 大文件分片上传时,会自动创建一个 contianer,名字为 原container名加上  _segments 用来保存各分片对象。
因为 swift 是 对象 存储,所以是 不支持 目录的。不过 swift支持前缀查询,所以可以指定 一部分 前缀来 当作目录。如
   object        d/a/c.txt
   object        d/a/1/2.txt
   object        s/5
 上面三个 object 中 前两个 有同样的前缀 d/a/ ,故而 可以当作在同一个目录下。也就是 在 list object时 使用 prefix参数。
  格式: GET /<api version>/<account>/<container>[?parm=value]
所以:理论上限 为 8^256 个 account,8^512 个 container ,8^1536 个 object。
           每个 account 有 8^1270 个 object。
3  分布与查询
     节点分布 : 集群信息保存在 ring 文件中。ring 文件 维护了 part 到 节点的映射。
     文件分布 :  每个 account 都有一个自己独立的元数据文件。account中保存账户信息和 container 列表。
                           container 元数据文件 保存 object 列表。
                          元数据文件 保存了 与用户有关的,如名字等的元数据信息。
                          元数据文件内容 可以参考【牛皮糖】, 或 源文件 swift/swift/account/backend.py 旧版本在 swift/swift/common/db.py
    分布时,首先将 文件 根据 HASH 得到的 ID映射到 集群中的 part,然后根据part找到所有replica,再 以 replica号 和 part 号 从 ring 文件中 找到 dev_id 和 dev 信息。完成 数据文件 的 分布,并写入到 元数据文件中。
    查询时,直接 查找 元数据文件。而元数据文件的位置根据 分布算法 和 ring文件 即可找到。
   注意: swift 的 客户端 是没办法 计算 元数据文件的位置的。也就是 swift 客户端不能直接和 storage host 通信。
References:
swift 官方文档 github, deploy_guide , develop_guide , administrator_guide : http://docs.openstack.org
swiftstack :  http://swiftstack.com/
gholt(swift core developer): http://greg.brim.net/
launchpad/swift:     https://launchpad.net/swift
Google: google
others: www.chenshake.com ,http://www.openstack.cn  。。。
对以上组织或个人,以及其他没有写上的 表示感谢!
=======================================
swift 看了 好久, 前些时间 都是 零零碎碎的,其实现在也是,整理了些笔记,没有写 博客。由于每次查找资料 都有找到网上的博客。所以也整理一份。顺便自己也理一理。只要是 数据文件是如何 保存在 集群上的。一致性 和 容错方面没有提到。可以查看上面参考的资料。

 

肖微

2013 年 12 月 4 日

肖微

开源的世界,也有很多奇怪的事情。记得前些天面试还被问到异步编程,虽然用的不多,但是还是了解过libev,libevent ,libuv 这几个库。今天看微博,看到 nodejs 和 libuv 的 核心开发人员,最大贡献者之一:Ben Noordhuis 被迫 离开。原因 是 某个人修改了 源码的注释 从 him 改为 them,commit 注释如下。被 Ben 拒绝了 pull request 请求,理由是该提交带来的麻烦大于其价值,详细内容见下面链接。然后 项目管理者 joyent 不满意。。。然后 。。。。最后就这样(Ben 宣布离开这两个项目)了。nodejs发表公告表示感谢ben的贡献,遗憾 他的离开。

Removed use of gendered pronoun

具体文件修改内容 参见:https://github.com/joyent/libuv/pull/1015/files

Ben Noordhuis 本人的 说明:https://github.com/joyent/libuv/pull/1015#issuecomment-29568172

Nodejs 的 公告:http://blog.nodejs.org/2013/12/03/bnoordhuis-departure/

某个网站的讨论:https://news.ycombinator.com/item?id=6845286

我想这会伤很多开源贡献者的心吧。。。。

想起大学讲计算机仿真的老师的话,大意:开源无疑是未来主流,但是开源没有保障的服务,所以商业软件依然会存在下去。。。。 哎 , 就这样 因为 “ 性别” 的原因 。。。。