いま知っておきたいLinux─WebアプリがOSのプロセスとしてどのように見えるか? を運用に生かす
Webアプリを動かして負荷をかけると、OSのプロセスという観点ではどのように見えるのでしょう? それを通して運用やトラブルシューティングではどういったことが分かるのでしょう? Linuxカーネルの開発者でもある武内覚(sat)さんによる解説です。
こんにちは、sat(@satoru_takeuchi)と申します。
コンピュータが誕生してから現在まで、最終的にエンドユーザが意識するアプリケーション開発はどんどん楽になっています。先人たちのたゆまぬ努力の結果、アプリ開発者はOSや、そのさらに下にあるハードウェアのことをほとんど意識することなく開発ができるようになりました。
しかし、「作ったアプリが、OSレベルでどのように動いているか?」が今一つピンと来なくて、モヤモヤしていないでしょうか。それが分からないため、実運用においてトラブルシューティングがうまくできなかったり、トラブルが起きないように何をすればいいか分からなかったりしていないでしょうか。
本記事では、そのようなことで困っている方々に向けて、基本的なOSについての知識、とくにLinuxについての知識について述べた上で、さらにどのようなことに配慮すればいいのかについて説明します。
本記事を読むために必要な前提知識は、次の3つです。
- 簡単なプログラムを作れる。言語は問わない
- Linuxのコマンドラインツールの基本的な使い方を知っている
- Linuxにおけるプロセスがどういうものかを知っている
あれもこれもと一度に説明しても消化不良になるだけですので、ここでは話をLinuxのプログラム実行単位であるプロセスに絞ります。具体的には、Webアプリを実行する際に、Linuxのプロセスのレベルではどのように見えるのかについて書きます。
本記事は、単に概念の説明をするだけではなく、実際にコマンドを実行することを重視しています。ぜひ、皆さん自身の環境でも試してみてください。
本記事における実行例は、全て以下の環境において実施したものです。
- CPU
- AMD Ryzen 5 PRO 2400GE(SMTは無効化1)
- OS
- Ubuntu 18.04
- パッケージ
- qemu-kvm、docker、go、curl
なお、本記事ではソフトウェアのインストール方法および使い方については述べません。
実験に使う「おみくじ」アプリについて
本記事で使用する実験プログラムは、次のように非常に単純な「おみくじ」アプリです。
- ユーザが、WebブラウザなどからURLにアクセスする
- Webブラウザから、サーバサイドプログラムにリクエストを送る
- サーバサイドプログラムが、ランダムに選ばれた結果を示すページをWebブラウザに返す
- 結果をもとに、Webブラウザが文字列を表示する
このWebアプリのサーバサイドプログラムのソース(Go言語)は次の通りです。ここでは名前をfortuneとします。
package main import ( "fmt" "log" "math/rand" "net" "net/http" "strconv" "golang.org/x/net/netutil" ) const Port = 8080 func main() { listener, err := net.Listen("tcp", ":"+strconv.Itoa(Port)) if err != nil { log.Fatal(err) } http.HandleFunc("/", HandleTopPage) limitListener := netutil.LimitListener(listener, 1000) err = http.Serve(limitListener, http.DefaultServeMux) if err != nil { log.Fatal(err) } } func HandleTopPage(w http.ResponseWriter, r *http.Request) { for i := 0; i < 100000000; i++ { } results := []string{"大吉", "吉", "小吉", "凶", "大凶"} fmt.Fprintf(w, "<html><body><p>%s</p></body></html>\n", results[rand.Intn(len(results))]) }
HandleTopPageの中にあるfor文は、本記事における説明をしやすくするため、リクエストのたびにCPUに疑似的な負荷を与える処理です。あまり気にする必要はありません。
アプリを動かす
fortuneをインストールするには、次のようなコマンドを発行します。
$ go get github.com/satoru-takeuchi/engineerhub-article-sample/cmd/fortune
この後に次のコマンドを発行するとfortuneが起動し、このシステムの8080番ポートにアクセスするとWebアプリにアクセスできるようになります。
$ ./fortune & $ ps PID TTY TIME CMD ... 28600 pts/0 00:00:00 fortune ... $
Webアプリにアクセスする
このWebアプリには、http://fortuneを動かしているマシンのIPアドレス:8080
というURL経由でアクセスできます。ここではWebブラウザからではなく、curlコマンドを使ってWebアプリに数回アクセスした結果を次に示します。
$ curl http://192.168.0.32:8080 # fortuneを動かしているマシンのIPアドレスが192.168.0.32 <html><body><p>吉</p></body></html> $ curl http://192.168.0.32:8080 <html><body><p>小吉</p></body></html> $ curl http://192.168.0.32:8080 <html><body><p>小吉</p></body></html> $
別の端末上でtopコマンドを実行しながら同じことをしてみると、topの上位にはfortuneプログラムはほとんど、あるいは全く出てきません。なぜなら、1つのリクエストを処理する程度では、CPUには全く負荷がかからないからです。
レスポンス時間も見てみましょう。
$ curl -sS http://192.168.0.32:8080 -o /dev/null -w "%{time_total}\n" 0.028721
出力の単位は秒なので、上記リクエストのレスポンス時間はおよそ30ミリ秒だと分かりました。この後、10回程度実行しましたが、どれも同じような数値でした。つまり、何も負荷がかかっていない状態では、Webアプリのレイテンシは30ミリ秒程度ということです。
Webアプリに負荷をかける
インターネット上に公開したこのアプリが何かの間違いで大人気になったと仮定して、アプリに大量のアクセスをしてみましょう。次に示すaccessというbashスクリプトでは、第1引数で与えた並列度で、fortuneに対して合計10,000回リクエストをします(結果は重要ではないので捨てています)。
#!/bin/bash TOTAL=10000 FORTUNE_URL=http://192.168.0.32:8080 # 192.168.0.32はfortuneを動かしているマシンのIPアドレス if [ $# != 1 ] ; then echo "usage: $0 <concurrency>" >&2 exit 1 fi CONCURRENCY=$1 for ((i = 0; i < TOTAL/CONCURRENCY; i++)) ; do for ((j = 0; j < CONCURRENCY; j++)) ; do curl -sS $FORTUNE_URL >/dev/null & done for ((j = 0; j < CONCURRENCY; j++)) ; do wait done done
それでは、fortuneコマンドを実行中のマシンにおいて、topコマンドを実行した状態で、別のマシンからaccessに1という引数を与えて実行してみましょう。筆者の環境では、fortuneのCPU使用率(%CPUフィールドの値)はリクエストが終了するまでの間、60%近辺の値になりました。次に示すのはtopの出力例です。
$ top ... PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 4551 sat 20 0 631560 14952 5608 S 67.0 0.1 8:25.83 fortune # CPU使用率は67.0% ... $
この状態で、別の端末を使ってWebアプリのレスポンス時間を見てみると、負荷がない場合と同様に30ミリ秒程度でした。この後に、並列度(accessに与える引数の値)を増やしつつ、そのときのfortuneのCPU使用率、およびレイテンシを測った結果を次に示します。
並列度 | 平均CPU使用率 | 平均レイテンシ |
---|---|---|
1 | 60%程度 | 30ミリ秒程度 |
2 | 140%程度 | 30ミリ秒程度 |
5 | 200%程度 | 40ミリ秒程度 |
10 | 300%程度 | 50ミリ秒程度 |
20 | 300%程度 | 70ミリ秒程度 |
50 | 350%程度 | 150ミリ秒程度 |
100 | 380%程度 | 300ミリ秒程度 |
これによって、アクセス数が増えるごとにCPU使用率、およびレスポンス時間が増加することが分かります。なお、マシンに搭載されているCPU数が4なので、最大CPU使用率は400です。つまり、並列度が100の場合はシステムのCPUリソースはほぼ使い切ってしまっており、リクエストをさばき切れていないことが分かります。
これは当たり前といえば当たり前なのですが、実際に動かして確かめてみることが大事です。頭の中で「こうなるはず」と想像しているだけではなく、実際に動かしてみる方が、より深い理解が得られるのです。
本節の最後にfortuneプログラムを終了しておきましょう。
$ killall fortune
$
CPU使用率から何が分かるのか?
前節において分かったことが、実運用にどう関係してくるのでしょうか。次のような例を使って説明します。
- アプリのユーザから「このアプリのレスポンスが悪い」という苦情が来た
- 原因は、Webアプリへの同時リクエスト数が常に100程度と多いため(
access 100
を実行している状態に近い)、fortuneがCPU要因で処理をさばききれなくなること - ただし、皆さんはその原因をまだ知らず、トラブルシュートして見つける必要がある
このような場合、「これはアプリのあそこが悪いな」と既に当たりがついているような幸運がなければ、OSについての知識が欠かせなくなってきます。なぜならこれがなければ、そもそもレスポンスが悪い原因がCPUにあるのか、ストレージI/OやネットワークI/Oの問題になるのか、などの初期切り分けが全くできないからです。
初期切り分け
では、初期切り分けをやってみましょう。Linuxにはシステム全体の統計情報を表示するためのsarというコマンドがあります。次に示すのは、sarによってCPUの利用率を1秒ごとに表示するところです。
$ sar 1 Linux 5.0.0-29-generic (coffee) 2019年09月28日 _x86_64_ (4 CPU) 11時03分53秒 CPU %user %nice %system %iowait %steal %idle 11時03分54秒 all 0.74 0.00 0.50 0.00 0.00 98.76 11時03分55秒 all 0.75 0.00 0.00 0.00 0.00 99.25 11時03分56秒 all 0.50 0.00 0.50 0.00 0.00 99.01 ...
上述の結果では、CPU負荷はほぼかかっていません。一方で、accessを使って100並列のリクエストが発生しているときにsarコマンドを実行すると、次のようになります。
$ sar 1 Linux 5.0.0-29-generic (coffee) 2019年09月28日 _x86_64_ (4 CPU) 11時04分53秒 CPU %user %nice %system %iowait %steal %idle 11時04分54秒 all 91.77 0.00 0.75 0.00 0.00 7.48 11時04分55秒 all 93.75 0.00 0.00 0.00 0.00 6.25 11時04分56秒 all 94.75 0.00 0.00 0.00 0.00 5.25 11時04分57秒 all 96.77 0.00 0.50 0.00 0.00 2.74 11時04分58秒 all 92.98 0.00 0.00 0.00 0.00 7.02 11時04分59秒 all 94.47 0.00 0.00 0.00 0.00 5.53
補足しておくと、%userで示される値はユーザプログラムによるCPU使用率であり、かつ、ここで示される値は全CPUの平均値なので、最大値は100です。したがって、ユーザプログラムによってこのシステムはCPUリソースをほとんど使い切っていることが分かります。
この後、「誰がCPUリソースを食いつぶしているのか?」とさらに個々のプロセスを見てみると、fortuneプログラムのCPU使用率が高いことが分かります。ここでWebアプリへの、その先にあるfortuneへの同時リクエスト数が増えるとCPU使用率が上がるということを知っていて、はじめて「もしかしてリクエスト数が多過ぎることによって、CPUリソースを使い果たしているのでは……?」という仮説が立てられるのです。
さらにその先の調査
この後は、問題の原因が本当にリクエスト数が多過ぎることによるものかを確かめるために、同時リクエスト数がどれだけあるか、負荷とどれだけ連動しているかを確認することになります。
fortuneプログラムにはそのような情報は存在しないので、一時的にプログラムを改変することによって直接的に、あるいはfortune以外の情報(例えばネットワークの統計情報)から間接的に、同時リクエスト数を得られないかを考えたりします。
この調査によってリクエスト数が多過ぎることが原因だと分かれば、後は状況に応じて対処方法を考えることになります。対処方法には例えば次のようなものがあります。
- 多くのリクエストに耐えられるように、ハードウェアを増強したり、ソフトウェアを改善したりする
- 特定のホストからのリクエストが集中していて。DoS攻撃が疑われるようであれば、そこからのリクエストを拒否する
- 現実は非情であり、予算も工数もないので事情を説明して平謝りする
ここで運用エンジニアの視点に立ってみると、最初から次のような機能を入れておくと、これまでに述べたような解析が楽になることに気づけます。
- 単位時間あたりのリクエスト数が諸定の数を越えたら、ログを出力する
- →原因がリクエスト過多である可能性の有無を確認できる
- プログラム実行の要所要所で「どの時刻に何をしたか?」というログを出す
- →どの処理に時間がかかっているのかを確認できる
筆者は、経験が浅い開発者から「ログを仕込む際に、どこに何を仕込めばいいのかが分からない……」という相談を受けることがあります。そのようなときは、上記のように「トラブルの際にどういう情報があれば役立つのか」という観点を持てば、適切なログを仕込む能力が上がっていきます。
優れたソフトウェアを開発するには、「私は開発者なので運用のことには興味がない、知らなくていい」などとは言ってられないのです。
問題発生の予測・予防
ここまでは問題が起きてしまった後の話をしてきましたが、問題を予測・予防することも重要です。
例えば、次のような場合に運用エンジニアに通知が飛ぶようにする監視系を作っておくと、トラブルを予測するのに非常に便利です。
- 同時リクエスト数が一定以上の値になった
- 同時リクエスト数が急激に上がった
- CPU使用率が一定以上の値になった
- CPU使用率が急激に上がった
これらの通知をもとに、ユーザに問題が顕在化する前に運用エンジニアはいろいろな準備、および対処ができます。
IaaSを使ったサービスにおいては、リクエストをさばききれないと判断した際には、運用エンジニアを介さずに自動的に新しいインスタンスを立ち上げて、リクエストを当該インスタンスに負荷分散するというような高度なことも当たり前になっています。
Webサーバを介する場合
本記事では簡単にするため、Webアプリのサーバサイドでは、fortuneプロセスが直接HTTPリクエストを受け取っていました。しかし、現実的にはNginXやApacheなどのWebサーバを介して、fortuneプロセスにアクセスすることの方が多いでしょう。実はこの場合も、話が複雑になりますが、基本的な考え方は変わりません。
まず、sarなどによって、システム全体のCPU使用率が高いことが分かったとします。続いて、topなどによって、個々のプロセスのCPU使用率を見ます。ここで、fortuneのプロセスがCPUを大量に使っているのであれば、fortuneを詳しく調べます。fortuneのCPU使用率は大したことがないものの、WebサーバのプロセスがCPUを大量に使っているのであれば、Webサーバのプロセスを詳しく調べます。
ただし後者の場合は、「クライアントから受け取るリクエストはWebサーバのプロセスが受け取り、その後リクエストを転送されたfortuneが実際に処理をする」という関係が分かっていて、はじめて「リクエストがWebサーバで詰まっているために処時間がかかっているのでは」という仮説が立てられるわけです。
おわりに
本記事では、次のことを解説しました。
- Webアプリを動かして、負荷をかけると、OSのプロセスという観点ではどのように見えるか
- それをもとに、運用時には何に気を付ければよいか
- アプリを作るときに、どういうログを仕込めばよいか
これらの知識によって、皆さんのLinuxに対する理解が多少なりとも深まったのであれば幸いです。
武内 覚 (たけうち・さとる)@satoru_takeuchi satoru-takeuchi
SMTが有効な場合はCPU使用率の解釈方法が難しくなるので無効化しています。↩