21 Jul

论Object.wait()要放到while循环里

wait()放while里面算是一个常识性的准则。为什么要这样呢,如果放到if里面会有什么后果?今天水木有人贴出了一段出错的代码,对这个问题现身说法:

public class A {
        private Object[] queue = new Object[1024];
        private int cMsg;

        public synchronized boolean accept(Object msg, Object token)  {
                if (cMsg >= queue.length) {
                        try {
                                wait();
                        }
                        catch (InterruptedException e) {
                                return false;
                        }
                }

                queue[cMsg++] = token;
                queue[cMsg++] = msg;
                return true;
        }

        public synchronized Object[] getMessages()  {
                if (cMsg == 0) {
                        return null;
                }

                Object[] tmp = (Object[]) Arrays.copyOf(queue, cMsg);
                Arrays.fill(queue, 0, cMsg, null);
                cMsg = 0;
                notify();
                return tmp;
        }
}

这个代码在大并发下测试,抛出了java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 1025异常。

要分析原因的话,就是wait()被唤醒后,队列已经满了,cMsg >= queue.length这个条件已经不满足了,再往后移下标的话就数组越界了。

问题是为什么wait()唤醒后队列会满。在代码里,将队列清空后,才执行notify(),这个时候它应该只唤醒了一个线程,那么谁把队列填满的呢

答案是一个阻塞在accept上面的线程。首先要知道一点:其他线程收到信号并不是在notify调用的那一刻!notify的信号是在退出同步函数后才发出的,从退出同步函数,到信号发出,这中间有个时间差,因而就有可能出现以下执行序列:

1. getMessages方法中的 notify()调用
2. getMessages退出,此线程A释放类实例上的monitor
3. 一个阻塞在accept上的线程B,得以进入accept方法,因为此时数组被清空,线程B填入数据,下表+2;accept不断的调用,直到数组被填满,而阻塞在wait()调用上
4. notify信号发出,一个线程C被唤醒。这时没有再判断数组下标位置,直接想数组中塞数据,数组越界。

解决这个问题的方法当然就是把if改为while:

                while (cMsg >= queue.length) {
                        try {
                                wait();
                        }
                        catch (InterruptedException e) {
                                return false;
                        }
                }

在被唤醒后,重新判断条件是否满足。

19 Jul

如何停止一个超时的线程

经常,我们启动了一组线程,让他们去工作,并等待他们完成,获取他们的返回结果。为了保证程序不会卡死在这些线程的执行上,我们为线程设定了一个超时时间,希望线程如果超过这个时间没有完成,就终止执行。

在线程启动的时候,为其建立一个定时器就可以计算超时时间了。那么,问题就是,如何在一个线程已经超时的时候,停止这个线程的执行。

这是个从线程出现以后就在纠结的问题。

Java最初提供了一个Thread.stop方法用于终止线程,但是这个方法随后就因难以解决的线程安全问题被标记为deprecated,不建议继续使用。Thread.stop的原理是让线程抛出ThreadDeath异常(确切的说,它是一个Error),由于程序都不会捕获这个Error,所以这个线程依次退出调用栈,最终退到栈底而终止。在退栈的过程中,会执行所有的finally代码块,并释放线程持有的所有的锁!看起来这是一个设计相当完美的方案,当初那帮设计者应该会被自己的聪明智慧感动得流泪了吧。然而在实际使用的时候,由于被终止的线程释放了所有的锁,被这些锁保护的对象都变得可以被其他线程所访问,从而引发了意想不到的问题——问题可大可小,可能根本察觉不到,也可能造成莫名其妙的错误。

在剥夺了Thread.stop方法后,JDK转而建议使用共享条件变量来控制线程退出,就好似这样:

Class MyThread extents Thread{

    public volatile boolean stop = false;

    public void run(){
        dosomething1();
        if(stop){
            return;
        }

        dosomething2();
        if(stop){
            return;
        }

        dosomething3();

    }

}

需要停止时只要此线程的stop设为true就行了。这需要小心翼翼的编码,如果在最耗时的操作中间没有对退出标识进行判断,那所有其他的工作也就是白费了。

设置条件变量仍然没有办法处理线程被阻塞的情况(如调用Object.wait()、ServerSocket.accept()和DatagramSocket.receive()等方法时)。一个阻塞中的线程是没法检查条件变量的,它只有等到条件满足,解除阻塞时,才能对条件变量进行判断。这时候可以调用Thread.interrupt方法打破阻塞。Thread.interrupt会使目标线程抛出InterruptedException异常,这个异常通常在代码中被捕获,线程因而跳过阻塞的请求,继续执行。

所以应如下停止一个线程:

MyThread thread = new MyThread();
//wait for some time.
thread.stop = true;
thread.interrupt();

事情还没有完。有些阻塞的线程是不响应Thread.interrupt方法的(例如阻塞在socket.accept() 等旧式IO请求上),Thread.interrupt可以打断的阻塞调用只有Object.wait, Thread.join和Thread.sleep三种。对于这些情况没有通用的处理方法,只能却别对待。例如对于阻塞在socket.accept()的线程,我们可以调用socket.close()来接触阻塞。需要说的是,这种不响应Thread.interrupt的阻塞线程,也不会响应Thread.stop。

综上:多线程是强大的工具,但是也面临很多难以解决的问题,停止正在执行的线程就是其中之一。

14 Jun

难题有多难

2010-06-14的有道难题有多难?

答案是非常难。

内部赛时间延长到5个小时,还是只有46个人曾经提交,只有四个人做对了一题……

晚上很困,特地泡了两杯茶。看来多此一举了,反正出来的都是wrong answer。这个比赛平台很高级,也不会给出错误的用例,只能眼睁睁的看着错误。

但是,现在喝完很精神,可以不睡觉了……

