それが僕には楽しかったんです。

いろんなレイヤーに居ます

画像プロキシを Golang で作った話

はじめに

最近、アマゾンプライムビデオで「ミッションインポッシブル:フォールアウト」を見ていて最後に個人的にあまり好きでない3作目から登場していた元妻でなく前作でいい感じだった美人スパイとトム・クルーズがくっついて「この展開を待っていたああああああああ!!」と静かにガッツポーズしたけんつです。

この前まで Golang で並列処理の勉強をしていたけども急に忙しくなってしまい全く進んでいない現実に向きあいたくなくて今回は GolangSSL 画像プロキシを作りました。
そのことを雑に書いていこうかなと思っています。

なに作ったか

github.com

すごいもの作った感じのタイトルを書いたけど実は参考にしたというか Nodejs 製の画像プロキシを Golang で書きなおしただけ。
参考にしたのはこちら。

github.com

結構使われている画像プロキシで

Togetter とか
qiita.com

Qiita とか
qiita.com

探せば色々なサービスで使われていたりする。
Github の README 内に含まれる画像達もこれで配信されているらしい。
というのも、元々外部リソースを参照するときに HTTPS 化するときに全てのコンテンツが HTTPS で提供される必要があるのだが
Togetter のようなキュレーションサイトや Github, Qiita のような外部の画像を扱う可能性のあるものでは常にそれらが HTTPS で提供されているものを使えるというわけではなく
HTTP で提供される外部リソースをユーザが埋め込んでしまう場合がある。
そうなると、 Mix content になってしまいサービス自体を HTTPS で提供できないという問題がある。

そのような問題を上記の Camo では解決できる。具体的には何をするかというと Togetter の Qiita にわかりやすく要点が纏められていた。

実はCamoそのものはSSLプロキシ機能を提供していません。
Camoが提供する最も重要な役割は、プロキシ用URLに共通鍵から生成したダイジェストを含めて暗号化することなのです。

画像プロキシサーバを運用する上で懸念されることとして、プロキシ用URLに含まれるオリジン画像のURLを自由に書き換えられた結果、サービサーが想定していないリソースを配信してしまうことなどがあります。
共通鍵で暗号化することで、有効なプロキシ用URLを生成できるのは共通鍵を知るサービサーに限定することが可能になるのです。

これ考えた人、本当に天才ではと思う。

というわけで何か Golang で作りたくて、今回はこれの Golang 実装を作ってみた。

結構苦戦した

いくつか面倒だった部分となんとかこの機能を作るまでに色々と苦戦した部分をまとめる。

Proxy VS Reverse Proxy

最初は Golang で提供されている ReverseProxy 用の構造体を使って作ろうと思ったけど、あれを使うと Golang 側で Request と Response のハンドラーが完全に分離してしまい
'/', '/status' といった通常のレスポンスを返すルーティングを持つものが作りにくかったのでやめた。

普通に HTTP サーバを作るのと同じ要領で作り、画像プロキシのルーティングが呼ばれた時は NewRequest で新しくリクエストを作り直し得た画像情報を ResponseWriter でクライアントに返却するようにしている。

HMAC SHA1

この画像プロキシでは HMAC SHA1 で生成された 40 文字のダイジェストを元に鍵を確認しているがそこが問題だった。
SHA1 を吐けるものは Golang にも存在するがそれを使うと地味に鍵の設定がしづらい。

Go言語でSHA-1 - Misc Notes

そこで "crypto/hmac", "crypto/sha1"というパッケージを使った方が簡単だったからそうした。
参考↓
GoでHMAC SHA1を計算するサンプル · GitHub

Url Encoded Path

これは自分の勘違いのせいでめちゃくちゃ苦戦した。

camo は以下の形式でリクエストを受け取る。

http://example.org/digest?url=image-url
http://example.org/digest/image-url

この時、1つ目のパターンでは image-url を url encode された形式で渡しているが2つ目のパターンでは 16 進数に変換された image-url を受け取る必要がある。
これを両方 url encode された URL だと勘違いしてめちゃくちゃハマった。

というのも、なぜ URL Encode された情報をパスに出来ないかというと RFC にはこう書かれている。
https://tools.ietf.org/html/rfc3986#section-6.2.2.2

The percent-encoding mechanism (Section 2.1) is a frequent source of
variance among otherwise identical URIs. In addition to the case
normalization issue noted above, some URI producers percent-encode
octets that do not require percent-encoding, resulting in URIs that
are equivalent to their non-encoded counterparts. These URIs should
be normalized by decoding any percent-encoded octet that corresponds
to an unreserved character, as described in Section 2.3.

正規化されるのだ。

今回は gorilla mux をルーティングに使用したがそれを無効にするオプションがあるのになぜか上手くそれが解決されない問題があって苦戦した。
実際には2つ目の は 16進数表現されたものであるとわかってなるほどという気持ちになった。

そして2つ目のパターンは実装がめんどくさすぎるのとテストするときにわざわざ url を 16 進数に変換したものを使うひつようがあるのがまためんどくさくて実装していない。

