Jan 25

使用了一段时间的 MySQL,体验与使用 PostgreSQL 完全不一样。使用 PostgreSQL 时,「it just works」,而 MySQL 则是「it just doesn't work out of box」。

时间戳与整数

MySQL 有个数据类型叫TIMESTAMP,顾名思义,就是时间戳,支持的时间范围是从UTC 1970年元旦凌晨零点到UTC 2038年元月19日三点14分零七秒。毫无疑问,这是个32位的 UNIX 时间戳。

那么你觉得,当 MySQL 在整数和这样的时间戳之间比较时,会发生什么呢?报错?还是把整数转成时间戳?或者把时间戳转成整数?都不是!MySQL 会首先把整数以十进制转成字符串,然后再把字符串转成时间。也就是,20141028000000这么大的整数,会转成字符串,然后按时间的格式理解,变成2014年10月28日凌晨零点

直接把字符串转成时间没问题。可是,谁会把时间表达成如此奇怪的整数呢?

布尔值

MySQL 有个叫BOOL的类型。可是,它的文档位于数的类型章节之中。而且,它仅仅只是TINYINT(1)的别名!这意味着,MySQL 中的布尔值与整数 0 和 1 是没有分别的。连 Python 都不如,至少 Python 的 bool 是 int 的子类。

于是乎,明明 SQLAlchemy 定义时写的是布尔类型,结果因为表结构是 SQLAlchemy 自动解析的,取出来就变成了整数。

比虚设更讨厌的 CHECK

与上例类似。看起来,MySQL 是支持CHECK约束的。但是不能被表面现象蒙蔽了,文档后面写着「The CHECK clause is parsed but ignored by all storage engines」。只解析,假装自己支持,但是没有作用。这个问题在2007年二月有人报告了。近八年了,依旧如故。

继续骗人:utf8不是 UTF-8

MySQL 似乎从很早开始就支持一个叫utf8的编码了。可是,你往数据库里插入一个「😄」(😄)字符试试?你可以直接在本文后边评论试试。这个字符及其后的字符会消失,因为 MySQL 的utf8只支持 BMP(基本多文种平面)范围内的 Unicode 字符。也就是,MySQL 的utf8使用三字节表达,因此只支持 U+0000 到 U+ffff 范围内的字符。我曾经有篇文章就是因为插入了音调符号而被截断,现在只能小心地使用 HTML 转义形式来写了。

Arch Linux 的 AUR 也使用 MySQL,因此也遇到了字符神秘消失的事件。

如果使用的是 5.5 及以上的版本,可以使用一个 MySQL 称为utf8mb4的字符集,也就是用四字节表达的 UTF-8 编码。明明 UTF-8 是为了统一编码而诞生的,结果又被 MySQL 给分裂成了两个。

binlog 格式

通常,软件会默认一个尽量普适的配置,让大多数人不需要折腾就用着很爽。MySQL 反其道而行之,binlog 默认使用STATEMENT。然后,一不小心使用了它不支持的查询就报错了,让人经过 Google 之后再手动给设置成MIXED格式。

并发删除和更新

MySQL 默认的事务隔离级别是repeatable read,看上去比 PostgreSQL 默认的read committed级别要高。但是呢,有一些很怪异的行为。

两个事务 A 和 B 开始了。A 读取数据库发现 id=1 的记录。B 把 id=1 的记录删除掉。A 也决定把 id=1 的记录删除。然后 A 再读,咦?怎么 id=1 的记录还在??

PostgreSQL 在read committed级别下,删除也都能成功,但是删除之后是读不到数据了的。

PostgreSQL 在repeatable read级别下,后删除的那个事务会失败。

MySQL 在serializable级别下,后删除的那个事务才会失败。

并发更新时也是这样:

两个事务 A 和 B 同时执行update t set v = v + 1 where id = 2更新数据(假设原数据为 id=2, v=1),会和 PostgreSQL 的read committed级别一样,双方更新均成功。但是,当事务 B 提交之后,A 事务还是看到 v=1。在自己提交之前,自己的更新和其它已提交事务的更新都看不到。

我不知道 MySQL 这样的行为是否符合 SQL 标准。但我知道,它肯定不符合我的直觉:明明我都把数据给改了,为什么我自己都看不到呢?

糟糕的 Python 客户端库

MySQL 官方 C 库不支持异步,所以使用其的 Python 库完全没办法异步。

oursql 默认会处于自动提交模式,而且很久不更新了。

Oracle 自己弄的 MySQL Connector/Python 倒是没这个问题。但是有其它 N 个问题,比如你得重命名二进制数列的列名,不然会报错:

cursor.execute('select binary %s as a', (b'\xe0\xda\x94\xb8\x89\xf7',))

比如以下查询总是返回空,不管你的数据库里有什么:

cursor.execute('select * from users where token = %s', (bytes_object,))

