지난 1년간 네비게이션 시스템의 차세대 프로젝트에 참여했습니다. 개발 중후반 신규 개발된 서버들이 30개를 넘어섰고 계속해서 추가되고 있던 상황이었습니다. 이에 따라 매분 집계된 각 서버의 요청 처리 결과와 서버 상태를 Web page에서 실시간 조회가능하고 문제가 발생하였을 경우 담당자가 알림을 받을 수 있는 모니터링 시스템을 필요로 하였고 개발을 진행하였습니다.

모니터링 시스템의 요구사항

  • 각 서버의 요청건수, 처리성공률, 처리시간, 시스템 상태정보 등을 매분 집계하여 웹에서 확인가능 (표 + 그래프)
  • 관리자는 웹에서 모니터링 대상 서버, 사항별 임계치 및 담당자를 설정할 수 있다
  • 임계치 미달사항이나 시스템 오류가 발생하였을 경우 담당자에게 SMS 알림
  • 시간/일별 집계정보 email 알림

각 서버의 Agent는 매분 모니터링 서버로 집계된 데이타를 전송하고 모니터링 서버의 Collector는 Agent로 부터 받은 데이터를 DB에 저장하는 역할을 합니다. Scheduler는 주기적으로 DB 에서 데이터를 가져와 체크하고 담당자에게 알림 기능을 합니다.

Agent 에서는 각각의 요청발생시 호출시점과 처리완료시점에 interceptor를 통해 필요한 데이터를 queue에 쌓습니다. Quartz scheduler 에 의해 매분 0초 마다 queue에서 데이터를 꺼내어 종합 후 모니터링 서버로 RMI(Java remote method invocation)방식으로 전송합니다.

각 업무별 프로젝트에서는 Agent 프로젝트를 dependency설정 후 아래설정이 포함된 xml 파일을 import하도록 요청하였습니다.

<bean id="monitor.collect" class="com.###.frame.monitoragent.AgentInvokerProxyFactoryBean">
       <property name="serviceUrl" value="http://${monitor.host}:${monitor.port}/monitorserver/CollectServerInfoManager"/>
       <property name="serviceInterface" value="com.###.frame.monitoragent.collect.CollectServerInfoManager" />
       <property name="connectTimeout" value="1000" />
       <property name="readTimeout" value="60000" />
</bean>

<bean id="monitor.agentJob" class="com.###.frame.monitoragent.MonitorAgent" init-method="initialize" >
       <property name="collectServerInfoManager" ref="monitor.collect" />       
</bean>

<bean id="monitor.agentJobFactoryBean" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
       <property name="targetObject" ref="monitor.agentJob" />
       <property name="targetMethod" value="collect" />
       <property name="concurrent" value="false" />
</bean>

<bean id="monitor.agentCronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
        <property name="jobDetail" ref="monitor.agentJobFactoryBean" />
        <property name="startDelay" value="1000" />
        <property name="cronExpression" value="0 0/1 * * * ?" />
</bean>
public class MonitorAgent {
    //
    private CollectServerInfoManager collectServerInfoManager;

    public void setCollectServerInfoManager(CollectServerInfoManager collectServerInfoManager) {
        this.collectServerInfoManager = collectServerInfoManager;
    }

    public void collect() {
        //
        try {
            collectServerInfoManager.collect(monitorInfoDto);
        } catch (Exception e) {
            logger.error("!!! 모니터링 서버에 접속할 수 없습니다.-->" + e.getMessage());            
        }
    }
}

monitor.collect의 경우 timeout설정을 위해 HttpInvokerProxyFactoryBean를 그대로 사용하지 않고 상속받아 재선언하였습니다.
Agent에서 집계된 데이타를 전송하기 위해 호출할 모니터링 서버의 인터페이스와 url를 선언해준 bean객체를 Sender역할을 담당하는 MonitorAgent class에 주입하였습니다. MonitorAgent의 collect 메서드를 1분마다 호출하도록 설정하고 agentCronTrigger는 최종적으로 SchedulerFactoryBean에 주입합니다.

개발 및 배포가 진행되었고 신규 서버에 적용이 완료되는 시점에 as-is 시스템으로 동작하는 서버도 신규 개발된 웹에서 통합 모니터링 하고 싶다는 고객의 요청이 있었습니다. 최초 협의에서는 매분 as-is 모니터링 시스템에 의해 적재된 DB데이터를 읽어 신규 시스템 DB에 저장하는 방식이었으나 차후 as-is 모니터링 시스템을 폐기하기로 하여 아래와 같이 기존 시스템에서 쌓고 있던 log file을 활용하는 방식으로 변경되었습니다.

