微薄后台架构浅析

之前在网上看到关于Twitter、Sina以及腾讯微薄的一些实现技术,这个简单做个摘要。

1、读扩散还是写扩散?
inbox: 收件箱,你收到的消息,即你所关注的人发布的消息。
outbox: 发件箱,你发布的消息。

写扩散(Push)

该方式为每个用户维护一个订阅列表,记录该用户订阅的消息索引(一般为消息ID、类型、发表时间等一些元数据)。每当用户发布消息时,都会去更新其follower的订阅列表。
优点:读很轻。初始化时仅需要读取自己的inbox即可。
缺点:写很重。每发布一个消息,会导致大量的写操作。
注:一般来说,用户发布消息,并不会更新所有followers的订阅列表,仅更新在线followers即可。

读扩散(Pull)

该方式为每个用户维护一个发送列表,记录该用户所有发表过的消息索引。
优点:写很轻,节省空间。用户每发布一条消息,仅需更新自己的outbox。
缺点:读操作很重,计算量大。假设你收听了1k用户,则初始化时,需要从1k个用户的outbox拉取消息,然后计算获得最新的n条消息。

混合模式(Push+Pull)

该方式既为读写扩散的结合,根据用户followers的数量来决定是读扩散还是写扩散。例如followers大于1k的,则使用读扩散,否则使用写扩散。

从目前现在网上的一些资料来看,Twitter是写扩散,腾讯微薄是读扩散,新浪微薄则是二者结合。

2、关于Cache

对于一个千万级甚至亿级用户的大型网站来说,合理使用Cache至关重要。

一个用户的核心数据由如下几个部分组成:inbox,outbox,关系链,消息内容。

inbox:主要缓存消息索引,仅为在线用户缓存,从Timyang的PPT来看,新浪微薄估计是使用redis的list或set实现。
outbox:缓存一定数量(例如200)条最近发表的消息。
关系链:following相对于followers来说,缓存容易些,follower加载开销比较大,例如刘翔在腾讯微薄的听众超过1600万。
消息内容:按内容年龄缓存;一般需要多份支持容灾;需要缓存xml,json,rss,atom等多种格式的缓存以供API使用。
以Twitter为例,其将Cache分为四类:Vector Cache,Row Cache,Page Cache,Fragment Cache,均使用memcached实现。其中:

Vector Cache主要缓存用户的inbox以及outbox索引,其命中率高达99%;
Row Cache主要缓存用户关系链数据,以及Tweets内容,命中率为95%;
Fragment Cache缓存Tweet的xml,json,rss,atom四种格式数据,以供API使用,命中率为95%;
Page Cache主要原来缓存那些高人气用户的个人主页,命中率仅为40%。
下图为TwitterCache架构图:

Twitter为啥要为API通道设置Fragment Cache和Page Cahce呢?其原因是Twitter的80%流量来自API。

下面以新浪微薄介绍一下Cache流程:

消息发布流程:

更新自己的outbox
加载followers列表
更新followers inbox
获取首页流程:

检查inbox cache是否可用
获取关注列表
聚合内容,获得消息索引
根据索引,返回最终聚合的消息内容
3、关于存储

目前Twitter和新浪的落地存储,都是使用MySQL。而腾讯微薄则使用采用SSD+大文件存储(每次写操作都是append操作,写操作可以先用内存缓存,达到适当大小合并,尽量减少随机写)。其他细节因不清楚或不方便透露,不做细述。

4、关于洪峰处理。

一般用异步队列处理方式。消息队列产品有:Kestrel(twitter使用Scala实现),RabbitMQ(使用Erlang实现),MemcacheQ。

Twitter 09年时,用户的平均followers数量为126个。按照每秒400消息发布数算,那每秒就需要推送126*400=50400条消息出去。为了削峰,Twitter自己用Scala实现了一个分布式消息队列Kestrel,其代码仅为1200行,运行在3台机器上,其使用memcached协议,其Server之间无共享状态,且全内存。新浪使用的是MemcacheQ。

基于PHP的mongodb管理工具RockMongo

