sbt 1.x 系にアップグレードしたらCIでコンパイルキャッシュが効かなくなったので対処した

業務で開発しているプロジェクトで、sbtを0.13.17 から 1.1.1にアップグレードして喜んでいたらCIでキャッシュが効かなくなってしまい、ビルド時間が遅くなるという事象を経験したのでどのような調査をしたのかと、対処方法を書きます。ちなみにCIはCircleCI2.0を使っています。

コンパイルキャッシュが効かなくなる現象の確認

プロジェクトの作成

環境を用意します。Ubuntu16.04上にsbtをインストールしました。手順は以下のものを参考にしました。

sbt Reference Manual — Linux への sbt のインストール

hello と出力するだけのプロジェクトをつくります。 scala/scala-seed.g8 のテンプレートを使いました。

ubuntu@c01:~$ sbt new scala/scala-seed.g8
[info] Set current project to ubuntu (in build file:/home/ubuntu/)

A minimal Scala project.

name [Scala Seed Project]: hello-world

Template applied in ./hello-world

コンパイルと実行

動作確認をします。 sbt run を2度実行しています。

ubuntu@c01:~$ cd hello-world/
ubuntu@c01:~/hello-world$ sbt run
[info] Loading project definition from /home/ubuntu/hello-world/project
[info] Updating ProjectRef(uri("file:/home/ubuntu/hello-world/project/"), "hello-world-build")...
[info] Done updating.
[info] Compiling 1 Scala source to /home/ubuntu/hello-world/project/target/scala-2.12/sbt-1.0/classes ...
[info] Done compiling.
[info] Loading settings from build.sbt ...
[info] Set current project to hello-world (in build file:/home/ubuntu/hello-world/)
[info] Updating ...
[info] Done updating.
[info] Compiling 1 Scala source to /home/ubuntu/hello-world/target/scala-2.12/classes ...
[info] Done compiling.
[info] Packaging /home/ubuntu/hello-world/target/scala-2.12/hello-world_2.12-0.1.0-SNAPSHOT.jar ...
[info] Done packaging.
[info] Running example.Hello
hello
[success] Total time: 1 s, completed Mar 21, 2018 6:38:59 AM

ubuntu@c01:~/hello-world$ sbt run
[info] Loading project definition from /home/ubuntu/hello-world/project
[info] Loading settings from build.sbt ...
[info] Set current project to hello-world (in build file:/home/ubuntu/hello-world/)
[info] Packaging /home/ubuntu/hello-world/target/scala-2.12/hello-world_2.12-0.1.0-SNAPSHOT.jar ...
[info] Done packaging.
[info] Running example.Hello
hello
[success] Total time: 0 s, completed Mar 21, 2018 6:39:17 AM

1度目はコンパイルが走っていて、2度目はコンパイルキャッシュが効いているため、コンパイルを行わずに実行されていることがわかります。

CI上で行われているのと同様にコンパイルキャッシュの保存/展開操作を行う

CI上で行う操作は、

  1. ソースコードのチェックアウト
  2. (キャッシュがあれば)キャッシュの展開
  3. コンパイル・テスト
  4. キャッシュの作成・保存

です。

CI上の環境と同じ事を行ってみましょう。ソースコードはチェックアウトされたとみなして、初回のテストなのでキャッシュは存在しません。なので3のコンパイル・テストの工程からはじめます。

コンパイル・テスト

ubuntu@c01:~/hello-world$ sbt test
[info] Loading project definition from /home/ubuntu/hello-world/project
[info] Loading settings from build.sbt ...
[info] Set current project to hello-world (in build file:/home/ubuntu/hello-world/)
[info] HelloSpec:
[info] The Hello object
[info] - should say hello
[info] Run completed in 567 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 1 s, completed Mar 21, 2018 6:52:09 AM

先程実行した時のコンパイルキャッシュが残っているのでコンパイルは走っていませんね。テストもきちんとパスしました。

キャッシュの作成・保存

tarコマンドを使って、 target ディレクトリ以下を1つのアーカイブにまとめます。

その他にも実際のCIでは ~/.sbt~/.ivy2 といったディレクトリもキャッシュされるようにする必要があります。今回はこの2つのディレクトリは問題の事象に関係がなかったので target ディレクトリだけを対象にします。

