缓存,缓存,缓存(二)—— 言归正传说twitter

上一篇我们花了大部分的口水去说MQ,现在我们要言归正传说说twitter的架构了。

我们说到我们要应付的事情出了大量的消息要转发要处理,还有突然爆发的流量等等。解决这样的问题,思路应该是要提高服务器每秒对请求的响应数。通常我们是将Memcached作为Fragment Cache放在我们前端服务器上。当收到用户请求的时候我们的Router会去判断这样的Fragment Cache是否存在,如果存在的话则直接返回。这样的做法是有效率的,而且就memcahced而言,命中率很高,我们很难去确定请求的来源位置,所以我们不如把预先计算好的数据放在网络前端的RAM上面。

当然在这个之后我们还会有很多的问题,并且在twitter向我们展示的宏大架构中我们找到了一些有用的东西。

首先我们看看twitter用到的这些中间件。

  1. memcached,这个就不说了。
  2. Varnish,是取代Squid的一个HTTP缓存,也就是一个Page Cache,据说性能至少是Squid的四倍。这个数据来源也是据说挪威的在线报纸vg.no,用三台Varnish替代了12台Squid,效果还比之前要好。
  3. Kestrel,是twitter团队开发的一个开源的MQ,持久存储效果不错,听说代码写得也很漂亮1200来行,Kestrel是基于Memcached的文本协议,并对这些协议有所优化和完善。这个东西最初是用Ruby搞的后来又在Scala上实现了一遍。Ruby下的那个版本伸缩性不强,特别是Ruby的GC(垃圾处理)不是分代的,这意味这GC在处理完一个队列之后就会把自己给kill掉,然后MQ就崩溃了。后来移植到Scala上,它有成熟的JVM GC这个机制,而且代码还可以更简要。
  4. Comet,一种服务器push的技术,之前facebook貌似也在用。简单的说,就是Client发送一条请求,然后Server接收到之后进入一个无限循环,将Client需要的数据push到一个response里面并且刷新它,这个response并不会被关闭,而是不断的被push数据然后刷新。知道Client断开连接之后才跳出这个循环。正是这样,我们可以说,Ajax解决了单用户的响应,而Comet则在保障性能的前提下,解决了协同多用户的响应。Comet的优点是它可以随时向客户端发送数据而不是仅仅响应用户的输入请求。而且这个数据是在现有的一个单连接上面进行,所以大大降低了发送数据的延迟时间(建立链接的开销和用户发送请求的开销)。

按照twitter自己的说法,最初他cache的策略简单到无敌,有点让人蛋疼,除了在API那里有一个Page Cache之外,然后啥都没有了。不过还好他80%的流量是来自API的。后来随着发展,twitter进行了强有力的架构的建设,其中cache策略的发展也是飞速。

一、Vector Cache,直接放在Data Pool的上面。存放了tweetID和tweetID之间的关系,tweetID是序列化的64位整数。而直写式的设计保证了极高的命中率,使得程序在Data Pool中工作的效率大大的提高,并且极大程度上加大了Data Pool和上层Cache的关联。

二、再往上是一个Row Cache,存放了数据库信息,主要是用户和tweet详细信息的相互对应。同样是直写式,并且使用了一个叫做Cache-money的程序,这是一个针对rail的ActiveRecord的直写式缓存实现。Rail的ActiveRecord具有很强的灵活性,完全不亚于hibernate,而且使用简单,效率也很高。

三、接着往上依次便是用memcached做的Fragment Cache和用Varnish做的Page Cache。其中Fragment Cache是直接消费Row甚至是Vector中的数据,打包成JSON或者XML。虽然这是一个直读式的Cache,但是由于特殊的层级关系和工作特性,他的命中率依然能达到95%以上。

这样的缓存策略下,数据库的大部分压力都被转到了后端程序中,而后端程序对资源的占用较为平滑,可预估性也比较强。

另外twitter还提到要为Page Cache提供一个独立的池,理由很简单,他们希望能够为Page Cache提供一个永远不会自毁的Cache的,而是一种按时间来划分的键值模式,以致于可以根据HTTP的协议头来对数据进行切片。我不喜欢这样的方式,虽然它应该会很有用,但是当所有的流过的页面都要在留下痕迹时,情况也许就会变得不太可控。

