Posts Tagged ‘translation’

[译文]Cassandra实例

May 27th, 2010

原文: http://www.rackspacecloud.com/blog/2010/05/12/cassandra-by-example/#
原作者:Eric Evan 
原文发布日期:May 12, 2010
译者:王旭(http://wangxu.me/blog/ , @gnawux)
翻译时间:2010年5月15,25,26日

近来 Cassandra 备受瞩目,很多人正在评估是否可以应用 Cassandra。由于这些人更多的追求速度,相应的,我们的文档就过于粗浅了。这些文章中,最差的是为有关系数据库基础的人解释Cassandra数据模型的那些。

Cassandra 数据模型实际和传统的数据库差异非常大,足够让人眩晕,而且很多误解都需要修正。

有些人把这个数据模型描述成存放map的map,或对于super column的场景,是存放map的map的map。这些解释经常用类似 JSON 标记的视觉辅助展示方法来进行佐证。其他人则把列族看做是系数表,还有人把列族看作是存放列对象的集合容器。甚至有人有时把列看走势三元组。我觉得所有这些解释都不够好。

问题在于很难去用类比的方法来确切解释一个新的东西,而且如果比较的不准确的话常常把人搞糊涂。我仍然期望有人能解释清楚这个数据模型,但同时我觉得确切的例子可能更容易说明白一些。

Twitter

尽管 Twitter 本身就是 Cassandra 的一个实际的应用场景,它仍然是一个不错的教学实例,因为它众所周知而且易于抽象。在例子中,和很多站点一样,每个用户都有一份用户数据(显示名称、密码、email等),这些信息链接到朋友(译注:用户follow的人)和 follower(译注:follow用户的人)。此外,如果没有那些短 tweets 的话也就不是 twitter 了,tweet每条140个字符,它们都关联着诸如时间戳和惟一的id这样的元数据,这个id我们可以从URL里看到。

现在我们在一个关系数据库里来直接进行建模,我们首先需要一个表来存放用户。

CREATE TABLE user (
    id INTEGER PRIMARY KEY,
    username VARCHAR(64),
    password VARCHAR(64)
);

我们还需要两张表来存储一对多的follow关系。

CREATE TABLE followers (

    user INTEGER REFERENCES user(id),

    follower INTEGER REFERENCES user(id)

);

 
CREATE TABLE following (

    user INTEGER REFERENCES user(id),

    followed INTEGER REFERENCES user(id)

);

显然,我们还需要表来存储tweets。

CREATE TABLE tweets (
    id INTEGER,
    user INTEGER REFERENCES user(id),
    body VARCHAR(140),
    timestamp TIMESTAMP
);

由于仅仅是个例子,我已经极大简化了情况,但仅仅是这个极度简化的模型,也还有很多需要做的工作。例如,要以可行的方法达到达到数据归一化就需要一个外部键值约束,而因为我们需要从多张表join信息,我们需要对任意值建索引,以保证高效。

但是让一个分布式系统正常工作相当有挑战性,几乎不可能不做任何折衷。对Cassandra来说也是如此,而且这也是为什么上述数据模型对我们来说是无法工作的的原因。对于入门者,没有可供参考的完整性,缺乏次索引使得join很难进行,所以,你必须反归一化。另一方面,你被迫思考你要进行的查询的方式和期望结果,因为这差不多就是数据模型看起来的样子。

Twissandra

那么如何把上述模型翻译到Cassandra中呢?十分幸运,我们只需要看看 Twissandra,这是 Eric Florenzano 写的一个 Twitter 的简化版克隆,用作例子。那么让我们来使用 Twitter 和 Twissandra 作为例子来看看 Cassandra 的数据模型是如何的。

Schema

Cassandra 是一种无 schema 的数据存储方式,但为你的应用做一些特定的配置还是必要的。Twissandra 给出了一个可以工作的 Cassandra 配置,不过研究一下关于数据模型方面的配置还是物有所值的。

Keyspaces

Keyspaces 是 Cassandra 中最顶层的命名空间。在未来版本的 Cassandra 中,将可以动态创建 keyspace,正如在 RDBMS 中创建数据库一样,但是对于 0.6 和以前的版本,这些都在主配置文件中定义,如:

<Keyspaces>
  <Keyspace Name="Twissandra">
  ...
  </Keyspace>
</Keyspaces>
Column Families

对于每个 keyspace,都可以有一个或多个列族。列族是用于关联类型相近的记录的命名空间。Cassandra 在写操作时,在一个列族内部允许有记录级的原子性,对它们进行查询非常高效。这些特性十分重要,在进行你的数据建模前必须记牢,它们会在下面讨论到。

和keyspace类似,列族也在主配置文件中定义,虽然在将来的版本中你将可以在运行时创建列族,正像在RDBMS中创建表一样。

<Keyspaces>
  <Keyspace Name="Twissandra">
    <ColumnFamily CompareWith="UTF8Type"  Name="User"/>
    <ColumnFamily CompareWith="BytesType" Name="Username"/>
    <ColumnFamily CompareWith="BytesType" Name="Friends"/>
    <ColumnFamily CompareWith="BytesType" Name="Followers"/>
    <ColumnFamily CompareWith="UTF8Type"  Name="Tweet"/>
    <ColumnFamily CompareWith="LongType"  Name="Userline"/>
    <ColumnFamily CompareWith="LongType"  Name="Timeline"/>
  </Keyspace>
</Keyspaces>

需要指出的是,上面的配置片段中,指定名字的时候同时指定了一个比较者类型。这凸显了 Cassandra 和传统数据库的又一个重大不同,记录按照设计的顺序存储,在之后不能轻易改变。

这些列族都是什么?

一下子看所有的七个Twissandra列族是干什么的可能不那么直观,所以,我们来逐个仔细看一下:

  • User

User用于存储用户信息,大致相当于上面描述的用户表。列族中的每条记录以UUID为键值,并包含用户名和密码列。

  • Username

在User列族中查询一个用户需要知道用户的键值,但从用户名怎么找到这个UUID键值呢?在上面描述的SQL关系数据库里的话,我们就在User表里来一个匹配用户名的SELECT语句(WHERE username = ‘jericevans’)就行了。但这对于Cassandra来说却不可能。

首先,关系数据库可以顺序地扫描全表来进行这样一个 SELECT,但由于记录是基于键值分布在 Cassandra 集群中的,这个匹配将可能会在多个节点上进行,可能是很多节点。而且,即使是数据就在一个节点上,仍然有一个原因会让这一操作远没有关系数据库效率高,因为关系数据库可以对username列有索引。前面提到过,Cassandra是不支持第二索引的。

解决方案就是,建立一个我们自己的反向索引,进行用户名到UUID键值的映射,这就是Username列族的用途。

  • Friends
  • Followers

Friends 和 Follower 列族可以回答这些问题:用户X follow了哪些人?谁follow了用户X?这两个列族的键值都是这个唯一的用户ID,其中包含了哪些有follow关系的用户以及它们创建的时间。

  • Tweet

Tweet 列族用于存放所有的tweets。这个列族以每个 tweet 的 UUID为键值,还包含了用户id,tweet内容以及tweet时间这些列。

  • Userline

这是属于每个用户的时间线。记录的键值是用户的ID,其他的列中,包含有一个数字时间戳到Tweet列族中的tweet ID的映射。

  • Timeline

最后,Timeline列族类似于Userline,只是这里存储着每个用户的朋友的tweet的时间线视图。

有了上面这些列祖,现在我们可以看一些常用的操作都是如何发生的。

把这些列族放在一起来试一下

添加一个新用户

首先,新用户需要一个方法来注册一个账户,当他们注册的时候,组要将他们添加到Cassandra数据库中去。对于Twissandra,我们来看看里面的内容:

username = 'jericevans'
password = '**********'
useruuid = str(uuid())   
columns = {'id': useruuid, 'username': username, 'password': password}   
USER.insert(useruuid, columns)
USERNAME.insert(username, {'id': useruuid})

Twissandra是用Python写成的,使用 Pycassa 作为访问 Cassandra的客户端,上述大写的 USER 和 USERNAME 是 pycassa.ColumnFamily 的实例,它们需要在使用之前的某个位置被分别初始化。

这里说明一下,这不是从 Twissandra 里原样摘出来的。我让他们更加简单而且是自包含的。比如,在上面的例子中,如果没有对用户名和密码的赋值的话,可能不那么好理解,不过一个 web 应用只能从用户注册表单里得到这些内容。

从这个例子中回来,有两个不同的 Cassandra 写操作(insert()),第一个创建了一个用户列族,另一个更新了用户名到用户 UUID 键值的反向映射表。在两个例子中,参数都是用于查找记录的键值,以及包含列名和值的map。

Following 一个朋友
frienduuid = 'a4a70900-24e1-11df-8924-001ff3591711'   
FRIENDS.insert(useruuid, {frienduuid: time.time()})
FOLLOWERS.insert(frienduuid, {useruuid: time.time()})

这里我们再来两个不同的insert()操作,这次是加入一个用户到我们的朋友列表,并加入反向关系:给被 follow 用户添加一个 follower。

发出Tweet
tweetuuid = str(uuid())
body = '@ericflo thanks for Twissandra, it helps!'
timestamp = long(time.time() * 1e6)   
columns = {'id': tweetuuid, 'user_id': useruuid, 'body': body, '_ts': timestamp}
TWEET.insert(tweetuuid, columns)   
columns = {struct.pack('&gt;d', timestamp: tweetuuid}
USERLINE.insert(useruuid, columns)   
TIMELINE.insert(useruuid, columns)
for otheruuid in FOLLOWERS.get(useruuid, 5000):
    TIMELINE.insert(otheruuid, columns)

要存储一条新的tweet,我们需要使用一个新的UUID作为键值,在 Tweet列族创建一个记录,其中的列包含作者的用户ID,创建的时间,当然还有tweet的文本内容本身。

此外,用户的 Userline 中也要加入tweet的时间和它的id。如果这是用户的第一条tweet的话,这个insert()会产生一条新的纪录,后面的只是为这条记录添加新列。

最后要给发出tweet的用户和其他follower的 timeline 列族添加这条tweet的ID和时间。

值得注意的一件事是,这里,时间戳使用的是64位长整型变量,而当它成为一个列的名字的时候,它会被打包为网络字节序的二进制值。这是因为Userline和Timeline列族使用了一个LongType Comparator,允许我们使用数值区间指定查找指定范围,所以它们被按照数值来存放起来。

接收一个用户的 tweets
timeline = USERLINE.get(useruuid, column_reversed=True)
tweets = TWEET.multiget(timeline.values())

接收一个用户的tweet,首先从Userline获取tweet ID的一个列表,然后从Tweet列族通过multiget()方法莱读取这些tweet。得到的结果将是通过着数值表示的时间戳逆序排列的,因为Userline使用了LongTyper comparator,并且reversed设置为了True。

获取一个用户的时间线
start = request.GET.get('start')
limit = NUM_PER_PAGE   
timeline = TIMELINE.get(useruuid, column_start=start,
column_count=limit, column_reversed=True)
tweets = TWEET.multiget(timeline.values())

和上一个例子类似,这次是从 Timeline 读取 tweet ID,不过这次我们还使用了 start 和 limit 来控制读取列的范围。这样有助于输出结果的分页。

那么,下一步呢?

希望这足够提供给你一个大致的概念。重复一下,我从代码中提取了一些例子,为了简明起见,略去了一些操作,所以现在可能是 check out 出 Twissandra 的源代码并进行下一步深入研究的好时候了。有很多功能,诸如 retweet 和 lists,都还空着没有实现,可以作为一个练习的起点。如果你已经熟悉 Python 和 Django 的话,那你可以考虑实现一下这些方法。

Cassandra 的 wiki 包含了大量的信息,而且还在不断增多,还包括一个实时更新的其他人贡献的文章与幻灯片的列表。

如果你喜欢IRC的话,你可以加入 irc.freenode.net 的 #cassandra 频道,来和那里的人聊天,他们总是热衷于提供帮助和回答问题。如果你更青睐 email 的话,cassandra-user 邮件列表上也有很多可以提供帮助的人。

[译文]理解Cassandra源代码

May 13th, 2010

原文: http://prettyprint.me/2010/05/02/understanding-cassandra-code-base/
原作者:Ran Tavory 
原文发布日期:May 2nd, 2010
译者:王旭(http://wangxu.me/blog/ , @gnawux)
翻译时间:2010年5月12日

最近我为 cassandra 添加了一些小特性,于是我花了些时间来更仔细地考察了这个系统的内部设计。诸如 embedded service 这样的特性实际上并不需要对源代码和设计的深入理解,但其他特性,比如 truncate 就需要对系统中使用的不同算法有深入的了解,如写操作时如何进行的,读操作时如何进行的,值时如何删除的(提示:他们没有……)等等。

源代码虽然不是非常长,有大约91136行,但非常密级,而且有很多算法,所以直接读这些代码对我来说并不是非常简单。我是用如下的外科手段来进行行数计数的($ cassandra/trunk $ find * -name *.java -type f -exec cat {} \;|wc -l

我写这篇文章希望可以帮助其他人能更快地阅读这些代码。我不会介绍那些基础信息,比如“什么是Cassandra”,如何部署,如何检出代码,如何编译,如何下载thrift等。我也不想介绍算法最复杂的部分,比如ae-service使用的merkle-tree如何 ,Cassandra 中不同部分都是用的 bloom filter 是什么、如何工作,以及 gossip 是如何使用的。我不认为我适于解释所有这些问题,而且在cassandra的开发者wiki上也已经介绍这些了。我要写的就是我学习cassandra的途径,以及我在这个过程中学到了什么。我没有在其他地方发现过类似文档(当我完全完成的时候,可能我会把这些写入wiki),所以我觉得这对我下次再深入新的源代码会非常有用。

最后是一个免责声明:这里仅仅是我对系统如何工作的个人理解,它们是不完整、不确切的,特此警告。注意我也只是在学习,或多或少也是Cassandra菜鸟。也请注意,Cassandra 是一个运动目标,它一直在快速开发者,任何一个代码的快照或早或晚都会发生些变化。在本文写作的时候,官方版本是 0.6.1,不过我工作在 trunk 上,这个分支将来会成为 0.7.0。

这里是我所采取的几个步骤以及我学到的东西的一个描述。

下载,配置,运行…

首先你需要下载代码并运行单元测试。如果你使用 eclipes,IDEA,netbeans,vi,emacs等等这些的话,可能还需要配置一下。这非常简单,这里有更多介绍。

阅读

接下来你需要读一些背景材料,这依赖于你想搞哪个部分。我希望理解读操作、写操作以及值如何删除,所以我把下面每个文档都看了差不多五遍。没错,每个五遍。它们包含了大量的信息,我发现每次读我都能被更多的一些细节所吸引。我先读过这些文档,然后读源代码,确定我理解了算法如何在类和方法中实现,然后再读文档,然后再读源代码,读单元测试(并用debugger运行它们)等等。这是这些文档:

http://wiki.apache.org/cassandra/ArchitectureInternals

SEDA paper

http://wiki.apache.org/cassandra/HintedHandoff

http://wiki.apache.org/cassandra/ArchitectureAntiEntropy

http://wiki.apache.org/cassandra/ArchitectureSSTable

http://wiki.apache.org/cassandra/ArchitectureCommitLog

http://wiki.apache.org/cassandra/DistributedDeletes

我还看了 Google BigTable 的论文和让人着迷的亚马逊的 Dynamo 的论文,不过这都是很久以前的事情了。它们是很好的背景材料,不过对于理解实际代码并不是必须的。

好了,读完所有这些文档,我开始知道能做什么、如何做了,但我感觉我还没有到达能写新特性的阶段。在读代码几次之后,我发现我有点晕了,还是不了解诸如“值到底是不是真的被删除了”,那个累负责哪个功能,有几个Stage,Stage之间的数据流是什么样的,还有“如何标记整个列族为删除”,这是我在truncate操作中真正想做的。

Stages

Cassandra 的操作使用的并发模型在 SEDA 论文中有介绍。这个并发模型大致是这样的,和很多其他兵法系统不同,一个操作,比如一个写操作,并不是在同一个线程中开始和结束的。相反,一个操作在一个线程中开始,之后把操作(异步)交给了另一个线程,然后再传递到下一个进程,直至完成。事实上,操作并不是在线程间流转,而是在stage间流转。操作从一个stage转向另一个。每个stage都和一个线程池相关联,这个线程池在方便的时候来执行这个操作。一些操作是 IO bound 的(译注:这里应该是CPU吧,猜的),另一些则受限于磁盘或网络,所谓“方便”取决于资源的可用性。SEDA 论文把这个过程解释得非常好(很好的文章,值得一读),简单地说你从中得到的是更高级别的并发性和更好的资源管理,资源包含 CPU、磁盘、网络等。

所以,要理解 Cassandra 的数据流,你首先需要理解 SEDA。然后你需要了解 Cassandra 中有哪些 Stage,以及这些 stage 之间数据时如何流动的。

十分幸运,作为一个起点,StageManager 类中包含了一个不完整 stage 列表:

public final static String READ_STAGE = "ROW-READ-STAGE";
public final static String MUTATION_STAGE = "ROW-MUTATION-STAGE";
public final static String STREAM_STAGE = "STREAM-STAGE";
public final static String GOSSIP_STAGE = "GS";
public static final String RESPONSE_STAGE = "RESPONSE-STAGE";
public final static String AE_SERVICE_STAGE = "AE-SERVICE-STAGE";
private static final String LOADBALANCE_STAGE = "LOAD-BALANCER-STAGE";

 

我就不具体介绍每个 stage 都负责什么了(因为我也不知道……)但我可以说大致说,ROW-READ-STAGE 在读操作中,ROW-MUTATION-STAGE 参与了写和删除操作,而 AE-SERVICE-STAGE 负责 anti-entropy (译注:整理?不知道怎么确切用中文表达了)。这不是一个完整的 stage 列表,根据你感兴趣的代码路径,用这个方法,你可以找到更多。比如,查看文件ColumnFamilyStore你可以找到更多的stage,如FLUSH-SORTER-POOL, FLUSH-WRITER-POOL 和 MEMTABLE-POST-FLUSHER。在 Cassandra 中,stage 由 ExecutorService 的实例来唯一标识,这差不多是一个线程池,他们有全大写的名字,如 MEMTABLE-POST-FLUSHER。

我画了一张混有类和stage的图来便于理解。这不是合法的UML,但我觉得这对于了解数据在系统中如何流动是个很好的方法。这不是全部类和stage的完整的图示,仅仅是我感兴趣的一部分。

cassandra-1 
[SSTableTracker], [ColumnFamilyStore]->[Memtable (memtable_)], [CommitLog|CommitLogExecutor], [DeletionService|FILEUTILS-DELETE-POOL], [StorageLoadBalancer| lb_:LB-OPERATIONS; lbOperations_:LB-TARGET], [StorageService| consistencyManager_:CONSISTENCY-MANAGER], [StageManager| READ_STAGE; MUTATION_STAGE; STREAM_STAGE; GOSSIP_STAGE; RESPONSE_STAGE; AE_SERVICE_STAGE; LOADBALANCE_STAGE; MIGRATION_STAGE]">yUML source

Debugging

可以使用一个 debugger 来读代码,运行一个单元测试是了解事情如何工作的非常棒的方法。我不是一个debugger的铁杆粉丝,但是他们有一个可取之处就是通过单步执行单元测试来学习新代码。所以我所做的证实单步执行代码中的单步执行。这非常酷。我还运行了 Hector 的单元测试,它使用 thrift 接口并运行一个嵌入的 cassandra 服务器,这个方法一针见血、界面友好而且还能学到更多东西。

类图

接下来我所做的是使用一个工具来从已有代码中提取类图。这不是非常有用。

好,我使用的工具不是很棒,但这不是最关键的问题,问题是 cassandra 的代码书写方法使得类图对于理解代码作用很有限。UML 类图对于面向对象设计非常有效。类图的必要性在于列出类、成员以及它们的关系。比如类 A 是类 B 的一个列表,这样通过 UML 类图可以看出 A 是 B 的聚合,而且仅仅通过类图就可以学到很多。比如一架飞机有很多乘客。

Cassandra 是一个拥有坚实算法后台和优秀性能的系统,但是,老实说,依我之见,从好的面向对象实践的视角来看,它可不是一个很好的研究案例……它的类包含很多静态方法和成员,而且在很多地方,你可以看到一个类调用另一个类的静态方法。纯粹的C风格,所以我发现类图尽管从类的可视化以及类之间的关系方面有些帮助,但并不是非常有用。

我放弃了类图,继续进入下一种兔——序列图。

序列图

序列图非常适于实体之间的交互的抽象和可视化。这里,一个实例可能是一个类,一个 STAGE 或一个 thrift 客户端。很幸运,使用序列图,你不必太专注于序列图里实体的类型,你只需要把他们都表示为 actor 就行了(至少我觉得这么做就够了,希望UML大神们原谅)。

下面的图通过运行 Hector 的单元测试并使用一个(单节点)嵌入式 Cassandra 服务器得到。这个序列图并不很通用,它仅仅描述了一种可能的执行路径,而实际上可能有很多种,但我尼克让它们更简单一些,尽管有点不太精确。

我使用了一个简单的在线序列图编辑器(http://www.websequencediagrams.com)来生成他们。

读操作:

cassandra-2

写操作:

cassandra-3

Table is a Keyspace

最后提示:作为 Cassandra 的用户我应该使用 Keyspace, ColumnFamily, Column 这些名词。不过,代码中使用了 Table 这个名词。啥是 Table 呢?……原来,Table实际上就是 Keyspace…… 就是一个提示,仅此而已。

研究代码是一项艰巨而有成就感的工作,我希望这篇文章帮助你也有个好的起点,快点跑起来。

HBase vs Cassandra: 我们迁移系统的原因

March 25th, 2010

原文: http://ria101.wordpress.com/2010/02/24/hbase-vs-cassandra-why-we-moved/
原作者:Dominic Williams
原文发布日期:February 24, 2010 at 7:27 pm
译者:王旭(http://wangxu.me/blog/ , @gnawux)
翻译时间:2010年3月21-25日

我的团队近来正在忙于一个全新的产品——即将发布的网络游戏 www.FightMyMonster.com。这让我们得以奢侈地去构建一个全新的 NOSQL 数据库,也就是说,我们可以把恐怖的 MySQL sharding 和昂贵的可伸缩性抛在脑后了。最近有很多人一直在问,为什么我们要把注意力从 HBase 上转移到 Cassandra 上去。我确认,确实有这样的变化,实际上我们基本上已经把代码移植到了 Cassandra 上了,这里我将给出解释。

为了那些不熟悉 NOSQL 的读者,后面的其他文章中,我会介绍为什么我们将会在未来几年中看到地震式的从 SQL 到 NOSQL 的迁移,这正和向云计算的迁移一样重要。后面的文章还会尝试解释为什么我认为 NOSQL 可能会是贵公司的正确选择。不过本文我只是解释我们选择 Cassandra 作为我们的 NOSQL 解决方案的选择。

免责声明——如果你正在寻找一个捷径来决定你的系统选择,你必须要明白,这可不是一个详尽而严格的比较,它只是概述了另一个初创团队在有限时间和资源的情况下的逻辑。

Cassandra 的血统是否预言了它的未来

我最喜欢的一个工程师们用来找 bug 的谒语是“广度优先而非深度优先”。这可以可能对那些解决技术细节的人来说很恼人,因为它暗示着如果他们只是看看的话,解决方法就会简单很多(忠告:只对那些能够原谅你的同事说这个)。我造出这个谒语的原因在于,我发现,软件问题中,如果我们强迫我们自己在进入某行代码的细节层面之前,先去看看那些高层次的考虑的话,可以节省大量时间。

所以,在谈论技术之前,我在做 HBase 和 Cassandra 之间的选择问题上先应用一下我的箴言。我们选择切换的技术结论可能已经可以预测了:Hbase和Cassandra有着迥异的血统和基因,而我认为这会影响到他们对我们的业务的适用性。

严格的说,Hbase 和它的支持系统源于著名的 Google BigTable 和 Google 文件系统设计(GFS 的论文发于 2003 年,BigTable 的论文发于 2006 年)。而 Cassandra 则是最近 Facebook 的数据库系统的开源分支,她在实现了 BigTable 的数据模型的同时,使用了基于 Amazon 的 Dynamo 的系统架构来存储数据(实际上,Cassandra 的最初开发工作就是由两位从 Amazon 跳槽到 Facebook 的 Dynamo 工程师完成的)。

在我看来,这些不同的历史也导致Hbase更加适合于数据仓库、大型数据的处理和分析(如进行Web页面的索引等),而 Cassandra 则更适合于实时事务处理和提供交互型数据。要进行系统研究来证明这个观点超出了本文的范畴,但我相信你在考虑数据库的时候总能发现这个差异的存在。

注意:如果你在寻找一个简单的证明,你可以通过主要 committer 的关注点来进行验证:大部分 HBase 的 committer 都为 Bing 工作(M$ 去年收购了他们的搜索公司,并允许他们在数月之后继续提交开源代码)。与之对应,Cassandra 的主要 committer 来自 Rackspace,用来可以自由获得的支持先进的通用的 NOSQL 的解决方案,用来和 Google, Yahoo, Amazon EC2 等提供的那些锁定在专有的 NOSQL 系统的方案相抗衡。

Malcolm Gladwell 会说只是根据这些背景的不同就可以简单地选择了 Cassandra。不过这是小马过河的问题。但当然,闭着眼睛就进行一个商业选择是相当困难的……

哪个 NOSQL数据库风头更劲?

另一个说服我们转向 Cassandra 的原因是我们社区中的大风向。如你所知,软件平台行业里,大者恒大——那些被普遍看好的平台,会有更多人聚集在这个平台周围,于是,从长远看,你可以得到更好的生态系统的支持(也就是说,大部分支持的软件可以从社区中获得,也有更多的开发者可以雇佣)。

如果从 HBase 开始时,我的印象就是它后面有巨大的社区力量,但我现在相信,Cassandra 更加强大。最初的印象部分来源于 StumpleUpon 和 Streamy 的两位 CTO 的两个非常有说服力的出色的讲演,他们是 Web 行业中两个在 Cassandra 成为一个可选系统之前的 HBase 的两个重要的贡献者,同时也部分来源于快速阅读了一篇名为“HBase vs Cassandra: NoSQL 战役!”的文章(大部分内容都被广泛证实了)。

势头是很难确证的,你不得不自己进行研究,不过我可以找到的一个重要的标志是 IRC 上的开发者动向。如果你在 freenode.org 上比较 #hbase 和 #cassandra 的开发这频道,你会发现 Cassandra 差不多在任何时候都有两倍的开发者在线。

如果你用考虑 HBase 一般的时间来考察 Cassandra,你就能发现 Cassandra 的背后确实有非常明显的加速势头。你可能还会发现那些逐渐出现的鼎鼎大名,如 Twitter,他们也计划广泛使用 Cassandra(这里)。

注:Cassandra 的网站看起来比 HBase 的好看多了,但认真的说,这可能不仅是市场的趋势。继续吧。

深入到技术部分: CAP 和 CA 与 AP 的神话

对于分布式系统,有个非常重要的理论(这里我们在讨论分布式数据库,我相信你注意到了)。这个理论被称为 CAP 理论,由 Inktomi 的 联合创始人兼首席科学家 Eric Brewer 博士提出。

这个理论说明,分布式(或共享数据)系统的设计中,至多只能够提供三个重要特性中的两个——一致性、可用性和容忍网络分区。简单的说,一致性指如果一个人向数据库写了一个值,那么其他用户能够立刻读取这个值,可用性意味着如果一些节点失效了,集群中的分布式系统仍然能继续工作,而容忍分区意味着,如果节点被分割成两组无法互相通信的节点,系统仍然能够继续工作。

Brewer教授是一个杰出的人物,许多开发者,包括 HBase 社区的很多人,都把此理论牢记在心,并用于他们的设计当中。事实上,如果你搜索线上的关于 HBase 和 Cassandra 比较的文章,你通常会发现,HBase 社区解释他们选择了 CP,而 Cassandra 选择了 AP ——毫无疑问,大多数开发者需要某种程度的一致性 (C)。

不过,我需要请你注意,事实上这些生命基于一个不完全的推论。CAP 理论仅仅适用于一个分布式算法(我希望 Brewer 教授可以统一)。但没有说明你不能设计一个系统,在其中的各种操作的底层算法选择上进行这种。所以,在一个系统中,确实一个操作职能提供这些特性中的两个,但被忽视的问题是在系统设计中,实际是可以允许调用者来选择他们的某个操作时需要哪些特性的。不仅如此,现实世界并不简单的划分为黑白两色,所有这些特性都可以以某种程度来提供。这就是 Cassandra。

这点非常重要,我重申:Cassandra 的优点在于你可以根据具体情况来选择一个最佳的折衷,来满足特定操作的需求。Cassandra 证明,你可以超越通常的 CAP 理论的解读,而世界仍然在转动。

我们来看看两种不同的极端。比如我必须从数据库中读取一个要求具有很高一致性的值,也就是说,我必须 100%保证能够读取到先前写入的最新的内容。在这种情况下,我可以通过指定一致性水平为“ALL”来从 Cassandra 读取数据,这时要求所有节点都有数据的一致的副本。这里我们不具有对任何节点失效和网络分裂的容错性。在另一个极端的方面,如果我不特别关心一致性,或仅仅就是希望最佳性能,我可以使用一致性级别“ONE”来访问数据。在这种情况下,从任意一个保存有这个副本的节点获取数据都可以——即使数据有三个副本,也并不在意其他两个有副本的节点是否失效或是否有不同,当然,这种情况下我们读到的数据可能不是最新的。

不仅如此,你不必被迫生活在黑白世界中。比如,在我们的一个特定的应用中,重要的读写操作通常使用“QUORUM”一致性级别,这意味着大部分存有此数据的节点上的副本是一致的——我这里是个简要描述,具体写你的 Cassandra 程序之前最好还是仔细研究一下。从我们的视角看,这这提供了一个合理的节点失效与网络分裂的耐受性,同时也提供了很高的一致性。而在一般情况下,我们使用前面提到的“ONE”一致性级别,者可以提供最高的性能。就是这样。

对我们来说,这是 Cassandra 的一个巨大的加分项目。我们不仅能轻易地调整我们的系统,也可以设计它。比如,当一定数量的节点失效或出现网络连接故障时,我们的大部分服务仍然可以继续工作,只有那些需要数据一致性的服务会失效。HBase并没有这么灵活,它单纯地追求系统的一个方面(CP),这让我再次看到了 SQL 开发者和查询优化人员们之间的那道隔阂——有些事情最好能够超越它,HBase!

In our project then, Cassandra has proven by far the most flexible system, although you may find your brain at first loses consistency when considering your QUORUMs.在我们的项目之后,卡桑德拉已被证明是迄今为止最灵活的系统,虽然你可能发现一致性第一失去你的大脑在考虑您的法定人数。

在我们的项目中,Cassandra 已经证明了它是有史以来最灵活的系统,虽然你可能在对这个问题进行投票(QUORUM)的时候发现的大脑失去了一致性。

什么时候单体会比模块化强?

Cassandra 和 HBase 的一个重要区别是, Cassandra 在每个节点是是一个单 Java 进程,而完整的 HBase 解决方案却由不同部分组成:有数据库进程本身,它可能会运行在多个模式;一个配置好的 hadoop HDFS 分布式文件系统,以及一个 Zookeeper 系统来协调不同的 HBase 进程。那么,这是否意味着 HBase 有更好的模块化结构呢?

虽然 HBase 的这种架构可能确实可以平衡不同开发团队的利益,在系统管理方面,模块化的 HBase 却无法视为一个加分项目。事实上,特别是对于一些小的初创公司,模块化倒是一个很大的负面因素。

HBase的下层相当复杂,任何对此有疑惑的人应该读读 Google 的 GFS 和 BigTable 的论文。即使是在一个单一节点的伪分布式模式下来架设 HBase 也很困难——事实上,我曾经费力写过一篇快速入门的教程(如果你要试试HBase的话看看这里)。在这个指南里你可以看到,设置好 HBase 并启动它实际包含了两个不同系统的手工设置:首先是 hadoop HDFS,然后才是 HBase 本身。

然后,HBase 的配置文件本身就是个怪兽,而你的设置可能和缺省的网络配置有极大的不同(在文章里我写了两个不同的Ubuntu的缺省网络设置,以及 EC2 里易变的 Elastic IP 和内部分配的域名)。当系统工作不正常的时候,你需要查看大量的日志。所有的需要修复的东西的信息都在日志里,而如果你是一个经验丰富的管理员的话,就能发现并处理问题。

但是,如果是在生产过程中出现问题,而你又没有时间耐心查找问题呢?如果你和我们一样,只有一个小的开发团队却有远大的目标,没有经历去 7*24 的进行系统监控管理会怎么样呢?

严肃地说,如果你是一个希望学习 NoSQL 系统的高级 DB 管理员的话,那么选择 HBase。这个系统超级复杂,有灵巧双手的管理员肯定能拿到高薪。

但是如果你们是一个向我们一样尽力去发现隧道尽头的小团队的话,还是等着听听别的闲话吧

胜在 Gossip

Cassandra 是一个完全对称的系统。也就是说,没有主节点或像 HBase 里的 region server 这样的东西——每个节点的角色是完全一样的。不会有任何特定的节点或其他实体来充当协调者的角色,集群中的节点使用称为 “Cossip” 的纯 P2P 通信协议来协调他们的行为。

对 Gossip 的详细描述和使用 Gossip 的模型超过了本文的内容,但 Cassandra 所采用的 P2P 通信模型都是论证过的,比如发现节点失效的消息传播到整个系统的时间,或是一个客户应用的请求被路由到保存数据的节点的时间,所有这些过程所消耗的时间都毫无疑问的非常的短。我个人相信,Cassandra 代表了当今最振奋的一种 P2P 技术,当然,这和你的 NOSQL 数据库的选择无关。

那么,这个基于 Gossip 的架构究竟给 Cassandra 用户带来什么显示的好处呢。首先,继续我们的系统管理主体,系统管理变得简单多了。比如,增加一个新节点到系统中就是启动一个 Cassandra 进程并告诉它一个种子节点(一个已知的在集群中的节点)这么简单。试想当你的分布式集群可能运行在上百个节点的规模上的时候,如此轻易地增加新节点简直是难以置信。更进一步,当有什么出错的时候,你不需要考虑是哪种节点出了问题——所有节点都是一样的,这让调试成为了一个更加易于进行且可重复的过程。

第二,我可以得出结论,Cassandra 的 P2P 架构给了它更好的性能和可用性。这样的系统中,负载可以被均衡地三步倒各个节点上,来最大化潜在的并行性,这个能力让系统面临网络分裂和节点失效的时候都能更加的无缝,并且节点的对称性防止了 HBase 中发现的那种在节点加入和删除时的暂时性的性能都懂(Cassandra 启动非常迅速,并且性能可以随着节点的加入而平滑扩展)。

如果你想寻找更多更多的证据,你会对一个原来一直关注 hadoop 的小组(应该对 HBase 更加偏爱)的报告很感兴趣……

一份报告胜过千言万语。我是指图表

Yahoo!进行的第一个 NOSQL 系统的完整评测。研究似乎证实了 Cassandra 所享有的性能优势,从图表上看,非常倾向于 Cassandra。

目前这些论文还是草稿,你可以从这里找到这些论文:
http://www.brianfrankcooper.net/pubs/ycsb-v4.pdf
http://www.brianfrankcooper.net/pubs/ycsb.pdf

注意:这份报告中 HBase 仅在对一个范围的记录进行扫描这一项上优于 Cassandra。虽然 Cassandra 团队相信他们可以很快达到 HBase 的时间,但还是值得指出,在通常的 Cassandra 配置中,区间扫描几乎是不可能的。我建议你可以无视这一点,因为实际上你应该在 Cassandra 上面来实现你自己的索引,而非使用区间扫描。如果你对区间扫描和在 Cassandra 中存储索引相关问题有兴趣,可以看我的这篇文章

最后一点: 这篇文章背后的 Yahoo!研究团队正尝试让它们的评测应用通过法律部门的评估,并将它发布给社区。如果他们成功的话,我当然希望他们成功,我们将能够看到一个持续的竞争场面,不论 HBase 还是 Cassandra 无疑都会进一步提高他们的性能。

锁和有用的模块性

毫无疑问,你会从 HBase 阵营听到这样的声音:HBase 的复杂结构让它可以提供 Cassandra 的 P2P 架构无法提供的东西。其中一个例子可能就是 Hbase 提供给开发者行锁机制,而 Cassandra 则没有(在 HBase 中,因为数据副本发生在 hadoop 底层,行锁可以由 region server 控制,而在 Cassandra 的 P2P 架构中,所有节点都是平等的,所以也就没有节点可以像一个网管囊样负责锁定有副本的数据)。

不够,我还是把这个问题返回到关于模块化的争论中,这实际是对 Cassandra 有理的。Cassandra 通过在对称节点上分布式存储数据来实现了 BigTable 的数据模型。它完整地实现了这些功能,而且是以最灵活和高性能的方式实现的。但如果你需要锁、事务和其它功能的话,这些可以以模块的方式添加到你的系统之中——比如,我们发现我们可以使用 Zookeeper 和相关的工具来很简单地为我们的应用提供可扩展的锁功能(对于这个功能,Hazelcast 等系统可能也可以实现这个功能,虽然我们没有进行研究)。

通过为一个窄领域目的来最小化它的功能,对我来说,Cassandra 的设计达到了它的目的——比如前面指出可配置的 CAP 的折衷。这种模块性意味着你可以依据你的需求来构建一个系统——需要锁,那么拿来 Zookeeper,需要存储全文索引,拿来 Lucandra ,等等。对于我们这样的开发者来说,这意味着我们不必部署复杂度超出我们实际需要的系统,给我们提供了更加灵活的构建我们需要的应用的终极道路。

MapReduce,别提 MapReduce!

Cassandra 做的还不够好的一件事情就是 MapReduce!对于不精通此项技术同学简单的解释一句,这是一个用于并行处理大量数据的系统,比如从上百万从网络上抓取的页面提取统计信息。MapReduce 和相关系统,比如 Pig 和 Hive 可以和 HBase 一起良好协作,因为它使用 HDFS 来存储数据,这些系统也是设计用来使用 HDFS 的。如果你需要进行这样的数据处理和分析的话,HBase 可能是你目前的最佳选择。

记住,这就像小马过河!

因此,我停止了对 Cassandra  的优点的赞美,实际上,HBase 和 Cassandra 并不一定是一对完全的竞争对手。虽然它们常常可以用于同样的用途,和 MySQL 和 PostgreSQL 类似,我相信在将来它们将会成为不同应用的首选解决方案。比如,据我所知 StumbleUpon 使用了 HBase 和 hadoop MapReduce 技术,来处理其业务的大量数据。Twitter 现在使用 Cassandra 来存储实时交互的社区发言,可能你已经在某种程度上使用它了。

作为一个有争议的临别赠言,下面我们进入下一个话题。

注意:在继续下一个小节之前,我要指出,Cassandra 在 0.6 版本会有 hadoop 支持,所以 MapReduce 整合能获得更好的支持。

兄弟,我不能失去数据…

作为先前 CAP 理论争议的一个可能结果,可能有这样的印象,HBase 的数据似乎比 Cassandra 中的数据更安全。这是我希望揭露的最后一个关于 Cassandra 的秘密,当你写入新数据的时候,它实际上立刻将它写入一个将要存储副本的仲裁节点的 commit log 当中了,也被复制到了节点们的内存中。这意味着如果你完全让你的集群掉电,只可能会损失极少数据。更进一步,在系统中,通过使用 Merkle tree 来组织数据的过分不一致(数据熵),更加增加了数据的安全性:)

事实上,我对 HBase 的情况并不是非常确切——如果能有更细节的情况,我回尽快更新这里的内容的——但现在我的理解是,因为 hadoop 还不支持 append,HBase 不能有效地将修改的块信息刷入 HDFS (新的对数据变化会被复制为多个副本并永久化保存)。这意味着会有一个更大的缺口,你最新的更改是不可见的(如果我错了,可能是这样,请告诉我,我回修正本文)。

所以,尽管希腊神话中的 Cassandra 非常不幸(译注:Cassandra 是希腊神话里,特洛伊的那个可怜的女先知的名字,如果你不知道详情的话,可以参考wiki),但你的 Cassandra 中的数据不会有危险。

注意:Wade Amold 指出, hadoop .21 很快就会发布,其中将会解决 HBase 的这个问题。

[译文] Observers: 让ZooKeeper更具可伸缩性

December 22nd, 2009

原文: http://www.cloudera.com/blog/2009/12/15/observers-making-zookeeper-scale-even-further/
Tuesday, December 15th, 2009 at 10:30 am by Henry Robinson
译文: 王旭(http://wangxu.me, @gnawux)2009年12月16,21日

看过我们之前相关文章的读者都知道,ZooKeeper是一个分布式协作服务,用于实现锁和并发事务排队等协作原语。ZooKeeper 的一个优势是可伸缩性(Scalability)。五台或七台机器集群可以满足很多大型应用的需求。

最近我们给ZooKeeper增加了一个新的特性,进一步增强了它可伸缩性——一种新的称为 Observers 的服务器。在这篇文章中,我想要介绍一下添加这个特征的动机,并解释这个服务器如何帮助我们的系统更具有可伸缩性。可伸缩性对不同的人意味着不同的事情,而在这里是说,如果我们的工作负载可以通过给系统分配更多的资源来分担,那么这个系统就是可伸缩的;一个不可伸缩的系统却无法通过增加资源来提升性能,甚至会在工作负载增加时,性能急剧下降。

要了解 Observers 为何能影响 ZooKeeper 的可伸缩性,我们需要首先了解一下这个服务时如何工作的。宽泛地说,ZooKeeper 集群上的任何操作不是读操作就是写操作。ZooKeeper 确保所有读和写操作在所有客户端看来,都具有完全相同的顺序,这样他们就不会为操作的顺序而疑惑了。

在提供强一致性保障的同时,ZooKeeper同时给出高可用性承诺,这可以被简单地解释为它可以在多台服务器失效的情况下仍然为客户端提供服务。ZooKeeper使用一个传统的手段来达到可用性——通过将数据读写分布到几台机器上来实现,这样如果一台失效了,其它的可以接管它的服务,而无需让客户端更聪明。

然而,一致性和可用性这两个属性是很难同时达到的,目前,ZooKeeper 必须确保集群中的每个副本都对读写操作是顺序性的。它通过一个一致性协议来达到这一目标。简单地说,这个协议由一个选定的领导者将新操作高速其他服务器,所有节点投票支持并反馈给领导。一旦领导节点收集到过半数的投票,它就认为投票已经获得了通过,并将进一步消息传送给服务器们,以使他们可以继续工作,将操作提交到内存中。

这个从始至终的数据流如下图所示。客户进程将一个值提交给它连接的服务器。服务器将消息转送给领导节点,它发起这个一致性协议,一旦最初的服务器从领导节点得到结果,它就可以返回给用户了。

Simplified flow of a ZooKeeper write request

图1:简化的写请求工作流

Observers 的需求源于 ZooKeeper 服务器在上述协议中实际扮演了两个角色。它们从客户端接受连接与操作请求,之后对操作结果进行投票。这两个职能在 ZooKeeper集群扩展的时候彼此制约。如果我们希望增加 ZooKeeper 集群服务的客户数量(我们经常考虑到有上万个客户端的情况),那么我们必须增加服务器的数量,来支持这么多的客户端。然而,从一致性协议的描述可以看到,增加服务器的数量增加了对协议的投票部分的压力。领导节点必须等待集群中过半数的服务器响应投票。于是,节点的增加使得部分计算机运行较慢,从而拖慢整个投票过程的可能性也随之提高,投票操作的会随之下降。这正是我们在实际操作中看到的问题——随着 ZooKeeper 集群变大,投票操作的吞吐量会下降。

所以,这让我们不得不在增加客户节点数量的期望和我们希望保持较好吞吐性能的期望间进行权衡。要打破这一耦合关系,我们引入了不参与投票的服务器,称为 Observers。 Observers 可以接受客户端的连接,将写请求转发给领导节点。但是,领导节点不会要求 Observers 参加投票。相反,Observers 不参与投票过程,仅仅在上述第3歩那样,和其他服务节点一起得到投票结果。

这个简单的扩展给 ZooKeeper 的可伸缩性带来了全新的镜像。我们现在可以加入很多 Observers 节点,而无须担心严重影响写吞吐量。规模伸缩并非无懈可击——协议中的一歩(通知阶段)仍然与服务器的数量呈线性关系。但是,这里的穿行开销非常低。我们可以认为在通知服务器阶段的开销无法成为主要瓶颈。

Observers Write Throughput Benchmark

图2: Observers 写吞吐量 Benchmark

图2 显示了一个简单评测的结果。纵轴是我能够从一个单一的客户端发出的每秒钟同步写操作的数量(一个调优的 ZooKeeper 可以得到更好的每秒钟操作数——这里我们更感兴趣的是相对值)。横轴是 ZooKeeper 集群的尺寸。蓝色的是每个服务器都是 voting 服务器的情况,而绿色的则只有三个是 voting 服务器,其它都是 Observers。图中显示,我们扩充 Observers,写性能几乎可以保持不便,但如果同时扩展 voting 节点的数量的话,性能会明显下降。显然 Observers 是有效的。

Observers 同样提升读性能的可伸缩性

增加客户端的数量是 Observers 的一个重要用例,但是实际上它们还给集群带来很多其它的好处。

作为一个优化,ZooKeeper 服务器可以直接读取它们的本地数据存储,而无需经过投票过程,这面临一定的“时光旅行”风险,可能在读到新值之后又读到老值,但这只在服务器故障时才会发生。事实上,在这种情况下客户端可以请求一个 ‘sync’ 操作来保证下一个值是最新的。

因此,在大量读操作的工作负载下,Observers 是个巨大的性能提升。写操作直接进入标准的投票路径,这样,与客户端可扩展性类似,提高投票服务器数量来承担读操作会影响写性能。Observers 允许我们将读性能和写性能分开。这适用于 ZooKeeper 的很多应用场景,大部分客户端很少写,但经常读。

Observers 提供了广域网能力

Observers 还能做更多。Observers 对于跨广域网连接的客户端来说是很好的候选方案。这有三个原因。为了获得很好的读性能,有必要让客户端离服务器尽量近,这样往返时延不会太高。然而,将 ZooKeeper 集群分散到两个集群是非常不可取的设计,因为良好配置的 ZooKeeper 应该让投票服务器间用低时延连接互联——否则我们将会遇到上面提到的低反映速度的问题。

而 Observers 可以被部署在需要访问 ZooKeeper 的任意数据中心中。这样,投票协议不会受到数据中心间链路的高时延的影响,性能得到提升。投票过程中 Observers 和领导节点间的消息远少于投票服务器和领导节点间的消息。这有助于在远程数据中心高写负载的情况下降低带宽需求。

最后,由于Observers即使失效也不会影响到投票集群,这样如果数据中心间链路发生故障,不会影响到服务本身的可用性。这种故障的发生概率要远高于一个数据中心中机架间的连接的故障概率,所以不依赖于这种链路是个优点。

如何开始使用 Observers

Observers 还没有成为某个 ZooKeeper release 的一部分,所以要使用它,你需要从 Subversion trunk 获取源代码。

下面的内容提取自 Observers 用户手册,可以在源代码的 docs/zooKeeperObservers.html 文件中看到。

如何使用 Observers

注意,在ZOOKEEPER-578 解决之前,你必须在每个服务器的配置文件中设置 electionAlg=0 。否则当你启动服务的时候会抛出一个异常。

原因:因为 Observers 并不参与领导节点的选举,它们依赖于投票 Followers 来获知领导的变动。目前,只有基本选举算法启动一个线程来响应 Observers 确定当前领导的请求。其他 JIRA 上的工作将会让其他所有的领导选举协议都支持这一功能。

设置 ZooKeeper 使用 Observers 非常简单,只需要在配置文件中有两处改动。首先是每个 Observer 的配置文件中都要有这么一行:

peerType=observer

这行让服务器作为一个 Observer 来工作。之后,在每个服务器配置文件中,你必须在服务器定义行给每个 Observer 加入 :o bserver  。比如:

server.1:localhost:2181:3181:observer

这让每个其他服务器知道 server.1 是一个 Observer,就不会期望它进行投票了。这就是要加入一个 Observer 的时候,所有你需要做的配置。现在可以将它作为一个正常的 Follower 来看待了。可以这么试试:

bin/zkCli.sh -server localhost:2181

这里 localhost:2181 是 Observer 在每个配置文件中指定的主机名和端口号。你应该看到命令行提示了,这时就可以查询 Zookeeper 服务了。

下一步工作

Observers 特性还有很多工作要做。短期内,我们将致力于让 Observers 与 ZooKeeper 中的所有领导选举算法完全兼容 —— 我们希望这个可以在未来几天内完成。长期看,我们希望能研究一下进行性能优化,比如基于 Observers 的集群的批量和离线读取,来更好的利用 Observers 不像一般 ZooKeeper 服务器一样严格要求时延的好处。

我们希望 Observers 能进入明年年初的 ZooKeeper 3.3.0。我们会很高兴能听到你的反馈,不管是在邮件列表里还是直接发送email。ZooKeeper 长期招募贡献者,我们有足够多的有趣的问题来解决,所以,如果你想加入的话请联系我,我回很高兴能帮你开始的。

[译文] NOSQL们背后的共有原则

December 20th, 2009

原文: http://natishalom.typepad.com/nati_shaloms_blog/2009/12/the-common-principles-behind-the-nosql-alternatives.html
Posted by Nati Shalom at 12:01 PM Dec 15, 2009
译文: 王旭(http://wangxu.me , @gnawux)2009年12月16/19日

几个星期之前,我写了一篇文章描述了常被称作 NOSQL 的一类新型数据库的背后驱动。几个星期之前,我在Qcon上发表了一个演讲,其中,我介绍了一个可伸缩(scalable)的 twitter 应用的构建模式,在我们的讨论中,一个显而易见的问题就是数据库的可扩展性问题。要解答这个问题,我试图寻找隐藏在各种 NOSQL 之后的共有模式,并展示他们是如何解决数据库可扩展性问题的。在本文中,我将尽力勾勒出这些共有的原则。

nosql

实现们的共有原则

假设失效是必然发生的

与我们先前通过昂贵硬件之类的手段尽力去避免失效的手段不同,NOSQL实现都建立在硬盘、机器和网络都会失效这些假设之上。我们需要认定,我们不能彻底阻止这些时效,相反,我们需要让我们的系统能够在即使非常极端的条件下也能应付这些失效。Amazon S3 就是这种设计的一个好例子。你可以在我最近的文章 Why Existing Databases (RAC) are So Breakable! 中找到进一步描述。哪里,我介绍了一些来自 Jason McHugh 的讲演的面向失效的架构设计的内容(Jason 是在 Amazon 做 S3 相关工作的高级工程师)。

对数据进行分区

通过对数据进行分区,我们最小化了失效带来的影响,也将读写操作的负载分布到了不同的机器上。如果一个节点失效了,只有该节点上存储的数据受到影响,而不是全部数据。

保存同一数据的多个副本

大部分 NOSQL 实现都基于数据副本的热备份来保证连续的高可用性。一些实现提供了 API,可以控制副本的复制,也就是说,当你存储一个对象的时候,你可以在对象级指定你希望保存的副本数。在 GigaSpaces,我们还可以立即复制一个新的副本到其他节点,甚至在必要时启动一台新机器。这让我们不比在每个节点上保存太多的数据副本,从而降低总存储量以节约成本。

你还可以控制副本复制是同步还是异步的,或者两者兼有。这决定了你的集群的一致性、可用性与性能三者。对于同步复制,可以牺牲性能保障一致性和可用性(写操作之后的任意读操作都可以保证得到相同版本的数据,即使是发生失效也会如此)。而最为常见的 GigaSpaces 的配置是同步副本到被分界点,异步存储到后端存储。

动态伸缩

要掌控不断增长的数据,大部分 NOSQL 实现提供了不停机或完全重新分区的扩展集群的方法。一个已知的处理这个问题的算法称为一致哈希。有很多种不同算法可以实现一致哈希。

一个算法会在节点加入或失效时通知某一分区的邻居。仅有这些节点受到这一变化的影响,而不是整个集群。有一个协议用于掌控需要在原有集群和新节点之间重新分布的数据的变换区间。

另一个(简单很多)的算法使用逻辑分区。在逻辑分区中,分区的数量是固定的,但分区在机器上的分布式动态的。于是,例如有两台机器和1000个逻辑分区,那么每500个逻辑分区会放在一台机器上。当我们加入了第三台机器的时候,就成了每 333 个分区放在一台机器上了。因为逻辑分区是轻量级的(基于内存中的哈希表),分布这些逻辑分区非常容易。

第二种方法的优势在于它是可预测并且一致的,而使用一致哈希方法,分区之间的重新分布可能并不平稳,当一个新节点加入网络时可能会消耗更长时间。一个用户在这时寻找正在转移的数据会得到一个异常。逻辑分区方法的缺点是可伸缩性受限于逻辑分区的数量。

更进一步的关于这一问题的讨论,建议阅读 Ricky Ho 的文章 NOSQL Patterns

查询支持

在这个方面,不同的实现有相当本质的区别。不同实现的一个共性在于哈希表中的 key/value 匹配。一些市县提供了更高级的查询支持,比如面向文档的方法,其中数据以 blob 的方式存储,关联一个键值对属性列表。这种模型是一种无预定义结构的(schema-less)存储,给一个文档增加或删除属性非常容易,无需考虑文档结构的演进。而 GigaSpaces 支持很多 SQL 操作。如果 SQL查询没有指出特定的简直,那么这个查询就会被并行地 map 到所有的节点去,由客户端完成结果的汇聚。所有这些都是发生在幕后的,用户代码无需关注这些。

使用 Map/Reduce 处理汇聚

Map/Reduce 是一个经常被用来进行复杂分析的模型,经常会和 Hadoop 联系在一起。 map/reduce 常常被看作是并行汇聚查询的一个模式。大部分 NOSQL 实现并不提供 map/reduce 的内建支持,需要一个外部的框架来处理这些查询。对于 GigaSpaces 来说,我们在 SQL 查询中隐含了对 map/reduce 的支持,同时也显式地提供了一个称为 executors 的 API 来支持 map/reduce。在质疑模型中,你可以将代码发送到数据所在地地方,并在该节点上直接运行复杂的查询。

这方面的更多细节,建议阅读 Ricky Ho 的文章 Query Processing for NOSQL DB

基于磁盘的和内存中的实现

NOSQL 实现分为基于文件的方法和内存中的方法。有些实现提供了混合模型,将内存和磁盘结合使用。两类方法的最主要区别在于每 GB 成本和读写性能。

最近,斯坦福的一项称为“The Case for RAMCloud”的调查,对磁盘和内存两种方法给出了一些性能和成本方面的有趣的比较。总体上说,成本也是性能的一个函数。对于较低性能的实现,磁盘方案的成本远低于基于内存的方法,而对于高性能需求的场合,内存方案则更加廉价。

内存云的显而易见的缺点就是单位容量的高成本和高能耗。对于这些指标,内存云会比纯粹的磁盘系统差50到100倍,比使用闪存的系统差5-10倍(典型配置情况和指标参见参考文献[1])。内存云同时还比基于磁盘和闪存的系统需要更多的机房面积。这样,如果一个应用需要存储大量的廉价数据,不需要高速访问,那么,内存云将不是最佳选择。
然而,对于高吞吐量需求的应用,内存云将更有竞争力。当使用每次操作的成本和能量作为衡量因素的时候,内存云的效率是传统硬盘系统的 100 到 1000 倍,是闪存系统的 5-10 倍。因此,对于高吞吐量需求的系统来说,内存云不仅提供了高性能,也提供了高能源效率。同时,如果使用 DRAM 芯片提供的低功耗模式,也可以降低内存云的功耗,特别是在系统空闲的时候。此外,内存云还有一些缺点,一些内存云无法支持需要将数据在多个数据中心之间进行数据复制。对于这些环境,更新的时延将主要取决于数据中心间数据传输的时间消耗,这就丧失了内存云的时延方面的优势。此外,跨数据中心的数据复制会让内存云数据一致性更能难保证。不过,内存云仍然可以在夸数据中心的情况下提供低时延的读访问。

仅仅是炒作?

近来我见到的最多的问题就是 “NOSQL 是不是就是炒作?” 或 “NOSQL 会不会取代现在的数据库?”

我的回答是——NOSQL 并非始于今日。很多 NOSQL 实现都已经存在了十多年了,有很多成功案例。我相信有很多原因让它们在如今比以往更受欢迎了。首先是由于社会化网络和云计算的发展,一些原先只有很高端的组织才会面临的问题,如今已经成为普遍问题了。其次,已有的方法已经被发现无法跟随需求一起扩展了。并且,成本的压力让很多组织需要去寻找更高性价比的方案,并且研究证实基于普通廉价硬件的分布式存储解决方案甚至比现在的高端数据库更加可靠。(进一步阅读)所有这些导致了对这类“可伸缩性优先数据库”的需求。这里,我引用 AWS团队的接触工程师、VP, James Hamilton 在他的文章 One Size Does Not Fit All 中的一段话:

“伸缩性优先应用是那些必须具备无限可伸缩性的应用,能够不受限制的扩展比更丰富的功能更加重要。这些应用包括很多需要高可伸缩性的网站,如 Facebook, MySpace, Gmail, Yahoo 以及 Amazon.com。有些站点实际上使用了关系型数据库,而大部分实际上并未使用。这些服务的共性在于可扩展性比功能公众要,他们无法泡在一个单一的 RDBMS 上。”

总结一下——我认为,现有的 SQL 数据库可能不会很快淡出历史舞台,但同时它们也不能解决世上的所有问题。NOSQL 这个名词现在也变成了 Not Only SQL,这个变化表达了我的观点。

参考:

[译文] Avro: 大数据的数据格式

November 3rd, 2009

Monday, November 2nd, 2009 at 8:00 am by Doug Cutting, filed under data collection, general, hadoop, mapreduce.
王旭 [ gnawux(at)gmail.com , @gnawux, http://wangxu.me ]于11月2-3日译

译注:Doug Cutting 是 Hadoop 的大佬,目前在 Cloudera,Avro 基本上将成为未来 Hadoop 的数据描述和 RPC 的基础,今天看到这篇,就立刻翻译了,水平有限且译的比较草,请见谅,且欢迎纠错。

Avor 是 Apache 的 Hadoop 项目族的一个新成员。Avro 定义了一种用于支持大数据应用的数据格式,并为这种格式提供了不同的编程语言的支持。

背景

我们希望处理大数据的应用可以更加动态化:人们应该能够快速的从不同的数据源合并数据集。我们希望能够让新颖的、创造性的数据分析变得更方便。比如说,有人需要完美地将销售各个网点的交易、网站的访问量以及外部的统计数据关联在一起,而不需要很多的准备工作。这应该可以使用脚本和交互工具即时完成。

目前的数据格式通常都做的不是很好。XML 和 JSON 可以承载很多信息,但它们本身就很大,处理起来很慢。当你处理上 PB 的数据的时候,尺寸和速度绝对是个大问题。

Google 使用一个称为 Protocol Buffers 的系统来解决这个问题。(还有其他的系统,比如 Thrift,与 Protocol Buffers 很类似,我不会在这里深入讨论它们,但我对 Protocol Buffers 的评价对它们同样适用。)Google 已经开放了 Protocol Buffers,但它对我们的目的来说也不完美。

通用数据

在 Protocol Buffers 中,用户首先定义数据结构,然后生成可以有效读写这些数据的代码。不过,如果你需要在一个脚本语言中直接使用 Protocol Buffer 的数据,你必须首先确定数据结构定义的位置;为它生成代码;最后,在获取数据之前装载代码。这么做可能还算是不错,但如果我们想要一个能浏览任意数据集的通用工具,它将不得不首先定位定义,再为每个数据集生成与装在代码。这让很多本来简单的事情变得复杂了。

与 Protocol Buffer 不同,Avro 格式将数据结构的定义以一种易于处理的形式存储在数据之中。这样,Avro 的实现可以在运行时使用这些定义,将数据以一种通用的方式展现给应用,而不需要生成代码。

代码生成在 Avro 中是可选的:如果某些编程语言要对某些数据结构是由代码生成的话也相当不错,比如需要频繁串行化的数据类型。但是,对于像 HivePig 这样的脚本来说,代码生成可能会是一种过分的负担,所以,Avro 不需要代码生成。

把数据结构定义存储在数据中的一个附加的好处是允许数据可以被更快和更小巧地存储。Protocol Buffers 为数据增加了注释,以保证在定义和数据不完全匹配的时候仍然能够得到处理。然而这些注释会让数据略微增大、处理稍稍变慢。一些评测结果显示,Avro 这样不需要这些注释的数据和其他串行化系统相比,更小而且处理起来更快。

Avro Schemas

Avro 使用 JSON 来定义数据结构的 schema。比如,一个二维平面上的点可以定义为如下的 Avro 记录:


{"type": "record", "name": "Point",
"fields": [
  {"name": "x", "type": "int"},
  {"name": "y", "type": "int"},
]
}

指针的每个实例都包含两个整数,不包含附加的每个记录或每个域的注释。整数使用变长 zig-zag 编码存储。所以小的正负值可能只需要两个字节:100个点可能只需要两百个字节。

在记录和数值类型之外,Avro 支持数组、map、每句、变长与定长二进制数据以及字符串。它还定义了一个容器文件格式,这样可以更好的支持 MapReduce 和其他大数据处理框架。对于更多的细节信息,可以参考 Avro 的规范

兼容性

应用程序都在演进,在这一过程中数据结构是可以改变的。我们希望应用的新版本仍然能够处理老版本创建的数据,反之亦然。Avro 和 Protocol Buffers 处理这个问题的方式比较类似。当应用需要的域没有出现的时候,Avro 会提供一个 schema 中规定的缺省值。Avro 忽略掉数据中的意外值。这并不能处理所有的后向兼容问题,但它让大部分的兼容性问题更容易处理。

RPC

Avro 也定义了一个 RPC 协议。尽管 RPC 中使用的数据类型和数据集中的类型通常是不同的,使用一个通用的串行化系统仍然是有用的。大数据需要一个分布式的基于 RPC 的框架。所以,在我们需要处理数据集文件的所有地方,我们也需要能够使用 RPC。这样,将 RPC 和数据集都建立在同一个基础之上将会最小化地减少那些代码可以处理数据,却无法使用分布式框架来这么做的可能性。

与 Hadoop 集成

我们希望在 Hadoop MapReduce 中可以简单地使用 Avro 数据。目前这项工作仍然在进展中。目前 issue MAPREDUCE-1126MAPREDUCE-815 在处理这个问题。

注意,Avro 数据结构可以定义它们的排序方式,所以一个编程语言中创建的复杂的数据可以在另一个语言中被排序。Avro 可以在不还原序列化的情况下进行排序,这可以加快处理速度。

我们希望 Avro 可以替换掉 Hadoop 中现有的 RPC。Hadoop 目前需要客户端和服务器必须使用相同版本的 Hadoop。我们希望使用 Avro 可以允许一个 Hadoop 应用能和多个运行不同版本的 HDFS 和/或 MapReduce 的多个集群交互工作。

Finally, we hope that Avro will permit Hadoop applications to be more easily written in languages besides Java.  For example, once Hadoop’s built on Avro, we hope to support native MapReduce and HDFS clients in languages like Python, C and C++.

最后,我们希望 Avro 可以允许 Hadoop 应用更容易的使用 Java 之外的语言开发。比如,一旦 Hadoop 构建在 Avro 之上,我们希望支持 Python,C 和 C++ 等语言的 HDFS 和 MapReduce 的原生客户端。

Hadoop World 访谈

很多话题都已经包含在了我最近在 Hadoop World 的访谈,我很乐于在这篇 blog 里包含这段视频。(译注:译者撞墙看不到视频,不贴了哈。)

[译文] XI2 食谱(1)

September 28th, 2009

作者:Peter Hutterer
原文发布时间:2009年6月26日
原文来源:http://who-t.blogspot.com/2009/05/xi2-recipes-part-1.html
译者:王旭 ( http://wangxu.me , @gnawux )
翻译时间:2009年9月28日

XI2 (X Input 2)目前已经进入 XServer 的主线了。在接下来的几天里,我将发布出来一组“食谱”,来介绍如何和这些新功能打交道。这里的例子仅仅是一些片段,每部分的完整的示例都将放在这里

09年7月13日更新:调整到新的 cookie event API

在第一部分中,我将介绍一些总体的信息,初始化以及事件选择。

为什么需要 XI2?

 

引入 XI2 的一个重要原因是要将 MPX 并入 X Server。当前的 1.5 版本的 X Input 扩展余地非常有限,很难进行扩展以完全支持 MPX。在 XI 1.5 上编程非常痛苦,所以,我们开始寻找一个替代方案,从而让支持多设备的程序更容易写一些,并且能灵活地应付将来的应用场景。

XI2 的协议时相当保守的,只增加了一些新的请求和事件,但它也留下了很多的发展空间。XI2 和它的 API 与 X 核心 API 的模式非常接近。从客户应用(X Client)的角度看,与 XI 的最大不同在于调用需要附带一个设备ID参数,不需要去打开设备,而事件类型和掩码都是常数(更多的在下面)。

现在,惟一的 XI2 绑定是 Xlib 绑定。尽管如此,我仍鼓励你开始采用 XI2 并思考程序如何使用它。通过现在进行测试,你可以在 2.0 发布之前帮助发现问题和当前版本中的缺陷。当然,在发布前修正 bug 可以惠及所有人。

MPX

 

MPX允许同时使用多个鼠标指针和键盘焦点。这将有助于引领相当时髦的用户界面——双手操作、多人操作……所有你能说出来的。它还抛弃了当前 GUI 设计的一些假设,但我将在将来再去谈这些。

MPX引入了一个显式的主/从设备层次关系。最简单的记住这个层次关系的方式是:物理设备是从设备,而鼠标指针或键盘焦点是主设备。

从设备附着到一个主设备上,每当从设备产生一个事件,这个事件将被通过主设备路由到客户应用上。主设备总是成对出现(一个指针,一个键盘焦点)。

这样,一个常见的配置可能是一个笔记本有四个主设备(两个键盘焦点和两个鼠标指针),触控板和内建键盘控制第一对主设备,一个USB无线键鼠套装控制第二对主设备。标准配置是有一堆主设备,所有设备都附着到这对主指针和焦点上。这个方案意外地(或无意外地)和我们自从 X Server 1.4 的配置是一样的。这也意味着除非你建立一对新的主设备,否则 MPX 根本就不可见。

我将在后面的文章里更多的介绍 MPX,从大多数客户应用的角度看,主设备是惟一需要关心的问题,只有配置工具和一些其他的特殊程序才真的需要去关心从设备。

XI2 初始化

 

一个典型的 XI2 程序如下面这么开头:

/* Connect to the X server */Display *dpy = XOpenDisplay(NULL);

/* XInput Extension available? */int opcode, event, error;if (!XQueryExtension(dpy, "XInputExtension", &opcode, &event, &error)) {   printf("X Input extension not available.\n");   return -1;}

/* Which version of XI2? We support 2.0 */int major = 2, minor = 0;if (XIQueryVersion(dpy, &major, &minor) == BadRequest) {  printf("XI2 not available. Server supports %d.%d\n", major, minor);  return -1;}

首先,客户应用连接 X Server,之后询问可用的 XI 扩展的版本。XQueryExtension 不仅告诉我们 XI 扩展是否存在,也会返回扩展的 opcode。这个 opcode 在事件处理时需要用到(所有的 XI2 事件都用到了这个 opcode)。这个 opcode 是在 X Server 启动时设置的,所以,在跨 X Server 的应用中,你不能把它当做个常量。

最后,我们宣布我们支持 XI 2.0,服务器返回它支持的版本。 XIQueryVersion 是一个纯 XI2 调用,即使你在一个不支持 XI2 的服务器上调用,它的实现也不会返回一个 BadRequest 错误(这是在 Xlib 中的实现,所以如果你使用xcb,这个行为可能会不同)。

XIQueryVersion 不仅返回支持的版本,X Server 也会存储你的客户应用支持的版本。随着 XI2 的进程,使用这个调用变得非常重要,服务器将根据支持的版本来区别对待不同的客户应用。

事件的选择


初始化GUI之后,客户应用通常需要选择事件。这一工作可以通过 XISelectEvents 实现。


XIEventMask eventmask;

unsigned char mask[1] = { 0 }; /* the actual mask */

eventmask.deviceid = 2;

eventmask.mask_len = sizeof(mask); /* always in bytes */

eventmask.mask = mask;

/* now set the mask */

XISetMask(mask, XI_ButtonPress);

XISetMask(mask, XI_Motion);

XISetMask(mask, XI_KeyPress);

/* select on the window */

XISelectEvents(display, window, &eventmask, 1);

XIEventMask 定义了一个设备的掩码。掩码定义为(1 事件类型),匹配位必须在 eventmask.mask 域中设置。eventmask.mask 的尺寸可以是任意大小的,倡导可以满足所有需要设置的掩码。XI_LASTEVENT 定义为当前 XI2 协议中的最高的事件类型,所以你可以使用它来确定需要设置的掩码的位数。在本例中,我们只需要6位,所以一个字节就够了。

XISelectEvents 携带了多个事件掩码,所以,你可以一次提交多个事件的选择(比如每个对应一个设备)。

如上所示,每个 XIEventMask 都带有一个设备ID。要么是一个设备的数字ID,或是 XIAllDevices 或 XIAllMasterDevices 这样的特殊 ID。如果你选择 XIAllDevices 的事件掩码,所有的设备都会送来选择的事件。XIAllMasterDevices 也是一样,但只是所有的主设备。

XIAllDevices 和 XIAllMasterDevices 的事件掩码是特定设备事件掩码的一个补充。例如,你可以选择所有设备的按下事件,所有主设备的松开事件,以及设备2的移动事件,设备2将会向客户应用报告所有三类事件(有效掩码是按位或的)。

并且,XIAllDevices 和 XIAllMasterDevices 对所有设备都会生效,即使设备在客户应用选择事件之后才加入也会生效。所以,你只要设置事件掩码一次,无需关心当前有多少设备可用,将来还会进入多少设备。

检查事件的非常简单:

XEvent event;XNextEvent(display, &event);if (ev.xcookie.type == GenericEvent &&    ev.xcookie.extension == opcode &&    XGetEventData(dpy, &ev.xcookie)){    switch(ev.xcookie.evtype)    {        case XI_ButtonPress:        case XI_Motion:        case XI_KeyPress:            do_something(ev.xcookie.data);            break;    }}XFreeEventData(dpy, &ev.xcookie);


对每个事件的数据进行更加细节的分析将在后面的文章中被介绍。

注意,你需要检查 XI 扩展的 opcode,和 XQueryExtension(3) 返回的 opcode 进行比较(如前所述)。

就这么简单,有了这些信息,你已经可以开始写简单的监听所有设备的事件的程序了。在下一部分,我将介绍如何列出输入设备,并监听主/从设备层次结构发生的变化。

[译文] 空指针的乐趣(2)

September 14th, 2009

作者:Jonathan Corbet
原文发布日期:July 21, 2009
来源:http://lwn.net/Articles/342420/
译者:王旭 ( http://wangxu.me , @gnawux )
翻译时间:2009年9月14日

译者注:本来没准备翻译本系列的2,翻译文章实际上是译者的一种自我消遣方式,不过,翻译了1之后,有很多朋友的反应很不错,也有朋友在期待2,所以,就接着把2翻译了,时间仓促,翻译得不一定到位,请各位将就着看吧 :)

本系列的第一篇详细分析了一长串的偶然错误,利用它们可以用一个空指针解引用来对内核进行攻击。清除这个 bug 本身可以直接修改;实际上在这个问题的本质被大部分人所理解之前就已经修正了。从这个意义上讲,这个问题本身相当小,很少有发布版带有有问题的内核版本。但是,这个攻击证明了,内核中会有一大类类似的问题,在被修正之前,绝对有攻击者有机会可以发现这些错误并利用它们。

一个显而易见的问题是,当安全模块机制被配置入内核的时候,管理员指定的最小合法用户控件虚拟地址会被忽略,安全模块允许覆盖管理员指定的最小合法用户空间地址限制((mmap_min_addr)。这个行为颠覆了人们对安全模块工作的理解:它们被认为能够限制权限而从不增加权限。而在这种情况下,SELinux 的存在实际是增加了权限,大部分部署的 SELinux 都没能关闭这个漏洞(攻击代码的注释中指出,AppAmor 也不会更好)。

此外,当完全不配置安全模块的 时候,mmap_min_addr 也没有被完全强制执行。目前主线(译注:2.6.31)内核有一个补丁来保证 map_min_addr sysctl 设置永远可用,这个补丁同时也被提交进入 2.6.27.27 和 2.6.30.2 更新了(以及其他的地方)。

问题同时也在SELinux层面进行了修复。Red Hat 未来的版本的 SELinux 将不再允许无限制的(在其他方面却无特权的)进程将页面映射到地址空间底端。虽然这里仍然有一些未解决问题,特别是像WINE这样的程序将陷入混乱。目前仍然不确定如何让系统安全地支持一小撮需要映射到0页面的程序。有个想法是让 WINE 以 root 权限运行,这样,或许太过类似 Windows 的行为了,这些想法只收到很少的关注。

另一个关于 map_min_addr 的方法也必须要谈到:一个带有 SVR4 个性化的特权进程在 exec() 的时候,会有一个只读页面映射到0页面。在十分罕见的情况下,一些老的 SVR4 程序会需要那里有这么一个页面,但是,它实际让空指针攻击成为了可能。于是,另一个被并入主线和稳定版更新中的补丁用来在 setuid 的程序运行的时候重置 SVR4 个性化(或者,至少重置那个关于映射0页面的部分)。

这个改变对有些用户仍然不够,他们要求能够完全关闭这个个性化功能。这个能力用于直接运行基于 386 的 Unix 的程序,其实已经没有它 1995 年的时候所拥有的重要性了,所以,很多人质疑是否值得为这个个性化特征付出这样的代价了。Linus 答道

我们可能可以扔掉那个愚蠢的特征。它已经不再那么重要了。有人真的关心么?同时,这么多年来,我们已经发展出了很多其他个性化标志,其中有些仍然是有用的。

特别的,禁止地址空间随机化(一个个性化特征)的能力在很多情况下是很有用的。所以 personality() 应该被留下来,不过,0页映射的特征可能要一去不返了。

这一连串错误中的一个就是被编译器优化掉的空指针校验。这个校验可以阻止攻击,但 GCC 却把它优化掉了,因为理论上说这个指针不应该是空的(因为它已经被正常的解引用过了)。GCC(本身)有一个标志可以禁止某种特殊优化;所以,从现在开始,内核将缺省使用 -fno-delete-null-pointer-checks 编译开关。由于在内核中空指针的确可能是个合法的指针,无限地禁止这种优化史合理的。

有人可能会提出,虽然所有上面的改变都是好的,但可能部分地没有切中要害:高质量的内核不应该在第一次发生的地方允许解引用空指针。这些解引用本身确实是 bug,这里才是最该修正的地方。这有些有趣的历史,实际上内核开发者们常常建议忽略空指针校验。比如,如下 BUG_ON() 代码:

    BUG_ON(some_pointer == NULL);
    /* dereference some_pointer */

经常被以这样的注释而删除掉:

如果我们解引用 NULL,那么内核将基本上会和 BUG 一样显示一个信息,也会采取相同的措施。所以添加一个 BUG_ON 实际无法得到任何收益。

这是因为,解引用空指针会导致一个内核 oops。表面上,这是合理的:如果我们的硬件可以检测到空指针解引用,那么添加软件开销来检测它没什么意义。但这个原因实际上不是无懈可击的,这次的攻击代码就说明了这一点。映射到0页确实是有一定原因的,所以,一个空指针未必总是非法的。你可能会假设所有相关开发者都理解这一点,但实际上内核中仍然有很多地方确实有游泳的空指针检验代码被删除掉了。

虽然如此,大部分内核中的空指针问题可能仅仅就是失察造成的。如果没有方法在相关代码中实际使用空指针的话,大部分的空指针问题是不可被利用的,缺少检验并不能带来什么。不过,如果能把它们全都修复肯定是件好事。

发现这些问题可能可以通过 Smatch 静态代码分析工具。Smatch悄无声息很多年了,不过 Dan Carpenter 正在让它死灰复燃;他最近发布了 Smatch 为他发现的一个空指针 bug。如果 Smatch 能成为一个通用的发现此类错误的工具的话,内核将会更加安全。但不幸的是,这个检查器似乎没有吸引开发者们的很多兴趣,自由软件依旧落后这个领域中的先进技术很远,这让我们都很受伤。

另一个方法由 Kulia Lawall 所采用,她使用一个 Coccinelle “语义补丁”来发现并修复 TUN 驱动中发现的这种先解引用再检查的 bug。一系列补丁()被发布出来,用于修复一系列类似 bug。指针在检查之前被解引用可能只是内核中的空指针问题的一个子集,但每个都被程序员们认为有可能出现空指针,并且是有问题的。所以它们显然是应该被修复的。

总之,这个攻击代码可以看做是内核社区的一个起床号,十分幸运,它将帮助清理很多代码,并消灭很多安全问题。攻击代码的作者 Brad Spengler 还明确的希望得到更多:他经常会发布一些对于内核安全问题的关切:严重的内核安全 bug 被悄悄地修复,或在最差情况下被变为拒绝服务的问题。这个问题是否会改变现状呢,在内核环境中,很多 bug 会造成安全隐患,这些在 bug 被修复的时候并不会立刻显现出来。所以我们无法看到更多 bug 被以安全问题的方式被发布出来,不过幸运的是,我们将会看到更多的 bug 被修复。

[译文] 空指针的乐趣(1)

September 4th, 2009

作者:Jonathan Corbet
原文发布日期:July 20, 2009
来源:http://lwn.net/Articles/342330/
译者:王旭 ( http://wangxu.me , @gnawux )
翻译时间:2009年9月2-3日

现在,大部分读者都已经知道了 Brad Spengler 发布的“the local kernel exploit”(本地内核漏洞利用)。这一漏洞会影响 2.6.30 内核(以及 RHEL5的一个测试版 2.6.18 内核),受到了多方关注。本文将详细分析如何利用这一漏洞,以及让这个漏洞得以成真的令人震惊的一连串错误。

TUN/TAP 驱动提供了一个虚拟的网络设备,它会建立一个隧道;这一驱动在多种场合都有很有用,包括虚拟化、VPN 等很多地方。使用 TUN 时,程序通常打开 /dev/net/tun,然后使用 ioctl() 调用来建立网络端点。Herbert Xu 近来注意到,缺少对包的审计可能会导致恶意程序能够占用大量的内核内存,并导致系统性能下降。他的解决方案是通过一个补丁给该设备添加一个“伪 socket”,使它可以使用内核的审计机制。问题是解决了,但是,回头看来,它的代价是已入了一个更严重问题。

TUN 设备支持 poll() 系统调用。(在 2.6.30 内核中)实现这个功能的函数的开头是这样的:

    static unsigned int tun_chr_poll(struct file *file, poll_table * wait)
    {
	struct tun_file *tfile = file->private_data;
	struct tun_struct *tun = __tun_get(tfile);
	struct sock *sk = tun->sk;
	unsigned int mask = 0;

	if (!tun)
	    return POLLERR;

上面有下划线的的那行代码是 Herbert 的补丁中添加的,这正是惹祸的开始。精心编写的内核代码都会小心的避免对指针的解引用,以避免 NULL;事实上,这里只在那个条件语句那里检查了 tun 指针。并且,这是件好事;现在看来,如果进行了 configuring ioctl() 调用,tun 确实将是 NULL。这时,按照预期,本来 tun_chr_poll() 应该返回一个错误状态。

但 Herbert 的补丁添加的指针解引用是在检查之前的,这显然是一个 bug。在正常操作中,这个 bug 的影响会比较有限,如果 tun 是 NULL 的话,会导致内核 oops。oops 会首先杀掉进行这个系统调用的进程,并将回溯信息加入系统日志,此外就不应该发生什么其他的事情了。最坏情况下,这应该也就是个拒绝服务问题。

依照上述推理,这是一个小问题,虽人 NULL (0)可能确实是个合法的指针地址。缺省的,不论在用户空间还是内核空间,虚拟地址空间的底部(“0页”和它上面的一些页面)都是不允许任何访问的,以用来捕捉空指针错误(如上描述)。不过,使用 mmap() 系统调用将真实的内存映射到虚拟地址空间的底部仍然是可能的。这个功能有一些合法的用例,包括运行一些过时的程序。尽管如此,大部分现代的系统都通过使用 mmap_min_addr sysctl 设置来禁止映射到0页。

安全模块检查被认为可以作为内核已经进行了的检查的一个补充,但这次,它并没有如愿工作。

这一设置应该阻止用户空间的程序区映射零页,这也就保证了空指针的解引用只会导致一次内核的oops。但是,不知何故,如果安全模块机制被配置入内核的话,2.6.30 中的 mmap() 的代码会显示地拒绝执行 mmap_min_addr 。取而代之的是将这个工作留给特定的安全模块来进行。安全模块的检查工作被认为是内核中已有的检查工作的一个补充,但它此时却并不工作。对于 0 页,安全模块会授权访问,而其他情况下则会拒绝访问。这个错误的最后一步是,Red Hat 的缺省 SELinux policy 允许映射 0 页。这样,运行 SELinux 实际是降低了系统的安全性。

但没有 SELinux 的生活也不是就一马平川了。在没有 SELinux 的时候,攻击行为会被 mmap_min_addr 限制,这似乎足够让一切结束了。但这是可以通过使用 personality() 系统调用绕过的。打开 SVR4 个性化会在程序被 exec() 调用的时候将一个只读页面映射到 0 地址,但只有进程有 CAP_SYS_RAWIO 能力的时候才会这样。所以,需要一个更进一步的欺诈行为:顶级的攻击代码设置 SVR4 更兴华,然后使用 exec 运行有特殊插件的 pulseaudio 服务器。pulseaudio 服务器是 setuid root 的,所以它将会在调用时映射到 0 页面。当调用到插件代码的时候,pulseaudio 将会放弃它的权限,但是,这时 0 页已经对攻击代码可用了,攻击代码可以让 0 页可写,并将其自己的数据放在这里。

上面这些攻击的结果就是,用户空间进程是有可能映射0页而不让 tun_chr_poll() 发生内核 oops。不过,你可能会想,攻击者还不能高兴得太早,毕竟接下来 tun 就会检查空指针。这正是这一系列错误中的下一个:GCC编译器缺省会优化掉 NULL 的彻底检验。原因在于,因为这个指针已经被解引用过了(而且也什么都没发生),所以它不可能是 NULL。所以,没有理由再去检查它了。于是,尽管这个逻辑本来在大部分情况下都有效,但是在 NULL 是一个合法指针的时候却是错误的。

所以,攻击者这时就能通过一个空 tun 指针而成功进入 tun_chr_poll() 内部了。接下来需要指出如何利用这种情况控制内核。tun_chr_poll() 中后面的下一步代码是这样的:

	if (sock_writeable(sk) ||
	    (!test_and_set_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags) &&
	     sock_writeable(sk)))
		mask |= POLLOUT | POLLWRNORM;

注意,sk 的值来自于 tun 的解引用,所以它位于攻击者的控制之下。SOCK_ASYNC_NOSPACE 是 0,所以 test_and_set_bit() 调用可以用于设置内存中任何字的最低权重位。这是个小小的内存冲突,但这已经被证实是足够的了,在 Brad 展示的攻击代码中,sk->sk_socket->flags 指针指向了 TUN 驱动的 file_operations 结构;特别的,它是指向了 mmap() 函数。TUN驱动不支持 mmap() 调用,所以这个指针通常应该是 NULL,在 poll() 调用之后,它就是 1 了。

攻击代码的最后一步就是调用这个打开的 TUN 设备的文件描述符的 mmap() 调用。由于内部的 mmap() 已经不是空了(刚刚被我们设置成了 1),内核将会跳到哪里。那个地址已经在攻击代码所映射的0页面中了,所以,它在攻击者的控制之下。于是,攻击代码使用下一个跳转跳到其自己的代码处即可。这样,当内核调用(它以为的)TUN 驱动的 mmap() 函数的时候,结果就是任意代码都可以在内核模式下运行;这里,攻击代码获得了完全的控制权。

在一个良好设计的系统中,一个单独的错误很少导致灾难性的故障。而这里就是这样一个例子。很多东西都出错才导致了这个攻击成为可能:安全模块能够不顾系统策略而授权访问地位内存,SELinux 策略允许这些映射,pulseaudio 可以被攻击代码利用从而让这一映射可以被攻击代码使用,空指针在解引用之前未被检验,并且检验被编译器优化掉了,代码以某种方式使用空指针可以获取系统的控制权。这是一条长长的错误链,其中的每一环节都是让这一攻击成功的必要条件。

这个漏洞如今已经被关闭了,不过几乎可以肯定还有类似的问题。本系列的下一篇文章将会介绍内核开发者们如何应对这一攻击行为。

[译文] Linux 内核设计模式 (1)

August 30th, 2009

作者:Neil Brown
原文发布日期:June 8, 2009
来源:http://lwn.net/Articles/336224/
译者:王旭 ( http://wangxu.me , @gnawux )
翻译时间:2009年8月29-30日

[译注:初稿,未经审校,欢迎意见建议。]

内核社区中,始终受到关注的一个话题便是维持代码质量。显而易见,我们需要维护乃至提高内核的质量,不过,如何能更好的做到这一点却不那么显而易见了。一个已经取得一定成果的普适方法便是提升内核的各个方面的可见性。这将让这些方面的质量更加透明,进而可以提高它们的质量。

可见性的提高以不同的形式发生:

  • checkpatch.pl 脚本会检查代码的各式风格,将不一致的风格高亮标出,这会帮助(还记得使用这个脚本的)人们来修正代码风格上的错误。这样,通过提高代码风格指南的可见性,我们统一了代码的外观,于是在某种意义上说,提高了质量。
  • 在打开“lockdep”系统时,它可以动态计算锁(以及相关状态,比如是否允许中断等)之间的依赖性。如果有什么东西看起来比较异常的话,它会进行报告。这些异常并非总意味着可能死锁或有类似问题,但很多时候确实如此,而且这种死锁可能性是可以被消除掉的。这样,通过提升锁依赖关系图的可见性,可以提高代码质量。
  • 内核还包含着其他的可见性改进,如对不用的内存空间进行“poisoning(下毒)”,这样非法访问将更为明显,或者通过在 stack trace 中直接使用符号名称替代十六进制地址,从而使得 bug 报告能更有意义。
  • 在更高层面上,用于跟踪内核变化的”git”版本管理系统让观察补丁提交的时间和补丁作者变得十分容易。它鼓励让每个补丁的提交者都附加一个说明,由此可以获知为什么代码应该是这样的。这个可见性可以帮助理解代码,并且由于更多开发者能更好地了解代码的含义,从而可以提高质量。

除此之外,还有很多种方法可以提高其他方面的可见性,从而提高代码质量。在本系列文章中,我们将探讨一个特定的领域,这让作者们可以感到可见性可以发生质的改变的方式改进。这个领域就是阐明内核特定的设计模式。

设计模式

“设计模式”这个概念最早见于建筑学,1994年出版的《设计模式:可重用面向对象软件元素》一书将这个概念引入到了计算机工程,特别是面向对象编程领域之中。Wikipedia上有关于此主题的更加深入的介绍

简而言之,设计模式描述了一类特定的设计问题,并描述了已经被验证有效的解决这一类问题的方法细节。设计模式的一个特别的好处是,它将问题描述与解决方法的描述结合在一起并进行命名。一个模式具有一个简单好记的名称非常有好处。如果开发者和审阅者都知道一个模式的名字,那么重要的设计决策就可以用一两个词就做到有效沟通,决策问题也就变得非常具有可视性了。

在 Linux 内核代码中,很多设计模式都已经被证实是有效的了。不过,它们中的大多数都还从来没有被文档化过,对其他开发者来说也就不怎么可用了。我的希望就是显式地描述这些模式,让这些模式被更广泛地使用,进而开发者可以更快更有效的解决常见问题。

在本系列文章的后面的部分,我们将考察三个问题域,并从中发现适用范围和重要性迥然不同的各种设计模式。我们的目标不仅是描述这些模式,而且要介绍这些模式适用的范围和所具有的价值,这样,其他人也可以尝试来描述他们遇到的模式。

这个系列中将引用很多 Linux 内核中的例子,因为例子是解释模式的一个重要部分。如无特殊声明,这些例子都取自2.6.30-rc4。

引用计数

使用引用计数来管理对象的生命周期的想法非常普遍。核心思想就是使用一个计数器,当有一个新引用的时候就加一,释放引用的时候就减一。当计数器到达0的时候,这个对象所占用的所有资源(如用于存储它本身的内存)就都可以释放了。

管理引用计数的机制看起来非常直接。不过,实际上有一些陷阱会使得它非常容易出错。部分的处于这个原因,Linux 内核(自从 2004 年)就有了一个“kref”类型和一组相关机制(参考 Documentation/kref.txt, <linux/kref.h>, 和 lib/kref.c)。这些方法封装了一部分陷阱,特别的,让一个计数器明确地作为引用计数被以一种特定的方式来使用。如上所述,设计模式的名称非常有价值,开发者只提供要使用的设计模式的名字对于审阅者非常有好处。

Andrew Morton 语录

我更希望开发者们能这么说:“啊,这里用了kref。这样,我理解它用了 refcounting,我知道它被很好的 debug 了,并且知道它能处理一般的错误了。”这比下面这样强多了:“哦,这个东西实现了自己的refcounting — 这样我就得在这里对常见错误进行审阅”。

Linux 内核中引入 kref  为其显式支持设计模式既打了一个勾也打了一个叉。勾就是 kref 就是具体实现了一个重要的设计模式,良好的文档化,并在使用的时候使得代码清晰可见。而这个叉则是因为 kref 仅仅封装了部分关于引用计数的内容。有些引用计数的应用并未被 kref 模型所惠及,稍后我们将看到。一个没有提供所需的方法的引用计数具有“blessed”机制实际可以导致错误,因为人们可能会在 kref 不该使用的地方使用它,在实际上它不能工作的地方认为它可以工作。

了解引用计数的复杂性的有用的第一步是要了解,经常有两类截然不同的引用指向某个对象。事实上,可能会有三类甚至更多,但这并不常见,并且可以被理解成为更广义的两类的情况。我们将这两类引用称为“外部的”和“内部的”,虽然有的场合“强的”和“弱的”可能更合适一些。

“外部”引用是指我们最习惯于考虑到的引用。外部引用在“get”与“put”的时候被计数,可以被与管理对象的子系统相去甚远的子系统所持有。一个对象的外部引用的存在代表着一个强烈而简单的含义:对象在使用中。

与之相对应的,“内部”引用常常会被忽略,它仅在管理对象的系统内部(或与其密切相关的系统内)持有。不同的内部引用具有不同的含义,因此实现的含义也十分不同。

最常见的内部引用的例子是提供“按名称查询”服务的缓存。如果你知道了一个对象的名称,那么只要缓存中确实存在这个对象,就可以通过缓存来得到一个外部引用。这个缓存会在一个列表或一组链表中的一个链表,如一个哈希表中保存每个对象。这个列表中存在的对象实际是一个对对象的引用。但是它却不应该是一个被计数的引用。它不具有“对象在使用中”这样的语义,而仅仅是“当有人想用这个对象的时候,它是可用的”。在所有外部引用被释放之前,对象将一直不会被从列表中删除,而且即使所有外部引用都被释放的时候,它也可能不会被立刻从列表中删除。内部引用的存在和本意喻示着着引用计数的实现方式。

一个有用的对不同引用计数风格进行分类的方法是通过其所需要的“put”操作的实现来进行区分。“get”方法一般是一样的。它获取一个外部引用,并产生了另一个外部引用。它的实现差不多是这样的:

    assert(obj->refcount > 0) ; increment(obj->refcount);

或者如 Linux 内核中的 C 代码:

    BUG_ON(atomic_read(&obj->refcnt)) ; atomic_inc(&obj->refcnt);

注意,“get”不能被用于一个已经被释放了的对象。还需要一些其他的处理。

“put” 操作有三个变种。尽管在用例上还有一些重叠,但区分这三者对于保持代码的简洁是有好处的。这三种方式以 Linux C 的写法是这样的:

   1      atomic_dec(&obj->refcnt);

   2      if (atomic_dec_and_test(&obj->refcnt)) { ... do stuff ... }

   3      if (atomic_dec_and_lock(&obj->refcnt, &subsystem_lock)) {
                 ..... do stuff ....
		 spin_unlock(&subsystem_lock);
	  }
“kref” 风格

从中间一种方法开始,第2项是 kref 使用的风格。这个风格适用于对象没有其外部引用活得长的情况。当引用计数到达0的时候,对象需要被释放,否则就要进行处理,这就需要使用 atomic_dec_and_test() 来检查引用计数为为0的情况。

符合这一风格的对象通常都没有需要顾虑的内部引用,大部分 sysfs 中的对象就是这样,它们也使用了大量的 kref。但如果一个使用 kref 风格引用计数的对象有内部引用,那么它就不允许用一个内部引用来创建外部引用,除非能确定仍有其它的外部引用。如果有此必要,可以使用如下原语:

     atomic_inc_not_zero(&obj->refcnt);

这样,在计数器不为零的情况下自加,返回值指示操作是否成功。在 linux 内核中,atomic_inc_not_zero() 是一个较新引入的操作,在2005年末作为不加锁的 page cache 的一部分引入。因而,这个原语的使用还不够广泛,一些本可以使用这个原语的代码使用了自旋锁。遗憾的是,kref 包也没使用它。

这个风格的引用有一个没用 kref 的有意思的例子,甚至连 atomic_dec_and_test() 都没有用(虽然实际可以用并且确实应该用),这就是 struct super 里面的两个引用计数:s_counts_active

s_active 非常符合 kref 的风格。超级块的生命周期开始时 s_active 为1(alloc_super() 中设置),并且,当 s_active 变成零之后,就无法再获取外部引用了。这个规则位于 grab_super(),虽然超级块没有立刻被清除。当前的代码(由于历史原因)在 s_active 非零时给加上一个很大的数(S_BIAS),而 grab_super() 去检查 s_count 是否超过 S_BIAS 而不是 s_active 是否为零。后面这次检查实际上可以直接使用 atomic_inc_not_zero(),从而避免使用自旋锁。

s_count 提供了另一类不同类型的引用,既是内部引用,又是外部引用。它的内部。从语义远弱于 s_active 方面说,它是内部引用计数。s_count 引用计数的意思仅在于“超级块还不能立刻被释放”,而不检查它是否真的处于 active 状态。而它却又非常类似 kref 那样,从 1 开始生命周期(实际是 1*S_BIAS),当它到 0 的时候(在 __put_super() 中)超级块就被销毁了,从这个意义上讲又是外部引用计数。

只要进行如下操作,这两个引用计数就可以用两个 kref 所代替:

  • S_BIAS 置为 1
  • grab_super() 使用 atomic_inc_not_zero() 原语,而不是和 S_BIAS 进行比较

这样,很多自旋锁都可以不用了。具体细节可以作为练习留给各位读者了。

“kcref” 风格

Linux 内核并没有 “kcref” 对象,但这个名字似乎适用于接下来的这种引用计数风格。“c”是指的“缓存(cached)”,这种风格经常用于缓存之中。所以可以称为 Kernel Cached REFerence。

kcref 引用计数如上面第三种情况使用 atomic_dec_and_lock() 。这是因为,最后一次 put 的时候,需要释放掉资源或检验是否需要其他特定的处理。这需要进行一次加锁来保证当前状态被重新求值期间不会再产生新的引用。

一个简单的例子是 struct inode 中的 i_count 引用计数。iput() 中的主要部分是这样的:

    if (atomic_dec_and_lock(&inode->i_count, &inode_lock))
	iput_final(inode);

其中 iput_final() 检验 inode 的状态,并决定它是否可以被销毁,或是还要留在缓存中,以备稍后重用。

特别的,inode_lock 会阻止从 inode 哈希表中的内部引用建立外部引用。由于这个原因,仅有持有 inode_lock 的时候才能将内部引用转化为外部引用。支持这个操作的函数称为 iget_locked() (或 iget5_locked())。

略有一点复杂的例子是 struct dentry,这里的 d_count 是用的类似 kcref 的方式管理的。它更复杂一些的原因在于在我们获得引用之前,需要先获取两个锁—— dcache_lockde->d_lock。这要求我们必须先获得一个锁,然后再用 atomic_dec_and_lock() 来获取另一个(如 prune_one_dentry 之中),或者先用 atomic_dec_and_lock() 然后请求另一个锁并重新检查引用计数,如 dput() 中的操作。这是一个很好的例子,它说明了你永远不能肯定你已经封装了所有的可能的引用计数风格。需要两个锁的情况很难被预见到。

一个更复杂的例子是 struct vfsmount 中的 mnt_count。这里的复杂性源于两个引用计数的相互影响:mnt_count 是一个典型的外部引用计数,而 mnt_pinned 是进程审计模块的内部引用计数。特别地,它统计文件系统中打开的审计文件的数量(实际应该使用个更贴切的名字)。复杂性来自于当只有内部引用存在的时候,它们将全被转化为外部引用。关于这个例子的细节同样留作练习,留给感兴趣的读者。

“plain”风格

最后一种引用计数涉及到的风格就是直接对引用计数进行减值操作(atomic_dec())而不做其他任何事情。这个风格在内核中并不常见,必须有充分的理由才会使用。毕竟随意放着一个无人饮用的对象不是个好主意。

这个风格的一个例子出现在 struct buffer_head 中,位于 fs/buffer.c<linux/buffer_head.h>。函数非常简单 put_bh()

    static inline void put_bh(struct buffer_head *bh)
    {
        smp_mb__before_atomic_dec();
        atomic_dec(&bh->b_count);
    }

这样做没什么问题,因为 buffer_heads 有它们自己的生命周期管理规则,他们是紧密和页相关的。一个页面中会分配出一个或多个 buffer_heads,以分成小片(buffer)。它们会被保存着,直到页面本身被释放,这时,所有的 buffer_heads 都会被清除(通过 try_to_free_buffers() 调用 drop_buffers())。

通常,”plain”风格适用于那些内部引用会一直存在,对象不会丢的情况,这个内部引用的所在的进程会最终找到并释放对象。

反模式(Anti-patterns)

现在要回顾一下这个引用计数来作为设计模式的介绍,我们将要讨论反模式的相关概念。设计模式都是已经经过检验可以工作的方法,并应该鼓励使用,而反模式则是那些已经证明不能良好工作,并应被慎用的。

作者建议把在引用计数中使用”偏置(bias)“作为一个反模式的例子。偏置是一个大数,在引用计数中进行加减,它被用于保存一些信息。我们已经在超级块的 s_count 中见过这种方式了。在这个例子中,偏置的存在表示 s_active 是非零值,这很容易直接检验。所以偏置实际没有任何价值,只是使得代码的真实用意更不清楚了。

另一个使用偏置的例子是 fs/sysfs/sysfs.hfs/sysfs/dir.c 中的 struct sysfs_dirent 。十分有趣,struct sysfs_dirent 和超级块一样,有两个以用,也是叫做 s_counts_active。这里,当选项被去激活时,s_active 有一个大的负数偏置。同样信息可以被有效并更清晰地存储在一个标志字 s_flags 中。在标识中存放一些信息比用偏置的方法存放在计数器中更为易懂,也更应该被推荐。

总之,使用偏置不能增加清晰度,不是一个常用模式。它不能比一个单独的标志位提供更多的信息,极端缺少内存、无法发现其它可用内存的情况下使用偏置来存放信息的情况非常罕见。因此,引用计数中,偏置应该被视为反模式并尽力避免使用。

总结

到时候结束我们对各种引用计数相关的设计模式的介绍了。简单列出如”kref”与”kcref”,”外部“与”内部“引用这样的术语是非常有帮助的。像我们一样找到 kref 和可以用 kcref 的代码,并在所有可用的地方使用它们,这对开发者和审阅者都有好处,开发者可以在一开始就找到正确的方法,而审阅者也更容易知道代码的用意。

我们本文中涉及的设计模式包括

  • kref: 当对象的生命周期仅仅延续到最后一次释放外部引用的时候,kref 是恰当的设计模式。如果对象有内部引用,那么它们只有使用 atomic_inc_not_zero() 才能变成外部引用。例子:struct super_block 中的s_actives_count
  • kcref: 如果对象的生命周期超出最后一次释放外部引用,那么应该使用 atomic_dec_and_lock() 和 kcref。内部引用只有在获得了子系统锁的时候才能转化为外部引用。例如:i_count 中的 i_count
  • plain: 当对象的生命周期挂靠在其他对象之上的时候,应该采用plain 引用模式。对象的非零引用计数必须被看作是对父对象的内部引用,内部引用转化为外部引用必须遵循和父对象相同的规则。例子:struct buffer_head 中的 b_count
  • biased-reference: 当你想要在引用计数中使用一个大的偏移值来表征一些特殊状态的时候,停下,别这么干。使用个别的标志位吧,这是反模式。

下个星期我们将换一个领域看其中 Linux 内核已经证明了的成功的设计模式,并看一些复杂的数据结构略多的地方。

练习

作者在准备这个系列的时候就被提醒到,没有比直接研究代码更能让人理解这些问题了。所以也给感兴趣的读者留了一些练习。

  1. 使用 kref 在 struct super 中替换 s_active,抛弃 S_BIAS。比较使用 trifecta 进行正确性、可维护性和性能检查的结果 。
  2. mnt_pinned 和处理它的相关函数选择一个更贴切的名字。
  3. 给 kref 库添加一个使用 atomic_inc_not_zero() 的函数,并使用它(或相反的操作)去掉 net/sunrpc/svcauth.c 中使用的atomic_dec_and_lock() ,这里它破坏了 kref 的抽象。
  4. 检查 struct page (如 mm_types.h 中) 的 _count 引用计数,观察它的行为更像 kref 还是 kcref (提示:肯定不是 plain)。这应该包括标记所有的内部引用和相关的锁定规则。指明为什么 page cache (struct address_space.page_tree) 有一个引用计数或为什么不应该有。浙江包括理解 page_freeze_refs() 和它在 __remove_mapping() 中的使用,以及 page_cache_{get,add}_speculative()。

补充:一个系列的最小的自包含的补丁来实现上述这些研究结果的改变被证实是有用的。

Switch to our mobile site