分散アプリケーションの依存発見に向いたTCP/UDPソケットに基づく低負荷トレーシング

この記事は、分散アプリケーションを構成するネットワークサービス間の依存関係マップを構築するための基礎技術の改善提案をまとめたものである。第8回WebSystemArchitecture研究会での発表と同等の内容であり、そのときのスライドを以下に掲載しておく。

また、本手法のプロトタイプと評価実験のためのコードを次のGitHubリポジトリに公開している。

1. はじめに

クラウドの普及により、サービス事業者は機能追加やアクセス増加への対応が容易となっている。その一方で、クラウド上に展開される分散アプリケーション内の構成要素の個数と種類が増加しているため、構成要素の依存関係が複雑化している。そのため、システム管理者が、システムを変更するときに、変更の影響範囲を特定できず、想定よりも大きな障害につながりうる。よって、システム管理者の手によらず、ネットワークサービス(ネットワーク通信するOSプロセス)の依存を自動で発見することが望ましい。

自動で依存を発見する手法として、ネットワーク通信を傍受するアプローチと、アプリケーションの通信の前後に依存発見のための処理をコードに追加するアプローチがある。アプリケーションやミドルウェアを修正する手間を避けるため、コードを追加せずに依存を発見できることが望ましい。また、既存のサーバやアプリケーション処理に悪影響を与えないように、低オーバヘッドで依存を発見する必要がある。

Linuxカーネル内のソケットを傍受する手法12345は、分散アプリケーションの主要なネットワーク通信プロトコルがTCPまたはUDPであることに着目して、Linuxカーネル内のTCP/UDPの処理内容を傍受することにより、通信先と通信元の組(フロー)を追跡する。TCP/UDP処理に介入することにより、より上位のアプリケーション層プロトコルによらずに依存を発見可能となり、アプリケーションコードを修正する必要がなくなる。Nevesらによる追跡手法は4とDatadogの手法5は、カーネル内で傍受された同一のフローを一定時間内で集約する。集約処理により、TCP/UDPのメッセージ送受信数が増加した際に、カーネル空間からユーザ空間へのフローの転送に要するCPU負荷を低減させている。しかし、持続的な接続が利用される環境と比較して、TCPの短命接続数が大きな環境では、時間あたりの接続数が増加するため、フロー数が増大する。フロー数の増加に対して、カーネル空間からユーザ空間への転送コストが増加するという課題がある。

そこで、我々は、ネットワークサービス間のTCP/UDP通信による依存を自動で発見するために、フロー数の増大に対しても、低オーバヘッドを維持可能なカーネル追跡手法を提案する。手法は、ネットワークサービスが動作するホスト上のカーネル内で、同一のネットワークサービスとの通信であれば、その通信のTCP/UDPメッセージに含まれる複数の異なるフローを単一のフローに集束する。集束の結果、カーネル空間からユーザ空間へ転送されるフロー数が低減され、追跡処理に要するCPU負荷を低減できる。実装には、Linuxカーネル上のサンドボックス環境で、制約の範囲内で任意のプログラムを実行可能とするeBPF(extended Berkeley Packet Filter)を利用した。実験の結果、フロー数の増大に対して、提案手法がCPU負荷のオーバヘッドを2.2%以下に維持したことを確認した。また、本手法により発生するアプリケーションへの遅延オーバヘッドが十分に小さいことを確認した。

(追記:次の図に、各手法の差異を図解する)

2. 提案するカーネル内フロー集束法

クラウド上の分散アプリケーションでは、構成要素間のネットワーク通信を並行して処理するために、クライアントが同一のIPアドレスで複数の異なる短命ポートからサーバに接続することがある。例えば、Webアプリケーションであれば、Webサーバは複数のHTTP要求を並行して処理するために、複数のワーカープロセスを持ち、各ワーカープロセスが、同一のデータベースサーバに接続する。したがって、クライアントの異なる短命ポート番号から、同一の待ち受けポートへ接続する複数のフローを単一のフローと見なしても、依存が欠落することはない。そこで、複数のフローをカーネル内で集束させるための設計と実装を以下に示す。

2.1 設計

(図1: カーネル内フロー集束法によるトレースの全体像)

Input: Socket structure S, listening ports P
Output: Dump all bundled flows on H

new hash table H for storing bundling flows

unction get_listening_port_and_direction(S)
    if P.lookup(S.sport) then
        return S.sport, INCOMING
    else
        return S.dport, OUTCOMING
    end if
end function

function insert_flow(S,proto,msglen)
    lport, dir ← get listening port and direction(S) key ← {S.saddr,S.daddr,lport,dir,proto}
    stats ← H[key]
    if stats == NULL then
        Initialize stats stats.msglen ← msglen H.insert(key, stats)
    else
        stats.msglen+ = msglen
        H.update(key,stats) end if
end function

function probe__tcp_connect(S )
    insert flow(S, TCP, 0)
end function

function probe__tcp_accept(S )
    insert flow(S, TCP, 0)
end function

function probe__tcp_sendmsg(S,msglen)
    insert flow(S, TCP, msglen)
end function

function probe__tcp_recvmsg(S,msglen)
    insert flow(S, TCP, msglen)
end function

function probe__udp_sendmsg(S,msglen)
    insert flow(S, UDP, msglen)
end function

function probe__udp_recvmsg(S, msglen)
    insert flow(S, UDP, msglen)
end function

(アルゴリズム1: カーネル内フロー集束法)

図1に、ホスト上でフローを追跡する全体像を示す。一般に、ホスト上のユーザ空間に配置されたネットワークサービスは、カーネル内のソケットを通じて、TCP/UDP通信を行う。ホスト上に配置されたTracingプロセスは、起動時にフローを追跡するためのプログラムをカーネルへ転送する。このカーネルプログラムの動作手順は次のようになる。

ステップ1: カーネルプログラムが、TCPの接続開始、TCP/UDPのメッセージ送受信に対応する関数呼び出しを傍受する。

ステップ2: カーネルプログラムは、ソケット構造体を入力として、アルゴリズム1に示すカーネル内集約を実行する。アルゴリズム1は、フローの重みとして、データ転送量を計測する。Tracingプロセスの起動時に、フローを集束させ、格納するためのハッシュ表を作成しておく。傍受の対象となるカーネル関数が呼び出されるたびに、フローをハッシュ表に格納する。このとき、統計データとしてメッセージ長を更新する。ハッシュ表のキーとして、<送信元アドレス、送信先アドレス、待ち受けポート番号、フローの方向、IPプロトコル番号>の組を指定する。重み付きの依存グラフを構成する前提で、TCPのメッセージ送受信関数をそれぞれ傍受し、メッセージ長などの統計データを取得する。

次に、フローの方向を決定するための手順を示す。UDPは非接続型のプロトコルであるため、TCPのconnectとacceptのような接続を確立するための手順がない。そのため、待ち受け側は、起動時に、bindシステムコールにより、待ち受けポートを指定することに着目する。カーネルプログラムをbindシステムコールにアタッチさせ、引数からポート番号を取得し、カーネルプログラムの構造体に格納しておく。UDPのメッセージ送受信関数(PROBE__UDP_SENDMSGPROBE__UDP_RECVMSG)が、フローに含まれる宛先ポート番号と構造体内のポート番号を照会し、フローの方向を判定する。

TCPの場合は、カーネルプログラムが呼び出された関数が、connectシステムコールから呼ばれる関数(PROBE__TCP_CONNECT)であれば要求側、acceptシステムコールから呼ばれる関数(PROBE__TCP_ACCEPT)であれば待ち受け側となる。しかしながら、TCPのメッセージ送受信関数(PROBE__TCP_SENDMSGPROBE__TCP_RECVMSG)についても、TCPは双方向でメッセージを送受信するため、関数名から依存の方向を識別できない。そのため、UDPと同様に、bindシステムコールを傍受する方式により、依存の方向を識別する。

ステップ3: ユーザ空間内に配置されたTracingプロセスが、ハッシュ表から集束されたフローのリストを取得し、取得したフローをハッシュ表から除去する。これを一定の時間間隔で繰り返す。

2.2 実装

カーネルの関数とシステムコールにカーネルプログラムを傍受するために、Linuxカーネルの動的トレーシング技術であるKprobes(Kernel Probes) を利用する。Kprobesは、カーネルコードのアドレスにブレイクポイントを設定し、ブレイクポイントで事前に定義されたハンドラを実行できる。 また、eBPFのカーネルプログラムをKprobesのブレイクポイントにアタッチできる。カーネルプログラムは、ソケットにアクセスする必要があるため、カーネル内のオブジェクトであるstrcut sock構造体、または、struct sk_buff構造体を引数か返り値にとるカーネル関数にアタッチさせる。

