TECH PLAY

株式会社エブリー

株式会社エブリー の技術ブログ

411

はじめに こんにちは。DELISH KITCHEN開発部でデータサイエンティストをやっている山西です。 今回は、 DELISH KITCHENへバンディットアルゴリズムを採用した経緯 バンディットサーバーおよびそのAWSインフラ構築 をテーマに紹介いたします。 経緯 現在DELISH KITCHENでは、サービスをより良くするために、デザインの改善施策を継続的に行っています。 その手段として、これまでは主にA/Bテストによる効果検証を行ってきました。 参考記事 tech.every.tv tech.every.tv A/Bテストにより、複数のデザイン案の良し悪しを統計的に解釈し、”良い”デザインを見極めたうえでユーザーに展開することが出来ます。 適切なA/Bテストの設計によってその恩恵を最大限に享受できる一方、トレードオフとして以下のようなデメリットにも直面することとなります。 手動操作が多く、”良い”デザインを採用するプロセスを自動化出来ない 例 ユーザーに展開する割合の計算(サンプルサイズの決定) サービス内部へのデザイン実装の手間 「良くないデザイン案」を一定期間露出してしまうリスクがある 複数のデザイン案を同時にテストしようとすると、サンプルサイズが足りなくなる そこで、「複数のデザイン案の中から”良い”ものを見つけ出し、自動で表示する」という場面で採用できないかと試したのがバンディットアルゴリズムになります。 バンディットアルゴリズムとは バンディットアルゴリズムは、探索と活用のジレンマに陥る多腕バンディット問題を効率的に解くために考案されているアルゴリズムです。 まず以下に、これらの用語について解説します。 多腕バンディット問題 以下のような問題設定に当てはまるものを多腕バンディット問題と定義します。 「複数の選択肢の中から一つを選択する」試行を逐次的に行い続ける 選択を繰り返した結果、得られる総報酬を最大化したい 選択試行を繰り返すためのリソースには制約がある(時間的制約、試行回数の上限など) どの選択肢が良いか(=多くの報酬を得られるか)は未知である その具体例として、「当たりの確率が異なる複数のスロットマシンのアーム(腕)を複数回引く中で、最大の報酬を得られるように模索する」問題が挙げられます。 図1 バンディットアルゴリズム スロットマシンの例 ※ スロットマシンがプレイヤーからどんどんお金を奪い取っていく様を盗賊(bandit)に見立てたのが語源となり、多腕バンディット問題と呼ばれるらしいです。 Webサービスの運用においても、「バナーや広告等の表示コンテンツをCTR、 CVRが高くなるように最適化したい」という問題設定等が、多腕バンディット問題に当てはまります。 探索と活用のジレンマ 多腕バンディット問題では「どの選択肢が”良い”(=より多くの報酬が期待できる)のかは事前にわからない」ため、それを試しながら確かめる必要があります。 しかしここで、探索と活用のジレンマに陥ることとなります。 複数の選択肢を代わる代わる試すような”探索”ばかり行っていると、有用な選択肢を発見するまでに機会損失を被るリスクがある ex. 新しいスロットマシンを試し続ける、その中には”悪い”スロットも含まれる とりあえず、とある選択肢を”良い”とみなして活用し続けると、もっと”良い”選択肢があった場合、それを発見できないという機会損失が生じる ex. 現時点で一番”良い”スロットマシンを引き続けることになるが、もしかしたら他にもっと良い選択肢があるのを見過ごしているかもしれない バンディットアルゴリズム バンディットアルゴリズムは、このような多腕バンディット問題、およびそれに起因する探索と活用のジレンマを解決するためのアルゴリズムの総称です。 これらのアルゴリズムは主に統計的なアプローチを用いて、探索と活用のバランスをうまく取り扱いつつ、総報酬の最適化を図るように設計されています。 代表的なバンディットアルゴリズム ε-greedy : シンプルに実装可能なアルゴリズム。小さい確率εをパラメータとし、確率εで選択肢をランダムに選ぶ(探索フェーズ)。残りの確率(1-ε)で、その時点で得られた報酬の期待値が最大となる選択肢を選ぶ(活用フェーズ)。 Thompson sampling : ベイズによるアプローチ。”良い”選択肢を選ぶために、各選択肢の事後分布(これまでの選択回数と報酬が得られた回数を元に更新)を使用する。 UCB(Upper Confidence Bound) : 試行回数が少ない選択肢の「不確かさ≒”良い選択肢かもしれないというポテンシャル”」も考慮しつつ、”良い”選択肢を探索する。そのために、各選択肢の報酬の期待値の信頼区間の上限(UCB値)を利用する。 本記事では詳細な説明を割愛しますが、詳しく知りたい方は以下の書籍を読んでみてください。 www.oreilly.co.jp A/Bテストとの違い A/Bテスト、バンディットアルゴリズムは最終的なゴールとして、「複数の選択肢の中から、最も高い報酬が期待できるものを選びたい」という同じ目標を掲げているように見えます。 しかし、主眼の置き方はそれぞれ、 A/Bテスト : 検証としての振り返りに重きを置く バンディットアルゴリズム : 報酬の最大化そのものを目的とする という差異があり、それぞれに得意、不得意があるとも解釈できます。 以下、私たちなりの解釈にはなりますが、選択肢を選定するプロセスにおける違いをまとめてみたものになります。 選択肢選定プロセスの比較 バンディットアルゴリズム A/Bテスト 目的 選定の最適化による報酬の最大化 どの選択肢が優れているかの判断 自動化 される されない※1 属人性 無し 有り 効果検証 やりにくい やりやすい 環境に対する感度※2 高い 低い ※1 施策として実施した後に、結果を解釈してどの選択肢を採用するか意思決定することになる ※2 流行の変化によって、”良い”選択肢が変化した場合、バンディットアルゴリズムはその変化を追従できる可能性があるが、A/Bテストはやり直しによってしか対処できない DELISH KITCHENでは、「複数のデザイン案から”良い”ものを選定する」A/Bテスト以外の選択肢としてバンディットアルゴリズムならではの強みを活かせるのではないかと考え、採用に至りました。 DELISH KITCHENへのバンディットアルゴリズムの適用 これから、DELISH KITCHENにおけるバンディットアルゴリズムの適用事例を簡単にですが紹介します。 ボタン文言の出し分け事例 DELISH KITCHEN(モバイルブラウザ版)のトップ画面には、アプリ版へのダウンロードを促すボタンが設定されています(下図参照)。 ここを、 選択肢: ボタンに表示させる文言 (5パターンを準備) 報酬: ボタンクリック という多腕バンディット問題に落とし込みます。 そして、バンディットアルゴリズムによって、ここの文言がクリック率の高いものに寄っていくか実験してみました。 ※ Thompson samplingというアルゴリズムを利用 図2 ボタンとその文言の配信面 delishkitchen.tv 結果 実際に配信してみた結果、最初のうちは複数の選択肢を試し、一定期間経過後、一つの文言(赤色部分)を集中的に表示する様子を観察することが出来ました。 図3 表示文言の時系列変化 ※ 縦軸のビン(棒)は、その時々のボタン100回表示の中で、5パターンの文言(凡例の0~4)のどれが何回表示されたかを色分けしています。 そして、横軸は時系列を表し、右に進むほど時間が経過したことを意味します。 全選択肢(文言)の中で、1番の文言(グラフ上の赤色部分)のクリック率が最も高い結果となりました。 このクリック率は、元々ページ上に表示していた文言(グラフ上の0番=青色部分)よりも1.4倍ほど高いものとなります。 よって、アルゴリズムの探索によって、クリック率観点で”良い”文言を見つけ出したことになります。 バンディットサーバーをAWSに構築 ここからは、バンディットアルゴリズムの実装について紹介します。 Pythonでバンディットアルゴリズムを実装 FastAPIにて↑をサーバー化 AWS上にインフラ構築(FastAPIをコンテナ化してECRにアップロード&ECS-Fargateとして立てる) という流れを取りました。 そして、 既存DELISH KITCHENバックエンドサーバーがこのバンディットサーバーに対してAPIリクエストを送信 「バンディットアルゴリズムの判断結果として、どの選択肢を表示すれば良いか」の情報をレスポンスで返す Databricks(データ集計基盤)を経由して、配信に向けたマスタ情報や、バンディットアルゴリズムが必要とする報酬情報等のバッチ更新を実施 という構成にしています。 以下がその構成図になります。 図4 バンディットインフラ構成図 FastAPIコードの例 以下、一部抜粋にはなりますが、バンディットサーバーのコードの実装の雰囲気を紹介します。 内部実装ではMVCアーキテクチャを意識しており、 service層 バンディットアルゴリズムのロジックそのもの APIとして実現したい操作(コード部分) model層 必要なデータソース(上記構成図におけるElasticache-redis)を操作する部分 といった具合に各々の責務を分離し、見通しよく開発できるようにしています。 service層のget_arm(APIとしてどの選択肢をリクエスト元に返すかを操作する部分)の実装↓ from typing import List, Dict, Tuple from model.arm import select_arm_score as redis_select_arm_score from model.environment import select_environment_all_arms as redis_select_environment_all_arms from services.bandit_environment import select_bandit_environment from redis.exceptions import RedisError from exception import InvalidIdentifierTypeException, IdentifierNotExistsException, InvalidArmTypeException, ArmNotExistsException # あるidentifier(配信面)が持つ全てのarm(選択肢)のcount(選択回数)とreward(報酬を受け取った回数)を取得 def select_arms_score (identifier: str , arms:List[ int ]) -> Dict[ int , Tuple[ int , int ]]: try : arms_score = {} for arm in arms: # model層から必要な情報を取得 count, reward = redis_select_arm_score(identifier, arm) # 各選択肢(arm)ごとに、何回選択されたか(count)と、何回報酬を受け取ったか(reward)を格納 arms_score[arm] = (count, reward) except (RedisError, InvalidIdentifierTypeException, IdentifierNotExistsException, InvalidArmTypeException, ArmNotExistsException): raise else : return arms_score # あるidentifier(配信面)が持つ全てのarm(選択肢)の一覧を取得 def select_environment_all_arms (identifier: str ) -> List[ int ]: try : # model層から必要な情報を取得 arms = redis_select_environment_all_arms(identifier) except (RedisError, InvalidIdentifierTypeException, IdentifierNotExistsException): raise else : return arms # 配信面に対してどのarm(選択肢)を返せば良いか、バンディットアルゴリズムに判断させる。 # その際に、count(選択回数)とreward(報酬を受け取った回数)の情報を用いる def get_arm (identifier: str , algorithm: str ) -> int : try : # バンディットアルゴリズムのインスタンスを立ち上げる bandit_environment = select_bandit_environment(identifier, algorithm) # identifier(配信面)が持つarm(選択肢)の一覧情報 # およびそれら選択肢に対するscore情報(選択回数:countと報酬回数:reward)を取得 arms = select_environment_all_arms(identifier) arms_score = select_arms_score(identifier, arms) # バンディットアルゴリズムのインスタンスに対して、どのarm(選択肢)を返せば良いかを判断させる arm_number = bandit_environment.get_arm(arms_score) except (RedisError, InvalidIdentifierTypeException, IdentifierNotExistsException, InvalidArmTypeException, ArmNotExistsException): raise else : return arm_number # どのarm(選択肢)にするか、番号で返す よかったこと 次に、インフラ構築、およびバンディットサーバー内部実装それぞれで責務の分離を意識することで得られたメリットについてまとめます。 DELISH KITCHENサーバーとの責務の分離 DELISH KITCHEN自体のバックエンドサーバーと、バンディットサーバーを切り離して実装したことによって、実装の柔軟性や耐障害性の観点で以下のメリットが得られました。 実装の柔軟性 データサイエンティストがバンディットのロジックのみに責務を持ち、開発リソースを集中できる 新しいバンディットアルゴリズムを追加する場面等での拡張性が高い DELISH KITCHEN自体のバックエンド実装を意識せず、ロジックに集中してメンテナンスを行うことができる 耐障害性 万が一問題が発生した時にも、事業に与える悪影響を最小限に抑えやすくなる 予めDELISH KITCHENバックエンド側にフェールセーフ機構※を備えておくことで、バンディットサーバーへのAPIリクエストに失敗した際でも、ユーザー影響無くサービス運用を継続できる ※ バンディットサーバーとの疎通に失敗した場合、複数選択肢の出し分けは中止し、予め決め打ちしたデザインを表示するように設定しておく等 MVC化による責務の分離 さらに、バンディットサーバー内部実装でMVCアーキテクチャによる責務の分離を意識したことで、可読性、およびコード全体のメンテナンス性が向上しました。 Model層、Service層などの各層の役割が明確に分かれ、各々に対する変更が局在化されるため、システム全体の安定性を保ちつつ 新規バンディットアルゴリズムを追加実装する データソースを変更する ことが可能となりました。 大変だったこと “探索と活用”のテストの難しさ バンディットアルゴリズムは確率的に生成される情報を取り扱うため、実装したコードの挙動をテストする際はその性質も考慮する必要があります。 以下は、「バンディットアルゴリズムの試行が100回進んだ際、最も”良い”選択肢が最も多く選ばれることを期待する」unittestのコード例となります。 # 試行を進めた結果、最も報酬が得られる確率が高いarm(選択肢)の選択回数が多くなることを確認する。 # 良いarmを探索し、その結果を活用できているかの観点で簡易テスト def assert_most_selected_arm_is_best_arm (self, agent: Union[EpsilonGreedyAgent, ThompsonSamplingAgent, UCBAgent], thetas: Dict[ int , float ], num_trials= 10000 ): # 最も報酬が得られる確率が高いarm best_arm = max (thetas, key=thetas.get) most_selected_arm = self.get_most_selected_arm_from_bandit_simulation(agent, thetas, num_trials) with self.subTest(test_input=most_selected_arm, expected=best_arm): self.assertEqual(most_selected_arm, best_arm) def test_exploit_best_arm (self): agent = ThompsonSamplingAgent( "test_agent" ) # 後のシミュレーションにおいて、armから報酬が得られる確率を定義 thetas = { 0 : 0.1 , 1 : 0.5 , 2 : 0.8 } # {選択肢の番号:その選択肢から報酬が得られる確率} self.assert_most_selected_arm_is_best_arm(agent, thetas) このように、アルゴリズムが「良い選択肢によっていく性質」を簡易的にテストしたい場合は、確率的な振る舞いを前提にし、ある種の曖昧さを許容するような視点を求められることがあります。 一方、この粒度の単体的なテストではバンディットアルゴリズムが時間経過と共に、探索と活用のバランスを取るように設計されている(図3で示したような様子)ことを確かめるのは困難です。 さらに、ビジネスシーンで実運用する際は、上記のテストとは異なり、各選択肢から報酬が得られる確率は未知となります。   こうした難しさがあるため、本番環境に投入して経過観察してみないと時間推移的な挙動の確からしさが判別しづらいのが現状です。 そのため、オフライン評価手法をもっと整備しなければならないという課題感を感じているところではあります。 最後に バンディットの要件整理&アルゴリズムの実装はもちろん、それに留まらずサーバー実装〜AWSインフラを一気通貫で構築した今回の取り組みは、データ職責として挑戦的な機会となりました。 慣れない技術スタック、苦労も多くあったものの、 MLをどうサービスに組み込むか そのために如何にして開発サイドのエンジニアと連携するか 等の視座を上げることができ、非常に有意義な経験となりました。 また、バンディットアルゴリズムの導入により、「サービス内の表示コンテンツを最適化する手段」としてA/Bテスト以外の選択肢が加わることとなりました。 今後は、検証の目的によってA/Bテストとは使い分けつつ、バンディットアルゴリズムの利用拡大や文脈付きバンディットへの拡張などに取り組んでいきたいと思います。 参考書籍 飯塚 修平. 2020. ウェブ最適化ではじめる機械学習―A/Bテスト、メタヒューリスティクス、バンディットアルゴリズムからベイズ最適化まで. オライリージャパン.
アバター
はじめに こんにちは トモニテ でバックエンド周りの開発を行っている rymiyamoto です。 トモニテ ではこれまで構成を AWS の EKS を使ってきましたが、2 月の初旬に ECS に移行したのでその内容を紹介していきます。 本内容のスライドverはこちら↓ speakerdeck.com 経緯 移行を決断したのは最大の理由は、現状のメンバーで kubernetes(以下 k8s) のをメンテナンスしていくコストが高すぎるためです。 k8s 自体が高頻度にアップデートが進んでおり、日々の業務を進めがらのキャッチアップが難しく、いざアップデートするのは EKS のサポートが切れる間際になってしまい後手に回っていました。 (大体年 1 回ぐらいのペースでやっていました) 対応をすすめる際もバージョンが大きく飛んでしまうため、リリースノートを追ってちゃんとアップデートを完了するにはだいたい 1 メンバー 1 ヶ月ぐらいはかかってしまいます。 かかる工数がもったいなく、また社内の別プロダクトで ECS の運用実績がしっかりとあるので合わせることとなりました。 移行までのロードマップ 基本的には EKS 部分を ECS に乗り換えるに止め、全体的なアーキテクチャの変更はしない方向で進めました。 理由としては諸事情により対応期間があまり取れなかったためです。 実際進めていったときのロードマップは以下のようになります。 AWS コンソール上で ECS 環境を用意 ECS 環境の IaC 化 & CI/CD の整備 ECS 環境に切り替え 本番移行も移行 EKS 環境の破棄 AWS コンソール上で ECS 環境を用意 まずは ECS に乗り換えてアプリケーションレベルので修正が必要かを確認するために、DEV 環境のコンソール上から環境を構築していきました。 クラスターの作成 API server やら web 等をタスク定義 タスク定義をもとに ECS のサービスを Fargate で作成 これらに伴う role や policy の作成/変更 ECS 環境の IaC 化 & CI/CD の整備 アプリケーション側の調整が済み次第、terraform におこし反映していきます。 ただ ECS のタスク定義は初回の生成移行それぞれのサービスで環境変数やコンテナイメージを管理したいため、それぞれの repository で json ファイルとして管理するようになりました。 タスク定義(task-definition.json) { " taskDefinition ": { " containerDefinitions ": [ { " name ": " api-server ", " image ": " 111111111111.dkr.ecr.ap-northeast-1.amazonaws.com/api-server:develop ", " cpu ": 0 , " portMappings ": [ { " containerPort ": 1323 , " hostPort ": 1323 , " protocol ": " tcp " } ] , " essential ": true , " command ": [ " /bin/sh ", " -c ", " './run.sh' " ] , // 起動コマンド、スクリプトあるのでそれを実行 " linuxParameters ": { " initProcessEnabled ": true } , " environment ": [ // more... ] , " ulimits ": [ { " name ": " nofile ", " softLimit ": 1024 , " hardLimit ": 4096 } ] , " mountPoints ": [] , " volumesFrom ": [] , " secrets ": [ // more... ] , " logConfiguration ": { " logDriver ": " awslogs ", " options ": { " awslogs-create-group ": " true ", " awslogs-group ": " /ecs/api-server ", " awslogs-region ": " ap-northeast-1 ", " awslogs-stream-prefix ": " ecs " } } } ] , " family ": " api-server ", " taskRoleArn ": " arn:aws:iam::111111111111:role/server-ecs-task-role ", " executionRoleArn ": " arn:aws:iam::111111111111:role/ecsTaskExecutionRole ", " networkMode ": " awsvpc ", " volumes ": [] , " placementConstraints ": [] , " requiresCompatibilities ": [ " FARGATE " ] , " cpu ": " 256 ", " memory ": " 512 ", " runtimePlatform ": { " cpuArchitecture ": " X86_64 ", " operatingSystemFamily ": " LINUX " } , " tags ": [] } } サービス関連 今回は別プロダクトでも運用している ecs-deploy を利用しました。 (こちらは現在メンテナンスモードで機能追加はありませんが保守は滞りなく進んでいるようです) 先程のパートで作成したタスク定義の json を渡すことで簡単にデプロイすることができます。 # ecs-deployの用意 curl -sL https://github.com/silinternational/ecs-deploy/archive/ 3 . 10 . 7 .tar.gz | tar zxvf - mv ecs-deploy-3. 10 . 7 ecs-deploy chmod +x ecs-deploy/ecs-deploy # 途中でdocker image 作成やECRへのアップロード等の処理 # 省略... # サービスへデプロイ ecs-deploy/ecs-deploy --cluster ecs-cluster \ --task-definition-file task-definition.json \ --service-name api-server \ --region ap-northeast-1 \ --timeout 600 \ --image 111111111111 .dkr.ecr.ap-northeast-1.amazonaws.com/api-server:develop 余談ですが ecspresso の採用も視野にありましたが、今回は対応できる期間も短く社内で運用実績ないため採用は見送りとなっています。 バッチ関連 これまで k8s 上では定期実行を cronjob、ショット実行で job を用いており、またバッチの docker image は全てまとめていました。 (バッチが増えるたびに image を ECR へ登録していくのは面倒なので) そのためそのまま AWS CLI で作成すると CloudWatch のロググループがひとまとめになり見づらくなってしまう + ショット実行の方法が面倒になってしまいます。 今回は scheduled task を管理するために ecschedule で反映するようにしました。理由としては、スケジュール、override 含めて yaml で管理可能なところと run での即時実行にも対応していたためです。 共通化できる部分は yaml のテンプレート化したいので ytt を採用しています。 構成イメージとしては以下のようになります。 . ├── base-task-def.json(テンプレートとなるタスク定義) ├── config.yaml(ecsscheduleで使う設定ファイル) ├── tasks(各batchの設定ファイル) ├── batch-hoge.yaml └── batch-fuga.yaml └── more... base-task-def.json { " executionRoleArn ": " arn:aws:iam::111111111111:role/ecsTaskExecutionRole ", " containerDefinitions ": [ { " logConfiguration ": { " logDriver ": " awslogs ", " options ": { " awslogs-group ": " /ecs/server-batch ", " awslogs-region ": " ap-northeast-1 ", " awslogs-stream-prefix ": $job_name } } , " entryPoint ": [] , " portMappings ": [] , " command ": [] , " cpu ": 0 , " environment ": [ // more... ] , " mountPoints ": [] , " secrets ": [ // more... ] , " volumesFrom ": [] , " image ": " 111111111111.dkr.ecr.ap-northeast-1.amazonaws.com/server-batch:develop ", " name ": " server-batch " } ] , " placementConstraints ": [] , " memory ": " 1024 ", " taskRoleArn ": " arn:aws:iam::111111111111:role/scheduled-task ", " family ": $job_name, " requiresCompatibilities ": [ " FARGATE " ] , " networkMode ": " awsvpc ", " runtimePlatform ": { " operatingSystemFamily ": " LINUX " } , " cpu ": " 512 ", " volumes ": [] } config.yaml #@ load("@ytt:data", "data") region : ap-northeast-1 cluster : ecs-cluster rules : #@ data.values.rules batch-hoge.yaml #@data/values --- rules : - name : batch-hoge scheduleExpression : cron(0 0 * * ? *) taskDefinition : batch-hoge containerOverrides : - name : server-batch command : - ./exec environment : # more... launch_type : FARGATE platform_version : LATEST network_configuration : aws_vpc_configuration : subnets : - subnet-hoge - subnet-fuga - subnet-foo security_groups : - sg-hoge assign_public_ip : DISABLED disabled : false この構成のもとに CI 側でタスク定義と scheduled task の登録を行っています。 # tasks配下のbatchをもとにタスク定義 for item in ` ls tasks` do job = `basename ${item} .yaml` new_task_definition = $( jq -n --argjson job_name " \" $job \" " -f " base-task-def.json " ) aws ecs register-task-definition --cli-input-json " $new_task_definition " done # yttでecscheduleで反映させるためのyaml生成 ytt -f " config.yaml " -f " tasks " --file-mark ' config.yaml:exclusive-for-output=true ' > " ecschedule.yaml " # 生成したyamlを元に反映 ecschedule -conf " ecschedule.yaml " apply -all また余談ですがこの構成のときショット実行の際は以下のように実行できます。 # ショット実行したいタスク item = " batch-hoge.yaml " job = `basename ${item} .yaml` # yttでecscheduleで反映させるためのyaml生成 ytt -f " config.yaml " -f " tasks/ ${job} " --file-mark ' config.yaml:exclusive-for-output=true ' > " ecschedule.yaml " # 生成したyamlを元に反映 ecschedule -conf " ecschedule.yaml " run -rule $job ECS 環境に切り替え 一気に新環境に切り替えていくのは、不測の事態があったときに対応が大変になるので、Route53 で新環境へ加重を数日かけて少しず増やしていきました。 また batch 系は 二重で走らないように EKS 側を停止した状態でスケジュールタスクを active に変更して反映させています。 EKS 環境の破棄 不要になった EKS 向けの CI/CD や関連する aws リソースの削除を進めていきました。 一気に terraform で関連リソースを削除しようとすると、都合上依存すると別のリソースまで影響してしまいます。 そのため信頼関係を見つつ地道にモジュール削除を進めていきました。 移行してみて よかったところ k8s の頃に比べてサービスやバッチの構成をマニュフェストで管理するよりはシンプルな構造になりました。 また最大の課題であった EKS のアップデート作業からの開放でよりサービス開発に重きを降ることができるようになったと感じます。 大変だったこと EKS on EC2 で運用していたものを ECS Fargate に変えたことで厳密にリソースの管理をしないといけないので、これまで見えていなかったメモリリークが起きていたことに気付かされました。 (この対応の話は以下の記事で紹介しています) tech.every.tv また k8s 自体が活発なコミュニティ故にツールが豊富でしたが、ECS は AWS に縛られる形となるのでデファクトスタンダードと呼べるデプロイ方法が見当たらず何かと創意工夫が必要となってしまいました。 最後に 世間的の逆の流れを行く対応となっていますが、k8s を採用する場合はサービスと組織の規模感を意識しておかないと後々のメンテナンスが辛くなってしまいます。 もちろん k8s を正しく使いこなせると様々な機能の恩恵を受けれるので、体制的にちゃんと面倒を見れるかどうかを判断してとり入れるのは問題ないと思います。 EKS(k8s)と ECS をどっちですすめるか迷っている方や、同じように ECS への移行を検討している方の手助けになれば幸いです。
アバター
はじめに こんにちは。MAMADAYS開発部でiOSエンジニアをやってる國吉です。 この度、MAMADAYSから姉妹アプリ第一弾となる”陣痛カウンター”をリリースしました。 MAMADAYSアプリはスーパーアプリになっていて機能数も多く長く利用して頂くユーザさんも多いアプリです。一方で、陣痛時の利用という利用期間が短い用途のものは小さいアプリに切り出して機能特化することでシンプルで使いやすい戦略を取っています。 そしてタイトルにも書いてある通り、陣痛カウンターはSwift Package Managerを採用しています。採用理由は下記で、お試しで導入するには最適だと考え、導入に至りました。 - アプリ自体の規模が小さい - 前からSwift Package Managerを使ってみたかった - 使用するライブラリ数が少ない 今回はそんなSwift Package Managerについてお話ししていきます。 陣痛カウンター 本題に入る前に少し陣痛カウンターアプリについて宣伝をさせてください。 陣痛カウンターは機能・デザイン共にシンプルですごく使いやすい!また、オフライン状態でも動作します! 陣痛がきたかなと感じたら”きたかも”ボタンをタップし、時間計測を始めます。陣痛が治まったら”おさまったかも”ボタンをタップして計測を終了します。 それらデータを履歴として表示して、陣痛間隔が一定の間隔より下回ったらお知らせする。といったアプリです! 実際の使い勝手などは実際にインストールして使ってみてください!是非周りに出産を控える妊婦さんがいたら紹介して頂けると嬉しいです。   陣痛カウンター - すぐ計測できる陣痛アプリ・陣痛タイマー every, Inc. メディカル 無料 apps.apple.com Swift Package Manager さて、本題のSwift Package Manager(以下”SPM”と略)のお話をしていきます。 SPMはApple公式から提供されているパッケージマネージャーになっており、Xcode9以降から使うことができます。 SPMに対応しているか調査 まず、アプリに入れようとしているライブラリがSPMに対応しているのか調べる必要があります。 ライブラリ側がSPMに対応していなかった場合は、SPMは使えません。 調べ方はすごく簡単で、ライブラリのディレクトリ内に「Package.swift」というファイルがあるかどうかを見るだけです。 例:nuke https://github.com/kean/Nuke 1. ライブラリのディレクトリが確認できるページを開きます。 2. ディレクトリ内を確認するとPackage.swiftがあるのが確認できます。 3. これでnukeはSPM対応されていることがわかります。 次にアプリ側でライブラリ使えるようにしていきます。  [File] -> [Add Packages ...]  Search or Enter Package URL のサーチエリアでライブラリのリポジトリURLを入力し検索をかけます。 すると、このようにnukeがサジェストされます。 Dependency Ruleで導入したいバージョン指定を行い、[Add Package]をクリックすると、ダウンロードが開始されます。 複数パッケージが存在する場合は、どのパッケージを導入するか選択する画面がでるので、任意のパッケージを選択しダウンロードしましょう。 ダウンロードが完了すればライブラリが使えるようになります。CocoaPodsと同様にimportして使ってください。  実態はどこに ここまででSPMを用いてライブラリを追加してきましたが、設定ファイルや実態はどこにいるんだ?チーム開発をしているからメンバーにはどうやって共有されるんだ? という疑問が生じました。調べた結果下記の場所に設定ファイルや実態がありました。 依存関係を解決した結果や各ライブラリのバージョンが”Package.resolved”というファイルに書き出されるので、これを他メンバーに共有されることでライブラリのバージョンを揃えることができます。  # ライブラリ群 > /Library/Developer/Xcode/DerivedData/{projectID}/SourcePackages/checkouts/ # Package.resolved(設定ファイル) > /{project}.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved CocoaPodsとの比較 Xcodeのみでライブラリの追加/削除が簡単にでき、バージョン確認等々も完結できます。 "pod init"や"pod install"とかしなくていいので、結果的に操作する箇所が減って楽になりました。 SPMはApple公式が出しているパッケージマネージャーなので、安心感はあります。Xcode13以降ようやく動作も安定してきたようです。  SPM導入で苦労したこと 陣痛カウンターはFirebaseのCrashlyticsを使用しています。 fastlaneでFirebaseにdSYMを上げているんですが、実行しても「upload-symbolsが存在しません」とエラーが吐かれました。 CocoaPodsでライブラリ管理をしているとPodsフォルダ配下にupload-symbolsというスクリプトファイルが存在しますが、 SPMはライブラリの実態がプロジェクトのディレクトリ配下にはいないので、参照できないということでした。 回避策として、Firebase公式のページからupload-symbolsをダウンロードし、fastlaneではそのスクリプトファイルを動かすように対応しました。 導入してみた感想 結論、この規模感のアプリにはSPMは適していると思いました。 まず、CocoaPodsとの比較セクションでも触れましたがライブラリの追加/削除がXcode内で簡単にできるのが一番大きいメリットです。 Pod関連のファイルも無くなるので、ディレクトリの見通しも良くなります。 Crashlyticsとか独自の対応を行いましたが、一度対応しておけば使い回すことができるので、最初だけ我慢しましょう。 ただ、陣痛カウンターのような小規模なアプリに適していると感じているだけで、中規模以上のアプリには不適切かもしれません。 あと、既にCocoaPodsで実装を進めてしまっている場合も、CocoaPodsのままでいいと思います。現状、移行するコストを払うほどのメリットは無いと感じています。 また古いライブラリはSPMに対応していないケースも多々あるので、導入前に追加予定のライブラリがSPMに対応しているかの調査はすごく大事です。その中で1つでもSPM対応されていないライブラリがあるのであれば、手間が増えるだけなのでCocoaPodsにした方がいいです。 最後に ここまで読んでくださりありがとうございました。 SPMについて触りぐらいは伝わったでしょうか?この記事がSPM導入の手助けになることがあれば嬉しいと思います!
アバター
DELISH KITCHENの定期購読 こんにちは、DELISH KITCHEN開発部でバックエンド開発を担当している南です。 主にDELISH KITCHENのプレミアムユーザー向けの機能の開発を行っております。 DELISH KITCHENでは、人気順検索、プレミアムレシピ(ダイエット、ヘルスケア、美容・健康、作りおき)、 プレミアム献立など、さまざまな機能を提供するプレミアムサービスという定期購読(サブスクリプション)商品を販売しております。 プレミアムサービスは、おもにiOSやAndroidのプラットフォーム上で管理、販売されておりDELISH KITCHENアプリ内から購入できます。 ここではiOSの課金をIAP(In-App-Purchase), Androidの課金をIAB(In-App-Billing)と呼んで区別したいと思います。 IAPとIABとDELISH KITCHEN iOSやAndroidのプラットフォームに対してAPIを実行することでそれぞれ課金状態を表したレシートを取得できます。 IAPレシートもIABレシートも課金状態を表現するものという点では共通しているのですが、 表現の仕方がことなるためDELISH KITCHENのサーバー側で違いを吸収する必要があります。 IAPとIABの課金状態について IAPもIABもレシートが表現する課金状態はほぼ同じですが、IABにのみ一時停止という状態があります。 課金状態名と、それがDELISH KITCHENにおいて、どのような状態かを説明した表です。 AndroidのIABレシート IABレシートの構造はシンプルで、現時点の購読状態のみ返します。 購読が更新されれば、 expiryTimeMillis の日時が増加し、支払いに関して変化がおきたら paymentState の値が変化します。 { " kind ": " xxxxxxx ", " startTimeMillis ": 1111111111111 , " expiryTimeMillis ": 2222222222222 , " autoRenewing ": true , " priceCurrencyCode ": " JPY ", " priceAmountMicros ": 480000000 , " countryCode ": " JP ", " developerPayload ": "", " paymentState ": 1 , " cancelReason ": 0 , " userCancellationTimeMillis ": 0 , " orderId ": " GPA.0000-0000-0000-0000 ", " linkedPurchaseToken ": "", " purchaseType ": 0 } IABレシートとユーザー状態 レシートの値からユーザーの課金状態を把握する必要があります。 IABは返す情報がシンプルで、情報と状態を結びつける資料も整備されているので判別することが簡単です。 一方で履歴のような過去の情報が一切ないため、状態の変遷をAPIから知る方法がありません。 Google Play の課金システム > 定期購入を販売する (*) 一時停止状態とはユーザーがPlayストアの定期購読一覧から指定した期間だけ購読を中断して、期間がすぎたら再び自動再開する仕組みです。 AppleStoreのIAPレシート 一方でIAPレシートは、IABレシートと比べると構造が複雑で情報も多めです。 latest_receipt_info には定期購読商品の購入履歴が含まれています。 履歴の1つ1つには、「どんな商品を購入したか?」、「何時購入したか?」、「何時期限切れになるか?」といった変化しない情報が含まれています。 (例外として返金キャンセルが発生すると履歴の値が変化します) また状況に応じて刻々と値が変わる pending_renewal_info という項目があります。 pending_renewal_info からは「次回の更新で購入する予定の情報」、「定期購読を継続するか否か」、「期限切れになった理由」、といった状況に応じて変化する情報が含まれています。 { ... " latest_receipt_info ": [ { " quantity ": " 1 ", " product_id ": " delishkitchen ", " transaction_id ": " 111111111111111 ", " original_transaction_id ": " 111111111111111 ", " purchase_date_ms ": " 1629307052000 ", " purchase_date ": " 2022-02-01 07:00:00 Etc/GMT ", " purchase_date_pst ": " 2021-02-01 00:00:00 America/Los_Angeles ", " original_purchase_date_ms ": " 1643698800000 ", " expires_date_ms ": " 1646118000000 ", " expires_date ": " 2021-03-01 07:00:00 Etc/GMT ", " expires_date_pst ": " 2021-03-01 00:00:00 America/Los_Angeles ", " cancellation_date_ms ": " 0 ", " cancellation_date ": 0 , " cancellation_date_pst ": 0 , " web_order_line_item_id ": " 333333333333333 ", " is_trial_period ": " true ", " is_in_intro_offer_period ": " false ", " promotional_offer_id ": "" } , { " quantity ": " 1 ", " product_id ": " delishkitchen ", " transaction_id ": " 222222222222222 ", " original_transaction_id ": " 111111111111111 ", " purchase_date_ms ": " 1646118000000 ", " purchase_date ": " 2022-03-01 07:00:00 Etc/GMT ", " purchase_date_pst ": " 2021-03-01 00:00:00 America/Los_Angeles ", " original_purchase_date_ms ": " 1629307053000 ", " expires_date_ms ": " 1648796400000 ", " expires_date ": " 2022-04-01 07:00:00 Etc/GMT ", " expires_date_pst ": " 2021-04-01 00:00:00 America/Los_Angeles ", " cancellation_date_ms ": " 0 ", " cancellation_date ": 0 , " cancellation_date_pst ": 0 , " web_order_line_item_id ": " 444444444444444 ", " is_trial_period ": " false ", " is_in_intro_offer_period ": " false ", " promotional_offer_id ": "" } ] , " pending_renewal_info ": [ { " expiration_intent ": "", " auto_renew_product_id ": " delishkitchen ", " original_transaction_id ": " 111111111111111 ", " is_in_billing_retry_period ": "", " product_id ": " delishkitchen ", " auto_renew_status ": " 1 " } ] } IAPレシートとユーザー状態 IAPレシートは情報が多めですが、レシートからユーザーの状態を把握する際は以下の情報を用いています。 latest_receipt_infoの最新のレシート is_trial_period: 無料トライアルか否か expires_date_ms: 有効期限の日時 cancellation_date_ms: 返金した日時 pending_renewal_info auto_renew_status: 購読を継続するか否かを表す。 expiration_intent : レシートが期限切れになった理由を表す。期限内は常に空 is_in_billing_retry_period: 支払いリトライ中か否かを表す。ExpirationIntent=2以外のときは空 grace_period_expiration_date: 猶予期間の期限 auto_renew_product_id: 次回の更新に購入するプロダクトID 課金状態と注意点 猶予期間 と 保留中(支払いリトライ状態) と 一時停止 の扱いは気をつけないといけません。 猶予期間 期限が切れたユーザーを引き続き課金状態として扱うため、IABでは expiryTimeMillis の日時が自動的に伸びます。 一方IAPでは、latest_receipt_infoの expires_date_ms は伸びません。代わりに、 grace_period_expiration_date_ms に猶予期間の日時が入ります。 保留中(支払いリトライ状態) 猶予期間後も支払いに失敗しつづけている状態です。 期限切れになり無料ユーザー状態となりますが、一定期間(デフォルトでiOSは60日間、Androidは30日間)支払いをリトライし続けます。 リトライによって支払いが成功すると購読状態に戻りますが、一定期間以上失敗し続けると、プラットフォームが自動的に解約状態にしてリトライをやめます。 また支払いリトライ期間中に解約することもできます。 一時停止 IAB特有のユーザー状態です。こちらも一度期限が切れるため、一見解約したように見えます。 しかし指定した期間をすぎると何事も無かったかのように購読を再開するため、一時停止状態中だと判別できていないと解約したユーザーが再び戻ってきたかのように見えてしまいます。 また定期購読一時停止中に解約することもできます。 定期購読と状態管理 Choosing a Receipt Validation Technique こちらで述べられている通り定期購読状態を適切に扱うにはサーバーで購読状態を管理し、同期する必要があります。 ですが、これだけでは下記のようなユーザーの行動の変遷を追うことはできません。 ユーザー状態をと経緯を正確に判断するためにも、レシートの履歴をサービスのサーバー側で保存することも大切です。 ユーザーが猶予期間中になっていたか? 保留中から戻ったのか?、それともキャンセルしたのか? 一時停止から戻ったのか?、それともキャンセルしたのか? まとめ 定期購読の難しいところ でも述べられておりますが、 一見単純そうにみえる定期購読ですが、正しくやろうとすると実は面倒なことが多いです。 またIAP、IABの仕様追加にも追従していく必要がありサーバー側の保守コストがかかります。 ですがDELISH KITCHENのプレミアム機能を多くの方に提供し続けるためにも定期購読の管理・アップデートを続けていきたいと思っております。 参考資料 Google Play の課金システム > 定期購入を販売する App Store Receipt Data Types Choosing a Receipt Validation Technique Question About Ios Receipt Fields Addition on July 19 2017 App Store の In-App Purchase の Grace Period対応 アップルはApp Storeのサブスク期限切れに「猶予期間」を導入 Engineering Subscriptions(WWDC 2018) Auto Renewing Subscriptions for iOS Apps
アバター
こんにちは。TIMELINE開発部の齊藤です。好きなエディタはEmacsです。社内の一部エンジニアからは珍獣扱いされてますが、Emacsは最強のエディタなので20年近く愛用しています 1 。 さて、皆様は日頃のサービス運用に、社内向けの管理サイトなどを作っているかと思われますが、弊社でもご多分に漏れず管理サイトを用意して、日々の運用を行なっております。 この管理サイトの出来不出来によっては、運用コストも大きく変わったりするので、案外重要なものだったりするのですが、作るのは正直めんどくさいです。 ユーザさんにお見せするサイトと異なり、MAUは一桁ぐらいですし、いいものを作っても誰かに誉められることも少ないので、正直めんどくさいです(大事なことなのでry なので、めんどうなことは少しでも楽をしつつ、それでいて運用事故/コストを少しでも減らせられるようにがんばっていたりします。 序文 私が所属しているTIMELINE開発部では、「au payマーケット」アプリで提供している「ライブTV」の運用/開発などを主に担当しています。 コロナ禍でライブコマースがEC市場で再燃。複数のプラットフォームで配信できる『TIMELINE』ならではの強みとは 元々、弊社ではCHECKというライブコマースサービスを運用していました。 ライブTVはその時の資産を利用しており、管理サイトなども数年前のリリース時に私が作ったのがそのまま利用されていたりします。 当時は何も考えなしに作ってたのですが、TIMELINEに合流して改めて見ると、あちらこちらでめんどくせーってところが散見されたため、そこらへんを修正していったあれやこれやを紹介させていただきます。 ビルドの自前管理がめんどくさい webpackを用いてビルドしているのですが、元々はそのコンフィグファイルを自前管理していました。 当時は「 grunt に比べてなんて楽なんだろう...」と感動して使っていたのですが、久々に昔書いたコンフィグファイルを眺めていると、「こんなん管理するとかムリー!!」っていう感情に溢れてしまうほどめんどくさいものでした。 そこで、MAMADAYSで採用していた Next.js へ全面的に載せ替えることにしました。 Next.js の利点として SSR/SSGによるものがよく挙げられますが、管理サイトのフレームワークとして採用する利点はそこではなく、「ビルド管理をNext.js に任せられる。また、それに伴う各種恩恵(後述)に与れる」ところです。 もちろん載せ替える手間はあります。しかし、今後発生するであろうめんどくささと比べたら微々たるものですし、元々が React で書いていたので最低限の修正だけで載せ替えることができました。 これにより next build するだけでビルドしてくれます。コンフィグを自前管理しなくてもいい 2 だなんて、神! さらには、開発時のホットリロードも独自で書いてたのですが、それすらも next dev するだけでやってくれます。至れり尽くせりすぎる! 開発効率は格段に上がり、細かい修正とかへのストレスもだいぶ軽減されました。 ルーティングがめんどくさい 元々は react-router-dom を使って、下記のような方法でルーティングを行っていました。 import { HashRouter, Switch, Route } from 'react-router-dom'; import Index from './containers/Index'; import Hoge from './containers/Hoge'; const routes = [ { path: '/', exact: true, component: Index, }, { path: '/hoges, exact: true, component: Hoge, }, ]; export default class App extends Component { render() { return ( <HashRouter> <Switch> {routes.map(({ path, exact, component }) => ( <Route key={path} path={path} exact={exact} component={component} /> ))} <Route render={(props) => <NoMatch {...props} /> } /> </Switch> </HashRouter> ); } } 教科書どおりな react-router-dom の使い方ですし、NestedRouteing などの考えはものすごく良いのですが、管理サイトでそこまでやるメリットが思いつきませんでした。 むしろ、これだとページを追加するたびに routes を修正しないといけないし、path とファイル名が一致しないケースがあったりと、めんどくささが満載です。 ですが、Next.js に載せ替えたため、pages 以下にファイルを置くだけで、ルーティングをよしなにしてくれます。 だいぶめんどくささが軽減されましたし、path とファイル名が一致してるだけで、ものすごく気分が楽になります。 コンポーネント名を覚えるのがめんどくさい JavaScript は型がなく厳密な書き方をせずとも動くので楽っちゃ楽なのですが、型がないがゆえに補完をうまくしてくれません。 そのため、ファイル名や位置とかをある程度、脳内メモリに格納した上で開発しないといけないわけですが、私も本厄を迎える歳となってしまい細かい記憶力に心配がでてくるようになってきました。 TypeScript であれば、最強のエディタであるEmacsがよしなに補完してくれる 3 し、import とかも気にせずに済むんですが...。 という悩みも、Next.js に載せ替えたことにより、 tsconfig.json を置くだけでTypeScript化は完了です。 無事に加齢による衰えをシステムでフォローしてくれようになり、高齢化対策も万全です! デザインがめんどくさい cssが苦手です。書きたくないです。レスポンシブデザインとかになると、めんどくささに溢れてます。 なので、管理サイトは基本的に Bootstrap を使ってデザインしてます。 ただそれでもクラス名を覚えるのがめんどくさいので、 ReactBootstrap を使ってます。 もちろんReactBootstrapのコンポーネント名とかもEmacsが補完してくれます 4 。 また、どうしてもcssを書かなきゃいけない個所であっても、Next.js が Sass に対応してくれるので、生でcssを書くよりも格段に楽ができます。 バリデーションがめんどくさい 元々は下記のように各項目ごとにバリデーションを書いていました。 const [form, setForm] = useState({ name: '', state: 0 }); const isValidName = () => { if (form.name.length < 5) { return false; } if (form.name.length > 10) { return false; } return true; }; const isValidState = () => { if ([0, 1].includes(form.state)) { return false; } return true; }; const onSubmit = (e) => { e.preventDefault(); if (!isValidName() || !isValidState()) { // エラーハンドリング return; } // 正常処理 }; まー、めんどくさい。しかも、漏れも生じまくる。入稿してくれる方の職人芸もありつつの、事故回避でした。 そこで zod を用いることにしました。 zodに関しては uttkさんのこの記事 が秀逸です。めちゃくちゃ参考にしました。 上記の例をzodを使って書き直すと const schema = z.object({ name: z.string().min(5).max(10), state: z.union([z.literal(0), z.literal(1)]), }); const [form, setForm] = useState({ name: '', state: 0 }); const onSubmit = (e) => { e.preventDefault(); try { f = schema.parse(form); } catch (e) { // エラーハンドリング return; } // 正常処理 }; 意識するのはzodの定義のみで、非常にわかりやすくなりました。 しかも、zodからTypeScriptの型も吐き出せます type IForm = z.infer<typeof schema>; // IForm = type { // name: string, // state: 0 | 1, // } なので、一石二鳥!使わない手はないです。 フォームがめんどくさい 運用の大部分を占めるのが入稿作業だと思います。 これも元々はお見せするのも恥ずかしいレベルのオレオレフォームを作っていたのですが、 useForm を使うようにしました。 zodと組み合わせるとすげー便利ですし、すっきりさせることができました。 const methods = useForm<IForm>({ resolver: zodResolver(schema), defaultValues: { name: '', state: 0, }, }); const onSubmit: SubmitHanlder<IForm> = (data) => { // 正常処理 }; 跋文 こんな感じで手を抜けるところは抜いて、それでいて運用コストを少しでも下げられるような改善を日々行っています。 また、手を抜くことにより、理解する箇所を極限まで減らしていくことにもつながるので、普段フロントエンドを書いていないエンジニアでも、運用サイトの更新ができるという面もあったりします。 Emacsは エディタではなくOSである という方もおられますが、ここではエディタとして扱わせていただきます ↩ もちろん細かい設定をいじりたいときは修正する必要がありますが、そうだとしても格段に管理が楽になりました ↩ だいたいのエディタでやってくれます… ↩ だいたいのエディタでやってくれます… ↩
アバター
はじめに こんにちはMAMADAYSバックエンドチームのrymiyamotoです。最近エルデンリングを遊び倒しています。 MAMADAYSではアプリとWebで利用しているAPI(golang)の仕様をドキュメント化するためにSwaggerを利用しています。 導入をしてから3年以上経過したため、APIの開発運用を進める中で出てきた課題点への施策を綴っていこうと思います。 そもそもSwaggerとは? SwaggerはOpenAPIというRESTful APIの仕様を記述するためのフォーマットを使用したツールで、仕様が文章化されることで開発者や関係者での認識が取りやすくなります。 動作環境 MAMADAYSではSwaggerの利用にあたって以下のツールを使っています。 ツール名 用途 バージョン swag ドキュメントの自動生成 v1.8.0 echo-swagger Swagger UIの表示で利用(ドキュメントの可視化) v1.3.0 Swaggerをそのまま使う分にはyamlを表記するだけですが、MAMADAYSではドキュメントを自動生成するための swag を使っています。 swag では定義したstructの型に合わせてドキュメントを生成するのでyamlを直接手で変更する必要がなく楽です。 また生成されたドキュメントのままだと視覚的に分かりにくいため、Swagger UIを表示できるように echo-swagger を利用しています。 以下MAMADAYSので表記に合わせた簡易的な例です。 (goのバージョンは1.17.8です) package main import ( "net/http" _ "github.com/rymiyamoto/swagger-test/docs" "github.com/labstack/echo/v4" echoSwagger "github.com/swaggo/echo-swagger" ) type ( Response struct { Int64 int64 `json:"int64"` String string `json:"string"` World *Item `json:"world"` } Item struct { Text string `json:"text"` } ) // @title example // @version 1.0 // @license.name rymiyamoto // @BasePath / func main() { e := echo.New() e.GET( "/swagger/*" , echoSwagger.WrapHandler) e.GET( "/" , hello) e.Logger.Fatal(e.Start( ":1323" )) } // hello godoc // @Summary Hello World ! // @ID HelloWorldIndex // @Tags HelloWorld // @Produce json // @Success 200 {object} Response // @Router / [get] func hello(c echo.Context) error { return c.JSON(http.StatusOK, &Response{ Int64: 1 , String: "example" , World: &Item{ Text: "hello world !" , }, }) } ※go.modとgo.sumは省略しています $ go install github.com/swaggo/swag/cmd/swag@v1. 8 . 0 $ swag init $ go mod tidy $ go run main.go NULL許容の値を表現する sql.NullString や sql.NullInt64 などのNULL値を含むデータをそのまま使うことができないため swaggertype:"XXX" で対象のキーに表現したい型を定義するかと思います。 しかしこのままだとNULL許容であるかどうかがわかりません。 方法としては2種類あるので紹介します。 descriptionを追加する 対象のキーにコメントとして書くことでdescriptionが追加でき、ここでNULL許容であるかどうかを表現してます。 Hello struct { NullInt64 sql.NullInt64 `json:"null_int64" swaggertype:"integer"` // nullable NullString sql.NullString `json:"null_string" swaggertype:"string"` // nullable } extensionsで任意の追加情報を付与する(echo-swagger v1.3.0では非対応) extensions:"x-XXX" で任意の追加情報を付与することが可能です。 NULL許容を表現するにあたっては extensions:"x-nullable" で指定することにします。 Add extension info to struct field Hello struct { NullInt64 sql.NullInt64 `json:"null_int64" swaggertype:"integer" extensions:"x-nullable"` NullString sql.NullString `json:"null_string" swaggertype:"string" extensions:"x-nullable"` } ただし echo-swagger(v1.3.0) 上では表示できないため、出力されたyamlを Suwagger Editor 上で確認する人向けです。 (表示がうまくいかないのは、依存packageである swaggo/files の内部で保持しているファイルが古そうです) 同一リソース名を扱う 同一リポジトリ内でAppやWeb・Dashboard等でAPIを作成している場合、リソース名が重複します。 このとき swag 側で全体のパスを含めた構造体名に変更してくれますが、その表記が長く冗長になってしまいます。 単純に1つしかSwaggerを利用しなければ気にすることはありませんが、表記が長くならないようにそれぞれprefixを足して見通しを良くしています。 // DashboardHoge 内部向けDashboard用 DashboardHoge struct { Text string `json:"text"` StartAT time.Time `json:"start_at"` EndAT time.Time `json:"end_at"` } // Hoge App用 Hoge struct { Text string `json:"text"` } structでリクエストのbodyを表現する bodyを扱う場合に各APIのコメントに以下のように記載すればよいです。 // post godoc // @Summary Hello World ! // @ID HelloWorldPost // @Tags HelloWorld // @Produce json // @Param title body string true "タイトル" // @Param description body string false "説明" // @Success 200 {object} Response // @Router / [post] func post(c echo.Context) error { // ... } しかしこの状態だと パラメーターが増えると定義が面倒 リクエストの定義とコントローラーの定義を別階層で管理していると抜け漏れが発生しやすくなる 同一のリクエストを使いまわしていると修正が冗長になってしまう となってしまいます。 その対策として、bodyには定義しているformのstructを渡すようにしています。 こうすることで、form部分の修正のみで対応するAPIのbodyも一括で変更できるため管理が簡単になります。 // post godoc // @Summary Hello World ! // @ID HelloWorldPost // @Tags HelloWorld // @Produce json // @Param body body Form true "request body" // @Success 200 {object} Response // @Router / [post] func post(c echo.Context) error { // ... } Form struct { Title string `json:"title"` // require Description string `json:"description"` // option } 終わりに swag を使い続けて3年も経過すると色々と気になるところが出てくるので、利用ルールの制定多くなってきました。 理想を言えばその部分も定義できればより汎用性が出そうです。 しかし適期的にアップデート内容を確認してきましたが、少しずつOpen API 3.0の記法も使えるようになってきているのでしばらくは使っていこうと思います。 皆様良きSwaggerライフを!
アバター
tl;dr Fargate ではホストが隠蔽されていて、EC2 のように SSH でコンソールに入って検証することができない ECS Exec は十分に SSH の代用となる ECS Exec の導入に必要なことは このセクション を参照 DELISH KITCHEN on ECS 弊社では DELISH KITCHEN というサービスを運用しており、主なアプリケーションサーバは ECS の上に構築しています。 ECS には EC2 によるものと、Fargate によるものの2つの環境が存在しますが、現時点ではほぼ全てのアプリケーションサーバが EC2 の環境です。 ECS は、十分な機能を備えながらシンプルで柔軟に運用できる優れたコンテナオーケストレーションサービスです。 しかしながら、EC2 環境においては常にホストとなる EC2 インスタンスをメンテナンスする必要があります。 それほど頻繁に発生する作業ではありませんが、定期的に AMI を更新しなければなりませんし、ECS 以外にホストで動作させているものがあれば、新しい AMI でも同様に動作するかの検証が必要になります。 また、EC2 の定期メンテナンスを行なっても、基本的にサービスへの影響がないことも、どのタイミングでメンテナンスを行うべきかの判断を難しくさせます。 同じだけ人員と工数をかけるのであれば、サービスにとってプラスになるタスクを優先したいというのは当然の考えです。 必然的にこのようなタスクは後回しになりますが、何らかの理由で(多くの場合はセキュリティに起因します)本当に作業が必要になった時には、それまでに積み重なっていた問題を一度に解決しなくてはいけないような状況に陥ることすらあります。 DELISH KITCHEN on Fargate Fargate とは、ECS 上でコンテナのホストとなる EC2 の管理を不要にし、ECS タスクの管理に集中することができるサービスです。 DELISH KITCHEN で Fargate を採用できれば、前述したようなインスタンスの管理に必要なタスクが削減でき、よりサービスの開発に注力できるようになります。 これは 2022年 エブリーの開発組織の抱負 にある 事業にこだわる開発組織 にもマッチしており、導入に向けて検証を進めることになりました。 検証を進めていく中で、一番ネックになったのはホストに SSH ができないことです。 従来の EC2 環境ではホストに SSH 後、docker exec 経由でコンテナの内部の調査ができたのですが、Fargate ではホストも docker も隠蔽されており、通常はアクセスすることができません。 最初は原始的にアプリケーション自体に printf デバッグを仕込んだりしていたのですが、あまりに効率が悪いため、 ECS Exec を導入しました。 ECS Exec の評価 ECS Exec では以下のようなコマンドで shell を実行できます aws ecs execute-command --cluster "$CLUSTER" --task "$TASK_ID" --container "$CONTAINER_NAME" --interactive --command sh shell だけ使うならほとんど SSH と使い勝手は変わりません。強いて言うならタイムアウトが 20 分とやや短いのと、その設定がグローバルでしか設定できないところが使い勝手が悪いでしょうか。 scp でファイルの送受信ができないことも困るタイミングがありますが、s3 を経由するのが一般的な解決法のようです。 port forwarding は ECS Exec では提供されませんが、大元の SSM では 提供されています 。 targetに ecs:{CLUSTER}_{TASK_ID}_{CONTAINER_NAME} を指定すれば Fargate で起動しているタスクへのセッションが開始できます。 例えばコンテナ内の nginx に port forwarding するなら aws ssm start-session \ --target "ecs:${CLUSTER}_${TASK_ID}_${CONTAINER_NAME}" \ --document-name AWS-StartPortForwardingSession \ --parameters '{"portNumber":["80"], "localPortNumber":["$LOCAL_PORT"]}' のようにします。 Fargate への道 このように、多少の制限はあるものの、Fargate でも従来の使い勝手に近い形でアプリケーションの調査ができることが確認できました。 現在は引き続き Fargate の評価と移行準備を行なっており、次回があればその話をできれば良いと考えています。 また、ECS Exec を有効化するのにいくつか戸惑った点があったため、最後にドキュメントと共に補足としてまとめておきました。参考になれば幸いです。 ECS Exec の有効化 ECS Exec を利用するためには、事前にいくつかの準備が必要になります。 公式のドキュメント の通りにすれば良いのですが、セッションマネージャープラグインのインストールを見落としやすいため、注意が必要です。 こちらのドキュメントではプラグインのインストールに関するリンク先が英語のみですが、 こちら にほぼ同様のものの日本語版があります。 以下にドキュメントの各セクションについて補足の説明を記載します。 ECS Exec を使用するための考慮事項 このセクションでは特に以下の点に注意すれば良いでしょう ECS Exec は linux でしか動作しない 既存のタスクは変更できない(設定後に新しく配置されたタスクから有効になる) コマンドは root からのものになる デフォルトのセッションタイムアウトは 20 分 ECS Exec を使用するための前提条件 aws cli は余程古いバージョンを使っていない限り問題にならないでしょう。 前述の通り、プラグインのインストールが必要になります。aws cli のセットアップ後、 ドキュメント に従って各自の環境に合わせたプラグインをインストールしましょう。 Fargate のバージョンも、最近作られたサービスなら問題にならないと思いますが、2021/03/19 より以前に作られたものの場合、注意が必要です。 ECS Exec の有効化と使用 最低限 IAM の設定とサービス(ないしタスク)の設定をすれば ECS Exec を使用できるようになります。 対象がサービスの場合、設定だけでなく、タスクの再配置が必要になります。ウェブコンソールからの場合、"サービスの更新"から"新しいデプロイの強制"を有効にすると設定を変えずにタスクを再配置することができます。 現時点ではウェブコンソールからはサービスやタスクについて、ECS Execを有効にするための機能は提供されていません。ドキュメントに従い、--enable-execute-command 引数を使って aws cli から設定する必要があります。 aws ecs execute-command で SessionManagerPlugin is not found というエラーが出る場合、セッションマネージャープラグインが認識されていません。初回の設定後、シェルやマシンの再起動などが必要な場合があります。
アバター
こんにちは。MAMADAYSバックエンドチームのsa9sha9です。最近Diablo3にハマりました。 MAMADAYSでは検索基盤としてElasticsearch(以下ES)を利用していますが、時たま再起動を実施したいケースがあります。 本記事では、ゼロダウンタイムでのESの再起動を実現するための注意点を実際のフローに沿ってまとめたいと思います。 MAMADAYSのアーキテクチャについては以前のTechBlogをご参照ください。 tech.every.tv おことわり 本記事でご紹介する手順については必ずしもご自身の環境とマッチするか保証しかねます。 バージョンごとの差異については、しっかりと公式ドキュメントにてご確認ください。 安直に再起動ができない理由 ESを利用する場合には複数台のクラスタ構成にするのが常かと思いますが、ESクラスタを安直に再起動してしまうとダウンタイムが発生してしまいます。 検索機能が主機能なサービスの場合には致命的な障害となってしまうでしょう。 MAMADAYSでは3台のノードでクラスタを構成していますが、それぞれのノードを1台ずつ再起動することでゼロダウンタイムでの再起動を目指します。 再起動の準備体操 1. ヘルスチェック 必ずはじめにヘルスチェックを行いましょう。 ここでステータスが yellow / red だった場合 は必ず settings や shards の状態を確認し、 green ステータスにしてから再起動に臨みましょう。 GET _cat/health // 結果 epoch timestamp cluster status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent 1645172581 08:23:01 mamadays-es-cluster green 3 3 xxx xxx 0 0 0 0 - 100.0% 2. ノード一覧の確認 現時点でどのノードが master node になっているかを確認しましょう。 そして再起動する際には、 master node は最後に再起動しましょう。 というのも master node が停止した場合、別のノードが master node に成り代わるわけですが、最初に master node を停止すると master node の移動が最低でも2回行われてしまいます。 master node の再起動を最後に行うことで master node の移動は必ず1回になるので、余計な移動を避けられます。 GET _cat/nodes?v // 結果 ip heap.percent ram.percent cpu load_1m load_5m load_15m node.role master name 192.168.1.95 37 88 1 0.11 0.07 0.02 dilm - ip-192-168-1-95 192.168.2.238 27 89 1 0.00 0.00 0.00 dilm - ip-192-168-2-238 192.168.3.72 24 89 1 0.04 0.01 0.00 dilm * ip-192-168-3-72 3. インデックスの状態を確認 再起動後にデータの欠落がないか確認するため、 docs.count を控えておきましょう。 GET _cat/indices/index_01?v // 結果 health status index uuid pri rep docs.count docs.deleted store.size pri.store.size green open index_01 xxxxxxxxxxxxxxxxxxxxxx 3 1 x 0 x.xmb xxx.xkb 4. 自動アロケーション機能を無効化 ESは replica shards の存在が確認できないと、別ノードに新たな replica shards が作成されます。ノードの再起動をかけた際に一時的に replica shards が確認できなくなるためこの処理が実行されます。 ただし、すぐに複製を開始するわけではなく、 デフォルトでは1分間待機 してから複製を開始します。 再起動だけなのでほぼ1分以内にノードは復旧するはずですが、何らかの理由で1分を超えてしまうと不要な複製処理が行われ膨大なI/Oが発生してしまうため、 作業中は自動複製を停止 しておきましょう。 ただし、 primaries を指定して primary shards を他のノードへ再配置することは許可しておきましょう。 PUT _cluster/settings { "persistent": { "cluster.routing.allocation.enable": "primaries" } } // 結果 { "acknowledged" : true, "persistent" : { "cluster" : { "routing" : { "allocation" : { "enable" : "primaries" } } } }, "transient" : { } } 5. 機械学習機能が有効になっている場合は停止 もし機械学習機能を使っているなら、一時的に停止しましょう。 MAMADAYSでは使っていないのでこの手順はスキップします。 これで再起動準備は整いました! いざ、再起動 1. 各ノードに入ってプロセスを再起動 sudo systemctl restart elasticsearch.service 必須ではありませんが、この時に他の生きているESノードに1sごとに _cat/shards APIでシャード状態を確認すると、 primary shard の再配置の動きが肌で感じられてとても良いです。 2. (1つ目のノードの再起動を行った後に) 試しにヘルスチェック 1つ目のノードを再起動したタイミングでヘルスチェックを行うと、ステータスが yellow になっているかと思います。 これは replica shards が一時的に切り離されたことによるもので、検索機能に影響はありません。 GET _cat/health // 結果 epoch timestamp cluster status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent 1645170501 07:48:21 mamadays-es-cluster yellow 3 3 xxx xxx 0 0 xxx 0 - 66.7% 3. (全ノードの再起動を行った後に) ノードとインデックスの確認 master nodeの位置が変わっているはずです。 GET _cat/nodes?v // 結果 ip heap.percent ram.percent cpu load_1m load_5m load_15m node.role master name 192.168.1.95 37 88 1 0.11 0.07 0.02 dilm * ip-192-168-1-95 192.168.2.238 27 89 1 0.00 0.00 0.00 dilm - ip-192-168-2-238 192.168.3.72 24 89 1 0.04 0.01 0.00 dilm - ip-192-168-3-72 GET _cat/indices/index_01?v // 結果 health status index uuid pri rep docs.count docs.deleted store.size pri.store.size green open index_01 xxxxxxxxxxxxxxxxxxxxxx 3 1 x 0 x.xmb xxx.xkb 4. 後処理 自動アロケーション機能を有効化 PUT _cluster/settings { "persistent": { "cluster.routing.allocation.enable": null } } // 結果 { "acknowledged" : true, "persistent" : { }, "transient" : { } } 5. (必要なら) 機械学習機能を有効化 MAMADAYSでは使っていないのでスキップ。 6. 各方面のチェック WebやAppから検索機能が使えるかどうかなどを確認しましょう。 これにて、ESクラスタの再起動は完了です。 困った点 ロードバランサーが再起動中のESノードにも疎通させてしまい一定の確率で接続できなくなる MAMADAYSではESの前面にロードバランサーを置いているのですが、ロードバランサーはESノードの死活状態を即時に検知しないため、ESノードの再起動の如何にかかわらず一定の確率で疎通させてしまいます。 ESノードの再起動中はもちろん応答ができないためエラーを返してしまい、結果としてダウンタイムが発生することになります。 そのため、ロードバランサーのターゲットグループから再起動させるESノードを予め除外し、再起動中は疎通させないようにしておく必要があります。 kibanaのアクセス先のESノードが落ちると、その間だけ状態確認ができなくなる 再起動中にも _nodes APIなどで状態を確認したい場合は、curl で他のESノードのAPIを呼びましょう。 理想はkibanaが自動で障害検知して他のESノードへ接続してくれると良いんですが、どうにもそれができなかったので今回はやむなく上記の方法で対処しました。 Sniffing がそういった機能を有しているらしいのですが、うまく動作せず今回は見送りました。詳しい方がいればぜひ入社して欲しいです。 切り戻しについて 万が一何らかの障害が発生して、インデックスデータなどを失ってしまった場合に備えてsnapshotを取っておきましょう。 本記事では詳しい説明は省略しますが、バージョン違いによる互換性などは必ず確認することをお勧めします。 Snapshot and restore | Elasticsearch Guide [8.0] | Elastic 最後に 本来はこのような トイル は自動化すべきなのですが、本件が緊急対応ということもあって手動で行うことになりました。 今後は、誰でも簡単かつ迅速かつ安全に実施できるようにAnsible化を行おうと考えています。 参考文献 https://www.elastic.co/guide/en/elasticsearch/reference/current/restart-cluster.html#restart-cluster-rolling
アバター
少し遅くなりましたが、あけましておめでとうございます。エブリーのCTO今井です。 早速ですが、2021年の振り返りと今年の抱負についてお話しできればと思います。 2021年の振り返り 2021年は開発本部に属する全開発部の部長が入れ替わり、 僕自身もDELISH KITCHEN開発部長となり、そして10月にはCTOに就任することとなりました。 会社としてもPMV(パーパス・ミッション・バリュー)を定めるなど、次のステップに向けて足場を整え、 深くしゃがんだ1年だったように思います。 個人としては、未知の領域がさらに増えパンクしそうになりながらも、 チームの仲間のサポートもあり、過去一番成長した1年だったと思います。 今後の抱負 DataUtilization 全社をあげてDataUtilization(データ活用)を推進しています。 これはデータを活用できる人材を増やすことと、活用可能なデータを増やすことの両面が必要だと考えます。 開発本部ではこの両面を支援すべく尽力していきたいと思います。 具体的には非エンジニアのSQLの習得、より利用しやすいデータプラットフォームの構築、 AI・機械学習の推進などはその第一歩になると考えています。 事業にこだわる開発組織へ 僕が大切にしている価値観の1つに エンジニアが事業を引っ張る というのがあります。 当たり前ですが、事業が求める成果を最短・最速で実現することが、もちろんエンジニアにも求められます。 これに愚直にこだわれる組織にしていきたいと思います。 開発をしていると目の前の機能実装に追われ、この機能が事業を伸ばす上で本当に必要なのか、 もっと良い手段がないのか、それを考えるのをおろそかにしてしまうこともあるかもしれません。 時には最新の技術よりも既に涸れてる技術を選択することもあるし、 コストを払ってスピードを優先することもコスト削減のために地道な開発をすることもあると思います。 その時々で最適な選択は事業に一番貢献することだということに立ち返って、 開発を進められるようになって欲しいと思います。 技術的な挑戦の推進 事業にこだわる一方で、新しい技術やよりチャレンジングな挑戦をしていかないと衰退していくのがエンジニアだと思います。 僕個人も打算的である程度想定できる範囲で開発をすることよりも、 今まで使ったことない、未知の技術に触れながら開発する方がワクワクします。 それはエンジニアの働きがいなどにつながることもあれば、 長い目で見て生産性の向上や、採用にもつながることも大きいと考えています。 目の前の事業貢献と天秤にかけた時に、どちらを取るのかは大変難しい判断になることもありますが、 日々事業貢献を考えているエンジニアが必要と判断した場合に、積極的に挑戦できるよう、 周囲へ理解してもらうための説明やそのための予算取りなどで推進していけたらと思います。 まとめ 上記では足りないことも多々あるとは思いますし、 自分自身、CTOになったばかりでこの考えが数ヶ月後には変わっていることがあるかもしれませんが、 それも成長だと思いますし、日々変化の多いこの業界だからこそだとも思います。 エブリーでは常に仲間を募集しています。 少しでも共感する部分があった方、または自分ならもっとこう推進できるのにと思った方、 まずはカジュアルに面談からでもお待ちしております! コーポレートサイト リクルートページ エブリー公式オウンドメディア
アバター
はじめに はじめまして。DELISH KITCHEN 開発部でバックエンド開発を担当している池と申します。2021 年 9 月にエブリーに転職してバックエンドエンジニアとして働いています。入社して 3 ヶ月ですがサーバーサイド、フロントエンド、クラウド、CI/CD など多岐に渡る技術領域を触ることができ、とても有意義な毎日を送っています。 今回はこれまでに触ってきた技術の中から Datadog APM を試した際の内容についてご紹介したいと思います。 Datadog APM とは ご存知の方も多いとは思いますが、Datadog は SaaS 型運用監視サービスです。様々なプラットフォームにおけるホストの監視、アプリケーション監視、ログ蓄積などシステム監視全般を Datadog 一つで行うことができます。その中で APM(Application Performance Management)は、名前の通りアプリケーションのパフォーマンスを監視する機能になります。 詳細は後述しますが、Datadog APM は分散トレーシングという監視の手法を用いており、マイクロサービスのような分散したアーキテクチャのフロントエンドからデータベースまで、エンドツーエンドのアプリケーション監視を行うことができます。 下記は具体的に計測できるメトリクスの一例です。 時系列でのリクエスト数・レイテンシー、エラー数 エンドポイント毎のリクエスト数・レイテンシー、エラー率 各リクエストの処理時間・ボトルネック 分散トレーシングについて 分散トレーシングは分散されたアーキテクチャのアプリケーションを監視するための手法です。 マイクロサービスのような複数システムから構成されるアーキテクチャでは、複数のサービスをまたいで処理が動くため、全体の振る舞いを把握することが難しいという課題があります。 また、障害発生時に原因を特定することも困難です。 分散トレーシングの手法を用いることで、サービス間をまたいだ処理の計測や可視化が可能になり、それら課題の解決に繋がります。 簡単に用語を説明します。分散トレーシングの主要な用語としてトレース(Trace)とスパン(Span)があります。 ※ 公式の APM 用語集 から抜粋 用語 意味 トレース(Trace) トレースは、アプリケーションがリクエストを処理するのに費やした時間と、このリクエストのステータスを追跡するために使用されます。各トレースは、一つまたは複数のスパンで構成されます。 スパン(Span) スパンは、特定の期間における分散システムの論理的な作業単位を表します。複数のスパンでトレースが構成されます。 例えば、API サーバが一つのリクエストを受け取ってからレスポンスを返却する一連の処理を計測するとします。よくある API では、クライアントからリクエストが送られてきた後、DB や外部サーバなど複数のサービスと連携してデータを処理し、ビジネスロジックを経由して最終的にレスポンスが返却されると思います。 それらをトレースとスパンを用いて表すと次の図のようになります。各サービスにおける処理の単位がスパンで表され、複数のスパンで構成される一連の全体処理がトレースで表されます。 実装方法 前提 今回は下記の前提のもと、 Datadog APM の導入および、一つの API リクエストを受け取ってからレスポンスを返却するまでのトレース計測の実装を行います。 Go で実装されている API サーバでトレース処理を実装する Web フレームワークには echo を利用 API サーバは AWS ECS で管理 次の図は今回取り扱う全体のイメージ図です。Server へ Datadog Agent を導入し、API アプリケーション本体へトレース計測の処理を実装します。 datadog-agent の導入 datadog-agent は計測対象の API が動作しているサーバーで起動される必要があります。公式の datadog-agent コンテナを AWS ECS のタスク定義から起動することで、簡単に導入することができます。 次のようにコンテナの定義をタスク定義に設定します。詳細なセットアップ方法は 公式ページ を参照ください。 トレースの実装 Datadog APM の trace ライブラリを導入します。 go get gopkg.in/DataDog/dd-trace-go.v1/... トレースエージェントにホスト設定を伝えます。 import ( "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" // traceライブラリ追加 ... ) func main() { ... resp, err := http.Get( "http://169.254.169.254/latest/meta-data/local-ipv4" ) bodyBytes, err := ioutil.ReadAll(resp.Body) host := string (bodyBytes) if err == nil { //set the output of the curl command to the DD_Agent_host env os.Setenv( "DD_AGENT_HOST" , host) // tell the trace agent the host setting tracer.Start(tracer.WithAgentAddr(host)) defer tracer.Stop() } 次にスパンを実装します。 スパンの実装には Datadog が公式でサポートしている専用の統合ライブラリを利用します。この専用の統合ライブラリは、一般的に広く用いられている Go の Web フレームワークや、データストア、ライブラリを Datadog APM に統合するために作られているライブラリで、それら Web フレームワークやライブラリと互換性を持っています。(一覧は こちら を参照ください。) 今回の例では、echo と olivere/elastic ライブラリを専用の統合ライブラリに置き換えます。 echo を専用の統合ライブラリに置き換える import ( ddEcho "gopkg.in/DataDog/dd-trace-go.v1/contrib/labstack/echo" //追加 "github.com/labstack/echo" ... ) func main() { ... e := echo.New() e.Use(ddEcho.Middleware(ddEcho.WithServiceName( "echo-service-name-test" ))) 専用の統合ライブラリを import し、datadog echo で準備されている Middleware を echo.Use することで統合することができます。 統合することで、datadog echo によって API リクエスト処理のスパンが計測されて datadog-agent に送られ、Datadog の UI 上で可視化されるようになります。 datadog echoで計測したスパンのFlameGraph olivere/elastic を専用の統合ライブラリに置き換える 次に olivere/elastic を専用の統合ライブラリに置き換えます。 import () elastictrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/olivere/elastic" ... ) func NewElasticSample() *ElasticSampleImpl { tc := elastictrace.NewHTTPClient(elastictrace.WithServiceName( "my-es-service-test" )) cli, _ := elastic.NewSimpleClient( ... elastic.SetHttpClient(tc), ) echo と olivere/elastic を同じトレースとして認識させるためには、同じ Context を利用する必要があります。 今回は簡易的に特定の elastic の呼び出し時に echo の Context を渡すように実装して動作を確かめました。 // SampleMethod func (s *ElasticSampleImpl) SampleMethod(c echo.Context) { svc := s.client.Search().Index( "Sample" ) ctx = c.Request().Context() result, err := svc.Do(ctx) ... 以上の実装により、各サービス(echo、elasticsearch)の処理時間が計測されるようになりました。 サービス毎の処理時間 また、ここに示すのは一部ですが、様々なパフォーマンスデータを UI 上で確認することができ、それらのデータに対してアラート設定や、リアルタイム検索の機能なども実行可能です。 エンドポイント毎のリクエスト数 時系列毎のリクエスト数・レイテンシー レイテンシー分布 エンドポイント毎の計測結果 時系列でのリクエスト数・レイテンシー レイテンシー分布 Continuous Profiler の導入 次に Datadog Continuous Profiler という機能を試します。この機能はアプリケーションの性能をプロファイリングする機能です。 APM と Continuous Profiler の双方を有効化することで、Code Hotspots という機能によりコードベースでパフォーマンスのボトルネックを特定することができると記載があったため、試してみました。 APM 分散型トレーシングと Continuous Profiler の双方が有効化されたアプリケーションプロセスは自動的にリンクされるため、Code Hotspots タブでスパン情報からプロファイリングデータを直接開き、パフォーマンスの問題に関連する特定のコード行を見つけることができます。 ※ 引用元: https://docs.datadoghq.com/ja/tracing/profiler/connect_traces_and_profiles/ Go では下記のプロファイルタイプがサポートされています。(詳細は こちら を参照ください。) CPU Time Allocations Allocated Memory Heap Live Objects Heap Live Size Mutex Block Goroutines プロファイリングの有効化は、次のように数行で実装できます。 import ( "gopkg.in/DataDog/dd-trace-go.v1/profiler" ... ) func main() { ... if err := profiler.Start( profiler.WithService( "profiler-service-name" ), profiler.WithEnv( "profiler-env-test" ), profiler.WithProfileTypes( profiler.CPUProfile, profiler.HeapProfile, // The profiles below are disabled by // default to keep overhead low, but // can be enabled as needed. // profiler.BlockProfile, // profiler.MutexProfile, // profiler.GoroutineProfile, ), ); err != nil { log.Fatal(err) } defer profiler.Stop() 上記の実装を行うだけで、プロファイリング結果が Datadog の UI 上で可視化されます。 時系列での CPU Time 上位 10 件 CPU Time の FrameGraph Code Hotspots 残念ながら Code Hotspots 機能はまだ Go に対応していませんでした。 Code Hotspots 未対応(2021/10時点) dd-trace-go の github を見ると Code Hotspots に関する PR が出ているので開発中であることがわかります。 アップデートに期待します。 料金 / サンプリング 最後に料金とサンプリングについてご紹介します。 (※2021/10 時点での料金体系です。) 料金 公式の 料金ページ から最小料金と加算費用を抜粋しました。 年払い 最小料金(1 ホスト、1 か月あたり) $31(オンデマンド払いは$36) プラス料金:スパンの取り込み APM ホストあたり 150GB (すべての APM ホスト平均)無料、その後 $0.10 /GB プラス料金:スパンの保存 APM ホストあたり Indexed Span 100 万件 (すべての APM ホスト平均)、その後 ・保存期間 7 日、$1.27 / 100 万スパン / 月 (年払いまたはオンデマンド払いで $1.91) ・保存期間 15 日、$1.70 / 100 万スパン / 月 (年払いまたはオンデマンド払いで $2.55) ・保存期間 30 日、$2.50 / 100 万スパン / 月 (年払いまたはオンデマンド払いで $3.75) プラス料金:アドオン AWS Fargate $2 / タスク 簡単にですが、以下の仮定をもとにざっくりと料金を試算します。 仮定 月の総リクエスト数:1 億リクエスト(1 日あたり約 333 万リクエスト) ホスト数:10 ホスト 1 リクエスト:3 スパン スパンの保存期間:7 日 年払い スパンの取り込みは試算が難しいため除く AWS Fargate アドオンのプラス料金は掛からない $1 = 113 円換算 料金試算 最小料金:$31 × 10 ホスト = $310 スパン数:1 億リクエスト × 3 スパン = 3 億スパン スパンの保存料金:(3 億スパン - 100 万スパン) ÷ 100 万スパン × $1.27 ≒ $380 総料金:$310 + $380 = $690 ≒ 77,970 円 本来はここに、スパンの取り込みに応じたプラス料金と、もし利用があれば AWS Fargate アドオンのプラス料金が加算されます。 この試算のように全トレースデータを取り込んで保存すると大きな費用がかかるため、実際には一部のデータを取り込むようにサンプリングすることで費用を抑えつつ運用する形が現実的だと考えられます。 サンプリング サンプリングは次の図の「Your instrumented applications」と「Intelligent retention & custum filters」の 2 箇所で設定することができます。前者はサーバで設定する Datadog に送るトレースのサンプリング設定です。後者は送られてきたトレースに対して Datadog の UI 上で設定するトレース保存のフィルター設定になります。 Datadog にトレースが保存されるまでのフロー ※引用元: https://docs.datadoghq.com/ja/tracing/trace_retention_and_ingestion 本記事ではサーバ側のサンプリング設定についてご紹介します。 サーバ側のサンプリングは、デフォルト設定で 50 トレース / 1 秒まで 100%のトレースを取り込まれる設定になっています。超えた分は Datadog Agent によって自動的に選択/削除されて Datadog へ送られます。 このデフォルトのサンプリング率は、下記の環境変数を設定することで変更できます。 DD_TRACE_SAMPLE_RATE = 1.0 基本的には DD_TRACE_SAMPLE_RATE に基づいて Datadog Agent が自動的にサンプリングしてくれるのですが、Span に MANUAL_KEEP 、 MANUAL_DROP タグを追加することで、優先的に 100%保持・削除するように設定できます。 // 100%削除 span.SetTag(ext.ManualDrop, true ) // 100%保持 span.SetTag(ext.ManualKeep, true ) ここまでがサーバ側で設定できるサンプリング設定になります。 最終的には Datadog の UI 上で設定するフィルターを通して保存されるトレースが決定されるので、そちらの詳細は こちら を参照ください。 まとめ 今回 Datadog APM を触ってみて、公式のドキュメントが豊富だったため、大きく迷わずに導入を進めることができました。一方、既に運用されている複数のサービスにおいて導入・整備を進める作業にコストがかかることもわかりました。特に、サービス間のスパンを紐付けるために同一の Context を用いる設計にする必要があることや、専用の統合ライブラリに未対応のライブラリも多くあることから、既存システムへの導入の難しさを感じました。 しかし、本記事では紹介できていない機能もとても数多くあり、導入して環境整備さえできてしまえば、Datadog 一つにシステム監視ツールを統一でき、相応のメリットがあると思います。 また、CodeHotspots が Go に対応されれば、コードベースのパフォーマンス分析が可能になってメリットも増えるため、アップデート情報を待ちたいと思います。 最後まで閲覧いただきありがとうございました。少しでも参考になれば幸いです。
アバター
はじめに こんにちは。MAMADAYSでAndroidアプリの開発を担当している高野です。 UIはXMLで作成したほうが楽なのではないかと思い、まだ Jetpack Composeを触ったことがなかったのでチュートリアルに沿って進めながら体験してみたいと思います。 ※Jetpack Compose は Android の UI を構築するための新しいツールキットです。2021年7月にバージョン 1.0 をリリースし、チュートリアルのページも用意されました。 チュートリアルでやること チュートリアル では4つのレッスンを通して次のことを学ぶことができるようです。 要素の追加 プレビュー方法 複数要素のレイアウト デザインテーマの適用 リストの実装 アニメーションの実装 環境の準備 現在使っているAndroid Studioをそのまま使用します。 Android Studio Arctic Fox 2020.3.1 Patch 3 プロジェクトは新規で作成し、テンプレートは Empty Activity を使用します。 Empty Activity のプロジェクトでJetpack Composeを使用できるように セットアップの例 に合わせてgradleファイルを修正します。 build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { classpath "com.android.tools.build:gradle:7.0.3" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.21" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files app/build.gradle @@ -15,7 +15,9 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - + buildFeatures { + compose true + } buildTypes { release { minifyEnabled false @@ -28,6 +30,11 @@ android { } kotlinOptions { jvmTarget = '1.8' + useIR = true + } + composeOptions { + kotlinCompilerVersion '1.4.21' + kotlinCompilerExtensionVersion '1.0.0-alpha10' } } @@ -37,7 +44,17 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'com.google.android.material:material:1.4.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.1' + + implementation 'androidx.compose.ui:ui:1.0.0-alpha10' + implementation 'androidx.compose.ui:ui-tooling:1.0.0-alpha10' + implementation 'androidx.compose.foundation:foundation:1.0.0-alpha10' + implementation 'androidx.compose.material:material:1.0.0-alpha10' + implementation 'androidx.compose.material:material-icons-core:1.0.0-alpha10' + implementation 'androidx.compose.material:material-icons-extended:1.0.0-alpha10' + implementation 'androidx.compose.runtime:runtime-livedata:1.0.0-alpha10' + testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.0.0-alpha10' } この後の流れはチュートリアルの内容をなぞるだけなので実装については割愛します。 チュートリアルを終えて チュートリアルはとてもよくできていて、つまずくポイントはほとんどありませんでした。強いて言えば、Lesson3のマテリアルデザインを使用するための ComposeTutorialTheme って何だろう…?という部分ぐらいです。一通り終えるため、今回は Theme と入力した時にサジェストされた MaterialTheme という記述で代用しました。 Lesson1からLesson4までを通して次のような成果物ができました。 少しカスタマイズしてみる チュートリアルのサンプルはチャット画面のようなレイアウトになっています。サンプルのままだと一人でチャットしているようで寂しいので、登場人物を二人にしてみたいと思います。 イメージとしてはこんな感じです。 Message の修正 データクラスMessageをsealed classにしてMe/Youを追加します。 sealed class Message { abstract val author: String abstract val body: String data class Me( override val author: String , override val body: String ) : Message() data class You( override val author: String , override val body: String ) : Message() } サンプルデータ サンプルデータは好みで修正します。 object SampleData { // Sample conversation data val conversationSample = listOf( Message.You( "Colleague" , "Test...Test...Test..." ), Message.Me( "Me" , "List of Android versions: \n " + ... 幅一杯に広げる ModifierクラスにfillMaxWidthという関数があるので追加します。 @Composable fun MessageCard(msg: Message) { Row( modifier = Modifier .padding(all = 8 .dp) .fillMaxWidth() 位置の調整 MessageCardを含むRowの定義を見てみます。 @Composable inline fun Row( modifier: Modifier = Modifier, horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, verticalAlignment: Alignment.Vertical = Alignment.Top, content: @Composable RowScope.() -> Unit ) { ... horizontalArrangement という引数があります。この引数に Arrangement の値を渡すことで位置の調整ができるようです。 horizontalArrangement を指定する際に引数msgの型を判定して位置を調整します。 @Composable fun MessageCard(msg: Message) { Row( modifier = Modifier .padding(all = 8 .dp) .fillMaxWidth(), horizontalArrangement = when (msg) { is Message.Me -> Arrangement.End is Message.You -> Arrangement.Start } 合わせて、アイコンや名前・メッセージも修正を加えます。名前とメッセージはColumnの中なので、Columnの定義をみてみます。 inline fun Column( modifier: Modifier = Modifier, verticalArrangement: Arrangement.Vertical = Arrangement.Top, horizontalAlignment: Alignment.Horizontal = Alignment.Start, content: @Composable ColumnScope.() -> Unit ) { ... Columnの場合は horizontalAlignment で Alignment を指定するようですね。ついでに surfaceColor も修正しています。 if (msg is Message.You) { Image( painter = painterResource(R.drawable.ic_launcher_foreground), modifier = Modifier .size( 40 .dp) .clip(CircleShape) .border( 1.5 .dp, MaterialTheme.colors.secondary, CircleShape) ) Spacer(modifier = Modifier.width( 8 .dp)) } ... val surfaceColor: Color by animateAsState( if (isExpanded) MaterialTheme.colors.primary else when (msg) { is Message.Me -> MaterialTheme.colors.surface is Message.You -> MaterialTheme.colors.secondary }, ) Column( modifier = Modifier.clickable { isExpanded = ! isExpanded }, horizontalAlignment = when (msg) { is Message.Me -> Alignment.End is Message.You -> Alignment.Start } ) { ... } if (msg is Message.Me) { Spacer(modifier = Modifier.width( 8 .dp)) Image( painter = painterResource(R.drawable.ic_launcher_foreground), modifier = Modifier .size( 40 .dp) .clip(CircleShape) .border( 1.5 .dp, MaterialTheme.colors.secondary, CircleShape) ) } エミュレータで確認 チャット画面らしくなりましたね。 最後に Composeでの実装は想像していたよりずっと簡単でした。特に、リストがたった5行で実装できることに驚きました。RecyclerViewと比べるととても簡単ですよね。 まだComposeに慣れていないためもどかしさは感じましたが、簡単なアプリをいくつか作っていくうちに感覚的につかめそうだなといった印象です。
アバター
はじめに こんにちは、DELISH KITCHEN開発部でバックエンド開発を担当している高木です。 DELISH KITCHENのリテールソリューションズ事業部(以下、RS事業部)が、小売向けに展開している店頭サイネージの管理画面等の開発をしています。 DELISH KITCHEN RS事業部で提供している管理画面は、 Angular というWebフレームワークを用いて開発されています 1 。 画面のレイアウト(Flexbox, Grid CSS + mediaQuery)には、Angular公式である @angular/flex-layout というライブラリを使用していたのですが、 開発が活発ではないのと、ずっとベータの状態というのもあり、他の方法を模索していました。 そんな時に最近、 tailwindcss というCSSフレームワークが話題になっていたのと、@angular/flex-layoutと同じようにHTML上でレイアウトが記述できることから、試しに置き換えた話をします。 tailwindcssとは flex、pt-4、text-center、rotate-90などのCSSクラスが集まったutility-first CSSフレームワークです。下記のように、用意されているutilityクラスをHTML上で組み合わせることによってデザインしていくことが出来ます。 <div class="flex flex-row gap-2 p-4"> <div class="text-blue-600 text-xl">Hello,</div> <div class="text-green-600 text-2xl">World</div> </div> 導入 Angular CLI v11.2 からtailwindcssが公式サポートされたので、導入は簡単です。 まずはtailwindcssをプロジェクトに追加します。 npm install tailwindcss インストールが終わったら、設定ファイルを生成します。 npx tailwindcss init 初期設定のままでは不都合が多いので、下記の項目を設定しています。 purge tailwindcssで用意されている全クラスが含まれてCSSのサイズが肥大化してしまので、使用しているクラス以外は除外する設定 prefix クラス名が他のCSSフレームワークやアプリ固有のクラスと被らないための設定 screens レスポンシブ対応のための設定で、ここではモバイルとそれ以外で分けている important tailwindcssのクラスを常に優先させるための設定 module.exports = { prefix: 'tw-', purge: { content: ['./apps/**/*.{html,ts,css,scss}', './libs/**/*.{html,ts,css,scss}'] }, darkMode: false, // or 'media' or 'class' theme: { extend: {}, screens: { xs: { max: '599px' }, 'gt-xs': { min: '600px' } } }, variants: { extend: {} }, plugins: [], important: true }; 最後にstyles.scssに下記を追加します。 @import 'tailwindcss/components'; @import 'tailwindcss/utilities'; 公式ドキュメントでは tailwindcss/base も追加していますが、これは標準タグのスタイルに変更が入ってしまうので、抜いています。 以上で導入は完了したので、次は実際に@angular/flex-layoutの部分をtailwindcssに置き換えていきます。 変換表 @angular/flex-layoutは、tailwindcssと同じくHTML上でレイアウトを記述するので移行は簡単でした。 プロジェクトで使用していたAPIのみ、下記に変換表を載せておきます。 @angular/flex-layout tailwindcss <div fxLayout="column"> <div class="tw-flex tw-flex-col"> <div fxLayout="row"> <div class="tw-flex tw-flex-row"> <div fxLayout.xs="column" fxLayout.gt-xs="row"> <div class="tw-flex xs:tw-flex-col gt-xs:tw-flex-row"> <div fxLayout="row wrap"> <div class="tw-flex tw-flex-wrap"> <div ... fxLayoutGap="16px"> <div class="... tw-gap-4"> <div ... fxLayoutAlign="start center"> <div class="... tw-items-center"> <div fxFlexFill> <div class="tw-w-full"> まとめ @angular/flex-layoutからtailwindcssにレイアウト部分の記述を移行しました。Angularが公式にtailwindcssをサポートしているのと、 どちらも似たような書き方で記述できることから、移行は比較的に簡単に出来ました。 今回tailwindcssを使用してみて、用意されているクラス名を組み合わせていくことで、クラス名を考える必要がなくなり、 CSSサイズの削減にも繋がるメリットを感じられたので、レイアウト以外のスタイルもtailwindcssに乗り換えていくことを検討中です。 Nxを使ってnpm projectをmonorepo管理した話 ↩
アバター
はじめに こんにちは、DELISH KITCHENのiOSアプリ開発をしている山口です。 今年のWWDC21でiPadのSwift Playgroundsを使ってアプリ製作ができるようになるというアナウンスがありました。本当はそれを試そうと思ったのですが、執筆時点だとまだPlaygroundsが対応していないようなので、今回は前段として、iPadのPlaygroundsでSwiftUIを使って簡単な動くものを作ろうと思います。 WWDC 2021 そもそも今までXcodeでの開発はやっているもののMac・iPadどちらのPlaygroundsもまともに使ったことがなく、またSwiftUIもちゃんと使ったことがない状態からのスタートになります。 使用端末は、iPadPro 11インチ(2018)です。 実作に作ってみる あまりデザインなどは考えずに、カップラーメンタイマーを作ろうと思います。 カップラーメンの種類によっても時間が違ってくるので、3分、5分のようにデフォルトでいくつか設定できるのと、すこし硬めに麺を作りたい時もあると思うので、自分で時間を設定できるようにしようと思います。 1. プロジェクトをつくる 左上の新規作成マークを押すと新しいプロジェクトファイルができました。 プロジェクト新規作成後 そもそもSwiftという言語を学ぶためのアプリということもあり、Page(キャプチャーのようなものを作って)ステップごとに学んでいけるようになっているようです。 Source Code部分は走らせた時に自動実行されるMainと、モジュールという構成になっています。 今回は既存のプロジェクトを参考に使っていきます。 2. Mainを書く SwiftUIのViewを UIHostingController に渡してそれをPlaygroundのLiveViewに渡せば表示はできるようになるみたいです。 UIHostingController(rootView: xxx) PlaygroundPage.current.liveView = yyy 3. SwiftUIで画面をつくる とりあえず、こんな感じに書きました。 ちなみにモジュール内で定義しているのでMainで読むためにPublicにさせられます。 public var body : some View { VStack { Spacer() HStack(spacing : 24 ) { ForEach(model.preset, id : \. self ) { time in Button(action : { setTime(time : time ) }) { Text( " \( time ) m" ) .foregroundColor(selectedTime == time ? Color.black : Color.white ) .font(.largeTitle) } .padding(. init (top : 8 , leading : 8 , bottom : 8 , trailing : 8 )) .background(selectedTime == time ? Color.yellow : Color.gray ) .cornerRadius( 8.0 ) } Button(action : { model.alertRelay.send(( "Input new time" , true )) }) { Text( "+" ) .foregroundColor(Color.white) .font(.largeTitle) } .padding(. init (top : 8 , leading : 16 , bottom : 8 , trailing : 16 )) .background(Color.gray) .cornerRadius( 8.0 ) } Spacer() Text( " \( seconds ) s" ) .foregroundColor(Color.white) .font(.system(size : 64 , weight : .bold)) .fontWeight(.bold) Spacer() Button(action : { isPlay ? stopTimer() : startTimer () }) { Text(isPlay ? "Stop" : "Start" ) .foregroundColor(Color.black) .font(.largeTitle) .fontWeight(.bold) } .padding(. init (top : 8 , leading : 32 , bottom : 8 , trailing : 32 )) .background(isPlay ? Color.yellow : Color.white ) .cornerRadius( 8.0 ) Spacer() } } ただ単にVStackとHStackを組み合わせて要素を羅列しただけなのですが、Spacerが良い感じに間を取ってくれていて、それっぽいデザインになりました。 SwiftUIで実装した画面 ボタンを押した時の色の変更などは、SwiftUIの @State や @ObservedObject を使用すると、値の変更を自動検知して再描画してくれます。 public struct ContentView : View { @ObservedObject var model : TimerModel @State private var seconds : Int = 0 @State private var isPlay : Bool = false @State private var timer : Timer? = nil @State private var selectedTime : Int = 0 さて、Viewが組み立てられたので、あとはStartをタップした時に再生するようにして、Stopした時に一時停止するようにすればタイマーの完成です。 3. ロジックを書く タップした時に、 Timer.scheduledTimer() を使って1秒間隔で処理を実行させれば設定した時間分カウントダウンしてくれます。 timer = Timer.scheduledTimer(withTimeInterval : 1 , repeats : true , block : { _ in guard seconds > 0 else { model.alertRelay.send(( "Done" , false )) setTime(time : selectedTime ) stopTimer() return } seconds -= 1 }) あとは、適当にModelを定義してプリセットの時間をいくつか持てるようにしてあげます。 final public class TimerModel : ObservableObject { @Published var preset : [ Int ] = [ 1 , 3 , 5 ] ... } ただカウントダウンするだけだと終わった時に気づかない可能性があるため、ダイアログを出すようにします。 今回は、Combineを使ってSwiftUI側からMain側に知らせるようにしました。 ついでに、ユーザがプリセットの時間を作る時もAlertのダイアログで指定できるようにしました。 let cancellable = model.alertRelay.sink{ [unowned vc] (title, isInputEnable) in guard ! title.isEmpty else { return } let alertVC = UIAlertController(title : title , message : "" , preferredStyle : .alert) if isInputEnable { alertVC.addTextField() } alertVC.addAction(UIAlertAction(title : "OK" , style : . default ) { _ in guard isInputEnable else { return } guard let value = Int(alertVC.textFields?[ 0 ].text ?? "" ) else { model.alertRelay.send(( "Invalid time input" , false )) return } model.addPresetTime(time : value ) }) vc.present(alertVC, animated : true ) } これで全て動くようになりました! カウントダウン 新しい時間をセット 注意点 これは何かの相性なのかデフォルトの挙動なのかわからないのですが、プレビュー画面の左下にあるメニューの「Enable Results」が有効の状態で、実行するとSwiftUIの再描画がされない現象がありました。 最後に 普段UIKitを使ったUIしか作っていないので、今更ながらかなり新鮮でした。アイテムが規則的に並んでいる画面であれば、UIKitよりもかなり簡単に作れるので良さそうです。 iPadのPlaygrounds自体は、やはりSwiftを学ぶために最適化されていて、現時点だとアプリを作るのは大変そうです、ただMacを買わなくても身近な不満を手軽にアプリを作って解決できるようになるのは良い仕組みだなと感じました。 今回iPadのPlaygroundsのみで作ってみたのですが、最低外部キーボードがあれば、画面は多少小さいもののコーディングできそうでした。また、デスクだけでなくベッドの上などでもコーディングできるか試してみたのですが、意外とできたので将来的にiPadで快適に開発できる可能性も見えました。 以上、ありがとうございました!
アバター
はじめに こんにちは。株式会社エブリーでデータサイエンティストをしている伊藤です。 『DELISH KITCHEN』では、サービスをより良くするため、新機能の開発や既存機能・デザインの改善など様々な施策が行われています。 これらの施策は、一部のユーザのみを対象とする「A/Bテスト」によってオンライン評価され、その効果が認められてからユーザ全体にリリースされます。 直近、A/Bテストの信頼性・アジリティをより高めるため、データチームが主導となり新しくA/Bテスト基盤を構築・導入しました。 本記事では、新しく導入したA/Bテスト基盤の概観を紹介させていただきます。 今回紹介するA/Bテスト基盤の活用については、少し前の記事でも紹介していただいているので、そちらも是非合わせてご覧ください。 tech.every.tv これまでの課題 これまで、A/Bテストは各運営チームが主導となって実施されてきましたが、改めて運用体制を見直すといくつかの課題点がありました。 ここでは特に ユーザグループの選び方 評価指標設定と結果の解釈プロセス の2つに関して詳しく解説します。 ユーザグループの選び方 A/Bテストは Randomized Controlled Trial (RCT) とも呼ばれ、何らかの変化(介入)がユーザにもたらす因果効果を評価するための分析手法です。 具体的には、 ランダム なユーザの割り当てによって2つの同質なグループを用意し、その片方のグループ(テスト群)にのみ介入を加え変化を計測します。 2つのグループが介入の有無を除き同質ならば、計測された変化をそのまま介入による効果として扱えますが、注目する介入以外の影響が混入している場合計測された変化にはバイアスがかかり、正確な因果効果が評価できません。 従って、A/Bテストではいかにランダム性の高いユーザ選定を行うかが重要になります。 これまで『DELISH KITCHEN』で実施されていたA/Bテストでは、アプリユーザにランダムに付与されるユーザIDを利用し、その末尾の数字を元に割り当てグループが決められていました。 この方式は、ランダムなユーザIDを元にグループを分けており、また「末尾1のユーザはテスト群にする」など集計時にSQLで表現しやすいというメリットもあるため、一見すると有効な方法だと思えます。 しかしながら、この方式は割り当ての粒度が粗く、運用する上で 同時に実施される別の施策と割り当ての重複が発生しやすい 割り当て対象は施策の担当者が決定する場合が多く、選ばれる番号に偏りが生じやすい といった状況が発生します。 1つ目の状況では、関係のない施策の効果が本来計測したい介入効果のバイアスとなるため、正確な評価が難しくなります。 また、これを回避するために関係者間での調整が発生するため、アジリティの低下に繋がる可能性があります。 2つ目の状況では、過去に実施した施策の影響(キャリーオーバー効果)が特に問題となります。 特定のユーザグループに何度も介入を続けると、そのグループは複数の介入効果を含んだ性質を持つため、一度も介入を受けていないグループとの差分が単一の施策によるものであるという保証が難しくなります。 以上から、よりランダム性の高いユーザ選定を実施するためには、ユーザID末尾を用いない方式が必要だと考えられます。 評価指標設定と結果の解釈 A/Bテストでは、CVRや機能利用回数といったビジネス目標・ユーザ体験を表す評価指標を複数設定し、コントロール群・テスト群の差分から結果を評価します。 そのため、適切な意思決定を行うためには 評価指標が正しく定義され、かつ継続的に正しさが担保されている 結果を解釈・評価するプロセスが整備されている の2点が重要となります。 『DELISH KITCHEN』ではデータ分析のためのBIツールにRedash 1 を採用しており、誰でもSQLを書いてログの分析やダッシュボード作成が可能な環境が整備されています。 A/Bテストについても、担当者それぞれがRedashを使って評価指標設定と結果の解釈を行っていましたが、それ故に 評価指標集計のために書かれたクエリが散逸しており、統一的に管理されていない 結果の解釈の仕方が担当者それぞれの方法に依存してしまっている といった課題も生じていました。 従って、より信頼性の高いA/Bテストの運用体制を実現するためには、これらのプロセス整備も必要だと考えられます。 A/Bテスト基盤方針 要件整理 以上挙げた課題をふまえて、まずA/Bテスト基盤の要件を整理したいと思います。 2017年にMicrosoftから発表された論文では、A/Bテスト環境の成長過程を表現したExperimentation Maturity Modelsが提案されています 2 。 このモデルでは、1つ1つのA/Bテストが単発的に実施される段階から、数多くのA/Bテストが絶え間なく実施される段階までを、Crawl、Walk、Run、Flyの4段階に分類しています。 A/Bテスト基盤での技術的な観点に注目すると、A/Bテストをより効果的かつ効率的に実施するためには、以下のような仕組みが必要であるとわかります。 適切な割り当てグループの作成(検定力分析、A/Aテスト、キャリーオーバー効果の制御) ユーザ体験への悪影響の最小化(アラートシステム、A/Bテストの自動停止、イテレーションの効率化、介入の相互作用の制御と検知) A/Bテストの実施履歴の保存 全体像 Experimentation Maturity Modelsで示されていた技術要件全てをいきなり実現するのは難しいですが、これまでの課題と照らし合わせ、最初の取り組みとして新しいA/Bテスト基盤では次の内容に取り組みました。 評価指標を管理するための評価指標ライブラリの作成 他の施策の影響が含まれないランダムなコントロール群・テスト群の割り当て方式の整備 統計的手法に基づく考察と意思決定が可能なダッシュボードの作成 A/Bテスト基盤全体像 以下では、これら3つのより詳細な内容について紹介したいと思います。 A/Bテスト基盤内容 評価指標を管理するための評価指標ライブラリの作成 評価指標ライブラリでは、A/Bテストに使われる様々な評価指標の登録と管理を行います。 ひとくちに評価指標といっても、その集計プロセスは イベントデータの組み合わせ方といった大枠の集計フロー 集計軸や期間・アプリバージョンなどの集計条件 の2つの要素に分解できます。 例として「アクティブユーザ数」を挙げてみます。 大枠の集計フローは、評価指標自体の性質から「アクセステーブルに記録されたユーザ数」となります。 集計軸や集計条件はユースケースにより変化し、 ユーザ全体について日別で集計する場合 (DAU) は、集計軸を日付に設定 あるA/Bテスト実施期間内のアクティブユーザ数を集計する場合は、集計軸に割り当てられるユーザグループ、集計条件にA/Bテスト対象のユーザや実施期間、対象となるOSなどを設定 というような流れになります。 従って、評価指標を扱う上では、評価指標それぞれに対して大枠のフローを定義し、それに追加条件を付与する形で集計クエリを操作できると、運用上使いやすくなると考えられます。 エブリーのデータ基盤はLakehouseプラットフォーム 3 を採用しており、イベントデータのETL処理は一通りSparkSQLで記述してDatabricks 4 上で実行可能です。 このプラットフォームを活用し、SparkSQL形式で評価指標を登録でき、用途に応じてDatabricksから集計軸などのパラメータを与えてクエリを呼び出せるような、評価指標ライブラリを構築しました。 評価指標集計の流れ 評価指標ライブラリは PythonによるSparkSQLクエリの発行 集計される統計量に応じた評価指標の分類 分類ごとに使用される統計手法(サンプルサイズ計算・仮説検定など)と可視化方式の管理 の3つの機能を持ちます。 1つ目の「PythonによるSparkSQLクエリの発行」は上で述べたような流れを実現する機能で、登録されている評価指標に必要なパラメータを渡すとSparkSQLクエリが文字列として発行されます。 2つ目の「集計される統計量に応じた評価指標の分類」、 3つ目の「分類ごとに使用される統計手法(サンプルサイズ計算・仮説検定など)と可視化方式の管理」は、登録される評価指標をユースケース別に管理するために用意した機能です。 例えば「アクティブユーザ数」は単純にユーザ数をカウントする評価指標ですが、「1人あたりの検索回数」の場合は検索が実行された回数をユーザごとに集計した値の平均値が統計量となります。 集計される統計量が変わると統計処理や可視化の方式も変わるため、これらを対応づけた状態で整理しておくと、他のチームメンバーも統一した形で評価指標を発行・利用できます。 以上のような評価指標ライブラリを活用し、A/Bテスト基盤では評価指標を集約的に管理された状態で運用しています。 他の施策の影響を取り除くランダムなコントロール群・テスト群の割り当て方式の整備 前述のように、A/Bテストではランダムなグループ選定によって同質なコントロール群・テスト群を用意する必要があります。 また、介入による変化で思わぬ悪影響が発生した場合にその影響を最小限に留めるため、割り当てグループは必要十分なユーザ規模であることが望ましいです。 これらを実現するため、新しいABテスト基盤では サンプルサイズ計算 ランダムサンプリング A/Aテスト の3段階による割り当てプロセスを構築しました。 本記事では、特に2つ目の「ランダムサンプリング」について紹介します。 ランダムサンプリング ランダムサンプリングでは、計算されたサンプルサイズを元にユーザをランダムに選定し、コントロール群とテスト群を用意します。 ここで注意すべきは、「同時期に実施されているA/Bテスト」と「過去に実施されたA/Bテスト」の両方の影響を受けないようなグループでなければならない、という点です。 これを満たすような割り当て方式として、今年の3月にSpotifyのテックブログで紹介されていたBucket Reuse方式が目に留まり、エブリーでも実現可能だと判断できたため採用してみました。 ここでは大まかな方針を紹介に留めるため、詳細はSpotifyのテックブログをご覧ください。 engineering.atspotify.com Bucket Reuseでは、何人かのユーザがランダムに所属する バケット というグループ単位を定義します。 ユーザの所属バケットは、アプリのユーザIDをハッシュ化し10進数に変換したものを、バケットの総数Nで割った余りによって算出され、サンプリングもバケットを最小単位として行います。 例えば、バケットあたりのユーザ数が7人だった場合、サンプルサイズ40人のA/Bテストでは6バケット(42人)をサンプリングします。 ここまではユーザID末尾をより拡張したグルーピングと考えられますが、Bucket Reuseの肝はA/BテストをNonexclusive experiments(非排他実験)とExclusive Experiments(排他実験)の2種類に分類し、それぞれに応じたサンプリング方式を選択する点にあります。 非排他実験は、割り当てられたユーザ(バケット)が別のA/Bテストにも同時に割り当て可能なA/Bテストで、サンプリングは他の実験を意識せず バケット全体 から実施されます。 つまり、非排他実験に割り当てられたユーザは、並行して実施されている他の実験にも同時に属する可能性があります。 非排他実験サンプリング例 例えば、「バナーのデザインを変更するA/Bテスト」の実施中に、「ボタンのアイコンを変更するA/Bテスト」を非排他実験として開始したいとすると、割り当て対象のユーザは全体からランダムに選ばれるため、すでに実施中の「バナーA/Bテスト」に割り当てられているユーザの一部は、これから開始する「ボタンA/Bテスト」にも割り当てられる、という状況になります。 一見すると、複数の実験に割り当てられたユーザが存在すると、測定される介入効果にバイアスが含まれるように感じます。 しかし、バケットは全体からランダムにサンプリングされているため、コントロール群・テスト群それぞれに他の実験のバケットが混ざる確率は同等になり、結果としてそれぞれの平均の差を測定する上では、他の実験によるバイアスが抑制されるとみなせます(より厳密には、このサンプリング方式の元で、測定された平均の差による推定量はATEの不偏推定量になるとBucket Reuse記事内の論文で証明されています)。 一方排他実験は、互いに割り当てユーザ(バケット)の重複が発生しないようなA/Bテストを指し、同時期に実施されている 他の排他実験を除いた上で サンプリングを行います。 つまり、排他実験と非排他実験のバケットは重複する可能性がありますが、排他実験同士は重複がないような割り当てとなります。 排他実験サンプリング例 排他実験は、検索画面などの介入の影響が大きい配信面でのA/Bテストで特に重要となります。 例えば、「検索画面のレシピの表示方法を変更するA/Bテスト」の実施中に、「検索結果のソートアルゴリズムを変更するA/Bテスト」を開始したいとします。 これらは検索体験に強く関わる介入であるため、同時に両方の介入を受けたユーザは検索体験が大きく変化してしまうおそれがあります。 従って、この2つのA/Bテストは排他実験に分類し、ユーザは多くともいずれか一方にしか割り当てられないように設定するのが望ましい状態となります。 非排他実験とは異なり、排他実験のサンプリング方式の元で算出された介入効果(平均の差)は、過去に実施された排他実験によって均質にサンプリングされず、バイアスが生じます。 しかしながら、前回の割り当てから十分な空白期間(必要な期間の計算方法も論文中で記載されています)があれば、そのバイアスは緩和可能であると示されており、運用上の工夫で対処可能であると考えられます。 以上紹介したようなBucket Reuse方式による割り当てによって、「同時期に実施されているA/Bテスト」と「過去に実施されたA/Bテスト」による影響の少ない効果測定が可能になりました。 統計的手法に基づく考察と意思決定が可能なダッシュボードの作成 A/Bテストでは、ユーザの確率的な行動によりコントロール群とテスト群との間に多かれ少なかれ差分が出るため、施策の影響が全く無かったとしても差分がゼロになるのは極めて稀です。 このような結果の解釈では、適切な意思決定へ繋げるために「得られている差分が本当に意味のある差分なのか」を慎重に吟味する必要があります。 新しいA/Bテスト基盤では、適切な解釈プロセスを統一化された形で実現するため、統計的仮説検定などの検証手法を組み込んだダッシュボードをRedashで提供し、結果のレポーティングを実施しています。 ダッシュボードでは コントロール群・テスト群それぞれのサンプルサイズと結果 差分の統計的仮説検定と有意な結果のハイライト 差分の信頼区間 を表形式で可視化しています。 ダッシュボード例 可視化処理は、直接Redash上で記述すると管理が難しくなるため、社内向けのPythonライブラリにまとめてRedashサーバにデプロイしておき、Pythonデータソースからimportして使用できるようにしました。 実際の運用では、このダッシュボードを使いながら、運営チームとA/Bテストの結果を確認しつつ「事前に設定していた見込みの効果量と比較して意味ある差分が得られているか」「なぜこのような結果になったのか」といった部分を中心に議論を行っています。 現状と今後の展望 本記事では、新しく導入したA/Bテスト基盤の概観を紹介させていただきました。 導入にあたっては、所属するデータチームのメンバーをはじめ、バックエンド開発チームやクライアント開発チーム、プロダクトマネジャーの方々に多々ご協力をいただきました。 はじめに述べたように、これまでは ユーザグループの選び方 評価指標設定と結果の解釈プロセス といった課題がありましたが、今回の取り組みを通して Bucket Reuse方式による他の施策の影響が抑制されるユーザ選定 評価指標ライブラリによる評価指標の管理と統計的手法を組み込んだダッシュボードを用いた結果の考察 が実現でき、以上の課題は一定以上解消できたと思います。 もちろん、A/Bテスト基盤としてはこれで完成ではなく、より信頼性・安全性の高いA/Bテストが絶え間なく実行可能なRun, Flyフェーズに向けて継続的に改善を続けていきたいと考えています。 お読みいただき、ありがとうございました。 参考文献 最後に、A/Bテスト基盤を作成する上で参考になった書籍・記事をいくつか紹介したいと思います。 Trustworthy Online Controlled Experiments: A Practical Guide to A/B Testing 5 A/Bテストを通じたサービス改善の文化から具体的な方法までが一通りまとめられている書籍です。 Experimentation Maturity Modelsなども紹介されており、A/Bテスト基盤の構想はこの本を足掛かりとして進めました。 最近は 邦訳版 も出版されています。 Spotify's New Experimentation Platform ( Part 1 , Part 2 ) SpotifyがA/Bテストをどのように運用しているかが紹介されています。評価指標ライブラリの設計やダッシュボードの設計で参考になりました。 Spotify's New Experimentation Coordination Strategy 同じくSpotifyの記事で、ユーザ割り当てのBucket Reuseが紹介されています。 Reimagining Experimentation Analysis at Netflix NetflixのA/Bテスト分析基盤について紹介されており、評価指標ライブラリの設計で参考になりました。 サンプルサイズの決め方 6 サンプルサイズの計算方法について、基礎的な部分から体系的にまとめられており、勉強のための良い参考書でした。 効果検証入門 7 A/Bテスト (RCT) とは何か、A/Bテストが実施できない状況ではどのような分析があるのか、などが具体例を通じて紹介されており、実際に施策を進めていく中で活用しやすい書籍だと思います。 https://redash.io/ ↩ Aleksander Fabijan, et al . 2017. The Evolution of Continuous Experimentation in Software Product Development. In ICSE. ↩ Michael Armbrust, et al . 2021. Lakehouse: A New Generation of Open Platforms that Unify Data Warehousing and Advanced Analytics. In CIDR. ↩ https://databricks.com/jp/ ↩ Ron Kohavi et al . 2020. Trustworthy Online Controlled Experiments: A Practical Guide to A/B Testing. Cambridge University Press. ↩ 永田 靖. 2003. サンプルサイズの決め方. 朝倉書店. ↩ 安井 翔太,株式会社ホクソエム. 2020. 効果検証入門〜正しい比較のための因果推論/計量経済学の基礎. 技術評論社. ↩
アバター
はじめに DELISH KITCHEN プレミアムサービスとは 決済システムについて一般的なお話 そもそも決済システムは難しい 決済プラットフォーム別の調査&実装が必要である 決済処理は処理のフローが不安定なケースがある 決済状態と内部システムの同期が必要である 不具合対応やお客様対応の難易度が高くなりやすい 購読の状態が複雑 購読、解約 解約後、再購読 購読後、商品切替(1ヶ月→半年) 分析要件の難しさ なにを見たいか なんの軸で集計するか 過去の状態を判定 プラットフォーム差異 本番での課金テストの辛さ 初回無料に関するテスト 解約済に関するテスト 終わりに 定期購読の難しいところ システム開発部部長の内原です。 今回はバックエンドエンジニア観点で、定期購読(サブスクリプション)を扱う際に問題となるであろう様々なことについてお話しします。 私は現在システム開発部という部署を担当していますが、以前はDELISH KITCHENのプレミアムサービスチームでバックエンドエンジニアとして働いていたので、その経験を元にして実装や運用で難しさを感じたことについて語ります。 はじめに DELISH KITCHEN においてもプレミアムサービスという定期購読サービスを提供しています。 内容は以下のようなものです。 DELISH KITCHEN プレミアムサービスとは 機能面 お気に入り数無制限 人気順検索 プレミアムレシピ(ダイエット、ヘルスケア、美容・健康、作りおき) おまかせ献立(1週間ぶんまるごとで献立を提供) プレミアム検索(糖質オフ、塩分控えめ、などの検索条件を利用可能) 価格面 月480円、半年2,400円、年4,500円 初回登録時は1ヶ月無料(キャンペーン時期により2ヶ月無料、3ヶ月無料) 決済システムについて一般的なお話 DELISH KITCHENに限らず、自社アプリに決済機能を設ける場合は、なんらかの外部サービスが提供する決済システムを利用するケースが殆どではないでしょうか。 自前の決済システムを構築するとなると、技術的難易度が上がることもさることながら、それ以上にセキュリティや法律の観点での考慮が重要になりますので、決済システムそのものがコア技術となるようなサービスを開発するのでない限り、費用対効果を考慮して外部が提供している決済システムを利用することが多いと予想します。 DELISH KITCHENでは、決済システムとして以下を利用しています。 そもそも、iOS/Androidについてはアプリ内でデジタルコンテンツを販売する場合はIAP/IABを利用する必要があるため、事実上の標準となっています。 iOS Apple In App Purchase (IAP) Android Google Play Billing Library (In App Billing / IAB) 携帯キャリア (DoCoMo/au/Softbank)決済。DELISH KITCHEN WEBで利用 外部決済システム ※クレジットカードや銀行口座、コンビニ払いといった支払い方法には現状対応しておりません そもそも決済システムは難しい そもそも、定期購読に限らず消費型(買い切り型)の商品を扱う場合でも、決済システムの構築にはいくつか考慮しなければならない問題があります。 決済プラットフォーム別の調査&実装が必要である 決済プラットフォーム間でいろいろと仕様が異なる部分が存在します。プラットフォーム間での相違から、必要となるデータが得られないというケースもあり、そうすると差分を埋めるために独自に実装が必要になることも多いです。例えば、IAPでは講読更新ごとに講読更新日時が取得できるが、IABだと初回の講読日時しか取得できないといったケースです。 決済処理は処理のフローが不安定なケースがある ユーザ側の操作として、(初回は決済方法の指定が必要、など)いったん別画面に遷移したりすることが多いので、途中でユーザが脱落したり、通信エラーが発生したりする頻度も自然と高くなります。さらに、決済部分はアプリ外の挙動であり、課金ボタンはタップしたがその後脱落というケースが多くても、その原因は分からないということが多いです。プラットフォーム側で決済は完了したが、正しく通達できていないというケースのことです。 決済状態と内部システムの同期が必要である 上記問題に対応するため、購入情報の同期を行う機能が必要になります(アプリ内では購入情報の復元と表現されます)。 不具合対応やお客様対応の難易度が高くなりやすい そもそも不具合が発生すること自体が問題ではあるのですが、金銭的損害が絡まない場合に比べて問題が複雑化&深刻化する傾向が高いです。また、お客様のお問い合わせ対応において返金作業が別途必要になるなど、他の問題と比較してお問い合わせクローズまでの時間が長くなりやすいです。 購読の状態が複雑 定期購読は、未購読、購読中、定期購読中止(未解約)、解約済、といった状態が時系列で存在しているため、いったん確定した購読状態が時間経過によって変化することになります。 よってこれらの状態変化を正しく認識する必要があります。 以下に購読ライフサイクルの例を挙げます。 特にユーザが操作をしていなくても、時間経過によって購読状態が変化する場合があるのが分かります。 購読、解約 - 登録直後 0.5ヶ月後 1ヶ月後 1.5ヶ月後 2ヶ月後 現在 購読中 無料期間 - 更新1回目 - 更新2回目 購読中 購読後解約A 無料期間 - 更新1回目 定期購読中止 解約 解約済 購読後解約B 無料期間 定期購読中止 解約 - - 解約済 解約後、再購読 ※過去に購読→解約を経験しているため、無料期間が存在しない 登録直後 1ヶ月後 1.5ヶ月後 2ヶ月後 6ヶ月後 現在 無料期間 更新1回目 定期購読中止 解約済 更新通算2回目 購読中 購読後、商品切替(1ヶ月→半年) ※購読している商品の購読期間を変更することが可能 登録直後 1ヶ月後 1.5ヶ月後 7ヶ月後 現在 無料期間(1ヶ月) 更新1回目(1ヶ月) 購読切替(半年) 更新2回目(半年) 購読中 分析要件の難しさ 「どのような理由で購読/解約したのか?」、「どのような施策が売上に効いている/効いていないのか?」、「キャンペーン内容は適切であるか否か?」といった様々な観点での分析が必要となります。 前述の通り、購読状態は時間とともに複雑に変化するため、時系列での評価を行わなければならなくなります。 なにを見たいか 課金ページ閲覧数、課金数 新規購読UU 全体購読UU 課金転換率 無料期間によって対象期間は変わる 解約率 購読開始してから何日めか、が重要 なんの軸で集計するか 購読期間別 1ヶ月、6ヶ月、1年 無料期間別 1ヶ月、2ヶ月、3ヶ月、6ヶ月 登録日別 キャンペーンやプロモーションの影響を分析する 訴求内容別 献立、ダイエット、ヘルスケア、など 上記の組み合わせ 過去の状態を判定 分析観点において、特定ユーザのN日前の購読状態を知りたいというニーズがあったとしても、状態遷移としては購読開始、購読更新×N、定期購読中止(未解約)、解約済、という起点があるだけです。単にN日前のデータを見ただけでは判別ができないということになります。 よって、時系列で購読状態を解釈する必要性があります。 プラットフォーム差異 前述したように、プラットフォームごとに提供される機能やデータ種類に差分がありますが、DELISH KITCHENシステムとしてはなるべくプラットフォーム差異を意識しないで済むようにしたいので、これらの差分を吸収する実装を行う必要が出てきます。 とは言え、そもそもの仕様が異なる関係で、完全に吸収することは難しいものも存在します。 例えば以下のように、IAP/IABでは仕様が異なる部分があり、いずれかに合わせる、または代替となるデータを自前で算出する機構を実装するなどの対応が必要になります。 - 価格 無料期間 購読履歴 定期購読中止日 購読商品切替時 IAP 価格帯から選択 期間帯から選択 取得可能 取得不可 現購読完了後に切替 IAB 1円単位で指定 1日単位で指定 取得不可 取得化 即時切替 本番での課金テストの辛さ 検証環境でのテストを十分に行っていたとしても、本番環境での検証を一切しないままだと不安が残ります。 よって、検証環境ほどの作業項目ではないにしても、最低限の動作確認は本番環境でも行っておきたいところです。 ただそれについても困難が伴います。 初回無料に関するテスト 初回無料が有効になるのは、ストアアカウントごとに1回ずつというのがIAP/IABにおける仕様です。 つまり、初回Nカ月無料が本当に正しくシステムで捕捉できているかといったようなテストを行う際は、都度ストアのアカウントを作り直さないといけないということになります。 解約済に関するテスト いったん購読開始した後、定期購読を中止して解約済になった状態でテストする必要が出てきたとします。 しかし、実際に提供している商品の購読期間は1カ月や6カ月といった単位なので、定期購読中止してもその期間内はまだ購読済みのままです。 解約済み状態にするには最低でも1カ月前には準備しておかなければならないということを意味します。 終わりに 定期購読(サブスクリプション)に関する実装、運用の観点からの難しさについて記述しました。 今後の参考になりましたら幸いです。
アバター
はじめに エブリーでは日々大量のデータをDatabricksで処理し、MetabaseなどのBIツールで可視化や分析を行っています。 MetabaseとDatabricksのデータベースを接続する方法がまとまっている記事があまりなかったので、ここにまとめたいと思います。 手順 1.Databricks用のJDBCドライバー入りのMetabaseイメージを作成 2.MetabaseとDatabricksを接続する 1. databricks用のJDBCドライバー入りのMetabaseイメージを作成 こちらのフォークリポジトリを使って、ローカルにDatabricks用のJDBCドライバー入りのMetabaseイメージを作成します。 git clone https://github.com/rajeshkumarravi/metabase-sparksql-databricks-driver.git cd metabase-sparksql-databricks-driver curl -L "https://github.com/rajeshkumarravi/metabase-sparksql-databricks-driver/releases/download/v1.2.0/sparksql-databricks.metabase-driver.jar" -o sparksql-databricks.metabase-driver.jar Dockerfileの一部を修正します。 FROM metabase/metabase:v0.37.0.2 ENV MB_DB_CONNECTION_TIMEOUT_MS=60000 #コメントアウトor削除 #COPY ./target/uberjar/sparksql-databricks.metabase-driver.jar /plugins/ COPY sparksql-databricks.metabase-driver.jar /plugins/ 「metabase-blog」と名前をつけたMetabaseイメージを作成します。 docker build -t metabase-blog . コンテナを作成し、 http://localhost:3000/  にアクセスしてMetabaseの画面が表示されれば成功です。 docker run -d -p 3000:3000 --name metabase metabase-blog 2. MetabaseとDatabricksを接続する Metabaseの管理画面から「データベース」→「データベースを追加」をクリックします。 DatabricksからMetabaseと接続したいクラスターの「configuration」に進み、「Advanced Options」から「JDBC/ODBC」に表示される値を確認します。 これらをMetabaseに入力していきます。 「データベースのタイプ」→ Spark SQL (Databricks) 「ホスト」→ databricks画面の「Server Hostname」 「データベース名」→ default 「ユーザー名」→ token 「パスワード」→ Databricksのアクセストークン 「追加のJDBC接続文字列オプション」→ Databricks画面の「JDBC URL」の <personal-access-token> を1つ上の「パスワード」に置き換えたもの 「簡単なフィルタリングと要約を行うときに自動的にクエリを実行する」→ False 「データベースが大きいため、Metabaseの同期とスキャンのタイミングを選択します」→ True これでデータベースとの接続は完了です。 3. 確認する 最後にDatabricksのサンプルデータベース「default」にあるテーブル「smsdata」にクエリを投げてみます。 ちゃんと接続されているようです。 以上です。最後まで閲覧いただきありがとうございました。
アバター
はじめに はじめまして。最近創立されたSite Reliability Engineering(SRE)チームに所属している吉田です。 今回は「カンファレンススタッフ」についてお話しようと思います。 ”カンファレンススタッフをやってみたいけど、まだまで全然プログラミングできないから貢献できないかも…”という方がいらっしゃったらぜひ読んでいただければと思います! カンファレンススタッフって何? 名の通りカンファレンスのスタッフをします。 といっても何をしてるの?という疑問が出てくると思うので、実際に僕がメインで参加しているPyConJPを例にご紹介します。 (全部はリストアップできないので一部です webサイトの構築 タイムテーブル作成 プロポーザル グッズ・ノベルティ作成 宣伝 予算組み スポンサー様とのやりとり 進行の台本作成 企画組み keynote決定 通訳業者様とのやりとり ロゴデザイン チケット設計 会場下見・決定 託児所決め etc… パッと見てわかった人もいらっしゃるかもしれませんが、Pythonを使わないタスクが多いです。Pythonどころかプログラミングの知識が必要なタスクは意外とありません。 もちろんPythonの知識は生かせないというわけではなく、keynoteやプロポーザルなどではPythonの専門知識が必要となります。 お伝えしたいのは「その言語に対しての知識が無いからカンファレンススタッフは無理か」 とはならない ということです。これはPyConJPだけではなく他カンファレンス様でも同様のケースが多いと感じています。 ( photo by PyConJP ) モチベーション では何故カンファレンススタッフを始めたか、続けているモチベーションは何かという話です。 IT業界に還元したい 僕の場合はプログラマーとして日々インターネットで様々な情報をインプットしています。インプットというのは誰かがアウトプットしているから成り立つものです。 それ以外にも開発でOSSを使用していますが、OSSも湧いて出てくる訳もなく誰かが時間を割いてコミットしてくれたものです。 「その行為に対して何か還元できないか」という気持ちは昔からあったのですが、当時は自分の技術的にOSSコミットは難しいな…アウトプットできるほどの知識も無いな…と思っていてなかなか踏み出せずにいました。しかしたまたまスタッフ募集のお知らせを見かけて自分にもきっとなにかできることはあるやろという精神でエイッと入ったのがカンファレンススタッフになった経緯となります。 実際に入ってみたら本当にプログラミング1mmもわからんという人でもタスクが山程あったので結果オーライでした。 面白い人に出会える カンファレンススタッフには様々な会社・業種・レイヤー・国籍の人がいます。 個人的にはNOC(Network Operations Center)というタスクを中心に行うことがあり、そこで知り合った方たちとカンファレンス以外でも交流が続き、界隈の情報交換など色々な話が聞け非常に有意義な時間になっています。 これは小話なんですが、カンファレンススタッフの中には複数のカンファレンスを跨いでスタッフをしてる人も少なくないので、交流を持つことで他のカンファレンスのスタッフ求人を紹介してもらって数珠つなぎに色んなカンファレンススタッフに参加しやすくなることが多々あります。僕自身も複数のカンファレンスのスタッフをNOC繋がりでやったこともあり、それぞれのカンファレンス同士でスタッフが手伝いする循環が発生することがあります。 ( photo by PyConJP ) 市場価値向上 これは入った後に気づいたのですが、カンファレンススタッフをやること自体が個人の実績として有用であり1エンジニアとして市場価値が付与されるというものがあります。 上記にも記載したとおりコミュニティへの貢献という文脈として社内外の評価が上がることがあります。 自分の場合はたまたまメディアスポンサー様のご協力の元カンファレンスレポートを執筆し、無事執筆実績の解除を行うことがありました。感謝します! 自分のペースで進められる/お互いリスペクトしあえる 実際にカンファレンススタッフになってみて一番思うことはカンファレンススタッフというのはあくまで有志のボランティアなので、プライベートの時間に合わせて作業が進められるという点です。 学生は学業が、社会人には仕事があるので時には振られたタスクを締め切りまでにこなすことが難しいときがありますが、そういった時でもキチンと「ちょっと期限までには厳しいです」と言うだけで手が空いている人が率先して手伝いや引き継ぎを行ってくれることが多く、非常に心理的に助かる面があります。 これは互いが本業を持っていること・プライベートの時間を割いていることを自覚していてリスペクトしあっているからこそ為せることだと思っています。 逆に本業が早めに終わったりしてプライベートの空き時間が開いてる時は残ってるタスクや、手一杯になっている他の人の手助けを積極的に行える環境が整っていることが多く無理なく自分のプライベートの時間に合わせて無理なく進めることができます。 スタッフTシャツがもらえる 寝間着が増えます まとめ "カンファレンススタッフ"と聞いて腰が重かった方もいらっしゃると思いますが本当にカジュアルにプライベートのスキマ時間を使ってコミュニティに貢献ができます。 「コミュニティ貢献はしたいけどそれでもカンファレンススタッフはまだちょっと…」という方も大丈夫です。コミュニティの貢献は色んな方法があり、これは一例です。財団への寄付やSNSでイベントの宣伝、その言語やフレームワークを使うこと自体で十分貢献できてきます。 イベントをスタッフとして作り上げるのは非常に達成感があります。ぜひ一緒にカンファレンス・コミュニティを盛り上げていきましょう! ( photo by PyConJP ) P.S. そして何故このタイミングでこういった投稿をしたかと言うと、僕が参加しているPyConJP2021が10月15・16日と開催されます! https://2021.pycon.jp/ 更に更にPyConJPではサポートスタッフを募集中です! PyCon JP 2021 サポートスタッフ募集開始のお知らせ
アバター
# Apache AirflowのPoCをした話 はじめに 弊社『DELISH KITCHEN』のデータプラットフォーム上では、日々発生するデータをLakehouseプラットフォームに集約しており、Databricks上で処理される多数のETLジョブが存在しています。しかし、現在利用しているジョブ管理ツールでは、Databricksのジョブ同士の依存関係を細かく設定することが出来ず、実行ジョブが肥大化してしまう問題があります。 これらを適切な粒度で依存関係を設定出来るようにするため、DAGによるワークフロー定義が可能なApache Airflowを導入しました。その際に行ったPoCでの所感をお話します。 Lakehouseプラットフォームについてはこちらの記事で紹介されています。 Delta LakeとLakehouseプラットフォームによるデータウェアハウス設計 現状 Databricks Jobsによる管理 弊社ではデータ分析基盤として主に Databricks を利用しており、ETLバッチジョブの多くを Databricks Jobs を用いて管理・運用しています。 ETLバッチジョブの各タスクの依存関係や実行順を定義するワークフローは、DatabricksのNotebook上で定義されます。これらのジョブのスケージュール、アラートなどの設定はDatabricks JobsのWeb UI上から行っています。 これらのジョブは次のサンプルコードのように、Notebook上で定義して処理されます。 // Bigqueryからイベントの生ログを転送し、イベント毎に保存するジョブ // task01,02が順次実行される dbutils.notebook.run( "./bronze/01_ExtractEventDataFromBQ" ) dbutils.notebook.run( "./silver/02_TransferAppEventToDelta" ) 次の図のように、Web UI上からスケージュールなどの設定をします。 Databricks Jobsスケジュール設定画面 実際のアプリケーションのジョブでは、以下のようなデータ処理が実行されています。 BigQuery上のApp/Webのイベントの生ログを転送後、イベント毎に分解して保存 S3上に保存されるサーバーログをイベント毎に保存 各事業部のKPIを計算 Databricks Jobsによるジョブ管理の問題点 これらのDatabricks Notebookで処理されるジョブには、ジョブ間の依存関係を設定できない問題が存在します。この問題は、処理方式がCRONとなっており、時間制御によるジョブスケジュール管理ツールのため、他のジョブの状態を考慮していないことから発生しています。 ジョブの完了をトリガーとしたジョブの実行ができず、トリガーとなるジョブのワークフロー内に後続ジョブの実行を定義する必要があります。 ジョブのワークフロー内に別のジョブ実行を定義することは、本来ジョブが持つ関心事を曖昧にし、データワークフローの見通しが悪くなるだけでなく、データワークフロー管理の観点やジョブ肥大化の観点などにおける様々な問題を引き起こします。 例えば以下のような3つのジョブがあるとします。 アプリのログを保存する アプリ内検索のKPIを計算する アプリ内課金のKPIを計算する 検索、課金のKPIを計算するジョブは、アプリログを保存するジョブの完了を実行条件として要求します。 3つのジョブの流れを図に起こすと次のようになります。 3つのジョブのワークフロー 各ジョブには論理的な繋がりがなく、関心事毎に個別のジョブとして定義・管理されている状態が望ましいですが、Databricks Jobsでは依存関係の設定ができないため、本来は個別に定義されるべきジョブが1つのジョブとして定義されてしまいます。 例えば、アプリログを保存するジョブの完了をトリガーに、各KPIを計算するジョブの実行が不可能なため、1つのジョブ内でそれぞれのジョブを定義することになります。 次の図はそれぞれのジョブが同一のノートブック内に1つのジョブとして定義されている状態を表します。 依存関係を設定できない場合のワークフロー 単一のジョブとして定義された各ジョブは、順次実行されるため実行時間が長くなるだけでなく、ジョブ毎に要求される適切な計算資源の割当ができません。そのため最も重い処理に要求される計算資源を長時間使い続けることになります。大きな計算資源を長時間使用することは、ジョブの運用に要求されるコストを増加させるため望ましくありません。 例えば、KPIの計算はアプリログを保存するジョブと比較して計算資源を要求しませんが、ジョブをまとめて定義してしまったことで、必要な計算資源よりも大きな計算資源が割り当てられてしまい、不必要なコストの増加が起こります。 このように、関心事によって適切な粒度でジョブを分割し定義されていない状態は以下のような問題を引き起こします。 ジョブの肥大化による実行時間の長期化 ジョブの計算量に応じた適切な計算資源の分配ができない 上記2つに起因するコストの増加 将来的なワークフロー変更に対応できない データワークフローの見通しの悪化 Apache Airflowの導入 ジョブ間の依存関係を設定できない問題を解決するため、 Apache Airflow を導入します。 Apache Airflowはジョブ間の依存関係を設定できるだけではなく、GCP、AWS、Databricksなどの弊社で利用している様々なサービスへのタスク実行をサポートしているため、検討することにしました。 Apache AirflowではETLバッチジョブのワークフローを DAG(有向非巡回グラフ) として、1つのPythonファイルで定義します。これらのジョブのスケジュール、アラートなどの設定もDAGを定義したPython ファイル内で定義され、ワークフローだけではなくジョブの設定を含めたコードベースでの管理が可能です。 次のサンプルコードのように、Databricks上のジョブを実行するDAGを定義できます。 # need pip install airflow==2.0.2, apache-airflow[databricks]==2.0.2 import datetime from airflow import DAG from airflow.providers.databricks.operators.databricks import DatabricksRunNowOperator, DatabricksSubmitRunOperator from airflow.utils.dates import days_ago cluster_settings = {} with DAG( # アラート、スケジュールを設定 dag_id= 'example_databricks_jobs' , default_args={ 'owner' : 'admin' , 'depends_on_past' : False , 'email_on_failure' : False , 'email_on_retry' : False , 'retries' : 2 , 'retry_delay' : datetime.timedelta(seconds= 10 ) }, schedule_interval= '@hourly' , start_date=days_ago( 2 ) ) as dag: task1 = DatabricksRunNowOperator( task_id= 'run_now_operator' , job_id= '65' , # databricks > jobs > job ID notebook_params={ 'env' : 'prd' } ) task2 = DatabricksSubmitRunOperator( task_id= 'submit_run_operator' , json={ 'notebook_task' : { 'notebook_path' : 'Product/bronze/01_ExtractEventDataFromBQ' , 'base_parameters' : { 'env' : 'prd' } }, 'new_cluster' : cluster_settings } ) # ワークフローを定義 task1 >> task2 定義したDAGによるワークフローは、ダッシュボード内のGraph Viewによって可視化できます。データワークフローが可視化されることは、依存関係の素早い把握に繋がり、多数のジョブの運用を助けます。 次の画像は上記のDAGによるワークフローが可視化されたもの表しています。 airflowによるワークフローの可視化 ジョブ間の依存関係の考慮 Apache Airflowでは、 ExternalTaskSensor モジュールという他のDAGに定義されているタスクの実行結果(成功/失敗)を検知するモジュールを使用することで ジョブ間(DAG間)の依存関係を設定できます。 このモジュールを後続として実行したいDAGの最初のタスクとして定義すると、アプリログを保存するジョブの終了後にKPIを計算するジョブを実行するなどのジョブの依存関係を設定したワークフローを表現できます。ジョブの実行結果を検知するタスクを後続のワークフロー内に定義することで、トリガーとなるジョブ内に後続ジョブの実行を定義する必要がなくなります。 ジョブのワークフロー内に別のジョブ実行を定義する必要がなくなったことは、ジョブが持つ関心事を明確にし、データワークフロー管理の観点やジョブ運用の観点などに様々な利点をもたらします。 次の図のようにジョブを個別のDAGとして定義できます。 依存関係を設定したワークフロー ジョブを個別に定義することで、並行実行による実行時間の短縮や、ジョブごとの適切な計算資源の割当により、不必要な計算資源を長時間使い続けることがなくなります。 必要な計算資源を必要な時間だけ使用することにより、ジョブの運用に要求されるコストが最適化されます。 例えば、アプリのログを保存するジョブには大きな計算資源を、各KPIを計算するジョブには小さな計算資源を割り当て、KPI計算を並行して実行することで、ジョブ全体での実行時間の短縮と計算資源の適切な割当により運用コストを抑えることができます。 このように関心事が分離され、適切な粒度でジョブを分割することで以下のような恩恵を受けます。 ジョブの計算量に応じた適切な計算資源配分 上記によるコスト削減 ジョブの並列実行によるワークフロー全体で見た実行時間の短縮 ワークフロー変更に対する柔軟性 データワークフローの見通しの好転 最後に 以上、ETLバッチジョブの管理ツールとしてApache AirflowをPoCした話でした。 Apache Airflowを導入したことで、ジョブ間の依存関係を考慮したワークフロー定義が可能になり、適切なジョブの定義によりで様々な恩恵を得ることができました。 ジョブを関心事毎に適切な粒度で定義することで、各ジョブの見通しがよくなる、コストが最適化されるなどの恩恵を享受でき、多数のETLバッチジョブを長期に渡る管理・運用を可能とすることが期待されます。 参考 Databricks Jobs | Databricks on AWS Apache Airflow Apache Airflow Concepts 有向非巡回グラフ - Wikipedia
アバター
はじめに はじめまして。 2021年2月から、インターンとしてデータ&AIチームでデータエンジニア業務に携わっている金安です。 入社からの約半年間、データに関わる多種多様なタスクを経験させていただきました。 ここではエブリーでのデータ分析の様子を紹介するとともに、業務を通して学んだことを整理しようと思います。 いきさつ 私は大学で情報処理技術・人工知能技術について勉強しており、アカデミックな研究の道と、ものづくりに携わるエンジニアとで進路に迷っていました。 そのような中で、何となく参加した逆求人イベントでエブリーのCTOとお話しする機会があり、インターンでエンジニアとして就業することになりました。 プログラミング歴も浅く、実務経験も全くない私を採用していただき感謝しています。 現在はエブリーで週2日業務に従事しながら、大学で量子熱力学・量子情報科学分野を研究しています。 熱力学に登場するエントロピーと情報理論におけるエントロピーは本質的に等価であることからもわかるように、両者の間には接点があります。 これに注目し、情報理論の知見を物理学に持ち込むことで、非平衡熱力学など未完成の理論の発展に貢献する、というのが当分野の一つの目標です。 私の研究は、近年発見された「熱力学不確定性関係」という不等式にスポットが当てられています。 これはエネルギーと精度の関係を示唆すると解釈することができ、実用的には、量子コンピューターの性能限界の理解に繋がります。 総じて分野横断的な研究であり、情報が物理世界で果たす役割を新たな視点から探ることができます。 エブリーのデータ分析基盤 エブリーでは、毎日大量のデータを集計し、加工・分析して得た知見を施策に反映させるデータ分析の OODAループ が回っています。 OODAは、Observe, Orient, Decide, Act の頭文字で、意思決定プロセスにおける4つのステップにあたります。 第一ステップは観察(Observe)で、データを集計・分析し、事実を集めることに相当します。 これをもとに、仮説構築(Orient)を行います。 これは今後の施策の方向性を決める非常に重要なプロセスです。 仮説を立てたら、具体的にどのような施策を行うかを決定(Decide)します。 最後に施策を実行(Act)します。 以上のステップを何度も繰り返すことで、変化し続ける状況の中で、適切な判断を下し、迅速に行動に移すことが可能になります。 これがOODAループです。 このデータによるOODAループを回す根底には、 DIKWモデル が前提としてあります。 DIKWは Data, Information, Knowledge, Wisdom の頭文字で、下図のようなピラミッド構造になっています。 DIKWモデル (出典: The Knowledge Pyramid: A Critique of the DIKW Hierarchy ) 集めてきただけの生のデータ(Data)は、乱雑で整理されていないうえに重複や欠落が含まれている可能性もあり、それだけでは意味を持ちません。 これをクレンジングし、何らかの基準で整理して初めて、情報(Information)として意味を見出すことができるようになります。 さらにここから、特定の観点で情報を凝集させたり、統計的な数量を算出したりすることで、より抽象度の高い知識(Knowledge)や知恵(Wisdom)に昇華させることができます。 これらは施策を行う上での判断の根拠となり、OODAループを回し、データから価値を生み出すことに繋がります。 エブリーでは、これに則ったデータ分析基盤が構築されています。 大量の生データが、Google BigQuery や Amazon S3 といったストレージに集約されます。 これを、databricksというデータ分析プラットフォームを通して加工・分析・凝集し、目的ごとに多様なテーブルを作成しています。 さらにこれをRedashというツールで集計・可視化し、組織の意思決定に役立てています。 最近体験した業務とその振り返り ここでは、私がインターン生として最近取り組んだタスクについて振り返ってみます。 エブリーが様々な施策やサービスを運用する中で、「施策の定量的な検証・可視化をより高速に行いたい」という課題がありました。 これを達成するため、Redash上で高度な統計処理を行い、よりカジュアルに統計情報を処理できる可視化基盤を作成する、という目標が立てられました。 RedashではもともとPythonスクリプトを実行できますが、通常はRedashのコンソール上で直接コードを入力して実装します。 しかしこの方式には、実装コードがGitHubで管理されない、テストが行われていないなどの問題がありました。 そこで、統計処理用のスクリプトをモジュール化したものを、Redashサーバにデプロイすることを検討しました。 このタスクは、直接データに触れるよりは、サービスを運用するインフラ構成の把握が主だったのですが、周辺知識がほぼ0だった私にとっては、次から次へと学びを得られて非常に新鮮でした。 Redashのサーバが構築されているdockerコンテナに触れてみたり、そのサーバからS3上に通信してファイルを受け渡してみたり、Pythonライブラリが0から作られる様子を見てみたりと、新しい体験の連続でした。 特に印象的だったのが、チームリーダーからの「 GitHubでmergeすると、どうしてそのコードが本番環境で動き始めると思う? (意訳)」という言葉です。 私はそこで初めて、GitHub上からサーバにコードをデプロイしている、circleCIというツールの存在を知りました。 確かに言われてみれば、git上でコードのバージョン管理をしても、全然違う場所にあるサービスのコードが勝手に書き換わるわけはないのですが、そんなことは考えたことも疑問に思ったこともありませんでした。 このRedashサーバへのモジュールデプロイは、客観的には特別難易度の高いタスクではないと思いますが、本当に有意義な体験ができたと感じています。 おわりに エブリーでは日々実力不足を痛感させられていますが、同時に成長を実感することもあり、業務は非常に充実しています。 今後ともよろしくお願いします! エブリーでは、エンジニア以外にもインターン生を募集しています。 興味を持たれた方は、 こちら をご覧ください。
アバター
はじめに  はじめまして。2021年4月にエブリーに入社した山西と申します。  データサイエンティストとしてデータ関連部門に所属後、DELISH KITCHENアプリ改善施策のA/Bテストに約3ヶ月間従事してまいりました。  今回はその実業務の中での体験も踏まえ、A/Bテストにおける評価指標の選定プロセスや苦労したポイントなどを紹介していきます。 A/Bテストについて  DELISH KITCHENのアプリでは機能の改善に向けたUIやアプリ内動線等の変更を定期的に実施しております。  このようなアプリ機能の変更はユーザーの体験を多かれ少なかれ変化させるものであるため、ユーザー視点、および事業視点で良い効果が見込めるものを見極めたうえでユーザー全体へ展開するのが望ましいといえます。  そこでエブリーでは、プロダクトマネージャーがアプリ機能変更施策を企画し、データサイエンティストが A/Bテスト によりそれらの効果を事前検証することで、変更を施すべきか否かの意思決定への還元を行っています。   A/Bテスト は、文字通りAパターン(変更前)とBパターン(変更後)の結果を比較する手法です。  「とあるアプリ機能の変更施策」の文脈においては、 一部ユーザー(テスト群)のみに介入した結果の効果を、変更を施さなかったユーザー(コントロール群)と比べて検証する手法 とも言い換えられます。  あらかじめ検証期間を定めて行うのが一般的であり、その期間内でテスト群がコントロール群よりもどの程度良くなったか(または悪くなったか)を評価指標を用いて統計的な見地から判断する、という流れをとります。  以下、その流れを簡潔にですが整理します。 A/Bテストの流れ  プロダクトマネージャー、データサイエンティストが連携しつつ実施することとなります。  データサイエンティストが主体となるのは2~6のフェーズとなります。 A/Bテストの流れ 1. 施策の企画  プロダクトマネージャーが施策の案を企画し、データサイエンティストへ共有、内容の精緻化を行う。 2. 評価指標の選定  施策の効果を測るために事前設定しておく指標を選定する。  ※ とある機能の継続率や訪問回数などのユーザーのエンゲージメントを測る指標や、CTRやCVRなどのビジネス上注視すべき指標が例として挙げられる。 3. 対象ユーザーの抽出  アプリ利用ユーザーをランダムに抽出した後、コントロール群(施策を実施しないグループ)とテスト群(施策を実施するグループ)に分ける。 4. 施策の実施  テスト群のユーザーに対してアプリ機能の変更を適用する。 5. 結果の集計  アプリのログを各評価指標へと集計するSQLクエリを作成。  コントロール群とテスト群それぞれに対して算出した評価指標値の有意差検定や信頼区間の推定を行う。  各集計値の比較結果を表や時系列グラフで表現し、ダッシュボードにまとめる。 6. 結果の報告  プロダクトマネージャーにダッシュボードを共有すると共に結果を報告する。  有意差の有無や効果量の結果レポート、及びそこから得られる考察を述べつつ議論する。 7. 意思決定  A/Bテストの結果を省みて、施策を全ユーザーに適用するか否かの判断、または追加施策の実施の有無について判断を下す。  本記事では以後、2.の評価指標を選定するプロセスについて掘り下げていきます。 評価指標の選定  前述の通り、施策の効果による「良し悪し」をA/Bテストによって見極めるためには、その目的にあった評価指標を設定する必要があります。  エブリーでは現在、以下の指標分類フレームワークをその選定基準として採用しております。 指標の分類  これらの分類は書籍 『A/Bテスト実践ガイド』 にて提唱されているものです。  要点を整理し紹介いたします。 ゴール指標 ビジネスにおける最終的な目標を直接反映した指標。 施策に携わるステークホルダー間で広く受け入れられるようなシンプルなものであるのが望ましい。 ドライバー指標 ユーザー体験等を元にした、ビジネス目標を間接的に表現する指標。 短期的に観察することが出来、その変動が捉えやすいという特徴を持つ。 ゴール指標の代理指標としての役割も担う。 ガードレール指標 施策が問題なく進行しているか確認するための指標 予期せぬバグ等でユーザー体験が悪化してないかを確認する指標  また、優れたオンライン実験を設計するためには各指標が『短期的(実験期間内で)に測定可能であり、計算可能であり、十分に敏感(分析感度)で即時的(即時性)に反応するものでなければならない。』ことが書籍『A/Bテスト実践ガイド』で指摘されています。  つまり、計算するためのログが存在し集計可能であること、それらが有限である検証期間内に測定可能であり、さらにその変化が観察できることを確かめたうえで、ゴール、ドライバー、ガードレールの枠組みに照らし合わせながら評価指標を選んでいくことになります。  このようなA/Bテストに向けた評価指標に求められる観点は、ビジネスでの報告目的で用いられる指標の観点とは一致しないケースが多々あることにも留意する必要があります。 指標の実装  施策の目的と指標分類のフレームワークを照らし合わせながら評価指標を洗い出し、それらが集計可能であること(元となるログが存在していること)を確認した後は、各々の評価指標を集計するためのSQLクエリを作成します。  その後、ダッシュボード上にこれらのクエリを登録し、表やグラフとして可視化します。  このようにして、A/Bテストの結果をダッシュボードを軸に報告、考察および意思決定へ還元する準備が整います。 ダッシュボード可視化の例 大変だったこと  次に、施策の目指す向きをうまく表現する指標を選定したり、それを実装へ落とし込むために元となるログ周りを理解したり、さらにプロダクトマネージャーと連携したりする過程で大変だったことについて紹介します。 指標の選定作業 ゴール指標の代理指標の設定  本来は上記の指標の分類にて挙げたゴール指標で施策の良し悪しの意思決定が出来れば理想ですが、実際には代理指標(指標の分類でいうところのドライバー指標)を設定し、これを主要な指標として観察していく場合も多々あります。  主な理由として以下2点が挙げられます。 ゴール指標が遅行指標(数値として現れるには一定の期間が必要な指標)であり、施策実施期間終了まで、すなわちビジネスにおける意思決定のタイムリミット時点までに十分な観察が行えない。 ゴール指標には関連する因子が多くあることが想定されることから、施策によってその一部に介入を行っただけではほとんど変化しない恐れがある。  この際には代理指標としての妥当性(擬似相関になっていないか、因果関係まで整理できているか)の模索に、多くのリソースを割かなければなりません。 施策の影響範囲を見極めた集計  ひとえに施策の効果を測るといっても、集計対象とするユーザーをどう選ぶかでその意味合いは大きく変わります。  例えば「アプリトップ画面→機能A→機能B→機能C」と遷移するアプリ内動線の中で機能Cに介入するA/Bテストを実施する場合、機能Cを利用したユーザーのみ集計するのか、それより前の動線のユーザーも集計対象に含めるか、を注意深く設定しないと求めたいものと乖離する場面が多々ありました。  求めたいものに応じてどう集計するか、以下2パターンに整理します。 「使った人がどう反応するか」を知りたいとき  機能の使い心地に介入するような施策を打った場合は、その機能が利用されない限り効果を測ることが出来ないため、機能利用実績があるユーザーのみを集計対象とするのがふさわしいといえます。  ユースケースとしては「ユーザーが必ず1回は利用または訪問するであろう機能のデザインに対して介入する施策を打ったとき、2回目以降の利用が伸びるか否か」を判断したいとき等が挙げられます。 「使わなかった人がどれだけ使ってくれるようになったか」も含めて知りたいとき  アプリ内動線の追加や、デザイン変更等で機能の利用を促す施策を打った場合はその機能の獲得傾向に興味があるため、注目する機能を利用していないユーザーも集計対象となります。  このケースではまず施策検証期間内に、アプリトップ画面にアクセスしたA/Bテスト対象ユーザー全体を対象に集計することが多くなります。  しかし、「注目する機能があまり使われていないときに指標を平均値として集計したい」場合は、単なる全アプリアクセスユーザーの平均値を出すと0に非常に近い値が出てしまい意味のある解釈が難しくなってしまいます。  その際には集計対象を「該当機能により近いアプリ内動線を利用したユーザー」に絞ったうえで平均値を算出する場面もありました。  このようにして「どのユーザー集団に対して平均を取り、そこから何を知りたいのか」をイメージしながら意味のある指標に落とし込むにはアプリのUI/UX側の深い理解が必要だと感じました。  アプリ内の機能、動線にどのようなものがあるか把握しておくことはもちろん、各機能のアプリ利用傾向を普段からモニタリングしておくことで、求める分析軸に対して適切な指標を設計する目を養っていきたいと思いました。 ログ周りの理解 ログの仕様および状態の確認  評価指標を実装するためには当然その元となるログの存在が前提となるため、選定時点でそれらの仕様を頭に入れておく必要があります。  また、アプリ内OSの特定のバージョンでのバグ発生等により、ログが汚染され評価指標として正しく集計できなくなっている場合は、事前にその影響を排除する措置を取ったうえで指標をデザインする必要があります。  しかし、複雑なクライアント・サーバーサイドの実装背景を咀嚼したり、大量に存在するアプリ内動線から必要なログを洗い出したり、さらにそれらが正しく記録されているかをモニタリングしたりするのは中々に骨が折れる作業でもあり、入社後しばらくは評価指標一つの集計クエリを書くだけでも多くの時間を費やす日々が続いてしまいました。  こうした経験から、アプリ内のユーザーの動きがログとしてどのように反映されているか、そのログが何処に格納されているか、さらに仕様変更や更新、バグ等にも目を張り巡らせておくこともデータサイエンティストの重要な役割であることを実感いたしました。 実装したい評価指標に対応するログが存在しない場合の対処  もし評価指標の実装に必要なログが存在しない場合は、以下のいずれかの選択肢を取ることになります。 クライアント・サーバーサイドで新たに実装してもらう 代替、近似できるような既存ログを探す  これらのうちどの方向に舵を切るかは、当該指標のビジネス的な重要性、意思決定までのタイムリミットから逆算した施策の実施期間、他部署のリソース等の間のトレードオフ関係の熟慮ののちに各ステークホルダーと相談することによって決定されることとなります。  データ側の視点から、こうした調整を円滑に進めることの難しさも実感しました。 プロダクトマネージャーとの連携 わかりやすく説明する能力   流れの説明 にて解説した通り、A/Bテストの狙いはその分析結果をプロダクトマネージャーへ報告し、そこから意思決定につなげる洞察を得ることです。  それを円滑に行うため、「データサイエンスの見地をわかりやすく、かつ誤解無く説明する」能力がデータサイエンティスト側に求められることとなります。  評価指標を解釈する観点、有意性の解釈の仕方、懸念事項などの多くの情報をどのようにしてまとめれば簡潔にアウトプットできるか、試行錯誤の毎日です。 評価指標についてのすり合わせ   指標の分類 の項でも述べたように、プロダクトマネージャーが追跡するKGIなどのビジネス指標とA/Bテストとして設定する評価指標は必ずしも一致しない場合が多くあります。  例えば、一般的にKGIとして利用されるビジネス指標の多くは短期的に変化を観察するのが困難であり、そのままA/Bテストで追っていくとすると多くの場合 ゴール指標の代理指標の設定 にて説明した懸念に直面するため、この場合はより効果検証にふさわしい代理指標を主に見ていくことになります。  そのため「なぜA/Bテストとしてその評価指標を選定したのか」「どう解釈すれば良いか」等の認識をプロダクトマネージャーとデータサイエンティスト側でしっかりと認識を合わせておく必要があります。 最後に  「コントロール群とテスト群を比較する」というコンセプトだけ聞くと単純明快な印象を受けがちなA/Bテストですが、それを良質な意思決定へつなげるためには一筋縄にはいかない工夫が必要であることをこの数ヶ月間の業務で改めて認識しました。  分析要件とアプリ側のドメイン知識を相互に照らし合わせつつ、試行錯誤も繰り返しながら最適な指標を見定める力を付けていきたいと思います。  最後までお読みいただきありがとうございました。 参考文献 Ron Kohavi,Diane Tang,Ya Xu,大杉 直也.(2021)「A/Bテスト実践ガイド 真のデータドリブンへ至る信用できる実験とは (Japanese Edition) 」 Alex Deng, Xiaolin Shi. Data-Driven Metric Development for Online Controlled Experiments: Seven Lessons Learned. In Proceedings of the 22nd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining, KDD, 2016
アバター