让代码取代你的配置文件吧

最近, 在编写一个专门压测NameNode的工具(以下简称s4nn), 它有两个难点 :

  1. s4nn需要可以模拟上万个DataNode ;
  2. s4nn 需要灵活的支持对NameNode访问行为的定义.

后者导致了本文的思考.

命令行参数和配置文件是最常用来配置系统的方法, 前者用于配置项较少, 后者则适合配置复杂情况. 这两种方式都有共同令人痛苦的地方:

  1. 编写代码去载入->解析->转换, 通常如同处理协议般无聊(要是有个什么变更, KMN!!);
  2. 对于复杂的配置文件编写而言, 总是没有顺手的编辑器支持, 写起来既累又易错.

要是用代码取代配置文件呢?

呃… 这会很麻烦吧, 像java这样的改了代码那还不要重新编译啊?

嗯, 确实, 但并没有想象中那么麻烦, 一些技巧可以让它变得简单(至少java是这样), 如Btrace.

用代码取代独立的配置文件不是新鲜的做法, 像GuiceGant 等已经以Internal DSL的代码代替了XML. 好处很明显:

  1. 良好的DSL风格, 简洁易懂 ;
  2. 免去对配置文件的解析转换 ;
  3. 最好的编辑器支持, 语法高亮, 一键格式化, 提示补全, 重构 ;
  4. 编译器帮助查错;
  5. 与代码无缝结合, 能够容易在变化中保持一致性.

简言之两个字, “高效”! 这倒是挺适合s4nn的, 不妨一试!

从需求的角度出发, 配置应该能够完成:

  1. 定义一组Client RPC调用行为, 其调用参数, 次数;
  2. 定义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>

简单吗? 简单, 那是因为这只是最基本的, 实际的配置应考虑到:

  1. 有30种RPC方法, 其中参数个数最多的有6,  参数类型并非都是基本类型, 参数值可能需要按照某种规则随即生成;
  2. 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来实现, 那么还要简化, 使得代码味更少些, 而且直接运行简单到一条命令就搞定了:)

 

bash下利用trap捕捉信号

我在之前的文章里写了myisam读数据压缩的情况,最近决定把它用在生产环境上,所以避免不了写一个“安全”的处理脚本放在DB服务器上,这就引入了本文所讨论的话题。

我希望这个bash脚本在退出的时候做一些事情,包括:

  1. 它启动的切到后台的job需要被杀死;
  2. 一些临时文件的清理。

在这个脚本里我用到了trap这个命令,关于它,你可以man一下,我这里就不啰嗦了。直接上示例代码:

$ cat test_trap.sh

declare -i run_terminate=0

trap "run_terminate=1" SIGINT SIGTERM

# 启动io监控,IO较大时不进行压缩
vmstat 1 &gt;&gt; ./a.log &amp;

while [ ${run_terminate} -eq 0 ]
do
    # 核心代码
    sleep 30
done

for pid in $(ps -ef | awk -v p=${$} '{if ($3 == p){print $2}}')
do
    kill -9 ${pid} &gt; /dev/null
done

rm -f ./a.log
echo "Terminated."

在上面的代码中,我们捕捉INT信号(CTRL+C)和TERM信号(kill产生)。运行程序:

$ /bin/bash test_trap.sh

^C

按照我们预期的,当我CTRL+C退出程序,或者kill进程时,上面的脚本应该停掉vmstat进程,并且删除a.log,输出“Terminated”后退出。

但是,CTRL+C确实按照我们的设想进行了。可kill之后程序并没有任何反应,这是为何?

Google了一遍,天下文章一大抄,相似的例子,却没有人抛出这个问题。思索了半天,幡然醒悟:

  1. kill(killall)命令只是发出一个TERM信号(15),至于这个信号是否被对应进程捕捉并处理了,它并不管;
  2. kill发出的时候,test_trap.sh正在sleep——这个时间有点长(30秒)。

也就是说,等它“睡醒”了,它自然会处理TERM信号的。不信,你多等一会。

 

mysql数据压缩性能对比(二)

