RubyistのためのGolangメタプログラミング

Goでは、reflectパッケージを使うとリフレクションを行うことができる。
そこで、以下のことを任意のstringによる動的な指定で行うことができるかを調べた。

  • send (関数, メソッド呼び出し)
  • define_method (関数, メソッドの定義)
  • const_get (任意の型のオブジェクトの取得)

send (関数, メソッド呼び出し)

stringで関数, メソッド名を指定して呼び出すことができるか?

  • 関数: No
  • メソッド: Yes

関数のsend

変数に関数が入っていればその変数を使って呼び出せるが、普通に定義した関数をstringから参照する手段はない。もしやりたければ手動でmap[string]funcにつっこんでおくしかない。(これやっちゃうとメタプロにならなそう)

メソッドのsend

method := reflect.ValueOf(&receiver).MethodByName("MethodName")
method.Call([]reflect.Value{reflect.ValueOf(arg), ...})

(reflect.Value).MethodByName()でいける。
メソッドも引数もいちいちreflect.Valueでラップする必要があるのがちょっと面倒。

http://play.golang.org/p/9tCV4xAI-l

define_method (関数, メソッドの定義)

stringで指定した名前で呼び出せる関数, メソッド名を定義できるか?

  • 関数: No
  • メソッド: No

関数のdefie_method

reflect.MakeFunc()によって関数を動的に作ることはできる()。このため、関数を返す関数を作るとある程度楽はできる。しかし結局は定義済みの変数にいれなければ参照できない、というか当たり前だけどコンパイル通らない。
なので、Rubyみたいに「stringで指定した名前で呼び出せる」関数は定義できない。

メソッドのdefine_method

最初から目的のメソッドの型のフィールドを用意しておけばそこに動的に生成した関数をいれることはできるが、stringからフィールドを定義できない以上はこれも無理。

const_get (任意の型のオブジェクトの取得)

stringで指定した任意の型をnewできるか?: No

任意型のnew

reflect.New()を使うと、何かしらの値のreflect.Typeからそのnewをとることはできる。 が、stringで指定した型のreflect.Typeを持ってくることができないので、無理。

まとめ

stringによるメソッド呼び出しはできるが、他のことはあまりできない。

reflectについて調べたい人用リンク

RubyとGoの思想を比較して思うこと

僕は主にRubyとGoを書く。この2つを使っていると、「(Rubyに比べて)Go辛いなあ」と思うことがあるのだが、なんで辛いのかはよくわかってなかった。 最近仕事にGoを使い始めたRubyistが社内ブログでGoのPros/Consを書いていたんだけど、そのConsも自分が辛い理由には該当しなかった。

今日Go ConferenceでRob Pike先生がGoの設計思想について話をしていて、思うところがあったので書いてみる。

書き方が1つだと冗長な書き方を迫られることがある

Robは、「すでにできることのための機能の追加は行わない。genericsはinterfaceとかぶるから、genericsを入れるならinterfaceを取り除き標準ライブラリを全てgenericsで実装し直す」 というようなことを言っていた。 この思想が徹底されることによりコードの可読性を担保できるという。

一方でこれは、「シンタックスシュガーや用途が限られたリッチな構文が導入されないこと」とのトレードオフだと思う。

文字列操作全般がつらい

文字列操作の比較表: RubyとGo言語という鵜飼さんの記事がある。

可読性は個人の好みだと思う(僕はRubyのほうが読みやすいです)が、書きやすさに関しては明らかにGoのほうが文字数が多く書くのに手間がかかる。
文字列操作をするのが本当に疲れるので、Webアプリケーションを作るのには向かないと思う。

書き方が1つだけだと読みやすい

一方で、方法が制限されていることにより得られるメリットもある。

Robは、「書き方がたくさんあると、どちらで書けばいいのか迷って時間を無駄にするし、読むときもなぜその方法が選ばれたのかわからない」というようなことを言っていた。 これは実際そうだと思っていて、Rubyだと書き方がたくさんあるので困るということが実際ある。

例えば、

def hoge_foo
  hoge.foo
end

def hoge_bar
  hoge.bar
end

のようなコードは

[:foo, :bar].each do |method|
  define_method(:"hoge_#{method}") do
    hoge.send(method)
  end
end

と書き直すことができる。