PHP5写的一个MongoDB管理工具。
最新版本:v1.1.0 – 2011-04-25
项目地址:http://code.google.com/p/rock-php/

  1. 下载安装包
  2. 解压到你的网站目录下
  3. 用编辑器打开config.php (v1.0.5版本以前是index.php),修改host, port, admins等参数
  4. 在浏览器中访问index.php,比如说:http://localhost/rockmongo/index.php
  5. 使用用户名和密码登录,默认为”admin”和”admin”
  6. 开始玩转MongoDB!

BSON——易于数据交换的格式

BSON是由10gen开发的一个数据格式,目前主要用于MongoDB中,是MongoDB的数据存储格式。BSON基于JSON格式,选择JSON进行改造的原因主要是JSON的通用性及JSON的schemaless的特性。

BSON主要会实现以下三点目标:

1.更快的遍历速度

对JSON格式来说,太大的JSON结构会导致数据遍历非常慢。在JSON中,要跳过一个文档进行数据读取,需要对此文档进行扫描才行,需要进行麻烦的数据结构匹配,比如括号的匹配,而BSON对JSON的一大改进就是,它会将JSON的每一个元素的长度存在元素的头部,这样你只需要读取到元素长度就能直接seek到指定的点上进行读取了。

2.操作更简易

对JSON来说,数据存储是无类型的,比如你要修改基本一个值,从9到10,由于从一个字符变成了两个,所以可能其后面的所有内容都需要往后移一位才可以。而使用BSON,你可以指定这个列为数字列,那么无论数字从9长到10还是100,我们都只是在存储数字的那一位上进行修改,不会导致数据总长变大。当然,在MongoDB中,如果数字从整形增大到长整型,还是会导致数据总长变大的。

3.增加了额外的数据类型

JSON是一个很方便的数据交换格式,但是其类型比较有限。BSON在其基础上增加了“byte array”数据类型。这使得二进制的存储不再需要先base64转换后再存成JSON。大大减少了计算开销和数据大小。

当然,在有的时候,BSON相对JSON来说也并没有空间上的优势,比如对{“field”:7},在JSON的存储上7只使用了一个字节,而如果用BSON,那就是至少4个字节(32位)

目前在10gen的努力下,BSON已经有了针对多种语言的编码解码包。并且都是Apache 2 license下开源的。并且还在随着MongoDB进一步地发展。

MongoDB接口MongoDB::execute()测试

了解过Mongo的朋友都知道,Mongo客户端对CURD的常用方法是经过封装的,不像MySQL,所有的操作都通过mysql_query()一个入口。
其实Mongo也有提供一个基于BSON的接口MongoDB::execute()可以直接执行命令。如:
PHP代码:

$mo = new Mongo;
$ret = $db->execute("db.hdj_messages.count();");
dump($ret['retval']);
$ret = $mo->test->execute('db.hdj_messages.getName()');
var_dump($ret['retval']);
$ret = $mo->test->execute("db.hdj_messages.findOne();");
var_dump($ret['retval']);

结果:

float(2)

string(12) "hdj_messages"

array(5) {
  ["_id"] => object(MongoId)#8 (1) {
    ["$id"] => string(24) "4eb732b564a637ac607fbf41"
  }
  ["addtime"] => string(19) "2011-11-07 09:23:00"
  ["content"] => string(4) "test"
  ["title"] => string(12) "test_title_1"
  ["views"] => int(1)
}

通过测试

db.hdj_messages.insert();
db.hdj_messages.save();
db.hdj_messages.stats();
db.hdj_messages.getName();

等大部份操作都能执行成功,但还是有一部分操作是不支持的,应该说不直接支持,例如:db.hdj_messages.find()返回的不是一个数组,而是一个指针对象,所以直接这样执行是不行的。
那要怎么办才能让它正常支持呢?答案很简单,只要把find()返回的结果转换为array类型就可以,直接看代码吧:
PHP代码:

$mo = new Mongo;
$ret = $mo->test->execute('db.hdj_messages.find().toArray()');
var_dump($ret['retval']);

结果:

array(2) {
  [0] => array(5) {
    ["_id"] => object(MongoId)#8 (1) {
      ["$id"] => string(24) "4eb732b564a637ac607fbf41"
    }
    ["addtime"] => string(19) "2011-11-07 09:23:00"
    ["content"] => string(4) "test"
    ["title"] => string(12) "test_title_1"
    ["views"] => int(1)
  }
  [1] => array(4) {
    ["_id"] => object(MongoId)#9 (1) {
      ["$id"] => string(24) "4eb73c5f7de6444c0e000000"
    }
    ["title"] => string(4) "aaaa"
    ["content"] => string(3) "bbb"
    ["addtime"] => string(19) "2011-11-07 10:03:11"
  }
}