TCPでは、connectシステムコールに対応して呼ばれるtcp_v4_connect関数と、acceptシステムコールに対応して呼ばれるinet_csk_accept関数、TCPのメッセージ送信のためのtcp_sendmsg関数、TCPのメッセージ受信時に呼ばれるtcp_cleanup_rbuf関数にそれぞれカーネルプログラムをアタッチする。また、UDPでは、sendmsgシステムコールの文脈で呼ばれるip_send_skb関数と、recvmsgシステムコールの文脈で呼ばれるskb_consume_udp関数にそれぞれアタッチする。

集束されたフローを格納するために、eBPFのカーネルプログラム間や、カーネル空間とユーザ空間プロセスでデータを共有可能な汎用のデータ構造であるeBPF mapsを利用する。eBPF mapsは、ハッシュ表や配列などの複数種類のデータ構造をサポートする。本実装は、bindシステムコールにより束縛された待ち受けポート番号を格納するためにも、eBPF mapsを利用する。

ユーザ空間のTracingプロセスが集束フローを取得するために、本実装では、eBPF mapsから複数のエントリをアトミックに取得および削除するBPF_MAP_LOOKUP_AND_DELETE_BATCHシステムコールを利用する。 このシステムコールを1秒間隔で繰り返すことにより、最新の集束フローを取得し続ける。

著者らは、以上の実装をGo言語のライブラリとして広く利用できるように、OSSとして公開している https://github.com/yuuki/go-conntracer-bpf。本ライブラリは、Weave Scope3などのネットワーク依存関係の可視化システムに組み込まれて利用されることを想定している。

3. 実験と評価

カーネル内フロー集束手法の有効性を確認するために、依存の追跡処理が、アプリケーションが稼働するホストに与えるCPU負荷と、アプリケーションの遅延を実験により評価する。

3.1 実験の環境と設定

計算機環境 実験用の計算機として、クライアントとサーバのそれぞれ1台ずつ、さくらのクラウドの仮想マシンを用意する。仮想マシンのハードウェア仕様は、CPUがIntel Xeon Gold 6212U 2.40GHz 6コア、メモリが16GiBであり、クライアントとサーバのそれぞれの仮想マシンは同一の仕様とする。各マシンのOSは、Ubuntu 20.10 Kernel 5.8.0であり、仮想マシン間のネットワーク帯域幅は1Gbpsである。

負荷生成 実験では、著者らが開発したTCP/UDP向けの負荷生成ツールconnperfにより、アプリケーションの負荷を擬似的に生成する。connperfのTCPとUDPでの通信内容は、単純なエコー方式であり、クライアントがサーバへメッセージを送信し、サーバが受信したメッセージをクライアントへ返送する。

比較手法 ソケットベースアプローチをとる既存の手法をカーネル内フロー集束法と比較する。 スナップショットポーリング手法1は、Linuxカーネル内のソケット情報を、カーネル空間とユーザー空間との間でメッセージ通信するための機構であるNetlinkとProcess Filesystem(procfs)を通じて、一定間隔でスナップショットを取得する。 ストリーミング手法2,3は、TCPではtcp_connect_v4関数とinet_csk_accept関数にアタッチして、接続確立時に生成されるフローのみを取得する。ユーザ空間とカーネル空間との間のイベント転送に要するCPU利用率を比較するため、イベント転送箇所以外の処理内容を揃えなければならない。そのため、ストリーミング手法は、カーネル内集約手法と同様の集約処理をユーザ空間で処理する。 カーネル内フロー集約法4は、前節で述べた実装をもとに、フローを格納するeBPF mapのキーとして、送信元と送信先のそれぞれのポート番号を含める。

実験を再現できるように、著者らが管理するリポジトリ https://github.com/yuuki/shawk-experiments に、実験手順を自動化するためのプログラムを公開している。

3.2 実験の結果

CPU利用のオーバヘッドの計測

(図2: CPUオーバヘッド (a)TCP短命接続 (b)TCP永続接続 (c)UDP)

最初の実験では、TCPの短命接続とUDPに対して秒間のラウンドトリップ数、TCPの持続接続に対しては計測期間中に常時持続する接続数をそれぞれ変化させながら、CPU利用率の変化を計測した。新規接続数、持続的接続数、および、メッセージ数のパラメータを5,000から35,000の間で変化させた。そのときの計測値を図2に示す。

図2(a)に示すTCPの短命接続では、カーネル内フロー集束法は、ラウンドトリップ数の増加に対して、1.2%以下のCPU利用率を維持した。ストリーミング手法のCPU利用率は、2.9%から21.3%まで増加し、カーネル内集約法では、2.6%から11.5%まで増加した。スナップショットポーリング手法のCPU利用率は、各手法のなかで、最も低い1%以下となった。これは、スナップショットポーリング手法が、全ての短命接続を追跡できず、取得された接続数が小さくなるためである。

図2(b)に示すTCPの持続的接続では、カーネル内フロー集束法は2.2%以下のCPU利用率となった。スナップショットポーリング手法のCPU利用率は、3%から23.3%まで増加し、カーネル内フロー集約法では、2.1%から5.0%まで増加した。持続的接続の数が増加するにつれて、スナップショットポーリング手法は、スナップショット作成のためのスキャン処理に要するCPU利用が増加する。ストリーミング手法は、接続開始時のフローのみを追跡するため、持続的接続では、計測開始直後に1回だけ追跡する。そのため、ストリーミング手法の負荷は低くなる。

図2(c)におけるUDPの場合のCPU利用率は、TCPの短命接続と同様の変化の傾向となった。connperfにおけるUDPの処理内容は、TCPの短命接続における接続確立の処理が除去されたものとみなせる。そのため、UDPが、TCPの短命接続と同等の傾向になることは妥当である。Linuxのprocfsの制約上、スナップショットポーリング手法はUDPをスキャンできないため、本手法の計測は行わなかった。

TBD (図3: CPUオーバヘッド - 複数ネットワークサービス)

カーネル内フロー集束法は、通信先・通信元のホスト数が増大すると、単一のホストとの通信時と比較し、全体のフロー数に対する集束されたフロー数の割合(フローの集束率)が低下する。そのため、ステップ3におけるカーネル空間からユーザ空間へ転送するフロー数が増加する。その際に処理コストが増加する影響を計測するために、connperfのプロセスをコンテナ上に配置することにより、複数のホストとの通信を擬似的に再現する。

図3に、秒間ラウンドトリップ数または持続的接続数を10,000に固定し、コンテナの個数を200から1,000まで変化させたときのCPU利用率を示す。図3の凡例に、clientと表記したグラフはクライアントの、serverと表記したグラフはサーバのコンテナ数を変化させたときの計測値である。クライアントとサーバ、それぞれのコンテナ数によらず、カーネル内フロー集約法のCPU利用率は2%以下を示した。

遅延オーバヘッドの計測

(図4: 遅延オーバヘッド (a)TCP短命接続 (b)TCP持続的接続 (c)UDP)

図4は、CPU利用率の計測実験と同様の環境とパラメータを変化させたときのeBPFプログラムの実行時間を示す。スナップショットポーリング手法は、他手法のように通信経路へ介入しないため、本実験では、スナップショットポーリング手法による遅延オーバヘッドを計測しない。一度のラウンドトリップの間に、複数のeBPFプログラムが実行される場合には、各プログラムの平均実行時間を合計した値をプロットした。

図4では、各計測値のなかでも最大の実行時時間は、高々6マイクロ秒である。カーネル内フロー集束法の遅延オーバヘッドは、TCP短命接続では、カーネル内フロー集約法に対して0.4-4%増、および、ストリーミング手法に対して54-58%増である。TCP持続的接続における同オーバヘッドは、カーネル内フロー集約法に対して-7.0-0.7%増、UDPでは、カーネル内集約法に対して14-25%減、ストリーミング手法に対して8-12%増となった。ストリーミング手法は、TCPの接続確立時のフローのみを追跡するため、他手法と比較し、TCP短命接続ではオーバヘッドが低く、TCPの持続的接続では、オーバヘッドが0となった。

4. まとめ

クラウド上に展開された分散アプリケーションを構成するネットワークサービス間の依存を低オーバヘッドで発見するために、TCP/UDPのフローをカーネル内で集束するためのカーネル内フロー集束法を提案した。カーネル内フロー集束法は、TCPの短命接続と持続的接続のいずれの方式が利用されたアプリケーションであっても、低オーバヘッドでフローを追跡可能である。実験の結果、カーネル内フロー集束法は、1,000個以下の数のネットワークサービスへの通信であっても、CPU負荷が2.2%以下を維持できていることを確認した。また、アプリケーションに与える遅延オーバヘッドは、ラウンドトリップあたり最大でも6マイクロ秒程度であった。

今後は、本手法を活用し、フローデータを永続化するためのトレーシングアプリケーションの開発を進めていきたい。

あとがき

