NGW TECH BLOG
← 記事一覧へ
·#090

プログラムは1行も変えていないのに、周りが5箇所変わったbind mount で添付を外付けへ出す

Mac mini の内蔵SSDは増設できない。添付書類だけを外付けへ出したいが、NGW の PHP は添付の場所を相対パスで直書きしていて設定が無い。bind mount で「PHP が見る場所」を変えずに実体だけを動かした。仕組みは古典的で単純。難しかったのは、止め方・複製・バックアップの一覧・監視・2本目のディスクという、bind mount の外側だった。

仕組みそのものは、昼前に動いた。そこから先が、この記事の中身である。

Mac mini の内蔵SSDは、あとから増やせない。次期 NGW の Linux サーバは Mac mini の上の仮想マシンで動くので、添付書類が育っていけば、いつか内蔵に収まらなくなる。添付だけを外付けSSDへ出したい。 ただし、プログラムは触りたくない。

結論から書くと、mount --bind で済んだ。PHP の変更は 0 行である。

AI はこれを「古典的で単純な手法」と言った。私には魔法のような技術だった。同じディレクトリを開いているのに、中身だけが別のディスクになる。30 年 PHP を書いてきて、こういう道具があることを知らなかった。

ところが、bind mount を据えた「あと」に、周りが 5 箇所変わった。止め方、Mac 側の道具、バックアップの一覧、複製、2 本目のディスク。どれも bind mount の説明には出てこない。仕組みは単純で、難しいのはその外側だった、という話を、失敗の順に書く。


まず、どれだけ育つのかを測った

これまで「添付は年に 200GB 増える」と言われてきた。根拠を探したら、無かった。実測したほうが早い。

最大規模の団体で、添付の実体を数えた。

項目実測
添付の総量404GB
ファイル数70.6万
年の増加25〜30GB

年 200GB ではなく、年 25〜30GB。それでも 1TB の内蔵SSDに OS と DB と一緒に置き続けるのは、この団体に限っては無理がある。他の団体は桁が違って、内蔵のままで数十年もつ計算だった。外付けが要るのは、7 団体のうち 1 団体だけである。ここは後で効いてくる。


PHP は、添付の場所を相対パスで直書きしている

NGW の添付は、Webルートの下の docuKianAttcDocs/ のようなディレクトリに置かれる。全部で 10 箇所ある。PHP はそこを ../docuKianAttcDocs/… と、相対パスで直書きしている。置き場所の設定項目は無い。

しかも、レコードごとに index.php が入る作りになっている。添付ディレクトリの中に PHP がある。コードとデータに、切れ目が無いのだ。25 年前の私が書いた構造で、当時はこれが便利だった。今それを責めても仕方がない。

「設定でパスを変える」という王道が、最初から使えない。ここが出発点である。


却下した 3 つ

方式却下した理由
コードを直す画面が数十本。現行版にも同じコードがあり、二重に保守する
シンボリックリンクrsync --delete の同期先でリンクの扱いがずれる。複製とバックアップの両方が絡む
UTM の共有フォルダ9p は所有権とハードリンクを扱えない。バックアップは --link-dest のハードリンクが命綱

シンボリックリンクは、いちばん手軽に見えた。だが NGW にはそっくりな予備機を最新に保つ同期があり、そこで rsync --delete を使っている。リンクをリンクとして運ぶのか、実体として運ぶのか、同期先で解釈が割れる。バックアップの世代管理もハードリンク頼みなので、9p の共有フォルダも同じ理由で落ちる。

残ったのが bind mount だった。


bind mount にした。PHP から見て、何も変わらない

やることは古典的で、3 手である。

  1. 外付けSSD(ラベル NGW_FILES、ext4)を /mnt/ngw-files にマウントする。
  2. 添付 10 ディレクトリの中身を、/mnt/ngw-files/ の下へ移す。
  3. 移した先を、元の場所へ mount --bind で被せる。

外付けSSD(ラベル NGW_FILES、ext4)を /mnt/ngw-files にマウントし、その下の docuKianAttcDocs/ など計10ディレクトリを mount --bind で内蔵SSDの Webルート /var/www/html/ngw/ の同名ディレクトリに被せる図。右端に PHP のコードがあり、../docuKianAttcDocs/… という相対パスのまま「変更 0 行」で Webルート側を見ている

