every Tech Blog

株式会社エブリーのTech Blogです。

Pyroscope の Continuous Profiling により Go サーバーのメモリリークを調査・改善した話

Eye-catching image

はじめに

子育てメディア「トモニテ」でバックエンドやフロントエンドの設計・開発を担当している桝村です。

2023年8月1日、MAMADAYSはトモニテに生まれかわりました。

tomonite.com

アプリのメイン機能である「育児記録」「妊娠週数管理」「食材リスト」を軸として、家族やパートナー、家族以外の人や社会との接点を作るためのシェア機能やコミュニティ機能などの拡充をめざしていきます。

今回は、Continuous Profiling を実施することができる Pyroscope を使用して、トモニテで運用している Go サーバーのメモリリークを調査・改善した話をしたいと思います。

以前、トモニテでEKSからECSに移行した話という記事において、EKS on EC2 から ECS Fargate への移行に伴い、厳密なリソース管理の必要性が生じ、メモリリークを検出した件の対応になります。

tech.every.tv

Pyroscope について

Pyroscope とは

Continuous Profiling を実施することができるオープンソースのプラットフォームです。

Continuous Profiling とは、ソフトウェアやアプリケーションが実行されている間、リアルタイムでパフォーマンスデータや実行情報を収集し、CPUやメモリ等のリソースをどこで多く消費しているか分析・チューニングする手法です。

2023年3月にデータ可視化ツール Grafana などを開発している Grafana Labs が Pyroscope を買収し、 オープンソース Grafana Pyroscope として統合されました。

本記事では Grafana Pyroscope の情報にも触れつつ、Pyroscope で Go サーバーのメモリリークを調査・改善した話となります。

Pyroscope でできること

  • コード内のパフォーマンスに関連する問題やボトルネックを見つけることができます。例えば、CPU使用率の上昇やメモリリークの発生等です。

pyroscope single view
pyroscope の Single View 画面

  • タグ機能 Tags や時間指定により、UI上のプロファイリングデータを絞り込みできます。

pyroscope tagged view
pyroscope の Tags でのフィルタリング画面

  • ビュー機能 Comparison View により、2つの時間区間を並べて比較分析できます。

pyroscope comparison view
pyroscope の Comparison View 画面

  • ビュー機能 Diff View により、2つの時間区間の差分を取得してヒートマップのように色付けして比較分析できます。

pyroscope diff view
pyroscope の Diff View 画面

参考: demo.pyroscope.io

Pyroscope の仕組み

Pyroscope Agent という言語ごとに用意されているプロセスが、アプリケーションの動作を定期的に記録・集計し、そのデータを Pyroscope Server に送信します。

Pyroscope Server がそのデータを処理・集約することで、ユーザーがプロファイリングの結果を WEB UI から Flame Graph (フレームグラフ)を閲覧することが可能になります。

なので、Pyroscope で Continuous Profiling を実施するには、Pyroscope AgentPyroscope Server の設定が必要になります。

how does pyroscope work
pyroscope の全体像

参考: pyroscope.io

Grafana Pyroscope について

2023年3月にデータ可視化ツール Grafana などを開発している Grafana Labs が Pyroscope を買収し、 オープンソース Grafana Phlare と統合され、 Grafana Pyroscope になりました。

grafana pyroscope log

参考: grafana.com

また、2023年8月末に Grafana Pyroscope として version 1.0 が公開されました。

このバージョンでは、以下をはじめとした改善が行われました。

  • Grafana と完全に統合され、Grafana ダッシュボードでメトリクスやログ、トレースなど他の可観測性の指標と一緒に Profiling のデータを表示可能になりました。

  • 様々なオブジェクトストレージサービス (ex. AWS S3, Google Cloud Storage) との統合がサポートされ、プロファイルデータをストレージサービスに保存可能になりました。

  • 水平方向のスケールアウトがサポートされ、あらゆる規模のプロジェクトで最適なパフォーマンスを出すことが可能になりました。

参考: github.com

Pyroscope を活用したトラブルシューティング

問題だったこと

トモニテで利用している Go サーバーの基盤である ECS のメモリ使用率が時間の経過とともに上昇し続けるという問題がありました。

もしサーバーのメモリ解放せず長時間経過した場合、サーバーの一時停止といったリスクがあったため、調査・改善が求められていました。

memory usage before
ECS メモリ使用率 改善前

Pyroscope の導入

Pyroscope Server の起動

Pyroscope Server をサーバー上で Docker 経由で起動しました。

Pyroscope によるリソース消費の最適化のため、データの保持期間 retentionexemplars-retention を調整し、それ以外の設定値は、デフォルトの値を利用しました。

Pyroscope Server と Go サーバーのネットワーク設定

それぞれでAWSアカウントが異なる構成だったのもあり、ネットワークの疎通のため、VPCピアリング接続やルーティングテーブルへのルートの設定をしました。

