flyhigh

Use your source, Luke!

Go1.7からSSAが導入された

初めに

Go 1.7がリリースされる。目玉の一つは、SSA-IRが導入されたことだろう。Go1.7でSSAが入るんだけど、SSAって何?と居酒屋で聞かれたことが本稿の発端だった。私の知識だけでは包括的な説明にならなかったので、いろいろ調べつつそれをまとめた。 以下、一般的な話ではなく、なるべくGoに絞って話を進めている(コンパイラのコードははcmd/compile/internal/gcあたりにある)。より一般的な話は、参考文献等を示したのでそちらを参考にしてほしい。いろいろ調べて、Goに追加されたSSAについて知るべきことは、SSAが何か?よりも、SSA導入したGoがどうなったか、であると思った。

SSAとは何か

SSA とは Static Single Assignmentの略だ。一つの変数への代入は一度しか行われてない事が確約できる形式のコードだ。この形式のコードは、最適化が行いやすいらしく、例えばSSAが前提と成ってる最適化手法(GVNなど)もある。SSAが導入された経緯の説明の前に、まずは現状(1.6まで)についてまとめる。

なぜ最適化が必要か

SSAは最適化のために利用される。このためコンパイラが行っている最適化について知る必要がある。まず、最適化なしのコンパイラが何をしているかを説明する。(もう知ってるよという方は、「問題」から入っていただければ良い。)簡単に言えば、プログラムを一文一文、愚直な機械語に変換している。

1
2
3
a := 3   // (a)
b := 4   // (b)
c := a + b    // (c)

というコード片があるとしよう。これを最適化せずにコンパイルすると、以下のように換える。

1
2
3
4
5
6
MOVQ $0x3 0x10(SP)   // (1) <-- (a)
MOVQ $0x4 0x8(SP)    // (2) <-- (b)
MOVQ 0x10(SP) BX     // (3) <-- (c)
MOVQ 0x8(SP) R8      // (4) <-- (c)
ADDQ R8 BX           // (5) <-- (c)
MOVQ BX 0(SP)        // (6) <-- (c)

コメントに対応関係を示したが、(a)~©はそれぞれ(1)~(6)へと変換される。まず、(a),(b)のように定数を変数に代入する場合、これを変数のアドレス(スタックに取られた変数用の領域)に転送する。次に©では加算を行うが(3),(4)では先ほ変数に格納した値をBXとR8レジスタに読み込む。そして、これらは(5)で加算され、(6)で計算結果が再び変数領域に書き込まれる。 では、この機械語列をもっと効率的に実行するにはどうしたら良いだろうか。上の機械語列、(1)~(4)は、計算すべき値を一度変数に格納して、レジスタに読み直すということをしてるが、これは無駄である。初めから計算すべき値だけをレジスタに乗せれば良い。 最適化がなされていないコードはこのように元のコードと生成された機械語の対応関係が明らかな場合が多い。これは、ソースコードを一文一文、素直にコンパイルしているだろうことがうかがえる。

Go1.6での最適化

素直に変換したコードには無駄があった。最適化は、この非効率なコードをより効率的なものへと、機械的に置き換える。Goのコンパイラは1.6以前、最適化と言ったら主に2つあった(-Nフラグで切り替えることのできる最適化を主なものとしてる)。それらはniloptregoptという関数でそれぞれ行われている。上のコードは、regopt によって効率的に置き換えられるため、regoptについて話す。(コードはreg.go)

一般にはレジスタ割り付けと呼ばれるものだろう。CPUでの演算、すなわちコンピューターで行うほとんどの演算は、レジスタに計算すべき値を読み込んで行う。しかしレジスタは数が限られているので、これらをいかに効率よく使うかという問題が発生する。生成された命令で、使ってるレジスタをマークしていき、もし空いているレジスタがあればそちらを使うようにする。すると、以下のような状態になる。

1
2
3
4
5
6
MOVQ $0x3, CX
MOVQ $0x4, AX
MOVQ CX, BX
MOVQ AX, R8
ADDQ R8, BX
MOVQ BX, AX

