Learning Go Chapter 11. The Standard Library

The Standard Library

  • Go の標準ライブラリは Python の "batteries included" の哲学と同じで、アプリケーション開発に必要なライブラリを標準装備している

io とその仲間たち

  • Goの入出力哲学の中心は、ioパッケージにある
    • ReaderWriter インタフェースはよく使われるインタフェース
// スライスを入力にして、直接変更される
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}
func counteLetters(r io.Reader) (map[string]int, error) {
    buf := make([]byte, 2048)
    out := map[string]int{}

    for {
        n, err := r.Read(buf)
        for _, b := range buf[:n] {
            if (b >= 'A' && b < 'Z') || (b >= 'a' && b < 'z') {
                out[string(b)]++
            }
        }

        if err == io.EOF {
            return out, nil
        }

        if err != nil {
            return nil, err
        }
    }
}
  • 注意すべき点が3つ
    • 1.バッファを1回作成し、r.Readを呼び出すたびに再利用する
      • これにより、単一のメモリ割り当てを使用して、潜在的に大きなデータソースから読み取ることが可能
      • Readメソッドが[]バイトを返すように記述されている場合、呼び出しごとに新しい割り当てが必要になる
      • 各割り当てはヒープ上で終了するため、ガベージコレクターにとって非常に多くの作業が必要
      1. r.Readから返されたn値を使用して、バッファーに書き込まれたバイト数を確認し、bufスライスのサブスライスを反復処理して、読み取られたデータを処理する
      1. r.Readから返されたエラーがio.EOFの場合、rからの読み取りが完了したことがわかる
      2. このエラーは、実際にはエラーではない
      3. これは、io.Readerから読み取るものが何も残っていないことを示す
      4. io.EOFが返されると、処理が終了し、結果が返される
func testCountLetter() error {
    s := "The quick brown fox jumped over the lazy dog"
    sr := strings.NewReader(s)

    counts, err := countLetters(sr)
    if err != nil {
        return err
    }
    fmt.Println(counts)

    return nil
}
  • io.MultiReader: 複数のio.Readerインスタンスから次々に読み取るio.Readerが返される
  • io.LimitReader: 指定されたio.Readerから指定されたバイト数までしか読み取らないio.Readerが返される
  • io.MultiWriter: 複数のio.Writerインスタンスに同時に書き込むio.Writerが返される

  • 他によく使うインターフェース

    • 大抵 Close メソッドは defer と一緒に使われる
type Closer interface {
    Close() error
}

type Seeker interface {
    Seek(offset int64, whence int) (int64, error)
}
  • io.Seekerインターフェースは、リソースへのランダムアクセスに使用される

    • whenceの有効な値は、定数io.SeekStart、io.SeekCurrent、およびio.SeekEnd
    • ただし、whenceはint型
  • io パッケージは上記のインタフェースを組み合わせ様々なインタフェースを定義する

    • io.ReadCloser
    • io.ReadSeeker
    • io.ReadWriteCloser
    • io.ReadWriteSeeker
    • io.ReadWriter
    • io.WriteCloser
    • io.WriteSeeker
  • ioutilパッケージは、io.Reader実装全体を一度にバイトスライスに読み取る、ファイルの読み取りと書き込み、一時ファイルの操作などのためのいくつかの簡単なユーティリティを提供する

    • ioutil.ReadAll、ioutil.ReadFile、およびioutil.WriteFile関数は、小さなデータソースには適している
  • 大きなデータソースを操作するには、bufioパッケージのReader、Writer、およびScannerを使用する

time パッケージ

  • time.Duration と time.Time がメインで使用する型
  • time パッケージで指定する時刻フォーマット文字列は、フォーマット文字列ではなく、以下のような具体的な日時を指定する
// 第一引数がフォーマット文字列
    t, err := time.Parse("2006-02-01 15:04:04 -0700", "2009-13-01 13:04:04 -0700")
    if err != nil {
        fmt.Println("Error!")
    }
  • これは、1から7まで順番に書くと以下のようになるから
01/02 03:04:05PM ’06 -0700
  • 書式設定に使用される日付と時刻は巧妙なニーモニックを目的としていますが、覚えるのが難しく、使用するたびに調べる必要がある
    • 幸いなことに、最も一般的に使用される日付と時刻の形式には、時間パッケージで独自の定数が与えられている

Monotonic Time

  • ほとんどのオペレーティングシステムは、現在の時刻に対応する壁時計と、コンピューターの起動時から単純にカウントアップする単調時計の2種類の時刻を追跡する

    • 2つの異なる時計を追跡する理由は、掛け時計が均一に増加しないため
    • 夏時間、うるう秒、およびNTP(Network Time Protocol)の更新により、掛け時計が予期せず前後に移動する可能性がある
    • これにより、タイマーを設定したり、経過時間を確認したりするときに問題が発生する可能性がある
  • この潜在的な問題に対処するために、Goは単調な時間を使用して、タイマーが設定されているとき、またはtime.Timeインスタンスがtime.Nowで作成されるたびに経過時間を追跡する

    • このサポートは目に見えない
    • Subメソッドは、モントニッククロックを使用してtime.Durationを計算する(両方のtime.Timeインスタンスに設定されている場合)
    • そうでない場合(インスタンスの一方または両方がtime.Nowで作成されなかったため)、Subメソッドはインスタンスで指定された時間を使用してtime.Durationを計算する

encoding/json

メタデータを追加するために構造体タグを利用する

  • 構造体タグは構造体メンバの型名の後にバックスラッシュで囲った文字列で指定する
    • バックスラッシュの中身は tagName:"tagValue"のように指定する
    • マーシャリングまたはアンマーシャリング時にフィールドを無視する必要がある場合は、名前にダッシュ(-)を使用します。フィールドが空のときに出力から除外する必要がある場合は、名前の後に、omitempty を追加する

    • 残念ながら、「空」の定義は、ご想像のとおり、ゼロ値と正確に一致していません。構造体のゼロ値は空としてカウントされませんが、長さゼロのスライスまたはマップは空としてカウントされる

type Order struct {
    ID          string    `json:"id"`
    DateOrdered time.Time `json:"date_ordered"`
    CustomerID  string    `json:"customer_id"`
    Items       []Item    `json:"items"`
}

type Item struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}
  • json.Unmarshal関数は、io.Readerインターフェースの実装と同様に、データを入力パラメーターに入力とする
    • これには2つの理由がある
      • まず、io.Readerの実装と同様に、これにより同じ構造体を何度も効率的に再利用できるため、メモリ使用量を制御できる
      • 第二に、それを行う他の方法がない
        • Goには現在ジェネリックがないため、読み取られるバイトを格納するためにインスタンス化する必要があるタイプを指定する方法がない

データ指向アプリケーションデザイン第9章: 一貫性と合意

ch09 一貫性と合意

  • 耐障害性をもつシステムを構築する最も最善の方法は、有益な保証をもつ汎用的な抽象概念を見出し、それを一度だけ実装し、アプリケーションをその保証に依存させること

    • 例えば、トランザクション
      • アプリケーションはクラッシュすることなく(原子性)
      • データベースが並行にアクセスされることなく(分離性)
      • ストレージデバイスは完璧な信頼性をもつ(永続性)
      • かのように振る舞える
    • トランザクションがクラッシュ、レース条件、ディスク障害などのフォールトを隠蔽化し、アプリケーションが気にする必要がなくなる
  • 分散システムにおいても、分散システムにおける問題をアプリケーションがいくつか無視できるようにしてくれる抽象概念がある

    • 例えば、合意 (consensus): すべてのノードが何かについて同意すること

一貫性の保証

  • レプリケーションを行うデータベースのほとんどは少なくとも結果整合性を提供する

    • ある程度待てば最終的には読み取りリクエストに対してすべて同じ結果を返すようになること
    • 収束性という言葉の方が合っている
  • 分散一貫性モデルとトランザクション分離モデル

    • トランザクション分離モデル: 並行してトランザクションを実行することから生じるレース条件を避けることに主眼
    • 分散一貫性モデル: 遅延やフォールトに際してレプリカの状態を調整すること

