LoginSignup
1
4

More than 3 years have passed since last update.

alpineでC言語依存モジュールを pip install すると激重になる話

Last updated at Posted at 2020-05-19

TL;DR

PyPiに上げられているC言語依存のpythonモジュールは、alpine標準のmuslには対応してないから毎回手元でコンパイルされるよ。
docker:alpineでどうしても使いたい場合は必要モジュールをビルドしたイメージを個別に用意しておくと良いよ。

alpineさんちのpip事情

pythonのモジュール管理行うpip。そのpipで採用されており快適にモジュールの導入を可能にしているwheel形式だが、alpineに対応した.whlがPyPi上に存在しないモジュールがある。

wheel

先に少しだけwheelの話。
wheelは元々Eggという形式の後継で作られたもので、Built Distributionと呼ばれるインストール形式であるそう。
Built Distributionここの用語集を見る限りでは以下の通り。

Distribution 形式のうち、中身のファイルとメタデータをターゲットシステムの正しい場所へ移動するだけでインストールができるもの。

Egg や Wheel の実体は zip/tar などの圧縮形式の拡張なので、pip install時はダウンロードして展開後にpipで管理している場所にファイルを移動しているだけとなる。なので速い。

コンパイル済み拡張モジュールを含んだパッケージ1も同様で取得・展開・移動するだけなので、これによりストレスフリーに使うことができている訳だ。
...ただしPyPi上に使用しているOS・アーキテクチャに対応したwheelファイルが存在する場合のみである。2

alineはmusl-libcで動く

alpineはmuslを採用している。
muslは軽量・高速・シンプルを目標に標準Cライブラリの実装を1から行っており、glibcなどの非標準な拡張ライブラリにも対応している。alpineにぴったりだ。
ただし、完全な互換はまだ実現できていない模様で稀に使いたい関数が存在しなかったりする。

で、今回のここで書く内容もalpineがmuslを使用している事が原因で掲題通りの事象が発生している。

ちなみに読み方はマッスルらしい。つよそう。

alpineはglibcを使っていない

今日、多くのLinuxディストリビューションはglibcを採用しているし、バイナリも共有ライブラリとして動的リンクさせてるのも少なくない。
numpyなどC言語を使用しているモジュールも例に漏れず3、PyPiにアップロードされているwheelファイルはglibc環境下で動くことを想定したビルドがなされている。つまり musl lib上で動く事を想定していない。4
そのためalpine上でpip install numpyをするとサポートされた.whlが存在しないため.zipやら.tar.gzが降ってくる。ビルド前のソースコードを丸々落としてきているのだ。

Collecting numpy
  Downloading numpy-1.18.3.zip (5.4 MB)
     |████████████████████████████████| 5.4 MB 1.3 MB/s 
  Installing build dependencies ... done

ダウンロード後はalpine用に一からコンパイルが実行されwheelファイルを作成する。そのあと出来上がったwheelファイルをpipは取り込み直すことでpython上でimportできるようにしている。
これがC言語依存モジュールをpip installすると時間がかかる原因である。

※ 他言語やOS・アーキテクチャに依存しないモジュールや、pureなpythonで書かれたモジュールだとalpineでもwheel形式で落ちてくるため時間はかからない。

浮かび上がる諸問題

C言語依存しているものでも数秒でコンパイルできるものもあるが、numpyを入れようとすると数分程度かかってしまうしnumpy依存のscipyなどを使おうとすると更に数十分コンパイルに時間がかかってしまうこともある。

一回限りのビルドで今後全ての開発環境を賄えるローカル環境であればそれでも良いかもしれないが、
製品やサービスをCI/CDを含めた環境構築することを考えていくとなると、膨大なコンパイル時間はとてつもないほど大きな障害となる。
git のブランチをフックにしてunitテストが走るCI環境、サーバーなどにサービスを安全にデリバリーするCD環境、etc、etc...
毎回毎回長いコンパイルが走ってしまうと細かい修正の確認ですら一時間単位で浪費してしまうことになるし、他の作業が滞ってしまう原因にもなる。
そしてなによりtwitterをする時間が減ってしまう。大問題だ。
実際に業務でpandasをalpineに突っ込んでしまった時は睡眠時間まで減ってしまったのだから、冗談抜きで死活問題である。

