- 投稿日:2019-12-09T23:38:49+09:00
Go + Stackdriver Logging富豪的利用でWebサーバ開発を加速する
サーバーのログが全文検索可能かつ、処理単位で構造化されたログ表示の存在は便利で開発運用が捗ります。例えばサーバとクライアントで開発者が分離している場合、StackdriverのWebコンソールでHTTP Request Header, Request/Response Bodyの3点セットがログ全文検索可能になっているとクライアントE単独で調査できるようになるためクライアントEの開発も加速する場合があります。
Goで構造化ログを出力してWebサーバ開発を加速させる Go3 Advent Calendar 2019 10日目の記事です。9日目はuniさんのdive into iota: iotaはいつ誰が管理しているのか?記事でした。全文検索可能な構造化ログは便利になる反面ログ出力数がどうしても通常より多くなりすぎるため費用と便利さのトレードオフが発生します。本番環境で有効にするかは十分に吟味して検討するとよいでしょう。
GCP詳しい人向けの補足
GAE 1st gen時代に大変便利 だったカスタマイズされたStackdriverログ表示をCloudRunやその他GCPサービスでも同様に出力できるよう試行錯誤して再現した内容となっています。StackdriverからBigQuery export時にもGAE 1st gen同様親子関係を維持したまま同一BQテーブルに出力するよう調整しています。
サンプルアプリについて
ピンを刺したら裏でCloudRunで動作するGolang Serverに問い合わせGoogleMapから住所を応答して表示するサンプルアプリを用意しました。
開発が加速するログとは
1. リクエスト単位で構造化され全文検索可能なログ
「福岡」を含むログを全文検索した例
高速に検索するにはindexが効くプロパティ指定が有効です。例:httpRequest.requestUrl:"/v1"
HTTPのリクエストごとに複数のログ出力が構造化されて記録されているため全文検索が可能になっています。またBigQuery Exportした際に、こちらの子ログが同じBQテーブルにまとめて出力されるためExport前提の場合は必須となります。
2. ネスト構造された親子関係でログ表示
1つのWeb通信ではTraceIDを共通でログ出力するとStackdriverが表示する際に自動でログを親子関係づけネスト表示します。TraceIDとはStackdriverでログ出力時に設定できるパラメータの1つとなります。
みづらい
みやすい(この表示を諦めるなら富豪的なログ出力実装は不要となります)
開発環境だけでも有効にすると、例えばログレベルがWarning以上のログが一目で視認できるようになるなど有効活用できます。
![]()
3. [option]HTTP Request HeaderとRequest/Response Bodyを全部ログ出力
WebAPI開発時に何かと調査が楽に便利になります。開発環境だけでも出力を検討しましょう。全てのWeb通信が通過するproxyサーバを用意するなどして、プロジェクトの通信を1箇所に集約して通信内容を全てログ出力し可視化するとエンジニアの開発手法もそれに依存するようになり少しだけよい変化が発生する場合があります。一方で本番環境で何でもかんでも出力していると 個人情報 がログに平文露出して記録されたり、容量から費用問題が発生するため工夫が必要です。
4. [option]ログをbig queryにexportして集計可能にしておく
Stackdriver Logの保持期間は30日です。消えてしまう前にStackdriver Export機能でBigQueryにアウトプットしておくと後追い調査で活躍する場合があります。
過去に活用した事例(障害や緊急対応ばかり!)
- あるAPIで特定の登録コードを障害発生期間に入力したユーザをすべて調べる
- 特定OSの特定バージョンでとあるボタンを押したユーザ数を通信ログからUserAgentで絞ってすべて調べる
- あるエラーが発生した回数を日別で集計して発生頻度が急増した日時を特定する5. [option]Stackdriver Monitoring連携で異常時のSlack通知を準備する
本番運用が始まるとエラー時に即応しサービスを継続する必要があります。理想は発生させないこと、もしくはユーザ様が気付く前に検知して対処できるのが理想です。また開発規模が拡大すると開発環境やQA環境の停止すらも大きな損失が発生するようになります。Stackdriver Monitoring連携で簡単に各種サービスと通知連携できるため、そういったサービスが存在する点を覚えていると役に立つ日がくるかもしれません。通知オプション | Stackdriver Monitoring
Stackdriver Loggingの要点を2つだけ抑える
Stackdriver Logging仕様に特化したデータ構造を理解しないと、実装をみても理解しにくいため先に説明します。逆にデータ構造さえ理解すれば自由自在です。
1. ログの全文検索 と 構造化されたログを実現する
省略した擬似実装で説明します。
cloud.google.com/go/logging
前提の話です。logging.Entry.Payloadに encoding/json でJSON出力可能なarrayを設定すると実現します。1回のログ出力で構造化したJSONを出力するため、WEBコンソールで全文検索が実現します。詳細はEntry構造体の特にPayloadのコメントを読むとよいでしょう。parent-log// 出力するログをencoding/json でJSON出力できるように設定 // lineはログごとの構造体 type line struct { Severity logging.Severity Payload string Time time.Time } func (l *line) toLog() interface{} { return struct { Severity string Message string Time string }{ Severity: string(l.Severity), // todo: logging.Severityはint型なのでstring型への置き換え実装を推奨(今回は省略) Message: l.Payload, Time: l.Time.Format("2006-01-02 15:04:05.000 -0700"), } } func main() { // ログ文字列をサンプルで設定(実際はInfoF関数などなどで設定) lines := []line{ {logging.Debug, "Debug出力ログ", time.Now()}, {logging.Info, "Info出力ログ", time.Now()}, {logging.Warning, "Warning出力ログ", time.Now()}, {logging.Error, "Error出力ログ", time.Now()}, } // jsonPayload.lines を生成 // Stackdriver Logging で検索した際にjsonPayload.linesに全てのログ文字列が存在するため検索文字列で該当の通信ログが見つかる。 var payload = struct { Lines []interface{} }{} maxSeverity := logging.Info for _, line := range lines { if maxSeverity < line.Severity { maxSeverity = line.Severity // 出力したlogから一番logレベルが高いものを親のlogレベルと設定 } payload.Lines = append(payload.Lines, line.toLog()) } // ログ出力 entry := logging.Entry{ HTTPRequest: logHTTPRequest, // r *http.Request を設定 Trace: TraceID(r), Severity: maxSeverity, Payload: payload, } parentLogger.Log(entry) parentLogger.Flush() // バッファされているログをすべて送信 }2.ネストされた親子関係のログ出力
子ログを大量に出力する実装を行います。Stackdriver LoggingのWebコンソールで親子関係としてログ出力するために、 TraceIDを親子共通にして 1行ずつ子ログを出力します。詳細はGoDoc Grouping Logs by Request ¶ や GAE のログに憧れての記事が詳しいです。
child-log// ネストされた親子関係でログ表示するために子ログを出力するサンプル func main() { // log init logClient, _ := logging.NewClient(context.Background(), googleCloudProject) parentLogger := logClient.Logger( // 親ログ "your_service_log", logging.CommonResource(&monitoredres.MonitoredResource{ Type: logType, Labels: map[string]string{ "project_id": googleCloudProject, }, }), ) childLogger := logClient.Logger("child_log") // 子ログ // 子ログ出力(実際の出力は親ログの Flush() 時に出力される) line := line{logging.Debug, "Debug出力ログ", time.Now()} childLogger.Log(logging.Entry{ Severity: line.Severity, Payload: map[string]interface{}{ "serviceContext": map[string]interface{}{}, "message": line.Payload, }, Resource: &monitoredres.MonitoredResource{ Type: logType, Labels: map[string]string{ "project_id": googleCloudProject, }, }, Trace: TraceID(r), }) }所感
オンプレサーバに溜まったログをtailで監視していた時代から、クラウド時代になりログにも変化の波が訪れつつあります。ログを徹底的に可視化しプロジェクト内に布教成功すると開発者の開発スタイルにも少しだけ変化がおきます。それがよい変化ならきっと別プロジェクト移動時に真似してくれることでしょう。私にとってGAE 1st genのログ出力の完成度は衝撃的で、CloudRun導入時に思わず真似してしまいました。一方でStackdriverはlocal emulatorが存在しておらず、開発は試行錯誤が必要になります。この記事が少しでも助けになれば嬉しいです。最後まで読んで頂きましてありがとうございました。
- 投稿日:2019-12-09T22:49:50+09:00
goqueryとは
はじめに
goでスクレイピングしたいと考え、google先生に質問すると皆こぞってgoqueryを使っています。しかし、goqueryを説明してくれている記事はあまり見当たらなかった為、今回まとめてみました。
ページを取得する
今回は以下の公式のgoのページにアクセスしてみます!
func main() { res, err := http.Get("https://golang.org/") if err != nil { log.Fatalln(err) } defer res.Body.Close() if res.StatusCode != 200 { log.Fatalf("status code error: %d %s\n", res.StatusCode, res.Status) } fmt.Println(res) }実行結果としては200で成功しています。
実行結果&{200 OK 200 HTTP/2.0 2 0 map[Alt-Svc:[quic=":443"; m a=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q0 49=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q 046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000] Co ntent-Type:[text/html; charset=utf-8] Date:[Mon, 09 D ec 2019 03:48:51 GMT] Strict-Transport-Security:[max- age=31536000; includeSubDomains; preload] Vary:[Accep t-Encoding] Via:[1.1 google]] 0xc00041a060 -1 [] fals e true map[] 0xc00010a000 0xc00031f970}ここまではgoの標準packageを使って行っているだけですが、ここからはgoqueryを用いて取得した情報から欲しい情報を取得していきます!
HTMLのドキュメント取得
goqueryを用いてHTMLのドキュメント取得する方法としては
NewDocumentFromReader
がありますfunc main() { res, err := http.Get("https://golang.org/") if err != nil { log.Fatalln(err) } defer res.Body.Close() if res.StatusCode != 200 { log.Fatalf("status code error: %d %s\n", res.StatusCode, res.Status) } doc, err := goquery.NewDocumentFromReader(res.Body) if err != nil { log.Println(err) } fmt.Println(doc) }こちらを実行した結果としては
&{0xc0004271d0 <nil> 0xc000136000}こちらは詳しいことはわかりませんが、ひとつ目の返り値である0xc0004271d0はclassや特定のセレクタを検索する時に使用するMethodのFindで使用するようです(間違ってたらすいません)
NewDocumentFromReaderは標準packageであるhtml.Parceを用いてhtmlのドキュメントを取得しているみたいです(ざっくりしすぎ)
また、毎回アクセスせずにページの情報をローカルに記述してそこからHTMLのドキュメントを取得する方法もあるみたいです
fileInfos, _ := ioutil.ReadFile("HTMLのファイルパス") stringReader := strings.NewReader(string(fileInfos)) doc, err := goquery.NewDocumentFromReader(stringReader) if err != nil { panic(err) }特定の要素に移動
find
Findを使うことで自分の知りたい情報に直接アクセスすることができます
今回は.Hero-headerの中身を取得してみます
findfunc main() { res, err := http.Get("https://golang.org/") if err != nil { log.Fatalln(err) } defer res.Body.Close() if res.StatusCode != 200 { log.Fatalf("status code error: %d %s\n", res.StatusCode, res.Status) } doc, err := goquery.NewDocumentFromReader(res.Body) if err != nil { log.Println(err) } section := doc.Find(".HomeContainer .Hero-header") text := section.Text() fmt.Println(text) }Find("セレクタを指定")すると条件にあった物を絞れます。またText()とすることで取得されたテキストコンテンツを結合して返します
結果Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.next
nextは兄弟要素を返します。今回は.HomeSection-headerの兄弟要素である.Playground-popoutを返しました
nextfunc main() { res, err := http.Get("https://golang.org/") if err != nil { log.Fatalln(err) } defer res.Body.Close() if res.StatusCode != 200 { log.Fatalf("status code error: %d %s\n", res.StatusCode, res.Status) } doc, err := goquery.NewDocumentFromReader(res.Body) if err != nil { log.Println(err) } section := doc.Find(".HomeSection-header") section = section.Next() text := section.Text() }結果Open in PlaygroundRead more >children
childrefunc main() { res, err := http.Get("https://golang.org/") if err != nil { log.Fatalln(err) } defer res.Body.Close() if res.StatusCode != 200 { log.Fatalf("status code error: %d %s\n", res.StatusCode, res.Status) } doc, err := goquery.NewDocumentFromReader(res.Body) if err != nil { log.Println(err) } section := doc.Find(".Playground-headerContainer") section = section.Children() text := section.Text() fmt.Println(text) }結果Try GoOpen in PlaygroundPrev&Siblings
これらはnextと同じで兄弟要素を取得しますが範囲が少しだけ違います。
したの図のように
* Nextは直後の兄弟要素
* Prevは直前の兄弟要素
* Sliblingsは全ての兄弟要素になりますFirst,Last,Eq,Slice
名前の通りです(雑すぎへん?)
First// 一部省略 section := doc.Find(".Footer-link") section = section.First() text := section.Text() fmt.Println(text) // 結果:CopyrightLast// 一部省略 section := doc.Find(".Footer-link") section = section.Last() text := section.Text() fmt.Println(text) // 結果:Report a website issueEq// 一部省略 //Eqは引数に取った数字の要素を返します section := doc.Find(".Footer-link") section = section.Eq(1) text := section.Text() fmt.Println(text) // 結果:Terms of ServiceEq(-1)// 一部省略 //Eqの引数が-1の時は最後の要素を返します section := doc.Find(".Footer-link") section = section.Eq(-1) text := section.Text() fmt.Println(text) // 結果:Report a website issueSlice// 一部省略 section := doc.Find(".Footer-link") section = section.Slice(0,2) text := section.Text() fmt.Println(text) // 結果:CopyrightTerms of Service特定の情報をゲット!!
自分のいきたい要素まで到達したらいざ情報を吸い取っていきます!
Html
欲張りセット。タグも含めて情報をゲットします!
返り値がふたつな事に注意です(ふたつ目はerror)
Htmlfunc main() { res, err := http.Get("https://golang.org/") if err != nil { log.Fatalln(err) } defer res.Body.Close() if res.StatusCode != 200 { log.Fatalf("status code error: %d %s\n", res.StatusCode, res.Status) } doc, err := goquery.NewDocumentFromReader(res.Body) if err != nil { log.Println(err) } section := doc.Find(".Hero-header") text, _ := section.Html() fmt.Println(text) }結果Go is an open source programming language that makes it easy to build <strong>simple</strong>, <strong>reliable</stro ng>, and <strong>efficient</strong> software.Text
上の方でも散々使ったので説明要りませんよね・・・
ただ単に文字だけ取得しますAttr
特定の属性値を抜き出します(やっとクローラーっぽい・・・)
返り値が二つある事に注意です(ふたつ目は属性値が存在するかのbool値)Attrfunc main() { res, err := http.Get("https://golang.org/") if err != nil { log.Fatalln(err) } defer res.Body.Close() if res.StatusCode != 200 { log.Fatalf("status code error: %d %s\n", res.StatusCode, res.Status) } doc, err := goquery.NewDocumentFromReader(res.Body) if err != nil { log.Println(err) } section, _ := doc.Find("iframe").Attr("src") fmt.Println(section) }結果https://www.youtube.com/embed/cQ7STILAS0M最後に
今回は簡単にgoqueryの説明について書きました。自分で調べながら、これを使えば簡単な画像クローラーくらいはすぐに実装できそうだと感じました。今までフロント側をやってきた身としてはこう言うツールを簡単に実装できるgolangは勉強していてとても楽しいです!
アドベントカレンダーはあと一日分あるので次回はgolangで何かを実装してみたいと思います!!
- 投稿日:2019-12-09T18:30:08+09:00
Go 並行処理
ゴルーチンやチャンネルといった並行処理についてまとめます
ゴルーチンとは
- Goにはゴルーチンという軽量スレッドの仕組みがある。(main()関数も1つのゴルーチンの中で実行されている)
- go構文を用いて、任意の関数を別ゴルーチンとして起動することで処理を並行にして走らせることができる
チャネル
- 複数のゴルーチン間でデータをやり取りしたい場合に用いる
- メッセージパッシング(情報をメッセージとして送受信する)によってデータを送受信することができる
- make()関数に型を指定して生成することで、その型のデータの書き込みと読み出しができる。
main.go// stringを扱うチャネルを生成 ch := make(chan string) // チャネルにstringを書き込む ch <- "a" // チャネルからstringを読み出す message := <- chゴルーチン内で取得したステータスコードをチャネルに書き込み、main()のゴルーチンで読み出すことでデータの受け渡しを実現する
main.gopackage main import "fmt" func main() { urls := []string { "https://example.com", "https://example.jp", "https://example.net", } statusChan := make(chan string) for _, url := range urls { go func(url string) { res, err := http.Get(url) if err != nil { log.Fatal(err) } defer res.Body.Close() statusChan <-res.Status }(url) } for i := 0; i < len(urls); i++ { fmt.Println(<-status) } }
- 投稿日:2019-12-09T17:12:56+09:00
"unnecessary guard around call to delete" という不思議なエラーが出た話
突然ですが以下のコードをご覧ください。
動物の種類とその鳴き声の組み合わせがmapになっていて、それを全部出力するコードです。sample.gopackage main import ( "fmt" ) func main() { sampleMap1 := map[string]string{ "犬": "ワンワン", "猫": "ニャーン"} sampleMap2 := map[string]string{ "犬": "わんわん", "猫": "にゃーん"} sampleMaps := []map[string]string{sampleMap1, sampleMap2} for _, sampleMap := range sampleMaps { fmt.Println(sampleMap) } }ここでもし、データの一部に出力したくない物が混じっていたとしましょう。
例えばsampleMap2が以下のようになっていたとします。sample.gosampleMap2 := map[string]string{ "犬": "わんわん", "猫": "にゃーん", "おじさん": "ぴえん"}この場合、mapのkeyに"おじさん"があったら、それをdeleteで取り除いてから出力するとします。
sample.gopackage main import ( "fmt" ) func main() { sampleMap1 := map[string]string{ "犬": "ワンワン", "猫": "ニャーン"} sampleMap2 := map[string]string{ "犬": "わんわん", "猫": "にゃーん", "おじさん": "ぴえん"} sampleMaps := []map[string]string{sampleMap1, sampleMap2} for _, sampleMap := range sampleMaps { if _, ok := sampleMap["おじさん"]; ok { delete(sampleMap, "おじさん") } fmt.Println(sampleMap) } }この段階では何のエラーも起きず、出力結果もちゃんと
"おじさん": "ぴえん"
が取り除かれたものになります。pushしたらstaticcheck Failedが出る
しかしこの変更をリモートリポジトリにpushしたところ、staticcheck Failedと言われてしまいました。
sample.go:21:3: unnecessary guard around call to delete (S1033)
とのこと。ローカルでは何のエラーも出なかったのに、どういうことだろう??
というかunnecessary guardってなんぞ?原因
僕が書いたコードでは「mapのkeyに"おじさん"があったら、それをdeleteで取り除いてから出力」という場合分けをしていましたが、どうやらそこら辺の場合分けはGoが自動でやってくれるらしいです。
つまり、
sample.goif _, ok := sampleMap["おじさん"]; ok { delete(sampleMap, "おじさん") }この部分は単に
delete(sampleMap, "おじさん")
で良かったとのこと。
イメージとしては、ぼく「このkeyを削除してほしいやで」
Gopher君「そんなこと言われても、指定されたkeyがないで……せや! この命令は無視したろ」的な処理なのでしょうか。細かいことは分かりません。
失礼します。
- 投稿日:2019-12-09T14:08:04+09:00
Go エラーハンドリング
エラー処理の基本
- Goにはtry〜catch〜finallyの例外処理は存在しない
http://golang.jp/go_faq#exceptions- エラーを処理するためにerrorインターフェースが用意されている
main.gotype error interface { Error() string }実際には、返されたエラーがnilかどうかで条件分岐し、nilではない場合error.Error()でエラー内容を出力しるような使い方をする
errro packageのよく使う4つの機能
1.func New(message string)erro
エラーメッセージを文字列で指定して単純にエラーを生成する時に使う
main.goerr := errors.New("エラー") fmt.Println("output:", err) // output:エラー2.func Errorft(format string, args ...interface{})error
フォーマット形式とエラーメッセージ文字列を指定してエラーを生成
main.goerr := errors.Errorf("output: %s", "エラー") fmt.Printkn("%+v", err) // output: エラー3.func Wrap(err error, message string)error
エラーをラップする時に使う
main.goerr := errors.New("repository err") err = errors.Wrap(err, "service err") err = errors.Wrap(err, "usecase err") fmt.Pringln(err) // usecase err: service err: repository err4.func Cause(err error)error
一番最初に起きたエラーの原因を特定する際に有効
main.goerr := errors.New("repository err") err = errors.Wrap(err, "service err") err = errors.Wrap(err, "usecase err") fmt.Pringln(errors.Cause(err)) // repository err参考
- 投稿日:2019-12-09T11:46:56+09:00
golangでk8sクラスターのDeploymentをscale
題名の通り、golangでk8sクラスターのDeploymentをscaleさせます。
まずはコードの依存関係です。package main import ( "fmt" "net/http" "io/ioutil" "crypto/x509" "crypto/tls" "encoding/json" "strings" )以下、メイン関数です。
(以下コード中のAPIサーバーのエンドポイントの変数apiserver
、証明書パスの変数crtpath
、トークンの変数token
についてはこちらを参照してください。リンク先で、それぞれAPISERVER
、CRTFILE
、TOKEN
変数で格納しています)func main () { apiserver := "https://192.168.99.100:8443" //以下はネームスペースdefaultのデプロイメントnginx-deploymentをスケールさせる例 path := "/apis/apps/v1/namespaces/default/deployments/nginx-deployment/scale" endpoint := apiserver+path //scaleはPATCHメソッド method := "PATCH" //PATCH対象データ。 data := `{"spec":{"replicas":3}}` //PATCH時のContent-Typeはapplication/strategic-merge-patch+json ctype := "application/strategic-merge-patch+json" //証明書のパス crtpath := "/Users/sota-n/.minikube/ca.crt" //SAトークン token := "xxx..." result := request(endpoint,method,data,ctype,crtpath,token) //リクエストに対するリターンとステータスコードを表示。 respCode,respBody := request(endpoint,method,data,ctype,crtpath,token) fmt.Println(respCode) fmt.Println(respBody) }以下がリクエストを発行する関数です。
エラーハンドリングは全て省略しています。//response bodyとステータスコードを返す関数。 func request (endpoint,method,data,ctype,crtpath,token string) (string,int) { //証明書読み取りと構成 caCert, _ := ioutil.ReadFile(crtpath) caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) //リクエスト構成 req, _ := http.NewRequest(method, endpoint, strings.NewReader(data)) req.Header.Set("Accept","application/json") req.Header.Set("Content-Type",ctype) req.Header.Set("Authorization","Bearer "+token) tr := &http.Transport{ TLSClientConfig: &tls.Config{RootCAs: caCertPool,}, } client := &http.Client{ Transport: tr, } //リクエスト発行 resp, _ := client.Do(req) defer resp.Body.Close() //レスポンス処理 body, _ := ioutil.ReadAll(resp.Body) //"body" -> []uint8 var val interface{} json.Unmarshal([]byte(body),&val) respBody,_ := json.MarshalIndent(val,""," ") return string(respBody),resp.StatusCode }以上のコードをビルドして実行するか
go run <上のコード>.go
を実行します。
- 投稿日:2019-12-09T05:37:59+09:00
hugoでその日の記事がビルドされない問題
hugoでその日の記事がビルドされない問題があります。その回避法です。
config.toml[frontmatter] date = [":filename", ":default"]ファイルは
content/post/2019-12-09-test.md
(その日の日付)とします。hugoでその日の記事がビルドされない問題
hugoは、通常、
.Date.Local
を使っていても、その日の記事がビルドされない問題があります。layout/_default/list.html{{ dateFormat "2006-01-02T15:04:05JST" .Date.Local }}そこで、dateをファイル名から取得する方法に切り替えると、回避できます。
- 投稿日:2019-12-09T05:37:59+09:00
hugoでその日の記事がビルドされない問題を回避する
hugoは、その日の記事がビルドされない問題があります。その回避法です。
config.toml[frontmatter] date = [":filename", ":default"]ファイルは
content/post/2019-12-09-test.md
(その日の日付)とします。hugoでその日の記事がビルドされない問題
これは、UTCがデフォルトになっているためだと思いますが、hugoでは、通常、
.Date.Local
を使っても、その日の記事がビルドされない問題があります。layout/_default/list.html{{ dateFormat "2006-01-02T15:04:05JST" .Date.Local }}そこで、dateをファイル名から取得する方法に切り替えると、回避できます。
- 投稿日:2019-12-09T01:46:28+09:00
リアルタイム画像リサイズAPIをGo + Serverless Application Modelで作った時の感想
ちょっと遅れましたすみません。
fushimiと申します。投稿させていただきます。Api Gateway + Lambda + Go + S3 で リアルタイム画像変換処理
Goで画像リサイズをやる機会があったので記述します。
今回は、事前に動画のエンコード済みパターンを作っておくのではなく、さくらのimagefluxのように、リアルタイムで動画のサイズを命令通りに変換しようっていう試みです。
数年前初めてこのアイディアを聞いたときは、「キャッシュに乗らないエンコードパターンいくらでもパラメータで作り放題だけど不正アクセスとか大丈夫かなあ...」と思ったものですが、各社なんだかんだやっていっている試みがあるようです。類似のものは結構見つかるかと思います。
サービスの画像が重くて困り始めた社内の人間から、「うちもLambdaとかでサクッと作れない...?」と聞かれたのもあり、試しにやってみました。
image-resizer-service
まず、aws-serverless-application-repositoryにimage-resizer-serviceというnodejs製のやつを見つけました。
1clickでLambda,AplGatewayが立ち、リアルタイムの画像変換サービスがサクッと使えたので、「これでいいじゃん」となりましたが、nodeのversionが2020年2月にランタイムサポートを切られる8系でした。
じゃ、forkして12系にするか~~と思ったところ、このアプリケーションはRuntime上のimagemagickに依存していることが判明。また、LambdaのnodejsのRuntimeには10-系からimagemagickが同梱されていないことも判明。まずはforkしたアプリケーションをいじり「imagemagickを同梱しようとしたり」 webpackから一部のモジュールを同梱したり除外したりしてサイズをチューニングしたり、公式の例にあるlibvipsを使ったモジュールを同梱しようとしたり...っていうのをゴニョゴニョやっていましたが、最近はこの手の作業に時間を使うのはなんか違うんじゃないかという気持ちもムクムクあったので、せっかくなのでLambdaのRuntimeの耐用年数高そうなGoで試しにこのアプリのクローンを組んでみることにしました。
image-resizer-service-go
package main import ( "bytes" "encoding/base64" "encoding/json" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" "github.com/disintegration/imaging" "github.com/labstack/gommon/log" _ "image/gif" _ "image/jpeg" _ "image/png" "net/http" "os" ) var BUCKET string func main() { BUCKET = os.Getenv(`IMAGE_BUCKET`) lambda.Start(serveFunc) } func serveFunc(request events.APIGatewayProxyRequest) (resp events.APIGatewayProxyResponse, err error) { log.Info(`start`) sess, err := session.NewSession() if err != nil { log.Error(err) return } s3Sdk := s3.New(sess) path := request.Path obj, err := s3Sdk.GetObject(&s3.GetObjectInput{ Bucket: &BUCKET, Key: &path, }) if err != nil { log.Error(err) return } original := new(bytes.Buffer) _, err = original.ReadFrom(obj.Body) if err != nil { log.Error(err) return } mimeAndDecodeType, err := getMimeAndDecodeType(original) if err != nil { log.Error(err) return } dst := []byte{} mimeType := mimeAndDecodeType.ContentType const LAMBDA_MAX_RESPONSE = 1024 * 1024 * 5 params := getParams(request.QueryStringParameters) if params.HasOptions() { dst, mimeType, err = imagProcess(original, params, *mimeAndDecodeType) if err != nil { return } } else if *obj.ContentLength > LAMBDA_MAX_RESPONSE { forceResize := 1920 dst, mimeType, err = imagProcess(original, Params{ Width: &forceResize, }, *mimeAndDecodeType) if err != nil { return } } else { dst = original.Bytes() } sEnc := base64.StdEncoding.EncodeToString(dst) resp = events.APIGatewayProxyResponse{ StatusCode: 200, Headers: map[string]string{ `Content-Type`: mimeType, }, //MultiValueHeaders: nil, Body: sEnc, IsBase64Encoded: true, } return resp, nil } func imagProcess(buf *bytes.Buffer, params Params, md MimeAndDecodeType) (encoded []byte, mimeType string, err error) { decodeOptions := []imaging.DecodeOption{} if params.AutoRotate { decodeOptions = append(decodeOptions, imaging.AutoOrientation(true)) } img, err := imaging.Decode(buf, decodeOptions...) if err != nil { log.Error(err) return nil, "", err } rctSrc := img.Bounds() w, h := func() (int, int) { if params.Width == nil && params.Height == nil { return rctSrc.Dx(), rctSrc.Dy() } if params.Width != nil && params.Height != nil { return *params.Width, *params.Height } if params.Width != nil { ratio := float64(rctSrc.Dy()) / float64(rctSrc.Dx()) return *params.Width, int(float64(*params.Width) * ratio) } ratio := float64(rctSrc.Dx()) / float64(rctSrc.Dy()) return int(float64(*params.Height) * ratio), *params.Height }() dst := new(bytes.Buffer) imgDst := imaging.Resize(img, w, h, imaging.Lanczos) err = imaging.Encode(dst, imgDst, md.Format) if err != nil { log.Error(err) return nil, "", err } return dst.Bytes(), mimeType, nil } type Params struct { Width *int `json:"width,string,omitempty"` Height *int `json:"height,string,omitempty"` AutoRotate bool `json:"auto_rotate,string,omitempty"` } func (self *Params) HasOptions() bool { if self.Width != nil { return true } if self.Height != nil { return true } return false } func getParams(m map[string]string) Params { p := Params{} if len(m) == 0 { return p } j, err := json.Marshal(m) if err != nil { log.Error(err) } err = json.Unmarshal(j, &p) if err != nil { log.Error(err) } return p } func getMimeAndDecodeType(b *bytes.Buffer) (*MimeAndDecodeType, error) { // todo: be lightweight bb := b.Bytes() contentType := http.DetectContentType(bb) f := func() imaging.Format { switch contentType { case `image/jpeg`: return imaging.JPEG case "image/gif": return imaging.GIF case "image/png": return imaging.PNG case "image/tiff": return imaging.TIFF default: return imaging.JPEG } }() return &MimeAndDecodeType{ ContentType: contentType, Format: f, }, nil } type MimeAndDecodeType struct { ContentType string Format imaging.Format }Goの場合、元のをサクッとコピーするだけならコード部分は実質200行程で済んでしまいました。かつビルドすると何も依存がいらないところがいいですね。
オリジナルの実装に加えて「EXIFの自動画像回転」オプションもつけておきました。このへんを雑にやらせてくれたimaging ライブラリに感謝。
変換の流れ
元のnodejsの実装そのままなのですが、
- (1) 事前に画像が格納されているS3 Bucketを指定しておき、
- (2)
https://xxxx/com/production/image.jpg?width=512&auto_rotate=true
パラメータつきでアクセスすると画像変換Lambdaが実行され、リアルタイムにS3から指定の画像をGetしてきて、指定した通りのサイズ表示に変換して返します。- (3) あとは結果をCDNでキャッシュするなどしておけばOK.
感想
実装
まあS3から落としてきて、パラメータ通りにリサイズして返すというだけだったんですが、pure goな物だけで画像を扱う処理がかなりできるということをあまり知らなかったので驚きです。こういうリアルタイム処理に限らず、「imagemagickをサクッと代替できるんじゃないか」という手応えも得られました。
画質
客観的な検証は今回行っていません。ユーザー向けサイトでは mozjpegやpngquantやwebpフォーマットを利用したゴリゴリのチューニングが必要なケースもあるかと思いますが、弊社サービスでのアセット構築など、これで十分なケースも多いかと思われます。
パフォーマンス
今回のgoバイナリはzip時で3MBあり、nodejs (+ runtime上のimagemagick)版(350kb)と比べてどうか不安だったのですが、結果的にはコールドスタート時も10msも違わないほどの差でした。
問題点
- 今回気になった事ととして、Lambdaで返せるサイズが6MBまでであるという学びを得ました。それを超えると、Lambdaがエラーを吐いてしまいます。 大抵のケースでweb向けに数MBの画像ファイルをわざわざ返すことはなかなかないんじゃないかという気もしますが、注意が必要そうです。
- 公式のnodejs版画像リサイズで紹介されていたsharpと同じくlibvips を使用したモジュールbimgもあるようなので、こちらを使うともっと画像変換が速いかもしれません。 (未検証です)
- 投稿日:2019-12-09T01:20:17+09:00
[Golang][競プロ]4つの数値をもつスライスを、大きい順に任意の変数に代入していく処理について
とりあえずコード
大きい順にそれぞれの変数に代入していく処理// スライスsに「4つ」の値が格納されているとする。 var 1st, 2nd, 3rd, 4th int for _, v := range s { if 1st <= v { 4th = 3rd 3rd = 2nd 2nd = 1st 1st = v } else if 2nd <= v { 4th = 3rd 3rd = 2nd 2nd = v } else if 3rd <= v { 4th = 3rd 3rd = v } else { 4th = v } }以下、今回この処理に行き着いた、競技プログラミングの問題です。
問題
1〜9までの数値が4つ与えられ、あなたはその4つの数値で、自由に2桁の整数を2つつくるとします。
様々な組み合わせがあるかと考えられますが、2つの2桁の整数を合計したときの、最も大きい値を調べて下さい。例
たとえば、
1 2 3 4
という数値を与えられた場合、「12+34」や「13+24」などさまざまな組み合わせができます。
しかし、合計が最も大きくなるのは、この場合だと「42+31」であり、その合計は73
です。
この場合の答えは73
として下さい。なお、この値は以下のような形で標準入力で渡されることとし、全て同じ値であることもありえるものとします。
与えられる数値の標準入力例11 2 3 4与えられる数値の標準入力例1に対しての期待する回答73↑この場合、42+31= 「73」 が正解になります。
与えられる数値の標準入力例29 2 7 5与えられる数値の標準入力例2に対しての期待する回答167↑この場合、92+75= 「167」 が正解になります。
与えられる数値の標準入力例29 9 9 9与えられる数値の標準入力例2に対しての期待する回答198↑この場合、99+99= 「198」 が正解になります。
回答と解説
それでは解説していきます。
コード
回答したコードpackage main import "fmt" func main(){ s := []int{0,0,0,0} fmt.Scanf("%d %d %d %d", &s[0], &s[1], &s[2], &s[3]) var 1st, 2nd, 3rd, 4th int for _, v := range s { if 1st <= v { 4th = 3rd 3rd = 2nd 2nd = 1st 1st = v } else if 2nd <= v { 4th = 3rd 3rd = 2nd 2nd = v } else if 3rd <= v { 4th = 3rd 3rd = v } else { 4th = v } } r := 1st * 10 + 3rd + 2nd * 10 + 4th fmt.Println(r) }解説
基本的な考え方
今回は、与えられた数値の組み合わせについてはあまり考えませんでした。
私が考えたポイントとしては…
- 与えられた数値を、それぞれ大きい順に認識する。
- 「最も大きい数値」と「その次に大きい数値」を十の位にする必要があるので、10倍する。
- 残りの小さい2つの数値は一の位ということで、そのまま。(1倍する)
- こうした上で、全部合計すれば求められている値が出る。
という感じです。
解説用にコメント書きまくったコードpackage main import "fmt" func main(){ s := []int{0,0,0,0} // 標準入力をスライスに格納する形で受け取りたいと考えたので、0が4つ入ったスライス「s」を準備します。 fmt.Scanf("%d %d %d %d", &s[0], &s[1], &s[2], &s[3]) // ここで標準入力を受け取っています。 var 1st, 2nd, 3rd, 4th int // 最も大きいものからこの変数に入る想定で、1位〜4位までの「席」を準備します。 for _, v := range s { // 受け取った標準入力をループで回します。 if 1st <= v { // もし渡ってきた数値が、暫定1位にいる値より大きければ… 4th = 3rd // 3位が4位に、2位が3位に、1位が2位にそれぞれ順位を落とします。 3rd = 2nd 2nd = 1st 1st = v // そして1位の席に、渡ってきた数値が座ります。 } else if 2nd <= v { // もし渡ってきた数値が、暫定1位よりは小さいけれど、暫定2位より大きければ… 4th = 3rd // 3位と4位が順位を落とします。 3rd = 2nd 2nd = v // そして2位の席に、渡ってきた数値が座ります。 } else if 3rd <= v {// もし渡ってきた数値が、暫定1位、2位よりは小さいけれど、暫定3位より大きければ… 4th = 3rd //3位が4位に順位を落とします。 3rd = v // そして3位の席には、渡ってきた数値が座ります。 } else { // 最後に、暫定1、2、3位より小さい場合… 4th = v // 渡ってきた数値は4位確定なので、4位の席に座ります。 } } r := 1st * 10 + 3rd + 2nd * 10 + 4th // 1位と2位は十の位になる必要があるので10倍、3位と4位は一の位で良いので1倍して、全て合計します。 fmt.Println(r) // 最後に結果を出力しておしまい! }さいごに
最後まで読んで頂き、ありがとうございます。
問題の解き方としての考え方は悪くない…と思うのですが、コードのロジックとしてはなんだか微妙な感じもします…
こうした方がもっと効率的だよ!とか、そういった意見があれば、ぜひお気軽にコメントして頂けると嬉しいです!ありがとうございました!
- 投稿日:2019-12-09T01:20:17+09:00
[Golang][競プロ]与えられた1~9の数値4つで任意に2桁の整数を2つ作り、それらの和が最大になる場合の、その和を求める問題について解説
問題
1〜9までの数値が4つ与えられ、あなたはその4つの数値で、自由に2桁の整数を2つつくるとします。
様々な組み合わせがあるかと考えられますが、2つの2桁の整数を合計したときの、最も大きい値を調べて下さい。例
たとえば、
1 2 3 4
という数値を与えられた場合、「12+34」や「13+24」などさまざまな組み合わせができます。
しかし、合計が最も大きくなるのは、この場合だと「42+31」であり、その合計は73
です。
この場合の答えは73
として下さい。なお、この値は以下のような形で標準入力で渡されることとし、全て同じ値であることもありえるものとします。
与えられる数値の標準入力例11 2 3 4与えられる数値の標準入力例1に対しての期待する回答73↑この場合、42+31= 「73」 が正解になります。
与えられる数値の標準入力例29 2 7 5与えられる数値の標準入力例2に対しての期待する回答167↑この場合、92+75= 「167」 が正解になります。
与えられる数値の標準入力例29 9 9 9与えられる数値の標準入力例2に対しての期待する回答198↑この場合、99+99= 「198」 が正解になります。
回答と解説
それでは解説していきます。
コード
回答したコードpackage main import "fmt" func main(){ s := []int{0,0,0,0} fmt.Scanf("%d %d %d %d", &s[0], &s[1], &s[2], &s[3]) var 1st, 2nd, 3rd, 4th int for _, v := range s { if 1st <= v { 4th = 3rd 3rd = 2nd 2nd = 1st 1st = v } else if 2nd <= v { 4th = 3rd 3rd = 2nd 2nd = v } else if 3rd <= v { 4th = 3rd 3rd = v } else { 4th = v } } r := 1st * 10 + 3rd + 2nd * 10 + 4th fmt.Println(r) }解説
基本的な考え方
今回は、与えられた数値の組み合わせについてはあまり考えませんでした。
私が考えたポイントとしては…
- 与えられた数値を、それぞれ大きい順に認識する。
- 「最も大きい数値」と「その次に大きい数値」を十の位にする必要があるので、10倍する。
- 残りの小さい2つの数値は一の位ということで、そのまま。(1倍する)
- こうした上で、全部合計すれば求められている値が出る。
という感じです。
解説用にコメント書きまくったコードpackage main import "fmt" func main(){ s := []int{0,0,0,0} // 標準入力をスライスに格納する形で受け取りたいと考えたので、0が4つ入ったスライス「s」を準備します。 fmt.Scanf("%d %d %d %d", &s[0], &s[1], &s[2], &s[3]) // ここで標準入力を受け取っています。 var 1st, 2nd, 3rd, 4th int // 最も大きいものからこの変数に入る想定で、1位〜4位までの「席」を準備します。 for _, v := range s { // 受け取った標準入力をループで回します。 if 1st <= v { // もし渡ってきた数値が、暫定1位にいる値より大きければ… 4th = 3rd // 3位が4位に、2位が3位に、1位が2位にそれぞれ順位を落とします。 3rd = 2nd 2nd = 1st 1st = v // そして1位の席に、渡ってきた数値が座ります。 } else if 2nd <= v { // もし渡ってきた数値が、暫定1位よりは小さいけれど、暫定2位より大きければ… 4th = 3rd // 3位と4位が順位を落とします。 3rd = 2nd 2nd = v // そして2位の席に、渡ってきた数値が座ります。 } else if 3rd <= v {// もし渡ってきた数値が、暫定1位、2位よりは小さいけれど、暫定3位より大きければ… 4th = 3rd //3位が4位に順位を落とします。 3rd = v // そして3位の席には、渡ってきた数値が座ります。 } else { // 最後に、暫定1、2、3位より小さい場合… 4th = v // 渡ってきた数値は4位確定なので、4位の席に座ります。 } } r := 1st * 10 + 3rd + 2nd * 10 + 4th // 1位と2位は十の位になる必要があるので10倍、3位と4位は一の位で良いので1倍して、全て合計します。 fmt.Println(r) // 最後に結果を出力しておしまい! }さいごに
最後まで読んで頂き、ありがとうございます。
問題の解き方としての考え方は悪くない…と思うのですが、コードのロジックとしてはなんだか微妙な感じもします…
こうした方がもっと効率的だよ!とか、そういった意見があれば、ぜひお気軽にコメントして頂けると嬉しいです!ありがとうございました!
- 投稿日:2019-12-09T01:20:17+09:00
[Golang][競プロ]与えられた1〜9までの数値4つを並び替えて、2桁の整数2つにしてそれらを足したとき、最も大きい値を求める問題について解説
問題
1〜9までの数値が4つ与えられ、あなたはその4つの数値で、自由に2桁の整数を2つつくるとします。
様々な組み合わせがあるかと考えられますが、2つの2桁の整数を合計したときの、最も大きい値を調べて下さい。例
たとえば、
1 2 3 4
という数値を与えられた場合、「12+34」や「13+24」などさまざまな組み合わせができます。
しかし、合計が最も大きくなるのは、この場合だと「42+31」であり、その合計は73
です。
この場合の答えは73
として下さい。なお、この値は以下のような形で標準入力で渡されることとし、全て同じ値であることもありえるものとします。
与えられる数値の標準入力例11 2 3 4与えられる数値の標準入力例1に対しての期待する回答73↑この場合、42+31= 「73」 が正解になります。
与えられる数値の標準入力例29 2 7 5与えられる数値の標準入力例2に対しての期待する回答167↑この場合、92+75= 「167」 が正解になります。
与えられる数値の標準入力例29 9 9 9与えられる数値の標準入力例2に対しての期待する回答198↑この場合、99+99= 「198」 が正解になります。
回答と解説
それでは解説していきます。
コード
回答したコードpackage main import "fmt" func main(){ s := []int{0,0,0,0} fmt.Scanf("%d %d %d %d", &s[0], &s[1], &s[2], &s[3]) var 1st, 2nd, 3rd, 4th int for _, v := range s { if 1st <= v { 4th = 3rd 3rd = 2nd 2nd = 1st 1st = v }else if 2nd <= v { 4th = 3rd 3rd = 2nd 2nd = v }else if 3rd <= v { 4th = 3rd 3rd = v }else if 4th <= v { 4th = v } } r := max1 * 10 + max3 + max2 * 10 + max4 fmt.Println(r) }解説
基本的な考え方
今回は、与えられた数値の組み合わせについてはあまり考えませんでした。
私が考えたポイントとしては…
- 与えられた数値を、それぞれ大きい順に認識する。
- 「最も大きい数値」と「その次に大きい数値」を十の位にする必要があるので、10倍する。
- 残りの小さい2つの数値は一の位ということで、そのまま。(1倍する)
- こうした上で、全部合計すれば求められている値が出る。
という感じです。
解説用にコメント書きまくったコードpackage main import "fmt" func main(){ s := []int{0,0,0,0} // 標準入力をスライスに格納する形で受け取りたいと考えたので、0が4つ入ったスライス「s」を準備します。 fmt.Scanf("%d %d %d %d", &s[0], &s[1], &s[2], &s[3]) // ここで標準入力を受け取っています。 var 1st, 2nd, 3rd, 4th int // 最も大きいものからこの変数に入る想定で、1位〜4位までの「席」を準備します。 for _, v := range s { // 受け取った標準入力をループで回します。 if 1st <= v { // もし渡ってきた数値が、暫定1位にいる値より大きければ… 4th = 3rd // 3位が4位に、2位が3位に、1位が2位にそれぞれ順位を落とします。 3rd = 2nd 2nd = 1st 1st = v // そして1位の席に、渡ってきた数値が座ります。 } else if 2nd <= v { // もし渡ってきた数値が、暫定1位よりは小さいけれど、暫定2位より大きければ… 4th = 3rd // 3位と4位が順位を落とします。 3rd = 2nd 2nd = v // そして2位の席に、渡ってきた数値が座ります。 } else if 3rd <= v {// もし渡ってきた数値が、暫定1位、2位よりは小さいけれど、暫定3位より大きければ… 4th = 3rd //3位が4位に順位を落とします。 3rd = v // そして3位の席には、渡ってきた数値が座ります。 } else { // 最後に、暫定1、2、3位より小さい場合… 4th = v // 渡ってきた数値は4位確定なので、4位の席に座ります。 } } r := 1st * 10 + 3rd + 2nd * 10 + 4th // 1位と2位は十の位になる必要があるので10倍、3位と4位は一の位で良いので1倍して、全て合計します。 fmt.Println(r) // 最後に結果を出力しておしまい! }さいごに
最後まで読んで頂き、ありがとうございます。
問題の解き方としての考え方は悪くない…と思うのですが、コードのロジックとしてはなんだか微妙な感じもします…
こうした方がもっと効率的だよ!とか、そういった意見があれば、ぜひお気軽にコメントして頂けると嬉しいです!ありがとうございました!