2013年11月24日星期日

LightDM不能启动

开了20多天的Ubuntu 12.04,重启后LightDM不能开机自动启动了。决定查个究竟。

根据开机后的画面显示,目测是LightDM的Upstart脚本没有执行。看了看/etc/init/lightdm.conf,里面有相关的依赖服务,但我不知道是哪一步出问题了。折腾了几下,包括检查plymouth, dbus服务,删除和重装lightdm包后,lightdm服务竟然手动启动都会失败了。

还好LightDM在/var/log/lightdm/lightdm.log记录了日志,里面有/usr/share/xgreeters/default.desktop文件找不到的错误。在/usr/share/xgreeters/只有unity-greeter.desktop文件。之前正常工作的时候,/etc/lightdm/lightdm.conf里面指定了unity-greeter的,现在没有lightdm.conf这个文件,所以就出了上述的错误。我就做了一个符号链接,再次sudo start lightdm,结果还是没有起来,不过lightdm.log里面有记录启动greeter出错了,并友好地提示greeter的日志记在/var/log/lightdm/x-0-greeter.log里面。在这个文件里面看到具体的错误是没有权限打开/var/lib/lightdm/.Xauthority文件。我记得之前用Apt删除lightdm的时候,显示/var/lib/lightdm目录未能删除,现在我就把/var/lib/lightdm里面的所有文件都删除了,之后lightdm服务就能成功启动了。

从这个故事我得到的体会是,应用设计里面重要的一点,就是要有关键的日志记录,特别是错误记录,这样在事后分析的时候容易找到问题产生的原因。LightDM的日志文件就是比较清楚的,特别是在lightdm.log里面,写入其它日志文件x-0-greeter.log时,也会记下这条,这样看日志的人立刻就知道还有别的日志文件;如果安静地写到另一个日志文件里面而不提示,就会增加用户检索的成本。

2013年9月29日星期日

批量改照片名为拍摄时间

去台北玩照的七百多张照片,是用手机和数码相机照的。摩托罗拉手机(Atrix 2)照的照片命名方式是YYYY-MM-DD_HH-MM-SS_XXX.jpg,例如2013-06-11_00-15-48_100.jpg,最后的100可能是指00:15:48秒的第100毫秒;而佳能相机(EOS 5D)照的照片命名方式是IMG_XXXX.JPG,例如IMG_5845.JPG。这些照片放到一个相册里面,按照名字排序后是混乱的,这让我在几百张照片里面很难快速找到某张照片。我用的相册软件(Picasa)不支持按拍摄时间排序,否则按照拍摄时间排序就可以了。

这么对比,才发现传统的数码相机给照片起名的方式有点蠢。我第一台数码相机是惠普的,前缀是hpim。索尼是DSC,莱卡是L,后面都跟着照片序列号。IMG_5845.JPG这样的名字没有任何有用的信息,而2013-06-11_00-15-48_100.jpg却给出了非常重要的照片元信息。在浏览相册时,看到名字就知道了这张照片是什么时候照的。

于是写了个批量改名的脚本,把佳能相机照的照片都改名为照片拍摄的时间。这样就能和手机照的照片放到一个相册里面,按名字排序,也就自然是按照拍摄时间排序了,浏览时候就很方便了。我觉得数码相机照的照片都可以这么处理,方便浏览,不过这要求数码相机的时间是准确的。

2013年9月25日星期三

MongoDB的高可用

一个应用的数据源是MongoDB,DBA已经配置了多机的Replication Set,但是在客户端程序用Pymongo连接的时候还指定的是一个IP。结果在网络升级的时候的网络瞬断,让MongoDB自动切换了master,导致客户端连接失败了,出现Master has changed的异常。其实早就计划要修改客户端配置,支持RS的,但是一直没有完成,终于掉进这个坑里面了。

有意思的是,如果MongoDB没有做RS,那么网络瞬断只会让服务短暂不可用,网络恢复的时候服务即可恢复;而服务端做了高可用,客户端没有进行相应的配置,可用性反而降低了。看来在服务端和客户端都进行正确配置的情况下才能实现真正的高可用性。

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
可是这次有线网络出问题,我竟然折腾了这么久才找到个解决办法。

Git仓库的体积问题

我用git-filter-branch(1)手册中的方法:
git filter-branch --subdirectory-filter foodir -- --all
把Git仓库中的一个目录独立成一个新的Git项目。发现项目的.git目录没变小太多。原项目:
$ du -sk .git
55332 .git
新项目:
$ du -sk .git
49792 .git
新项目其实代码和历史都不多。我做过一次相同的事情,新项目会显著变小。我猜测是不是推到Git服务器上后会自动压缩,去掉历史里面无关的内容。于是我把内容推到GitLab(3.1版,后台是Gitolite管理),再clone下来,果然变小了很多:
$ du -sh .git
268K .git
我到服务器上检查,服务器上的仓库目录也压缩了。
所以git-filter-branch之后不用担心体积大小,放到服务器上会自动压缩的。

2013年3月1日星期五

Gitlab:缺乏工程性的项目

GitLab是一款界面类似GitHub的Git管理软件。虽然很强大,但是在工程性上欠佳。

GitLab的开发很快,也很流行。到官方博客去看,差不多一个月一个小版本,几个月就出一个大版本。3.0还是去年10月发布的,现在master分支已经是针对5.0的了,应该很快就要发布了。这些大版本都有重大的不兼容引入。

