#topicpath *OpenBSDでhttpdをスケールさせる [#v19f2374] RIGHT:川俣 吉広 ~ EBUG勉強会 @ 新潟, 2014/11/15 **発端 [#wdbc1063] 以前この勉強会で紹介したウェブ/メルマガのサーバが、アクセス過多によりトップページが見れなくなっているという報告を受ける。 以前この勉強会で紹介した[[ウェブ/メルマガのサーバ>EBUG勉強会/20110903_ロードバランサなしで負荷分散してみた]]が、アクセス過多によりトップページが見れなくなっているという報告を受ける。 - トップページが「500 Internal Server Error」を表示していることを確認。 - SSHでログインし、状況調査 -- httpd (Apache) の error_logを確認 ---子プロセスが起動できなくなっている。 [Mon Nov 3 15:15:51 2014] [error] [client xxx.xxx.xxx.xxx] (35)Resource temporarily unavailable: couldn't spawn child process: /var/www/docs/pub/index.cgi ---静的なページは正常にアクセスできる ---ロードアベレージは、1時間平均で0.8~1.0, 瞬間最大で7~8程度 ---CPU usageはピークで85%程度 ---スワップは0MB … 起動から一回も使われていない。 ---ネットワークのスループットは10Mb/sを少し超える程度。 - ... CPU, メモリなどのリソースは枯渇していないが、プロセスの起動数になんらかの制限がかかっている。 - ... サーバは2台が並列稼動しているが、両方とも同様な状況。 **応急対応 [#n2f7a8d8] - /index.cgi を /index.html に変更 ~ → User Agent によって、ケータイ端末をリダイレクトしていた。 - 以下のように設定値を変更し、httpdを再起動 ~ /etc/login.conf default:\ :path=/usr/bin /bin /usr/sbin /sbin /usr/X11R6/bin /usr/local/bin:\ :umask=022:\ :datasize-max=512M:\ :datasize-cur=512M:\ :maxproc-max=256:\ :maxproc-cur=128:\ :openfiles-cur=128:\ :stacksize-cur=4M:\ :localcipher=blowfish,6:\ :ypcipher=old:\ :tc=auth-defaults:\ :tc=auth-ftp-defaults: # www - resource definition for web processes # www:\ - :maxproc=512:\ + :maxproc-max=384:\ + :maxproc-cur=384:\ :tc=default:\ 設定をチェックした段階では、defaultログインクラスではmaxproc-max, maxproc-curがそれぞれ 指定されているのに対し、wwwログインクラスではmaxproc指定のみのため、プロセス最大数の設定数が 上書きされているかどうか不明だったため、maxproc-max, maxproc-curを用いる設定に書き直した。 /var/www/conf/httpd.conf # It is intended mainly as a brake to keep a runaway server from taking # the system with it as it spirals down... # -MaxClients 256 +MaxClients 192 プロセス数の上限で制限がかかっていると仮定し、httpdのプロセス数を減らすことで 子プロセス(*.cgi)の数を増やせるようにした。 → 30分ほどで不具合は解消 - アクセスそのものが減少し - 上記対策が効を奏した(かどうか、本当は不明) **恒久対策 [#o48c211b] #ref(ProcessModel.jpg,wrap,around,right,70%) 状況をモデリングする -httpdに関しては、最大でhttpd.confで定義されたMaxClients個(右図ではNh個)のプロセスが起動する。 -そのhttpdのうちの幾つかがNp個の子プロセス(*.cgi)を起動する。 -httpdとその子プロセスの総数(Nh+Np)の上限が login.conf の wwwエントリにある maxproc 設定値で制限される。 →httpd.confのMaxClients、 login.confの maxprocを幾つにすればよいか? #img(,clear) ---- #ref(0005mem.gif,wrap,around,left,70%) 設定値の算定 -プロセス一つ当り、どの程度メモリを消費するか。 -動作記録を基に概算する。~ ... 11/8に使用メモリの大幅な変動がある。 これは設定変更の反映のためにhttpdの停止・起動を行ったため。~ ~ ... サーバの実装メモリは4096MB~ ~ ... sysctlによりkern.bufcachepercentが75%に設定されている。つまり、3GBはバッファキャッシュとして使用されている。~ ~ ... 左のグラフをみると、httpdの停止時はhttpdプロセス約200個ほどが停止し、これにより実装メモリの約2割程が解放されている。~ ~ ... よって 4000*0.2 / 200 = 4。大体プロセス一個あたり4MB消費 ... topコマンドの SIZE及びRSS表示値 とも大体一致. #img(,clear) ---- バッファキャッシュ3GBのうち、2GBを取り崩してhttpd用に割り当てるとすると ~ 2048 / 4 = 512 とりあえず、MaxClientを512、ワーストケースとして *.cgiが同数起動されると仮定して、wwwのログインクラスのmaxprocを1024として 動作をみる。 **実施と検証 [#n2987708] ***httpd.conf、MaxClientsの設定 [#rf253fa6] MaxClientを512にして実行 # /usr/sbin/httpd -u -DSSL WARNING: MaxClients of 512 exceeds compile time limit of 256 servers, lowering MaxClients to 256. To increase, please see the HARD_SERVER_LIMIT define in src/include/httpd.h. # httpd.confの設定値にかかわらず、MaxClientsは256を超えないように ハードコーディングされている。(Apache 2.xでは、ServerLimitディレクティブで 設定可) ということで /* Limit on the total --- clients will be locked out if more servers than * this are needed. It is intended solely to keep the server from crashing * when things get out of hand. * * We keep a hard maximum number of servers, for two reasons --- first off, * in case something goes seriously wrong, we want to stop the fork bomb * short of actually crashing the machine we're running on by filling some * kernel table. Secondly, it keeps the size of the scoreboard file small * enough that we can read the whole thing without worrying too much about * the overhead. */ #ifndef HARD_SERVER_LIMIT #define HARD_SERVER_LIMIT 256 #endif この部分変更してコンパイル # cd /usr/src/usr.sbin/httpd # make -f Makefile.bsdwrapper obj # make -f Makefile.bsdwrapper clean # CFLAGS="-DHARD_SERVER_LIMIT=4096" make -f Makefile.bsdwrapper : : # make -f Makefile.bsdwrapper install # /usr/sbin/httpd -V Server version: Apache/1.3.29 (Unix) Server's Module Magic Number: 19990320:15 Server compiled with.... (略) -D DYNAMIC_MODULE_LIMIT=64 -D HARD_SERVER_LIMIT=4096 -D HTTPD_ROOT="/var/www" ***login.confのmaxproc設定 [#u4f94e5a] #ref(loginclass.jpg,wrap,around,right,70%) # www - resource definition for web processes # www:\ :maxproc-max=1024:\ :maxproc-cur=1024:\ :tc=default:\ この部分の課題 ... setuidした子プロセスについてはlogin classはどうなる(右図)? ~ ~ (11/21, 後日検証した結果を追記) #img(,clear) ***運用の監視 [#ve937cc8] SAG(Server Activity Grapher)にプロセス数の項目を組み込んで監視 #ref(0005procs.gif,wrap,around,right,70%) # # t0005 - executing every 5 minutes # # $Id: t0005,v 1.2 2004/05/17 01:21:52 cvs Exp $ (echo =dt $DAYTIME echo =mem cat /proc/meminfo echo =end echo =procs `sysctl -n kern.nprocs; pgrep httpd | wc -l` echo =sensors `sysctl -n hw.sensors.cpu0.temp0` ) >> var/rl0005 この状況で様子を見る。 #img(,clear) **さらに... [#zac46601] 将来的にさらにプロセス数の増加が必要な場合を想定し、どの程度まで増やせるかやってみる。 -httpd 2048程度に設定。~ ... pgrep httpd | wc -l --実際には1000少しまでしか設定できず。 --他アカウントでログイン不可となる。 -sysctl kern.maxproc が1310となっていた。 --増加させてみる。 # sysctl -w kern.maxproc=8192 # sysctl kern.maxproc kern.maxproc=8192 --再試行 ... pgrep httpd | wc -l --プロセス数増えたが、設定値どおりにならず ... 2000弱 ~ httpdでやってるとかったるいので、簡単なスクリプト書いてテスト; ~ maxchild-test.sh #!/bin/sh if [ "$1" = "" ]; then childlevel=1 echo -n "forking " else childlevel=$((1+$1)) fi echo -n " $childlevel" if $0 $childlevel; then : else echo "failed at $childlevel" fi 実行結果 # ./maxchild-test.sh forking 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 (略) 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874^C/home/kaw/maxchild-test.sh[17]: cannot fork - try again ^C # このとき、syslog (/var/log/messages) に出力されることを発見。 Nov 14 17:56:37 mediakaw /bsd: process: table is full カーネル内を探す; # grep -rs ': table is full' /usr/src/sys Binary file /usr/src/sys/arch/i386/compile/GENERIC/bsd matches Binary file /usr/src/sys/arch/i386/compile/GENERIC/subr_prf.o matches /usr/src/sys/kern/subr_prf.c: log(LOG_ERR, "%s: table is full\n", tab); # /usr/src/sys/kern/subr_prf.c内のtablefull関数が実行されている; # grep -rs 'tablefull.*process' /usr/src/sys /usr/src/sys/kern/kern_fork.c: tablefull("process"); kern_fork.cの該当部分 if ((flags & FORK_THREAD) == 0) { if ((nprocesses >= maxprocess - 5 && uid != 0) || nprocesses >= maxprocess) { static struct timeval lasttfm; if (ratecheck(&lasttfm, &fork_tfmrate)) tablefull("process"); nthreads--; return (EAGAIN); } nprocesses++; maxprocessをどこで初期化しているか? # grep -rs 'maxprocess.*=' /usr/src/sys /usr/src/sys/conf/param.c:int maxprocess = NPROCESS; NPROCESSは? # grep -rs '#.*define.*NPROCESS' /usr/src/sys /usr/src/sys/conf/param.c:#define NPROCESS (30 + 16 * MAXUSERS) ということで -カーネルレベルでのプロセス最大数はsysctlのkern.maxprocで設定可 -ただし、kernel configのMAXUSERSで規定される「真の最大値」が存在する。 この値はsysctlで変更不可(変更にはカーネルの再コンパイルが必要)。 ** 11/21追記: login.confでのmaxproc設定関連について [#scb7eb91] 前項「実施と検証」/「login.confのmaxproc設定」について示された疑問を実際に設定を行うことで調査した。 ***調査方法 [#k0b124c7] -3種類のログインクラスエントリ, class0, class1, class2を作成する。 -class1, class2ともtcによりclass0の設定を引き継ぐようにする。 -ユーザアカウントuser1, user2を作成し、それぞれのログインクラスをclass1, class2とする。 -user1, user2アカウントにログインし、ulimit にてmaxprocの表示と設定を行ってみる。 ***調査1: login.confの記述方法について [#lcad16ec] 以下のケースAでは、ログインクラス class1 でログインした場合、設定値は幾つになるか。~ さらに、ケースBではどうか。 ケースA class0:\ :maxproc-cur=100:\ :maxproc-max=200: class1:\ :maxproc=150:\ :tc=class0: ケースB class0:\ :maxproc=300: class1:\ :maxproc-cur=150:\ :maxproc-max=250:\ :tc=class0: 結果:ケースA、ケースBのどちらでも、class1のログインクラスではclass1自身で書いた値が有効となり、class0で書いたエントリは無効となる。 ***調査2: [#p6e8e91b] 以下の設定で、user1 (ログインクラス:class1)からuser2 (ログインクラス:class2) にsuidした場合、maxprocはどうなるか? class0:\ :maxproc=200: class1:\ :maxproc=225: 結果:class1の設定値に関係なく、class2の値が新規に設定される。 ***調査3: [#s1869268] ulimitの挙動について :maxproc-cur=A:\ :maxproc-max=B: 上記のように記述した場合、ログイン直後の最大プロセス数はAに設定されている。 シェルのビルトイン・コマンドulimit -pで変更する場合、 プロセス数を増加する方向には「Bを超えない範囲、かつ一回だけ」変更できる。~ プロセス数を減少させる方向には何回でも可能。~ (以上の挙動は使用するシェルに依存する可能性あり)~ また、 :maxproc=C: と記述した場合は、 :maxproc-cur=C:\ :maxproc-max=C: と書いた場合と等価であり、上記のルールに従う。 ---- #topicpath