(1)や(2)で行われていた、変数への書き出しがなくなって、レジスタ(そういえば、AXBXなどがレジスタであることを言い忘れた。一般的なレジスタ一覧はこちら)への直接書き込みに変換されていることがわかる。 しかしまだ、効率が良くない。ADDが利用してるレジスタであるR8BXに直接読み込みしてほしい。 この後、peephole最適化というものが行われる。局所最適化とも呼ばれるが、要は、コードを眺めて、あぁここは明らかに効率悪い、こうしましょうね。というものをピックアップし、アドホックに行う最適化のことである。ここでは、レジスタの無駄なコピーを省くというpeephole最適化が適用されており、以下のようなコードになる。

1
2
3
MOVQ $0x3, BX
MOVQ $0x4, AX
ADDQ AX, BX

ここまでくれば、効率良い機械語へ変換できている。具体的にどの程度差がつくか、測ってみてもいいかもしれない。

Go1.6までの問題?

問題というより、Go1.6の最適化はまだ未発達な部分がある。手をつけてないと言った方が正しいだろう。「Go 1.5からGo言語が全てGoでかかれるようになったのは周知の事実だ。これを踏まえて、GoのIRもSyntax-tree-basedのものから近代的なSSA-basedのものに変えれば、今のコンパイラでは実装が難しい(Go1.4時点)、様々な最適化が行えるようになる(意訳)」というくだりからSSAの提案書は始まっている。つまり、明示的にこの最適化がしたい!というより、とりあえずSSAにしておけば種々の最適化が導入できそうだよね、というモチベーションっぽい。

SSA導入で何が変わったか

最適化が実装しやすくなる、という名目で導入されたSSA。では1.7ではどのような最適化が入っているのか。実際にリストアップしてみたのだが、かなりの数入ってることがわかる。それぞれ対応する関数が書かれてるので、中身はそちらでみれるようだ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
var passes = [...]pass{
  // TODO: combine phielim and copyelim into a single pass?
  {name: "early phielim", fn: phielim},
  {name: "early copyelim", fn: copyelim},
  {name: "early deadcode", fn: deadcode}, // remove generated dead code to avoid doing pointless work during opt
  {name: "short circuit", fn: shortcircuit},
  {name: "decompose user", fn: decomposeUser, required: true},
  {name: "opt", fn: opt, required: true},               // TODO: split required rules and optimizing rules
  {name: "zero arg cse", fn: zcse, required: true},     // required to merge OpSB values
  {name: "opt deadcode", fn: deadcode, required: true}, // remove any blocks orphaned during opt
  {name: "generic domtree", fn: domTree},
  {name: "generic cse", fn: cse},
  {name: "phiopt", fn: phiopt},
  {name: "nilcheckelim", fn: nilcheckelim},
  {name: "prove", fn: prove},
  {name: "loopbce", fn: loopbce},
  {name: "decompose builtin", fn: decomposeBuiltIn, required: true},
  {name: "dec", fn: dec, required: true},
  {name: "late opt", fn: opt, required: true}, // TODO: split required rules and optimizing rules
  {name: "generic deadcode", fn: deadcode},
  {name: "check bce", fn: checkbce},
  {name: "fuse", fn: fuse},
  {name: "dse", fn: dse},
  {name: "tighten", fn: tighten}, // move values closer to their uses
  {name: "lower", fn: lower, required: true},
  {name: "lowered cse", fn: cse},
  {name: "lowered deadcode", fn: deadcode, required: true},
  {name: "checkLower", fn: checkLower, required: true},
  {name: "late phielim", fn: phielim},
  {name: "late copyelim", fn: copyelim},
  {name: "phi tighten", fn: phiTighten},
  {name: "late deadcode", fn: deadcode},
  {name: "critical", fn: critical, required: true}, // remove critical edges
  {name: "likelyadjust", fn: likelyadjust},
  {name: "layout", fn: layout, required: true},       // schedule blocks
  {name: "schedule", fn: schedule, required: true},   // schedule values
  {name: "flagalloc", fn: flagalloc, required: true}, // allocate flags register
  {name: "regalloc", fn: regalloc, required: true},   // allocate int & float registers + stack slots
  {name: "trim", fn: trim},                           // remove empty blocks
}

