Loading...

Archive for February, 2010

    AD: 猛买网,精彩团购 | Jobsdigg | 很棒的男装店 | 网站地图

提供Linode技术支持

最近使用Linode的朋友越来越多,我收到各种各样的咨询邮件。前几天,我还通过GTalk帮助一位朋友从开始购买Linode直到迁移完成网站所有数据。但是不论是邮件还是IM,都不免空对空,往往一个简单的问题需要交流很久。

因此我打算,向使用Linode的朋友提供有偿的技术支持。如果你:

  1. 打算/正在使用Linode但不熟悉shell
  2. 没有足够的时间或干脆不想自己维护服务器

我可以帮你打理,这样也节约你的宝贵时间。我可以提供的服务包括:

  1. LAMP环境搭建
  2. 安装nginx并配置php-fastcgi
  3. 安装开源软件(如Wordpress)并配置好webserver、db
  4. 从其他托管商(如Dreamhost,需要提供shell)迁移网站到Linode
  5. 搭建Rails运行环境,安装Passenger
  6. 服务器性能优化咨询+实施

除最后一项外,其他每项费用¥150或$30,接受百付宝Paypal支付。

我的邮箱是 zhanglei909(at)gmail.com,如有需要,请联系我。

* 对于有时间倒腾的朋友们,我还是建议多看看各种guide,自己摸索。收获会很大。

WordPress的一个彩蛋

昨晚不小心点了blog中某文章的“Compare Revisions”功能,深夜把我吓出一身冷汗。


      Wake up, 张磊...
      The Matrix has you...
      Follow the white rabbit.

把这消息发到Twitter(@blogkid),大家都和我说“Knock Knock”。

我想我是遇到彩蛋了(若不是就惨了),于是今天打算寻找一下出处。在目录中grep Matrix、wake up、rabbit都未果,猜到可能是做了加密,于是去翻代码。在Wordpress 2.92版本的wp-admin/revision.php中找到了线索:

54   if ( $left_revision->ID == $right_revision->ID ) {
55       $redirect = get_edit_post_link( $left_revision->ID );
56       include( 'js/revisions-js.php' );
57       break;
58   }

大意是,如果两个相比较的文章ID一致,就会包含js/revision-js.php这个文件。

wp-admin/js/revisions-js.php文件中,有一堆加密过的字符串。粗略判断,就是看到的这几行字了。下面可以自己触发这个彩蛋,只要构造一个这样的url:

http://you_blog_address/wp-admin/revision.php?action=diff&left=2414&right=2414

把后面的“2414”替换为blog上随便一个已有的文章ID,就能看到彩蛋出现啦。

PS:不知道是不是自己太没娱乐细胞了,初看到震惊了半晌。都是被逼的啊。

PS2:据说早在2.6版本就发现了这彩蛋

Rails中压缩存储文本数据

继续压缩话题。之前写了 Rails生成压缩的静态缓存,以及配置nginx以支持直接发送压缩文件两篇文章。今天谈的是在Rails中,将大段文本内容以压缩格式存在数据库。这也是从公司的一个项目中获得的灵感。

原始需求

手头有一个结构简单的文本库,可以看做是key=>value。约180万条数据,2.8G(确实不算大,见笑了)。

  1. 这个库查询频繁,CPU在IOWAIT耗时颇多。
  2. Linode服务器硬盘紧张,而CPU富裕。
  3. 若能通过压缩减小体积,备份/回导时也会更省时。

实施过程

整个实施过程做到了无缝转换,不需要停服务。

第一步:将文本字段类型由TEXT修改为BLOB;新增一个is_gzip字段用于标识是否经过压缩,默认为0。

* 需要注意,BLOB和TEXT最长支持65535字节

第二步,修改Rails相应的Model。代码示例(压缩content字段):

class TextData < ActiveRecord::Model
  def content
    if self.is_gzip
      Zlib::Inflate.inflate(super)
    else
      super
    end
  end

  def content=(info_content)
    super Zlib::Deflate.deflate(info_content, 9)
    self.is_gzip = true
  end
end

再一次感叹super的便捷。修改代码后,需要重新部署一下Rails应用。