比如 network.py:226 这里是这么写的:

            packet = bytearray(4)
            read = self.sock.recv_into(packet, 4)
            if read != 4:
                raise errors.InterfaceError(errno=2013)

于是,当你的返回结果很大,导致这里想接收的四字节数据不在同一次recv系统调用中时,就会抛出异常。真不知道写这库的人学过网络编程没,连 short read 都不知道处理。

至于像 PostgreSQL 的连接那样,通过with语句进行自动提交或者回滚就更别想了。

经常误退出的命令行工具

在 MySQL 的交互式命令行里,不小心写了一个反悔了的查询怎么办?比如回车后才发现输出太多了根本没意义,又或者查询里有个地方写错了。我下意识的反应是,按Ctrl-C,中止查询。在按下Ctrl-C的时候,如果查询还没结束,那么查询中止,一切安好。如果不小心慢了 0.01 秒,按键时查询已经执行完毕了呢?MySQL 命令行工具会直接退出(包括 MySQL 官方的,和 MariaDB 的版本),你只能再重新输入密码、重新连接。这是很神奇的事情。我试过了一些别的软件的成熟的交互式命令行工具,比如 bash、zsh、python、ghci、irb、erl,都不会在Ctrl-C时退出。lua、awesome-client 和 rusti 会退出,可前者只使用 ANSI C 的没办法,而后两者并不算成熟。

当然它有一个--sigint-ignore选项,如果你记得加上的话,Ctrl-C时就不会退出了,而是没有任何反应。它都不取消输入到一半的命令。

连接 localhost 等同于连接默认 socket 文件

MySQL 的客户端库,在连接localhost时,或者不指定要连接的主机时,会连接到它默认的 socket 文件。

我有个 MySQL 实例在 3306 端口和默认 socket 文件上监听,另启动了一个实例用于一些测试性工作,监听在 3307 端口和自己指定的 socket 文件上。但是,当不指定-h 127.0.0.1时,即使指定了端口号-P 3307也无济于事。它依旧会连接默认的 socket 文件。我花了很长时间去调试我遇到的问题,直到在 htop 里按了一下l键,看到 MySQL 命令行工具连接的不是我设想的地址才恍然大悟。

指定连接不同的地方当然就应该连接到不同的地方,不然你告诉我我的指定没有生效也好啊。这点 PostgreSQL 就处理得更好。它也是默认连接 socket 文件,即使指定了端口号。但是,指定不同的端口号时它会去连不同的 socket 文件!对于 socket 文件来说,「端口号」其实是文件名后缀,比如/run/postgresql/.s.PGSQL.5433。这样子就不会不小心连错而不自知。(当然 PostgreSQL 也不会自作聪明地在你要连接localhost的时候给连接到 socket 文件上。)

Jan 20

tmux 我已经用了好几年了,然而从未使用得多么深入,偶尔有些小不满也一直没有去研究看看能不能解决,其中就包括这么一项:tmux 窗口名称(就是显示在状态栏上的那个)默认会随着前台所运行的命令的不同而自动变化。但是,如果窗口名称被设置过之后,不管是通过prefix A设置的,还是通过终端转义序列设置的,之后它就再也不会自动变化了。

本来这也不是多大的事。偶尔会因为不小心往终端输出了些二进制数据弄乱终端标题,我要么是把它重新设置成「zsh」,要么直接关掉再开一个窗口,反正是很容器的事情。可是呢,公司服务器的 zsh 会把终端标题设置成当前的工作目录,ssh 退出时也不会清除。本来呢,我是专门再开一个终端来跑,完事之后再关掉。可是,习惯的力量是巨大的,我还是会时不时地在 tmux 窗口里 ssh 连过去,然后 tmux 窗口名称就坏掉了。

今天我终于决定把此事查个水落石出。既然主动设置之后就不再变化,那么 tmux 肯定用某种方法把「主动设置过窗口名称」这个信息给记录了下来。然后我就去 tmux 源码里找啊找,结果很意外地看到一个叫「automatic-rename」选项!敢情 tmux 早知道有人会对此不爽,专门弄了个选项呀。然后直接在1500行的 man 文档里搜索这个选项名称就可以了。

默认,tmux 的「automatic-rename」选项的全局值为「on」,也就是根据正在前台运行的命令自动设置。一旦窗口获得了一个用户或者程序指定的标题,不管是创建窗口时指定的,还是后来通过「rename-window」改的,又或者是通过终端转义序列改的,窗口局部的「automatic-rename」值就会被设置为「off」,也就是不会再自动变化了。所以,想要恢复 tmux 窗口的这个行为,只要把这个选项再次打开即可:

tmux setw automatic-rename on

或者,取消设置此窗口的局部值,这样 tmux 会使用全局值:

tmux setw -u automatic-rename

终于又解决了一个困扰已久的小麻烦~话说,直接去源码里寻找,远比在比 wget 手册还要长的 manpage 里乱逛要高效呢=w=

