puma はメモリ食い

puma にして喜んでたけど二時間後くらいに NewRelic で様子を見てみたらメモリ 90% 以上消費してて swap ファイルもめっちゃでかいのができてて暴発一歩手前になってた。

スクリーンショット 2017-01-21 14.36.34.png

一旦 puma をシングルモードからクラスターモードに変えて puma_worker_killer を導入し、 worker を定期的に再起動するようにした。ただ restart 後のメモリ増加傾向は変わらない。

引き続き NewRelic で様子を見ていると Python2 が一番メモリを食っている。このブログは Syntax Highlighting に Python の Pygmentspygments.rb 越しに使っている。これまで unicorn の worker プロセス 2 個で動かしていたときには最大でも Python のプロセスは 2 個で事足りてたけど、 puma にして 16 スレッド( puma のデフォルト)が起ち上がるので、 16 個の Python プロセスが起動していたっぽい。

これは意味ないなということで以前利用したことがあった Pygments の Ruby 移植版である rouge に変えてみることにした。こちらは Ruby なのでスレッドが別プロセスを起動してメモリ爆発ということにはならないと思われる。

puma_worker_killer と Pygments 置き換え後半日様子を見たが、これでメモリ消費量爆発ということはなくなった。

sidekiq でスレッドを増やしたときにも似たようなことを経験したけど、 Ruby から外部のプロセスを起動するようなプログラムを書いているときにマルチスレッドで処理をさせると、 Ruby のプロセスは増えなくても外部のプロセスがめっちゃ増えてそのせいでメモリあっぷあっぷになったり CPU の使用率が高まったりする。マルチスレッドプログラミングのご利用は計画的に。

異なるワーカー間でセッションを継続できない?

puma をクラスターモードにしたことで別の問題も発生した。なんとセッション情報が異なるプロセス間で共有されていないっぽい。なのでログインしてすぐに再ログインを求められるようになった。これは不便。原因を調査するには時間がかかりそうだったのでシングルモードに戻してみることにする。

まとめ

  1. デフォルトの 16 スレッドでは puma は unicorn に比べてメモリを沢山消費する可能性がある
  2. Ruby から外部のプロセスを起動するような処理に注意
  3. puma のクラスターモード( master worker モデル)でセッション情報がリセットされる問題に遭遇

追記

セッション情報が異なるプロセス間で共有されていない

これはちゃんと設定があって、 puma.rb に preload_app! を記述すればよかった。

If you're running in Clustered Mode you can optionally choose to preload your application before starting up the workers. This is necessary in order to take advantage of the Copy on Write feature introduced in MRI Ruby 2.0. To do this simply specify the --preload flag in invocation:

# CLI invocation
$ puma -t 8:32 -w 3 --preload

If you're using a configuration file, use the preload_app! method, and be sure to specify your config file's location with the -C flag:

$ puma -C config/puma.rb

# config/puma.rb
threads 8,32
workers 3
preload_app!

-- https://github.com/puma/puma#clustered-mode

これでプロセス間でセッション情報が共有されるようになった。

ブログの管理画面にファイルアップロード機能をつけてみた。 GitHub の Issue みたいにドラッグアンドドロップでアップロードしてくれる。こんな感じ。

file upload demo gif

いまのところ埋め込みフォーマットは Markdown にしか対応してない。

Lokka は heroku とか PaaS を使うことを前提に作られているのでファイルをアップロードする機能が提供されてこなかった( heroku は永続的なファイル書き込みができない)。 heroku で動かしている人でも使えるように Amazon S3 に上げるようにしてみた。 AWS のアカウント作成とかが必要なのでレンタルサーバーに設置してある WordPress にファイルアップロードするのに比べたら敷居が高いけど、設定するのは1回だけだし全くアップロードする手立てがなかったこれまでに比べたら劇的に快適になるはず。自分で使ってみたけどめっちゃ便利になった。

ファイルアップロード、ちゃんと作るなら Paperclip みたいにファイルのチェックを厳密に行なわないと危ないと思うけど、管理画面から上げるの前提なのでチェックなしで雑にアップロードできるようになっている。 AWS の token 系の設定画面を作ったら Lokka 本体に Pull Request 出します。