昨年末にeBPFを学び始めてから、以前の研究にeBPFならではの改善を加えて発展させるとこんな研究になった。アイデアとしてはナイーブではあるけど、既存の論文やツールに対する小さな貢献をちゃんと示せたようには思う。論文誌に英文で投稿して採録されたのでよかった。この研究の経験を踏まえて、eBPFの概要からBPFトレーシングツールの実装に至るまでのガイドラインとなるような記事を執筆中なのでお楽しみに。

参考文献

  • [1]: Chen, P., Qi, Y., Zheng, P. and Hou, D.: CauseInfer: Automatic and Distributed Performance Diagnosis with Hierarchical Causality Graph in Large Distributed Systems, IEEE Conference on Computer Communications (INFOCOM), pp. 1887–1895 (2014).
  • [2]: Lin, J., Chen, P. and Zheng, Z.: Microscope: Pinpoint Performance Issues with Causal Graphs in Micro-Service Environments, International Conference on Service-Oriented Computing (ICSOC), pp. 3–20 (2018).
  • [3]: Weaveworks Ltd.: Weave Scope, https://github.com/weaveworks/scope
  • [4]: Neves, F., Vila ̧ca, R. and Pereira, J.: Black-box inter-application traffic monitoring for adaptive container placement, Annual ACM Symposium on Applied Computing (SAC), pp. 259–266 (2020).
  • [5]: Datadog, Inc.: Datadog Network Performance Monitoring, https://docs.datadoghq.com/networkmonitoring/performance/
  • [6]: Sigelman, B. H., Barroso, L. A., Burrows, M., Stephenson, P., Plakal, M., Beaver, D., Jaspan, S. and Shanbhag, C.: Dapper, a Large-Scale Distributed Systems Tracing Infrastructure, Technical report, Google (2010).
  • [7]: The OpenTelemetry Authors: OpenTelemetry, https://opentelemetry.io/.

著者 坪内 佑樹(*1), 古川 雅大(*2), 松本 亮介(*1)
所属 (*1) さくらインターネット株式会社 さくらインターネット研究所、(*2) 株式会社はてな
研究会 第8回Webシステムアーキテクチャ研究会

RedisサーバのCPU負荷対策パターン

Redisは多彩なデータ構造をもつ1インメモリDBであり、昨今のWebアプリケーションのデータストアの一つとして、広く利用されている。 しかし、一方で、性能改善のための手法を体系的にまとめた資料が見当たらないと感じていた。 実際、最初にCPU負荷が問題になったときにどうしたものかと悩み、調査と試行錯誤を繰り返した。 そこで、この記事では、自分の経験を基に、RedisサーバのCPU負荷対策を「CPU負荷削減」「スケールアップ」「スケールアウト」に分類し、パターンとしてまとめる。

背景

Redisのハードウェアリソース使用の観点で重要なことは、Redisサーバはシングルスレッドで動作する(厳密には他にもスレッドあるがクエリ処理をするスレッドは1つ)ことと、Redisサーバ上のデータをメモリ容量を超えて保持できないことだ。 前者については、シングルスレッドで動作するということは、マルチコアスケールしないということであり、特定CPUコアのCPU使用率がボトルネックになりやすい。 後者については、Redisはディスクにデータを永続化2できるとはいっても、MySQLやPostgreSQLのようなメモリ上のバッファプールにデータがなければ、ディスク上のデータを参照するといったアーキテクチャではないため、基本的に全てのデータをメモリ容量以下に収めなければならない。

設定やクラスタ構成によっては、ディスクI/OやネットワークI/Oがボトルネックとなることもある。 例えば、Redisのデータの永続化方式[^2]としてRDBを利用していると、Redisサーバのメモリ使用量が大きいほど、ディスクにフラッシュするタイミングで、ディスクIOPSを消費する。 また、マスター・スレーブ構成をとる場合、スレーブのマスター昇格時の再同期処理でネットワーク帯域の使用がバーストすることがある。 さらに、Redisは低レイテンシで応答するため、1台あたりのスループットを高くしやすく、ホスト1台あたりのネットワーク帯域が大きくなることもある。

これらの中でも、特にCPU利用率と戦ってきた経験があり、Mackerelというサービスでは、以下のように、なぜか毎年CPU利用率と戦っていた。

f:id:y_uuki:20170910003050p:plainf:id:y_uuki:20170910003102p:plainf:id:y_uuki:20170910003054p:plain
issue

RedisのCPU負荷対策パターン

以下の図は、CPU負荷対策パターンをカテゴリごとにまとめたものになる。

RedisのCPU負荷対策のカテゴリとして、CPU負荷削減、スケールアップ、スケールアウトがある。 CPU負荷削減は、Redisサーバが実行する処理そのものを減らし、スケールアップはハードウェア性能そのものを向上させ、スケールアウトは複数のCPUコアやサーバに処理を分散させる。 実際には、各カテゴリの対策を組み合わせることが多いだろう。

CPU負荷削減

CPU負荷削減のためのテクニックとして、「multiコマンド」「Redisパイプライン」「Luaスクリプティング」「Redisモジュール」がある。

multiコマンド

MGETMSETMSETNXなどの複数のキーに対して操作するコマンドを使うことで、余分なCPU処理をせずにすむ。 これは、multiコマンドを使わずに複数回コマンドを発行することに比べて、リクエスト/レスポンスの往復回数が減り、Redisサーバ側でリクエスト受信とレスポンス送信のためのオーバヘッドを削減できるためだ。

Redisパイプライニング

Redisパイプライニング3は、クライアントがレスポンスを待たずにリクエストを投入しつづけることで、ラウンドトリップタイムを削減する。 ドキュメント[^4]には、ラウンドトリップタイムだけでなく、Redisサーバ上でのトータルでの処理量を削減できると書かれている。

ただし、全てのコマンド発行パターンにおいてパイプライニングが有効なわけではない。 Read After WriteとWrite after Readのような、レスポンスに含まれる結果を利用して次のコマンドを発行する場合には、レスポンスを待たないパイプライニングは有効ではない。 パイプライニングにおいて、クライアント側のコマンド実行順序は保証されるため、Write after Writeのパターンでは有効だ。 Read After WriteとWrite after Readのパターンで処理効率を向上させたい場合、後述するLuaスクリプティングを使うとよい。

一部のRedisクライアントは、pipelineという名前がついたインタフェースであっても、Redisプロトコルレベルでのパイプライニングではないことがある。 Redisプロトコルレベルでのパイプライニングを真のRedisパイプライニングと同僚と呼びあっている。 真のRedisパイプライニングかどうかは、tcpdumpでコマンド発行の度に同期的にQUEUD応答が返ってくるかどうかで確認できる。

パイプライニング自体は伝統的な技術であり、CPUプロセッサの命令パイプラインが有名だ。 ネットワークプロトコルの中では、例えばHTTPパイプライン4がある。Redisのドキュメント[^4]には、多くのPOP3実装でパイプライニングをサポートしていると書かれている。

Luaスクリプティング

Luaスクリプティング5は、Redisサーバに対してLuaコードを送り込んで実行できる機能だ。 具体的には、クライアントとサーバ間でコマンド発行と応答を往復させなければ一連の処理を、Luaで記述することでRedisサーバ上で処理を完結できる。

Redisパイプライニングのドキュメント[^4]の「Pipelining VS Scripting」の項を読むと、パイプライニングを利用できるような多くのユースケースにおいて、より効率的に処理を実行できると書かれている。 さらに、Redisパイプライニングでは対応できないコマンドパターンにおいて、ラウンドトリップタイムを改善できる。 Luaスクリプティングにより、例えば、HASH型に対するmultiコマンドは実装されていませんが、Luaで複数キーに対応したHMSET(MHMSET)のような処理をする関数を実装できる。

しかし、Luaコードの処理内容によっては、もともとクライアント側で計算していた処理をRedisサーバ側で実行するため、RedisサーバのCPU利用率が増加する可能性がある。

Redisモジュール(夢)

これまで、Redisのデータ構造として使えるのは、LIST、SET、HSET、ZSETなどの汎用のデータ構造だけだった。 しかし、Redis 4.0からRedisモジュール6が追加され、独自のコマンドとデータ構造をC拡張により追加できるようになった。 これを使えば、アプリケーションに最適なコマンドを実装することで、CPU利用率に限らず、その他の性能も大幅に向上させることが理屈上は可能である。

自分では使ったことも作ったこともないが、Redis Modules Hubには検索エンジンやJSONフォーマットをサポートするような拡張が公開されている。

スケールアップ

スケールアップは、CPUをのクロック周波数を高いもに変更するか、CPUアーキテクチャの世代を新しいものにするかといった選択肢がある。 経験上、CPUのクロック周波数に対してほぼ線形にRedisサーバのCPU使用率が変化する。

前述のようにRedisサーバはシングルスレッドで動作するため、CPUのコア数が大きいものを選んでも意味はあまりない。 ただし、ディスクの永続化方式にRDBを選択している場合、Redisサーバプロセスからforkされた子プロセスが、メモリ上のデータをディスクに書き出す処理をバックグラウンドで実行するため、最低でも2コアにしておくと安心だと思う。

