人間とウェブの未来

「ウェブの歴史は人類の歴史の繰り返し」という観点から色々勉強しています。

CPU使用率100%のWebサーバをOSのチューニングだけでCPU使用率20%まで改善する

こんばんは、 @matsumotoryです。

hb.matsumoto-r.jp

上記エントリにおいて、プロセスの大量メモリ確保に伴うページテーブルサイズとベージテーブルエントリ数の肥大化によるcloneやexecveの性能劣化とCPU使用時間の専有問題、および、それらの解決方法についてシステムコールレベルで確認しました。

そこで今回は、システムコールやそのカーネル内部の処理の性能、というよりは、より実践的な環境であるApache httpdとmod_cgiを用いて、phpinfo()を実行するだけのCGIに対してベンチマークをかけた時にどれぐらいCPUのidleが空くか、システムCPUの使用量が変わるかを、前回示した解決方法の1つであるHugePagesを使うかどうかの観点で比較してみましょう。

特定条件下のWebサーバ環境のシステムCPUに起因する高負荷問題から、システムコールやカーネルのソースコードを読みながら根本原因の仮説をたて、それを前回エントリで検証してミクロな視点で効果を確認した上で、今回は再度実践的な環境で負荷検証を行って、OS・カーネルレベルの対応が全体として実践的な環境でどれぐらい効果があるかを確認したいという意図です。

まずはページサイズがデフォルト4KBのhttpd

弊社では超高集積なWebホスティングを提供しているので、httpd+mod_cgiにおいては、その超高集積を再現すべく10万vhostを作成しておいて、予め各preforkされたhttpdプロセスが4GB程度メモリを確保している状態を再現します。これは、上記エントリでcloneの速度を検証した時と同じ環境(CPUコア24個、メモリ32GB、256個のprefork MPM)になります。

  • 以下のように普通に起動
/usr/sbin/httpd

で普通に起動した後、以下のベンチマークを実施します。このベンチマークによって、よくある高負荷状態を再現します。

ab -l -HUser-Agent:hoge -k -c 150 -n 10000000 http://test.example.jp/info.php
  • ベンチマーク中のtop

そして、ベンチマーク中のCPU使用率をコア単位で見てみましょう。

top - 18:07:01 up 9 days, 13 min,  4 users,  load average: 140.95, 88.39, 43.33
Tasks: 689 total, 151 running, 538 sleeping,   0 stopped,   0 zombie
Cpu0  :  4.3%us, 95.7%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu1  :  3.6%us, 95.7%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.7%si,  0.0%st
Cpu2  :  3.0%us, 96.7%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.3%si,  0.0%st
Cpu3  :  7.2%us, 92.4%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.3%si,  0.0%st
Cpu4  : 12.2%us, 87.5%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.3%si,  0.0%st
Cpu5  :  3.6%us, 96.1%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.3%si,  0.0%st
Cpu6  :  4.3%us, 95.7%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu7  :  4.3%us, 95.7%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu8  :  9.2%us, 90.8%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu9  :  5.9%us, 94.1%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu10 : 13.8%us, 86.2%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu11 : 19.5%us, 80.5%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu12 :  4.0%us, 96.0%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu13 :  3.6%us, 96.1%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.3%si,  0.0%st
Cpu14 :  3.3%us, 96.7%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu15 :  9.2%us, 90.5%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.3%si,  0.0%st
Cpu16 : 15.2%us, 84.5%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.3%si,  0.0%st
Cpu17 :  8.3%us, 91.4%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.3%si,  0.0%st
Cpu18 :  7.2%us, 92.8%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu19 : 18.8%us, 81.2%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu20 : 15.5%us, 84.5%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu21 : 10.6%us, 89.4%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu22 : 24.1%us, 75.9%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu23 :  7.3%us, 92.7%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Mem:  32862764k total, 29007852k used,  3854912k free,    13300k buffers
Swap: 16777212k total,   222652k used, 16554560k free,   199396k cached

このように、通常のページサイズではidleは0%であり、特にシステムCPUの専有時間が90%前後と非常に大きいことがわかります。ようするに完全にCPUを100%使いきっている状況といえます。

  • perfでkernelのシンボルのCPU使用率を確認