ubuntu@c01:~/hello-world$ tar cvf cache.tar target/
target/
target/streams/
target/streams/$global/
target/streams/$global/dependencyPositions/
target/streams/$global/dependencyPositions/$global/
[... 以下略]
target/scala-2.12/hello-world_2.12-0.1.0-SNAPSHOT.jar

実際のCIであれば、生成された cache.tar ファイルを保存して、次回のビルド時にこのファイルを展開してあげればコンパイルキャッシュが効くはず…です。

キャッシュの展開

先程生成したキャッシュを展開してみます。tarでアーカイブしたものをそのまま展開して上書きしただけなので特に変わりはないはずです。

ubuntu@c01:~/hello-world$ ls
build.sbt  cache.tar  project  src  target

ubuntu@c01:~/hello-world$ tar xvf cache.tar
target/
target/streams/
target/streams/$global/
target/streams/$global/dependencyPositions/
target/streams/$global/dependencyPositions/$global/
[...以下略]
target/scala-2.12/hello-world_2.12-0.1.0-SNAPSHOT.jar

再びテスト実行

再びテストを実行させてみます。

ubuntu@c01:~/hello-world$ sbt test
[info] Loading project definition from /home/ubuntu/hello-world/project
[info] Loading settings from build.sbt ...
[info] Set current project to hello-world (in build file:/home/ubuntu/hello-world/)
[info] Updating ...
[info] Done updating.
[info] Compiling 1 Scala source to /home/ubuntu/hello-world/target/scala-2.12/classes ...
[info] Done compiling.
[info] Compiling 1 Scala source to /home/ubuntu/hello-world/target/scala-2.12/test-classes ...
[info] Done compiling.
[info] HelloSpec:
[info] The Hello object
[info] - should say hello
[info] Run completed in 640 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 7 s, completed Mar 21, 2018 7:04:58 AM

なんと、コンパイルが再び走ってしまっています。キャッシュが効いていないということですね。sbt0.13.17を使っていたときはこのやり方でうまくいっていたのですが…。

原因

どのディレクトリをtarでアーカイブしてからリストアするとこの事象が発生するのか、 target ディレクトリ以下を順番に掘り下げて行って調査したところ、 target/scala-2.12/resolution-cache/com.example/hello-world_2.12/0.1.0-SNAPSHOTディレクトリ以下のファイルが影響しているということがわかりました。

ubuntu@c01:~/hello-world/target/scala-2.12/resolution-cache/com.example/hello-world_2.12/0.1.0-SNAPSHOT$ ls -l
total 3
-rw-rw-r-- 1 ubuntu ubuntu  647 Mar 21 07:04 resolved.xml.properties
-rw-rw-r-- 1 ubuntu ubuntu 1943 Mar 21 07:04 resolved.xml.xml

このディレクトリにはファイルが2つあります。アーカイブなのでファイルの中身が変わるはずはありませんが、一応md5ハッシュの値を展開前と展開後で比べてみます。

# 展開前
ubuntu@c01:~/hello-world/target/scala-2.12/resolution-cache/com.example/hello-world_2.12/0.1.0-SNAPSHOT$ md5sum resolved.xml.*
d369e9bf8b52bddc7da2f93e58b6b86f  resolved.xml.properties
2c8964010ef6a2835dce1c3f2cc8853d  resolved.xml.xml

# 展開後
ubuntu@c01:~/hello-world/target/scala-2.12/resolution-cache/com.example/hello-world_2.12/0.1.0-SNAPSHOT$ md5sum resolved.xml.*
d369e9bf8b52bddc7da2f93e58b6b86f  resolved.xml.properties
2c8964010ef6a2835dce1c3f2cc8853d  resolved.xml.xml

当たり前ですが、ハッシュ値は一致しています。

もう一点疑う余地があるとすれば、タイムスタンプです。

# 展開前
ubuntu@c01:~/hello-world/target/scala-2.12/resolution-cache/com.example/hello-world_2.12/0.1.0-SNAPSHOT$ ls -l
total 3
-rw-rw-r-- 1 ubuntu ubuntu  647 Mar 21 07:04 resolved.xml.properties
-rw-rw-r-- 1 ubuntu ubuntu 1943 Mar 21 07:04 resolved.xml.xml

