はじめに
HDFSからAmazon S3への移行後も、保存先を変更しただけではdataは増え続ける。本記事では、S3 rawを7日、batch Icebergを14日、Flink realtime Icebergを当日分だけ保持する。
保持期間と物理file削除は分けて扱う。
| 対象 | table / prefix | data保持期間 | 削除方法 |
|---|---|---|---|
| Raw |
raw/syslog/、raw/authlog/
|
7日 | S3 Lifecycle |
| batch Iceberg |
syslog_iceberg、authlog_iceberg
|
当日を含む14日 | Iceberg DELETE
|
| Flink event |
syslog_events、authlog_events
|
当日 | Iceberg DELETE
|
| Flink 1分集計 |
syslog_host_1m、authlog_host_1m
|
当日 | Iceberg DELETE
|
Iceberg table配下へS3 Lifecycleやaws s3 rmを設定しない。Icebergはdata fileだけでなくsnapshot、manifest、metadataの参照関係を持つため、削除とmaintenanceはIceberg procedureから実行する。
前提
- S3 rawとS3 Icebergへの移行が完了している
- Glue Databaseは
logsである - Sparkから
glue_prodcatalogを利用できる -
dt列はdate型である - Flink jobのcheckpointとIceberg commitが正常である
- 操作端末にAWS CLI v2と
jqがある
環境を読み込む。
export AWS_REGION=ap-northeast-1
source /etc/rawlog-s3-backup/common.env
export RAW_BUCKET="${S3_BUCKET_NAME}"
source /etc/iceberg/aws.env
export LAKE_BUCKET="${ICEBERG_BUCKET}"
export GLUE_DATABASE="${GLUE_DATABASE:-logs}"
export AWS_RAW_PROFILE=home-rawlog-backup
export AWS_ADMIN_PROFILE=home-aws-admin
aws sts get-caller-identity --profile "${AWS_ADMIN_PROFILE}"
aws s3api head-bucket --bucket "${RAW_BUCKET}" --profile "${AWS_RAW_PROFILE}"
aws glue get-tables \
--region "${AWS_REGION}" \
--database-name "${GLUE_DATABASE}" \
--query 'TableList[].Name' \
--profile "${AWS_ADMIN_PROFILE}"
tableをbatchとrealtimeへ分類する
本構成では用途ごとにtableを分離している。
syslog_eventsとauthlog_eventsはevent明細、_host_1mはFlinkが生成する1分集計である。いずれもrealtime系として当日分だけ保持する。
Rawへ7日のS3 Lifecycleを設定する
まず既存Lifecycleをbackupする。put-bucket-lifecycle-configurationはbucketの既存設定全体を置き換えるため、他のruleがある場合は必ず同じJSONへ残す。
aws s3api get-bucket-lifecycle-configuration \
--bucket "${RAW_BUCKET}" \
--profile "${AWS_ADMIN_PROFILE}" \
> raw-lifecycle-before.json
Lifecycle設定へ次の2 ruleを追加する。
{
"Rules": [
{
"ID": "expire-raw-syslog-after-7-days",
"Status": "Enabled",
"Filter": {"Prefix": "raw/syslog/"},
"Expiration": {"Days": 7},
"AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 1}
},
{
"ID": "expire-raw-authlog-after-7-days",
"Status": "Enabled",
"Filter": {"Prefix": "raw/authlog/"},
"Expiration": {"Days": 7},
"AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 1}
}
]
}
上記をraw-lifecycle.jsonとして保存し、既存ruleがある場合はそのruleもRulesへ含めて適用する。
aws s3api put-bucket-lifecycle-configuration \
--bucket "${RAW_BUCKET}" \
--lifecycle-configuration file://raw-lifecycle.json \
--profile "${AWS_ADMIN_PROFILE}"
aws s3api get-bucket-lifecycle-configuration \
--bucket "${RAW_BUCKET}" \
--profile "${AWS_ADMIN_PROFILE}" \
| jq '.Rules[] | {ID, Status, Filter, Expiration}'
Expiration.Days=7はobject作成時刻を基準にする。directory名の日付を基準に削除する設定ではない。再投入が必要なdataは、objectがexpireする前に処理する。
Iceberg maintenance jobを作成する
毎日1回実行するPySpark jobを用意する。次の順序を維持する。
- 保持期間外のrowを
DELETEする - small fileをcompactionする
- 古いsnapshotをexpireする
- orphan file候補をdry-runで確認する
- 十分に古いorphan fileだけ削除する
# iceberg_retention_maintenance.py
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from pyspark.sql import SparkSession
CATALOG = "glue_prod"
DATABASE = "logs"
BATCH_TABLES = (
"syslog_iceberg",
"authlog_iceberg",
)
REALTIME_TABLES = (
"syslog_events",
"authlog_events",
"syslog_host_1m",
"authlog_host_1m",
)
spark = SparkSession.builder.appName("iceberg-retention-maintenance").getOrCreate()
today = datetime.now(ZoneInfo("Asia/Tokyo")).date()
# 今日を含めて14 calendar daysを残す。
batch_cutoff = today - timedelta(days=13)
realtime_cutoff = today
def full_name(table):
return f"{CATALOG}.{DATABASE}.{table}"
def delete_old_rows(table, cutoff):
spark.sql(
f"DELETE FROM {full_name(table)} "
f"WHERE dt < DATE '{cutoff.isoformat()}'"
)
def maintain(table, snapshot_days, retain_last):
procedure_table = f"{DATABASE}.{table}"
# DELETE後に残ったsmall fileをまとめる。
spark.sql(f"""
CALL {CATALOG}.system.rewrite_data_files(
table => '{procedure_table}',
strategy => 'binpack',
options => map(
'target-file-size-bytes', '536870912',
'min-input-files', '5'
)
)
""").show(truncate=False)
spark.sql(f"""
CALL {CATALOG}.system.expire_snapshots(
table => '{procedure_table}',
older_than => TIMESTAMP '{(today - timedelta(days=snapshot_days)).isoformat()} 00:00:00',
retain_last => {retain_last}
)
""").show(truncate=False)
# Flinkが利用中のfileを誤削除しないよう、3日より新しいfileは対象外にする。
orphan_before = today - timedelta(days=3)
spark.sql(f"""
CALL {CATALOG}.system.remove_orphan_files(
table => '{procedure_table}',
older_than => TIMESTAMP '{orphan_before.isoformat()} 00:00:00',
dry_run => true,
stream_results => true
)
""").show(100, truncate=False)
spark.sql(f"""
CALL {CATALOG}.system.remove_orphan_files(
table => '{procedure_table}',
older_than => TIMESTAMP '{orphan_before.isoformat()} 00:00:00',
dry_run => false,
stream_results => true
)
""").show(truncate=False)
for table in BATCH_TABLES:
delete_old_rows(table, batch_cutoff)
maintain(table, snapshot_days=7, retain_last=10)
for table in REALTIME_TABLES:
delete_old_rows(table, realtime_cutoff)
# dataは当日分だけにするが、Flink復旧用snapshotは3日かつ最低100件残す。
maintain(table, snapshot_days=3, retain_last=100)
spark.stop()
「realtimeは当日」という条件はtableから参照できるrowの保持期間であり、snapshotとorphan fileを即日物理削除する指定ではない。Flinkのcheckpoint復旧に必要なsnapshotと一時fileを保護するため、snapshotは3日、orphan fileは最低3日残す。
初回は削除対象を確認する
初回実行前に件数と日付範囲を記録する。
SELECT 'syslog_iceberg', COUNT(*), MIN(dt), MAX(dt) FROM glue_prod.logs.syslog_iceberg
UNION ALL
SELECT 'authlog_iceberg', COUNT(*), MIN(dt), MAX(dt) FROM glue_prod.logs.authlog_iceberg
UNION ALL
SELECT 'syslog_events', COUNT(*), MIN(dt), MAX(dt) FROM glue_prod.logs.syslog_events
UNION ALL
SELECT 'authlog_events', COUNT(*), MIN(dt), MAX(dt) FROM glue_prod.logs.authlog_events
UNION ALL
SELECT 'syslog_host_1m', COUNT(*), MIN(dt), MAX(dt) FROM glue_prod.logs.syslog_host_1m
UNION ALL
SELECT 'authlog_host_1m', COUNT(*), MIN(dt), MAX(dt) FROM glue_prod.logs.authlog_host_1m;
本番tableへ適用する前に、job内のDELETEとdry_run => falseをcomment outして、compaction対象とorphan候補を確認する。特に初回のorphan候補に直近のFlink data fileが含まれる場合は実行しない。
実行する
既存のSpark Iceberg実行環境から起動する例を示す。
sudo -u spark /usr/local/spark/bin/spark-submit \
--conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions \
--conf spark.sql.catalog.glue_prod=org.apache.iceberg.spark.SparkCatalog \
--conf spark.sql.catalog.glue_prod.catalog-impl=org.apache.iceberg.aws.glue.GlueCatalog \
--conf spark.sql.catalog.glue_prod.io-impl=org.apache.iceberg.aws.s3.S3FileIO \
--conf "spark.sql.catalog.glue_prod.warehouse=s3://${LAKE_BUCKET}/warehouse" \
iceberg_retention_maintenance.py
Flink realtime tableへのmaintenanceは、checkpoint成功直後かつjobが安定している時間に実行する。Flink jobが再起動中、checkpoint失敗中、savepoint復旧中の場合は実行しない。
定期実行する
毎日01:30に実行する例である。日次batchやFlinkの再起動時間と重ならない時刻へ調整する。
# /etc/systemd/system/iceberg-retention-maintenance.service
[Unit]
Description=Iceberg retention and maintenance
[Service]
Type=oneshot
User=spark
EnvironmentFile=/etc/iceberg/aws.env
ExecStart=/usr/local/bin/run-iceberg-retention-maintenance
# /etc/systemd/system/iceberg-retention-maintenance.timer
[Unit]
Description=Run Iceberg retention maintenance daily
[Timer]
OnCalendar=*-*-* 01:30:00 Asia/Tokyo
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now iceberg-retention-maintenance.timer
systemctl list-timers iceberg-retention-maintenance.timer
AWSマネージドサービス移行後は、同じPySpark fileをEMR Serverlessへ配置し、EventBridge Schedulerなどから1日1回起動する。local AWS profileはjobへ渡さず、runtime roleへGlueと対象Iceberg prefixのread/write/delete権限を付与する。
実行後に確認する
保持期間を確認する。
SELECT MIN(dt), MAX(dt), COUNT(*) FROM glue_prod.logs.syslog_iceberg;
SELECT MIN(dt), MAX(dt), COUNT(*) FROM glue_prod.logs.authlog_iceberg;
SELECT MIN(dt), MAX(dt), COUNT(*) FROM glue_prod.logs.syslog_events;
SELECT MIN(dt), MAX(dt), COUNT(*) FROM glue_prod.logs.authlog_events;
SELECT MIN(dt), MAX(dt), COUNT(*) FROM glue_prod.logs.syslog_host_1m;
SELECT MIN(dt), MAX(dt), COUNT(*) FROM glue_prod.logs.authlog_host_1m;
snapshotとfile数も確認する。
SELECT committed_at, snapshot_id, operation
FROM glue_prod.logs.syslog_iceberg.snapshots
ORDER BY committed_at DESC;
SELECT COUNT(*) AS data_files,
CAST(AVG(file_size_in_bytes) AS BIGINT) AS avg_file_size
FROM glue_prod.logs.syslog_iceberg.files;
次も監視する。
- maintenance jobの成功時刻と所要時間
- DELETE、compaction、snapshot expirationで削除された件数
- Flink checkpointの成功時刻
- Kafkaコンシューマーの遅延
- Glueの最新
metadata_location - S3 storage sizeとobject数
- Athenaから見た最古/最新の
dt
rollback
S3 Lifecycleは即時実行ではないが、expire済みobjectはversioningや別backupがなければ復元できない。誤設定時は、backupしておいた既存Lifecycleを戻す。
aws s3api put-bucket-lifecycle-configuration \
--bucket "${RAW_BUCKET}" \
--lifecycle-configuration file://raw-lifecycle-before.json \
--profile "${AWS_ADMIN_PROFILE}"
IcebergのDELETEはsnapshot expiration前なら、対象snapshotを確認してrollbackできる。snapshot expiration後やorphan file削除後は復旧できない場合があるため、初回は削除procedureを無効化し、複数回の定期実行結果を確認してから有効化する。
参考資料
- S3 Lifecycle configuration
- AWS CLI put-bucket-lifecycle-configuration
- Apache Iceberg Spark procedures
関連記事
- 【導入編】HDFSログ分析基盤をAWS S3へ段階移行する全8編の構成を紹介する
- 【第5編】ログ分析基盤に残るHDFS依存をAWS S3へ移行してみる
- 【第8編】検証で作成したAWS S3・Glue・IAM環境を安全に削除してみる
- 後続の【第7回】EMR ServerlessとEventBridge SchedulerでIcebergメンテナンスを定期実行するでは、本記事のPySpark jobをAWSから定期実行する。
実機検証について
本手順は執筆時点では一連の実機検証を完了していない。最初は検証tableと短い対象期間で、件数、snapshot、Flink checkpoint、orphan候補を確認してから本番tableへ適用する。