rclone でローカルファイルを OpenList の WebDAV インターフェース経由でクォークのクラウドドライブにアップロードすると、OpenList のメモリ使用量が明らかに増加し、コンテナ OOM を発生させるすることもあります。調査の結果、重要なのは、クォークドライバーがアップロード前にファイル全体をキャッシュすることと、コンテナの統計値にファイルページキャッシュが含まれていることでした。

本稿では、OpenList v4.2.7、通常のクォークドライバー、Docker、cgroup v2 を対象として分析します。 다른ドライバーやその他のバージョンを使用する場合は、実装を再確認する必要があります。

クォークのアップロードでキャッシュが必要な理由

OpenList の WebDAV エントリーポイントは HTTP リクエストボディをファイルストリームにラップし、対象のストレージドライバーに渡します。この処理自体は、ファイル全体を一度にメモリへ読み込むものではありません。

しかし、通常のクォークドライバーはまずファイルの MD5 と SHA1 を取得します。WebDAV アップロード経路では、ファイルオブジェクトにこの 2 つのハッシュ値が提供されないため、ドライバーは次を呼び出します。

stream.CacheFullAndWriter(&up, io.MultiWriter(writers...))

この呼び出しは、ハッシュを計算する一方でファイル全体をキャッシュします。続いて、ドライバーはクォークに事前アップロードとハッシュの要求を送信します。即時アップロードができない場合は、キャッシュからデータを読み出し、分割してアップロードします。

ハッシュの計算にはごく少量のメモリしか必要ありませんが、実際に大きな領域を占めるのは、後から読み戻すために保持されるファイル内容だからです。 クォークが分割アップロードに対応していても、先ほどのファイル全体キャッシュが存在しないことを意味しません。

v4.2.7 の HybridCache には、メモリとファイルという 2 種類のバックエンドがあります。メモリが許可されている場合、キャッシュの拡張を試み、割り当てが制限された後でファイルに切り替えます。そのうち一部は匿名 mmap によって管理されています。そのため、Go の GC パラメータを調整するだけでは、すべてのキャッシュ使用量を制約することはできません。

rclone の --transfers は同時に転送されるファイル数を制御します。2 つのファイルを並行してアップロードすると、ファイル全体のキャッシュが 2 つ同時に存在する可能性があります。rclone 自身の --buffer-size を小さくしても、OpenList プロセス内のこの部分のキャッシュを直接制限することはできません。

一時ディレクトリがあってもメモリを消費する理由

たとえば次のとおりです。

"temp_dir": "data/temp"

これは一時ファイルの保存場所を指定するだけで、毎回キャッシュをディスクへ書き込むことを強制するものではありません。この相対パスの実際の場所は、OpenList の作業ディレクトリと Docker のマウントを組み合わせて判断する必要があります。

さらに、v4.2.7 のキャッシュ拡張チェックではシステム全体の空きメモリ統計を使用しますが、このチェックではコンテナの memory.max と memory.current を直接読み取りません。そのため、ホストに空きメモリが残っていても、コンテナはまず自分自身のメモリ上限に達する可能性があります。

ディスク上の一時ファイルを使用するように変更

まずアップロードを停止し、設定をバックアップしてください。config.json のトップレベルに次を設定します。

"min_free_memory": -1

これはフィールドの断片であり、完全な設定ファイルではありません。変更|other内容を保持し、有効な JSON 構造を維持してください。

v4.2.7 の初期化コードは、負の値を HybridCache のメモリキャッシュを無効化するように解釈します。0 はしきい値を自動的に計算する設定であり、無効化を意味しません。この変更が影響するのは該当するキャッシュの仕組みだけで、プロセス全体のメモリに対するハード上限ではありません。

temp_dir が実際のディスク上にあり、十分な空き容量があることを確認してください。一時ディレクトリが tmpfs 上にある場合、ファイルキャッシュも引き続きメモリを消費するため、期待した効果が得られません。ファイル全体をキャッシュするため、並行してアップロードされるファイル数分のディスク領域も 충분히確保しておく必要があります。

Docker の作業ディレクトリとマウントを確認してください。

container='openlist'
docker inspect -f '工作目录={{.Config.WorkingDir}}
{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}' "$container"

作業ディレクトリが /opt/openlist、一時ディレクトリが data/temp であることを確認済みのデプロイ環境では、次のように確認できます。

docker exec openlist df -h /opt/openlist/data/temp

その他のデプロイ環境では、実際のパスに置き換えてください。設定の完了後は再起動します。