SSA形式に変換されたコードは、ウォーターフォール的にこれらの最適化ステージを全て通る。そして出来上がったコードから、機械語が生成される。

さて、実装者はデバッグ用に、便利な機能を残してくれている。

1
GOSSAFUNC=main go build

でプログラムをコンパイルしてみてほしい。GOSSAFUNCにはデバッグしたい関数名を入れる。コンパイル結果と別に、ssa.htmlというHTMLが生成されているだろう。これは、各最適化ステージ別にその時のSSAコードの様子を示してくれる。以下は、最初のコードをコンパイルしてみた結果だ。early deadcodeというステージで、関数ごと消されている。これはテスト用に作った関数で、最終的には_=cとしていたためである。

ssa.html

最適化の結果、ベンチマークのほぼ全てが5~35%の高速化を達成しており、バイナリのサイズも20%ほど小さくなっている。これは無駄なコードが最適化により削れたことの恩恵だ。しかし一方で、コンパイル時間(純粋なコード生成の時間)は10%ほど増えている。他の人との話をすり合わせてわかったが、go buildの時間は改善してるので、リンカなどが早くなったためにビルド全体の時間は改善されたようだ(?)。

終わりに

先日、Go Release party TokyoでSSAについて喋ってきた。SSAとは何か?という話をするつもりだったが、GoのSSAについてはほとんど触れてなかったため、本稿を書いた。また、遅くなったが、稿末に質疑応答でペンディングしてた質問への回答も書いているので参考にしていただきたい。

質疑への回答

Q1. Go-1.7で速度が落ちてるベンチマークもあるがその理由は?

A1. まず結果の図はこちら。Manderbrotが遅くなってる。MLに回答があるが、Mandelbrotはコードにループのネストがある。peephole最適化が行っていたレジスタ割り当てについてはSSAの方が劣っているようだ。ここは今後の改善が見込まれるところだといっている。

Q2. こう書けば早くなる、というコードはあるか?

A2.一般的にループ最適化に弱いようで、上のMandelbrotも示すように複雑なループはGo1.6以前の方が積極的に最適化されてたようだ。もしループのネストが多くあるようなコードの場合は、手動で最適化した方が速度は出ると思われる。

参考

Javaのテスト実行時間を62%削るvmvmを試してみた

概要

vmvmてのを使うと、テスト実行時間が短くなることがあるらしいので、試してみた。 結果、私の試行では早くならなかったが、早くなる人もいるかと思われ、使い方をシェアしたい。

vmvmて何?

論文漁ってたらたまたま見つけた、ICSE ‘14のペーパ。すでに1年経過してる。タイトルは、”Unit Test Virtualization with VMVM”。Unit test virtualization??と思いつつも、読み進める。どうやらJavaのテスト実行時間を62%短くする、vmvmてのを作ったらしい。62%て。 なんとも驚異的な結果。さすがICSE。

使ってみる(maven編)

(antは公式READMEを参照)

論文の中身はさておき、まずはチェックアウト。ビルドしてインストール。バイナリもあるのでそれにパスとおしても良い。

1
2
git clone https://github.com/Programming-Systems-Lab/vmvm.git
mvn install

vmvmmavensurefireプラグインから使うコンポーネントとして提供されている。 surefire2.15以上がいるので、そうでない場合はアップグレード。

dependencyまで含めたsurefire部分のpom記述はこうなる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

    <dependencies>
        <dependency>
            <groupId>edu.columbia.cs.psl.vmvm</groupId>
            <artifactId>vmvm</artifactId>
            <version>1.0.0-SNAPSHOT</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>edu.columbia.cs.psl.vmvm</groupId>
            <artifactId>vmvm-ant-junit-formatter</artifactId>
            <version>1.0.0-SNAPSHOT</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
                <configuration>
                    <properties>
                        <property>
                            <name>listener</name>
                            <value>edu.columbia.cs.psl.vmvm.MvnVMVMListener</value>
                        </property>
                    </properties>
                </configuration>
            </plugin>
        </plugins>
    </build>

