その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

docker composeでワンショットタスクを実行する

TL;DR

docker compose up --abort-on-container-exit

[追記]

docker compose run --rm ${service_name} で良かった......
[追記ここまで]




例えばなんらかのテストを実行する時、テスト用にDB等のストレージコンポーネントを用意してそいつに対し読み書きすることでend to endのテストを模擬しているというようなことがあると思います。
かつてはテストケースごとにデータベースプロセスを上げそれに対してread/writeを実行、ということもよくしていましたが *1、最近ではテスト起動時にストレージコンテナを立ち上げてそれを読み書きしている事も多くなってきました。


さて、そのような時に「テスト起動するにあたってはコンポーネントAとコンポーネントBを事前にマシン上で立ち上げておいてください」みたいなのはいかにも面倒くさいので、こういう時に便利なツールとしてパッと思い浮ぶのはdocker composeでしょう。
docker compose自体については本記事では説明しませんが、本記事ではそういった「ストレージのような永続的なライフサイクルを持つコンテナ」と「テストランナーのような一時的なライフサイクルのコンテナ」を同時に起動させ、テストランナーの実行が終了したらすべて店仕舞いさせるというワンショットタスクをdocker composeで実行する方法について記します。

version: '3.9'

services:
  redis:
    image: redis:7.0.4
    container_name: redis
    ports:
      - "6379:6379"
    network_mode: host
  app-test:
    image: app-test-env:latest
    container_name: app-test
    command: npm test
    volumes:
      - ./:/app
    working_dir: /app
    network_mode: host
    depends_on:
      - redis

上記の docker-compose.yml では redis というRedisコンテナと、app-testというテストランナーコンテナを同時に実行するという定義をしています。


まずapp-test はテストの実行にあたってRedisを利用するので depends_onredis を指定することでredisコンテナの起動を先に行うようにします。
なお、これはあくまでredisコンテナの起動を先に行うというだけで「Redisが6379番ポートで実際にlistenしてくれるまで待つ」ということをしてくれるわけではありません。なのでドキュメントにも「実際に使えるようになるまで待つ必要がある場合は適切な方法でやるように」と書かれていますので注意しましょう *2: https://docs.docker.com/compose/startup-order/

ネットワークの設定は適宜書き換えてください。この例では簡単のためにredisコンテナの6379ポートをホストの6379ポートに公開し、redisとapp-testの両方のnetwork_modeをhostモードにすることでホストネットワークを介して6379ポートを通じて通信できるようにしてあります。


と、ひとまずこのようにしておくと、docker compose up を実行することでテストの実行は可能となります。
しかし、テストランナーの実行が終了してもredisコンテナの実行は継続するため明示的にSIGINT等を送ってあげないとこのdocker compose upは終了しなくなります。healthcheckなどを適切に設定するとこのへん上手くやれるはずですが、ワンショットのタスクにそこまでやるのも凝りすぎでは?


ということで、docker compose up --abort-on-container-exit というふうに --abort-on-container-exit を付与して実行すると、いずれかのコンテナがexitした時にそのexit codeを引き継いでdocker composeを終了してくれるようになります。

--abort-on-container-exit   Stops all containers if any container was stopped. Incompatible with -d

[追記]
冒頭にも書いたように、こうしておいて docker compose run --rm app-test と実行すると良いです。
[追記ここまで]

良かった良かった、これで簡単にワンショットのタスクをdocker composeで実行できましたね。
ちなみに、exitしたら全体をabortさせたいコンテナを PID=1 にすればexitした際にすべてを終わらせてくれるのではないか、と思って app-test に対し init: true を指定してみましたがこれは効きませんでした。

小ネタ

docker compose runの時はそのserviceのログしか出てこないので問題無いので大丈夫なんですが (つまり何も問題が無い)、docker compose upの場合はすべてのサービスのログが流れてきます。その際、今回のようなケースだとRedisのログが見れてもあまりうれしいことは無いので抑制したい。というわけで

...
  redis:
    image: redis:7.0.4
    logging:
      driver: none
...

このようにlogging driverを none に設定するのですがこれは期待通りに動きません。

