Learning Go Chapter 11. The Standard Library
The Standard Library
- Go の標準ライブラリは Python の "batteries included" の哲学と同じで、アプリケーション開発に必要なライブラリを標準装備している
io とその仲間たち
- Goの入出力哲学の中心は、ioパッケージにある
Reader
とWriter
インタフェースはよく使われるインタフェース
// スライスを入力にして、直接変更される 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メソッドが[]バイトを返すように記述されている場合、呼び出しごとに新しい割り当てが必要になる
- 各割り当てはヒープ上で終了するため、ガベージコレクターにとって非常に多くの作業が必要
- r.Readから返されたn値を使用して、バッファーに書き込まれたバイト数を確認し、bufスライスのサブスライスを反復処理して、読み取られたデータを処理する
- r.Readから返されたエラーがio.EOFの場合、rからの読み取りが完了したことがわかる
- このエラーは、実際にはエラーではない
- これは、io.Readerから読み取るものが何も残っていないことを示す
- io.EOFが返されると、処理が終了し、結果が返される
- 1.バッファを1回作成し、r.Readを呼び出すたびに再利用する
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で作成されるたびに経過時間を追跡する
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"` }
データ指向アプリケーションデザイン第9章: 一貫性と合意
ch09 一貫性と合意
耐障害性をもつシステムを構築する最も最善の方法は、有益な保証をもつ汎用的な抽象概念を見出し、それを一度だけ実装し、アプリケーションをその保証に依存させること
分散システムにおいても、分散システムにおける問題をアプリケーションがいくつか無視できるようにしてくれる抽象概念がある
- 例えば、合意 (consensus): すべてのノードが何かについて同意すること
一貫性の保証
レプリケーションを行うデータベースのほとんどは少なくとも結果整合性を提供する
- ある程度待てば最終的には読み取りリクエストに対してすべて同じ結果を返すようになること
- 収束性という言葉の方が合っている
分散一貫性モデルとトランザクション分離モデル
線形化可能性
考え方の背景
- 同時に異なる二つのレプリカにクエリを投げると異なる結果が返ってくることがある(結果整合性)
- データベースがレプリカが一つしかないように見せる方がシンプル
- クライアントはデータを同じように見ることになり、レプリケーションラグを気にする必要がない
線形化可能性
- 原子的一貫性、強い一貫性、即時一貫性、外部一貫性と呼ばれることがある
- 線形化可能性とは最新性の保証 (recency guarantee) を意味する
- データベースから読み取りを行う場合、読み取られる値が最新であり、古くなったキャッシュやレプリカからの値でないことを保証する
システムを線形化可能にする条件
- 3 つのクライアントが読み書きする例でどういう条件が必要か考える
- パターン 1:
レジスタに対する操作
線形可能性である場合のレスポンス
- クライアント A の最初の読み取り => 間違いなく 0 が返ってくる
- クライアント A の最後の読み取り => 書き込みが完了した後に始まっているので間違いなく 1 が返ってくる
- 書き込み処理と重なって実行された読み取り => 0 または 1 が返ってくる
これだけだと線形可能性であるための条件としては不十分
- 読み取りと書き込みが並行している場合に、新旧の値のいずれかが返ってくるとなると、読み取りのたびに異なる値が返ってくる可能性がある
- これはデータのコピーが一つしかなく、常に最新の値が返ってくるという線形可能性に違反する
そこで制約を加える
どこかの時点で x の値が 0 から 1 にアトミックに変更され、あるクラインアントの読み取りに対して新しい値 1 が返されたら、 それ以降の読み取りはすべて新しい値を返さないといけない
もっと複雑な例
追加の操作
- cas(x, v_old, v_new) => r: クライアントがアトミックな compare-and-set 操作を要求したという意味
線形化可能にするためには、操作のマーカーを連結する線が常に時間軸の中で前に進み、決して後ろに戻らないこと
- 操作のマーカー(垂直線):データベースが処理を実行したタイミング
- このモデルはトランザクションの分離は想定されていないため、値はいつでも他のクライアントによって書き換えられる
線形化可能な直感イメージは上記の通りだが、形式的な定義も存在する。この定義にしたがっているかどうかテストすることも可能だが、演算負荷が非常に高い。
線形化可能性と直列化可能性
直列化可能性
線形化可能性
- レジスタの読み書きにおける最新性の保証(ここのオブジェクトに関すること)
データベースは、直列化可能性と線形化可能性を同時に提供できる
- 厳密な直列化可能性、強い単一コピーの直列化可能性と呼ばれる
- 2PL による直列化可能性の実装や完全順次実行の実装はこれにあたる
直列化可能なスナップショット分離 (SSI)は、線形化可能ではない
線形化可能性への依存
- 線形化可能性が役立つ場面は?
ロックとリーダー選出
- 単一リーダーのレプリケーションを利用するシステムは、単一のリーダーしかいないことを保証する必要がある
- リーダー選出の方法の一つはロックを使うこと
- このロックは線形化可能でなければならない
制約及びユニーク性の保証
- ユーザー名、メールアドレス、ファイルストレージサービスのファイルパスに対するユニーク性
- これらを書き込まれる際に保証したいなら線形化可能性でなければならない
クロスチャネルタイミングの依存関係
- メッセージキューとファイルストレージのように二つの異なる通信チャネルが合った場合に、線形化可能性の最新性の保証がない場合にレース条件が発生する
線形化可能なシステムの実装
データのコピーが一つしかないように振る舞い、データに対するすべての操作はアトミックであるというのが線形化可能性
- 合意アルゴリズム(線形化可能)
- マルチリーダーレプリケーション(線形化可能ではない)
リーダーレスレプリケーション(おそらく線形化可能ではない)
線形化可能性とクオラム
- 厳密なクオラムでも線形化可能にできない
- パフォーマンス低下を受け入れることができるのであれば、Dynamo スタイルのクオラムを線形化可能にすることはできる
- 読み取りの際にはアプリケーションに結果を返す前に読み取り修復を同期的に行う
- 書き込みの際には書き込みの送信前にノードのクオラムの最新状態を読み取れるようにする
線形化可能にすることによるコスト
- アプリケーションに線形可能化が必須である場合、一部のレプリカが他のレプリカからネットワークの問題で切り離されてしまったら、 切り離されているレプリカは利用できない(ネットワークの問題が解消されるのをまつか、エラーを返す)
アプリケーションに線形可能化が必須でない場合、レプリカが切り離されてしまったとしても、それぞれのレプリカが独立してリクエストを処理できるような方法で書き込みを受け付けることができる
- ネットワークの問題が生じても利用できるが、その動作は線形化可能ではない
CAP 定理の対象範囲は非常に狭い
- 1 つの一貫性モデル(線形化可能性)と 1 種類のフォールト(ネットワーク分離)だけを対象とする
線形化可能性とネットワーク遅延
線形化可能性を持っているシステムは少ない...
- マルチコア CPU の RAM でさえ線形化可能ではない
- メモリバリア、フェンスが使用されないかぎり...メモリからの読み取りの最新性は保証されない
- マルチコア CPU の RAM でさえ線形化可能ではない
なぜ線形化可能性をもつシステムが少ないのか
- ひとえにパフォーマンス影響のせい。耐障害性よりもパフォーマンスを重視するケースの方が多いため、線形化可能性を捨てて、
パフォーマンス向上を図る
- 線形化可能にすると、そうしない場合と比較すると必ず読み書きのパフォーマンスが悪化する
- ひとえにパフォーマンス影響のせい。耐障害性よりもパフォーマンスを重視するケースの方が多いため、線形化可能性を捨てて、
パフォーマンス向上を図る
順序の保証
- 順序は重要な概念
順序と因果関係
- 順序は因果関係を保つのに役立つ
- 因果律はイベント間に順序関係を発生させる
因果律に基づく順序と全順序の違い
全順序があれば任意の二つの要素を比較できるので、必ず大小関係を判断できる
線形化可能性: 操作に全順序がある
線形化可能は因果律を暗に含む
因果律における依存関係の補足
- 因果律を保持するためには、ある操作に対してどの操作が先行したのかを知る必要がある
シーケンス番号の順序
シーケンス番号、タイムスタンプを使ってイベントの順序づけを行うことができる
- 物理クロックでなくて良い。論理クロック(操作を特定するための数値の並びを生成するアルゴリズム)で問題ない。
因果的でないシーケンス番号生成器
シングルリーダーではない、マルチリーダーやリーダーレスの場合、シーケンス生成は以下のようになる
- 各ノードが独立して、自分用のシーケンス番号の集合を生成できる(A ノードは偶数、B ノードは奇数等)
- 24 時間制のクロックから取得したタインプスタンプを各操作に割り当てる(LWW)
- あらかじめ、シーケンス番号のブロックを割り当てておく(A ノードは 1-1000、B ノードは 1001 - 2000 等)
カウンタをインクリメントする方法よりもスケーラビリティがあり、パフォーマンスが高いかもしれない...
ランポートタイムスタンプ(lamport timestamp)
- ただし、全順序があるだけでは不十分なケースがある
- 例えばユーザ名に対するユニーク制約のようなものを実装する場合
- 順序がいつまでに確定するのかがわからなければならない
- 同じユーザ名を全順序中でその操作よりも先に要求しているノードが他にはないことが確実になってはじめて、その操作が成功したと安全にいえる
- 順序がいつまでに確定するのかがわからなければならない
- 例えばユーザ名に対するユニーク制約のようなものを実装する場合
全順序のブロードキャスト
シングルリーダーレプリケーションは、リーダーの単一の CPU コアですべての操作を並べることによって全順序を保証する
- リーダーに障害があった場合にはどうする?
- 単一ののリーダーで処理できる以上のスループットだった場合は?
これらの問題は全順序のブロードキャスト、アトミックブロードキャストと呼ばれる
全順序のブロードキャストは通常ノード間のメッセージ交換プロトコルとして記述される
- 非形式的には二つの安全性が常に満たされていなければならない
- 信頼できる配信: メッセージがロストしてはいけない
- 全順序づけされた配信: メッセージはすべてのノードに同じ順序で配信されなければならない
- 非形式的には二つの安全性が常に満たされていなければならない
全順序ブロードキャストの利用
全順序ブロードキャスト = 線形化可能な CAS = 合意
分散トランザクションと合意
- 合意は分散コンピューティングにおいて最も基本的で重要な問題
合意は難しい。少なくとも以下のことを理解していないといけない。
ノード群の合意をとることが重要な状況
- リーダー選出
- アトミックコミット
FLP 帰結(合意の不可能性)
アトミックなコミットと 2 相コミット
- トランザクションの原子性: 複数の書き込みの途中で問題が生じた場合のシンプルなセマンティクス
- 失敗する(中断する)か、成功するかのどちらか
- データベースに中途半端な状態の結果を残さない
- セカンダリインデックス(主たるデータとは別のデータ構造)と主たるデータの一貫性も保つ
単一ノードから分散アトミックコミットへ
- 単一ノードの実行されるトランザクションは、原子性は一般にストレージエンジンによって実装される
単一ノードのデータベースにおける永続性の提供の仕組み
単一ノード上では、トランザクションのコミットはデータが永続性を持ってディスクに書き込まれる順序に依存している
- データの書き込み -> コミット
2 相コミット(2PC)
- コーディネータ(トランザクションマネージャー)が登場する
アプリケーションがコミットできる準備が整ったら、コーディネータはフェーズ 1 を開始する
- コーディネータは準備リクエストを各ノードに送信し、それらがコミットできるかを問い合わせる
- コーディネータは参加者からのレスポンスを追跡する
すべての参加者が yes を返し、コミットの準備が整っていることを示してきたら、コーディネータはフェーズ 2 でコミットリクエストを送信する => 実際にコミットが行われる
いずれかの参加者が no を返したら、コーディネータはフェーズ 2 ですべてのノードに中断リクエストを送信する
2PC 詳細
- 分散トランザクションを開始したい場合、アプリケーションはコーディネータに対してグローバルユニークなトランザクション ID を要求する
- アプリケーションは、各参加者上で単一ノードのトランザクションを開始し、それらのトランザクションに対してグローバルユニークなトランザクション ID を添付する
アプリケーションコミットの準備が整ったら、コーディネータはグローバルなトランザクション ID をタグづけした準備のリクエストをすべての参加者に送信する
準備のリクエストを受信した参加者は、いかなる環境下でもそのトランザクションを間違いなくコミットできることを確認する
- トランザクションの全データをディスクに書き込めること、制約違反の有無のチェックを行う
コーディネータは準備のリクエストに対するレスポンスのすべてを受け取ったら、トランザクションをコミットするか中断するか最終的に判断する
- コーディネータはコミットポイント(クラッシュが生じたとしてもコミットするか中断するかを下した判断がわかるようにするポイント)を残す
コミットポイントの判断がディスクにかかれたら、コミットもしくは中断のリクエストがすべての参加者に送られる
コーディネータの障害
- もしコーディネータに障害が発生した場合は、参加者はコーディネータのリカバリを待つこと以外に 2PC を完結させる方法はない
- 参加者同士で通信して互いの状況を知って何らかの同意を得るということは可能だが、2PC にはこうしたプロトコルはない
スリーフェーズコミット
2 相コミットはブロッキングアトミックコミットプロトコルと呼ばれる
- これは 2PC がコーディネータのリカバリを持って行き詰まってしまう場合があることからきている
2PC に代るものとして 3PC が提唱された
- ただしネットワークの遅延に上限があり、ノードのレスポンスタイムにも上限があることを想定している
- ので現実的なシステムでは 3PC で原子性を保証することはできない
- ただしネットワークの遅延に上限があり、ノードのレスポンスタイムにも上限があることを想定している
分散トランザクションの実際
2PC はパフォーマンス上のペナルティを伴うケースが多い
分散トランザクションとは何か?2 種類ある
exactly-once なメッセージ処理
- ヘトロジニアスな分散トランザクションは多様なシステムを結合する
XA トランザクション
- X/OpenXA: ヘトロジニアスな技術間での 2 相コミットの標準
未確定状態中のロックの保持
- 未確定状態で行き詰まったトランザクションは、ロックの影響を受ける
コーディネータからの障害のリカバリ
- orphaned 未確定トランザクションが生じる可能性がある
分散トランザクションの限界
- コーディネータは一種のデータベース扱い
- 単一マシン上でコーディネータが動作している場合にはシステム全体にとっての単一障害点になる
- アプリケーションサーバはステートレスで、永続化すべきステートはデータベースに保存することがほとんど。コーディネータはアプリケーション側のライブラリとして提供されることがほとんどなので、ステートレスなアプリケーションサーバにステートが持ち込まれることになる
- SSI と共に動作することはできない
- 一つの参加者が壊れたら、他のすべてに影響が及ぼされる(つまり、XA トランザクションは障害を増加する傾向にある)
耐障害性をもつ合意
- 合意とは、複数のノードが何かについて同意すること
合意の問題は以下のように形式化される
- 1 つ以上のノードが値を提案 (propose)する
- それらの値の中から一つを決定 (deside)する
合意が満たさなければいけない性質
- 一様同意 (uniform agreement)
- 2 つのノードが異なる決定をしていないこと
- 整合性 (integrity)
- 2 回決定をしているノードがないこと
- 妥当性 (validity)
- ノードが値 v を決定したら、v を提案しているノードがあること
- 終了性 (termination)
- クラッシュしていないすべてのノードは最終的に何らかの値を決定すること
- 一様同意 (uniform agreement)
合意のシステムモデル
- クラッシュしたノードは突然消え去り、決して戻ってこないことを前提としている
- 2PC は終了性の要件を満たすことができない
- クラッシュしたノードは突然消え去り、決して戻ってこないことを前提としている
合意アルゴリズムと全順序ブロードキャスト
耐障害性をもつ合意アルゴリズムとして最も広く知られているもの
- VSR (Viewstamped Replication)
- Paxos
- Raft
- Zab
これらのアルゴリズムは、一様同意/整合性/妥当性/終了性という同意の形式的性質を直接利用せずに、値の並びを決定することによって全順序ブロードキャストになっている
- 全順序ブロードキャストは、メッセージが厳密に一度だけ、同じ順序でメッセージがすべてのノードに配信されなければならない
- 全順序ブロードキャストは、複数回に渡って合意をとることと等価
シングルリーダーレプリケーションと合意
- リーダーの選出を自動で(管理者の手を介在することなく)行う場合には、合意が必要
エポック番号とクオラム
合意プロトコルでは、内部的に何らかのリーダーを使っているがユニーク性は保証していない。その代わりに弱い保証を与える
- エポック番号
- Paxos の場合: 投票番号
- VSR の場合: ビュー番号
- Raft の場合: 期間番号
- エポック番号
現在のリーダーが落ちたと考えられるたびに、ノード間で新しいリーダーを選出するための投票が始まる
- この選出に対してインクリメントされるエポック番号が与えられる
- エポック番号は全順序をもち、単調増加する
- 二つの異なるエポック番号があり、二つの異なるリーダーが選出されたら、大きいエポック番号をもつリーダーが優先される
- この選出に対してインクリメントされるエポック番号が与えられる
リーダーは何かを決定する前にまず自分より大きいエポック番号を持ち、衝突する判断をするかもしれない他のリーダーがいないことを確認する必要がある
- リーダーはクオラムから投票を集める
- 投票には 2 回のラウンドがある
- 1 回目はリーダー選出のための投票
- 2 回目はリーダーの提案に対する投票
- 2 つの投票のクオラムには重複部分が必要
- 最初のリーダー選出の投票に参加したノードは、2 回目のリーダーの提案に対する投票にも参加していなければならない
- 投票には 2 回のラウンドがある
- リーダーはクオラムから投票を集める
2PC と合意は似ているけれど違う
- コーディネータは選出されないが、合意におけるリーダーは選出される
- 2PC ではすべての参加者の yes が必要だが、合意は過半数で良い
合意の限界
- 合意によって強固な安全性が提供されて、耐障害性を保てる
全順序ブロードキャストを提供するため、線形化可能でアトミックな操作を実装できる
限界点
- 合意プロトコルにおける投票は同期レプリケーションの一種であるため、パフォーマンス上の問題が発生する可能性がある
- 処理を行う上で厳密な過半数を必要とする
- 投票に参加するノード集合が固定の集合であることが前提とされることが多い
- 動的なメンバーシップの拡張を加えたアルゴリズムの理解はまだそれほど進んでいない
- 障害が発生しているノードの検出をタイムアウトに依存している
- ネットワーク遅延の影響によりリーダー選出プロセスが頻繁に起きてパフォーマンスを損なう可能性がある
- Raft にはよく知られた問題がある
- ネットワークが全体的には正しく動作しており、特定の一つのネットワークリンクで信頼性が低い状態が続いている場合、継続的にリーダー湿布が 2 つのノード間で行き来し続けてしまったりして効果的な処理を進められなくなる
メンバーシップと協調サービス
- ZooKeeper, etcd は「分散キーバリューストア」「協調及び設定サービス」と説明される
- 普通のユーザーがデータベースを利用するように ZooKeeper や etcd を使うことは滅多にない
- 通常は何らかの他のプロジェクトを通じて間接的に利用することがほとんど
ZooKeeper の場合以下のプロジェクトから依存されている
- HBase
- Hadoop YARN
- OpenStack Nove
- Kafka
ZooKeeper や etcd は完全にメモリ内に収まる少量のデータを保持するように設計されている(永続性のためにディスクへの書き込みは行われる)
分散システムを構築する上で有益な機能群
- 線形化可能ででアトミックな操作
- 操作の全順序
- 障害検出
- 変更通知
データ指向アプリケーションデザイン第8章分散システムの問題
プロセスの一時停止
- 分散システムにおけるクロックの危険な使い方例
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 の読み取り完了まで待機される
- ディスクへのスワッピング(ページング)が起きているなら、ディスクからメモリにページが読み込まれている間スレッドは一時停止される
- スラッシング: ページングが頻発して、実際の作業を行う時間がほとんど取れない状態になること
- SIGSTOP シグナル
- 上記の状況はすべて、実行中のスレッドを任意の時点でプリエンプション(一時的に中断)するが、すべてそのスレッド自身は気がつかない
- 単一マシン上のマルチスレッド制御のために使用可能な道具: ミューテックス、セマフォ、アトミックなカウンタ、ブロッキングキュー等
- これは分散マシンでは使えない。共有メモリが存在しないため。ネットワークを通じてメッセージを送信するしかない。
- おかしくない
レスポンスタイムの保証
- ソフトウェアが反応する時間に期限が指定されているものがある。これがハードリアルタイムシステム
- Web の世界におけるリアルタイムは、厳密なレスポンスタイムの制約のないストリーム処理とう指す曖昧な言葉
- 組込システムにおけるリアルタイムは、あらゆる環境下で指定されたタイミングの保証が満たされるように注意深くシステムの設計とテストが行われるという意味
- RTOS: 指定された間隔で CPU 時間の割当保証を伴うプロセスのスケジューリングが必要
GC によるインパクトの制限
- GC による一時停止を事前に計画された短時間のノードの停止のように扱い、一つのノードが GC を待っている間は、
他のノードがクライアントからのリクエストを処理する、という方式もある
- これにはノードの GC による一時停止がもう直ぐ必要になることをランタイムがアプリケーションに通知できる必要がある
- ヤング GC だけ行い、Old GC (Full GC) が必要になる前にプロセスを再起動するという方法もある
- 再起動するノードは一つだけにして、トラフィックはそのノード以外に流されるようにすればよい
知識、真実、虚偽
- 分散システムにおいて他のノードの状態を知るためには、ネットワークを通じたメッセージ交換を行うしか方法はない
- 分散システムでは、ある前提条件を仮定し、その前提の範囲内で正しく動作することを保証するシステムモデルがある
真実は多数決で決定される
- ノードが自身の状況に対して下す判断は常に信じられるわけではない
リーダーとロック
あるものを一つだけにしなければならない状況
ノードの過半数から落ちているとみなされるにもかかわらず自分がリーダとして振る舞い続けようとすると、問題が生じる可能性がある
フェンシングトークン
ロックサーバーがロックやリースを渡すたびに、同時にフェンシングトークンも渡す
ロックサービスとして Zookeeper が使われている場合、トランザクション ID の zdix やノードバージョンの cversion をフェンシングトークンとして使用できる
ビザンチン障害
分散システムを難しくする問題は、ノードが嘘つくケース
ビザンチン耐性をもつとは、一部のノードに不具合があってプロトコルに従わなかったり、悪意を持った攻撃者がネットワークにおいて妨害をしたりしているような状況においても正しく動作し続けられることをいう
- ビザンチン耐性を持たないといけないシステム
弱い嘘
- ハードウェアの問題やソフトウェアのバグのような弱い嘘に対応する仕組みを持たせることに価値はある
システムモデルと現実
システムモデル: 分散アルゴリズムが前提としているシステム中で生じると予想されるフォールトの種類の定式化
タイミングの前提に関する 3 つのシステムモデル
- 同期モデル
- ネットワークの遅延やプロセスの一時停止機関、クロックの誤差に限度があることを前提とする
- ほとんどの分散システムにおいて同期モデルは現実的ではない
- 部分同期モデル
- システムのほとんどの場合同期モデルのように振舞うものの、ネットワークの遅延、プロセスの一時停止、クロックの変動が上限をこえることが時々生じることを意味する
- 非同期モデル
- タイミングに関するいかなる前提を置くことも許されない
- クロックを持っていないので、タイムアウトは利用できない
- タイミングに関するいかなる前提を置くことも許されない
- 同期モデル
ノードに関する一般的なモデル
アルゴリズムの正しさ
- アルゴリズムの正しさは、その性質を記述することによって定義できる
例フェンシングトークンの性質
あるアルゴリズムが何らかのシステムモデルにおいて正しいとは、そのシステムモデルにおいて生じうるあらゆる状況下においてその性質が満たされること
安全性とライブ性
ライブ性の性質: 最終的にはよいことが生じること。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 value は nil
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") }
複数のケースに読み取りまたは書き込みが可能なチャネルがある場合はどうななるか?
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チャネルを使用して、選択でケースを無効にすることができる
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 }
Learning Go Chapter 9. Modules, Packages, and Imports
モジュール、パッケージ、インポート
リポジトリとモジュール、パッケージ
Go のライブラリ管理は3つの概念から成り立つ
- リポジトリ: github のようなバージョン管理システムにおけるプロジェクト
- モジュール: リポジトリに格納された Go ライブラリやアプリケーション
- パッケージ: モジュールを構成するコード群
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ファイルには、同一のパッケージ句が必要
- パッケージの名前がインポートパスではなく、パッケージ句によって決定される
- 原則として、パッケージの名前は、パッケージを含むディレクトリの名前と一致させる必要がある
- ただし、パッケージにディレクトリとは異なる名前を使用する場合がいくつかある
パッケージの命名
util
みたいなパッケージ名はやめよう
モジュールを構成する方法
- モジュールが小さい場合一つのモジュールのみで構成しよう
- モジュールが1つ以上のアプリケーションで構成されている場合は、モジュールのルートに
cmd
というディレクトリを作成するcmd
内で、モジュールから構築されたバイナリごとに1つのディレクトリを作成する
- モジュールのルートディレクトリに、プロジェクトのテストとデプロイを管理するための多くのファイル(シェルスクリプト、継続的インテグレーション構成ファイル、Dockerfilesなど)が含まれている場合は、すべてのGoコード(cmdの下のメインパッケージを除く)をpkgというディレクトリに入れる
- 機能のスライスごとにコードを整理する
- 例
- 顧客管理をサポートするすべてのコードを1つのパッケージに配置
- 在庫を管理するすべてのコードを別のパッケージに配置
- 参考: GopherCon 2018: Kat Zien - How Do You Structure Your Go Apps
- 例
パッケージ名のオーバライド
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、モジュールの依存関係、およびモジュールに依存するオープンソースプロジェクトを公開
モジュールの公開
セッションの分離性レベルを変更、確認する方法
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