Pyroscope Agent の起動

Profiling したいサーバーは Go 製なので、同様に Go の Pyroscope Agent を利用しました。

Pyroscope Server 側のリソース消費の最適化のため、SampleRate を調整しました。

また、アプリケーションサーバー側の基盤が ECS なのもあり、バージョン単位でより詳細に分析できるように、Tags にタスク定義のバージョンを設定しました。

// Init initialize
func Init() {
    address := conf.PyroscopeAddress()
    if address == "" {
        return
    }

    pyroscopeConfig := pyroscope.Config{
        ApplicationName: "tomonite-server",
    ServerAddress: address,
    SampleRate:    conf.PyroscopeSampleRate(),
    
        Tags: map[string]string{
            "taskArn":               ecs.TaskARN(),
            "taskDefinitionVersion": ecs.TaskDefinitionVersion(),
        },
    }

    if _, err := pyroscope.Start(pyroscopeConfig); err != nil {
        log.Alert(fmt.Errorf("failed to start profiling for pyroscope. err: %w", err))
    }
}

Profiling の結果

時間の経過とともに Go サーバー全体に対してメモリの使用率が著しく上昇している処理を特定することができ、その処理は、grpc-go というモジュールであることが分かりました。

  • ある時点A

pyroscope before view
grpc-go のメモリ使用率はサーバー全体の15%ほど

  • ある時点Aから12時間後

grpc-go のメモリ使用率はサーバー全体の30%ほど

さらなる調査・改修対応

grpc-go は Go サーバー側で明示的に利用されている実装箇所を発見できなかったため、メインのモジュールとの依存関係を調査しました。

すると、Cloud Firestore データベースの読み取りと書き込みのためのクライアントを提供する cloud.google.com/go/firestore が依存していたことが判明しました。

$ go mod why -m google.golang.org/grpc

github.com/everytv/tomonite-server/db
cloud.google.com/go/firestore
google.golang.org/grpc

実際に firestore 周りのコネクションが保持されてそのままになっている処理があったので、コネクションを解放するため、(*firestore.Client).Close() を追記してリリースしました。

pr connection release
firestore のコネクションを解放する修正

その結果、Go サーバー全体に対するメモリの使用率の増加が顕著に緩やかになり、無事にメモリリークの問題を解決することができました。

memory usage after
ECS メモリ使用率 改善後

おわりに

今回は Pyroscope と Continuous Profiling 、Pyroscopeを活用したトラブルシューティングの事例について紹介しました。

Pyroscope を利用するメリットとして、WEB UIで直感的に操作できたり、ビュー機能である Comparison ViewDiff View でより効率的な分析が可能なところだと考えてます。

Continuous Profilingをしておくことで、問題発生後に Profiling 結果を得て、即座に調査が可能になります。今後も Pyroscope をうまく活用してサービスの安定稼働を実現していきます。

また、Grafana Labs による買収・統合に伴い、さらなる機能開発が見込まれるため、今後の開発の動向を注視しつつ最新バージョンへのアップグレードも検討できればと考えています。

Xcode Cloudを活用してDELISH KITCHEN iOSのCI/CD環境を更新しました

はじめに

DELISH KITCHENで主にiOSの開発やマネジメントを担当している久保です。

以前、DELISH KITCHEN iOSアプリ開発のCI環境についてという記事でCI環境を紹介しました。

今回は、Xcode Cloudの導入経緯とCI/CD環境の変化についてご紹介します。

Xcode Cloudへ移行した理由

Xcode Cloudの発表以降、さまざまな試行を行ってきましたが、特に以下の理由から全面的な導入を決定しました。

  • 他のサービスと比較して機能が少ないため、学習コストが低い
  • TestFlightやApp Store Connectへのアップロード時に証明書の管理が不要
  • XcodeやmacOSのアップデートへの追従が速い
  • ビルド番号の管理が不要

Firebase App DistributionからTestFlightに移行した理由

アプリ配布に使うサービスも、Firebase App DistributionからTestFlightに移行しました。App Distributionを用いてAdHoc配信をしていた際には、以下の作業が発生していました。

  • インストールしたい端末のUUIDを登録
  • Provisioning profileの更新

これらの作業はfastlaneを使って半自動化していたのでそこまで手間ではなかったのですが、TestFlightを利用することによってこれらの雑務から解放されました。

Xcode Cloud以外を利用している場合は、App Store Connect APIを利用するようなスクリプトを組むことになると思うのですが、Xcode Cloudはここもよしなに処理してくれるというメリットがあります。

構成

Xcode Cloud導入後のアプリの社内向け配布およびApp Store Connectへ提出を行う全体のフロー図を以下に示します。

前回の記事ではfastlaneが色々な役割を果たしていたのですが、今回の用途に限るとfastlaneは不要になりました。

Release版のワークフロー