在上一篇文章中,我们用生产环境的真实数据与真实SQL测试了archive和myisampack两种方式下的性能对比情况。我们得到一个对这个测试case有效的结论,那就是在240万数据量的情况下,采用archive引擎将使得某些查询慢得无法忍受!

那么,原因是什么呢?

我们知道,mysql提供archive这种存储引擎是为了降低磁盘开销,但还有一个前提,那就是被归档的数据不需要或者很少被在线查询,偶尔的查询慢一些也是没关系的。鉴于上述原因,archive表是不允许建立自增列之外的索引的。

有了这个共识,我们拿一条测试SQL来分析一下不用索引前后的查询性能差别为什么这么大。在我们的测试SQL中有这么一条:

SELECT c1,c2,...,cn FROM  mysqlslap.rpt_topranks_v3
WHERE ... AND partition_by1 = '50008090'
ORDER BY added_quantity3 DESC
LIMIT 500

我们前边说过,测试的这个表在partition_by1这个字段上建立了索引,那么,我们初步判断在基准表和myisampack表上,这个查询应该用到了partition_by1的索引;EXPLAIN一下:

mysql&gt; EXPLAIN
    -&gt; SELECT ... FROM  mysqlslap.rpt_topranks_v3
    -&gt; WHERE ... AND partition_by1 = '50008090'
    -&gt; ORDER BY added_quantity3 DESC
    -&gt; LIMIT 500\G
*************************** 1. ROW ***************************
           id: 1
  select_type: SIMPLE
        TABLE: rpt_topranks_v3
         TYPE: REF
possible_keys: idx_toprank_pid,idx_toprank_chg
          KEY: idx_toprank_pid
      key_len: 99
          REF: const
         ROWS: 2477
        Extra: USING WHERE; USING filesort
1 ROW IN SET (0.00 sec)

正如我们所料,这个查询用到了建立在partition_by1这个字段上的索引,匹配的目标行数为2477,然后还有一个在added_quantity3字段上的排序。由于added_quantity3没有索引,所以用到了filesort。

我们再看一下这条SQL在归档表上的EXPLAIN结果:

mysql&gt; EXPLAIN
    -&gt; SELECT ... FROM  mysqlslap.rpt_topranks_v3_<strong>archive</strong>
    -&gt; WHERE ... AND partition_by1 = '50008090'
    -&gt; ORDER BY added_quantity3 DESC
    -&gt; LIMIT 500\G
*************************** 1. ROW ***************************
           id: 1
  select_type: SIMPLE
        TABLE: rpt_topranks_v3_archive
         TYPE: ALL
possible_keys: NULL
          KEY: NULL
      key_len: NULL
          REF: NULL
         ROWS: 2424753
        Extra: USING WHERE; USING filesort
1 ROW IN SET (0.00 sec)

EXPLAIN说:“我没有索引可用,所以只能全表扫描2424753行记录,然后再来个filesort。”你要追求性能,那显然是委屈MySQL了。

扩展阅读:

关于MYSQL的ORDER BY实现原理,请参考 http://isky000.com/database/mysql_order_by_implement

 

mysql的数据压缩性能对比(一)

数据魔方需要的数据,一旦写入就很少或者根本不会更新。这种数据非常适合压缩以降低磁盘占用。MySQL本身提供了两种压缩方式——archive引擎以及针对MyISAM引擎的myisampack方式。今天对这两种方式分别进行了测试,对比了二者在磁盘占用以及查询性能方面各自的优劣。至于为什么做这个,你们应该懂的,我后文还会介绍。且看正文:

1. 测试环境:

  1. 软硬件
  2. 一台 64位 2.6.18-92 内核Linux开发机,4G内存,4个2800Mhz Dual-Core AMD Opteron(tm) Processor 2220 CPU。

    MySQL放在一块7200转SAT硬盘,未做raid;

    MySQL未做任何优化,关闭了query cache,目的在于避免query cache对测试结果造成干扰。

  3. 表结构
  4. 2424753条记录,生产环境某一个分片的实际数据;

    分别建立了(partition_by1,idx_rank) 和 (partition_by1,chg_idx)的联合索引,其中partition_by1为32长度的varchar类型,用于检索;其余两个字段均为浮点数,多用于排序;

    autokid作为子增列,充当PRIMARY KEY,仅作为数据装载时原子性保证用,无实际意义。

