O_APPENDの追記、末尾への書き込みと一つの記録を分けて考える

白い細長い台に四つの淡色のブロックが並び、その右端の先に青いブロックが浮く。手前には桃色の二片の間に緑の一片が置かれている。

複数の処理が同じログへ書き足すとき、「末尾を調べて、その位置へ書く」という方法で足りるでしょうか。二つの処理が同じ末尾位置を見つければ、片方の記録をもう片方が上書きする可能性があります。LinuxのO_APPENDは、この末尾位置の決定と書き込みの間にある隙間をなくすための指定です。

ただし、追記モードで開けば一行のログが必ず丸ごと保存され、障害後も残る、という意味ではありません。末尾へ置くこと、一つの記録としてまとまること、保存が確定することを分けて考えます。

簡単にまとめると?

  • O_APPENDでは、writeのたびに末尾へ位置を合わせ、その書き込みと一つの不可分な操作として扱います。最初に一度だけ末尾へ移動する方法とは異なります。
  • 一つの論理レコードを複数回のwriteに分けると、その間に別の追記が入り得ます。短い書き込みの残りを送る場合も、呼び出しをまたぐまとまりは自動では守られません。
  • この保証をNFSや電源断後の保存へ広げません。保存先の仕様、実際の書き込み単位、戻り値、永続化の条件を個別に確認します。

末尾を見つけてから書くまでに、別の処理が入る

通常ファイルの末尾が100バイト目にあるとします。処理AとBは同じファイルを別々に開いていて、それぞれ独立したファイルオフセット、つまり次に読み書きする位置を持っています。Aは4バイトのAAAA、Bは4バイトのBBBBを追記したいとします。

まずAがlseekで末尾へ移動し、位置100を得ます。その後、Bも末尾へ移動して位置100を得たとします。まだ書き込みは行われていないので、両方が同じ位置を指します。Aが4バイトを書いた後も、Bが別に持つ位置は100のままです。Bがその位置へ4バイトを書けば、Aの記録と重なります。

この順序では最後の4バイトはBBBBとなり、ファイルの長さは104バイトです。AもBも4バイトを書けたとしても、二つの記録を保持できたわけではありません。「末尾を得る」と「そこへ書く」の二段階を、別の書き手が割り込めない一つの操作にはできていなかったことが原因です。

O_APPENDはwriteのたびに末尾を決める

O_APPENDで開いたファイルへのwriteでは、書く直前にファイルオフセットを末尾へ設定し、その位置の変更と書き込みを不可分に行うとLinux man-pagesは定義しています。[1、2] ここで不可分なのは、末尾を選ぶ処理と一回のwriteの組です。

先ほどの例で、Aの4バイト書き込みが先に終わるなら、Aは100から103へ書き、次のBは新しい末尾104から107へ書きます。ファイルは108バイトとなり、追加された部分はAAAABBBBです。反対にBが先ならBBBBAAAAとなります。両方の記録を末尾に置けても、どちらが先になるかまでO_APPENDが業務上の順序として決めるわけではありません。

重要なのは、末尾への移動が「開いたときに一度だけ」ではなく、writeごとに行われる点です。最初にlseekで末尾へ移動しておくことや、一度得たファイルサイズを保存しておくことでは、後から別の書き手が延ばした長さに追随できません。

別々に末尾100を選ぶとAとBが同じ位置に書き長さ104。追記操作が順に完了するとAは100、Bは104から書き長さ108。
各writeが4バイトすべてを書けると仮定した独自の順序モデル。別々のオープンファイル記述を使い、Aの書き込み後にBが書く一つの順序を示す。実機の並行負荷試験ではない。

一行と、一回のwriteは同じとは限らない

ログの一行を一つの記録として扱いたいとします。アプリケーションでは一行でも、実際には「時刻と識別子」「本文」「改行」を別々のwriteで送っているかもしれません。O_APPENDは、それら複数回の呼び出しを一つのトランザクションにまとめません。

説明用に、Aの記録をA[と]\nの二回に分け(\nは改行を表す)、その間にBがB\nを書いた順序を考えます。すべてが正常な追記でも、追加部分はA[B\n]\nとなります。Aの記録を完成させるまでBを待たせる約束は、O_APPENDだけからは得られないためです。

また、言語の「一回の出力関数」が、必ず一回のシステムコールになるとも限りません。バッファリングするライブラリを使う場合には、その層でどの単位にまとめられ、いつwriteへ渡されるのかを確認する必要があります。ここで問題にしている単位は、画面上の行数や関数名ではなく、実際の書き込み呼び出しです。

一つの記録を一つのバッファにまとめ、全量が一回で書けた場合は、複数回に分割した隙間を減らせます。ただし、これをあらゆるサイズ・保存先での無条件な全量成功の保証に置き換えることはできません。

短い書き込みの続きを、別の追記として考える

writeは要求したバイト数より少ない数を返して成功する場合があります。[2] たとえば8バイトのレコードを書こうとして3バイトしか書けなければ、まだ5バイト残っています。戻り値は「成功か失敗か」だけでなく、「どこまで進んだか」を示す情報です。

残りを続けて書くときは、元のバッファの先頭3バイトを除いた、残り5バイトを渡します。元の8バイトをそのまま再送すれば、先頭3バイトを重複させます。しかし、O_APPENDで残りを正しく渡したとしても、その次のwriteは、その時点での末尾を使う別の呼び出しです。

最初の3バイトの後にBが4バイトを追加し、その後でAが残り5バイトを書けば、Aの記録の途中にBの記録が入ります。このモデルでは誰も過去の位置を上書きしていません。それでも、Aの8バイトが連続した一レコードとして残る条件は満たしていません。

まとまりを必要とするなら、すべての書き手が従う排他制御でレコードの全書き込みを囲む、あるいは書き込みを一つの担当へ集める、といった別の設計を検討します。どちらもエラー時や途中終了時の処理が必要で、フラグを一つ追加するだけの話ではありません。

保存先と、障害後に残る条件は別に確認する

Linux man-pagesのopen(2)は、NFS上で複数プロセスが同時に追記すると破損につながり得ると注意しています。クライアント側で追記を模擬する必要があり、競合を避けられないためです。[1、O_APPEND] ローカルの通常ファイルで説明した保証を、ファイルパスが見えるという理由だけでネットワーク上の保存先へ広げないことが必要です。

さらに、writeが正常に戻ったことは、データが記憶装置へ永続化された保証ではありません。[2、NOTES] 追記位置が正しいかという問いと、電源断後にも必要な記録が残るかという問いは、別の層にあります。後者にはfsync等の同期処理とその戻り値、保存先や装置の条件も関わります。

複数の書き手からログを残す設計では、まず保存先が提供する追記の意味を確認します。次に、一つのレコードが何回のwriteになるか、短い書き込みやエラーをどこまで回復するかを決めます。そのうえで、必要な記録順序や障害後の残り方を別の要件として扱います。O_APPENDが解決する「末尾を選んでから書くまでの競合」を正確に切り出すと、まだ必要な仕組みが見えてきます。

出典

  1. Linux man-pages 6.19, open(2)、O_APPEND。末尾位置と書き込みの不可分性、NFS上の注意点。
  2. Linux man-pages 6.19, write(2)(ページ日付2026年2月8日)、DESCRIPTION、RETURN VALUE、NOTES。短い書き込みと永続化との区別。図と数値例は、明記した順序を与えた独自モデルです。