Kubernetesで完了済みのJobをまとめて削除する
Kubernetesの完了済みJobリソースをまとめて削除するには、field-selectorに"status.successful=1"を指定することで削除できます。
kubectl delete jobs --field-selector status.successful=1
field-selector に指定できるフィールドはリソース毎に定義されていて、Jobの場合は、metadataまたは、 status.successfulのみが指定できます。どこにもドキュメンテーションされていないようです。
指定できるフィールドは、kubernetesのコード内で見ることができます。 kubectl get で取得しても、status.successfulというフィールドは存在しないのでかなりわかりにくいですね。。
func addConversionFuncs(scheme *runtime.Scheme) error { return scheme.AddFieldLabelConversionFunc(SchemeGroupVersion.WithKind("Job"), func(label, value string) (string, string, error) { switch label { case "metadata.name", "metadata.namespace", "status.successful": return label, value, nil default: return "", "", fmt.Errorf("field label %q not supported for batchv1.Job", label) } }, ) }
https://github.com/kubernetes/kubernetes/blob/master/pkg/apis/batch/v1/conversion.go#L34
完了済みジョブを削除するときの条件が複雑な場合は、go-clientを利用したプログラミングが必要になりますが、大量に完了済みのジョブが残っているのが気になるくらいであれば、kubectl delete で解決してしまうのもよいんじゃないかなと思います。(まだAlphaですがTTLコントローラを利用する方法もあります )
1日1回CronJobを利用して完了済みのジョブを削除するだけであれば、下記のようにシンプルに済ませることができます。
Delete complete k8s jobs everyday · GitHub
備考
status.successfulはこの投稿で知りました
GolangでGKE用(StackdriverLogging)のログを出力する
GKE(Google Kubernetes Engine)でアプリケーションを開発する場合、ログは標準出力に出力し、GKEデフォルトでインストールされるfluentdでStackdriverLoggingに出力することが多いと思います。
その場合、特定のJSONフォーマットで出力することでStackdriverLogging上で扱いやすくなります。フォーマットの詳細はこちらです。 https://cloud.google.com/logging/docs/agent/configuration#special-fields
Golangの場合、デフォルトのLoggerでは
という問題があったので、どうすればよいのか調べてみました。
まず、ログ用のライブラリですが、logrusというライブラリを使うのがよさそうでした。 https://github.com/sirupsen/logrus
構造化したログを扱うことができ、フォーマットも指定することができます。
JSONのFormatterは標準で用意されているので下記のように利用することでJSON形式でログを出力することが可能です。init() でformatterを指定している箇所で環境変数を使った分岐を使えば、手元ではテキスト形式、サーバではJSONという分岐も可能です。
import ( "os" log "github.com/sirupsen/logrus" ) func init() { // Log as JSON instead of the default ASCII formatter. formater := log.JSONFormatter{ FieldMap: log.FieldMap{ log.FieldKeyLevel: "severity", }, } log.SetFormatter(&formater) // Output to stdout instead of the default stderr log.SetOutput(os.Stdout) // Only log the warning severity or above. log.SetLevel(log.InfoLevel) } func main() { log.Info('hello') // => {"msg": "hello", "level":info, "time":"2019-11-24T18:06:23+09:00"} }
StackdriverLoggingに連携するにあたって、levelは、"severity"というkeyで連携する必要があります。これは、JSONFormaterにFieldMapを指定することで実現できます。
func init() { // Log as JSON instead of the default ASCII formatter. formater := log.JSONFormatter{ FieldMap: log.FieldMap{ log.FieldKeyLevel: "severity", }, } log.SetFormatter(&formater) }
この解決策は、下記Issueで紹介されていました
また、logrusで用意されているレベルとStackdriverLoggingで定義されているレベルには差異があるので、カスタムレベルを定義する方法があれば、それを利用したいところですが、logrusには用意されていないようでした。info, warning,errorは利用できますが、それ以外を利用するとStackdriverLoggingでは正しく扱えないので注意が必要です。 それぞれがサポートしているレベルは下記です。
StackdriverLogging: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
logrus: https://github.com/sirupsen/logrus/blob/39a5ad12948d094ddd5d5a6a4a4281f453d77562/logrus.go#L27
Custom Log Levelについては、Issueで要望があがっていますが、いまのところ実装される見込みは薄そうです。
ということで手軽にJSON形式のログを出力する場合はlogrusがおすすめです。Golang標準のloggerインターフェースを実装しているので、後から差し替えるのも簡単だと思います。
gorilla/sessionでメモリリークが起きる原因を調べた
はじめに
開発中のGoで書かれたアプリケーションがメモリリークを起こしていて、gorilla/sessions ( https://github.com/gorilla/sessions ) というライブラリをバージョンアップしたら解決した。
メモリリークを解決したPRはこれ。
gorilla/contextが原因らしく、Contextが存在するよりも前にContext相当のことをしていたライブラリなので、併用するとよくないことが起こりそうだなーとは思うものの、なぜメモリリークするのかが理解できなかったので調べてみた。
調べた結果
Go 1.7で追加されたContextとgorilla/contextの競合によりメモリリークが発生するようになったのかと思い、少し混乱したのだが、
- メモリリークする
- gorilla/contextとGoのContextの競合
はそれぞれ独立した事象だった。が、後者のContextの競合は、メモリリークが発生する可能性をより高めてしまっている。 具体的には、 gorilla/contextと http.request.WithContextを併用すると、メモリリークに対する既知の対応を入れた場合でも、gorilla/contextで保存した内容を削除できずにメモリリークが起きる。
メモリリーク
gorilla/session を使う場合は、リクエストの最後にClearを行わないとcontextに保存した内容が削除されず、メモリリークが起きる。 これはgorilla/contextの仕様であり、GoのContextとは関係ない問題だった。 gorilla/muxを利用する場合はライブラリが面倒を見てくれるので利用者は気にしなくて良くて、gorila/session または gorilla/contextを単独で利用している場合に限り、利用者側が気をつける必要がある。
gorilla/contextとGoのContextの競合
gorilla/contextは、http.Requestのポインタをkeyにしたmapにデータを保存している。
var ( mutex sync.RWMutex data = make(map[*http.Request]map[interface{}]interface{}) datat = make(map[*http.Request]int64) )
https://github.com/gorilla/context/blob/master/context.go#L13..L17
一方、http.Request.WithContextは、requestをshallow copyした上でcontextをセットするので、引数として渡したrequestと戻り値のhttp.requestのポインタが変わってしまう。
WithContextの定義はこれ。
func (r *Request) WithContext(ctx context.Context) *Request { if ctx == nil { panic("nil context") } r2 := new(Request) *r2 = *r r2.ctx = ctx r2.URL = cloneURL(r.URL) return r2 }
go/request.go at 3409ce39bfd7584523b7a8c150a310cea92d879d · golang/go · GitHub
WithContextを利用すると、gorilla/contextで保存する先のmapが変わってしまう。 コードにすると以下のようなことが起こる。
package main import ( "context" "fmt" "net/http" gcontext "github.com/gorilla/context" ) func main() { r := new(http.Request) fmt.Printf("%v\n", &r) // => 0xc000010020 gcontext.Set(r, "key", "value") fmt.Printf("gorilla/context: %v\n", gcontext.Get(r, "key")) // => gorilla/context: value r = r.WithContext(context.Background()) fmt.Printf("%v\n", &r) // => 0xc00009c010 fmt.Printf("gorilla/context: %v\n", gcontext.Get(r, "key")) // => gorilla/context: <nil> }
上記の例では省略したが、context.Clear(r) を行ってもセットした値を消すことはできない
ライブラリ側の解決策
gorilla/sessionが、1.6 以前のサポートを切ったので、gorilla/contextを利用しなくなった。 上記の回避策が不要になった。 なので、最新のgorilla/sessionを使えばメモリリークは発生しない。
再掲だが、PRはこちら
gorilla/contextを直接利用すると変わらずメモリリークの可能性はあるので注意
備考
調べたときに利用したページを自分の備忘のため残しておく
そもそもContextとは
ちゃんと理解できてなかったので改めて調べた。こちらのページに日本語訳があって助かった www.ymotongpoo.com
なぜgorilla/muxを利用するとメモリリークしないのか
gorilla/mux ( https://github.com/gorilla/mux ) の mux.go の旧バージョンを参照すると、ServeHTTPメソッドに下記の記述があった。
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { ~~~~ if !r.KeepContext { defer contextClear(req) } handler.ServeHTTP(w, req) }
実行されているcontextClearの定義は gorila/context の context.Clear。muxではライブラリ側で、gorilla/contextのcontextClearが実行されていたことが確認できた
ちなみに、gorilla/sessions でメモリリークの回避策で紹介されていた ClearHandlerの定義はこちら
func ClearHandler(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer Clear(r) h.ServeHTTP(w, r) }) }
ほぼ同じ効果が期待できそう。
muxのGoのContextへの対応
前述の通り、メモリリークとGoのContextへの対応はまた別の問題。gorilla/muxでは、Goのバージョンによって挙動を変更していた。
修正のきっかけはこれっぽい
mux.Vars breaks with Go 1.7 · Issue #168 · gorilla/mux · GitHub
メモリリークが報告されている様子
https://groups.google.com/forum/#!msg/gorilla-web/clJfCzenuWY/N_Xj9-5Lk6wJ
GKEリージョンクラスタの制約
GKEにはリージョンクラスタという設定があり、この設定を追加すると、リージョン内の複数のゾーンにノードを複製し、可用性を高めることが出来ます
Regional clusters | Kubernetes Engine Documentation | Google Cloud
可用性を高める必要があり、インフラ費用に余裕があるならonにしておけばよいと思っていたのだけど、思わぬ制約がありました
前述のとおり、リージョンクラスタはリージョン内の複数のゾーンにノードを複製し、複製するノードはゾーン間で同じスペックである必要があります。(これはおそらくリージョンクラスタというかノードプールの制約)
そのため、ノードのマシンタイプは複製されるすべてのゾーンで利用できる必要があり、複製されるゾーンに利用できないマシンタイプが指定されているとエラーになります。
具体的には、東京リージョン(asia-northeast1)でリージョンクラスタを設定した場合、ノードのmachine-typeにc2マシンタイプを指定することはできなません。なぜなら、asia-northeast1-bではc2マシンタイプに必要なCascade Lakeがサポートされていないからです。 リージョンクラスタは後から取り消すことができないので、リージョンクラスタを作成したあとでマシンタイプをc2インスタンスにしたくなった場合、クラスタ再作成が必要になってしまいます。
これを知らずに既存のGKEクラスタにノードプールを追加しようとしたら、asia-northeast1-bにc2-standard-4というマシンタイプは存在しない、というエラーが返ってきてびっくりしたのでまとめておきました。
RailsアプリをGKEに移植するときにやったこと
GAEで稼働していたアプリをGKE用に移植したので、そのときやったことを自分用にまとめておく
まずベースコンテナを作る
まずはRailsアプリを動作するためのコンテナを作成します。ベースとなるコンテナにアプリケーションをコピーすればアプリが動作するようにするのが目的です。
Railsアプリが必要とするベースコンテナの要件は以下のようになると思います。
- Ruby
- native extensionを利用しているgemが依存している様々なライブラリ
- Node.js
ライブラリは、nokogiri、mysql2のようなgemのための対応です。コンテナサイズを最小にするには、利用しているgemが依存しているライブラリだけをインストールすればよいのですが、今回はメジャーなgemが依存しているライブラリは全部入りにすることにしました。
Node.jsは、Asset Pipeline、Webpackerを利用する場合必要になります。rubyracerのようなgemでも対応可能ですが、Node.jsをインストールしてしまう方がよいと判断しました。
上記の要件を満たすコンテナがDockerHubにあればよかったのですが、探してもなかったので作成しました。
https://github.com/k0uki/docker-ruby-node
カレントディレクトリにアプリケーションが配置されているとして、下記のようなDockerfileでRailsアプリをコンテナ上で動作させることができるようになります。
FROM k0uki/ruby-node:2.6.5-10-slim ENV RACK_ENV=production \ RAILS_ENV=production \ NODE_ENV=production \ APP_ENV=production \ RAILS_SERVE_STATIC_FILES=true \ RAILS_LOG_TO_STDOUT=true \ NOKOGIRI_USE_SYSTEM_LIBRARIES=1 COPY . /app/ WORKDIR /app RUN bundle install --without test development && yarn install && yarn cache clean RUN bundle exec rails assets:clobber assets:precompile CMD ["bundle", "exec", "rackup", "--port=8080", "-o", "0.0.0.0"]
ログを標準出力に出力する
ログは標準出力に出力するのが一般的です。Railsデフォルトではファイルに出力されるので、これを標準出力に変更します。 Rails5以降であれば、RAILS_LOG_TO_STDOUTという環境変数をtrueにすると標準出力に変更できます。 このとき、
STDOUT.sync = true
をinitializersかenvironmentsに追加しないと、出力がバッファリングされるので注意が必要です。
GKEにデプロイ
Dockernizeさえ出来てしまえば、GKEへのデプロイは何も特殊なことが必要ありません。
DBのmigrationのタイミングは、色々なやり方があると思いますが、僕はアプリの起動時にdb:migrateしてしまえばよいと思っています。今回移行したアプリはDBがなかったのでこの考慮は不要でした。
kubernetesにRailsをデプロイするときには、l.gcr.io/google/rubyは使わない方がいい
運用しているGKE上にコンテナ化されていないRailsアプリをデプロイすることになり、コンテナ化を進めていたときに気づいた話。
デプロイしたいアプリはRailsアプリはGAE上で動いていて、GAEで動作するときにどうもコンテナ化されているらしい。
そこでたどり着いたのが、このレポジトリ。
RubyとNode.jsがインストールされていて、Railsのための設定もすでに行われているのでそのまま使えそうです。 ruby-baseディレクトリのREADMEにもGKEで動作させるため記載がありました。
# Use the Ruby base image FROM l.gcr.io/google/ruby:latest # Copy application files and install the bundle COPY . /app/ RUN bundle install && rbenv rehash # Default container command invokes rackup to start the server. CMD ["bundle", "exec", "rackup", "--port=8080"]
https://github.com/GoogleCloudPlatform/ruby-docker/tree/master/ruby-ubuntu16
ところが、この通りDockerfileを記述してビルドすると、bundlerが存在しないエラーになります。
この事象は下記Issueで報告されていました。 github.com
Issueを読むと、こんな記述があります
Hmm. Looks like that readme is incorrect: I don't think gcr.io/google/ruby is the correct image. Generally, we should probably remove that section on GKE from the readme, as this docker image has generally evolved to be pretty app engine specific. If you want to roll your own docker image, I recommend using the canonical ruby images from DockerHub.
要約すると、gcr.io/google/rubyを使うこと自体が間違い。READMEから削除すべき。gcr.io/google/rubyは、GAE独自の仕様にあわせたコンテナになっている。ということっぽい。
google/rubyを仕様を調べれば対応できるとは思うけど、このコンテナ自体汎用的な用途を想定しているわけではなく、GAE用のランタイムとして設計されているようなので、使うべきではないと判断し、DockerHubにあるrubyのオフィシャルイメージ使って自前で構築することにした。
Pumaはあるサイズ以上のデータをPOSTされると一時ファイルを作成する
表題の通り。Pumaはあるサイズ以上のデータをPOSTされると一時ファイルを作成します。
puma/server.rb at 482ea5a24abaccf33c49dc9238a22e2a9affe288 · puma/puma · GitHub
# Use a Tempfile if there is a lot of data left if remain > MAX_BODY stream = Tempfile.new(Const::PUMA_TMP_BASE) stream.binmode else # The body[0,0] trick is to get an empty string in the same # encoding as body. stream = StringIO.new body[0,0] end
GKE上で書き込み禁止にして運用していたところ、ある条件下でファイル作成しようとしてエラーになったので調べてみたらこれが原因でした。