線形化可能性

  • 考え方の背景

    • 同時に異なる二つのレプリカにクエリを投げると異なる結果が返ってくることがある(結果整合性)
    • データベースがレプリカが一つしかないように見せる方がシンプル
    • クライアントはデータを同じように見ることになり、レプリケーションラグを気にする必要がない
  • 線形化可能性

    • 原子的一貫性、強い一貫性、即時一貫性、外部一貫性と呼ばれることがある
    • 線形化可能性とは最新性の保証 (recency guarantee) を意味する
      • データベースから読み取りを行う場合、読み取られる値が最新であり、古くなったキャッシュやレプリカからの値でないことを保証する

システムを線形化可能にする条件

  • 3 つのクライアントが読み書きする例でどういう条件が必要か考える
  • パターン 1:

f:id:tmks0820:20210708114022p:plain

  • レジスタに対する操作

    • read(x) => v: クライアントがレジスタ x の値の読み取りをリクエストし、データベースが値として v を返したという意味
    • write(x, v) => r: クライアントがレジスタ x に値 v を設定し、データベースがレスポンスとして r (ok or error) を返したという意味
  • 線形可能性である場合のレスポンス

    • クライアント A の最初の読み取り => 間違いなく 0 が返ってくる
    • クライアント A の最後の読み取り => 書き込みが完了した後に始まっているので間違いなく 1 が返ってくる
    • 書き込み処理と重なって実行された読み取り => 0 または 1 が返ってくる
  • これだけだと線形可能性であるための条件としては不十分

    • 読み取りと書き込みが並行している場合に、新旧の値のいずれかが返ってくるとなると、読み取りのたびに異なる値が返ってくる可能性がある
    • これはデータのコピーが一つしかなく、常に最新の値が返ってくるという線形可能性に違反する
  • そこで制約を加える

f:id:tmks0820:20210708114104p:plain

  • どこかの時点で x の値が 0 から 1  にアトミックに変更され、あるクラインアントの読み取りに対して新しい値 1 が返されたら、 それ以降の読み取りはすべて新しい値を返さないといけない

  • もっと複雑な例

f:id:tmks0820:20210708114116p:plain

  • 追加の操作

    • cas(x, v_old, v_new) => r: クライアントがアトミックな compare-and-set 操作を要求したという意味
  • 線形化可能にするためには、操作のマーカーを連結する線が常に時間軸の中で前に進み、決して後ろに戻らないこと

    • 操作のマーカー(垂直線):データベースが処理を実行したタイミング
    • このモデルはトランザクションの分離は想定されていないため、値はいつでも他のクライアントによって書き換えられる
  • 線形化可能な直感イメージは上記の通りだが、形式的な定義も存在する。この定義にしたがっているかどうかテストすることも可能だが、演算負荷が非常に高い。

線形化可能性と直列化可能性

  • 直列化可能性

  • 線形化可能性

    • レジスタの読み書きにおける最新性の保証(ここのオブジェクトに関すること)
  • データベースは、直列化可能性と線形化可能性を同時に提供できる

    • 厳密な直列化可能性、強い単一コピーの直列化可能性と呼ばれる
    • 2PL による直列化可能性の実装や完全順次実行の実装はこれにあたる
  • 直列化可能なスナップショット分離 (SSI)は、線形化可能ではない

線形化可能性への依存

  • 線形化可能性が役立つ場面は?

ロックとリーダー選出

  • 単一リーダーのレプリケーションを利用するシステムは、単一のリーダーしかいないことを保証する必要がある
    • リーダー選出の方法の一つはロックを使うこと
    • このロックは線形化可能でなければならない

制約及びユニーク性の保証

  • ユーザー名、メールアドレス、ファイルストレージサービスのファイルパスに対するユニーク性
  • これらを書き込まれる際に保証したいなら線形化可能性でなければならない

クロスチャネルタイミングの依存関係

  • メッセージキューとファイルストレージのように二つの異なる通信チャネルが合った場合に、線形化可能性の最新性の保証がない場合にレース条件が発生する

線形化可能なシステムの実装

  • データのコピーが一つしかないように振る舞い、データに対するすべての操作はアトミックであるというのが線形化可能性

    • 本当にデータのコピーを一つにすると、障害があればデータが失われてしまう
    • なのでレプリケーションを使って、データを複数のホストに複数コピーする方法がある
    • レプリケーション + 線形化可能性をどうやって実現するのか?
  • シングルリーダーレプリケーション(潜在的に線形化可能)

  • 合意アルゴリズム(線形化可能)
  • マルチリーダーレプリケーション(線形化可能ではない)
  • リーダーレスレプリケーション(おそらく線形化可能ではない)

  • 線形化可能性とクオラム

    • 厳密なクオラムでも線形化可能にできない
    • パフォーマンス低下を受け入れることができるのであれば、Dynamo スタイルのクオラムを線形化可能にすることはできる
      • 読み取りの際にはアプリケーションに結果を返す前に読み取り修復を同期的に行う
      • 書き込みの際には書き込みの送信前にノードのクオラムの最新状態を読み取れるようにする

f:id:tmks0820:20210708114130p:plain

線形化可能にすることによるコスト

  • アプリケーションに線形可能化が必須である場合、一部のレプリカが他のレプリカからネットワークの問題で切り離されてしまったら、 切り離されているレプリカは利用できない(ネットワークの問題が解消されるのをまつか、エラーを返す)
  • アプリケーションに線形可能化が必須でない場合、レプリカが切り離されてしまったとしても、それぞれのレプリカが独立してリクエストを処理できるような方法で書き込みを受け付けることができる

    • ネットワークの問題が生じても利用できるが、その動作は線形化可能ではない
  • CAP 定理の対象範囲は非常に狭い

    • 1 つの一貫性モデル(線形化可能性)と 1 種類のフォールト(ネットワーク分離)だけを対象とする

線形化可能性とネットワーク遅延

  • 線形化可能性を持っているシステムは少ない...

    • マルチコア CPU の RAM でさえ線形化可能ではない
      • メモリバリア、フェンスが使用されないかぎり...メモリからの読み取りの最新性は保証されない
  • なぜ線形化可能性をもつシステムが少ないのか

    • ひとえにパフォーマンス影響のせい。耐障害性よりもパフォーマンスを重視するケースの方が多いため、線形化可能性を捨てて、 パフォーマンス向上を図る
      • 線形化可能にすると、そうしない場合と比較すると必ず読み書きのパフォーマンスが悪化する

順序の保証

  • 順序は重要な概念
    • シングルリーダーレプリケーションの場合、リーダーの主な目標はレプリケーションログにおける書き込みの順序の決定
    • 直列化可能性はトランザクションがあたかも何らかの順序にしたがって順次実行されたかのように振舞うことを保証する
    • タイムスタンプやクロックを利用して、二つの書き込みの順序を決定する

順序と因果関係

  • 順序は因果関係を保つのに役立つ
  • 因果律はイベント間に順序関係を発生させる
    • 原因は結果よりも先に生じていなければならない
    • システムが因果律から導かれる順序づけに従うのであれば、そのシステムは因果律において一貫している (casually consistency)

因果律に基づく順序と全順序の違い

  • 全順序があれば任意の二つの要素を比較できるので、必ず大小関係を判断できる

    • 自然数 -> 全順序: 5 は 13 より小さく、13 は 5 より大きい
    • 数学における集合 -> 全順序ではない: { a, b } は {b, c} より大きい...?
      • -> 比較不能
      • 半順序: 片方の集合が他の集合の全要素を含んでいる場合には、その集合は他の集合よりも大きいと判断できる
  • 線形化可能性: 操作に全順序がある

  • 因果律

    • 二つの出来事の間に因果関係があるなら、必ず順序関係がある
    • ただし、それらが並行に行われているなら、比較不能
    • つまり、因果律は半順序を定義する
      • 操作によっては順序付けられるが、比較不能な場合もある
  • 線形化可能は因果律を暗に含む

    • ただし、線形化可能はパフォーマンス、可用性に影響を与える
    • 因果律がほしいだけの場合に線形化可能なシステムはオーバー?
      • 因果律だけがほしい(線形化可能まではいらない)場合には別の方法もある