詳しくはこのissueに書かれているのですが、docker compose upのログは実行中のコンテナに実際にattachしたものが表示されるのであって、logging driverの設定とは別とのことです。
つまり、ここでredisコンテナに対してnoneを設定すると、docker compose upのログには表示される一方、logging driverを使っているロガー、例えば docker logs ${container_id} 等には表示されなくなるという挙動をするようです。

なので、起動オプション --attach でログを見たいコンテナを指定すると、docker compose upのログにはそれだけが表示されるようになります。今回の例だと --attach app-test などとすると良いでしょう。

特定のユーザーのイベントによるGitHub ActionsのActionを保留状態にしておき、後で手動実行できるようにする

dependabotだとかrenovateだとかを使ってライブラリのバージョンアップのpull requestを自動的に送ってもらう、というような機構を利用されている方が多いと思います。
常にこれらのpull requestに目を光らせておいて常に取り込み続けるというのが理想的な形・そうあるべきだとは思うのですが、ふと気を抜くとバージョンアップのpull requestが溜まっていき、pull request自身も改訂に改訂を重ねている......みたいなことが起きがちではないでしょうか。

そういった折、誰も結果を見もしないCI (i.e. GitHub Actions) だけが回り続けているのを見て「このチェックは『ライブラリアップグレード業』をやる時に手動で回せばコンピューティングリソースの削減になるのでは?」と思い、それを試したという次第です。

この記事では例として、renovateからのpull requestに対して自動でActionを回す (つまり一般的な "CI" ) のではなくレビュアーの承認が降りた際に回すようにしてみます。

Environmentを作る

RepositoryにGitHubのEnvironmentを作ります。例えば、"renovate" という名前のEnvironmentを作ってみることとします。

"Required reviewers" にチェックを入れて、renovateからのpull requestに対するActionの実行を承認するユーザーあるいはチームを指定します。
この用途の場合は "Deployment branches" は "All Branches" で適当かと思いますが、都度環境に合わせると良いでしょう。

Action定義を書く

ここは普通にGitHub Actionsの定義を書きましょう。
重要な点は、普段 (つまりrenovate以外が) 実行するActionとrenovateが実行するActionをそれぞれ用意するということです。

普段実行するActionの定義については

...
jobs:
  your-action:
    if: ${{ github.actor != 'renovate[bot]' }}
...

と書いておき、renovateが実行するActionの定義には

jobs:
  your-action:
    if: ${{ github.actor == 'renovate[bot]' }}
    environment: renovate

と書いておくことで、renovate以外の時は普通に自動実行され、renovateの時は environment: renovate が有効になり承認が降りないとActionが実行されないこととなります。

このyaml中のifにどういった条件が書けるかについては以下のドキュメントを参照すると良いでしょう。

今回はactor (イベントの主体) の名前によって判断するという素朴な方法を採りましたが、その他のcontextを使うと色々ハイテクなことができそうです。例えばrenovateに限らず「botの時」はこういうふうにするとか。

あと、手元のyamlではrenovateが実行するActionのイベントに on push で指定しているのですが、これは on pull_request で十分なのかもしれない。

こんな感じになる

この場合 "e2e-test.yml" というのが普段走るActionのyaml、 "e2e-test-for-renovate.yml" がrenovateの時に走らせるActionのyamlとなっています。
これはrenovateによって作られたpull requestなのですが、「普段走るAction」がスキップされており「renovateの時に走らせるAction」がpending状態になっていることがわかると思います。

この時、 "This branch is waiting to be deployed" からメニューをポチポチ選んでゆくと

このような状況になるので、Review Deploymentsから承認することでActionがマニュアル実行されるという寸法です。あとは実行結果を見て、マージするかどうかを精査しましょう!

課題

task定義を複数用意するのダサくない・メンテ面倒じゃない?

それは本当にそう! 何か良い方法があると良いのですが......

[追記]

あと、composite actionを使えば良いのではないか、というアドバイスもありました: Creating a composite action - GitHub Docs

知見ありがとうございます。

[追記ここまで]


承認できるReviewersを6人までしか指定できないのは少ないし、何より権威主義的だ!!!

個人だけでなくチームを指定できるのでそこである程度カバーできるのではないでしょうか。

依存アップデートのpull requestのActionが自動で完了してないと、更にマージするモチベーション下がらない?

