俺の報告

RoomClipを運営するエンジニアの日報(多分)です。

GAで十分なはずなのにAPI使うのよ、なぜ? - 日報 #131

GAは僕の知る限りトップクラスに使い易く、かつ取得情報量が多い「無料の」アクセスログサービスです。
度肝抜かれるほど便利で充実したコンソールを使えば、
自分がコーディングしなければならないことなんて想像できないくらい複雑な情報を取得できます。
こんな汎用なアクセスログツールがあるのなら、
本腰入れたアクセス分析ならまだしも、
デイリーで眺める数値情報に困ることは無いはずです。
今でもそう思います。

しかし、
今僕はGAのAPIリファレンスを読みながら、
自前のDBにGAのデータを食わせるスクリプトを書いております。
(ここが分かりやすくオススメです。) なぜでしょう。
本当になぜこんな事態になってしまったのでしょう。
別に複雑な統計処理をかけたデータをデイリーで眺めたいわけでもなんでもないです。
でも、どうしても一旦DBに移してから自由にデータを取り出したくなってしまったのです。

なんででしょうか。
未だもってわかりません。
デイリーで眺めたいグラフがあったのですが、
それをGAのコンソールで描画することも勿論できます。
でもどうしてもDBにいれないといけないと思っているのです。
DBに入れて、目標の達成度とか、CTRとかCVRとか複数のバナーの効果の比較とか、
あとはレポートの出力とか、
そういうのを一画面で全部やりたくなったのです。
これ全部GAのコンソールでも実現可能っちゃ可能なんですよね。

なんでだろうなぁ。。。
本当に分からない、、、どうしてそんなものが「必要だ」と思っているのだろうか、、、
この気持分かる人、他にもいませんかね。
GAクラスの素晴らしいサービスに対して冗長なデータを保持するって、
本当に意味ないなぁと思うのですが、、、
不思議だなぁ…

CDNをさくらクラウドで構築する - N氏の日報 #2

今日は僕の隣に鎮座DopeしているN氏の日報です。
RoomClipにさくらでCDNを構築した敏腕エンジニアのN氏です。
全三回程度で、ウチにCDNが導入される様子をご覧頂きます。

では、はじまりはじまり。

- 「男のCDN烈伝」第1話 搾取され続ける人生 -

どうもこんにちわ。
最近ヒゲが伸びてきた俺氏の右隣にひっそりと座っているオッサンです。
俺氏とは色々あって同じpemファイルを使う関係になりました。

今回はCDNを作ってみたので、そのお話。


コンテンツデリバリネットワーク(Contents Delivery Network, CDN)とは、Webコンテンツをインターネット経由で配信するために最適化されたネットワークのことである。コンテンツ配信網とも。
http://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84%E3%83%87%E3%83%AA%E3%83%90%E3%83%AA%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF


RoomClipのインフラにはAWSを利用しておりまして、画像データはS3に保存する事にしています。
ですがこのS3、転送量ベースで課金されるため、 サービスの成長に比例して画像数やPVが増えてコストがシャレにならなくなってきます。

例えば、1日1万円かかるとして、 一年間、画像を表示するだけで365万円かかります。 その辺の若手プログラマーの年収より高いかもしれません。
やはり、S3に数百万というコストをかけるのは、貧乏性の人間にとっては割りに合わない話です。

そんな悔しい毎日から脱却したいと考えていた我々は、このCDNというものを作る事にしました。

RoomClipでは、画像データが主なコンテンツにあたりますので、 画像データを置いて、
その画像に対するHTTPアクセスを捌くサーバーを含めた一連のシステムが弊社で言うところのCDNとなります。
まず、S3の代わりに使うサービスを探します。
ネックになっているのは前述の転送量になりますので、S3みたいに、転送量ベースで課金されるサービスは使う事ができません。