第三步,创建一个rake任务,用来批量做转换。关键部位只消这么做:

text_data.content = text_data.content
text_data.save

第四步,在做过修改的表上做一次optimize table,以释放那些不需要的空间。

效果观察

  1. 经如上处理,2.8G数据只剩1.4G。如果用了Gzip而不用Deflate,有可能更小。
  2. IOWAIT未见明显下降。压缩前,单条记录理论上可以通过一次IO完成。
  3. 备份的速度确实有所提升,意料之中。

更多信息

  1. 这种思路同样适用于PHP、Python以及其他语言下的Web应用,只是在Rails中搞起来更为轻松。
  2. Mysql提供了Compress/Uncompress两个函数,但是不建议直接使用。一方面会增加数据库服务器计算的压力;另一方面,如果用了主从,每个服务器都得算一次。
  3. Zlib::Deflate.deflate 的第二个参数是压缩的level。我用随机数据测试,Level 9压缩后的体积比Level 1 小10%。
  4. 如果用了Sphinx之类的从数据库导出数据的全文检索引擎,此法需慎用。

榨干服务器:让进程运行在指定的CPU

我的Linode十分繁忙,在跑一些密集操作数据库的Rake任务时尤其如此。但我观察发现,Linode服务器的4核CPU,只有第1个核心(CPU#0)非常忙,其他都处于idle状态。

不了解Linux是如何调度的,但目前显然有优化的余地。除了处理正常任务,CPU#0还需要处理每秒网卡中断。因此,若能将CPU#0分担的任务摊派到其他CPU核心上,可以预见,系统的处理能力将有更大的提升。

两个名词

SMP (Symmetrical Multi-Processing):指在一个计算机上汇集了一组处理器(多CPU),各CPU之间共享内存子系统以及总线结构。 [更多...]

CPU affinity:中文唤作“CPU亲和力”,是指在CMP架构下,能够将一个或多个进程绑定到一个或多个处理器上运行。[更多...]

一、在Linux上修改进程的“CPU亲和力”

在Linux上,可以通过 taskset 命令进行修改。以Ubuntu为例,运行如下命令可以安装taskset工具。

# apt-get install schedutils

对运行中的进程,文档上说可以用下面的命令,把CPU#1 #2 #3分配给PID为2345的进程:

# taskset -cp 1,2,3 2345

但我尝试没奏效,于是我关掉了MySQL,并用taskset将它启动:

# taskset -c 1,2,3 /etc/init.d/mysql start

对于其他进程,也可如此处理(nginx除外,详见下文)。之后用top查看CPU的使用情况,原来空闲的#1 #2 #3,已经在辛勤工作了。

二、配置nginx绑定CPU

刚才说nginx除外,是因为nginx提供了更精确的控制。

conf/nginx.conf中,有如下一行:

worker_processes  1;

这是用来配置nginx启动几个工作进程的,默认为1。而nginx还支持一个名为worker_cpu_affinity的配置项,也就是说,nginx可以为每个工作进程绑定CPU。我做了如下配置:

worker_processes  3;
worker_cpu_affinity 0010 0100 1000;

这里0010 0100 1000是掩码,分别代表第2、3、4颗cpu核心。

重启nginx后,3个工作进程就可以各自用各自的CPU了。

三、刨根问底

  1. 如果自己写代码,要把进程绑定到CPU,该怎么做?可以用sched_setaffinity函数。在Linux上,这会触发一次系统调用
  2. 如果父进程设置了affinity,之后其创建的子进程是否会有同样的属性?我发现子进程确实继承了父进程的affinity属性。

四、Windows?

在Windows上修改“CPU亲和力”,可以通过任务管理器搞定。

* 个人感觉,Windows系统中翻译的“处理器关系”比“CPU亲和力”容易理解点儿

—————–

进行了这样的修改后,即使系统负载达到3以上,不带缓存打开blogkid.net首页(有40多次查询)依然顺畅;以前一旦负载超过了1.5,响应就很慢了。效果很明显。

cURL使用心得

cURL

cURL是我在Linux上经常用的一个小工具,我理解它是一个“客户端”。今天记录一下我的使用心得。达人请忽略。

cURL是一个利用URL语法在命令行方式下工作的文件传输工具。它支持很多协议:FTP,  FTPS,  HTTP, HTTPS, GOPHER等。[更多...]

场景一:测试域名绑定

我常需要在开发环境中,测试某台服务器上的Web Server是否正确绑定了域名。比如,我希望在服务器192.168.1.10上绑定www.blogkid.net。但需要修改hosts才能看到效果,这活儿很累人。

所谓“域名绑定”,就是把host映射到对应的目录。如果手头有cURL,可以使用 -H 参数,在请求头信息中多写一个 Host 字段。就可以测试是否配置正确了。

# curl -H "Host: www.blogkid.net" http://192.168.1.10/

场景二:查看头信息

响应头信息中包含了很多东西。除了HTTP版本和响应代码,还有Server、Content-Type、Content-Length等信息,如果有写入Cookie的操作,也会体现在头信息中。

使用cURL的 -I 参数,就可以看到这些头信息。比如淘宝的:

# curl -I http://www.taobao.com/
HTTP/1.1 200 OK
Date: Sun, 14 Feb 2010 08:57:35 GMT
Server: Apache
Set-Cookie: abt=b; expires=Sun, 28-Feb-2010 08:57:35 GMT; path=/; domain=www.taobao.com
at_catetype: b (咦,这是什么?)
Set-Cookie: _lang=zh_CN:GBK; Domain=.taobao.com; Path=/
Cache-Control: max-age=3600
Expires: Sun, 14 Feb 2010 09:57:35 GMT
Vary: Accept-Encoding
Content-Type: text/html; charset=GB2312
Content-Language: cn

我昨天也修改了一下我服务器的server信息,大家感兴趣可以 curl -I http://www.blogkid.net/ 看看。

这里插一句,不建议把使用Web服务器的版本暴露出来(其实服务器信息也最好隐藏掉,或者把Apache伪装成nginx什么的 :P )。免得突然爆出漏洞时,措手不及,被人利用。

场景三:跟踪URL跳转

如果遇到了一个多次跳转的URL,可以先用curl的 -L 参数看看,这个URL最终跳转到了什么地方。-L 参数最好配合 -I 使用,不然cURL会把最后一次请求获得的数据输出到控制台。

没有合适的URL拿来做例子,意会一下吧 :D

场景四:发送压缩的请求

cURL提供了一个 –compress 参数,可以用来发送支持压缩的请求。但使用了–compress之后,虽然传输过程是压缩的,cURL的输出还是解压之后的,难以看到效果。

我一般用 -H 参数,自己写一个 Accept-Encoding 字段在头信息中。

curl -H "Accept-Encoding: gzip" http://www.blogkid.net/

如果直接运行上面的命令,会得到一堆乱码,因为cURL输出的内容,是压缩后的数据。不妨在后面接一个gunzip试试。

curl -H "Accept-Encoding: gzip" http://www.blogkid.net/ | gunzip

使用gunzip解压之后,信息又被还原了。前几天我写的压缩话题(12),就用了类似的方法来测试。

场景五:忽略证书错误

平日上网,遇到证书错误一定要小心。但我在工作中,经常需要用自签的假证书搭建开发环境。cURL在遇到证书错误时罢工,使用 -k 参数就可以让它不做证书校验。

春节快乐,情人节快乐

难得春节和情人节在一起。

先祝朋友们开开心心过大年,再祝有情人终成眷属。

脚本分享:备份数据库到Amazon S3

家里网络不好,备份服务器上几G的数据并下载回来极其痛苦(这还是压缩过的)。于是我想把数据备份上传到AmazonS3上,一来Linode到Amazon网速应该不是问题;二来,“Cloud Storage”也要比我的笔记本硬盘更靠得住。

科普一下:

S3 = Simple Storage Service。是由Amazon提供的在线存储服务。

我使用了ruby下的s3sync作为与Amazon交互的客户端,并写了一个shell脚本对它进行了简单包装。脚本用法很简单:

./backup_mysql_and_sync_s3.sh dbname1 [ dbname2 ...]

只要在参数中指明数据库名,就会自动将数据库内容用mysqldump导出并压缩,然后上传到S3中。下面详述一下脚本的获取和配置,搞定之后备份东西很轻松。

0. 环境依赖

依赖于ruby。Dreamhost上面自己有ruby,如果在用Linode而且是Debian系列,可以apt-get install ruby装上。

需要有Amazon AWS账号。脚本需要使用AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY进行身份验证。

1. 获得s3sync-s3cmd,创建bucket

svn checkout \
  http://s3sync-s3cmd.googlecode.com/svn/trunk/ \
  s3sync-s3cmd
cd s3sync-s3cmd/s3sync
export AWS_ACCESS_KEY_ID="your_access_key_id"
export AWS_SECRET_ACCESS_KEY="your_serect_access_key"
ruby s3cmd.rb createbucket bucket_name

如果一切正常,创建操作不会有任何返回。出错时会有相应提示。

此处需记下s3cmd.rb的位置、以及创建的bucket_name,备用。

2. 获得shell脚本,修改配置

脚本地址在此处,可以直接用下方shell脚本获得并解压。

wget http://www.blogkid.net/wp-content/uploads\
  /2010/02/backup_mysql_and_sync_s3.zip
unzip backup_mysql_and_sync_s3.zip
chmod +x backup_mysql_and_sync_s3.sh

打开backup_mysql_and_sync_s3.sh。修改下方这些配置:

export AWS_ACCESS_KEY_ID="your_access_key_id"
export AWS_SECRET_ACCESS_KEY="your_serect_access_key"
BUCKET_NAME="you_bucket_name"

PATH_TO_S3SYNC="path_to_s3sync"
MYSQL_USER="your_db_user"
MYSQL_PASSWD="your_db_passwd"

MYSQL_PASSWD可以留空,这样的话运行时需要自己输入。

如有必要,还可以修改MYSQL_HOST和MYSQL_PORT。比如在Dreamhost上,MYSQL_HOST就不是localhost。

做好配置后,就可以试着运行一下。想备份哪个db,只要输入数据库名即可,指哪打哪。当然,也不要滥用,免得Amazon账单不留情。

如果想作为crontab任务,建议用mutt把运行结果发送到邮箱。命令大致如下:

./backup_mysql_and_sync_s3.sh \
  dbname1 dbname2 | /usr/bin/mutt \
  -s "backup mail notification" zhanglei909(at)gmail.com

延伸阅读:

车东:如何利用Amazon S3服务将文件备份到国外

UPDATE: 车东在他的文章中说,从国内备份到国外,每天可以备份1G左右。但在Linode服务器上,这个速度是3M每秒。在我的测试中,一个900M的文件,传输到S3花费的时间甚至小于生成此文件的时间。

解决Rails+Sphinx特定搜索词报错

我在一个Rails应用中使用了sphinx做全文检索(用的是coreseek提供的版本),但在搜索一些词(比如“环境”)时,Rails日志中会有“comparison of Fixnum with nil failed”的错误,Sphinx的query.log/sphinx.log却无任何记录。此问困扰了我很久,也没搜到什么解决方案。今天最终解决了,于是把查找问题的过程,写在这里。

一、问题初析

仅从Rails的报错信息来看,是将一个数字与nil作比较,导致ruby报错(谁再把脚本语言和弱类型语言混在一起我跟他急)。出错的代码在ultrasphinx-1.11/vendor/riddle/lib/riddle/client.rb的433行:

430        header = socket.recv(8)
431        status, version, length = header.unpack('n2N')
432
433        while response.length < length
434          part = socket.recv(length - response.length)
435          response << part if part
436        end

经验证,length是nil导致ruby报错,而length是从431行的header.unpack得到的。进一步debug发现,header是一个空串,也就是说,430行的socket.recv根本没有从服务器获得响应。

最初想得很简单,在header之外用了一个死循环。只要header为空,就继续调用socket.recv。没想到这直接导致ruby进程陷入死循环,也就是说,对某些query,根本无法收到服务器的响应。

二、tcpdump未果

ruby不停报错,于是想到抓包,看看究竟是哪里出问题了。

祭出tcpdump,自己功力不深,感觉dump出的内容极其晦涩,根本无助于定位问题。

三、初试gdb

仔细看了ruby代码后,感觉Riddle中使用的Socket,是对C语言中Socket操作的简单封装,出问题的可能性不大。因此我把目光放在了sphinx的后台服务searchd上面。

服务器上还留有编译时的C++代码,选了几个看看,没能理出头绪。开了gdb也实在不知该从何处下断点。

到此时,我已经到了放弃的边缘。我一直觉得,是自己对Socket不熟悉于是不能深入地追踪此问题。直到想到去查一下dmesg

四、从coredump找到线索

dmesg是从一同事处学来的,没想到今天帮我了大忙。服务器的dmesg显示,searchd进程出现了segfault:

searchd[16818]: segfault at 0000000000000000 rip 000000000047242c rsp 00007fffea067490 error 4
searchd[16822]: segfault at 0000000000000000 rip 000000000047242c rsp 00007fffea067490 error 4
searchd[16858]: segfault at 0000000000000000 rip 000000000047242c rsp 00007fffea067490 error 4

再用报错的关键词搜索一次,segfault也多了一条。至此,可以断定是sphinx处理出错导致没有返回结果。于是我把coredump打开(如果早打开,可能可以更早定位问题),顺利地获得了一个core文件。

再用core文件gdb,很快就定位了问题。在src/sphinx.cpp中,第12647行出了core。

12631     // boundary checks
12632     if ( !pHlist )
12633     {
12634         // there are no more hits for current docs block; do we have a next one?
12635         assert ( pDocs );
12636         pDoc = pDocs = GetFilteredDocs ();
12637
12638         // we don't, so bail out
12639         if ( !pDocs )
12640             break;
12641
12642         // we do, get some hits
12643         pHlist = m_pRoot->GetHitsChunk ( pDocs, m_uMaxID );
12644         assert ( pHlist ); // fresh docs block, must have hits
12645      }
12646      // carry on
12647      assert ( pDoc->m_uDocid<=pHlist->m_uDocid );
12648      while ( pDoc->m_uDocid<pHlist->m_uDocid ) pDoc++;
12649      assert ( pDoc->m_uDocid==pHlist->m_uDocid );

在gdb中print了一下指针pHlist,发现它指向了0×0,不出问题才怪。

UPDATE(本段应子宁要求而增加) 至此,问题已经明朗:在searchd后台服务执行搜索时,因为暂时未知的原因,pHlist指针指向了0×0,导致程序崩溃。于是前端的rails应用没有收到任何返回。

五、快速修复

定位问题之后,我打算简单修复一下。将代码做如下修改并编译之后,顺利地解决了此问题。奇怪的是,12644行assert为何没起作用?

12643         pHlist = m_pRoot->GetHitsChunk ( pDocs, m_uMaxID );
12644         assert ( pHlist ); // fresh docs block, must have hits
12645       }
12646       // added by blogkid
12647       if (!pHlist)
12648       {
12649         // fix the null point error
12650         // by zhanglei
12651         break;
12652       }
12653       // added end by blogkid
12654       // carry on
12655       assert ( pDoc->m_uDocid<=pHlist->m_uDocid );
12656       while ( pDoc->m_uDocid<pHlist->m_uDocid ) pDoc++;

