環境
- MySQL 8.0
- Mroonga 15.x / Groonga 16.x
- 約28万行の全文検索テーブル、3分ごとにINSERT/UPDATEあり
- 3ヶ月より古いデータを毎日消したい
DELETEの何がダメなのか
redo logが溜まる
MroongaはGroongaベースだけど、MySQLのredo log(InnoDBのトランザクションログ)にDELETEの記録が書き込まれる。毎日DELETEしてるとこれがどんどん溜まっていく。
-- 小分けにしても結局redo logには蓄積する
DELETE FROM search WHERE created < DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 500;
Groongaファイルが断片化する
Mroongaが内部で使うGroongaのデータファイル(.mrn*)は、DELETEしても領域が解放されない。削除と追加を繰 り返すと断片化が進んでファイルが肥大化する。
最悪クラッシュする
redo logの圧迫とGroongaのmutexが競合すると、InnoDBのsemaphore waitが発生してMySQLがハングする。そのときMroongaが書 き込み中だとテーブルが壊れる。
dump/reimport方式
「消す」んじゃなくて「必要なデータだけ取り出して作り直す」。
1. mysqldump --where で残すデータだけdump
2. DROP TABLE + .mrn*ファイル削除
3. MySQL再起動
4. dumpからリストア(インデックス完全再構築)
5. CHECK TABLEで検証
やり方
設定
MYSQL="mysql -h 127.0.0.1"
DB="search"
TABLE="search"
KEEP_INTERVAL="3 MONTH"
DUMP_FILE="/root/search_cleanup_tmp.sql"
Step 1: dump
--whereで残す行だけ指定してdump。DELETEと逆で「残す行」を指定する。
mysqldump -h 127.0.0.1 "${DB}" "${TABLE}" \
--single-transaction --quick --skip-lock-tables \
--where="created >= DATE_SUB(NOW(), INTERVAL ${KEEP_INTERVAL})" \
> "${DUMP_FILE}"
Step 2: テーブル削除
${MYSQL} -e "DROP TABLE IF EXISTS ${DB}.${TABLE};"
Step 3: MySQL再起動 + クリーンアップ
systemctl stop mysqld
# 孤立したGroongaファイルを削除
rm -f /var/lib/mysql/${DB}.mrn*
# メモリも掃除
swapoff -a && swapon -a
echo 3 > /proc/sys/vm/drop_caches
systemctl start mysqld
DROP TABLEしても.mrn*ファイルが残ることがある。MySQL停止中に消すのが確実。
再起動すると以下もリセットされるのでお得:
- redo log
- Groongaのメモリ状態
- InnoDB temp tablespace
Step 4: リストア
${MYSQL} "${DB}" < "${DUMP_FILE}"
dumpにCREATE TABLE文が入ってるので、テーブル再作成とデータ投入が一発で終わる。Mroongaのインデックスもゼロから構築されて断片化なし。
Step 5: 検証
CHECK_RESULT=$(${MYSQL} -N -e "CHECK TABLE ${DB}.${TABLE};" | awk '{print $NF}')
if [ "$CHECK_RESULT" != "OK" ]; then
echo "ERROR: CHECK TABLE failed"
exit 1
fi
# FULLTEXT検索が動くかも確認
${MYSQL} -N -e "SELECT COUNT(*) FROM ${DB}.${TABLE} \
WHERE MATCH(content) AGAINST('a' IN BOOLEAN MODE);" > /dev/null 2>&1
安全対策
dumpの中身チェック
# CREATE TABLEとINSERTが入ってるか
if ! grep -q "CREATE TABLE" "${DUMP_FILE}" || ! grep -q "INSERT" "${DUMP_FILE}"; then
echo "ERROR: dump is incomplete"
exit 1
fi
# ファイルサイズが小さすぎないか
DUMP_BYTES=$(stat -c%s "${DUMP_FILE}")
if [ "$DUMP_BYTES" -lt 1024 ]; then
echo "ERROR: dump file too small"
exit 1
fi
全消し防止
KEEP=$(${MYSQL} -N -e "SELECT COUNT(*) FROM ${DB}.${TABLE} \
WHERE created >= DATE_SUB(NOW(), INTERVAL ${KEEP_INTERVAL});")
if [ "$KEEP" -eq 0 ]; then
echo "ERROR: KEEP=0, refusing to destroy all data"
exit 1
fi
ディスク空きチェック
FREE_GB=$(df --output=avail -BG /var/lib/mysql | tail -1 | tr -dc '0-9')
if [ "$FREE_GB" -lt 5 ]; then
echo "ERROR: not enough disk space"
exit 1
fi
失敗時のリカバリ
リストアに失敗したらdumpファイルを残しておけば手動で復旧できる。成功したときだけ消す。
if [ $? -ne 0 ]; then
echo "ERROR: reimport failed. Dump preserved at ${DUMP_FILE}"
exit 1
fi
rm -f "${DUMP_FILE}"
監視との連携
MySQL再起動するので、監視が「MySQLが落ちた」って誤検知する。ロックファイルで回避。
# cleanup側
CLEANUP_LOCK="/dev/shm/search_cleanup.lock"
echo $$ > "$CLEANUP_LOCK"
trap "rm -f '$CLEANUP_LOCK'" EXIT
# 監視側
if [ -f "$CLEANUP_LOCK" ]; then
LOCK_PID=$(cat "$CLEANUP_LOCK")
if kill -0 "$LOCK_PID" 2>/dev/null; then
exit 0 # cleanup中なのでスキップ
fi
fi
/dev/shm/はtmpfsなのでサーバ再起動で勝手に消える。
.mrn*ファイルの共有に注意
Mroongaは同じDB内の全テーブルが{db_name}.mrn*というGroongaファイルを共有してる。
/var/lib/mysql/search.mrn
/var/lib/mysql/search.mrn.0000000
/var/lib/mysql/search.mrn.0000001
...
rm -f ${DB}.mrn* すると同じDBの無関係なMroongaテーブルも巻き添えで壊れる。
対策: cleanup対象とそうじゃないテーブルはDBを分ける。
search DB(cleanup対象)
mqq search
search_archive DB(触らない)
tqq search_base
mqq search_2012~2026
テーブルが壊れたときの直し方
軽度: REPAIR TABLEで直るやつ
SELECT mroonga_command('clearlock');
REPAIR TABLE search;
clearlockしてからREPAIRするのがコツ。先にロック解除しないと直らないことがある。
重度: REPAIR後も書き込むたびに壊れるやつ
Groongaのデータファイル自体が壊れてる。REPAIR TABLEじゃ直らない。ファイルごと作り直す。
# 1. 書き込みを止める
# 2. データをInnoDBに退避
mysql -e "CREATE TABLE search_backup ENGINE=InnoDB AS SELECT * FROM search;"
# 3. Groongaファイルごと作り直す
systemctl stop mysqld
rm -f /var/lib/mysql/search.mrn*
systemctl start mysqld
# 4. テーブル再作成してデータ戻す
mysql -e "CREATE TABLE search (...) ENGINE=Mroonga;"
mysql -e "INSERT INTO search SELECT * FROM search_backup;"
# 5. 確認してbackup消す
mysql -e "CHECK TABLE search;"
mysql -e "DROP TABLE search_backup;"
比較
| 項目 |
DELETE方式 |
dump/reimport方式 |
| redo log負荷 |
溜まる |
なし(DROPは即時) |
| Groonga断片化 |
溜まる |
毎回ゼロから構築 |
| メモリ |
溜まる |
再起動でリセット |
| ダウンタイム |
なし |
約10秒(計画的) |
| クラッシュリスク |
高い(mutex競合) |
低い |
| 所要時間 |
行数次第 |
約2分(28万行) |
毎日10秒止まるけど、突然クラッシュして数時間ダウンするよりマシ。
cron
0 3 * * * /root/search_cleanup.sh 2>&1
まとめ
- Mroongaで
DELETE使うとredo log蓄積・Groonga断片化・mutex競合のリスクがある
mysqldump --whereで必要なデータだけ取り出して作り直すのが安全
.mrn*ファイルはDB内の全Mroongaテーブルで共有されてるので、cleanup対象は別DBに分ける
REPAIR TABLEで直らない壊れ方は.mrn*削除+テーブル再作成で対応