# 展開後
ubuntu@c01:~/hello-world/target/scala-2.12/resolution-cache/com.example/hello-world_2.12/0.1.0-SNAPSHOT$ ls -l
total 3
-rw-rw-r-- 1 ubuntu ubuntu  647 Mar 21 07:04 resolved.xml.properties
-rw-rw-r-- 1 ubuntu ubuntu 1943 Mar 21 07:04 resolved.xml.xml

特に変わりはないように思えますね…。ここでファイル作成時刻のタイムスタンプはもっと詳細な粒度で保持しているはず、という事を思い出しました。 ls コマンドの --full-time オプションで表示ができるようです。

# 展開前
ubuntu@c01:~/hello-world/target/scala-2.12/resolution-cache/com.example/hello-world_2.12/0.1.0-SNAPSHOT$ ls -l --full-time
total 3
-rw-rw-r-- 1 ubuntu ubuntu   647 2018-03-21 07:04:52.724370184 +0000 resolved.xml.properties
-rw-rw-r-- 1 ubuntu ubuntu  1943 2018-03-21 07:04:52.716369762 +0000 resolved.xml.xml

# 展開後
ubuntu@c01:~/hello-world/target/scala-2.12/resolution-cache/com.example/hello-world_2.12/0.1.0-SNAPSHOT$ ls -l --full-time
total 3
-rw-rw-r-- 1 ubuntu ubuntu  647 2018-03-21 07:04:52.000000000 +0000 resolved.xml.properties
-rw-rw-r-- 1 ubuntu ubuntu 1943 2018-03-21 07:04:52.000000000 +0000 resolved.xml.xml

おっと…!これですね。タイムスタンプが秒単位で丸められています。ここのタイムスタンプが完全に一致していないためにプロジェクトに変更が加えられたと認識されて、フルでコンパイルが走ってしまっているのではないかという当たりがつけられました。どうやらtarは標準ではファイルのタイムスタンプは秒未満は切り捨てるようです。zipは標準だと2秒単位でタイムスタンプを丸めるようです。

コンパイルキャッシュが効くようにするために対処した

社内でtarでアーカイブを作成するときにナノ秒単位でタイムスタンプを保持する方法がないか聞いたところ、以下のリンクを教えてもらいました。

unix.stackexchange.com

GNU tarを使っているときにposixフォーマットでアーカイブを作成するとよいということがわかりました。 --format posix をつけるとよいということですね。

再び実験

キャッシュを作ってからリストアする

ubuntu@c01:~/hello-world$ tar --format posix -cvf cache.tar target/
target/
target/streams/
target/streams/$global/
target/streams/$global/dependencyPositions/
target/streams/$global/dependencyPositions/$global/
[... 以下略]
target/scala-2.12/hello-world_2.12-0.1.0-SNAPSHOT.jar

ubuntu@c01:~/hello-world$ tar xvf cache.tar
target/
target/streams/
target/streams/$global/
target/streams/$global/dependencyPositions/
target/streams/$global/dependencyPositions/$global/
[...以下略]
target/scala-2.12/hello-world_2.12-0.1.0-SNAPSHOT.jar

テストを実行する

再びテストを実行してみます。

ubuntu@c01:~/hello-world$ sbt test
[info] Loading project definition from /home/ubuntu/hello-world/project
[info] Loading settings from build.sbt ...
[info] Set current project to hello-world (in build file:/home/ubuntu/hello-world/)
[info] Updating ...
[info] Done updating.
[info] HelloSpec:
[info] The Hello object
[info] - should say hello
[info] Run completed in 710 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 2 s, completed Mar 21, 2018 7:25:45 AM

今度はちゃんとコンパイルキャッシュが効きました!これで安心してCIをたくさん回せます。

ちなみにCircleCI2.0の persist_to_workspace 機能を使っている時も同様な事象が起きますので、自前で上記の手法でtarアーカイブを作成してリストアするとよいでしょう。

