2012年12月30日星期日

2012年12月27日星期四

转:各种操作的延迟时间

下图来自此页面:http://blog.hesey.net/2012/06/evolution-of-large-website-architecture.html


另有一张同事贴的页面:http://www.eecs.berkeley.edu/~rcs/research/interactive_latency.html
比较有趣,每年的变化都能看见,不知道是如何预测出来的。

protocol buffers and thrift:当心类型暴涨

最近在阅读另一个部门交接的代码。
尝试增加一个小小的新功能,然后编译、链接……这个过程相当痛苦。
这个部门在数据传输协议上,采用了类似于protocol buffers或thrift类似的技术:用中间语言定义类型,然后用工具编译成C++代码。

这样做无可厚非。但我在阅读代码和编译链接的时候,非常痛苦:
1、 类型实在太多了,类型套类型,分散在很多个不同的目录中;
2、编译不通过,必须把所有引用到的类型的头文件都include进去;链接也不通过,必须把类型的encode/decode库链接进去。

因此,使用protocol buffers and thrift类似的技术初看很好,随着业务的发展,类型越来越多,代码就变得越来越臃肿,越来越难以维护。

我建议,这样去避免类型暴涨:
1. 采用自动编译,把中间的定义文件放在某个目录下,自动生成各种语言需要的代码;
2. 所有的类型放在一起,甚至可以把所有的类型包含在一个all_types.h中;后续的代码要引用类型,包含这一个头文件即可。怕影响编译速度?可以用预编译头文件解决;
3. 所有的类型的encode/decode代码,全部编译后,打包到一个大的all_types.a中,到时候链接一个库即可;
4. 定期清理,不要的类型丢弃。

2012年12月20日星期四

试验:多线程竞争写

下面一段代码将试验不同条件下多线程竞争对变量进行累加的效果:

#include <stdio.h>
#include <pthread.h>

#define MAX_NUM 5000000

int num = 0;

void* thread_func(void* param)
{
for (int i=0; i<MAX_NUM; ++i)
{
++num;
}
return NULL;
}

