良好地调试你的部署和安装会花费你一些时间和精力。这里列出了一些能够提高OpenStack对象存储系统性能的注意事项。
有一些服务依赖于memcached来缓存一定的查询类型,比如认证令牌(auth tokens)、容器或者账户是否存在等。Swift并不缓存对象的真实数据。Memcached需要能够运行在任何具有可用的RAM和CPU的服务器上。在Rackspace,我们在代理服务器上运行memcached。Proxy-server.conf文件中的memcache_servers选项需要包含所有的memcached服务器。
时间可能是相对的,但是对于swift来说,时间是非常重要的。Swift使用时戳来决定对象的哪个版本是最新的。集群中每个服务器的时间都要同步地尽可能接近,这一点是非常重要的(尤其是对于代理服务器,但是一般来说所有的服务器都应该是时间同步的)。在Rackspace,我们使用本地NTP服务器的NTP服务来确保系统时间是尽可能接近的。通过监控来保证时间不会变化地太多。
大部分服务在设置里支持一个或并发的工作线程。这能使服务充分利用CPU的内核。一个好的起点是为代理服务器和存储服务设置并发数为CPU内核数的两倍。如果多于一个服务在共享服务器,那么大概需要做一些实验来找到最好的均衡。
在Rackspace,我们的代理服务器有两个四核心的处理器,提供给我们8个核,我们的测试显示在饱和的10G网络下,16个工作线程是一个非常好的均衡,提供了良好的CPU利用率。
我们的存储服务一起运行在相同的服务器上,这些服务器具有两个四核心的处理器,一共8个核。我们分别使用8个工作线程运行账户、容器和对象服务。后台的工作除了复制器的并发数是2以外,大部分的并发数是1。
上述的配置只是给出了一个建议,需要针对不同的配置做测试来保证最佳的CPU利用率、网络链接性和磁盘I/O。
我们推荐在swift中不使用RAID。
Swift中的负载是写繁忙的,随机IO访问很少。这种类型的负载在大多数奇偶检验RAID(比如RAID2-6)中性能很差。Rackspace做的测试结果表明在很重的负载下,整体RAID性能会下降到单个磁盘的级别。
此外,RAID中一个磁盘的故障会导致该节点很差的性能,直到RAID修复,这将花费很长的时间。Rackspace做了测试,使用24个2T的磁盘,测试结果显示在一个磁盘失效后RAID重建的时间是两周,这期间由于RAID运行在很低的级别上,节点的性能显著下降。这种性能的显著下降会在其他的集群产生潜在的涟漪效应(ripple effects)。
Swift在设计过程中是不大关注文件系统的类型的,它对文件系统只有一个要求,就要支持扩展属性(xattrs)。在我们对测试用例和硬件配置做了完整的测试后,XFS是最佳的全方位的选择。如果你决定使用其他的文件系统,我们推荐你先进行全面的测试。
如果你使用XFS,一些设置能够明显影响性能。在创建XFS分区时,我们推荐下述设置:
Mkfs.xfs –i size=1024 –f /dev/sda1
设置索引节点(inode)大小是重要的,XFS在inode中存储xattr数据。如果元数据太大了导致inode放不下,那么一个新的extent会被创建,这将会导致性能问题。将inode大小设置为1024字节提供了足够的空间来写默认的元数据加上一点动态余量(headroom)。我们不推荐在RAID上使用swift,但是如果你使用RAID的话,有一项很重要的事情就是要确保设置了合适的sunit和swidth,这样的话XFS就能够充分利用RAID。
在使用XFS时,我们推荐下述挂载选项:
Mount –t xfs –o noatime,nodiratime,nobarrier,logbufs=8 /dev/sda1 /srv/node/sda
在一个标准的swift安装中,所有的数据驱动盘都挂载到/srv/node下(就像在上述命令中看到的一样,以/srv/node/sda挂载/dev/sda1)。如果你选择在其他的目录挂载磁盘,确认在所有的服务器配置文件中设置了devices配置选项来指向正确的目录。
Rackspace当前在Ubuntu Server 10.04中运行swift,在这种情况下,下述更改是有用的。
下述设置在/etc/sysctl.conf文件中:
运行sudo sysctl –p来加载更新后的sysctl设置。
改变TIME_WAIT值,默认情况下,系统会保持一个60秒的端口来确保所有剩余的数据包被接收。由于我们负载很高,所创建的链接数很容易就将该端口耗尽了。由于我们能够控制网络,我们刻个改变这个设置。如果你不能控制网络,或者不会有高的负载,那么就不需要调整这些值。
另一个在性能低的系统上比较有用的调试参数是swift-init的-k N(或者--kill-wait N),这个参数能够保证足够的时间。它允许你改变swift等待进程结束的默认的时间(15s),你可以使用swift-init传递-run-dir标识来设置保留哪个PID。
Swift直接记录在系统日志中,每个服务都可以使用log_facility选项来配置使用哪个系统日志设施。我们推荐使用syslog-ng来路由日志到服务器本地特定的日志文件以及远程的日志收集服务器。
Rings决定数据放在集群的什么地方。账户数据库、容器数据库和独立的对象有不同ring,但是每个ring的工作方式都相同。Rings是外部管理的,也就是服务器自己不能更改ring,被修改过的ring通过其他的工具发送给这些服务器。
Ring使用路径的MD5哈希值中一些可配置的比特位作为指派设备的分区索引。比特的位数就是分区的指数,2的该指数次方就是分区数。把这个MD5哈希环进行分割允许集群中的其他部分一次处理一批items,这会比每次处理一个item或者处理整个集群更加有效或者至少复杂度更低。
另一个可配置的值是副本数,表明一个单独的ring由多少分区设备组成。对于给定的分区数,每个副本的设备都不在同一个zone中。Zone可以用来根据物理位置、分离的电力系统、分离的网络或其他可用来降低多副本同时不可用的特性来给设备分组。
4.6.8.1 使用ring builder管理ring
使用ring-builder来创建和管理rings。Ring-builder向设备分配分区,向一个gzipped、pickled文件中写入一个优化的Python结构来传送到服务器上。服务器进程不定期地检测这个文件的更改时间,按需重新加载他们的ring结构的内存拷贝。由于ring-builder管理对ring的变化的这样的方法,导致使用一个稍微过期的ring通常意味着分区的一个子集的三个副本之一是不正确的,不过这个很容易解决。
Ring-builder保留它自己的builder文件和ring的信息以及其他的将来创建ring需要的数据。保留这些builder文件的多个副本备份是很重要的。你可以在拷贝ring文件时将builder文件也拷贝到其他的服务器上。另一个方法是把builder文件上传到集群中。Builder文件全部丢失了意味着要创建一个全新的ring,几乎所有的分区都会被分配到不同的设备上,所以几乎所有的数据都必须被复制到新的位置。因此,builder文件丢失进行恢复是可行的,但是数据会在很长一段时间内完全不可达。
(1) ring的数据结构
ring的数据结构包括三个最上层的域:集群的设备列表、一个设备id列表的集合列表(设备id列表表明为设备分配的分区)、一个表明MD5哈希值便宜的比特位的整数(用来为hash计算分区)。
ring中的设备列表:
设备列表只对ring内部可见。每个条目是一个字典类型的变量
键值 |
类型 |
描述 |
Id |
Interger |
设备列表的索引 |
Zone |
Integer |
设备所在的区域 |
weight |
Float |
对其他设备的相对权重。这通常和该设备相对于其他设备来说的磁盘空间相一致。比如说一个1T的设备的权重是100.0,那么另一个2T的设备的权重可能是200。当随着时间流逝,一个设备中的数据比我们预期的多或者少时,这个设备的权重可以重新进行均衡。一个不错的平均权重是100.0,这在必要的情况下为我们提供了将来降低权重的灵活性。 |
ip |
string |
拥有该设备的服务器的IP地址 |
port |
int |
监听服务器进程所使用的TCP端口号,服务于对该设备的请求。 |
device |
string |
设备在磁盘上的名字,如sdb1 |
meta |
string |
用来存储设备附件信息的普通域。服务器进程不直接使用这个信息,但是在debug的时候是有用的。比如,安装的日期和时间或者硬件制造商可以存储在这里。 |
注意:设备列表可以包含空洞或者设置为None的索引,因为设备可能已经从集群中移除了。一般来讲,设备id不可重用。另外,一些设备可能会通过把权重设置为0.0来临时停用。
(2) 分区分配表
这是设备id的一个数组(‘l’)列表。最外面的列表对每一个副本包含一个数组(‘l’)。每个数组(‘l’)的长度等于ring中的分区数。数组(‘l’)中的每一个整数都是上述设备列表的一个索引。分区列表只对ring内部可见,叫做_replica2part2dev_id.
所以,创建一个分区的设备字典列表的python代码是:
devices = [self.devs[part2dev_id[partition]]for part2dev_id in self._replica2part2dev_id]
(3) 分区偏移值
分区偏移值只对ring内部可见,叫做_part_shift。这个值用来对一个MD5哈希移位,以便计算这个哈希的数据应该在哪个分区。这个过程中只是用哈希值的前四字节。比如,计算/account/container/object的分区,python代码是:partition = unpack_from(‘>l’,md5(‘/account/container/object’).digest())[0] >> self._part_shift
4.6.8.2 创建ring
创建ring的第一步是根据设备的权重,计算分配给每个设备的理想的分区数。比如,如果分区指数是20那么ring就会有1048576个分区,如果有相等权重的1000个设备,他们每个就期望1048.576个分区。然后这些设备就根据他们期望的分区数进行排序并且在整个初始化的过程中保持有序。
然后,ring builder就向期望最多分区数的设备分配每个分区的副本,限制条件是这个设备和该分区副本所在的其他设备不能在同一个zone。一旦分配完成,该设备期望的分区数会减少,然后被移动到设备列表重新排序后的位置,程序继续运行。
当在旧的ring基础上创建一个新ring时,每个设备期望的分区数会被重新计算。接下来,要被重新分配的分区会被聚集起来。任何分配给已移除的设备的分区都要变成未分配状态并被加入到重分配集合列表中。任何拥有超过他们期望分区数的设备将随机数量的分区设置为未分配并将这些分区加入到重分配集合列表中。最后,集合列表中分区使用和初始化分配一样的方法进行重分配。
每当一个分区的一个副本被重新分配了,重分配的时间会被记录下来。在进行重新分配时会将这个作为一个考虑的要素,所以在可配置的时间内,没有分区被分配两次。这个可配置的时间是ringbuilder内部知道的,叫做min_part_hours.如果分区副本所在的设备被删除了,那么这个限制会被忽略,因为删除一个设备的动作只发生在设备出现故障时,这时,除了做重分配外没有其他选择。
由于聚集用来重分配的分区的随机特性,上述过程并不总是完美重新均衡的。为了使ring更加均衡,重新均衡的过程会重复进行知道接近完美或者当均衡的结果提升度低于1%(表明我们可能达不到完美均衡,因为不均衡的zone或者最近有很多分区被移除了)。
4.6.8.3 ring设计的历史
ring 的代码经历了在到达现在这个已经稳定了一段时间的版本之前经历了许多次的迭代式的修改,如果有了新的想法产生,这个算法很可能会被改变或者甚至从根本上改变。这一部分将尝试去描述以前的尝试过的想法,尝试去解释为什么它们被废弃了。
"live ring "选项被考虑过,每一个服务器可以维护它们自己的环的拷贝,使用gossip协议传达它们改变的。这个方法由于太复杂而且在有效的时间内正确的编码是很容易出现错误的,所以被废弃了。一个bug可以很容易把坏数据通过gossip带到整个集群并且很难恢复。一个外部的管理环可以简化这个过程,允许当数据被送出服务器之前进行完整的校验,确保每一个服务器使用一个ring在相同的时间轴。这也意味着这些服务器不会花费很多的资源维护rings。
一对"ring server"选项被考虑使用过。一个是所有环的查询可以通过调用一个在单独服务器或者组服务器的服务,但是由于涉及到延迟就被废弃了。另一个更像当前的进程但是服务器可以提交改变的请求到ring server来构造一个新的环。然后送回到服务器上。由于项目时间限制和因为ring的改变在当前足够的罕见,手工的控足够了,所以被废除了。然而,缺乏快速自动的ring改变,意味着其他系统的组成部分不得不编码来处理设备不可用在几个小时内知道有人手动更新ring。
当前的rign进程虚节点的每一个副本独立的分配到设备。有个ring版本尝试使用三分之一的内存,一个虚节点的第一个副本直接分配,其他两个通过"walking"ring知道找到额外的设备在其他域来决定。这个被废弃因为失去了有多少副本对于一个已经给予的虚节点被移动在一次的控制。保持每一个副本独立允许只移动一个虚节点副本在给定的时窗(除了由于设备故障)。使用额外的内存被视作是换来,移动数据在集群中更少次。
另一个ring涉及尝试虚节点对于设备的分配不存储在一个大列表在内存,但是每个设备被分配一个哈希 或者集合。虚节点从这些数据的哈希和最近设备集合将决定副本被存在哪里。然而,获得每一个合理分配的数据,设备不得不由很多的集合和查找这些集合来找到副本的开始合计。最后,内存保存不是很好,和更好的处理能力被使用,随意这个想法被放弃了。
一个完整的没有虚节点的环也被尝试过,但是因为徐几点帮助系统的其他组成部分,特别是复制,复制可以再虚节点和其他副本的批处理中尝试和重试,而不是每个数据项独立尝试和重试。目录结构的哈希值可以计算和比较其他副本较少目录的遍历和网络的流量。
虚节点和独立分配虚节点副本也允许最佳平衡集群。其他最佳的策略倾向于给出+-10%变化在设备平衡,设备相等权重的+-15%设备权重的变化。当前的策略允许我们使用+-3%和+-8%的变化。
多种哈希算法被尝试过。SHA提供了更好的安全性,倒是ring不需要安全可靠的加盟而且SHA比较的慢。Murmur更快,但是MD5是内建的,并且哈希计算只是整个请求处理时间中很小的一部分。总之,一旦服务器不能维护rings而且只能做哈希查找,MD5被选择因为它的通用性,好的分布,和足够快的速度。
(这一小节不是我翻的,抄的http://my.oschina.net/zhouxingxing/blog/69956)
Account reaper在后台清除以删除账户的数据。
一个账户通过服务器的remove_storage_account XMLRPC调用被reseller标记为删除。这会将账户数据库(和副本)的account_stat表的状态列标记为DELETED,表明这个账户的数据之后应该被删除掉,没有滞留时间和反删除。在确实期望删除账户的数据时,reseller才会使用这个性质,调用remove_storage_account。
Account reaper在每个账户服务器运行,不定期地扫描服务器来查看是否有标记为删除的账户数据库。这个操作只会在主节点服务器的账户触发,所以有多个账户服务器时并不会同时做同样的工作。
一个账户的删除过程是相当直接的。对于账户的每个容器,先删除每个对象,然后删除这个容器。任何删除操作都要求失败不会停止所有的删除操作,但是它会最终导致整个进程的失败(比如,如果删除一个对象时超时,这个容器之后也不会被删除掉,所以账户也不会被删除掉)。即使删除过程中有失败,整个进程也会继续进行,所有进程不会被挂起并回收集群空间。Account reaper会一直尝试删除账户直到它最终是空的,然后数据库db_replicator中的回收过程会最终删除数据库文件。
4.6.9.1 Account Reaper的背景和历史
最开始,我们考虑的是一个简单的完全通过外部调用的删除一个账户的方法,它不能对系统做出其他变化。所有数据都必须通过公有的REST API以实际用户想要的方式被简单地删除掉。然而,不利的一面是这个方法需要使用代理服务器的资源并且纪律不必要的日志,并且,它可能需要一个或两个专用的服务器,就是为了发出删除请求。
接下来考虑了一个完全自底向上的方法,对象和容器服务器会不时地扫描他们的数据来检查账户是不是被删除了,如果账户被删除了,那么就删除对应的数据。有利的一面是回收的速度快并且不影响代理节点或日志记录,但是不利的一面是大约100%的扫描最终没有删除操作,这导致了大量的I/O负载。
接下来考虑到一个更加以容器服务器为中心的方法,账户服务器会标记所有要删除的容器,然后容器服务器会删除每个容器中的对象最后删除容器自己。优势就是对有很多容器的账户能够快速地回收,不利的是会有一个相当大的负载峰值。进程可以放缓来缓解这个可能的负载峰值,但是快速回收的优势就没了,而且剩下的是一个更负载的过程。并且,当大部分容器被标记为删除时,扫描所有的容器并不浪费时间。Db_replicator在它进行副本扫描时就可以做这个工作,但是它不得不生成新的线程来跟踪删除过程,这个复杂性貌似不是必要的。
最后,一个以账户服务器为中心的方法看似是最好的,就像上面描述的那样。