这样的Cache的架构也有他的问题,因为底层的两个直写式的Cache总是要去修补上面直读式Cache中的数据,这必然会造成MQ的压力。但毕竟有舍才有得,或许这才是架构的魅力吧。

缓存,缓存,缓存(一)—— MQ

今天看了twitter关于cache的策略,领略了cache的威力,抒发了一下情感。

Everything runs from memory in Web 2.0.

优化Cache的策略是为了什么,不外乎三点:

  1. 减少IO,减少传输,减少CPU计算,减少同时工作的服务器的数量。
  2. 对结果进行共享,比如搜索,通常我们将关键词和结果串化进行Cache,然后在时间范围内使用Cache来对分享服务器工作结果。
  3. 更快。这永远都是终极的目标。

通常的架构,对于Cache而言,我们有两层,最前端是一个Page Cache,然后是Memcached,最下来就是Data Pool。
simple cache
这样的架构下我们能够解决大部分的问题,特别是在处理更多常态化的需求时,访问量平稳,数据提交远小于数据读取。但是这么做有瓶颈特别是对于2.0的网站,我们要应付越来越多的用户数据,要不断的缩短Page Cache的有效时间,我们还得应付更多的异常的流量爆发,于是这样的简单的策略就不顶用了。除非我们不计成本的增加我们的服务器,加大我们的带宽,但也许你的老板会说找你还不如请IDC的人吃饭。我们是架构师嘛,为了不让老板这么说,那我们就得找出策略来。

对于无穷无尽的用户数据的提交,我们通常会想到MQ。说说MQ是个什么东西。他是一种消息队列的处理方式,消息列队是分布式应用间交换数据的一种技术,队列被存在磁盘或者RAM里,队列里面是消息,消息直到它被app读走才被删除。这种机制使得app可以独立的运行,它们不必知道各自的位置和状态,执行过程中也不需要等待其他app的响应。

MQ的最上层是一个消息管理器,下面是队列,队列里存放消息。中间是通道,这里是通道是消息传递的管道,是建立在物理网络中的概念,可以说通道MQ整个概念的精华。通道通常被分为三种,消息通道,MQI,Cluster通道。消息通道是一个单向的通道,有Post, Get, Request, Server等不同的类型,一般是建立在MQ服务器与其他事务服务器之间;MQI是建立在app与MQ之间的,是双向的通道;而Cluster通道则是建立在不同的MQ服务器或者同一MQ集群下的不同消息管理器之间。

MQ的工作原理差不多可以来说。
mq
我们先看看单个系统的情况。app1发送一条消息到MQ,并告诉它消息属于队列1,于是MQ则将消息放到队列1里面,app2便可以读取这个消息然后MQ等待app2读取后把这条消息del掉。

下面这个便要复杂一些。app1发送一条消息到MQ,并告诉它消息属于队列2,然而MQ将消息放到队列2之后发现,这个队列其实是在sysB里面的,这时候MQ会把消息放到一个特殊的传输队列中,然后我们建立一个cluster通道,这个通道更像是一个代理app,这个代理app读到数据并传输到sysB,而在确认sysB真正获取到消息之后MQ才释放掉这条消息。在这里我们意识到MQ在确保消息的准确传输,并且一条消息只有一次这样的传输。

移步:缓存,缓存,缓存(二)—— 言归正传说twitter

php的Socket

当我们需要在不同服务器或者不同的语言之间做数据交换,我们有一种方法是用Socket做数据代理。Socket是源于UNIX的套接字,基于TCP/IP协议,是从UNIX早期的命令集中演化而来的,基础的模式是“连接-读写-关闭”。Socket可以应用于B/S和C/S两种不同的网络软件架构上,现在已经被广泛的引用。
php的Socket模块虽然相对比较简陋,一些复杂的应用会出现一些莫名奇妙的问题,但是单单作为基础的数据代理来讲还是经受了人们的检验。

一、Socket传输的是什么

