NTTドコモR&Dの技術ブログです。

本当にWireGuardはすごいのか?eBPFで暴く最新VPNのパフォーマンスモニタリング

はじめに

こんにちは!NTTドコモ クロステック開発部の藤枝です! この記事は、NTTドコモ R&D Advent Calendar 2025の記事です。 まずは皆様、今年一年本当にお疲れ様でした! 今年も残すところあと僅か。年末年始の休暇まであと少し乗り越えて、良い年を迎えましょう!! 私のアドベントカレンダー参戦はこれで3回目となります。 過去記事では「Linuxカーネルモジュール」を作ってみたり、「WebGPU」に触れてみたりしました。もしよろしければ、過去記事も載せておきますのでご覧ください!

nttdocomo-developers.jp nttdocomo-developers.jp

普段の業務では、都市デザイン領域における研究開発を担当しており、建設DXや3Dの分散処理技術の開発等を行っています。今回のタイトルを見て「あれ?また全然違う分野じゃないか!」と思われた方もいらっしゃるかもしれません。

ですが、「新しい技術や、自分の専門外の技術に触れていきたい!」「普段は見えない深部を覗いてみたい!」 そんなR&D社員ならではの挑戦心から、アドベントカレンダーではあえて普段の業務とは異なる領域に挑戦することにしています!

そこで、今年は何をしようかと技術トレンドを眺めていたのですが、インフラ・ネットワーク界隈でずっと気になっていた「あるもの」を深掘りすることにしました。

それは・・・・ 「WireGuard*1です!!

きっかけは、個人的な引っ越しでした。 新居のマンションで「自宅サーバに外部からアクセスしたい」と考えた際、グローバルIPが自由に使えない環境(いわゆるマンションISP)であるという壁にぶつかりました。その時に出会ったのが、Tailscale*2というVPNサービスです。

Tailscaleの公式イメージ

Tailscaleは、サーバ側とクライアント側双方からNAT越え(UDPホールパンチング)を行うことで、グローバルIPがない環境同士でもVPN接続を確立できてしまう、まさに「魔法のような」恐ろしいサービスです。おかげで、マンションの一室にある自宅サーバへ、外出先から快適にアクセスできるようになりました。

この便利さに感動すると同時に、エンジニアとしての好奇心が湧いてきました。 「Tailscaleは、一体どうやってこの爆速かつ安定した通信を実現しているんだろう?」

詳しく調べてみると、そのコア技術として採用されているのが「WireGuard」だったのです。

巷では「WireGuardは爆速VPNだ」という噂をよく耳にします。 でも、「どれくらい早いのか?」と、少し気にならないでしょうか??

そこで今回は、Linuxカーネルの挙動を透視できる技術 「eBPF」 を使って、WireGuardのパフォーマンスをモニタリングし、皆さんと一緒にその実力を丸裸にしていければと思います!

では、さっそく・・・スタートです!!

WireGuardとeBPFについて

今回の検証に入る前に、主役となる2つの技術「WireGuard」と「eBPF」について、なぜこの組み合わせが面白いのか?という観点で簡単におさらいしておきましょう。

WireGuard:シンプル・イズ・ベストがコンセプトなVPN

まず、VPN界の革命児、WireGuardです。 これまでのVPN(IPsecやOpenVPNなど)は、非常に高機能な反面、設定が複雑だったり、コードベースが膨大(数十万行レベル)だったりしました。しかし、WireGuardは「シンプルさ」を極限まで追求しています。 こちらでは、公式から訴求されている5つの観点で特徴を記載します。

WireGuardの公式イメージ

1. Simple & Easy-to-use(究極のシンプルさと使いやすさ)

これまでのVPN構築といえば、複雑なデーモンの設定や証明書管理に頭を悩ませるものでした。しかしWireGuardは、「SSHのように簡単であること」を目指しています。 複雑なハンドシェイクやデーモンの状態管理を意識する必要はありません。SSH鍵を交換する感覚で「公開鍵」を交換するだけで接続が確立します。

2. Cryptographically Sound(堅牢な暗号化技術)

「枯れた技術」ではなく、現代的で学術的に評価された最新の暗号技術(Noiseプロトコルフレームワーク、Curve25519、ChaCha20-Poly1305など)を採用しています。 また、特徴的なのが「Cipher Agility(暗号スイートの交渉)」を排除している点です。古い暗号方式をサポートしないため、ダウングレード攻撃のリスクがなく、設定ミスによる脆弱性も防ぎます。