そうかも......なのでモチベーション高く常に依存アップデートのpull requestを見てるよ・マージしてるよ、という環境では不要だと思います。

一方、ある程度まとめて依存のアップデートをしているような環境だと都度都度Actionが回っている必要が無い、ということでリソースを節約できるのではないか、というのがこの仕組みの狙いです。

とはいえ、この方法を運用しはじめてから日が浅いので今後何か良い点・悪い点が見えてくる可能性があります。どうなることやら。

zigコードから生成したdynamic libraryおよびstatic libraryをCでコンパイルして使う

TL;DR

zig ccを使うと色々と簡単、特にstatic linkをする場合はハマりにくく楽なので、使える場合は使うと良さそうです。
zig cc自体、gccやclangのdrop-in replacementを目的として出来たCコンパイラなので多くの場合はそのままポンと乗せ替えができそうですが、環境によっては色々な事情もあるでしょうし本記事ではgccを使う方法についても記します。

前提

zig init-libで吐き出されるコード (src/main.zig) を使います。

const std = @import("std");
const testing = std.testing;

export fn add(a: i32, b: i32) i32 {
    return a + b;
}

test "basic add functionality" {
    try testing.expect(add(3, 7) == 10);
}

なお、export modifierが付いていないとライブラリを経由してCから参照することができません。

One of the primary use cases for Zig is exporting a library with the C ABI for other programming languages to call into. The export keyword in front of functions, variables, and types causes them to be part of the library API:
https://ziglang.org/documentation/0.9.1/#Exporting-a-C-Library

Cのコード (main.c) は以下の通り。zigで実装しているadd関数をCコード中で利用します。

#include <stdio.h>
#include <stdint.h>

int32_t add(int32_t a, int32_t b);

int main() {
    printf("Added: %d\n", add(123, 321));
    return 0;
}

zigとCの型の対応については以下のドキュメントを参照すると良いでしょう。
Documentation - The Zig Programming Language

なお試した環境の各種バージョンは以下の通りです。

$ uname -srvmo
Linux 5.15.0-47-generic #51-Ubuntu SMP Thu Aug 11 07:51:15 UTC 2022 x86_64 GNU/Linux
$ zig version
0.9.1
$ gcc --version
gcc (Ubuntu 11.2.0-19ubuntu1) 11.2.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

これは至って普通です。

zigのコードをsoファイルのdynamic libraryにするには、以下のようにzig build-libを実行すると良いです。-targetに何を指定できるのかについてはzig targetsコマンドの結果を参照すると良いでしょう。色々とクロスビルドできて便利ですね。

$ zig build-lib -dynamic -target x86_64-linux-gnu src/main.zig

すると、libmain.soというファイルが出力されるので

$ gcc -L . main.c -lmain

このようにgccdynamic linkすると、

$ LD_LIBRARY_PATH=. ./a.out
Added: 444

このように実行することができます。

なおzig ccを使う場合は"gcc"の部分を"zig cc"に書き替えるだけで動きます:

$ zig cc -L . main.c -lmain

ちなみにzig ccを使ってコンパイルしたバイナリを実行する場合はLD_LIBRARY_PATHを指定する必要が無くなるのですが、gccでも同じことをしたい場合は-Wl,-rpath,.みたいな感じでオプションを指定してRPATHにライブラリの検索パスを追加してあげると良いでしょう *1

static linkする場合はいろいろとzig側にオプションが必要になってきます。

$ zig build-lib -static -fPIC -fcompiler-rt -target x86_64-linux-musl src/main.zig

このようにするとlibmain.aというstatic libraryが生成されます。

今回はstatic linkをするので、glibcの代わりにmuslを使います *2
-fcompiler-rtcompiler-rtのシンボル情報を出力に含めるためのオプションです。これが無いと、static linkをする際にzigランタイムの持つシンボルを解決できず、Cコンパイラコンパイルできなくなります。
そしてmuslを使う場合は-fPICオプションにより位置独立コードとしてライブラリを出力する必要があります *3

そしてgcc側では

$ gcc -L . -static main.c -lmain

としてコンパイルするとstatic linkされた実行形式が得られることとなります *4

