これだけ覚えればOK、rsyncのディレクトリ、ファイルパスの指定方法

ディレクトリどうしをまるごとコピーしたい場合

コピー元・先両方に末尾に / をつけて、コピー先のディレクトリまで指定する。

rsync -av /path/to/dir/ remote:/path/to/dir/

rsync -av /path/to/dir remote:/path/to とか別の書き方もあるけどパット見わかりづらいので ダメ

ディレクトリの末尾に / をつけるのが肝要。

ファイルをコピーしたい場合

コピー元・先両方をファイルのパスで指定する、 もしくは、 コピー元をファイルのパスで、コピー先を末尾に / をつけたディレクトリのパスで指定する。

rsync -av /path/to/file remote:/path/to/file
もしくは
rsync -av /path/to/file remote:/path/to/

おまけ

コピー元・先でパスが同じ場合

コピー元・先でパスが同じなら -R を使うのもよい。コピー先のパス指定ミスが防げるので。

rsync -avR /path/to/dir/ remote:/
rsync -avR /path/to/file remote:/

コピー元がリモートで、複数指定する場合

rsync -av remote:/path/to/file1 remote:/path/to/file2 /path/to/dir/

と書いてもよいが、リモートホスト名は省略できる。

rsync -av remote:/path/to/file1 :/path/to/file2 /path/to/dir/

Ubuntu 18.04 で Postfix の multi instances の service unit を有効にする方法

