技術備忘記

日々調べたこととかとりとめなく

go/typesでgo moduleを有効にするまでの旅路

はじめに

この記事はGo2 Advent Calendar 2021 23日目の記事です。

TL; DR;

go/types で go moduleを有効にするためには golang.org/x/tools/go/packages を使う。以上。

pkg.go.dev godoc.org

旅のはじまり

日頃仕事でDX(Developer Experienceの方)おじさんをやっている関係上、Code Generate ツールを書くことが結構ありまして、その際に go/types が便利なのでよく使っています。

AST は生で触ると結構大変ですが、このgo/typesパッケージを利用することで、あたかも reflection のような感覚で扱うことが出来ます。 例えば embedded は AST の鬼門ですが、go/types を使うと非常に楽ちんです。go/typesサイコー!

例えばこんなファイルがあったとして

sample/foo.go

package sample

type Foo struct {
    Bar
}

type Bar struct {
}

こんな感じで Foo の型情報にアクセスすることが出来ます。

func main() {
    set := token.NewFileSet()
    path := "path/to/sample/foo.go"
    pkgs, _ := parser.ParseDir(set, path, nil, parser.ParseComments)
    pkg, _ := pkgs["sample"]
    cfg := types.Config{
        Importer: importer.Default(),
    }
    afs := []*ast.File{}
    for _, f := range pkg.Files {
        afs = append(afs, f)
    }
    pkz, _ := cfg.Check("", set, afs, nil)
    obj := pkz.Scope().Lookup("Foo")

    tp := obj.Type().Underlying()
    fmt.Println(tp) // *types.Struct

    strct := tp.(*types.Struct)
    f := strct.Field(0)
    fmt.Println(f.Name(), f.Embedded()) // Bar true
}

そんなこんなで、go/types を使って Happy Hacking な毎日を送っていましたが、 とある問題に直面しました。

GOPATH モード非対応パッケージで躓く

日頃使っていて、どうやら go/types が 依存パッケージの参照を GOPATH を参照、 いわゆるGOPATHモードで動作していることには気づいていました。

2021年にもなってどうなのか、とは思っていましたが、基本利用シーンはツールですので、 深追いはせずに、適宜依存パッケージが見つからない旨のエラーが出たら

$ GO111MODULE=off go get -u xxxx

をして事なきを得ていました。しかしある日、事件は起きました。

$ GO111MODULE=off go get -u github.com/labstack/echo/v4
cannot find package "github.com/labstack/echo/v4" in any of:
        $GOROOT/src/github.com/labstack/echo/v4 (from $GOROOT)
        $GOPATH/src/github.com/labstack/echo/v4 (from $GOPATH)

ついに来てしまいました。 モジュールモードがデフォルトでONになって久しく、 昨今こういったGOPATHモード非対応のライブラリも増えてきています。

という訳で、仕方がないので重い腰を上げて go/types のコードを読むことにしました。

go/types Deep Dive

先程掲載したサンプルコードのこの部分。

   cfg := types.Config{
        Importer: importer.Default(),
    }

怪しいですね。ということで定義を見てみました。

go/types/api.go

// A Config specifies the configuration for type checking.
// The zero value for Config is a ready-to-use default configuration.
type Config struct {
~
    // An importer is used to import packages referred to from
    // import declarations.
    // If the installed importer implements ImporterFrom, the type
    // checker calls ImportFrom instead of Import.
    // The type checker reports an error if an importer is needed
    // but none was installed.
    Importer Importer
~
}

~

// An Importer resolves import paths to Packages.
//
// CAUTION: This interface does not support the import of locally
// vendored packages. See https://golang.org/s/go15vendor.
// If possible, external implementations should implement ImporterFrom.
type Importer interface {
    // Import returns the imported package for the given import path.
    // The semantics is like for ImporterFrom.ImportFrom except that
    // dir and mode are ignored (since they are not present).
    Import(path string) (*Package, error)
}

~

// An ImporterFrom resolves import paths to packages; it
// supports vendoring per https://golang.org/s/go15vendor.
// Use go/importer to obtain an ImporterFrom implementation.
type ImporterFrom interface {
    // Importer is present for backward-compatibility. Calling
    // Import(path) is the same as calling ImportFrom(path, "", 0);
    // i.e., locally vendored packages may not be found.
    // The types package does not call Import if an ImporterFrom
    // is present.
    Importer

    // ImportFrom returns the imported package for the given import
    // path when imported by a package file located in dir.
    // If the import failed, besides returning an error, ImportFrom
    // is encouraged to cache and return a package anyway, if one
    // was created. This will reduce package inconsistencies and
    // follow-on type checker errors due to the missing package.
    // The mode value must be 0; it is reserved for future use.
    // Two calls to ImportFrom with the same path and dir must
    // return the same package.
    ImportFrom(path, dir string, mode ImportMode) (*Package, error)
}

types.Config に設定された Importer インターフェースを使って依存パッケージを探しに行くことが読み取れました。 そして、更に ImporterFrom が実装されていればそっちを使うと。なるほど。

で、importer.Default() を見てみると、

go/importer/importer.go

// Default returns an Importer for the compiler that built the running binary.
// If available, the result implements types.ImporterFrom.
func Default() types.Importer {
    return For(runtime.Compiler, nil)
}

コメントが何やら含みをもたせていますが ImporterFrom を実装してる可能性がありそうです。  runtime.Compiler は長くなるので引用しませんがGoデフォルトのコンパイラを示す gc を返します。

go/importer/importer.go