続いて、ユーザランドとカーネルで実行されいてる処理のシンボルを統一的にみて、その使用率を確認します。

Samples: 12M of event 'cycles', Event count (approx.): 873395738882               
 21.55%  [kernel]                       [k] zap_pte_range   
 20.55%  [kernel]                       [k] copy_pte_range   
  5.72%  [kernel]                       [k] change_pte_range  
  5.47%  [kernel]                       [k] page_remove_rmap   
  5.00%  [kernel]                       [k] free_pages_and_swap_cache 
  3.47%  ld-2.12.so                     [.] do_lookup_x   
  3.16%  [kernel]                       [k] release_pages    
  2.37%  ld-2.12.so                     [.] strcmp   
  1.48%  [kernel]                       [k] vm_normal_page              
  1.43%  ld-2.12.so                     [.] _dl_map_object  
  1.13%  ld-2.12.so                     [.] _dl_relocate_object        
  1.10%  [kernel]                       [k] copy_page  
  1.04%  [kernel]                       [k] __tlb_remove_page   
  0.91%  [kernel]                       [k] clear_page_c_e  
  0.89%  libc-2.12.so                   [.] _int_free   
  0.71%  [kernel]                       [k] _raw_spin_lock 
  0.58%  libc-2.12.so                   [.] _int_malloc  

これまでの調査、及び、前回のエントリの調査のとおり、pte(ページテーブルエントリ)の数に比例して処理コストが高くなるシンボル(cloneから呼ばれるcopy_pte_rangeevecveから呼ばれるzap_pte_rangeなど)が支配的であることがわかります。

これは、非常に実用上厳しい状況ですね。

HugePagesを使って起動したhttpdの場合

では、前回のエントリで調査したとおり、pteを少なくすることでcloneから呼ばれるcopy_pte_rangeevecveから呼ばれるzap_pte_rangeのコストを大幅に削減する事が原理的にわかりました。それが実環境やベンチマーク上でどのような効果があるかをより詳細に確認します。

  • 以下のようにHugePagesを使ってhttpdを起動
HUGETLB_MORECORE=yes LD_PRELOAD=/usr/lib64/libhugetlbfs.so /usr/sbin/httpd

HugePagesが既に20GB確保されている状態で、上記のようにlibhugetlbfs経由で起動します。HugePagesの確保の仕方は前回のエントリの調査を参照してください。

次に同様のベンチマークを流します。

  • top
top - 15:51:57 up 8 days, 21:58,  3 users,  load average: 87.35, 50.87, 28.25
Tasks: 599 total,   7 running, 592 sleeping,   0 stopped,   0 zombie
Cpu0  :  9.6%us, 16.8%sy,  0.0%ni, 72.6%id,  0.3%wa,  0.0%hi,  0.7%si,  0.0%st
Cpu1  : 19.1%us, 21.1%sy,  0.0%ni, 59.4%id,  0.0%wa,  0.0%hi,  0.3%si,  0.0%st
Cpu2  : 17.6%us, 18.0%sy,  0.0%ni, 64.4%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu3  : 10.2%us, 15.6%sy,  0.0%ni, 73.6%id,  0.3%wa,  0.0%hi,  0.3%si,  0.0%st
Cpu4  : 12.5%us, 17.9%sy,  0.0%ni, 69.3%id,  0.0%wa,  0.0%hi,  0.3%si,  0.0%st
Cpu5  : 12.7%us, 18.7%sy,  0.0%ni, 68.0%id,  0.3%wa,  0.0%hi,  0.3%si,  0.0%st
Cpu6  : 13.6%us, 22.8%sy,  0.0%ni, 63.3%id,  0.3%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu7  : 10.1%us, 35.8%sy,  0.0%ni, 53.7%id,  0.3%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu8  : 10.3%us, 26.8%sy,  0.0%ni, 62.9%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu9  : 14.6%us, 26.9%sy,  0.0%ni, 58.5%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu10 :  7.5%us, 23.6%sy,  0.0%ni, 68.8%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu11 : 16.7%us, 23.2%sy,  0.0%ni, 60.1%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu12 :  9.1%us,  9.4%sy,  0.0%ni, 81.2%id,  0.3%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu13 :  7.7%us, 11.7%sy,  0.0%ni, 80.2%id,  0.3%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu14 :  6.4%us,  9.1%sy,  0.0%ni, 84.6%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu15 : 13.0%us, 10.4%sy,  0.0%ni, 76.3%id,  0.3%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu16 :  9.8%us, 11.4%sy,  0.0%ni, 78.8%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu17 :  7.4%us, 10.7%sy,  0.0%ni, 81.9%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu18 : 10.1%us, 11.8%sy,  0.0%ni, 78.1%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu19 : 11.7%us, 17.1%sy,  0.0%ni, 71.1%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu20 :  3.4%us, 12.8%sy,  0.0%ni, 83.8%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu21 :  7.0%us, 11.0%sy,  0.0%ni, 81.9%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu22 : 15.4%us, 13.4%sy,  0.0%ni, 71.2%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu23 : 10.4%us, 11.1%sy,  0.0%ni, 78.2%id,  0.3%wa,  0.0%hi,  0.0%si,  0.0%st
Mem:  32862764k total, 22727968k used, 10134796k free,   151744k buffers
Swap: 16777212k total,    34764k used, 16742448k free,   944064k cached

