오늘은 어디로 갈까...

Java Daemon 본문

낙서

Java Daemon

剛宇 2009. 4. 20. 12:43
 이번 시간에는 자바 데몬을 한번 만들어보자. 일반적으로 실 운영 환경은 UNIX 인 관계로 유닉스에 초점을 맞추도록 하겠다.
 각고의 노력(?) 끝에 만든 PostMan. 이 놈도 성격상 데몬이기에 데몬처럼(?) 돌려야한다. 본인의 경험으로는 대부분의 사이트에서 crontab에 등록하여 자바를 실행하는 형식으로 사용하는것 같다. 다행인지 불행인지 몰라도, 본인은 crontab을 한번도 사용못해본(?) 풋내기 사용자라는것이다. 그래서 crontab은 거들떠보지고 않고, 자바 독립적(?)으로 데몬을 돌리곤 했다.
 PostMan은 자체 스케줄링 기능이 있는 관계로, crontab을 사용하면 왠지 이상하지 않은가? 그러니 자체 실행을 해보도록 하자.

 1. nohup 사용하기
 - 가장 간단한 방법은 nohup을 이용해서 java를 실행시키는것이다.
   ex) $ nohup java kr.kangwoo.postman.core.PostMan &
 - 사용하기 편한 장점은 있으나, 문제는 중지를 못 시킨다. 해당 프로세스를 찾아 kill 할 수밖에, 물론 파일 체크 기법, 소켓으로 제어 등 여러 가지 방법등을 동원하여 중지 시킬수는 있지만, 문제는 구현해야하는 귀찮음이 존재한다는것이다. ^^;

2. Commons daemon
 - 자바는 유닉스의 시그널 처리를 못함으로, 중지 신호(TERM Signal)를 처리하기 위해서는 따로 구현을 해야한다. 물론 본인은 c를 전혀 모르기에 이건 재앙에 가까운 일이다. 다행히, jakarta 하위 프로젝트로 commons daemon이란게 있는데, 중지 신호를 받으면, 미리 지정한 메소드를 실행하게 해준다.
 - http://commons.apache.org/daemon/ 사이트에 가서 다운받자.
 - 유닉스용 Jsvc와 윈도우용 Procrun이 있는데, M$를 안좋아하는 관계로 여기선 Jsvc만 설명하겠다.
 - 일단 유닉스에서 Jsvc를 사용하기 위해서는 운영 환경에 맞게 빌드를 해야한다. (http://commons.apache.org/daemon/jsvc.html)
   1) 빌드에 필요한 도구
  • GNU AutoConf (at least version 2.53)
  • An ANSI-C compliant compiler (GCC is good)
  • GNU Make
  • A Java Platform 2 compliant SDK

  2) 빌드 방법

    $ support/buildconf.sh
    $ ./configure --with-java=/usr/java
    $ make

   - 하늘이 도우신다면 아무 이상없이 끝날것이다. ^^; 혹시 F_LOCK이 선언되어 있지 않는다~ 이런 에러가 나면, jsvc-unix.c의 선언부에 아래 내용을 추가해주자. (본인은 CYGWIN에서 에러가 발생하여 아래처럼 추가해주었다. 아득히 먼 옛날 구글에서 검색해서 찾아낸 것인데, 정확한 출처는 모르겠다. ^^;)
#ifdef OS_CYGWIN
#include 
#define lockf(fd, op, sz)	flock((fd), (op))
#define F_LOCK			(LOCK_EX | LOCK_NB)
#define F_ULOCK			LOCK_UN
#endif

   - make가 완료되면 jsvc라는 실행 파일이 생성되었을거다. 나중에 이 파일을 가지고 데몬을 실행하니, 잘 보관해두자.

 3) org.apache.commons.daemon.Daemon 인터페이스 구현하기
   - 데몬을 만들려면 org.apache.commons.daemon.Daemon 인퍼페이스를 구현해야한다. init, start, stop, destory 메소드를 구현해야한다.
   - init에서 초기화 부분을 구현하고, start에서는 처리할 작업을 별도의 쓰레드로 생성해서 실행하게 구현해야한다.(start 메소드 호출후, return 되어야하지만 데문이 시그널 처리를 제대로 할 수 있다.)
  - stop, destory는 중지 신호가 오면 차례되로 호출되므로, 중지시 처리할 작업을 구현하면 된다.
