2013年9月17日星期二

性能比较

以下两个Python 3函数的性能差10倍。

def upgrade(url):
    """Upgrade HTTP URL to HTTPS URL."""
    components = urllib.parse.urlsplit(url)
    if components.scheme.lower() == 'http':
        return urllib.parse.urlunsplit(['https'] + list(components[1:5]))
    else:
        return url

def upgrade1(url):
    """Optimized version of upgrade, 10x faster."""
    if url[:5].lower() == 'http:':
        return 'https' + url[4:]
    else:
        return url

timeit分别测试100万次,耗时分别如下:
4.574674844741821
0.46399807929992676
什么时候用库函数,什么时候用基本的函数实现,需要综合考虑开发速度、灵活性和性能等因素。

2013年9月16日星期一

用Python进行HTTP基本认证

curl调用公司Jira需要基本认证(Basic access authentication)的REST API没有问题,但是用Python3的urllib.request按照文档操作,总是返回401

我怀疑是Jira的API需要某些特殊的header,我又用GitHub的API尝试,仍然返回401错误。我看到GitHub的API调用要求必须要有User-Agent,我以为urllib没有设UA,我又尝试设置UA,仍然是401。而实际上urllib是设置了UA的。

Google后发现不是urllib的问题,而是API服务器不遵守RFC的原因,原文在此urllib第一次发送不带验证信息的请求,在401之后会根据服务器响应的WWW-Authenticate头来进行BA。Jira的401是有这个头,可是返回的是Outh的challenge,所以BA也会失败:
WWW-Authenticate: OAuth realm="...snip..."
而GitHub的401完全没有这个头,没有遵守RFC 2616里面对这个头的"Must"要求。

解决办法是请求前就把Authorization头加上,这样直接进行BA,就可以规避服务器不返回正确WWW-Authenticate头的问题。

2013年8月22日星期四

Django的单元测试

运行Django的单元测试:python manage.py test,出错。经查发现某些列的数据是中文,而Django自动创建的测试数据库编码是latin1。需要用数据库的TEST_CHARSET变量来指定测试数据库的编码,设为utf8就可以了。

运行一次完整的测试很慢。用time python manage.py test这里的测试用例大部分都是Django默认的,跑完432个测试总共用了2分7秒,而执行测试的时间只有36秒,大部分时间在创建和销毁数据库了。

Ran 432 tests in 36.068s

FAILED (errors=1, skipped=1)
Destroying test database for alias 'default'...

real 2m7.215s
user 0m25.922s
sys 0m1.028s

如果用SQLite做测试数据库,则完全使用的内存数据库,想必会比MySQL快。按照这个帖子,把测试数据库改为SQLite后,测试速度大大提高,同样的测试只用了18秒,其中16秒都是跑测试的时间。

Ran 432 tests in 15.844s

FAILED (errors=1, skipped=1)
Destroying test database for alias 'default'...

real 0m17.866s
user 0m17.121s
sys 0m0.432s

以后测试就用SQLite了。

2013年8月19日星期一

Python程序的日志顺序混乱

Python 2程序的日志打在文件里面,但是文件中日志出现的顺序和打印日志语句的执行顺序不一致,是混乱的。

研究发现,有的日志是用logging模块打出的,而有的用print语句打出。查看logging模块的源码,对stream输出,每次emit一条record,就会对stream进行flush。而print用的是系统默认的缓存方式,对没有tty的文件的输出,默认是块缓存。

程序启动时候用python -u就可以让stdinstdout不缓存了,应该就不会混乱了。

参考12

2013年7月4日星期四

Facebook内部的代码发布

公司的代码发布是在网页操作的。最近修复了几个性能问题,问题产生的一个原因是Web后端服务器增长到80多台,在并行发布时突然产生太多进程让系统变慢。需要限制并发的进程数目。随着服务器数目增长,集中式发布的发布时间会越来越长,发布服务器的CPU、磁盘IO、网络IO的负载也会持续增长。

早就听说Facebook使用了点对点(P2P)的发布方式,我想P2P方式可以把负载分布在所有服务器上,避免发布服务器成为瓶颈。正好看到了这篇文章:A behind-the-scenes look at Facebook release engineering,就想学习下Facebook是怎么发布代码的。这篇文章是2012年4月发出的;我对我感兴趣的核心内容做个摘要。

Facebook用公司研发的HipHop,把PHP转译成C++代码,之后再编译为原生二进制文件,性能大幅提升,可以让服务器的CPU负载降低50%。但是这种模式下,整个Facebook的代码(不包括静态资源)是一个1.5 GB大的独立的文件。正是因为这个庞大的文件,才让Facebook的发布这么特殊,这也不难理解Facebook要用P2P部署了。Facebook用的是BitTorrent,我们下载Ubuntu的ISO镜像时候也用BT,因为用BT比普通下载快。

Facebook小规模改进的部署每天一次,每次约需30分钟,15分钟用来生成这个大文件,15分钟用来部署。较大规模的部署每周一次。一天一次,一次半小时,尽管作者对Facebook未来将要做的改进描述为"more agile",但我觉得这样的发布频率实在不能算是agile。未来发布的改进是要做出PHP的HipHop虚拟机,把PHP编译成字节码,由虚拟机来执行。这样就可以增量发布,而不用发布一个巨大的文件了,发布时间也可由现在的30分钟缩短到几分钟。

