epgm在不同镜像的容器中,无法同时绑定地址的问题

问题背景

两个不同镜像,记为A,B镜像,A镜像生成两个的容器A1,A2,B镜像生成B1,B2。使用zmq组件的,epgm方式进行通信,需要先进行地址绑定。表示该服务加入某个组播地址,订阅该组播的消息。

问题表现:

1、直接在A1,A2同时跑这段代码,没有问题。都能成功绑定组播地址。

2、直接在B1,B2同时跑这段代码,也是没有问题,都能成功绑定组播地址。

3、直接在A1上运行这段代码,同时也在B1上运行,后启动的会报错退出,对应的错误是端口绑定失败,被占用。errno为98,表示地址被占用。

问题初步分析


直接先上一段代码

#include <zmq.hpp>
#include <iostream>
#include <sstream>
#include <exception>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
usingnamespace std;
intmain (intargc, char*argv[])
{
    try{
        zmq::context_t context (1);
        zmq::socket_t subscriber (context, ZMQ_SUB);
        intrate = 10000;
        subscriber.setsockopt(ZMQ_RATE, &rate, sizeof(rate));
        subscriber.setsockopt(ZMQ_SUBSCRIBE,"", 0);
        inthwm = 0;
        subscriber.setsockopt(ZMQ_RCVHWM, &hwm, sizeof(hwm));
        //这里表示加入组播,订阅该组播的消息
        subscriber.connect("epgm://127.0.0.1;239.192.1.1:5555");
        intupdate_nbr;
        for(;;){
            zmq::message_t update;
            cout << "before recv" << endl;
            subscriber.recv(&update);
            cout << "recv data = " << (char*)update.data() << endl;
            cout << "data size = " << update.size() << endl;
            std::cout << std::endl << std::endl;;
        }
    }catch(exception& e) {
        cerr << "exception occur: " << e.what() << endl;
    }
    return0;
}


扫描二维码关注公众号,回复: 1926032 查看本文章

环境说明:

1、A镜像的OS是CentOS release 6.6, gcc version 4.4.7。

2、B镜像的OS是Debian GNU/Linux 7, g++ (Debian 4.7.2-5) 4.7.2

3、装的libpgm版本为libpgm-5.1.118~dfsg, zmq版本为zeromq-4.2.1

4、容器启动网络配置用的都是host模式。

 


可能怀疑的方向:

1、因为是在容器上跑,所以一上来先分析是不是网络模式有问题。经过排查和找资料,host模式并不存在什么问题。

2、因为对docker容器的底层不了解,则怀疑是不是同一个镜像的不同容器之间,是不是有某些特殊的关系。网上查找资料,无果。

3、或者是不是因为不同容器是不同的OS,所以不能这么干呢?理论上就是行不通的?

4、想这么多也没用,直接跟到代码里面去看,最终发现,失败是在一个系统调用bind上。函数原型为int bind(int socket, const struct sockaddr *address,socklen_t address_len);

想到了是不是送进去的参数不同导致绑定失败。则分别在GDB上去跟进,一次能成功重复绑定的时候,对应的参数,与一次失败重复绑定的时候,是不是有区别,进而根据这些区别找问题所在。

 

问题深入分析

好像问题到此已经被卡住了,无法再往下走了。此时,眼前只有一条路,源码。幸好用的libpgm我们手上是有源码的,可以对照源码来分析问题。先静下心来,理理头绪。

回到最初的一些基本概念。

组播、linux,Lo回环地址,把《unix网络编程》翻出来,重新理了一下思路。

重新用GDB跟下基本逻辑,再加上自己对源码的解读,搞清楚了问题的本质:

我们对127.0.0.1的本地回环地址的绑定,本身是不支持组播的,epgm是模拟出来的,它是如何模拟,它是直接对INADD_ANY上进行绑定一个对应的端口。例如:即是我们在Lo上如果绑定一个epgm地址为

