Agentic OS 技術スタックを下から読む 第23回:同じ作業場を、共有させない ―― 隔離で並行を成り立たせる
第13回では、複数エージェントの不具合は、一体の中ではなく、エージェントとエージェントの「間」に出ると見た。原因の多くは、共有する資源での衝突だった。
間の衝突は、束ねる前に消せる
第13回では、複数エージェントの不具合は、一体の中ではなく、エージェントとエージェントの「間」に出ると見た。原因の多くは、共有する資源での衝突だった。
今回は、その衝突を、編成でうまく束ねる前に、もっと手前で消す話をする。共有させない。各エージェントに、自分だけの作業場を与える。さらに、自分だけの実行資源も与える。
並行は、気合いでは成立しない。衝突しない構造があって、初めて成立する。
同じ作業場を共有すると、具体的にこう壊れる
複数のエージェントを、同じ一つの作業ディレクトリに向ける。つまり、同じファイル群を、同時に読ませ、同時に書かせる。
この時点で、すでに危ない。
一体目のエージェントが、別の作業の枝へ切り替える。すると、作業ディレクトリの中身が入れ替わる。二体目のエージェントは、まさにその瞬間、あるファイルを読んでいるかもしれない。
人間から見ると「枝を切り替えた」だけに見える。だが、二体目から見ると違う。読み始めた時点では存在した部品が、次の瞬間には消えている。途中まで読んだファイルの中身と、あとから読む別のファイルの中身が、別々の時点のものになる。依存関係の定義は新しいのに、実装ファイルは古い。あるいはその逆になる。
型を検査する道具は、そこで「部品が見つからない」と止まる。実際には部品がないのではない。読んでいる途中で、足元の作業場が別物に変わっただけである。
開発用のサーバも壊れる。変更を監視しているサーバは、あるファイルが変わったことを検知して読み直す。その読み直しの途中で、枝の切り替えが走る。すると、監視していたファイルが消える。読み込み中のファイル記述子は古い場所を指し、次にたどる依存先は新しい場所を指す。結果として、存在しない経路を読みに行く。落ちる。再起動しても、また別のエージェントが切り替えれば落ちる。
依存パッケージの管理ファイルも壊れやすい。二体のエージェントが、別々の枝で、同時に依存を追加する。一方は解析用の部品を入れ、もう一方は表示用の部品を入れる。どちらも、同じ管理ファイルを読み、そこに自分の変更を書き戻す。後から書いた方が、先に書いた方の変更を消すことがある。運が悪いと、片方の管理ファイルだけが更新され、固定版を記録する別のファイルは別時点のまま残る。
症状だけを見ると、競合状態に見える。実際、競合状態である。だが根はもっと単純だ。ファイルシステムの状態を、複数の主体が、同時に書き換えている。読んでいる側にとって、作業場が安定した一枚の面ではなく、読み込み中に動く床になっている。
一体なら、多少乱暴でも動く。二体に増やした瞬間、足元が共有されていることが不具合になる。
順番に走らせる、は解にならない
いちばん安直な逃げは、一度に一体だけ走らせることだ。作業場を共有したまま、エージェントを列に並ばせる。前の一体が終わったら、次の一体を走らせる。
これは壊れにくい。だが、並行の意味が消える。
たとえば、一つの作業が二十分かかるとする。五体のエージェントに別々の修正を任せたい。並行に走れば、理想的には二十分強で見通しが立つ。だが直列にすれば、単純に百十分近くかかる。途中で検査ややり直しが入れば、さらに伸びる。
ボトルネックが、人間のタイプ速度から、エージェントの直列実行へ移るだけである。せっかく複数体に分けたのに、入口で一列に並ばせている。
並行の価値は、待ち時間を重ねられることにある。読み、調査し、修正し、検査する時間を、複数の作業で重ねる。共有作業場を守るために全部を直列化すると、その得が蒸発する。
必要なのは、順番待ちではない。互いに邪魔できない作業場である。
履歴は一つ共有し、作業場だけを分ける
ここで効くのが、古いが強い仕組みである。
バージョン管理の仕組みには、変更の履歴をしまう倉庫を一つだけ共有しながら、実際に編集する作業場を複数に分ける機能がある。履歴の倉庫は一つ。作業ツリー、つまりファイルを広げて編集する場所だけを、作業ごとに分ける。
各作業場は、自分が今どの枝のどこにいるかを別々に持つ。どのファイルを編集しかけているかも別々に持つ。一方の作業場で枝を切り替えても、もう一方の作業場のファイルは動かない。同じ名前のファイルを直していても、別のディレクトリにある別の実体なので、読み込み中に消えることがない。
これは、単なる約束ではない。ファイルシステム上の分離である。
作業場Aは、あるディレクトリの下にある。作業場Bは、別のディレクトリの下にある。Aの枝を切り替えても、Aの中身だけが変わる。Bの中身は変わらない。Bの開発用サーバは、自分のディレクトリだけを監視している。Aで何が起きても、Bの監視対象にはならない。
ディスク使用量も、丸ごと複製より軽い。二ギガの履歴倉庫があるとして、五体のエージェントのために倉庫ごと五回複製すると、およそ十ギガを使う。さらに依存パッケージや生成物が乗る。だが、履歴を一つ共有し、作業場だけを五つ作るなら、増えるのは主に取り出したファイルと作業中の差分である。履歴そのものを五重に持たなくてよい。
重要なのは、この隔離が、待ち合わせの旗や、ファイルの錠前や、メッセージの行列なしに成立することだ。もちろん、それらが必要な場面はある。だが、同じファイル群を同時に触らせない、という問題については、ディレクトリの構造そのものが境界になる。
第11回で見たサンドボックスは、できることを制限する隔離だった。今回の作業場の分離も、同じ筋にある。手を縛るためだけではない。互いの足元を動かさないための隔離である。
ファイルの隔離は、まだ半分
ただし、作業場を分けただけでは半分しか解けていない。
エージェントは、ファイルを編集するだけではない。実行する。検査する。開発用サーバを立てる。データベースを使う。外から接続するための番号を開く。そこで、別の衝突が起きる。
まず依存パッケージである。
各作業場には、依存パッケージを別々に入れる必要がある。一か所に入れたものを、複数の作業場から近道で共有したくなる。ディスクを節約できるからだ。だが、その近道は隔離を壊す。
作業場Aは、部品の版を一つ上げたい。作業場Bは、まだ古い版のまま検査したい。依存置き場を共有していると、Aの更新がBへ漏れる。Bの検査が急に壊れる。Bから見ると、自分のファイルは変えていないのに、実行結果だけが変わる。原因を追うのが難しい。
ディスクは安い。競合状態は高い。依存パッケージが一つの作業場で一ギガ増えるとして、十個の作業場で十ギガである。痛い数字ではある。だが、共有依存が原因で一日を失う方が高い。
次に、外から接続するための番号である。
多くの開発用サーバは、何も指定しなければ、同じ既定の番号を使う。たとえば三千番である。二体のエージェントが、別々の作業場で、同時にサーバを起動する。どちらも三千番を使おうとする。先に立った方は動く。後から立った方は、番号が使われていると言って落ちる。あるいは、落ちずに別番号へ逃げるが、検査側は三千番を見に行くので失敗する。
これは、作業場を分けても起きる。番号はディレクトリの中にないからである。
だから、番号も作業場ごとに割り当てる。作業場Aは三千一番。作業場Bは三千二番。作業場Cは三千三番。検査用の補助サーバが必要なら、三千番台の前半を画面用、後半を補助用にする。たとえば、Aの画面は三千一番、Aの補助は三千五十一番と決める。作業場の識別子から番号を計算できるようにしておくと、さらによい。
データベースも同じである。
二体のエージェントが、同じ開発用データベースを使う。一方は利用者の表に列を足す。もう一方は、古い表構造を前提に検査する。検査が壊れる。もっと悪い場合、一方が検査のためにデータを消し、もう一方の検査対象まで消える。
これも、作業場ごとに分ける。データベース名に作業場の印を入れる。作業場Aなら末尾にAの番号を付ける。作業場BならBの番号を付ける。初期化も、移行も、消去も、自分のデータベースだけに向ける。
コツは、資源の名前に、隔離の境界を埋め込むことだ。
作業場が違うなら、ディレクトリが違う。番号が違う。データベース名が違う。依存置き場が違う。生成物の出力先が違う。ログの出力先も違う。
そうすれば、衝突を検出して避けるのではなく、そもそも起こりようがない形にできる。
作業場は、放っておくと溜まる
隔離された作業場は、便利である。だからこそ、放っておくと溜まる。
三週間ほど並行開発を続ける。毎日、二体から五体のエージェントを動かす。使い捨ての作業場を作る。合流したものもある。失敗して止めたものもある。途中で検査が落ちたものもある。
気づくと、使い終わった作業場が二十個ほど残っている。各作業場に依存パッケージが一ギガ、生成物が五百メガあるなら、それだけで三十ギガである。履歴の倉庫を共有していても、作業場側の実体は消えない。
一覧も散らかる。今生きている作業場がどれか分からない。合流済みなのか、検査待ちなのか、失敗して放置されたのかが分からない。人間が見る画面にも、管理する仕組みにも、余計なノイズが増える。
だから、作業場には寿命が要る。
合流が済んだ作業場は片付ける。失敗して破棄すると決めた作業場も片付ける。長く使う作業場は、定期的に最新の履歴へ追従させる。依存パッケージも入れ直す。古い枝のまま一週間放置された作業場は、今の土台とずれている可能性が高い。
いちばんやっかいなのは、孤児である。
エージェントが途中で落ちる。親の管理プロセスも落ちる。だが、作業場だけは残る。開発用サーバも残るかもしれない。番号を握ったままかもしれない。データベースも残る。
この状態を人間が毎回探すのは無理がある。見張りが必要になる。
各作業場には、生きている合図を残させる。たとえば、一分ごとに時刻を書き換える小さな印を置く。見張りは五分ごとにそれを見る。最後の合図から五分以上過ぎていて、対応する実行主体も見つからないなら、その作業場は孤児とみなす。
孤児は、すぐ完全削除しなくてもよい。まず停止する。番号を解放する。サーバを落とす。データベースを退避名に変える。作業場は、さらに一日残してから消す。調査の余地を残しつつ、実行資源だけ先に返す。
作業場は、作った瞬間から管理対象である。置きっぱなしにできる一時物ではない。
三つの隔離のしかたと、その取捨
並行のための隔離には、大きく三つのやり方がある。
一つ目は、全部を丸ごと別々に複製する方法である。履歴の倉庫も、作業ファイルも、依存パッケージも、すべて作業ごとに持つ。
これは分かりやすい。最大の隔離である。一つの複製で何が起きても、別の複製には漏れにくい。設定も単純になりやすい。
ただし、ディスクを食う。二ギガの倉庫を五体ぶん複製すれば、およそ十ギガである。依存パッケージが一体あたり一・五ギガなら、さらに七・五ギガである。生成物や検査結果を含めると、五体で二十ギガ近くになることもある。三十体に増やすと、単純計算で百ギガを超える。
二つ目は、実行ごと容器に入れて隔離する方法である。ファイルだけでなく、実行環境、環境変数、ネットワーク、場合によってはプロセスの見え方まで分ける。
これは強い。外部接続を制限したい自動検査や、危ない入力を扱う作業に向いている。各実行を箱に入れ、箱ごと捨てられる。番号の割り当ても、箱の外側で制御できる。
ただし、組み立てと監視が複雑になる。箱の元になる像を作る。依存を入れる。起動を速くする。ログを外へ出す。失敗時に中を調べる。孤児の箱を片付ける。ここまで含めると、単なる作業場分離より重い。
三つ目は、作業場で分ける方法である。履歴の倉庫は共有し、作業ツリーだけを分ける。
これは手元の並行に向く。ディスク効率がよい。履歴を一度取り込めば、全作業場が新しい変更を参照できる。枝ごとの差分も見やすい。人間が作業場に入って確認するのも簡単である。
ただし、制約もある。同じ枝を、二つの作業場に同時に出すことはできない。なぜなら、一つの枝の現在位置を、二つの作業場が別々に動かすと、枝の意味が壊れるからだ。並行にしたいなら、作業ごとに枝を分ける。あるいは、一方を読み取り専用にする。
目安はこうである。
手元で三体から十体ほどを動かすなら、作業場分離が扱いやすい。厳しい実行隔離が必要な自動検査なら、容器に入れる。ディスクが、調整の手間より安い場面なら、丸ごと複製でもよい。
多くの現場では、これらを混ぜる。履歴は共有し、作業場を分ける。その上で、危ない検査だけ容器に入れる。長時間の実験だけ丸ごと複製に逃がす。
さらに数を増やすなら、手作業では足りない。作業場を動的に割り当てる仕組みが要る。空いている番号を三千番台から探して配る。使い終わった番号を返す。データベース名を自動で作る。五分以上止まった孤児を見つける。作業場の上限を決め、古いものから掃除する。
計算が重い仕事なら、六十四コアの強い一台でも、五十体ほどが現実的な上限になることがある。各エージェントが検査や変換でコアを使うからだ。通信待ちや読み込み待ちが多い仕事なら、二百体ほどまで伸びる場合もある。だが、その場合でも、番号、データベース、作業場、ログの隔離を自動化していなければ、管理の方が先に壊れる。
数を増やすほど、隔離はぜいたく品ではなくなる。基礎設備になる。
並行は局所、統合は直列
それでも、動かせない一線がある。
並行で進められるのは、各自の作業場の中までである。最後に、全部を一つにまとめる段は、順番にやるしかない。
理由は単純だ。全体の状態は一つだからである。
作業場Aでは、設定の構造を変えた。作業場Bでは、その古い設定構造を前提に機能を足した。どちらも、自分の作業場では検査に通るかもしれない。だが、一つにまとめると矛盾する。Aの変更が先に入れば、Bの検査は壊れる。Bの変更が先に入れば、Aの移行が壊れる。
互いに矛盾する設計変更は、同時には検証できない。検証対象になる全体状態が、一つに定まらないからだ。
だから、統合は直列になる。順番に取り込み、そのたびに全体を検査する。一つ目を入れる。検査する。二つ目を入れる。検査する。壊れたら、その時点で直す。
これは負けではない。並行の範囲を正しく区切っているだけである。
並行開発は局所で行う。統合の確認は直列で行う。局所では隔離して速く進める。全体では順番に確かめる。この二つを混ぜると、局所の速さで全体も進むと誤解する。そこから事故が起きる。
隔離は、統合を不要にしない。統合までの衝突を減らす。
Agentic OS への含意
第13回で見た「間の衝突」は、編成で束ねる前に、隔離でかなり消せる。
各エージェントに、自分の作業場を与える。自分の依存置き場を与える。自分の番号を与える。自分のデータベースを与える。ログも生成物も、自分の名前の下に出させる。
これは、第11回で見たサンドボックスと同じ筋である。隔離は、安全のためだけにあるのではない。並行を成り立たせるためにも要る。
複数のエージェントを動かすとき、設計者はつい、どう束ねるかを考える。どの役割を持たせるか。どう指示を渡すか。どう結果をまとめるか。それはもちろん大事である。
だが、束ねる設計の半分は、実は、いかに共有させないかの設計である。
同じ作業場を共有させない。同じ番号を取り合わせない。同じデータベースを触らせない。同じ依存置き場を更新させない。衝突しそうなものには、最初から別名を与える。
並行とは、同じ場所へ同時に押し込むことではない。別々の場所で進め、最後に順番に合わせることである。
隔離こそが、並行を成り立たせる。次回は、土台の方へ戻り、別の細部を掘る。
← 一覧へ