ただし、EC2を利用する場合、EC2インスタンスのCPUコアは論理コアなので、RDB利用の場合、4コアのインスタンスを選ぶことをすすめる。7 物理CPUコア数は論理コア数(vCPU数)を2で割った値になる。 例えば、c4.2xlargeは表記上のコア数は8だが、物理コア数は4となる。

スケールアウト

Redisのスケールアウトのための手法として、「参照用スレーブ」「垂直分割」、「水平分割」、「Redis Clusterによる水平分割」がある。 「参照用スレーブ」「垂直分割」、「水平分割」は、Redisに限らずMySQLなどのRDBMSにおいても、一般的なスケールアウト手法として知られている。8 参照用スレーブは名前の通り、参照クエリのみ分散できる。 更新クエリを分散するならば、「垂直分割」または「水平分割」が必要になる。 垂直分割と水平分割は、CPU負荷の分散以外に、メモリ使用の分散にも利用できる。

参照用スレーブ

Redisはレプリケーションによりマスター・スレーブ構成のクラスタを作成できる。 はてなでは、主にスタンバイサーバの作成に利用しており、KeepalivedによりVIPベースで冗長化している。

スレーブは他にも用途があり、参照用スレーブは、アプリケーションから参照クエリを向けるためのスレーブだ。 スレーブに読み込みクエリを投げることにより、マスターの負荷をスレーブへ分散できる。

以下の図のように、クライアントから複数のスレーブに向けて参照クエリを投げます。複数のスレーブに向けてロードバランシングするための手段はいろいろあるが、TCPロードバランサを挟むか、DNSラウンドロビンによるロードバランシングが一般的だ。

|-------------------------------------|
|              |----> redis (slave)   |
|        read  |                      |
| client ------|----> redis (slave)   |
|        |     |                      |
|        |     |----> redis (slave)   |
|        |                            |
|        |----------> redis (master)  |
|        read/write                   |
|-------------------------------------|

何も考えずに、すべての参照クエリを参照用スレーブに向けるのは危険だ。 Redisのレプリケーションは基本的に非同期であり、レプリケーション遅延があるため、クライアントからは結果整合性をもつデータストアとして扱う必要がある。 具体的には、マスターにデータを書き込んだときにスレーブにデータが転送され書き込まれるのを待たずにクライアントへ応答を返す。 したがって、応答がクライアントへ返った後に、書き込んだキーの参照をスレーブへ向けても、該当データが書き込まれている保証がない。 参照用スレーブを使う場合は、アプリケーションロジックがデータの一貫性を要求しないものかをよく確認しておく必要がある。

垂直分割

垂直分割は、データの種類ごとに複数のRedisサーバを使い分ける手法だ。 下記の図のように、データの種類A、B、Cに必要なデータを異なるRedisサーバに書き込むことで、参照と更新クエリを機能ごとに分散できる。

|------------------------------------|
|              |----> redis (種類A)  |
|              |                     |
| client ------|----> redis (種類B)  |
|              |                     |
|              |----> redis (種類C)  |
|------------------------------------|

可用性のために、各Redisサーバは冗長化されている必要があり、1つのクラスタで処理を捌くより、無駄なサーバが増えやすいというデメリットがある。 また、特定種類のデータに対するクエリの負荷が大部分を占める場合、その種類のデータを別のRedisサーバに移しても、結局分散先のRedisサーバのCPU負荷が問題になることがある。

水平分割

水平分割は、ある特定の種類のデータをレコードごとに分割し、それぞれのレコードを別のRedisノードへ配置する手法だ。 水平分割のことをシャーディングやパーティショニングと呼ぶこともある。

レコード分割の手法は様々であり、後述するRedis Clusterのようなミドルウェア側で分割する機構がないもしくは利用しない場合は、データの性質を利用する。 例えば、RedisにユーザIDに紐付いたデータを格納するとして、ユーザのIDをノード数で割った剰余の値により、配置するノードをマッピングするといった方法がある。 下記の図はノード数が3の場合を示している。 他にはユーザ名の頭文字がaの場合はノード1、bの場合はノード2といった分割の方法もある。

|------------------------------------------|
|              |----> redis (id % 3 == 0)  |
|              |                           |
| client ------|----> redis (id % 3 == 1)  |
|              |                           |
|              |----> redis (id % 3 == 2)  |
|------------------------------------------|

水平分割は、垂直分割同様に、CPU負荷の分散以外に、メモリ使用の分散にも利用できる。

この手法のデメリットとして、ノードの負荷の偏りと、リシャーディングの困難さの2つがある。

ノードの負荷の偏りは、特定のユーザ(もしくはブログIDや記事IDなど)に関するコマンドは必ずマッピング先のノードに向けて発行されるため、特定のユーザの活動が非常に活発な場合、負荷が特定のノードに偏ってしまうという問題がある。

リシャーディングの困難さは、ノード数を増減させるオペレーション(リシャーディング)に非常に手間がかかることだ。 マッピングの手法にもよりますが、上記のような剰余ベースでマッピングしていると、リシャーディング時に既存のマッピングが変更される。 既存のマッピングが変更されると、すでに書き込み済みのデータを新しいマッピング先のノードに移動する必要がある。 最もナイーブな方法で移動するなら、新旧のマッピングで配置が変更されるレコードを抽出し、新マッピングにしたがってデータを移動するバッチスクリプトを実行するといった手段がある。 バッチスクリプトを流す時間だけサービス停止できればよいが、これをオンラインでやろうとすると大変だ。 例えば、新旧のマッピングを意識したアプリケーションに改修しつつ、裏で新マッピングへの移動スクリプトを流すといった泥臭い運用が待っている。

さらに、ノードの負荷の偏りを平滑化する(リバランス)となるとさらに困難なことになる。 このリシャーディングとリバランスの難しさから、できるだけ水平分割を選択しないようにしている。

しかし、Redisをキャッシュとして用いる場合は、リシャーディングはもうすこし簡単になる。 レコードにTTLを設定しておき、マッピングが変更された後に、たとえキャッシュミスしたとしても、キャッシュ元のなんらかのデータソースからデータを引く実装になっていれば、アプリケーションの動作としては問題ない。TTLにより、旧マッピングのデータがずっと残っているということもない。 キャッシュミスをできるだけ抑えたい場合は、consitent hashingを使うと、リシャーディング時にマッピングの変更を少なく抑えられる。

Redis Clusterによる水平分割

Redis Cluster9は、複数のRedisサーバに対してデータを「自動で」水平分割する。 前述の水平分割の場合は、アプリケーション開発者がレコードを分散する処理を書き、リシャーディング運用をしなければなかった。 Redis Clusterを使うと、自動でキーとノードのマッピングを作成され、アプリケーションからのコマンドは自動で分散される。 さらに、redis-tribというRedisが提供しているオペレーションスクリプトを使ってリシャーディングとリバランスができる。

しかし、Redis Clusterにはdatabaseと複数キーのコマンドの扱いに制限がある。

まずdatabaseについて、複数のdatabaseを使えない制限がある。databaseはRedisのキー空間を分ける機能で、キーの重複を考えなくてすむため、データの種類ごとにdatabaseを分割するといった使い方をする。すでにアプリケーションが複数のdatabaseを使っている場合、途中からRedis Clusterに移行するには手間が発生する。

次に複数キーコマンドについて、複数キーにまたがるコマンドは、全てのキーが同じノード上になければならないという制約がある。 そこで、hash tagsを使うと、キー文字列の中の部分文字列を中括弧で囲うことで、同じ部分文字列をもつキーであれば、同じノードにマッピングするようになる。 したがって、hash tagsにより、キーとノードのマッピングをある程度開発者がコントロールできる。 パイプライニングやLuaスクリプティングについても、基本的に操作対象の全てのキーが同じノード上にある必要がある。 ただし、一部のクライアント10では、同じノード上にキーがなくても、複数のノードにクエリを投げてマージするといった実装が入っていることがあるようだ。

これらの制限に加えて注意する点は、水平分割の項で説明したノードの負荷の偏りを、Redis Clusterを使ったからといって防げるわけではないということだ。 システムのワークロードによって、よくアクセスされるキーが存在すると、特定ノードへ負荷が偏ることは避けられない。 ただし、slotを別のノードへ移動させるコマンドにより、負荷の高いslotを移動させてマニュアルで負荷の偏りをある程度均すことは一応可能である。

ちなみに、Redis Clusterは、Mackerelの時系列データベース11で利用している。 まだ投入して間もないため、運用ノウハウを積んでいくのはまだまだこれからというところになる。

その他