実体は外付けにあり、PHP は今までどおり Webルートの下を見る。パスは 1 文字も変わらない。

fstab には、ディスク 1 行と bind 10 行が入る。要旨はこうだ。

# /etc/fstab(抜粋・要旨)
UUID=xxxxxxxx-....  /mnt/ngw-files  ext4  defaults,nofail  0 2
/mnt/ngw-files/docuKianAttcDocs  /var/www/html/ngw/docuKianAttcDocs  none  bind,nofail  0 0
# …添付ディレクトリの数だけ、計 10 行

設置は ngw-attach-files-disk.sh という 1 本のスクリプトにした。空のディスクを 1 本選ぶ、ext4 で初期化する、写す、件数とバイト数を照合してから元を消す、bind する、Apache の安全装置を入れる。この順である。「照合してから消す」を先に決めておいたので、途中で手が震えることは無かった。

実機 2 台で流した。どちらも 16,887 件・1.0GB、照合 OK、数分で終わった。

Mac 側は、バックアップ用ディスクと同じ APFS+イメージファイル方式に揃えた。ngw-backup.img の隣に ngw-files.img が並ぶ。保守コンソールの「NGW Mac設定」に、バックアップ用か添付用かの切替を足しただけである。

ここまでが昼前。仕組みは動いた。


ここからが本題。周りが 5 箇所変わった

失敗1:nofail を真似すると、静かに二箇所へ分裂する

fstab の nofail は、外付けが無くても起動を通すための印である。バックアップ用ディスクの行にも付けてあるので、同じように書いた。

これが穴だった。

外付けが外れたまま起動すると、bind は乗らない。だが nofail なので起動は通り、Apache が上がる。Apache から見えるのは、bind の下にある内蔵の空ディレクトリである。新規アップロードはそこへ書かれる。エラーは出ない。サイトは動く。

そして外付けが戻ると、bind が乗り直し、さっき書かれたファイルは bind の下に隠れる。

3コマの図。①正常: 外付けありで bind が乗り、アップロードは外付けへ書かれる。②外付けが外れたまま起動: nofail で起動は通るが bind は乗らず、Apache が上がって内蔵の空ディレクトリへ新規ファイルが書かれる。エラーは出ずサイトは動く。③外付けが戻る: bind が乗り直し、②で書かれたファイルは bind の下に隠れて見えなくなり、添付が二箇所に分裂する。下段に対策として apache2.service に RequiresMountsFor= を足し、②で Apache が上がらないようにすると書かれている

②の間はエラーが出ない。だから気づけない。③になって初めて「あのファイルが無い」と言われる。

添付が二箇所に分裂する。しかも、いつ分裂したのか誰も知らない。

対策は、fstab の nofail を残したまま、Apache だけ上げないことにした。systemd の RequiresMountsFor= を apache2.service に足す。

# /etc/systemd/system/apache2.service.d/ngw-files.conf(要旨)
[Unit]
RequiresMountsFor=/mnt/ngw-files /var/www/html/ngw/docuKianAttcDocs …

外付けが無ければ、OS は上がるが Apache は上がらない。サイトが止まる。止まれば気づく。 分裂して動き続けるより、止まって気づくほうを選んだ。

この判断は、あとで別の場所で跳ね返ってくる(失敗4)。

失敗2:設計した方式が、Mac 側の道具に無かった

設計の段階では、添付ディスクは raw device passthrough にするつもりだった。Mac の外付けをそのまま仮想マシンに渡す方式で、Spotlight に舐められない、APFS のスパースファイルの穴に悩まされない、という利点がある。

ところが、保守コンソールの「イメージを作る」機能は、APFS+イメージファイル専用だった。バックアップ用ディスクのために作ったもので、raw を渡す口が無い。

ここで「作る」か「揃える」かを決めた。揃えるにした。既存機能に「バックアップ用/添付用」の種別を足すだけで済む。raw を選んだ理由は消えたわけではない。残ったまま受け入れて、本番投入前に様子を見ると決めた。設計文書には「raw は見送り。理由はこれ」と書いた。