2. 测试目的:

  1. 压缩空间对比
  2. 压缩率越大,占用的磁盘空间越小,直接降低数据的存储成本;

  3. 查询性能对比
  4. 压缩后查询性能不应该有显著降低。Archive是不支持索引的,因此性能降低是必然的,那么我们也应该心里有个谱,到底降低了多少,能不能接受。

3. 测试工具:

  1. mysqlslap
  2. 官方的工具当然是不二之选。关于mysqlslap的介绍请参考 官方文档

  3. 测试query
  4. 截取生产环境访问topranks_v3表的实际SQL共9973条,从中抽取访问量较大的7条,并发50,重复执行10次。命令如下:

    ./mysqlslap --defaults-file=../etc/my.cnf -u**** -p**** -c50 -i10 -q ../t.sql --debug-info

4.测试结论

比较项 磁盘空间 耗时(秒) CPU Idle LOAD 并发
基准表(MyISAM) 403956004 2.308 30 15 50
ARCHIVE 75630745 >300 75 4 1
PACK 99302109 2.596 30 22 50

根据上面的表格给出的测试数据,我们简单得出以下结论:

  1. 针对测试表,Archive表占用空间约为之前的18.7%,myisampack后空间占用约为之前的24.6%;二者相差不多,单纯从空间利用情况来看,我们似乎需要选择archive表;
  2. 我们再看查询性能,与基准表进行对比。无论在总耗时还是系统负载方面,50并发下的pack表查询性能与基准表相当;而archive表在单并发情况下耗时超过了5分钟(实在等不了了,kill之)!

那么,我们似乎可以得出结论,针对需要在线查询的表,ARCHIVE引擎基本上可以不考虑了。

在下一篇文章中,我将会详细为什么这个测试过程中ARCHIVE引擎如此地慢。

 

nginx+resin 使用中文域名解决方案。

xxx中文域名.中国

这样的域名在resin下会出错:

[15:34:31.628] {hmux-127.0.0.1:6801-5} java.lang.StringIndexOutOfBoundsException: String index out of range: 9[15:34:31.628] {hmux-127.0.0.1:6801-5}  at java.lang.String.charAt(String.java:687)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at com.caucho.server.host.DomainName.decode(DomainName.java:205)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at com.caucho.server.host.DomainName.fromAscii(DomainName.java:86)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at com.caucho.server.host.HostContainer.buildInvocation(HostContainer.java:305)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at com.caucho.server.cluster.Server.buildInvocation(Server.java:915)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at com.caucho.server.dispatch.DispatchServer.buildInvocation(DispatchServer.java:209)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at com.caucho.server.hmux.HmuxRequest.handleRequest(HmuxRequest.java:427)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at com.caucho.server.port.TcpConnection.run(TcpConnection.java:603)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at com.caucho.util.ThreadPool$Item.runTasks(ThreadPool.java:721)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at com.caucho.util.ThreadPool$Item.run(ThreadPool.java:643)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at java.lang.Thread.run(Thread.java:619)[15:34:31.628] {hmux-127.0.0.1:6801-5} java.lang.RuntimeException: java.lang.StringIndexOutOfBoundsException: String index out of range: 9[15:34:31.628] {hmux-127.0.0.1:6801-5}  at com.caucho.server.host.DomainName.fromAscii(DomainName.java:109)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at com.caucho.server.host.HostContainer.buildInvocation(HostContainer.java:305)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at com.caucho.server.cluster.Server.buildInvocation(Server.java:915)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at com.caucho.server.dispatch.DispatchServer.buildInvocation(DispatchServer.java:209)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at com.caucho.server.hmux.HmuxRequest.handleRequest(HmuxRequest.java:427)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at com.caucho.server.port.TcpConnection.run(TcpConnection.java:603)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at com.caucho.util.ThreadPool$Item.runTasks(ThreadPool.java:721)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at com.caucho.util.ThreadPool$Item.run(ThreadPool.java:643)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at java.lang.Thread.run(Thread.java:619)[15:34:31.628] {hmux-127.0.0.1:6801-5} Caused by: java.lang.StringIndexOutOfBoundsException: String index out of range: 9[15:34:31.628] {hmux-127.0.0.1:6801-5}  at java.lang.String.charAt(String.java:687)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at com.caucho.server.host.DomainName.decode(DomainName.java:205)[15:34:31.628] {hmux-127.0.0.1:6801-5}  at com.caucho.server.host.DomainName.fromAscii(DomainName.java:86)[15:34:31.628] {hmux-127.0.0.1:6801-5}  … 8 more

 

 