docker restart openlist
docker logs --tail 100 openlist 2>&1 |
    grep -E 'disable memory cache|min free memory|auto memory limit'

次のログが表示されれば、設定が有効になったことを示します。

disable memory cache

この時点で auto memory limit: 4MB が引き続き表示されていても、そのキャッシュが再び有効になったことを意味しません。設定が反映されていない場合は、実際に使用されているファイルを編集したか、環境変数によって上書きされていないかを確認してください。変数の接頭辞は、実際の起動設定に従ってください。

メモリが実際に改善したかを判断する方法

memory.current だけでは判断できません。これには匿名メモリ、ファイルページキャッシュ、およびメモリ使用量として計上されるその他の領域が含まれます。ディスク上のファイルへ切り替えた後も、Linux は RAM を使ってファイルデータをキャッシュします。

以下のコマンドは Docker ホストで実行するもので、cgroup v2 環境向けです。コンテナ名は実際の値に合わせて変更してください。

(
    container='openlist'
    pid=$(docker inspect -f '{{.State.Pid}}' "$container") || exit 1
    [ "${pid:-0}" -gt 0 ] || { echo '容器未运行'; exit 1; }
    cg=$(awk -F: '$1 == "0" {print $3}' "/proc/$pid/cgroup")
    [ -n "$cg" ] || { echo '未找到 cgroup v2 路径'; exit 1; }
    dir="/sys/fs/cgroup$cg"

    awk '
        $1 ~ /^(anon|file|shmem|file_dirty|file_writeback|kernel|sock)$/ {
            printf "%-16s %.1f MiB\n", $1, $2/1048576
        }
    ' "$dir/memory.stat"
    cat "$dir/memory.events"
    docker stats --no-stream "$container"
)

最適化後の実測結果は次のとおりです。

指標使用量
anon26.8 MiB
file131.4 MiB
sock5.5 MiB
shmem0.0 MiB
file_dirty131.3 MiB
file_writeback0.0 MiB
Docker 表示メモリ36.2 MiB

この時点では匿名メモリが少なく、主な使用量はファイルページキャッシュでした。その大半はまだディスクへ書き戻されていません。file_dirty や shmem などは内訳の項目なので、file に再び加算してはいけません。

file_writeback=0 は、サンプリング時点で書き戻し中のデータ量が 0 だったことを示すだけで、ファイルがディスクを使用していないことを意味しません。ダーティページは回収される前にまず書き戻される必要があるため、すべてのファイルキャッシュを直ちに解放可能とはみなせません。

Linux 上の Docker CLI は一部のファイルキャッシュを控除するため、表示されるメモリ使用量が memory.current より明らかに低くなることがあります。両者の用途は異なるため、memory.stat も併せて確認してください。

これらの結果は、キャッシュ戦略の調整が有効であることを支持しています。ただし、単一のサンプルだけで、実行全体を通じてメモリリークがないことを証明することはできません。異常が疑われる場合は、アップロード終了直後にメモリ使用量が減少しないことだけから判断せず、同じタスクを複数回実行したときの anon、総使用量、OOM イベントを比較してください。

負荷に応じてコンテナの上限を設定する

まず、代表的な高負荷の同期処理を実施し、総メモリ使用量のピークを記録します。カーネルが memory.peak を提供している場合は、その値を読み取れます。そうでない場合、1 秒ごとに memory.current をサンプリングしても、観測期間内の近似ピークしか得られず、短時間のスパイクを検出できない可能性があります。

今回の調整後には、約 478 MiB のサンプリングピークが観測されたため、768 MiB を試行時の開始点として利用できます。

docker update --memory 768m openlist

この上限は完全な負荷条件下での検証が必要であり、すべての環境に対する推奨固定値ではありません。Docker が既存のスワップ設定と競合すると表示した場合は、まず memory-swap の設定を確認し、方針を決めてください。

Compose を使用する場合は、既存のサービス設定に上限を統合します。

services:
  openlist:
    # 保留原有镜像、挂载和网络设置
    mem_limit: 768m

Docker に表示される数十 MiB をそのまま根拠に、上限を低すぎる値に設定しないでください。ファイルキャッシュも cgroup のメモリ上限に算入されます。設定後、memory.events の oom と oom_kill が増加していないか、およびアップロードが失敗したり明らかに遅くなったりしていないかを監視してください。負荷が重い場合は、まず rclone の同時転送数を 1 に下げて比較するとよいです。

参考資料