まとめ

  • sbt 1.xではコンパイルキャッシュが使えるかどうかの判定にファイルのナノ秒単位のタイムスタンプを使っているように見える
  • sbt 1.xではコンパイルキャッシュ関連のファイルが作成されたタイムスタンプと、それがリストアされたときのタイムスタンプが完全に一致していないとコンパイルキャッシュが効かない
  • tar --format posix オプションでナノ秒単位のファイルのタイムスタンプが保存できる

Javaの超低レイテンシなGCアルゴリズム、ZGCをコンパイルして動作を試す

The Z Garbage Collector

以下の資料を見てZGCのことを知りました。

The Z Garbage Collector

ZGCは、 "A Scalable Low Latency Garbage Collector" というものだそうで、まだ開発中でリリースはされていないです。

f:id:kuro_m88:20180218013120p:plain

数TBまでのヒープメモリのサイズを想定していて、なおかつGCの最大停止時間が10msというのがゴール。既にヒープサイズが128GBのベンチマークにおいて、パラレルGCやG1GCより圧倒的に停止時間が短い。ベンチマーク上では最大停止時間も10msを切っています。 アルゴリズムについては別途まとめてみようかと思っています。Colored Pointerという概念が特徴です。

f:id:kuro_m88:20180218013454p:plain

ZGCを試す

開発版のZGCは既に試すことができるようです。ですが、自分でビルドする必要があります。紹介したスライドで書かれている方法ではうまくいかなかったのですが、なんとか動かすところまでできたので紹介します。

環境はUbuntu16.04.3で、24CPU、48GBメモリのサーバで作業しました。メモリは8GBもあればビルドできるのではないかと思います。

必要なパッケージをインストール

ubuntu@java-z:~$ sudo apt install build-essential mercurial zip unzip libx11-dev libxext-dev libxrender-dev libxtst-dev libxt-dev libcups2-dev libfontconfig1-dev libasound2-dev

ZGCプロジェクトのリポジトリをダウンロードしてくる

3GB以上あるので、そこそこ待たされます。

ubuntu@java-z:~$  hg clone http://hg.openjdk.java.net/zgc/zgc

configureする

ubuntu@java-z:~$ cd zgc
ubuntu@java-z:~/zgc$ sh ./configure

[...略]

checking for javac... no
checking for java... no
configure: Could not find a valid Boot JDK. You might be able to fix this by running 'sudo apt-get install openjdk-8-jdk'.
configure: This might be fixed by explicitly setting --with-boot-jdk
configure: error: Cannot continue

openjdk-8-jdk をインストールしろというメッセージが出ますが、実際に必要なのは JDK9 です。ここでOpenJDK 9をインストールしたくなりますが、後述する理由ビルドがコケます。

=== Output from failing command(s) repeated here ===
* For target buildtools_interim_langtools_modules_java.compiler.interim__the.BUILD_java.compiler.interim_batch:
javac: invalid flag: --module-path
Usage: javac <options> <source files>
use -help for a list of possible options
javac: no source files
Usage: javac <options> <source files>
use -help for a list of possible options

javacに --module-path というオプションがないというエラーです。実はこれ、OpenJDKとOracle JDKでコマンドオプションの引数の名前が違うせいです。このまま通るようにするにはOracleのJDK9をインストールする必要があります。

ubuntu@java-z:~/zgc$ sh ./configure

[...略]

Configuration summary:
* Debug level:    release
* HS debug level: product
* JDK variant:    normal
* JVM variants:   server
* OpenJDK target: OS: linux, CPU architecture: x86, address length: 64
* Version string: 10-internal+0-adhoc.ubuntu.zgc (10-internal)

Tools summary:
* Boot JDK:       openjdk version "9-internal" OpenJDK Runtime Environment (build 9-internal+0-2016-04-14-195246.buildd.src) OpenJDK 64-Bit Server VM (build 9-internal+0-2016-04-14-195246.buildd.src, mixed mode)  (at /usr/lib/jvm/java-9-openjdk-amd64)
* Toolchain:      gcc (GNU Compiler Collection)
* C Compiler:     Version 5.4.0 (at /usr/bin/gcc)
* C++ Compiler:   Version 5.4.0 (at /usr/bin/g++)

Build performance summary:
* Cores to use:   24
* Memory limit:   48287 MB

うまくいくと、上記のような出力が出るはずです。

makeする