原因是中文域名做了“转码”,不知道是什么鬼东西。反正就出错了。要解决。

分析一下中文域名转码后是怎样的?

例如 中文域名.中国   转码后   xn--wlqpfy8in9c9zd3hc50a.xn--io0a7i

原来这样:当resin接收 到host时,用正常的域名去解释这个地址,由于这个地址不符合正规的域名格式,所以出错。

解决思路,能不能在nginx转发时,改变host呢?查一查nginx说明文档,有一条这样写:

proxy_set_header Host $host;

应该就是这条了。将后面的$host改为 一个英文域名

proxy_set_header Host www.youdomain.com;

 

问题解决了。哈。。。大家加油。

 

 

用Drupal 创建的8种网站

Drupal没有像WordPress那样普及,或许是因为它稍有难度来学习。但是如果你创建大的网站,使用Drupal,高度灵活的开源CMS,可以得到你想要的更漂亮、更强大的各种类型的网站。

下面是8种类型的网站,使用Drupal来创建,希望帮你对Drupal有更深刻的认识。

1. 文件存储分享站点

使用Drupal创建文件分享,你可以使用 CCK 和 Views ,也包括一些模块,比如Media MoverFilebrowser 或者Web File Manager。看看Box.net,你会非常感兴趣的^_^。

2. 社交网站

在社交网络能力方面,Drupal可能是最好的CMS。Drupal提供了强大的用户管理和权限管理系统。但是如果你想创建强大的社交网站,就需要一些模块,见http://drupal.org/node/206724

你想看一些案例?Imbee 或者 GoingOn

3. Twitter Clone

建议你不要尝试利用Drupal创建Twitter竞争产品,但是,如果你想整合Twitter功能到你的站点,Drupal的微博模块可以帮到你。

4. 新闻News portal

如果你想创建新闻站点或杂志站点,Drupal的完美的选择。使用CCK 和 Views ,你可以创建所有的发布内容类型,并且可灵活列表。这样的新闻站点非常之多,比如New York Observer

5. 博客网络

用Drupal创建博客网站,很轻松,甚至无需额外模块。看看 Wisebread吧。

6. 视频分享站点

这类站点太耗带宽了,如果你决定创建,那么Drupal来帮你实现吧。FlashVideo 模块提供了创建Youtube克隆的强大能力,它整合了CCK,转换视频到FLV,并有分享代码。另外你也可以尝试Media Mover 和 SWF ToolsMTV UK 站点就是Drupal创建的。

7. 图片分享站点

Image module ,这个模块将派上用场,可让你创建类Flickr站点,很好很强大。MyFinePix 就是Drupal创建的照片分享站点。

8. 类Digg-like news site

感谢 Drigg module, 这个模块可帮助你快速建立Digg克隆站点。流行的设计社交新闻网Designbump在使用Drupal。

英文原文:http://www.designer-daily.com/8-types-of-sites-you-can-build-with-drupal-13924

Nginx源码分析-内存池

Nginx的内存池实现得很精巧,代码也很简洁。总的来说,所有的内存池基本都一个宗旨:申请大块内存,避免“细水长流”。

一、创建一个内存池