僕は多くの場合「前者のほうが可読性が高いと思っているので」意図的に前者の冗長な書き方をするし、 Ruby2.2になったら「Symbol GCが効くから」前者を使うべきみたいな人も出るかもしれないが、 これらの意図はコードを読んでも多分伝わらないので、なぜそういう書き方をしたのか考えるコストが発生する。 どちらの書き方も良い点悪い点あるため、その時の状況によって迷うことになる。

Rubyのコードレビューを見ていると、個人の書き方の好みや統一のための指摘が結構多くて、時間を無駄にしていると思う。

まとめ

みんな違ってみんな良い

Railsでデバイスの判定をするのに便利なgemを作った

rack-user_agent を作った

rack-user_agentという、User-Agentに応じていろいろな判定ができるメソッドを生やすRack::Request拡張を作った。
tagomorisさんのWootheeを使ってRack middlewareでUser-Agentをパースしておいて、 その結果に応じてrequestから簡単にいろいろな情報を得られるようにしてある。

たとえばRailsだとGemfileにgem "rack-user_agent"と書くだけで以下のように使うことができる。

class ApplicationController < ActionController::Base
  before_action :set_request_variant

  def index
    # example
    request.user_agent       #=> "Mozilla/5.0 (Linux; Android 4.3; Nexus 7 ..."
    request.device_type      #=> :smartphone
    request.os               #=> "Android"
    request.os_version       #=> "4.3"
    request.browser          #=> "Chrome"
    request.browser_version  #=> "29.0.1547.72"
    request.from_pc?         #=> false
    request.from_smartphone? #=> true
  end

  private

  # Action Pack Variants向け
  def set_request_variant
    request.variant = request.device_type # :pc, :smartphone, etc
  end
end

なぜ便利か

例えばwootheeでスマホかどうか判定したかったら、ApplicationControllerとかに

def request_from_smartphone?
  Woothee.parse(request.user_agent)[:category] == :smartphone
end

みたいなのを書く必要がある。けど、

request.from_smartphone?

で最初から書けたら便利だと思った。

Action Pack Variantsに使う

Ruby on Rails 4.1 Release Notes

Rails 4.1から、jsonやhtmlみたいなformatだけではなく、pcとかスマホのような条件によってテンプレートをスイッチできるようになった。
例えば、

app/views/projects/show.html.erb
app/views/projects/show.html+phone.erb

みたいなビューがあったとき、request.variant:phoneが入っていればshow.html+phone.erbが使われ、そうでなければshow.html.erbが使われる。 jpmobileとかでできるやつ。

ただ、このrequest.variantに何をセットするかはユーザーに委ねられていて、UAをパースして:phoneを入れたりするのは自分でやらないといけない。 そこを信頼できるUAパーサライブラリにまかせておけば、Railに乗ったまま楽にjpmobileみたいなことができて良いと思う。

作った背景、所感

最近触っているRailsアプリは、正規表現によるデバイスの判定を自前の正規表現で行っていた。
最近アプリ対応とかでその周りをいじることが多くなってきたんだけど、どんどん新しいデバイスが出てきてこれ保守するの無理だなと思った。
そこで、多言語に対応しているからいろんな人にメンテされそうなwootheeにUAのパースを任せることにした。

UAのパーサ保守するの大変なのでなるべく多くの人が保守してるやつを使って助け合いたい。

ISUCON4本戦に「railsへの執着はもはや煩悩」というチームで参加しました

f:id:k0kubun:20141109002503p:plain

@cnosuke@rkmathiと僕で学生枠で予選通っていたので、ISUCON4本戦出てきた。

予選で反省して直した点

悲惨な前回の記録

予選の時は常に精神的に余裕がなかった上にチームワークが全く成立していなかったので、@cnosukeに指揮をとってもらってお互いの作業が被らないようにし、 サーバー上で変更を加えるときは声を掛け合い、またチームの誰かが話してるときは落ち着いて聞くようにした。

前回はDevOps的なことに時間を大量に使っていたけど、普通に全員サーバー上で作業して手動デプロイするようにした。 @cnosukeと@rkmathiがインフラを見てくれていて、僕はアプリのコードの読み書きに集中できたのでよかった。

戦略

