ngx_mruby/mod_mruby でロードバランサ (LVS) を操作する、あるいはその他のユースケースについて

--------------------------------------------------------------------------------------

どうも、最近めっきりサーバにログインすることが減り、コードを書く機会が増えてきた [@rrreeeyyy](http://twitter.com/rrreeeyyy) です。

この記事は [mod_mruby ngx_mruby Advent Calendar 2014](http://qiita.com/advent-calendar/2014/mod-ngx-mruby) 7 日目の記事です。
公開が遅れてしまってすみません。

ngx_mruby/mod_mruby、この Advent Calendar の今までの記事や今後の記事で、その便利さがとても伝わってくるかと思います。
やはり HTTP のリクエスト/レスポンスをプログラマブルに操作できると言う所が大変魅力的だな、と個人的に感じています。
(他にも nginx/apache で同一のコードが流用できるような設計や、処理速度に配慮した内部実装は大変素晴らしいと感じていますが長くなるので省略します。)[1]

また、最近では [30d.jpでの導入事例](http://www.hsbt.org/diary/20141113.html) が公開されるなど、
実際のサービスでの導入事例も公開されてきました。(他にも何社か実サービスに投入している、という噂を耳にしています。)

さて、プロダクション環境では Nginx の上位にロードバランサを置くこともあるかと思います。
Nginx 自体もメール・HTTP のプロキシは出来るのでそれを使ってもよいのですが、
今回は Nginx 以外のロードバランサと ngx_mruby を組み合わせて使用する例について書いてみます。

---

## ngx_mruby/mod_mruby と LVS (mruby-ipvs を用いた手法)

Linux のロードバランサとして、L7 なら HAProxy, UltraMonkey, 特に HTTP なら Nginx, Apache 等を使用することが多いかと思います。
しかし、内部トラフィックが気になり DSR 構成にしたかったり、パフォーマンスを少しでも向上させたい時に、L4 でのロードバランサとして、
Linux カーネルに組み込まれている LVS を使用したいケースもあるかもしれません。

ここでは、LVS を mruby から操作できる mruby-ipvs と、mruby-redis, そして mruby-ngx を組み合わせて、
Nginx 起動時に LVS に組み込まれて、更にコネクション数が一定に達した場合に LVS から切り離されるような構成を考えてみましょう。

```
                             +------------------+
                             |                  |
                          +--+ Nginx(ngx_mruby) |
                          |  |     10.0.0.2     |
                          |  +------------------+
+-----------------------+ |                      
|                       | |                      
| LVS(mruby-ipvs)+Redis +-+                      
|       10.0.0.1        | |                      
+-----------------------+ |                      
                          |  +------------------+
                          |  |                  |
                          +--+ Nginx(ngx_mruby) |
                             |     10.0.0.3     |
                             +------------------+
```


まずは、ngx_mruby 側の設定です。

```ruby
:
http {
    mruby_init_code '
        userdata = Userdata.new "redis_data_key"
        userdata.redis = Redis.new "10.0.0.1", 6379
        userdata.key = [Nginx::Connection.new.local_ip, Nginx::Connection.new.local_port].join(':')
        userdata.redis.lpush('real_servers', key)
        userdata.redis.hset(key, 'weight', ENV['weight'] || 1)
    ';


     location /mruby-test {
         mruby_content_handler /usr/local/nginx/hook/conns.rb;
     }
:
```

```/usr/local/nginx/hook/conns.rb``` は以下のようなコードです (HttpStubStatusModule が必要です)。

```ruby
r = Nginx::Request.new
a = r.var.send(:connections_active)
if a.to_i > (ENV['conns_threshold'] || 100) then
  userdata.redis.hset(Nginx::Connection.new.local_ip, 'weight', 0)
else
  # some healthy codes...
end
```

ngx_mruby 側はこれだけです。

次に、mruby-ipvs 側を見ていきます。

[Keepalived の DSL Example](https://github.com/rrreeeyyy/mruby-ipvs/blob/master/example/keepalived.rb#L1-L79) の 1 行目から 79 行目まではそのまま使うので、省略します。

DSL として、LVS サーバ側で ```mruby-ipvs.rb``` をこんな感じで書きます。

```ruby

userdata = Userdata.new "redis_data_key"
userdata.redis = Redis.new "10.0.0.1", 6379

virtual_server("10.0.0.1:80"){
  lb_algo "wrr"
  lb_kind "NAT"
  protocol "TCP"

  healthcheck {
     loop.do
       sleep 5
       real_servers = userdata.redis.lrange('real_servers', 0, -1)
       next if real_servers.empty?
       real_servers.each do |rs|
         real_server(rs) {
           weight userdata.redis.hget(rs, 'weight')
         }
       end
     end
  }

  end
}

Keepalived.start
```

として、LVS サーバ側で ```mruby mruby-ipvs.rb``` を実行します。

このタイミングでは、Web サーバ側の Nginx は起動していないので、振り分けが無い状態でスタートします。
その後、Web サーバ側で順次 Nginx を起動すると、LVS サーバ側の Redis に起動した Nginx の IP:PORT が記録されていきます。
LVS サーバ側では、5 秒に 1 度 Redis から情報を取り出し、取り出した情報を基に LVS への情報登録、重みの更新を行っています。
コネクション数が一定を超えた場合には重みが 0 になるため、既存のコネクションは維持されますが、新規接続のコネクションは当該のサーバには振り分けられません。

このようにすることで、Nginx を起動すれば勝手に LVS に登録され、コネクション数が一定を超えると weight を 0 にするようなシステムを作ることが出来ます。

---

## ngx_mruby/mod_mruby で考えられるその他のユースケース

...これだけではアレなので、今回の Advent Calendar を書くにあたって他に考えたネタを紹介してお茶を濁そうと思います。
ngx_mruby/mod_mruby ならこんなこともできちゃうんじゃないかなーと思います。

### ngx_mruby/mod_mruby と ELB

- 本記事では LVS との連携をメインにしましたが、当然 ELB とも連携が可能だと思います。
- mruby には WebAPI を叩く mrbgem があるので、コネクション数が一定を超えたらインスタンスを clone して、ELB に登録するといったオートスケールを自前で実装することも出来ると思います。

### ngx_mruby/mod_mruby と [mruby-oauth](https://github.com/matsumoto-r/mruby-oauth) を組み合わせた OAuth 認証付き Web サーバ
  
- basic 認証や digest 認証を OAuth で実現する
- 特定のパスに対してのリクエストに対して、Twitter や Google アカウントでの認証が掛けられそう
- 認証済みでない場合は /login 等にリダイレクトする
- Google のメールアドレスや Twitter のアカウントを yaml で管理し、アカウント名やメールアドレス、直近のツイートによって振り分けを変えられそう
- [gate](http://unknownplace.org/archives/gate.html) のような事ができそう

### ngx_mruby/mod_mruby と [mruby-redis](https://github.com/matsumoto-r/mruby-redis) を組み合わせた REST API サーバ
- Redis の DB や Key, Hash に対応した URL へのアクセスを ngx_mruby/mod_mruby でマッピングしてあげれば良さそう
    - ```/api/v1/<DB名>/<KEY名>.json``` へアクセスが来たら、Redis から当該 DB/KEY を取得し json で返す
    - 存在しない場合は 404 にリダイレクトする
    - ``` location ~ ^\/api\/.*\.json$ ``` に ```mruby_content_handler``` をセットしてあげれば良さそう
    - handler の中では ```Server::Request.new.var.arg_path.split('/') ... ``` みたいな感じでパスを取得する
    - 取得したパスから Redis でキーを取得
    - みたいな実装を考えてちょっと実装しましたが面白いか自信がなかったのでボツになりました
- [openrestyでの実装](http://tech.3scale.net/2013/01/09/augment-your-api-without-touching-it/) はちょっと違うけどこんな感じですね
- MongoDB の [DrowsyDromedary](https://github.com/zuk/DrowsyDromedary) とかに近い感じです

### ngx_mruby/mod_mruby による画像変換 Proxy

- mgem がない気がするので完全に妄想ですが、例えば ImageMagick の mgem があったら、画像を変換して返すことも出来そうです。
- [openrestyでの実装](http://qiita.com/mah0x211/items/8870d7d1063f3d754076#2-7) が mruby でも出来るといいですよね

### ngx_mruby/mod_mruby と [mruby-v8](https://github.com/mattn/mruby-v8/) による JavaScript minify Proxy サーバ

- Rails の asset pipeline みたいなのが Web Proxy サーバ側で出来るとすごそうですよね

と、こんな感じで、[mgem-list](https://github.com/mruby/mgem-list) を見ると色々な可能性を秘めていることが分かります。
また、mrbgem は意外と簡単に作れ、特に C 言語で実装されたアプリケーションとの連携はとてもやりやすいので 
Web サーバでこんなこと出来たら面白そう！というものがあれば、mrbgem を作ってみるのも面白いかなと思います。

---

## まとめ

- ngx_mruby とロードバランサを組み合わせて使ってみる例を紹介
- ngx_mruby のユースケースの妄想を紹介

明日は mookjp さんの "Dockerコンテナを作るプロキシを作った話" です。なにこれめっちゃわくわくする。

---

- [1]: 是非 1 日目の記事 [mod_mrubyとngx_mrubyの最新のアーキテクチャとリファレンス公開](http://hb.matsumoto-r.jp/entry/2014/12/01/095842) や論文等をお読み下さい。
- [2]: 素晴らしい記事でした。