07 Jun

Google App Engine的性能问题

在春节之后,就明显的感觉到google app engine的稳定性和性能都变差了,部分时段还出现down机或者访问缓慢、大量请求失败的情况,cpu cost飙升到以前的好几倍。看到其他一些使用google app engine的人也出现了类似的问题。

Pcworld在最近的一篇报道中写到,google在推出app engine企业版两周后,发现数据存储的性能急剧下降,数据的读写速度降低,并且容易失败。数据存储的性能问题在两个月以前已经出现,在企业版推出后愈发严重了。app engine的服务压力在以每两个月25%的速度增长,这么汹涌的速度即使是google也感受到了痛苦。

31 May

这是一个伟大的时刻

这是一个伟大的时刻。终于,克服封闭的腾讯和“开放”的Google种种奇怪的限制,我的BLOG能够同步到Qzone了!无论Qzone多么封闭,无论google app engine的限制多么奇异,都挡不住一颗同步的心!

28 Apr

Memcachedb与Tokyo Tyrant

Memcachedb与Tokyo Tyrant这两个东西,出发点不同,但结果却有点殊途同归。Memcachedb以bdb作为后端,给memcached增加持久化功能、事务支持及主辅同步等功能;Tokyo Tyrant 则是为 Tokyo Cabinet提供了网络接口和前端缓存。最终的结果,都是出现了一个支持高并发的分布式持久存储系统,适合于高速读写、无严格事务要求的应用场景。Tokyo Tyrant 同时提供了兼容Memcachedb的接口,因而理论上memcached的各种client也可用于Tokyo Tyrant 。

Memcachedb比较一提,因为它是国内的老牌门户sina的开源作品,广泛用于新浪博客等产品线中,像digg.com这样的站点也使用了memcachedb。虽然在此之前,cache+key-value数据库的方案其实已然大行其道,但首先开源的新浪,还是值得表扬表扬的。

Memcachedb引入bdb持久存储的代价还是不小的,虽然数据是先写到bdb的过程是异步的,性能上的cutoff还是相当客观;其并发写入的性能降到memcached的一般一下,不过读数据的性能损失较小。

22 Apr

UTF-8到GBK转码的特殊字符问题

Unicode字符集现在有超过10万个字符,其BMP部分也有六万多个字符;而GBK字符集只有两万以前多个字符。这样的话,从支持unicode字符集或者unicode字符集BMP的编码方式,转化到GBK编码的时候,就会有编码落到GBK字符集以外,不能转化成GBK编码。在java中,转换之后的字符串,这部分字符都变成了’?’。

通常这些都是非常生僻的字符,倒是可以不考虑;但是有一个特殊的unicode字符,不在GBK字符集中。却频繁用于xml/html等格式的文件中。这个字符unicode序号为0xA0,utf-8编码结果为C2A0,作用是一个排版空格——普通的ascii空格在xml/html中是被忽略的。大量UTF-编码的网页使用这个字符用作占位的空格。而且似乎浏览器对它的处理方式也不同:IE8浏览器会认得这个空格,firefox3.6简单的把它替换成 。当把一个utf-8编码的网页转成gbk编码时,这个字符就变成讨厌的问号了。

处理方法,就是在字符串以GBK编码写出之前,把这个字符替换掉:

str = str.replace('\u00A0', ' ');

彻底而保险的方法是过滤所有GBK不能表示的字符:

str = str.replaceAll("[^\u4E00-\u9FA5\u3000-\u303F\uFF00-\uFFEF\u0000-\u007F\u201c-\u201d]", " ");

13 Mar

洪强宁谈豆瓣网技术架构

Q: 各位观众朋友大家好,这里是InfoQ中文站的赖翥翔,现在在首届 QCon北京大会的现场,坐在我旁边的是来自豆瓣网的洪强宁。强宁你好,向大家介绍一下自己以及自己和豆瓣的联系。

A: 我是大概在06年的3月份加入豆瓣的。当时应该是豆瓣的02号程序员。01号是阿北。现在是任豆瓣的首席架构师。负责豆瓣技术开发的相关工作

Q: 我记得在之前社区中有对豆瓣高并发能力的讨论,豆瓣现在的用户数量以及访问量如何?用了多长 时间达到了现在的水平?

A: 现在的话,我刚才没有上网,不知道现在是不是已经达到了300万用户,如果还没有达到的话,马上就会到了,可能是今天,可能是明天。300万是指我们的注 册用户,另外还有千万级的非注册用户。访问量的话,现在应该是两千万每天。

Q: 如果能达到这样的访问量,确实说明豆瓣高并发的能力是相当强,我想请您从技术这个角度介绍一 下豆瓣网的架构。

A: 这个话题比较大一点,我刚才在演讲的时候,已经表述这方面的问题了。可以这么说,最简单的方法来说,豆瓣网可分割成两大块:一块是前端的Web,也就是用 户在浏览器访问的时候会触发一系列的操作,从数据库拿出数据,渲染成HTML页面反馈给用户,这是前端;另外一块是后端,在豆瓣有一个很强的数据挖掘团 队,每天把用户产生的数据进行分析,进行组合,然后产生出用户推荐,然后放在数据库里面,前端会实时的抓取这些数据显示给用户。

Q: 如果是这样子,要是让你重新设计的话,你会觉得有必要改进里面哪些部分 吗?

A: 豆瓣(架构)设计现在在WEB这一端主要是用这么几种技术:前端是nginx和lighttpd,中间是Quixote的Web框架,后面是MySQL以 及我们自己开发的DoubanDB。这些除了Quixote都是一些比较流行的、尖端的技术。Quixote稍微老一点,如果要重新设计的话,可能会在这 方面做一些考虑。比如Python社区中的Django、Pylons等等都是可以考虑的,那么在豆瓣的内部的话,我们一般是用web.py,很轻量的一 个Web框架来做,也是非常不错的选择,它可能需要自己做的事情多一点。但是,也不太可能完全重新设计了。