因果律における依存関係の補足

  • 因果律を保持するためには、ある操作に対してどの操作が先行したのかを知る必要がある
    • 因果律における依存関係を判断するためには、システム中のノードの知識を記述する何らかの方法が必要
    • 依存関係の順序を判断するためには、データベースはアプリケーションが読み取ったデータのバージョンを知っていなければいけない
      • SSI における衝突検出でも同様の考え方が適用される

シーケンス番号の順序

  • シーケンス番号、タイムスタンプを使ってイベントの順序づけを行うことができる

    • 物理クロックでなくて良い。論理クロック(操作を特定するための数値の並びを生成するアルゴリズム)で問題ない。
  • シングルリーダーレプリケーションの場合には、単調増加するシーケンス番号をレプリケーションログ中の各操作に割り当てられる

因果的でないシーケンス番号生成器

  • シングルリーダーではない、マルチリーダーやリーダーレスの場合、シーケンス生成は以下のようになる

    • 各ノードが独立して、自分用のシーケンス番号の集合を生成できる(A ノードは偶数、B ノードは奇数等)
    • 24 時間制のクロックから取得したタインプスタンプを各操作に割り当てる(LWW)
    • あらかじめ、シーケンス番号のブロックを割り当てておく(A ノードは 1-1000、B ノードは 1001 - 2000 等)
  • カウンタをインクリメントする方法よりもスケーラビリティがあり、パフォーマンスが高いかもしれない...

    • ただし、これらのシーケンス番号生成器は因果律に対する一貫性を持たない
      • 偶数カウンタと奇数カウンタでどちらが先の番号なのかは判断できない
      • 物理クロックから取得するタイムスタンプはクロックのスキューに影響される
        • 因果律としては後から行われた操作がタイムスタンプ上先の値が割り当てられるときがある
  • ランポートタイムスタンプ(lamport timestamp)

    • 一貫性をもつシーケンス番号を生成するシンプルな方法
    • 物理的なクロックには全く関係がない
    • 2 つのタイムスタンプがあるとき、カウンタの値が大きい方が大きいタイムスタンプ
    • カウンタの値が同じである場合ノード ID が大きい方が大きなタイムスタンプ
    • すべてのクライアントが過去に見た最大のカウント値を追跡し、その値をすべてのリクエストに含めるようにする
      • あるノードが受信したリクエストもしくはレスポンスに含まれるこの値がそのノード自身のカウンタ値よりも大きかったら、そのノードはすぐに自分のカウンタ値を最大値にする

f:id:tmks0820:20210708114147p:plain

  • ただし、全順序があるだけでは不十分なケースがある
    • 例えばユーザ名に対するユニーク制約のようなものを実装する場合
      • 順序がいつまでに確定するのかがわからなければならない
        • 同じユーザ名を全順序中でその操作よりも先に要求しているノードが他にはないことが確実になってはじめて、その操作が成功したと安全にいえる

全順序のブロードキャスト

  • シングルリーダーレプリケーションは、リーダーの単一の CPU コアですべての操作を並べることによって全順序を保証する

    • リーダーに障害があった場合にはどうする?
    • 単一ののリーダーで処理できる以上のスループットだった場合は?
  • これらの問題は全順序のブロードキャスト、アトミックブロードキャストと呼ばれる

  • 全順序のブロードキャストは通常ノード間のメッセージ交換プロトコルとして記述される

    • 非形式的には二つの安全性が常に満たされていなければならない
      • 信頼できる配信: メッセージがロストしてはいけない
      • 全順序づけされた配信: メッセージはすべてのノードに同じ順序で配信されなければならない
  • 全順序ブロードキャストの利用

    • 全順序ブロードキャストは、etcd や ZooKeeper のような合意サービスは全順序ブロードキャストを実装している
    • 全順序ブロードキャストはレプリケーションに必要不可欠
  • 全順序ブロードキャスト = 線形化可能な CAS = 合意

分散トランザクションと合意

  • 合意は分散コンピューティングにおいて最も基本的で重要な問題
  • 合意は難しい。少なくとも以下のことを理解していないといけない。

  • ノード群の合意をとることが重要な状況

    • リーダー選出
    • アトミックコミット
  • FLP 帰結(合意の不可能性)

    • ノードがクラッシュするリスクがあるならば、常に合意を達することができるアルゴリズムは存在しないという証明
    • FLP 帰結は非同期のシステムモデルを前提としている

アトミックなコミットと 2 相コミット

  • トランザクションの原子性: 複数の書き込みの途中で問題が生じた場合のシンプルなセマンティクス
    • 失敗する(中断する)か、成功するかのどちらか
    • データベースに中途半端な状態の結果を残さない
    • セカンダリインデックス(主たるデータとは別のデータ構造)と主たるデータの一貫性も保つ

単一ノードから分散アトミックコミットへ

  • 単一ノードの実行されるトランザクションは、原子性は一般にストレージエンジンによって実装される
  • 単一ノードのデータベースにおける永続性の提供の仕組み

  • 単一ノード上では、トランザクションのコミットはデータが永続性を持ってディスクに書き込まれる順序に依存している

    • データの書き込み -> コミット

2 相コミット(2PC)

  • コーディネータ(トランザクションマネージャー)が登場する
  • アプリケーションがコミットできる準備が整ったら、コーディネータはフェーズ 1 を開始する

    • コーディネータは準備リクエストを各ノードに送信し、それらがコミットできるかを問い合わせる
    • コーディネータは参加者からのレスポンスを追跡する
  • すべての参加者が yes を返し、コミットの準備が整っていることを示してきたら、コーディネータはフェーズ 2 でコミットリクエストを送信する => 実際にコミットが行われる

  • いずれかの参加者が no を返したら、コーディネータはフェーズ 2 ですべてのノードに中断リクエストを送信する

2PC 詳細
  1. 分散トランザクションを開始したい場合、アプリケーションはコーディネータに対してグローバルユニークなトランザクション ID を要求する
  2. アプリケーションは、各参加者上で単一ノードのトランザクションを開始し、それらのトランザクションに対してグローバルユニークなトランザクション ID を添付する
  3. アプリケーションコミットの準備が整ったら、コーディネータはグローバルなトランザクション ID をタグづけした準備のリクエストをすべての参加者に送信する

  4. 準備のリクエストを受信した参加者は、いかなる環境下でもそのトランザクションを間違いなくコミットできることを確認する

    • トランザクションの全データをディスクに書き込めること、制約違反の有無のチェックを行う
  5. コーディネータは準備のリクエストに対するレスポンスのすべてを受け取ったら、トランザクションをコミットするか中断するか最終的に判断する

    • コーディネータはコミットポイント(クラッシュが生じたとしてもコミットするか中断するかを下した判断がわかるようにするポイント)を残す
  6. コミットポイントの判断がディスクにかかれたら、コミットもしくは中断のリクエストがすべての参加者に送られる

    • このリクエストが失敗したり、タイムアウトしたりした場合、コーディネータは成功するまで永遠にリトライし続ける

コーディネータの障害

  • もしコーディネータに障害が発生した場合は、参加者はコーディネータのリカバリを待つこと以外に 2PC を完結させる方法はない
    • 参加者同士で通信して互いの状況を知って何らかの同意を得るということは可能だが、2PC にはこうしたプロトコルはない

スリーフェーズコミット

  • 2 相コミットはブロッキングアトミックコミットプロトコルと呼ばれる

    • これは 2PC がコーディネータのリカバリを持って行き詰まってしまう場合があることからきている
  • 2PC に代るものとして 3PC が提唱された

    • ただしネットワークの遅延に上限があり、ノードのレスポンスタイムにも上限があることを想定している
      • ので現実的なシステムでは 3PC で原子性を保証することはできない

