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 で動くのがいいんだけど。。。

PythonのClickのサブコマンドをsymlinkで表現する作例

PythonClick

command [--debug] foo [--force] [--yes]

なのを実装したとして、これと同じのを command-foo というsymbolic linkを作って

command-foo [--debug] [--force] [--yes]

でも実行できるようにしたい作例。

command-foo --force --debug と実行するとエラーになるのがイマイチ。。。
他にいい方法があったら教えてください!!

REST API フレームワーク Connexion のススメとその作例

Python の Connexion というフレームワークとそのサンプルアプリケーションを書いたのでその紹介です。


Connexion は「API (spec) First」を謳うフレームワークです。

API First」とは、 ご存じ The Twelve-Factor App の追補として 2016 年に Pivotal 社が提唱したガイドライン Beyond the Twelve-Factor App の中のひとつです。

簡単にいうと、コードを書き始める前にまずAPIの仕様を定める。たとえば昨今のマルチデバイス対応のような複数チームで開発を進めるような現場で、お互いこのAPI仕様を拠り所とすることで、他方の開発プロセスに干渉することなく、円滑に開発を進めることができる、というものです。 (たぶん)

Connexion はこの API First を実践するのを助けてくれるプロダクトで、OpenAPI (過去に Swagger spec と呼ばれていたものです)で記述した仕様を軸として次のことが実現できます。

  • API 利用者向けのドキュメントを生成できる
  • 仕様に記述した入力パラメータの validation ルール(必須パラメータのあり/なしとか、正規表現で記述した値の形式チェックとか)を自動で実施してくれるので、適用コードを一切書く必要がない

現職で API サーバーを 3 つ実装して、今は Connexion で 4 つめを実装しているのですが、ドキュメントと実装の乖離は頭の痛い問題でした。具体的にいうと、値の形式チェックのルールをちょっと変えたんだけどドキュメントに反映しわすれていたとか、初期に書いたドキュメントの一部が実装から漏れていたとか、みなさんも容易に想像できるんじゃないでしょうか。

また、エンドポイントの情報(HTTPのメソッドとパス)をドキュメントと controller を呼ぶ router の両方に書かないといけないのが常々無駄だと思っていたのですが、Connexion が仕様を読んで所定の controller のメソッドを呼んでくれるので router を記述する必要がなくなったのもよい点の1つです。

Connexion を使うことでこういった問題から解放されたので本当に助かっています。

ほかにも API の動作をちょっと試したりデモするのに便利な Web UI のダッシュボードがついていたり、OAuth 2 のトークンベースの認証に対応していたりといろいろと特徴があるので詳しくは Connexion のサイトをみてみてください。


そんなこんなでここしばらく Connexion の試し書きをしていました。

などのサンプル実装を大いに参考にさせてもらったのですが、もうちょっと実践的な作例を書いてみました。

  • データストアは RDBMS で、 SQLAlchemy ORM の declarative mapping を使用
  • Connexion (の内部の Flask )との連携は Flask-Alchemy を使用
  • ORM のシリアライズ(dict化)には sqlathanor を使用
  • サンプルとして 2 つのテーブル(リソース)を定義して、one-to-many のリレーション
  • 検索条件の処理は、所定の SQL っぽい記法(JSON)でリクエストをすると、いい感じに SQLAlchemy の query オブジェクトを返してくれる簡易 query builder を書いた
  • pytest と WebTest を使ってテストも用意した

まだまだ Python は B- ぐらいウデマエの上に SQLAlchemy なども使ったことがなかったので、「こう書いたほうがいいでし!」とかあったらぜひ プルリ お待ちしています!

たくさんのホストにpingするのに便利なツールpingerを書きました

たくさんのホストにpingするのに便利なツールpingerをgoで書きました。

こちらから Linux, macOS, Windows 用のバイナリがダウンロードできるので是非お試しください。(手元に環境がないのでWindowsでは動作確認していません)

メンテナンス時、特にネットワーク関連のメンテには、多数のホストへの到達性をモニタリングしがら作業したいことがあると思いますが、(自分が知ってる限りの)既存のツールでは一覧性がよいものがなかったのがこのpingerを書いた動機です。

pingerを実行すると、このように上部には指定したホストの状況を2カラムで表示し、下部に失敗したホストの履歴を表示するので、現在の状況(上部)と過去の状況(下部)を同時に把握できるのが便利ポイントです。




ping対象のホストはpingerの引数にホスト名かIPアドレスで指定してください。思うがままにたくさん指定してください。