epgm://127.0.0.1;239.192.1.1:5555,则它是直接在本机绑定到0.0.0.0:5555上面。

通过netstat可以验证自己的相法。如下图所示:

那么问题就可以有个简单的思路了,尝试验证一下,在A1与B1之间,是否可以进行0.0.0.0:5555这样的绑定了

 

理论猜想验证

还是先上一段代码,验证在不同镜像的不同容器之间,是否可以这样重复绑定UDP端口。

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <fcntl.h>
externinterrno;
usingnamespacestd;
intmain(intargc,char** argv) {
    intufd_listener = socket(PF_INET,  SOCK_DGRAM, 0);
    intopt = 1;
    if(setsockopt(ufd_listener,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)) < 0) {
        cerr << "error set SO_REUSEADDR" << endl;
        return-1;
    }
    structsockaddr_in my_addr;
    bzero(&my_addr,sizeof(my_addr));
    my_addr.sin_family = AF_INET;
    my_addr.sin_port = htons(4000);
    my_addr.sin_addr.s_addr = INADDR_ANY;
    if(bind(ufd_listener, (structsockaddr *) &my_addr, sizeof(structsockaddr)) == -1)  {
        cerr << "bind error, errno = " << errno<< endl;
    }
    do{
        sleep(1);
        cout << "sleep 1" << endl;
    }while(true);
    return0;
}


结论是:可以在不同镜像的不同容器之间绑定相同的地址。

使用关键技术点是对SO_REUSEADDR,具体可以进行百度找相关技术说明。

继续寻找问题

上一步中,已经证明了不同的镜像的不同容器之中,是可以进行绑定相同端口,这条路理论上是可以走得通的。

再回到源码中,把epgm的库的日志打开,再围绕问题进行代码阅读。

直接尝试在bind系统调用之前,对相应的socket值,进行强制设置为SO_REUSEADDR,再对libpgm和zmq,在两个容器A1,B1进行重新安装。

神奇的事情发生了,A1与B1能够同时跑起代码1了。

真相露出

现在算是能解决了两个不同镜像的不同容器在lo上绑定相同组播地址的问题了,还剩下1个疑问:

1、如果A1与A2之间没有设置SO_REUSEADDR,那他们是怎么能一起绑定成功呢?

还是再次深入阅读代码,在关键的地方,发现了另一个可能共存属性值的设置叫,SO_REUSEPORT,这个是在linux内核3.9之后加入的。马上写代码验证,如果只设置SO_REUSEPORT,是不是也可以共存?结论果然如此。

那进一步怀疑,会不会两个镜像,由于OS版本不同,导致一个设置了SO_REUSEPORT, 另一个设置了SO_REUSEPORT,他们各自是能够绑定相同地址,但互相不能重复绑定。

即是:A镜像设置的是SO_REUSEPORT,A1,A2可以绑定相同的地址。B镜像设置的是SO_REUSEADDR,B1,B2可以绑定相同的地址。但A1与B1无法绑定相同的地址。

求证:在libpgm的socket.c中,找到如下代码,

并使用getsockopt,在bind之前,分别在两个容器,A1,B1中去获取SO_REUSEADDR和SO_REUSEPORT的标志。得到最终真相。

A镜像中,由于是比较新的内核,他定义了SO_REUSEPORT,则他只有SO_REUSEPORT被设置了。

B镜像中,由于可能比较旧的内核,他没有定义SO_REUSEPORT,则他只有SO_REUSEADDR被设置了。

对于SO_REUSEPORT和SO_REUSEADDR的区别,可以参考这个链接http://blog.qiusuo.im/blog/2014/09/14/linux-so-reuseport/

所以就解释了为什么A1,A2可以绑定相同,B1,B2也能绑定相同地址,但A1,B1就无法绑定相同地址的原因了。


猜你喜欢

转载自blog.csdn.net/dreamvyps/article/details/79151699