App Store Connectへの提出と最終的な動作確認のための配布を行います。

特定のプレフィックスを含むタグがGitHubにプッシュされると、次のワークフローが実行されます。

  • TestFlightで内部テスターに配布
  • App Store Connectへの提出
  • 成功または失敗をSlackチャンネルに通知

Develop版のワークフロー

主に社内関係者への配布を行います。別アプリとして扱いたいため、Release版とは別のバンドルIDを設定しています。

こちらも同様に、特定のプレフィックスを含むタグがGitHubにプッシュされると、次のワークフローが実行されます。

  • TestFlightで内部・外部テスターに配布する
  • 成功または失敗をSlackチャンネルに通知

内部・外部テスターの使い分け

内部テスターと外部テスターの使い分けに関して、以下に簡単な違いをまとめました。

内部テスター (Internal Testers) 外部テスター (External Testers)
Apple ID App Store Connectで管理されたApple IDが必要 特に制限なし
配布までの時間 アップロード後即時 審査通過後

デザイナーやプロダクトマネージャーなど、すぐに確認してもらいたい場合は内部テスターを、手軽に確認したい利用者向けには外部テスターを利用してもらっています。

まとめ

Xcode Cloudの導入により、特にデプロイに関連する作業がシンプルになりました。

あえて気になる点を挙げると

  • 設定がGUI
  • App Store Connectに存在しないアプリに対してワークフローが設定できない
  • 任意のスクリプトを実行する機構はあるが、実行タイミングが限定的
  • キャッシュの設定などが不明瞭

などが考えられます。

今回はデプロイに焦点を当てた内容になりましたが、iOS開発の取り組みに関して少しでも知っていただければ幸いです。

Google CloudのData Analytics Workshopに参加してきました!

Google CloudのData Analytics Workshopに参加してきました!

こんにちは。 株式会社エブリーの開発本部データ&AIチーム(DAI)でデータエンジニアをしている吉田です。

今回は、先日参加したGoogle CloudのData Analytics Workshopについて紹介します。

はじめに

エブリーでは、各サービスからのログをデータ基盤に集約し、これをデータ分析や機械学習のために活用しています。
データ基盤の構成は、Databricks、TreasureData、そしてRedashを組み合わせたものです。
具体的には、DatabricksでETL処理を行い、そのデータをTreasureDataに保存します。
そして、Redashを用いてデータの可視化を行っています。

現在のデータ基盤には、歴史的な経緯から生じたデータ処理フローの複雑さや、全体的なコスト最適化といった課題が存在しています。
このような状況でデータ基盤の構成を見直している中、Google Cloudの方からData Analytics Workshopの案内を頂き、参加させていただきました。

Data Analytics Workshopとは

Data Analytics Workshopは、Google Cloudのスペシャリストに現状のデータ基盤/データ活用の課題をヒアリングしていただき、Google Cloudの関連サービスを用いたソリューション・アーキテクチャを提案していただくワークショップです。

ワークショップの流れ

今回のワークショップでは、Google CloudからData Analytics Specialistを中心に、Customer EngineerやSalesの方々も含む3名の方が担当してくださいました。
エブリーからは、CTOとDAIメンバー、トモニテのバックエンドエンジニアが参加しました。

ワークショップは全2日あり、以下のような流れで進められました。

  • Day 1
    • エブリーの現状のデータ基盤と課題の認識合わせ
    • 主要プロダクトのご紹介
      • 事前ヒアリングを元に、いくつかのGoogle Cloudのサービスを紹介していただきました。
  • Day2
    • アーキテクチャ案の検討
      • 我々の課題を解決するためのアーキテクチャ案を提案いただき、それについてのディスカッションを行いました。
    • ハンズオン
      • いくつかのGoogle Cloudのサービスを用いたハンズオンを行いました。

ワークショップでの学び

以下にワークショップで学びや気付きを記載します。

データ基盤の課題とアーキテクチャディスカッション

Day1では、エブリーの現状のデータ基盤と課題の認識合わせを行いました。
現行のアーキテクチャの共有だけでなく、そのアーキテクチャがなぜそのような形になったのか、AI活用に向けた取り組みの進捗、BIツールやデータガバナンスに関する課題など、データに関連する問題点を幅広くヒアリングしていただけました。
エブリーのデータ基盤には、以下のような課題が存在しています。

  • DatabricksとTreasureDataの2つの基盤にそれぞれデータ処理フローが存在するため、フローが複雑化している
  • データ活用を促進するためのデータカタログの整備とデータガバナンスの強化が必要である
  • 機械学習をプロダクトに適用するためのMLOpsの整備が求められている
  • トモニテのデータ基盤であるBigQueryのコストが増加傾向にある

これらの課題を考慮した上で、Day2ではGoogle Cloudのサービスを活用したアーキテクチャ案を提案いただきました。