package kr.kangwoo.postman.daemon;

import org.apache.commons.daemon.Daemon;
import org.apache.commons.daemon.DaemonContext;
import org.quartz.SchedulerException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

public class PostManDaemon implements Daemon {

	private ApplicationContext applicationContext;

	public void init(DaemonContext context) throws Exception {
		System.err.println("PostManDaemon: instance "+ this.hashCode() + " created");
		String[] args = context.getArguments();
		if (args != null) {
			for (String arg : args) {
				System.out.println(arg);
			}
		}
	}

	public void start() throws Exception {
		System.err.println("PostManDaemon: instance "+ this.hashCode() + " start");
		applicationContext = new ClassPathXmlApplicationContext(new String[] {"post-man-scheduler.xml"});
	}

	public void stop() throws Exception {
		System.err.println("PostManDaemon: instance "+ this.hashCode() + " stop");
		if (applicationContext != null) {
			SchedulerFactoryBean scheduler = (SchedulerFactoryBean)applicationContext.getBean("scheduler", SchedulerFactoryBean.class);
			if (scheduler != null) {
				scheduler.stop();
			}
		}
	}

	public void destroy() {
		System.err.println("PostManDaemon: instance "+ this.hashCode() + " destroy");
		if (applicationContext != null) {
			SchedulerFactoryBean scheduler = (SchedulerFactoryBean)applicationContext.getBean("scheduler", SchedulerFactoryBean.class);
			if (scheduler != null) {
				try {
					scheduler.destroy();
				} catch (SchedulerException e) {
					e.printStackTrace();
				}
			}
		}
	}
}



 - start() 메소드에서 새로운 쓰레드로 구현을 하라고 설명을 했는데, 이 소스에는 그 부분이 안보인다. 여기서는 spring의 ApplicationContext가 초기화되면서 SchedulerFactoryBean이 생성되는데, 이 SchedulerFactoryBean이 쓰레드로 돌아가므로, 따로 처리할 필요가 없다.
 - stop(), destroy() 메소드를 대충 구현하기는 했는데, 정확히 맞는것인지는 모르겠다. SchedulerFactoryBean을 분석해봐야 정확한 방법을 알것 같은데, 시간 관계상 넘어가도록 하겠다. (PostMan 자체가 중지 처리 부분이 많이 부족하다. ^^;)


  4) 이제 실행/중지 스크립트 파일을 만들어보자
    - jsvc를 실행하는데 필요한것은 실행하는 계정(-user), 자바 홈(-home), 프로세스 아이디 파일(-pidfile), 콘솔로 출력(-outfile, -errfile) 클래스 패스(-cp)등이 있다.
    - 꼭 UNIX 형식 파일로 저장하자!!!(DOS는 줄바꿈 CF/LF, UNIX는 LF, Max은 CF이다.)

#!/bin/sh
JAVA_HOME=/usr/java/jdk1.5.0_16
DAEMON_HOME=/home/jsvc
DAEMON_USER=kangwoo

PID_FILE=/var/run/postman.pid
POSTMAN_HOME=/home/kangwoo/kr.kangwoo.postman

