TCP Fast Open 测试(2)

21 Apr 2014 Posted in  linux

接上篇。

18 日提到采用 wireshark 而不是 tcpdump 来抓取数据。wireshark 会自动把一些数据解释成可读的内容,于是看到其实在每次 httping 发出请求的时候,第一个 SYN 包后面都有附加了 TCP FASTOPEN COOKIE 请求:

于是回头重新好好读了一下 TFO 的原理,发现自己对 TFO 的理解是有问题的 - 原先我以为在 SYN 里是可以直接带上请求数据的 - 而这很容易被攻击。实际上的流程应该是:

  1. 客户端发送 SYN 包,包尾加的是一个 FOC 请求,只有 4 字节。
  2. 服务器端收到 FOC 请求,验证后根据来源 IP 地址生成 COOKIE(8 字节),将这个 COOKIE 加载 SYN+ACK 包的末尾发送回去。
  3. 客户端缓存住获取到的 COOKIE 可以给下一次使用。
  4. 下一次请求开始,客户端发送 SYN 包,这时候包后面带上缓存的 COOKIE,然后就是要正式发送的数据。
  5. 服务器端验证 COOKIE 正确,将数据交给上层应用处理得到响应结果,然后在发送 SYN+ACK 时,不再等待客户端的 ACK 确认,即开始发送响应数据。

示图如下:

所以可以总结两点:

  1. 第一次请求是不会有时间节约的效果的,测试至少要 httping -F -c 2
  2. 从第二次开始节约的时间可以认为是第一个来回,httping 本身是个 HEAD 请求,可以认为是 50% 的节约。

但是用 -c 2 运行依然没有看到 RTT 变化。这时候用stap 'probe kernel.function("tcp_fastopen_cookie_gen") {printf("%d\n", $foc->len)}' 命令发现这个最重要的生成 COOKIE 的函数(net/ipv4/tcp_fastopen.c里)居然一直没有被触发!

认真阅读了一下调用这个函数的 tcp_fastopen_check 函数(net/ipv4/tcp_ipv4.c里),原来前面首先有一步检查 sysctl 的逻辑:

    if ((sysctl_tcp_fastopen & TFO_SERVER_ENABLE) == 0 ||
        fastopenq == NULL || fastopenq->max_qlen == 0)
        return false;

这个 TFO_SERVER_ENABLE 常量是 2。而我电脑默认的 net.ipv4.tcp_fastopen 值是 1。1 只开启客户端支持 TFO,所以这里要改成 2(或者 3,如果你不打算把客户端搬到别的主机上测试的话)。

重新开始 httping 测试,RTT 依然没有缩短。这时候的 stap 命令发现 tcp_fastopen_cookie_gen 函数虽然触发了,但是函数里真正干活的这段逻辑依然没有触发(即 crypto_cipher_encrypt_one):

void tcp_fastopen_cookie_gen(__be32 addr, struct tcp_fastopen_cookie *foc)
{
    __be32 peer_addr[4] = { addr, 0, 0, 0 };
    struct tcp_fastopen_context *ctx;
    rcu_read_lock();
    ctx = rcu_dereference(tcp_fastopen_ctx);
    if (ctx) {
        crypto_cipher_encrypt_one(ctx->tfm,
                      foc->val,
                      (__u8 *)peer_addr);
        foc->len = TCP_FASTOPEN_COOKIE_SIZE;
    }
    rcu_read_unlock();
}

我试图通过 stap 'probe kernel.function("tcp_fastopen_cookie_gen"){printf("%s\n", $$locals$$)}' 来查看这个 ctx 是什么内容。输出显示 ctx 结构里的元素值都是问号。

目前就卡在这里。

为了验证除了这步没有其他问题,我”野蛮”的通过 systemtap 修改了一下 tcp_fastopen_cookie_gen 里的变量。命令如下:

stap 'probe kernel.function("tcp_fastopen_cookie_gen") { $foc->len = 8 }'

赋值为 8,就是 TCP_FASTOPEN_COOKIE_SIZE 常量的值。

然后再运行测试,就发现 httping 的第二次运行的 RTT 时间减半了(最后那个 F 应该就是标记为 Fastopen 的意思吧)!可见目前问题就出在这里。

$ httping -F -g http://192.168.0.100 -c 2
PING 192.168.0.100:80 (/url):
connected to 192.168.0.100:80 (154 bytes), seq=0 time= 45.60 ms 
connected to 192.168.0.100:80 (154 bytes), seq=1 time= 23.43 ms  F
--- http://192.168.0.100/url ping statistics ---
2 connects, 2 ok, 0.00% failed, time 2069ms
round-trip min/avg/max = 23.4/34.5/45.6 ms

注:上面这个强制赋值 foc->len 没有改变其实 foc->val 是空的事实,所以只能是测试验证一下想法,真用的话多客户端之间会乱套的。

继续阅读……

TCP Fast Open 测试(1)

16 Apr 2014 Posted in  linux

首先,这是一个未完成的测试。

新闻上大家都知道,Nginx从1.5.8开始支持fastopen参数,Linux从3.5开始支持fastopen特性,并在3.10开始默认开启。

httping是一个模拟ping输出的http请求客户端。从1.5开始支持发送fastopen请求,目前版本是2.3.4。

我在 fedora 20 (内核3.13版) 上编译了 nginx 1.5.13,yum 安装了 httping 2.3.3版。

开两个终端,一个运行tcpdump,然后另一个运行httping如下:

httping -F -g http://www.google.com.hk/url -c 1

这时候看到前一个终端的输出是这样的:

[chenlin.rao@com21-100 tfo]$ sudo tcpdump -i p5p1 -vvnxXs 0 tcp port 80
tcpdump: listening on p5p1, link-type EN10MB (Ethernet), capture size 65535 bytes
20:40:15.034486 IP (tos 0x0, ttl 64, id 52862, offset 0, flags [DF], proto TCP (6), length 147)
    10.2.5.100.40699 > 74.125.128.199.http: Flags [S], cksum 0xbb34 (correct), seq 3616187260:3616187335, win 29200, options [mss 1460,sackOK,TS val 31091970 ecr 0,nop,wscale 7,exp-tfo cookie 9a8e5a15f1deab96], length 75
	0x0000:  4500 0093 ce7e 4000 4006 913c 0a02 0564  E....~@.@..<...d
	0x0010:  4a7d 80c7 9efb 0050 d78a a37c 0000 0000  J}.....P...|....
	0x0020:  d002 7210 bb34 0000 0204 05b4 0402 080a  ..r..4..........
	0x0030:  01da 6d02 0000 0000 0103 0307 fe0c f989  ..m.............
	0x0040:  9a8e 5a15 f1de ab96 4845 4144 202f 7572  ..Z.....HEAD./ur
	0x0050:  6c20 4854 5450 2f31 2e30 0d0a 486f 7374  l.HTTP/1.0..Host
	0x0060:  3a20 7777 772e 676f 6f67 6c65 2e63 6f6d  :.www.google.com
	0x0070:  2e68 6b0d 0a55 7365 722d 4167 656e 743a  .hk..User-Agent:
	0x0080:  2048 5454 5069 6e67 2076 322e 332e 330d  .HTTPing.v2.3.3.
	0x0090:  0a0d 0a                                  ...
20:40:15.295644 IP (tos 0x0, ttl 30, id 42640, offset 0, flags [none], proto TCP (6), length 52)
    74.125.128.199.http > 10.2.5.100.40699: Flags [S.], cksum 0x71c1 (correct), seq 1878126810, ack 3616187261, win 42900, options [mss 1430,nop,nop,sackOK,nop,wscale 6], length 0
	0x0000:  4500 0034 a690 0000 1e06 1b8a 4a7d 80c7  E..4........J}..
	0x0010:  0a02 0564 0050 9efb 6ff1 f0da d78a a37d  ...d.P..o......}
	0x0020:  8012 a794 71c1 0000 0204 0596 0101 0402  ....q...........
	0x0030:  0103 0306                                ....
20:40:15.295694 IP (tos 0x0, ttl 64, id 52863, offset 0, flags [DF], proto TCP (6), length 115)
    10.2.5.100.40699 > 74.125.128.199.http: Flags [P.], cksum 0x5bf7 (correct), seq 1:76, ack 1, win 229, length 75
	0x0000:  4500 0073 ce7f 4000 4006 915b 0a02 0564  E..s..@.@..[...d
	0x0010:  4a7d 80c7 9efb 0050 d78a a37d 6ff1 f0db  J}.....P...}o...
	0x0020:  5018 00e5 5bf7 0000 4845 4144 202f 7572  P...[...HEAD./ur
	0x0030:  6c20 4854 5450 2f31 2e30 0d0a 486f 7374  l.HTTP/1.0..Host
	0x0040:  3a20 7777 772e 676f 6f67 6c65 2e63 6f6d  :.www.google.com
	0x0050:  2e68 6b0d 0a55 7365 722d 4167 656e 743a  .hk..User-Agent:
	0x0060:  2048 5454 5069 6e67 2076 322e 332e 330d  .HTTPing.v2.3.3.
	0x0070:  0a0d 0a                                  ...
20:40:15.560807 IP (tos 0x0, ttl 30, id 42641, offset 0, flags [none], proto TCP (6), length 40)
    74.125.128.199.http > 10.2.5.100.40699: Flags [.], cksum 0x5720 (correct), seq 1, ack 76, win 670, length 0
	0x0000:  4500 0028 a691 0000 1e06 1b95 4a7d 80c7  E..(........J}..
	0x0010:  0a02 0564 0050 9efb 6ff1 f0db d78a a3c8  ...d.P..o.......
	0x0020:  5010 029e 5720 0000 0000 0000 0000       P...W.........
20:40:15.568068 IP (tos 0x0, ttl 30, id 42642, offset 0, flags [none], proto TCP (6), length 269)
    74.125.128.199.http > 10.2.5.100.40699: Flags [P.], cksum 0x85ae (correct), seq 1:230, ack 76, win 670, length 229
	0x0000:  4500 010d a692 0000 1e06 1aaf 4a7d 80c7  E...........J}..
	0x0010:  0a02 0564 0050 9efb 6ff1 f0db d78a a3c8  ...d.P..o.......
	0x0020:  5018 029e 85ae 0000 4854 5450 2f31 2e30  P.......HTTP/1.0
	0x0030:  2034 3034 204e 6f74 2046 6f75 6e64 0d0a  .404.Not.Found..
	0x0040:  436f 6e74 656e 742d 5479 7065 3a20 7465  Content-Type:.te
	0x0050:  7874 2f68 746d 6c3b 2063 6861 7273 6574  xt/html;.charset
	0x0060:  3d55 5446 2d38 0d0a 4461 7465 3a20 5765  =UTF-8..Date:.We
	0x0070:  642c 2031 3620 4170 7220 3230 3134 2031  d,.16.Apr.2014.1
	0x0080:  323a 3430 3a31 3520 474d 540d 0a53 6572  2:40:15.GMT..Ser
	0x0090:  7665 723a 2067 7773 0d0a 436f 6e74 656e  ver:.gws..Conten
	0x00a0:  742d 4c65 6e67 7468 3a20 3134 3238 0d0a  t-Length:.1428..
	0x00b0:  582d 5853 532d 5072 6f74 6563 7469 6f6e  X-XSS-Protection
	0x00c0:  3a20 313b 206d 6f64 653d 626c 6f63 6b0d  :.1;.mode=block.
	0x00d0:  0a58 2d46 7261 6d65 2d4f 7074 696f 6e73  .X-Frame-Options
	0x00e0:  3a20 5341 4d45 4f52 4947 494e 0d0a 416c  :.SAMEORIGIN..Al
	0x00f0:  7465 726e 6174 652d 5072 6f74 6f63 6f6c  ternate-Protocol
	0x0100:  3a20 3830 3a71 7569 630d 0a0d 0a         :.80:quic....

没错,在第一个 SYN 包的时候就把 HEAD 请求带过去了。

但是发现比较奇怪的是很多时候一模一样的命令,SYN 包上就没带数据。

按我的想法,既然还是第一个 SYN 包,客户端这边压根不知道服务器端的情况,那么应该不管服务器端如何 SYN 里都带有 HEAD 请求啊?


另外,用 httping -F 命令测试自己编译的 nginx 的时候,一直都没看到正确的抓包结果,HEAD 请求一直都是在三次握手后发送的。

试图用 systemtap 来追踪一些问题。

第一步确认我的 nginx 的 socket 是不是真的开了 fastopen:

一个终端运行如下命令:

stap -e 'probe kernel.function("do_tcp_setsockopt") {printf("%d\n", $optname)}'

另一个终端启动nginx,看到前一个终端输出结果为23,查 tcp.h 可以看到 23 正是 TCP_FASTOPEN 没错!

第二步确认 httping 发送的时候是不是开了 fastopen:

一个终端运行如下命令:

