netty 4.1.23.Final ver사용중입니다. idletStateHandler 가 동작을 안하는 경우도 있나요?

139 views
Skip to first unread message

박세진

unread,
Jul 6, 2021, 9:58:05 PM7/6/21
to Netty Korean User Group
멍청한 질문일수도 있어서 죄송합니다만
제가 생각했을때 예상되는 원인인것같아 이렇게 질문 올립니다.

현재 네티를 이용해서 다수의 서버(현장장비)에
클라이언트로 1:N 접속을 하여 
TCP 통신으로 데이터를 수신받는부분을 처리하고있습니다.

서버와 연결되어있는 상태에 따라서 망 상태를 불량으로 인지하고 재접속하기 위해서
매 5초마다 서버에 응답요청패킷을 보냅니다.
그리고 readIdletime 을 60 으로 설정하여 (all, write 는 0)
망 단절이 일어난 경우 채널을 통해 수신받는 데이터가 없는경우,
재접속하도록 구현하였습니다.


하지만 현장 장비에 대해서 응답받는 패킷이 없는지
decode메소드가 동작하지 않지만 
(수신받을때마다 무조건적으로 출력하는 로그로 판별했습니다.)

IdlestateHandler 의 channelIdle 메소드가 동작하지 않아 연결을 종료하지 않았고,
연결을 종료하지않았으므로 재연결이 되지 않았습니다.

현장서버에서 netstat 명령어로 확인해봐도,
사무실에서 현장장비에 붙여서 확인해봐도 동일한 증상이 지속적으로 발견되서
방법을 구글링해도 확인하기 어려워 질문드립니다.

IdlestateHandler 구현부분은 다음과 같습니다.
@Override
protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception 
{
String PisID = pisManager.getPISCtlrNmbrByIp(NettyHelper.getRemoteAddress(ctx.channel()));
MDC.put("PIS", PisID);
logger.info("PIS ID : {} | channelIdle | IdleStateEvent : {} " , PisID , evt.state() );
try {
if(evt.state() == IdleState.READER_IDLE) 
{
//특정 개소에서 간혹 동작하지 않는 부분입니다...
logger.info("Terminate Reader idle channel | 읽기 대기시간 초과 | PIS ID : {} | #Remote[{}:{}:{}]", 
PisID, NettyHelper.getRemoteAddress(ctx.channel()), NettyHelper.getRemotePort(ctx.channel()), ctx.toString());
pisManager.getPISTemplateById( PisID ).getRecentStatus().setCMNC_STTS_CD(PISConStatus.Communication_DisConnect);
logger.info("Terminate Reader idle channel before");

ctx.channel().close();
// ctx.channel().disconnect();
// super.channelIdle(ctx, evt);
// ctx.close();
logger.info("Terminate Reader idle channel ctx.channel().close");
// super.channelIdle(ctx, evt);
}
else if (evt.state() == IdleState.ALL_IDLE ) 
{
logger.info("Terminate All idle channel | PIS ID : {} | #Remote[{}:{}:{}]", 
PisID, NettyHelper.getRemoteAddress(ctx.channel()), NettyHelper.getRemotePort(ctx.channel()), ctx.toString());
pisManager.getPISTemplateById(PisID).getRecentStatus().setCMNC_STTS_CD(PISConStatus.Communication_DisConnect);

logger.info("Terminate All idle channel");

ctx.channel().close();
// ctx.channel().disconnect().channel().close();
logger.info("Terminate All idle channel ctx.channel().close");
// super.channelIdle(ctx, evt);
}
else if(evt.state() == IdleState.WRITER_IDLE) 
{
logger.info("Terminate Writer idle channel | 쓰기 대기시간 초과 | PIS ID : {} | #Remote[{}:{}:{}]", 
PisID, NettyHelper.getRemoteAddress(ctx.channel()), NettyHelper.getRemotePort(ctx.channel()), ctx.toString());

logger.info("Terminate Writer idle channel before");
// super.channelIdle(ctx, evt);
ctx.channel().close();
// ctx.channel().disconnect();
logger.info("Terminate Writer idle channel ctx.channel().close");
// super.channelIdle(ctx, evt);
}
}
catch(Exception e) 
{
logger.error( "channelIdle | 작동중 예외 발생 | Exception log : {} " , LogHelper.getPrintStackTrace(e) );
}
finally 
{
MDC.remove("PIS");

super.channelIdle(ctx, evt);
}
}