Jan 6

swapview 起源于我很早之前看到的一个 shell 脚本。当时正在学习 Haskell,所以就拿 Haskell 给实现了一遍。为了对比,又拿 Python 给实现了一遍。而如今,我又在学习另一门新的语言——Rust,也拿 swapview 来练习了。相比仅仅输出字符串的「Hello World」程序,swapview 无疑更实际一些:

  • 文件系统操作:包括列目录、读取文件内容
  • 数据解析:包括简单的字符串处理和解析,还有格式化输出
  • 数据处理:求和啊排序什么的
  • 流程控制:循环啊判断啊分支什么的都有
  • 错误处理:要忽略文件读取错误的

因此,swapview 成为了依云版的「Hello World」:-)

感谢所有给 swapview 提交代码的朋友们

本文只是借 swapview 这个程序,一窥众编程语言的某些特征。很显然,编程语言们各有所长,在不同的任务下会有不同的表现。而且 swapview 各个版本出自不同的人之手,代码质量也会有所差异。

闪耀!那些令人眼前一亮的语言们

从运行效率上来看,C 如预期的一样是最快的。但令人惊讶的是,由我这个 Rust 初学者写的 Rust 程序竟然紧随其后,超越了 C++。

而原以为会跟在 Rust 之后的 C++,却输给了作为脚本语言存在的 Lua 语言的高效实现 LuaJIT(与 Rust 版本相当)。而且非 JIT 版本的 Lua 5.1 和 5.2 也都挺快的。Lua 这语言自带的功能非常少,语法也简单,但是效率确实高,让人又爱又恨的。

失望!那些没预期中的高效的语言们

没想到 Python 2 也挺快的,很接近 Go 了。PyPy 大概是因为启动比较慢的原因而排在了后面。Python 3 有使用两个版本的代码,Python3_bytes 把文件读取改为使用 bytes,仅在需要的时候才解码成 str。仅此之差,运行速度快了10%。可见 Python 的 Unicode 处理十分耗时,难怪 Python 3 在各种测试中都比 Python 2 要慢上一截。至于 PyPy3,怎么跑到那么靠后的地方去了呢……

Go 很快。至少比 Python 快。但也仅此而已了,不仅比 C++ 慢,甚至连 Lua(非 JIT 版)都不如。Go 语言版本虽然不是我写的,但我看过代码,感觉很原始。至少比 Lua 原始。看起来 Go 只不过是带接口和并发支持的 C 而已。而且,作为静态类型的编译型语言,却我却有一种很不放心的感觉。大约是因为我改动时发现传给 fmt.Printf 的参数类型和数目错了都不会得到警告或者错误的原因。而且我从来没见过 Go 编译时出现警告,对于还没入门的初学者写的、改过的程序,这样子不科学啊。早期我倒是见过 Go 报错了,但那只不过是编译器还不完善的表现而已。

传闻 NodeJS 很快。但至少它在 swapview 这种脚本中没能体现出来。正常版本比 Python 3 还要慢一点。而使用异步啊并行什么的版本还要慢上差不多三分之一,不知道怎么搞的。

编译型的 Chicken、OCaml、Haskell 都排在了一众脚本语言后边,虽然很可能是对语言本身不熟导致写出来的程序比较慢,但还是挺令人失望的。经过高手优化的 Haskell2 版本效率接近于 Python 3,但也到此为止了(因为不想使用 cabal 安装依赖,所以 Haskell2 没有参与这场对决)。我曾见过有人把 Haskell 代码优化到比 C 还快,但我宁愿去看汇编也不要去读那种代码……

Lisp 系(Chicken、Racket、SBCL(标注为 CommonLisp 的项)、Guile)也都挺慢的。不知道 LispWorks 之类的会不会快一大截呢。

预料之中的以及结果截图

Ruby 比 Python 略慢一点。

Java、Elixir 比较靠后。没办法,它们启动慢。也许以后我会出不考虑启动时间的版本。

以下是本文发表前的测试结果截图。其中 Erlang 版本因为有问题被信号所杀所以被扔在了最后。

测试结果截图

测试使用的是benchmark子目录中的 Rust 程序,使用cargo build --release命令即可构建。另外也可以使用 farseerfc 的 Python 脚本。

代码量