使い方

ほとんど camo と同じ使い方でいける。
ただ先程も書いたが 2つ目のパターンはまだ実装していないから使えない。

http://example.org/digest?url=image-url:urlencoded

このパターンなら行ける。

おわりに

ミドルウェア書くの楽しい。

Golangのgoroutine周りを気合で理解する

はじめに

どうも最近netflixで配信されている「センターオブジアース」の吹き替えが絶妙に酷いことに気がついてしまったけんつです。

昨日はGolangのContextについて記事を書きましたが、goroutineやchanelへのある程度の理解なしに勉強しても得られるものは少ないことに気がついたので今日はそのあたりをガッツリやります。
まるでマラソンのようになっていますが明日の記事が投稿されることは保証されません。

goroutine

えぇ、Golangの特徴と言えば?と聞かれて真っ先に思い浮かぶやつ。
これが何かと言われると、「Go言語による並行処理」では次のように書かれている。

ゴルーチンはGoのプログラムのなかで最も基本的な構成単位です。

なぜこれが最も基本的な構成単位なのかというと、main関数はメインゴルーチンと呼ばれるゴルーチンの上で動作するためである。
メインゴルーチンはプロセスが開始するときに自動的に生成されて起動されるが、それ以外のゴルーチンは明示的に宣言することで使用できる。

go sampleFunc()

これはsampleFuncという名前の関数を並行に実行するということを表している。(必ずしも「並列」とは限らない)
めちゃくちゃ便利。

それでは一体、ゴルーチンとは何なのか。

パッと見たときに、ゴルーチンってすごくスレッドっぽいなと思うかもしれない。特にJavaやなにかその類のマルチスレッドを搭載している言語を使ったことがある人なら。
ただ参考文献の中には次のように書かれている。

ゴルーチンはGo特有のものです。ゴルーチンはOSスレッドではなく、また必ずしもグリーンスレッドではありません。ゴルーチンはコルーチンとして知られる高水準の抽象化です。

つまり何が言いたいのかというと、実行中のタスクを一時停止しない、割り込みされないサブルーチンのことらしいです。
ゴルーチン自体には一時停止や再エントリーを認めているいくつかのポイントが存在するが、ゴルーチンはそれらを定義しているわけではなくそのあたりはGoのランタイムが制御しています。
ただ、よくWeb上ではスレッドを更に改良したものだ。と言われることも多く厳密に理解する必要がない場合は軽量なGo特有のスレッドと思うとよいのかもしれません。

そしてここまでまとめた背景の上にゴルーチンが暗黙的の並行処理要素として成立しています。

goroutineをもう少し掘り下げる

ゴルーチンはM:Nスケジューラーという実装の上に成立しているらしいです。

このM:Nスケジューラーが一体何なのか、というとそれは「任意の数のOSスレッドに任意の数のゴルーチンをスケジューリングします。」*1
とのこと。

また、Goはfork-joinモデルという並行処理モデルに従っているためメインゴルーチン、または親となるゴルーチンから派生させる場合にはforkが行われあるタイミングで親のゴルーチンにjoinされ合流するというようになっています。
「Go言語による並行処理」ではそのように書かれていますが、同時に合流ポイントが発生しないまま終了してしまう可能性に注目しています

というのも次のようなコードを実行すると一目瞭然なのですが

package main

import "fmt"

func main() {

    go sayHello()
    fmt.Println("say hello from main goroutine")

}

func sayHello() {
    fmt.Println("say hello")
}

go sayHello() によりゴルーチンが生成(fork)されるがメインゴルーチンの処理が終わるまでにその実行タイミングが無い場合はそのゴルーチンは実行されないのです。

この場合に、前回の記事の様に Time Sleep してもいいのですが参考書籍ではそれは単に競合状態を作っているだけに過ぎず確実に起動するものを保証はしていないため Sync パッケージを使用するべきとなっています。*2

おまけ

「Go言語による並行処理」でゴルーチンが絡んだ面白い例が紹介されていたのでここでも紹介したい
次のようなコードがあった場合に出力が全てPHPになってしまうという事例がある。

package main

import (
    "fmt"
    "sync"
)

func main() {

    var wg sync.WaitGroup

    for _, value := range []string{"golang", "scala", "PHP"} {
        wg.Add(1)
        go func() {
            defer wg.Done()
            fmt.Println(value)
        }()
    }
    wg.Wait()

}

なぜそのような事態が起こるかというと、forで使用している変数 value を表示はしているがゴルーチンが開始するよりも先にループが終わってしまうために発生する。
ではなぜ最後の要素である"PHP"が三回表示されるのかというと、これはGolangGCの話でゴルーチン内で該当する変数が使用されていると判断されスライスの最後の要素である"PHP"への参照が維持されるからである。

これを回避するため(正しく動作させるため)に次の様に少し変更を加えるだけでよい。

     go func(value string) {
            defer wg.Done()
            fmt.Println(value)
      }(value)

