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"が三回表示されるのかというと、これはGolangのGCの話でゴルーチン内で該当する変数が使用されていると判断されスライスの最後の要素である"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型は並行処理内でアクションの通知以外にもキャンセルした理由やデッドラインの有無などという状態を付加するために使用される。
詳しい説明は公式ドキュメントに記載されているので気になる場合は確認して欲しい。
次からはよく使う関数やメソッド群を実例を踏まえてまとめていく。
実例
今回使用したコード↓
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
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を使った監視を実際にやってみる。
*1:etcd - Linux入門 - http://kaworu.jpn.org/linux/etcd
*2:gRPC - https://grpc.io/
トランザクションと同時実行制御
トランザクションと同時実行制御
トランザクションは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つのパターンにおいてトランザクション同士が干渉してしまう場合がある。
- P0(ダーティーライト):トランザクションT1がデータを変更するときに、別トランザクションT2がT1完了以前に同じデータを変更してしまう場合に不整合が起きる。この場合、どちらかのトランザクションがロールバックすると正しいデータが何か不明確になってしまう。これは一貫性を損なう。
- P1(ダーティーリード):トランザクションT1がデータを変更し確定していないときにトランザクションT2がそのデータを読んでしまうこと。
- P2(ファジーリード):トランザクションT1がある行を参照する場合にそのトランザクションが終了する前にトランザクションT2が変更を加える。この状態でT1がロールバックするとT2の変更がかかったあとで変更後の値をとるか行そのものが存在しない。
- P3(ファントムリード):トラザクションT1が検索結果としてN行の行を読みだす。トランザクションT2がその条件に合う行を挿入すると、同じ条件でも前に実行された結果とは異なる結果が取得されること。
- P4(ロストアップデート):トランザクションT1がデータを読みだし、トランザクションT2がそのデータを更新する。その後、T1がデータを更新するとT2の更新は失われる。
この様に、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&...¶mN=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&...¶mN=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) } }
さいごに
とりあえず、基本的な部分はまとめたので次はトランザクション周りをまとめていきたい。
MySQLで大量のテストデータを用意したい
はじめに
どうも、最近みたハクソーリッジという映画が最高に琴線に響いて何度も見ているけんつです
最近、データベース周りの勉強をしてみようと思ったのですが何かが足りないことに気が付きました。
そう大量のテストデータです。
なので今回は作ります。
環境構築
まずは環境を構築する。
基本的にはMySQLの話になるけども、インフラの勉強をするとき用の環境を後に整える予定なのでdocker-composeを使う。
version: '3' services: mysql: image: mysql:5.7 environment: - MYSQL_ROOT_PASSWORD=root - MYSQL_USER=sample - MYSQL_PASSWORD=sample - MYSQL_DATABASE=sample volumes: - ./docker/mysql:/var/lib/mysql ports: - "3306:3306"
こんな感じでハイパー雑にymlを用意する。
$ docker-compose up -d $ mysql -h 127.0.0.1 -u sample -D sample -p Enter password: Welcome to the MariaDB monitor. Commands end with ; or \g. Your MySQL connection id is 4 Server version: 5.7.20 MySQL Community Server (GPL) Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. MySQL [sample]>
これでログインできた。
次にinit.sqlという名前でマイグレーションを用意する
CREATE TABLE tweets ( uid int default 0, user varchar(255) not null, body varchar(255) not null, liked int default 0, retweet int default 0, include_reply boolean default false, created_at datetime default current_timestamp );
ツイートに見立てて簡略的に作成してみた。
ここまでが環境構築
大量のデータをどうにかして用意する
じゃあ早速用意する
<?php declare(strict_types=1); //user name list $names = ['Jack', 'John', 'Smith', 'Noah', 'Harry', 'Sophia', 'Emma', 'Olivia', 'Ava', 'Isabella']; //generate random body function generateRandBody() : string { $result = ''; $limit = rand(1, 100); for($i = 0; $i < $limit; $i++) { $result .= chr(mt_rand(65, 90)); } return $result; } $limits = 10; $count = 0; for (; $count < $limits; $count+=1) { $user = $names[$count%10]; $body = generateRandBody(); $liked = rand(0, 1000000000); $retweet = rand(0, 1000000000); $include_reply = ($count%2 == 0) ? 0 : 1; $sql = "INSERT INTO tweets (`user`, `body`, `liked`, `retweet`, `include_reply`) value ('".$user."','".$body."',".$liked.",".$retweet.",".$include_reply.");"; echo $sql . "\n"; }
こんなスクリプトを組んでいて思ったのだが、当初はこのスクリプトから挿入させようと思ったがまともに1000万件とか挿入するとめちゃくちゃ遅い。
というわけで、↑はもうすでに変更したものだが最初の10件だけ登録してそれをひたすら単純結合させて増加させてしまおうという作戦に出た。
init.sql↓
CREATE TABLE tweets ( uid int primary key auto_increment, user varchar(255) not null, body varchar(255) not null, liked int default 0, retweet int default 0, include_reply boolean default false, created_at datetime default current_timestamp ); INSERT INTO tweets (`user`, `body`, `liked`, `retweet`, `include_reply`) value ('Jack','CSHMFFHXYIAY',101262607,788244618,0); INSERT INTO tweets (`user`, `body`, `liked`, `retweet`, `include_reply`) value ('John','BNYPLLSXIFXZQWZSYISTIVUDXICZZJDDTTBHUO',455425608,660218612,1); INSERT INTO tweets (`user`, `body`, `liked`, `retweet`, `include_reply`) value ('Smith','ZVMAHSWKBYYVNDJYVCIINSTYUELEUNVDJKETJGZIAITPJJSCEPLFRTDJFDVMYKVG',155967009,720719955,0); INSERT INTO tweets (`user`, `body`, `liked`, `retweet`, `include_reply`) value ('Noah','KIDUVZVHTQEIEHJAWRFOTXMERFASBVFNPHSSTSEMAUDPVRGHCSOJFMQHRLEFGAYPOQPKYBKBHEOXOFUERGOK',717252756,32221742,1); INSERT INTO tweets (`user`, `body`, `liked`, `retweet`, `include_reply`) value ('Harry','NEZXVIAVARL',361169251,640449508,0); INSERT INTO tweets (`user`, `body`, `liked`, `retweet`, `include_reply`) value ('Sophia','NBPOFOVUXPFYWGTLKIYFUUOPUGJMVWKENTEKJDKAKJLDHMFEHSYSXOLWCKLWJ',587239726,530171748,1); INSERT INTO tweets (`user`, `body`, `liked`, `retweet`, `include_reply`) value ('Emma','IIHSNNHUQIPPXLNHEKLSLN',334524943,69008302,0); INSERT INTO tweets (`user`, `body`, `liked`, `retweet`, `include_reply`) value ('Olivia','BHLZISBQELBLJVFZBTHBJUKAHILWJXKRHLAPYBMHXBPTSPFGVFGGLXTAEKUHMHAFHYAWQKYZ',779212237,102552681,1); INSERT INTO tweets (`user`, `body`, `liked`, `retweet`, `include_reply`) value ('Ava','BUPFCEDTZEQQEBUFEUVDZLWKTNWRKZBLTWLXIWGXWNYEYNEHHRDLYWFAMFMEZQXCZXVSAAJWRTNFXRHYUKWPWAQCXM',40163778,286463918,0); INSERT INTO tweets (`user`, `body`, `liked`, `retweet`, `include_reply`) value ('Isabella','OQOUHLPDOWJNRIKOYXOOYUEPEVQQENEKZFYZJBEKQPO',763927989,187392711,1); SOURCE increase.sql;
increase.sql ↓
insert into tweets ( select tweets.uid = null, tweets.user, tweets.body, tweets.liked, tweets.retweet, tweets.include_reply, tweets.created_at from tweets, tweets tweets2, tweets tweets3, tweets tweets4, tweets tweets5, tweets tweets6);
このようにあらかじめ用意した10個のレコードを単純結合させることで膨大なデータを用意する
そしてこれらのファイルを実行するとこうなる。
MySQL [sample]> source init.sql; Query OK, 0 rows affected (0.51 sec) Query OK, 1 row affected (0.09 sec) Query OK, 1 row affected (0.06 sec) Query OK, 1 row affected (0.11 sec) Query OK, 1 row affected (0.11 sec) Query OK, 1 row affected (0.06 sec) Query OK, 1 row affected (0.06 sec) Query OK, 1 row affected (0.05 sec) Query OK, 1 row affected (0.08 sec) Query OK, 1 row affected (0.05 sec) Query OK, 1 row affected (0.06 sec) Query OK, 10000000 rows affected (5 min 13.20 sec) Records: 10000000 Duplicates: 0 Warnings: 0 MySQL [sample]> source datasize.sql +------------+--------+----------+------+--------+---------+----------+ | table_name | engine | tbl_rows | rlen | all_mb | data_mb | index_mb | +------------+--------+----------+------+--------+---------+----------+ | tweets | InnoDB | 9706337 | 100 | 929 | 929 | 0 | +------------+--------+----------+------+--------+---------+----------+ 1 row in set (0.00 sec)
1000万行程ある、900MB以上の容量を持つテーブルが作成できた。
何故か、30万レコード程損失があるとなっているがselect * from では1000万行あることになっている理由は謎これから調べてみる
おわりに
とりあえず、アホみたいにデカイDBは作れたのでこれを元にいろいろやっていく