我用的是coreseek出品的修改版sphinx,csft 3.1rc-1和3.0beta3均有此问题。而我尝试搜许多英文词汇,都不会引发此问题,这也解释了为什么之前我一直找不到解决方法。希望这篇文章,能对遇到同样问题的朋友们有些帮助。

整个追查问题的过程,其实有很多可以“扯远”的细节,比如查看tcpdump结果、配置开启coredump、使用gdb调试core文件等等;而且也留下了一些问题,比如GetHisChunk方法什么情况下会返回0;为什么此处用一句break居然修复了此问题(我本意是想让searchd不再报错,没想到它可以正常返回搜索结果了),这些问题,还有待进一步从sphinx的源码中,寻找答案。

配置nginx以支持直接发送GZip文件

Web服务器大多是支持直接发送GZip文件的。本文算是Rails中使用压缩的静态缓存的姊妹篇,谈谈在nginx中,进行适当配置使nginx直接发送压缩文件到浏览器。

配置起来非常容易。大致如下(实际情况可能更为复杂):

location / {
  set $gzip_suffix '.gz';
  if (-f $request_filename$gzip_suffix) {
    add_header  Content-Type  'text/html';
    add_header  Content-Encoding  gzip;
    gzip off;
    rewrite (.*) $1.html$gzip_suffix break;
  }
}