nginx内存池主要有下面两个结构来维护,他们分别维护了内存池的头部和数据部。此处数据部就是供用户分配小块内存的地方。

//该结构用来维护内存池的数据块,供用户分配之用。
typedef struct {
    u_char               *last; 	//当前内存分配结束位置,即下一段可分配内存的起始位置
    u_char               *end;  	//内存池结束位置
    ngx_pool_t           *next; 	//链接到下一个内存池
    ngx_uint_t            failed; 	//统计该内存池不能满足分配请求的次数
} ngx_pool_data_t;
//该结构维护整个内存池的头部信息。
struct ngx_pool_s {
    ngx_pool_data_t       d; 		//数据块
    size_t                max;		//数据块的大小,即小块内存的最大值
    ngx_pool_t           *current;	//保存当前内存池
    ngx_chain_t          *chain;	//可以挂一个chain结构
    ngx_pool_large_t     *large;	//分配大块内存用,即超过max的内存请求
    ngx_pool_cleanup_t   *cleanup;	//挂载一些内存池释放的时候,同时释放的资源。
    ngx_log_t            *log;
};

有了上面的两个结构,就可以创建一个内存池了,nginx用来创建一个内存池的接口是:ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log)(位于src/core/ngx_palloc.c中);调用这个函数就可以创建一个大小为size的内存池了。这里,我用内存池的结构图来展示,就不做具体的代码分析了。

ngx_create_pool接口函数就是分配上图这样的一大块内存,然后初始化好各个头部字段(上图中的彩色部分)。红色表示的四个字段就是来自于上述的第一个结构,维护数据部分,由图可知:last是用户从内存池分配新内存的开始位置,end是这块内存池的结束位置,所有分配的内存都不能超过end。蓝色表示的max字段的值等于整个数据部分的长度,用户请求的内存大于max时,就认为用户请求的是一个大内存,此时需要在紫色表示的large字段下面单独分配;用户请求的内存不大于max的话,就是小内存申请,直接在数据部分分配,此时将会移动last指针。

二、分配小块内存(size <= max)

上面创建好了一个可用的内存池了,也提到了小块内存的分配问题。nginx提供给用户使用的内存分配接口有:
void *ngx_palloc(ngx_pool_t *pool, size_t size);
void *ngx_pnalloc(ngx_pool_t *pool, size_t size);
void *ngx_pcalloc(ngx_pool_t *pool, size_t size);
void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment);

ngx_palloc和ngx_pnalloc都是从内存池里分配size大小内存,至于分得的是小块内存还是大块内存,将取决于size的大小;他们的不同之处在于,palloc取得的内存是对齐的,pnalloc则否。ngx_pcalloc是直接调用palloc分配好内存,然后进行一次0初始化操作。ngx_pmemalign将在分配size大小的内存并按alignment对齐,然后挂到large字段下,当做大块内存处理。下面用图形展示一下分配小块内存的模型:

上图这个内存池模型是由上3个小内存池构成的,由于第一个内存池上剩余的内存不够分配了,于是就创建了第二个新的内存池,第三个内存池是由于前面两个内存池的剩余部分都不够分配,所以创建了第三个内存池来满足用户的需求。由图可见:所有的小内存池是由一个单向链表维护在一起的。这里还有两个字段需要关注,failed和current字段。failed表示的是当前这个内存池的剩余可用内存不能满足用户分配请求的次数,即是说:一个分配请求到来后,在这个内存池上分配不到想要的内存,那么就failed就会增加1;这个分配请求将会递交给下一个内存池去处理,如果下一个内存池也不能满足,那么它的failed也会加1,然后将请求继续往下传递,直到满足请求为止(如果没有现成的内存池来满足,会再创建一个新的内存池)。current字段会随着failed的增加而发生改变,如果current指向的内存池的failed达到了4的话,current就指向下一个内存池了。猜测:4这个值应该是作者的经验值,或者是一个统计值。

三、大块内存的分配(size > max)

