转自霸爷的博客:

转载自系统技术非业余研究

本文链接地址: Erlang节点重启导致的incarnation问题

遇到个问题,

=ERROR REPORT==== 10-Mar-2016::09:44:07 ===
Discarding message {'$gen_cast',close_be_covered} from <0.771.0> to <0.774.0> in an old incarnation (2) of this node (3)

网上查阅了霸爷的博客,对该问题脑洞大开.

今天晚上mingchaoyan同学在线上问以下这个问题:

152489 =ERROR REPORT==== 2013-06-28 19:57:53 ===
152490 Discarding message {send,<<19 bytes>>} from <0.86.1> to <0.6743.0> in an old incarnation (1 ) of this node (2)
152491
152492
152493 =ERROR REPORT==== 2013-06-28 19:57:55 ===
152494 Discarding message {send,<<22 bytes>>} from <0.1623.1> to <0.6743.0> in an old incarnation (1) of this node (2

我们中午服务器更新后,日志上满屏的这些错误,请问您有遇到过类似的错误吗?或者提过些定位问题,解决问题的思路,谢谢

这个问题有点意思,从日志提示来再结合源码来看,马上我们就可以找到打出这个提示的地方:

/*bif.c*/
Sint
do_send(Process *p, Eterm to, Eterm msg, int suspend) {
    Eterm portid;
...
else if (is_external_pid(to)) {
        dep = external_pid_dist_entry(to);
        if(dep == erts_this_dist_entry) {
            erts_dsprintf_buf_t *dsbufp = erts_create_logger_dsbuf();
            erts_dsprintf(dsbufp,
                          "Discarding message %T from %T to %T in an old "
                          "incarnation (%d) of this node (%d)\n",
                          msg,
                          p->id,
                          to,
                          external_pid_creation(to),
                          erts_this_node->creation);
            erts_send_error_to_logger(p->group_leader, dsbufp);
            return 0;
        }
..
}

触发这句警告提示必须满足以下条件:
1. 目标Pid必须是external_pid。
2. 该pid归宿的外部节点所对应的dist_entry和当前节点的dist_entry相同。

通过google引擎,我找到了和这个描述很相近的问题:参见 这里 ,该作者很好的描述和重现了这个现象,但是他没有解释出具体的原因。

好,那我们顺着他的路子来重新下这个问题.
但演示之前,我们先巩固下基础,首先需要明白pid的格式:
可以参见这篇文章:

pid的核心内容摘抄如下:

Printed process ids < A.B.C > are composed of [6]:
A, the node number (0 is the local node, an arbitrary number for a remote node)
B, the first 15 bits of the process number (an index into the process table) [7]
C, bits 16-18 of the process number (the same process number as B) [7]

再参见Erlang External Term Format 文档的章节9.10
描述了PID_EXT的组成:

1 N 4 4 1
103 Node ID Serial Creation
Table 9.16:
Encode a process identifier object (obtained from spawn/3 or friends). The ID and Creation fields works just like in REFERENCE_EXT, while the Serial field is used to improve safety. In ID, only 15 bits are significant; the rest should be 0.

我们可以看到一个字段 Creation, 这个东西我们之前怎么没见过呢?
参考erlang的文档 我们可以知道:

creation
Returns the creation of the local node as an integer. The creation is changed when a node is restarted. The creation of a node is stored in process identifiers, port identifiers, and references. This makes it (to some extent) possible to distinguish between identifiers from different incarnations of a node. Currently valid creations are integers in the range 1..3, but this may (probably will) change in the future. If the node is not alive, 0 is returned.

追踪这个creation的来源,我们知道这个变量来自epmd. 具体点的描述就是每次节点都会像epmd注册名字,epmd会给节点返回这个creation. net_kernel会把这个creation通过set_node这个bif登记到该节点的erts_this_dist_entry->creation中去:

/* erl_node_tables.c */
void
erts_set_this_node(Eterm sysname, Uint creation)
{
...
    erts_this_dist_entry->sysname = sysname;
    erts_this_dist_entry->creation = creation;
...
}
 
/*epmd_srv.c  */
...
        /* When reusing we change the "creation" number 1..3 */
 
        node->creation = node->creation % 3 + 1;
...

从上面的代码可以看出creation取值是1-3,每次登记的时候+1. 未联网的节点creation为0.

知道了createion的来龙去脉后,我们再看下DistEntry的数据结构,这个数据结构基本上代表了联网的节点和外面世界的交互。

typedef struct dist_entry_ {

Eterm sysname; /* name@host atom for efficiency */
Uint32 creation; /* creation of connected node */
Eterm cid; /* connection handler (pid or port), NIL == free

} DistEntry;
其中最重要的信息有上面3个,其中cid代表port(节点之间的TCP通道).

我们知道外部pid是通过binary_to_term来构造的, 代码位于external.c:dec_pid函数。

static byte*
dec_pid(ErtsDistExternal *edep, Eterm** hpp, byte* ep, ErlOffHeap* off_heap, Eterm* objp)
{
 ...
    /*                                                                                                                   
     * We are careful to create the node entry only after all                                                            
     * validity tests are done.                                                                                          
     */
    node = dec_get_node(sysname, cre);
 
    if(node == erts_this_node) {
        *objp = make_internal_pid(data);
    else {
        ExternalThing *etp = (ExternalThing *) *hpp;
        *hpp += EXTERNAL_THING_HEAD_SIZE + 1;
 
        etp->header = make_external_pid_header(1);
        etp->next = off_heap->first;
        etp->node = node;
        etp->data.ui[0] = data;
 
        off_heap->first = (struct erl_off_heap_header*) etp;
        *objp = make_external_pid(etp);
    }
...
}
static ERTS_INLINE ErlNode* dec_get_node(Eterm sysname, Uint creation)
{
    switch (creation) {
    case INTERNAL_CREATION:
        return erts_this_node;
    case ORIG_CREATION:
        if (sysname == erts_this_node->sysname) {
            creation = erts_this_node->creation;
        }
    }
    return erts_find_or_insert_node(sysname,creation);
}

如果creation等0的话,肯定是本地节点,否则根据sysname和creation来找到一个匹配的节点。
继续上代码:

typedef struct erl_node_ {
  HashBucket hash_bucket;       /* Hash bucket */
  erts_refc_t refc;             /* Reference count */
  Eterm sysname;                /* name@host atom for efficiency */
  Uint32 creation;              /* Creation */
  DistEntry *dist_entry;        /* Corresponding dist entry */
} ErlNode;
 
/* erl_node_tables.c */
ErlNode *erts_find_or_insert_node(Eterm sysname, Uint creation)
{   
    ErlNode *res;
    ErlNode ne;
    ne.sysname = sysname;
    ne.creation = creation;
 
    erts_smp_rwmtx_rlock(&erts_node_table_rwmtx);
    res = hash_get(&erts_node_table, (void *) &ne);
    if (res && res != erts_this_node) {
        erts_aint_t refc = erts_refc_inctest(&res->refc, 0);
        if (refc < 2) /* New or pending delete */
            erts_refc_inc(&res->refc, 1);
    }
    erts_smp_rwmtx_runlock(&erts_node_table_rwmtx);
    if (res)
        return res;
 
    erts_smp_rwmtx_rwlock(&erts_node_table_rwmtx);
    res = hash_put(&erts_node_table, (void *) &ne);
    ASSERT(res);
    if (res != erts_this_node) {
        erts_aint_t refc = erts_refc_inctest(&res->refc, 0);
        if (refc < 2) /* New or pending delete */
            erts_refc_inc(&res->refc, 1);
    }
    erts_smp_rwmtx_rwunlock(&erts_node_table_rwmtx);
    return res; 
}
 
static int
node_table_cmp(void *venp1, void *venp2)
{
    return ((((ErlNode *) venp1)->sysname == ((ErlNode *) venp2)->sysname
             && ((ErlNode *) venp1)->creation == ((ErlNode *) venp2)->creation)
            ? 0
            : 1);
}
 
static void*
node_table_alloc(void *venp_tmpl)
{
    ErlNode *enp;
 
    if(((ErlNode *) venp_tmpl) == erts_this_node)
        return venp_tmpl;
 
    enp = (ErlNode *) erts_alloc(ERTS_ALC_T_NODE_ENTRY, sizeof(ErlNode));
 
    node_entries++;
 
    erts_refc_init(&enp->refc, -1);
    enp->creation = ((ErlNode *) venp_tmpl)->creation;
    enp->sysname = ((ErlNode *) venp_tmpl)->sysname;
    enp->dist_entry = erts_find_or_insert_dist_entry(((ErlNode *) venp_tmpl)->sysname);
 
    return (void *) enp;
}

这个erts_find_or_insert_node会根据sysname和creation的组合来查找节点,如果找不到的话,会新建一个节点放入ErlNode类型的erts_node_table表中。而ErlNode有3个关键信息 1. sysname 2. creation 3. dist_entry。 新建一个节点的时候,dist_entry填什么呢?

核心代码是这行:
enp->dist_entry = erts_find_or_insert_dist_entry(((ErlNode *) venp_tmpl)->sysname);
这个dist_entry是根据sysname查找到的,而不是依据sysname和creation的组合。

这时候问题就来了, 我们仔细看下 dec_pid的代码:

node = dec_get_node(sysname, cre);
if(node == erts_this_node) {
*objp = make_internal_pid(data);
} else {

etp->node = node;

*objp = make_external_pid(etp);
}

由于creation不同,所以相同的sysname, 无法找到目前的节点。在新建的节点里面,它的dist_entry却是当前节点对应的dist_entry.
创建出来的外部pid对象包含新建的node。

所以send的时候出警告的三句代码:

} else if (is_external_pid(to)) {
dep = external_pid_dist_entry(to);
if(dep == erts_this_dist_entry) {

external_pid_dist_entry宏会从外部pid中取出node,再从node中取出dist_entry. 这个dist_entry很不幸的和erts_this_dist_entry相同,于是就有了上面的悲剧。

分析了半天总算有眉目了,喝口水先!
现在有了这些背景知识我们就可以演示了:

$ erl -sname a
Erlang R15B03 (erts-5.9.3.1) 1 [64-bit] [smp:16:16] [async-threads:0] [hipe] [kernel-poll:false]
 
Eshell V5.9.3.1  (abort with ^G)
(a@rds064076)1> term_to_binary(self()).
<<131,103,100,0,11,97,64,114,100,115,48,54,52,48,55,54,0,
  0,0,37,0,0,0,0,2>>
(a@rds064076)2> binary_to_term(<<131,103,100,0,11,97,64,114,100,115,48,54,52,48,55,54,0,0,0,37,0,0,0,0,2>>).
<0.37.0>
(a@rds064076)3> binary_to_term(<<131,103,100,0,11,97,64,114,100,115,48,54,52,48,55,54,0,0,0,37,0,0,0,0,3>>).
<0.37.0>
(a@rds064076)4> binary_to_term(<<131,103,100,0,11,97,64,114,100,115,48,54,52,48,55,54,0,0,0,37,0,0,0,0,1>>).
<0.37.0>
(a@rds064076)5> binary_to_term(<<131,103,100,0,11,97,64,114,100,115,48,54,52,48,55,54,0,0,0,37,0,0,0,0,1>>)==self().
false
(a@rds064076)6> binary_to_term(<<131,103,100,0,11,97,64,114,100,115,48,54,52,48,55,54,0,0,0,37,0,0,0,0,2>>)==self().
true
(a@rds064076)7> binary_to_term(<<131,103,100,0,11,97,64,114,100,115,48,54,52,48,55,54,0,0,0,37,0,0,0,0,3>>)==self().
false
(a@rds064076)8> binary_to_term(<<131,103,100,0,11,97,64,114,100,115,48,54,52,48,55,54,0,0,0,37,0,0,0,0,3>>)!ok.    
ok
(a@rds064076)9> erlang:system_info(creation).
2
=ERROR REPORT==== 28-Jun-2013::23:10:58 ===
Discarding message ok from <0.37.0> to <0.37.0> in an old incarnation (3) of this node (2)

上面的演示我们可以看出,creation确实是每次+1循环,同时虽然pid打出来的是一样的,但是实际上由于creation的存在,看起来一样的还是不同的pid.
到这里,我们大概明白了前应后果。但是并没有回到上面同学的疑问。
他的集群,只是重新启动了个节点,然后收到一屏幕的警告。
注意是一屏!!!

我重新设计了一个案例,在深度剖析这个问题:
在这之前,我们需要以下程序:

$ cat test.erl
-module(test).
-export([start/0]).
 
start()->
  register(test, self()),
  loop(undefined).
 
loop(State)->
   loop(
  receive
   {set, Msg} -> Msg;
   {get, From} -> From!State
   end
   ).

这段代码的目的是:
test:start进程启动起来后,会在目标节点上把自己登记为test名字,同时可以接受2中消息get和set。set会保持用户设置的信息,而get会取回消息。

我们的测试案例是这样的:
启动a,b节点,然后在b节点上通过spawn在a节点上启动test:start这个进程负责保存我们的信息。这个信息就是b进程的shell的进程pid.
然后模拟b节点挂掉重新启动,通过a节点上的test进程取回上次保持的进程pid, 这个pid和新启动的shell pid是相同的,但是他们应该是不完全相同的,因为creation不一样。
好了,交代清楚了,我们就来秀下:

$ erl -name a@127.0.0.1
Erlang R15B03 (erts-5.9.3.1) 1 [64-bit] [smp:16:16] [async-threads:0] [hipe] [kernel-poll:false]
 
Eshell V5.9.3.1  (abort with ^G)
(a@127.0.0.1)1>

好,A节点准备好了,接下来启动B节点保存shell的进程pid到节点a去。

$ erl -name b@127.0.0.1
Erlang R15B03 (erts-5.9.3.1) 1 [64-bit] [smp:16:16] [async-threads:0] [hipe] [kernel-poll:false]
 
Eshell V5.9.3.1  (abort with ^G)
(b@127.0.0.1)1> R=spawn('a@127.0.0.1'test, start,[]).
<6002.42.0>
(b@127.0.0.1)2> self().
<0.37.0>
(b@127.0.0.1)3> R!{set, self()}.  
{set,<0.37.0>}
(b@127.0.0.1)4> R!{get, self()}.
{get,<0.37.0>}
(b@127.0.0.1)5> flush().
Shell got <0.37.0>
ok
(b@127.0.0.1)6>
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution
^C

这时候把节点b退出,模拟b挂掉,再重新启动b,取回之前保存的pid,和现有的shell pid对比,发现不是完全一样。

$ erl -name b@127.0.0.1
Erlang R15B03 (erts-5.9.3.1) [64-bit] [smp:16:16] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.9.3.1 (abort with ^G)
(b@127.0.0.1)1> {test, ‘a@127.0.0.1’}!{get, self()}.
{get,<0.37.0>}
(b@127.0.0.1)2> flush().
Shell got <0.37.0>
ok
(b@127.0.0.1)3> {test, ‘a@127.0.0.1’}!{get, self()}, receive X->X end.
<0.37.0>
(b@127.0.0.1)4> T=v(-1).
<0.37.0>
(b@127.0.0.1)5> T==self().
false
(b@127.0.0.1)6> T!ok.
ok
(b@127.0.0.1)7>
=ERROR REPORT==== 28-Jun-2013::23:24:00 ===
Discarding message ok from <0.37.0> to <0.37.0> in an old incarnation (2) of this node (3)
[/erlang]
我们发消息给取回的上次保持的pid, 就触发了警告。

这个场景在分布式环境里面非常普遍,参与协作的进程会保持在其他节点的系统里面,当其中的一些进程挂掉重新启动的时候,试图取回这些进程id的时候,却发现这些id已经失效了。

到这里为止,应该能够很好的回答了上面同学的问题了。

这个问题的解决方案是什么呢?
我们的系统应该去monitor_node其他相关节点并且去捕获nodedown消息,当节点失效的时候,适时移除掉和该节点相关的进程。 因为这些进程本质上已经失去功效了。

小结:看起来再无辜的警告,也是会隐藏着重大的问题。

祝玩得开心。

Erlang节点重启导致的incarnation问题(转)的更多相关文章

  1. elasticsearch之节点重启

    Elasticsearch节点重启时背后发生的故事有哪些,应该注意哪些配置内容,本篇文章做一个简单的探讨. 节点离开 在elasticsearch集群中,假设NodeA因为种种原因退出集群,在Node ...

  2. 11gR2 如何诊断节点重启问题

    本文对如何诊断11gR2 GI环境下的节点重启问题进行了一些介绍. 首先,像10g版本一样,我们首先介绍在GI中能够导致节点重启的进程.1.Ocssd.bin:这个进程的功能和10g版本的功能基本差不 ...

  3. Oracle教程:如何诊断节点重启问题(转载)

    本文对如何诊断RAC环境中节点重启问题进行了介绍.适用于10gR2和11gR1. 首先我们对能够导致节点重启的CRS进程进行介绍.1.ocssd : 它的主要功能是节点监控(Node Monitori ...

  4. Erlang中如何在同一台机器上运行多个erlang节点?

    首先打开shell,然后在打开cmd输入:erl -sname bilbo  这样就启动了一个gandal的erlang节点. 如图:

  5. rac 11g_第二个节点重启后无法启动实例:磁盘组dismount问题

    原创作品,出自 "深蓝的blog" 博客,欢迎转载,转载时请务必注明以下出处,否则追究版权法律责任. 深蓝的blog:http://blog.csdn.net/huangyanlo ...

  6. erlang节点互相ping,一个能ping通,另外一个不行。

    今天发现一个问题,2个erlang节点,1个主动ping另外一个不通,然后等待另外一个ping过来,2个节点才连通.记录一下. 首先,erlang节点的cookie是一致的.查了文档,cookie一致 ...

  7. erlang节点互通查看

    在局域网内部,一般用短节点名来完成短节点的全联通.     全联通的前提之一是cookie要相同,cookie记录在一个文件中.     对于同一个物理机上的两个erlang节点,不用其他配置就可以全 ...

  8. Hadoop 分布式环境slave节点重启忽然不好使了

    Hadoop 分布式环境slaves节点重启: 忽然无法启动DataNode和NodeManager处理: 在master节点: vim /etc/hosts: 修改slave 节点的IP (这个时候 ...

  9. Linux由于物理节点故障导致的异常重启-Case1

    问题描述:Linux VM异常重启,需要排查问题原因 排查结果: 查询Messages日志获取到的信息 虚拟机内核版本: Jun :: test01 kernel: Linux version -.e ...

随机推荐

  1. The type or namespace name 'Html' does not exist in the namespace 'System.Web.Mvc' (are you missing an assembly reference?)

    The type or namespace name 'Html' does not exist in the namespace 'System.Web.Mvc' (are you missing ...

  2. Docker(一):什么是docker

    Docker 是一个开源项目,诞生于 2013 年初,最初是 dotCloud 公司内部的一个业余项目.它基于 Google 公司推出的 Go 语言实现. 项目后来加入了 Linux 基金会,遵从了 ...

  3. Python学习杂记_6_字典常用操作

    字典操作 字典是由一对花括号括起来的一组“键值对”,每个键值对就是字典的一个元素,元素在字典中是无序的,常见操作如下: info = { 'name':'xiaoming', 'sex':'nan', ...

  4. AC日记——[ZJOI2008]树的统计Count bzoj 1036

    1036: [ZJOI2008]树的统计Count Time Limit: 10 Sec  Memory Limit: 162 MBSubmit: 15007  Solved: 6092[Submit ...

  5. HYSBZ 2818 Gcd【欧拉函数/莫比乌斯】

    I - Gcd HYSBZ - 2818 给定整数N,求1<=x,y<=N且Gcd(x,y)为素数的数对(x,y)有多少对. Input 一个整数N Output 如题 Sample In ...

  6. HDU 1060 Leftmost Digit【log10/求N^N的最高位数字是多少】

    Leftmost Digit Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)To ...

  7. Install Ruby on Rails on Ubuntu 12.04 LTS

    1:Update package repository. sudo apt-get update 2:Install git and Curl.     Git:是一个简单,快速,高效的版本控制系统. ...

  8. Jenkins强制设置语言为中文

    解决方法如下: 1.使用中文版的google浏览器,并确定把语言设置成了中文.语言配置在设置页.如下: 2.直接设置jenkins的语言.强制性. 前提:先安装插件:Locale plugin 配置如 ...

  9. 基于Tiny4412的I2C驱动分析

    本文以tiny4412平台上到三轴加速度器为例简单分析了Linux下到i2c驱动编程. http://pan.baidu.com/s/1c0H5vRq

  10. 【spring boot】6.idea下springboot打包成jar包和war包,并且可以在外部tomcat下运行访问到

    接着上一章走呗:http://www.cnblogs.com/sxdcgaq8080/p/7712874.html 然后声明一点,下面打包的过程中,scope一直都是使用默认的范围 <!--用于 ...