在上面这段配置中,先判断是否有xxx.gz文件存在,若有,则进入相应分支。在这个分支中,使用了两个add_header,会在响应头信息中增加相应的字段,以使客户端了解这是经过压缩的内容。若没有Content-Type ‘text/html’,打开页面时浏览器会弹出一个下载提示;若没有Content-Encoding gzip,打开只能看到一堆乱码。下面的 gzip off 则告诉nginx,不必再对这部分内容做压缩。

这样,就可以使nginx支持直接发送GZip压缩后的内容了,而且在浏览器中打开和普通html没有任何区别。但这种配置方式还遗留了一个问题,就是任何gz文件都被作为text/html而发送了。也许有这种需要:请求xxx.xml就将Content-Type设置为text/xml;请求xxx.css就将Content-Type设置为text/css。这当然有办法满足,不过我把这问题留给诸位,可以参考下这篇老文:在nginx中使用多个条件进行rewrite,相信可以助你秒杀这种需求。

此外,推荐一下RackspaceCloud。如果手头没有linux而且不方便拿服务器上的nginx做实验,可以在RackspaceCloud新建个服务器玩儿,搞完删掉就好了,既方便又干净。

Rails中使用压缩的静态缓存

最近我的Linode服务器硬盘吃紧,一个16G的分区,使用率达到99%,就快无立锥之地了。某个Rails应用,生成的静态文件占用了超过9G的空间,并且还在不断增长。最初没想到好办法,想,难道要被逼升级服务器?后来联想到WP-SuperCache的原理,计上心来:用GZip压缩这些静态文件。

