運用
iPXE を導入した記念に blog を書いてみたけど、タイトルが悩ましいので、この記事のタイトル募集中!
(自分の書いたタイトルはダメ出しされたので、募集することにしてみました(^^;;記事を読んでいただいて、もし、よいタイトルを思いついたら、ぜひ、コメントにでもお願いしますm(_ _)m。次回のタイトルに使わせていただくかも(^^)
- PXEとは、ネットワークブートのための仕組みの1つ。NIC上のフラッシュROM に書き込まれている。ネットワークブートに必要なファイルの取得には、DHCPとTFTP を用いる
- iPXE(gPXE)とは、PXEを拡張したオープンソースなネットワークブートファームウェア/ローダ
iPXE(gPXE)には、下記のような特徴があります。
- MACアドレスの他にもサーバのDMI情報を取得できる
- コマンドが用意されていてそのコマンドで組んだスクリプトを利用可能
- httpでのリクエストをサポートしている
つまり・・・
- DMI情報を取得できるということは、そのサーバがどんな機種なのか分かる
- スクリプトから取得した機種情報を使ったリクエストをすることができそう
- http+cgiで、リクエストに応じて動的にレスポンスを返すことができそう
どうでしょう。試してみたくなりませんか?
ではでは、これから、実際にiPXEを触っていくとしましょう。
iPXE(gPXE)の紹介
gPXEのほうがよく知られているかもしれませんが、gPXEの開発が止まり、その後釜として始まったプロジェクトがiPXEとなります。
- iPXE: http://ipxe.org/
- gPXE: http://etherboot.org/wiki/start
本来は、PXEと同じくNICのROMに焼いて使用するようですが、そうすると、元からあるPXEは当然消えてしまいますし、数十台、数百台のサーバを運用している場合に、すべてのNICに適用するとしたら、気が遠くなりそうです。しかしながら、よくみてみると、PXEからiPXEをチェインロードできるとあります。この方法を使えば、NICのROMに手を加えることなく、iPXEを利用することができそうです。
iPXEを実際に起動してみて、触ってみよう
PXEからチェインロードする方法でiPXEを試しに起動させてみます。
ここでは、isc-dhcpd,tftpd,thttpd(cgiは、bashスクリプト)を使用し、設定は、以下のようになっているとします。
(ここでは、iPXE以外の細かい設定に関しては、割愛します)
・tftpd,thttpdサーバのIPアドレス
192.168.0.1
・tftpd,thttpdルートディレクトリ
/tftp/
・thttpdのcgiディレクトリ
/tftp/cgi-bin/
・iPXE関連ファイルの格納ディレクトリ
/tftp/iPXE/
まずは、PXEチェインロード用のバイナリであるundionly.kpxeを以下のページ内のリンクからDLし、/tftp/iPXE/undionly.kpxeに保存します。
http://ipxe.org/howto/chainloading
上記ページのリンク先に紹介されているように、DHCPの設定ファイルに以下のように記述を追加します。
if exists user-class and option user-class = "iPXE" { filename "http://192.168.0.1/iPXE/boot.ipxe"; } else { filename "iPXE/undionly.kpxe"; } next-server 192.168.0.1;
filenameの値を、iPXEからのリクエストにはiPXEの起動用スクリプト(boot.ipxe)を返し、iPXE以外のリクエストにはiPXEをロードするためのundionly.kpxeを返すようにしています。
これにより、以下のような流れとなります。
PXE起動
↓
iPXE(undionly.kpxe)を取得してロード
↓
iPXE起動
↓
起動用スクリプト(boot.ipxe)を取得してロード
↓
boot.ipxeの内容を実行
/tftp/iPXE/boot.ipxeは、以下のように記述しておきます。
#!ipxe imgfree chain http://${next-server}/cgi-bin/boot_ipxe.cgi?type=${product:uristring}&mac=${mac}
boot.ipxeの中身の説明はとりあえず一旦おいといて、実際にマシンをPXEブートしてみると、以下のようになります。
Intel(R) Boot Agent GE v1.2.40 Copyright (C) 1997-2006, Intel Corporation CLIENT MAC ADDR: XX XX XX XX XX XX GUID: xxxxxxxxxxxxxxxxxxxxxxxxxxx CLIENT IP: 192.168.0.xx MASK: 255.255.255.0 DHCP IP: 192.168.0.1 GATEWAY IP: 192.168.0.xx PXE->EB: !PXE at 97CA:0070, entry point at 97CA:0106 UNDI code segment 97CA:3F20, data segment 919C:62E0 (582-623kB) UNDI device is PCI 07:00.0, type DIX+802.3 582kB free base memory after PXE unload iPXE initialising devices...ok iPXE 1.0.0+ -- Open Source Network Boot Firmware -- http://ipxe.org Features: HTTP iSCSI DNS TFTP AoE bzImage ELF MBOOT PXE PXEXT Menu Press Ctrl-B for the iPXE command line...
iPXEがチェインロード起動されると、自動的にboot.ipxeを取得して実行となるのですが、インタラクティブモードに入ると、自動実行は行わず、手動でコマンド入力をすることが出きるようになります。"Press Ctrl-B for the iPXE command line..."の表示がされたら、すかさず、Ctrl+Bを入力してみましょう。すると、下記のプロンプトが表示され、コマンドの入力ができるようになります。では、試しに、いくつかコマンドを入力してみましょう。
iPXE> # マックアドレスを表示 iPXE> show mac net0/mac:hex = xx:xx:xx:xx:xx:xx # DHCPにてIPアドレスを取得 iPXE> dhcp DHCP (net0 xx:xx:xx:xx:xx:xx)... ok # IPアドレスを表示 iPXE> show ip net0.dhcp/ip:ipv4 = 192.168.0.xx # DHCPで設定されたfilenameを表示 iPXE> show filename net0.dhcp/filename:string = http://192.168.0.1/iPXE/boot.ipxe # DHCPで設定されたnext-serverを表示 iPXE> show next-server net0.dhcp/next-server:ipv4 = 192.168.0.1 # サーバの機種情報を表示 iPXE> show product smbios/product:string = Express5800/i120Ra-e1 [N8100-1482Y] # urlエンコードして表示 iPXE> show product:uristring smbios/product:uristring = Express5800%2Fi120Ra-e1%20%5BN8100-1482Y%5D
サーバの機種情報も無事取得できているようです。次は、boot.ipxeをhttp経由で取得できるかどうか試してみます。
iPXE> imgfetch ${filename} http://192.168.0.1/iPXE/boot.ipxe.... ok iPXE> imgstat boot.ipxe : 102 bytes
無事、取得できているようですので、boot.ipxeの内容を手動で試してみましょう。ここでは、まだ、cgiを準備していないので、エラーとなりますが、どのようにアクセスされるかの確認ができます。
iPXE> imgfetch http://${next-server}/cgi-bin/boot_ipxe.cgi?type=${product:uristring}&mac=${mac} http://192.168.0.1/cgi-bin/boo_ipxe.cgi?type=Express5800%2Fi120Ra-e1%20%5BN8100-1482Y%5D&mac=xx%3Axx%3Axx%3Axx%3Axx%3Axx... No such file or directory (http://ipxe.org/2d0c613b)
機種情報とMACアドレスが代入されてhttpリクエストをしていることが確認できました。
もうお分かりのようにboot.ipxeは、機種情報とMACアドレスを取得し、それをboot_ipxe.cgiにクエリ文字列として付与して叩き、そのレスポンスの内容を実行するスクリプトということになります。(${product}でも、自動的にurlエンコードされるようですが、念のため、uristringでの出力指定をしています。)
では、サーバ起動用の簡単なiPXEスクリプトを、機種情報やMACアドレスを元にboot_ipxe.cgiで動的に生成して、実際にサーバを起動させてみましょう。
Linuxを起動してみる
MACアドレスと機種情報にしたがって、initrdとkernelを決定して起動するだけの簡単なboot_ipxe.cgiを用意してみます。
ちなみに、iPXEでは確認していませんが、gPXEでは、Content-Lengthを指定しないとうまく動作しなかったため、指定するようにしています。
#!/bin/bash # クエリ文字列は、$QUERY_STRINGに格納されている # 変数type,macをクエリ文字列から生成する for q in $(echo ${QUERY_STRING} | tr "&" " "); do eval $q done case $type in *Express*) initrd_file=initrd.gz ;; esac case $mac in *xx%3Axx%3Axx%3Axx%3Axx%3Axx*) kernel_file=vmlinuz ;; esac body=$(#!ipxe initrd http://\${next-server}/${initrd_file} kernel http://\${next-server}/${kernel_file} boot ) echo "Content-type: text/plain" echo "Connection: close" echo "Content-Length: $(echo -e "${body}" | wc -c)" echo "" echo -e "${body}"
wgetなどで、先ほどのiPXEが出力したパスを叩いて確認してみると、以下のような出力となります。
$ wget -q -O - "http://192.168.0.1/cgi-bin/boo_ipxe.cgi?type=Express5800%2Fi120Ra-e1%20%5BN8100-1482Y%5D&mac=xx%3Axx%3Axx%3Axx%3Axx%3Axx" #!ipxe initrd http://${next-server}/initrd.gz kernel http://${next-server}/vmlinuz boot
では、/tftp/以下に適当なvmlinuzとinitrd.gzを準備して、マシンを再起動させてみます。
Intel(R) Boot Agent GE v1.2.40 Copyright (C) 1997-2006, Intel Corporation CLIENT MAC ADDR: XX XX XX XX XX XX GUID: xxxxxxxxxxxxxxxxxxxxxxxxxxx CLIENT IP: 192.168.0.xx MASK: 255.255.255.0 DHCP IP: 192.168.0.1 GATEWAY IP: 192.168.0.xx PXE->EB: !PXE at 97CA:0070, entry point at 97CA:0106 UNDI code segment 97CA:3F20, data segment 919C:62E0 (582-623kB) UNDI device is PCI 07:00.0, type DIX+802.3 582kB free base memory after PXE unload iPXE initialising devices...ok iPXE 1.0.0+ -- Open Source Network Boot Firmware -- http://ipxe.org Features: HTTP iSCSI DNS TFTP AoE bzImage ELF MBOOT PXE PXEXT Menu net0: xx:xx:xx:xx:xx:xx using undionly on UNDI-PCI07:00.0 (open) DHCP (net0 xx:xx:xx:xx:xx:xx)... ok net0: 192.168.0.xx/255.255.255.0 gw 192.168.0.xx Next server: 192.168.0.1 Filename: http://192.168.0.1/iPXE/boot.ipxe http://192.168.0.1/iPXE/boot.ipxe... ok http://192.168.0.1/cgi-bin/boot_ipxe.cgi?type=type=Express5800%2Fi120Ra-e1%20%5BN8100-1482Y%5D&mac=xx%3Axx%3Axx%3Axx%3Axx%3Axx...ok http://192.168.0.1/initrd.gz... ok http://192.168.0.1/vmlinuz... ok [ 0.000000] Initializing cgroup subsys cpuset [ 0.000000] Initializing cgroup subsys cpu [ 0.000000] Linux version 3.0.21 ..... .... .... ....
カーネルがロードされ、無事起動できたようです。
実際の運用で使ってみるとすると・・・
DSASでは、ほとんどのサーバがPXEブート+PXELINUXを利用してディスクレスサーバとして起動されるようになっています。
また、リモートからのサーバ管理にIPMIを利用しており、SOL(Serial over Lan)対応シリアルポートを、PXELINUXの起動設定ファイルにて記述することにより、IPMIのSOL経由にてPXELINUXの起動メニューの表示/編集やLinuxの起動メッセージの表示ができるようにしています。
iPXEは、独自メニューを作成可能なのですが、残念ながら、PXELINUXやGRUBのように、起動時にインタラクティブに編集することは出来ないようです。
ですが、PXELINUXのpxelinux.0やSYSLINUXのcom32をロードして実行することができます(com32に関しては、SYSLINUXのver4.xxのcom32ファイルの直接実行はできず、ver3.86以下のものを使用する必要があります)。
そこで、pxelinux.0やmenu.c32(com32ファイルで用意されているメニュー用ファイル)を利用し、そのメニュー設定をiPXEで取得できる情報から動的に生成すれば、サーバ機種によって適切な起動カーネルパラメータを指定することができそうです。また、起動時にインタラクティブにパラメータを編集するといったこともいけそうです。
さて、今回は、ここまでとして、次回ではmenu.c32のメニュー機能をiPXEから利用したり、IPMI SOL経由で表示したりする例を紹介できたらと思っています。
ソーシャルアプリのボトルネック調査例(strace編)
KLab Advent Calendar 2011 「DSAS for Social を支える技術」の6日目です。
はじめに
ソーシャルゲームの開発では、仕様変更への柔軟な対応が求められることが多い上、突発的なアクセス増加にも耐えられる応答性能が要求されます。
一昔前までは、サービスの性能を担保するにはきちんとアーキテクチャを設計し、入念に動作チェックして、負荷試験して、プロファイル取って・・・みたいなことをリリース前にひらすやるのが理想だと思っていた頃もありました。
しかし、ソーシャルゲームの世界ではリリース直後からイベントやキャンペーンなどの追加開発が入ったり、ユーザの動向やコミュニティを参考にして仕様を変更することが多いので、リリース前に頑張ってチューニングしていても、その性能を担保し続ける事が難しいといった現状があります。
まあ、これはこれで刺激があって楽しい面もありますし、遊んでくれているユーザの皆様にできるだけ良いサービスを提供したいという気持ちもあるので、きちんと運用ができるように様々な工夫をしていますが、所詮プログラムは人間が書くものなので、時には思わぬ爆弾を仕込んでしまい、著しく応答性能が悪くなってしまうこともあったりします。
このような問題のほとんどは、原因さえわかってしまえば容易に解決できるケースが多いので、イカに速くボトルネックを見つけ出すかが鍵となります。
ボトルネック調査といっても、発生している事象によってアプローチの仕方は様々です。DBサーバに想定以上のI/Oが発生している場合は、slowログを見てexplainするでしょうし、接続エラーやタイムアウトが頻発している場合はネットワークの品質を疑って、スイッチのログを確認したりもするでしょう。
必要な場面で必要な情報を取り出す手段を知っていることが、ボトルネックを調査する上で必要とされるスキルなのかもしれません。
えーっと、、すっかり前置きが長くなってしまいましたが、今回はstraceを使ってアプリケーションのボトルネックを調査した例を紹介してみたいと思います。
apacheのプロセスを覗いてみる
「実稼動中のapacheをstraceなんかして本当に解析できるの?」と疑問に感じる方もいるかもしれませんが、実際にやってみると以外と簡単です。
# strace -p 8139 -tt -T -e trace=network,poll 15:21:01.490665 accept(3, {sa_family=AF_INET, sin_port=htons(xxxxx), sin_addr=inet_addr("x.x.x.x")}, [16]) = 11 <0.000011> -- snip -- 15:21:02.039149 accept(3, {sa_family=AF_INET, sin_port=htons(xxxxx), sin_addr=inet_addr("x.x.x.x")}, [16]) = 11 <0.000007> -- snip -- 15:21:02.727402 accept(3, {sa_family=AF_INET, sin_port=htons(xxxxx), sin_addr=inet_addr("x.x.x.x")}, [16]) = 11 <0.000007> -- snip -- 15:21:03.625652 accept(3, {sa_family=AF_INET, sin_port=htons(xxxxx), sin_addr=inet_addr("x.x.x.x")}, [16]) = 11 <0.000009>
apacheの子プロセスをstraceすると上記のような結果となります。accept(2) を境界にしてリクエスト毎の処理を追うことができそうです。
15:21:04.244882 accept(3, {sa_family=AF_INET, sin_port=htons(37421), sin_addr=inet_addr("x.x.x.x")}, [16]) = 11 <0.000011> 15:21:04.254675 socket(PF_INET, SOCK_DGRAM, IPPROTO_IP) = 20 <0.000007> 15:21:04.254702 connect(20, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("0.0.0.0")}, 28) = 0 <0.000007> 15:21:04.254749 poll([{fd=20, events=POLLOUT}], 1, 0) = 1 ([{fd=20, revents=POLLOUT}]) <0.000055> 15:21:04.254829 sendto(20, "W\"..., 43, MSG_NOSIGNAL, NULL, 0) = 43 <0.000018> 15:21:04.254873 poll([{fd=20, events=POLLIN}], 1, 5000) = 1 ([{fd=20, revents=POLLIN}]) <0.000014> 15:21:04.254918 recvfrom(20, "W\"..., 1024, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.1")}, [16]) = 79 <0.000006> 15:21:04.254974 socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 20 <0.000007> 15:21:04.255018 connect(20, {sa_family=AF_INET, sin_port=htons(11211), sin_addr=inet_addr("x.x.x.x")}, 16) = -1 EINPROGRESS (Operation now in progress) <0.000019> 15:21:04.255059 poll([{fd=20, events=POLLIN|POLLOUT|POLLERR|POLLHUP}], 1, -997199722) = 1 ([{fd=20, revents=POLLOUT}]) <0.000098> 15:21:04.255182 getsockopt(20, SOL_SOCKET, SO_ERROR, [0], [4]) = 0 <0.000005> 15:21:04.255219 sendto(20, "get "..., 38, MSG_DONTWAIT, NULL, 0) = 38 <0.000014> 15:21:04.255255 poll([{fd=20, events=POLLIN|POLLERR|POLLHUP}], 1, -997199722) = 1 ([{fd=20, revents=POLLIN}]) <0.000174> 15:21:04.255456 recvfrom(20, "VALUE "..., 8192, MSG_DONTWAIT, NULL, NULL) = 3655 <0.000007> 15:21:04.367211 socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 21 <0.000009> 15:21:04.367408 socket(PF_INET, SOCK_DGRAM, IPPROTO_IP) = 22 <0.000010> 15:21:04.367445 connect(22, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("0.0.0.0")}, 28) = 0 <0.000010> 15:21:04.367511 poll([{fd=22, events=POLLOUT}], 1, 0) = 1 ([{fd=22, revents=POLLOUT}]) <0.000007> 15:21:04.367551 sendto(22, "e("..., 43, MSG_NOSIGNAL, NULL, 0) = 43 <0.000027> 15:21:04.367614 poll([{fd=22, events=POLLIN}], 1, 5000) = 1 ([{fd=22, revents=POLLIN}]) <0.000008> 15:21:04.367663 recvfrom(22, "e("..., 1024, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.1")}, [16]) = 79 <0.000007> 15:21:04.367761 connect(21, {sa_family=AF_INET, sin_port=htons(3306), sin_addr=inet_addr("x.x.x.x")}, 16) = -1 EINPROGRESS (Operation now in progress) <0.000025> 15:21:04.367837 poll([{fd=21, events=POLLIN|POLLPRI}], 1, 30000) = 1 ([{fd=21, revents=POLLIN}]) <0.000246> -- snip -- 15:21:04.381269 shutdown(21, 2 /* send and receive */) = 0 <0.000011> 15:21:04.382572 shutdown(11, 1 /* send */) = 0 <0.000015>
このログの黄色の部分は、11211番ポートへ接続してgetという文字列を送信してることからmemcachedへの問い合わせでしょう。また、水色の部分は3306番ポートに接続しているので、MySQLへ接続していることがわかります。
そして、memcachedから値を取得し、MySQLへ接続するまでに100ms以上の時間がかかっていることが読み取れます。したがって、この処理のボトルネックは、SQLでもネットワークでもなく、MySQLへ接続するまでのアプリケーションコードの中にあると推測できます。
straceで把握できるのはここまでですが、アプリケーションのボトルネックを発見するための有力な手がかりにはなるのではないでしょうか。
ただ、さすがに稼働中の全サーバの全プロセスをトレースするのは大変すぎるので、「問題が見つかればらっきー☆」な気持ちで、アクセスが増える時間帯だけ、一部のサーバだけでstraceをかけてみるところから始めるのがよいと思います。
まとめ
本来であれば、この手の問題は試験環境などでXDebugなどを使ってプロファイルを取るのが定石だと思いますが、試験環境で再現させる事が難しい事象も数多くあります。このようなケースでは、アプリケーションの中でデバッグプリントを仕込み、ログを大量に出力させながら調査するといった手法を取ったりすることもありますが、今回ご紹介したstraceや、ltrace、oprofile、LatencyTOPなどのツールをうまく利用することで、調査にかかる時間や手間を減らせる可能性もあります。
ただ、この手のツールはアプリケーションエンジニアには馴染みが薄く、どちらかというとサーバエンジニアやネットワークエンジニアの得意分野な気がしています。不慣れなツールを頑張って駆使しようとする意気込みも素敵ですが、どうしても困った時には、最寄りのサーバ担当者に「アプリケーションプロセスをトレースしてちょ!」と依頼してみるのも悪くはないと思っています。
まあ、要は何が言いたかったかというと、「これは僕が自分で解決しなければいけない問題さ!」と信じて一人で頑張るだけでなく、ダメもとでいいので、たまにはサーバ屋とか、ネットワーク屋とか、八百屋とか、魚屋とかにも相談してみると、もしかすると別の視点から問題解決の糸口が見つかるかもしれないよ!、、というお話でした。
最後に念のため補足
straceでapacheプロセスをトレースする際は、httpd.confに
MaxRequestsPerChild 0
を指定しないと、プロセスが勝手に終了してしまうのでご注意ください。
また、straceは昔からバグが多いことでも有名です。できるだけ新しいバージョンを使い、事前に安全な環境でテストしてから利用することをおすすめします。
Apacheのアクセスログをsyslog経由で出力するためのモジュールを作りました
皆さんは、負荷分散環境でのApacheのアクセスログをどのように取り扱ってますか?
通常、Apacheのログは動作サーバ上のローカルファイルとして出力されるので、 Webサーバを同時に何台も稼働させて負荷分散を行うような環境では、それらすべ てのWebサーバのログファイルを集めなければなりません。ローカルファイルとし て出力されるということは、Webサーバの台数分だけログファイルがばらけること を意味します。考えるだけでめんどくさいですね。
KLabでは、このApacheログを2パターンを使い分けて集めています。ひとつは syslogによるリモート出力を使い、全Webサーバからのログ出力を一か所に集中さ せる方法です。これは、CustomLogディレクティブにloggerコマンドを使用するこ とで可能です。
CustomLog "|/usr/bin/logger -p local6.info --" (書式文字列)
これにより、通常ファイル出力されるアクセスログをloggerコマンド経由でsyslog に渡すことができます。あとはsyslogの設定で、リモートのログ集積サーバに集め させるというわけです。
ただし、これには問題もあります。syslogのログ転送はUDPやバッファリングなど の関係で取りこぼしがどうしても発生してしまう点です。いくつか対策は考えられ ますが、私たちはこれに従来型のファイル出力のログを併用することで解決してい ます。これが2パターンの2つめで、すなわち従来のように各Webサーバ上で出力す るログファイルを日次バッチで集積するという方法です。詳細なログ解析などはこ の日次の集積バッチによるログファイルを使用し、その日その日の緊急のアクセス 確認などはsyslog出力の側を見る、という使い分けを行っているのです。
***さて、このsyslog出力について、もう一度CustomLogを確認します。loggerコマン ドへのパイプを用いていますので、httpdの子プロセスとしてloggerプロセスが多 数立ち上がることになります。
しかしふと考えてみると、syslogに送るって言ってもやることはsyslog()関数を 呼ぶくらい(他にもopenlog()とかありますが)なので、わざわざ外部プロセスにし なくても、Apache本体から出力できたっていいよね、と考えたわけです。
ということで、Apacheから直接syslogにアクセスログを出力するためのApacheモ ジュールを一つ作ってみました。
syslogに送る設定は、以下のようにCustomLogを変更して行います。
CustomLog syslog:foo (書式文字列)
"syslog:"をプレフィックスとして付加することで、mod_syslogがsyslogへの出力 であることを判別して処理を行いますが、パイプやファイルパス形式、つまり "syslog:"が付かない場合は引き続き従来と同じ処理が行われます。
***mod_syslogのやることは至ってシンプルです。Apacheは、ログ出力処理のための関 数2つを下に表わすように定義しています。
static ap_log_writer_init* ap_log_set_writer_init(ap_log_writer_init *handle); static ap_log_writer* ap_log_set_writer(ap_log_writer *handle);
ap_log_set_writer_init()は、ログ出力のための初期化処理(ファイルのオープン など)を行うための関数、またap_log_set_writer()は、実際のログ出力を行う際に 呼ばれる関数をそれぞれコールバック登録します。あとは、Apacheがそれらのコー ルバック関数を必要なタイミングで呼び出してくれます。
さらに上記の関数2つは返値を持ち、それぞれのコールバック関数の事前の値を表し ます。mod_syslogは"syslog:"のプレフィックス以外のログ出力先、すなわちパイプ やファイル出力については従来の処理に任せますので、mod_syslog自身のコールバッ ク関数登録と同時に従来のコールバック関数へのポインタを、この返値によって取得、保持します。
#define PREFIX_SYSLOG "syslog:" #define PREFIX_SYSLOG_LENGTH 7 static void * ap_syslog_writer_init(apr_pool_t *p, server_rec *s, const char* name) { syslog(LOG_DEBUG, "%s: prev_log_writer_init = %p, name = %s", __func__, prev_log_writer_init, name); if (strncasecmp(PREFIX_SYSLOG, name, PREFIX_SYSLOG_LENGTH) == 0) { return &dummy[0]; // NULL以外を返す。 // 同じ値(&dummy[0])かどうかをap_syslog_writer()関数で判別できればよい。 } if (prev_log_writer_init) { return prev_log_writer_init(p, s, name); } return NULL; } // ap_hook_pre_config()でフック登録 static int syslog_pre_config(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp) { ... if (!prev_log_writer_init) { void* f; f = ap_log_set_writer_init(ap_syslog_writer_init); if (f != ap_syslog_writer_init) { prev_log_writer_init = f; } f = ap_log_set_writer(ap_syslog_writer); // ap_syslog_writerの内容については省略 if (f != ap_syslog_writer) { prev_log_writer = f; } } return OK; }
つまり、上記の例で言えばprev_log_writer_initとprev_log_writerに従来の処理 へのコールバック関数のポインタが入るわけです。 なお、この2関数が返値を返すようになったのはApache2.2以降なので、Apache2.0 系ではそのままでは使用できません。一応、同名の関数から旧い値を返させるだけ で動作するところまでは確認していますが、もしお試しの際はご注意ください。
***このように、Apacheからアクセスログを出力するにあたり、モジュールの追加だけ でも結構な機能追加ができることが分かります。しかも、添付の例をご覧のように、 わずかなコード量で実現できています。あったらいいな、こんなことできたらうれ しいな、というちょっとした発想を柔軟に取り入れられるApacheの仕組みにはやは り目を見張るものがありますね。
まくおさんバージョンアップのお知らせ(Ver1.2.0)
まくおさんをバージョンアップしましたのでお知らせします。
主な変更点は以下のとおりです。
【Ver1.1.3→Ver1.2.0】
- 応答速度と安定性の向上
- エラーログに"[error]"という文字列を追加
- メンバがタイムアウトしたときの状況を詳細に出力するようにした
- msync --statusで、実行中のコマンドを表示するようにした
- msyncを複数同時実行したときに挙動がおかしくなる不具合を修正
- ファイルクローズに失敗したときに異常終了する不具合を修正
- msyncにdeleteオプションを指定したときの結果表示がおかしくなる不具合を修正
- その他細かい不具合を修正
【ダウンロード】
【注意】Ver1.2系とVer1.1系はプロトコルに互換性がありませんので、
バージョンアップは全サーバに対して実施して下さい。
MAKUOSANバージョンアップのお知らせ(Ver1.1.2)
MAKUOSAN をバージョンアップしましたのでお知らせします。
主な変更点は以下のとおりです。
【Ver1.1.1→Ver1.1.2】
- シャットダウンメッセージを送出しない不具合を修正
- Ver1.1.1には、makuosanの終了通知が送出されないという不具合がありました。そのため、makuosanを終了しても、しばらくの間メンバーリストに残ってしまうという問題がありましたが、これを修正しました。
【Ver1.0.1→Ver1.1.1】
- msyncコマンドに--deleteオプションを追加
- 送信元に存在しないファイルを転送先で消すことができるようになりました。
- 転送速度が向上
-
ファイル転送速度が約1.5倍(当社比)になりました。
1対1での速度はまだscpにかないませんが、今後も随時改善していきます。
- エラーレポートを詳細に表示
- 転送先で発生したエラーなどを詳細に表示するようにしました。
【注意】
Ver1.0系とVer1.1系ではプロトコルに互換性がありません。
バージョンアップの際は全サーバに対して実施して下さい。
MAKUOSAN の詳細につきましてはプロ
ジェクトサイトを参照してください。
新機能の使用例
rsyncと同名のオプションなのでピンときた方もいると思いますが、 今回追加した--deleteオプションの使用例をご紹介します。 まずは、送信元サーバ(以下src)に適当にファイルを作ります。
src:~$ mkdir hoge src:~$ mkdir hoge/test1 src:~$ mkdir hoge/test2 src:~$ mkdir hoge/test3 src:~$ touch hoge/test1/aaa src:~$ touch hoge/test1/bbb src:~$ touch hoge/test1/ccc src:~$ touch hoge/test2/12345 src:~$ find hoge/ hoge/ hoge/test1 hoge/test1/aaa hoge/test1/bbb hoge/test1/ccc hoge/test2 hoge/test2/12345 hoge/test3
srcと転送先サーバ(以下dst)でmakuosanを起動します。
src:~$ makuosan -b ./
dst:~$ makuosan -b ./
srcからdstへhogeを転送します。
src:~$ msync -rv hoge update dst:hoge/test1/aaa update dst:hoge/test1/bbb update dst:hoge/test1/ccc update dst:hoge/test2/12345 update dst:hoge/test3 update dst:hoge/test1 update dst:hoge/test2 update dst:hogedstにhogeが転送されていることを確認します。
dst:~$ find hoge/ hoge/ hoge/test1 hoge/test1/ccc hoge/test1/aaa hoge/test1/bbb hoge/test2 hoge/test2/12345 hoge/test3
srcでhoge/test1を削除します。
src:~/work$ rm -fr hoge/test1 src:~/work$ find hoge hoge hoge/test2 hoge/test2/12345 hoge/test3
--deleteオプションを付けて再度転送します。
src:~$ msync -rv --delete hoge delete sag14:hoge/test1/ccc delete sag14:hoge/test1/aaa delete sag14:hoge/test1/bbb delete sag14:hoge/test1 update sag14:hoge
dstでもhoge/test1が消えています。
dst:~$ find hoge/ hoge/ hoge/test2 hoge/test2/12345 hoge/test3
DSASのファイル転送システムをオープンソースで公開します
DSASのファイル転送システムを、オープンソースで公開します。
その名は、makuosan(まくおさん:通称「まくお」)っていいます。
名前は冗談っぽいですが、内容はわりと真面目です(^^;
Webサイトの運用に欠かせない作業のひとつに、「デプロイ」という作業があります。 これは、新しいプログラムやデータなどをWebサーバに設置して利用できるようにす ることを指していますが、サイトの規模が大きくなってWebサーバの台数が増えると、 それに比例してファイル転送にかかる時間も長くなっていきます。
一般的な話として、サイトの規模が大きくなるほど運用コストは増大しますが、 その要因のひとつとして「デプロイ時のファイル転送に時間がかかる」という 点がありました。そこで、できるだけ運用コストを抑える(作業者の負担を減 らす)ために、独自のファイル転送システムをこしらえてみました。
「まくお」は、マルチキャストを利用して、複数のサーバへ同時にディレクト リ構造を複製するファイル転送システムです。
【まくおの特徴】- 転送時間がサーバ台数に依存しない
- サーバが増えても、転送にかかる時間はほとんどかわりません。10台のサーバ に転送しても、20台のサーバに転送しても、ほとんど同じ時間で完了します。 ただし、転送時間は一番応答が遅いサーバの性能に引っ張られるので、同程度 のスペックのサーバで構築された環境で利用することが望ましいです。
- すべてのサーバで同時にファイルが更新される
- マルチキャストを使って全サーバへ同時にファイルを転送します。そのため、 「このサーバのファイルは更新されてるけど、あのサーバのファイルはまだ 更新されていない」といったことが起こりません。
- 面倒な設定は不要
- 「まくお」のメインプログラムは、全サーバにデーモンとして常駐させます。 それぞれのサーバに常駐している「まくお」は、互いの存在を確認しあうこ とで、自動的にネットワーク上のサーバ構成を把握します。サーバを増設も しくは撤去する際においても、既設サーバの設定を変更する必要はありません。
「まくお」は、DSASの都合に合わせて仕様を決めていたので、一見意味不明 とも思えるオプションがあったり、この手のソフトでは当り前に付いている はずの機能が未実装だったりもしています。しかし、これでも誰かの役に立 てるのではないかと思い、オープンソースとして公開することにしました。
「まくお」は、DSASを運用する上で長年の懸念であった「ファイル転送の悩
み」を解消するためにこしらえました。今回は、どのような事に悩んでいて、
どう解決しようとしたのかをお話ししながら、作り始めたきっかけなどを紹
介したいと思います。
※機能の詳細や具体的な使用例などはプロ
ジェクトサイトを参照してください。
負荷分散環境におけるファイル転送の悩み
何十台ものWebサーバを負荷分散環境で運用していると、コンテンツの更新 や追加をするために、大量のファイルを複数台のサーバへ、できるだけ速く転 送する必要がでてきます。以下のようなスクリプトで、1台づつ順番に転送す るのは簡単で確実ですが、これではサーバ台数に比例して転送にかかる時間も 増えていってしまいます。
#!/bin/sh for i in host2 host3 host4; do rsync -aR /var/www/ $i:/ done
+-------+ rsync +-------+ | host1 |------->| host2 | +-------+ +-------+ +-------+ rsync +-------+ | host1 |------->| host3 | +-------+ +-------+ +-------+ rsync +-------+ | host1 |------->| host4 | +-------+ +-------+
また、このスクリプトのように転送先ホスト名をハードコードしてしまうと、 サーバが増えたり減ったりして構成が変わってしまった場合に、スクリプト を書き換える必要がでてきます。
サーバが故障したり増設した際に、うっかり書き換えを忘れてしまうと「フ ァイルが転送されない!」という障害に直結してしまいます。これはあまり にも危険すぎます。
この問題は、「稼働中のサーバを管理する機構」と「並列にファイル転送す る機構」を構築し、これらを組み合わせることで解決できると思います。稼 働中のサーバ一覧を取得し、下図のような論理的なツリー構造を形成してか らファイルを転送すればよいのです。
+-------+ +--->| host4 | +-------+ | +-------+ +--->| host2 |--+ | +-------+ | +-------+ | +--->| host5 | +-------+ | +-------+ | host1 |--+ +-------+ | +-------+ | +--->| host6 | | +-------+ | +-------+ +--->| host3 |--+ +-------+ | +-------+ +--->| host7 | +-------+
DSASの中には、実際にこのようにして全サーバのファイルを同期をしている システムがあります。この構成は、とても転送効率はよいのですが、システ ム構成や転送プログラムの処理が必要以上に複雑になってしまう点が悩まし いところです。その結果として、以下のような別の問題を抱えることになり ます。
- 軽微な変更や機能追加をするだけでもかなりの労力が必要
- 転送中に発生したエラーを拾うのが大変
- 不具合が発生したときに挙動を追うのが結構大変
- 動作検証が超面倒
速度を求めれば構成が複雑になり、シンプルにすると遅くなるというジレン マに陥ってしまいました。
DSASにおけるファイル転送のニーズ
DSASでファイルを転送するニーズは大きく分けて二種類あります。
まずひとつめは、サーバを増設したりHDDが故障したときなどの”システム
全体のミラーリング”です。これは、1対1の転送なのでrsyncが便利です。
ふたつめは、Webアプリケーションのデプロイや、素材ファイルのアップロ ードなどの”コンテンツの転送”です。運用フェーズでは、こちらのニーズ の方が圧倒的に多いです。冒頭でお話した長年の懸念とは、このニーズを満 たすことを指しています。今までは、この作業にrsyncを利用したスクリプト を使っていました。(今でも使ってますが(^^;
このスクリプトは、htdocsやwebapps以下のファイルをrsyncで全サーバに転 送するものです。コマンド一発で簡単に転送できて便利ですが、いろいろと 問題もありました。
サイトの規模やWebアプリケーションが肥大化するとともに、ファイル構成 やアーキテクチャが複雑になってきたため、サイト毎に特殊なニーズがでて くるようになりました。たとえば、、、
- あるタイミングで一部のファイルだけ転送したい
- このファイルは転送したいけどこれはしたくない
- 定期的に指定した部分だけを転送したい
- Webアプリケーションの動作と連動して転送したい
- あるイベントをトリガにして転送を開始したい
- できるかぎりリアルタイムに転送したい
- などなど
これまで使っていたスクリプトは、簡単にファイルを転送できる反面、きめ 細かなニーズに対応することが困難でした。そのため、効率よくファイルを 転送できるようにするためには、それぞれのニーズに見合ったスクリプトを 書く必要があります。しかし、実際に「rsync で全サーバに効率よくファイ ルを転送するスクリプト」を書くのはとても大変な作業です。
そこで、「全サーバに効率よくファイルを転送する」という機能をDSASの内 部に組み込んでしまい、「転送したいファイルやディレクトリのみを指定す るインターフェイス」をアプリケーション開発者に提供できれば、きめ細か なニーズに対して、簡単で柔軟に対応できるようになるのではないかと考え ました。これが「まくお」を作り始めたきっかけです。
「まくお」の仕組み
最後に、「まくお」がどのようにしてファイルを転送しているのかを簡単に
紹介します。
「まくお」では、複数のサーバへ同時にファイル転送をするために、マルチ
キャストを利用しています。送信元のサーバは、マルチキャストアドレスに
対してファイルの内容を送出します。転送先のサーバはそれを拾ってローカ
ルファイルを生成します。
+-------+ +-------+ | host2 |<-----+-------->| host4 | +-------+ | +-------+ | +-------+ | +-------+ | HOST1 |------->(Multicast)---->| host5 | +-------+ 224.0.0.108 +-------+ | | +-------+ | +-------+ | host3 |<----+-------->| host6 | +-------+ +-------+
送信元のサーバは、ひとつのファイルを何度も送りなおす必要がありません。 転送先のサーバが何台あったとしても、一度の送信ですべてのサーバへ転送 することができます。
とはいっても実際は、取りこぼしたパケットを補間するために再送処理が必 要になるので、同じデータが何度か繰り返し流れることもあります。その場 合でもファイルをまるごと再送するわけではなく、取りこぼしたパケットの みを再送するので、すべてのサーバへ一台づつ転送するよりも、ネットワー ク全体を流れるデータ量や転送時間は少なくて済んでいます。
今後の予定
現在の「まくお」には、以下のような問題(というか未実装の機能)があります。
- 転送先のファイルを消せない(rsync --delete相当の機能がない)
- SysVinit向けの、起動/停止スクリプトを作っていない
- マニュアルが不十分(><)
- ファイルの転送効率をもっと上げられるはず
これらは、今後のバージョンアップで随時対応していきたいと思います。
低温環境でのHDDの動作
あるデータセンターに導入したサーバーの初期セットアップ時に今までに経験したことのないHDDトラブルに見舞われました。今回はそのトラブルの原因究明と解決に至るまでのお話です。
HDDの故障?
7月、新データセンターでの初期セットアップ時にHDDの書き込み速度がやたら遅いディスクがあるという話が出てきました。 話を聞くと1台だけでなく複数台に発生しているとの事です。 その時の測定値は次のような感じでした。
実際の測定値
# dd if=/dev/zero of=/mnt/p0/test bs=1M count=2048 (RAIDカードの0番ポートにあるディスク) 2048+0 records in 2048+0 records out 2147483648 bytes (2.1 GB) copied, 43.8163 seconds, 49.0 MB/s # dd if=/dev/zero of=/mnt/p1/test bs=1M count=2048 (RAIDカードの1番ポートにあるディスク) 2048+0 records in 2048+0 records out 2147483648 bytes (2.1 GB) copied, 514.409 seconds, 4.2 MB/s # dd if=/dev/zero of=/mnt/p2/test bs=1M count=2048 (RAIDカードの2番ポートにあるディスク) 2048+0 records in 2048+0 records out 2147483648 bytes (2.1 GB) copied, 515.813 seconds, 4.2 MB/s # dd if=/dev/zero of=/mnt/p3/test bs=1M count=2048 (RAIDカードの3番ポートにあるディスク) 2048+0 records in 2048+0 records out 2147483648 bytes (2.1 GB) copied, 43.2759 seconds, 49.6 MB/s
今回のサーバは4台のHDDをもっておりそれぞれRAIDカードにつながっています。 そして、調査を行った結果、これらのサーバのうち数台で上記測定値のようにHDD4台のうち2台ないしは1台の書き込み速度が極端に遅いことがわかりました。 何度測定を行ってみても書き込みが遅いことは変わらなかったため、最終的にHDDの故障である可能性が高いと判断し、購入元(いつもお世話になっている協力会社さん)に調査をお願いしました。
データ保護機能 RAW(Read After Write)
1ヵ月後、協力会社さんから衝撃的な連絡が入ります。 なんと「データセンターの温度が低すぎる」というのです。 温度が高すぎるとHDDの故障しやすいなどはよく聞く話ですが、逆に温度が低すぎてHDDに影響がでるというのは初耳です。
協力会社さんの話によると、今回導入した Seagate製の Barracuda ES(SATA高耐久)シリーズには、RAW(Read After Write)という機能があり、その機能が働いて遅くなっているとの事でした。 このRAWというのは、ある温度を閾値として(58℃以上 または 18℃以下)有効になる機能です。 この機能が有効になると、データを書き込む際はブロックにデータを書き込みした直後にそのブロックを読み出し正確に書き込みができているかチェックするようになります。 この読み込みで、もしエラーが確認された場合は再度同じ手順で書き込みを行います。
つまり、低温時でのデータ書き込みには、通常の温度では発生しない「読み込み」と「書き込み内容の確認」処理が追加された結果、本来の1/10以下のパフォーマンスしか発揮できなくなったのです。 しかも、メーカー側からの回答によるとこの機能はOFFにすることができないとの事でした。
では、なぜこの温度(18度)を閾値にしてこのような機能を実装しているのでしょうか。
これはおそらく、HDDの記録媒体として利用されている磁性体の特性を考慮していると考えられます。 一般的なHDDに使われている磁性体は低温で保磁力が高く(磁力が抜けにくく)なります。 その結果、読み込みは問題ないが、書き込みをしてもそれが記録できていない状態が発生する可能性が出てくるため、Seagate 社はこのような処理をする事でデータの正確性を確保しようとしているのでしょう。 そして、おそらくは利用している磁性体で確実に書き込みできる最低温度が18度なのでしょう。
ちなみに、過酷な条件下で使われることが前提となるカーナビのHDDの場合は、低温でも保持力が高くならない磁性体を利用しており、氷点下でも利用ができるようになっているようです(デメリットとして記録できる容量が少なくなっています)。
事の顛末
では、実際の温度はどれくらいなのか調べてみました。
HDDの温度
# for i in `seq 0 3` ; do smartctl -d 3ware,$i -a /dev/twa0|grep Temp ; done 194 Temperature_Celsius 0x0022 020 040 000 Old_age Always - 20 (Lifetime Min/Max 0/16) 194 Temperature_Celsius 0x0022 017 040 000 Old_age Always - 17 (Lifetime Min/Max 0/15) 194 Temperature_Celsius 0x0022 017 040 000 Old_age Always - 17 (Lifetime Min/Max 0/15) 194 Temperature_Celsius 0x0022 018 040 000 Old_age Always - 18 (Lifetime Min/Max 0/15)
見事に、閾値の前後の温度となっています。 となると、この状態を改善するにはサーバの温度を上げるしかありません。
サーバの温度を下げる対応を行うことはあっても、上げるための対応などなかなか行うことはありませんのでかなり苦労しました。 ただ単純にサーバのFANを止めたりして冷却能力を下げると、CPUの温度が上昇した時に問題となる可能性があります。 そこで、FANの回転数を温度によって制御するモードに変更を行い高負荷時以外は回転数を落とすよう変更したり、ラック全体の冷却性能を落とすため、冷気の入ってくるラック下部に棚板を設置し空気の流れをさえぎる対応を行いました。
こうした対応の結果、各HDDの温度は閾値より高いものになり、すべてのHDDで期待したパフォーマンスを発揮できる状態となりました。
まとめ
今回、われわれにとって問題となったRAWという機能ですが、調べる限りSeagate社のみの機能のようで、他のHDDメーカーは実装していないようです。 これは、利用している磁性体の違いなのか、それともメーカーとしてのポリシーの違いなのかは残念ながらわかりません。 また、18度付近でのエラー率などの情報もありません。 今後、このような情報を各HDDメーカーが公開して下さる事を期待します。
知っていても損はしないkeepalivedの話 〜 notification_emailの罠
続きを読む
UML のコンソールを screen に表示させる 〜疑似端末を screen に結びつける
UML(User Mode Linux)では,ゲストOS上のコンソールデバイスのバインディング先として,色々なものが選べます.先日公開した LVS の実験パックでは,ホストOS上の(空き)コンソールデバイスに割り付けていました.この方法はお手軽なのですが,反面ホストOSのコンソールにアクセスできる環境じゃないと,ゲストOSのコンソールにもアクセスできなくなってしまいます.折角の疑似環境なのに,ハードウェアに縛られるのはあまり嬉しくありません.ということで,私がUMLを使うときには,コンソールのバインディング先を(ホストOSの)疑似端末デバイスにして,この疑似端末と screen を結びつけて使っています.
続きを読むOpenSSH クライアントの proxy -- 踏み台サーバを経由しての ssh
このような形にしている場合,DSAS にログインしようとする際は,一旦社内のサーバに ssh 接続する必要があって,小さなことですが一手間かかってしまいます.できればワンステップで接続できる方法が無いかと思って色々検索してみた(※)ところ,このページで
ProxyCommand
という設定項目を見つけました(見つけたのがボスの個人サイトなのは本当に偶然です(^^ ).
※
ProxyCommand
の記述は ssh_config(5)にあります.設定ファイルではコマンドラインオプションで指定できる以上のことが設定できる,ということに思い至らず,盲点でした.