pingerの実行にはroot権限が必要なので、いずれかの方法で実行してください。

  • sudo pingerで実行する
  • rootで実行する
  • root で chown root pinger; chmod 4755 pinger しておいてから実行する
  • Linuxの場合、root で setcap cap_net_raw=ep pinger しておいてから実行する (azs! id:mapk0y

ESCかC-cをタイプすると終了します。

終了すると画面がクリアされてしまいます。pingに失敗した履歴を保存しておきたい場合は、標準エラー出力をファイルにリダイレクトしてください。

$ sudo pinger example.com example.net 192.0.2.1 192.0.2.2 192.0.2.3 2> pinger.log

ホスト名(IPアドレス)の横に2つの数値が表示されますが、左のはRTT、右の括弧内のは最大10個の直近の結果の平均RTTで、どちらも単位は ms です。


それではよいpingライフを!

hardware clockがずれる件と 11 minute mode

localtimeにしていたhardware clockをなんとなくUTCにしてみたら10分ぐらいで元に戻っちゃうというお話です。環境は Ubuntu 16.04 です。

結局、reboot しないとダメだったんですが、いい方法あったら教えてください><



hardware clockがlocaltimeになっているかUTCになっているかは、timedatectl の "RTC time" や "RTC in local TZ" のところを見るか、

### UTCの場合
$ sudo timedatectl
      Local time: Wed 2016-12-14 19:19:46 JST
  Universal time: Wed 2016-12-14 10:19:46 UTC
        RTC time: Wed 2016-12-14 10:19:46 # ココ
        Timezone: Asia/Tokyo (JST, +0900)
     NTP enabled: yes
NTP synchronized: yes
 RTC in local TZ: no # ココ
      DST active: n/a

### localtimeの場合
$ sudo timedatectl
      Local time: Wed 2016-12-14 19:34:20 JST
  Universal time: Wed 2016-12-14 10:34:20 UTC
        RTC time: Wed 2016-12-14 19:34:20 # ココ
        Timezone: Asia/Tokyo (JST, +0900)
     NTP enabled: yes
NTP synchronized: no
 RTC in local TZ: yes # ココ
      DST active: n/a

Warning: The RTC is configured to maintain time in the local time zone. This
         mode is not fully supported and will create various problems with time
         zone changes and daylight saving adjustments. If at all possible use
         RTC in UTC, by calling 'timedatectl set-local-rtc 0'.

hwclock --show --debug でも確認できます。

### UTCの場合
$ sudo hwclock --show --debug
hwclock from util-linux 2.20.1
Using /dev interface to clock.
Last drift adjustment done at 1478666918 seconds after 1969
Last calibration done at 1478666918 seconds after 1969
Hardware clock is on UTC time
Assuming hardware clock is kept in UTC time. # ココ
Waiting for clock tick...
...got clock tick
Time read from Hardware Clock: 2016/12/14 10:36:58
Hw clock time : 2016/12/14 10:36:58 = 1481711818 seconds since 1969
Wed Dec 14 19:36:58 2016  -0.074581 seconds

### localtimeの場合
$ sudo hwclock --show --debug
hwclock from util-linux 2.20.1
Using /dev interface to clock.
Last drift adjustment done at 1481708948 seconds after 1969
Last calibration done at 1481708948 seconds after 1969
Hardware clock is on local time
Assuming hardware clock is kept in local time. # ココ
Waiting for clock tick...
...got clock tick
Time read from Hardware Clock: 2016/12/14 19:37:03
Hw clock time : 2016/12/14 19:37:03 = 1481711823 seconds since 1969
Wed Dec 14 19:37:03 2016  -0.031894 seconds


hwclock --systohc --utc や timtdatectl set-local-rtc 0 で hardware clock を localtime から UTC に変更できるのですが、10分ぐらいするとなぜか元に戻っちゃいました。

これは hwclock(8) に依れば "11 minute mode" という、system clockがずれていないと信頼できるような環境で有益な、11分ごとにhardware clockをsystem clockの時刻に合わせるモードが原因でした。

"11 minute mode" が有効になっているかどうかは、ntptime の実行結果の status 行を見れば確認できます。ここにUNSYNCがなければ有効、あれば無効です。

### "11 minute mode" が有効になっている(=UNSYNCがない)
$ ntptime | grep status
  status 0x2001 (PLL,NANO),

### "11 minute mode" が無効になっている(=UNSYNCがある)
$ ntptime | grep status
  status 0x41 (PLL,UNSYNC),

もしくは adjtimex --print の実行結果の status の数値の 64 ビット目が0(STA_UNSYNC)ならば "11 minute mode" が有効になっていることを示します。


hwclock(8) にも書いてありますが、手元の環境でも ntpd を起動すると "11 minute mode" が有効になりました。(落とすと無効になる)


というわけで、ntpd を起動すると "11 minute mode" が有効になり kernel(?) が認識しているタイムゾーンと異なるのか、10分ぐらいすると hardware clock の時刻がずれてしまいました。

いろいろ

  • /etc/adjtime を消して hwclock --systohc --utc
  • timtdatectl set-local-rtc 0

やってみたんですが、解消できず、結局は ntpd を落として hardware clock を UTC にして reboot したら直りました。(起動後に ntpd を起動してももうずれない)