分散トランザクションの実際

  • 2PC はパフォーマンス上のペナルティを伴うケースが多い

    • 例えば、 MySQL における 2PC は単一ノードのトランザクションと比べて 10 倍遅いという報告がある
    • その原因は、リカバリに必要なディスクへの強制書き込み (fsync)の増加と、ネットワークラウンドトリップの増加
  • 分散トランザクションとは何か?2 種類ある

exactly-once なメッセージ処理

  • ヘトロジニアスな分散トランザクションは多様なシステムを結合する
    • メッセージキューからきたメッセージを、メッセージを処理するデータベーストランザクションが成功した場合にかぎり、処理済みとして承認する、といったことができる
      • メッセージの配信、データベーストランザクションのどちらかが失敗したら、両方中断されるので再配信できる

XA トランザクション

未確定状態中のロックの保持

  • 未確定状態で行き詰まったトランザクションは、ロックの影響を受ける
    • Read Commited や Serializable な分離性を提供する場合、ロックを保持する。これはトランザクションが中断する、もしくはコミットされるまで保持され続ける
    • 未確定状態をそのままにしておくと、ロックされている部分にアクセスしようとしているすべてのトランザクションに影響を与えてしまい、アプリケーションの大部分が利用できなくなってしまう可能性がある

コーディネータからの障害のリカバリ

  • orphaned 未確定トランザクションが生じる可能性がある
    • トランザクションログが失われてしまったり、ソフトウェアのバグによって壊れてしまった場合
    • 自動で解決はできない
    • 2PC の正しい実装は、たとえ再起動をまたいだとしても未確定のトランザクションのロックは保持し続けなければならないので、データベースの再起動でも解決されない
    • 解決策は管理者が手作業で、未確定の各トランザクションの参加者を調べて、すでにコミット、ロールバックされている参加者があるかを確認し、同じ結果を他の参加者にも適用する

分散トランザクションの限界

  • コーディネータは一種のデータベース扱い
    • 単一マシン上でコーディネータが動作している場合にはシステム全体にとっての単一障害点になる
    • アプリケーションサーバはステートレスで、永続化すべきステートはデータベースに保存することがほとんど。コーディネータはアプリケーション側のライブラリとして提供されることがほとんどなので、ステートレスなアプリケーションサーバにステートが持ち込まれることになる
    • SSI と共に動作することはできない
    • 一つの参加者が壊れたら、他のすべてに影響が及ぼされる(つまり、XA トランザクションは障害を増加する傾向にある)

耐障害性をもつ合意

  • 合意とは、複数のノードが何かについて同意すること
  • 合意の問題は以下のように形式化される

    • 1 つ以上のノードが値を提案 (propose)する
    • それらの値の中から一つを決定 (deside)する
  • 合意が満たさなければいけない性質

    • 一様同意 (uniform agreement)
      • 2 つのノードが異なる決定をしていないこと
    • 整合性 (integrity)
      • 2 回決定をしているノードがないこと
    • 妥当性 (validity)
      • ノードが値 v を決定したら、v を提案しているノードがあること
    • 終了性 (termination)
      • クラッシュしていないすべてのノードは最終的に何らかの値を決定すること
  • 合意のシステムモデル

    • クラッシュしたノードは突然消え去り、決して戻ってこないことを前提としている
      • 2PC は終了性の要件を満たすことができない

合意アルゴリズムと全順序ブロードキャスト

  • 耐障害性をもつ合意アルゴリズムとして最も広く知られているもの

    • VSR (Viewstamped Replication)
    • Paxos
    • Raft
    • Zab
  • これらのアルゴリズムは、一様同意/整合性/妥当性/終了性という同意の形式的性質を直接利用せずに、値の並びを決定することによって全順序ブロードキャストになっている

    • 全順序ブロードキャストは、メッセージが厳密に一度だけ、同じ順序でメッセージがすべてのノードに配信されなければならない
    • 全順序ブロードキャストは、複数回に渡って合意をとることと等価

シングルリーダーレプリケーションと合意

  • リーダーの選出を自動で(管理者の手を介在することなく)行う場合には、合意が必要

エポック番号とクオラム

  • 合意プロトコルでは、内部的に何らかのリーダーを使っているがユニーク性は保証していない。その代わりに弱い保証を与える

    • エポック番号
      • Paxos の場合: 投票番号
      • VSR の場合: ビュー番号
      • Raft の場合: 期間番号
  • 現在のリーダーが落ちたと考えられるたびに、ノード間で新しいリーダーを選出するための投票が始まる

    • この選出に対してインクリメントされるエポック番号が与えられる
      • エポック番号は全順序をもち、単調増加する
      • 二つの異なるエポック番号があり、二つの異なるリーダーが選出されたら、大きいエポック番号をもつリーダーが優先される
  • リーダーは何かを決定する前にまず自分より大きいエポック番号を持ち、衝突する判断をするかもしれない他のリーダーがいないことを確認する必要がある

    • リーダーはクオラムから投票を集める
      • 投票には 2 回のラウンドがある
        • 1 回目はリーダー選出のための投票
        • 2 回目はリーダーの提案に対する投票
      • 2 つの投票のクオラムには重複部分が必要
        • 最初のリーダー選出の投票に参加したノードは、2 回目のリーダーの提案に対する投票にも参加していなければならない
  • 2PC と合意は似ているけれど違う

    • コーディネータは選出されないが、合意におけるリーダーは選出される
    • 2PC ではすべての参加者の yes が必要だが、合意は過半数で良い

合意の限界

  • 合意によって強固な安全性が提供されて、耐障害性を保てる
  • 全順序ブロードキャストを提供するため、線形化可能でアトミックな操作を実装できる

  • 限界点

    • 合意プロトコルにおける投票は同期レプリケーションの一種であるため、パフォーマンス上の問題が発生する可能性がある
    • 処理を行う上で厳密な過半数を必要とする
    • 投票に参加するノード集合が固定の集合であることが前提とされることが多い
      • 動的なメンバーシップの拡張を加えたアルゴリズムの理解はまだそれほど進んでいない
    • 障害が発生しているノードの検出をタイムアウトに依存している
      • ネットワーク遅延の影響によりリーダー選出プロセスが頻繁に起きてパフォーマンスを損なう可能性がある
      • Raft にはよく知られた問題がある
        • ネットワークが全体的には正しく動作しており、特定の一つのネットワークリンクで信頼性が低い状態が続いている場合、継続的にリーダー湿布が 2 つのノード間で行き来し続けてしまったりして効果的な処理を進められなくなる

メンバーシップと協調サービス

  • ZooKeeper, etcd は「分散キーバリューストア」「協調及び設定サービス」と説明される
  • 普通のユーザーがデータベースを利用するように ZooKeeper や etcd を使うことは滅多にない
  • 通常は何らかの他のプロジェクトを通じて間接的に利用することがほとんど
  • ZooKeeper の場合以下のプロジェクトから依存されている

    • HBase
    • Hadoop YARN
    • OpenStack Nove
    • Kafka
  • ZooKeeper や etcd は完全にメモリ内に収まる少量のデータを保持するように設計されている(永続性のためにディスクへの書き込みは行われる)

  • 分散システムを構築する上で有益な機能群

    • 線形化可能ででアトミックな操作
    • 操作の全順序
    • 障害検出
    • 変更通知

データ指向アプリケーションデザイン第8章分散システムの問題

プロセスの一時停止

  • 分散システムにおけるクロックの危険な使い方例
    • パーティションごとに 1 つのリーダーをもつデータベース
      • 書き込みを受け付けられるのはリーダーのみ
      • 自分が書き込まれても安全であるとノードが知る方法
        • 例: 他のノードからリースを取得すること
          • リース: タイムアウト付きのロック。ある時点でリースを保持できるのは 1 つのノードのみ。 つまり、リースを持っているのがリーダー
          • リーダーでありつづけるには、リースを更新し続けなければならない
