h2ologにみるBPF toolsの構造と設計

最近はお仕事で h2olog を開発しています。これはC++で書いたBPF toolsで、H2Oに組み込まれたUSDTが出力するイベントをトレースするためのツールです。

具体的にはこんな感じで使ってH2OプロセスをトレースしてJSON-Linesにして出力します:

# QUICを有効にした H2O serverが起動しているとして
$ sudo h2olog quic -p "$(pgrep -o h2o)"
{"type":"accept","seq":1,"conn":2,"time":1594471121930,"dcid":"9546e6940ebeec5f"}
{"type":"crypto-decrypt","seq":2,"conn":2,"time":1594471121930,"pn":0,"decrypted-len":1236}
{"type":"quictrace-recv","seq":3,"conn":2,"time":1594471121930,"pn":0}
{"type":"stream-receive","seq":4,"conn":2,"time":1594471121930,"stream-id":-1,"off":0,"len":212}
...

さて、BPFのためにUSDTを定義するプログラムは、次のようにD言語でUSDTを定義します。

https://github.com/h2o/h2o/blob/master/h2o-probes.d

provider h2o {
    probe h1_accept(uint64_t conn_id, struct st_h2o_socket_t *sock, struct st_h2o_conn_t *conn);
    probe h1_close(uint64_t conn_id);
    // ...
}

これらは関数に見えますが、実際にはむしろ probe イベント名(このイベントの構造) という意味のデータスキーマです。そして struct st_h2o_socket_t * などのデータ構造は、USDTを提供するプログラム自身が定義するデータ構造です。つまり、BPF toolsはトレース対象のプログラムのデータ構造を知っていなければいけないのです。

(ところで、USDTをうまく加工するためのBPF toolsを作るときは、USDT定義ファイルからBPFとBPF toolsの一部を生成するコードジェネレータを用意することになります。ひとつひとつのUSDTに対応するPBF program用のイベントハンドラとそれを(たとえば)JSONに加工するBPF tools側のロジックを手動でメンテナンスするのは、あまりにもコストが掛かりすぎるからです。)

初期の h2olog (toru/h2olog) は、データ構造については妥協してH2Oのソースコードから必要な定義だけを抜きだしたヘッダファイルを用意していました(quic.h)。しかし、この方法はMVPを開発する際には十分でしたが、長期的な開発を見据えるといくつかデメリットがあります。つまり、同期を忘れるとリグレッションが容易に起こりうるし、もともとの構造体が複雑だと(実際複雑なのです!)、「必要な定義だけ抜き出す」というだけでも精査するコストがかかります。

BPF toolsがトレース対象のCヘッダファイルにアクセスするのは、BPF tools自体をC++で書くならば簡単です。そこでBPF toolsが使うBPF programが、トレース対象のデータ構造を参照できればこの問題は解決です。そしてsのための方法をいくつか検討しました。

まず、C structのパーサーを用意してトレース対象のヘッダファイルから情報を抜き出すという方法があります。h2ologでいえば、最初はこの方法でコードジェネレータが quic.h をパースしていました。しかし、実際にH2Oのヘッダファイルをパースするとなると、様々なtypedefやネストした構造体をパースするために、ほとんど完全なCの構造体のパーサを再発明することになります。これは現実的ではありません。

次に、H2Oのヘッダファイル全体をBPF programに埋め込む、または BPF program でH2Oのヘッダファイルをincludeする、という方法です。BPF programの文法はCなので、これは一見うまくいきそうでしたが、結局コンパイルできませんでした。BPF programは構文がCというだけで、まったく異なるコンパイルターゲットです。たとえば、BPF programからはCの標準ライブラリやPOSIX/Linuxの標準ライブラリも使えません。つまりH2Oの構造体がCの標準ライブラリにある構造体を含む場合、BPF programにincludeしてもコンパイルできないのでした。