上記カテゴリのいずれにも該当しないチューニング例として、RPS12とCPU Affinityによるネットワーク割り込み負荷をRedisプロセスが処理するCPUコアとは別のコアへ分散がある。 この手法は、はてなのLinuxネットワークスタックパフォーマンス改善事例13の後半で紹介している。

スライド資料

あとがき

発表から少し時間が空いてしまいましたが、この記事の内容は、Kyoto.なんか #3での発表を加筆修正したものになります。

これらの内容は一人でやったものではなく、自分は主にアーキテクチャや作戦を考える係で、アプリケーションの実装は主に同僚のid:mechairoiさんとid:itchynyさんによるものです。 真のRedisパイプライニングについては、同じく同僚の id:ichirin2501 さんに教えてもらいました。

Redisは好きなミドルウェアで、[^11]でもお世話になっています。 オンディスクDBは、RDBMSならMySQLやPostgreSQL、分散データストアならHBase、Cassandra、Riak、Elasticsearchなどさまざまな選択肢がありますが、インメモリDBはウェブ業界の中では実績や使いやすさを考えるとRedis以外の選択肢をあまり思い付きません。

しかし、Redisがこれほど利用されているにもかかわらず、議論の土台となる運用をまとめた資料がないと感じていました。これは、単に自分が発見できていないだけかもしれません。 以前から、Webの技術を自分の中で体系化することに興味があり、過去の試みには 2015年Webサーバアーキテクチャ序論 - ゆううきブログWebシステムにおけるデータベース接続アーキテクチャ概論 - ゆううきブログ といった記事があります。 そこで、今回は、RedisのCPU負荷対策についてまとめまてみました。メモリやネットワークI/O全般について書ければよかったのですが、まだ知見が及ばないところも多いので、一旦CPUの部分のみにスコープを絞りました。

ところで、今年のはてなサマーインターン2017の大規模システムコースでは、Serfで使われているGossipプロトコルベースの自律分散監視や、グラフDBのNeo4jを用いた分散トレーシングといったとてもおもしろい成果がでています。

参考資料

ISUCON予選突破を支えたオペレーション技術

ISUCONに参加する会社の同僚を応援するために、ISUCONの予選突破する上で必要なオペレーション技術を紹介します。 自分がISUCONに初出場したときに知りたかったことを意識して書いてみました。 一応、過去2回予選突破した経験があるので、それなりには参考になると思います。 といっても、中身は至って標準的な内容です。 特に、チームにオペレーションエンジニアがいない場合、役に立つと思います。

今年のISUCON6は開催間近で、まだ予選登録受付中です。

※ 文中の設定ファイルなどはバージョンやその他の環境が異なると動かなかったりするので必ず検証してから使用してください。

ISUCONでやること (Goal)

ISUCONでやることは、与えられたウェブアプリケーションをとにかく高速化することだけです。 高速化と一口に言っても、複数のゴールがあります。ウェブアプリケーションの場合は以下のようなものでしょう。

  • レスポンスタイムが小さい
  • スループット (req/s) が大きい
  • CPUやメモリなどリソース消費量が小さい

ISUCONでは、基本的にはレスポンスタイムを小さくすることを目指します。 これは実際のウェブアプリケーションにおいても、ユーザ体験に最も直結するレスポンスタイムを改善することが重要なので理にかなっています。

とはいっても、リトルの法則により、安定した系において、レスポンスタイムが小さくなれば、スループットは向上するため、レスポンスタイムとスループットは相関します。

リソース消費量の改善は、レスポンスタイムに寄与するというよりは、サーバ管理にまつわる人的または金銭的なコストを下げることに寄与します。 例えばメモリが余っているのに、メモリ使用量を削減しても、レスポンスタイムには影響しません。ただし、そういった無駄を省くことで、アプリケーションの処理効率がよくなり、結果としてレスポンスタイムが良くなることはあります。

ISUCONは、具体的には、以下のような要素で構成されていると考えます。

  • サーバを含む環境構築
  • OS・ミドルウェアの選択とチューニング
  • アプリケーションロジックとデータ構造の改善

ここでは、前者2つをオペレーションの領域、後者をプログラミングの領域とします。 必ずしも、オペレーション要員が必要ということはなく、あくまで領域なので、分担することも多いと思います。 自分の場合、過去2回とも、チームの構成上、オペレーションまわりはほぼ全部1人でやっていました。

ISUCONの考え方 (Principles)

自分が考えるISUCONの原則は、「オペレーション(System Engineering)で点を守り、 プログラミング(Software Engineering)で点をとる」です。

OS・ミドルウェアのチューニングが劇的な加点要素になることはあまりありません。 そのレイヤのチューニングはリソース消費量を小さくすることに寄与することが多いためです。

ただし、チューニングしていないために、劇的に性能が劣化することはあります。 例えば、InnoDBのバッファプールサイズを小さくしていると、データがメモリに乗り切らず、大量のディスクI/Oが発生し、スコアが大きく下がるはずです。

もちろん、アプリケーションロジックが薄いとそれだけOSやミドルウェアが仕事をする割合が大きくなるため、 OSやミドルウェアのチューニングによりスコアが伸びることはあります。

そうはいっても、基本的にスコアを伸ばす手段は、アプリケーションロジックとデータ構造の改善です。 これは実際のウェブアプリケーション開発の現場でも同じことが言えます。 雑な体感によると、パフォーマンスの支配率は、アプリケーションが8割、OS・ミドルウェアが2割程度(要出典)だと思っています。

ISUCONにおけるオペレーション

環境構築

予選の場合は、クラウド環境セットアップが必要です。ISUCON3、ISUCON4ではAWS、ISUCON5ではGCE、ISUCON6はAzureです。 アカウント作成やインスタンス作成がメインです。 当日のレギュレーションにも一応手順は記載されるはずですが、事前に触っておくと本番で混乱せずに済むと思います。

サーバログイン環境

例年であれば、/home/isucon 以下に必要なアプリケーション一式が入っています。

isuconユーザがもしなければ (useradd -d /home/isucon -m isucon) などで作成します。 さらに、/home/isucon/.ssh 以下に公開鍵認証するための公開鍵を設置します。 /home/isucon/.ssh/authorized_keysファイルを作成し、.sshディレクトリのパーミッションは700、authorized_keysファイルのパーミッションは600であることを確認します。(この辺のパーミッションがおかしいとログインできない) メンバー分のユーザを作成するのは面倒なので、共通ユーザ1つだけで問題ないと思います。

デプロイ自動化

Capistranoのような大げさなものは使わなくて良いので、以下のような雑スクリプトでデプロイを自動化しましょう。

やっていることはSlackへ開始デプロイ通知、git pull、MySQL、Redis、memcached、app、nginxの再起動、Slackへデプロイ完了通知ぐらいです。 MySQLやnginxもいれているのは、設定ファイルを更新したはいいが、再起動をし忘れるということがよくあるので、まとめて再起動してしまいます。 接続先のプロセスがしぬと、うまく再接続されない場合もなくはないので、再起動するのは原則バックエンドからが一応よいです。

リソース利用率把握とログの確認

ここでいうリソースとはCPU、メモリ、ディスクI/O、ネットワーク帯域などのハードウェアリソースを指します。 要はtopの見方を知っておきましょうということです。以前ブログに書いたので、参照してください。

冒頭でISUCONではレスポンスタイムを小さくするのがよいと述べました。とはいえ、リソース消費量を把握しておくことはボトルネック特定のヒントになるので重要です。 例えば、予選問題の初期状態ではMySQLのCPU負荷が支配的であることが多いかつディスクI/Oが高ければ、まずMySQLのバッファプールを増やしたり、クエリ改善が必要そうという程度のことはすぐわかります。 ISUCON4で話題になったCache-Controlの件も、ネットワーク帯域が限界であることにもっと早く気づいていれば、何かしら手は打てたかもと思います。(当時は思い込みでそこでボトルネックになってるとは思っていなかった。)

監視

去年のISUCONでは、自分たちのチームではMackerelの外形監視を使ってみました。 競技中に、アプリケーションのsyntax errorか何かで500でてるのに、ベンチマーカーを走らせてしまい、時間を無駄にしてしまうことがあります。 インターネット経由でポート80番が疎通できてさえいれば、アプリケーションが落ちていると、Mackerelの外形監視がすぐに通知してくれます。

MySQLデータサイズ確認

MySQLのデータサイズとして、テーブルごとのサイズや行数などを把握しておくと、クエリ改善の参考にできる。 行数が小さければ、多少クエリが悪くてもスコアへの影響は小さいため、後回しにできる。

mysql> use database;
mysql> SELECT table_name, engine, table_rows, avg_row_length, floor((data_length+index_length)/1024/1024) as allMB, floor((data_length)/1024/1024) as dMB, floor((index_length)/1024/1024) as iMB FROM information_schema.tables WHERE table_schema=database() ORDER BY (data_length+index_length) DESC;

アクセスログの解析