なお一つ前の記事でアプリケーションサーバーを puma に変更 - portal shit!というのを書いたけど、このファイルアップロード機能がうまく動かなかったので unicorn を捨てたのだった( pow で動かしている環境や bundle exec rackup して WEBRick で起動しているサーバーではちゃんとアップロードできる)。なぜか unicorn で multipart/form-data な POST リクエストを rack アプリケーションに適切に渡すことができず EOFError が発生してしまう。

"EOFError: bad content body" for multipart form requests · Issue #903 · rack/rack

仕事で unicorn で動いてるサーバーにファイルアップロードする仕組み入れたことは何度かあるけどこんなエラーになったことはなかったのになぁ。謎い。

ブログのアプリケーションサーバーには unicorn を使っていたのだけど puma に変えてみた。

PassengerやめてunicornにしたらLokkaが速くなった - portal shit!

2011 年から使っていたようなので丸 5 年は unicorn を使っていたことになる。 puma はスレッドセーフにしないと死ぬ、ということは聞き及んでいたのでおそるおそる変えてみたけどいまのところ問題なさげ。

手元で確認したときには scss のコンパイルで怪しい雰囲気があった(特定ページにアクセスしたあとレスポンスを返さなくなってしまう)ので 0.12.7 だった compass のバージョンを 1.0.3 に上げてみたところ問題が解決した。結果オーライということで深追いはしてないけど、リポジトリを見に行ってみたらメンテナンス停止しているようだった。最近はフロントエンドは JavaScript でやるのが当たり前ですもんね。

Compass/compass: Compass is no longer actively maintained. Compass is a Stylesheet Authoring Environment that makes your website design simpler to implement and easier to maintain.

Lokka のフロントエンドは 2010 年頃ナウかった Ruby ベースの技術が満載で最近の傾向とは異なるので、フロントエンドで利用してる gem がメンテされておらず Ruby のバージョンアップ時のボトルネックとなってくることが考えられる。この前 Ruby 2.4.0 で動くか素振りしてみたけどダメだった。padrino-helpers 、 slim 、 haml 、 coffeescript (この機能は自分が追加した)あたりとどういう風に折り合いを付けていくか考えないといけない。

10 年くらい前に買ったナイキの靴、靴紐が切れたので新しい紐を買って付け替えた。キャタピランとかいうやつでゴムのいぼいぼが付いてて、靴を履くときに緩めなくても伸びるし履いたら足のかたちに合わせて勝手に締まる。

この靴はハイカットで上の方を緩めても下の方まで緩みが伝播せず脱ぎ履きがかなり面倒な靴だった。それがキャタピランを導入したことで脱ぎ履きが楽になり、おまけにちょうどいい具合に勝手に締められるので紐をきつくしすぎて足が痛いということにもならない。

キャタピランに変えた状態で一日歩いてみたけどすごく履き心地が良くなって最高だった。両足セットで 980 円くらいしたけど靴が生まれ変わるので元を取った感ある。

JR東日本エリア在住ではないのだけど iPhone 7 を買って Suica の設定をしたので試してみたくなり iPhone 7 で電車に乗ってみた。JR九州の改札も福岡市地下鉄の改札もちゃんと通れた(当たり前)。ただ IC カードに比べて反応が少し遅くて「もしや使えないのでは?」と挙動不審になってしまった。もう少し右往左往してたら駅員に声かけされたと思う。買い物するときみたいにダブルタップして Wallet を呼び出してから通過したけど改札通過時にはそれをする必要はなくてただかざせばよいっぽい。

改札を通過するとすぐに iPhone に通知が来て残額がいくらかわかる。また移動中は「現在乗車中です」みたいな表示が出る。こういう通知はコンビニとかで買い物をしたときも来るけどめっちゃ便利。 一度 iPhone の Suica で金を払う体験をしてしまうと、レシートを見たり券売機で照会をしないと残高がいくらかわからない旧来型の IC カードには戻れなくなってしまう。カパカパ式ケースに iPhone と電子マネーを挟むのとは圧倒的にユーザー体験が違っていて、残高のリアルタイム通知が来て、乗車ステータスの確認ができ、残高が不足している場合はその場でチャージすることができる。

おサイフケータイ使ってた人たちは 10 年前にすでにこういう便利な体験してたのだろうか。自分は携帯は Motorola や Ericsson 、 NOKIA などの洋物携帯ばかり使ってたので今さらながら携帯電話に決済機能がくっつくことの便利さを実感した。これは本当に便利な機能だと思う。