これは面白い問題だった。

Syncパッケージ

ここからはぼちぼち公式ドキュメントも参照していく。
golang.org


Syncパッケージは排他制御や同期処理といったものに必要な基本的な機能を提供している。

WaitGroup

WaitGroupは先程も紹介した例にあるが、これは複数のゴルーチンを利用している場合に同期処理を行いたい時に使われる。
特定の集計などの処理を行った場合にその処理が全て終わるのを待ちたい場合などに有効である。

例えば先程のおまけで紹介したコードを次のように修正したとしよう。

package main

import (
    "fmt"
    "sync"
)

func main() {

    var wg sync.WaitGroup

    for _, lang := range []string{"Golang", "Scala", "PHP"} {
        wg.Add(1)
        go func(lang string) {
            defer wg.Done()
            fmt.Println(lang)
        }(lang)
        wg.Wait()
    }
}

まずここで疑問に思うことは Add(1) だろう。
この関数では delta 、つまり変化量として整数値を渡すが内部カウンタの値がそれによって変動する。
カウンタが0であればブロックしていたゴルーチンが全て開放されていたことになり、正の整数であればブロックしているゴルーチンが存在していることになり負の値になれば panic となる。

つまり同期処理したいものがあればこの Add には 1 を渡すべきである。

そしてゴルーチン内で defer を使って Done を呼び出しているが、これはこの関数が終了した時に呼ばれる
そして Done はカウンタに対して作用し、カウンタの値を1減算する。

ループ内最後の Wait() これはカウンタが0になるまで待つものである。

これらの WaitGroup に含まれるメソッドを使うことで同期処理が可能となっている。

Mutex

これは割とよく見かけるのでわかると思うが、並行処理において共通リソースにたいして排他的アクセスを安全に提供するために存在している。

まず共有リソースにアクセスしてしまっているまずい例から見ていく。

package main

import (
    "fmt"
    "sync"
)

func main() {

    count := 0

    var wg sync.WaitGroup
    
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            count++
        }()

        wg.Add(1)
        go func() {
            defer wg.Done()
            fmt.Println(count)
        }()
    }

    wg.Wait()

}

これは wait が for のスコープ外にあるためにそれぞれのゴルーチンの起動順が守られずに値がバラバラに表示される。

こいつに適切な排他制御を加えることで想定どおりの解を出力させる。
上記のコードを以下のように変更する。

package main

import (
    "fmt"
    "sync"
)

func main() {

    count := 0

    var wg sync.WaitGroup
    var mutex sync.Mutex

    for i := 0; i < 10; i++ {
        wg.Add(1)
        mutex.Lock()
        go func() {
            defer wg.Done()
            defer mutex.Unlock()
            count++
        }()

        wg.Add(1)
        mutex.Lock()
        go func() {
            defer wg.Done()
            defer mutex.Unlock()
            fmt.Println(count)
        }()
        wg.Wait()
    }

}

やっていることは非常に単純で、上のゴルーチンが起動する前にロックをかける
次のゴルーチンが起動するときにロックがかかったままならアンロックされるまで待つというもの。

これで排他的に共有リソースにアクセスできる。
他にも似ているもので RWMutexというものも存在している。
RWMutexはRead Writeにおいて片方にロックをかけることができる。

おわりに

ここまでで前回と関連のありそうなものを中心に触れたが、まだ他にも Cond, RWMutex, Poolなどが残っているため次回にまわすことにする。

*1:GoConで発表してきたのでついでにruntime以下の知識をまとめていく #golang - niconegoto Blog https://niconegoto.hatenadiary.jp/entry/2017/04/11/092810

*2:Go言語における並行処理の注意点 https://medium.com/eureka-engineering/go-waitgroup-map-data-race-ec3561de6e47

GolangのContextを頑張って理解していく

はじめに

どうも最近吹き替えが酷い映画とドウェイン・ジョンソン主演の大体の映画にハマっているけんつです。

Golang書いていてちょくちょくでてくる context.Background() って何やっているのと思ったのでちょいと勉強がてらまとめていこうかと思います。

Contextパッケージ

今回の主な話としてはContextパッケージの話とそのContextとは何かというところになる。
ContextパッケージはGolangに標準で組み込まれているパッケージで、このパッケージに含まれるContextなる型はプロセスをまたいでデッドラインやキャンセルシグナルを伝達することができる機能群を提供している。
オライリーから出版されている「GO言語による並行処理」を参考にこれらを簡潔に説明するのならば、Context型は並行処理内でアクションの通知以外にもキャンセルした理由やデッドラインの有無などという状態を付加するために使用される。

golang.org

詳しい説明は公式ドキュメントに記載されているので気になる場合は確認して欲しい。
次からはよく使う関数やメソッド群を実例を踏まえてまとめていく。

実例

今回使用したコード↓

package main

import (
    "fmt"
    "context"
    "time"
)

func main() {

    // 空のContextを生成
    ctx := context.Background()
    go parentRoutine(ctx)

    time.Sleep(20 * time.Second)

    fmt.Println("main func finish")

}

