Rebar是个不错的东西,特别是它的模版和配置自动生成功能,能省去大把机械操作。 但是凡是现成的东西用起来都会多多少少和你的项目性格不和——相信我,这事情屡试不爽——当然 rebar也不是个例外。
刚开始用的时候我承认有点不知所措,因为它的各种操作要么封装得太好,不满足需求的时候 甚至不知道从哪入手,要么一点都不经封装,直接用Erlang和OTP里现成的代码和配置格式。前 一种情况还好,用脚本炮制的土方法修补一下还是ok的;但是对后一种情况,貌似rebar的文档 认为这不关自己的事,应该由相应的Erlang文档说明,问题是前者里没有任何后者的pointer, 这对我这种Erlang刚入门的人来说太凶残了。
所以,我在这把这些乱七八糟的问题记一下,免得年纪大了忘掉。为什么我不给rebar来个 issue呢?或者直接提交补丁呢?因为这些东西顶多只能算额外feature,自己来加的话,等到 upstream接受补丁我们的项目可能已经结束了,这期间我没有人手和精力再给rebar维护一个 branch——好吧,我只是不想给开源软件做贡献而已。
我们的Release流程
大体上就是这样:
1 2 |
|
不过这只是生成发布包的过程,如果有上个版本的话还会生成升级包,仅此而已。可能rebar
的开发者觉得版本管理不是他们能/应该管的事情,所以做升级包的时候要我们自己提供一个
previous_release
. 然后Erlang的reltool比较笨——没错rebar用的是reltool——只会根据
*.app
和reltool.config
里指定的版本确定发布版本。即使你把程序改得自己都不认识
了,只要配置文件里的版本没改,它就是同一个release.
于是这里有两个问题:
- previous_release从哪里来;
- 不同release的版本怎么确定。
对于第一个问题,由于我们用Jenkins做发布,所以我在rebar generate
之后加了一个步
骤,在这个步骤里用shell脚本维护一个release列表,以前生成的每个release都会保存在
编译服务器上,用作后续发布的previous_release,除非有人手工删除。我们的服务没有
重要到必须保证随时能热更新的程度,所以这些保存下来的release也没有备份或者提交到
VCS(Subversion、Git……),到目前为止还算ok.
对于第二个问题,我们选择的方案是用程序代码的SVN revision,这大概是对开发者最友好
的方式(之一?)了吧……实际操作是在rebar compile
之前,用脚本将*.app
和
reltool.config
里的版本号与SVN revision同步,这样就能保证每次修改代码之后生成的
release都是独一无二的,而且来历清清楚楚。
所以最后的发布包是这样出来的:
1 2 3 4 5 6 7 8 |
|
Release包中配置文件的位置
好了,包打好了,来部署吧。
我们的服务实例可能需要不同的认证方式和数据库连接等,所以每个实例的配置都会不一
样,对我们来说最重要的是vm.args
和sys.config
这两个文件——对,我们名字都懒得
改。Rebar生成的控制脚本支持三个文件位置:
- $CWD
- $ROOT/releases/$RELEASE/
- $ROOT/etc/
$CWD
就是执行脚本的当前目录,$ROOT
是部署目录,而$RELEASE
是当前激活的release
版本。脚本执行时会按顺序检查这三个目录,如果存在vm.args
或者sys.config
就会用
上。$ROOT/releases/$RELEASE/
目录下的配置是随release分发的,方便我们在不同
release间增加或者删除配置项;但是如果里面含有实例相关的选项,每个实例再单独修改
会非常麻烦。
所以我倾向于用$CWD
,但是这也有问题:每次执行控制脚本之前都要cd到指定目录?万一
我忘了怎么办?写个wrapper?还不如直接改控制脚本。于是我就改了。vm.args
的优先级
改成这样:
- $ROOT/etc/
- $ROOT/releases/$RELEASE/
然后我们在$ROOT/etc/
里放symlink,指向真正的实例配置。
那sys.config
呢?我发现erl -config a -config b
命令中b里存在的选项会覆盖掉a里
的同名选项(虽然没有任何官方文档提到这一点),所以在$ROOT/etc/
里增加了
spec.config
,在里面指定实例相关的选项,其他通用选项则还是放在
$ROOT/releases/$RELEASE/sys.config
.
好吧,改完控制脚本之后,貌似服务启动时能找到正确的配置文件了,但是还有一个可能 没那么明显的坑:热更时的配置自动更新。
在release_handler
(OTP代码)里,执行install_release
命令时,程序会固定到
$ROOT/releases/$NEW_RELEASE/
目录下找sys.config
文件(其实可以通过环境变量等方
式更改这个路径,但是仅限于erlang虚拟机启动的时候),并把这个文件读进来,作为新的
application配置。所以,如果我们像上面那样更改了配置文件的优先顺序,下次热更新的时
候这个顺序还是会被破坏掉,导致线上的配置与spec.config
不一致;而且,决定热更新时
配置文件优先顺序的代码在OTP里,不好去改。
这里我选择了比较简单的方案:成功热更新之后,命令一个进程去主动读取spec.config
更新配置。当然这个方案并不完美,最坏的情况是,热更新完成和读取配置这两步之间有
一个竞争窗口,这个窗口里可能会有新代码带着旧配置去执行,结果不可预料——这个情况在我
们看来还是可以接受的,因为对于相对重要的配置,我们都要求通过重启服务实例来更新,热
更新时更改的配置不会造成不可挽回的后果。
动态生成配置文件
这是在后来翻rebar文档的时候发现的。比方说,在生成oceanus.app
的时候,rebar
会去找叫做oceanus.app.script
的文件,然后把里面的Erlang程序执行一遍,再将结果写
到oceanus.app
里。上面release流程里提到的“确定新release版本”这一步我们是用shell
做的,现在发现其实可以动态生成……不过话说回来,这个做法还真有够dirty的,把程序写
在配置文件里,就不怕出事么→_→
热更新
Rebar用模版生成发布目录的时候还提供了协助热更新的脚本,一般就叫做
install_upgrade.escript
,按照rebar官方文档的说法,升级包复制到$ROOT/releases
目录下后,执行rebar upgrade
命令时其实就会去调用它。问题是这个脚本里做rpc:call(...)
的时候,脚本自己是用short name的,而我们为了方便节点间通信,部署的服务实例都用了
long name,按Erlang的规定,这两种节点没办法互相通信,热更新也铁定会失败。
所以,我在rebar create-node ...
之后都会再给install_upgrade.escript
打个补丁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|