Q: 那如果要缓解高并发所带来的压力,Cache的利用肯定是一个非常有效的途径。那么豆瓣的缓 存命中率一般是多大?这方面的策略是怎样?

A: Memcache命中率一般都在97%左右,应该还算是比较高的。策略其实是比较简单的,如果每次要去执行一个比较耗时耗资源的操作,比如说去数据库查询 的话,就会以Python的Object形式存放在Memcache里面,下次再拿这个数据的时候就直接从Cache中拿就行了。这边选择什么样的东西, 尽量有一个Guideline,必须是要耗时的,耗资源的,而且是重复使用的。比如它是耗资源的,但是只用一次,Cache也没有意义。差不多用这种方法 保证Cache的东西都是真正有效的,也提高了命中率。

Q: 要提高承受高压力的流量,另外一个有效的措施是对数据库来进行分区分片,在这方面豆瓣是怎么 做的?

A: 豆瓣现在还没有达到数据库分片的程度。我们现在最常见的手段是,按照功能分区。我们会把数据表分成几个独立的库,现在是一共有4个库。每个表都是库的一个 部分,每个库会有主副两个。通过这种方式来减轻数据库的压力,当然这个是现在的方案,再往后的话,表的行数会增长,到达一定的程度后,还要进行水平分割, 这是肯定的。然后我们现在的技术方面,在操作数据库之前,首先获取数据库的游标,有一个方法,这个方法会干所有的事情,我们以后做的时候会从这个方法中进 行判断该从哪取东西。这个架构已经在了,只是现在还没有做这一步而已。

Q: 数据库这边主要采用什么解决方案呢?

A: 在数据库这边,我们主要用的是MySQL。MySQL有一个问题,大文本字段会影响它的性能。如果数据量过大的话,它会挤占索引的内存。那么现在一个行之 有效的方法是,我们另外建立一套可伸缩的Key-Value数据库,叫做DoubanDB。我们把不需要索引的大文本字段,放到DoubanDB里面去。 MySQL只保存需要索引的Relationship这方面的信息。这样给MySQL数据库降低了压力,也就可以保证它的性能。

Q: 比如说像保证数据的安全性,以及数据库的吞吐量,豆瓣是怎样的策略呢?

A: 首先DoubanDB会把每个数据在三个节点进行备份,任何一个出现故障都不会影响索取数据。MySQL是通过双Master方案,同时还会带1到2个 slave,所以说在MySQL中我们会有三到四个的备份。这点是可以放心的。

Q: 你刚才说到MySQL的双Master方案,这方面会不会存在什么问题?比如说同步的问题, 等等?

A: 在MySQL里面,双Master方案是一个比较经典的方案,我们现在用它很大一部分是为了解决我们同步延迟的问题。在做切换的时候,会出现同步延迟的问 题,但其实MySQL的同步速度还是可以的,在切换的时候,我们会忍受几秒钟等待同步的时间。在做脚本的切换的时候,我们会稍微等一下。

Q: 豆瓣的数据表一般是怎么样的规模?

A: 数据表,这个不好说了,因为不同的表都是不一样的。我们最大的表是“九点”的Entry表,“九点”的爬虫爬过来的所有的文章,现在应该有四千万左右的行 数。然后其他的上百万的表也有很多。还有包括收藏表也有千万级的行数。

Q: 在这种海量数据的情况下,对数据表的就结构变更,一定是一个比较麻烦的问题。常见的情况, 比如增加一个新的索引,会导致索引好几个小时。像豆瓣之前会存在这样的问题,是怎么解决的呢?

A: 这个问题曾经让我们吃过苦头,在忽视它的状况下就去改表,然后就锁了很长时间。后来我们意识到这个问题,如果有表的改动的话,我们会先在一个测试的库上试 验一下它的时间长短,是不是在可接受的范围,如果是可接受的范围,比如说几分钟,就做一个定时任务,在深夜里面去执行。如果耗时是不可忍受的,就必须通过 其他技术手段,我们现在的手段一般是建一个新表,这个新表从旧表同步数据,然后再写数据的时候,也会同步,往两边写,一直到两边完全一样了,再把旧表删 掉,大概是这样一个方式。

Q: 刚才您好像提过你们设计了自己的DoubanDB,还有一个是DoubanFS,这两者关 系是怎么样的?

A: 首先是先出来的DoubanFS,我们刚开始的时候用MogileFS来解决我们可扩展图片存储的问题,由于MogileFS有一个重型数据库,这成为了 它的性能瓶颈。我们为了解决这个问题,开发了DoubanFS,基于哈希来寻找节点。之后,我们又发现了新的问题,数据库中的大文本字段也会影响性能。所 以,我们在DoubanFS的基础上,换了一个底层,做了一些调整,参照Amazon的dynamo思想,搭建了DoubanDB,把文本字段放在 DoubanDB里面。做完之后,又反过来用DoubanDB来实现FS,大致是这么一个过程。

Q: DoubanFS跟DoubanDB的实现,他们在对于内容的安全性,或者内容的冗余性…

A: 都是(备份)三份。这都是可以配置的,现在的配置是3份。

Q: DoubanDB就是用什么机制实现的?

A: DoubanDB简单来说是这样子:你来一个Key,它是Key-Value数据库,你要写或读的时候,通过这个Key来寻找这个值。拿一个Key对它做 哈希,通过Consistent哈希方法去查找它在哪个节点上,然后往这个节点上去写或读。在这个节点上,顺着哈希的wheel顺次的找到第二、三个节 点,写的时候会保证这三个节点都写,读的时候是任意一个,如果其中一个读失败了,会自动切换到下一个。

