AWS Glueのコストを30%削減した話
はじめに
この話でAWSのユーザーコミュニティであるJAWS-UG山梨支部でLT登壇してきました。
サクッと見たい方をこちらを〜
本ブログは上記をほんのちょっとだけ細かくしたものです。
背景
JINSでは数ヶ月前にETLシステムのリプレイスを実施しました。
きっかけは既存ツールのEOLで、そのツールに対して色々と課題もあったので、バージョンアップはせずにこのタイミングで別ツールに切り替えようという判断になりました。
いくつかのツールを検討しましたが、
- AWSのワークロードも多いのでAWS使いはそれなりに多い
- 非エンジニアも一緒にノーコードでETLをというよりは、エンジニア側がメインでコードベースでやりたい
等々からAWS Glueを採用しました。
対象システム
リプレイス後のAWS Glueを利用したシステムのざっくりの構成図は以下のとおりです。
ソースとなる構成としてはAuroraがいたり、EC2にインストールされているSQL Server(=RDSではない)がいたり、BigQuery、外部SaaSがいたりとAWS内外からデータを取ってきています。
リリースされてからそこまで期間が経ってなくあまり情報が多くなかったAWS Glue Salesforce Connectorを使ったり、クロスアカウントでのデータ取得にハマったりなどもありましたが、今日はコスト最適化の話なので省略。
また、既存ツールではETL(Extract → Transform → Load)で実装されていたのですが、今回AWS GlueにリプレイスしたタイミングでELT(Extract → Load → Transform)に変更しました。
そのため、基本的にはAWS Glueでは上流からデータをそのまま取得してきてSnowflake側に取り込み、Transform部分はSnowflake + dbtにお願いしています。
後段のSnowflakeやdbtの話は同僚が記事書いてるのでこっち見たりしてください。
コストが意外と高い
数カ月かけて構築を実施し、12月某日のツールのEOLタイミングでリリース実施しました。
パラパラとトラブルは発生したものの大トラブルはなく安定稼働し始めたなと安堵していたものの、コストを見ると想定していたより高い...
コストが高いところとしては、Glue、VPC、CloudWatchとなっています。
これを見たときの感想が以下です。
- Glueは本番環境で160本前後ジョブがあるので、まぁそれなりにかかるだろうと予想はしていたものの高い
- VPCもまぁNAT Gateway経由してしまう通信がそれなりにはあるけど高い
- CloudWatch高くね....
既存ELTツールはEC2上にインストールされたシステムだったため、もともとはEC2やその周辺のネットワーク、ツール自体のライセンス料の構成で使おうが使わまいが課金が発生するコストが多いものでした。
今回AWS Glueによって従量課金になったこともあり、利用料も既存ツールよりは抑えられるかと予想してたのですが・・・
コスト削減
CloudWatchカスタムメトリクスの見直し
まずはCost Explorerを用いて利用料の詳細の確認です。
この棒グラフの通り、MetricMonitorUsage
とGMD-Metric
が明らかに多い状況になっています。
個人的にはCloudWatch logs関連の利用量が多く料金も高騰しているかと想像していたので、CloudWatch Metrics側が原因になっていたは少し意外な展開でした。
これらの詳細を見ていきます。
MetricMonitorUsage
はカスタムメトリクスや詳細モニタリングなどの使用量を指しています。
ap-northeast-1での課金は下記のとおりです。
範囲 | コスト (メトリクス/月) |
---|---|
最初の10,000メトリクス | USD 0.30 |
次の240,000メトリクス | USD 0.10 |
次の750,000メトリクス | USD 0.05 |
1,000,000超え | USD 0.02 |
例えば詳細モニタリングを12メトリクスを追加で利用した場合、最初の10,000メトリクスの価格体系の中であれば3.60 USD/月といった形です。
次にGMD-Metric
はCloudWatchバルクAPIの使用量でap-northeast-1での課金は下記となります。
範囲 | コスト (メトリクス/月) |
---|---|
リクエストされた1000メトリクスあたり | USD 0.01 |
こちらもAPIコール数なので対象とするメトリクスと頻度に応じて費用が純増していきます。
調査を行った結果、これらが増えていた原因はAWS Glueのジョブメトリクスでした。
ジョブメトリクスを有効にするとCloudWatchカスタムメトリクスが生成され、それがコストに積み重なってきていました。
ジョブメトリクスとは、Glueジョブの各ExecutorやDrive nodeのリソース状況を確認できるメトリクスなります。
例えば、このメトリクスはExecutorごとのメモリ使用量を表しており、このジョブであればメモリ負荷は高すぎないからWorker Typeは比較的適切そう、Executorのメモリ使用量に偏りがありそうだから適切に並列処理ができていないかもなどが見えたりします。
こういったメトリクスが見えるため、初期構築のタイミングであったり運用中に処理のチューニング必要になったときなどは、このジョブメトリクスを見ることでGlueジョブの改善を実施できます。
一方で、本番環境で160本、ステージング環境にもジョブがある環境で全てのジョブで有効化しているとそれだけでCloudWatchメトリクスの使用料が膨大になってしまします。
そのため、今回は安定して稼働しているジョブやチューニングが完了しているジョブ等でこのジョブメトリクスを無効化する対応を実施しました。
実施方法は、マネジメントコンソールからであれば、「Job Details」のページから「Job metrics」のチェックボックスを外してあげる、
Terraformであれば、下記で無効化ができます。
resource "aws_glue_job" ”main" {
~省略~
default_arguments = {
"--enable-metrics" = "false"
}
再度チューニングなど必要になった場合は、有効化してあげれば再度ジョブメトリクスを取得できるようになります。
DPUの見直し
そもそもDPUとはAWS Glueの課金体系で、DPU(Data Processing Unit)という単位でAWSは計算リソースが提供され、その利用に対して課金が発生する仕組みになっています。
ap-northeast-1ではSpark Jobが1DPU hourあたりUSD 0.44課金され、Glueバージョンが2.0以降であれば1秒ごとに課金がされ最低請求期間は1分となっております。
例えば実際に稼働しているジョブですが、このジョブであれば4.50DPU hour稼働しているので、
4.5 x 0.44 → USD1.98
となり、1回あたり約USD1.98、日次処理であれば1ヶ月でUSD59.4(=ざっと約9,000円)になります。
2DPUで稼働し数分で終わっているジョブも多いので上記のようなジョブばかりではないのですが、DPU消費量が多いジョブが積み重なってくるとそれなりの課金額になってきます。
DPU削減のために、まずAutomatically scale the number of workers
の有効化を実施しました。
これは名前の通り、実行中のジョブのワーカー数の自動最適を行ってくれる機能になります。
上に書いたジョブはこの機能の有効化で 4.50DPU hourから2.29DPU hourまで削減されました。
Automatically scale the number of workers
の有効化はジョブ作成後から実施することができ、マネジメントコンソールからであれば「Job Details」のページから「Automatically scale the number of workers」のチェックボックスで有効化できます。
このジョブの有効化はマネジメントコンソール/AWS SDK/AWS CLIから出ないと実施ができず、Terraformを用いて構築している本環境では気づくのがだいぶ遅くなりました。
また、DPU削減のため取得データ量の見直しも行いました。
具体的には、全件取得していたデータを差分取得にすることでGlueの処理時間を減らしDPU消費利用を減らす方法です。
全件取得から差分取得に変えている分の処理は後段のdbt側に任せています。
差分取得自体は、MySQLに対しての実行であれば、下記のようにcreate_dynamic_frame
内でSampleQuery
として実行クエリ内にWHERE句を仕込んであげ、
import sys
from awsglue.transforms import *
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from awsglue.context import GlueContext
from awsglue.job import Job
from datetime import datetime, timedelta
~省略~
from_time = (now - timedelta(days=過去遡及日数)).strftime('%Y-%m-%d %H:%M:%S')
table_name = 何らかのテーブル名
connection_name = connectionの名前
~省略~
retrieve_query = f"""
Select
* ,
'{table_name}' AS table_name
, NOW() as glue_getdate
, CURRENT_TIMESTAMP as glue_leading_timestamp
from
{table_name}
WHERE update_date >= '{from_time}'
MySQL_result = glueContext.create_dynamic_frame.from_options(
connection_type. = "mysql",
connection_options = {
"useConnectionProperties": "true",
"dbtable": table_name,
"connectionName": connection_name,
"sampleQuery":retrieve_query
},
transformation_ctx = "MySQL_result"
)
~省略~
"""
Salesforceに対しての実行であれば、lowerBound
に過去遡及期間対象とする日時とタイムゾーンを定義し、それをcreate_dynamic_frame
の中で同様に定義してあげることで実施できます。
import sys
from awsglue.transforms import *
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from awsglue.context import GlueContext
from awsglue.job import Job
from datetime import datetime, timedelta
~省略~
from_time = (now - timedelta(days=過去遡及日数)).strftime('%Y-%m-%d %H:%M:%S')
now = datetime.now(ZoneInfo("Asia/Tokyo")).time()
now = time(now.hour, 0, 0).strftime("%H%M%S")
select_fields = オブジェクトに選択する列
partition_field = クエリをパーティション化するために使用するフィールド
lowerBound = 'TIMESTAMP ' + str(from_time) +' 00:00:00 Pacific/Tokyo' + '\'' +'\\\"'
entity_name = Salesforce のオブジェクトの名前
connection_name = connectionの名前
~省略~
Salesforce_result = glueContext.create_dynamic_frame.from_options(
connection_type = "salesforce",
connection_options = {
"numPartitions" : "5",
"lowerBound" : lowerBound,
"API_VERSION" : "v60.0",
"connectionName" : connection_name,
"partitionField" : partition_field,
"selectedFields" : select_fields ,
"ENTITY_NAME" : entity_name
},
transformation_ctx = "Salesforce_result"
)
~省略~
結果
下のCost Explorerの通り上記実施して30%コストを削減できました。
まとめ
AWS Glueに対していくつかのコスト削減施策を実施することで約30%コスト削減をできました。
今回はAWS Glueを利用したELTの仕組みをコスト削減の対象としましたが、これ以外でもAWSを利用しているシステムはJINS内に数多あり、そこに眠るコスト削減の余地も正直数多あるので(これが正しい状態とは思っていないです)これらに対してのコスト削減活動も順次進めていく予定です。
Discussion