Android アプリ「AppNetBlocker」を公開しました
AppNetBlocker は、所定のアプリから「完全なインターネットアクセス」の許可を除去するツールです。実行に root 権限は必要ありません。Android 1.6 以上の環境で動作します。興味のある方はご利用下さい。もちろん無料です。
(2011/12/26 追記)
本アプリは、現時点では安全面において不安要素の少なくない Android をめぐる状況において Android 利用者が自分自身を守るためにとり得る対策のひとつを形にしたものであり、他者の権利を脅かすことを目的とするものではありません。
もし、Android を今よりもさらに安全に利用することが可能となればより多くの利用者・開発者の利益につながることでしょう。本アプリはたとえ僅かでもその一助になればと手がけたものであり、開発の動機もそこにあります。
しかしながら、一部の方から本アプリと Android マーケット規約とのかねあいを懸念するご指摘がありました。その話題については判断の余地があるものと認識していますが、少なくとも利用者の不安を誘引することはまったく本意ではなく、マーケットでの配布という形態は一時中断することとします。
実験用のいわゆる「野良アプリ」として apk のダウンロードリンクを当面残しておきます。このリンクから端末へ直接インストールすることはできません。意図を理解される方のみ自己責任でご利用下さい。
[ AppNetBlocker.apk ]
md5sum [CC8104C9DDE44AD308F09FF22B551575]
AppNetBlocker とは?
Android 端末上のデータを狙うマルウェアの問題が取り沙汰されていることもあり、アプリに付与された「許可」の内容は何かと気になります。 特に、それがネットワークアプリやバナー広告を表示するアプリではなく、また、機能面でインターネットへのアクセスが必須とは考えにくい内容のアプリであるにもかかわらず「完全なインターネットアクセス」許可を持っている場合は悩ましいですね。 そんな時には AppNetBlocker が役に立つかもしれません。 続きを読む
KLab Advent Calendar 2011 「DSAS for Social を支える技術」
12/1: MySQL を PDO で使うときは ATTR_EMULATE_PREPARES を設定しよう
12/2: クエリキャッシュは切ったほうがいいんじゃなイカ?
12/5: SHOW FULL PROCESSLIST を使った MySQL のプロファイリング
12/6: JavaScriptでつくる量子コンピューター
12/7: DSAS for Social での MySQL のボトルネックと今後の方針
12/8: ソーシャルアプリのボトルネック調査例(strace編)
12/9: php のプロセス数を絞ろう
12/12: 高負荷でも安定したサービスを提供するためのリバースプロキシ
12/13: 過負荷をかわす Apache の設定
12/14: Apache の並列数を CPU コア数に応じて決定する
12/15: SHOW PROCESSLIST を使ったカジュアルなプロファイラを強化しました
12/16: DSAS環境でのDNS活用法 〜ネットワーク設定の格納にDNSを使う〜
12/19: DSAS環境でのDNS活用法2 〜tinydns-get活用術〜
12/20: トラフィックが急増! ボトルネックを退治しよう〜
12/21: Ajax開発のテストツールとしてのPython
トラフィックが急増! ボトルネックを退治しよう〜 【設定編】
DSR構成のレシピ
まずは、設定項目をおさらいしておきましょう。次の6つでした。- LVS の負荷分散の設定をDSRに変更する(ipvs の設定)
- Webサーバが、DSRなリクエストパケットを扱えるようにする(iptables の設定)
- Webサーバを、outer VLAN に参加させる(L2 スイッチの設定)
- Webサーバが、outer VLAN において通信できるように設定する(VLAN 用インタフェースの追加)
- Webサーバにおいて、上位ルータへの通信経路を設定する(ルーティングと iptablesの設定)
- Webサーバが、上位ルータに対して応答パケットを送信できるように設定する(ARP エントリの設定)
LVS の負荷分散の設定をDSRに変更する
ipvs で負荷分散する場合、LVS マシンに到達したクライアントからのパケットを Webサーバへと転送する方法には、3種類あります。1つ目は LVS 上で NATして転送する方法、2つ目は届いた IPパケットをカプセル化して転送する方法、3つ目が DSRです。 ipvs の仮想サービスの設定で DSR を指定するには、は、ipvsadm コマンドで設定する場合でしたら --masquerading (-m)(= NAT)や --ipip (-i)(= カプセル化)の代わりに --gatewaying (-g) を指定します。 keepalived を使っているのでしたら、仮想サービスの設定で lb_kind オプションに DRを指定します。
Webサーバが、DSRなリクエストパケットを扱えるようにする
DSR 構成を採った場合、LVS から Webサーバに転送されてくるパケットは前回説明したように、送信先IPアドレスが LVSの仮想サービス用のアドレス(図では A.B.C.D)のままになっています。
このパケットを、Webサーバの OSが受け取ったときに自分宛のパケットだと認識させるためには、追加の設定が必要になります。方法はいくつかありますが、DSAS for Social では Webサーバ上で NAT する方法を使っています。つまり、送信先IPアドレスが A.B.C.D であるパケットを受け採った場合、Webサーバ自身がそのパケットの送信先アドレスを自分のアドレス(ss.tt.uu.vv)に書き換えるのです。そうすれば、そのパケットは Webサーバの OSが自分宛のパケットだと理解して、Apacheなどのアプリケーションに渡してくれるようになります。
設定方法は、OSに依って変わりますが、Linux の場合、次のようにするのが簡単です。
# iptables -t nat -A PREROUTING -p tcp --dport 80 -d A.B.C.D -j REDIRECTこれで、クライアントから LVSを経由して Webサーバに届いた宛先アドレス=
A.B.C.Dなパケットが、Webサーバ自身で処理されるようになります。
Webサーバを、outer VLAN に参加させる
ここからは、Webサーバが直接上位ルータに対して応答パケットを渡すために必要となる設定になります。そのためには、まずは Webサーバが "outer VLAN" に参加しないと始まりません。その際 "inner VLAN" と "outer VLAN" が混ざらないように、どちらかの VLAN には tag VLAN として参加させる必要があります。DSAS for Social では、"outer VLAN" 側を、tag VLAN にして参加させています。
Webサーバを "outer VLAN" に属させるためには、L2 スイッチの設定が必要になります。どのような設定になるのかはスイッチによって変わってきますが、DSAS for Social で主に使っている hp社の Procurve シリーズでは、例えば次のようにします。
# config (config)# vlan 4 (vlan-4)# tagged 10-20これは、"outer VLAN" の VLAN番号が 4、Webサーバ(群)が接続されているポートのポート番号が 10番〜20番ポートの場合の例です。
Webサーバが、outer VLAN において通信できるように設定する
Webサーバが "outer VLAN" に対して通信するには、Webサーバ側でも追加設定が必要になってきます。というのも、先ほどのスイッチの設定において、 Webサーバは tag VLAN で "outer VLAN" に参加するようにしたので、Webサーバがスイッチと "outer VLAN" 向けのパケットをやりとりするときは、VLAN tag をつけてるようにしなければならないのです。Linux において tag VLAN を扱うためには、VLAN ID に対応した仮想的なネットワークインタフェース(NIC)を作成してやります。この仮想的な NIC は物理的な NIC にひもづけられます。仮想 NIC はどの物理 NIC に対してひもづけるかと言うと、当然ながら先ほどスイッチの設定で "outer VLAN" に参加させたポートに接続している NIC に対してになります。