ubuntu@java-z:~/zgc$ make images

ビルドが終わるまで待ちます。自分の環境だと4分半くらいで終わりました。

f:id:kuro_m88:20180225224416p:plain

CPUが全コアほぼ100%になった。

javaのバージョンの確認

ubuntu@java-z:~$ cd zgc/build/linux-x86_64-normal-server-release/images/jdk/bin
ubuntu@java-z:~/zgc/build/linux-x86_64-normal-server-release/images/jdk/bin$ ./java -version
openjdk version "10-internal" 2018-03-20
OpenJDK Runtime Environment (build 10-internal+0-adhoc.ubuntu.zgc)
OpenJDK 64-Bit Server VM (build 10-internal+0-adhoc.ubuntu.zgc, mixed mode)

ZGC開発用のJava10がビルドされたのがわかります。

ZGCを使ってみる

100KB程度のオブジェクトを大量に生成するだけのプログラムを作りました。

import java.util.Arrays;

class Data {
    byte[] data;

    Data() {
        byte[] array = new byte[100000];
        Arrays.fill(array, (byte)1);
        this.data = array;
    }
}

public class Hello {
    public static void main(String[] args){
        System.out.println("########## START ##########");
        for(int i =0; i < 1000; i++) {
            new Data();
        }
        System.out.println("########## END ##########");
    }
}

コンパイル

ubuntu@java-z:~$ zgc/build/linux-x86_64-normal-server-release/jdk/bin/javac Hello.java

実行

-XX:+UseZGC をつけるとZGCが有効になります。-Xlog:gc* で詳細なGCのログが出ます。 ヒープサイズを最大100MBまでに制限して実行してみましょう。

ubuntu@java-z:~$ zgc/build/linux-x86_64-normal-server-release/jdk/bin/java -Xmx100M -XX:+UseZGC -Xlog:gc* Hello
[0.006s][info][gc,init] Initializing The Z Garbage Collector
[0.006s][info][gc,init] Version: 10-internal+0-adhoc.ubuntu.zgc (release)
[0.006s][info][gc,init] NUMA Support: Enabled
[0.006s][info][gc,init] NUMA Nodes: 2
[0.006s][info][gc,init] CPUs: 24 total, 24 available
[0.006s][info][gc,init] Memory: 48287M
[0.006s][info][gc,init] Large Page Support: Disabled
[0.006s][info][gc,init] Workers: 15 parallel, 3 concurrent
[0.010s][info][gc,init] Pre-touching: Disabled
[0.010s][info][gc,init] Pre-mapping: 100M
[0.035s][info][gc     ] Using The Z Garbage Collector
[0.187s][info][gc,start] GC(0) Garbage Collection (Warmup)

[略...]

