読者です 読者をやめる 読者になる 読者になる

wataメモ

日々のメモをつらつらと書くだけ

SwiftでHimotokiの使い方メモ

 iOSのアプリを開発している時にSwiftHimotokiを使った時のメモ。 執筆時点のHimotokiのバージョンは2.0.1。 自分が使うときにいい感じにまとまった情報が見つからなかったので誰かの為になればと書くことにした。

Himotokiとは

Himotoki (紐解き) is a type-safe JSON decoding library purely written in Swift. This library is highly inspired by popular JSON parsing libraries in Swift: Argo and ObjectMapper.

ということなので、タイプセーフなJSONデコーダー。デコード専門でエンコードする機能は無い。

本家のサンプルソース

gist3f4f01e767f580cfddfd5aa0629ed1dd

使い方

基本

 以下のDecodable protocolを実装することになる。

public protocol Decodable {
    /// - Throws: DecodeError or an arbitrary ErrorType
    public static func decode(e: Himotoki.Extractor) throws -> Self
}

 本家の例ではstructだがclassでももちろん問題ない。 ルールとしてDecodableなクラスがデータ保持クラスとならなければならず、必然的に戻りもDecodableを実装したインスタンスになる。 結果としてDecodableはフィールドとデコードロジックを持つことになる。

 今回の記事ではJSON形式で記載するが、実際Himotokiでデコードする場合は以下のように変換したものを渡す。

// JSON文字列
let jsonString = "{\"id\": 1, \"name\": \"taro\"}"
// JSONをシリアライズ
let json = try! NSJSONSerialization.JSONObjectWithData(
  jsonString.dataUsingEncoding(NSUTF8StringEncoding)!,
  options: NSJSONReadingOptions(rawValue: 0)
)

// Himotokiでデコード
print(try! Person.decodeValue(json))

 もしくは最初からデータ形式なものでもいい。

let json:AnyJSON = [
  "name": "Himotoki",
  "floor": 12,
  "owner": ["id": 1, "name": "taro"],
  "members": [
    ["id": 1, "name": "taro", "remarks": [
        "age": "18",
      ]
    ],
    ["id": 2, "name": "hanako"],
  ]
]

// Himotokiでデコード
print(try! Group.decodeValue(json))

演算子(Operators)

 Himotokiの分かりづらい部分としては演算子がある。 初見では「これは一体何なんだ?」となるが慣れれば簡単。

Decodable protocolの引数として渡されるExtractorの演算子として以下がある。

演算子 デコード結果 備考
<| T 値(単体)
<|? T? optional(単体)
<|| [T] 値(Array)
<||? [T]? optional(配列)
<|-| [String: T] 値(Dictionary)
<|-|? [String: T]? optional(Dictionary)

 APIのレスポンスを例に考えると

  • 必ず存在するキーの場合は「値(単体)」

e <| "name"

  • 存在しない場合があるキーの場合は「optional(単体)」

e <|? "name"

  • 必ず存在するキーで値が配列の場合は「値(Array)」

e <|| "emails"

  • 存在しない場合があるキーで値が配列の場合は「optional(配列)」

e <||? "emails"

  • 必ず存在するキーで値がハッシュの場合は「値(Dictionary)」

e <|-| "remarks"

  • 存在しない場合があるキーで値がハッシュの場合は「optional(Dictionary)」

e <|-|? "remarks"

ネストしたハッシュをフラットにデコード

{
  "name": {
    "first": "taro",
    "last": "tanaka"
  }
}

 上記のような階層がある場合でもフラットにデコードしたい場合は以下のように書ける。

struct Person: Decodable {
  let firstName: String
  let lastName: String

  static func decode(e: Extractor) throws -> Person {
    return try Person(
      firstName: e <| ["name", "first"],
      lastName:  e <| ["name", "last"]
    )
  }
}

結果。

print(try! Person.decodeValue(json))
=> Person(firstName: "taro", lastName: "tanaka")

つまり配列で、階層を表現することが出来る。

ネストしたハッシュを階層を付きでデコード

{
  "name": "Himotoki",
  "floor": 12,
  "owner": {"id": 1, "name": "taro"},
  "members": [
    {"id": 1, "name": "taro"},
    {"id": 2, "name": "hanako"}
  ]
}

 実際はフラットなハッシュではなく、親子関係を持つのも多いと思う。 子供が共通な情報だったりすることもあるのでそういう場合のデコードももちろん可能。 例えば上記のowner(単体)やmembers(Array)の階層部分を別のクラスとしてもデコード出来る。 その場合は親(Group)と子クラス(Person)のDecodableを作る必要がある。