3. Minimal Attack Surface(最小限の攻撃面)

セキュリティの強度は、最新の暗号技術に加え、コードベースの「小ささ」に支えられています。 IPsecやOpenVPNといった巨大なコードベースを持つ「怪物(Behemoths)」は監査が困難ですが、WireGuardはコード行数が約4,000行ほどしかありません。これは一人のセキュリティ専門家が数時間で監査できるサイズです。コードが短いということは、バグや脆弱性が入り込む隙間も少ないという理にかなった設計です。

4. High Performance(高いパフォーマンス)

今回の記事のメインテーマです。 WireGuardは、「IPsecよりも速く、OpenVPNよりもはるかに高性能」であることを意図して設計されています。 これを実現しているのが、Linuxカーネル内部での動作です。ユーザー空間とカーネル空間を行き来するオーバーヘッドを極限まで減らし、スマホからスーパーコンピューターまで、あらゆる環境でIPsecやOpenVPNを凌駕するパフォーマンスを発揮します。

5. Well Defined & Thoroughly Considered(熟慮された設計)

WireGuardは、継ぎ接ぎで作られたものではなく、最初から全体最適を考えて設計されています。 例えば、Cryptokey Routing(公開鍵とIPアドレスを紐付けてルーティングと認証を同時に行う仕組み)による設定の簡素化や、スマホで回線が切り替わっても接続が維持される「内蔵ローミング機能」、Dockerコンテナの通信をWireGuard経由に限定できる「ネットワーク名前空間」への対応など、現代的なユースケースに完璧にフィットするように作られています。

つまり、「余計な機能を削ぎ落とし、最新の暗号とカーネルの力を最大限に引き出す」のがWireGuardです。

eBPF:Linuxカーネルを透視することのできるツール

次に、eBPF (extended Berkeley Packet Filter)*3 についての解説です。 eBPFを一言で表すと、「Linuxカーネルの中で、安全に、動的にプログラムを実行させる技術」です。

eBPFの公式イメージ
これだけだと少しイメージしにくいかもしれませんが、Web開発に例えると非常に分かりやすくなります。
※この例え方には、異を唱えたい人もいるかもしませんが今回ばかりはお許しを。。より抽象的な目線で見るようにしてください。

  • Webブラウザ = Linuxカーネル
  • Webページ上のイベント(クリックなど) = システムコールや関数の実行
  • JavaScript = eBPFプログラム

eBPFをWeb開発に例えた場合
Webブラウザでは、特定のボタンがクリックされたときにJavaScriptを実行して、裏側でログを送ったり計算したりしますよね? あれと同じことをOSのカーネルレベルでやってしまおうというのがeBPFです。 つまり、「WireGuardがパケットを受信した(=イベント)」瞬間に、「その処理時間を計測するプログラム(=eBPF)」をサッと実行させるわけです。

eBPFがなかった時代には、「SystemTap」がありました。SystemTapは現在のeBPFの様なカーネルトレースを実現することのできるツールで、 独自のスクリプト言語でトレースロジックを記述することができ、柔軟性がとても高いものでした。 一方で、その仕組みは、書いたスクリプトをC言語のソースコードに変換し、それをGCCでコンパイルしてカーネルモジュール(.ko)を生成し、カーネルにロードして実行すると言ったものであるため以下の様な課題がありました。

  • セットアップが大変 : カーネルのデバッグ情報(debuginfo)やヘッダーファイルが必須で、バージョンが少しでも違うと動かない依存関係を永遠に治し続ける事件が頻発しました。
  • 安全性 : 独自モジュールをロードするため、バグがあるとカーネルパニック(OSクラッシュ)を引き起こすリスクがありました。
  • 起動が遅い : 実行のたびにコンパイルが走るため、スクリプト実行までに数秒〜数十秒かかりました。 こうした課題を解決したのが、eBPFで、以下の様なメリットがあります。

1. 安全性が保証されている

カーネル内で変なコードが動いてOSがクラッシュしたら大惨事です。 eBPFは実行前に「Verifier(検証器)」がコードを厳しくチェックし、危険な操作(無限ループや不正なメモリアクセス)があれば実行を拒否します。 つまり、「書いてもOSを壊さない」という安心感があります。

