大量小文件的实时同步方案
传统的文件同步方案有rsync(单向) 和 unison(双向)等,它们需要扫描所有文件后进行比对,差量传输。如果文件数量达到了百万甚至千万量级,扫描所有文件将非常耗时。而且正在发生变化的往往是其中很少的一部分,这是非常低效的方式。
之前看了Amazon的Dynamo的设计文档,它们每个节点的数据是通过Hash Tree来实现同步,既有通过日志来同步的软实时特点(msyql, bdb等),也可以保证最终数据的一致性(rsync, unison等)。Hash Tree的大体思路是将所有数据存储成树状结构,每个节点的Hash是其所有子节点的Hash的Hash,叶子节点的Hash是其内容的Hash。这样一旦某个节点发生变化,其Hash的变化会迅速传播到根节点。需要同步的系统只需要不断查询跟节点的hash,一旦有变化,顺着树状结构就能够在logN级别的时间找到发生变化的内容,马上同步。
文件系统天然的是树状结构,尽管不是平衡的数。如果文件的修改时间是可靠的,可以表征文件的变化,那就可以用它作为文件的Hash值。另一方面,文件的修改通常是按顺序执行的,后修改的文件比早修改的文件具有更大的修改时间,这样就可以把一个目录内的最大修改时间作为它的修改时间,以实现Hash Tree。这样,一旦某个文件被修改,修改时间的信息就会迅速传播到根目录。
一般的文件系统都不是这样做的,目录的修改时间表示的是目录结构最后发生变化的时间,不包括子目录,否则会不堪重负。因为我们需要自己实现这个功能,利用Linux 2.6内核的新特性inotify获得某个目录内文件发生变化的信息,并把其修改时间传播到它的上级目录(以及再上级目录)。Python 有 pyinotify,watch.py的代码如下:
在已经有Hash Tree的时候,同步就比较简单了,不停地获取根目录的修改时间并顺着目录结构往下找即可。需要注意的是,在更新完文件后,需要设置修改时间为原文件的修改时间,目录也是,保证Hash Tree的一致性,否则没法同步。mirror.py的代码如下
如果源文件不在同一台机器上,可以通过NFS等共享过来。或者可以通过支持列目录的HTTP服务器来访问远程目录,mirror.py 已经支持这种访问方式。server.py 是用webpy做的一个简单的只是列目录的文件服务器。由于瓶颈在IO上,它的性能不是关键。server.py的代码如下:
为了获得更好性能,以达到更好的实时性,Hash Tree最好是平衡的,比如BTree。如果一个文件发生变化,同步它需要进行的IO操作为N*M,其中N为数的层数,M为每层的文件数目。现在我们N为2,M最大为10000,适当减少它可以获得更好的性能,比如N为4,M为100。在以后创建目录结构时,最好能够考虑这方面的因素。
之前hongqn推荐过一个利用inotify的文件同步方案,同步方式类似于mysql和bdb等,由于过于复杂导致不可靠而没有采用。上面这个方案只用了一百多行Python代码就基本解决问题了,是不是很帅?:-)
davies 发表于 2008 年 04 月 24 日 | 0 条留言
给 log 加上时间
程序输出的 log 对维护长时间运行的程序非常有帮助,但有时面对大量没有时间的信息时会很困惑,错误发生在什么时候,或者某个步骤用了多长时间?逐行去修改程序以加上时间信息是比较麻烦的,利用操作系统的管道和一个辅助程序可以轻松地给每行日志输出加上时间。
用法如下:
# program | timeit.py > log
如果有错误输出,则要将错误输出重定向到标准输出,也可以加上自己的日期格式:
# program 2>&1 | timeit.py "%Y/%m/%d %H%M%S" > log
用 Python 实现这样的程序则非常简单,代码如下:
davies 发表于 2008 年 02 月 23 日 | 0 条留言
Bug of keepalive.py
刚发现了一个 urlgrabber 中
由于 urllib2 模块对 URL 的处理由一系列相互独立的 Handler 组成,如果后面的处理环节抛出异常,前面的 Handler 是不知道的,不能做响应的处理。一种解决办法是修改urllib2 中的每个错误处理 Handler,先关闭HTTPResponse对象再抛出异常。这种方法可能涉及的内容比较多,而且urllib2是标准库,不便于修改,影响程序的可程序移植行。可选的做法是拷贝 urllib2 到 keepalive.py 所在目录,优先使用修改后的版本。针对目前所遇到的问题,只要在http_error_default函数中增加一行:fp.close() 即可。
另一种办法就是修改
这个Bug隐藏得很深,害我折腾了好长时间。最开始只看到抛出了临时DNS解析错误的异常,而且一旦某个进程抛出这个异常就再也不能抓取页面了。以为是爬虫的抓取速度过块被封禁了,后来在本地开了DNS缓存,仍然还有DNS解析错误,只是错误信息变了。后来偶然发现还有"Too many open files"的异常,才慢慢找到问题的真相,检查
davies 发表于 2007 年 03 月 11 日 | 0 条留言
抓取电影评论数据
最近几周做了一个小项目,写个程序从某几个影片评论网站抓取评论数据,用来做影片评论与票房关系的统计分析。与直觉不同的是,它不是分析评论平均值与票房的关系,而是分析评论方差与票房的关系,评论方差对好电影与差电影的票房影响是不一样的。
所需数据分为三部分:
- 影片的基本信息和票房数据,来源于Boxofficemojo.com;
- 影片在Blog中的出现频率,来自http://www.blogpulse.com/trend;
- 影片的评论,包括分数、评论时间与长度,以www.metacritic.com为入口,再根据给出的每篇评论的链接到相应的网站上去找时间和长度信息。
说得好听点,属于数据挖掘,要从很不可靠很不规整的 Internet 上抓取想要的信息,听上去很有挑战性。说得不好听,就是一个爬虫加正则表达式,似乎没什么技术含量。实际上它们是一回事,可见包装和忽悠的重要性。
做这样的事情,正是 Python 或者 Perl 等脚本语言所擅长的,能够最直接最简便地实现想法,而不是在程序语言本身耗费太多时间。程序的性能瓶颈在网络上,因此不用担心脚本语言的性能问题。而且这种程序一旦写完,不会反复运行,大部分时间是处于不断修改过程中,也正是脚本语言所擅长地。对 Python 最熟悉,于是采用 Python 来实现,它简单直白的语法让我在大部分时候都不会碰到语法错误,而更专注于解决问题本身。用来 urllib 打开网页或者构造搜索,用 re 模块实现正则表达式匹配,time 模块解析各种格式的日期,再加上之前写过的线程池模块,差不多轮子都有了,剩下的就是进行组装和搭配。
Internet 是很不可靠的 IO,速度慢还容易出错,需要特别对待一下。我把所有下载的页面或者查询的结果都在本地做了缓存,将 URL 映射成 文件名。为了便于查看缓存,最好采用 html 作为后缀,并且 URL 和文件名有直观的对应关系。对于长度超过 255 字节的URL,不能直接作为文件名的,则用它的 md5 之类的哈希值。缓存的作用相当大,否则测试一下就要几分钟,一个月都难把程序写完。由于请求的页面特别多,最后达到了 4.3G,超过100K个文件,硬盘碎片得非常厉害。用了两级目录,打开文件夹还是非常慢。
获取影片的基本信息,需要先在 boxofficemojo.com 上查询得到该影片的页面URL,然后在该页面中提取相应数据。放映的前两天票房数据稍微麻烦一点,得根据首映日推算得到带有那几天票房数据的 URL,然后从中提取该部电影该天的数据。
Blog 趋势部分比较有意思,blogpulse.com上给出的是关键词在最近六个月的频率曲线。天啦,识别图象可不是那么容易的事情!幸运的是,曲线图上的每一个点都有链接,链接的说明文字中包含了时间和出现频率。太好了,只要用正则表达式就可以从 html 页面中提取想要的曲线数据。是不是有点 Hack 的味道?
由于好多网站都改版过,URL和页面模板都发生了变化,metacritic.com 上提供的评论链接很多都失效了,导致这一部分相当痛苦。需要构造关键字去它们的数据库中查询,并在结果页面中选择合适项目,不一定准确。
另外一个影评汇集站点http://www.rottentomatoes.com/上有更多的影评资料,如果用上面的方法仍然找不到评论时间的话,可以在该站点上找。评论基本上是影片和评论员的二维组合,如果对每个影片和每个作者都去查询的话,那是相当痛苦的。一种方法,就是把需要的某个作者的所有评论文章的日期都获取,一个页面就有 50 项数据,把它们全部保存在本地的文本文件中,等后面需要的时候直接使用。
前面说过 Internet 是很不可靠的 IO,程序应该具有多次运行以继续获取结果的能力,否则一旦出错就要重新来过的话也会很痛苦。一来是要捕捉异常,将 IO 异常和程序错误区分开来,保证在部分出错的情况下不会把所有结果都丢失。还需要差额工作,选择上次未完成的部分来继续。
davies 发表于 2006 年 12 月 1 日 | 5 条留言
Python中默认参数的副作用
注:该问题在Python 2.4 的 Tutorial 4.7.1 已经提出了,感谢 Rogerz 提醒。
看看下面这段程序的运行结果:
def foo(arg={}):
print arg
arg['key'] = "value"
foo()
foo()
其输出结果为:
{}
{'key': 'value'}
同一个函数,两次运行的结果居然不一样,它做了一些我们不期望的事情,称这个函数具有副作用。这段程序中,foo函数中是打印默认参数arg的值,应该为{},而第二次运行时默认值居然便成了{'key':'value'},诡异!
Python是完全面向对象的动态语言,函数也是一个动态的对象,函数的默认参数为该对象的func_defaults属性,在函数foo中,func_defaults为({},)。arg的默认值是一个空字典,为可修改对象。当函数没有传递参数时,参数arg指向func_defaults中这个可修改的字典,后面对arg的操作也就是对func_defaults中该字典的操作,修改arg也就修改了函数对象的属性,于是产生了副作用,与期望的行为不符。
默认参数的副作用,与传递可修改对象参数时的副作用是一样的。为了避免副作用,默认参数尽量采用不可修改的类型,比如将上面的程序段改成下面的样子将没有副作用:
def foo(arg=None):
if not arg: arg = {}
print arg
arg['key'] = "value"
davies 发表于 2006 年 11 月 29 日 | 2 条留言
第 1 / 6 页 | 下一页