PATH
环境变量的问题:
以前偷懒一直用 workaroud 避开问题,前段时间终于弄清了原因和解决办法,今天终于有时间写下来。问题的起因得从 zsh 的启动说起,zsh 启动过程会读取如下文件(摘录自 man zsh
):
/etc/zshenv
$ZDOTDIR/.zshenv
/etc/zprofile
, $ZDOTDIR/.zprofile
(未设置 ZDOTDIR
时默认使用 HOME
)/etc/zshrc
, $ZDOTDIR/.zshrc
/etc/zlogin
, $ZDOTDIR/.zlogin
zshenv
无论是否为 login shell 都会读取,但设置环境变量我们只需在 zprofile
中完成即可。OS X 在 /etc
目录下有两个一模一样的 zshenv
和 zprofile
,我删除了多余的 /etc/zshenv
,并在 .zprofile
中设置用户的环境变量。完全抛弃 zshenv
可以减少 zsh 启动时需要读取的文件数,同时避免它对环境变量做出不必要的修改。
PATH
环境变量问题来源于 tmux 默认以 login shell 的方式执行 shell。zsh 作为 login shell 时会执行 /etc/zprofile
,而该文件会调用 /usr/libexec/path_helper
,根据 /etc/paths
等文件设置系统环境变量。path_helper
会将当前 PATH
环境变量添加到系统环境变量之后,这一行为导致了 PATH
的重复,并且自己设置的路径出现在系统路径之后。
了解问题的根源之后解决办法很简单,在 ~/.tmux.conf
中添加下面选项禁止以 login shell 启动 zsh 即可:
set -g default-command /usr/local/bin/zsh
]]>首先介绍一下背景。(如果你不熟悉 HTTP 协议,推荐 HTTP Made Really Easy,这是我见过最简洁清晰而且较为全面的 HTTP 协议介绍。)HTTP 1.1 定义了 persistent connection,允许用一个连接完成多个 HTTP 请求。重用单个连接完成多个请求的主要好处有两个:
COW 处理并发 client 连接采用的方式是每个连接由一个 goroutine 处理,称之为 client goroutine。由于 Go 能利用多核,为避免在 client goroutine 之间共享数据引入同步开销,以前的设计是每个 client goroutine 创建的 web 服务器连接是私有的。这个设计的缺点是减少了重用服务器连接的机会:
在实现 COW 之前我没什么网络编程的经验,当时没有意识到建立连接的开销,slow start 我也压根就没想到。当时错误得以为避免同步开销的好处更大,所以采用了服务器连接私有的设计。
发布 0.8 之前,为了解决 taobao 首页加载缓慢的问题添加了统计连接情况的代码,然后发现不同浏览器在创建和使用代理服务器连接上的行为非常不同。(之前我在推上发布过一些数据,不过很抱歉,最开始的数据有误,而且当时没注意到更有意思的现象。)Chrome, Firefox, Safari 通过 COW 打开 taobao 首页的 log 我放在了 gist 上,有兴趣的同学可以自己观察一下按 client connection 分组的日志 (文件名带 grouped 后缀)。
打开 taobao 首页需要连接的 host 大概在 20 个,各个浏览器使用代理服务器连接的特点如下:
grep 'close idle connections' firefox
的输出,跟 Chrome 做对比)在观察到这个现象之后我决定修改 COW,实现共享连接的功能。选择私有连接还是共享连接,其实是在同步开销和重用连接之间进行取舍,回头想想其实一开始就应该实现共享连接。TCP 建立连接需要两个 RTT,建立之后还得 slow start,时间开销在几十几百毫秒的量级;同步的开销用 pthread mutex 无竞争的情况下完成一次拿锁解锁操作耗时也就 20 多纳秒,我最终实现的 connection pool,用了 mutex, map, channel,时间开销跟毫秒应该还差好几个量级,而目前 COW 也没有使用很多核的必要,所以即使共享也不会出现严重的竞争。
从去年八月份写下 COW 的第一行代码到现在已经快一年了,其实在第一次发布以前就一直吃自己的狗粮。目前的工作方式已经让我自己感到满意,搭配 shadowsocks 自己觉得还是挺方便的。我自己能发现的 bug 都已经解决,剩下的只能依靠其他用户来发现了。遗憾的是代码不够整洁,很多时候看 Go 标准库时会自叹设计能力太差,偶尔会直接抄一些代码用到 COW 中。
接下去的打算是继续修 bug,重构代码。很早之前就有考虑过开发 Mac 的 GUI,不过这方面的经验基本为 0,而且时间不允许。如果有同学有兴趣做的话欢迎跟我联系。
]]>这次实现的算法跟 One Shall Pass 的一致,可以指定密码长度、包含多少特殊字符等选项。插件用 Chrome 来同步密码选项。这个算法依赖 PBKDF2 生成 key 的开销来增加暴力破解 passphrase 的代价,所以可以公开算法,也因此可以用 JavaScript 实现然后放在网上以便随时在浏览器中使用。
除 Chrome 插件外实现了两个独立的页面,其中一个专为 iOS 优化,添加到主屏幕后可离线使用。有兴趣的同学可以移步 github 获取更多技术细节,体验地址如下:
密码管理很多人可能会推荐 1Password,为每个网站生成完全独立的密码看起来也更安全。不过这种方式必须要存加密后的密文。1Password 是用 master password 通过 PBKDF2 生成 key 来加密密码存储到文件系统。(参考 1Password 官方博客文章,值得一读,对 master password 的重要性给了非常好的说明。)下面是两种方式的比较:
两种方式各有优缺点,但选择一个强 passphrase/master password 都是保证安全的更为有效的办法。
其实我是密码学门外汉(所以才选择用 One Shall Pass 的密码生成算法),自己写一个密码生成工具最大的原因是我觉得 1Password 定价过高,自己写一个也比较有意思。写过几个插件以后觉得浏览器真是个不错的平台,用 HTML 实现这种简单的工具比实现 native 更方便,而且还跨平台。CoffeeScript 真是个好语言,融合 Ruby/Python 等语言的优点,写起来舒服,同时还可以让我这个 JavaScript 半吊子能避免 JavaScript 的各种坑。
]]>写了个 Safari 扩展 InstapaperBookmarklet,在工具栏添加一个按钮来执行 Instapaper bookmarklet。如果当前标签是空白页面的话点击会访问 Instapaper 网站。
如果你跟我一样使用 SafariTabSwitching 这个基于 SIMBL 的插件(实现 cmd+num 切换标签页),习惯隐藏书签栏,又喜欢官方 bookmarklet 的视觉反馈,那么可以试试看这个扩展。
Safari 上其实有很多不错的扩展。canisbos.com 这个网站上有很多,我目前使用如下几个:
其他推荐扩展:
写 InstapaperBookmarklet 的时候了解了 Safari 扩展的工作机制。简单来说扩展包含一些 html 页面和 JavaScript 代码。html 负责扩展界面,JS 实现逻辑。扩展的 JS 执行时只能访问扩展页面的 DOM,要在用户打开的网页添加功能则必须向这些网页注入 JS。有些扩展需要向所有页面注入 JS,如果注入的 JS 文件很大则可能对浏览器性能造成影响。
]]>测试网页链接在此。
其实写这个测试页面更多是尝试下写网页。Twitter Bootstrap 对我这样几乎完全没有前端经验的程序员来说的确是好东西。顺便还尝试了下 CoffeeScript,这语言看起来真不错,融合了 Ruby, Python 很多好的 feature。
P.S.
Update: 因为 iPhone 屏幕亮斑昨天再次去 Apple Store 更换,得知现在的电池跟以前的不同,如果拿下来会导致变形,所以现在维修都是机头和电池一起更换。
很多人应该知道今年央视 315 是故意黑苹果。即使我这次换到的屏幕有亮点(苹果会同意再次更换),我还是想说这次去 Apple Store 维修的经历很愉快。维修时我只是随口提了一下,Apple Genius 就额外给我换了新的电池和耳机。这样大度的售后或许是苹果产品较高价格的另一个合理之处。
]]>并行程序在多个 CPU 上同时执行代码,如果每个 CPU 访问的都是自己私有的数据,那世界将非常美好,我们可以既不需要同步也不需要考虑 memory consistency model 的问题。但实际情况是总会有一些数据需要共享,有些 CPU 修改共享数据,有些读取。
Memory consistency model 定义了多个 CPU 同时对共享内存进行读写操作时内存的行为。
在开始介绍令人发晕的多核问题前,先考虑单核上对同一块内存地址的读写操作应该有怎样的行为。直观的想法是读操作得到最近一次写操作写入的值,这就是 Strict Consistency。一般在单核上,我们观测到的就是这样的行为。
程序代码中指令的先后顺序被称为 program order。为了优化性能,代码中指令的实际执行顺序有可能跟 program order 不同。例如 CPU 在执行读指令等待内存加载到 cache 的过程中,可以先执行接下来的寄存器操作指令。这样的好处是 CPU 不用白白等着从而更好的利用流水线。
我们可能遇到两种 reorder:
为便于编写程序,通常在单核上保证 strict consistency,也就是说对同一块内存地址,读指令总是得到上一次写指令写入的值。但这并不阻止我们对不同内存地址读写指令进行 reorder。
多说一句,写设备驱动的人有时需要防止 compiler/CPU reorder,因为对硬件寄存器的读写会影响硬件的状态,reorder 可能会改变驱动的语义。
单核有了 reorder,在多核上推断不同 CPU 上指令之间的执行顺序就有了问题。下面看一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
假设 boo
, bar
在不同 CPU 上执行。如果 foo
中的两条写操作发生了 reorder,bar
观察到 b
变为 1
并不意味着 a == 1
,所以 bar
中的 assert 就有可能触发。
reorder 在多核上带来的一个问题是:我们不能简单的利用单核上的 program order 去推断不同核之间指令执行的先后顺序。
要注意的是,并不一定要有 reorder 才会导致上面的这个问题。每个 CPU 有自己的 cache,cache 之间通过 cache coherence protocol 来维护一致性。例如某个 core 在修改自己的缓存时需要向其他 core 发消息说这个地址的内存值我已经修改了,你们不能直接使用之前缓存的内容。这些消息何时传播到其他 CPU,是否要等到消息得到确认才能继续往下执行等等问题才是影响 memory consistency 的关键。我们可以把不同的 CPU 看做分布式系统中的不同节点,即使每个节点按顺序执行操作,对共享数据操作的结果何时传播到其他节点,会影响其他节点观测到的本节点的操作执行顺序。
有人可能会问,为什么不在多核上也实现 strict consistency 来简化编程?问题在于实现 strict consistency 将迫使我们放弃很多优化的机会,例如很多 cache coherence 消息都需要得到确认之后才能继续往下执行,从而使得 CPU 的流水线不能得到最好的利用。
为了实现一些优化,常用的办法就是牺牲一些一致性的保证。其中最简单的一个一致性模型是 sequential consistency。它要求不同 CPU 上执行的指令能够等价于让它们以某种顺序执行,而且每个 CPU 上的指令在这个序列中以 program order 出现。换句话说就是多个 CPU 上执行的指令能够等价于把它们穿插在一起放在一个 CPU 上执行。sequential consistency 保证所有的指令之间可以定义一个全序。
sequential consistency 比 strict consistency 要弱。下面的例子中 CPU 1 和 CPU 2 都访问内存地址 x,横向是时间,例子中的序列在 sequential consistency 下是合法的,但在 strict consistency 下不是(参考 stack exchange 上的一个问题)
CPU1: W(x)1
------------------------
CPU2: R(x)0 R(x)1
sequential consistency 允许一个 CPU 对内存的修改晚一些传播到所有其他的 CPU,但是一定要同时传播到所有 CPU。
除了 strict/sequential consistency,还有很多其他的更弱的一致性模型。关于不同的 consistency model 以及各自能够实现的优化,请参考 Shared Memory Consistency Models: A Tutorial(这篇文章不是从程序员的视角出发,对我来说不是很好懂,我只仔细看了前面几章)。
x86 是一个提供比较强 memory consistency 的体系结构,它只会把访问不同地址的读指令提前到写指令前执行,而像 ARM, SPARC, POWER 这些体系结构的 memory consistency 就弱很多,所以写这对这些平台的程序更加需要当心 memory consistency 的问题。
从我自己的经验来看,在 x86 上使用 pthread 提供的同步原语来写程序,一般不用考虑这个问题。但如果尝试自己实现同步操作,或者尝试实现 lock-free algorithm,那一定要小心 memory consistency 的问题。我在实现一个记录不同线程对共享内存访问顺序的算法时第一次遇到这个问题。如果你想真的观察到 memory reorder,推荐 Memory Reordering Caught in the Act 这篇博客文章。
Paul E. McKenney 的文章 Memory Barriers: a Hardware View for Software Hackers 从 cache coherence protocol 出发,以程序员的视角解释了 CPU 为何要 reorder 以及如何使用 memory barrier 来编写正确的程序,强烈推荐。
有些 language 会专门定义自己的 memory model,Java 是第一个这么做的主流语言,C++11 和 C11 也定义了 memory model,Go 也有自己的 memory model。
Go 的 memory model 定义一个 goroutine 中的读操作何时保证能够读到其他 goroutine 中写入的值,另外还定义了同步操作需要保证的先后顺序。一个有意思的地方是 Go 的 channel 操作不满足 sequential consistency,所以在用 channel 来实现 semaphore 时要特别当心,具体请看这篇博客(最好跟下面提到的 reorder 优化一起看)。
Language memory model 限制了哪些操作不能被 reorder,除此以外的操作可以安全的利用 reorder 优化。golang-dev 上的这个回复给了一个利用 channel 操作不要求 sequential consistency 来做 compiler 优化的例子。
Memory model 是一个不小的话题,我不是这方面的专家,这篇博客只是掀开了冰山一角而已。感兴趣的读者还请自己阅读前面提到的几篇文章,对程序员来说最重要的还是 Paul McKenney 的那篇。
]]>没有刹车,意味着不能很快减速,出于安全考虑不得把车开慢些。刹车给了我们减速的能力,因此让我们能够驾驶的更快。
原文作者将此类比到软件开发中,如果将软件构建的过程类比为开车,那测试就好比是刹车。
最近优化 COW 性能的过程中对此非常有同感。之前写 COW 的时候偷懒,没有写很多测试。最近做性能优化的过程中引入过一些 bug,没有测试代码即使 fix 了也不是非常放心。几次之后索性修改哪部分代码就先把测试补上。虽然写测试需要花额外的时间和精力,但有测试之后可以放心修改代码、尽早发现 bug,总的来说应该反而节省更多的时间。
这次我才是切身体会到测试对于重构以及优化代码的重要性。
最后,顺带宣布一下 COW 0.6 版发布。主要变化:
感谢 go test 以及 Travis CI 在测试上提供的帮助。
]]>感谢 glacjay 的建议,让我把 COW 做的更加自动化一些。0.5 版本最大的变化也在此。
由于没法准确判断超时是由于被墙还是连接状况差导致,被墙网站的判断没法做到非常准确。COW 现在的做法是:
我自己用下来这个实现的效果还不错。原先的 updateBlocked
, updateDirect
, autoRetry
这些需要人工干预的选项都去掉了,名字很囧的 chou
文件也不需要了。
其他改动:
之前想过的解决 DNS 污染的功能还是不打算做了,像 twitter 即使拿到了正确的 IP 其实还是不能访问,加入这个功能或许意义不大。使用 Go 的 ssh 实现来内建 ssh 代理支持的功能暂时也不做了,一来不清楚这个实现的稳定性,二来我觉得 shadowsocks 比 ssh 代理更加好用,而且除了 windows 平台以外加入这个功能的好处不大。
欢迎更新使用,发现 bug 麻烦在 github 上给我发 issue report。
]]>It’s genesis was the fact that writing a binary encoder for the x86 is an unredeemably disgusting experience.
我想说,写 x86 的反汇编器是一样的感受。
折腾了 QEMU 两年多,看到 Bellard 大神用 Javascript 写的 x86 模拟器后也曾想用 Go 写一个 x86 的模拟器。
2012 年的时候花了些时间从反汇编 x86 指令开始写,写着写着就没有动力继续下去了。想要模拟 x86 指令集非常麻烦,光是反汇编和 condition code 处理就包含了大量无趣的体力活。暂时不会有兴趣继续这个项目了。(或许开始时应该采纳实验室同学的建议选一个简单一些的体系结构来模拟,例如任天堂的游戏机,不过因为懒得去学其他体系结构而没有采纳。)
不过这个半途而废的项目倒是让我更加了解 x86 的指令。这篇博客就分享一些当时找到的关于 x86 指令集的资料和发现吧。
前面提到这些文档都比较容易理解,可以对 x86 指令的编码方式有一个整体的了解。当想要了解具体某条指令的编码和含义时,Intel® 64 and IA-32 Architectures Software Developer’s Manual 是最权威的官方文档。其中 Volume 2: Instruction Set Reference 是专门介绍指令的。
我自己做的一些笔记对阅读这些文档会有一定的帮助。
最后还是把没写完的代码放出来吧,见 github。
]]>先介绍一下为什么 Valgrind 需要 GIL。Valgrind instrument 二进制文件并且执行采用的方式叫做 Disassemble-and-resynthesise (D&R)。其大概过程如下:
由于中间代码是 RISC like 的,一条 CISC 的读写指令操作可能对应多条中间代码,最后生成的对应原先读写指令的 native code 有多条指令组成。在多线程的情况下这会使得读写操作不再是原子性的。(即使在单核机器上也会有这个问题,因为线程可能在执行一条读指令的过程中被调度走。)
Valgrind 解决这个问题的方式是让多线程程序在任何时候都只有一个线程在执行,这就需要一个类似 Python/Ruby 解释器里 GIL 的东西,在 Valgrind 中这个机制实现很巧妙。
Valgrind 利用一个 pipe 来实现这个机制。尝试拿锁的线程都尝试从全局的 pipe 中读,由于 pipe 中没有内容可读而 block 住。锁拥有者在解锁时往 pipe 中写一个字节,OS 会保证只有一个线程被唤醒,读出 pipe 中内容然后往下执行,其他线程则继续 sleep。这个方案里究竟那个线程被唤醒还是由 OS 决定,但可以严格保证任何时候都只有一个线程执行。(用 pthread mutex 的话还是会在 spin 的时候有一些线程同时执行,虽然对 Valgrind 的应用场景来说应该没有什么问题。)
Valgrind 的论文还是挺有意思的。虽然也是 binary translation,但它没有使用 code chaining,而是用手写的汇编来提高从 translated code 执行跳出后再次进入 translated code 的速度;翻译的单元也不是通常使用的 basic block 而是 super block。论文里提到的很多机制现在实现方式都是已经修改过多次,写出这样一个成熟的工具真是非常不容易。
]]>严格的说应该是 pthread mutex 在 Linux 上其实非常快,用户态程序没有特殊需求就不要自己去实现 spinlock 了。
今天受实验室同学的启发,把以前测试 spinlock 性能的 micro benchmark 用 pthread mutex 也试了一下,结果发现这货真是逆天了!
我的 micro benchmark 做的操作非常简单,拿锁、加 counter、然后解锁。不管测试使用多少个核都做 1600 万次这样的操作。
pthread mutex 在无竞争下完成这 1600 万次操作耗时 0.36 秒(Intel Xeon E7 处理器,2 GHz),平均下来每次拿锁、加 counter、解锁操作耗时仅 22.5 纳秒。(如果对这个数字没什么概念,推荐看下 Latency Numbers Every Programmer Should Know)
无竞争下 pthread mutex 的开销的确是比 xchg 的实现开销大(0.21 秒),但是只要超过两个核,它的性能就已经超过 xchg,而且核数增多对性能几乎没有影响。到 32 核时,pthread mutex 完成 1600 万次操作总共耗时 1.31 秒,而 xchg 需要 2.82 秒,pthread mutex 快了一倍。要注意的是 xchg 还是实现 spin lock 时伸缩性最好的一个(用户态测试结果)。
这个结果让我有些吃惊。
无竞争下为何速度如此之快?从 stack overflow 上的一个回答来看,Linux 上的 pthread mutex 使用 futex 实现。无竞争时拿锁操作可以完全在用户态完成。
为何竞争激烈时能 pthread mutex 能比 xchg 快如此之多?futex 在发生 contention 时会使用系统调用,调用线程因此可能进入 sleep 状态,从而使得同时尝试拿锁的核数变少。相反,xchg 实现的 spinlock 中,每个 waiter 都在不断的检查锁的状态,lock holder 解锁的写操作会使得所有的 waiter 都发生 cache miss。waiter 很多时,解锁操作使每个 waiter 发生 cache miss,而处理 cache miss message 的硬件(home directory)对这些消息只能一个一个处理,所以同时尝试拿锁的核数越多,拿锁解锁操作变慢越多。实际上,在执行使用 pthread mutex 的 benchmark 时,用 htop
可以看到 CPU 大多数时间都在内核态,而使用 xchg 的 benchmark 运行时则是几乎所有时间都在用户态。
更多关于 cache 对锁性能的巨大影响推荐看 Silas Boyd-Wickizer 的论文 Non-scalable locks are dangerous。(顺带说下,这篇论文的作者中 Frans Kaashoek 是 system research 界的大牛,Robert Morris 就是著名的 morris 蠕虫的作者,跟 Paul Graham 一起创办了 ViaWeb。)
其实 critical section 越短,拿锁解锁操作开销占整个 critical section 执行时间的比例就越长,这时由于竞争导致拿锁解锁操作变慢的影响体现的越明显。我的 micro benchmark 就是这种情况。
这个测试结果给我的启示是,用户态程序想要有良好的伸缩性关键还是好好设计程序结构,避免共享状态导致的竞争。至于同步原语,pthread 提供的实现性能已经足够良好。没有特殊需要自己设计用户态的锁,如果实现不够好反而会导致糟糕的性能。
]]>用 mmap 写日志的麻烦之处是 log 内容超过文件大小时必须要把文件变大才能继续往后写。这个库主要做的事情除了创建文件,调用 mmap 完成初始化之外就是增大日志文件了。
增大文件现在的处理方式是当文件不够大时通过 truncate
将文件变大,然后将扩大的部分 mmap 上来,同时归还之前 map 的内存。
其实 bprint 就是在写完这个 log 库之后写的。或许有同学会觉得有用。
]]>代码在 github 上。这个脚本应该是我为数不多的使用管道风格写的 python 脚本。(从 10 年起不做生物信息学之后基本上就没有再写过 python 代码,日常使用的脚本转向 ruby 了。)
这个脚本主要就一些日期解析和格式化的代码,然后由一个 delaysrt
函数把这些代码整合起来,
1 2 |
|
看到这行代码时感慨了下自己居然写过这样的 python 代码。cat
, tr
之类函数的实现在我 2009 年的一篇文章中有介绍,没想到一晃已经到了 2012 年的末尾了,时间过得好快。
github 不支持上传文件了,所以把 binary 放到 google code 上了,下载时可能会撞墙。
后面打算先实现简单的用户认证,然后会考虑加入类似 pydnsproxy 的防 DNS 污染的机制(是否会最终引入要看效果)。
]]>在看这个演讲之前,我并没有意识到 C 的头文件存在如此之大的问题。我对 C (以及 C++) 头文件最大的抱怨在于需要额外维护一份函数接口文件。
从 Gregor 的演讲来看,C 头文件最大的两个问题有两个:
这两个问题的根源都在于 C 头文件的工作方式:#include
只是简单的将指定文件包含进来,而没有程序任何的语义信息。
先来看第一个问题,采用幻灯片里的例子,下面的 C 代码定义了 FILE
这个宏,然后包含了头文件 stdio.h
1 2 3 4 5 6 |
|
尝试编译这个文件会遇到一大堆的编译错误,原因很简单,stdio.h
中出现的所有的字符串 FILE
都被替换成了 “MyFile.txt”。这里定义一个名为 FILE
的宏错误还是比较明显的,如果是使用其他库,还是很有可能不小心在引入头文件时发生名字冲突。引入名字冲突导致编译错误还好,最怕的就是 silient error,出错了得找半天。
编译开销的问题其实很简单,之所以以前没有意识到是因为小项目感觉不到这个问题,大项目上编译速度的影响才会比较严重。假设有 M 个头文件,被 N 个 C 文件包含,那么头文件在编译时总共被包含 M*N 次。这里的开销在于编译每个文件需要打开很多其他的头文件,而且每个头文件都需要重新解析。C++ 由于很多头文件中包含代码,所以使得编译速度的问题变得更加严重。Gregor 给的例子是 LLVM 中一个 469K 的实现文件在展开后达到了 3.8M,一个数量级的差异!
目前名字冲突的避免方案有几个:
#undef
#undef
,每个文件内部都定义相同名字的宏,那么头文件包含顺序不同得到的同名宏定义就会不同,这是一个危险的 silient error编译速度的一个解决方案是预编译头文件,但是这个要求用户始终保持头文件和预编译版本的一致性。Gregor 认为这是一个糟糕的解决方案,我自己没有使用过这一技术,不加评论。
Gregor 对此提出了一个兼容现在 C 的使用习惯的方案,有兴趣的同学可以自己去看他的 slides。
在看这个 talk 时候经常想起 Rob Pike 在 SPLASH 2012 的演讲 Go at Google。其中也提到了头文件导致编译速度慢的问题。Go 的解决方案一方面是把 import
作为语法级别上的语句,另一方面是精心设计 package binary,其中包含解析过的代码信息,使得 import 任何一个 package 只要打开一个文件而且不需要重复编译。在后面的博客里打算介绍一下这次演讲的内容,其中包含很多 Go 的 design choice。
Update 2012-12-10: 发布之后发现引入了一个新的 bug,会导致有些网站访问不正常。所以把下载页面的二进制文件删除了,等 bug 修完再重新上传。
新功能:加入 shadowsocks 支持,允许指定 ssh server 端口。
除臭虫:修正了一个 crash bug,还有一个 http chunked encoding 处理的 bug。
推荐更新。
Update on 2013-01-01: 其实 ReadFrom
只对某个 fd 是普通文件的时候会使用 sendfile
系统调用。否则会 fallback 到普通的先读到 buffer,再写的方式。ReadFrom
在实现的时候发现无法使用 sendfile
时会把 witer 包在一个仅暴露 io.Writer
接口的 struct 中再调用 io.Copy
。
还有一点性能的优化。对所有的 net.Conn
接口,不再使用 bufio.Writer
来缓冲写操作。Go 的 net.TCPConn
实现了 ReadFrom
函数,io.CopyN
函数发现写端实现了这个函数时会调用该函数,因此可以避免从 bufio.Reader
中读出再写到 bufio.Writer
这样没必要的拷贝。虽然会多一些写操作,但理论上来说减少不必要的拷贝应该可以减小加载网页时的延迟。不过不熟悉网络性能测试工具,没有实际测试过。(其实用 ab 测试过 cow,结果 ab 说数据标准差过大,结果很可能不可靠。)
昨天才发现 go 原来有 ssh 的实现,在考虑要不要用这个来创建 socks 连接。好处是不需要系统安装有 ssh (windows 上使用会方便很多),可以不用另外开一个 ssh 进程,避免网络数据在两个进程间传递的开销。不过不清楚这个 ssh 实现的成熟度,得先看下才能决定。
]]>预编译的二进制文件提供了 OS X 和 Linux 的版本,用下面的命令即可安装:
curl -s -L https://github.com/cyfdecyf/cow/raw/master/install-cow.sh | bash
希望可以方便没有安装 Go 的人使用。
前几天用 HTTPS 访问 Google 经常出错,所以才想到去检测除 timeout 和 reset 之外的 HTTP CONNECT 的错误。对 SSL 没有了解,不过觉得 SSL 错误代理服务器没法可靠的检查出来(除非像 GoAgent 一样做中间人)。现在的检测基于这样的观察:浏览器发现 SSL 错误会马上关闭连接。COW 在发现这种情况时会把请求的域名加到被墙列表中。其实这个观察有一个明显的漏洞,用户主动停止请求也会遇到这样的情况。好在用户主动停止请求应该是少数操作,而且只有在关闭连接的时间小于 1 秒的情况下才会认为被墙。
这个机制看起来不是非常可靠,但实际工作效果还可以。为了能正常访问 Google,COW 默认开启这个检测机制。
]]>我的需求是只有访问被墙网站时走其他通道,其他网站直接访问。使用 VPN 需要通过路由表来实现这个目的,因此需要被墙网站 ip 列表,或者反过来拿到国内网站 ip 列表。不过我不太喜欢在系统上添加一堆路由表,所以以前一直是通过 socks 代理加 PAC 文件来达到目的。socks 代理加 PAC 的问题有两个:
COW 解决的就是这两个问题。对客户端的 HTTP 请求,COW 会首先尝试直接连接,如果发现被墙则记录下访问网站的域名,然后尝试使用 socks 代理完成请求。以后再次访问这个域名下的主机时就会直接走 socks 代理。对于抽风型网站,被墙后每过一段时间会再次尝试直连。
COW 还支持生成 PAC 文件,其中包含所有访问过的可以直接连接的域名。通过 PAC 来配置代理时,client 在访问这些域名下的主机时会直接连接而不通过 COW。所以使用 COW 一段时间后,经常访问的那些未被墙网站会直接连接而不会有走代理的开销。(PAC 解析开销可以忽略。)
目前 COW 版本号为 0.3.1,只支持 socks 作为二级代理。我日常在 MBP 上将 COW 设置为全系统代理,实验室 Linux 台机上也开着 COW 同时提供给实验室多位同学使用。肯定还存在一些 bug,不过就日常使用来说还算稳定了。用 socks 代理的同学可以试试看,欢迎提供 bug report 和建议。
PS: 写 COW 的时候越来越觉得 Go 是个不错的语言。
]]>vyatta 的问题是对于它设计时支持的网络环境配置起来的确非常方便,但是如果需求超出它的范围,则需要绕过它来自己配置,但由于它不是标准的 Debian,有些事情做起来不太方便。我们使用 vyatta 时就遇到了这样的问题。
我们希望实验室的每台机器配置好自己的 hostname,通过 DHCP 获取 IP 时自动更新 DNS 服务器中对应 hostname 的记录,从而通过名字来实现机器间的互相访问。ISC 提供的 DHCP 和 BIND 就提供了这样的功能。原理是 DHCP 服务器在分配 IP 给 client 之后发送更新 DNS 记录的请求给 BIND 服务器,当然 BIND 需要配置成允许更新 DNS 记录。(动态更新 DNS 在 RFC 2136 中有描述。)
vyatta 使用了一个修改过的 DHCP server,而 DNS 服务器使用了 dnsmasq,似乎这两个服务不能很好的合作来实现我们的需求。实际上 dnsmasq 可以同时作为 DHCP 和 DNS 服务器,所以它可以很方便的实现动态更新 DNS 记录的功能(不需要通过单独的协议),这其实是它的默认行为。所以最后我们选择禁用 vyatta 的 DHCP 服务器,然后将 dnsmasq 配置成同时提供 DHCP 和 DNS 服务。实现这一功能的 dnsmasq 配置文件非常简单,如下:
# Never forward plain names
domain-needed
bogus-priv
# Don't forward request for this domain
local=/example.com/
# listen on eth0 for both DHCP and DNS service
#interface=eth0
# if you want to have VPN access to have DHCP and DNS working,
# it's better to listen on IP address instead of network interface
listen-address=192.168.1.1
domain=example.com
# netmask is 255.255.0.0, so 192.168.1.255 is OK to use
dhcp-range=192.168.1.2,192.168.2.255,24h
服务器端就这些配置,每台机器只要设置好自己的 host,并且在 DHCP 请求中包含自己的 hostname 即可。
使用中发现 Debian 6 在使用 /etc/network/interfaces
来配置网络时,无法更新 DNS 记录,Ubuntu 和使用桌面的 Debian 6 都没问题。查看 dnsmasq 的 DHCP log 发现 Debian 6 的 DHCP client 在 DHCPACK
消息中没有包含 hostname。这篇文章 提到了这个问题。Ubuntu 对 DHCP 客户端打了 patch,默认会在 DHCPACK
中包含 hostname,而 Debian 在使用 networking 来配置时必须要自己配置(使用 NetworkManager 配置网络时正常)。具体方法是在 /etc/dhcp/dhclient.conf
,加入如下行:
send host-name "your-host-name";
其实最开始想配置成通过名字互访时有考虑过 zeroconf,但看起来有点复杂,而且实验室 OS X, Linux, Windows 共存,Windows 平台的 zeroconf 自成一套,部署可能不便。倒是 dnsmasq 这个软件小巧好用,解决这个问题非常方便。
]]>最先考虑的是静态链接,但开发用的 Ubuntu 并没有提供所有依赖库的静态链接版本。自己重新编译所有的库比较麻烦,所以没有用这个方案。
LD_LIBRARY_PATH
和 dynamic linker之后尝试过 CDE (后面会具体介绍),由于一些限制最后还是决定自己打包,这样不依赖其他工具,可以获得完全的控制。最开始的时候以为只要简单的把程序依赖的动态链接库找出来全部放到一个目录下,在其他系统上指定 LD_LIBRARY_PATH
即可正常使用,但实际上这么做还差了一步。
只指定 LD_LIBRARY_PATH
时遇到了两种错误:
为了让编译得到的 binary 文件同时工作在 32/64-bit 的系统上,我在 32-bit Ubuntu 上编译。拷贝 binary 到 64-bit 的系统上执行时遇到了如下错误:
/lib/ld-linux.so.2: bad ELF interpreter: No such file or directory
32-bit 的 Fedora 6 上在设置了 LD_LIBRARY_PATH
之后,执行任何程序都会段错误
Google 之后发现 Stack Overflow 上也有人遇到过第二个问题。其实这两个错误的原因都跟 dynamic linker (dynamic linking loader) 有关。
在使用 GNU C 库的系统上,运行 ELF 文件时会自动执行 dynamic linker (在 32-bit Linux 上一般是 /lib/ld-linux.so.2
,64-bit 上是 /lib/ld-linux-x86-64.so.2
),dynamic linker 会读取 LD_LIBRARY_PATH
然后在指定的目录下找 shared library。但 dynamic linker 自身的路径是硬编码在 ELF 文件头中的。
所以 64-bit 系统上报的错误真正的含义是找不到 dynamic linker,因为该系统上并没有安装 32-bit 的 C 库。第二个错误应该是由于 Fedora 6 上的 dynamic linker 和我打包的 Ubuntu 的 C 库不兼容。(Stack Overflow 上的回答里说 loader 是 C 库的一部分,不过不兼容的具体原因不清楚。)
解决这个问题的办法是指定程序启动使用的 loader,调用方式如下:
LD_LIBRARY_PATH=<lib path> /lib/ld-linux.so.2 <executable>
/lib/ld-linux.so.2
由于是 dynamic linker,所以支持这样的调用方式。但其实通过 LD_LIBRARY_PATH
环境变量来指定 library search path 有一个缺点:进程会带有这个环境变量。因此进程在尝试执行系统上的其他命令时可能遇到上述的第二个问题。事实上应该尽可能避免使用 LD_LIBRARY_PATH
这个环境变量,参考 Why LD_LIBRARY_PATH is bad。
好在 dynamic linker 可以使用参数来指定 library search path,如下:
/lib/ld-linux.so.2 --library-path <lib path> <executable>
给 dynamic linker 指定参数后就会忽略 LD_LIBRARY_PATH
。
知道 dynamic linker 的问题之后,打包剩下的工作就是修改脚本来用 dynamic linker 启动可执行文件了。
如果发布的程序经常要在 shell 里交互使用,每次执行时指定 dynamic linker 还是会很不方便。实际上 dynamic linker 和 library path 信息都保存在 ELF header 中的,程序在链接时可以指定这些信息。
library path 可以通过传递 --rpath=<path list>
参数给链接器来指定,dynamic loader 则是通过 --dynamic-linker=<path to loader>
来指定。如果是由 gcc 来调用 linker,可以通过 -Wl,
来把选项传递给 linker,如下:
gcc ... -Wl,--rpath=<path list> -Wl,--dynamic-linker=<path to loader>
可以使用 readelf -a <executable>
来查看得到的 ELF 文件头信息,其中 INTERP
指定的就是 dynamic linker,而 RPATH
就是 library search path。
因为不想修改 configure 脚本,所以我最后没有采用这种方式。
其实 Linux 下打包程序的问题以前就有人尝试解决过。前段时间读了 Philip J. Guo 的
The Ph.D. Grind,他博士期间的一项工作 Code Data Environment (CDE) 就是用来为应用程序创建一个自包含的包,然后发布到其他 Linux 系统上去执行。扫过一眼 CDE 的论文,其最主要的工作机制是使用 ptrace
来拦截系统调用,从而捕获程序依赖的文件以及系统环境(例如环境变量)。简单起见只考虑程序依赖的文件,CDE 工作其原理如下:
open
这样的系统调用来看应用程序使用到了哪些文件,拷贝或者链接到打包目录下open
调用,然后将其尝试打开的文件用打包目录下的文件来替代这个原理简单有效,CDE 实际使用下来我觉得也非常方便,10 分钟内我就成功的把一台 Fedora 6 上的 vim 安装到另一台 Debian 6 上并且正常使用了。对某些需求来说,CDE 的确可以作为解决 Linux dependency hell 一个方便的工具。
然而 CDE 打包后的程序是在 sandbox 中执行,不能访问 sandbox 之外的文件,除非使用 seamless execution mode。但要让 seamless execution mode 正常工作比较 tricky,CDE 对我来说毕竟是新工具,担心了解不够需要花太多时间 debug 所以最后没有采用。(CDE 执行应用程序时需要使用 ptrace
监控程序,对程序执行性能会有一点影响,不过影响不大,对我的应用来说性能不是主要问题。)
Program Library HOWTO,这是一篇不错的介绍 shared library 的文章。这里是我做的一些笔记。
]]>