Q: 您刚才提DoubanDB的话,是采用的技术是?

A: DoubanDB的底层存储用的是TokyoCabinet,是一个很轻量级、高效的Key-Value数据库。我们在它的基础之上,做了分布式,用这种 方式来实现的。

Q: 实际上有一些其他的方案可以解决,比如说像Berkeley DB(简称BDB)、CouchDB等等,你们为什么要选择TokyoCabinet?

A: 最简单的原因是由于它足够快,实际上BDB跟它比较类似,BDB更加强大一些。对我们而言,我们在这边就是需要一个可靠、高效的Key-Value存储, 这两个其实是我们都可以替换的,只要统一下接口就可以。CouchDB的话就是另外一个东西了,它是一个文档型数据库,它不仅仅做了一个Key- Value的工作,它还在这上面做了很多其他的事情,比如它有View的概念,可以进行query。这些TokyoCabinet是没有的,而我们暂时也 不需要这些功能。CouchDB是一个很有意思的数数据库,我们可能会在其他方面(应用),我们也在研究它。

Q: 从我们刚才的讨论中,Web前端你用了nginx又用了lighttpd。它们都是非常流 行的前端,这两种方案经常打架,豆瓣为什么把它们融合在一块?

A: 这是历史原因。我们其实没有刻意地去倾向某一个。这两个都是非常优秀的Web Server,都很轻量,都很高效。最开始的时候我们用的是lighttpd,然后是因为出现过一些问题,其实不是lighttpd的问题,但当时我们怀 疑可能是lighttpd有问题,就尝试了一下nginx,觉得这个也不错,然后这个结构就保留下来了。nginx对开发者和用户的友好性都更好一些。我 举个例子,比如说重启,其实在豆瓣的Web Server是经常要重启的,我们会有一个健康检查的脚本,定时的检查网站是不是正常,如果觉得不正常的话,就会做一些保护措施,其中就包括重启。 lighttpd的重启,是一个很粗暴的Kill。Nginx是一个reload的方案,会先把手头的事情做完了再重启。这样会好很多,而且它会在重启之 前会帮你做一些好的事情。所以,现在我们用Nginx越来越多。Nginx的配置文件也比lighttpd写起来更舒服一些。

Q: 豆瓣现在有一个庞大的用户群体,针对这样一些海量数据做好数据挖掘,肯定不是一件容易的事 情,能从技术这个角度讲讲挖掘的实现吗?

A: 在豆瓣专门有一个算法团队,他们的主要工作就是数据挖掘。这边讲技术实现的话,可能就讲不完了。只能讲一些大概,数据挖掘是怎么和前端结合起来的,让用户 看见的。每天用户在豆瓣上的操作都会产生很多数据,在豆瓣上面看到的东西,收藏的东西,都会存在数据库或是访问日志。每天这些信息都会传到算法团队的机器 上,然后会从这个数据中建立一个稀疏矩阵,你看过什么,干过什么。他们维维护了一个很高效的稀疏矩阵运算库,然后用它来做各种各样的尝试,去看是否能得到好 的结果,一旦发现这个结果很好,就会把它写到数据库里面。然后用户在访问的时候,前端从数据库中取出推荐给你的数据,然后把这些数据做一些过滤(比如你读 过的东西就不再给你展现了)、调整,最后展现给用户。基本上是这么一个逻辑。

From http://www.infoq.com/cn/interviews/douban-hqn;jsessionid=ED7EE84F6F31059E218CA06C5A4D33E6

03 Dec

康神之bbs心得

原作者:kxn@bbs.Tsinghua.edu.cn / smth.org / bbs.newsmth.net
康神kxn的blog:kangkang.org 内含:康神之心得、发布Fterm最新版本、提交Fterm的bug
注:这是康神的一篇旧作,转载时请勿删除以上信息,以示对原作者kxn之尊重!谢谢!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   我们现在提到的 BBS ,通常指的都是 Telnet BBS ,用一个 term 软件连接上,就可以看到文本的界面,比起如今花哨到无以复加的 WWW BBS 们来可谓是简陋到了极点,然而就是这样的 BBS,无数人每天面对它长达两位数小时还乐在其中,恐怕 UI 设计专家们知道也要气到吐血。也不时有人发表预言,预言 Telnet BBS 将很快消亡而被更加富有表现力的WWW BBS全面取代,只是年复一年,当年的预言者已经消失不见,BBS 上的用户数目却翻了一番又一番。。。这就是 Telnet BBS 的魅力。
Telnet BBS 系统数目众多,但是从根源找起,大致可以分成两大家族,Firebird BBS 和 Maple BBS,在大陆 Firebird BBS 的变种占据了绝对优势,在台湾地区则是 Maple BBS 的天下,由于台湾地区计算机发展历史比较长,因此 BBS 的人气也比大陆高,同时上站人数过万的站点有好几个,不过大陆毕竟有着人口优势,近年来教育网几大 BBS 的人数也迅速增长。下面我们就分别介绍这两大 BBS 家族。首先是在大陆最为流行的Firebird BBS ,最有名的 SMTH BBS, YTHT BBS,  Firebird 2000 三大流派都是由此而来。
很久很久以前,有那么一群大学生,也可能是科研机构的研究员什么的,他们整天在Unix主机上面打滚,觉得要是能在主机上面做一个论坛样的东西多好,于是他们就写了一个命令行程序,运行这个程序,操作者可以在界面下面留言,为了让多个人同时可以操作这个系统,他们把这个程序设置为系统某个用户的 shell ,每个 telnet 上该主机的用户,只要使用这个用户的用户名和密码登陆,就可以进行交流。这就是Internet BBS 的雏形。经过一段时间的发展,这个系统具有了相当多的交互功能,用户不仅可以留言,还可以互相发送信件,发送信息,看到同时在线的用户等等。