2. 再コンパイル・再起動が不要

カーネルそのものを書き換える必要はありません。稼働中のサーバに対して、動的にプログラムを注入(ロード)し、用が済んだらアンロードできます。

3. 爆速である

JIT (Just-In-Time) コンパイラによってネイティブなマシン語に変換されて実行されるため、オーバーヘッドが極めて少なく、WireGuardのような高速なネットワーク処理の計測にも追従できます。

今回はこの技術を使って、カーネルの深部にあるWireGuardの処理関数をフックし、パケット処理にかかっている時間をナノ秒単位レベルで計測してみたいと思います!

検証計画

前章までのアーキテクチャ解説を踏まえ、本章からは実践的な検証に入ります。 今回のテーマは、eBPFを用いた性能解析です。

既存のVPN製品との「速い・遅い」の比較ではなく、「ハードウェアの物理限界やCPUサイクル上の限界に対し、WireGuardがどこまで無駄なく動作できているか」という観点で評価を行います。

検証のアプローチ

本検証では、ブラックボックス的な外部観測(スループット計測)を用いて負荷をかけ、eBPFを用いたホワイトボックス的な内部観測を実施します。

具体的には、以下のように意図的にCPUリソースが枯渇する状況を作り出し、WireGuardが悲鳴を上げる瞬間の挙動を観測します。

  • iperf3 を用いて、WireGuard Gateway(DUT)のCPUを100%使い切る負荷をかけ続けます。
  • その極限状態において、eBPF (bpftrace) とシステム統計 (mpstat) を用いて、カーネル内部の「関数の処理時間」や「実行コンテキスト」をトレースします。

計測指標(KPI)

WireGuardの実装品質と「速さの理由」を定量化するため、以下の3つの指標を設定しました。

  • 実装の効率性:Context Switches(コンテキストスイッチ)

    • 計測内容: 1秒間あたりにシステム全体で発生したコンテキストスイッチ(CS)の回数。
    • 狙い: ユーザー空間VPN(OpenVPN等)の最大の弱点は、パケット処理のたびに発生する User/Kernel 空間の往復コストです。WireGuardはカーネルモジュールとして動作するため、このCSが「負荷時であっても爆発的に増えないこと」を証明します。
  • コードの品質:Function Latency Histogram(関数実行時間の分布)

    • 計測内容: WireGuardのパケット生成・暗号化を司る関数(wg_packet_create_data 等)の実行所要時間を計測し、ヒストグラム化します。
    • 狙い: 平均値ではなく「分散(Jitter)」に着目します。グラフが鋭い針のような形状になれば、それはキャッシュミスや分岐予測ミスが極めて少ない、高品質なコードであることの証明になります。
  • リソース配分:CPU Execution Context(CPU実行内訳)

    • 計測内容: CPU時間が「ユーザープロセス(%usr)」と「カーネル割り込み(%soft/%sys)」のどちらで消費されているかの比率。
    • 狙い: WireGuardが宣伝通り「カーネルネイティブ」であれば、ユーザー空間での処理(%usr)は ほぼ0% になるはずです。パケット転送のためにCPUリソースの全てを直結できているかを確認します。

環境構築

手っ取り早く環境を作れるため、

ではなく、、、検証の再現性を担保するため、AWS(Amazon Web Services)上に実験環境を構築します。 クラウド環境では、仮想化技術を用いてハードウェアを論理的に分割しているため、場合によっては近隣テナントの影響が懸念されますが、これを最小化するために クラスタープレイスメントグループ を活用し、物理的に近接したハードウェア上でインスタンスを起動させます。

ネットワーク構成

今回の検証では、ボトルネックをWireGuardの処理能力そのものに絞り込むため、役割を明確に分けた3ノード構成を採用します。

検証環境構成図

  • Traffic Generator (Client):

    • WireGuardクライアントとして動作し、VPNトンネル越しにServerへ向けて iperf3 で負荷をかけます。
    • 暗号化負荷に負けないよう、ハイスペックなマシンを用意します。
  • DUT (Device Under Test):

    • 今回の主役です。Clientからの暗号化パケットを受け取り、復号し、Serverへルーティングします。
    • ここで eBPF を実行し、カーネル内部を監視します。
  • Traffic Receiver (Server):

    • 通信の終端です。DUTの背後に配置され、復号された平文パケットを受信します。

