GitHub Action 上で docker layer をキャッシュする

GitHub Action を CI やビルドツールとして使うとき,手元と動作を完結にさせたいので docker で済ませたいことがある. コンテナリポジトリにある docker image から直接 run できる場合は pull するだけで良いが, Dockerfile を別途用意する場合は image の取得に加えてビルドが必要になり,毎回時間がかかる.

例えば,circleci を使用する場合は,docker layer をキャッシュするオプションを追加するだけで良いが, GitHub Action には(現状)そういった使い勝手の良いキャッシュは無い.

circleci.com

代わりに任意のパス以下をキャッシュしてくれる組み込みの機能が存在している.

github.com

ただ,この actions/cache はまだまだ機能が不十分で,docker layer のキャッシュには工夫が必要だったのでこの記事はその対策を書く. 記事公開時点での対策であるため,公式なサポート等が出た場合はそれを利用することを推奨する.

actions/cache の制約

指定したパス以下をなんでもキャッシュしてくれるわけではなく,使用にあたっていくつかの制約が存在する.

  • パスごとの最大サイズは 400MB
  • リポジトリごとに最大のサイズが 2GB
    • 2GB を超える場合は古いキャッシュから順に削除される

2GB は仕方ないとして,docker layer のキャッシュとしてディレクトリごとに 400MB は制約が結構厳しい.

対策: 複数のパス以下に分散して配置する

あまり難しいことを考えずに,docker history コマンドから layer のハッシュ値を持ってきて,それを docker save で書き出す. もし 400MB を超える場合があるならば複数ファイルへ分割して配置する.

以下はサンプル.cache の複数パス指定は現段階ではサポート外であるため*1, 残念ながら冗長に必要な分だけ配置する必要がある. サンプルは2分割であるため冗長に書いているが,必要に応じて分割数を指定できるスクリプト等を用意したほうが良いかもしれない.

