tnakata's blog

Webエンジニア(2017/01/01~)。プログラミングやWebについて語る。学生時代は航空宇宙工学を専攻してた。

ハイパフォーマンスブラウザネットワーキングを読んでkeep-alive接続を試した

ハイパフォーマンスブラウザネットワーキングを読んで、keep-alive接続を試したくなった。

ハイパフォーマンスブラウザネットワーキングを読んだ - tnakata's blog

keep-alive接続とは、複数のHTTP通信でTCP接続を最初の1回目だけ行い、2回目以降は最初の接続を使いまわすこと。TCP接続の時点でクライアントサーバ間で通信が発生するため、この通信で発生するレイテンシを削減できパフォーマンス改善に有効である。HTTP1.1でConnectionヘッダが追加されたのに伴い利用できるようになった。詳細はハイパフォーマンスブラウザネットワーキングを読むとよくわかる。

自分で作ったtwitterで特定のユーザーのツイートから画像を引っこ抜くツールを使用する。画像のURIをツイートから取得して、URIを元に画像を取得する処理となっている。今回は520件ほど画像を取得するので、(そのままだが)520回HTTP通信することになる。この520回通信するところにkeep-alive通信を適用する。

GitHub - takayukinakata/tiny_twitter: You can get tweet from CLI

以下のパターンを試す。

  • Not keep-alive接続
    • 実装はRubyのopen-uriライブラリを使用する
    • 一回の通信が終わると接続をクローズする
  • keep-alive接続
    • 実装はRubyのnet/httpライブラリを使用する
    • 複数の通信で接続を使いまわすことができる
