ましめも

技術系メモ

Scala + Play Framework の環境に分散トレーシングを導入する(したい)

Scala + Play Framework の環境で分散トレーシングを導入したくなったので色々調べたり実験しました。(結論から言うと、まだうまくいっていません…。)

分散トレーシングって?なんで使いたいの?

今回、僕が導入したいシステム(サーバサイドアプリケーション)はマイクロサービスアーキテクチャではないのですが、APIのレスポンスタイム向上のために分散トレーシングシステムを入れたくなりました。 もちろん本当は「分散」トレーシングシステムである必要はなく、単純に グラフィカルに 処理のボトルネックを見たいだけです。

例えば、並行に処理している場合、ボトルネックを分析するのがログ等のテキストだと難しいことがあります。

以下のような処理があったとします。 f:id:mashijp:20191228150054p:plain

それぞれの処理のログがあったとして、すぐにボトルネックを見つけることはできるでしょうか?

A: 100ms
B: 50ms
C: 120ms
D: 10ms
E: 45ms

答えですが、ボトルネックAとE です。次のように図にすると明らかです。

f:id:mashijp:20191228151016p:plain

これが現実のAPIだった場合、AとE以外の処理の高速化をいくら試みても、全くの見当違いでAPIレスポンスタイムは向上しないことになります。

それでもまだ今回はまず図があったため数字さえ書き込めばボトルネック分析は容易ですが、現実のアプリケーションの場合、そもそも図に正確に起こすところが大変になる場合もあるでしょう。

分散トレーシングでは、分散トレーシングのための仕組みをアプリケーションに取り込むことで、処理の流れを上図のようにグラフィカルに見ることができます。

分散トレーシングを実現するミドルウェア

たくさんありますが、今回は以下のアプリケーションを試してみることにしました。

Jaeger

www.jaegertracing.io

  • Uber Technologies が作っているやつ。
  • バックエンドのデータストアとして Cassandra, Elasticsearch が利用可能。
  • OpenTracing という規格に対応

Elastic APM

www.elastic.co

  • Elastic が作っているやつ
  • 当然バックエンドは Elasticsearch
  • OpenTracing という規格に対応

どのように処理を記録していくのか?

https://www.jaegertracing.io/docs/1.16/architecture/ から引用

f:id:mashijp:20191228163919p:plain

処理の単位を Span と呼び、 Span は親を持つことができます。 Span の塊を Trace と呼びます。

「分散」トレーシングにおいては別のマイクロサービスに対しコンテキストを渡す…などの概念がありますが、今回はモノリシックアプリケーション内でのトレーシングを目標としているので、割愛します。

また、使うミドルウェアによって多少言葉は異なりますが、大まかには同じです。

次回以降、実装例を紹介します。

MacでGUIを使って簡単にSANs対応のオレオレ証明書(マルチドメイン証明書)を発行する

以下の記事の続き。 mashi.hatenablog.com

オレオレ認証局を使って、オレオレ証明書を発行する。

CSRを使って証明書を作成する場合

本物の証明書を作る手順同様、CSRを作成してからオレオレ認証局で証明書を作成するパターン。CSRを作成する方法は世の中に溢れてるので適当にググってほしい。

1) キーチェーンを開きメニューからアシスタントを開く

f:id:mashijp:20190414212254p:plain

CSRをドラッグドロップする画面が出てくるのでCSRを入れる。

2) 発行認証局

f:id:mashijp:20190414212745p:plain

この要求に対するデフォルトを無効化にチェックを入れる

3) 証明書情報

f:id:mashijp:20190414212846p:plain

4) 【重要】 証明書情報(2)

f:id:mashijp:20190414172438p:plain

  • 【重要】名前(通称) は Common Name (CN) に相当する。配信したいFQDNを入力する
    • SANs領域はあとで設定できるので、複数のFQDNで配信したい場合、メインとなるFQDNを設定しておく
    • 近代のブラウザだとCNは無視されるものもあるらしい (SANs 領域しかみない)

5) 鍵用途拡張領域 【スキップ可能】

f:id:mashijp:20190414172838p:plain そのまま

6) 拡張鍵用途拡張領域

f:id:mashijp:20190414172912p:plain

ごくまれに、ここの項目が異なっているとSSL証明書として使えない(選択できない)ロードバランサ等が存在するため注意。

  • SSLクライアント認証、SSLサーバ認証 にチェックを入れる

7) 基本成約拡張領域 【スキップ可能】

f:id:mashijp:20190414173002p:plain

  • 基本制約拡張領域を含める にチェックを入れる

8) 【重要】サブジェクト代替名拡張領域