name: Build and Run with cache
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v1

    - uses: actions/cache@v1
      name: Splitted cache 01
      id: cache01
      with:
        path: docker-cache/01
        key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }}-01
        restore-keys: ${{ runner.os }}-docker-01

    - uses: actions/cache@v1
      name: Splitted cache 02
      id: cache02
      with:
        path: docker-cache/02
        key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }}-02
        restore-keys: ${{ runner.os }}-docker-02

    - name: Load cached docker layers
      if: steps.cache01.outputs.cache-hit == 'true'
      run: |
          cat docker-cache/*/file > layer-cache-sample.tar
          docker load < layer-cache-sample.tar

    - name: Build docker image with cache
      if: steps.cache01.outputs.cache-hit != 'true'
      run: |
        docker build --file Dockerfile --cache-from layer-cache-sample --tag layer-cache-sample .
        docker save layer-cache-sample $(docker history -q layer-cache-sample | awk '!/<missing>/{print}') > layer-cache-sample.tar
        rm -rf docker-cache && mkdir -p docker-cache/01 docker-cache/02
        split -n l/1/2 layer-cache-sample.tar > docker-cache/01/file
        split -n l/2/2 layer-cache-sample.tar > docker-cache/02/file

    - name: dokcer run
      run: docker run --rm layer-cache-sample:latest echo "Hello World!"

補足

docker layer のキャッシュについては以下の PR でサンプルについて議論されていて(記事執筆時点ではまだ議論中), 今回はその内容を参考にしつつ書いた.

github.com

ISUCON9 に出て Ruby で予選通過した

ISUCON9 の予選にチーム「ソレイユ」で参加して 28 位でぎりぎり予選通過してきた。チームメンバーは osYoYu (@osyoyu) | Twitter霧矢あおい (@KOBA789) | Twitter で、なんだかんだ ISUCON5 の頃から参加している。ちなみに予選通過は初めて。

カッコ良いところは他の二人が書いてくれるとして、 自分は事前準備で Provisioning 周りとインスタ映えする Grafana ダッシュボード作成とアプリケーション周りのツール予習、 本番はドメイン知識貯めつつアプリケーションの調整をやっていたのでその辺を書く。 参加言語は Ruby で、最終得点は 10,090 点だった。

事前準備

参加 5 回目にしてチームで初めて準備と素振りというものをやった。勝因の 95% はそのおかげだと思う。

itamae 業

今回は周辺ツールのインストールに時間をかけないため、予め必要になりそうなものは全て itamae の cookbook を書いておいた。 後述するダッシュボードのための {node,mysql}_exporter とか、nginx, alp といった雑多なプロファイラなど、過去の参加記憶を掘り起こして書いてた。 あとはメンバーが適当なパッケージ名を issue に書いてくれるのでそれらのバイナリをポンするものも書いた。

github.com

事前に ubuntu(bionic)を使うと書いてあったので、その環境で動作するように Systemd Unit File も勝手に置くようにしてた。 素振り時に mtail の設定で正規表現のミスが見つかった意外に不具合もなく、本番も itamae apply してもらって完動してた気がする。

可視化業

チームメンバー全員可視化に関心があるおかげで、お家 Prometheus とお家 Grafana を持っており、 特に抵抗もないことから数値監視系は全て Grafana に寄せる話になった。

node_exporter を適当に入れて、 Node Exporter Full という既存のダッシュボードを持ってきて可視化したときは感動したと同時に情報量に惑わされてた。

grafana.com

最終的に過去必要になった Cpu, Memory, Network(receive, transmit), DiskIO だけに絞って PromQL を書いたら映えるダッシュボードが生まれた。

f:id:everysick:20190911002458p:plain
isucon monitor - 通称 isumon

ちなみに MySQL QPS は mysqld_exporter を、Nginx RPS は mtail で Nginx のアクセスログから HTTP Status Code ごとにサマリをカウントする設定を書いた。

正直 Nginx RPS が一番便利だった。403 Forbidden や 5XX 系の数がぼーっとしているだけで見えるので、そこから逆算して例えば出品者が自分の商品を買っていたり、売り切れ商品を買っていたりしそうだという目星をつけることができた。

本番

時系列に自信がそんなに無いので箇条書きで覚えていること書く。

  • 悠長にレギュレーション読んでいたら Alibaba ECS 完売してた
    • インスタンス生えるまで やれることがない スタンプを送りつつレギュレーション2周くらいした
  • 10:50 くらいで Grafana に node_exporter の状況がやってきて競技開始
  • koba789 や osyoyu はそれぞれかっこいいことやってた
    • alp や pt-query-digest の結果をベンチ後自動で GitHub issue に post してくれるやつとか(koba789)
    • rsync で 3 台一気に設定を同期するスクリプトの設定や mysql8 に移行するとか(osyoyu)
  • その間自分は
    • アプリケーション触ったり
    • APPLICATION_SPEC.mdEXTERNAL_SERVICE_SPEC.md を読み込んだり
    • エンドポイントや Table の schema 一覧引っ張ってきたり
    • /initialize 後のレコード数引っ張ってきたり
    • N+1 にコメント書いたり
    • index.html を nginx から返すようにしたり
  • 改善の最初は alp 等の結果を見つつユーザーをどうやって購買まで導くかチームで話していた
    • ベンチ直後の APPLICATION_SPEC.md にある isucari ステータス遷移表 ごとにレコード数を見て、どこの数値遷移で詰まっていてどうすれば購買フローをスムーズに流せるかなど
    • というのも過去の ISUCON と比べて初期の負荷が低くてリソースが余るなどしていたから
  • とりあえず売れる商品はあるのものの /buy が叩かれないことをどうにかしようとなる
  • 買うために必ず通る新着一覧やカテゴリごとの新着一覧で 403 が多い
    • コード上で 403 を返すのは「自分の商品を買う」「買えない(もう売れている)商品を買おうとしている」が原因だとわかる*1
    • レギュレーション曰くそこまで新着一覧の制約がきつくないのでそれらを非表示にするなどをした
    • 100イスコイン の椅子非表示とか値段順でソートとかやったけど全部怒られた。それはそう。
  • index.html を sinatra が返していたのを Nginx で返すようにした
  • この辺で 3 台構成になったので事前準備していた puma の設定を突っ込んだり調整したりした
    • 突っ込んだら Nginx が 502 を喋り始めたので SOMAXCONN や Backlog の設定もしてもらう(osyoyu)
    • 最終的な構成は server1 が LB と app, server2 が DB, server3 が app (without 画像配信) という感じ
  • categories をインメモリにしてたりした(koba789)
  • 403 問題がある程度解決したあたりから N+1 潰しが始まる
    • データ一般に強い氏(koba789)が殺めていくのを横で typo 指摘してた
  • N+1 が潰れたあたりで index の検討が始まる(koba789)
  • /buy が微妙に重いので rack-lineprof の結果見ながらなるほどって言ってる
  • server3 の Disk が調子悪い問題が出現する
    • server4 のセットアップが始まる(osyoyu)
    • 発覚から20分くらいでしれっとサービスインさせる(osyoyu)
  • transactions あたりの N+1 をとってる(koba789)
  • puma や campaign の調整してたら puma がスタックする問題が発生し始める(なぜ?)
    • 各サーバーで puma を restart して回る
  • スコア 9,210
  • 再起動試験(17:30 くらい)
  • 何故かスコア 1/3 になる
    • server2 で golang 実装が動いてたのを止めたら治る(なぜ?)
  • この時点で 8,530 なので予選通過ライン達してない
  • puma がスタックする(なぜ?)
    • 各サーバーで puma を restart して回る
  • スコア 9,510
  • ログ系と exporter 系全部切る
  • メモリ余ってるので雑な index どんどん貼る(koba789)
  • ベンチマーカーの cancelled 祭りが始まる
    • exporter を停止したことで isumon 上で経過が見れなくなり不安になる
    • 部屋内でボレロを流して治安悪くする
  • スコア 10,090
  • 競技終了

所感

予選は通過することができた。最後のスコアはガチャのように思えるが、再起動試験後のスコアも予選通過ラインに達していたのでそこまで運が良かったわけではないと思う。ログを切って本当に良かった。

問題の構成や点数配分、レギュレーションの匂わせ方まで、過去一で面白い ISUCON だった。運営の皆さんありがとうございます。本選も面白いの期待しています。

心残り

今回は外部 API のリクエスト並列化をやりそこねたので、それがあればスコアはまだまだ上がったと思う。 一応外部 API リクエストは rack-lineprof を使ってローカルからアクセスしたログを見た。そのときは複数回送っても気にするほど遅くなかったので気に留めてなかったんだけど、解説・講評*2見たら

2つ目の課題のとなる配送サービスAPIですが、APIのレスポンスにかかるレイテンシがベンチマーク走行中のみ遅延を入れて 0.8秒かかるようにしてあります。アクセスログの分析をしたチームは気づいたかと思いますが、取引中の商品が多くなればレスポンスがかなり遅くなります。

と書いてあった。うーん。気付かんよ。 ということでこういうのへの対策として stackprof などの使用を検討し始めている。

golang で出ていたチームは bcrypt で苦戦していたけど Ruby は気になるほどではなかった。むしろ支配的になってくるのは MySQL のロックだと思う。非 golang 勢の参加記いっぱい読みたいので待ってます。

チーム方針として git を使わないでやってきた(サーバー上に開発環境整えている)のだけれど、事前準備をしたことによって当日手が空いてしまって(同時編集できないがために)作業が詰まる現象が起きた。後半はアプリケーションコードがボトルネックになったためにもどかしい感じが残る。本選はファイル分割とかしていこういうのを避けていこうとチームで話している。

感情が無いエントリになってしまった。予選通過はちゃんと嬉しいです。本選も準備して頑張るぞ〜!


これは isumon を部屋の壁に投影していたやつ。見てると精神が安定する。

LaTeX のコンパイル環境を docker に閉じ込めて使う

VM を含めて普段から4箇所ほど環境を使いまわしていると LaTeX の環境をそれぞれで整えるのがかなり億劫である. 昨年の卒業論文で書き始める前に構築した環境が割とポータブルだったのでそれをまとめて書く.

求めていた環境は以下のような形で,割とシンプルに満たせたと思う.

  • ローカルに LaTeX およびそれに関係するパッケージは一切入れない
  • docker とプロジェクトがあればどこでもビルドできる
  • ローカルに LaTeX 環境を整えた場合とコンパイル等の速度に差は無い
  • (編集はターミナルで完結させたい = GUI使いたくない)

github.com

README にも書いてある通り,プロジェクトごと一度引っ張ってきて解凍するだけで良い. docker を使っているので(docker 以外に)インストールが必要なものはないし,パッケージで必要なのがあっても Dockerfile に書いてしまえば問題無い.

Dockerfile のベースイメージは alpine をベースに作ってあるもの*1を使っているのでディスクもそんなに食べない.

Dockerfile

FROM paperist/alpine-texlive-ja

RUN apk add --update ghostscript git

RUN tlmgr update --self

# You can install package using tlmgr
# e.g.) RUN tlmgr install ulem

日本語入力系の設定が記載している .latexmkrc を参考に*2してコマンド類をまとめることで,コンパイル時のスクリプトも単純にしている.

build.sh

docker build -t latex-template .
docker run --rm -it -v $PWD:/workdir latex-template:latest latexmk main.tex

.latexmkrc

#!/usr/bin/env perl
$latex            = 'platex -synctex=1 -halt-on-error';
$latex_silent     = 'platex -synctex=1 -halt-on-error -interaction=batchmode';
$bibtex           = 'pbibtex';
$dvipdf           = 'dvipdfmx %O -o %D %S';
$makeindex        = 'mendex %O -o %D %S';
$max_repeat       = 5;
$pdf_mode         = 3;

ipsj のテンプレートくらいの内容なら特に問題無く扱える.コンテナ起動速度もコンパイル時間に比べれば一瞬なので気にならないはず. ブログの内容は古い可能性があるので,適宜リポジトリを参照してほしい.