GDBでGo言語のスライスの値をのぞいてみた
最近Go言語をよく触っています。触っているうちに、スライス、文字列、インターフェースなどの値がメモリ上でどう表現されているか知っておくと実装やデバッグ時に役立ちそうだなーと思うことが度々でてきました。そこでGo言語本体のソースコードを読みつつ、そのあたりをGDBでみてみました。そのときのメモです。今回はまず、スライスと文字列についてです。
使用するデバッガ
そもそもGo言語のデバッガについてよく知らなかったので、どんなのがあるか調べてみました。有名なのは次の3つのようです。
- godebug
- delve
- GDB
機能的にはdelveが一番リッチな感じでしたが、今回は簡単にメモリを覗けそうなGDBにしました。
ちなみにOSはMac OS X 64bit、Goのバージョンは1.6.2です。
スライスの値
スライスは次のような構造体をもっています(ソース)。
type slice struct { array unsafe.Pointer len int cap int }
array
は配列へのポインタ、len
はスライスが保持している値のリストの長さ、cap
は(array
が指すアドレスを始点とした)配列の長さです。len
とcap
の違いについてはGo Blogを読むのがいいかと思います。
スライスの値をGDBでみるにあたり、シンプルな例として次のようなコードを使います。
package main func main() { s := []int{1, 2, 3} _ = s }
GoのバイナリをGDBから扱いやすくするため、次のようにgcflagsオプションをつけてビルドします。
go build -gcflags "-N -l" slice1.go
GDBを起動して、スライスの値をみてみます。// 以降の文字は後で書き足しています。
% gdb ./slice1 ...省略... (gdb) break main.main // main関数にbreakpointを設置 Breakpoint 1 at 0x2040: file /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice1.go, line 3. (gdb) run // 実行 Starting program: /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice1 [New Thread 0x1313 of process 64589] [New Thread 0x1403 of process 64589] [New Thread 0x1503 of process 64589] Breakpoint 1, main.main () at /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice1.go:3 3 func main() { // breakpointで止まった行 (gdb) next // 次の行へ 4 s := []int{1, 2, 3} (gdb) next // 次の行へ 6 } // 今はこの行の実行前 (gdb) print &s // 変数sのアドレスを確認 $1 = (struct []int *) 0xc820035f30 (gdb) x/3gx 0xc820035f30 // 変数sの値を確認(1) 0xc820035f30: 0x000000c820035f10 0x0000000000000003 0xc820035f40: 0x0000000000000003 (gdb) x/3gx 0xc820035f10 // arrayの中身を確認 0xc820035f10: 0x0000000000000001 0x0000000000000002 0xc820035f20: 0x0000000000000003
(1)の結果から、変数sに配列へのポインタ、長さ、キャパシティが格納されているのがわかります。
スライスを関数の引数にする
次に、関数の引数にスライスを与えたとき、スライスがどうなるか確認してみます。次のコードを使います。
package main func add4(s []int) []int { s = append(s, 4) return s } func main() { s := []int{1, 2, 3} s = add4(s) }
ビルドして、GDBを起動します。
% gdb ./slice2 ...省略... (gdb) break main.main // main関数にbreakpointを設置 Breakpoint 1 at 0x2170: file /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice2.go, line 8. (gdb) run // 実行 Starting program: /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice2 [New Thread 0x1313 of process 68743] [New Thread 0x1403 of process 68743] [New Thread 0x1503 of process 68743] Breakpoint 1, main.main () at /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice2.go:8 8 func main() { // breakpointで止まった行 (gdb) next // 次の行へ 9 s := []int{1, 2, 3} (gdb) next // 次の行へ 10 s = add4(s) // 今はこの行の実行前 (gdb) print &s // main関数内の変数sのアドレスを確認 $1 = (struct []int *) 0xc820035f30 (gdb) step // add4関数に入る main.add4 (s=..., ~r1=...) at /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice2.go:3 3 func add4(s []int) []int { (gdb) next // 次の行へ 4 s = append(s, 4) (gdb) next // 次の行へ 5 return s // 今はこの行の実行前 (gdb) print &s // add4関数内の変数sのアドレスを確認 $3 = (struct []int *) 0xc820035ee0 (gdb) x/3gx 0xc820035ee0 // add4関数内の変数sの値を確認(3) 0xc820035ee0: 0x000000c82000e060 0x0000000000000004 0xc820035ef0: 0x0000000000000006 (gdb) x/3gx 0xc820035f30 // main関数内の変数sの値を確認(4) 0xc820035f30: 0x000000c820035f10 0x0000000000000003 0xc820035f40: 0x0000000000000003
ポイントとしては、add4関数内の変数sの値(3)と、main関数内の変数sの値(4)が異なっているところです。add4関数内の変数sは、appendによりlen
が4になっています。また、append時に配列の再確保が行われ、cap
とarray
の値も変わっています。一方で、main関数内の変数sはそのままです。ここから、関数の引数にスライスを与えると、スライスの値はコピーされることがわかります。
文字列の値
ついでに文字列の値もみてみます。文字列は次のような構造体をもっています(ソース)。
type stringStruct struct { str unsafe.Pointer len int }
str
は配列へのポインタ、len
は配列の長さです。
今度は次のコードを使います。
package main func main() { str := "Hello World" _ = str }
ビルドして、GDBで見てみます。
% gdb ./string (gdb) break main.main // main関数にbreakpointを設置 Breakpoint 1 at 0x2040: file /Users/yagami/src/go-lib/src/github.com/ks888/hello/string.go, line 3. (gdb) run // 実行 Starting program: /Users/yagami/src/go-lib/src/github.com/ks888/hello/string [New Thread 0x1313 of process 74658] [New Thread 0x1403 of process 74658] [New Thread 0x1503 of process 74658] Breakpoint 1, main.main () at /Users/yagami/src/go-lib/src/github.com/ks888/hello/string.go:3 3 func main() { // breakpointで止まった行 (gdb) next // 次の行へ 4 str := "Hello World" (gdb) next // 次の行へ 6 } (gdb) print &str // 変数strのアドレスを確認 $2 = (struct string *) 0xc82003bf38 (gdb) x/2gx 0xc82003bf38 // 変数strの値を確認 0xc82003bf38: 0x0000000000071ca0 0x000000000000000b (gdb) x/2gx 0x0000000000071ca0 // 変数str内のポインタの値を確認 0x71ca0 <go.string.*+8024>: 0x6f57206f6c6c6548 0x0000000000646c72
変数strの値をみると、配列へのポインタ(0x0000000000071ca0
)と、配列の長さ(0x000000000000000b
)が格納されています。また、ポインタの指す先をみてみると、リトルエンディアンな上16進数で見づらいですが、Hello World
の文字列が確認できます。
感想
こういう風に理解しておくと、スライスで迷いにくくなるのでは、と期待しています。次はインターフェースがメモリ上でどう表現されているかについて書こうと思います。
参考にした記事
- http://research.swtch.com/godata Go開発者のRuss CoxによるGo言語のデータ構造解説
- https://golang.org/doc/gdb Go言語のドキュメント内にあるGDBの使い方解説
- https://blog.golang.org/go-slices-usage-and-internals スライスの内部構造解説
StackdriverでGCP上のサービスを監視してみた
今関わっているサービスは、GCPのGoogle App Engine (GAE) とGoogle Container Engine (GKE) を使って開発しています。そのサービスの監視にStackdriver Monitoringを使ってみたときのメモです。
現在の印象としては、かなり面白い機能はあるけど、基本的に思えることがうまくできないことがある、なかなか癖のある監視サービスという感じです。いくつかの機能について思ったことを以下に書いておきます。
Uptime Checks
Uptime ChecksはStackdriverの各種機能の中でも割とちゃんと使える機能だと感じました。
大抵の監視サービスだとUptime Checksのチェック先はURLで指定すると思いますが、StackdriverではGAEのModuleやグループで指定できます。下の図は(ぼかしでやや分かりづらいですが)GAEのModuleを選んでいるところです。
細かな設定として、文字列マッチングによる成否条件の設定や、任意のHTTP Header付与もできます。
デフォルト設定でも6リージョンからチェックしてもらえるのは驚きました。まぁ6リージョンいるかは分かりませんが。笑
Pingdomのような外形監視が売りのサービスに比べるとトランザクショナルな監視がないとかありますが、基本的な機能は揃っており、十分使えるレベルだと思います。
リソースの監視
リソース監視は、最初から色々設定されていて助かりましたが、結構ハマりどころがありました。
ある程度のリソース監視は最初から設定されています。特にGAEについては、結構充実しています。まぁPaaSですしね。例えば以下の項目です。
- Response Codeごとのカウント
- Response LatencyのAverageと95th percentile
- CPU使用量
- メモリの使用量
- Datastoreの各種操作回数
また、以下のようなグラフも最初から設定されています。以下はHTTP関連のグラフです。
一方で、最初から設定されているグラフにデータが全く来ない、ドキュメントの手順どおりに操作しても監視エージェントをインストールできないなど、ハマりどころもかなりありました。
特に、GKEが起動・管理するGCEインスタンスに、監視エージェントを自動インストールする方法は今でもよく分かりません。手動ならできるのですが。。そもそも、そこは監視しなくていいということなんでしょうか。。
また、相対値をグラフで見たいときにどうするのか、よくわかりませんでした。例えばメモリ使用量ではなくメモリ使用率を見たい場合とか、HTTPリクエストが5XXを返す割合を見たい場合です。収集した値に対する演算は、合計、平均、95th Percentileなど色々ありますが、相対値を見るための方法は見つかりませんでした。
リソース監視については、機能あるいはドキュメントがやや不足気味で、ちょっと使いにくい部分があると思います。
アラート
アラートは設定が大変だったので、正直あまり使いたくないです。
機能不足なわけではないです。機能に関しては、AWSのCloudWatchとかと比較してもそれほど遜色ないと思っています。アプリケーションのログにフィルターをかけてメトリクスとアラートを作成したり、複数のリソースからの値を集約したりできます。以下のように、結構色々な集約方法を選べます。
また、アラートを鳴らす条件を複数設定できるので、例えば、特定のパスにリクエストが来ているときはアラートを鳴らさない、みたいな設定もできます。まぁあまりこだわった条件にすると、メンテが大変そうですが。
アラートで大変だったのは、アラート作成のAPIが見当たらなかったことと、(全GAE Moduleなどの)グループに対してまとめてアラートを設定する機能がうまく動かなかったことです。その結果、個別のGAE Moduleごとに複数のアラートを手動で設定することになってしまいました。当然、時間がかかりますし、設定ミスもありますし、バージョン管理もできません。
さらに、作成したアラートを保存しようとすると「Failed to Save Policy. Please try again or contact support.」と言われ、何度やっても保存できなくなるバグもありました。諦めて最初からアラートを作りなおすとうまく行きます。
このように設定作業で苦しんだので、アラートは僕の中ではあまり使いたくない機能になっています。こうすると楽だよーみたいなのあれば教えて下さい!
感想
まだβですし、ドキュメントも公開されているノウハウもかなり不足していますし、バグじゃないか?と思える挙動もあります。でも、GAEとの相性はいいし、面白い機能もあるので、余裕があれば使ってみるのもありだと思います。
AWS LambdaでCI環境を作れるOSS「LambCI」試用メモ
これまでそれほど縁がなかったサーバレスですが、LambCIというOSSを使うとAWS LambdaでCI環境が作れるという話を聞き、ちょっと使ってみました。
GitHub - lambci/lambci: A continuous integration system built on AWS Lambda
LambCIとは
LambCIの紹介としては作者がMediumに投稿した記事がわかりやすいです。
Introducing LambCI — a serverless build system – Michael Hart – Medium
LambCI開発の背景部分をかいつまんで書き出すと、以下のような感じです。
- CircleCIみたいなサービスは、運用は楽なんだけど、結構高い
- Jenkinsみたいな自前環境は、運用が大変
- サーバレスアーキテクチャでCIサービスを作れば、運用は楽だし、ビルド処理分だけのお金で済みそう
LambCIの主な構成要素としては、以下のような感じです。ほとんどの構成要素は、CloudFormationで数分で作成できます。
- GitHub: Push等のイベントをAmazon SNSに伝える
- Amazon SNS: イベントをトリガーにAWS Lambdaを起動する
- AWS Lambda: ビルド処理を実行し、Amazon S3、Amazon DynamoDB、Slackに結果を保存・通知する
- Amazon S3: ビルド結果のhtmlや成果物を保存する
- Amazon DynamoDB: ビルド結果と設定値を保存する
ビルド処理は、レポジトリ内の.lambci.json
あるいは.lambci.js
というファイルに書きます。以下は.lambci.json
の例で、Pythonのtoxでテストを実行しています。
{ "cmd": "pip install --user tox && tox" }
実際にビルドしてみると、以下のような画面でビルド結果を確認できます。この画面はS3内にhtmlファイルとして保存されています。
AWS Lambda内の処理について
Lambda Function内の処理はNode.jsで記述されています。Node.jsが、.lambci.json
で指定されたコマンドをbash経由で実行します。
実行できる処理
Lambda Functionの実行環境内でできる処理はなんでもできますが、足りないバイナリやライブラリは自分で用意する必要があります。例えばGoツールは含まれていないので、go test
とかしたい場合は、Goツールをダウンロードする必要があります。
そのほか、Lambda Functionの実行環境については、以下の記事に書かれています。
ちなみにPythonはやや特別扱いされています。PythonはLambdaの実行環境に元々含まれている上に、LambCIがpipをバンドルしてくれているので、いきなりpipを使用できます。
ビルドの並列化と実行時間制限
TravisやCircleCIだと、設定ファイルを読んでビルドの並列化をしてくれますが、LambCIにはまだそういう機能はまだありません。これに、Lambda Functionの最大実行時間5分という制限が加わると、ちょっと厄介です。ビルドを並列化なしで5分で済ませないといけません。
LambCIとしては、ビルドをAmazon ECS(EC2 Container Service)上で実行することで、この制限を超える方法を用意しています。Dockerコンテナの中でさらにDockerコンテナを動かす仕組みになっていて、内側のDockerコンテナでビルド処理が動きます。このECSを使った方法ですが、避けたはずの「運用」の二文字が再び見え隠れしてきて、個人的にはイヤな感じがします。
まぁビルドの並列化についてはv1.0へのロードマップにも出ているので、そのうち実装されると思います。
感想
これまでCI環境を用意するとなると、それなりの運用コストを払って自前環境を作るか、お金を払ってCircleCIとか使うか、という選択肢だった気がします。LambCIはここに、運用コストを大幅に抑えつつ自前環境を作る、という選択肢をもたらすかも、と思いました。現状はまだ最低限の機能しかありませんが、v1.0が出る頃にはかなり使えるモノになっている気がします。
個人的には、LambCIに対するCircleCIとかの反応が気になります。例えば、課金モデルはどうなるでしょうか。CircleCIとかは、並列度を上げたいならもっとお金を払ってね、というモデルです。対してLambCIは、どれだけ並列度を上げてもAWSに払う金額は変わりません。あくまで合計ビルド時間に応じた料金が発生するだけです。
ユーザは、最初はCircleCIを使っていたとしても、並列実行するために高いお金を払うぐらいならLambCIに移行しようと考えそうです。そんな感じでユーザーが離れていくと、並列度に対して課金するモデルは成立しなくなり、合計ビルド時間に対して課金するモデルとかに移行せざるを得なくなるのでは、とか妄想しています。
追記: LambCIと似たコンセプトで、LambStatusというOSSを作っています。サービスの稼働状況をユーザに伝えるステータスページ(例:GitHubのステータスページ、Travis CIのステータスページ)を、サーバレスに実現するOSSです。よかったら見てみてください。