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

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

ISUCON 9 決勝を AWS 環境に本番さながらに構築するメモ

github.com

今日 (2020-09-24) の時点では「ローカル環境」で動かす方法については記載がある一方で,何らかのリモートの環境に「本番」っぽく動かす方法についての記載が無いので,それを AWS 上に構築するためのメモを記します.

競技用 application のデプロイ

isucon.net

これを見る限り,参加者側の環境は以下の通り:

アリババクラウドさんの ecs.sn1ne.large を採用しました。
CPUは2コア (Intel(R) Xeon(R) CPU E5-2682 v4 @ 2.50GHz)、メモリは4GBの、オーバーコミットのないインスタンスです。ネットワーク帯域も100Mbpsです。

ただし、今回のアプリケーションではメモリに全ての切らない環境を再現するために、Linuxにはメモリを1GBしか認識させていません。CPUは2コアで、メモリ1GBの環境を再現しています。
参加者にはこちらのインスタンスを3台提供しました。
デプロイには Docker Compose を採用しており、ホストのUbuntu 18.04にはDockerとNginxのみをインストールしています。

AWS で言うところの c5.large で良さそうに見えます.
c5.large だとメモリが 4GB なのでメモリを絞る必要がありそうですが,コレは大丈夫でした (後述).

application のセットアップについては普通に userplaybook 流したら動きました (ansibleのバージョンによっては細々手直しする必要はありそう,実際あります).

前述したメモリの量については ulimit かなんかで素朴に制限かけるかな〜と思っていたところ,playbook がいい感じで /etc/default/grub を以下のように (mem=1G) 書き換えて制限してくれたので問題ありませんでした *1.ただし reboot が必須.

...
GRUB_CMDLINE_LINUX=" net.ifnames=0 vga=792 console=tty0 console=ttyS0,115200n8 noibrs mem=1G"
...

「ハーンなるほど,それであればこの mem=1G を外して reboot すればフルでメモリ使えるから優勝だな? ガッハッハ!!」と悪巧みをしていたところ,それは以下のレギュレーション項目によって潰されていたのでズル不能でした.しっかりしていますね.

OSのメモリサイズが提供時と同じ状態であること

帯域幅については「ネットワーク帯域も100Mbps」とのことで制限が必要そうなのでこれは tc によって行いました *2

tc qdisc add dev ens5 root tbf rate 100mbit burst 50kb limit 500kb

これを手っ取り早く /etc/rc.local にでも書いておくと再起動しても適用されるので便利ですね.

ところでこの素朴な tc による帯域制御は egress にしか適用されないので,ingress にも適用しようとすると若干工夫をする必要があります.まあちょっと頑張って ingress にも適用できるスクリプトを書くか〜と思ったのですが,冷静に考えるとベンチマーカーと外部 APIインスタンスの egress にも帯域制限を適用すれば,とりあえず通る経路が一通り 100Mbps に制限されるのでそれで良しとしました.

あと TLS 対応しているのでなんとかする必要があるのですが……これについては諦めました. HTTP:80 で listen させるようにして,ベンチマーカーにはネットワーク内の private IP を参照してもらうようにしました.

ベンチマーカーと外部 API のデプロイ

ISUCON 9 のベンチマーカーおよび外部 API の動作環境についての記載がなかったので,代わりに ISUCON 8 決勝の環境を参照しました:

ベンチマーカー x 1
 - CPU 3コア : Intel(R) Xeon(R) CPU E5-2640 v4 @ 2.40GHz
 - メモリ 2GB
 - ネットワーク帯域 1Gbps
 - ディスク SSD
外部API x 1
 - vCPU 3コア : Intel(R) Xeon(R) CPU E5-2640 v4 @ 2.40GHz
 - メモリ 2GB
 - ネットワーク帯域 1Gbps
 - ディスク SSD