最後にテストを実行する。

1
mvn clean test

結果

残念ながら、私がピックアップした小さなレポジトリではあまり変わらなかった。

どんな場合に早くなりそうか

そもそも、vmvmが対象にするのは、大規模かつ、プロセスが分かれてる(isolatedな)テストセットのようだ。 プロセスが分かれてるというのは、おそらくテストクラスがわかれてるということだ。 これを同じプロセスで実行すると、リソースが再利用、節約できて、実行時間が劇的にはやくなるよ (ざっくりすぎて怖い。詳しくはぜひ論文を)、というわけだ。 従って、同じ初期化処理を共有する、小さなテストクラスがいくつも存在する大規模ソフトウェアに対して 有用であると考えられる。私が試したプロジェクトは、テストクラスはたかだか20程度だろう。

まとめ

vmvmは劇的な数字と一流学会論文成果という敷居が高そうなソフトウェアだが、気軽に使えたので記事を書いてみた。

個人的には、これまでで、慣習的に、同じ初期化処理があるものは、同じテストクラスに書いている場合もあった。 しかし、これはクラス内のコードが長くなり、かつ、読みにくく、エラーもひろいにくくなる。 vmvmがあれば、わざわざテストを同じところに書かずに、小分けにしても実行速度が担保できるのかも。

参考

Gitで一個前のコミットから作業したいケースと方法

概要

git revert --no-commitが使えるね、というメモ。

動機

Javaを書いていると、以下のようなバージョニングを行う。(pom.xmlなどの記述)

1
version=1-SNAPSHOT

そもそも、1.0.0-SNAPSHOTじゃないのか、という話はここでは一旦置いておく。このバージョンをリリースしたら、こうなる。

1
version=1

これでgit tag version-1などして、タグ付けしたら、バージョンをあげてコミットしたいだろう。

1
version=2-SNAPSHOT

この作業を手作業でやることになったが(自動リリーススクリプトがうごかないケースがあった)、記述箇所が多い為、sedで置換などする。 1-SNAPSHOT1にするのは、簡単だった。しかし、12-SNAPSHOTにするのは至難である。 1はバージョン以外にも使われてる為である。 だったらコミット前の状態(1-SNAPSHOTから1にしたもの)に戻して、1-SNAPSHOT2-SNAPSHOTに置換したい。

git reset HEAD^ではなく?

git reset HEAD^を試してみる。1-SNAPSHOTに戻った。さて、これを2-SNAPSHOTにしよう。 置換できた。コミットしよう。。。 いや、だめだ。これだと分岐してしまう。 この状態をパッチにして、HEADに当てようか、、いや、それもだめだ。 HEADは1がbaseであるためパッチはあたらない。 commitの逆操作をcommitなしにパッチとして取り出して、それを書き換えたい。

どうやるか

git revertは、あるコミットを打ち消すコミットをすると記憶していた。 具体的には + 逆操作を行うパッチを作成する + パッチをコミットする という2段階の操作が行われる。これを1段階目だけで終えたい。そのためには、git revert --no-commit(もしくは-n)とすればいいそうだ。

1
2
3
4
git revert HEAD --no-commit
git reset HEAD 
(1-SNAPSHOTを2-SNAPSHOTへ置換)
git commit

かゆいところに手が届く。やはり良い。

参考

Vagrant内のJVMに対してJconsoleとか使う、JMX Over SSH

Javaのパフォーマンス解析には、Jconsoleとか、VisualVM などを使う。

最初の小さなジレンマは、これらの解析ツールがGUIであることだ。私が使うテスト環境Vagrant内部にはGUIをインストールしてないため、Vagrant外部から解析したいわけだが、VMの外から中のポートはたたけない。ポートフォワーディングでいいかとおもいきや、単純にフォワードしてもダメだった。なぜかはよくわからない。