后来经测试,MongoDB::execute()与Mongo提供的API效率相差挺大的,findOne()操作效率比大概为2:1,find()效率比大概是30:1。

从SQL过渡至MongoDB查询对照表

SQL 说明 Mongo 说明
CREATE TABLE USERS (a Number, b Number) Implicit or use MongoDB::createCollection().
INSERT INTO USERS VALUES(1,1) $db->users->insert(array(“a” => 1, “b” => 1));
SELECT a,b FROM users $db->users->find(array(), array(“a” => 1, “b” => 1));
SELECT * FROM users WHERE age=33 $db->users->find(array(“age” => 33));
SELECT a,b FROM users WHERE age=33 $db->users->find(array(“age” => 33), array(“a” => 1, “b” => 1));
SELECT a,b FROM users WHERE age=33 $db->users->find(array(“age” => 33), array(“a” => 1, “b” => 1));
SELECT a,b FROM users WHERE age=33 ORDER BY name $db->users->find(array(“age” => 33), array(“a” => 1, “b” => 1))->sort(array(“name” => 1));
SELECT * FROM users WHERE age>33 $db->users->find(array(“age” => array(‘$gt’ => 33)));
SELECT * FROM users WHERE age<33 $db->users->find(array(“age” => array(‘$lt’ => 33)));
SELECT * FROM users WHERE name LIKE “%Joe%” $db->users->find(array(“name” => new MongoRegex(“/Joe/”)));
SELECT * FROM users WHERE name LIKE “Joe%” $db->users->find(array(“name” => new MongoRegex(“/^Joe/”)));
SELECT * FROM users WHERE age>33 AND age<=40 $db->users->find(array(“age” => array(‘$gt’ => 33, ‘$lte’ => 40)));
SELECT * FROM users ORDER BY name DESC $db->users->find()->sort(array(“name” => -1));
CREATE INDEX myindexname ON users(name) $db->users->ensureIndex(array(“name” => 1));
CREATE INDEX myindexname ON users(name,ts DESC) $db->users->ensureIndex(array(“name” => 1, “ts” => -1));
SELECT * FROM users WHERE a=1 and b=’q’ $db->users->find(array(“a” => 1, “b” => “q”));
SELECT * FROM users LIMIT 10 SKIP 20 $db->users->find()->limit(10)->skip(20);
SELECT * FROM users WHERE a=1 or b=2 $db->users->find(array(‘$or’ => array(array(“a” => 1), array(“b” => 2))));
SELECT * FROM users LIMIT 1 $db->users->find()->limit(1);
EXPLAIN SELECT * FROM users WHERE z=3 $db->users->find(array(“z” => 3))->explain()
SELECT DISTINCT last_name FROM users $db->command(array(“distinct” => “users”, “key” => “last_name”));
SELECT COUNT(*y) FROM users $db->users->count();
SELECT COUNT(*y) FROM users where AGE > 30 $db->users->find(array(“age” => array(‘$gt’ => 30)))->count();
SELECT COUNT(AGE) from users $db->users->find(array(“age” => array(‘$exists’ => true)))->count();
UPDATE users SET a=1 WHERE b=’q’ $db->users->update(array(“b” => “q”), array(‘$set’ => array(“a” => 1)));
UPDATE users SET a=a+2 WHERE b=’q’ $db->users->update(array(“b” => “q”), array(‘$inc => array(“a” => 2)));
DELETE FROM users WHERE z=”abc” $db->users->remove(array(“z” => “abc”));

从MySQL到MongoDB(添加MongoDB循环插入数据方法)

MySQL中的常用SQL语句在MongoDB中的写法,如果你长期使用MySQL而对MongoDB跃跃欲试,这篇简单的文章可以帮助你更快的进入角色。
查询:
MySQL:

SELECT * FROM user

Mongo:

db.user.find()

MySQL:

SELECT * FROM user WHERE name = ‘starlee’

Mongo:

db.user.find({‘name’ : ‘starlee’})

继续阅读从MySQL到MongoDB(添加MongoDB循环插入数据方法)