Darcy's Blog

不如烂笔头


  • 首页

  • 标签

  • 分类

  • 归档

  • 搜索

efficiency_guide:七个Erlang性能的误区

发表于 2017-10-14 | 更新于 2018-05-04 | 分类于 Erlang Efficiency Guide |

这些内容是有些是由于Erlang版本变化做的一些优化,和之前的有些要点有些出入,快速的扫一下即可

尾递归总是比普通递归来的快

这个说法在R12B之前是真的。在R7B之前更是如此。但是现在普通递归通常使用与尾递归相同的内存量,通常不可能预测尾递归或身体递归版本是否更快。因此,使用使代码更清洁的版本就可以了(通常是普通递归的版本)。但是死循环还是要使用尾递归,防止内存耗尽。

“++” 操作总是不好的

如果是这样[H] ++ Tail 使用 “++” 操作的话,没有什么不好的,编译器会自动把该操作转换成[H| Tail]。

字符串操作很慢

如果不正确地使用字符串,字符串操作速度可能很慢。在Erlang中,需要更多地思考如何使用字符串并选择适当的字符表示。如果使用正则表达式,请使用STDLIB中的re模块,而不是过时的regexp模块。

修复Dets文件非常慢

Dets文件的修复时间与文件中的记录数成正比,虽然Dets文件修复以前很慢,但是Dets的实现已被大量改写和改进。

BEAM是一个基于堆栈的字节码虚拟机(因此比较慢)

BEAM是一个基于寄存器的虚拟机。它有1024个虚拟寄存器,用于保存临时值,并在调用函数时传递参数。需要在函数调用中使用的变量将保存到堆栈中。 BEAM是一个线程代码解释器。每个指令是直接指向可执行C代码的字,使得指令调度非常快。

当变量不被使用时,使用“_”来加快程序速度

这个在R6B版本之前是这样的,但是在这个版本之后,编译器能够自动识别不使用的变量,所以用不用“_”都一样。

NIF总是能使你的程序更快

将Erlang代码重写为NIF以使其更快,应该被视为最后的手段。使用NIF肯定有风险,但是不能保证程序能更快。在每个NIF调用中进行太多的工作会降低VM的响应能力。做太少的工作可能意味着NIF中更快处理的优势被调用NIF并检查参数的开销所抵消了。所以在写NIF之前,请务必阅读 Long-running NIFs 。

Erlang文档的efficiency_guide总结

发表于 2017-10-13 | 更新于 2018-05-04 | 分类于 Erlang Efficiency Guide |

之前刚开始学习的Erlang的时候稍微看过这个教程,但是没有看全,发现这个教程还涵盖了挺多的信息的,今天把这个教程看完,顺便做一下总结,教程原版地址

本来是想把所有的总结写在一篇文章里面的,但是由于篇幅比较大,所以就把所有的总结分为以下几篇文章:

  • efficiency_guide:七个Erlang性能的误区
  • efficiency_guide:需要注意的模块和BIF
  • efficiency-guide:Binary的构建和匹配
  • efficiency-guide:List处理
  • efficiency-guide:函数
  • efficiency-guide:表和数据库
  • efficiency-guide:进程

最后再提两个误区:

  • 匿名函数很慢 匿名函数过去很慢,慢于apply/3。最初,使用编译器技巧,普通元组,apply/​3和大量的精巧方法实现了匿名函数。但那是历史。匿名函数在R6B中给出了自己的数据类型,并在R7B中进一步优化。现在,一个匿名函数的调用开销大概在调用本地函数和apply/3的开销之间。
  • 列表推导很慢 以前通过匿名函数实现列表推导,而在过去匿名函数确实很慢。 如今,编译器将列表推导重写成一个普通的递归函数。

怎么实现一个Sublime的自动补全插件

发表于 2017-09-03 | 更新于 2018-05-04 | 分类于 教程 |

使用Erlang开发了快三年的游戏了,一直使用的是Sublime编辑器,也就这样没有自动补全的情况下使用了三年,本来打算切换到有Erlang自动补全的Ide的,但是在Sublime上面开发了那么久,切换到其他的编辑器觉得很不习惯,所以就自己写了一个Erlang的自动补全的插件,点这里可以看到我的插件