提案いただいたアーキテクチャ案では、基本的にBigQueryを中心とした構成となっています。
これにより、以下のようなメリットが得られると考えられます。

  • データ処理フローを単純化できる
  • Google Cloudの他のサービスとの連携が容易になり、Google Cloudの各サービスを最大限活用できる
    • MLOps基盤としてBigQueryと連携が容易なVertexAIを利用できる
    • データカタログとしてBigQueryから自動的にメタデータを収集可能なDataplexを利用できる
  • BigQueryの強力な計算資源を活用できる

提案されたプランでは、Plan3からPlan1に向けてGoogle Cloudのサービス利用率を高めていく想定をしています。
Plan3では現行のDWHであるTreasureDataをBigQueryに移行し、Plan2ではDatabricksをGoogle CloudのSpark基盤であるDataProcに移行します。
そして、Plan1ではすべての処理をBigQueryに集約します。

Google Cloud上にデータ基盤を構築すると、BigQueryの計算資源を活用しつつ現行アーキテクチャに対してコストを抑えることが可能となります。
また、BigQuery MLなどの機能を使用が可能となります。
さらに、Google Cloudの他のサービスとの連携が容易になり、フルマネージドのデータカタログやML基盤、BIツールなどを導入しやすくなります。

提案されたアーキテクチャ案を検討した結果、現行のアーキテクチャと同等の性能を維持しつつコストを抑えることが可能だと判断しました。
しかし、移行コストの大きさやアプリ基盤との連携など、解決すべき課題も多く存在します。

今回のワークショップでは時間の制約から、アーキテクチャ案の詳細なディスカッションを行えませんでした。
しかし、ワークショップ終了後もオフィスアワーという形で、Google Cloudのスペシャリストの方々とオンラインでディスカッションする時間をいただいています。

主要プロダクトのご紹介

主要プロダクトのご紹介では、事前ヒアリングの際に伝えたLLMとデータガバナンスを中心にGoogle Cloudのサービスをご紹介いただいたので一部を以下で紹介します。

VertexAIの利点として、モデルのトレーニングからデプロイまでの一貫した操作が可能であるという点です。
これにより、MLOpsの実行が効率化されます。
さらに、Generative AI Studioの使用により、自前でファインチューニングを行うコストを削減できると考えられます。

Dataplexは、データガバナンスの運用をサポートするために、複数のサービスに分散して存在するデータに対して、データの管理単位を作成し、管理単位での権限管理を行なえます。
さらに、フルマネージドであり、Google Cloudサービスとシームレスに連携できるデータカタログとして、データカタログの管理・運用コストを削減できると考えられます。
一方、エブリーではデータカタログとしてOpenMetadataをセルフホストで使用しています。
OpenMetadataは、GCP/GCP以外も含めた様々なデータソースからの横断的なメタデータ収集が容易にできる点が魅力的ではありますが、セルフホスティング由来の運用コストなどの課題もいくつか存在しています。
そのため、使いやすさや運用コストを含め、Dataplexも選択肢の一つとして検討していきたいと考えています。

Google Cloudサービスのハンズオン