struct Person: Decodable {
  let id: Int
  let name: String

  static func decode(e: Extractor) throws -> Person {
    return try Person(
      id:   e <| "id",
      name: e <| "name"
    )
  }
}

struct Group: Decodable {
  let name: String
  let floor: Int
  let owner: Person     // 変数の型をPersonにする
  let members: [Person] // 変数の型をPersonの配列にする

  static func decode(e: Extractor) throws -> Group {
    return try Group(
      name:    e <| "name",
      floor:   e <| "floor",
      owner:   e <| "owner",   // 変数の型がDecodableの場合は階層的にデコードしてくれる
      members: e <|| "members" // もちろん配列でも問題ない
    )
  }
}

結果。

print(try! Group.decodeValue(json))
=> Group(
        name: "Himotoki",
        floor: 12,
        owner: Person(id: 1, name: "taro"),
        members: [
          Person(id: 1, name: "taro"),
          Person(id: 2, name: "hanako")
        ]
   )

Dictionary

// json1
{
  "id": 1,
  "name": "taro",
  "remarks": {"age": "18"} // ここをDictionaryとして受け取る
}

// json2
// こちらの例はremarksキーが無い(optional説明用)
{
  "id": 1,
  "name": "hanako"
}

 structやclassを用意しないでもDictionaryとして受け取ることも出来る。 その時も一応タイプセーフとなるが、専用のstructやclassを作る程はタイプセーフ度は高くない。 動的に項目が変わる場合かつ表示もそれに合わせて対応できている時に使うのが良いかもしれない。

struct Person: Decodable {
  let id: Int
  let name: String
  let remarks: [String: String]? // optional系の場合は変数もoptionalにしておく必要がある

  static func decode(e: Extractor) throws -> Person {
    return try Person(
      id:      e <| "id",
      name:    e <| "name",
      remarks: e <|-|? "remarks" // 今回はoptionalにする必要は無い(「e<|-|」でいい)が例としてoptionalを使用
    )
  }
}

結果。

// json1
print(try! Person.decodeValue(json1))
=> Person(id: 1, name: "taro", remarks: Optional(["age": "18"]))

// json2
print(try! Person.decodeValue(json2))
=> Person(id: 2, name: "hanako", remarks: nil)

一番上の階層が配列の場合

[
  {"id": 1, "name": "taro"},
  {"id": 2, "name": "hanako"}
]

 今まではJSONがハッシュで始まっていたが、上の例の場合はdecodeValueではなくdecodeArrayを使えば良い。 この場合はDecodableのメソッドではなくグローバルなメソッドのdecodeArray。

struct Person: Decodable {
  let id: Int
  let name: String
  let remarks: [String: String]? // optional系の場合は変数もoptionalにしておく必要がある

  static func decode(e: Extractor) throws -> Person {
    return try Person(
      id:      e <| "id",
      name:    e <| "name",
      remarks: e <|-|? "remarks" // 今回はoptionalにする必要は無い(「e<|-|」でいい)が例としてoptionalを使用
    )
  }
}

結果。

// 型推論なので変数の型でPersonのArrayだと示す必要がある
let people:[Person] = try! decodeArray(json) // Person.decodeArray(json)ではないことに注意
print(people)
=> [Person(id: 1, name: "taro", remarks: nil), Person(id: 2, name: "hanako", remarks: nil)]

 蛇足だが、グローバルなdecodeValueを使っても今までのことと同じことが出来る。 その場合は型推論用に変数などに受ける必要がある。

let person:Person = try! decodeValue(json)
print(person)

// 以下の様に型を指定しないとAmbiguous reference to member 'decodeValue'となってコンパイルエラーとなる
let person = try! decodeValue(json)
print(person)

rootKeyPath

{
  "name": "Himotoki",
  "owner": {"id": 1, "name": "taro(owner)"}
  "remarks": {
    "language": "Swift",
    "version: "2.0.1"
  },
  "members": [
    {"id": 1, "name": "taro"},
    {"id": 2, "name": "hanako"}
  ]
}

 一番上が配列でなくても、一発で配列部分だけをデコードすることができる。 例えば上のJSONでmembersをいきなり取得したい場合は以下の様に書ける。 rootKeyPathを引数に渡すことで、JSONを辿ってデコードしてくれる。 (rootKeyPathは配列なのでさらに深くても辿ってくれる。)

