AWS 기술 블로그
Apache Iceberg Table Management작업에서 발생하기 쉬운Amazon S3 이슈 분석과 해결 방안
AWS에서 Apache Iceberg 테이블을 운영하며 테이블에 대한 유지보수 작업을 Apache Spark 혹은 Amazon EMR, AWS Glue를 통해 수행하는 경우가 많습니다. 특히 쿼리 성능 및 스토리지 사용량에 대한 최적화 혹은 Snapshot 관리를 위하여 expire_snapshots, remove_orphan_files , rewrite_manifests와 같은 Iceberg table의 metadata를 관리하기 위한 Spark procedure들을 사용하게 됩니다. 실제 고객 사례에서 Table management를 위한 Procedure 사용 중 몇가지 이슈를 마주치게 되었습니다.
본 글에서는 Iceberg Table management 작업을 수행할 때 발생하는 이슈를 해소하기 위해 Amazon CloudWatch 메트릭과 S3 Server Access Logging을 활용한 문제 분석 과정과, 하나의 S3 버킷에서 다수의 Iceberg 테이블을 동시에 관리할 때 발생할 수 있는 S3 Throttling 이슈에 대한 해결 방안을 다룹니다. 특히 Hash 파티셔닝을 통한 S3 요청 분산 방식이나 날짜 기반 prefix 설계를 통해 어떻게 성능을 개선할 수 있는지를 설명합니다.
Apache Iceberg Table 작업 시 사용할 수 있는 Spark Procedures
Apache Iceberg 테이블을 사용하게 될 경우 Apache Spark를 통해 미리 저장된 Procedure들을 직접 호출하여 사용할 수 있습니다. 특히 Iceberg 테이블의 경우 다양한 Maintenance action들을 Procedure 형태로 제공하고 있습니다. 이 중 Iceberg의 table management와 관련이 있는 세가지 Procedure를 살펴보고자 합니다.
- expire_snapshots
Iceberg의 write/update/delete/upsert/compaction 작업은 항상 새로운 스냅샷을 생성합니다. expire_snapshots을 사용하면 만료된 스냅샷과 메타 데이터들이 더 이상 참조하지 않는 데이터 파일을 제거하게 됩니다.
- remove_orphan_files
remove_orphan_files Procedure는 orphaned로 간주되는 파일들을 제거하는 Procedure 입니다. Orphaned 파일은 메타데이터에서 참조되지 않는 모든 데이터 파일이나 메타데이터 파일들이며 대부분 커밋 실패, 부분적으로 완료된 작업 등에 의해 발생합니다.
- rewrite_manifests
Scan 계획을 최적화하기 위해 table의 manifest들을 다시 작성하는 procedure 입니다.
위의 세가지 Procedure는 Iceberg table metadata management 작업 시 실제로 수행되어지던 Procedure들 입니다. 워크로드에서 AWS Glue를 이용하여 위의 Spark Procedure들을 호출하는 작업 중 S3에서의 503 SlowDown 에러와 함께 빈번한 실패를 겪었으며 이러한 이슈의 원인 분석을 위해 Amazon CloudWatch 및 Server Access Logging을 통한 모니터링 방식을 구성하였습니다.
S3 측 모니터링 – Amazon CloudWatch 메트릭 및 S3 Server Access Logging 활성화
S3 Bucket에 대한 Amazon CloudWatch 메트릭을 모니터링 한 후, 주기적으로 실행되는 몇가지 AWS Glue Job의 스케줄 시간이 맞물릴 때 S3 측의 503 에러가 급증하는 것을 확인할 수 있었습니다. 또한 모니터링을 통해 503 에러가 발생함과 동시에 알 수 없는 4XX대 에러가 다수 발생하는 것을 추가로 확인할 수 있었습니다. 보다 에러의 원인들을 면밀히 파악하고자 S3 Server Access Logging을 활성화하여 경과를 지켜보았습니다.
에러의 발생 수 확인 방법
버킷에 발생하는 에러 종류 4xx/5xx의 숫자를 파악하기 위해서는 Amazon CloudWatch의 S3 Request Metrics를 사용하여야 합니다. S3 Request Metrics를 통해 4xxErrors, 5xxErrors, AllRequests, HeadRequests 등의 메트릭을 확인할 수 있습니다.
S3 Request Metrics를 통해 버킷에 4xx, 5xx에러가 다수 발생함을 확인할 수 있으며 이제 각 에러 메시지가 발생하는 위치 및 원인을 파악하기 위해서는 S3 Server Access Logging을 통한 S3 로그들을 확인하여야 합니다.
S3 Server Access Logging 활성화 방법
AWS의 다른 서비스와는 다르게 S3의 경우 객체 레벨의 operation들의 히스토리를 확인하기 위해서는 별도 활성화가 필요합니다. 객체 레벨 로깅은 다음과 같은 두 가지 방법을 이용할 수 있습니다.
- S3 Server Access Logging
- AWS CloudTrail data events logging
두 가지 로깅 방법이 기록하는 로그에는 조금 차이점이 있으나, 두 가지 로깅 방식의 차이는 S3 공식 문서에서 확인하실 수 있습니다. 본 게시글에서는 로그가 저장되는 용량만큼의 S3 스토리지 비용만 발생하는 S3 Server Access Logging을 사용하는 방법을 중점으로 작성되었습니다. S3 Server Access Logging에는 두 가지 버킷이 필요합니다.
- Source Bucket: operation이 실제로 수행되는 버킷
- Destination Bucket: 로그가 저장 될 버킷
S3 Server Access Logging을 활성화 하기 위해서는 아래와 같은 절차를 수행하여야 합니다.
- S3 콘솔에서 소스 버킷을 클릭합니다.
- 소스 버킷에서 Properties 탭을 클릭합니다.
- Server access logging 섹션을 수정하여 로깅을 활성화하고, destination 버킷명을 입력합니다.
- destination 버킷의 버킷 정책을 수정합니다.
이제 Server Access logging을 수행하기 위한 설정은 모두 완료되었습니다. 보다 자세한 설정 방법이 궁금하시다면 공식 문서를 확인하세요.
S3 Server Access Logging Query 방법
Server Access Logging는 로그가 여러 파일로 나누어 생성이 되며, 각 필드는 공백 문자를 기준으로 나누어 지게 됩니다. 이러한 특징으로 인해 로그 파일 각각을 확인하여 이슈의 원인을 파악하거나 경향성을 찾기 매우 어렵습니다. 따라서, Amazon Athena 서비스를 이용하여 쿼리를 하는 방식을 사용하였으며 실제로도 Amazon Athena를 사용하여 분석을 하는 것을 권장합니다.
특정 시간 동안 5xx가 발생한 요청을 쿼리하는 쿼리문은 아래와 같습니다.
SELECT requestdatetime, key, httpstatus, errorcode, requestid, hostid
FROM s3_access_logs_db.mybucket_logs
WHERE httpstatus like '5%'AND timestamp
BETWEEN '2024/01/29'
AND '2024/01/30'
특정 시간 동안 4xx가 발생한 요청을 쿼리하는 쿼리문은 아래와 같습니다.
SELECT requestdatetime, key, httpstatus, errorcode, requestid, hostid
FROM s3_access_logs_db.mybucket_logs
WHERE httpstatus like '4%'AND timestamp
BETWEEN '2024/01/29'
AND '2024/01/30'
4XX 에러의 원인을 우선 파악하기 위해, 위의 table management 작업을 위한 Procedure들을 시간을 달리해가며 수행해본 결과 remove_orphan_files 수행 시 테이블 내의 모든 Partition을 스캔하는 동작을 확인할 수 있었습니다.
해당 prefix, 즉 날짜 파티션 아래에 데이터들은 존재하고 있기 때문에 객체가 존재하지 않는다는 에러는 고려하지 않아도 무방하지만 여러 부서의 고객분들 께서 협업하며 검토하실 때에는 이런 사항들이 에러로 간주될 수 있기에 이러한 에러가 발생하지 않도록 설정하는 방법에 대한 요구가 있었습니다.
객체 스토리지를 고려하지 않은 타 Application에서 S3에 객체가 존재하는지 여부는 prefix가 아닌 (기존 스토리지의 folder) 객체에 직접하여야하는데, Iceberg Procedure들이 기존 블록/파일 스토리지를 고려하여 설계되었기에 이러한 차이가 있는 것으로 확인됩니다. 이러한 경우에는 따로 ‘/’로 끝나는 객체를 수동으로 생성해준다면 HeadObject 호출에 대하여 200 코드를 리턴받을 수 있습니다. 다만, 객체 수동 생성시 주의해야 할 부분은 바로 S3 요청 비용입니다. S3는 지난 2024년 5월 13일 부터 4xx에러가 발생한 요청들에 대해 비용이 발생하지 않고 있습니다. 정리하자면 200 OK를 받는 요청에는 과금이 되지만, 404 Not Found를 받는 요청은 과금이 되지 않기에 운영 비용이 청구되지 않는 404 Not Found가 발생하는 것이 오히려 비용적으로 유리합니다.
다음으로는 S3 Slowdown Throttling (503)관련 에러에 대한 원인을 분석한 내용과 이를 해소하는 방안입니다. 고객의 경우 하나의 S3 버킷에 다수의 Iceberg 테이블을 생성하여 작업 중에 있었으며, AWS Glue Job에서는 S3 Bucket 내의 테이블들에 대한 table management 작업을 병렬로 수행하고 있었습니다.
각 테이블들은 yyyy-mm-dd 형식으로 된 파티션을 갖고 있었으며 과거의 날짜부터의 데이터가 쌓이고 있었습니다.
s3://bucket-name/
├── table1/
│ ├── metadata/
│ │ ├── snap-123456789.avro
│ │ ├── manifest-list.avro
│ │ └── manifests/
│ └── data/
│ ├── 2024-04-07/
│ │ ├── file1.parquet
│ │ └── file2.parquet
│ └── 2024-04-06/
│ ├── file3.parquet
│ └── file4.parquet
├── table2/
│ ├── metadata/
│ └── data/
│ ├── 2024-04-07/
│ └── 2024-04-06/
├── table3/
│ ├── metadata/
│ └── data/
│ ├── 2024-04-07/
│ └── 2024-04-06/
└── table4/
├── metadata/
└── data/
├── 2024-04-07/
└── 2024-04-06/
S3는 파티션화 된 prefix 당 3,500 PUT/COPY/POST/DELETE or 5,500 GET/HEAD requests per second를 지원하고 있습니다. S3 요청량과 관련된 자세한 내용은 공식 문서를 참고해주세요.
위의 구조처럼 하나의 S3 버킷에 다수의 Iceberg Table이 존재하고 Table들에 대한 작업을 동시에 수행할 경우 위에서 소개한 전체 파티션 스캔 등의 작업과 같이 여러 S3 API가 동시에 호출되어 위에 언급드린 파티션화 된 prefix 당 요청 횟수가 넘어 503 slowdown Throttling 이슈를 겪을 수 있습니다.
자동 조정을 통한 S3 측 파티션화가 일어나도록 하기 위해서는 지속적인 요청을 계속해서 S3측으로 보내야하기 때문에 번거로운 점이 있습니다. 따라서, S3 버킷 내에 여러 Iceberg Table에 대해 동시에 작업이 일어날 경우 Throttling 완화를 위하여 prefix를 나누어서 요청을 분산 시키는 것을 권고드리고 있습니다.
요청을 분산시키기 위한 prefix 설계는 아래와 같이 hash 값을 넣어 수행할 수 있습니다.
as-is
service_log.2012-02-27.hostname1.mydomain.com
service_log.2012-02-27.hostname2.mydomain.com
service_log.2012-02-27.hostname3.mydomain.com
service_log.2012-02-27.hostname4.mydomain.com
service_log.2012-02-27.john.myotherdomain.com
service_log.2012-02-27.paul.myotherdomain.com
service_log.2012-02-27.george.myotherdomain.com
service_log.2012-02-27.ringo.myotherdomain.com
service_log.2012-02-27.pete.myotherdomain.com
to-be
c/service_log.2012-02-27.com.mydomain.hostname1
4/service_log.2012-02-27.com.mydomain.hostname2
9/service_log.2012-02-27.com.mydomain.hostname3
2/service_log.2012-02-27.com.mydomain.hostname4
b/service_log.2012-02-27.com.myotherdomain.john
7/service_log.2012-02-27.com.myotherdomain.paul
2/service_log.2012-02-27.com.myotherdomain.george
0/service_log.2012-02-27.com.myotherdomain.ringo
d/service_log.2012-02-27.com.myotherdomain.pete
Apache Iceberg와 같은 경우 Table 생성 시 write.object-storage.enabled = True 옵션을 추가하여 요청을 분산할 수 있습니다. Amazon Athena를 사용하여 Iceberg Table을 생성한다면 이러한 Hash 파티셔닝이 자동으로 적용되며, AWS Glue와 같이 Spark를 사용할 경우 수동으로 해당 옵션을 추가해야 합니다.
<Hash Partition Example>
s3://iceberg-bucket/table_name/data/_6aaRw/date=2014-11-21/
s3://iceberg-bucket/table_name/data/_AoM0Q/date=2014-11-22/
이렇게 Hash 값을 이용한 파티셔닝을 적용한다면 요청을 여러 prefix로 분산시킬 수 있습니다. 다만, 난수의 hash 값을 추가하는 것은 운영 측면에서 관리하기 어려운 부분이 있습니다. 특히 객체가 어느 prefix 아래에 존재하는지 전부 관리하기 어려울 수 있습니다. 이런 경우에는 날짜마다 쌓이는 데이터의 특성(yyyy-mm-dd)을 이용하여 아래 예시와 같이 prefix를 분리해 줄 수 있습니다.
as-is
service_log.2012-02-27.hostname1.mydomain.com
service_log.2012-02-27.hostname2.mydomain.com
service_log.2012-02-28.hostname1.mydomain.com
service_log.2012-02-28.hostname2.mydomain.com
service_log.2012-02-29.hostname1.mydomain.com
service_log.2012-02-29.hostname2.mydomain.com
service_log.2012-02-30.hostname1.mydomain.com
service_log.2012-02-30.hostname2.mydomain.com
to-be
service_log.02-27-2012.hostname1.mydomain.com
service_log.02-27-2012.hostname2.mydomain.com
service_log.02-28-2012.hostname1.mydomain.com
service_log.02-28-2012.hostname2.mydomain.com
service_log.02-29-2012.hostname1.mydomain.com
service_log.02-29-2012.hostname2.mydomain.com
service_log.02-30-2012.hostname1.mydomain.com
service_log.02-30-2012.hostname2.mydomain.com
2월달의 모든 로그를 쿼리한다고 가정하였을 때 위의 경우 2012-02 까지 모두 동일한 prefix에 요청이 진행되게 되지만, to-be와 같이 변경될 경우 dd로 설정된 날짜의 부분에서 위의 prefix가 분산이 되는 것을 확인할 수 있습니다. 이를 통해 앞서 언급한 hash 값을 추가하는 것과 유사한 효과를 발생시킬 수 있습니다.
마무리
이렇게 Iceberg 작업 시 S3에서 발생할 수 있는 이슈들에 대하여 트러블슈팅 및 분석하는 과정을 알아보았습니다. 특정 Procedure 사용 시 4XX 에러가 발생할 수 있으나 이는 에러로 간주하지 않아도 괜찮습니다. 또한 하나의 S3 버킷에 여러 Iceberg Table들이 저장되어있고 테이블들에 대한 작업들이 동시에 일어난다면 503 slowdown Throttling 이슈을 초래할 수 있으며 이에 대한 해결 방안으로 Hash 파티셔닝 이용을 통한 요청 분산 혹은 하나의 S3 버킷에 있는 여러 테이블들에 대한 작업의 동시성을 줄이는 방안을 활용할 수 있습니다.