ハードウェアとソフトウェア構成

本検証では、ボトルネックの所在を明確にするため、意図的にリソース差を設けた構成を採用します。 ハードウェアには、第4世代/第5世代 Intel Xeon (Sapphire Rapids / Emerald Rapids) を搭載した AWS c7i シリーズ を使用します。

共通ソフトウェアスタック

全ノードで以下のOSイメージを使用します。

  • OS: Ubuntu 24.04 LTS (Noble Numbat)
  • Kernel: Linux 6.8+ (AWS標準カーネル)
    • 選定理由: 最新のBPF JITコンパイラとWireGuardモジュールが標準で統合されており、追加のビルド手間なく高性能な検証が可能なため。

インスタンス選定

Role Instance Type vCPU Memory Bandwidth 採用理由
Client (Traffic Gen) c7i.2xlarge 8 16 GiB Up to 12.5 Gbps 負荷生成能力を確保し、送信側の詰まりを防ぐため。
DUT (WireGuard) c7i.large 2 4 GiB Up to 12.5 Gbps 暗号化コストを担保し、2vCPUを飽和させてカーネル挙動を暴くため。
Server (Receiver) c7i.2xlarge 8 16 GiB Up to 12.5 Gbps 受信処理でボトルネックを作らないため。

構築ステップ

では、上記に記してきた構成をAWS上に作ってみましょう。

プレイスメントグループの作成

若干AWSを触るところなので、画像ありでやっていきましょう。 まずは、AWSにログインし「EC2」のサービストップページに移動します。

EC2サービス

すると左側メニューに「ネットワーク&セキュリティ」というセクションに「プレイスメントグループ」というボタンがあるので押下します。

その後、出現した画面で、右上の「プレイスメントグループを作成」を押下します。

プレイスメントグループ選択画面

グループ名にはお好きな名前を入れていただき、プレイスメント戦略には「クラスター」を選択します。

その後、右下のグループを作成で完了です。

プレイスメントグループ設定内容

セキュリティグループとソース/宛先チェック

WireGuardはUDPを使用します。また、DUTはルーターとして振る舞うため、AWS特有のパケットフィルタを解除する必要があります。

  • Security Group
    • UDP/51820 (WireGuard) と TCP/5201 (iperf3) を許可。
  • ソース/宛先チェック
    • DUTインスタンスを選択し、アクションから「ソース/宛先チェックを変更」を選び、「停止」に設定します。
    • これを行わないと、AWSの仮想スイッチがルーティングパケットを破棄してしまいます。

カーネルパラメータのチューニング(全ノード共通)

Linuxをルーターおよび高速パケット処理機として動かすための必須設定です。

# /etc/sysctl.conf に追記
net.ipv4.ip_forward = 1          # ルーティングを有効化
net.core.default_qdisc = fq      # BBRなどを使うための準備
net.ipv4.tcp_congestion_control = bbr

sudo sysctl -pで適用します。

WireGuardの設定 (wg-quick)

Ubuntu 24.04にはカーネルモジュールは含まれていますが、ユーザースペースのツール(wg-quickなど)はインストールする必要があります。また、設定ファイルは手動で作成します。

インストールと鍵の生成(Client / DUT 共通) して、まずツールをインストールし、秘密鍵と公開鍵のペアを生成します。

# インストール
$ sudo apt update
$ sudo apt install -y wireguard

# 鍵ペアの生成(秘密鍵と公開鍵を作成)
$ umask 077
$ wg genkey | tee privatekey | wg pubkey > publickey

# 生成された鍵の中身を確認(後で設定ファイルにコピペします)
$ echo "Private Key: $(cat privatekey)"
$ echo "Public Key:  $(cat publickey)"

次に設定ファイルの作成 を行います。/etc/wireguard/wg0.confは存在しないため、エディタで新規作成します。

$ sudo vim /etc/wireguard/wg0.conf

【DUT (Gateway) インスタンスの設定内容】 * 下記の <DUT_PRIVATE_KEY> には、DUTで生成した privatekey の文字列を貼り付けます。 * <CLIENT_PUBLIC_KEY> には、Client側で表示させた publickey の文字列を貼り付けます。

[Interface]
Address = 10.100.0.1/24
SaveConfig = false
PrivateKey = <DUT_PRIVATE_KEY>  # ★ここを書き換える
ListenPort = 51820
MTU = 1420