Elixir 代码量挺少的。Python、Ruby 也挺不错。Java 版本竟然跟 Haskell 一样。不管是 JavaScript 还是 CoffeeScript 都比较长,比 Java 还长。Rust 比 Python 长不少,但也比 Go 短不少。而 Go 比起 C、C++ 要短一些。最长的,除了我不了解的 Pascal,竟然还有因为程序出错还没有测试的 Erlang!如果不算按行读取的 line_server.erl 的放大,只有不到一百行,倒还不算多。

                  Elixir:   50
                   Julia:   51
           Python3_bytes:   53
                  Python:   56
                    Ruby:   56
                  Racket:   58
                    Bash:   63
                   OCaml:   65
          CommonLisp_old:   67
          CommonLisp_opt:   67
           Bash_parallel:   69
             C++14_boost:   69
                   Guile:   70
                 Haskell:   73
                 Chicken:   75
                    Java:   75
                  NodeJS:   76
                    Vala:   78
                Haskell2:   81
                       D:   86
                    Rust:   88
                   C++14:   89
                  CSharp:   91
                     Lua:   91
            NodeJS_async:   93
            CoffeeScript:   93
   CoffeeScript_parallel:   95
                     PHP:   97
           Rust_parallel:   98
                      Go:  103
                   C++11:  128
                   C++98:  141
                       C:  149
              FreePascal:  185
                  Erlang:  232

编译速度

这个比较非常粗糙,比如联网下载依赖也被算进去了。不过可以肯定,不算下载依赖部分的话,Rust 是最慢的!其次是 Haskell。标榜编译速度非常快的 Go 并不是最快的,和 C++ 不相上下(当然不知道代码复杂之后会如何了)。

0.36 C
0.60 FreePascal
0.80 OCaml
0.83 CoffeeScript_parallel
1.48 CSharp
1.67 Vala
1.68 Erlang
2.13 NodeJS_async
2.27 C++14
2.49 Go
2.53 CoffeeScript
2.90 C++11
3.01 C++98
3.23 Java
3.52 Racket
3.98 NodeJS
6.05 CommonLisp_opt
7.07 D
9.01 C++14_boost
10.41 Haskell
13.07 Rust
14.74 Chicken
15.37 Rust_parallel

结语

这个项目最初只是练习而已。后来不同语言的版本有点多,于是才演变成众编程语言的竞技。也就随意地测试了一下在给定需求下不同语言的表现而已。其实比较有意思的部分,一是使用正在学习的编程语言写作程序的新奇感、新知、新的领悟(这也是我的测试程序使用 Rust 编写的原因),二是对比不同编程语言的风格和对同样需求的处理方式。

各位读者对 swapview 有任何补充和改进,欢迎贡献代码哦~项目地址:https://github.com/lilydjwg/swapview

更新区

2015年1月9日更新:又收到了不少版本和改进,以下是最新的测试结果。很不幸地,现在已经跑得很快的 Erlang 在测试中又没反应被杀掉了。并行版的 Rust 的结果很不稳定,这次跑得好快!C++ 的除了 C++98 版的之外都到 Rust 前边去了。PHP 竟然比 LuaJIT 还要快!D 怎么到 PyPy 后边去了。

2015年1月9日的测试结果截图

2015年1月10日更新:C++ 版本继续改进,好多都超越 C 了,Rust 1.0.0alpha 的并列版本又快又稳定,Erlang 版本终于跑完了全部测试而没有出事,LLVM 版 D 快了好多。

2015年1月10日的测试结果截图

2015年1月18日更新:继续更新。又添加了若干语言,不过期待中的 Nim、Zimbu 以及传统脚本语言 Perl、Tcl 依旧缺席中。另外,正文也进行了更新,重新计算了代码量,添加了编译速度的粗略比较。

2015年1月18日的测试结果截图

Dec 24

最近又看到 Rust 的相关东西了,入门指南也写得挺不错的。这语言我越看越喜欢。

Rust 的目标是系统级编程,就像 C 那样,快速高效。同时它继承了 Haskell 的诸多特性,包括其类型系统(包括类型类和类型推断)、模式匹配。而读写起来,又和 Python 差不多简单明了。简直是把这三种语言的优点全学到了!(当然 Rust 不仅仅受到了这几种语言的影响啦。)

当然,要体验一门编程语言,最好的方式就是使用它。于是我拿它实现了我最开始用来练习 Haskell 用的 swapview 程序。

swapview 的功能是,读取/proc下每一个进程目录下边的cmdlinesmaps文件,得到其命令行和 swap 使用量,然后排序、格式化,并打印出来。

Haskell 第一版实现挺慢的:

swapview  1.27s user 0.26s system 98% cpu 1.555 total

我随手写了个 Python 版,效率翻了一倍还要多!很令人惊讶的呢。作为解释执行、还一直被认为很慢的 Python 竟然在没有任何优化的情况下就超过了编译型的 Haskell:

swapview.py  0.35s user 0.18s system 97% cpu 0.548 total

后来在 IRC 上遇到一位懂行的人,用了不少手段优化,最终得到了 Haskell 第二版:

swapview2  0.42s user 0.15s system 98% cpu 0.583 total

比 Python 版略慢。

才学 Rust 没几天,我对 Rust 比对 Haskell 更不熟。花了不少时间查阅文档、调整代码。不过因为之前的 Haskell 基础,也没遇到太大的困难。结果如下:

swapview  1.84s user 0.15s system 97% cpu 2.038 total

