📦

【Laravel】DBのデータを定期的にS3へアーカイブして負荷軽減!!!

2022/10/22に公開

はじめに

こんな悩みはないでしょうか?

「ログや記録をDBに残し続けているので、定期的に手動で削除しないといけない」
「溜まり続けるデータのせいで、サーバーが重くなってしまう」
「頻繁にAWSのauroraがフェイルオーバーする😭」

※実は上記全て実体験;;

データが溜まりすぎた場合手動で削除してもいいのですが、せっかくエンジニアという職業なので、コードの力で自動化を実現したいですね!
筆者自身も業務でレコードをアーカイブする機能を作成する機会があり、この記事で解説する機能でDBの負荷をかなり軽減することが出来ました。

なので今回は、実装の際に得た知見とともに、作成方法を解説したいと思います!

この記事を見るメリット

  • DBのデータをアーカイブすることができるようになる
  • PHP上でコマンドを実行する方法を理解できる
  • Scheduleで定期実行する方法がわかる
  • Storage、S3の扱い方が理解できるようになる

対象者

この記事は下記のような人を対象にしています。

  • プログラミング初学者
  • 駆け出しエンジニア
  • 溜まり続けるデータがあるサービスの開発者
  • コマンドを実装したい人

結論

データが本当に多すぎる場合にmysqldumpを実行すると、重すぎてエラーになるので、1週間に1度Scheduleで実行するようにしよう!

解説

手動実行・定期実行どちらにも対応できるようにしたかったので、artisanコマンドを作成します。

前提

  • 1週間に一度実行する
  • 万が一のことを考え、テーブルのデータを削除するだけでなくS3にアーカイブする
  • 定期実行だけでなく、コマンドで手動実行できるようにもする

コマンドの作成

コマンド用のファイルを作成します。

php artisan make:command ArchiveTableData

作成したファイルにコマンド名や説明、エラー処理などを記述します。

<?php

namespace App\Console\Commands;

use App\Actions\Command\UploadTableDataS3;
use Illuminate\Console\Command;

class ArchiveTableData extends Command
{
        // 定数の定義(定数ファイルがある場合は、そちらに記述)
    public const ARCHIVE_SUCCESS             = 0; // 正常終了
    public const ARCHIVE_NO_MODEL            = 1; // モデルが存在しない
    public const ARCHIVE_DUMP_ERROR          = 2; // mysqldumpに失敗
    public const ARCHIVE_LOCAL_NO_FILE_ERROR = 3; // ローカルにファイルが存在しない
    public const ARCHIVE_S3_NO_FILE_ERROR    = 4; // s3にファイルが存在しない
    public const ARCHIVE_OTHER_ERROR         = 9; // 例外エラー

    // アーカイブするテーブルのModel名
    // 配列にModel名を追記するだけでテーブルの指定ができる
    // ※ここはコマンドのオプションで指定できるようにしても有り
    public const MODEL_NAMES = [
        'HogeAppLog',
        'HogePlayer',
        'HogeArticle',
    ];

    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    // コマンド名
    protected $signature = 'db:archive-table-data';

    /**
     * The console command description.
     *
     * @var string
     */
    // コマンドの説明
    protected $description = '
        以下テーブルのデータをダンプする。
	sqlファイルをS3にアップし、テーブルから1週間以上前ののデータを削除する。
        ------------------------------
        hoge_app_logs,
        hoge_players,
        hoge_articles,
        ------------------------------';

    /**
     * Execute the console command.
     *
     * @return int
     */
    // コマンド実行時に以下の処理が実行される
    public function handle(UploadTableDataS3 $action)
    {
	// 次項で解説するActionsの関数を実行
        $results = $action->execute(self::MODEL_NAMES);
	
	// 関数の返り値でメッセージを分ける
        switch ($results['result']) {
            case self::ARCHIVE_SUCCESS:
                $this->info('アーカイブに成功しました。');
                $this->comment('s3とDBを確認してください。');
                break;
            case self::ARCHIVE_NO_MODEL:
                $this->error('以下のModel名に対応したテーブルが見つかりませんでした。');
                $message = 'Model名:' . $results['message'];
		// \033[0;31m  \033[0m \nでコンソールの表示を赤色にしている
                echo "\033[0;31m $message \033[0m \n"; 
                $this->comment('ArchiveTableData.phpの$modelNames配列内の誤字などを確認してください。');
                break;
            case self::ARCHIVE_DUMP_ERROR:
                $this->error('DBのDumpに失敗しました。以下のエラーをご確認ください。');
                $message = $results['message'];
                echo "\033[0;31m $message \033[0m";
                break;
            case self::ARCHIVE_LOCAL_NO_FILE_ERROR:
                $this->error('ローカルのStorageにファイルが保存されていませんでした。');
                break;
            case self::ARCHIVE_S3_NO_FILE_ERROR:
                $this->error('S3内の以下のパスにファイルが存在しませんでした。ファイルのアップロードに失敗している可能性があります。');
                $message = $results['message'];
                echo "\033[0;31m $message \033[0m";
                break;
            case self::ARCHIVE_OTHER_ERROR:
                $this->error('例外をキャッチしました。以下のエラーを参照ください。');
                $message = $results['message'];
                echo "\033[0;31m $message \033[0m";
                break;
            default:
                $this->error('その他のエラーです。コードの内容を確認してください。');
                break;
        }
    }
}

