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 が使いたい今日この頃です。

DropboxからGoogle Driveに乗り換えた

以前から Mac 複数台、Linux 複数台、iPhoneDropboxを使ってきたのだけど、最近マシンを交換したら3台制限に引っかかって同期できなくなってしまったので、Google One を契約してるのもあり無難なところでGoogle Driveに移行することにした。

方針

  • Googleドライブ直下に Sync というフォルダを作って、このフォルダだけ同期する (以前の ~/Dropbox/ 的な感じ)

セットアップ

macOSはまぁ普通にダウンロードしてインストールして設定すればいいんだけど、Googleドライブ直下のファイルの同期を抑止する方法がわからんかった。

LinuxGUIは必要ないのでGrive2を使うことにした。

0.5.1-dev(2019-07-12現在未リリース)から .griveignore ファイルで同期する対象の指定ができるようになったのでこれを使う。

ビルドとインストールはドキュメントの通り(ソースはリリースページのじゃなくて、git cloneするかmaster.zipを使う)ににして、初回実行はこんな感じ。

$ grive --version
grive version 0.5.1-dev Jul 11 2019 18:10:47

$ mkdir ~/GoogleDrive/ && cd ~/GoogleDrive/

$ cat << EOF > .griveignore
*
!Sync
EOF

$ grive -a

これで Sync だけが同期される。

griveは同期が終わると終了して常駐はしないので、定期的に、更新があったときに同期が実行されるようにする。

今回はホームディレクトリ下のディレクトGoogleDrive を基点ディレクトリとしたので、一般ユーザーで次のように仕込めばよい。

systemctl --user enable grive-timer@$(systemd-escape GoogleDrive).timer
systemctl --user start grive-timer@$(systemd-escape GoogleDrive).timer
systemctl --user enable grive-changes@$(systemd-escape GoogleDrive).service
systemctl --user start grive-changes@$(systemd-escape GoogleDrive).service

ログは journalctl で確認できる。

journalctl -f --user -u grive-timer@GoogleDrive.timer
journalctl -f --user -u grive-changes@GoogleDrive.service

というわけで今までありがとうDropbox

今回のおハマり

sshで入った先で systemctl --user がエラーになるときは、

$ systemctl --user
Failed to connect to bus: No such file or directory

sshd_configでUsePAM yesになっているか確認しましょう。

pam経由からのsystemd-logindからのログインじゃないのでセッションが作られません。

あわせて読みたい

Ubuntu 18.04 LTSからAMIを作る前にやらなければならないこと

AWS Marketplace の Ubuntu 18.04 LTS 20190514 からインスタンスを立てて、そのインスタンスからAMIを作る前には次のことをやらなければなりません。

ifupdownを削除する

sudo apt purge ifupdown

ifupdownパッケージがインストールされている状態で作ったAMIで起動すると、起動はするもののNICが見つけられずに一切のネットワークアクセスができないインスタンスになってしまいます。

/etc/netplan/50-cloud-init.yaml は削除する必要はなく、ifupdown がインストールされていなければ、新規インスタンスMAC アドレスにあわせて適切な設定ファイルが生成されました。

深追いしてませんが、ifupdown がインストールされていると、ifupdownに代わる新しいネットワーク管理システムの netplan が NIC を見つけられない/見つけようとしないんじゃないかと思います。

ちなみに ifupdown に依存しているパッケージはまだ結構あって( apt-cache rdepends ifupdown 調べ)、例えば ifenslave をインストールすると ifupdown もインストールされたりします。

なお、一度起動してしまえば ifupdown をインストールしても問題ありません。が、その後で /etc/netplan/50-cloud-init.yaml を削除して再起動すると、NIC がみつけられないインスタンスになってしまい詰むので注意してください。言い換えると、適切な /etc/netplan/50-cloud-init.yaml が存在するならば、ifupdown をインストールしても問題ありません。

/etc/machine-id を0バイトのファイルにする

: | sudo tee /etc/machine-id

/etc/machine-id にはそのホスト固有のIDが格納されています。

/etc/machine-id がある状態で作ったAMIで起動すると、同じIDを持ったインスタンスが作られてしまいます。

なので /etc/machine-id を初期状態にする必要があるのですが、先に示したように、0バイトのファイルにしなければなりません。

sudo rm -f /etc/machine-id でファイルを消したり、sudo bash -c 'echo > /etc/machine-id' で改行1バイトのみファイルにした場合は、起動時に再生成されないので注意してください。

