株式会社ZOZOのブログ - TECH PLAY

TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

1025

こんにちは、SRE部MA基盤チームの谷口( case-k )です。私達のチームでは、データ連携基盤の開発・運用をしています。 データ基盤には大きく分けて2種類あり、日次でデータ連携してるものとリアルタイムにデータ連携しているものがあります。本記事ではリアルタイムデータ連携基盤についてご紹介します。 既存のデータ連携基盤の紹介 リアルタイムデータ連携基盤の紹介 なぜ必要なのか 活用事例の紹介 データ連携の仕組みと課題  リプレイス後のリアルタイムデータ連携基盤 SQL Serverの差分データの取り方を検討 アーキテクチャ概要と処理の流れ Fluentdのプラグインを使った差分データの取得 Dataflowでメッセージの重複を排除 Dataflowで動的にBigQueryの各テーブルに出力 Pub/Subのメッセージ管理 イベントログ収集基盤 個人情報の取り扱い ビルド・デプロイ戦略 監視 データの欠損 データの遅延 性能評価 まとめ 既存のデータ連携基盤の紹介 まず既存のデータ連携基盤について簡単にご紹介させていただきます。 既存のデータ連携基盤ではオンプレ環境やクラウドにあるデータをBigQueryへ日次で1回連携しています。ETLツールはSQL Server専用の「bcp」とTreasure Dataが開発しているOSSである「Embulk」を使っています。まずbcpを使い、オンプレ環境の基幹データベース内のテーブルを中間データベースへ連携します。中間データベースへ連携されたデータはEmbulkを使って、BigQueryへ連携されます。この際秘密情報のハッシュ化なども行っています。 これらの処理はワークフローエンジンで制御されていてTreasure Dataが開発しているOSSであるDigdagを使っています。余談ですがZOZOテクノロジーズにはDigdagのコントリビューターが7人もいます! リアルタイムデータ連携基盤の紹介 なぜ必要なのか これまでZOZOテクノロジーズでは日次でBigQueryへデータを連携していました。最近では機械学習を使った案件も増えてきており、リアルタイムなデータを必要とするサービスが増えてきています。機械学習の他にも配信基盤では商品が残り1点になったタイミングで通知を行う仕組みがあります。このような要件に対応するためには商品の在庫状況をリアルタイムで連携する必要があります。その他にも、施策のモニタリングや不正利用を素早く検知したいなど様々な案件があり、ZOZOテクノロジーズでもリアルタイムデータ連携基盤を構築することになりました。 活用事例の紹介 リアルタイムデータ連携基盤は検索パーソナライズ基盤の商品在庫の連携で使われています。検索パーソナライズとはユーザごとに商品をおすすめ順で紹介する機能となります。商品在庫がないとユーザに対してレコメンドしたにもかかわらず、商品が既に売り切れているといった機会損失が起きてしまいます。このような要件から検索パーソナライズ基盤ではリアルタイムに商品の在庫状況を知る必要があります。 データ連携の仕組みと課題 リアルタイムデータ連携基盤はオンプレ環境からGCP環境まで多段にレプリケーションを行いデータ連携をしています。SQL ServerからKafkaへの差分連携にはQlik Replicateを採用してます。 Qlik ReplicateはSQL ServerからCDCを取得し、解析可能なメッセージの形へ変換する役割をになっています。CDCとはChange Data Captureの略で、データベース内で行われた変更履歴を追うことができる機能です。オンプレ環境のSQL Serverはバージョンが古くCDCを使うことができないため、Compute Engine上にSQL Serverを立てて差分データを取得しています。 https://www.qlik.com/us/attunity www.qlik.com techblog.zozo.com 多段にレプリケーションを行うことで高頻度連携を実現しようとしましたが、運用する過程でデータの欠損や遅延が課題としてあがりました。 データの遅延 オンプレ環境からBigQueryまでに多段にレプリケーションを行うことで10分から30分程度の遅延が発生していました。 データの欠損 既存の処理系にメモリリークがあり、定期的な再起動によるデータの欠損も発生していました。 コスト インフラ費用も月額で約200万円程度かかっており汎用的な基盤として使うには課題がありました。  リプレイス後のリアルタイムデータ連携基盤 既存のリアルタイムデータ連携基盤の課題を解決するため、私たちのチームでリプレイスをすることになりました。 SQL Serverの差分データの取り方を検討 新規に作成するリアルタイムデータ連携基盤では、差分の取得にSQL ServerのChange Trackingを採用しました。 Change TrackingとはCDCのようにSQL Serverの差分データを取得する仕組みです。CDCとの違いはCDCが非同期的な連携であるのに対しChange Trackingは同期的に連携します。CDCよりもリアルタイム性をもって連携できる一方で、変更履歴などは取得はできません。取得できるのは削除や更新、追加といった更新処理の内容や更新バージョン、それと更新のあった主キーのみです。 具体的には、次のようにして差分データの最新の状態を取得しています。 SELECT a.SYS_CHANGE_OPERATION as changetrack_type, a.SYS_CHANGE_VERSION as changetrack_ver, #{columns} FROM CHANGETABLE(CHANGES #{@tablename}, @前回更新したバージョン) AS a LEFT OUTER JOIN #{@tablename} ON a.#{@primary_key} = b.#{@primary_key} 差分データの取得方法としてはChange Tracking以外にも、テーブルの更新タイムスタンプを参照する方法やCDCを使う方法も検討しました。 しかし、更新タイムスタンプは付与されているテーブルが非常に少なく、付与されていても更新されないタイムスタンプが多くありました。CDCもオンプレ環境にCDCが使える2016以降のSQL Serverがほとんどありませんでした。また、非同期的なCDCより同期的なChange Trackingの方が高速にデータを取得できます。 このような理由からChange Trackingを使って差分データを取得することになりました。 アーキテクチャ概要と処理の流れ ここからはリプレイス後のリアルタイムデータ連携基盤のアーキテクチャ概要と処理の流れについてご紹介します。 アーキテクチャの全体図は次の通りです。 Fluentdのプラグインを使った差分データの取得 Change Trackingの実行からPub/Subへのメッセージ転送はFluentdのプラグインを使っています。 冗長構成を実現するため、Compute Engine 2台にプラグインをデプロイしています。片方のインスタンスに問題が起きても、もう片方が生きていればデータの欠損が起きない仕組みとなっています。 オンプレ環境からGCP環境へ高速にデータ連携できるよう専用線としてDedicated Interconnectを使っています。多段にレプリケーションを行ったことによる遅延が課題だったので、最も根元の基幹データベースからデータを取得するようにしています。取得したデータはPub/Subのアウトプットプラグインを使い転送されます。 cloud.google.com プラグインでは次のようにChange Tracking実行時にレコード単位でユニークとなるメッセージIDを生成しています。生成されたメッセージIDはDataflowのメッセージの重複排除で使います。 BigQueryで主キーの最新の状態を集計できるようChange Trackingのバージョンも渡しています。Dataflowでテーブル名を考慮してBigQueryへ書き込みができるようテーブル名も渡しています。 query = """ declare @last_synchronization_version bigint; SET @last_synchronization_version = #{ changetrack_ver } ; SET lock_timeout #{ @lock_timeout } SELECT CONCAT(' #{ @tablename } ','-',a. #{ @primary_key .join( ' , ' ).gsub( ' , ' , ' ,a. ' ) } ,a.SYS_CHANGE_VERSION) as massage_unique_id, ' #{ @tablename } ' as table_name, ' #{ @changetrack_interval } ' as changetrack_interval, ' #{ Time .now.utc } ' as changetrack_start_time, a.SYS_CHANGE_OPERATION as changetrack_type, a.SYS_CHANGE_VERSION as changetrack_ver, #{ columns } FROM CHANGETABLE(CHANGES #{ @tablename } , @last_synchronization_version) AS a LEFT OUTER JOIN #{ @tablename } ON a. #{ @primary_key } = b. #{ @primary_key } """ Pub/Subのアウトプットプラグインでは差分データに加えてattributeにDataflowの重複排除で使うメッセージIDを渡しています。Dataflowの重複排除は次章でご紹介します。 <system> workers ' <worker count> ' < / system> <worker 1> <source> / / Input Plugin < / source> <match '<tag_name>' @type gcloud_pubsub project " #{ ENV [ ' PROJECT_ID ' ] } " key /us r/src/app/config/gcp_credential.json topic " projects/ #{ ENV [ ' PROJECT_ID ' ] } /topics/<topic-name> " autocreate_topic false max_messages 1000 max_total_size 9800000 max_message_size 4000000 attribute_keys [ " message_unique_id " ] // Buffer Plugin < / match> < / worker> github.com docs.fluentd.org Dataflowでメッセージの重複を排除 Fluentd2台の冗長構成によるデータの重複はDataflowのidAttributeを使い重複を排除しています。idAttributeを使うことで、プラグインで付与したメッセージIDを参照して10分以内であれば同じメッセージの重複を排除します。Dataflowを使うとPub/Subで自動的に付与されるメッセージIDの重複は自動で排除でき、at least onceを採用しているPub/Subとは相性の良いツールです。しかし、パブリッシャーが複数回同じメッセージを送った場合、Pub/Subでは異なるメッセージと扱われるため重複の自動排除はできません。このような理由からメッセージのユニーク性を担保したい場合はDatafflowでidAttributeを使います。idAttributeを使うことで2台のFluentdから送られてくるデータの重複を排除しています。 次のサンプルはidAttributeを使ってメッセージの重複排除をする例です。 public static PipelineResult run(Options options) { // Create the pipeline Pipeline pipeline = Pipeline.create(options); pipeline .apply( "Read PubSub Events" , PubsubIO.readMessagesWithAttributes() .withIdAttribute( "message_unique_id" ) .fromSubscription(options.getInputSubscription())) .apply( "Filter Events If Enabled" , ParDo.of( ExtractAndFilterEventsFn.newBuilder() .withFilterKey(options.getFilterKey()) .withFilterValue(options.getFilterValue()) .build())) .apply( "Write PubSub Events" , PubsubIO.writeMessages().to(options.getOutputTopic())); return pipeline.run(); } cloud.google.com cloud.google.com Dataflowで動的にBigQueryの各テーブルに出力 Pub/Subに送られ重複排除されたメッセージはDataflowを使ってBigQueryのテーブルに書き込まれます。DataflowのDynamic Destinationsを使うとメッセージ内のテーブル名に基づいて、出力先のテーブルを動的に振り分けることが可能です。そのため、Dynamic Destinationsを使うことで、1つのDataflowで複数テーブルのデータ連携ができるようになりインフラコストを抑えることができます。 なお、DataflowのDynamic Destinations機能は現時点だとJavaのみサポートしてます。 次のサンプルはDynamic Destinationsを使ってストリーム内のテーブル名を参照してBigQueryのテーブルに書き込む例です。監視や分析用の遅延時間を計測するため、BigQueryへのインサート時刻も取得しています。 WriteResult writeResult = convertedTableRows.get(TRANSFORM_OUT) .apply( BigQueryIO.<TableRow>write() .to( new DynamicDestinations<TableRow, String>() { @Override public String getDestination(ValueInSingleWindow<TableRow> elem) { return elem.getValue().get( "table_name" ).toString(); } @Override public TableDestination getTable(String destination) { return new TableDestination( new TableReference() .setProjectId( "project_id" ) .setDatasetId( "dataset_name" ) .setTableId( "table_prefix" + "_" + destination), // destination: table name "destination table" + destination); } @Override public TableSchema getSchema(String destination) { TableSchema schema = new TableSchema() switch (destination) { case "table_a" : schema.setFields(ImmutableList.of( new TableFieldSchema().setName( "column" ).setType( "STRING" ).setMode( "NULLABLE" ))); break ; case "table_b" : schema.setFields(ImmutableList.of( new TableFieldSchema().setName( "column" ).setType( "STRING" ).setMode( "NULLABLE" ))); break ; default : } return schema } }) // BigQuery Insert Time .withFormatFunction((TableRow elem) -> elem.set( "bigquery_insert_time" , Instant.now().toString())) .withoutValidation() .withCreateDisposition(CreateDisposition.CREATE_NEVER) .withWriteDisposition(WriteDisposition.WRITE_APPEND) .withExtendedErrorInfo() .withMethod(BigQueryIO.Write.Method.STREAMING_INSERTS) .withFailedInsertRetryPolicy(InsertRetryPolicy.retryTransientErrors())); www.case-k.jp beam.apache.org Pub/Subのメッセージ管理 重複排除されてPub/Subに送られてきたメッセージは別のサブスクライバーからも参照できるよう7日間メッセージを保持しています。新しくサブスクライバーを作ればBigtableなどBigQuery以外にも出力できるようになっており、Dataflowのウィンドウ処理等でリアルタイムに特徴量生成などもできるようになっています。 次のサンプルはPub/Subでメッセージを7日間保持するTerraformの例となります。retain_acked_messagesをtrueとすることでサブスクライブされたメッセージを破棄せずに保持します。 message_retention_durationはメッセージの保有期間を決めることができます。なお、メッセージの保有期間は最大で7日です。 resource "google_pubsub_subscription" "message_hub" { name = "message_hub" topic = google_pubsub_topic.message_hub.name # subscribe from multiple subscriber message_retention_duration = "604800s" retain_acked_messages = true ack_deadline_seconds = 60 } cloud.google.com beam.apache.org イベントログ収集基盤 まだ構想段階ではありますが、データ量の多いイベントログのリアルタイムデータ連携基盤も作ろうとしています。イベントログについてもPub/Subに投げてもらうのが理想的ですが、クライアント側の負担も考慮し現在検討中です。 個人情報の取り扱い BigQueryとPub/Subに保持される個人情報や秘密情報はアクセスできるユーザを制限しています。 BigQueryではカラムレベルでのアクセス制御を行い、Pub/Subはトピック単位でアクセス制御をしています。カラムレベルのアクセス制御を行うため、ポリシータグを個人情報や秘密情報のカラムに付与しています。ポリシータグとはBigQueryのテーブルに対してカラムレベルのアクセス制御を行うリソースです。 ポリシータグのカラム付与はTerraformで次のようにしてできます。ポリシータグ自体を作ることはまだTerraformではサポートされていないようです。 resource " google_bigquery_table " " table-name " { dataset_id = google_bigquery_dataset. < dataset - name > .dataset_id table_id = " <table-name> " schema = << EOF [ { " name " : " column-name> ", " type " : " STRING ", " mode " : " NULLABLE ", " policyTags " : { " names " : [ " projects/<project-id>/locations/<location>/taxonomies/<taxonomies-id>/policyTags/<policy-tag-id> " ] } } ] EOF } cloud.google.com github.com Pub/SubはDataflowで個人情報や秘密情報をNULL置換したトピックを作ろうと考えています。トピック単位で参照ユーザを制限することで、秘密情報を必要としないサブスクライバーからは参照できないようにします。 cloud.google.com ビルド・デプロイ戦略 FluentdのプラグインとDataflowのビルド・デプロイ方法についてご紹介できればと思います。CI/CDツールとしてはCircleCIを使っています。Fluentdのプラグインはコンテナイメージを作り、作られたコンテナイメージをContainer RegistryにPUSHしています。Container RegistryのコンテナイメージはCompute Engine起動時にPULLされデプロイされます。データ欠損や遅延が発生しないよう2台のCompute Engineを1台ずつ再起動させ無停止でデプロイできるようにしています。 module " gce-container " { source = " Terraform-google-modules/container-vm/google " version = " ~> 2.0 " container = { image = " gcr.io/${var.project}/<image-name> " tty : true } restart_policy = " Always " } resource " google_compute_instance " " compute engine " { name = " name " machine_type = " n2-custom-4-10240 " zone = " asia-northeast1-a " boot_disk { initialize_params { image = module.gce - container.source_image size = 500 } } metadata_startup_script = " #!/bin/bash /usr/bin/docker-credential-gcr configure-docker EOF " metadata = { gce - container - declaration = module.gce - container.metadata_value google - logging - enabled = " true " google - monitoring - enabled = " true " } service_account { email = " ${google_service_account.tracker_app.email} " scopes = [ " https://www.googleapis.com/auth/cloud-platform ", ] } } Dataflowはカスタムテンプレートをビルドし、既存のパイプラインの更新を行います。 DataflowのカスタムテンプレートはGoogle提供のテンプレートをベースにカスタマイズしています。ビルド時にenableStreamingEngineオプションを利用すると使用するディスク容量を420GBから30GBにインフラ費用を抑えることができます。 次のコードはテンプレートをビルドする際にenableStreamingEngineオプションを指定する例です。 mvn - Pdataflow - runner compile exec : java \ - Dexec.mainClass = com.google.cloud.teleport.templates.PubsubToPubsub \ - Dexec.args = " --project= ${project_id} \ --tempLocation=gs:// ${project_id} /tmp \ --templateLocation=gs:// ${project_id} /templates/<template-name> \ --experiments=enable_stackdriver_agent_metrics \ --enableStreamingEngine \ --runner=DataflowRunner " cloud.google.com cloud.google.com github.com ビルドされたテンプレートはupdateオプションを使い既存のパイプラインの更新を行っています。互換性チェックにより、中間状態やバッファデータなどが前のジョブから置換ジョブに確実に転送することが可能です。 次のコードはPythonクライアントを使ってパイプラインを更新する例です。 def create_template_request (self, job_name, template_path, parameters, environment, update_options): request = self.dataflow.projects().templates().launch( projectId = self.project_id, location = 'us-central1' , gcsPath = template_path, body = { "jobName" : job_name, "parameters" : parameters, "environment" : environment, "update" : update_options } ) return request def deploy_dynamic_destinations_datatransfer (self, active_jobs): job_name= 'dynamic_destinations_datatransfer' template_name = 'PubSubToBigQueryDynamicDestinationsTemplate' template_path = "gs://{}/templates/{}" .format(self.project_id, template_name) input_subscription = 'message_hub' output_default_table = 'streaming_datatransfer.streaming_dynamic_changetracktransfer' parameters = { "inputSubscription" : "projects/{}/subscriptions/{}" .format(self.project_id, input_subscription), "outputTableSpec" : "{}:{}" .format(self.project_id, output_default_table), "autoscalingAlgorithm" : "THROUGHPUT_BASED" } environment = { "machineType" : 'n2-standard-2' , "maxWorkers" : 5 } update_options= 'false' if 'dynamic_destinations_datatransfer' in active_jobs: update_options= 'true' request = self.create_template_request(job_name, template_path, parameters, environment, update_options) request.execute() cloud.google.com 監視 監視対象としてはデータの欠損や遅延が発生してないかCloud LoggingやMonitoring、Redashを使い監視しています。 データの欠損 データの欠損はリトライログとメモリの使用率を確認してます。リトライの上限を超えるとデータが欠損してしまうのと、メモリの使用率が100%に達すると基幹データベースへ接続ができなくなるからです。 次のようにしてFluentdのプラグイン側でリトライ時にログを出力しています。 def execute_changetracking (changetrack_ver) try = 0 begin try += 1 query = generate_query(changetrack_ver) changetrack_results = execute_query(query) if !changetrack_results.nil? changetrack_results.each_slice( @batch_size ) { |rows| es = MultiEventStream .new rows.each do |r| r[ " changetrack_end_time " ] = Time .now.utc es.add( Fluent :: Engine .now, r) if changetrack_ver < r[ " changetrack_ver " ] then changetrack_ver = r[ " changetrack_ver " ] end end router.emit_stream( @output_tag , es) } update_changetrack_version(changetrack_ver) end rescue => e puts " Write Retry Cnt: #{ try } , Table Name: #{ @tablename } , Error Message: #{ e }" sleep try** 2 retry if try < @retry_max_times raise end end リトライ時のログはCompute Engine起動時にデプロイしたCloud Loggingエージェントでログを取得しています。 metadata = { gce - container - declaration = module.gce - container.metadata_value google - logging - enabled = " true " google - monitoring - enabled = " true " } 次のようにしてCloud Loggingでメトリクスを作ります。今回リトライ回数が10回でアラートを通知するように設定しました。 resource " google_logging_metric " " retry_error_tracker_a_metric " { name = " retry-error-tracker-a/metric " filter = " resource.type=\"gce_instance\" severity>=DEFAULT jsonPayload.message: \"Write Retry Cnt: 10\" resource.labels.instance_id: \"${google_compute_instance.streaming_datatransfer_a.instance_id}\" " metric_descriptor { metric_kind = " DELTA " value_type = " INT64 " } } 作成したメトリクスを使いCloud Monitoringでアラートを通知します。 resource " google_monitoring_alert_policy " " tracker_a_retry_error_alert_policy " { display_name = " Tracker A Retry Error " depends_on = [ google_logging_metric.retry_error_tracker_a_metric ] combiner = " OR " conditions { display_name = " condition " condition_threshold { filter = " metric.type=\"logging.googleapis.com/user/retry-error-tracker-a/metric\" resource.type=\"gce_instance\" " duration = " 0s " comparison = " COMPARISON_GT " aggregations { alignment_period = " 60s " per_series_aligner = " ALIGN_DELTA " } trigger { count = 1 } threshold_value = 0 } } enabled = true # gcloud alpha monitoring policies list -- project = streaming - datatransfer - env notification_channels = [ " projects/${var.project}/notificationChannels/${var.slack_notification_channel_id} " ] } メモリが枯渇するとプラグインから基幹データベースへのデータ取得が失敗するので、メモリ使用率が80%を超えた場合アラートを投げるよう設定してます。 resource " google_monitoring_alert_policy " " tracker_a_memory_alert_policy " { display_name = " Tracker A Memory Utilization " combiner = " OR " conditions { display_name = " condition " condition_threshold { filter = " metric.type=\"agent.googleapis.com/memory/percent_used\" resource.type=\"gce_instance\" resource.labels.instance_id=\"${google_compute_instance.streaming_datatransfer_a.instance_id}\" metric.label.\"state\"=\"used\" " duration = " 60s " comparison = " COMPARISON_GT " aggregations { alignment_period = " 60s " per_series_aligner = " ALIGN_MEAN " } trigger { count = 1 } threshold_value = 80 } } enabled = true notification_channels = [ " projects/${var.project}/notificationChannels/${var.slack_notification_channel_id} " ] } データの遅延 データの遅延はRedashを使い定期的にクエリを投げて監視しています。 データの遅延はChange Trackingの開始時間とBigQueryのインサート時刻の差分を確認しています。不正なレコードが混在した際はBigQueryの_error_recordsテーブルに書き込まれるため、書き込みを検知してアラートを通知するようにします。 また、CPUの使用状況も遅延に影響があるため、メモリ使用率と同様に監視しています。 filter = " metric.type=\"agent.googleapis.com/cpu/utilization\" resource.type=\"gce_instance\" resource.labels.instance_id=\"${google_compute_instance.streaming_datatransfer_a.instance_id}\" metric.label.\"state\"=\"used\" " 性能評価 リプレイスよってデータ欠損もなくなり、遅延時間としても取得のインターバルを除けば数秒程度でデータ連携を行うことができるようになりました。 次の図はChange Trackingで取得したレコード数と遅延時間(秒)の関係となります。取得するレコード数が多いと遅延しますが、40万レコードほどの更新でも5分以内に連携できる基盤を作ることができました。 またコスト面でも月間で約200万円ほどかかっていましたが約5万円程度にできました。 まとめ 今回リアルタイムデータ連携基盤についてご紹介しました。 現在ZOZOTOWNでは、リアルタイムデータを活用した案件が増えてきています。この記事を読んで、もしご興味をもたれた方は是非採用ページからお申し込みください。 https://tech.zozo.com/recruit/ tech.zozo.com また、8/27(木)にリアルタイムデータ連携基盤含めMAの取り組みについてのイベントを行いますのでぜひご参加ください。 zozotech-inc.connpass.com
こんにちは。ECプラットフォーム部のMA(マーケティングオートメーション)アプリケーションチームで、社内向けのマーケティング運用ツールを開発している長澤( @snagasawa_ )です。 先日、日本時間の2020年7月18日に Vue 3.0のRelease Candidate(v3.0.0-rc.1) がリリースされ、今後は最終リリースまで主要なAPIのbreaking changeは想定していないとのアナウンスがされました。アナウンスを受け、現在社内ツールで進めているOptions APIからComposition APIへの移行で得られたTipsについて紹介します。 この記事では公開時点でのVue 3.0 betaへのアップグレードの方法と、Vue + TypeScriptでのOptions APIからComposition APIへの移行のTipsについてまとめました。Vue 3.0へのアップグレードを検討されている方、またはComposition API単体での導入を検討されている方の参考になりましたら幸いです。なお、あくまで公開時点での情報であるため、今後は更新される可能性があることをご留意ください。 Vue CLIによるVue 3.0 betaへのアップグレード方法 Vue本体のアップグレード マイグレーションヘルパー Vue 3.0 へアップグレード可能かどうかの判断 Vue 3.0 betaへのアップグレードを断念した理由 Composition APIのメリットと導入理由 Options APIからComposition APIへの書き換え 書き換えの順番 Vue.extend -> defineComponent data -> reactive methods -> Functions v2.x computed -> v3.0 computed emit -> context.emit props -> setup(props) Composition Function methodsやcomputedの関数化 引数の型はRef<T> まとめ さいごに Vue CLIによるVue 3.0 betaへのアップグレード方法 Vue本体のアップグレード 初めにVue CLI(v4.5.3)で利用可能(または提供予定)なVue 3.0 betaへのアップグレードの方法を紹介します。npmかyarnで vue@next をインストールすることもできますが、Vue 3.0 betaを試すためのプラグインをインストールする方法もあります。 github.com vue add vue-next このコマンドでは vue-cli-plugin-vue-next というプラグインをインストールします。 このプラグインではVue本体だけでなく、Vuex, Vue Routerもアップグレードされます。また、コードが自動で書き換えられ、 src/main.ts, src/store/index.ts, src/router/index.ts がそれぞれのbetaのAPIに書き換えられます。以下はサンプルコードのdiffです。 diff --git a/src/main.ts b/src/main.ts index e9c1f28..39f0f54 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,12 +1,6 @@ -import Vue from 'vue'; +import { createApp } from 'vue'; import App from './App.vue'; import router from './router'; import store from './store'; -Vue.config.productionTip = false; - -new Vue({ - router, - store, - render: (h) => h(App), -}).$mount('#app'); +createApp(App).use(router).use(store).mount('#app'); diff --git a/src/router/index.ts b/src/router/index.ts index 25a46df..f6e682c 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,9 +1,6 @@ -import Vue from 'vue'; -import VueRouter, { RouteConfig } from 'vue-router'; +import { RouteConfig, createRouter, createWebHashHistory } from 'vue-router'; import Home from '../views/Home.vue'; -Vue.use(VueRouter); - const routes: Array<RouteConfig> = [ { path: '/', @@ -20,7 +17,8 @@ const routes: Array<RouteConfig> = [ }, ]; -const router = new VueRouter({ +const router = createRouter({ + history: createWebHashHistory(), routes, }); diff --git a/src/store/index.ts b/src/store/index.ts index 9ea7685..73b4b8d 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,9 +1,6 @@ -import Vue from 'vue'; import Vuex from 'vuex'; -Vue.use(Vuex); - -export default new Vuex.Store({ +export default Vuex.createStore({ state: { }, mutations: { このように一括でのアップグレードとコードの自動変換を行ってくれますが、あくまでトライアウト用のプラグインのようです。 issueのコメント によるとVue CLIの vue create コマンドでVue 3を選択可能になった現在はこのプラグインが不要になるとのことでした。開発も最終コミットが5月でそれ以降は中断されているようで、このプラグインの最新バージョンのv0.1.3ではVue 3.0 betaのバージョンが最新ではないことに注意してください。 とはいえ、既存プロジェクトでの自動コード変換は現状では他の手段で提供されていないようなので、そのために利用してみるのもよいかもしれません。 マイグレーションヘルパー 次に、マイグレーションヘルパーの提供が予定されていますが、こちらは8月21日現在では開発中のため利用できません。アップグレードが急ぎでない、もしくは比較的規模が大きいプロダクトは、こちらの完成を待つのが得策かもしれません。 Where should I start in a migration? Start by running the migration helper (still under development) on a current project. v3.vuejs.org 過去の v1.xからv2.0へのマイグレーションヘルパー ではdeprecatedな記法をwarningとして出力できます。おそらく、これに近いものが提供されるものと予想しています。 以上の方法でVue 3.0 betaやパッケージのアップグレードの一部を簡略化できます。最終リリース以降には開発中のマイグレーションヘルパーを含めて、より便利なアップグレード方法が提供されることを期待したいですね。 Vue 3.0 へアップグレード可能かどうかの判断 前述のようにアップグレードのサポートは提供されているとはいえ、Vue 3.0へのアップグレードはGlobal APIの書き換えが必要なため、影響範囲はプロダクト全体に及びます。当然のことながら、アップグレード後のプロダクトの正常な挙動を担保しなければなりませんが、中〜大規模であった場合にQAのリソースの確保が難しくなることも予想されます。 以下の観点をクリアできている場合には、Vue 3.0へのアップグレードも可能でしょう。 Vue 3.0に未対応のパッケージを利用していない テストコードが十分に書かれている プロダクトの規模がそれほど大きくない、あるいはQAのリソースを十分に確保できる 一方でVue 3.0へのアップグレードが困難ではあるものの、Vue 3.0 betaの新APIを採用したい場合は Composition APIをプラグイン としてその一部をコンポーネント単位で利用することもできます。ここからは冒頭で触れた社内ツールの開発において進めているComposition APIへの移行について紹介します。 Vue 3.0 betaへのアップグレードを断念した理由 社内ツールでComposition APIを採用した一方で、Vue 3.0 betaへのアップグレードは一旦断念することになりました。理由としては利用している VuetifyがVue 3.0に未対応 だったためです。なお、Vuetifyの Roadmap では2020 Q3/Q4での対応を予定しているとのことです。 Composition APIのメリットと導入理由 Composition APIのメリットはいくつかありますが、特に採用の決め手となったのはロジックの関心ごとにコードをまとめやすくなることでした。 v2.xのOptions APIの課題は、コードがコンポーネントの this に依存するため関心ごとの単位でまとめることが難しく、Options APIのoptions(data, computed, methodsなど。以下: v2 options)にコードが分散しがちであることでした。 実際に開発している社内ツールでは、入力フォームの画面でフィールドが入れ子になるようなケースが複数ありました。フォームは複数のコンポーネントに分割し、関数はシンプルな実装を心がけていましたがコードの分散は避けられませんでした。そのため、機能追加や修正を行う際にコードの中からしばしば該当のコードを探す必要があり、致命的ではないものの可読性に課題感を持っていました。 そんな状況でComposition APIのRFCを読み、実装を試してみたところ手応えがあったため、この課題を解消すべく採用を決めました。 Options APIからComposition APIへの書き換え ここからはサンプルコードを交えて、コンポーネントをOptions APIからComposition APIに書き換えていく際のTipsについて紹介します。 インストール方法はGitHubリポジトリの README にある通りなので省略します。 書き換えの順番 Composition APIへの移行で念頭に置いておきたいことは Options APIと併用可能 という点です。 これはコンポーネント内の依存関係を考慮して順番に書き換えを進めることで、そのすべてが終わらなくとも、途中でテストコードや動作確認で正しい挙動を確認しつつ進められることを意味しています。特に大きなコンポーネントの際に、すべてを書き換え終えたつもりでようやく動作確認を始めたものの、正常に動作せずバグ修正に苦心するという事態を避けられるかもしれません。 併用した場合の処理の順番はComposition APIの setup が先に呼ばれ、そのあとv2 optionsが解決されます。 setup 内では this は undefined でなおかつv2 optionsの前に呼ばれるため、 setup 内からv2 optionsのプロパティにはアクセスできません。逆に setup でreturnしたプロパティはv2 optionsからアクセスできます。したがって、移行の流れとしてはコンポーネント内で他の実装に依存していないものから setup 内に移していくとよさそうです。 以下が順番の例です。順番には絶対解はなく、ある程度入れ替え可能です。 Vue.extend -> defineComponent data -> reactive methods -> Functions v2.x computed -> v3.0 computed emit -> context.emit props -> setup(props) Composition Function サンプルコードは去年の アドベントカレンダーで書いた記事 のリポジトリを元にOptions APIで実装し直したものです。 github.com < template > < div > < div > < input v-model = "taskName" type= "text" / > < button @click = "addTask" > Add < /button > < /div > < div >< input v-model = "searchText" type= "text" / > Search < /div > < div class= "task-list-wrapper" > < ul > < h4 > DOING < /h4 > < li v- for= "(task, index) in doingTasks" :key = "index" > < input type= "checkbox" :checked = "task.status" disabled / > < label > {{ task.name }} < /label > < button @click = "toggleTask(task, true)" > toggle < /button > < /li > < /ul > < ul > < h4 > COMPLETED < /h4 > < li v- for= "(task, index) in completedTasks" :key = "index" > < input type= "checkbox" :checked = "task.status" disabled / > < label > {{ task.name }} < /label > < button @click = "toggleTask(task, false)" > toggle < /button > < /li > < /ul > < /div > < /div > < /template > < script lang = "ts" > import Vue from 'vue' ; import { Task } from '../types' ; interface Data { taskName: string ; searchText: string ; tasks: Task [] ; } export default Vue.extend ( { data: () : Data => { return { taskName: '' , searchText: '' , tasks: [] , } ; } , computed: { doingTasks () : Task [] { return this .searchedTasks.filter ( t => !t. status); } , completedTasks () : Task [] { return this .searchedTasks.filter ( t => t. status); } , searchedTasks () : Task [] { return this .tasks.filter ( t => t.name.includes ( this .searchText )); } , } , methods: { addTask () { this .tasks.push ( { name: this .taskName , status : false , } ); this .taskName = '' ; } , toggleTask ( task: Task , status : boolean ) { const index = this .tasks.indexOf ( task ); this .tasks.splice ( index , 1 , { ...task , status : status } ); } , } , } ); < /script > < style scoped > .task-list-wrapper { display: flex ; justify-content: center ; } < /style > Vue.extend -> defineComponent 初めに Vue.extend を defineComponent に変更します。 Options APIでTypeScriptの型推論のために Vue.extend が必要だったのと同様、Composition APIでも defineComponent が必要になります。 -import Vue from 'vue'; +import { defineComponent } from '@vue/composition-api'; -export default Vue.extend({ +export default defineComponent({ + setup() { + }, 空の setup を定義してv2 optionsの挙動を確認した後、順次この setup にコードを移していきます。 data -> reactive 次に data をreactive関数に書き換えます。 data のinterfaceは reactive の型引数として渡します。 returnでは ...toRefs(state) のように書くのがオススメです。 state をそのままreturnすることもできますが、その場合はtemplateやv2 optionsで state.xxx のように書き換える必要があります。 また、 toRefs に渡さずspread operatorで展開する場合はstateがリアクティブではなくなってしまうため、この場合は toRefs が必須です。 export default defineComponent({ setup { - data: (): Data => { - return { + const state = reactive<Data>({ taskName: '', searchText: '', tasks: [], + }); + + return { + ...toRefs(state), }; }, methods -> Functions methods は関数を setup に切り出し、 this を state に変えてreturnで返すだけです。 export default defineComponent({ setup() { const state = reactive<Data>({ taskName: '', searchText: '', tasks: [], }); + const addTask = () => { + state.tasks.push({ + name: state.taskName, + status: false, + }); + state.taskName = ''; + }; + + const toggleTask = (task: Task, status: boolean) => { + const index = state.tasks.indexOf(task); + state.tasks.splice(index, 1, { ...task, status: status }); + }; + return { ...toRefs(state), + addTask, + toggleTask, }; }, computed: { @@ -60,19 +75,6 @@ export default defineComponent({ return this.tasks.filter(t => t.name.includes(this.searchText)); }, }, - methods: { - addTask() { - this.tasks.push({ - name: this.taskName, - status: false, - }); - this.taskName = ''; - }, - toggleTask(task: Task, status: boolean) { - const index = this.tasks.indexOf(task); - this.tasks.splice(index, 1, { ...task, status: status }); - }, - }, }); v2.x computed -> v3.0 computed Composition APIの computed はgetter関数を引数にとり、computedプロパティを返します。 こちらも書き換えはシンプルで methods とほぼ同じ要領です。 以下がその2つの比較です。 Options API computed: { searchedTasks () : Task [] { return this .tasks.filter ( t => t.name.includes ( this .searchText )); } , doingTasks () : Task [] { return this .searchedTasks.filter ( t => !t. status); } , completedTasks () : Task [] { return this .searchedTasks.filter ( t => t. status); } , } , Composition API const searchedTasks = computed (() => { return state.tasks.filter ( t => t.name.includes ( state.searchText )); } ); const doingTasks = computed (() => { return searchedTasks.value.filter ( t => !t. status); } ); const completedTasks = computed (() => { return searchedTasks.value.filter ( t => t. status); } ); 今回はシンプルなアプリのため、ここまでの変更でv2 optionsはすべて setup 内に書き換えられました。 ここまでの変更に少し手を加えたバージョンのソースコードは以下で確認できます。 github.com emit -> context.emit emit はsetupの第2引数の context から呼び出します。 以下の例はTaskを表示する列をコンポーネント化したコードです。 github.com 注意点として、 emit は camelCase では呼び出せなくなっており、 kebab-case で呼び出す必要があります。 export default defineComponent ( { props: { title: { type : String , required: true } , tasks: { type : Array as () => Task [] , required: true } , } , setup ( props , context ) { const toggleTask = ( task: Task ) => { context.emit ( 'toggle-task' , task ); } ; return { toggleTask , } ; } , } ); props -> setup(props) propsの定義は上の例のようにOptions APIの時と変わらず、setupの第1引数として受け取ることができます。setupの引数に型を指定せずともpropsの型推論が効きます。 Composition Function ここまでの説明でコンポーネント内の主要なAPIの書き換えの流れはおおよそ掴むことができるかと思います。最後にComposition APIによって実装可能なComposition Functionを実装する際のTipsを紹介します。 Composition Functionとは、関連するロジックでまとめられ、カプセル化された関数のことです。Composition APIによって this への依存がなくなり、コードは関心ごとでまとめられるようになりました。まとめられた関数は純粋なJavaScript or TypeScriptの関数として抽出し、他のコンポーネントで再利用しやすくなります。 Notice how all the logic related to the create new folder feature is now collocated and encapsulated in a single function. The function is also somewhat self-documenting due to its descriptive name. This is what we call a composition function. vue-composition-api-rfc.netlify.app methodsやcomputedの関数化 methods や computed を関数でラップ(あるいはカリー化)しておくと、Composition Functionが作りやすくなり、関数合成がしやすくなるというメリットがあります。 下は先述の computed を関数化した例です。 const searchedTasks = (( tasks , text ) => computed (() => { return tasks.value.filter ( t => t.name.includes ( text.value )); } ))( toRef ( state , 'tasks' ), toRef ( state , 'searchText' )); const doingTasks = ( tasks => computed (() => { return tasks.value.filter ( t => !t. status); } ))( searchedTasks ); const completedTasks = ( tasks => computed (() => { return tasks.value.filter ( t => t. status); } ))( searchedTasks ); 先ほどの computed の例に戻って関数化していない例をもう一度見てください。 setup 内であれば関数化しない実装も可能ですが、その場合は他の実装に依存します。例の computed をComposition Function化する場合は、 state.tasks や searchedTasks への依存を関数化した時と同じように引数への変更が必要です。その点、関数化しておくと変更が少なくて済みます。 以下がComposition Functionとして抽出した例です。 import { computed , Ref } from '@vue/composition-api' ; import { Task } from '@/types' ; export default function useFilter ( tasks: Ref < Task [] >) { const doingTasks = computed (() => tasks.value.filter ( t => !t. status)); const completedTasks = computed (() => tasks.value.filter ( t => t. status)); return { doingTasks , completedTasks , } ; } 逆に関数化をしない方がパッと見ではシンプルでわかりやすいというメリットがあるため、Composition Function化の可能性が低い場合は2番目のような実装に留めておくのがよさそうです。 引数の型はRef<T> Composition Functionや関数化された関数で実装する時に、リアクティブな引数を受け取る場合の型定義はどうすべきか疑問に浮かぶかもしれません。 引数次第では推論で Ref<T> や ComputedRef<T> などになる可能性があり、一見すると使い分けるか、もしくはいずれかで統一するといった選択の余地がありそうに見えます。 結論としては Ref<T> で統一すればよさそうですが、理解を深めるために型定義を確認してみましょう。 Ref<T> は ref 、 reactive などでリアクティブ化されたオブジェクトで、 ComputedRef<T> は computed が返すオブジェクトの型です。 ComputedRef<T> は WritableComputedRef<T> をextendsしていますが、 WritableComputedRef<T> は Ref<T> をextendsしているため、引数の型定義と渡される引数の組み合わせがいずれでもTypeErrorにはなりません。 したがって基本となる Ref<T> で統一するとよいでしょう。 interface ComputedRef < T = any > extends WritableComputedRef < T > { readonly value: T ; } interface WritableComputedRef < T > extends Ref < T > { } interface Ref < T = any > { readonly [ _refBrand ] : true ; value: T ; } まとめ Vue 3.0 betaへのアップグレード方法の紹介と、社内ツール開発でのComposition APIへの移行について紹介しました。Composition APIでの実装で最近よく考えることは、コードの再編の自由度が上がった分、コードをまとめる境界をどのように見つけるのがよいかという点です。Composition Functionへの分割をパターン化できるとより開発をスムーズに進められそうです。その点を踏まえて、引き続きVue 3.0と周辺パッケージのキャッチアップと移行を進めていきたいと思います。 さいごに ZOZOテクノロジーズではマーケティングに関連するプロダクトの開発や、フロントエンド開発に興味のあるエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください! tech.zozo.com また、8/27に、この記事で紹介した社内ツールを含めたMAの取り組みについてのイベントを行いますので、こちらも奮ってご応募ください! zozotech-inc.connpass.com
こんにちは。ZOZOテクノロジーズZOZOTOWN部 検索チーム 兼 ECプラットフォーム部 検索基盤チームの有村です。 ZOZOTOWNでは 先日公開した記事 の通り、すべての検索をElasticsearchへ置き換えました。置き換え直後は順調に見えたのですが、実際に数%ずつリリースしていく中で一部時間帯、一部リクエストでレスポンス速度の低下がみられました。 本記事ではその解決のために行ったパフォーマンス調査、チューニング方法についてご紹介します。なお、一般的に行われるであろうElasticsearch本体のパラメータチューニングの話ではなく、クエリやmapping、setting面の話がメインとなります。 改善前後の速度について 詳細な内容の前に、本改善によるレスポンス速度の最終的な改善結果を示します。 今回の計測では、一定パターンのリクエストを10秒間繰り返し、95%tileのレスポンス速度をp95、99%tileのレスポンス速度をp99と表しています。 p95 (sec) p99 (sec) 改善前 0.553 0.667 改善後 0.147 0.205 95%tileで 約276% の改善、99%tileで 約225% の改善となりました。 調査方法 環境 今回の検証は以下の環境で行いました。 ツール バージョン Elasticsearch 7.5.1 Kibana 7.5.1 Gatling 3.3.1 Search Profilerの使用 RDBMSのチューニングでは実行計画を見て適切なインデックス設計・クエリチューニングを行うように、今回はKibanaの Search Profiler を用いて検証を行いました。 Search ProfilerはElasticsearchの Profile API を可視化したものであり、具体的には以下の情報が取得可能です。 項目 詳細 create_weight 検索の最中にWeightを保持するオブジェクトの生成にかかる時間 build_scorer スコアラー(≠ドキュメントのスコア)オブジェクトの生成にかかる時間 next_doc マッチした次のドキュメントIDを返すのにかかった時間 advance next_docの内部でも呼ばれている、低レベルなイテレータの実行にかかった時間 match phrase queryのような、2フェーズ目の厳密なマッチの際にかかった時間 score スコアラーを用いて実際に特定のドキュメントをスコアリングした際にかかった時間 *_count 特定のメソッドの呼び出し回数 下記はKibanaをインストールした際にデフォルトで選択可能なSample eCommerce ordersデータセットを用いてプロファイリングしたサンプルです。 -- 検索リクエスト GET /kibana_sample_data_ecommerce/_search { " query ": { " bool ": { " filter ": [ { " term ": { " currency ": " EUR " } } , { " range ": { " products.base_price ": { " gte ": 50.00 } } } ] } } , " size ": 0 , " profile ": true } -- レスポンス { " took " : 29 , " timed_out " : false , " _shards " : { " total " : 1 , " successful " : 1 , " skipped " : 0 , " failed " : 0 } , " hits " : { " total " : { " value " : 2097 , " relation " : " eq " } , " max_score " : null , " hits " : [ ] } , " profile " : { " shards " : [ { " id " : " [8Xdz-7ZTSzyo4IAHkWSTsA][kibana_sample_data_ecommerce][0] ", " searches " : [ { " query " : [ { " type " : " BooleanQuery ", " description " : " #currency:EUR #products.base_price:[50.0 TO Infinity] ", " time_in_nanos " : 21453981 , " breakdown " : { " set_min_competitive_score_count " : 0 , " match_count " : 0 , " shallow_advance_count " : 0 , " set_min_competitive_score " : 0 , " next_doc " : 3014562 , " match " : 0 , " next_doc_count " : 2097 , " score_count " : 0 , " compute_max_score_count " : 0 , " compute_max_score " : 0 , " advance " : 848900 , " advance_count " : 7 , " score " : 0 , " build_scorer_count " : 14 , " create_weight " : 45100 , " shallow_advance " : 0 , " create_weight_count " : 1 , " build_scorer " : 17543300 } , " children " : [ { " type " : " TermQuery ", " description " : " currency:EUR ", " time_in_nanos " : 20980103 , " breakdown " : { " set_min_competitive_score_count " : 0 , " match_count " : 0 , " shallow_advance_count " : 0 , " set_min_competitive_score " : 0 , " next_doc " : 277970 , " match " : 0 , " next_doc_count " : 591 , " score_count " : 0 , " compute_max_score_count " : 0 , " compute_max_score " : 0 , " advance " : 7522992 , " advance_count " : 1828 , " score " : 0 , " build_scorer_count " : 21 , " create_weight " : 7400 , " shallow_advance " : 0 , " create_weight_count " : 1 , " build_scorer " : 13169300 } } , { " type " : " IndexOrDocValuesQuery ", " description " : " products.base_price:[50.0 TO Infinity] ", " time_in_nanos " : 4954816 , " breakdown " : { " set_min_competitive_score_count " : 0 , " match_count " : 0 , " shallow_advance_count " : 0 , " set_min_competitive_score " : 0 , " next_doc " : 683762 , " match " : 0 , " next_doc_count " : 1506 , " score_count " : 0 , " compute_max_score_count " : 0 , " compute_max_score " : 0 , " advance " : 217728 , " advance_count " : 598 , " score " : 0 , " build_scorer_count " : 21 , " create_weight " : 5800 , " shallow_advance " : 0 , " create_weight_count " : 1 , " build_scorer " : 4045400 } } ] } ] , " rewrite_time " : 6600 , " collector " : [ { " name " : " EarlyTerminatingCollector ", " reason " : " search_count ", " time_in_nanos " : 2358000 } ] } ] , " aggregations " : [ ] } ] } } これをKibanaのDev Tools内にあるSearch Profilerから可視化すると以下のような表示となります。 右側にあるView detailsをクリックすると、上に記載した詳細な項目が表示され、内部でどのような処理にどれだけ時間がかかるか可視化されます。 また、analyzerを適用したフィールドに対する検索をプロファイリングした場合、内部でどのように展開され、どれだけ時間がかかるか可視化されます。 -- マッピング PUT /kuromoji_sample { " mappings ": { " properties ": { " category ": { " type ": " keyword ", " fields ": { " kuromoji ": { " type ": " text ", " analyzer ": " kuromoji " } } } } } , " settings ": { " index ": { " analysis ": { " analyzer ": { " kuromoji ": { " type ": " custom ", " tokenizer ": " kuromoji_tokenizer " } } } } } } -- データ登録 POST /kuromoji_sample/_bulk { " index ": {} } { " category ": " 靴 " } { " index ": {} } { " category ": " かばん " } { " index ": {} } { " category ": " Tシャツ " } -- 検索 GET /kuromoji_sample/_search { " query ": { " match ": { " category.kuromoji " : " 通勤かばん " } } } 効果のあった変更点 Rangeクエリの丸め込み ZOZOTOWNでは発売日での絞り込みやタイムセールの制御など、検索の要所要所でdate型データに対する絞り込みを行っています。その際、Elasticsearchへのリクエストには現在時刻のタイムスタンプを用いていました。 { " query ": { " bool ": { " filter ": { " range ": { " order_date ": { " gte ": " 2020-08-20T09:01:12+00:00 " } } } } } } しかし、 Elasticsearch公式のドキュメント にもある通り、時刻によるフィルタリングはfilter cacheに載らないため、毎回絞り込みが行われ低速になる傾向がありました。 Tune for search speed を参照すると、丸め込まれた時刻指定のfilterクエリはfilter cache対象になる、との記載がありサイトとしての必要要件を確認する事になりました。その結果、発売日の絞り込みやその他イベントに関して1分単位の精度が確保できていればよく、必ずしも秒単位で考える必要がないと判明しました。 そこでリクエストを以下のような末尾に ||/m を付与し、明示的に丸め込んでいることがわかる形式に変更しました。 { " query ": { " bool ": { " filter ": { " range ": { " order_date ": { " gte ": " 2020-08-20T09:01:12+00:00||/m " } } } } } } p95 (sec) p99 (sec) 改善前 0.553 0.667 改善後 0.202 0.516 95%tileで 約174% の改善に対し、99%tileでは 約29% の改善に留まりました。 search_analyzerの使用 上記のRangeクエリの改善によって95%tileのレスポンス速度は改善しましたが、99%tileの改善度合いが思わしくなかったため、追加の改善案を模索しました。 この時点までは全体の最適化を行っていましたが、リクエスト単位に注目して深堀りしたところ、特定のリクエストが毎回遅くなっていることが判明しました。 その特定のリクエストは日本語によるキーワードが含まれるような検索リクエストで、かつZOZOTOWN独自で定義しているシノニム(類義語)が含まれるものでした。 そこで、前述のSearch Profilerを用いて内部でどの部分に時間を要しているのか確認したところ、シノニムによって展開されたキーワード分検索リクエストが走っている様子でした。 インデックスのマッピングは、以下の設定となっていました。 { " mappings ": { " properties ": { " category ": { " type ": " keyword ", " fields ": { " synonym ": { " type ": " text ", " analyzer ": " synonym_analyzer " } } } } } , " settings ": { " index ": { " analysis ": { " filter ": { " synonym_graph ": { " type ": " synonym_graph ", " synonyms ": [ " かばん, カバン, バッグ, 鞄 " ] } } , " analyzer ": { " synonym_analyzer ": { " type ": " custom ", " tokenizer ": " kuromoji_tokenizer ", " filter ": [ " synonym_graph " ] } , } } } } } デフォルトの設定では、検索時に使用されるアナライザと、インデキシング時に使用されるアナライザは同一のものが設定されます。そのため、上記の設定ではインデキシング時・検索時共にkuromojiとシノニムが適用されることになります。 一方、追加の設定として search_analyzer を設定することにより、インデキシング時と検索時で異なるアナライザを指定することも可能です。 { " mappings ": { " properties ": { " category ": { " type ": " keyword ", " fields ": { " synonym ": { " type ": " text ", " analyzer ": " synonym_analyzer ", " search_analyzer ": " kuromoji " } } } } } , " settings ": { " index ": { " analysis ": { " filter ": { " synonym_graph ": { " type ": " synonym_graph ", " synonyms ": [ " かばん, カバン, バッグ, 鞄 " ] } } , " analyzer ": { " synonym_analyzer ": { " type ": " custom ", " tokenizer ": " kuromoji_tokenizer ", " filter ": [ " synonym_graph " ] } , " kuromoji ": { " type ": " custom ", " tokenizer ": " kuromoji_tokenizer " } } } } } } このようにsearch_analyzerではシノニムの展開を行わない設定が可能ですが、この設定には以下のようなメリット・デメリットがあります。 メリット インデキシング時のみにシノニム展開するため、検索時の負荷が低減される デメリット 再インデックスしない限り、既存のドキュメントに対して類義語の更新が反映されない 詳しくはElastic社公式の 記事 で議論されていましたので、そちらをご覧ください。記事内ではデメリットとメリットを比較した結果、検索時にアナライザを適用する方向で収束していました。 しかし、弊社におけるユースケースでは以下のポイントから、インデキシング時に適用することとなりました。 インデキシングリクエスト数より検索リクエスト数が圧倒的に多いため、検索時のパフォーマンスを重視したい 日次で新規インデックスに対して全件インデキシングを行い、エイリアスを用いて検索先の管理を行っているため、類義語の更新に対して再インデックスを考える必要がない ドキュメントに対するマッチスコアを用いた評価を行っていない 実際に上記の設定を適用したうえで計測されたパフォーマンスは以下の通りとなります。 p95 (sec) p99 (sec) 改善前(オリジナル) 0.553 0.667 改善前(Range丸め込み) 0.202 0.516 改善後(Range丸め込み + search_analyzer) 0.147 0.205 Range丸め込み改修後と比較すると、95%tileで 約37% の改善、99%tileで 約152% の改善となりました。 Search Profilerで確認しても、検索時にシノニムが展開されていないことが確認できます。 効果のなかった変更点 max_result_window Elasticsearchのindexのsettingsに max_result_window という設定項目があり、これによって取得できる件数の制限が発生しています。 具体的には、検索時に指定する from と size の合計がmax_result_window以下でなくてはならず、超えた場合は query_phase_execution_exception が発生します。 弊社では初期設定時に max_result_window の値を内部のドキュメント数以上に設定し、制限なく検索が可能な状態にしていました。 { " mappings ": { ... } , " settings ": { " index ": { " max_result_window ": 100000 } , ... } } 一方で、 公式ドキュメント に記載がある通り、深いページングを行う際は (from + size) * number_of_shard 分のドキュメントを取得するためコストがかかります。 このオプション値の変更によって内部のパフォーマンスがどれ程変わるかは全く未知数でしたが、悪影響を与えていないかを確認するため検証を行うことにしました。 以下がその検証結果です。 p95 (sec) p99 (sec) max_result_window = 10,000,000 0.599 0.763 max_result_window = 10,000 0.580 0.653 95%tileでは約17%、99%tileで約3%の改善とあまり効果はありませんでした。 改善度合いに対して、一定数以上の検索に対する制限をかけてしまうデメリットが大きいと考え、今回採用は見送ることとなりました。 番外編 BulkのExceptionによるパフォーマンス影響 こちらは完全にデータの前処理が原因のミスでしたが、自分への戒めも兼ねて書き残します。 上記で解決した全体的なレスポンス速度低下の問題とは別に、1日の中で数十分ほど局所的にレスポンス速度の低下がみられました。 その際、具体的には以下のような症状が見られました。 レスポンス速度の低下(タイミングによっては、通常の数倍かかることも) /_nodes/stats から観測できるOld GCの増加 該当の時間にはドキュメントの一部を更新する処理が走っており、 _id 指定でbulk updateを行っていたのですが、多くのリクエストで更新対象がインデックス内に存在していませんでした。 Bulkで返ってくるレスポンスの実態は BulkResponse 、 BulkItemResponse ですが、この BulkItemResponse の挙動の差に原因がありました。通常アップデートに成功すると、この BulkItemResponse には DocWriteResponse 型のオブジェクトが格納されますが、失敗時には Failure 型のオブジェクトが格納されます。このオブジェクトにはメンバー変数として、例外の状態を示すオブジェクトが存在しており、 DocWriteResponse よりオブジェクトのサイズが大きいと考えられます。 またもう1つの要因として、Bulkの1リクエスト当たりのサイズを全バッチ共通の値でかなり大きめに設定していたことも関係していました。1フィールドだけを更新するシンプルなリクエストの場合、1リクエストで1万件以上の更新がかかっている状況でした。レスポンスは List<BulkItemResponse> として保持されているため、最大1万件以上分の BulkItemResponse が更新中メモリ上に居座ります。そのため、上記画像のような特定時間帯に集中したGCが発生し、局所的にリクエストが遅くなる状況となっていました。 検証 実際に今回のケースで発生していた DocumentMissingException と、成功時に生成される UpdateResponse を用いて、オブジェクトのサイズ比較を行いました。 また、1万件分を実際のレスポンスである BulkResponse に格納した際、差がどれほど出るかの検証も合わせて行いました。 public void test() throws IOException { int nbBulkItems = 10000 ; List<BulkItemResponse> succeedItems = new ArrayList<>(); List<BulkItemResponse> failureItems = new ArrayList<>(); UpdateResponse updateResponse = null ; Failure failure = null ; for ( int i = 0 ; i < nbBulkItems; i++) { updateResponse = new UpdateResponse( new ShardId( "index" , "index_uuid" , 0 ), "type" , "id" , - 2 , 0 , 0 , UPDATED); failure = new Failure( "index" , "type" , "id" , new DocumentMissingException( new ShardId( "index" , "index_uuid" , 0 ), "1" , "1" ), RestStatus.fromCode( 404 )); succeedItems.add( new BulkItemResponse(i, DocWriteRequest.OpType.UPDATE, updateResponse)); failureItems.add( new BulkItemResponse(i, DocWriteRequest.OpType.UPDATE, failure)); } BulkResponse succeedResponse = new BulkResponse(succeedItems.toArray( new BulkItemResponse[succeedItems.size()]), 0 , 0 ); BulkResponse failureResponse = new BulkResponse(failureItems.toArray( new BulkItemResponse[failureItems.size()]), 0 , 0 ); System.out.println( "SucceedResponse => " + RamUsageEstimator.sizeOf( new BulkItemResponse( 1 , DocWriteRequest.OpType.UPDATE, updateResponse))); System.out.println( "FailureResponse => " + RamUsageEstimator.sizeOf( new BulkItemResponse( 1 , DocWriteRequest.OpType.UPDATE, failure))); System.out.println( "SucceedResponse(10000) => " + RamUsageEstimator.sizeOf(succeedResponse)); System.out.println( "FailureResponse(10000) => " + RamUsageEstimator.sizeOf(failureResponse)); } 結果 ucceedResponse => 696 FailureResponse => 1392 SucceedResponse(10000) => 1960560 FailureResponse(10000) => 6840768 上記のような簡易的な検証でも、 BulkItemResponse 単体で2倍、1万件の BulkResponse では約3.5倍となっていることがわかりました。 解決策 解決策として以下の2つを試したところ、GC・レスポンス速度共に大幅な改善が見られました。 Bulkリクエストのサイズ縮小 DocumentMissingExceptionの回避(更新対象の再考) まとめ 本記事では、ZOZOTOWNの検索改善におけるクエリ、mapping、settingのパフォーマンス調査、チューニング方法について紹介しました。個人的にはSearch Profilerが特に気に入っており、SQLの実行プランと同じ感覚で実際に発行されるリクエストレベルの確認できるためとても勉強になります。 最後に、ZOZOテクノロジーズでは検索をさらに改善する検索エンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com 謝辞 本記事に含まれる検証の一部については、検索分野の技術顧問である 大谷氏(@johtani) による協力のもと行われました。また、テックブログの執筆に際してもレビュー・アドバイスを頂くなど、多大なるご協力を頂きましたことをこの場を借りて御礼申し上げます。
こんにちは、SRE部MA基盤チームの田島です。 私達のチームでは、マーケティングシステムの開発・運用を自前で行っています。マーケティングシステムの内容としては、主にユーザに向けてのメールやLINE・PUSH通知などへの配信です。 マーケティングシステムは大きく分けて2種類あります。1つ目がSQLによるセグメント抽出を行い、抽出したユーザに対してバッチで配信を行うバッチ配信システムです。2つ目がユーザの行動や商品情報等データの変更をリアルタイムに検知して配信を行うリアルタイムマーケティングシステムです。 本記事ではリアルタイムマーケティングシステム(RTM)について紹介します。また現在、RTMのリプレイス計画を行っているのでそれについても紹介いたします。 ZOZOTOWNのリアルタイムマーケティングシステム リアルタイムマーケティングシステムではユーザの行動や商品情報等データの変更を検知し、ユーザへリアルタイムでアクションを行います。例えばある商品の値段が下がったとき、RTMはその商品情報をリアルタイムに検知し、その商品をお気に入りしているユーザに対して配信を行います。 なぜリアルタイムに配信する必要があるのか リアルタイム配信を行っている理由は2つあります。1つ目が配信システムのバージョンアップの変遷で、2つ目が配信キャンペーンの性質にあります。 配信システムのバージョンアップの変遷 元々ZOZOTOWNでは1日1回決まった時間に配信をするということしかしていませんでした。 そこでまず、1日に1回抽出したユーザに対して9時・12時・18時の3パターンでユーザ毎に配信時間の振り分けをしてみたしたところ、効果に差が出ました。次に、配信ごとに最新のデータを使ってセグメント抽出し配信をするとCVRの向上がみられました。 このような検証を得てRTMによるリアルタイム配信が誕生しました。リアルタイム配信だけが要因ではありませんが、実際RTMによる配信によって開封率・CTR・CVRの指標が全て向上(最大で開封率2倍、CVR5倍)しました。 配信キャンペーンの性質 ZOZOTOWNのユーザへの通知には、「商品が残り1点になったお気に入りアイテムをユーザにお知らせする」と言ったキャンペーンがあります。そのため、商品が残り1点になり数時間たってからユーザに通知を行っても、既にその商品が完売になっている可能性があります。 また、まだ未実装の状態ではありますが配信チャネルとして訪問中のユーザに対しアプリ内通知としてメッセージを送るということを当初より計画していました。そのため、訪問中のユーザが離脱するよりも前にメッセージを届ける必要があります。 このようなことからZOZOTOWNではリアルタイムマーケティングの仕組みが必要になります。 RTMの機能 イベントの検知から、配信までの流れは以下のようになっています。 以下でそれぞれの機能の詳細を説明します。 イベント検知 まず最初に行うのがイベント検知です。イベントはユーザ行動とZOZOTOWNのデータの変化があります。ユーザ行動はアクセスログから検知を行い、データの変更はZOZOTWONのDBの差分データを取得して検知を行っています。イベント検知されたものを整形しキャンペーン判定の処理に渡します。 キャンペーン判定 イベントを検知するとキャンペーン判定を行います。キャンペーンとはどのようなイベントが発生したときにどのようなお知らせをユーザに提供するかを定めたものです。例えば、商品が値下がりしたときにその商品をお気に入りしているユーザに対して、「あなたがお気に入りしているこの商品が値下がりしました」といったメッセージを配信するキャンペーンがあります。 ユーザ行動や商品情報の変更のタイミングでどのようなキャンペーンを実施するかを決めるのが、このキャンペーン判定の処理になります。 ユーザ抽出/メンバーフィルタ 続いての処理がユーザ抽出処理です。キャンペーンが決まると、そのキャンペーンに該当するユーザの抽出を行います。そしてユーザ抽出後、本当にそのユーザにキャンペーンを配信するべきかのフィルターをユーザごとに行います。これによって得られたユーザに対してキャンペーンの配信を行います。 チャネル最適化 RTMから配信されるチャネルは複数あり、現在MAIL/LINEがあります。また、PUSH配信・アプリ内通知も対象チャネルとして実装を進めています。RTMからどのチャネルに対して配信されるかは、システムによって決められます。例えば、MAILへの反応は悪いがLINEにはよく反応するユーザに対してはLINEのみで配信をするといったことを行います。 時間最適化 RTMではリアルタイム配信を行っていますが、ただただイベント検知からリアルタイムに配信をするだけではなく時間最適化ということも行っています。例えば、このユーザは特定の時間にMAILを確認することが多いと判断すると、リアルタイム配信せず一度キューにためます。その後最適な時間にキューから取り出しキャンペーン配信を行います。もちろん、キャンペーンやチャネルによってリアルタイム配信のほうが良いと判断したら、キューにためずリアルタイム配信を行います。 通数最適化 ユーザへの配信はただ数多く配信をすればいいというものではありません。ユーザが配信をうるさいと感じオプトアウトしてしまうと、ユーザとコミュニケーションを取ることができなくなってしまいます。そこでRTMでは通数最適化を行っています。これはユーザにとって最も最適な通数は何件なのか判断し、それ以上の通知はしないようにしています。 コンテンツ生成・配信処理 コンテンツ生成では、実際に配信するコンテンツを生成します。コンテンツとはLINEであれば、LINEに配信するメッセージと対応するJSONです。 最後に、作成したコンテンツを各チャネルに対し配信を行います。 リアルタイムマーケティングシステムを支える技術 続いて実際にマーケティングシステムで利用している技術について紹介します。 アーキテクチャ アーキテクチャは以下のようになっています。RTMのシステムはすべてAWS上で動いています。 以下でそれぞれの項目について紹介します。 Analyzer Analyzerは上記で説明した「キャンペーン判定」から「配信処理」までを行うアプリケーションです。このアプリケーションがRTMのメインアプリケーションとなります。Analyzerについては次の章で詳しく説明します。 RTM DB RTM DBはRTMのデータを永続化するためのDBです。RTMに関係するデータはこのDBに格納されます。DBにはPostgreSQLを利用しています。 レプリDB ZOZOTOWNではDBにSQL Serverを利用しており、用途によって複数のSQL Serverに別れています。RTMでは、ZOZOTWONのDBを直接参照するのではなくレプリケーションしたDBをAWS上に立てて利用しています。 SQL Serverではテーブル単位でレプリケーションできるため、各DBから必要なテーブルのみをレプリケーションし、1つのDBに集約しています。 Tracker TrackerはZOZOTOWNのDBからレプリケーションしたSQL Serverの変更データを追跡し、Analyzerに差分データとして連携するアプリケーションです。差分データの取得にはSQL ServerのChange Trackingという仕組みを利用しています。Change Trackingをかんたんに説明すると、変更が発生したテーブルのPKを保持する機能です。Change Trackingの詳細については以下をご参照ください。 docs.microsoft.com Change Trackingを有効にすると CHANGETABLE(CHANGES ...) を利用できるようになります。 CHANGETABLE(CHANGES ...) からは以下のリンクようなデータを取得できます。 docs.microsoft.com この機能を利用すると以下のSQLで差分データを取得できます。削除された行に関しては deleted という列を設けて判断できるようにしています。以下はorderテーブルの例です。 SELECT CT.id id, O.必要なカラム 1 , O.必要なカラム 2 CASE WHEN O.id IS NULL THEN 1 ELSE 0 END deleted FROM CHANGETABLE(CHANGES dbo. order , 前回の取得したときのversion) AS CT LEFT OUTER JOIN dbo. order AS O ON CT.id = O.id 上記クエリで差分データを取得したらそれをJSONの形式に整形しAnalyzerへ連携します。 Analyzer 先程紹介したとおり、Analyzerが「キャンペーン判定」から「配信処理」までを行うRTMのメインアプリケーションとなっています。リアルタイム配信を実現するため高速かつ柔軟なルール判定が必要となります。それらを実現している技術について紹介します。 Analyzerの非機能要件 「RTMの機能」で説明した機能要件の他に、Analyzerは以下の非機能要件が必須となります。 複雑なルール判定 動的なルール追加 高速な条件判定 複雑なルール判定 Analyzerではユーザの取った様々な行動や、ZOZOTOWNで扱う様々なデータの変化を検知しキャンペーン判定を行います。また各種最適化の処理条件は何パターンにもなり複雑になります。 そこでそれらの複雑なルール判定を統一的かつシンプルに管理できるような仕組みが必要となります。 動的なルール追加 マーケティングシステムのキャンペーン判定や最適化処理のルールはエンジニアではなくビジネスサイドのメンバーによって定められます。そのためプログラムでルール判定をハードコードするのではなく動的にルールの追加や修正できることが必要となります。 高速な条件判定 システムの必須条件としてリアルタイム配信があります。それを実現するためには定義したルール判定を高速に行う必要があります。またメンバーフィルターのような機能はキャンペーン対象のメンバー一人ひとりに対して条件判定を行います。ZOZOTOWNで扱う商品数やユーザ数を考えると同時に発生するキャンペーンの数並びにキャンペーンの対象となるメンバー数は膨大になります。そこでそれらを高速に処理できるよな仕組みが必要となります。 技術構成 上記で紹介してきた機能要件・非機能要件を満たすシステムを構築するためAnalyzerの技術構成は以下のようになっています。 言語 Java 8 フレームワーク JBoss EAP(Java EE) JBoss Data Grid Red Hat Decision Manager JBoss EAP JBoss EAPはRed Hatが提供しているJava EEの実装です。Analyzerでは以下で紹介するJBoss Data GridやRed Hat Decision Managerと組み合わせて機能の実現を行っています。 Red Hat Decision Manager 「複雑なルール判定」並びに「動的なルール追加」を実現するために導入したのがRed Hat Decision Managerです。キャンペーン判定処理やユーザ抽出・メンバーフィルター・チャネル最適化のような処理はすべてルールベースで行っています。それらのルールはすべてRed Hat Decision Managerというルールエンジンによって定義・判定処理を行っています。 Red Hat Decision Mangerを利用することで、Javaで言うところの複雑なswitch文をExeclで表現できます。以下はデバッグユーザ以外を除外するルールを定義したものです(実際に使われているルールではありません)。 3行目がプログラムから渡って来るオブジェクトを定義しており、プログラムからDecision Mangerを呼び出すときに引数で値を渡すことができます。この3行目が実際の条件式になっておりJavaの式をそのまま書くことが可能です。 $param に5行目以降の値が入った状態で評価されます。それぞれオレンジの部分の条件を評価しそれがすべてtrue判定となると一番右のActionが実行されます。 この例では、 campaignId == 1 かつ memberId not in (1,2,3) の場合に配信対象外フラグを立てます。campaignIdが2の場合は配信対象外フラグが立つことはありません。また、このExcelで定義したルールはアプリケーションの再起動をすることなしに動的にロードできます。 JBoss Data Grid Analyzerアプリケーションで1番の肝となる技術がJBoss Data Grid(JDG)です。これによって「高速な条件判定」を実現しています。JDGはかんたんに言うとインメモリな分散キャッシュデータストアで、KeyValue形式でJavaのオブジェクトを直接保存します。JDGでは複数サーバーでクラスタを組み分散してデータを保持します。 JDGにはServer-Client modeとEmbeded modeがありますが、AnalyzerではEmbededモードを利用しています。Embeded modeはJavaアプリケーションとJDGを同一のJVMで動かします。Analyzerでは必要なデータをほぼ全てJDGのキャッシュとして保持しています。このとき以下の機能を組み合わせることですべてのデータへのアクセスを、アプリケーションがアクセスするメモリと同一メモリへのアクセスだけで完結させることが可能です。 アプリケーションの分散実行 キャッシュが自分のノードに存在するかの判定 これを使った、具体的な処理に関しては次の「メンバー抽出後のメンバーフィルタリング処理」で実例を紹介します。JBoss Data Gridについては以前に以下のブログにて詳しく説明していますので、合わせてご参照ください。 techblog.zozo.com メンバー抽出後のメンバーフィルタリング処理 どのようにして複雑な条件判定を高速に実現しているかの例として、メンバー抽出後のメンバーフィルタリング処理を擬似コードで紹介します。簡潔にするため、色々と省略しています。 メンバーフィルタリングの実行 int campaign_id = xxx; List<Integer> memberIds = [id1, id2, id3 ... ]; AdvancedCache<Object, Object> memberCache = JdgUtil.lookupCache(CacheNames.MEMBER); DefaultExecutorService des = new DefaultExecutorService(memberCache); task = new DistributedMemberFilteringService(campaign_id, memberIds); des.submitEverywhere(task, memberIds) 実際のメンバーフィルタリング処理 public class DistributedMemberFilteringService { public DistributedMemberFilteringService(List<Integer> memberIds) { this .campaign_id = campaign_id; this .memberIds = memberIds; } public void call() { List<Integer> localMemberIds = narrowDownToLocal(memberIds); List<Integer> offerMemberIds = new ArrayList<Integer>(); for (Integer memId : localMemberIds) { Member member = (Member)memberCache.get(memberId); MemberFilteringFact fact = new MemberFilteringFact(member); MemberFilterRule rule = new MemberFilterRule(fact); rule.fire(); if (!fact.isFiltered()) { offerMemberIds.add(memberId); } } 次の処理(offerMemberIds); } private List<Integer> narrowDownToLocal(List<Integer> memberIds) { List<Integer> localMemberIds = new ArrayList<Integer>(); AdvancedCache<Object, Object> memberCache = JdgUtil.lookupCache(CacheNames.MEMBER); JdgDistributionUtil localityChecker = new JdgDistributionUtil(memberCache); for (Integer memberId : memberIds) { if (localityChecker.isLocal(memberId)) { localMemberIds.add(memberId); } } return localMemberIds; } } メンバーフィルタリングはメンバー抽出処理によりキャンペーンの対象となりうるメンバーidの一覧が取得できている状態からスタートします。メンバーidはメンバーキャッシュというメンバーの情報を格納しているキャッシュテーブルのkeyとなります。 des.submitEverywhere(task, memberIds) を実行すると、memberIdsに含まれるmemberIdをkeyとして1つでも保持しているノード全てでtaskを実行します。 taskは、 des.submitEverywhere が呼ばれたタイミングで call() メソッドが実行されます。callメソッドの中で最初に narrowDownToLocal(memberIds) を呼び出しています。JDGの機能に指定したkeyが自分のノードで保持しているかどうかを判定するためのメソッドがあります。それを利用し narrowDownToLocal(memberIds) では、メソッドを実行しているノードが保持しているmemberIdのみになるようmemberIdのListを絞り込みます。これによって、絞り込まれたmemberIdでキャッシュを取得すると必ずアプリケーションと同じメモリからデータを取得できるようになります。 その後、絞り込んだmemberIdのリストをmemberIdごとにRed Hat Dicision Mangerのルールエンジンによってフィルタリングします。ルールの結果はfactのフィールドに保存されます。ルール判定が終わるとfilterに引っかからなかったid( !fact.isFiltered() )について次の処理に進みます。 課題 以上のようにリアルタイム配信の仕組みや各種最適化の紹介してきました。それらの機能はうまくいっているものばかりではなく様々な課題があります。現在課題となっているものの一部を紹介します。 リリースの問題 アプリケーションのパフォーマンスを最大化するために、アプリケーションとキャッシュを同一JVMで動作させていると紹介しました。これにより最も課題となるのがリリースになります。 アプリケーションを止めてしまうとそのノードのキャッシュも同時に失われてしまいます。そのためリリースは1台ずつアプリケーションを停止し、キャッシュを別ノードにリバランスしながら行う必要があります。 上記の方法で無停止リリースは可能ですがサーバーが複数台あるため現在3〜4時間ほどリリースに時間がかかっています。メンテナンスタイムを設けることでキャシュをダンプしアプリケーションをリリース、キャッシュをロードし直すといった方法も可能です。この方法であれば1.5時間とリバランスを行うよりも短時間ですみますが、それでも時間がかかります。また、アプリケーションに問題があったときのロールバックにも時間がかかってしまいます。 以上のことからAnalyzerのリリースは慎重に行う必要があり、リリースのサイクルが遅いといった問題があります。 スケーリングの問題 アプリケーションとキャッシュが同一JVMでのっていることはスケーリングも困難にしています。サーバーが増減した場合JDGはキャッシュのリバランスを行います。ただし、サーバーが増えるぶんにはいいのですがサーバーが減る場合適切にキャッシュをリバランスしてやらないとデータロストしてしまいます。またサーバーを減らしすぎた場合キャッシュがクラスター内のメモリにのり切らなくなってしまうと言った問題も発生します。 これらのことから現在RTMではオートスケーリングの仕組み使っておらず常にピークに備えたサーバーを用意して運用しています。 シングルAZ構成 現在AnalyzerシステムはあえてシングルAZ構成にしています。Analyzerで必要となるデータをすべてJDGのキャッシュにのせていると紹介しました。そのためメモリを大量に確保する必要があり、かなり大きめのインスタンス複数台でクラスタを構成しています。 JDGはサーバーが2台以上同時にダウンするとデータロストする設定にしています。そのため、マルチAZ対応を考えるとJDGクラスタをもう一式用意する必要があります。JDGクラスタをもう一式用意するコストと、AZ障害の発生頻度ならびに復旧時間を比較検討した結果シングルAZのほうが好ましいと判断しました。 しかしAZ障害発生時にはダウンタイムは避けられず、ユーザへの配信が滞ってしまいます。これはユーザへの価値提供を犠牲にしていることにほかなりません。 分散キャッシュの運用の難しさ 分散システムは普通のWebアプリケーションよりも格段に複雑で運用が難しくなります。実際に運用しているJDGクラスタがスプリットブレインを起こしたといったことがありました。 techblog.zozo.com また、JDGの情報がインターネット上に出回っていないため知見があまり得られないという問題もあります。あったとしても数年前の情報ということがほとんどです。このようにAnalyzerを運用し続けることだけでも大変な手間がかかっています。 ルールベースでの条件判定 現在キャンペーン判定やそれぞれの最適化処理はすべてヒューリスティックなルールベースで条件を決めています。ルールはすべてRed Hat Decision Managerで定めており、機械学習での判定処理などはほんの一部にしか適用できていません。 さらにRed Hat Decision Managerの導入理由としてビジネスサイドのメンバーが動的にルールを追加できるようにするためと紹介しました。しかし実際に運用してみるとルールで使うためのデータを参照できるようにする必要があったりと、ルールの追加のたびにプログラムの改修が高頻度で発生してしまっています。 リプレイス計画 以上のような課題を解決するために、RTMシステムの大幅なリプレイスを検討しています。まだ具体的な構成や進め方は決まっていませんが、現在の考えていることを紹介いたします。以下が検討しているアーキテクチャの概要です。 Analyzerのアプリケーションとキャッシュが同じJVMにのっている問題を解決するために何らかのデータストアを外出しすることが第1の目標となります。ただし、外出しすることで現在のパフォーマンスを維持できるかが問題になります。そのため、どのようなデータストアを利用するかと言ったことはまだ決められていません。また、今までAnalyzerの中でやっていた各処理をジョブキューの形にすることで柔軟なスケーリングを実現したいと考えています。 そして現在RTM専用となっていた配信処理の部分を、配信チャネルごとにモジュール化したいと考えています。こうすることでZOZOTOWNのトランザクション処理の中で配信が必要な場合、統一した仕組みで配信できます。 Trackerの部分ですが、現在リアルタイムデータ連携基盤というものを開発しています。それを利用することで差分データを取得できます。アクセスログについても独立した基盤の作成またはツールの導入を検討しています。 まとめ 今回リアルタイムマーケティングシステムについてご紹介しました。紹介したように現在ZOZOTOWNでは、リアルタイムマーケティングシステムのリプレイスを計画しています。この記事をよんでこんなアーキテクチャがいいのでは、もっとこんなことができるのではと思った方はぜひお話しましょう。 tech.zozo.com また、8/27にリアルタイムマーケティングシステムを含めMAの取り組みについてのイベントを行いますのでぜひご参加ください。 zozotech-inc.connpass.com
基幹システム部ブランド連携チームの三橋です。ZOZOTOWNとお取引きをさせていただいているテナント様とのデータ連携部の開発・保守運用を行っております。 TECH BLOGという事で技術的なところにフォーカスした記事が多いのですが今回はZOZOTOWNの主要なサービスでもある取寄せ商品を業務・システム面より(業務系強めの記事として)ご紹介させていただきます。なお、先日公開されました同チームメンバーのブログ ZOZOBASEの出荷データ連携を支えるAPI も併せてご覧いただけると幸いです。 取寄せ商品ってナニ? 取寄せ商品とは、ご注文受付後に販売ショップの店舗や倉庫から取り寄せる商品のことです。取寄せ商品は、カートに入れた時点で「取寄せ商品」と表示され発送までの期間が表示されます。 取寄せサービスを導入した経緯と意義 要はZOZOBASEに在庫がない商品でもテナント様の倉庫や店舗に在庫があれば、ZOZOTOWNから取寄せ注文ができるというサービスなのですが下記のような目的を持って生まれました。 顧客満足度の向上 お客様のZOZOTOWNに対する不満の第1位は『欲しい商品の在庫がない』こと。 ZOZOTOWNの倉庫であるZOZOBASEにある商品(在庫)が欠品した場合、次回の入荷があるまでお客様は購入できず機会損失となっていました。そこで、テナント様倉庫・店舗にて保持する在庫をお客様が購入できたらそれを解決できるのではと考えました。 テナント様全体での在庫消化率の向上 プロパー消化率(定価での販売割合)が上昇し、利益率の向上の一手につながります。 ZOZOTOWNの実在庫での販売フローと取寄せ商品での販売フロー ZOZOTOWNの実在庫での販売フローと取寄せ商品での販売フローを以下にまとめました。 ZOZOTOWNの実在庫での販売フロー (1)テナント様よりZOZOBASEへの出荷指示 (2)テナント様倉庫よりZOZOBASEへ商品を出荷 (3)ZOZOBASEにて商品を荷受検品しZOZOTOWNの販売可能個数(実在庫)となる (4)ZOZOBASEの在庫数分受注が付いた場合、在庫切れ状態となってしまう ZOZOBASE内にある実在庫でのみ商品購入可能であるため実在庫以上の受注は取れず機会損失となります。 ZOZOTOWN取寄せ商品フロー (1)テナント様倉庫・店舗在庫情報をZOZOTOWNへ連携し販売可能数(取寄在庫)となる (2)ZOZOBASE在庫が無くても販売可能数(取寄せ在庫)にて受注が可能となる (3)取寄せ在庫での受注後、毎朝倉庫・店舗スタッフ様宛に受注した商品情報をメールにて配信 (4)集配業者への集荷依頼情報を連携 (5)集荷スタッフがテナント様店舗へ商品集荷に伺う (6)商品集荷後ZOZOBASEまで商品を移送 (7)取寄せ注文に入荷した商品が引き当たりお客様へ発送処理。また、商品入荷が無く取寄せ期限を過ぎてしまった場合は取寄せ不可となり注文のキャンセルと共にお客様へお詫びのメールを通知。 ZOZOBASE内の実在庫を売り切った場合、取寄せ在庫での購入が可能となりテナント様の倉庫や店舗の在庫で受注をとれるため 『欲しい商品の在庫がない』 が解消され 顧客満足度の向上につながりテナント様全体での在庫消化率も向上 といった効果が見込めます。 欠品対策について 取寄せ商品のためテナント様倉庫、店舗にて同タイミングで注文がついて欠品となる場合もあります。 テナント様へのご連絡(以後「配分」と呼ぶ)後、取り寄せまでの期限を超過してしまったものが取寄せ不可になるのは理解できるけど、その期限までただ待つだけ?欠品対策は?(と思った方は鋭いです。是非 こちら まで。) ここからは、そのような課題の中でも、欠品対策について紹介します。 欠品対策について 取寄せ商品の受注よりテナント様への配分は2回実施 初回配分から2日後までに入荷が無ければ、3日目に初めに配分した店舗以外の他倉庫・店舗に配分されます。要求のリトライ処理という位置づけです。 配分フローの簡単なイメージを図で示します。 ※キャンセルメール・・・N+5日までに検品完了されなければ、お客さまに自動配信 取寄せ商品の入荷は優先で検品 取寄せ商品が納品されたものの検品作業が遅れてしまい入荷期限切れでキャンセルされてしまう事もあるため、ZOZOBASEでは取寄せ商品用の検品作業は優先的に行われます。 販売可能数には閾率を掛けて販売 閾率とは販売してはいけない数量を算出する割合です。 テナント様倉庫、店舗にて購入等での欠品リスク防止のため閾率を掛けたもので販売可能数が決まります。 テナント様の在庫コントロールも各々で違ってくるため決定方法は2パターンから選択可能としました。 ※SKU=とある商品の1つのカラー・サイズの組み合わせ 監視について 当然ながら監視も行っています。 残念ながら詳しい画面はお見せできませんが各倉庫、店舗へ配分された回収率の推移を可視化して監視しており、回収率が低い店舗が出てきた場合は弊社営業とテナント担当者様で協議し、閾率の見直しや優先店舗のコントロール、店舗スタッフ様への伝達等をして改善していく運用をしています。 また、閾率の設定、ヒューマンエラーによるミスが大量キャンセルなどの障害にも繋がってしまうためアラート監視を実施しており、何か異常があれば都度対策を講じています。 テナント様が取寄せサービスを導入するまでのハードル この取り寄せサービスはZOZOTOWN側だけの準備・設定だけでは実現できません。 以下のようにシステム面・運用面でテナント様にご協力をいただく必要があります。 テナント様側のシステムに関する準備 テナント様倉庫・店舗在庫の情報を連携する必要があります。自動化していない場合には管理画面上で都度アップロードしてもらう運用が発生します。そのあたりの運用を自動化できるようAPIの提供もしていますが、APIを利用する場合はテナント様側の開発が必要となってきます。 店舗のオペレーション変更 毎朝取寄せ注文の連絡が配信され、店舗スタッフは受信した商品情報をもとに取寄せ商品を確保し、当日配送業者の集荷スタッフに商品を渡して頂きます。 普段の店舗での業務に加えてZOZOTOWNの取寄せサービスに関する業務も増えるため、店舗オペレーション再整理と店舗スタッフ様への落とし込みが必要になります。 店舗スタッフのマインドの切り替え 「店舗の在庫を取られる」と思われがちなため、サービス開始時には実際にそのような反発を耳にすることもありました。 ZOZOTOWNとしてはしっかり取寄せの売上実績を作ること、テナント様には店舗の売上成績に取寄せ商品の回収実績を加味していただいたりするなど、地道な努力の結果、取寄せに対する認知が進み、導入実績も増加しています。 まとめ ZOZOTOWNの客注実施ショップは全体の3割を超えてきており、取寄せ対象の店舗は数千店規模になっています。 取寄せ注文の売上比率も伸びており、取寄せ導入時の課題解決に大きく寄与できています。しかし、まだまだ伸びしろのあるサービスであるため、今後もなるべくリードタイムをZOZOBASEの在庫販売に近づける工夫や、欠品によるご迷惑をおかけしないような仕組みをアップデートし、より多くのお客様・テナント様にご利用いただけるよう改善をしていきます。 さいごに 前述のとおり、導入する準備としてテナント様にも多分にご協力をいただく必要があるサービスではありますが、導入や課題解決に関するノウハウはかなり蓄積されています。そのため、しっかりとしたサポートも可能です。取寄せサービス含め、データ連携にご興味を持たれたテナント様がおられましたらZOZOTOWNの営業までお気軽にお問い合わせください。 ZOZOテクノロジーズでは、今回紹介したような裏側の仕組み作りや、運営する様々なサービスを一緒に作り上げていただける方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。 https://tech.zozo.com/recruit/ tech.zozo.com
こんにちは。SRE部BtoBチームの竹田です。本記事では、クラウドインフラ環境のセキュリティ対策を講じようと思いつつも何から着手すれば良いのか分からないという方向けに、マルチクラウドに対応したオープンソースのセキュリティ監査ツールであるScout Suiteを紹介します。 Scout Suiteとは Scout Suiteはマルチクラウドに対応したオープンソースのセキュリティ監査ツールです。各クラウドプロバイダーから公開されているAPIを利用してクラウドの設定情報を収集し、リスクとなる項目をHTML形式のレポートファイルで出力してくれます。マルチクラウドとあるように2020/08/07時点では以下のクラウドサービスに対応しています。 Amazon Web Services Microsoft Azure Google Cloud Platform Alibaba Cloud (alpha) Oracle Cloud Infrastructure (alpha) 参考: nccgroup/ScoutSuite: Multi-Cloud Security Auditing Tool AWSに対しての実行 今回はCentOS 7.8環境にScout Suiteの実行環境を整えます。まずはAWSに対してScout Suiteを実行してみます。Scout Suiteの実行までにはいくつか準備が必要となるため、順を追って説明します。 必要なパッケージのインストール Scout SuiteはPythonで記述されており、Pythonとpipが必要になります。Pythonのバージョンは3.5以上をサポートしています。 $ yum install python3 python3-devel python3-pip Scout Suiteのインストール GitHubからScout Suiteの本体をダウンロードします。2020/08/07時点の最新バージョンは5.9.1でした。 $ git clone https://github.com/nccgroup/ScoutSuite.git ScoutSuiteディレクトリに移動して、必要なパッケージをインストールします。 $ cd ScoutSuite $ pip3 install -r requirements.txt AWS CLIのインストール AWS CLIをダウンロードします。 $ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" zipを解凍してインストールします。 $ unzip awscliv2.zip $ sudo ./aws/install AWS CLIの初期設定をします。 $ aws configure AWS Access Key ID [None]:{アクセスキーID} AWS Secret Access Key [None]:{シークレットアクセスキー} Default region name [None]:ap-northeast-1 Default output format [None]:json 以下のポリシーはScout Suiteの実行に必要な権限を保有しているため、このポリシーを関連付けたIAMユーザーのアクセスキーIDおよびシークレットアクセスキーを設定します。 ReadOnlyAccess SecurityAudit 最低限必要な権限に絞ったカスタムポリシーもあるようです。 AWS Minimal Privileges Policy Scout Suiteの実行 ScoutSuiteディレクトリ内に移動してscout.pyを実行します。 $ python3 scout.py aws 以下のように監査が進行していくので、監査が完了するまでしばらく待機します。 2020-07-21 08:40:46 hostname scout[19403] INFO Launching Scout 2020-07-21 08:40:46 hostname scout[19403] INFO Authenticating to cloud provider 2020-07-21 08:40:51 hostname scout[19403] INFO Gathering data from APIs 2020-07-21 08:40:51 hostname scout[19403] INFO Fetching resources for the ACM service 2020-07-21 08:40:51 hostname scout[19403] INFO Fetching resources for the Lambda service 2020-07-21 08:40:52 hostname scout[19403] INFO Fetching resources for the CloudFormation service 2020-07-21 08:40:53 hostname scout[19403] INFO Fetching resources for the CloudTrail service 2020-07-21 08:40:54 hostname scout[19403] INFO Fetching resources for the CloudWatch service 2020-07-21 08:40:55 hostname scout[19403] INFO Fetching resources for the Config service 2020-07-21 08:40:55 hostname scout[19403] INFO Fetching resources for the Direct Connect service 2020-07-21 08:40:56 hostname scout[19403] INFO Fetching resources for the EC2 servicea . . . 監査が完了すると、ScoutSuiteディレクトリの下にscoutsuite-reportディレクトリが生成されます。このディレクトリの中身がレポートファイル一式になっています。 レポート結果の確認 scoutsuite-reportディレクトリの中のHTMLファイルをブラウザで開くと以下のようなレポート結果が確認できます。 サービス毎に監査項目の数や発見された問題の数がリストされています。一番左側のアイコンが緑色の場合は問題無し、アイコンが黄色や赤色の場合は問題ありです。EC2の項目が赤くなっているので確認してみます。 EC2の設定に対する問題点がリストされています。各項目の右側にあるプラス記号をクリックすると、項目毎の詳細を確認できます。Security Group Opens SSH Port to Allの項目のプラス記号をクリックしてみます。 SSH(22)という攻撃の格好の餌食となるポートがフルオープンになっていると書いてあります。また、22番ポートのオープンが必要な場合はIP制限すると攻撃を受ける可能性が減少するといったアドバイスもあります。プラス記号ではなく項目名自体をクリックすると、問題が見つかったセキュリティグループの設定情報を確認できます。 アウトバウンドルールやインバウンドルールの設定内容に加え、対象のセキュリティグループの利用状況も確認できます。問題となる設定は赤くハイライトされており、確かに22番ポートがフルオープンになってしまっているようです。アドバイスに従ってIP制限を施したり、22番ポートを利用していなければルールごと削除するのも手です。そもそもセキュリティグループが利用されていないようであればセキュリティグループごと削除してしまうと良いでしょう。 GCPに対しての実行 マルチクラウドに対応しているということで、GCPに対してもScout Suiteを実行してみます。必要なパッケージとScout Suite本体の準備は整っているものとして、GCP独自で必要な準備について説明します。 サービスアカウントの作成と鍵ファイルの取得 サービスアカウントを作成してJSON形式の鍵ファイルを取得しておきます。サービスアカウントに付与するIAMロールは以下のものです。 閲覧者(Viewer) セキュリティ審査担当者(Security Reviewer) Stackdriverアカウント閲覧者(Stackdriver Account Viewer) Cloud Resource Manager APIの有効化 監査するプロジェクトのCloud Resource Manager APIを有効にしておく必要があります。Cloud Resource Manager APIが無効の場合、一見問題なく監査が進みますが、監査対象が0という悲しいレポート結果が出力されてしまいます。 Scout Suiteの実行 ScoutSuiteディレクトリ内に移動してscout.pyを実行します。--service-accountには先ほど取得しておいた鍵ファイルを指定します。 $ python3 scout.py gcp --service-account {/path/to/key.json} AWSとGCPのレポート結果の違い 監査対象となるサービス数や監査項目の数はAWSの方が多く、また以下のような点からもAWSの方が良くメンテナンスされているのかなという印象を受けました。 AWSだとSSHやRDPなどポート毎に項目分けされていたものが、GCPだとウェルノウンポートでまとめられている GCPのファイアウォールの設定で22番ポートがフルオープンでも黄色(Warning)判定される まとめ AWSにはTrusted AdvisorやConfig、GCPにはRecommenderといった同様機能を持つサービスがあります。しかし、Scout Suiteは異なるクラウド環境を横断して、統一したオペレーションでレポートを出力できるメリットがあります。そのため、特に複数のクラウド環境を運用している方にとっては、Scout Suiteの利用が運用の省コスト化につながるのではないでしょうか。また、機能開発も頻繁に行われており、監査対象となるサービスや監査項目の充実も予定されているようです。執筆時点においてSRE部BtoBチームではまだ本格導入には至っていないですが、引き続き評価しつつ今後のバージョンアップに期待しています。 さいごに 今回紹介したScout Suiteの監査項目はごく一部であり、他にも多数の監査項目があります。オープンソースのScout Suiteを利用すれば、クラウド環境のセキュリティ監査を無料で実施できるので、まずは一度ご自身のクラウド環境の監査を実施してみてはいかがでしょうか。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
はじめに こんにちは、計測プラットフォーム部バックエンドチームの高木( @TAKAyuki_atkwsk )です。 ZOZOMAT システムではAPIのリバースプロキシとして Envoy および付随するgRPC-JSON transcoderを導入しています。これらによって依存するサービスにレガシーなサーバーが存在していても部分的に gRPC を採用しモダンなアーキテクチャを広めようとしていることについて紹介します。 なお、本記事は以下記事のシリーズとなっておりますので合わせてご覧いただけるとZOZOSUITやZOZOMATなど計測プラットフォーム部の取り組みについてより深くご理解頂けるかと思います。 techblog.zozo.com ZOZOMATシステムとEnvoyについて ZOZOMATシステム構成におけるEnvoyの役割について簡単に説明しておきます。クライアント(ZOZOTOWNアプリ等)とZOZOMAT APIサーバー(以下ZOZOMATサーバー)の間にリバースプロキシとしてEnvoyを配置し主に以下の役割を担っています。 TLSの終端 ルーティング gRPCリクエストの負荷分散 HTTP JSON APIとgRPCの相互変換 gRPCをシステムに導入する際、負荷分散の観点でZOZOMATサーバーの前段にEnvoyを置く構成を想定していました。以下の記事がZOZOMATシステムの構成として似ているものだったので参考にさせていただきました。 Envoy プロキシを使用して GKE 上で gRPC サービスの負荷分散を行う  |  ソリューション  |  Google Cloud Amazon EKSでgRPCサーバを運用する - 一休.com Developers Blog 先ほど挙げたEnvoyが担当する役割の中の「HTTP JSON APIとgRPCの相互変換」機能、これがgRPC-JSON transcoderなのですが詳しくは記事の後半で触れていきます。ちなみにリバースプロキシとしてはnginxも候補として有力ではありますが、上記の事例に後押しされてEnvoyを使うことにしたという経緯がありました。 また、ZOZOMATのシステム構成についてはこちらの記事で詳しく説明されていますので合わせてご覧ください。 techblog.zozo.com ZOZOSUITを振り返って ZOZOMATでの取り組みを紹介する前に ZOZOSUIT での課題について触れておきます。ZOZOSUITはZOZOMAT以前に発表された体のサイズを測るツールおよびシステムで、このシステムはZOZOTOWNアプリ、ZOZOTOWNサーバーと連携します。 ZOZOSUITで扱うデータの中に3Dデータというものがあります。これは、計測時にZOZOTOWNアプリで生成されてZOZOSUITサーバーに送られます。また計測結果を参照する際にもZOZOSUITサーバーからZOZOTOWNサーバーを経由し最終的にZOZOTOWNアプリに送られます。 この3Dデータはサイズが大きいためデータ送信で少なからず時間がかかってしまうことが課題でした。 また、データのシリアライズ方法に関してはZOZOTOWNアプリとZOZOSUITサーバー間ではmsgpack、ZOZOTOWNサーバーとZOZOSUITサーバー間ではJSONを利用していました。このため、サーバー側の開発者はmsgpackで扱うための実装やJSONで扱うための実装をエンドポイントごとに意識する必要がありました。 以下の図では各コンポーネントとプロトコル・シリアライズ方法の関連を示しています。 ZOZOMATでの解決策 ZOZOMATでもZOZOSUIT同様の3Dデータを利用するため、前述の問題を解決しておく必要がありました。そこで、ZOZOMATではgRPC + protocol buffersを利用することにしました。 大きなサイズのデータ送信に関して検証するためシリアライズ方式によるデータサイズの比較を簡単に行いました。今回は3Dデータを表す以下のようなデータを利用し、それぞれの方式でシリアライズを行いました。 { " faces ": [ [ 1444 , 1453 , 1435 ] , [ 1444 , 1435 , 1416 ] , ... ] , " verticies ": [ [ 0.19088252916100196 , -0.025855744239442646 , 0.0743707061820768 ] , [ 0.1902995247773912 , -0.029133848767236424 , 0.07462623400745416 ] , ... ] , " rings ": [ [ [ 0.06820038723992675 , 0.01848489483093979 , -0.37634277191524135 ] , [ 0.06906485745721565 , 0.02409594156229787 , -0.3780734263629579 ] , ... ] , ... ] } 結果は以下の通りで、JSONと比較してmsgpackとprotocol buffersはそれぞれ約半分のサイズになることが分かりました。 方式 サイズ(bytes) JSON 431,413 msgpack 210,388 protocol buffers 211,895 msgpackとprotocol buffersではサイズに大きな違いは見られませんでした。ただ、gRPCを利用することでHTTP/2のヘッダー圧縮など効率的な通信が見込まれるため全体としてはデータ送信について更なる改善が期待できるという判断になりました。 また、protocol buffersのメッセージとサービスを定義するprotoファイルをバックエンドおよびアプリ開発者の間で共有しAPI定義の連携漏れを防ぐことができる利点もありました。さらに、protocコマンドによってprotoファイルからバックエンド・iOS/Androidアプリ開発用言語に対応したインタフェースのソースコードが自動生成されます。そのためスキーマ変更によるメンテナンスコストも低くなった印象でした。 gRPC導入にあたっての課題 gRPCを導入してめでたしめでたし。とはいかないもので、ZOZOTOWNアプリではgRPC実装ができる見込みだったのですが連携するZOZOTOWNサーバーではgRPCの実装が厳しい見込みでした。ZOZOTOWNサーバーは現在VBScriptで構成されるレガシーシステムであり、リプレイスを進めているためです。結果、ZOZOTOWNサーバーとZOZOMATサーバーの通信方式については課題が残った状態でした。 しかし、これらの検討を行いながら調査していた時にEnvoyの機能の1つに gRPC-JSON transcoder というものがあることに気がつきました。これは、HTTP JSON APIクライアントがEnvoyにリクエストを送るとgRPCサービスにプロキシしてくれるものです。 ZOZOTOWNアプリからはgRPCで、ZOZOTOWNサーバーからはJSON APIでリクエストされます。途中にあるEnvoyでJSONのリクエストをgRPCに変換してくれるのでZOZOMATサーバーはリクエストをgRPCで統一して扱うことができるようになります。以下の図は各コンポーネントとそれぞれの間のリクエスト形式の関係になります。 似たようなアプローチのツールの1つに grpc-gateway があります。こちらもHTTP JSON APIリクエストをgRPCに変換して背後にあるgRPCサーバーへプロキシするものです。以下の図にgrpc-gatewayを利用する構成の例を示します。gRPCとHTTP JSON APIでリクエストの経路を分離する場合やEnvoyではないコンポーネントを使う場合は良いかもしれません。一方ZOZOMATシステムの場合、両者とも同じ経路となるためEnvoyを利用、かつgRPC-JSON transcoderを有効にした構成となっています。 gRPC-JSON transcoderの利用方法 HTTP JSON互換にするためにはprotoファイルに設定を追加する必要があります。ここではZOZOMATで足のサイズを測ると見れるようになるシューズの相性度 1 を取得するAPIを例として紹介します。以下にシューズの相性度を取得する疑似rpcを定義したprotoファイルを示します。 // シューズの相性度を取得するrpcのサンプル定義 syntax = "proto3" ; package shoes; // importする必要がある import "google/api/annotations.proto" ; service Shoes { rpc ListMatchingRates (ListMatchingRatesRequest) returns (MatchingRates) { // HTTP JSONでやり取りしたい場合はここを追加する option (google.api.http) = { get: "/shoes/{item_id}/sessions/{session_id}/matching-rates" }; } } message ListMatchingRatesRequest { string item_id = 1 ; string session_id = 2 ; } message MatchingRates { string item_id = 1 ; string session_id = 2 ; repeated MatchingRate matching_rates = 3 ; } message MatchingRate { string size_label = 1 ; float rate = 2 ; } ここでのポイントはrpcに対して option (google.api.http) を追加してHTTP JSON APIエンドポイントの定義を行うことです。上記の例ですと https://[domain]/shoes/123abc​/sessions/abc456/matching-rates というエンドポイントに対してGETでリクエストできるようになります。gRPC-JSON transcoderを経由したレスポンスは以下のように MatchingRates のデータ構造がJSON化されたものになります。 { " itemId ": " 123abc ", " sessionId ": " abc456 ", " matchingRates ": [ { " sizeLabel ": " 25 ", " rate ": 0.9543 } , { " sizeLabel ": " 24.5 ", " rate ": 0.8561 } ] } これらの設定をgRPC-JSON transcoderに渡すためには、protocでdescriptor setというものを生成しEnvoyの設定で参照します。詳しくはこちらの ドキュメント を参考にしてください。 gRPC-JSON transcoderを採用しての振り返り 採用してみて良かったポイントを挙げてみます。 gRPCを使って通信する前提で設計や実装が可能になった ZOZOTOWNサーバーのようなgRPC未対応のクライアントからでもHTTP JSON APIでアクセス可能になった HTTP JSON API用のエンドポイントをZOZOMATサーバー側に実装する方法ではリクエストボディを処理で扱うデータ型に変換する部分がgRPCとJSONの2種類考える必要があります。一方、リクエストがgRPCのみになるとその部分については考えなくてよくなるため実装がよりシンプルになります。 さらに、HTTP JSON APIにおいてもprotocol buffersによってインタフェースの定義を管理できる恩恵を受けられます。 また、将来的にZOZOTOWNサーバーがgRPC対応するという場合を考えると、gRPC-JSON transcoderの設定やprotoファイル上から google.api.http オプションを削除する必要がありますが、ZOZOMATサーバーの実装およびEnvoyの設定は何一つ変える必要がないため移行もスムーズになると考えられます。 一方、注意すべきポイントとして int64 で扱う値はJSONのレスポンスで数値を表す文字列の値に変換される点です。こちらの ドキュメント にprotocol buffersとJSONの対応表が載っており、JSONのレスポンスはこれに沿う形で出力されます。 2 さいごに 今回はEnvoyのgRPC-JSON transcoderを利用して部分的にHTTP JSON APIを生かしながらgRPCでコンポーネント間通信を実現することを紹介しました。個人的にはEnvoyを導入するのは初めてだったので、今のところZOZOMATシステムの構成でうまく機能していてホッとしています。何らかの制約により一部のコンポーネントでgRPCが導入できない場合でもこのような相互変換する層があると全体としてうまくいくというのは、gRPCに限らず他の技術においても応用できそうだなと思います。 計測プラットフォーム部バックエンドチームでは、ZOZOMATでより精度の高いサイズを推奨するバックエンドエンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com 相性度とは、サイズ感に満足できる確率です。足のサイズが同じ人でも、シューズを履いた場合に、どのサイズがピッタリと感じるかは、シューズの形、足型、締め付け感や、ゆとりの好み等により異なります。そこでZOZOではZOZOMATの計測データ、シューズデータ、お客様にお答えいただいたアンケートデータなどを元に、お客様がそのシューズを履いた場合に、サイズ感に満足いただける確率を「相性度」として、サイズ別に表示しています。(ZOZOTOWN >【ZOZOMAT】相性度の高いシューズ ページの説明より引用) ↩ When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the proto to JSON conversion must follow the proto3 specification. ( https://github.com/googleapis/googleapis/blob/master/google/api/http.proto#L288-L290 より引用) ↩
こんにちは、ZOZOテクノロジーズ CTO室の池田( @ikenyal )です。 ZOZOテクノロジーズでは、7/22に ZOZO×一休×PayPay AWS Night を開催しました。 zozotech-inc.connpass.com ZOZO×一休×PayPay AWS Nightは、ZOZOテクノロジーズ・一休・PayPayの3社による合同イベントです。 登壇内容 まとめ 弊社のエンジニア2名、一休とPayPayからもエンジニアが1名ずつ登壇し、各社での日頃のAWSの活用事例の紹介を行いました。 Fulfillment by ZOZOと中国版ZOZOTOWNでのAWS活用事例 (ZOZOテクノロジーズ:岡元 政大) AWS Single Sign-Onを用いた、セキュアでより良いログイン体験への取り組み (ZOZOテクノロジーズ:光野 達朗 / @kotatsu360 ) 一休のリアルタイム施策を支えるサーバレスログ基盤 (一休:清水 一輝) PayPayでのAWS活用事例について (PayPay:西中 智樹 / @_tomoki_n ) Fulfillment by ZOZOと中国版ZOZOTOWNでのAWS活用事例 AWS Single Sign-Onを用いた、セキュアでより良いログイン体験への取り組み 一休のリアルタイム施策を支えるサーバレスログ基盤 PayPayでのAWS活用事例について 最後に ZOZOテクノロジーズ、一休、PayPayの各社で、AWSを用いたプロダクト開発を行うエンジニアを募集しています。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com www.ikyu.co.jp about.paypay.ne.jp
はじめに こんにちは。ZOZOテクノロジーズBtoB開発部の三好です。 今回はServerlessなシステムの監視、保守、運用を行う中で、これまでやってきたことや、その反省点などを紹介します。マネージドサービスでの開発・運用で同じ苦悩をされている、もしくはこれからマネージドサービスを利用して構築する方々の参考になれば幸いです。 サービス紹介 私の所属するBtoB開発部は、 Fulfillment by ZOZO (以下FBZ)を提供しています。FBZはZOZOBASEと自社EC、店舗の在庫を一元化し、在庫切れの無い世界を実現するためのサービスです。 その中で、ZOZOBASEと自社ECをつなげるためのデータ連携API開発を担っています。 なぜ監視をするのか 私たちは、日々APIが正常に稼働しているか、不正なデータが発生していないかを監視してます。マネージドなサービスを使って、Serverlessなシステムと聞くと、保守は不要と思われる方もいらっしゃるかもしれません。 しかし、想定していない運用によるイレギュラーデータへのリカバリ対応や、プログラムのバグはありえます。さらに、高負荷による処理遅延の発生などシステムの監視、保守は必要になります。 安定したユーザー体験を実現するために私たちは日々システム監視を続けています。 監視の基本的な仕組み 私たちはAPIサービスを開発し運用をしておりますが、FBZのAPIは、AWSのLambda + API Gateway + その他マネージドサービスにて構成されています。その中でも今回はLambdaのログ監視について紹介させていただきたいと思います。 Lambdaでは、CloudWatch Logsにログを出力してます。Lambda内でINFO、WARNING、ERROR等のログレベルを設定した上でCloudWatch Logsにログ出力をするような仕組みになっています。 CloudWatch Logsではロググループに対し「サブスクリプションフィルタ」を設定できます。サブスクリプションフィルタにLambdaを設定すると、出力されたログをLambdaで処理する事ができます。 FBZのAPIではこの機能を使ってERRORログを検知し、検知したERRORログをPagerDuty、Datadogに通知する仕組みを組んでいます。 これをベースにエラーを検知し監視対応を行っています。 運用で発生した課題 この仕組みでしばらく運用した結果「エラーのノイズが多く対応しきれない」といった課題が発生しました。 サービスの成長と共にエラーが増大していきました。一番多い時では一日に起票されるエラーが何百というレベルで、常にPagerDutyの通知がされているような状態でした。 こういった状況が続くと以下のような状態になってきます。 数が多すぎる上に、ほとんどノイズなので次第に見なくなる 本当に対応すべきエラーが埋もれる リカバリや修正対応が遅くなり、外部や運用者に迷惑をかけてしまう エラーノイズが増えてしまっていた原因 なぜノイズが増えてしまっていたのかを考察した結果、主な原因は以下の3つでした。 そもそものログレベル設定が間違っている ログレベルの変更で対応できないエラーの存在 ノイズをフィルタをすることへのハードルが高い 1. そもそものログレベル設定が間違っている WARNINGやINFOで良いものまでERRORで出力されている状態でした。例外が発生した場合はほぼ全てがERRORログとして出力されている状態で、適切なログレベルが設定されている状態ではありませんでした。 2. ログレベルの変更で対応できないエラーの存在 Lambdaに記述したコード内でハンドリングできているエラーの場合は適切にログレベルが設定され通知されます。しかし、開発者がエラーハンドリングできない部分で発生したエラーについては、そもそもログレベルが設定できません。 例えば、メモリエラーやタイムアウトエラー、importエラーなどがこれに該当します。これらはログレベルが設定できないエラーであるため、発生を知らせる特定の文字列を元に検知する仕組みでした。 Lambdaのタイムアウトであれば、 Task timed out after X seconds が検出されたらエラーとして通知するといった仕組みでした。そのため、Lambda関数毎に検知する、しないの制御ができていませんでした。 3. フィルタを適用することへのハードルが高い 元々エラー検知されてノイズと判定されたものは、PagerDutyのフィルタ機能を使ってフィルタを行なっていました。 しかし、PagerDutyのフィルタではシンプルな条件でしかフィルタをできないため、特定のLambdaの特定のエラーのみフィルタするなど細かな制御ができませんでした。 また、PagerDutyでは実際にフィルタされたものの確認ができません。本来検知するべきだったエラーまでフィルタしてないだろうかという不安が常にありました。 上記のような理由から、PagerDutyのフィルタ機能は積極的には利用されず、結果ノイズとして残ってしまっている状態でした。 解決に向けたアクション これらの課題を解決するために、以下の2つのアクションをとりました。 ログレベルの見直し フィルター機能の強化 根本的な解決としては、ログレベルを適切に設定する事を目標としました。ただ、ログレベルの見直しでは改善できないノイズを削減するために、フィルタ機能を強化するアクションも合わせて行いました。 ログレベルの見直し まず行ったのは、ログレベルの見直しです。ほぼ全てがERRORとして出力されている状態から、ノイズとなっているログのログレベルを見直し、PagerDutyへ通知されないようにしました。 フィルター機能の強化 ログレベルの見直しでは解決できないノイズを削減するためにフィルタ機能の強化を行いました。大まかなアーキテクチャとしては以下の図のようになります。これまでERROR文字列を検知してPagerDuty、Datadogに通知していた機能を拡張し、この部分にフィルタ機能をもたせました。 フィルタのためのロジックはPythonで記述しています。当初設定値としてコード外で管理する案もあったのですが、自由度と後述する自動テストを入れるという観点からPythonにてロジックを記述しています。 また、エラーをフィルタするハードルを下げるために、工夫した点が2つあります。 1点目はフィルタのロジックに対して自動テストを書いていることです。フィルタをする際、本来通知されるべきものまでフィルタしてないか不安になります。ロジックの正当性を担保するため、フィルタの追加と共に自動テストも追加しました。 2点目はフィルタした結果が分かるようにしたことです。フィルタを導入した後に想定している件数とのギャップがないかを確認する目的と、フィルタした件数をLambda毎に集計し異常値検出をする目的です。 運用から得た教訓(みんなの安眠のために) 上記のようなアクションをとった結果、最大500件近く起票されていたエラーを少ない日は数件という程度に抑えることができました。 そして、運用をしていく中で以下の教訓を得ることができました。 エラー出力をする前に、なんのために必要なのか考えないといけない エラーが発生したが、再実行やリトライによって成功する(成功した)エラー 手動でデータ補正等を行う必要があるエラー 再実行やリカバリは不要・できないが、見逃せないエラー(バグなど) etc. 様々なパターンがありますが、よく自分も陥りがちなのは、「色々な例外がありそうだからとりあえずERROR出力をする」といったパターンです。とりあえず設定したログレベルは、一度リリースされると改修による不具合発生のリスクや、リソースの都合でななかな変更できない事が多いと思います。 本当に検知や対処が必要なものであればERROR出力は必要です。しかし、ERROR通知を受けた後何もしないのであれば、通知する必要はないのかもしれません。実装の時点で、検知したエラーを元に何をしたいのかを考慮した上で適切なログレベルをつける事が大切です。 ノイズは放置しない ノイズが多いという場合、ほとんどが急に発生したものではなく時間をかけて蓄積されたものです。少ないノイズだからと放置すると、いつの間にかノイズだらけ、という状態になります。 ただ、ノイズとして残っているものがある場合、除去できない理由があるのかもしれません。今回はフィルタを作成する事でノイズを除去する事ができました。 今ノイズとして残っている理由を見極め、効果のあるところから地道に改善していくという事の大切さを実感しました。 これからやっていきたいこと 今回はノイズへの対策をメインに紹介させていただきましたが、これからやりたい事はたくさんあります。 エラー発生後に自動でリカバリをする仕組みの構築 インシデントの適切な優先度付け SREチームとの連携 上記にあげたような、そもそもエラーとして検知しなくて済む仕組み作りや、検知後の運用がスムーズに回る仕組みなどに注力できればと考えています。 現状監視できていない部分や、そのリスクを把握しながら、逐次改善のアクションへとつなげていきたいです。ちょっとした改善で劇的に変わるものもあれば、なかなか手強い解決できない課題もあります。日々挑戦しながら、改善を進めていきたいと思っています。 さいごに 私たちは、AWSのマネージドサービスをフル活用し、ServerlessなAPIサービスを提供しています。失敗と成功を繰り返して、エンジニアとしてのスキルアップを目指しています。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
作った経緯 AWS料金Botの機能 実装 気をつけるべき点 Lambdaのコードをアップロードする際の問題 Serverless Application Modelの採用 CloudFormationとServerless Application Modelの比較 まとめ 最後に こんにちは。今年の4月に新卒で入社し、SRE部MA基盤チームに配属された川津( @jon_ground )です。 MA基盤チームではMAで利用しているインフラの使用料金が把握できていない問題があります。そこで気軽に料金を確認できるようにAWSの料金をSlackに報告してくれるBotを作成しました。本記事では上記の問題を解決するため、Botを作成するまでに至った経緯や数ヶ月運用して得られたメリットについて紹介します。 作った経緯 MA基盤チームでは、MA関連のインフラをAWS上で構築しており、開発、ステージング、本番と環境毎にAWSのアカウントを分けて運用しています。AWSの使用料金を確認する際、都度料金を確認するために各環境のAWSのコンソールからCost Explorerを確認する必要があり非常に面倒な作業でした。またCost Explorerを閲覧するためには強い権限が必要であり、確認できる人が限られていました。これによって料金が上がった部分を見落としてしまい、検証用のインスタンスを落とし忘れてしまったり、必要以上にAWSのリソースを使用してしまったことがありました。 こうした無駄な消費を避けるためにチームの人が毎日確認でき、リソース毎の使用料金も確認できるようにする必要がありました。サービスを運用する上でインフラのコストを意識し、妥当な料金であるか把握することは大事です。 AWS料金Botの機能 現状の問題点を解決するためには以下の要件を満たす必要がありました。 チームの人が毎日AWSの料金を把握できるようにする AWSの強い権限を持っていない人でも料金を確認できるようにする 上記の要件を満たすためにMA基盤チームが利用しているSlackのChannelに料金の情報を流すことにしました。理由としては対象のChannelで勤怠の確認を行っており、毎朝チーム全員が必ずSlackを見るからです。また、ZOZOテクノロジーズのSlackは原則オープンチャンネルなのでMA基盤チーム以外の人も見ることができ、強い権限を持っていなくてもAWSの料金を確認できます。 Slackに料金の情報を表示する方法に AWS チャットボット を利用する方法もあります。しかしリソース毎に料金が知りたい点とリソースの使いすぎを防止するために一昨日と昨日の料金の差分を表示し、利用料金がどれだけ変化したのかを知りたい要望があり、今回の要件には合わないためBotを自作することにしました。 作成したアプリケーションの機能としては、平日朝10時に対象のSlack Channelに各環境毎の使用料金をまとめたメッセージを送ります。また、使用料金の内訳も表示されます。全リソースの使用料金を表示するとメッセージが長くなるので、1USD以下の金額のリソースはその他の金額にまとめています。 実装 今回使用した言語、ツールは下記のものです。 言語:Go 1.14 プロビジョニングツール:CloudFormation(CFn)、Serverless Application Model(SAM) アーキテクチャーは下記の通りです。 作成されたアプリケーションはLambda上で動作しており、CloudWatch Eventsでcronを平日10時に設定してLambdaを動かしています。また、AWSの料金の取得はAWSのCost Explorer APIを使って取得しています。 今回使用したGo言語ではAWS SDKが提供されているので GetCostAndUsage を呼び出すことによりAWSの使用料金を取得できます。具体的には以下のコードのように関数を作成し料金を取得できるようにしました。 package costexplorer import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/costexplorer" "github.com/aws/aws-sdk-go/service/costexplorer/costexploreriface" ) type CostExplorer struct { session costexploreriface.CostExplorerAPI } func NewCostExplorer(session costexploreriface.CostExplorerAPI) CostExplorer { return CostExplorer{ session: session, } } func (c CostExplorer) GetCostForDaily(time_start string , time_end string , metrics [] string ) (*costexplorer.GetCostAndUsageOutput, error ) { granularity := aws.String( "DAILY" ) metric := aws.StringSlice(metrics) resp, err := c.session.GetCostAndUsage(&costexplorer.GetCostAndUsageInput{Metrics: metric, Granularity: granularity, TimePeriod: &costexplorer.DateInterval{Start: aws.String(time_start), End: aws.String(time_end)}}) if err != nil { return nil , err } return resp, nil } func (c CostExplorer) GetCostDetail(time_start string , time_end string , metrics [] string ) (*costexplorer.GetCostAndUsageOutput, error ) { granularity := aws.String( "DAILY" ) metric := aws.StringSlice(metrics) group := costexplorer.GroupDefinition{Key: aws.String( "SERVICE" ), Type: aws.String( "DIMENSION" )} resp, err := c.session.GetCostAndUsage(&costexplorer.GetCostAndUsageInput{GroupBy: []*costexplorer.GroupDefinition{&group}, Metrics: metric, Granularity: granularity, TimePeriod: &costexplorer.DateInterval{Start: aws.String(time_start), End: aws.String(time_end)}}) if err != nil { return nil , err } return resp, nil } 気をつけるべき点 Lambdaのコードをアップロードする際の問題 最初はCFnを使って全てのインフラリソースを管理していました。しかし、Lambda部分をCFnでInfrastructure as code(IaC)化している最中、Lambdaのコードをアップロードする部分で問題が発生しました。CFnではLambda上で動かすコードをCFnに直接埋め込む形と、S3で管理する形があります。 前者は AWS::Lambda::Function リソースの Code プロパティの ZipFile で管理できます。 しかし全ての言語に対応しているわけではなく、AWS料金Botの作成時点ではNode.jsとPythonしか対応していませんでした。また、CFnにコードを埋め込むとファイルが肥大化し、インフラとアプリケーションのコードが混ざってしまうのでコードの管理が大変になります。 後者ではLambdaのリソース作成時点でS3に対象のファイルが存在していない場合はエラーになります。そのため、Lambdaの作成リソースとLambdaのコードを管理するS3 bucketの作成リソースを同じCFnで管理すると、S3 bucketの中身は空なので作成エラーが発生します。 上記の理由によりCFnを適用する前にS3 bucketを作成し、Lambdaで動かすコードを事前にアップロードする必要があります。また、コードの変更がある際は対象のファイルのobject versionを S3ObjectVersion に指定しなければならず、コードの変更がある毎にobject versionを確認してCFnに変更を加えなければなりません。 Serverless Application Modelの採用 前述の理由からCFnでLambdaを管理するのには限界がありました。代替の方法がないかチームの人と相談した結果、 Serverless Application Model(SAM) を採用しました。 SAMはAWSがオープンソースで提供しているサーバレスアプリケーションを構築するためのフレームワークです。SAMの特徴は以下の点です。 SAMはCFnの拡張で、CFnに似たフォーマットなので学習コストが低い Localで開発する際に便利な揃っている環境でLambdaのテストが可能 Serverless関連のリソースのデプロイが簡単になる さらにSAMは SAM CLI が提供されています。変更セットの進行状況や変更セットのdiffが出力されるので、CIを組み込む場合でも親和性は高そうだなと思いました。 CloudFormationとServerless Application Modelの比較 具体的にCFnとSAMの相違点をまとめると下記の表で示す相違点があります。 項目 CloudFormation Serverless Application Model デプロイ aws cliを用いてデプロイできるが複数コマンドを組み合わせる必要がある sam deployコマンドを用いてデプロイできる 変更セットの確認 AWSのコンソール上からしか確認できない CLI上に変更セットが出力される テスト AWSのリソース上に上げないと確認できない Local環境でテストできる Lambdaのコード管理 CFnに埋め込む or S3で管理(ユーザがversioningする必要有り) S3で管理(versionigはSAM側で行ってくれる) 上記の表の比較から、Lambdaで扱うコードの管理をよしなに行ってくれ、Localで開発する際に便利な機能が揃っているSAMを採用しました。 まとめ 数ヶ月運用してみたのですが、毎日見るチャンネルに使用料金が通知されるようになったので、朝会のタイミングでAWSの料金について話す機会が生まれチームの人がAWSの料金に気を配るようになりました。また、AWSのCost Explorerを閲覧する権限を持っていない人でも確認できるので余分な権限を渡す必要もありません。 このタスクを通してMA内で使われているAWSの料金を知ることができ、その後の開発の際に使われるリソースの料金体系を気にするようになりました。また、Infrastructure as codeを行う事で他のAWS環境へ導入する際に反映する時間が短くなり、かつコードとしてインフラの構成を残しておけるのでInfrastructure as codeはアプリケーションを開発する上で必要であると感じました。 最終的にこのBotは間接的ですが会社で運用しているサービスの無駄なコストを省けるようになり、その分キャンペーンや企画にコストをかけられ、ユーザーにより質の良いサービスを提供できたと思います。 今回採用したSAMはまだまだ開発途上です。最近はAWS Step Functionsに対応しました!今後のアップデートに期待したいです。 最後に ZOZOテクノロジーズではより良いサービスを提供するための基盤作りを開発したい仲間を募集中です。以下のリンクからご応募ください。 tech.zozo.com
こんにちは、基幹システム部サポートチームの西山です。 私の所属するサポートチームでは、主にZOZOTOWNの注文の返品・返金のシステムや、カスタマーサポートの使用するバックオフィスシステムの開発に従事しております。 今回はECサイトの不正対策として、ZOZOではどういった観点で対策をしているのか、不正検知までどういったフローを踏んでいるのか、一部だけですがご紹介できればと思います。 はじめに 一般的なECサイトの不正利用ってこういうものがよく言われていますよね。 なりすましによるクレジットカード不正 集合住宅の空室やレンタルオフィスでの不正な受け取り 海外転送サービスの利用 その中でも、なりすましによるクレジットカード不正に関しては、特に不正の代表的な事例だと思います。 ZOZOTOWNも例外ではなくて、当然対策をしないといけません。 どのような対策をすれば良い? ほとんどのECサイトではクレジットカード決済時に、セキュリティコードの導入による対策をとられているのではないでしょうか。 セキュリティコードとは、クレジットカードの裏面に記載されている3~4桁の番号のことを言い、クレジットカード番号とは別の数字ですので、カード所有者にしかわからない情報です。 ECサイトでの決済時に、このセキュリティコードも入力してもらうことで、クレジットカード情報をスキミングなどで取得されたとしても、不正利用をしにくくする対策です。 このような対策を決済時に行うのはもちろんですが、うまく決済をすり抜けて発送までされてしまうケースがあります。 クレジットカードの不正利用をされたお客様は、見覚えのない請求があるということでクレジットカード会社へ返金を求めます。 クレジットカード会社より、ECサイト側にチャージバック請求がくるので、大量に不正注文が発生してしまうとECサイト側は大きな損失を負ってしまうことになります。 チャージバックとは、クレジットカード利用者が不正利用などの理由により利用代金の決済に同意しない場合に、クレジットカード会社が加盟店に対して支払いを拒絶することを言います。 このような損失を防ぐには、決済時の対策に加えて受注後の注文に対しても不正を検知することで、うまく決済をすり抜けてきた不正注文をあぶり出し、未然にチャージバックを防止する必要があります。 どうやって検知するの? ZOZOTOWNでも様々な不正の対策はしていますが、そのうちの1つとして独自の不正検知システムを利用しており、受けた注文の不正度合いを判定し、発送される前に対策を打つという使い方をしています。 不正度合いを判定する上で、こんな観点で見るといいよねというところを少し挙げてみます。 普通のお客様に迷惑をかけない これは一番大事なことかもしれません。 雑な条件で、一般のお客様の注文まで不正判定してしまうと、無駄に正常なお客様への確認や配送停止処理が発生してしまい、一般のお客様のショッピングを邪魔してしまうことになってしまいます。 逆に条件を絞りすぎた判定にしてしまうと、その条件に該当する不正だけは防げますが、本来防ぎたかった不正注文を広く防げなくなるため、バランスの見極めが非常に難しい部分でもあります。 不正ユーザーの気持ちになってみる 不正注文したことないのでどんな気持ちか実際はわかりませんが・・ 不正ユーザーはどういう商品をほしがるのか、どういう風にクレジットカード情報を入手するかなどを考えてみます。 イメージした不正手法をもとに、「ではそういう不正を防止するには、どういう判定条件や機能があればいいか」ということを考え、判定条件の土台を作ります。 顧客情報から推測する 過去の不正注文から、氏名、年齢、住所、電話番号などの顧客情報から不正ユーザーの法則を探し出したりもします。 注文特性から推測 注文金額、注文頻度、注文商品、配送先などの特性を掛け合わせて不正度合いを見極めます。 アクセスログから推察する 不正利用ユーザーがグループ犯罪であるケースもありえます。 最新の不正情報の傾向を把握する 不正対策の方向性を検討するのにセキュリティ系のサイトを参考にしています。 また、カスタマーサービス部門からも情報を吸い上げつつ、ZOZOTOWNならではな不正判定の視点での判定ロジックも組んでいたりします。 条件の作成までの過程 カスタマーサポート(CS)から不正判定の相談が来ます。 そこで、CS担当者とコミュニケーションをとりながら条件を決めていきます。 何かのサービスが始まるタイミングだったり、日々の運用の中で追加したい条件が発生したタイミングなどで都度相談が来る感じですね。 私が不正判定の条件作成時に気を付けていることとしては、こんなところでしょうか。 提示された条件で作成した判定処理のパフォーマンス 裏側のデータを知っているからこそ見える不正判定の切り口がないか 対象件数が数百件出るような場合は条件を疑う(誤検知を疑う) すでに設定されている判定条件に似たようなものがないか このようにパフォーマンス対策や、過剰検知にならないように担当者と調整して作成された条件を不正検知システムに仕込んでいくわけなのですが、以下のようなフローをたどって検知されていきます。 検知された注文は、不正検知システム上で管理されます。 CSによる確認を行いますが、一発で不正と判断できるものは当然、キャンセルという流れになっていきます。 一次判定では判別つかないものは保留になり、その後の判定で発送orキャンセルというフローです。 不正判断されたユーザー:不正確定、キャンセル対象 通常判定ユーザー:商品発送へ 不正の疑いがあると判断されたユーザー:怪しいけどセーフライン、確認のため保留 日々運用はアップデートしていきますが、基本的にはこういったフローが一般的なものです。 どれくらい効果があるの? 今年度の不正件数は昨年に比べて二倍強くらい増えてはいるのですが、その分防止もできていて、不正件数の精度としては年々向上しています。 精度の向上の裏には、エンジニアだけではなくZOZOTOWNの分析チームの協力もあり、より専門的で高度な観点から条件を作成できてきているという背景もあります。 課題もあります 毎年の検知件数はどんどん増えていますが、その中には実際は通常の購入であったのに検知された、誤検知もあります。 やはり、そういった誤検知によってお客様に対して不快な思いをさせてしまうこともあり、そこはZOZOTOWNに対して悪い印象を持たれてしまうことにもなってしまうためケアしていきたい部分です。 お客様だけでなく、CSの心理的負担や作業負荷にも繋がってしまうため、しっかり改善をしていかないとダメですね。 課題解決にむけて 現在は、より専門的な視点が必要と判断し分析チームに協力してもらっています。 そのおかげで、2020年度の不正防止率は向上し、だいぶ成果が出ている状況です。 さらに今後の取り組みとして、より高度な精度を求めるために、機械学習を用いて不正の疑いをスコアリングし、不正の疑いが高い注文に対して自動検知していくといった新しい検知方法の取り組みを進めています。 これが実現すると、不正検知の精度が向上して誤検知を防ぎ、お客様に気持ちの良いショッピングを楽しんでもらえます。 また、CSの負担軽減にも繋がっていきますね。 おわりに ZOZOTOWNのプロダクトで導入されている不正対策ということで、あまり詳しい情報まで提供はできないのですが、ZOZOTOWNとしての不正対策の観点や基本となる検知フローなどおわかりいただけたでしょうか? 少しでも参考になることがあればうれしいです。 このように、私たちサポートチームでは日々現場の業務改善、生産性UPを目標に取り組んでおります。ZOZOTOWNならではのスピード感があるので、相談を受けてから決まるまでが早いです。 また、いかに早く判定を開始できるかも重要なので、判定パターンのアップデートが容易にできるような作りにしています。どの部署も協力的なマインドを持っているからこそ実現できていると感じています。 ZOZOテクノロジーズでは、今回紹介したような裏側の仕組み作りや、運営する様々なサービスを一緒に作り上げていける方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
こんにちは、基幹システム部USEDチームの柳瀬です。現在は主にZOZOUSEDで取り扱う商品の価格算出に関するシステムの開発・運用を中心に担当しています。 先日、とある案件でAmazon Aurora上のPostgreSQLに新規でのテーブル作成を伴う機能を開発する機会がありました。そのテーブルは3億件ほどのレコードを格納し、高頻度の参照および日次でのデータ追加が行われるものでした。 大量データを扱ううえでクリアすべき点として「処理速度など性能面での問題が発生しない事」があります。実装にあたってこの問題をクリアするために工夫した際に、パーティションテーブルを活用する事で解決する事ができましたので、その時の経験談をお伝えしたいと思います。 なお、今回の記事ではPostgreSQLを対象に説明しております。パーティションテーブルが実装されている他のRDBMSに関してもある程度は参考にできるかと思いますので、ご自身で担当されておりますシステムのDBに置き換えて読み進めていただければと思います。 そもそもなぜPostgreSQLを採用したのか 今回紹介する機能で使っているデータベース(以下DB)ですが、リリース当初はAWS上のMariaDBで稼働していました。しかしながら、その直後の追加改修においてwindow関数を使用したい場面が出てきました。 当時AWSで利用可能だったMariaDBのバージョン(10.2)においてwindow関数の動作を確認したところ、こちらが期待した通りの動作をせず、実用に耐えうるものでない事が判明しました。 window関数を使用しない方法で実装しようと思えばそれ自体は可能です。ただ今回は処理が複雑化して工数が大幅に増加する事が予見できたため、分析・集計系の処理に強いとされるPostgreSQLへ切り替えた方が速いという判断になりました。 やろうとした事 そもそも今回のテーブルは、別のチームが別のDBで運用していたものを巻き取る形で追加する事になったテーブルでした。そこで、移行するにあたって現状を確認したところ、データが増えすぎたせいでインデックスの追加が1日で終わらなくなっていた事が判明しました。 今回のテーブルでも同じような事が起こったら、移行する意味がありません。上位のインスタンスに置き換えてDBの性能を上げる事は費用の関係で難しかったため、テーブルの構成を工夫する必要に迫られました。 そこで採用したのがパーティションテーブルです。 外からは1つの大きなテーブルに見えているのですが、実際にはより小さなテーブルにデータが分けて入れられています。そのためアプリケーション側から見た場合、テーブルが分けられている事を意識させないような構造になっていると言えます。 利点としてあげられるのは処理の高速化です。 通常の検索・更新処理については参照する物理領域を減らせる事で速くなる場合があります。またバッチ処理で大量にデータを追加・削除する場合も、パーティション自体を操作すれば時間をかけずに実施する事が可能です。 パーティションテーブルの作り方 それでは作ってみましょう。まずは「親テーブル」を以下のSQLで作成します。 CREATE TABLE sample_table ( id serial, partition_column int, -- 中略 ) PARTITION BY RANGE (partition_column); カラム定義の括弧を閉じるところまでは普通のテーブルを作るのと同じで、括弧を閉じた後にPARTITION BYでパーティションの種類と対象の列を指定するのが普通のテーブルと異なるところです。 指定の仕方には範囲パーティション、リストパーティション、ハッシュパーティション(PostgreSQL 11以降)の3通りの指定方法がありますが、今回は範囲パーティションで説明を進めます。 次に、親テーブルに紐づけるパーティション(以下「子テーブル」)を以下のSQLで作成します。 CREATE TABLE sample_table_partition_1 PARTITION OF sample_table FOR VALUES FROM ( 0 ) TO ( 1000000 ); テーブル名のすぐ後にPARTITION OFで親テーブルを指定し、FOR VALUESで分割する値の範囲を指定します。この指定の仕方でセットされる値は 0 <= partition_column < 999999 になります。TOで指定した値より1小さくなる事に留意してください。 ちなみにパーティションに対するインデックスの設定ですが、自力で全ての子テーブルに作成しています。これは検証時のAurora PostgreSQL最新バージョン(10)にて親テーブルへインデックスが追加できなかったためです。 なお、PostgreSQL 11からは親テーブルへの指定で自動的に全ての子テーブルに反映させる事ができるようになっております。AuroraでもPostgreSQL 11が利用可能です。 この辺りは機会があれば改めて検証してみたいと思います。 インデックス作成時間の比較 上記のような手順で3億件のデータを10分割したパーティションテーブルに対し、PARTITION BYで指定したカラムに対するインデックスを作成してそれにかかった時間を計測してみました。 また、比較のため全く同一のデータが入った単一のテーブルを用意し、そのテーブルに対しても同様のインデックスを作成して比較してみました。 以下がその結果です。 単一テーブル:396.819秒 10分割パーティション:39.729秒(1パーティションあたり) パーティションテーブルの場合、単一のテーブルと比較して1/10の時間で作成できている事がわかります。物理テーブル1個あたりのデータ量が単一テーブルと比較して1/10になっていますのでほぼ想定通りです。 また、各パーティションのインデックスは並列で作成・再構築する事が可能なため、並列処理する事で単一テーブルに対するインデックス作成よりも短時間で作業を完了する事ができました。 また、作成したインデックスのサイズは、1パーティションあたりのサイズが単一テーブルのインデックスと比較して1/10になっていました。サイズが小さいほどINSERTや再構築にかかる時間も短縮されますので、より高速に動作する事が期待でき、単一のテーブルで作成するよりも運用面での負担が少ないと言えます。 実際にリリースしてから今日まで、大きなトラブルは発生しておりません。また、特定の条件でSELECTした場合も高速になるようですが、こちらも今後改めて検証したいと思います。 まとめ 今回得られた知見は他のテーブルやDBでも応用が可能です。特に今後は大量データを活用する場面がさらに増えると想定されますので、それに呼応して活用の場面が増えるものと考えられます。 ZOZOテクノロジーズでは、共にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は以下のページをご確認ください! tech.zozo.com
こんにちは。MLOpsチームリーダー兼プラットフォームSREチームリーダーの sonots です。今年の4月からZOZOTOWNリプレイスプロジェクトにも関わるようになりました。Zoomの背景画像を「進め!電波少年」にしてみても、チームの若者に伝わらないのが最近の悩みです。 今回の記事は、昨年度にタスクフォースとして発足したOSSポリシー策定委員会を代表して、今年の4月に弊社で策定したOSSポリシーについて紹介します。 OSSポリシー策定の背景と目的 弊社でもOSSを利用・貢献・公開しているメンバーが増えてきています。また、会社としても業界貢献、技術アピールの側面からOSS活動を奨励したいという想いがあります。 しかし、弊社にはOSSポリシーが存在しなかったため、相談を受けた際にCTO室が都度判断するという状況がしばらく続いていました。都度判断ではスケールしないため、「社員がOSS活動しやすいようにする」ことを目的として、CTO室からの依頼でOSSポリシー策定委員会が発足され、OSSポリシーを策定することとなりました。 代表を務めた私(そのっつ)個人としては、OSSポリシー策定にあたって以下のような想いを抱いていました。 OSS開発は公私混同しがちなので、いっそのことそれが許されるようにしたい 社内秘伝パッチをあてて、本家にPRを送らないような事例は、負債にもなる(バージョンアップのたびにパッチをあてなければならない)ので極力なくしたい 特に 1. は著作権法や就業規則に絡んでくるため、エンジニアだけではなく、法務、労務のメンバーにも委員会に入ってご協力頂きました。 就業規則の改定も行い、取締役会での承認を得ました。 他社事例 OSSポリシー策定にあたっては、事前に他社事例を調査し、以下を参考にさせて頂きました。 オープンソースビジネスに取り組む SI 企業のための企業ポリシー策定ガイドライン オープンソースソフトウェアポリシーをつくろう - クックパッド開発者ブログ サイボウズのオープンソースソフトウェアポリシーを紹介します - Cybozu Inside Out | サイボウズエンジニアのブログ 特に サイボウズのOSSポリシー は、私の思想に近いものがあり、今回おおいに参考にさせて頂きました。この場を借りてお礼申し上げます。 著作権の帰属と職務規定 上述した私の想いである「1. OSS開発は公私混同しがちなので、いっそのことそれが許されるようにしたい」という課題をクリアするためには、いくつかの壁がありました。 第一に、弊社の就業規則に以下のような記述がありました。 職務著作 (1) 従業員が職務上作成する著作物の著作権(著作権法第27条、第28条所定の権利を含む。)は、会社に帰属する。 この規則により業務時間中に、入社以前から開発しているOSSをメンテナンスする場合や、他者OSSに送付するパッチを作成する場合でも、著作権が会社に帰属することになり非常に面倒です。弊社はフルフレックス制度を採用しているため、一度退勤してメンテナンス業を行い、終わったら出勤することでこの規則を躱すことはできますが、毎回やりたいことではありません。そこで取締役会にて相談し「OSS活動を行ったことによって作成した著作物については、別途定めるOSSポリシーに基づき、その著作権の帰属先を判断する」という一文の追加を許可して頂きました。OSSポリシーについては後述します。 第二に、就業規則に以下のような記述があり、OSS活動は職務に入るのか否かが議論としてあがりました。 職務専念義務 (1) 勤務中は職務に専念すること この点については、取締役会で「OSS活動は職務として認める」と決定して頂きました。 これらの結果、弊社では業務の一貫であるが個人の著作物としてのOSS活動を行うことができるようになりました。 本OSSポリシーの概要とポイント 今回作成したOSSポリシーは CC0 ライセンスで GitHub上 にて公開しています。また、社内にはOSS公開ガイドライン、OSSコントリビューションガイドライン、OSS利用ガイドラインを別途用意してあり、こちらのドキュメントも今後可能であれば公開していきたいと考えています。 本OSSポリシーは「社員がOSS活動しやすいようにする」ことを全面に押し出したポリシーになったと思います。本OSSポリシーには、以下のような特徴があります。 業務時間中であっても指示なく自発的に作ったソフトウェアは個人のものにできる(例えば.emacsや.vimrcなどを指していますが、それに限りません) 業務時間中に指示があって書いたソフトウェアでも著作権譲渡申請の許諾によって個人のものにできる 従業員が自己の所有するOSSプロダクトに対して自己が業務で作成した著作物を取り込む場合、著作権譲渡申請がなくても個人の著作物にできる パッチについても同様に、自発的に書いたOSSパッチは個人の著作物とすることができます。上長の明示的な指示のもと書かれたパッチにおいても、著作物譲渡申請を行うことで個人の著作物とすることができます。 ガイドラインおよびポリシーに準拠している限りは、OSS公開時に許可を得る必要はありません ガイドラインおよびポリシーに準拠している限りは、OSSコントリビューション時に許可を得る必要はありません CLA (Contribution License Agreement) 同意を求められた場合も、許可を得ずに当社を代表して署名できます おわりに 今年の4月に策定した ZOZOテクノロジーズ オープンソースソフトウェアポリシー について紹介しました。これにより非常にOSS活動しやすい組織になったと自負しています。 OSSポリシー策定にあたり、社内で委員会を発足し、関係各所のご協力をいただきました。委員会のメンバーに御礼申し上げます。特に takanamito にはOSS公開ガイドラインも書いて頂き大変助かりました。 ZOZOテクノロジーズではOSS活動を奨励しています。OSS活動により会社および業界に良い影響を与えてくれるメンバーを絶賛募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
はじめに こんにちは。ZOZOTOWN部サービスグロースチームでアナリティクスをしている井ノ口です。 この記事ではBigQueryで使える、ユーザー定義関数(UDF)という便利な武器をご紹介します。「UDFって何?」「何のために使うの?」という方に向けた記事のため、高度な分析などはこの記事では扱いません。 UDFとは UDFとは、最初から用意されているSUMやCOUNTのような関数を、ユーザー自身が定義する関数です。 私のチームではGoogle Analyticsのデータから、ユーザーが閲覧したページを分類したり、日時を計算したりする際などにUDFを用いてます。利用法など詳細は公式のドキュメントに記述されているため、そちらをご参照ください。 参考: 標準 SQL ユーザー定義関数 | BigQuery | Google Cloud UDFを使うメリット 使い方によって様々なメリットを受けられますが、特に大きいと感じるメリットは以下の3つです。 1. コーディングのコストを低減できる UDFは一度処理を記述するとUDFを呼び出すだけで処理を実行できるため、同じコードを繰り返し記述する必要がありません。UDFをチームで共有できるなら、時には自身が処理を記述する必要すらなくなるため、コーディングにかかる時間・労力のコストを低減できます。 また、作成したコードについてレビューを依頼する機会があれば、相手は既存のUDFについてレビューする必要がなくなるため、レビューのコスト低減にも繋がります。 2. コードを理解しやすくできる 処理が複雑で長いものであるほど、処理がどのようなものかを考える、あるいは理解するために大きなコストを要します。そこで処理の記述にUDFを用いると、コード中の複雑あるいは長い処理であっても役割として理解しやすくなります。UDFを用いた処理が複数回繰り返されるなら、それらが同じ役割であるとも容易に理解できます。 3. バグ混入のリスクを低減できる UDFは自身以外が作ったものでも使い回せます。 一度バグがないか検証されたUDFを利用すると、新たに処理を記述してバグが混入するケースを避けられます。たとえUDFにバグが含まれていたとしても、UDFの中身を修正すれば全ての処理が修正されます。 例えばコピー&ペーストで同じ処理を複数箇所に記述していた場合、該当箇所を全て修正する手間や修正漏れによるバグ混入の恐れがあります。 デメリットとしてはUDFは結合条件など一部では使えなかったり、UDFの中でWITH句の内容が使えなかったりします。ただし、結合条件の代わりにWHERE句で指定したりWITH句の内容をUDFで宣言できるケースがあるなど、工夫でなんとかなることもあります。 UDFを使用したクエリ例 上記のようなメリットがあるため、サービスグロースチームでは以下の用途などで使用します。 IDの管理 覚えるのが大変なため、UDFを使用しないとバグを混入させたり見逃したりしがちです。 条件の管理 ページや流入元を分類・判定する正規表現などを扱うことが多いです。条件は取りたい値が、なんらかの事情で変更されることが少なくないため、UDFを用いると条件の確認や変更に重宝します。 日時の処理 頻繁に使うものの直感的に記述・理解しづらかったり、記述が長くなりがちな処理なため、UDFが活躍する場面です。 実際にこのようなUDFを使用したクエリの例をご紹介します。 ZOZOTOWNの仕様に合わせた独自の条件などが含まれるため、一部をマスクしてあります。このクエリでは、セッションのテーブルから閲覧した日付、ページカテゴリごとにUUを求め、ページカテゴリが”その他”のものだけ除外します。 ------------------------------------ユーザー定義関数------------------------------------ -- visitStartTimeを日本時間の日付に変更する関数 CREATE TEMPORARY FUNCTION DATE_BY_VISITSTARTTIME(visitStartTime INT64) AS ( DATE (TIMESTAMP_SECONDS(visitStartTime), " Asia/Tokyo " )) ); -- ページに対応するIDを返す関数 --★ 複数回使っているUDFだが、この処理を変更すると全ての処理に変更が適応される CREATE TEMPORARY FUNCTION PAGECATEGORY_ID_BY_PAGECATEGORY_NAME(pageCategoryName STRING) AS ( CASE pageCategoryName WHEN " トップ " THEN 1 WHEN " 検索結果 " THEN 2 WHEN " 商品詳細 " THEN 3 WHEN " その他 " THEN 4 END ); -- ページを判定し対応するページIDを返す関数 CREATE TEMPORARY FUNCTION CATEGORIZE_WEB_PAGECATEGORY_ID(pagePath STRING) AS ( CASE WHEN REGEXP_CONTAINS(pagePath, r " [トップのページパスに該当する正規表現] " ) THEN PAGECATEGORY_ID_BY_PAGECATEGORY_NAME( " トップ " ) WHEN REGEXP_CONTAINS(pagePath, r " [検索結果のページパスに該当する正規表現] " ) THEN PAGECATEGORY_ID_BY_PAGECATEGORY_NAME( " 検索結果 " ) WHEN REGEXP_CONTAINS(pagePath, r " [商品詳細のページパスに該当する正規表現] " ) THEN PAGECATEGORY_ID_BY_PAGECATEGORY_NAME( " 商品詳細 " ) ELSE PAGECATEGORY_ID_BY_PAGECATEGORY_NAME( " その他 " ) END ); --★ ここまではチーム内で共通して使っているUDFなので、レビュー不要 SELECT DATE_BY_VISITSTARTTIME(visitStartTime) AS visitDate --★ 日時の変換処理が直感的でバグが混入しづらい , CATEGORIZE_WEB_PAGECATEGORY_ID(hits.page.pagePath) AS pageCategoryId --★ 記述すると長くなる分類処理が短く済む , COUNT ( DISTINCT fullVisitorId) AS UU FROM `[セッション情報からなるテーブル]`, UNNEST(hits) AS hits WHERE pageCategoryId <> PAGECATEGORY_ID_BY_PAGECATEGORY_NAME( " その他 " ) --★ <> 4 だと4の意味がわからない、誤りがあっても気づけない GROUP BY visitDate , pageCategoryId クエリ全体で見ると行数が多く感じるかもしれませんが、書く・読む作業が発生するのはほぼSELECT文のみで行数は少ないです。UDF部分はすでに用意してあるものを使っているため、書く・読む手間はほぼありません。 今回はUDFを一時的なUDFとして扱っていますが、永続的なUDFとして予め用意していると記述する必要もなくなります。 参考: 標準 SQL ユーザー定義関数 | BigQuery | Google Cloud このクエリでは日時の処理、条件の管理、IDの管理にあたるUDFをそれぞれ使っています。UDFの使用によりSELECT文中の記述量は減り、馴れもありますが処理の内容を理解しやすくなっているかと思います。 また、クエリ例の中ではIDの管理にあたる PAGECATEGORY_ID_BY_PAGECATEGORY_NAME() は別のUDFやWHERE句で複数回使われており、条件を変更したい場合にはこのUDF一箇所で済みます。 加えてコード中にIDの数値をそのまま書いていると、そのIDが何を指し正しいのかどうかが理解しづらいですが、IDを管理するUDFによって理解しやすくなっています。このようなマジックナンバーを避ける働きもできます。 UDFの例 最後に、このクエリに含まれていないUDFも含め、処理の説明を併せた例を紹介します。 セッション固有のIDを返す CREATE TEMPORARY FUNCTION SESSION_ID_BY_FULLVISITORID_VISITID (fullVisitorId STRING, visitId INT64) AS ( CONCAT (fullVisitorId, " - " , CAST (visitId AS STRING)) ); fullVisitorIdとvisitIdをハイフンでつなぎ、セッション固有のIDを生成します。セッションを比較し、同じものかどうかを判定する際などに用います。 参考: BigQuery Export のスキーマ - アナリティクス ヘルプ visitStartTimeを日本時間のTIMESTAMP型に変更する CREATE TEMPORARY FUNCTION TIMESTAMPDT_BY_VISITSTARTTIME (visitStartTime INT64) AS ( CAST (DATETIME(TIMESTAMP_SECONDS(visitStartTime), ' Asia/Tokyo ' ) AS TIMESTAMP) ); visitStartTimeを日本時間のTIMESTAMP型に変換する関数です。セッションが始まった日時を把握するために用います。 参考: BigQuery Export のスキーマ - アナリティクス ヘルプ yyyymmdd型の日付をDATE型に変換する CREATE TEMPORARY FUNCTION DATE_BY_yyyymmdd(yyyymmdd STRING) AS ( DATE ( CAST ( SUBSTR (yyyymmdd, 1 , 4 ) AS INT64), CAST ( SUBSTR (yyyymmdd, 5 , 2 ) AS INT64), CAST ( SUBSTR (yyyymmdd, 7 , 2 ) AS INT64)) ); "20200202"(2020年2月2日)のようにyyyymmddの形で書かれた日付をDATE型へ変換する関数です。 ワイルドカードテーブルを使用する際に、対象テーブルを指定するためにyyyymmddの形で日付を指定します。このyyyymmddを、DATE型でも使いたい際に用います。 例えば、他のテーブルとジョインしたいが、そのテーブルが持つ日時はyyyymmddでなくDATE型のため同じ形に合わせる必要があるというときに使用します。 参考: ワイルドカード テーブルを使用した複数テーブルに対するクエリ  |  BigQuery  |  Google Cloud yyyymmdd型の日付をx日前にずらす CREATE TEMPORARY FUNCTION DATE_SUB_yyyymmdd(yyyymmdd STRING, sub_date INT64) AS ( REPLACE (SAFE_CAST(DATE_SUB( DATE ( CAST ( SUBSTR (yyyymmdd, 1 , 4 ) AS INT64), CAST ( SUBSTR (yyyymmdd, 5 , 2 ) AS INT64), CAST ( SUBSTR (yyyymmdd, 7 , 2 ) AS INT64)), INTERVAL sub_date DAY) AS STRING), " - " , "" ) ); "20200202"(2020年2月2日)のようにyyyymmddの形で書かれた日付をSUB_DATE日前へずらす関数です。yyyymmddに加え、数日前のテーブルもデータ抽出の対象とする際などで用います。 yyyymmdd型の日付をx日後にずらす CREATE TEMPORARY FUNCTION DATE_ADD_yyyymmdd(yyyymmdd STRING, add_date INT64) AS ( REPLACE (SAFE_CAST(DATE_ADD( DATE ( CAST ( SUBSTR (yyyymmdd, 1 , 4 ) AS INT64), CAST ( SUBSTR (yyyymmdd, 5 , 2 ) AS INT64), CAST ( SUBSTR (yyyymmdd, 7 , 2 ) AS INT64)), INTERVAL add_date DAY) AS STRING), " - " , "" ) ); ひとつ上の関数の後ろにずらすバージョンです。 おわりに UDFについてイメージがついたでしょうか? コピー&ペーストなどで繰り返している処理をUDFに置き換えるだけでも、コードは書きやすく、読みやすくなるはずです。ぜひ簡単なところからでもお試しください。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
こんにちは。技術開発本部SRE部の渡邉です。 リモートワークによる運動不足を解消するために毎朝ロードバイクで走る事を始めたところ、今では印旛沼 1 のまわりを走るのが生きがいになりました。 そんな私ですが2019年に入社して以降、現在に至るまで、ZOZOTOWNやWEARのインフラを担当しております。 前職からずっとインフラ周りの仕事をしておりますが常に頭を悩ませてきたのは監視の設計と構築というテーマでした。長年稼働しているプロダクトならば、まず抱えている問題のひとつにあがるのではないでしょうか。私たちが日々運用しているZOZOTOWNでも同様に監視システムで課題を持っていました。 本記事ではリプレイスプロジェクトが進むZOZOTOWNのインフラの可視化を向上させる目的でSplunkを導入し、得られたメリットについて紹介したいと思います。 背景 2020年現在、ZOZOTOWNは劇的な変化を遂げており、サービス開始当初から稼働しているオンプレミスとマイクロサービス化が進んでいるパブリッククラウドのハイブリッドな構成になっています。 リプレイスが進むにつれて、必然的に従来の監視システムだけでは成り立たなくなります。 また、移行を推進していく過程における副産物として監視システム自体のサイロ化を生みやすくもなるため、見直しを図る時期に差しかかっていました。 そこで、様々な機器からログを収集することで障害検知までの速度改善が見込め、今後の一元管理も可能となるSplunkの導入を決定しました。 Splunkの基礎知識 私自身、今までにSplunkを使用した経験はありませんでしたが、度々Splunkに関する説明を受けていて、以下の点が私たちにとってメリットがあると感じていました。 クラウド版を選択することで、インフラの設計を待たずに導入が可能であること 様々なAdd-onの提供が Splunkbase でされており、可視化する際に特別な工夫をしなくても簡単にダッシュボードを作成できること 取り込んだログはSplunk側で適切なログフォーマットにあわせた成形をしてくれること Search Processing Launguage(SPL)について 基本的な情報については様々な記事で紹介されているため詳細は割愛しますが、Splunkを使うためには Search Processing Launguage(SPL) を理解する必要があります。 SPLはBashと似ている部分もあればSQLと似ている部分もあるので、慣れるには実際のログを入れて動かすことが手っ取り早いと思います。そのため、Splunk Free版を使うことで手元のログを所定の形式でインポートして実際に試すことができます。Free版なので様々な制約はあるものの大体の使用感を理解するのは充分です。 なかでも参考にしたのは以下のリファレンスです。 Splunk Fundamentals 1 は無償で受講可能となっているEラーニングですがとても充実しています。 Command quick reference Splunk Fundamentals 1 サーチの使い方 index =main sourcetype= " ms:iis:auto " host= " Server* " サーチ結果について、デフォルトでは イベント が表示されています。この結果をどのように表示させるか以下の4種類から選択可能です。 イベント パターン 統計情報 視覚エフェクト グラフィカルに結果を出力できる視覚エフェクトを一番多く使います。チャートやグラフなどあらかじめ複数用意されていますし、サーチ結果に合わせてエフェクトを推奨するパターンもあり、親切に作られています。 次のサーチ文は特定のUri_Pathのアクセス回数をホストごとにカウントした結果を出力する例です。このサーチ結果を単純に回数を表現する Single Value を使って出力してみます。 index =main sourcetype= " ms:iis:auto " host= " Server* " uri_path= " /men-category/tops/ " |stats count by host Single Value の表示を"トレリスレイアウト"で設定すると、それぞれのホストごとに集計した画面を作ることができました。 監視ダッシュボードを作る あらかじめ監視画面を作成しておくために、必要なサーチ結果を組み合わせてダッシュボードを用意します。 作成したダッシュボードはこんな感じです。 エラー発生件数 レスポンスタイム CPU使用率 今回はサーバで HTTP Status 500 が発生していた例を使って説明します。 エラーカウントの推移 まず、 HTTP Status 500 の発生推移を確認します。 特定のサーバに偏っている場合はエラー回数を単純にカウントして降順ソートで出せば完了です。 index =main sourcetype= " ms:iis:auto " host " =Server* " AND status= 500 | stats count by host | sort - count しかし、今回は時間軸と傾向を把握したいため、サーバグループごとに分けて抽出しています。 index =main sourcetype= " ms:iis:auto " AND (host=ServerA* OR host=ServerB*) AND status= 500 | eval servergroup= case ( like (host, " %ServerA% " ), " ServerGroup1 " , like (host, " %ServerB% " ), " ServerGroup2 " ) | timechart span=1m count as error_count by servergroup エラー発生状況のグラフを確認するとServerGroup1に偏っている状態がわかります。ServerGroup2側と比較すると、よりその差が明確になっています。 レスポンスタイムの推移 続いてレスポンスタイムの傾向も同様にサーバグループごとに抽出しています。 index =main sourcetype= " ms:iis:auto " AND (host=ServerA* OR host=ServerB*) | eval servergroup = case ( like (host, " %ServerA% " ), " ServerGroup1 " , like (host, " %ServerB% " ), " ServerGroup2 " ) | timechart span=1m avg (time_taken) as responsetime by servergroup レスポンスタイムの比較でもServerGroup1とServerGroup2に明らかな差が生じていました。 CPU使用率の推移 最後に mstats というメトリクスサーチでサーバのメトリクスデータからCPU使用率を確認しています。 | mstats avg (_value) prestats= true WHERE metric_name= " Processor.%_Processor_Time " AND " index " = " em_metrics " AND (host=ServerA* OR host=ServerB*) AND `sai_metrics_indexes` span=1m | eval servergroup = case ( like (host, " %ServerA% " ), " ServerGroup1 " , like (host, " %ServerB% " ), " ServerGroup2 " ) | timechart avg (_value) AS Avg span=1m by servergroup | fields - _span* 同じ時間帯にも関わらずServerGroup1のCPU使用率が100%で張り付いてしまっている状況でした。エラー発生数とレスポンスの悪化を引き起こしていた原因はCPUの高負荷状態にあったと言えます。 アクセス数の単純増加など、高負荷を引き起こす外的要因が明らかになっていれば、調査に要する時間はもっと短くなるためダッシュボードにアクセス数も組み込んでいくこともできます。 このあたりはログ分析とあわせてメトリクス監視の要素を組み込むこともできる自由度の高さもSplunkの特徴だと思います。 Splunk App for Stream Splunkには Splunk App for Stream というネットワークキャプチャも存在しています。 これによってWebサーバから外部APIへ接続する際のレスポンスを取得していく事が可能になるため、サーバ内外のレスポンスの変化を調べることも可能になります。 index =main sourcetype= " stream:http " site IN (*api*) host= " Server* " | rename " sum(time_taken) " as time_taken | eval response=(time_taken/ count / 1000 ) | timechart avg (response) span=1m as avg by site Streamで取得できるレスポンスタイムの値はマイクロ秒になっているため計算をミリ秒に合わせています。 サーバからAPIへの内部通信のレスポンスを確認できるのはかなり重宝します。 しかしながらIngestされるデータ量が爆増するため事前にSplunkの方と容量の増加について相談をしておいたほうが良いです。 Splunk App for Infrastructure すでにダッシュボードで使用していたメトリクスサーチという機能で機器のメトリクスデータを扱うことができるAppです。 以下の画面はサーバ単体のメトリクス概要になります。様々な項目を収集できていることがわかります。 ほかのサーチ文同様に複数のサーバを組み込むことができるのでちょっとした変化に気づきやすくなりますし、比較対象をわかりやすく表示したい場合に重宝します。 | mstats avg (_value) prestats= true WHERE metric_name= " Memory.Available_Bytes " AND " index " = " em_metrics " AND " host " = " ServerA01 " OR " host " = " ServerA02 " OR " host " = " ServerA03 " AND `sai_metrics_indexes` span=10s | timechart avg (_value) AS Avg span=10s by host[f:id:vasilyjp:20200713120129j:plain] | fields - _span* ここで使用している mstats は、ほかのサーチと比べて結果を高速で返すため、あまりストレスがかからずに調査できる点もメリットです。 Splunk導入の効果とまとめ Splunkを導入してから、アラート発生時にSplunkで調査するというアクションをチーム全員が自然と取るようになっています。得られたデータをすぐにダッシュボードで共有する流れになったことで、誰が、どういった問題を検知したのかを視覚的に理解しやすくなったといえます。その結果、以前と比較して、アラートに対する動きが格段に速くなったと感じています。 今回は紹介しきれませんでしたが、次の機会があればSplunkの機械学習を使った監視設定や 効率よいサーチの作り方 について紹介できたらと思います。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com 印旛沼 ↩
こんにちは、ZOZOテクノロジーズ CTO室の池田( @ikenyal )です。 ZOZOテクノロジーズでは、6/27-28(開発2日間・発表は6/29)にアスクル・一休・PayPay・ヤフーと合同で社内ハッカソン「Internal Hack Day」を開催しました。これまでヤフーの社内で開催されてきたハッカソンイベントを、今回は初のZホールディングスの各社合同で実施することになりました。合わせて、新型コロナウイルス感染症対策として、初のオンライン開催でもありました。 techblog.yahoo.co.jp techblog.yahoo.co.jp そして、今回のHack DayではヤフーのHack Day史上で初めてテーマが設定されました。「新しい生活様式での課題解決」というテーマです。このHack Dayの開催の形式もそうであったように、新型コロナウイルス感染症の影響で我々の生活様式が新しいものに移り変わってきています。その中で出てくる課題を解決し、世の中の役に立つものづくりをしようという思いが込められています。 弊社ではこれまでにも開発合宿を企画したり、エンジニアがプロダクト開発以外でも技術を学べる場の提供を行っております。 techblog.zozo.com 新型コロナウイルス感染症の影響により、計画していた開発合宿もできなくなっており、なかなかこのような場を作り出すことができない状況にありました。そのような状況で、ヤフーからInternal Hack Dayのお誘いをいただきました。Zホールディングスの仲間として、絶好の機会を得ることができました。 普段であれば社内のエンジニアとしか交流する場がなかったりするので、他社のエンジニアと同じハッカソンに参加し発表を見る、場合によっては同じチームで開発する機会は貴重な場です。 今回、弊社からは3チーム(1チームはヤフー・PayPay・ZOZOテクノロジーズの合同チーム)が参加しました。そこで得られた知見や感想をダイジェストでお伝えします。 参加者の感想 以降、それぞれのチームの感想を紹介します。 地図周りの知見を深めたくてHack Dayに参加(@kurarararara) ZOZOテクノロジーズ、iOSエンジニアの @kurarararara です。自分達は二人チームで参加しました。 同じく同僚でiOSエンジニアの @koiwai2020 さんに、「地図を使ったアプリを作ってみたいので参加しませんか」と声をかけHack Dayに参加しました。たった二日の開発期間というのはエキサイティングかつ非常に緊張感のあるものでしたがとても良い経験になりました。 実際どういった流れで参加したのかダイジェストでお伝えします。 1日目 朝 普段から同じチームで開発しており、テーマの方針も決めていたので余裕あり。 Discordで談笑しながら、具体的にどんなUIでどんな動きにするか話し合う。 余裕があるので、SwiftFormatや、.gitignoreを指定したりなど環境整理に費やす。 昼 事前に使おうと思っていたヤフーのローカルサーチAPIを調査。 同時にMKMapView + Core Locationについて調べる。特に引っかかることなく、地図の表示や現在地などを取得することに成功。 マスコットキャラクターの作成に着手。@koiwai2020さんの書く牛が非常に味のある絵で感心する。 マスコットキャラクターの役割を示すため、レーダーのようなアニメーションを追加。 夕 構想していた機能を追加し終えて、ほぼアプリ完成。 「終わっちゃいましたね。後1日どうしましょう?」と談笑。 この時はプレゼンに耐えられるアプリやスライド作りの大変さに、まだ二人とも気がついていない…。 夜 落ち着いて改めてアプリを見直す。「このアプリ大丈夫かな?機能も足りないし、マスコットキャラクターも変更した方が良いのではないか?」と軽く議論になる。 とりあえず、マスコットキャラクターを描き直すことに決めて翌日へ。 2日目 朝 2日目を迎えて、改めてマスコットキャラクターを見てみると非常に愛らしく素晴らしいことに気づいて続投決定。 マスコットキャラクターをより際立たせるためにコメント機能を追加。 変化が伝わるようにコメントの吹き出しにCore Animationを使ってアニメーションを追加。 Core Locationを使って地名の検索機能を追加。 昼 ここで急に残り時間がもうないことに気づく。焦る。 さらに、プレゼンとスライドを作らねばならないことに気づく。余計に焦る。 90秒のプレゼン時間に対して、20ページ近いスライドを作成。リハーサルしてみるとやはり尺が足りないことに気づく。とても焦る。 夜 なんとかプレゼンをブラッシュアップし、90秒の枠に収まるように修正。 なんとかアプリがきちんと動くようになり、完成。 全工程完了。なんとかやり切ったと達成感に包まれる。 3日目(発表日) 今回はリモートということもあり、他の参加しているチームが何をしているのか全くわからなかったが、蓋を開けてみるとどのチームも非常にユニークで技術的にも凄い作品ばかりで感心する。 発表は@koiwai2020さんに託す。プレゼンは非常にわかりやすく、自分達のアプリの機能の全てを90秒でしっかり伝えていただいて感謝。 ただ、機材トラブルで実際にアプリが動いているところが映らないアクシデント発生。どんまいです笑。 最後に 色々ありましたが、二日間に一気に集中してアプリを開発することで、普段の学習よりも素早く濃密に知見を深められた気がします。 前々から地図アプリを作ってみたかったので、この知見を生かして近々何か一個作ってみたいと思います。 また何よりあーだこーだ談笑しながらアプリを作るのはとても楽しかったですね。再びこんな機会があるならば参加してみたいと思います。 新卒5人でHack Dayに挑戦(さっと) ZOZOテクノロジーズZOZOTOWN部・検索チームのさっとです。 私達は、20新卒のエンジニア4人とデザイナー1人の計5人でHack Dayに参加しました。2日間という非常に短い時間の中での開発だったため、時間に追われ大変でしたが、得るものが多い2日間となりました。 開発したサービス 私達は、「身しりとり」というサービスを作りました。 身しりとりは、ZoomやLINE通話などのビデオ通話アプリと画像認識・画像分類の技術を活用し、体を動かしながらジェスチャーで楽しく「しりとり」を繋いで遊べるサービスです。 外出自粛による、運動不足やコミュニケーション不足といった問題を解決できないか、という思いから開発しました。 開発での苦労話 身しりとりを実現するために、ユーザがどのようなポーズを取っているかリアルタイムで判断する必要がりました。 そのため、サーバ側の処理としては、ビデオアプリでキャプチャーした画像からユーザのポーズを抽出するためにユーザの骨格情報を使ってピクトグラム化する処理を行います。 次に、事前に深層学習を使って作成した分類器を用いて、ユーザがどのようなジェスチャーをしているか類推します。2日間で分類器を作り、さらにチューニングを行うことはとても大変でしたが誤検知率が低下し、かつリアルタイム性を実現できたので努力した甲斐がありました。 チーム内でやってよかった行動 開発に詰まったとき、作業画面を共有してペアプロを行うようにしていました。自分では気づかない間違いにすぐ気づくことができ、さらに、方向性を話しながら修正できるので効率的に開発を進めることができました。 最後に 残念ながら賞を取ることはできませんでしたが、新卒研修後あまり接触がなかった同期とわいわいと話しながら開発ができてとても楽しかったです。 それぞれ2日間の開発で知識や技術力不足といった課題を見つけることができました。1年後パワーアップしてまた望みたいと思います。 ヤフー・PayPay・ZOZOテクノロジーズ 合同チーム(松浦・むーさん) 3チーム目は、ヤフー・PayPay・ZOZOテクノロジーズの3社の社員が集まった合同チームです。こちらのチームの参加レポートは後日ヤフーのテックブログにて公開されています。 techblog.yahoo.co.jp 最後に ZOZOテクノロジーズでは、プロダクト開発以外にも、今回のようなHack Dayやイベントの開催など、積極的に技術に接する機会を用意しています。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上やエンジニアの組織づくりにも興味のある方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! https://tech.zozo.com/recruit/ tech.zozo.com
こんにちは。ZOZOTOWN部の荒井です。 先日 WWDC20 が開催され、今年も弊社iOSメンバーが参加してきました。Apple Siliconや各次世代OSなど面白い発表が目白押しでしたね。 カンファレンスの内容も非常に興味深いものでしたが、今年は諸般の事情を鑑みて、初のオンライン開催となったことも印象的でした。 本記事ではWWDC20オンライン開催にあたり、ZOZOTOWN iOS担当のメンバーがどう臨んだのか、参加して感じた現地開催との相違点をお伝えします。また、Developer Labsに参加し、Appleのエンジニアと日頃疑問に思ってる点について話をしてきました。可能な範囲で内容を公開しますので、是非最後までご覧ください。 WWDC? WWDC(Worldwide Developer Conference)は、Appleが年に1度開催している開発者向けのカンファレンスです。ZOZOテクノロジーズでは海外カンファレンスへの参加が推奨・サポートされており、例年当選したメンバーが業務の一環として参加しています。現地の様子は昨年の参加レポートをご覧ください。 techblog.zozo.com オンライン開催にどう臨んだか 初のオンライン開催ということで事前情報が少なく、働き方も非常に悩ましいものでした。ここでは開催期間中の働き方、Developer Labs参加への事前準備について紹介します。 開催期間中の働き方 ZOZOTOWNのiOSチームは話し合った結果、以下のような働き方にしました。 現地時間に合わせた日本時間2:00 - 11:00での勤務 6月27日(土)を出社とし、6月22日(月)を休みに振り替え 各自自宅からの参加 Engineering Sessionsはいつでも視聴できることが想定されていましたが、Developer Labsへ参加することを中心に考えていたため、現地時間に合わせました。弊社はカンファレンスへの参加が推奨されており、フルフレックス制度といった働き方にも柔軟性があります。ただし深夜勤務は推奨されていないためWWDC期間中のみ例外的な対応をしました。なお、WWDC期間中の業務は事前に調整しています。オンライン開催に限らず、希望し当選したメンバーは業務として全員が参加予定でした。 Developer Labs参加への事前準備 Developer LabsはAppleのエンジニアやデザイナーに直接質問できる貴重な機会です。WWDCに参加する醍醐味でもあり、効率よく参加するために僕たちのチームでは事前準備を行っています。以下の内容をチーム全員で共有し管理しました。 質問内容 業務との関連性 ラボ名 参加者 英文 結果 この管理は例年行っていることですが、オンライン開催にあたり「1-on-1」「録音禁止」「英語のみ」といった開示があったため、今年はその点を考慮した内容となっています。結果10程度のラボに参加でき、有益な情報収集ができました。「英語のみ」については通訳係として当選者以外のエンジニアも参加できたので、言語面での不安も軽減できました。 当日になって質問を考えると漏れが発生したり、間に合わず申請できないといったことがあるため、今後Developer Labsへ参加を予定している方は事前準備をおすすめします。 開催期間中の動き 効率的な情報共有をするため、ルールを2つだけチームで決めて動きました。 Slackの専用チャンネルにてテキストでの情報共有 1日に1回通話での情報共有 基本的にコミュニケーションの場を整える目的で、日報のような目的ではありません。各々がEngineering Sessionsを観たり、Developer Labsへ参加したり、サンプルコードを書いたりと自由に行動していました。Engineering Sessionsは後日でも閲覧可能であると確認が取れたため、2日目以降はEngineering Sessionsの優先度を下げていたメンバーが多かったです。 現地開催との相違点 今回のオンライン開催は現地開催と比べてどうだったのでしょうか。参加メンバーが感じたことを環境、セッション、ラボの観点でオンライン開催のメリット・デメリットをお伝えします。 環境 はじめに、オンライン開催と現地開催でどのような環境面の差を感じたかを紹介します。 メリット 現地よりもネットワーク環境が良く、βのダウンロードや検証がスムーズ みんなオンライン参加なので、同じPDTでの活動がタイムラインに流れてきて、例年とにぎやかさが全然違う デメリット 深夜開催なので音声に配慮が必要 現地で見る方がモチベーションアップに繋がる 移動がなかったため、ラボでいきなり英語をしゃべる落差がある 他社のエンジニアと情報交換が難しい 物販がない 発表された技術の検証はオンライン開催の方がやりやすいという意見がありましたが、モチベーション面は現地開催の方があがるという意見が多かったです。 Engineering Sessions 次はEngineering Sessionsです。 メリット いつでも閲覧可能なので、何回も巻き戻したり、技術について話しあったりしてセッションが身近に感じられた 日本語・英語字幕に助けられた時もあった デメリット オンラインだとライブ感がなく、参加者が何に注目しているのか掴みにくい 当日に何回も巻き戻して確認できるのはオンラインならではのメリットです。ただし「拍手」などのリアクションがないため、盛り上がりポイントが分かりづらいという意見もありました。 Developer Labs 最後にDeveloper Labsについての相違点です。 メリット 予約制なのでAppleエンジニアも回答準備をしており、参考サイトの共有などもあった デメリット ラボに入り浸れない 混んでいるラボ、空いているラボが判断できず、予約状況も分からない 予約制なので手を動かしてもう一度聞きたい時にいけない オンラインではすべてのラボが予約制のため、ラボに対する自由度は少なくなりました。繰り返し行くということがしづらいため、限られた時間内で問題解決しなくてはいけないことがデメリットの意見として多かったです。 個人で作業する分にはオンラインの方が効率的な印象を受けました。ただ、やはり現地参加の方がモチベーションも上がりやすくエンジニアとのコミュニケーションも活発になるので、現地開催のメリットは大きそうですね。 Developer Labs ここからはDeveloper Labsで聞いてきた内容の一部を各メンバーが紹介します。Developer Labsでは業務に直結することを優先的に質問していますが、エンジニアがそれぞれ疑問に思った業務外のことも時間の許す限り質問しています。内容はAppleとのNDAのため、可能な範囲での紹介となります。 UICollectionViewとUITableViewのこれから こんにちは、ZOZOTOWN部の小松です。例年のWWDCはKeynoteしか見ていなかったのですが、今回はセッションやラボに参加し、初めてのWWDCの全参加となりました。 今回のセッションをみていく中で、とあることに気付きました。UICollectionViewに昨年同様新しい機能が追加されていて、その新機能追加により、UITableViewで実装していたレイアウトがUICollectionViewでも実装しやすくなるとのことです。 「そうすると、UITableViewとどのようにして使い分けの判断をするべきか?」その疑問をAppleのエンジニアにぶつけるべく、UIKit and Build for iPad labへ参加することに決めました。初めてのラボでしたが、Appleのエンジニアの方は大変優しく、質問に対してとても丁寧にお答えいただいたのが印象的です。 今回質問した内容は以下の3つです。 UICollectionViewとUITableViewのどちらを使用するべきなのか? UITableViewはDeplicatedになるのか? なぜ、既存でUITableViewがあるのに、UICollectionViewListを作ったのか? 結論として、もし既存のアプリとしてUITableViewを使用しているのであれば、そのままUITableViewを使用し続けることには問題ない。しかし、新しいアプリを作るのであれば、UICollectionViewListが推奨なようです。今後はおそらく、UICollectionViewには追加されるが、UITableViewには追加されないであろう機能が増えていくであると思われるそうです。UICollectionViewListを使用するメリットは、複雑なレイアウトを容易に組むことができるようになることだそうです。たとえば、App Storeのアプリを参考にみてもらうとわかりやすいとのことでした。 以上を踏まえると、現状UITableViewを使用し続けることに問題はないが、より簡単にそして最新の機能を使うためにも徐々にUICollectionViewへと移行していくのが良さそうです。ただ、UICollectionViewListはiOS 13以上対応なので、今すぐにとはいかないところが難しい点ですね。 Appleのエンジンニアにxcresulttoolの使い方を教えていただきました ZOZOTOWN部の林です。 Xcode11からxcresultファイルを解析できるxcresulttoolコマンドが推奨されています。 View and share test results ZOZOTOWNアプリでも今後テストを充実していくために、xcresulttoolを利用することが避けられないでしょう。xcresulttoolをもっと理解するために、Testing and Continuous Integration lab(25分)に申請を出して当選しました。 事前にサンプルアプリのUIテストを用意して、達成したいことを申請時に伝えておりました。xcresultからUIテストで撮ったスクリーンショットを取り出して、指定したフォルダに保存することを目標としました。そして、ラボ当日にAppleのエンジニアの指示を聞きながら、用意したxcresultの解析を行いました。 xcresulttoolでxcresultファイルから変換されたjsonファイルの確認方法、xcresultのツリー階層など、xcresulttoolについてドキュメントに記載されていない内容もたくさん教えていただきました。 情報を聞くだけではなく、コマンドの操作も教えていただいたので、欲しかった結果を得られた瞬間、一緒に仕事ができた気分になりました。自分にとって貴重な経験でした。 SideBarはハンバーガーメニューと違うのか? ZOZOTOWN部のえんどうです。 iPadのナビゲーションにSideBarが推奨されるようになりましたね。 しかし、WWDC 2014の「Designing Intuitive User Experiences」のセッションでiOSにはハンバーガーメニュー(a.k.a SideBar)は推奨しないとありました。ではなぜ、2020年では推奨されるようになったのでしょうか? そもそも、「ハンバーガーメニューとAppleのいうSideBarは違うものなのか?」ということが気になりラボで質問をしてきました。 結論として、SideBarとハンバーガーメニューは違うものという回答でした。その違いは「常に表示されているか」です。 ハンバーガーメニューは非表示のところからユーザーがハンバーガーメニューを表示しなければなりません、SideBarは常に表示されています。そのため、ユーザーはいつでもどこにいるのか見失うことはありません。 ナビゲーションでは、「何が見つけられるか」「どこにいるか」「どこにいけるか」が大切だと教えてもらいました。この考えは画面遷移を考える上でとても重要なことなので意識して気をつけていきたいと思いました。 Interface Builder and Auto Layout lab ZOZOTOWN部の名取です。 ZOZOTOWNではInterface Builderを多くの画面で使用しているため、特定のStoryBoardを複数人で開発する際のtipsを教えてもらいました。 複数のViewControllerが配置されたStoryBoardにおいてはXcodeのEditor→Refactor To StoryBoardを選択することで特定のViewControllerのみを切り出したStoryBoardを作成できます。 この機能によりコンフリクトの発生を幾ばくか抑えることができるため複数人で開発する際は推奨とのことでした。 また近年SwiftUIが大きな盛り上がりを見せていますが、Interface Builderが将来的になくなることはなくそれぞれが違う技術として共存していくだろうという話もされていました。 NotarizationとWatch FaceとXcode 11.4の静的リンクについてラボで聞いてきました ZOZOTOWN部自称macOS担当の @banjun です。今年は10年に1度のウニ(・∀・∀・)バーサルyearでしたね(注:PPC→Intel移行のときの流行語)。私は AquaSKK などのOSSのmacOSアプリをメンテしていることもあり、 Universal App Quick Start Program にも参加しています。 ラボでは「**アプリとCLIではNotarizationチェックの仕組みと検証方法が異なる**こと(= 具体例では[SwiftBeaker]( https://github.com/banjun/SwiftBeaker )を検証する方法)」「Watch FaceでサポートされているLive Photosのfps上限や、**Watch Face Sharingのシリアライズフォーマット**が公開仕様か」「Xcode 11.4で導入されている**静的リンクのビルド前チェックとApple Siliconとの関連**や今後のライブラリーの管理はどう変わっていくのか」などを聞いてきました。いずれの知見も次の開発に活かせそうです。 まとめ 今回はWWDC20の参加レポートをお伝えしました。オンラインは初の試みでしたが、チームとしては参加して成功だと感じています。リアルタイムで情報をキャッチアップすると共に、すでにZOZOTOWNではiOS 14への調査・対応も進めています。来年は現地でWWDCが開催されると良いですね。 さいごに ZOZOテクノロジーズでは、一緒にモダンなサービス作りをしてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! https://tech.zozo.com/recruit/ tech.zozo.com
はじめまして、SRE部の高塚です。新卒として4月に入社し、チーム研修ではBEARというSlackアプリを作成しました。 technote.zozo.com BEARは社内システムとして正式に導入が決まり、準備期間として1か月半が用意されました。この記事では、その期間に行ったインフラの再構築について紹介します。 BEARとは 旧BEARが抱えていた問題点 コード化されていないインフラ 自動化されていないデプロイ Re:ゼロから始めるインフラ構築 インフラをコード化する デプロイを自動化する まとめ 最後に BEARとは まずはBEARについて簡単にご説明します。なおこの章は読み飛ばすことも可能です。 BEARは本のレビューをRDBで管理し、Slackアプリとして CRUD や検索が行えるシステムです。AIによる「あなたにおすすめの本」の推薦機能もあります。 弊社には 書籍購入補助制度 があり、これまではSlackチャンネルに各自が本のレビューを投稿していましたが、以下のような問題がありました。 検索しにくい 過去のレビューを遡りにくい 書籍名やAmazonの購入ページのリンクなど、入力項目が多い BEARはこれらの問題を解決したSlackアプリです。 繰り返しで恐縮ですが、詳細は以下のカンパニーブログをご覧ください。 technote.zozo.com かくして新卒研修の発表会のあと、BEARは正式な社内システムになることが決定したのですが、インフラを担当していた私は青ざめました。 なぜなら発表会で披露したデモアプリのインフラは、まったく本番運用を想定したものではなかったからです!(ハッカソンあるある?) 旧BEARが抱えていた問題点 デモ時点でのBEAR(以下、旧BEAR)は、2つのインフラの問題を抱えていました。 コード化されていないインフラ 旧BEARはAWSで稼働していますが、各リソースはマネジメントコンソールの画面から作っていたため、詳細な設定はインフラ担当の自分しか把握していませんでした。 もちろんドキュメント化を心がけてはいましたが、設定を変更したあとドキュメントの更新を忘れ、バックエンド担当に迷惑をかけたこともありました。変更前のレビューや変更履歴の管理もできていませんでした。 自動化されていないデプロイ 旧BEARはデプロイをすべて手動で行っていました。例えば基盤のAPIを更新する際は、社内VPNからEC2にSSHでログインし、 git pull や docker-compose up をしていました。毎回とても面倒くさいことに加え、オペレーションミスも多発しました。また、デプロイのたびにダウンタイムが発生していました。 Re:ゼロから始めるインフラ構築 これらの問題を解決するため、新BEARではインフラをゼロから作り直しました。参考程度にアーキテクチャ図もご紹介します。基盤のAPIはECSに変えることで、ダウンタイムなしのデプロイを可能にします。 また今までは私一人でインフラ周りの作業を行っていましたが、同じくSRE部の新卒・川津にも合流してもらい、インフラチームとして開発を進めました。 インフラをコード化する まず取り組んだのがインフラのコード化です。一般にはInfrastructure as Code(IaC)と呼ばれています。IaCのメリットを以下に挙げます。 GitHubが使える。つまりバージョン管理ができ、変更履歴も残り、レビューもできる 手順書や環境定義書が不要になり、それらに起因するヒューマンエラーを防げる 雛形を作っておくことで、様々なプロジェクトでそれを使い回せる AWSの場合はCloudFormationを使うことで、リソースの設定をYAML(またはJSON)で書くことができます。なおCloudFormation自体は無料で、実際に作ったリソースだけに料金がかかります。 例えばVPCにプライベートサブネットを作る場合は以下のようになります。 AWSTemplateFormatVersion : 2010-09-09 Resources : VPC : Type : AWS::EC2::VPC Properties : CidrBlock : 10.10.0.0/16 EnableDnsHostnames : true EnableDnsSupport : true Tags : - Key : Name Value : my-vpc PrivateSubnet1a : Type : AWS::EC2::Subnet Properties : VpcId : !Ref VPC #上のVPCを参照している AvailabilityZone : ap-northeast-1a CidrBlock : 10.10.10.0/24 Tags : - Key : Name Value : my-private-subnet-1a このファイルから実際にリソースを作るには、aws-cliの以下のコマンドを使用します。 # テンプレートの検証 aws cloudformation validate-template --template-body file://my-file.yml # 変更セットの作成 aws cloudformation create-change-set \ --change-set-type " UPDATE " \ #初回はCREATE --stack-name " my-stack " \ --change-set-name " my-stack-change-set " \ --template-body " file://my-file.yml " # 変更セットの実行 aws cloudformation execute-change-set --change-set-name " my-stack-change-set " このようにCloudFormationを使い、すべてのAWSリソースをコード化しました。 しかし、せっかくIaCを実現しても、コマンドを人間が打ち間違えたら台無しです。 そこで、上記コマンドの実行を含む、すべてのデプロイ作業をコード化・自動化していきました。使ったのはGitHub Actionsというサービスです。 デプロイを自動化する GitHub Actionsは、GitHubが用意する仮想マシン上で任意の処理を行えるサービスです。リポジトリへのプッシュやプルリクエスト、あるいは日時指定といったトリガー条件が用意されています。処理やトリガー条件をYAMLで書き、 .github/workflows/ に置くと自動で実行されます。 気になるお値段は、パブリックリポジトリでは無料、プライベートリポジトリでも一定量までは無料です!(詳細は GitHub公式サイト をご覧ください) 例えばプルリクエストをトリガーにechoするには以下のようにします。 name : my-actions on : pull_request : branches : - master types : [ opened, synchronize, closed ] jobs : Greet : name : Greet runs-on : ubuntu-latest #仮想マシンの種類 steps : - name : Hello world run : | #コマンドは複数実行できる echo "Hello world!" echo "Hello world!!!" AnotherJob : # 以下省略 次に、GitHub Actionsでaws-cliを使う例として、画像のディレクトリをS3にアップロードするコードを示します(詳細は こちら )。この処理は新BEARにも組み込まれています。 - name : S3 sync working-directory : static #カレントディレクトリの指定 run : aws s3 sync . s3://my-static-files --delete --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --exclude "README.md" 以下のように自作のスクリプトを実行することもできるので、可能性は無限大です!ただし仮想通貨のマイニング等は GitHub利用規約 で禁止されています。 - name : Execute script working-directory : scripts #カレントディレクトリの指定 run : ./my-script.sh 新BEARでは、GitHub Actionsで主に以下の処理を行っています。 プルリクエストの作成や編集で実行 lint ユニットテスト プルリクエストがマージされたときのみ実行 CloudFormationの実行(=AWSリソースの作成) Dockerイメージのビルド、プッシュ、ECSタスクの更新 ソースコードのアップロード、Lambda関数の更新 これにより、テストからビルド、デプロイまでを自動化できました。プルリクエストが作成されたらすぐに一連の処理が実行され、もしテストやビルドでエラーが発生したらGitHub上に通知されます。問題なくプルリクエストがマージされれば、すぐにAWS環境がダウンタイムなしで更新されます。 これは一般にCI/CD(継続的インテグレーション・継続的デリバリー)と呼ばれる手法です。BEARはCI/CDで開発効率を高め、リリースサイクルを短くできました。 まとめ 新卒研修で作ったSlackアプリのインフラを再構築し、IaCとCI/CDを実現しました。CloudFormationとGitHub Actionsを使うのは初めてで苦労しましたが、とても勉強になりました。 ちなみに、ここまで新卒研修についてご紹介しましたが、ZOZOTOWNのリプレイスプロジェクトではより高度なCI/CDを実現しています。よろしければこちらの記事もお読みください。CI/CDについてもっと詳しく学べます! techblog.zozo.com 最後に ZOZOテクノロジーズでは、SREをはじめ様々な職種で一緒に働く仲間を募集中です。ぜひ以下のページをご覧ください! tech.zozo.com また、7月16日には「案件別採用説明会」をオンラインで開催します!このイベントでは「ZOZOのやりたいことリスト」に掲載している10の案件について、今やっていること、今後やっていくことを赤裸々にお伝えする予定です。奮ってお申し込みください! tech.zozo.com zozotech-inc.connpass.com
こんにちは。ECプラットフォーム部 推薦基盤チームで、DWH・DMP・広告まわりのデータエンジニアリングを担当している大谷です。 本記事では、マーケティング部門の広告運用のインハウス化に伴ってこれまで取り組んできた広告データの収集と活用、その仕組みにフォーカスして事例をご紹介します。 背景 データの収集と活用 Arm Treasure Data Integrations Hub ログ収集 アクセスログ 検索インプレッションログ Workflow フィードローダー (Google) レポーティング Googleスプレッドシート × BigQuery CausalImpact (Google) まとめ 背景 ZOZOでは事業・開発部門を問わず、様々な部門のスタッフが各自の業務に必要となるデータを取り扱い、レポーティングなどに活用する文化が根付いています。社内では人づてにデータを扱うノウハウが伝わり、Google BigQueryの発行済アカウント数は現在200を超え、様々なビジネスシーンにデータが活用されております。 マーケティング部門はここ1年ほどで広告運用がインハウス体制に切り替わりました。インハウス化以前から様々なセグメント配信を試行したり、日々レポートと向き合って効果の良し悪しを見極める運用体制ができていたので、現場の進め方が大きく様変わりすることはなかったように思います。 自社でアカウントを持つことによって、各種広告プラットフォームへ直接接続してデータの授受が可能になりましたので、キャンペーンの都度出稿先が変わるたびに接続先のパターンを増やしてきました。 データの収集と活用 弊社では現在、下図のような構成でデータの収集・活用を行っております。 Google BigQueryがデータ利活用の中心となる役割を担い、業務データ・Webログデータ・広告データ・データマート等、種々のデータがここに集約されております。 ZOZOTOWNの基幹データベースからBigQueryへの業務データ転送、データマートの更新処理については、DigdagとEmbulkを導入し運用しています。冪等性の保証やデータマートの依存関係を自動的に組み立てて実行される集計処理など、よく考え抜かれた仕組みとなっています。こちらについては、以前に弊社の塩崎・田島・平田の3名が記事を掲載しておりますのでご参照頂ければと思います。 techblog.zozo.com techblog.zozo.com techblog.zozo.com 広告データについては、オーディエンスリスト生成に必要な属性データはBigQueryから収集し広告プラットフォームへ転送。レポートデータは広告プラットフォームから収集しBigQueryへ蓄積し、レポーティングに活用されています。 現時点 1 で接続している広告プラットフォーム(代表3つを順不同で記載) Google Ads Facebook Ads Yahoo!広告 広告データ連携については、Google Compute Engineのインスタンス上もしくはCloud Functionsに、JavaもしくはPythonのプログラムを置いて動作させています。各種プラットフォームからクライアントライブラリが提供されているケースも多く、データをBigQueryと橋渡しする比較的シンプルな処理構造になっています。 内製処理と併せて、既成のプロダクトの力を借りることで、少人数でデータ運用が実現できています。ここでは、2つのプロダクトをご紹介します。 Arm Treasure Data ... 主に外部プラットフォームとのデータ連携ハブとしての役割を担う フィードローダー(Google) ... Google Adsのショッピング広告出稿に必要な商品フィードデータ連携を自動化する役割を担う それぞれの活用事例を見ていきましょう。 Arm Treasure Data Arm Treasure Data(以降TD)は、ログ収集・Workflow・分析・機械学習・外部データ連携などの機能を1つのプロダクトで実現する、フルマネージドなデータプラットフォームサービスです。弊社における活用事例をご紹介します。 Integrations Hub 広告プラットフォームへのデータコネクターが豊富に組み込まれており、それらを活用することで内製にかかる工数を削減し、データをビジネスに活用するまでのリードタイムを短縮できています。プラットフォームAPIの仕様変更がデータコネクターで吸収されたり、パラメータ変更方法などの事前アナウンスやサポートが受けられることも障害抑止に役立っています。 ログ収集 サイトから収集したログをニアリアルタイムで利用できるログ収集機能の強みを活用して、2種類のログを収集し活用しています。 アクセスログ ZOZOTOWNのアクセスログを収集しています。TDのSDKを使わず、IMGビーコンとしてサイトにタグを設置しています。このログをBigQuery ExportされたGoogle Analyticsのアクセスログと組み合わせることで、BigQuery上でセグメントしたオーディエンスデータを広告配信に活用しています。 タグの拡張パラメータに、Google AnalyticsのCookieの1つであるClientID(以降GoogleClientID)を渡してトラッキングします。これにより、ログ活用時にtd_global_id・各種CookieID・会員ID・GoogleClientIDの相互変換が可能になります。 TDのデータベースに記録されたアクセスログのうち、商品ページの閲覧ログを30分サイクルで名寄せして、ユーザーの閲覧データを生成後BigQueryのテーブルとして扱えるようにしています。このデータは、閲覧ベースの商品ランキングや、マーケティングオートメーションシステムのメール・プッシュ配信のレコメンド枠のデータソースとして活用されています。 検索インプレッションログ ZOZOTOWNの検索結果ページにおいて、インプレッションされた商品データの収集を行っています。Web/アプリの検索結果ページが表示され、ユーザーがインビューした検索結果の商品情報を1行単位でIMGビーコンタグにまとめてログ収集し、BigQueryのテーブルとして扱えるようにしています。このデータは、検索結果の分析やロジック改善のために活用されています。 これらの膨大なログ収集を、現在までダウンタイムやデータロスト等が1度も発生することなく運用できております。 Workflow 日々のCookie名寄せ処理、広告データの収集とオーディエンスデータの更新を自動化するため、Workflowで手続きを組んでいます。 Integrations Hubの豊富なコネクターをWorkflowで扱い、データ収集〜オーディエンスデータ更新のサイクルを自動実行するようにしています エラー通知をSlackへ集約しており、トラブルがあった際、すぐ対応できるようにしています もしこれらの用途を自社基盤で賄うとしたら、インフラ・システム開発/運用体制を用意する必要があり、ビジネス活用の段階までには相応のコストとリードタイムが発生します。実質1名のオペレーターが構築〜運用までを実現できている機能性が、TDを導入する最大のメリットだと考えております。 フィードローダー (Google) ECサービス企業が取り扱う広告の中でも効果的なものの1つが商品フィード広告です。サイト外の広告面に自社の商品名、画像、価格等の情報を掲載し、直接ユーザーが求める商品ベースでサイト流入〜購入に導くことができるため、一般的によいパフォーマンスが得られやすくなります。 商品フィード広告を出稿するためには、予め広告プラットフォーム各社へ自社の商品データを転送する仕組みを用意する必要があります。一般的に、各種プラットフォームのデータ仕様に応じた商品データの変換部分と、APIもしくはSFTPサーバ経由でデータを転送する部分の両方の準備が必要です。 フィードローダーは、Google Adsのショッピング広告出稿に必要となる商品データ転送を自動化するために開発され、Googleから提供を受けているツールです。 フィードローダーを利用するメリットは以下の通りです。 APIの繋ぎ込み不要 ... 広告主側で実装する必要があるGoogle Merchant CenterのContent API連携処理がツールに内包されています。 商品データの更新が高速 ... 最短1時間間隔の更新サイクルが組めることで、商品データ更新の遅れに伴って発生しがちなトラブルである在庫欠品や、プロパー価格とセール価格の切替不備などを低減できます。 フィードローダーのアーキテクチャはすべてGoogle Cloud Platform上で構成されます。予め作成したGoogle Cloudプロジェクト内でフィードローダーのインストーラを実行すると、必要なアーキテクチャがデプロイされ、実行環境が整います。 Google Cloud Storageのバケットに商品ファイルと転送完了ファイルを置くと、Google Merchant Centerのフィードデータ更新まで一連の処理が実行されます。 フィードローダーは、前回反映から変更・削除のある商品を検知して更新処理を行うため、短時間で処理が完了するように設計されております。このような仕組みを自社で用意するのは開発リソースがかかるので、止むを得ず少ない更新頻度で運用しているケースが多いのではないでしょうか。 弊社では、広告掲載・非掲載時を問わず、常時400万件ほどの商品データを数時間間隔で更新しています。現状かなり時間的な余裕を持たせていますので、さらに更新サイクルを早めることは可能です。 現在のところフィードローダーの不具合による更新不備などは発生しておらず、高い安定性も運用担当者にとって魅力のひとつとなっています。 レポーティング レポーティングにおいては、広告の出稿情報とその効果をデータとして一元管理することによって、以下の3点の課題の解決を目指しています。 マーケターやアナリストが、サイト流入の増減理由を正確に把握し、より精度の良い分析ができるようにする 広告プラットフォームによって仕様の異なる指標群を束ねてレポーティングするために指標を標準化する 広告によるアップリフト効果を定量的に把握できるようにする 1,2に関しては、広告プラットフォームのデータと、外部から収集できない独自のマスタ情報がセットで必要となるため、運用担当者がデータの参照とメンテナンスをしやすい方式にする必要がありました。 Googleスプレッドシート × BigQuery 慣れ親しんだツールであるスプレッドシートとBigQueryを組み合わせることで、データの参照と、マスタテーブルの管理が可能になります。スプレッドシートはBigQueryのデータコネクターを標準でサポートしており、日々BigQueryに蓄えられたデータをスプレッドシート上で扱うことができます。 広告レポートデータ、Google Analyticsのアクセスログ、業務データなどを、クエリベースで結合・集計したデータをシート上に展開できます。2020年6月30日時点でデータコネクターは Connected Sheets としてアップデートされ、クエリを記述しなくてもテーブルのデータを展開できるなど、さらに利便性が向上しました。 また逆に、スプレッドシートをBigQueryのテーブル(フェデレーションテーブル)として扱うこともできます。レポーティングに必要な自社のマスタデータをスプレッドシート上で更新し、テーブルとしてデータを扱うことが可能になります。 弊社では、シートをマスタにしたキャンペーン付加情報テーブルを用意しました。各種広告プラットフォームで運用しているキャンペーンIDに、広告の種類・常時掲載またはイベント掲載・成果地点はどこかといった付加情報を定義します。これにより、各種広告プラットフォームを横断して、種類別・目的別の収益・コストなどをクエリベースで集計可能にしました。 同様のことをBIや内製ツールなどで実現する場合、高機能なツールの使い方を学ぶ必要があったり、データ更新用のインタフェースを用意する必要があったりします。またツールが分散することでマスタデータの更新作業が疎かになりがちです。 レポーティングの初期段階では、スプレッドシート活用がおすすめです。 CausalImpact (Google) 3.広告によるアップリフト効果を定量的に把握できるようにする この課題を解決するために、GoogleがOSS提供している CausalImpact を活用しています。CausalImpactは、イベントやキャンペーンによってどのくらいの介入効果があったと推定されるかを定量的に評価してくれるライブラリです。 以下のようなパラメータを与えて実行することにより、評価結果を表組の数値もしくはプロットで描画可能です。 キャンペーンの影響を受けていないコントロール群の時系列データ キャンペーンの影響を受けているトリートメント群の時系列データ キャンペーン実施前の期間 キャンペーン実施期間 時系列の周期やサンプリング回数等のオプションパラメータ 弊社では、予めオーディエンスリストを居住地や会員IDのルール等の条件でコントロール群とトリートメント群に分けておき、トリートメントリストを配信対象に設定して配信します。そして配信後、コントロール群・トリートメント群それぞれに紐づく実績値を時系列データで与え、日別の初回購入数・新規登録数・新規訪問数などのアップリフト値を算出する運用を試行しています。 まとめ 本記事でご紹介した事例について、長く広告運用に携わられている方々の中には、こうすればもっと上手にできるのに…とご感想を持たれる方もいらっしゃるかと思います。ぜひ忌憚のないご意見・ご感想をお聞かせ頂ければ幸いです。 私たちのようにインハウス化に舵を切り、日々やり方を模索されている方々に少しでも参考になる事例が含まれていればと思い、記事を書かせていただきました。 ECプラットフォーム部 推薦基盤チームではメンバーそれぞれの強みを活かしながら、検索・レコメンド・広告など、打ち手となるさまざまなプロダクトの開発に取り組んでいます。 一緒にプロダクトを成長させていくメンバーを募集しておりますので、ご興味のある方は以下のリンクからご応募ください! tech.zozo.com 本記事が公開された2020年7月時点 ↩
こんにちは、ZOZOテクノロジーズ CTO室の池田( @ikenyal )です。 ZOZOテクノロジーズでは、6/22に ZOZO Technologies Meetup -ZOZOテクノロジーズの大規模データ活用- を開催しました。 zozotech-inc.connpass.com 「ZOZOテクノロジーズの大規模データ活用に興味のある方」を対象としたイベントです。 登壇内容 まとめ 弊社のエンジニア4名が登壇し、ZOZOテクノロジーズにおける大規模データ活用の事例紹介を行いました。 ZOZOTOWNを支える検索パーソナライズ基盤 (児玉 悠 / @dama_yu ) 大規模データをAIに活かすワークフローツールの紹介 (渡辺 慎二郎 / @shikajiro ) ZOZOTOWNにおけるRecommendations AI、AI Platformの事例紹介 (安田 征弘 & アニルドフ ジャムカンヂ / @anirudhgj ) 最後に ZOZOテクノロジーズでは、プロダクト開発以外にも、今回のようなイベントの開催など、外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com