응답 요청 패킷을 1분으로 설정하고 응답대기시간을 5초로 설정하면 작동하긴 하지만
정상적인 동작을 위해서 그렇게 설정하지 않았습니다...
사용하는 네티 버전은 4.1.23.Final 입니다.


재접속 부분은
클라이언트 객체 생성시

clientChannel = connectChannelFuture
.addListener(new ConnectFutureListener(this)).sync().channel().closeFuture()
.addListener(new CloseFutureListener(this) ).sync().channel();
위와 같이 리스너를 이용해서 구현했습니다.
sync() 사용시 연결 재접속시 block Exception 이라는게 발생하는걸 확인하고
종료시 channel 연결 종료 상태에 따라 재접속 하도록 하였습니다.
스택 오버 플로에서 sync() 를 사용하지 않는것을 권장하는 글을 보긴 했지만
재접속 시도할 대기시간이 짧을수록 서버에 대한 접속 시도가 무한정 늘어나는것을 확인해서 sync()를 사용하였습니다...
------------------------------------------------------------------------------------------------------------------------------------

접속 리스너 클래스는
logger.info("연결 시도");
if(channelFuture.isCancelled()) {
logger.warn("Future Canceled | Connection Future | Retry Connect - {} | {}",channelFuture.channel().toString(), LogHelper.getPrintStackTrace(channelFuture.cause()));
channelFuture.channel().eventLoop().schedule(nettyClient, nettyClient.getReconnectTime(), TimeUnit.SECONDS); 
}
else if (!channelFuture.isSuccess()) {
logger.warn("Connect Fail | Connection Future | Retry Connect - {} | {}",channelFuture.channel().toString(), LogHelper.getPrintStackTrace(channelFuture.cause()));
channelFuture.channel().eventLoop().schedule(nettyClient, nettyClient.getReconnectTime(), TimeUnit.SECONDS); 
}
else if(!channelFuture.channel().isActive()) {
logger.warn("Channel In-Active | Connection Future | Retry Connect - {} | {}",channelFuture.channel().toString(), LogHelper.getPrintStackTrace(channelFuture.cause()));
channelFuture.channel().eventLoop().schedule(nettyClient, nettyClient.getReconnectTime(), TimeUnit.SECONDS);
}
}
catch(NullPointerException e) 
logger.error("접속시 예외처리. 접속 예외 | NullPointerException | Exception Log : {} " ,  LogHelper.getPrintStackTrace(e) );
// channelFuture.channel().eventLoop().schedule(nettyClient,  nettyClient.getReconnectTime(), TimeUnit.SECONDS);
catch(Exception e) 
{
logger.error("접속시도중 예외처리 | EXCEPTION | Exception Log : {} " ,  LogHelper.getPrintStackTrace(e) );
// channelFuture.channel().eventLoop().schedule(nettyClient,  nettyClient.getReconnectTime(), TimeUnit.SECONDS);
}finally { 
}