let people: [Person] = try! decodeArray(json, rootKeyPath: ["members"])
print(people)
=> [
  Person(id: 1, name: "taro", remarks: nil),
  Person(id: 2, name: "hanako", remarks: nil)
]

 同じくハッシュ版もある。

let remarks: [String: String] = try! decodeDictionary(json, rootKeyPath: ["remarks"])
print(remarks)
=> ["language": "Swift", "version": "2.0.1"]

 そしてもちろんDecodable版もある。

// rootKeyPathは1階層の場合は配列にしなくても良い
print(try! Person.decodeValue(json, rootKeyPath: "owner"))
=> Person(id: 1, name: "taro(owner)", remarks: nil)

Transformation

 Himotokiはデフォルトでは基本的な文字列や数値しかデコード出来ない。 URLや日時をデコードする場合は一旦Himotokiから文字列として受け取って変換してもいいが、スマートにやるのであれば独自のTransformationを書くことも出来る。

以下、本家のサンプルソース

// Creates a `Transformer` instance.
let URLTransformer = Transformer<String, NSURL> { URLString throws -> NSURL in
    if let URL = NSURL(string: URLString) {
        return URL
    }
    
    throw customError("Invalid URL string: \(URLString)")
}

let URL = try URLTransformer.apply(e <| "foo_url")
let otherURLs = try URLTransformer.apply(e <|| "bar_urls")

例外について

 値が返った(デコードできた)場合は型が保証される関係上、失敗した場合は即例外が発生する。

do {
    return try decodeValue(json)
} catch let DecodeError.MissingKeyPath(keyPath) {
    // optionalではなく値系でキーが存在しない場合に発生する。
    // keyPathにはキー情報が入っているが、ネストしたオブジェクトのパースも対応しているため配列が入っている。
    // 
    // ネストしたオブジェクトのパースとは
    // {"name": {"first": "taro", "last": "tanaka"}}
    // e <| ["name", "first"]
    // と書くことでtaroがデコードされる。
    // その場合はkeyPath.componentsに["name", "first"]が入っている。
    // 通常の場合も["name"]が入っている。
    print("key: \(keyPath.components)")
} catch let DecodeError.TypeMismatch(expected: expected, actual: actual, keyPath: keyPath) {
    // 型に変換出来なかった場合に発生する。
    // expectedとactualはString型で形名が入っている。
    // keyPathに関しては上の「DecodeError.MissingKeyPath」と同様。
    print("description: Failed to convert JSON value to model's property.")
    print("             Key path \(keyPath.components) expected `\(expected)` but was `\(actual)`.")
    print("key        : \(keyPath.components)")
    print("expected   : \(expected)")
    print("actual     : \(actual)")
} catch let DecodeError.Custom(string) {
    // カスタムエラーの場合。
    // Transformationなどで自分が発生させる例外。
    // エラーメッセージを入れておくことが出来る。
    print("error: [decode error] custom.")
    print(string)
}

最後に

 ちょっと世の中に情報が少ないので理解するのが少し大変だが、慣れれば簡単に書けるようになるので便利。

ゴロム・ライス符号を試してみた

 前回のKazuhoさんのブログでブルームフィルタがソート済の整数列として表現できるという記載があった。 「ブルームフィルタを試してみた」のやり方ではソート済の整数列として扱ってはいない。 では「ソート済みの整数列として扱える」ということはどういうことかというと以下のページ(ブログの参考リンクの内の1つ)に書いてあった。 今回は上記のブログの解説的な話になっている。

Golomb-coded sets: smaller than Bloom filters - Giovanni Bajo's swapfile

 上記のブログによるとブルームフィルタでハッシュ値を計算した値を整数として扱うのだ。 上記のページでは「alpha」という文字は「1017」と計算されている。 *1

要素 ハッシュ値
alpha 1017
bravo 591
charlie 1207

 この1017という値を使って本来なら1017ビット目を立てるのだが、今回はそのまま整数として扱う。 そして、すべての値を計算したらハッシュ値をソートするのだ。 ブルームフィルタの特性上、どの値がどの要素だったから覚えておく必要はない。 以下はブログより引用したNATO フェネティックコードの文字列の計算結果。

[151L, 192L, 208L, 269L, 461L, 512L, 526L, 591L, 662L, 806L, 831L, 866L, 890L, 997L, 1005L, 1017L, 1134L, 1207L, 1231L, 1327L, 1378L, 1393L, 1418L, 1525L, 1627L, 1630L]

 これでブルームフィルタをソート済の整数列として扱うことが出来る。

