こんにちは。はてなブログチームの
id:Furutsuki です。
はてなブログはPerlで書かれています。内部で利用しているPerlのバージョンを、今年、Perl 5.42.0 に更新しました。その際、Perlの正規表現の内部処理の変更に伴うパフォーマンス問題に遭遇しました。この記事では、どういう問題だったのかとその対応について書きます。
Perl 5.42.0へのバージョンアップ
問題に気がついたのは、バージョンアップ対応を行い、開発環境で動作確認しているときです。動作確認として、適当なエントリを作成し投稿してみたところ、サーバからのレスポンスが返ってこないという現象に遭遇しました。
調べたところ、記事投稿時の処理の中でメモリの使用量が急増し、OOM Killerによってリクエストを処理するプロセスが強制終了されていることがわかりました。
この記事投稿時の処理というのが、はてなブログ タグへの自動リンク機能(以後自動リンク機能)というものです。自動リンクとはかつてはてなブログに存在した機能で、本文中に登場するキーワードを自動的に認識し、同様のキーワードを含む他のエントリとリンクすることができました*1。
はてなブログでは、このキーワードの認識に正規表現を利用していました。具体的には、キーワードの一覧をもとにキーワードのいずれかとマッチするような正規表現を事前に構成しておき、本文中から正規表現にマッチする文字列を列挙する、というアルゴリズムです。はてなブログがキーワードとして認識する語は30万語以上あり、それらすべてにマッチするような正規表現文字列は4MB以上ある巨大なものになっていました。
Perlのバージョンアップによってこの正規表現のコンパイル処理に時間がかかるようになり、またこのタイミングのメモリの要求量が数GBを超えたことで問題になったということがわかりました。
さらにこの問題はPerl 5.29.9の正規表現周りの処理の変更によって発生したということまではわかったのですが、うまく解決する方法は見つけられず、はてなブログのPerlバージョンアップは難航しました。結局、自動キーワードリンク機能の廃止によって問題自体が消滅したことで、Perl 5.42.0 へのアップグレードが果たされました。
はてなブログのPerlのバージョンアップを達成できたことは良かったのですが、一方、Perlがバージョンアップされて様々な性能が向上するなかで、正規表現マッチというPerlのコア機能で性能のデグレードが起こっていて、それが現代まで認識されず修正されていない状態になっていること、そしてそのことに気がついたうえで解決できていないままになってしまっているということは、エンジニアとして無念に感じていました。
問題の特定と修正について
その後しばらく時間が経ち、コーディング領域でのAIの能力が大きく高まった頃、ふとこの問題のことを思い出しました。そして、この問題に対してもAIによるアプローチが十分可能ではないかと感じ、ある週末に以下のようなプロンプトで codex を走らせました。利用したモデルは gpt-5.6-sol でした。
/goal Perl 5.29.9 で正規表現マッチングにおけるパフォーマンスが劣化していることがわかりました。hoge.pl というスクリプトを実行することでパフォーマンスを確認できます。docker-perl をうまく使い、また perl5ディレクトリ以下にperlのgitリポジトリがあるのでこれも使って、なぜパフォーマンスが劣化しているのかを説明し、修正してください
git bisectによる問題commitの特定や周辺コードの解読などの作業を含めてAIは20分ほど自走して、問題を修正して停止しました。現代のAIにとっては簡単すぎる課題だったようでした。
その後、大雑把にdiffやその原理をAIと対話して説明してもらったところ、その説明が信用できそうで、また改修にかかるdiffが小さく、自分でもハンドリング可能そうだったので、この修正をperl5リポジトリにPull Requestとして提出しました。
PRについて簡単な解説
今回の問題とこのPRについて簡単に解説します。Perlでは、正規表現をregnodeと呼ばれる内部命令列に変換してからマッチングを行います。 通常の分岐には BRANCH や JUMP という命令が使われますが、正規表現が大きくなりジャンプ距離が一定の上限を超える場合には、より長い距離を扱えるBRANCHJ や LONGJMP を使って命令列を作り直す必要があります。
Perl 5.29.9では、本当に長距離ジャンプが必要かを正確に判定するため、巨大な命令列をほぼ最後まで構築してからジャンプ距離を調べるようになりました。しかし、そこで BRANCHJ が必要だとわかると、それまでに構築した命令列を破棄して最初からコンパイルし直すことになります。今回のように巨大な正規表現を扱うケースでは、この一度目の走査に大量の時間とメモリを使うことが、パフォーマンス悪化の原因になっていました。
今回の修正では、判定の精度を多少犠牲にしつつ、命令列が一定以上の大きさになった段階で早めに BRANCHJ を使った再コンパイルへ切り替える、以前に近い挙動を復活させました。処理の都合上、すべてのケースで早期に切り替えられるわけではありませんが、今回問題になったような巨大な正規表現における時間とメモリの浪費を抑え、実用上の最悪パフォーマンスを改善する修正となっています。
その後
なんらか議論やすり合わせがあるかと思っていましたが、特にそのようなことはなくこのPull Requestはそのままマージされました。そのうちPerlの何らかのバージョンでリリースされることと思います。
はてなブログのようなプロダクトに携わっていると、おそらくこれまで誰も踏み抜いてこなかったような、Perlの細かい挙動の変化に遭遇できて面白いですね。少し前まではすぐには解決できなさそう、と思って放置していたこういった問題も、現代ではAIが30分足らずでさっと解決できるようになっているというのも、変化の早さを感じて面白いです。
この記事では、はてなブログのPerlのバージョンアップで遭遇した問題をきっかけに、perlの正規表現エンジンの挙動に踏み込んで問題を解消し、Pull Requestを提出するに至るまでの経緯を記しました。はてなブログでは今後もPerlを活用して開発をすすめていきます。
*1:現在は廃止されています 【追記あり】「はてなブログ タグ」への自動リンク機能 廃止のお知らせ - はてなブログ開発ブログ