while(true) {
    request = getIncomingRequest();

    // ロックの有効期限は別のマシンで設定されたもの
    // 比較されているのは、ローカルクロック
    // 問題1: ローカルクロックの同期がずれているとおかしな結果になる
    if (lease.expiryTimeMillis - System.currentTimeMillis() < 10000) {
        lease = lease.renew();
    }


    if (lease.isValid()) {
        // 問題2: もし仮に isValid メソッドが実行されてから以下の行が実行されるまでに
        // アプリケーションが15秒停止したら、リースの有効期限は切れてしまっている
        // が、リースの有効期限が切れているかどうかの判定は次のループまでわからない
        process(request);
    }
}
  • アプリケーションスレッドが長い時間停止すると想定するのはおかしい?
    • おかしくない
      • ランタイムに実装されている GC は時折実行中のスレッドをすべて停止する
      • 仮想マシンサスペンド(すべてのプロセスが一時停止され、メモリがディスクに退避される) されうる
      • パーソナルコンピュータだったら、液晶パネルを閉じるだけでサスペンドされることがある
      • コンテキストスイッチやハイパーバイザーが他の仮想マシンへのスイッチを行うとき
        • スチール時間: ほかの仮想マシンで消費される CPU 時間
      • 同期的なディスクアクセスの場合、ディスク I/O の読み取り完了まで待機される
        • Java のクラスローダーはディスクからの読み取りを行う(ユーザーの指定なしに)
        • しかも、クラスが実際に利用されるまで遅延されるので、ディスク I/O と GC が同じタイミングで起きることさえある
        • ディスクがネットワークファイルシステムやネットワークブロックデバイスなら、ネットワーク遅延の変動にも影響をうける
      • ディスクへのスワッピング(ページング)が起きているなら、ディスクからメモリにページが読み込まれている間スレッドは一時停止される
        • スラッシング: ページングが頻発して、実際の作業を行う時間がほとんど取れない状態になること
      • SIGSTOP シグナル
    • 上記の状況はすべて、実行中のスレッドを任意の時点でプリエンプション(一時的に中断)するが、すべてそのスレッド自身は気がつかない
    • 単一マシン上のマルチスレッド制御のために使用可能な道具: ミューテックスセマフォ、アトミックなカウンタ、ブロッキングキュー等
      • これは分散マシンでは使えない。共有メモリが存在しないため。ネットワークを通じてメッセージを送信するしかない。

レスポンスタイムの保証

  • ソフトウェアが反応する時間に期限が指定されているものがある。これがハードリアルタイムシステム
    • Web の世界におけるリアルタイムは、厳密なレスポンスタイムの制約のないストリーム処理とう指す曖昧な言葉
    • 組込システムにおけるリアルタイムは、あらゆる環境下で指定されたタイミングの保証が満たされるように注意深くシステムの設計とテストが行われるという意味
    • RTOS: 指定された間隔で CPU 時間の割当保証を伴うプロセスのスケジューリングが必要

GC によるインパクトの制限

  • GC による一時停止を事前に計画された短時間のノードの停止のように扱い、一つのノードが GC を待っている間は、 他のノードがクライアントからのリクエストを処理する、という方式もある
    • これにはノードの GC による一時停止がもう直ぐ必要になることをランタイムがアプリケーションに通知できる必要がある
  • ヤング GC だけ行い、Old GC (Full GC) が必要になる前にプロセスを再起動するという方法もある
    • 再起動するノードは一つだけにして、トラフィックはそのノード以外に流されるようにすればよい

知識、真実、虚偽

  • 分散システムにおいて他のノードの状態を知るためには、ネットワークを通じたメッセージ交換を行うしか方法はない
  • 分散システムでは、ある前提条件を仮定し、その前提の範囲内で正しく動作することを保証するシステムモデルがある

真実は多数決で決定される

  • ノードが自身の状況に対して下す判断は常に信じられるわけではない
    • 分散システムは単一ノードに依存できない。そのノードはいつ障害を起こして、システムを停止して、回復不能にしてしまうかもしれない
    • 多くの分散アルゴリズムは、クオラムに依存する
      • ノード群によって行われる投票。特定のノードへの依存を減らすために、判断を下す際に複数のノードの最小限度以上の投票がなければならない

リーダーとロック

  • あるものを一つだけにしなければならない状況

    • データベースパーティションにおいて、リーダーになれるノードは一つに限られる
    • あるリソース、オブジェクトのロックを保持できるトランザクション、クラインアントは一つに限られる
      • あるリソースやオブジェクトへの書き込みが並行に行われるようにするため
  • ノードの過半数から落ちているとみなされるにもかかわらず自分がリーダとして振る舞い続けようとすると、問題が生じる可能性がある

フェンシングトーク

  • ロックサーバーがロックやリースを渡すたびに、同時にフェンシングトークンも渡す

    • ロックが渡されるたびに増加する数値
    • クライアントはリクエストをストレージサービスに送信する際に、フェンシングトークンも含める
      • 増加するフェンシングトークンの順序でのみ書き込みを許すことによってストレージへのアクセスを安全にする
  • ロックサービスとして Zookeeper が使われている場合、トランザクション ID の zdix やノードバージョンの cversion をフェンシングトークンとして使用できる

ビザンチン障害

  • 分散システムを難しくする問題は、ノードが嘘つくケース

    • 例えば、ノードがメッセージを受信していないのにもかかわらず、受信したと主張する場合
    • これをビザンチン障害という
    • こうした信頼のおけない環境下で合意を形成する問題は、ビザンチン問題という
      • 二人の将軍問題を一般化したもの
  • ビザンチン耐性をもつとは、一部のノードに不具合があってプロトコルに従わなかったり、悪意を持った攻撃者がネットワークにおいて妨害をしたりしているような状況においても正しく動作し続けられることをいう

  • ビザンチン耐性を持たないといけないシステム
    • 航空宇宙産業に関わるシステム
      • 放射線によって破損し、予測つかない反応を返す可能性がある
    • 複数の組織が関わるシステム
      • 例えばブロックチェーンのようなピアツーピアのネットワークは相互に信頼し合っていない関係者が中央集中の認可機関なしにトランザクションが生じたかどうかについて合意する方法

弱い嘘

  • ハードウェアの問題やソフトウェアのバグのような弱い嘘に対応する仕組みを持たせることに価値はある
    • ネットワークパケットに対するアプリケーションレベルのチェックサム
    • 値の基本的なサニティーチェック
    • NTP のサーバーアドレスのように複数のサーバーとやりとりして誤差を推定する

システムモデルと現実

  • システムモデル: 分散アルゴリズムが前提としているシステム中で生じると予想されるフォールトの種類の定式化

    • タイミングの前提に関する 3 つのシステムモデル

      • 同期モデル
        • ネットワークの遅延やプロセスの一時停止機関、クロックの誤差に限度があることを前提とする
        • ほとんどの分散システムにおいて同期モデルは現実的ではない
      • 部分同期モデル
        • システムのほとんどの場合同期モデルのように振舞うものの、ネットワークの遅延、プロセスの一時停止、クロックの変動が上限をこえることが時々生じることを意味する
      • 非同期モデル
        • タイミングに関するいかなる前提を置くことも許されない
    • ノードに関する一般的なモデル

      • クラッシュストップフォールト
        • ノードの障害はただ一つのみしかないという前提
      • クラッシュリカバリフォールト
        • ノードはいつクラッシュするかわからず、いつになるかもわからないもののレスポンスを再び返しはじめるかもしれないという前提をおく
        • ノードはクラッシュがあっても内容を保持する安定したストレージを持ち、メモリに保持されていたものは失われるとみなす
      • ビザンチン(任意)障害
        • ノードは他のノードを欺いたりすることも含めて何をするか全くわからない

アルゴリズムの正しさ

  • アルゴリズムの正しさは、その性質を記述することによって定義できる
  • 例フェンシングトークンの性質

    • ユニーク性: フェンシングトークンを求める二つのリクエストに同じ値が返されないこと
    • 単調増加するシーケンス: リクエスト x に対するトークン t_x が返され、リクエスト y に対してトークン t_y が返される。この場合に、y の処理が始まる前に x の処理が終わっていれば、t_x < t_y となる
    • 可用性
      • フェンシングトークンを要求し、クラッシュしないノードは最終的にいつかはレスポンスを受けること
  • あるアルゴリズムが何らかのシステムモデルにおいて正しいとは、そのシステムモデルにおいて生じうるあらゆる状況下においてその性質が満たされること