CLASSPATH=\
$POSTMAN_HOME/bin:\
$POSTMAN_HOME/resources:\
$POSTMAN_HOME/lib/kr.kangwoo.util_v1.0.jar:\
$POSTMAN_HOME/lib/activation.jar:\
$POSTMAN_HOME/lib/commons-collections-3.2.1.jar:\
$POSTMAN_HOME/lib/commons-daemon.jar:\
$POSTMAN_HOME/lib/commons-dbcp-1.2.2.jar:\
$POSTMAN_HOME/lib/commons-logging.jar:\
$POSTMAN_HOME/lib/commons-pool-1.4.jar:\
$POSTMAN_HOME/lib/freemarker.jar:\
$POSTMAN_HOME/lib/ibatis-2.3.4.726.jar:\
$POSTMAN_HOME/lib/log4j-1.2.15.jar:\
$POSTMAN_HOME/lib/mail.jar:\
$POSTMAN_HOME/lib/ojdbc14.jar:\
$POSTMAN_HOME/lib/quartz-1.6.5.jar:\
$POSTMAN_HOME/lib/slf4j-api-1.5.6.jar:\
$POSTMAN_HOME/lib/slf4j-log4j12-1.5.6.jar:\
$POSTMAN_HOME/lib/spring.jar


case "$1" in

	start)
    #
    # Start PostMan
    #
    $DAEMON_HOME/jsvc \
    -user $DAEMON_USER \
    -home $JAVA_HOME \
    -wait 10 \
    -pidfile $PID_FILE \
    -outfile $POSTMAN_HOME/logs/postman.out \
    -errfile '&1' \
    -cp $CLASSPATH \
    kr.kangwoo.postman.daemon.PostManDaemon
    #
    # To get a verbose JVM
    #-verbose \
    # To get a debug of jsvc.
    #-debug \
    exit $?
    ;;

  stop)
    #
    # Stop PostMan
    #
    $DAEMON_HOME/jsvc \
    -stop \
    -pidfile $PID_FILE \
    kr.kangwoo.postman.daemon.PostManDaemon
    exit $?
    ;;
#
  *)
    echo "Usage postman.sh start/stop"
    exit 1;;
esac

  - 본인은  cywin에서 윈도우 자바를 실행시켜서 테스트를 했는데, 유닉스 + 윈도우의 자아정체성을 상실한 실행파일을 만들어줘야 정상적으로 돌아갔다. (클래스패스를 윈도우 기준으로 잡아줘야 정상작동했음 ㅠㅠ) 혹시 삽질할 분들 위해서 참고하시길 바란다.
  - Cygwin 버젼(중복 부분 생략)
#!/bin/sh
JAVA_HOME=/cygdrive/d/develop/java/jdk1.5.0_16
DAEMON_HOME=/cygdrive/d/kr.kangwoo.postman
DAEMON_USER=kangwoo

PID_FILE=/var/run/postman.pid
POSTMAN_HOME=/cygdrive/d/kr.kangwoo.postman
CP_HOME=/workspace/kr.kangwoo.postman

CLASSPATH="$CP_HOME/bin;$CP_HOME/resources;$CP_HOME/lib/kr.kangwoo.util_v1.0.jar;$CP_HOME/lib/activation.jar;$CP_HOME/lib/commons-logging.jar;$CP_HOME/lib/commons-collections-3.2.1.jar;$CP_HOME/lib/commons-daemon.jar;$CP_HOME/lib/commons-dbcp-1.2.2.jar;$CP_HOME/lib/commons-pool-1.4.jar;$CP_HOME/lib/freemarker.jar;$CP_HOME/lib/ibatis-2.3.4.726.jar;$CP_HOME/lib/log4j-1.2.15.jar;$CP_HOME/lib/mail.jar;$CP_HOME/lib/ojdbc14.jar;$CP_HOME/lib/quartz-1.6.5.jar;$CP_HOME/lib/slf4j-api-1.5.6.jar;$CP_HOME/lib/slf4j-log4j12-1.5.6.jar;$CP_HOME/lib/spring.jar"

 5) postman.sh 실행해보기
     - postman.sh start로 데몬을 실행. postman.sh stop으로 데몬을 중지 시킬수 있다.

 - 로그를 확인해보면 정상적으로 실행되는것을 알 수 있을것이다.