最近, 在编写一个专门压测NameNode的工具(以下简称s4nn), 它有两个难点 :
- s4nn需要可以模拟上万个DataNode ;
- s4nn 需要灵活的支持对NameNode访问行为的定义.
后者导致了本文的思考.
命令行参数和配置文件是最常用来配置系统的方法, 前者用于配置项较少, 后者则适合配置复杂情况. 这两种方式都有共同令人痛苦的地方:
- 编写代码去载入->解析->转换, 通常如同处理协议般无聊(要是有个什么变更, KMN!!);
- 对于复杂的配置文件编写而言, 总是没有顺手的编辑器支持, 写起来既累又易错.
要是用代码取代配置文件呢?
呃… 这会很麻烦吧, 像java这样的改了代码那还不要重新编译啊?
嗯, 确实, 但并没有想象中那么麻烦, 一些技巧可以让它变得简单(至少java是这样), 如Btrace.
用代码取代独立的配置文件不是新鲜的做法, 像Guice, Gant 等已经以Internal DSL的代码代替了XML. 好处很明显:
- 良好的DSL风格, 简洁易懂 ;
- 免去对配置文件的解析转换 ;
- 最好的编辑器支持, 语法高亮, 一键格式化, 提示补全, 重构 ;
- 编译器帮助查错;
- 与代码无缝结合, 能够容易在变化中保持一致性.
简言之两个字, “高效”! 这倒是挺适合s4nn的, 不妨一试!
从需求的角度出发, 配置应该能够完成:
- 定义一组Client RPC调用行为, 其调用参数, 次数;
- 定义DataNode的运行时特征, 个数.
为满足这两点, 代码设计上可能会有:
class ClientDriver {
void addRpc(times, name, args)
void setNameNode(address)
}
class DataNodeSimulator {
void setCapacity(size)
void setHeartbeatInterval(sec)
void setBlockReportInterval(sec)
void setNameNode(address)
}
那么其配置文件会是:
1 <configuration>
2 <client>
3 <rpc times="100000" name="getFileInfo">
4 <arguments>
5 <param name="src">/foo</param>
6 </arguments>
7 </rpc>
8 </client>
9 <namenode>
10 <address>host:port</address>
11 </namenode>
12 <datanode>
13 <simulator num="10000">
14 <capacity unit="GB">200</capacity>
15 <heartbeatInterval unit="SECOND">1</heartbeatInterval>
16 <blockReportInterval unit="HOUR">1</blockReportInterval>
17 </simulator>
18 </datanode>
19 </configuration>
简单吗? 简单, 那是因为这只是最基本的, 实际的配置应考虑到:
- 有30种RPC方法, 其中参数个数最多的有6, 参数类型并非都是基本类型, 参数值可能需要按照某种规则随即生成;
- DataNode模拟器也会有可能需要支持多种选项组合, 如实际集群种不是所有的机器的容量都一样的等.
这样的XML会臃肿到什么程度…
好吧, 看看用代码的效果:
1 import com.taobao.s4nn.*;
2
3 public class Main extends Bootstrap {
5
6 protected void config() {
7 setConcurrency(16);
8 setMaxRandomPathDepth(10);
9
10 /*
11 * DataNode simulator config
12 */
13 simulate(3, new DataNodeSimulatorGenerator() {
14 protected void config() {
15 setCapacity(gibibyte(1));
16 setTickPeriodSecond(1);
17 setHeartbeatInterval(1);
18 setBlockReportInterval(3);
19 setNameNode(proxyOfNameNodeAt("localhost:10001"));
20 }
21 });
22
23 /*
24 * NameNode Status pre-setting config
25 */
26 parallel(1, new StatusPresetterBuilder() {
27 protected void config() {
28 setName(client("InitalStatus"));
29 setNamenode(proxyOfNameNodeAt("localhost:10001"));
30 times(1, create(randomSrc, defaultFsPermission, overwrite(true), replication((short) 3), blocks(1)));
31 }
32 });
33
34 /*
35 * Client RPC driver config
36 */
37 parallel(10, new ClientDriverBuilder() {
38 protected void config() {
39 setName(seqNameWith("client"));
40 setNamenode(proxyOfNameNodeAt("localhost:10001"));
41
42 times(1, getBlockLocations(src("/foo"), randomOffsetIn(0, 1024), randomLengthIn(0, 2048)));
43 times(2, getListing(src("/foo")));
44 times(3, getLocatedListing(src("/foo")));
45 times(4, getStats());
46 times(5, getDatanodeReport(all));
47 times(6, getPreferriedBlockSize(src("/foo")));
48 times(7, getFileInfo(src("/foo")));
49 times(8, getContentSummary(src("/foo")));
50 times(9, setOwner(src("/foo"), username("jushi"), groupname("dwbasis")));
51 times(10, setReplication(src("/foo"), replication((short) 4)));
52 times(11, setPermission(src("/foo"), defaultFsPermission));
53 times(12, rename(src("/foo"), src("/bar")));
54 times(13, mkdirs(src("/bar"), defaultFsPermission));
55 times(14, setQuota(src("/foo"), randomNamespaceQuotaIn(1024, 2048), randomDiskspaceQuotaIn(1024, 4096)));
56 times(15, setTime(src("/foo"), currentTimeMillis, unchanged));
57 }
58 });
59 }
60 }
嗯, 既保持易读性又更为简洁, 重点是不用再写那些额外处理XML的代码了.
这里效仿了Guice中AbstractModule的DSL做法, 提供了setXxx, times等内置方法, 用Java IDE编写起来那是相当轻快啊~~
要是改用Scala或Groovy来实现, 那么还要简化, 使得代码味更少些, 而且直接运行简单到一条命令就搞定了:)