安全性とライブ性

  • ライブ性の性質: 最終的にはよいことが生じること。eventually という言葉その定義に含まれることが多い (eventual consistency)

  • 安全性の性質: 何も悪いことが起こらないこと

Learning Go Chapter 10 Concurrency in Go

Concurrency in Go

はじめに

  • Go の並行処理モデルは他の言語と少し違う
  • CSP (Communicating Sequential Process) という理論に基づいた並行処理モデル

並行処理いつ使うのか

  • 並行処理を行うと必ず実行速度が速くなると誤認している人が多いが実際は違う

    • 並行処理は並列処理ではない
  • 並行処理を使うかどうかは、どのようにデータが流れるか次第

    • 独立して操作可能な複数の操作からデータを取得して結合する場合
  • 時間のかからない処理を並行処理で実行してもあまり価値はない

    • I/O の処理が並行処理で実行されることが多い
    • いつ並行処理を使えばよいかわからない場合にはまずシーケンシャルな処理を書いて、そのあとベンチマークを取れば良い

Goroutine

  • Go の並行処理におけるコアコンセプト
  • Goroutine は Go ランタイムによって管理される軽量なプロセス
  • Goプログラムが起動すると、Goランタイムは多数のスレッドを作成し、単一のゴルーチンを起動してプログラムを実行

    • OS がCPUコア全体でスレッドをスケジュールするのと同じように、プログラムによって作成されたすべてのゴルーチンは、最初のゴルーチンを含め、Goランタイムスケジューラによって自動的にこれらのスレッドに割り当てられる
  • スレッドと比較した Goroutine のメリット

    • 作成が速い (システムリソースを消費しないから)
    • スタックサイズが小さい (し、必要に応じて拡張できる)
    • スイッチコストが小さい
    • Go プロセスに最適化されている
  • Goroutineは、関数呼び出しの前にgoキーワードを配置することによって起動

    • 関数によって返される値はすべて無視される
  • Goroutineの例

func process(val int) int {
    return 1
}

func runThingConcurrently(in <-chan int, out chan<- int) {
    go func() {
        for val := range in {
            result := process(val)
            out <- result
        }
    }()
}

Channel

  • Goroutine はチャネルを使って通信する
  • Slice や Map と同じように makeを使って作成できるビルトインタイプ
    • Slice や Map と同じように チャネルは参照型。関数に渡すとポインターの値がコピーされる
  • zero valuenil
ch := make(chan int)

Reading, Writing, and Buffering

  • <- オペレータをつかって チャネルと通信する
a := <- ch // ch チャネルから値を読み取る
ch <- b     // ch チャネルに値bを書き込む
  • チャネルに書き込まれた値は一度しか読み出されない

    • 複数の goroutine がチャネルを読み出す場合、どちらか一方しかチャネルの値を読み出すことができない
  • チャネルの型宣言の仕方で読み取るのか、書き出すのかを示す

    • in <-chan int このように <- の後に chan がある場合は読み取り
    • out chan<-int このように chan の後に<- がある場合は書き出し
  • デフォルトでは、チャネルはバッファリングされていない

  • バッファリングされていない開いているチャネルに書き込むたびに、別のゴルーチンが同じチャネルから読み取るまで、書き込みゴルーチンが一時停止される
  • 同様に、開いているバッファなしチャネルからの読み取りにより、別のゴルーチンが同じチャネルに書き込むまで、読み取りゴルーチンが一時停止される
  • => 少なくとも2つの同時に実行されているゴルーチンがないと、バッファリングされていないチャネルに書き込みまたは読み取りを行うことができないことを意味

  • バッファリングされたチャネルは、チャネルの作成時にバッファの容量を指定することによって作成される

    • バッファの capacity を変更することはできない
ch2 := make(chan int, 10)
  • たいていの場合はバッファリングされていないチャネルで十分

for-range とチャネル

  • ループは、チャネルが閉じられるまで、またはbreakまたはreturnステートメントに到達するまで続く
for v := range ch {
        fmt.Println(v)
}

チャネルを閉じる

  • チャネルを閉じる場合、ビルトインファンクションの close を使う
close(ch)
  • チャネルが閉じられると、チャネルへの書き込みまたはチャネルを再び閉じようとすると、パニックになる

    • ただし、閉じたチャネルからの読み取りの試行は常に成功する...
  • チャネルが閉じられているかどうかを検出するために ok カンマイディオムを使用する

v, ok := <-ch
  • チャネルを閉じる責任は、チャネルに書き込むゴルーチンにある
  • チャネルを閉じる必要があるのは、チャネルが閉じるのを待機しているゴルーチンがある場合のみ

    • チャネルはただの変数なので、使用されなくなったら GC される
  • チャネルはコードを一連の段階として考え、データの依存関係を明確にすることをガイドする

    • 他の言語の場合、変更可能なグローバルな共有状態を使うため、データがプログラムをどのように流れるかを理解することが困難、スレッドの独立性を判断するのも困難になる

チャネルの振る舞い

バッファされていないチャネル OPEN バッファされていないチャネル CLOSE バッファされているチャネル OPEN バッファされているチャネル CLOSE nil
Read 書き込みがあるまで待機 zero バリューを返す バッファが空なら待機 バッファに値が残っているならその値を返す。空なら zero バリューを返す 永久にハングする
Write 読み込みがあるまで待機 Panic! バッファがいっぱいなら待機 Panic! 永久にハングする
Close 正常に機能する Panic! 正常に機能する、値はバッファリングされたまま Panic! Panic!

select

  • select 文は Go の並行性モデルにおけるチャネル以外の主たる構成要素
    • selectキーワードを使用すると、ゴルーチンが複数のチャネルのセットの1つから読み取りまたは書き込みを行うことができる
select {
case v := <-ch:
    fmt.Println(v)
case v := <-ch2:
    fmt.Println(v)
case ch3 <- x:
    fmt.Println("wrote", x)
case <-ch4:
    fmt.Println("got value on ch4, but ignored it")
}
  • 複数のケースに読み取りまたは書き込みが可能なチャネルがある場合はどうななるか?

    • 選択アルゴリズムは単純
    • 前進できるケースからランダムに選択。順序は重要ではない
    • これは、trueに解決される最初のケースを常に選択するswitchステートメントとは大きく異なる
    • また、他のケースよりも優先されるケースはなく、すべてが同時にチェックされる
    • ランダムに選択することのもう1つの利点は、デッドロックの最も一般的な原因の1つである、一貫性のない順序でロックを取得することを防ぐこと
  • selectは多数のチャネルを介した通信を担当するため、多くの場合、forループ内に埋め込まれている

   for {
        select {
        case <-done:
            return
        case v := <-ch:
            fmt.Println(v)
        }
    }
  • チャネルにノンブロッキングの読み取りまたは書き込みを実装する場合は、selectで default を使用する

Concurrency の実践とパターン

  • 並行性は API に公開しない
  • 並行性制御の仕組みは実装の詳細であるため、API として並行性制御の仕組みを公開すると、利用者が注意深く並行制御する責務を負ってしまう

  • ただし、並行性制御のためのヘルパー関数を提供するライブラリの場合は例外。一部の関数はチャネルを引数にとる関数を export している。time.After

Goroutines, for Loops, and Varying Variables

  • ゴルーチンを起動するために使用するクロージャにはパラメータがない
  • 代わりに、宣言された環境から値をキャプチャする
  • これが機能しない一般的な状況がforループのインデックスまたは値をキャプチャしようとしたとき

Always Clean Up Your Goroutines

  • ゴルーチン関数を起動するときはいつでも、それが最終的に終了することを確認する必要がある
  • 変数とは異なり、Goランタイムは、ゴルーチンが二度と使用されないことを検出できない
  • ゴルーチンが終了しない場合でも、スケジューラは定期的に何もしない時間を与え、プログラムの速度を低下させる

    • これは Goroutine リークと呼ぶ
  • ジェネレータの実装のために Goroutine 使うと、例えば使用者側が途中で break しただけで、Goroutine リークが起きる