func parentRoutine(ctx context.Context) {

    subCtx, cancel := context.WithCancel(ctx)
    subsubCtx, cancel2 := context.WithCancel(subCtx)

    go childRoutine(subCtx, "sub context")
    go childRoutine(subsubCtx, "sub sub context")

    time.Sleep(5 * time.Second)

    cancel2()

    time.Sleep(5 * time.Second)

    cancel()

    tCtx, _ := context.WithTimeout(ctx, time.Second * 5)
    go childRoutine(tCtx, "context with timeout")

}

func childRoutine(ctx context.Context, prefix string) {
    for i := 0; ; i++{

        select {

        case <-ctx.Done():
            fmt.Printf("routine %s cancel.\n", prefix)
            return

         case <-time.After(1 * time.Second):
             fmt.Printf("routine %s has value %d \n", prefix, i)

        }
    }
}

実行結果↓

$ go run context.go 
routine sub sub context has value 0 
routine sub context has value 0 
routine sub sub context has value 1 
routine sub context has value 1 
routine sub sub context has value 2 
routine sub context has value 2 
routine sub sub context has value 3 
routine sub context has value 3 
routine sub sub context cancel.
routine sub context has value 4 
routine sub context has value 5 
routine sub context has value 6 
routine sub context has value 7 
routine sub context has value 8 
routine sub context cancel.
routine context with timeout has value 0 
routine context with timeout has value 1 
routine context with timeout has value 2 
routine context with timeout has value 3 
routine context with timeout cancel.
main func finish

解説

コンテキスト生成

まずはコンテキスト生成に着目していく。

ctx := context.Background()

コンテキスト生成には今回上記の関数を使用している。
これには context.TODO() も使用することができる。BackGround() はそれぞれ nil 以外の空のコンテキストを返し、これは値もデッドラインも存在しない最上位のコンテキストとして存在する。
TODO() はnil 以外の空のコンテキストを返すという部分は同じだがどのコンテキストを使用するか不明な場合やまだ使用できない?場合はこちらを使用するべきらしい。今のところはTODOで生成されたコードを見たことがないのでよくわからない。

もうひとつの方法として context.WithValue(...) を使用することもできる。この場合はそのコンテキストに値を渡すことが可能になる。

subCtx, cancel := context.WithCancel(ctx)
subsubCtx, cancel2 := context.WithCancel(subCtx)

次に、この部分に注目して欲しい。この部分ではコンテキストを生成しているが明示的にキャンセルすることが可能な状態で生成している。
そして subCtx から subsubCtx を生成していることにもこの時注目して欲しい。ちなみに cancel は _ で省略することもできる。

キャンセルシグナル

WIthCancel でコンテキストを生成した場合、第2変数で明示的にそのgoroutineを終了させることができる。そのように終了した場合 ctx.Done() がコールされる。
最初に cancel2() をコールして subsubCtx 側をキャンセルしているがこれの順序を逆にすると subCtx から生成された subsubCtx も同時にキャンセルされることとなる。

そして WithTImeout の場合は明確で、設定した時間だけ時間が経過すると自動的にキャンセルされる。

おわりに

Contextとは 最初にも記述したが並行処理においてそのアクションをした通知だけでなく何故そのアクションをしたかなどの状態をもつ伝播するものだということがわかった。
またそれには値を伝播させることができるため、同一のリクエストスコープで使用することが推奨される。つまり、構造体やグローバルな変数として扱っては行けないということだ。
今回はContextの割と基本的な部分についてまとめたが goroutineリークなどと絡めてまたまとめることがあれば続きを書く。かも。

DevStackでOpenStackを導入しようとした時に躓いた話

はじめに

どうも、最近海外ドラマを見ているときのハラハラに耐えられず結末を見てから鑑賞することがやめられないけんつです。
最近OpenStackの環境構築でやたら躓いた部分があったが、まぁ情報が少ないこと少ないことと困ったのでまとめます。

前提条件

OSはUbuntu 18.04を最小構成でインストールした直後として
linux kernel は 4.18.0-20-generic を使用する環境で devstack を使った場合を想定しています。

stackユーザを作ることや、レポジトリのクローンは全て以下のドキュメント準拠で行いました。
docs.openstack.org

ハマりまくった部分

ブランチ問題

これは最初に引っかかった部分で、devstackを使った場合の手順などを紹介している諸々のブログではブランチ(バージョン)を指定してクローンしている。
しかし、Ubuntu 18.04で古いブランチを指定してしまうと途中のpip周りで導入するライブラリのバージョンが衝突する場合がありインストールが終わらない。

これはmasterブランチを利用することで解決できる。

インストール時のトラブルシュート

1. Could not install packages due to an EnvironmentError

この記述がdevstackのインストール中に表示された場合は該当するライブラリをpip3 or pipを使って手動でインストールする必要がある。

$ pip3 [or pip] install [library name] --user
2. env: ‘/opt/stack/requirements/.venv/bin/pip’: No such file or directory