压缩文件做缓存有什么好处?

  1. 文本压缩的比例够大,这样做节约
  2. 压缩好之后,无需Web服务器再做压缩

那,会有什么问题?

  1. 不支持GZip的客户端,只能看到一堆乱码。据我观察,YSlurp疑似不支持GZip。
  2. CPU的开销会增加

第一个问题也可以通过web服务器的一些逻辑判断,给不支持GZip的客户端返回未经压缩的内容(WP-SuperCache就是这么做的)——但个人感觉意义不大,大部分客户端都支持GZip,Google的SPDY(自备翻墙工具)都强制压缩强制SSL了。对第二个问题,VPS上主要是内存不够,CPU资源相对富裕。

在Rails中,做到这点非常轻松。原Controller中代码大致如下,无需改动这些代码,即可把静态页面缓存改为GZip格式。

cache_pages函数原型在action_pack/lib/action_controller/caching/pages.rb 中,大致的调用顺序为:

其中,class.cache_page函数进行了写入文件的操作,写入的位置是由class.page_cache_path决定的。看到这里,难道要撸起袖子hack Rails框架?No,只要在ApplicationController中加入几行代码,覆盖这两个函数即可做到。

  def self.cache_page content, path
    # 若目录不存在,则创建,以免写入时报错
    FileUtils.makedirs(File.dirname(page_cache_path(path)))
    Zlib::GzipWriter.open(page_cache_path(path)) do |gz|
      gz.write content
    end
  end

  # 使用super可以调用父类的同名方法
  def self.page_cache_path path
    super + ".gz"
  end

将上面代码加入后,生成的静态缓存,就变成.gz后缀的压缩文件了。此外,Ruby的super函数真是一个很棒的玩意。

但这仅做了一半,我们还需要配置WEB服务器以支持直接发送GZip压缩文件。对Apache来说,可以大部分照搬WP-SuperCache的Rewrite规则;对nginx来说,还需要写一些配置。下一篇文章,将谈谈在nginx中如何做配置以支持直接发送GZip文件。