# Clientへの接続情報
[Peer]
PublicKey = <CLIENT_PUBLIC_KEY> # ★ここを書き換える
AllowedIPs = 10.100.0.2/32

【Client (Generator) インスタンスの設定内容】 * 同様に、Clientの秘密鍵と、DUTの公開鍵を埋め込みます。

[Interface]
Address = 10.100.0.2/24
PrivateKey = <CLIENT_PRIVATE_KEY> # ★ここを書き換える
MTU = 1420

# DUTへの接続情報
[Peer]
PublicKey = <DUT_PUBLIC_KEY>      # ★ここを書き換える
Endpoint = 10.0.1.20:51820        # DUTのプライベートIP (AWSコンソールで確認)
AllowedIPs = 10.100.0.0/24, 10.0.1.30/32
PersistentKeepalive = 25

最終的に設定ファイルを保存したら、両者とものインスタンスで以下のコマンドで起動します。

$ sudo wg-quick up wg0

# ステータス確認
$ sudo wg show
# 出力例
interface: wg0
  public key: HOGEHOGEhogehogeHOGEHOGEhogehoge
  private key: (hidden)
  listening port: 45943

peer: HOGEHOGEhogehogeHOGEHOGEhogehoge
  endpoint: 10.20.1.x:51820
  allowed ips: 10.100.0.0/24, 10.0.1.x/32
  latest handshake: 4 seconds ago
  transfer: 92 B received, 328 B sent
  persistent keepalive: every 25 seconds

上記の出力が出ていれば準備完了です!

検証結果

環境は整いました。ここからは、ClientからServerに向けてパケットを送り込み、その中心に位置するDUT(WireGuard Gateway)の限界を観測します。

負荷試験の開始

まずは、ClientとServer同士の疎通を実現します。 今の状態だと、Serverには「送信元:10.100.0.2」としてパケットが届きますが、Serverは「10.100って誰?(ルートがない)」となり返信できません。 DUTから出ていく時に、送信元をDUT自身の物理IPに書き換える(NATする)ことで、Serverが返信できるようにします。

# インターフェース名を確認 (ens5 や eth0 など。私の場合はenp39s0でした。)
$ ip route get 8.8.8.8 | awk '{print $5; exit}'
$ sudo iptables -t nat -A POSTROUTING -o enp39s0 -j MASQUERADE

これで、あとはpingで疎通確認をしておきましょう。

$ ping <ServerのプライベートIP>

次に、検証対象であるDUTのCPUを限界まで追い込むための負荷を生成します。 【Traffic Receiver (Server) での操作】

サーバー側でパケットを受け取る準備をします。

# サーバーモードで起動
$ iperf3 -s

【Traffic Generator (Client) での操作】

VPNトンネル(10.100.0.1)の向こう側にいるServer(10.21.x.x)へ向けて、並列ストリームで負荷をかけます。 ※ServerのIPアドレスは、ServerプライベートIPを指定してください。

# 8並列で、測定時間を長め(60秒)にして実行
# -t 60: 60秒間
# -P 8:  8並列 (CPUを使い切るため)
$ iperf3 -c <ServerのプライベートIP> -t 60 -P 8


Connecting to host 10.21.139.57, port 5201
[  5] local 10.100.0.2 port 57048 connected to 10.21.139.57 port 5201
[  7] local 10.100.0.2 port 57060 connected to 10.21.139.57 port 5201
[  9] local 10.100.0.2 port 57066 connected to 10.21.139.57 port 5201
[ 11] local 10.100.0.2 port 57074 connected to 10.21.139.57 port 5201
[ 13] local 10.100.0.2 port 57090 connected to 10.21.139.57 port 5201
[ 15] local 10.100.0.2 port 57102 connected to 10.21.139.57 port 5201
[ 17] local 10.100.0.2 port 57110 connected to 10.21.139.57 port 5201
[ 19] local 10.100.0.2 port 57118 connected to 10.21.139.57 port 5201
[ ID] Interval           Transfer     Bitrate         Retr  Cwnd
[  5]   0.00-1.00   sec  58.4 MBytes   489 Mbits/sec  6340    345 KBytes
[  7]   0.00-1.00   sec  71.5 MBytes   600 Mbits/sec  7088    550 KBytes
[  9]   0.00-1.00   sec  54.2 MBytes   455 Mbits/sec  6233    341 KBytes
[ 11]   0.00-1.00   sec  65.5 MBytes   549 Mbits/sec  7003    462 KBytes
[ 13]   0.00-1.00   sec  67.8 MBytes   568 Mbits/sec  7616    462 KBytes
[ 15]   0.00-1.00   sec  68.8 MBytes   576 Mbits/sec  7053    481 KBytes
[ 17]   0.00-1.00   sec  76.9 MBytes   645 Mbits/sec  7739    529 KBytes
[ 19]   0.00-1.00   sec  83.0 MBytes   696 Mbits/sec  8696    454 KBytes
[SUM]   0.00-1.00   sec   546 MBytes  4.58 Gbits/sec  57768
- - - - - - - - - - - - - - - - - - - - - - - - -

