headlint を並列化したら、自分のサイトだけ遅くなった
headlint という CLI を作った。
URL を渡すと OGP・favicon・robots まわりを見て、足りないものを ✗ や ! で出してくれるやつ。
v0.2.0 でファイル分割と型付けをやり直して、ついでに速くした。速くするほうで一つ引っかかったので書いておく。
取得を並列にした
headlint はページを取ったあと、og:image、rel=icon の全部、manifest、/favicon.ico、robots.txt、Sitemap と、見つけたものを 1 件ずつ取りに行っていた。github.com だと 9 回。
どれも互いに関係ないので、std::thread::scope で全部同時に投げるようにした。依存は増えていない。
で、--validate を 3 回ずつ測った。単位は秒。
| サイト | 直列 | 並列 |
|---|---|---|
| github.com | 5.79 / 2.51 / 2.45 | 0.54 / 0.15 / 0.17 |
| zenn.dev | 0.24 / 0.44 / 0.20 | 0.09 / 0.13 / 0.09 |
| polidog.jp | 0.17 / 0.17 / 1.24 | 3.24 / 5.24 / 4.26 |
自分のサイトだけ遅くなった。 出力は直列版と同じなので、壊れてはいない。ただ遅い。
1 件ずつ時間を出す
fetch::get に経過時間を出す eprintln! を一時的に入れた。
DBG 42.000852ms https://polidog.jp None
DBG 14.227016ms https://polidog.jp/robots.txt None
DBG 32.451429ms https://polidog.jp/assets/favicon/icon-180.png None
DBG 60.13784ms https://polidog.jp/favicon.ico None
DBG 68.407055ms https://polidog.jp/assets/ogp/top.jpg None
DBG 68.617ms https://polidog.jp/favicon.ico None
DBG 1.093908388s https://polidog.jp/assets/ogp/top.jpg None
DBG 2.104461804s https://polidog.jp/assets/favicon/icon-32.png None
ほとんどは 50ms 前後で、いくつかだけ 1.09 秒と 2.10 秒。遅いのがきっちり 1 秒・2 秒に揃っている。
polidog.jp は Cloudflare の後ろにいるので、同時に来たから絞られているのかと思った。curl で同じ URL を 7 本同時に叩いてみたら、一番遅くても 0.26 秒。サーバーは悪くない。
ureq 2 は接続のたびに getaddrinfo を呼ぶので、DNS が詰まっているのかとも思った。8 スレッドで同時に引いても全部一瞬だった。
IPv6 だった
curl の結果を見直すと、time_connect が 0.208 秒のものが 3 本あって、ほかは 0.009 秒だった。この 0.2 秒は、curl の Happy Eyeballs(IPv6 がつながらなかったら 200ms で IPv4 も試す)の待ち時間に見える。
なので IPv6 と IPv4 に固定して、8 本同時を 3 回ずつやってみた。遅いほうから 4 本。
| 接続にかかった時間(秒) | |
|---|---|
curl -6 |
1.06 / 2.06 / 2.25 / 4.13 |
curl -4 |
0.008 / 0.009 / 0.010 / 0.010 |
IPv6 だけ、1 秒・2 秒・4 秒で待たされている。 倍々になっていくのは、TCP の SYN が落ちて再送されたときの待ち時間と同じ形。この環境だと、IPv6 で同時に接続を張ると SYN がときどき落ちる。
curl は 0.2 秒で IPv4 に逃げていた。ureq 2 には Happy Eyeballs が無くて、解決したアドレスを順番に試すだけなので、再送をそのまま待っていた。
なぜ IPv6 で SYN が落ちるのかは、まだ分かっていない。
IPv4 を先に試す
ureq 2 には名前解決を差し替える口があるので、IPv4 が先に来るように並べ替えた。IPv6 のアドレスは後ろに残しているので、IPv6 しかないホストにもつながる。
.resolver(|addr: &str| {
let mut addrs: Vec<_> = addr.to_socket_addrs()?.collect();
addrs.sort_by_key(|a| a.is_ipv6());
Ok(addrs)
})
測り直した。4 回ずつ、単位は秒。
| サイト | 直列 | 並列 + IPv4 優先 |
|---|---|---|
| polidog.jp | 1.21 / 1.19 / 0.16 / 0.16 | 0.11 / 0.10 / 0.12 / 0.12 |
| github.com | 0.61 / 0.29 / 0.26 / 0.22 | 0.14 / 0.14 / 0.13 / 0.14 |
| zenn.dev | 0.25 / 0.18 / 0.20 / 0.19 | 0.09 / 0.09 / 0.09 / 0.09 |
2〜4 倍速くなって、ばらつきも消えた。
並列にしたせいではなかった
振り返ると、遅くなった原因は並列化そのものではなく、この環境の IPv6 で SYN が落ちることと、ureq 2 に Happy Eyeballs が無いことの組み合わせだった。
直列のときも polidog.jp で 1.2 秒かかる回があったので、罠はたぶん前からあった。接続を使い回していたから踏む回数が少なかっただけで、並列にして新しい接続を一気に張るようになったら、それが表に出てきた。
Cloudflare も DNS も外れで、決め手になったのは curl の time_connect にあった 0.2 秒だった。