ハンズオンでは、2グループに別れて以下のサービスを用いたハンズオンを行いました。 - BigQuery ML (https://cloud.google.com/bigquery/docs/bqml-introduction?hl=ja) - BigQuery Interactive SQL Translator (https://cloud.google.com/bigquery/docs/interactive-sql-translator?hl=ja)

BigQuery MLでは、BigQuery上にあるDELISH KITCHENのイベントログを対象に、モデルのトレーニングと予測を行いました。

BigQuery SQL変換は、他のSQL言語をGoogle SQLに変換が可能なサービスです。
エブリーでは、RedashからTreasureDataへのクエリにPresto SQLを使用しているため、BigQuery基盤への移行を考えると、Presto SQLからGoogle SQLへの変換が必要となります。
現在、Redash上には約17,000件のクエリが存在しており、これらをGoogle SQLに変換する移行コストは大きな課題となります。
この問題を解決するために、BigQuery SQL変換を紹介いただき、実際に変換を試みました。
手順としてWebブラウザ上で変換前の言語を選択した後、SQLを入力してボタンを押すだけです。

その結果、標準的な関数はエラーなく変換でき、移行コストの大幅な削減が期待できると感じました。 しかし、TreasureDataの独自関数については変換が行えないため、移行コストを完全にゼロにすることは難しいと感じました。 移行を行う際は独自関数の変換にのみ注力し、標準的な関数については自動変換を行うことで、コストを抑えつつ移行を進められると考えられます。

今後の取り組み

今回提案いただいたアーキテクチャ案を参考に、エブリーのデータ基盤の改善を進めていく予定です。
データ基盤の大規模な刷新は、エンジニアリングリソースの確保や既存のデータ処理フローの移行など、多くの課題を伴います。
そのため、小規模なPoCを実施するなどして、移行の見積もりや移行後のアーキテクチャの選定を行っていく予定です。
また、ML周りのサービスなど、部分的に導入可能なサービスが存在するため、それらのサービスを活用したPoCも実施していく予定です。

おわりに

今回のワークショップでは、Google Cloudのスペシャリストの方々にエブリーのデータ基盤の課題をヒアリングしていただき、その上でGoogle Cloudのサービスを活用したアーキテクチャ案を提案いただきました。
さらに、様々なサービスの紹介も行っていただき、これらを利用してどのようなデータ活用基盤を構築するかについて様々なアイデアが湧きました。
ワークショップの開催にあたり、多くのリソースを提供していただいたGoogle Cloudの皆様に心から感謝申し上げます。

エブリーのデータ組織の取り組み紹介

はじめまして。株式会社エブリーの開発本部のデータ&AIチームでマネージャー兼データサイエンティストをしている伊藤です。

今回は、エブリーのデータ組織が普段どういった取り組みを行なっているかを、簡単にご紹介したいと思います。

エブリーについて

株式会社エブリーは、「DELISH KITCHEN」「トモニテ」「TIMELINE」という3つのメディアを運営しています。 各メディアはそれぞれ主力となるサービスがあり、それらを起点に多岐に渡る事業を展開しています。

どのメディアも戦略上「データ」が不可欠となっています。

サービスのグロースのためのKGI・KPIのモニタリングはもちろんですが、 広告事業でのクライアント様向けの媒体資料や、OMO事業におけるオフラインデータの活用など、 データを起点とした取り組みは数多く実施されています。

チームObjective

データ&AIチームでは、「データ資産を最大限活用した事業運営の実現」をObjectiveに掲げています。

このObjectiveが達成された状態は、2つに分けられます。

  1. データ資産が適切に使える状態になっている
  2. データ資産の活用が事業改善に貢献している

前者は、サービスの動きやサービス内外で発生したユーザーの行動が正しく収集され、利用したい人が誰でも利用できるようになっている状態で、 データ基盤やBIツールの提供・データガバナンスなどが関連します。

後者は、データによって適切な意思決定がなされたり、データを使って事業価値が創出されている状態で、 データ分析や効果検証・統計モデル・機械学習などが関連します。

これら2つの状態は、どちらか片方だけでは不十分で、両方が機能して初めて data-driven / data-informed な意思決定につながるため、 それらを両立させるのがデータ&AIチームとしてのミッションだと考えています。

開発体制

2023年9月時点で、データ&AIチームは「データエンジニア」1名、「データサイエンティスト」3名、「データストラテジスト」1名が在籍しています。

それぞれの定義や担当領域は諸説ありますが、エブリーでは主に以下のようになっています。

  • データエンジニア: データ収集・分析基盤の構築と改善、データガバナンスの強化
  • データサイエンティスト: グロース施策の分析・効果検証、ロジック改善
  • データストラテジスト: マーケティングソリューションにおける営業支援

これらは大きな括りとしては分割されていますが、他の領域にまたがった取り組みを行う場面も多くあります。

例えば、データサイエンティスト・データストラテジストが、モニタリングや分析のために新しいデータが必要になったら、 基本的にはデータ取得処理の実装まで含めて担当します。

分業体制にできるほどの人数がいないという側面もありますが、 スキルの属人性を緩和しつつ、 大元のデータを活用できる状態に加工する経験を通してデータに対する解像度を上げられる、といったメリットもあると考えています。

技術スタックは、主要なものを挙げるとおおよそ以下のようになっています。

  • インフラ系: AWS、GCP、terraform
  • データ基盤系: Databricks、TreasureData、Fivetran
  • データ分析系: Redash、streamlit
  • プログラミング言語: Python、Scala、SQL (SparkSQL、Prestoなど)
  • ML系: MLflow、FastAPI (ML API)
  • AIツール系: Github Copilot、OpenAI API

主な業務内容

ここでは、データ&AIチームが日頃取り組んでいる業務のうち、直近のものをいくつかをピックアップして紹介します。

データカタログ構築

現在データ基盤は様々な利用者に使っていただいていますが、 「欲しいデータがどこにあるか分かりづらい」「テーブルのカラムの定義が分からない」といった課題がたびたび挙げられていました。

そこで、データエンジニア中心にデータカタログの構築を進めました。

いくつかの実現方法に関してPoCを行い、結果的にいくつかのツールを跨いでメタデータを収集できるOpenMetadataを採用しました。

インフラはAWS上に構築しており、現在はデータカタログの普及に向けた取り組みを実施しています。

効果検証

主にデータサイエンティストが、PdMやエンジニアと連携し、日々の施策の効果検証を担当しています。

効果検証はA/Bテストを選択することが多く、実験設計の部分から最後の意思決定の部分まで、データサイエンティストが並走するケースが多々あります。

直近では、細かいクリエイティブの改善に割く工数を削減するため、バンディットアルゴリズムの導入も進めています。

PR強化プロジェクト

これまで、マーケティングソリューションの課題の1つに、トレンド予測などのプレスリリースの少なさがありました。

プレスリリースは公開までに少なくない労力が必要ですが、営業メンバーの方が直近のトレンドを正しく把握するのに有効な側面に加え、 メディアとしてデータ資産を日頃利用いただいているユーザー様に還元できるという社会貢献としての側面もあります。

このプロジェクトはデータストラテジストと広報・管理栄養士が連携して進めており、

  • データストラテジストが探索的な分析ができるダッシュボードを作成
  • 広報・管理栄養士を交えて議論しつつ、トレンドの探索どダッシュボードの調整を進める
  • 発信の方向性が決まるとデータの整形と発信内容の調整を実施

といった流れで、データストラテジストを起点に各自の持つドメイン知識をうまく集約させたプロジェクトになっています。

直近はチョコミントに関するプレスリリースが発信され、いくつかのメディアでも取り上げていただいております。

データ組織としての直近の取り組み

また、データ組織として、直近は以下のような取り組みを行っています。

勉強会の実施

データ分析や機械学習などのスキルを身につける上では、実践的な経験に加えて、数学などの基礎知識の継続的な学習も重要になります。

そこでデータ&AIチームでは、数式に向き合う時間を作ろうという名目で、有志のメンバーで輪読会を開催しています。

過去何度か社内で勉強会を開催したり参加したりしてきましたが、予習と発表資料の準備はそこそこの負担になっていたため、 この輪読会は担当者がその場でホワイトボードに書きながら読み進める、という形式をとっています (そのため、内容が難しい場合には参加者全員が頭を抱えてほとんど進まないまま終わる回もあります)。

現在は「統計的機械学習の数理100問 with Python」という書籍を扱っています。 週に1時間のペースで、2023年9月時点で開始から1年半以上(最近ようやく最終章に入りました)続いており、今では比較的古参の取り組みになっています。

社内ChatApp作成

最近ChatGPTのような生成AIを組み込んだサービスやツールが日々増加している中で、それらを日常的に触れて肌感を掴み、 より良い活用の仕方を考え業務改善に活かしていくスキルが求められつつあります。

そのような動きを社内で高めていくため、データ&AIチームとして社内ChatAppを作成・提供する取り組みを行いました。

リリース時のSlackメッセージ

ChatAppはOpenAI APIをベースに動いており、入力した情報が学習に使われないよう設定できるため、ChatGPTよりも業務利用の敷居が下がっています。 また、GPT-4も社員は誰でも無料で使用できるようになっています。

ChatAppの作成は、チームで作ってみようという話題が出た翌週に、1日時間をとってデータエンジニア・データサイエンティストの2名で一気に作り上げた、 という点は地味なアピールポイントになっています。 また、作成においては以下の記事を参考にさせていただきました。

このような、メンバーの経験値になり、かつ業務にも役立つような取り組みを熱量を持って遂行するような動きは、今後もチームとして増やしていきたいと考えています。

今後の取り組み

最後に、データ組織としての今後に向けた取り組みを紹介します。

データ基盤の継続的な改善

現在のデータ基盤は幅広い事業部にデータを提供し利用いただいている状態ですが、 コストをはじめ、品質や提供速度など、より改善できる部分は多々存在しています。

また、利用者が使いやすいBIツールの設計や、見られなくなってしまうダッシュボードの管理方法なども、取り組むべき課題として挙がっています。

直近ではOpenMetadataを使ったデータカタログを作成しましたが、それをいかに社内普及させていくかもホットな課題です。

これらは一気に解決できるものではないですが、 チームとしては継続的に選択肢を検討し続ける状態を作り、少しでも良い状態を目指したいと考えています。

データによる技術更新の推進

現状DELISH KITCHENを初め、サービス内部には様々なロジック(検索やレコメンドなど)が埋め込まれています。

それらのほとんどは、サービス初期の頃に作成されたルールベースのアルゴリズムや、精度が継続的に評価されていないモデルになっており、 改善の余地が多く残されている状態です。

ユーザー様の利用体験を少しでも向上させるために、現在はサービスとロジックが適切に分離された状態を導入する取り組みを始めています。

サービスとロジックの分離がなされることで、データサイエンティストがロジックの改善に集中できる環境になりつつ、 必要であればロジックを前提としたUXの設計にも取り組めるようになると考えています。

今は少しずつ導入事例を増やしていく段階ですが、将来的にはインフラも整備しつつよりスケールする基盤を構築していけるよう取り組んでいきたいです。

おわりに

データ&AIチームの直近・将来に向けた取り組みを、簡単にご紹介しました。

エブリーのデータ組織に興味を持っていただいた方の解像度が少しでも上がるような内容になっていると幸いです。

より詳しい話を聞きたい方は、カジュアル面談をお申し込みいただけると嬉しいです!

corp.every.tv

iOS17で追加されたTipKitに触れてみる

こんにちは。トモニテでiOSアプリを開発している國吉です。 

トモニテではサテライトを含め、複数のアプリをリリースしています。それぞれアプリのリリースが終えてからグロースするために改善/運用を行っていますが、N1インタビュー等でユーザーの声を聞いてみると「そんな機能があったんですね!知らなかったです」という意見がちらほら見受けられます。我々機能を提供している側としては、できるだけ機能認知してもらえるようにデザインを工夫して提供していても一定数は機能にたどり着く前に離脱してしまう可能性があります。 

そこで今回はiOS17で追加されたTipKitを用いて、機能誘導できないか検証がてら触れていきたいと思います! 

TipKitとは

まずTipKitの概要をお話しします。 TipKitはアプリ上でヒントを表示しユーザーに機能/操作方法を知ってもらうための手段として提供されています。 

これにより、アップデートで追加した機能を知ってもらったり、オンボーディングでアプリのコアとなる機能を知ってもらうことができます。

ただし、ユーザーアクションを促す上で、”ヒントをどのタイミングで表示するか””どのようなメッセージでアプローチするか”を考えるのが難しく、設計がすごく大事になると思います。 

実装 

早速実装に取り掛かります。今回はUIKitで表示するとこまでを紹介しますが、SwiftUIでも実装はすごく簡単です。 

まずはAppDelegateでTipKitを使う設定だけしておきます。 

import TipKit
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    
    Task {
        try? await Tips.configure()
    }
    
    /*
     .....
     */
}