BBS 系统的开发者们为了让更多的人能使用这个系统并完善之,将BBS系统以开源协议发布于网络上面。只要拥有Unix主机,就可以取得源代码并安装BBS系统。因此BBS系统以很快的速度发展起来。在众多BBS系统中,某个叫做 Pirate BBS ,经过某些人修改后叫做  Eagle BBS 的分枝,流传入了台湾地区,交大资讯工程系从他发展出了Phoenix BBS,Phoenix BBS 是如今大部分中文 Telnet BBS 系统的祖先,然而它的名字却远不如其后辈响亮,在它的基础上由中正资工进一步修改的 BBS 系统,被赋予了那个大陆 BBS 开发者耳熟能详的名字 -- Firebird BBS。
   应该说, BBS 系统在传入台湾地区时候虽然功能还比较简陋,但是 BBS 系系统的基本架构已经定型,比如多进程模型,共享内存信息交换,利用系统信号来传递呼叫消息,用文件存储文章和索引等,这些设计在现在的 BBS 系统中大部分还在沿用,其中不少设计即使现在来看,也是相当标准有效的多进程 Unix 服务器设计。
   下面我们进入正文。
   Telnet BBS是一种流行于大学和研究机构中的电子公告牌系统,和时下流行的Web BBS系统不同,bbs的界面采用纯文本方式表现,用户使用终端软件连接bbs系统,文本界面在服务器端生成并发送出来,客户端软件仅原样显示文本内容,属于一种瘦客户机的应用。Telnet bbs (后面除非特殊提到,否则简称bbs)在台湾地区和大陆的教育网地区比较流行,比较大规模的站点在线人数一般都在万人以上。
   由于历史原因,bbs系统采用的是Unix下相当传统的1:1多进程模型,每进程处理一个连接的模型,此种模型的好处是服务相对比较稳定,不会因为一个用户出错导致整个系统的不可用,但是也带来耗费资源较多和进程之间通信比较困难的问题。Bbs服务器端的复杂逻辑也使得分布式设计很难实施。因此 bbs通常是单机承担几乎所有负载,大陆地区较大规模的bbs服务器上经常同时保持超过7000进程,台湾地区的bbs站甚至有并发20000进程以上的纪录。
   我们在维护大型bbs站点的过程中,积累了一些优化和维护如bbs这样高并发进程服务器的经验,考虑到1:1进程模型服务仍然有很广泛的应用,在这里写出和读者共享。

   优化服务器是综合性的工作,不仅需要修改代码,还需要调整系统参数,包含有很多琐碎的内容,根据目的来讲,大致可以根据节约资源的类型分为磁盘IO优化,内存优化,和CPU优化等几方面。下面介绍的优化思路虽然应用于bbs,但是也适用于其他应用系统。