さらに、pahole(1)でバイナリのデバッグ情報 (DWARF) からCの構造体を再構築する方法 (h2o/h2o#2365-651525032) も検討されました。paholeはDWARFを読んでCのソースコードを出力するため、うまくいけばコードジェネレータとその生成コードは一番シンプルになると思われたのですが、pahole自体は「特定の構造体とその依存関係だけをCコンパイラでコンパイル可能なソースコードとして出力する」という機能はありません。結局paholeの出力したCの構造体をパースする必要があり、今回の目的に使うのは不可能ではないにせよかなり困難でした。

最後に、必要な構造体から情報を抜き出すCプログラムを生成して情報を吐き出させる、という方法です (h2o/h2o#2365-651448880)。これはBPF programを生成するコードジェネレータのためにさらに別のコードジェネレータでCプログラムを生成する必要があるため、コードジェネレータは複雑になります。しかしこのコードジェネレータの二重構造以外はコードジェネレータをシンプルに保てるうえ、長期的にも安定した動作が見込めるため、最終的にはこの方法を採用しました。

pull-req: https://github.com/h2o/h2o/pull/2372

最終的なh2ologの設計はこんな感じになりました

  • gen-quic-bpf.py は、USDT定義ファイル(*.d)を入力にして C の BPF program と C++ の BPF tools を生成する、Python製の code generator
    • gen-quic-bpf.pygenerated-quic.cc を生成する、これが BPF tools の一部で、中に BPF program を文字列リテラルで持つ
    • generated-quic.cc はrepoにコミットする。そのほうが gen-quic-bpf.py の変更の影響を把握しやすいので
  • gen-quic-bpf.py はさらに、「BPF programの一部を生成するコードジェネレータ(C++製のcode generator)」を生成する
    • これはh2ologの実行中にコードを生成する。生成コードは h2olog(1) に -dd を与えることで見られる

BPF toolsプログラムにだけ注目すると、トレース対象プログラム(ここではh2o)と同じコンパイラでコンパイルしたBPF tools(ここではh2olog)が、トレース対象プログラムのデータ構造へのアクセス方法を実行時に構築してBPF programに渡すということです。

h2ologのソースコードはこちらです。

ここではh2ologについてのみ触れましたが、特定のプログラムに特化したBPF toolsを開発するときはいずれにせよこういう設計になると思います。

アデノウィルス感染症の後遺症で長期の視力障害を経験した

追記 (2021年12月): このエントリを書いた当初、1年経過時点では完全に回復しきったとは言い難くて、エディタのフォントサイズを16ptくらいにしていたのですが、3年たってようやく完全回復してフォントサイズを14にしました。長かった…。


アデノウィルスによる流行性角結膜炎という病気がありまして、これに保育園〜子供経由で感染した*1結果、しばらく視力障害になりました。

一年半経過した今はほとんど回復していますが、一番悪いときでメガネをしていても両目ともに視力0.1程度といった様相でした。これは角膜が濁っているので、目を凝らしたり近づけたりしてもはっきり見えたりはしません。この視力だと、ディスプレイを使った仕事はまともにできないし、スマホの文字を読むことすら困難で、日常生活にも支障がありました。この視力障害が重い状態が半年は続いて、そのあと1年くらいかけて徐々に回復したわけですが、けっこうつらかったなあという思い出です。

この視力が落ちている間はディスプレイの文字が読みにくくて、特に一部のサイトで薄く設定された文字がほぼ読めないレベルでした。今もそういうコントラストの低い文字は少し読みにくいです。視力測定で測れる範囲の視力は回復しましたが、まだ少し後遺症が残っているのでしょう。

そしてこのときに調べたのですが、こういう「コントラストの低い文字は読みにくい」という現象はアクセシビリティ界隈ではよく知られた事であるようです。たとえばコントラストについての測定方法や基準はW3CのWCAG: Web Content Accessibility Guidelinesとして標準化されています(WCAG自体はコントラストだけではなくアクセシビリティ全体を扱ってます)。この基準をテストするためのColor Contrast Checkerというツールもあります。当時いろいろ自分の眼で調べたかぎり、色のコントラストについてはこの基準を最低限満たすだけで「読めない、または非常に読みにくい」レベルのものが「ギリギリ読める」くらいになったのが確認できました。アクセシビリティを考えるときは気にすべきことがあまりにも広いので尻込みしてしまいますが、まずはMDNの色とコントラストあたりから初めるのでもいいのではないでしょうか。

ところで運が悪いことに、ちょうどこの視力障害を患っているときに運転免許の更新タイミングがきたため、視力検査をパスできずに免許を失効しました。運転免許の失効は、失効して半年以内であれば所定の手続きをすることで再取得できますが、半年以上経ってしまうとやむを得ない理由がない限りは非常に面倒な再取得の手続きが必要になります。

今回は失効してから視力が回復するまで半年以上時間が経ちましたが、一時的な視力障害は「免許の更新のできないやむを得ない理由」にあたるため、医師の診断書を添えて比較的簡単な方法での再取得となりました。とはいえ診断書をとったり免許センターで状況を説明したりなどしてめんどくさかった…。

https://www.keishicho.metro.tokyo.jp/smph/menkyo/koshin/shikko/index.html

*1:これ自体は仕方がないことだと思っています。そういうこともあります…。なおアデノウィルス自体はとてもありふれたものです。

git tips: git push $remote HEAD で現在のブランチを$remoteにpushできる

git tips: git push $remote HEAD で現在のブランチを$remoteにpushできて、コマンドラインでgitを使うときはよく使います。

ところで、この HEAD の実体はファイル(.git/HEAD)なので、ファイルシステムが大文字小文字を無視する(case-insensitive; e.g. macOSのデフォルト)ときは、 git push $remote head でも動いてしまいます。しかしこれを習慣にしていると、大文字小文字を無視しないファイルシステムで動かずハマることがあります(というかまあ、ハマってしばらく悩みました)。普段から HEAD とタイプする習慣にしておくとよいと思います。

BCCでBPF toolsの開発をするための基礎知識

最近仕事で h2olog を開発しています (H2O の QUIC 層をトレースしよう | Toru Maesaka) 。これはh2oのUSDT からデータを取り出すBPF toolです。その関係でBPFとかBCCをいろいろ触ったので、BPF toolsの開発に必要なことを書いておきます。

なおBCCまわりはまだ細かいところの作りが荒いので、思わぬトラブルがあるかもしれません。幸いpull-reqのレビュー&マージもかなり早いので、気になったところはどんどん直していっています。

ToC

開発環境のセットアップ

Linuxマシンが必要です。Ubuntu 18.04 以降だと簡単にインストールできましたが、Ubuntu 16.04だとビルドまわりでトラブルことが多いです。BPFはLinux 4.xのときに大きく開発が進んだ機能なので、カーネルだけ新しめのものにすることも必要かもしれません。

BCCのビルドはINSTALL.mdにあるとおりです。

トレース対象のプログラムのdtrace supportを有効にする

H2Oの場合は、SystemTap package (Linux用のdtrace)がインストールされていると自動でDTrace supportが有効になります。

トレース対象のプログラムのUSDTを確認する

BCC toolsに付属している tplist(8) (tplist-bpfcc という名前でインストールされる) で確認できます。

USDTは library (path to object), provider, probe という構成要素で、たとえばh2oの場合、h2oバイナリに組み込まれたUSDTは次のように確認します。

# h2o project dirで build/ でビルドした場合
$ sudo tplist-bpfcc -l build/h2o
build/h2o picotls:new_secret
...
build/h2o quicly:new_token_acked
...
build/h2o h2o:h1_close
...

h2o binaryにはpicotls, quicly, h2oというproviderのもとでそれぞれいくつかUSDTがある、ということがわかりますね。

生きているプロセスのUSDTは次のようにして確認できます。

$ sudo tplist-bpfcc -p $(pgrep -o h2o)
...
/proc/63337/root/usr/lib/x86_64-linux-gnu/libc-2.30.so libc:memory_mallopt_mxfast
/proc/63337/root/usr/lib/x86_64-linux-gnu/libpthread-2.30.so libpthread:pthread_start
...

h2oバイナリのほか、リンクされているライブラリのUSDTも確認できました。

開発する

あとは https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md を眺めながら開発するだけ!

実際のコードはこんな感じです。

https://github.com/toru/h2olog/blob/master/h2olog

BCCPython bindingを使っているので、Python code中の文字列リテラルのなかのC言語がメインロジックという、一見するとちょっとつらい感じのコードになってますが、まあC言語なのでsyntax highlightや補完がなくても意外となんとかなります。

h2ologはC++で書き直されました。C++のなかの文字列リテラルにCでBPFプログラムを書くという構造になってます。

用語集

BPF

Berkley Packet Filter。現在は eBPF (extended BPF) と統合され汎用的なトレース技術となっている。Linux kernelに組み込まれた機能。トレーサープログラム (BPF tools) が対象プロセスにアタッチして対象プロセス内で発生するイベントを取得する。

BPFはカーネルに組み込まれているので、バグ修正や新機能追加の恩恵をうけるにはカーネルのアップデートが必要。

BPF module (BPF program)

Linux kernelに組み込まれたBPF virtual machineで実行されるプログラムで、バイトコードで表現される。

BCC (iovisor/bcc)

BPF Compiler Collection。BPF moduleのフロントエンドのひとつで、C言語で書いたコードをLLVMをつかってBPF moduleにコンパイルする。C言語とBPF moduleには表現力に大きな違いがあるため、実質的にはC言語のサブセットとなっている。たとえば、初期化されていない可能性のある変数へのアクセスや一部のループはBPF verifierによって弾かれる。また、関数のスタックが非常に小さい(512バイト)。

BPF verifier

BPF moduleのverifier。kernel内で任意のコードを実行させるため安全性を確認する必要がある。

eBPF

extended BPF。オリジナルのBPFを拡張して汎用性を高めたもの。現在は "BPF" といえばオリジナルのBPFではなくeBPFのこと。 なお、eBPF以前のBPFは cBPF (classic BPF) と呼ばれることもある。

USDT

User Statically-Defined Tracing。 ユーザースペースで静的に定義したトレースポイント。Linuxには様々なトレーシング技術があり、そのなかの一つという位置付け。

USDTはD言語で構造を宣言し、それをdtrace(1)でCヘッダファイルに変換してCプログラムに手動で埋め込む。

bpftrace (iovisor/bpftrace)

BPFのフロントエンドのひとつで、DスクリプトでBPF moduleを書く。ツール・スクリプト言語ともにDTrace互換らしい…が、h2oのdtrace単体テストをみるかぎり分岐もそこそこあるため、まったく同じように使えるわけではない。

https://github.com/h2o/h2o/blob/master/t/90dtrace.t

IO Visor

Linux Foundation傘下のプロジェクト。BCCやbpftraceを開発している。

SystemTap

Linux用のdtrace(1)を提供するプロジェクト。このdtrace(1)を使って、D言語で書いたUSDT (*.d) を使うためのCヘッダファイルを生成する。このあたりの作業フローはmacOSのdtrace(1)とほぼ同じ。

https://sourceware.org/systemtap/

参考文献

Ubuntuで任意のバージョンのLinux Kernelにする

追記: ツールはこちらの getkernel がメンテされているのでこちらを使うほうがいいでしょう! ubuntu用のカーネルをとってくるやつ - w_tl00’s blog


Ubuntuなどのディストリは、ディストリに同梱されたカーネルを別のバージョンに差し替えることができます。開発者の視点では、たとえばLinuxのBPFを使ったツールを開発しているときはカーネルのバージョンを本番に合わせたほうがよかったりします。

Ubuntu用のカーネルのパッケージは https://kernel.ubuntu.com/~kernel-ppa/mainline/ のあたりにあって、適切なアーキテクチャのものをダウンロード&インストールするだけなんですが、ダウンロードするのがわりと面倒くさいのでスクリプトを書きました。

./get-kernel 4.19.106 などとするとカレントディレクトリにパッケージがダウンロードされるので、あとは sudo dpkg -i *.deb でインストールしたあと sudo update-grub && sudo reboot するだけです。

(repo: https://github.com/gfx/get-kernel )

#!/usr/bin/env perl
use strict;
use warnings FATAL => "all";
use feature 'say';

die "usage: $0 kernel-version\n" unless @ARGV;

my $kernel_version = shift(@ARGV);

my $base_url = "https://kernel.ubuntu.com/~kernel-ppa/mainline/";


# e.g.
# "4.19.100" => "4.19.100-0419100"
# "4.9-rc1" => "4.9.0-040900rc1"
sub make_version_id {
        my($v) = @_;

        my @extra;
        if ($v =~ s/-([\w-]+)\z//xms) {
                push @extra, $1;
        }

        my(@parts) = split /\./, $v;
        if (@parts == 2) {
                push @parts, "0";
        }

        return join(".", @parts) . "-" . join("", map { sprintf "%02s", $_ } @parts, @extra)
}

sub uniqstr {
        my %seen;
        return grep { !$seen{$_}++ } @_;
}

chomp(my $arch = `dpkg --print-architecture 2>/dev/null` || "amd64");

my $detail = `curl --fail -sL $base_url/v$kernel_version/`;
if(not defined $detail or $? != 0) {
        die "Invalid kernel version '$kernel_version'. Find a correct one in $base_url\n";
}

my $version_id = make_version_id($kernel_version);

my(@paths) = uniqstr(($detail =~ m{
        <a\s+href="(
                        linux-[\w-]+ # package name
                        -(?:\Q$version_id\E)
                        (?:-generic)? # build variant (none and "generic")
                        _(?:\Q$version_id\E)
                        \.\d+ # timestamp
                        _(?:all|$arch)
                        \.deb
        )"}xmsg));

foreach my $path(@paths) {
        say "Downloading $path";
        system "curl", "-LO", "$base_url/v$kernel_version/$path";
}

say "Done. Try `sudo dpkg -i *.deb && sudo update-grub && sudo reboot`";

フィルターバブル vs QoL至上主義

フィルターバブル (filter bubble) とは、「インターネットの検索サイトが提供するアルゴリズムが、各ユーザーが見たくないような情報を遮断する機能」(フィルター)のせいで、まるで「泡」(バブル)の中に包まれたように、自分が見たい情報しか見えなくなること。 Wikipedia/フィルターバブル

世界の真の姿をみるためにはフィルターバブルに陥らないようにしなきゃな!と数年前までは思ってたんですが、最近はQoL至上主義に傾いてきて、SNSなどでもミュートをどんどん活用するようになりました。

自分と主義主張が異なるという理由でミュートしたりはしません。また「この話に付き合うのは大変だけど付き合うことによって世界を少しでも良くできる」と思えれば全然いいんです。しかしそうでない場合は議論自体まったくの無駄かもしれません。

どうやっても分かり合えない人たちはいるし、そういう人たちを議論によってコントロールすることはできません。そもそも他人の意見を自分がコントロールできると考えることすらおこがましいのかもしれません。だから、自分がコントロールできることにだけ時間を使いたいと思っています。

Google公式のLinux用Chromeバイナリのバージョンを調べる

UbuntuなどにむけたChromeバイナリのインストール方法をググると以下のようなコマンドを指示されるわけですが

wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
sudo sh -c 'echo "deb https://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' 
sudo apt update

これをせずに、できればコマンドラインで配布されているChromeバイナリのバージョンを調べたかったんですが、 https://dl.google.com/linux/chrome/deb/ は404だしぐぐってもあまり情報はないしどうしたものかと思ってました。が、どうやら特定のプロトコルにしたがって Packages fileを探すとよさそう、というところであれこれ探すとありました。

curl -s https://dl.google.com/linux/chrome/deb/dists/stable/main/binary-amd64/Packages | grep Filename:

Filename: pool/main/g/google-chrome-beta/google-chrome-beta_80.0.3987.78-1_amd64.deb
Filename: pool/main/g/google-chrome-stable/google-chrome-stable_79.0.3945.130-1_amd64.deb
Filename: pool/main/g/google-chrome-unstable/google-chrome-unstable_81.0.4040.5-1_amd64.deb

参考文献:

2019年はキャリアの方向性を大きく変えた年だった

2019年のまとめです。

前半3Qはウェブアプリのバックエンドやフロントエンドを開発し、最終1Qで転職してC言語でHTTPサーバーを開発するという感じに大きく方針を転換した年でした。

転職にあたっては「毎日楽しく開発をしたい」「生涯現役のプログラマーでいたい」というのを基本的な軸として今後のことを考えていて、これから中長期的に投資するのはどういうスキルにするべきかなあと考えた結果です。

また、それはそれとして2017年から子育てをしていて、子供が保育園から風邪をもらってくると私と妻が仕事を休んで世話をし、そうこうしてる間に親(私 or 妻)に風邪がうつってしばらく仕事を休む、といった感じで思うように仕事を進められない1年でもありました(これは2018年からそういう状況)。来年はもうちょい丈夫に過ごせるといいなあと願ってやみません。


あとはざっくり今年書いたブログ記事を眺めると

gfx.hatenablog.com

このあたりが印象深いです。

完全にフロントエンドエンジニアって感じですね。

いまは打って変わってHTTP/3とかRustの勉強をしています。Rustは会社の新規プロダクトでよく採用されているので、いよいよRustを仕事で使うときが来たかとワクワクしています。

JSConf JP 2019で "How to Boost Your Code with WebAssembly" という話をしました

abstract: https://jsconf.jp/2019/talk/fuji-goro

Wasmを触り始めるにはまだ少しはやくて、おそらく2020年にはリリースされるであろうSIMDなどがほしいところです。とはいえ、JSの最適化コンパイラ(スライドではV8のTurboFanにだけ触れていますがほかのJSエンジンでも基本的には同じ傾向なはず)に頼らず安定したパフォーマンスを出せるというのは大きなメリットなので、その方面だと現在の状況でも考慮に値する可能性はあります。

ところでスライドでも触れてますが、eBayのバーコードスキャナ事例は大変興味深いです。

ここのエントリでも次のように書かれていて

This is sort of expected, as JavaScript can indeed be equally fast as native code, but only when it is in a “hot path,” i.e. heavily optimized by JIT compilers.

「JSはネイティブコード並の速度を出せるが、それはホットパスでJITコンパイラが高度な最適化をできる場合にかぎる」ということです。そしてプロダクションでは最適化コンパイラが必ずしも効果がなく結果が不安定だったと。

JSのベンチマークはしばしばこの「ホットパス」(スライドではホットスポット)だけを対象にしてしまうので、Wasmの真の力は V8 の --no-opt オプションを使ってベンチマークしてみるとよくわかるということですね。

Fastly に入社しました

2019年9月9日からFastlyに入社しています。勤務地は東京です。今後ともよろしくお願いいたします。

前職の Bit Journey, Inc. では3年ほどKibelaのサーバーサイドやフロントエンドアプリの開発に関わりました。Bit Journey在職中に子供がうまれ、現在も夫婦で分担しながら子育てをしていますが、この子育て初期という大変な時期*1にBit Journeyで気持ちよく働けたのはたいへんな僥倖でした。ここで改めて感謝いたします。

さて、Fastlyは方向性を変えて、ウェブアプリではなくVarnishやH2Oなどのミドルウェアの開発に関わります。

Kibelaは自分が数年のあいだ心血を注ぐにふさわしいサービスでしたし、実際のところ大いに開発を楽しみました。しかし、しばらく今後のキャリアの方向性を考えた結果、かねてから経験してみたいと思っていた低レイヤーなソフトウェア開発をしてみたいと決意しました。また、クラウド全盛のいまの時代だからこそ、クラウドサービスを提供する側を経験したいということもあります。

Fastlyはまさにそういうサービスであり、仕様と実装の双方をもって「インターネットを速くする」ことが価値であるCDNサービスを提供しています。興味関心のある領域で毎日楽しく開発をして、その結果としてインターネットを速くできるのであれば、ソフトウェアエンジニアとしてこれにまさる幸せはありません。この会社で過ごすこれからの時間も楽しみたいと思っています。

*1:保育園に通いはじめると頻繁に風邪をひくようになり、ひどいときには1~2週間ごとに子供が体調をくずしてそのたびに一週間ほどまともに仕事をできなくなります。