呃呃呃,怎么比 Haskell 版本还要慢上不少啊?

本来是找 profiling 方法的。翻着 rustc 的 man 文档,看到了-O选项,眼前一亮——我忘记告诉编译器要优化了!这是启用优化的结果,比 Python 版又快了一倍:

swapview  0.10s user 0.13s system 96% cpu 0.237 total

真棒呢~

不过很遗憾的是,它的格式化函数的第一个参数必须是字面量,连常量都不行。因为那是个宏,要在编译期解析格式……另外似乎也不支持现在连 JavaScript 都已经支持了的 generator(只支持 iterator,得先写一个 struct 才能用)。

PS: Rust 的文档挺赞的,和 Python 的一样有 JavaScript 实现的搜索功能,比起 Nimrod 和 Zimbu 的好用太多了。

PPS: 谁有兴趣可以贡献个 Go 版、C 版、C++ 版、LuaJIT 版什么的=w=


2014年12月25日更新:目前的结果是(运行时间):Rust < LuaJIT < C++14 (gcc 4.9.2) < Lua 5.1 / 5.2 << Python 3 < Haskell <<< OCaml < SBCL。手动测试的。有空我再写个好点的自动测试程序。

2015年1月6日更新:添加了更多的编程语言,以及更准确的运行时间测试,请见新文章编程语言对决——战场:swapview

Dec 19

昨天有朋友说支付宝官网 https://www.alipay.com/ 访问时浏览器报证书错误了。我试了一下,访问正常啊,不过 Certificate Patrol 告诉我支付宝更换 HTTPS 证书了,因为旧的证书要过期了。同时,证书的颁发者也换了,新证书是賽门铁克颁发的。后来我又使用 wget 访问了一下,竟然真的报错了!随即我更换了 Google Chrome 和另一个火狐配置,前者没有报错,后者却也报错了,报错信息是「该证书因为未提供证书颁发链而不被信任」(代码:sec_error_unknown_issuer):

支付宝证书报错截图

后来又使用 Android 上的 Opera Mobile 和火狐访问,也是报错。

很奇怪,大体上是经常使用的浏览器不会报错,而那些很少使用的浏览器、不记录访问数据的工具都报错了。

昨晚一直没想明白,今天早上却突然想到了:是不是支付宝配置证书时没加入中间证书、或者加错了啊?(疲劳工作什么的果然会严重效率啊喵)

支付宝现在已经能够正常访问了,而我昨天也没有进行抓包,所以没有办法证明(或者证伪)这个猜测是否正确了。不过我却可以实验一下这样的配置会导致客户端如何反应。

准备工作

域名一枚。SSL 私钥一枚、证书一枚、中间证书一枚,如果你的 SSL 证书直接由浏览器信任的根证书机构颁发的就没办法实验这个了哦。浏览器若干。哦当然还要 nginx 一枚,至少要支持 SSL。

为了便于各位重现,这里使用了两个子域名,使用不同的配置。如果你访问的时候浏览器说找不到服务器的话,那是 DNS 那边还没更新啦,今天晚些时候再试试看 :-)

第一幕:不提供中间证书

用户使用firefox -no-remote -P命令创建了一个全新的火狐配置实例。

用户:浏览器,我要访问 https://brokenchain.lilydjwg.me/

浏览器:Hi,服务器,我要以 SSL 协议访问 brokenchain.lilydjwg.me

服务器:浏览器你好,这是我的 SSL 证书。

浏览器:看看。由 AlphaSSL 颁发的证书?AlphaSSL 是谁呀??

没有中间证书

浏览器:(对用户)啊咧,服务器给出了一个咱不认识的 SSL 证书,咱不能确认其真实性。有可能是服务器配置有问题,也有可能是你被中间人攻击啦,要小心了哦!(与昨支付宝的证书错误如出一辙。)

第二幕:使用正确配置的证书

运维人员把相应的 AlphaSSL 证书附加到了网站证书文件的后面,并且重新加载了配置。

用户:浏览器,我要访问 https://goodchain.lilydjwg.me/

浏览器:Hi,服务器,我要以 SSL 协议访问 goodchain.lilydjwg.me。

服务器:浏览器你好,这是我的 SSL 证书。

浏览器:看看。是由 AlphaSSL 颁发的证书,不知道它是谁。不过对方给出了 AlphaSSL 的证书,它又是由 GlobalSign 颁发的。这货我认识,是我信任的机构。开始检查签名……签名无误。GlobalSign 确实信任 AlphaSSL,而我信任 GlobalSign。所以我信任这个证书。

有中间证书

浏览器:(对用户)一切正常!以安全的方式收到了你要访问的内容了喵~

第三幕:不提供中间证书,但浏览器拥有中间证书

用户再次访问配置有问题的服务器。

用户:浏览器,我要访问 https://brokenchain.lilydjwg.me/