The Done Channel Pattern

  • いくつかの関数を指定して最も速く結果を返した関数の値を返す関数
func searchData(s string, searchers []func(string) []string) []string {
    done := make(chan struct{})

    result := make(chan []string)

    for _, searcher := range searchers {
        go func(searcher func(string) []string) {
            select {
            case result <- searcher(s):
            case <-done:
            }
        }(searcher)
    }

    r := <-result
    close(done)
    return r
}

Using a Cancel Function to Terminate a Goroutine

  • Goroutine リークを防ぐために、Goroutine を起動する側で Groutine をクローズするための関数(クロージャー)を返す
func countTo(max int) (<-chan int, func()) {
    ch := make(chan int)
    done := make(chan struct{})
    cancel := func() {
        close(done)
    }

    go func() {
        for i := 0; i < max; i++ {
            select {
            case <-done:
                return
            case ch <- i:
            }
        }
        close(ch)
    }()

    return ch, cancel
}

func testCountTo() {
    ch, cancel := countTo(10)
    for i := range ch {
        if i > 5 {
            break
        }
        fmt.Println(i)
    }

    cancel()
}

When to Use Buffered and Unbuffered Channels

  • バッファリングされたチャネルは、起動したゴルーチンの数がわかっている場合、起動するゴルーチンの数を制限したい場合、またはキューに入れられる作業の量を制限したい場合に役立つ

Backpressure

  • Backpressure の実装でバッファーチャネルが使用できる可能性がある
type PressureGauge struct {
    ch chan struct{}
}

func New(limit int) *PressureGauge {
    ch := make(chan struct{}, limit)

    for i := 0; i < limit; i++ {
        ch <- struct{}{}
    }

    return &PressureGauge{
        ch: ch,
    }
}

// チャネルが使用できなかった場合(defaultの場合) エラーがかえる
func (pg *PressureGauge) Process(f func()) error {
    select {
    case <-pg.ch:
        f()
        pg.ch <- struct{}{}
        return nil
    default:
        return errors.New("no more capacity")
    }
}

Turning Off a case in a select

  • 複数の同時ソースからのデータを組み合わせる必要がある場合は、selectキーワードが最適
  • ただし、閉じたチャネルを適切に処理する必要がある
  • 選択のケースの1つが閉じたチャネルの読み取りである場合、それは常に成功し、ゼロ値を返す

    • そのケースを選択するたびに、値が有効であることを確認し、ケースをスキップする必要がある
    • 読み取りの間隔が空いていると、プログラムはジャンク値の読み取りに多くの時間を浪費する
  • nilチャネルを使用して、選択でケースを無効にすることができる

    • チャネルが閉じられたことを検出したら、チャネルの変数をnilに設定
    • nilチャネルからの読み取りが値を返さないため、関連するケースは実行されなくなる
for {
    select {
    case v, ok := <-in:
        if !ok {
            in = nil
            continue
        }
                         
        fmt.Println(v)
    case v, ok := <-in2:
        if !ok {
            in2 = nil
            continue
        }
        fmt.Println(v)
    case <-done:
        return
    }
                         
}

How to Time Out Code

func timeLimit() (int, error) {
    var result int
    var err error

    done := make(chan struct{})
    go func() {
        result, err = 1, nil
        close(done)
    }()
    select {
    case <-done:
        return result, err
    case <-time.After(2 * time.Second):
        return 0, errors.New("work timed out")
    }
}

Using WaitGroups

  • 1つのゴルーチンが、複数のゴルーチンが作業を完了するのを待つ必要がある場合がある
  • 単一のゴルーチンを待っている場合は、前に見た完了チャネルパターンを使用できる
  • ただし、複数のゴルーチンを待機している場合は、標準ライブラリの同期パッケージにあるWaitGroupを使用する必要がある
func testWaitGroup() {
    var wg sync.WaitGroup
    wg.Add(3)

    go func() {
               // wg を直接渡さない => コピーされて、デクリメントされないため
               // キャプチャして同一インスタンスであることを保証する
        defer wg.Done()
        // dothing1
    }()

    go func() {
        defer wg.Done()
        // dothing2
    }()

    go func() {
        defer wg.Done()
        // dothing3
    }()
    wg.Wait()
}
  • 同じチャネルに複数のゴルーチンを書き込んでいる場合は、書き込まれているチャネルが1回だけ閉じられていることを確認する必要がある
    • sync.WaitGroupはこれに最適
func processAndGather(in <-chan int, processor func(int) int, num int) []int {
    out := make(chan int, num)
    var wg sync.WaitGroup
    wg.Add(num)

    for i := 0; i < num; i++ {
        go func() {
            defer wg.Done()
            for v := range in {
                out <- processor(v)
            }
        }()
    }

    go func() {
        wg.Wait()
        close(out)
    }()

    var result []int
    for v := range out {
        result = append(result, v)
    }
    return result
}
  • Goの作成者は、標準ライブラリを補足する一連のユーティリティを維持
    • 総称してgolang.org/xパッケージと呼ばれるこれらのパッケージには、WaitGroupの上に構築され、いずれかがエラーを返したときに処理を停止する一連のゴルーチンを作成するErrGroupと呼ばれるタイプが含まれる

Running Code Exactly Once

type SlowComplicatedParser interface {
    Parse(string) string
}

var parser SlowComplicatedParser
var once sync.Once

func Parse(dataToParse string) string {
    once.Do(func() {
        parser = initParser()
    })

    return parser.Parse(dataToParse)
}

func initParser() SlowComplicatedParser {
    return parser
}

Putting Our Concurrent Tools Together

func (p *processor) launch(ctx context.Context, data int) {
    go func() {
        aOut := AOut{}
        var err error
        if err != nil {
            p.errs <- err
            return
        }
        p.outA <- aOut
    }()
    go func() {
        bOut := BOut{}
        var err error
        if err != nil {
            p.errs <- err
            return
        }
        p.outB <- bOut
    }()
    go func() {
        select {
        case <-ctx.Done():
            return
        case <-p.inC:
            cOut := COut{}
            err := errors.New("")
            if err != nil {
                p.errs <- err
                return
            }
            p.outC <- cOut
        }
    }()
}

When to Use Mutexes Instead of Channels

type MutexScoreboardManager struct {
    l          sync.RWMutex
    scoreboard map[string]int
}

func NewMutexScoreboardManager() *MutexScoreboardManager {
    return &MutexScoreboardManager{
        scoreboard: map[string]int{},
    }
}

func (msm *MutexScoreboardManager) Update(name string, val int) {
    msm.l.Lock()
    defer msm.l.Unlock()
    msm.scoreboard[name] = val
}

func (msm *MutexScoreboardManager) Read(name string) (int, bool) {
    msm.l.RLock()
    defer msm.l.RUnlock()
    val, ok := msm.scoreboard[name]
    return val, ok
}
  • ゴルーチンを調整したり、一連のゴルーチンによって変換される値を追跡したりする場合は、チャネルを使用
  • 構造体のフィールドへのアクセスを共有している場合は、ミューテックスを使用する
  • チャネルの使用時に重大なパフォーマンスの問題を発見し問題を修正する他の方法が見つからない場合は、ミューテックスを使用するようにコードを変更する

www.amazon.co.jp

Learning Go Chapter 9. Modules, Packages, and Imports

モジュール、パッケージ、インポート

リポジトリとモジュール、パッケージ

  • Go のライブラリ管理は3つの概念から成り立つ

  • Go のモジュールは、リポジトリ(とモジュール)のパスで一意に表される

    • 例えば、github.com/jonbodner/proteus