[0.483s][info][gc,ref    ] GC(7) Clearing All Soft References
[0.483s][info][gc,start  ] GC(7) Garbage Collection (Allocation Stall)
[0.483s][info][gc,phases ] GC(7) Pause Mark Start 0.141ms
[0.485s][info][gc,phases ] GC(7) Concurrent Mark 2.015ms
[0.486s][info][gc,phases ] GC(7) Pause Mark End 0.667ms
[0.486s][info][gc,phases ] GC(7) Concurrent Process Non-Strong References 0.133ms
[0.486s][info][gc,phases ] GC(7) Concurrent Reset Relocation Set 0.054ms
[0.486s][info][gc,phases ] GC(7) Concurrent Destroy Detached Pages 0.001ms
[0.486s][info][gc        ] Allocation Stall (main) 3.605ms
[0.490s][info][gc,phases ] GC(7) Concurrent Select Relocation Set 3.574ms
[0.490s][info][gc,phases ] GC(7) Concurrent Prepare Relocation Set 0.289ms
[0.491s][info][gc,phases ] GC(7) Pause Relocate Start 0.519ms
[0.491s][info][gc        ] Allocation Stall (main) 1.226ms
[0.493s][info][gc,phases ] GC(7) Concurrent Relocate 1.953ms
[0.493s][info][gc,load   ] GC(7) Load: 0.61/0.70/0.51
[0.493s][info][gc,mmu    ] GC(7) MMU: 2ms/16.2%, 5ms/52.6%, 10ms/75.0%, 20ms/84.0%, 50ms/88.6%, 100ms/93.2%
[0.493s][info][gc,marking] GC(7) Mark: 8 stripe(s), 1 proactive flush(es), 1 terminate flush(es), 0 completion(s), 0 continuation(s)
[0.493s][info][gc,reloc  ] GC(7) Relocation: Successful, 1M relocated
[0.493s][info][gc,nmethod] GC(7) NMethods: 128 registered, 0 unregistered
[0.493s][info][gc,ref    ] GC(7) Soft: 153 encountered, 19 discovered, 19 dropped, 0 enqueued
[0.493s][info][gc,ref    ] GC(7) Weak: 147 encountered, 78 discovered, 78 dropped, 0 enqueued
[0.493s][info][gc,ref    ] GC(7) Final: 0 encountered, 0 discovered, 0 dropped, 0 enqueued
[0.493s][info][gc,ref    ] GC(7) Phantom: 3 encountered, 3 discovered, 3 dropped, 0 enqueued
[0.493s][info][gc,heap   ] GC(7)                Mark Start          Mark End        Relocate Start      Relocate End           High               Low
[0.493s][info][gc,heap   ] GC(7)  Capacity:      100M (100%)        100M (100%)        100M (100%)        100M (100%)        100M (100%)        100M (100%)
[0.493s][info][gc,heap   ] GC(7)   Reserve:       62M (62%)          62M (62%)          62M (62%)          62M (62%)          44M (44%)          62M (62%)
[0.493s][info][gc,heap   ] GC(7)      Free:        0M (0%)            0M (0%)            0M (0%)            0M (0%)           10M (10%)           0M (0%)
[0.493s][info][gc,heap   ] GC(7)      Used:       38M (38%)          38M (38%)          38M (38%)          38M (38%)          56M (56%)          28M (28%)
[0.493s][info][gc,heap   ] GC(7)      Live:         -                 1M (2%)            1M (2%)            1M (2%)             -                  -
[0.493s][info][gc,heap   ] GC(7) Allocated:         -                 0M (0%)           12M (12%)          38M (38%)            -                  -
[0.493s][info][gc,heap   ] GC(7)   Garbage:         -                36M (36%)          24M (24%)          16M (16%)            -                  -
[0.493s][info][gc,heap   ] GC(7) Reclaimed:         -                  -                12M (12%)          20M (20%)            -                  -
[0.493s][info][gc        ] GC(7) Garbage Collection (Allocation Stall) 38M(38%)->38M(38%)

[略...]

[0.528s][info][gc,heap,exit] Heap
[0.528s][info][gc,heap,exit]  ZHeap           used 30M, capacity 100M, max capacity 100M
[0.528s][info][gc,heap,exit]  Metaspace       used 6395K, capacity 6442K, committed 6656K, reserved 8192K

こんな感じでGCのログが出ました。これでZGCをベンチマーク取ったりして試して遊べますね!

サーバの大量構築をした事がある学生を80人養成した

この記事はwhywaitaアドベントカレンダー21日目の記事です。 タイトルは言い過ぎかもしれません。
2年ぶりの参加です。前回は雑すぎてごめんよ (参考)

ICTトラブルシューティングコンテストという参加者も運営も学生が行うインフラ系のコンテストがあります。学生は本戦の問題作成および競技ネットワーク等の設計で忙しいので予選問題はお手伝いをしてる社会人(僕含め)で作りました。9問出題されましたが、この記事はそのうちの1問の紹介です。

ここでサーバを100台構築させる問題を出題させ、何かしらの構築を効率化できる手法を用いたチームが25チーム中16チームいて、1チーム5名なので80人の学生がサーバの大量構築をしたことがあるということになります。(難易度は別として)

問題

問題文は長いのでこちらに貼っておきます。伏せる内容もないので全文公開です。

【公開用】ICTSC9予選 問1 · GitHub

内容を簡単に言うと、

  • sshして、nginxをインストールしてもらうまでのチュートリアル
  • 同じサーバでnginxに固定のJSONを返すための設定を投入してもらう (問 1-1)
  • 同様な構成で100台構築してもらう (問 1-2)