1: 磁盘IO优化
磁盘IO优化可以说是服务器优化中最重要的一环,除了极少数的纯计算性应用,几乎所有的重载服务器最后都是卡在磁盘IO瓶颈上面。
a) 尽量使用shm等IPC手段而不是文件
多进程和多线程相比,最大的麻烦是不同执行环境交换信息不方便,因此很多程序员选择了使用文件交换信息,例如最早的bbs设计中,用户的帐户信息是存在于文件中的,进程从文件中读出内容,有修改后就写入文件。改进后的设计是将账号信息文件完整读入共享内存,所有修改都写入共享内存,然后由外部进程定时往磁盘上面同步。
甚至flock这样看起来不会造成太多IO的同步操作都应该尽量避免,原因是flock需要先open文件,而open 文件需要找到i节点,因此会占用文件系统的inode缓存空间,可能造成其他IO操作的性能降低。在很多情况下面需要的只是一个跨进程的mutex,可以使用0/1信号灯来实现。
b) 使用应用层缓存。
很显然,操作系统的缓存会受到很多因素的干扰,对于一些确定会经常访问的内容,例如版面的最新几片文章和最新列表,如果放入shm中缓冲,性能会有大幅度提升。
c) 尽量减少关键IO数据结构的大小
Bbs 文章列表的索引文件是由定长数据结构构成的,在这个数据结构中为了将来扩展方便,留下了很多保留域,造成了很多不必要的IO,删除不必要的域之后,数据结构变小了一半,减少了很多IO。很多时候,扩展性和性能其实是对立的,如果很需要性能,那么损失一定的扩展性也是不错的选择。
d) 避免在同一目录下放过多文件或者使用合适的文件系统
大部分文件系统对在同一目录中的文件列表采用线性存储,因此在一个目录下面存在很多文件的时候,打开文件变得非常的慢,因此通常要将文件根据某种规则散列到不同的子目录中,例如,文件 Atest 会被存放在 A/Atest ,如果文件太多,可能会需要对子目录下面的文件再次进行散列。
另一种解决文件过多影响效率的方法是使用有特殊优化的文件系统,例如Linux下的reiserfs。在这些文件系统中,目录中的文件列表是用平衡树来组织的,因此同一目录下面可以同时有数十万个文件而不会降低太多性能。
e) 根据系统的访问模式选择适合的硬件配置和系统参数
Bbs 系统使用零散的文件存放文章,它的访问模式基本是小文件随机读写,而文章数据相对比较重要,因此bbs使用strip大小比较小的raid5比较合适。文件系统选择专门为小文件优化的reiserfs,系统的预读长度也可以调小一些,Linux 默认的长度是 256K, 有些偏大。如果是大文件连续读写的话,那么raid的strip 大小和系统的预读长度应该放大,文件系统则尽量选择结构简单的文件系统例如ext2/3 等,如果数据并不是非常重要,那么甚至可以取消raid5,代之以raid0或者直接使用单独的硬盘。
2: 内存使用优化
BBS系统使用的多进程模式相当耗费内存,在bbs发展过程中,最早遭遇的瓶颈就是内存。减少内存的不必要浪费,可以节约出来作为系统缓存,从而间接提高更重要的IO性能
a) 尽量避免动态初始化常量,使用const说明将变量和常量区分开来。
Unix 系统在fork出新进程的时候,子进程和父进程共享相同的空间,之后按照COW机制,对修改的页面才进行复制操作,常量如果可以预先计算出来(例如一些转换表之类),就应该尽量避免在运行时动态初始化。另外因为只要修改一个字节,整个页面就都会被复制,因此应该避免常量和会被修改的变量混在一起,编译器本身会自动将不会被修改的内容放在一起,程序员需要做的事情,就是用const通知编译器哪些内容是不会被修改的。
b) 减小内存的峰值使用,特别是堆栈中内存
很多人习惯写程序时候在堆栈上声明一个比较大的临时数组,认为退出函数之后这部分内存会自动被释放。殊不知这样分配的内存并不会被动态被系统回收,因为系统并没有一个明显的标记可以得知堆栈内存是否还在使用中,特别是在多线程的环境下面,操作系统通常采用的措施是需要的时候分配页面,但是在进程退出之前并不回收。即使是通过malloc分配的堆内存,其页面是否回收也视库函数的实现而不确定。因此在无论什么情况下,贸然分配过大的内存,都会对性能造成一定的影响。
c) 如有可能,尽量使用shm来保证页面一定会被多个进程共享。
3: CPU优化
这里说的CPU使用优化,不包括像使用hash来代替线性查找这类最基本的算法优化,而是涉及一些和系统关系比较密切的操作。
a) 使用针对硬件优化的编译器
这应该是所有CPU相关优化中最容易做到也是最容易看到效果的,Intel CPU的Linux系统上面使用 Intel C/C++ 编译器,可以获得很好的效果,甚至AMD的Athlon系列CPU也能获得一定程度的加速。。Bbs进站时候需要初始化很多内容,计算量比较大,使用gcc 时候负载在4左右,使用icc编译以后负载马上下降到1以下。推荐编译时候针对特定CPU指令集优化并且打开跨文件优化选项(-ipo)
b) 使用单独进程来初始化和维护共享内容,避免出现竞争导致逻辑错误
严格讲这并不能提升很多性能,只是为了减少多进程服务器上面经常出现的逻辑错误。在原始的bbs设计中,共享资源的创建是由第一个访问的进程在打开失败时候创建的,但是重负载服务器上面有时候打开也会失败,从而导致多次创建共享资源。
c) 序列化容易导致负载上升的行为
BBS进程在进站时候需要进行很多的初始化工作,同样进程退出的时候也要做很多的收尾工作,此时对CPU或者IO的占用比较大,通过一个互斥锁可以使多个进程不要同时进行这些操作,否则系统负载有可能上升到一定程度引起正反馈,导致系统彻底崩溃。
d) 尽量减少信号的使用
Unix系统下面对于信号的实现的代价是比较大的,同时信号本身也很容易导致处理逻辑的混乱。高负载服服务器应该尽量减少信号的使用。
e) 对于大范围IO读取操作,使用mmap调用
使用mmap操作比传统的read操作好处是减少了一次内核态到用户态的拷贝。在大范围IO操作的时候具有优势,bbs中使用mmap操作来在文件中搜索内容,速度最高时候提高了5倍左右。但是需要注意的是,mmap并不适用于有写入的情况,因为mmap写盘的时候是以页为单位进行操作,页中只要有一个字节被改写,就要往磁盘上面写整个页面的数据,无端增加了IO量。
以上是我们在维护大型bbs站点时积累的一些经验,供各位读者参考。

28 Nov

为什么要用非关系数据库?

From javaeye

随着互联网web2.0网站的兴起,非关系型的数据库现在成了一个极其热门的新领域,非关系数据库产品的发展非常迅速。而传统的关系数据库在应付 web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,例如:

1、High performance - 对数据库高并发读写的需求
web2.0网站要根据用户个性化信息来实时生成动态页面和提供动态信息,所以基本上无法使用动态页面静态化技术,因此数据库并发负载非常高,往往要达到每秒上万次读写请求。关系数据库应付上万次SQL查询还勉强顶得住,但是应付上万次SQL写数据请求,硬盘IO就已经无法承受了。其实对于普通的 BBS网站,往往也存在对高并发写请求的需求,例如像JavaEye网站的实时统计在线用户状态,记录热门帖子的点击次数,投票计数等,因此这是一个相当普遍的需求。

2、Huge Storage - 对海量数据的高效率存储和访问的需求
类似Facebook,twitter,Friendfeed这样的SNS网站,每天用户产生海量的用户动态,以Friendfeed为例,一个月就达到了2.5亿条用户动态,对于关系数据库来说,在一张2.5亿条记录的表里面进行SQL查询,效率是极其低下乃至不可忍受的。再例如大型web网站的用户登录系统,例如腾讯,盛大,动辄数以亿计的帐号,关系数据库也很难应付。

3、High Scalability && High Availability- 对数据库的高可扩展性和高可用性的需求
在基于web的架构当中,数据库是最难进行横向扩展的,当一个应用系统的用户量和访问量与日俱增的时候,你的数据库却没有办法像web server和app server那样简单的通过添加更多的硬件和服务节点来扩展性能和负载能力。对于很多需要提供24小时不间断服务的网站来说,对数据库系统进行升级和扩展是非常痛苦的事情,往往需要停机维护和数据迁移,为什么数据库不能通过不断的添加服务器节点来实现扩展呢?