CentOS 6でPython 3でTensorFlowを使う方法、もしくはdynamic linkerとshared objectの差し替え

Python 3

CentOS 6のopensslのバージョン(1.0.1e)の関係で、Python 3.7以上はビルドが失敗します。

今回は3.7にこだわりはないので、pyenvでPython 3.6の最新の3.6.8をインストールして、pipでtensorflowをインストールします。詳細は割愛ググってね。

glibc, stdc++

インストールに成功しても、使おうとするとエラーが出ます。

$ python3.6 -c 'import tensorflow; print("hello")'
Traceback (most recent call last):
...
ImportError: /lib64/libc.so.6: version `GLIBC_2.16' not found (required by /home/ore/.pyenv/versions/3.6.8/lib/python3.6/site-packages/tensorflow/python/_pywrap_tensorflow_internal.so)
...

$ ldd /home/ore/.pyenv/versions/3.6.8/lib/python3.6/site-packages/tensorflow/python/_pywrap_tensorflow_internal.so |& grep 'not found' | awk '{print $4}'
`GLIBC_2.16'
`GLIBC_2.14'
`GLIBC_2.17'
`GLIBC_2.15'
`GLIBCXX_3.4.15'
`GLIBCXX_3.4.19'
`GLIBCXX_3.4.14'
`CXXABI_1.3.7'
`GLIBCXX_3.4.17'
`GLIBCXX_3.4.18'
`CXXABI_1.3.5'
`GLIBC_2.14'
`GLIBC_2.16'
`GLIBC_2.17'
`GLIBCXX_3.4.14'
`GLIBCXX_3.4.18'
`CXXABI_1.3.5'
`GLIBCXX_3.4.15'
`GLIBCXX_3.4.19'
`GLIBCXX_3.4.17'
`CXXABI_1.3.7'

どようやらglibc 2.17とlibstdc++ 3.4.19が必要そうです。

さすがにシステムのglibcを置き換えるのは怖いので、別のディレクトリに展開して今回のpython3.6はそこのshared objectを使うようにします。

glibcとlibstdc++は、https://pkgs.org/ で探してCentOS 7のを拝借します。

$ cd ~/tmp/
$ wget \
  http://mirror.centos.org/centos/7/updates/x86_64/Packages/glibc-2.17-260.el7_6.5.x86_64.rpm \
  http://mirror.centos.org/centos/7/updates/x86_64/Packages/glibc-devel-2.17-260.el7_6.5.x86_64.rpm \
  http://mirror.centos.org/centos/7/os/x86_64/Packages/libstdc++-4.8.5-36.el7.x86_64.rpm \
  http://mirror.centos.org/centos/7/os/x86_64/Packages/libstdc++-devel-4.8.5-36.el7.x86_64.rpm \
  ;

~/lib/ の下に展開します。

