読者です 読者をやめる 読者になる 読者になる

技術備忘記

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

Docker network周りでハマった話

Docker Compose Swarm

何をしたか

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 golang 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

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 のretry 時の動きに似ていますね。

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でアルゴリズムのお勉強

Go golang

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

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 ZooKeeper

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実現方法を投稿しようと思います。

GoからGitHub APIを利用する

Go golang Github

はじめに

とあるツールを作っていて、GoのコードからGithub APIを叩く必要があったので、使用したライブラリや、スニペットなどを残しておこうと思います。ちなみにエラーハンドリングは一切していないです。予めご了承ください。

使用ライブラリ

go-github を使いました。Goから Github APIを利用する際に使用するライブラリとしては、現状実質これ一択になると思います。 ドキュメントがすごくしっかりしているので、Github API v3の知識がそれなりにさえあれば、ほぼ詰まることなくやりたいことが実現できると思います。オススメです。

スニペット

oauth2 認証

owner := "your github account name"
repo := "your github repository"
token := "your github access token"

ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
tc := oauth2.NewClient(oauth2.NoContext, ts)

cl := github.NewClient(tc)
srv := cl.Git // *github.GitService

push 的な流れ

branch 作成

dRef, _, _ := srv.GetRef(owner, repo, "heads/develop")
branchName := "feature/new_branch"
ref := &github.Reference{
    Ref: github.String("refs/heads/" + branchName),
    Object: &github.GitObject{
        SHA: mRef.Object.SHA,
    },
}
// developから新しくfeature/new_branchを作成
srv.CreateRef(owner, repo, ref)

commit tree 作成

blob := &github.Blob{
    Content:  github.String("test"),
    Encoding: github.String("utf-8"),
    Size:     github.Int(len("test")),
}
resB, _, _ = srv.CreateBlob(owner, repo, blob)

entry := github.TreeEntry{
    Path: github.String("path/to/test"),
    Mode: github.String("100644"),
    Type: github.String("blob"),
    SHA: resB.SHA,
}

entries := []github.TreeEntry{entry}

tree, _, err := srv.CreateTree(owner, repo, *dRef.Object.SHA, entries)

commit

// developの最新commitを取得
parent, _, _ := srv.GetCommit(owner, repo, *dRef.Object.SHA)

commit := &github.Commit{
    Message: github.String("commit from golang!"),
    Tree:    tree,
    Parents: []github.Commit{*parent},
}
// commit作成
resC, _, err := srv.CreateCommit(owner, repo, commit)

// 作成したbranchに紐付ける
nref := &github.Reference{
    Ref: github.String("refs/heads/" + branchName),
    Object: &github.GitObject{
        Type: github.String("commit"),
        SHA:  resC.SHA,
    },
}
srv.UpdateRef(owner, repo, nref, false)

pull request

prSrv := cl.PullRequests
npr := &github.NewPullRequest{
    Title: github.String("test"),
    Head:  github.String(owner + ":" + branchName),
    Base:  github.String("develop"),
    Body:  github.String("test"),
}
prSrv.Create(owner, repo, npr)

pull 的な流れ

dRef, _, _ := srv.GetRef(owner, repo, "heads/develop")

tree, _, _ := srv.GetTree(owner, repo, *dRef.Object.SHA, true)

for _, e := range tree.Entries {
    if *e.Type != "blob" {
        continue
    }
    blob, _, _ := srv.GetBlob(owner, repo, *e.SHA)
    dec, _ := base64.StdEncoding.DecodeString(*blob.Content)
    fmt.Println(dec)
}

終わりに

今回はブログというか、本当に備忘といった感じです。 まあ本ブログのテーマ通りなので良いのですが (^^;)

Goパッケージ依存管理最新事情

Go golang

はじめに

Goの依存パッケージ管理といえば、以前から以下のような問題点が指摘されており、 総じてイマイチ、というのが通説である気がします。

  • 外部依存ライブラリのバージョン等が指定できないためビルドの再現性が保証されない
  • 複数prjの開発を同一環境で行う場合 GOPATH が汚染される、あるいは個別に切り替える必要があり面倒

こういった問題点を解消するため、バージョン1.6から(試験的には1.5から) Vendoring という機能が導入されました。 以前から存在しているパッケージ依存管理ツールもこの機能に絶賛対応中(あるいは対応済)といった状況のようです。

今回はこのVeondroing機能と、代表的なパッケージ管理ツールの特徴を調べてみましたので、ご紹介したいと思います。

Vendoring

Vendoringというと大層な機能に聞こえますが、言ってしまえば依存ライブラリの参照先(PATH)に./vendor を追加しただけです。(少なくとも利用側からすれば) この機能が有効になったことで、Goはコンパイル時に依存ライブラリを ./vendor => GOROOT => GOPATHの順番で探しに行くことになります。