というわけで、今日は簡単な方法を発見したのでシェアだ。

SOCKS プロトコル

SOCKSというプロトコルがあるそうだ。クソなプロトコルのことではない。Socketsからきている。SOCKSは、あるポートにバインドした通信路へのソケットを提供してくれるらしい。これがTCP/IP全般を肩代わりできるらしいので、用途は広い。この通信路でSSHを経由してJMXを通す。sshにはこのSOCKSを使うことができるオプション-Dがついている。これと、バックグラウンド実行の-f, リモートコマンド実行しない-Nをつけて、

1
ssh -fND $(PORT) vagrant@myenv

これで$(PORT)番のポートにSOCKSができる。

Jconsoleから使う

1
jconsole -J-DsocksProxyHost=localhost -J-DsocksProxyPort=$(PORT) service:jmx:rmi:///jndi/rmi://localhost:$(JMX_PORT)/jmxrmi

これでいけた。

Visual VMから使う

1
jvisualvm -J-Dnetbeans.system_socks_proxy=localhost:$(JMX_PORT) -J-Djava.net.useSystemProxies=true

こっちはまだ試していないが、これでいけるらしい。

まとめ

今回の話は、vagrant内のJVMに限らない。Firewallで囲まれてる環境の外からJMXツールを使う場合は汎用的に使えるはずだ。 ただし、CUIから使うことに苦がない、RMIの呼び出しなどは、jmxtermがおすすめである。 グラフをGUIでみるなど外部からモニタリングしたい、かつ、プロダクションみたく監視ソフトウェアが充実している環境ではない場合に、使える小技である。

参考

recoverでGoのテストのスタックトレースを省略する

概要

Goでテストを書く際にt.Errorfなどで問題を表示することは多々ある。しかし、テストがいくつかあった際にダンプをズラズラと表示されたときは、一概でどのテストが失敗したのか追うのがめんどくさいと感じることもあるだろう。そういう時は、recoverdeferをうまく使えばよいらしい。 具体的にはテストに以下ようなコードを足せば、うまく省略できる。

1
2
3
4
5
defer func() {
  if r := recover(); r != nil {
    t.Error("Too lazy to show stack trace. This test has failed. Fix it.")
  }
}()

問題

たとえば、以下のようなテストを考える。例ではStackという型を定義したパッケージでも作ったこととしよう。

1
2
3
4
5
6
7
8
9
10
11
12
package stack

import (
  "testing"
  )

  func TestNewStack(t *testing.T) {
    var s Stack
    if s.Len() != 0 {
      t.Errorf("The length of stack shold be 0, not %d", s.Len())
    }
  }