Pinboard にブックマークしたらはてなブックマークに同期するやつを作った。

morygonzalez/pinboard2hatena: Sync はてなブックマーク with Pinboard

なんで Pinboard を使うのか、どうしてはてブに一本化しないのかというと、洋物のサービスを使うときに Pinboard の方が使いやすいから。 IFTTT に Pinboard 連携機能はあってもはてブ連携機能はないし、 Delibar や Reeder や ReadKit も Pinboard には対応しているけどはてブには対応してない。あと Pinboard は広告出ないしホッテントリ的なものもないので気がついたら Amazon で買い物してたとかホッテントリの海に溺れてた、ということも起こらない。とはいえはてブのコメントでキャッキャッウフフはしたい。なのでブックマークするときは両方にしたい。パソコンの Chrome からブックマークするときは Taberareloo でクロスポストできるのだけど、最近 iPhone からブックマークすることが増えて(iPhone 進化し過ぎて仕事するときしかパホコン使わなくなった) Pinboard とはてブにそれぞれブックマークするのがだるかった。そういうわけで Pinboard をメインにしつつはてブに同期を試みた。

はてなスタッフの aereal さんが作ってるはてブ API 用の gem (aereal/hatena-bookmark-restful: A client library for Hatena::Bookmark RESTful API)あって使わせてもらったのだけど、これはそのままだと使えない。タグが複数あるケースに対応してない& User-Agent を送らないので API 側から 401 Unauthorized が返ってくる。なので雑にモンキーパッチした。

はてなブックマークの API は tag を 10 個まで設定できるけど、以下のように Request Body が Encode されるのを期待しているっぽい。

"comment=&tags=ruby&tags=http&url=https%3A%2F%2Fgithub.com%2Flostisland%2Ffaraday"

この様な Request パラメーターを Ruby で表現すると以下のような Hash になると思う。

params = {
  comment: '',
  tags: ['ruby', 'http'],
  url: 'https://github.com/lostisland/faraday'
}

Hash は当然のことながら同じキーを複数持つことはできないから、 tags は配列として表現される。これをこのまま Faraday (Ruby の HTTP クライアント。上述の gem でも使われてる)に渡して Request Body を生成するとエラーになってしまうのだった。

最近の Faraday には encode option が追加されて FlatParamsEncoder というのを選べるようになってた。こいつを使うと配列を value に持つ Hash をシリアライズしたときに tags=foo&tags=bar みたいな形式にしてくれる。加えて User-Agent も載せるようにもした。こんな感じ。

class Hatena::Bookmark::Restful::V1
  def create_bookmark(bookmark_params)
    res = connection.post("/#{api_version}/my/bookmark") {|req|
      req.params = bookmark_params
    }
    attrs = JSON.parse(res.body)
    bookmark = Bookmark.new_from_response(attrs)
  end

  private

  def connection
    @connection ||= Faraday.new(url: 'http://api.b.hatena.ne.jp/') do |conn|
      conn.request :url_encoded
      conn.options.params_encoder = Faraday::FlatParamsEncoder
      conn.request     :oauth, {
        consumer_key:    @credentials.consumer_key,
        consumer_secret: @credentials.consumer_secret,
        token:           @credentials.access_token,
        token_secret:    @credentials.access_token_secret
      }
      conn.headers['User-Agent'] = 'Hatena::Bookmark::Restful Client'
      conn.adapter Faraday.default_adapter
    end
  end
end

なおはてブの API はリクエストパラメーターが不正なとき 400 Bad Request を返すのではなく 401 Unauthorized を返す。のみならず User-Agent なしのリクエストに対しても 401 を返す。一方で OAuth ヘッダーが不正なときは 400 を返す。原因の切り分けがむずかしくなるので、リクエストパラメータが不正なときは 400 を返して欲しいし認証できないときは 401 を返して欲しい。

iPhone 7

iPhone 7 を買った。 iPhone 6 を買ったのは 2015 年 7 月で買い換えサイクルからするとだいぶ早いのだけど、いい加減両親にガラケーを卒業してもらおうと思って、弟も少し前に iPhone を買い換えたので自分のと弟のを両親に渡して docomo から MVNO に引っ越してもらおうという算段。弟の iPhone 6 は docomo のやつで自分の iPhone 6 は SIM フリーなので問題ない。いい世の中になった。

