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への執着はもはや煩悩」というチームで参加しました
@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:cnosuke と id: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
にアクセスすると、コンソールに以下のようなプロファイリング結果が出る。
↑は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を使っていきたい。