All posts by dotte

案例学习:仅使用Redis+PHP设计实现一个简单的Twitter

原文位于Redis官网http://redis.io/topics/twitter-clone

Redis是NoSQL数据库中一个知名数据库,在新浪微博中亦有部署,适合固定数据量的热数据的访问。

作为入门,这是一篇很好的教材,简单描述了如何使用KV数据库进行数据库的设计。新的项目www.xiayucha.com亦采用Redis + MySQL进行开发,考虑Redis文档比较少,故翻译了此文。

其他参考资料:

我会在此文中描述如何使用PHP以及仅使用Redis来设计实现一个简单的Twitter克隆。
很多编程社区常认为KV储存是一个特别的数据库,在web应用中不能替代关系数据库。
本文尝试证明这恰恰相反。

这个twitter克隆名为Retwis,结构简单,性能优异,能很轻易地用N个web服务器和Redis服务器以分布式架构。
在此获取源码http://code.google.com/p/redis/downloads/list
我们使用PHP作为例子是因为它能被每个人读懂,也能使用Ruby、Python、Erlang或其他语言获取同样(或者更佳)的效果。

注意:Retwis-RB是一个由Daniel Lucraft用Ruby与Sinatra写的Retwis分支!
此文全部代码在本页尾部的Git repository链接里。
此文以PHP为例,但是Ruby程序员也能检出其他源码。他们很相似。

注意Retwis-J是Retwis的一个分支,由Costin Leau以Java和Spring框架写成。
源码能在GitHub找到,并且在springsource.org有综合的文档。

Key-value 数据库基础

KV数据的精髓,是能够把value储存在key里,此后该数据仅能够通过确切的key来获取,无法搜索一个值。
确切的来讲,它更像一个大型HASH/字典,但它是持久化的,比如,当你的程序终止运行,数据不会消失。
比如我们能用SET命令以key foo 来储存值 bar
 SET foo bar
Redis会永久储存我们的数据,所以之后我们可以问Redis:“储存在key foo里的数据是什么?”,Redis会返回一个值:bar
 GET foo => bar
KV数据库提供的其他常见操作有:DEL,用于删除指定的key和关联的value;
SET-if-not-exists (在Redis上称为SETNX )仅会在key不存在的时候设置一个值;
INCR能够对指定的key里储存的数字进行自增。
 SET foo 10
 INCR foo => 11
 INCR foo => 12
 INCR foo => 13

原子操作

目前为止它是相当简单的,但是INCR有些不同。设想一下,为什么要提供这个操作?毕竟我们自己能用以下简单的命令实现这个功能:
 x = GET foo
 x = x + 1
 SET foo x
问题在于要使上面的操作正常进行,同时只能有一个客户端操作x的值。看看如果两台电脑同时操作这个值会发生什么:
 x = GET foo (返回10)
 y = GET foo (返回10)
 x = x + 1 (x现在是11)
 y = y + 1 (y现在是11)
 SET foo x (foo现在是11)
 SET foo y (foo现在是11)
问题发生了!我们增加了值两次,本应该从10变成12,现在却停留在了11。这是因为用GET和SET来实现INCR不是一个原子操作(atomic operation)。
所以Redis\memcached之类提供了一个原子的INCR命令,服务器会保护get-increment-set操作,以防止同时的操作。
让Redis与众不同的是它提供了更多类似INCR的方案,用于解决模型复杂的问题。
因此你可以不使用任何SQL数据库、仅用Redis写一个完整的web应用,而不至于抓狂。

超越Ke-Value数据库

本节我们会看到构建一个Twitter克隆所需Redis的功能。首先需要知道的是,Redis的值不仅仅可以是字符串(String)。
Redis的值可以是列表(Lists)也可以是集合(Sets),在操作更多类型的值时也是原子的,所以多方操作同一个KEY的值也是安全的。
让我们从一个Lists开始:
 LPUSH mylist a (现在mylist含有一个元素:’a’的list)
 LPUSH mylist b (现在mylist含有元素’b,a’)
 LPUSH mylist c (现在mylist含有’c,b,a’)
LPUSH的意思是Left Push, 就是把一个元素加在列表(list)的左边(或者说头上)。
在PUSH操作之前,如果mylist这个键(key)不存在,Redis会自动创建一个空的list。
就像你能想到的一样,同样有个RPUSH操作可以把元素加在列表(list)的右边(尾部)。
这对我们复制一个twitter非常有用,例如我们可以把用户的更新储存在username:updates里。
当然,我们也有相应的操作来获取数据或者信息。比如LRANGE返回列表(list)的一个范围内的元素,或者所有元素
 LRANGE mylist 0 1 => c,b
LRANGE使用从零开始的索引(zero-based indexes),第一个元素的索引是0,第二个是1,以此类推。该命令的参数是:LRANGE key first-index last-index
参数last index可以是负数,具有特殊的意义:-1是列表(list)的最后一个元素,-2是倒数第二个,以此类推。
所以,如果要获取整个list,我们能使用以下命令:
 LRANGE mylist 0 -1 => c,b,a
其他重要的操作有LLEN,返回列表(list)的长度,LTRIM类似于LRANGE,但不仅仅会返回指定范围内的元素,而且还会原子地把列表(list)的值设置这个新的值。
我们将会使用这些list操作,但是注意阅读Redis文档来浏览所有redis支持的list操作。

数据类型:集合(set)

除了列表(list),Redis还提供了集合(sets)的支持,是不排序(unsorted)的元素集合。
它能够添加、删除、检查元素是否存在,并且获取两个结合之间的交集。当然它也能请求获取集合(set)里一个或者多个元素。
几个例子可以使概念更为清晰。记住:SADD是往集合(set)里添元素;SREM是从集合(set)里删除元素;SISMEMBER是检测一个元素是否包含在集合里;SINTER用于显示两个集合的交集。
其他操作有,SCARD用于获取集合的基数(集合中元素的数量);SMEMBERS返回集合中所有的元素
 SADD myset a
 SADD myset b
 SADD myset foo
 SADD myset bar
 SCARD myset => 4
 SMEMBERS myset => bar,a,foo,b
注意SMEMBERS不会以我们添加的顺序返回元素,因为集合(Sets)是一个未排序的元素集合。如果你要储存顺序,最好使用列表(Lists)取而代之。以下是基于集合的一些操作:
 SADD mynewset b
 SADD mynewset foo
 SADD mynewset hello
 SINTER myset mynewset => foo,b
SINTER能够返回集合之间的交集,但并不仅限于两个集合(Sets),你能获取4个、5个甚至1000个集合(sets)的交集。
最后,让我们看下SISMEMBER是如何工作的:
 SISMEMBER myset foo => 1
 SISMEMBER myset notamember => 0
Okay,我觉得我们可以开始coding啦!

先决条件

