- 投稿日:2020-07-03T16:29:45+09:00
【Go言語】contextパッケージを理解する-概要編-
Go言語でAPI開発をしていて、一番多く使う標準パッケージと言っても過言ではないcontext。
その動作はなんとなく理解はしているものの、実際のコードを読んで深く理解したことがなかったため、今回は実際にコードやGoDocを読んで中身をまとめていこうと思います。
概要
contextは、Go1.7から標準パッケージに組み込まれています。
まずはGo公式ブログのcontextについてまとめられた記事のイントロをそのまま訳してみます。
In Go servers, each incoming request is handled in its own goroutine. Request handlers often start additional goroutines to access backends such as databases and RPC services. The set of goroutines working on a request typically needs access to request-specific values such as the identity of the end user, authorization tokens, and the request's deadline. When a request is canceled or times out, all the goroutines working on that request should exit quickly so the system can reclaim any resources they are using.
Goサーバーでは、サーバーに来たリクエストはそれぞれそれ自身のゴルーチンで処理されます。 リクエストハンドラーはよくデータベースやRPCサービスといったバックエンドにアクセスするために追加でゴルーチンを起動します。 リクエストの処理を行っているゴルーチンは、通常エンドユーザーのアイデンティティや認証トークン、リクエストの期限などリクエスト固有の値へのアクセス権が必要です。 リクエストがキャンセルされたりタイムアウトした場合には、システムがそれらのゴルーチンが使っていたリソースを再度要求することができるように、 そのリクエストの処理を行っていたすべてのゴルーチンは素早く終了すべきです。
つまりcontextパッケージが担う役割は別サーバへのリクエストのように時間のかかる処理をgoroutineで実行する場合に、処理に適切なTimeoutやDeadlineを設定して処理が停滞するのを防ぐ役割やCancel行うことで適切にネットワークのリソースを開放する役割を担います。
さらにはそのgoroutineは別のgoroutineを呼び出しそれがまた別の…と呼び出しの連鎖は深くなることが考えられます。その場合も親のTimeoutに合わせてその子は全て適切にキャンセルされリソースは解放されるべきです。
また、そのほかにも認証情報などをkvとして受け渡す機能も備わっています。キャンセルのためのシグナルをAPIの境界を超えて受け渡すための仕組みである.ある関数から別の関数へと,親から子へと,キャンセルを伝搬させることが可能になっていっます。
また、ContextはAPIの境界を越えて期限とキャンセルシグナルとリクエスト固有の値を保持します。
メソッドは複数のゴルーチンから同時に呼び出されても安全なものとなっています。さらにはCancelのシグナルをAPI挟んで受け渡すことで、関数間でキャンセルを伝達することができるようになっています。
要するに
- Goのサーバーで入ってきた各リクエストは個別のgoroutineで処理される。
- リクエストのハンドラは、データベースやRPCのサービスにアクセスする。
- リクエスト上で動作するgoroutineはユーザー識別子や認証トークン、リクエストの期限みたいな特定のリクエストの値にアクセスする必要がある。
- リクエストがタイムアウトされた時、システムがそのリソースを再度利用できるように、すべての動作しているゴルーチンも素早く終了されるべきである。Contextインターフェイス
ここはcontextパッケージで定義されているインターフェイスをみていきます。
golang.org/x/net/contextにある
// A Context carries a deadline, cancelation signal, and request-scoped values // across API boundaries. Its methods are safe for simultaneous use by multiple // goroutines. type Context interface { // Deadline returns the time when work done on behalf of this context // should be canceled. Deadline returns ok==false when no deadline is // set. Successive calls to Deadline return the same results. Deadline() (deadline time.Time, ok bool) // Done returns a channel that's closed when work done on behalf of this // context should be canceled. Done may return nil if this context can // never be canceled. Successive calls to Done return the same value. // The close of the Done channel may happen asynchronously, // after the cancel function returns. // // WithCancel arranges for Done to be closed when cancel is called; // WithDeadline arranges for Done to be closed when the deadline // expires; WithTimeout arranges for Done to be closed when the timeout // elapses. // // Done is provided for use in select statements: // // // Stream generates values with DoSomething and sends them to out // // until DoSomething returns an error or ctx.Done is closed. // func Stream(ctx context.Context, out chan<- Value) error { // for { // v, err := DoSomething(ctx) // if err != nil { // return err // } // select { // case <-ctx.Done(): // return ctx.Err() // case out <- v: // } // } // } // // See https://blog.golang.org/pipelines for more examples of how to use // a Done channel for cancellation. Done() <-chan struct{} // If Done is not yet closed, Err returns nil. // If Done is closed, Err returns a non-nil error explaining why: // Canceled if the context was canceled // or DeadlineExceeded if the context's deadline passed. // After Err returns a non-nil error, successive calls to Err return the same error. Err() error // Value returns the value associated with this context for key, or nil // if no value is associated with key. Successive calls to Value with // the same key returns the same result. // // Use context values only for request-scoped data that transits // processes and API boundaries, not for passing optional parameters to // functions. // // A key identifies a specific value in a Context. Functions that wish // to store values in Context typically allocate a key in a global // variable then use that key as the argument to context.WithValue and // Context.Value. A key can be any type that supports equality; // packages should define keys as an unexported type to avoid // collisions. // // Packages that define a Context key should provide type-safe accessors // for the values stored using that key: // // // Package user defines a User type that's stored in Contexts. // package user // // import "context" // // // User is the type of value stored in the Contexts. // type User struct {...} // // // key is an unexported type for keys defined in this package. // // This prevents collisions with keys defined in other packages. // type key int // // // userKey is the key for user.User values in Contexts. It is // // unexported; clients use user.NewContext and user.FromContext // // instead of using this key directly. // var userKey key // // // NewContext returns a new Context that carries value u. // func NewContext(ctx context.Context, u *User) context.Context { // return context.WithValue(ctx, userKey, u) // } // // // FromContext returns the User value stored in ctx, if any. // func FromContext(ctx context.Context) (*User, bool) { // u, ok := ctx.Value(userKey).(*User) // return u, ok // } Value(key interface{}) interface{} } }ここでインターフェイスの各メソッドを見ていこうと思います。
//いつこのcontextがキャンセルされるかを返す。 Deadline() (deadline time.Time, ok bool) //channelがクローズされていれば、その理由を返す。 Err() error //コンテキストがキャンセルされたりタイムアウトした場合にcloseされ、channelを返す Done() <-chan struct{} //同コンテキストでに保存した値のkeyを指定するとvalueを返す Value(key interface{}) interface{}変数
//Canceled は,コンテキストがキャンセルされたときに Context.Err によって返されるエラー var Canceled = errors.New("context canceled") //DeadlineExceeded は,コンテキストの期限が過ぎたときに Context.Err によって返されるエラー var DeadlineExceeded error = deadlineExceededError{}type
CancelFunc は,作業を中止するよう指示します。 CancelFunc は,作業が停止するのを待ちません。 CancelFunc は,複数のゴルーチンから平行に呼び出すことができます。 最初の呼び出しの後, CancelFunc への後続の呼び出しは何もしません。
type CancelFunc func()function
WithCancel ¶
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)WithCancel は、新しい Done チャンネルを持つ親のコピーを返します。返されたコンテキストの Done チャンネルは、返された cancel 関数がコールされたとき、あるいは親コンテキストの Done チャンネルがクローズされたときのいずれか早いほうでクローズされます。
このコンテキストをキャンセルすると、コンテキストに関連付けられているリソースが解放されるので、このコンテキストで実行されている操作が完了したらすぐに cancel をコールするようにしましょう。
サンプルコード
https://play.golang.org/p/PUtYHTQbNpG
WithDeadline ¶
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)親コンテキストのデッドラインがすでに d よりも早くなっている場合は、WithDeadline(parent, d) は意味的に parent と同等です。返されたコンテキストの Done チャンネルは、デッドラインの期限が切れたとき、 返されたキャンセル関数が呼び出されたとき、または親コンテキストの Done チャンネルが閉じられたときのいずれか早いほうで閉じられます。
このコンテキストをキャンセルすると、コンテキストに関連付けられているリソースが解放されるので、このコンテキストで実行されている操作が完了したらすぐに cancel をコールするようにしましょう。
指定した時間が経過ではなく指定した時刻になったらcancelが走ります。
サンプルコード
https://play.golang.org/p/xdwLGigcY7U
WithTimeout ¶
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)WithTimeout は WithDeadline(parent, time.Now().Add(timeout)) を返します。
timeoutの指定付きのWithCancel。第2引数で指定した時間が経過するとcancelが走ります。サンプルコード
https://play.golang.org/p/Rta-7ZzGFla
Background ¶
func Background() ContextBackground は、null ではない空の Context を返します。キャンセルされることはなく、値もなく、期限もありません。これは通常、メイン関数、初期化、テストで使用され、 リクエストを受け取る際のトップレベルのコンテキストとして使用されます。
TODO ¶
func TODO() ContextTODO は、nullではないの空の Context を返します。どのコンテキストを使用するかが不明な場合や、まだ利用できない場合 (周囲の関数がまだ拡張されていないため、Context パラメータを受け付けることができません) には、コードは context.TODO を使用しなければなりません。
BackgroundとTODOの違いは?
機能的には何もありません。これらはビット単位で全く同じ値です。
違いは意図の違い。
context.TODO()で生成されるcontext.Contextは関数名のとおり、一時的なcontext.Contextになります。 GoDocにも「どのcontext.Contextを使うかわからないとき、他の関数が対応していなくてcontext.Contextが用意できないときに使ってね。」のように記載されている。
context.TODO() は、将来的にコンテキストを作成する必要がなくなることが予想される場合に使われることを意味している。
しかし、実際にはどちらも同じように動作します。
WithValue ¶
func WithValue(parent Context, key, val interface{}) ContextWithValue は、キーに関連付けられた値が val である親のコピーを返します。
context Values は、プロセスや API を通過するリクエスト・スコープされたデータにのみ使用し、パラメータを関数に渡す場合には使用しません。
提供されるキーは同等のものでなければなりません。また、コンテキストを使用するパッケージ間の衝突を避けるために、文字列型やその他の組み込み型であってはなりません。WithValue のユーザは、鍵の型を独自に定義しなければなりません。インターフェイス{}に代入する際の割り当てを避けるために、コンテキストキーはしばしば具象型の struct{} を持つことがあります。あるいは、エクスポートされたコンテキストキー変数の静的型はポインタかインターフェースでなければなりません。
サンプルコード
https://play.golang.org/p/ePfJSs9v14Z
以上がGoのcontextパッケージの概要になります。
Go playgroundの簡単なコードも置いているので、ぜひ動かしてみてください。
- 投稿日:2020-07-03T16:26:46+09:00
【Go言語】contextパッケージを理解する-実践編-
実際にContextを使ってみる
キャンセルの伝播
package main import ( "context" "fmt" "time" ) func main() { // contextを生成 ctx := context.Background() ctxParent, cancel := context.WithCancel(ctx) go parent(ctxParent, "Hello-parent") time.Sleep(5 * time.Second) cancel() time.Sleep(1 * time.Second) fmt.Println("main end") } func parent(ctx context.Context, str string) { childCtx, cancel := context.WithCancel(ctx) go child(childCtx, "Hello-child") defer cancel() // 無限ループ for i := 1; i <= 1000; i++ { select { case <-ctx.Done(): fmt.Println(ctx.Err(), str) return case <-time.After(1 * time.Second): fmt.Printf("%s:%d sec..\n", str, i) } } } func child(ctx context.Context, str string) { // 無限ループ for i := 1; i <= 1000; i++ { select { case <-ctx.Done(): fmt.Println(ctx.Err(), str) return case <-time.After(1 * time.Second): fmt.Printf("%s:%d sec..\n", str, i) } } }https://play.golang.org/p/88dYnmE92yH
もちろんWithDeadlineやwithTimeoutを使うと明示的に指定した時間以上にかかっている場合にその処理をキャンセルすることが可能。
context導入によるコード比較
context利用なし
func handler(w http.ResponseWriter, r *http.Request) { doneCh := make(chan struct{}, 1) errCh := make(chan error, 1) go func() { errCh <- request(doneCh) }() // 別途goroutineを準備してTimeoutを設定する go func() { <-time.After(2 * time.Second) // Timeout後にdoneChをクローズする // 参考: https://blog.golang.org/pipelines close(doneCh) }() select { case err := <-errCh: if err != nil { log.Println("failed:", err) return } } log.Println("success") } func request(doneCh chan struct{}) error { tr := &http.Transport{} client := &http.Client{Transport: tr} req, err := http.NewRequest("POST", backendService, nil) if err != nil { return err } errCh := make(chan error, 1) go func() { _, err := client.Do(req) errCh <- err }() select { case err := <-errCh: if err != nil { return err } case <-doneCh: // キャンセルが実行されたら適切にリクエストを停止して // エラーを返す. tr.CancelRequest(req) <-errCh return fmt.Errorf("canceled") } return nil }context利用
func handler(w http.ResponseWriter, r *http.Request) { . ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() errCh := make(chan error, 1) go func() { errCh <- request3(ctx) }() select { case err := <-errCh: if err != nil { log.Println("failed:", err) return } } log.Println("success") } func request(ctx context.Context) error { tr := &http.Transport{} client := &http.Client{Transport: tr} req, err := http.NewRequest("POST", backendService, nil) if err != nil { return err } // 新たにgoroutineを生成して実際のリクエストを行う // 結果はerror channelに投げる errCh := make(chan error, 1) go func() { _, err := client.Do(req) errCh <- err }() select { case err := <-errCh: if err != nil { return err } // Timeoutが発生する,もしくはCancelが実行されるとChannelが返る case <-ctx.Done(): tr.CancelRequest(req) <-errCh return ctx.Err() } return nil }valueにはどんな値を格納すべきか?
・map[interface{}]interface{}であり、完全に型安全ではなく、コンパイラでチェックできないということです。
・文字列やデータ構造体のような値を格納するためにコンテキストを使用し、ポインタやハンドルのような参照を格納するためにコンテキストを使用するのは避けたほうが良い
・リクエストのライフサイクルが始まる前に、そこに入れた情報がミドルウェアチェーンで利用可能かどうかも判断基準にhttps://peter.bourgon.org/blog/2016/07/11/context.html
context使用上の注意とまとめ
- 構造体の中には、Contextを保持してはならない。Contextが必要な関数には明示的に渡す事。また、Contextは第1引数であるべきで、だいたいはctxと名付ける
→contextを含んだpublicな構造体を定義すると、いろんなところからcontexを使えてしまうため、意図しないcancelや親子関係が複雑になってしまうから
関数側が許容するとしても、nilのContextを渡してはいけない。どのコンテキスト渡して良いか確証が持てない時は、Context.TODOを使って空のContextを渡す。
同じContextは別々に実行されているgoroutineで関数渡しても良い。Contextは複数のgoroutineから同時に使われても安全。
Context leakを避ける。WithCancelやWithTimeout,WithDeadlineで返されるcancelが呼ばれないと,その親Contextがcancelされるまでその子ContextがLeakする。→go vetで指摘してくれる
contextをもつ関数は適切なキャンセル処理を実装するべきである.この関数を使う側は呼び出し側(つまり親context)でTimeoutが発生した,もしくはCancelを実行した場合に適切にキャンセル処理・リソースの解放が実行されることを期待する。
ContextのValueはAPIやプロセスをまたぐリクエストスコープな値だけに使う。オプショナルな値を関数に渡すためではない。
contextはos,net,net/httpなど他のパッケージでも使われている
- 投稿日:2020-07-03T12:27:33+09:00
Golang+Docker+LINE BOT+QiitaAPI+herokuで何か作ろうとした話
記事概要
Golang+Docker+LINE BOT+QiitaAPI+herokuで何か作ろうとしてハマった事とか色々まとめるだけの記事。
筆者について
- Golang初見。
- linebot、herokuはPHPとRailsで触った事がある。
- Dockerは便利すぎてハマりだしてる。
リポジトリ
https://github.com/YanaPIIDXer/QiitaLineBot
実際に動作させるherokuとLINEのアカウントは非公開で。。。
「どんなものを作るか」「設計はどうするか」はここのREADMEに。採用したもの
- gin
- Webサーバライブラリ。
- Realize
- ホットリロード。
- 後述。
Golangのホットリロード
「コードを書き換えると自動でビルドが走って、後はブラウザを更新するだけ」と言う機能。
導入
Realizeを使ってGoでホットリロードを実現する
上記記事を参考にRealizeを導入。
・・・が、Realize起動時のビルドは走ってるっぽいがホットリロードは機能しない。legacyの有効化
どうもRealizeをDocker(コンテナ)上で動作させるには、legacyを有効化する必要がある模様。
ここを参照。
.realize.yamlの記述を.realize.yamlsettings: legacy: force: true (省略)として再起動したら無事動作。
herokuへのdeploy
ポート
ローカル環境で立ち上げている時はポート3000を使用しているが、heroku上で動作させる場合はherokuが指定したポートで処理をしなければならない。
プログラム側の対応
環境変数「PORT」に値が入っていればheroku上での動作としてそのポートを、入っていなければローカルでの動作としてポート3000を利用するようにしてみよう。
Dockerfile# 以下の記述を追加。 ENV PORT=${PORT}main.go// サーバ起動部分。 var port = os.Getenv("PORT") // heroku上で動かす場合は指定されたPortじゃないと死なますよ。 if port == "" { port = "3000" } fmt.Println("Server Port:" + port) router.Run(":" + port)これでdeployしたら動・・・かなかった。
コンテナ構成の変更
色々調査しながら、ふとdocker-compose.ymlを眺めると・・・
docker-compose.yml(省略) ports: - "3000:3000" command: realize start --name='server' --run (省略)・・・
docker-compose.ymlports: - "3000:3000"お 前 か 。
ここもherokuが使ってるポートにしてやらないと動かないんじゃないかな・・・?色々調べて、以下の記事がヒット。
golang,docker,mysqlの環境をherokuにデプロイするこの記事にある通りにコンテナ構成を変更。
上記の記事ではdocker-compose.ymlをdockersディレクトリ内に配置しているが、同じようにすると動作せず。
多分どこかでパスに問題が出てるんじゃないかな・・・?未調査だけど。
面倒なのでルートディレクトリに別名(docker-compose-local.yml)で放り込むと動作するようになった。herokuにdeployして、ログを見た感じエラーとかも無さそう。
いざアクセス・・・が、動作せず。SSL対応
「あれ?」と思っておもむろに
heroku logs
を実行。
アクセス時にエラーになってた。
で、そのログをよく見ると・・・protocol=https・・・FxxK!!
こりゃまた頭の痛い問題だ・・・。
(今ここ。進展あれば更新予定。)
- 投稿日:2020-07-03T12:27:33+09:00
Golang+Docker+herokuで何か作ろうとした話
記事概要
Golang+Docker+herokuで何か作ろうとしてハマった事とか色々まとめるだけの記事。
筆者について
- Golang初見。
- herokuはPHPとRailsで触った事がある。
- Dockerは便利すぎてハマりだしてる。
採用したもの
- gin
- Webサーバライブラリ。
- Realize
- ホットリロード。
- 後述。
リポジトリ
https://github.com/YanaPIIDXer/QiitaLineBot
試行錯誤の経緯とかはここのコミットログを見れば大体分かる・・・かも。
ちなみに今回ブランチ分岐は無しの方向。Golangのホットリロード
「コードを書き換えると自動でビルドが走って、後はブラウザを更新するだけ」と言う機能。
導入
Realizeを使ってGoでホットリロードを実現する
上記記事を参考にRealizeを導入。
・・・が、Realize起動時のビルドは走ってるっぽいがホットリロードは機能しない。legacyの有効化
どうもRealizeをDocker(コンテナ)上で動作させるには、legacyを有効化する必要がある模様。
ここを参照。
.realize.yamlの記述を.realize.yamlsettings: legacy: force: true (省略)として再起動したら無事動作。
herokuへのdeploy
ポート
ローカル環境で立ち上げている時はポート3000を使用しているが、heroku上で動作させる場合はherokuが指定したポートで処理をしなければならない。
プログラム側の対応
環境変数「PORT」に値が入っていればheroku上での動作としてそのポートを、入っていなければローカルでの動作としてポート3000を利用するようにしてみよう。
Dockerfile# 以下の記述を追加。 ENV PORT=${PORT}main.go// サーバ起動部分。 var port = os.Getenv("PORT") // heroku上で動かす場合は指定されたPortじゃないと死なますよ。 if port == "" { port = "3000" } fmt.Println("Server Port:" + port) router.Run(":" + port)これでdeployしたら動・・・かなかった。
コンテナ構成の変更
色々調査しながら、ふとdocker-compose.ymlを眺めると・・・
docker-compose.yml(省略) ports: - "3000:3000" command: realize start --name='server' --run (省略)・・・
docker-compose.ymlports: - "3000:3000"お 前 か 。
ここもherokuが使ってるポートにしてやらないと動かないんじゃないかな・・・?色々調べて、以下の記事がヒット。
golang,docker,mysqlの環境をherokuにデプロイするこの記事にある通りにコンテナ構成を変更。(Realizeはローカルでのみ動作させるように。)
上記の記事ではdocker-compose.ymlをdockersディレクトリ内に配置しているが、同じようにすると動作せず。
多分どこかでパスに問題が出てるんじゃないかな・・・?未調査だけど。
面倒なのでルートディレクトリに別名(docker-compose-local.yml)で放り込むと動作するようになった。herokuにdeployして、ログを見た感じエラーとかも無さそう。
いざアクセス・・・が、動作せず。
SSL絡みかと思ったが、普通にhttpでアクセスしても同じ結果だった。No web processes running
ログをみた所、「No web processes running」と言うエラーメッセージが。
これを調べた所、以下の記事に行き着いた。
【Heroku】デプロイ後にcode=H14 desc="No web processes running"記事にある通り、
heroku ps:scale web=1
を叩いたところ、動作はしなかったもののエラーメッセージが変わった。
(記事では「このコマンドを叩いただけでは動作せず、heroku.ymlが必要」との事だったが、自分はRealizeをローカルでのみ動作させるようにする過程で作成済みなのでこのコマンドだけでOKだった。)App crashed
何かクラッシュしたらしい。
このエラーメッセージで調べたところ、以下の記事がヒット。
HerokuでApplicationErrorが発生したときの対処法この記事に書かれていたコマンドで再起動。しかし変わらず。
そこからログを少し遡ると、
.env Load Failed.
。
「・・・これってさ、.envの読み込み失敗時にそのままエラーとしてreturnしてた時のログだよね?herokuへのdeploy作業の過程でそのエラー処理は撤去したんだけど・・・」
「何かしらの原因で過去のやつが動作しているのでは?」と言う疑いが。再度deployしてアクセス→router.Runメソッドが走るところまでは確認したがApp crashは解消されず→再度上記記事のコマンドで再起動→
.env Load Failed.
気になる事としては、「herokuへのdeploy時、releaseから処理が返ってこない」と言う事。(毎回Ctrl+Cしてる。)
「コマンドがフォアグラウンドで走ってるとか・・・?」とか考えて、適当にENTRYPOINTをlsに変えてみても結果は同じ。
で、ENTRYPOINTがlsのものが動いてる(・・・と思い込んでいる)状態でheroku logs
。
普通にGoのプログラムが動いていた。
・・・まぁ、「処理が返ってこない」と言いながらCtrl+Cで強制的に抜けてるんだからそりゃそうだわな。アプリケーションの作り直し
とりあえず
heroku destroy
で一旦破棄。
heroku create
で作り直す。
再度pushしてアクセスすると無事動作。
app crashedの原因はよく分からないが、一旦作り直してみるのも手ではある。(今回は「過去のプログラムが動作した」と言う事から、「ゴミが残っている」と言う事が明白だったので。)ちなみにここでdeploy方法を
git push heroku master
からheroku container:push web
→heroku container:release web
にも変更している。
(自分はdocker-composeを使ったビルドしかした事が無く、docker-composeを使わずにコンテナを単体で動かす時にどうやってコンテナ名を付けるのかが分からなかった為。。。これもここで覚えた。)最後に
「GolangをDockerでインストールし、heroku上で動作させる」まででハマったところを色々まとめてみた。
冒頭に書いたリポジトリでのアプリケーション開発活動自体はまだまだ継続するものの、今回の表題である「Golang+Docker+heroku」での動作までは進み、「後はGolang内で色々と実装するだけ」と言う段階に至ったので、ひとまずこの記事は完結とする。
(それに伴い、余計な記述も撤去。)ここまで来るのに数日を要したが、その分多くのことをインプットできたように思える。
そこでブチ当たった様々な問題とその解決策をなるべくリアルタイムで更新してきた。
「誰かに伝える事」は正直苦手だから色々と拙い所はあると思うが、こういった記事でも役に立ってくれれば幸いである。
- 投稿日:2020-07-03T10:26:10+09:00
GO言語学習メモ ~ Embeddedについて~ GOには継承がない
はじめに
完全初心者で、GO言語の学習を進めています。
学んだ事のメモとし記事を投稿します。理解できていない部分が、多くありますが随時編集しますのでお手柔らかにお願いします。
継承について再確認
あるクラスに定義されたメソッドを、別の新規クラスで利用できるようにした上でクラスを定義することを継承と言う。
クラスの継承には、親クラスと子クラスの関係があり、元となるクラスを親クラス、親クラスのメソッドを引き継ぎ新しく作成するクラスを子クラスと呼ぶ。
ずっとrubyを使っていので、rubyでイメージ式
rubyclass 子クラス名 < 親クラス名 endGoで継承みたいな事する
GO言語にはクラスがないので、上に書いた様な事ができないって事。
Goで継承で継承みたいな事をしたい時は、Embeddedの仕組みを使用できるみたいです。
実際に色を見てみる
package main import "fmt" //最初に定義========= type Vertex struct { x,y int } func (v Vertex) Area() int{ return v.x * v.y } func (v *Vertex) Scale(i int) { v.x = v.x * i v.y = v.y * i } //=============== //注目する式① type Vertex3D struct { Vertex z int } //注目する式② func (v Vertex3D) Area3D() int{ return v.x * v.y * v.z } //注目する式③ func (v *Vertex3D) Scale3D(i int) { v.x = v.x * i v.y = v.y * i v.z = v.z * i } //注目する式④ func New (x,y,z int) *Vertex3D{ return &Vertex3D{Vertex{x,y}, z} } //注目する式⑤ func main() { v := New(3,4, 5) //注目する式⑤ v.Scale(10) fmt.Println(v.Area()) fmt.Println(v.Area3D()) }最初の定義した、x,yのint型を継承をみたいな事させたいと時、注目する式①の様な書き方をする事で元の定義したx,yのint型に、新たにz int型を加える事ができる。
Vertex3D定義し、中身に最初に定義したVertexを加える事で、Vertex3Dの中にx,y,z int型が定義された状態になる
//注目する式① type Vertex3D struct { Vertex z int }あとは、func main()で呼び出す値を変えていけば、継承の様な事ができる。
終わりに
はじめに定義したものを、新しく定義するもの中に書いてあげれば上手くイメージなのかな。。
まだまだ、勉強が足りないですね、、、
内容は随時編集します!
- 投稿日:2020-07-03T09:10:10+09:00
go修行15日目 サードパーティ製パッケージを使う
サードパーティパッケージのインストール
- 株価分析のためのパッケージ
https://github.com/markcheno/go-talib
インストール
go get github.com/markcheno/go-talib go get github.com/markcheno/go-quoteインストールされる場所
PS C:\Users\yuta\go\src\github.com> ls .\markcheno\ ディレクトリ: C:\Users\yuta\go\src\github.com\markcheno Mode LastWriteTime Length Name ---- ------------- ------ ---- d----- 2020/07/03 8:45 go-talibサンプルコード
package main import ( "fmt" "github.com/markcheno/go-quote" "github.com/markcheno/go-talib" ) func main() { spy, _ := quote.NewQuoteFromYahoo("spy", "2016-01-01", "2016-04-01", quote.Daily, true) fmt.Print(spy.CSV()) rsi2 := talib.Rsi(spy.Close, 2) fmt.Println(rsi2) }出力
datetime,open,high,low,close,volume 2016-01-04 00:00,200.49,201.03,198.59,184.03,222353500.00 2016-01-05 00:00,201.40,201.90,200.05,184.34,110845800.00 2016-01-06 00:00,198.34,200.06,197.60,182.01,152112600.00 2016-01-07 00:00,195.33,197.44,193.59,177.64,213436100.00パッケージ自体を呼ばせない場合
- importしたパッケージが呼ばれないとビルド時にエラーになるため
package main import ( "fmt" "github.com/markcheno/go-quote" _ "github.com/markcheno/go-talib" ) func main() { spy, _ := quote.NewQuoteFromYahoo("spy", "2016-01-01", "2016-04-01", quote.Daily, true) fmt.Print(spy.CSV()) // rsi2 := talib.Rsi(spy.Close, 2) // fmt.Println(rsi2) }godoc
- ローカル環境でgodocを見ることができる
- Example関数などで自分で書いたコードをExampleとしてみることができる
インストール
go get golang.org/x/tools/cmd/godoc利用方法
godoc -http=:6060アクセス
- 投稿日:2020-07-03T06:08:05+09:00
fluentdでdockerコンテナのログをファイルごとに出し分けてみたお話
fluentdとは
fluentdとはログ収集ツールです。
様々な形式のinputからログを収集でき、それを必要に応じて整形・加工してログ出力できます。環境
- OS:debian 10.3
- docker:19.03.11
- docker-compose:1.16.1
- fluentd:1.3.2
今回の目標
今回の目標はサーバーのログを標準出力からファイル出力へ切り替えることです。
さらに、ファイル出力も全てのログを出力するログファイルと、エラーログのみを出力するエラーログファイルの2種類作成していきます。fluentdの設定ファイルについて
デフォルトでは、
/fluentd/etc/fluent.conf
が読み込まれます。tag
fluentdではログをtagというもので管理していきます。
このtagを使って設定を任意のログのみに適用できたりします。ディレクティブ
fluentdの設定はディレクティブと呼ばれるもので設定していきます。
ディレクティブには下記のようなものがあります。
- source: ログの入力を設定するディレクティブ。
- match: ログの出力を設定するディレクティブ。引数にtagを指定。
- filter: ログに対して行う処理を設定するディレクティブ。引数にtagを指定。
- label:
@label
でラベル名を設定することで、<label ラベル名>
の設定にルーティングできる。下記はmatchディレクティブの例です。
<match myapp.access> @type file path /var/log/fluent/access </match>
myapp.access
の部分がtagになります。
@type
の部分が次に説明する、プラグインを指定しています。設定ファイルの主なディレクティブの流れとしては、
< source > → < filter > や < label >など → ...... → < match >
となることを頭に入れておくと設定ファイルの読み書きがしやすくなると思います。
参考:ディレクティブ 公式ドキュメントプラグイン
fluentdには標準のプラグインが豊富にあります。
プラグインは先ほどのディレクティブ内に@type
で指定することで利用できます。
このプラグインを使うことで様々な形式でログを入出力できたり、ログを加工したりできるようになります。下記は filterディレクティブ のプラグインの一部です。
- record_transformer:ログにフィールドを追加したり削除したりできる。
- grep:正規表現でマッチするログだけを通したり、マッチするログを出力しないようにしたりできる。
- parser:ログ内のフィールドをjsonなどの形式を指定してパースすることができる。
今回使う各ファイル
ファイル階層
├── app │ └── main.go ├── docker │ ├── app │ │ ├── app │ │ └── Dockerfile │ ├── docker-compose.yml │ └── fluentd │ ├── config │ │ └── fluent.conf │ ├── Dockerfile │ └── log │ ├── alert.buf │ └── log.buf └── Makefileapp/main.go
今回はこちらの超簡易サーバーを使っていきたいと思います。
main.gopackage main import ( "fmt" "net/http" "github.com/sirupsen/logrus" ) func main() { // server起動 http.HandleFunc("/sample", outputQueryString) http.ListenAndServe(":8080", nil) } func outputQueryString(w http.ResponseWriter, r *http.Request) { log := logrus.New() // 処理開始のログ log.Infoln("Start!") // クエリストリングからキーが name の値を取得 qs := r.URL.Query().Get("name") // キーが name の値がない場合のエラー処理 if qs == "" { log.Errorln("Query string is nothing.") w.WriteHeader(http.StatusBadRequest) return } // name があった場合の出力 fmt.Fprintf(w, "Your name is %s", qs) log.Infoln("Complete!") }このサーバーのログをfluentdで収集します。
こちらのサーバーは、
- (IPアドレス):8080/sample?name=XXX にアクセスすると、「Your name is XXX」を返す
- (IPアドレス):8080/sample?key=XXX のようにクエリストリングのキーに name の値がない場合、エラーログを出力し、400エラーを返す
という処理内容になってます。
出力されるログは、
- アクセスされたときに処理開始のログ
Start!
を出力- key が name のクエリストリングがなかった場合、エラーログ
Query string is nothing.
を出力- 処理が正常終了した場合、処理完了のログ
Complete!
を出力となっています。
なので、ログファイルには1, 2, 3のログが、エラーログファイルには2のログのみが出力されることになります。docker/app/app
app/main.go の実行ファイル。
こちらをappのコンテナ内にコピーして実行します。docker/app/Dockerfile
DockerfileFROM alpine:latest COPY app /bin CMD "app"今回、appのdockerイメージはalpineを使用しています。
alpineを選択した理由はdockerイメージの容量が小さくなるからです。
詳しくはこちらの記事を参考にしてみてください。
Alpine Linux で Docker イメージを劇的に小さくするDockerfileの内容としては、先ほどのappの実行ファイルをコンテナ内の /bin ディレクトリにコピーして実行しているだけです。
docker/fluentd/config/fluent.conf (fluentd設定ファイル)
いよいよ、本命のfluentdの設定ファイルです。
ちょっと長いかもしれませんが、ブラウザバックするのは待ってください。
わかります。わたしも設定ファイルアレルギーの重症者ですから。
ですが、安心してください。
部分に分けて説明をしているので、どうか我慢して少し読み進めてみてください。
わからないところがあればコメントいただければできるだけ(アレルギー重症者レベルで)回答します。fluent.conf<source> @type forward @label @mainstream port 24224 </source> <label @mainstream> <match docker.**> @type copy <store> @type relabel @label @all_log </store> <store> @type relabel @label @err_log </store> </match> </label> <label @all_log> <match **> @type file path /var/log/fluent/log_*.log format ltsv buffer_type file buffer_path /var/log/fluent/log.buf symlink_path /var/log/fluent/log_current time_slice_format %Y%m%d flush_at_shutdown true append true </match> </label> <label @err_log> <filter **> @type grep <regexp> key log pattern /level=(warn|error)/ </regexp> </filter> <match **> @type file path /var/log/fluent/alert_*.log format ltsv buffer_type file buffer_path /var/log/fluent/alert.buf symlink_path /var/log/fluent/alert_current time_slice_format %Y%m%d flush_at_shutdown true append true </match> </label>設定ファイルの内容説明
設定ファイルの内容を上から分けて見ていきます。
<source> @type forward @label @mainstream port 24224 </source>この sourceディレクティブでは、inputの設定を定義しています。
取得したログに対して@mainstream
というラベルを設定しているので、この次は<label @mainstream>
の設定に飛びます。<label @mainstream> <match docker.**> @type copy <store> @type relabel @label @all_log </store> <store> @type relabel @label @err_log </store> </match> </label>この
<label @mainstream>
では、ログの複製を行っています。
@type copy
でログの複製ができます。
なぜ複製しているのかというと、この後ログを、全てのログが出力されるログファイルと、エラーログだけが出力されるエラーログファイルに分けたいからです。
複製したログには@all_log
、@err_log
というラベルを再設定しているので、
それぞれ<label @all_log>
、<label @err_log>
のディレクティブに飛びます。<label @all_log> <match **> @type file path /var/log/fluent/log_*.log format ltsv buffer_type file buffer_path /var/log/fluent/log.buf symlink_path /var/log/fluent/log_current time_slice_format %Y%m%d flush_at_shutdown true append true </match> </label>この
<label @all_log>
では、ログファイルの出力設定を行っています。
path
ではファイルの出力先を指定しています。* は後述の time_slice_format で指定したフォーマットで日付や日時が入ります。buffer_type
ではファイルに出力する前のバッファリングをfileに出力するか、メモリに溜めておくかを設定できます。メモリの方が処理は速いですが、fluentdが途中で落ちるとログが消失する可能性があります。buffer_path
ではバッファをfile出力にした際の出力先を指定しています。symlink_path
ではファイル出力しているバッファへのシンボリックリンクを設定しています。これにより、バッファが出力されているファイル名がどんなものであれ、同じファイル名でアクセスできるようになります。time_slice_format
ここで設定したフォーマットでファイル名の * が置き換えられます。デフォルトでは %Y%m%d なので日付単位でファイルが生成されますが、ここの設定を時間単位にすればファイルが1時間ごとに生成されるようになるので、ファイルの生成単位を設定しているとも言えます。flush_at_shutdown
では、fluentdがシャットダウンされた際に最後に1度だけバッファの内容を出力するかどうかを設定しています。trueなら出力を試します。append
では、既にファイル出力先ファイルがある場合、追記するか別ファイルに出力するかを設定しています。trueなら追記します。<label @err_log> <filter **> @type grep <regexp> key log pattern /level=(warn|error)/ </regexp> </filter> <match **> @type file path /var/log/fluent/alert_*.log format ltsv buffer_type file buffer_path /var/log/fluent/alert.buf symlink_path /var/log/fluent/alert_current time_slice_format %Y%m%d flush_at_shutdown true append true </match> </label>この
<label @err_log>
では、エラーログファイルの出力設定を行っています。
filterディレクティブでは、grepプラグインを使い、ログのフィルタリングを行っています。
grepプラグインでは、フィールドのキーを設定して、それに対して正規表現で通すログ通さないログを設定できます。
この設定では、 log というキーに対してlevel=warn
かlevel=error
という値を持つログのみを通します。
matchディレクティブについては、上記ログファイル出力設定と重複するので割愛します。以上が、fluent.confの設定内容です。
label
のおかげで流れを掴みやすく、他の設定ファイルより読みやすいのではと思います。fluentdのDockerfile
DockerfileFROM fluent/fluentd:latestdockerイメージで、fluentdを指定しているだけです。
アプリのcompose.yml
docker-compose.ymlversion: "3.3" services: app: build: ./app container_name: app restart: always ports: - "8080:8080" logging: driver: "fluentd" options: fluentd-address: "localhost:24224" tag: "docker.{{.Name}}" depends_on: - fluentd fluentd: build: ./fluentd container_name: fluentd volumes: - ./fluentd/config:/fluentd/etc - ./fluentd/log:/var/log/fluent restart: always ports: - "24224:24224"appの logging というところでログ出力先の設定を行っています。
こちらのように driver に fluentd を指定することで、fluentdでログを収集できるようになります。
optionsではfluentdのコンテナへの接続とログのtagの設定をしています。fluentdのvolumesでは、2つのディレクトリをマウントしています。
1つめの./fluentd/config
では、デフォルトのfluent.confが格納してあるディレクトリをマウントすることで、設定ファイルを上書きしています。
2つめの./fluentd/log
では、fluentdのログファイルが出力されている /var/log/fluent をマウントすることで、ログファイルをローカルの /docker/fluentd/log のディレクトリから取得できるようにしています。実際に起動してみる
それでは、実際に起動してみます。
$cd docker/; docker-compose up -d --build Creating network "docker_default" with the default driver Building fluentd Step 1/1 : FROM fluent/fluentd:latest ---> 9406ff63f205 Successfully built 9406ff63f205 Successfully tagged docker_fluentd:latest Building app Step 1/3 : FROM alpine:latest ---> a24bb4013296 Step 2/3 : COPY app /bin ---> a57975cbc6b8 Step 3/3 : CMD "app" ---> Running in fa1f4446976e Removing intermediate container fa1f4446976e ---> 515558e9745f Successfully built 515558e9745f Successfully tagged docker_app:latest Creating fluentd ... Creating fluentd ... done Creating app ... Creating app ... done $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES b76f4a1c2bdd docker_app "/bin/sh -c \"app\"" 57 seconds ago Up 56 seconds 0.0.0.0:8080->8080/tcp app b732ea83c8e7 docker_fluentd "/bin/entrypoint.sh …" 58 seconds ago Up 57 seconds 5140/tcp, 0.0.0.0:24224->24224/tcp fluentdfluentdとサーバーのコンテナが起動できました。
では、アクセスしてみましょう。
まずは、 (IPアドレス):8080/sample?name=Qiita でアクセスしてみます。
クエリストリングで指定したQiita
がちゃんと表示されてますね。続いて、 (IPアドレス):8080/sample?key=Qiita でアクセスしてみます。
こちらもちゃんとエラーが返されてます。サーバーは意図通り動いてそうですね。
続いて、ログについて見ていきます。
log_yyyymmdd.log には全てのログが出力されます。
alert_yyyymmdd.log にはエラーログのみが出力されます。
実施日が2020/06/29なので yyyymmdd は 20200629 になります。log_20200629.logsource:stderr log:time="2020-06-29T14:18:06Z" level=info msg="Start!" container_id:1b3e5a8deaaa2322bec250db7a47d67f49dd5654bc037af1730e31db1c4af74d container_name:/app container_id:1b3e5a8deaaa2322bec250db7a47d67f49dd5654bc037af1730e31db1c4af74d container_name:/app source:stderr log:time="2020-06-29T14:18:06Z" level=info msg="Complete!" source:stderr log:time="2020-06-29T14:18:09Z" level=info msg="Start!" container_id:1b3e5a8deaaa2322bec250db7a47d67f49dd5654bc037af1730e31db1c4af74d container_name:/app container_id:1b3e5a8deaaa2322bec250db7a47d67f49dd5654bc037af1730e31db1c4af74d container_name:/app source:stderr log:time="2020-06-29T14:18:09Z" level=error msg="Query string is nothing."alert_20200629.logcontainer_id:1b3e5a8deaaa2322bec250db7a47d67f49dd5654bc037af1730e31db1c4af74d container_name:/app source:stderr log:time="2020-06-29T14:18:09Z" level=error msg="Query string is nothing."log_20200629.log を見てみるとの1行目と3行目には処理開始のログが出力され、2行目と4行目にはそれぞれ正常終了時のログとエラー時のログがすべて出力されていますね。
一方、alert_20200629.log の方には log_20200629.log の4行目と同じログ(エラーログ)が出力されています。
これでエラー時のログのみ見たい場合は alert_20200629.log を見ればよくなりましたね。おわりに
今回はfluentdの設定ファイルの概要と実際に使ってみた実例を紹介しました。
ここまで読んでいただいた方には、fluentdの設定ファイルがそこまで難解ではないというのが少しは伝わったのではないでしょうか?
設定ファイルアレルギーの重症者であるわたしでも簡単な設定ファイルについては記述できるようになりました。
みなさんもぜひこの記事をきっかけにfluentdデビューをしてスマートなログ収集ライフを!