ゴロム符号

 Wikipediaより引用

ゴロム符号(ゴロムふごう、Golomb coding)とは、南カリフォルニア大学のソロモン・ゴロムによって開発された、幾何分布に従って出現する整数を最適に符号化することのできる整数の符号化手法である。 ゴロム符号と類似の手法にライス符号があるが、ゴロム符号の特別な場合がライス符号になるため、ライス符号のことをゴロム・ライス符号(Golomb-Rice coding)と呼称することが多い。特にライス符号は符号化・復号の計算量が少ないことが特徴。圧縮率は幾何分布の時はハフマン符号と同一で、それ以外ではそれよりも悪い。

ライス符号

 符号化のパラメータmが2の冪であるときにライス符号となる。 これだけだと何を言っているのかわからないので、ブルームフィルタの例を上げる。

 ハッシュ値計算に用いる関数は1つとし26要素が登録されていて、1/64の確立で偽陽性が発生するデータ構造にしたい場合。 ハッシュ値の計算が数値になる都合上、26要素×64=1664ビット用意すれば満たすことになる。 この64というのが符号化のパラメータになり、64は2の6乗なので2の冪となる。

Golomb-coded sets

 さて、ここからが本題になる。 ブルームフィルタをソート済みの整数列として扱えたとしても表現するのに元のブルームフィルタより多くのビットが必要では意味が無い。 Kazuhoさんのブログにも書いてある通り、ソート済の整数列というのは汎用的なデータ構造で検索エンジン転置インデックスとか、色々なところで使うらしい。 その為、このデータ構造の様々な研究がなされている。 その内の空間効率が理論限界に近いのが今回のアルゴリズムということだ。

 整数列を圧縮するには、zlibのような汎用アルゴリズムは向かない。 なぜなら文字列をハッシュ値に変換した値はzlibからするとランダムデータに見えるからだ。

 今回の考え方を理解するために例を上げながら考えてみよう。 26x64=1664の範囲から26個の数値をランダムに選択したとする。 あなたがある値とその次の値との差がどの程度になる予想するとなると「64ぐらい」と考えないだろうか。 もちろん全要素が均一に64の差で選択されることは殆どないが、大体それぐらいに分布することがイメージできる。 つまり、ある値とその次の値の差は64未満や64から128未満に収まる可能性は非常に高く、128から194未満に収まるのは少し稀で194から256未満に収まるのはさらに少し稀と続いていく。 これは幾何分布に従っていると言える。

 そしてこの性質を利用することでデータと圧縮しようと言うのだ。 ある値とその次の値の差を普通に数値として表現するのではなく、符号化パラメータ(今回は64)の余りに分けて考える。 「商」は幾何分布の特性上恐らく0か1になる可能性が高く、それ以上になるのは確率的に低くなっていくので、アルファ符号で表現する。 そして「余り」の部分はどんな値になるかはランダムなので通常のビットで表現する。 今回の場合は0から63(0相対)となるので6ビットで表現する。

商の符号化

数値 符号
0 0
1 10
2 110
3 1110
4 11110
5 111110
6 1111110

 より大きい数値の場合は表現するのにビット数がより多く要るようになっている。

符号化

 全体としての符号化した結果の一部は以下のようになる。 ※最初の要素は差としては0との差とする

数値 余り ゴロム符号
151 2 23 110 010111
41 0 41 0 010111
16 0 16 0 010000
61 0 61 0 11101

 そして最終的な符号化結果は以下となる。

11001011 10101001 00100000 11110111 10000000 01100110 00111010 00000110 00011111 00100000 01100101 00011001 10001010 10110001 00000011 00101101 01100010 01001100 01010000 00110011 00011110 01100110 10101110 10011000 00011

 これは197ビットとなり、約7.57ビット/単語となる。 理論値は26×log2(64)=156なので、そこまでは及ばないが非常に近いのがわかる。

復号

 符号化の手順を逆にすることで復号することが出来る。

まとめ

 こういったアルゴリズムの理解は場合によっては不要なこともあると思うが、知っておいても損は無いのではないだろうか。 ただ、無数有るアルゴリズムすべてを勉強することは難しい。 そういう場合はやはり自分が関わっている、もしくは興味がある分野で使われているアルゴリズムを学ぶのが良いと考えている。

*1:ハッシュ値計算ロジックとしては要素のmd5値を計算し、それを16進数の数値として扱い余りを使っている。

ブルームフィルタを試してみた

 最近、H2Oの開発者であるKazuhoさんのブログブルームフィルタという単語があったので調べてみた。