如果你还没下载,请前往<http://code.google.com/p/redis/downloads/list>下载Retwis的源码。它包含几个PHP文件,是个简单的tar.gz文件。
实现的非常简单,你会在里面找到PHP客户端(redis.php),用于redis与PHP的交互。该库由Ludovico Magnocavallo(http://qix.it/ )编写,你可以在自己的项目中免费使用。
但如果要更新库的版本请下载Redis的发行版。(注意:现在有更好的PHP库了,请检查我们的客户端页面<http://redis.io/clients>)
你需要的另一个东西是正常运行的Redis服务器。仅需要获取源码、用make编译、用./redis-server就完工了,点儿也不须配置就可以在你的电脑上运行Retwis。

数据结构规划

当使用关系数据库的时候,这一步往往是在设计数据表、索引的表单里处理。我们没有表,那我们设计什么呢? 我们需要确认物体使用的key以及key采用的类型。
让我们从用户这块开始设计。当然了,首先需要展示用户的username, userid, password, followers,自己follow的用户等。第一个问题是:如何在我们的系统中标识一个用户?
username是个好主意,因为它是唯一的。不过它太大了,我们想要降低内存的使用。如果我们的数据库是关系数据库,我们能关联唯一ID到每一个用户。每一个对用户的引用都通过ID来关联。
做起来很简单,因为我们有我们的原子的INCR命令!当我们创建一个新用户,我们假设这个用户叫”antirez”:
 INCR global:nextUserId => 1000
 SET uid:1000:username antirez
 SET uid:1000:password p1pp0
我们使用global:nextUserId为键(Key)是为了给每个新用户分配一个唯一ID,然后用这个唯一ID来加入其他key,以识别保存用户的其他数据。这就是kv数据库的设计模式!请牢记于心,
除了已经定义的KEY,我们还需要更多的来完整定义一个用户,比如有时需要通过用户名来获取用户ID,所以我们也需要设置这么一个键(Key)
 SET username:antirez:uid 1000
一开始看上去这样很奇怪,但请记住我们只能通过key来获取数据!这不可能告诉Redis返回包含某值的Key,这也是我们的强处。
用关系数据库方式来讲,这个新实例强迫我们组织数据,以便于仅使用primary key访问任何数据。

关注\被关注与更新

这也是在我们系统中另一个重要需求.每个用户都有follower,也有follow的用户.对此我们有最佳的数据结构!那就是…..集合(Sets).那就让我们在结构中加入两个新字段:
 uid:1000:followers => Set of uids of all the followers users
 uid:1000:following => Set of uids of all the following users
另一个重要的事情是我们需要有个地方来放用户主页上的更新。这个要以时间顺序排序,最新的排在旧的前面。所以,最佳的类型是列表(List)。
基本上每个更新都会被LPUSH到该用户的updates key.多亏了LRANGE,我们能够实现分页等功能。请注意更新(updates)和帖子(posts)讲的是同一个东西,实际上更新(updates)是有点小的帖子(posts)。
 uid:1000:posts => a List of post ids, every new post is LPUSHed here.

验证

OK,除了验证,或多或少我们已经有了关于该用户的一切东西。我们处理验证用一个简单而健壮(鲁棒)的办法:我们不使用PHP的session或者其他类似方式。
我们的系统必须是能够在不同不同服务器上分布式部署的,所以一切状态都必须保存在Redis里。所以我们所需要的一个保存在已验证用户cookie里的随机字符串。
包含同样随机字符串的一个key告诉我们用户的ID。我们需要使用两个key来保证这个验证机制的健壮性:
 SET uid:1000:auth fea5e81ac8ca77622bed1c2132a021f9
 SET auth:fea5e81ac8ca77622bed1c2132a021f9 1000
为了验证一个用户,我们需要做一些简单的工作(login.php):
* 从登录表单获取用户的用户名和密码
* 检查是否存在一个键 username:<username>:uid
* 如果这个user id存在(假设1000)
* 检查 uid:1000:password 是否匹配,如果不匹配,显示错误信息
* 匹配则设置cookie为字符串”fea5e81ac8ca77622bed1c2132a021f9″(uid:1000:auth的值)
实例代码:

PHP代码
  1. include(“retwis.php”);   
  2.   
  3. # Form sanity checks   
  4. if (!gt(“username”) || !gt(“password”))   
  5.     goback(“You need to enter both username and password to login.”);   
  6.   
  7. # The form is OK, check if the username is available   
  8. $username = gt(“username”);   
  9. $password = gt(“password”);   
  10. $r = redisLink();   
  11. $userid = $r->get(“username:$username:id”);   
  12. if (!$userid)   
  13.     goback(“Wrong username or password”);   
  14. $realpassword = $r->get(“uid:$userid:password”);   
  15. if ($realpassword != $password)   
  16.     goback(“Wrong useranme or password”);   
  17.   
  18. # Username / password OK, set the cookie and redirect to index.php   
  19. $authsecret = $r->get(“uid:$userid:auth”);   
  20. setcookie(“auth”,$authsecret,time()+3600*24*365);   
  21. header(“Location: index.php”);   
  22.   

每次用户登录都会运行,但我们需要一个函数isLoggedIn用于检验一个用户是否已经验证。
这些是isLoggedIn的逻辑步骤
* 从用户获取cookie里auth的值。如果没有cookie,该用户未登录。我们称这个cookie为<authcookie>
* 检查auth:<authcookie>是否存在,存在则获取值(例子里是1000)
* 为了再次确认,检查uid:1000:auth是否匹配
* 用户已验证,在全局变量$User中载入一点信息
也许代码比描述更短:

PHP代码
  1. function isLoggedIn() {   
  2.     global $User, $_COOKIE;   
  3.   
  4.     if (isset($User)) return true;   
  5.   
  6.     if (isset($_COOKIE[‘auth’])) {   
  7.         $r = redisLink();   
  8.         $authcookie = $_COOKIE[‘auth’];   
  9.         if ($userid = $r->get(“auth:$authcookie”)) {   
  10.             if ($r->get(“uid:$userid:auth”) != $authcookie) return false;   
  11.             loadUserInfo($userid);   
  12.             return true;   
  13.         }   
  14.     }   
  15.     return false;   
  16. }   
  17.   
  18. function loadUserInfo($userid) {   
  19.     global $User;   
  20.   
  21.     $r = redisLink();   
  22.     $User[‘id’] = $userid;   
  23.     $User[‘username’] = $r->get(“uid:$userid:username”);   
  24.     return true;   
  25. }   
  26.   

把loadUserInfo作为一个独立函数对于我们的应用而言有点杀鸡用牛刀了,但是对于复杂的应用而言这是一个不错的模板。
作为一个完整的验证,还剩下logout还没实现。在logout的时候我们怎么做呢?
很简单,仅仅改变uid:1000:auth里的随机字符串,删除旧的auth:<oldauthstring>并增加一个新的auth:<newauthstring>
重要:logout过程解释了为什么我们不仅仅查找auth:<randomstring>而是再次检查了uid:1000:auth。真正的验证字符串是后者,auth:<randomstring>是易变的.
假设程序中有BUGs或者脚本被意外中断,那么就有可能有多个auth:<something>指向同一个用户id。
logout代码如下:(logout.php)

PHP代码
  1. include(“retwis.php”);   
  2.   
  3. if (!isLoggedIn()) {   
  4.     header(“Location: index.php”);   
  5.     exit;   
  6. }   
  7.   
  8. $r = redisLink();   
  9. $newauthsecret = getrand();   
  10. $userid = $User[‘id’];   
  11. $oldauthsecret = $r->get(“uid:$userid:auth”);   
  12.   
  13. $r->set(“uid:$userid:auth”,$newauthsecret);   
  14. $r->set(“auth:$newauthsecret”,$userid);   
  15. $r->delete(“auth:$oldauthsecret”);   
  16.   
  17. header(“Location: index.php”);   
  18.   

以上是我们所描述过的,应该比较易于理解。

更新(Updates)


更新,或者称为帖子(posts)的实现则更为简单。为了在数据库里创建一个新的帖子,我们做了以下工作:
 INCR global:nextPostId => 10343
 SET post:10343 “$owner_id|$time|I’m having fun with Retwis”
就像你看到的一样,帖子的用户id和时间直接储存在了字符串里。
在这个例子中我们不需要根据时间或者用户id来查找帖子,所以把他们紧凑地挤在一个post字符串里更佳。
在新建一个帖子之后,我们获得了帖子的id。需要LPUSH这个帖子的id到每一个follow了作者的用户里去,当然还有作者的帖子列表。
update.php这个文件展示了这个工作是如何完成的:

PHP代码
  1. include(“retwis.php”);   
  2.   
  3. if (!isLoggedIn() || !gt(“status”)) {   
  4.     header(“Location:index.php”);   
  5.     exit;   
  6. }   
  7.   
  8. $r = redisLink();   
  9. $postid = $r->incr(“global:nextPostId”);   
  10. $status = str_replace(“\n”,” “,gt(“status”));   
  11. $post = $User[‘id’].”|”.time().”|”.$status;   
  12. $r->set(“post:$postid”,$post);   
  13. $followers = $r->smembers(“uid:”.$User[‘id’].”:followers”);   
  14. if ($followers === false) $followers = Array();   
  15. $followers[] = $User[‘id’]; /* Add the post to our own posts too */  
  16.   
  17. foreach($followers as $fid) {   
  18.     $r->push(“uid:$fid:posts”,$postid,false);   
  19. }   
  20. # Push the post on the timeline, and trim the timeline to the   
  21. # newest 1000 elements.   
  22. $r->push(“global:timeline”,$postid,false);   
  23. $r->ltrim(“global:timeline”,0,1000);   
  24.   
  25. header(“Location: index.php”);   
  26.   

函数的核心是foreach。 通过SMEMBERS获取当前用户的所有follower,然后循环会把帖子(post)LPUSH到每一个用户的 uid:<userid>:posts里
注意我们同时维护了一个所有帖子的时间线。为此我们还需要LPUSH到global:timeline里。
面对这个现实,你是否开始觉得:SQL里面用ORDER BY来按时间排序有一点儿奇怪? 我确实是这么想的。

分页

现在很清楚,我们能用LRANGE来获取帖子的范围,并在屏幕上显示。代码很简单:

PHP代码
  1. function showPost($id) {   
  2.     $r = redisLink();   
  3.     $postdata = $r->get(“post:$id”);   
  4.     if (!$postdata) return false;   
  5.   
  6.     $aux = explode(“|”,$postdata);   
  7.     $id = $aux[0];   
  8.     $time = $aux[1];   
  9.     $username = $r->get(“uid:$id:username”);   
  10.     $post = join(array_splice($aux,2,count($aux)-2),”|”);   
  11.     $elapsed = strElapsed($time);   
  12.     $userlink = “<a class=\”username\” href=\”profile.php?u=”.urlencode($username).”\”>”.utf8entities($username).”</a>”;   
  13.   
  14.     echo(‘<div class=”post”>’.$userlink.’ ‘.utf8entities($post).”<br>”);   
  15.     echo(‘<i>posted ‘.$elapsed.’ ago via web</i></div>’);   
  16.     return true;   
  17. }   
  18.   
  19. function showUserPosts($userid,$start,$count) {   
  20.     $r = redisLink();   
  21.     $key = ($userid == -1) ? “global:timeline” : “uid:$userid:posts”;   
  22.     $posts = $r->lrange($key,$start,$start+$count);   
  23.     $c = 0;   
  24.     foreach($posts as $p) {   
  25.         if (showPost($p)) $c++;   
  26.         if ($c == $count) break;   
  27.     }   
  28.     return count($posts) == $count+1;   
  29. }   
  30.   

当showUserPosts获取帖子的范围并传递给showPost时,showPost会简单输出一篇帖子的HTML代码。

Following users 关注的用户

如果用户id 1000 (antirez)想要follow用户id1000的pippo,我们做到这个仅需两步SADD:
SADD uid:1000:following 1001
SADD uid:1001:followers 1000
再次注意这个相同的模式:在关系数据库里的理论里follow的用户和被follow的用户是一张包含类似following_id和follower_id的单独数据表。
用查询你能明确follow和被follow的每一个用户。在key-value数据里有一点特别,需要我们分别设置1000follow了1001并且1001被1000follow的关系。
这是需要付出的代价,但是另一方面讲,获取这些数据即简单又超快。并且这些是独立的集合,允许我们做一些有趣的事情,比如使用SINTER获取两个不同用户的集合。
这样我们也许可以在我们的twitter复制品中加入一个功能:当你访问某个人的资料页时显示”你和foobar有34个共同关注者”之类的东西。
你能够在follow.php中找到增加或者删除following/folloer关系的代码。它如你所见般平常。

使它能够水平分割

亲爱的读者,如果你看到这里,你已经是一个英雄了,谢谢你。在讲到水平分割之前,看看单台服务器的性能是个不错的主意。
Retwis让人惊讶地快,没有任何缓存。在一台非常缓慢和高负载的服务器上,以100个线程并发请求100000次进行apache基准测试,平均占用5ms。
这意味着你可以仅仅使用一台linux服务器接受每天百万用户的访问,并且慢的跟个傻猴似的,就算用更新的硬件。
虽然,就算你有一堆用户,也许也不需要超过1台服务器来跑应用,但让我们假设我们是Twitter,需要处理海量的访问量呢?该怎么做?

Hashing the key

第一件事是把KEY进行hash运算并基于hash在不同服务器上处理请求。有大量知名的hash算法,例如ruby客户端自带的consistent hashing
大致意思是你能把key转换成数字,并除以你的服务器数量
 server_id = crc32(key) % number_of_servers
这里还有大量因为添加一台服务器产生的问题,但这仅仅是大致的意思,哪怕使用一个类似consistent hashing的更好索引算法,
是不是key就可以分布式访问了呢?所有用户数据都分布在不同的服务器上,没有inter-keys使用到(比如SINTER,否则你需要注意要在同一台服务器上进行)
这是Redis不像memcached一样强制指定索引算法的原因,需要应用来指定。另外,有几个key访问的比较频繁。

特殊的Keys

比如每次发布新帖,我们都需要增加global:nextPostId。单台服务器会有大量增加的请求。如何修复这个问题呢?一个简单的办法是用一台专门的服务器来处理增加请求。
除非你有大量的请求,否则矫枉过正了。另一个小技巧是ID并不需要真正地增加,只要唯一即可。这样你可以使用长度为不太可能发生碰撞的随机字符串(除了MD5这样的大小,几乎是不可能)。
完工,我们成功消除了水平分割带来的问题。

另一个问题是global:timeline。这里有个不是解决办法的解决办法,你可以分别保存在不同服务器上,并且在需要这些数据时从不同的服务器上取出来,或者用一个key来进行排序。
如果你确实每秒有这么多帖子,你能够再次用一台独立服务器专门处理这些请求。请记住,商用硬件的Redis能够以100000/s的速度写入数据。我猜测对于twitter这足够了。
请随意在下面评论处提问以及反馈。

 

本文采用Creative Commons协议,复制本文需遵守三点:1、保留署名(链接);2、非商业性使用;3、再次创作的作品必须以相同的许可协议发布。法律顾问:庄毅雄律师

Tumblr:150亿月浏览量背后的架构挑战

导读:和许多新兴的网站一样,著名的轻博客服务 Tumblr 在急速发展中面临了系统架构的瓶颈。每天 5 亿次浏览量,峰值每秒 4 万次请求,每天 3TB 新的数据存储,超过 1000 台服务器,这样的情况下如何保证老系统平稳运行,平稳过渡到新的系统,Tumblr 正面临巨大的挑战。近日,HighScalability 网站的 Todd Hoff 采访了该公司的分布式系统工程师 Blake Matheny,撰文系统介绍了网站的架构,内容很有价值。我们也非常希望国内的公司和团队多做类似分享,贡献于社区的同时,更能提升自身的江湖地位,对招聘、业务发展都好处多多。

Tumblr 每月页面浏览量超过 150 亿次,已经成为火爆的博客社区。用户也许喜欢它的简约、美丽,对用户体验的强烈关注,或是友好而忙碌的沟通方式,总之,它深得人们的喜爱。

每月超过 30% 的增长当然不可能没有挑战,其中可靠性问题尤为艰巨。每天 5 亿次浏览量,峰值每秒 4 万次请求,每天 3TB 新的数据存储,并运行于超过 1000 台服务器上,所有这些帮助 Tumblr 实现巨大的经营规模。

创业公司迈向成功,都要迈过危险的迅速发展期这道门槛。寻找人才,不断改造基础架构,维护旧的架构,同时要面对逐月大增的流量,而且曾经只有 4 位工程师。这意味着必须艰难地选择应该做什么,不该做什么。这就是 Tumblr 的状况。好在现在已经有 20 位工程师了,可以有精力解决问题,并开发一些有意思的解决方案。

Tumblr 最开始是非常典型的 LAMP 应用。目前正在向分布式服务模型演进,该模型基于 ScalaHBaseRedis(著名开源K-V存储方案)、Kafka(Apache 项目,出自 LinkedIn 的分布式发布-订阅消息系统)、Finagle(由 Twitter 开源的容错、协议中立的 RPC 系统),此外还有一个有趣的基于 Cell 的架构,用来支持 Dashboard(CSDN 注:Tumblr 富有特色的用户界面,类似于微博的时间轴)。

Tumblr 目前的最大问题是如何改造为一个大规模网站。系统架构正在从 LAMP 演进为最先进的技术组合,同时团队也要从小的创业型发展为全副武装、随时待命的正规开发团队,不断创造出新的功能和基础设施。下面就是 Blake Matheny 对 Tumblr 系统架构情况的介绍。

网站地址

http://www.tumblr.com/

主要数据

  • 每天 5 亿次 PV(页面访问量)
  • 每月超过 150 亿 PV
  • 约 20 名工程师
  • 峰值请求每秒近 4 万次
  • 每天超过 1TB 数据进入 Hadoop 集群
  • MySQL/HBase/Redis/memcache 每天生成若干 TB 数据
  • 每月增长 30%
  • 近 1000 硬件节点用于生产环境
  • 平均每位工程师每月负责数以亿计的页面访问
  • 每天上传大约 50GB 的文章,每天跟帖更新数据大约2.7TB(CSDN 注:这两个数据的比例看上去不太合理,据 Tumblr 数据科学家 Adam Laiacano 在 Twitter 上解释,前一个数据应该指的是文章的文本内容和元数据,不包括存储在 S3 上的多媒体内容)

软件环境

  • 开发使用 OS X,生产环境使用 Linux(CentOS/Scientific)
  • Apache
  • PHP, Scala, Ruby
  • Redis, HBase, MySQL
  • Varnish, HAProxy, nginx
  • memcache, Gearman(支持多语言的任务分发应用框架), Kafka, Kestrel(Twitter 开源的分布式消息队列系统), Finagle
  • Thrift, HTTP
  • Func——一个安全、支持脚本的远程控制框架和 API
  • Git, Capistrano(多服务器脚本部署工具), Puppet, Jenkins

硬件环境

  • 500 台 Web 服务器
  • 200 台数据库服务器(47 pool,20 shard)
  • 30 台 memcache 服务器
  • 22 台 Redis 服务器
  • 15 台 Varnish 服务器
  • 25 台 HAproxy 节点
  • 8 台 nginx 服务器
  • 14 台工作队列服务器(Kestrel + Gearman)

架构

1. 相对其他社交网站而言,Tumblr 有其独特的使用模式:

  • 每天有超过 5 千万篇文章更新,平均每篇文章的跟帖又数以百计。用户一般只有数百个粉丝。这与其他社会化网站里少数用户有几百万粉丝非常不同,使得 Tumblr 的扩展性极具挑战性。
  • 按用户使用时间衡量,Tumblr 已经是排名第二的社会化网站。内容的吸引力很强,有很多图片和视频,文章往往不短,一般也不会太长,但允许写得很长。文章内容往往比较深入,用户会花费更长的时间来阅读。
  • 用户与其他用户建立联系后,可能会在 Dashboard 上往回翻几百页逐篇阅读,这与其他网站基本上只是部分信息流不同。
  • 用户的数量庞大,用户的平均到达范围更广,用户较频繁的发帖,这些都意味着有巨量的更新需要处理。

2. Tumblr 目前运行在一个托管数据中心中,已在考虑地域上的分布性。

3. Tumblr 作为一个平台,由两个组件构成:公共 Tumblelogs 和 Dashboard

  • 公共 Tumblelogs 与博客类似(此句请 Tumblr 用户校正),并非动态,易于缓存
  • Dashboard 是类似于 Twitter 的时间轴,用户由此可以看到自己关注的所有用户的实时更新。与博客的扩展性不同,缓存作用不大,因为每次请求都不同,尤其是活跃的关注者。而且需要实时而 且一致,文章每天仅更新 50GB,跟帖每天更新2.7TB,所有的多媒体数据都存储在 S3 上面。
  • 大多数用户以 Tumblr 作为内容浏览工具,每天浏览超过 5 亿个页面,70% 的浏览来自 Dashboard。
  • Dashboard 的可用性已经不错,但 Tumblelog 一直不够好,因为基础设施是老的,而且很难迁移。由于人手不足,一时半会儿还顾不上。

老的架构

Tumblr 最开始是托管在 Rackspace 上的,每个自定义域名的博客都有一个A记录。当 2007 年 Rackspace 无法满足其发展速度不得不迁移时,大量的用户都需要同时迁移。所以他们不得不将自定义域名保留在 Rackspace,然后再使用 HAProxy 和 Varnish 路由到新的数据中心。类似这样的遗留问题很多。

开始的架构演进是典型的 LAMP 路线:

  • 最初用 PHP 开发,几乎所有程序员都用 PHP
  • 最初是三台服务器:一台 Web,一台数据库,一台 PHP
  • 为了扩展,开始使用 memcache,然后引入前端 cache,然后在 cache 前再加 HAProxy,然后是 MySQL sharding(非常奏效)
  • 采用“在单台服务器上榨出一切”的方式。过去一年已经用C开发了两个后端服务:ID 生成程序Staircar(用 Redis 支持 Dashboard 通知)

Dashboard 采用了“扩散-收集”方式。当用户访问 Dashboard 时将显示事件,来自所关注的用户的事件是通过拉然后显示的。这样支撑了 6 个月。由于数据是按时间排序的,因此 sharding 模式不太管用。

新的架构

由于招人和开发速度等原因,改为以 JVM 为中心。目标是将一切从 PHP 应用改为服务,使应用变成请求鉴别、呈现等诸多服务之上的薄层。

这其中,非常重要的是选用了 Scala 和 Finagle

  • 在团队内部有很多人具备 Ruby 和 PHP 经验,所以 Scala 很有吸引力。
  • Finagle 是选择 Scala 的重要因素之一。这个来自 Twitter 的库可以解决大多数分布式问题,比如分布式跟踪、服务发现、服务注册等。
  • 转到 JVM 上之后,Finagle 提供了团队所需的所有基本功能(Thrift, ZooKeeper 等),无需再开发许多网络代码,另外,团队成员认识该项目的一些开发者。
  • Foursquare 和 Twitter 都在用 Finagle,Meetup 也在用 Scala。
  • 应用接口与 Thrift 类似,性能极佳。
  • 团队本来很喜欢 Netty(Java 异步网络应用框架,2月 4 日刚刚发布3.3.1最终版),但不想用 Java,Scala 是不错的选择。
  • 选择 Finagle 是因为它很酷,还认识几个开发者。

之所以没有选择 Node.js,是因为以 JVM 为基础更容易扩展。Node 的发展为时尚短,缺乏标准、最佳实践以及大量久经测试的代码。而用 Scala 的话,可以使用所有 Java 代码。虽然其中并没有多少可扩展的东西,也无法解决 5 毫秒响应时间、49秒 HA、4万每秒请求甚至有时每秒 40 万次请求的问题。但是,Java 的生态链要大得多,有很多资源可以利用。

内部服务从C/libevent 为基础正在转向 Scala/Finagle 为基础。

开始采用新的NoSQL 存储方案如 HBase 和 Redis。但大量数据仍然存储在大量分区的 MySQL 架构中,并没有用 HBase 代替 MySQL。HBase 主要支持短地址生产程序(数以十亿计)还有历史数据和分析,非常结实。此外,HBase 也用于高写入需求场景,比如 Dashboard 刷新时一秒上百万的写入。之所以还没有替换 HBase,是因为不能冒业务上风险,目前还是依靠人来负责更保险,先在一些小的、不那么关键的项目中应用,以获得经验。MySQL 和时间序列数据 sharding(分片)的问题在于,总有一个分片太热。另外,由于要在 slave 上插入并发,也会遇到读复制延迟问题。

此外,还开发了一个公用服务框架

  • 花了很多时间解决分布式系统管理这个运维问题。
  • 为服务开发了一种 Rails scaffolding,内部用模板来启动服务。
  • 所有服务从运维的角度来看都是一样的,所有服务检查统计数据、监控、启动和停止的方式都一样。
  • 工具方面,构建过程围绕 SBT(一个 Scala 构建工具),使用插件和辅助程序管理常见操作,包括在 Git 里打标签,发布到代码库等等。大多数程序员都不用再操心构建系统的细节了。

200台数据库服务器中,很多是为了提高可用性而设,使用的是常规硬件,但 MTBF(平均故障间隔时间)极低。故障时,备用充足。

为了支持 PHP 应用有 6 个后端服务,并有一个小组专门开发后端服务。新服务的发布需要两到三周,包 括 Dashboard 通知、Dashboard 二级索引、短地址生成、处理透明分片的 memcache 代理。其中在 MySQL 分片上耗时很多。虽然在纽约本地非常热,但并没有使用 MongoDB,他们认为 MySQL 的可扩展性足够了。

Gearman用于会长期运行无需人工干预的工作。

可用性是以达到范围(reach)衡量的。用户能够访问自定义域或者 Dashboard 吗?也会用错误率。

历史上总是解决那些最高优先级的问题,而现在会对故障模式系统地分析和解决,目的是从用户和应用的角度来定成功指标。(后一句原文似乎不全)

最开始 Finagle 是用于 Actor 模型的,但是后来放弃了。对于运行后无需人工干预的工作,使用任务队列。而且 Twitter 的 util 工具库中有 Future 实现,服务都是用 Future(Scala 中的无参数函数,在与函数关联的并行操作没有完成时,会阻塞调用方)实现的。当需要线程池的时候,就将 Future 传入 Future 池。一切都提交到 Future 池进行异步执行。

Scala 提倡无共享状态。由于已经在 Twitter 生产环境中经过测试,Finagle 这方面应该是没有问题的。使用 Scala 和 Finagle 中的结构需要避免可变状态,不使用长期运行的状态机。状态从数据库中拉出、使用再写回数据库。这样做的好处是,开发人员不需要操心线程和锁。

22台Redis服务器,每台的都有8-32个实例,因此线上同时使用了 100 多个 Redis 实例。

  • Redis 主要用于 Dashboard 通知的后端存储。
  • 所谓通知就是指某个用户 like 了某篇文章这样的事件。通知会在用户的 Dashboard 中显示,告诉他其他用户对其内容做了哪些操作。
  • 高写入率使 MySQL 无法应对。
  • 通知转瞬即逝,所以即使遗漏也不会有严重问题,因此 Redis 是这一场景的合适选择。
  • 这也给了开发团队了解 Redis 的机会。
  • 使用中完全没有发现 Redis 有任何问题,社区也非常棒。
  • 开发了一个基于 Scala Futures 的 Redis 接口,该功能现在已经并入了 Cell 架构。
  • 短地址生成程序使用 Redis 作为一级 Cache,HBase 作为永久存储。
  • Dashboard 的二级索引是以 Redis 为基础开发的。
  • Redis 还用作 Gearman 的持久存储层,使用 Finagle 开发的 memcache 代理。
  • 正在缓慢地从 memcache 转向 Redis。希望最终只用一个 cache 服务。性能上 Redis 与 memcache 相当。

内部的 firehose(通信管道)

  • 内部的应用需要活跃的信息流通道。这些信息包括用户创建/删除的信息,liking/unliking 的提示,等等。挑战在于这些数据要实时的分布式处理。我们希望能够检测内部运行状况,应用的生态系统能够可靠的生长,同时还需要建设分布式系统的控制中心。
  • 以前,这些信息是基于 Scribe (Facebook 开源的分布式日志系统。)/Hadoop 的分布式系统。服务会先记录在 Scribe 中,并持续的长尾形式写入,然后将数据输送给应用。这种模式可以立即停止伸缩,尤其在峰值时每秒要创建数以千计的信息。不要指望人们会细水长流式的发布文 件和 grep。
  • 内部的 firehose 就像装载着信息的大巴,各种服务和应用通过 Thrift 与消防管线沟通。(一个可伸缩的跨语言的服务开发框架。)
  • LinkedIn 的 Kafka 用于存储信息。内部人员通过 HTTP 链接 firehose。经常面对巨大的数据冲击,采用 MySQL 显然不是一个好主意,分区实施越来越普遍。
  • firehose 的模型是非常灵活的,而不像 Twitter 的 firehose 那样数据被假定是丢失的。
  • firehose 的信息流可以及时的回放。他保留一周内的数据,可以调出这期间任何时间点的数据。
  • 支持多个客户端连接,而且不会看到重复的数据。每个客户端有一个 ID。Kafka 支持客户群,每个群中的客户都用同一个 ID,他们不会读取重复的数据。可以创建多个客户端使用同一个 ID,而且不会看到重复的数据。这将保证数据的独立性和并行处理。Kafka 使用 ZooKeeper (Apache 推出的开源分布式应用程序协调服务。)定期检查用户阅读了多少。

为 Dashboard 收件箱设计的 Cell 架构

  • 现在支持 Dashboard 的功能的分散-集中架构非常受限,这种状况不会持续很久。
  • 解决方法是采用基于 Cell 架构的收件箱模型,与 Facebook Messages 非常相似。
  • 收件箱与分散-集中架构是对立的。每一位用户的 dashboard 都是由其追随者的发言和行动组成的,并按照时间顺序存储。
  • 就因为是收件箱就解决了分散-集中的问题。你可以会问到底在收件箱中放了些什么,让其如此廉价。这种方式将运行很长时间。
  • 重写 Dashboard 非常困难。数据已经分布,但是用户局部升级产生的数据交换的质量还没有完全搞定。
  • 数据量是非常惊人的。平均每条消息转发给上百个不同的用户,这比 Facebook 面对的困难还要大。大数据+高分布率+多个数据中心。
  • 每秒钟上百万次写入,5万次读取。没有重复和压缩的数据增长为2.7TB,每秒百万次写入操作来自 24 字节行键。
  • 已经流行的应用按此方法运行。
  • cell
  • 每个 cell 是独立的,并保存着一定数量用户的全部数据。在用户的 Dashboard 中显示的所有数据也在这个 cell 中。
  • 用户映射到 cell。一个数据中心有很多 cell。
  • 每个 cell 都有一个 HBase 的集群,服务集群,Redis 的缓存集群。
  • 用户归属到 cell,所有 cell 的共同为用户发言提供支持。
  • 每个 cell 都基于 Finagle(Twitter 推出的异步的远程过程调用库),建设在 HBase 上,Thrift 用于开发与 firehose 和各种请求与数据库的链接。(请纠错)
  • 一个用户进入 Dashboard,其追随者归属到特定的 cell,这个服务节点通过 HBase 读取他们的 dashboard 并返回数据。
  • 后台将追随者的 dashboard 归入当前用户的 table,并处理请求。
  • Redis 的缓存层用于 cell 内部处理用户发言。
  • 请求流:用户发布消息,消息将被写入 firehose,所有的 cell 处理这条消息并把发言文本写入数据库,cell 查找是否所有发布消息追随者都在本 cell 内,如果是的话,所有追随者的收件箱将更新用户的 ID。(请纠错
  • cell 设计的优点:
  • 大规模的请求被并行处理,组件相互隔离不会产生干扰。 cell 是一个并行的单位,因此可以任意调整规格以适应用户群的增长。
  • cell 的故障是独立的。一个 Cell 的故障不会影响其他 cell。
  • cell 的表现非常好,能够进行各种升级测试,实施滚动升级,并测试不同版本的软件。
  • 关键的思想是容易遗漏的:所有的发言都是可以复制到所有的 cell。
  • 每个 cell 中存储的所有发言的单一副本。 每个 cell 可以完全满足 Dashboard 呈现请求。应用不用请求所有发言者的 ID,只需要请求那些用户的 ID。(“那些用户”所指不清,请指正。)他可以在 dashboard 返回内容。每一个 cell 都可以满足 Dashboard 的所有需求,而不需要与其他 cell 进行通信。
  • 用到两个 HBase table :一个 table 用于存储每个发言的副本,这个 table 相对较小。在 cell 内,这些数据将与存储每一个发言者 ID。第二个 table 告诉我们用户的 dashboard 不需要显示所有的追随者。当用户通过不同的终端访问一个发言,并不代表阅读了两次。收件箱模型可以保证你阅读到。
  • 发言并不会直接进入到收件箱,因为那实在太大了。所以,发言者的 ID 将被发送到收件箱,同时发言内容将进入 cell。这个模式有效的减少了存储需求,只需要返回用户在收件箱中浏览发言的时间。而缺点是每一个 cell 保存所有的发言副本。令人惊奇的是,所有发言比收件箱中的镜像要小。(请纠错)每天每个 cell 的发言增长 50GB,收件箱每天增长2.7TB。用户消耗的资源远远超过他们制造的。
  • 用户的 dashboard 不包含发言的内容,只显示发言者的 ID,主要的增长来自 ID。(请 Tumblr 用户纠错)
  • 当追随者改变时,这种设计方案也是安全的。因为所有的发言都保存在 cell 中了。如果只有追随者的发言保存在 cell 中,那么当追随者改变了,将需要一些回填工作。
  • 另外一种设计方案是采用独立的发言存储集群。这种设计的缺点是,如果群集出现故障,它会影响整个网站。因此,使用 cell 的设计以及后复制到所有 cell 的方式,创建了一个非常强大的架构。
  • 一个用户拥有上百万的追随者,这带来非常大的困难,有选择的处理用户的追随者以及他们的存取模式(见 Feeding Frenzy
  • 不同的用户采用不同并且恰当的存取模式和分布模型,两个不同的分布模式包括:一个适合受欢迎的用户,一个使用大众。
  • 依据用户的类型采用不同的数据处理方式,活跃用户的发言并不会被真正发布,发言将被有选择的体现。(果真如此?请 Tumblr 用户纠错)
  • 追随了上百万用户的用户,将像拥有上百万追随者的用户那样对待。
  • cell 的大小非常难于决定。cell 的大小直接影响网站的成败。每个 cell 归于的用户数量是影响力之一。需要权衡接受怎样的用户体验,以及为之付出多少投资。
  • 从 firehose 中读取数据将是对网络最大的考验。在 cell 内部网络流量是可管理的。
  • 当更多 cell 被增添到网络中来,他们可以进入到 cell 组中,并从 firehose 中读取数据。一个分层的数据复制计划。这可以帮助迁移到多个数据中心。

在纽约启动运作

  • 纽约具有独特的环境,资金和广告充足。招聘极具挑战性,因为缺乏创业经验。
  • 在过去的几年里,纽约一直致力于推动创业。纽约大学和哥伦比亚大学有一些项目,鼓励学生到初创企业实习,而不仅仅去华尔街。市长建立了一所学院,侧重于技术。

团队架构

  • 团队:基础架构,平台,SRE,产品,web ops,服务;
  • 基础架构:5层以下,IP 地址和 DNS,硬件配置;
  • 平台:核心应用开发,SQL 分片,服务,Web 运营;
  • SRE:在平台和产品之间,侧重于解决可靠性和扩展性的燃眉之急;
  • 服务团队:相对而言更具战略性,
  • Web ops:负责问题检测、响应和优化。

软件部署

  • 开发了一套 rsync 脚本,可以随处部署 PHP 应用程序。一旦机器的数量超过 200 台,系统便开始出现问题,部署花费了很长时间才完成,机器处于部署进程中的各种状态。
  • 接下来,使用 Capistrano(一个开源工具,可以在多台服务器上运行脚本)在服务堆栈中构建部署进程(开发、分期、生产)。在几十台机器上部署可以正常工作,但当通过 SSH 部署到数百台服务器时,再次失败。
  • 现在,所有的机器上运行一个协调软件。基于 Redhat Func(一个安全的、脚本化的远程控制框架和接口)功能,一个轻量级的 API 用于向主机发送命令,以构建扩展性。
  • 建立部署是在 Func 的基础上向主机发送命令,避免了使用 SSH。比如,想在组A上部署软件,控制主机就可以找出隶属于组A的节点,并运行部署命令。
  • 部署命令通过 Capistrano 实施。Func API 可用于返回状态报告,报告哪些机器上有这些软件版本。
  • 安全重启任何服务,因为它们会关闭连接,然后重启。
  • 在激活前的黑暗模式下运行所有功能。

展望

  • 从哲学上将,任何人都可以使用自己想要的任意工具。但随着团队的发展壮大,这些工具出现了问题。新员工想要更好地融入团队,快速地解决问题,必须以他们为中心,建立操作的标准化。
  • 过程类似于 Scrum(一种敏捷管理框架),非常敏捷。
  • 每个开发人员都有一台预配置的开发机器,并按照控制更新。
  • 开发机会出现变化,测试,分期,乃至用于生产。
  • 开发者使用 VIM 和 TextMate。
  • 测试是对 PHP 程序进行代码审核。
  • 在服务方面,他们已经实现了一个与提交相挂钩的测试基础架构,接下来将继承并内建通知机制。

招聘流程

  • 面试通常避免数学、猜谜、脑筋急转弯等问题,而着重关注应聘者在工作中实际要做什么。
  • 着重编程技能。
  • 面试不是比较,只是要找对的人。
  • 挑战在于找到具有可用性、扩展性经验的人才,以应对 Tumblr 面临的网络拥塞。
  • 在 Tumblr 工程博客(Tumblr Engineering Blog),他们对已过世的 Dennis Ritchie 和 John McCarthy 予以纪念。

经验及教训

  • 自动化无处不在
  • MySQL(增加分片)规模,应用程序暂时还不行
  • Redis 总能带给人惊喜
  • 基于 Scala 语言的应用执行效率是出色的
  • 废弃项目——当你不确定将如何工作时
  • 不顾用在他们发展经历中没经历过技术挑战的人,聘用有技术实力的人是因为他们能适合你的团队以及工作。
  • 选择正确的软件集合将会帮助你找到你需要的人
  • 建立团队的技能
  • 阅读文档和博客文章。
  • 多与同行交流,可以接触一些领域中经验丰富的人,例如与在 Facebook、Twitter、LinkedIn 的工程师多交流,从他们身上可以学到很多
  • 对技术要循序渐进,在正式投入使用之前他们煞费苦心的学习 HBase 和 Redis。同时在试点项目中使用或将其控制在有限损害范围之内。

from:

Tumblr:150亿月浏览量背后的架构挑战(上)

Tumblr:150亿月浏览量背后的架构挑战(下)

英文原文:High Scalability

百万级访问网站前期的技术准备

开了自己域名的博客,第一篇就得来个重磅一点的才对得起这4美金的域名。作为一个技术从业者十年,逛了十年发现有些知识东一榔头西一棒槌的得满世界看个遍才整理出个头绪,那咱就系统点的从头一步一步的说,一个从日几千访问的小小网站,到日访问一两百万的小网站,怎么才能让它平滑的度过这个阶段,别在技术上出现先天不足,写给一些技术人员,也写给不懂技术的创业者。

转载请注明出自 http://zhiyi.us ,假如您还想从这转到好文章的话。

对互联网有了解的人都有自己的想法,有人就把想法付诸实现,做个网站然后开始运营。其实从纯网站技术上来说,因为开源模式的发展,现在建一个小网站已经很简单也很便宜。当访问量到达一定数量级的时候成本就开始飙升了,问题也开始显现了。因为带宽的增加、硬件的扩展、人员的扩张所带来的成本提高是显而易见的,而还有相当大的一部分成本是因为代码重构、架构重构,甚至底层开发语言更换引起的,最惨的就是数据丢失,辛辛苦苦好几年,一夜回到创业前。

减少成本就是增加利润。很多事情,我们在一开始就可以避免,先打好基础,往后可以省很多精力,少操很多心。

假设你是一个参与创业的技术人员,当前一穷二白,什么都要自己做,自己出钱,初期几十万的资金,做一个应用不是特别复杂的网站,那么就要注意以下几点:

一、开发语言

一般来说,技术人员(程序员)创业都是根据自己技术背景选择自己最熟悉的语言,不过考虑到不可能永远是您一个人写程序,这点还得仔细想想。无论用什么语言,最终代码质量是看管理,所以我们还是从纯语言层面来说实际一点。现在流行的javaphp.netpythonruby都有自己的优劣,python和ruby,现在人员还是相对难招一些,性能优化也会费些力气,.net平台买不起windows server。java、php用的还是最多。对于初期,应用几乎都是靠前端支撑的网站来说,php的优势稍大一些,入门简单、设计模式简单、写起来快、性能足够等,不过不注重设计模式也是它的劣势,容易变得松散,隐藏bug稍多、难以维护。java的优势在于整套管理流程已经有很多成熟工具来辅助,强类型也能避免一些弱智BUG,大多数JAVA程序员比较注重设计模式,别管实不实际,代码格式看起来还是不错的。这也是个劣势,初学者可能太注重模式而很难解决实际需求。

前端不只是html、css这类。整个负责跟用户交互的部分都是前端,包括处理程序。这类程序还是建议用php,主要原因就是开发迅速、从业人员广泛。至于后端例如行为分析、银行接口、异步消息处理等,随便用什么程序,那个只能是根据不同业务需求来选择不同语言了。

二、代码版本管理

如果开发人员之间的网络速度差不多,就SVN;比较分散例如跨国,就hg。大多数人还是svn的.

假设选了svn,那么有几点考虑。一是采用什么树结构。初期可能只有一条主干,往后就需要建立分支,例如一条开发分支,一条上线分支,再往后,可能要每个小组一个分支。建议一开始人少时选择两条分支,开发和线上,每个功能本地测试无误后提交到开发分支,最后统一测试,可以上线时合并到上线分支。如果喜欢把svn当做移动硬盘用,写一点就commit一次也无所谓,就是合并的时候头大一些,这些人可以自己建个分支甚至建立个本地代码仓库,随便往自己的分支提交,测试完毕后再提交到开发分支上。

部署,可以手工部署也可以自动部署。手工部署相对简单,一般是直接在服务器上svn update,或者找个新目录svn checkout,再把web root给ln -s过去。应用越复杂,部署越复杂,没有什么统一标准,只要别再用ftp上传那种形式就好,一是上传时文件引用不一致错误率增加,二是很容易出现开发人员的版本跟线上版本不一致,导致本来想改个错字结果变成回滚的杯具。如果有多台服务器还是建议自动部署,更换代码的机器从当前服务池中临时撤出,更新完毕后再重新加入。

不管项目多小,养成使用版本管理的好习惯,最起码还可以当做你的备份,我的 http://zhiyi.us 虽然就是一个wordpress,可还是svn了,只改动一两句css那也是劳动成果。

三、服务器硬件

别羡慕大客户和有钱人,看看机房散户区,一台服务器孤独的支撑的网站数不清。如果资金稍微充足,建议至少三台的标准配置,分别用作web处理、数据库、备份。web服务器至少要8G内存,双sata raid1,如果经济稍微宽松,或静态文件或图片多,则15k sas raid1+0。数据库至少16G内存,15k sas raid 1+0。备份服务器最好跟数据库服务器同等配置。硬件可以自己买品牌的底板,也就是机箱配主板和硬盘盒,CPU内存硬盘都自己配,也可以上整套品牌,也可以兼容机。三台机器,市场行情6、7万也就配齐了。

web服务器可以既跑程序又当内存缓存,数据库服务器则只跑主数据库(假如是MySQL的话),备份服务器干的活就相对多一些,web配置、缓存配置、数据库配置都要跟前两台一致,这样WEB和数据库任意一台出问题,把备份服务器换个ip就切换上去了。备份策略,可以drbd,可以rsync,或者其他的很多很多的开源备份方案可选择。rsync最简单,放cron里自己跑就行。备份和切换,建议多做测试,选最安全最适合业务的,并且尽可能异地备份。

四、机房

三种机房尽量不要选:联通访问特别慢的电信机房、电信访问特别慢的联通机房、电信联通访问特别慢的移动或铁通机房。那网通机房呢?亲,网通联通N久以前合并改叫联通了。多多寻找,实地参观,多多测试,多方打探,北京、上海、广州等各个主节点城市,还是有很多优质机房的,找个网络质量好,管理严格的机房,特别是管理要严格,千万别网站无法访问了,打个电话过去才知道别人维护时把你网线碰掉了,这比DOS都头疼。自己扯了几根光纤就称为机房的,看您抗风险程度和心理素质了。机房可以说是非常重要,直接关系到网站访问速度,网站访问速度直接关系到用户体验,我可以翻墙看风景,但买个网游vpn才能打开你这个还不怎么知名的网站就有难度了。或许您网站的ajax很出色,可是document怎么也不ready,一些代码永远绝缘于用户。

五、架构

初期架构一般比较简单,web负载均衡+数据库主从+缓存+分布式存储+队列。大方向上也确实就这几样东西,细节上也无数文章都重复过了,按照将来会有N多WEB,N多主从关系,N多缓存,N多xxx设计就行,基本方案都是现成的,只是您比其他人厉害之处就在于设计上考虑到缓存失效时的雪崩效应、主从同步的数据一致性和时间差、队列的稳定性和失败后的重试策略、文件存储的效率和备份方式等等意外情况。缓存总有一天会失效,数据库复制总有一天会断掉,队列总有一天会写不进去,电源总有一天会烧坏。根据墨菲定律,如果不考虑这些,网站早晚会成为茶几。

六、服务器软件

Linux、nginx、php、mysql,几乎是标配,我们除了看名字,还得选版本。Linux发行版众多,只要没特殊要求,就选个用的人最多的,社区最活跃的,配置最方便的,软件包最全最新的,例如debianubuntu。至于RHEL之类的嘛,你用只能在RHEL上才能运行的软件么?剩下的nginx、php、mysql、activemq、其他的等等,除非你改过这些软件或你的程序真的不兼容新版本,否则尽量版本越新越好,版本新,意味着新特性增多、BUG减少、性能增加。总有些道听途说的人跟你说老的版本稳定。所谓稳定,是相对于特殊业务来说的,而就一个php写的网站,大多数人都没改过任何服务器软件源代码,绝大多数情况是能平稳的升级到新版本的。类似于jdk5到jdk6,python2到python3这类变动比较大的升级还是比较少见的。看看ChangeLog,看看升级说明,结合自己情况评估一下,越早升级越好,别人家都用php6写程序了这边还php4的逛游呢。优秀的开源程序升级还是很负责任的,看好文档,别怕。

以上这六点准备完毕,现在我们有了运行环境,有了基本架构骨架,有了备份和切换方案,应该开始着手设计开发方面的事情了。开发方面的事情无数,下一篇会先说一些重点。

七、数据库

几乎所有操作最后都要落到数据库身上,它又最难扩展(存储也挺难)。对于mysql,什么样的表用myisam,什么样的表用innodb,在开发之前要确定。复制策略、分片策略,也要确定。表引擎方面,一般,更新不多、不需要事务的表可以用myisam,需要行锁定、事务支持的,用innodb。myisam的锁表不一定是性能低下的根源,innodb也不一定全是行锁,具体细节要多看相关的文档,熟悉了引擎特性才能用的更好。现代WEB应用越来越复杂了,我们设计表结构时常常设计很多冗余,虽然不符合传统范式,但为了速度考虑还是值得的,要求高的情况下甚至要杜绝联合查询。编程时得多注意数据一致性。 复制策略方面,多主多从结构也最好一开始就设计好,代码直接按照多主多从来编写,用一些小技巧来避免复制延时问题,并且还要解决多数据库数据是否一致,可以自己写或者找现成的运维工具。

分片策略。总会有那么几个表数据量超大,这时分片必不可免。分片有很多策略,从简单的分区到根据热度自动调整,依照具体业务选择一个适合自己的。避免自增ID作为主键,不利于分片。

用存储过程是比较难扩展的,这种情形多发生于传统C/S,特别是OA系统转换过来的开发人员。低成本网站不是一两台小型机跑一个数据库处理所有业务的模式,是机海作战。方便水平扩展比那点预分析时间和网络传输流量要重要的多的多。

NoSQL。这只是一个概念。实际应用中,网站有着越来越多的密集写操作、上亿的简单关系数据读取、热备等,这都不是传统关系数据库所擅长的,于是就产生了很多非关系型数据库,比如Redis/TC&TT/MongoDB/Memcachedb等,在测试中,这些几乎都达到了每秒至少一万次的写操作,内存型的甚至5万以上。例如MongoDB,几句配置就可以组建一个复制+自动分片+failover的环境,文档化的存储也简化了传统设计库结构再开发的模式。很多业务是可以用这类数据库来替代mysql的。

八、缓存。

数据库很脆弱,一定要有缓存在前面挡着,其实我们优化速度,几乎就是优化缓存,能用缓存的地方,就不要再跑到后端数据库那折腾。缓存有持久化缓存、内存缓存,生成静态页面是最容易理解的持久化缓存了,还有很多比如varnish的分块缓存、前面提到的memcachedb等,内存缓存,memcached首当其冲。缓存更新可用被动更新和主动更新。被动更新的好处是设计简单,缓存空了就自动去数据库取数据再把缓存填上,但容易引发雪崩效应,一旦缓存大面积失效,数据库的压力直线上升很可能挂掉。主动缓存可避免这点但是可能引发程序取不到数据的问题。这两者之间如何配合,程序设计要多动脑筋。

九、队列。

用户一个操作很可能引发一系列资源和功能的调动,这些调动如果同时发生,压力无法控制,用户体验也不好,可以把这样一些操作放入队列,由另几个模块去异步执行,例如发送邮件,发送手机短信。开源队列服务器很多,性能要求不高用数据库当做队列也可以,只要保证程序读写队列的接口不变,底层队列服务可随时更换就可以,类似Zend Framework里的Zend_Queue类,java.util.Queue接口等。

十、文件存储。

除了结构化数据,我们经常要存放其他的数据,像图片之类的。这类数据数量繁多、访问量大。典型的就是图片,从用户头像到用户上传的照片,还要生成不同的缩略图尺寸。存储的分布几乎跟数据库扩展一样艰难。不使用专业存储的情况下,基本都是靠自己的NAS。这就涉及到结构。拿图片存储举例,图片是非常容易产生热点的,有些图片上传后就不再有人看,有些可能每天被访问数十万次,而且大量小文件的异步备份也很耗费时间。

为了将来图片走cdn做准备,一开始最好就将图片的域名分开,且不用主域名。很多网站都将cookie设置到了.domain.ltd,如果图片也在这个域名下,很可能因为cookie而造成缓存失效,并且占多余流量,还可能因为浏览器并发线程限制造成访问缓慢。

如果用普通的文件系统存储图片,有一个简单的方法。计算文件的hash值,比如md5,以结果第一位作为第一级目录,这样第一级有16个目录。从0到F,可以把这个字母作为域名,0.yourimg.com到f.yourimg.com(客户端dns压力会增大),还可以扩展到最多16个NAS集群上。第二级可用年月例如,201011,第三级用日,第四级可选,根据上传量,比如am/pm,甚至小时。最终的目录结构可能会是 e/201008/25/am/e43ae391c839d82801920cf.jpg。rsync备份时可以用脚本只同步某年某日某时的文件,避免计算大量文件带来的开销。当然最好是能用专门的分布式文件系统或更专业点的存储解决方案。

下面,我们要谈谈代码了。

这一系列的最后一篇写给普通编程人员,如果不感兴趣可直接看本文最后几段。

开始设计代码结构之前,先回顾一下之前准备过的事情:我们有负载均衡的WEB服务器,有主从DB服务器并可能分片,有缓存,有可扩展的存储。在组织代码的各个方面,跟这些准备息息相关,我一二三的列出来分别说,并且每一条都以“前面讲到”这个经典句式开头,为了方便对照。 别着急看经典句式,我思维跳跃了,插一段。实际开发中,我们总会在性能和代码优雅性上作折中。对于当今的计算机和语言解释器,多几层少几层对象调用、声明变量为Map还是HashMap这种问题是最后才需要考虑的问题,永远要考虑系统最慢的部分,从最慢的部分解决。例如看看你用的ORM是不是做了很多你用不到的事情,是不是有重复的数据调用。我们做的是web应用开发,不是底层框架API,代码易读易懂是保证质量很重要的一方面,你的程序是为了什么而设计,有不同的方法……算了,这个话题另起一篇文章来说,扯远了,想交流可关注我的微博 http://t.sina.com.cn/liuzhiyi,咱继续……

前面讲到,WEB服务器是要做负载均衡的,图片服务器是要分开的。对于这点,代码在处理客户端状态时,不要把状态放到单机上,举例,不要用文件session,嗯,常识。如果有可能,最好在一开始就做好用户单点认证的统一接口,包括跨域如何判断状态、静态页面如何判断状态,需要登录时的跳转和返回参数定义,底层给好接口,应用层直接就用(可参考GAE的user服务)。登录方面的设计要考虑移动设备的特性,比如电脑可以用浮动层窗口,但NOKIA自带的浏览器或UCWEB就无法处理这种表现形式,程序一定既能处理AJAX请求又能直接通过URL来处理请求。图片服务器分开,资源文件最好也布局到图片服务器,也就是WEB服务器只服务动态程序。虽然开发测试时稍微复杂(因为需要绝对URI才能访问),但将来页面前端优化上会轻松许多,并且你的WEB服务器IO优化也轻松许多。程序引用资源文件时,要有一个统一的处理方法,在方法内部可以自动完成很多事情,例如将css/js根据组合,拼成一个文件,或者自动在生成的URI后面加上QUERYSTRING,如果将来前端用了缓存服务,那生成QUERYSTRING是最简单的刷新服务端缓存和客户端缓存的办法。

前面讲到,数据库会有复制,可能会多主多从,可能会分片。我们程序在处理数据的过程中,最好能抽象出来单独放做一层。拿现在流行的MVC模式来说,就是在M层下方再放一个数据层,这个数据层不是通常所说的JDBC/PDO/ActiveRecord等,而是你自己的存取数据层,仅对外暴露方法,隐藏数据存取细节。这个数据层内部不要怕写的难看,但一定要提供所有的数据存储功能,其他任何层次不要看到跟数据库打交道的字眼。之所以这样做,是因为在单关系数据库的情况下,可能会SELECT…JOIN…或直接INSERT…INTO…,可你可能会将一些表放到key-value数据库里存储,或者分片,这么做之后原来的语句和方式要全部改变,如果过于分散,则移植时会耗费很大精力,或得到一个很大的Model。在数据层面的设计上,尽量避免JOIN查询,我们可以多做冗余,多做缓存,每种数据尽量只需要一次查询,然后在你的程序里面进行组合。对于比较复杂的数据组合,在实时性要求不高的情况下,可采用异步处理,用户访问时只取处理后的结果。在对于主键的处理上,避免使用自增ID,可以用一定规则生成的唯一值当做主键,这种主键是最简单的分片分布策略。即使用自增ID,也最好用一个自增ID发生器,否则从数据库不小心被写了一下,那主键很容易冲突。

前面讲到,咱数据库前面还有某些缓存挡着。别把mysql的query cache当缓存,应用稍复杂的时候QUERY CACHE反而会成为累赘。缓存跟数据库和业务结合的很紧密,正因为跟业务关系紧密,所以这点没有放之四海而皆准的方法。但我们还是有一些规则可参照。规则一:越接近前端,缓存的颗粒度越大。例如在WEB最前端缓存整个页面,再往后一层缓存部分页面区域,再往后缓存区域内的单条记录。因为越靠近后端,我们的可操作性越灵活,并且变化最多的前端代码也比较方便编写。在实践中,因为产品需求变化速度非常快,迭代周期越来越短,有时很难将Controller和Model分的那么清楚,Controller层面处理部分缓存必不可免,但要保证如果出现这种情况,Controller所操作的缓存一定不要影响其他数据需求方,也就是要保证这个缓存数据只有这一个Controller在用。规则二:没有缓存时程序不能出错。在不考虑缓存失效引发的雪崩效应时,你的程序要有缓存跟没缓存一个样,不能像新浪微博一样,缓存一失效,粉丝微博全空,整个应用都乱套了。在缓存必不可少的情况下,给用户出错信息都比给一个让人误解的信息强。规则三,缓存更新要保证原子性或称作线程安全,特别是采用被动缓存的方式时,很可能两个用户访问时导致同一个缓存被更新,通常情况这不是大问题,可缓存失效后重建时很可能是引发连锁反应的原因之一。规则四:缓存也是有成本的。不只是技术成本,还有人工时间成本。如果一个功能使用缓存和不使用,在可预见的访问量情况下区别微小,但使用缓存会使复杂度增加,那就不用,我们可以加个TODO标注,在下次迭代的时候加上缓存处理。

前面讲到,文件存储是独立的,那么所有的文件操作就都是远程调用。可以在文件服务器上提供一个很简单的RESTful接口,也可以提供xmlrpc或json serveice,WEB服务器端所生成和处理的文件,全部通过接口通知文件服务器去处理,WEB服务器本身不要提供任何文件存储。你会发现很多大网站的上传图片跟保存文章是分两步完成的,就是基于这个原因。

以上几条“前面讲到”,其实无数人都讲过,我也只是结合前几篇文章用自己的话重复了一遍,真正分析起来精髓很简单——除了良好的功能逻辑分层,我们还要为数据库存储、缓存、队列、文件服务等程序外层资源调用单独设计接口,你可以把你的程序想象成是运行在 Amazon EC2 上并用他的所有web service服务,你的数据库就是它的SimpleDB,你的队列就是他的SQS,你的存储就是他的S3,唯一不同是amazon的接口是远程调用,你的是内部调用。

将支撑服务接口化,意味着将MySQL更换到PostgreSQL不需要更改业务处理程序,移植团队甚至不需要跟业务开发团队过多沟通;意味着业务开发团队是对接口编程而不是对数据库编程;意味着不会因为某个业务开发人员的失误而拖垮性能。

对程序扫盲不感兴趣的直接看这里——

产品设计完了,程序框架搭完了,可能有矛盾在这个节骨眼儿产生了。不断有产品设计抱怨说他的创意没实现到预期效果,有程序员抱怨说产品设计不切实际。这种抱怨多缘于产品人员不懂技术,技术人员不理解产品。从广义上来讲,产品包含市场策略、营销手段、功能设计,产品和技术在争论时往往把焦点放在功能上,而实际重点是,实现这个功能所消耗的成本跟能这个功能带来的利益能否换算,能否取其轻重。若可以,争议解决。若不能,则抛硬币看运气。因为一个功能的加强而引发指标井喷,或因项目拖延而导致贻误战机的例子比比皆是。激进的决策者注重利益,保守的决策者注重损失,聪明的决策者会考虑这个问题是否真的那么严重。

关系到未来的事情谁都说不准,要不怎么说创业一半靠运气呢。不过总有能说的准的事情,那就得靠数据说话。

没有100%也有99.9%的网站安装了访问统计代码,连我的 http://zhiyi.us 也不例外,新闻联播也总说科学决策科学发展的。有了统计,能确定的事情就很多了。例如,可以根据来源-目标转化率来分析哪类渠道的人均获取成本低,根据来源-内容访问猜测用户跳出率原因,根据用户点击行为判断链接位置是否合理等。将数据以不同方式组合起来,找到内在联系,分析内因外因,制定对应策略,减少拍脑门决策。靠数据支撑运营是个非常专业的事情,虽然不懂深奥的数学模型不会复杂的公式计算,渐渐学会因为A所以B,因为A和B所以C还是相对简单的。

全系列完毕。老话,大半夜连抽烟带码字的挺伤身,转载请注明出处.

from:

http://zhiyi.us/internet/thinking-twice-before-building-your-site-one.html

http://zhiyi.us/internet/thinking-twice-before-building-your-site-two.html

http://zhiyi.us/internet/thinking-twice-before-building-your-site-final.html

RAID的概念和RAID对于SQL性能的影响

简介

我们都听说过RAID,也经常作为SQL DBA、开发人员或构架师在工作中讨论RAID。但是,其实我们很多人都对RAID的原理,等级,以及RAID是如何影响SQL Server性能并不甚了解。

本篇文章就是为了补上这一课。

 

磁盘构架

今天的磁盘,和70年代45rpm(转/分钟)的唱片机很像(你还能记得吗?),仅仅是一个有着轴(磁道)旋转的媒体(面)并将数据存入称之为扇区的磁盘段。

就像唱片机那样,磁盘驱动器拥有一个摆臂来控制针(在这里可以称之为”磁头”)来访问数据。但对于磁盘来说,并不像唱片机那样只读,而是既可以读又可以写。

为了从特定的扇区读或者写数据,磁盘必须进行旋转然后摆臂移动使得磁头移动到垂直于指定扇区的正上方以访问数据。

这个过程就是基本的输入/输出操作的过程(I/O)。

 

IOPS

IOPS这个术语也是被经常拿出来装X的,但同样,对这个术语真正理解的人并不多。

很多人都明白IOPS是Input Output Operations per Second的缩写,但是将这个定义转换为实际的概念对于某些人就有点难了。

对IOPS基本的理解是对满足特定输入输出请求的平均时间的一种衡量。

这里重点需要知道这个度量标准是基于读取0字节的文件,这仅仅是为了统计和标准化的目的因为一个磁盘扇区大小并不同。

 

物理磁盘的限制

磁盘会有一些物理限制会限制磁盘能达到的IOP级别。这个限制是磁道寻址时间(seek time)和旋转延迟(rotational latency)。

磁道寻址时间是为了使得磁头可以移动到所要读的扇区,移动摆臂所花费的平均时间。

旋转延迟是为了使磁头读取盘面特定位置旋转磁盘所话费的时间(通常为毫秒级)。

单位IOP所花的时间公式如下:

单位IOP时间=磁道寻址时间+旋转延迟

 

所以,通过这个公式我们就可以轻松计算给定磁盘的最大IOPS。

而每秒的IOPS数字也是我们最感兴趣的,公式如下:

1秒/磁道寻址时间+旋转延迟。

 

我们来看如下例子:

HP 300GB 15k SAS drive(200刀)
转速 15000
平均磁道寻址时间 2.9ms
平均旋转延迟 1.83ms

 

我们可以用公式计算IOPS了:

IOPS = 1/(2.9ms + 1.83ms)    = 1/(4.73ms)     = 1/(0.00473)    = 211 IOPS

 

    我们可以看到,这个磁盘的IOPS为211(并不是很惨)。

假如我们想要节省更多的钱,我们再来看一个磁盘的例子以及和上面磁盘的区别:

 

HP 300GB 7200 SATA drive(100刀)
转速 7200
平均磁道寻址时间 10ms
平均旋转延迟 2.1ms

 

通过公式可以看到这个磁盘的最大IOPS:

IOPS = 1/(10ms + 2.1ms)    =  1/(12.1ms)         =  1/(0.0121)         = 82 IOPS

 

    这个7200转的磁盘的最大IOPS为82。

通过对比可以看到上面两个磁盘的性能上的巨大差异,这也不难理解为什么同样的容量价格会差这么多。

此外,文档显示如果你使用磁盘到其IOPS峰值,会产生请求队列而造成延迟(一个在SQL Server中非常邪恶的词汇,像躲避瘟疫一样躲避它)。

而我看过的大多数文档建议IOP保持在最大IOPS的80%左右。所以我们上面讨论的第一个磁盘的最大IOPS是211如果服务于超过168的IOPS就会开始显出延迟的征兆了。

现在我已经知道了单独一个磁盘可以达到的IOPS数字,那么下一件事就是要满足生产环境下的SQL Server实例需要多少IOPS?

我获取这些数据仅仅通过在生产环境下查看PerfMon工具的physical disk: Disk Transfers/Sec 计数器。

这个数字是:

 

驱动 平均IOPS 最大IOPS
数据和索引 2313 16,164
日志 81.5 1,127
TempDB 141 2,838

 

通过上面的数据看到,我们的快速磁盘仅仅能处理168的IOPS,所以结论是一个磁盘无论如何也无法满足上面的IOPS要求。

所以解决这个问题的唯一办法是使用某种机制来调整多个磁盘满足上述需求。

如果我们有100个300GB 15k SAS驱动器,我们不仅获得了30TB的存储量,还获得了16800的IOPS。

如果我们使用前面例子中较慢的磁盘,为了达到16800 IOPS,我们需要205块这样的驱动器,这使得我们需要比使用快速磁盘花更多的钱($20,000 vs $20,500),听上去很讽刺,不是吗?

 

RAID的必要性

现在,我们需要一堆磁盘来满足我们的速度或是容量需求,所以我们需要某种机制来将工作负载加到多个磁盘,实现这个目的的主要手段就是RAID。         RAID代表”Redundant Array <of> Inexpensive Disks”(译者注:这是最开始的定义,后来行业标准将I改为Independant,难道这是因为Inexpensive这个词妨碍了他们收取更多的钱?),RAID提供了将一堆磁盘连接起来使得逻辑上变为1个的方法。

基于你如何串联你的磁盘,RAID可以提供容错性—当磁盘阵列中有一个磁盘崩溃时数据不会丢失。

此外,因为串联了多个磁盘,我们可以消除单个磁盘的IOPS限制,更多的磁盘意味着更多的IOPS,就是这么简单。

 

RAID级别

RAID只为了两个目的:1)通过提高IOPS提高性能  2)提供容错。更高的容错性意味着更低的磁盘性能,同样,高性能方案也会降低容错性。

根据容错性和性能的目标配置RAID就是所谓的RAID层级。RAID是对常用的RAID阵列的一种分裂,常见的RAID级别为:RAID0,RAID1,RAID5,RAID1+0,RAID0+1。

根据你对RAID级别的选择,你需要付出所谓的”RAID代价”。某些RAID级别需要重复写入两次数据来保证容错性,但这样会牺牲性能。此外,因为重复写入数据还需要更多的磁盘空间。RAID代价会大大提高你的RAID方案的成本。

为了明白RAID对于你的系统的影响和收益,熟悉常见的RAID级别和它们的实现原理变得非常重要。

 

RAID 0

第一个,也是最基本的RAID级别是RAID 0.RAID 0强调为了解决IO的限制而将数据写入到磁盘阵列中。如果IO希望写100MB的数据,RAID0会将100MB数据写入到磁盘阵列的每个磁盘中。

这种方式大大减少了每个磁盘的负载,并且减少了旋转延迟(每个磁盘不再需要转和原来一样的圈数就能满足请求)。

虽然RAID0大大提高了IO性能,但没有提供任何容错措施,这意味着如果磁盘阵列中的某一块磁盘崩溃,则整个磁盘阵列中的数据全部丢失。

因为RAID0并没有提供任何容错措施,所以在生产环境中RAID0几乎不被使用。

还有一点值得注意的是,由于RAID0磁盘阵列中的每个磁盘都用于存储数据,所以没有任何磁盘空间的损失,比如使用RAID0,10个300GB的磁盘就会有3TB的可用存储空间,这意味着没有损失磁盘空间的RAID代价。

 

RAID 1

RAID1也被称为”镜像”,因为其通过一个镜像磁盘来保证容错性。在镜像集中的每个磁盘都会有一个镜像磁盘,RAID 1写入的每一笔数据都会分别在两个磁盘中各写一份。这意味着任何一个磁盘除了问题,另一个磁盘就会顶上。用户的角度来看并不知道出现了磁盘崩溃。

RAID 1需要付出写入时的性能代价。每个写入IOP需要运行两次,但是对于读来说却会提升性能,因为RAID控制器对于大量数据请求会从两个磁盘中读取。

 

RAID 5

RAID 5也被称为”Striping With Parity)”,这种方式既可以通过磁盘分割(Striping raid0)来提高性能,也可以通过奇偶性(Parity)来提供容错,当一个磁盘崩溃后,奇偶数据可以通过计算重建丢失的数据。

虽然奇偶性是实现容错的一种不错的方式。但是从磁盘写入来说代价高昂。也就是说对于每一个IOP写请求,RAID5需要4个IOPS。

为什么需要这么高写入代价的过程如下:

  •     读取原始数据(1 iop)
  •     读取当前奇偶数据(1 iop)
  •     比较当前数据和新写入请求
  •     基于数据差异计算新的奇偶值
  •     写入新数据(1 iop)
  •     写入新的奇偶值(1 iop)

RAID 1+0

    RAID 1+0 和其名字所示那样,融合了RAID 0(磁盘分割)和RAID1(镜像)。这种方式也被称为:分割镜像。

RAID 1+0 由于将数据分割到多个磁盘中使得并且不像RAID5那样有奇偶效验码,所以写入速度非常快。

但写入速度还是会有影响因为需要重复写入镜像盘,但仍然,写入速度还是非常的快。

而对于RAID 1+0 存储的代价等同于RAID1 (镜像),在RAID1+0中只有一半的磁盘空间可以用于存储数据。

 

RAID 0+1

RAID 0+1 和RAID1 +0 是很像,它们都是通过磁盘分割和镜像来实现目的。他们的区别更加学术化,这里我们假设他们一样。

RAID 0+1和 RAID 1+0所付出的代价是一样的。

 

其它RAID级别(2,3,4,6,DP等)

还有一些其它不常见的非标准RAID层级,RAID 2,3,4,6和RAID DP都和RAID5类似,他们都是通过分割和某种奇偶校验来提供性能上和容错。这些类似RAID 5的RAID层级的区别仅仅是它们如何写入奇偶数据。它们之中有些是通过保留一个磁盘来存储奇偶数据,还有一些是将奇偶数据分布到多个磁盘当中等等。如果需要,你可以去做这些研究,但对于我来说,我都称它们为”RAID 5”

还有一个值得讨论的非标准的RAID级别是RAID DP,DP的是”Dual Parity”的缩写,这和RAID 5很像但其将奇偶数据写入两次,这对于写入来说代价高昂,写入代价被提高到了6(每一次IO写请求需要6 IOPS)

 

RAID 比较

选择合适的RAID层级并不容易,需要考虑多方面因素:成本,性能和容量。

下表总结了每个标准RAID层级的好处和坏处。

RAID Level Fault Tolerance Read Performance Write Performance RAID Write Penalty Cost
0 None Good Excellent 1 Excellent
1 Good Good Good 2 Fair
5 Fair Good Poor 4 Good
1+0 Excellent Excellent Excellent 2 Poor
DP Good Good Terrible 6 Good

 

SQL存储推荐

 

SQL Server文件 RAID级别
操作系统和SQL二进制文件 RAID 1
数据和索引 RAID 1+0 (如果预算不允许可以使用RAID 5)
日志 RAID 1+0
TempDB RAID 1+0
备份 RAID 5

 

其它考虑因素

当需要计划你的IO子系统和SQL文件分布以及RAID层级时,你需要多考虑其它因素。

 

RAID控制器

RAID可以通过2种方式实现:软件实现和硬件实现。

在软件RAID配置中,操作系统管理RAID级别以及多磁盘之间的IO负载。

在硬件RAID配置中,物理上会有一个硬件作为RAID控制器。

通常来说,硬盘RAID解决方案会更健壮,灵活和强大。根据你对RAID控制器的预算,你能获得对应预算的配置选项。

比如,某些RAID控制器仅仅提供一个RAID层级(比如RAID5),一些更昂贵的RAID控制其提供了缓存功能。缓存可以用于缓存读取操作,写入操作以及它们两者。更好的RAID控制器甚至提供了对于读取和写入分配缓存的百分比选项。

缓存对于SQL Server来说非常重要,尤其是对于写来说。无论是何种RAID级别,都没有读取性能上的代价,所有的RAID层级都提高了读取数据的速度。而写入才是RAID的代价,你可以通过RAID缓存来缓存所有的写入操作,这极大的提高了写入性能。通常来说,有缓存的RAID控制器都带有电池,这使得即使断电,缓存的数据也不会丢失。

记住,SQL Server本身非常善于缓存读取,所以使用昂贵的RAID控制器中的缓存来缓存读取并没有什么意义。

 

虚拟化

还有一个值得考虑的因素是虚拟化,无论你喜欢与否,我们已经步入了虚拟化的世界。在VMWare环境下部署生产环境下的SQL Server实例变得越来越普遍。

虚拟化也影响RAID和IO方面,根据你使用的虚拟化产品,当你选择RAID级别时就需要考虑更多的因素,比如,VM是如何和存储系统交互的。

总结

很明显,我们还有一些信息没有讨论到,RAID对于SQL Server性能和容错的重要性不言而喻。

我希望本篇文章能够帮你理解RAID是如何影响你的SQL Server的性能。作为一个DBA或是数据库构架师来说,你必须明白当前RAID配置有着怎样的性能和容错性。

 

参考资料

MSDN:     http://msdn.microsoft.com/en-us/library/ms190764.aspx

TechNet:     http://technet.microsoft.com/en-us/library/cc966534.aspx

Ed Whalen – PerfTuning.com http://www.perftuning.com/files/pdf/RAID1.pdf

 

原文链接:http://www.sqlservercentral.com/articles/RAID/88945/

from:http://www.cnblogs.com/CareySon/archive/2012/05/08/HowRAIDImpactSQLServer.html

SQL Server文章目录

SQL Server的文章写了也不少了,一直没有做一个目录方便大家阅读。现在把之前写的关于SQL Server的文章做一个目录,方便大家阅读 眨眼

 

SQL入门

SQL查询入门(上篇)

SQL查询入门(中篇)

SQL查询入门(下篇)

 

SQL进阶

T-SQL查询进阶–深入理解子查询

T-SQL查询进阶–基于列的逻辑表达式

T-SQL查询进阶–流程控制语句

T-SQL查询进阶–变量

T-SQL查询进阶–数据集之间的运算

T-SQL查询进阶-10分钟理解游标

T-SQL查询进阶–深入浅出视图

T-SQL查询进阶–详解公用表表达式(CTE)

T-SQL查询进阶–理解SQL Server中索引的概念,原理以及其他

T-SQL查询高级–理解SQL SERVER中非聚集索引的覆盖,连接,交叉和过滤

T-SQL查询进阶–理解SQL SERVER中的分区表

T-SQL查询高级—SQL Server索引中的碎片和填充因子

T-SQL查询进阶—理解SQL Server中的锁

 

SQL日志

浅谈SQL Server中的事务日志(一)—-事务日志的物理和逻辑构架

浅谈SQL Server中的事务日志(二)—-事务日志在修改数据时的角色

浅谈SQL Server中的事务日志(三)—-在简单恢复模式下日志的角色

浅谈SQL Server中的事务日志(四)—-在完整恢复模式下日志的角色

 

SQL的一些单篇

T-SQL中的GROUP BY GROUPING SETS

理解SQL SERVER中的逻辑读,预读和物理读

SQL Server中数据库文件的存放方式,文件和文件组

浅谈SQL SERVER中事务的ACID

SQL Server中生成测试数据

SQL Server中灾难时备份结尾日志(Tail of log)的两种方法

从性能的角度谈SQL Server聚集索引键的选择

SQL Server中的Merge关键字

浅谈SQL Server中的快照

细说SQL Server中的加密

C#实现平衡多路查找树(B树)

SQL Server中的执行引擎入门

浅谈SQL Server中统计对于查询的影响

 

SQL Server 2012新特性探秘

SQL Server 2012中的ColumnStore Index尝试

SQL Server2012 T-SQL对分页的增强尝试

SQL Server2012中的SequenceNumber尝试

SQL Server 2012新增的内置函数尝试

SQL Server 2012中的Contained Database尝试

SQL Server2012中的Throw语句尝试

SQL Server2012中的Indirect CheckPoint

SQL Server 2012中的AlwaysOn尝试

 

SQL Server权限

理解SQL Server中的权限体系(上)—-主体

理解SQL Server中的权限体系(下)—-安全对象和权限

 

翻译的文章

【译】初识SSRS —-通向报表服务的阶梯系列(一)

【译】SSRS基础 —-通向报表服务的阶梯系列(二)

【译】无处不在的数据 —-通向报表服务的阶梯系列(三)

【译】Tablix指南—-通向报表服务的阶梯系列(四)

【译】用图表展示未知—-通向报表服务的阶梯系列(五)

【译】设计仪表盘—-通向报表服务的阶梯系列(六)

【译】一些优化你的SQL语句的TIPs

【译】RAID的概念和RAID对于SQL性能的影响

from:http://www.cnblogs.com/CareySon/archive/2012/05/08/2489748.html