pipを使ってvirtualenvを導入するのに失敗しているため以下のコマンドを実行することで解決する。
~/devstack$ virtualenv ../requirements/.venv/

3.libvirtd failed to start
$ systemctl status libvirtd

まず上記のコマンドでステータスを確認する。
Loadedになっている状態であれば以下のコマンドを Failed になっている場合はエラー内容を確認して対処する。
Loaded の場合はサービスの再起動を試してみる

$ systemctl restart livirtd

このとき以下のようなメッセージが表示されたログを確認しても何も無い場合はlibvirtdをaptで導入し再起動すると解決した。

please check journalctl -xe ...
$ sudo apt update
$ sudo apt install libvirtd
$ sudo reboot

DevStackで構築したあとのトラブルシュート

1. [Openstack] [Glance] Error 503 when creating image

このエラーはインスタンスイメージの登録時に起こることがあった。

OpenStackのユーザプルダウンメニューからopenstackrcをダウンロードし

$ source ./[ユーザ名]-openrc.sh

と実行する。
そのあとに以下のコマンドを実行することで解決された。

$ openstack user set --password glance glance
2. Missing value auth-url required for auth plugin password

openstackコマンド実行時に表示されることがあります。環境変数が正しく1.の手順で設定されていないことが原因だと考えられる。

$ export | grep "OS_"

そのため、このコマンドで環境変数がexportされていることを確認する。
上記のコマンドで何も表示されない場合は、1.の手順を確実に行う。そのときsourceコマンド以外で実行するとexportが設定されないということにも注意が必要。
自分が引っかかったのはここだった。

3. No valid host was found. There are not enough hosts available.

インスタンスイメージの形式がkvmになっている場合にqemu2のイメージを登録する際に発生する。
/etc/nova/nova.confのvirt_typeをkvmからqemuにするか追加することで解決する。

etcd を golang で使う

はじめに

最近、OpenStackのインストールで無限に消耗しているけんつです。

OpenStackのインストールがまじで辛いので現実逃避のために etcd を Golang で使ってみたのでなんとなく気分で記事にしてみます。

etcdとは

そもそも etcd とは何かというと、簡潔にまとめているサイトの引用によると*1

etcd は、分散KVS (Key Value Store) です。クラスタのマシンでデータを共有できます。 アプリケーションから etcd にデータの読み書きが行えます。 etcd は、 CoreOS のコンポーネントとして使われます。

とのこと。


Golangで実装されており、HTTP(正確には gRPC)でJSONを使ってやり取りすることが可能となっている。
まてKVSであり、キーやディレクトリの変更を監視し値が変わった場合にそれを検知することができる。

gRPC

gRPCの前にRPCとは何かというところに触れる。 RPCとはプログラムから別のネットワーク上などに存在するコード等をリモートで呼び出すものらしい。

gRPCはその高性能なフレームワークであり、ロードバランスやトレースなどのサポートによってデバイスやモバイル、ブラウザをバックエンドサービスに接続することができる。*2

具体的な実装例があったので紹介しておく。
medium.com

上のサイトではメッセージ送信に関しては従来のプロセスと何も変わらないが、redisのpub/subとgRPC サーバを接続することでメッセージの受信を高速化したとのこと。

etcd をインストールする

Ubuntu上では apt を使って導入できるようだが、以下のサイトにしたがってインストールする。
computingforgeeks.com

$ etcd --version
etcd Version: 3.3.12
Git SHA: d57e8b8
Go Version: go1.10.8
Go OS/Arch: linux/amd64

(中略)

$ systemctl status etcd.service
● etcd.service - etcd key-value store
   Loaded: loaded (/etc/systemd/system/etcd.service; disabled; vendor preset: enabled)
   Active: active (running) since 木 2019-05-16 23:07:43 JST; 9s ago
     Docs: https://github.com/etcd-io/etcd
 Main PID: 19379 (etcd)
    Tasks: 13
   Memory: 3.7M
      CPU: 101ms
   CGroup: /system.slice/etcd.service
           └─19379 /usr/local/bin/etcd

etcdをGolangから使う

公式のクライアントを使う。

$ go get go.etcd.io/etcd/client
package main

import (
    "go.etcd.io/etcd/client"
    "time"
    "context"
    "fmt"
)


func main() {

    config := client.Config{
        Endpoints: []string{"http://127.0.0.1:2379"},
        Transport: client.DefaultTransport,
        HeaderTimeoutPerRequest: 2 * time.Second,
    }

    clt, err := client.New(config)
    if err != nil {
        panic(err)
    }

    keyapi := client.NewKeysAPI(clt)

    //Set key with value
    response, err := keyapi.Set(context.Background(), "/test", "test set", nil)
    if err != nil {
        panic(err)
    } else {
        fmt.Printf("Set is Done. Metadata is %q\n", response)
    }

    response2, err := keyapi.Get(context.Background(), "/test", nil)
    if err != nil {
        panic(err)
    } else {
        fmt.Printf("Get is Done. Metadata is %q\n", response2)
        fmt.Printf("%q key has %q value\n", response2.Node.Key, response2.Node.Value)
    }

}
$ go run main.go 
Set is Done. Metadata is &{"set" "{Key: /test, CreatedIndex: 4, ModifiedIndex: 4, TTL: 0}" <nil> '\x04' "cdf818194e3a8c32"}
Get is Done. Metadata is &{"get" "{Key: /test, CreatedIndex: 4, ModifiedIndex: 4, TTL: 0}" <nil> '\x04' "cdf818194e3a8c32"}
"/test" key has "test set" value