func ForCompiler(fset *token.FileSet, compiler string, lookup Lookup) types.Importer {
    switch compiler {
    case "gc":
        return &gcimports{
            fset:     fset,
            packages: make(map[string]*types.Package),
            lookup:   lookup,
        }
~
}

どうやら、Importer を実装しているのはこの gcimports のようでした

go/importer/importer.go

type gcimports struct {
    fset     *token.FileSet
    packages map[string]*types.Package
    lookup   Lookup
}

func (m *gcimports) Import(path string) (*types.Package, error) {
    return m.ImportFrom(path, "" /* no vendoring */, 0)
}

func (m *gcimports) ImportFrom(path, srcDir string, mode types.ImportMode) (*types.Package, error) {
    if mode != 0 {
        panic("mode must be 0")
    }
    return gcimporter.Import(m.fset, m.packages, path, srcDir, m.lookup)
}

gcimporter は ImporterFrom を実装していました。 ということは、どうにかして srcDir を指定することが出来れば良さそうです。

// If the installed importer implements ImporterFrom, the type

// checker calls ImportFrom instead of Import.

こちらのコメントの実装部分を探します。

go/types/resolver.go

func (check *Checker) importPackage(at positioner, path, dir string) *Package {
~
    // no package yet => import it
    if path == "C" && (check.conf.FakeImportC || check.conf.go115UsesCgo) {
        imp = NewPackage("C", "C")
        imp.fake = true // package scope is not populated
        imp.cgo = check.conf.go115UsesCgo
    } else {
        // ordinary import
        var err error
        if importer := check.conf.Importer; importer == nil {
            err = fmt.Errorf("Config.Importer not installed")
        } else if importerFrom, ok := importer.(ImporterFrom); ok {
            imp, err = importerFrom.ImportFrom(path, dir, 0)
            if imp == nil && err == nil {
                err = fmt.Errorf("Config.Importer.ImportFrom(%s, %s, 0) returned nil but no error", path, dir)
            }
~
}

ありました。このpathがどこから来ているかというと・・ この辺は長くなるので割愛します。結論、

   pkz, _ := cfg.Check("", set, afs, nil)

冒頭のサンプルコードのこの部分。定義を見てみます

go/types/api.go

// Check type-checks a package and returns the resulting package object and
// the first error if any. Additionally, if info != nil, Check populates each
// of the non-nil maps in the Info struct.
//
// The package is marked as complete if no errors occurred, otherwise it is
// incomplete. See Config.Error for controlling behavior in the presence of
// errors.
//
// The package is specified by a list of *ast.Files and corresponding
// file set, and the package path the package is identified with.
// The clean path must not be empty or dot (".").
func (conf *Config) Check(path string, fset *token.FileSet, files []*ast.File, info *Info) (*Package, error) {
    pkg := NewPackage(path, "")
    return pkg, NewChecker(conf, fset, pkg, info).Files(files)
}

第一引数で空文字を渡していた path 、こちらに任意のパスを指定することで そちらを見てくれるようです。尚、その指定がない場合gcimportsの実装的には $GOROOT$GOPATH を参照、つまり GOPATHモードと同じ動きになるようでした。

一旦動作。しかし・・

pathの指定の仕方がわかった所で、次のような手順でやってみました

1 ツール上で必要なパッケージをimport

後述しますが、ツール自体がパッケージを必要としているのではなく、 Importerに知らせる場所(vendor/) に配置するためimportします。

import (
~

    _ "github.com/labstack/echo/v4" // ツールでは使用しないため blank identifier を指定
)

2 ツール上で go mod vendor を実行

$ go mod vendor
go: finding module for package github.com/labstack/echo/v4
go: found github.com/labstack/echo/v4 in github.com/labstack/echo/v4 v4.6.1

これで vendor/ 以下に依存パッケージがダウンロードされます

$ tree vendor/ -L 2
vendor/
├── github.com
│   ├── labstack
│   ├── mattn
│   └── valyala
├── golang.org
│   └── x
└── modules.txt

3 path にvendorのパスを知らる

   pkz, _ := dcg.Check("/path/to/tool/vendor", set, afs, nil)

こうして、めでたく動作しました。やった!! ・・とはなりませんでした。

解析するプログラムが依存しているだけで、本来ツールに不要な依存を生み出してしまいます。 それはあまりにもイケてません、こういったパッケージが増えるたびにvendor以下に追加していく? そもそも2021年にもなって (略)

というわけで、この方法はボツにしました。

Google先生に聞いて一瞬で解決

仕方がないのでGoogle先生の力を借りることにしました。 はい。すぐ見つかりました(半ギレ)

https://github.com/golang/go/issues/28328 https://github.com/golang/go/issues/27556#issuecomment-419468978 (上記issueから引用)

github.com github.com

旅のおわり

というわけで、回り道しましたが golang.org/x/tools/go/packages を使うことで、 めでたく go module を参照することができるようになりました。こんな感じです。

import (
~
    "golang.org/x/tools/go/packages"
    "golang.org/x/tools/imports"
)


func main() {
    set := token.NewFileSet()
    path := "path/to/sample/foo.go"
    pkgs, _ := parser.ParseDir(set, path, nil, parser.ParseComments)
    pkg, _ := pkgs["sample"]
    cfg := &packages.Config{
        Mode: packages.LoadAllSyntax,
    }
    ptns := []string{}
    for k := range pkg.Files {
        ptns = append(ptns, k)
    }
    pkzs, _ := packages.Load(cfg, ptns...)
    var pkz *types.Package
    for _, p := range pkzs {
        if p.Types.Name() == name {
            pkz = p.Types
            break
        }
    }
    obj := pkg.Scope().Lookup("Foo")
    tp := obj.Type().Underlying()
    fmt.Println(tp) // *types.Struct

    strct := tp.(*types.Struct)
    f := strct.Field(0)
    fmt.Println(f.Name(), f.Embedded()) // Bar true
}

最初からGoogle先生に聞けば5分で終わった話ではありますが、 久しぶりに標準パッケージをじっくり読んで、遠回りした分、学びもそれなりにあったの良しとします。

今回は以上です。それでは、良いお年を。

Written A Compiler In Go を読んだ

はじめに

この記事はGo2 Advent Calendar 2018 23日目の記事です。

タイトルの通りWritten A Compiler In Go (以下Compiler本)を読んで、とても面白かったので、その内容を紹介したいと思います。

compilerbook.com

Writing An Interpreter In Go

Compiler本の話をする前に、まずWriting An Interpretor In Go(以下Interpreter本)の説明をする必要があります。 なぜなら、Compiler本は実質Interpreter本の続編であり、そこに書かれている内容を理解していることが前提となっているためです。

interpreterbook.com

Interpreter本(Compiler本でも同様、後述)では、Monkey というjavascriptRubyっぽい独自言語(作者はC言語風 と称しています)に関して

Source Code > (Lexer & Parser) > AST > (Evaluator) > Print(REPL)

これらの流れをGoで実装し、REPLという形で、Goのランタイム上で実行できるようにしていきます。 Parserを独自に実装するなど色々興味深い点はあるのですが、今回の主題はInterpreter本ではなくCompiler本ですので深くは触れません。 Interpreter本に関しては他にも書評を書いている方がいらっしゃいますのご紹介しておきます。 よろしければ併せてご覧ください。

https://deeeet.com/writing/2017/01/12/go-interpreter/deeeet.com

medium.com

razokulover.hateblo.jp

そうしてなんやかんやあって、 最終的には、たとえばこんな感じのことがREPLを通して実行できるようになります。

let foo = "bar";
puts(foo);

>> bar

let myArray = [1, 2, 3, 4, 5];

let fibonacci = fn(x) {
  if (x == 0) {
    0
  } else {
    if (x == 1) {
      return 1;
    } else {
      fibonacci(x - 1) + fibonacci(x - 2);
    }
  }
};

fibonacci(myArray[4]);

>> 5

third party libraryは一切使いません。200ページちょっとの内容でここまでのものができる のって、 結構すごくないでしょうか。

Writing An Compiler In Go

さて、ようやく本題です。 Compiler本において見た目上できること(REPL)は同じだったりするのですが、 Interpreter本とは当然そのアプローチ、アーキテクチャが異なります。

具体的には、ASTをそのまま即Evaluateするのではなく、 Compilerを通して Bytecodeに変換し、Virtual machine (VM) 上で実行(Execute)します。 ・・というのをInterpreter本と同じくGoのランタイム上で行います。

Source Code > (Lexer & Parser) > AST > (Compiler) > Bytecode > (VM) > Print(REPL)

実装に入る前に、そもそも Compiler、VM(Process Virtual MaicheCode)とはなにか? なぜVMか?なぜ独自Bytecodeを使用するか?など、きちんと説明してくれています。

例えばVMを導入することに関して、要はまあ学習コスパが一番良いってことになると思うのですが、 その辺を懇切丁寧に説明してくれており、もやもやしながらとりあえず読み進める、ということはありません。

最終系がREPL上で一通り動作するまでにいくつかの技術スタックが登場しますが、 Compiler、VMに関して、それぞれをどのように実装していくかをご紹介したいと思います。

Stack Machine

本著のVMの計算モデルではスタックマシンを採用しています。レジスタマシンに比べてシンプルで学習しやすい、 というのがその理由です。

まずCompilerにおいては、以下のような構造体を擬似的なIR(中間表現)として実装します (以下、コードスニペットは説明の為局所化・簡略化します。完全な内容を知りたい方は本著を御覧ください)

type Compiler struct {
    constants    []Object
    instructions []Instruction
}

constants は、JVMなどでも採用されているconstant poolです。 instructions には独自の命令コードである Instruction (Opecode: stackへのpushやpop、四則演算など 1byte + オペランド: constant poolのアドレス 0-2byte)のリストが入ります 命令コードにはオペランドが存在しないものもあるため、オペランドのサイズは可変長となっています。

ざっくり言ってしまうと、この構造体がASTを処理して、constantsやinstructionsへと変換していく、 という流れとなります。

func (c *Compiler) Compile(node ast.Node) error {
    switch node := node.(type) {
    case Integer:
        i := Integer{Value: node.Value}
        c.emit(OpConstant, i)
    case Add:
        c.emit(code.OpAdd)
    }

雰囲気伝わりましたでしょうか・・? 続いて、VMです

type VM struct {
    constants    []Object
    instructions []Instruction
    stack        []Object
}

VMでは、Compilerが出力したBytecodeを入力としてconstantsやinstructionsを受け取り、 stackに積みつつ fetch-decode-execute cycle のloop処理を実行していきます。

func (vm *VM) Run() error {
    for ip := 0; ip < len(vm.instructions); ip++ {
        op := Opcode(vm.instructions[ip])
        switch op {
        case code.OpConstant:
            constIndex := ReadUint16(vm.instructions[ip+1:])
            vm.push(vm.constants[constIndex])
        case code.OpAdd:
            left := vm.Pop()
            right := vm.Pop()
            result := add(left, right)
            vm.push(result)
        }

大分簡略化しましたが、スタックマシンを実装する上でのCompilerとVMの動きをご紹介しました。

Conditionals

各命令をシーケンシャルに実行するスタックマシン上で、 Ifなど条件分岐(Conditionals)は、どのように実現するのでしょうか。

JUMP命令 を用いて、特定の命令を呼び飛ばすことで実現します。 以下はif-elseを含むInstructionsのイメージ図です。

f:id:junchang1031:20181219153735p:plain

  • OpJumpNotTruthy 直前の命令の結果がFALSEなら所定の位置までの命令を読み飛ばす

  • OpJump 無条件で所定の位置までの命令を読み飛ばす

Keeping Track of Names

letキーワードを使っての変数束縛や、変数の値の取り出しには OpSetOpGet 命令を使用します。 さて、CompilerやVMはこれらの命令に関して、何を行うのでしょうか。

まず、Compilerにおいては symbol table を使って変数の管理を行います。

type Symbol struct {
    Name  string
    Index int
}

type SymbolTable struct {
    Store          map[string]Symbol
    NumDefinitions int
}

Symbol は自身を示す NameSymbolTable 内の Index を保持します。 SymbolTable はSymbolを名前で引くためのmapと、Symbolの総数を保持します。

あれ、変数自体の値は?と思うかもしれません。値は式を評価した結果ですので Compile時点では決定していません。(もちろん自明なものはありますが) したがってSymbolは値を持たないのです。

こちらを前述のOpSetなどの命令で使用します。 雰囲気だけですが以下のような感じです。

func (c *Compiler) Compile(node ast.Node) error {
    switch node := node.(type) {
    ・
    ・
    case LetStatement:
        symbol := c.symbolTable.Define(node.Name)
        c.emit(OpSetGlobal, symbol.Index)

    case Identifier:
        symbol := c.symbolTable.Resolve(node.Value)
        c.emit(code.OpGetGlobal, symbol.Index)
    }

続いてVMです。VMにおいては変数名にはもはや関心が無く、必要なのはその定義された 順番(Index)のみです。 VMでは、Compilerで定義されたSymbol.Index(がOpSetなどのオペランドに入っている)を使って変数をリストで管理します。

type VM struct {
    ・
    ・
    variables []Object
}

評価、実行はこのような感じで行います。

func (vm *VM) Run() error {
    for ip := 0; ip < len(vm.instructions); ip++ {
        op := Opcode(vm.instructions[ip])
        switch op {
        ・
        ・
        case OpSet:
            variableIndex := ReadUint16(vm.instructions[ip+1:])
            vm.variables[variableIndex] = vm.pop()

        case OpGet:
            variableIndex := ReadUint16(vm.instructions[ip+1:])
            vm.push(vm.variables[variableIndex])
        }

なお本著では、最終的には上記に加えてスコープ(Global/local)の概念を取り入れ、 スコープごとに変数を管理するようになります。 その際には、もっとも内側のスコープから順に探索し、見つかった最初の値を返す、という動きになります。

f:id:junchang1031:20181219181616p:plain

let foo = 1;
let bar = 10;
let buzz = 100;

let innerOne = fn() {
    let foo = 2;
    let bar = 20;
    let innnerTwo = fn() {
        let foo = 3;
        puts(foo);
        puts(bar);
        puts(buzz);
    }
    innnerTwo();
}

innerOne();

>> 3
>> 20
>> 100

Others

FunctionやClousureをどう実装するか? stack frame とは?などなど、他にも技術的な学びはたくさんあるのですが、疲れたので今回はこの辺にしておきたいと思います。 ここから先は君自身の目で確かめてくれ!!

おわりに

文中で作者が

私が欲しかったのは、900ページにも及ぶコンパイラについての書籍と、50行のRubyコードでLispインタプリタを実装する方法に関するブログ記事との間にあるものだ

と述べていますが、Goをある程度読み書きできて、かつ同じ想いを持っている方(私もそうでした)にはぜひ読んでみることをオススメします。

また、少し本題からは外れますが、Compiler本は2018年12月現在、英語版しかありません。 しかし、日本語版が出ているInterpreter本の続編ということもあり、そちらの内容を理解していればそれほど苦もなく読み進めることが できましたので、原著を読む練習としても良さそうです。

Interpreter本とCompiler本、合わせても500ページちょっとくらいですし、 年末年始のお供にいかがでしょうか。以上です。

Docker network周りでハマった話

何をしたか

Docker Swarm + Composeな構成を、VPNなどネットワーク的な制約が幾つかある環境に構築しました。 その際にいくつかハマり、学びがあったので記事にしたいと思います。

バージョンなど

ホストOS

cat /etc/redhat-release 
CentOS Linux release 7.2.1511 (Core) 

docker

docker version
Client:
 Version:      1.11.0
 API version:  1.23
 Go version:   go1.5.4
 Git commit:   4dc5990
 Built:        Wed Apr 13 18:40:36 2016
 OS/Arch:      linux/amd64

Server:
 Version:      1.11.0
 API version:  1.23
 Go version:   go1.5.4
 Git commit:   4dc5990
 Built:        Wed Apr 13 18:40:36 2016
 OS/Arch:      linux/amd64

docker-compose

docker-compose -v
docker-compose version 1.7.0, build 0d7bf73

ハマったこと

  • ネットワークの作成時にsubnetを指定しない時の挙動
  • Docker Composeが隠蔽しているネットワークの作成
  • docker_gwbridgeの利用するネットワーク

よくよく公式ドキュメントを読むと、普通に書いてあることがほとんどですが、きちんと読まず適宜ググりながら作業してしまったため、色々な情報に振り回され結果解決に時間がかかってしまいました。反省。

それぞれ少し補足していきます。

ネットワークの作成時にsubnetを指定しない時の挙動

Docker v1.9から、Dockerが通常使うネットワークのセグメントとは別に、ユーザー独自のネットワークを定義できるようになりました。 おそらく日頃Dockerを触っている方はほとんどご存知だと思いますし、私も存在は知っていました。

ただし、私が構築するシステムでは特に独自ネットワークは必要ないと思い、あまり詳しく追っていませんでした。 (これは大きな間違いでした。後ほど触れます)

で、subnetを指定しない時どうなるか。結論を言ってしまうと、

Dockerがデフォルトで使用するネットワークとは別の、Docker側で用意されているネットワーク領域のうち一つが自動で適用されます。

Dockerが使用するネットワークであるdocker0(デフォルト172.17.0.0/16)が、私の環境では内部LANと競合するのはわかっていたので、予め別のネットワークを割り当てていました。 しかし、自分が予期しないところで作成していたbridgeネットワークがこのルールに適用されたため、 sshが突如繋がらなくなるなど予期しない挙動に苦しまされました。

ちなみに、公式ドキュメントにはこの旨しっかりと記載があります。

Note : It is highly recommended to use the --subnet option when creating a network. If the --subnet is not specified, the docker daemon automatically chooses and assigns a subnet for the network and it could overlap with another subnet in your infrastructure that is not managed by docker. Such overlaps can cause connectivity issues or failures when containers are connected to that network.

せっかくなのでDockerのコードちょっとだけ見ました。

docker/libnetwork/ipamutils/utils.go

var (
    // PredefinedBroadNetworks contains a list of 31 IPv4 private networks with host size 16 and 12
    // (172.17-31.x.x/16, 192.168.x.x/20) which do not overlap with the networks in `PredefinedGranularNetworks`
    PredefinedBroadNetworks []*net.IPNet
    // PredefinedGranularNetworks contains a list of 64K IPv4 private networks with host size 8
    // (10.x.x.x/24) which do not overlap with the networks in `PredefinedBroadNetworks`
    PredefinedGranularNetworks []*net.IPNet

    initNetworksOnce sync.Once
)    

この辺が使われているようです。

Docker Composeが隠蔽しているネットワークの作成

Docker Compose、便利ですよね。 docker-compose up で開発環境を一発で立ち上げたり、docker-compose scale では ホストをまたいだSwarm cluster内でのコンテナの立ち上げを、非常に簡単に行うことができます。

ただし、便利、簡単ゆえにですが、裏側でのネットワークの作成を開発環境等では意識しておらず、前述の環境下でハマりました。

Docker Compose で Swarmクラスタ上にコンテナの立ち上げを行う際には、以下のネットワークが必要になります。

  • {サービス名}_default

    • overlay ネットワーク。swarm manager、nodeを横断して一つ作られる
    • 名前は指定なしの場合。別のネットワークを設定可能。
  • docker_gwbridge

    • bridge ネットワーク。swarm nodeで一つずつ作成される
    • overlay ネットワーク上に所属するコンテナから外部アクセスする際に使用する
    • 複数overlayネットワーク間で共通してこの一つのネットワークが利用される

先ほど大きな間違いだったと述べたのはまさにここで、 Dockerのクラスタを組む、イコール独自ネットワークを作るということに他ならないのです。

何が問題だったかというと、

自動作成される

これらのネットワークは、存在しない場合は自動で作成されます。 便利なのですが、逆にこのネットワークが問題になった時にとてもわかりにくいです。(知ってしまえば簡単ですが)

docker_gwbridgeの利用するネットワーク

docker_gwbridgeはbridgeネットワークなので、ホスト側の設定として反映されます。 存在しない場合デフォルトで作成され、かつ使用するネットワークがDockerに自動で決められるため、環境によっては前述のとおり非常に困ることになります。

言ってしまえば、今回私が苦しんだ原因の9割がこの動きのせいでした・・

docker_gwbridgeのsubnet指定

ところで、docker_gwbridgeは自動で作られてしまうし、subnetはどうやって指定すれば良いのでしょうか。 これはドキュメントにも載っておらず、最後の悩みとなりました。

で、また調べてみると、docker compose scale する前に事前に作っておけとのこと。

github.com

docker network create --opt com.docker.network.bridge.enable_icc=false --subnet={your prefered subnet} docker_gwbridge

※ コメントでは --opts とありましたがtypoだと思われます。

なんともwork aroundな解ですが、overlay networkの仕組みはまだ流動的なようなので、一旦これで良しとしました。

結論

なんだかんだで、公式ドキュメントにしっかりと目を通すことが大事です。 stackoverfl○wやQ○itaに頼りすぎると痛い目を見ます。いやホント・・

GoでORM利用の際のオーバーヘッドについて

はじめに

GoのORM的なライブラリには幾つか種類がありますが、例えばgorpで あるテーブルからレコードを取得するのは以下の流れとなります。

1)以下のようなテーブルと対になる構造体を定義

type TestStruct struct {
    ID    int            `db:"id"`
    Col1  sql.NullString `db:"col1"`
    Col2  sql.NullString `db:"col2"`
    Col3  sql.NullString `db:"col3"`
    Col4  sql.NullString `db:"col4"`
    Col5  sql.NullString `db:"col5"`
    Col6  sql.NullString `db:"col6"`
    Col7  sql.NullString `db:"col7"`
    Col8  sql.NullString `db:"col8"`
    Col9  sql.NullString `db:"col9"`
    Col10 sql.NullString `db:"col10"`
}

(2)テーブルとのマッピングをライブラリに知らせる

dbMap = &gorp.DbMap{
    Db:      db,
    Dialect: gorp.MySQLDialect{"InnoDB", "UTF8"},
}
dbMap.AddTableWithName(TestBinder{}, "test").SetKeys(true, "ID")

(3) 構造体(のポインタ)をinterface{}で受ける。そこにselectの結果をセット

tbl := TestStruct{}
dbMap.Get(&tbl, 1)

他のメジャーどころな物も、大枠一緒な印象です。 ところで、これらの一連の処理は、要件やテーブル構造によっては、パフォーマンスに問題を抱える可能性があります。 原因は、reflection の利用です。

gorpではreflectionを使って、以下の処理を行っています。

  • (2)でinterfaceから構造体の情報を取り出し、fieldのタグ情報からテーブルのカラムとのマッピング
  • (3)でinterfaceから構造体の情報を取り出し、その構造体field一つずつに値のセット

このreflectionの処理がどれくらいのオーバーヘッドになるのか、計測してみました。

また、gorpでは

  • (2)をselectを発行する前にあらかじめセットする方法 (以降 gorp-1)
  • (2)と(3)とを、select時に同時に行う方法 (gorp-2)

があります。後者の方法をとった場合、reflectionの回数が増えるため、よりオーバーヘッドが顕著になることが予想されます。 そちらも合わせてベンチマークを取ってみたいと思います。

ベンチマーク

テストコード

以下3つのテストを用意しました。

  1. BenchmarkFindByID がgorpを使わないテスト
  2. BenchmarkGorpFindByID がgorpを使ったテストで上記gorp-1に該当
  3. BenchmarkGorpFindByIDDynamicMappingがgorpを使ったテストで上記gorp-2に該当
package main

import (
    "database/sql"
    "fmt"
    "os"
    "testing"

    "github.com/go-gorp/gorp"
    _ "github.com/go-sql-driver/mysql"
)

var db *sql.DB
var dbMap *gorp.DbMap

type TestStruct struct {
    ID    int            `db:"id"`
    Col1  sql.NullString `db:"col1"`
    Col2  sql.NullString `db:"col2"`
    Col3  sql.NullString `db:"col3"`
    Col4  sql.NullString `db:"col4"`
    Col5  sql.NullString `db:"col5"`
    Col6  sql.NullString `db:"col6"`
    Col7  sql.NullString `db:"col7"`
    Col8  sql.NullString `db:"col8"`
    Col9  sql.NullString `db:"col9"`
    Col10 sql.NullString `db:"col10"`
}

func init() {
    var err error

    // initialize sql.DB
    host := os.Getenv("MYSQL_HOST")
    database := os.Getenv("MYSQL_DATABASE")
    usr := os.Getenv("MYSQL_USER")
    pass := os.Getenv("MYSQL_PASSWORD")

    if db, err = sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s", usr, pass, host, database)); err != nil {
        panic(err)
    }

    // initialize table
    db.Exec(`CREATE TABLE IF NOT EXISTS test(
              id   INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
              col1 VARCHAR(100),
              col2 VARCHAR(100),
              col3 VARCHAR(100),
              col4 VARCHAR(100),
              col5 VARCHAR(100),
              col6 VARCHAR(100),
              col7 VARCHAR(100),
              col8 VARCHAR(100),
              col9 VARCHAR(100),
              col10 VARCHAR(100)
  )`)
    db.Exec("TRUNCATE TABLE test")
    db.Exec(`INSERT INTO test VALUES(
              1,
              '1',
              '2',
              '3',
              '4',
              '5',
              '6',
              '7',
              '8',
              '9',
              '10'
  )`)

    // initialize gorp.DbMap
    dbMap = &gorp.DbMap{
        Db:      db,
        Dialect: gorp.MySQLDialect{"InnoDB", "UTF8"},
    }
    dbMap.AddTableWithName(TestStruct{}, "test").SetKeys(true, "ID")
}

func BenchmarkFindByID(b *testing.B) {
    tb := TestStruct{}
    for i := 0; i < b.N; i++ {
        rows, err := db.Query("SELECT * FROM test WHERE id = ?", 1)
        if err != nil {
            panic(err)
        }
        if !rows.Next() {
            panic(sql.ErrNoRows)
        }
        err = rows.Scan(&tb.ID,
            &tb.Col1,
            &tb.Col2,
            &tb.Col3,
            &tb.Col4,
            &tb.Col5,
            &tb.Col6,
            &tb.Col7,
            &tb.Col8,
            &tb.Col9,
            &tb.Col10)

        if err != nil {
            panic(err)
        }
        rows.Close()
    }
}

func BenchmarkGorpFindByID(b *testing.B) {
    tb := TestStruct{}
    for i := 0; i < b.N; i++ {
        if _, err := dbMap.Get(&tb, 1); err != nil {
            panic(err)
        }
    }
}

func BenchmarkGorpFindByIDDynamicMapping(b *testing.B) {
    tb := TestStruct{}
    for i := 0; i < b.N; i++ {
        if err := dbMap.SelectOne(&tb, "SELECT * FROM test WHERE id = ?", 1); err != nil {
            panic(err)
        }
    }
}

カラム数 11個で測定

go test -bench . -benchmem
PASS
BenchmarkFindByID-5                        10000        198918 ns/op        1776 B/op         46 allocs/op
BenchmarkGorpFindByID-5                    10000        206618 ns/op        2432 B/op         61 allocs/op
BenchmarkGorpFindByIDDynamicMapping-5       5000        276175 ns/op       13104 B/op        592 allocs/op

1 > 2 > 3 の結果になっています。予想通りですね。 カラム数を増やすとどうなるでしょうか。

カラム数 101個で測定

go test -bench . -benchmem
PASS
BenchmarkFindByID-5                 5000        355918 ns/op       14161 B/op        316 allocs/op
BenchmarkGorpFindByID-5                     3000        434415 ns/op       20049 B/op        421 allocs/op
BenchmarkGorpFindByIDDynamicMapping-5        100      11087651 ns/op      699789 B/op      41632 allocs/op

順番は一緒ですが、その差がより顕著になっています。 特に3の爆発っぷりがすごいですね。ここまで差が出るとは正直予想外でした。

終わりに

この結果を受けてORMなんて使っちゃダメだ!などと結論付ける気は毛頭ありません。 ただし、要件によってはこのオーバーヘッドが無視できないケースは確実にあります。

先日あるバッチプログラムの速度を10倍に改善しなくてはならないという 問題に直面しました。

その際に色々対応はしたのですがどうしても一定以上の速度が出ず悩み、 仕方がないので今回のテストコードのように自前でmappingするコードに変えたところ あっさり目標を達成することができました。 (今回の記事を書くきっかけとなりました)

とはいえ、カラム数が増えるとこれを手で書くのは辛いし、コピペミスなどバグの温床にもなるので、 構造体からmappingの処理をいい感じにgo generateしてくれるツールでもそのうち作ろうかなぁと思ってます。

2016.06.15 追記 ツール作りました。よかったら使ってみてください。

GitHub - Jun-Chang/gdbinder: gdbinder is light-weight data mapper for Golang

今回は以上です。

Docker Compose restart の挙動

Docker (Compose) の 自動再起動について

ホストOSを起動したタイミングであるアプリケーションを自動で立ち上げたい、 あるいは何らかの問題で落ちた時に、自動で再起動して欲しい、というニーズは何処にでもあるかと思います。

今回は Docker Compose (以下単純にComposeと記します)に関して、コンテナの再起動や、他コンテナとの依存関係が設定されている場合に どのような挙動をとるのかを調べてみました。

なお、今回検証に使用した Compose バージョンは 1.6.0 です。

restart policy

Docker 及び Compose では、 run/upの restart policy の設定することにより、 コンテナが停止した際の再起動にまつわる設置を行うことができます。

オプション 意味
no 再起動しない (デフォルト)
on-failure[:max-retries] プロセスが 0 以外のステータスで終了した場合、 最大:max_retries の分だけ再起動を行う
always 明示的に stop がされない限り、終了ステータスに関係なく常に再起動が行われる
unless-stopped 最後にdocker daemon が起動していた際に ステータスが終了状態だった場合は再起動しない。それ以外はalwaysと同じ。

ちなみに、起動したもののすぐに(10秒。今の所固定っぽいです)終了してしまう場合は 起動回数に応じて遅延処理(100ms => 200 => 400..)が入るようになっています。 fluentd のリトライでも使われている、いわゆる Exponential Backoff アルゴリズムですね。

Compose の 「依存関係、起動順」 と 「再起動」の関係

依存関係、コンテナの起動順

Compose内のコンテナの依存関係、起動順を決定する要因には以下があります。

  • depends_on
  • links
  • volumes_from
  • network_mode

再起動との関連は?

「依存関係、起動順」 と 「再起動」にまつわる設定を両方行った場合、 どういった挙動になるのか気になったので試してみました。

version: "2"

services:
    elasticsearch:
        image: elasticsearch
        ports:
            - 9200:9200
        restart: always
    kibana:
        image: kibana
        ports:
            - 5601:5601
        depends_on:
            - "elasticsearch"
$ docker-compose up -d
Starting composerestarttest_elasticsearch_1
Starting composerestarttest_kibana_1
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                              NAMES
d9b3f1c7796b        kibana              "/docker-entrypoint.s"   2 minutes ago       Up 3 seconds        0.0.0.0:5601->5601/tcp             composerestarttest_kibana_1
3e493eaf0fb7        elasticsearch       "/docker-entrypoint.s"   2 minutes ago       Up 3 seconds        0.0.0.0:9200->9200/tcp, 9300/tcp   composerestarttest_elasticsearch_1

上記の設定では kibana サービスが elasticsearch サービスに依存しています。 この状態で elasticsearch が停止/再起動した場合、従属するkibnaコンテナはどのような動きになるでしょうか。

docker exec -i -t composerestarttest_elasticsearch_1 kill 1

具体的に確認したかった内容とその結果を以下に示します。

Q. 従属 コンテナのプロセスはそのまま? それとも再起動される?

従属コンテナであるkibanaはそのままでした。

docker ps                                                  
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                              NAMES
d9b3f1c7796b        kibana              "/docker-entrypoint.s"   3 minutes ago       Up About a minute   0.0.0.0:5601->5601/tcp             composerestarttest_kibana_1
3e493eaf0fb7        elasticsearch       "/docker-entrypoint.s"   3 minutes ago       Up 28 seconds       0.0.0.0:9200->9200/tcp, 9300/tcp   composerestarttest_elasticsearch_1

restartのオプションを幾つか試してみましたが挙動は同じでした。 依存関係の設定と再起動のそれに相関はなさそうです。

Q. コンテナ間の接続は再開される?

docker exec -i -t composerestarttest_kibana_1  ping elasticsearch
PING elasticsearch (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.073 ms
64 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.092 ms
64 bytes from 172.18.0.2: icmp_seq=2 ttl=64 time=0.089 ms
64 bytes from 172.18.0.2: icmp_seq=3 ttl=64 time=0.107 ms
64 bytes from 172.18.0.2: icmp_seq=4 ttl=64 time=0.111 ms

再起動後も接続は問題なさそうです。

まとめ

上記以外にも docker inspectの内容など見てみましたが、 バージョン 1.6.0 時点では、 Compose の restart の挙動は Docker単体と特に変わりはなく、 その他依存関係や起動順などの設定とは無関係に動作するようでした。

個人的な予想ですが、おそらくバージョンUPを重ねてもこの動作に変更はないように思えます。 そもそもDocker、あるいはコンテナというものが以下の思想で作られているからです。

  • 状態や複雑な依存関係は極力持たず、各サービス間が疎結合であるべき (Microservices)
  • アプリケーションは壊れるものであり、修復・維持を考えるより新しく作り直す方が良い (Imuutable Infrastracture)
  • 依存関係を管理せざるをえないのであれば別途Consulなど専用のサービスを使う(Service Discovery)

というわけで今回は以上です。

Goでアルゴリズムのお勉強

珠玉のプログラミングを読んだ

GWには普段はやらないことをやろうと思い、数年前に流し読みだけして積んであった、珠玉のプログラミングという本を読みました。 www.amazon.co.jp

かなり古い本なのですが、富豪的プログラミングに慣れてしまっている自分にとっては、アルゴリズムの大切さや、パフォーマンス・チューニングの勘所を再認識する、とても良い機会となりました。

今回は本文中の擬似コードをGoで実装し、その効果を確認してみたいと思います。

お題

n要素のint配列xを入力とし、配列xの連続した要素でその和が最大になるものをみつけ、
その和を出力とする。

例えば以下の様な配列があった時、最大となるのはx[2..6]の和で187です。

|31|-41|59|26|-53|58|97|-93|-23|84|

これをいくつかのアルゴリズムで実装します。 事前に、各処理から呼び出す max を定義しておきます。

func max(comp ...int) int {
    i := 0
    for _, c := range comp {
        if c > i {
            i = c
        }
    }
    return i
}

実装

O(n³) な実装

一番シンプルに実装すると

0 ≦ i ≦ j < nを満たす全てのi, j のペアについて x[i..j] の和を計算し、 それらのうち最大のものを返却する、と言った感じになるかと思います。

具体的な実装は以下です。計算量は O(n³) です。

func cubed(list []int) int {
    maxsofar := 0
    len := len(list)
    for i := 0; i < len; i++ {
        for j := i; j < len; j++ {
            sum := 0
            for k := i; k <= j; k++ {
                sum += list[k]
                maxsofar = max(maxsofar, sum)
            }
        }
    }
    return maxsofar
}

O(n²) な実装

上記アルゴリズムを少し改良します。 x[i..j] の和は、その前までの x[i..j - 1] の和にx[j]を加えるだけなので、 最後のループは必要無さそうです。という訳で削ります。

これで計算量は O(n²) となりました。

func squared(list []int) int {
    maxsofar := 0
    len := len(list)
    for i := 0; i < len; i++ {
        sum := 0
        for j := i; j < len; j++ {
            sum += list[j]
            maxsofar = max(maxsofar, sum)
        }
    }
    return maxsofar
}

O(n log n) な実装

アルゴリズムの定番、二分木 x リニアサーチな実装にして、計算量を O(n log n)にします。 配列を abに分割し、それぞれの計算を再帰的に行います。 注意点としては ab両方にまたがる領域(ここでは c と呼びます)の考慮が必要となるところです。 cを計算するときには、分割する左側の配列は、aの最後の要素を含む配列の中で、要素の和が最大となるものを使用します。

func nlogn(list []int) int {
    return maxsum3(0, len(list)-1, list)
}

func maxsum3(l, u int, list []int) int {
    if l > u {
        return 0
    }
    if l == u {
        return max(0, list[l])
    }
    m := (l + u) / 2
    lmax, sum := 0, 0
    for i := m; i >= l; i-- {
        sum += list[i]
        lmax = max(lmax, sum)
    }

    rmax, sum := 0, 0
    for i := m + 1; i <= u; i++ {
        sum += list[i]
        rmax = max(rmax, sum)
    }

    return max(lmax + rmax, maxsum3(l, m, list), maxsum3(m + 1, u, list))
}

O(n) な実装

個人的には目からウロコでしたが、これを線形(O(n))で実装する方法があります。 配列の開始位置x[0]から終端のx[n-1]まで、つねにそこまでの配列の和の最大値を記録しながら走査する アルゴリズムです。

func linear(list []int) int {
    len := len(list)
    maxsofar := 0
    maxend := 0
    for i := 0; i < len; i++ {
        maxend = max(maxend + list[i], 0)
        maxsofar = max(maxsofar, maxend)
    }

    return maxsofar
}

シンプルかつ、非常に強力な実装ですね!

確認

テストコード

var list []int

func init() {
    var i int
    flag.IntVar(&i, "num", 0, "num")
    flag.Parse()
    fmt.Println("list num =", i)

    rand.Seed(time.Now().UnixNano())
    list = rand.Perm(i)
}

func BenchmarkCubed(b *testing.B) {
    for i := 0; i < b.N; i++ {
        cubed(list)
    }
}

func BenchmarkSquared(b *testing.B) {
    for i := 0; i < b.N; i++ {
        squared(list)
    }
}

func BenchmarkNlogn(b *testing.B) {
    for i := 0; i < b.N; i++ {
        nlogn(list)
    }
}

func BenchmarkLinear(b *testing.B) {
    for i := 0; i < b.N; i++ {
        linear(list)
    }
}
$ go test -bench . -num=10
list num = 10
BenchmarkCubed-5         1000000              1093 ns/op
BenchmarkSquared-5       5000000               256 ns/op
BenchmarkNlogn-5         5000000               309 ns/op
BenchmarkLinear-5       20000000                83.3 ns/op
$ go test -bench . -num=100
list num = 100
BenchmarkCubed-5            2000            838238 ns/op
BenchmarkSquared-5        100000             22884 ns/op
BenchmarkNlogn-5          300000              4729 ns/op
BenchmarkLinear-5        2000000               834 ns/op
$ go test -bench . -num=1000
list num = 1000
BenchmarkCubed-5               2         775887991 ns/op
BenchmarkSquared-5          1000           2182157 ns/op
BenchmarkNlogn-5           20000             64348 ns/op
BenchmarkLinear-5         200000              8250 ns/op

期待通りの動作をしているようです。

最後に

O(n log n) の実装までは割りとスムーズに理解できましたが、最後の O(n) のそれは、理解するのに少し時間がかかりました。

ことWeb開発で言うと、パフォーマンスのボトルネックはネットワークやディスクI/O等になることが多く、 CPUやMemoryマターな問題に対して、アルゴリズムを駆使して解決する、みたいなケースはあまり多くないかもしれません。

しかし、エンジニアと名乗るからにはこの辺のスキルもきっちり抑えて置いたほうがよいなあと改めて思いました。 日々是学習ですね (^^;)

MacにDocker Swarmクラスタを構築 ①

Docker Swarm

最近Dockerを本番環境(オンプレミス)への導入を検討しています。となると、オーケストレーションどうしようかという話になると思いますが、 選択肢がいろいろあってどうしようかなーと思っていました。

  • Kubernetes
  • Docker Swarm
  • Mesos
  • Nomad

Nomad がだいぶ良さそうでしたが、いかんせん日本語の情報、導入事例などが少なかったので、まず今回はとりあえずはお手軽そうな Docker Swarm を候補として触ってみました。尚、Docker Swarmの特徴は以下です。

構築

MacなのでDocker-machineを使います。 Docker for Mac/Windows をお使いの方は適宜読み解いて下さい。 (執筆時点ではプライベートベータなので、ほぼいらっしゃらないとは思いますが・・)

検証バージョン

以下の通りです。

$ docker version
Client:
 Version:      1.10.3
 API version:  1.22
 Go version:   go1.6
 Git commit:   20f81dd
 Built:        Sat Mar 12 04:08:57 UTC 2016
 OS/Arch:      darwin/amd64

Server:
 Version:      1.11.0-rc3
 API version:  1.23
 Go version:   go1.5.3
 Git commit:   eabf97a
 Built:        Fri Apr  1 23:33:49 2016
 OS/Arch:      linux/amd64

$ docker-machine version
docker-machine version 0.6.0, build e27fb87

$ docker-swarm --version
docker-swarm version 1.1.3 (HEAD)

Swarm Discovery

公式ドキュメントにもあるとおり、Swarmクラスタの構築にはService DiscoveryとしてのKVSが必要となります。 etcdでもconsulでも何でも良かったのですが、Zookeeperの環境が手元にあったので、今回はZookeeperでやってみます。

Zookeeperのコンテナを起動します。(もちろんコンテナである必要は特にありません)

$eval "$(docker-machine env zk-host)"
$docker run -d -p 2181:2181 -p 2888:2888  -p 3888:3888 wurstmeister/zookeeper

docker ps
CONTAINER ID        IMAGE                    COMMAND                  CREATED             STATUS              PORTS                                                                            NAMES
d58f1a1b5b70        wurstmeister/zookeeper   "/bin/sh -c '/usr/sbi"   About an hour ago   Up About an hour    0.0.0.0:2181->2181/tcp, 0.0.0.0:2888->2888/tcp, 22/tcp, 0.0.0.0:3888->3888/tcp   furious_mahavira

$ docker-machine ls
docker-machine ls
NAME            ACTIVE   DRIVER       STATE     URL                         SWARM                    DOCKER        ERRORS
zk-host         *        virtualbox   Running   tcp://192.168.99.108:2376                            v1.11.0-rc3 

Docker swarm manager用のホストを作成

さきほど起動したZookeeperホストを -swarm-discovery zk:// と指定します。 その他 --swarm などswarm関連のオプションを指定します。

$ docker-machine create --driver virtualbox --swarm --swarm-master --swarm-discovery zk://192.168.99.108:2181 swarm-manager

$ docker-machine ls
NAME            ACTIVE   DRIVER       STATE     URL                         SWARM                    DOCKER        ERRORS
zk-host         *        virtualbox   Running   tcp://192.168.99.108:2376                            v1.11.0-rc3   
swarm-manager   -        virtualbox   Running   tcp://192.168.99.109:2376   swarm-manager (master)   v1.11.0-rc3 

Docker swarm node用のホストを作成

managerを起動したコマンドから --swarm-masterだけを取り除いてnode用のホストを立ち上げます。 今回は2つ立ち上げてみます。

$ docker-machine create --driver virtualbox --engine-insecure-registry 10.6.215.231:5000 --swarm --swarm-discovery zk://192.168.99.108:2181 swarm-node01

$ docker-machine create --driver virtualbox --engine-insecure-registry 10.6.215.231:5000 --swarm --swarm-discovery zk://192.168.99.108:2181 swarm-node02

確認

swarmの情報を得る為にいつものeval..に --swarmを付けます

eval "$(docker-machine env --swarm swarm-manager)"

こんな感じでクラスタが立ち上がっています。

$ docker-machine ls
NAME            ACTIVE   DRIVER       STATE     URL                         SWARM                    DOCKER        ERRORS
zk-host         *        virtualbox   Running   tcp://192.168.99.108:2376                            v1.11.0-rc3   
swarm-manager   -        virtualbox   Running   tcp://192.168.99.109:2376   swarm-manager (master)   v1.11.0-rc3   
swarm-node01    -        virtualbox   Running   tcp://192.168.99.110:2376   swarm-manager            v1.11.0-rc3   
swarm-node02    -        virtualbox   Running   tcp://192.168.99.111:2376   swarm-manager            v1.11.0-rc3  

$ docker info
Containers: 3
 Running: 1
 Paused: 0
 Stopped: 2
Images: 13
Server Version: 1.11.0-rc3
Storage Driver: aufs
 Root Dir: /mnt/sda1/var/lib/docker/aufs
 Backing Filesystem: extfs
 Dirs: 44
 Dirperm1 Supported: true
Logging Driver: json-file
Plugins: 
 Volume: local
 Network: bridge null host
Kernel Version: 4.1.19-boot2docker
Operating System: Boot2Docker 1.11.0-rc3 (TCL 7.0); HEAD : fed36ed - Sat Apr  2 00:03:40 UTC 2016
OSType: linux
Architecture: x86_64
CPUs: 1
Total Memory: 996.1 MiB
Name: zk-host
ID: PXK5:IAL6:DVXA:33ZH:CTMP:GVES:HJUG:KW5U:PZOX:PXWV:3HJC:55MY
Debug mode (server): true
 File Descriptors: 15
 Goroutines: 36
 System Time: 2016-04-18T06:39:56.693979855Z
 EventsListeners: 0
 Init SHA1: 
 Init Path: 
 Docker Root Dir: /mnt/sda1/var/lib/docker
Labels:
 provider=virtualbox

ちなみに、managerが1nodeして動いてしまっていたり、そもそもレプリケーション出来てなかったりと 本番環境の参考とするには色々まずいのでご注意下さい。

せっかくなので、zk-webを使ってZooKeeperの中身も少し覗いてみました。 swarmのノード情報が書き込まれています。 f:id:junchang1031:20160418165133p:plain f:id:junchang1031:20160418165145p:plain

Blue/Green Deployment

長くなりそうなので今回はこの辺で。 次回はこの環境下での Blue/Green Deployment実現方法を投稿しようと思います。