なお、gcc (あるいは別のCコンパイラ) の代わりにzig ccを使う場合はbuild-libのオプションは簡略化することができます。

$ zig build-lib -static -target x86_64-linux-musl src/main.zig
$ zig cc -L . -static main.c -lmain
まとめ
  • zigのコードからCのdynamic libraryおよびstatic libraryを作成してCに組み込むことができた
    • Cで書くにはしんどいコードをzigで書いて組み込む、みたいなadaptiveな使い方が比較的簡単にできそう
  • zig ccは便利

*1:パスについては絶対パスで指定してあげたほうが良いような気はしますが

*2:glibcだと主にNSS周りでトラブルになるので。muslはglibcの互換でstatic linkをオフィシャルにサポートしている

*3:-fPIEでも良い

*4:メモ: なぜか `gcc -I . -L . -static -static-libgcc -Wl,-Bstatic main.c -lmain` などとしてやらないと動かない時があった。けれど本文に書いたように `gcc -L . main.c -lmain` で十分なはず。

令和最新! Linux Desktop環境上の tmux (3.3a) でOSのクリップボードとtmuxのコピペバッファを直結する

Linux Desktop上でterminalを使っている時にOSのクリップボードとtmuxのコピペバッファを直結させたいという話です。
つまり Prefix + [ 等でtmux上で範囲選択してEnterを叩いた時にその内容がOSのクリップボードに格納され、Prefix + ]でペーストする時はOSのクリップボードから引っぱってきて貼り付ける、という挙動にしたいのです。なんでそういう挙動にしたいかというと、そういう設定で長らくmacOS上で生活してきたので……

TL;DR

Wayland環境の場合、xselxclipを使った方法は上手く動きません。wl-clipboardで提供されるwl-copywl-pasteを使わないと駄目でした。

example:

set -s copy-command 'wl-copy'
bind ] run "tmux set-buffer \"$(wl-paste)\"; tmux paste-buffer"

とりあえずこれで動いているという状況。

以下メモ。

tmux 3.2以降では copy-command が使える

tmuxのGitHub RepositoryのWikiにはClipboardという名前のそのものズバリな聖典があり、基本的にはこれを読むとほとんど解決します。なのでこれを読みましょうという感じなのですが、

tmux 3.2 introduced an option called copy-command to set a command to pipe to for all key bindings. This is used when copy-pipe is called with no arguments which is now the default. If the option is empty, the copied text is not piped.

To pipe to xsel(1):

set -s copy-command 'xsel -i'

The more complex configuration in the next section also works with tmux 3.2 and later versions.

https://github.com/tmux/tmux/wiki/Clipboard#how-to-configure---tmux-32-and-later

とあるように、tmux 3.2以降ではcopy-commandという設定項目が導入されているのでこれを使うと手っとり早いです。あらゆるモードでのtmux上でのコピー時に、これで指定したコマンドがフックされて実行されます。つまり、OSのクリップボードにコピーした内容を叩き込むコマンドをここに指定すると良いということです。上記の例では xsel が使われていますが、Wayland環境だと上手く動かなかったのでwl-copyに変えてあげる必要があるでしょう。
古いバージョンのtmuxの場合はドキュメントに書かれているようにもうちょい複雑な設定を細々書かなきゃ駄目で面倒なので、copy-commandがあって良かったですね。

ただこれはあくまでコピーの設定なのでペーストは別途設定する必要があり、ペーストについては聖典にもあんま有効な情報は書かれていません。
なので素朴に

bind ] run "tmux set-buffer \"$(wl-paste)\"; tmux paste-buffer"

とすることで Prefix + ] 時に wl-paste を実行してOS側のクリップボードから内容を引っぱってきて、それをtmuxのpaste bufferにつっこむことで実現しているという感じです。X11環境であればxsel/xclipも使えるのではないでしょうか。

set-clipboard を使えば良いのでは?

set -g set-clipboard on のようにしてset-clipboardを有効にしてやると、

that on both makes tmux set the clipboard for the outside terminal, and allows applications inside tmux to set tmux's clipboard (adding a paste buffer).
https://github.com/tmux/tmux/wiki/Clipboard#changing-set-clipboard