# Not keep-alive通信
[tiny_twitter tnakata]$ ./bin/tiny_twitter -u mogatanpe -j
I, [2017-07-08T17:17:06.721482 #4986]  INFO -- : Start fetching tweets
I, [2017-07-08T17:17:09.911793 #4986]  INFO -- : Finish fetching tweets
I, [2017-07-08T17:17:09.911855 #4986]  INFO -- : Start downloading images
--------------------------------------------------
                      user     system      total        real
downloading       2.470000   2.280000   4.750000 ( 36.749917)
--------------------------------------------------
I, [2017-07-08T17:17:46.661400 #4986]  INFO -- : Finish downloading images
I, [2017-07-08T17:17:46.661456 #4986]  INFO -- : 520 images downloded!

[tiny_twitter tnakata]$ ./bin/tiny_twitter -u mogatanpe -j
I, [2017-07-08T17:18:03.209045 #5006]  INFO -- : Start fetching tweets
I, [2017-07-08T17:18:06.387328 #5006]  INFO -- : Finish fetching tweets
I, [2017-07-08T17:18:06.387383 #5006]  INFO -- : Start downloading images
--------------------------------------------------
                      user     system      total        real
downloading       2.510000   2.310000   4.820000 ( 35.784499)
--------------------------------------------------
I, [2017-07-08T17:18:42.171433 #5006]  INFO -- : Finish downloading images
I, [2017-07-08T17:18:42.171506 #5006]  INFO -- : 520 images downloded!



# keep-alive接続
[tiny_twitter tnakata]$ ./bin/tiny_twitter -u mogatanpe -j
I, [2017-07-08T17:21:11.115537 #5109]  INFO -- : Start fetching tweets
I, [2017-07-08T17:21:14.530462 #5109]  INFO -- : Finish fetching tweets
I, [2017-07-08T17:21:14.530673 #5109]  INFO -- : Start downloading images
--------------------------------------------------
                      user     system      total        real
downloading       1.590000   1.290000   2.880000 ( 24.081778)
--------------------------------------------------
I, [2017-07-08T17:21:38.612308 #5109]  INFO -- : Finish downloading images
I, [2017-07-08T17:21:38.612355 #5109]  INFO -- : 520 images downloded!

[tiny_twitter tnakata]$ ./bin/tiny_twitter -u mogatanpe -j
I, [2017-07-08T17:21:48.847077 #5128]  INFO -- : Start fetching tweets
I, [2017-07-08T17:21:52.485432 #5128]  INFO -- : Finish fetching tweets
I, [2017-07-08T17:21:52.485492 #5128]  INFO -- : Start downloading images
--------------------------------------------------
                      user     system      total        real
downloading       1.580000   1.300000   2.880000 ( 23.637650)
--------------------------------------------------
I, [2017-07-08T17:22:16.122914 #5128]  INFO -- : Finish downloading images
I, [2017-07-08T17:22:16.122978 #5128]  INFO -- : 520 images downloded!

keep-alive接続だと10秒ほど早くなっていることがわかる。10(s) / 520(回) = 0.019…(s/回) ≒ 20(ms/回) となるので、1回あたりの通信で20msほど速度改善できている。(使っているライブラリが違うので、keep-alive接続以外の面でも速度改善されている可能性はあるが。とはいえ、一応、net/httpライブラリで毎回接続をクローズするように実装してテストしてみたら遅くなったので、keep-alive接続が速度改善に貢献していることは確かなのかなと個人的には納得した。)

# レスポンスヘッダ
# keep-alive接続
access-control-allow-origin : *
access-control-expose-headers : Content-Length
cache-control : max-age=604800, must-revalidate
content-md5 : GZltGvEI5YV91a3h/IdlbQ==
last-modified : Sat, 08 Jul 2017 03:38:33 GMT
x-connection-hash : 63ff4efa83f9d723ac35324e9414065d
x-response-time : 112
content-type : image/jpeg
content-length : 47570
accept-ranges : bytes
date : Sat, 08 Jul 2017 08:26:07 GMT
via : 1.1 varnish
age : 17132
connection : keep-alive
x-served-by : cache-tw-tyo12434-TWTYO3
x-cache : HIT
x-timer : S1499502368.676755,VS0,VE0
expires : Sun, 23 Jul 2017 08:26:07 GMT
x-content-type-options : nosniff

connection : keep-aliveがレスポンスヘッダで返ってきている。

# デバッグ
# keep-alive接続
Conn keep-alive
<- "GET /media/CUQ5rsKUwAA457Q.jpg HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: pbs.twimg.com\r\n\r\n"
-> "HTTP/1.1 200 OK\r\n"
-> "access-control-allow-origin: *\r\n"
-> "access-control-expose-headers: Content-Length\r\n"
-> "cache-control: max-age=604800, must-revalidate\r\n"
-> "content-md5: Cg9YcoePzQIF0j3UMDL8lg==\r\n"
-> "last-modified: Fri, 20 Nov 2015 15:10:01 GMT\r\n"
-> "x-connection-hash: 64e4cd01cadca36b82d3e1a9decd1234\r\n"
-> "x-response-time: 361\r\n"
-> "Content-Type: image/jpeg\r\n"
-> "Content-Length: 89805\r\n"
-> "Accept-Ranges: bytes\r\n"
-> "Date: Sat, 08 Jul 2017 08:30:22 GMT\r\n"
-> "Via: 1.1 varnish\r\n"
-> "Age: 1087\r\n"
-> "Connection: keep-alive\r\n"
-> "X-Served-By: cache-tw-tyo12427-TWTYO3\r\n"
-> "X-Cache: HIT\r\n"
-> "X-Timer: S1499502623.540782,VS0,VE1\r\n"
-> "Expires: Sun, 23 Jul 2017 08:30:22 GMT\r\n"
-> "X-Content-Type-Options: nosniff\r\n"
-> "\r\n"
reading 89805 bytes...

デバッグで再接続するような記述が出ていなかったので、正しくkeep-alive接続できていることが確認できた。

補足

サーバ側で、最初の接続から一定時間の間しかkeep-alive接続を認めない、という設定ができる。例えば、nginxだと、最初の接続からkeepalive_timeoutに設定した時間(s)を経過した後に再度接続しようとすると、もう一度TCPコネクションを張り直す必要が出てくる。ずっと接続を維持し続けると、その分のメモリを確保したままになってしまうからだと思う。

感想

  • HTTPのヘッダを「読む + 読める」ようになった
  • ブラックボックス気味だったHTTP通信ライブラリ(open-uri, net/http)の使い分けができるようになった
  • 低レイヤーの学習をアプリに生かすことができた

ハイパフォーマンスブラウザネットワーキングを読んだ

https://www.amazon.co.jp/ハイパフォーマンス-ブラウザネットワーキング-―ネットワークアプリケーションのためのパフォーマンス最適化-Ilya-Grigorik/dp/4873116767

TCP, TLS, HTTPといったアプリケーションよりも低いレイヤーについて基本的な構造を説明し、パフォーマンスの観点からエンジニアはどう関わるべきかが書かれている。文章は読みやすかった。読み飛ばした部分もあるが10日間ほどで読み終わった。380ページ。

以下、自分用メモ。

1章 レイテンシ・帯域幅入門

  • ネットワークのパフォーマンスを決める2つの要素がある
  • レイテンシは要するにパケットを送信してから受信するまでの時間。光ファイバーを通る時間やルータの処理時間などの計。
  • 帯域幅は要するに速度(bps)。
  • 光ファイバーを通る時間(propagation delay)は光の速さが29km/sであり、限界が決まっている。
  • 感覚的に数百ミリ秒遅れると人間は遅れを認識する。
  • ラストワンマイルのレイテンシ。要するにパケットが太平洋を渡っても、最後に複数のプロバイダを経由することでレイテンシが発生してしまうこと。
  • ほとんどの場合、帯域幅ではなくレイテンシがパフォーマンスのボトルネック

2章 TCPの構成要素

  • TCP接続で3ウェイハンドシェイクから始まる。ここでもクライアントがデータを送信し始めるまでに1往復分のレイテンシが発生している。
  • フロー制御。データの送信サイズがウィンドウ値で制限されてしまう。データ受信側が処理しきれないデータ量を送信側が送信してしまうのを防ぐための仕組み。
  • スロースタート。データの送信サイズは小さい値から始まって、1往復ごとに送信サイズが大きくなっていく。ネットワーク自体の許容量を超えないようにするための仕組み。
  • 上記のようにデータサイズが制限されることにより発生するレイテンシ(データを余計に送信するためデータの往復が発生するなど)がボトルネックであり、帯域幅はそんなに関係ない。
  • ウィンドウ値を大きくしたり、スロースタートリスタートを無効にしたりするとパフォーマンスにはいいらしい。
  • そもそもとして、CDNを利用してデータを近くでやり取りするようにするといい

3章 UDPの構成要素

  • 受信側に確実に届けるための仕組みがない。このため信頼性は低い。
  • その分、余計なデータの往復などが発生しないため、レイテンシの発生が少ない。
  • NATの話が出てきた。ここでも処理時間(レイテンシ)が発生する
  • 疲れてたので読み飛ばしながら進んだ。

4章 TLS

  • HTTPSのSのやつ。暗号化、認証、データ整合性を提供する。要するに、認証局を利用して、クライアントから送信されたデータが本当にクライアントから送られてきたものかどうかを保証する。逆も同じ。
  • TLSハンドシェイクから通信を始める。最大2往復。
  • 1往復目で使用するTLSのバージョンや多分だけど利用できる認証局をクライアントが送信して、サーバがバージョンや認証局はこれを使います、っていうレスポンスを返す。
  • 2往復目で暗号化のために共通鍵のやり取りを行う。
  • TCPハンドシェイクと合わせると、最大で3往復分のレイテンシが発生する。暗号化の計算によるレイテンシは大したことないらしいので、ここがボトルネックとなる。
  • パケットはすでに光速で近い速度なためここは大きなレイテンシ改善は見込めないが、CDNを利用することで距離を近くすることができる。つまり、CDNTCPTLSハンドシェイクを行わせることでレイテンシ改善が見込める。
  • これが動的コンテンツのやり取りにおいて、キャッシュ機構は使用しないのにCDNを利用する理由。

5章 ワイヤレスネットワーク入門

  • パフォーマンスは周波数帯域幅とシグナル強度に依存する

6章 WiFi

7章 モバイルネットワーク

  • 3G,4G,LTEの規格について
  • RRCは物理層での接続を管理する。TCPハンドシェイクが行われる前段階。ここでもレイテンシが発生する。
  • 読み飛ばした

8章 モバイルネットワークの最適化

  • 読み飛ばした

9章 HTTPの歴史

  • 0.9 => 1.0 => 1.1 => 2.0
  • 0.9のリクエストはGETメソッドとパスの1行で構成される。めちゃシンプル
  • 1.0でキャッシュ(Expires)がレスポンスで返るようになった
  • 1.1でKeep-Alive接続が可能になった
  • 2.0はパフォーマンス向上が主なテーマ

10章 Webパフォーマンス入門

  • JavaScriptの実行はCSSにブロックされる可能性がある。
  • パフォーマンスを計測できるツールを使う、そして計測する。
  • ウォーターフォールチャートがパフォーマンスを計測する上で見やすい
  • ブラウザがやっているパフォーマンス最適化

11章 HTTP 1.x

  • キープアライブ接続がとにかく重要!
    • TCPTLSハンドシェイクを省略できるのでレイテンシ削減に繋がる
  • クライアントは一度に一つのリクエストしか送れない、サーバは一度に一つのリクエストしか処理できない、といったように並列処理ができないという課題がある。HTTPのHoLブロッキング(TCPのHoLブロッキングとは別)。
  • ここを解決できればパフォーマンス改善に繋がる。これをやりたいのがHTTP2.0。

12章 HTTP 2.0

  • パフォーマンス向上に主眼が置かれている
  • バイナリフレーミングレイヤー(?)。これでクライアントから複数のリクエストを一度に送信できるようになるらしい
  • サーバからプッシュで画像などを配信できるようになるらしい

13章 アプリケーション配信最適化

  • 定番のパフォーマンスベストプラクティス
    • DNSルックアップを減らす(キープアライブ接続)
    • TCP接続の再利用(キープアライブ接続)
    • HTTPリダイレクトを減らす(新たにDNSルックアップやTCP接続が必要になるため)
    • 不要なリソースはそもそも配信しない
    • リソースのキャッシュ

14章 ブラウザネットワーク入門

  • 印象に残っていない

15章 XMLHttpRequest

  • ajaxで使うやつ
  • CORSを気にしている

16章 Sever-Sent Events

  • 読み飛ばした
  • サーバ => ブラウザ

17章 WebSocket

  • 読み飛ばした
  • サーバ <=> ブラウザ

18章 WebRTC

  • 読み飛ばした
  • リアルタイム通信で使用する(Web会議など)

macでipコマンドを使った

自分のipアドレスを確認してみたくなった。ifconfigコマンドはipアドレスが付与されていないインターフェースも出てきて見づらかったし(少なくとも初めて使う自分には)、どうやら現在は非推奨になっているらしいので、ipコマンドを入れることにした。

ifconfigコマンドとipコマンドの違いと比較 - エラーの向こうへ

インストール方法

以下のコマンドでipコマンドを使えるようになる。以下のリンクを参考にした。

Macにipコマンドをインストールする

$ brew tap brona/iproute2mac
$ brew install iproute2mac

$ ip
Usage: ip [ OPTIONS ] OBJECT { COMMAND | help }
       ip -V
where  OBJECT := { link | addr | route | neigh }
       OPTIONS := { -4 | -6 }

iproute2mac
Homepage: https://github.com/brona/iproute2mac
This is CLI wrapper for basic network utilities on Mac OS X inspired with iproute2 on Linux systems.
Provided functionality is limited and command output is not fully compatible with iproute2.
For advanced usage use netstat, ifconfig, ndp, arp, route and networksetup directly.

ipアドレスの確認

ip addrコマンドで確認できる。まだ見方がよくわからないのでxxxで隠す。

$ ip addr
lo0: flags=xxx
    inet6 xxx
    inet 127.0.0.1/8 lo0
    inet6 xxx
en1: flags=zzz
    ether xxx
    inet6 xxx
    inet xxx
awdl0: flags=xxx
    ether xxx
    inet6 xxx

lo0ローカルアドレスen1Wi-Fi接続。

Webエンジニアに転職して半年が経った

Webエンジニアに転職して半年が経った。

仕事にも慣れてきたのでこれまでとこれからについて整理しようと思う。

転職前

企業向けのパッケージ開発をしていた。Web経由で使用してもらうものではなく、お客さんのとこに作ったサーバにパッケージをインストールして使ってもらう形態だった。

いわゆるオレオレフレームワークJavaを使って、ビジネスロジックをひたすら書いてた。ちなみにDBはOracle

人間関係とか仕事の出来とかは問題なかったけど、やはりオレオレフレームワークJavaでは何か物足りなかった。オレオレフレームワークの上だと、クラス設計すら何も考えなくていいのでただif文書いてメソッド呼び出して、みたいなことしか書かなくてよかった。

そんな中、インターネットを覗くと、Webエンジニアたちが自分の知らない言葉でしゃべってた。gitとは、デザインパターンとは、HTTPとは、他にもたくさん。まるで話についていけない。自分自身とインターネット上のWebエンジニアを比較して、技術的なことが身についていないことが不安で仕方がなかった。もちろん、そもそも非WebとWebを単純に比較してどうなのとは今は思うが。

というわけで、将来的にエンジニアとしての技術力が足りなくて会社の外では食っていけないと思った。一番大事なのは社内ではなく社外(市場)での評価だと考えていたので、転職することを決意した。

ちなみに、技術的には物足りなかったが、技術以前に問題解決に対する考え方に関しては徹底的に教えてもらっていて、これは本当によかったと今感じている。直属だった先輩、上司にはかなりよくしてもらっていたのでありがたかった。

転職後〜今

新しい会社ではRubyRailsを使ってサイトを作っている。同じフレームワークでもRailsだとググったら情報がたくさん出てくるし、ソースも読めるし、勉強しがいがあって楽しく過ごせている。

自分たちで運用しているので、アプリケーションを動かすサーバや下のレイヤーであるHTTP, TCP/IPなどについても知っておく必要があり、正直いくら勉強してもなかなか追いつけないでいる。それでもやはり勉強しがいがあるというのは楽しいものであると思った。

入社したての時はWebど素人であり、HTMLも若干怪しいくらいだったが、やっていけば案外なんとかなった。前職で学んだ仕事のやり方のおかげ。頭の中で考えているリスクはほとんどが必要以上にリスクとして認識されている、って話はよく聞くが本当にそうだった。そういえば、前職の先輩に「具体的にここがヤバい、って言えなければそれは大体ヤバくない。必要以上に頭がリスクとして認識しているだけ。」と言われたことがあったのを思い出した。

スキルセット

今現在、自分がわかる範囲はこんな感じだろうか。どれも雰囲気使えるレベルではあるが。

こう書くとなかなか不安になるからたまに列挙するのはいいかも。

今後

次の半年だが、大きく2つのテーマで勉強する。キーワードはRubyと低レイヤー。

一つはRubyRubyでできることを増やす。

基本的な文法はわかったので手を動かしてとにかく書く。正直、仕事で使っているからRubyなのだが、一つの言語で大体のことできるようになると他の言語の習得も容易になるという話をよく聞くので、最初はあえてこだわってRubyを書いていく。

もう一つは低レイヤー。HTTP、TCP/IPなどなどネットワークまわりを中心に。

流行り廃りのない知識がないと将来的に死ぬ気がする。今後死にたくないので勉強する。

md2keyを使った

社内LTでスライド作るのにmd2keyを使った。便利。

github.com

k0kubun.hatenablog.com

太字や取り消し線はまだ使えないらしい

Rich texts · Issue #11 · k0kubun/md2key · GitHub

とのこと。

調べてみた

実装できないかなと思って、applescriptで太字や取り消し線を実現できないか調べてみた。keynote見る限り、文字のスタイルにボールドはあるから案外すぐ見つかるんじゃないかと思った。

結果としては見つけられなかった。リッチテキストのプロパティがcolor, font, sizeしかなかったのでそもそもできないんじゃないか(styleみたいなのがあると期待してたがなかった)。ちなみに色やフォントはうまく指定できた。

rich text n, pl rich text : This provides the base rich text class for all iWork applications.
elements
contains characters, paragraphs, words.
properties
color (color) : The color of the font. Expressed as an RGB value consisting of a list of three color values from 0 to 65535. ex: Blue = {0, 0, 65535}.
font (text) : The name of the font. Can be the PostScript name, such as: “TimesNewRomanPS-ItalicMT”, or display name: “Times New Roman Italic”. TIP: Use the Font Book application get the information about a typeface.
size (integer) : The size of the font.

applescriptをあんまり深追いしたくないのでこの辺でやめる。

Rustのスタックとヒープについて書いてみたがまだよくわからない

変数を宣言して値を代入するということは、メモリ上のアドレスに値を設定するということらしいが、今までこの辺りのことを全く意識せずにプログラミングしてきたのでイメージがわかない。普段Rubyを書いているのでなおさら。

ふと静的言語見てみようと思ってRustのドキュメントを読んでたらちょうどメモリ管理の話が出てきたので、自分の理解をまとめてみる。

スタックとヒープ

スタック

  • Rustではintなどを使用したときにメモリが確保される場所
  • 処理が高速
  • サイズに制限がある
  • ブロックを抜けるとメモリが解放される

ヒープ

  • Rustでは、Box 型を使うことでヒープ上にメモリが確保される
  • 処理が遅い
  • 事実上サイズに制限がない

具体例

  • fn main() {}は省略する。

let x = 5;
address variable value
01 x 5

let x = 5;
let y = 10;

|address|variable|value| |:–|:–|:–| |01|x|5| |02|y|10| ``


let x = Box::new(5);
let y = 42;
address variable value
01 x ->(last)-1
02 y 42
* * *
(last)-1 5

感想

  • ざっくりだが変数を定義して値を代入したときに、メモリ上のアドレスを意識できた
  • とはいえ、だから何?、って思いがある。これがどう役に立つのかがまだわかってないからだし、理解がふわふわしている
  • 「メモリを意識してプログラミングする」というのがわからない状態で、問題が大きすぎる気がする。問題を自分が理解できるくらいまで分解する必要がある気がする

dotfiles を github で管理した

dotfilesとはドットから始まる設定ファイルのこと。.bashrcや.gitconfigなど。

自宅macと仕事macの2台使ってるんだけど、linuxコマンドやgitコマンドのaliasを自宅用と仕事用で同じにしたかった。いちいち設定ファイルを人力でコピペして同期させるのも面倒なのでコマンドで簡単に実現できる方法を探した。

調べてみると、githubでファイル管理 + シンボリックリンクを作成するシェルスクリプトを用意、している人が多かったので真似した。

dotfilesを管理しよう - Qiita

dotfilesをGitHubで管理 - Qiita

dotfilesをgithubで管理し始めた - すぎゃーんメモ

githubで公開している人も多かったので参考にした。

GitHub - yuroyoro/dotfiles: dotfiles

GitHub - sugyan/dotfiles: my dotfiles configuration

自分用のを作った。自分のはaliasくらいしか設定してないので、他の人の設定をどんどんパクっていきたい。

github.com

Working With Unix Processes を読んだ

Working With Unix Processes (English Edition)

Working With Unix Processes (English Edition)

この前読んだ本と同じ作者が書いている。

Working with TCP Sockets を読んだ - tnakata's blog

こちらもサンプルソースRubyで書かれている。Ruby書いててプロセスのことまだ何もわかってない(言語化できてない)という人にはいいかもしれない。

読もうと思った理由

  • プロセスの概念を言語化したいから。

読んでて勉強になった部分

  • プロセスとはUnix上での処理単位。どんなプログラムもプロセスの中で実行される。
    • 例えば、irbを実行するとプロセスが一つ立ち上がる。psコマンドを実行することで立ち上がったプロセスの存在を確認できる。
  • Unixでは全てはFileとして扱われる。
    • test.rbといったファイルやソケットも全てファイル。RubyではIOクラスが該当する。
    • ターミナルでのコマンドで出てくる標準入力、標準出力、標準エラー出力も同様にファイルである。
  • プロセスはforkでコピーできる(子プロセスが作られる)。並列処理に関係するところ。
  • 子プロセスが終了したのに親プロセスがProcess.waitを実行しないと、子プロセスのメモリは解放されないままになる。Zombie Processという。
  • シグナルという概念がある。プロセス間、もしくはキーボードからプロセスへシグナルを送り、プロセスを終わらせることができる。
    • Ctrl+Cによるプロセスの強制終了やProcess.kill
    • ちょっと自信がないが、ソケットを利用したやり取りとは異なる概念。
  • プロセス間の通信で代表的なものはパイプとソケット。どちらもデータの流れはSTREAM(データの始まりと終わりの目印がない)。
  • STREAMの反対はMESSAGE。UNIXソケットで利用される。
  • Daemon Processesとはバックグラウンドで走っているプロセスのこと。
  • 全てのプロセスの親としてinitプロセスがある。
  • execrc(2)で現在のプロセスを他のプロセスに変えることができる。Rubyのexecはexecrc(2)のラッパー。なおプロセスを変えた後に元のプロセスに自動で戻ることはない。
  • Ruby のexecは元のプロセスのfile descriptorsを全て閉じる。メモリリークを防ぐため。
  • PreforkingはCopy on Writeによってメモリを効率的に使用できる。
    • CoWはコピーしたプロセスを使う(たしかacceptした)タイミングでメモリがコピーされるので効率的とのこと。

読んでてよくわからなかったところは以下の通り。

  • Zombie ProcessやDaemon Processが実際にどのようなときに使われるのか
  • 仕事でやってることとどう結びつければよいか

Working with TCP Sockets を読んだ

Working With TCP Sockets (English Edition)

Working With TCP Sockets (English Edition)

一通り読んだけど結構勉強になった。サンプルソースRubyで書かれているから、Rubyを使っていてWebサーバとかソケットとかのことについて基礎から勉強したいって人にはいいと思う。

読もうと思った動機

以下に書いたわからないことを言語化したいから

  • ネットワークプログラミングについて基礎的なことをまるで知らない。
  • Railsを使っているとWebrickとかPumaとかnginxとかいろんなWebサーバが出てくるけど違いがわからない。
  • そもそもWebサーバの仕組みを知らない。

勉強になった部分

  • serverやclientがどのようなlifecycleを送るのかについて
    • server : create, bind, listen, accept, close
    • client : create, bind, connect, close
  • RubyのSocketライブラリはsystem callを呼んでいること
  • Befferがいい感じにデータ量を調整してくれること
  • forkやthreadを利用することで並列接続を実現していること
  • WebrickやPumaやnginxのような様々なサーバが存在するけど、違いは並列接続を実現させる方法だということ(ざっくり)

逆に理解できなかったところ

  • non-blocking IO が理解できた気でいるけど自信ない
  • Preforkingがどうして効率的なのか
  • Eventedは何が何だか、Hybridも

'rails -v'コマンドは何をしているのか

rails -v'コマンドは何をやっているのか

rails -vコマンドがどのような仕組みになっているか気になったので調べてみる。RailsのバージョンはRails 5.0.1

which railsコマンドで実行ファイルの場所を取得する。

$ which rails
~/.rbenv/shims/rails

rbenvを使用している場合には上記が実行ファイルとなるが、上記のファイルはただのラッパーのようである。ここでは詳細は追わないが、以下のリンクが後で理解するときに参考になりそう。

memo.sugyan.com

実際の実行ファイルをrbenv which railsコマンドで取得する。

$ rbenv which rails
~/.rbenv/versions/2.3.1/bin/rails

~/.rbenv/versions/2.3.1/bin/railsの中身を見ると、

(省略)

gem 'railties', version
load Gem.bin_path('railties', 'rails', version)

#[1] pry(main)> Gem.bin_path('railties', 'rails')
#=> "~/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/railties-5.0.1/exe/rails"

となっており、railtiesというgemのrailsという実行ファイルを実行している。 /exe/railsを見てみる。

#!/usr/bin/env ruby

git_path = File.expand_path('../../../.git', __FILE__)

if File.exist?(git_path)
  railties_path = File.expand_path('../../lib', __FILE__)
  $:.unshift(railties_path)
end
require "rails/cli"

上位のフォルダに.gitがあれば、自分自身のlibが探索されるように探索パス($:)にパスを追加している。 そしてrequire "rails/cli"cli.rbを実行している。/lib/rails/cli.rbが読み込まれるはずだが、念のためにrequire "rails/cli"の上の行にputs $:を差し込んで~/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/railties-5.0.1/libが含まれているかどうか確認した。ちゃんとあったので多分これが読み込まれているはず。

/lib/rails/cli.rbを見てみる。

require 'rails/app_loader'

# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
Rails::AppLoader.exec_app

require 'rails/ruby_version_check'
Signal.trap("INT") { puts; exit(1) }

if ARGV.first == 'plugin'
  ARGV.shift
  require 'rails/commands/plugin'
else
  require 'rails/commands/application'
end

Rails::AppLoader.exec_appを詳しく見るため、/lib/rails/app_loader.rbを見る。

require 'pathname'
require 'rails/version'

module Rails
  module AppLoader # :nodoc:
    extend self

    RUBY = Gem.ruby
    EXECUTABLES = ['bin/rails', 'script/rails']

(省略)

    def exec_app
      original_cwd = Dir.pwd

      loop do
        if exe = find_executable
          contents = File.read(exe)

          if contents =~ /(APP|ENGINE)_PATH/
            exec RUBY, exe, *ARGV
            break # non reachable, hack to be able to stub exec in the test suite
          elsif exe.end_with?('bin/rails') && contents.include?('This file was generated by Bundler')
            $stderr.puts(BUNDLER_WARNING)
            Object.const_set(:APP_PATH, File.expand_path('config/application', Dir.pwd))
            require File.expand_path('../boot', APP_PATH)
            require 'rails/commands'
            break
          end
        end

        # If we exhaust the search there is no executable, this could be a
        # call to generate a new application, so restore the original cwd.
        Dir.chdir(original_cwd) and return if Pathname.new(Dir.pwd).root?

        # Otherwise keep moving upwards in search of an executable.
        Dir.chdir('..')
      end
    end

    def find_executable
      EXECUTABLES.find { |exe| File.file?(exe) }
    end
(省略)

if exe = find_executableは現在自分がRails::Rootにいるならtrue、そうでないならfalseを返す。

カレントディレクトリがRails::Root以外

ここでは、一旦homeディレクトリにいるとして続きを見ていく。

cli.rbに戻ると、今回のコマンドの引数は-vなのでrequire 'rails/commands/application'が実行されることがわかる。 /lib/rails/commands/application.rbでは、args = Rails::Generators::ARGVScrubber.new(ARGV).prepare!が実行されている。

require 'rails/generators'
require 'rails/generators/rails/app/app_generator'

module Rails
  module Generators
    class AppGenerator # :nodoc:
      # We want to exit on failure to be kind to other libraries
      # This is only when accessing via CLI
      def self.exit_on_failure?
        true
      end
    end
  end
end

args = Rails::Generators::ARGVScrubber.new(ARGV).prepare!
Rails::Generators::AppGenerator.start args

Rails::Generators::ARGVScrubber.new(ARGV).prepare!を詳しく見るために、/lib/rails/generators/rails/app/app_generator.rbを見る。

    # This class handles preparation of the arguments before the AppGenerator is
    # called. The class provides version or help information if they were
    # requested, and also constructs the railsrc file (used for extra configuration
    # options).
    #
    # This class should be called before the AppGenerator is required and started
    # since it configures and mutates ARGV correctly.
    class ARGVScrubber # :nodoc:
      def initialize(argv = ARGV)
        @argv = argv
      end

      def prepare!
        handle_version_request!(@argv.first)
        handle_invalid_command!(@argv.first, @argv) do
          handle_rails_rc!(@argv.drop(1))
        end
      end

(中略)

      private

        def handle_version_request!(argument)
          if ['--version', '-v'].include?(argument)
            require 'rails/version'
            puts "Rails #{Rails::VERSION::STRING}"
            exit(0)
          end
        end

prepare!メソッド内で呼び出しているhandle_version_request!Railsのバージョンを表示していることがわかった。

これでRails::Root以外の場所からrails -vコマンドを打った時の流れがわかった。

カレントディレクトリがRails::Root

次にRails::Rootにいる場合を見ていきます。Rails::AppLoader.exec_appを再び見ていくと、bin/railsをreadしている。

#!/usr/bin/env ruby
APP_PATH = File.expand_path('../../config/application', __FILE__)
require_relative '../config/boot'
require 'rails/commands'

APP_PATHという文字列が含まれているので、Rails::AppLoader.exec_app

          if contents =~ /(APP|ENGINE)_PATH/
            exec RUBY, exe, *ARGV

により、bin/railsRubyスクリプトとして実行される。bin/rails../config/bootrails/commandsを実行する。

# ../config/boot

ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)

require 'bundler/setup' # Set up gems listed in the Gemfile.

詳細までは追いませんが、gemを使用可能にできるようロードパスを更新してるみたい。

Bundler: The best way to manage a Ruby application's gems

# rails/commands

ARGV << '--help' if ARGV.empty?

aliases = {
  "g"  => "generate",
  "d"  => "destroy",
  "c"  => "console",
  "s"  => "server",
  "db" => "dbconsole",
  "r"  => "runner",
  "t"  => "test"
}

command = ARGV.shift
command = aliases[command] || command

require 'rails/commands/commands_tasks'

Rails::CommandsTasks.new(ARGV).run_command!(command)

railsコマンドに続けて入力した引数をここで変数として取り扱っている。rails sといったエイリアスもここで定義されている。そして、rails/commands/commands_tasksをrequireしている。rails/commands/commands_tasksでは、

require 'rails/commands/rake_proxy'

module Rails
  # This is a class which takes in a rails command and initiates the appropriate
  # initiation sequence.
  #
  # Warning: This class mutates ARGV because some commands require manipulating
  # it before they are run.
  class CommandsTasks # :nodoc:
    include Rails::RakeProxy

    attr_reader :argv

(中略)

    def initialize(argv)
      @argv = argv
    end

    def run_command!(command)
      command = parse_command(command)

      if COMMAND_WHITELIST.include?(command)
        send(command)
      else
        run_rake_task(command)
      end
    end

(中略)

    def version
      argv.unshift '--version'
      require_command!("application")
    end

(中略)

    private

(中略)

      def require_command!(command)
        require "rails/commands/#{command}"
      end

(中略)

      def parse_command(command)
        case command
        when '--version', '-v'
          'version'
        when '--help', '-h'
          'help'
        else
          command
        end
      end
  end
end

となっており、Rails::CommandsTasksを定義している。 Rails::CommandsTasks.new(ARGV).run_command!(command)Rails::CommandsTasksインスタンスを作り、run_command!メソッドを実行している。run_command!では、まずparse_commandエイリアスを取り扱い、versionコマンドとしてまとめて扱うようにしている。そして、versionメソッド→require_command!メソッドの順に実行され、最終的にrails/commands/applicationを実行する。

ここから先は、Rails::Root以外から実行した場合と同じになる。