日記マン

動画広告プロダクトしてます。Go, Kubernetesが好きです。

Kotlinで書いたプログラムをデーモン稼働させた時のメモ

OS上で常にバックグランド稼働し続けるプロセスをデーモンといいます。
自作のアプリケーションをCentOS6系にてデーモン稼働しておけば、管理が楽です。

普段はcronで定期実行するバッチプログラムを書くことが多く、単純にjarファイルを定期的に実行してました。
ただ、jarプログラム自体を無限ループさせておいて、ずっと稼働しておく、というサービスの要件*1があったので、
init.dでデーモン登録する方法を調べました。

参考になったのはこちらのテンプレートで、ぶっちゃけここを参考にしていただければ事足ります。

fhd/init-script-template - GitHub

この記事では、Kotlinで書いたjarをデーモン化する方法として、備忘録がてらにメモっておきます。

ソースコードはこちら GitHub

プログラム例

全ての依存ライブラリを内包したひとつのスタンドアロンなjarファイルを生成します。
今回はこのjarファイルをデーモンにしたいです。

kotlinのクラス(Kt)をjarにコンパイルするための最低限のpom.xmlです。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.github.kazukousen</groupId>
    <artifactId>kt_example</artifactId>
    <packaging>jar</packaging>
    <version>1.0</version>

    <name>kt_example</name>
    <url>http://maven.apache.org</url>

    <properties>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.source>1.8</maven.compiler.source>
        <kotlin.version>1.2.10</kotlin.version>
        <main.class>com.github.kazukousen.kt_example.HogeKt</main.class>
        <kotlin.compiler.incremental>true</kotlin.compiler.incremental>
    </properties>

    <dependencies>
        <!-- start Kotlin -->
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib</artifactId>
            <version>${kotlin.version}</version>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-test</artifactId>
            <version>${kotlin.version}</version>
            <scope>test</scope>
        </dependency>
        <!-- end Kotlin -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- start Kotlin -->
            <plugin>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-plugin</artifactId>
                <version>${kotlin.version}</version>
                <executions>
                    <execution>
                        <id>compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                        <configuration>
                            <sourceDirs>
                                <sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
                                <sourceDir>${project.basedir}/src/main/java</sourceDir>
                            </sourceDirs>
                        </configuration>
                    </execution>
                    <execution>
                        <id>test-compile</id>
                        <phase>test-compile</phase>
                        <goals>
                            <goal>test-compile</goal>
                        </goals>
                        <configuration>
                            <sourceDirs>
                                <sourceDir>${project.basedir}/src/test/kotlin</sourceDir>
                                <sourceDir>${project.basedir}/src/test/java</sourceDir>
                            </sourceDirs>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <!-- end Kotlin -->
            <!-- start .java -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.1</version>
                <executions>
                    <execution>
                        <id>default-compile</id>
                        <phase>none</phase>
                    </execution>
                    <execution>
                        <id>default-testCompile</id>
                        <phase>none</phase>
                    </execution>
                    <execution>
                        <id>java-compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>java-test-compile</id>
                        <phase>test-compile</phase>
                        <goals>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <!-- end .java -->

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.0.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <mainClass>${main.class}</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.0.0</version>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                        <configuration>
                            <archive>
                                <manifest>
                                    <mainClass>${main.class}</mainClass>
                                </manifest>
                            </archive>
                            <descriptorRefs>
                                <descriptorRef>jar-with-dependencies</descriptorRef>
                            </descriptorRefs>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

src/main/kotlin/com/github/kazukousen/kt_example/Hoge.kt は例えばこうです。

package com.github.kazukousen.kt_example

import java.io.File
import java.lang.Runtime
import java.lang.Thread
import java.lang.Thread.sleep
import java.time.ZonedDateTime

object Hoge {
    fun run() {
        Misc.print("Hello, World!")
    }

}

object Misc {
    fun print(s: String) {
        println("[${ZonedDateTime.now()}] ${s}")
    }

    fun printErr(s: String) {
        System.err.println("[${ZonedDateTime.now()}] ${s}")
    }
}

class ShutdownThread : Thread() {

    override fun run() {
        Misc.print("get HUP signal.")
        // ここに安全に終了する処理
        Misc.print("Shutdown.")
    }
}

fun main(args: Array<String>) {

    // HUPをフックする
    Runtime.getRuntime().addShutdownHook(ShutdownThread())

    // 処理
    while(true) {
        Hoge.run()
        Thread.sleep(5000L)
    }
}

例として、5秒ごとにこんにちは世界するしょぼいプログラムです

mvn package

killコマンドなどでHUPシグナルが送られた時に、
処理途中のものを安全に終了させるためにハンドリングしたい場合は、
JVMでは Runtime.getRuntime().addShutdownHook() にThreadクラスのサブクラスを渡すことで、終了処理を追加できます。

さて、これで mvn package を実行すれば、
target/kt_example-1.0-jar-with-dependencies.jar というjarファイルが出来上がります。

試しに実行してみて、ctrl-Cコマンドでkillしてみると、期待した動きをします。

