반응형

OS: Ubuntu 16.04

ns-3 version: 3.26


ns-3에서 기존의 리눅스에서 돌아가던 코드를 포팅하는 과정에서, UDP (또는 TCP) 소켓을 열고 패킷을 보내는 코드를 그대로 옮겨 오면서 ns-3 클래스 중 하나인 Ptr<Socket>을 직접 만들어서 패킷을 보내도록 만들어 두었다.


그런데, 이상하게 1초마다 보내는 패킷의 개수를 대폭 늘리기만 하면 (일부러 링크가 saturated 되도록 만들고 경로 변경을 유도하게 만들어야 했다), 시뮬레이션 타임이 끝나기 전에 이유 없이 시뮬레이션이 아주 느려지면서 몇 시간씩 걸리는 것이었다. 에러 메세지는 하나도 없었고, 메모리 문제라도 있었으면 Segmentation fault라도 떠서 디버그를 했을 텐데 그런 문제도 없었고, 실제로 몇 시간씩 걸리더라도 메모리 사용량이 그에 비례해서 늘어나지도 않고 오로지 CPU 사용량만 100%를 찍고 있었다.



결론부터 얘기하자면, 원인은 Socket 오브젝트의 잘못된 사용이었는데, 소켓을 열고 나서 하나도 닫지 않아서 발생한 문제였다. 나는 ns3::Socket 오브젝트를 생성하는 함수를 따로 만들고, 그걸 원하는 조건에서 보내면서 나에게 필요한 통계 처리를 하기 위해서 아래와 같은 간단한 함수를 정의해서 코드 이곳저곳에서 사용하였다.


void sendPacket (Ptr<Socket> socket, Ptr<Packet> packet) {

        socket->Send(packet);

        // 특정 통계 내는 작업

}



이미 언급했듯이 위의 함수에서 결정적인 실수를 하나 했는데, sendPacket 함수를 call 하기 전에 매번 socket을 새로 생성하도록 해 놓고는 잊고 socket을 한 번도 닫지 않은 것이었다. 소켓을 하나만 열고 재활용을 한다면 위의 함수가 문제가 없지만, 매번 소켓을 새로 생성한다면 위의 함수에서 socket->Close(); 를 반드시 추가해야 하는데 그러지 못했었다.


즉, 시뮬레이션 타임이 끝날 때까지 내가 직접 열어 놓은 모든 소켓이 하나도 안 닫히고 끝까지 열려 있었고 시뮬레이션 규모가 작을 때에는 그게 문제가 되지 않다가, 보내는 패킷 개수를 대폭 늘렸더니 에러는 없지만 시뮬레이션을 마치는 데 걸리는 실제 시간이 너무 오래 걸리는 것이었다. 내 컴퓨터에서 40초짜리 point-to-point 네트워크 환경에 대한 시뮬레이션을 돌리는 데 6-7시간이 걸렸다.


처음에 원인을 모를 때에는 내가 file I/O를 너무 많이 열어 놔서 그런가 하는 생각에 특정 로그를 기록하기 위해 열어 놓은 모든 파일 포인터를 다 주석처리해 보기도 했고, 콘솔 화면에 찍히는 메세지를 다 없애 보기도 했고, 랜덤 숫자 생성을 너무 자주 하는 건가 해서 그 부분까지 랜덤을 쓰지 않도록 했는데도 여전히 특정 시간대에서 거의 진행을 하지 못하고 한참을 멈춰 있는 것이었다.


그리고 그 때의 CPU 사용량은 코어 1개를 100% 사용하고 있었는데, 내가 parallel processor (mpi)를 쓰지 않았기 때문에 ns-3가 내부적으로 엄청나게 많은 정체불명의 연산을 하고 있다는 추측밖에 들지 않았다. 그 때에 메모리 사용량이 같이 늘어났다면 내가 뭔가 데이터 구조를 잘못 관리했다거나, 버그가 있어서 데이터가 무한정 늘어난다거나 하는 추측이라도 했을 텐데 메모리 사용량도 늘어나지 않았다.


시뮬레이션 전체 시간 40초 중에서 유독 33초 전까지는 1분도 안돼서 진행을 하다가, 그 뒤부터 갑자기 원인 불명으로 느려지면서 6시간을 소비하는 것이 이상해서, 보내는 패킷의 개수를 줄였더니 전체 시뮬레이션이 1분 만에 잘 끝나는 것이었다.


생각해 보니, 약 15~20초 사이에 4개의 트래픽을 생성하고, 매 초마다 250~300개 정도의 패킷을 보내도록 했더니 33~34초 정도에서 항상 멈추는 것이 확인되었고, 그 중에서 똑같은 링크를 통해서 3개의 트래픽이 동시에 지나가는 영역이 있는데, 시뮬레이션 시간으로 약 33초 정도면 그 링크를 지나가는 패킷이 약 15000~20000개쯤 되는 것 같았다.


그러면 ns-3가 실제 TCP/IP 프로토콜과 transport 작동을 그대로 구현했기 때문에 소켓을 하나 열면 보내는 쪽에서는 50001~65535 중에서 아직 다른 소켓에 bind되지 않은 하나의 랜덤 숫자를 찾아서 소켓을 열고, 패킷을 보내는 방식으로 작동할 것이므로, 15535개 이상의 소켓을 열기 시작하면 bind할 포트 번호를 찾지 못할 것이다.