4.0前,仓库是在一个名字空间内。4.0开始,仓库是按用户名来区分的,类似GitHub的user/repo的命名方式。这和之前版本的用法是不兼容的。如果用户不想用这种用户名区分的仓库命名方式,想安装去年11月发布的3.1,就不能按照官方给出的文档地址来安装。因为这个是stable分支,现在指向的是4-1-stable,我们需要切换到3.1分支的文档来参考安装。

仅仅3月多月前的稳定版都没有一个正确的安装文档。这个文档里面init script和Nginx配置文件的地址分别是:https://raw.github.com/gitlabhq/gitlab-recipes/master/init.d/gitlab和https://raw.github.com/gitlabhq/gitlab-recipes/master/nginx/gitlab。master分支已经针对5.0了,和3-1-stable不兼容。gitlab-recipes这个项目没有针对3.1的分支。怎么办呢?我只好按照GitLab 3.1的发布日期2012年11月22日,去找到gitlab-recipes最接近那天的版本,下载这个版本的配置文件。

从4.1版本开始,GitLab的作者已经全职开发GitLab。充足的时间让这款软件的演进速度很快,但由于作者更专注于新功能的开发,而忽略了维护工作,导致文档缺乏,没有一个长期支持的稳定版本,缺乏对发行版本生命周期的管理。

GitLab一直依赖他们fork的Gitolite。5.0开始,将不依赖Gitolite,而用他们自己开发的GitLab Shell来管理仓库。有用户提出,之前GitLab宕了Git仓库还能访问(Gitolite是独立的);这样改动后,GitLab如果宕了整个Git仓库就不能访问了。

此外,GitLab安装过程繁琐,容易出错,而目前没有打包好的安装包。我觉得这对推广软件是很重要的,但貌似作者并不重视发行渠道。

根据我安装GitLab的经历,我觉得目前的GitLab还不太适合企业里面全面推广和长期部署。

2013年2月25日星期一

Ruby的psych错误

按照官方文档安装GitLab,执行有些命令时会出现如下警告:
It seems your ruby installation is missing psych (for YAML output).
To eliminate this warning, please install libyaml and reinstall your ruby.
我觉得这只是个警告,就没有处理。而且按照上面的说明,只是用于YAML输出的,应该没有大问题。

后面执行这一步
sudo -u gitlab -H bundle exec rake gitlab:setup RAILS_ENV=production
出了这样的错误。我没有处理,继续按后面的步骤操作。安装完后,GitLab的网页可以打开,但是用LDAP认证登录是出现类似错误:
Could not authorize you from Ldap because "Can't dump anonymous class class".
GitLab诊断文档说这个错误出现是因为没有用Ruby 1.9.3,可我是新装的Ruby 1.9.3啊。在这个issue里面发现我的错误与一开始被我忽略的psych错误可能有关,需要先解决psych的错误。

按照这个方法重新安装Ruby之后还是有错。后来觉得这个答案更适合我的情况,可还是未能解决。折腾了多次后无解,我觉得我的Ruby环境已经被搞乱了,决定按照GitLab的安装文档从头安装一遍Ruby。

最终通过重新安装Ruby解决了上述错误。至于Ruby没有psych的原因,是我犯错误没有执行其中一条安装依赖的命令(我看到公司Wiki上有人贴的过程说没有执行这步也可以,而实际是我看错了,他是执行了这一步的):
sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl git-core openssh-server redis-server postfix checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev
其中的libyam-dev没有安装,就导致Ruby没有了YAML的输出功能。

2013年2月5日星期二

SSH公钥认证的问题

在Gitolite上配置了公钥,对应的私钥放在用户A的.ssh目录下可以认证,而用户B的目录下认证则失败。区别在于用户A的.ssh目录下只有私钥id_rsa,而用户B的目录下除了私钥,还有一个老的公钥id_rsa.pub,这和私钥是不配对的。

经过对比,这俩用户下的私钥是一模一样的,.sshid_rsa的权限也都是正确的。

ssh-v参数,看到这两个用户的公钥认证的输出信息稍微有所不同。用户A的:
debug1: Next authentication method: publickey
debug1: Trying private key: /home/userA/.ssh/id_rsa
debug1: read PEM private key done: type RSA
debug2: we sent a publickey packet, wait for reply
debug1: Authentication succeeded (publickey).
用户B的:
debug1: Next authentication method: publickey
debug1: Offering RSA public key: /home/userB/.ssh/id_rsa
debug2: we sent a publickey packet, wait for reply
debug1: Server accepts key: pkalg ssh-rsa blen 279
debug2: input_userauth_pk_ok: fp ef:a2:55:bc:86:1f:0a:48:e1:45:fc:b5:da:ca:70:e0
debug1: read PEM private key done: type RSA
debug1: Authentications that can continue: publickey,gssapi-keyex,gssapi-with-mic,password,keyboard-interactive
debug1: Trying private key: /home/userB/.ssh/id_dsa
用户A开始打出的是:Trying private key,而用户B打出的是:Offering RSA public key

最后实在看不出配置哪里有不同,就把用户B下面不相关的公钥id_rsa.pub移出去,然后认证成功了。因为我认为公钥不重要,认证看的是私钥,所以没有管那个无效的公钥。结果罪魁祸首竟然就是这个无关的公钥。

看来OpenSSH如果看到公钥文件名,则从公钥检查起,而这样不配对的私钥就认证失败了。而如果只放私钥,反而会成功。从这个文件里面也能验证这一点。

2013年1月28日星期一

Zip压缩包乱码的处理

别人用Windows发过来的Zip压缩包,用unzip命令解压缩后是乱码的,也是编码问题。用如下命令即可:
unzip -O gbk foo.zip
RAR压缩包乱码处理方式在此