大块内存的分配请求不会直接在内存池上分配内存来满足,而是直接向操作系统申请这么一块内存(就像直接使用malloc分配内存一样),然后将这块内存挂到内存池头部的large字段下。内存池的作用在于解决小块内存池的频繁申请问题,对于这种大块内存,是可以忍受直接申请的。同样,用图形展示大块内存申请模型:

注意每块大内存都对应有一个头部结构(next&alloc),这个头部结构是用来将所有大内存串成一个链表用的。这个头部结构不是直接向操作系统申请的,而是当做小块内存(头部结构没几个字节)直接在内存池里申请的。这样的大块内存在使用完后,可能需要第一时间释放,节省内存空间,因此nginx提供了接口函数:ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);此函数专门用来释放某个内存池上的某个大块内存,p就是大内存的地址。ngx_pfree只会释放大内存,不会释放其对应的头部结构,毕竟头部结构是当做小内存在内存池里申请的;遗留下来的头部结构会作下一次申请大内存之用。

四、cleanup资源


可以看到所有挂载在内存池上的资源将形成一个循环链表,一路走来,发现链表这种看似简单的数据结构却被频繁使用。由图可知,每个需要清理的资源都对应有一个头部结构,这个结构中有一个关键的字段handler,handler是一个函数指针,在挂载一个资源到内存池上的时候,同时也会注册一个清理资源的函数到这个handler上。即是说,内存池在清理cleanup的时候,就是调用这个handler来清理对应的资源。比如:我们可以将一个开打的文件描述符作为资源挂载到内存池上,同时提供一个关闭文件描述的函数注册到handler上,那么内存池在释放的时候,就会调用我们提供的关闭文件函数来处理文件描述符资源了。

五、内存的释放

nginx只提供给了用户申请内存的接口,却没有释放内存的接口,那么nginx是如何完成内存释放的呢?总不能一直申请,用不释放啊。针对这个问题,nginx利用了web server应用的特殊场景来完成;一个web server总是不停的接受connection和request,所以nginx就将内存池分了不同的等级,有进程级的内存池、connection级的内存池、request级的内存池。也就是说,创建好一个worker进程的时候,同时为这个worker进程创建一个内存池,待有新的连接到来后,就在worker进程的内存池上为该连接创建起一个内存池;连接上到来一个request后,又在连接的内存池上为request创建起一个内存池。这样,在request被处理完后,就会释放request的整个内存池,连接断开后,就会释放连接的内存池。因而,就保证了内存有分配也有释放。

总结:通过内存的分配和释放可以看出,nginx只是将小块内存的申请聚集到一起申请,然后一起释放。避免了频繁申请小内存,降低内存碎片的产生等问题

 

HFile存储格式

HBase中的所有数据文件都存储在Hadoop HDFS文件系统上,主要包括两种文件类型:

1. HFile, HBase中KeyValue数据的存储格式,HFile是Hadoop的二进制格式文件,实际上StoreFile就是对HFile做了轻量级包装,即StoreFile底层就是HFile

2. HLog File,HBase中WAL(Write Ahead Log) 的存储格式,物理上是Hadoop的Sequence File

下面主要通过代码理解一下HFile的存储格式。

HFile

下图是HFile的存储格式:

HFile由6部分组成的,其中数据KeyValue保存在Block 0 … N中,其他部分的功能有:确定Block Index的起始位置;确定某个key所在的Block位置(如block index);判断一个key是否在这个HFile中(如Meta Block保存了Bloom Filter信息)。具体代码是在HFile.java中实现的,HFile内容是按照从上到下的顺序写入的(Data Block、Meta Block、File Info、Data Block Index、Meta Block Index、Fixed File Trailer)。

KeyValue: HFile里面的每个KeyValue对就是一个简单的byte数组。但是这个byte数组里面包含了很多项,并且有固定的结构。我们来看看里面的具体结构:

开始是两个固定长度的数值,分别表示Key的长度和Value的长度。紧接着是Key,开始是固定长度的数值,表示RowKey的长度,紧接着是 RowKey,然后是固定长度的数值,表示Family的长度,然后是Family,接着是Qualifier,然后是两个固定长度的数值,表示Time Stamp和Key Type(Put/Delete)。Value部分没有这么复杂的结构,就是纯粹的二进制数据了。