一括払いで 9 万円も払えるような富裕層ではないのでオリコのローン( 20 回分割まで金利が 0% のやつ)で買った。 iPhone 6 を買ったときは転職したばかりで社会的信用ゼロだった& iPhone 5 をクロアチアでなくしてしまって一刻も早く電話が必要だったためクレジットカードの分割払いで買っていたので、今回が人生初のオリコローン。審査結果出るまでに結構時間がかかり、ベンチャー企業に勤めてるせいで審査通らないのかなとやきもきした。急いで欲しい人はローンは使わない方がいいと思う。ひょっとすると一部上場企業勤務の人だと一瞬で審査降りるのかもしれない。

Suica on iPhone 7

iPhone 7 では Suica を Wallet に登録できるはずなので ANA VISA Suica を登録して電子マネーはこれに一本化しようかと思っていたけど、 ANA VISA カードの Suica を吸い出して Wallet に追加することはできないみたいだった。調べてみたところ JR 東日本の Suica アプリでモバイル Suica を新規発行し、そのチャージ用のカードに ANA VISA Suica をクレジットカードとして指定すればよいっぽい( Suica が一枚増える感じ)。 ANA VISA Suica は View カードでもあるのでモバイル Suica の年会費はかからない。これでいつでも iPhone からチャージできて便利。オートチャージの都合で JR 九州の SUGOCA を使っていたけど( Suica は関東でないとオートチャージできない)、 iPhone からいつでもチャージできるとなれば Suica でも困らなそう。ほかの電子マネーの Apple Pay 対応が遅れたら Suica が交通系電子マネーの乱世を統一するかもしれない。

ハードとしての iPhone 7 の感想は、とにかくめっちゃ速いのがいい。 Touch ID とか一瞬で認証される。 Force Touch もよい。 iPhone で長い文章を入力中にカーソル位置を動かすの、結構ストレスだったんだけど Force Touch だと押し込んだだけで Vim のビジュアルモードのようにするするとカーソルを動かせて便利。ストレージが増えてるのもいい。いわゆる "竹" スペックの 128GB のやつを買ったのだけど、これだけあれば Eye-Fi で写真転送しまくっても iPhone がお腹いっぱいになったりしないだろうから安心。音楽もいっぱい入れられるので飛行機に乗ったときに iTunes Match 使えなくてほとんど聞ける音楽がない、という事態も避けられる。

一方で残念な点。ホームボタンが物理ボタンじゃなくなったのは慣れない。押し込んだ時のバイブレーションが MacBook Pro のトラックパッドの完成度とは大違いで残念。あとこのホームボタン、風呂で使える系のケースに入れてると押せないことが判明して愕然とした。無印良品の風呂スマートフォンケースに入れてみたところ押しても反応せずホームボタンはお飾りとなった。防水とはいえ風呂で使って水漏れ判定されたという話を聞くので風呂 iPhone するときは防水ケースに入れたいもの。ホームボタン効かないと全く使えないわけではないけど著しく操作性が低下する(Today widget を呼び出して適当なアプリを開きロック解除、 Force Touch でアプリ切り替えを呼び出しホームスクリーンに切り替え)。

Silicone case for iPhone 7

ケースは Apple のシリコーンケースを買ってみたけどめっちゃすべって良くない。 iPhone 5 や iPhone 6 には革のケースを使っていて、滑るということはなかった。革ケースは高いし(安い Android が買えそうな値段) 2 年近く使ってると破れてくるので次はシリコーンケースにしてみようと変えてみたけど失敗だった。やっぱり iPhone は裸が薄くて持ちやすくて最高。ケースを付けずに iPhone を使えるような精神力が欲しい。ガラスの保護には Anker のやつを買った。 iPhone 6 使ってるとき 2 回くらいコンクリートの上に落としたことあるけど iPhone のガラスは何ともなかったのでこれは頼りになると思う。

不満なところ、不便なところもあるが、 iPhone 6 に比べると格段に良くなっている。地方に住んでて Suica 使えないので iPhone 7 はパス、と思ってる人も実は使えて便利になる可能性があるのでご一考をお勧めします。