Linux において、tag VLAN 用の仮想 NIC を使うためには、8021qドライバモジュールが必要となります。カーネルを手元でコンパイルする際にこのドライバを含めるには
CONFIG_VLAN_8021Q CONFIG_VLAN_8021Q_GVRPの2つのオプションを有効にしてください(モジュールにしても組み込みにしても、どちらでも構いません)。
物理NICに紐づいた tag VLAN 用の仮想 NIC を作成するには、vconfig コマンドを使います。
# vconfig add eth0 4 # ip link set eth0.4 upこれで、Webサーバが "outer VLAN" 上でパケットをやりとりする準備が整いました。tcpdump などで eth0.4 を観察すれば、broadcast パケットなどが流れる様子が観察できるはずです。
Webサーバにおいて、上位ルータへの通信経路を設定する
Web サーバが直接上位ルータとパケットをやりとりするための環境は整いました。しかしこれだけでは OS は Webサーバの応答パケットを上位ルータに渡してはくれません。ルーティングを設定する必要があります。そして、前回の要件であげたように、default gateway を変更すること無く、DSRしたいパケットだけ上位ルータに対してルーティングするように設定する必要があります。
これを実現するためには、Linux のポリシールーティング機能を使います。これは、様々な条件に基づいてルーティング設定を切り替える機能で、Linux の他の機能の例に漏れず非常に柔軟な設定をすることができます。今回は Netfilter の mangle テーブル上で DSRしたいパケットに対してマーキングを行い、そのマーキングしたパケットに対してのみ適用する DSR専用のルーティングテーブルを、通常のルーティングテーブルとは別に作成しすることで、DSRを実現します。
Netfilter の mangle テーブルと、ポリシールーティング機能を使うためには、それぞれカーネルの機能を有効にする必要があります。mangle テーブルを使うには、モジュールがすでにある場合は iptable_mangleモジュールを読み込みます(iptables コマンドで mangleテーブルを触れば自動的にロードされます)。無ければ CONFIG_IP_NF_MANGLEを有効にして、カーネルを作り直してください。ポリシールーティング機能はカーネルモジュールにはできず、組込みにしなければなりません。お使いのカーネルの config を見て、CONFIG_IP_MULTIPLE_TABLES が y になっているか、確認してください。y になってなければ、カーネルを作り直す必要があります。