신규 시스템에서 Agent 프로젝트에 의존관계를 설정하였던 방식과 달리 as-is 시스템용 Agent는 독립적인 tomcat에 의해 구동되었고 매분 적재된 log 데이터를 종합하여 모니터링 서버로 전송합니다 (as-is 시스템의 경우 java가 아닌 C언어 체계여서 maven dependency 불가능). 1분치 데이타를 구분하는 방식을 살펴보면 서버에서는 특정 로그 파일에 데이터를 쌓고 매분 0초가 되면 Agent는 해당 로그 파일을 rename한 후에 분석합니다. 이렇게 하면 기존의 로그파일은 사라지고 서버에서는 동일명의 로그파일을 새로 생성하여 로그를 쌓게됩니다.

집계 데이터를 모니터링 서버로 전송하는 과정에서 connection timeout으로 인해 데이터가 누락되는 경우가 발생하여 위와 같은 처리가 추가되었습니다. 데이타 전송중 예외가 발생하였을 경우에 해당 데이타를 실패 처리용 queue에 쌓았다가 5초 간격으로 전송 재시도를 합니다. 재전송시에도 예외가 발생한다면 다시 queue에 쌓게되고 성공할때까지 반복됩니다.

대략적인 모니터링 시스템이 갖춰졌고 추가로 배치 현황과 캐시 서버의 모니터링을 진행하였습니다.
배치의 경우 프로젝트 초기에 종합 관리를 위한 모니터링 요구사항으로 별도의 모니터링 방식을 사용하고 있었습니다. As-is에서 개별적으로 사용하던 배치의 통합 관리를 위해 Spring batch 기반의 배치 프레임워크를 프로젝트 초반에 제공하였고 신규 개발되는 배치는 실행 shell script를 호출하는 작업을 Jenkins에 Job으로 등록하고 Jenkins web페이지를 통해 배치 모니터링을 하는 방식이었습니다. CI(Continuous Integration)인 Jenkins의 본래 용도와는 다소 맞지 않을수도 있지만 Jenkins를 선택한 이유는 별도의 시스템 개발 없이 배치들의 스케쥴링, 처리결과에 따른 email 알림 및 내역조회 기능을 제공하기 때문이었습니다.

작업 과정에서 Skip 발생시 jenkins에서의 처리와 관련한 이슈가 있었습니다. 배치가 모두 성공하여 완료되었을 경우의 success, 실패하였을 경우의 fail은 자동으로 처리됩니다. 하지만 배치가 정상적으로 완료되었으나 Skip이 발생한 경우에는 성공,실패가 아닌 다른 표기로 배치 모니터링 담당자가 인지하기 쉽도록 처리할 필요가 있었고 이를 어떻게 처리하느냐였습니다. Jenkins에서의 Job의 실행 결과는 success, fail, unstable 아이콘으로 표기됩니다. 일반적인 Build Job의 경우, 빌드는 성공하였으나 testcase 실패시 unstable이 표기됩니다. 하지만 Jenkins에 등록된 배치 Job의 경우 Build Job이 아닌 단순 배치 실행 스크립트 호출을 위한 simple Job이기 때문에 unstable이 발생하는 일이 없습니다. 그래서 Skip 이 발생하였을 경우 이 unstable 상태로 처리하는 방법을 선택하였습니다.

인위적으로 Job의 실행 결과의 변경 방법을 찾던 중 command상에서 jenkins를 사용할 수 있도록 제공하는 인터페이스인 Jenkins CLI(Command Line Interface)를 발견하였고 CLI를 사용하여 동적으로 job의 결과변경이 가능하게 되었습니다. CLI를 사용하기 위해선 Jenkins-cli.jar파일이 필요한데 설치된 Jenkins web page url 뒤에 /cli 경로를 추가하면 download 받을 수 있는 페이지로 연결됩니다.

최종적으로 배치에서는 Skip이 발생하였을 경우 특정 로그를 발생하도록 하였고 아래와 같이 expect shell을 사용하여 해당 로그 발생시 Jenkins build 상태를 변경는 shell script파일을 호출 하도록 처리하였습니다.

send “sh /data/batch/shell/batch_start.sh”

