RHEL 7系のCentOS 7などではそれまでRHEL 6系で使われていたSystem V系のinitから Systemdを用いたデーモン管理がベースになるようになりました。
CentOS 7でデーモンを自作したいものがあって作ったので 基本的な作り方についてまとめておきたいと思います。
- Systemd (systemctl)
- デーモン本体作成
- 最小限の設定
- サービスファイル
- rsyslogの設定ファイル
- logrotate
- インストール/アンインストールスクリプト
- 動作チェック
- まとめ
Systemd (systemctl)
initのときには/etc/init.d/の中にデーモン名の(通常)シェルスクリプトが入っていて、
このスクリプトがstart
とかstop
とかの引数を受ける様に作られ、
直接
# /etc/init.d/httpd start
とかするか、
# service httpd start
のようなservice <daemon> <command>
と言った形でデーモンを管理していました。
service
というコマンドを使いますが、実態は単にinit.d
にあるスクリプトを呼ぶラッパーの様なものでした。
また、OS起動時に走らせる様にするためにはchkconfig
というコマンドなど、service
とは別のコマンドで管理されていました。
chkconfig
用の設定みたいなものもデーモン用スクリプトに書いておく必要があったりもしました。
なので色々なものがごちゃごちゃしていてデーモンを作るのは結構面倒だったイメージがあります。
これがSystemdではこれで全てを一括管理し、さらにはデーモン用ファイルも いわゆる設定ファイルの様な簡単な記述をするだけで良いようになりました。
Systemdでは/usr/lib/systemd/system/や/etc/systemd/system/と言ったディレクトリに
<daemon.service>
みたいな名前のファイルを置いて管理します。
これらのディレクトリに同じデーモンがある場合には後者の方が優先されます。
yum
などでインストールされる際には前者に入れられ、
ユーザーが変更したりしたい場合には後者のディレクトリにコピーして使ったりすることが想定されています。
ファイルの内容はinitの場合と全く変わって独自のフォーマットで いろいろと指定するような形になっています。
繰り返しになりますが、initのときには基本的に全ての動作をシェルスクリプトなどで自分で書く必要がありましたが、 Systemctlでは簡単な記述でデーモンに登録できるようになっています。
initの場合には/etc/init.d/httpd start
みたいに直接呼んでもservice httpd start
と
同じ動作をしましたが、
Systemctlでは/usr/lib/systemd/system/httpdなどのファイル自体はスクリプトではないので直接は呼べません。
# systemctl start httpd
の様にsystemctl
コマンドを通じて管理することになります。
service
のときとデーモン名とstart
などのコマンドの順番が逆なので未だに時々どっちだかわからなくなりますが、
systemctl
ではGitなどの様にサブコマンドを呼んでデーモン名を引数として渡す
# systemctl <subcommand> <daemon>
いった感じになっています。
(initの場合はスクリプトへのラッパー的な感じなので、最後のstart
とかがデーモン名スクリプトへの引数、といった雰囲気で
先にデーモン名が来てた感じです。)
systemctl
の基本的なサブコマンドとしては
start
: デーモン開始stop
: デーモン停止restart
: デーモンリスタートis-active
: デーモンが走っているかどうかの確認enable
: デーモンをOS開始時にスタートする様にするdisable
: デーモンをOS開始時にスタートしない様にするis-enabled
: デーモンがOS開始時にスタートするようになっているかどうかの確認
など。
initと違いOS起動時の登録なども基本的に全てsystemctl
コマンドで管理できる様になっています。
デーモン本体作成
簡単なデーモンを作って登録してみようと思います。
1 2 3 4 5 6 7 8 |
|
こんな感じのただひたすら10秒毎にHello world!
を言うだけのスクリプト。
これをデーモンとして登録し、ログファイルなども管理してみます。
最小限の設定
単にデーモンとして動かしたいだけなら
1 2 3 4 5 6 7 8 9 |
|
というサービスファイルを作ってデーモン本体とこのサービスファイルを設置して
# systemctl enable hello_world
# systemctl start hello_world
とするだけでOKです(起動時に走らせる必要がなければstart
だけでOK。)
ログはjournalctl -u hello_world.service
で確認出来ます。
サービスファイル
ログファイルを書き出したりしたいのでもうちょっと色々やってみます。
以下の様な内容でサービスファイルを作ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
これら以外にもたくさん設定項目があって出来ることもたくさんありますが、 とりあえず載っているものだけ説明しておくと
Unit
Description
: デーモンの説明
Service
ExecStart
: デーモン開始のコマンドExecStop
: デーモン停止のコマンド- 今回のスクリプトは自分で止まる手段を持たないので
kill
する。 - この際、
MAINPID
という値がExecStart
で開始されたプログラムのPIDを持っていてこれを使って上の様にkill
することができる。 - この様な簡単な単独コマンドであれば/var/run/<daemon>.pidみたいなPIDファイルを使わずに管理できる。
- 今回のスクリプトは自分で止まる手段を持たないので
Restart
:always
にしておくと何らかの理由で落ちた場合(手動でkill
した場合なども)自動でリスタートする。no
にすればしない。(他にもいくつかオプションあり。)StandardOutput
: 標準出力の出力先StandardError
: 標準エラーの出力先SyslogIdentifier
: syslogでこの名前で認識される
Install
WantedBy
:enable
したときにどの状態ならスタートするかを決めるターゲットを決める。multi-user.target
がいわゆるGUIなしで普通に立ち上げたときに対応するので通常はこれを指定しておけばOK。
デーモンとして走らせるのに特に重要なのはExecStart
とExecStop
の部分です。
Systemdによってこんな感じでコマンドを指定するだけでスタートできますし、
停止も$MAINPID
という便利な変数を使って簡単にkill
する設定を作れます。
その下、StandardOutput
などでsyslog
を指定していますが、
デフォルトはjournal
です。
このjournal
を指定すると
$ journalctl -u hello_world.service
などでアクセスできるジャーナルに書き込まれます。
systemsctl status hello_world
などで表示されるログもこれの一部。
journalctl
を使うと時間をしていしてログを抜き出したりすることが簡単にできるので
これを上手く使えるといろいろ便利なのですが、
今回は普通にファイルに書き出したいのでsyslog
を指定しています。
使った環境にはrsyslog
がインストールされていて、syslog
を指定するとrsyslogに渡されます。
単純に
ExecStart = /usr/bin/hello_world >/var/log/hello_world.log 2>&1
みたいなことをしてもコマンドを実行する際の出力が先にStandardOutput
などで指定されたものに指定されるので
上手くいきません。
ここで指定したいなら
ExecStart = /bin/sh -c 'exec /usr/bin/hello_world >/var/log/hello_world.log 2>&1'
の様にシェルの中でコマンドを実行してそこで出力、的なことをする必要があります。
シンプルにやるのであればこれでも十分だとは思います。
もしくはコマンドの中でファイルに出力するようにしておけばもちろんそれはそのままファイルに出力されますが。
今回はサービスファイルをきれいに保ってログは別途管理する、ということを考えて
syslog
を使ってみます。
ただし、syslog
を指定した場合でもログはjournal
にも同時に流れる様になっています。
ここからもjournal
を使うのが主流になっていく感もあります。
一方、やはり直接ファイルに書き出したい、と考える人も多いようで、
昨年末位にこれに関するコミットがマージされて
最新版ではfile:...
とすることで直接ファイルへの書き出しをできるようになっています。
(v236から。今入ってるのはv219だった。)
Support pointing StandardInput/StandardOutput/StandardError to file · Issue #3991 · systemd/systemd
まだCentOS 7などの標準では使えませんが
使える様になったらこれを使うのもありなのかも。
(少なくともsh -c
とかして無理やりやるよりはきれい。)
最後のWantedBy
で決めるターゲットとしては以下の様なレベルがあります。
ランレベル | ターゲット | 説明 |
---|---|---|
0 | poweroff.target | システム停止 |
1 | rescue.target | レスキューシェル(シングルユーザーモード) |
2(,3,4) | multi-user.target | マルチユーザーモード |
5 | graphical.target | マルチユーザーモード+GUI |
6 | reboot.target | 再起動 |
通常multi-user.target
で良いと思います。
GUIを使うためだけのデーモンなどがもしあればgraphical.target
など。
rsyslogの設定ファイル
rsyslogに送ったログは、そのまま何もしなければ/var/log/messagesに書き出されます。
これを変更するための各プログラムに対する設定ファイルは/etc/rsyslog.dの下に入っています。
ここに次の様なファイルを用意します。
1 2 |
|
ここでprogramname
は先程
SyslogIdentifier
で指定した名前を指定してそれと同じなら
/var/log/hello_world.log
に書き出す、という設定にします。
次の& stop
でこの出力を他に出さない、つまり/var/log/messagesには出力しない、という設定になります。
古いシステムで& ~
としているものもあったりしますが、これだと最近の環境だとwarningをだす環境もあるので
stop
の方が良いです。
ちなみにこれで書き出されるログには
<date> <hostname> <processname>[<pid>] logoutput...
といった感じに時間情報などが追加された状態になります。
もともとのログ出力に時間情報などが全く無い場合には便利ですが、 逆にあると邪魔かもしれません。
その場合は出力を考え直すか、 もしくは上に書いた例の様に直接サービスファイルの中の設定でログに 書き出す様にした方が良い場合もあると思います。
logrotate
これでログまで思い通りに書き出せる様になったわけですが、 せっかくなのでちゃんとしたデーモンになるためにログをローテションできる様にしたいと思います。
ログが大きくなったり古くなったりしたらlog.1
とかlog-20180910
とかに移して
新たなlog
に書き出していく、ということ。
これにはlogrotate
がインストールされて動いている必要があります。
logrotate
では/etc/logrotate.d/に各ログファイルの細かい設定などを書くことができます。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
missingok
: ログがなくてもエラーを出さない。rotate
: この数だけログが保存される。dateext
: hello_world.log-20180910の様な日付のついたローテーションファイルを作る。これがないとhello_world.log.1とか数字の着くローテーションファイルになる。delaycompress
:gzip
で圧縮する。ファイルはhello_world.log-20180910.gzの様になる。compress
だとすぐに圧縮、delaycompress
だと最新の一つは圧縮せずに通常ファイルのままにしておく。- プロセスがファイルに出力を続けていると変更された元のファイルに書き出し続けることがある場合、テキストファイルのママにしておかないと一部情報が失われてしまう。
daily
: 1日毎にローテーションを実行。minsize
: このサイズ以下ならローテーションしない。100M
で100MB、100k
なら100kB。postrotate
~endscript
: ローテーション後に実行するコマンド。
これで、ファイルが100MB以上になったら1日一回のチェックタイムのときに hello_world.logがhello_world.log-20180910に変更され、 それ以前のhello_world.log-20180909があればhello_world.log-20180909.gzと圧縮されます。
さらに古いファイルが10個以上になったら一番古いファイルが削除されます。
このとき、rsyslog
もhello_world
も変更を知らないので
元のファイル(hello_world.log-20180910)に書き出し続けます。
そこでpostrotate
を使ってrsyslog
とhello_world
のデーモンを再起動しています。
これで再び/var/log/hello_world.logに書き出される様になります。
rsyslog
の再起動に関しては、/etc/logrotate.d/syslogに
postrotate
/bin/kill -HUP `cat /var/run/syslogd.pid 2> /dev/null` 2> /dev/null || true
endscript
という記述があって、PIDファイルを使ってPIDを直接kill
する様になっています。
/usr/lib/systemd/system/rsyslog.serviceを見ると
Restart=on-failure
という記述があり、正常に終了しなかった場合にはリスタートするようになっていて、
kill
されたときなどは自動で再起動するのでlogrotate
でkill
して
再起動する様にしています。
systemctl
を使ったほうが良さげな感じですが、
システムによってはsystemctl
でかなりしてなかったりもするので
他のプログラムでもこの様なPIDファイルを使った記述が結構あります。
また、rsyslogのバージョン?によっては/var/run/rsyslogd.pidを使う場合もあるようで、
postrotate
/bin/kill -HUP `cat /var/run/syslogd.pid 2> /dev/null` 2> /dev/null || true
/bin/kill -HUP `cat /var/run/rsyslogd.pid 2> /dev/null` 2> /dev/null || true
endscript
と2つ書いてあるものもあります。
今回はhello_world
自体がPIDを使ってkill
することが面倒なので
systemctl
がある環境専用になりますが上の様な形で書いています。
インストール/アンインストールスクリプト
上のテストデーモンのスクリプトや設定ファイルを以下のレポジトリに置きました。
これをcloneして簡単にインストール出来るようにしてみました。
# https://github.com/rcmdnk/systemctl-hello-world.git
# cd systemctl-hello-world
# ./install.sh
で上のスクリプトや設定ファイルが入れられます。
uninstall.sh
でそれらのファイルを削除します。
今回のデーモンはシェルスクリプトなんでコンパイルとかも必要ないので 実行ファイルを置いて、上で設定した各種設定ファイルを置くだけです。
アンインストールも置いたものを削除するだけ。
インストール/アンインストールスクリプトの内容は以下の様な感じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
1 2 3 4 |
|
動作チェック
フィアルを置いてただけですが、ちゃんとsystemctl status
で確認でき、systemctl start
、systemctl stop
で起動/停止出来ることを確認します。
それからログファイルがちゃんと/var/log/hello_world.logに書かれ、かつ/va/log/messagesには出てないことを確認。
上にも書いたようにsyslog
に書き出すとjournal
からも参照出来る様になるのでそれも確認。
# systemctl status hello_world
● hello_world.service - Hello world daemon
Loaded: loaded (/usr/lib/systemd/system/hello_world.service; disabled; vendor preset: disabled)
Active: inactive (dead)
# systemctl is-active hello_world
inactive
# systemctl start hello_world
# systemctl status hello_world
● hello_world.service - Hello world daemon
Loaded: loaded (/usr/lib/systemd/system/hello_world.service; disabled; vendor preset: disabled)
Active: active (running) since Mon 2018-XX-XX XX:XX:XX UTC; 2s ago
Main PID: 29545 (hello_world)
CGroup: /system.slice/hello_world.service
├─29545 /bin/sh /usr/bin/hello_world
└─29546 sleep 10
Sep XX XX:XX:XX example_host systemd[1]: Started Hello world daemon.
Sep XX XX:XX:XX example_host systemd[1]: Starting Hello world daemon...
Sep XX XX:XX:XX example_host hello_world[29545]: Starting Hello World...
# systemctl is-active hello_world
active
# journalctl -u hello_world.service
-- Logs begin at Sun 2018-XX-XX XX:XX:XX UTC, end at Mon 2018-XX-XX XX:XX:XX UTC. --
Sep XX XX:XX:XX example_host systemd[1]: Started Hello world daemon.
Sep XX XX:XX:XX example_host systemd[1]: Starting Hello world daemon...
Sep XX XX:XX:XX example_host hello_world[29545]: Starting Hello World...
Sep XX XX:XX:XX example_host hello_world[29545]: Starting Hello World...
...
# cat /var/log/hello_world.log
Sep XX XX:XX:XX example_host hello_world: Starting Hello World...
Sep XX XX:XX:XX example_host hello_world: Hello world!
Sep XX XX:XX:XX example_host hello_world: Hello world!
...
# grep "hello_world" /var/log/messages
#
# systemctl status hello_world
● hello_world.service - Hello world daemon
Loaded: loaded (/usr/lib/systemd/system/hello_world.service; disabled; vendor preset: disabled)
Active: inactive (dead)
Sep XX XX:XX:XX example_host hello_world[29545]: Starting Hello World...
Sep XX XX:XX:XX example_host hello_world[29545]: Starting Hello World...
...
Sep XX XX:XX:XX gcp-wn-1core-09-template systemd[1]: Stopping Hello world daemon.
Sep XX XX:XX:XX gcp-wn-1core-09-template hello_world[29644]: 29645
Sep XX XX:XX:XX gcp-wn-1core-09-template systemd[1]: Stopped Hello world daemon.
このままだと再起動時には起動されませんが、起動するようにするには
# systemctl is-enabled hello_world
disabled
# systemctl enable hello_world
Created symlink from /etc/systemd/system/multi-user.target.wants/hello_world.service to /usr/lib/systemd/system/hello_world.service.
# systemctl is-enabled hello_world
enabled
と、is-enabled
でenabled
になってればOK。
logrotateも確認してみます。
# systemctl start hello_world
# logrotate -d /etc/logrotate.d/hello_world.conf
reading config file /etc/logrotate.d/hello_world.conf
Allocating hash table for state file, size 15360 B
Handling 1 logs
rotating pattern: /var/log/hello_world.log after 1 days (10 rotations)
empty log files are rotated, only log files >= 104857600 bytes are rotated, old logs are removed
considering log /var/log/hello_world.log
log does not need rotating (log has been already rotated)
#
logrotate -d
で実際にローテーションを行わないデバッグモードでテストすることが出来ます。
ここでは設定項目によってどの様な動作が行われてるか、というのが書いてあってdaily
だとか10 rotations
、100M
(104857600 bytes)とかが
正しく反映されてる事が確認できます。
最後のメッセージですでにローテション済というメッセージが出てますが、 これは一回もローテーションを行ってないと前回の実行時間の記録がなく、その場合は期間設定では引っかからない仕様になってるためです。
前回実行時間は/var/lib/logrotate/logrotate.statusにあります。
# cat /var/lib/logrotate/logrotate.status
logrotate state -- version 2
"/var/log/yum.log" 2018-9-09-17:0:0
"/var/log/boot.log" 2018-9-09-17:0:0
"/var/log/chrony/*.log" 2018-9-9-17:0:0
...
一度hello_world
について実行してみると
# logrotate -d /etc/logrotate.d/hello_world.conf
# cat /var/lib/logrotate/logrotate.status
logrotate state -- version 2
"/var/log/yum.log" 2018-9-9-17:0:0
"/var/log/boot.log" 2018-9-9-17:0:0
"/var/log/hello_world.log" 2018-9-10-1:0:0
"/var/log/chrony/*.log" 2018-9-9-17:0:0
...
とhello_world.log
の情報が加わります。
このままだとまた1日経ってないことになるので、このファイルを書き換えます。
1 2 |
|
これでデバッグしてみると
# logrotate -d /etc/logrotate.d/hello_world.conf
reading config file /etc/logrotate.d/hello_world.conf
Allocating hash table for state file, size 15360 B
Handling 1 logs
rotating pattern: /var/log/hello_world.log after 1 days (10 rotations)
empty log files are rotated, only log files >= 104857600 bytes are rotated, old logs are removed
considering log /var/log/hello_world.log
log does not need rotating ('misinze' directive is used and the log size is smaller than the minsize value
と言った感じで今度はminsize
よりも大きいからまだやる必要ない、と出ます。
ということでminsize
を書き換えてみます。
1 2 |
|
これで
# logrotate -d /etc/logrotate.d/hello_world.conf
reading config file /etc/logrotate.d/hello_world.conf
Allocating hash table for state file, size 15360 B
Handling 1 logs
rotating pattern: /var/log/hello_world.log after 1 days (10 rotations)
empty log files are rotated, only log files >= 1024 bytes are rotated, old logs are removed
considering log /var/log/hello_world.log
log needs rotating
rotating log /var/log/hello_world.log, log->rotateCount is 10
dateext suffix '-20180910'
glob pattern '-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'
glob finding old rotated logs failed
fscreate context set to system_u:object_r:var_log_t:s0
renaming /var/log/hello_world.log to /var/log/hello_world.log-20180910
running postrotate script
running script with arg /var/log/hello_world.log: "
systemctl restart rsyslog
systemctl restart hello_world
"
と、こんな感じにローテーションが必要で-20180910
というのでローテーションします、と出てきます。
ここで実際にやってみると
# logrotate /etc/logrotate.d/hello_world.conf
# ls /var/log/hello_world*
/var/log/hello_world.log /var/log/hello_world.log-20180910
# head -n3 /var/log/hello_world.log
Sep 10 01:34:57 example_host hello_world: 29888
Sep 10 01:34:57 example_host hello_world: Starting Hello World...
Sep 10 01:35:07 example_host hello_world: Hello world!
Sep 10 01:35:17 example_host hello_world: Hello world!
Sep 10 01:35:27 example_host hello_world: Hello world!
# tail -n3 /var/log/hello_world.log
Sep 10 01:34:32 example_host hello_world: Hello world!
Sep 10 01:34:42 example_host hello_world: Hello world!
Sep 10 01:34:52 example_host hello_world: Hello world!
みたいな感じで新しいログファイルに書き始めている事がわかります。
logrotateについては/etc/cron.daily/logrotateにcronファイルがあって、一日一回実行される様になっています。
logrotateには最近hourly
の設定もありますが、cronが一日一回なので
そのままhourly
設定をしても1日一回だけになります。
1時間に一回実行したいならこの/etc/cron.daily/logrotateを/etc/cron.hourlyに移すか
別途
/etc/cron.hourlyに
1
|
|
みたいなファイルを置いたりする必要があります。
まとめ
Systemdの導入によりデーモンの登録が非常に楽になりました。
今回は書いてませんが デーモン同士の依存関係なんかも簡単に書けて、 起動順序とかをサービスファイルに書くだけで簡単に設定できます。
これがかなり協力で、いままで複数のデーモン(サービス)を組み合わせて使いたいときにはユーザーがある程度管理する必要がありましたが Systemdでは設定がきちんと書いてあれば自動的に必要なものを起動したりもしてくれます。
Systemdでも以前のinitファイルもそのままラッパーをかけて使える様な形になってるので 古いサービスも動かすことも出来ます。
ただこれから自作するとするとあのinitスクリプトを書くのはとても面倒で Systemd用のものだけで済ませたいと思うところです。