pumaのthread数の伸縮がどのように行われているか

pumaが同時に建てるthread数はこちらで指定することが出来る

実際の定義を見るとこのように2つの値を入れることが出来る

threads 5, 16

第一引数が最小値、第二引数が最大値を表しているらしく、処理の際に自動でスレッド数をスケールしてくれるらしい

実際どのように動くか空気感を調べる必要があったためメモ

最初にまとめ

語弊がある気もするけどこんな感じ

  • 最初は最小値の数だけthreadを用意する (thread poolに格納される)
  • 既存のthreadが全てビジーな状態になった際、現在のthread poolのスレッド数が最大値に達していない場合は Thread.new して新しいthreadを確保
    • thread数が +1 される
  • 30秒おきにthread poolの状況を確認し、もし待ち状態のthreadが存在するのであればthreadを1つ削除するよう宣言
  • threadの処理に入る際、実際に待ち状態だった場合に自身を削除する

表題の関心事について調べた時に見たところ

以下のh4要素はそれぞれgithubへのリンクになっているので自分で実際のコードを見る場合はそちらも参考にしてください

Puma::Server#run

Puma::Server#run の中という如何にもなメソッドの中に如何にもな記述があり、ここで設定した最小値と最大値を渡している

@thread_pool = ThreadPool.new(@min_threads,
                              @max_threads,
                              IOBuffer) do |client, buffer|

#run メソッド内部で Puma::ThreadPool#auto_trim! というメソッドを呼んでいる

if @auto_trim_time # デフォルト値は 30秒
  @thread_pool.auto_trim!(@auto_trim_time)
end

その後 #handle_servers を呼び、実際にハンドリングされるっぽい (今回リクエストがどうハンドルされるかという点はちゃんと追ってない)

if background
  @thread = Thread.new { handle_servers }
  return @thread
else
  handle_servers
end

今回は先に #handle_servers の処理を追う

Puma::Server#handle_servers

無限ループになっていて、Puma::Client (実際にリクエスト/レスポンスを処理する実装があるっぽい) をthread poolに渡す箇所がある

pool = @thread_pool
# 無限ループの中
client = Client.new io, @binder.env(sock)
# 中略
pool << client

Puma::ThreadPool#<<

@todo (ただの配列) にclientを挿入する

ここで今回の関心事について重要っぽい条件分岐が存在していて、

  • 待ち状態のthreadの数(@waiting) が todoの要素数 より小さい かつ
  • 現在存在するthreadの数(@spawned) が 設定した最大値(@max) より小さい

という場合にのみ #spawn_thread を呼び出す

def <<(work) # work には前述した client が渡される
  @mutex.synchronize do
    # 略
    @todo << work

    if @waiting < @todo.size and @spawned < @max
      spawn_thread
    end
    # 略
  end
end

Puma::ThreadPool#spawn_thread

実際にthreadを新しく作って @workers に溜めるところ

threadの内容的には無限ループしていて、処理内容は

  • todoに要素が残っている場合
    • その処理
  • todoに要素が残っていない場合
    • @trim_requested(threadを削除したいという要求の数が格納された変数) が 1以上の場合
      • ループから抜ける
    • @trim_requested が 0の場合
      • todoに要素が入ったことを受け取るまで待つ ( @waiting に +1 する)
      • 待ち終わったら @waiting -1

となっていて、ループから抜けるとworkersから自身を削除する、という動きをする

(todoの各要素は前述したがclientとなっていて、実際にhttpのリクエストを処理するものとなっている)

ここまでがthreadsを実際に増やすまでの流れと、減らす機構の実装で、ここからは減らす起因となる部分を追う

Puma::ThreadPool#auto_trim!