eBPF測定①:コンテキストスイッチの発生頻度

OpenVPNなどのユーザースペースVPNが遅い最大の理由は、パケットのたびに「カーネル空間」と「ユーザー空間」を往復(コンテキストスイッチ)するからです。 カーネルモジュールで実装されたWireGuardは、理論上これが少ないはずです。それを確認してみましょう。

まずは、上記のiperf3によるパケットが飛んでいる60秒の間に、以下のeBPFのスクリプトを実行して、コンテキストスイッチの回数をカウントして表示してみてください。 ※もし、iperf3が止まっていたらやり直しです!

# 1秒ごとにシステム全体のコンテキストスイッチ回数をカウントして表示
$ sudo bpftrace -e 'tracepoint:sched:sched_switch { @CS_per_sec = count(); } interval:s:1 { print(@CS_per_sec); clear(@CS_per_sec); }'
Attaching 2 probes...
@: 23652
@: 18336
@: 30639
@: 21551
@: 21572
@: 19692
@: 21668
@: 21599
@: 22879
@: 20927
@: 22400
@: 22512
...

以下のように@の横にコンテキストスイッチの回数が出力されていれば成功です。

コンテキストスイッチの発生回数計測

これらの結果から分かるように * 全力でパケットを転送しているにもかかわらず、コンテキストスイッチは平均 2.4万回/秒 に留まっていました。もしこれがTUN/TAPデバイスを使用するユーザー空間VPN(OpenVPN等)であれば、この桁数では済まないと考えられます。(僕は疲れたのでどなたかぜひ。。。)数十万回以上のコンテキストスイッチが発生し、CPU時間の多くが空間の切替に浪費されていたと思われます。

  • WireGuardはカーネルモジュールとして動作するため、パケット処理のためのシステムコール(User/Kernel間の往復)が基本的には発生しないはずです。 ここで観測された2.4万回のコンテキストスイッチは、おそらくデータ移動のコストではなく、マルチコア分散処理のために kworker(暗号化スレッド)をCPUに割り振るための純粋なスケジューリングコストだと考えられます。

これらから、WireGuardが訴求する「アプリケーション」ではなく、「Linuxのネットワークスタックそのもの」として振る舞っていることがわかりそうですね。

eBPF測定②:関数実行時間の分布

次に、WireGuardのコアである「暗号化処理」の品質を見ます。 平均値ではなく「ヒストグラム」を見ることで、処理の安定性(Jitterのなさ)を評価します。

こちらも同様にクライアントからサーバに向けてiperf3でトラフィックを発生させます。

WireGuardのデータパケットの作成(暗号化してヘッダをつける)関数 wg_packet_create_data の実行にかかった時間を計測し、分布図を描いてみます。

# 関数に入ってから出るまでの時間を計測し、対数ヒストグラムで表示
sudo bpftrace -e 'kprobe:wg_packet_create_data { @start[tid] = nsecs; } kretprobe:wg_packet_create_data /@start[tid]/ { @ns = hist(nsecs - @start[tid]); delete(@start[tid]); }'
@ns:
[32, 64)               1 |                                                    |
[64, 128)              7 |                                                    |
[128, 256)           229 |                                                    |
[256, 512)        439654 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[512, 1K)         108139 |@@@@@@@@@@@@                                        |
[1K, 2K)           45236 |@@@@@                                               |
[2K, 4K)           39433 |@@@@                                                |
[4K, 8K)            2700 |                                                    |
[8K, 16K)            493 |                                                    |
[16K, 32K)             6 |        