expect –re “COMPLETED” {
	expect –re “isSkipped” {
		spawn sh “/home/shell/set-unstable.sh”
	}
}

set-unstable.sh에는 아래와 같이 cli에 set-build-result 명령어를 사용하여 처리 결과를 unstable로 변경하도록 작성하였습니다.

java –jar Jenkins-cli.jar set-build-result unstable

위의 방식으로 몇개월간 배치 모니터링이 이루어졌지만 프로젝트 후반 신규 모니터링 시스템이 개발되면서 배치 역시 동일웹에서의 모니터링을 원하였고 Spring batch에서 db에 저장한 배치 실행 정보를 모니터링 하도록 변경됩니다.

신규 시스템에서 분산 캐시를 위해 terracotta 서버를 사용 중이었고 terracotta에서는 자체적인 웹을 제공하고있습니다. 하지만 고객사에서 캐시 상태 역시 모니터링 웹에서 확인하기를 원하였고 이를 위해 모니터링 서버에서는 매분 JMXConnector 를 사용하여 terracotta 서버 상태 및 캐시 client 정보를 가져와 모니터링 웹에서 조회 가능하도록 처리하였습니다.
배치와 캐시 역시 특정 문제 발생시 SMS 알림 기능을 포함합니다.

최종적으로 초기 설계에서 확장되어 아래와 같은 형태를 이루었습니다.

시스템 적용 후 초반에 발생한 가장 큰 문제점은 모니터링 시스템의 오류로 인한 데이터 누락과 이로 인한 SMS 발송이었습니다.
각 서버에서는 정상적으로 데이터를 집계하여 모니터링 서버로 전송을 시도하였지만 전송이 실패하였을 경우, 또는 전송은 성공하였으나 모니터링 서버의 오류로 정상적인 db insert가 이루어지지 않았을 경우에 해당 분의 데이터는 누락되고 인스턴스 상태가 비정상이라 판단되어 업무 담당자에게 SMS가 발송되는 일들이 발생하였습니다. 대부분은 로직상의 문제가 아닌 몇일 혹은 몇주에 한번꼴로 발생하는 서버의 일시적인 불안정 현상이었습니다. 무시할수도 있지만 모니터링 시스템에서는 한번의 요청 실패로도 SMS 발송이 이루어지기 때문에 민감한문제였습니다. (시스템실에서는 SMS 긴급메시지가 발생하면 경고음이 울려퍼집니다..)

SMS가 발송 되기 까지의 과정을 보면 매분 0초부터 각 서버에서 집계된 데이타가 모니터링 서버로 전송되고 30초가 되면 모니터링 서버는 모니터링 대상 서버중 집계 데이타를 전송받지 못한 서버의 인스턴스 상태를 비정상으로 판단하여 40초에 해당 서버의 업무담당자에게 SMS를 발송하도록 되어있습니다. 실질적인 담당 서버의 문제가 아닌 모니터링 시스템의 문제로 인해 SMS 알림을 받게 되다 보면 담당자는 차후에 정말 담당 서버에 문제가 발생하였을 경우에도 이를 경시하는 일이 생길수도 있던 상황이었습니다. 양치기 소년처럼 말이죠..

해당 문제의 개선을 위해 앞에서 언급한 resend 처리(전송실패시 queue에 쌓았다가 재전송)를 하였고 collector에서는 매분 0초에 50개 이상의 인스턴스로 부터 집계되는 요청에 의한 db insert를 분산하기 위해 각 쓰레드에 0~5초 사이의 random sleep을 주도록 처리하였습니다. 마지막으로 시스템 오류로 과다한 비정상 인스턴스가 판단되었을 경우 해당 인스턴스의 문제가 아닌 모니터링 시스템의 문제로 판단하여 업무 담당자가 아닌 모니터링 시스템 담당자에게 SMS를 발송하도록 처리하였습니다. 다행히 처리 이후 지난 몇달간 모니터링 시스템의 오류로 인한 SMS 발송은 발생하지 않았습니다.

데이터 집계와 처리 그리고 알림 까지가 매분 마다 순차적으로 이루어져야 했기 때문에 시간에 많은 구애를 받았고 설계 초반에 크게 고려하지 못했던 대용량 DB 조회로 인해 쿼리 속도개선의 이슈도 있었지만 전반적으로 만족스럽게 마무리 되었고 무엇보다 새로운 시스템을 구축해 볼 수 있었던 프로젝트였습니다.