Managing multiple Postfix instances on a single host に従い、multi instances の設定をした後、その service unit を用意する方法のメモ。

  • /lib/systemd/system-generators/* などにあるファイルはOS起動時や systemd のリロード (systemctl daemon-reload) 時に実行される
  • postfix パッケージに /lib/systemd/system-generators/postfix-instance-generator が含まれている
  • postfix-instance-generator は、 postconf -h multi_instance_directories の結果に応じて /run/systemd/generator/postfix.service.wants/postfix@postfix-XXX.service な symlink を作る
    • postmulti -I postfix-XXX -G mta -e create すると、 /etc/postfix/main.cfmulti_instance_directories/etc/postfix-XXX が追加される
  • /run/systemd/generator/postfix.service.wants/* は、 systemctl start postfix したときに一緒に start される
  • なので、systemctl start postfix で子インスタンスも起動される。stopとかrestartも同様

結論

インスタンスの設定したあとに systemctl daemon-reload すればおk

Apache が AH00144 で落ちる件

事象

  • Ubuntu 18.04
  • apache2 (2.4.29-1ubuntu4.12)

で、apache2 プロセスが次のエラーメッセージを吐いて落ちるという連絡を受けて調べました。これはその原因と対処法のメモです。

[mpm_prefork:emerg] [pid 18633] (43)Identifier removed: AH00144: couldn't grab the accept mutex
[mpm_prefork:emerg] [pid 18632] (43)Identifier removed: AH00144: couldn't grab the accept mutex
[core:alert] [pid 18624] AH00050: Child 18632 returned a Fatal error... Apache is exiting!
[:emerg] [pid 18624] AH02818: MPM run failed, exiting

原因

簡単にいうと、Apache が Mutex に使っているセマフォを systemd-logind が消してしまうのが原因です。

Apache の Mutex

Mutex ディレクティブ のデフォルトは Mutex default で、どの機構が採用されるかは APR に委ねられています。

どの機構が採用されるかはこのようにして確認できます。

$ cat mutex-default.c
#include <apr_portable.h>

int main(int argc, char** argv)
{
    printf("%s\n", apr_proc_mutex_defname());
    return 0;
}

$ gcc $(apr-config --includes) mutex-default.c $(apr-config --link-ld)
$ ./a.out
sysvsem

存在する IPC セマフォなどの確認は ipcs コマンドでできます。

Apache が起動していれば、セマフォがいくつか表示されるはずです。

$ ipcs -a
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status

------ Semaphore Arrays --------
key        semid      owner      perms      nsems
0x00000000 5111808    gyoza      600        1
0x00000000 5144577    gyoza      600        1
0x00000000 5046274    gyoza      600        1
0x00000000 5177347    gyoza      600        1
0x00000000 5210116    gyoza      600        1

systemd-logind の RemoveIPC

systemd-logind はデフォルトで RemoveIPC=yes になっています。

RemoveIPC とは logind.conf(5) によれば次のように説明されています。

RemoveIPC=
    Controls whether System V and POSIX IPC objects belonging to the user shall be removed
    when the user fully logs out. Takes a boolean argument. If enabled, the user may not
    consume IPC resources after the last of the user's sessions terminated. This covers
    System V semaphores, shared memory and message queues, as well as POSIX shared memory
    and message queues. Note that IPC objects of the root user and other system users are
    excluded from the effect of this setting. Defaults to "yes".

つまり、

  • logind 管理下のセッションが全てなくなるときに、そのユーザーの IPC セマフォなどを全て削除する
  • ただし、root とシステムユーザーは除外される

ということです。

システムユーザーとは、 uid が 999 以下のユーザーのことです。

再現方法

  • Apache を非システムユーザー (uid が 1000 以上) で起動する → セマフォが作られる)
  • その非システムユーザーで SSH ログインする
  • ログアウトする → セマフォが消される
  • Apache にリクエストが来るとプロセスが死ぬ

これで再現します。

通常は Apache はシステムユーザである www-data の権限で起動されるのでこの問題は発生しませんが、この環境では深淵な理由で非システムユーザーで起動していました。

対処法

内製のデーモンで IPC を使っていると同じようにハマりそうなので、後者の対処法を採用しました。

Apacheセマフォではない Mutex を使う

Mutex file:/var/lock/apache2 default

systemd-logind で IPC を消さないようにする

# install -d -o root -g root -m 755 /etc/systemd/logind.conf.d/

# cat << EOF > /etc/systemd/logind.conf.d/ipc.conf
[Login]
RemoveIPC=no
EOF

# systemctl restart systemd-logind.service

kernel panic 時の oops メッセージを netconsole でインターネット越しに送信する

自宅サーバー (!) がちょいちょい落ちるんです。たぶんコンソールには kernal panic 時の oops メッセージが表示されてると思うんですが、モニタは繋いでないし宅内には他にサーバーもいないのでシリアル経由で送信することもできないので確認する術がなく。

で、そういえば netconsole ってのがあったなーと思い出したんで、設定してみた次第です。

ちなみに送信先GCE の Always Freeインスタンス (ちゃんと中国とオーストラリアからのアクセスはフィルタしています) です。

疎通の確認

netconsole は UDP を使います。受信側のポート番号を決めて (6666 とします) 、まずはそれが通るようにフィルタリングの設定をします。

疎通確認はこんな感じで。

# 受信側
sudo tcpdump -i any -nlxX port 6666
# 送信側
echo konyanya-chiwa > /dev/udp/DEST_IP_ADDRESS/6666

送信側の設定

echo 'options netconsole netconsole=6665@SRC_IP_ADDRESS/eth0,6666@DEST_IP_ADDRESS/GW_MAC_ADDRESS' | sudo tee /etc/modprobe.d/netconsole.conf
echo netconsole | sudo tee -a /etc/modules

sudo modprobe netconsole

試行錯誤するときはこんな感じでおk

sudo modprobe netconsole 'netconsole=6665@SRC_IP_ADDRESS/eth0,6666@DEST_IP_ADDRESS/GW_MAC_ADDRESS'

sudo rmmod netconsole

受信側の設定

syslog は使わずに udplogger を使いました。

curl -L -o udplogger.c 'https://osdn.net/projects/akari/scm/svn/blobs/head/branches/udplogger/udplogger.c?export=raw'
gcc -Wall -o udplogger udplogger.c

mkdir /var/log/netconsole
udplogger dir=/var/log/netconsole port=6666

動作確認

送信側で

echo s | sudo tee /proc/sysrq-trigger

すると、受信側で

# /var/log/netconsole/YYYY-MM-DD.log
2020-02-21 13:25:36 192.0.2.100:6665 [271267.178610] sysrq: Emergency Sync

と出力されるはずです。

参考

Ubuntu 18.04 で OS 起動時の apt update と unattended-upgrade を抑制する方法

時間のない人向けのまとめ

sudo systemctl edit apt-daily.timer
sudo systemctl edit apt-daily-upgrade.timer

どちらも次の内容で保存します。

[Timer]
Persistent=false

もしくは、直接ファイルを編集して反映してもよいです。

sudo install -d -o root -g root -m 755 /etc/systemd/system/apt-daily.timer.d
cat <<EOF | sudo tee /etc/systemd/system/apt-daily.timer.d/override.conf
[Timer]
Persistent=false
EOF

cp -pR /etc/systemd/system/apt-daily.timer.d/ /etc/systemd/system/apt-daily-upgrade.timer.d/

sudo systemctl daemon-reload

何が問題なのか?

Ubuntu 18.04、少なくとも EC2 用の Ubuntu cloud image の 20200131 版 (18.04.4) では、毎日 6:00 頃に apt updateunattended-upgrade (セキュリティ関連の更新パッケージのインストール) が行われる (詳しくは後述) のですが、その時間帯に電源がオフで稼働していなかった場合は、次の OS 起動時に実行される設定になっています。

「その時間帯に電源がオフで稼働していなかった」は、AMI からインスタンスを起動したときも同じ状況になるので、AMI が古ければ古いほど更新パッケージが多くなり、起動直後に負荷が高まったり、しばらく apt install できない (unattended-upgrade がロックを獲得しているので) 時間が続いたりします。

その結果、次のような問題が発生します。

  • インスタンス起動後に自動的にプロビジョニングを行うようにしている運用の場合、プロビジョニング (の過程のパッケージのインストール) が完遂するまでの時間が安定しなかったり、場合によってはタイムアウトしてエラー終了してしまう

特にオートスケーリングの場合は、一刻も早くインスタンスを投入したいので深刻な問題になります。

またもし、AMI 採取用のインスタンスから日次で AMI を作る運用の場合は、パッケージはほぼ最新であることが期待できます。

というわけで、 OS 起動時の更新処理を実行されないようにした、というお話でした。

時間のある人向けの詳細

まず定期的に更新処理が実施される仕組みの説明から。

次の 2 つの systemd の timer ユニットがトリガーとなります。

  • apt-daily.timer
  • apt-daily-upgrade.timer

内容を確認すると、

$ systemctl cat apt-daily.timer
# /lib/systemd/system/apt-daily.timer
[Unit]
Description=Daily apt download activities

[Timer]
OnCalendar=*-*-* 6,18:00
RandomizedDelaySec=12h
Persistent=true

[Install]
WantedBy=timers.target


$ systemctl cat apt-daily-upgrade.timer
# /lib/systemd/system/apt-daily-upgrade.timer
[Unit]
Description=Daily apt upgrade and clean activities
After=apt-daily.timer

[Timer]
OnCalendar=*-*-* 6:00
RandomizedDelaySec=60m
Persistent=true

[Install]
WantedBy=timers.target

となっており、 apt-daily.timer は毎日 6:00 と 18:00 頃、 apt-daily-upgrade.timer は毎日 6:00 頃に発火するのがわかります。

発火すると、対応する systemd の service ユニットが実行されます。 systemctl cat で確認すると、実行されるコマンドライン (ExecStart) が確認できます。

  • apt-daily.service
    • ExecStart=/usr/lib/apt/apt.systemd.daily update
  • apt-daily-upgrade.service
    • ExecStart=/usr/lib/apt/apt.systemd.daily install

ざっくり言うと、

/usr/lib/apt/apt.systemd.daily update は - apt-get update - apt-get --download-only dist-upgrade - unattended-upgrade --download-only

/usr/lib/apt/apt.systemd.daily install は - unattended-upgrade

を行うためのもので、APT の設定で個別に実施する/しないを制御することができます。

デフォルトでの次のような設定になっていて、

$ apt-config dump | grep Periodic
APT::Periodic "";
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "0";
APT::Periodic::AutocleanInterval "0";
APT::Periodic::Unattended-Upgrade "1";
  • apt-get update 実施する
  • --download-only 系は実施しない
  • unattended-upgrade 実施する

という挙動になります。

ここまでが定期的に実行される仕組みの説明で、次からは主題の OS 起動時の更新処理の仕組みについてです。

timer ユニットの定義をみると Persistent=true というのがあり、これがキモです。

systemd.timer(5) から引用すると

Persistent=

Takes a boolean argument. If true, the time when the service unit was last triggered is stored on disk. When the timer is activated, the service unit is triggered immediately if it would have been triggered at least once during the time when the timer was inactive. This is useful to catch up on missed runs of the service when the system was powered down. Note that this setting only has an effect on timers configured with OnCalendar=. Defaults to false.

とのことなので、冒頭の方法で Persistent=false と上書き設定したわけです。

ちなみに

定期的なのも含め一切の自動的な更新処理をオフにしたい場合は、 /usr/lib/apt/apt.systemd.daily

# check if the user really wants to do something
AutoAptEnable=1  # default is yes
eval $(apt-config shell AutoAptEnable APT::Periodic::Enable)

if [ $AutoAptEnable -eq 0 ]; then
    exit 0
fi

という処理があるので

echo 'APT::Periodic::Enable "0";' | sudo tee /etc/apt/apt.conf.d/99disable-periodic

とかするといいんじゃないかと思います。

あと /etc/cron.daily/apt-compat という crontab があるんですが、

if [ -d /run/systemd/system ]; then
    exit 0
fi

となってるので systemd な環境では無いのと同じです。

MySQL 5.7でクライアントプログラムがCPUを食いつぶす件

同根のバグレポートが散見されますが、ここ数ヶ月、状況をみるに修正される見込みがなさそうなので記録しておきます。

事象

MySQL 5.7 の libmysqlclient (libmysqlclient.so.20) を使用しているプロセスが、無限ループに突入して CPU (user%) を食いつぶし続ける。

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
28150 hirose31  20   0   32488   6080   5492 R  93.8  0.3   0:16.70 mysql

発現条件

MySQLが提供しているパッケージのMySQL 5.7.25, 5.7.26, 5.7.27 で確認。

再現手順

  1. mysql_real_connect() でサーバーに接続する
    • その際に以下の条件を満たすこと
    • reconnect option を有効にしない
    • TCP で接続する(unix domain socket では再現しない)
    • 同じホストの mysqld に接続する
  2. なんらかのシグナルを受信する
  3. mysqld サーバー側からコネクションを切断する
    • 例えば以下のようにして切断する
    • wait-timeout を超過する
    • mysqld を停止する
  4. mysql_real_query() もしくは mysql_ping() が呼ばれる
  5. 無限ループに突入して CPU (user%) を食いつぶす

なお、MySQL 5.6 (libmysqlclient 18) や MySQL 8.0 (libmysqlclient 21) では発生しない。原因は yaSSL にあるのだけど、MySQL 8.0 は yaSSL ではなく OpenSSL を使っているので。

再現コード

回避方法

MySQL サーバーの my.cnf の [mysqld] グループに skip-ssl と書く。

# /etc/my.cnf
[mysqld]
...
skip-ssl
...

追記 2019-11-22

SSL の実装はソースツリー内にある yaSSL と OpenSSL とで選択できたのですが、MySQL 5.6.46, 5.7.28 から yaSSL が削除され OpenSSL のみとなりました。

当然、バイナリで配布されているパッケージも OpenSSL を使うようにビルドされています。そこで 5.7.28 で試したところ、問題が再現しないことが確認できました。

gunicornをsupervisorで制御しつつhot deployできるようにする

タイトルの通り、gunicornなWebアプリをsupervisorで制御しつつhot deployできるようにしたメモです。

登場人物

問題

  • gunicornのhot deployを利用する場合、直接はsupervisorの制御下に置けない
    • gunicornはhot deployの仕組みを持っている
    • 流れ
      • gunicornのmasterプロセスにUSR2シグナルを送ると、新しいmasterプロセスを産む
        • つまり新masterの親プロセスは旧master
      • 旧masterと旧workerは生き続けているので、ほどよい頃合いで旧masterにQUITシグナルなどを送って死んでもらう
      • 新masterの親が死んだので、init (PID 1) が新masterの親となる
    • というわけで、supervisor視点だとgunicorn(旧master)が死んだのでまた立ち上げようとするが、新masterはinitを親として起動しているので競合しちゃう次第

結論

  • supervisor + unicornherder + gunicorn

案1: start_server配下でgunicornを立ちあげ、hot deployはstart_serverの機能を使う

## /etc/supervisor/conf.d/oreno.conf
[program:oreno]
command=/home/hirose31/oreno/script/start
directory=/home/hirose31/oreno
...
## /home/hirose31/oreno/script/start
#!/bin/bash

export PYENV_ROOT="/home/hirose31/oreno/pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

exec start_server \
     --port 127.0.0.1:1919 \
     --signal-on-term=TERM \
     --signal-on-hup=QUIT \
     --kill-old-delay=10 \
     -- \
     /home/hirose31/oreno/script/start-gunicorn
## /home/hirose31/oreno/script/start-gunicorn
#!/bin/bash

if [[ -n "$SERVER_STARTER_PORT" ]]; then
  GUNICORN_FD=
  for l in $(echo $SERVER_STARTER_PORT | tr ';' ' '); do
    GUNICORN_FD="$GUNICORN_FD $(echo $l | cut -d= -f2)"
  done
  export GUNICORN_FD=$(echo $GUNICORN_FD | tr ' ' ',')
fi

exec /usr/local/bin/envdir /home/hirose31/oreno/env \
  gunicorn -w 3 --reuse-port --capture-output oreno:application

これで supervisorctl signal HUP oreno で start_server に HUP を送ると、start_serverが新たにgunicornを起動して、10秒後に古いgunicornにQUITを送って殺してくれる。はず。

つまり、(gunicornの機能ではなく)start_serverの機能でhot deployが実現できる。はず。

start_server に対応するにはいくつかの要件が必要ですが、

UnicornUNICORN_FD と同様のものが gunicorn には GUNICORN_FD という環境変数名で存在するので、gunicornもstart_serverに対応できる。はず。

そう。はず、なのであった!

gunicornの このpull request で、GUNICORN_FD を評価するところのがこのような条件付きになり、

### gunicorn/arbiter.py: start
            elif self.master_pid:
                fds = []
                for fd in os.environ.pop('GUNICORN_FD').split(','):
                    fds.append(int(fd))

初っ端の起動時は master_pid は 0 なので GUNICORN_FD は評価されないのであった!!

条件を elif os.environ.get('GUNICORN_FD', ''): にすれば期待通り動くことは確認できたんだけど、できればgunicornに手を入れないで使いたいのでこの案はとりあえずボツに…

将来、外部由来の GUNICORN_FD を評価するようになったら、gunicornは--configオプションでpythonの文法で設定が書かれたファイルを指定することができるので、そこで SERVER_STARTER_PORT を参照して GUNICORN_FD をセットする処理を書いたり、pre_fork, post_fork hookを使って、Server::Starter で Unicorn を起動する場合の Slow Restart - sonots:blog と同じことも実現できるんじゃないかと思います。

余談ですが、start_serverserver-prog の指定は、ラップしたスクリプトのみ、オプション指定無しにするのをお勧めします。

例えば start_server -- /my/app -p 8 で起動していて -p 16 に変えたいときは、start_server 自体の再起動が必要(つまりhot deployできない)になってしまいます。これがラップしていて start_server -- /my/app_wrapper で起動している場合は、app_wrapper の中を書き換えて start_server に HUP を送ればOKになります。

start_server -- envdir ./env sh -c 'exec /my/app -p ${MAX_PROCESS:-8}' と書けばHUPで引数の値を(envdir経由で)変えることは可能ですが、やや技巧的だしオプションの種類の増減には対応できないので、素直にラッパースクリプトを用意した方がよいでしょう。

案2: unicornherder配下でgunicornを立ちあげ、hot deployはgunicornの機能を使う

「gunicorn supervisor」でググるとみなさんお困りのようで、対応として rainbow-saddleunicornherder がよく挙げられています。

rainbow-saddleは「cannot actively maintain it」だそうなので、unicornherderを試してみます。

## /etc/supervisor/conf.d/oreno.conf
[program:oreno]
command=/home/hirose31/oreno/script/start
directory=/home/hirose31/oreno
...
## /home/hirose31/oreno/script/start
#!/bin/bash

export PYENV_ROOT="/home/hirose31/oreno/pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

exec unicornherder -o 10 -p /var/run/oreno.pid -- \
    -w 3 --reuse-port --capture-output oreno:application

unicornherderの流れはこんな感じです。

  • gunicornを--pid FILE --daemonつきで起動する
    • masterのPIDは所定のファイルに書き出される
      • unicornherderの--pidfile PATHでPIDファイルを指定しないと、cwdにPIDファイルが作られるので注意
    • gunicornはunicornherderの元を離れ、initの子プロセスとなる
  • unicornherderはHUPを受けると、gunicornのmasterにUSR2を送る
  • gunicornのhot deployが開始され、新masterが誕生する
    • gunicornによってPIDファイルが更新される
  • unicornherderはPIDファイルを2秒間隔でポーリングして、masterの変化を検知する
  • masterの変化を検知すると、
    • overlap (デフォルト120秒) の時間だけsleepする
      • この間は新旧workerが混在する
    • 後、旧masterを殺す
      • masterにWINCH, QUITシグナルを送る
      • QUITを受けたworkerは処理中であっても即、死ぬ

unicornherderはgunicornを子プロセスとしておかず、gunicornはinitの子プロセスとなる点に最初「オッ!」っと思いましたが、これは古き良きdaemonしぐさですからまぁ違和感はないです。

これで一応、問題は解決できたんですが、unicornherderはgunicornコマンドでのgunicornの起動しかサポートしていません。

gunicornの起動方法、Pythonの各種WAFとの連携方法は gunicornコマンド以外にもあります。例えば、Bottle だと bottle.run()server='gunicorn' を与えると、gunicornが起動します。

その場合は、unicornherderは--gunicorn-bin, -gオプションでgunicornのフルパスを指定できるのでこれを使います。

## /home/hirose31/oreno/script/start
#!/bin/bash

export PYENV_ROOT="/home/hirose31/oreno/pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

exec unicornherder -o 10 -p /var/run/oreno.pid \
    -g /home/hirose31/oreno/script/oreno-gunicorn \
    --

oreno-gunicorn はお好みの方法でgunicornなプロセスを立ち上げる処理を書いたスクリプトです。

## /home/hirose31/oreno/script/oreno-gunicorn
#!/bin/bash

exec /home/hirose31/oreno/bin/oreno-python /home/hirose31/oreno/omaeno.py

注意点は以下の通りです。

  • unicornherderの-pオプションで指定したPIDファイルと、gunicornが書き出すPIDファイルを一致させる
  • gunicornはdaemonモードで起動するようにする

おわりに

unicornherderを使えば目的が達成できることがわかりましたが、個人的に安心と信頼と実績のある start_server が使いたい今日この頃です。