冒頭でも述べた通り、Goは外部ライブラリをimportする際にバージョンやタグ、リビジョンなどを指定することが出来ません。 開発環境・ビルド環境を構築するタイミング(=外部ライブラリをgo getにより取得するタイミング)により、 全く同じソースコードでも異なる動作をする可能性があります。

それを解決するためにこのVendoringが導入されました。プロジェクトに関連するリソースは全てvendor下にぶち込んでしまえば再現性が担保できるでしょ、といった考え方です。余談ですが、Genericsを要望する声に対してのCode Generate、という解と、どことなく同じ印象を受けます。

加えて、GOPATH下に依存ライブラリを配置する必要がなくなるため、複数prjにおけるGOPTH汚染問題も解決できるようになります。

代表的なパッケージ依存管理ツール

上記のVendoringがサポートされる前から、様々なパッケージ依存管理ツールGopher達によって開発されていました。

私自身はGoを触り始めた2014年中頃からずっとgodepを使っていました。 当時は特に他のツールときちん比較検討せず、単純にGithub上のスターが一番多いし・・と割と安易に導入を決めてしまいましたw

godepは非常に優秀なツールではあるのですが、全く不満がない訳でもなく、もし他に良いものがあれば乗り換えるのも有りだな、 と思うに至り、今回改めて代表的なツールを一通り触ってみて特徴をまとめました。比較用にgodepも入れています。

ちなみに、それぞれGoのバージョンにより動作に違いがあったりするのですが、 今回は全てバージョン1.6前提とします。

godep

  • vendoring対応済。
  • godep save でプロジェクトのimportしている内容と、$GOPATH以下のVCSの情報などを解析し、以下の処理を行う
    • Godeps/Godeps.jsonに依存しているライブラリのバージョン情報等を出力
    • ./vendor以下に依存しているライブラリを$GOPATHからコピー (Godeps.jsonの例)
{
    "ImportPath": "github.com/kr/hk",
    "GoVersion": "go1.6",
    "Deps": [
        {
            "ImportPath": "code.google.com/p/go-netrc/netrc",
            "Rev": "28676070ab99"
        },
        {
            "ImportPath": "github.com/kr/binarydist",
            "Rev": "3380ade90f8b0dfa3e363fd7d7e941fa857d0d13"
        }
    ]
}
  • godep restore でGodeps.jsonの内容をもとに、$GOPATHに依存ライブラリをダウンロードする(リバースエンジニアリング)。汚染問題あり
  • その他 go testをwrapする機能(1.6では不要?)や、godep update foo/...による依存ライブラリの更新など、機能が充実している

gb

  • vendoring未対応。同じようなことは実現できるが、独自ルールのディレクトリ構成を強制する。(個人的には大きくマイナス..)
  • gb buildでビルドと同時に以下の処理を行う
    • $GOPATHをカレントディレクトリに書き換え
    • 依存ライブラリを./vendor/src/以下にダウンロード
  • 依存ライブラリのバージョン管理、リバースエンジニアリングの機能を使いたい場合は追加で gb-vendor が必要。機能的にはgodepと大差無いが、$GOPATHを汚染することは無い。

glide

  • vendoring対応済。
  • glide createglide.yamlが作成される。以後 golide get hogehoge(go getの代わり)をするとこのファイルに反映される
  • glide install/updateglide.yamlの内容をもとに、依存ライブラリを ./vendor以下にダウンロード
  • リバースエンジニアリング可能。依存ライブラリのバージョン情報などを自動で入れる仕組みは、単体では無いが、godepの出力するGodep.jsonから自動反映させることは可能。
  • bitbucketやstashなど、go getのルールにマッチしていないURLのサービスを指定することも可能
  • glide list glide treeなどの依存関係の確認するコマンドがとても便利。

gom

  • vendoring対応済。
  • gom gen gomfileGomfileが作成される。
  • gom installGomfileの内容をもとに、依存ライブラリを ./vendor以下にダウンロード
  • リバースエンジニアリング可能。ただしGomfileの入力は今の所手作業のみっぽい
  • go getのルールにマッチしていないURLのサービスを指定することも可能
  • RubyGemインスパイア

まとめ

Vendoringは、ご紹介した通り非常にプリミティブな機能だけを提供しているので、 細かなバージョン管理などを含めたパッケージの依存管理には、やはりツールを利用するようにした方が色々捗る、 ということは1.6でも変わらなそうです。

今回改めて色々触ってみましたが、godep以外だとglideがだいぶ良さそうな印象でした。

ただし、私がパッケージ依存管理ツールに求める機能、 という意味ではそこまで大きな差はなく、今回調べた限りではコストをかけて移行する気にはなれませんでした。

そして、残念ながらどのツールも、現時点では私のニーズを100%満たしてくれることは無さそうでした(ニッチなのかもれません、、)

こうなったら自作してみるのもありかな・・ 既存ツール達のどこに不満に感じていて、その不満を解決するために作りました! みたいな記事を、近いうち書けるように頑張ります(^^;)