TECH PLAY

株式会社エブリー

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

411

はじめに はじめまして。 データストラテジストの田中です。普段は『DELISH KITCHEN』レシピ視聴実態の可視化やオーディエンス配信のレポート作成、サービス好意度の分析などの業務を行っています。 サービス好意度など定性的な要素が多い分析ではWEBアンケート調査のデータを活用していますが、WEBアンケート調査のローデータは質問内容がカラムとして横持ちで存在することが多いのが特徴です。 今回はデータベースでも扱いやすいよう「Apache Spark環境下で横持ちのデータを縦持ちにする」TIPSをお伝えします。 stack関数を用いて縦持ちにする Spark SQLにてデータを横持ちから縦持ちにするには stack 関数を使用します。 stack関数については、Apacheより公式ドキュメントが提供されていますので、詳細は下記リンクをご覧ください。 Apache公式ドキュメント サンプルデータ 横持ちのサンプルデータとして、アンケートデータに近い形のものを用意しました。 +------+------+---+------+--+--+-----------+--+ |sample|gender|age| area|Q1|Q2| Q3|Q4| +------+------+---+------+--+--+-----------+--+ | AAA| 1| 40| Tokyo| 1| 5| 特になし| 0| | BBB| 2| 15| Shiga| 2| 6| アプリを見て| 1| | CCC| 1| 20| Osaka| 3| 7| 広告を見て| 0| | DDD| 1| 55|Nagoya| 4| 8| null| 1| +------+------+---+------+--+--+-----------+--+ 各ユーザID(sample)毎に付帯情報(gender 〜 area)と質問内容(Q1 〜 Q4)の回答結果が1行ずつ積まれています。今回はユーザIDと付帯情報に紐づく形で、質問内容を縦持ちにしたい場合の実装例を提示いたします。 実装方法 ドキュメント通りですとstack関数は SELECT stack(n, col1, col2 ...) と記述し、「col1, col2 ...」を「n」行で分割するといった仕様になります。 サンプルデータでは SELECT stack(4, Q1, Q2, Q3, Q4) と記述しても良いのですが、実際のケースシナリオを想定した場合、汎用的に使えるよう質問内容のカラム情報を動的に取得できることが望ましいです。汎用性を加味した実装例を以下に提示します。 実装例 import org.apache.spark.sql.functions._ val result = data.columns.filter(x => x.contains("Q")).map{ v => data.select($"sample", $"gender", $"age", $"area",expr(s"stack(1,'$v',$v) as (q_id, q_value)")) }.reduce(_ unionByName _) display(result) 質問内容がスケールすることを考慮し、 columns を使用しカラム名を取得、 filter を使用し質問内容のカラム名のみ抽出を行っています。 抽出したカラム名の値を map にて、1つずつ読み出し、縦積みにしています。 $v のようにカラム名を指定すると回答結果だけが縦積されるため、カラム名も '$v' で取得する形にしています。 最後に質問内容のカラム情報を1つずつ縦積みしたものを reduce(_ unionByName _) で結合しています。 出力結果 各ユーザIDと付帯情報に紐づく、質問内容のカラム名 q_id と回答結果 q_value の一覧を出力することができました。 +------+------+---+------+----+------------+ |sample|gender|age| area|q_id| q_value| +------+------+---+------+----+------------+ | AAA| 1| 40| Tokyo| Q1| 1| | BBB| 2| 15| Shiga| Q1| 2| | CCC| 1| 20| Osaka| Q1| 3| | DDD| 1| 55|Nagoya| Q1| 4| | AAA| 1| 40| Tokyo| Q2| 5| | BBB| 2| 15| Shiga| Q2| 6| | CCC| 1| 20| Osaka| Q2| 7| | DDD| 1| 55|Nagoya| Q2| 8| | AAA| 1| 40| Tokyo| Q3| 特になし| | BBB| 2| 15| Shiga| Q3| アプリを見て| | CCC| 1| 20| Osaka| Q3| 広告を見て| | DDD| 1| 55|Nagoya| Q3| null| | AAA| 1| 40| Tokyo| Q4| 0| | BBB| 2| 15| Shiga| Q4| 1| | CCC| 1| 20| Osaka| Q4| 0| | DDD| 1| 55|Nagoya| Q4| 1| +------+------+---+------+----+------------+ また別のサンプルデータを元に、Databricksのdisplay関数でプロットを作成しました。 縦持ちのテーブルのメリットは下記プロット結果のように質問全体での足し上げがしやすいことと、その他2軸以上のグラフを作成するときにも便利なことです。 サンプルデータ サンプルデータ プロット結果 サンプルプロット 最後に 実装自体はシンプルですが、stack関数の実装例が少ないと感じたため取り上げてみました。 最後まで閲覧いただきありがとうございました。
アバター
はじめまして。DELISH KITCHEN開発部でバックエンド開発等に携わっている南です。 今回は2021年4月の中旬にリリースされた、「DELISH KITCHENチラシの郵便番号・地域名・店舗名検索実装」の裏側をお話したいと思います。 DELISH KITCHEN チラシ 検索エンジンによる、郵便番号・地域名・店舗名検索 DELISH KITCHENチラシにはもともと郵便番号検索機能がありましたが、今回、その郵便番号検索の入力欄に郵便番号・地域名・店舗名、いずれの文字をいれても検索できるよう機能拡張しました。 1つの入力欄で郵便番号・地域名・店舗名検索をできるようにするにあたり、今回はElasticsearchを用いました。 Elasticsearch n-gram vs. 形態素解析 検索エンジンで用いる際は、文字列をどのようにトークン化するかが重要になってきます。 検索エンジンのトークン化といえば半角スペース区切り、形態素解析、n-gramあたりが主流です。 昔の話ですが、カーナビの目的地住所検索ではn-gramを使用していると聞いたことがありました。 そこで最初にn-gramによるトークン化を試してみましたが、本来1位付近に表示したかった店舗が、他の店舗に埋もれてしまうという悪い結果に終わりました。 地域名・店舗名で検索される方は、短いワードで検索することが予想されます。 その短いワードに「海、川、木、山」や「東、西、南、北」など地域名に頻出するワードが含まれていると、大量の結果が返ってきてしまいます。 (昔のカーナビの住所検索は、住所を8-9割入力してようやく絞り込みができたな・・・、ということを思い出しました。) n-gramでは良い検索体験を得られないことが分かったので、DELISH KITCHENチラシの郵便番号・地域名・店舗名検索では 形態素解析 することにしました。 郵便番号・地域名・店舗名検索と形態素解析 まず郵便番号は数字&記号であるため、形態素解析ではなくもっとシンプルな解析器を用いました。これについては後述いたします。 地域名・店舗名は、いずれも日本語の非分かち書きではありますが、文章ではありません。 形態素解析をするというより、辞書を充実させて形態素解析器に名詞判定してもらいトークン化する作戦です。 辞書の優劣が結果の優劣に直結してきます。 郵便番号の解析器 郵便番号検索は日本語を含まないため形態素解析も辞書も不要です。 ただし郵便番号検索では、例えば 「 106-6238 」とハイフン付きの7桁で検索するユーザーと「 106 」と3桁で検索するユーザーへの対応が求められます。 そこで「106-6238」のハイフンを「106 6238」(半角スペース)に置換したのち、半角スペース区切りでトークン化する解析器を用意しました。 indexに [106, 6238] とリストとして情報をもたせておくことで、 106-6238 (106 AND 6238) で検索されても 106 のみで検索されても〒106-6238の店舗を検索結果に含めることができます。 { " settings ": { " analysis ": { " char_filter ": { " hyphen_to_space " : { " type " : " mapping ", " mappings " : [ " -=>% " ] } } , " analyzer ": { " postal_code_analyzer ": { " type ": " custom ", " tokenizer ": " standard ", " char_filter ": [ " hyphen_to_space " ] , " filter ": [ " split_delimiter " ] } } } } } 地名辞書データの作成 エブリーにはデータ分析業務を行っているData&AIチームがあり、地名の読み仮名データを過去に作成していたため、それを活用して地名辞書を作成しました。 ただし漢字一文字の地名は、細かくトークン化されてしまいn-gramのようになってしまう恐れがあるため削除しました。 また長すぎる住所は、辞書の1単語としてふさわしくないためこちらも削除しました。 検索対象となる店舗の住所の大半は市街地になります。「漢字2-5,6文字の地名さえ網羅できればよいだろう」くらいの気持ちで辞書を作成しました。 ... 左曽,左曽,サソ,地域 巨勢,巨勢,コセ,地域 布佐,布佐,フサ,地域 布勢,布勢,フセ,地域 布太,布太,フダ,地域 布施,布施,フセ,地域 布木,布木,フキ,地域 布瀬,布瀬,フゼ,地域 布良,布良,メラ,地域 ... 店舗名辞書データの作成 こればかりは、人力で作成するほかなかったため、DELISH KITCHENのデータベースから店舗名一覧を取り出して1つ1つ読み仮名を振っていきました。 幸いにも膨大な数ではなかったので手作業でこなすことができましたが、単純作業というわけにはいきませんでした。 たとえば店舗名が「エブリー商店」だった場合、「エブリー商店」で検索するユーザーもいれば、「エブリー」のみで検索するユーザーもいるでしょう。 そこで「エブリー」で1単語、「商店」で1単語、辞書作成することにしました。そうすることで「エブリー」でも、「エブリー商店」でも検索できるようになります。 店舗名は凄くユニークな店舗名もあれば、一般名詞や人名の店舗名もあります。1つずつ店舗名を確認し「どうのように検索されるだろうか?」「自分ならどんな検索をするだろうか?」と考えながら辞書作成を行いました。 kuromoji_iteration_mark filterに注意 リリース直前に「代々木」で検索できないという報告があがりました。 これはkuromoji_iteration_markをfilterに設定していたことが原因でした。iteration_markとは踊り字、つまり代々木の「々」を意味します。 kuromoji_iteration_markを設定すると、検索エンジンが踊り字を前の漢字に変換してしまいます。「代々木」で検索すると、「代代木」に変換されます。「代代木」という単語は地域名辞書には存在しないため、「代/代/木」とトークン化されていたのが不具合の原因でした。 地域名辞書に「々」を含む地域がいくつあるか確かめてみたところ170個ほど存在しました。 さほど多くはないのですが、幸運にも「代々木」という有名な地域名があったため気がついてくれた方がいました。安易にkuromoji_iteration_markを使うと辞書とマッチしなくなるため注意しないといけません。どうしてもkuromoji_iteration_markを使わなければならない場合は、辞書に「代々木」と「代代木」の両方を含めないといけません。 ... 久々知,久々知,ククチ,地域 久百々,久百々,クモモ,地域 久野々,久野々,クノノ,地域 代々木,代々木,ヨヨギ,地域 佐々木,佐々木,ササキ,地域 佐々生,佐々生,サソウ,地域 佐々礼,佐々礼,サザレ,地域 ... 最後に 今回は、郵便番号・地域名・店舗名検索公開に至るまでに悩んだことや躓いたことの地道な活動をまとめてみました。 検索結果の良し悪しにゴールはありません。今後、提携する店舗が増えていけば、それに伴った調整も必要になりますし、ユーザーの声にあわせた調整も必要なります。良い検索結果を返し続けるためにも、絶え間なく改善活動を続けていきたいと思っています。
アバター
社内でkubernetesの輪読会を開催しました はじめに 経緯 輪読会とは 利用した書籍 運用 実際やってみて 良かったこと 大変だったこと 最後に 社内でkubernetesの輪読会を開催しました はじめに こんにちはMAMADAYS バックエンド担当エンジニアの宮本です。 今回は私の所属している開発チームでkubernetes(以下k8s)の輪読会を行ったので、その内容を紹介していきます。 MAMADAYSのサービスやバックエンドシステムの全体像については MAMADAYSのサービスとバックエンドシステムのお話 にて紹介していますので、よろしければご覧ください。 経緯 現状MAMADAYSのバックエンドシステムはAWSのEKS上で運用されています。 しかしk8s周りを触っているのが特定のメンバーのみとなっており、チーム内で知識にばらつきがありました。 またそのメンバーも体系的な学習を行っているわけではなく、十分な理解がない状況でもありました。 そのためメンバー内のSREから「チーム全体でk8sへの理解を深める必要があるのではないか」という意見が出され、チーム全員で輪読会を行う流れになりました。 私のいるバックエンドチームはweb開発も行っており、web担当メンバーも自主的に参加し実施されました。 輪読会とは 参加しているメンバーが同じ書籍を事前に読んできて、その内容について意見を交わす会です。 事前に決められた担当者が本の内容を要約し、他のメンバーが理解できるような形で発表を行います。 複数人で同じ書籍をそれぞれの視点から読み解くため、個人では理解が難しい部分をフォローしあうことで、よりメンバー間での知見が深まるようになります。 社外も含めた輪読会は実績もなくハードルが高いため、今回はチームメンバーの知識のベースアップと実際に業務で用いている部分を見比べながら行うことに重きを置くようにしました。 またコロナの感染状況も考慮して、Zoomを使ったオンライン開催となりました。 利用した書籍 インプレス社から出版されている Kubernetes完全ガイド 第2版 を用いて行われました。 Kubernetes完全ガイド 第2版 本書はk8sに関する機能でアプリケーションエンジニアが利用する可能性が高いものを網羅的に解説されており、様々なユースケースが紹介されています。 体系的に説明されていて図による視覚的な理解を得やすく、サンプルが添付されているためこちらを利用することとなりました。 運用 輪読会の進め方は様々ありますが、今回は知識のベースアップが目的のためチームメンバーが週一回入れ替わりで担当。事前に対象となるページを決めて、その内容をスライドにまとめて議論していく方針を取りました。 このときの進行役としては発案者であるSREのメンバーが執り行ってくれました。 全体の流れとしては以下のとおりです。 開催1週間前までに対象となるページと発表者を決める。 各メンバーが対象箇所を読んでおく、また発表者はさらにその箇所をスライドにまとめる。 当日にスライドを使って発表(30分程度)し、残り時間で質疑応答 業務を圧迫しないように、毎週金曜日開催で水曜日の時点で間に合わない場合はスキップも可としています。 緩くですが確実に進めれるようにしました。 実際やってみて 輪読会は週1ペースで全19回にわたり緩く行われ、読破までには6ヶ月弱かかりました。 本書に書かれていることはもちろんためになりましたが、さらに輪読会をする上で得た経験としては以下のとおりです。 良かったこと メンバーによってはベースの知識の差で理解度のブレがありましたが、メンバー内でしっかりと深堀りをしていくことで埋め合わせができました。 チーム全体の知識のベースアップができたのはとても大きかったです。 また書籍の内容については新しい発見もあり、プロダクトで生かせる機能が数多くありました。 例を上げると、Pod起動時のヘルスチェックを Startup Probe で行うことができ早速導入しました。 大変だったこと 書籍のボリュームが多く、読破までの時間がかかってしましました。 ですが、下手に章を飛ばしたりすることなく全体的に学ぶことができました。 また業務や参加メンバーのスケジュールによっては調整が必要でした。 なのであまりかっちりとした予定は組まずに緩く進めるのは重要だったと思います。 最後に 以上、社内でのk8s勉強会の簡単な報告となります。 なんとなく理解している状況は継ぎ接ぎの対応で済ましてしまうため、しっかり時間をとって学ぶことは大変有意義となりました。 k8を新規に採用したり運用している場合は、体系的に学ぶことで今後のユースケースに対応しやすくなると思います。 社内でのk8sの勉強会を検討している方に参考になれば幸いです。
アバター
はじめに はじめましてDELISH KITCHEN Androidエンジニアの友部です。 私は現在、プレミアムチームに所属しており、主にAndroidの課金が関係している施策などを担当しています。 今回はGoogle I/O 2021で発表されたアプリ内購入の新機能について書いていきたいと思います。 DELISH KITCHENのプレミアムサービスについて まず、少しだけDELISH KITCHENの話をさせてください。 DELISH KITCHENではプレミアムサービスとして、有料で利用できる機能やコンテンツを提供しています。主な内容は以下で、1ヶ月のアプリ内購入によるサブスクリプションで販売をしています。プランとしては6ヶ月、1年のものも販売していますが、ひと月あたりの金額が安くなるといったもので、内容は同じものです。 機能 人気ランキング 広告非表示 1週間献立 すべての栄養成分表示 お気に入り無制限 コンテンツ(限定レシピ) ダイエット 作り置き ヘルスケア ベビー 今回発表された新しい販売方法について 今回のGoogle I/Oでは新しく3つのアプリ内購入の販売方法が発表されました。 1つ目が Multi-Quantity Purchases 2つ目が Multi-line Subscriptions そして3つ目が Prepaid Plans です。 Multi-Quantity Purchases 消費型の商品を複数選択し、購入できるようになります。 今まではある商品を1個売り、10個セット売り、20個セット売り…としていたものをユーザーが必要な個数だけ選択し購入できるようになります。ユーザーにとっては嬉しい仕組みになりそうです。 動画の中ではPlay Consoleより設定できるとのことですがまだ項目が表示されていないためもう少し待つ必要がありそうです。現在DELISH KITCHENでは、そのような消費型の商品は存在していないため出番はないかもしれません。 Multi-line Subscriptions 1つのサブスクリプションの一部として、複数のサブスクリプションを販売することができます。 今まで1ユーザーに対して、1つの商品しかサブスクリプションとして提供しないのが主流でしたが(別の商品はアップグレード、ダウングレード扱い)、機能を個別に切り出してユーザーに選択して購入してもらうといったことが可能になります。 例えばDELISH KITCHENの場合なら、限定レシピを分割して提供し、ダイエット、作り置き、ヘルスケア、ベビーをそれぞれをサブスクリプションの商品として販売します。ユーザーは必要なレシピを選択し、購入するといったことができるようになります。 それに加えて、別のジャンルが欲しくなった場合は追加で購入したり、不要になった場合は削除したりできます。 Prepaid Plans 一定期間、ユーザーにコンテンツへのアクセスを提供できるようになります。 ユーザーにプレミアムサービスの一部を切り出して提供することで、プレミアムサービスの価値を感じてもらった上で通常のプレミアムサービスを購入してもらうといったことが可能になります。 具体的には、機能の広告非表示の部分だけを提供したり、限定レシピのうちいくつかだけを提供したりするようなイメージです。 有効期限が切れそうになるとユーザーに通知が届くので、そのタイミングでアップデートしてもらえるようにするのが良さそうです。この Prepaid Plans は Real-time developer notifications や Subscription API などもサポートされます。 最後に 2021/5/18にこれらの発表があり、Billing Library 4.0は提供されたもののこれらの機能自体はまだ使えないようです。 しかし、Googleが公式に提供しているアプリ内課金のサンプルコード( Google Play Billing Samples ) にもこれらの機能を示唆するコメントがコード内に記載されていたので間もなく使えるようになるでしょう。公開されたら実際に使ってみて次の機会にブログに書いていけたらと思います。 最後までお読みいただいてありがとうございました。 参考: Grow your business with new engagement and monetization features | Session
アバター
Core Web Vitals 改善のお話 はじめに まずは計測してみる 弱点を特定する LCP の改善 無駄なリソースの読み込みを除去 http2 への切り替え 画像を適切なサイズで配信 改善したものの数値に影響がなかったもの 巨人の肩に乗る(大切) CLS の改善 CLS の算出アルゴリズムが変わったことで CLS が向上 スコアロジックにも変更あり 改善の結果 まとめ Core Web Vitals 改善のお話 はじめに こんにちは。MAMADAYS Web 担当の櫻井です。 以前のエブリーエンジニアブログ にて Google の Core Web Vitals (以降 CWV ) についてご紹介しました。今回は CWV のパフォーマンス改善について、MAMADAYS の Web チームが実際に行ったこと、またその結果についてをご紹介したいと思います。 ゴールとしては CWV の3指標 FID, LCP, CLS を合格基準にすることと、ベンチマークしているサイトよりも優れた数値に改善する*こととしました。 *Google の公式 FAQ によると、CWV が検索ランキングシグナルとして使われるケースは"tie-breaker"の役割が強いようで、ひとまずは競合の中で上位に入り込むことがページパフォーマンスの恩恵を受ける第一歩となります。 - What is the page experience update and how important is it compared to other ranking signals? まずは計測してみる パフォーマンス改善においては何よりもまず現状を計測してみることに始まります。今回はひとまず Page Speed Insights (以降 PSI )でサイトの状態を確認してみました。 MAMADAYS のとある記事ページを測定した結果、2021/01 時点では以下のようになりました。 なお、結果の見方としては一番上の数字がこの時点で測定した Lab データから算出されるスコアです。直下の「フィールド データ」は現実のユーザの体験をビッグデータとして Google が蓄積したものを反映した数値です。そのため、上のスコアと下のフィールドデータには必ずしも連動しているわけではありません。 CWV は FID, LCP が Good 圏内で、CLS が Bad 圏内。そしてスコアは 17pt。これはベンチマークしているサイトのページと比較してもワースト1位を競う非常に悪いスコアでした。 また Web パフォーマンス改善にあたって、FID, LCP, CLS の各数値については改善の結果をリアルタイムで把握したいため、PSI で表示されるフィールドデータではなくラボデータを取得・蓄積すると良いでしょう。前回の記事で紹介したように MAMADAYS ではラボデータを BigQuery に蓄積し metabase でモニタリングするようにしています。ただし、CLS に関しては正しく値を取得できなかったため、特定の記事の Lab データを毎日取得するスクリプトを作成し、モニタリングを行いました。(API は PSI API を使用) 弱点を特定する CWV の値をある程度把握できたところで、次にどこの改善に着手すべきかを特定します。 MAMADAYS では CWV 合格へ向けて CLS の改善はもちろん、スコアを底上げするために LCP の改善も目指しました。FID はこの時点で最も合格閾値を超過している(100ms)ため、注力しないことを決めました。 具体的な改善アクションを決める際には PSI や Lighthouse が非常に役に立ちます。これらは当該サイトの何がパフォーマンス的に悪いかを親切に文章で教えてくれる機能を有しています。 PSI の結果の画面をスクロールしてみるといくつかの項目で指摘されていました。 Remove unused JavaScript 使用されていない無駄な JavaScript が読み込まれているようです。tree-shaking を適切に行い、デッドコードの除去をする必要があります。また、そもそも使われていない余分なコードをリファクタして削除するなどの整理をすると良さそうです。なお、今回はここの改善は行っていないため説明は割愛しますが、多くの場合ここを改善すれば大きく LCP が下がることが期待できます。 Defer offscreen images 画像の遅延読み込みがされていないようです。ファーストビューに表示されない画像の遅延読み込みをする必要があります。特に sp_footer_banner@3x.png , babyfood_merit_banner@3x.png はファイルサイズが大きく、かつフッター部分に表示される画像なので必ず遅延読み込みをした方が良い画像です。 Remove unused CSS 使用されていない無駄な CSS が読み込まれているようです。 特に viceo-react.css は使用率が 0%で、かなり無駄なロードになってしまっているようです。 Eliminate render-blocking resources 初回レンダリングを遅くする読み込み方法のリソースがあるようです。MAMADAYS では CSS の読み込みが阻害要因となっており、読み込みタイミングの見直しが必要そうです。 Properly size images 画像のサイズが最適でないようです。ここでも一番上の画像については 93%も太っているようなので、適切にサイズを制限する必要がありそうです。 このように PSI で検査するだけで多くの改善ポイントがあることが把握できました。ただし PSI や Lighthouse ではフロントエンドの改善項目しか検査できません。例えば LCP についてはサーバーサイドやインフラの構成を見直すことでも十分に改善できることを念頭においた方がよいでしょう。 LCP の改善 LCP の改善では大きく改善が進んだポイントが3つありました。 無駄なリソースの読み込みを除去 1つ目は2月頭の -2000ms ほどの改善です。 「 Remove unused CSS 」と「 Eliminate render-blocking resources 」について対応しました。上記の指摘では、カルーセルを実装する slick や、動画プレイヤーの video-react が挙げられています。 Chrome Developer Tool の Coverage 機能で確認すると確かに無駄な CSS であることがわかります。 MAMADAYS では CSS を Next.js の CSS Modules で読み込みをしているため、これらの読み込み箇所を _app.js から実際に必要なコンポーネントに移動しました。これにより不要なCSSの読み込みが除去されたことがわかります。 http2 への切り替え 2つ目は3月頭の -2200ms ほどの改善です。 これは Web 内で使われる画像の配信元 CDN のプロトコルを http/1.1 から http/2 へ切り替えたことによるものです。この切り替え設定自体は2月頭に行っていたものですが、このタイミングで PSI がリクエストを http/2 で行うように変更されたため、数値に大きく改善が現れました。 ref: March 3, 2021 | PageSpeed Insights uses http/2 to make network requests before after 画像を適切なサイズで配信 3つ目は3月末の -1800ms ほどの改善です。 PSI での指摘項目にあった「 Properly size images 」に対応した結果でした。MAMADAYS では画像を CloudFront で配信しており、同時に Lambda Edge にてリサイズを行なっています。取得する画像の URL クエリパラメータに ?w=400 などサイズを指定することでリサイズできるようにしているため、これを使ってサイズの最適化を行いました。 以下はコードの抜粋ですが、 source の media attribute で sp/pc 時の画像サイズを適切に切り分けました。Retina 対応のために 2x 時の指定も srcset attribute で行うと良いでしょう。 また、リサイズと同時に画像タイプも WebP に変換することでよりサイズの軽量化を行なっています。 const spSize = [imgSize.sp, imgSize.sp * 2]; const pcSize = [imgSize.pc, imgSize.pc * 2]; <source type='image/webp' media='(max-width: 767px)' srcSet={`${src}?w=${spSize[0]}&fm=webp, ${src}?w=${spSize[1]}&fm=webp 2x`} /> <source type='image/webp' media='(min-width: 768px)' srcSet={`${src}?w=${pcSize[0]}&fm=webp, ${src}?w=${pcSize[1]}&fm=webp 2x`} /> <source type='image/webp' srcSet={`${src}?w=${pcSize[0]}&fm=webp, ${src}?w=${pcSize[1]}&fm=webp 2x`} /> <img src={`${src}?w=${pcSize[0]}&fm=jpg`} alt={alt} loading={loading} {...(hasSize ? { height, width } : null)} className={classnames(css.image, className)} /> 改善したものの数値に影響がなかったもの PSI の指摘項目のうち、数値に影響があまり現れないものもあります。 例えば「 Defer offscreen images 」は 確かに対応した方が良いものですが多くの場合、遅延読み込みするべき画像はファーストビュー外の画像になるため、LCP の対象になるものが少ないです。そのため効果があまり現れなかったのでしょう。この辺りは改善コストを省みて実施するかを判断するのがよいでしょう。 巨人の肩に乗る(大切) MAMADAYS Web は React 製で、フレームワークに Next.js を使用しています。Next.js はパフォーマンス観点の積極的な改善を行っており、v10.1 ではバンドルサイズが 58%も縮小されたことで純粋にロードするファイルサイズが小さくなり、LCP の向上につながります。このようにコミッティーの勢いがあり積極的に改善が行われているフレームワーク・ライブラリを選定することもパフォーマンス改善において重要になるでしょう。 Announcing Next.js 10.1: • 3x Faster Refresh (200ms faster per save) • 58% Smaller Install (54% fewer deps) • Webpack 5 as opt-in flag • Apple Silicon (M1) Support • Next.js Commerce @Shopify Integration • More <Image> layout and loader options https://t.co/swmB7h2sOg — Vercel (@vercel) March 29, 2021 CLS の改善 CLS の改善では主に画像の領域確保と広告の領域確保を行いました。 3月末の時点で画像のサイズ最適化と同時に width と height を指定して領域の確保を行いました。これにより 20%ほどの改善ができました。 その後に数値が大きく上ぶれてしまっているのですが、これは新機能として CLS と相性の悪い機能を実装したことによるものです。これについてはその後改善を行ったり、CLS の算出アルゴリズムが変わったことで元の水準まで戻していますが、新機能を実装する際には Web パフォーマンスの観点からも慎重に検討を行いたいところです。 CLS の算出アルゴリズムが変わったことで CLS が向上 2021/06 に Lighthouse8 がリリースされました。これを機に CLS の算出アルゴリズムが変更になりました。元々は CLS はページ滞在中全てのレイアウトシフトを累計したものになっていましたが、変更後は 5 秒間の内に発生したレイアウトシフト群の合計の中で最も値の大きいもの、というようになりました。これにより多くのサイトで CLS が改善し、MAMADAYS もその恩恵を受けることができました。詳細については以下をご参照ください。 https://web.dev/cls/#what-is-cls スコアロジックにも変更あり また Lighthouse8 ではスコアロジックが変わりました。各指標のスコアに反映される重み付けが変更になったのですが、以下のようになりました。 特に CLS が3倍になっているため、従前よりもレイアウトシフトを起こさない実装を心がける必要があります。 指標 従前 → 変更後 First Contentful Paint 15% → 10% Speed Index 15% → 10% Largest Contentful Paint 25% → 25% Time to Interactive 15% → 10% Total Blocking Time 25% → 30% Cumulative Layout Shift 5% → 15% https://web.dev/performance-scoring/ 改善の結果 この時点での PSI を測定してみると以下のようになりました。 フィールドデータは遅れて徐々に反映されるため、変化がないように見え少々分かりにくいですが、特に LCP の改善が効いたことで 20pt ほどスコアが向上しました。 これによってベンチマークしているサイトの中でもランキング上位を推移するようになりました。 今回はランキング上位に入ることはできましたが、CWV の合格基準に達することはかないませんでした。合格基準を満たせるように、MAMADAYSでは今後も改善を重ねていきます。 まとめ 今回はほとんどがフロントエンドにフォーカスした改善でしたが、より改善を進めるためにはサーバーサイドアプリケーションの改善やキャッシュの改善を行う必要があります。MAMADAYS では今後も継続的によりよい UX を提供するためパフォーマンスの改善を続けていきます。よい UX を作ることやパフォーマンスの改善に興味がある方・造詣が深い方はぜひ RECRUIT | every, Inc. までご連絡ください。
アバター
はじめに DELISH KITCHEN のデータベースについて紹介します。 サービスやバックエンドシステムの全体像については DELISH KITCHEN のサービスとバックエンドシステムのお話 - every Engineering Blog で紹介しています。よろしければご覧ください。 概観 DELISH KITCHEN ではサービスの大半のデータの保存に Amazon RDS を使用しており、データベースエンジンとしては主に MySQL を使用しています。サーバーがいくつかに分かれておりデータベースもそれぞれにありますが、今回はレシピやユーザーの情報の入ったメインのデータベースの話をします。 DELISH KITCHEN は規模としては月間総利用者数 ※ 5200 万人のサービスです。 ※ DELISH KITCHEN のアプリ、Web、SNS、サイネージなど全ての提供内容における総利用者数のこと。 オンメモリキャッシュ レシピなどのマスタ系のデータについては、更新頻度が低いことからサーバーアプリ内にオンメモリキャッシュを持ち、定期的に更新するというアプローチを取っています。管理画面外からのアクセスにはそのキャッシュを利用しており、トランザクション系のデータの読み書きについてはキャッシュは行わずサーバーから RDB へ読み書きを行っています。 RDB 負荷分散 MySQL レプリケーションを利用して複数のインスタンスへ負荷を分散しています。また、マスタ系とトランザクション系の DB インスタンスを分けており、トランザクション系の方の RDS インスタンスタイプを大きめにしています。 Amazon Aurora の導入 最近読み込み頻度が高く、レコード量が将来的に数千〜億単位になることが見込まれ、かつ読み書きのレイテンシがアプリの初期描画速度に影響するために相応に低いことが求められるデータの保存方法を考える機会がありました。 書き込みは素直なテーブル構造にするとランダムインサートとなり、通常の RDS ではレコード量増加に伴って書き込みのレイテンシは増えていくことが想定されました。 そこで、レコード量が増加しても I/O が低下しにくい Amazon Aurora MySQL を導入しました。導入にあたり簡単な負荷試験をして RDS (MySQL) との比較を行いましたのでその概要を紹介します。 Aurora と非 Aurora RDS の負荷試験による比較 ランダムインサートを行う下記のような簡素な Go のコードを実行し、レコード量増加に伴った INSERT のスループットの変化を確認しました。 func main() { insertedCount := int64(0) insertStart := time.Now() countMutex := sync.RWMutex{} wg := &sync.WaitGroup{} for i := 0; i < 8; i++ { wg.Add(1) go func() { sess := getSession() // DB セッションを作成 for insertedCount < 10000000 { // 主キーをランダムに発行した適当なレコードを INSERT record := genNewRecord() if _, err := sess.InsertInto("test_table").Columns("col_a", "col_b").Record(record).Exec(); err != nil { panic(err) } countMutex.Lock() insertedCount++ if insertedCount%100000 == 0 { // 100000 レコードごとに経過時間を出力 fmt.Println(insertedCount, time.Now().Sub(insertStart)) } countMutex.Unlock() } wg.Done() }() } wg.Wait() os.Exit(0) } 結果的に、レコード量が増えても Aurora ではまるでスループットは一定でしたが、非 Aurora では対数的ではありますが顕著な増加が見られました。(グラフはイメージです) Aurora はレコードが増えても INSERT のパフォーマンスが変わらない! 導入決定〜使用開始まで Aurora RDS インスタンスの作成、サーバーアプリからの接続までについては非 Aurora のそれとあまり変わりませんでした。DB インスタンスがたとえ 1 つのみでも Aurora クラスターが作成され、Web コンソールの DB 一覧を見るとクラスタ配下にインスタンスがあると表示されます。 DB 一覧画面。クラスタ配下に DB インスタンスが表示される MySQL 互換なので、接続後は通常の MySQL エンジンと(完全ではないようですが)同じように使用できます。導入後のパフォーマンスについてはまだレコード数が多くないのであまり大きなことは言えませんが、期待通り動作しています。 Aurora を監視する Aurora は非 Aurora の RDS と比べて取れるメトリクスが変わってきます。 How to Collect Aurora Metrics | Datadog に非常にわかりやすくまとまっていたので、参考にして以下の項目に合致するメトリクスをモニタリングしています。 Query throughput Query performance Resource utilization Connections Read replica metrics 最後に DELISH KITCHEN のデータベースについての紹介でした。DELISH KITCHEN ではレシピ動画サービスの安定稼働に向けて改善を続けています。お読みいただきありがとうございました。 参考 Amazon Aurora(高性能マネージドリレーショナルデータベース)| AWS
アバター
はじめに 今すぐできるレビュワーに優しいPull Requestをつくる7つのポイント 1. WhyとWhatをそれぞれ記載する 2. 説明文は構造化する 3. コミットは課題を解決した単位で行う 4. Pull Requestは適切な大きさに分割する 5. 個別説明が必要な箇所は積極的にコメントをつける 6. テストを書く 7. Pull Requestでのコメントを Slack に通知させる さいごに はじめに はじめまして。DELISH KITCHEN開発部の桝村です。DELISH KITCHENのWEBフロントやAPIサーバーの開発等に携わっています。 突然ですが、みなさんは本日もPull Requestを使ってレビュー依頼しましたか?もしくは、誰かからレビュー依頼を受けましたか? チーム開発におけるコードレビューというものは、プロダクトの品質向上やチーム内での知見共有に貢献しているものの、 チームがコードレビューに対して相当な時間や労力をかけているのも事実 かと思います。 加えて、レビュー対象の実体でもあるPull Requestの品質は、作り手である実装者に大きく依存しており、コミットから説明文まで自由に作れる反面、 レビューしやすいPull Requestを作成しないと、より一層自身やチームに大きな負担がかかる可能性 があります。 そこで、今回はレビュワー目線に焦点を当てて、レビューしやすいPull Requestをつくるために自分が心がけていることを紹介させて頂きます。 簡単かつすぐに改善できるポイントをまとめたので、ぜひ参考にして頂けると幸いです。 今すぐできるレビュワーに優しいPull Requestをつくる7つのポイント 1. WhyとWhatをそれぞれ記載する WhyとWhatが不十分な場合、レビュワーはそれらをコードから想像せざるを得なかったり、実装者へ直接確認する手間が生じて、大きな負担になる可能性があります。また、WhyとWhatが区別されず混合している場合も、実装内容の難易度や複雑性により、実装者とレビュワーの認識に齟齬が生じ得ます。 WhyとWhatをそれぞれきちんと記載することで、レビュワーは、 本来のレビュー内容である、仕様通りかどうか、改善の余地はあるか等の確認作業に集中でき 、よりコードレビューをしやすくなります。 また、Pul Request自体が履歴的な情報としてリポジトリ内に残り続ける点で、実装に関するドキュメントとしての役割も担います。よって、WhyとWhatをきちんと記載することは、長期的に見てもチームにとって非常に貴重な財産になります。 加えて、以下のような情報があると、実装内容の正当性を容易に検証できたり、アウトプットがひと目で分かる点で、よりレビュワーが実装概要を理解しやすくなります。 ローカルでの動作確認の手順 関連するPRやタスク管理システムのチケットへの参照リンク フロントエンド実装の場合、デバイス別でスクリーンショットを添付 バックエンド実装の場合、レスポンスや必要なパラメータを記載 2. 説明文は構造化する 説明文が各項目について整理されず文章のみで構成されている場合、読み手であるレビュワーは実装内容の要点を理解するのに時間がかかり、大きな負担になる可能性があります。 説明文では以下のようにマークダウン記法を使用できるので、見出しや箇条書き、コード埋め込み等のスタイルを利用することで、レビュワーは、 実装概要をひと目で理解でき 、よりコードレビューをしやすくなります。 ### Why - 実装背景 ### What - 実装内容 - 実装内容詳細(その1) - 実装内容詳細(その2) ### Ref - 関連PRへの参照リンク ### Check - [ ] レビュー依頼前に必ず実施すること(その1) - [ ] レビュー依頼前に必ず実施すること(その2) Pull Request 説明文の構造化例 参考: Basic writing and formatting syntax また、Pull Request TemplatesというPull Requestの説明文に対して開発者に含めて欲しい情報をカスタマイズし、標準化できる機能があり、これを使用すると、レビュワーにとって見やすい説明文になるだけでなく、実装者にとっても構造化する手間がなくなったり、何を記載すれば良いか明確になる点で導入するメリットが非常に大きいです。 参考: Creating a pull request template for your repository 3. コミットは課題を解決した単位で行う コミットの粒度がバラバラであったり複数の変更が入った曖昧なコミットである場合、レビュワーはどんな変更をしているのか把握しづらく、大きな負担がかかる可能性があります。 機能実装やバグ修正、リファクタ等、まずは単一の課題や目的を単位としてコミットすることで、レビュワーは、 変更概要や意図を正確かつ容易に理解でき 、よりコードレビューをしやすくなります。 加えて、以下のようにコミットメッセージにPrefix (テキストの先頭につける文字) をつけると、 どのカテゴリの修正をしたのか、プロダクションコードに影響があるコードかがひと目でわかるようになり 、よりコードレビューをしやすくなると思います。 feat : (new feature for the user, not a new feature for build script) fix : (bug fix for the user, not a fix to a build script) docs : (changes to the documentation) style : (formatting, missing semi colons, etc; no production code change) refactor : (refactoring production code, eg. renaming a variable) test : (adding missing tests, refactoring tests; no production code change) chore : (updating grunt tasks etc; no production code change) 参考: Semantic Commit Messages 4. Pull Requestは適切な大きさに分割する Pull Requestが大きすぎる場合、レビュワーは単純に時間や労力がかかるだけでなく、既存のコードへの影響範囲が大きくなるゆえに問題点の発見も困難になり、大きな負担がかかる可能性があります。 適切な大きさに分割すると、レビュワーは、 影響範囲もより限定的になるため、レビューが楽になったり、その精度も上がり 、よりコードレビューをしやすくなります。Pull Requestの粒度としては、自分の場合、スコープ、つまり機能セットを絞り込み、1つのPull Requestで解決するタスクを減らすことを意識しています。 5. 個別説明が必要な箇所は積極的にコメントをつける 特定のコードについて説明が必要な場合があると思います。例えば、実装したものの自信がなく注意深くレビューをお願いしたい時やコードのみで実装の意図が伝わりにくい時、知見を共有したい時などです。そういった場合、インラインコメントを記載すると、レビュワーは、 自ずとコメント周りのコードを注意深く確認したり、早期に問題提起・解決策の話し合いができ 、よりコードレビューをしやすくなります。 Pull Requestへのインラインコメント例 (Githubの場合) 参考: Adding line comments to a pull request また、実装者が躊躇せず積極的に発信することが、有意義な議論やコミュニケーションが生み、結果的にチームの成長や開発効率の向上に繋がると思います。 6. テストを書く テストがない場合、レビュワーは実装内容の仕様をソースコードのみから読み取る必要があり、大きな負担がかかる可能性があります。 テストがきちんと書かれていると、ただソフトウェアの品質を向上させるだけでなく、ソースコードの仕様(期待する処理結果)に関するドキュメントとしての役割も担うため、レビュワーは、 その仕様や振る舞いを容易に読み取ることができ 、よりコードレビューをしやすくなります。 7. Pull Requestでのコメントを Slack に通知させる コードレビューにて実装者のレスポンスが遅い場合、レビュワーは返信が無くて気になったり、コメント内容を忘れる等により、レビューの効率を下げ、大きな負担がかかる可能性があります。 そこで、実装者が簡単かつ最初にできることは、レビュワーによるコメントにいち早く気づくことです。Slackとの連携機能を使用することで、 レビュワーによるコメントやレビューを任意のチャンネルへ通知させることができ 、少しでも早く返信できるようになります。 SlackへのPull Request コメント通知例 (Githubの場合) 参考: GitHub と Slack を連携させる さいごに 今回は、チーム開発において、レビュワーに優しいPull Requestをつくるポイントをまとめてみました。 冒頭でお話ししたとおり、チーム開発ではコードレビューは結構な時間と労力がかかります。裏を返せば、メンバー一人一人がレビューしやすいPull Requestの作成を心がけることで、チームの開発速度が大きく改善する可能性があると思います。 今回紹介させて頂いたポイントを実際の開発現場で試して頂けると嬉しいです。 ここまでお読みいただき、ありがとうございました。
アバター
はじめに はじめまして。普段はMAMADAYSでiOSエンジニアをしている國吉です。 iOSエンジニアではありますが、アプリのストアレビュー改善企画も兼務で行っているため、時にはAndroidの実装を担当することもあります。 そこで今回は2020年8月頃にGoogleから提供されたIn-App Review APIをMAMADAYSのAndroidアプリに導入し、実際レビュー評価にどのような変化を及ぼしたのかをお話していきます。 In-App Review APIとは アプリから離脱することなく、アプリのレビューを行うことができるAPIです。 APIレベルは21以上(Android5.0)がサポートされています。 developer.android.com これまでの課題 ストアのレビュー評価はアプリのインストールに大きく関わってきます。 ただ、MAMADAYSではこれまで”独自ポップアップを実装し、Google Play Storeに遷移させレビューを行ってもらう”というフローだったため、レビューまで手間がかかってしまいユーザーの離脱が多かったです。 そのためレビュー評価4.0から全く上がらないのが課題でした。 導入してみた結果 In-App Review APIを導入し約8ヶ月程経過しましたが、徐々にレビュー評価の件数が上がってきています。 もちろんその8ヶ月間でアプリ自体に様々な機能を追加しており、利便性が向上していることも影響してると思いますが、アプリ内でレビュー完結できることがレビューという行為のハードルを下げています。 導入前と比較すると0.3程度上がり、レビュー評価は4.3~4.4程度にまで成長しました。 レビュー要求を表示するタイミング レビュー要求は表示するタイミングが重要です。 MAMADAYSでは、ユーザーが何かを達成した時(例えば記事をお気に入りした。育児記録を登録した。など)に下記の公式リファレンスに記載されている事項を考慮しレビュー要求を表示しています。 ユーザーがアプリやゲームを十分体験してから、アプリ内レビューのフローを開始してください ユーザーに過度にレビューを求めないでください 評価ボタンや評価カードを表示する前または表示中に質問をしないでください(「アプリを気に入りましたか?」といったユーザーの意見に関する質問など) 実装方法 In-App Review APIはPlay Core SDKの一部なので、gradleファイルにCoreライブラリ1.8.0以上を追加します。 dependencies { implementation 'com.google.android.play:core:1.8.2' } 様々な画面でレビュー要求を表示したいので、In-App Review APIをリクエストして表示する処理を共通関数として作成していきます。 ただし、公式リファレンスにも注意書きされていますが勝手に表示されるViewのデザインやサイズ、背景色等のカスタマイズは一切行わないようにしてください。 import com.google.android.play.core.review.ReviewManagerFactory ... fun showInAppReview(activity: Activity) { val manager = ReviewManagerFactory.create(activity) val request = manager.requestReviewFlow() request.addOnCompleteListener { if (it.isSuccessful) { manager.launchReviewFlow(activity, it.result) } else { // error } } } 最後に Googleから提供されているAPIのため実装が比較的簡単であり、レビュー評価の向上にも繋がるためまだ導入されていない方は是非導入してはいかがでしょうか。 ただ、In-App Review APIをリクエストした際、必ずレビュー要求が表示されるわけではないので、絶対にレビュー要求を表示したい画面は”独自のポップアップ”を表示するように切り分けて使っていくのもいいかもしれません。  ここまで読んでいただき、ありがとうございました。
アバター
はじめに iOSでTableviewやCollectionViewを扱っていると、UIとデータとの間で不整合が起きた際に NSInternalInconsistencyException というエラーを吐いてアプリが落ちるというのはよくある話だと思います。 TableViewに関してはiOS13から UITableViewDiffableDataSource が追加され、Apple曰くこの問題を回避できるらしいので、DELISH KITCHENのiOSアプリで採用してみました。 導入方法 Hashable化 セクションやアイテムに対応するオブジェクトがHashableに適合している必要があります。 今回は対象となるオブジェクトがユニークなIDを既に持っていたので簡単でした。 /// TableViewの各セルに対応するオブジェクト struct Item : Hashable { let id : Int ... /// 追加1 static func == (lhs : MessageDetailRowItem , rhs : MessageDetailRowItem ) -> Bool { return lhs.id == rhs.message.id } /// 追加2 func hash (into hasher : inout Hasher) { hasher.combine(id) } } UITableViewDataSourceをUITableViewDiffableDataSourceに変更する struct Section { let items : [Item] } このようなセクションがあると仮定して class SomeClass : UITableViewDataSource { let sections : [Section] func numberOfSections ( in tableView : UITableView ) -> Int { return sections.count } func tableView (_ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int { return sections[section].items.count } func tableView (_ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell { /// cellをデキューして加工して返す処理 } } というUITableViewDataSourceの実装があった場合の変更点を示します。 まず、 Section をHashableに適合させます。 次に、UITableViewDataSourceの代わりにUITableViewDiffableDataSourceを使うように変更します。 class SomeClass { private var dataSource : UITableViewDiffableDataSource <Section, Item> ? func setupDataSource (tableView : UITableView ) { dataSource = UITableViewDiffableDataSource < Section, Item > (tableView : tableView , cellProvider : { [weak self ] (tableView, indexPath, item) -> UITableViewCell ? in /// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCellと一緒の内容 } } func setupSnapshot () { var snapShot = NSDiffableDataSourceSnapshot < Section, Item > () let sections : [Section] = /// 省略 snapShot.appendSections(sections) sections.forEach { snapShot.appendItems( $0 .items, toSection : $0 ) } dataSource.apply(snapShot) } } setupDataSource はViewControllerのViewDidLoad、 setupSnapshot は setupDataSource より後でデータが取得できたタイミングで実行すれば良いと思います。 また、変更がある場合は現在のスナップショットを dataSource から取得できるので、それに対して変更を加えて再度applyするだけで良いです。 performBatchUpdateなどの処理 dataSourceにapplyしたらTableViewにも反映されるので不要になります。 結果 日に数件エラーが出ていたのですが、0件になりました。 今回はTableViewに対しての改善でしたがCollectionViewにも同様のAPIが存在するので、そちらも改善していきたいと考えています。 参考 https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views Appleが提供しているサンプルプロジェクトです。 2021年3月18日時点では WiFiSettingsViewController や TableViewEditingViewController がUITableViewDiffableDataSourceを使っているので参考になると思います!
アバター
はじめに 前提技術スタック pre-commit、CIでのLintチェック、パッケージをクリーンアーキテクチャ構成にする pre-commit 良かった事 CIでのLintチェック 良かった事 パッケージをクリーンアーキテクチャ構成にする 良かった事 まとめ はじめに DELISH KITCHEN開発部の福山です。 社内向けシステムとしてAPI Serverを新規に構築する機会がありました。新規開発にあたり導入してみて良かった事をいくつかご紹介したいと思います。 前提技術スタック 新規GitHubリポジトリを用意してREST APIをGoで実装する。Webフレームワークはechoを採用。インフラはAWS ECS、RDSを利用。ログ情報はfluentd経由でS3やTreasuredataに格納しています。SentryやDatadogによる監視も行っています。 pre-commit、CIでのLintチェック、パッケージをクリーンアーキテクチャ構成にする pre-commit 『DELISH KITCHEN』のメインAPI Serverの方で途中から採用された内容とほぼ同じ内容となります。今回は実装初期段階で pre-commit を導入しました。 pre-commitを使えばローカルでのGit commit時に任意のスクリプトを実行出来ます。 以下の処理が行われる様に設定されています。 - go generate - go vet - gofmt - goimports - golint - wire 主に実装内容の静的解析を行っておりcommit前にルールから外れたコードを発見し修正を促します。 その他go generate契機でmockファイルの作成や wire でDIコードの生成処理( *後述 )を実行しています。 良かった事 コードレビュー時にレビュアーがコードフォーマットや生成ファイルの有無等の指摘をする必要が無くなり、仕様やバグのチェックに集中出来る様になりました。 CIでのLintチェック 自動テストは導入しているのですが、更に実装初期段階からGithub Actionsにて reviewdog/golangci-lint を導入しました。 golangci-lintには 様々なLinter が用意されておりプロジェクト状況に合わせて任意のLinterを利用出来ます。 今回の開発では以下のLintチェックを有効化しています。 - bodyclose - deadcode - depguard - errcheck - goconst - gocritic - gofmt - goimports - gosec - gosimple - govet - ineffassign - interfacer - misspell - nakedret - noctx - prealloc - scopelint - staticcheck - structcheck - typecheck - unconvert - unparam - unused - varcheck pushした内容に指摘対象のコードが存在する時にGithub上で以下の様に表示してくれます。 reviewdog/action-golangci-lint 良かった事 pre-commit と同じなのですがレビュアーに指摘される前に自動チェックが行われる為、コードレビュー時に仕様やバグのチェックに集中出来る様になりました。 具体的には gosec でセキュリティ面での指定が有ったり、 gosimple でシンプルなコードの書き方に気付かされたりとコードの品質向上のきっかけとなります。 特に実装初期段階から導入した事によりほぼ全てのコードが随時チェックされている事になるので途中から採用するよりオススメです。 パッケージをクリーンアーキテクチャ構成にする 既存のシステムでMVC的な構成で苦労する場面が有りました。Goで domain/model 的なパッケージとデータの永続化処理は切り離したいと考え今回はクリーンアーキテクチャ構成で実装する事にしました。 (色々なクリーンアーキテクチャの詳細解釈が存在するのであくまで一例とお考え下さい。) ざっくり過ぎる図解なのですが以下の様なパッケージ構成となっております。(→は依存方向) パッケージレイヤー構成の一例 最低限下記は意識しつつ、ルールに拘り過ぎて工数が掛かり過ぎない様に状況に応じて詳細実装を進めました。 - 依存方向を守る(domainは他に依存しない) - 抽象に依存する(interfaceに依存する) handlerパッケージから wire で生成されたコードでDependency Injectionされる構成となっております。 動作のポイントとしては domain/repository で定義されたinterfaceの詳細実装は infrastructure に存在しており、注入された内容として実行される様になっています。 他にはWebフレームワークの固有処理もhandler内に閉じており usecase 以降には影響しない様になっています。 良かった事 テストの容易性 一般的に言われている事ですがテスタブルになりました。 MVC構成だと要件ロジックのユニットテストを書く時も依存するデータベース情報等を実際に用意する必要が有りました。しかし抽象に依存しつつパッケージレイヤーを切った事により、例えば usecase でのユニットテスト時にパッケージ内で扱う永続化処理に対してmockを利用し任意の結果が返却出来る事になります。 従ってテスト対象パッケージ以外の状態に悩まされる事無く確認したい要件ロジックに集中してユニットテストを行う事が出来る様になりました。 ドメイン情報の認識が深まる 単一パッケージにほぼ全てのロジックを詰める様な形だと肥大したり処理の責務等は考えなくなってしまう可能性が高いのですが、 要件ロジックをどのパッケージレイヤーに書くべきかを個人的にもチーム内でも意識する様になりました。 実際にPRレビューの時にチームメンバーと議論する事が有り、結果として当初より見通しの良いコードになる事が有りました。 この部分は個々人の解釈の違いも有るので工数が膨らみ過ぎないバランスで進める様にしています。 まとめ 新規開発を行うタイミングで導入して良かった事として pre-commit 、 CIでのLintチェック 、 パッケージをクリーンアーキテクチャ構成にする をご紹介しました。 工数面のバランスや未経験な技術要素を導入するリスクも有りますが開発工程初期に開発効率を向上させる仕組みを用意するメリットは大きいと考えています。 まだ開発は続きますので良い仕組みを活かして効率良くアウトプットしていきたいと思います。
アバター
Core Web Vitalsの計測環境を整える はじめに 現在、MAMADAYSのWebチームでは昨年発表されたCore Web Vitalsを中心としたパフォーマンス改善に注力しています。 今回はパフォーマンス改善でも重要な計測部分について、MAMADAYSではどのようにCore Web Vitalsのデータを定点観測する環境を整えているのかをご紹介したいと思います。 Core Web Vitalsとは Core Web Vitalsとは、全てのサイトにおいて共通してユーザー体験をよくするために重要な、Google社が提唱するパフォーマンス指標のことです。本記事ではCore Web Vitalsの解説を目的としないため、詳細な説明は割愛しますが、Core Web VitalsにはLCP・FID・CLSという3つの具体的なパフォーマンス指標があり、将来的にはGoogle検索のランキング要因にも組み込まれると言われています。 画像出典: https://web.dev/vitals/ LabデータとFieldデータ パフォーマンス改善をする際に重要になってくるのがパフォーマンスの定点観測ですが、計測データは大きく分けて以下の2種類があります。それぞれにメリットとデメリットがあるので、両方をうまく使い分けながらサイトのパフォーマンス観測を行っていくことが大切になります。 Labデータ : Googleが開発するLighthouseなど特定の環境下で収集されたパフォーマンスデータのことです。特定の環境下で行うことにより再現可能なデータを提供でき、パフォーマンス観測もしやすいのがメリットですが、実際の利用者との実行環境の差異がある可能性があります。 Fieldデータ : 利用者の実際の環境下で収集されたパフォーマンスデータのことです。実際の利用環境のパフォーマンスが収集できることがメリットですが、収集するデータにはばらつきがあるためFieldデータに比べると観測がしにくいです。 参考: https://web.dev/how-to-measure-speed/#lab-data-vs-field-data 計測環境の検討 計測環境の検討にあたっては有料の計測サービスの SpeedCurve やNext.jsでVercelを使っていればNext.js製の Analytics も候補に出ると思います。ただ、MAMADAYSではBIツールとしてMetabase、分析データの保存先としてBigQueryを使っているのでうまく既存のアセットを生かした形でコストをかけずに実現する方法を模索していました。 Labデータの計測 Labデータの計測にあたっては、 PageSpeed Insights API を利用してLabデータの収集を行っています。 PageSpeed Insights はブラウザでサイトのパフォーマンスを確認できるツールとして便利ですが、APIも用意されており、簡単に同じデータを取得することができます。 // PageSpeed Insights APIのレスポンスの一部抜粋 { " lighthouseResult ": { " audits ": { " largest-contentful-paint ": { " id ": " largest-contentful-paint ", " title ": " Largest Contentful Paint ", " description ": " Largest Contentful Paint marks the time at which the largest text or image is painted. [Learn more](https://web.dev/lighthouse-largest-contentful-paint/) ", " score ": 0.92 , " scoreDisplayMode ": " numeric ", " displayValue ": " 1.1 s ", " numericValue ": 1110 } , " total-blocking-time ": { " id ": " total-blocking-time ", " title ": " Total Blocking Time ", " description ": " Sum of all time periods between FCP and Time to Interactive, when task length exceeded 50ms, expressed in milliseconds. [Learn more](https://web.dev/lighthouse-total-blocking-time/). ", " score ": 0.97 , " scoreDisplayMode ": " numeric ", " displayValue ": " 110 ms ", " numericValue ": 105 } , " cumulative-layout-shift ": { " id ": " cumulative-layout-shift ", " title ": " Cumulative Layout Shift ", " description ": " Cumulative Layout Shift measures the movement of visible elements within the viewport. [Learn more](https://web.dev/cls/). ", " score ": 1 , " scoreDisplayMode ": " numeric ", " displayValue ": " 0 ", " details ": { " items ": [ { " finalLayoutShiftTraceEventFound ": true } ] , " type ": " debugdata " } , " numericValue ": 0.00018970055161544525 } } } } 注意点として 公式でも記載 されていますが、Lighthouseのように特定の環境下でユーザーなしにパフォーマンス計測をする場合にFIDは計測できません。したがって、LabデータでFIDの計測を行いたい場合は代替手段としてFIDと相関のあるTotal Blocking Time (TBT)を見るようにします。 MAMADAYSではこちらのAPIを利用して、複数ページを2時間おきにデータを収集し、BigQueryに転送しています。1回のみ特定のページを毎日計測する方法だとパフォーマンスデータとしてはあまりにも信憑性に欠けてしまうので複数のページで頻繁にデータを取得するようにしています。 Fieldデータの計測 パフォーマンス改善に取り組み始めた当初、前述したLabデータの観測のみを行っていました。ただ、Labデータのみだと実際の環境下でのパフォーマンスデータが観測できないことが課題としてあり、Fieldデータの計測方法を検討しました。 Next.jsとGoogle Analyticsを利用した計測基盤の構築 まずはWeb側のデータ収集方法ですが、MAMADAYSのWebではNext.jsを採用しており、Next.jsはバージョン9.4から標準機能としてCore Web Vitalsの計測を行えるようになったのでその機能を使って公式のガイドを参考に実装しました。また、収集したパフォーマンスログはすでに連携済みだったGoogle Analyticsのイベントとして保存することで継続してパフォーマンス推移を観測できる環境を作りました。 // pages/_app.js // googleAnalyticsのイベントとしてパフォーマンスデータを保存 function performanceMetricsEvent( { id, name, label, value } ) { const eventValue = Math.round(name === 'CLS' ? value * 1000 : value); window .gtag( 'event' , name, { event_category: 'パフォーマンス' , value: eventValue, event_label: id, non_interaction: true , } ) } // Next.jsの標準機能 reportWebVitalsを定義する export function reportWebVitals(metrics) { performanceMetricsEvent(metrics); } 参考: https://nextjs.org/docs/advanced-features/measuring-performance 直面した問題点 しかし数週間こちらの計測方法で検証していたところ、送っているイベントのラベルがページロードごとのユニークな値にしているため、ラベル数が上限に達してしまい他のイベントに影響を及ぼしてしまう問題がGoogle Analyticsのアラートから発覚しました。 その時点で対応するのであれば、全体の利用者の何割かに絞って計測をすることで上記の問題は解決できそうでしたが、今後利用者の増加を考慮して計測基盤の見直しを行いました。 計測方法の改善 計測基盤を見直すにあたって、MAMADAYSでは分析にBigQueryを使用しているためBigQueryへの転送を考えました。 また大量のパフォーマンスログのデータ転送をアプリケーションとは切り離して行うために、サーバー側はパフォーマンスのログ出力のみを行い、fluentdでBigQueryへのストリーミング挿入し、dailyでシャーディングテーブルを作るように変更しました。fluentdでは fluent-plugin-bigquery というgemを使うことによって簡単にfluentdでのBigQueryへのストリーミング挿入が実現できます。 ログの出力形式 {"id":"1618905791407-4433185739018","label":"web-vital","level":"INFO","name":"LCP","path":"/articles/999","time":"2021-04-20T08:03:11.870117321Z","type":"WEB_PERFORMANCE","value":"1500"} fluentdでのinsert部分の設定 <label @web-performance-log> <filter **> @type grep <regexp> key $.parsed_log.type pattern ^WEB_PERFORMANCE$ </regexp> </filter> <filter> @type record_transformer renew_record enable_ruby <record> id ${record["parsed_log"]["id"]} time ${record["parsed_log"]["time"]} label ${record["parsed_log"]["label"]} name ${record["parsed_log"]["name"]} path ${record["parsed_log"]["path"]} value ${record["parsed_log"]["value"]} </record> </filter> <match **> @type bigquery_insert auth_method json_key json_key /etc/secrets/google-credentials/fluentd-to-bq.json project "#{ENV['BQ_PROJECT']}" dataset "#{ENV['BQ_DATASET']}" table web_performance_%Y%m%d auto_create_table true <buffer time> @type file flush_interval 30s path /var/log/fluentd-buffers/bq-event.buffer timekey 1d </buffer> schema [ {"name": "id", "type": "STRING"}, {"name": "time", "type": "STRING"}, {"name": "label", "type": "STRING"}, {"name": "name", "type": "STRING"}, {"name": "path", "type": "STRING"}, {"name": "value", "type": "STRING"} ] </match> </label> この改善により、BigQueryのストリーミング挿入でコストが多少掛かってしまいましたが、他の分析への影響を与えずにFieldデータの継続的な観測を実現できました。また、Google Analyticsへのデータ保存時にはMetabaseというBIツールで計測結果が見れるようにBigQueryへのデータの加工と転送を自前で別途行う必要がありましたが、直接BigQueryに転送できたことでその手間も省ける結果となりました。 まとめ 今回はWebパフォーマンスの計測でCore Web Vitalsをどう計測しているのかについて話しました。パフォーマンス改善において、憶測ではなく現状のボトルネックなどを正しく理解して改善する上でもパフォーマンスの継続的な計測は重要になってくると思います。計測方法やGoogle Analyticsでの問題に関して同じような課題に直面されている方の参考になれば幸いです。 MAMADAYSのWEBチームではパフォーマンス改善に注力しており、改善結果も出ているので実施した改善内容についても今後お話していきたいと思います。
アバター
はじめに 振り返り会の意義 振り返り会のやり方 ファシリテーターを誰が担当するのか 何について振り返るのか 前回の振り返り会を確認する やったこと・良かったことを洗い出す もっと良くできそうなことを洗い出す 共通認識を生み出す 批判する会ではない やってみたいことを考える よくあるNGパターン やることを決める おわりに はじめに 昨今のコロナウィルス感染拡大に伴う対応として弊社ではリモートワーク中心の働き方に変化し1年ほどが経過しました。 働き方が大きく変わっていった状況の中で、滞りなくチーム開発が進められた要因の1つが毎週開催している振り返り会にあったのではないかと私は考えています。 今回は、以前私が所属していたDELISH KITCHENのバックエンド開発のチームとプロダクトマネージャーとの間ではどのように振り返り会を実践してきたのかを紹介させていただきます。 振り返り会の意義 計画して実行した結果に対して「何が良かったのか?何が悪かったのか?次はどうするのか」を考える、いわゆるPDCAサイクルを回すことの有意性については今更議論する必要がないと思います。 PDCAサイクルによる改善活動は、個人で行う仕事であれば自分がやったことを見直し次に活かせば良いので簡単に実現できるのですが、チームで行う仕事の場合は誰か1人の力だけで行うのは非常に困難です。 リーダーが1人でチームの改善活動を行う場合、リーダーの力量以上にチームが成長することは難しいでしょう。それはリーダーの視点から気付ける課題や改善策に限定されてしまうからです。 リーダーからすると取るに足らない些細な課題が実は複数のメンバーが感じている重要な課題かもしれませんし、ある課題に対してリーダーが考えつかないような改善策が他のメンバーから提案されるかもしれません。 基点となる1人のフィルターを通してしまうと、その人の考えに大きく依存してしまいチームはいずれうまく動かなくなることが予想されます。 振り返り会では様々な課題をチームの課題として捉え、メンバーが相互作用しながら解決に導くことでチームのPDCAサイクルを回します。 また、プラクティスの共有や課題についての議論を行う対話の場ができることによって「協調するチーム」作りに寄与する重要な機会になると考えています。 振り返り会のやり方 チームで行っている振り返り会は、週に1回/半期に1回行う定期的なものとプロジェクトごとに行う不定期なものがありますが、今回は週に1回定期的に開催しているやり方について取り上げたいと思います。 やり方はKPTをベースにいくつかのオリジナリティを加えており、参加メンバーはPdMとエンジニアの4-6人ほどで開催しています。 全体は以下のような流れになっています。 前回の振り返りを確認する 取り組んだアクションはどうだったのか 解決していない課題は何か やったこと・良かったことを洗い出す もっと良くできそうなことを洗い出す やってみたいことを洗い出す やることを決める ファシリテーターを誰が担当するのか 振り返り会の進行を行うファシリテーターは職種によらず参加メンバー全員の持ち回りで進行しています。これはメンバーそれぞれがやり方を工夫する余地を持たせるためです。 最適な振り返りの方法はチームや状況によって変わるため自分がファシリテーターの時には自由にアレンジすることが許されており、振り返り自体をより良くするための案として採用しています。 また、ファシリテーターを固定してしまうとどうしても参加させられてる感・他人事感が出てきてしまうと考えているため、持ち回りにすることで自分たちのために開催しているという当事者意識を持ちやすくする効果があります。 何について振り返るのか 振り返り会で最初にやるべきことは、何について振り返るのか認識を合わせることです。 1週間を振り返るという抽象的なテーマで始めると出てくるトピックの粒度にばらつきが生じ時間配分がとても難しくなるでしょう。 振り返りの勘所がわかっているチームであれば問題ありませんが、多くのチームでは具体的なテーマを決めて何について話すかを明確にした方がスムーズに進行できるでしょう。 多くの問題を抱えたチームが自由に問題点を列挙するような振り返り会の場合、広く浅く問題について話したことで満足してしまい結局何も解決されていないなんてことは良くあるのではないでしょうか。 定期開催している場合1回の振り返り会にかける時間は短いでしょうし、次の振り返り会までに取り組めるアクションは限られるため一度に多くの問題を解決しようとせず、まずは問題の1つをテーマとして取り上げて確実に改善に取り組んでいくのが良いと思います。 と、書きましたが実際にチームでは特にテーマを決めずに1週間を振り返っています。 これは1年以上毎週振り返り会を続けており、チームの中で共通のナレッジになっているものやすでに解決した課題が大半で抽象的なテーマでもうまく進められる状態になっているからです。 前回の振り返り会を確認する 2回目以降の振り返り会の場合、まずは前回の振り返り会を確認するところから始めます。 前回決めたアクションに取り組むことができた場合結果はどうだったのか、継続していくべきかを話し合います。取り組んでみた結果効果がなければ他にやってみたい案を考えます。 取り組むことができなかった場合、なぜできなかったかを考えます。時間がなかっただけなのか何か問題があるのかを明らかにします。何度も時間がないことが理由になる場合、そのアクションは重要ではないことが多いため思い切ってやめてしまうこともあります。 前回あがった問題の中でまだ解決できていない問題についてもここで確認します。何か進展があれば議論し、解決のためにやってみたいことがあれば案を出し合います。 大きな問題は1回の振り返り会で解決できないことがあるため、このように次回に持ち越していき少しずつ解決のために取り組んでいきます。 やったこと・良かったことを洗い出す 今週やったこと・良かったことをできるだけ多くあげていきます。 これはYWTという振り返り手法におけるY(やったこと)とKPTにおけるKEEP(今後も続けたいことや良かったこと)を融合させたフェーズです。 Y(やったこと)もあげるのは今週起きたことを全員で思い出すためと、話しているうちに良かったことや課題が見つかることがあるためです。 また、良かったことのみとすると素晴らしい出来事をあげなくてはいけない気がして、全く出てこなくなってしまうことを避けるためです。 良かったこととしてあげるほどでもないことを、やったこととしてならば言いやすいこともあります。 例えば「〇〇の機能を無事リリースしました!」などです。スケジュール通り問題なくリリースできたならば良かったこととして捉えられますが、人によっては当然のことと考えるかもしれません。 深掘りしてみると実はスケジュール通り進めるために様々な工夫しており、チームのナレッジにすべきことが隠れているかもしれません。 もっと良くできそうなことを洗い出す ここで重要なのは「問題点」ではなく「もっと良くできそうなこと」を洗い出すことです。 「問題点」としてしまうと現在発生している問題にのみフォーカスしてしまい、今後問題になりそうなことやなんとなくモヤモヤしていることについて話す場がなくなってしまいます。 問題になっていない些細なことを共有するのは非常に大切です。 誰も気付いていない今後大きな問題になる可能性に気づくことができるかもしれませんし、話してみた結果問題ではないことを知ることができるかもしれません。 いずれにせよ周りのメンバーが事象に対してどのように捉えているかを知れる機会になり、チーム内の相互理解を促進させてくれるはずです。 共通認識を生み出す 実際の振り返り会で「プルリクエストのレビュー依頼が多く出ていたので優先的に進めるべきだった」という意見がありました。 当事者としてはレビューを溜めてしまったことに問題を感じて出した意見だと思いますが、チームとしては限られたリソースの中でレビューを回しており、差し込みの対応依頼などもあったため妥当な対応で問題ではなかったという着地になりました。 「問題ではなかった」という結論を導くための対話を通じて、チーム内にこのような状況であれば「レビューが溜まることがある」という共通認識が生まれています。 今後同じ状況になった時レビューする側は必要以上に焦ってレビューせずにすみますし、レビューされる側も時間がかかりそうということを事前に認識することができます。 このように振り返り会では問題を解決するだけでなく、共通認識を作ることができるという点でも効果的な機会となっています。 批判する会ではない このフェーズでは問題を起こした誰かを責めるのではなく、チームとして もっと良くできそうなことを考える というポジティブな議論指向が重要なポイントだと思います。 他のフェーズにも共通して言えることですが意見を出すハードルを下げることが大切で、課題感はあるけど自分が責められそうだからやめておこう、、、とならない雰囲気づくりを心がける必要があります。 やってみたいことを考える 「もっと良くできそうなこと」のためにやってみたいことや、新しい試みとしてやってみたいことをあげます。 このフェーズではやってみたいことをできるだけ多く考えるブレスト形式であることを重視しています。 突拍子もないアイディアから素晴らしい改善策を思いつくかもしれませんし、現実的ではない理想論から妥当な策に落ち着かせることができるかもしれません。 よくあるNGパターン 問題の逆を実行する改善案があげられることがあります。「〇〇ができていなかった」という問題に対し「〇〇をやる」というようなものです。 例えば「レビュー依頼を溜めてしまった」という問題に対し「溜めないようにする」といった改善案です。大抵の場合このような案は精神論になり解決に導くことはできないでしょう。 そのためにとるべきアプローチとして「レビュー依頼を溜めてしまった」ことでどこに支障をきたしているのか、何が要因なのかを整理しましょう。 「レビュー依頼を溜めてしまった」のならば「レビューがボトルネックになりリードタイムが長くなる」ことが実質的な問題点で、要因は「レビューに時間がかかる」「レビュー依頼されていることを忘れていた」「レビュアーが1人しかいない」など様々考えられるでしょう。 要因によって改善策は大きく変わるため、ファシリテーターを中心に分析を行ってからやってみたいことを考えるようにするとスムーズに進行できます。 やることを決める やってみたいことをブレストした後、このフェーズで次の振り返り会までに取り組むアクションを決めます。 たくさんの案が出ているはずなので、実際に実行できる粒度・内容に整理する必要があります。 あまり多くのアクションを決定しても実行できないため、いくつか選択するのが良いでしょう。選択の仕方は効果的なものを選んでもいいですし、投票でもいいです。 チームでは、やるべきことを決めたらタスク管理ツールで管理するようにしており、必要であれば担当者のアサインや期限までその場で決めてしまいます。 おわりに 以上、チームで実際に行っている振り返り会のやり方を紹介させていただきました。 私の考えが多分に含まれているためチームメイトは違う考えを持って振り返りをしているかもしれません。 チームや状況によって適したやり方は異なるため上記の方法では上手くいかないこともあると思います。また、最初から効果的な振り返り会を行うのは難しいかもしれません。 しかしながら振り返り会自体の改善を行ったり、チームの問題を解決していくプロセスは「協調するチーム」作りに大きく寄与すると思いますので、是非継続して振り返り会を開催してみてください。 これから振り返り会をやってみようという方、やり方を模索している方の参考になれば幸いです。
アバター
Delta LakeとLakehouseプラットフォームによるデータウェアハウス設計  こんにちは。ビッグデータ処理基盤の物理レイヤーから論理レイヤーの設計実装、データエンジニアやデータサイエンティストのタスク管理全般を担当している、Data/AI部門の 何でも屋 マネージャの @smdmts です。  この記事は、弊社のデータ基盤の大部分を支えるDelta LakeとLakehouseプラットフォームによるデータウェアハウス設計の紹介です。 Databricks社が主体となり開発している Delta Lake をご存じでしょうか?  Delta Lakeは、Apache Sparkを利用したLakehouseプラットフォームを実装可能とするオープンソースです。 Lakehouseプラットフォームの詳細は、こちらの 論文 に記載されています。 Lakehouseプラットフォームとは、一つのデータレイクのプラットフォームにETL処理、BI、レポート、データサイエンス、マシンラーニングを搭載することで、性能面やコスト面・仕様変更に強いなど、多方面で有利に働くとされます。 Delta Lakeとは  Delta Lakeは、以下の公式サイトの delta.io の図にあるとおり、S3やGCSなどのストレージレイヤーに機械学習や目的別に特化したデータ構造のアーキテクチャパターンです。 Delta Lakeは主にApache SparkからのRead/Writeをサポートしていますが、制約つきで Presto/Athenaによる読込 もできます。 DELTA LAKE  公式サイトで紹介されている以下の動画によると、Delta Lakeを利用した場合のデータ構造を、以下のように、Bronze、Silver、Goldと定義される三段階に構造を分離すると、より信頼性の高いデータレイクの構築可能にするとされます。 ステージ データの内容 Bronze Ingestion Tablesと呼ばれる、生ログを保存するステージ Silver Refined Tablesと呼ばれる、Bronzeテーブルをクレンジングした中間テーブル Gold Feature/Aggregation Data Storeと呼ばれる、目的別に特化したテーブル Delta LakeとLakehouseプラットフォーム  Delta Lakeに関わらずデータレイクで何らかのデータを取り扱う場合、アプリケーションのドメイン知識の考慮が必要です。 一般的なアプリケーションでは、ドメイン知識の原料となるユビギタス言語を元にデータモデルの設計がされますが、イベントソーシングを利用しない限り、ドメインモデルが出力するデータモデルの変更は可能です。 たとえば、 DELISH KITCHEN は、レシピ動画を視聴出来るサービスですが、「動画」と「レシピ」などのコアとなるドメインモデルがある事に対して、仕様変更などで「レシピ」に何らかの新しい付加情報となるデータモデルの変更や追加は可能です。  一方でデータ基盤におけるドメイン知識とは、KPIやKGIなどの観測したい対象を指します。 たとえば、動画におけるデータ分析のドメイン知識では「視聴数」や「視聴維持率」などがその対象となります。  データウェアハウスで管理されるイベントログは、基本的に過去に保存したデータモデルの変更は許されず、将来仕様変更が発生した場合でも、データ構造はKPIなどの観測したい事象に追随する必要があります。 そのため、以下のように各ステージ毎の領域別でドメイン知識の保有などの考慮が必要となります。 Bronzeステージ(生ログ) データソースから発生するデータ構造を極力変更しないデータ領域 基本的に生ログで最小限の構文解析のみ行いドメイン知識を有さない Silverステージ(クレンジング/一次集計テーブル) データ構造の仕様変更などに追随するバッファーとなるデータ領域 BronzeステージとSilverステージのデータを集計対象とする 生ログからイベント毎に分割するなど最小のドメイン知識を有する Goldステージ(最終集計テーブル) ビジネス上の価値が観測できる多くのドメイン知識を有するデータ領域 SparkやPrestoなどから読み込まれる BIツールやMLなどから利用し、エンドユーザーの知識や知恵となり得る  このように各ステージ毎にデータが持つ役割を明確にすると、観測対象となるドメイン知識の全てがGoldステージに集約されます。 また、ドメイン知識の原料となるデータとして、SilverステージとBronzeステージにデータが保存されると明文化されます。  Bronzeステージには生ログが保存され、Silverステージにはイベント毎などで分割された最小限の粒度となるドメイン知識を有するデータが保存されます。 データが保持する情報の抽象度はBronze、Silver、Goldの順番に上がり、最終的にビジネスに何らかの役に立つドメイン知識となる情報がGoldステージで参照可能となります。  Lakehouseプラットフォームのアーキテクチャは以下の図の通り、データレイクに対して一つのエンドポイントでさまざまなデータを参照可能とする仕組みです。 データレイク内のデータをドメイン知識の保有の有無など抽象度の異なるデータをBronze、Silver、Goldと分離すると、データガバナンスに良い影響をもたらす事が期待できます。 Lakehouseアーキテクチャ Delta Lakeと関心の分離  ビッグデータの処理基盤は入力元となるデータ源泉は多種多様でカオスになりがちですが、Lakehouseプラットフォーム内のデータ構造をBronze、Silver、Goldの各ステージでデータを蒸留すると、関心の分離が促進されます。 関心の分離はSoC(Separation of Concerns)とも呼ばれ、オブジェクト指向設計やモジュール設計で重要とされる「凝集度」や「結合度」の観点から重要な概念です。 Delta Lake内の各データ領域を利用者別に分類すると、以下のように分離できます。 Bronzeステージ(生ログ) データ入力部分を処理担当するインフラエンジニア SaaSによる外部入力データ連係を担当するデータエンジニア Silverステージ(クレンジング/一次集計テーブル) ドメインモデルを構築するデータエンジニア 自分が担当したアプリ成果を確認するアプリケーションエンジニア 探索的データ分析を行うデータサイエンティスト 目的となるKPIの検討を行うプロダクトマネージャ Goldステージ(最終集計テーブル) 機械学習のモデル精度をチューニングするデータサイエンティストや機械学習エンジニア 対顧客や経営層へのレポーティングを行うデータアナリスト 日々のKPIを観測する事業責任者や経営者、プロダクトマネージャ  データ領域における関心の分離は、各ステージのデータ設計や最終的な可視化対象の選定に当たる洞察に良い影響を与えます。 たとえば、アプリケーション開発者が開発した機能の状況を把握するためにはSilverステージを参照すれば、機能が正常に動作しているかを把握できます。 また、達成されるべきKGIに因果関係があるKPIがはっきりしない場合は、Silverステージのデータから探索的データ分析によりKPIの検討が可能です。  データが保持する抽象度がBronze、Silver、Goldと順番に上がることの裏返すと、Gold、Silver、Bronzeの順番にデータ量が増え探索可能となる情報が増えるということです。 一度集計してしまうと集計前のデータが欠落してしまうことから、新たな洞察を得たい時にはSilverステージより前のデータを利用したい場合もあります。 Goldステージのデータは特定の目的以外のデータは保持しないことからデータの持つ柔軟性は低いです。 観測したいKPIが未知の場合は、前ステージのSilverステージやBronzeステージのデータを集計し、Goldステージに昇格させるべきか検討する必要があります。  実際のアプリケーション運営の現場ではLTVなどのKGIに因果関係があるKPIを試行錯誤して発見に至るケースも多く、しばらくの間はBIツールからはSilverステージのテーブルをスキャンする事も珍しくありません。 一方でSilverステージはGoldステージと比較してデータ量が多くなることから、計算量や処理コストの観点では不利に働きます。 そのためSilverステージのスキャンで観測したいKPI決まると、Goldステージのデータを作成するバッチを作成し、BIツールからはGoldステージのテーブルを参照するようになります。  このように、データが保持する主な情報を各ステージ毎に分離すると、データ軸でも利用者毎の関心の分離が促されます。 「システムを設計する組織は、その構造をそっくりまねた設計を生み出してしまう」とコンウェイの法則の有名な一説がありますが、データ構造とその配置を定義するだけで、利用者毎の関心が綺麗に分離するのは興味深い事例ではないでしょうか。 Delta Lakeがデータレイクにもたらす恩恵  今回はDelta Lakeの機能詳細に触れませんでしたが、Delta LakeにはUpsertを可能とするMerge文、過去に保存した時点のデータに巻き戻すTime Travelなど様々な便利な機能が実装されており、Bronze、Silver、GoldのステージのETL処理を強力にサポートします。 たとえば、Bronzeステージは生ログのためアプリケーションの実装の都合で頻繁にカラム追加などのデータ構造が変更されますが、自動的にスキーマの変更を検出してマージするスキーマオートマージ機能は非常に便利です。  私が所属するデータ/AI部門のデータ基盤では、一部の機能をDelta Lakeを利用したLakehouseプラットフォームで実装していますが、仕様変更が頻繁に発生するデータ領域でもアジリティ高く即日〜三営業日程度で観測したいKPIを追加できる状況が実現できています。  データ構造をBronze、Silver、Goldとステージを分解するだけでも、データ利用者の関心の分離を促し、データガバナンスにも数多くの恩恵をもたらすため、データウェアハウス設計の参考にして頂ければ幸いです。  ここまでお読みくださり、ありがとうございました。
アバター
データ分析する前に知っておきたい因果関係と相関関係 はじめに エブリーでデータアナリストをしている近藤と申します。 元々サーバーエンジニアでGoを書いていましたが、昨年7月からデータアナリストとして働いています。 普段はデータガバナンスの整備やredashによるデータ提供、データによる営業支援といった業務を行っています。 因果関係と相関関係の理解 データ分析を行う意義は、データの規則性を見つけて活用し、ビジネスをドライブさせることです。 しかし、見つけた規則性の解釈を誤るとビジネスに役立たず、貴重なリソースを浪費してしまいます。 規則性を見つけて終わりではなく、見つけた規則性が一体何を意味するのかを常に考えなければいけません。 特に相関関係と因果関係の混同はよく起こりうる問題です。相関関係だけをみて因果関係があると判断すると、おそらく効果のある施策を打つことはできないでしょう。 因果関係と相関関係の違いの理解はデータ分析をする上では必須と言えます。 そこで、因果関係と相関関係を理解してデータ分析をするための考え方をまとめたスライドを作成しました。 テックブログなのにSEO最悪なのでCTOに怒られそうですが、自分が伝えたいことはスライドのほうが伝わるのでスライドにしました。 是非ご覧いただければ幸いです。 まとめ 相関関係を見つけると因果関係がどのように存在しているのかを考え、仮説を立ててリサーチデザインを決め、データを収集・分析し、因果関係に迫っていく必要があります。 相関関係と因果関係を混同しないように気をつけましょう!!
アバター
運用していたAPI Serverが気づいたら異常終了するようになっていた話 はじめに 今回は運用していたAPI Serverが気づいたら異常終了するようになっており、原因の特定と対策をした話をしようと思います。 発生していた障害 今回発生していた障害の詳細は以下になります。 ECS上で運用していたAPI Serverが異常終了するようになっていた タスクの終了ステータスを監視するスクリプトを動かし始めたタイミングで発覚 ExitCode 2 でタスクが終了している 異常終了は発生する日としない日がある 同一の日に複数回発生はしていない 異常終了が発生するのは12時から13時の間 タスク数は2で起動していたが、2つのタスクが同日に異常終了することはなかった 異常終了する直前のメトリクスに通常時と異なる箇所は見られなかった 外形監視はしていたのですが、タスクの終了ステータスは監視していなかったため発見が遅れました。 また、発見が遅れたためどの変更が原因でいつから異常終了するようになっていたのかがわからない状態でした。 原因調査 調査1 : コードの更新 まず最初に ExitCode 2 でタスクが終了していることから panic が発生しているのではないかと考えました。 今回異常終了していたAPI Serverは、同一のdocker imageを使用し、環境変数によって内部向け・外部向けを変更する構成になっており、外部向けの方でのみ異常終了は発生していました。 外部向けのAPI Serverに関しては、自動デプロイの対象になっておらず直近でデプロイも行われていなかったため、内部向けAPI Serverと差分が発生している状態でした。 差分が発生し、外部向けAPI Serverでのみ異常終了が発生していたため、差分に原因があるのではないかと考え差分をなくすためにデプロイを実施しました。 しかし、差分がなくなった状態でも状況に変化はなく、外部向けAPI Serverでのみ異常終了は発生し続けました。 調査2 : アクセスに起因したものではないか 調査1にて内部向けとの差分をなくしても状況に変化がなかったで、次は特定のリクエストによって発生しているのではないかと考えました。 API Serverではアクセスログを出力していたのですが、このアクセスログはレスポンスを返すタイミングで出力していたため、処理の途中で異常終了してしまった場合にはログは出力されていません。 そこで、調査のために処理の途中でも適宜ログを出力するようにして、処理途中で異常終了した場合にもどんなリクエストが来ていたかわかるよう変更を加えました。 しかし、異常終了が発生した後にログを確認したところ、該当の時間に処理を行っているログは出力されていませんでした。 調査3 : システム系を疑う 調査2によって、リクエストによって発生しているわけではないことがわかったので、API Serverのコード以外の要素で異常終了する理由がないかと考え調査を続けていました。 異常終了が発生するのは12時から13時の間だけのため、この時間帯に何かしらの処理が動いて、それが原因なのではないかと考えました。 API Serverのコンテナが動いているインスタンスにて該当の時間帯に動いている処理を確認したところ、ログローテートの処理がありました。 ログローテートの設定は下記のようになっていました。 { missingok notifempty compress delaycompress daily rotate 7 postrotate docker container kill -s HUP `docker ps | grep <image-name> | awk '{print $1}'` 2> /dev/null || true endscript sharedscripts } ログローテート後に、ログの出力先ファイルを変更するために条件に合致するコンテナに対して SIGHUP シグナルを送っていました。 ここではシグナルを送る先として grep <image-name> で対象のコンテナをしぼっています。 調査1にて記載していますが、異常終了していたAPI Serverは同一のdocker imageを使用し、環境変数で内部向け・外部向けを変更するようになっています。 そのため、内部向けと外部向けのAPI Serverが同一のインスタンスに存在した場合、実際にはログローテートをしていない方のAPI Serverにもシグナルが送られるようになっていました。 どちらのAPI Serverでも SIGHUP をハンドリングするようになっている場合には問題はないのですが、外向けのAPI Serverでは SIGHUP のハンドリングをするようになっていませんでした。 確認のため、検証環境にて外向けのAPI Serverに対して SIGHUP シグナルを送ってみると異常終了することが確認できました。 行った対応 原因の特定ができたので、対応策を考えます。 今回候補に上がった対応策は下記の3つになります。 - SIGHUP を送る先の抽出条件を修正する - 内向けと外向けのimage名を分離する - シグナルをハンドリングする 本来でしたら3つすべて実施したほうがいいのですが、 まずは応急処置として実装工数が一番少なく済むと判断した、シグナルハンドリングの修正を行うことにしました。 DELISH KITCHENではGoでAPI Serverの実装を行っており、Goではシグナルハンドリングos/signageパッケージに定義されている Ignore メソッドを使えばできます。 https://golang.org/pkg/os/signal/#Ignore 実際に追加した処理は下記になります。 signal.Ignore(syscall.SIGHUP) 上記の対応を実施したあと、検証環境にて外向けのAPI Serverに SIGHUP を送ったところ問題なく稼働し続けていることが確認できました。 振り返り 今回は ExitCode 2 でAPI Serverが終了していたという情報と障害が発生していた時間から原因を想像して、対処をすることができました。 対応後にチーム内にて簡単に振り返りを実施してみたところ、トレースを実施することでより詳しい情報が取得でき、原因の特定がスムーズにできたのではないかという意見がありました。 トレースする対象としてはシステムコール・パケット・ブロックIO等が考えられます。 今回の障害の場合、システムコールをトレースしてみれば SIGHUP が送られて来ていたことがわかったはずです。 実際にシステムコールをトレースしてみた例を下記に示します。 今回障害が発生していたAPI ServerはGoで記述したものをdocker上で動かしており、dockerを動かしているホスト及びAPI Serverが起動しているコンテナ内に strace がインストールされていないため、PID名前空間を共有したコンテナを起動し、起動したコンテナ内で strace を実行しています。 echo -e 'FROM alpine\nRUN apk add --no-cache strace' \ | docker build -t debug -f - . \ && docker run -it --rm --pid container:<containe_id> --cap-add sys_ptrace debug strace -fp 1 docekrで動かしているコンテナに対して、別のコンテナから strace を動かす方法については、下記のサイトを参考にさせていただきました。 https://qiita.com/minamijoyo/items/9dd59109e9fe1a35f888 straceをした状態で SIGHUP を受信するした時のログは下記になります。 [pid 6] nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...> [pid 13] <... futex resumed>) = 0 [pid 13] futex(0xc000211d48, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...> [pid 6] <... nanosleep resumed>NULL) = 0 [pid 6] futex(0x17c4e78, FUTEX_WAIT_PRIVATE, 0, {tv_sec=59, tv_nsec=137259289} <unfinished ...> [pid 10] <... epoll_pwait resumed>[], 128, 1, NULL, 0) = 0 [pid 10] epoll_pwait(3, [], 128, 0, NULL, 2) = 0 [pid 10] epoll_pwait(3, <unfinished ...> [pid 1] <... futex resumed>) = ? ERESTARTSYS (To be restarted if SA_RESTART is set) [pid 1] --- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=0, si_uid=0} --- [pid 1] futex(0x17efbc0, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...> [pid 10] <... epoll_pwait resumed>[{EPOLLIN, {u32=4118929128, u64=140509679050472}}], 128, 59136, NULL, 0) = 1 [pid 10] futex(0x17c4e78, FUTEX_WAKE_PRIVATE, 1) = 1 [pid 6] <... futex resumed>) = 0 障害が起こっているAPI Serverに対して strace を実行し上記のようなログがでていることを確認できていれば、どこからか SIGHUP が送られてきていることがわかり、調査をスムーズに進めることができたと思いました。 しかし、トレースを実施すると何かしらオーバヘッド等が発生するため、なるべくなら検証環境などで不具合を再現し、その環境でトレースを行うことが望ましいです。 ですが、今回のように再現が困難な場合にはオーバーヘッドが発生することを考慮にいれ、本番環境でトレースを行うことも1つの方法としてあったと思います。 さいごに 今回は実際に起こった障害の事例を元にどういったことを考え調べていったのかについて話しました。 障害の調査をする時には、想像力を働かせて色々な原因を考えて一つ一つ確認していくことになると思います。 その時今回のように気づくのが遅れてしまうと、考えうる原因が増え対応の時間が長引くだけでなく難易度もあがってしまいます。 こうならないためにも、適切な監視を設定することが大事だと改めて感じることができました。 今回のような失敗談を記事にすることで、みなさんの障害調査の時の手助けや監視設定を見直すきっかけになれば幸いです。
アバター
はじめに 日本時間の2021年2月25日に Jetpack Compose のbeta版がリリースされました。APIも固まってきたようですので触ってみた範囲のうち、導入的なところをコードで示しつつ、感想を述べていきます。 使用環境 使用した環境は以下の通りです。他にもandroidx.activityなどにcomposeがありますが、いずれも2021年3月15日時点で最新のバージョンを使用しました。 バージョンはJetpackのLibraries(*1)から調べることができます。 Android Studio Arctic Fox | 2020.3.1 Canary 9 androidx.compose 1.0.0-beta02 最初につくるもの トップレベルの関数に @Composable を指定することで、その関数内にてComposeを使用したレイアウトを組めます。合わせて @Preview を指定すればAndroid Studio上でプレビューもできます。 このプレビューは同時に複数表示可能なので、プレビュー用の関数を複数作成すればダークテーマ対応有り/無しの表示を同時に確認できます。 @Composable fun MyScreen() { // ここでComposeを使用して表示を組む } @Preview @Composable fun PreviewMyScreen() { // MyScreenで組んだ表示がAndroid Studio上にプレビューされる MyScreen() } レイアウトたち Box、Column、Rowがそれぞれ従来のFrameLayoutやinearLayoutに相当しています。 Box { // 重なる Text("hoge") Text("piyo") } Column { // 縦に並ぶ Text("hoge") Text("piyo") } Row { // 横に並ぶ Text("hoge") Text("piyo") } 他に、gradleファイルに指定を追加することでCompose版のConstraintLayoutも使えますが、公式Document中のConstraintLayoutの補足(*2)を読むと無理して使わなくても良さそうです。 // テキスト2つを縦に並べる ConstraintLayout { val (text1, text2) = createRefs() Text( "hoge", modifier = Modifier.constrainAs(text1) { linkTo( parent.start, parent.top, parent.end, text2.top, 0.dp, 0.dp, 0.dp, 8.dp ) } ) Text( "piyo", modifier = Modifier.constrainAs(text2) { linkTo( parent.start, text1.bottom, parent.end, parent.bottom, 0.dp, 0.dp, 0.dp, 0.dp ) } ) } 表示のパーツたち Text、Button、Image、Cardなど多くの表示が揃っています。Spacerなるものもあり、わかりやすくmarginを仕込めます。 ただ、RecyclerView(ListView)相当がLazyColumn(or LazyRow)という名称であったりと、一部は従来の名前から大きく変わっている点に注意が必要です。 val items = (0 until 100).map { "item $it" } LazyColumn( // 項目の間隔を空ける verticalArrangement = Arrangement.spacedBy(8.dp) ) { item { // リストの一番上に横スクロールのリストを入れる LazyRow( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { items(items) { Text(it) } } } items(items) { // 縦スクロールのリストの項目としてテキストとボタンを横に並べる Row { Text(it) Spacer(modifier = Modifier.width(8.dp)) Button(onClick = { // ボタンクリック時の処理 }) { Text("button") } } } } ものが多すぎるので使いたいものを公式Reference(*3)から頑張って探す必要があります。androidx.composeパッケージ関連を漁れば色々と見つかります。 表示の設定を変更する これまでレイアウトのxmlで指定していたlayout_widthやpaddingなどは Modifier というobjectを通して設定します。 Modifierにサイズやpaddingを設定する拡張関数があり、ものによってはColumnなどのscope限定で使える拡張関数が存在していることもあります。 Box( // 縦横とも画面一杯に広げてpaddingを設ける modifier = Modifier .fillMaxSize() .padding(16.dp) ) { Text( "hoge", // 背景を赤色かつ角に丸みを与え、中央に配置する modifier = Modifier .background(Color.Red, shape = RoundedCornerShape(8.dp)) .align(Alignment.Center) ) } 表示の操作として行えることはModifierの関数だけなのでわかりやすいです。 ガワを作る Scaffold() でMaterial Designに則った画面を簡単に構築できます。各種AppBarやFABを設定できる口があるので、従って作るだけでそれらしい画面になります。 Scaffold( // 他にもbottomBarやfloatingActionButtonなどを設定できる口がある topBar = { TopAppBar( title = { Text("title") }, actions = { IconButton(onClick = { // メニュークリック時の処理 }) { Icon( imageVector = Icons.Default.ImageSearch, contentDescription = "search" ) } } ) }, content = { // ここで画面の表示を作る MyContentScreen() } ) その他 viewModelを viewModel() で取得できたり、Navigationによる表示切り替えも行えるため、やりたいことは一通り行えそうであることが感じとれます。 また、これまでに作成した既存のViewは AndroidView なるものを使用することでComposeの世界に引き込んだりもできます。 他にCompose独自に覚えることとして、remember系の関数で値を保持したり、表示更新の契機としてStateを操作したりと従来にはなかった考え方を覚えて行く必要があります。 このあたりはReactのComponentで表示を作るときに近いものを感じました。 ハマったところ Android StudioがCanaryであったり、Composeがbetaであるためか、いくつかハマったところがありました。 viewModel()を使うとプレビューが表示されない viewModel()を使わずにViewModelの実体を渡すか、あるいはViewModelから取得した値だけをComposableな関数へ渡す プレビューを使わず、エミュレータや実機で確認するだけなら問題なし 自動importがよきに行われないものがあるため毎回手動でimportを書くことになるものがあった viewModel()を使うための import androidx.lifecycle.viewmodel.compose.viewModel var value by remember { mutableStateOf("") } などと by を使ってStateのvalueへのシンタックスシュガーを利用する場合の import androidx.compose.runtime.getValue / setValue stackoverflowの回答(*4)に助けられました (3/29追記) by xxx.observeAsState を使用した場合のgetValue(※7)や by remember を使用した場合のsetValueのimportなど、一部に対応されたようです BottomSheetやSnackbarの使い方のベストプラクティスがわからない Textなどと同じように作ることでとりあえず表示は行えるが、BottomSheetScaffoldやSnackbarHostなるものがあるため、よりよい使い方があると思われる さいごに 今回の記事は公式Document(*5)を一通り読んでその中のおおよそを触ったものの一部です。Composeの情報は多いため覚えきることも紹介しきることも難しいですが、触ってみた範囲ではxmlで組むより簡単に表示を構築できる印象がありました。 対応されたAndroid Strudioとともにstable版になる日が楽しみです。 Jetpack Composeを使ったチャレンジとして Android Dev Challenge (*6) なるものも開催されているので、挑んでみるのも良いと思います。 参照 *1 JetpackのLibraries https://developer.android.com/jetpack/androidx/explorer *2 公式Document中のConstraintLayoutの補足 https://developer.android.com/jetpack/compose/layout#contraintlayout のNote部分 *3 公式Reference https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary *4 stackoverflowの回答 https://stackoverflow.com/questions/64951605/var-value-by-remember-mutablestateofdefault-produce-error-why *5 公式Document https://developer.android.com/jetpack/compose *6 Android Dev Challenge こちらは最終チャレンジのWeek 4 https://android-developers.googleblog.com/2021/03/android-dev-challenge-4.html ※7 Android Studio Arctic Fox Canary 12のFixes https://androidstudio.googleblog.com/2021/03/android-studio-arctic-fox-canary-12.html
アバター
誰でもわかるStoreKitTesting はじめに はじめまして。エブリーでiOSエンジニアをしている佐藤です。 DELISH KITCHENで、主にプレミアムサービスや課金周りを担当しています。 今回は、WWDC2020で発表されたStoreKitTestingについて紹介したいと思います。 概要 概要としては以下が挙げられるかと思います。 AppleStoreサーバに接続せずにローカル課金テストができる ローカルでテスト商品を作れる 購入トランザクションの管理ができる 割引系(お試しオファー、プロモーションオファーなど)のデバッグが可能 プロモーションオファーはローカルの秘密鍵を作成可能 レシートはローカルで署名されている 課金のユニットテストが可能 ローカルで実行できる課金環境がとても充実してきていますね。 次にStoreKitTestingの導入の流れを簡単に説明していきたいと思います。 Configurationファイルを作成 まずはNew FileでStoreKit Configuration Fileを選択し、作成します。 次にサブスクリプショングループを作成します。 次に課金商品を作成していきます。 選べるのは以下の3つ。 消耗型 非消耗型 自動更新サブスクリプション 作成が終わるとこんな画面になります。 Product IDや期間などは任意で選択可能です。 オファーの作成 更にStoreKitTestingではAppleが用意している様々な購入オファーの選択も可能です。 そのひとつが、 Introductory Offer(お試しオファー) です。 これは初回購入ユーザーに対してアプローチ可能なオファーになります。 選べるのは以下の3つ Pay As You Go(都度払い) Pay Up Front(前払い) Free (無料) 2つ目が Promotional Offer(プロモーションオファー) これは再登録者や継続ユーザーへアプローチ可能なオファーです。 詳しい実装は割愛しますが、AppleStoreConnectで作成した秘密鍵をもとに署名を作成し、StoreKitでの購入時に必要なパラメータを含めると購入可能になります。 詳しい実装方法 StoreKit Testingではローカルで秘密鍵を作成可能で、それをもとに署名を行うことで購入テストが可能になります。 残念ながらiOS14から使用可能になったもうひとつのオファーである、 オファーコード はStoreKit Testingで使用することはまだできないようです。 参照 Configuration Fileを設定する Product > Scheme > Edit Scheme > Runを開き、 以下画像のように作成したConfiguration Fileを設定します。 Schemeごとに設定可能なので、 任意のSchemeに作成したConfiguration Fileを設定することでStoreKit Testingでの購入が有効になります。 様々なデバッグ StoreKitTestingでは様々な購入デバッグが可能です。 以下のようなものが設定可能です。 既定購入ストアの設定(購入する国別ストアの選択) 既定表示言語 タイムレート(購入有効期限の時間短縮率) 購入割込のデバッグ有効化 トランザクションを失敗させる(エラー種別も選択可能) 購入確認の表示 プロモーションオファーのローカル秘密鍵とKeyIDの生成 ローカル証明書の生成(StoreKitTestingでのレシート検証のため) ※選択できるエラー種別 またトランザクションの管理も行うことができます。 まとめ StoreKitTestingの設定方法をかんたんにまとめてみましたがいかがだったでしょうか。 今まで開発中は実機でSandboxでの課金テストを行っていましたが、StoreKitTestingによって開発効率は上がったように感じます。 直近ではプロモーションオファー関連の開発デバッグを簡単に行うことが出来、導入のメリットを感じることができました。 課金テストの自動化など安全面でも導入の強みはあるのではないかと思いますので、 まだ未導入の方もしくはIn App Purchase実装の練習をしたい方などはぜひお試しを!
アバター
はじめに DELISH KITCHENでは日々多くのレシピ動画を公開していますが、その動画は全てAdobe Premiere Pro(以下 Premiere Pro)を使用して編集しています。 今回はPremiere Proのエクステンションを作成して動画の編集効率を向上させた話をご紹介します。 これまで発生していた問題 レシピ動画にどのような材料を使っているか、どのような工程があるかは全てダッシュボード(データ管理用Webサイト)で管理しています。 動画編集者は動画データをPremiere Proに取り込み、対象レシピの情報をダッシュボードから開き、見比べながら作業していました。 動画にテロップを表示する際には、材料や工程の文章をダッシュボードからPremiere Proにコピペしながら編集作業を行っていました。 肉巻き半熟卵の動画テロップ例 この作業は地味に時間がかかる上にコピペミスをしやすく、手戻りが発生したりミスが残ったままレシピ動画が公開されてしまうこともありました。 そこで、編集作業の効率化 & ミスの削減のためにPremiere Proのエクステンションを開発しました。 エクステンションの紹介 エクステンションを有効にした状態でPremiere Proを開くと以下のような画面になります。 画像右上のパネルがエクステンションの画面です。 これから編集するレシピの情報が自動的に表示されるため、自分でダッシュボードを開く必要はありません。 Premiere Proでエクステンションを開いたところ レシピのタイトルを挿入するときはタイトルの項目にある緑のボタンをタイムラインの好きな位置にドラッグ&ドロップします。 そうすると自動的にDELISH KITCHENで決められたデザインのタイトルテロップが挿入されます。 あとは表示する秒数やデザインを微調整すればタイトルテロップは完成です。 タイトルテロップをドラッグ&ドロップで挿入 材料も同様に、表示させたい位置にドラッグ&ドロップすれば材料用のテロップが表示されます。 材料テロップをドラッグ&ドロップで挿入 このようにタイトルや材料などのテロップをドラッグ&ドロップで動画内に挿入できるので、これまでのコピペ作業は必要なくなり、 ポチポチするだけである程度レシピ動画が作れてしまうというのがこのエクステンションの良いところです。 使用している技術 Adobe CEP このエクステンションはAdobe CEP (Common Extensibility Platform) という技術を用いて作られています。 これはHTML5/JavaScript/Node.jsを使って様々なAdobeソフトウェアのエクステンションを開発できるというものです。 詳しくは以下のGitHubや記事で詳しく解説されています。 Adobe-CEP/Getting-Started-guides: Getting Started guides and samples for CEP extensions Adobe-CEP/CEP-Resources: Tools and documentation for building Creative Cloud app extensions with CEP CEP スーパー メガ ガイド: HTML5 + Node.js で Adobe のツールを拡張する | aphall.com エクステンションの画面は Chromium Embedded Framework というアプリケーションに埋め込んで使用するChromiumフレームワークを用いて表示しているため、 一部制限はありますがほぼ通常のChromeでHTML5/JavaScriptを実行するのと遜色ありません。 またNode.jsが実行できるためローカルファイルの読み書きも行えます。 AdobeソフトウェアのバージョンによってAdobe CEPのバージョンも変わり、それによって使えるAPIやChromium / Node.jsのバージョンも決まってくるため、 事前にサポートするバージョンを確認しておいた方が開発がスムーズになります。 参考: CEP-Resources/CEP 10.0 HTML Extension Cookbook.md at master · Adobe-CEP/CEP-Resources Nuxt.js Chromiumの上でHTML5/JavaScriptが実行できるとなると、ほぼWebサイトを作成するのと変わりません。 そのため、このエクステンションは Nuxt.js を使用しています。 (当初は素のVue.jsだったのですが、最近Nuxt.jsにリプレイスしました) DELISH KITCHEN WEB を構成する技術のお話 でも紹介したように、 DELISH KITCHENのWebサイトにはNuxt.jsを使用しています。またレシピ管理用のダッシュボードでも同様にNuxt.jsを使用しています。 Nuxt.jsはサーバーサイドレンダリング(SSR)が特徴として挙げられがちですが、SSR以外にもページのルーティングルールやコンポーネント、プラグインの設置場所などが全て定められているというのが大きな特徴で、 Nuxt.jsで開発したことのあるWebエンジニアであればすぐにプロジェクトの構成が把握できるのが強みです。 そのためこのエクステンションでもNuxt.jsを使うことによって自分以外のエンジニアがエクステンションを開発する際にもプロジェクト構成の把握が容易になり、 共通のライブラリやプラグインが使用できるため開発がスムーズになります。 工夫した点 編集対象のレシピ情報を自動的に表示する 動画を編集する際に、毎回ダッシュボードから編集対象のレシピを開くのは地味に面倒な作業なので、Premiere Proを開くと自動的にレシピの情報が表示されるようにしました。 仕組みは単純で、以前からレシピ編集のルールとしてPremiere Proのプロジェクトファイルには <レシピID>_<レシピタイトル>.prproj というような命名規則で名前をつけることが決まっていたので、 Premiere ProのAPIでファイル名を取得し、正規表現でレシピIDを抜き出してからDELISH KITCHENのAPIでレシピ情報を取得し表示しています。 ドラッグ&ドロップでテロップを挿入 なるべく直感的にテロップを挿入できるようにしたかったため、ドラッグ&ドロップで挿入できるようにしました。 ドラッグ&ドロップ自体は通常のWebと同じく drag イベントをハンドリングし dataTransfer.setData() でテロップのデータを渡せば良いのですが、 setData() の第一引数に渡すformatは "com.adobe.cep.dnd.file.0" という文字列でなければいけません。 (複数データを渡す場合は最後の数字をインクリメントしていく) 参考: Samples/ext.js#L84 Adobe-CEP/Samples PRTL ドラッグ&ドロップでテロップを挿入、と言いましたが、実際には何のファイルをドラッグ&ドロップで渡しているかというと、PRTLというフォーマットのファイルを渡しています。 これは現在Premiere Proでレガシータイトルと呼ばれているテロップのフォーマットで、XML形式のファイルになっています。 テンプレートとして使用するPRTLファイルは予めエクステンション内に保存してあり、 材料などのテロップ挿入ボタンがドラッグされた瞬間にテンプレートのPRTLを読み込み、指定された箇所の文字列を材料名で置き換えます。再度PRTLファイルとして書き出した後に dataTransfer.setData() にそのファイルパスを指定すると、 ドロップした場所にそのPRTLファイルがテロップとしてインポートされ表示されるという仕組みです。 余談 Premiere Proにはレガシータイトル以外にもエッセンシャルグラフィックスというテロップの機能があります。 こちらもテンプレートがありAPIでPremiere Proに取り込むことができるのですが、若干扱いづらく、XML形式であることや直感的にドラッグ&ドロップで取り込めるという点でPRTLを採用しました。 ただし、PRTLはフォーマット仕様が公開されておらず(公開されていたらどなたか教えて下さい)、独自にパースする必要があります。 また、Premiere ProでテロップをPRTLファイルとして書き出せるのはCS6までで、それ以降のバージョンでは書き出し機能は削除されてしまいました。 このような状況で、将来的にPremiere Proがレガシータイトルをサポートしなくなる恐れもあるため、できればエッセンシャルグラフィックスを使ってテロップを挿入できるようにしたいところです。 エクステンションのパッケージング Adobe CEPのエクステンション自体はPremiere Proのエクステンション用のディレクトリに配置すれば使用できるのですが、動画編集をするスタッフに毎回その場所に配置してもらうのも手間がかかります。 そこで、エクステンションをインストーラーでインストールできるようにしました。 全員Mac上でPremiere Proを使用するので、インストーラーとしてpkgファイルを生成します。 パッケージングの詳細や使用したコマンド、npmパッケージなどは以下を参考にしてください。 Packaging and Signing Adobe Extensions CEP-Resources/ZXPSignCMD at master · Adobe-CEP/CEP-Resources codearoni/zxp-sign-cmd: A JS wrapper for Adobe's extension signer パッケージングを手動で行うのも面倒なので、GitHub上でPullRequestをmasterにマージした際にCIでパッケージングし、 そのpkgファイルを tcnksm/ghr を使用してGitHub Releasesに自動的にアップロードするようにしました。 これでpkgファイルをGitHub Releasesからダウンロードして開けばMacでおなじみのインストーラーが起動し、エクステンションをインストールできるようになります。 エクステンションのインストーラー まとめ このエクステンションを使うことによってPremiere Pro内でテロップの挿入が完結できるようになりました。 また、ドラッグ&ドロップでテロップを非常に簡単に挿入できるようになり、コピペミスを減らすことができました。 もちろん動画編集作業はこれだけで終わりではないのですが、地味に時間のかかっていた作業を減らすことができ、編集効率を向上させることができました。 このPremiere Proのエクステンションは2018年に作成したものですが、3年弱経った現在も毎日使われています。 自分が作ったものが毎日使ってもらえているというのは開発者冥利に尽きる思いですね。 DELISH KITCHENのサービスも毎日沢山の方に使っていただくため日々開発に励んでいます。 これからもDELISH KITCHENをよろしくお願いします。
アバター
はじめに MAMADAYSにはiOSとAndroidのアプリがあります。 Flutterなどのクロスプラットフォーム開発ではなく、それぞれネイティブで開発しています。 この記事ではMAMADAYSのiOSアプリの全体的な構成を紹介します。 全体の雰囲気を掴んでもらうことを目的とし、細かい採用技術はまた別の機会に紹介できればと思います。 MAMADAYSアプリの機能 MAMADAYSアプリには大きく次のような機能があります。 メディア MAMADAYSには動画を含む数千本の記事があり、ユーザーさんの状態やお子さんの月齢に合わせて適切な記事をおすすめする仕組みがあります。 リリース当初は記事を WKWebView で表示していましたが、表示速度向上などのためにネイティブに変更しています。 動画再生には AVPlayer を用いていて、自動再生処理や待ち時間短縮のために、それなりに複雑な非同期的な制御が必要になっています。 育児記録 お子さんの睡眠や授乳などを簡単な操作で記録し、グラフでわかりやすく振り返ることができる機能です。 この機能のみ、 Firebase Cloud Firestore を利用しています。 Firebase Cloud Firestore の採用には以下のような利点がありました。 サーバーの実装が不要 クライアントの実装が簡単 非同期通信で待ち時間なし 入力した記録をパートナーにリアルタイムに同期できる リリース当初の時間が無い中では、工数削減、品質向上に非常に役立ったと思います。 ただし、育児記録機能と他機能の連携を進める上で障害になってきているため、将来的には独自のAPIに置き換えることになりそうです。 離乳食 離乳食の進み具合を記録したり、時期にあった調理法を探せる食材リスト機能があります。 カレンダー 家族の予定を一つのカレンダーで共有できます。 また、思い出として写真をアップロードし、簡易的なアルバムのようにする機能があります。 パートナー共有 上記全ての機能はデータを家族内で共有できるようになっています。 アーキテクチャー アーキテクチャーはMVVMとしています。 ただし ViewController から ViewModel の接続はメソッドコール、 ViewModel から ViewController の接続はクロージャーで、Rx等によるバインディングを用いていないので一般的なMVVMとは異なるかもしれません。 Viewはほぼ UITableView で構成されており、Viewの更新は UITableView のリロードですることが多いため、個別のデータをバインディングするメリットを感じなかったためにこのような構成になっています。 また、ViewControllerの階層化を積極的に使っています。 コンテナを使って階層化することによってView Controllerの肥大化を防ぐようにしています。 まとめ MAMADAYSのモバイルアプリは2019年10月にリリースして以来コンスタントにアップデートを続け、離乳食、カレンダー、妊娠週数と大きめの機能を追加してきました。 今後もユーザーさんの声を聴きながら進化を続けていきます。 開発者としてはコードをできるだけシンプルで変更に強い状態に保つことで、開発速度と品質でサービスに貢献したいと考えています。
アバター
DELISH KITCHEN RS事業部では、小売向けにサイネージやチラシ等のサービスを提供しています。 従来は、そのサービスの管理が出来るWebアプリのみ運用していたのですが、新たに広告配信設定用のWebアプリが必要になりました。 そこで Nx を使って、2つのアプリをmonorepoで管理し、コードの共通化を計りました。 Nxとは Nx はmonorepo用の拡張可能な開発ツールセットです。堅牢なCLI、キャッシュシステム、依存性管理などを提供すると共に、Jest、Cypress、ESLint、Prettierなどのモダンなライブラリの統合をサポートしています。元GoogleのAngularチームにいたメンバーによって創設されたNrwlが開発しており、Googleは全てのプロジェクトをmonorepoで管理しているという有名な話がありますが(詳細は知りません)、それと似た開発体験を提供することを目的に開発されているそうです。 Nxへの移行 RS事業部で開発しているWebアプリはAngularで作られており、それをまずNxに移行しました。 従来のディレクトリ構成 NxはAngularをサポートしているので、移行自体は簡単でした。 まずNxで新しくworkspaceを作成します。 npx create-nx-workspace --preset=angular その後、既存のアプリのコードを apps 以下に配置し、angular.jsonやtsconfig.json、tslint.jsonなどの設定ファイルを修正し、既存のアプリで使用していたサードパーティのライブラリ(dayjsなど)を新しいworkspaceに追加して、移行を完了しました。 現在のディレクトリ構成 ※ 現在のcreate-nx-workspaceは、テストフレームワークにデフォルトでJestとCypressが選択されており、AngularデフォルトのKarma、Protractorを使用したい場合は、別途以下のコマンドでアプリを作成する必要があります。 nx generate @nrwl/angular:app myapp --unit-test-runner=karma --e2e-test-runner=protractor` ライブラリの作成 複数のアプリから共通のコードを使用するために、ライブラリを作成します。 nx generate @nrwl/angular:lib shared 上記のコマンドで libs 配下に shared ディレクトリが作成されます。 今回は例として SampleComponent をライブラリに作成します。 nx generate component sample --project=shared 作成したら、 shared.module.ts のexportsに SampleComponent を追加します。 次に、アプリから SampleComponent を使用するために、tsconfigにパスを追加します。 { ... "compilerOptions": { ... "paths": { "@lib/shared": ["libs/shared/src/index.ts"], "@lib/shared/*": ["libs/shared/src/lib/*"] }, ... }, ... } あとは使用したいモジュールでimportすると、使用可能になります。 import { SharedModule } from '@lib/shared'; @NgModule({ imports: [SharedModule], bootstrap: [AppComponent] }) export class AppModule {} また、直接 SampleComponent をimportしたい場合は、以下のコードで可能です。 import { SampleComponent } from '@lib/shared/sample/sample.component.ts' CSSの共通化 上記で作成したsharedライブラリに共通のCSSも置いて、使えるようにします。 場所はどこでもいいのですが、私は libs/shared/src/styles にファイルを配置しています。 html, body { height: 100%; } body { margin: 0; font-family: 'Helvetica Neue', Arial, 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', Meiryo, sans-serif; } そしたら、angular.jsonの、このスタイルを適用したいアプリの箇所に以下を追加します。 "app": { "projectType": "application", ... "architect": { "build": { ... "options": { ... "styles": [ "libs/shared/src/styles/styles.scss", // この部分 ], ... } } } } また、partialファイル(_mixin.scssなど)をsharedに置いて参照することも可能です。 これも好きな場所にファイルを配置して、angular.jsonでパスを指定するだけです。 "app": { "projectType": "application", ... "architect": { "build": { ... "options": { ... "stylePreprocessorOptions": { "includePaths": ["libs/shared/src/styles/partials"], // この部分 }, ... } } } } ここで注意なのが、指定するのはファイルパスではなくディレクトリパスということです。こうしておくことで、partials以下にあるファイル(_mixin.scss)を @import 'mixin' という形で使うことができます。 CI/CD monorepoにするとCI/CD周りも変わってきます。おそらく多くの人が、変更があったアプリ、またはライブラリだけをテスト、デプロイしたいと考えると思います。Nxはその希望を叶えてくれます。Nxには affected コマンドがあり、変更の影響があるプロジェクトのみに対してテスト、ビルドを実行する機能があります。 CI/CDはGithub Actionsで行っていて、実際のワークフローを例に紹介したいと思います。Github ActionsはPull Requestを作った時と develop 、 master ブランチにマージされたタイミングで走るように設定しています。 例えば、Pull Requestを作った時に走らせるビルドは以下のように設定しています。 name: build on: pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - uses: actions/setup-node@v1 with: node-version: '12.x' - name: Run cache/restore node_modules uses: actions/cache@v1 with: path: node_modules key: v1-${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }} - name: Run build run: make affected-build BASE=${{ github.event.pull_request.base.sha }} // nx affected:build --base=${BASE} を呼んでるだけ ここで、 affected はbase optionでブランチやコミットIDを指定でき、baseとHEADの差分から、影響のあるプロジェクトを判断します。その仕様から、 actions/checkout@v2 では fetch-depth: 0 を指定することで、コミット履歴を全部取得するようにしています。 また master にマージされた際のデプロイは以下のように設定しています。 name: deploy on: push: branches: - master jobs: build: ... deploy: needs: build runs-on: ubuntu-latest steps: - name: Run cache/restore dist uses: actions/cache@v1 with: path: dist key: v1-${{ runner.os }}-dist-${{ github.run_number }} - name: Run deploy run: | app_paths="dist/apps/*/" if ! ls -d $app_paths &>/dev/null ; then exit 0 fi for app_path in $app_paths; do app=$(basename "$app_path") if [ "$app" == "appName1" ]; then elif [ "$app" == "appName2" ]; then fi done 差分があるもののみデプロイするという仕組みはNx自体にはないので、buildフェーズで生成されたものをデプロイするというスクリプトを書いて対応しています。 現在のデプロイの仕組みだと、 master に入ったものは全部対象になってしまうので、リリース前に全プロジェクトの確認が必要になってしまいます。また master に入ってもリリースのタイミングは調整したいこともあるでしょう。monorepoにして一番の課題で、依然他に良いフローがないか検討中の部分です。 まとめ 今回はNxを使ってnpm projectをmonorepo管理した話をしました。Nxを使ったComponentやCSSの共通化、CI/CDの運用とかは普段聞く機会がないので、誰かの参考になれば幸いです。
アバター