$ etcdctl ls
/test
$ etcdctl get /test
test set

おわりに

ひとまずetcdをgolangから使うことはできたので次はいくつかdockerコンテナを建てて
etcdを使った監視を実際にやってみる。

トランザクションと同時実行制御

はじめに

データベースを扱う上で、無くてはならないトランザクションについてと
それを用いた同時実行制御について「プログラマのためのSQL第4版」を読んで勉強したことをまとめる。


トランザクションと同時実行制御

トランザクションはDBを触る人なら聞いたことがあるだろう。
しかし、それらは大抵DBMSが内部でゴニョゴニョして複雑なことを単純に表現してくれている。
これはDBに限らず大抵の基盤的システムに言えることでもある。
普段は見ることがないけど、それを気にしてみるとその基盤に乗っているもののことを理解しやすくなることがある。
ここではSQLを理解する上でトランザクションという普段はみないものを見てみる。

セッション

ユーザセッションともいうが、これはDBを使うときにパスワードなどを使って接続すると確立されるもので
いったんセッションが確立されるとユーザはその上で権限の範囲でならあらゆるアクセスが可能になる。
そしてこのセッション中では0個以上のトランザクションを実行することができる。

トランザクションとACID特性


トランザクションが必要とする特性がある。それは頭文字を取ってACID特性よぶ

  • 原子性 (Atomicity)
  • 一貫性 (Consistency)
  • 独立性 (Isolation)
  • 耐久性 (Durability)
原子性

原子性は、トランザクション全体が永続的な状態であるかそうでないかという2つの状態のどちらかをとることを指す。つまり何かというと、トランザクションが完全に実行されるかされないかの2つの状態しかないということ。
SQLではCOMMIT文が実行された時に永続化され、ROLLBACK文はトランザクションを破棄しそれが始まる前の状態に戻す。これらは明示的に実行される場合もあるしDB側で処理している場合もある。
よく起こるパターンとして、SQLが何らかのエラーを吐いた時にROLLBACKが行われる。また原子性に従うので、仮に100万行をINSERTする場合に1行でも整合性制約に反していれば全てのINSERTが拒否されて自動的にロールバックされる。

このようにひとつの長いトランザクションを利用する場合はいくつか考慮することがある。
というのも、原子性に従う場合に長いトランザクションは少しのエラーでロールバックされてしまう可能性が捨てきれない。逆にトランザクションの粒度を細かくするとトランザクショントランザクションの間に別の予期しない処理が走ってしまい思わぬ結果になってしまう場合が考えられる。
これを防ぐために、SAVEPOINTというものがある。これはトランザクションの最中に設定することで限定的なロールバックを行うものである。これによりトランザクションの粒度を細かくせずに長いトランザクションを利用することができる。

一貫性

これはトランザクションが開始されたときに、DBは全てのデータ整合性制約や参照整合性制約などを満たしている状態にあるということを指す。
ただし、トランザクション実行中は一貫性が保たれているということは保証されていない。

独立性

これは非常に簡単で、トランザクションは他のトランザクションから独立し、いかなる影響も受けないという性質のことである。これはどういうことかというと、トランザクションは並列に実行しようと直列に実行されたときと同じ結果になるという意味で別名として直列性と言われる。
このあたりはトランザクションの分離レベルで触れる。

耐久性

これはDBのプログラム自体が破損したとしても、DBそのものは保全されるという性質。この性質が満たされている場合、DBは破損したとしても一貫した状態に復元することができる。ロギングとバックアップの機能がこれらを担保している。

同時実行制御

同時実行制御というのは複数のユーザが同時にDBを利用する環境でいかにトランザクションを独立させて処理するかという問題を解決するための手段である。

5つの現象

SQLが全て検索などのREADである場合は無条件にACID特性が満たされているが2人以上が同時にデータを更新作成する場合にはACID特性を満たすように動作させる必要がある。
しかし、実際には5つのパターンにおいてトランザクション同士が干渉してしまう場合がある。

この様に、5つのパターンでトランザクションは干渉する。しかし、これらが全て悪いわけではなくDBを利用するサービスによっては起こることも容認する場合がある。それに全てのパターンを防がないとするとDBは高速に動作する。
そのため実際には分離レベルを考慮してDBを設計する必要がある。

分離レベル

分離レベルとはトランザクションがどの程度干渉を受けるかを設定するのに設定することが多い。
分離レベルは以下の種類がある。

