ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • asyncio.Task 소스코드 분석 및 오버라이딩을 통해 원하는 기능 구현하기
    Python/Library 2023. 5. 21. 16:43

     이번주는 회사에서 SNMP Trap을 수신하는 NMS(Network Management System) 제품에 대한 보수 요청이 들어와 비동기 라이브러리 asyncio를 뜯어보고 오버라이딩을 통해 원하는 기능을 추가한 경험에 대해서 써보려고 합니다.

    요청사항
    이벤트 중복 발생 시 이벤트 자동 해제 시간을 최신 발생시점으로 갱신해야 함

     

     우선, Trap이란 간단히 말해서 SNMP 프로토콜에서 제공하는 '단방향' 이벤트 전달방식을 말합니다. SNMP(Simple Network Management Protocol)는 네트워크 장비 관리 프로토콜로서 Manager와 Agent간에 주고받는 메세지 패킷의 형식을 정의하며 Manager와 Agent의 상호 데이터 교환을 가능하게 합니다.

     

     SNMP 메세지는 네트워크 관리정보 및 명령을 운반하는 PDU에 인증/보안 및 기타 정보를 담은 메세지를 말하는데 크게 SNMP Get, SNMP Set, SNMP Trap 세 가지가 있습니다. Get은 정보를 가져오고, Set은 정보를 변경하고, Trap은 이상 발생 시 장비가 즉시 메세지를 전송합니다.

     

     아래 그림에서도 알 수 있듯이 Get과 Set은 polling 방식으로 Manager가 Agent에게 주기적으로 관리정보 요청을 하고 Agent는 Manager에게 정보를 전달해주는 구조입니다. 여기서 주의할 점은 Manager와 Agent가 데이터를 주고받는 과정에서 문제가 생길 경우 중요한 관리 정보가 손실될 가능성이 존재한다는 것입니다. 

     

     반면 Trap은 Manager의 요청 없이도 Agent가 자발적으로 정보를 제공합니다. Agent에서 예외 동작이나 예상치 못한 장애 발생 등의 이유로 특정 OID값의 변화를 감지했을 때 162번 포트로 Manager에게 이벤트를 알립니다(통상적으로 Trap은 162번 Port를 사용하며 Get/Set은 161번  포트 사용). 이와 같은 방법으로 장비 운용에 있어서 중대한 영향을 미칠 수 있는 정보 변경사항을 사용자에게 UDP로 전송함으로써 장비 모니터링 과정에서 놓칠 수 있는 부분을 보완합니다.

     

     

    SNMP 메세지 종류 (참고: 정보통신기술용어 해설)

     

     참고로 현업에서는 Manager라고 지칭하기보다는 'NMS(Network Management System)'라고 합니다. 제가 작성한 NMS 프로그램은 UDP 서버를 열어놓고 Trap 수신을 대기합니다. 그러다 Trap을 수신하게 되면 이벤트 발생/해제 처리를 수행하는 이벤트 핸들러에게 Trap 이벤트 발생을 알립니다.

     

     자동 해제 기능을 활성화했을 경우에는 발생 시간을 기점으로 지정한 자동 해제 시간이 초과했을 때 이벤트를 해제할 수 있도록 이벤트루프에 이벤트 해제 태스크를 예약합니다. 바로 이 지점에 맨 처음 언급했던 이벤트 해제 시간 갱신에 대한 이슈가 존재합니다. 기존 로직은 이벤트 발생 후 동일 이벤트가 발생했을 때 기존 이벤트 발생 시점을 기준으로 해제하기 때문에 이벤트 해제 시점에 대한 기준을 이벤트 최신 발생 시간으로 갱신할 필요가 있었습니다.

     

    NMS의 Trap 수신 및 이벤트 처리 로직

     

    변경 전 이벤트 해제 로직

     

     기존 코드는 이벤트 발생 시 evt_free_sec 이후 이벤트를 해제할 수 있도록 asyncio.create_task()를 통해 이벤트 루프에 이벤트 해제 태스크를 예약하고 있었습니다.

    async def reserve_stop_evt(self, evt_no, mng_no, evt_free_sec):
          """
          이벤트 자동 해제 설정한 경우
          이벤트 발생 시점으로부터 evt_free_sec이 경과한 시점에 이벤트 해제시키는 함수
          * 이벤트 발생 시점은 Trap 메세지 수신한 시간과 동일하다고 가정
          """
          await asyncio.sleep(evt_free_sec)
          
          key = f"{evt_no}:{mng_no}"
          self.eventhandler.stop_event(key)
    
    def treat_evt():
        ...
        # 이벤트 발생 시 이벤트 해제 예약
        if evt_flag == 1:
            asyncio.create_task(self.reserve_stop_evt(evt_no, mng_no, evt_free_sec))

     

     위 로직을 요구사항에 따라 이벤트 발생시 이벤트루프에서 동일 이벤트가 발생 중인지 확인 후 해당 발생중이라면 task를 취소시킨 후 이벤트 해제 태스크를 이벤트루프에 새롭게 예약할 필요가 있었습니다. 제가 생각한 동일 이벤트 식별 방법은 이름을 지정하는 것이었습니다. ChatGPT에게 task의 이름을 지정하는 방법에 대해 질문해보니 asyncio.create_task()함수에 name 파라미터를 넘겨서 이름을 지정하는 방법을 알려줬습니다. 

     

     

     하지만 name 파라미터는 python 3.8에서 도입되었고 회사에서는 아직 python 3.7 버전을 사용 중이라 해당 기능을 사용할 수 없었습니다. 따라서 저는 다시 python 3.7 버전에서도 사용할 수 있는 방법에 대해서 다시 질문했고 ChatGPT의 답변을 참고해 asyncio. Task를 상속받아 name 파라미터를 받을 수 있도록 Task 클래스를 오버라이딩했습니다. 

     

     

     Task 클래스 코드와 변경된 이벤트 해제 코드는 아래와 같습니다.Task 클래스 init 시에 name을 받아 변수로 저장할 수 있도록 했습니다. Task._repr_info()는 태스크의 정보를 반환해줍니다. Task 클래스의 객체 task를 만들게 되면 내부적으로 _repr_info를 호출해서 해당 task의 정보를 반환받게 되므로 task의 정보에 name에 대한 정보를 추가해주었습니다. name을 지정하고 가져올 수 있도록 set_name()과 get_name()도 작성해주었습니다.

    class Task(asyncio.Task):
       """
       Task의 이름을 지정하기 위한 클래스
       TODO: python 3.11로 버전업하면 본 Task 클래스 삭제 및 check_evt()의 이벤트 자동 해제 예약 로직 수정
       cf. python 3.8부터 create_task() 매개변수에 name이 추가됨 => asyncio.create_task(coro, *, name=None)
          create_task()를 통해 Task 객체 생성 시 매개변수로 name을 전달하거나 생성된 Task 객체에 set_name 메소드를 사용하여 Task 이름 지정 가능
          ex) task = asyncio.create_task(coro)
             task.set_name('task_name')
       """
       def __init__(self, coro, *, name=None):
          super().__init__(coro)
          self._name = name
    
       def _repr_info(self):
          info = base_tasks._task_repr_info(self)
          info.append('name={!r}'.format(self._name))
          return info
       
       def set_name(self, value):
          self._name = str(value)
    
       def get_name(self):
          return self._name

     

     변경된 treat_evt 함수 코드는 아래와 같습니다. 기존 동일 이벤트가 존재하는지 판단하기 위해서 asyncio.all_tasks()를 통해 전체 태스크를 조회한 뒤 Task의 객체이면서 현재 발생한 이벤트와 동일한 식별자를 가지면 동일이벤트로 판단하고 기존의 이벤트는 해제해줍니다. 새로운 이벤트 해제 태스크를 예약할 때 Task 클래스를 사용하여 name을 지정해주면 또 다음 이벤트 발생 시 name을 확인할 수 있게 됩니다. 

    def treat_evt(self):
    ....
        # 기존 동일이벤트 존재 시 해제
        tasks = asyncio.all_tasks()
        for task in tasks:    
            if isinstance(task, Task) and task.get_name() == f"{evt_no}:{mng_no}":
                task.cancel()
            
        # 이벤트 발생 시 이벤트 해제 예약
        if evt_flag == 1:
            Task(self.reserve_evt_free, name = f"{evt_no}:{mng_no}")

     

     변경 후 이벤트 해제 로직 도식은 다음과 같이 수정되었습니다. 이상 asyncio 라이브러리를 뜯어본 후, 이벤트 루프를 얻어 task를 이벤트루프에 예약 후 task 객체를 반환해주는 클래스를 오버라이딩해서 현재 버전에는 없는 name 지정 기능을 구현한 과정을 정리해봤습니다.

    변경 후 이벤트 해제 로직

     

     

    참고:

    https://aws-hyoh.tistory.com/179

     

    SNMP 쉽게 이해하기 #1

    제 친구 중에 초등학교 교사가 있습니다. 하는 말을 들어보니 예전보다야 학생 수는 줄었지만 돌보고 가르치는 것은 더더욱 어렵다고 합니다. 요즘 같이 교권이 바닥에 추락한 현실에 한창 뛰어

    aws-hyoh.tistory.com

    http://www.ktword.co.kr/test/view/view.php?m_temp1=5270&id=430 

     

    SNMP 메세지 종류

        SNMP 메세지 종류, SNMP PDU 종류(2021-05-13)

    www.ktword.co.kr

    https://github.com/python/cpython/blob/3.11/Lib/asyncio/tasks.py

     

    GitHub - python/cpython: The Python programming language

    The Python programming language. Contribute to python/cpython development by creating an account on GitHub.

    github.com

     

    댓글

Designed by Tistory.