------------------------------------------------------------------------------------------------------------------------------------
연결 종료 리스너 클래스는는 
try 
{  
StringChannelStatus(channelFuture); 
if(channelFuture.isSuccess()) {
logger.warn("Close Future | Success | Retry Connect - Channel Close | {}", channelFuture.channel().toString());
channelFuture.channel().eventLoop().schedule(nettyClient, nettyClient.getReconnectTime(), TimeUnit.SECONDS);
}else {
logger.warn("Close Future | Fail | Retry Connect - Channel Close | {} | {}", channelFuture.channel().toString(),channelFuture.cause());
channelFuture.channel().close();
}
catch(NullPointerException e){ 
logger.error("연결 종료 후. 재접속 오류. | PIS : {} | NullPointerException | Error Log : {} " , pisID, LogHelper.getPrintStackTrace(e) ); 
}
catch (Exception e) { 
logger.error("연결 종료 후. 재접속시 예외사항 발생 | PIS ID : {} | errorLog : {} " , pisID , LogHelper.getPrintStackTrace(e)); 
finally { 
}

답변 요청드리기 위해서 필요하다고 생각하는 부분의 모든 소스를 최대한 첨부하였습니다.
답변주시면 정말 감사하겠습니다!

꾸언

unread,
Jul 7, 2021, 2:53:51 AM7/7/21
to Netty Korean User Group
회사에서 개인 메일이 안되서 여기에 다시 답을.. ^^

1. idle 상태에 대한 설명.
TCP/IP 연결은 처음에 connection 하게 되면 세션이 맺어지게 되지요.
이후 세션이 리셋 되기 전까지는 파이프? 세션이 유지되고 있는 상황입니다.

세션이 유지되는 상황에서 전문이 왔따갔다 하면 전문 전송 성공/실패 등이 발생하며 세션이 다시 맺어지겠지만.
전문이 송/수신되지 않은 상태가 발생하지요
이게 idle 상태 입니다.

이런 idle 세션 유지상황은 네트웍이 단절되어도 클라이언트/서버에서 유지되는것처럼 보이기 때문에
세션을 유지하기 위해서 하트비트를 사용해서 세션이 끊어졌을때 다시 연결하는 작업을 하는거구요.

2. Netty 핸들러에는 많은 메소드 (상태) 가 있습니다.
channelActive : 세션을 처음 맺을때 (세션 커넥션시 Auth에 대한 전문 발송에 사용)
channelInActive : 세션이 끝어진 다음. (세션끊김 로그만 찍고있음)
userEventTriggered : 이벤트 발생 시 호출 (Idle 상태 하트비트 전문 보내는데 사용)
channelUnregistered : 채널이 생성 됐지만, 이벤트 루프에 등록되지 않았을때 호출됨 ( 세션단절 시 재접속 용도로 사용)

저는 idle 이벤트를 userEventTriggered  여기에서 캐치하여 하트비트를 보내는 용도로 사용하고,
세션단절 시에는 channelUnregistered  를 이용하여 재접속 처리를 하고 있습니다.
추가로 세션단절은 클라이언트에서도 서버에서 일어날수 있습니다. 어디든 발생할수 있습니다.

Netty가 여느 인터프린드한 언어/프레임워크와 다르게 이벤트 드리븐이라는 개념이 있어서 조금 햇갈리는데..
조금만 감이 오면 정말 편한 프레임워크이종..
자료가 많이 없고 이제는 TCP/IP 를 많이 사용하지 않아사 프레임워크에 녹아 있는 상황이지만..

제가 회사가 망분리가 되어 있어서 예제 코드를 못보여드려 아쉽네요 ㅠ_ㅠ

아래 참조 자료 입니다.


2021년 7월 7일 수요일 오전 10시 58분 5초 UTC+9에 neoki...@gmail.com님이 작성:

박세진

unread,
Jul 7, 2021, 4:10:23 AM7/7/21
to Netty Korean User Group
다시 한번 답변과
TCP 통신시 필요한 개념과 네티에서 사용하는 메소드에 대한 설명을 해주셔서 감사합니다!

userEventTriggered 라고 말씀해주신 메소드가 
혹시 내가 알고 IdleStateHandler와 다른 부분인건가? 싶어서 다시 찾아보니 다른 부분이 아니어서 안심했습니다.

그리고 채널의 연결 상태 및 등록상태를 각 채널별마다 주기적으로 출력하도록 한 로그를 살펴보니
isRegistered 부분을 확인해보니 전부 등록되어있는것으로 보아 등록되지 않아 생긴 문제는 없는것 같습니다.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

제 미흡한 글솜씨로 인해서
제가 겪는중인 어려움의 원인 설명이 부족했던 것 같아 추가합니다.

1.  질문글을 작성하게 된 계기.
==========
제가 클라이언트고 다수의 서버에 접속을 유지하는 과정에서
어떤 원인 인지는 모르지만 
IdleStateHandler 에서 제공하는 
channelIdle() 메소드가 실행이 되지않는 상황이 있습니다.
channelIdle() 메소드가 실행이 됐다면 연결하고 있던 서버와 연결을 종료하고 재접속을 하는 로그가 남았을텐데 남지않아서
응답대기시간 초과 이벤트의 실행이 안되는 조건이 있는지 확인차 질문글을 작성했었습니다.


2. IdleStateHandler 에서 문제가 발생했다고 생각하는 이유.
==========
지속적으로 서버에 대해서 데이터를 보내는 데이터는 존재하고
서버에서는 이에 대한 응답이 없습니다. (이때 서버에서는 응답이 없으면서 받은 데이터가 있다고 로그를 남깁니다.)
이때 제 프로그램이 설치된 컴퓨터에서는 응답이 없어 연결을 끊고 재접속 시도를 해야하는 아이피와 연결을 유지하고 있는것을 확인했습니다.
패킷을 보내는중인 것은 서버프로그램의 로그를 확인해서,
연결 유지상태를 확인한 것은 netstat | grep "포트번호" 을 통해서 파악했습니다.


3. 문제의 메소드가 실행되지 않는지 확인결과
==========
현재 응답 대기 시간을 줄이고  (evt.state() == IdleState.READER_IDLE)
응답대기시간을 초과하지 않도록 
지속적으로 서버에 응답요청 패킷을 보내는 주기 시간을 늘려서
응답 대기 시간이 초과한 경우
channelIdle() 메소드에서 
evt.state() == IdleState.READER_IDLE
위의 조건이 작동하는걸 확인했습니다.


4. 현재 상황
==========
지금 당장 확인할때는 문제가 없지만
프로그램을 돌려놓고 하루이상 경과시
불특정한 시간부터 서버로부터 수신 데이터가 없습니다.
이때 응답대기시간이 초과해서 
IdleStateHandler의 evt.state() == IdleState.READER_IDLE 
조건에 충족해서 연결을 끊고 재접속을 해야하지만 
이 조건이 동작하지 않아서 연결을 끊지도 않고, 응답대기시간을 초과했다는 로그도 남지 않습니다.


최대한의 구글링과 테스트를 통해서 고민하고 고심해서 
더이상 제가 원인 파악을 할 수 없는 부분이라고 생각해서 작성했습니다.
많은 조언 부탁드립니다.
긴글 읽어주셔서 감사합니다
2021년 7월 7일 수요일 오후 3시 53분 51초 UTC+9에 꾸언님이 작성:

박세진

unread,
Jul 7, 2021, 4:31:27 AM7/7/21
to Netty Korean User Group
구글 그룹이 사용이 익숙치 않아 
중간에 누락된 내용이 있어 공유 게시 할 목적으로 작성합니다.

다시한번 꾸엔님 답변 감사합니다.



꾸엔님.
=====================================================

idle 상태는 세션이 유지되는 상태에서 아무런 전문이 왔따갔다 안하는 상태를 말합니다.
세션이 끊어진 상태가 아니구요.

세션이 끊어졌다면  핸들러 쪽에 ChannelUnregistered 메소드로 재접속 될수 있게 해줘야 합니다.
TCP/IP 전문에는 세션 유지를 위한 하트비트 전문을 만드는게 일반적입니다.
세션은 유지한채 하드비트 전문을 보내 세션상태를 확인하고 비정상적인 상태이면 세션을 다시 접속시켜야 하는겁니다.

전문이 발송되지 않은상태 idle 상태에서는 이벤트를 발생할 수있습니다.

단지 addLast 할쩍에 Encoder, Decoder, idleStateHandler, hander 순으로 넣어야지 동작을 하더군요.
addLast 할쩍에 순서에 영향을 받는지 몰라서 저도 동작을 안했습니다.

도움이 되셨으면 합니다.
감사합니다.





질문 작성자
======================================================================================

답변 감사합니다.
답변에 대한 몇가지 질문해도 괜찮을까요?

1. 채널 Idlestate 답변에 관한 질문 
 이 연결상태를 ctx.channel().isActive() 로 판단 + 응답 대기 시간으로 판단하고있는데
말씀하신 세션이 끊긴 상태는 IdleStateHandler 의 channelInActive메소드를 동작시키지 않는 이벤트?상황? 같은건가요?

( 서버에서 하트비트를 위한 패킷이 따로 없어서 서버에 요청했을때 응답이 있는 요청패킷을 지속적으로 요청하고있습니다.

망문제로 인해서 연결이 끊긴 상태인지를 테스트 환경에서 똑같이 구현하는 방법을 몰라서
노트북에서 와이파이를 껐다 켰다 하면서 망에 붙었다 떨어졌다 하면서 테스트를 해봤었는데, 
이때는 ChannelInActive메소드가 동작해서 이게 맞나 싶더라구요... )

1-2 세션 재접속 
세션을 재접속 해야한다고 하셨었는데
현재 제가 구현해놓은 각 Idle 조건에 따라 연결을 끊도록
ctx.channel().close() 를 선언해놨는데 이부분으로 연결이 끊기고
연결 종료 리스너 클래스에서
channelFuture.channel().eventLoop().schedule(nettyClient, nettyClient.getReconnectTime(), TimeUnit.SECONDS);
를 선언해서 이벤트 루프에서 재접속 시간의 설정에따라 재접속을 하도록 선언한것같은데
이러한 방식으로는 꾸언님께서 말씀해주신 재접속과는 다른건가요??

2. 파이프라인 설정.
현재 저는 파이프라인을 아래와 같이 설정했습니다.
pipeline.addLast( new IdleStateHandler(reader, writer, all) );
pipeline.addLast( new TCPDecoder() );
pipeline.addLast( new TCPEncoder() );
pipeline.addLast( new ChannelInboundHandler() );

이렇게 했을 때 현재는 비정상적으로 동작하는 것 같긴하지만
확실히 확인하기위해서  응답대기시간 1초로 설정했을때
응답대기시간이 초과하여 재접속하는 로그와 netstat으로 syn이 나오는걸로 확인을 했는데
순서가 달라도 동작이 되는건지, 아니면 동작이 아예 안되는건지 잘 모르겠습니다..

기존에 받았던 소스들을 분석해서 큰 틀을 벗어나지 않고 개발한 상황에서
서비스를 시작하고나니 지속적으로 비슷한 오류가 일어나고 있어서요..

테스트에서는 일어나지 않았던 상황들이 현장에서 발생하는중이라
괜히 개발할때 문제 없었던 코드를 썼다가 문제가 터진다면 제가 없을때 다른분들이 수정할수 없는 상황이라서요..

일단 지금은 제 프로그램을 재실행한다면 연결에 관한 문제들이 일단 해결되는 상황입니다....

답변 정말 감사합니다!

2021년 7월 7일 수요일 오후 5시 10분 23초 UTC+9에 박세진님이 작성:

박세진

unread,
Jul 15, 2021, 1:29:36 AM7/15/21
to Netty Korean User Group
문제를 추가로 발견해서 작성합니다.

파이프라인이 꼬이는 것 같습니다.

---------------------------------------------------------------------------------------
프로그램 동작 환경
대략적으로 프로그램이 돌아가는 환경을 설명하면
 10개가량의 기기에 접속을 하는상황이고
그중 3개의 기기는 아이피와 포트번호의 정보가 있지만
기기가 설치되지 않은 상황이라 지속적으로 재접속 시도를 하는 상황입니다.
---------------------------------------------------------------------------------------


로그를 상세히 남긴결과

channelRegistered 동작시 채널의 해쉬코드 출력하도록 했는데  (기존연결  ctx.toString 통해 확인한 해쉬코드 예시 0x1111)
기존 연결이 끊어지지 않아 위의 메소드가 동작할이유가 없는 연결에 대해서
위의 메소드가 동작합니다.

(새로운 연결 ctx.toString 통해 확인한 해쉬코드 예시 0x2222 )
새로운 핸들러가 종료되기전까지 기존 파이프라인 설정대로 동작하다
신규 연결이 종료되면서 기존 연결이 같이 종료되는것같습니다.
기존 연결의 해쉬코드로 닫히는 동작에 관련된 로그 출력도 없을뿐더러

신규 연결이 종료되는 로그가 두번씩 출력되는것을 확인했습니다.

2021년 7월 7일 수요일 오후 5시 31분 27초 UTC+9에 박세진님이 작성:

박세진

unread,
Jul 21, 2021, 2:23:36 AM7/21/21
to Netty Korean User Group
아... 오류의 정확한 원인이 확인됐습니다....

연결 종료시 제대로 연결 종료했는지 확인할용도로 만들어놓은 부분으로 인해서 
실제로 접속이 불가능한 지점과 연결 정보가 꼬이는 됐던 부분과  

재접속시 제대로 못붙던 부분의 원인도 같이 알게 됐습니다.

연결을 종료하고 재접속시 연결 종료리스너 클래스가 동작하고 난 뒤 새로운 연결을 시도하는 부분까지 확인했는데

public class NettyTCPClient implements Callable<Object> {
         ....
@Override public Object call() throws Exception {
         ...
              clientChannel = connectChannelFuture
.addListener(new ConnectFutureListener(this)).sync().channel().closeFuture()
.addListener(new CloseFutureListener(this) ).sync().channel();
         ....
        }
}

연결시 사용하는 리스너 클래스가 생성되지만 연결 종료 리스너 클래스가 생성되지 않는 걸 로그를 통해 확인했습니다..

이건 대체 무슨 문제인지 모르겠는데 아예 생성이 안되는건 자바문제인지 해당 서버 컴퓨터의 문제인지
이부분은 또 어떻게 해결해야할지 감도 안잡히네요..

결국 뻘글이 되버린 질문글이 되버렸지만..
감사합니다...
2021년 7월 15일 목요일 오후 2시 29분 36초 UTC+9에 박세진님이 작성:
Reply all
Reply to author
Forward
0 new messages