トランザクション分離レベル P1(ダーティリード) P2(ファジーリード) P3(ファントムリード)
SERIALIZABLE o o o
REPEATABLE READ o o x
READ COMMITED o x x
READ UNCOMMITED x x x

SERIALIZABLEは一般的にデフォルトで設定されている。このレベルでは複数のトランザクションが並列に実行されても直列で実行された場合と結果が変わらない。
REPEATABLE READでは同じセッションの間では同じ読み取り結果が保証される。
READ COMMITEDでは同じセッション間でも他のトランザクションが変更しコミットしたデータが読み出される。
READ UNCOMMITEDでは同じセッション間において他のトランザクションがコミットしなくても、変更したデータが見れる。

ただし、分離レベルによらずSQLの文の実行以外では全ての現象は発生しないように動作する。
また何故この3つのパターンのみなのかというと、オリジナルのANSIモデルではこの3つのパターンしか定義されていないからである。

悲観的な同時実行制御

悲観的な同時実行制御というものは、トランザクション同士は衝突するという前提の元に設計される同時実行制御のことを指す。
このような場合は必ずロックを使って排他的アクセスを許可する。ロックメカニズムには色々とありDB2 for z/OSではラッチという仕組みを使って、DB全体をロックしてしまうのではなくテーブルレベルでロックする。こうすることでテーブル全体を対象にするトランザクションではひとつのフラグしか利用しない。
さらに、テーブルを行レベルでロックすれば他のユーザはそのテーブルの他の行を利用することができる。ただしこれは効率を優先するがパフォーマンスは低下してしまう問題を抱えている。
テーブルロックと行ロックの間にあるのがページロックで、これは行のサブセットに対してロックをかける。これは物理ストレージにおいてテーブルがページという単位で実装されていることが多いからこういう名前になっている。

スナップショット分離と楽観的同時実行制御

楽観的同時実行制御はトランザクション同士がそこまで頻繁に衝突しないということを前提に設計されている同時実行制御を指す。基本的には問題が起きてから対処しようという方針になる。これを実現するのにスナップショット分離というものがある。

スナップショット分離では。それぞれのトランザクションはデータをトランザクションが開始された時点のコミット済みのデータのスナップショットから読み込む。この仕組みによりトランザクションは読み込みにおいてブロックされることは無い。ただしこの仕組みによりトランザクションによって作られた複数のバージョンを持つ可能性がある。


実際にはどういう仕組みになっているかというと、トランザクションT1はコミットする準備ができるとコミットタイムスタンプを取得する。これは全ての既存の開始タイムスタンプとコミットタイムスタンプより後の時刻になる。
トランザクションT1のコミットが成功する場合はT1の実行期間にT1が書き込もうとしたのと同じデータを書き込むためのコミットタイムスタンプを取得したT2がない場合になる。つまり、コミット早いもの勝ち戦略によってP4を防止できる。

論理的な同時実行制御

論理的な同時実行制御というのは、SQL文を互いに比較・分析してどのSQL同士であれば同時に実行することが許されるかどうかという観点において設計された同時実行制御のことを指す。
SELECT文であれば、単純な処理になりやすいがそれ以外にデータを操作するSQLの場合は非常に複雑な処理を必要とする。一見すると完璧に見えるかもしれないが、待ち時間の長いSQLをどのようにして処理していくかが次のライブロック問題を引き起こす原因となる。
これを回避するのにもっとも簡単な処理は優先度をつけることだがそれを行うと、重要度の異なる処理を割りこませるという処理が必要になる。これをしてしまうと、優先度が低いが待ち時間の長いSQLが本当に重要なSQLの前に割り込まれてしまうなどの問題を抱えることがある。

デッドロックとライブロック

同時実行制御において真っ先に思いつく困ったこととして、デッドロックがある。これはSQL同士が互いに必要とするリソースを確保してしまい相手にとって必要なリソースを開放できないという問題である。こうなってしまうと、片方のセッションを管理者が切断してロールバックするしか無い。

またライブロックという問題も起こることがある。これはひとつのセッションでリソースを専有する場合に他のセッションがそのリソースの開放を待ち、開放を待っている側がなかなか実行されないという問題である。これも起こってしまうとセッションを切ってロールバックするのが一般的な解決手法としてあげられる。
しかし、いつまでも管理者を待つこともできないのでセッションごとに優先度を決めて実行する場合も存在する。

おわりに

今回はトランザクションと同時実行制御について書いたが、これは普段DBAが責任をもつ領域でアプリケーションなど比較的高いレイヤーではあまり考えない分野になっている。
しかし、これを知っておくのはトラブルの際に役立つし何より学んでいて楽しい部分ではある。

Golangのdatabase/sqlパッケージを使っていく

はじめに

どうも、最近マイクラをかってしまい無限に時間を溶かしているけんつです。

何故かgolangが書きたくなる衝動に駆られているのですがその勢いでORMを自作しようとしたときに色々と謎が深くなってきたので
まず今まで使ってこなかった database/sql パッケージを使ってみようかと思いたったので書きます