次にヒントを表示したいViewControllerで処理を記述していきます。

下記ではヒントで表示するTipの中身を定義しています。

import TipKit
struct StartButtonFeatureTip: Tip {
    
    var title: Text {
        Text("陣痛が始まったと思ったらタップ!!")
    }
    
    var message: Text? {
        Text("陣痛がおさまってきたら「おさまったかも」ボタンをタップして計測を終了しましょう")
    }
    
    var image: Image? {
        Image(systemName: "hand.tap.fill")
    }
}

動作検証では画面表示タイミングでヒントを表示したかったので、viewDidAppearで表示登録をしています。

画面から離れる際に、インスタンスを解放できるようにnilを代入しています。

    // クラスプロパティ
    private var startButtonFeatureTip = StartButtonFeatureTip()
    private var tipObservationTask: Task<Void, Never>?
    private weak var tipPopoverController: TipUIPopoverViewController?
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        /*
         ...
         */
        
        tipObservationTask = tipObservationTask ?? Task { @MainActor in
            for await shouldDisplay in startButtonFeatureTip.shouldDisplayUpdates {
                if shouldDisplay {
                    let popoverController = TipUIPopoverViewController(startButtonFeatureTip, sourceItem: contractStartButton)
                    present(popoverController, animated: animated)
                    tipPopoverController = popoverController
                } else {
                    if presentedViewController is TipUIPopoverViewController {
                        dismiss(animated: animated)
                        tipPopoverController = nil
                    }
                }
            }
        }
    }
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        tipObservationTask?.cancel()
        tipObservationTask = nil
    }

