メモ-PrometheusにおけるMetricsのData Type
最近、EKS(Elastic Kubernetes Service)を使ってMSA(Micro Service Architecture)でシステムを運用する場面に出くわして、Observabilityを割と真剣に勉強している
このObservability(=可観測性)については、勉強し始めたらかなり面白くてまた別の機会にまとめようと思っている
今回は、Observabilityの3本柱(Log, Metrics, Trace)のうちの一つであるMetricsを支えるPrometheusについて、
メトリクスのデータタイプがいまいち腹落ちしなかったので、自分の言葉(公式サイトの翻訳)でメモを残しておこうと思う
Prometheusについて(超ざっくり)
RPSやレイテンシ、CPU使用率、Podの死活状態など、サービスとかサーバーの各種メトリクスを集めるためのツール
Kubernetesを使ってMSAでシステムを構築する際、Metricsを実現するためのツールとして現状は デファクトの地位にいる認識(同列でDatadog?)
実際は、Prometheusで取得したメトリクスを可視化するツールとしてGrafanaと一緒に使われることが多くて、Prometheus自体のメトリクス可視化ツールはあまり使われず、Prometheusはあくまで時系列データベースのような形で使われてるイメージ
ちなみに・・・
「現状は」と書いたのは、3, 4年くらい前から上で挙げたObservabilityを向上させるぜという話題は出ていたけど、去年とか今年に入ったあたりから、Observabilityを可能にするための取組みが主要Cloud Serviceで急加速してる印象で、Kubernetesもマネージドサービスで運用するのが主流になっているのでこの状況はいずれ崩れるんじゃないかなーと思ってる
事実、GCPが提供しているAnthosなんかは、デフォルトでCloud LoggingとCloud Monitoringをサポートして、Observabilityの実現を容易にしてくれてるし・・・
メトリクスのデータタイプ
とても話が逸れてしまいましたが、本題のPrometheusのメトリクスのデータタイプについて
Prometheusには用途に合わせて4つのデータタイプが用意されていて、これらから目的の形にデータを加工して可視化することができる
Counter
再起動などのタイミングに0にリセットされることはあっても、単調増加するという性質は変わらない
減少しうる値などには使用できない
Gauge
気温やメモリ使用率など、単一の上下する値のためのメトリクス
Histogram
観測値をサンプリングし、設定可能なバケット毎にカウントする
全観測値の合計も分かる
basenameという名前のHistogramを用意したとすると、
basename_backet{le="<upper inclusive bound>"} : 指定したバケットまでの、バケット毎の累積の数 basename_sum : 全観測値の合計 basename_count : 観測されたイベントの回数 (これは、basename_bucket{le="+Inf"}としているのと同様)
quantileを計算したい場合は、histogram_quantile()を使用することで計算可能
注意点としては、basename_bucketは累積的ヒストグラム(cumulative histogram=累積度数図)であるということ
-> 普段目にするヒストグラムとは異なる
-> Wikiがちゃんとまとめてくれてる
Bucketを採用していることで、Apdex scoreを計算することができる
-> Apdexを計算したい場合はHistogram1択
Summary
Histogram同様、観測値をサンプリングする
Histogram同様、全観測値の合計や観測回数を取得することもできる
Summaryの特徴的な点は、Sliding Time Window方式で、設定したquantileを計算できるということ
basenameという名前のSummaryを用意したとすると、
basename{quantile="φ"} : φ-quantileの観測イベント basename_sum : 全観測値の合計 basename_count : 観測されたイベントの回数
HistogramとSummary
共通点
両方とも累積的である点で、本質的にはCounterと同じ
ただし、負数を扱うこともできる点でCounterとは異なる
HistogramやSummaryが使われる典型的な場面は、Latencyやレスポンスのサイズのメトリクスを取得するためだが、これらは負数にはならないので一旦負数のことは考えない
負数を取らない限りにおいて、prometheusのrate関数を使用することができるようになる相違点
Histogramは、histogram_quantileを使ってサーバー側で計算する
Summaryは、クライアント側で計算した結果をサーバーに送る
使い分け
Histogram
良い点
Histogramは、promQLを使って柔軟にデータを取得できる
クライアント(実際にサービスが動いているアプリケーションサーバーやウェブサーバー)で処理をする必要がない良くない点
いくつのBucketを、いくつ間隔で用意するか、正しく設定できないと有効でない
5つをBucketを0.01秒ごとに用意すると0.05秒以上は全てまとめてカウントされるし、逆に5つのBucketを1秒ごとに用意すると、1秒以下は全てまとめてカウントされる
このバランスをどこで取れるかを、事前にある程度分かっていないとうまくメトリクスを取れない
Summary
良い点
クライアント側で計算できるので、サーバーの負荷はかからない良くない点
Summaryは事前にquantileがわかっていないと、後から変更する場合コストが高い
特にオートスケールなど、複数のインスタンスで同じサービスが動いている場合は、個々のクライアントで計算してprometheusサーバーに送り集約する形だと意味がない、もしくはほとんど意味がわからない
(「個々のインスタンス」というよりは「同じサービス全体として」のメトリクスを取りたい)
結論
SummaryでできるがHistogramでできないということは基本的にないし、メリデメを比較しても「使い分け」というよりは、特別必要がない限り基本的にHistogramを使う方が良いと思う
量子ビット
反省
前回のポスト
量子コンピューティングのためのクソみたいなメモ - 外に出るねくら
であまりにお粗末なことをしてしまったと反省しました。
そしてお粗末なことをしたと思っている割にアクセスがあり、しかもfacebookでのシェアが勘違いじゃなければ8もあり、、、
なので、可及的速やかに次のポストをせねばと思いながらこのポストを書きました。
はい、今回のテーマは
『量子ビット』
割と序盤からぶっ飛ばしていきます。
コードは0です。
ちなみに、今回数式を扱う関係でmarkdownではなく、はてな記法で書いたため、今までと見え方が異なっています。
読みにくかったらすみません。
そうでなくても読みにくい? ですね。すみません。
まず前提
量子力学の世界では、僕たちが普段使っているコンピュータは「古典コンピュータ」と呼ぶらしいです。
また、ビットも同様に「古典ビット」と呼ばれます。
ここでもこれに従います。
さて内容
古典コンピュータは0か1の状態で情報が表現されている。
これだけ考えてればよかった。
これが古典ビット。
量子ビットは重ね合わせがあるのでそうはいかない。
量子ビットはまず、複素ベクトルを用いて
とすることで、量子の状態を表します。
、はどの程度0と1が重ね合わさっているかを表現しており、「複素確率振幅」なんて呼ばれてます。
なぜ複素数?という疑問に対しては、
ビシッと「そうした方がのちのち計算が簡単だから」と答えます。
すみません、正直わからないです。
自分の理解ですが、
量子力学では、粒子の運動状態を波動関数で表します。
(波動関数は読んで字の如く波の動きについての関数)
波なので、素直に計算しようとすると懐かしのやらやらの三角関数が出てくるし、そしてこれが2乗とか微分とかやろうとすると計算しにくいです。
そこで、指数関数に変換して計算を行うんですが、このときに使うオイラーの公式でどうしても虚数が出てきます。
だったら最初から虚数が出てくることを前提に、複素数で表した方が計算は楽だろうという理解です。
さて、話を戻します。
と表現すると、古典ビットの0に対応し、
と表現すると、古典ビットの1に対応しています。
ところで毎回毎回上のような列ベクトルを書くのはめんどくさいです。
少なくとも過去の偉人たちはそう考えたみたいです。
なので、量子力学の世界では、量子状態を表すためにブラ・ケット表記というものが使われます。
ブラ記法: で記載し、行ベクトルを表しています
ケット記法: で記載し、列ベクトルを表しています
少なくともこのポストでブラは使わないです。ケットのみ使います。
これを使うと先ほどの複素ベクトルを
量子ビットの状態
と簡潔に書くことができるようになります。
急にと書きましたが、量子力学における「適当な変数」です。
xみたいなものです。
注意すべきなのは、上の式の、は0と1がどの程度重なり合っているかを示しているだけであり、出現する確率を直接表しているわけではありません。
「重なり合っていると言っても程度があって、量子ビットは0が割、1が割で重なり合っているんだぜ」
ということを言っているだけで、
「量子ビットは、%の確率で0となり、%の確率で1となる」
と言っているわけではないということです。
さぁクライマックスです
量子力学では、「測定」という操作を行うことで、ある量子ビットが0なのか1なのかを観測します。
「測定によって、量子状態が測定結果に収束する」という表現がされます。
なんだか、「あなたが0っていうから0になったのよ」って言われてるみたいですが、まぁだいたいそういうことらしいです。
測定を行うまでは0と1が重なり合った状態であり、測定をしたときに初めて0か1かが確率的に決定します。
上で言ったように、やは重なり合っている程度を表していますが、
ボルンの法則というものがあり、それによると、ここでいうやの絶対値を2乗するとそれぞれの値が測定結果として現れる確率になります。
空間のどこに粒子が存在するかについての確率なので、足せば当然1となります。
(あとは「波動関数の規格化」なんて話があったりしてゴチャゴチャして...)
結果、
となります。
散々やらやら書いた上に、どの程度重なり合っているか...なんて言ってましたが、
0と1は同じ程度重なり合っているとされ、同じ確率で観測されるので、
量子の状態は
と表すことができます。
ですが、だけでは一般式としては不十分で、
最初の方に言った通り、量子力学では粒子の運動状態を波動関数で表すので、位相因子というものを考慮する必要があり、
これを考慮すると、最終的に量子ビットの状態の一般的な表し方としては
となります。
特に、の部分は量子力学全般で重要な役割を果たします。
今日はこの辺でまとめます.
量子コンピューティングのためのクソみたいなメモ
超久しぶりの投稿。
テーマは量子コンピューティング。
がっつり内容に踏み込むのではなく、
表面だけサラッと、しかもそれをまとめることで驚異的な殴り書きを実現した。
そもそも量子コンピュータとは
従来のコンピュータが、0または1の値しか取らない「ビット」(bit)を扱うのに対して、
量子コンピュータは、0、1または両方を取りうる「量子ビット」(qubit)を扱う。
量子力学をベースに設計され、作られたコンピュータ。
従来のコンピュータではできないような膨大な量の計算を行うことができるとされている。
Keyward
Superposition
Entanglement
ゲート方式
- 論理ゲート:Logic Circuit
- 量子ゲート:Quantum Circuit
アニーリング方式
Superposition
直訳すると「重ね合わせ」
0、1、または両方を取りうる状態のこと。
qubitがこの性質を持っていることを利用することで、量子コンピューティングが可能となる
Entanglement
直訳すると「もつれ」
なんと説明したらいいのかがあまりわからない。
これも表現が正しいかどうかわからないが、
各量子が強い相互関係にある状態のことをいう。
ある量子xの結果が決まると、たちまち他の量子の結果まで決まってしまう状態。
量子計算を行うためにはこの「もつれ」が欠かせない。
N qubitの量子コンピュータの単位計算量は2のN乗なんて言ったりするが、
それは「重ね合わせ」の性質をもつqubitが「もつれ」ているから。(よくわからん)
ゲート方式
論理ゲート:Logic Circuit
従来のコンピュータがこっち
真理値の「真」「偽」、二進法の「0」「1」、電圧の強弱などなど、
これらを使って論理演算を実現する。
フリップフロップ回路っていうとなんとなくあんな感じかな??っていうのは思い浮かぶかも。
これまでは単に「ゲート」というと、こっちの論理演算を行う回路のことを指していた。
語弊を恐れないのであれば、ANDとかORとかXORといった、論理演算子に対応する回路と言ってもいいかもしれない。
量子ゲート:Quantum Circuit
従来の論理演算ではなく、量子演算を行う回路。
ここにはANDとかORとかXORとか出てこない。
論理演算でいうところのNOTであるCNOTゲート、0と1両方を取りうる状態(Superposition)を実現するアタマールゲート、
2つの量子ビットを入れ替えるswapゲートなどを使って計算を行う。
量子アニーリング方式
国産の方式
扱える変数が2000個ほどで従来のコンピュータよりも少ないため、
用途が非常に限定的で、しかも超低温状態にしないと動作しない。
一方それゆえシンプルで、組み合わせを最適化する問題に適していると言われている。
こっちを勉強するつもりはあまりないのでこれ以上はwikiで。
https://ja.wikipedia.org/wiki/%E9%87%8F%E5%AD%90%E7%84%BC%E3%81%8D%E3%81%AA%E3%81%BE%E3%81%97%E6%B3%95
以上、驚異的な殴り書きでした。
golangでpprof使ってみたら使え過ぎな件
あまりにも使え過ぎたので使い方を忘れないためにアウトプットする。
pprofとは
GOのアプリケーションやバイナリをプロファイリングするために使う。
runtimeで動かしてプロファイリング結果をダウンロードし確認するタイプのものと、
HTTP経由でプロファイリング結果を参照し確認するタイプのものがある。
runtimeで動かす方は こちら を見てください。
ここではやりません。
HTTP経由で確認する方をやります。
導入する
ISUCON9の内容でやります。
本当はちゃんとベンチマークを実行するべきですが、(面倒なのと、)導入の仕方がわかればいいのと、見方がわかればいいので、ローカルで動かしたものに対してプロファイリングします。
実行環境
$ go version go version go1.13 darwin/amd64 $ sw_vers ProductName: Mac OS X ProductVersion: 10.14.6
導入
導入自体は一瞬で終わる。
まずはgraphvizをインストールする。
$ brew install graphviz
main関数に以下の記述を追加。
import _ "net/http/pprof" // ===== 中略 ===== func main() { go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() // ===== 以下略 =====
実行する
プロファイリング対象のアプリを起動する。
起動したら以下を実行
go tool pprof -http=":8081" ~/dev/isucon9/webapp/go http://localhost:6060/debug/pprof/profile
このコマンドを実行すると、30秒くらいプロファイリングした後、自動的にブラウザが立ち上がり、結果が表示される。
この30秒の間にベンチマーク実行したり、画面ぽちぽちしたりする。
結果
TOP
ブラウザで開くと最初は以下のような画面になっている。
関数ごとにプロファイリング結果が表示されている。
それぞれの項目の見方は以下。
- Flat:関数の処理時間
- Flat%:各Flatの全体に対する割合
- Sum%:スタック履歴からの累計Flat%
- Cum:待ち時間も含めた処理時間
- Cum%:各Cumの全体に対する割合
- Name:関数名
Graph
こんな感じのチャートが表示される。
こっちを見るより次に紹介するflame graphの方が見やすいのでそっちを見る。
Flame Graph
こんな感じのチャートが表示される。
- 各箱は関数を表している
- 各箱の幅は、CPU上での合計の処理時間
- 大きい幅の箱は、小さい幅の箱に比べて、一回あたりの処理時間が長いか、複数回呼ばれる処理であるということを意味する
- ちなみに、上の箱が下の箱を呼び出す形になっており、これがそのままスタックトレース
箱にカーソルを合わせると、関数名と処理時間が表示される。
この関数そのものに時間がかかっているか、その関数が呼び出している関数の処理に時間がかかっているのかすぐにわかる。
ある箱Aの処理時間が10ms、その下の箱Bの処理時間が1msの場合 Aの関数はBの関数の結果をせいぜい1ms待つだけ。 つまりAの処理そのものに9msかかっていることになるので、ここにボトルネックがあるのかも?ということになる
さらに、箱をクリックすると、その箱にフォーカスした表示に切り替わる。
まじですごくないですかこれ、、、
Source
極め付きがこれ。
Sourceを開き、検索窓に関数名を入れて、 REFINE
でSHOWをクリックすると、以下のような表示になる。
ある関数の中で、具体的にどこに時間がかかっているのかがわかる。
これ、本当に使える。。。
ぜひ使ってみてください。
以上でした。
Goで実行時間を7msから4msまで減らした件
TL;DRってなんかかっこいいから使ってみたかった。
けどこんな記事を見かけて、僕は顰蹙買いたくなかったのでこれからも使わないことにする。
一度使い出したらクセになりそうだし。
さて、そんなことはさておき
Goで実行時間を7msから4msまで減らした件
ってどういうことか説明します。
毎年半分記念でGoogle Code Jamに出てるんですが、毎年qualifier stageで脱落してて半分記念のクセに超悔しいので、そろそろ本気モードで勉強しようと思いまして。
golangを使っている理由は単純に使ってみたかったから。ホントはpythonの方がいいんだろう。でもホントのホントはCとかC++の方がいいんだろう。
それでも使いたい欲求を抑えることはできなかった。
golang使いたいがために勉強しそうなんてのも期待した。(そして実際そうなってる)
で、ある問題を、初めにめちゃくちゃ愚直に解いて、そのあとリファクタリングをしてどこまで早くなるのかを試してみたくなった。
その問題がこちら。(問題文掲載していいのかわからなかったのでやめとく)
atcoder.jp
2つの数字(N, Y)を与えられて、 a+b+c = N
かつ 10000 × a + 5000 × b + 1000 × c = Y
になる a, b, c
の組合せを探す感じ。
2秒以内に、256Mのメモリで。
ちなみに組合せがない場合は -1 -1 -1
って出力してね。
解法はめちゃくちゃ簡単。
総当り
残念ながら僕にはそれしか方法思いつかなかった。
でも当たり方にもいろいろあるんですよということで。
めちゃくちゃ愚直な解法
文字通り 総当り
package main import ( "fmt" "strconv" "bufio" "os" ) var sc = bufio.NewScanner(os.Stdin) func nextInt() int { sc.Scan() i, e := strconv.Atoi(sc.Text()) if e != nil { panic(e) } return i } func main() { sc.Split(bufio.ScanWords) n := nextInt() // N の読み込み y := nextInt() // Y の読み込み print(logic(n, y)) } func logic(n, y int) string { maxA := y/10000 // Yを10000で割ってaの最大値を算出。aを見つけるためのループの終端 maxB := y/5000 // Yを5000で割ってbの最大値を算出。bを見つけるためのループの終端 maxC := y/1000 // Yを1000で割ってcの最大値を算出。cを見つけるためのループの終端 for a := 0; a < maxA+1; a++ { for b := 0; b < maxB+1; b++ { for c := 0; c < maxC+1; c++ { // ルーーーーーープ if a+b+c == n && 10000*a+5000*b+1000*c == y{ // a+b+c = N かつ 10000 × a + 5000 × b + 1000 × c = Y だったらその組合せを返す return strconv.Itoa(a)+" "+strconv.Itoa(b)+" "+strconv.Itoa(c) } } } } return "-1 -1 -1" // 組合せがなければこれを返す } func print(ans string) { fmt.Println(ans) }
凄まじい・・・
もちろん2秒オーバーで実行時間制限超過。
ここからリファクタリングが始まる・・・
とりあえずテスト通す
修正内容一つ一つ書いてたら記事が長くなるので、テストが通るところまで一気にワープする。
package main import ( "fmt" "strconv" "bufio" "os" ) var sc = bufio.NewScanner(os.Stdin) func nextInt() int { sc.Scan() i, e := strconv.Atoi(sc.Text()) if e != nil { panic(e) } return i } func main() { sc.Split(bufio.ScanWords) n := nextInt() y := nextInt() print(logic(n, y)) } func logic(n, y int) string { maxA := y/10000 if y%10000 == 0 && maxA == n { // Y が10000で割りきれて、かつ商がnならそれ答えじゃん return strconv.Itoa(maxA)+" 0 0" } maxB := y/5000 if y%5000 == 0 && maxB == n { // Y が5000で割りきれて、かつ商がnならそれ答えじゃん return "0 "+strconv.Itoa(maxB)+" 0" } maxC := y/1000 if y%1000 == 0 && maxC == n { // Y が1000で割りきれて、かつ商がnならそれ答えじゃん return "0 0 "+strconv.Itoa(maxC) } for a := 0; a < maxA+1; a++ { if a > n { break // aがNより大きくなったらそれ以降確認する意味無いよね } for b := 0; b < maxB+1; b++ { if a+b > n { break // a+bがNより大きくなったらそれ以降確認する意味無いよね } // c = N - a - bじゃん if 10000*a+5000*b+1000*(n-a-b) > y { break // 10000*a+5000*b+1000*c がYより大きくなったらそれ以降確認する意味無いよね } if 10000*a+5000*b+1000*(n-a-b) == y{ return strconv.Itoa(a)+" "+strconv.Itoa(b)+" "+strconv.Itoa(n-a-b) } } } return "-1 -1 -1" } func print(ans string) { fmt.Println(ans) }
これでテストが通る。
実行時間7ms。
実行時間6msへ
package main import ( "fmt" "strconv" "bufio" "os" ) var sc = bufio.NewScanner(os.Stdin) func nextInt() int { sc.Scan() i, e := strconv.Atoi(sc.Text()) if e != nil { panic(e) } return i } func main() { sc.Split(bufio.ScanWords) n := nextInt() y := nextInt() logic(n, y) } // string返すのをやめた func logic(n, y int) { maxA := y/10000 if y%10000 == 0 && maxA == n { fmt.Printf("%d 0 0", maxA) return } if maxA > n { // for文の中で判定してたものを外出し maxA = n } maxB := y/5000 if y%5000 == 0 && maxB == n { fmt.Printf("0 %d 0", maxB) return } maxC := y/1000 if y%1000 == 0 && maxC == n { fmt.Printf("0 0 %d", maxC) return } for a := 0; a < maxA+1; a++ { for b := 0; b < n-a; b++ { // bもn-aにすることで n以上かどうかを気にしなくてよくなる c := n - a - b // c を最初に計算しておく if 10000*a+5000*b+1000*c > y { break } if 10000*a+5000*b+1000*c == y{ fmt.Printf("%d %d %d", a, b, c) return } } } fmt.Print("-1 -1 -1") }
for文の中でいちいち判定してたのは外出し。
条件に一致すればfor文の終端をnにすればいいだけ。
n以上かどうかを判定するif文をほとんど抹殺できたのがよかった。
実行時間4ms
package main import ( "fmt" "strconv" "bufio" "os" ) var sc = bufio.NewScanner(os.Stdin) func nextInt() int { sc.Scan() i, _ := strconv.Atoi(sc.Text()) // よくよく考えたらエラーの処理とか絶対いらない return i } func main() { sc.Split(bufio.ScanWords) n := nextInt() y := nextInt() logic(n, y) } func logic(n, y int) { maxA := y/10000 if y%10000 == 0 && maxA == n { fmt.Printf("%d 0 0", maxA) return } if maxA > n { maxA = n } maxB := y/5000 if y%5000 == 0 && maxB == n { fmt.Printf("0 %d 0", maxB) return } maxC := y/1000 if y%1000 == 0 && maxC == n { fmt.Printf("0 0 %d", maxC) return } for a := 0; a < maxA+1; a++ { for b := 0; b < n-a; b++ { c := n - a - b // 10000*a+5000*b+1000*c > y の判定を抹殺 // この判定させるくらいなら追加でfor文回ることを許容する方がいいのでは // という考えが頭をよぎった if 10000*a+5000*b+1000*c == y { fmt.Printf("%d %d %d", a, b, c) return } } } fmt.Print("-1 -1 -1") }
これで4ms!!
10000*a+5000*b+1000*c > y
を消したのは正直賭け。
考えた限りありえなかったけど、本当にそうなのかは全く自信ない。
ここまでやって得た教訓
- ifは減らしまくる
- 新しく変数を宣言するのをできるだけ避ける(最低限の変数でどうにかできないかを考える)
- 調査対象を絞りに絞る
長々と書きましたが以上
Base64とmulti-part/form-dataのファイルアップロードを実践した
背景
ファイルのアップロード方法を考えたので、いくつかのパターンを実践してみた。
超おさらい
こちらのポストで考えていた内容。
ファイルをS3にアップロードするときはサーバー?クライアント? - 外に出るねくら
サーバー側でアップロードを実装する場合の方針は3つ。
- 画像アップロード用のエンドポイントを作り、multi-part/form-dataを使う。
- Base64 Encodeを使う。
- 全てmulti-part/form-dataを使う。
クライアント側でアップロードを実装する場合の方針は1つ。
リソース、時間との相談でサーバー側の2もしくは3がいいだろうというのが結論。
今回は前回いいだろうという結論だった
Base64 Encodeを使う 全てmulti-part/form-data
のパターンで実装してみます。
以下、環境情報。
$ cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=16.04 DISTRIB_CODENAME=xenial DISTRIB_DESCRIPTION="Ubuntu 16.04.4 LTS" $ ruby --version ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux] $ rails --version Rails 5.2.3
さぁ、いってみよう
今回のソースコードはこちらに置いています。
サーバー側
https://github.com/kennyTakimura/file-uploader
クライアント側
https://github.com/kennyTakimura/file-uploader-client
今回はユースケースとして、
とある管理画面で、アイテムの名前と個数、そして画像を登録する
の想定で実装します。
ただし、ログイン機能等は実装しない。
エンドポイントが叩ける状態、アップロードできることを確認できる状態にすることがゴール。
サーバー側で実装する場合
共通部分
ActiveStorageの準備
どの選択肢にしても、S3へのアップロードの処理にはActiveStorageを使います。
Railsネイティブの機能で、画像に限らずファイルのアップロード機能を提供します。
たぶんGoogleで「Rails ファイル アップロード」とかって調べるとCarrierWaveというgem(プラグインみたいなもの)を使った方法が結構出てくると思いますが、従前から処理が重くなりがちという課題があります。
ActiveStorageが追加されたのがRails 5.2からで、これが大体1年前なのでここから先ActiveStorageを使った方法が主流になるんじゃないかなと思ってます。
実際、英語縛りで「Rails file upload」で調べると、ActiveStorageを使った方法が結構出ててきます。
英語ほどの量はないですが、もちろん日本語でも出てきます。
ActiveStorageを使うにはこちらのガイドを参考にしてます。
bundle exec rails active_storage:install
を実行- active_storage_attachmentsとactive_storage_blobsのマイグレーションファイルができていることを確認
bundle exec rails db:migrate
を実行- テーブルが作成されていることを確認
config/storage.yml
を確認し、以下のようになっていることを確認
ただし、今回はS3にアップロードするので、testとlocalはなくてもいい Rails.application.credentials.digって何だって人はこちらに記事書いてるので見てください。
Rails 5.2のCredential管理方法 - 外に出るねくら
test: service: Disk root: <%= Rails.root.join("tmp/storage") %> local: service: Disk root: <%= Rails.root.join("storage") %> # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) amazon: service: S3 access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> region: your_region bucket: your_own_bucket
6.config/environments/production.rb
に以下の記述を追記
config.active_storage.service = :amazon
7.aws-sdk-s3
のgemをGemfileに追加し、bundle instal --path vendor/bundle
を実行
model作り
アイテムのmodelを作る。
1. bundle exec rails g model item
を実行
2. itemのマイグレーションファイルができていることを確認
3. 以下の記述にする
名前、個数をカラムに用意する。
画像はActiveStorage側でよしなに保存されるのでここにカラムを持つ必要は無い。
class CreateItems < ActiveRecord::Migration[5.2] def change create_table :items do |t| t.string :name t.integer :count t.timestamps end end end
bundle exec rails db:migrate
を実行- テーブルが作成されていること、
app/model
内にitem.rb
ができていることを確認 item.rb
に以下を追記
これを追記することでActiveStorageで追加したテーブルと、itemとのリレーションを張って、item.attach(image)
みたいな感じでアイテムに画像をアタッチすることができる
has_many_attached :image
これで事前準備完了。
保存処理に着手できる。
corsの設定
クライアントとサーバーとで分けてJSONで通信するような仕組みを採用する場合、大体オリジンが異なるのでcorsの設定をしておかないとクライアントからのリクエストをサーバー側で受け付けられなくなる。
クライアントはVueを使いますが、Vueを使うとデフォルトがlocalhost:8080
のドメインなのに対して、Railsはlocalhost:3000
と、オリジンが異なるので、サーバー側でcorsの設定をしないとクライアントからのリクエストがサーバー側で受け付けられないということになります。
Gemfileにgem "rack-cors"
を追記
bundle install --path vendor/bundle
を実行
config/initializer/cors.rb
のコメントアウトを解除し、オリジンを編集。
Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins 'localhost:8080' resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end
Base64 Encodeを使う。
この選択肢を採用する場合は、クライアント側でBase64にエンコードする処理、サーバー側でBase64をデコードする処理を実装する必要がある。
ダウンロードするなら逆に、クライアント側でBase64をデコードし、サーバー側でBase64にエンコードする処理が必要ですが、今回は割愛。
サーバー側
modelの修正
作成したitem.rbにデコード処理を追加する。
1. 拡張子を調べる(png or jpeg)
2. data:image/${拡張子};base64より後の部分を抜き出す
3. 抜き出した部分をデコード
4. デコードすると文字列なので、一旦ファイルに書き込んで一時保存
5. 保存したデータをActiveStorageでitemにアタッチ
6. アタッチしたら一時保存したファイルを削除
class Item < ApplicationRecord has_many_attached :image def attached_image=(image) if image.present? || regex_image(image) == '' extension = make_extension(image) encoded_data = image.sub(/data:(image|application)\/(jpeg|png);base64,/, '') decoded_data = Base64.decode64(encoded_data) filename = Time.zone.now.to_s + '.' + extension File.open("#{Rails.root}/tmp/#{filename}", 'wb') do |f| f.write(decoded_data) end attach_image(filename) end end private def make_extension(image) content_type = regex_image(image) content_type[/\b(?!.*\/).*/] end def regex_image(image) image[/(image\/[a-z]{3,4})|(application\/[a-z]{3,4})/] end def attach_image(filename) image.detach if image.attached? image.attach( io: File.open("#{Rails.root}/tmp/#{filename}"), filename: filename) FileUtils.rm("#{Rails.root}/tmp/#{filename}") end end
controllerに追記
- インスタンス(item)を作成
- アイテムの保存
アイテムの保存に成功した場合
3. itemの保存に成功したら場合は、画像のアップロード処理
4. 保存したアイテムの情報を返す
アイテムの保存に失敗した場合
3. unprocessable_entity(422)を返す
class ItemsController < ApplicationController def create_via_base64 item = Item.new(name: item_params[:name]) if item.save item.attached_image = item_params[:image] render json: { data: item, status: :created } else render json: { data: '', status: :unprocessable_entity } end end private def item_base64_params params.require(:item).permit(:name, :count, :image) end end
ルーティングに追記
config/routes.rb
に以下を追記する。
post '/item/base64', to: 'items#create_via_base64'
以上でサーバー側の実装は完了。
やっぱりBase64をデコードする部分が一番面倒くさい。
本来はこの処理はitem.rb
ではなくて、別クラスに切り出した方がいいけど今回はそこまでしない。
クライアント側
続いてクライアント。
Vueを使う。
すでにプロジェクトは作っている前提で進めます。
componentを分けるとかいうことは一切考えず、全パターン1ファイルに書いていきます。
とにかくアップロード処理を作ることが優先。
vue-base64-file-uploadを入れる
外部のコンポーネント。
これのおかげでめちゃくちゃ簡単に機能を実装できる。
こちらがREADME
npm i -g vue-base64-file-upload
を実行。
まずはJS部分から書いていきます。
import axios from 'axios' import VueBase64FileUpload from 'vue-base64-file-upload' export default { name: 'app', components: { ~~~ 省略 ~~~ VueBase64FileUpload, }, data: function () { return { ~~~ 省略 ~~~ name_base64: '', count_base64: 0, image_base64: '' } }, methods: { ~~~ 省略 ~~~ async postItem() { const res = await axios.post( "http://localhost:3000/item/base64", { item: { name: '', count: 0, image: this.image_base64 } } ) this.clear() }, onLoad(dataUri) { this.image_base64 = dataUri }, clear() { this.name_base64 = '' this.count_base64 = 0 this.image_base64 = '' } } }
次にtemplate部分。 名前、個数、画像をアップロードするので、フォームを用意する。
<template> <div id="app"> <h2>アイテム投稿</h2> ~~~ 省略 ~~~ <h3>Base64</h3> <form> <p> <label>Name </label> <input name="name-base64" type="text" v-model="name_base64"/> </p> <p> <label>Count </label> <input name="count-base64" type="number" v-model="count_base64"> </p> <p> <vue-base64-file-upload accept="image/png, image/jpeg" @load="onLoad"></vue-base64-file-upload> </p> <button v-on:click.prevent="postItem()">Submit</button> </form> ~~~ 省略 ~~~ </div> </template>
これで完了。
すごいスッキリ書ける。
これでBase64を使った実装は終わり。
全てmulti-part/form-dataを使う。
王道での実装。
サーバー側
controllerに追記
処理の概要としてはBase64 Encodeの場合と変わらない。
- インスタンス(item)を作成
- アイテムの保存
アイテムの保存に成功した場合
3. itemの保存に成功したら場合は、画像のアップロード処理
4. 保存したアイテムの情報を返す
アイテムの保存に失敗した場合
3. unprocessable_entity(422)を返す
class ItemsController < ApplicationController def create_via_multipart_formdata item = Item.new(name: item_multipart_params[:name], count: item_multipart_params[:count]) if item.save item.image.attach(item_multipart_params[:image]) render json: { data: item, status: :created } else render json: { data: '', status: :unprocessable_entity } end end ~~~ 省略 ~~~ private ~~~ 省略 ~~~ def item_multipart_params params.permit(:name, :count, :image) end end
Base64 Encodeのときは画像の情報が文字列だったので、Decodeの処理が必要だったけど、
今回はその処理が必要ないので直接ActiveStorageのattachメソッドを使うことができる。
つまりcontroller以外に編集する必要がない。
サーバー側の修正はこれで終わり。
ルーティングに追記
config/routes.rb
に以下を追記する。
post '/item/multi_part', to: 'items#create_via_multipart_formdata'
クライアント側
まずはJS部分から書いていく。
multi-part/form-dataを使う場合は外部のコンポーネントを入れる必要ない。
import axios from 'axios' import VueBase64FileUpload from 'vue-base64-file-upload' export default { ~~~ 省略 ~~~ data: function () { return { ~~~ 省略 ~~~ name_multi_part: '', count_multi_part: 0, image_multi_part: '', } }, methods: { async postItemViaMultiPart() { let formData = new FormData() formData.append("name", this.name_multi_part) formData.append("count", this.count_multi_part.toString()) formData.append("image", this.image_multi_part) const res = await axios.post( "http://localhost:3000/item/multi_part", formData, { headers: { 'Content-Type': 'multipart/form-data' } } ) if (res.status === 200) { this.clear() alert("Item is created") } else { alert("Failed to register item") } }, loadMultiPart() { this.image_multi_part = this.$refs.file.files[0] }, ~~~ 省略 ~~~ } }
FormDataを作って、データを入れて、axiosで送信する。
簡単に実装できる。
次にtemplate部分。
<template> <div id="app"> <h2>アイテム投稿</h2> ~~~ 省略 ~~~ <h3>全部multi-part/form-data</h3> <form> <p> <label>Name </label> <input name="name-multi-part" type="text" v-model="name_multi_part"/> </p> <p> <label>Count </label> <input name="count-multi-part" type="number" v-model="count_multi_part"/> </p> <p> <label>Image </label> <input name="image-multi-part" type="file" ref="file" @change.prevent="loadMultiPart"/> </p> <button v-on:click.prevent="postItemViaMultiPart">Submit</button> </form> </div> </template>
なんら難しいことはないですね。
まとめ
なんか最初から分かってたことではあるけど、multi-part/form-dataで実装する方がはるかに楽ですね。
基本的にmulti-part/form-dataを使う方法でいいんじゃないかと思った。
APIを外部に公開するときはBase64 Encodeを使って、 そうではなく、自システム内に閉じているなら、multi-part/form-data。
前回の投稿では思考実験的な感じで、時間があればBase64 Encode、なければmulti-part/form-dataと結論づけました。
今回はそれぞれのパターンで実践してみました。
画像に関係するところはmulti-part/form-dataで、そうでないところはJSONで、とすることでコードの見通しが悪くなったりするのかなと思ってたけど、案外そうでもない。
実装量は大した差はないけど、multi-part/form-dataの方が当然少ない。
時間があろうとなかろうと、普通にmulti-part/form-dataを使う方がいい気がしました。
AWS CLIに複数のAWSアカウントを登録する
前に書いた備忘録。
AWS CLIのインストール
awsコマンドを使えるようにするために必要なツール
https://docs.aws.amazon.com/ja_jp/streams/latest/dev/kinesis-tutorial-cli-installation.html
デフォルトの設定
デフォルトで行う場合は以下の手順で実行可能だが、一つのアカウントを紐づけることしかできない
(~/.aws)$ aws configure AWS Access Key ID [None]: xxxxxxx AWS Secret Access Key [None]: ABCDEFGHIJKLMN Default region name [None]: REGION Default output format [None]: json
AWS CLIに複数のアカウントを登録する
--profile
オプションを指定することで、複数のアカウントを登録することができるようになる
(~/.aws)$ aws configure --profile ${any_name} AWS Access Key ID [None]: ${Your Default Access Key ID} AWS Secret Access Key [None]: ${Your Default Secret Access Key} Default region name [None]: ${Your Default Region} Default output format [None]: ${Output Type [json, text, table]}
~/.aws/config [default] output = json region = REGION [profile ${any_name}] output = ${Output Type [json, text, table]} region = ${Your Default Region}
~/.aws/credentials [default] aws_access_key_id = xxxxxxx aws_secret_access_key = ABCDEFGHIJKLMN [${any_name}] aws_access_key_id = ${Your Default Access Key ID} aws_secret_access_key = ${Your Default Secret Access Key}
終わり