道具に合わせて設計を曲げるのは気持ちが悪い。だが、道具を 1 本増やすと、構築マニュアルも移行ランブックもテストも 1 本増える。今回はそれをしないほうが安全だった。

失敗3:二つの一覧が、ずれていた

外付けへ出すディレクトリの一覧は ngw-files.conf にある。世代バックアップの対象一覧は ngw-backup.conf にある。この二つを並べたら、ずれていた。

二つの角丸の枠が重なった図。左が「外付けへ出す一覧 ngw-files.conf」、右が「世代バックアップの一覧 ngw-backup.conf」。重なりに添付10ディレクトリがあり、両方にある。左の枠だけに赤く「職員写真(5,000枚超)」があり、外付けへは出るが世代バックアップに入っていないと書かれている。右の枠だけに MySQL ダンプなどがある。下段に「設置時に無ければ足す、包含関係をテストで縛る、例外は名指しで許す」とある

職員写真は「外へ出す」側にはあって「世代を取る」側に無かった。実データが世代バックアップから漏れていた。

職員写真が 5,000 枚を超えて外付けへ出る一方で、世代バックアップの一覧に入っていなかった。つまり、実データが世代バックアップから漏れていた。bind mount の作業をしなければ、並べることも無かった。

厄介なのは、conf は現地で温存されることだ。雛形を直しても、稼働中の機体には届かない。そこで設置スクリプトに「無ければ足す」補正を入れ、二つの一覧の包含関係をテストで縛った(ATTACH_SUBDIRS ⊆ FILE_SUBDIRS、例外は名指し)。監視の緑が隠していた穴の続きのような話である。緑は出ていた。漏れていた。

失敗4:複製すると、添付が運ばれなくなる

NGW は、原本の仮想マシン(qcow2)を複製して、クローンや新しい団体の機体を作る。添付ディスクを据えた原本を複製したら、何が運ばれるか。

qcow2 には添付の実体は無い。運ばれるのは、fstab の UUID 行と bind 行、そして失敗1で足した RequiresMountsFor だけである。複製先にはその UUID のディスクが無い。マウントは満たせない。Apache が起動しない。

原本(NGWメイン)の qcow2 に fstab の UUID 行・bind 行・RequiresMountsFor が入っており、外付け NGW_FILES に添付の実体がある。複製の矢印は qcow2 だけを複製先へ運び、外付けは「運ばれない」と赤い×が付く。複製先では UUID が存在せず bind も乗らず RequiresMountsFor も満たせず、Apache が起動しない。右端に「個体化」の枠があり、ラベル NGW_FILES があれば UUID を直す、無ければ設定を外して内蔵に戻す。下段に構成の型は A(BACKUP+FILES)と B(BACKUP のみ)の2つだけ、ペアは A+A か B+B、混成はあり得ないことにすると書かれている

失敗1の「止まって気づく」が、複製先では「最初から上がらない」になる。同じ判断の表と裏。

原本を添付ディスク構成にした瞬間、全ての複製先がこれに当たる。失敗1で選んだ「安全側に倒す」が、別の場所では「そもそも立ち上がらない」として現れた。同じ判断の表と裏である。

対策は、複製直後の個体化に整合を足した。ラベル NGW_FILES のディスクがあれば fstab の UUID を直す。無ければ添付ディスクの設定を外して内蔵に戻す。個体化はもともと server-id や UUID を割り直す工程なので、そこに 1 項目増えただけである。

同時に、構成の型を 2 つに絞った。

型構成使いどころ
ABACKUP + FILES添付が内蔵に収まらない団体
BBACKUP のみそれ以外(7 団体のうち 6)

ペアは A+A か B+B。クローンはメインと同じ型。混成(A+B)は「あり得ないこと」にした。 あり得ることにすると、画面もテストも文書も倍になる。

失敗5:空ディスクが 2 本あると、VM から区別できない

A 型の機体には、バックアップ用と添付用の空ディスクが 2 本ぶら下がる。Mac 側で付けた名前は仮想マシンには届かない。VM から見えるのは /dev/sdb と /dev/sdc だけである。

設置スクリプトは「判断できない」で止まった。これは正しい慎重さで、直すべきはスクリプトではない。