カーネルの機能を有効にしたら設定していきましょう。まずは、DSRしたいパケットに対してマーキングをします。
# iptables -t mangle -A OUTPUT -s A.B.C.D -j MARK --set-mark 4
-s オプションで DSRしたいパケット=ソースアドレスが A.B.C.D のものを指定します。そして -j MARK --set-mark 4 でマーキングを施します。最後の 4 は DSRしたいパケットにつける識別ID になります。この後に設定するポリシールーティングにおいて、どのパケットに対して DSR用のルーティングテーブルを適用するのかを、この IDを使って指定します。
ポリシールーティングのための設定は、次の様になります。
# ip route add H.I.J.K/32 dev eth0.4 table 100 # ip route add default via H.I.J.K table 100 # ip rule add prio 100 fwmark 4 table 100 # ip route add H.I.J.K/32 dev eth0.4まず、デフォルトのルーティングテーブルとは別の、DSR用のルーティングテーブルを作成します。このテーブルの名前をここでは "100" にしています。最初の2行はこの "100" というルーティングテーブルに対して、ルーティング情報を追加しています。つまり、上位ルータ(アドレス=H.I.J.K)とは eth0.4 という NICを通じてやりとりできることを示し(1行目)、インターネットに対してパケットを送信するときは、その上位ルータを中継すればよいことを指定(2行目)します。
3行目は、先ほど Netfilter の mangle テーブルにて DSRしたいパケットに対してマーキングした 4というIDを手がかりにして(fwmark 4 の部分です)、新しく作った "100" というルーティングテーブルを参照するよう、指示しています。つまり、mangle テーブルで 4 という ID を付与されたパケットは、3行目の指示にしたがって、1,2行目で新しく作った特別なルーティングテーブルを参照して、行き先が決定されるようになります。
Webサーバが、上位ルータに対して応答パケットを送信できるように設定する
さて最後に、上位ルータと Webサーバがパケットをやりとりする上で必要な、ちょっとした設定を追加します。先ほどは、IP上での(L3的)上位ルータとのやりとりする経路の設定をしました。通常はこれだけで問題なく上位ルータと Webサーバはやりとりができるのですが、今回の設定例では Webサーバの VLAN インタフェースに IPアドレスを振らなかったため、ちょっとした小細工が必要になります。どういうことかというと、上位ルータの MACアドレス=Ethrnetのアドレスが、このままでは Webサーバには分からないのです。通常は通信相手の MACアドレスは、 ARPプロトコルを使って自動的に取得されるのですが、Webサーバは上位ルータと直接通信するために必要な、同一のサブネットに属したIPアドレスを持っていないので、ARPプロトコルが使えないのです。しかたがないので、上位ルータの MAC アドレスは、人間が手動で与えてやることにします。やり方は、上位ルータの MACアドレスが hh:ii:jj:kk:ll:mm だとすれば
# arp -s -i eth0.4 H.I.J.K hh:ii:jj:kk:ll:mmとなります。
以上で、Webサーバが DSRするための設定が完成しました。これでもう負荷分散機がボトルネックになることはありません。
Ajax開発のテストツールとしてのPython
KLab Advent Calendar 2011 「DSAS for Social を支える技術」 の15日目です。
最近はソーシャルゲームの開発案件も増えてきました。Android/iOSアプリの開発をはじめとしたAjax通信の事例が数多くなり、開発スタイルも様変わりしつつあります。以前はWebサイト・Webアプリ構築の場合はPCや携帯のブラウザをリロードしながらだったのが、スマートフォンが加わりつつあります。
スマートフォンにおいてはさらにアプリ開発が大きな意味を持ちつつあります。携帯時代にもJavaアプリやBREWアプリなど多様なアプリがありましたが、スマートフォンアプリでの違いは、サーバとの通信を非常に多く行うようになったことです。クライアントアプリは多くの場合自前かもしくはWebViewを用い、通信はAjaxが使われます。リクエストもGETだけでなくPOSTを使う事も多くあり、ブラウザだけでサーバ開発・デバッグを進めるのは厳しい事が多くあります。
そんなわけで今回、Ajax通信を行う簡単なテストツールがほしいと思って少々探してみたのですが、「Selenium」というブラウザベースのツール以外見当たらないようでした。私が欲しかったのは単に一回のAjaxリクエストとレスポンスのそれぞれ引数と返値を検査して、想定通りの処理がされてるかどうかを見たかったのですが、今回の用途にはオーバースペックでした。
最初はサーバの動作確認にはwgetやcurlといったコマンドラインツールを使用していました。しかし基本的な疎通確認やプログラムがエラーなく動作するかどうかくらいには使えても、Ajaxレスポンスが複雑になってくるにつれてレスポンスのJSONの値が本当に正しいかどうかの判定はできません。目視での確認も限界があります。クライアントアプリが送った認証要求を正しく判別してOK/NGを返すかどうかとか、ゲームロジックとして正しい値を返すかどうか、などなど。開発が進むにつれてAjaxサーバの行う処理も複雑化しますので、早めにダミークライアント、サーバテスト用のスクリプトが是非欲しいところです。
Pythonにはユニットテストモジュールunittestが標準ライブラリとして組み込まれています。さらにHTTP通信を行うurllib2やjsonモジュールも標準ですので、これらを組み合わせればAjaxサーバの開発を強力に援護することができます。
import urllib2
import json
import unittest
session = 'xxxxxxxx'
url = 'http://foo.bar/apps/?p={"session_id":"%s","action":"%s","params":"%s"}'
class TestAjaxProcess(unittest.TestCase):
def test01(self):
f = urllib2.urlopen(url % (session, 'hoge_action', 1))
result = json.loads(f.read())
print result
# Ajaxレスポンスには { "result":"ok", "data":{"response_code":1, ... }} のような内容を想定
self.assertEqual(result['result'], 'ok')
self.assertEqual(result['data']['response_code'], 1)
def test02(self):
f = urllib2.urlopen(url % (session, 'hoge_action', 2))
result = json.loads(f.read())
print result
# Ajaxレスポンスには { "result":"ng", "data":{"message": }} のような内容を想定
self.assertEqual(result['result'], 'ng')
self.assertEqual(result['data']['message'], '"params" invalid')
if __name__ == '__main__':
unittest.main()
下は動作例です。
$ python tester.py
{ u'result':u'ok', u'data': {u'response_code": 1, u'value':100, u'extra_comment': u'hogehoge' }}
{ u'result':u'ng', u'data': {u'message': u'"params" invalid' }}
..
----------------------------------------------------------------------
Ran 2 tests in 0.160s
OK
標準ライブラリが充実したスクリプト言語であれば、このようにAjaxテストスクリプトは容易に組めます。素早い実装柔軟な変更を求められる開発現場において、こうしたAjaxテスト環境を揃えておくことは、今後さらに重要度が増す事でしょう。
トラフィックが急増! ボトルネックを退治しよう〜
このシリーズも、初めは専らアプリケーション寄りの話題でしたが、ここ二回ほどはインフラ寄りの話題でした。今日はさらに(OSIの7階層モデルにあてると)下寄りの話題になります…。できるだけ分かりやすく書くつもりですので、お付き合い頂ければと思います。
負荷分散機がボトルネック
さて、DSAS for Social ではいくつかのアプリケーションが動いているわけですが、では1つのアプリケーションがピーク時に使う帯域はどれくらいになるか、皆さん想像がつきますでしょうか。答えはもちろんアプリケーションによって全く変わるのですが、今まで記録した中での最大値は、2Gbps を越えました。これは、サーバに搭載されている NIC の能力を越えています。もちろん、1台の web サーバでこのトラフィックを全て捌いたわけじゃないのですが、実は DSAS の構成上、どうしてもこのトラフィックが集中する箇所があります。それは、負荷分散機の部分です。
なぜ負荷分散機がボトルネックになるのか
DSAS for Social では、負荷分散機に、Linux 上に実装された L4 負荷分散システムである、IPVS を使っています(以下、この IPVS が動作するマシンのことを、LVSマシンと呼びます)。そして LVS マシンは、DSAS for Social におけるルータの役割を兼用しています。つまりどういうことかというと、DSAS for Social における外部との相互通信は、全て LVS マシンを経由する、ということです。図で書くと、次のようになります。

LVS マシンは、負荷分散機だからと言って、特別なマシンを使っているわけではありません。Webサーバと同じものを使っています。これは LVS マシンがこわれた場合、隣に並んでいる Webサーバを使って LVS マシンの代わりをさせることができるように、という考えに基づいてこうしています。ですので、LVS マシンに搭載されている NIC(Network Interface Card) も、普通の 1Gbps のものです。DSAS for Social 以前の DSAS では、LVS が捌かなければならない帯域が 1Gbps を越えることは無く問題にはなりませんでした。しかしながら冒頭にも書いたように、DSAS for Social では 1つのアプリケーションが 2Gbps の帯域を消費するケースも出てきます。そのため、LVS がネットワーク通信に置いてボトルネックになってしまいました。
ボトルネックを解消するには
このボトルネックを解消するには、いくつかの方法があります。例えば NIC をもっと広帯域のものに変える、あるいは複数の NIC を束ねて帯域を太くする、などです。しかし、いずれの方法でもハードウェア的な変更が必要となるため、LVS用マシンが特別なマシンとなり、故障時に web サーバを代わりに充てる、ということが手軽にできなくなってしまいます。ということで、ハードウェアを拡張すること無く、どうにかできる方法を考えてみました。
DSAS for Social における L2ネットワークは、どうなっているのか
さて先ほどの図は DSAS for Social の IP ネットワーク = L3 ネットワークの図でした。これがどのような Ethernet のネットワーク = L2 ネットワークの上に載っているかというと、次のようになっています。

インターネットの回線=上位ルータ(データセンタが管理している)は、直接 LVS に接続せずに一旦 L2 スイッチに収容しています(これも、Webサーバを LVSマシンの代替機にし得るようにするための工夫の1つです)。このスイッチには Webサーバや DBサーバも接続されているため、内部のネットワークと外部のネットワークが混在しないように、VLANを使って分離しています。LVS は両方の VLAN に接続し、それぞれの間を IP レベル(L3レベル)で橋渡し(ルーティング)しています。
クライアントからの Webサーバに対する通信は、次のような経路を通ることになります。

つまり、外部のネットワークから送られてくるパケットは、図にある outer のVLANを通って LVS に到達し、LVS が Web サーバへと渡します。Webサーバからの応答パケットは、この逆順をたどります。
この図を見ていると、何となく、Webサーバからの応答パケットが LVS を経由してクライアントへと送信されいていることが、無駄なように思えてきます。なぜわざわざ LVS を経由させているかというと、外部との通信が全て LVS を経由するこの形であれば、通信でトラブルが発生した場合でも、LVS 上で全ての状況を観察できるため、運用上都合がよいからです。
どうすれば、ボトルネックを解消できるか
しかしながら、どうしても LVSの部分が通信のボトルネックになってしまいます。ですので DSAS for Social では、トラフィックの多い一部のアプリケーションに関しては、次の図のように、Webサーバからの応答パケットが LVS を経由しないような通信経路に切り替えることにしました。いわゆる、DSR(Direct Server Return)構成というやつです。

これでもまだクライアントからのリクエストパケットは、LVSを経由しているので、ここがボトルネックになるんじゃないかと思われるかもしれません。しかしながら Webアプリケーションにおいて、そのトラフィックのほとんどは Webサーバからの応答パケットによるものです。クライアントからのリクエストパケットに要する帯域は応答パケットのものに比べると微々たるものです。冒頭で紹介した最大 2Gbpsを記録したトラフィックも、応答パケットのものです。ですので、今回の問題はこの構成で解決できるのです。
解決策のまとめ
以上、長い前フリが終わったところで、要件をまとめておきましょう。
- ipvs の設定と Webサーバの動作を、DSR 構成にする
- HTTP リクエスト以外の通信に関しては、これまでどおり LVS を経由して通信する
DSAS for Social におけるDSR構成の作り方
では要件が明確になったところで、次に設定するべき項目をあげていきましょう。
- LVS の負荷分散の設定をDSRに変更する(ipvs の設定)
- Webサーバが、DSRなリクエストパケットを扱えるようにする(iptables の設定)
- Webサーバを、outer VLAN に参加させる(L2 スイッチの設定)
- Webサーバが、outer VLAN において通信できるように設定する(VLAN 用インタフェースの追加)
- Webサーバにおいて、上位ルータへの通信経路を設定する(ルーティングと iptablesの設定)
- Webサーバが、上位ルータに対して応答パケットを送信できるように設定する(ARP エントリの設定)
それぞれどういうことか、簡単に説明していきましょう。
DSR 構成をとるためには
まず DSR な負荷分散構成を採る場合、Webサーバに届くリクエストパケットは、負荷分散機などにおいて何も手を加えられてないものになります。どういうことかというと、(Webサーバに届いた)リクエストパケットに記述されている送信先IPアドレスは、いわゆる仮想サービス用のアドレスになります。つまり、LVS へとパケットが届けられたときに送信先アドレスとして使われたグローバルアドレスそのままです。これはWebサーバに割り振られた(プライベート)IPアドレスとは別物です。Webサーバはこのパケットを受け入れて、かつ、応答を返信する際は送信元アドレスとして同じグローバルアドレスを記述して送信しなければいけません。これを解決するのが (b) の設定になります。
(c) と (d) に関しては特に難しい問題は孕んでいません。ちょっと変わっている点は、(d) で作る VLAN 用のインタフェースに、IPアドレスを設定しないことくらいです。しかし、IPアドレスを振らずにすませるために1つ小細工が必要になります。それが (f) です。
(e) は、要件の 2 を実現するための設定です。今回 Webサーバのルーティングテーブルに設定するデフォルトゲートウェイのアドレスは、LVS のIPアドレスのまま変更しません。その上で DSR の応答パケットのみルーティングを切り替えて、上位ルータに渡すようにしたいのです。そのための小細工が必要になります。
さて、ではいよいよ設定です… といきたいところですが、紙面がつきました(笑)ので、続きは明日に…
DSAS環境でのDNS活用法2 〜tinydns-get活用術〜
KLab Advent Calendar 2011 「DSAS for Social を支える技術」 の13日目です。
先週に引き続き、DSAS 環境での DNS 活用法を紹介します。
スクリプト中でのゾーン参照方法
DSAS で使用している各種スクリプト内で、DNS の情報を参照する際に使ってるコードを紹介します。
# 設定情報用のゾーン(.dsas)を検索
mzone ()
{
R="$1";
( cd $INTERNALZONE 2> /dev/null;
_zone TXT $R.dsas )
}
# 名前解決を行う
# tinydnsのゾーンファイルのコピーがあればtinydns-getを使用
# ゾーンファイルがなければDNS問い合わせを行う
_zone ()
{
if [ -r data.cdb ]; then
DNSCMD="_zonedjb";
else
DNSCMD="_zonedig";
fi;
if [ -z "$2" ]; then
Q="A";
R="$1";
else
Q="$1";
R="$2";
fi;
$DNSCMD $Q $R
}
_zonedig ()
{
dig +short $1 $2 | sed 's/^\"\(.*\)\"$/\1/'
}
# tinydns-getを使い、カレントディレクトリのdata.cdbを検索
_zonedjb ()
{
tinydns-get $1 $2
}
mzone() と呼んでいる関数では、前回紹介した TXT レコードによる設定データを参照します。
この他に、通常の内部ホスト名の名前解決を行う pzone() や、そのグローバル版の gzone() 等を定義しています。
各サーバでは、起動スクリプト中でこれらの関数等を使い、設定ファイルを生成しています。
下記に抜粋したコードでは、この関数を使い、keepalived.conf の VRRP に関する設定を生成しています。
lan=$(mzone net.private)
lan=$(pzone lvs)/${lan#*/}
wan=$(mzone net.link)
wan=$(mzone lvs.link)/${wan#*/}
cat <<EOF
vrrp_instance VI {
state BACKUP
interface bond0
garp_master_delay 5
virtual_router_id $(mzone lvs.vrid)
priority ${PRIO}
nopreempt y
advert_int 1
authentication {
auth_type PASS
auth_pass ********
}
virtual_ipaddress {
${lan} dev bond0
${wan} dev bond0
}
}
EOF
パラメータ部分を DNS に逃すことで、生成スクリプトの環境依存部分を極力減らせるほか、複数のスクリプトから参照している設定情報を1箇所で管理することができるようになります。
オフライン時の内部ゾーン参照
DSAS 環境では、多くの設定ファイルを同様の方法で起動時に生成しています。
ネットワークインタフェースはもちろん、resolv.confや、全サーバのローカルで動いているDNSキャッシュサーバの設定ファイルも起動時に生成しているため、これらの生成スクリプトは、ネットワークが使えない状態でも動作出来る必要があります。
ネットワーク設定の生成にネットワークアクセス(DNS参照)が必要という矛盾した状況ですが、とても単純な方法で解決しています。
そもそも DNS 上の情報は、それほど頻繁に更新されるものではないため、サーバの起動時にゾーンファイルを転送してローカルで検索を行い、起動が完了したら DNS の参照に切り替えてやればいいのです。
先ほど紹介した、検索用の関数でも、ローカルにゾーンファイルの一時コピー(data.cdb)が存在するかをキーに動作が切り替わるようになっています。
この方法では、ゾーンファイルの一時コピーを消し忘れると、いつまでも古い情報を参照してしまう恐れがあるので、起動処理の一番最後で必ずゾーンファイルを削除するようにします。
tinydns-getの使い方
tinydns-get はカレントディレクトリの data.cdb をゾーンファイルとして読み込むため、ゾーンファイルの置かれてるディレクトリに移動して、下記のように実行します。
$ tinydns-get A www.klab.jp 1 www.klab.jp: 189 bytes, 1+1+4+4 records, response, authoritative, noerror query: 1 www.klab.jp answer: www.klab.jp 86400 A 211.13.209.202 authority: klab.jp 259200 NS ns1.klab.org authority: klab.jp 259200 NS ns2.klab.org authority: klab.jp 259200 NS ns8.klab.org authority: klab.jp 259200 NS ns9.klab.org additional: ns1.klab.org 14400 A 61.195.64.249 additional: ns2.klab.org 14400 A 61.195.64.247 additional: ns8.klab.org 86400 A 211.13.207.96 additional: ns9.klab.org 86400 A 211.13.207.97
「answer:」の行がクエリの結果です。
tinydns-get では、dig のような「+short」オプションがなく、自分で出力を加工する必要がある上に、TXT レコードの処理に問題があります。
'test.dsas.jp:TEST
answer: test.dsas.jp 86400 16 \004TEST
'test.dsas.jp:TEST5678901234567890123456789TEST
answer: test.dsas.jp 86400 16 !TEST5678901234567890123456789TEST
'test.dsas.jp:TEST5678901234567890123456789012345678901234567890
12345678901234567890123456789012345678901234567890
123456789012345678901234567TEST
answer: test.dsas.jp 86400 16 \177TEST56789012345678901234567890
1234567890123456789012345678901234
5678901234567890123456789012345678
90123456789012345678901234567\004T
EST
上記は、上段がバイナリ変換前のゾーンファイルでのレコード、下段が tinydns-get コマンドを使って検索した結果を示します。
各レコードは、1桁目がレコードの種類(TXTレコードは「'」)、続いてコロン区切りでFQDNとレコードの値が続きます。
answer行の各カラムは、検索したFQDN、TTL、レコードのタイプ値(TXTは16)、レコードの値となっています。
ご覧のとおり、レコードの値に「\004」、「!」など、元のゾーンにない文字列が混ざってしまっています。
なんと tinydns-get は、TXT レコードに対応しておらず、data.cdb ファイルに格納されている 「1バイトの文字列長 + 最大127バイトのデータ値」の繰り返しによるデータ列をそのまま表示してしまっているのです。
データにより、「\xxx」や「!」など、表示され方に差があるのは、出力時に ASCII Printable であればそのまま、制御コードなどであれば8進数表記に変換して表示しているためです。
tinydns-get-suppress-msgs.patch
上記のパッチを当てると、tinydns-get が TXT レコードに対応し、余分な文字列を出力しなくなります。
また、少々手荒に書いたパッチですが、2つ目のパッチを当てると、検索結果に関する余分な情報が出力されなくなり、スクリプト等で使いやすくなると思います。
DSAS環境でのDNS活用法 〜ネットワーク設定の格納にDNSを使う〜
KLab Advent Calendar 2011 「DSAS for Social を支える技術」 の12日目です。
昨日までの apache の話題からガラリとかわって DNS についてお話します。
DSAS 内では、サービスに用いるドメインに関する権威サーバのほかに、システム内部の各サーバのホスト名や DB、memcache などの役割に応じた名前を登録した内部向けドメインの権威サーバやキャッシュサーバを運用しています。
システム内に、内部向けの権威サーバや、キャッシュサーバを設置するのは、珍しい構成ではありませんが、DSAS では、内部向け DNS サーバに、システムの設定情報を一部格納しています。
今回は、DSAS 環境で DNS サーバに設定情報を格納している理由や運用方法を紹介します。
ネットブートと設定ファイルの動的生成
DNS の話題に入る前に、DSAS の特徴を 1 つ紹介します。
DSAS では、マスタとなる冗長化された1組のサーバ以外、全てのサーバはネットブートによって起動します。
マスタサーバには、ロードバランサ用・Webサーバ用・DBサーバ用等の役割のサーバを構成するファイル一式が圧縮されたアーカイブが置かれていて、これをOSイメージと呼んでいます。
PXEブートにより、カーネルと initramfs がロードされて起動処理が開始されると、マスタサーバから自分の役割に応じたOSイメージ等をダウンロードして、tmpfs に展開します。
この OS イメージは、さすがに個々のサーバごとに個別に用意するわけにはいかないため、機能毎の共通ファイルとなっています。
そこで問題となるのが、個々のサーバに個別のパラメータを記述する必要がある設定ファイル類の扱いです。
DB サーバの MySQL で使用する server-id、ロードバランサやmemcacheの冗長化に使っている VRRP のルータID 等の各種デーモンの設定ファイルはもちろん、自身のネットワークインタフェースに割り当てるIPアドレスすら、起動処理内で決定する必要があります。
DSAS では、これらの設定ファイル類は、いくつかのタネになる情報を元にして、起動スクリプト内で動的に生成する仕組みを導入しています。
各デーモンや、インタフェースの設定を行う rc スクリプトの前に、設定ファイルを生成するための rc スクリプトを実行するという方法です。
そして、設定ファイルを生成するためのタネ情報を仕込む場所の1つが DNS サーバなのです。
DNSに格納するパラメータの例
DSAS 環境で DNS に登録するパラメータの一例を紹介します。
主に、ネットワークの設定を生成するために必要な情報が中心になります。
パラメータを登録するドメインとして、とあるゾーンを定義して、下記のようにTXTレコードで値を登録しています。
なお、実際の DSAS 環境では、権威サーバの実装に tinydns を使用しているため、tinydns-data フォーマットで記述しています。
private IN TXT "dsas.jp" net.private IN TXT "192.168.0.0/20" global IN TXT "example.klab.jp" net.global IN TXT "XXX.XXX.XXX.XXX/xx" net.link IN TYT "YYY.YYY.YYY.0/29" lv1.link IN TYT "YYY.YYY.YYY.4" lv2.link IN TYT "YYY.YYY.YYY.5" lvs.link IN TYT "YYY.YYY.YYY.6" gw.link IN TYT "YYY.YYY.YYY.3" lv1.vlanid.link IN TXT "1" lv2.vlanid.link IN TXT "1" lvs.vrid.link IN TXT "100"
private/global/net.private/net.globalは、それぞれ内部用・外部用のドメイン名、プライベートアドレス、サービス用グローバルIPアドレスのレンジになっています。
真ん中の *.link 系のレコードは、ロードバランサが自身のネットワーク設定を行うための情報です。
データセンタ側の上位ネットワークと DSAS をつなぐセグメントのアドレスや、その中で使用するIPアドレスなどが定義されています。
( lv1、lv2というのは、冗長化された2台のロードバランサを指し、lvs は VRRP で使用する仮想IPアドレスを指します)
VRRP のルータIDや、インタフェースに設定する VLAN I Dなども登録されているので、ロードバランサのインタフェース設定に必要な情報は、殆ど DNS 内の情報を編集するだけで調整できます。
このロードバランサを含め、ほとんどのサーバが起動時に DNS 上の情報を参照して、自身のネットワーク設定を行なっています。
すると、ネットワーク設定のために DNS を参照する必要があって、それにはネットワークが必要という、鶏が先か卵が先かという問題が発生してしまいます。
次週は、この問題を解決するための方法や、使用しているdjbdnsのツールを紹介したいと思います。
SHOW PROCESSLIST を使ったカジュアルなプロファイラを強化しました
KLab Advent Calendar 2011 「DSAS for Social を支える技術」の11日目です。
「SHOW FULL PROCESSLIST を使った MySQL のプロファイリング」 で紹介したプロファイラですが、 id:sh2 さんからはてブで
秒間10回叩く例も http://developer.cybozu.co.jp/kazuho/2009/07/mysql-539d.html 。変数ぽい部分をカットする処理はmysqldumpslowのコードを移植するといいかも
というコメントをいただきました。
そろそろネタに困っていたので、 せっかくなので、多くのユースケースで
便利に使えるように改良しました。ぜひご活用ください。
解説
クエリのサマライズ
前のバージョンでは = 以降をバッサリとカットしてしまっていたのですが、 例えば "WHERE user_id='xyz' AND enable=1" が "WHERE user_id=" になってしまうので、 問題のクエリを探すのが難しいケースがありました。
mysqldumpslow を参考にクエリのサマライズを改良し、 "WHERE user_id='S' AND enable=N" のような形式で集計できるようにしました。
ini形式のファイルからMySQLの設定を読み込むように
オプション無しで実行した場合、 ~/.my.cnf の [DEFAULT] セクションから user, password, host, port などを読み込みます。 (port はオプション)。
別のファイルから読み込むときは -c 設定ファイルパス でファイルを指定できます。
別のセクションから設定を読み込むときは -s セクション名 でセクション名を指定できます。
行数やインターバルを指定できるように
頻出する順で何件を表示するかを -n オプションで指定できるようにしました。
また、 show full processlist を実行する間隔を -i オプションで指定できます。 秒間10回サンプリングしたい場合は -i 0.1 と指定してください。
加工前のクエリを出力できるように
標準出力に表示しているクエリは、同じ形のクエリをまとめて集計するために 実際のパラメータ等が消えてしまっています。例えば LIMIT 100 でも LIMIT 5 でも LIMIT N と表示されてしまいます。
実際のパラメータを調べたり、 explain を実行するために、元のクエリが見えた 方がいいこともあるので、 -o 出力ファイル名 で、取得したクエリをファイルに 出力できるようにしました。
注意点
Python 2.6 以上で動くようにするつもりで書いていますが、自分の環境も
DSAS for Social も Python 2.7 を利用しているので Python 2.6 での動作は
未確認です。 (Python 2.5 以下は窓から投げ捨ててください)
(追記: Python 2.5 で使えない機能も避けました。未確認ですが使えるかもしれません)
MySQL への接続に MySQLdb を使っていますが、これは拡張ライブラリなので、 インストールには libmysqlclient のヘッダファイルや Python のヘッダファイルが 必要になります。 手軽にインストールしたい場合は、ピュアPythonの PyMySQL をインストールしてください。
Apache の並列数を CPU コア数に応じて決定する
KLab Advent Calendar 2011 「DSAS for Social を支える技術」の10日目です。
昨日の記事 では並列数を設定する基本的な方法を紹介しました。 今日は実際に DSAS for Social で利用している設定方法を紹介します。
背景
実際の並列数の設定はマシンのCPUスペックやアプリの特性(レスポンスタイムの 何割をWebサーバーのCPUを使う処理が占めているか)に応じて設定するのですが、 DSAS for Social ではアプリの負荷に応じて柔軟にWebサーバーを 追加・削除するので、CPUスペックが一定ではありません。 具体的に言えば、 Core2 世代の4コアサーバーと、Core i7世代の4コア8スレッド サーバーが Web サーバーとして利用されています。
でも、Webサーバーごとに違う設定ファイルを用意したくはありません。 なんとかできないかと思っていたところ、 @hamano が httpd.conf 内で環境変数が 使えるよ、と教えてくれました。
CPUコア数から並列数を計算し設定する
DSAS for Social では daemon tools を使って Apache を起動しています。 なので、 run スクリプトでコア数に応じて並列数を計算します。
あるMVCフレームワークを使ったCPU使用率の高いphpアプリでは、 コア数+2 に設定しています。(Core2世代4コアなら6並列、Core i7世代4コア8スレッド なら10並列になります)
CPUCOUNT=`getconf _NPROCESSORS_ONLN` export MAX_CLIENTS=$((CPUCOUNT+2))
次に、httpd.conf でこの環境変数を利用して並列数を設定します。
MinSpareServers ${MAX_CLIENTS}
MaxSpareServers ${MAX_CLIENTS}
StartServers ${MAX_CLIENTS}
ServerLimit ${MAX_CLIENTS}
MaxClients ${MAX_CLIENTS}
これで Web サーバーを40台まで増やしても MySQL の接続数は400以下に抑えられますし、 実際にCPU使用率が80%以上になってレスポンスタイムが伸びてきても安定して サービスが継続できています。
補足: ProxyPass の connectiontimeout を1秒未満に設定する
昨日の記事で使っていたテスト環境は apache 2.2.10 を利用していたのですが、 connectiontimeout パラメータは 2.2.10 で導入された後、 2.2.11 から ms という サフィックスを付けて1秒未満の値を設定できるようになりました。
ループバックアダプタへの接続は backlog が足りていたら 1ms もかからないので、 "connectiontimeout=10ms" などに設定したらいいと思います。
過負荷をかわす Apache の設定
KLab Advent Calendar 2011 「DSAS for Social を支える技術」の9日目です。
前回は php を動かしている Apache の手前にリバースプロキシを 置く必要性を解説しました。 今日は、 その前の php のプロセス数を絞る設定と合わせて、実際に Apache で 設定する方法を紹介します。
以降、 php を動かしている Apache の事をアプリサーバー、リバースプロキシ+ 静的ファイル配信を行っている Apache の事をプロキシサーバーと呼びます。
基本設定
まずは基本的な設定のおさらいです。アプリサーバー
並列数を絞るには MaxClients を設定します。アプリがどれくらいの時間を CPUの処理で使って、どのくらいの時間を外部リソース待ちに使っているかにも よりますが、だいたいCPU数の1.5倍〜2倍くらいが適当だと思います。 HypertThreading や TurboBoost などがある場合は、論理CPUを使いきらなくても 他のCPUの性能がその分上がるので、CPU数とイコールでも良いかもしれません。
以下の設定例では並列数を2にしています。 (追記: この設定は後述の試験をした2コアサーバー用のものです。DSAS for Social の本番環境用Webサーバーはもっとコア数多いし、DBやMemcachedの待ち時間もあるので、もっと大きい値を利用します)
MinSpareServers 2 MaxSpareServers 2 StartServers 2 ServerLimit 2 MaxClients 2
プロキシサーバー
ローカルホストのアプリサーバーのポート(1234としておきます)にリバースプロキシします。 静的ファイルは /static/ に置くことにして、それ以下のパスはプロキシせずに プロキシサーバーが直接返します。
ProxyTimeout を 4 秒に設定することで、5秒ルールを避けます。
# VirtualHost 内 ProxyRequests Off ProxyPass /static/ ! ProxyPass / http://127.0.0.1:1234/ ProxyPreserveHost On ProxyTimeout 4
プロキシサーバーはほとんどメモリを食わないので、並列数は128とか 256とか大き目に設定しても大丈夫です。
スロットル制御
本題の、過負荷になったら、アプリサーバーにプロキシせずにレスポンスを返す 方法に入りましょう。 アプリサーバーの並列数を使いきっている状況では、リクエストをキューイング しておき、処理中だったリクエストのどれかが完了したら次のリクエストを キューから取り出すようにします。 キューの長さが一定値を超えたら、それ以上はキューイングしないでエラーに すれば良さそうです。
もう判りましたよね?キューとは backlog のことです。
アプリサーバーの設定
キューの長さは、安定してレスポンスを返していたらこれ以上は溜まらない、 という数に設定します。 たとえば秒間50リクエストを安定して捌ける場合、 バックログを 50 にしたら、バックログの最後にキューイングされた リクエストは1秒前後で返されることになります。これで5秒ルールに十分 間に合います。
この記事を書きながら触っている環境は古い2コアサーバーなので、 20 程度にしておきます。
Listen 1234 ListenBackLog 20
プロキシサーバーの設定
少なくとも Linux では、 Backlog に空きがある状況で接続の syn パケットが来ると、 すぐに syn/ack を返します。 プロキシサーバーとアプリサーバーの間はループバックアダプタ経由で接続されているので、 パケットロスの心配はありませんし、並列数も絞っていてロードアベレージが 極端に増えることもないので、 1ms もかからずに接続が成功するはずです。
一方、 backlog がいっぱいになった場合、 syn に対して syn/ack が返らないので、 通常は接続元であるプロキシサーバーが 3秒ほど待ってから syn を再送するはずです。 connectiontimeout を短い時間に設定することで、接続できなかったときにすぐに エラーを返すことができます。
connectiontimeout を設定するには、上で行った ProxyPass ディレクティブに パラメータを追加します。とりあえず 1 秒に設定しておきましょう。また、接続がエラーになっても リクエストの転送が止まらないように、 retry=0 もセットで設定しておきます。
ProxyPass / http://127.0.0.1:1234/ connectiontimeout=1 retry=0
効果確認
スロットル制御の動作確認をしましょう。こんな php ファイルを用意しておきます。
<?php
$prime = array();
$i = 2;
while (count($prime) < 1000) {
$ok = true;
foreach ($prime as $p) {
if ($i % $p == 0) {
$ok = false;
break;
}
}
if ($ok) {
$prime[] = $i;
}
$i++;
}
まずは、1並列で実行してみます。
$ ab -n100 -c1 -H 'Host: virtualhost.example.org' http://testhost:1234/nadeko.php ... Concurrency Level: 1 Time taken for tests: 10.129 seconds Complete requests: 100 Failed requests: 0 Write errors: 0 ... Percentage of the requests served within a certain time (ms) 50% 101 66% 101 75% 101 80% 102 90% 102 95% 102 98% 103 99% 117 100% 117 (longest request)
だいたい100msちょっとでレスポンスが返ってきますね。 テストに使っているのは2コアのサーバーで、アプリサーバーは2並列なので、 2並列で負荷をかけてもレスポンスタイムは少ししか増えないはずです。
$ ab -n100 -c2 -H 'Host: virtualhost.example.org' http://testhost:1234/nadeko.php ... Concurrency Level: 2 Time taken for tests: 5.771 seconds Complete requests: 100 Failed requests: 0 Write errors: 0 ... Percentage of the requests served within a certain time (ms) 50% 102 66% 102 75% 103 80% 104 90% 198 95% 202 98% 207 99% 208 100% 208 (longest request)
ベンチマーク用に用意した専用環境ではなくて共有のテスト環境を使っているので、 一部のリクエストが遅くなっているのはご了承ください。
次は、4並列で負荷をかけてみましょう。2リクエストを並列処理して2リクエストが backlog に溜まるので、平均レスポンスタイムは2倍くらいに増えるものの、 ちゃんとエラーにならずに全部のリクエストを処理できるはずです。
$ ab -n100 -c4 -H 'Host: virtualhost.example.org' http://testhost:1234/nadeko.php ... Concurrency Level: 4 Time taken for tests: 6.282 seconds Complete requests: 100 Failed requests: 0 Write errors: 0 ... Percentage of the requests served within a certain time (ms) 50% 205 66% 207 75% 210 80% 304 90% 440 95% 450 98% 476 99% 477 100% 477 (longest request)
うまく行きましたね。次は、 backlog と同じ 20 並列で負荷をかけてみましょう。 最悪のレスポンスタイムが 100ms X 20 で 2秒くらいになりますが、ぎりぎりでエラーには ならないはずです。 (リクエスト数も100から200に増やします)
$ ab -n200 -c20 -H 'Host: virtualhost.example.org' http://testhost:1234/nadeko.php ... Concurrency Level: 20 Time taken for tests: 12.485 seconds Complete requests: 200 Failed requests: 0 ... Percentage of the requests served within a certain time (ms) 50% 1042 66% 1083 75% 1324 80% 1559 90% 2002 95% 2011 98% 2017 99% 2034 100% 2052 (longest request)
さて、次は倍の40並列です。2リクエストが処理中になり20リクエストが backlog にたまりますが、以降はアプリサーバーがレスポンスを返して新しいリクエストを 受け取るまでは接続できないので、エラーになるリクエストが出はじめるはずです。
$ ab -n200 -c40 -H 'Host: virtualhost.example.org' http://testhost:1234/nadeko.php ... Concurrency Level: 40 Time taken for tests: 6.223 seconds Complete requests: 200 Failed requests: 83 ... Percentage of the requests served within a certain time (ms) 50% 1026 66% 1119 75% 1130 80% 1133 90% 1138 95% 1224 98% 2117 99% 3187 100% 3191 (longest request)
20並列の時と比べると、200リクエスト中83リクエストがエラーになり、 テストにかかった時間も 12.5秒から 6.2秒と半分程度になりました。 backlog を超えたリクエストを「処理せずにエラーを返す」ことで、 過負荷状況下でも「秒間正常レスポンス数」を維持していることが解ります。
終わりに
以前は、 MySQL の set コマンドで設定できるパラメータを変更したことはあっても、 Apache の設定は完全に「インフラの人」にお願いしていました。 自分で Apache の設定を触るきっかけになったのは チューニンガソンに出場して、 自分でもサーバーのチューニングができるんだという自信がついたからです。 チューニンガソンを開催してくださった運営・スポンサーの方、ありがとうございました。