Kazuho's Weblog: ソート済の整数列を圧縮する件

 以下はKazuhoさんのブログを読んだ前提で書いています。

特徴

 Wikipediaより引用。

空間効率の良い確立的データ構造であり、要素が集合のメンバーであるかどうかのテストに使われる。 偽陽性(False Positive)による誤検出の可能性があるが、偽陰性(False Negative)はない。要素を集合に追加することができるが、削除することはできない(Counting filter を使えば削除できる)。集合に要素が追加されればされるほど、偽陽性の可能性が高くなる。

 つまり、特定の要素(例えば名前)が集合(名簿)に含まれているかどうかを調べるために使われる。 ただ、毎回名簿情報をサーバに投げているとデータ量が多くなってしまうので、ある程度簡略化したものだ。

実装

 実装を簡単に説明すると

  1. 0で初期化されたmビット配列を用意
  2. 特定の値をk個のハッシュ関数でmまでの値に変換する
  3. 2で計算したビット目を立てる

 ※既に立てようとしているビット目が立っていても気にしないで立てておく。

 利用する場合は、特定の値を同じk個のハッシュ関数で求められたビット目が すべて 立っているかどうか確認する。 すべて立っていれば、その値がブルームフィルタに含まれている 可能性 がある。 ここで可能性があると書いているのは、アルゴリズム上、確実に入っていることを保証するわけではないからだ。 ブルームフィルタは特徴として、偽陽性の可能性はあるが 偽陰性はない。

 つまり、ブルームフィルタに登録されている値が本当は含まれているのに、含まれていないと判定されることは無いが、登録されていない値が含まれていると誤判定することはあるということだ。

なぜ使われているのか

 勝手な想像ではあるが、H2Oではキャッシュがクライアント含まれている場合は無駄なのでサーバープッシュでコンテンツを送りたくない。 だが、HTMLに含まれている静的コンテンツ(cssやjs等)が100個とかあった場合に、それの内容をハッシュ化した(ファイル名だけだと内容が更新されている場合があるので)値を毎回送受信するのも効率が良くない。 なのでブルームフィルタ(ビット列)はソート済整数列として表現出来るのでそのブルームフィルタだけを送受信すれば効率的だ。 先程も記述した通り、クライアントがキャッシュを持っているのに、持っていないと判定されることは無いので、無駄なコンテンツを送ることはない。 ただ、キャッシュを持っていない物を、持っていると判定されることがあるのでその場合はサーバプッシュでは送られず、通常通りのリクエスト・レスポンスによって配信される。

 キャッシュという特性上、「あればラッキー」ぐらいな感覚なので、誤判定されてサーバプッシュされなくても(今までと同じなので)マイナスにはならないという考えだろう。

 このブルームフィルタをCookieでブラウザに食わせてやりとりを行うようだ。 その為にCookie値のバイト数分もったい無いと感じるかもしれないが、HTTP/2にはHPACKという仕様があり、前回と同じヘッダ項目は送らなくても良くなっている。 このCASPER(cache-aware server pusher)Cookieという枯れた技術を利用することで、あらゆるブラウザに対応し、HTTP/2の仕様もうまく利用したとても良い実装だと感じた。

サンプルソース

 やっつけのサンプルソース

gist37f8a980bbdc0e733ba2

踏み台サーバ立ち上げAPIを作成してみた

 踏み台サーバは以前の記事でも書いたようにbastionが立ち上がっていて、SecurityGroupを更新してssh出来るようにしている。 その都合上ずっとインスタンスを立ち上げっぱなしにしていた。 それは勿体無いのでAmazon API GatewayAWS Lambdaで、単純なhttpアクセスで踏み台サーバが立ち上がるようにした。

構成

 API Gatewayを入れることで、クライアント側にSDK等特別な物が必要なく、curl等があれば踏み台インスタンスを立ち上げることができるようになる。 インスタンスを落とす場合は以前のLambdaを定期的に仕込んでおくことで、勝手に落ちるようにしてある。

f:id:wata_htn:20151026155107p:plain

AWS Lambda

 以前のを少しカスタマイズして、「Server」Tagに「bastion」と着けたインスタンスを起動するようにした。(複数可) 今回はLambdaのExecute RoleにIAMを付けるパターンなので、accessKeyIdとかは無し。

gist22c7a01e5b826957124c

Amazon API Gateway

 認証についてはIAMではなく、API Keyで行うようにし、curlではヘッダを追加するようにしている。 何度も叩くのでaliasを切っておく。

