标签归档:shell

利用iptables实现基于端口的网络流量统计

如何统计某个应用的网络流量(包括网络流入量和网络流出量)问题,可以转换成如何基于端口号进行网络流量统计的问题。大部分网络应用程序都是传输层及以上的协议,因此基于端口号(tcp, udp)统计网络流量基本能覆盖到此类需求。

利用iptables实现基于端口的流量统计是一种比较简单可行的方案。它可以对流经每一条规则的包数量和流量进行计数。例如要对常规的Web服务器进行流量统计,可以设置如下规则:

1
2
root@debian:~# iptables -A INPUT -p tcp --dport 80
root@debian:~# iptables -A OUTPUT -p tcp --sport 80

第一条规则表示,在INPUT链上添加一条规则,该条规则对所有来自外部网络的、与本机80端口通信的请求有效,即网络流入量,第二条规则则相反,它用于统计从本机80端口发出的网络流量,即网络流出量。因为我们的目的是统计流量,故此处可以省略ACCEPT或DROP之类的动作。

查看流量计数时,只要加上-nvx参数即可:

1
2
3
4
5
6
7
8
9
10
11
root@debian:~# iptables -nvx -L
Chain INPUT (policy ACCEPT 320 packets, 33045 bytes)
pkts     bytes target     prot opt in     out     source  destination         
0        785      tcp  --  *      *   0.0.0.0/0   0.0.0.0/0 tcp dpt:80
 
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts     bytes target     prot opt in     out     source  destination         
 
Chain OUTPUT (policy ACCEPT 311 packets, 33711 bytes)
pkts     bytes target     prot opt in     out     source  destination         
0        1252      tcp  --  *      *   0.0.0.0/0   0.0.0.0/0 tcp spt:80

iptables的-vx参数表示详细信息,可以以字节为单位显示包数量和网络流量,-n参数表示以数字方式表示ip地址和端口号等,不加的话iptables会默认将ip反解为主机名,例如127.0.0.1用localhost表示,端口号显示为协议号,例如80显示成http, 5672显示成amqp。

利用iptables规则统计网络流量可以保证应用在关闭重启后统计数据不丢失,但是对于主机重启的情况,它就无能为力了,因为默认情况下主机重启iptables规则会清空,即使使用开机任务等方式重建iptables规则,流量计数器也会清零。解决此类问题一个变通的方法是定期将网络流量值保存到文件,并清0重新计数,定期更新的时间越短,机器重启造成的流量计数丢失问题影响越小(特别涉及计费相关的业务)。

创建一个crontab任务,定期将流量统计追加至文件,例如:

1
root@debian:~# iptables -vxn -L >> file

也可以选择直接将流量值追加到文件:

1
root@debian:~# iptables -vxn -L | awk '{print $2}' >> file

当写入文件成功后,记得将流量值清0:

1
root@debian:~# iptables -Z

-Z参数支持指定链名称和规则索引号,例如下列命令表示清空INPUT链的第一条规则的流量计数器。

1
root@debian:~# iptables -Z INPUT 1

Reference:
[1] iptables man page: http://ipset.netfilter.org/iptables.man.html.

--EOF--

Erlang顺序编程 [1]

1. 变量不变性
Erlang中的变量分为绑定变量和自由变量。绑定变量是指经过一次赋值操作的变量,此后,该变量不能被再次改变,也就是说,一个变量在其可见域内只能赋值一次。除了绑定变量之外的变量都是自由变量,经过一次绑定后成为绑定变量。不要将‘=’理解为赋值操作,而是将其理解为模式匹配能更好理解变量的单次赋值特性。例如:

1
2
3
4
1> A=1.
1
2> A=2.
** exception error: no match of right hand side value 2

赋值之前,A是自由变量,通过模式匹配符=将A与1进行绑定,此后A的值不能再变,所以A=2模式匹配失败。变量绑定的本质是模式匹配,而其实现方式是:一个绑定变量就是一个指针,这个指针指向存放那个值的存储区,那个值是无法改变的。假如将某个变量绑定到一个空列表[],再往[]中append元素,此时,变量A还是指向[],要引用到添加新元素后的列表,必须将新列表绑定到新的变量。这点与Java等语言的列表引用非常不同。

Shell中可以通过b()查看所有当前已绑定的变量,f()命令释放所有绑定过的变量,f(X)释放变量X的绑定。

2. 浮点数运算
除号‘/’操作符永远返回浮点数,即使进行两个整数的除法运算,结果也会自动转换为浮点数。div和rem则只适用于整数的除和取余。例如:

1
2
3
4
5
6
7
8
9
10
11
12
1> 5 / 2.
2.5
2> 4 / 2.
2.0
3> 4.0 div 2.
** exception error: an error occurred when evaluating an arithmetic expression
     in operator  div/2
        called as 4.0 div 2
4> 5 div 2.
2
5> 5 rem 2.
1

3. 原子
原子(atom)是一串以小写字母开头,后跟数字字母、下划线或@的字符,一个原子的值就是原子自身。用单引号引起来的字符也是原子,通过这种形式,原子可以用大写字母开头或者包含其他可打印字符,如'Var', 'an atom'等。

4. 列表
列表的第一个元素称为列表的头(Head),剩下的称为尾(Tail),列表的Head可以是任何东西,但是列表的Tail通常还是一个列表。访问列表的头是非常高效的操作,实际上所有的列表处理函数都是从提取表头开始的,关于表头操作和表尾操作的性能差异可参考『Erlang列表操作性能分析』

当用[...|T]的方式来构造一个列表时,应该尽量保证T是一个列表。如果T是一个列表,那么新的列表就是正规形式的;反之,称为非正规列表,很多Erlang库都假定列表是正规的,可能不能正确处理非正规列表。两种列表形式如下:

1
2
3
4
1> [a|b]. %非正规列表
[a|b]
2> [a|[b]]. %正规列表
[a,b]

5. 字符串
Erlang中的字符串是用双引号(")括起来的一串字符,它的本质上是一个整数列表,列表中的每一个元素都是相应字符的整数值。只有列表中所有整数都是可打印字符(ISO 8859-1编码,也称Latin-1编码)时,Shell才把它当做字符串来打印。例如:

1
2
3
4
1> A = "erlang".
"erlang"
2> [1|A].
[1,101,114,108,97,110,103]

在列表头部加个不可打印字符的整数值,Erlang字符串立刻暴露出了它的本质。

另外,通过$符号可以显示一个可打印字符的整数值。例如:

1
2
3
4
5
6
1> $a.
97
2> $ .
32
3>.
234

摘自Joe Armstrong - 『Programming Erlang』。

--EOF--

如何使RabbitMQ用IP进行集群配置

RabbitMQ默认采用“节点名@主机名”作为集群间节点的通信地址。例如两节点rabbit@rabbit1和rabbit@rabbit2组成一个集群:

1
2
rabbit@debian$ cd ~/rabbitmq_server-3.1.3/sbin
rabbit@debian$ ./rabbitmqctl join_cluster rabbit@rabbit1

这里要求把rabbit1加到域名解析系统里去,或者修改/etc/hosts文件,添加主机名rabbit1到其节点IP的映射关系,反正通过rabbit1必需能找到其对应的IP。

若要改成使用“节点名@IP”作为集群间节点的通信地址,则要修改RabbitMQ的启动脚本。默认在${RABBITMQ_HOME}/sbin目录中,以rabbitmq_server-3.1.3版为例:

1. 修改rabbitmq-env文件中NODENAME全局变量,用本地IP替换@后边的HOSTNAME部分。

1
2
3
4
5
rabbit@debian:~/rabbitmq_server-3.1.3/sbin$ vim rabbitmq-env
 
[ "x" = "x$HOSTNAME" ] && HOSTNAME=`env hostname`
#NODENAME=rabbit@${HOSTNAME%%.*}
NODENAME=rabbit@192.168.1.100

2. 修改所有脚本里的-sname参数为-name。

1
2
3
4
5
rabbit@debian:~/rabbitmq_server-3.1.3/sbin$ grep -rin "\-sname" *
rabbitmqctl:34:    -sname rabbitmqctl</span> \
rabbitmq-plugins:<span style=33:    -sname rabbitmq-plugins" /> \
rabbitmq-server:90:	    -sname rabbitmqprelaunch$$ \
rabbitmq-server:110:    -sname ${RABBITMQ_NODENAME} \

将grep命令找出的多处-sname改为-name。

例如两个节点的IP分别是192.168.1.100和192.168.1.101,修改后的集群模式配置方式就变为:

1
2
rabbit@debian(192.168.1.101)$ cd ~/rabbitmq_server-3.1.3/sbin
rabbit@debian(192.168.1.101)$ ./rabbitmqctl join_cluster rabbit@192.168.1.100

done!

解释下这么修改的原因:
-name和-sname是Erlang节点的启动参数,表示以分布式模式启动Erlang节点,两者的区别是: -name表示long name,需要跟上可通过DNS解析的节点的完整域名(FQDN)或者IP,-sname表示short name,通常跟上主机名,并且主机名称中不能有点号.。Erlang节点只能在相同的集群模式下才能进行通信,-name启动的Erlang节点无法net_adm:ping/1到-sname启动的节点,反之亦然。

RabbitMQ默认采用-sname参数,Erlang节点名称NODENAME的生成方式为:

1
2
[ "x" = "x$HOSTNAME" ] && HOSTNAME=`env hostname`
NODENAME=rabbit@${HOSTNAME%%.*}

它表示先通过env hostname命令获取hostname,如果hostname包括域名,则取出第一个点号.之前的主机名部分。最后拼出一个rabbit@host形式的节点名称。

--EOF--

git completion

git源码包提供了命令行下git自动补全的脚本git-completion.bash,下载地址: github.comkernel.org

它能补全的内容包括:
*) git子命令,git subcmd。
*) git长选项参数,形如git subcmd --long-options。
*) 目录和文件名。
*) 本地和远程分支名。
*) 本地和远程tag名。
*) .git/remotes目录的文件名称。

