Debug on S3 event crawler
s3イベントクローラ
$ aws glue get-crawler --name json_db
{
"Crawler": {
"Name": "json_db",
"Role": "glue-role",
"Targets": {
"S3Targets": [
{
"Path": "s3://[[my-bucket]]/json_db",
"Exclusions": [],
"EventQueueArn": "arn:aws:sqs:us-west-2:[[account_id]]:crawler-sqs"
}
],
"JdbcTargets": [],
"MongoDBTargets": [],
"DynamoDBTargets": [],
"CatalogTargets": []
},
"DatabaseName": "json_db",
"Classifiers": [],
"RecrawlPolicy": {
"RecrawlBehavior": "CRAWL_EVENT_MODE"
},
"SchemaChangePolicy": {
"UpdateBehavior": "UPDATE_IN_DATABASE",
"DeleteBehavior": "DEPRECATE_IN_DATABASE"
},
"LineageConfiguration": {
"CrawlerLineageSettings": "DISABLE"
},
"State": "READY",
"CrawlElapsedTime": 0,
"CreationTime": "2022-01-08T01:15:17+09:00",
"LastUpdated": "2022-04-13T03:17:23+09:00",
"LastCrawl": {
"Status": "SUCCEEDED",
"LogGroup": "/aws-glue/crawlers",
"LogStream": "json_db",
"MessagePrefix": "9424b57f-6c7c-48d4-8255-1ab7811a099d",
"StartTime": "2022-04-13T23:02:32+09:00"
},
"Version": 11,
"Configuration": "{\"Version\":1.0,\"Grouping\":{\"TableLevelConfiguration\":3}}"
}
}
table_level: 3 は s3://[my-bucket]/[db-name]/[table-name]/[partition]と判断させるための設定。s3側には json_db/のイベントをSQSに投げるように仕込む。
なお、ログを読めばわかるが、s3イベントクローラ作りたての1回目は、通常と同じようにSQS無視して全ファイルを舐めるクローラとして動く。この挙動を無視するために、初回は空回したほうがいい。
俺用語
意味を限定させる。どっちのことか一発でわかるように
代表スキーマ: カタログ上のスキーマのこと、テーブル全体としてこのスキーマという意味
パーティションスキーマ: パーティションごとのスキーマ
データ準備
parquetのみで発生を確認、jsonの場合は問題が発生しない。parquetを作るために一旦 jsonファイルをS3において、クローラ回して適当にテーブル作る
json_db/
├── json_a
│ ├── dt=2022-01-01
│ │ └── data.json
│ └── dt=2022-01-02
│ └── data.json
└── json_b
├── dt=2022-01-01
│ └── data.json
└── dt=2022-01-02
└── data.json
data.jsonの中身は全部一緒でもよい
{"id": 1, "name": "hoge", "user_id": 2147483650}
{"id": 2, "name": "fuga", "user_id": 2147483651}
athenaで json_a というテーブルが見えれる。
ctasでparquetを作る
unload( SELECT id, name, user_id
FROM
"json_db"."json_a" where dt = '2022-01-01')
TO 's3://[[my-bucket]]/json_db/parquet_b/dt=2022-01-01'
WITH (format = 'PARQUET',compression = 'SNAPPY');
unload( SELECT id, name, user_id
FROM
"json_db"."json_a" where dt = '2022-01-01')
TO 's3://[[my-bucket]]/json_db/parquet_b/dt=2022-01-02'
WITH (format = 'PARQUET',compression = 'SNAPPY');
unload( SELECT id, name, user_id
FROM
"json_db"."json_a" where dt = '2022-01-01')
TO 's3://[[my-bucket]]/json_db/parquet_b/dt=2022-01-03'
WITH (format = 'PARQUET',compression = 'SNAPPY');
3パーティションぐらいある状態にしないと、クローラ回したときにちゃんとパーティションとしてみなされないことが多い、3で足りないときは5パーティションぐらい足してクローラ回す。
クローラ回すと parquet_b
テーブルができる、この状態は user_id (bigint)
で代表スキーマ=パーティションスキーマの状態で全く問題無い状態
decimal型のパーティションを追加する
unload( SELECT id, name,
cast(user_id as decimal(20,0)) as user_id
FROM
"json_db"."json_a" where dt = '2022-01-01')
TO 's3://[[my-bucket]]/json_db/parquet_b/dt=2022-01-04'
WITH (format = 'PARQUET',compression = 'SNAPPY')
dt=2022-01-04
は user_id(decimal)
で作製される この状態でクローラ回すと
% aws glue get-table --database-name=json_db --name parquet_b
{
"Table": {
"Name": "parquet_b",
"DatabaseName": "json_db",
"Owner": "owner",
"CreateTime": "2022-04-13T11:23:58+09:00",
"UpdateTime": "2022-04-13T11:23:58+09:00",
"LastAccessTime": "2022-04-13T11:23:58+09:00",
"Retention": 0,
"StorageDescriptor": {
"Columns": [
{
"Name": "id",
"Type": "int"
},
{
"Name": "name",
"Type": "string"
},
{
"Name": "user_id",
"Type": "bigint"
}
],
...
% aws glue get-partition --database-name=json_db --table-name=parquet_b --partition-values=2022-01-04
{
"Partition": {
"Values": [
"2022-01-04"
],
"DatabaseName": "json_db",
"TableName": "parquet_b",
"CreationTime": "2022-04-13T23:05:09+09:00",
"LastAccessTime": "2022-04-13T23:05:08+09:00",
"StorageDescriptor": {
"Columns": [
{
"Name": "id",
"Type": "int"
},
{
"Name": "name",
"Type": "string"
},
{
"Name": "user_id",
"Type": "decimal(20,0)"
}
],
ここまでは良い、問題はここから。
最終的に user_id
は decimal(20,0) にしたい。 bigint <=> decimalで互換性は無いので、 dt=2022=01-01 - 03 までのデータを decimal(20,0) に変換する、それからクローラを回せば、代表スキーマ・パーティションスキーマすべて decimalになることを期待する
リカバリ方法模索
データを出し直してクローラを回す
すでに dt=2022-01-01 - 03 まで bigintで入っているので、S3上のデータを消して、上記CTASで decimal(20,0) で出し直す。
出し直した状態でクローラ回すと parquet_b
は廃止フラグがたち、各パーティションごとに1テーブルが出来てしまう。こうなってしまうと、もうS3イベントクローラを維持したままテーブルを修復することが出来ない。
パーティション数が1500ぐらいある場合は、テーブルの分割問題が発生しないこともあるが、小規模環境では再現しなかった。
1500パーティションでスキーマズレが発生した => S3上のファイルを全部スキーマ修正した => S3イベントクローラのママで回す => 代表スキーマは正常になったが、1500パーティション中 30%程度のパーティションスキーマが更新されない => Athenaでスキーマ更新されなかったパーティションを触るとERROR
代表スキーマを手で書き換える
s3イベントクローラなので、新しく追加されたS3オブジェクトしかクロールしないはずだと考え、DDL文などで代表スキーマをいじってしまう(decimalを指定)と良さそうだが、クローラが回ったらbigintに戻ってしまう。
パーティションスキーマも手で書き換える
これもクローラが回ると、S3イベントが存在しないはずなのにそのパーティションのスキーマが書き換わる=現状のS3上のParquetスキーマを無視して、昔のパーティションスキーマに固執する。パーティションを削除した場合も同じ。
一度テーブルをカタログから削除してクローラ回す
カタログから消し、S3上のParquetはそのまま。SQSにイベントが入っていないとクローラは一切回らないので、新規パーティションのデータをS3に入れる(一日分だけ)。この状態で回すと、一日分だけのパーティションでテーブルが作られそうだが、そうではなく、過去のスキーマそのままでテーブルがつくられる。ユーザーが把握する領域には潰したテーブルのカタログ情報はないはずなのに。
これによりおそらく、クローラは独自にストレージ領域を持っており、たとえ消されたテーブルやパーティションであってもその情報を保持していると推測できる。一連のおかしな挙動はこれが原因と思う。
s3イベントクローラやめる
残念ながらこれしか無い。S3 ITストレージでアクセス発生していないファイルの自動コストダウンが狙いでs3イベントクローラやってるのに、使う意味なくなる。
Discussion