f:id:mashijp:20190414173106p:plain

  • dNSName に配信したいFQDNを半角スペース区切りですべて登録する
    • Common Nameも含めてすべて指定する
    • 近代のブラウザの中には Common Name を無視しこの SANs 領域しかみないものもあるらしい

9) 作成完了!

f:id:mashijp:20190414173654p:plain SANs がそれっぽくなっていることを確認

10) 証明書のエクスポート

発行した証明書を右クリックし、エクスポートする。

秘密鍵と証明書を同時に作成する場合

1) キーチェーンを開きメニューからアシスタントを開く

f:id:mashijp:20190414172148p:plain

2) 証明書を作成

f:id:mashijp:20190414172239p:plain

  • 固有名のタイプ を リーフに
  • 証明書のタイプ を SSLサーバに
  • 「デフォルトを無効化」 にチェックを入れる

3) 証明書情報(1)

f:id:mashijp:20190414172358p:plain 好きに設定

4) 【重要】 証明書情報(2)

f:id:mashijp:20190414172438p:plain

  • 【重要】名前(通称) は Common Name (CN) に相当する。配信したいFQDNを入力する
    • SANs領域はあとで設定できるので、複数のFQDNで配信したい場合、メインとなるFQDNを設定しておく
    • 近代のブラウザだとCNは無視されるものもあるらしい (SANs 領域しかみない)

5) 発行者を選択

f:id:mashijp:20190414172729p:plain 先ほど発行したルート証明書を選択する

(中略)

このへんは上のCSRを元にするやつと同じ

11) 証明書の場所を指定【スキップ可能】

f:id:mashijp:20190414173605p:plain

12) 作成完了!

f:id:mashijp:20190414173654p:plain SANs がそれっぽくなっていることを確認

13) エクスポート

証明書の出力

f:id:mashijp:20190414173940p:plain f:id:mashijp:20190414174006p:plain

秘密鍵の出力

f:id:mashijp:20190414174215p:plain

パスワード保護されたPKCS12形式でしか出力できない模様。PEM形式等にしたい場合は自分でopensslコマンド等を使って変換する必要がある。

MacでGUIを使ってオレオレルート認証局を作る

GUIを使ってオレオレルート認証局を作り、楽に証明書を発行したい!

オレオレ認証局を作ること自体は簡単だが、SANs証明書(マルチドメイン証明書)を発行するのは実はかなりめんどくさい!めったに作らないので手順も覚えられない!

qiita.com

実は Mac には標準に組み込まれているキーチェーンでGUIベースでオレオレ認証局を開設し SANs証明書を払い出す仕組みがあるので、今回はそれを利用して開設する。 Windows を使っている人はわからないのでご自身で調べてください><

オレオレ認証局を開設するメリット

常時SSL化が当たり前となった今、ローカル環境や開発環境等でもSSL化しテストすることが多くなってきたと思う。

たとえ商用の環境でなかったとしても、やっぱりこういうエラーは出てほしくないし無視したくない。

f:id:mashijp:20190414175200p:plain f:id:mashijp:20190414175239p:plain

f:id:mashijp:20190414175455p:plain

例えばよくある事故として、「開発時は」証明書の信頼チェックをオフにしオレオレ証明書を使えるようにしているつもりが、実際にエンドユーザーにリリースしたアプリケーションでもチェックをスキップしたままになっていた、などがある。

このような事故を防ぐために&ブラウザ等で毎回証明書を無視する画面を見なくてすむよう、「真面目に」オレオレ証明書を発行しようと思う。 (あらゆる環境で正規の証明書を発行できる人は正規のものを使うにこしたことはないのでそうしてほしい)

f:id:mashijp:20190414180710p:plain

目指したい姿はこんな感じ。オレオレルート認証局を作り、使いたい環境でルート証明書を信頼するようにしておく。 一度オレオレルート認証局を作ってしまえば、あとは 本番環境等と同じ手順で証明書を発行できるようになる。

本番環境

  1. 秘密鍵を払い出し、CSRを発行する
  2. 認証局CSR を渡し、証明書を発行する
    • いわゆるマルチドメイン証明書にしたい場合はこのときに複数のドメインを設定する (SANs)
  3. 証明書を使う

オレオレ環境

  1. 秘密鍵を払い出し、CSRを発行する
  2. オレオレルート認証局で証明書を発行する
    • いわゆるマルチドメイン証明書にしたい場合はこのときに複数のドメインを設定する (SANs)
  3. 証明書を使う

キーチェーンを使うメリット

最初に書いたとおり、Macを使っているなら、オレオレルート認証局をキーチェーンで開設することができる。そのメリットは以下の通り。

  • GUI
  • ルート認証局秘密鍵が自動的にそれなりに安全なところで保存される
  • OS上の信頼設定もできる

ルート認証局を開設する手順