go.mod

  • ルートディレクトリに有効な go.mod ファイルがあるとモジュールになる
  • go mod init <GO MODULE PATH> でカレントディレクトリをルートディレクトリとしたモジュールとする go.mod ファイルを作成する

    • モジュールパスは大文字小文字を区別する
    • 混乱をさけるため通常は小文字のみを使用する
  • go.mod の例

    • module セクションでモジュールのユニークなパスを指定
    • go セクションで go の最小バージョンを指定
    • require セクションで依存するモジュールとその最小バージョンを指定
    • replace セクションで依存するモジュールのパスを上書きする
    • exclude セクションで依存するモジュールの特定バージョンが使用されないようにする
module github.com/htamakos/ch08

go 1.15

require github.com/pkg/errors v0.9.1 // indirect

インポートとエクスポート

  • Go では他の言語と同じようにパッケージを import できる
    • 公開された変数、定数、関数、型、インターフェースをインポートできる
  • Go で他パッケージから変数等を利用可能にするには?
    • 先頭文字を大文字にすると他パッケージに対して公開される
    • 小文字とか _ で始まる識別子は同一パッケージからのみしか使えない (export されない)

パッケージの作成とアクセス

  • Go でパッケージを作成するのは簡単
package_exmple
|- formatter
|      |__ formatter.go
|_ math
       |__ math.go
// math.go
package math

func Double(a int) int {
  return a * 2
}
// formatter.go
package print

import "fmt"

func Format(num int) string {
  return fmt.Sprintf("The number is %d", num)
}
  • main.go から使う
package main

import (
  github.com/package_exmple/formatter
  github.com/package_exmple/math
)

func main() {
  print.Format(math.Double(3))
}
  • ディレクトリ内のすべてのGoファイルには、同一のパッケージ句が必要
  • パッケージの名前がインポートパスではなく、パッケージ句によって決定される
  • 原則として、パッケージの名前は、パッケージを含むディレクトリの名前と一致させる必要がある
  • ただし、パッケージにディレクトリとは異なる名前を使用する場合がいくつかある
    • main パッケージ: アプリケーションのエントリーポイント
    • ディレクトリ名に Go 識別子で無効な名前が使用されている場合
    • ディレクトリを利用したバージョン管理のため

パッケージの命名

  • util みたいなパッケージ名はやめよう

モジュールを構成する方法

  • モジュールが小さい場合一つのモジュールのみで構成しよう
  • モジュールが1つ以上のアプリケーションで構成されている場合は、モジュールのルートにcmdというディレクトリを作成する
    • cmd内で、モジュールから構築されたバイナリごとに1つのディレクトリを作成する
  • モジュールのルートディレクトリに、プロジェクトのテストとデプロイを管理するための多くのファイル(シェルスクリプト継続的インテグレーション構成ファイル、Dockerfilesなど)が含まれている場合は、すべてのGoコード(cmdの下のメインパッケージを除く)をpkgというディレクトリに入れる
  • 機能のスライスごとにコードを整理する

パッケージ名のオーバライド

import (
  crand "cryipt/rand"
  "math/rand"
)
  • _ でパッケージ名の export を回避する
    • パッケージの init関数のみが実行される

パッケージコメントと godoc

  • パッケージレベルのコメント
// package level comment
package money
  • struct のコメント
// struct comment
type Money struct {
  Value decimal.Decimal
  Currency string
}
  • function のコメント
// function comment
// args
// return
func Hoge() {
}
  • go doc <パッケージパス> でパッケージのドキュメントを生成し、ブラウザで確認できる

internal パッケージ

  • モジュール内のパッケージ間で関数、型、または定数を共有したいが、それをAPIの一部にしたくない場合に internal パッケージを使う
  • internalというパッケージを作成すると、そのパッケージとそのサブパッケージにエクスポートされた識別子は、internalの直接の親パッケージとinternalの兄弟パッケージにのみアクセスできる

init 関数

  • 値を返さないinitという名前の関数を宣言すると、そのパッケージが別のパッケージによって最初に参照されたときに実行される
  • init関数には入力または出力がないため、パッケージレベルの関数および変数と相互作用して、副作用によってのみ機能する
  • のでできれば使うのは避けたほうが良い
    • ただし、データベースドライバなどの一部のパッケージは、init関数を使用してデータベースドライバを登録する

循環依存

  • Go ではパッケージ間で循環依存はできないようになっている

Gracefully Renaming and Reorganizing Your API

  • モジュールをリファクタしたい場合
    • エクスポートされた識別子の一部の名前を変更するか、モジュール内の別のパッケージに移動する
    • 過去にさかのぼる変更を避けるために、元の識別子を削除することを避ける

Minimum Version Selection

  • Go は依存するライブラリのバージョンが複数選択可能である場合、最小のバージョンの依存ライブラリをダウンロードする
    • これは 他の言語において 例えば npm のように依存ライブラリを複数ダウンロードする動作と異なる
    • これにより単一バイナリに組み込むライブラリの数が少なくなり、アプリケーションのバイナリサイズを減らすことができる

vendoring

  • 外部依存ライブラリを vendor ディレクトリいかにダウンロードする方法
    • これにより依存するライブラリのバージョンを固定化できる
    • ただし、Go Module と Proxy Server により支持されなくなった

pkg.go.dev

  • Goモジュールに関するドキュメントをまとめる単一のサービス
  • godocs、使用されているライセンス、README、モジュールの依存関係、およびモジュールに依存するオープンソースプロジェクトを公開

モジュールの公開

  • Goプログラムはソースコードからビルドされ、リポジトリパスを使用して自身を識別するため、Maven Centralやnpmの場合のように、モジュールを中央ライブラリリポジトリに明示的にアップロードする必要はない

www.amazon.co.jp

セッションの分離性レベルを変更、確認する方法

SQL> alter session set isolation_level = READ COMMITTED;

Session altered.

SQL> select sid, name, isdefault, value from V$SES_OPTIMIZER_ENV where name like '%isolation%' and sid = (select sys_context('USERENV', 'SID') from dual);

       SID NAME                           ISD VALUE
---------- -------------------------------------------------- --- -------------------------
       289 transaction_isolation_level               YES read_commited

SQL> alter session set isolation_level = SERIALIZABLE;

Session altered.

SQL> select sid, name, isdefault, value from V$SES_OPTIMIZER_ENV where name like '%isolation%' and sid = (select sys_context('USERENV', 'SID') from dual);

       SID NAME                           ISD VALUE
---------- -------------------------------------------------- --- -------------------------
       289 transaction_isolation_level               NO  serializable

依存 jar を特定のディレクトリ化にコピーして jar を生成する方法

以下は libs 以下に依存する jar を入れる方法

jar {
  manifest { 
    attributes "Main-Class": "$mainClassName"
  }
  zip64 = true

  def copySpec = project.copySpec {
    into "libs"

    from {
        configurations.runtimeClasspath.collect { it }
    }
  }

  with copySpec
}
$ gradle build
$ jar tvf build/libs/sample.jar
     0 Tue Jun 08 16:52:50 JST 2021 META-INF/
    64 Tue Jun 08 12:01:08 JST 2021 META-INF/MANIFEST.MF
     0 Tue Jun 08 11:51:34 JST 2021 sample/
  1275 Tue Jun 08 11:51:34 JST 2021 sample/App.class
     0 Tue Jun 08 16:52:50 JST 2021 libs/
2746681 Fri Feb 12 10:45:22 JST 2021 libs/guava-28.0-jre.jar
  4617 Wed Oct 14 19:19:36 JST 2020 libs/failureaccess-1.0.1.jar
  2199 Wed Oct 14 19:19:34 JST 2020 libs/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar
 19936 Wed Oct 14 19:19:34 JST 2020 libs/jsr305-3.0.2.jar
200629 Wed Oct 14 19:19:34 JST 2020 libs/checker-qual-2.8.1.jar
 13166 Wed Oct 14 19:19:34 JST 2020 libs/error_prone_annotations-2.3.2.jar
  8781 Wed Oct 14 19:19:34 JST 2020 libs/j2objc-annotations-1.3.jar
  3448 Fri Feb 12 10:45:18 JST 2021 libs/animal-sniffer-annotations-1.17.jar