とドキュメントにも書いてあるようにtmux外のクリップボードと連携し、paste bufferについても連携するようであります。
一見するとこれで良いじゃん! となるのですが、set-clipboardのHow it worksのセクションに書かれているようにOSC 52に対応しているターミナルエミュレータを使う必要があります。まあ使えば良いんですが、使っているターミナルがOSC 52に対応していなかったので今回は諦めることに。
というか、試しにOSC 52対応のKittyをインストールして set-clipboard をonにして使ってみたのですが、tmux上でのコピーはOSのクリップボードへ行く一方で、OSのクリップボードからtmuxのpaste bufferに入れられないという挙動になってしまっており、まあなんか厳しいなと思って撤退しました。このへんそこまで深く検証してないです。

まとめ
  • 結局、tmux上のコピーイベントとペーストイベントにフックして、OSのクリップボードと適切にやりとりするソフトウェアを呼び出してインターフェイスする方法が安定する
    • Waylandだとwl-clipboardが提供するツールを使う必要がある。X11だったらxselとかxclipとかは依然使えそう。
  • OSC 52対応端末だと set-clipboardをonにするともしかしたら一発で動くかもしれない (軽く試した感じ駄目だった)。

S3にホストされているMaven RepositoryをMaven (pom.xml) から参照する

S3にホストされているprivateなMaven Repositoryというものがあり、それをMaven (pom.xml) から参照する必要があったのでの方法についてのメモ。Gradleとかsbtとかからは割と使ったことはあったのだけれど、Mavenからは無かったので……

<project>
    ...

     <repositories>
        <repository>
            <id>your-repository-name</id>
            <url>s3://path/to/your/repository</url>
        </repository>
    </repositories>

    ...

    <build>

        ...

        <extensions>
            <extension>
                <groupId>com.github.seahen</groupId>
                <artifactId>maven-s3-wagon</artifactId>
                <version>1.3.3</version>
            </extension>
        </extensions>

        ...

    </build>
</project>

というふうに pom.xml 書いてあげると s3://path/to/your/repository でホストされているpackagesを dependencies から参照できるようになります。 maven-s3-wagon のバージョンについては適宜ドキュメントやリリースノートなどを見て調整しましょう。

AWSのアクセストークンについては READMEのAuthenticationのセクション の通りに解決されるので、そこを読むと良いでしょう。

簡単でしたね。良かった良かった。

Zigのビットシフト演算がちょっと面白い

なんとなくZigを触っているのですが、ビットシフト演算が独特の挙動で面白かったです。

const n: u8 = 0b00000001;
const shifted: u8 = n << 1;
std.debug.print("{b}\n", .{shifted}); // => 0b00000010

これは直感的なコードでしょう。n が1バイト左にシフトしています。

さて以下のコードはどうでしょうか。

const n: u8 = 0b00000001;
const shifted: u8 = n << 8;
std.debug.print("{b}\n", .{shifted}); // expects 0, but...

このコードは

./main.zig:5:30: error: integer value 8 cannot be coerced to type 'u3'
    const shifted: u8 = n << 8;

というコンパイルエラーが返却されます。u8型の値に対しては u3 型すなわち高々 7 までの整数値でのみシフトできるということです。たしかに8 bitの値なんだから最大7 bitシフトできれば良いでしょう、ということのようです。なるほど。
これはシフト対象の型 T に対し、シフトbit数の型が Log2T と定義されているからです。Log2T 型って面白いですね。u3 みたいな型を今まであまり見かけたことが無かったので斬新でした。まあこれが便利かどうかでいうと賛否ありそうな感じはしますね。

// generates the u3 random number
var rnd = std.rand.DefaultPrng.init(@intCast(u64, std.time.milliTimestamp()));
const random_num: u3 = rnd.random().int(u3);
std.debug.print("[debug] rand: {}\n", .{random_num});

const n: u8 = 0b00000001;
const shift_amout: u3 = 7;
const shifted: u8 = n << (shift_amout + random_num);
std.debug.print("{b}\n", .{shifted});

ではこのような、実行するまでシフトするbit数が不定の場合はどうなるでしょうか。
生成された乱数が 0 だった場合は単に7 bitシフトして 0b10000000 が無事得られますが、1以上だった場合は……