Sublime插件是用Python写的,所以打算开发Sublime插件的话要稍微去学习下Python,不用学的很深入,懂得基本的语法就可以愉快的开始开发插件了。我之前的入门教程看的是creating-sublime-text-3-plugins-part-1,如果打算开发Sublime插件的话,看这篇文章就可以写一个简单的Sublime插件的Demo。这个网址api_reference可以查看开发Sublime插件所提供的各种API。

我写Erlang自动补全代码和自动跳转的原理是在打开Sublime的时候,扫描所有Erlang的源代码和Sublime中已经打开的所有的Erlang代码,然后利用正则表达式匹配来找出所有函数和模块所在的文件和位置,把这些信息都写入到Sqlite数据库中,然后在用户在编写Erlang源代码的时候提供补全的函数和模块。当用户把鼠标指向某个函数的时候,在Sqlite数据库中查询相应的函数所在的文件和位置,当用户选中的时候打开该文件并且定位到文件的相应的位置。具体的代码可以在点这里可以看到我的插件这里查看。当写好一个插件后我们最好能把插件放到Package Control中,这样用户安装和升级插件就会非常的方便,通过这个submitting_a_package教程能够顺利的提交自己的插件到Package Control中。

自己写一个小插件有时候还是可以学到一点东西的,通过这次编写自动补全的插件,让我对正则表达式稍微熟悉了一点。

mochiweb的x-forwarded-for实现引发的线上掉单

发表于 2017-08-18 | 更新于 2018-05-04 | 分类于 Erlang |

记录一次线上充值服的掉单问题,同时学习下什么是x-forwarded-for

掉单原因?