ここまでの記述でシミュレーターを起動するとヒントが表示されるようになります。

すごく簡単ですね。次はテキストなどカスタマイズできるとこは調整しアプリに馴染むようにしていきたいと思います!

テキストカラーやフォントなどはTipの中身を定義しているとこで記述し、アイコンの色についてはTipUIPopoverViewControllerの中にあるViewのtintColorを変更することで指定できます。

struct StartButtonFeatureTip: Tip {
    var title: Text {
        Text("陣痛が始まったと思ったらタップ!!")
            .foregroundStyle(Color(uiColor: .defaultTint))
    }
    
    var message: Text? {
        Text("陣痛がおさまってきたら「おさまったかも」ボタンをタップして計測を終了しましょう")
            .foregroundStyle(Color(uiColor: .defaultTint))
    }
    
    var image: Image? {
        Image(systemName: "hand.tap.fill")
    }
}
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        /*
         ...
         */
        
        tipObservationTask = tipObservationTask ?? Task { @MainActor in
            for await shouldDisplay in startButtonFeatureTip.shouldDisplayUpdates {
                if shouldDisplay {
                    let popoverController = TipUIPopoverViewController(startButtonFeatureTip, sourceItem: contractStartButton)
                    popoverController.view.tintColor = .defaultTint
                    present(popoverController, animated: animated)
                    tipPopoverController = popoverController
                } else {
                    if presentedViewController is TipUIPopoverViewController {
                        dismiss(animated: animated)
                        tipPopoverController = nil
                    }
                }
            }
        }
    }

