個人開発している Web アプリが、そろそろ Closed Alpha まで来そうだ。
結構うれしい。
今回はかなり本気で、ダークファクトリー的な開発を目指している。
僕が「こんな機能がほしい」と指示する。AI がコードを書く。テストを書く。動かす。問題があれば直す。僕は出来上がったものを確認して、次の指示を出す。
これを繰り返している。
もちろん完全無人というわけではないけれど、少なくとも僕自身がコードを書くことはかなり減った。
そして、マジでアプリケーションが出来上がっていく。
すごい。
前にもダークファクトリーについて書いたけれど、実際に自分でやってみると未来感がすごい。
「これ、ソフトウェア開発のやり方、本当に変わるんじゃないか?」
などと思いながら、僕は出来上がりつつあるアプリケーションを眺めていた。
Closed Alpha も見えてきた。
いいぞ。
ワイもダークファクトリーや!
所で、最近トップページがもっさりしている。
計ってみる。
3000ms オーバー。
……
…………
ウッソだろ!? まだ、ユーザ数1人だぞ!?
このアプリは Go と Neon DB、それから Go に埋め込んだ HTML で作っている。フロントエンドで巨大な JavaScript を読み込んでいるわけでもない。それなのに、メインページを開くのに3秒以上かかる。
ちなみにローカルで動かすと70msを切る。
何かがおかしい。
まずログを見る。しかしそこには何もない。何もだ。
こういうときは、まずログを見る。
一応、僕もプログラマを20年くらいやっている。3秒という数字だけ眺めながらコードを片っ端から読むようなことはしない。
ログを見て、どこで時間を使っているのか調べる。
ところがどっこい、ログなんてありません。
ぼくはダークファクトリーを目指した。そんな訳て、このアプリは、かなりAIに実装を任せている。
「こんな機能がほしい!」
と指示する。
AIが実装する。テストする。動く。
では次の機能。
そんな感じで、最小限の機能をインクリメンタルに積み上げてきた。
すると、「どんなログをどこに仕込むか」を指示する機会がない。
僕が自分でコードを書いていたら、おそらく違った。
開発中にデバッグしたくなるので、「ここどうなってるんだ?」となって、とりあえずログを出す。別に立派なObservability戦略があるわけではない。自分が困るから入れる。print デバッグはいつの時代もプリミティブかつ有用である。
ところが今回は、その工程そのものをAIに渡している。
機能は増えた。
ログは増えなかった。
仕方がないので、まずリクエストログを整備した。フォーマットを決めて、所要時間、ステータス、リクエストとレスポンスのボディサイズを出す。セッションごとに絞り込んで見られるようにもした。
これでわかったのは、ブラウザとサーバ間のネットワークが遅いわけではないということだった。
サーバ側で3000msかかっている。
目も当てられない。
OpenTelemetryを入れる
次に欲しいのはメトリクスとトレースだ。もちろん指示しない限り、AIは自動的に入れてくれない。
いつも通りのOpenTelemetry を入れることにした。コーディングエージェントは「時期尚早」と言う。いや、いま困ってるんよ。
Prometheus や Zipkin みたいな構成をイメージすればいい。計装は一度入れてしまえば、あとは自動で測ってくれるのがいい。
ただし、個人開発なのでトレースを受け取るためだけの環境を真面目に作りたくはない。金もかかる。
なので、トレースはリクエストログに続けてテキストで出力することにした。
DBライブラリとサーバライブラリは自動計測。ビジネスロジックには必要なところにspanを設定した。
これでようやく、3000msの中身が見える。
1200ms以上かかっているDBクエリが一件。
200msくらいかかっているクエリが数件。
なるほど。
なるほど?
な、なるほどぉ!?
シンガポールは遠い
そもそも1クエリに100ms以上かかるのはおかしい。
ただし、心当たりはある。
Neon DBはシンガポールにある。そしてアプリケーションサーバは関東にある。
遠い。
これについてはいくつか対応方法がある。
接続をプールしてもいい。独立したDBアクセスを並列化してもいい。キャッシュしてもいい。DBそのものを関東へ持ってきてもいい。
逆にアプリケーションをシンガポールへ持っていく手もあるが、これはちょっと躊躇する。DBとの距離は縮まるけれど、今度は日本のブラウザからアプリケーションまでが遠くなる。HTTPリクエスト側を分散させることも考えると、単純にアプリをDBの隣へ持っていけばいいとも限らない。
まあ、この辺はあとでいい。
200msのお前らはあとでまとめて面倒を見る。
1200ms、お前はダメだ。
一体何をしているんだ。
そこで、ようやくクエリを見ることにする。
全件取得ちゃん
なんということでしょう……
唯一のユーザである僕のデータを600レコード全部取得して、それでページングによって10件だけレンダリングする……
豪華な使い方やでほんま……
許されるはずがない。
ローカルのDBには高々100件くらいしかレコードがない。100件なら全部取ってきても問題は顕在化しない。
本番環境には10kレコードオーダーでデータがある想定だ。
許される……はずがない……。
では、なぜこんな実装になったのか。
過去を振り返ってみる。
僕はAIにこう言った。
「こんな機能がほしい!」
以上。
そこに非機能要件なんて入っていない。
データが増えたときにどうするのか。何件までを想定するのか。全件取得を避けるのか。どの程度のレスポンスタイムを許容するのか。
そんなことは指示していない。
AIは要求された機能を実装した。
ローカルでは動いた。テストも通った。
何も間違っていない。
いや、全件取得はやめろよ、とは思うけれど。
3000msから150msへ
全件取得ちゃんを修正した。
すると3000ms以上かかっていたレスポンスは、だいたい150msくらいになった。
めでたい。
そして一匹いたということは、当然ほかにもいる可能性がある。
泣きながらコーディングエージェントに「全件取得が起こり得るパターンをリストアップしてくれ」とお願いして、一つずつ潰した。
簡単なお仕事である。
本番サービスを始める前に見つかって本当によかった。
レスポンスが3秒かかるだけなら、ユーザーが怒る。
全件取得を繰り返して転送量まで増え始めたら、僕が泣く。
下手すると破産する。
「普通こうするよね」はどこに行ったのか
今回の件は、単純に「AIがクソコードを書いた」という話ではない気がしている。
もちろん、10kレコードを毎回全部取ってくるのはやめてほしい。
ただ、人間のプログラマに同じ機能を頼んだとしても、仕様書に「全件取得は禁止です」と書いてあったわけではない。
経験のあるプログラマなら、たぶん避ける。
なんとなく嫌だからだ。
データが今後増えそうだとか、この処理は何度も呼ばれそうだとか、あとで絶対面倒になるとか。
20年くらいプログラムを書いていると、そういう「嫌な予感」が大量に蓄積される。
ログもそうだった。
僕が自分で実装していたら、「ログを実装する」というタスクを切らなくても、デバッグ中に困って勝手にログを仕込んでいたと思う。
つまり、人間がコードを書くという工程には、仕様には書かれていない非機能要件が大量に混ざっていたらしい。
ダークファクトリー的に人間を実装工程から外すと、それも一緒に消える。
では、最初から全部AIに指示すればいい。
性能要件を書こう。
ログ要件を書こう。
監視要件を書こう。
全件取得は禁止しよう。
N+1も禁止しよう。
……それで全部だろうか。
僕には、そうは思えない。
経験のあるプログラマが無意識に避けているものを、あらかじめ全部列挙できるだろうか。
少なくとも僕にはできない。
今回の3000msは150msになった。
まだ並列化もしていないし、コネクションプールもキャッシュも入れていない。Closed Alphaの結果が良ければ、NeonからFly.ioのManaged Postgresへ移すことも考えている。
本質的には、アプリケーションとDBが同じネットワークにいないことが問題だからだ。
ただ、それには金がかかる。
150msなら、とりあえず今はこれでいい。
今回の全件取得ちゃんは見つけた。
では、まだ僕が言語化していない「普通こうするよね」は、あといくつあるんだろう。
よくわからない。
ダークファクトリー、なかなか興味深い。