因为充值服都设有白名单,如果充值请求的机器的IP不在白名单里面的话会被视为非法IP,在掉单期间,线上的充值服发现有大量的100.116. . 的非法IP的访问,之后在网上一查,原来100.64.0.0/10也是属于内网IP的。我们的充值服务器使用了负载均衡,所以100.116..的IP应该是负载均衡机器的内网IP,同时由于我们充值服务器使用的是mochiweb的服务器,所以第一时间查看了下mochiweb获取IP的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
get(peer, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
case mochiweb_socket:peername(Socket) of
{ok, {Addr={10, _, _, _}, _Port}} ->
case get_header_value("x-forwarded-for", THIS) of
undefined ->
inet_parse:ntoa(Addr);
Hosts ->
string:strip(lists:last(string:tokens(Hosts, ",")))
end;
%% Copied this syntax from webmachine contributor Steve Vinoski
{ok, {Addr={172, Second, _, _}, _Port}} when (Second > 15) andalso (Second < 32) ->
case get_header_value("x-forwarded-for", THIS) of
undefined ->
inet_parse:ntoa(Addr);
Hosts ->
string:strip(lists:last(string:tokens(Hosts, ",")))
end;
{ok, {Addr={192, 168, _, _}, _Port}} ->
case get_header_value("x-forwarded-for", THIS) of
undefined ->
inet_parse:ntoa(Addr);
Hosts ->
string:strip(lists:last(string:tokens(Hosts, ",")))
end;
{ok, {{127, 0, 0, 1}, _Port}} ->
case get_header_value("x-forwarded-for", THIS) of
undefined ->
"127.0.0.1";
Hosts ->
string:strip(lists:last(string:tokens(Hosts, ",")))
end;
{ok, {Addr, _Port}} ->
inet_parse:ntoa(Addr);
{error, enotconn} ->
exit(normal)
end;

从上面的代码可以看出,如果在服务器内网里面使用了代理服务器之后,mochiweb是能够自动获取原始的访问IP。但是仅限内网代理服务器的IP是一些常见的内网IP,100.64.0.0/10段的IP地址并不包括在里面,所以这时候获取的IP就不是原始IP,而是负载均衡机器的内网IP。

内网IP段有哪些?

10.0.0.0/8
10.0.0.0 - 10.255.255.255

172.16.0.0/12
172.16.0.0 - 172.31.255.255

192.168.0.0/16
192.168.0.0 - 192.168.255.255

以上三个网段分别属于A、B、C三类IP地址

100.64.0.0/10
100.64.0.0 - 100.127.255.255
由运营商使用的私网IP段,随着IPv4地址池的耗光,会有更多用户被分配到这个网段。我们的线上掉单问题就是因为阿里云把内网IP切换到这个网段造成的。

http协议头标:x-forwarded-for

X-Forwarded-For(XFF)是用来识别通过HTTP代理或负载均衡方式连接到Web服务器的客户端最原始的IP地址的HTTP请求头字段。 Squid 缓存代理服务器的开发人员最早引入了这一HTTP头字段,并由IETF在Forwarded-For HTTP头字段标准化草案中正式提出。

总结

这次的掉单问题算起来应该算是一个不太能够发现的坑,主要是依赖第三方库的实现,我们这边相关的同事已经把修复代码提交pull request到mochiweb的github主页了,防止有更多的人碰到这个坑。

基于Erlang的全区服分数竞技场的设计的优化

发表于 2017-07-13 | 更新于 2018-05-04 | 分类于 算法设计 |

在上一篇文章Erlang动态代码载入小实验中,我提到的竞技场设计中存在一些性能问题,这篇文章主要是针对上一篇文章提到的性能问题在整体设计方案不进行大改的情况下进行优化。

主要性能问题

回顾上一篇文章,我们知道之前的设计方案的主要问题是:频繁的在分数ETS中拿取和更新新的玩家List,而且该List的大小有可能是几十万的级别的,主要的性能问题是ETS和排行榜进程之间的数据拷贝。

在同一分数段的所有玩家都用List来存储的话,每次在一个分数中增加一个玩家的代价是,先从ets中lookup拿出所有这一分数的玩家,然后在这个列表中增加新的玩家,最后再把新的玩家列表更新回去,删除也是如此。如果每个分数的玩家列表都不是很大的话,这个应该问题也不会很大,但是由于同一分数段的玩家比较多,所以这个方案的性能就很差了。

优化一

既然主要的性能问题出在List的更新和删除的操作,所以这个优化方案的主要方法是把List替换成ETS,当List的长度大于N(N可以自己设置,比如100、200之类)时,同一分数的所有玩家都存储在ETS中,这样在一个分数的玩家列表中增加一个玩家也只是在ETS中增加一个玩家ID而已,删除一个玩家的话,也只是在ETS中删除一个玩家ID,这两个操作都非常的快。

经过这一次的优化,竞技场玩家已经可以在正式环境中上线,但是在游戏最高峰的时间段,玩家还是会有点卡,此时游戏服务器(16核)的cpu几乎全部跑满,所以还需要进一步的优化。

优化二

通过上一次优化我们知道,服务器在高峰的时候几乎把cpu全部跑满。这时候我就开始怀疑寻找对手的算法是否有问题,之前寻找对手都是现算的,把玩家的对手的排名算出来,然后在分数的ETS里面开始从头到尾遍历所有分数,找出符合对手排名的玩家。这样子寻找一个玩家的三个对手,大概要遍历分数ETS一千多次,在平时的时候还是非常快的,下面是我用eprof测量的在平时寻找一次对手的一些关键操作的开销:

1
2
3
4
legend_arena_global_rank:query_apprentice_rank/7              389   4.25   290  [      0.75]
legend_arena_global_rank:query_apprentice_key/3 1076 7.45 508 [ 0.47]
ets:lookup_element/3 1465 34.31 2340 [ 1.60]
ets:prev/2 1459 45.52 3105 [ 2.13]

再下面的是我用eprof测试的在小高峰期寻找一次对手的一些关键操作的开销:

1
2
3
4
legend_arena_global_rank:query_apprentice_rank/7              386   1.16    216  [      0.56]
legend_arena_global_rank:query_apprentice_key/3 1029 2.95 551 [ 0.54]
ets:lookup_element/3 1415 39.47 7371 [ 5.21]
ets:prev/2 1409 54.38 10157 [ 7.21]

通过上面两次的测量可以看到:分数ETS在大量访问的时候出现了性能下降,本来ets:prev/2操作只需要2.13us,在高峰期居然需要7.21us;本来ets:lookup_element/3操作只需要1.6us,在高峰期居然需要5.21us。

所以这次的主要优化方法是把对分数ETS的访问次数降下来,建立一些排名的缓存,当一个玩家寻找对手的时候直接在缓存中寻找,同时每秒钟刷新一次缓存(确保排名比较正确)。这样不管是在高峰期还是平时,分数ETS都不会有非常明显的访问量的提升。

总结

通过这次的优化,我认为不管用什么语言来实现一个系统,都要了解这个语言的优势和劣势,这样才能找出一个合理的解决方案来解决一些比较棘手的问题。

设置Shell脚本执行错误自动退出

发表于 2017-07-06 | 更新于 2018-05-04 | 分类于 备忘 |

这是一篇备忘记录,以后再写Shell脚本的时候需要注意!

之前项目使用Jenkins打包的时候,有时候因为一些错误的提交,导致出包的时候编译失败,从而导致打包出来的包里面只有部分的代码,这是因为我们写的Shell脚本没有对每条Shell命令的结果进行检查,不管执行结果是否成功都会继续往下执行。所以即使我们在编译环节有错误产生,打包的脚本还是会继续执行后面的打包指令。所以必须让脚本在某条命令执行失败的时候停止执行后续的指令。
在Shell脚本中加入:

#!/bin/bash -e 或者 set -e

就能够让脚本在有错误的时候退出。下面是网上查的拓展:

使用set -e

1
2
3
4
5
6
7
8
9
你写的每一个脚本的开始都应该包含set -e。这告诉bash一但有任何一个语句返回非真的值,则退出bash。 

使用-e的好处是避免错误滚雪球般的变成严重错误,能尽早的捕获错误。更加可读的版本:set -o errexit

使用-e把你从检查错误中解放出来。如果你忘记了检查,bash会替你做这件事。

不过你也没有办法使用$? 来获取命令执行状态了,因为bash无法获得任何非0的返回值。

你可以使用另一种结构,使用command

使用command

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if [ "$?"-ne 0]; then echo "command failed"; exit 1; fi "

可以替换成:

command || echo "command failed"; exit 1; (这种写法并不严谨,我当时的场景是执行ssh "commond",
所以可以返回退出码后面通过[ #? -eq 0 ]来做判断,如果是在shell中无论成功还是失败都会exit)

修改如下(谢谢评论的朋友指正)


command || (echo "command failed"; exit 1) ;

或者使用:

if ! command; then echo "command failed"; exit 1; fi

基于Erlang的全区服分数竞技场的设计

发表于 2017-06-19 | 更新于 2018-05-04 | 分类于 算法设计 |

这篇文章主要介绍我之前开发的一个基于Erlang的全区服分数竞技场的设计思路,同时会给出该设计的在现实项目中出现的问题,并且在后续的几篇文章中优化设计。

关键需求

该分数竞技场的实际需求比较多,这边只列举对我们设计有影响的几个关键需求,以下是该分数竞技场的关键需求:

  • 每个玩家拥有一个分数,该分数大概是6000以内的数字。
  • 玩家要实时知道自己的分数和排名。
  • 玩家可以手动刷新自己的对手,玩家的对手由于玩家的排名乘以30%、60%、90%左右的排名的玩家随机出来。比如一个1000名的玩家,他的对手可能是由278、632、945名次的玩家组成。
  • 玩家挑战对手,如果战胜,则玩家自己加分,对手扣分,反之亦然。
  • 该竞技场每天在某一时间点进行结算发奖。

玩家数量级

玩家的数量级有两个,分别是测试环境和正式环境:

  • 测试环境的玩家帐号有几十万,日活是2万左右。
  • 正式环境的玩家帐号有几百万,日活是60万左右。

设计思路

下面介绍该分数竞技场的具体设计思路:

  • 由于该竞技场是采用分数来排名,而且分数的区间比较小,所以我想到了使用桶排序来对玩家分数排名。
  • 由于该竞技场是所有玩家共同访问的,所以打算用ETS来实现这个桶排序。
  • 由于分数需要是有序存储的,所以该ETS为ordered_set类型,而且Key为分数,Value有两个字段:
    • list——存储该分数的所有玩家的key
    • cnt——存储该分数的玩家总数
  • 玩家的排名为:从该分数ETS分数最大的元素开始遍历,遍历到玩家所在的分数的前一个分数,在遍历的同时累加遍历到的cnt值,玩家的排名为累加值加1,同一分数玩家的排名一致。例如:玩家分数为5000分,在5000分之前有100个5020分,1个5500分,则玩家为第102名。
  • 当玩家进行一场挑战的时候,把玩家key从他原来的分数list里面移除,并且该分数的cnt-1;同时把玩家key加入到新的分数的list,并且该新分数的cnt+1;对手的分数改变也是进行同样的操作;这些操作在gen_server中进行,确保数据不会被脏写。

运行结果

  • 该设计在测试环境中测试通过了,而且没有发现什么异常。
  • 在正式环境中,只有少量玩家访问的情况下,访问时间到达几秒的级别,只能暂时关闭该功能进行优化。

主要问题

这边列举两个比较严重的问题:

  • 同一分数的玩家数量很多,同一分数最多的玩家有50万人,使用list来存储玩家的key,每次对这个list增删代价巨大。
  • 由于ETS是另外一个单独的进程,每次从ETS中拿一个50万人的list,然后再把新的list更新回去,代价同样巨大。

主要优化目标

由于留给优化的时间比较短,所以要在原有的设计思路下对该分数竞技场进行优化,达到能够上线的标准。

Markdown语法速记

发表于 2017-06-17 | 更新于 2018-05-04 | 分类于 教程 |

有时候自己也会忘记Markdown的语法,在这边做一个备忘,以后找起来比较方便,这边记录的是最基本的Markdown语法。

粗体和斜体

1
_下划线是斜体_

下划线是斜体

1
**两个星是粗体**

两个星是粗体

1
**_粗体斜体一起用_**

粗体斜体一起用

六种标题

几个#号代表标题几,#号后面有空格

1
2
3
4
5
6
# 标题1
## 标题2
### 标题3
#### 标题4
##### 标题5
###### 标题6

链接

1
2
3
4
5
6
这是一个 [普通的链接方式](https://www.github.com)
这是一个 [引用的链接方式][another place].
这还是一个 [引用的链接方式][another-link].

[another place]: https://www.github.com
[another-link]: https://www.google.com

这是一个 普通的链接方式 这是一个 引用的链接方式. 这还是一个 引用的链接方式.

图片

1
2
3
4
![我的头像](https://lintingbin2009.github.io/img/avatar.jpg)
![又是一个头像][other]

[other]: https://lintingbin2009.github.io/img/avatar.jpg

我的头像 又是一个头像

引用

1
2
3
>在要被引用的段落或者行前面加大括号
>
>即使是空行也要加一下,保持一致

在要被引用的段落或者行前面加大括号

即使是空行也要加一下,保持一致

列表

1
2
3
4
5
1. 有序用数字
继续保持缩进,只需加空格
2. 有序用数字
* 无序用星号
* 还可再缩进,只需再加空格
  1. 有序用数字 继续保持缩进,只需加空格
  2. 有序用数字
    • 无序用星号
      • 还可再缩进,只需再加空格

段落

1
2
我在逗号后加了两个空格,  
所以不在一行

我在逗号后加了两个空格,
所以不在一行

Erlang中由rpc:cast错误引起的对error_logger的研究

发表于 2017-06-15 | 更新于 2018-05-04 | 分类于 Erlang深入 |

之所以会写这篇文章,是因为rpc:cast函数的使用超出了我的理解范围,本来我的理解是:如果rpc:cast执行失败的话,是不会有任何报错的。但是由于最近的线上存在两个版本的代码进行的互相调用引发了一些报警,让我好奇rpc:cast和error_logger是怎么工作的。

rpc:cast 是怎么工作的?

我们在查阅rpc的源代码的时候可以发现以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-define(NAME, rex).
...
handle_cast({cast, Mod, Fun, Args, Gleader}, S) ->
spawn(fun() ->
set_group_leader(Gleader),
apply(Mod, Fun, Args)
end),
{noreply, S};
...
cast(Node, Mod, Fun, Args) when Node =:= node() ->
catch spawn(Mod, Fun, Args),
true;
cast(Node, Mod, Fun, Args) ->
gen_server:cast({?NAME,Node}, {cast,Mod,Fun,Args,group_leader()}),
true.
...

从上面的代码我们可以看到,rpc:cast的时候如果目标node和本地node一样的话就会直接spawn一个进程处理,如果是远程的话,则会调用一个名字为rex的gen_server到远程的服务器上执行,远程的服务器同样也是spawn一个进程来处理。如果一个服务器是为其他服务器提供服务的(通过rpc模块),那么这个服务器的rex应该会是最繁忙的。通过上面的分析我们知道rpc:cast的错误是spawn函数通知error_logger的。

spawn出来的进程执行遇到错误怎么处理?

我自己试验了下,比如我自己在shell里面执行spawn(fun() -> 1 = 2 end).语句的话,error_logger就会收到如下的一个错误事件:

1
2
3
{error,<113291.32.0>,
{emulator,"~s~n",
["Error in process <0.13313.2075> on node 'all_in_one_33000@192.168.1.102' with exit value: {{badmatch,2},[{erl_eval,expr,3,[]}]}\n"]}}

为了明白上述的情况为什么会发生,我在Erlang邮件列表里面找到两个类似的问题,可以解答我的疑问:

[erlang-questions] An answer: how does SASL know that a process died?

[erlang-questions] error_logger events sent by emulator

简单的总结上面的两个问题,spawn执行的程序遇到异常的话,是由虚拟机的C语言代码向error_logger发送的错误事件。

error_logger 是怎么工作的?

error_logger是Erlang的错误记录器,由gen_event实现,在Erlang系统中会有一个注册名为error_logger的事件管理器(event manager),可以在事件管理器中加入各种处理模块来处理事件。默认的系统中会加入以下两个错误处理模块:

1
2
3
4
5
6
7
$ erl
Erlang R16B03 (erts-5.10.4) [source] [64-bit] [smp:12:12]
[async-threads:10] [hipe] [kernel-poll:false]

Eshell V5.10.4 (abort with ^G)
1> gen_event:which_handlers(error_logger).
[error_logger,error_logger_tty_h]

简单的说下这两个错误处理模块,首先是error_logger模块,以下是该模块的处理事件的部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
handle_event({Type, GL, Msg}, State) when node(GL) =/= node() ->
gen_event:notify({error_logger, node(GL)},{Type, GL, Msg}),
%% handle_event2({Type, GL, Msg}, State); %% Shall we do something
{ok, State}; %% at this node too ???
handle_event({info_report, _, {_, Type, _}}, State) when Type =/= std_info ->
{ok, State}; %% Ignore other info reports here
handle_event(Event, State) ->
handle_event2(Event, State).
...
handle_event2(Event, {1, Lost, Buff}) ->
display(tag_event(Event)),
{ok, {1, Lost+1, Buff}};
handle_event2(Event, {N, Lost, Buff}) ->
Tagged = tag_event(Event),
display(Tagged),
{ok, {N-1, Lost, [Tagged|Buff]}};
handle_event2(_, State) ->
{ok, State}.
...
display2(Tag,F,A) ->
erlang:display({error_logger,Tag,F,A}).

该模块把是本node产生的事件调用erlang:display()输出,把不是本node产生的事件发送到目标node上面,由目标node的error_logger进行处理。

接着是error_logger_tty_h模块,以下是该模块的处理事件的部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
handle_event({_Type, GL, _Msg}, State) when node(GL) =/= node() ->
{ok, State};
handle_event(Event, State) ->
write_event(tag_event(Event),io),
{ok, State}.
...
write_event({Time, {error, _GL, {Pid, Format, Args}}},IOMod) ->
T = write_time(maybe_utc(Time)),
case catch io_lib:format(add_node(Format,Pid), Args) of
S when is_list(S) ->
format(IOMod, T ++ S);
_ ->
F = add_node("ERROR: ~p - ~p~n", Pid),
format(IOMod, T ++ F, [Format,Args])
end;
...
format(IOMod, String) -> format(IOMod, String, []).
format(io_lib, String, Args) -> io_lib:format(String, Args);
format(io, String, Args) -> io:format(user, String, Args).

该模块把不是该node的事件直接忽略,然后把本node的事件调用io:format输出到终端上面。

除了这两个处理模块,Erlang的sasl应用还提供了三个模块:sasl_report_tty_h、sasl_report_file_h、log_mf_h。log_mf_h模块的功能最为强大,能够把错误写入指定个数的文件中,当文件用完后会自动删除最老的事件以腾出空间记录最新的事件。但是log_mf_h的缺点是记录的是二进制的格式,要查看记录的事件的话,还需要使用sasl提供的rb模块来解析,颇为繁琐。而且该模块没有对单事件的最大上限做保护,如果有超大的事件写入的话,就会导致文件错乱,看不了事件(这个可以自己写代码做保护,我们项目之前就是这样做的)。

当然除了官方提供的处理模块,也可以使用第三方提供的模块。现在我们项目就把所有官方提供的模块都删除掉了,只使用lager提供的error_logger_lager_h模块来处理事件,然后自己编写了一个alarm_handle_error模块用来发送报警。error_logger_lager_h使用文本的方式来记录事件,查看起来比较方便,而且对Erlang内部一些比较难以理解的错误进行翻译,比较容易理解;但是由于使用文本的方式进行记录,没有对事件消息进行格式化,如果消息比较大的话,读起来比较费劲。

error_logger 添加处理模块的注意事项

当使用sasl提供的log_mf_h处理模块的时候不能删除系统提供的error_logger模块,不然像rpc:cast通知的事件就不能正常的捕获了,原因如下:

1
2
3
4
5
6
%% 当NodeA执行以下函数的时候,在NodeB会接收到一个错误,
%% 由于NodeB只有log_mf_h模块,log_mf_h模块会对接收的事件使用sasl:pred/1函数进行过滤
%% sasl:pred/1会过滤不是本node的产生的事件,因此该错误被过滤
%% 如果这时候NodeB有error_logger模块的话,error_logger模块就会将事件通知NodeA
%% 然后NodeA就能使用log_mf_h模块正确记录该错误,该错误记录在NodeA的机器上
NodeA: rpc:cast(NodeB, M, ErrorFun, []).

lager的error_logger_lager_h模块默认会记录所有的事件,不管该事件是属于哪个Node的,如下:

1
2
3
%% 当NodeA执行以下函数的时候,在NodeB会接收到一个错误,
%% error_logger_lager_h直接记录错误在NodeB的机器上
NodeA: rpc:cast(NodeB, M, ErrorFun, []).

最后说两句

之前项目使用log_mf_h模块处理事件的配置文件如下:

1
2
3
4
5
6
7
[{sasl, [
{sasl_error_logger, false},
{errlog_type, error},
{error_logger_mf_dir, "logs"},
{error_logger_mf_maxbytes, 1073741824}, % 1GB
{error_logger_mf_maxfiles, 10}
]}].

之前一直觉得errlog_type是控制log_mf_h模块的处理事件级别的参数,这边设置的参数是error,为什么info的信息还会记录下来呢?后面看了下sasl.erl模块的代码,errlog_type和log_mf_h模块根本没有关系,然后回头再看了一下sasl的文档:

log_mf_h

This error logger writes all events sent to the error logger to disk. Multiple files and log rotation are used. For efficiency reasons, each event is written as a binary. For more information about this handler, see the STDLIB Reference Manual.

To activate this event handler, three SASL configuration parameters must be set, error_logger_mf_dir, error_logger_mf_maxbytes, and error_logger_mf_maxfiles. The next section provides more information about the configuration parameters.

文档中all已经加黑了,我居然没看到,以后还得好好认真看文档!

使用lager为什么要加入编译选项{parse_transform,lager_transform}

发表于 2017-05-30 | 更新于 2018-05-04 | 分类于 Erlang深入 |

在使用的lager的时候我们需要加入一行编译选项——{parse_transform,lager_transform},或者是在每个使用lager的文件模块的头部加入一行-compile([{parse_transform, lager_transform}]).,这通常会让我们感觉非常的麻烦,但是大家有没有觉得好奇,为什么使用这个参数呢?

首先我们看下Erlang文档,在compile模块中有parse_transform参数的相关说明:

{parse_transform,Module} Causes the parse transformation function Module:parse_transform/2 to be applied to the parsed code before the code is checked for errors.

通过上面的文档我们知道,在编译的时候使用{parse_transform,Module}参数,会使用Module:parse_transform/2函数对代码进行一次解析转换。接下来我们在lager的源代码目录下可以看到lager_transform.erl的代码文件,里面也有一个parse_transform/2的函数。

1
2
3
4
5
6
7
8
9
10
11
parse_transform(AST, Options) ->
TruncSize = proplists:get_value(lager_truncation_size, Options, ?DEFAULT_TRUNCATION),
Enable = proplists:get_value(lager_print_records_flag, Options, true),
Sinks = [lager] ++ proplists:get_value(lager_extra_sinks, Options, []),
put(print_records_flag, Enable),
put(truncation_size, TruncSize),
put(sinks, Sinks),
erlang:put(records, []),
%% .app file should either be in the outdir, or the same dir as the source file
guess_application(proplists:get_value(outdir, Options), hd(AST)),
walk_ast([], AST).

parse_transform/2函数的第一个参数是AST,这个是代码在被编译成二进制前的一种格式The Abstract Format,第二个参数是在编译的时候传入的编译参数,比如要加入一个sink的话不单单要在配置文件里面加入配置,还要在编译参数里面加入{lager_extra_sinks, [audit]},这样parse_transform/2函数才能在proplists:get_value(lager_extra_sinks, Options, [])的时候获得audit这个sink。

顺着代码往下走,我们看到只有调用的函数的模块名是Sinks中之一的才会被解析转换(lists:member(Module, Sinks)),比如lager:info、lager:error、audit:info、audit:error等函数(audit为我们配置的sink)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
walk_body(Acc, []) ->
lists:reverse(Acc);
walk_body(Acc, [H|T]) ->
walk_body([transform_statement(H, get(sinks))|Acc], T).

transform_statement({call, Line, {remote, _Line1, {atom, _Line2, Module},
{atom, _Line3, Function}}, Arguments0} = Stmt,
Sinks) ->
case lists:member(Module, Sinks) of
true ->
case lists:member(Function, ?LEVELS) of
true ->
SinkName = lager_util:make_internal_sink_name(Module),
do_transform(Line, SinkName, Function, Arguments0);
false ->
case lists:keyfind(Function, 1, ?LEVELS_UNSAFE) of
{Function, Severity} ->
SinkName = lager_util:make_internal_sink_name(Module),
do_transform(Line, SinkName, Severity, Arguments0, unsafe);
false ->
Stmt
end
end;
false ->
list_to_tuple(transform_statement(tuple_to_list(Stmt), Sinks))
end;

最后来到解析转换真正起作用的地方,这边的注释写的很清楚,下面的解析转换等于就是lager:dispatch_log/6里面的内容,如果直接调用lager:dispatch_log/6函数的话,是不需要这样的解析转换的,我对此特地问了下lager的开发者,这样做的话能够提高多少的性能,对方给的答复是能快一倍(图 1-1),因为在log不需要输出的情况下就不需要拷贝内容到外部的函数了,个人觉得一次外部函数调用应该费不了多少时间吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
%% Wrap the call to lager:dispatch_log/6 in case that will avoid doing any work if this message is not elegible for logging
%% See lager.erl (lines 89-100) for lager:dispatch_log/6
%% case {whereis(Sink), whereis(?DEFAULT_SINK), lager_config:get({Sink, loglevel}, {?LOG_NONE, []})} of
{'case',Line,
{tuple,Line,
[{call,Line,{atom,Line,whereis},[{atom,Line,SinkName}]},
{call,Line,{atom,Line,whereis},[{atom,Line,?DEFAULT_SINK}]},
{call,Line,
{remote,Line,{atom,Line,lager_config},{atom,Line,get}},
[{tuple,Line,[{atom,Line,SinkName},{atom,Line,loglevel}]},
{tuple,Line,[{integer,Line,0},{nil,Line}]}]}]},
%% {undefined, undefined, _} -> {error, lager_not_running};
[{clause,Line,
[{tuple,Line,
[{atom,Line,undefined},{atom,Line,undefined},{var,Line,'_'}]}],
[],
%% trick the linter into avoiding a 'term constructed but not used' error:
%% (fun() -> {error, lager_not_running} end)()
[{call, Line, {'fun', Line, {clauses, [{clause, Line, [],[], [{tuple, Line, [{atom, Line, error},{atom, Line, lager_not_running}]}]}]}}, []}]
},
%% {undefined, _, _} -> {error, {sink_not_configured, Sink}};
{clause,Line,
[{tuple,Line,
[{atom,Line,undefined},{var,Line,'_'},{var,Line,'_'}]}],
[],
%% same trick as above to avoid linter error
[{call, Line, {'fun', Line, {clauses, [{clause, Line, [],[], [{tuple,Line, [{atom,Line,error}, {tuple,Line,[{atom,Line,sink_not_configured},{atom,Line,SinkName}]}]}]}]}}, []}]
},
%% {SinkPid, _, {Level, Traces}} when ... -> lager:do_log/9;

图 1-1

总结一下,我们平时在用Erlang编程的时候应该不会涉及到自己编写parse_transform函数的需求,这个函数的功能非常强大,可以理解成是一个功能非常强大的宏,但是我觉得编写这个函数的话也会非常容易出错的,看下lager_transform.erl文件里面的代码就知道了。其实不单单lager使用了parse_transform函数的功能,ets也使用了这个功能,由于ets的select和match匹配的可读性实在太差了,所以可以使用ets:fun2ms/1模拟函数的写法来写匹配规则(当然不是真正的函数了,写起来有很多限制的),然后在编译的时候转化成select和match的匹配格式。

1…345
Darcy

Darcy

欢迎来到我的个人站

50 日志
17 分类
40 标签
RSS
GitHub E-Mail
© 2019 Darcy
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Mist v6.4.2