このように見た瞬間効果が分かる程度に、システムCPUも90%前後で頭打ちしてたものがコア平均15%レベルまで改善。idleも0%の全く空きなしの状態から、一気にコアによっては80%程度まで空きができ改善していることが分かります。また、性能も概ね数倍以上になっていました。CPU使用率も、デフォルトのページサイズで100%使用していた状態と比べると、全体的にCPU使用率20%から30%といった値まで改善することができています。

  • perf

最後にperfでこれまで支配的にシステムCPUを専有していたpte周りの処理を確認してみましょう。

Samples: 17M of event 'cycles', Event count (approx.): 268305691276                                                                                                          
  7.10%  [kernel]                       [k] rwsem_spin_on_owner
  6.47%  [kernel]                       [k] anon_vma_interval_tree_insert
  6.04%  ld-2.12.so                     [.] do_lookup_x                  
  5.60%  [unknown]                      [.] 0x00007fbbab2c8bff           
  4.96%  [kernel]                       [k] osq_lock
  4.79%  ld-2.12.so                     [.] strcmp       
  4.32%  [kernel]                       [k] copy_page    
  1.85%  ld-2.12.so                     [.] _dl_map_object
  1.71%  [kernel]                       [k] native_queued_spin_lock_slowpath
  1.28%  [kernel]                       [k] zap_pte_range                                                                                                                    
  1.23%  libc-2.12.so                   [.] _int_free                       
  1.13%  ld-2.12.so                     [.] _dl_name_match_p    
  1.13%  ld-2.12.so                     [.] _dl_relocate_object                                                                                                              
  1.10%  libc-2.12.so                   [.] _int_malloc     
  1.06%  [kernel]                       [k] anon_vma_interval_tree_remove
  0.99%  [kernel]                       [k] clear_page_c_e               
  0.91%  [kernel]                       [k] page_fault                   

このようにほぼpteに関する処理のオーバーヘッドがなくなったことがわかります。

まとめ

HugePages化することによって、24コアすべてのCPUが主にシステムCPU使用時間によって専有されるような状況下において、idleをコアによっては80%近く空けることができるようになり、CPU使用率もほぼ100%から10%までシステムCPUを低減することができました。

これは、fork -> execve suexec -> execve php-cgi における、pteのcopyやzapのループが単純計算で1/512の数に大幅低減されるから、と考えられます。

特に、前回着目したcloneだけでなくexecvezap_pte_rangeなどでも効いてくるので、結果的には非常にシステムCPUを節約できた事になります。

今後は、これによってCPUのidleが作れる様になった一方で、このようなfork元のプロセスが4GBを抱えるような状況で、CGIを実行する場合にそこからCPUをさらに効率よく使ってレスポンスを速く返すためにはどうしたらよいか、また、マルチプロセス対応の自作cgiデーモンによって起動時からプロセスの抱えるメモリサイズを減らしておいてfork(やvfork)execのみを担当する、といったアプローチについても調査・開発しておりますのでご期待ください。