[debug] rand: 2
thread 431510 panic: integer overflow
/private/tmp/main.zig:10:39: 0x1005e6727 in main (main)
const shifted: u8 = n << (shift_amout + random_num);
                                      ^
/usr/local/Cellar/zig/0.9.1_1/lib/zig/std/start.zig:551:22: 0x1005e9b7c in std.start.callMain (main)
            root.main();
                     ^
/usr/local/Cellar/zig/0.9.1_1/lib/zig/std/start.zig:495:12: 0x1005e6de7 in std.start.callMainWithArgs (main)
    return @call(.{ .modifier = .always_inline }, callMain, .{});
           ^
/usr/local/Cellar/zig/0.9.1_1/lib/zig/std/start.zig:460:12: 0x1005e6d25 in std.start.main (main)
    return @call(.{ .modifier = .always_inline }, callMainWithArgs, .{ @intCast(usize, c_argc), c_argv, envp });
           ^
???:?:?: 0x10302351d in ??? (???)
???:?:?: 0x0 in ??? (???)
Abort trap: 6

このようなruntime panicが発生します。キャー

とはいえ、そうは言っても様々な理由で Log2T 型 以上のbit数でshiftしたい時もあるじゃないですか、どうすれば……

const n: u8 = 0b00000001;
const shift_amout: u3 = 7;
var shifted: u8 = n << (shift_amout);

var additional_shift_amount: u3 = 1;

var i: u3 = 0;
while (i < additional_shift_amount) {
    shifted = shifted << 1;
    i += 1;
}

std.debug.print("{b}\n", .{shifted}); // => 0

というわけでこうです。追加でシフトしたいbit数 (上記の場合 additional_shift_amount) ぶんだけループで1 bitずつシフトしてあげることでコンパイルエラーとruntime panicを回避することができます。良かった良かった。

ちなみに左シフトをしつつその操作によるオーバーフロー・アンダーフローの発生場合を検出したい時には @shlWithOverflow() という組み込み関数が利用できます。

[追記]

一旦整数のbit数を拡張して、十分な空間でシフトしてから元の型にtruncateするという方法。これも (むしろこれで) 良さそうですね。ありがとうございます。

[追記ここまで]




なお、なんとなくZigを触った結果としてはこのようなライブラリができつつあります。

書評: ISUCONを「ゴール」で終わらせない。『達人が教えるWebパフォーマンスチューニング ~ISUCONから学ぶ高速化の実践』

著者のid:catatsuyさんよりご恵投いただきました。ありがとうございます。実は著者の方から本を頂戴するのってはじめてです。

さて、この書籍のタイトルをはじめて見たときは「オッ、ついにISUCONの攻略本が来ましたね、これでワシも優勝間違いなしや!!」と思ったものですが実際に手に取ってみると必ずしもそうではないことに気付きました。むしろ「ISUCONで勝つための小手先のテクニック」のような話題は極力排除されており、高速かつ高可用なWebアプリケーションをどのように構築・運用していくか、というような実戦的な内容がその多くを占めています。

まず書籍の冒頭では「『Webアプリケーションのパフォーマンス』の定義」から始まり、「なぜWebアプリケーションが高速かつ安定して動作する必要があるのか」という目指すべき方向に対する動機付けが成されています。そういったハイパフォーマンスWebアプリケーションの構築・運用を模したコンテストがISUCONであり、「そのISUCONにどのようなアプローチを以って攻略していくか」というところから逆説的に「ハイパフォーマンスWebアプリケーションの構築・運用」を学ぶという筋書になっています。


「パフォーマンスに問題がある際は必ず計測をし、正しくデータを比較し、そこで明かになったボトルネックにのみ対応していく。勘や憶測で直す部分を決めるのは無駄なのでやらない」といったパフォーマンスチューニングの王道を下敷きとし、パフォーマンスを計測するための負荷試験のプランニングと実施方法、負荷に対するモニタリングの手法、その計測したデータをファクトとしたボトルネックの特定、そしてそのボトルネックに対する具体的な対応、といった内容が体系的に書かれています。