在上面提到的“三高”需求面前,关系数据库遇到了难以克服的障碍,而对于web2.0网站来说,关系数据库的很多主要特性却往往无用武之地,例如:

1、数据库事务一致性需求
很多web实时系统并不要求严格的数据库事务,对读一致性的要求很低,有些场合对写一致性要求也不高。因此数据库事务管理成了数据库高负载下一个沉重的负担。

2、数据库的写实时性和读实时性需求
对关系数据库来说,插入一条数据之后立刻查询,是肯定可以读出来这条数据的,但是对于很多web应用来说,并不要求这么高的实时性,比方说我(JavaEye的robbin)发一条消息之后,过几秒乃至十几秒之后,我的订阅者才看到这条动态是完全可以接受的。

3、对复杂的SQL查询,特别是多表关联查询的需求
任何大数据量的web系统,都非常忌讳多个大表的关联查询,以及复杂的数据分析类型的复杂SQL报表查询,特别是SNS类型的网站,从需求以及产品设计角度,就避免了这种情况的产生。往往更多的只是单表的主键查询,以及单表的简单条件分页查询,SQL的功能被极大的弱化了。
因此,关系数据库在这些越来越多的应用场景下显得不那么合适了,为了解决这类问题的非关系数据库应运而生,现在这两年,各种各样非关系数据库,特别是键值数据库(Key-Value Store DB)风起云涌,多得让人眼花缭乱。前不久国外刚刚举办了NoSQL Conference,各路NoSQL数据库纷纷亮相,加上未亮相但是名声在外的,起码有超过10个开源的NoSQLDB,例如:

Redis,Tokyo Cabinet,Cassandra,Voldemort,MongoDB,Dynomite,HBase,CouchDB,Hypertable, Riak,Tin, Flare, Lightcloud, KiokuDB,Scalaris, Kai, ThruDB,  ......

这些NoSQL数据库,有的是用C/C++编写的,有的是用Java编写的,还有的是用Erlang编写的,每个都有自己的独到之处,看都看不过来了,我(robbin)也只能从中挑选一些比较有特色,看起来更有前景的产品学习和了解一下。这些NoSQL数据库大致可以分为以下的三类:

一、满足极高读写性能需求的Kye-Value数据库:Redis,Tokyo Cabinet, Flare
高性能Key-Value数据库的主要特点就是具有极高的并发读写性能,Redis,Tokyo Cabinet, Flare,这3个Key-Value DB都是用C编写的,他们的性能都相当出色,但出了出色的性能,他们还有自己独特的功能:

1、Redis
Redis是一个很新的项目,刚刚发布了1.0版本。Redis本质上是一个Key-Value类型的内存数据库,很像memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过10万次读写操作,是我知道的性能最快的Key-Value DB。
Redis的出色之处不仅仅是性能,Redis最大的魅力是支持保存List链表和Set集合的数据结构,而且还支持对List进行各种操作,例如从List两端push和pop数据,取List区间,排序等等,对Set支持各种集合的并集交集操作,此外单个value的最大限制是1GB,不像 memcached只能保存1MB的数据,因此Redis可以用来实现很多有用的功能,比方说用他的List来做FIFO双向链表,实现一个轻量级的高性能消息队列服务,用他的Set可以做高性能的tag系统等等。另外Redis也可以对存入的Key-Value设置expire时间,因此也可以被当作一个功能加强版的memcached来用。
Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,并且它没有原生的可扩展机制,不具有scale(可扩展)能力,要依赖客户端来实现分布式读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。目前使用Redis的网站有 github,Engine Yard。

2、Tokyo Cabinet和Tokoy Tyrant
TC和TT的开发者是日本人Mikio Hirabayashi,主要被用在日本最大的SNS网站mixi.jp上,TC发展的时间最早,现在已经是一个非常成熟的项目,也是Kye-Value 数据库领域最大的热点,现在被广泛的应用在很多很多网站上。TC是一个高性能的存储引擎,而TT提供了多线程高并发服务器,性能也非常出色,每秒可以处理 4-5万次读写操作。
TC除了支持Key-Value存储之外,还支持保存Hashtable数据类型,因此很像一个简单的数据库表,并且还支持基于column的条件查询,分页查询和排序功能,基本上相当于支持单表的基础查询功能了,所以可以简单的替代关系数据库的很多操作,这也是TC受到大家欢迎的主要原因之一,有一个Ruby的项目miyazakiresistance将TT的hashtable的操作封装成和ActiveRecord一样的操作,用起来非常爽。
TC/TT在mixi的实际应用当中,存储了2000万条以上的数据,同时支撑了上万个并发连接,是一个久经考验的项目。TC在保证了极高的并发读写性能的同时,具有可靠的数据持久化机制,同时还支持类似关系数据库表结构的hashtable以及简单的条件,分页和排序操作,是一个很棒的 NoSQL数据库。
TC的主要缺点是在数据量达到上亿级别以后,并发写数据性能会大幅度下降,NoSQL: If Only It Was That Easy提到,他们发现在TC里面插入1.6亿条2-20KB数据的时候,写入性能开始急剧下降。看来是当数据量上亿条的时候,TC性能开始大幅度下降,从TC作者自己提供的mixi数据来看,至少上千万条数据量的时候还没有遇到这么明显的写入性能瓶颈。
这个是Tim Yang做的一个Memcached,Redis和Tokyo Tyrant的简单的性能评测,仅供参考