ファイルサイズの大きい動画配信だったので、何か去年と同じような問題なのかなーと思って全台の帯域を使い切ることを目標にした。 フロントは3台でリクエストを受け、動画は各サーバーにおいてそのホストにリクエストが行くようにして、それ以外の資源はコアが多いサーバーにおいて共有するようにした。

僕がやったこと

チームのリポジトリ: k0kubun/isucon4-final

時間 score やったこと
11:00 - リポジトリの準備。ここから2時間くらいじっくりコードを読み始める。
12:00 - めっちゃおいしい弁当食べた。
13:00 1344 みんなで戦略を立てた。僕がRedis周りのコードを改修することになったのでRedisがどう使われてるかじっくり読み始める。
14:00 1344 まだRedis周りのコード読んでる
15:00 4746 やっと僕の中でRedisをどう変えればいいか確信がもてたのでコードに触り始める。POSTされた時点で動画のリクエストを飛ばす先を固定する変更をデプロイしてベンチが通ることを確認。なんかインフラ側でVarnishの導入などで点が上がっている。
16:00 4746 1台のRedisを共有し、動画だけは各サーバーのRedisに置くという変更を入れ、3台に向けてベンチを走らせるように変更するも、FATALが出る。
17:00 4746 3台にリクエストを飛ばすとFATALになる理由がログだけ各サーバローカルになっていることに気づき、ファイルロックの代わりにMySQLを使うことにする。必要なスキーマ定義とインデックス張りをやった。
18:00 5922 ちゃんと動き始めたんだけどメモリが足りなくてRedisで死に、ここでやっと動画をRedisに入れなくした。余裕がなかったのでFile.writeしてsinatraでFile.readするだけ。うまく動いてからはローカルで8000〜12000くらいのベンチが出るようになったので、再起動確認をして提出することにした。

結局FATALが出まくってリモートは一度も高い点出せていなくて、実際最終スコアは5922になっていてそれほど高くならなかった。 でもFAILしなくてよかった。

感想

なんかみんなFAILすると思ってたのと、点差がめっちゃ開くと思ってたのに、みんなFAILしないで似たような点数だったのが面白かった。 1万点を超えるチームあんまいないと思ってたけど、1位がめちゃくちゃ高い点出してるの見て、実力の違いを感じた。

普段Railsのおかげでソース読むスピード上がっていて、Sinatraしんどかった。(煩悩)
複数台構成も動画配信も自分は普段やってないことなので、出題の意図を考える余裕すらなかったけど、現状できることはできた気がするのでよかった。 けど、正直学生相手には勝って学生賞をとって帰りたかったので、くやしい。

社会人相手にも勝って当然なくらい実力をつけられるようがんばります。
ISUCONとても楽しくて、参加できて本当によかったです。運営の皆様、本当にお疲れ様でした。

ISUCON4予選に参加しました

去年ISUCON3予選に参加していた id:cnosukeid:rkmathii のチームが一人欠けたので、 そこに初出場の僕が入り「railsへの執着はもはや煩悩の域であり、開発者一同は瞑想したほうがいいと思います。」というチーム名でISUCON4予選に出場しました。1日目の学生枠4位でした。

戦略

去年の本戦の結果とか見ているととにかくFAILするチームが多くて、FAILしなければ良い線行くと考えていました。提出用のインスタンスと各自の開発用インスタンスを立ち上げ、提出用は絶対FAILしない状態を保つようにしました。NewRelicのmost time consumingが一番高いactionを、時間のかかっている行から順に潰すという方針で臨みました。

僕がやったこと

rkmathiの記事もどうぞ。

時間 score やったこと
10:00 1411 newrelicいれた。デプロイとベンチを手元から走らせるスクリプト書いた。
11:00 1469 redisインストールした。なんかデプロイツールを使いやすくするのに時間を使った。
12:00 1638 redisを使ったフラグメントキャッシュをいれた。いろいろキャッシュしてみたがスコアが変わらない。
13:00 1620 loginで特に遅い2つのメソッドをキャッシュするようにした。 間違えて /report にinizializerを書き始めた。
14:00 1664 この辺でやっとデプロイツールが満足な感じになりいじるのをやめた。重いメソッドをフラグメントキャッシュにいれた。
15:00 FAIL rack-lineprofを入れ忘れていたのに気付き今更入れた。なぜかベンチが通らなくなっていることに気づき、問題を探し始めた。
16:00 FAIL 全員でrevertとかcheckoutしてデプロイしてベンチを繰り返した
17:00 FAIL 原因のプルリが見つかり修正したらチェッカーを通り突然まともな点数になった。再起動確認を繰り返し、動くことを確認しまくった。