아...

이런 상황에서 실제 컴퓨터는 소켓이 bind를 못한다던지 하는 오류를 냈을 텐데 ns-3는 아무 오류도 없이 bind될 소켓을 찾느라 계속 기다리기만 했던 것이었다. 그런데 bind할 소켓이 하나도 없었을 텐데 실제 시간으로 6시간쯤 걸려서 결국 패킷을 다 보내기는 했다는 것도 신기하네. Timeout 같은 게 설정되어 있어서 그랬을까?


앞으로는 socket->Send를 쓸 때에는 꼭 socket->Close를 잊지 말아야겠다.



반응형
블로그 이미지

Bryan_

,
반응형

자바에서 DatagramSocket (UDP 소켓)을 파라미터 없이 생성하거나, 포트 번호만 파라미터로 지정하면 특정 네트워크 인터페이스에 지정되는 것이 아니고, 인터페이스에 상관 없이 포트 번호만 맞으면 패킷을 주고 받는 식으로 작동한다.


실제로 인터넷에 검색해 보면 UDP 소켓을 특정한 네트워크 인터페이스에 지정하고자 하는 요구가 상당히 많은 것을 알 수 있다. 하지만 컴퓨터에 네트워크 인터페이스가 여러 개 있을 경우에 UDP 소켓에서 특정 네트워크 인터페이스를 지정하는 것이 Java Virtual Machine (JVM) 입장에서는 자연스럽지 않을 수 있다. "패킷이 컴퓨터 밖으로 나갈 때, 이를 내보낼 가장 적합한 네트워크 인터페이스를 선택한다"는 보편적인 논리에서 보면 이것은 원래 운영체제(OS)에 내장된 라우팅 프로토콜의 역할이기 때문에, User mode에 있는 JVM 위에서 돌아가는 자바 어플리케이션이 이 역할에 일부분 끼어드는 것처럼 보이는 것이다. 아무튼 자바 어플리케이션 입장에서 불가능하지는 않지만, 바람직한 구성은 아닐 수도 있다.


어쨌든 패킷을 받는 서버 입장에서 DatagramSocket을 생성할 때, 생성자(constructor) 파라미터에 특정 IP 주소와 포트 번호를 모두 파라미터로 넘김으로써 특정 네트워크 인터페이스에서만 패킷을 받도록 지정할 수는 있다.


byte[] receiveData = new byte[1024];

String ip = "192.168.3.4"; // 현재 컴퓨터의 특정 네트워크 인터페이스에 할당된 IP 주소

String port = 54321; // 원하는 포트 번호

DatagramSocket socket = new DatagramSocket(new InetSocketAddress(ip, port));

DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);

socket.receive(receivePacket);


패킷을 보내는 클라이언트 입장에서는, 그냥 DatagramPacket을 보낼 때 destination IP 주소를 어떻게 지정하느냐에 따라서 특정 네트워크 인터페이스를 통한 전송 여부가 결정된다. 이것은 운영체제의 라우팅 테이블 설정을 따라간다.


byte[] sendData = new byte[1024];

// sendData에 대한 메세지 생성 작업 수행

String ip = "192.168.3.6";

int port = 54321;

DatagramSocket clientSocket = new DatagramSocket();

DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, InetAddress.getByName(ip), port);

clientSocket.send(sendPacket);


예를 들어, 리눅스의 route 설정이 유선랜 eth0과 무선랜 wlan0 모두 활성화되어 있고, default (0.0.0.0)에 대한 게이트웨이가 유선랜(eth0) 게이트웨이로 설정되어 있다고 가정하자. 이 때, 로컬 네트워크 밖에 있는 인터넷 상의 어떤 기기에 UDP 패킷을 보내고자 한다면, 자바 어플리케이션은 eth0을 통해서 패킷을 보낼 것이다. 이런 경우에 반드시 무선랜 wlan0으로 패킷이 나가도록 하고 싶으면 리눅스에서 route 명령으로 wlan0을 통해서 인터넷으로 나가는 경로를 설정해 주어야 한다.


문제는 서버의 입장에서 DatagramSocket에 특정 IP 주소를 지정해 버리면, 브로드캐스트(broadcast) 메세지를 받지 못하는 문제가 생긴다. 상대편에서 정확하게 IP 주소를 지정해서 보내는 경우에만 패킷을 받을 수 있고, 테스트해 본 결과, 가령 192.168.3.255 와 같은 destination IP로 전송되는 패킷은 받지 못했다.


이 경우에는 서버의 입장에서는 특정한 IP 주소를 지정하지 말고 모든 인터페이스와 모든 주소에 대해서 받을 수 있도록 해 두고, 일단 패킷을 받고 나서 전송자의 IP 주소 대역을 보고 판단하는 루틴을 코드에 추가하는 것이 좋을 것 같다. (이것이 best solution인지는 알 수 없으나, 현재로써는 이것이 working solution이다.)


byte[] receiveData = new byte[1024];

String port = 54321; // 원하는 포트 번호

DatagramSocket socket = new DatagramSocket(port);

DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);

socket.receive(receivePacket);




반응형
블로그 이미지

Bryan_

,