ヒントのbackgroundColorを指定することもできるのですが、閉じるアイコンの色の指定は現状不明なので、注意が必要そうでした(iOS17が正式リリースされたらUIKitからでも閉じるアイコンの色も指定できるようになることを願います。。)

ヒントの中にアクションリンクを付与することも可能です。

Tipの中身の定義にActionを追加します。TipUIPopoverViewControllerのイニシャライザにactionHandlerがあるので、ハンドラー内でAction処理を記述していきます。

struct StartButtonFeatureTip: Tip {
    
    var title: Text {
        Text("陣痛が始まったと思ったらタップ!!")
            .foregroundStyle(Color(uiColor: .defaultTint))
    }
    
    var message: Text? {
        Text("陣痛がおさまってきたら「おさまったかも」ボタンをタップして計測を終了しましょう")
            .foregroundStyle(Color(uiColor: .defaultTint))
    }
    
    var image: Image? {
        Image(systemName: "hand.tap.fill")
    }
    
    var actions: [Action] {
        [Action(id: "start_button", title: "詳細はこちら")]
    }
}
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        Tracker.track(event: .counterScreen)
        AdjustTracker.track(event: .counterScreen)
        let statusBarHeight = self.view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
        headerViewHeightConstraint?.constant = 44 + statusBarHeight
        
        tipObservationTask = tipObservationTask ?? Task { @MainActor in
            for await shouldDisplay in startButtonFeatureTip.shouldDisplayUpdates {
                if shouldDisplay {
                    let popoverController = TipUIPopoverViewController(startButtonFeatureTip, sourceItem: contractStartButton, actionHandler: { action in
                        guard action.id == "start_button" else { return }
                        /*
                         「詳細はこちら」をタップ後の動作
                         */
                    })
                    popoverController.view.tintColor = .defaultTint
                    present(popoverController, animated: animated)
                    tipPopoverController = popoverController
                } else {
                    if presentedViewController is TipUIPopoverViewController {
                        dismiss(animated: animated)
                        tipPopoverController = nil
                    }
                }
            }
        }
    }

ルールをつける

ヒントを表示したい時に「ユーザーがXXXXをしたら」「ユーザーがログインしたら」等々条件を満たした時に表示したい場合があると思います。そこで使用するのがルールです。

ルールは2種類あります。

  • パラメータベース
    • 感覚的には”ログインしているか”等Boolで管理できるものだと思います。
  • イベントベース
    • 「XXXをしたら」等のイベントトリガーを指定できます。

今回はお試しでイベントベースのルールを使用し、「きたかも」Buttonを2回タップしたら「おさまったかも」Buttonにヒントを表示する処理を書いていきます。

まずは、Tipの中身の定義にEventとRuleと記述します。

viewDidAppear内は特に変わりありません。

「きたかも」Buttonをタップされたタイミングで、startButtonTappedCountをインクリメントする必要があります。

struct StopButtonFeatureTip: Tip {
    
    static let startButtonTappedCount = Event(id: "start_button_tapped_count")
    
    var title: Text {
        Text("陣痛がおさまったらタップ!!")
            .foregroundStyle(Color(uiColor: .defaultTint))
    }
    
    var message: Text? {
        nil
    }
    
    var image: Image? {
        Image(systemName: "hand.tap.fill")
    }
    
    var rules: [Rule] {
        #Rule(Self.startButtonTappedCount) {
            $0.donations.count >= 2
        }
    }
}
    private lazy var contractStartButton: ContractStartButton = {
        let button = ContractStartButton()
        button.configure(tapped: { [weak self] in
            guard let self = self else { return }
            Task {
                try? await StopButtonFeatureTip.startButtonTappedCount.donate()
            }
            /*
             ...
             */
        })
        return button
    }()

その他

他にも1日1個のヒントしか表示しない。同じヒントが出続けないように出現回数を制限する。等のオプションもあります。ぜひ調べてみてください。

また、見た目の確認したいなど常にヒントを出したい時もあると思いますが、その時はこのように記述することで全てのヒントを表示することができます。

Tips.showAllTipsForTesting()

最後に

今回はiOS17で追加されたTipKitを触ってみました。感想としてはオンボーディングに組み込むことで初日の機能利用を促すことができ、細かいとこまで機能認知に繋がりそうだと感じました。

また大きめな機能を追加しアップデートを実施した際にも同様に価値を感じそうです。

ただ、ベースのレイアウトはOSが提供したものに則る必要があるため、すごくリッチな見た目で表示したい要望がある場合には使えなさそうです。

トモニテではサテライトアプリで検証し、ユーザーの反応が良ければ随時、他のアプリには展開していこうかなと考えています。

参考

https://developer.apple.com/videos/play/wwdc2023/10229/

https://developer.apple.com/documentation/tipkit/highlightingappfeatureswithtipkit