要約すると、

  • 最初の4時間はデプロイ環境やミドルウェアの整備とフラグメントキャッシュの準備をした
  • 1時間は初期化の際に全部キャッシュに載せとこうとするも勘違いにより失敗
  • 残り3時間はFAILと戦っていた

1日目に参加したため、最終的に使われるスコアはまだわかりません。
v2配布後何度か試しましたが、インスタンスガチャでかなり大きく揺れたので結果が出るまで秘密で…

反省点

見るからにひどくて、言い訳はいろいろあるんですが、反省点は以下の通りです

  • チーム内での役割はDevなのにOpsに時間を使いすぎた
  • チームで練習してないことを本番でやるべきではなかった
  • ベンチはフィードバックが遅いのでブラウザでレスポンスタイムを見てチューニングするべき
  • レギュレーションとベンチの結果はよく読むべき

感想

実際参加してみると上位陣がいかにすごい人達かわかりました。競技中はずっと精神が極限状態にあり、すごく刺激的な1日でした。
技術系コンテストの中ではISUCONは僕の興味のど真ん中にある競技で、参加できて非常によかったです。この大会に参加させてくれたチームメンバーと、非常に大変な運営をこなされ大会を支えてくださっている運営チームの皆さんにはとても感謝しています。お疲れ様でした。

Rack applicationのプロファイリングにはrack-lineprofが便利

RubyKaigi 2014行った。良い発表がいろいろ聞けたんだけど、最近ISUCONに向けてwebアプリのチューニングに興味があったので特にfinal keynoteが興味深かった。
その中で紹介されていたtmm1/rblineprofが行ごとの実行時間を表示してくれるのでとても便利そうだったんだけど、GitHubではpeek/peek-rblineprofというRails用のプラグインでrblineprofを活用しているので、ISUCONでおそらく使用されるであろうsinatraでどうやって使うか考えていた。

kainosnoema/rack-lineprofというgemがその用途に便利そうだったので紹介したい。

使い方

rack-lineprofはRack middlewareで、まず以下のようにuseする必要がある。

require 'rack-lineprof'

class MyApp < Sinatra::Base
  use Rack::Lineprof
end

プロファイルする対象はクエリパラメータlineprofで指定する。

例えば、sinatraアプリをapp.rbで保存している場合、http://localhost:4567/?lineprof=app.rbにアクセスすると、コンソールに以下のようなプロファイリング結果が出る。

rack-lineprof

↑はISUCON3予選の初期状態なんだけど、どこがボトルネックになっているかが一発でわかって便利。

profile オプション

クエリパラメータに毎度プロファイリング対象を書くのは結構めんどくさいので、profileというオプションを作った。
Add profile option to specify profile target by k0kubun · Pull Request #1 · kainosnoema/rack-lineprof

これにより、以下のようにrack middlewareをuseするときのオプションでプロファイリング対象を指定することができる。

require 'rack-lineprof'

class MyApp < Sinatra::Base
  use Rack::Lineprof, profile: 'app.rb'
end

プロファイリング対象のファイルが固定の場合はこれを使うと便利。

まだgem pushされてないので、Gemfileは以下のようにする必要がある。

gem 'rack-lineprof', github: 'kainosnoema/rack-lineprof'

まとめ

Sinatraアプリのプロファイリングにはrack-lineprofが便利

ConoHaのVPSの初期設定をChef Soloでやってみた

普段はさくらのVPSに普通に手でソフトウェアをインストールしてWebアプリを動かしていたんだけど、 さすがにそろそろサーバーの状態管理を自動化したくなったのと、単純に面白そうなのでChefによるサーバー管理に挑戦することにした。
それで、気軽に壊して遊ぶ用に新たにConoHaのVPSを借りて、Chef Soloで設定してみた。

目標

以下の作業を全てChefで行う

  • 一般ユーザーの作成
  • 鍵認証の設定
  • sudo有効化
  • sshとiptablesの設定

下準備

knife solo init