Socket传输的是字节流,没有定义边界。只是通过调节缓冲区的大小来完成流的截断,当然设置缓冲区的大小都是必须的,否则Socket也许会一直工作下去。Socket传输的数据很容易被截取并且会直接展示出来,所以你通常需要对你的数据进行加密,比如AES,就是很好的加密算法,不要用MD5这样的散列表,否则你会死得很惨。
PHP似乎想要get字节流加上边界,在它的socket_write这个函数中声明的三个参数,它的文档里面说如果你将第三个参数设置为PHP_NORMAL_READ,那么将会读取到/r或者/n,可惜,当我这样做的时候,程序溢出了。原因至今不明。

二、用php做一个SocketServer

我们先抛弃类,这篇将没有一个类定义出现。

前面我们已经说了Socket的工作模式是“连接-读写-关闭”。作为服务器,我们要做的事情是建立一个socket,绑定一个端口,然后监视每一个连接,读取他们传来的消息,并给予他们反馈,然后将这个连接毙掉。现在我们开始

< ?php
//===================
//SockServ.php
//===================
define('SOCKIP', 'localhost');
define('SOCKPORT', '12345');
 
/**
 * Set up our socket
 */
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); //创建一个socket
printf("Socket created.\r\n");
socket_bind($sock, SOCKIP, SOCKPORT); //绑定一个端口
socket_listen($sock); //开始监视了
printf("Socket has set up.\r\n\r\n");
 
while(true) {
	$conn = socket_accept($sock); //抓到一个,允许它的连接
	if ($conn) {
		printf("==========================================\r\n");
		printf("Socket connected.\r\n");
		while($data = socket_read($conn, 1024)) { 
		//千万不能加上PHP_NORMAL_READ啊
			$buffer = $data;
			printf("Data Received.\r\n");
			print_r("Buffer: ".$buffer."\r\n");
			socket_write($conn, "OK"); //告诉客户端,OK,我收到了
			printf("Successed!\r\n");
		}
		socket_close($conn); //工作完成,你可以去了
		printf("Closed the socket\r\n");
		printf("==========================================\r\n\r\n");
	}
}
?>

上面这段程序,你最好别用浏览器来跑,你可能会得到一个超时,你应该在命令行里面跑。在服务器上,让它跑在后台,然后就让它呆在那里吧,没事的。

三、还有客户端呢

我们当然还需要一个客户端,不然你执行上面程序时会一直停在“Socket has set up.”。客户端的任务就是在浏览器里打开一张页面程序,向服务器端发送那么一个字符串,然后接受服务器返回的“OK”。我们开始吧

< ?php
//===================
//SockClient.php
//===================
define('SOCKIP', 'localhost');
define('SOCKPORT', '12345');
 
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); //创建一个socket
$connStat = socket_connect($sock, SOCKIP, SOCKPORT); //连接到服务器
if ($connStat) { //连接成功
	echo '<p>连接成功!';
	$buffer = 'Hi, Server!';
	socket_write($sock, $buffer);
	while($data = socket_read($sock, 1024)) {
	//依然切记不要PHP_NORMAL_READ
		if ($data == 'OK') echo '<p>OK,搞定!</p>';
	}
	socket_close($sock); //你可以安心的去了
} else { //连接失败啦 
	echo '<p>连接居然失败了,是不是服务器没开啊!</p>';
}
?>

这个程序是跑在浏览器上面的,通常呢,我们会写成一个类这个类独立在系统的架构之外,将这个类文件交给负责不同app的程序员,让他们来调用,从而实现和服务器的通信。

四、当然这是个很简单的例子

实际上,我们通常有更复杂的事情去做,但是并不代表对于socket来说我们要做更多的事情,我们只需要在客户端的数据开源(现在是直接定义的),服务器端的数据处理(现在只是打印到屏幕上),还有服务器根据客户端数据进行不同的反馈以及客户端对反馈的处理,这几个方面进行扩展,便可以完成几乎所有的需求。
Socket作为一种有效的数据代理方式,还能够为更好的程序架构提供帮助,比如我们提供更多的服务器数据交换来降低前段服务器的计算压力,Socket本身有很好的栈策略也能在服务器的部署上带来帮助。