色々と調べた結果、以下のメリットがあるさくらのクラウド(http://cloud.sakura.ad.jp/)でした。

  • ロードバランサーがある
  • サーバーをコピーできる
  • VPCが構成できる
  • ディスクが拡張できる
  • 転送量課金ではなく、時間での課金
  • 転送量課金ではなく、時間での課金
  • 転送量課金ではなく、時間での課金
  • 転送量課金ではなく、時間での課金
  • 転送量課金ではなく、時間での課金

それだけに止まらず、サービス的にも神対応だったんです。
我々も初めてだったので、試しにインスタンス一台作って起動してみたり、ディスク付けてみたり、ロードバランサーVPCをいじったりと、 色々と実現性についての試し打ちをする必要があったんですが、時間で課金されるのでなかなか思い切った事ができませんでした。

そんな時、さくらのクラウドさんが、初期テストについて色々と神レベルの配慮をしてくれたのです。
目に涙を浮かべながらコントロールパネルをいじくり倒しました。
これでS3の転送量を駆逐できそうです。 次回は具体的な実装方法について説明していこうと思います。


さて、次回がいつになるかはまだわかりませんが、 N氏のCDN構築をお楽しみに。

ApacheユーザでPHPからsudo権限のシェルをたたく - 日報 #130

さて話すことないので、
お互い(?)のためにもさっさと終わらせましょう。

注意深く使用すればPHPスクリプトからシェルを叩くというのはとても強力です。
Apacheモジュールで起動したPHPスクリプトから叩ければなお可能性は広がります。
もちろんセキュリティに関しては要注意で、
変数をコマンドに挿入することは極力避けて、実行コマンドは静的にすべきですが。

より強力な活用をするために、sudo権限というか、
apacheユーザに結構強めの権限を与えるシーンも出てくると思います。
これはとても躊躇することですが、
本当に限定的に使うのであればやってみたいものです。

ということで、その手順を。
超簡単ですが。
sudo visudo
これで、
apache-user-name ALL=(ALL) NOPASSWD: /hoge/hoge.sh
こんな感じのテキストを挿入するだけ。

もうちょい詳しく。

sudo visudo
ってのは
/etc/sudoers
に存在する設定ファイルをいじる専用のエディタです。
vimとかでいじっちゃうとsudo権限がなくなったままどーしようもなくなっちゃうので、
間違ってもファイルを直接いじらないようご注意。
で、
apache-user-name ALL=(ALL) NOPASSWD: /hoge/hoge.sh
これですが、書式としては下記の通り。

  • apache-user-name : これは付与権限が与えられるユーザ名(www-dataとかhttpdとか)。これはグループ名でもOKです。
  • ALL=(ALL) : 最初のALLはWhereのことでホスト名かIPアドレス。ちょっと複雑だけど、sudoersをどのホストからでも共有する場合はALLでいい。2番目のALLはAs_Whomのことで、誰の権限で?ということを記述すr。指定しなければrootになるので最強権限だが、ALLとかいておけばまず間違いなく誰かの権限で通過できるはず。
  • NOPASSWD: /hoge/hoge.sh : これがWhatの部分。起動させたいコマンドを書く。これをALLにしちゃうとちょい危険なので、特定のものにしておくとよいと思います。

という感じ。
ホスト名のところとか結構きわどいけど、多くのサンプルでALL指定なので、
深く考えずにならっております。

あぁ、明日も元気出していこう。

意思決定フレームワークでいいのない? - 日報 #129

どんなレイヤーでも意思決定はありますよね。
戦略決定フェーズはもちろん、 実装フェーズでも意思決定の連続なわけです。
そして「よりよい」意思決定のためには、情報が必要です。
この意思決定に必要な情報の「適度な」量と質がイマイチ分かりません。

例えば、掲示板機能を実装しようとした時を考えます。
調べたら例えばこんな選択肢があることが分りました。

  1. 掲示板サービスをレンタルする
  2. 掲示板スクリプトを自分のサーバに設置する
  3. 掲示板スクリプトフルスクラッチで作って設置する

とにかく今掲示板機能が欲しければ1番で済むかもしれません。
多少コストを割いてもいいなら2番を選択したほうが、後からカスタマイズが出来るかもしれません。
ふんだんにコストを割いて、とにかく改変しまくれる3を選択したほうが、最終的な開発コストは一番安上がりかもしれません。

または1番でとりあえず開始して、
その様子を見ながら2番で軽くモックを作りはじめ、
ガッツリ準備が整ったら3番みたいな段階的な採用もあるかもしれません。

もちろんその時の目的で選択肢は変化すると思います。
例えば、どんな目的なら1番は最も適当になるでしょうか。
一見して「すぐ掲示板が欲しい!」という目的に対して最適な気もしますが、
コストをかけていいなら2番でも3番でも高速に手に入る可能性はあります。
どちらかというと、「今月だけ掲示板機能が欲しい」という目的に対して1番は沿っているように見えます。 ですが、「とりあえず今月だけ掲示板機能が絶対欲しいが、うまくいくようならずっと続けてもいいし、今月やる中でも色々改変したい」
みたいな欲求になったら2番が良さそうにみえてきたりもします。
なんだかよくわかりません。
かけられるコストと、得られる見込みのあるメリットのバランスで、、、などと考えたところで、
結局そのコストとメリットの「実体」を具体的に記述しろと言われると困るのが僕です。

開き直って、目的に優先度を付与して、それぞれの目的達成のためのコストとメリットを各選択肢ごとに数値化、
そしてその行列を眺めて最もROIが良い物を選択する!みたいなことをしみてもいいですが、
その優先度を付ける意思決定の情報がまた必要になります。
「どっかでは決めの問題や!」と威勢よくしてみてもいいですが、
それならその決めの問題の人が最終判断をすれば良いと思うので、コストだとメリットだのは参考程度というわけです。

僕は多数ある情報から重要な情報をフィルタリングすることが本当に苦手です。
逆に言うと要らない情報を捨て去ることがとても不得意です。
だから過不足なく表現できるシンプルな状態がとても落ち着きます。
意思決定においても、過不足なく表現できるシンプルな状態に持ち込むことさえできればいいのになぁという思いになります。
なんかそういう意思決定フレームワークみたいなもの、ないかなぁ。。。
面倒だからもう書くの辞めようなぁ。。。

CodeigniterのPDOドライバで安全にDB再接続 - 日報 #128

有名なフレームワークを使っていて、
なんか変だなぁと思う時は大体自分が変な時なんですが、
極稀に「やっぱり変だなぁ」ということもあります。
今日はそんな箇所があったので少しご報告。

バージョン2.2.1がリリースされ、さらに3.0もRCとなってにわかに活気づいているCodeigniterですが、 バージョン2.1系を使用し続けている方も沢山いらっしゃると思います。
本日はsystem配下のファイルに、「泣く泣く」追加コードをいれてしまった事件について共有をいたします。

諸々の事情にて、MySQLへのコネクションタイムアウトを小さめの値にしていると、
バッチ処理などにおいて困った自体になることがあります。
サービスフロントのコードなら1秒かかるような処理なんて見当たらない「はず」ですので、
どうwait_timeoutを小さくしていても滅多なことがない限り1処理中でコネクションロストすることなんてありません。
ですが、そうでないようなシーンというのも勿論多々あるわけで。
コネクション確立をしてから計算ノード側で数十秒たってから、改めてクエリリクエストを投げるなんてことも、
まぁないわけではないんですね。
その際、コネクションが正しく存在しているのかを安全に確認しなければなりません。
CodeigniterにはDB_driverみたいなクラスにreconnectメソッドが用意されており、
あたかも任意のタイミングで再接続できるようになっております。
ですが、PDO_driverの場合、どうやらそうでもないようでした。
少なくとも僕の環境(バージョン 2.1.4)では、reconnectしても特に何も起こりませんでした。

ということで、よくよくSysytemディレクトリ配下のPDO_driverクラスを読んでみると、
確かにreconnect関数は「別段何もしてない」ように読めます。
これは困った。
というか、、、そもそも、コネクションの確認作業自体は、クエリ実行の直前で、暗黙的に呼ばれているべきです。 そうでなければ安全にクエリ実行ができません。
さらに言えば、確実にコネクションが確立されていると考えられる時は、無意味に接続確認をしたくはありません。
無駄な負荷になる可能性が高いからです。
そこのところを暗黙的に上手くやってくれるのがDB_driverだと思っていたのですが、、、
んー…

ということで、本当に仕方なく、嫌々、DB_driver.phpを改編することにしました。
やることは2つです。

  1. 実際のクエリ実行の直前で、きちんとDBコネクションが確立されているのかを確認する
  2. コネクション確立は無闇矢鱈に行わない

ということで、このような仕様にします。

  1. コネクションが確立されているかチェックするpingメソッドを作成する
  2. コネクションが無ければ、明示的にクローズし、再接続を試みる
  3. 以上の処理を一定の時間間隔で行う

3番が厄介そうですが、
まぁ「最後に接続が確認されてから、次のクエリが走る直前までに3秒以上かかっていたら、接続確認を改めて行う」
という程度にしておけば、まぁ機能は果たすでしょう。

ということで、まずpingメソッドの実装です。

<?php
/**
* Ping to the sql connection handler.
* @return bool
*/
function ping()
{
  if ( ! $this->conn_id)
  {
    return FALSE;
  }
  try
  {
    $result_id = @$this->_execute('SELECT 1');
    $driver = $this->load_rdriver();
    $RES = new $driver();
    $RES->conn_id = $this->conn_id;
    $RES->result_id = $result_id;
    $result = $RES->result();
    if ( ! $result)
      return FALSE;
    else
      return TRUE;
    }
    catch (Exception $e)
    {
        return FALSE;
    }
}

コネクションハンドラは$this->conn_idに保存されているので、
それに対してSELECT 1を実効するという方式です。
あとは、クエリ実行の直前でこのpingを放つコードを挿入します。

<?php
     // メンバ変数に書きを追加
     var $last_act_time  = 0; // 最後にアクション(クエリ実行または接続)
     var $active_timeout = 3; // タイムアウト秒数

...

          // コネクションを確立した直後にlast_act_timeを代入する
          // Connect to the database and set the connection ID
          $this->conn_id = ($this->pconnect == FALSE) ? $this->db_connect() : $this->db_pconnect();
          // 追加 start --------------------------
          $this->last_act_time = time();
          // 追加 end --------------------------

...

     // 既に存在しているはずの simple_queryメソッドに対して、コード追加
     function simple_query($sql)
     {
          // 追加 start ---------------------------
          $execute_time = time();
          if ( ! $this->last_act_time)
          {
               $this->last_act_time = $execute_time;
          }
          if ( ! $this->active_timeout)
          {
               $this->active_timeout = 3;
          }
          if (($execute_time - $this->last_act_time) > $this->active_timeout)
          {
               if ( ! $this->ping())
               {
                    $this->close();
                    $this->initialize();
               }
          }
          $this->last_act_time = $execute_time;
          // 追加 end ---------------------------

...

こうしておけば、接続してから、またはクエリを実行してから3秒後に再びクエリを実行しようとした時に、
安全に接続確認、再接続が実行されます。
多分。
systemフォルダに手を加えるのは全力でやらないほうがいいことなので、
特に必要ない限りやらないほうがいいと思いまうす。
ですが、まぁ世界の何処かに困っている人がいれば、、、ということで共有いたします。
とにかくreconnectメソッドに頼るのはちょっと避けたほうがいいかもね、という話でした。

PHPでの日付計算はDateTimeで - 日報 #127

コーディングしている最中に、
どこでストレスを感じますか?
ストレスって言っても「こんな書き方しやがって!」的なものではなくて、
苦手意識というか、心理的ハードルが高くなる瞬間のことです。

例えば、僕は正規表現でのmatchパターンを書く時は結構ストレスを感じます。
「あぁ…だれか他に書いている人のをコピペしたい。。。」とすぐに思ってしまいます。
あと、沢山変数を使ったループ文を書くときも、変数の数の最適化ばかり気になって、
命名がぐちゃぐちゃになり、結局上手にかけないという苦手意識が働いちゃいます。

で、date周りの演算も結構苦手な領域の1つです。
2015-03-10の2日前はいつでしょう?って言われた時に、
時間ベースか日付ベース、どっちで引き算すればいいんだろう、、、
って本気で悩んでしまうタイプなのです。
しかもPHPの場合は特に警戒心が高まります。
というのもstrtotime関数が安心できぬ奴だからです。
https://www.google.co.jp/search?q=strtotime+%E3%83%90%E3%82%B0
日付周りの足し引き処理に苦手意識がある僕にとって、
表向き便利なのに、ちょいちょいバグがある関数ってのは本当に厄介です。
なので自分ではstrtotimeの使用を禁じています。
できるだけ安全にPHP上で日付計算をするためDateTimeクラスを使っています。
そしてどんな単純な日付オペレーションでも関数化しちゃいます。
怖くてしかたがないので、、、

例えば、2つの日付stringがあったとき、その差分を計算するときは、
こんな分かりづらいヘルパー関数とかも作って置いておいたりします。

/**
* recommended fromat : %a (day), %s (sec)
* @see http://php.net/manual/ja/dateinterval.format.php
* @param string $base
* @param string $opponent
* @param string $format
* @return number
*/
function date_dif($base,$opponent,$format='%a')
{
  $base_time=new DateTime($base);
  $opponent_time=new DateTime($opponent);
  $interval=$opponent_time->diff($base_time);
  return (int)$interval->format($format);
}

こんな周りくどいやりかたじゃなくて、秒数にして引いて、日付に戻せばいいじゃんとか思うと思いますが、
それでもこうやっちゃうくらいなんか苦手です。

皆さんもありませんかね、、、
ある特定の瞬間だけすごい「苦手だなぁ」という思いが強くなることって。。。
まぁ、、、雑談なんですけどね。

td-agentのlogをslackで監視 - 日報 #126

続々と日本中から花粉が集まっております。
集合場所は俺の粘膜だそうで、
村を上げての大祭りです。

さて、本日はFluentd先生のお話。

せっかくfluentd使ってログ取得しているんだから、
td-agent自体のログもfluentdで回収したいものです。
というより、fluentdで生じたエラーに対して俊敏に反応したいものです。
そうじゃないと大事なログをロストしたり、大量のログ吐き出しによってディスク容量が圧迫されたりしちゃうかもしれないですしね。

ということで、td-agent.logを何とか監視したいものです。
td-agent.logをtailしてみているcronスクリプトみたいなのでもいいですが、
fluentd自体で自らのログをタグ付けで拾う機能があったはずなので、
それを使ってもっと簡単にやってみます。

おおまかな流れとしては、
自分のhostに対してtd-agentログを送信して、
自分で受信する。
その後適当にフィルタ条件を設定して、
気になるものだけをslackで投げる。
的な感じにしてみます。
素晴らしいプラグインたちのお陰で、案外簡単です。

使うプラグイン達は4つくらいです。
まずは送信元fluentdが複数個あることを想定して、
レコードを個別にするためrecord-modifierを使用します。
その後、大量にきちゃうことを想定して、
それらをsuppressを使って一旦まとめます。
最終的にそれらメッセージをslackプラグインで送信します。
slackのサービスはIncoming WebHooksを使います。
ということで、まずプラグインを全部インストールしましょう。

# install fluent-plugin-record-modifier
fluent-gem install fluent-plugin-record-modifier

# install fluent-plugin-suppress
fluent-gem install fluent-plugin-suppress

# install fluent-plugin-slack
fluent-gem install fluent-plugin-slack

# install fluent-plugin-filter
fluent-gem install fluent-plugin-filter

その後、config設定します。
例えばこんな感じ。

# for logging td-agent
<match fluent.**>
  type record_modifier
  tag internal.message

  host ${hostname}
  include_tag_key
  tag_key original_tag
</match>

<match internal.message>
  type       filter
  all        allow
  deny       message: /^detected rotation of/, message: /^following tail of/, message: /^out_forest plants new output/
  add_prefix filtered
</match>

<match filtered.internal.message>
  type              suppress
  interval          10
  num               2 
  attr_keys         host,message
  remove_tag_prefix filtered.
  add_tag_prefix    slack.suppressed.
</match>

<match slack.**>
  type buffered_slack
  webhook_url https://hooks.slack.com/services/xxxx/xxxx
  team xxxx
  channel %23fluentd_warn # %23 means '#'
  username xxxx
  color danger
  icon_emoji :fluentd:
  buffer_path /tmp/td_slack_buffer
  flush_interval 5s
</match>

これでfluent-catでわざとエラーを発生させてslackが飛ぶかを確認しましょう。
これでfluentdが何か不思議な挙動をしたときは、把握できるようになります。
あぁ安心。