となっていてチュートリアル的にnginxをインストールしつつ、ちょこっと設定等を変えてもらい、それを100台のサーバに対して展開してもらうという問題構成です。 問1として出題したので、Linuxのサーバにsshしてパッケージをインストールするという操作にあまり慣れていない学生でも点数を取れる難易度を目指しました。結果、0点のチームは居ませんでした!

学生のうちに大量のサーバにsshして構築する経験をした学生は居ないだろうという想像のもと、100台構築してもらうことにしました。100台というのはきりがよかったのと、手動でコマンドを打つのは諦めたくなるくらいにしたかったという意図があります。 何かしらの方法で構築を効率化/自動化できたチームは高得点で、手動でやっていたチームは10台くらい構築して時間の無駄だと思ったのか、諦めた形跡がありました。

出題環境

予備も含めるとサーバを3,200ホスト用意しなければなりません。予選環境としてさくらインターネット様にさくらのクラウドを提供して頂いたのですが、流石にこれだけのために3,200ホストを実際に建てるわけにはいきません。ただ大量のホストは欲しい…そこでLXDです!!Dockerじゃないです。LXDですよ。 LXDはLinux Containerを使ってコンテナの中でinitプロセスが立ち上がり、普通のサーバマシンのように振る舞います。Dockerがアプリケーションコンテナであれば、LXDはシステムコンテナです。

Linux Containers - LXD - イントロダクション

LXDで大量のホスト(コンテナ)を運用する上でいろいろつまづきましたがそれは別記事で紹介します。当初はCPU36コア, メモリ224GBの仮想マシンの上で1,500ホスト(コンテナ)稼働させるのにチャレンジするつもりで構築していましたが、本番を見据えた構成でホストを生成していったところ、1,300コンテナあたりでcgroupが壊れてしまいました。各ホストには1CPU, メモリ256MBの環境となるように制限を掛けていたのですが、途中から新しいcgroupが作れなくなりました。ロードアベレージもアイドル状態で600くらいだったのでプロセス数が異常に増えている環境だとかなり厳しい状態になるということがわかりました。

ubuntu@s01:~$ sudo mkdir /sys/fs/cgroup/memory/test
mkdir: cannot create directory '/sys/fs/cgroup/memory/test': Cannot allocate memory

試しに手動でcgroupを作ってみてもこんな感じで、さらにsyslogを見るとよくわからないエラーが…