まずサーバー上ではなくローカルで作業する。
knife solo initでchef soloのリポジトリを作る。

$ gem install knife-solo
$ knife solo init conoha-bootstrap-cookbooks
$ cd conoha-bootstrap-cookbooks

knife solo prepare

ConoHaは最初rootでsshできないようになっているが、それだとknife solo prepareできない。
ConoHaの共通サーバにsshした後rootでログインし、rootの~/.ssh/authorized_keysをいじって、共通サーバを経由せずssh root@conohaできるようにしておく。

その後、以下のコマンドでサーバーにChefをインストールする。

$ knife solo prepare root@conoha
Bootstrapping Chef...
...
Downloading Chef 11.16.0 for el...
Installing Chef 11.16.0
installing with rpm...
...
Thank you for installing Chef!
Generating node config 'nodes/conoha.json'...

一般ユーザーの作成、鍵認証の設定とsudo有効化

cookbookの作成

$ knife cookbook create site_user -o site-cookbooks

site_userというcookbookを作成した。
site-cookbooks/site_user/recipes/default.rb にレシピを書いて設定していく。

cookbookのrun_listへの追加

run_listに追加しないとcookbookは実行されない。

// nodes/conoha.json
{
  "run_list": [
    "recipe[site_user]"
  ]
}

templateの追加

site-cookbooks/site_user/templates/defaultにerbをいれて設定を変更する。

$ scp conoha:/etc/sudoers ./site-cookbooks/site_user/templates/default/sudoers.erb
  • sudoers.erb 修正
    • wheelをsudo可能に

recipeの修正

# site-cookbooks/site_user/recipes/default.rb

user_name = "k0kubun"
home_dir  = "/home/#{user_name}"
ssh_key   = "ssh-rsa AAAAB3Nza..."

# ユーザーと ~/ の作成
user user_name do
  home home_dir
  password "kogaidan"
  action :create
end

# ~/.ssh 作成
ssh_dir = "#{home_dir}/.ssh"
directory ssh_dir do
  owner user_name
  group user_name
  mode 0700
end

# ~/.ssh/authorized_keys 作成
# user_name でssh可能になる
file "#{ssh_dir}/authorized_keys" do
  owner user_name
  content ssh_key
  mode 0600
end

# wheelにuser_nameをいれる
group "wheel" do
  action :modify
  members [user_name]
  append true
end

# sudoersを手元のsudoers.erbで置き換える
template "sudoers" do
  path "/etc/sudoers"
  source "sudoers.erb"
  mode 0440
end

適用

$ knife solo cook root@conoha

これでssh k0kubun@conohaでのログインとsudoが可能になっているはず。
knife solo prepare k0kubun@conohaしておいて、次からはcookする際もk0kubun@conohaを使う。

sshとiptablesの設定

templateの追加

$ scp conoha:/etc/ssh/sshd_config ./site-cookbooks/site_user/templates/default/sshd_config.erb
$ scp conoha:/etc/sysconfig/iptables ./site-cookbooks/site_user/templates/default/iptables.erb

sshの設定

  • sshd_config.erb 修正
    • rootログイン無効化
    • パスワードログイン無効化
    • ssh用ポートを22番から変更

iptablesの設定

  • iptables.erb 修正
    • ssh用に開いている22番を別のポートに変更
    • HTTP 80番のポートを開ける
    • HTTPS 443番のポートを開ける

recipeの修正

以下を追加する。

# site-cookbooks/site_user/recipes/default.rb

# ...

# sshd_configを手元のsshd_config.erbで置き換える
template "sshd_config" do
  path "/etc/ssh/sshd_config"
  source "sshd_config.erb"
  mode 0600
end

# sshd_conig の反映のためにsshd再起動
service "sshd" do
  action :restart
end

# iptablesを手元のiptables.erbで置き換える
template "iptables" do
  path "/etc/sysconfig/iptables"
  source "iptables.erb"
  mode 0600
end

# iptables の反映のためにiptables再起動
service "iptables" do
  action :restart
end

適用

$ knife solo cook k0kubun@conoha

これで最低限のセキュリティ対策はできたと思う。

まとめ

Chef soloを用いてサードパーティのcookbookを使わないでサーバーの初期設定を行った。
慣れてきたり不便を感じたらBerkshelfのcookbookを使っていきたい。