NginxとLuaを用いた動的
なリバースプロキシでデ
プロイを 100 倍速くした
サイボウズ株式会社
深谷敏邦
#devsumi [19-G-6]
1
自己紹介
• 深谷 敏邦
• 2012年サイボウズ株式会社入社
• インフラチーム Hazama 所属 (2012/09~)
• お仕事
• デプロイツールの作成
• MySQL HA 環境の構築
• Apache のデバッグ・パッチの作成
• Nginx を使ったリバースプロキシの構築
2
1. イントロダクション
2. Apache から Nginx へ
3. 実装について
4. 結果とまとめ
3
cybozu.com とは
• サービスとして
• サイボウズが運営するクラウドサービス
• 2011 年 11 月サービスイン
• 開始から 4 年で導入社数 9000 社以上
4
クラウド基盤としての cybozu.com
• 1000 台規模の物理マシン、数倍の VM
• 物理機材から全て自社構築
• 1億リクエスト/日
• 契約ユーザーライセンス数29万人以上
5
サービス概要
• お客様ごとにサブドメインを発行
• 契約内容に従って複数のサービスが同ドメインで利用可能
6
fukaya-coop.cybozu.com
サブドメイン毎の処理
• アプリケーション振り分け
• IP 制限
• Basic 認証
• クライアント証明書
7
LB
APAP
fukaya-coop.cybozu.com
/o//k/12.34.5.6
サブドメイン毎の処理の裏側
• 1 サブドメインにつき 1 設定ファイルを用意
• 読み込みのたびに全ファイルのロードが必要
• サブドメインが増えるとロード処理にも時間がかかり….
8
サブドメイン数×設定変更にかかる時間
9
150
170
190
210
230
250
270
290
2/17/2014 3/17/2014 4/17/2014 5/17/2014 6/17/2014
Apache 再起動時間
設定変更に
時間かかり過ぎ
秒
日付~サブドメイン数
2.5分
5分
ミッション
• サブドメインの設定変更を 1秒で完了させる
10
1. イントロダクション
2. Apache から Nginx へ
3. 実装について
4. 結果とまとめ
11
これまで
• LB として Apache 2.2 を利用
• Apache を普通に使うと
• サブドメイン毎に設定ファイルが必要
• 設定ファイルを安全にリロードするには再起動が必要
12
仕組みの転換
• 1サブドメインの変更のためにすべての設定を読み込む
13
• アクセス毎に対応するサブドメインの設定を動
的に読み込む
実際どうやるか?
• さすがに自力で全部作るのは厳しい
• 候補に挙がったのは以下の 2 つ
• Apache 2.4
• nginx
• 今の機能を実現できるのは最低ライン
• パッチを当てすぎると本家の変更に追随できなくなる
• そうだ Luaがあるじゃないか!
14
ということで比較してみる
機能 ◎
(当然)
○
(何とかいけそう)
Lua
△
(experimental)
◎
(実績十分)
性能
○
(event mpm)
◎
(完全イベント駆動)
15決定
nginx と lua-nginx-module
• nginx はイベント駆動型の HTTP サーバ
• 2015年1月現在シェア 14%
• 大量のリクエストの処理が得意
• lua-nginx-module は HTTP リクエスト処理を lua で書ける拡張
モジュール
• cybozu.com で必要な設定項目がおおよそ実現できる
• 出来ないものは C で書く ☺
16
1. イントロダクション
2. Apache から Nginx へ
3. 実装について
4. 結果とまとめ
17
nginx の設定の基本
• location
• リクエスト URI に応じて設定を変えるためのディレクティブ
18
location / {
root /www/default;
}
location /hoge/ {
root /www/hoge;
}
• / にアクセスした場合は、
/www/default からコン
テンツを探す
• /hoge/ にアクセスした
場合は、/www/hoge か
ら探す
internal redirect
• location 間を移動する仕組み
• 処理が異なる複数の location を組み合わせる事ができる
19
location /
location /login/
location /proxy/
nginx
location /maintenance/
メンテナンス中 初回アクセス
アプリケーションサーバへ
ログイン後
lua-nginx-module
• HTTP 処理の様々なタイミングで lua で記述した任意のコード
を実行出来る
• https://github.com/openresty/lua-nginx-module
• 可能なこと
• HTTP ヘッダの読み書き
• location に対するリクエストの発行
• internal redirect の実行
20
データベースファイルの読み込み
• リクエスト毎にサブドメインに対応する設定をデータベース
ファイルから読み込む
• key: value の簡単なテキスト形式
• lua を使って Host ヘッダを元にローカルファイルを読む
21
nginx
hagi
hagi.cybozu.com
fukaya-coop.cybozu.com
sato-shoji.cybozu.com
fukaya
-coop
sato-
shoji
データベースファイルの読み込み実装例
22
location / {
rewrite_by_lua ‘
path = (“/settings/” .. ngx.var.host)
ret = ngx.location.capture(path)
ret.body...
‘;
}
location /settings/ {
internal;
root /var/settings;
}
lua による設定読
み込み用location
DB ファイル配信
用location
データベースファイルの読み込み実装例
23
location / {
rewrite_by_lua ‘
path = (“/settings/” .. ngx.var.host)
ret = ngx.location.capture(path)
ret.body...
‘;
}
location /settings/ {
internal;
root /var/settings;
}
• Host ヘッダを元に
DB ファイルパスを
作成
データベースファイルの読み込み実装例
24
location / {
rewrite_by_lua ‘
path = (“/settings/” .. ngx.var.host)
ret = ngx.location.capture(path)
ret.body...
‘;
}
location /settings/ {
internal;
root /var/settings;
}
• Host ヘッダを元に
DB ファイルパスを
作成
• DB ファイルを取得
するリクエストを
投げる
データベースファイルの読み込み実装例
25
location / {
rewrite_by_lua ‘
path = (“/settings/” .. ngx.var.host)
ret = ngx.location.capture(path)
ret.body...
‘;
}
location /settings/ {
internal;
root /var/settings;
}
• Host ヘッダを元に
DB ファイルパスを
作成
• DB ファイルを取得
するリクエストを
投げる
• レスポンスボディ
から設定値を取得
アクセス毎に変更すべき設定
1. 利用アプリケーション
2. IP アドレス制限
3. Basic 認証
4. クライアント証明書による認証
26
1. 利用アプリケーション
• 各サブドメイン毎に利用可能なアプリケーションが異なる
• 申込時に選択
• 後に追加・削除が可能
• lua から以下の制御を行う
• 利用不能なアプリケーションへのアクセスは拒否する
• 利用可能なアプリケーションへのアクセスは適切なアプリケーション
サーバにリバースプロキシする
27
nginx
hagi.cybozu.com
fukaya-coop.cybozu.com
garoon
AP
kinton
eAP
g
k
利用アプリケーション動的化実装例
28
location / {
rewrite_by_lua ‘
if use_garoon then
ngx.exec(“/proxy/garoon”)
end
if use_kintone the
ngx.exec(“/proxy/kintone”)
end
ngx.exit(ngx.HTTP_NOT_FOUND)
‘;
}
location /proxy/garoon {
proxy_pass http://garoon-ap/;
}
location /proxy/kintone {
proxy_pass http://kintone-ap/;
}
lua によるアプリケー
ション利用可否と振り分
けロジック
リバースプロキシ用の
location
利用アプリケーション動的化実装例
29
location / {
rewrite_by_lua ‘
if use_garoon then
ngx.exec(“/proxy/garoon”)
end
if use_kintone the
ngx.exec(“/proxy/kintone”)
end
ngx.exit(ngx.HTTP_NOT_FOUND)
‘;
}
location /proxy/garoon {
proxy_pass http://garoon-ap/;
}
location /proxy/kintone {
proxy_pass http://kintone-ap/;
}
• 予め DB ファイルの内容を lua
の変数に読み込んでおく
• アプリケーションが利用可能な
らリバースプロキシ用の
location に internal redirect す
る
利用アプリケーション動的化実装例
30
location / {
rewrite_by_lua ‘
if use_garoon then
ngx.exec(“/proxy/garoon”)
end
if use_kintone the
ngx.exec(“/proxy/kintone”)
end
ngx.exit(ngx.HTTP_NOT_FOUND)
‘;
}
location /proxy/garoon {
proxy_pass http://garoon-ap/;
}
location /proxy/kintone {
proxy_pass http://kintone-ap/;
}
• 予め DB ファイルの内容を lua
の変数に格納しておく
• アプリケーションが利用可能な
らリバースプロキシ用の
location に internal redirect す
る
• 利用可能なアプリケーションが
なければ 404 エラーをクライ
アントに返す
2. IP アドレス制限
• 標準モジュールではアクセス毎動的に設定を変更することが出
来ない
• lua で IP アドレス制限を実装した
31
• 192.168.0.1
• 192.168.0.2
• 172.16.0.1
fukaya-coop
192.168.0.1
192.168.0.3
IP アドレス制限の実装
32
function authenticate_remote_addr(allows)
local bit = require("bit")
local remote_addr = ngx.var.binary_remote_addr
local x0, x1, x2, x3 = string.byte(remote_addr, 1, 4)
local ip
ip = x0 * 16777216
ip = x1 * 65536 + ip
ip = x2 * 256 + ip
ip = x3 + ip
for i, allow in ipairs(allows) do
if bit.band(ip, allow[2]) == bit.tobit(allow[1]) then
return true
end
end
return false
end
IP アドレス制限の実装
33
function authenticate_remote_addr(allows)
local bit = require("bit")
local remote_addr = ngx.var.binary_remote_addr
local x0, x1, x2, x3 = string.byte(remote_addr, 1, 4)
local ip
ip = x0 * 16777216
ip = x1 * 65536 + ip
ip = x2 * 256 + ip
ip = x3 + ip
for i, allow in ipairs(allows) do
if bit.band(ip, allow[2]) == bit.tobit(allow[1]) then
return true
end
end
return false
end
nginx の内部変数からクライア
ント IP アドレスを取得する
IP アドレス制限の実装
34
function authenticate_remote_addr(allows)
local bit = require("bit")
local remote_addr = ngx.var.binary_remote_addr
local x0, x1, x2, x3 = string.byte(remote_addr, 1, 4)
local ip
ip = x0 * 16777216
ip = x1 * 65536 + ip
ip = x2 * 256 + ip
ip = x3 + ip
for i, allow in ipairs(allows) do
if bit.band(ip, allow[2]) == bit.tobit(allow[1]) then
return true
end
end
return false
end
データベースファイルから読み込んだ
許可リストと比較してアクセス可否を
判定
3. Basic 認証
• 標準モジュールでリクエストごとにパスワードファイルを変更
することが出来る
• ただし認証自体は全ドメインにかかってしまう
• 一方認証自体を利用するか否かはサブドメイン毎に設定できる
• 認証を利用するかどうかを lua で制御するようにした
35
nginx の リクエスト処理
• nginx ではリクエストはいくつかのフェイズを通って処理され
る
• Basic 認証は Access フェイズで実施される
36
rewrite フェイズ
access フェイズ
content フェイズ
Basic 認証
Basic 認証のスキップ
• Basic 認証を利用しない場合 lua を使って Access フェイズより
前に internal redirect する
• 後のフェイズが実行されないので Basic 認証が行われなくなる
37
rewrite フェイズ
access フェイズ
content フェイズ
lua でスキッ
プ
Basic 認証の実装例
38
location / {
rewrite_by_lua ‘
if not use_basic_auth then
ngx.exec(“/contents/”)
end
‘;
auth_basic “closed site”;
auth_basic_user_file /var/passwd/$host;
content_by_lua ‘
ngx.exec(“/contents/”)
‘;
}
location /contents/ {
internal;
root /var/www;
}
basic 認証用 location
実コンテンツ用
location
Basic 認証の実装例
39
location / {
rewrite_by_lua ‘
if not use_basic_auth then
ngx.exec(“/contents/”)
end
‘;
auth_basic “closed site”;
auth_basic_user_file /var/passwd/$host;
content_by_lua ‘
ngx.exec(“/contents/”)
‘;
}
location /contents/ {
internal;
root /var/www;
}
rewrite フェイズ
access フェイズ
content フェイズ
Basic 認証の実装例
40
location / {
rewrite_by_lua ‘
if not use_basic_auth then
ngx.exec(“/contents/”)
end
‘;
auth_basic “closed site”;
auth_basic_user_file /var/passwd/$host;
content_by_lua ‘
ngx.exec(“/contents/”)
‘;
}
location /contents/ {
internal;
root /var/www;
}
• basic 認証を利用しない
場合は /content/ に直接
飛ばす
Basic 認証の実装例
41
location / {
rewrite_by_lua ‘
if not use_basic_auth then
ngx.exec(“/contents/”)
end
‘;
auth_basic “closed site”;
auth_basic_user_file /var/passwd/$host;
content_by_lua ‘
ngx.exec(“/contents/”)
‘;
}
location /contents/ {
internal;
root /var/www;
}
• basic 認証を利用する場
合は rewrite フェイズで
は何もしない
• access フェイズではサ
ブドメイン毎にパス
ワードファイルを切り
替える
Basic 認証の実装例
42
location / {
rewrite_by_lua ‘
if not use_basic_auth then
ngx.exec(“/contents/”)
end
‘;
auth_basic “closed site”;
auth_basic_user_file /var/passwd/$host;
content_by_lua ‘
ngx.exec(“/contents/”)
‘;
}
location /contents/ {
internal;
root /var/www;
}
• basic 認証を利用する場
合は rewrite フェイズで
は何もしない
• access フェイズではサ
ブドメイン毎にパス
ワードファイルを切り
替える
• content フェイズで実コ
ンテンツ用 location に
移動する
4. クライアント証明書による認証
• クライアント証明書は SSL/TLS 接続で利用できる認証方法
• サーバ証明書とは逆に HTTP サーバがクライアントに証明書を
要求する
• クライアントはサーバが持つ CA が発行した証明書を送信する
必要がある
• cybozu.com ではサブドメイン毎に CA を作成
43
LB
クライアント証明書を要求
証明書を送信
証明書を確認
クライアント証明書認証の実装上の課題
• 標準モジュールではアクセス毎に CA を切り替えるという器用
なことは出来ない
• SSL/TLS プロトコルでの認証なので lua では実装できない
• lua-nginx-module は HTTP レベルの処理しか書けない
• nginx に直接パッチを行った
44
クライアント証明書認証の実装の概要
• サーバはクライアント証明書の発行者を動的に呼び出して証明
書を検証
• ただし他のサブドメインの証明書でも認証が通るためその証明
書がアクセス先のサブドメインのものかどうかチェックが必要
45
LB
クライアント証明書を要求
証明書を送信
証明書を確認
CA
クライアント証明書に
対応したCAを取得
SNI について
• SNI は TLS 拡張の一つで TLS ハンドシェイク時にアクセスした
い FQDN をサーバに渡すこと
• 名前ベースバーチャルホストでも FQDN 毎に証明書を変えることが出
来る
• クライアント証明書認証ではサーバは受け付けるクライアント
証明書の CA の DN を送信することが出来る
• 複数のクライアント証明書を持っている場合ユーザーの証明書選択が
楽になる
46
LB
この CA が発行した証明書を送ってね
証明書を送信
fukaya-coop.s.cybozu.com にアクセスしたいよ
SNI 対応
• パッチでは SNI で送信された FQDN に従って CA をルックアッ
プするようにした
• CA は FQDN をファイル名とした通常の PEM ファイルとして保存
47
LB
この CA が発行した証明書を送ってね
fukaya-coop.s.cybozu.com にアクセスしたいよ
fukaya-coop.s.cybozu.com
その他おこなったこと
• LB を複数サーバにした場合の問題
• 各 LB 毎に SSL ハンドシェイクを行うので、iOS だと証明書選択ダイ
アログ複数回でてしまう
• LB 全体で状態を共有できない
1. 複数サーバー間での SSL セッションキャッシュ共有パッチ
2. DoS 対策用同時リクエスト制限モジュールの開発
• yrmcds のセマフォを利用
• いずれも OSS として公開予定
48
1. イントロダクション
2. Apache から Nginx へ
3. 実装について
4. 結果とまとめ
49
適用結果
•5分かかっていた設定の反映が1~2秒で終わるようになった
50
0
50
100
150
200
250
300
設定リロード時間
適用前 適用後
秒
100倍以上高速化
パフォーマンスについて
• リクエストごとに lua を実行するためパフォーマンスの劣化が
懸念されたが問題なかった
51
count count
log10(response time[μs]) log10(response time[μs])
適用前のレスポンスタイムの分布 適用後のレスポンスタイムの分布
nginx 化のメリット
• 大量のコネクションを扱えるので keepalive 秒数を伸ばした
• 15秒から75秒に
• 送信トラフィックが顕著に落ちた
• 処理したリクエスト数は減っていない
• サーバ証明書の送信量が減ったことが理由
• 中間証明書を合わせると3~4KB
52
適用前トラフィック 適用後トラフィック
まとめ
• 従来は LB への設定の反映に5分かかっていた
• nginx + lua を使いサブドメイン毎に設定を動的に読み込んだ
• アプリケーションの利用可否
• IP 制限
• Basic 認証
• クライアント証明書認証
• 新しい LB の実装は設定の反映が1秒になった
53

NginxとLuaを用いた動的なリバースプロキシでデプロイを 100 倍速くした