proxyでアクセスログを解析することにより、URLごとのリクエスト数やレスポンスタイムを集計できます。

解析には、tkuchikiさんのalpが便利です。 自分の場合は、適当な自作スクリプトを使っていました。https://gist.github.com/yuuki/129983ab4b02e3a646ad

isucon@isucon01:~$ sudo parse_axslog isucon5.access_log.tsv taken_sec
req:GET / HTTP/1.1 taken_sum:474.08 req_count:714 avg_taken:0.66
req:GET /footprints HTTP/1.1 taken_sum:58.378 req_count:198 avg_taken:0.29
req:GET /friends HTTP/1.1 taken_sum:27.047 req_count:238 avg_taken:0.11
req:POST /diary/entry HTTP/1.1 taken_sum:6.51 req_count:195 avg_taken:0.03
…

nginxでLTSVによるアクセスログをだすには、以下のような設定を去年は使いました。

log_format tsv_isucon5  "time:$time_local"
 "\thost:$remote_addr"
 "\tvhost:$host"
 "\tforwardedfor:$http_x_forwarded_for"
 "\treq:$request"
 "\tstatus:$status"
 "\tsize:$body_bytes_sent"
 "\treferer:$http_referer"
 "\tua:$http_user_agent"
 "\ttaken_sec:$request_time"
 "\tcache:$upstream_http_x_cache"
 "\truntime:$upstream_http_x_runtime"
 "\tupstream:$upstream_addr"
 "\tupstream_status:$upstream_status"
 "\trequest_length:$request_length"
 "\tbytes_sent:$bytes_sent"
 ;
access_log /var/log/nginx/isucon5.access_log.tsv tsv_isucon5;

MySQLスロークエリログの解析

MySQLのスロークエリログは、実行時間が閾値以上のクエリのログを吐いてくれるというものです。 最初は、long_query_time = 0 にして、全クエリのログをとります。もちろん、ログ採取のためのオーバヘッドはありますが、最終的にオフにすればよいです。

slow_query_log                = 1
slow_query_log_file           = /var/lib/mysql/mysqld-slow.log
long_query_time               = 0
log-queries-not-using-indexes = 1

pt-query-digestで集計するのが便利です。 https://gist.github.com/yuuki/aef3b7c91f23d1f02aaa266ebe858383

計測環境の整備

ログの解析については、前回のベンチマークによるログを対象に含めたくないため、ベンチマーク前に過去のログを退避させるようにします。

設定ファイルを変更したまま、プロセスの再起動を忘れることがあるので、ベンチマーク前に各プロセスを念の為に再起動させます。

チューニング

MySQL、nginx、redis、memcachedについては、特殊なお題でないかぎり、kazeburoさんのエントリの通りでよいと思います。 比較的スコアに効きやすいのは、静的ファイルのproxy配信、UNIXドメインソケット化あたりかなと思っています。