alias bastion='curl --header "x-api-key: xxxxxxxxxxx" https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/xxxxxx'

実行

起動

➜  ~  bastion
{"ip":"","message":"started instance(instanceId: i-xxxxxxxx)"}

 しばらくしてもう一度叩くとIPアドレスが分かる。 EIPは起動していないインスタンスにつけておくと課金されるので、毎回GIPが変わるがEIPをつけていない。 EIPがあればしばらくしてsshでつなぎに行けば良い。

➜  ~  bastion
{"ip":"x.x.x.x","message":"no instance to start."}

まとめ

 コストを減らす為に、利便性を下げないことは重要で、かつそれが色々な人に手軽に出来るようになることは重要。 「本当にEC2インスタンスは必要なのか」、「必要だとしても常時必要か」は考えていく必要がある。

Amazon Web Services クラウドデザインパターン 設計ガイド読了

 Amazon Web Services クラウドデザインパターン設計ガイド 改訂版を読んだ。

www.amazon.co.jp

 57種の設計パターンについてまとめた書籍である。 これらのパターンはCloud Design Patternにまとめられているものであり基本的には書籍を読まなくても知ることは出来る。 大きく違うのは「2章 CDPの適用シナリオ」で「画像動画配信サイト」、「Eコマースサイト」、「キャンペーンサイト」というシステム構築シナリオをベースにデザインパターンの適用を紹介している。 コストメリットや稼働率のバランスがサービスのライフサイクルによって変わるのに対応していくのが良いということ。

 如何に速く、安くユーザにコンテンツを届けるのか。 そこを突き詰めて考えていくのも非常に面白い。 そしてAWSのサービスはそういう観点で捉えていくと「なぜこういうサービスが生まれたのか」「組み合わせればこんなことが出来るのでは」という発想が生まれやすくなっていくだろう。

なるほどUnixプロセス ― Rubyで学ぶUnixの基礎読了

 なるほどUnixプロセス ― Rubyで学ぶUnixの基礎を読んだ。

tatsu-zine.com

お勧め対象者

 基本的にはUNIXのプロセスの話がわかりやすくまとまっている。 なぜRubyが絡んで来るかというと、C言語で理解を進めようとすると色々煩雑なことが多いがRubyなら簡潔に記述することが出来、UNIXのプロセスを理解するという本質にフォーカスしやすいからだそうだ。 内容のレベルとしては、入門書といった感じの印象を受けた。 その為「プロセスのことがよくわからない」という人にも勧められる内容。

 以下の内容の理解を深めたい人は購入を検討するのが良いだろう。

  • プロセスの親子関係
  • ファイルディスクリプタ
  • プロセスの終了コード
  • シグナル
  • fork、exec
  • ゾンビプロセス
  • プロセス間通信
  • デーモンプロセス
  • prefork

分かりやすかった理由

 ネットワーク周りや、メモリ、CPU周りまではそこまで掘り下げていないのが、逆に理解しやすいポイントだと感じた。 1つ1つの確認自体も丁寧に解説、補足してくれるので「あれ?この場合はどうなるんだろう」というのを実際試さなくても理解出来る。 付録のSpyglass(Rubyで実装されたWebサーバ)も実際動くコードを見て試しながら理解を進められるのは重宝する。

まとめ

 書かれた時点が結構前(2011年12月)なので、情報が古い部分があるのは否めない。 ただプロセスの事を知りたくなったら入門としては是非とも読んでもらいたい素晴らしい書籍だ。

nginx1.9.5のHTTP/2の機能を使ってみた

 次世代Web カンファレンスでも話題になっていたHTTP/2でnginxも1.9.5でサポートということで使ってみた。 サポートしたと言ってもnginxのページには以下の注意書きが書かれていた。

  • もしアプリでWAFを使っていて、nginxの前にあるならHTTP/2に対応しているか確認し、指定なければnginxの後ろにしてください
  • HTTP/2のサーバープッシュはこのリリースではまだサポートされません
  • もしssl_prefer_server_ciphersがonに設定されていて、ssl_ciphersブラックリストに乗っているものを使っている場合は、ブラウザはハンドシェイクエラーとなり動きません

サーバプッシュはまだサポートされない模様。 これを使ってみたかったのだが、残念。

試すこと

 せっかく試すので、nginx単体と静的ファイルだけでは面白く無いので、裏にrailsアプリを動かすことにした。

環境準備

OS

 CentOS 6.6を使用。 Vagrantfileを適当に作成してvagrant upを実行。