$ java -jar target/kt_example-1.0-jar-with-dependencies.jar
[2018-03-04T13:45:32.238+09:00[Asia/Tokyo]] Hello, World!
[2018-03-04T13:45:37.242+09:00[Asia/Tokyo]] Hello, World!
[2018-03-04T13:45:42.243+09:00[Asia/Tokyo]] Hello, World!
[2018-03-04T13:45:47.248+09:00[Asia/Tokyo]] Hello, World!
^C[2018-03-04T13:45:47.796+09:00[Asia/Tokyo]] get HUP signal.
[2018-03-04T13:45:47.796+09:00[Asia/Tokyo]] Shutdown.

このプログラムをinit.dに登録したいと思います。

init.d

今回のプログラムを ktd というサービス名でデーモン化したいと思います。

service ktd start
service ktd status
service ktd stop
service ktd restart

これらの操作が効くようにしたいです。
後ろにdがつくのは、デーモンという意味で、慣習です。

/etc/init.d/ktd というファイル名で以下のように記述します。
こちらのテンプレートを思い切りパクらせていただきました。
fhd/init-script-template - GitHub

# !/bin/bash
#
# ktd        Startup script for the jobd
#
# chkconfig: - 85 15
# description: The JobWatcher.
# processname: ktd
#
### BEGIN INIT INFO
# Provides: ktd
# Required-Start: $local_fs $remote_fs $network $named
# Required-Stop: $local_fs $remote_fs $network
# Should-Start: distcache
# Short-Description: start and stop ktd
# Description: ktd
### END INIT INFO

# Source function library.
. /etc/rc.d/init.d/functions

dir="/path/to/kt_sample"
cmd="java -jar kt_example-1.0-jar-with-dependencies.jar"

name=`basename ${0}`
pid_file="/var/run/${name}.pid"
stdout_log="/var/log/${name}/running.log"
stderr_log="/var/log/${name}/error.log"

get_pid() {
    cat "${pid_file}"
}

is_running() {
    [ -f "${pid_file}" ] && ps -p `get_pid` > /dev/null 2>&1
}

case "${1}" in
    start)
    if is_running; then
        echo "Already started"
    else
        echo "Starting $name"
        cd "$dir"
        $cmd >> "${stdout_log}" 2>> "${stderr_log}" &
        echo $! > "${pid_file}"
        if ! is_running; then
            echo "Unable to start, see $stdout_log and $stderr_log"
            exit 1
        fi
        echo "OK"
    fi
    ;;
    stop)
    if is_running; then
        echo -n "Stopping ${name}.."
        kill `get_pid`
        for i in 1 2 3 4 5 6 7 8 9 10
        do
            if ! is_running; then
                break
            fi

            echo -n "."
            sleep 1
        done
        echo

        if is_running; then
            echo "Not stopped; may still be shutting down or shutdown may have failed"
            exit 1
        else
            echo "Stopped"
            if [ -f "${pid_file}" ]; then
                rm "${pid_file}"
            fi
        fi
    else
        echo "Not runnning"
    fi
    ;;
    restart)
    $0 stop
    if is_running; then
        echo "Unable to stop, will not attempt to start"
        exit 1
    fi
    $0 start
    ;;
    status)
    if is_running; then
        echo "Running"
    else
        echo "Stopped"
        exit 1
    fi
    ;;
    *)
    echo "Usage: $0 {start|stop|restart|status}"
    exit 1
    ;;
esac

exit 0
}

pidファイルは /var/run/ktd.pid に、
ログは標準出力とエラー出力それぞれが /var/log/ktd/running.log/var/log/ktd/error.log に出力されます。
ディレクトリは事前に用意しておく必要があります。

mkdir /var/log/ktd

あとは、serviceコマンドに登録してあげます。

chmod 755 /etc/init.d/ktd
chown root:root /etc/init.d/ktd
service ktd start
service ktd status # Runningと表示されればOK

chkconfig コマンドでランレベルを設定し、自動起動をオンにしてあげます。

chkconfig --add ktd
chkconfig ktd on
chkconfig --list ktd # 確認

logrotate.d

次に、1日ごとにログを圧縮ファイルにしていきたいです。
logrotateは便利です。

以下の内容を /etc/logrotate.d/ktd というファイルで保存します。

/var/log/ktd/*.log {
    daily
    rotate 7
    missingok
    notifempty
    compress
    postrotate
        /sbin/service ktd restart
    endscript
}

参考: ログローテーションするためのlogrotate設定とちょっとしたtips - Qiita

rotate後に1度リスタートをする理由は、プログラムがログファイルを噛んでしまっているのを解除する必要があるからです。

chown root:root /etc/logrotate.d/ktd

これで1日ごとにログファイルが切り出されます。

まとめ

kotlinの自作プログラムをinit.dでデーモン登録し、logrotateでログ管理するようにしました。
思いの外JVMでHUPシグナルのフックすることが簡単だったので助かりました。
JVMガベージコレクションタイミングや、色々と理解しておくべきものがありそうです。

*1:例えばBigQueryに対し、重めのクエリをユーザドリブンで同期的実行させるのではなく、キューのように非同期ハンドリングするクエリ管理プログラム