アーカイブを実行する関数を作成

Actionsに関数用のファイルを作成し、コマンド実行時に呼び出す関数を記述

<?php

namespace App\Actions\Command;

use App\Console\Commands\ArchiveTableData;
use Carbon\Carbon;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\Process\Process;
use Throwable;

class UploadTableDataS3
{
    public function execute(array $modelNames)
    {
        // 結果を格納する配列を用意
        $results = [
            'result'  => '結果に対応した定数を格納',
            'message' => '結果の詳細を格納',
        ];

                // コマンドのファイルで定義したモデルをforeachで回す
        foreach ($modelNames as $modelName) {
            $kebabModelName = strtolower(preg_replace('/[A-Z]/', '-$0', lcfirst($modelName)));
            $dbTableName    = strtolower(preg_replace('/[A-Z]/', '_$0', lcfirst($modelName))) . 's';

            // テーブルの存在チェック(Model名の記述ミスなどの際はエラーを返す)
            if (!Schema::hasTable($dbTableName)) {
                $results['result']  = ArchiveTableData::ARCHIVE_NO_MODEL;
                $results['message'] = $modelName;

                return $results;
            }

            // 環境設定を取得し、mysqldumpコマンドを作成
            $dbUser         = 'root';
            $dbHost         = config('database.connections.mysql.host');
            $dbName         = config('database.connections.mysql.database');
            $filePath       = $this->resolvePath($kebabModelName);
            $mysql_command  = "mysqldump --skip-column-statistics -u {$dbUser} -h {$dbHost} {$dbName} {$dbTableName} > $filePath";

	    // mysqldympを実行し、Storageに保存
            try {
                $process_mysql = Process::fromShellCommandline($mysql_command);
                $process_mysql->mustRun();
            } catch (ProcessFailedException $exception) {
                \Log::error($exception->getMessage());
                $results['result']  = ArchiveTableData::ARCHIVE_DUMP_ERROR;
                $results['message'] = $exception->getMessage();

                return $results;
            } catch (Throwable $exception) {
                \Log::error($exception->getMessage());
                $results['result']  = ArchiveTableData::ARCHIVE_OTHER_ERROR;
                $results['message'] = $exception->getMessage();

                return $results;
            }

            // ローカルのStorageにファイルが保存できているか確認
            if (file_exists($filePath)) {
                // s3にファイルをアップ
                $key = sprintf('archives/%s', $kebabModelName);
                Storage::disk('s3')->putFileAs($key, $filePath, $this->resolveFileName($kebabModelName), 'public');

                // s3にファイルが保存できているかを確認
                $s3FileExists = Storage::disk('s3')->exists("archives/{$kebabModelName}/{$this->resolveFileName($kebabModelName)}");
                if ($s3FileExists) {
                    // 1週間前以降のデータを空にする
                    $model = 'App\Models\\' . $modelName;
                    $model::query()
                            ->where('created_at', '<=', Carbon::now()->subDay(7)->format('Y-m-d H:i:s'))
                            ->forceDelete();

                    // 一時保存していたローカルファイルを削除
                    Storage::delete($this->resolveFileName($kebabModelName));
                } else {
                    $results['result']  = ArchiveTableData::ARCHIVE_S3_NO_FILE_ERROR;
                    $results['message'] = "archives/{$kebabModelName}/{$this->resolveFileName($kebabModelName)}";
                    \Log::error($results);

                    return $results;
                }
            } else {
		\Log::error($results);
                $results['result']  = ArchiveTableData::ARCHIVE_LOCAL_NO_FILE_ERROR;

                return $results;
            }
        }

	// エラーに引っ掛からなかったので成功を返す
        $results['result'] = ArchiveTableData::ARCHIVE_SUCCESS;

        return $results;
    }

        // ローカルに保存する際のパスを指定
    private function resolvePath($modelName): string
    {
        return sprintf('storage/app/%s', $this->resolveFileName($modelName));
    }

    // ファイル名
    private function resolveFileName($modelName): string
    {
        return sprintf(
            "{$modelName}-archive-%s.sql",
            Carbon::now()->format('Y-m-d'),
        );
    }
}

定期実行のための設定

/app/Console/Kernel.phpのschedule()メソッド内に以下の記述を追加
曜日、時刻は自由に設定してください🙇‍♂️

// 負荷対策のため、毎週水曜日18:30に負荷がかかるデータをs3にアーカイブ
$schedule->command(ArchiveTableData::class)
    ->weekly()
    ->wednesdays()
    ->at('18:30');

mysqldump not foundエラーになった場合

プロジェクトが実行されているサーバー内に、mysql-clientがないとmysqldumpが実行できないのでインストールしてください。
サーバー内でmysql --versionなどで確認も可能です!

おわりに

今回はDBのデータのアーカイブ方法を解説させていただきました。
一度実装してしまうと、手動で保存・削除を行わなくてもいいので他の業務に専念することができますね😊

最後まで記事を見てくださりありがとうございました。
誤字や脱字、コードのリファクタリングできる箇所などがありましたらコメントくださるとありがたいです!
また、いいねをしてくださると、筆者が喜びます:)

Discussion