Vagrantfile

Vagrant.configure(2) do |config|
  config.vm.box = "http2-test"
  config.vm.box_url = "https://github.com/tommy-muehle/puppet-vagrant-boxes/releases/download/1.0.0/centos-6.6-x86_64.box"
  config.vm.network "private_network", ip: "192.168.33.80"
end

パッケージのインストール

 とりあえずこちらも適当にvagran sshしてパッケージをインストール。

sudo yum install -y git readline-devel \
  libxml2-devel libxslt-devel \
  mysql mysql-server mysql-devel \
  nodejs npm ImageMagick ImageMagick-devel

rubyのインストール

 いつものrbenvのお決まり作業。 githubのgistを使って2.2.3をインストール。

curl -L http://git.io/vWmCs | sh

 短縮URLを使ったが、ソースは以下。

gist7f813b7e214a7b9846a3

nginx 1.9.5のインストール

sudo rpm -i http://nginx.org/packages/mainline/centos/6/x86_64/RPMS/nginx-1.9.5-1.el6.ngx.x86_64.rpm

自己証明書作成

 毎度の事なのでコマンドだけ。

openssl genrsa -des3 -out server.key 2048
openssl req -new -key server.key -out server.csr
cp server.key server.key.org
openssl rsa -in server.key.org -out server.key
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
# nginxのディレクトリにコピーしておく
sudo cp -ip server.crt server.key /etc/nginx/

nginxのログの設定

 HTTP/2で動いているか確認するためにログ出力変更。 GETのところでもわかるが、最後の「h2:$http2」を追加。

/etc/nginx/nginx.conf

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                  '$status $body_bytes_sent "$http_referer" '
                  '"$http_user_agent" "$http_x_forwarded_for" '
                  'h2:$http2';

こうすることでHTTP/2の場合は「h2:h2」と出力される。 HTTP/2でない場合は「h2:」となる。

実際のログ

192.168.33.1 - - [21/Oct/2015:04:03:10 +0200] "GET / HTTP/2.0" 404 640 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36" "-" h2:h2

railsアプリ作成

 railsプロジェクト作成。

# nokogiriインストール用
bundle config build.nokogiri --use-system-libraries
# railsのインストール
gem install rails
# プロジェクトディレクトリ作成
mkdir projects
cd projects
# プロジェクト作成
rails new http2 -d mysql -T --skip-bundle

 Gemfileに必要なgemを追加。

Gemfile

# rails 4.2.4とmysql2の0.4.0の相性が良くないのでダウングレード
gem 'mysql2', '~> 0.3.20'

# 以下を追加
gem 'therubyracer'
gem 'unicorn'

 bundle installしてwelcomeコントローラを作成。

# bundle install
bundle --path vendor/bundle
# welcomeコントローラ作成
./bin/rails g controller welcome

app/controllers/welcome_controller.rb

class WelcomeController < ApplicationController
  def index
  end

  def nginx
  end

  # サーバプッシュテスト用(今回はnginxがまだ対応していないので関係ない)
  def push
    response.headers['Link'] = '</assets/14.jpg>; rel=preload'
    render :index
  end
end

まずはrails側で画像ファイルを裁く場合のテスト。

app/views/welcome/index.html.erb

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <h1>hello http/2!</h1>
    <img src="/assets/14.jpg">
    <img src="/assets/150704-05.jpg">
    <img src="/assets/20150707100135.jpg">
    <img src="/assets/B-U2e0ACcAAAuq6.jpg">
    <img src="/assets/akagami.jpg">
    <img src="/assets/yasashi.jpg">
  </body>
</html>

静的ファイルはnginxで裁く場合のテスト。 nginxの設定で「/img/」はunicornではなくnginxが裁くように設定している。

app/views/welcome/nginx.html.erb

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <h1>hello http/2!</h1>
    <img src="/img/14.jpg">
    <img src="/img/150704-05.jpg">
    <img src="/img/20150707100135.jpg">
    <img src="/img/B-U2e0ACcAAAuq6.jpg">
    <img src="/img/akagami.jpg">
    <img src="/img/yasashi.jpg">
  </body>
</html>

 ルーティングを設定。

config/routes.rb

root 'welcome#index'
get 'nginx' => 'welcome#nginx'
get 'push'  => 'welcome#push'

unicornの設定、起動

 unicornタスクを作成。

./bin/rails g task unicorn

 いつものごとく設定。 環境(RACK_ENV)は環境変数を利用するようにしてある。

lib/tasks/unicorn.rake