※ 負荷(iperf3)をかけている状態で数秒間実行し、Ctrl+C で止めるとグラフが表示されます。

関数実行時間計測の様子

これらの計測結果からは以下のことがわかります。 * グラフは見てわかるようにピークが目に見えてわかるほど出ています。また、データの約73%が 256ns 〜 512ns という極めて狭い範囲に収束しています。これは、キャッシュミスがほとんど発生していないことが考えられます。

  • 1パケットの暗号化と生成をわずか 0.0003ミリ秒 で完了しています。WireGuardの採用しているChaCha20-Poly1305アルゴリズムをいかに効率的に処理しているかが分かります。これなら、CPUがボトルネックになって遅延することもかなり少なそうに見えます。

eBPF測定③:CPU内訳の詳細化

最後に、CPUが「何に」時間を使っているかを確認します。これは標準コマンド mpstat で確認するのが最も確実です。

# 全CPUコアの使用率を1秒ごとに表示
$ mpstat -P ALL 1
Linux 6.14.0-1015-aws (ip-10-21-138-251)        12/22/25        _x86_64_        (2 CPU)

17:40:26     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice %idle
17:40:27     all    0.00    0.00   56.78    0.00    0.00   39.70    0.00    0.00    0.00  3.52
17:40:27       0    0.00    0.00   30.69    0.00    0.00   68.32    0.00    0.00    0.00  0.99
17:40:27       1    0.00    0.00   83.67    0.00    0.00   10.20    0.00    0.00    0.00  6.12

CPU内訳の詳細化の様子
 

これらの検証結果をまとめると以下のような結果となっています。

Average:     CPU     %usr    %nice     %sys  %iowait     %irq    %soft   %steal   %idle
Average:     all     0.07     0.00    55.48     0.00     0.00    40.40     0.00    4.06
Average:       0     0.09     0.00    28.23     0.00     0.00    70.29     0.00    1.39
Average:       1     0.04     0.00    83.24     0.00     0.00     9.93     0.00    6.78
  • %usr がわずか 0.07% です。OpenVPNのようなユーザー実装VPNでは、パケットハンドリングのためにユーザープロセスがCPUを奪い合いますが、WireGuardではそれが一切ありません。CPUリソースの99.9%が「パケット転送」という本来の目的に捧げられています。

  • 2つのCPUコアが興味深い役割分担をしており、CPU 0はNICからのパケット受信割り込み(SoftIRQ/NAPI)を集中的に処理。CPU 1はWireGuardの暗号化ワーカースレッド(wg-crypt)による計算処理を担当。 カーネルスケジューラとWireGuardの実装が連携し、キャッシュ効率を落とさないようリソースを最適配分している様子が見て取れます。

  • アイドル状態がほぼゼロになるまで負荷をかけていますが、システムの応答は維持されています。これは、コンテキストスイッチのオーバーヘッドでCPUが埋め尽くされることなく、純粋に計算リソースとして使い切れていると言えます。

終わりに

本記事では、Linuxカーネルの挙動を透視できる技術「eBPF」を武器に、WireGuardのパフォーマンスをモニタリングし、皆さんと一緒にその実力を「データ」として目で見て感じてきました。

eBPFで計測したレイテンシや、ユーザー空間との往復を行わない効率的なコンテキストスイッチなどは、WireGuardがこれまでのVPNが抱えていた課題である「VPN=遅い、重い」という常識を過去のものにする実力を持っていると感じたのではないでしょうか。

そして、今回の検証を通じて見えてきたのは、ソフトウェア進化の興味深いトレンドです。 かつて、ネットワークプログラムの高速化といえば、複雑な最適化コードを積み重ねていくのが常套手段でした。しかし、QUICや今回のWireGuardが示しているのは、むしろ「コードの複雑さを極限まで削ぎ落とし、シンプルさを追求することで、逆に速くなる」という新しいアプローチです。シンプルであるがゆえに速く、カーネル実装であるがゆえに軽いWireGuardをぜひこの記事を見ながら計測し、実際に普段使いしてみてください!

WireGuardの凄さだけでなく、こうした手で実際に動かして体感してみる楽しさを感じられるかもしれないですよ! ということで、今年のアドベントカレンダーもありがとうございました!

皆さん良いお年を!!

参考文献