int main()
{
pthread_t t1, t2;
pthread_create(&t1, NULL, thread_func, NULL);
pthread_create(&t2, NULL, thread_func, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("%d\n", num);
return 1;
}

假设没有竞争,则num最终输出的结果应该是10000000

试验一:执行上面的代码输出5315953
试验二:将蓝色的这行的定义修改为:volatile int num = 0;
        执行后输出5431651。
        虽然没有得到正确的结果,但从数值上看出,volatile对数据同步还是有一些效果的。
试验三:在红色的行前增加一行代码:__sync_synchronize();
        执行后输出7443185
        试验说明,内存屏障能够进一步加快多核间的内存数据同步。
试验四:将红色的这行修改为__sync_add_and_fetch(&num, 1);
        执行后得到了正确结果10000000
        由此说明,多核条件下并发累加,只有原子操作才能得到正确的结果。volatile关键字和内存屏障都不能解决。
==============================================
进一步思考:如下是CPU的结构图
线程在操作num这个数的时候,都会把num载入自己的 L1 cache line
当CPU0上的线程1改写了num的值,L1 cache line为标示为脏。然后,CPU0上的cache line会同步到CPU 1上的cache line,然后CPU1读取数据的时候,就会是最新的数据。

如果按照这种数据同步原理,则试验一应该得到正确的结果。但为什么结果又不正确呢?

CPU, L1 CACHE, 内存,这三者之间的数据同步机制究竟是如何进行的?希望有大虾予以指教。






2012年12月19日星期三

一个失败的经验:把内存映射文件当成共享内存用

服务器开发中常常采用共享内存来存储数据,好处有:
1. 预分配空间,性能高;
2. 服务器崩溃后,共享内存的数据还在,进程重启后可快速回复服务。(这点尤其重要)

但是,共享内存也有比较麻烦的问题:
1. 增加容量很麻烦:要先dump出数据,然后删除共享内存,再重新分配,再写入;
2. local cache带来了单点问题,机器死机或者掉电后,必然丢失数据。

于是,在某次服务器开发的时候,我尝试用内存映射文件来代替共享内存。因为内存映射文件有这样一些好处:
1. 与共享内存一样,进程重启后,数据得以保留;
2. 扩容方便:如果内存允许,我映射更大的一个内存区域就好;
3. 迁移方便:停止进程,然后把映射文件复制到另一个机器,再启动进程即可。

愿望总是美好的!使用内存映射文件后,系统常常莫名其妙地IO猛烈飚高,都是映射文件惹的祸。
首先,内存映射文件分配的是虚拟内存,等到程序访问这个区域后,发现没有对应的物理内存,会引起一个缺页中断,然后操作系统从文件对应的区域中,加载4KB的数据到page cache中。
然后,如果对这一内存区域执行写操作,数据并不立即被写往磁盘,而仅仅只是把这个page标记为脏页。当整个操作系统的脏页达到一定比例后,操作系统就会把这些脏页的数据写往磁盘,由此也就引起了IO突然飚高。


因此,当采用local cache的存储方案的时候,性能是第一个考虑点,其次才是方便进程快速恢复服务。而使用内存映射文件,必然增加了系统的IO,降低了性能,这与服务器开发的目的是违背的。

2012年12月17日星期一

学到两个新词:NOR和NAND

在看GCC原子操作函数的时候发现有个:__sync_nand_and_fetch
什么是nand操作?

找了半天终于找到:
位操作里除了创建的 and, or, xor, not
还有not or和not and
not or等价于: not (a or b)
not and等价于: not (a and b)

VC++里面貌似已经支持两个新的操作符来代表nor和nand: ~|  ~&

搜索到的相关帖子在这里:http://forums.codeguru.com/showthread.php?420830-bitwise-nor-operator


2012年12月10日星期一

测试伪共享对性能的影响

伪共享(false sharing)的知识需要了解的话,请google之。
下面是测试代码:两个线程分别计算5亿次累加两个相邻变量的时间消耗。
//test_false_sharing_1.cpp

#include <stdio.h>
#include <sys/time.h>
#include <time.h>
#include <pthread.h>

#define PACK  __attribute__  ((packed))
typedef int cache_line_int __attribute__((aligned(LEVEL1_DCACHE_LINESIZE)));

struct data
{
    int a;
    int b;
};

#define MAX_NUM 500000000

void* thread_func_1(void* param)
{
    timeval start, end;
    gettimeofday(&start, NULL);
    data* d = (data*)param;
    for (int i=0; i<MAX_NUM; ++i)
    {
        ++d->a;
    }
    gettimeofday(&end, NULL);
    printf("thread 1, time=%d\n", (int)(end.tv_sec-start.tv_sec)*1000000+(int)(end.tv_usec-start.tv_usec));
    return NULL;
}

void* thread_func_2(void* param)
{
    timeval start, end;
    gettimeofday(&start, NULL);
    data* d = (data*)param;
    for (int i=0; i<MAX_NUM; ++i)
    {
        ++d->b;
    }
    gettimeofday(&end, NULL);
    printf("thread 2, time=%d\n", (int)(end.tv_sec-start.tv_sec)*1000000+(int)(end.tv_usec-start.tv_usec));
    return NULL;
}

int main()
{
    data d = {a:0, b:0};
    pthread_t t1, t2;
    pthread_create(&t1, NULL, thread_func_1, &d);
    pthread_create(&t2, NULL, thread_func_2, &d);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("end, a=%d,b=%d\n", d.a, d.b);
    return 0;
}

/*
g++ -o test_false_sharing_1 test_false_sharing_1.cpp -g -Wall -O2
*/
----------------------------------------------
执行后输出:
thread 1, time=4121562
thread 2, time=4329193 

把以上程序稍稍修改:
struct data
{
    cache_line_int a;
    cache_line_int b;
};
//struct中的int修改为按照cache_line对齐的int
然后酱紫编译:
 g++ -o test_false_sharing_2 test_false_sharing_2.cpp -g -Wall -lpthread -DLEVEL1_DCACHE_LINESIZE=`getconf LEVEL1_DCACHE_LINESIZE` 
执行后输出:
thread 1, time=1607430
thread 2, time=1629508 
性能提高了2.5倍。

-------------------------------------------------
测试中注意两点:
1. int重新对齐的定义后,在struct中不要在定义对齐的属性,否则之前的对齐属性会失效;
2. 采用getconf LEVEL1_DCACHE_LINESIZE这样的命令获得cache line的大小;
3. 编译中不能加上-O2,否则编译器计算会导致瞬间出结果;(这个优化真是强大啊)

2012年12月2日星期日

刚刚知道了一个牛叉的工具:Google App Inventor

同事在群里贴了这张图,顿时觉得很震撼:



居然还可以画出这么漂亮的程序流程图。

更牛叉的是,这个工具是google用于生成ANDROID APP的。可以一行代码都不写就创建应用。


2012年11月28日星期三

python: 转换被错误定义为unicode类型的utf-8编码

某同事的代码中发现,本来是utf-8编码的字符串,从数据库读出来后,被错误返回为u'utf-8内码'这样的格式。

下面是一个简单的转换方法,还原其为正常的utf-8编码:

>>> a = '测试'
>>> a
'\xb2\xe2\xca\xd4'
>>> b = a.decode('gbk').encode('utf-8')
>>> b
'\xe6\xb5\x8b\xe8\xaf\x95'
>>> c = u'\xe6\xb5\x8b\xe8\xaf\x95'
>>> c
u'\xe6\xb5\x8b\xe8\xaf\x95'
>>> arr = array.array('B')
>>> arr.fromlist([ord(i) for i in c])
>>> print arr.tostring().decode('utf-8').encode('gbk')
测试 

最终的代码是这样:
import string
import array
s = u' \xe6\xb5\x8b\xe8\xaf\x95 '
arr = array.array('B')
arr.fromlist([ord(i) for i in s])
print arr.tostring().decode('utf-8').encode('gbk')

如果读者知道更好的转换方法,希望不吝赐教。

2012年11月27日星期二

安装python图标库pycha搞得很郁闷

根据帖子推荐,发现pycha是所有python图标库中比较简单的,于是打算采用这个。
结果,安装这个库花了我两个半天,仍未能解决,打算放弃。

1. 首先在pycha的首页https://bitbucket.org/lgs/pycha/wiki/Home找下载地址。
   找了半天居然没有,只有个项目管理软件hg的地址。
   好吧,先安装windows下的hg客户端。
2. 安装hg客户端:
  http://cdn.bitbucket.org/tortoisehg/files/downloads/tortoisehg-2.6.0-hg-2.4-x86.msi
3. 使用hg下载源码:
  hg clone http://bitbucket.org/lgs/pycha
4. 安装上了没效果,原来还需要库 cairo。于是下载cairo
  http://www.cairographics.org/releases/py2cairo-1.10.0.tar.bz2
5. cairo库不同于一般python库的安装,居然还需要python的头文件,于是再下载python源码包来编译:
    http://www.python.org/ftp/python/2.7/Python-2.7.tgz
6. 再编译cairo,指定头文件的路径,仍然编译不通过……

选择一个不成熟的库,果然很折腾啊!





2012年11月26日星期一

评《技术含量的问题》

原帖请见:http://www.dbanotes.net/review/Tech_Simple.html

不是很赞同冯大辉的观点。

    不重视技术问题,因技术的成本高转而采用人工;或者是技术成熟度没达到人工水准,继续采用人工——这些只能在一个特定的阶段来使用。
    比如,公司刚刚成立,还处理生存的压力下,无可厚非。
    而对于一个已经起步的公司,这样做无疑饮鸩止渴:

1. 这是技术债务,总有一天要还的。
   借来的钱一开始用得很爽,等到要还债的时候,就会变得痛苦。

2. 某技术当前阶段的发展不如人工,但不代表它永远都无法超越人工的水平。
   现在不去积累,失去了先机,放弃了成为技术壁垒的决心……等到它成熟的时候,你已经再也没机会去捡起来了。

3. 廉价的人力资源也是资源,随着环境的变化,它慢慢也会成为稀缺资源。
   当前珠三角的用工荒,已经说明了这个问题。

  因此,特别是大公司,应该在某些技术领域投入资源去进行攻关。如果总是以为没技术含量的手段也能解决问题,技术债务总有一天会令它头破血淋。

2012年10月25日星期四

小记:解决mysql机器上的单CPU过高

最近某一mysql机器有告警,显示单CPU占用常常超过90%。
奇怪的是,机器上4个CPU,仅CPU 0的占用较高,而其他三个CPU很闲。

继续检查发现,CPU 0的占用高,主要都花在iowait上。
可是,即便是IO高,也应该每个CPU的IOWAIT都高,毕竟mysqld是一个多线程服务器。

为了验证这点,输入:top,按f,按j,按空格,按shift+h——查看每个线程的执行情况,并显示该线程正在哪个CPU上执行。

观察发现,mysqld的线程几乎都在cpu 0上执行,难怪CPU 0的占用高。
于是简单地写个脚本来分担CPU 0的压力:

vi set_mysqld_cpu_affinity.sh
#!/bin/bash


v_list=`ps -Lf -C mysqld h| awk '{print $4}'`
for p in $v_list
do
        taskset -cp 1,2,3 $p
done

执行后CPU的IO WAIT都变得比较平均了。

2012年10月10日星期三

想要家庭版的照片云服务软件

有没有这种一种家庭版的照片云服务软件,支持酱紫的功能:
1. 各种终端可以自动通过WIFI同步最新拍照的照片;
2.自动根据拍照时间和地理位置建立索引;
3. 自动识别横竖;
4. 去重,始终不会存储同样的图片;
    如果A图是B图的一个子集,则A图只需要很小的存储空间即可
5. 海量存储;
6.自动生成各种规格的缩略图,可快速浏览大量的图片;智能识别终端,总是输出合适的大小。
7. 按照色彩搜索:选择某种简单的色调,或者模糊定义色彩丰富和色彩单调
8. 相似照片归类:根据16*16的缩略图的二进制相似算法来对图片进行聚类
9. 比较基本的功能:文字备注,按备注文字搜索
10. 进一步:语音备注,语音识别
11. 涂鸦:在触摸屏上对照片写写画画能保存下来,而且能识别是哪个家庭成员的涂鸦
12. 人脸识别,按照人脸聚类
13. 先对图片高斯模糊,提取特征,然后按照特征进行聚类。
    比如有山的图片和有海的图片会归类到一起。
14. 照片中的文本识别:照片中出现字母或数字,能够被搜索出来。

===========================================
2012-10-15续:
配合家庭使用的文件存储服务器,家庭版的照片处理云服务会大有可为,这可能是一个比较热门的细分市场:
1. 手机、数码相机、数码摄像机等设备的价格降低和兴起,必然使得一个家庭在日积月累中产生大量的照片和视频;
2. 由于中国的网络带宽的限制,不太可能把那么多的照片都上传到网络上共享,且用户还担心隐私的问题;
3. 如何快速找到想要的照片?独特的照片搜索将会让用户更加离不开家庭版的云服务。

2012年9月14日星期五

python PIL: 图片的倒影效果的简单实现

具体的算法就是把图片的每一行对称翻转,然后将透明度从低到高逐行设置。
见代码:


import sys,os
from PIL import Image

if __name__=='__main__':
    argc = len(sys.argv)
    if argc<3:
        print 'usage:%s <file> <dest>'%sys.argv[0]
        sys.exit(-1)
    im = Image.open(sys.argv[1]).convert('RGBA')
    arr = []
    (w,h) = im.size
    x = 0
    y = 0
    index = 0
    arr_row = []
    for pixel in im.getdata():
        pixel = list(pixel)
        x = index % w
        y = index / w
        index += 1
        pixel[3] = int(256.0*0.2*(float(y)/float(h)))
        arr_row.append(tuple(pixel))
        if index%w==0 and index>0:
            arr = arr_row + arr
            arr_row = []
    #
    im1 = Image.new('RGBA', (w,h))
    im1.putdata(arr)
    im1.save(sys.argv[2])

原图和效果见下面:


复习:多阶hash表

关于多阶hash表的具体代码实现,请移步到:《使用共享内存的多级哈希表的一种实现》http://webcache.googleusercontent.com/search?q=cache:GEiOeyiYdXEJ:www.cppblog.com/lmlf001/archive/2007/09/08/31858.html+&cd=2&hl=zh-CN&ct=clnk

本文主要讲多阶HASH表的结构。

1. 多阶hash表实际上是一个锯齿数组,看起来是这个样子的:
■■■■■■■■■■■■■■■
■■■■■■■■■■■■■
■■■■■■■■■■
■■■■■■
■■■

每一行是一阶,上面的元素个数多,下面的元素个数依次减少。
每一行的元素个数都是素数的。

2. 数组的每个节点用于存储数据的内容,其中,节点的前四个字节用于存储int类型的key或者是hash_code

3. 创建多阶HASH的时候,用户通过参数来指定有多少阶,每一阶最多多少个元素。
那么,下面的每一阶究竟应该选择多少个元素呢?从代码注释上看来,是采用了素数集中原理的算法来查找的。
例如,假设每阶最多1000个元素,一共10阶,则算法选择十个比1000小的最大素数,从大到小排列,以此作为各阶的元素个数。通过素数集中的算法得到的10个素数分别是:997 991 983 977 971 967 953 947 941 937。
可见,虽然是锯齿数组,各层之间的差别并不是很多。

4. 查找过程:

   先将key在第一阶内取模,看是否是这个元素,如果这个位置为空,直接返回不存在;如果是这个KEY,则返回这个位置。
   如果这个位置有元素,但是又不是这个key,则说明hash冲突,再到第二阶去找。
   循环往复。


5. 好处:
  1. hash冲突的处理非常简单;
  2. 有多个桶,使得空间利用率很高,你并不需要一个很大的桶来减少冲突。
  3. 可以考虑动态增长空间,不断加入新的一阶,且对原来的数据没影响。
整理后的源码在此:https://docs.google.com/open?id=0B2ZH5H4iY-oLSXdPSDVoVFBoSVU


2012年8月30日星期四

小记:python中用PIL进行图形合成

网站上需要展示一个类似如下的图:

整个图包含了三部分:背景,带透明度设置的阴影,以及封面。三个图通过程序合成成以上的样子。

用PIL实现极为简单,但摸索了差不多半小时才搞好,贴出来,或许对某人有用:


bg = Image.open(r'F:\temp\bg.png').convert('RGBA')
shadow = Image.open(r'F:\temp\shadow.png').convert('RGBA')
cover = Image.open(r'F:\temp\cover.png').convert('RGBA')
#下面这步是关键,蒙版的对象,采用自身即可根据透明度进行运算
bg.paste(shadow, ((bg.size[0]-shadow.size[0])/2,(bg.size[1]-shadow.size[1])/2), shadow)
bg.paste(book, ((bg.size[0]-book.size[0])/2,(bg.size[1]-book.size[1])/2))
bg.save(r'F:\temp\result.png', format='png')

2012年8月12日星期日

阿福技术BLOG所有文章打包下载

https://docs.google.com/open?id=0B2ZH5H4iY-oLdWYtMnlsblcya0U 看来,百度空间的业务对于百度来说是鸡肋,百度要放弃这个业务了。 不但故意改版得很难用,还提供文件打包下载的功能,潜台词是:东西都给你打包好了,滚吧,爷不伺候了……

2012年8月5日星期日

关于QQ音乐的一些看法

QQ音乐是我工作之余使用得比较多的一个腾讯的产品,因为已经使用了腾讯的其他很多服务,在选择音乐播放器上,也习惯性地选择了QQ音乐。 毕竟我自己不是音乐发烧友,仅仅只在闲暇时或者累了,放点音乐来舒缓的时候,才打开软件听歌。 使用QQ音乐有很长的时间了,但觉得这款软件时感觉越来越难用,老实说,它一直没让我爽过。 直到今天,偶然使用一款叫做飞乐电台的名不见经传的的播放器,惊奇的发现,里面推荐的歌我都非常喜欢。 为什么,一个亿级用户的公司,推荐的歌居然没有一款小软件的歌好听? 看看QQ音乐的新歌列表里,都是一些没见过的歌手,唱的一些不知所谓的烂歌,正常人一般都是听两首就听不下去的。 换了角度,假设我是QQ音乐的产品经理,我会怎么去考虑:坐拥上亿用户,不愁没流量;中国的盗版严重,从用户侧是收费很难;因为流量大,所以唱片公司很愿意接进来。 好吧,只要唱片公司给推广费,我就把你们的烂歌推到第一条,保证每天有百万次甚至千万次的曝光。 有钱赚,很好! 可用户不见得喜欢! 过度商业化,导致产品变成令用户讨厌的东西,而产品存在的意义则是对用户产生价值。这种背道而驰的例子,QQ音乐只是其中之一,如同百度,起家之时就是这么一路走过来的。 看来,创业小公司还是很有机会的,你们能抛开过度商业化去做用户真正喜欢的产品,直到赚钱的的目的胜过创造用户价值。

2012年8月1日星期三

遐想:微博的架构(一)

    不管是遐想还是瞎想,自己想试着从架构师的角度去试试,如果我自己去做一套微博系统,我会怎么去设计。于是想写一系列的文章,假想如何去构建一套微博系统。

1. 首先假想:在构建系统以前,一些基础服务已经完备,不必再重头再去一一构建。这些基础服务包括:

  日志服务:保存各种日志
  异步消息总线服务:用于系统间的解耦,进行异步消息通讯。
  监控服务:监控各种运营数据,便于发现问题。
  配置服务:提供一个容灾的集中的中心配置点,所有的相关配置都可以从配置中心里获取,简化运维,典型的比如开源的zoo_keeper
  日志计算服务:map-reduce, 典型的如hadoop,用于离线的各种数据分析。
  分布式key-value存储:由唯一的一个key快速定位到value,提供get/set/remove三个基本操作。实现为分布式系统,可自动扩容。
  分布式文件系统:可以存储海量的小文件,诸如图片服务等。
  CDN服务:做内容分发,把内容复制到离用户最近的访问点上去。
  短URL服务:把一个长URL转成极短的URL。
  以上服务都是逻辑上极度简单的,便于理解。但实现上有很不简单,比如分布式、比如容灾等,假想他们都是稳定而又可靠的。

2. 第二个部分是微博的存储:

   先看微博本身的特点,最长140个字,假设使用UTF-8编码,则每条微博最多140*3=420字节。因此1kb的存储空间可以存储两条微博,加上微博本身的一些元数据,以每条微博512字节来进行存储吧。
    已经定义了每条微博固定占512字节,则微博可以顺序存储,把所有微博放在一个连续的大文件里,根据偏移量就可以随机定位到任意一条微博。单个文件肯定是不行的,因此一个机器上可以放多个文件,多个机器分别存储不同内容,再按群集部署,一定的群集放在某个机房内,再进一步扩展,不同的城市再建立多个机房……
    城市,机房,群集,机器,文件,偏移量……以上的信息都用一个数字来编号的话,每条微博最终都能得到一条唯一的ID。 不过,城市和机房等因素一般是用来做异地容灾的,与ID无关。
    再者,由于微博的完全开放的,因此就算其他人猜出任意一条微博的ID,也不存在隐私方面的问题。就算用同样的存储结构存储私信等内容,仍可以在微博的额外的92个字节的meta区域加上权限和类型等信息,然后接入服务器读取微博的内容后,判断当前的读取者无权获得此条微博,再返回错误就行。
    决定了微博的存储结构和存储方式,下一步是确定微博的数据如何分布到多个文件,多个机器,甚至多个群集上。
    从群集上看,按用户进行分区是好办法。假设当前有1000万用户,则可以每个群集服务100万用户,一共十个群集即可。用户量增加后,再按群集进行扩展。
    微博的特性也决定了,微博是按时间顺序来进行读取的。因此第二个分区的维度是时间:简单的把当前发表的微博顺序存储在大文件中,然后返回微博的ID即可。一个文件写满后写另一个,一台机器写满后再写另一台……这样也带来了缺点:群集中存储最近的微博内容的机器总是IO最高的,其他的则比较闲。因此也可以在写微博的时候,前端随即分发给所有群集内的服务器,每个服务器内部进行切换文件。当所有机器都达到一定量级后,在群集内增加机器,把比较老的文件迁移到新的机器上,然后写分发从原来的N台机器变为N+M台。


    顺序存储带来的另一个问题是:修改和删除怎么办?好在微博的特点就是,不支持修改,且删除极少。对于删除的情况,修改92字节的meta部分,把微博标记为已经删除即可。前端的调用者读到微博已经删除的标记后,丢弃到这条数据。删除仅仅只是在文件内留下512字节的尸体,毫无压力。
    这样的存储不仅仅存储微博:转播,评论,私信,短URL等内容都可以以这样的方式保存下来。
    存储量的问题:TWITTER现在的发表量每天3.4亿条(参考:《 用户达1.4亿 每天Tweets发布量达3.4亿条 》, http://web2.iresearch.cn/87/20120324/167421.shtml ),按照这样的存储方式,则每天增加的存储空间为162Gb。sina微博每天发表1亿条微博: http://www.bjnews.com.cn/finance/2012/05/16/199252.html,存储空间占用为47.7G。考虑异地容灾,存储两份或者三份的话,压力也不算太大。
    关于存储的另一个考虑是:如果不固定分配512字节,按照紧凑存储的话,会更省空间。假设已经统计到平均每个用户的微博长度为200字节,加上头信息和预留存储位,可能最多300字节就能存储一条。因此,这里可以折中一下,把存储单元按128字节划分为一个块,一条微博最多占用4个块,也可以只占用一个块。假设平均的微博长度仅占用两个块,则上面计算出的3.4亿条微博每天的存储量减半,达到81.1G。



3. 存储之上:近期热点数据,CACHE,传输,聚合拉取等问题

    有了存储后,不可能用户读取微博的时候,都从磁盘去读取,一定是要有CACHE的。
    并且,微博的访问特点与时间相关性很大,比如,最近一两天的微博是最热的;最近一两个星期的内容,还有人会去看看;超过一个月以前的内容,可能几乎没人去访问了。
    根据这个特点,我们构建一个三级的存储:所有微博的最终落地是磁盘,容量大,成本低;最近一个月的微博,采用SSD存储,性价比适中;最近一周内的微博,全部存储在内存中。
    假设微博新增量有twitter那么大,那需要的容量为:(按最大存储容量计算,按照平均长度计算的话,容量减半)
        内存:162G*7 = 1134G
        SSD: 162*30 = 4860G
        磁盘:162*365 = 57.7T/每年
    内存,SSD,磁盘,其存储的格式可以都保持一致,仍可以以唯一的微博ID索引到。一条最新的微博会同时存在与内存,SSD和磁盘中;内存和SSD中的内容,相关的服务进程会定期对超时的数据进行淘汰。存储层的接口服务器来决定把请求转发到内存,SSD还是磁盘。
    另一个可能是,在SSD或磁盘中的某条微博突然死灰复燃,又火起来了。在架构上,可以在SSD存储和磁盘存储之上再加自己的CACHE层,把访问得多的数据CACHE住,避免偶发性的热点数据的情况。
    从这样的设计也可以看出,总体上采用了有损服务的策略:仅保证了近期的微博有较好的读取速度,晚一点的就会越来越慢。

    再一个问题是传输:每条微博最多512字节,你想到了什么?小数据量的高性能传输,是使用UDP通讯的极佳场合:
    1. 从性能上而言,网络上的IO次数,TCP的极限是2万次/s,而UDP的极限是10万次/s;
    2. 一条微博完全可以放在一个UDP包内,甚至放多条也是可以的;
    3. 丢包的问题:极端情况下,承载一条微博内容的UDP包丢了,会怎么样?那就让它丢吧,让用户在某个时间点少看一条微博又不会死,更不会怀孕……没什么大不了!
    4. 在系统内部的复杂网络环境里,可以做到点对点通讯。或许你认为点对点通讯没什么大不了的,我来举个例子:假设使用TCP协议,服务器A请求服务器B,服务器B再请求服务器C,服务器C响应给服务器B,服务器B再响应给服务器A,类似代理一样,司空见惯,没什么异常的;但是,如果使用了UDP,则可以有这样的效果:服务器A请求服务器B,并在包体中带上自己的IP和端口,服务器B转发请求给服务器C,服务器C响应的时候,根据包体内的地址,直接响应给服务器A。P2P通讯的好处就是每个代理服务器负责转发就好,不必等待请求的返回,代理本身的吞吐量提高了,且缩短了回路的路径长度,效率更高。

   下一个问题是微博主页的聚合拉取。经过前面的介绍,我们了解到如下信息:
       1. 微博有唯一ID,可以快速定位到确定的一条微博;
       2. 近期的微博都在内存中,访问极快;
       3. 机器间的微博使用UDP传输,性能极其快……
   有了上面的铺垫,下面看起来不可能实现的功能也就得以实现了:每个人收听了N个人,N个人发了M条微博,则进入微博主页的时候,需要拉取N*M条数据,然后按时间排序,显示给用户。
    计算方法很简单,但数据量非常大,要做到快速显示出结果,只有一个方法:并行计算。
    之前有提到,服务器群集按用户来划分,某部分用户存储在某个群集中。首先假设每个用户收听的人,还有每个人发表微博的ID列表这两种数据是已经存储好了的。拉取首页的流程如下:
    1. 主服务器拉取当前用户的收听的人的列表;(花费时间T1)
    2. 主服务器根据用户分区的规则,把获取这个人微博的请求分别发到对应的群集上,然后等待一定的时间;(发出请求只花费很少的时间,忽略不计,自身最多等待T2时间)
    3. 群集服务器收到请求后,请求用户创建微博的ID列表,返回给主服务器;(花费时间T3)
    4. 主服务器对N个用户的M条微博ID按时间排序,确定分页,取某个ID段;(花费时间T4)
    5. 主服务器把微博ID发到多个微博存储服务器,等待时间T5;
    6. 微博存储服务器根据ID返回微博,花费时间T6;
    7. 主服务器输出内容……完成!
    按照最慢的时间:T1+T2+T4+T5,T2,T5这个等待时间段内,可能都没返回,可能返回了一部分;都没返回就让用户重试,返回了一部分就显示这部分。有损服务,总比让用户一直傻等的好。
    最快的情况是T3<T2, T6<T5,数据都拉到了,皆大欢喜。
    在T3, T5这两个步骤,请求是分布在多个机器上完成的,庞大的数据分摊给了整个群集,而非顺序地逐个执行,因而性能得以提高。

2012年7月24日星期二

搬家,再次搬家

约在06年左右在CSDN写技术BLOG。
后来CSDN的服务实在是太烂了,无法忍受。
后来找到了百度hi,刚开始用用还可以……
直到最近,一个白痴产品经理把百度的博客系统也搞到无法忍受……

不得已,再搬家!
希望,这里不再把我赶走!