Data Block:由DATABLOCKMAGIC和若干个record组成,其中record就是一个KeyValue(key length, value length, key, value),默认大小是64k,小的数据块有利于随机读操作,而大的数据块则有利于scan操作,这是因为读KeyValue的时候,HBase会将查询到的data block全部读到Lru Block Cache中去,而不是仅仅将这个record读到cache中去。

private void append(final byte [] key, final int koffset, final int klength, final byte [] value, final int voffset, final int vlength) throws IOException {

this.out.writeInt(klength);

this.keylength += klength;

this.out.writeInt(vlength);

this.valuelength += vlength;

this.out.write(key, koffset, klength);

this.out.write(value, voffset, vlength);

}

Meta Block:由METABLOCKMAGIC和Bloom Filter信息组成。

public void close() throws IOException {

if (metaNames.size() > 0) {

for (int i = 0 ; i < metaNames.size() ; ++ i ) {

dos.write(METABLOCKMAGIC);

metaData.get(i).write(dos);

}

}

}

File Info: 由MapSize和若干个key/value,这里保存的是HFile的一些基本信息,如hfile.LASTKEY, hfile.AVG_KEY_LEN, hfile.AVG_VALUE_LEN, hfile.COMPARATOR。

private long writeFileInfo(FSDataOutputStream o) throws IOException {

if (this.lastKeyBuffer != null) {

// Make a copy.  The copy is stuffed into HMapWritable.  Needs a clean

// byte buffer.  Won’t take a tuple.

byte [] b = new byte[this.lastKeyLength];

System.arraycopy(this.lastKeyBuffer, this.lastKeyOffset, b, 0, this.lastKeyLength);

appendFileInfo(this.fileinfo, FileInfo.LASTKEY, b, false);

}

int avgKeyLen = this.entryCount == 0? 0: (int)(this.keylength/this.entryCount);

appendFileInfo(this.fileinfo, FileInfo.AVG_KEY_LEN, Bytes.toBytes(avgKeyLen), false);

int avgValueLen = this.entryCount == 0? 0: (int)(this.valuelength/this.entryCount);

appendFileInfo(this.fileinfo, FileInfo.AVG_VALUE_LEN,

Bytes.toBytes(avgValueLen), false);

appendFileInfo(this.fileinfo, FileInfo.COMPARATOR, Bytes.toBytes(this.comparator.getClass().getName()), false);

long pos = o.getPos();

this.fileinfo.write(o);

return pos;

}

Data/Meta Block Index: 由INDEXBLOCKMAGIC和若干个record组成,而每一个record由3个部分组成 — block的起始位置,block的大小,block中的第一个key。

static long writeIndex(final FSDataOutputStream o, final List<byte []> keys, final List<Long> offsets, final List<Integer> sizes) throws IOException {

long pos = o.getPos();

// Don’t write an index if nothing in the index.

if (keys.size() > 0) {

o.write(INDEXBLOCKMAGIC);

// Write the index.

for (int i = 0; i < keys.size(); ++i) {

o.writeLong(offsets.get(i).longValue());

o.writeInt(sizes.get(i).intValue());

byte [] key = keys.get(i);

Bytes.writeByteArray(o, key);

}

}

return pos;

}

Fixed file trailer: 大小固定,主要是可以根据它查找到File Info, Block Index的起始位置。

public void close() throws IOException {

trailer.fileinfoOffset = writeFileInfo(this.outputStream);

trailer.dataIndexOffset = BlockIndex.writeIndex(this.outputStream,

this.blockKeys, this.blockOffsets, this.blockDataSizes);

if (metaNames.size() > 0) {

trailer.metaIndexOffset = BlockIndex.writeIndex(this.outputStream,

this.metaNames, metaOffsets, metaDataSizes);

}

trailer.dataIndexCount = blockKeys.size();

trailer.metaIndexCount = metaNames.size();

trailer.totalUncompressedBytes = totalBytes;

trailer.entryCount = entryCount;

trailer.compressionCodec = this.compressAlgo.ordinal();

trailer.serialize(outputStream);

}