stap -e 'probe kernel.function("tcp_sendmsg") {printf("%d %x\n",$msg->msg_namelen,$msg->msg_flags)}'

另一个终端运行最开始提到的 httping -F 命令,看到前一个终端输出结果为 16 20000040,查 socket.h 可以看到 MSG_FASTOPEN0x20000000MSG_DONTWAIT0x40,也就是说 httping 也没问题。

现在比较郁闷的一点是:在 net/ipv4/tcp.c 里,tcp_sendmsg() 函数会判断 if ((flags & MSG_FASTOPEN)),就调用 tcp_sendmsg_fastopen() 函数来处理。但是试图用 systemtap 来调查这个函数的时候,会报一个错:

WARNING: probe kernel.function("tcp_sendmsg_fastopen@net/ipv4/tcp.c:1005") (address 0xffffffff815cca08) registration error (rc -22)

原因还未知。

留记,继续研究。


注1:发现 chrome 即使在 about:flags 里启用了 fastopen 好像也不行,必须命令行 google-chrome --enable-tcp-fastopen 这样打开才行。

注2:网上看到有人写server和client的demo演示fastopen,但其实不对,demo代码里print的数据是正常三次握手以后socket收到的。这点开tcpdump才能确认到底是什么时候发送的数据。


2014 年 04 月 18 日更新:

今天改用 wireshark 看了一下数据包,在第一个 SYN 包没有带请求数据的时候,其实最末尾可选项里是有 fastopen 的,截图如下。看来还是服务器端的问题。下一步研究 tcp_recvmsg() 函数去。

继续阅读……

Larry Wall 来中国参加 OSTC 和 PerlChina Workshop

07 Apr 2014 Posted in  perl

见到教主真身真的很让人兴奋。在 OSTC 会场外的茶座抓住机会完成了签名跟合影。

书是从同事那搜刮来的大骆驼,自己的 Perl 书不好意思拿,因为不是教主亲著,不过后来发现绝大多数人都没大骆驼……

穿上了 PerlChina Workshop 2013 的 T恤,教主夫人帮忙在后面扯直了也让教主签名了。

OSTC 上教主讲的是自己跟开源社区的联系和小故事,以他自己最早期的时候的一个小程序 rn (read news) 的开发过程做了示例。

接着一星期后又单独举办了 PerlChina 的 Workshop,场地是一家叫 Happylatte 的手游公司的作坊,很有氛围。作坊环境的图片大家可以进 Linux Deepin 的王勇写的文章里面去看,他拍了超多图片:http://planet.linuxdeepin.com/archives/5688

PerlChina 送给教主一本全员签名的新华大字典,一个 3D 打印的教主头像。

我在字典上签名

Linux Deepin的王勇从武汉过来送给教主一个新的笔记本电脑,教主自己那台太老了……

深度的帅小伙

教主首先分享,讲述了一些 Perl 语言的设计思想,跟其他语言的思想上的对比。然后现场演示了一个 Perl6 写的小程序,分别用 MoarVM、JVM 和 Parrot 三种虚拟机上的 Rakudo 实现跑了一下给我们看效果。然后基础语法什么的。

时不时还切换到 Perl6 的 IRC 频道上给外国朋友打个招呼,跑个单行命令让 rakudorobot 自动返回结果什么的:

irc

为了演示 Unicode 支持,教主还联系中文环境,直接从字库里搜索了”从一个龙到四个龙,不过后面几个都是Unicode扩展字库里的字,UTF8都不支持,我好不容易找到却没法通过jekyll build编译,只好截图了“出来,然后问:为什么没有五个龙叠在一起的字呢?哈哈,看来他是把汉字当做纯象形文字来学习了。于是就少不了著名的”biangbiang面”啦:

最后教主也稍微回答了几个我们提前准备的疑问,其中一个是我问的关于 Perl6 是否会去支持直接开发安卓应用的问题,因为有 JVM 实现了嘛。教主意思是”是的,理论上可以。不过实际上现在你要是写肯定会有问题跑不起来的,留作未来吧。”另一个大家都很关心的问题是核心库的问题,一来是 Perl5 的核心库比起 Python 来说少很多,二来是 Perl6 的 Rakudo Star 也要面临这这个第三方模块打包问题了,大家都想知道核心库是怎么选择的,为什么只选择这么多,未来 Perl6 会怎么选?不过教主回答说,核心库这个概念就不该有。语言设计和开发者做好核心,第三方库是发行版的打包者去选择的事情。回答很出乎意料之外,不过想想教主对 Perl6 只写启示录,留给别人做出多种实现,思路似乎是一脉相承的吧。

接着是我们几个人的小分享,本着活泼有趣的原则,都没有讲什么严肃的话题。我讲的是如何操作微博的 API。

最后的互动,教主让大家都说说自己是怎么开始写 Perl 的。一圈说下来起因还是蛮多的。

然后又是签名合影环节。不过这次我就没再去凑热闹了,教主很口耐的估计学每个合影的人的动作搞”镜像”~哈哈

签名必备的印章

最后照全家福,大家一起说好不喊茄子喊Wall~~

all

继续阅读……

腾讯云技术沙龙笔记

30 Mar 2014 Posted in  cloud

昨天去车库咖啡听了 InfoQ 办的腾讯云图技术沙龙,今天又听了 CSDN 办的开源技术大会上腾讯云的宣讲(没错,就是那个发明了”内部开源”概念的意思),总的来说,幸亏去了昨天的!

沙龙包括三个主题:

手机推送服务

手机推送其实是一个很难有亮点的服务,我之前试用过免费的 JPush 极光推送服务,应该说大家都差不多——引用SDK,通过 RESTful 接口或者网页后台发布通知。

从业务上说,腾讯云提出一个精准投放的推送概念。 这其实跟后面的多维度数据是联系在一起的,腾讯因为本身(可怕)的数据收集能力,可以很容易的区分几个基础维度——年龄、性别、地域。 (今天午饭跟@刘江总编在一起,他谈到CSDN如何跟技术社区、出版社一起做技术书籍时,提到类似问题,CSDN 上也有千万级的用户,但是怎么高质量的做推荐才不透支信誉或者徒劳无功呢?)

不过在技术周边介绍中,还是聊到了腾讯的 L5 里的技术点,在这记录一下:

起因是说到服务扩容,新服务器上线时会自动根据响应质量动态调整其在集群中的权重

这里我跟@liu点cy@守住每一天先后猜测并推论了几种在 Nginx 的 upstream 上的实现方式及相关技术。

不过这几种方案一般常见的用途都是上下线而不是权重调整(另一个需要注意的就是在线修改upstream不会同步到nginx.conf文本文件里)。

那么就涉及到下一步问题:怎么评定响应质量

Nginx 里是有个 HealthCheck 模块,不过还很基础。 于是联想到 LVS 项目中的调度算法,常见的RR、LC、LBLC和LBLCR,少见的还有NQ、SED。这都算是根据 RS 的情况智能调整流量导向。

后来跟讲师交流,稍微了解到了 L5 内部的一点信息。

  1. 流量到应用服务之前会经过两层调度(暂称为DNS agent和local agent);
  2. DNS agent 负责多个 local agent 之间的流量调度;
  3. local agent 只负责本组(原话是本机)的应用服务的流量权重调整;
  4. 一个新服务器上线,首先要经过一次镜像流量的试运行,达到5个9后才正式上线;
  5. local agent将收到的每秒10万个请求分配 1% 给新服务器,根据平均响应延时和成功率,判定是否合格,合格就继续加流量;
  6. 如果某个服务器被判定不合格了,比如低于5个9了,也并不是直接剔除,而是减流量;除非直接成功率只有85%这样,那就是直接踢。

从流程里”本机”还是”本组”的用词,很容易让我联想到类似 docker 或者说 PAAS 平台的做法。 我个人猜测确实有可能就是一组服务器,但是同时也是在一台真实主机上的多个容器。

这种做法应该适合业务运维尝试;CDN 方面,upstream 列表每次变动都会带来巨大的回源压力,反而是越少变动越好

多维度数据分析

前面提到了腾讯数据分析上最常用的几个维度就是年龄、性别和地域。但其实做数据挖掘维度是超级多的,讲师举了不少例子。

从腾讯云的概念上来说,这个数据分析主要是几个层次。

  1. 基础的经过整理和运算得到的 TopView。这个应该就是 Hive 里的表,按照讲师所说,TopView 里有 30 个左右的维度。 从交流来看,这个 Hive 表内容应该就是以 QQ 号为中心的用户行为数据。每天从原始数据里花点时间更新这个表。
  2. 选取需要的维度信息做 RollUp。也就是从 TopView 的30个维度数据中选取几个维度做统计分析。这个就是排列组合问题,挨个硬算了。
  3. 合作用户如果有自定义维度,并且勾选这个维度做统计分析,就要先退回到计算 TopView 这步,把自定义维度按照 TopView 的处理方式来做。

因为对 Hadoop 的 Map/Reduce 稍有了解,也用过 Hive,所以这里的东西不算太难理解。 其实整个重点是在如何用用户行为日志整理得到 TopView 这块,从讲师透露信息看,全腾讯的日志提前清洗过滤到一天只有几个 TB ,不到一百台的小集群几个小时就可以完成全部分析任务。但是这块属于纯 coding 问题,没什么太多可讲的。

在边听演讲的时候我也边思考了一下如果这个问题用 Elasticsearch 做,会怎么样?

由于ES不需要定义 schema,所以类似 TopView 整理这段应该更轻松一些; RollUp 计算就是写 bool query。 这个效率如何我不太了解。

(今天的会场上有介绍腾讯大数据平台的,应该跟这个多维度分析不是一个平台,今天的讲师说到他们的平台除了Hadoop这套还用到了pgsql)

移动动态加速

这一部分是个人比较关心的部分。移动来源占比越来越大,移动网络质量却一如既往的复杂和烂。如何有效提高移动访问质量现在也是大家都关心的问题,本周网宿也刚发布了他们的私有协议加速产品。

腾讯的做法是也提供了 SDK,但本质上没有做完全的私有协议优化而是尽量利用可靠的自建私有网络,软件的部分应该是今天宣布开源了,地址在:https://code.csdn.net/Tencent/mna

SDK 的主要工作流程如下:

  1. APP 初次运行,正常访问流程的同时,调用 SDK 开始运作;
  2. SDK 内置有 3 个主要运营商一共 9 个默认 ANS(应该是 application name service 的意思吧)的 IP 地址,同时向这 9 个地址发送 HTTP 请求; 请求内容包括应用使用的域名、 SDK 获取到的本机 IP 和接入运营商(后二者如果获取不到,其实 ANS 通过 HTTP 本身也没问题);
  3. ANS 根据请求,返回尽量近的 OC、RS 和 TEST 三个 IP 地址信息;
  4. SDK 根据最快返回的那个 ANS 的响应结果,开始并发测试本机到 OC 和 TEST 地址的链路情况; 其中,OC 应该是跟 SDK 地址在同省同运营商,并且是负载最低的;TEST 应该是跟 RS 在同机房,作为 RS 的替身来参加链路测试工作;
  5. 如果 TEST 测试结果占优,那 APP 继续直连 RS,走正常访问流程就可以了; 如果 OC 测试结果占优,那么 APP 之后的请求,将改为发往 OC 的地址,由 OC 转发给 RS;
  6. 在 APP 运行过程中,链路测试是定时每十分钟做一次;当然类似推送这样的长连接服务,不会因为链路测试结果切换而被主动断开。

OC 方面的主要工作包括:

TCP 代理

  • TCP 代理就是 sock5 代理。不过针对移动环境做了一些优化,去除了sock5里的一些验证算法;
  • 在 TCP 方面,去掉了 nagle 算法,也就是打开了 TCP_NODELAY 参数。 nagle 算法本身是做小包合大包,提高传输效率的;不过在移动环境下,某个包的丢失或者延迟是个很常态的情况,而 nagle 算法中一个包延迟,所有包都要等在后面的情况就会被放大了,所以打开 TCP_NODELAY 应该可以避免这个情况(个人尚未测试验证过,或许可以相信腾讯)。

HTTP 代理

没细说,应该就是 squid 或者 nginx 之类的。

集群层面

每个机房都做了集群,通过 VIP 统一发布。这方面跟@守住每一天浅聊了一下通过 MPLS 协议实现 Anycast 来在多机房间维护统一的 VIP。不过看起来大家系统运维跟精通 BGP 的网络专家联系都比较远,这方面还处于有所耳闻的状态。

最后还有一个小问题,就是上面我们看到过好几处,提到”并发”、”同时”这样的字眼,于是当时产生一个疑问:“三个演讲中,都反复强调为了手机省电我们做了这做了那的,为什么为了优化级别的测试工作,却这么频繁和高密度的做并发请求呢?比如 ANS 请求,我只给本运营商的2个ip发请求也可以接受啊?”

这个问题正好被旁边围观的另一位听众解答了:手机内的 3G 通信模块,一次大批量的数据发送跟几次小批量的数据发送相比其实更省电。