$ cd ~/lib/
$ for p in ~/tmp/*.rpm; do echo $p; rpm2cpio $p | cpio -idv; done
$ ls -F
etc/  lib64/  sbin/  usr/  var/

以下、パス指定が長くなるので簡略化のため変数に入れときます。

py36="$HOME/.pyenv/versions/3.6.8/bin/python3.6"
pywrap_tensorflow_internal="$HOME/.pyenv/versions/3.6.8/lib/python3.6/site-packages/tensorflow/python/_pywrap_tensorflow_internal.so"

my_ld="$HOME/lib/lib64/ld-linux-x86-64.so.2"
my_libs="$HOME/lib/usr/lib64:$HOME/lib/lib64"

ld-linux.so はコマンドとしても実行できて、 --library-path でdynamic loadするshared objectのパスを指定できます。環境変数 LD_LIBRARY_PATH でも同様のことができますが、作用範囲が子プロセスまで及ぶかどうかという違いがあります。

ld-linux.so 経由で python3.6 を起動して、先程エラーになったコードを実行してみます。

$ $my_ld --library-path $my_libs $py36 -c 'import tensorflow; print("hello")'
hello

今度は正常終了しました!やったネ!!

child process

早速、最終的に走らせたいスクリプトを実行してみると…

$ $my_ld --library-path $my_libs $py36 oreno.py
* Serving Flask app "server" (lazy loading)
* Environment: production
...
Traceback (most recent call last):
...
ImportError: /lib64/libc.so.6: version `GLIBC_2.16' not found (required by /home/ore/.pyenv/versions/3.6.8/lib/python3.6/site-packages/tensorflow/python/_pywrap_tensorflow_internal.so)
...

失敗しました。

エラーメッセージからすると、システムのglibcをロードしてしまっているようです。strace -f -s 1000 $my_ld ... で確認してみると、後半で $my_ld を介さないで $py36execve して異常終了していました。詳細は確認してませんが、どっかで fork してるんじゃないかと思います。

子プロセスにも新しいglibcの場所を教えてあげればよいので、試しに LD_LIBRARY_PATH をセットして実行してみると…

$ env LD_LIBRARY_PATH=$my_libs $my_ld --library-path $my_libs $py36 oreno.py
...
/home/ore/.pyenv/versions/3.6.8/bin/python3.6: relocation error: /home/ore/lib/lib64/libc.so.6: symbol _dl_starting_up, version GLIBC_PRIVATE not defined in file ld-linux-x86-64.so.2 with link time reference

失敗しました。

たぶん、システムの /lib64/ld-linux-x86-64.so.2 で新しいglibcをロードしようとして失敗しているんだと思います。

もし、成功したとしても、LD_LIBRARY_PATH は作用範囲が大きいので、他の方法があれば避けたい手段です。

patchelf

Linuxの実行ファイル形式のELFには、dynamic linker (interpreter) とライブラリのサーチパス(runpath)を埋め込むことができます。これらは readelfpatchelf で確認することができます。

CentOS 6用の patchelf はEPELにあります。

$ readelf -a /usr/bin/mailq | grep -e interpreter -e runpath
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
 0x000000000000001d (RUNPATH)            Library runpath: [/usr/lib/postfix]

$ patchelf --print-interpreter --print-rpath /usr/bin/mailq
 /lib64/ld-linux-x86-64.so.2
 /usr/lib/postfix

patchelf はこれらの情報を変更することができるので、$py36interpreterとrunpathを変更してしまえば、 $my_ld --library-path $my_libs の前置が不要になるというわけです。

やってみましょう。

$ cp -p $py36 ${py36}.bak
$ patchelf --set-interpreter $my_ld --set-rpath $my_libs $py36

さっそく、$my_ld の前置なしで実行してみると…

$ $py36 -c 'import tensorflow; print("hello")'
Traceback (most recent call last):
...
ImportError: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.15' not found (required by /home/ore/.pyenv/versions/3.6.8/lib/python3.6/site-packages/tensorflow/python/_pywrap_tensorflow_internal.so)

エラーが出ました。が、これは想定内です。

$py36 は期待したとおり新しいglibcをロードできたのですが、_pywrap_tensorflow_internal.so がlibstdc++をロードしようとして新しいのが見つけられずエラーになっているわけです。(glibcは既に $py36 が新しいのをロード済みなので大丈夫)

なので、_pywrap_tensorflow_internal.so もrunpathを書き換えてしまいましょう。

_pywrap_tensorflow_internal.so は既にrunpathが設定されているので、それに追加する感じでセットします。(純粋なshared objectなのでinterpreterの変更の必要はありません)

$ORIGIN が展開されないようにシングルクォートするのを忘れないでください。

$ patchelf --print-rpath $pywrap_tensorflow_internal
$ORIGIN/../../_solib_k8/_U_S_Stensorflow_Spython_C_Upywrap_Utensorflow_Uinternal.so___Utensorflow:$ORIGIN/:$ORIGIN/..

$ cp -p $pywrap_tensorflow_internal ${pywrap_tensorflow_internal}.bak
$ patchelf --set-rpath '$ORIGIN/../../_solib_k8/_U_S_Stensorflow_Spython_C_Upywrap_Utensorflow_Uinternal.so___Utensorflow:$ORIGIN/:$ORIGIN/..'":$my_libs" $pywrap_tensorflow_internal

さて、

$ $py36 -c 'import tensorflow; print("hello")'
hello

$ $py36 oreno.py
* Serving Flask app "server" (lazy loading)
* Environment: production
...
INFO:werkzeug: * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
...

ようやく正常に起動できました!!!

が、socket.getaddrinfo() が失敗して名前が引けず…

まとめ

  • ld-linux.so はコマンドとしても実行できて、 --library-path でdynamic loadするshared objectのパスを指定できる(環境変数 LD_LIBRARY_PATH に比べ作用範囲を限定的にできる)
  • ELFには、dynamic linker (interpreter) とライブラリのサーチパス(runpath)を埋め込むことができ、patchelf コマンドで変更できる

ところで、

debootstrap で bionic の環境を作ってまるっとCentOS 6にコピーして chroot した方がよかったかもしれん… 試してないけど動くんじゃないかと…

kernelが古くて残念、ダメでした!

# chroot ./bionic /bin/bash
FATAL: kernel too old

# file bionic/bin/bash
bionic/bin/bash: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 3.2.0, stripped

メインマシンをUbuntu 14.04から18.04にしてみた

Ubuntu 14.04 (trusty) が 2019-04-30 で EOL になる

Ubuntu 14.04 LTS Trusty Tahr Extended Security Maintenance | Ubuntu blog

のと、day jobの方でも18.04 (bionic)を使う予定なので、メインマシンのtrustyをbionicにしてみた。

Ubuntu

に書いてある通り、do-release-upgrade 実行するだけ。楽ちん。一足飛びにはいけないので、trusty→xenial、xenial→bionic した。

アップグレード自体は特に問題なかった。

グラフィカルログインの無効化

漢は黙ってコンソールログイン派なのでlightdmとかgdmとかそういうのを無効化する。

systemctl set-default multi-user.target

dig

デフォルトで +edns するようになったようで、一部のDNSサーバーがFORMERRを返して名前が引けないので無効化した。

echo '+noedns' > ~/.digrc

Emacs

なんとなく勢いで24.4から26.1にしてみた。

ファイルが意図しないwindowで開く

例えば、1つのframeを上下2つのwindowに分割してて、上のwindowでfind-fileすると下のwindowで開く、とか謎挙動しだした。

原因は popwin の古い設定

(setq display-buffer-function 'popwin:display-buffer)

が残っていたせいで、これを削除したらOKになった。

さようならsense-region、こんにちはsense-expand-region

sense-region.elをロードすると警告が出るようになった。

Warning (bytecomp): ‘interactive-p’ is an obsolete function (as of 23.2); use ‘called-interactively-p’ instead.
Warning (bytecomp): ‘mapcar’ called for effect; use ‘mapc’ or ‘dolist’ instead [16 times]

動作はするけど、sense-region.el自体、配布元にアクセスできなくなって久しいので、結局、sense-expand-region.el に乗り換えました。

sense-region.elと同じように C-SPC に割当。

(global-set-key (kbd "C-SPC") 'sense-expand-region)

最初 emacs-jp Slack の人たちに相談したら、矩形選択は標準の rectangle-mark-mode を、選択リージョンの拡張は expand-region を教えてもらいました。(あざます!!)

expand-region は最初の発動でいきなり範囲選択になる(sense-regionはset-mark-command)のに慣れなくて、ぐぐったら sense-expand-region.el を見つけて、なんとこれで矩形選択もできるので乗り換えた次第。

synergy

今まで

  • server
    • Ubuntu trusty
    • synergys 1.4.12
  • client
    • macOS Mojave (10.14.3)
    • synergyc 1.3.1 (数年前に昔のmacOSで自ビルドしたやつ)

で問題なく使えてたんだけど、bionicのsynergys 1.8.8 にしたらキーボードとマウスは共有されるんだけど、クリップボードが共有されない。これが思いの外、ストレスマックスボンバー。

1.8.8のバイナリ配布

https://sourceforge.net/projects/synergy-stable-builds/

から macOS 版をダウンロードして試したら、マウスとクリップボードは共有されるけど、なんとキーボードが共有されない!!

server, client共に最新の1.10.1(今回の件で買った!)にしても、キーボードが動かない。

trustyで動いてた1.4.12をbionicでビルドしたところ、たぶん libcrypto++ のシグネチャが違うかなんかでビルドできず。

他にもdebootstrapでtrustyの環境作ってchroot実行してみた(SEGV!)り、他のバージョンでも試したりいろいろやったけどダメで、結局、trustyからsynergysとlibcrypto++.so.9.0.0をコピーしてきて、

#!/bin/sh

exec env LD_LIBRARY_PATH=/usr/local/app/synergy-1.4.12/lib \
     /usr/local/app/synergy-1.4.12/bin/synergys "$@"

な感じで立ちあげて無事、全部共有できて一旦めでたしめでたし。

でも、macOSの方の1.3.1は、この前MacBookを乗り換えたときにソースコード消しちゃったようだし、ソースがあってもCarbonのヘッダーファイルないとコンパイルできないと思うのでもう最新のXcodeではビルドできないので、本当はせっかく買った 1.10 で動くのがいいんだけど。。。