注:上面的代码剪切自HFile.java中的代码,更多信息可以查看Hbase源代码。

参考:http://www.searchtb.com/2011/01/understanding-hbase.html

http://th30z.blogspot.com/2011/02/hbase-io-hfile.html

 

支持配额的共享线程池

用了几个小时动手实现了一个简陋支持配额的共享线程池. 基本思路与放翁相同, 区别在于引入了两种线程分配策略:

悲观策略

简单的共享一个线程池, 最容易出现的问题就是不同类型任务(或事件)在随机争抢线程资源时, 可能出现”饿死”现象(即抢不到线程).

因此, 悲观策略的宗旨是绝对的保证每种任务都会被分配到预留的(reserve)配额, 这种做法本质上和多个线程池的做法一样. 如总共100个线程, A任务可用50个线程, B任务可用30个线程, C任务可用20个, 三者互不占用, 一旦任意谁的任务实例超过配额, 将被迫等待直至先前的任务实例结束释放了线程.

统一到一个共享的池中, 好处自然是归一化管理, 容易从全局上比较不同任务的优先级, 做出合理的资源分配; 坏处可能就是需要去实现这样一个支持配额的共享线程池. 当然, 若不觉得多个线程池有什么不好, 悲观策略其实意义不大:(.

乐观策略

无论是使用悲观策略的共享线程池, 还是精心规划多个线程池, 由于都是预定义, 难免在环境变化过程中出现线程资源不足或闲置的情况. 要是可以这样, 某个时段当A任务较少时,  它所闲置的线程能协调给负载较高的B任务, 那就完美了!

故,  共享线程池的乐观策略就是在保证每种任务预留最低资源的情况下, 允许任务依据一个弹性(elastic)配额去争抢线程资源, 达到线程利用率的最大化. 如有100个线程的池, A任务大部分的时候负载较高, 则给予50个的预留配额, 30个的弹性配额; 而B任务是偶尔某个时段复杂较高, 则给予20个线程的预留配额, 30个的弹性配额, 这样留了一个30个线程的资源空间, 让AB去合理竞争.

很多实现的细节, 还请参见源代码.

源代码

CentralExecutor.java

CentralExecutorTest.java

 

逻辑划分线程池

现在很多系统中,特别是事件驱动的系统中,对于线程池的维护很多时候根据业务处理类型的不同做划分和管理,但分开维护会带来下面两个问题:

1. 到处线程池,每个线程池都有上限设置,但是所有线程池到达上限的时候也许系统已经无法承受了,所以局部设计和限制无法达到全局限制的目标。

2. 合理的利用线程池的资源,当线程池逻辑上真实隔离后,就无法将空闲的线程资源借调给繁忙的任务处理使用。

设计中关注的:

虚拟隔离线程池需要有模型可以保证对于一些处理的保护,对于一些处理的降级。

设计思路:

简单的两种配置模式:保留,限制。

举个例子:

默认线程池大小设置为100。

A类任务设置为保留10,B类任务设置为限制50。

假设有A,B,C三种任务进入。

A最大可以使用100个线程,其中10个是它独占的(通过配置可以选择优先使用公有的还是私有的)

B最大可以使用50个线程,当公有线程(100-10=90)被消耗后剩余总数小于50,那么B消耗的数量就会小于50,假如公有90个线程都没有被消耗,此时B最多也只能消耗50个线程。总结来说,B消耗公有的线程资源,同时最多只能消耗他的设置(当然他设置如果超过公有线程,则以公有线程池最大作为上限)

C最大可以使用90个线程,也就是所有的公有线程。

当任何一种请求没有线程资源可以被使用的时候,将会被放入队列,等待线程可用,队列不区分任务类型。

第一版简单的Java代码参看:http://www.rayfile.com/zh-cn/files/66a89e61-4357-11e0-9ad5-0015c55db73d/
这里只是探讨一种简单的设计思路,以最小代价来全局化管理维护线程池或者资源池。