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

TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

1025

こんにちはプラットフォームSREの亀井と 三神 です。 先日開催されました CloudNative Days Tokyo 2020 にて私達が取り組んできたID基盤リプレイスプロジェクトについて登壇してきました! ID基盤リプレイスプロジェクトはモノリスな環境をリプレイスするプロジェクトの1つであり、マイクロサービス化とそれに伴うメンバーの教育について挑戦した案件ですので是非とも御覧ください。 CNDTについて CloudNative Days Tokyo(以下、CNDT)は CloudNative Days における東京開催のイベントです。CloudNative Days実行委員会の方々が運営しているイベントで東京の他にも大阪や福岡等でも実績のある大規模な技術カンファレンスとなります。CloudNative Daysの開催目的は公式サイトにて以下のように紹介されています。 CloudNative Daysはコミュニティ、企業、技術者が一堂に会し、クラウドネイティブムーブメントを牽引することを目的としたテックカンファレンスです。 ​最新の活用事例や先進的なアーキテクチャを学べるのはもちろん、ナレッジの共有やディスカッションの場を通じて登壇者と参加者、参加者同士の繋がりを深め、初心者から熟練者までが共に成長できる機会を提供します。 ​皆様がクラウドネイティブ技術を適切に選択し、活用し、次のステップに進む手助けになることを願っています。 ​クラウドネイティブで、未来を共に創造しましょう。 ​ クラウドネイティブ上級者だけではなく初心者も対象とした間口の広いイベントなので、私達が得た知見を発表する良い機会と考えて参加しました。 2019年のCNDTは虎ノ門ヒルズフォーラムを会場として開催したのですが、今年は新型コロナウィルスの影響もありオンラインのみの開催となりました。スピーカーの我々は初の登壇かつ慣れないオンラインでの対応となりましたが、とてもスムーズな進行で無事に終えることができました。実行委員会の方々にはこの場で御礼を申し上げます。 オンライン登壇について 実行委員会の方より登壇方法については下記3つの方法を提案して頂きました。 自宅/自社オフィスからのリアルタイム配信 事前収録した動画を配信 配信会場からリアルタイム配信 今回私達は『事前収録した動画を配信』を選択しました。 事前収録しておけば当日に何らかのアクシデント(システム障害や体調不良等)があったとしても対応ができると考えたからです。 緊張して噛んでも編集でごまかせるとか考えてないです。 ​ 事前収録でも参加者からの質問を受け付けられるように Slido やTwitterのハッシュタグも用意されていて不自由を感じる事もなく、安心して対応ができました。 ZOZOにおけるID基盤のk8sへのリプレイスとセキュリティの取り組み こちらのセッションではZOZOTOWNのリプレイスプロジェクト全体と、ID基盤について亀井と同チームリーダーの瀬尾(a.k.a. sonots )にてお話しさせて頂きました。 動画リンクはこちらです。 event.cloudnativedays.jp アジェンダは下記の通りです。 ZOZOTOWNリプレイスの全体感 ID基盤の概要 更新系ワークロードのリプレイス方法 AWS(Amazon Web Services)/EKS(Elastic Kubernetes Service)で実施したセキュリティへの取り組み 課題と今後 我々のチームで進めているZOZOTOWNリプレイスとそれを加速させるための新アーキテクチャと新体制についての紹介、ID基盤リプレイスでの取り組みについて話しております。 サービスリプレイスを検討している方や、AWS/EKSを使いセキュリティ要件の高いインフラ構築を検討している方に少しでも参考になれば幸いです。 またセッションでは触れなかったCI/CD手法については、以前本ブログで川崎からご紹介しておりますのでご興味ある方はご一読ください。 techblog.zozo.com ​ Cloud Native Onboarding ~実践で身につけるモダンインフラの基礎~ こちらのセッションはオンボーディングについての内容です。三神と元同僚の inductor 氏にて登壇しました。 動画リンクはこちらです。 event.cloudnativedays.jp メンティーである三神、そしてメンターであるinductor氏が約3か月のオンボーディングについてそれぞれの視点で振り返った内容です。三神のパートでは『新しい事を学ぶ』という観点で自身が意識した事や改めて重要だと思う点をお話しました。 オンプレミス環境を運用してきた方がクラウドネイティブ環境について学ぶ事は今後も増えると思います。私達のチームにも10月より新メンバーが入り、クラウドネイティブな技術についてオンボーディング中です。この発表を見てオンボーディングを取り入れてもらい、新しいことを学ぶ人のお役に立てればとてもうれしく思います。 ここでは、セッションであまり触れる事ができなかった1on1について少し補足します。 私達のチームではKPTとは別の週にチームリーダーとの1on1を全メンバーがセッティングされています。チームリーダーから質問を受けるのではなく、メンバー側が聞きたいことや相談したい事を準備する形式で行っています。1on1の準備として自身の課題を整理して明文化する時間を定期的に取る事で早期解決をする事ができてとても役に立ちます。特にオンボーディング中は不慣れな業務の中で悩むことが多いので1on1として相談時間が確保されているのでは安心感がありました。 リモートワークによる働き方が一般化している中で、1on1は業務において重要な時間になります。円滑に業務を進めるための施策として、試して頂ければ幸いです。 おわりに CNDTでID基盤チームが半年間取り組んできた挑戦を技術面とチームビルディング面の両方から発表する事ができました。ID基盤での取り組みは今後のリプレイスプロジェクトの例として、とてもいい形で進められたと思っています。 弊社ではクラウド環境へのリプレイスプロジェクトがまだまだ道半ばとなっております。ご興味ある方是非一緒に進めていきましょう。以下のリンクからご応募ください! tech.zozo.com
こんにちは。SRE部MA基盤チームの川津です。 私たちのチームでは今年サービスを終了した「IQON」の10TBを超える大規模データをBigQueryからS3へ移行しました。本記事ではデータ移行を行った際に検討したこと、実際にどのようにデータ移行を行ったかを紹介します。 データ移行の経緯 IQONは2020年4月6日をもってサービスを終了しました。そのIQONではデータ分析にBigQueryを利用していましたが、Amazon Web Services(AWS)上にもIQONに関するリソースが存在します。そのため、IQONはGCPとAWSの2つのクラウドで運用していました。 しかし、サービス終了に伴いGCP・AWSどちらかにリソースを統一する必要が出てきました。統一する意図としては、終了したサービスが利用する取引先を減らし、請求対応などの事務的なコストを減らしたい意図がありました。そのためGCPとAWSの両方にあったデータをどちらか片方に寄せて、もう片方を解約することにしました。 解約するためにGCP・AWSどちらがIQONのリソースを多く利用しているのか確認を行いました。その結果、AWSではクローズ告知ページ用Webサーバーやドメインの管理を行っており、GCP側はBigQueryのみリソースを使用していました。移行の手間を考えた結果、AWSではなくGCPのリソースを消すことにしました。 以上の経緯からBigQueryのデータをAWS上に移行を行うことに決めました。 AWSにデータを移行するにあたり以下の要件を満たす必要がありました。 データにアクセスする頻度は年に数回程度なので、維持費を抑えたい データ量が多く、ローカルにダウンロードしてクライアントPCで検索すると時間がかかるため、BigQueryのようにSQLを使いクラウド上でデータ検索をしたい 以上の要件よりデータを保存するリソースとしてS3を選択しました。S3には S3の料金表 にある通り、低頻度アクセス向けの料金プランがあり、データの取り出しに料金がかかる代わりにストレージの料金を抑えるプランがあります。 またAmazon Athenaを利用することでS3に保存したデータに対してSQLを利用して検索できます。Amazon Athenaは Amazon Athenaの概要 にある通り、CSV、JSON、ORC、Parquetなどのデータフォーマットに対応しています。加えて圧縮されたファイル形式に対してもSQLが実行でき、データを確認できます。現在は ドキュメント に記載のあるSnappy、zlib、LZO、gzip、bzip2形式がサポートされています。 事前準備 アーキテクチャの選定 まずBigQueryからS3へ移行するにあたりどのような方法があるか調べることにしました。 アーキテクチャを選定する際、下記の項目を考慮しました。 費用をなるべく抑える 導入コストをなるべく小さくする データの欠損が起こらないようにする 以上を踏まえて検討をした結果、以下のようなアーキテクチャを採用しました。 下記の手順で作業を行います。 bq extractコマンドを用いてGCSへテーブルデータを転送する gsutil rsyncコマンドを利用しGCSからS3へ転送する GCSからS3へ移行できたか確認する ポイントは gsutil rsync コマンドを使いS3へデータを移行する点です。 gsutil コマンドはGoogle Cloud Storage(GCS)へアクセスできるコマンドラインツールで、Google Cloud PlatformがOSSとして公開されています。そして、gsutil rsyncコマンドはgsutilコマンドで提供されているコマンドの1つです。 gsutil rsyncコマンドはバケット間の同期ができる機能であり、この機能はGCP間のバケットの同期だけでなく、S3とGCS間のバケットの同期もサポートしています。また、オプションに -m をつけることにより並列でデータを同期することもできます。 gsutil rsyncコマンドを使う際には気をつける点があります。 gsutil rsyncの注意点 にも記載があるので引用して紹介します。 Note 2: If you are synchronizing a large amount of data between clouds you might consider setting up a Google Compute Engine account and running gsutil there. Since cross-provider gsutil data transfers flow through the machine where gsutil is running, doing this can make your transfer run significantly faster than running gsutil on your local workstation. この注釈の文脈から、gustil rsyncコマンドを利用する際、クラウド間の転送を行うと一度コマンドを実行した環境内にデータ転送される仕組みであることがわかります。そのため、転送するデータのサイズが大きい場合には、手元のPC環境から転送する際に注意する必要があると言えます。 IQONのデータサイズをBigQueryの information schema から取得したところ、およそ10TBのデータ量がありました。データ量的に手元のPCで転送を行うのは難しいのでAWS環境のEC2インスタンスを利用し転送を行うことにしました。GCPではなくAWS環境を利用した理由は、会社としてAWSを活用しており、契約面においてEC2の利用料金を抑えることができるからです。 AWSのアーキテクチャの選定 次に、gsutil rsync実行のためのEC2インスタンスを用意するためのインフラ構成について考えます。 まずGCS→EC2、EC2→S3の2つの経路に分けてどのような構成にするか考えました。 前半の経路:GCS→EC2 GCS→EC2の経路に関して考えられる経路としては下記の経路が考えられます。 GCS → Internet → Internet Gateway → EC2(public subnet) GCS → Internet → Internet Gateway → NAT Gateway → EC2(private subnet) 1つ目の経路はEC2をpublic subnetに配置する構成です。 この構成にかかる費用は GCPネットワーク料金 を元に算出しています。 EC2をpublic subnetに置く場合、グローバルIPを直接割り当てることができるのでインターネットと通信を行うことができます。しかし不特定多数のサーバーから通信を行うことが可能になるため、EC2のセキュリティグループの設定に気をつける必要があります。 2つ目の経路はEC2をprivate subnetに配置する構成です。 こちらの構成では、 GCPネットワーク料金 と NAT Gateway料金 を参考にして費用を参考にしています。 private subnetにEC2を配置することで外部のトラフィックを遮断できます。しかし、EC2単体だとGCSへ通信を行えないのでNAT Gatewayを配置してGCSへ通信を行うことができるようにしています。NAT Gatewayを配置することで外向きの通信を行うことができます。そしてprivate subnetにEC2を配置しているので内向きの通信を遮断できます。よって外部からの通信を必要とする攻撃を防ぐことができます。 ここで、10TBのデータを転送すると仮定してGCS→EC2の経路でかかる費用を算出してみます。 1つ目の経路を図と照らし合わせるとGCS → Internet Gateway間で料金が発生します。GCS → Internet Gateway間の料金は GCPネットワーク料金表 より0.11(USD/GB)なので1つ目の経路は約1100USDかかることがわかります。 2つ目の経路を図と照らし合わせるとGCS → Internet Gateway、NAT Gatewayで料金が発生します。NAT Gatewayの通信は NAT Gatewayの料金表 から0.062(USD/GB)と確認できるので約600USDかかることになります。1つ目の経路で計算したGCS → Internet Gateway間の料金と合計すると約1700USDです。 後半の経路:EC2→S3 次にEC2→S3の経路です。こちらは下記の経路が考えられます。 EC2(public subnet) → Internet Gateway → S3 EC2(public subnet or private subnet) → VPC Endpoint → S3 EC2(private subnet) → NAT Gateway → Internet Gateway → S3 1つ目の経路としてはInternet Gatewayを通る経路です。 この構成にかかる費用は AWSネットワーク料金 を元に算出しています。 2つ目の経路はVPC Endpointを経由する経路です。こちらはEC2をpublic subnetかprivate subnetに置く2つの方法が存在します。通信経路の部分はこの2つに相違点がありますが、後述する料金に関しては同じなのでまとめて考えます。 この構成にかかる費用は VPC Endpoint料金 を元に算出しています。 VPC Endpointを利用することで料金を抑えながらEC2 → S3の経路を内部の通信で完結できます。VPC EndpointはInterface EndpointとGateway Endpointの2種類存在します。今回はAmazon S3で利用可能なGateway Endpointを利用しています。 料金に関しては Gateway Endpointの料金説明 に記載のある通り追加料金なしで利用できます。しかし、用途次第ではVPC Endpointの料金とは別に、通常のAWSデータ転送料金が発生します。今回の用途の場合、 AWSのデータ転送料金 に説明があるので引用します。 Data transferred between Amazon S3, Amazon Glacier, Amazon DynamoDB, Amazon SES, Amazon SQS, Amazon Kinesis, Amazon ECR, Amazon SNS or Amazon SimpleDB and Amazon EC2 instances in the same AWS Region is free. つまり同一リージョンにあるEC2とS3間の通信は無料です。今回はEC2のリージョンとS3のリージョンを同じにしているので0USDで利用できます。 3つ目の通信はNAT Gatewayを経由し、Internet Gatewayを経てS3へ通信する経路です。 この構成にかかる費用は NAT Gateway料金 と AWSネットワーク料金 を元に算出しています。 3つ目の経路に使っているNAT GatewayはGCS→EC2に用いたNAT Gatewayと同じ用途で使っています。 EC2→S3の部分でも10TBを転送すると仮定してEC2→S3の経路までの費用を算出してみます。 1つ目の費用を図から辿るとEC2 → Internet Gatewayの間で料金が発生します。そのため AWSネットワーク料金 によると0.114(USD/GB)発生することになります。合計すると約1200USD発生することがわかります。 2つ目はVPC Endpointを経由してS3と通信します。 Gateway Endpointの料金説明 からEC2がpublic subnet、private subnetに配置しても0USDであることがわかります。 3つ目の費用はNAT Gateway、NAT Gateway → Internet Gatewayで発生していることがわかります。 NAT Gatewayの料金表 からNAT Gatewayを通る際0.062(USD/GB)発生し、AWSネットワーク料金も別途発生するので AWSネットワーク料金表 から0.114(USD/GB)かかることがわかります。合計すると約1700USDです。 経路全体:GCS→S3 単純に考えるとGCS→EC2とEC2→S3の経路の組み合わせで6通り考えられますがpublic subnet or private subnetの条件で以下の4つの経路の組み合わせに絞られます。 GCS → Internet → Internet Gateway → NAT Gateway → EC2(private subnet) → VPC Endpoint → S3 GCS → Internet → Internet Gateway → NAT Gateway → EC2(private subnet) → NAT Gateway → Internet Gateway → S3 GCS → Internet → Internet Gateway → EC2(public subnet) → VPC Endpoint → S3 GCS → Internet → Internet Gateway → EC2(public subnet) → Internet Gateway → S3 各経路で発生する費用をGCS→EC2、EC2→S3の2つの経路に分けて計算した費用を組み合わせると下記の通りです。 1700USD 3400USD 1100USD 2300USD 最後にNAT GatewayとVPC Endpointの有無での組み合わせを表にまとめます。 VPC Endpointあり VPC Endpointなし Private Subnet, NAT Gatewayあり 料金:1700USD セキュリティ:内向きの通信を遮断できる 通信経路: 1つ目 料金:3400USD セキュリティ:内向きの通信を遮断できる 通信経路: 2つ目 Public Subnet, NAT Gatewayなし 料金:1100USD セキュリティ:アクセス元をより厳格に管理する必要がある 通信経路: 3つ目 料金:2300USD セキュリティ:アクセス元をより厳格に管理する必要がある 通信経路: 4つ目 上記の表より、1番コストが低いのは左下の項目です。懸念点はセキュリティなのですが、今回用意したEC2インスタンスはgsutil rsyncを実行するだけで、内向きの通信はオペレーション用のSSHしかありません。public subnetに置く際、アクセス元を限定したSSHだけを許可して露出を最低限にしました。 これらの考察から、コストが一番安く、セキュリティも設定をしっかりすれば担保できる3つ目の経路の構成を採用することにしました。最終的な構成は下記の図の通りです。 移行手順 今回移行するBigQueryのデータはテーブルの数とテーブルサイズが大きいので、スレッドプールを作ってJOBを効率的に処理するためRubyを用いて自動化しています。 データ転送:BigQuery → GCS まずgsutil rsyncを扱うにはBigQueryに存在するデータをGCSに移行する必要があります。GoogleはRubyに対してBigQueryのSDKを提供しており、 extract_job メソッドを使うことによって対象のテーブルをGCSに転送できます。extract_jobを使う際にポイントが2つあります。 1つ目のポイントとして、extract_jobメソッドは転送するデータの圧縮形式が指定できる点です。圧縮形式はCSVであれはgzip形式がサポートされています。今回のIQONのデータは10TB以上あることがわかっています。そのためファイルを圧縮して転送できれば先程計算したデータ転送の料金を抑えることができます。また最終的にAmazon Athenaを利用する際もgzip形式でクエリを実行することが可能です。しかしgzipでどれだけ料金が抑えられるかわからないので、いくつかのファイルをgzipで圧縮し確認しました。適当にCSVのファイルを5ファイルほど用意しgzipで圧縮しました。下記の表が圧縮結果です。 圧縮前(Byte) 圧縮後(Byte) 圧縮率(%) 81920 7168 92 11264 2048 82 1266989 49177 62 23552 6843 71 57344 11787 80 gzipに圧縮するとおよそ70〜80%ほど圧縮できました。そのため、10TBのデータも7〜8割ほど圧縮できると予想できます。結論として、データ移行の料金は転送したデータの量に比例するので7〜8割ほどgzipで料金コストを削減できることがわかりました。他にgzipで圧縮した際に起こるデメリットはAmazon Athenaでクエリを実行する際、gzipを解凍する必要があるので速度低下が考えられます。しかしgzipにすることでクエリを実行する際のデータ量を削減できるのでAmazon Athenaの利用料金を抑えることができます。クエリを実行する頻度として年1〜2回程度実行する程度なのでS3の利用料金を抑える利点やAmazon Athenaの利用料金を抑える点を考慮するとgzipで圧縮するメリットが大きいのでgzipで転送しました。 2つ目のポイントはextract_jobを用いてファイルを転送する際、転送するファイルを分割する必要がある点です。分割する必要があるファイルの条件はサイズが1GB以上あるファイルです。そのため転送する元データのサイズが1GB以上の場合は別名を付けてファイルを分割する必要があります。 公式ドキュメント によるとワイルドカードで指定ができます。今回は下記のようなURIで分割を行いました。 定義するURI gs://hoge/file-*.csv 出力されるファイル名 gs://hoge/file-000000000000.csv gs://hoge/file-000000000001.csv gs://hoge/file-000000000002.csv . . . 実際に移行で利用したコードを以下に示します。これをEC2上でバックグラウンド実行しました。 require " google/cloud/bigquery " require " google/cloud/storage " require " logger " require " parallel " def import project_id = "" bigquery = Google :: Cloud :: Bigquery .new( project : project_id) storage = Google :: Cloud :: Storage .new( project : project_id) bucket_name = "" bucket = storage.bucket( "" ) bq_table_name = [] bigquery.datasets.all.each do |dataset| # 100並列で転送を行う Parallel .map(dataset.tables.all, in_threads : 100 ) do |table| if (table.bytes_count / ( 1024.0 * 1024.0 * 1024.0 )) < 1 import_gcs(table, bucket_name, dataset.dataset_id, " -*.csv.gz " , " CSV " ) else import_gcs(table, bucket_name, dataset.dataset_id, " -*.csv.gz " , " CSV " ) end end end end def import_gcs (table, bucket_name, dataset_name, extend , extension) log = Logger .new( " log.txt " ) uri = " gs:// #{ bucket_name } / #{ dataset_name } / #{ table.table_id } / #{ table.table_id }#{ extend }" extract_job = table.extract_job uri, compression : " GZIP " , format : extension do |config| config.location = " US " end extract_job.wait_until_done! if extract_job.failed? log.debug( "#{ table.table_id } failed " ) log.debug( "#{ extract_job.error }" ) end return extract_job.failed? end import() 実装のポイントは Parallel を用いて並列で転送を行っている点です。最初は並列に行わず直列で処理を行っていたのですが1日経っても終わりませんでした。CloudWatchでEC2のメトリクスを確認するとネットワークの帯域やCPU使用率は余裕がありそうでした。BigQuery → GCSの転送自体はGCP側で行っているので100並列で様子を見ながら転送を行いました。その結果、半日かからず終了させることができました。 また、念のためRubyのloggerで簡単なログを取っています。extract_jobの戻り値の failed? でJobの成功、失敗を確認できます。最初はログを取っておらず、途中でプログラムが落ちた際どのテーブルで失敗したのかがわからず原因を突き止めるのに苦労しました。結論としては、特にJobが失敗したログは発生しませんでした。 最終的にgzipで転送した結果、元のデータサイズと比較すると7〜8割ほどデータを圧縮できました。さらに、料金に関しても7〜8割コストを削減できました。 データ転送:GCS → S3 GCS→S3に関してはgsutil rsyncコマンドを使い転送を行いました。 S3のディレクトリは下記の構成にしました。 ├── BigQueryのdataset名 │   ├── BigQueryのtable名 │   ├── (BigQueryのtable名).csv.gz gsutil rsyncで転送する際は、ルートprefixからgsutil rsyncを行うとエラーが出た際に始点が最初からになってしまうので、今回はdatasetのprefix毎に分けて転送します。 転送には時間がかかるので、ログを残す点やバックグラウンドで動かす点などに注意し、下記のコードを実装しました。 require " google/cloud/bigquery " require " google/cloud/storage " require " parallel " require " logger " def gcs_to_s3 project_id = "" storage = Google :: Cloud :: Storage .new( project : project_id) log = Logger .new( " log.txt " ) bucket = storage.bucket( "" ) s3_bucket_name = "" gcs_bucket_name = "" directory_name = bucket.files( delimiter : " / " ) directory_name.prefixes.each do |directory| log.debug( " start #{ directory }" ) success = system( " gsutil -m rsync -r gs:// #{ gcs_bucket_name } / #{ directory.gsub( " / " , "" ) } s3:// #{ s3_bucket_name } / #{ directory.gsub( " / " , "" ) }" ) if not success log.debug( " failed #{ directory }" ) next end log.debug( " success #{ directory }" ) end end gcs_to_s3() 上記のプログラムで想定通りにgsutil rsync側で転送が行われているか確認を行いました。CloudWatchでEC2のメトリクスを確認するとCPU使用率が飽和している状態でした。CPU使用率が飽和している場合の対策としてEC2インスタンスのインスタンスタイプを上げたり、EC2インスタンスを複数作成して処理を分散する対策が考えられます。しかし、S3とGCPバケットの総データ量を都度確認しファイルの転送速度を確認すると対策をするほど遅くなかったのでこのままの状態で転送を行いました。 ファイルの確認作業 最後の作業として、GCSからS3にデータを転送する際、欠損が起きていないか確認を行います。 確認する項目は以下の通りです。 ファイルの存在確認 GCSとS3のチェックサム検証 GCSとS3のサイズ比較 上記の各項目の確認方法について説明します。 ファイルの存在確認 GCSとS3にあるファイルの存在確認をするためにはコンソール上で確認する方法があります。しかし、ファイル数が数千ファイル存在するのでファイルを1つずつ確認するためには時間と労力が必要です。 そのため、GCS、S3に対象のprefixが存在するか比較し存在の有無を確認します。GCSに存在するファイルはすでにBigQueryから全て転送できていることが確認できているのでGCSのprefixを起点としてS3のprefixを確認します。 object メソッドの戻り値の exits? メソッドで対象のファイルが存在するか確認できます。 GCSとS3のチェックサム検証 チェックサムを確認することによってGCSから送られてきたファイルはGCSと同一のファイルであるか確認できます。hash値の確認に関しては手元に対象のファイルをダウンロードして確認する方法でも可能ですが、こちらも時間と労力が必要です。hash値はGCS、AWSのSDKを使用して確認可能なので各環境のSDKを使用し確認します。 S3に関しては Aws::S3::Object クラスの etag メソッドで確認できます。 GCPでは Google::Cloud::Storage::File クラスの md5 メソッドで確認できます。こちらはbase64でエンコードされた値が返ってくるのでmd5で比較するために一度デコードして比較します。デコードした値はbinaryなのでunpackを行う必要があります。 GCSとS3のサイズ比較 GCSとS3のサイズ比較は gsutil du コマンドを用いることで確認できます。下記のようなコマンドを入力するとbyte表記でバケットの合計サイズを確認できます。 # GCSのバケットの容量の確認する場合 $ gsutil du -s gs://bucket_name 123456 # S3のバケットの容量の確認する場合 $ gsutil du -s s3://bucket_name 123456 GCSとS3のサイズ比較に関してはバケット単位での比較なのでコマンドを複数回実行すれば確認できます。しかし、ファイルの存在確認とチェックサムの検証はファイル単位なので下記のスクリプトを利用して確認します。 require " google/cloud/storage " require " aws-sdk " require " parallel " require " logger " require " google/cloud/bigquery " require " digest/md5 " require " base64 " def check_file project_id = " iqon-data-mining " storage = Google :: Cloud :: Storage .new( project : project_id) resource = Aws :: S3 :: Resource .new( region : " ap-northeast-1 " ) log = Logger .new( " log.txt " ) s3 = resource.bucket( " iqon-backup " ) bucket = storage.bucket( " export-s3-failed " ) files = bucket.files() Parallel .map(files.all, in_threads : 100 ) do |obj| dataset_name = obj.name.sub( /\/.+/ , "" ) file_name = obj.name.sub( /.+\// , "" ) directory_name = file_name.sub( / - \d+. csv .+/ , "" ).sub( /. csv .+/ , "" ) s3_directory = "#{ dataset_name } / #{ directory_name } / #{ file_name }" s3object = s3.object(obj.name) if (s3object.exists?) # gcsのetagとs3のetagを比較 if (s3object.etag.gsub( "\"" , "" ) == Base64 .decode64(obj.md5).unpack( " H* " )[ 0 ]) puts( " checked " ) else log.debug( " file etag validation failed at gcs: #{ obj.name } /s3: #{ s3_directory }" ) end else log.debug( " file not found gcs: #{ obj.name } /s3: #{ s3_directory }" ) end end end check_file() 今回、上記のプログラムで確認作業を行った際、GCSとS3のファイルのhash値が合わない問題に遭遇しました。原因としては、GCS → S3へ転送済みのテーブルに対してBigQuery → GCSへファイル転送を再び行いGCSのファイルを上書きしてしまったことでした。BigQuery → GCSへ転送する場合、CSVの行の順序が保証されていません。そのためBigQuery → GCSへ転送するたびにhash値が変わってしまいます。結局S3に保存されているhash値が一致しないファイルを削除し、削除したファイルをGCSから再転送を行いました。 最後にAmazon Athenaを使ってS3に転送完了したファイルに対してクエリを実行してみました。結果としてgzipで圧縮されたファイルでも問題なく中身を確認できました。 まとめ BigQueryからS3に移行するまでの手順を紹介しました。S3へデータ移行が完了したのでGCP側のリソースを削除できました。今回のデータ移行は転送するデータ量がかなり多く、転送完了するまで数日かかりました。また移行するデータ量が多い場合は転送時に発生する料金も多く発生し、選定するアーキテクチャによって料金も大きく変わることがわかりました。そのため、たった1回のデータ転送でもデータを移行する前に移行でかかる時間と料金を見積もることが重要になると感じました。 移行が完了した後、継続的にS3の料金が発生します。現在S3の料金プランはS3標準プランに設定していますが徐々にプランを変更し最終的にS3 Glacier Deep Archiveプランへ移行する予定です。気をつけるべき点としてAmazon AthenaがS3にアクセスできるプランはストレージタイプがスタンダードかスタンダードIAであることがあげられます。S3 Glacierプランまで変更するとAmazon Athenaでアクセスするにはファイルをrestoreする必要があります。そのため今後Amazon Athenaでクエリを打つ必要がなくなったタイミングでS3 Glacier Deep Archiveプランへ移行します。 aws.amazon.com 料金を比較するとBigQueryの場合 長期保存プラン は0.010(USD/GB)に対してS3の S3 Glacier Deep Archive プランは0.002(USD/GB)です。そのためS3 Glacier Deep Archiveプランに移行すると月およそ82USD削減できます。 MA基盤チームではデータ転送に関わる業務が多く、他のチームと連携しながら仕事をすることがあります。今回のタスクをこなすことで他部署と関わりながらデータの転送方法について知ることができました。 最後に ZOZOテクノロジーズではより良いサービスを提供するための基盤作りを開発したい仲間を募集中です。以下のリンクからご応募ください。 https://tech.zozo.com/recruit/ tech.zozo.com
はじめに MSP技術推進部の基幹化推進チームの池田( @ikeponsu )です。 私達のチームでは、 マルチサイズプラットフォーム事業(MSP) におけるデジタルトランスフォーメーション(DX)の取り組みを行っています。その取り組みの1つに、ケアラベル作成自動化システムの開発・導入があります。 このケアラベル作成という業務ですが、元々は人の手で1つずつ行われていたものでした。以前書いた「 Go言語でケアラベル発行の自動化 」の記事の中ではプロトタイプの紹介をしましたが、今回は実際にプロダクトで使われる様になったシステムの構成や、どの様に導入を行ったかといった内容を紹介します。 techblog.zozo.com 弊社のケアラベル 以前書いた記事でも少し紹介しましたが、ここではそもそも弊社のケアラベルがどういったものなのか、説明していきたいと思います。 ケアラベルとは上記の様な、繊維製品になくてはならない品質表示のことを指します。ケアラベルは、家庭用品品質表示法の下に適切で明確な表示が義務づけられています。 ケアラベル表記は画像の上から順に、以下の項目で構成されています。 サイズ表記(サイズごとに表記が異なる) 洗濯表記(品番ごとに表記が異なる) 素材混率の表記(使用する生地ごとに異なる) 生産国表記(品番ごとに表記が異なる) 会社表記(固定) 2次元バーコード(製品番号ごとに異なる) 裏面:付記用語(使用する生地ごとに異なる) サイズ、品番、製品番号の関係性をTシャツを例に説明すると、以下の様に表せます。 Tシャツ(品番) ├── ブラック(カラー = 生地) │ ├── XS(サイズ) │ │ ├── 製造番号1(製造番号) │ │ └── 製造番号2(製造番号) │ ├── S(サイズ) │ │ ├── 製造番号1(製造番号) │ │ ├── 製造番号2(製造番号) │ │ └── 製造番号3(製造番号) │ ├── M(サイズ) │ │ └── 製造番号1(製造番号) │ └── L(サイズ) │ └── 製造番号1(製造番号) └── ホワイト(カラー = 生地) ├── XS(サイズ) │ └── 製造番号1(製造番号) ├── S(サイズ) │ ├── 製造番号1(製造番号) │ └── 製造番号2(製造番号) ├── M(サイズ) │ └── 製造番号1(製造番号) │ ├── 製造番号2(製造番号) │ └── 製造番号3(製造番号) └── L(サイズ) └── 製造番号1(製造番号) デザイナー業務の自動化 ケアラベルのデザイン作成は、元々デザイナーがAdobe Illustratorを使い手作業で行っていました。下図は実際に行われていた作業フローです。 品番ごとにAdobe Illustratorを使って洗濯表示を並べた画像を作成していた 品番・カラーごとにAdobe Illustratorを使って素材表記を書き込んだ画像を作成していた 上記の作業に加え関係各社、各部署が最終決定したデータを家庭用品品質表示法に基づいて正確にデザインに落とし込まなければなりません。しかしデータ自体揃うのが生産直前になることも珍しくないため、短期的に負荷が集中し、差し戻し等も多発していました。 更に、1シーズンで数十品番を展開していたため単純に作業負荷も高く、ケアラベル用の画像作成を専属とするアルバイトを探そうとしていた程でした。 そのため、まずはこれらの手間を最短で減らすことを目的として、下図の部分を半自動化することになりました。 自動化を行う上で条件となったのが以下の項目です。 業務で使用しているExcelのテンプレートをインプットとすること 業務の都合上、Windows PC上で動作すること デザイナー指定のフォントで描画できること ケアラベルのデザイン自体がデザイナーの求める要件(見た目の良し悪しなど)を満たすこと これらの条件を満たすために作成したのが下図の構成のシステムです。 デスクトップアプリとしてGUIを操作しながら使用し、入力したExcelファイルを元にデザインされたPNGが出力されるという仕様です。入力されたExcel内の簡単な書き間違え等も検出できる様になっています。 アプリ自体はC#(WPF)で作成しており、フロントエンドからバックエンドまで同じ技術要素で作ることができています。技術の選定理由としては、システムを使用する現場の動作環境に合っていたことと、フォントの細かい設定を行うことのできるライブラリを含んでいたことが挙げらます。 このシステムを作成したことで、デザイナー業務とそれに関連して発生していた業務を自動化することに成功しました。 印刷用のレイアウトファイル作成業務の自動化 次に自動化の対象としたのが、印刷用のレイアウトファイル作成業務です。 半自動化によって画像は自動化できたものの、入力される画像の高さに合わせてレイアウトファイルを作る作業はエンジニアが行っていました。 ケアラベルを印刷する際に使用するプリンターは専用のものを使っており、専用のプリンターで印刷する場合には、下画像の様にPNGファイルからレイアウトファイルを作る必要がありました。 CSVで画像や指定するテキストを挿入できる プリンター専用のデザインアプリでレイアウトファイル、レイアウトにCSVをマッピングさせる設定ファイルを作る必要がある この頃は1シーズンで数百品番を展開していたため、上記で述べた作業を行うには作業負荷が高く、スケールアップしない作業になっていました。 また、生産管理の業務全体でkintoneを利用し始めたため、業務フローに変更がありました。この影響で、kintoneでケアラベルデザイン作成用のExcelファイルを管理しながらPC上でケアラベルデザインを作成、作成したデザインを更にkintone上で管理するといった二度手間が発生していました。 これらの手間を無くし、ケアラベル作成業務を完全に自動化することにしました。 完全自動化を行う上で条件となったのが以下の項目です。 データのやりとりはkintoneを介して行うこと ケアラベルの生成を自動で定期的に実行すること デザイナー指定のフォントで描画できること ケアラベルのデザイン自体がデザイナーの求める要件(見た目の良し悪しなど)を満たすこと ケアラベルの印刷ファイルが印刷する上での要件(文字が潰れないなど)を満たすこと これらの条件を満たすために作成したのが下図の構成のシステムです。 kintone上で入力された条件を読み取り、当てはまるレコードのケアラベルのデザイン、印刷用ファイルを定期実行で作成します。Excelの内容やkintoneの項目に不備があった場合は、Slackで担当者に修正依頼を行います。 レイアウトファイルの作成については、レイアウトファイル自体を解析し、エンジニアが手作業で行っていた手順をプログラムで再現しました。 アプリ自体はPythonで作成しています。技術の選定理由としては、ケアラベルのレイアウトファイルを作成できたことと、フォントの細かい設定を行うことのできるライブラリを含んでいたことが挙げらます。 このシステムを作成したことで、生産管理の業務とそれに関連して発生していた業務を自動化することに成功しました。 自動化での課題点 「デザイナー業務の自動化」と「印刷用ファイル作成業務の自動化」を行う上でいくつかの課題点がありました。 まず、デザイン上の課題です。 上記画像で確認できる通り、デザインする上で次の様な問題が発生し、それらに対しプログラムで調整を行いました。 文字のかすれ、つぶれ 英字と和字を並べて描画した際上下にずれて見える 左右の余白が対象に見えない 混率表記のパーセンテージが数字によって左右にずれて見える 次にシステム要件をまとめる上での課題です。 ケアラベル作成業務には様々な部署、担当者が関わっており、要件や要望も様々でした。自動化をする上では各担当者の作業を把握し、整理する必要がありました。 勿論全てが要望通りにシステム化できるわけではないので、オペレーションを変えなければいけない部分等はコミュニケーションを重ね、要件をすり合わせていきました。 その結果、各担当者の協力もあり、別々で進んでいた業務フローを1つにまとめることができました。 自動化の効果について まず「デザイナー業務の自動化」では、1品番あたり大体1時間程の作業時間がかかっていました。デザインの差し戻し等があった場合は、更に時間がかかります。これを自動化しようとしていた当時は、数十品番行っていました。 自動化後は上記のデザイナー業務が代替され、1品番あたり1時間+αかかっていた作業時間が削減できました。 次に「印刷用のレイアウトファイル作成業務の自動化」では、品質管理の担当者とエンジニアの作業で、1品番大体30分程の作業時間がかかっていました。作業ミス等で差し戻し等があった場合は、更に時間がかかります。これを自動化しようとしていた当時は、数百品番行っていました。 自動化後は上記の品質管理の担当者の作業削減、エンジニアの業務が代替され、1品番あたり30分+αかかっていた作業時間が削減できました。 おわりに 本記事ではMSP技術推進部の取り組みの1つのケアラベル完全自動化のアプローチと効果について紹介しました。 ZOZOテクノロジーズでは、ZOZOTOWNやWEARのサービスをはじめ、事業を支えるさまざまな職種を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
こんにちは、ジャポニカ学習帳の表紙に昆虫が戻って来た 1 ことに喜んでいる、SRE部エンジニアの塩崎です。 先日、有名な投稿型メディアプラットフォームで投稿者のIPアドレスが漏洩するという事象が発生しました。我々ZOZOテクノロジーズが開発・運用しているWEARも、ユーザー投稿型のサービスであるという意味では同様であり、もしかしたら投稿者のIPアドレスを漏洩しているかもしれません。 本記事ではWEARがIPアドレス漏洩をしていないかどうかをクローリングで調査する手法、及びその結果問題がなかったということをお知らせします。 WEARで行われているセキュリティ対策 WEARで行われているセキュリティ対策の一部についても簡単に説明します。WEARでは専門家による定期的なセキュリティ診断を行い、そのレポートに基づいたよりセキュアになるための修正を継続的に行っております。 また、リリースされるコードはチーム内でコードレビューを行い、機能要件のみならずセキュリティの担保やコードの可読性などの非機能要件に関する議論も活発です。 さらに、他社と合同で行っているセキュリティ演習に積極的に参加している開発メンバーもおります。 このようにユーザーさんに安心してWEARを使って頂くために様々な対策を行っております。 とはいえ、厳重な対策を行っていてもミスを完全にゼロにすることは難しいです。今回は既に行っている対策とは少し毛色の異なる、クローリングという、より直接的な方法でIPアドレス漏洩の有無を確認してみました。 Scrapy まず、今回の調査で使用したクローリング用のフレームワークであるScrapyを紹介します。ScrapyはScrapinghub社によって開発されているOSSです。クローリングの処理をPythonで書くため自由度が高く、また同社によって開発されているヘッドレスブラウザのSplashとの統合も容易などの特徴を持っています。作成したクローラーは自前のインフラで運用することも、同社の運用しているPaaSであるScrapy Cloud上で動かすこともできます。 scrapy.org 特徴 続いてScrapyの特徴を紹介します。 CSSセレクターやXPathで要素を抽出 HTMLから要素を抽出するための機能であるCSSセレクターやXPathが搭載されています。また、抽出された要素に対して正規表現でテキスト処理を行うことも容易です。 docs.scrapy.org < html > < head > < base href = 'http://example.com/' /> < title > Example website </ title > </ head > < body > < div id = 'images' > < a href = 'image1.html' > Name: My image 1 < br />< img src = 'image1_thumb.jpg' /></ a > < a href = 'image2.html' > Name: My image 2 < br />< img src = 'image2_thumb.jpg' /></ a > < a href = 'image3.html' > Name: My image 3 < br />< img src = 'image3_thumb.jpg' /></ a > < a href = 'image4.html' > Name: My image 4 < br />< img src = 'image4_thumb.jpg' /></ a > < a href = 'image5.html' > Name: My image 5 < br />< img src = 'image5_thumb.jpg' /></ a > </ div > </ body > </ html > 上記のHTMLソースに対して、XPathと正規表現を用いてパースを行った例を以下に示します。 >>> response.xpath( '//a[contains(@href, "image")]/text()' ).re( r'Name:\s*(.*)' ) [ 'My image 1' , 'My image 2' , 'My image 3' , 'My image 4' , 'My image 5' ] 対話的なインタフェース IPythonのようなREPLが標準で搭載されており、CSSやXPathの検証を対話的に行えます。クローラーの実行中に scrapy.shell.inspect_response 関数を呼び出すことで、そこにブレークポイントを仕込みREPLを起動することもできます。Rubyでの開発中に binding.pry でREPLを起動できることに似ています。 docs.scrapy.org 数多くのデータフォーマット、数多くのストレージに対応 JSON・CSV・XMLなどの数多くのデータフォーマットに対応し、またローカルファイル・FTP・Amazon S3などのストレージに対応しています。 docs.scrapy.org 文字コード自動判定 UTF-8以外の文字コードにも対応できます。日本で使われているEUC-JPやShift JISにも対応できます。 Middlewareなどを使ってプラグイン的に処理を拡張可能 以下のページにScrapyのアーキテクチャ、及び処理の流れが書かれています。ENGINE SPIDER間、ENGINE DOWNLOADER間にMIDDLEWAREという紺色のコンポーネントがあります。これはスクレイピングやダウンロードの処理の前後に特定の処理を挟むことができる機能です。RubyのRack Middlewareを使ったことがある人ならばそのイメージが湧きやすいと思います。 docs.scrapy.org 例えば以下のような機能を提供するMiddlewareが標準で搭載されており、必要に応じで組み込むことができます。 リダイレクト時にCookieを保持する Basic認証を行う 同一URLに対するレスポンスをキャッシュする WEARをクローリングしてみた ここからは、実際にScrapyを使ってWEARをクローリングしてみます。皆さんがクローリングする際には、自分自身で運営しているサイトか許可を得たサイトでのみ行ってください。 Scrapyのインストール まずはScrapyのインストールを行います。Scrapyは多くのPythonのライブラリと同様にpipでインストールできます。 pip install scrapy 人によっては環境分離ツールとしてPipenvやpoetryを使いたい方もいるかと思いますので、そこはご自由にどうぞ。 プロジェクト作成 インストール後、最初に行うことはプロジェクトの作成です。これによってクローリングに必要な多くのファイルが自動生成されます。Ruby on Railsにおける rails new に相当するものです。 scrapy startproject wear_crawler スパイダー作成 次にスパイダーの作成を行います。Scrapyでは特定のサイトをクローリングするための方法を定義するためのクラスをスパイダーと呼んでいます。スパイダーの中にページのパース処理や、次のページを辿る処理などを記述します。以下のコマンドを実行することでひな形が生成されるので、それを元に処理を記述していきます。 scrapy genspider wear wear.jp いきなりですが、完成したものがこちらになりますので、これを使って説明していきます。 import scrapy import re class WearSpider (scrapy.Spider): name = 'wear' start_urls = [ 'https://wear.jp/' ] allowed_domains = [ 'wear.jp’' ] def parse (self, response): ip_addresses = re.findall( r'[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' , response.text) yield { 'url' : response.url, 'ip_addresses' : ip_addresses, } urls = response.xpath( '//a/@href' ) yield from response.follow_all(urls, callback=self.parse) WearSpiderの先頭部分でいくつかのクラス変数の初期化を行っています。 nameはこのスパイダーの名前を表しており、コマンドラインからクローリング処理を行う時にスパイダーを指定するために使用します。 start_urlsはクローリング処理の起点となるURLのリストです。起点となるURLがDBに入っている場合などの動的な処理をしたい場合は start_requests メソッドを代わりに実装し、その中で複雑な処理を記述することもできます。 allowed_domainsはクローリング対象のドメインを表し、ここで指定したドメイン以外へのリクエストは自動的にスキップされます。リンクを辿る時に毎回チェックしても良いのですが、それは煩雑なためここで指定しています。 parseメソッドはクローリング処理を書く場所です。最初に response.text で取得できるページのHTMLソースから正規表現でIPアドレスと疑わしき文字列を取得しています。厳密にはこの正規表現ではIPアドレス以外の文字列も抽出してしまいます。ですが、False Negativeが増えることはないので、ここでは厳密性よりも分かりやすさを重視しています。そして、その結果をyieldを使いScrapy側に返しています。yieldに辞書型のオブジェクトを渡すことで、このオブジェクトがScrapyのデータ保存コンポーネントに渡されます。そこでシリアライズが行われ、CSV・JSONLなどのフォーマットに変換後、ファイルとして保存されます。 その後、XPathを使ってHTMLソースコード中の全てのaタグのhref属性を取得しています。その結果に対してresponse.follow_allを呼び出すことで、これらのリンクの全てを辿るジェネレーターを生成し、yield fromでそれの委譲を行っています。 HTTPリクエストが完了するとcallbackで指定したメソッドが呼ばれます。ここで自分自身であるself.parseメソッドを指定しているため、再帰的に処理が行われます。この時urlsにはwear.jp以外のドメインへのリンクも含まれますが、allowed_domainsにwear.jpのみを指定しているため、それらへのリクエストは自動的に排除されます。 yield fromに親しみのない方のために、動作がイメージしやすい同等の処理をするコードも以下に示します。 for url in urls: yield response.follow(url, callback=self.parse) yieldを使っていたりcallbackを指定したりしているため、カンの良い方はお気づきかと思いますが、HTTPリクエストは非同期的に行われています。内部的にはTwistedを使ったI/O多重化を行っています。そのため、スパイダーの内部でブロッキングI/Oを呼び出すと、パフォーマンスが低下するので注意が必要です。 twistedmatrix.com クローリングの実行 では、クローリングを実行してみます。以下のコマンドを実行するとクローリングを行い、その結果をresult.jlにJSONL形式で出力します。 scrapy crawl wear --output=result.jl ログを確認すると、WEARのトップページからリンクを辿り、それらの中にIPアドレスらしき文字列が含まれているかどうかをチェックしていることが分かります。今回のクローラーはWEARの全ページを辿るため、処理が完了するまで非常に時間がかかります。そのため、適当なタイミングで Ctrl-C を押して処理を止めましょう。 クローラーの改善 とりあえず動くものができましたが、いくつか改良してみようと思います。 幅優先探索 Scrapyのデフォルト設定ではクロール予定のURLをLIFOのスタックに積み、深さ優先で探索を行います。より色々な種類のページをクロールするために、これを幅優先探索に切り替えます。DEPTH_PRIORITYで階層の浅いページのクローリングを優先するとともに、クロール予定のURLを格納するデータ構造をLIFOからFIFOに切り替えています。 DEPTH_PRIORITY = 1 SCHEDULER_DISK_QUEUE = 'scrapy.squeues.PickleFifoDiskQueue' SCHEDULER_MEMORY_QUEUE = 'scrapy.squeues.FifoMemoryQueue' docs.scrapy.org リクエスト頻度の自動調整 次にリクエスト頻度の自動調整を行います。Scrapyにデフォルトで搭載されているAutoThrottle Extensionを使うことで、リクエスト頻度を動的に変えることができます。このExtensionは目標の並列度とダウンロードにかかった時間から最適なリクエスト頻度を計算して、クローリング中に動的にリクエスト頻度を変更します。 AUTOTHROTTLE_ENABLED = True AUTOTHROTTLE_START_DELAY = 5 AUTOTHROTTLE_MAX_DELAY = 60 AUTOTHROTTLE_TARGET_CONCURRENCY = 1 AUTOTHROTTLE_DEBUG = True docs.scrapy.org 結果 改良後のクローラーを起動し数時間放置すると、チェックしたURLとそこに含まれるIPアドレスと思われる文字列のリストがJSONL形式で溜まっていきます。数万URLのスキャンが完了したタイミングで一旦クローラーをストップさせて結果を確認します。 その結果、いくつかのページで正規表現にマッチする文字列が見つかりましたが、どれも投稿者のIPアドレスではありませんでした。 cat result.jl | grep -v ' \[\] ' 例えばとあるコーデ一覧のページから ***.***.1.2 という文字列(一部伏せ字)が発見されました。念のためにHTMLソースを確認したところ、画像のalt属性に ***.****.1.2 という文字列が見つかりました。このalt属性はユーザー名やアイテム名などから自動生成されるものであり、更に調査したところユーザーが自分自身のニックネームとして ***.***.1.2 を設定していることが分かりました。 他にもいくつかのページでIPアドレスらしき文字列が見つかりましたが、いずれも上記の例と同様なケースでした。 まとめ WEARのHTMLソースコード中にIPアドレスが含まれていないことをクローリングで確認しました。日頃から行っている、脆弱性診断やコードレビューとは少し違った方法でIPアドレスが漏洩していないことを直接的に確認できました。 ZOZOテクノロジーズでは、他社でセキュリティインシデントが発生した時に、それを対岸の火事と捉えずに自分たちのシステムを内省できる人材を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com ジャポニカ学習帳 50周年記念 昆虫シリーズ ↩
はじめに こんにちは、SRE部MLOpsチームの児玉( @dama_yu )です。この記事では、ZOZOTOWNのおすすめ順を支える検索パーソナライズ基盤について紹介します。 ZOZOTOWNのおすすめ順について ZOZOTOWNにおいて検索機能は非常に重要な機能の1つで、売上のうち多くの割合が検索経由です。ZOZOTOWNでは、検索結果の並び順として、おすすめ順、人気順、新着順など複数あり、現在おすすめ順がデフォルトになっています。 元々は人気順がデフォルトだったのですが、ユーザの嗜好に合わない商品まで検索結果に並んでしまうという課題がありました。そこで、この課題へのアプローチとしてユーザの行動履歴や属性を元にパーソナライズされた順番で検索結果を並べた、おすすめ順を新規追加することになりました。 この施策の結果、検索結果経由の商品CTRが向上しました。ユーザが求めている商品が並ぶようになったのではないかと考えています。 この記事では、この施策で構築されたおすすめ順のための検索パーソナライズ基盤のアーキテクチャや設計上のポイントについて、説明していきます。 検索パーソナライズ基盤のアーキテクチャ この章では、検索パーソナライズ基盤の全体感を見た後に、API・インデクシング基盤についてそれぞれアーキテクチャの詳細、設計段階での検証ポイントを説明します。 アーキテクチャ概観 検索パーソナライズAPIはマイクロサービスとして構築されており、ZOZOTOWNのバックエンドAPIから参照される構成になっています。 パーソナライズAPIはユーザ情報を元に、Elasticsearchのクエリ構築に必要なパラメータを生成します。そのパラメータを使用して構築したクエリを元に、ZOZOTOWNのバックエンドAPIがElasticsearchにアクセスします。 Elasticsearchにはインデクシングバッチで商品情報を登録しています。 アーキテクチャ詳細(API) ここでは、APIのアーキテクチャについて説明します。 先ほどの説明では省略していましたが、ZOZOTOWNのバックエンドAPIはAWS上に、検索パーソナライズAPIはGCP上にそれぞれ構築されています。このAWS-GCP間のレイテンシを軽減するためAWSは Direct Connect 、GCPは Dedicated Interconnect という専用線サービスを利用してオンプレ経由でアクセスするようにしています。パーソナライズロジック内で参照する必要のあるユーザ情報(年齢や性別、お気に入りに追加したショップやブランドなど)については、1日1回の頻度でDataflowを用いてBigQueryからCloud Bigtableに書き込んでいます。BigQueryにデータを連携している基盤についての詳細は、以前 ZOZOTOWNを支えるリアルタイムデータ連携基盤 というタイトルで紹介しているので参照してみてください。 techblog.zozo.com パーソナライズAPIはユーザIDをキーにして、パーソナライズロジックに必要な変数の値と係数の組み合わせのリストをJSONのレスポンスとして返します。そのパラメータと検索ワードを使用して構築したクエリを元に、ZOZOTOWNのバックエンドAPIがElasticsearchにアクセスします。パーソナライズAPIはリクエストが来たタイミングでキャッシュされていない場合、Cloud Bigtableに保存されているユーザ情報はMemorystoreにキャッシュされることで、APIのレスポンスを高速化しています。 アーキテクチャ詳細(商品情報インデクシング) 次に、商品情報のインデクシングについて説明します。 ZOZOTOWNの商品情報を保存しているSQL Serverのレプリケーションから、 Qlik Replicate を用いて、Kafka(マネージドサービスとしてConfluentを使用)-> Dataflow経由で、BigQueryに商品情報の更新差分を保存しています。その更新差分とBigQueryの商品情報のマスタデータをJOINし、App Engine上に構築したバッチで1日1回、Elasticsearchに商品情報インデックスを作成しています。 アーキテクチャ設計の検証 検索パーソナライズ基盤のアーキテクチャ検討段階において、最初の設計からいくつか変更した点がありました。ここでは、API基盤、ユーザ情報更新バッチ、商品情報インデックス作成バッチ、それぞれについて設計の変更点を説明します。 API基盤 今回、AWSから専用線経由でアクセスするため、内部IPでサービスを提供する必要性がありました。 そのため、外部IPしか利用できないGAE(Google App Engine)はNGとなり、GKE(Google Kubernetes Engine)またはCloud Runが候補として上がりました。GCPのフルマネージドなサーバレスコンテナプラットフォーム、Cloud Runは弊チームでまだ採用事例がなく、実際に構築して検証を行いました。 検証の結果、Cloud Runは以下のような挙動をすることがわかりました。 リクエストが来た時に始めてコンテナが起動し、何リクエストか捌くと停止する Javaコンテナ(主にJVM)の起動が遅く、我々の場合20秒以上かかる 結果として、初回もしくは複数リクエストのうち1回の、コンテナが起動するタイミングでユーザにレスポンスを返すまで20秒以上かかってしまう これにより、今回はCloud Runの採用は止め、GKEを使用することにしました。 なお、現在ではこの「Cold Start問題」は 解消しているようです 。 ユーザ情報更新バッチの定期実行 当初Dataflowで構築したユーザ情報更新バッチはCloud Scheduler + Cloud Functionsで定期実行をする予定でしたが、プロジェクトのCI/CDで用いていた、GitHub Actionsでの定期実行に切り替えました。 GitHub Actionsのon schedule機能を用いることで、シンプルなyamlファイル定義のみで定期実行の設定が可能になりました。 Elasticsearchインデックス作成バッチ 元々、Elasticsaerchのインデックス作成バッチは、以下の理由でDataflowを採用する予定でした。 GCPのサービスでBigQuery -> ElasticsearchのETLを楽に開発したい 運用の手軽なサーバレスが良い しかしDataflowのApache Beamが設計当時Elasticsearch 7系のVersionに対応していなかったので、代わりに、GAE上で、インデックス作成のためのバッチを動かしています。2020年10月現在では、Apache BeamのElasticsearch IOは Elasticsearch 7系をサポート していますが、Elasticsearch 8系が使えるようになった場合でも再度同じ状況になることが考えられます。クライアントライブラリの制約で使えなくなる基盤よりは、自由にライブラリをインストールできるGAEのほうが、今後も柔軟性が高く優位性があると判断しました。 なお、現在ではβではありますが、Dataflowでも Dockerfileを指定できるようになった とのことなので、この制約は弱まった可能性があります。次の案件では、そちらも使えないか検討してみたいと考えています。 GAEについては、メモリ制限の都合でStandard Environmentではなく、Flexible Environmentを採用しました。 Elastic Cloudについて 弊社では、Elasticsearchのマネージドサービスとして、Elastic社が提供するElastic Cloudを用いています。マネージドサービスなので運用の負担が小さい、かつElastic社の公式サポートが利用できるという理由でElastic Cloudを選定しました。ここでは、Elastic Cloudの監視・運用方法について紹介します。 Elastic Cloudの監視 Elastic Cloudの監視には、Datadog(dashboard, montior)を使用しています。Datadogの設定は、Terraformを使ってIaC化しています。メトリクスは、Datadog Agentから送ることができないものについては、GKE上に構築したCronJobバッチで定期的にメトリクス送信を行っています。 Datadog Dashboardは各メトリクスの状況を一覧で見るのに使用しています。主に、query/secやCPU使用率を見ることが多いです。 Datadog monitorは、しきい値アラートの設定に使用します。 Elsatic Cloudの運用について 運用上の作業で最も多いのは、ElasticsearchのNode数変更です。セールなどのキャンペーンごとに、過去の負荷傾向からNode数を見積もっています。基本的にはキャンペーン前日までに運用担当がNode数変更を行います。 当初は手動で、Elastic Cloudコンソールからノード数を変更していたのですが、現在は ecctl というCLIツールがElastic社から公開されており、GitHub Actionsを用いたCI/CDでノード数を変更できる仕組みを構築しています。 つい先日Elastic Cloudの terraform-provider が公開されたので、そちらも使えないかまた検討してみたいと考えています。 さいごに 今回は、ZOZOTOWNおすすめ順のための検索パーソナライズ基盤について紹介しました。おすすめ順はロジック面・システム面どちらもまだまだ改善の余地が残されていて事業インパクトも大きいため、重要度の高いプロジェクトの1つとして、引き続き絶賛改善中です。 SRE部MLOpsチームでは、データや機械学習を用いてサービスを成長させたいエンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com
はじめに こんにちは。SRE部USED基幹インフラの丸山です。 ZOZOUSED では2018年、当時社名がクラウンジュエルからZOZOUSEDに変更になるころからAWSの利用を開始致しました。当時はオンプレミス環境が多く、AWSの導入については画期的ではありましたが苦労も多かったとも聞いています。上記AWSで構築した環境について、前任者の異動に伴い私をはじめ他のメンバーで引き継ぐ事になりました。 当時私共はいわゆる情シスとして社内及び倉庫内のオンプレミス環境のインフラを管理する業務が多く、AWSについてはほぼ初心者の状態でした。そのため書籍をあさったり、Webの記事を検索したりと手探りで運用や改善を行っておりました。今回はAWS初心者だった私がタイトルの案件を通じてAWSのいろいろな機能に触れる経験ができたお話をしたいと思います。主にAWS初心者の方などにお役にたてばと考えております。 実装の背景 前任者から引き継いだAWS環境は大きく2つあり、そのうち1つの環境についてはまた別の部署で管理をしておりました。そちらに関してはほぼインフラ担当者が不在の状況で、私共から見てもブラックボックスの状況に近いものがありました。あらかじめ分かっている状況は以下の通りでした。 毎週必ずEC2、RDSなどインフラに負荷が急激にかかる時間帯があるが、サービスの提供においては問題が発生したことはない 上記の時間帯においてもEC2及びRDSのスケールアウトが発生するほど負荷が上昇したことは過去に一度も発生したことがない ある日、このAWS環境で「毎週必ず負荷が急激にあがる時間帯」において急激にアプリケーションが大量のエラーを出力し、 サービスを提供できないような状態になってしまった との連絡を担当の開発者から受けました。そのトラブルは、応急処置にて一旦は解消したもののその時点では根本的な原因究明には至りませんでした。 当時全く原因が分からない状況で、あらゆる方向から調査を行いたいとの強い要望がありました。開発担当者からも 「原因の切り分けのため一時的にEC2の台数を拡張してテストをしたい」 という要望を受け、今回の案件の環境を構築することとなりました。 構成で特徴的だったのはサービスの仕様上 「全てのEC2がElastic IP(以降、EIPと呼びます)をアタッチされている必要がある」 という点でした。 「NATゲートウェイを使えばいいのでは?」と考える方も多いと思いますが、その点に関しては後述させて頂きます。 元々の構成 依頼の構成 要件定義 開発担当と相談して、より詳細な要件を詰めました。 必須項目 10台のEC2は全てEIPを持つ必要がある 業務提携先のオンプレミスサーバと連携しており相手先FWに穴をあける必要がある 現状のEC2はパブリックサブネットに配置してありこの構成を変えたくない 当時の構成であり現在は異なる 開発側の任意のタイミングでEC2を増やしたり減らしたりしたい 問題・課題点 このようなケースでは本来NATゲートウェイがベストプラクティスです。EC2にEIPを一つずつ持たせるのは費用の面から見ても無駄遣いのようにみえました。 また、そもそもEC2がパブリックサブネット配置という要件がセキュアでないように感じました。なお、現在は改善しています。 そして、EC2 Auto ScalingのもととなるAMIがかなり古いことが分かりました。またスケールアウト時にデプロイするようなこともしていないため、このままでは元々存在するEC2とスケールアウトしたEC2で機能差が発生することが判明しました。 検討結果 まずNATゲートウェイを試してみるために、EC2もプライベートサブネットに設置してみます。 本件とは直接関係ありませんがリリースの都度、Auto Scalingで使用しているAMIと差分が発生してしまう問題に関してはリリースのタイミングを把握し、リリースの都度AMIを作成します。あくまで暫定対応です。 以下のような構成にできればEIPが一つで済みます。 NATゲートウェイを試してNGとなった理由 早速NATゲートウェイの構築に着手しました。構築自体はそれほど難しくなく以下のサイトなどを参考にすぐに完了しました。 docs.aws.amazon.com NATゲートウェイを利用すればEIPはそもそも1個で済みますし、料金、業務提携先のNWの設定の手間暇などの面からもいい事ばかりと考えていました。またEC2もプライベートサブネットに置き換えセキュアにする事ができ、色々な問題を解決したつもりでした。しかし担当者と相談していく中でいくつか問題があることが分かりました。 まず、NATゲートウェイを利用したアプリケーションの動作確認がそもそも何故か失敗しました。またこの問題や理由についてアプローチしている時間が開発側に当時ありませんでした。単純にトラブルシュートで使いたいだけなので大きなインフラ構成の改修はまた別のタイミングで行えば良く、 同様の構成を早く用意することが優先されました。 また、NATゲートウェイを利用するとEC2のサブネットが必然的にプライベートになってしまう点も問題として出てきました。 そのため直接SSHでログインして作業が必要となる際に、運用上困る場合があるとの意見を頂きました。 当時はこれが問題となっていましたが、後述する通り、この問題は類似したことがマネジメントコンソールよりできることが分かりました。 再検討 上記の状況を踏まえて、以下のような方法で進めようと再考しました。 EC2 Auto Scaling時にEIPを割り当てる手法が無いかの確認をする サブネットは一旦パブリックのままにし、インフラ構成の改修はまた別の機会に行うこととする 開発担当者が好きなタイミングでEC2増やしたり減らしたりしたいというリクエストについてはマネジメントコンソールから手動Auto Scalingしてもらう AMIについては引き続きリリースの都度作成する NATゲートウェイ環境(プライベートサブネット)でも直接EC2にアクセスすることは可能 少し余談となりますがプライベートサブネットのEC2についても踏み台サーバなどを利用せず、直接アクセスする方法もあるので紹介したいと思います。 System Manager の Session Manager という機能です。 詳しくは以下URLなど参考にして頂ければと思います。 docs.aws.amazon.com 該当のインスタンスにマネジメントコンソールから目的のインスタンスに接続できます。便利ですね。ちなみに接続時には ssm-user という独自のユーザーが使用されるようです。 以下が実際に対象のLinuxのインスタンスにSession Managerを使用して接続してみた画面です。 EC2 Auto Scaling時にEIPを割り当てる方法の調査開始 インターネット上の情報を探してみたところ、EIPをEC2に割り当てるシェルスクリプトのサンプルを紹介しているサイトは幾つかありました。そのシェルスクリプトをAWSのどの機能を利用してEC2 Auto Scaling時に実行しているかを紹介しているサイトはこの記事を執筆している時点では少ないように感じました。 実現に向けて AWSの機能としては提供されていないので自前で作り込む必要があることが分かりました。AWS CLIを利用してシェルスクリプトを頑張って作成すればEIPをEC2に割り当てる事はできそうなので、「Auto Scaling時にどのようにしてそのシェルスクリプトを実行するか」という方法を検討していきます。 AMIの作成 元となるAMIは、前回作成時からどのような変更があったか分からないため新しく作成することにします。リリースが発生する度にAMIを作成するような運用は効率が悪いのですが、こちらに関しては後述させて頂きます。 さて、AMIの作成に関しては「再起動」が前提となっています。前任者から引き継いだ手順書には「再起動は絶対にするな」とありましたのでこれは実は意外でした。今回は再起動が可能な環境だったので問題ありませんでしたが、どうしても再起動できない場合には「再起動しない」というオプションもあるので、そちらを利用する方法もあります。しかしこの方法で作成したイメージのファイルシステムの完全性は保証できないということなので、できるだけ再起動を伴ったほうがいいでしょう。AWSでもこの方法は推奨していないようです。 つまずきポイント1:CloudTrailによるAMI作成元EC2の追跡 AMIの作成時に困ったことが起きました。普段EC2は2台で運用しているのですが、AMIの取得元がその2台のどちらかなのか、もしくは全く違うEC2から作成したものかが分かりません。マネジメントコンソールから必死に探しますが証跡を見つけることができません。ここで今回役にたったのが CloudTrail です。 docs.aws.amazon.com CloudTrailの記録から、どのインスタンスからAMIを作成したか突き止めることができました。過去90日分のイベント履歴であれば画面からも追跡できます。AMIのIDから検索する場合は「リソース名」から検索をかけて下さい。 今回はAMI IDからリソース名で追跡し、無事にCreateImageのイベントを特定できました。 CreateImageをクリックすると作成元のEC2のインスタンスIDが分かります。 つまずきポイント2:Service Quotas この機能を実行するにはEIPがそもそも足りません。事前にまとまった数のEIPを発行しておく必要があることと、現状何個EIPを保持しているか確認する必要がありました。 しかし、 「今いくつEIPを持っていて、いくつ使っているのだろう?」 という疑問の解決方法が分かりませんでした。 この問題は Service Quotas という便利な機能で解決しました。以下のような機能があります。 EIPの総数の確認 クォーターの設定 EIP以外の値の設定や確認 上限緩和申請 aws.amazon.com 実装 いよいよシェルスクリプトの実装です。しかしインターネット上で提供されているシェルスクリプトのサンプルがそのまま自分たちの環境で動くとは限りません。まず手動でAMIから起動したEIPを割り振っていないEC2にてインターネット上にある割当スクリプトを持ってきて実行してみます。見事に失敗しました。 しかし何度か修正しているうちにEC2上から直接実行する形ではEIPを割り当てることが何とかできるようになりました。これをAuto Scaling時に動かすことができればいいわけです。 つまずきポイント3:ユーザーデータ さてここが肝の部分です。このシェルスクリプトをどこで設定して動かすのか。OSはLinuxなのでAMI作成時に「/etc/rc.local」に上記シェルスクリプトを設定して作成するという方法も考えました。 その方法の調査を続けたところマネジメントコンソールのEC2を起動する高度な設定の中に 「ユーザーデータ」 という項目があり、この中にシェルスクリプトを直接埋め込めることが可能なことが分かりました。高度な設定はそもそも触ったことがなく画面の1番下に隠れるようにあったので全然知りませんでした。また、それがAMIから起動するときも同様に使用できることが分かりました。EC2上ではシェルスクリプトがうまく動いたので次はAMIを起動する際にユーザーデータ内に先程のシェルスクリプトを埋め込んで上手く動くか検証します。 ユーザーデータ内には以下の様に通常のシェルスクリプトを作成するのと同様に記入可能です。なお、実際に使用したシェルスクリプトは実行できない環境もあるようなので今回は割愛させて頂きます。 #!/bin/bash # 変数も使用できます eip_groups = " eipalloc-XXXXXXXXXXXXX eipalloc-XXXXXXXXXXXXX ...etc " # AWS CLI が使用できる環境であればAWSコマンドも実行可能 aws ec2 associate-address ~ .....etc 以下がその画面です。 AMIを起動した際にプールしてあるEIPを見事に関連付けることができました。 AMIからの検証に成功しました。いよいよEC2 Auto Scalingに対して設定します。 つまずきポイント4:起動設定と起動テンプレート EC2の画面から「Auto Scaling グループを作成する」という部分をクリックすると以下のような画面が表示されます。特に赤枠内の「起動テンプレート」という部分が気になりました。前任者は「起動設定」から全て作成していて手順としてもチーム内で確立されていたのですが、「起動テンプレート」がそもそもデフォルトになっており、違いが気になったので少し調べてみました。 AWS公式サイトには以下のように記載がありました。 docs.aws.amazon.com どうやら 起動設定の後継の機能が「起動テンプレート」 に該当するようです。どちらにも「ユーザーデータ」内にシェルスクリプトを設定することができます。AWS推奨ということもあり、今回は起動テンプレートで挑戦してみることにしました。起動テンプレートには以下のような特徴があるようです。 既に存在する起動設定は全て起動テンプレートに置き換える(コピー)事が可能 起動テンプレートはバージョン管理が可能 起動設定と比較すると設定項目が多く、より細かい設定が可能 起動テンプレートの作成 では実際に「EC2」→「起動テンプレート」より起動テンプレートを作ってみます。 画面1番下の「高度な詳細」を展開しないとシェルスクリプトを設定する「ユーザーデータ」が表示されないのでご注意下さい。 沢山の項目がありますが全てを入力する必要はありません。今回私が使ったのは以下の項目だけでした。 AMI インスタンスタイプ VPC セキュリティグループ ストレージ ユーザーデータ また今回検証する中で何度か起動テンプレートを作り直したのですが、以下のようにバージョン管理されていることが分かります。 Auto Scaling グループへの設定 作成した起動テンプレートをAuto Scaling グループへ設定します。まず起動設定から起動テンプレートに変更します。 画面右上の「起動テンプレートに切り替える」をクリックします。 起動テンプレートに切り替わります。ここでは「dev_test」という名称で起動テンプレートを作り、バージョン管理をしています。私の場合は各バージョンでユーザーデータ内に記述するシェルスクリプトの内容などを変更したりしていました。 デフォルト値を変更していなかったので常に「Latest」を選択していました。 Auto Scalingのテスト 実際にAuto Scaleのテストをしてみます。手動Auto Scalingの設定をします。「Auto Scaling グループ」→「予定されたアクション」の画面からです。 10台をEIP付きでAuto Scalingさせる想定です。 10台のEC2が起動して、EIPがアタッチされていることが分かります。 総括 上記手動によるEC2 Auto Scaling方法を開発担当者に簡単に説明し、原因追求に着手します。その後、苦労の甲斐あってトラブルを引き起こすバグの特定・修正に至りました。 しかしそれ以上に今回「EC2 Auto Scaling時にEIPを割り当てる」という作業を行う中でAuto Scalingの設定以外にも今回取り上げたような技術や仕様を学ぶことができ、紆余曲折ありましたが決して費やした時間は無駄ではありませんでした。 NATゲートウェイ Service Quotas ユーザーデータ CloudTrail 起動設定と起動テンプレート Session Manager なお、Session Managerについては今回の作業の中で本格的に使用したわけではないのですが今後使用して行く予定です。 私的には「ユーザーデータ」をその後多用しています。例えばgitでソース管理している場合ですとスケールアウトするときにユーザーデータに 「git pull~」 のように書いておくだけで最新のリソースが反映されるので毎回AMIを作成することもなく、いろんな場面で重宝しています。またyumコマンドなども使えるので簡単なミドルウェアの構成管理としても利用できそうです。当時リリースの度にAMIを毎回作成して、起動テンプレートに設定して、ロードバランサーに反映するような作業を行っていたころが懐かしいです。 最新のリソースを反映するには書籍などから「CodeDeploy」、構成管理するには「OpsWorks」しかないという思い込みがあったので、ユーザーデータだけで運用がこんなに楽になるとは思ってもいませんでした。 さいごに SRE部USED基幹インフラでは、本案件後から今回学習したユーザーデータ、CloudTrail、起動テンプレート etc.を利用してより安定して効率のよいインフラのカスタマイズ、ならびに新しい技術のキャッチアップを心がけています。今後、多くのサーバをクラウドに移行する予定もありチューニングや見直しを引き続き実施し、さらなる効率化・安定化を目指しています ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募下さい! tech.zozo.com
こんにちは、ZOZOテクノロジーズ CTO室の池田( @ikenyal )です。 ZOZOテクノロジーズでは、9/29に After iOSDC Japan 2020 を開催しました。 zozotech-inc.connpass.com 本イベントは、Sansan、note、ZOZOテクノロジーズの3社による合同イベントです。9月19日〜9月21日に開催されたiOSDC Japan 2020について、各社の社員によるLT、パネルディスカッションを行いました。本イベントには、ZOZOテクノロジーズの技術顧問でもある岸川氏も登壇しました。 登壇内容 まとめ Sansan、note、ZOZOテクノロジーズよりそれぞれ1名ずつ、合計3名がLTで登壇し、ZOZOテクノロジーズ 技術顧問の岸川 克己氏が特別講演を、その後パネルディスカッションも実施されました。 LT1:iOSアプリの起動時間短縮にむけて (株式会社ZOZOテクノロジーズ 元 政燮) LT2:Firebase In-App Messaging で過去バージョンのユーザーへ更新を促したい! (Sansan 中川 泰夫 / @ynakagawa33 ) LT3:note社でのMagic Pod活用事例 (note 植岡 和哉 / @fromkk ) 特別講演: SourceKit-LSPを使ってWebブラウザでSwiftの入力補完を実現する (ZOZOテクノロジーズ 技術顧問 岸川 克己 / @k_katsumi ) パネルディスカッション: 『初のオンライン iOSDC、どうでしたか?』 (モデレーター: note / 森口 友也、パネラー: Sansan / 栗山 徹( @kotetu )・note / 植岡 和哉・ZOZOテクノロジーズ / 西山 博貴) 最後に ZOZOテクノロジーズでは、プロダクト開発以外にも、今回のようなイベントの開催など、外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! https://tech.zozo.com/recruit/ tech.zozo.com
ポリゴンメッシュの紹介 皆様、はじめまして! 計測プラットフォーム部バックエンドチームの村木と申します。 本記事では私達のチームが、お客様の足の形状を分析していく中で得た、様々な知見を紹介していきたいと思います。 まず、足の形を3Dで表現するために私達が採用しましたモデリング手法「ポリゴンメッシュ」について紹介します。 「ポリゴンメッシュ」(以降では単に「メッシュ」と表記します)は、3Dのオブジェクトをその表面を覆う多角形、または三角形の集合として表現する手法です。 上の画像は、足の形状をメッシュで表現した例になります。多数の細かい三角形でオブジェクトの表面が覆われているのが見て取れるかと思います。 trimeshの紹介 3Dオブジェクトを多角形の集合で表現するということについて、私達が使用しているPythonライブラリ、 trimesh を使って詳しく見ていきましょう。 trimeshは、三角形メッシュ(triangle mesh)を読み込み、簡単に操作・分析するためのピュアPythonライブラリです。 「三角形メッシュ」とは、その名の通り一般の多角形ではなく三角形のみでオブジェクトの表面を覆うメッシュの一種を指します。 では、trimeshを使って基本的なメッシュデータを定義してみます。 import trimesh mesh = trimesh.Trimesh( vertices=[[ 0 , 0 , 0 ], [ 0 , 0 , 1 ], [ 0 , 1 , 0 ]], faces=[[ 0 , 1 , 2 ]] ) このコードでは、引数 vertices に3つの頂点の情報を与え、また引数 faces に1つの面すなわち三角形の情報を与えています。1つの faces を構成する [0, 1, 2] という数値は、それぞれ0,1,2番目の vertices から構成される三角形であることを示しています。 このように頂点と面の集合をリストとして与えることで、メッシュを定義することが出来ます。 足の形状の特徴データ 次に、私達が考案しました、以下の1から3の処理で求まる足の形状に関するデータを紹介します。 XY平面に、等間隔で配置した格子点を設ける。 それぞれの格子点から、Z軸方向にのばした直線とメッシュデータとの交点を求める。 YZ平面、ZX平面についても同様の処理でメッシュデータとの交点を求める。 こちらが、1つの平面のみを図示したものです。 この処理で得られた交点の座標の集合は、足の形状の特徴を表すデータとして扱えることが、私達の分析の中で分かってきました。 次の節ではこの処理、すなわち「格子状に配置したベクトルとメッシュとの交点を求める処理」を、trimeshを使って実装していきます。 ベクトルとメッシュの交点計算 以下のコードが、格子状に配置したベクトルとメッシュとの交点を求め、ビジュアライズする処理です。 import trimesh import numpy as np def load_and_create_mesh (): npz_kw = np.load( 'np_vertices_faces.npz' ) vertices = npz_kw[ "vertices" ] faces = npz_kw[ "faces" ] return trimesh.Trimesh(vertices=vertices, faces=faces) def create_ray_origins (): x_coord = np.linspace(- 160 , 160 , num= 160 , endpoint= False ) y_coord = np.linspace( 0 , 80 , num= 40 , endpoint= False ) X, Y = np.meshgrid(x_coord, y_coord, sparse= False , indexing= 'xy' ) return np.dstack([np.zeros(X.shape), X, Y]).reshape([- 1 , 3 ]) def create_ray_directions (size): ray_directions = np.array([[ 1 , 0 , 0 ]] * size) return ray_directions def ray_intersects_location (mesh, ray_origins, ray_directions): locations, index_ray, index_tri = mesh.ray.intersects_location( ray_origins=ray_origins, ray_directions=ray_directions) return locations def create_scene (mesh, ray_origins, ray_directions, locations): # stack rays into line segments for visualization as Path3D ray_visualize = trimesh.load_path(np.hstack(( ray_origins, ray_origins + ray_directions)).reshape(- 1 , 2 , 3 )) # make mesh transparent- ish mesh.visual.face_colors = [ 100 , 100 , 100 , 100 ] # create a visualization scene with rays, hits, and mesh scene = trimesh.Scene([ mesh, ray_visualize, trimesh.points.PointCloud(locations)]) return scene if __name__ == '__main__' : mesh = load_and_create_mesh() ray_origins = create_ray_origins() ray_directions = create_ray_directions(ray_origins.shape[ 0 ]) locations = ray_intersects_location(mesh, ray_origins, ray_directions) scene = create_scene(mesh, ray_origins, ray_directions, locations) scene.show() なお、ビジュアライズの処理は、trimeshのリポジトリに含まれているサンプルコードを参考にしています。 github.com 以下、各処理の説明です。 load_and_create_mesh関数 vertices, facesのロードと、trimeshオブジェクトの生成 create_ray_origins関数 intersects_locationに与える引数ray_originsの生成 等間隔でならんだ格子状の座標を生成している create_ray_directions関数 intersects_locationに与える引数ray_directionsの生成 ray_intersects_location関数 格子状に配置した直線とメッシュとの交点を求める create_scene関数 pygletを使ったメッシュと交点の座標をビジュアライズする 最も注目して頂きたいのは、ray_intersects_location関数で行われている処理です。trimeshオブジェクトのフィールドのrayオブジェクトとそのメソッドである intersects_location を使って、複数のベクトルとメッシュ表面との交点を一括して計算しています。 メソッド intersects_location は、その引数ray_originsでは交点を求めたいベクトルの起点となる座標のリストを与え、引数ray_directionsでは各ベクトルの方向を表す座標を与える仕様であることにも注意して下さい。 さて、メソッド intersects_location の呼び出しによって無事に格子状に配置したベクトルとメッシュ表面との交点を求めることが出来ました。しかし、この処理では格子が細かい場合に処理速度が遅く、求める交点が数十万に及ぶ場合は数十秒に渡る時間がかかってしまうという問題がありました。 embreeを用いた高速化 幸いなことにtrimeshでは、rayオブジェクトを使った処理は、embreeというライブラリを使用することで高速化することが可能でした。 embreeは、Intelによって開発されたCPUベースの高性能レイトレーシングライブラリです。Intelの最新のプロセッサ向けにパフォーマンスが最適化されたライブラリであり、レンダリング処理のパフォーマンスを向上させることが出来ます。 trimeshはこのembreeの組み込みに対応しており、embreeとそのPythonラッパーであるpyembreeをインストールするだけで、ソースコードを修正することなく処理の高速化を行うことが出来ます。 embreeとpyembreeのインストール方法は、 公式のスクリプト が参考になります。 #!/bin/bash set -xe # Fetch the archive from GitHub releases. wget https://github.com/embree/embree/releases/download/v2. 17 . 7 /embree-2. 17 . 7 .x86_64.linux.tar.gz -O /tmp/embree.tar.gz -nv echo " 2c4bdacd8f3c3480991b99e85b8f584975ac181373a75f3e9675bf7efae501fe /tmp/embree.tar.gz " | sha256sum --check tar -xzf /tmp/embree.tar.gz --strip-components=1 -C /usr/ local # remove archive rm -rf /tmp/embree.tar.gz # Install python bindings for embree (and upstream requirements). pip install --no-cache-dir numpy cython pip install --no-cache-dir https://github.com/scopatz/pyembree/releases/download/ 0 . 1 . 6 /pyembree-0. 1 . 6 .tar.gz 上記のスクリプトの手順でDockerコンテナにembreeとpyembreeを組み込み、手元の環境(MacBook Pro / Intel Core i7 3.5 GHz)でembree組み込み前後の速度を、計算対象の交点数を変更して比較しました。 交点の数 embree組み込み前 embree組み込み後 8,000 0.57秒 0.01秒 64,000 4.70秒 0.06秒 168,000 12.41秒 0.18秒 320,000 25.66秒 0.33秒 embreeの組み込み後は、処理速度が数十倍高速になることが確かめられました。 まとめ 本記事では、ライブラリtrimeshでメッシュを扱う基本的な方法と複数のベクトルとメッシュ表面との交点を一括して計算する方法、さらにembreeを使用した高速化について説明しました。 なお、私達計測プラットフォーム部バックエンドチームでは、ZOZOMATでより精度の高いサイズを推奨するバックエンドエンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募下さい! www.wantedly.com
はじめに MSP技術推進部の基幹化推進チームの中嶋です。 私達のチームでは、 マルチサイズプラットフォーム事業(MSP) におけるデジタルトランスフォーメーション(DX)の取り組みを行っています。その取り組みの1つにAndroidを使って、検品結果を記録するアプリの開発・導入があります。 実はこの施策は約2週間で開発されたものです。今回のブログではどうやって短期間でリリースできたのかを紹介します。 開発の背景 検品検寸アプリ誕生のきっかけは、製品の販売前の検品にかかる時間を効率化したいという声からでした。 MSPの全ての製品は、工場が行う検品とは別に弊社が検品会社と協力して検品を行っています。 検品後速やかに弊社側で検品結果をまとめ、製造を担当している商社・工場へレポートする必要がありました。しかし、この工程では作業時間の多くをレポートを作成する業務が占めており、作業効率が大変悪い状態でした。 MSP事業の生産管理・品質管理の担当と話して、この問題はAndroidアプリを使って解決できると分かり、開発がスタートしました。 アプリ開発のアプローチ方法 どのようなアプローチで短納期の開発が実現できたのかを説明します。 システム このプロジェクトは開発者の人数が限られており、開発に使える時間が短いものでした。それに加えAndroidアプリ開発の比重が大きく、サーバーサイドの開発に工数を掛けられないため開発ボリュームを抑えたいと考えていました。そこでアプリのバックエンドとしてFirebaseを採用することにしました。 下図は開発時に目指した、Androidアプリとシステムが連携した構成です。 Firebaseを選んだ理由 以下の特徴があるため、Firebaseを選択しました。 導入のしやすさ モバイルデバイス(Android・iPhone)と連携することを前提としたサービスプラットフォームである点 ドキュメントが充実している点 会社がG Suiteを導入していることにより、社員アカウントで開発を始めらる点 設計・実装時間の圧縮 データベースとストレージサービスの両方が利用可能である点 Cloud Firestore:NoSQLデータベースで検品・検寸データを保存 Cloud Storage:不良画像の保存とアプリ更新配信のために最新APKを格納 スキーマレスDBでデータ保存できるという点 サービス立ち上げ時に好都合だった SDKが充実しており実装コストが少ない点 社内の導入実績 ZOZOTOWN Androidチームによる導入のサポートが得られる点 ZOZOTOWN Androidチームの利用実績がある点 参考:Firebase プロダクト Cloud Firestoreについて データ作成 Firebaseで新規プロジェクトを作成すると、利用したいプロダクトが表示されます。 そこで、データベースはCloud Firestoreを選択します。Cloud Firestoreのコレクションは、ボタンを数回押せばデータベースができあがります。 参考:Cloud Firestore スキーマレスなデータベースのためデータ構成を設計・調整しながら、クライアントとなるアプリケーション開発も別軸で進められます。限られた期間内で開発するような場合に、この柔軟さは工数の短縮に繋がります。 簡単な例ですが、 user というデータを考えます。 object user { name = "xxxx" age = 37 address = "xxxxxxxxxxxxx" telephone = "03-123-9876" } 格納するデータを設定する画面です。 データタイプも豊富です。オブジェクトの属性は、mapタイプで設定していきます。 セキュリティ データリソース毎にアクセス制御する仕組みがあり、各データに対するルールを設定することで柔軟なセキュリティルールを指定できるようになっています。 アクセス設定 権限種類 read 読取 write 書込 create 作成 update 更新 delete 削除 参考:基本的なセキュリティ ルール 下の例はCloud Firestoreのリソースへのアクセス制御の「ルール記述」のサンプルです。 service cloud.firestore { match /databases/ { database } /documents { // リソースへのアクセス制限をパスで指定する match /コレクション名/リソース名 { // リソースへの読み書きを条件付き許可する allow read, write: if <条件>; } } // 検品検寸アプリの権限設定は、特定のコレクションに対してはフルアクセスとしている match /databases/ { database } /documents { // *(アスタリスク)はワイルドカードでMatchさせるために指定 // Inspectionコレクションに対するセキュリティの設定例 match /Inspection/ { Inspection=** } { // リソースへの読み書きを常に許可する allow read, write: if true ; } } } セキュリティに関しても簡易にかつきめ細かく設定できるという点は安心ができました。 参考:セキュリティの記述方法 Androidからのアクセス実装方法 SDKをimportして実装します。 コールバックは非同期で受け取れるため、アプリケーション側はそれを考慮した実装にする必要があります。 Kotlinでの実装サンプルを下に示します。 データ登録 // Firestoreインスタンス val db = FirebaseFirestore.getInstance() // 設定 val settings = FirebaseFirestoreSettings.Builder() .setPersistenceEnabled( true ) .build() db.firestoreSettings = settings // 登録データはmapで設定 val user = hashMapOf( "name" to "User name" , "age" to 29 , "address" to "xxxxxxxxxxxxx" "telephone" to "090-123-4567" ) // 登録処理 db.collection( "users" ) // コレクション名 .document( "xxxxxxxxxxxxx" ) // リソースパス . set (user) .addOnSuccessListener { documentReference -> // 成功時のコールバック Log.d(TAG, "DocumentSnapshot added with ID: ${ documentReference.id } " ) } .addOnFailureListener { e -> // 失敗時のコールバック Log.w(TAG, "Error adding document" , e) } データ読み取り db.collection( "users" ) // コレクション名 . get () .addOnSuccessListener { result -> // 成功時のコールバック for (document in result) { Log.d(TAG, " ${ document.id } => ${ document. data } " ) } } .addOnFailureListener { exception -> // 失敗時のコールバック Log.w(TAG, "Error getting documents." , exception) } ドキュメントやサンプルなども揃っており、記述をしながらも躓くことなく直感的に実装できました。この実装のハードルの低さも工数削減につながりました。 Android開発 MSP技術推進部のメンバーはAndroid開発経験がなかったため、ZOZOTOWN Androidチームに開発のサポートを依頼しました。うまくコラボレーションするために以下の観点で担当分けをし、それぞれ実装を分担しています。 ビュー周りはAndroid独自の概念が多いためAndroidチーム ロジック周りは事業周りの知識が必要になるためMSP技術推進部メンバー アプリケーション構成 下図のような構成を考えてもらいました。特徴は ViewModel と Activity は「1対1」です。 MainActivity にぶら下がる Fragment がアプリケーションの表示の中心になります。 データ取得から表示までのイメージ UIなどイベント発行を Fragment で検知し、 ViewModel にアクションを移譲 ViewModel は要求されたアクションを実行しFirestoreにデータをリクエスト ViewModel がレスポンスを受け取る ViewModel が Fragment にデータへ渡す Fragment がデータを表示 Fragment の実装例を紹介します。 各Fragmentは下のようにViewModelの参照を持ちます。 vm = ViewModelProvider(requireActivity()). get (SharedViewModel:: class .java) userListButtonボタンのクリックイベント設定します。ViewModelのgetUserList(ユーザーリストの取得)メソッドを呼び出して非同期処理を設定しています。 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { userListButton.setOnClickListener { vm.getUserList().addOnSuccessListener { result -> // 成功処理 } .addOnFailureListener { exception -> // エラー処理 } } } 次に、 ViewModel の実装例です。 FirestoreのTaskオブジェクトを返し、Viewにコールバック処理を移譲します。結果は非同期のため、成否処理を表示側でコントロールしてもらいます。 fun getUserList() Task<QuerySnapshot> { return db.collection( "users" ). get () } 表示は主に Fragment の実装、ロジックは ViewModel とそれと連携する Model などを中心に実装しました。 それぞれの実装だけに注力できたため大きな混乱もなく開発ができました。 画面遷移 アプリの各画面表示はFragmentが入れ替わっているだけです。 BLEメジャーと通信してアプリに結果表示する BLEメジャーで計測したその結果をアプリの画面に表示する実装を紹介します。 BLEメジャーとAndroidアプリ 下図が連携しているイメージです。 デバイス同士のペアリングは予め設定しておきます。ペアリングしていれば、入力待ちになっている(フォーカスがある)テキストフィールドに採寸・測定値が自動的に入ってくる仕様です。 したがって、連携の実装はとてもシンプルで、テキストフィールドのフォーカスの管理とテキストが入力されたときの処理を実装するだけです。 メジャーからは「採寸値 + キー入力値」が入ってくるため、自動でフォーカスが移動します。 // 実装ではテキストフィールドのフォーカスの移動に処理を入れている editText.setOnFocusChangeListener { _, hasFocus -> if (hasFocus.not()) { // フォーカスが外れた時の処理 // // 判定: 採寸値が許容寸の範囲内で仕上がっているかどうかを計算する // 結果を表示に反映する } else { // フォーカスが入ったときの処理は、特に何もせず入力値が入るのを待つ } } 採寸入力デモ ペアリング中のメジャーから採寸値が入力され、検寸結果が即座に判定されます。 配信とアプリのアップデート方法 今回作成したアプリケーションはクローズドなアプリですが、自社以外に配布する必要がありました。さらに日本以外での展開を当初から念頭に入れていました。そのためアプリの配布と更新については、Google Playのストアを経由しない配布とアップデートの仕組みを考慮する必要がありました。 そこで配信に使用したのがFirebaseのCloud Storageです。Androidアプリ側でアプリケーションのバージョンをチェックし、更新の必要な場合はアプリケーションが自動的にダウンロードをするような仕組みを実装しました。 下図がCloud FirestoreとCloud Storageを利用した更新の流れです。 Androidの仕様上アプリ外に一度処理は流れますが、ユーザー操作数を極力少なくできているため、大きな障壁とはなりませんでした。 まとめ 新規Androidアプリとデータベースの組み合わせを2週間という納期でリリースできたポイントを振り返ると以下の通りです。 Firebaseを利用した開発が目指していたアプリサービスの要件と実装の難易度があっていた サーバレス Androidデバイスとの連携 アプリケーション開発の実装ハードルの低さ リリースまでの開発でいくつかの同時進行で開発ができたことで工数を削減できた クライアント側の実装を止めることなくバックエンドのデータ構成を調整しながら開発を進められた Android開発で開発スコープを絞った分業実装(表示レイアウトとロジック実装の開発の分割)ができた チーム内外でのサポートが受けられた ZOZOTOWNチームから人員と実装アイデアなどのサポートをもらえた チームからはデータ構成や資料提供などを優先的にしてもらえた 技術的なポイントとチームとしての連携がうまく絡み合い成し遂げられたリリースでした。 成果 「開発の背景」で説明した検品結果の記録と集計作業をシンプルにするため、下図の構成に変更しました。 検品検寸アプリ導入前は、製品の検品結果は紙とペンを使って記入していました。記入された紙は現場のマネージャーが集計し弊社にメールで送信、その後メールに書かれた結果を弊社で集約し、サマリーレポートとして作成していました。 導入後、検品会社のオペレーターやマネージャーが行っていた結果の記入・転記・集計作業を自動化することができ、作業全体の効率化に貢献できました。 1つ成果として、検品結果の集計作業では以下のような改善効果が得られました。 おわりに 本記事ではMSP技術推進部の取り組みの1つのAndroid検品・検寸アプリの立ち上げについて紹介しました。 ZOZOテクノロジーズでは、ZOZOTOWNやWEARのサービスをはじめ、事業を支えるさまざまな職種を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com ※「QRコード」は、株式会社デンソーウェーブの登録商標です。
こんにちは。ECプラットフォーム部の廣瀬です。 ZOZOテクノロジーズでは、お客様の氏名や住所をはじめとする秘密情報を保護するための様々な取り組みを行っています。本記事ではその中の一部である、データベース(以下、DB)に保存している秘密情報の取扱いルールについてご紹介したいと思います。なお、今回の内容は特定の製品の機能に関する話ではなく、取り組みの基本的な考え方についての話となっています。 背景 何も対策を講じない場合、開発者がDBの秘密情報を権限的には閲覧することが可能な状態となり得ます。例えば、 select * from [秘密情報が含まれるテーブル] といったSQLを実行することで、秘密情報を閲覧できてしまいます。 意図的に機密性の高い情報にアクセスするようなエンジニアは基本的にはいないでしょう。ただし、秘密情報を利用する意図がなくても、新機能のリリース時のデータ確認などで自然と秘密情報が目についてしまう可能性はあります。例えば、単一テーブルのデータを参照する際に「select *」で情報の確認を行った場合などです。 弊社では、以前から フルリモートワーク制度を導入 していました。また、3月末からは原則としてリモートワーク推奨となっております。弊社だけでなく、社会情勢に応じてリモートワークの機会が増えた会社は少なくないと思います。このような状況下においてリモートワークでDBの情報を参照した際に、秘密情報へのアクセスを制限し、情報の流出リスクを最小限に抑える必要性が高まっていると感じています。 そこで、弊社で運用しているDB秘密情報の取扱いルールをご紹介することで、秘密情報保護の取り組みの参考となる内容をご提供できればと考えました。まずは、「どのような情報を秘密情報とするか」についてご説明したいと思います。 秘密情報の明確化 弊社では、保護対象の情報を明確化するために「情報区分表」を作成しています。これは、機密性のレベルを数段階用意し、各レベルにどういった情報が対応するのかを定義した表です。 以下に、情報区分のイメージを示します。 秘密情報への該当 情報区分 考え方 例 〇 機密性最高 最も保護すべき機密性の高いデータ ... 〇 機密性高 ... ... ... ... ... ... × 機密性低 一般的に広く認知されているデータ 都道府県のマスタデータ この情報区分表において、機密性のレベルが一定以上の重要度が高い項目に該当している情報を秘密情報としています。 次に、秘密情報に関連するデータの管理方法についてご紹介します。 管理台帳の作成 弊社では、秘密情報に関連する台帳を2種類作成しています。 1. 秘密情報管理台帳 秘密情報に該当するテーブル、カラムを管理する台帳です。プロダクトやDB単位で作成し、秘密情報が増えるたびに追記していきます。弊社で使用している様々なデータストアに存在する秘密情報を集約するためにこの台帳を作成しています。この台帳があることで、例えば以下のようなメリットがあります。 別のDBにテーブルのデータを連携する際、台帳を確認するだけでマスク対象のカラムを判断できる センシティブな情報を可視化することで、今後必要なセキュリティ関連の施策やルール変更時に該当箇所の洗い出しを一からやらなくて済む 2. 閲覧者管理台帳 秘密情報の閲覧可能者を管理する台帳です。ここに記名のある人物だけが秘密情報を閲覧できる状態となっています。また、気軽に記名できるわけではありません。責任者の許可が必要である点や、DBごとに記名できる人数を制限している点など、記名のためのルールを整備しています。「誰が」「どのデータストアの秘密情報を閲覧可能なのか」を集約して管理するためにこの台帳を作成しています。 ここまでで、「どういった情報が秘密情報に該当するのか」や、「秘密情報やその情報を閲覧可能な人物をどう管理するのか」についてご紹介しました。続いて、秘密情報の取扱いルールを策定するために整備したフローをご紹介します。 秘密情報取扱いルールを策定するためのフロー 弊社で秘密情報の取扱いルールを策定した際には、次のようなフローの整備を行いました。 新しく追加されるデータの取扱い 既存データで秘密情報に該当する項目の洗い出し 秘密情報にアクセスできるアカウントの制限 権限のないアカウントからのアクセス制限 権限保持者の大幅な削減による運用負荷増への対処 以降、順を追って説明します。 1. 新しく追加されるデータの取扱い 「現時点で保持している情報」だけでなく「今後新しく取得される情報」の取扱い方法についても検討が必要となります。例えば、機能の改修や追加に伴ってDBに追加されるデータの種類が増える場合、そのデータが秘密情報に該当するかを判断する必要があります。そこで、新たに取得する情報について、「誰が」「どのような基準で判断し」「秘密情報に該当すると判断した場合はどういったアクションが必要なのか」を明確にしています。 「誰が」 プロダクトまたはDBごとにアサインしたレビュー担当者 「どのような基準で判断し」 社内の情報区分における機密性が一定以上の情報 「秘密情報に該当すると判断した場合はどういったアクションが必要なのか」 秘密情報管理台帳に記載 その秘密情報を閲覧可能となる人物が追加になる場合は閲覧者管理台帳に記載 秘密情報をマスク化し、アクセスできるアカウントを制限 情報の管理を行っている部署に報告 2. 既存データで秘密情報に該当する項目の洗い出し 弊社では、以前より情報区分が整理されており、どのような情報が秘密情報の対象となるかが明文化されていました。また、どのカラムが秘密情報に該当するかの精査も行われている状態でした。既存のシステムでこうした取り組みが行われていない状況下では、秘密情報に該当するか精査されていないカラムが大量に存在することになると思います。全カラムの精査が現実的でない場合は、効率的な既存データの精査方法として、以下のような手順が考えられます。 文字列型のカラムは氏名や住所など、秘密情報が入っている可能性が高いため、全ての文字列カラムの実データをカラムごとに上位100件ほどを目視で確認し、秘密情報が入っていそうなカラムリストを作成 「address」など、秘密情報に該当する可能性が高いカラム名で各DBのメタデータを検索し、秘密情報が入っていそうなカラムリストを作成 作成した「秘密情報が入っていそうなカラムリスト」を人力で精査 最後に責任者のチェックを通して、最終的な秘密情報カラムリストを作成 秘密情報管理台帳に記載 3. 秘密情報にアクセスできるアカウントの制限 DBの秘密情報を閲覧できる人数を限定します。例えば1DBあたり2名など、大きく絞り込むほうが望ましいです。何も対策を実施していない状況から人数の絞り込みを行えば、秘密情報の閲覧可能者を9割以上削減することも可能かと思います。 また、弊社では秘密情報が閲覧可能な人に対しても、秘密情報が「閲覧不可能なログインアカウント」と「閲覧可能なログインアカウント」の2種類を発行しています。これにより、普段は秘密情報を閲覧できないログインアカウントを使ってもらい、どうしても調査に必要な時だけ別のログインアカウントを使って秘密情報を使った調査を実施できるようにしています。 4. 権限のないアカウントからのアクセス制限 秘密情報と判断したカラムのアクセス制御をどこまで行えるかは、DB製品の機能次第かと思いますが、カラム単位でアクセス制御可能な製品も少なくないと思います。弊社では、閲覧者管理台帳に記載のない人は、秘密情報管理台帳に記載のあるカラムは一切閲覧できない状態を構築しています。 5. 権限保持者の大幅な削減による運用負荷増への対処 単純に秘密情報を閲覧する人数を限定するだけでは、エンジニアの調査や運用の負荷が大幅に増加してしまう懸念があります。例えばオンコール担当者がアラートの調査でどうしてもDBの秘密情報を閲覧する必要がある場合に、閲覧権限を保持している人に調査を依頼することで、権限保持者に負担が集中するケースなどです。 こういった懸念への解決策として、例えばワークフロー申請経由で誰でも一時的に有効な、秘密情報閲覧権限を発行できる仕組みの整備などが考えられます。 弊社で整備した仕組みのイメージ図です。kintoneのワークフローの承認をトリガとして専用のAPI経由でログインアカウントが発行されます。ログインの有効期間は短く、期限切れになると自動で削除されます。 まとめ 弊社で実施している、DB秘密情報の取扱いルールに関する取り組みについてご紹介しました。エンジニアの運用負荷をできるだけ上げずに、秘密情報の閲覧可能者をできるだけ限定することが重要だと考えています。 最後に ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
こんにちは! ZOZOTOWNのiOSアプリ開発をしている林と松井です。先日、9/19から9/21までの3日間iOSDC Japan 2020が開催されました。 ブログを書くまでがiOSDC!#didyoublog? 今年はコロナ禍でオンライン開催となり、現地の盛り上がりを体感できませんでしたが、ニコニコ生放送の弾幕などオンラインならではの楽しみがありましたね。また、例年通り素晴らしい発表が盛り沢山でした! オンライン開催においても弊社はスポンサーとして協賛し、10名を超えるメンバーが参加しています。うち5名はスピーカーとしての参加でした。この記事ではiOSDC Japan 2020で登壇するために行った弊社の取り組みと、登壇した社員の発表をご紹介いたします。 採択率向上と登壇内容ブラッシュアップのための取り組み iOSDC Japan 2019ではZOZOテクノロジーズの登壇者が1名のみでしたが、今年はなんと5名も採択されました! 担当しているプロダクトとしてはZOZOTOWNから4名、WEARから1名が登壇しました。今回はZOZOTOWN iOSチームからの採択率を大幅に向上させた取り組みを大公開します。 ネタの整理 過去にCfPに応募する際に、「何を発表すればいいのかわからない」という悩みを持っているメンバーがいました。 ZOZOTOWN iOSアプリは10年以上、改善を続けて進化しているサービスです。レガシーからモダンへのリファクタリング、新しい技術を導入してファッション業界を盛り上げたサービスなど、普段の開発業務でも掘り下げてみると面白い内容がたくさんあったはずです。 それらを可視化するために、iOSチームではネタ表を作成し、整理してみました。まず、最近導入した技術・解決した問題・または興味があって研究したいことを定期的に記入します。そしてチーム内に共有することで、ネタがどんどん集まりました。 実際にこのネタ表から展開して今回のiOSDC Japan 2020の発表タイトルとなった内容がいくつも存在します。 作業時間の確保 ネタがあっても、なかなか調査や検証に着手できないことが多々ありますよね。そこで「もくもく会」を導入しました。もくもく会とは、ネタに対して集中して作業できる専用の時間です。毎週の通常業務として1時間を確保し、チームメンバーが黙々と各自のネタを検証し、最後に進捗を共有します。CfPの応募期限が直前となった時期には、1回のもくもく会を2時間へ増やしたこともありました。気を散らさず集中的に作業することで、スムーズに調査や検証ができました。 このような取り組みを通して、チーム全体の力を合わせてメンバー一人一人をサポートし合っています。その結果、ZOZOTOWN iOSチームがCfPに応募した7件のうち、レギュラートーク2件、LT2件の合計4件が採択されました。 技術顧問レビュー そして採択された後も、当日を迎えるまでサポートは続きます! たとえば、弊社では毎月iOS技術共有会を開催しています。この会は弊社のiOS開発者全員が集まり、それぞれのプロダクト開発で得た知見を共有する場です。共有会には技術顧問である 岸川さん も出席しており、毎月様々なアドバイスを頂いています。iOSDC Japan 2020開催直前は、発表資料に対するレビュー会が行われ「iOSDC Japan殿堂入り」のスピーカーから貴重な意見をいただけました。チーム内の手厚いレビューを経て登壇資料が完成します。 iOSDC Japan 2020当日の工夫 今年のiOSDC Japan 2020は初のオンライン開催となりましたが、弊社ではすでにオンライン開催でのWWDC20参加を経験していたため、大きな混乱はありませんでした。WWDC20についてのまとめは他の記事にてまとめていますので、ぜひご覧ください。 techblog.zozo.com ここでは、開催期間中の働き方、効率的にトークを見るためにチームで行った取り組みをお話します。 開催期間中の働き方 弊社はカンファレンスへの参加が推奨されており、今回のiOSDC Japan 2020も勤務扱いとし、3日間それぞれ振替休日をとる形となりました。 チーム内での効率化 どの発表も興味深い内容であったため、トークの選択に迷われた方も多かったのではないでしょうか。より多くのトークを効率よく視聴するために、チーム内で視聴予定を共有できる工夫をしました。 まず、Miroというツールで事前にどのトークを見る予定か、もしくは迷っているかを宣言するタイムテーブルを作成します。その上にみんなそれぞれ自分の名前ラベルを貼っていき、どの時間帯にだれがどのトークを見ているのかをわかるようにしました。 このようにすると、迷っていた方のトークを他の人が見る予定であれば、もう1つの方を視聴し、後で共有し合うなどといった選択も可能になります。 実際にMiroで用意した画面をご紹介します。 iPadでフルページスクリーンショットを3枚撮り、貼って置くだけの簡単運用です。フルページスクリーンショット便利ですね! 拡大してみると、こんな感じです。 登壇内容の紹介 最後に、弊社のCTO今村からスポンサーセッション1件、採択された5件、技術顧問である岸川さん2件の合計8件のトークをご紹介します。 「ファッション業界を技術で変える、ZOZOの挑戦〜CTOが語る理想の組織像とは〜」 最初に紹介するのは、CTO今村( @kyuns )のスポンサーセッションです。 本セッションではiOSエンジニアの働き方やこの1年間の組織・プロダクトの変遷と改革について紹介しました。私たちiOSエンジニアが日頃どのように開発に取り組んでいるのか、組織やプロダクトはどのように進化しているのかを様々な数値やキーワードとともに説明しています。iOSエンジニアが日頃活用している開発環境や補助制度(iPad Proの貸与やApple Siliconの開発者Kit購入補助など)についても紹介しております。 fortee.jp 「iOSではじめるWebAR」 次は、 @ikkou のレギュラートークです。 ARやVRといったXR領域が大好きなikkouは、ARKitに関する技術を定常的にキャッチアップしています。今回テーマとした選んだWebARは、Web技術ということもあり取っつきやすさはピカイチです。もっと多くの人にARについて知ってもらいたく、iOSDC Japan 2020に登壇しました。ARの基礎からサンプル付きで主流の技術まで、目的別で各技術を詳しく紹介しています。これからWebARをはじめたい方はぜひご覧ください! fortee.jp 「iOSアプリのバッテリー消費を意識する」 次は、 @arara_jp のレギュラートークです。 Appleやユーザーはバッテリーを非常に重視していますが、デベロッパーはクラッシュフリー率や起動速度など他の指標と比べると、バッテリーにそれほど関心を示していない傾向があります。 ですが、この発表の後、Ask the Speakerで「バッテリーの調査方法ありがとう!」とコメントをいただいたり、「自分のアプリも見てみよう」とツイートがあったりと、少しでもバッテリー消費に関心を持つきっかけを生み出せた気がします。ぜひご覧いただき、開発者の皆さんに少しでもバッテリー消費に関しての意識付けがされたら幸いです。 fortee.jp 「テストコードが増えるとバグは減るのだろうか?「0%→60.3%」で見えた世界の話」 次は、 @ahiru のレギュラートークです。 テストコードを書く文化がなかったZOZOTOWNのコードにテストを導入し、この1年でテストカバレッジの割合を0%から56.4%(タイトルの60.3%から訂正)にまで増加させたお話です。 当日はトーク後のAsk the Speakerも大行列ができており、自動テストやQAチームとの勉強会の内容など、ZOZOTOWNの実情に関する質問が途切れませんでした。他社のテストコード導入事例に興味のある方が多いのではないでしょうか。本トークでは実体験を軸にしたメリット・デメリット、組織や開発体勢に適したテスト手法の一例も紹介しています。テストコードを導入した他社事例をはじめ、テストに興味がある方はぜひこのトークを見てください。 fortee.jp 「文字列をコピーできるスクリーンショットを作る」 次は、 @re___you のLTです。 こちらのLTでは、文字列をコピーできるスクリーンショットの作り方について、コードやわかりやすい図を使って解説しています。画像を文字に起こす必要がなくなり、iOSエンジニアとしてもデザインデータの検索をする際など、とても便利だと思います。iOS 13からフルページのスクリーンショットが可能となったことについて「知らなかった!」という声も多くありましたが、フルページのスクリーンショットの作成については他の記事にまとめてありますので、ぜひこちらもご覧ください! techblog.zozo.com fortee.jp 「100人以上の中高大学生にiOSアプリ開発を教えていて感じたこと」 次は、 @tosh_3 のLTです。今回、初めてのカンファレンス参加かつ登壇となりました。 新卒のtosh_3は、なんとすでに100人以上の中高大学生へ教える立場にあったんです! その驚異の経験から、発表中のニコ生コメントも「新卒とは」「新卒ですでに教える立場」とかなり盛り上がっていました。教え方の工夫だけではなく、iOSエンジニアとして考えるべき視点についても語っていますので、ぜひスライドを覗いて見てくださいね。 fortee.jp 「SourceKit LSPをブラウザでコードを読むために活用する」 ここからは岸川さんのレギュラートークを紹介します。 本トークは、岸川さんが今年仕事や趣味で携わってきた、SourceKit-LSPについての発表でした。ブラウザでGitHub上のSwiftコードを読むときに、Xcodeのようにメソッドの定義元にジャンプできるデモが行われた際には、ニコ生コメントで弾幕ができるほど盛り上がりました。SourceKit-LSPの基礎から活用まで一連の知識がわかりやすく紹介されて、コードレビューの効率が革命的に上がる技術になりますので、ぜひご覧ください。 fortee.jp 「400種類のアプリを毎日ビルドする自動化の技術」 次にも、岸川さんのレギュラートークとなります。 このトークでは、Slackbot・fastlane・Bitriseなどの機能を活用して、CIビルド用の情報の自動収集や更新・プッシュ証明書の自動更新・App Store Connectの申請情報の自動更新など、究極の自動化を追い求める技術が幅広く語られました。現状のZOZOTOWNアプリでも、究極を求めて改善できるところが多々あります。岸川さんにご協力を頂きながら、より自動化されたZOZOTOWNアプリを目指していきます。 fortee.jp 以上、セッションの紹介でした。 After iOSDC Japan 2020を開催 ブログを書くまでがiOSDC! と言いますが……いえいえ、iOSDC Japan 2020はまだ終わりませんよ! 今回開催されたiOSDC Japan 2020について、弊社ではAfter iOSDC Japan 2020を9月29日(火)に開催します。Sansan(株)、note(株)、(株)ZOZOテクノロジーズの3社による合同イベントです。 各社の社員によるLT、パネルディスカッションを行いますので、興味のある方はぜひご参加ください! こんな方におすすめです。 iOS関連技術およびすべてのソフトウェアエンジニア iOSDC Japan 2020の振り返りを一緒にしたい方 iOSDC Japan 2020には参加していないけど、情報が知りたいという方 本イベントには、ZOZOテクノロジーズの技術顧問でもある岸川さんも登壇します。 iOSDC Japan 2020に参加した人もそうでない人も、みんなで振り返りましょう! イベント申し込みはこちらから! zozotech-inc.connpass.com さいごに ZOZOテクノロジーズでは、一緒にモダンなサービス作りをしてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! https://tech.zozo.com/recruit/ tech.zozo.com
デバイスに関わる全国の情シスの皆様、日々の業務お疲れ様です。コーポレートエンジニアリング部ファシリティチームの佐藤です。いわゆる”情シス”と呼ばれる役割のチームに所属し、社内インフラ(PCやネットワーク機器)の管理・運用に携わっております。 今回はこのリモートワークが普及してきた中で情シスが解決したい課題の1つである貸与デバイスのキッティング、特に管理者が端末に一切触れずにキッティングを完了できる ゼロタッチキッティング を実現するまでの話を紹介します。 ゼロタッチキッティング導入前の状況 ZOZOグループではWindows 10およびMacのPC端末が数多く存在します。我々ZOZOテクノロジーズの情シスも日々デバイス関連の対応に尽力しています。 1つ大きな課題として、 社員(以下、ユーザーと呼びます)へPC端末を貸与して、社内規定のポリシーに従って正しく利用を開始してもらうための運用確立 というものがあります。 ゼロタッチキッティング導入前は、Windows端末のキッティングを オンプレミスActive Directory参加によるHybrid Azure AD参加 で対応しておりました。以下の通り、仕組みとしては単純でオンプレミスActive DirectoryドメインへPCを参加させると、GPO(グループ・ポリシー・オブジェクト)にて自動でIntune(現在はMicrosoft Endpoint Configuration Manager(MEM)に統合されているため、以降はMEMと記載します)へ登録されるというものです。 以前は当たり前のように会社へ出社して、当たり前のように情シスがその場で設定してユーザーへ手渡すという運用となっていました。 しかし。。。 テレワーク環境への対応と対策 2020年4月、新型コロナウィルス感染症拡大による緊急事態宣言の発令前に当社は強制在宅勤務が開始されました。そのような状況下で既存のオンプレミスActive Directory環境だけを用いてのキッティング運用では、ユーザーが即利用可能になるレベルでのPC配布には無理がありました。 問題点を挙げるときりがありません。 そもそもオフィスに行けないため、情シスからユーザーへ直接PCを手渡しすることができない そもそも情シスメンバーもオフィスに行けないため、オフィスに配送されたPCを社内LANに繋ぐことができない PCは一度社内LANに接続することで、MEMへの登録、規定ソフトのインストールを実施していました そもそもオフィスに行けないため、ユーザーアカウントでのログインができない 結論としては オフィスを訪れないとキッティングできない 状態でした。 このことから考え方を180度変えて、ユーザーがオフィスに来なくてもPCがセットアップできるようにするしかないという決断に至りました。そのため新しくHybrid Azure AD参加からAzure AD参加でのキッティング運用に取り掛かりました! ここで少しネタバレをしますと、このような状況になる前、情シスの内部でAzure AD参加やAutopilotの検証やプレビュー版の動作確認などを行っておりました。そのためMEMやAzure ADでの動作や条件付きアクセスに伴うセキュリティ関連の設定などはすでにある程度把握済みであったため、強制在宅勤務への対応にすばやく切り替えられました。 Azure AD参加によるキッティング ZOZOテクノロジーズではPCのキッティング方式を Azure AD参加を利用したユーザー主導のキッティング へ移行しました。 具体的な手順は以下の通りです。 Windows 10端末を初期化された状態で、ユーザーの自宅へ配送する 情シスはユーザーがキッティングを実施する前に、ユーザーアカウントを用意し、適切な構成プロファイルやアプリが導入されるためのグループへユーザーアカウントの追加作業を行う ユーザーは自宅で手順書を参照しながらキッティングを開始し、情シスは必要に応じてサポートを行う Azure ADへの参加完了後、自動でMEMへの登録や構成プロファイル、アプリケーションがバックグラウンドで導入されるため、自宅から業務ができる環境の構築が完了 Azure AD参加のキッティングへ移行することによって ユーザーがオフィスへ訪れることなくキッティングが完了する という目的を達成しました。 もちろん、その結果として貸与デバイスに「Azure AD参加端末」と「Hybrid Azure AD参加端末」が混在することになり、多くの運用方法の修正やIntuneの追加設定作業が発生しました。 Azure AD端末の運用で解決した主な事例を紹介します。 Azure AD参加端末のローカル管理者グループの編集 この課題は 構成プロファイルで解決 しました。 構成プロファイルの種類で「カスタム」を選択し、OMA-URI の設定にて以下の値を変更することでローカル管理者グループの編集が可能となります。 ./Device/Vendor/MSFT/Policy/Config/RestrictedGroups/ConfigureGroupMembership Azure AD参加端末はあくまでワークグループ端末になるため、GPOでのポリシーの適用が不可になります。そのため一括でのポリシー変更はMEMでの構成プロファイル機能を使用します。まだまだGPOと同じようなきめ細かいポリシー設定はできませんが、カスタム設定(OMA-URI)を使用することで設定範囲を広げることができます。 Intune で Windows 10 デバイス用のカスタム設定を使用する Windows Updateの制御 この課題は Windows 10 更新リングで解決 しました。 更新リングの作成によりWindows Update for Business からの Windows 10 ソフトウェア更新プログラムのインストールを制御しました。品質更新または機能更新プログラムの延期日数や自動更新時の動作の設定が可能になり、個別のPC対応は不要となります。 Azure AD参加端末はあくまでワークグループ端末になるため、GPOでのポリシーの適用が不可になるため、WSUSによる更新プログラム制御はできません。 この状態では、Windows 10は更新プログラムが適用可能時期になると累積更新や機能更新プログラムを即適用してしまうため一律でバージョン固定を行う必要があります。 更新リングでは品質も機能も延期期間の設定がそれぞれ指定できるため、適用時期のタイミングを日数にて制御することができます。 WSUSのように特定の更新プログラムに対しての個別での制御は不可になりますのでご注意ください。 Windows 10 更新プログラムの展開リングの構築 「Azure AD参加端末」と「Hybrid Azure AD参加端末」が混在した環境での問題の切り分けとして大事なことは、その端末がどのオンプレ環境へ依存しているのかを考えることです。 例としては、WSUSでできたことをどうすればMEMで同じように再現できるのかなどを柔軟に検討することです。 もともとAzure AD参加について検証やテストを実施していたことで、大きな混乱や問題が発生することなくこのキッティングを開始することができました。事前検証は大事だなと感じました。 Azure AD参加型キッティングに残る課題 2020年9月現在もいまだ在宅勤務期間は続き、新しい働き方の流れが進む中で情シスのPCキッティングも新しい様式へ変化していくことになります。この期間でAzure AD 参加を利用したユーザー主導のキッティングを進めてきましたが、この方式では完全な自動化にはなっていないのが現状でした。 現状では、ユーザーがキッティングを開始するタイミングで、我々情シスも都度サポート対応が必要になります。また、アプリやプロファイルが適用されるタイミングが端末やネットワーク環境によって異なり、キッティングの進捗確認が困難でもあります。 上記の課題があるので、このキッティング方式についてはユーザーが自由なタイミングでキッティングが開始できず、情シスとのスケジュール確認や連絡などが業務のボトルネックとなりました。いつでもどこでも安定したキッティング環境を提供できるようにするため、我々情シスはキッティングに関して多くの情報と検証を進めました。 ホワイトグローブ(White Glove)の導入 前述の課題を解決するために我々はWindows 10 バージョン1903 からサポートされる ホワイトグローブ(White Glove)展開 を新たに採用しました。 ホワイトグローブ展開とは、Windows 10のセットアップのうちユーザーに関連する一部を除く大部分のセットアップをOEMベンダーと情シスの手元で完了させておく展開方式です。 ホワイトグローブ展開の主な流れは以下の通りです。 OEMベンダーからキッティングセンターにPCが配送される OEMベンダーが配送したPCのデバイスIDをパートナーインビテーションを許可したZOZOグループのクラウドに登録する 情シスはデバイスの登録を確認後、適切なプロファイルおよびアプリが展開されるグループへ割り当てを行う キッティングセンターにて、ホワイトグローブ展開のためのセットアップを各PCにて実施する(これによりプロファイルやアプリなどがPCへ登録される) ユーザーの自宅へPCが配送されるため、ユーザーはPCを起動しAzure ADアカウントで初回ログインする ユーザーの個別設定のみ登録が開始され、ユーザーは即座に業務ができる状態となる これでOEMベンダーおよびキッティングセンターでクラウド登録からPCの基本設定などが完結するため、ホワイトグローブ展開でキッティング運用を行うと情シスの作業が少なくなりました。 また、社内の貸与デバイスでどのPCがホワイトグローブ展開でキッティングを行ったのかも、Azure ADのデバイス一覧でアイコンにて確認することができます。 ホワイトグローブ展開でのポイントとしては、ホワイトグローブ展開を設定する端末は初回起動時にインターネット接続されていない状態にすることです。 インターネット接続状態で起動すると即時でMicrosoftアカウントでのログインを求める画面になりますが、インターネット未接続の状態で起動すれば通常の言語選択の画面になるので、Windowsキーを5回押してホワイトグローブ展開設定の画面に遷移することができます。 まとめ ホワイトグローブ展開の採用でWindows 10端末のキッティング運用がスムーズになり、実際に利用していただくユーザーへの負担を大幅に減らすことができました。 もちろんこれでキッティングに関しての課題が無くなった訳ではありませんが、コーポレートエンジニアリング部ではどのような状況にも対応できるように新しいサービスや技術をいち早くキャッチアップできるように日々努めていく所存です。 また社内にはWindows 10端末以外にもMacやモバイルデバイスなども存在し、今後も増えていくことが想定されるため、そちらのゼロタッチキッティングにも取り掛かっていきます。 さいごに ZOZOテクノロジーズでは、社内の課題をITの力で解決する仲間を募集中です。WindowsとMacはもちろんiPhoneやiPadなど様々なデバイスを効率よく管理と制御することにどんどんチャレンジできます! ご興味のある方は、下記のリンクからぜひご応募ください! tech.zozo.com
こんにちは、ECプラットフォーム部の鶴見、竹中です。普段はZOZOTOWNのリプレイスに関わるID基盤とAPI Gatewayの開発を行っています。 本記事では、API Gatewayの開発で取り入れているJSON Schemaを使ったドキュメントの自動生成および、スキーマの自動検証を紹介します。 API Gateway設定ファイルの運用改善 弊社で開発しているAPI Gatewayは、APIへのリクエストのルーティングやリトライなど様々な機能の制御を設定ファイルで行っています。 バックエンドチームはアプリケーションおよび、設定ファイルの仕様書を作成し、SREチームはインフラの構成やパフォーマンスを考慮しながら仕様書に記載されたフォーマットで設定ファイルを記述しています。 設定ファイルの仕様書はConfluenceで、アプリケーションはGitHubで管理しています。設定ファイルの仕様変更が発生した場合は、アプリケーションのデプロイ後に仕様書を手動で更新するという運用をしていました。しかし、手動での更新は更新漏れやタイプミスによる設定可能値の誤りなどの問題を発生させていました。 この問題を解決するための方法として、以下の3つが考えられます。 仕様書のレビュー体制の強化 設定値に対する検証処理コードからの仕様書の自動生成 仕様書からの設定値に対する検証処理コードの自動生成 今回は、実現の容易さから最後の「仕様書からの設定値に対する検証処理コードの自動生成」を選びました。この場合、自然言語からの自動生成は困難ですが、何らかのスキーマ言語を用いることで実現が比較的容易になります。 このスキーマ言語に対して求められることは、仕様を記述するのに十分な表現力を有すること、および検証処理の自動生成が容易であることの2つです。 また同時に、スキーマ言語を用いることで可読性が落ちるため、HTMLドキュメントなどへの変換が容易に行える必要もあります。 これらの条件を満たすスキーマ言語として JSON Schema を選定しました。JSON Schemaは、JSONドキュメントを検証可能にするための標準規格です。 改善方法 今回の運用改善ですることをまとめると、以下の通りです。 JSON Schemaで設定ファイルの仕様を定義し、アプリケーションと同じGitHubリポジトリで管理 JSON SchemaからHTMLドキュメントを自動生成 アプリケーション起動時、個別に行っていた設定値の検証処理をJSON Schemaに基づいた検証処理へ変更 上記の改善で行ったJSON Schemaを使ったドキュメントの自動生成および、スキーマの自動検証の2つについて具体的な実装方法を紹介します。 JSON Schemaを使ったドキュメントの自動生成 最初にJSON Schemaからドキュメントを自動生成する方法を紹介します。ドキュメント生成には、Python製の JSON Schema for Humans を利用しました。 JSON Schema for Humans JSON Schema for Humansは、JSON Schemaからドキュメント化した静的なHTMLページを生成します。実際はAPI Gatewayの仕様を定義したJSON Schemaを使用しますが、今回は説明のため 公式サンプル のJSON Schemaを元にドキュメント化してみます。 { " $id ": " https://example.com/arrays.schema.json ", " $schema ": " http://json-schema.org/draft-07/schema# ", " description ": " A representation of a person, company, organization, or place ", " type ": " object ", " properties ": { " fruits ": { " type ": " array ", " items ": { " type ": " string ", " examples ": [ " apple " ] } } , " vegetables ": { " type ": " array ", " items ": { " $ref ": " #/definitions/veggie " } } } , " definitions ": { " veggie ": { " type ": " object ", " required ": [ " veggieName ", " veggieLike " ] , " properties ": { " veggieName ": { " type ": " string ", " description ": " The name of the vegetable. ", " examples ": [ " potato " ] } , " veggieLike ": { " type ": " boolean ", " description ": " Do I like this vegetable? ", " examples ": [ " true " ] } } } } } generate-schema-docコマンドを使い、JSON Schema(schema.json)を元にドキュメント(schema_doc.html)を生成します。 generate-schema-doc schema.json schema_doc.html コマンド実行により、以下3ファイルが自動生成されました。 schema_doc.html schema_doc.min.js schema_doc.css 生成されたHTML(schema_doc.html)を確認します。 JSON Schemaがドキュメント化されました。 ドキュメント生成の自動化 JSON Schemaを修正するたびに、generate-schema-docコマンドでドキュメント生成できます。しかし、手間がかかるため自動化します。自動化には GitHub Actions を利用しました。 GitHub Actionsは、ワークフローを自動化するサービスです。JSON SchemaはGitHubリポジトリで管理しているため、プッシュやプルリクエストがマージされたタイミングでドキュメント生成処理をすることで自動化できると考えました。 以下のステップでドキュメントを自動生成しています。 JSON Schemaを修正し、GitHub masterブランチにマージ GitHub Actions上でPythonおよび、JSON Schema for Humansをセットアップ generate-schema-docコマンドにより、JSON Schemaからドキュメント(HTML/CSS/JavaScript)を生成 ドキュメントファイルをS3にアップロード 自動化によりS3へアクセスすれば、常に最新のドキュメントを確認できます。S3は、社員のみ参照できるようアクセス制限をしました。 GitHub Actions GitHub Actionsで行っている処理内容を紹介します。JSON Schema for Humansをセットアップ、JSON Schemaからドキュメントを生成し、S3にアップロードする方法です。 name : Update Docs on : pull_request : branches : - master types : - closed paths : - schema.json jobs : updateDoc : name : Update Docs runs-on : ubuntu-latest steps : - name : Checkout uses : actions/checkout@v2 - name : Set up Python uses : actions/setup-python@v2 with : python-version : '3.8.5' - name : Generate Docs run : | pip install json-schema-for-humans generate-schema-doc schema.json ./docs/schema_doc.html - name : Configure AWS Credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ap-northeast-1 - name : Upload working-directory : docs run : aws s3 sync . s3://docs --delete プルリクエストがマージされたタイミングで動作するように設定しています。また、GitHubリポジトリにはJSON Schema以外のファイルも含まれているため、pathsで対象JSON Schemaファイル(schema.json)を指定しました。これによりJSON Schemaに変更があった時のみGitHub Actionsが動作します。 JSON Schemaを使ったスキーマの自動検証 設定ファイル(スキーマ)の誤りをアプリケーション起動時に気付けるよう、アプリケーション内で設定ファイルに対しJSON Schemaを利用して検証する仕組みを作りました。 API GatewayではGoを用いて開発しているため、実装にあたり gojsonschema というパッケージを利用しました。また、設定ファイルはYAMLで管理していたため、JSONにはせず、YAMLのままとしました。検証時はYAMLをJSONに変換しています。 JSON Schemaは、先程の JSON Schema for Humansのサンプル を利用します。 次に、検証するための設定ファイルを用意します。 fruits : - apple - orange - pear vegetables : - veggieLike : true veggieName : potato - veggieLike : false veggieName : broccoli JSON Schemaを利用して設定ファイルを検証する実装が以下のソースコードです。YAMLからJSONへの変換には、 yaml というパッケージを利用します。 package main import ( "fmt" "io/ioutil" "github.com/ghodss/yaml" "github.com/xeipuuv/gojsonschema" ) func main() { fruitsData, e := ioutil.ReadFile( "fruits.yaml" ) if e != nil { panic (e) } converted, e := yaml.YAMLToJSON(fruitsData) if e != nil { panic (e) } schemaLoader := gojsonschema.NewReferenceLoader( "file:///home/me/fruits-schema.json" ) documentLoader := gojsonschema.NewStringLoader(fmt.Sprintf( "%s" , converted)) result, e := gojsonschema.Validate(schemaLoader, documentLoader) if e != nil { panic (e) } if result.Valid() { fmt.Printf( "The document is valid \n " ) } else { fmt.Printf( "The document is not valid. see errors : \n " ) for _, desc := range result.Errors() { fmt.Printf( "- %s \n " , desc) } } } 実行結果は以下のようになり、設定ファイルがJSON Schemaの定義通りであることが確認できます。 > go run main.go The document is valid JSON Schemaとは異なる定義にした設定ファイルを用意します。 fruits : - apple - orange - pear vegetables : - veggieLike : true veggieName : potato # 必須プロパティ不足 - veggieName : broccoli 実行結果はエラーになり、設定ファイルのミスに気づけます。 > go run main.go The document is not valid. see errors : - vegetables.1: veggieLike is required 細かい制御の追加 ここからは、より実用的な制御例を紹介します。JSON Schemaの例は全体の一部を抜粋して記述しています。他の詳細なオプションは 公式ドキュメント をご参照ください。 不要なプロパティを許可しない "additionalProperties": false の追加により、記述されたプロパティ以外をエラーにします。typoを防ぐのにも役立ちます。 { " type ": " object ", " properties ": { " fruits ": { " type ": " array ", " items ": { " type ": " string " } } , " vegetables ": { " type ": " array ", " items ": { " $ref ": " #/definitions/veggie " } } } , " additionalProperties ": false } typoしたYAML設定ファイルを用意します。 # fruitsのtypo fruit : - apple - orange - pear vegetables : - veggieLike : true veggieName : potato - veggieLike : false veggieName : broccoli プロパティのエラーとして正しく検出されます。 > go run main.go The document is not valid. see errors : - ( root ) : Additional property fruit is not allowed 特定の値のみ許可する "enum": ["potato", "broccoli", "pumpkin"] の追加により、記述された値以外をエラーにします。 " definitions ": { " veggie ": { " type ": " object ", " required ": [ " veggieName ", " veggieLike " ] , " properties ": { " veggieName ": { " type ": " string ", " enum ": [ " potato ", " broccoli ", " pumpkin " ] , " description ": " The name of the vegetable. " } , " veggieLike ": { " type ": " boolean ", " description ": " Do I like this vegetable? " } } } } 許可されていない値を指定した設定ファイルを用意します。 fruits : - apple - orange - pear vegetables : - veggieLike : true veggieName : potato - veggieLike : false # enumに定義されていない名前 veggieName : carrot 値のエラーとして正しく検出されます。 > go run main.go The document is not valid. see errors : - vegetables. 1 .veggieName: vegetables. 1 .veggieName must be one of the following: " potato " , " broccoli " , " pumpkin " まとめ JSON Schemaを使ったドキュメントの自動生成およびスキーマの自動検証を紹介しました。JSON Schemaを用いたことで仕様が管理しやすくなり、仕様書と実際の検証処理に乖離が発生することを防ぐことができました。YAML、JSONを使った設定ファイルの運用方法に迷っている方は参考にしてみてください。 最後に、ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! https://tech.zozo.com/recruit/ tech.zozo.com
こんにちは。ZOZOテクノロジーズSRE部の市橋です。普段は主にAWSを用いてプロダクトのシステム構築、運用に携わっています。今回は弊チームで取り組んでいるZOZOMATのシステム改善業務の一例として、JVMの暖機運転の仕組みを取り入れた話をご紹介します。 ZOZOMATとは お客様の足を3Dで計測するために開発された計測用マットです。ZOZOMATでの計測情報をもとに、靴の推奨サイズを参照するなどのサービスをご利用いただくことが可能です。ご興味のある方は こちら をご確認ください。 JVMの暖機運転とは 今回テーマとして取り上げるJVMの暖機運転とは何かについて簡単に触れていきます。JVMではJIT(Just In Time)コンパイラによるコンパイル方式が取り入れられています。これはアプリケーションの実行前にプログラムの全てを機械語にコンパイルするのではなく、プログラムの実行時にコンパイルを行います。そのため起動直後の一度もプログラムが実行されていない状態では、インタプリタ型の言語のようにプログラムの各行を機械語に変換しながら実行します。コンパイル済みの場合は高速に動作することが期待できますが、機械語に変換しながら実行しなければならないケースでは、しばしば性能問題となることがあります。 JVMの暖機運転とは、コンパイル済みの状態で本番トラフィックの処理が可能になるよう、サービスインの前にアプリケーションを実行することです。これによりJITコンパイラによって引き起こされる問題を回避できます。 ZOZOMATとJVMの暖機運転について ZOZOMATでは開発言語としてScala、実行基盤としてEKSを採用しています。ScalaはJVMの上で動作するため、JVM上で動くことによる恩恵、制約を共に受けます。当該環境でもデプロイ時やスケールアウト時など、新規にアプリケーションが起動するタイミングでレスポンス遅延を確認しており、JVMが未暖機であることに起因する問題が発生していました。この問題を解消することが今回の目的となります。 この仕組みを取り入れる際、以下の記事が今回の要件と合致していたため大変参考にさせていただきました。 KubernetesでJVMアプリを動かすための実践的ノウハウ集 / JVM on Kubernetes - Speaker Deck 本記事では詳しく触れていないZOZOMATのシステム構成については別のブログ記事にて紹介しております。よろしければご確認ください。 techblog.zozo.com JVMの暖機運転の導入以前のレスポンス遅延問題 以下はデプロイ時のAPIごとのレスポンスタイムの平均値と最大値をそれぞれ表すグラフになります。波形が跳ね上がっている箇所がデプロイによりアプリケーションコンテナが入れ替わったタイミングです。APIによって差はあるものの、デプロイ直後にレスポンスタイムが悪化し、その後収束する傾向にあることがわかります。 レスポンスタイム - 平均値 レスポンスタイム - 最大値 JVM暖機運転の事前検証 暖機運転の有効性の確認 ここでは今回問題として取り上げたレスポンス遅延に対して、暖機運転の有効性を確認することを目的とした検証を行います。当該環境で実行回数が多い参照系、更新系のAPIを1つずつ選択し、未暖機状態と1から1,000回の間で段階的に回数を増やして暖機運転を行い、その後のレスポンスタイムを計測しました。結果は以下の通りです。 暖機回数 レスポンスタイム(参照系) レスポンスタイム(更新系) 暖機運転なし 4.38s 11.53s 暖機回数 1 0.30s 0.44s 暖機回数 10 0.23s 0.42s 暖機回数 100 0.17s 0.35s 暖機回数 1,000 0.16s 0.35s 暖機運転なしの状態と暖機運転ありの状態を比較すると、暖機運転ありの場合にレスポンスタイムが改善しているため、暖機運転の導入は有効と考えられます。ここでの結果をまとめると以下のようになります。 未暖機の状態では顕著にレスポンスが遅く、一度でも実行されると大幅に改善する。 暖機回数が増えるにつれてレスポンス速度が向上する。 一定回数を超えると暖機回数が増えるにつれて徐々にレスポンス速度の改善効果は緩やかになる。 JVM暖機運転の構成 暖機運転の有効性が確認できたところで、JVMの暖機運転を行うための構成を考えていきます。構成案としては以下の三点になります。 方式 概要 Pros Cons カナリアデプロイ方式 ロールアウト時にn%のトラフィックを新しく起動したpodに流す。 既存のpodに変更を加えることなく実現可能。 ユーザー影響が発生することに変わりはない。 Sidecar方式 暖機運転用コンテナをSidecarとして設定し、暖機運転がなされた後に本番トラフィックを流す。 既存のアプリケーションコンテナに変更を加える必要がない。 サイドカーコンテナのリソースが暖機運転を終えても残り続けるため、予約しているリソースが無駄になる。 postStart方式 KubernetesのpostStartの仕組みを利用し、アプリケーションコンテナの起動後に自分自身に対してリクエストを発行することで暖機運転を行う。 追加でリソースを予約する必要がない。 postStartはコンテナのentrypointの実行後に呼ばれる保証がない。 アプリケーションコンテナに暖機運転を行うための変更を加える必要がある。 それぞれの構成のイメージとしては次のようになります。 以上の方式の中から、ユーザー影響を発生させず、既存のアプリケーションコンテナに変更を加えることなく実現できるSidecar方式を採用しました。 この構成にする上ではいくつか考慮するポイントがありました。 本構成を導入する上での考慮ポイント 暖機運転の実行前にアプリケーションコンテナの起動を保証するには アプリケーションコンテナの起動前に暖機運転が開始されてしまうと、リクエストを処理できず暖機運転の効果が得られないため、これを回避する必要があります。この対策として、アプリケーションコンテナが最低限起動に要する時間まで暖機運転用コンテナが起動を待つように、KubernetesのinitialDelaySecondsを設定しました。 しかし、これだけではアプリケーションの起動に想定よりも時間がかかってしまった場合、起動前に暖機運転が開始してしまう懸念を拭いきれません。アプリケーションコンテナの起動に余裕を持たせるため、暖機運転用コンテナの起動開始を必要以上に遅らせてしまうと、今度はデプロイ時間が伸びるという別の問題にぶつかります。 そこで、暖機運転を行う前に暖機運転用コンテナからアプリケーションコンテナに対して、ヘルスチェック用のエンドポイントが実行可能かチェックする処理を設けました。私たちのアプリケーションはgRPCで稼働しているため、 grpc-health-probe を利用してアプリケーションの起動を確認しています。このチェックが通った後に暖機運転を開始することで、アプリケーションコンテナが起動していることを保証できます。 暖機運転の完了後に本番トラフィックが転送されることを保証するには 暖機運転が完了する前に、アプリケーションコンテナにリクエストが転送されてしまうと、レスポンス遅延発生の可能性があります。そのため、暖機運転の完了後にリクエストが転送されることを保証する必要があります。 これについてはkubernetesの機構であるReadinessProbeを利用しました。ReadinessProbeはコンテナがトラフィックを受ける準備ができているかを確認する設定です。これにパスしないとpodにIPが割り当てられず、リクエストは転送されません。これを暖機運転用コンテナに対して、暖機運転の完了後にパスするように設定することで、未暖機状態のアプリケーションコンテナにリクエストが転送されることを防ぎます。 また、これに似た設定としてLivenessProbeがあります。こちらはコンテナの生存確認を行う設定で、これにパスできない場合はコンテナが停止し、再生成されます。今回は暖機運転用コンテナにReadinessProbeを設定し、アプリケーションコンテナにLivenessProbeを設定しました。このように設定することでアプリケーションコンテナの死活監視を行いつつ、暖機運転の完了後にリクエストが転送されるように制御できます。 これを実現するには様々な手段があると思いますが、一例としてReadinessProbeを設定した暖機運転用コンテナの定義ファイルを以下に示します。ReadinessProbeでは /tmp/healthy ファイルが存在することをチェックしています。暖機運転用スクリプトが完了したことを以て、 touch /tmp/healthy コマンドを実行して対象のファイルを生成します。これによりReadinessProbeをパスし、トラフィックがアプリケーションコンテナに転送されます。 apiVersion : apps/v1 kind : Deployment metadata : name : api-deployment labels : app : api spec : # --------- omit template : metadata : labels : app : api spec : containers : - name : warmup image : zozomat/warmup command : [ "sh" , "-c" ] args : [ "bash scripts/warmup.sh && touch /tmp/healthy && tail -f /dev/null" ] readinessProbe : exec : command : [ "cat" , "/tmp/healthy" ] initialDelaySeconds : 60 periodSeconds : 5 failureThreshold : 5 # --------- omit 起動直後の遅延が解消するかの検証 暖機運転を行うための構成が決定したため、リリース前に実際のトラフィックに近い条件下で暖機運転の効果を最終確認するため、負荷試験を行いました。負荷試験ツールはGatlingを使用しています。暖機運転の導入前後で比較できるように暖機運転ありと暖機運転なしの状態でそれぞれ実施しました。以下は負荷試験時のレスポンスタイムを表すグラフです。上は暖機運転なし、下は暖機運転ありの結果となります。 暖機運転なしの負荷試験の結果 暖機運転ありの負荷試験の結果 暖機運転なしの結果では、開始直後は10秒を超えるリクエストが大多数を占めており、非常に低速です。開始から少し経つと徐々にレスポンス速度が向上していき、1分経過後は安定して高速にレスポンスできていることがわかります。暖機運転ありの結果では、未暖機時に発生していた開始直後の遅延が解消できていることがわかります。 導入後の改善効果 以下は参照系、更新系のデプロイ前後のレスポンスタイムの平均値と最大値を、暖機運転の導入有無の観点で比較した表とそれをグラフ化したものです。 導入前後のレスポンスタイム比較 - 平均値 API 導入前 (ms) 導入後 (ms) 差 (ms) 改善率 (%) 参照系 API1 27 5 -22 81.48 参照系 API2 373 98 -275 73.73 参照系 API3 28 5 -23 82.14 参照系 API4 253 49 -204 80.63 参照系 API5 289 197 -92 31.83 更新系 API1 486 423 -63 12.96 更新系 API2 468 434 -34 7.26 更新系 API3 119 134 15 -12.61 導入前後のレスポンスタイム比較 - 最大値 API 導入前 (ms) 導入後 (ms) 差 (ms) 改善率 (%) 参照系 API1 1,005 13 -992 98.71 参照系 API2 20,000 429 -19,571 97.86 参照系 API3 1,009 13 -996 98.71 参照系 API4 9,110 151 -8,959 98.34 参照系 API5 17,000 1,158 -15,842 93.19 更新系 API1 3,316 2,411 -905 27.29 更新系 API2 4,518 2,792 -1,726 38.2 更新系 API3 317 231 -86 27.13 改善した点 参照系APIについては平均値、最大値共にレスポンスタイムが改善しました。特に最大値においては90%以上レイテンシが改善し、極端に遅いリクエストの発生を抑止できました。この点は大きな収穫だと考えております。 残課題 更新系APIについては改善傾向はみられたものの、まだまだ遅いリクエストが目立つ印象です。また、更新系API3では最大値は改善しているものの、平均値では暖機運転前よりもわずかに劣化する結果となっています。 これらの要因として、暖機運転の対象をデータ集計には影響しないAPIに限定し、かつ不要なデータ生成を極力抑制するために暖機運転のリクエスト回数を減らしたことが挙げられます。更新系APIのうち暖機運転の対象としたものは更新系API1のみで、それ以外は対象外としています。これらのAPIには共通で読まれるコードがあり、暖機運転の対象としていないAPIにも改善がみられたことから、その部分の暖機運転の効果は得られていると言えます。一方で、改善効果の低さや一部の結果で劣化していることから、十分な効果を得られているとは言えません。この点については残課題として対応方法を検討中です。 まとめ 今回はZOZOMATシステムの改善業務の一例としてJVMの暖機運転を導入し、JVM環境特有のアプリケーションの起動直後の遅延を抑制する方法についてご紹介しました。これはほんの一例でまだまだやらなければならないことが山積みの状態です。 ZOZOテクノロジーズでは、一緒にサービスをより良い方向に改善して頂ける方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
こんにちは、WEAR部運用改善チームの三浦です。私たちのチームでは、WEARの日々の運用業務を安全かつ効率的に行えるよう改善を行っています。今回はバッチの定期実行に使用しているWindowsのタスクスケジューラーの運用改善について紹介します。 背景 WEARではバッチをWindowsサーバー上で定期実行させており、定期実行するために タスクスケジューラー を使用しています。WEARではバッチ実行用のサーバー(バッチサーバー)を用意しており、バッチサーバーへのアクセス権限を持つ人がタスクスケジューラーの設定を下記のような画面から変更していました。 しかしこの運用方法では次のような課題がありました。 バッチサーバーへのアクセス権限がある人しかタスクの設定を見ることができない タスクスケジューラー上ではタスクの変更履歴や変更した経緯が残らない GUI上での手動変更では操作ミスが起こる 引継ぎが手間 実際にこれらの課題により、業務に影響を及ぼす事象も発生しました。 タスクの実行頻度を元に戻したいが、以前の設定がどうなっていたか見ることができない タスクを手動で止め、実行頻度を変更後に再開するのを忘れ、タスクが止まったままになっていた 実現したいこと 前述した課題を改善するために2つの改善を行うことにしました。 タスクの設定をGitHubで管理する 各タスクの設定をファイル出力してGitHub上で管理します。これによりバッチサーバーへのアクセス権限を問わずタスクの設定を確認できるようになります。 また、Gitでのバージョン管理によって変更履歴も確認できるようになります。 コマンドでタスクの設定を変更する コマンドでタスクの設定が書かれたファイルを読み込んで反映させます。コマンドを実行するだけでよくなり、これまで必要だった手動作業が不要になることにより、結果として誤操作のリスクを防ぐことができます。事前にコマンドもレビューしておくと更に安全に変更ができます。 これらを実現するためにschtasksというコマンドを使用します。 schtasksについて schtasksはWindows OSに標準搭載されているコマンドで、タスクスケジューラーのタスクの操作ができます。Windowsのコマンドプロンプトから実行してタスクの登録や実行頻度の変更といった編集操作が行えます。 コマンドは下記のように操作用のオプション(create, change, query, etc.)とその操作に対するオプション(対象のタスク、実行頻度、etc.)を指定して実行します。各オプションの詳細などは 公式ドキュメント を参照ください。 schtasks <操作オプション> <その他オプション>... schtasksコマンドでは現在動いているタスクの設定内容をXMLで出力したり、XMLファイルを読み込んでタスクの設定内容を変更できます。 タスクの設定をXMLで出力する schtasks /query /tn ${XML出力したいタスクの名前} /XML XMLを読み込んでタスクの設定内容を変更する schtasks /create /tn ${変更したいタスクの名前} /XML ${読み込むXMLの完全修飾パス} /F ※厳密には既存のタスクを一旦削除してXMLを元に作り直しています。 今回はこのXMLの入出力の機能を使いたいと思います。 各タスクの設定をXML出力してGitHubで管理する 各タスクの設定をXML出力し、そのXMLをGitHubで管理する手順を説明します。 Windows PowerShellを管理者実行で開く バッチサーバーにログインしWindows PowerShellを管理者実行で開きます。管理者実行ではない場合、ログインしているユーザーが作成したタスクしか操作できないので注意が必要です。 schtasksコマンドはコマンドプロンプトからでも実行できますが、今回はPowerShellのコマンドも使用したいため、Windows PowerShellを開いています。 schtask + PowerShellのコマンドでXML出力 schtasksコマンドで現在動いているタスクの一覧を取得します。そして、一覧をループして各タスクの設定をschtasksコマンドでXML出力します。 schtasks /query /nh /fo CSV | Select-String -NotMatch Microsoft | ForEach-Object { $task = ($_-split(','))[0]; $file = Join-Path '${保存先ディレクトリ}' ([regex]::Replace($task, '[\[\]"\s]', '')); schtasks /query /tn $task /XML > $file'.xml' } 最初の schtasks /query /nh /fo CSV ではschtasksの検索機能(query)で現状動いているタスクを全て取得し、CSV形式で出力します。 取得結果は下記画像のように、1行に1タスクのタスク名、次回の実行時刻、実行ステータスがCSV形式で出力されます。この時点ではWEARのバッチとは関係ないWindows OS標準のタスクが混ざってしまうので、PowerShellのSelect-Stringコマンドで除外しています。 ループの中で schtasks /query /XML を実行して、各タスクの設定をXML出力します。出力結果は下記画像のようになります。 なお、XMLのスキーマに関しては 公式ドキュメント に詳しく記載されています。 <?xml version="1.0" encoding="UTF-16"?> <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> <RegistrationInfo> <Date>2020-06-18T18:21:35</Date> <Author>***</Author> <URI>\miura-task</URI> </RegistrationInfo> <Principals> <Principal id="Author"> <UserId>***</UserId> <LogonType>Password</LogonType> </Principal> </Principals> <Settings> <DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries> <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries> <Enabled>false</Enabled> <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <IdleSettings> <StopOnIdleEnd>true</StopOnIdleEnd> <RestartOnIdle>false</RestartOnIdle> </IdleSettings> </Settings> <Triggers> <CalendarTrigger> <StartBoundary>2020-06-18T18:20:00</StartBoundary> <Repetition> <Interval>PT1M</Interval> </Repetition> <ScheduleByDay> <DaysInterval>1</DaysInterval> </ScheduleByDay> </CalendarTrigger> </Triggers> <Actions Context="Author"> <Exec> <Command>C:\Users\***\Desktop\task-test\test.vbs</Command> <Arguments>hoge piyo hello</Arguments> </Exec> </Actions> </Task> 最後に、コマンドでタスク毎に出力されたXMLファイルをGitHubに保存して完了です。 GitHubに保存したXMLでタスクの設定変更を行う ここまでの手順で、各タスクの設定をXML出力し、そのXMLをGitHubで管理できるようになりました。次に、GitHubに保存したXMLでタスクの設定変更を行う手順を説明します。 タスク設定のXMLを修正 実行頻度やコマンドライン引数などタスクの設定を変更したい場合は、GitHubに保存したタスクのXMLを任意の値に修正します。修正したXMLをPull Requestに出し、コードレビューを行います。 XMLをバッチサーバーに持ってくる バッチサーバーに変更内容を記載したXMLを git pull で取り込みます。 schtasksコマンドでXMLの内容を反映 schtasksコマンドでXMLを読み込んでタスクの設定を変更します。 schtasks /create /tn ${変更したいタスクの名前} /ru ${バッチ実行ユーザー} /rp %PASS% /XML ${読み込みたいXMLの完全修飾パス} /F WEARではバッチ実行用のアカウントでバッチを実行させているため、バッチの実行ユーザーとパスワードを明記しています。パスワードはWindowsの環境変数で登録しておいたものを参照させています。実行ユーザーを明記しない場合はこのコマンドを実行したユーザーがバッチの実行ユーザーになります。 反映内容の確認 schtasksコマンドでタスクを検索し、反映した内容を確認できます。 schtasks /query /tn ${変更したタスクの名前} /v /fo LIST 検索結果は下記のように表示されます。 まとめ タスクスケジューラーの運用改善として、GitHubでのタスクの設定管理 + schtasksコマンドでリリースする方法をご紹介しました。この運用によって事前にレビューできるようになり、より安全にリリースができるようになりました。また、コマンドで簡単に更新ができるので操作ミスがなく、作業時間の短縮にもつながりました。 今後の改善として、CI/CD周りを整備してXMLのシンタックスチェックを自動で行なったり、Pull Requestマージ後に自動でリリースして手動作業を完全になくす運用を目指しています。 さいごに 他チームとコミュニケーションを取りながら既存の運用を安全かつ効率的に行えるよう改善していくのはとてもやりがいがあります。 ZOZOテクノロジーズでは、コミュニケーションと技術力を活かしながら一緒に会社を盛り上げてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
こんにちは、ZOZOテクノロジーズ CTO室の池田( @ikenyal )です。 ZOZOテクノロジーズでは、9/8に 第一回 AWSマルチアカウント事例祭り を開催しました。 zozotech-inc.connpass.com AWSを活用する複数社が集まり、事例に関してお話しする祭典が「AWSマルチアカウント事例祭り」です。専門性の高い、ここでしか聞けないコアなトークをお届けしました。特にAWSを使用している方、AWSのマルチアカウント運用を始めたい方、AWSのマルチアカウント運用に課題を感じている方に向けたイベントです。 登壇内容 まとめ ZOZOテクノロジーズ、ウェザーニューズ、リクルートよりそれぞれ1名ずつ、合計3名が登壇しました。 AWS Configを用いたマルチアカウント・マルチリージョンでのリソース把握とコンプライアンス維持への取り組みについて (株式会社ZOZOテクノロジーズ 光野 達朗 / @kotatsu360 ) マルチアカウント運用の開始までの取り組み (株式会社ウェザーニューズ 小野 晃路) 「進化し続けるインフラ」のためのマルチアカウント管理 (株式会社リクルート 須藤 悠) 最後に ZOZOテクノロジーズでは、プロダクト開発以外にも、今回のようなイベントの開催など、外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
ZOZOテクノロジーズ ECプラットフォーム部 マイグレーションチームの會田です。 ZOZOTOWNでは 先日公開した記事 の通り、すべての検索をElasticsearchへリプレイスしました。 検索エンジンのリプレイスに伴い、VBScriptで稼働していた検索システムをJavaへリプレイスすることも併せて行われました。 本記事ではその際に得た知見を、Elasticsearch初心者の方及びElasticsearch Java APIを初めて触る方向けに紹介します。 環境(開発当時) Elasticsearchバージョン:7.3.2 Javaバージョン:8 Spring Bootバージョン:1.5.15 Mavenバージョン:2.17 準備 <dependency> <groupId> org.elasticsearch </groupId> <artifactId> elasticsearch </artifactId> <version> 7.3.2 </version> </dependency> <dependency> <groupId> org.elasticsearch.client </groupId> <artifactId> elasticsearch-rest-high-level-client </artifactId> <version> 7.3.2 </version> </dependency> 上記をpom.xmlに記載するとElasticsearchのライブラリ及び後述するRestHighLevelClientが使えるようになります。 基本的なライブラリの説明と使用方法 ※以降に出てくるカラム名及びIDなどは全て実在するものではなくダミーデータです。 RestLowLevelClient(RestClient) RestLowLevelClient はhttp経由でElasticsearchクラスタと通信できるクライアントです。Elasticsearchのすべてのバージョンと互換性があります。 参考:   Java Low Level REST Clientのリファレンス RestHighLevelClient RestHighLevelClient はそれまで利用されていたTransportClientに代わって推奨されている、RESTクライアントです。これを使用することで、Javaアプリケーションからhttpを介してElasticsearchへアクセスできます。 なお、Java High Level REST ClientはJava Low Level REST Client上で動作しています。 final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials( "user" , "password" ) ); RestHighLevelClient client = new RestHighLevelClient( RestClient.builder( new HttpHost( "localhost" , 9200 , "https" )) .setHttpClientConfigCallback(httpAsyncClientBuilder -> httpAsyncClientBuilder .setDefaultCredentialsProvider(credentialsProvider)) ); searchResponse = client.search(searchRequest, RequestOptions.DEFAULT); 参考:   Java High Level REST Clientのリファレンス SearchSourceBuilder SearchSourceBuilder は検索動作を制御する大半のオプションを制御できます。 下記ソースコードの詳細は後述のライブラリ紹介の中で別途説明します。 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(QueryBuilders.termQuery( "FashionItemId" , 1 )); searchSourceBuilder.from( 0 ); searchSourceBuilder.size( 100 ); searchSourceBuilder.sort( new FieldSortBuilder( "ItemPrice" ).order(SortOrder.ASC)); 参考:   SearchSourceBuilderリファレンス SearchRequest 下記リファレンスより引用。 SearchRequestは、ドキュメント、集約、サジェストを検索する操作に使用され、結果として得られるドキュメントのハイライト表示を要求する方法も提供します。 とありますが、APIを使ってElasticsearchへリクエストを送るための大元になるものという捉え方でよいと思います。 ※Elasticsearchでは、データをドキュメントという単位で扱います。 下記コードの詳細は後述のライブラリ紹介の中で説明します。 SearchRequest searchRequest = new SearchRequest(); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(QueryBuilders.termQuery( "FashionItemId" , 1 )); searchRequest.source(searchSourceBuilder); 参考:   SearchRequestリファレンス CountRequest CountRequest は実行したクエリに一致したドキュメント数を取得するために使用します。 CountRequest countRequest = new CountRequest(); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(QueryBuilders.matchAllQuery()); countRequest.source(searchSourceBuilder); 参考:   CountRequestリファレンス QueryBuilders 検索クエリを作成するためのユーティリティクラスです。 参考:   QueryBuilders一覧 下記に代表的な検索クエリとQueryBuildersユーティリティクラス内の対応するQueryBuilderクラスおよびヘルパーメソッドを紹介します。 matchAllQuery すべてのドキュメントを取得します。 QueryBuilders.matchAllQuery(); 参考:   matchAllQueryリファレンス termQuery 条件に合致するか。SQLの = と同義。 QueryBuilders.termQuery( "FashionItemId" , 1 ); 参考:   termQueryリファレンス   termQuery Javadoc termsQuery 条件に合致したドキュメントがあるか。SQLの IN 句と同義。 Integer[] fashionItemIds = { 1 , 2 }; QueryBuilders.termsQuery( "FashionItemId" , fashionItemIds); 参考:   termsQueryリファレンス   termsQuery Javadoc rangeQuery 条件で指定した範囲のドキュメントがあるか。SQLの >= 、 <= 、 > 、 < と同義。 // StartDatetime >= "2020-01-01 00:00:00" QueryBuilders.rangeQuery( "StartDatetime" ).gte( "2020-01-01 00:00:00" ); // EndDatetime <= "2021-01-01 00:00:00" QueryBuilders.rangeQuery( "EndDatetime" ).lte( "2021-01-01 00:00:00" ); // StartDatetime > "2020-01-01 00:00:00" QueryBuilders.rangeQuery( "StartDatetime" ).gt( "2020-01-01 00:00:00" ); // EndDatetime < "2021-01-01 00:00:00" QueryBuilders.rangeQuery( "EndDatetime" ).lt( "2021-01-01 00:00:00" ); 参考:   rangeQueryリファレンス   rangeQuery Javadoc matchQuery 全文クエリを実行するための標準クエリ。指定されたテキスト、数値、日付、またはブール値に一致するドキュメントを返します。 第一引数にフィールド名、第二引数に検索ワードを指定します。 QueryBuilders.matchQuery( "FashionItemName" , "t-shirts" ); 参考:   matchQueryリファレンス   matchQuery Javadoc multiMatchQuery 前述した matchQuery() に基づいて構築され、複数フィールドにまたがる検索クエリを可能にします。 第一引数に検索ワード、第二引数以降に検索するフィールドを指定します。 QueryBuilders.multiMatchQuery( "t-shirts" ,  "FashionItemName" , "FashionItemCategoryName" ); 参考:   multiMatchQueryリファレンス   multiMatchQuery Javadoc boolQuery 複数のクエリを組み合わせるために使用します。AND, OR, NOTを組み合わせることができます。 boolQueryには4種類あります。 クエリ 説明 must SQLの AND と同義。指定された条件によってスコアが計算されます。 filter SQLの AND と同義。mustとは異なり、スコアが計算されません。 should SQLの OR と同義。 must not SQLの NOT と同義。 Integer[] fashionItemCategoryIds = { 1 , 2 }; QueryBuilders.boolQuery() .filter(QueryBuilders.termQuery( "FashionItemId" , 1 ) .filter(QueryBuilders.termsQuery( "FashionItemCategoryId" , fashionItemCategoryIds)); FunctionScoreQueryBuilder Elasticsearchはデフォルトで score の高い順にソートされて検索結果が返ってきます。この score をチューニングすることによって目的に最適な結果を取得できるようになります。 そのために用いるのが FunctionScoreQueryBuilder です。 function_score は、 query にヒットするドキュメントに対して、 functions に複数のスコアリングのルール( function )を設定して検索結果のソートを行うことができます。 function_score について詳しく知りたい方は 公式リファレンス でご確認ください。 ※score…検索条件に適合したドキュメントを返すための基準となる値のこと。 下記は書き方の一例です。 ArrayList<FunctionScoreQueryBuilder.FilterFunctionBuilder> functionScoreArrayList = new ArrayList<>(); filterFunctionList.add( new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.termQuery( "FashionItemId" , 1 ), ScoreFunctionBuilders.fieldValueFactorFunction( "field1" ).factor(Float.valueOf( "0.0254389" )).missing( 0.2 ))); filterFunctionList.add( new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.matchAllQuery(), ScoreFunctionBuilders.fieldValueFactorFunction( "field2" ).factor(Float.valueOf( "0.0937572" )).missing( 0.2 ))); // ArrayList型をFunctionScoreQueryBuilder.FilterFunctionBuilder[]にする FunctionScoreQueryBuilder.FilterFunctionBuilder[] functions = functionScoreArrayList.toArray( new FunctionScoreQueryBuilder.FilterFunctionBuilder[functionScoreArrayList.size()]); FunctionScoreQueryBuilder functionScoreQueryBuilder = new FunctionScoreQueryBuilder(queryBuilder, functions).scoreMode(FunctionScoreQuery.ScoreMode.SUM).boostMode(CombineFunction.REPLACE); searchSourceBuilder.query(functionScoreQueryBuilder); 参考:   Function score queryリファレンス   FunctionScoreQueryBuilder Javadoc fetchSource Elasticsearchの _source はSQLの SELECT 句と同義で取得したいフィールドを指定できます。APIの場合は取得するフィールドを fetchSource を使って指定します。 取得するフィールドを絞り込むことでデータ量の削減にもつながるので、速度の改善が期待できます。 第一引数には取得するフィールド、第二引数に除外するフィールドを指定します。 searchSourceBuilder.fetchSource( new String[]{ "FashionItemId" , "ItemPrice" , "FashionItemSize" , "FashionItemLargeCategory" , "FashionItemSubCategory" }, "ExclusionFashionItemId" ); 参考:   Source filteringリファレンス FieldSortBuilder FieldSortBuilder はSQLの ORDER BY と同義で、ソートの制御に使用します。 SearchSourceBuilder オプションの1つで、 SearchSourceBuilder に対して .sort でソートを追加できます。 searchSourceBuilder.sort( new FieldSortBuilder( "ItemPrice" ).order(SortOrder.ASC)) .sort( new FieldSortBuilder( "FashionItemId" ).order(SortOrder.DESC)) .sort( new FieldSortBuilder( "StartDatetime" ).order(SortOrder.DESC)); 参考:   Sortリファレンス   FieldSortBuilderリファレンス from & size SQLの OFFSET と LIMIT と同義です。 SearchSourceBuilder のオプションの1つで、 SearchSourceBuilder に対して .from .size で指定できます。 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.from( 0 ); searchSourceBuilder.size( 100 ); 参考:   ページングリファレンス   SearchSourceBuilderリファレンス SQLとElasticsearchとJavaでそれぞれ同じクエリを表現 SQLで以下のように書くクエリをElastiseach、Javaで記述するとどのように書くことができるのか比較してみます。 StartDatetime >= ' 2019-12-06 17:33:18 ' AND ( ( FashionItemLargeCategory <> 1 AND FashionItemSubCategory NOT IN ( 10 , 20 , 30 ) AND FashionItemSize IN ( 1 , 2 ) ) OR ( ( FashionItemLargeCategory = 2 OR FashionItemSubCategory IN ( 40 , 50 , 60 ) ) AND FashionItemSize IN ( 9 , 10 ) ) ) Elasticsearchの場合。 " bool ": { " filter ": [ { " range ": { " StartDatetime ": { " from ": " 2019-12-06 17:33:18 ", " to ": null , " include_lower ": true , " include_upper ": true } } } , { " bool ": { " filter ": [ { " bool ": { " should ": [ { " bool ": { " filter ": [ { " terms ": { " FashionItemSize ": [ 1 , 2 ] } } ] , " must_not ": [ { " term ": { " FashionItemLargeCategory ": { " value ": 1 , " boost ": 1 } } } , { " terms ": { " FashionItemSubCategory ": [ 10 , 20 , 30 ] } } ] } } , { " bool ": { " filter ": [ { " terms ": { " FashionItemSize ": [ 9 , 10 ] } } ] , " should ": [ { " term ": { " FashionItemLargeCategory ": { " value ": 1 } } } , { " terms ": { " FashionItemSubCategory ": [ 40 , 50 , 60 ] } } ] } } ] } } ] } } ] } Javaの場合。 Integer[] subCategories1 = { 10 , 20 , 30 }; Integer[] itemSizes1 = { 1 , 2 }; Integer[] subCategories2 = { 40 , 50 , 60 }; Integer[] itemSizes2 = { 9 , 10 }; BoolQueryBuilder qb1 = boolQuery() .mustNot(termQuery( "FashionItemLargeCategory" , 1 )) .mustNot(termsQuery( "FashionItemSubCategory" , subCategories1)) .filter(termsQuery( "FashionItemSize" , itemSizes1)); BoolQueryBuilder qb2 = boolQuery() .should(termQuery( "FashionItemLargeCategory" , 2 )) .should(termsQuery( "FashionItemSubCategory" , subCategories2)) .filter(termsQuery( "FashionItemSize" , itemSizes2)); BoolQueryBuilder qb3 = boolQuery() .should(qb1) .should(qb2); BoolQueryBuilder qb4 = boolQuery() .filter(rangeQuery( "StartDatetime" ).from( "2019-12-06 17:33:18" ).to( null )) .filter(qb3); Javaで生成したクエリの確認方法 BoolQueryBuilder に対して toString() することでElasticsearchのクエリを取得できます。 取得したクエリを想定していたクエリと照らし合わせたり、 Kibana で実行するなどしてクエリが正しいものか確認しましょう。 System.out.println(qb4.toString()); SearchResponse SearchResponse はRestHighLevelClientに対して .search を指定することで使用できます。 SearchResponse searchResponse = null ; // searchRequestにヒットした検索結果をSearchResponseの形式で取得できます。 searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 取得したドキュメントにアクセスするにはまずレスポンスに含まれるSearchHitsを取得する必要があります。 SearchHits searchHits = searchResponse.getHits(); // 個々の検索結果はSearchHits内にネストされているので細かい検索結果を見たい場合はこのように取得します。 SearchHit[] searchHit = searchHits.getHits(); // ドキュメントの総ヒット数はTotalHitsに対して.valueで取得できます。 long totalHitsNum = searchHits.getTotalHits().value; // ドキュメントの最大スコアはSearchHitsに対して.getMaxScore()で取得できます。 float maxScore = searchHits.getMaxScore(); for (SearchHit hit : searchHit) { // 各ヒットのスコアを取得できます。 float score = hit.getScore(); // Map<String, Object>形式でドキュメントソースを取得できます。 Map<String, Object> source = hit.getSourceAsMap(); // Map形式で取得したドキュメントソース対して、キー名を指定して.get("フィールド名")で値を取得できます。 Integer fashionItemId = Integer.parseInt(sourceAsObject.get( "FashionItemId" ).toString()); } 参考:   SearchAPIリファレンス   SearchResponse Javadoc CountResponse CountResponse はRestHighLevelClientに対して .count を指定することで使用できます。 CountResponse countResponse = null ; countResponse = restHighLevelClient.count(countRequest, RequestOptions.DEFAULT); long count = countResponse.getCount(); このようにするとsearchRequestにヒットした件数をlong型で取得できます。 参考:   CountAPIリファレンス   CountResponse Javadoc 開発中苦労したこと Elasticsearchのクエリに自信が持てない中、開発すること。 少し踏み込んだ実践的なElasticsearchのJava APIの参考となる資料が少なく、壁にぶつかると中々乗り越えられない。 私はElasticsearchの経験が無かったのでこの記事を読んでいる方の状況に近かったと思います。少しでもそのような方々の参考になればと思います。 ECプラットフォーム部 マイグレーションチームでは、仲間を募集しています。ご興味のある方は、こちらからご応募ください。 tech.zozo.com
※AMP表示の場合、数式が正しく表示されません。数式を確認する場合は 通常表示版 をご覧ください ※2020年11月7日に、「Open Bandit Pipelineの使い方」の節に修正を加えました。修正では、パッケージの更新に伴って、実装例を新たなバージョンに対応させました。詳しくは対応する release note をご確認ください。今後、データセット・パッケージ・論文などの更新情報は Google Group にて随時周知する予定です。こちらも良ければフォローしてみてください。また新たに「国際会議ワークショップでの反応」という章を追記しました。 ZOZO研究所と共同研究をしている東京工業大学の 齋藤優太 です。普段は、反実仮想機械学習の理論と応用をつなぐような研究をしています。反実仮想機械学習に関しては、拙著の サーベイ記事 をご覧ください。 本記事では、機械学習に基づいて作られた 意思決定の性能 をオフライン評価するための Off-Policy Evaluation (OPE) を紹介します。またOPEを含めたバンディットにまつわる研究利用のためにZOZO研究所が公開した Open Bandit Dataset と開発したパッケージ Open Bandit Pipeline の特徴や使い方を解説します。 research.zozo.com github.com なお、本記事は こちらのプレスリリース に関連する内容になっています。また本記事の内容は、先日arXivで公開した 論文 の内容を噛み砕いて日本語で紹介したものになっています。気になる方はぜひ元論文も参照してみてください。さらに、2020年8月27日にオンラインで開催された CFML勉強会 にて本記事の内容に関する発表を行っており、その際の 発表資料 も公開しています。この発表資料は、本記事の内容を補完するような例を紹介していたりするので、是非合わせてご覧ください。 モチベーション 機械学習は予測のための技術としてもてはやされています。実際多くの論文が、解く意味があるとされているタスクにおいてより良い予測精度を達成することを目指しています。もちろん予測値をそのまま活用する場面も多くあるでしょう。しかしとりわけウェブ産業における機械学習の応用場面に目を向けてみると、 機械学習による予測値をそのまま使うのではなく、予測値に基づいて何かしらの意思決定を行っていることが多くあります 。例えば、各ユーザーとアイテムのペアごとのクリック率を予測し、その予測値に基づいてユーザーごとにどのアイテムを推薦すべきか選択する。または、商品の購入確率予測に基づいてユーザーごとにどの商品の広告を提示するか決定する、などです。これらの例では クリック率や購入確率の予測そのものよりもそれに基づいて作られる推薦や広告配信などの意思決定が重要 です。 本記事の興味は、 機械学習による予測値などに基づいて作られる意思決定policyの性能を評価すること にあります。例えばクリック率の予測値に基づいてアイテム推薦の意思決定を行う場合、オフラインでクリック率の予測精度を評価指標としていることがあるでしょう。しかし、これは非直接的な評価方法です。予測値をそのままではなく何らかの意思決定を行うために用いているのならば、 最終的に出来上がる意思決定policyの性能を直接評価するべき です。 意思決定policyの性能評価として理想的なのは、policyをサービスに実装してしまい興味があるKPIの挙動を見るという オンライン実験 でしょう。しかし、何でもかんでもオンライン実験ができるわけではありません。なぜならば、オンライン実験には大きな実装コストが伴うからです。性能の悪い意思決定policyをオンライン実験してしまったときに、実験期間においてユーザー体験を害してしまったり、収入を減らしてしまう恐れもあります。従って、 古い意思決定policyにより蓄積されたデータのみを用いて、新しい意思決定policyをオンラインに実装したときの性能を事前に見積もりたい 、というモチベーションが生まれます。 このように、新たな意思決定policyの性能を過去の蓄積データを用いて推定する問題のことを Off-Policy Evaluation (OPE) と呼びます。NetflixやSpotify、Criteoなどの研究所がこぞってトップ国際会議でOPEに関する論文を発表しており、特にテック企業から大きな注目を集めています。正確なOPEは、多くの実務的メリットをもたらします。例えば、ある旧ロジックをそれとは異なる新ロジックに変えようと思ったとき、新ロジックがもたらすKPIの値を旧ロジックが蓄積したデータを用いて見積もることができます。また、ハイパーパラメータや機械学習アルゴリズムの組み合わせを変えることによって多数生成される意思決定policyの候補のうち、どれをオンライン実験に回すべきなのかを事前に絞り込むこともできます。 以降、OPEの一般的な定式化と標準的な推定量(手法)を紹介します。 Off-Policy Evaluationの定式化 ここでは、OPEの問題を定式化します。特徴量ベクトルを 、取り得る 個の行動を 、観測される報酬を とします。 は、潜在目的変数と呼ばれる因果推論で良く用いられる記法で、例えばある行動 を選択した時の報酬は として表されます。ここで注意が必要なのが、 という行動を選択したならば、その行動に紐づく潜在目的変数 しか観測されないことです。それ以外の潜在目的変数の組 は分析者には観測されないということになります。これらの記号をイメージするための例を表1に示しました。 表1:特徴量、行動、目的変数の例 応用例 特徴量 行動 目的変数 映画推薦 年齢、性別、過去の映画視聴履歴など 映画の種類 クリック有無、視聴時間など 投薬 年齢、性別、体重、過去の検査結果など 薬の種類 生存有無、血糖値など さて、 意思決定policy とは、特徴量 から行動空間 上の確率分布への写像 として定義されます。つまり は、ある というベクトルで特徴付けられるデータに対して、 という行動を選択する確率です(つまり、 )。 は、まさに どんな状況でどの行動をとるべきかを司る意思決定policyと言えるでしょう 。 ここである意思決定policy の性能を次のように定義します。 つまり は、 という意思決定policyを導入した際の目的変数の期待値 です。例えば目的変数がクリック有無ならば、 は によってもたらされるクリック率、ということになり、意思決定policyの性能の定義として妥当でしょう。 を実システムで一定期間走らせるオンライン実験ができるならば、その期間に観測される目的変数の平均をとることで困難なく を推定できます。しかし、冒頭で説明したようにオンライン実験を行うこと自体に多くの困難が付きまといます。 よってオンライン実験の代替案として、 の性能をオフライン評価することを考えてみましょう 。オフライン評価のためのデータとして、過去の意思決定policy(旧ロジック)である によって次のような 個のデータを含む で表されるデータセットを蓄積していたとします。 なおなぜ過去のpolicyの添え字が なのかというと、このようなデータを集める際に走っていた過去のpolicyのことを論文では良く behavior policy と呼ぶからです。一方で、これからオフラインで性能を評価したい新たなpolicyのことは counterfactual policy や evaluation policy と呼びます。 は、観測される特徴量ごとに過去の意思決定policyが というふうに行動を選択し、それに紐づく潜在目的変数 が観測されることで構成されます。OPEは、過去のpolicyとは異なる新たな意思決定policy(新ロジック) の真の性能である を過去の蓄積データ を用いて精度良く推定してくれる推定量 を作ることを目的とします。なお推定量 の理論的な性能は次の mean-squared-error (MSE)で評価されます。MSEが小さい推定量ほど、正確なオフライン評価を可能にしてくれるということです。 2つ目の等式はいわゆるbias-variance分解です。ここで、 で です。MSEの意味で良い推定精度を達成するためには、biasとvarianceの両方を考慮してあげる必要があります。次章で紹介する標準的な推定量は、ざっくりとbiasを抑えるのが得意なものとvarianceを抑えるのが得意なものに分けられます。bias抑制重視の推定量なのかvariance抑制重視の推定量なのかを理解しておくことは、場面ごとにどの推定量を用いるべきなのかを考える上で役に立ちます。 標準的な推定量 ここでは、意思決定policyの性能をオフライン評価するための標準的な方法として、Direct Method (DM)・Inverse Probability Weighting (IPW)・Doubly Robust (DR)という3つの推定量を紹介します。 Direct Method (DM) DMはまず、過去に蓄積されたデータ を使って特徴量から目的変数の期待値を推定するモデル を得ます。 には、リッジ回帰やランダムフォレストなど良く知られた機械学習の手法が用いられます。次に、 を用いて次のように の性能を推定します。 つまり、DMは潜在目的変数を で置き換えてしまおうという発想だとわかります。もちろんDMの推定精度は、 の推定精度に大きく依存します。 が を良く推定できていればそれを用いて出来上がる も を良く推定します。しかし、 の推定精度が悪ければ のオフライン評価に失敗してしまいます。現実的な設定において、 を良く推定することは難しい場合が多いです。これらの点から、DMはMSEのうちbiasの部分が大きいという問題を抱えていることが知られています。一方で、varianceが問題になることはあまりありません。 Inverse Probability Weighting (IPW) 次に紹介するのは、DMとは全く異なる発想に基づくIPWという手法です。これは、過去の意思決定policyと評価したい新たな意思決定policyの行動選択確率の比で観測されている目的変数を重み付けることで、次のように を推定します。 IPWはいくつかの妥当な仮定のもとで不偏性を持ちます。すなわち、MSEのうちbiasの部分が0ということです。一方で、データが少ない場合や と が大きく異なる場合に、varianceが大きくなってしまう問題があります。つまり、DMとIPWの間にはbiasとvarianceのトレードオフがあるのです。基本的にデータが十分にあればIPWが望ましいですが、データが少ないときにはDMの方が良い推定精度を発揮することがあります。 Doubly Robust (DR) DRはここまでに紹介したDMとIPWをうまく組み合わせた手法で、次のようにして の性能を推定します。 つまり、DRはDMによる推定値をベースラインとしつつも、第二項においてIPWのような方法によりDMが使っている目的変数の期待値のモデル の推定誤差を補正していることがわかります。このような賢い方法をとることにより、DRはIPWの不偏性を保ちつつ(多くの場合)varianceを減少できることが知られています。 まとめ 本章では、DM・IPW・DRというOPEにおいて標準的な推定量を簡単に紹介しました。これらの理論的性質やより発展的な推定量に関して詳しいことが知りたい方のために、本記事の最後にさらなる学習のための参考文献をまとめています。ここで紹介した基本となる3つの推定量さえきちんと理解しておけば、他のもう少し複雑な手法もそんなに悩むことなく理解できるはずです。 Off-Policy Evaluationの研究の課題 さて実はここからが本記事の本題です。ここまでOPEのモチベーションと標準的な推定量を簡単に紹介してきました。「早速使ってみたい」と思った方もいるかもしれません。事実、研究分野としてとても盛り上がっており、ここ数年で多くの理論的知見が得られています。しかし、OPEに関する論文で行われている実験に目を向けてみると、実は次のような課題があることに気が付きます。 理論系の論文では多クラス分類問題を無理やりOPEの設定とみなすなどの 人工的で非現実的 な実験が行われている 実証系の論文では実データを使う場合があるものの公開はされておらず、他の研究者による 再現が不可能 である OPEに関する既存論文が採用している実験方法を表2にまとめました。私たちの知る限り OPEの実験を可能にする研究用公開実データは存在しません 。もしご存知の方がいらっしゃったらぜひ連絡をください。 表2: 各論文の実験方法の分類。数字は記事末の参考文献の数字と対応。 OPEの実験方法 その実験方法が取られている論文 多クラス分類問題を無理やりOPEの設定とみなすなどの人工的な方法 [1-11] 実データを使っているが非公開 [12-16] さて現実的でかつ再現可能なOPEの実験評価を行うためには、次のような条件を満たす公開実データが必要になります。 複数の意思決定policyによって収集されたデータが収録されている データ収集に用いられた意思決定policyが何なのか公表されている 以上の条件が満たされていれば、後の章で示す方法によってOPEの実験評価を行うことができます。また追加で以下のような条件も満たされていると色々な設定での信頼度の高い実験が可能になるという意味で望ましいです。 大規模である(数千万レコード以上) 多くの意思決定policyによって収集されている 複数の目的変数が収録されている もしこれらの条件を満たすデータを公開可能だという方がいらっしゃったら、論文とともに世に送り出すことによって学術的に大きな貢献になる可能性があるので、是非検討してみてください。 なお Criteoが2016年に公開している実データ は一見OPEの実験が可能に見えますが、1つの意思決定policyによるデータしか含まれない、意思決定policyが何なのか公表されていないという点からOPEの実験に使うことはできません。もう少し新しいところでは Spotifyも似たようなデータセットを公開 しているようですが、これも1つの意思決定policyによるデータしか含まれないという理由によりOPEの実験に使うことはできません。 Open Bandit Dataset の公開 OPE研究の実験における課題を解決すべく私とZOZO研究所、Yale大学の成田悠輔氏らによる研究チームは、特にOPEの研究に適した Open Bandit Dataset を公開しました。このデータセットは、株式会社ZOZOが運営する大規模ファッションECサイト ZOZOTOWN で収集されたものです。同社は、ZOZOTOWNのトップページにおいて多腕バンディットアルゴリズムを用いて意思決定policyを構成し、数あるファッションアイテムの中からユーザーごとに適したアイテムを推薦しています。バンディットアルゴリズムによるファッションアイテム推薦の例を図1に示しました。 図1:ZOZOTOWNにおけるファッションアイテムの推薦の例 私たちは2019年11月下旬に7日間にわたる実験を行い、全アイテム(All)・男性用アイテム(Men's)・女性用アイテム(Women's)に対応する3つのキャンペーンでデータを収集しました。それぞれのキャンペーンでは、トップページ来訪ユーザーに対してRandomまたはBernoulli Thompson Sampling(BernoulliTS)という2種類の意思決定policyを確率的にランダムに選択して適用しています。表3はOpen Bandit Datasetの記述統計を示しています。 表3:Open Bandit Datasetのキャンペーンとデータ収集方策ごとの記述統計 3つのキャンペーン(campaigns)と2つのbehavior policyごとに、データ数(#Data)・アイテム数(#Items: のこと)・クリック率(CTR: behavior policyの性能 )などが記述されています。 1200万レコードを含む全アイテムキャンペーンとBernoulliTSの組み合わせを筆頭に、合計2600万レコード以上の大規模データセットとなっていることがわかります。 最後に、表4にOpen Bandit Datasetのイメージを示しました。各レコードは、推薦されたアイテムのid( )、推薦位置、行動選択確率( )、 クリック有無( )、特徴量( )で構成されています。なお推薦位置は、図1で示したZOZOTOWNにおけるファッションアイテムの推薦枠の3箇所の推薦位置のどこで推薦されたかを表しています(左から順に、1・2・3の値)。 表4:Open Bandit Datasetに含まれる情報 少量のサンプルデータを こちらのディレクトリ に置いているので、気になる方はチェックしてみてください。 Open Bandit Pipeline の公開 Open Bandit Pipelineの概要 さてOpen Bandit Datasetの公開だけでもOPEの研究を特に実験面からサポートするという意味で大きな貢献だと言えます。しかし我々の研究チームはデータセットに加えて、 Open Bandit Pipeline (OBP) というバンディットアルゴリズムやOPEの性能評価のためのPythonパッケージを実装・公開しました。我々のOBPにより、研究者はOPE部分の実装に集中しつつ、再現性のある手順で他の手法との性能比較を行うことができるようになります。また機械学習エンジニアやデータサイエンティストなどの実践者は自社サービスにおいて旧ロジックが収集した過去のデータを使って簡単に新ロジックの性能を推定することが可能になります。図2にOBPの構成を記しました。 図2:Open Bandit Pipelineの構成 図に示したように、OBPは4つの主要モジュールで構成されています。 datasetモジュール には、Open Bandit Dataset用のデータ読み込み用のクラスや人工データ生成のためのクラスを実装しています。 policyモジュール には、バンディットアルゴリズムなどに基づいた意思決定policyを実装するためのインタフェースやいくつかの標準的なアルゴリズムを実装しています。 simulatorモジュール には、オフラインで意思決定policyのシミュレーションを行うための関数を提供します。 opeモジュール には、標準的なOPE推定量や新たな推定量を実装するためのインタフェースを実装しています。 OBPを活用することで、研究者は独自の意思決定policyやOPE推定量の実装に集中し、それらの性能を評価できます(図2の赤い部分)。 さらに実践者は、独自のデータセットをパイプラインと組み合わせることで、自社の設定・環境でOPEを用いた意思決定policyの性能評価を行うことができるのです。 OBPについての詳細は リポジトリ や ドキュメント をご覧ください。 github.com zr-obp.readthedocs.io Open Bandit Pipelineの使い方 ここではOBPの基本的な使い方を紹介します。説明のために、「 旧ロジックであるRandom policyが収集した過去データを用いて、新ロジックであるBernoulliTS policyの性能をオフライン評価する 」という仮想的な分析例を用います。このようなオフライン評価は、OBPを使うと次のように実装できます。 >>> from obp.dataset import OpenBanditDataset >>> from obp.policy import BernoulliTS >>> from obp.ope import OffPolicyEvaluation, Inverse Probability Weighting as IPW # (1) データの読み込みと前処理 >>> dataset = OpenBanditDataset(behavior_policy= 'random' , campaign= 'all' ) >>> bandit_feedback = dataset.obtain_batch_bandit_feedback() # (2) オフライン方策シミュレーション >>> evaluation_policy = BernoulliTS( n_actions=dataset.n_actions, len_list=dataset.len_list, is_zozotown_prior= True , campaign= "all" , random_state= 12345 ) >>> action_dist = evaluation_policy.compute_batch_action_dist( n_sim= 100000 , n_rounds=bandit_feedback[ "n_rounds" ] ) # (3) Off-Policy Evaluation >>> ope = OffPolicyEvaluation(bandit_feedback=bandit_feedback, ope_estimators=[IPW()]) >>> estimated_policy_value = ope.estimate_policy_values(action_dist=action_dist) # Randomに対するBernoulliTSの性能の改善率(相対クリック率) >>> relative_policy_value_of_bernoulli_ts = estimated_policy_value[ 'ipw' ] / bandit_feedback[ 'reward' ].mean() >>> print (relative_policy_value_of_bernoulli_ts) 1.198126 ... 以下、重要な要素について解説します。 まず最初にデータを読み込みます。ここではすでにOBPのdatasetモジュールに実装されている OpenBanditDataset クラスを用いて、Open Bandit Datasetを読み込んでいます。 # 「全アイテムキャンペーン」においてRandom policyが集めたログデータを読み込む(これらは引数に設定) >>> dataset = OpenBanditDataset(behavior_policy= 'random' , campaign= 'all' ) # 過去の意思決定policyによる蓄積データ`bandit feedback`を得る >>> bandit_feedback = dataset.obtain_batch_bandit_feedback() >>> print (bandit_feedback.keys()) dict_keys([ 'n_rounds' , 'n_actions' , 'action' , 'position' , 'reward' , 'pscore' , 'context' , 'action_context' ]) 次にオフラインで意思決定policyのシミュレーションを行います。これは教師あり機械学習のモデルを評価する際に、一度検証用データに対して予測をかけることに対応します。 # 評価対象の意思決定policyとして、BernoulliTSを用いる >>> evaluation_policy = BernoulliTS( n_actions=dataset.n_actions, len_list=dataset.len_list, is_zozotown_prior= True , # ZOZOTOWN上での挙動を再現 campaign= "all" , random_state= 12345 ) # シミュレーションにより、BernoulliTSによる行動選択確率を算出 >>> action_dist = evaluation_policy.compute_batch_action_dist( n_sim= 100000 , n_rounds=bandit_feedback[ "n_rounds" ] ) is_zozotown_prior=True とすることにより、データ収集期間にZOZOTOWNの推薦枠で実際に稼働したBernoulliTSの挙動を再現できます。なお、 is_zozotown_prior=False とすると、自ら設定した事前分布か無情報事前分布が反映されます。 最後に、BernoulliTSの性能のオフライン評価を行います。ここでは、opeモジュールに実装されているIPW推定量を使います。 # 算出されたBernoulliTSの行動選択確率に基づき、IPW推定量を用いて性能をオフライン評価する # OffPolicyEvaluationクラスには、過去の意思決定policyによる蓄積データとOPE推定量を渡す(複数設定可) >>> ope = OffPolicyEvaluation(bandit_feedback=bandit_feedback, ope_estimators=[IPW()]) >>> estimated_policy_value = ope.estimate_policy_values(action_dist=action_dist) # 設定されたOPE推定量ごとの推定値を含んだ辞書が出力される >>> print (estimated_policy_value) { 'ipw' : 0.004553 ...} # 最後に、新ロジック(BernoulliTS)の性能と旧ロジック(Random)の性能を比較する # 新ロジックの性能はOPEによる推定値を、Randomの性能はログデータの目的変数の平均で推定できる真の性能を用いる >>> relative_policy_value_of_bernoulli_ts = estimated_policy_value[ 'ipw' ] / bandit_feedback[ 'reward' ].mean() # 以上のOPEによって、BernoulliTSの性能はRandomの性能を19.81%上回ると推定された >>> print (relative_policy_value_of_bernoulli_ts) 1.198126 ... 以上の簡単な実装で、新旧の意思決定policyの性能を旧ロジックが蓄積したデータのみに基づいて比較できました。ここでの実装例だと、旧ロジックのRandomによる過去の蓄積データのみを用いて、新ロジックであるBernoulliTSの性能が旧ロジックのそれを19.81%上回るという評価を得ました。この結果に基づいて、新ロジックをいきなり実戦投入したり、大きな被害が出ないと踏んだ上で安心してオンライン実験に進んだりできるのです。 なおここで用いた簡易例はquickstart exampleとして、 notebook で動かせるようになっているので確認してみてください。その他にも、人工データなどを用いた豊富な 活用例 も提供しており、すぐに手を動かしながらOBPの使い方を把握することが可能になっています。 是非、 git clone https://github.com/st-tech/zr-obp してから遊んでいただけたらと思います。 DatasetとPipelineを活用したOPEの実験評価 最後に、Open Bandit DatasetとOpen Bandit Pipelineの組み合わせによって、標準的なOPE推定量の性能評価(意思決定policyの性能のオフライン評価の正確さの評価)を行ってみます。このOPE推定量の実験評価が、データ公開とOBPの実装によって我々が可能にしたかったことになります。 Open Bandit Datasetを用いたOPEの評価方法 ここではOpen Bandit Datasetを用いてOPE推定量の性能評価を行うための手順を紹介します。準備のため、意思決定policyA(例えばRandom)によって収集されたデータを 、意思決定policyB(例えばBernoulliTS)によって収集されたデータを と表します。また意思決定policyAを 、意思決定policyBを としておきます。 次のような手順をとることで、OPE推定量 の正確さを評価することが可能です。 OPEの評価のための手順 と の性能をそれぞれもう一方のpolicyが収集したデータをもとに推定する。すなわち、 の真の性能 を で推定し、 の真の性能 を で推定する。 と の性能をそれぞれのpolicy自身が収集したデータを用いて推定し、これらを と の真の性能とみなす。すなわち、 を で、 を で代替する。これは、オンライン実験を行って と の性能を推定していることに相当するため、真の性能とみなすことに妥当性がある。 OPEの評価指標を用いて の正確さを評価する。ここでは次の relative estimation error をOPEの実験的な評価指標として用いる。これが小さい推定量ほど、意思決定policyの性能の正確なオフライン評価ができているということになる。なお、AとBを入れ替えると の性能を推定する場合のrelative estimation errorの定義式になる。 少々複雑な手順に見えますが、 と を交互に新旧ロジックとみなしてOPEを行うようなシミュレーションを行うことで推定量 の正確さを評価しています。上記のようなOPEの評価を行うためには、 複数の意思決定policyによって収集されたデータ が含まれており、かつ や をログデータ上で動作させる必要があるため それらの意思決定policyが何なのか公表されている ことが必要だということがわかります。我々のデータセットと実装は、このような現実的で再現可能なOPEの評価を可能にするのです。 DM・IPW・DRの性能比較 さて上述のOPE推定量の評価手順を用いて、標準的なOPE推定量であるDM・IPW・DRの推定性能評価を行ってみました。実験設定は以下の通りです。 Randomをbehavior policy(旧ロジック)、BernoulliTSをcounterfactual policy(新ロジック)とみなして、BernoulliTSの性能を推定した際のオフライン評価の正確さを評価 DMやDRに必要な機械学習モデル には、ロジスティック回帰を使用 10個の異なるブートストラップサンプルによる結果を用いて、relative estimation errorの信頼区間を推定 得られた実験結果を表5に示しました。 表5:キャンペーンごとのOPE推定量の推定精度 (relative estimation error) 全アイテム (All) 男性向けアイテム (Men's) 女性向けアイテム (Women's) DM 0.2319 0.2150 0.2261 IPW 0.1147 0.1347 0.0788 DR 0.1181 0.1200 0.0786 ここではシンプルなロジスティック回帰を使ったこともあるのか、DMは一貫して悪い推定精度を示しました。一方で、IPWとDRはほとんど大きな差が見られませんでした。よってより実装が簡単なIPWでオフライン評価をするのが実務的には良さそうです。しかし、DRもDMと同様に機械学習モデルを用いているため、これを違うモデルに変更するとDRの推定が良くなる可能性もまだ残されています。サンプリングなどによってデータ数を意図的に変えてみたりbehavior policyとcounterfactual policyを入れ替えてみると異なる結果が得られるかもしれません。是非色々試してみてください。 ここで行ったOPE推定量の性能評価に用いた実装は、 こちらのリポジトリ に置いてあります。READMEには、relative estimation errorの信頼区間も含めた詳細な結果を記載しています。 国際会議ワークショップでの反応 本記事で紹介したデータ公開とパッケージ実装を含む研究プロジェクトはこれまでに、 ICML2020 RealML や RecSys2020 REVEAL など、トップ国際会議の関連ワークショップで口頭発表を行ってきました。 特に、2020年9月26日にオンライン開催されたRecSys2020併設のREVEAL workshopでは、AmazonやGoogle・Criteo・Microsoftなどで研究をされている界隈で有名な方々と並んで約200人の聴衆の前で30分間本研究プロジェクトの内容を口頭発表する機会に恵まれました。反応は以下の通り大変好評で、発表が終わった後Zoomのチャットに好意的なコメントが並んでいた時は、とても嬉しかったです。本研究の方向性が間違っていないことを専門家からの反応によって確かめることができたり、プロジェクトの内容を広く周知できたとても良い研究発表機会になりました。 昨日、RecSys恒例のREVEAL WSにて、約200人の前で30分 live talkしました。発表自体が好評だっただけでなく、発表後に米トップ大の教授から直々に誘いを受けるなど反響がありました。オンライン学会でも入念に準備すれば十分チャンスや繋がりを掴めるようなので、あなどらず是非本気出していきましょう pic.twitter.com/MH869hxJvg — usaito (@usait0) September 27, 2020 twitter.com また学会後に執筆されたいくつかのブログ記事にて、Open Bandit DatasetやPipelineについて触れていただきました。界隈で大きな注目を浴びることに成功したので、これからもデータの拡大やパッケージの機能追加、及び国際会議での周知などに一層力を入れ、OPEの分野では知らぬものがいないオープンソースに成長させていくつもりです。 RecSys 2020: Highlights of a Special Conference Similar to last year, the REVEAL workshop attracted the most attention with more than 900 participants being around. You should definitely check it out. There was also the release of Open Bandit Pipeline – a python library for bandit algorithms and off-policy evaluation that was considered as one of the highlights of this workshop. 推薦システムの国際学会RecSys2020の参加録 REVEAL 2020: Bandit and Reinforcement Learning from User Interactions バンディットや強化学習に関するワークショップになります。NetflixやMicrosoftなどの企業から発表が多いです。日本からは、@usaitoさんのバンディットアルゴリズム用の大規模データセットの公開に関する発表があり、大きな注目を集めていました。 さいごに 本記事では、OPEと呼ばれる機械学習による予測ではなく意思決定policyの性能を直接評価する方法を紹介しました。またOPEの研究を 現実的で再現可能 なものにするために我々が公開した大規模データセットと研究用パッケージについて紹介しました。今後もZOZOTOWNでの追加実装をもとにデータセットを増強したり、継続してパッケージのメンテナンスや機能追加を行っていくつもりですので、ぜひチェックしてみてください。 なお本記事の内容や元論文・データセット・パッケージに関しての質問、間違いの指摘、改善の提案などがありましたらメール (ZOZO研究所: zozo-research@zozo.com, 本記事の著者: saito.y.bj@m.titech.ac.jp)やTwitter ( @usait0 )でご連絡ください。 また、ZOZOテクノロジーズでは一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com さらなる学習のための資料 ここではOPEのことをもっと知りたい方のために、私自身がこれまで独学でOPEを勉強してきた際に活用してきた有用資料や論文をいくつか紹介します。 Doubly Robust Off-policy Evaluation with Shrinkage ICML’19 Workshop on Real-world Sequential Decision MakingでのMicrosoft ResearchのMiro Dudík氏による招待講演。前半のOPEの導入が秀逸。OPEの定式化やDM・IPW・DRのなんとなくのイメージが沸いた方が視聴すると良い整理になるはず。 バンディットと因果推論 CyberAgent、AI Lab 安井 翔太氏のCFML勉強会での発表資料。本記事では紹介を省いたOPEにおける困難や必要な仮定などが説明されており、眺めてみると良い勉強になるだろう。 Contextual Bandits in Recommendation 元Spotifyで現Netflix ResearchのJames Mclnerney氏によるチュートリアル資料。推薦における利用の観点からcontextual bandit (意思決定policyを作る方法の一つ)から、そのオフライン評価に至までかなり詳細に解説されている資料。特に推薦システムにおけるOPEの利用を考えている方は眺めておいて損はないだろう。 Off-policy Evaluation and Learning Alekh Agarwal、Sham Kakade両氏によるワシントン大学での講義資料。本記事で紹介したDM・IPW・DRの理論的な基礎をきっちり把握したい方は目を通してみると良いだろう。 Doubly Robust Policy Evaluation and Optimization 本記事で紹介した標準的な推定量であるDM・IPW・DRの理論的背景がまとまっているので、こちらもこれらの推定量の理論的な基礎を把握したい方は一読してみると良いだろう。 Intrinsically Efficient, Stable, and Bounded Off-Policy Evaluation for Reinforcement Learning Nathan Kallus・Masatoshi Uehara両氏によるNeurIPS'19論文。1章と2章においていくつかのOPE推定量の理論性質の比較がなされており、推定量のいろんな理論性質を知りたい方は楽しめると思う。 その他、もう少し発展的な推定量について知りたい方は[2,3,7,10,13,14]を企業による活用事例を知りたい方は応用系学会に出ている[12,15,16]あたりを読んでみると良いでしょう。 参考文献 Miroslav Dudík, Dumitru Erhan, John Langford, and Lihong Li. Doubly Robust Policy Evaluation and Optimization . Statistical Science , 29:485–511, 2014. Yu-Xiang Wang, Alekh Agarwal, and Miroslav Dudik. Optimal and Adaptive Off-policy Evaluation in Contextual Bandits . In Proceedings of the 34th International Conference on Machine Learning , 3589–3597. 2017. Mehrdad Farajtabar, Yinlam Chow, and Mohammad Ghavamzadeh. More Robust Doubly Robust Off-policy Evaluation . In Proceedings of the 35th International Conference on Machine Learning , 1447–1456. 2018. Nathan Kallus and Masatoshi Uehara. Intrinsically Efficient, Stable, and Bounded Off-Policy Evaluation for Reinforcement Learning . In Advances in Neural Information Processing Systems . 2019. Nikos Vlassis, Aurelien Bibaut, Maria Dimakopoulou, and Tony Jebara. On the design of estimators for bandit off-policy evaluation . In International Conference on Machine Learning , pages 6468–6476, 2019. Cameron Voloshin, Hoang M Le, Nan Jiang, and Yisong Yue. Empirical study of off-policy policy evaluation for reinforcement learning . arXiv preprint arXiv:1911.06854 , 2019. Adith Swaminathan, Akshay Krishnamurthy, Alekh Agarwal, Miro Dudik, John Langford, Damien Jose, and Imed Zitouni. Off-policy Evaluation for Slate Recommendation . In Advances in Neural Information Processing Systems , pages 3635–3645, 2017. Noveen Sachdeva, Yi Su, and Thorsten Joachims. Off-Policy Learning with Deficient Support . In ACM SIGKDD International Conference on Knowledge Discovery and Data Mining (KDD) , 2020. Yi Su Pavithra Srinath Akshay Krishnamurthy. Adaptive Estimator Selection for Off-Policy Evaluation . In International Conference on Machine Learning , 2020. Aman Agarwal, Soumya Basu, Tobias Schnabel, and Thorsten Joachims. Effective Evaluation Using Logged Bandit Feedback from Multiple Loggers . In ACM SIGKDD International Conference on Knowledge Discovery and Data Mining (KDD) , 2017. Masahiro Kato, Masatoshi Uehara, and Shota Yasui. Off-Policy Evaluation and Learning for External Validity under a Covariate Shift . arXiv preprint arXiv:2002.11642 , 2020. James McInerney, Brian Brost, Praveen Chandar, Rishabh Mehrotra, and Ben Carterett, Counterfactual Evaluation of Slate Recommendations with Sequential Reward Interactions . arXiv preprint arXiv:2007.12986 , 2020. Yusuke Narita, Shota Yasui, and Kohei Yata. Off-policy Bandit and Reinforcement Learning . arXiv preprint arXiv:2002.08536 , 2020. Yusuke Narita, Shota Yasui, and Kohei Yata. Efficient counterfactual learning from bandit feedback . In Proceedings of the AAAI Conference on Artificial Intelligence , volume 33, pages 4634–4641, 2019. Alexandre Gilotte, Clément Calauzènes, Thomas Nedelec, Alexandre Abraham, and Simon Dollé. Offline a/b testing for recommender systems . In Proceedings of the Eleventh ACM International Conference on Web Search and Data Mining , pages 198–206, 2018. Alois Gruson, Praveen Chandar, Christophe Charbuillet, James McInerney, Samantha Hansen, Damien Tardieu, and Ben Carterette. Offline evaluation to make decisions about playlist recommendation algorithms . In Proceedings of the Twelfth ACM International Conference on Web Search and Data Mining , pages 420–428, 2019. Damien Lefortier, Adith Swaminathan, Xiaotao Gu, Thorsten Joachims, and Maarten de Rijke. Large-scale validation of counterfactual learning methods: A test-bed . arXiv preprint arXiv:1612.00367 , 2016. Weihua Hu, Matthias Fey, Marinka Zitnik, Yuxiao Dong, Hongyu Ren, Bowen Liu, Michele Catasta, and Jure Leskovec. Open Graph Benchmark: Datasets for Machine Learning on Graphs . arXiv preprint arXiv:2005.00687 , 2020.
こんにちは、ZOZOテクノロジーズ CTO室の池田( @ikenyal )です。 ZOZOテクノロジーズでは、8/27に ZOZO Technologies Meetup~マーケティング基盤とそれを支えるデータ基盤~ を開催しました。 zozotech-inc.connpass.com ZOZOテクノロジーズのマーケティング基盤とデータ基盤の開発に興味のある方を対象としたイベントで、ZOZOテクノロジーズが扱う大規模データを支える基盤の開発・活用方法についてのご紹介や、GCPやDataflowを用いてどのように大規模データを扱い、運用しているのかを現場のエンジニア目線で詳しくお伝えしました。 登壇内容 まとめ 弊社の社員4名が登壇しました。 ZOZOTOWNにおけるマーケティングオートメーションの概要 (川名 智久 / @emsgraphixx ) LINEセグメント配信基盤について (長澤 修平 / @snagasawa_ ) リアルタイムマーケティング基盤の紹介とリプレイス計画 (田島 克哉 / @katsuyan121 ) ZOZOTOWNを支えるリアルタイムデータ基盤 (谷口 恵輔 / @csk_pos ) 最後に ZOZOテクノロジーズでは、プロダクト開発以外にも、今回のようなイベントの開催など、外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
こんにちは。ZOZOテクノロジーズの廣瀬です。 私は2020年8月に、Microsoft MVPをData Platformカテゴリにおいて受賞しました。本カテゴリにおける現在の日本の受賞者は私で10人目となります。本記事ではMicrosoft MVPの概要と、受賞するためにどのようなことを意識して、どのような行動をとっていたのかについてご紹介します。 Microsoft MVPとは Microsoft MVPとは、Microsoftに関連する技術コミュニティにおいて大きく貢献した人物を表彰する制度で、1年間有効なアワードです。再受賞のためには、毎年審査が必要になります。Microsoft MVP for xxxというように、特定のカテゴリごとに受賞者が決まり、現在では11カテゴリにおいて約2900名のMVPが世界中に存在しています。受賞者は MVP一覧ページ にプロフィールが掲載されます。 mvp.microsoft.com 公式ページ によると、Microsoft MVPの人物像について以下のような記述があります。 熱意をもって自身の知識をコミュニティへ共有するテクノロジーの専門家 Microsoftの製品とサービスに関する極めて深い知識がある 優れた技術力に加えて、常に進んで他者を助ける また、MVPを受賞すると以下のような特典を受けることができます。 Microsoft製品への早期アクセス 製品チームと直接やり取りできるチャネルへの参加 本社が主催する年次イベントGlobal MVP Summitへの招待 MVPになるには、以下の2ステップが必要となります。 Microsoftの常勤従業員(FTE)またはMicrosoft MVPから推薦状を提出してもらう 自分自身で過去1年間のコミュニティ活動実績をアピールする MVP受賞者には、記念品が贈られます。 大きな箱が届きます。外見もかっこいいです。 トロフィーや盾などが入っています。早速部屋に飾りました。 次に、Microsoft MVPを受賞するためにどのようなことを意識して、どのような行動をとっていたのかについてご紹介します。 受賞のためにしたこと 1. Microsoft MVPの受賞を目標に設定する 私がMicrosoft MVPの受賞を目指そうと決めたのは、2年半ほど前のことです。その当時はまだコミュティ活動を活発に行えているとは言えない状況でした。そこで、受賞を目指すために必要な行動を整理し、少しずつアウトプットの質と量とチャネルを増やしていきました。その結果、受賞した活動期間(2019/07-2020/06)においては、以下のような活動実績となりました。 会社のテックブログ 4本 Qiitaの記事 18本 カンファレンスでの登壇 1回 コミュニティの勉強会での登壇 10回 社内勉強会を約20回開催 GitHubでの情報取得クエリ群 の公開 Microsoftの技術フォーラムでの回答活動 でのTOP5%の回答者 Twitter での情報発信 2. 継続的にコミュニティへ参加する 私は Japan SQL Server User Group という技術コミュニティに所属し、勉強会でほぼ毎月登壇しています。コミュニティに自身の学びを還元することで、他の方の学びにつながることもあれば、逆に参加者から有益なフィードバックをいただけることもあります。 登壇すると直接フィードバックをいただけるので、次回の登壇のモチベーションにつながります。このように、コミュニティに参加することでアウトプットのモチベーションを維持し続けることができたと思います。 3. コミュニティをアップデートするために自分ができることを考える SQL Serverに関する技術記事を読んだり勉強会に参加していると、以下のようなことを思いつくことがあります。 「こういうことが知りたいんだけどちょうどいい記事が無いな」 例えば、「クエリストアを使わず、サーバー負荷を上げずにCPUボトルネックなクエリを全体最適の観点で特定する方法は無いだろうか」という疑問から、 Qiitaの記事 にまとめました。 「この概念、自分だったらもっと分かりやすく説明できそう」 例えば、SQL Serverのロックについて分かりやすく説明することを目指して書いた Qiitaの記事 のPV数は約7万で、自分が書いた記事の中では最もPV数が多い記事の1つです。また、「SQL Server ロック」でGoogle検索すると1番目にヒットするため、SQL Serverのロックという概念の理解の助けになれていると感じています。 「こういう話をしている人がいないけど自分がやったらおもしろそう」 大規模な自社サービスでSQL Serverを運用しているという経験はあまり得られる機会も少なく、発生した障害の詳細や解決方法について話したらおもしろいのではと思い、 テックブログ にまとめました。このように、どうやったら自分が所属している技術コミュニティをアップデートできるか、ということを考え、思いついたことは実践してみる、ということを継続するようにしていました。 このように、「自分が参加しているコミュニティにまだ無いと思われる知見」を持ち込んだり、「より分かりやすくしてあげることでみんなの役に立ちそうなこと」をアウトプットすることで、コミュニティのアップデートに貢献できると思い、行動していました。 4. 社内向けの資料作成時も、公開可能な情報はQiitaやTwitterで発信 私のアウトプットは、業務の中で生じた疑問や課題についての検証結果をまとめることが多いです。業務に紐づいているため、社内にしか公開できない情報もありますが、その中で公開可能な検証結果についてはQiitaやTwitterで発信するように意識していました。 例えば、「変更の追跡」という機能を導入する際は、以下のようにアウトプット先を切り替えていました。 「どの環境のどのDBのどのテーブルに導入するか」といった情報は外部公開できないため、社内Wikiに記載 設定をプロダクション環境へ反映する際にハマった点があり、純粋に技術的な内容であったため Qiita で公開し、URLを社内Wikiに記載 このように、「アウトプットのためにネタを考える」のではなく、「業務の中から外部へアウトプット可能な箇所を切り出す」ことで、ネタを考える時間も、別途資料を作成する時間もできる限り省略するように意識していました。 「何かアウトプットしなくては」という意識だとハードルが高いと感じてしまいますが、普段の業務の中で自然と外部へのアウトプットが生まれるよう意識していたことで、アウトプットのハードルを低く保ち続けられたと思います。 Microsoft MVPの審査の際に提出した文章 他のMVP受賞者の方 を参考に、私もMVPの審査の際に提出した文書を公開したいと思います。 なぜMVPになりたいのか? コミュニティへの良い影響力を強めていきたいからです。 コミュニティのことを強く意識するきっかけとなったのは、2019年11月にPASS SUMMITへ参加したことです。学んだことをみんなが積極的にコミュニティへ還元することでPASSコミュニティが強くなり、それによってスピーカー自身もコミュニティから有益なフィードバックをもらえるという正のサイクルがうまく回っていると感じました。この経験から、私自身も学んだことをコミュニティに還元していくことで日本のSQL Serverコミュニティをより強く、より良いものにしていきたいと思うようになりました。MVPに認定いただけることで、今後のコミュニティへの貢献活動のさらなるモチベーション向上につながると思っています。 この1年間のコミュニティへの貢献の中で最もインパクトが大きいものは? 勤務している会社が展開している、日本最大級のファッションECサイト「ZOZOTOWN」で生じたSQL Serverに関する障害と、それを調査して原因を解消させた話をコミュニティに共有したことです。 db tech showcase2019 での登壇と、審査期間ではないですが会社の テックブログ (1万PV、120はてブ、Facebook250シェア)と、ユーザーコミュニティの勉強会での登壇を通して、多くの人々に大規模なSQL Serverを使ったシステムにおける障害事例や調査ノウハウを共有しました。登壇内容は、他社の教育資料として活用されているそうです。今までのコミュニティに持ち込まれていなかった情報を共有する、ということを日ごろから意識しており、ZOZOTOWNはSQL Serverを使ったサービスとしては日本最大級であり、そこで生じた生々しい障害と調査の過程を公開するということは今までのコミュニティではあまりされていなかったと思います。他にも、ZOZOTOWNを運用していく中で生じた課題と解決策についてブログで発信しております。例えば、 こちらの記事 や、 こちらの記事 などです。 今後コミュニティに対してどういった影響を与えていきたいか? 私がSQL Serverコミュニティに足りないと感じている点は、実務に直結する有益なノウハウの公開が不足している点です。 この問題点を踏まえて、たとえば このブログ記事 のように、単純に拡張イベントの使い方ではなく、「特定の目的をもった拡張イベントの設定方法と、解析方法」というように、直接実務で使えるようなノウハウをひとまとめの記事にして共有することを意識しています。 他の例として、 こちらの記事 と GitHub で公開しているクエリのように、ZOZOTOWNの運用経験から得られた情報後追いのためのDMVのダンプクエリの仕組みを整備して公開しております。 他の例として、 こちらのブログ のように、メンテ無しでプロダクション環境でのデプロイを成功させるためのノウハウについて、自社の高トラフィックなWebサイトでも成功例のある方法を紹介しています。 今年も、同様の貢献をコミュニティに対して行っていきます。合わせて、自身が最も得意としているPerformance Tuningについて、ウェビナーの講師として解説動画を収録して公開する予定です。日本のSQL Serverのチューニングレベルを引き上げていくために、ビデオの公開やユーザ勉強会でチューニングまわりの情報提供なども実施していきます。 コミュニティをより良くしていくために現在行っている活動は? ユーザーコミュニティの勉強会やDBカンファレンスで定期的に登壇したり、技術的な記事を定期的に執筆したりすることで、会社の業務を通して得たSQL Serverの知見をコミュニティに還元しています。 Microsoft MVP日本事務局の方からのコメント 今回のブログ執筆にあたり、Microsoft MVP日本事務局の森口様からコメントをいただきました。森口様、素敵なコメントをありがとうございました! Microsoft森口様より 国内でも有数のアクセスを集めるECサイト運営に関わるお仕事を通じて得たユニークな知見を登壇およびブログ寄稿などコミュニティ活動を通じて積極的に発信いただき、多くのデータベースエンジニアの学びに寄与されたご貢献により、この度廣瀬さんにData PlatformカテゴリでMicrosoft MVPアワードを授与いたしました。 業務で学んだ実践的なSQL Serverの知識を、社内で共有するだけにとどまらず社外のコミュニティメンバーの技術の学習のためにご尽力いただけますと、今後さらにSQL Serverを活用したい方への貴重な情報源となります。ひいては日本全体のSQL Serverコミュニティの力となる、大変ありがたい活動です。今後はぜひMVPとしてさらにコミュニティ活動をお楽しみいただくとともに、マイクロソフトとのコラボレーションにより製品やコミュニティをより良くするためにお力をお借りできましたら幸いです。 まとめ Microsoft MVPの受賞できたことはとても喜ばしく、アウトプットを継続していくためのモチベーションにもつながりました。これからもコミュニティへ自分が良い影響を与えていけるように考えながら活動していきたいと思います。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com