手順がかなり長いように見えるが、SSL大好きマン以外は【スキップ可能】のところは無視してそのまま進んでもらっていい(多分普段の開発で困ることはないと思う)。

1) 認証局作成ウィザードを起動する

f:id:mashijp:20190414165032p:plain

キーチェーンを起動し、メニューから起動できる

2) 認証局を作成

f:id:mashijp:20190414165411p:plain

  • 固有名のタイプ: 自己署名ルート証明書
  • ユーザー証明書: SSLサーバ
    • ここで何が変わるかは分からない…
  • 「デフォルトを無効化」にチェックを入れる
    • ここにチェックを入れるとカスタマイズが可能になる
  • メールの送信元は適当

3) 証明書情報(1)

f:id:mashijp:20190414165851p:plain

  • 有効期間: 好きに設定する
    • 長くないと不便だと思う人が多いと思うのでここでは10年にする
  • インビテーションに署名: 外す
    • 割愛するが今回の用途では不要なので外す (どっちでもいい)

4) 証明書情報(2)

f:id:mashijp:20190414170134p:plain 好きに入力してほしい

5) このCAの鍵ペア情報【スキップ可能】

f:id:mashijp:20190414170224p:plain 好きなものを選ぶ。2019年現在、世の中の主要ルート証明書RSA 2048bit。こだわりがないなら変えないほうがいい。

6) このCAのユーザーが使う鍵ペア情報を指定【スキップ可能】

f:id:mashijp:20190414170424p:plain これも特に変えなくていい

7) このCAが使う鍵用途拡張領域【スキップ可能】

f:id:mashijp:20190414170608p:plain

なんとなく本物のルート証明書っぽくしたいので以下のようにしてみた。詳しい方、間違ってたら教えてください。 多分オレオレ証明書として使う分にはここの設定をどう変えても普通に動くと思う。

  • この拡張領域は重要 にチェックを入れる
  • 証明書署名、CRL署名 にチェックを入れる

多分ここの部分に相当すると思うんだけど… f:id:mashijp:20190414170750p:plain

8) このCAのユーザーが使う鍵用途拡張領域【スキップ可能】

f:id:mashijp:20190414171014p:plain

  • この拡張領域は重要 にチェックを入れる
  • 署名、鍵の暗号化 にチェックを入れる

9) このCAが使う拡張鍵用途拡張領域【スキップ可能】

f:id:mashijp:20190414171158p:plain

そのまま進む

10) このCAのユーザーが使う拡張鍵用途拡張領域【スキップ可能】

f:id:mashijp:20190414171236p:plain - この拡張領域は重要 のチェックを外す - SSLクライアント認証、SSLサーバ認証 にチェックを入れる

11) このCAが使う基本成約拡張領域【スキップ可能】

f:id:mashijp:20190414171352p:plain そのまま進む

12) このCAのユーザーが使う基本成約拡張領域【スキップ可能】

f:id:mashijp:20190414171446p:plain - 基本成約拡張領域を含める にチェックを入れる

13) このCAが使うサブジェクト代替名拡張領域【スキップ可能】

f:id:mashijp:20190414171528p:plain そのまま

14) ユーザーが使うサブジェクト代替名拡張領域【スキップ可能】

f:id:mashijp:20190414171629p:plain

15) 保存場所の指定【スキップ可能】

f:id:mashijp:20190414171647p:plain

16) 完成!

f:id:mashijp:20190414171713p:plain

オレオレルート証明書の信頼設定

f:id:mashijp:20190414173820p:plain

発行した証明書を選び、上記のように「常に信頼する」になっていれば使える状態のはずだが、なぜか発行したてのルート証明書は信頼されないことがある? その場合は、一回「信頼しない」にし画面を閉じたあともう一度開いて「常に信頼する」に設定しておく。

f:id:mashijp:20190414173840p:plain

Mac以外の環境で信頼するようにしたい場合は、ルート証明書を書き出し、その証明書を信頼するようそれぞれのOS/デバイスで設定すればよい。

続き

mashi.hatenablog.com

この手順で作った僕の認証局の証明書を貼っておく。みんな僕のことすごく信頼していると思うので、みんなの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についてのスライド書きました/つまらないシステム

つまらないシステム(1) - Scala の Future と ExecutionContext

speakerdeck.com

つまらないシステム(2) - Scala 書きやすすぎるFutureの罠

speakerdeck.com

というスライドを公開しました。

Scala の Future を使うのに慣れてきた人向けの、Future の ExecutionContext を上手に使って詰まらない堅牢なシステムを作ろうという話です。

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)} 
  }
}

ご想像の通り、処理がつっかえてしまう(下図)。

f:id:mashijp:20141207235401p:plain

もちろん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)なら自由にしてくれ

うん!大体あってる気がする!それでは快適なトイレライフを!