可见P2P发布是Facebook独特的环境下产生的解决方案,普通项目的增量式发布是不需要这样的高科技的。

因为这篇文章是一年多前的,现在看到的HipHop的GitHub页面已经叫Virtual Machine了。不知Facebook现在的发布变成什么样了?原文对整个发布过程做了全面的描述,感兴趣的可参考原文。

2013年3月14日星期四

如何延时GNOME的自动启动程序

环境是Ubuntu 12.04 GNOME 3.2。GNOME可以通过Startup Applications这个应用来设置自动启动的程序。Dropbox就是通过这里自动启动的。我发现每次刚刚进入桌面系统负载都很高,用top查看与Dropbox有关系。
从右上角的菜单打开
我想让Dropbox延迟两分钟再启动,这样可以让GNOME启动的速度加快,体验会好一点。我试图在Dropbox的启动命令前面加上sleep 120来延时,但这样后Dropbox根本就不自动启动了。
更改自动启动程序的对话框
我打算探个究竟,搞定我的问题。先通过ps找到启动这个程序的命令是gnome-session-properties,通过dpkg -S /usr/bin/gnome-session-properties找到对应的包名,然后下载源代码包gnome-session,在Eclipse里面创建项目。

不知道相关的文件是哪个?竟然是从翻译文件po/zh_CN.po里面看出来的,在capplet目录下。在gsm-app-dialog.c里面有这样的代码:
if (gsm_util_text_is_blank (exec)) {
        error_msg = _("The startup command cannot be empty");
} else {
        if (!g_shell_parse_argv (exec, &argc, &argv, &error)) {
                if (error != NULL) {
                        error_msg = error->message;
                } else {
                        error_msg = _("The startup command is not valid");
                }
        }
}
看起来这是判断启动命令的代码。安装好相关的开发包之后,在Eclipse里面可以看到g_shell_parse_argvglib.h里面声明的。文档里面说了,这个函数不支持Shell的很多特性。看来我的命令行'sleep 120; dropbox start -i'被解析成了不能执行的命令。写了如下一个测试程序test_gshell.c
#include <glib.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
    const char *exec = argv[1];
    GError     *error;
    const char *error_msg;
    char      **myargv;
    int         myargc;
    if (!g_shell_parse_argv (exec, &myargc, &myargv, &error)) {
        if (error != NULL) {
            error_msg = error->message;
        } else {
            error_msg = "The startup command is not valid";
        }
    }
    printf("%d\n", myargc);
    for(int i = 0; i < myargc; i++) {
        printf("%s\n", myargv[i]);
    }
}
用如下的命令编译出执行文件:
gcc -o test_gshell test_gshell.c \
    -I/usr/include/glib-2.0/ \
    -I/usr/lib/x86_64-linux-gnu/glib-2.0/include/ \
    -L/lib/x86_64-linux-gnu/ \
    -L/usr/lib/x86_64-linux-gnu/ \
    -L/lib64/ -lglib-2.0 -std=c99
然后测试我的命令:
$ ./test_gshell "sleep 120; dropbox start -i"
5
sleep
120;
dropbox
start
-i
看到GLib把我本来期望的两条命令的List解析成了sleep命令后面跟4个参数,相当于执行了如下Shell命令:
sleep "120;" "dropbox" "start" "-"
这个命令显然不能达到我的目的。

我翻墙的脚本也是通过GNOME来自启动的,有时候网络还没好,就希望能够延时执行,那个脚本可以带命令行的延时参数。现在要让Dropbox延时启动,我也只能用一个脚本把延时和Dropbox的命令封装起来,然后再让GNOME自动运行这个脚本。主要要在Dropbox的设置里把自动启动去掉。

2013年3月6日星期三

Ubuntu在Mac下的网络故障

Ubuntu(Macbook Pro 8,2)在一次例行升级后,以太网开始失效,ping办公室的网关时候就出现Destination Host Unreachable的错误。检查路由没有异常,抓包看不到任何ICMP包,怀疑是系统bug。

办公室的Wifi很差劲,为了工作只好切换到OSX下。OSX下工作环境没有配置完整,而且安装相关的开发环境很不方便,回头还是切回Ubuntu,但Ubuntu网络还是不能用,Wifi很卡。这两周来来回回切换,影响工作效率。

今天试了试Ubuntu的恢复模式,发现在单用户下网络是可以用的。遂把故障定位到GNOME桌面,认为是GNOME下网络有问题。就在单用户模式下装了个KDE,进去后结果还是有问题。后来想到试试内核模块,这个网卡用的模块是tg3,我rmmod之后再modprode,结果网络就好了。

折腾了这么久,其实重新加载内核模块的方法我早就用过了。还是这台Mac这个Ubuntu,以前Wifi用着用着就断了,我就用重新加载内核模块的方法了,出问题时候执行一个叫reset-wifi的脚本:
#! /bin/bash
# Reload Wifi module
sudo rmmod b43
sudo rmmod bcma
sudo modprobe bcma
可是这次有线网络出问题,我竟然折腾了这么久才找到个解决办法。