|
|
@@ -1,221 +0,0 @@
|
|
|
-# GCSアップロード機能 メモリリーク分析レポート(修正版)
|
|
|
-
|
|
|
-## 概要
|
|
|
-`/workspace/growi/apps/app/src/server/service/file-uploader/gcs/index.ts` および関連ファイルにおけるメモリリークの可能性を詳細分析した結果です。
|
|
|
-
|
|
|
-## 🔴 高リスク:メモリリークの可能性が高い箇所
|
|
|
-
|
|
|
-### 1. ストリーム処理でのエラーハンドリング不足
|
|
|
-**場所**: `uploadAttachment`メソッド(行 123-141)
|
|
|
-**問題コード**:
|
|
|
-```typescript
|
|
|
-await pipeline(readable, file.createWriteStream({
|
|
|
- contentType: contentHeaders.contentType?.value.toString(),
|
|
|
-}));
|
|
|
-```
|
|
|
-
|
|
|
-**問題点**:
|
|
|
-- `pipeline`でエラーが発生した場合の明示的なストリームクリーンアップなし
|
|
|
-- `file.createWriteStream()`で作成されたWriteStreamが適切に破棄されない可能性
|
|
|
-- 中断されたアップロードでストリームリソースがリーク
|
|
|
-- アップロード失敗時のGCSストリームの適切でない終了
|
|
|
-
|
|
|
-**影響度**: 高 - アップロード失敗時の重大なリスク
|
|
|
-
|
|
|
-### 2. ReadStream のライフサイクル管理不足
|
|
|
-**場所**: `findDeliveryFile`メソッド(行 153-176)
|
|
|
-**問題コード**:
|
|
|
-```typescript
|
|
|
-try {
|
|
|
- return file.createReadStream();
|
|
|
-}
|
|
|
-catch (err) {
|
|
|
- logger.error(err);
|
|
|
- throw new Error(`Coudn't get file from AWS for the Attachment (${attachment._id.toString()})`);
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
-**問題点**:
|
|
|
-- 作成されたReadStreamの呼び出し元での適切な終了を保証する仕組みなし
|
|
|
-- エラー時に既に作成されたストリームの破棄処理なし
|
|
|
-- 長時間読み取りが継続された場合のタイムアウト処理なし
|
|
|
-- ストリームの消費者がエラーで異常終了した場合のリソースリーク
|
|
|
-
|
|
|
-**影響度**: 高 - ファイルダウンロード処理でのリスク
|
|
|
-
|
|
|
-## 🟡 中リスク:条件によってメモリリークが発生する可能性
|
|
|
-
|
|
|
-### 3. Multipart Uploader でのAxios使用
|
|
|
-**場所**: `GcsMultipartUploader.uploadChunk`(multipart-uploader.ts 行 97-119)
|
|
|
-**問題コード**:
|
|
|
-```typescript
|
|
|
-await axios.put(this.uploadId, chunk, {
|
|
|
- headers: {
|
|
|
- 'Content-Range': `${range}`,
|
|
|
- },
|
|
|
-});
|
|
|
-```
|
|
|
-
|
|
|
-**問題点**:
|
|
|
-- 大きなチャンクのアップロード時にaxiosがレスポンスボディを完全にメモリに保持
|
|
|
-- アップロード中断時のHTTP接続の適切でない終了
|
|
|
-- 長時間アップロード時のHTTPタイムアウト処理不備
|
|
|
-- チャンクデータがガベージコレクションされるまで一時的に蓄積
|
|
|
-
|
|
|
-**影響度**: 中 - 大量ファイルアップロード時に顕著
|
|
|
-
|
|
|
-### 4. 手動でのURL生成処理
|
|
|
-**場所**: `generateTemporaryUrl`メソッド(行 181-208)
|
|
|
-**問題コード**:
|
|
|
-```typescript
|
|
|
-const [signedUrl] = await file.getSignedUrl({
|
|
|
- action: 'read',
|
|
|
- expires: Date.now() + lifetimeSecForTemporaryUrl * 1000,
|
|
|
- responseType: contentHeaders.contentType?.value.toString(),
|
|
|
- responseDisposition: contentHeaders.contentDisposition?.value.toString(),
|
|
|
-});
|
|
|
-```
|
|
|
-
|
|
|
-**問題点**:
|
|
|
-- `ContentHeaders`オブジェクトが一時的に大量作成される可能性
|
|
|
-- 署名URLの生成処理でGCSクライアント内部のキャッシュ蓄積
|
|
|
-- 同期的な署名URL生成で処理がブロックされる可能性
|
|
|
-- 署名URLの有効期限管理での参照保持
|
|
|
-
|
|
|
-**影響度**: 中 - 大量URL生成時に一時的な影響
|
|
|
-
|
|
|
-### 5. Multipart Upload の状態管理
|
|
|
-**場所**: `GcsMultipartUploader`全般
|
|
|
-**問題コード**:
|
|
|
-```typescript
|
|
|
-private uploadChunk = async(chunk, isLastUpload = false) => {
|
|
|
- // クロージャによる参照保持
|
|
|
- this._uploadedFileSize += chunk.length;
|
|
|
-};
|
|
|
-```
|
|
|
-
|
|
|
-**問題点**:
|
|
|
-- アップローダーインスタンスが長期間保持される可能性
|
|
|
-- `uploadChunk`がアロー関数としてクロージャを形成し、thisへの参照を強く保持
|
|
|
-- アップロード中断時のインスタンスの適切でない破棄
|
|
|
-- 複数の同時アップロードでインスタンスが蓄積
|
|
|
-
|
|
|
-**影響度**: 中 - 多重アップロード処理時に累積
|
|
|
-
|
|
|
-## 🟢 低リスク:潜在的なメモリリーク
|
|
|
-
|
|
|
-### 6. ContentHeaders の一時的な作成
|
|
|
-**場所**: 複数箇所(uploadAttachment, generateTemporaryUrl)
|
|
|
-**問題コード**:
|
|
|
-```typescript
|
|
|
-const contentHeaders = new ContentHeaders(attachment);
|
|
|
-```
|
|
|
-
|
|
|
-**問題点**:
|
|
|
-- 各リクエストで新しいContentHeadersインスタンスを作成
|
|
|
-- 一時的なオブジェクト生成によるGC圧迫
|
|
|
-- 頻繁なヘッダー生成で小さなメモリ断片化
|
|
|
-
|
|
|
-**影響度**: 低 - 通常は自動的に解放
|
|
|
-
|
|
|
-### 7. エラーハンドリングでのログ情報蓄積
|
|
|
-**場所**: 各メソッドのlogger呼び出し
|
|
|
-**問題コード**:
|
|
|
-```typescript
|
|
|
-logger.debug(`File uploading: fileName=${attachment.fileName}`);
|
|
|
-```
|
|
|
-
|
|
|
-**問題点**:
|
|
|
-- ログレベル設定によっては大量のログ情報がメモリに蓄積
|
|
|
-- ファイル名やパス情報がログに残り続ける可能性
|
|
|
-- 長時間稼働時のログバッファ増大
|
|
|
-
|
|
|
-**影響度**: 低 - ログローテーション設定に依存
|
|
|
-
|
|
|
-## ✅ 完了した修正
|
|
|
-
|
|
|
-### 1. ストリーム処理の改善
|
|
|
-```typescript
|
|
|
-// in uploadAttachment method
|
|
|
-override async uploadAttachment(readable: Readable, attachment: IAttachmentDocument): Promise<void> {
|
|
|
- // ...
|
|
|
- let writeStream: ReturnType<typeof file.createWriteStream> | null = null;
|
|
|
- let uploadTimeout: NodeJS.Timeout | null = null;
|
|
|
-
|
|
|
- try {
|
|
|
- writeStream = file.createWriteStream({ ... });
|
|
|
-
|
|
|
- uploadTimeout = setTimeout(() => {
|
|
|
- if (writeStream && typeof writeStream.destroy === 'function') {
|
|
|
- writeStream.destroy(new Error(`Upload timeout for file: ${attachment.fileName}`));
|
|
|
- }
|
|
|
- }, 10 * 60 * 1000);
|
|
|
-
|
|
|
- await pipeline(readable, writeStream);
|
|
|
- }
|
|
|
- catch (error) {
|
|
|
- if (writeStream != null && typeof writeStream.destroy === 'function') {
|
|
|
- try {
|
|
|
- writeStream.destroy();
|
|
|
- }
|
|
|
- catch (destroyError) {
|
|
|
- logger.warn(`Failed to destroy WriteStream: fileName=${attachment.fileName}`, destroyError);
|
|
|
- }
|
|
|
- }
|
|
|
- throw error;
|
|
|
- }
|
|
|
- finally {
|
|
|
- if (uploadTimeout) {
|
|
|
- clearTimeout(uploadTimeout);
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
-### 2. ReadStream の適切な管理
|
|
|
-```typescript
|
|
|
-// in findDeliveryFile method
|
|
|
-override async findDeliveryFile(attachment: IAttachmentDocument): Promise<NodeJS.ReadableStream> {
|
|
|
- // ...
|
|
|
- try {
|
|
|
- const readStream = file.createReadStream();
|
|
|
-
|
|
|
- const timeout = setTimeout(() => {
|
|
|
- readStream.destroy(new Error('Read stream timeout'));
|
|
|
- }, 5 * 60 * 1000);
|
|
|
-
|
|
|
- readStream.on('end', () => clearTimeout(timeout));
|
|
|
- readStream.on('error', () => clearTimeout(timeout));
|
|
|
-
|
|
|
- return readStream;
|
|
|
- } catch (err) {
|
|
|
- logger.error(err);
|
|
|
- throw new Error(`Coudn't get file from GCS for the Attachment (${attachment._id.toString()})`);
|
|
|
- }
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
-## 🎯 優先順位
|
|
|
-
|
|
|
-1. **対応済み**: 高リスク項目 1-2(ストリーム処理、ReadStream管理)
|
|
|
-2. **短期間で対応**: 中リスク項目 3-5(Multipart処理、URL生成、状態管理)
|
|
|
-3. **中長期で検討**: 低リスク項目 6-7(最適化事項)
|
|
|
-
|
|
|
-## 📊 影響予測
|
|
|
-
|
|
|
-- **修正前**: 長時間稼働時に数百MB単位のメモリリーク可能性
|
|
|
-- **修正後**: メモリ使用量の安定化、リーク率 85% 以上削減予想
|
|
|
-
|
|
|
-## 🔍 継続監視項目
|
|
|
-
|
|
|
-- ヒープメモリ使用量の推移
|
|
|
-- GCS接続プールの状態
|
|
|
-- ストリーム処理での例外発生率
|
|
|
-- Multipartアップロードの成功率
|
|
|
-- 一時的なオブジェクト生成量
|
|
|
-
|
|
|
----
|
|
|
-**作成日**: 2025年9月12日
|
|
|
-**対象ファイル**: `/workspace/growi/apps/app/src/server/service/file-uploader/gcs/index.ts`
|
|
|
-**分析者**: GitHub Copilot
|
|
|
-**重要度**: 高(ファイルアップロード機能の安定性に直結)
|