본문 바로가기

대용량 플랫폼

[박혜웅] Make a Java Daemon with Jsvc on Linux

Daemon 이란 백그라운드로 실행되면서, 사용자의 인터페이스(tty)가 없는 프로그램을 말한다. 우리가 흔히 사용하는 리눅스 서비스들은 대부분 데몬으로 동작하며, -d로 끝나는 프로그램(예: sshd, syslogd)이 모두 해당된다. 리눅스 명령인 nohup으로 백그라운드 구동은 가능하지만, kill 명령으로 종료해야 한다.
따라서 비정상 종료시의 처리를 하려면, OS로부터 signal을 받아야 하므로 데몬으로 구동시켜야 한다. 
Apache Commons Daemon에서 제공하는 Jsvc를 이용하여, 간단한 데몬 프로그램을 작성해 보자.
(우리가 잘 알고 있는 Tomcat도 Jsvc를 이용하여, 데몬으로 프로세스를 실행한다고 한다.)

JsvcDaemon.zip

아래 내용대로 작성한 쉘스크립트와 소스로 만든 이클립트 프로젝트. (Ubuntu 12.04 에서 테스트함.)

java와 jsvc는 설치후에, 이클립스에서 구동해 보자.
TestDaemon.sh에서 java, jsvc, commons-daemon.jar 파일의 위치는 자신에 맞게 수정해야 한다.


0. Java 설치

1-1. Jsvc 설치 (Ubuntu)
apt-get install jsvc

1-2. Jsvc 설치 (CentOS)
이미 컴파일된 Jsvc를 다운로드할 수는 있지만, 실제로 받아서 실행해 보면 에러를 출력하므로, 직접 컴파일하여야 한다.
Linux(CentOS 5.5)에 /opt/jdk1.6.0_24/에 JAVA를 설치했다고 가정한다. 

mkdir /root/commons-daemon
cd /root/commons-daemon

wget http://www.apache.org/dist/commons/daemon/binaries/1.0.5/commons-daemon-1.0.5.jar
wget http://mirror.apache-kr.org//commons/daemon/source/commons-daemon-1.0.5-src.tar.gz
tar zxvf commons-daemon-1.0.5-src.tar.gz
cd commons-daemon-1.0.5-src/src/native/unix/
support/buildconf.sh
./configure --with-java=/opt/jdk1.6.0_24/
make
mv jsvc /root/commons-daemon
위와 같이 실행하면, 작업할 경로(/root/commons-daemon)에 jar와 jsvc파일이 생성되어 있을 것이다.

2. 실행할 JAVA 클래스 작성
jsvc에 의해서 실행되는 JAVA 클래스는 Daemon 인터페이스를 구현해야 한다. 또한 스레드로 실행되어야 하므로, Runnable도 구현해야 한다. 만약 스레드로 구동하지 않을 경우, 종료처리가 제대로 되지 않으며, "Service exit with a return value of 143"라는 오류를 표시할 것이다.

테스트를 위해, 1초마다 숫자를 하나씩 증가시키면서 찍는 프로그램을 JAVA로 아래와 같이 만들었다.
TestDaemon.java

package com.bagesoft.test.daemon;

import org.apache.commons.daemon.Daemon;
import org.apache.commons.daemon.DaemonContext;
import org.apache.commons.daemon.DaemonInitException;

public class TestDaemon implements Daemon, Runnable {
	private String status = "";
	private int no = 0;
	private Thread thread = null;

	@Override
	public void init(DaemonContext context) throws DaemonInitException,
			Exception {
		System.out.println("init...");
		String[] args = context.getArguments();
		if (args != null) {
			for (String arg : args) {
				System.out.println(arg);
			}
		}
		status = "INITED";
		this.thread = new Thread(this);
		System.out.println("init OK.");
		System.out.println();
	}

	@Override
	public void start() {
		System.out.println("status: " + status);
		System.out.println("start...");
		status = "STARTED";
		this.thread.start();
		System.out.println("start OK.");
		System.out.println();
	}

	@Override
	public void stop() throws Exception {
		System.out.println("status: " + status);
		System.out.println("stop...");
		status = "STOPED";
		this.thread.join(10);
		System.out.println("stop OK.");
		System.out.println();
	}

	@Override
	public void destroy() {
		System.out.println("status: " + status);
		System.out.println("destroy...");
		status = "DESTROIED";
		System.out.println("destroy OK.");
		System.out.println();
	}

	@Override
	public void run() {
		while (true) {
			System.out.println(no);
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			if (no > 1000) {
				break;
			}
			no++;
		}
	}
}
위의 클래스를 jar로 묶어서 BageSoft.jar 라는 파일을 만들고 작업경로(/root/commons-daemon/)에 저장했다.

3. JAVA를 실행할 쉘스크립트 작성
쉘스크립트를 작성할 때, 위에서 다운 받아서 압축 풀었던 위치(commons-daemon-1.0.5-src/src/samples)에서 ServiceDaemon.sh를 참고하면 된다.
아래에서 $DAEMON_HOME은 작업 경로를 의미한다.
나중에 디버깅의 편의성 때문에 에러 출력을 표준출력과 같은 파일에 저장하도록 했다.

TestDaemon.sh
#!/bin/sh
JAVA_HOME=/opt/jdk1.6.0_24
JSVC=/root/commons-daemon/jsvc
USER=root

DAEMON_HOME=/root/commons-daemon
PID_FILE=$DAEMON_HOME/daemon.pid
OUT_FILE=$DAEMON_HOME/daemon.out
#ERR_FILE=$DAEMON_HOME/daemon.err

CLASSPATH=\
$DAEMON_HOME/commons-daemon-1.0.5.jar:\
$DAEMON_HOME/BageSoft.jar

MAIN_CLASS=com.bagesoft.test.daemon.TestDaemon
case "$1" in
  start)
    #
    # Start Daemon
    #
	rm -f $OUT_FILE
    $JSVC \
    -user $USER \
    -java-home $JAVA_HOME \
    -pidfile $PID_FILE \
    -outfile $OUT_FILE \
    -errfile $OUT_FILE \
    -cp $CLASSPATH \
    $MAIN_CLASS
    #
    # To get a verbose JVM
    #-verbose \
    # To get a debug of jsvc.
    #-debug \
    exit $?
    ;;

  stop)
    #
    # Stop Daemon
    #
    $JSVC \
    -stop \
    -nodetach \
    -java-home $JAVA_HOME \
    -pidfile $PID_FILE \
    -outfile $OUT_FILE \
    -errfile $OUT_FILE \
    -cp $CLASSPATH \
    $MAIN_CLASS
    exit $?
    ;;

  *)
    echo "[Usage] TestDaemon.sh start | stop"
    exit 1;;
esac
4. 실행/종료
실행 
/root/commons-daemon/TestDaemon.sh start

로그를 남기도록 몇초 기다리고.... 
종료
/root/commons-daemon/TestDaemon.sh stop

제대로 처리 되는지는 daemon.out의 내용을 확인하면 된다.
init(), start(), stop(), destroy()가 모두 수행되면 정상이다.
아래는 start 후 10초 후에 stop 했을 때의 daemon.out의 내용이다.
init... init OK. status: INITED start... start OK. 0 1 2 3 4 5 6 7 8 9
status: STARTED stop... stop OK. status: STOPED destroy... destroy OK.