ISUCON 9 決勝の環境と大差なさそうだったので,ひとまずコレに従うことにしましょう.パッと見 c5.xlarge で良さそう.
メモリはgrubで絞れば良いとして,vCPUは……まあ良いかということで放置.あと tc は application と同様に適用する.

セットアップについては bench playbook を流し込めばOK.

こちらについても TLS 証明書を用意してあげる必要があり,まあ application と同じく平文でやっても良いか……と思ったのですが,なぜか気分が乗ったので certbot + nginx で乗り切りました (手動オペレーション,この辺読みながらやった: How To Secure Nginx with Let's Encrypt on Ubuntu 20.04 | DigitalOcean).




とりあえずコレで動きます.1GB しかメモリがないので mysqlinnodb_buffer_pool とかを潤沢に使うとインスタンスごとポシャったりして大変でした(なお潤沢に使わなくてもポシャる).swap が無いことに起因する話題な気がする.大変でしたね.

*1:それはそうとしてgrub書き換えてくるansible,良いですね……ちょい怖!

*2:本番は tc ではなかったでしょうが

Jenkins + AWS CodeBuildという構成をやめました

かつて Kyoto.なんか #4 で発表した話題ですけれども:

moznion.hatenadiary.com

これはもうやってません!!!(正確に言うと運用している組織内ではリタイアメントの段階に入っています)
今はCodeBuildを単体で使っています.


かつての AWS CodeBuild は

  • ビルド結果の通知が貧弱
  • Trigger が貧弱 (pull-requestに引っ掛けてビルドタスクを回す,みたいな機能が微妙だった)
  • ビルド履歴の一覧性が貧弱

という感じだったので,その不便さを補うために Jenkins を挟んで運用していましたが,今やこれらのペインポイントはほぼ解決しており*1 CodeBuild を単体で使っても充分に快適な開発体験が得られます.
むしろ今となっては Jenkins という部品を間に挟んでしまうとシステムの複雑度が上がってとっつきにくくなってしまいますし,なにより故障部品が増えるのであまり好ましい状況ではありません.強いこだわりや理由がなければ CodeBuild を単体で使うと良いと思います.

今考えると,昨年 fujiwara さんが発表していた「隙間家具」のような感じで Jenkins を活用していたと言えそうですね (結果的に円満に隙間家具ことJenkinsを外せたので).
speakerdeck.com


というわけで現在は Jenkins + CodeBuild という構成ではなく,CodeBuild 単体で使うようにしております,というご連絡でした.こういう情報はしっかりアップデートしておかないといけないなという気持ち (新たに使う人が出てくるかもなんで).

一時期の開発を支えてくれた Jenkins に感謝です.

*1:通知はもう少し細かい設定をしたくなることがありますが

GitHub Packagesにホストされたprivate packageをsbtから使う

表題のとおりです.
GitHubのオフィシャルドキュメントを読むとmavenで使う方法gradleで使う方法は紹介されているのですがsbtで使う方法が調べてもシュッと出てこなかったのでメモとして記す次第.


基本的には以下のようにmavenやgradleと同様にbuild.sbtに対して設定してやるとよろしい.

libraryDependencies += "com.example.your" % "package" % "0.0.1"

resolvers += "GitHub your-package" at "https://maven.pkg.github.com/example/your-package"

credentials += Credentials("GitHub Package Registry", "maven.pkg.github.com", "", sys.env.get("GITHUB_REGISTRY_TOKEN").orNull)

libraryDependenciesは普通にインストールしたいprivate packageを指定し,併せてresolversにはそのpackageに至る宛先を追加します (この時,例における"GitHub your-package"は自分の好きなわかりやすい名前をつけて良い./example/your-packageについてはpackageの属する/org/repoを指定する).

credentialsについては例の通り,第4引数になんらかの形でGitHubトークン文字列を与えると良いです.この際のGitHubトークンの詳細についてはこちらを参照ください: https://docs.github.com/en/packages/publishing-and-managing-packages/about-github-packages#about-tokens
注意としては,Credentialsの第1引数および第2引数は例のとおりである必要があるということです.第2引数については「まあそうでしょう」という感じですが,第1引数を自由気ままに設定するとうまく動きません.これはこの文字列を内部的にrealmとして利用しているためです.

また,Credentialsについてはbuild.sbtに直接書くのではなく,別ファイルに書き出しておくという方法もあると思います (ref: https://www.scala-sbt.org/1.x/docs/Using-Sonatype.html#step+3%3A+Credentials).この辺は状況やお好みに応じてという感じでしょう.


以上です.GitHub Packages,便利ですね.

いい感じにPHP Runtimeのdebパッケージを作る

PHPのエッジなバージョン (例えば今の時点だと 8.0.0alpha3) を使いたい場合,多くの場合はソースコードからビルドする必要があります.その他にもランタイムのオプションをいじりたいだとか,そういった様々な理由からPHPソースコードからビルドしなければならないことがあります.あるでしょう.

ソースコードからビルドするのはまあ良いんですが,そのビルド成果物 (つまりPHPランタイム) のデプロイ対象が多い場合に都度都度その環境でビルドをするのは時間がかかってしまいますし,計算機リソースの無駄遣いです.
というわけでDebianUbuntuを利用している場合だとdebパッケージを一度こさえて,それをばら撒くようにすると経済的です.

以下はミニマムな例:

$ curl -LO https://downloads.php.net/~carusogabriel/php-8.0.0alpha3.tar.gz
$ tar zxf php-8.0.0alpha3.tar.gz
$ cd php-8.0.0alpha3
$ ./configure
$ make
$ INSTALL_ROOT="/path/to/php-dist" make install

というふうにしておき, /path/to/php-dist/DEBIAN/control

Package: php8
Maintainer: moznion <moznion@gmail.com>
Architecture: amd64
Version: 8.0.0alpha3
Description: php 8 binary

というようなcontrolファイルを置いて,

fakeroot dpkg-deb --build /path/to/php-dist/ .

というふうにしておくと良い感じでdebパッケージが作成されます.


特記事項としては

  • ./configure の際に --prefix を付与してはならない
  • --prefix の代わりに make install の際に INSTALL_ROOT 環境変数を付加する

というあたりです.

というのも ./configure--prefix を付けてしまうと,build / install された成果物がそのprefixの内容に依存してしまうため,ビルドした環境の構成 (特にpath) とdebパッケージの展開先の環境構成が異なるときにうまく動作しなくなる場合があります.
例えば,./configure --prefix=/tmp/dist として makemake install すると,成果物は /tmp/dist 以下に展開されます.その状態でdebパッケージを作成し,そのパッケージを利用して別の環境にインストールすると,モノ自体は /usr/local 以下にインストールされますが,インストールされたモノたちは /tmp/dist に依存しています (例えば php.ini の場所は /tmp/dist/usr/local/lib/php.ini にあることが期待されてしまう).びみょう.

というわけで --prefix を渡さないようにしておきつつ (つまりデフォルトの /usr/local が利用される),しかしそのまま make install するとそのまま /usr/local 以下にモノがインストールされてしまいdebパッケージを作成するには具合が悪いので,INSTALL_ROOT 環境変数によって「インストール先だけを変える」ことにより,そのディレクトリに対してdebパッケージを作るといい感じに作成できます (つまりこの場合だと php.ini の場所は /usr/local/lib/php.ini になる).

良かったですね.

The Perl Foundationに寄付した

perl,やはり心のふるさとという感じがある……(流石に全盛期と比較して手のスピードは落ちたけど
大きな問題は他の人にとってふるさとではないということです

とりたてて特別な何かがあったわけではなく,久しくPerlを書いていなかったなか,たまたまPerlXML::XPathを使ってXMLをどやこやするスクリプトを書いたところ上記のような気持ちになったので「ふるさと納税」と称してPerlの総本山ことThe Perl Foundationに寄付したという経緯です.

www.perlfoundation.org

寄付はこちらからできます.PayPalを選ぶと日本からの寄付は未対応である旨が表示されるので,ここは一丁勇気を出して "Online credit card payment" すると良いでしょう.

心のふるさとに寄付をするというのもなかなかオツなものですね.

Javaの非同期アプリケーションのflame graph profileを取る

netty を使うような非同期 Java のアプリケーション (例えば Play2 Web アプリ) の flame graph profile を取るという話題です.色々な方法が考えられますが,jvm-profiling-tools/async-profiler を利用するのが最も手っ取り早そうな感じがしたので,その方法を示します.

github.com

使い方はいたって簡単で,Releases から任意のバージョンのアーカイブを取ってきて,

./profiler.sh -d 60 -f /path/to/flame_graph.svg $JAVA_APP_PID

として実行するだけで 60 秒間プロファイリングし,その結果の flame graph を取得することが可能です.

f:id:moznion:20200620234509p:plain

便利ですね.

tinygo 向けの JSON marshaler: go-json-ice を書いた

English article is here: Released go-json-ice: a code generator of JSON marshaler for tinygo - moznion's tech blog


tinygo では encoding/json を import するとコンパイルできなくなるという問題があり *1,なんらかの struct を JSON に marshal したい時に使える de facto な方法が無いように見えました.これに関しては例えば以下のような issue が立っています:

github.com

github.com

つまり tinygo 上で任意の struct を JSON にしたい時は「手で気を付けてシリアル化する」しか方法がなかったわけですが,まあそれだと何かと不便だったので表題の通り json-ice という encoding/json に依存しない JSON marshaler のコードジェネレータを作りました.

github.com

挙動としては,事前に marshaling 対象となる struct (の json カスタム struct タグ) を解釈して JSON に marshal するコードを吐き出す,という至ってシンプルなものとなります.似たような挙動をする先行実装に mailru/easyjson などがありますが,これらは内部的に encoding/json に依存しているようで,今回の用途にはマッチしませんでした.


例えば以下のような struct を marshal したい時には go:generate と一緒にコードを書いておくと

//go:generate json-ice --type=AwesomeStruct
type AwesomeStruct struct {
	Foo string `json:"foo"`
	Bar string `json:"bar,omitempty"`
}

MarshalAwesomeStructAsJSON(s *AwesomeStruct) ([]byte, error) というコードが生成されるので,それを利用して struct を JSON に marshal することが可能です:

marshaled, err := MarshalAwesomeStructAsJSON(&AwesomeStruct{
	Foo: "buz",
	Bar: "",
})
if err != nil {
    log.Fatal(err)
}
fmt.Printf("%s\n", marshaled) // => {"foo":"buz"}

これにより実行時に reflection を使って動的に marshaling する必要がなくなるので,tinygo でも JSON marshaling が簡単に行えるようになります.また,動的な reflection の代わりに事前計算するので当然の結果ですがパフォーマンスも少し良くなります *2
もちろんその副作用として interface{} な値を持つ struct については動的な型の解決ができないため marshaling ができません.Marshaling するためには静的に型の解決ができる必要があります.

また tinygo は wasm を吐き出す機能も有しており,この wasm が実行時に import するモジュールは「元のコードが何に依存しているか」によって変化してきます.この実行時の依存をできる限りミニマムにしたい (例えばブラウザランタイム以外の強力な sandbox 環境で wasm を動かすというユースケースが考えられる) という動機があったので,生成コードが依存するパッケージは可能な限り最小限にとどめました.結果的に現時点では strconv にのみ依存するようになっています.ミニマル!


そんな感じのライブラリです.どうぞご利用ください!
もちろん tinygo ではなく通常の go の処理系でも利用できますが,それについてはもっと良い先行実装 (それこそ easyjson とか) があると思うので,そちらの利用の検討をおすすめします.


なお,この実装は JSON の marshaling のみをサポートするものですが,逆に tinygo で JSON unmarshaling するにはどうすればよいかと言うと,buger/jsonparser を利用すれば良いように思いました.

> It does not rely on encoding/json, reflection or interface{}, the only real package dependency is bytes.

github.com




余談ですが

//go:generate json-ice --type=DeepStruct
type DeepStruct struct {
	Deep []map[string]map[string]map[string]map[string]string `json:"deep"`
}

のような深く,再帰的(?)な型についてもちゃんとしたコード生成が可能です:

given := &DeepStruct{
	Deep: []map[string]map[string]map[string]map[string]string{
		{
			"foo": {
				"bar": {
					"buz": {
						"qux": "foobar",
					},
				},
			},
		},
		{
			"foofoo": {
				"barbar": {
					"buzbuz": {
						"quxqux": "foobarfoobar",
					},
				},
			},
		},
	},
}

marshaled, err := MarshalDeepStructAsJSON(given)
if err != nil {
	log.Fatal(err)
}

log.Printf("[debug] %s", marshaled) // => {"deep":[{"foo":{"bar":{"buz":{"qux":"foobar"}}}},{"foofoo":{"barbar":{"buzbuz":{"quxqux":"foobarfoobar"}}}}]}

生成コードはこんな感じ

import "github.com/moznion/go-json-ice/serializer"

func MarshalDeepStructAsJSON(s *DeepStruct) ([]byte, error) {
	buff := make([]byte, 1, 54)
	buff[0] = '{'
	if s.Deep == nil {
		buff = append(buff, "\"deep\":null,"...)
	} else {
		buff = append(buff, "\"deep\":"...)
		buff = append(buff, '[')
		for _, v := range s.Deep {
			if v == nil {
				buff = append(buff, "null"...)
			} else {
				buff = append(buff, '{')
				for mapKey, mapValue := range v {
					buff = serializer.AppendSerializedString(buff, mapKey)
					buff = append(buff, ':')
					if mapValue == nil {
						buff = append(buff, "null"...)
					} else {
						buff = append(buff, '{')
						for mapKey, mapValue := range mapValue {
							buff = serializer.AppendSerializedString(buff, mapKey)
							buff = append(buff, ':')
							if mapValue == nil {
								buff = append(buff, "null"...)
							} else {
								buff = append(buff, '{')
								for mapKey, mapValue := range mapValue {
									buff = serializer.AppendSerializedString(buff, mapKey)
									buff = append(buff, ':')
									if mapValue == nil {
										buff = append(buff, "null"...)
									} else {
										buff = append(buff, '{')
										for mapKey, mapValue := range mapValue {
											buff = serializer.AppendSerializedString(buff, mapKey)
											buff = append(buff, ':')
											buff = serializer.AppendSerializedString(buff, mapValue)
											buff = append(buff, ',')
										}
										if buff[len(buff)-1] == ',' {
											buff[len(buff)-1] = '}'
										} else {
											buff = append(buff, '}')
										}

									}
									buff = append(buff, ',')
								}
								if buff[len(buff)-1] == ',' {
									buff[len(buff)-1] = '}'
								} else {
									buff = append(buff, '}')
								}

							}
							buff = append(buff, ',')
						}
						if buff[len(buff)-1] == ',' {
							buff[len(buff)-1] = '}'
						} else {
							buff = append(buff, '}')
						}

					}
					buff = append(buff, ',')
				}
				if buff[len(buff)-1] == ',' {
					buff[len(buff)-1] = '}'
				} else {
					buff = append(buff, '}')
				}

			}
			buff = append(buff, ',')
		}
		if buff[len(buff)-1] == ',' {
			buff[len(buff)-1] = ']'
		} else {
			buff = append(buff, ']')
		}

		buff = append(buff, ',')
	}
	if buff[len(buff)-1] == ',' {
		buff[len(buff)-1] = '}'
	} else {
		buff = append(buff, '}')
	}
	return buff, nil
}