安装流程:

1. 将git-completion.bash文件cp到某个目录并重命名,例如~/.git-completion.bash,加点变成隐藏文件可以减少视觉干扰。
2. 打开~/.bash_profile或者~/.bashrc,添加:

1
source ~/.git-completion.bash

3. 重启或新建Shell。OK。

--EOF--

Erlang record小结

Erlang中的record(记录)类似C语言中的结构体,它是一系列属性名和属性值的封装,是基本数据类型之外的一种伪数据类型,是一种key-value数据结构,通过属性名可以方便的取到属性值。record的标准定义形式是:

1
2
3
4
-record(name, {field1 [ = default1 ], 
               field2 [ = default2 ],
               ...
               fieldn [ = defaultn ]}).

name是record名,field1,field2...fieldn是属性名,要求必须是atom类型。属性值可以指定默认值,如果没有指定默认值,Erlang编译器会赋予该属性一个undefined值。
例如:

1
2
3
4
5
6
-module(record).
-export([print/0]).
-record(person, {name, age, hobby = ["Erlang"]}).
 
print() ->
  #person{name = "fcj"}. %构造一个name="fcj"的person record.

编译后执行:

1
2
3
4
5
Eshell V5.9.3.1  (abort with ^G)
1> c(record).
{ok,record}
2> record:print().
{person,"fcj",undefined,["Erlang"]}

#person可以看做是person记录的构造函数,通过给record各属性赋值来初始化record实例(可能表述不是十分准确)。例如:

1
Person = #person{name = "fcj", age = 25}.

要取到record的属性值,可以通过如下形式:

1
RecordExp#name.fieldName

其中,RecordExp表示一个record实例,name是record名, filedName是属性名,例如对参数中的#person实例年龄加1:

1
2
incrAge(#person{} = P) ->
  P#person.age + 1.

运行后如下:

1
2
3
4
5
6
7
Eshell V5.9.3.1  (abort with ^G)
1> c(record).
{ok,record}
2> rr(record). % rr表示从record.erl中加载record定义。
[person]
3> record:incrAge(#person{name="fcj", age=25}).
26

如果不加RecordExp,表达式#name.fieldName会返回filedName在record对应的元组中的位置,例如#person.name返回2,#person.age返回3。

record还支持嵌套定义,也就是说,record中某个属性的类型可以是个record。例如:

1
2
3
-record(name, {firstname, lastname}).
P = #person{name = #name{firstname = "f", lastname = "cj"}},
First = (P#person.name)#name.firstname.

record实质上是元组(tuple),Erlang虚拟机里没有record这种类型,Erlang编译器会在编译阶段把record替换成对应的元组。record相比元组优势在于:
1. record增加属性只需修改record定义即可,之前程序中用到该record实例的地方可以不用修改(除非需要用到新增属性);而元组增加一个元祖项时,必需在所有引用到该元组的地方增加一项,否则就会造成模式匹配失败。
2. 在守卫和函数参数模式匹配中,record可以直接取自己感兴趣的属性值;而元组必须必须先考虑模式匹配,会涉及到所有元组项。

前面说过Erlang虚拟机里没有record这种类型,所以Shell中使用record有些特殊指令:

  • rr(moduleName):加载moduleName模块中所有record定义。
  • rd(name, {field1 [ = default ], field2 [ = default ], ... }): 定义record。
  • rl():列出当前Shell中可见的record定义。
  • rf()/rf(name)/rf([name]): 卸载 全部/单个/多个 record定义。
  • --EOF--