namespace :unicorn do
  ##
  # Tasks
  ##
  desc "Start unicorn for development env."
  task(:start) {
    config = Rails.root.join('config', 'unicorn.rb')
    sh "bundle exec unicorn_rails -c #{config} -D"
  }

  desc "Stop unicorn"
  task(:stop) { unicorn_signal :QUIT }

  desc "Restart unicorn with USR2"
  task(:restart) { unicorn_signal :USR2 }

  desc "Increment number of worker processes"
  task(:increment) { unicorn_signal :TTIN }

  desc "Decrement number of worker processes"
  task(:decrement) { unicorn_signal :TTOU }

  desc "Unicorn pstree (depends on pstree command)"
  task(:pstree) do
    sh "pstree '#{unicorn_pid}'"
  end

  def unicorn_signal signal
    Process.kill signal, unicorn_pid
  end

  def unicorn_pid
    begin
      File.read("/tmp/unicorn.pid").to_i
    rescue Errno::ENOENT
      raise "Unicorn doesn't seem to be running"
    end
  end
end

 unicornの起動。

./bin/rake unicorn:start

nginxとunicornの連携

 HTTP/2テスト用に以下のnginx設定ファイルを追加。

/etc/nginx/conf.d/http2.conf

upstream unicorn {
  server unix:/tmp/unicorn.sock fail_timeout=0;
}
server {
    listen  443 ssl http2;
    ssl_prefer_server_ciphers on;
    ssl_ciphers AESGCM:HIGH:!aNULL:!MD5;

    server_name localhost;
    ssl_certificate     /etc/nginx/server.crt;
    ssl_certificate_key /etc/nginx/server.key;

    # /img/は静的に対処する
    location /img/ {
      root /var/www/html;
    }

    try_files $uri/index.html $uri @unicorn;
    location @unicorn {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_pass http://unicorn;
    }
}

 HTTP/2がオフの状態(HTTP)を試すためにdefault.confを修正。

/etc/nginx/conf.d/default.conf

erver {
    listen       80;
    server_name  localhost;

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    try_files $uri @unicorn;
    location @unicorn {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_pass http://unicorn;
    }
}

 nginxの起動。

sudo service nginx start

実際に試してみる

 後はブラウザ(Chrome)からアクセスして、Networkを見てみる。

HTTP

 見事に同時に6コネクションでブロックして終わってから残りをアクセスする感じになっている。 結果的には一番遅い。

f:id:wata_htn:20151021183356p:plain

HTTP/2(すべてrails)

 静的ファイルのリクエストがすべて同時に開始されている。 HTTPでの処理より早い。

f:id:wata_htn:20151021183935p:plain

HTTP/2(画像はnginx)

 同じく静的ファイルのリクエストがすべて同時に開始されている。 今回の結果としてはrailsを通すより早くなっている。 (想定通りの結果)

f:id:wata_htn:20151021183942p:plain

リクエスト、レスポンスヘッダについて

 HTTP/2はヘッダ周りの仕様も変わっているので確認すると以下のようになっていた。

HTTP

 HTTPはいつもどおりヘッダは大文字から始まっている。

リクエスト

f:id:wata_htn:20151021190531p:plain

レスポンス

f:id:wata_htn:20151021190553p:plain

HTTP/2

 ヘッダ名は小文字になっていて一部のヘッダは「:method:」とかになっている。 これはStatic Tableに定義されている物だ。 よく使うものは番号を振っておいて、データ転送量を減らす目論見。 この辺の考えはmod_ajpでも取り入れられていた。

リクエスト

f:id:wata_htn:20151021190519p:plain

レスポンス

f:id:wata_htn:20151021190546p:plain

まとめ

 nginxや他のサーバもHTTP/2を実装してきている。 どこまで既存のアプリがそこまで対処なしで移行出来るかが今後のポイントとなる。 ブラウザにキャッシュされているリソースをサーバプッシュで送るのは無駄なので サーバプッシュ周りが実装されたら、nginxやアプリ側でどう対応していくのかのベストプラクティスが必要。 h2oのようにCookieでブラウザのキャッシュのやり取りをして最適化するという手法もあるようだ。 Ruby界隈ではRackの対応やRailsでassets周りがHTTP/2に殆ど意識せずに移行出来るなら、HTTP/2は広まっていくだろう。 この辺の腰が重いと「HTTP/1.1で良くない?」という流れにもなる。

 サーバプッシュ周りで素晴らしい使い方が発見され、HTTP/2が流行ることを期待したい。