対策は、レプリ設定の確認画面で空ディスクを数え、「小さい方=バックアップ用/大きい方=添付用」と提案し、人が確定して --disk /dev/sdX で渡すことにした。提案はメインを基準にする。メインが B 型なら、クローンに添付用を出さない。失敗4で決めた「クローンはメインと同じ型」が、ここで提案の根拠になる。


周辺で、同時に決めたこと

記事の主題ではないが、同じ日に決めたことを並べておく。

  • 監視: ウォッチドッグに files_disk(使用率と未マウント)を足した。fstab に添付ディスクの印がある機体だけ、自動で対象になる。メインには binlog も足し、クローンが受け取れていない時間を数える。どちらも NGWモニターに出る。
  • binlog: MINIMAL+圧縮+保持 14 日をキットの正とした。15 万トランザクションのダミーで 61MB。FULL 時代は 28〜41GB だった。
  • 単独機: メイン 1 台+BACKUP、レプリなし、という B 単独もあり得る。ただし推奨ではないと明記した。設置で disable_log_bin にする。
  • 容量の判定: 現地で添付の年別増加を mtime で集計し、見込みは平均の 1.5 倍で見る手順を、健全性レポートに足した。「年 200GB」のような伝聞を、二度と根拠にしないためである。
  • USB のリンク速度: クローン機の外付けが USB 2.0(480Mb/s)で繋がっていて、バックアップ書込中の切断が 8 日で 6 回あった。起動順ではなく物理層の問題で、ioreg の Device Speed が 2 なら疑う。前にケーブル不良で 30MB/s しか出なかったのと同じ筋である。

数字で見る

項目値
プログラムの変更0 行
添付の移送(実機 2 台)各 16,887 件・1.0GB、照合 OK、数分
最大規模の団体の添付404GB/70.6万ファイル/年 25〜30GB
外付けが要る団体7 団体のうち 1
却下した方式コード改修・シンボリックリンク・UTM 共有フォルダ
足並みを揃えた場所保守コンソール・NGW Mac設定・移行.app・構築マニュアル・移行ランブック
構成の型A/B の 2 つ。ペアは A+A か B+B

bind mount の行数より、周りを直した場所の数のほうが多い。


AIとの協働作業での学び

設計と道具が食い違ったとき、「揃える」と決めたのは人だった。 raw device passthrough は設計上は正しい。AI はその利点を正確に並べてくれる。だが「今は既存機能に種別を足すだけにする」という判断は、構築マニュアルや移行ランブックまで含めた手間の見積もりから来る。それは AI の目の前の文脈には無い。

二つの一覧を並べる作業は、AI が得意だ。 ngw-files.conf と ngw-backup.conf の差分を出して、漏れを見つけ、テストで包含関係を縛るところまでは速い。ただし「並べろ」と言えるようになるには、仕様のメモを読み直して、二つの一覧が別々に存在することを思い出す時間が要った。並べるのは AI、並べるべきものに気づくのは人、という分担がはっきりした。

「あり得ないことにする」は、設計の道具である。 A+A か B+B しか無い、と決めた瞬間に、画面の分岐もテストも文書も半分になった。制約を決めるのは人で、それを画面とテストに写すのは AI に任せられる。逆は成り立たない。AI は「A+B の場合はこうします」と丁寧に分岐を増やしてしまう。

AI にとっての「古典的」は、私にとっての「魔法」だった。 bind mount は Linux では何十年も前からある。AI は当たり前の顔で出してきた。だが私は知らなかったし、知らないまま 30 年やってこれた。知らない道具を、AI は「古典的です」と言って差し出してくる。これを疑わずに受け取るのではなく、照合してから消すのように、自分の手で確かめる手順を一つ挟んで受け取る。魔法を魔法のまま使わない、というのが私の側の作法になった。

安全側に倒した判断は、別の場所で止まる。 失敗1で「止まって気づく」を選んだら、失敗4で「複製先が上がらない」になった。同じ判断の表と裏だと気づいたのは、複製先で Apache が上がらなかった時である。AI に「この変更は、複製とバックアップと監視にどう波及するか」と最初に聞いておけば、昼前に見えていたかもしれない。次からは、そう聞く。