記事の前半 (Puma::Server#run のあたり) で追従せずにすっとばした処理

AutoTrimなるものを動かす timeoutに渡される値は Puma::Server#run で渡されたものとなっており、デフォルトだと30秒

見たままのことをしている

def auto_trim!(timeout=30)
  @auto_trim = AutoTrim.new(self, timeout)
  @auto_trim.start!
end

Puma::ThreadPool::AutoTrim#start!

timeoutの分だけsleepを挟みつつ Puma::ThreadPool#trim を呼び出している

def start!
  @running = true

  @thread = Thread.new do
    while @running
      @pool.trim
      sleep @timeout
    end
  end
end

Puma::ThreadPool#trim

  • 待ち状態のスレッド(@waiting) が 1以上 かつ
  • 削除予定スレッド数(@trim_requested) を差し引いた現在のスレッド数 が 設定した最小値 より大きい

場合のみ @trim_requested を +1 する

def trim(force=false)
  @mutex.synchronize do
    if (force or @waiting > 0) and @spawned - @trim_requested > @min
      @trim_requested += 1
      @not_empty.signal
    end
  end
end

ここで +1 され、thread側のループが回ることでようやくthreadが削除される

わかること

ここまで追うと今回知りたかったことはだいたいわかる

以下2つの機構によってthreads数が伸縮する

  • リクエストを処理するところ
    • リクエストが来た場合はtodoに格納する
    • todoに入るリクエストは既存のthreadが処理していく
    • 既存のthreadがすべて処理中だった場合新規にthreadを作成
    • もし @trim_requested があった場合はthreadを削除する
  • threadを少なくするところ
    • 30秒おきにthreadsの状態を鑑みて @trim_requested を +1 する

こういうスレッドベースな処理を書いたこともコードを読んだこともなかったのでこの処理が良いのか悪いのか普通のことなのかわからないが、全体的に宣言的に動いているんだなというのがわかる (各処理ごとに現在のステータスを変更していき、各処理ごとにそのステータスに合わせて動くような実装だった)

しかもそういうものって非同期であることを意識して書かないといけないのでちょっと億劫になってしまうものだと思っていたが、そのあたりはruby標準ライブラリの良い感じな機構提供のおかげでわかりやすさが保たれているように感じた、Mutex と ConditionVariable すごい (Rubyもすごい)

今は別に非同期ななにかを書くつもりはないが、デザインのパターンとその実装例として大いに参考になるコードだった

カレントディレクトリにあるリポジトリと同じ名前の別リポジトリにcdするスクリプト

はじめに

自分でリポジトリをフォーク、クローンしてるけど、ローカルの別ディレクトリにもoriginのリポジトリをクローンしているってことよくあるだろうし、そのoriginのリポジトリまでcdしたいこともよくあると思う

$ echo $PWD
/Users/naari3/src/github.com/naari3/hyper-nice-repo
# ここにいるけど

$ cd /Users/naari3/src/github.com/alice/hyper-nice-repo
# ここに移動したい

ので移動するためのzsh functionを書いた

requirements

多分よくある組み合わせだと思います みなさんpecoもghqも使ってますよね?

ソース

function peco-cd-forked-repo () {
    local current_repo_name="$(basename "$PWD")"
    local selected_dir=$(ghq list --full-path --exact $current_repo_name |
        peco --prompt="cd-ghq-fork >" --query "$LBUFFER")
    if [ -n "$selected_dir" ]; then
        BUFFER="cd $selected_dir"
        zle accept-line
    fi
}

変哲なことはしていない

僕はこれを適当なkeybindで発火するように設定していていつでもリポジトリを反復横跳び出来るようにしている

おわりに

pecoはすごい

k3sを使う

はじめに

k3s.io

公式サイトがイカ

k3s とは

github.com

k8sの色々を削ぎ落としたものらしい

  • 使われていないコードなど

バイナリ一枚で40MB程度で軽い、とのこと (はたして40MBは軽いと言えるのか)

基本の使い方

Linux 3.10以上のマシンの上で以下を実行

sh curl -sfL https://get.k3s.io | sh -

これでk8sクラスタが完成する、とてもお手軽

agent を追加する

これも簡単で、k3sのバイナリを以下のように実行する

$ sudo k3s agent --server https://参加したいクラスタのIP:6443 --token ${NODE_TOKEN}
NAME STATUS ROLES AGE VERSION
ip-172-31-38-39 Ready 21m v1.13.3-k3s.6
ip-172-31-38-54 Ready 97m v1.13.3-k3s.6

やったね、楽勝

リモートで使いたい

同じネットワーク内でk3sを立ち上げて使う

これは特に問題ないはず

k3sを立ち上げると /etc/rancher/k3s/k3s.yaml にファイルが配置される

これは同じマシン上に存在するクラスタに接続するためのファイルで、別のマシンから接続するにはこのファイルの一部を同じネットワーク内のk3sが建っているマシンに向ける

apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: hogefuga
-    server: https://localhost:6443
+    server: https://k3sが建っているマシンのプライベートIPを入れる:6443
  name: default
contexts:
- context:
    cluster: default
    user: default
  name: default
current-context: default
kind: Config
preferences: {}
users:
- name: default
  user:
    password: hogefuga
    username: admin

k3sが建っているマシンのプライベートIPを入れる は 以降 (プライベートIP) と表記する

あとはこのymlを指定しながら kubectl を叩けばよさそう

kubectl --kubeconfig ~/.kube/k3s.yml get all

外部ネットワークからk3sのクラスタに接続する

頑張ってセキュア(?)なままアクセスしてみる

外部から愚直に(グローバルIPを直接指定して)接続する場合、上記手順だけでは接続できない

$ kubectl --kubeconfig ~/.kube/k3s.yml get all Unable to connect to the server: x509: certificate is valid for 127.0.0.1,(プライベートIP), not (グローバルIP)

どうやら証明書の対象が127.0.0.1とプライベートIPのみらしい 確認してみる

$ openssl s_client -connect localhost:6443 2>/dev/null | openssl x509 -text | grep -A 1 "Subject Alternative Name"
  X509v3 Subject Alternative Name:
    DNS:, IP Address:127.0.0.1, IP Address:(プライベートIP)

つまり、表示されたIP以外ではこの証明書は有効にならないのでクライアント側から通信を弾く状態となる

プライベートIPが動的にSANに割り当てられるところを見るとどうやらこの証明書はk3s内部で動的に生成しているようだ

どこで作成しているか追ってみる

k3s/server.go at f90cbed4081e7e1e6972861c196543b2d253bfcc · rancher/k3s · GitHub

func knownIPs() []string {
  ips := []string{
    "127.0.0.1",
  }
  ip, err := net.ChooseHostInterface()
  if err == nil {
    ips = append(ips, ip.String())
  }
  return ips
}

IPアドレスの文字列の配列を返す関数が見つかる

(このオブジェクトは serverConfig.TLSConfig.KnownIPs に渡され、追っていくと後に norman(rancher製のサーバーアプリケーションの実装?) に渡される)

net.ChooseHostInterface というのはk8sでも使われているutilに生えている関数で、ネットワークインターフェイスからIPを1つ取得して返す (Golangわからんけど合ってるよね?)

  • 先程まで雑に「プライベートIP」と読んでいたものの正体はこれっぽい

GoDoc k8s.io/apimachinery/pkg/util/net#ChooseHostInterface

apimachinery/interface.go at master · kubernetes/apimachinery · GitHub

この関数のIPのリストにグローバルIPを追加で書き込んでビルド、起動してみる (良い手ではないと思う)

func knownIPs() []string {
  ips := []string{
    "127.0.0.1",
+    "グローバルIP",
  }
  ip, err := net.ChooseHostInterface()
  if err == nil {
    ips = append(ips, ip.String())
  }
  return ips
}

すると、SANにもグローバルIPが追加される事が分かる

$ openssl s_client -connect localhost:6443 2>/dev/null | openssl x509 -text | grep -A 1 "Subject Alternative Name"
  X509v3 Subject Alternative Name:
    DNS:, IP Address:127.0.0.1, IP Address:(グローバルIP), IP Address:(プライベートIP)

これで kubectl を叩くと接続できるようになる

$ kubectl --kubeconfig ~/.kube/k3s.yml get all
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.43.0.1 443/TCP 49m

これ、手法としてだいぶダーティーだし、本当はもうちょっと別の手を取るべきなんだろうけど全然わからなかった

誰か正しい方法を教えてくれ〜

(もしこれが正しいのであれば何故k3sにはhostnameを受け取る起動引数が無いのだろうか…)

insecure-skip-tls-verify: true する

単にこちら側が弾いているだけなので、tls verifyをスキップすることでクラスターにアクセスできるようになる

apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: hogefuga
    server: https://グローバルIP:6443
+    insecure-skip-tls-verify: true
  name: default
contexts:
- context:
    cluster: default
    user: default
  name: default
current-context: default
kind: Config
preferences: {}
users:
- name: default
  user:
    password: hogefuga
    username: admin

または

$ kubectl --kubeconfig ~/.kube/k3s.yml get all --insecure-skip-tls-verify=true

結局どうするのが正解なの??

k3sのユースケース

k3sリポジトリの記念すべき1つめのissueもユースケースを尋ねるものだった

github.com

製作者のDarren Shepherd曰く、これは軽量のk8sを作るためのプロダクトだとのこと

https://github.com/rancher/k3s/issues/1#issuecomment-424943047

軽く使えることが大きな理由となるような事に利用するものなのだろうと想像している

  • 実際公式サイトの方でもEdgeとかIoTとかCIとか書かれている

個人的にとても良いと思っているのはk8sの練習台として使うユースケースがある

k8sの練習を行う場合、勿論ながら何かしらの形でクラスタを用意する必要がある

おおよそminikubeかDocker for Desktopに付属のk8sを使うことになると思うが、ここに第三の選択肢としてk3sを上げることが出来るようになった

これは上で説明したように簡単に立ち上げることが出来るし、他より動作が軽いために気軽さはとても大きいと思う

しかもノードの追加も容易なため、複数ノード構成の練習も簡単に出来る

  • 多分先程挙げたminikube, Docker for Desktopのk8sで複数ノード構成を取るより容易
    • そもそもこの2つで複数ノード構成出来るのか?

↓こんな docker-compose.yml もあるので、 github.com

$ docker-compose up -d
$ kubectl get node --kubeconfig kubeconfig.yaml

とすればすぐにk8sの環境が立ち上がるの、普通に便利では?

Androidスマホ OnePlus 5 が Decryption unsuccessful と表示されて起動できなくなった 起動できるようにするまでのメモ

はじめに

  • データは全部ロストしました。バックアップは大事なので常日頃取っときましょう😢
  • 原因は分かっていません
  • この手の問題を解決するために久々に苦労したし情報が纏まっていないのでメモ

物故割れた

朝起きてスマホを触ろうとしたら何故か電源が落ちていて、電源を付けたらこうなってた

f:id:naari_3:20190303000936j:plain
Decryption unsuccessful

Decryption unsuccessful

The password you entered is correct, but unfortunately your data is corrupt.

To resume using your phone, you need to perform a factory reset. When you set up your phone after the reset, you'll have an opportunity to restore any data that was backed up to your Google Account.

(雑な訳、僕はこう受け取った)
復号失敗

入力したパスワードはあってたけど、残念ながらあなたのデータは壊れてます。

スマホを使い続けるためにはファクトリーリセットが必要です。リセット後のセットアップ時、Googleアカウントにバックアップしたデータから復旧できるかもしれませんね。

何事かと思いながらも何回か再起動をしたが、同じ画面が出るだけ

こういう時は既に取っていたTWRPのバックアップデータからリストアすればいいのだろう、と思いリカバリーモードに入ったのだが、ストレージが全部なくなっているように見えた

f:id:naari_3:20190302222024j:plain
Systemがおかしい図

17592186044373MBって何… 17EB(エクサバイト)…?

自分の使っているモデルはストレージが128GBのモデルなので何らかを使って天元突破したんでしょうか、どうやら完全にパーティションがぶっ壊れている様子です 僕は寝てる間に何をしたんだ…?

  • TWRPのバックアップを外のストレージに逃したりするほど要領のよい自分ではなかったこと
  • 復旧させるほうがめんどくさいと判断したこと
  • 元々Android 7系から8系に上げられない要因(後述)が元々あったこと

等が重なり、良い機会だと思ってデータを完全に削除してファクトリーリセットをしようと考えた (一応メニュー上にあるfix filesystemなる項目は試してみたが状況変わらず)

ROMも物故割れてた

そのままTWRPでファクトリーリセットを試み、System(3GB弱)とInternal storage(112GB程度)の要領表示は元に戻ったが、起動できず…

それもそのはずで、/system 以下が全部ぶっ壊れていたのだからROMも全壊状態で、とすればROMを焼く他ないためROM焼きに専念することにした

ROMが焼けない

ROMを焼くと決まればまずは最新のROMをそのまま焼くことにした

最新のROMのバージョンは OOS 9.0.4で、Android 9.xベースらしい

以下からFull ROM Zipをダウンロード

[OFFICIAL] OxygenOS 9.0.4 (Android Pie 9.0) OTA for OnePlus 5 (cheeseburger)

これは余談なんですが、OnePlus系のスマホは面白くて、公式ROM(Oxygen OS、略称OOS)についての情報をOnePlusの人(?)がそのままXDAフォーラムに書き込んでくれている

OnePlus Oneの時はたしかCyanogenModが公式にROMを提供していて、標準ROMがCyanogenMod 11Sというものだった(後に色々あったため、今は独自ROMのOOSになった)というものあるためなのか、割とギークに優しい

話を戻して、ダウンロードしたzipして、焼く

TWRPの AdvancedADB sideloadを選択、受け付ける状態にした後に

adb sideload /path/to/OnePlus5Oxygen_23_OTA_047_all_1902221956_9e0803ddc2.zip

普通はこれで焼けるはずだが、何故か失敗する

失敗時のログを取っていなかったが、「 /vendor が壊れている 」 だとか「 E3004 this package is for "oneplus 5" devices; this is a "" 」のようなエラーだった覚えがある (Warning: No file_contexts Error: Vendor partition doesn't exist! も出たがこちらはもう一度ファクトリーリセットをかけたら表示されなくなった)

よくわからなかったが、詳しい知り合いに相談してみたところ、ちょっと前からAndroidでProject Trebleというものが始まっているらしく、 /vendor はそのためのディレクトリなのではないかということだった

juggly.cn

ここではProject Trebleについては大きく説明しない(よくわかっていないため)

OnePlus 5は登場してからそろそろ2年が経とうとしているわけで、そんな昔の端末でも対応しているのか分からなかったが、調べてみるとどうやら OOS 5.1.5 からProject Trebleに対応するようになったらしい

OnePlus 5 & OnePlus 5T get OxygenOS 5.1.5 with Project Treble support

forums.oneplus.com

つまりバージョンのアップデートで /vendor がまともになり、そうでないと 最新のOOSを焼くことが出来ないんじゃないか?と考えた

適当にそのままOOS 5.1.5を焼こうとしたがこれもまた焼くことが出来ず…

このときのエラーは E3005: This device is unlocked;bootstate: ""; sdk version "25". というエラーだった

そういえば昔「この端末で OOS 4.x (Android 7.x) から OOS 5.x (Android 8.x) にアップデートしようとした時、なぜかブートローダーをロックした状態でないとアップデート出来なかったなぁ、ブートローダーをロックするとデータが消えるし、面倒くさいし放置でいいや」となったことを思い出した

今回は既にデータを全損したので面倒くさがる理由も無くなったため、次はブートローダーをロックすることにした

公式のリカバリじゃないとだめ

というわけで次はブートローダーをロックする

fastbootに切り替えてからこれをする

fastboot oem lock

端末の操作をしてロック成功

さてTWRPからROMを焼こうとするが、TWRPが起動しない

どうやらlock状態ではTWRPは使えないようで、そんな状態からリカバリー的な行動を起こすためにはリカバリーを最初から入っていた標準のものに焼き変える必要があるとのことだった

以下で標準リカバリーのダウンロード

[OFFICIAL] OxygenOS 9.0.4 (Android Pie 9.0) OTA for OnePlus 5 (cheeseburger) (Stock recovery imagesという項目の “7.1.1_2017-05: OP5_recovery.img` のほう)

fastboot flash recovery /path/to/OP5_recovery.img

これであればlock状態でもリカバリーに入って焼くことが出来る

OOS 5.1.5のROMが焼けないのでアンロックしてからOOS 4.xのものを焼く

これでようやくOOS 5.1.5のROMが焼けるようになったので焼こうとしたが、焼くことが出来なかった (標準リカバリではTWRPと違い、ターミナル上の進捗表示が45%前後になるとエラーを吐いて死ぬ)

ここでまた思い出したことがあり、ブートローダーのロック/アンロックをした後は一度ROMを起動しないと正しく動かない事があるらしく、もしやそれに引っかかってしまったのでは?と考えてみた

そもそも前述したとおりにこの端末では未だ OOS 5.x の稼働実績がないこともあったため、もう一つ戻って OOS 4.x を入れてみることにした

今回はOOS 5 になる直前の OOS 4.5.15 を焼く

ROMは以下の公式リンクのコピー集からダウンロードした(XDAにも有志のミラー集があるが、Android transfileというアップローダーが激で遅いため以下からのダウンロードがおすすめ)

forums.oneplus.com

まずは念の為に前まで動いていた状態に戻すため、もう一度ブートローダーをアンロックする

fastboot oem unlock

標準リカバリーでもadb sideloadでROMが焼けるので焼く

adb sideload /path/to/OnePlus5Oxygen_23_OTA_020_all_1712052226_98fd584ab032fe.zip

これでようやく成功し、ROMの起動ができるようになった🎉

OOSのアップデートを繰り返す

この次に、まずは OOS 5.1.5 へのアップデートを目指すため、段階を踏んでのアップデートを行う

まずは OOS 5.1.1 へのアップデート (このバージョンを選んだ理由は大きくなく、殆ど当てずっぽうでアップデートした)

先程貼った公式ダウンロードリンク集から OOS 5.1.1 のROMのダウンロード、同じようにリカバリからROMを焼く

adb sideload /path/to/OnePlus5Oxygen_23_OTA_034_all_1804201219_3824995916a49ee.zip

こちらも問題なく成功

次に OOS 5.1.5 (さっき焼けなかったやつ) を焼いてみる

adb sideload /path/to/OnePlus5Oxygen_23_OTA_038_all_1808082017_ebb1d69f37.zip

こちらも成功!先程出来なかった工程が出来るようになった

OOS 5.x を焼くには lock状態 かつ ROMが存在する状態でないと成功しないのかもしれない

OTAで OOS 9.0.4 へアップデート

OOS 5.1.5 の起動を確認したところ、 OOS 9.0.4 へのアップデートを催促されたため、そのままアップデート (OTAで1.6GBのファイル落とさせるのってどうなんですかね…)

既に上記の手順でクリーンになっているためか、問題なく終了した

消えたデータはどうしようもなかったが、なんとか起動できる状態まで持ち込めた

つまり我々はどうすればよかったのか

今回成功した手順のみを書き記していくと以下のようになる

  • TWRPでFactory Reset
  • 標準リカバリの書き込み
  • ブートローダーのロック
  • OOS 4.5.15 の書き込み
    • 一回起動しておくこと
  • OOS 5.1.1 の書き込み
  • OOS 5.1.5 の書き込み
  • OOS 9.0.4 へOTAでアップデート

結局具体的に何が原因でこうなったのか、何が良くて成功出来たのか分からなかったが、起動できるようになったので良しとしましょう

余談: LineageOSの最新版もすぐに焼けなかった

最初に OOS 9.0.4 を焼こうとした後、LineageOSの最新版も焼こうとしたが、こちらも失敗した

XDAのフォーラムを見てみると HOW TO INSTALL LINEAGEOS の欄に

Make sure your phone was running OxygenOS 5.1 at least once and is currently on latest firmware

と書いてあり、一度 OOS 5.1.x を起動する必要があったようだ

もしかしたら先程 OOS 4 の後に.OOS 5.1.1 を焼いたのは知らずに良い手を打っていたのかもしれない

さいごに

一体何故こうなったのか分かっておらず、こういった状態になると近いうちにまた同じ問題が起こるのではないかと不安になる

スマホも特定のアプリしか使っていなかった上に殆どの情報がクラウド上に保存されている状態だったため、次回使えるようになるまでの復旧は楽だろうが、遠出中に急にこのような事態になると困ってしまう、こわい

早いうちに機種変しようかなぁ…高い出費こわいなぁ…

pcapファイルからhttpリクエストとレスポンスを扱えるように色々するgem httpcap-rbを公開した

httpcap-rbを公開しました

rubygems.org

github.com

未練

本当はhttpcapっていう名前で公開したかったけど2年程前に既に名前が取られてたし、しかも内容が

module HTTPCap

end

のみとかいう殆ど機能していないライブラリで、とても悔しかったんですがどうにもできないのでhttpcap-rbという名前にしました

機能

pcapファイルからTCPのsend/recvのパケットをよしなに結合したあと、HTTPのメッセージとしてパースして、HTTPリクエストとHTTPレスポンスを一つの纏まりとして固めてから返します

require 'httpcap'
HTTPcap.http_flows('./http.pcap') do |flow|
  p flow.request.body
  # => "{\"userId\":12345}"
  p flow.request.headers['Authorization']
  # => "Bearer hogehoge123455567890"
  p flow.response.http_status
  # => 200
  p flow.response.body
  # => "{\"userId\":12345,\"name\":\"naari3\",\"author\":true}"
  p flow.request.headers['Content-Length']
  # => "46"
end

(これ、リクエストとレスポンスを1:1として考えてるんですけど、もしかして仕様的に間違ってたりしますか?)

なお、httpsとかの暗号化された通信については特に考慮されてません

そのうちしたいけど、平文の通信だけ捌けるようになった今、自分の要件を満たすことは出来たのであまりモチベがない

実装

TCPのパケットの結合と振り分け

tcpの結合や取り扱いは reassemble_tcp というgemをそのまま使用しました

wiresharkと同じ結合をしてくれます(synとackを見てbodyをくっつけるアレ)

READMEに書かれている ReassembleTcp.tcp_data_stream を使うとセッションが担保されなくなったり、pktじゃなくてbodyだけが返ってきたりするので ReassembleTcp.tcp_connections を使うと良いです (こっちはPacketFuでパースされたあとの各packetが返る)

というわけで ReassembleTcp.tcp_connections を使うとtcpの各セッションごとに格納、かつ結合されたパケットが返ってきます

各セッションの結合後のパケットを #each_slice(2) を使って回します
おそらくどちらか片方ががリクエストで、もう片方がレスポンスなのでそう思ってパースをします

HTTPメッセージのパース

HTTPのメッセージはリクエストとレスポンスの両方を捌く必要があります

PumaとかWEBrickみたいなよく使われているライブラリがパースした結果を使うことが出来ればそれなりにリッチなものが返せるかと期待したんですが、リクエストとレスポンスの両方を良い感じに扱った実装を見つけることが出来ませんでした

例えば WEBrick::HTTPRequest には #parse というのがいてIOを渡すとよしなにHTTPのパースをしてくれるらしいんですが、 WEBrick::HTTPResponse#parse は存在しません

なぜならサーバーの実装としてHTTPのメッセージをパースする機能は、クライアントからのリクエストを解釈する時以外に必要ないからです

なので、CとかC++とかに存在する素朴なHTTPパーサー実装のRubyバインドを探すことにしました (つまり要素によしなにアクセスしたりする部分は自分で書かなければいけない…)

今回は http-parser-lite を使いました

httpの気持ちは知らないので何故httpのパーサーがおおよそオブザーバーパターンなのがよくわからないんですが、上のgemも例に漏れずオブザーバーパターンを採用したものになっています

他と違ってこういう書き方が出来るのが特徴とのことですが、

parser = HTTP::Parser.new

parser.on_message_begin do
  puts "message begin"
end

parser.on_message_complete do
  puts "message complete"
end

parser.on_status_complete do
  puts "status complete"
end

今回は上記の機能を使うことでうまいことリクエストとレスポンスのクラスの抽象化が出来ました

# リクエストとレスポンスの抽象概念
class Message
  attr_reader :body, :headers

  def initialize(type, data)
    @parser = HTTP::Parser.new(type)
    # 略
    %i[on_message_complete on_url on_header_field on_header_value on_headers_complete on_body].each do |name|
      @parser.send(name, &method(name))
    end
    receive_data(data)
  end

  def on_header_field(value)
    @headers.stream(Headers::TYPE_FIELD, value)
  end

  def on_header_value(value)
    @headers.stream(Headers::TYPE_VALUE, value)
  end

  def on_headers_complete
    @headers.stream_complete
  end
  # 略
end

ヘッダーが存在することとボディが存在することは共通なのでこのクラスで実装して、それ以外の差異を継承先のクラスで実装する形をとっています

ふあんなこと

  • TCPのシーケンス順に2つずつ結合済みパケットを取得して 片方をリクエスト、片方をレスポンス、と組を雑に決めてしまったが、もしかしてもうちょっとまともにリクエストとレスポンスの組を特定する方法がある??
  • 自由な内容のpcapファイルを作る方法がわからない…(調べていない)のでテストケースが不足している
    • ノイズとして別のTCPパケットも紛れ込んできそうだよね

おわりに

今回は久々に自分の要望がメインとなってgemをつくることになったが、モチベの保持がとても楽でよかったです

自分の作りたいものが出てこないというのが失格感あって悲しいので、問題解決を頑張っていきたいと思います

CentOS7.3でdocker0がないため/ネットワークの重複でdocker0が作成されなかったのでdocker.serviceが立ち上がらなかった

docs.docker.com

↑に従ってインストールしてる時、

$ sudo systemctl start docker

で失敗した

以下はjournalctlのログ

12月 20 16:09:52 example systemd[1]: docker.service holdoff time over, scheduling restart.
12月 20 16:09:52 example systemd[1]: Starting Docker Application Container Engine...
-- Subject: Unit docker.service has begun start-up
-- Defined-By: systemd
-- Support: http://lists.freedesktop.org/mailman/listinfo/systemd-devel
--
-- Unit docker.service has begun starting up.
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.180328678+09:00" level=info msg="parsed scheme: \"unix\"" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.180393506+09:00" level=info msg="scheme \"unix\" not registered, fallback to default scheme" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.180440304+09:00" level=info msg="parsed scheme: \"unix\"" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.180449023+09:00" level=info msg="scheme \"unix\" not registered, fallback to default scheme" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.183865860+09:00" level=info msg="[graphdriver] using prior storage driver: overlay2"
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.184705891+09:00" level=info msg="ccResolverWrapper: sending new addresses to cc: [{unix:///run/containerd/containerd.sock 0  <nil>}]" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.184734395+09:00" level=info msg="ClientConn switching balancer to \"pick_first\"" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.184783244+09:00" level=info msg="pickfirstBalancer: HandleSubConnStateChange: 0xc420612910, CONNECTING" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.184977899+09:00" level=info msg="pickfirstBalancer: HandleSubConnStateChange: 0xc420612910, READY" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.185005436+09:00" level=info msg="ccResolverWrapper: sending new addresses to cc: [{unix:///run/containerd/containerd.sock 0  <nil>}]" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.185016987+09:00" level=info msg="ClientConn switching balancer to \"pick_first\"" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.185046636+09:00" level=info msg="pickfirstBalancer: HandleSubConnStateChange: 0xc420612be0, CONNECTING" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.185160010+09:00" level=info msg="pickfirstBalancer: HandleSubConnStateChange: 0xc420612be0, READY" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.188935526+09:00" level=info msg="Graph migration to content-addressability took 0.00 seconds"
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.189454109+09:00" level=info msg="Loading containers: start."
12月 20 16:09:52 example dockerd[26871]: Error starting daemon: Error initializing network controller: list bridge addresses failed: no available network
12月 20 16:09:52 example systemd[1]: docker.service: main process exited, code=exited, status=1/FAILURE
12月 20 16:09:52 example systemd[1]: Failed to start Docker Application Container Engine.
-- Subject: Unit docker.service has failed
-- Defined-By: systemd
-- Support: http://lists.freedesktop.org/mailman/listinfo/systemd-devel
--
-- Unit docker.service has failed.
--
-- The result is failed.

以上はjournalctlのログ

Error starting daemon: Error initializing network controller: list bridge addresses failed: no available network

ほう

$ ip l
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
    link/ether 00:50:56:b9:26:14 brd ff:ff:ff:ff:ff:ff
3: ens224: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
    link/ether 00:50:56:b9:76:49 brd ff:ff:ff:ff:ff:ff

なんでdocker0が生成されないんだ…

何故

追記(2018/12/24)

ちゃんと見てなかったのでとても恥ずかしいんですが、上記ルーティングの ens192 というやつがこういうroutingをしていた

$ ip r
172.16.0.0/12 via 10.26.149.254 dev ens192  proto static  metric 100

docker0 が作りたいルーティングは以下の通りで、ホストアドレス部分が被ってしまうためdockerインストール時にルーティングが作られないようだった

172.17.0.0/16 dev docker0  proto kernel  scope link  src 172.17.0.1

また、このままホストアドレス部分が被っているとまずいので被っているルーティングについて解決する必要がある

今回は ens192 側のサブネットマスクを大きくして必要な部分だけルーティングで追加していくのが手っ取り早そうだったので ens192 側を解決させた(ここでは省略するけども)

追記ここまで(2018/12/24)

作られなかったブリッジを作る

というわけで本来作ってくれるはずだった仮想ブリッジの docker0 を作ってあげます

$ sudo ip link add name docker0 type bridge
$ sudo ip addr add dev docker0 172.17.0.1/16
$ sudo systemctl start docker.service
$ sudo systemctl status docker.service
● docker.service - Docker Application Container Engine
   Loaded: loaded (/usr/lib/systemd/system/docker.service; disabled; vendor preset: disabled)
   Active: active (running) since 木 2018-12-20 16:32:23 JST; 3min 27s ago

docker0の使用するip帯を変えるには

追記(2018/12/24)

もしdocker側を変えたい場合はdocker daemon起動時の引数に --bip=他のアドレス を指定すると良いです

$ sudo systemctl stop docker
$ sudo ip link set dev docker0 down
$ sudo ip link del dev docker0
$ ip r
default via 10.0.2.2 dev enp0s3  proto static  metric 100
10.0.2.0/24 dev enp0s3  proto kernel  scope link  src 10.0.2.15  metric 100
172.18.0.0/16 dev br-da27a92cc9df  proto kernel  scope link  src 172.18.0.1
192.168.100.0/24 dev enp0s8  proto kernel  scope link  src 192.168.100.20  metric 100
// docker0 がないことを確認する

$ cat /usr/lib/systemd/system/docker.service
~~省略~~
[Service]
~~省略~~
+EnvironmentFile=/etc/sysconfig/docker
-ExecStart=/usr/bin/dockerd -H unix://
+ExecStart=/usr/bin/dockerd -H unix:// $OPTIONS
~~省略~~

$ cat /etc/sysconfig/docker
+OPTIONS="--bip=10.17.0.1/16"

$ sudo systemctl daemon-reload
$ sudo systemctl start docker
$ ip r
default via 10.0.2.2 dev enp0s3  proto static  metric 100
10.0.2.0/24 dev enp0s3  proto kernel  scope link  src 10.0.2.15  metric 100
172.18.0.0/16 dev br-da27a92cc9df  proto kernel  scope link  src 172.18.0.1
10.17.0.0/16 dev docker0  proto kernel  scope link  src 10.17.0.1
192.168.100.0/24 dev enp0s8  proto kernel  scope link  src 192.168.100.20  metric 100
// docker0のdestがbipで指定したやつになっている💯

追記ここまで(2018/12/24)

さいごに

おわりです

参考

github.com

qiita.com

Railsで動くRspecのテストが遅いのでparallel_testsを導入してみる

--- ここからテンプレ --- 仕事で関わっているRailsアプリではPRの最新コミットのごとにCIが周る。
CIはcloneしたRailsアプリに対してRspecを実行し、その結果がリポジトリに反映される。

という、ごく普通のフローが存在するが、リポジトリが成長すると共にテストもそれなりに数を増やし、共にRspecの実行時間もそれなりに伸び、つまりCIの実行時間も伸びていく。

CIの結果を頼りにレビューをする方も居るだろうし、CIの結果がわからないとマージはできないように、CIの結果を待たなければいけないケースはそれなりに発生すると思っている。

そういった待ち時間は時間の程度にも寄るが、他の作業の妨げになるかもしれない。他の作業の妨げになるのであれば、待ち時間は短ければ短いほど良く、つまりテストの実行時間は早ければ早いほど良いということだろう。

--- ここまでテンプレ ---

ただあまり大きな労力はかけたくない(めんどくさい)ので、まずは大きく有効そうな手立てから試していきたい。

parallel_tests とは

github.com

簡単に言えば

  • CPUのコア毎にRspecのプロセスを立ち上げてテストを並列実行する

というもの

癖はあるようで、

  • プロセスごとに独立したDBが必要(rails db:setup のように複数のDBを作るためのrakeタスクは存在している)
  • 実行時にファイルサイズを鑑みていて、各プロセスに渡るspecファイルの合計ファイルサイズが近くなるように振り分けられる
    • 小さなファイルサイズでも激重なテストケースが入っている場合は考慮されない

ということらしいが、導入はそんなに苦なものではないようなので一旦導入してみた

導入方法

Gemfileに以下を追加してbundle install

# Gemfile
gem 'parallel_tests', group: %i[development test]
$ bundle install

test用のdatabaseを作る

# config/database.yml
test:
  database: yourproject_test<%= ENV['TEST_ENV_NUMBER'] %>
# 環境変数 `TEST_ENV_NUMBER` に実行されるプロセスの番号が入ってくるらしい
$ rails parallel:setup

これでコア数に応じて

  • yourproject_test
  • yourproject_test2
  • yourproject_test3
  • yourproject_test4

みたいに4つのdatabaseができるはず

おそらくお手持ちのRailsアプリケーションはmemcachedやRedisなどを噛んでいると思うが、その場合は先程書いた環境変数 TEST_ENV_NUMBER を使ってそれぞれのネームスペースを切ると良さそう(以下参照2つ)

# config/environments/test.rb
config.cache_store = :memory_store, { namespace: "yourproject_test#{ENV['TEST_ENV_NUMBER']}" }
# mastodonの例
# spec/rails_helper.rb
Redis.current = Redis::Namespace.new("mastodon_test#{ENV['TEST_ENV_NUMBER']}", redis: Redis.current)

あと何故かcache_storeがfile_store(未指定時デフォ)の場合、テストが途中で死ぬ(下記エラー)ことがあった
多分 Rails.cache.clear! で別プロセスが参照するキャッシュが消えたんだと思う、ここをうまく切るためにnamespaceを儲けようとしたが結局うまく行かなかったので一旦はmemory_storeを使うように変更している

Errno::ENOENT:
     #   No such file or directory @ apply2files - /Users/naari3/src/github.com/naari3/some_project/tmp/cache/FB1/000/.permissions_check.70365749710360.44558.326263
     #   ./app/models/user.rb:25:in `info

実行

実行する

$ rails parallel:spec
4 processes for 224 specs, ~ 56 specs per process

Randomized with seed 58546
....
Randomized with seed 39588
..
Randomized with seed 40142
......................
Randomized with seed 39700
........
(省略)
Coverage report generated for (1/4), (2/4), (3/4), (4/4), RSpec to /Users/naari3/src/github.com/naari3/some_project/coverage. 4352 / 5499 LOC (79.14%) covered.

1707 examples, 0 failures, 1 pending

Took 171 seconds (2:41)

このマシンには4コア積まれているので4プロセス立ち上がってそれぞれ実行された

なお普通にRspecを実行した際のログはこちら

$ bundle exec rspec
(省略)
Finished in 6 minutes 35 seconds (files took 5.08 seconds to load)
1707 examples, 0 failures, 1 pending

開発マシンで裏で色々立ち上がっている中、parallel_tests経由で並行に実行するだけで 1/2 くらいの実行時間にすることができた

実際はおそらくエージェントとかDockerプロセスとか最低限のものだけが立ち上がっているCIの環境上で動かされるのでコア数分だけ数倍速になるんだろうなぁと思っている(まだ動かしてない)