3、Flare
TC是日本第一大SNS网站mixi开发的,而Flare是日本第二大SNS网站green.jp开发的,有意思吧。Flare简单的说就是给 TC添加了scale功能。他替换掉了TT部分,自己另外给TC写了网络服务器,Flare的主要特点就是支持scale能力,他在网络服务端之前添加了一个node server,来管理后端的多个服务器节点,因此可以动态添加数据库服务节点,删除服务器节点,也支持failover。如果你的使用场景必须要让TC可以scale,那么可以考虑flare。
flare唯一的缺点就是他只支持memcached协议,因此当你使用flare的时候,就不能使用TC的table数据结构了,只能使用TC的key-value数据结构存储。

二、满足海量存储需求和访问的面向文档的数据库:MongoDB,CouchDB
面向文档的非关系数据库主要解决的问题不是高性能的并发读写,而是保证海量数据存储的同时,具有良好的查询性能。MongoDB是用C++开发的,而CouchDB则是Erlang开发的:

1、MongoDB
MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。他支持的数据结构非常松散,是类似json的bjson格式,因此可以存储比较复杂的数据类型。Mongo最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。
Mongo主要解决的是海量数据的访问效率问题,根据官方的文档,当数据量达到50GB以上的时候,Mongo的数据库访问速度是MySQL的 10倍以上。Mongo的并发读写效率不是特别出色,根据官方提供的性能测试表明,大约每秒可以处理0.5万-1.5次读写请求。对于Mongo的并发读写性能,我(robbin)也打算有空的时候好好测试一下。
因为Mongo主要是支持海量数据存储的,所以Mongo还自带了一个出色的分布式文件系统GridFS,可以支持海量的数据存储,但我也看到有些评论认为GridFS性能不佳,这一点还是有待亲自做点测试来验证了。
最后由于Mongo可以支持复杂的数据结构,而且带有强大的数据查询功能,因此非常受到欢迎,很多项目都考虑用MongoDB来替代MySQL来实现不是特别复杂的Web应用,比方说why we migrated from MySQL to MongoDB就是一个真实的从MySQL迁移到MongoDB的案例,由于数据量实在太大,所以迁移到了Mongo上面,数据查询的速度得到了非常显著的提升。
MongoDB也有一个ruby的项目MongoMapper,是模仿Merb的DataMapper编写的MongoDB的接口,使用起来非常简单,几乎和DataMapper一模一样,功能非常强大易用。

2、CouchDB
CouchDB现在是一个非常有名气的项目,似乎不用多介绍了。但是我却对CouchDB没有什么兴趣,主要是因为CouchDB仅仅提供了基于 HTTP REST的接口,因此CouchDB单纯从并发读写性能来说,是非常糟糕的,这让我立刻抛弃了对CouchDB的兴趣。

三、满足高可扩展性和可用性的面向分布式计算的数据库:Cassandra,Voldemort
面向scale能力的数据库其实主要解决的问题领域和上述两类数据库还不太一样,它首先必须是一个分布式的数据库系统,由分布在不同节点上面的数据库共同构成一个数据库服务系统,并且根据这种分布式架构来提供online的,具有弹性的可扩展能力,例如可以不停机的添加更多数据节点,删除数据节点等等。因此像Cassandra常常被看成是一个开源版本的Google BigTable的替代品。Cassandra和Voldemort都是用Java开发的:

1、Cassandra
Cassandra项目是Facebook在2008年开源出来的,随后Facebook自己使用Cassandra的另外一个不开源的分支,而开源出来的Cassandra主要被Amazon的Dynamite团队来维护,并且Cassandra被认为是Dynamite2.0版本。目前除了 Facebook之外,twitter和digg.com都在使用Cassandra。
Cassandra的主要特点就是它不是一个数据库,而是由一堆数据库节点共同构成的一个分布式网络服务,对Cassandra的一个写操作,会被复制到其他节点上去,对Cassandra的读操作,也会被路由到某个节点上面去读取。对于一个Cassandra群集来说,扩展性能是比较简单的事情,只管在群集里面添加节点就可以了。我看到有文章说Facebook的Cassandra群集有超过100台服务器构成的数据库群集。
Cassandra也支持比较丰富的数据结构和功能强大的查询语言,和MongoDB比较类似,查询功能比MongoDB稍弱一些,twitter的平台架构部门领导Evan Weaver写了一篇文章介绍Cassandra:http://blog.evanweaver.com/articles/2009/07/06/up-and-running-with-cassandra/,有非常详细的介绍。
Cassandra以单个节点来衡量,其节点的并发读写性能不是特别好,有文章说评测下来Cassandra每秒大约不到1万次读写请求,我也看到一些对这个问题进行质疑的评论,但是评价Cassandra单个节点的性能是没有意义的,真实的分布式数据库访问系统必然是n多个节点构成的系统,其并发性能取决于整个系统的节点数量,路由效率,而不仅仅是单节点的并发负载能力。

2、Voldemort
Voldemort是个和Cassandra类似的面向解决scale问题的分布式数据库系统,Cassandra来自于Facebook这个 SNS网站,而Voldemort则来自于Linkedin这个SNS网站。说起来SNS网站为我们贡献了n多的NoSQL数据库,例如 Cassandar,Voldemort,Tokyo Cabinet,Flare等等。Voldemort的资料不是很多,因此我没有特别仔细去钻研,Voldemort官方给出Voldemort的并发读写性能也很不错,每秒超过了1.5万次读写。
从Facebook开发Cassandra,Linkedin开发Voldemort,我们也可以大致看出国外大型SNS网站对于分布式数据库,特别是对数据库的scale能力方面的需求是多么殷切。前面我(robbin)提到,web应用的架构当中,web层和app层相对来说都很容易横向扩展,唯有数据库是单点的,极难scale,现在Facebook和Linkedin在非关系型数据库的分布式方面探索了一条很好的方向,这也是为什么现在 Cassandra这么热门的主要原因。

共37篇,第1/4页 首页 1 2 3 4 下一页 尾页