讲师则从实际效果角度证明,目前的频率和策略,从使用上看,确实看不出来对电量的影响。

继续阅读……

Perl5 的 Source Filter 功能

10 Mar 2014 Posted in  perl

去年在 p5-mop-redux 项目里看到他们在 Perl5 里实现了 Perl6 的面向对象设计的很多想法,尤其下面这段示例让人印象深刻:

    use mop;
    class Point {
        has $!x is ro = 0;
        has $!y is ro = 0;
        method clear {
            ($!x, $!y) = (0, 0);
        }
    }
    class Point3D extends Point {
        has $!z is ro = 0;
        method clear {
            $self->next::method;
            $!z = 0;
        }
    }
    my $p = Point3D->new(x => 4, y => 2, z => 8);
    printf("x: %d, y: %d, z: %d\n", $p->x, $p->y, $p->z);

这种 $!x 的变量是怎么实现的?最近几天,又在 CPAN 上看到另一个模块叫 Perl6::Attributes,实现了类似的语法。于是点进去一看,实现原来如此简单!

package Perl6::Attributes;
use 5.006001;
use strict;
no warnings;
our $VERSION = '0.04';
use Filter::Simple sub {
    s/([\$@%&])\.(\w+)/
        $1 eq '$' ? "\$self->{'$2'}" : "$1\{\$self->{'$2'}\}"/ge;
    s[\./(\w+)][\$self->$1]g;
};

原来这里用到了 Perl5.7.1 以后提供的一个新特性,叫做 Source Filters 。在解释器把 file 变成 parser 的时候加一层 filter。

继续阅读……

Docker Meetup 参会总结

09 Mar 2014 Posted in  docker

昨天去车库咖啡参加了 Docker Meetup,一共有三位做了分享。

第一位主要演示用法,这个基本都了解; 第二位描述了一下相关生态圈,我自认算是对DevOps工具和动态了解比较多的人了,听完后对这位自称10年前作为运维的Rails开发者不得不说个佩服,知道的真广泛; 第三位是BAE的技术负责人,很诚恳的介绍了自己是怎么从一抹黑的环境开始摸索着搞 PAAS 平台的,波折的选型中一些想法和顾虑也都很坦白。

问答聊天过程中,大家主要纠结两个疑难:

  1. docker 和 puppet 会是什么关系?
  2. docker 和 kvm 会是什么关系?

这里我个人也稍微写几句我的想法:

docker 和 puppet

docker 无疑是一种非常干净的大规模部署方案。而 puppet 本质是一个配置管理工具(官网说法是通过简洁易懂的DSL描述服务器配置),注意:这里并没有提到是大规模部署,事实上 puppet 自己就有好几种完全不同架构设计的部署运行方式。

所以,从概念定义上来说,我不觉得这两者会是一个替代关系。

那么,puppet 目前的用法,如何跟 docker 一起工作呢?从当前技术点上来说有两个不适应:

  1. puppet 非常强大的一件事情是 template 系统和 Facts 变量配合达到的灵活性。但是在 docker 容器里,Facts 变量是不可信的! 刚才测试了一下,以 docker -m 56m run ubuntu facter | grep memorysize 得到的结果是主机原始大小512m。所以,我们原先习惯的通过 Facts 变量来自动生成最佳配置的方法失效了。 事实上, docker 官博上关于 metrics 的获取有好几篇文章,也都很明确是从主机上来获取而不是容器内部。

  2. puppet 的通用运行方式,是 agent 和 master 通过 SSL 加密交互,根据 agent 的 hostname 来查询对应配置。但是目前的 docker 里,hostname 设置(docker run -h 参数)是只对容器内部生效的,在容器外部显然无法通过 DNS 反查。 以 docker 的愿景,一台主机上就应该运行几百个容器,在某个 master 里维护 hosts 列表显然不现实。 而且从目前看, docker 对容器间更偏向采用 IP 的方式。比如 -link 设置的主机,就是在环境变量里提供对方主机 IP。

这两个问题可能更多的不是从技术方面来追求解决它,而是在用法上规避它或者说无视它。

首先,要习惯横向扩展而不是单机提升。 应用压力上来了,第一反应不是“申请提高容器的 memory 限额”这样,而是“再开两个完全一样的容器加入负载均衡”。这就是 fip 工具提供 fip scale web=2这种命令的场景吧。 这样就规避了 Facts 变量的问题,反正你只会有一种系统一种配置文件,压根用不上异构和模板技术。

其次,从 Vagrant 的 provision 里学用法。 目前 Dockerfile 的 RUN 指令其实很类似 Vagrant 的 provision 中的 shell 实现。而 Vagrant 的 provision 实现还包括 puppet、chef等等。所以我们或许能琢磨一种替代 RUN 的优雅的 docker 镜像构建方式。 比如 puppet-librarian 的做法或许就是一个思路。Dockerfile 里 只需要 ADD 一个 Puppetfile,然后 RUN 一个 librarian-puppet 命令完成容器内一切配置。

docker 和 kvm

前面提到了 docker 中系统性能数据的采集问题。这或许就是容器和虚拟化一个差别问题,即便未来大家越来越普遍采购 ops 产品而不是自己搭建监控系统,也不会完全放心的认可主机提供商的系统性能数据,至少也还有一个核算和度量问题。

此外,容器目前比较普遍的一个用法,是一个容器里只跑一个业务进程。一个完整的业务系统的每个部分,都通过分散的各种服务相互走 API 来调用。迁移到这种环境,对传统业务显然是有重构压力的。而 kvm 虚拟机则基本没有这个问题。 当然,最近也已经看到文章在讨论单个 docker 容器里运行多个不同业务进程的问题。这方面,如果 docker 真有心往替代 kvm 努力,除了网络方面的硬技术外,这个 PAAS 层已经养成的思维逻辑也需要改变。

OK,说到网络问题。目前 docker 的运用,通过 -link 来连接,或者通过 etcd、serf 这类工具来获取想要连接的其他服务器的 IP,都是一种在相同主机上的应用。 看 pipework 和相关文章,似乎 openswitch 也只是做单个宿主机之上的 VLAN 划分管理? SDN 到底是怎么回事,我现在还完全不了解。

PAAS 层的另一个习惯用法,在第三个演讲中也提到,就是一般对程序的任何更新,都是重新创建一个新容器,然后在中控转发里转移流量导向,然后删除原有容器。这个和现有 kvm 云主机的玩法也是不一样的。 现在还不好评价哪种做法更优。不过个人有个疑惑: BAE 既然试图做到像 kvm 虚拟机一样,对一个用户长期锁定一个 docker 容器使用而不是随着更新开关新容器,那么整个平台上容器的创建删除频率就大大降低了,针对每个用户,整个生命周期里就只有一次初创建,那么他们为什么同时又还在纠结于容器创建和删除的速度太慢,要在 5s 内完成呢?

提到的从 warden 学来的 wsh 听起来蛮有趣~

继续阅读……

如何搜索 Elasticsearch 中存储的动态请求 URL

07 Mar 2014 Posted in  logstash

当我们用 logstash 处理 WEB 服务器访问日志的时候,肯定就涉及到一个后期查询的问题。

可能一般我们在 Kibana 上更多的是针对响应时间做数值统计,针对来源IP、域名或者客户端情况做分组统计。但是如果碰到这么个问题的时候呢——过滤所有动态请求的响应时间

这时候你可能会发现一个问题:我们肯定都是用 URL 里带有问号? 来作为过滤条件。但是实际是 Kibana 里一条数据都过滤不出来。

于是我开测试库模拟了一下:

# 插入两条数据
curl http://localhost:9200/test/log/1 -d '{"url":"http://locahost/index.html"}'
curl http://localhost:9200/test/log/2 -d '{"url":"http://locahost/index.php?key=value"}'
# 搜索显示全部数据
curl http://localhost:9200/test/log/_search?pretty=1 -d '{"query":{"regexp":{"url":{"value":".*"}}}}'
# 搜索返回请求格式解析失败
curl http://localhost:9200/test/log/_search?pretty=1 -d '{"query":{"regexp":{"url":{"value":"\?.*"}}}}'
# 搜索返回空数据
curl http://localhost:9200/test/log/_search?pretty=1 -d '{"query":{"regexp":{"url":{"value":".*\\?.*"}}}}'

后来发现问题出在分词上面。

# 删除之前的测试数据和索引
curl -XDELETE http://localhost:9200/test/log
# 预定义索引类型的映射,url字段在索引的时候不分词
curl http://localhost:9200/test/log/_mapping -d '{"log":{"properties":{"url":{"index":"not_analyzed","type":"string"}}}}'
# 还是插入两条数据
curl http://localhost:9200/test/log/1 -d '{"url":"http://locahost/index.html"}'
curl http://localhost:9200/test/log/2 -d '{"url":"http://locahost/index.php?key=value"}'
# 同样的搜索请求,返回了一条结果(index.php?这条)
curl http://localhost:9200/test/log/_search?pretty=1 -d '{"query":{"regexp":{"url":{"value":".*\\?.*"}}}}'

上面这个搜索还可以简写成 Query DSL 的样式:

curl 'http://localhost:9200/test/log/_search?q=url:/.*\\?.*/&pretty=1'

而在 Logstash 比较新的 1.3.3 版本之后,有自带的 template 定义,会对每个 fields 采用 multi-fields 特性,也就是除了默认分词的 URL 字段以外,还有一个 URL.raw 字段都是不分词的。所以只要过滤这个字段就可以了。

(注意,ES1.0版的multi-fields的template写法完全不一样了,所以要用这个特性的童鞋还是谨慎测试logstash和es的版本配套)

Medcl 大神提示我:不指定 mapping 的情况下,ES 默认采用 unigram 分词。也就是切成尽可能小的单词。

继续阅读……

转换 diagramo 绘制的拓扑图成 fig.yml 格式

07 Mar 2014 Posted in  docker

前几天在微博上跟 @易度-潘俊勇 在评论里提到,已经有了 Fig 工具可以通过写一个 fig.yml 来快速定义主机上各 docker 容器的配置和角色。如果再进一步,可以通过绘图的方式,直接拖拽生成整个 docker 集群,那就更好了。

这个FIG挺有趣的,我自己写了一个类似的脚本。 不过我觉得终极的解决方案是画个关系图,就配置好了。 这个图的存储形式应该就是这个FIG,或者FIG可以转换为图。然后又可以转换为systemd的配置文件。

画关系图,桌面上肯定是 visio,visio保存成 XML 后分析 XML 就可以了。不过 visio 本身也算笨重的了,如果可以在浏览器中完成这个工作,才算够 cool!

网页上的 visio 已经有些产品,不过有名的几个都是有限免费试用的。好在找到一个叫做 diagramo 的项目,虽然提供的元素图表不多,但是也够用了。

下载源码包,在 LAMP/LEMP 环境下就直接跑起来,首次访问会要求注册一个用户名。环境配置中有一点必须重点点出来的:

Apache/Nginx 上配置的 server_name 必须跟你浏览器访问的完全一致

我曾经因为测试,所以写了个 localhost 做 server_name,然后用服务器 IP 地址来访问页面,结果在绘图完成保存的时候会出错!因为这是一个 HTML5 项目,保存这步是调用的 canvas.toDataURL() 函数,这个函数有强制性安全限定,以保证调用这个函数的页面,跟生成的图片路径必须是同一个域名!否则跨域抓图太方便了。

(写到这里感慨一下,chrome的调试工具不会用,这问题最后还是在 IE开发者工具的帮助下发现的 ==!)

然后就可以画关系图了,比如下图这样:

sample of diagramo

点击保存后,就会在服务器上的 $document_root/editor/data/diagrams 目录下生成对应的 .dia.png 文件。这个所谓的 .dia 文件,其实内容就是 JSON数据。下面我们只要抽取 JSON 里有关的数据就可以了:

use File::Slurp;
use JSON;
use YAML;
use Test::Deep::NoTest;
use 5.010;
use warnings;
use strict;
my $hash = from_json( read_file( $ARGV[0] ) );
my $hostinfo;
for my $host ( @{ $hash->{s}->{figures} } ) {
    $hostinfo->{ $host->{id} } = Load( $host->{primitives}->[1]->{str} );
}
for my $conn ( @{ $hash->{m}->{connectors} } ) {
    my $connid = $conn->{id};
    my $start  = $conn->{turningPoints}->[0];
    my $end    = $conn->{turningPoints}->[1];
    if ( $conn->{endStyle} eq 'Normal' and $conn->{startStyle} eq 'Arrow' ) {
        ( $start, $end ) = ( $end, $start );
    }
    my ( $startid, $endid );
    for my $point ( @{ $hash->{m}->{connectionPoints} } ) {
        if (    eq_deeply( $point->{point}, $start )
            and $point->{parentId} != $connid
            and exists $hostinfo->{ $point->{parentId} } )
        {
            $startid = $point->{parentId};
        }
        elsif ( eq_deeply( $point->{point}, $end )
            and $point->{parentId} != $connid
            and exists $hostinfo->{ $point->{parentId} } )
        {
            $endid = $point->{parentId};
        }
    }
    my ($startname) = keys %{ $hostinfo->{$startid} };
    my ($endname) = keys %{ $hostinfo->{$endid} };
    push @{ $hostinfo->{$startid}->{$startname}->{link} }, $endname;
}
say Dump { map { my ($k) = keys $_; $k => $_->{$y} } values $hostinfo};