参考文献は以下のページ
http://go-database-sql.org/overview.html

Database driver をインポートする

Golang で database/sql パッケージを使ってDBを操作するには必要となるドライバーを自分で持ってくる必要がある。
今回は対象となるDBがMySQLなので、MySQL用のドライバーをもってくる。といってもいくつかあるのでチュートリアル通り "github.com/go-sql-driver/mysql" を持ってくる

$ go get github.com/go-sql-driver/mysql

これでDBにアクセスする準備ができた。

データベースにアクセスする

DBにアクセスするには以下のようなコードを書く。

package main

import (
    "database/sql"
   _ "github.com/go-sql-driver/mysql"
)

func main() {
    //[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]
    db, err := sql.Open("mysql", "sample:sample@tcp(127.0.0.1:3306)/sample")

    if err != nil {
        panic(err)
    }

    defer db.Close()
}

まず、はじめにsql.Openを呼ぶ。
ここでは利用するDBドライバを指定し、DSNを指定の形式に沿って記述する。
DSNでは dbname が require になっているがあとは必要に応じて記述する形となる。

この sql.Open 時に何かしら接続できないなどの事象が発生するとerrが返ってくるためこの部分はエラーハンドリングするべきである。
さらに接続といっても、この関数をコールし正常に終了したところで実はデータベースへの接続は確率されていない。
そのため、さらに以下の様に db.Ping() をコールして疎通確認をするべき(らしい

err = db.Ping()
if err != nil {
    panic(err)
}

加えてこの sql.DB オブジェクトのライフサイクルは長くなるように設計されている。
そのため、Open, Close を頻繁に呼び出すのではなくライフサイクルの短い関数に引数と渡すことが推奨されている(シングルトンみたいな感じ
なぜライフサイクルが長く設計されているかというと、逆に短くしてしまうとネットワークリソースを食いつぶしてしまったりTCPコネクションがTIME_WAIT状態になって残ってしまうなどの問題があるから(らしい

SQLの実行と結果の取得

まず、参考サイトと同じ構成のテーブルを作る。insert文は適当にgoでコード書いてsqlファイルにくっつけた。

CREATE TABLE users (
    id INT NOT NULL AUTO_INCREMENT,
    name varchar(30) NOT NULL,
    PRIMARY KEY (id)
);

INSERT INTO users(`name`) value ('John');
INSERT INTO users(`name`) value ('Rose');
INSERT INTO users(`name`) value ('Jack');
INSERT INTO users(`name`) value ('Mary');
INSERT INTO users(`name`) value ('Olivia');
INSERT INTO users(`name`) value ('John');
INSERT INTO users(`name`) value ('Rose');
INSERT INTO users(`name`) value ('Jack');
INSERT INTO users(`name`) value ('Mary');
INSERT INTO users(`name`) value ('Olivia');

ここまでやったら database/sql パッケージを使ってSQLを実行するコードを次のように追加する。

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "fmt"
)

func main() {

    //[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]
    db, err := sql.Open("mysql", "sample:sample@tcp(127.0.0.1:3306)/sample")
    if err != nil {
        panic(err)
    }

    err = db.Ping()
    if err != nil {
        panic(err)
    }
    defer db.Close()

    var id int
    var name string
    rows, err := db.Query("select * from users where id < ?", 6);
    if err != nil {
        panic(err)
    }
    defer rows.Close()

    for rows.Next() {
        err := rows.Scan(&id, &name)
        if err != nil {
            panic(err)
        }
        fmt.Println(id, name);
    }

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

ひとつずつ解説していく。

まず、db.Open() が正常終了したら db.Query でSQLを実行する。値を入れたいときは ? をつかう。
これにより、そのSQLの結果が行単位かエラーで返ってくるのでハンドリングする。

次にその行の集合は rows.Nextでアクセスできる。
ここで rows.Scan() を使い、結果を変数に代入していく。

そして最後には行に対するイテレータが無事に終了していることを確認するべきらしい。

予めクエリを用意しておく

db.Queryで実行するのではなく予めプレースホルダーを含むSQLなどを用意しておき実行時にその値だけを差し替えるなんていうこともできる。

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "fmt"
)

func main() {

    db, err := sql.Open("mysql", "sample:sample@tcp(127.0.0.1:3306)/sample");
    if err != nil {
        panic(err)
    }

    err = db.Ping()
    if err != nil {
        panic(err)
    }


    stmt, err := db.Prepare("select * from users where id < ?")
    if err != nil {
        panic(err)
    }
    defer stmt.Close()

    var (
        id int
        name string
    )
    rows, err := stmt.Query(3)
    for rows.Next() {
        err := rows.Scan(&id, &name)
        if err != nil {
            panic(err)
        }
        fmt.Println(id, name)
    }
    defer rows.Close()

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

さいごに

とりあえず、基本的な部分はまとめたので次はトランザクション周りをまとめていきたい。