このテストは、ヌルポでpanicを起こす(t.Errorfは内部でpanicを呼ぶ)。なぜなら、この例で使ってるStackinterfaceとして定義しており、 実際の変数宣言はstackというstructで宣言しなければならないからだ。(実際のコードはGistに置いた。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
➜  stack  go test
--- FAIL: TestNewStack (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xb code=0x1 addr=0x20 pc=0x5b97f]

goroutine 5 [running]:
testing.func·006()
/usr/local/Cellar/go/1.4.1/libexec/src/testing/testing.go:441 +0x181
_/Users/xxx/D/prac/go/stack.TestNewStack(0x2082ac000)
/Users/xxx/D/prac/go/stack/main_test.go:9 +0x2f
testing.tRunner(0x2082ac000, 0x201f10)
/usr/local/Cellar/go/1.4.1/libexec/src/testing/testing.go:447 +0xbf
created by testing.RunTests
/usr/local/Cellar/go/1.4.1/libexec/src/testing/testing.go:555 +0xa8b

goroutine 1 [chan receive]:
testing.RunTests(0x1906c0, 0x201f10, 0x1, 0x1, 0xb80634fbdb741301)
/usr/local/Cellar/go/1.4.1/libexec/src/testing/testing.go:556 +0xad6
testing.(*M).Run(0x2082800a0, 0x20e380)
/usr/local/Cellar/go/1.4.1/libexec/src/testing/testing.go:485 +0x6c
main.main()
_/Users/xx/D/prac/go/stack/_test/_testmain.go:52 +0x1d5
exit status 2
FAIL  _/Users/xxx/D/prac/go/stack 0.079s

 recoverの動作とdeferの連携

recoverはこれまで使ったことなかったが、panicによるスタックのトレースを中断して、 panicが呼ばれたところに戻る関数らしい。 recover()は、事前にpanicが呼ばれていなければnilを返し、呼ばれていなければそうじゃないものを返す。

つまり、if r:=recover(); r != nilpanic判定を行い、そこでの動作を終えればpanicが呼ばれた場所に戻る

deferは、その関数のスタックを抜けるときに呼び出される関数を登録できる。 これらを合わせると、panicによるトレースダンプを中断することができる。

結果

以下が実際のコードだ。

1
2
3
4
5
6
7
8
9
10
11
func TestNewStack(t *testing.T) {
  defer func() {
    if r := recover(); r != nil {
      t.Error("New allocation for stack has failed")
    }
    }()
    var s Stack
    if s.Len() != 0 {
      t.Errorf("The length of stack shold be 0, not %d", s.Len())
    }
  }

すると、こうなる。

1
2
3
4
5
6
➜  stack  go test
--- FAIL: TestNewStack (0.00s)
main_test.go:10: New allocation for stack has failed
FAIL
exit status 1
FAIL  _/Users/xxx/D/prac/go/stack 0.072s

まとめ

recoverは使い方よくわからなかったが、テストだけでなく、panicによるスタックトレースを止めて、戻ることができる。ってことを書いておきたかったんだなぁ、きっと。

参考

Understanding defer, panic, and recover

Goプログラムのサイズを小さくする

Goで作られるプログラムサイズはそこそこ大きい。配布する場合はやはり小さい方が良い。 そんなときは、リンカの設定を利用するといいようだ。

1
go build -ldflags '-s -w'

Linuxならバイナリサイズが小さくなるはずだ。拙作、comstockでは8MiBから5.2MiBに縮んだ。

何をやっているか?

-ldflagsは、gccなどを使ったことある方はよくご存知だろうが、ldへの引数である。 ldとはgccが使うリンカであり、Goもリンカを持っている。Goのリンカは、例えば、go tool 6l(番号はアーキテクチャ依存)から呼び出せる。伝統的にリンカへのフラグは LDFLAGSであるので、goでは小文字で指定するようになってる。

go buildはリンカへの引数を-ldflagsの後で文字列として渡すことができる。

指定できるリンカフラグは様々で、標準ドキュメントよりもgo 1.4では数が増えているように見える。とりあえず、上で使ったのは以下の2つだ。

1
2
-s    disable symbol table
-w    disable DWARF generation

シンボルテーブルは、シンボルと、シンボルの位置(アドレス)が記述された表である。

1
2
➜  nm `which comstock-cli` | grep Log | grep Printf
000000000007d8c0 t log.(*Logger).Printf

例えば、上はLogger.Printf()の位置を示している。

このシンボルテーブルは外部から参照することがない限り、 不必要だ。 よって、-sを渡して削る。 ちなみに、ビルドしてるのがライブラリだったり、ビルド後に デバッガを使う場合は、シンボルテーブルは必要になるので、削ってはいけない。

DWARFというのは、Linuxの実行可能形式であるELFに付属する デバッグ情報である。 こちらも配布用のプログラムには必要無い。よって-wで削る。 こちらはデバッグしない限り、ライブラリでも削って良い。

darwinではELFではなくMach-Oという実行可能形式を用いるため、DWARFはそもそも生成されない。 シンボルテーブルは削れると思うが、私の環境では削れてなかった。 とりあえずLinuxで縮んだので良しとする。

参考

Command ld (Go document)

Golang application auto build versioning ※直接関係ないが、ldflag利用の一例。

Mac OSXで、IntelliJをJDK1.7で動かす

Goのコードがでかくなってきたので、リファクタの手間がかかる。昔から、リファクタはIDEと決めているので、 IntelliJでやってみた。

現時点の最新版は、IntelliJ14。この前出たばっかり。

しかし、まさかのJDK1.6を要求してくる。そういう時は、Info.plistを書き換えましょう。

1
2
cd /Application/IntelliJ\ IDEA\ 14\ CE.app/
emacs Info.plist

で、JVMVersionというところをみつけて、1.6*1.7*に書き換えたらオッケー。

1
2
  <key>JVMVersion</key>
  <string>1.7*</string>

参考

How Do I Run idea IntelliJ on Mac OSX with JDK 17, stackoverflow

VPNとDockerを併用する

VPNが導入されている環境で、Dockerを使いたい、という場合。 VPNは製品によるが、Linuxであればtun/tapをつかって実現されている ことも多いそうだ。とうわけでメモ。

問題

Dockerを使ってみよう。なんて素敵だ、こんなに簡単にLinuxが立ち上がるなんて。 あれ。yum updateができない。おかしいな、PROXYは設定してるのに。お、VPNを切ったら動くぞ。 いや、しかしVPNが使えないなら意味ないじゃないか。。。

という場合。

結論

私の環境では、tun/tapを利用したVPNは、クラスBのプライベートアドレスのすべてをtun0で 上書きしてた。ここはdocker0も使う。なので、docker0の設定を変えて、クラスCのアドレスを バインドした。

1. Dockerの動作範囲を確認

Dockerを起動して、netstat -rを打ってみる。

1
2
3
4
5
Destination     Gateway         Genmask         Flags MSS Window irtt iface
192.168.0.0     *               255.255.255.0   U     0      0    0   eth0
169.0.0.0       *               255.255.0.0     U     0      0    0   eth0
172.17.0.0      *               255.255.0.0     U     0      0    0 docker0
default         192.168.0.1     0.0.0.0         UG    0      0    0   eth0

docker0に割り当てられるのはデフォルトでは、172.17.0.0/255.255.0.0のようだ。

2. tun0の動作範囲を確認

VPNに接続してnetstat -rを打ってみる。

1
2
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
172.16.0.0      x.x.x.x     255.240.0.0     UG    1      0        0 tun0

関係がありそうなのは、172.16.0.0/255.240.0.0だ。

3. サブネットマスクから重複を確認

ぱっと見てわからない場合は、以下のようなサイトがある。

http://note.cman.jp/network/subnetmask.cgi

サブネットマスクを計算すると、被ってるかどうか確認できる。 実際、172.16.0.0/255.240.0.0172.16.0.0 ~ 172.31.255.255を示してる。クラスB全部だ。

4. 被ってたら--bipで再設定

上では、172.16.0.0172.16.255.255tun0に割り当てられてる。 ここは、Dockerのネットワークインタフェースであるdocker0が使う部分だ。 スーパーユーザで起動していれば起動順序によってはdockerで上書きされる かもしれないが、dockerユーザを使ってる場合がほとんどだろう。 このため、docker0が使っていないアドレス空間をブリッジすればよい。

dockerの設定は/etc/default/dockerにある。今回は以下のようにバインドする。

1
DOCKER_OPTIONS="--bip 192.168.51/24"

再起動は必要。

ちなみに、docker1などの別ブリッジを作ってもよい。ブリッジの作り方は、How Docker networks a container にも書いてあるため、割愛。

参考

How Docker networks a container

Customizing Docker0

TUN/TAPがまともに使えるようになるまで

route ~ ルーティングテーブルの表示・設定を行う(@IT)

Go Build -tagsを使ってRelease/Debugを切り替える

Goでウェブアプリのクライアントを書いている。 ローカルでテストする場合は、サーバもローカルにたてるので、アクセスする先はlocalhostになる。 リリースするときは正しいウェブアプリのURLを指定しなければならない。 この際、切り替えはナイーブにコメントアウトで行ってきた。

1
2
3
4
const (
  // APIServer = "http://www.mywebapp.com"
  APIServer = "http://localhost:8080"
  )

当然、デバッグ用を有効にしたままリリースしてしまったり、その逆だったりと、混乱があった。 このような、デバッグ・リリースで機能を切り替えたい場合はBuild constrainsを使えば良いらしい。

Build Constrainsとは?

従来の機能が充実しているGoに抜かりはない。Build constrainsとは必要に応じてビルドするファイルを切り替える、Goの機能だ。 公式ドキュメントに紹介されている例は、アーキテクチャ毎にビルドするソースを切り替える方法だ。

ビルドするファイルを切り替える方法は以下の3つ。

1. コメントで切り替える

1
// +build linux

このコメント行をファイルの先頭に書いておくと、GOOS=linuxの場合のみビルドされる。i386などのGOARCHもタグとして使うことが可能だ。 ちなみに、否定は以下のように記述する。

1
// +build !linux

2. ファイル名で切り替える

ファイル名でもBuild constrainsが行える。以下のファイルは、GOOS=windows, GOARCH=amd64の環境でしかビルドされない。 ファイル名からビルド対象がわかるため、開発者にも優しい。ここについては、後に補足を入れた。

1
source_windows_amd64.go

3. build -tagsで切り替える

これはCheney氏のブログをきっかけで知ったのだが、 +buildの後に付加されるシンボルはタグと呼ばれ、go build -tags tagAのようにオプションで指定できる(らしい)。 Build constrainsがBuild tagともよばれる所以か?これを使えば以下のようにコメントされたファイルは、先のコマンドからのみビルドされることになる。

1
// +build tagA

build -tagsを使ってRelease/Debugを切り替える

さて、上に挙げた3番目の手法を使えば、debug/releaseのビルドを切り替えられる。没頭にあげたように、テスト時はlocalhostを。 リリース時は正しいアドレスを使いたい場合、以下のように2つのファイルを記述すればよい。

release.go

1
2
3
4
5
// +build !debug

package main

const APIServer = "http://localhost:8080"

debug.go

1
2
3
4
5
// +build debug

package main

const APIServer = "http://www.mywebapp.com"

注意すべきは、1行目のコメントに続く3行目の間に空行が必要な点だ。

そして、debug時は以下のようにビルドを行う。

1
go build -tags debug

まとめ

  • Build constrainsを使えばファイル単位でビルドを制御することができる
  • go build -tags tagNameで好きなタグを指定できる
  • タグは// +buildで指定する。コメントの後の空行を忘れずに。

補足1

ちなみに、手法1, 2ともに適用する必要はなく、どちらかで良い。どちらか選ぶ場合は、ファイル名で切り替えを選べば、grepなどで簡単に ビルド対象のファイルを選別できるのでおすすめだそうだ。

補足2

デフォルトで用意されているbcは、GOOSGOARCH以外に、ignoreがある。

1
// +build ignore

と書かれたファイルはビルドされない。 個人的にもっとわかりやすくて、よく使うのは、以下のように_でファイル名を始めることだ。

1
_source.go

エラーが多いファイルなどをとりあえず無視したいときに良い。

参考

go/build from Go Document

Using //+build to swtich between debug and release builds by Dave Cheney

How to properly use build tags? from Stackoverflow

Jdbからbreakpointをいれる

リモートでJDB使うことは稀なのだが、たまにしかしないので忘れる為、メモ。 基本的には処理を一時的に止めて、問題を再現させたいときなどに使う。

Remote Debugger用のポートをオプションで指定して起動する

通常のプログラムの起動に、以下のオプションを追加する。 やっていることは、デバッグオプションで起動し、コネクト用のポートを指定している。

1
-Xdebug -Xrunjdwp:transport=dt_socket,address=12347,server=y,suspend=n

JDBから接続する

1
jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=12347

ブレークポイントの挿入

  • stop in SomeClass.someMethod : メソッドへ挿入
  • stop in SomeClass.someMethod(int) : オーバーロード時はシグネチャごと
  • stop in SomeClass.<init> : コンストラクタへ

以上。