生成的 fig.yml 如下:

---
Haproxy:
  link:
   - Serf
Nginx1:
  link:
   - Serf
Serf:
Nginx2:
  link:
   - Serf
MySQL:
  link:
   - Serf

只是根据关系图生成了 link,其他配置都在图里的 Text 里照样写 yaml 格式,会自动带入。当然,示例另一个意思是:大家尽量都只 link 像 serf/etcd 这样的服务自动发现服务器。在 docker 层面就简洁明了。

继续阅读……

Gearman 任务的优先级

20 Feb 2014 Posted in  perl

今天同事跟我说 Gearman 客户端添加任务的时候似乎设置优先级没有效果,于是去实现了一下,发现 Gearman 的任务优先级只有在任务本身属性完全一致的时候才能起到作用。比如说:新提交的 background 任务优先级虽然是 high,也不会在已经提交的 background、优先级是 low 的任务之前执行。

考虑到之前没用过优先级,这里贴一下测试代码当做笔记:

worker

use Gearman::XS::Worker;
my $worker = new Gearman::XS::Worker;
my ($host, $port) = ('10.4.1.21', 4730); 
my $ret = $worker->add_server($host, $port);
my $ret = $worker->add_function("reverse", 0, \&reverse, $options);
while (1) {
  my $ret = $worker->work();
}
sub reverse {
  my $job = shift;
  my $workload = $job->workload();
  my $result   = $workload;
  printf("Job=%s Function_Name=%s Workload=%s Result=%s\n",
          $job->handle(), $job->function_name(), $job->workload(), $result);
  sleep(5);
  return $result;
}

client

use Gearman::XS::Client;
use Time::HiRes qw/time/;
my $client = new Gearman::XS::Client;
my ($host, $port) = ('10.4.1.21', 4730); 
my $ret = $client->add_server($host, $port);
while (1) {
    my ($ret, $job_handle) = $client->do_background("reverse", 'low'.time() );
} 

high-client

use Gearman::XS::Client;
use Time::HiRes qw/time/;
my $client = new Gearman::XS::Client;
my ($host, $port) = ('10.4.1.21', 4730); 
my $ret = $client->add_server($host, $port);
while (1) {
    my ($ret, $job_handle) = $client->do_high_background("reverse", 'high'.time() );
} 

同时运行三个脚本,可以看到 worker 的输出,一直都是这样:

Job=H:YZSJHL1-21.opi.com:29434227 Function_Name=reverse Workload=high:1392887687.87583 Result=high:1392887687.87583 Job=H:YZSJHL1-21.opi.com:29434228 Function_Name=reverse Workload=high:1392887687.87594 Result=high:1392887687.87594 Job=H:YZSJHL1-21.opi.com:29434229 Function_Name=reverse Workload=high:1392887687.87605 Result=high:1392887687.87605

全都是高优先级的任务

继续阅读……

Facts 变量中 lsbdistid 和 operatingsystem 的区别

20 Feb 2014 Posted in 

Facts 变量是 puppet 里广泛使用的东西。在多种操作系统的混合环境中,通过 Facts 变量灵活定义不同的 package 名称、file 路径等应该是非常好用的办法。

不过关于操作系统,存在两类 Facts 变量,分别是 lsbdistidoperatingsystem。一般情况下,这两者结果基本一致,大家(至少我周围是)习惯采用 operatingsystem 这个一目了然的变量。

但是前两天发现有些机器的 puppet agent 运行失败,debug 后发现,居然是 operatingsystem 变量匹配不上!这台 CentOS 的服务器的 operatingsystem 结果是 OracleLinux!

翻看这两个变量的获取代码,他们的获取办法并不一致。

  • lsbdistid 是通过运行 lsb_release -i -s 命令获取的;
  • operatingsystem 是通过一串超长的 if-elif-else 逻辑来判断的。恰好其中探测 /etc/oracle-release 是否存在的步骤优先于探测 /etc/redhat-release 的步骤。

而这台服务器上,不知道怎么被人安装了一个 oraclelinux-release-5-8.0.2 的软件包,这个包里只有一个文件,就是 /etc/oracle-release

这个软件包怎么出现的可以慢慢追查,但是这件事情本身提醒我们,operatingsystem 变量的获取方式过于简单,这些文本文件稍有问题可能就会导致错误。所以在只有 Linux 类服务器的情况,还是尽量确保所有节点都安装有 lsb_release 命令然后使用 lsbdistid 变量吧。

继续阅读……

用 logstash 统计 Nginx 的 http_accounting 模块输出

19 Feb 2014 Posted in  logstash

继续捡宝贝~

http_accounting 是 Nginx 的一个第三方模块,会每隔5分钟自动统计 Nginx 所服务的流量,然后发送给 syslog。

流量以 accounting_id 为标签来统计,这个标签可以设置在 server {} 级别,也可以设置在 location /urlpath {} 级别,非常灵活。 统计的内容包括响应字节数,各种状态码的响应个数。

公司原先是有一套基于 rrd 的系统,来收集处理这些 syslog 数据并作出预警判断、异常报警。不过今天不讨论这个,而是试图用最简单的方式,快速的搞定类似的中心平台。

这里当然是 logstash 的最佳用武之地。

logstash.conf 示例如下:

input {
    syslog {
        port => 29124
    }
}
filter {
    grok {
        match => [ "message", "^%{SYSLOGTIMESTAMP:timestamp}\|\| pid:\d+\|from:\d{10}\|to:\d{10}\|accounting_id:%{WORD:accounting}\|requests:%{NUMBER:req:int}\|bytes_out:%{NUMBER:size:int}\|(?:200:%{NUMBER:count.200:int}\|?)?(?:206:%{NUMBER:count.206:int}\|?)?(?:301:%{NUMBER:count.301:int}\|?)?(?:302:%{NUMBER:count.302:int}\|?)?(?:304:%{NUMBER:count.304:int}\|?)?(?:400:%{NUMBER:count.400:int}\|?)?(?:401:%{NUMBER:count.401:int}\|?)?(?:403:%{NUMBER:count.403:int}\|?)?(?:404:%{NUMBER:count.404:int}\|?)?(?:499:%{NUMBER:count.499:int}\|?)?(?:500:%{NUMBER:count.500:int}\|?)?(?:502:%{NUMBER:count.502:int}\|?)?(?:503:%{NUMBER:count.503:int}\|?)?"
    }
    date {
        match => [ "timestamp", "MMM dd YYY HH:mm:ss", "MMM  d YYY HH:mm:ss", "ISO8601" ]
    }
}
output {
    elasticsearch {
        embedded => true
    }
}

然后运行 java -jar logstash-1.3.3-flatjar.jar agent -f logstash.conf 即可完成收集入库! 再运行 java -jar logstash-1.3.3-flatjar.jar web 即可在9292端口访问到 Kibana 界面。

然后我们开始配置界面成自己需要的样子:

  1. Top-N 的流量图

点击 Query 搜索栏左边的有色圆点,弹出搜索栏配置框,默认是 lucene 搜索方式,改成 topN 搜索方式。然后填入分析字段为 accounting。

点击 Event Over Time 柱状图右上角第二个的 Configure 小图标,弹出图表配置框:

  • Panel 选项卡中修改 Chart valuecounttotalValue Field 设置为 size;
  • Style 选项卡中修改 Chart OptionsBars 勾选项为 LinesY Format 为 bytes;
  • Queries 选项卡中修改 Charted Queriesselected,然后点中右侧列出的请求中所需要的那项(当前只有一个,就是*)。

保存退出配置框,即可看到该图表开始自动更新。

  1. 50x 错误的技术图

点击 Query 搜索栏右边的 + 号,添加新的 Query 搜索栏,然后在新搜索栏里输入需要搜索的内容,比如 count.500

鼠标移动到流量图最左侧,会移出 Panel 快捷选项,点击最底下的 + 号选项添加新的 Panel:

  • 选择 Panel 类型为 histogram
  • 选择 Queries 类型为 selected,然后点中右侧列出的请求中所需要的那项(现在出现两个了,选中我们刚添加的 count.500)。

保存退出,即可看到新多出来一行,左侧三分之一(默认是span4,添加的时候可以选择)的位置有了一个柱状图。

重复这个步骤,添加 502/503 的柱状图。

  1. 仪表盘设置存档

页面右上角选择 Save 小图标保存即可。之后再上界面后,就可以点击右上角的 Load 小图标自动加载。

==================================

上面这个 grok 写的很难看,不过似乎也没有更好的办法~下一步会研究在这个基础上合并 skyline 预警。

继续阅读……

squid-ssd方案和trafficserver的interim层的异同

18 Feb 2014 Posted in  cache

最近重新捡起来两年前做的 cache 软件测试对比,把原先的 trafficserver 淘宝分支升级到了现在的社区主分支,主要区别就是配置文件里不再直接叫 ssd.storage,而是正规化的起了一个名字叫interim cache layer

运行结果和当初类似,SATA 盘的 ioutil% 依然是远高于鄙司自创的 squid-ssd 方案。

于是沉下心来思考了一下为什么会有这么大的差距。

首先,squid-ssd 的设计其实非常简单,参照 Facebook 的 flashcache 原理扩展了 squid 原有的 COSS 存储引擎而已。所以我们先回忆一下 flashcache 的原理:

flashcache 是利用了 Linux 的 device-mapper 机制来虚拟逻辑块设备,在 ssd 和 sata 设备之间,flashcache 设计了三种模式:

  1. Writethrough 模式,数据同时写到 ssd 和 sata 硬盘,官方文档的说明是:

safest, all writes are cached to ssd but also written to disk immediately. If your ssd has slower write performance than your disk (likely for early generation SSDs purchased in 2008-2010), this may limit your system write performance. All disk reads are cached (tunable).

  1. Writearound 模式,数据绕过 ssd,直接写到 sata 设备上,官方文档的说明是:

again, very safe, writes are not written to ssd but directly to disk. Disk blocks will only be cached after they are read. All disk reads are cached (tunable).

  1. Writeback 模式,数据一开始只写到 ssd 上,然后根据缓存策略再移到 sata 设备上,官方文档的说明是:

fastest but less safe. Writes only go to the ssd initially, and based on various policies are written to disk later. All disk reads are cached (tunable).

squid-ssd 方案,学习的是 Writeback 模式,这种模式极大的缓解了普通 sata 设备的读写压力,牺牲了一定的数据安全。但是作为 CDN 缓存软件,本身就不需要保证这点 —— 这应该是源站来保证的。

相反,阅读了 ats 的文档说明后,发现 ats 的 interim 方案学习的是 Writearound 模式,而且默认的 tunable 那点还设的比较高, sata 设备上一个缓存对象要累积 2 次读取请求(最低可以修改到1,不能到0)后,才会缓存到 ssd 设备里去。

这一点从另一个细节上也可以反映出来:ats 的监控数据中,proxy.process.cache.bytes_total 是只计算了 storage.config 里写的那些 sata 设备容量的,不包括 interim 在的 ssd 设备容量。

继续阅读……

【翻译】Kibana 发生什么事了?

08 Feb 2014 Posted in  logstash

注:本文是 Elasticsearch 官方博客 2014 年 1 月 27 日《what’s cooking in kibana》的翻译,原文地址见:http://www.elasticsearch.org/blog/whats-cooking-kibana/


Elasticsearch 1.0 即将发布, Kibana 团队也准备发布自己的新版。除了一些常见的 bug 修复和小调整,下一个版本中还有一些超棒的特性:

面板组

面板现在可以组织成组的形式,组内可以容纳你乐意加入的任意多的面板。每行的删减都很干净,隐藏面板也不会消耗任何资源。

图表标记

变更部署,用户登录以及其他危险性事件导致的流量、内存消耗或者平均负载的变动,图表标记让你可以输入自定义的查询来将这些重要事件标记到时间轴图表上。

即时过滤器

创建你自己的请求过滤器然后保存下来以备后用。过滤器将和仪表盘一起保存,而且可以在对比你定义的数据子集的时候菜单式展开或收缩。

top-n 查询

单击某个查询旁边的带色的点,就可以设置这个查询的颜色。新版的top-N 查询会找出一个字段 最流行的结果,然后用他们来完成新的查询。

stats 面板

Stats 面板最后都将把搜索归总成一个单独的有意义的数值。

terms_stats 模式

按国家统计流量?每个用户的收入?每页的内存使用?terms面板的terms_stat模式正是你想要的。

继续阅读……

Mojo::IOLoop::Delay 模块测试代码解释

22 Jan 2014 Posted in  perl

昨天有人在群里问起Mojolicious/t/mojo/delay.t 中一段代码的执行原理。代码如下:

use Mojo::Base -strict;
BEGIN {
  $ENV{MOJO_NO_IPV6} = 1;
  $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll';
}
use Test::More;
use Mojo::IOLoop;
use Mojo::IOLoop::Delay;
my $delay = Mojo::IOLoop::Delay->new;
my $finished;
my $result = undef;
$delay->on(finish => sub { $finished++ });
$delay->steps(
  sub {
    my $delay = shift;
    my $end   = $delay->begin;
    $delay->begin->(3, 2, 1);
    Mojo::IOLoop->timer(0 => sub { $end->(1, 2, 3) });
  },
  sub {
    my ($delay, @numbers) = @_;
    my $end = $delay->begin;
    Mojo::IOLoop->timer(0 => sub { $end->(undef, @numbers, 4) });
  },
  sub {
    my ($delay, @numbers) = @_;
    $result = \@numbers;
  }
);
is_deeply [$delay->wait], [2, 3, 2, 1, 4], 'right return values';
is $finished, 1, 'finish event has been emitted once';
is_deeply $result, [2, 3, 2, 1, 4], 'right results';
done_testing();

首先介绍一下这个 Mojo::IOLoop::Delay 模块,这是异步编程中很火很实用的一个概念,一般叫 Promise / Deferred 。你可以按照顺序编程的思路组合那些异步函数,比如在这个例子里主要就体现了 steps 方法和 finish 事件。

steps 方法中可以传递任意多个异步函数。第一个函数立刻执行,然后等 $delay 信号量(由 begin 方法控制)释放(即重新等于0)后逐次执行后面的函数,直到碰到一个不调用 begin 控制信号量的函数,或者触发 error 或者 finish 事件。

begin 方法返回的回调函数 $end->() 用来减信号量。如果传递了参数给这个回调函数,那么第一个参数会被忽略,剩下的参数会 push 进下一个顺序或者事件触发函数的参数列表里,同时推送到 wait 方法。

所以上面这段测试的数据执行结果是这样的:

  1. $delay->wait 开始整个 ioloop, steps 方法首先执行 sub1 ,首先通过 $delay->begin()给信号量加1;
  2. 随即触发 timer 事件,$end->(1, 2, 3)(2, 3) 推入下一个函数 sub2 的 @_ 里,同时把信号量减1;
  3. 信号量变成0,继续执行,这一行 $delay->begin()->(3, 2, 1),将 (2, 1) 推入下一个函数 sub2 的 @_ 里,注意这里信号量实际也加减过一次,只是这里的回调函数直接匿名调用了;
  4. sub1 执行完成,信号量为0,那么开始下一个sub2,sub2 传入的参数列表其实是 ($delay, (2, 3), (2, 1)),也就是说这时候的 @numbers(2, 3, 2, 1)
  5. sub2 执行流程类似 sub1 ,信号量加1,触发 timer 事件,然后 $end->(undef, @numbers, 4)((2, 3, 2, 1), 4) 推入下一个函数 sub3 的 @_ 里,同时信号量减1;
  6. sub2 执行完成,信号量为0,那么开始下一个sub3,sub3 传入的参数列表就是 ($delay, (2, 3, 2, 1, 4)),也就是说这时候的 @numbers(2, 3, 2, 1, 4)
  7. sub3 将 @numbers 的引用赋值给 $result,因为 sub3 里没有对信号量的操作,而且也是最后一个了,steps 完成,触发 finish 事件;
  8. 注册的 finish 事件回调函数把 $finish 变量加1;
  9. $delay->wait 这时候也收集完毕前面每个 $end->() 的参数列表,和每步 @numbers 是同步的,同时因为 finish 事件被触发,就此停止 ioloop,程序完成,返回整个列表。

如上。

继续阅读……

【翻译】Kibana3 里程碑 4

15 Jan 2014 Posted in  logstash

本文来自Elasticsearch官方博客,2013年11月5日的文章Kibana 3: mileston 4,作为kibana3 Milestone 4重要的使用说明,翻译如下:

Kibana 3: Milestone 4 已经发布,带来了一系列性能、易用性和可视化上的提升。让我们来看看这些重大改变。如果你还在Milestone 3上,先看看之前这篇博客里的新特性介绍。

一个全新的界面

Kibana 面板改造成了一个标签更突出,按键和链接更易用,风格全新的样子。改造结果提高了可用度,因为有了更高效的空间利用设计,来支持更大的数据密度和更一致的UI。

Kibana的新界面

一致性查询和过滤布局

为了改善UI,查询和过滤面板现在有自己的可折叠、下拉的区域,具体位置在导航栏的下方。以后不再需要你自己摆放这些基本面板的布局了,它们默认会包含在每一个仪表盘里。和很多Kibana的特性一样,你也可以在仪表盘配置对话框里禁用这个一致性布局。

100%全新的时间范围选择器

如果你熟悉Kibana这两年来的历史,你可能知道曾经存在过好几个时间选择器方案。新的时间选择器经过了完全的重写,不仅占用空间比原来的小,也更容易使用。把这个重要组件移出主仪表盘后,Kibana 现在有更多空间专注于重要数据和图表。还有,新的过滤格式实现了Elasticsearch的时间运算,所以不用每次重新选择一个时间范围来移动你的时间窗口了,每个搜索都能自动更新这个窗口。

全新的时间选择器

可过滤的字段列表

利用表格的”即输即过滤”特性,可以简单而快速的找到字段。

可过滤的字段列表

即时(ad-hoc) facets

然后,当你找到了这些字段,就可以利用即时 facets 快速分析他们。只需要点击一个字段然后选择可视化即可查看到前10个匹配该字段的term。

研究起来也更加简单了

不需要添加面板,饼图可以直接悬浮出现!

动态的仪表盘和url参数

Kibana 3: Milestone 4现在可以通过URL参数获取输入!这个备受期待的特性体现为两个方式:模板化的仪表盘和脚本化的仪表盘。Kibana 3: Milestone 4附带两个可以和Logstash完美配合的示例,在此基础上你可以构建自己的仪表盘。模板化仪表盘的创建非常简单,导出当前仪表盘结构成文件,编辑文件然后保存添加进你的 app/dashboards 目录既可以了。比如,从 logstash.json 里摘录下面一段:

  "0": {
    "query": "",
    "alias": "",
    "color": "#7EB26D",
    "id": 0,
    "pin": false
  }

模板化仪表盘用”handlebar 语法”添加动态区段到基于JSON的仪表盘结构里。比如这里我们就用一个表达式替换掉了查询键的内容:使用URL里的请求参数,如果不存在,使用’*‘。 现在我们可以用下面这条URL访问这个仪表盘了:

http://kibana.example.com/index.html#/dashboard/file/logstash.json?query=extension:zip

更灵活的脚本化仪表盘

脚本化仪表盘在处理URL参数的时候更加强大,它能运用上Javascript的全部威力构建一个完整的仪表盘对象。同样用 app/dashboards 里的 logstash.js 举例。因为脚本化仪表盘完全就是javascript,我们可以执行复杂的操作,比如切割URL参数。如下URL中,我们搜索_最近2天内的HTML, CSS 或者 PHP,然后在表格里显示 request, responseuser agent。_注意URL本身路径从 file__变成了__script

http://localhost:8000/index.html#/dashboard/script/logstash.js?query=html,css,php&from=2d&fields=request,response,agent

立刻下载

Milestone 4对作者和使用者都是一个飞跃。它功能更强大,当然使用也更简单。Kibana 继续集成在 Logstash 里,最新发布的 Logstash 1.2.2 中就带有。Kibana现在也可以直接用elasticsearch.org官网下载,地址见:http://www.elasticsearch.org/overview/kibana/installation/

继续阅读……

【翻译】2013 年 9 月的 kibana 周报

14 Jan 2014 Posted in  logstash

本文来自Elasticsearch官方博客,2013年9月19日的文章this week in kibana,作为kibana3 Milestone 3重要的使用说明,翻译如下:

直方图零填充

直方图面板经过了一番改造,实现了正确的零填充。也就是说,当一个间隔内查询收到0个结果的时候,就显示为0,而不是绘制一条斜线连接到下一个点。零填充也意味着堆叠式直方图从顶端到底部的次序将保持不变。

此外,堆叠提示栏现在允许你在累积和个人模式之间自由选择。

数组字段的微分析

数组字段现在可以在微分析面板上单独或者分组处理。比如,如果我有一个tags数组,我即可以看到前10个最常见的tags,也可以看到前10个最常见的tags组合。

_source 作为默认的表字段

如果你没有给你的表选择任何字段,Kibana现在默认会给你显示 _source 里的 json 数据,直到你选择了具体的字段。

可配置的字段截取

注意到下面截图中 _source 字段末尾的”…“了吗?表格字段能被一个可以配置的”因子”截断。所谓因子就是,表格的列数除以它,得到一个字段的最大长度,然后各字段会被很好的截断成刚好符合这个长度。比如,如果我的截断因子是300,而表格有3列,那么每个字段会被截断成最大100个字符,然后后面跟上’…‘。当然,字段的完整内容还是可以在细节扩展视图里看到的。

关于细节视图

你可能已经知道单击表格某行后可以看到包含这个事件的字段的表格。现在你可以选择你希望如何观察这个事件的细节了,包括有语法高亮的JSON以及原始的未高亮的JSON。

更轻,更快,更小,更好

Kibana有了一个全新的构建系统!新的系统允许我们构建一个优化的,小巧的,漂亮的新Kibana。当你升级的时候它还可以自动清除原来的缓存,定期构建的Kibana发布在 http://download.elasticsearch.org/kibana/kibana/kibana-latest.zip ,zip包可以直接解压到你的web服务器里。

如果愿意,你也可以从 Github repository 开始运行。不用复制整个项目,只需要上传 src/ 目录到服务器就可以了。不过我们强烈建议使用构建好的版本,因为这样性能好很多。

继续阅读……

【翻译】kibana发生什么变化了?

14 Jan 2014 Posted in  logstash

本文来自Elasticsearch官方博客,2013年8月21日的文章kibana: what’s cooking,作为kibana3重要的使用说明,翻译如下:

还没有升级Kibana么?那你可错过了一个好技术!Kibana 发生了翻天覆地的变化,新面板只是这个故事中的一部分。整个系统都被重构,给表盘提供统一的颜色和图例方案选择。接口也经过了标准化,很多函数都修改成提供更简单,快速和功能更强大的方式。让我们进一步看看现在的样子。

Terms 面板;全局色彩;别名和查询;过滤器。

新的查询输入

新的查询面板替代了原来的“字符串查询”面板作为你输入查询的方式。每个面板都有自己独立的请求输入。你也还可以为特殊的面板定制请求,不过你要先在这里输入他们,包括可以有别名和颜色设置,然后再在面板编辑器里选取。在没有被激活修改的时候, 查询也可以被固定在一个可折叠的区域。

分配查询到具体面板

分配查询到具体面板非常非常简单。面板编辑器里就可以直接打开或关闭查询,哪怕这个查询已经更新或者过滤掉,它的别名是保持全局一致性的。你还会注意到配置窗口被分割成了选项卡形式,已提供更清晰的配置界面。

自定义颜色和别名

当你给一个查询分配某个颜色的时候,它会立刻反映到所有的面板上。通常用于做图例值的别名也一样。这样,我们可以很简单的通过在一个逻辑组里分配颜色变化,调节整个仪表盘和数据的意义。

你好,terms!

引入了一个新的terms面板,可以使用3种不同的格式展示顶层字段数据:饼图、柱状图和表格。而且都可以点击进入新的过滤器面板。

过滤器面板?

刚刚提到过滤器面板,对吧?没错,过滤器!过滤器允许你深入分解数据集而不用你去修改查询本身。然后,过滤器也可以被删除、隐藏和编辑。过滤器有三种模式:

  • must: 记录必须匹配这个过滤器;
  • mustNot: 记录必须不能匹配这个过滤器;
  • either: 记录必须匹配这些过滤器中的一个。

字段列表和微面板

字段面板集成在表格面板里。字段列表现在会通过访问Elasticsearch的/_mappingAPI来自动填充。注意你可能需要更新自己的代理服务器配置来适应这个变更。为了节约空间,这个字段列表现在也是可折叠的,而新的图形也添加到了微面板。

嗨,那配色方案呢?!

对,你在我解释之前已经发现这个变化了!Kibana现在允许你在黑白两个配色方案之间切换以刚好的匹配你自己的环境和偏好。

汇报完毕!当然kibana一直在更新,注意继续关注这里,给我们的github项目加星,然后上推特fo @rashidkpc@elasticsearch

继续阅读……

私有 docker 仓库部署测试

08 Jan 2014 Posted in  cloud

docker 的官方仓库 CDN 的ip 总是被 GFW 认证。为了更好的使用 docker ,有必要在自己内部搭建一个私有仓库。方法很简单:

git clone https://github.com/dotcloud/docker-registry.git
cd docker-registry
# 安装依赖
yum install python-devel libevent-devel python-pip openssl-devel xz-devel --enablerepo=epel
python-pip install -r requirements.txt
# 默认读取config/config.yml里的dev配置
WORKER_SECRET_KEY="${WORKER_SECRET_KEY:-$(< /dev/urandom tr -dc A-Za-z0-9 | head -c 32)}"
cat > config/config.yml<EOF
dev:
    storage: local
    storage_path: /tmp/registry
    secret_key: ${WORKER_SECRET_KEY}
EOF
# 默认的镜像存储位置,可以在 config.yml 里更改 storage_path
mkdir /tmp/registry
# 默认监听5000端口,前台运行,可以加入daemontools、supervisor、ubic之类的来负责
sh run.sh

这就完成了。如果想用 nginx 作代理和加速镜像下载性能的,代码里也提供了 nginx.conf 可用。不过注意要求 nginx 版本在 1.3.9 以上,同时编译的时候还要加上 chunkin 模块。否则镜像上传的时候会出错。

然后就是客户端如何指定镜像推送到私有仓库里:

# 在私有仓库注册用户
docker login 127.0.0.1:5000
# 给要提交的镜像打标签
docker tag <CONTAINER ID> 127.0.0.1:5000/tagname
# 推送到私有仓库
docker push 127.0.0.1:5000/tagname

注意这里推送的时候使用的是REPOSITORY,也就是说不能是 127.0.0.1:5000/ubuntu:12.04 这样的格式。

现在就可以在其他地方用了:

docker pull 192.168.0.2:5000/tagname
继续阅读……

利用 staticperl 和 upx 生成 单个可执行 perl

06 Jan 2014 Posted in 

Perl 程序打包的问题由来已久。

最早是 perlcc,但是从5.10版本以后,B::CC 等一系列模块跟不上开发脚本导致 perlcc 也无法使用。

然后是PAR::Packer,唐凤大神的作品。

今天介绍另一个模块,App::Staticperl,同样是大神级作品,作者是Marc Lehmann。他的 AnyEvent、Coro、EV 无不大名鼎鼎。而staticperl,就是他开发出来用以方便自己部署程序的。

staticperl 官网上有一句很霸气的描述:“perl, libc, 100 modules, all in one standalone 500kb file”。

不过经我测试,按照官网上的步骤是做不出来这么小的单文件的!幸运的是我在 Perlmonks 上的发问很快收到了答案,这个还要用上另一个工具:upx。

测试过程如下:

# cpanm App::Staticperl
# staticperl install
# staticperl instcpan AnyEvent AnyEvent::HTTP
# staticperl mkperl -MAnyEvent -MAnyEvent::HTTP
# staticperl mkapp myapp --boot myapp.pl -MAnyEvent -MAnyEvent::HTTP

而如果是官网说的 smallperl,则是采用 mkbundle 的方法。

除了使用单独的配置文件存放太长的参数,其他和 mkapp / mkperl 一致。

不过运行结果是:生成的单个文件有3.5MB大小。

然后使用 upx:

# apt-get install upx
# upx --best smallperl.bin

就得到压缩后的超小型perl了。这个perl内含了AE、Socket、common::sense、List::Util 等一系列常用模块可以直接使用。不过大小依然有 1.7MB 。看来是 Perl5.14 本身大小也变大了。

补充

按照评论里的建议,改用 --lzma 选项再压缩一次:

# upx -d smallperl.bin
# upx --lzma smallperl.bin

结果到 1.4MB 大小。

继续阅读……

通过网页运行 Perl 代码的安全实现

05 Jan 2014 Posted in  perl

这几天折腾Perl中国用户组网站,觉得类似 Ruby 的 tryruby,Scala 的 scala-tour 这样的新手入门教程非常好玩。于是准备自己也尝试一下。

理论上,通过 Ajax 传递代码到服务器上,直接 eval {} 即可。不过这样会导致一个安全问题。如何防止用户执行错误代码导致严重后果呢?

我想到了最近一直在跟踪看的 Docker 容器。如果我们把代码放在 Docker 里运行,不就不怕了么。

首先要构建一个可以运行大多数示例代码的 Docker 镜像。

首先打开一个终端运行初始镜像:

# docker run -i -t ubuntu /bin/sh
# apt-get install -y wget gcc make
# useradd tour
# echo 'tour hard nproc 8' >> /etc/security/limits.conf
# wget http://cpanmin.us -O bin/cpanm
# cpanm List::AllUtils Moo Path::Tiny DBD::SQLite AnyEvent::HTTP DateTime

然后打开另一个终端保存前一个终端的变更:

# docker ps
CONTAINER ID ...
# docker commit <ID> perl-tour

注意一定要在之前 cpanm 已经成功执行完毕后保存,但是前面登录进 docker 的会话千万不要退出,否则后面的 docker ps 就查看不到 id 了。退出时这些临时变更都毁掉了。

2014 年 1 月 7 日补充

被莫莫用死循环 fork() 轰炸了一回,发现 docker 容器的一个问题,容器技术本身没有对用户最大进程数的限制。因为其实际运行的是 docker -d 服务进程的子进程。

直接在镜像里编辑 /etc/security/limits.conf 实测没有作用。而主机上限定普通用户的 nproc 也没用(因为普通用户运行不了 docker )。

最后想到的办法,是启动 docker -d 的时候,先 ulimit -HSu 16,这样这个 docker 下一共也跑不了多少 fork 了。

顺带提一句,查阅系统日志可以发现,在 fork 的时候,其实触发了主机的 OOM-killer,但是这个机制在死循环这个变态攻击下挽救不了主机……

END

现在我们已经有了一个安装好很多常用 CPAN 模块的镜像了。可以取构建网站了。

网站里添加下面一段:

use Dancer::Plugin::Ajax;
use File::Temp qw(tempfile);
use IPC::Run qw(start harness timeout);
ajax '/run' => sub {
    my $code = param('code');
    my @cmd = qw(docker run -m 128m -u tour -v /tmp/:/tmp:ro perl-tour perl);
    my ($fh, $temp) = tempfile();
    binmode($fh, ':utf8');
    print $fh $code;
    push @cmd, $temp;
    my $h;
    eval {
        $h = harness \@cmd, \$in, \$out, \$err, timeout(5);
        start $h;
        $h->finish;
    };
    if($@) {
        my $x = $@;
        $h->kill_kill;
        return $x;
    };
    unlink $temp;
    return to_json({
        Errors => [ split(/\n/, $err) ],
        Events => [ split(/\n/, $out) ],
    });
};

页面上通过 Ajax 请求交互:

  $.ajax("/run?code=" + encodeURIComponent(codeStr), {
    type: "GET",
    dataType: "json",
    success: function(data) {
      if (!data) {
        return;
      }
      if (data.Errors && data.Errors.length > 0) {
        setOutput(outputDiv, null, null, data.Errors);
        return;
      }
      setOutput(outputDiv, data.Events, data.ErrEvents, false);
    },
    error: function() {
      outputDiv.addClass("error").text(
        "Error communicating with remote server.");
    }
  });

静态页面部分严重参考了 Scala 的 Tour 页。趁机学习了 impress.js 制作幻灯片效果、codemirror 实现代码高亮效果。

最终效果见 少年 Perl 的魔法世界。欢迎大家莅临指导~

最后,阅读了 Golang Tour 关于 Go Playground 的原理说明,发现它们是在 Google App Engine 上运行实例,然后走消息队列把代码发送给后台实例运行结果。

当然,Go Playground 不单单是支持 Tour,而且还包括社区各式第三方模块的测试和使用。把角色拆分出来也是正常的。

继续阅读……

Future模块和AnyEvent事件驱动的结合

05 Jan 2014 Posted in  perl

上个月的 advent calendar 活动中,有一个新的模块进入我们视野,这就是 IO::Async 模块作者写的 Future 模块。通过 Future 模块,我们可以做到对异步请求的各种控制,比如:

  • needs_all / needs_any / wait_any / wait_all
  • then / else / and_then / or_else / followed_by
  • on_ready / on_done / on_fail / on_cancel

目前来说,IO::Async 是原生支持 Future 了的。但是 AnyEvent 框架才是目前 Perl 社区事件驱动编程的主流选择。还好 Future 源码目录下 examples/ 里有关于 AnyEvent 和 POE 如何跟 Future 一起运行的示例。

示例统一举例的是 timer 事件。而我更看好的是 Future::Utils 提供的一些关于循环的函数,比如 fmap 可以很简单的控制住异步的并发数。稍微试验,得到脚本如下:

package Future::AnyEvent;
use base qw( Future );
use AnyEvent;
use AnyEvent::HTTP; 
sub await {
   my $self = shift;
   my $cv = AnyEvent->condvar;
   $self->on_ready(sub { $cv->send });
   $cv->recv;
}
sub httpget {
   my $self = shift->new;
   http_get(shift, sub {
      my ($content, $headers) = @_;
      $self->done($content);
   });
   return $self;
}
package main;
use Future::Utils qw/fmap/;
my @urls = qw(
    http://www.sina.com.cn
    http://www.baidu.com
    http://www.sohu.com
#    ...
);
my $f = fmap {
    Future::AnyEvent->httpget( shift );
} foreach => \@urls, concurrent => 5;
my @res = $f->get;
print @res;

看起来稍显复杂。这里其实最关键的就是几个接口函数:

  • await / on_ready

Future 对象到实际执行时(即->get调用处),会寻找 await 方法。所以必须给自己选用的事件驱动实现这个 await 方法。

ready 状态即一个 Future 执行完成,注意执行完成不意味着执行成功,ready 状态包括 success 和 fail 两种,其实是可以分别定义 on_successon_failure 回调的。 on_ready 回调的作用是:在该 Future 对象达到 ready 状态的时候,执行这步调用。

在本例使用 AnyEvent 的时候,也就是一般来说都会在每步操作结束的 $cv->send 改到这里来等待调用。

  • done / done_cb

那 Future 对象的 ready 状态是怎么来的呢?就是这步了:$f->done 一旦被调用,就意味着该 Future 对象进入了 ready and success 状态。

同样,如果你要详细控制 Future 对象进入具体的 ready but failure 状态,就使用 $f->fail 好了。

调用 ->done|fail() 的时候,你可以选择传递具体哪些数据。比如本例中,就只传递了抓取的 $content 而没有 $headers

Future 提供了 ->done_cb->fail_cb 两个回调函数,默认传递回当前全部数据。本例如果要传回全部,就可以直接写成http_get shift, $self->done_cb

好了,就到这里。这个例子虽然比 Future 自带的 anyevent.pl 示例稍微复杂一点,但是依然很简单。如果能引起大家的兴趣,请直接阅读官方文档。

继续阅读……

2013 年度个人总结

31 Dec 2013 Posted in 

又到了一年年底。照例(虽然这个例也就是去年开始的)开始年度总结。

在一年前写总结时,我决然想不到今年会是这样。事实上,当初的计划是往底层深入学习,在 Linux 或者 TCP/IP 方面有所得。但是一年后现在看,今年的工作依然集中在 Puppet 和 监控两方面。所以今年盘点,可能能让自己记忆深刻的,大多“功夫在课外”了。

年初,在编辑鼓励下开始尝试整理过去的知识体系,准备写一本网站运维相关的书籍。感谢这几年坚持记录博客,大几百的 Word 文档最后还比原计划提前写完了。写书的几个月中,我总是开玩笑的说“赚点钱买的起沙发就满足了”,虽然书还没出版,但其实这个过程中本身的收获已然很多。网上很多大神们说写书没意义,或许我还没到那种举重若轻的层次吧,我觉得这真的是一个不错的提升自我修养的手段。(当然,即便是现在回想,也觉得这过程中犯傻不断,给我一个重来的机会,绝对不选这么大话题动笔)

年中,参与 Perl 中国社区大会的举办。和其他老手不同,我本身只在三年前听过一次 Perl Beijing Workshop 而已,这次直接就被“骗上贼船”,作为报名网站的管理员维护一点信息发布,邀请还不算很熟的朋友一同演讲,当然也贡献了自己的第一次公开演讲。演讲前一晚,特意在家试讲让老婆帮忙提意见,毫不意外的被老婆批为乱七八糟。最后临场刚好卡在45分钟结束,但是从反响来看,依然选题有些宽泛,要在一个演讲里同时展示 Elasticsearch 的知识、logstash 的知识、Message::Passing 的知识,只能让听众更加迷惑。意外之喜是这次演讲的 slide 后来发到网上,倒是被不少外国人 like 甚至转到 twitter 上,赢得不少关注。

原本在 ChinaUnix 论坛上答应在大会的 lighting talk 上稍微讲一下 autobox 的运用,结果有事提前退场了,感觉失约这种事情真是超级不好意思,但愿明年还有 Perl Workshop 来给我弥补!

会上见到了 90 后的 Perler,会后没多久读到 stevan little 收回他《Perl isn’t dead, Perl is dead end》一文并重启 MOP 计划的通告,让我对 Perl 的未来依然有信心多了。

话题之外,参加了 RubyConfChina2013,Rubist 普遍比 Perl Monger 土豪多了。我们演讲人清一色小黑,他们清一色苹果……

年底读了许式伟的《Go 语言编程》,完全不是给我们这些非科班的运维人员读的东西,看完以后一点对 golang 的兴趣都没有增加,虽然本人依然坚持“能够在运维社区火起来的东西肯定是比较靠谱的”。

博客方面,欠了两篇一直没时间写,关于 docker 如何自己作image,以及 staticperl 的使用。

工作之外,花了点时间在一些开源社区:

  1. logstash

    logstash 在今年渐成气候,连它的竞争对手 fluentd 在年度报告中都承认 logstash 在美国已经势不可挡(fluentd的主阵地是日本)。个人在 logstash 代码方面只是保持跟踪阅读,因为没有业务需求推动,所以不再跟去年那样大肆修改代码。倒是通过weibo、QQ等方式回答了应该有好几十个人的问题,最后在各方鼓励下开设了 logstash 的QQ群(315428175),欢迎爱好者加入~

    另一个惊喜的事情,两位 logstash 同好在问完问题之后,主动送了《Elasticsearch Server》和《thelogstashbook》给我。

  2. Rexify

    Rexify 是德国2012年度最佳开源软件。不过受国内 Perl 社区总体不给力的影响,不可能如 Python 的 SaltStack 那样突然窜起。去年提交的 krb5 认证的 patch 在今年终于被作者合并,年中翻译完成了 Rexify 中文站 后,有一段时间没有进展了。

  3. Docker

    这是今年下半年重点看好的项目。博客中从 8 月开始就有好几篇关于这个内容,甚至专门订阅了 DockerWeekly。Docker 文档非常全,使用非常简单,实在爱不释手,最近老琢磨如何用在工作中去。最后这周,配合 pstuifzand 改进 Docker 的 Perl 客户端,主要是他写的时候 docker 默认还是监听在本地的 4243 端口,现在已经改成 /var/run/docker.sock 了。于是把 Net::DockerLWP::UserAgentAnyEvent::HTTP 的 Unix-Socket 支持都实现出来。

    另一方面,也给 Rexify 项目实现了 Rex::Virtualization::Docker 的支持,不过这个则是调用 docker 命令行的方式。

    最后,强烈谴责 GFW 屏蔽 Docker 的 CDN 边缘节点 IP 地址的行为。我本来提议让官方提供镜像方式,让我们在国内作镜像服务。结果官方表示 docker pull 的过程中连接了多个 api 服务,不是单单搞镜像可以解决的。目前只能是通过绑定 /etc/hosts 的方式直接访问官方的源站 IP,不走 CDN 了。

  4. Perl

    Perl5 社区今年发起了一系列活动,激发社区活跃性。其中包括一项挽救濒死模块。根据自己的情况,花了 3 个月时间走流程,最终成功认领了 HTML::TagHelper 模块。这个模块的原作者是个北欧妹纸,后来写 php 去了,看 linkedin 信息都已经是 CTO 了。

    因为工作中用到 MooseFS,所以仿造 moosefs.cgi 里的接口写了 Perl 版的模块发到 CPAN,结果 moosefs 的作者之一 peter aNeutrino 主动发邮件来问是否需要更多帮助。只能说大神们真的好热情……

    和氓氓等一起试图给 PerlChina 增加活跃气氛,申请了 @perldaily 帐号专门发技术内容,创建了 www.perl-china.com 网站。总的来说,努力过,成效就不在能力范围内了。

再说一些跟国外的事情,虽然都是小事,但迈出第一步,总是值得纪念的:

  1. Perl 社区每年12月会发起 advent calendar 活动。今年主动去日本的 Qiita 技术社区投稿,写了一篇文章讲 Rex::Box 的运用。虽然写不来日语,不过代码就是最好的语言~~
  2. 被 facebook 的 tech recruiter 找上来聊天,算是见识了一下除了美剧以外的英语,嗯,也就如此了。

最后一项不得不记的,关于比特币。我个人其实没有资金投入到这场狂欢中,不过暴赚几十万的同事、步步踩空的同事历历在目。一方面也回顾了一下当初的股票分析知识,通过程序作出了一些“不负责任的”指导意见,没有被同事们暴揍,算是一件很值得娱乐的事情~ 从严肃意义上来说,这件事情提醒了已经迈向26岁的自己,你已经不年轻了,Perl 之外,请考虑人生和理财的问题。

“Yes, sir!”

继续阅读……

为比特币绘制 MACD、BOLL、KDJ 指标图

09 Dec 2013 Posted in  python

比特币是最近相当火爆的一个金融衍生品(瞧咱这口径)。比特币中国提供了一系列 API 来获取和操纵其市场内的比特币。我的小伙伴们基于其 API,完成了一套交易程序。为了提高操作的有效性和技术性,同时作为 python 学习需要,我也参与进来,仿造股票交易软件,为比特币中国绘制了一系列指标图,包括 MACD、BOLL、KDJ 等。截止上周,btc123 也开始提供了 MACD 指标图,所以把自己的实现贴到博客。

首先是获取数据,比特币中国的 API 是个很鬼怪的东西,实时交易数据的接口,返回的数据中最高最低和成交量都是基于过去24小时的,要知道比特币交易是没有休市的啊。所以获取数据过程中需要自己计算这些。这里考虑到股市一般一天实际交易4小时,所以整个设计也是默认4小时的图形展示。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# query price data from BTCChina.
from urllib import urlopen
from ast import literal_eval
import MySQLdb
import json
import yaml
import time
config = yaml.load(open('config.yaml'))
conn = MySQLdb.connect(host=config['database']['host'],user=config['database']['username'],passwd=config['database']['password'],db =config['database']['databasename'],charset=config['database']['encoding'] )
def write_db(datas):
    try:
        cur_write = conn.cursor()
        sql =  "insert into ticker(sell, buy, last, vol, high, low) values( %s, %s, %s,%s,%s,%s)"
        cur_write.execute(sql,datas)
        conn.commit()
        cur_write.close()
    except MySQLdb.Error,e:
        print "Mysql error %d : %s." % (e.args[0], e.args[1])
def get_tid():
    try:
        vol_url = config['btcchina']['vol_url']
        remote_file = urlopen(vol_url)
        remote_data = remote_file.read()
        remote_file.close()
        remote_data = json.loads(str(remote_data))
        return remote_data[-1]['tid']
    except MySQLdb.Error,e:
        print "Mysql error %d : %s." % (e.args[0], e.args[1])
def get_ohlc(num):
    try:
        read = conn.cursor()
        hlvsql = "select max(last),min(last) from ticker where time between date_add(now(),interval -%s minute) and now()" % num
        read.execute(hlvsql)
        high, low = read.fetchone()
        closesql = "select last from ticker where time between date_add(now(),interval -%s minute) and now() order by time desc limit 1" % num
        read.execute(closesql)
        close = read.fetchone()
        opensql = "select last from ticker where time between date_add(now(),interval -%s minute) and now() order by time asc limit 1" % num
        read.execute(opensql)
        opend = read.fetchone()
        return opend[0], high, low, close[0]
    except MySQLdb.Error,e:
        print "Mysql error %d : %s." % (e.args[0], e.args[1])
def write_ohlc(data):
    try:
        cur_write = conn.cursor()
        ohlcsql =  'insert into ohlc(open, high, low, close, vol) values( %s, %s, %s, %s, %s)'
        cur_write.execute(ohlcsql, data)
        conn.commit()
        cur_write.close()
    except MySQLdb.Error,e:
        print "Mysql error %d : %s." % (e.args[0], e.args[1])
    except Exception as e:
        print("执行Mysql写入数据时出错: %s" %  e)
def instance():
    try:
    # returns something like {"high":738.88,"low":689.10,"buy":713.50,"sell":717.30,"last":717.41,"vol":4797.32000000}
        remote_file = urlopen(config['btcchina']['ticker_url'])
        remote_data = remote_file.read()
        remote_file.close()
        remote_data = json.loads(str(remote_data))['ticker']
    #   remote_data = {key:literal_eval(remote_data[key]) for key in remote_data}
    except:
        remote_data = []
    datas = []
    for key in remote_data:
        datas.append(remote_data[key])
    return datas
lastid = 0
ohlc_period = 60
next_ohlc = int(time.time()) / ohlc_period * ohlc_period
while True:
    datas = instance()
    if datas:
        write_db(datas)
    if(int(time.time()) > next_ohlc):
        next_ohlc += ohlc_period
        data = list(get_ohlc(1))
        latestid = get_tid()
        data.append(int(latestid) - int(lastid))
        lastid = latestid
        write_ohlc(data)
        time.sleep(1)

这里主要把实时数据存入ticker表,分钟统计数据存入ohlc表。然后是各指标算法。首先是 MACD :

#/*******************************************************************************
# * Author: Chenlin Rao | Renren inc.
# * Email: rao.chenlin@gmail.com
# * Last modified: 2013-11-26 22:02
# * Filename: macd.py
# * Description: 
#       EMA(12)=LastEMA(12)* 11/13 + Close * 2/13
#       EMA(26)=LastEMA(26)* 25/27 + Close * 2/27
#       
#       DIF=EMA(12)-EMA(26)
#       DEA=LastDEA * 8/10 + DIF * 2/10
#       MACD=(DIF-DEA) * 2
# * *****************************************************************************/
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import time
import hashlib
import MySQLdb
import yaml
class MACD():
    def __init__(self):
        config = yaml.load(open('config.yml'))
        self.sleep_time = config['btcchina']['trade_option']['sleep_time']
        self.conn = MySQLdb.connect(host=config['database']['host'],user=config['database']['username'],passwd=config['database']['password'],db =config['database']['databasename'],charset=config['database']['encoding'] )
    def _getclose(self, num):
        read = self.conn.cursor()
        sql = "select close,time from ohlc order by id desc limit %s" % num
        count = read.execute(sql)
        results = read.fetchall()
        return results[::-1]
    def _ema(self, s, n):
        """
        returns an n period exponential moving average for
        the time series s
        s is a list ordered from oldest (index 0) to most
        recent (index -1)
        n is an integer
        returns a numeric array of the exponential
        moving average
        """
        if len(s) <= n:
            return "No enough item in %s" % s
        ema = []
        j = 1
        #get n sma first and calculate the next n period ema
        sma = sum(s[:n]) / n
        multiplier = 2 / float(1 + n)
        ema.append(sma)
        #EMA(current) = ( (Price(current) - EMA(prev) ) x Multiplier) + EMA(prev)
        ema.append(( (s[n] - sma) * multiplier) + sma)
        #now calculate the rest of the values
        for i in s[n+1:]:
            tmp = ( (i - ema[j]) * multiplier) + ema[j]
            j = j + 1
            ema.append(tmp)
        return ema
    def getMACD(self, n):
        array = self._getclose(n)
        prices = map(lambda x: x[0], array)
        t = map(lambda x: int(time.mktime(x[1].timetuple())) * 1000, array)
        short_ema = self._ema(prices, 12)
        long_ema = self._ema(prices, 26)
        diff = map(lambda x: x[0]-x[1], zip(short_ema[::-1], long_ema[::-1]))
        diff.reverse()
        dea = self._ema(diff, 9)
        bar = map(lambda x: 2*(x[0]-x[1]), zip(diff[::-1], dea[::-1]))
        bar.reverse()
        return zip(t[33:], diff[8:]), zip(t[33:], dea), zip(t[33:], bar)

然后是 BOLL :

#/*******************************************************************************
# * Author: Chenlin Rao | Renren inc.
# * Email: rao.chenlin@gmail.com
# * Last modified: 2013-11-26 22:02
# * Filename: macd.py
# * Description: 
#       MA=avg(close(20))
#       MD=std(close(20))
#       
#       MB=MA(20)
#       UP=MB + 2*MD
#       DN=MB - 2*MD
# * *****************************************************************************/
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import random
import hashlib
import MySQLdb
import yaml
import time
class BOLL():
    def __init__(self):
        config = yaml.load(open('config.yml'))
        self.sleep_time = config['btcchina']['trade_option']['sleep_time']
        self.conn = MySQLdb.connect(host=config['database']['host'],user=config['database']['username'],passwd=config['database']['password'],db =config['database']['databasename'],charset=config['database']['encoding'] )
    def _getMA(self, array):
        length = len(array)
        return sum(array) / length
    def _getMD(self, array):
        length = len(array)
        average = sum(array) / length
        d = 0
        for i in array: d += (i - average) ** 2
        return (d/length) ** 0.5
    def getOHLC(self, num):
        read = self.conn.cursor()
        sql = "select time,open,high,low,close,vol from ohlc order by id desc limit %s" % num
        count = read.execute(sql)
        results = read.fetchall()
        return map(lambda x: [int(time.mktime(x[0].timetuple())) * 1000, x[1],x[2],x[3],x[4],x[5]], results[::-1])
    def _getCur(self, fromtime):
        curread = self.conn.cursor()
        cursql = "select last,vol from ticker where time between date_add('%s', interval -0 minute) and now()" % time.strftime('%F %T', time.localtime(fromtime))
        curread.execute(cursql)
        curlist = map(lambda x: x[0], curread.fetchall())
        vollist = map(lambda x: x[1], curread.fetchall())
        if len(curlist) > 0:
            return int(time.time())*1000, curlist[0], max(curlist), min(curlist), curlist[-1], sum(vollist)
        else:
            return None
    def _getClose(self, matrix):
        close = map(lambda x: x[4], matrix)
        return close
    def getBOLL(self, num, days):
        matrix = self.getOHLC(num)
        cur = self._getCur(matrix[-1][0]/1000)
        if cur:
            matrix.append(cur)
        array = self._getClose(matrix)
        up = []
        mb = []
        dn = []
        x = days
        while x < len(array):
            curmb = self._getMA(array[x-days:x])
            curmd = self._getMD(array[x-days:x])
            mb.append( [ matrix[x][0], curmb ] )
            up.append( [ matrix[x][0], curmb + 2 * curmd ] )
            dn.append( [ matrix[x][0], curmb - 2 * curmd ] )
            x += 1
        return matrix[days:], up, mb, dn

最后是 KDJ :

#/*******************************************************************************
# * Author: Chenlin Rao | Renren inc.
# * Email: rao.chenlin@gmail.com
# * Last modified: 2013-11-26 22:02
# * Filename: macd.py
# * Description: 
#       RSV=(close-low(9))/(high(9)-low(9))*100
#       K=SMA(RSV(3), 1)
#       D=SMA(K(3), 1)
#       J=3*K-2*D
# * *****************************************************************************/
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import hashlib
import MySQLdb
import yaml
import time
class KDJ():
    def __init__(self):
        config = yaml.load(open('config.yml'))
        self.sleep_time = config['btcchina']['trade_option']['sleep_time']
        self.conn = MySQLdb.connect(host=config['database']['host'],user=config['database']['username'],passwd=config['database']['password'],db =config['database']['databasename'],charset=config['database']['encoding'] )
    def _getHLC(self, num):
        read = self.conn.cursor()
        sql = "select high,low,close,time from ohlc order by id desc limit %s" % num
        count = read.execute(sql)
        results = read.fetchall()
        return results[::-1]
    def _avg(self, a):
        length = len(a)
        return sum(a) / length
    def _getMA(self, values, window):
        array = []
        x = window
        while x < len(values):
            curmb = self._avg(values[x-window:x])
            array.append( curmb )
            x += 1
        return array
    def _getRSV(self, arrays):
        rsv = []
        times = []
        x = 9
        while x < len(arrays):
            high = max(map(lambda x: x[0], arrays[x-9:x]))
            low = min(map(lambda x: x[1], arrays[x-9:x]))
            close = arrays[x-1][2]
            rsv.append( (close-low)/(high-low)*100 )
            t = int(time.mktime(arrays[x-1][3].timetuple())) * 1000
            times.append(t)
            x += 1
        return times, rsv
    def getKDJ(self, num):
        hlc = self._getHLC(num)
        t, rsv = self._getRSV(hlc)
        k = self._getMA(rsv,3)
        d = self._getMA(k,3)
        j = map(lambda x: 3*x[0]-2*x[1], zip(k[3:], d))
        return zip(t[2:], k), zip(t[5:], d), zip(t[5:], j)

最后通过一个简单的python web框架完成界面展示,这个叫 bottle.py 的框架是个单文件,相当方便。

#!/usr/bin/python
import json
import yaml
from macd import MACD
from boll import BOLL
from kdj import KDJ
from bottle import route, run, static_file, redirect, template
config = yaml.load(open('config.yml'))
color = {
    'cn':{'up':'#ff0000','dn':'#00ff00'},
    'us':{'dn':'#ff0000','up':'#00ff00'},
}
@route('/')
def index():
    redirect('/mkb/240')
@route('/mkb/<ago:int>')
def mkb(ago):
    like = config['webui']['color']
    return template('webui', ago = ago, color = color[like])
@route('/js/<filename>')
def js(filename):
    return static_file(filename, root='./js/')
@route('/boll')
def boll():
    return "boll"
@route('/macd/<day:int>')
def macd(day):
    m = MACD()
    dif, dea, bar = m.getMACD(day)
    return json.dumps({'dif':dif, 'dea':dea, 'bar':bar})
@route('/boll/<day:int>')
def boll(day):
    b = BOLL()
    ohlc, up, md, dn = b.getBOLL(day, 20)
    return json.dumps({'ohlc':ohlc, 'up':up, 'md':md, 'dn':dn})
@route('/kdj/<day:int>')
def kdj(day):
    kdj = KDJ()
    k, d, j = kdj.getKDJ(day)
    return json.dumps({'k':k, 'd':d, 'j':j})
run(host='127.0.0.1', port=8000, debug=True)

唯一的一个 html 就是具体用 highcharts 画图的地方,如下:

<html>
<head>
   <meta http-equiv="refresh" content="60">
   <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
   <script type="text/javascript" src="/js/highstock.js"></script>
   <script type="text/javascript" src="/js/highcharts.js"></script>
   <script>
    $(function () {
        Highcharts.setOptions({  
            global: {  
                useUTC: false  
            }  
        }); 
        $.getJSON('/boll/', function(bolldata) {
            var ohlc = []
                volume = [],
                dataLength = bolldata['ohlc'].length;
            for (i = 0; i < dataLength; i++) {
                ohlc.push([
                    bolldata['ohlc'][i][0],
                    bolldata['ohlc'][i][1],
                    bolldata['ohlc'][i][2],
                    bolldata['ohlc'][i][3],
                    bolldata['ohlc'][i][4],
                ]);
                volume.push([
                    bolldata['ohlc'][i][0],
                    bolldata['ohlc'][i][5],
                ])
            };
            $.getJSON('/kdj/', function(kdjdata) {
               $.getJSON('/macd/', function(macddata) {
                    $('#container').highcharts('StockChart', {
                        rangeSelector: {
                            enabled: 0
                        },
                        chart: {
                            backgroundColor: '#333333',
                        },
                	    tooltip: {
                	    	formatter: function() {
                				var s = '<b>'+ Highcharts.dateFormat('%A, %b %e, %H:%M', this.x) +'</b>';
                				$.each(this.points, function(i, point) {
                					s += '<br/>'+this.series.name+': '+parseFloat(point.y).toFixed(2);
                				});
                				return s;
                			}
                	    },
                        plotOptions: {
                            series: {
                                marker: {
                                    enabled: false
                                },
                                lineWidth: 1.1,
                            }
                        },
                        yAxis: [{
                          title: {
                              text: 'MACD(12,26,9)'
                          },
                          height: 200,
                        }, {
                          title: {
                              text: 'KDJ(9,3,3)'
                          },
                          top: 250,
                          height: 150,
                          offset: 0,
                          gridLineDashStyle: 'Dash',
                          tickPositions: [0, 20, 50, 80, 100, 200]
                        }, {
                            title: {
                                text: 'BOLL(20)'
                            },
                            top: 450,
                            height: 300,
                            offset: 0,
                        }, {
                            title: {
                                text: 'VOL'
                            },
                            top: 800,
                            height: 100,
                            offset: 0,
                        }],
                        series: [{
                            name: 'BAR',
                            color: '',
                            negativeColor: '',
                            borderColor: '#333333',
                            type: 'column',
                            data: macddata['bar'],
                            yAxis: 0,
                        }, {
                            name: 'DIFF',
                            color: '#ffffff',
                            type: 'line',
                            data: macddata['dif'],
                            lineWidth: 2,
                            yAxis: 0,
                        }, {
                            name: 'DEA',
                            color: '#ffff00',
                            type: 'line',
                            data: macddata['dea'],
                            lineWidth: 2,
                            yAxis: 0,
                        }, {
                            name: 'K',
                            color: '#ffffff',
                            type: 'line',
                            data: kdjdata['k'],
                            yAxis: 1,
                        }, {
                            name: 'D',
                            color: '#ffff00',
                            type: 'line',
                            data: kdjdata['d'],
                            yAxis: 1,
                        }, {
                            name: 'J',
                            color: '#cc99cc',
                            type: 'line',
                            data: kdjdata['j'],
                            yAxis: 1,
                        }, {
                            type: 'candlestick',
                            name: 'ohlc',
                            data: ohlc,
                            upColor: '',
                            upLineColor: '',
                            color: '',
                            lineColor: '',
                            yAxis: 2,
                        }, {
                            type: 'spline',
                            name: 'up',
                            data: bolldata['up'],
                            color: '#ffff00',
                            lineWidth: 2,
                            yAxis: 2,
                        }, {
                            type: 'spline',
                            name: 'md',
                            data: bolldata['md'],
                            color: '#ffffff',
                            lineWidth: 2,
                            yAxis: 2,
                        }, {
                            type: 'spline',
                            name: 'dn',
                            data: bolldata['dn'],
                            color: '#cc99cc',
                            lineWidth: 2,
                            yAxis: 2,
                        }, {
                            name: 'VOL',
                            borderColor: '#333333',
                            type: 'column',
                            data: volume,
                            yAxis: 3,
                        }]
                    });
                });
            });
        });
    }); 
   </script>
</head>
<body>
   <div id="container" style="min-width:800px;height:1000px;"></div>
</body>
</html>

highcharts 有个问题,就是不能跟 amcharts 或者 echarts 那样提供一个画笔工具,让用户自己在生成的图形上再涂抹线条,这个功能其实在蜡烛图上判断压力位支撑位的时候很有用。不过蜡烛图 btc123 也提供了,我也就懒得再用 amcharts 重写一遍。

效果如下:

继续阅读……

为 gitolite 实现 mailinglist 命令行操控

09 Dec 2013 Posted in  perl

<embed src='http://www.docin.com/DocinViewer-737880351-144.swf' width='100%' height='600' type=application/x-shockwave-flash ALLOWFULLSCREEN='true' ALLOWSCRIPTACCESS='always'>

或者上微盘下载 http://vdisk.weibo.com/s/CW3E1ZROXE9g

继续阅读……

Puppet 的类参数传递

04 Nov 2013 Posted in  puppet

之前使用 ENC 管理 puppet,尽量保持了输出 yaml 内容的简单,只提供了一个统一的全局参数定义 node 的 role。(题外话,puppetlabs 推荐了另一个通过继承关系实现 role 的示例,见:Designing Puppet - Roles and Profiles。)

但是 puppet 中有些配置确实修改比较频繁,文件操作不得不说是一件不甚方便的事情,于是重新考虑通过类参数的方式来灵活化某些配置的操作。

修改前

nginx/manifests/init.pp

class nginx {
    include "nginx::${::role}"
}

nginx/manifests/loadbalancer.pp

class nginx::loadbalancer {
    $iplist = ['192.168.0.2:80']
    file { 'nginx.conf':
        content => template('nginx/nginx.conf.erb'),
    }
}

enc nginxhostname

---
classes:
  - nginx
  - base
environment: production
parameters:
  role: loadbalancer

修改后

nginx/manifests/init.pp

class nginx ($iplist = []) {
    class { "nginx::${::role}":
        iplist => $iplist
    }
}

nginx/manifests/loadbalancer.pp

class nginx::loadbalancer ($iplist = []) {
    file { 'nginx.conf':
        content => template('nginx/nginx.conf.erb'),
    }
}

enc nginxhostname

---
classes:
  nginx:
    iplist:
      - 192.168.0.2:80
  base: ~
environment: production
parameters:
  role: loadbalancer

要点

  1. 虽然真正需要 $iplist 的是下面的一个子类,但是 ENC 传值是给的父类,所以需要一层层传递下去;
  2. ENC 中给类传参,类就要写成哈希形式,否则是数组形式;
  3. 有参数的类,在调用的时候无法使用 include 形式的写法,只能用资源调用形式的写法。

修改中出现了一个很搞笑的错误,因为是在 vim 里批量转换,结果子类名字后面多了一个空格,成了class { "nginx::${::role} ":这样。结果 puppet 一直返回报错说 “Invalid Parameter”。这时候一个习惯性的思维造成了误会:我们一般会认为:后面的那一行行键值对才是 parameter,但其实这里子类名也是 class 这个资源调用的 parameter。当然,如果可以在这里报一个 Class Not Found 就更好了。

继续阅读……