この中でもモニタリングの項が象徴的で、「ログのモニタリング」や「エージェントを使ったモニタリング」などといった継続して長期的にモニタリングを行なうための実践的な内容がおさえられており、ぶっちゃけこのへんってISUCONではやらないというか個人的にはISUCON競技中は目視でdstatの内容とかhtopの内容とか見て済ませがちなんですけど、こういった「実務で使える『ちゃんとした』モニタリング」に多くの紙面が割かれているのはISUCONに使えるテクニックの紹介だけを目的としているのではなく、その先にある「実際のWebアプリケーションを良くしていく」という部分をしかりと見据えているように感じました。


また、負荷試験のプランニングや実施については案外体系的・網羅的な情報が巷に無いように感じていたところ、本書籍ではそのへんが上手く言語化されていて非常に参考になりました。一方で、この書籍でもそうだったんですが負荷試験の項目ってアプリケーションの内容とアクセス特性を知った上でホワイトボックス的に作られがちですよね。ただ実際にアプリケーションを運用していると予期しないアクセスパターンがあったり意図せぬパフォーマンスの秘孔みたいなものがあったりするというのがままあると思っていて、そういうのをこう、探索的に負荷試験のプランニングをしていく方法みたいなのってあるんでしょうか? 毎度頭を悩ませております……


あと特筆すべきはデータベースの章で、著者のid:kazeburoさんのDBのパフォーマンス問題に対する試行・思考が文章としてトレースされている大変貴重な内容だと思います。常日頃から my.conf kazeburo 最強 などとGoogleで検索してはkazeburoさんの秘伝のタレをパクってきてカスタムして使っている身としては非常にありがたいものです、いつもお世話になっております。この章は声に出して読みましょう。
それはそうとしてこの章では言及されていあんかったのですが、ISUCONでよくボトルネック爆弾として設置されているさまじい多重JOINだとか、恐るべき量のサブクエリとかってどうやってほどいて (解決して) いますか? ああいうのを見ると本能的に脳が理解を拒んでしまうのですが、やはり気合と根性でやるしかないのでしょうか……あとこの手のやつってDBレベルでなんとか解決するべきなのか、あるいは別の方法 (例えばRedisとか) でやるべきなのか、といった見切りを付けるのもなかなか難しいところですよね。

キャッシュの章については拙スライドを多く引用・参照していただいてありがとうございました。こういった書籍に自分が書いた内容が載っているというのはありがたいというか、面映ゆいというか、なんだか不思議な感情になりますね。スライドはコレです、もう5年前になるのかこれ……:

また、CDNを活用するパターンの話があったり (これもISUCONというよりは実戦寄りの内容という印象)、Linux Kernel・Kernel Parameterの基礎的な内容があったりと内容が多岐に渡っており面白かったです。ISUCONのベンチマークの作り方については惜しげもなく大公開されていたため技術的にベンチマーカーは作れそうな一方で、実際の問題を作るにあたってはどうすれば? (パフォーマンスが終わっている、しかし筋道立てた解決方法があるアプリケーションを故意に作っるには?) という部分には触れられていなかったので、これは続編の書籍に期待したいと思います!




というわけで、ISUCONはゴールではなく、ISUCONで得た知識・経験を実際のアプリケーションに還元し、ワンランク上を行くことこそが真のGOAL……
『達人が教えるWebパフォーマンスチューニング ~ISUCONから学ぶ高速化の実践』はISUCONを題材としながら、実際のWebアプリケーションをどのように高可用なハイパフォーマンスWebアプリケーションとして構築・運用していくかを体系的に解説している大変良い書籍でした。すなわちこれはISUCONで勝つための単なるTips集ではなく、本チャンのWebアプリケーションを地道に良くしていくための知識の集合であり、つまり本チャンのWebアプリケーションを常日頃からハイパフォーマンスにしていればISUCONにも勝てるはず……この本を買って、毎日実践して、ISUCON勝ちましょう!!!

しかしこんな体系的かつ網羅的なISUCONの本があるなんて本当に良い時代ですね、僕もISUCON 3とかの時代にこれを読んでサクセスしたかった……これからはパフォーマンスチューニングに悩んでいる人がいたら恵比須顔でこの本をおすすめしたいと思います。あとぜんぜん知らなかった内容が普通にポロポロあったんで勉強になりました、それがどういう内容か、ここから先は君の目で確かめてくれ!