Scala + Play Framework の環境に分散トレーシングを導入する(したい)
Scala + Play Framework の環境で分散トレーシングを導入したくなったので色々調べたり実験しました。(結論から言うと、まだうまくいっていません…。)
分散トレーシングって?なんで使いたいの?
今回、僕が導入したいシステム(サーバサイドアプリケーション)はマイクロサービスアーキテクチャではないのですが、APIのレスポンスタイム向上のために分散トレーシングシステムを入れたくなりました。 もちろん本当は「分散」トレーシングシステムである必要はなく、単純に グラフィカルに 処理のボトルネックを見たいだけです。
例えば、並行に処理している場合、ボトルネックを分析するのがログ等のテキストだと難しいことがあります。
以下のような処理があったとします。
それぞれの処理のログがあったとして、すぐにボトルネックを見つけることはできるでしょうか?
A: 100ms B: 50ms C: 120ms D: 10ms E: 45ms
答えですが、ボトルネックは AとE です。次のように図にすると明らかです。
これが現実のAPIだった場合、AとE以外の処理の高速化をいくら試みても、全くの見当違いでAPIレスポンスタイムは向上しないことになります。
それでもまだ今回はまず図があったため数字さえ書き込めばボトルネック分析は容易ですが、現実のアプリケーションの場合、そもそも図に正確に起こすところが大変になる場合もあるでしょう。
分散トレーシングでは、分散トレーシングのための仕組みをアプリケーションに取り込むことで、処理の流れを上図のようにグラフィカルに見ることができます。
分散トレーシングを実現するミドルウェア
たくさんありますが、今回は以下のアプリケーションを試してみることにしました。
Jaeger
- Uber Technologies が作っているやつ。
- バックエンドのデータストアとして Cassandra, Elasticsearch が利用可能。
- OpenTracing という規格に対応
Elastic APM
- Elastic が作っているやつ
- 当然バックエンドは Elasticsearch
- OpenTracing という規格に対応
どのように処理を記録していくのか?
https://www.jaegertracing.io/docs/1.16/architecture/ から引用
処理の単位を Span
と呼び、 Span
は親を持つことができます。
Span
の塊を Trace
と呼びます。
「分散」トレーシングにおいては別のマイクロサービスに対しコンテキストを渡す…などの概念がありますが、今回はモノリシックアプリケーション内でのトレーシングを目標としているので、割愛します。
また、使うミドルウェアによって多少言葉は異なりますが、大まかには同じです。
次回以降、実装例を紹介します。
MacでGUIを使って簡単にSANs対応のオレオレ証明書(マルチドメイン証明書)を発行する
以下の記事の続き。 mashi.hatenablog.com
CSRを使って証明書を作成する場合
本物の証明書を作る手順同様、CSRを作成してからオレオレ認証局で証明書を作成するパターン。CSRを作成する方法は世の中に溢れてるので適当にググってほしい。
1) キーチェーンを開きメニューからアシスタントを開く
CSRをドラッグドロップする画面が出てくるのでCSRを入れる。
2) 発行認証局
この要求に対するデフォルトを無効化にチェックを入れる
3) 証明書情報
4) 【重要】 証明書情報(2)
- 【重要】名前(通称) は Common Name (CN) に相当する。配信したいFQDNを入力する
5) 鍵用途拡張領域 【スキップ可能】
そのまま
6) 拡張鍵用途拡張領域
ごくまれに、ここの項目が異なっているとSSL証明書として使えない(選択できない)ロードバランサ等が存在するため注意。
7) 基本成約拡張領域 【スキップ可能】
- 基本制約拡張領域を含める にチェックを入れる
8) 【重要】サブジェクト代替名拡張領域
- dNSName に配信したいFQDNを半角スペース区切りですべて登録する
- Common Nameも含めてすべて指定する
- 近代のブラウザの中には Common Name を無視しこの SANs 領域しかみないものもあるらしい
9) 作成完了!
SANs がそれっぽくなっていることを確認
10) 証明書のエクスポート
発行した証明書を右クリックし、エクスポートする。
秘密鍵と証明書を同時に作成する場合
1) キーチェーンを開きメニューからアシスタントを開く
2) 証明書を作成
- 固有名のタイプ を リーフに
- 証明書のタイプ を SSLサーバに
- 「デフォルトを無効化」 にチェックを入れる
3) 証明書情報(1)
好きに設定
4) 【重要】 証明書情報(2)
- 【重要】名前(通称) は Common Name (CN) に相当する。配信したいFQDNを入力する
5) 発行者を選択
先ほど発行したルート証明書を選択する
(中略)
このへんは上のCSRを元にするやつと同じ
11) 証明書の場所を指定【スキップ可能】
12) 作成完了!
SANs がそれっぽくなっていることを確認
13) エクスポート
証明書の出力
秘密鍵の出力
パスワード保護されたPKCS12形式でしか出力できない模様。PEM形式等にしたい場合は自分でopensslコマンド等を使って変換する必要がある。
MacでGUIを使ってオレオレルート認証局を作る
GUIを使ってオレオレルート認証局を作り、楽に証明書を発行したい!
オレオレ認証局を作ること自体は簡単だが、SANs証明書(マルチドメイン証明書)を発行するのは実はかなりめんどくさい!めったに作らないので手順も覚えられない!
実は Mac には標準に組み込まれているキーチェーンでGUIベースでオレオレ認証局を開設し SANs証明書を払い出す仕組みがあるので、今回はそれを利用して開設する。 Windows を使っている人はわからないのでご自身で調べてください><
- オレオレ認証局を開設するメリット
- キーチェーンを使うメリット
- ルート認証局を開設する手順
- 1) 認証局作成ウィザードを起動する
- 2) 認証局を作成
- 3) 証明書情報(1)
- 4) 証明書情報(2)
- 5) このCAの鍵ペア情報【スキップ可能】
- 6) このCAのユーザーが使う鍵ペア情報を指定【スキップ可能】
- 7) このCAが使う鍵用途拡張領域【スキップ可能】
- 8) このCAのユーザーが使う鍵用途拡張領域【スキップ可能】
- 9) このCAが使う拡張鍵用途拡張領域【スキップ可能】
- 10) このCAのユーザーが使う拡張鍵用途拡張領域【スキップ可能】
- 11) このCAが使う基本成約拡張領域【スキップ可能】
- 12) このCAのユーザーが使う基本成約拡張領域【スキップ可能】
- 13) このCAが使うサブジェクト代替名拡張領域【スキップ可能】
- 14) ユーザーが使うサブジェクト代替名拡張領域【スキップ可能】
- 15) 保存場所の指定【スキップ可能】
- 16) 完成!
- オレオレルート証明書の信頼設定
- 続き
オレオレ認証局を開設するメリット
常時SSL化が当たり前となった今、ローカル環境や開発環境等でもSSL化しテストすることが多くなってきたと思う。
たとえ商用の環境でなかったとしても、やっぱりこういうエラーは出てほしくないし無視したくない。
例えばよくある事故として、「開発時は」証明書の信頼チェックをオフにしオレオレ証明書を使えるようにしているつもりが、実際にエンドユーザーにリリースしたアプリケーションでもチェックをスキップしたままになっていた、などがある。
このような事故を防ぐために&ブラウザ等で毎回証明書を無視する画面を見なくてすむよう、「真面目に」オレオレ証明書を発行しようと思う。 (あらゆる環境で正規の証明書を発行できる人は正規のものを使うにこしたことはないのでそうしてほしい)
目指したい姿はこんな感じ。オレオレルート認証局を作り、使いたい環境でルート証明書を信頼するようにしておく。 一度オレオレルート認証局を作ってしまえば、あとは 本番環境等と同じ手順で証明書を発行できるようになる。
本番環境
オレオレ環境
キーチェーンを使うメリット
最初に書いたとおり、Macを使っているなら、オレオレルート認証局をキーチェーンで開設することができる。そのメリットは以下の通り。
ルート認証局を開設する手順
手順がかなり長いように見えるが、SSL大好きマン以外は【スキップ可能】のところは無視してそのまま進んでもらっていい(多分普段の開発で困ることはないと思う)。
1) 認証局作成ウィザードを起動する
キーチェーンを起動し、メニューから起動できる
2) 認証局を作成
- 固有名のタイプ: 自己署名ルート証明書
- ユーザー証明書: SSLサーバ
- ここで何が変わるかは分からない…
- 「デフォルトを無効化」にチェックを入れる
- ここにチェックを入れるとカスタマイズが可能になる
- メールの送信元は適当
3) 証明書情報(1)
- 有効期間: 好きに設定する
- 長くないと不便だと思う人が多いと思うのでここでは10年にする
- インビテーションに署名: 外す
- 割愛するが今回の用途では不要なので外す (どっちでもいい)
4) 証明書情報(2)
好きに入力してほしい
5) このCAの鍵ペア情報【スキップ可能】
好きなものを選ぶ。2019年現在、世の中の主要ルート証明書は RSA 2048bit。こだわりがないなら変えないほうがいい。
6) このCAのユーザーが使う鍵ペア情報を指定【スキップ可能】
これも特に変えなくていい
7) このCAが使う鍵用途拡張領域【スキップ可能】
なんとなく本物のルート証明書っぽくしたいので以下のようにしてみた。詳しい方、間違ってたら教えてください。 多分オレオレ証明書として使う分にはここの設定をどう変えても普通に動くと思う。
- この拡張領域は重要 にチェックを入れる
- 証明書署名、CRL署名 にチェックを入れる
多分ここの部分に相当すると思うんだけど…
8) このCAのユーザーが使う鍵用途拡張領域【スキップ可能】
- この拡張領域は重要 にチェックを入れる
- 署名、鍵の暗号化 にチェックを入れる
9) このCAが使う拡張鍵用途拡張領域【スキップ可能】
そのまま進む
10) このCAのユーザーが使う拡張鍵用途拡張領域【スキップ可能】
- この拡張領域は重要 のチェックを外す - SSLクライアント認証、SSLサーバ認証 にチェックを入れる
11) このCAが使う基本成約拡張領域【スキップ可能】
そのまま進む
12) このCAのユーザーが使う基本成約拡張領域【スキップ可能】
- 基本成約拡張領域を含める にチェックを入れる
13) このCAが使うサブジェクト代替名拡張領域【スキップ可能】
そのまま
14) ユーザーが使うサブジェクト代替名拡張領域【スキップ可能】
15) 保存場所の指定【スキップ可能】
16) 完成!
オレオレルート証明書の信頼設定
発行した証明書を選び、上記のように「常に信頼する」になっていれば使える状態のはずだが、なぜか発行したてのルート証明書は信頼されないことがある? その場合は、一回「信頼しない」にし画面を閉じたあともう一度開いて「常に信頼する」に設定しておく。
Mac以外の環境で信頼するようにしたい場合は、ルート証明書を書き出し、その証明書を信頼するようそれぞれのOS/デバイスで設定すればよい。
続き
この手順で作った僕の認証局の証明書を貼っておく。みんな僕のことすごく信頼していると思うので、みんなのOSで信頼するように設定しておいてね*1! (^^)
-----BEGIN CERTIFICATE----- MIIDWTCCAkGgAwIBAgIBATANBgkqhkiG9w0BAQsFADBOMRkwFwYDVQQDDBBtYXNo aSBSb290IENBIFIyMQswCQYDVQQGEwJKUDEkMCIGCSqGSIb3DQEJARYVd2VibWFz dGVyQG1hc2hpanAubmV0MB4XDTIwMTEwMTAxMzA0N1oXDTMwMTAzMDAxMzA0N1ow TjEZMBcGA1UEAwwQbWFzaGkgUm9vdCBDQSBSMjELMAkGA1UEBhMCSlAxJDAiBgkq hkiG9w0BCQEWFXdlYm1hc3RlckBtYXNoaWpwLm5ldDCCASIwDQYJKoZIhvcNAQEB BQADggEPADCCAQoCggEBANJ6LVh2asGdpJsNKwub634xu5lXqMUcCCIh6urzkXTd zFazh3S39QfMUZvUpAblGQPnXM9uLqlDRjj2Pqnj4tCiigNP28XjCbXAN3uM8vPa nufjjZaGSUtbQGLtVJgdCdmD0WWBjEQLDboIqTQbwS/287yiaSfSiscqzqBAvGy1 r+ZkYCKb4F9M/ZgMDvBDBl5MSTENx7BQsoHyniaD0yBWUl7uiZ23wv2KQGp6Hq3T 7+NmdsRX3AMwXPysfLL+IBPV0NBK6biKWTeVH6JYaBBe13svDhZDDgcqm7OHsPpi OXbRtfElC8yutks63s7zyIzW/Ypp24E4ATN2T1dhiAUCAwEAAaNCMEAwDwYDVR0T AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAqQwHQYDVR0lBBYwFAYIKwYBBQUHAwIG CCsGAQUFBwMBMA0GCSqGSIb3DQEBCwUAA4IBAQAKCDZedLoFoC2+P1ILyf57KQle PakiQibwYE680ye+XD0mseOHAp2xAvonPNkLXi639m/5rxxflOP9nA8P6DiuPOQq xgeCdYK0NSPVqpW2NSYu2NrOCHWRGKc6eKwXkPjIcklAf2QvRTJwb48BPpOCwZGo aXR0CrSTo/N6J53AA6JlnjK0hZWxir8m49hMRXvuI/jkCf3uartQO8tDQQAaXSzr py3zGZzGt+Tw+upv29LwPBEvJmXsxyolRQkEScazYZPDXgpwA7mXeP1fX6Kr8hIR yQ0CHDvdOOXxn1OPTWEgVphQyo3Vidv1dwwT0jbfAAvbEQajF06PUraSSeua -----END CERTIFICATE-----
ScalaのFutureについてのスライド書きました/つまらないシステム
Scala の Future ってどうやって使うの?Promiseって何?
"Scala Future" で検索して出てくるFutureの解説は、Scala公式サイトのドキュメントを除いて大体こんな感じで紹介されてることが多い。
import scala.concurrent._ import ExecutionContext.Implicits.global Future { Thread.sleep(1000) println("hoge") } println("fuga") //-> // fuga // hoge
こういうFuture.applyにThread.sleepやIOのblockingをする例って非常に悪いと思っている。まるで、Futureでsleepするのが普通のコードっぽく見えるじゃん。違うの、単に説明の時に楽だからsleepしてるだけなの。説明コードが短くてすむの。ちょっと使ってるだけ。プロダクトコードでsleepするのやめろ。うわあ!!Future内でブロッキングやめろ!!そこみんなのトイレだから!無駄に占有しないで!
かといって、公式ドキュメント読めっていってもあのドキュメント長いし退屈。詳細に書いてあるのはわかるんだけど、初学者がちょっとFuture使ってみたいわ〜ってときに「ん?長いな別のサイト見るか」ってなってもしょうがない。
じゃあお前がかけよって言われるわけだが、なんか上手に書けない。前々から書こう書こうと思ってるんだけど。。箇条書きレベルでメモを残すので誰か日本語にしてください。
Scala の Future と Promise って何?
Promise なんて興味ない?いや、Futureの本体はPromiseといっても過言じゃないですよ。
Future[A]... いつか型Aの値が与えられる型
Promise[A]... いつか型Aの値を与える型
Future[A] いつか型Aの値が与えられる型
Future にはいつか値が与えられる。値が与えられたときどういう動作をしてほしいのかは、foreach(等)で定義できる。
import scala.concurrent._ import ExecutionContext.Implicits.global val a: Future[String] = getFuture() // a にはいつかStringの値が与えられる。 // a に値が与えられたら、printlnは実行される。 a.foreach(e => println("値やっときたわ: " + e)) println("piyopiyo") // -> // piyopiyo // 値やっときたわ: ???
Promise[A] いつか型Aの値を与える型
import scala.concurrent._ val b: Promise[String] = Promise[String] // b にはいつかStringの値を与える b.success("値あげるよ〜") b.success("2回目実行するよ〜") // -> java.lang.IllegalStateException: Promise already completed.
これ何の役にたつの?
Promise[A] から Future[A] を作ることができる
Promise#futureを呼ぶと、Futureを作ることができる。
Promiseのsuccessを呼ぶと、その瞬間にFutureのforeach(等)が呼び出される。
import scala.concurrent._ import ExecutionContext.Implicits.global val promise: Promise[String] = Promise[String] val future: Future[String] = promise.future future.foreach(e => println("値やっときたわ: " + e)) println("piyopiyo") promise.success("こんにちは") // -> // piyopiyo // 値やっときたわ: こんにちは
じゃあ Future.apply って何
Futureの作り方ってFuture.applyじゃないんですか?!そう聞きました!こういう例で習いました!
val future: Future[A] = Future { Thread.sleep(1000) function1("fugahoge") }
それでも作れるけど内部で同じように Promise 使ってます。ただの便利関数です。
まとめ
- Future[A]... いつか型Aの値が与えられる型
- Promise[A]... いつか型Aの値を与える型
- Promise と Future で イベントハンドリング が楽に書ける
- FutureでThread.sleepやめろ!(Thread.sleepやブロッキングすると当然暇なスレッドができます。それを意識してやってるなら別にいいです)
というのがわかる5分で読める解説ページができるといいな〜
Future内でThread.sleepはするな
前回の記事(Scala ExecutionContextって何 / Futureはスレッド立ち上げじゃないよ - ましめも) で import scala.concurrent.ExecutionContext.Implicits.global とは何なのか、そもそも ExecutionContext とは ということについて解説した。
おさらい
- ExecutionContext は スレッドプールを持っていて、そこにタスクを割り当てる機構
- ExecutionContext.Implicits.global はデフォルトではCPUコア数分のスレッドを持っている
Future内でThread.sleepはご法度
ExecutionContext.Implicits.global を使っている状態でThread.sleepをすると非常に迷惑になることがある。
例えば次のようなコードがあったとして、CPUコア数4のマシンで関数fugaと関数hogeが同時に実行されると一体どういうことになってしまうか?
import scala.concurrent._ import ExecutionContext.Implicits.global def fuga = { // CPUコストのかかる function1 という関数をマルチコアを使って効率よく処理したい (1 to 10).map {e => Future {function1(e)} } } def hoge = { // 3秒待ってから処理するのを簡単に書いてみた (1 to 3).map {e => Future {Thread.sleep(3000); function2(e)} } }
ご想像の通り、処理がつっかえてしまう(下図)。
もちろんCPU使用率が100%であればマシン資源をフルに使っているからいいのだが、緑色のタスクはただ寝てるだけだ!(Thread.sleep しているだけ) 資源を無駄にしてしまっている!
橙色のタスクは律儀にglobalのスレッドが空くのをただただ待っている状態だ…
よって、ExecutionContext.Implicits.global を使う場合に原則 Thread.sleep を行ってはいけない。同様に、ブロッキングが発生する操作を行うべきではない(IOのblockingなど)。ここ、みんなの使うところだから。
Thread.sleepしたい場合やIOのblockingを行う場合どうすりゃいいのか
タイトルは半分ウソで、絶対にThread.sleepしてはいけないわけではない。みんなで共用する(可能性のある) ExecutionContext.Implicits.global でそういうことをすると思わぬパフォーマンスの低下を生む可能性があるわけであって、自分で使う用の ExecutionContext を作ってそれを使えば問題ない。
ExecutionContext のscaladocにもそう書いてある
A custom ExecutionContext may be appropriate to execute code which blocks on IO or performs long-running computations. ExecutionContext.fromExecutorService and ExecutionContext.fromExecutor are good ways to create a custom ExecutionContext.
例えばこんな感じ
import scala.concurrent._ import java.util.concurrent.Executors val ec = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(2)) (1 to 30).foreach {_ => Future{Thread.sleep(3000); println(new java.util.Date)}(ec)} /* 2個ずつ動いている Sun Dec 07 23:47:55 JST 2014 Sun Dec 07 23:47:55 JST 2014 Sun Dec 07 23:47:58 JST 2014 Sun Dec 07 23:47:58 JST 2014 Sun Dec 07 23:48:01 JST 2014 Sun Dec 07 23:48:01 JST 2014 Sun Dec 07 23:48:04 JST 2014 **/
これで、global側を邪魔しない。
なおPlay frameworkを使っている場合だと、内包してる akka で簡単に ExecutionContext を作ることができたりする ("Many specific thread pools" のところ参照)
https://www.playframework.com/documentation/2.3.x/ThreadPools
ExecutionContext.Implicits.global のご利用は計画的に
ここまで書いて、ExecutionContext とトイレ(個室)はほとんど同じなんじゃと思い始めた。ExecutionContext.Implicits.global は共用トイレと考えると…
- 個室の数(= Thread数 = 同時に処理できる数)には限りがある。先着順で処理する
- 空いてるときは100歩譲って寝てようと何してようと問題にならない
- 待ち行列ができてるにも関わらずトイレ(ExecutionContext)で寝てるのは許せない。本当に用を足してるならしょうがない
- 待ち行列ができることに何も問題ないと言い切れるなら別に構わない
- 自分で作った自分専用のトイレ(ExecutionContext)なら自由にしてくれ
うん!大体あってる気がする!それでは快適なトイレライフを!