回避策

これを防ぐためには以下のものが考えられる。

  1. alpineを諦める
  2. コンパイル済みdocker imageで対処する
    1. ベースのディストリビューションとして使う
    2. multi stage ビルドを利用する

alpineを諦める

最も手間が少なく考える時間をかける必要がない有効な方法である。
ubuntuやcentosなどコンパイル済みで手段も確立しているディストリビューションに乗り換えてしまうのだ。
ただし、既に作り込んでしまっていて容易に乗り換えられないケースもあるのでは無かろうか?
業務で詰まった時は次の方法を取った。

コンパイル済みdocker imageで対処する

wheelのコンパイルが済んだ状態のdocker imageを自由に使える場所に置いてしまい、使いたい時にpullするというもの。

base-image
FROM python:3.8-alpine3.11
COPY require/requirements.txt /tmp
RUN apk update && \
  apk add --no-cache hoge-dev && \
  pip install --upgrade --no-cache-dir pip setuptools wheel && \
  pip install -r --no-cache-dir /tmp/requirements.txt

先ずはこんな感じでimageを作っておき、docker pushでdocker hubやawsのecrなどに保存しておく。

docker push hoge/huga:latest

2パターンあるがビルド完了しているものを使うという意味では同じ。

ベースのイメージとして使う

公式イメージなどと同じようにFROMを使ってベースイメージとして使う。
簡単だが、コンパイル時のみに必要なライブラリを消し忘れたりするとこちらのイメージサイズも大きくなってしまうなど、base-imagedockerfileへの依存度が高くなりメンテナンス性が下がってしまう。

builder-pattern
FROM hoge/huga:latest

multi stage ビルドとして使う

Docker17.05以上なら使える機能で、成果物だけイメージから取り出すことができる方法。
wheelコンパイルに関連するものはhoge/huga:latestに封じ込められるので後々気が楽になるから個人的におすすめ。

builder-pattern
FROM hoge/huga:latest as pip_build
FROM python:3.8-alpine3.11

COPY --from=pip_build /usr/local/lib/ /usr/local/lib/
COPY --from=pip_build /usr/local/bin/ /usr/local/bin/
COPY --from=pip_build /usr/local/include/ /usr/local/include/

上記だとディレクトリを直接張り付けてしまっているので、コンパイル済みの*.whlhoge/huga:latestから持ってきてpip installする方が安全かも。5

欠点

解決策としなかったのは上記の方法だと、複数のdockerイメージのメンテナンスを避けられないためだ。
作成した実行環境よりもソースコードで使用するライブラリや書式の方が最新になってしまった場合が起こるたび、長大なコンパイルを実行しpushし直す必要が出てくる...
安眠できる日々は長くは続かなさそうだ。

参考文献

おまけ記事

alpineでC言語依存モジュールを pip install した時の時間を計測してみた
【5/26追記】
multi stage build でpythonのC言語依存モジュールを wheel形式でインストールする


  1. 用語集曰くBinary Distributionと呼ぶらしく、拡張モジュールごと移動させ使用している。 

  2. PyPiのモジュールのページ左側[Navigation]->[Download files]から確認できる。 

  3. ダウンロードしたwheelをunzipで展開してやると*.soが見える。pipが正しく動いている以上、OS・アーキテクチャに最適化されたものが存在しているはずだ。 

  4. muslはアプリケーションを単一のポータブルなバイナリファイルとして配布できるように静的リンクに最適化している。(wikipediaから引用) ld-musl-x86_64.so.1しか存在しないのでld-linux-x86-64.so.2にダイナミックリンクしているものは軒並み落ちる。 

  5. 記事書いてみました。multi stage build でpythonのC言語依存モジュールを wheel形式でインストールする 

1
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
4