Dec 14 05:57:38 s01 kernel: [14972.817859] BUG: unable to handle kernel NULL pointer dereference at           (null)
Dec 14 05:57:38 s01 kernel: [14972.819204] IP: [<          (null)>]           (null)
Dec 14 05:57:38 s01 kernel: [14972.820514] PGD 0
Dec 14 05:57:38 s01 kernel: [14972.820584] Oops: 0010 [#2] SMP
[略]
Dec 14 05:57:38 s01 kernel: [14972.836239] Fixing recursive fault but reboot is needed!
Dec 14 08:18:02 s01 kernel: [23396.544849] htb: too many events!

徹夜に近い状態で準備をしていたのであきらめてCPU16コア、メモリ196GBの仮想マシンを4台つかって、1台あたり800ホスト(コンテナ)、4台で3200ホスト(コンテナ)ということになります。

4台分の監視は普段業務で使っていて慣れているのでDatadogを使いました。 f:id:kuro_m88:20171220133022p:plain

CPUとメモリとロードアベレージとiowaitとネットワークトラフィックくらいしか監視していませんでしたが。メモリもだいぶ余裕があったのでZFSの重複排除機能をONにしていました。結果は…

ubuntu@s02:~$ sudo zpool list
NAME       SIZE  ALLOC   FREE  EXPANDSZ   FRAG    CAP  DEDUP  HEALTH  ALTROOT
lxd-pool   248G  3.11G   245G         -     5%     1%  63.49x  ONLINE  -

1ホストあたり 256MB x 800ホスト ≒ 200GBのディスク容量消費で見積もっていたのですが、結果的には重複排除が63.49倍効いた結果3.11GBしか消費しませんでした。同じ環境で全員が同じようなことをするので当たり前といえば当たり前ですが、重複排除すごいですね。

CPUとメモリは検証結果からそこまで心配していなくて、ディスクとネットワークを心配していたのですが、実際ネットワークは本番競技中に一瞬ひやっとしました。とあるチームが100台同時に構築するスクリプトを回したようで、一瞬440Mbpsほど帯域を使いました。1Gbpsで張り付いたらどうしようと思っていたのですが、これ以上帯域を使うことはなく、一瞬で落ち着きました。 最終的にトラブルもなく環境を提供できたのは良い経験になりました。

出題してみてどうだったか

無事100台構築できたチームは、1チームを除き、踏み台サーバからシェルスクリプトを使ってsshをして遠隔でコマンドを投入する方式でした。プロビジョニングツールを使うほどの作業内容でもなさそう & コンテストなのでとりあえず動けばいいということでみんなシェルスクリプトかなと想像していました。

一番シンプルだった解答はこんな感じです。

export SSHPASS=password
for (( i=2; $i <= 100; i++ )); do
  target=`printf "teamxx-c%03d" $i`
  sshpass -e ssh -oStrictHostKeyChecking=no $target 'sudo apt install nginx -y && echo "{\"hostname\": \"`hostname`\"}" | sudo tee /var/www/html/hostname.json'
done

ちなみに芸術点があるのではないかという参加者の声を聞きましたが、芸術点とかはなく、機械的に全台に対してリクエストを投げて点数をモニタリングしていました。構築が終わる前に構築が終わったという報告をしてもすぐに分かる状況になっていたということです。(実際にそういうチームがあったかは触れません)常に監視して採点をしていたので、構築完了の報告を受けたらそのホストに入って手順の報告と全く違う構築方法をしていないか調べていました。

1チームだけansibleを使ってplaybookを書いてきちんとプロビジョニングをしているチームがありました。時間がない中ansibleを使ったということは普段から使い慣れていたのでしょう。素晴らしい。

感想

徹夜して急いで作成した問題にしてはいい感じの問題が出題できたのではないかと思っています。 予選なのでトップ層の点差がつかない事は気にしておらず、予選落ちするチームが勉強になる/楽しんで貰える問題を出したいなと思っていました。本戦は順位を決めるための勝負です。優秀なトラコン運営学生が問題と競技ネットワークの設計を練っているので期待です。 競技用ネットワークを設計している学生はこんなことを言っています。

f:id:kuro_m88:20171220133734p:plain

前回はCLOSトポロジやVXLAN、BGP MPLSにチャレンジしていましたね。

www.slideshare.net

今回はどんなネットワークなのでしょうか。クラウドもOpenStackを卒業して自作しているみたいですね。社会人から見ても恐ろしいです。

ということで、予選を突破したチームの皆さんは本戦で日頃の技術研鑽の結果を見せつけて下さい! 予選に落ちてしまったチームはまたの挑戦をお待ちしております!

Slackで寿司を回転させる技術

こんなツイートをしたらいいねが1000件以上ついたのでやり方を紹介します。

きっかけ

以前Slackで絵文字を回転させる方法を紹介しましたが、もっとちゃんと寿司を回転させたくなりました。

kurochan-note.hatenablog.jp

画像生成

今回もImage Magickを使います。Mac上で生成しました。

絵文字を3行3列の9個順番に並べるといい感じに回っているように見えるのですが、中心は冒頭で紹介した記事で絵文字を回転させてください。今回は中心以外の8種類の画像を生成します。

出来上がったものはこちらです。

f:id:kuro_m88:20170918183136g:plainf:id:kuro_m88:20170918183152g:plainf:id:kuro_m88:20170918183156g:plainf:id:kuro_m88:20170918183202g:plainf:id:kuro_m88:20170918183210g:plainf:id:kuro_m88:20170918183214g:plainf:id:kuro_m88:20170918183218g:plainf:id:kuro_m88:20170918183221g:plain

スクリプトはこんな感じです。最後のgifアニメ生成時の -colors 80 -fuzz 11% というオプションはSlackの絵文字のファイルサイズの64KB制限に引っかからないように画質を手でパラメータ調整した結果です。もしオーバーするようであれば値をいじってみてください。

gist.github.com

もちろん寿司以外にも使えます。いろんな絵文字を回していきましょう。寿司を静止させられるように明日から頑張ります。