ただし、sysctl.confの設定内容反映については、Systemd バージョン 207と21xで注意が必要です。 具体的には、/etc/sysctl.conf が読まれず、/etc/sysctl.d/*.conf または /usr/lib/sysctl.d/*.conf が読まれるようです。 sysctl - ArchWiki

アプリケーションの把握

オペレーションばかりやってると意外と、何のアプリケーションか知らずに作業しているということがあります。 特に序盤は定例作業が多いので、一息つくまでにそれなりに時間をとられます。 その間に、アプリケーション担当が把握していて、把握していることが前提になるので、会話についていけないこともあります。

そこで、以下のようなことを最初に全員でやって認識を合わせるといった工夫をしたほうがよいです。

  • ウェブサービスとしてブラウザからひと通りの導線を踏んでみる
  • テーブルスキーマをみる
  • MySQLのコンソールに入ってデータの中身をみてみる
  • コードを読む

競技終了前のチェック

競技終了前になにか見落としがないかチェックします。

  • ディスクサイズに余裕があるか。運営サイドでベンチマークを回す時にログ出力でディスクがうまることがないとも限らない。ログを吐きまくってると意外とディスクに余裕がなくなっていたりするため、最後に確認する。
  • ページの表示は正常か。CSSがなぜかあたってないとかないか。
  • ログの出力を切る。アクセスログやスロークエリログ。
  • (厳密には競技終了30分〜60分の間ぐらい) OSごと再起動してもベンチマークが通るかどうか

参考文献

kazeburoさんの資料が非常に参考になります。何度も読みました。

昨年の予選のコードと設定ファイルは公開しています。https://github.com/yuuki/isucon5-qualifier 昨年の様子です。ISUCON 5予選で5位通過した話 - ゆううきブログ

発表資料

この記事は、京都.なんか#2 で発表した内容を元にしています。 準備・運営ありがとうございました! > id:hakobe932 / id:hitode909

どの発表もおもしろかったけど、個人的にはkizkohさんのRust&netmapの話がよくて、netmapは昔論文読んだりしたものの、全然使っていなかったので、実験的とはいえ実際にコード書いて動かしているのがとてもよいなと思いました。 というかこんなところで、netmapとかdpdkの話がでてくるのかとびっくりしました。

あとがき

今年もはてなから何チームか出場するとのことだったので、知見共有のために、これまでのISUCON出場経験をまとめてみました。

ISUCON当日に調べながらやっていては間に合わないので、今のうちに練習しておくことをおすすめします。 さらに、全体を把握するために、過去の問題を一人で解いてみることもおすすめします。

おもしろいのは、多少ISUCON特有のノウハウは混じっているものも、実際の現場でやっているようなことがほとんどだということです。 昨今では、ウェブアプリケーションが複雑化した結果、高速化の余地があったとしても、大きすぎる問題を相手にすることが多いため、ISUCONは手頃な環境で経験を積むにはもってこいの題材です。

さらに本戦出場して惨敗すると、人権をロストしたり、ヒカリエの塔からハロウィンでめちゃくちゃになった街に突き落とされるような体験が待っています。

Linuxサーバにログインしたらいつもやっているオペレーション

主にアプリケーション開発者向けに、Linuxサーバ上の問題を調査するために、ウェブオペレーションエンジニアとして日常的にやっていることを紹介します。 とりあえず調べたことを羅列しているのではなく、本当に自分が現場で使っているものだけに情報を絞っています。 普段使っているけれども、アプリケーション開発者向きではないものはあえて省いています。

MySQLやNginxなど、個別のミドルウェアに限定したノウハウについては書いていません。

ログインしたらまず確認すること

他にログインしている人がいるか確認(w)

wコマンドにより現在サーバにログインしてるユーザ情報をみれます。

$ w
 11:49:57 up 72 days, 14:22,  1 user,  load average: 0.00, 0.01, 0.05
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
y_uuki   pts/0    x.x.x.x 11:49    1.00s  0.02s  0.00s w

再起動などシステム影響がありうるオペレーションをする場合、同僚が他の作業をしていないかどうかをチェックします。

サーバの稼働時間の確認 (uptime)

サーバが前回再起動してから現在までの稼働している時間をみれます。下記の例の場合、72日間稼働しています。

$ uptime
 11:55:39 up 72 days, 14:28,  1 user,  load average: 0.00, 0.02, 0.05

DOWNアラートがきたときに、このコマンドをよく打ちます。DOWNアラートが来ていても、実際にOSが再起動していないということはよくあります。 これは監視システムと対象ホスト間のネットワーク不調が原因で、DOWNアラートが来ることがあるためです。 AWS EC2のようなVM環境の場合、ハイパーバイザ側のネットワークスタック不調などにより、監視システムと疎通がなくなるということがありえます。

uptimeコマンドにより、前回のシャットダウンから現在までの経過時間を知れます。上記の例の場合、72日間ですね。 OSが再起動している場合、MySQLサーバのようなデータベースのデータが壊れている可能性があるので、チェックする必要があります。

プロセスツリーをみる (ps)

サーバにログインしたときに必ずと言っていいほどプロセスツリーを確認します。 そのサーバで何が動いているのかをひと目で把握できます。

ps auxf

ps コマンドのオプションに何を選ぶかはいくつか流派があるようです。自分はずっと auxf を使っています。 aux でみてる人もみたりしますが、 f をつけるとプロセスの親子関係を把握しやすいのでおすすめです。 親子関係がわからないと、実際には同じプロセスグループなのに、同じプロセスが多重起動していておかしいと勘違いしやすいです。

...
root     25805  0.0  0.1  64264  4072 ?        Ss    2015   0:00 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
nginx    25806  2.6  0.7  70196 15224 ?        S     2015 2691:21  \_ nginx: worker process
nginx    25807  2.7  0.8  71700 16416 ?        S     2015 2725:39  \_ nginx: worker process
nginx    25808  2.7  0.7  70672 15336 ?        S     2015 2725:30  \_ nginx: worker process
nginx    25809  2.7  0.7  71236 15976 ?        S     2015 2709:11  \_ nginx: worker process
nginx    25810  2.6  0.9  74084 18888 ?        S     2015 2646:32  \_ nginx: worker process
nginx    25811  2.6  0.6  69296 14040 ?        S     2015 2672:49  \_ nginx: worker process
nginx    25812  2.6  0.8  72932 17564 ?        S     2015 2682:30  \_ nginx: worker process
nginx    25813  2.6  0.7  70752 15468 ?        S     2015 2677:45  \_ nginx: worker process
...

pstree でも可。ただし、プロセスごとのCPU利用率やメモリ使用量をみることも多いので、ps をつかっています。

NICやIPアドレスの確認 (ip)

ifconfig コマンドでもよいですが、ifconfig は今では非推奨なので、 ip コマンドを使っています。 ipコマンドはタイプ数が少なくて楽ですね。

$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 00:16:3e:7d:0d:f9 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.10/22 brd z.z.z.z scope global eth0
       valid_lft forever preferred_lft forever
    inet 10.0.0.20/22 scope global secondary eth0
       valid_lft forever preferred_lft forever

上記の出力例ではよくみているとセカンダリIPがついています。セカンダリIPがついているということは、なんらかの特殊な役割をもつホストである可能性が高いですね。 特にVIPを用いた冗長構成をとっている場合、VIPがついているならばマスタであると判断できます。 社内では、eth0に対するセカンダリIPや、eth1にプライマリIPをVIPとして利用することが多いです。

このような事情もあり、サーバにログインするとIPアドレスのチェックを癖でやってしまっています。

ファイルシステムの確認(df)

ファイルシステムの容量確認は df コマンドを使います。-T をつけると、ファイルシステムの種別を確認できます。-h をつけると、サイズ表記がヒューマンリーダブルになります。

$ df -Th
Filesystem                                             Type      Size  Used Avail Use% Mounted on
rootfs                                                 rootfs     20G  2.4G   17G  13% /
udev                                                   devtmpfs   10M     0   10M   0% /dev
tmpfs                                                  tmpfs     3.0G  176K  3.0G   1% /run
/dev/disk/by-uuid/2dbe52e8-a50b-45d9-a2ee-2c240ab21adb ext4       20G  2.4G   17G  13% /
tmpfs                                                  tmpfs     5.0M     0  5.0M   0% /run/lock
tmpfs                                                  tmpfs     6.0G     0  6.0G   0% /run/shm
/dev/xvdf                                              ext4     100G   31G    69G   4% /data/redis

dfコマンドにより、ディスク容量以外にも、いくつかわかることがあります。

例えば、MySQLのようなデータをもつストレージサーバは、本体とは別の専用のディスクデバイスをマウントして使う(EC2のEBSボリュームやFusion-IOの ioDriveなど)ことがあります。

上記の出力例の場合、/data/redis に着目します。前述の「プロセスツリーをみる」により、Redisが動いていることもわかるので、RedisのRDBやAOFのファイルが配置されていると想像できます。

負荷状況確認

次は、Linuxサーバの負荷を確認する方法です。そもそも負荷とは何かということについて詳しく知りたい場合は、「サーバ/インフラを支える技術」の第4章を読むとよいです。

Mackerelのような負荷状況を時系列で可視化してくれるツールを導入していても、以下に紹介するコマンドが役に立つことは多いと思います。 秒単位での負荷の変化やCPUコアごと、プロセスごとの負荷状況など、可視化ツールで取得していない詳細な情報がほしいことがあるからです。

Mackerelである程度あたりをつけて、サーバにログインしてみて様子をみるというフローが一番多いです。

top

top -c をよく打ってます。 -c をつけると、プロセスリスト欄に表示されるプロセス名が引数の情報も入ります。(psコマンドでみれるのでそっちでも十分ですが)

さらに重要なのが、top 画面に遷移してから、 キーの 1 をタイプすることです。1 をタイプすると、各CPUコアの利用率を個別にみることができます。学生時代のころから当たり前に使ってたので、たまにご存知ない人をみつけて、意外に思ったことがあります。(mpstat -P ALL でもみれますが)

かくいう自分も これを書きながら、top の man をみてると知らない機能が結構あることに気づきました。

top - 16:00:24 up 22:11,  1 user,  load average: 1.58, 1.43, 1.38
Tasks: 131 total,   2 running, 129 sleeping,   0 stopped,   0 zombie
%Cpu0  : 39.7 us,  4.1 sy,  0.0 ni, 48.6 id,  0.3 wa,  0.0 hi,  6.9 si,  0.3 st
%Cpu1  : 24.4 us,  1.7 sy,  0.0 ni, 73.9 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  : 14.7 us,  1.7 sy,  0.0 ni, 83.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu3  :  0.0 us,  2.0 sy, 98.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu4  :  3.3 us,  0.7 sy,  0.0 ni, 96.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu5  :  2.0 us,  0.3 sy,  0.0 ni, 97.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu6  :  2.3 us,  0.3 sy,  0.0 ni, 97.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu7  :  0.7 us,  0.3 sy,  0.0 ni, 99.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem:   7199160 total,  5764884 used,  1434276 free,   172496 buffers
KiB Swap:        0 total,        0 used,        0 free,  5161520 cached

  PID USER      PR  NI  VIRT  RES  SHR S  %CPU %MEM    TIME+  COMMAND
16659 root      39  19  8612  632  440 R 100.3  0.0   0:12.94 /bin/gzip
 2343 nginx     20   0 60976  14m 1952 S  23.2  0.2 112:48.51 nginx: worker process
 2328 nginx     20   0 61940  15m 1952 S  19.9  0.2 111:49.12 nginx: worker process
 2322 nginx     20   0 61888  15m 1952 S  19.3  0.2 113:44.95 nginx: worker process
 2324 nginx     20   0 61384  14m 1952 S  16.6  0.2 113:30.52 nginx: worker process
 2340 nginx     20   0 61528  14m 1952 S  11.0  0.2 114:02.36 nginx: worker process
 ...

上記の出力例は結構おもしろいタイミングを示していて、下のプロセスリストをみると、 /bin/gzip がCPU 100%使いきっています。 これはlogrotateがアクセスログを圧縮している様子を表しています。 上段のCPU利用率欄をみると、Cpu3 が 0.0 idとなっています。 id は idle の略であり、 id が 0% ということはCpu3を使いきっているということです。これらの状況から gzip プロセスが Cpu3 を使いきっているということが推測できます。

CPU利用率欄には他にも、us, sy、ni、wa、hi、si、stがあります。(カーネルバージョンにより多少異なります) これらは、CPU利用率の内訳を示しています。よくみるのは、us、sy、waの3つですね。

  • us(user): OSのユーザランドにおいて消費されたCPU利用の割合。userが高いということは、アプリケーション(上記の場合nginx)の通常の処理にCPU処理時間を要していることです。
  • sy(system): OSのカーネルランドにおいて消費されたCPU利用の割合。systemが高い場合は、OSのリソース(ファイルディスクリプタやポートなど)を使いきっている可能性があります。カーネルのパラメータチューニングにより、負荷を下げることができるかもしれません。fork 回数が多いなど、負荷の高いシステムコールをアプリケーションが高頻度で発行している可能性があります。straceでより詳細に調査できます。
  • wa(iowait): ディスクI/Oに消費されたCPU利用の割合。iowaitが高い場合は、次の iostat でディスクI/O状況をみましょう。

基本は各CPUコアのidleをざっと眺めて、idle が 0 に近いコアがないかを確認し、次に iowait をみてディスクI/Oが支配的でないかを確認し、user や system をみます。

ちなみに、自分は si (softirq) についてこだわりが強くて、 Linuxでロードバランサやキャッシュサーバをマルチコアスケールさせるためのカーネルチューニング - ゆううきブログ のような記事を以前書いています。

iostat

ディスクI/O状況を確認できます。-d でインターバルを指定できます。だいたい5秒にしています。ファイルシステムのバッファフラッシュによるバーストがあり、ゆらぎが大きいので、小さくしすぎないことが重要かもしれません。 -x で表示する情報を拡張できます。

$ iostat -dx 5
Linux 3.10.23 (blogdb17.host.h)     02/18/16    _x86_64_    (16 CPU)

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
xvda              0.00     3.09    0.01    3.25     0.13    36.27    22.37     0.00    1.45    1.59    1.45   0.52   0.17
xvdb              0.00     0.00    0.00    0.00     0.00     0.00     7.99     0.00    0.07    0.07    0.00   0.07   0.00
xvdf              0.01     7.34   49.36   33.05   841.71   676.03    36.83     0.09    8.08    2.68   16.13   0.58   4.80

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
xvda              0.00     3.20    0.40    2.80     2.40    30.40    20.50     0.00    0.25    0.00    0.29   0.25   0.08
xvdb              0.00     0.00    0.00    0.00     0.00     0.00     0.00     0.00    0.00    0.00    0.00   0.00   0.00
xvdf              0.00     5.00  519.80    4.00  8316.80    96.00    32.12     0.32    0.61    0.60    1.40   0.52  27.36

iostat コマンドは癖があり、1度目の出力はディスクデバイスが有効になってから現在まのの累積値になります。 現在の状況を知る場合は、2度目以降の出力をみます。

自分の場合、IOPS (r/s、w/s)と%utilに着目することが多いです。 上記の場合、r/s が519と高めで、%util が 27%なので、そこそこディスクの読み出し負荷が高いことがわかります。

netstat / ss

netstat はネットワークに関するさまざまな情報をみれます。 TCPの通信状況をみるのによく使っています。

-t でTCPの接続情報を表示し、 -n で名前解決せずIPアドレスで表示します。-n がないと連続して名前解決が走る可能性があり、接続が大量な状況だとつまって表示が遅いということがありえます。(-n なしでも問題ないことも多いので難しい)

-l でLISTENしているポートの一覧をみれます。下記の場合、LISTENしているのは 2812, 5666, 3306, 53549, 111, 49394, 22, 25 ですね。

$ netstat -tnl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 0.0.0.0:2812            0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:5666            0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:3306            0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:53549           0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:49394           0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:25              0.0.0.0:*               LISTEN
tcp6       0      0 :::54282                :::*                    LISTEN
tcp6       0      0 :::111                  :::*                    LISTEN
tcp6       0      0 :::22                   :::*                    LISTEN
tcp6       0      0 :::53302                :::*                    LISTEN
tcp6       0      0 :::25                   :::*                    LISTEN

TCPの全部のステートをみるには -a を指定します。 -o はTCP接続のタイマー情報、 -pはプロセス名の表示 (-p には root権限が必要) ができます。

$ sudo netstat -tanop
...
tcp        0      0 10.0.0.10:3306         10.0.0.11:54321        ESTABLISHED 38830/mysqld keepalive (7190.69/0/0)
tcp        0      0 10.0.0.10:3306         10.0.0.12:39150       ESTABLISHED 38830/mysqld keepalive (7157.92/0/0)
tcp        0      0 10.0.0.10:3306         10.0.0.13:49036         TIME_WAIT  38830/mysqld timewait (46.03/0/0)
tcp        0      0 10.0.0.10:3306         10.0.0.14:41064         ESTABLISHED 38830/mysqld keepalive (7223.46/0/0)
tcp        0      0 10.0.0.10:3306         10.0.0.15:34839        ESTABLISHED 38830/mysqld keepalive (7157.92/0/0)
...

一番、よくみるのは、このホストはどのホストから接続されているかです。 なぜか本番サーバなのに、ステージングサーバから接続されているというようなことがわかったりすることもあります。

他にも、単にやたらと接続が多いなとかざっくりした見方もします。そのときに、TCPのステートでESTABLISHED以外がやたらと多くないかなどをみたりします。

netstat は非推奨なので、ss コマンドをつかったほうがよいようです。 ただし、自分の場合、ss コマンドの出力の余白の取り方があまり好きではないので、netstat をつかっています。

ログ調査

いうまでもなくログ調査は重要です。 ログをみるためには、OSや、各種ミドルウェア、アプリケーションが吐くログがどこに履かれているのかを知る必要があります。

基本的には /var/log 以下を眺めてそれっぽいものをみつけて tail してみます。

/var/log/messages or /var/log/syslog

まずはここを見ましょう。カーネルやOSの標準的なプロセスのログをみることができます。他にもcron実行するコマンドの標準出力や標準エラー出力を明示的に logger にパイプしている場合などはここにログが流れます。

/var/log/secure

ssh 接続の情報がみれます。他の人がssh接続しているのに接続できない場合、ここに吐かれているログをみると原因がわかることがあります。

/var/log/cron

cronが実行されたかどうかがわかります。ただし、cronが実行したコマンドの標準出力または標準エラー出力が /var/log/cron に出力されるわけではなく、あくまでcronのスケジューラが動いたかどうかがわかるだけです。cronが実行したコマンドの標準出力または標準エラー出力はどこに出力されるか決まっているわけではなく、crontab内でloggerコマンドにパイプしたり、任意のログファイルにリダイレクトしたりすることになります。

/var/log/nginx, /var/log/httpd, /var/log/mysql

ミドルウェアのログは /var/log/{ミドルウェア名} 以下にあることが多いです。 特によくみるのはリバースプロキシのアクセスログやDBのスロークエリログですね。

自分が開発しているシステムのログの位置は確認しておいたほうがよいです。

/etc

/var/log 以下にログを吐くというのは強制力があるものではないので、ログがどこにあるのか全くわからんということがあります。

ログファイルのパスは設定ファイルに書かれていることもあります。 設定ファイルは /etc 以下にあることが多いので、 /etc/{ミドルウェア名} あたりをみて、設定ファイルの中身を cat してログファイルのファイルパスがないかみてみましょう。

lsof

/etcをみてもわからんというときは最終手段で、lsofを使います。 ps や top でログをみたいプロセスのプロセスIDを調べて、lsof -p <pid> を打ちます。 そのプロセスが開いたファイルディスクリプタ情報がみえるので、ログを書き込むためにファイルを開いていれば、出力からログのファイルパスがわかります。

他には例えば、daemontools を使っていると、 /service 、もしくは /etc/service 以下に multilog が吐かれているなど、使用しているスーパーバイザによっては、特殊なディレクトリを使っている可能性があります。

関連

あとがき

ここに紹介した内容はあくまでy_uukiが普段やっていることであり、どんな状況でも通用するようなベストプラクティスではありません。 むしろ、自分はこうやってるとか、こっちのほうが便利だということがあれば教えてほしいです。

オペレーションエンジニアは息を吸うように当たり前にやっているのに、意外とアプリケーションエンジニアはそれを知らない、ということはよくあることだと思います。オペレーションエンジニア同士でも、基本的過ぎてノウハウを共有していなかったということもあるのではでしょうか。

知ってる人は今さら話題にするのもなとかみんな知ってるでしょとか思いがちです。しかし、これだけ毎日のように新しい話題がでてくると、基本ではあっても、意外と見落としているノウハウは結構あると思います。見落としていた話題にキャッチアップできるように、新卒研修の時期にあわせて定期的に話題になるとよいですね。

昨今は、No SSHと呼ばれるように、サーバにSSHしないという前提で、インフラを構築する発想があります。 少なくとも現段階では、No SSHは一切サーバにSSH(ログイン)しないという意味ではなく、あくまでサーバ投入や退役など、通常のオペレーションをするときにSSHしないという意味だと捉えています。 もしくは、Herokuのユーザになり、サーバ側の問題はなるべくPaaS事業者に任せるということかもしれません。

したがって、サーバにログインして問題調査するという形はまだしばらくは続くでしょう。

異常を発見した場合、より深く問題に踏み込むために、straceやgdbなどを駆使するという先のオペレーションの話もあります。そのうち書きたいと思います。

はてなで大規模サービスのインフラを学んだ

中〜大規模サービスのインフラの様子を知りたいアプリケーションエンジニア向けに、もともとアプリケーションコードを書いていた視点から、個人的な体験をベースにはてなで大規模サービスのインフラを学んだ過程や学んだ内容の一部を紹介します。

続きを読む

Mackerelを支える時系列データベース技術

【追記 2018/01/06】現在Mackerelは、時系列データベースという概念をクラウドの技で再構築する - ゆううきブログの時系列データベース実装へ移行しています。

サーバモニタリングサービス Mackerel で採用している時系列データベース Graphite を用いたシステムの構築と運用事情を紹介します。Graphiteについては、プロビジョニングやアプリケーションからの使い方、Graphite自体のモニタリングなど様々なトピックがありますが、特に大規模ならではのトピックとして、Graphiteの内部アーキテクチャ、パフォーマンスチューニングおよびクラスタ構成についての知見を書きます。

続きを読む

Linuxでロードバランサやキャッシュサーバをマルチコアスケールさせるためのカーネルチューニング

本記事の公開後の2016年7月にはてなにおけるチューニング事例を紹介した。 はてなにおけるLinuxネットワークスタックパフォーマンス改善 / Linux network performance improvement at hatena - Speaker Deck

HAProxy や nginx などのソフトウェアロードバランサやリバースプロキシ、memcached などの KVS のような高パケットレートになりやすいネットワークアプリケーションにおいて、単一の CPU コアに負荷が偏り、マルチコアスケールしないことがあります。 今回は、このようなネットワークアプリケーションにおいて CPU 負荷がマルチコアスケールしない理由と、マルチコアスケールさせるための Linux カーネルのネットワークスタックのチューニング手法として RFS (Receive Flow Steering) を紹介します。

Redis や Nodejs のような1プロセス1スレッドで動作するアプリケーションをマルチコアスケールさせるような話ではありませんのでご注意ください。

続きを読む