浏览器:Hi,服务器,我要以 SSL 协议访问 brokenchain.lilydjwg.me

服务器:浏览器你好,这是我的 SSL 证书。

浏览器:看看。由 AlphaSSL 颁发的证书?AlphaSSL 我以前遇到过,找找看……找到了!AlphaSSL 的证书,它是由 GlobalSign 颁发的。这货我认识,是我信任的机构。开始检查签名……签名无误。GlobalSign 信任 AlphaSSL,而我又信任 AlphaSSL。所以我信任这个证书。

浏览器:(对用户)一切正常!以安全的方式收到了你要访问的内容了喵~

附录

用于实验的 nginx 配置:https://goodchain.lilydjwg.me/nginx.conf

结束语

我这里,新建的火狐实例如预期的在没有获取过中间证书时报错,在获取过之后就不再报错了。而 Google Chrome 一开始就不报错,可能是它内置了该证书,也可能是它从网上的其它某个地方取得了这个证书。wget、curl 等命令行工具总是会报错,因为它们并不存储访问过的证书。

另外注意一下,使用 cat 命令连接网站证书和中间证书时,先确保证书文件最后有换行符,不然会出错的。UNIX 传统,文本的每一行以换行符结束。而微软的做法是,换行符仅仅作为两个行中间的分隔符,最后一行并不以换行符结束,所以在连接多个文件时会因为缺少换行符而出错。

Nov 29

APEC 蓝:

北京灰:

这不是月亮:

在玩过山车的空气质量:

Nov 8

前边,我已经尝试过在 Arch Linux 下安装 Funtoo在 Arch 中安装 Arch 就更简单了。为了测试,我还通过 Aufs 来将我的 Arch Linux 在 LXC 里复制一份。至于安装个 Debian 或者 Ubuntu LXC,由于有在任何 Linux 下都可以跑的 deboostrap,安装起来也十分容易。

现在难题来了:在 LXC 里安装一个 openSUSE。LXC 自带了个 openSUSE 模板,但是它需要 zypper 等。虽然说 AUR 里就有 zypper 和 libzypp(还都是 git 版本的,下载很耗时的),不过还是不怎么够呢。经过尝试,我发现需要以下包来运行这个模板:

  • Arch 里有的:augeas
  • Arch 里没的:libsolv-tools build rpm libzypp zypper

有的就直接安装啦。没有的,可以从它的软件源下载。x86_64 架构的在这里,那个 build 是 noarch 的,在这边

把它们全部下回来,拿 7z 解压能够得到 cpio 档。然后建立个目录并 cd 过去,使用以下命令解开:

$ mkdir t
$ cd t
$ for f in ../*.cpio; do cpio -id < $f; done

这样就把它们解压到目录t里边的。我没有把它们解压到/,因为我不想弄乱我的系统,即使能够清理也是相当麻烦的,而且一不小心还可能删错文件。

所以,又该 Aufs 上场啦。当然在此之前还有件事:openSUSE 没有进行/usr合并。所以要手动去把binusr/sbinsbin等目录下的文件移动到usr/bin下,然后删掉那些目录;把usr/lib64下的文件移动到usr/lib下,并删掉usr/lib64

然后就可以将这个目录和我的 Arch Linux 合体啦:

$ mkdir root
$ sudo mount -t aufs -o br:$PWD/root=rw:$PWD/t=ro:/=ro aufs root

但是!这样子的话,新装好的 openSUSE LXC 会在这个root目录里呢。所以要把外边真实的 LXC 目录给 bind mount 过来。我使用了自定义的 LXC 路径,所以是这样子的:

$ sudo mkdir -p root/ldata/media/temp/lxc
$ sudo mount --bind /ldata/media/temp/lxc root/ldata/media/temp/lxc

然后编辑一下 openSUSE 的模板,搜索「http」把软件源的链接全部改到中国的镜像:

$ sudo vim root/usr/share/lxc/templates/lxc-opensuse

我使用的是中科大的源镜像。

一切就绪,开始安装~

$ sudo chroot root /usr/bin/lxc-create --lxcpath=/ldata/media/temp/lxc -n opensuse -t opensuse

耐心等待哦。最终安装完成根文件系统的大小是 333MiB。

安装完毕之后卸载刚刚挂载的那些东西:

$ sudo umount -R root

然后编辑一下自动生成的 LXC 配置文件,比如属改改网络什么的。以下是我改过的配置文件:

# Template used to create this container: /usr/share/lxc/templates/lxc-opensuse
# Parameters passed to the template:
# For additional config options, please look at lxc.container.conf(5)
lxc.rootfs = /ldata/media/temp/lxc/opensuse/rootfs
lxc.utsname = opensuse
lxc.autodev=1
lxc.tty = 4
lxc.pts = 1024
lxc.mount.entry = run run tmpfs rw 0 0
lxc.mount.entry = tmp tmp tmpfs rw 0 0
lxc.mount.auto = proc sys
lxc.cap.drop = sys_module mac_admin mac_override mknod sys_time
lxc.kmsg = 0

# When using LXC with apparmor, uncomment the next line to run unconfined:
#lxc.aa_profile = unconfined

#networking
lxc.network.type = veth
lxc.network.link = br0
lxc.network.flags = up
lxc.network.ipv4 = 192.168.57.6
lxc.network.name = eth0

lxc.cgroup.devices.deny = a
# /dev/null and zero
lxc.cgroup.devices.allow = c 1:3 rwm
lxc.cgroup.devices.allow = c 1:5 rwm
# consoles
lxc.cgroup.devices.allow = c 5:1 rwm
lxc.cgroup.devices.allow = c 5:0 rwm
lxc.cgroup.devices.allow = c 4:0 rwm
lxc.cgroup.devices.allow = c 4:1 rwm
# /dev/{,u}random
lxc.cgroup.devices.allow = c 1:9 rwm
lxc.cgroup.devices.allow = c 1:8 rwm
lxc.cgroup.devices.allow = c 136:* rwm
lxc.cgroup.devices.allow = c 5:2 rwm
# rtc
lxc.cgroup.devices.allow = c 254:0 rm

当然别忘记修改 root 密码啦:

$ sudo chroot /ldata/media/temp/lxc/opensuse/rootfs /bin/passwd

不过默认会有两个 getty 进程跑在 console 上边。要稍微修改一下。顺手把多余的 tty 上的也关掉好了:

$ sudo rm /ldata/media/temp/lxc/opensuse/rootfs/etc/systemd/system/getty.target.wants/getty@tty*
$ sudo mv /ldata/media/temp/lxc/opensuse/rootfs/etc/systemd/system/console-{shell,getty}.service

然后就可以启动啦:

$ sudo lxc-start -n opensuse --lxcpath=/ldata/media/temp/lxc

会默认启动 sshd,所以直接 ssh 连过去就可以用啦=w=

PS: 这个模板默认安装的是 openSUSE 12.3。记得自己改改或者装好后升级一下。

Nov 7

alembic 这个升级/降级的工具,看上去挺好的,编写好一系列版本脚本之后,能够自动地把数据库给升级或者降级到指定版本。它也使用类似 git 的一串十六进制数来表示各个版本,也支持分支,不过呢,比 git 的易用性差太远了。

我今天有个需求,给一些列添加外键。因为懒得单独新写一些脚本,所以我直接改了相关脚本,手动去数据库执行了 SQL。本以为这样子就好了,后来发现新添加外键所引用的表的创建顺序不对,应该在所有引用到它的表之前创建才对。

可是 alembic 没有 git rebase -i 命令啊,不能简单地调整各种版本的顺序。我尝试着手工编辑了一下,结果弄出来两个 head,一个 branchpoint,但是我就是没能看出来是哪里分叉了……于是想到把各个版本的关系给画出来。这种图 GraphViz 最适合了,而简单地解析 alembic history 的输出,用 awk 就好了:

#!/usr/bin/awk -f

BEGIN {
  print "digraph alembic {";
  shape = "box";
}

/^Rev:/ {
  switch($3) {
    case "(branchpoint)":
      shape = "hexagon";
      break;
    case "(head)":
      shape = "ellipse";
      break;
    default:
      shape = "box";
  }
}

/^Path:/ {
  finding_title = 1;
}

/^    \S/ && finding_title {
  sub(/^\s+|\s+$/, "");
  title = $0;
  finding_title = 0;
}

/^    Revision ID:/ {
  rev = $NF;
}

/^    Revises:/ {
  printf("  r%s -> { r%s };\n", rev, $NF);
  printf("  r%s[label=\"%s: %s\",shape=%s];\n", rev, rev, title, shape);
}

END {
  print "}";
}

head(以及第一个之前的 None 版本)会使用椭圆,分叉点(alembic 说的)会使用六边形,而其它版本是矩形的。这样就可以很方便地看出来是哪里分叉啦:

alembic history | alembic_graph | dot -Txlib

结果发现,我的数据库版本们根本就没有分叉嘛……没办法 revert 回去,把关系图导出 SVG 然后放 Inkscape 里边画边改,总算是把顺序给调整对了=w=

Oct 12

从前,恋爱像 TCP 一样,一旦建立了连接,就永远地连接上了,直到生命。而现代,恋爱像 UDP 一样,连接上一个 peer 之后,也随时可以断开连接,重新连接到另一个地址

Oct 2

因为工作上一直在用 nginx,对 nginx 配置的了解日益深入,而对 Apache httpd 配置的了解依然非常少以至于不知道如何添加一个虚拟主机的配置而不用修改大量已有配置,决定将自己本地的 Apache httpd 替换成 nginx。一开始这个 httpd 只是跑了一个 MediaWiki、一些静态文件和单独的 PHP 脚本,随着时间的流逝,后来陆续添加了 phpPgAdmin、RockMongo、PHP Xcache、into2html CGI 脚本等东西。于是配置起来似乎也不那么容易了。

首先第一点:这么多 PHP 的服务,我不想每一个 location 块里边一堆相同或者相似的 FastCGI 配置。于是把相关配置写到一个文件里去。(直到这时我才意识到 location 是可以嵌套使用的!)

index   index.php index.html;
location ~ (.+\.php\d?)($|/) {
        fastcgi_pass    unix:/run/php-fpm/php-fpm.sock;
        fastcgi_index   index.php;
        set     $script $request_filename;
        if ($request_filename ~ ^(.+\.php\d?)(/.*)$){
                set $script     $1;
                set $pathinfo   $2;
        }
        fastcgi_param   PATH_INFO       $pathinfo if_not_empty;
        fastcgi_param   SCRIPT_FILENAME $script;
        include         fastcgi_params;
}

因为是嵌套的 location,所以得在外边也写一下index,不然 nginx 会不知道的。

另一个问题是默认的 fastcgi.conf 里定义的SCRIPT_FILENAME$document_root$fastcgi_script_name。但是我的 MediaWiki 使用了alias而不是root,于是$document_root会访问错地方。网上似乎没人完全地解决或者绕过了这个问题,大概是因为他们的配置不会被包含到多个 location 里吧。后来使用$request_filename这方案是我自己读了文档之后「发明」的。

另外,nginx 自定义的变量似乎是词法作用域,不能被 include 进来的配置访问到。大概因为 FastCGI 脚本路径是自己处理的,PATH_INFO也得自己处理。

然后就可以这么用啦(这是我的 MediaWiki 配置):

location /w/ {
        alias /usr/share/webapps/mediawiki/;
        include php;
}
location /wiki {
        rewrite ^/wiki(/.*)?$ /w/index.php$1 last;
}

以前的 Apache httpd 的配置是这样的:

Alias /w /usr/share/webapps/mediawiki

RewriteEngine On
RewriteRule ^/?wiki/(.*)$ /w/index.php/$1 [PT,L,QSA]
RewriteRule ^/?wiki$ /w/index.php [PT,L,QSA]

<Directory /usr/share/webapps/mediawiki>
        Options +FollowSymLinks
        AllowOverride All
        Require all granted
</Directory>

另外一个被我写成单独的配置文件以便被 include 的限制只允许本地访问用的:

allow   127.0.0.1;
allow   ::1;
deny    all;

比 httpd 的好理解一些。

另一个问题是 CGI 脚本。nginx 是有两个方案的,fcgiwrap 或者 nginx-fcgi。前者是个二进制程序,在 Arch 和 Debian 源里都有。后者是个 Perl 脚本,已经难以下载到了(给的链接是我从互联网存档取得的)。

Arch 的 fcgiwrap 包提供了一个 systemd socket 文件,直接启动它就可以了。启动 .service 服务似乎会有问题。

$ sudo systemctl start fcgiwrap.socket

nginx 里就这么写就可以了:

fastcgi_pass    unix:/run/fcgiwrap.sock;
fastcgi_param   QUERY_STRING       $query_string;
fastcgi_param   REQUEST_METHOD     $request_method;
fastcgi_param   CONTENT_TYPE       $content_type;
fastcgi_param   CONTENT_LENGTH     $content_length;
fastcgi_param   SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param   REQUEST_URI        $request_uri;
fastcgi_param   DOCUMENT_URI       $document_uri;
fastcgi_param   DOCUMENT_ROOT      $document_root;
fastcgi_param   SERVER_PROTOCOL    $server_protocol;
fastcgi_param   GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param   SERVER_SOFTWARE    nginx;
fastcgi_param   REMOTE_ADDR        $remote_addr;
fastcgi_param   REMOTE_PORT        $remote_port;
fastcgi_param   SERVER_ADDR        $server_addr;
fastcgi_param   SERVER_PORT        $server_port;
fastcgi_param   SERVER_NAME        $server_name;
fastcgi_param   REMOTE_USER        $remote_user;

引用的时候就是这样子:

location /cgi-bin {
        root /srv/http;
        include fcgiwrap;
}

经过测试,默认参数时 php-fpm 空闲时有三个 worker,高峰时会多一点,但是 Apache httpd 平时就有八个 worker,高峰时更多。每个 worker 对内存的占用是差不多的。至于响应速度,对于 MediaWiki nginx 稍慢 httpd 一点,但是服务启动后第一次访问会很多,而且 ab 测试时失败数少不少。更重要的是,systemd-analyze blame表示 nginx + php-fpm 组合启动时间远远少于 Apache httpd(0.1 秒 vs 好几秒)!

PS: 谁能告诉我 systemd-analyze 怎么查看以前启动的时间信息呀?