こんにちは、株式会社タイミーでデータサイエンティストをしている貝出です。直近はカスタマーサポートの業務改善に向けたモデルやシステムの開発を行っております。 新規プロジェクトを始めるにあたって、予測や検知の機能といった複雑な課題に取り組む必要がある際、機械学習モデルを使うことを検討される方も多いと思います。しかし、Google の定めた Rules of Machine Learning: Best Practices for ML Engineering でも記載されているように、必ずしも最初から機械学習モデルを使うことが最適とは限りません。データの不足や問題の不明確さから、機械学習が効果的に機能しない場合もあります。 その一方で、 ルールベースなどのシンプルなアプローチ を用いることで、早期にプロダクトやサービスを開発し、ユーザーの反応を得られます。この段階でデータサイエンティストや機械学習エンジニアが関与することで、後々の機械学習モデルの導入をスムーズに進めることが可能です。 本記事では、新規プロジェクトでのルールベースの活用方法と、その段階でデータサイエンティストができることについて書きたいと思います。 新規プロジェクトにおける早期フィードバックと早期成果 構築・計測・学習のフィードバックループ 新しいプロダクトやサービスを開発する際には、できるだけ早く最小限の機能を持つ製品(MVP:Minimum Viable Product)を作成し、ユーザーからのフィードバックを得ることが重要です。これは、エリック・リースの『 リーン・スタートアップ 』で提唱されている手法で、迅速なサイクルで仮説検証を行うことで、無駄を最小限に抑えながら製品を改善していく考え方です。 早期にユーザーの反応を得ることで、問題設定や解決策が適切かを迅速に判断できます。もしユーザーに使われなかったり、期待した効果が得られなかったりする場合は、素早く方向転換(ピボット)することが可能です。このように、早期フィードバックはプロジェクトの成功確率を高める重要な要素となります。 具体的な成果物を早く提供することで、ステークホルダーからの信頼を得やすくなり、追加のリソース確保やプロジェクトの優先順位向上にもつながります。 機械学習モデル構築の課題 機械学習モデル(今回は教師あり学習の場合に限定します)を構築する際には、データの質と量が極めて重要です。しかし、新規プロジェクトでは利用可能なデータが限られていたり、ラベリングルールが明確でなかったりすることも多く、高性能なモデルを開発するのが困難である場合があります。 例えば、数千から数万件のラベル付きデータが必要な場合、データの収集とラベリングには数週間から数ヶ月、さらにはそれ以上の時間と多大なコストがかかることがあります。また、専門知識が必要なラベリング作業では、ラベルの一貫性を保つために教育や管理も必要です。 データが少ない場合、モデルは訓練データに過度に適合し、未知のデータに対しては性能が低下する「 過適合(オーバーフィッティング) 」が起こりやすくなります。逆に、データのパターンを十分に学習できず、訓練データでも性能が低い「 過小適合(アンダーフィッティング) 」の状態になることもあります。 このような状況では、データの追加収集やラベリングにかかる時間とコストを考慮し、新規プロジェクトではまずシンプルなルールベースの方法を検討することが有効です。 ルールベースと機械学習ベースの違い ルールベースと機械学習ベースのアプローチの違いを簡単に整理します。 ルールベース 機械学習ベース 定義 人間が設定したルールで動作 データからパターンを学習 メリット ・簡易性 ・迅速な開発 ・解釈性 ・高い精度 ・適応性 ・高度なパターン認識 デメリット ・柔軟性の欠如 ・拡張性の限界 ・開発コスト ・解釈の難しさ ルールベース 定義 :人間が設定したルールや条件に基づいて動作する メリット : 簡易性 :実装が比較的容易で、結果が理解しやすい 迅速な開発 :短期間での開発・デプロイが可能 解釈性 :なぜその結果になったのか説明しやすい デメリット : 柔軟性の欠如 :複雑なパターンや例外への対応が難しい 拡張性の限界 :ルールが増えると管理が煩雑になる 機械学習ベース 定義 :データからパターンを学習し、予測や分類を行う メリット : 高い精度 :大量のデータから複雑なパターンを学習可能 適応性 :新しいデータや状況にも柔軟に対応 デメリット : 開発コスト :データ収集やモデル構築に時間とリソースが必要 解釈の難しさ :モデルがブラックボックス化しやすく、結果の説明が難しい場合がある 事例:スパムメールのフィルタリング ルールベースの場合 :特定のキーワードや送信元をブロックリストで管理。実装が簡単で明確な基準で判定可能。 機械学習モデルの場合 :メールの内容を解析し、ナイーブベイズや深層学習モデルでスパムを分類。大量のデータで高精度を実現。 ルールベースモデルの活用 迅速な実装とデプロイ ルールベースモデルはシンプルなため、 短期間で実装・デプロイ が可能です。これにより、ユーザーからのフィードバックを早期に得られ、製品の改善に活かせます。 ルールベースモデルにおけるデータサイエンティストの役割 データサイエンティストは、ルールベースのアプローチでも以下のような重要な役割を果たせます。 データ分析によるルール策定支援 :データの傾向やパターンを分析し、効果的なルールの策定をサポートする 性能評価とモニタリング :モデルの性能を定期的に評価し、改善点を提案する エラー分析とルール改善 :誤判定の原因を特定し、ルールの精度向上に貢献する 将来の機械学習モデルへの布石 :データの蓄積とラベリング戦略を立案し、スムーズな機械学習モデルの導入を支援する 段階的な開発プロセス 段階的な開発プロセスの一例として、以下のような進め方が考えられます。 段階的な開発プロセスのイメージ ルールベースでの MVP 開発 : シンプルなルールを用いて基本機能を実装 迅速なデプロイを目指す フィードバックの収集 : ユーザーや領域専門家(ドメインエキスパート)から意見を収集 ルールの有効性や不足点を把握 評価と改善の繰り返し : 性能指標を用いてモデルを評価 エラー分析を行い、ルールを最適化 機械学習モデルの導入検討 : ルールベースの限界が見えた段階で機械学習の導入を検討 データの蓄積状況やビジネス価値を総合的に判断 まとめ ルールベースと機械学習モデルの双方にメリットとデメリットがあります。新規プロジェクトにおいて、データが十分に揃っていない初期段階では、 ルールベースなどのシンプルなアプローチ を活用することで迅速に価値を提供し、フィードバックを得られます。 データサイエンティストが早期から関与し、評価やエラー分析、データ品質の向上に努めることで、プロダクトの質を高めつつ、将来的な機械学習モデルの導入をスムーズに進めることが可能となります。 ビジネス価値が確認できた段階で機械学習モデルを導入することで、より高度な機能や精度の向上を実現できます。この段階的なアプローチは、リソースの有効活用とプロジェクトの成功に寄与することが期待されます。 おわりに 今回はルールベースのアプローチの活用を中心に紹介しましたが、技術的にルールベースでは実現が難しいケースもあります。例えば、画像内の文字を機械が読み取れる形式に変換する光学文字認識(OCR)は、ルールベースでの実装が困難です。そのようなケースでは、ルールベースではなく、既存の機械学習モデルや Google の Cloud Vision API などの SaaS を最初のアプローチとして利用することになるかもしれません。 新規プロジェクトでは、限られたリソースや時間の中でいかに迅速にユーザーに価値を提供するかが重要です。シンプルなアプローチから始めて、段階的に機械学習モデルを導入することで、プロジェクトのリスクを最小限に抑えつつ、より高度な機能を実現できます。 私はタイミーにおけるデータサイエンティストとして、このプロセスに積極的に関与し、ビジネスと技術の橋渡し役を目指しています。 現在、タイミーでは、データサイエンスやエンジニアリングの分野で、共に成長し、革新を推し進めてくれる新たなチームメンバーを積極的に探しています! product-recruit.timee.co.jp また、気軽な雰囲気での カジュアル面談 も随時行っておりますので、ぜひお気軽にエントリーしてください。↓ hrmos.co hrmos.co
2024年10月7日から10日(現地時間)にかけて、dbt Coalesce 2024がラスベガスで開催されています。 私たち株式会社タイミーからは、4名が現地参加しました (※)。 今回は、カンファレンスの最初のKeynoteについてご紹介したいと思います。 このKeynoteでは、dbtのビジョンや今後の新機能リリースに関する熱いトピックが多く取り上げられ、非常に刺激的な内容となりました。 以下がアジェンダです。 イントロ 新機能の紹介 dbt Copilot Visual Editing Experience Advanced CI Data health tileの埋め込み dbtセマンティックレイヤーのコネクタ追加 クロスプラットフォームのためのdbt Mesh まとめ References ここからはKeynoteのレポートをお届けします。 発表内容とその他を切り分けるため弊社メンバーの今回の発表に対するコメントや確認した内容はこのマークアップスタイルで表記します。 イントロ 2024年のdbt Coalesceには、世界中から2000人以上の参加者がラスベガスに集まり、オンラインでは8000人もの方が参加していました。分析エンジニアやデータエンジニアはもちろん、アナリスト、CDO、財務、マーケティング、プロダクト担当者など、さまざまな役割を持つ人々が一堂に会し、dbtのコミュニティの成長を感じました。 既に5万以上のチームが本番環境でdbtを利用しており、毎日1350万回もの実行が行われ、過去1年間で49億ものモデルが構築されているそうです。 データ変換の課題が解決されつつある一方で、品質やオーナーシップ、そしてステークホルダーのリテラシーといった新たな課題が浮上しています。特に、データシステムをソフトウェアシステムと同様に扱う必要性がますます強調されていることが印象的でした。 dbtは、ソフトウェアエンジニアリングのベストプラクティスを取り入れた「ADLC(Analytics Development Lifecycle)」という新しい概念を提案し、分析の全過程をカバーするエンドツーエンドのワークフローを提供しています。このアプローチが、今後どのようにデータ活用を進化させていくのか、とても楽しみです。 画像は https://www.getdbt.com/resources/guides/the-analytics-development-lifecycle より抜粋 新機能の紹介 今回の Keynote では dbt Cloud の新機能として dbt Copilot、 Advanced CI、ビジュアルエディティング、クロスプラットフォーム連携などが紹介されました。 dbt Cloud はオーケストレーションやオブザーバビリティ、カタログやセマンティクスの機能を持った Data Control Plane と説明されていました。 今回発表された新機能も Data Control Plane の一部とのことで、抜粋しながら紹介していきたいと思います。 画像は https://www.getdbt.com/blog/coalesce-2024-product-announcements より抜粋 dbt Copilot LLMはコード作成に優れており、データワークを大幅に簡素化できる可能性がある。 dbt Copilot は、モデル、テスト、メトリクス、ドキュメントなどを支援してくれます。 ログ分析による根本原因分析、クエリプロファイルの分析と改善の提案によるコスト削減、ビジネス上の質問などを行うための自然言語インターフェースの提供など、さまざまな機能を持つチャット画面も追加されます。 OpenAIのAccesskeyを登録することで利用が可能になるそうです。 dbt開発に慣れ親しんでいないメンバーがこれを使ってすぐに開発者になることは難しそうですが、これまでdbt開発をしていたメンバーがアシスタントとして並走してもらう形で活用することで、開発スピードが向上させられそうです。 Visual Editing Experience NoCodeでブロックを繋げるだけでdbtモデルを作成可能となる機能が発表されました。 ドラッグ&ドロップ・インターフェースをベースとして、元データからブロックを繋げるだけで変換、計算、フィルターなど少し風雑なモデルも作ることができます。ビジュアル編集モデルはSQLに変換されgitによって管理されます。作ったモデルはdbt上の基本機能であるテストとドキュメントに接続できるため、dbt上と同じ信頼性を担保できます。ベータ期間に申し込むのは こちら から。 DataMesh的な思想で開発を分散可能になりうる部分は素晴らしいと感じましたが、ビジネスメンバーに機能を譲渡する前にしっかりガードレールを敷くような開発を行わないとカオスが発生しうると感じたため、機能利用可能時にはその辺りは深くチェックしたいです! 弊社はSQLに慣れ親しんでいないビジネスメンバーが大きなボリュームを占めているので、この機能がしっかりしたガードレールの元でハマることがあれば開発スピードを大きく高めることができそうです! Advanced CI これまでのdbt Cloudではslim CIの概念に則って、変更差分とその影響範囲をテストして開発速度を向上させてきましたが、「CIでテストが通ってビルドが通ること」と「期待した変更が正しくされたこと」が一致しないケースも多々あり、今回の advanced CI はそちらを解決する機能として紹介されていました。 そのPRで変更対象となったモデルの実行結果と、直近の本番ビルドの実行結果と比較して「作成されるモデルがこのPRでどのように変化したのか」を確認できるような新しいCIです! 以下の内容を本番とCIの差分として確認できます。 追加、変更、または削除された行または列 列内の値の変更 列のデータ型または列の順序の変更 主キーの変更または重複 データモデル全体と比較して、変更、追加、または削除された行の割合 紹介されている画面 dbt Cloud上のCI Job詳細画面 PullRequest上に投稿されるdbt Cloudからのコメント画面 こちらの機能によって以下のメリットがあると説明されています。 不正なデータを本番環境にデプロイされることを防止できることによるデータ品質の向上 行レベルの変更を確認可能となることで、行レベルテストなどが不要になることによる開発者の速度向上 本番環境の手前で消化活動が可能なため、事後対応が減ることによるコスト効率の向上 早速ちょっとやってみました。(色々な事情ありが成功版と中身をお見せできず、すみません)CI環境のJobで Run compare changes のオプションをONにします。 この機能をONにしないとCI Jobの画面で以下のような表示になります。 pull request上にadvaced CIのコメントがしっかりされました! (advanced CIに対するコメント) 全てのケースを網羅するテストを書くことは難しいため、コードやデータの変更を自動的に確認できるようになるこのCIは、「これこそ顧客が求めていた機能だ」と感じました。 ただ、本番環境のビルドに関してもCIを意識しなければならないため、ビルド戦略は少し複雑になるかもしれません。それでもビルドの信頼性の向上によって、より安定したリリースプロセスに近づくはずです。 Data health tileの埋め込み 自動公開とそれに対応するData health tileにより、信頼できるデータセットを保証できます。 Tableauワークブックやiframeで埋め込めるData health tileで信頼性を可視化することが可能です。 こちらTableauとの連携が主にフィーチャーされていましたが、こちらLookerStudioにも埋め込み可能なことを確認しました。 このようにdbt Cloud exploreからexposureを選択してiframeを取得 iframe内のtokenパラメータにdbtのBearerトークンを渡した状態で、LookerStudioに埋め込むことで以下のようにLookerStudio上に表示されることを確認できました! dbtセマンティックレイヤーのコネクタ追加 Tableau(updated)、Sigma、Power BIなど、さまざまなBIツールとの統合が強化されました。 クロスプラットフォームのためのdbt Mesh これまでの dbt では、複数のデータプラットフォームをまたいだプロジェクトの構成が困難でした。例えば、あるプロジェクトで Databricks を利用していて、後続のプロジェクトで Snowflake を使っていたとしても、後続のプロジェクトから source として Databricks のプロジェクトのテーブルを参照することはできませんでした。 今回の Keynote でクロスプラットフォーム dbt Mesh の機能が発表されて、プラットフォームをまたいだプロジェクトの構成が現実味を帯びてきました。 実現に至ったのは、 Apache Iceberg に対応したデータプラットフォームが増えたことがきっかけでした。 Apache Iceberg とは Open Table Format と呼ばれる、主にデータレイクハウスでの利用が想定されたオープンなテーブルフォーマットです。Apache Iceberg 自体の詳細の説明は割愛しますが、データレイクハウスにおいて Iceberg 形式でテーブルを作成することで、他のデータプラットフォームから読み取りが可能になる仕組みです。 2024年10月に入って dbt が Apache Iceberg のサポートを開始したことで、クロスプラットフォームでの連携が可能になりました。 弊社では BigQuery を利用しており Iceberg 互換でデータを保持することはできないため、残念ながら今の環境で試すことはできませんが、企業間のデータ連携などを見据えると触っておきたい機能だと思いました。 さらに Apache Iceberg 形式への対応に加えて、 Amazon Athena や Azure Synapse Analytics といったアダプターが新しくサポートされました。 プラットフォーム間の連携が容易になるとのことで、柔軟な形式でデータを保持できるデータレイクハウスの強みがこういった形で出てくるのは印象深いです。さらにアダプターの追加によって dbt とデータレイクハウスの相性がますます良くなったように感じました。 まとめ dbt Coalesce 2024のKeynote「Innovating with dbt」は、dbtのビジョンと共に新規機能がいち早く聞ける非常に刺激的な内容でした。特に、advanced CIやdbt Copilotによってさらに効率的に分析・開発が推進できそうですし、ビジュアルエディティングによって開発に関われるデータ関係者の幅が広がりそうで可能性を感じました。 セッション内で繰り返し強調される One dbt というワードの名の通り、dbtがカバーできる新しい領域が沢山できたと思える内容でした。 タイミーでも、こういった新機能を活用することでdbt開発の効率化や品質向上を図り、データ基盤の信頼性とアジリティをさらに高めていきたいです。 ※今回私たちは、タイミーの Kaigi Pass 制度を利用してdbt Coalesce 2024に参加できました。 References One dbt: the biggest features we announced at Coalesce 2024 https://www.getdbt.com/blog/coalesce-2024-product-announcements About dbt Copilot https://docs.getdbt.com/docs/cloud/dbt-copilot Build with speed and confidence https://www.getdbt.com/product/develop Advanced CI https://docs.getdbt.com/docs/deploy/advanced-ci Data health tile https://docs.getdbt.com/docs/collaborate/data-tile
株式会社タイミーでデータサイエンティストをしている渡邉です。 タイミーでは、スマートフォンへのプッシュ通知を利用してタイミーを利用されているワーカーの方にキャンペーン情報やおすすめのお仕事を通知しています。通知した情報をワーカーの方が開封することで初めて詳細な情報を確認することができるのですが、タイミーのアプリ以外のアプリでもプッシュ通知を利用されていると、様々なアプリから日々大量の通知を受け取っているという状況が発生しているかと思います。 このような状況下では、ワーカーの方々が重要な情報を見逃したり、通知の頻度が多すぎてストレスを感じたりする可能性があります。そのため、より効果的でユーザーフレンドリーな通知システムの構築が必要だと考えています。 そんな中でこの問題について有効と思われる手段を提案されている論文 「 Real-World Product Deployment of Adaptive Push Notification Scheduling on Smartphones 」[1] を見つけたので、本ブログではこの論文の内容を紹介したいと思います。 論文の概要 本論文では、プッシュ通知技術のアプローチである「適応型通知スケジューリング」について詳細に報告しています。慶應大学と Yahoo! JAPAN が実施した大規模研究(382,518人のユーザー、28日間)を通じて、この新しいアプローチの仕組みとその効果が検証されました。 まず従来のプッシュ通知システムの問題点を明らかにし、それらを解決するための適応型通知スケジューリングの手法を提案しています。この手法は、ユーザーの行動パターンを学習し、最適なタイミングで通知を配信することで、通知の効果を大幅に向上させることを目指しています。 システムの設計から実装、そして大規模な実験結果までが詳細に記述されており、この新しいアプローチがユーザーエンゲージメントにどのような影響を与えるかを明らかにしています。さらに、この技術の実用化に向けた課題や今後の展望についても議論されています。 従来のプッシュ通知手法の問題点 従来のプッシュ通知システムには、以下のような問題点があると述べられています。 ユーザーの状況を考慮しない一方的な通知配信 ユーザーが何をしているかに関係なく通知を送信するため、作業の中断や集中力の低下につながる可能性がある。 頻繁な割り込みによるユーザーの生産性低下とストレス増加 頻繁に作業を中断されることで、ユーザーにストレスや不満を与える可能性がある。 通知の無視や設定オフにつながるユーザーの疲弊 インタラプション過負荷の状態になると、ユーザーは通知を無視したり、通知設定をオフにしたりする可能性がある。これは、プッシュ通知本来の目的である情報伝達を阻害する要因となる。 クリック率や反応時間の低下によるマーケティング効果の減少 通知が頻繁に送られてくることで、ユーザーは通知に対して鈍感になり、クリック率や反応時間の低下につながる可能性がある。 これらの問題は、ユーザーエンゲージメントの低下と、プラットフォームの価値低下につながる危険性があります。 適応型通知スケジューリングの概要 適応型通知スケジューリングの大事なポイントは、ユーザーの「ブレークポイント」を検出することです。ブレークポイントとは、ユーザーが一つのタスクを終え、次のタスクに移る瞬間のことを指します。この瞬間は、ユーザーの認知負荷が低く、新しい情報を受け取るのに適しているとされています。 本論文では、スマートフォンのセンサーデータと機械学習を組み合わせて、リアルタイムでブレークポイントを検出するシステムについて述べられています。このシステムは、ユーザーの活動状態、時間帯、デバイスの状態などの多様なコンテキスト情報を活用しています。 具体的には、474 の特徴量を用いた分類モデルを構築し、ユーザーの状態をリアルタイムで評価します。 主な特徴量には以下が含まれます。 時間帯情報 デバイスの状態(充電状況、音量設定など) ユーザーの活動状態(Google Activity Recognitionを使用) アプリケーションの使用状況 モデルは線形回帰をもとにしており、日々のユーザーデータを用いて更新されます。また、平日と週末で異なる行動パターンに対応するため、コンテキストに応じた重み付けを導入しています。 そして、ブレークポイントが検出されるまで通知の配信を遅延させ、最適なタイミングで通知を表示します。 システムアーキテクチャ システムは主にクライアント側とサーバー側のコンポーネントで構成されています。 クライアント側: センサーデータの収集 特徴抽出 リアルタイムのブレークポイント検出 通知の遅延と表示 サーバー側: ログデータの収集と分析 日次モデルの更新 新しいモデルのクライアントへの配信 このアーキテクチャにより、ユーザーの行動パターンを日々学習し、モデルを継続的に改善することが可能となります。 実験と結果 382,518人の Android 利用ユーザーを対象に実験を実施しています。実験では、ユーザーを Test 群と Control 群にランダムに分割し、Test 群には新しい適応型通知システムを適用し、Control 群には従来の通知システムを使用しています。 主な結果は以下の通りです。 クリック率の平均が 23.3% 向上(最大 41.6%) 適応型通知スケジューリングを導入した結果、ユーザーのクリック率は平均で 23.3% 向上し、最大で 41.6% の向上率が確認されました。 ターゲット通知のクリック率の平均が 30.6% 向上(最大 60.7%) 特に、ユーザーの興味関心にもとづいてパーソナライズされたコンテンツを含むターゲット通知では、平均 30.6%、最大 60.7% と、より高いクリック率の向上が見られました。 一般通知のクリック率の平均が 11.1% 向上(最大 42.9%) 一方、すべてのユーザーに同じ内容が配信される一般通知でも、平均 11.1%、最大 42.9% のクリック率向上が確認されました。 通知配信後 120 秒以内のクリック率が 2.6〜2.86 倍に向上 適応型通知スケジューリングでは、通知配信後 120 秒以内のクリック率が、大幅に向上しました。 一般的な通知の場合:クリック率が2.60倍に向上 (実験群のクリック率 / 対照群のクリック率) ターゲット通知の場合:クリック率が2.86倍に向上 (実験群のクリック率 / 対照群のクリック率) これらの結果はユーザーのブレークポイントに合わせて通知が配信されることで、ユーザーの関心を高め、即座に反応しやすくなったためと考えられます。特にターゲット通知(パーソナライズされた内容)の場合、より大きな効果が見られました。この結果は、適切なタイミングとパーソナライズされた内容の組み合わせが、ユーザーエンゲージメントを大幅に向上させる可能性を示唆しています。 速報ニュースが多い日でも高いパフォーマンスを維持 速報ニュースが多い日でも、適応型通知スケジューリングは高いパフォーマンスを維持しました。これは、ユーザーのブレークポイント以外のタイミングで配信される速報ニュースが多い日でも、適応型通知は、ユーザーの適切なタイミングで配信されるため、クリック率への影響が少なかったと考えられます。 これらの結果は、適応型通知スケジューリングの有効性を強く示唆しています。 考察 実験結果から、以下の点が考察されます。 ユーザーの状況を考慮した通知は、大幅にエンゲージメントを向上させる パーソナライズされた内容(ターゲット通知)と適切なタイミングの組み合わせが最も効果的 適応型システムは、緊急時の通知と日常的な通知のバランスを効果的に管理できる ユーザーの即時反応性の向上は、情報のタイムリーな伝達に有効 これらの知見は、マッチングプラットフォームにおける通知戦略の最適化に大きく寄与すると考えられます。 今後の課題と展望 今後の主な課題と展望として、以下が挙げられています。 さらなるパーソナライゼーションの追求(通知内容と配信タイミングの最適化) クロスプラットフォーム対応(iOS, ウェブなど) リアルタイム処理の効率化 これらの課題に取り組むことで、より効果的でユーザーフレンドリーな通知システムの実現が期待されます。 まとめ 適応型通知スケジューリングは、ユーザーの状態を考慮したアプローチです。大規模実験の結果は、この手法が従来の問題点を大きく改善し、ユーザーエンゲージメントを向上させることを示しています。 マッチングプラットフォームにおいても、求職者と求人情報のマッチング精度の向上、ユーザー満足度の向上、そしてプラットフォーム全体の価値向上が期待できます。今後もこのような論文から得られた知見をもとに、タイミーアプリの継続的な改善を重ね、ユーザーとビジネスの双方に価値をもたらすことに貢献していきたいと考えています。 現在、タイミーでは、データサイエンスやエンジニアリングの分野で、共に成長し、革新を推し進めてくれる新たなチームメンバーを積極的に探しています! また、気軽な雰囲気での カジュアル面談 も随時行っておりますので、ぜひお気軽にエントリーしてください。↓ product-recruit.timee.co.jp hrmos.co hrmos.co 参考文献 [1] : Okoshi, T., Tsubouchi, K., & Tokuda, H. (2019). Real-World Product Deployment of Adaptive Push Notification Scheduling on Smartphones. In Proceedings of the 25th ACM SIGKDD International Conference on Knowledge Discovery & Data Mining (KDD ’19) (pp. 2792-2800), DOI: 10.1145/3292500.3330732 (2019)
ふなち( https://x.com/_hunachi )です。 DroidKaigi 2024で登壇してきました! 初めてのDroidKaigiでの登壇はとても緊張しましたが、良い経験になりました。 登壇内容 タイムテーブル 2024.droidkaigi.jp 登壇資料 speakerdeck.com 登壇動画 www.youtube.com この内容で登壇した経緯 昨年から今年の頭にかけて、基本一人でもくもくPDFを表示するコードを書く機会がありました。 話はそれますが、iOSでPDFを表示させたいとなった時、公式のFrameworkであるPDFKit( https://developer.apple.com/documentation/pdfkit )を使うことができます。これが高性能で、リンクの表示や文字列検索など色々できてしまうのです。 しかしAndroidは… 調査した結果、公式から出ているAPI(PdfRenderer)では、Bitmap生成とページ数取得くらいしかできませんでした。 また、実装するときに気にすることも色々あり少し大変な道でした。Bitmapを大量に扱うので何も考えないで実装すると簡単にメモリリーク等させることができるのです。 しかもネット上の記事にも、(当時は特に)英語のもの含めPDFビュアーに関してメモリを考慮して実装したというようなものはありませんでした。 そんな困難に見舞われながらも実装することができたので一旦よかったのですが、一部悔しさはありました。 そんな時です。 Android 15のリリース予告記事でPDF周りのAPIがかなり強化されると発表されました🎉 記事を読んでみると、文字列検索などもできるらしく、革命だ!と思いました。 そのような中、Android 15(Vanilla Ice Cream)をエミュレーターで動かせるようになったタイミングで、休日も使い色々実装してみました。 ハッカソンで作ったようなコードと見た目なので今後も公開はしない予定ですが、PDFを要約する機能や、特定の文字列がある部分を枠で囲ってくれる機能、フォームの内容を変更できる機能を試すことができるアプリを作りました。 色々できるようになっていることを知れて、嬉しかったのを覚えています。 しかもこの時、運よくDroidKaigiのプロポーザル提出期間でした。 もともと緊張するタイプの私にとって、40分という時間のハードルはそこそこ高く、特に発表したいことがなければ提出するつもりはなかったのですが、PdfRendererの素晴らしい進化を伝えたいという気持ちに負けて、プロポーザルを提出しました。 その結果嬉しいことに、採択されました🙌 登壇準備 採択された後は、発表資料を作成するため、今までやってきた経験をもとに一から簡易的なPDF Viewerを作り、そのコードを使い発表資料を作成しました。 コードを書く方に時間を費やしすぎて、焦って資料作りすることになったので今後は気をつけようと思います。 資料を作る中で、やらかしたかも!と思う出来事もありました。 プロポーザルの編集期限が過ぎた後に、GDEの方のSNSでの発言により、androidx配下にpdf/pdf-viewerというディレクトリが爆誕していることに気がついたのです。プロポーザルに書き損ねてしまったことに後悔していたら、資料作成途中の8月頭に androidx.pdf:pdf-viewer の alpha がリリースされました。 そこで、プロポーザルに書いていないが話さないわけにはいかないなと思い、発表資料に androidx.pdf:pdf-viewer のことも組み込むことになりました。 この androidx.pdf:pdf-viewer の内部実装について紹介しようか迷いましたが、このライブラリと同じようなことをしたいのであればこのライブラリを使えばいいなと思い、重要な部分(プロセス分離の部分)を除いて内部実装については発表しないことにしました。 また同時通訳対象だったため、日英両方でスライドを書いたり、事前にスライドを提出したりする必要がありました。 大変な部分もありましたが、英語のチェックを社内の方や家族にしてもらえて助かりました🙇 発表数日前にはタイミーのメンバーの前で発表練習もさせてもらいました。 その際に色々と質問等をもらえたおかげで、ブラッシュアップすることができました。 忙しい中時間を作っていただき感謝です! 余談ですが、弊社には登壇や執筆をサポートする制度もあります。 productpr.timee.co.jp 今回私は入社前に登壇準備をすることになったため、あまり利用していませんが、今後はこちらの制度も活用したいです! 発表当日 発表では、めちゃくちゃ緊張していましたが、なんとか発表を無事終えることができました。 緊張で止まってしまったりすることもなく、発表時間もちょうどで終わったようでよかったです。 ただ、早く喋り過ぎたので同時通訳の方には負荷をかけただろうなと思い反省しています。次に同じような機会に恵まれた際には気をつけます。 少しニッチな内容なので、Ask the Speakerも暇になるかなと思っていましたが、 「今PDF関連のことを触っていて〜」や「ちょうど調べている内容なので助かりました!」と英語話者の方も含め質問していただいたり、声をかけていただいたりして嬉しかったです。 当日だけじゃなく後日含め、SNSやブログでもピックアップしてコメントをくださった方々もありがとうございます。 直接の反応はあまりできていませんが、見るたびにとても嬉しい気持ちになっています。 写真はDroidKaigiが公開しているアルバムから引用 引用元: https://x.com/DroidKaigi/status/1840692565538136507 発表以外の感想 実は今年もスタッフをしており、主にセッション進行のお手伝いを行なっていました。 また、スタッフ業務をしながら他の方のセッションを見ていたのですが、そこにも多くの学びがありました。 まだ全てのセッションを見ることができてないので時間を作って見てみようと思います。 アフターパーティなどでも他の登壇者の方や私の発表を聞いてくださった方、初めましての方や久しぶりにお会いした方々とお話しすることができ、とても楽しかったです。 まとめ 私の発表を聞いてくださった方々、登壇のサポートをしてくださった方々、スタッフの方々など、皆様に感謝しています。ありがとうございます! 40分の登壇を経験したことで、他の登壇している人たちをさらに尊敬できるようになりました。 とても良い経験でした。 これからも積極的にインプットとアウトプットをしていこうと思います! 余談 発表中緊張して途中で声が出なくなるといけないので、いざという時にお面をつけて落ち着こうと考えてました。お面は前日の夜に緊張具合を考慮し作成しました。 登壇直前のマイク確認でお面をつけて喋ってもマイクが通るかの確認もしました。 確認の結果、お面をしていてもマイクに音は乗ることがわかりました。 このいざという時の備えができた安心感も発表を乗り切れた理由の一つかもしれません。 お面参戦なら登壇できるかもという方は、来年以降プロポーザル提出チャレンジをしてみると良いと思います! また、他のメンバーによるDroidKaigi 2024参加レポートも公開しています。是非こちらも読んでみて下さい↓ tech.timee.co.jp
9/11~9/13 にかけて DroidKaigi 2024 が開催され、タイミーの Android アプリエンジニアチームが参加してきました。 はじめに 9月にジョインされた hunachi が登壇しています。Android の PDF Viewer に関する歴史や詳細な実装からライブラリの紹介まで PDF Viewer を網羅したセッションとなっているのでぜひアーカイブでご覧ください。 2024.droidkaigi.jp タイミーではブースも出しておりノベルティやタイミンの写真を撮りに来られる方など盛況でした。足を運んでいただいた皆様ありがとうございます! この DroidKaigi から配布するノベルティに新しく「マイクロファイバークロス」が追加されました。めちゃめちゃかわいいデザインになっているので手に入れた方はぜひ使ってみてください! また、ネイルブースの横に出していたので「せっかくだし」と初めてネイルを体験しましたが、とてもかわいくテンションも上がって最高でした。 今回は DroidKaigi 2024 に参加した Android アプリ開発メンバー( tick-taku , murata, haru, nashihara)が気になったセッションの感想などをレポートします。 セッション紹介 nashihara Day1: From 0 to 100 with Kotlin and Compose Multiplatform 2024.droidkaigi.jp Kotlin Multiplatform / Compose Multiplatform について、初学者向けにどうやって書いたらいいのかや platform ごとの書き分けの方法などが解説されていました。 Day1のセッションはこちらのみでワークショップ形式での講演でした。 ワークショップの内容は、platform ごとのコードを書く場所の説明から始まり、platform ごとに使えるAPIの種類(例えば kotlin api は全ての platform で使えるが android api は android platform でしか使えない)の説明やそれぞれの Lifecycle がどうなっているかなど、主にこれから KMP/CMP を触り始める人を対象とした内容でした。 自分は今回初めて触る内容が多く新鮮で面白かったです。 特にワークショップの課題として実際に Compose で書いたUIを web / desktop / android platform ごとに実行し確認する、という内容がありましたが、いつも android でUIを作る感覚でUIを書くだけで、 web / desktop アプリを作れてしまうのは、わかっていても感動しました。 Lifecycle に関しても、ちゃんとハンドリングできるようになっていて、web であれば focus → onResume、 blur → onPause 、ios であれば viewWillAppear → onStart、 viewDidDisapper → onStop となっており、 android アプリ開発者が見慣れた形で実装できる点がすごく良いです。 また、collectAsStateWithLifecycle を使っておけばいい感じになる、という便利メソッドも教えてもらいました。 初学者にとって、KMP / CMP の良い導入になるワークショップで楽しいセッションでした。 tick-taku デザインからアプリ実装まで一貫したデザインシステムを構築するベストプラクティス 2024.droidkaigi.jp デザインシステムにおけるFigma との連携やプラクティス、導入することのメリットや心構えなどが解説されています。 最近タイミーではデザインシステムを導入しました。その時にリードしてくれたデザイナーの方が話していた内容が、エンジニア目線から解説されていてより理解が深まりました。 個人的には F imga を中間言語として会話できるようになる ことにとても共感しています。 UI 実装においてデザインファイルがスナップショットになっており、実装との差が発生することでデザイナーとのコミュニケーションが都度発生し、開発スピードが遅くなったり心理的ハードルになったりしていました。デザインシステムを導入することで Figma を起点としてデザイン製作時にエンジニアとデザイナー双方のエッセンスが考慮されたコンポーネントで作られることになり、これらの課題のコスト軽減が期待できます。 また Figma の property と Composable の引数を同期させれば、 Figma 上に実装のために必要な情報が全て書かれています。それだけ見ればある種脳死で実装できるようになり、より統一感のある UI 実装がスピーディ & スムーズに行えるようになります。 Figma は素晴らしいツールですね! デザインシステム導入時に非常に勉強になるポイントがちりばめられたセッションでした。スライドもとても分かりやすく見やすかったです。 2024年のナビゲーション・フォーカス対応:Composeでキーボード・ナビゲーションをサポートしよう 2024.droidkaigi.jp Android のアクセシビリティにおける focus についての紹介や実装のポイントが解説されているセッションでした。 実装についてはアクセシビリティを意識したユースケースの紹介と実装や、xml を何年も書いてない人のために Compose での実装方法や tips も紹介されていました。 特に「宣言順序とレベルによって focus が流れる」という話は、なんとなくそうなんだろうなと思っていたところもあって納得できました。focus のことを考えるとやはり ConstraintLayout を多用するのはよくないのかも。 やっぱり Modifier 順序問題は難しいですね... また動作確認方法についても触れられていました。 特に Android Studio 上で物理デバイスのミラーリングができることは個人的に初耳で知れて非常に良かったです。 いつもスプリントレビューなどでチームに画面共有する際に、ブラウザと並べて表示するため scrcpy を使っていましたが IDE だけで済みそうです。 ソフトウェアキーボード体験改善の tips はまた今度とのことなのでそちらも楽しみに待っています。 haru Jetpack ComposeにおけるShared Element Transitionsの実例と導入方法 またその仕組み 2024.droidkaigi.jp Shared Element Transition, 皆さんは使っていますか? 私たちタイミーのアプリ内でもいくつかの場所で使用しており、Compose化するときに泣く泣くShared Element Transitionを使わない方法で実装し直したりしていましたが、ついにComposeでも取り入れられるようになってきました。 このセッションでは、実際にGoogle PhotosのようなUIをComposeで実装していくことによって実際の実装方法、つまづきポイント、その解決方法などを順番に紹介してもらうことができました。 Transitionにもいくつか種類があり、どちらの方が見え方が綺麗なのかといったところまで説明されていて実際に取り入れる際にもとても参考になるセッションでした。 murata Kotlin 2.0が与えるAndroid開発の進化 2024.droidkaigi.jp タイトル通り、Kotlin 2.0によって受けられる恩恵がこれでもかというくらい紹介されていたセッションでした! 数も多かったですが、特に個人的に刺さったものをピックアップして紹介します。 Power AssertをKotlinが正式にサポート 🎉 従来のUnitTestにおけるFailed Messageは非常にシンプルにExpectedとActualの値が表示されるだけのメッセージでした。 Expected : 0 Actual : 6 Power Assertを導入した場合は、以下のようにテストが失敗した理由を事細かに表示してくれます。※公式ページより引用 Incorrect length assert ( hello . length == world . substring ( 1 , 4 ) . length ) { "Incorrect length" } | | | | | | | | | | | 3 | | | | orl | | | world ! | | false | 5 Hello 注意点として、assert式の書き方をPower Assertを意識した書き方に少し変える必要はありそうです。 例えば、 assert(isValidName && isValidAge) のような変数のみをassert式に入れてしまうと以下のようにFailed Messageの情報量も減ってしまう為、先ほどの例のように変数をインライン化する必要があります。 Assertion failed assert ( isValidName && isValidAge ) | | | false true ですが、導入することでテストを用いた開発やCIが落ちた時の要因調査が捗ること間違い無しの素晴らしい機能ですね!! Jetpack Compose Strong Skip Mode enabled by default Kotlin 2.0.20 より、ComposeのStrong Skip ModeがデフォルトでONになりました。 具体的には、Unstableな引数を使用していても、同一instanceであればRecomposeされなくなります。 Strong Skip Modeについては既に各所で話題になっていましたが、さらにこのセクションで紹介されていた以下のポイントが個人的に刺さりました。 Stability Configuration File 使用する箇所とは遠いところでStableの指定を行う行為自体に懐疑的でしたが、セッションで紹介されていた「java.time.LocalDateのようなJavaのクラスをStableだと認識させる」ようなユースケースではかなり有用だと感じました。 Lambdaの再生成の条件 Lambdaの中で一般的なViewModelのようなUnstableな変数を参照していても再生成されなくなるといった点は非常に便利だなと感じました。onClickのコールバックにてViewModelのメソッドを呼ぶようなこと、よくありますもんね。 代わりに、Lambdaの再生成を前提にしていたようなケースでは @DontMemoize を新たに指定する必要が出てくる点にも地味に注意が必要だと感じました。たまによくありそう。 Object equalsとInstance equals 従来はListは常にUnstableだと見なされていた為、タイミーではComposableの引数にリストを指定する場合はListではなくkotlinx.collections.immutable.ImmutableList を利用するルールとしていました。 ただし、セッションで紹介されている通り、パフォーマンス観点において「Object equalsはO(n)に対してinstance equalsはO(1)」となります。 よって、件数が多く複雑なlistである場合には、ImmutableListではなくListを指定することでinstance equalsとした方が速くなるケースが考えられる為、Strong Skip ModeがONになった際にはどちらを使用するのか都度検討する必要がありそうです。 これらの他にもKotlin 2.0 の良きポイントを理解できる情報がたっぷりな、とても良いセッションでした!! 発表いただき本当にありがとうございました! まとめ 9月に新たにジョインされたメンバーも KaigiPass 制度を利用してみんなでワイワイ参加できました。カンファレンス後にチーム内でセッションのシェアやディスカッションを行うなど、非常に多くの学びやヒントを得ることができモチベーションへ繋がるカンファレンスとなりました。 ブースやアフターパーティーなどでさまざまな人と交流できたり、セッションのアーカイブのアップが早くその日に見直すこともできたり素晴らしい体験ができました。スタッフの皆様ありがとうございました! 次回の開催も楽しみにしています! 余談 今回タイミーはDroidKaigi 2024にゴールドスポンサーとして協賛し、冒頭のブース出展だけでなくDroidKaigiスカラーシップの活動もお手伝いさせていただきました。 そして、DroidKaigiスカラーシップでは企業訪問の取り組みに参加し、DroidKaigiに参加されていた学生さんたちをタイミーのオフィスにご招待しました! タイミー社員と一緒にランチをしたり、オフィス内を一緒に周り紹介したりする中で、熱心に活動されている学生のみなさんとお話できたことは、私たちにとっても楽しく、とても良い機会でした。 タイミーはこれからも技術コミュニティへの協賛、協力を通して一緒に盛り上げていきます!それではまたどこかの勉強会・カンファレンスでお会いしましょう👋
タイミーでバックエンドのテックリードをしている新谷( @euglena1215 )です。 タイミーでは RBS の活用を推進する取り組みを少しずつ進めています。意図は こちら メンバーと雑談していたときに「steep check でコケたときにその名前で調べても全然ヒットしないので型周りのキャッチアップが難しい」という話を聞きました。 いくつかのエラー名でググってみたところ、 Ruby::ArgumentTypeMismatch や Ruby::NoMethod など有名なエラーはヒットしますがほとんどのエラーはヒットせず、ヒットするのは Steep リポジトリの該当実装のみでした。 これでは確かにキャッチアップは難しいだろうと感じたので、Steep のエラーリファレンスを作ってみました。ググってヒットするのが目的なのでテックブログとして公開してインデックスされることを期待します。 各エラーの説明は以下のフォーマットで行います。 エラー名 説明: 簡単なエラーの説明 例: エラーが検出される Ruby コード steep check を実行して得られるエラーメッセージ severity : Steep のエラープリセットに対して、該当エラーの severity がどのように設定されているかの表 Ruby::ArgumentTypeMismatch 説明 : メソッドの型が一致しない場合に発生します。 違反例 : ' 1 ' + 1 test.rb:1:6: [error] Cannot pass a value of type `::Integer` as an argument of type `::string` │ ::Integer <: ::string │ ::Integer <: (::String | ::_ToStr) │ ::Integer <: ::String │ ::Numeric <: ::String │ ::Object <: ::String │ ::BasicObject <: ::String │ │ Diagnostic ID: Ruby::ArgumentTypeMismatch │ └ '1' + 1 ~ severity : all_error default strict lenient silent error error error information nil Ruby::BlockBodyTypeMismatch 説明 : ブロックの body の返り値の型が期待される型と一致しない場合に発生します。 違反例 : lambda {|x| x + 1 } #: ^(Integer) -> String test.rb:1:7: [error] Cannot allow block body have type `::Integer` because declared as type `::String` │ ::Integer <: ::String │ ::Numeric <: ::String │ ::Object <: ::String │ ::BasicObject <: ::String │ │ Diagnostic ID: Ruby::BlockBodyTypeMismatch │ └ lambda {|x| x + 1 } #: ^(Integer) -> String ~~~~~~~~~~~~ severity : all_error default strict lenient silent error warning error information nil Ruby::BlockTypeMismatch 説明 : ブロックの型が期待される型と一致しない場合に発生します。 違反例 : multi = ->(x, y) { x * y } #: ^(Integer, Integer) -> Integer [ 1 , 2 , 3 ].map(&multi) test.rb:2:14: [error] Cannot pass a value of type `^(::Integer, ::Integer) -> ::Integer` as a block-pass-argument of type `^(::Integer) -> U(1)` │ ^(::Integer, ::Integer) -> ::Integer <: ^(::Integer) -> U(1) │ (Params are incompatible) │ │ Diagnostic ID: Ruby::BlockTypeMismatch │ └ [1, 2, 3].map(&multi) ~~~~~~ severity : all_error default strict lenient silent error warning error information nil Ruby::BreakTypeMismatch 説明 : break の型が期待される型と一致しない場合に発生します。 違反例 : 123 .tap { break "" } test.rb:1:10: [error] Cannot break with a value of type `::String` because type `::Integer` is assumed │ ::String <: ::Integer │ ::Object <: ::Integer │ ::BasicObject <: ::Integer │ │ Diagnostic ID: Ruby::BreakTypeMismatch │ └ 123.tap { break "" } ~~~~~~~~ severity : all_error default strict lenient silent error hint error hint nil Ruby::DifferentMethodParameterKind 説明 : メソッドのパラメータの種類が一致しない場合に発生します。省略可能な引数の prefix に ? をつけ忘れることで発生することが多いです。 違反例 : # @type method bar: (name: String) -> void def bar ( name : " foo " ) end test.rb:2:8: [error] The method parameter has different kind from the declaration `(name: ::String) -> void` │ Diagnostic ID: Ruby::DifferentMethodParameterKind │ └ def bar(name: "foo") ~~~~~~~~~~~ severity : all_error default strict lenient silent error hint error nil nil Ruby::FallbackAny 説明 : 型が不明な場合に untyped が使用されることを示します。一度 [] で値を初期化したのちに再代入するような実装で発生することが多いです。 違反例 : a = [] a << 1 test.rb:1:4: [error] Cannot detect the type of the expression │ Diagnostic ID: Ruby::FallbackAny │ └ a = [] ~~ severity : all_error default strict lenient silent error hint warning nil nil Ruby::FalseAssertion 説明 : Steep の型アサーションが誤っている場合に発生します。 違反例 : array = [] #: Array[Integer] hash = array #: Hash[Symbol, String] test.rb:2:7: [error] Assertion cannot hold: no relationship between inferred type (`::Array[::Integer]`) and asserted type (`::Hash[::Symbol, ::String]`) │ Diagnostic ID: Ruby::FalseAssertion │ └ hash = array #: Hash[Symbol, String] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ severity : all_error default strict lenient silent error hint error nil nil Ruby::ImplicitBreakValueMismatch 説明 : 引数無し break の値( nil )がメソッドの返り値の期待される型と一致しない場合に発生します。 違反例 : class Foo # @rbs () { (String) -> Integer } -> String def foo '' end end Foo .new.foo do |x| break end test.rb:9:2: [error] Breaking without a value may result an error because a value of type `::String` is expected │ nil <: ::String │ │ Diagnostic ID: Ruby::ImplicitBreakValueMismatch │ └ break ~~~~~ severity : all_error default strict lenient silent error hint information nil nil Ruby::IncompatibleAnnotation 説明 : 型注釈が不適切または一致しない場合に発生します。 違反例 : a = [ 1 , 2 , 3 ] if _ = 1 # @type var a: String a + "" end test.rb:5:2: [error] Type annotation about `a` is incompatible since ::String <: ::Array[::Integer] doesn't hold │ ::String <: ::Array[::Integer] │ ::Object <: ::Array[::Integer] │ ::BasicObject <: ::Array[::Integer] │ │ Diagnostic ID: Ruby::IncompatibleAnnotation │ └ a + "" ~~~~~~ severity : all_error default strict lenient silent error hint error nil nil Ruby::IncompatibleArgumentForwarding 説明 : 引数に ... を使ってメソッドの引数を forward する際に、引数の型が一致しない場合に発生します。 違反例 : class Foo # @rbs (*Integer) -> void def foo (*args) end # @rbs (*String) -> void def bar (...) foo(...) end end test.rb:8:8: [error] Cannot forward arguments to `foo`: │ (*::Integer) <: (*::String) │ ::String <: ::Integer │ ::Object <: ::Integer │ │ Diagnostic ID: Ruby::IncompatibleArgumentForwarding │ └ foo(...) ~~~ severity : all_error default strict lenient silent error warning error information nil Ruby::IncompatibleAssignment 説明 : 代入の際の型が不適切または一致しない場合に発生します。 違反例 : # @type var x: Integer x = " string " test.rb:2:0: [error] Cannot assign a value of type `::String` to a variable of type `::Integer` │ ::String <: ::Integer │ ::Object <: ::Integer │ ::BasicObject <: ::Integer │ │ Diagnostic ID: Ruby::IncompatibleAssignment │ └ x = "string" ~~~~~~~~~~~~ severity : all_error default strict lenient silent error hint error hint nil Ruby::InsufficientKeywordArguments 説明 : キーワード引数が不足している場合に発生します。 違反例 : class Foo def foo ( a :, b :) end end Foo .new.foo( a : 1 ) test.rb:5:8: [error] More keyword arguments are required: b │ Diagnostic ID: Ruby::InsufficientKeywordArguments │ └ Foo.new.foo(a: 1) ~~~~~~~~~ severity : all_error default strict lenient silent error error error information nil Ruby::InsufficientPositionalArguments 説明 : 位置引数が不足している場合に発生します。 違反例 : class Foo def foo (a, b) end end Foo .new.foo( 1 ) test.rb:5:8: [error] More keyword arguments are required: b │ Diagnostic ID: Ruby::InsufficientKeywordArguments │ └ Foo.new.foo(a: 1) ~~~~~~~~~ severity : all_error default strict lenient silent error error error information nil Ruby::InsufficientTypeArgument 説明 : 型引数に対する型注釈が不足している場合に発生します。 違反例 : class Foo # @rbs [T, S] (T, S) -> [T, S] def foo (x, y) [x, y] end end Foo .new.foo( 1 , 2 ) #$ Integer test.rb:8:0: [error] Requires 2 types, but 1 given: `[T, S] (T, S) -> [T, S]` │ Diagnostic ID: Ruby::InsufficientTypeArgument │ └ Foo.new.foo(1, 2) #$ Integer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ severity : all_error default strict lenient silent error hint error nil nil Ruby::InvalidIgnoreComment 説明 : steep:ignore:start コメントはあるが steep:ignore:end コメントがないなど、無効なコメントが存在する場合に発生します。 違反例 : # steep:ignore:start test.rb:1:0: [error] Invalid ignore comment │ Diagnostic ID: Ruby::InvalidIgnoreComment │ └ # steep:ignore:start ~~~~~~~~~~~~~~~~~~~~ severity : all_error default strict lenient silent error warning warning warning nil Ruby::MethodArityMismatch 説明 : キーワード引数なのに順序引数としてメソッドの引数の型を記述しているなど、メソッドの引数の型が一致しない場合に発生します。 違反例 : class Foo # @rbs (Integer x) -> Integer def foo ( x :) x end end test.rb:3:9: [error] Method parameters are incompatible with declaration `(::Integer) -> ::Integer` │ Diagnostic ID: Ruby::MethodArityMismatch │ └ def foo(x:) ~~~~ severity : all_error default strict lenient silent error error error information nil Ruby::MethodBodyTypeMismatch 説明 : メソッドの返り値が期待される型と一致しない場合に発生します。 違反例 : class Foo # @rbs () -> String def foo 1 end end test.rb:3:6: [error] Cannot allow method body have type `::Integer` because declared as type `::String` │ ::Integer <: ::String │ ::Numeric <: ::String │ ::Object <: ::String │ ::BasicObject <: ::String │ │ Diagnostic ID: Ruby::MethodBodyTypeMismatch │ └ def foo ~~~ severity : all_error default strict lenient silent error error error warning nil Ruby::MethodDefinitionMissing 説明 : メソッドの型定義が存在するがメソッドの実装が欠落している場合に発生します。 違反例 : class Foo # @rbs! # def bar: () -> void end test.rb:1:6: [error] Cannot find implementation of method `::Foo#bar` │ Diagnostic ID: Ruby::MethodDefinitionMissing │ └ class Foo ~~~ severity : all_error default strict lenient silent error nil hint nil nil Ruby::MethodParameterMismatch 説明 : メソッドのパラメータの型が一致しない場合に発生します。 違反例 : class Foo # @rbs (Integer x) -> Integer def foo ( x :) x end end test.rb:3:10: [error] The method parameter is incompatible with the declaration `(::Integer) -> ::Integer` │ Diagnostic ID: Ruby::MethodParameterMismatch │ └ def foo(x:) ~~ severity : all_error default strict lenient silent error error error warning nil Ruby::MethodReturnTypeAnnotationMismatch 説明 : メソッドの戻り値の型注釈が期待される型と一致しない場合に発生します。 違反例 : class Foo # @rbs () -> String def foo # @type return: Integer 123 end end test.rb:3:2: [error] Annotation `@type return` specifies type `::Integer` where declared as type `::String` │ ::Integer <: ::String │ ::Numeric <: ::String │ ::Object <: ::String │ ::BasicObject <: ::String │ │ Diagnostic ID: Ruby::MethodReturnTypeAnnotationMismatch │ └ def foo ~~~~~~~ severity : all_error default strict lenient silent error hint error nil nil Ruby::MultipleAssignmentConversionError 説明 : 複数代入の変換に失敗した場合に発生します。 違反例 : class WithToAry # @rbs () -> Integer def to_ary 1 end end a, b = WithToAry .new() test.rb:8:8: [error] Cannot convert `::WithToAry` to Array or tuple (`#to_ary` returns `::Integer`) │ Diagnostic ID: Ruby::MultipleAssignmentConversionError │ └ (a, b = WithToAry.new()) ~~~~~~~~~~~~~~~ severity : all_error default strict lenient silent error hint error nil nil Ruby::NoMethod 説明 : 型定義が存在しないメソッドが呼び出された場合に発生します。 違反例 : "" .non_existent_method test.rb:1:3: [error] Type `::String` does not have method `non_existent_method` │ Diagnostic ID: Ruby::NoMethod │ └ "".non_existent_method ~~~~~~~~~~~~~~~~~~~ severity : all_error default strict lenient silent error error error information nil Ruby::ProcHintIgnored 説明 : Proc に関する型注釈が無視された場合に発生します。 違反例 : # @type var proc: (^(::Integer) -> ::String) | (^(::String, ::String) -> ::Integer) proc = -> (x) { x.to_s } test.rb:2:7: [error] The type hint given to the block is ignored: `(^(::Integer) -> ::String | ^(::String, ::String) -> ::Integer)` │ Diagnostic ID: Ruby::ProcHintIgnored │ └ proc = -> (x) { x.to_s } ~~~~~~~~~~~~~~~~~ severity : all_error default strict lenient silent error hint information nil nil Ruby::ProcTypeExpected 説明 : Proc 型が期待される場合に発生します。 違反例 : -> (&block) do # @type var block: Integer end test.rb:1:4: [error] Proc type is expected but `::Integer` is specified │ Diagnostic ID: Ruby::ProcTypeExpected │ └ -> (&block) do ~~~~~~ severity : all_error default strict lenient silent error hint error nil nil Ruby::RBSError 説明 : 型アサーションや型適用に書かれたRBS型がエラーを生じる場合に発生します。 違反例 : a = 1 #: Int test.rb:1:9: [error] Cannot find type `::Int` │ Diagnostic ID: Ruby::RBSError │ └ a = 1 #: Int ~~~ severity : all_error default strict lenient silent error information error information nil Ruby::RequiredBlockMissing 説明 : メソッド呼び出し時に必要な block が欠落している場合に発生します。 違反例 : class Foo # @rbs () { () -> void } -> void def foo yield end end Foo .new.foo test.rb:7:8: [error] The method cannot be called without a block │ Diagnostic ID: Ruby::RequiredBlockMissing │ └ Foo.new.foo ~~~ severity : all_error default strict lenient silent error error error hint nil Ruby::ReturnTypeMismatch 説明 : return の型とメソッドの戻り値の型が一致しない場合に発生します。 違反例 : # @type method foo: () -> Integer def foo return " string " end test.rb:3:2: [error] The method cannot return a value of type `::String` because declared as type `::Integer` │ ::String <: ::Integer │ ::Object <: ::Integer │ ::BasicObject <: ::Integer │ │ Diagnostic ID: Ruby::ReturnTypeMismatch │ └ return "string" ~~~~~~~~~~~~~~~ severity : all_error default strict lenient silent error error error warning nil Ruby::SetterBodyTypeMismatch 説明 : セッターメソッドの戻り値の型が期待される型と一致しない場合に発生します。 違反例 : class Foo # @rbs (String) -> String def foo= (value) 123 end end test.rb:3:6: [error] Setter method `foo=` cannot have type `::Integer` because declared as type `::String` │ ::Integer <: ::String │ ::Numeric <: ::String │ ::Object <: ::String │ ::BasicObject <: ::String │ │ Diagnostic ID: Ruby::SetterBodyTypeMismatch │ └ def foo=(value) ~~~~ severity : all_error default strict lenient silent error information error nil nil Ruby::SetterReturnTypeMismatch 説明 : セッターメソッドの return の型が期待される型と一致しない場合に発生します。 違反例 : class Foo # @rbs (String) -> String def foo= (value) return 123 end end test.rb:4:4: [error] The setter method `foo=` cannot return a value of type `::Integer` because declared as type `::String` │ ::Integer <: ::String │ ::Numeric <: ::String │ ::Object <: ::String │ ::BasicObject <: ::String │ │ Diagnostic ID: Ruby::SetterReturnTypeMismatch │ └ return 123 ~~~~~~~~~~ severity : all_error default strict lenient silent error information error nil nil Ruby::SyntaxError 説明 : Ruby の構文エラーが発生した場合に発生します。 違反例 : if x == 1 puts " Hello " test.rb:2:14: [error] SyntaxError: unexpected token $end │ Diagnostic ID: Ruby::SyntaxError │ └ puts "Hello" severity : all_error default strict lenient silent error hint hint hint nil Ruby::TypeArgumentMismatchError 説明 : 型引数が期待される型と一致しない場合に発生します。 違反例 : class Foo # @rbs [T < Numeric] (T) -> T def foo (x) x end end Foo .new.foo( "" ) #$ String test.rb:7:19: [error] Cannot pass a type `::String` as a type parameter `T < ::Numeric` │ ::String <: ::Numeric │ ::Object <: ::Numeric │ ::BasicObject <: ::Numeric │ │ Diagnostic ID: Ruby::TypeArgumentMismatchError │ └ Foo.new.foo("") #$ String ~~~~~~ severity : all_error default strict lenient silent error hint error nil nil Ruby::UnexpectedBlockGiven 説明 : ブロックが予期されない場面で渡された場合に発生します。 違反例 : [ 1 ].at( 1 ) { 123 } test.rb:1:10: [error] The method cannot be called with a block │ Diagnostic ID: Ruby::UnexpectedBlockGiven │ └ [1].at(1) { 123 } ~~~~~~~ severity : all_error default strict lenient silent error warning error hint nil Ruby::UnexpectedDynamicMethod 説明 : 動的に定義されたメソッドが存在しない場合に発生します。 違反例 : class Foo # @dynamic foo def bar end end test.rb:1:6: [error] @dynamic annotation contains unknown method name `foo` │ Diagnostic ID: Ruby::UnexpectedDynamicMethod │ └ class Foo ~~~ severity : all_error default strict lenient silent error hint information nil nil Ruby::UnexpectedError 説明 : 予期しない一般的なエラーが発生した場合に発生します。 違反例 : class Foo # @rbs () -> String123 def foo end end test.rb:1:0: [error] UnexpectedError: sig/generated/test.rbs:5:17...5:26: Could not find String123(RBS::NoTypeFoundError) │ ... │ (36 more backtrace) │ │ Diagnostic ID: Ruby::UnexpectedError │ └ class Foo ~~~~~~~~~ severity : all_error default strict lenient silent error hint information hint nil Ruby::UnexpectedJump 説明 : 予期しないジャンプが発生した場合に発生します。 違反例 : break test.rb:1:0: [error] Cannot jump from here │ Diagnostic ID: Ruby::UnexpectedJump │ └ break ~~~~~ severity : all_error default strict lenient silent error hint error nil nil Ruby::UnexpectedJumpValue 説明 : ジャンプの値を渡しても値が無視される場合に発生します。 違反例 : while true next 3 end test.rb:2:2: [error] The value given to next will be ignored │ Diagnostic ID: Ruby::UnexpectedJumpValue │ └ next 3 ~~~~~~ severity : all_error default strict lenient silent error hint error nil nil Ruby::UnexpectedKeywordArgument 説明 : 予期しないキーワード引数が渡された場合に発生します。 違反例 : class Foo # @rbs (x: Integer) -> void def foo ( x :) end end Foo .new.foo( x : 1 , y : 2 ) test.rb:7:18: [error] Unexpected keyword argument │ Diagnostic ID: Ruby::UnexpectedKeywordArgument │ └ Foo.new.foo(x: 1, y: 2) ~ severity : all_error default strict lenient silent error error error information nil Ruby::UnexpectedPositionalArgument 説明 : 予期しない位置引数が渡された場合に発生します。 違反例 : class Foo # @rbs (Integer) -> void def foo (x) end end Foo .new.foo( 1 , 2 ) test.rb:7:15: [error] Unexpected positional argument │ Diagnostic ID: Ruby::UnexpectedPositionalArgument │ └ Foo.new.foo(1, 2) ~ severity : all_error default strict lenient silent error error error information nil Ruby::UnexpectedSuper 説明 : super を呼び出した際に親クラスに同名のメソッドが定義されていないなど、予期しない場面で super が使用された場合に発生します。 違反例 : class Foo def foo super end end test.rb:3:4: [error] No superclass method `foo` defined │ Diagnostic ID: Ruby::UnexpectedSuper │ └ super ~~~~~ severity : all_error default strict lenient silent error information error nil nil Ruby::UnexpectedTypeArgument 説明 : 予期しない型引数が渡された場合に発生します。 違反例 : class Foo # @rbs [T] (T) -> T def foo (x) x end end Foo .new.foo( 1 ) #$ Integer, Integer test.rb:8:27: [error] Unexpected type arg is given to method type `[T] (T) -> T` │ Diagnostic ID: Ruby::UnexpectedTypeArgument │ └ Foo.new.foo(1) #$ Integer, Integer ~~~~~~~ severity : all_error default strict lenient silent error hint error nil nil Ruby::UnexpectedYield 説明 : yield が予期しない場面で使用された場合に発生します。 違反例 : class Foo # @rbs () -> void def foo yield end end test.rb:4:4: [error] No block given for `yield` │ Diagnostic ID: Ruby::UnexpectedYield │ └ yield ~~~~~ severity : all_error default strict lenient silent error warning error information nil Ruby::UnknownConstant 説明 : 未知の定数が参照された場合に発生します。 違反例 : FOO test.rb:1:0: [error] Cannot find the declaration of constant: `FOO` │ Diagnostic ID: Ruby::UnknownConstant │ └ FOO ~~~ severity : all_error default strict lenient silent error warning error hint nil Ruby::UnknownGlobalVariable 説明 : 未知のグローバル変数が参照された場合に発生します。 違反例 : $foo test.rb:1:0: [error] Cannot find the declaration of global variable: `$foo` │ Diagnostic ID: Ruby::UnknownGlobalVariable │ └ $foo ~~~~ severity : all_error default strict lenient silent error warning error hint nil Ruby::UnknownInstanceVariable 説明 : 未知のインスタンス変数が参照された場合に発生します。 違反例 : class Foo def foo @foo = ' foo ' end end test.rb:3:4: [error] Cannot find the declaration of instance variable: `@foo` │ Diagnostic ID: Ruby::UnknownInstanceVariable │ └ @foo = 'foo' ~~~~ severity : all_error default strict lenient silent error information error hint nil Ruby::UnreachableBranch 説明 : if , unless による到達不可能な分岐が存在する場合に発生します。 違反例 : if false 1 end test.rb:1:0: [error] The branch is unreachable │ Diagnostic ID: Ruby::UnreachableBranch │ └ if false ~~ severity : all_error default strict lenient silent error hint information hint nil Ruby::UnreachableValueBranch 説明 : case when による到達不可能な分岐が存在し、分岐の型が bot でなかった場合に発生します。 違反例 : x = 1 case x when Integer " one " when String " two " end test.rb:5:0: [error] The branch may evaluate to a value of `::String` but unreachable │ Diagnostic ID: Ruby::UnreachableValueBranch │ └ when String ~~~~ severity : all_error default strict lenient silent error hint warning hint nil Ruby::UnresolvedOverloading 説明 : オーバーロードが行われているメソッドに対して型が解決できない場合に発生します。 違反例 : 3 + " foo " test.rb:1:0: [error] Cannot find compatible overloading of method `+` of type `::Integer` │ Method types: │ def +: (::Integer) -> ::Integer │ | (::Float) -> ::Float │ | (::Rational) -> ::Rational │ | (::Complex) -> ::Complex │ │ Diagnostic ID: Ruby::UnresolvedOverloading │ └ 3 + "foo" ~~~~~~~~~ severity : all_error default strict lenient silent error error error information nil Ruby::UnsatisfiableConstraint 説明 : RBSと型注釈の辻褄が合わないなど、どうやっても型制約が満たされない場合に発生します。 違反例 : class Foo # @rbs [A, B] (A) { (A) -> void } -> B def foo (x) end end test = Foo .new test.foo( 1 ) do |x| # @type var x: String end test.rb:9:0: [error] Unsatisfiable constraint `::Integer <: A(1) <: ::String` is generated through (A(1)) { (A(1)) -> void } -> B(2) │ ::Integer <: ::String │ ::Numeric <: ::String │ ::Object <: ::String │ ::BasicObject <: ::String │ │ Diagnostic ID: Ruby::UnsatisfiableConstraint │ └ test.foo(1) do |x| ~~~~~~~~~~~~~~~~~~ severity : all_error default strict lenient silent error hint error hint nil Ruby::UnsupportedSyntax 説明 : Steep としてサポートされていない構文が使用された場合に発生します。 違反例 : (_ = []).[]=(*(_ = nil )) test.rb:1:13: [error] Unsupported splat node occurrence │ Diagnostic ID: Ruby::UnsupportedSyntax │ └ (_ = []).[]=(*(_ = nil)) ~~~~~~~~~~ severity : all_error default strict lenient silent error hint information hint nil 狙ったエラーを引き起こすというのは今年の RubyKaigi であった Ruby "enbugging" Quiz に近い感覚でした。難しい。 基本的には Steep リポジトリにあるテストケースを見ながら埋めていったんですが、中にはテストケースがないものもあったので soutaro さんに直接質問をしながら進めていきました。 また、副産物として Steep で使われなくなったが定義として残っているルールを発見し、削除する patch を作れたのも個人的には良かったです。 github.com
エンジニアリング本部 プラットフォームエンジニアリング1G 橋本です。我々のグループでは業務の柱の一つとして、クラウドインフラの構築・運用を行っています。その中でAmazon Aurora MySQL(以下、AuroraもしくはAurora MySQL)のアップグレードがビジネスインパクトが大きい作業となりました。本記事はAurora MySQLアップグレード方法の検討について記述した投稿になります。 この記事のまとめ 前提情報や課題感について Blue/Green Deploymentsによるアップグレードとは もしもの場合はロールバックしたい 検討したロールバック手法 DMS方式 リストア方式 逆レプリ方式(没案) 困った。どうしよう? タイムリーな機能を教えてもらえた? SwitchOver時に静止断面を教えてくれる 逆レプリ(新案)はどうなるのか? いままでの方式案との比較 まとめ この記事のまとめ 比較的 大きなデータ で且つ 更新量の多い Auroraクラスターのアップグレードで且つ ダウンタイムが少ない ロールバック方式を検討していました ダウンタイム最小化の部分で大きな課題感 があったが、 Auroraの機能追加により大きく緩和 できることが分かりました この記事では、ダウンタイム最小化を軸にした場合のロールバックに関する課題感と解決方法を追加された当該機能に触れながら紹介します 前提情報や課題感について Blue/Green Deploymentsによるアップグレードとは Auroraの機能により既存クラスタをベースに無停止で新規クラスターをBlue/Green Deploymentsという機能を用いて作成することができます。なお、Green側クラスターにはEngineVersion(8.0.mysql_aurora.3.02.0, 5.7.mysql_aurora.2.11.2 etc.)や、パラメータグループを既存クラスターと異なるものを適用できるので、アップグレードや新規設定の適用をダウンタイム少なく行うことができます。 切替自体はワンクリックで可能で SwitchOver という命令を与えると、1分程度のダウンタイムは発生しますが特に難しいことはなくGreenクラスターに切り替えることができます。この際にアプリケーションサーバー等が参照しているエンドポイントも自動で切り替わります。 B/Gデプロイメントのイメージ もしもの場合はロールバックしたい この記事のテーマ はGreenクラスターに切り替えたあとにアプリケーションサーバー等で不具合があった場合に、元のクラスター(Blueクラスターそのもの or 同等の構成のもの)に ロールバックできるかどうか、その手法 についてになります。 ロールバックの可能性はそれほど高くない と考えています。当然事前に動作検証はテスト環境で行い、アップグレードに臨みます。しかし、何らかの不具合が 本番環境でのみ発生する可能性はゼロではない 為、万が一の備えであってもロールバックの手法はコンティンジェンシープランとして持っておく必要があります。弊社でもサービスの中核にAurora MySQLを利用しているため、 事業継続性の観点でも重要 なものとなります。 ロールバックから戻せるか? 検討したロールバック手法 当初は次の表の3つの方式を検討しました。なおダウンタイムはできれば数分、長くても30分程度のダウンタイムを許容(◯)として考えています。 この比較は 更新頻度や量が多く、ダウンタイムを極小化したいユースケースでAurora MySQLを使用している前提 としています。たとえば、分析用DBであったり、小規模のDBであったりすると許容できるポイントが変わると思われます。 方式名 ダウンタイム 互換性 容易さ 特徴 AWS Database Migration Service(DMS) ◯ ☓ ◯ データ互換性の担保が難しい GreenのバックアップデータをBlueへリストア ☓ ◯ △ ダウンタイムはデータ量に比例 GreenからBlueへの逆方向レプリケーション (没案) △ ◯ △ ロールバック時のダウンタイムに加えて、 SwitchOver前にダウンタイムが必要 DMS方式 AWS DMS を利用してGreenクラスタの更新情報をロールバック用クラスターに同期する方式です。当初この方式での移行を検討していました。流れは以下の通りになります。 ★ 事前準備 予めロールバック用クラスターを既存クラスターと同じ定義で作成しておく DMSでGreenからロールバック用クラスターに 変更データキャプチャ(CDC)により適宜データ同期が行われる構成 にしておく ★ SwitchOver! ★ 問題発生!ロールバック開始 - ダウンタイム サービス・メンテナンス状態にする(=DBの更新が行われないようにする) DMS定義を削除する ★ ロールバック終了 - サービス再開 アプリケーション・サーバーの接続先をロールバック用クラスターに変更してサービスを再開する この方式はDMSの設定をしてしまえば 複雑な操作・設定を必要とせず、簡単にロールバックができる ところが特徴になります。この点が最大の魅力であり、当初の採用理由だったのですが、テストを行っていると データ互換性の担保が難しい ことに気づきました。 特に、 DMSのユーザーガイド に記載されている制限やデータ型の変換が大きな問題となりました。 例えば、上記ガイドに制約として 列の AUTO_INCREMENT 属性は、ターゲットデータベース列に移行されません。 と記載されています。AUTO_INCREMENTのような属性はMySQL独自の自動増分の機能であり、ターゲットデータベースがOracleやPostgreSQLなど異なる場合にも汎用的に移行可能なように引き継がれない仕様となっているようです。 同じく、例えばJSON型がCLOB型に型変換をして同期するデフォルトマッピングがあり、ソースとターゲットデータベースでデータ型が変わってしまう点も、MySQL to MySQLでの単純なデータコピーに使いたい用途としては考慮事項が多く データ互換性という観点では☓(バツ)を付けざるを得ないと判断 しました。 リストア方式 次に検討したのはMySQLのバックアップ・リストア機能を用いてロールバックをする方式です。互換性の維持を主眼にすると以下の方式は確実な方式となります。 ★ 事前準備 予めロールバック用クラスターを既存クラスターと同じ定義で作成しておく ★ SwitchOver! ★ 問題発生!ロールバック開始 - ダウンタイム サービス・メンテナンス状態にする(=DBの更新が行われないようにする) GreenクラスタのDBバックアップを取得する ロールバック用クラスターに先の バックアップデータでリストア を行う ★ ロールバック終了 - サービス再開 アプリケーション・サーバーの接続先をロールバック用クラスターに変更してサービスを再開する DBバックアップ取得やリストアはmysqldumpなどのバックアップツールを用います。マネージド・サービスを普段利用しているとCLIベースでのバックアップ・リストアには一定の習熟が必要となるため、 ”容易さ”は△ としています。 この方式の最大の問題点は バックアップ・リストアに要する時間 でした。1TB弱(数百GB)オーダーのデータを対象として事前にテストしたところ、mysqldumpでは数十時間かかることが分かり全く現実的ではありませんでした。 mysqlshを用いて並列度を上げることで高速化できますが、それでも4時間程度の時間を要することが分かりました。Aurora MySQLのデータ書き込みがボトルネックとなっておりインスタンスサイズを大きくするなどして検証しましたが、これ以上の高速化は見込めず採用が難しいと判断しました。 なお、データ量が100GB以下程度と比較的小さければ数分〜30分程度のダウンタイムを許容する限りは、この方式が確実ではないかと考えています。 逆レプリ方式(没案) ここまで来て、最後の手段ではありますがGreenクラスターからのデータ同期にMySQLのレプリケーション機能(Primary/Secondary方式)を用いることができれば良いのではないかということに思い当たります。Auroraでレプリケーション設定が可能か分からなかったのですが、 Cyber Agentさまの記事 にズバリ書いていたため参考にさせていただきました。流れは以下のようになります。 ★ 事前準備 Blueクラスターを利用するため、ロールバック用クラスターは”作成しない” ★ 静止断面取得作業 - ダウンタイム サービス・メンテナンス状態にする(=DBの更新が行われないようにする) ★ SwitchOver! GreenクラスターをPrimary、BlueクラスターをSecondaryとした レプリケーションを設定 して同期させる サービス・メンテナンスを解除する ★ サービス再開 ★ 問題発生!ロールバック開始 - ダウンタイム サービス・メンテナンス状態にする(=DBの更新が行われないようにする) Blueクラスターを昇格させる(レプリケーション設定を解除) ロールバック終了 - サービス再開 アプリケーション・サーバーの接続先をBlueクラスターに変更してサービスを再開する この方式は標準的なMySQLレプリケーションを用いるため、データ互換性に対する懸念は少ないことが期待できます。しかしながら、SwitchOverを行う前に サービスダウンを発生させる必要がある ことが問題となりました。サービスダウンが必要な理由は所謂、静止断面を作るためになります。 # ※前提: サービス停止(更新停止)をした状態でSwichOverを行う # Greenクラスターで現在のポジション(静止断面)を取得する [Green] > show master status; +----------------------------+----------+--------------+------------------+-------------------+ | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set | +----------------------------+----------+--------------+------------------+-------------------+ | mysql-bin-changelog.000133 | 941 | | | | +----------------------------+----------+--------------+------------------+-------------------+ # BlueクラスターでGreenクラスターをPrimaryとしたレプリケーション定義を行う(CHANGE MASTER TO相当) CALL mysql.rds_set_external_master( 'GreenクラスターのWriterEndpoint', 3306, 'replicationユーザ名', 'replicationパスワード', 'mysql-bin-changelog.000133', 941, 0); Green, Blueクラスターは、それぞれ異なるbinlogファイル・ポジションを保持しているため、BlueクラスターにとってGreenクラスターのどのポジションから読み出しにいくべきか?、レプリケーション開始時点のポジションが必要になります。 上記のコマンド例のように、 show master status; コマンドにより ポジション取得を行うためには書き込みを一旦停止する 必要があり、これがSwitchOver前にサービス・メンテナンスを行う理由となります。 結果的には、本案は没案となりました。 そもそもロールバックは万が一の備えであるため、その備えのための事前作業でサービス停止が100%発生することは許容できなかった からです。 困った。どうしよう? ここまで長々と各方式の説明をしてきましたが、これ!という方式が見つかりません。大きめのデータを持つAuroraをダウンタイム少なくロールバックする手法を確立すべく検討をしていましたが、行き詰まってしまいました。 タイムリーな機能を教えてもらえた? AWSの方にも相談をしながら検討をしていたのですが、24’ 8/6に公開された このブログポスト を紹介していただきました。先ほど没案となった逆レプリ方式を改善できる一手になる!という手応えを得て検証を開始しました。 SwitchOver時に静止断面を教えてくれる ポイントはSwitchOverでGreenクラスターに切り替わった瞬間のポジションを 静止断面として教えてくれる ことにあります。次の図のようにSwitchOverしたときにクラスターEventとしてバイナリファイルとポジションがメッセージに出力されます。 一見すると地味な機能ですが、先に没案の課題として述べたとおり 通常はサービス停止をしなければ取得できない情報が無停止で取得できる ので、とても強力な機能です。 SwitchOver時のイベントメッセージ あとは次の図のように、もともとあったBlueクラスターをSecondaryとしたレプリケーション設定を行います。なお、ブログ執筆時点では記事中の設定に誤りがあり、BlueクラスターのWriterEndpointを指定する記述になっています。正しくはここに記載したように”GreenのEndpoint”を指定する必要があるのでご注意ください。 詳細の手順は先のAWSブログポストにすべて記載されていますので、ここではこれ以上の詳細は割愛します。気になった方は是非ご参照ください。 逆レプリケーションのイメージ 逆レプリ(新案)はどうなるのか? 逆レプリ(新案)は以下のような流れになります。SwitchOverを行ったあとに逆レプリ設定をして同期を行っておく点がポイントになります。 ★ 事前準備 Blueクラスターを利用するため、ロールバック用クラスターは”作成しない” ★ SwitchOver! クラスターEventに出力された静止断面のポジションを確認する GreenクラスターをPrimary、BlueクラスターをSecondaryとした レプリケーションを設定 して同期させる ★ 問題発生!ロールバック開始 - ダウンタイム サービス・メンテナンス状態にする(=DBの更新が行われないようにする) Blueクラスターを昇格させる(レプリケーション設定を解除) ★ ロールバック終了 - サービス再開 アプリケーション・サーバーの接続先をBlueクラスターに変更してサービスを再開する いままでの方式案との比較 上記を踏まえて、他案との比較を行いました。あくまで 弊社環境においてダウンタイムは30分以内という条件 で選んだ場合、この逆レプリ(新案)方式はベストな選択となりました。 1点、 容易さが△ としているのは、 Aurora MySQLの運用において普段はあまり行わないCLIベースでの操作を行うことであったり、binlogによるレプリケーション同期の機序について理解する必要がある 点に起因しています。 組織的な対応強度を備えるために一定の学習コストがかかりますが、一つの技術習得としてじっくりと時間をかけて取り組んでいこうと考えています。 方式名 ダウンタイム 互換性 容易さ 特徴 AWS Database Migration Service(DMS) ◯ ☓ ◯ データ互換性の担保が難しい GreenのバックアップデータをBlueへリストア ☓ ◯ △ ダウンタイムはデータ量に比例 GreenからBlueへの逆方向レプリケーション(没案) △ ◯ △ ロールバック時のダウンタイムに加えて、 SwitchOver前にダウンタイムが必要 GreenからBlueへの逆方向レプリケーション (新案) ◯ ◯ △ 没案の事前ダウンタイムが克服された! まとめ この投稿では弊社でAurora MySQLアップグレードを行う際に、データ量が比較的多いデータベースで、ダウンタイムを極小化してロールバックを行う方式について検討した軌跡についてシェアをしました。 同じような課題に突き当たった方もいるのではないかなと思います。この記事が課題解決の参考になれば幸いです。
iOSDC 2024 こんにちは、iOSエンジニアの前田( @naoya_maeda ) 、Androidエンジニアの伊藤( @tick_taku77 )です。 2024年8月22-24日に早稲田大学理工学部 西早稲田キャンパスで開催されたiOSDC Japan 2024に、タイミーもゴールドスポンサーとして協賛させていただきました。 イベントは以下のように、3日間連続で行われました。 8月22日(木):day0(前夜祭) 8月23日(金):day1(本編1日目) 8月24日(土):day2(本編2日目) 私達もイベントに参加したので、メンバーそれぞれが気になったセッションや感想をご紹介します。 naoya編 前夜祭で、「動かして学ぶDockKit入門」というタイトルで発表しました。 www.docswell.com 去年行われたWWDC23で発表されたDockKitフレームワークでできることを、体系的に紹介するトークになります。DockKit対応デバイス自体はよくメディアに取り上げられていて認知度が高いですが、DockKitフレームワークはまだ歴史が浅いこともあり、DockKitフレームワークを使用して何ができるかということはあまり知られていません。 本トークでは、DockKitフレームワークでできることを開発者目線でデモを通して紹介しました。 技術記事ではさらに詳しく解説していますので、ご興味をお持ちいただけた方は是非ご覧いただけますと幸いです。 zenn.dev zenn.dev zenn.dev ここからは僕が気になったセッションをご紹介します。 タイトル : GIS入門 - 地理情報をiOSで活用する 登壇者 : 堤 修一 さん www.docswell.com 地図の仕組みを一から理解し、iOSアプリでさまざまな応用ができるようになることを目的としたトークです。iOSアプリエンジニアの方であれば、MapKitのAPIを使用してiPhoneの画面に地図を表示したことは、一度はあるのではないでしょうか。 一方で、地図が表示される仕組みについて深く考える機会はほとんどないと思います。本トークでは、地図の仕組みから始まり、実際にコードを見ながら地図の仕組みの解説を進めていきます。最初はベーシックな地図を表示する方法、最後はマップ上に人間とモンスターを配置した、某ゲームのようなデモを披露してくださいました。 タイトル : iPhone × NFC で実現するスマートキーの開発方法 登壇者 : 岡 優志 さん www.docswell.com NFCのハードウェア特性や規格といった基礎知識的な話に始まり、NFCでスマートキーを作成する方法をデモを通じて紹介するトークです。NFC周りは僕も触っていたことがあるのですが、曖昧な理解だった部分が多いことを実感したトークでした。 このトークを聞いた後、僕もNFCデバイスを使用して何か作ってみたいなと思いました。 基礎を丁寧にわかりやすく説明してくださる、聞き手のことをしっかり考えてお話しされるokaさんらしいトークだなと感じました! iOSDC 2023でご登壇された「作って学ぶBluetoothの基本攻略 〜スマートキーアプリを作ってみよう〜」も非常に面白いので是非ご覧ください! www.youtube.com tick_taku編 タイトル : App Clipの魔法: iOSデザイン開発の新時代 by log5 登壇者 : log5 さん speakerdeck.com App Clip の概要やユースケースについて熱量高く解説されていて、iOSDC 2024のトップバッターを飾るにふさわしい未来にワクワクできる素晴らしいセッションでした! App Clipは名前だけ知っていましたが、Androidで言うInstant Appのようなものでしょうか。 想定している活用方法を聞いてとても感動しました。 自分自身(特に普段使わないのにクーポンなどのために)アプリをインストールすることに結構抵抗があるタイプですし、昔バイトでレジを打ちながらアプリを勧めてインストールのヘルプまで行うのはカロリーも高く、忙しい時間帯ですとレジ待ちの列が長くなりかなり大変でした。 そのステップが短縮されるならユーザーにとっても店舗の方にとってもかなり負担が減ると思われるのでとても効果が高そうだと感じました。 また開発においてもモバイルアプリの共有には非常に課題を感じていて、webと違いURLを共有するだけでは完結せず準備に手間がかかります。App Clipを利用し、スプリントレビューなどにおいてその場でエンジニア以外にロールプレイをしてもらうことでより当事者意識の高いフィードバックが得られるデモンストレーションができそうな予感がします。 ゆくゆくはモバイルアプリはインストールするものではなくなる未来が待っているかもしれませんね。 とはいえ、話されていたユースケースを実現するにはまだまだ課題がありそうなので今後の動向に注目したいと思います。 タイトル : Wallet API, Verifier APIで実現するIDカード on iPhoneの世界 登壇者 : 下森 周平 さん speakerdeck.com ここ最近マイナンバーカードをiPhone(Apple Wallet)に搭載できるようになると話題になっていますね。自分も持ち歩くものがまた減るので非常に楽しみにしています! www.digital.go.jp モバイルeID (モバイル端末に搭載される身分証明書)に関する国際標準規格 (ISO 23220)があり、マイナンバーカード搭載の話もこの一環だそうです。このセッションでは モバイルeID についての理解と、証明書情報を取り扱うAPIの特徴やユースケースを紹介されていました。 我々アプリケーションエンジニアが気になっているアプリからの取得もAPI ( Verify with Wallet API )経由でサポートされているとのことなので、ユーザー登録に本人確認が必要なサービスはセキュリティ的にも利便性的にもぜひ対応した方がいいとのことでした。タイミーでも登録時に本人確認を行っているので導入できたら面白そうだなと思っています。 ただし、現時点ではAppleへの利用申請が必要なことに加えて金融など特定のカテゴリーに分類されるサービスでしか利用が許可されていないそうなので注意が必要です。 これからのデジタル社会に向けてキャッチアップが必要そうな内容が解説されていて非常に勉強になりました。 ちなみにスピーカーの方は日本在住でカナダの仕事をしているそうです。僕はカナダへのあこがれがあるので職の探し方などもとても興味があります。 参考 https://www.soumu.go.jp/main_content/000779585.pdf https://www.jssec.org/column/20231127.html 最後に この三日間を通して技術的な知見を深めたり、久しい友人に会って話をすることができ、すごく有意義な時間を過ごすことができました。この場を用意してくださったiOSDCスタッフの方々、参加者のみなさん本当にありがとうございました! 上記で紹介したセッション以外にも非常に興味深いセッションが多くありました。 記事にある内容や、その他の内容についても、もしタイミーのエンジニアと話したいという方がいらっしゃればぜひお気軽にお話ししましょう! product-recruit.timee.co.jp
こんにちは、タイミーDevRelの河又です。 タイミーはDroidKaigi 2024にゴールドスポンサーとして協賛しています。 当日はブースも出展しておりますので是非、お立ち寄りください。 今回はDroidKaigiを前に一度、タイミーのAndroid開発を数字で振り返ろうという企画です。 Androidエンジニアの中川をインタビューアーとしてAndroid領域のリードエンジニアである村田にタイミーのAndroid開発についてインタビューする形式でお届けします! タイミーのAndroidアプリのクラッシュフリーレートについて 2022年 2024年現在 ※グラフ上の7日間、30日間の数値は当該期間全体の数値ではなく、デイリーの移動平均の数値です 中川: 僕が入社したのが今年の1月なんですが、その時点でクラッシュフリーレートは高いな、という印象がありました。 タイミーでは2年前の段階から99.9付近で安定していますよね。僕は前職でライブ配信アプリをやっていたので一概に比較出来るものではないですが、このレベルの数字は中々見たことがありませんでした。 この2年間で更に改善されていて、クラッシュに対して高いプライオリティを持っていると感じることも多いのですが、改めて取り組みとか方針みたいなところを聞かせてください。 村田: まず、段階的なリリースプロセスを採用しています。新しいバージョンをリリースする際、最初は全体の30%のユーザーにのみ提供します。その後、半日から1営業日ほどの観察期間を設け、Crashlyticsを用いて慎重にモニタリングを行って問題がないことを確認できた場合にのみ、全ユーザーへのリリースを行います。 次に、静的解析ツールを積極的に活用しています。今はCodeClimateを使用していますが、現在はより柔軟なカスタマイズが可能なDetektなどのツールの導入を検討しています。これらのツールを使用することで、人間のレビューでは見逃しやすい細かな問題も検出することができます。 そして定期的にメトリクスを眺める同期会を開催し、2桁以上のクラッシュがあるものは細かくIssue化し解決に取り組んでいます。 また、コードスタイルの統一にも注力しています。ktlintを使用して、コードスタイルの一貫性を自動的に担保しています。チーム全体でのコードの可読性が向上し、バグの早期発見にもつながっています。 中川: こうやって聞いてみると結構色々取り組んでいますよね。 あと、無闇に新しい技術をすぐに取り入れる、っていう文化ではないですよね。ある程度、実績や信頼性を重視していると感じます。 村田: そうですね。そこはバランス感覚を重視してます。 どっちに偏り過ぎても良くないと思っていて、DroidKaigiアプリがやっている様な最先端なスタイルを今のタイミーのプロダクト規模で積極的に取り入れるのは難しいと思っていますが、とはいえ新しいものを取り入れていかないのはメンバーのモチベーション観点や学習サイクルの観点で言っても良くないと思っています。 例えばJetpack Composeとかも比較的、早めに取り組み自体は開始しました。それこそ中川さんにリードして貰いましたが、Kotlin2.0へのバージョンアップとかも早めに対応出来た方なのではないでしょうか? ただ、本当に新しいものに飛びつく、みたいなカルチャーではないのはその通りで代表的なところで言うとCoroutineですよね。 Composeといえば、CoroutineみたいなところがあるのでCompose化に伴って導入を開始しましたが、それまではRxがメインだったので一旦、そこは慎重に立ち止まったりしながらメリット・デメリット考えて技術選定に向き合いましたね。 中川: 前職とかだと、Coroutine最高だからRx剥がそう! みたいな動きが強かったのでそれはそれで面白かったのですが、同じパラダイムがアプリ開発してる中でも結構存在していて、認知負荷とか学習コストは高くなった部分があるので確かにメリット・デメリットの比較は大事だなと感じます。 今のタイミーだとRepository層から返すのがRx、それ以降がFlowという形が出来ているので迷うことが少なくて良いなと思います。 村田: 併存はしているものの、しっかり境界が分かれているので分かりやすいですよね。 慎重に検討して、今の境界が出来上がっているのでそこはしっかりと検討して良かったと思います。 既存のView systemがRxにしっかり紐付いているので、Composeの導入は慎重に進めないといけないのですが、既存のView systemがなくなってJetpack Composeの割合が100に近づいたらRepository層以降も全部Coroutineに置き換えても良いのかな、とは思っています。 人が増えても品質を保つために 中川: そもそもCodeClimateってどういった経緯で導入したんですか? 村田: 有難いことに採用が上手く進んで、Android開発メンバーが3人から6人なるということが見えた時にそれに備えて導入しました。 3人体制で、開発プロセスも非常にシンプルでした。CIの警告チェックや、品質指標を追跡するような仕組みはほとんどなく、主に手動のテストとコードレビューに頼っていました。6人にチームが倍増することが決まり、従来の手法では立ち行かなくなると感じました。 まず、コーディングの方針を明確化しました。それまでは特に明文化された方針がなかったので 「こういう方針で書いていきましょう」という指針を作りました。その上で、コードレビューやテスト以外に何ができるかを考え、仕組みで品質を保証したいという思いから、静的解析に着目しました。 ただ、3人という限られたリソースでは、独自の静的解析ツールを開発するのは難しいと判断し、既存の静的解析サービスであるCodeClimateを導入することにしました。 中川: 視座が高い、というか、しっかり長期的な目線を持ってそういった取り組みをしていること自体がめちゃくちゃ偉いな、と思いました。 村田: ありがとうございます(笑) ただ、やってみないと分からなかったことも多かったですね。 良かった点からいくと技術的負債の見える化には非常に役立ちました。特に、行数や技術的負債に関するレポートを自動生成し、チーム全体で共有できた点は良かったです。また、プルリクエストに対して自動的にコメントを付けてくれる機能も便利でした。「このメソッドが長すぎます」などの指摘を機械的に行ってくれるのですが、機械的なルールに基づく指摘なので受け取る方も素直に受け取りやすい。 一方でアプリ全体で一つのルールセットしか作れないという制約があり、機能ごとに異なるニーズに応じたルール設定が出来ませんでした。なので、ある部分ではルールが緩すぎる一方、別の部分では厳しすぎるといった問題が発生しました。例えば、1メソッドの行数は25行に制限されていました。Android開発としては厳しいので80行に緩和しました。他にもファイル全体の行数も250行まで、といった設定があったりなど……。 これでは厳しいということで現在CodeClimateは主に技術的負債の指標取得にしか使用していない状況です。なので、現在Detektの検討を進めており、もう少し各所のニーズにあった柔軟な設定が出来ることを期待しています。 開発のスピードが上がる中で品質を追求する 中川: LOCは順調に増えている中で負債は減って、クラッシュフリーレートも上がっているのは単純に偉業だなと感じるのですが何か心がけていることはありますか? 村田: そうですね。それこそ、3人体制の頃は今ほど自動化された仕組みがなかったため、逆に常に全員が品質を意識しながらという所に尽きるのですが例えば、コードレビューの際は単にバグを見つけるだけでなく、「自分が次にこのコードを触るときにスムーズに作業できるか」という視点で見たりしていましたね。他人が書いたものでも自分がオーナーシップを持ってたらどうするか、みたいな意識はあったと思います。 今だとコーディングとかComposeのガイドラインもあって、意識レベルではなく仕組みレベルで整ってきたなと感じています。 中川: 自動化、というところを除いても、以前からリリースフローみたいなのは結構洗練されてるなとも感じます。 村田: 3人時代でも一部はしっかり自動化されてはいました。例えば、アプリのバイナリを作ってそれをアップロードするところとか、クリティカルな部分は手動にならない様にみたいなことはその時代からも意識はしてましたね。やっぱり違う間違ったブランチのバイナリあげちゃったみたいなことが発生してしまうと大きい問題なのでそういったものは起こらない様に気をつけてました。 やっぱり3人でやってた頃はリソースが限定されていたので取捨選択して重要なものだけしっかりと取り組む意識は強かったのかなと思いますね。 タイミーは社会インフラとなるプロダクトになっていくことを掲げていますし、エンジニアも各々がしっかりとそれを意識してくれているので品質に対する意識は皆、高く持ってくれているなと感じます。 まとめ インタビュー、如何でしたでしょうか? タイミーのAndroid開発チームは、急速な成長と変化の中で、高い品質基準を維持しながら開発速度を向上させてきました。クラッシュフリーレートの改善、新技術の慎重な導入、そしてチーム拡大に伴う品質管理の進化など、多くの課題に取り組んできました。 今後は、Compose化やCoroutineへの移行などにも取り組みつつ、様々な部分で自動化をより推し進め、効率的で品質の高い開発プロセスを目指していきます! DroidKaigi 2024ではタイミーのAndroidエンジニアも数多く参加しますので是非、現地来場される方はタイミーのエンジニアとお話頂ければ幸いです! 皆さんとお話出来ることを楽しみにしております!
こんにちは! Agile Practice Teamでプロセス改善やアジャイルコーチとしてチームの支援を担当しています、吉野です。 2024年4月にタイミーに入社後、初めてオフラインにて社内のOST(オープンスペーステクノロジー)イベントを体験してきましたので、レポートします。 今回お話しする内容 どんなイベントを行ったのか? タイミーでのOSTはどんな雰囲気で開催されたのか? を、お話ししていきます! どんなイベントを行ったのか? 今回開催されたイベントの概要 OST(オープン・スペース・テクノロジー) 形式でのディスカッション 会議室をレンタルしてのオフサイト開催 14:00~15:30の1.5h という形式でのOSTが開催されました。 参加者について タイミーのプロダクト開発組織に関わっている方の中から参加希望者を募っての開催でした。先陣を切って動かれていたrazさん、 りっきーさんの一声で、20人近くの人が一気に集まりました!(しかも3日ぐらいで!すごい!!) 結構OSTとか慣れているのかな?と思いながら当日参加したところ、なんと今までOSTを経験したことのある参加者は1/3ほどでした。 OSTの様子 私はまだ社内のオフサイトイベントに参加していなかったので、応募時点ではみんなのモチベーションがどれぐらい高いのかわからずの参加でした。 未体験のイベントにオフサイトで参加することには、一定のハードルがあると思っていましたが、一気に参加者が集まり「みんなのイベント参加へのオープンさ」を感じることができました! イベント参加や勉強へ前向きな環境は、その場にいてめちゃくちゃテンションが上がります! タイミーでのOSTはどんな雰囲気で開催されたのか? どんなOSTだったのか? 枠としては、15分区切りの4枠にて開催されました。 お題がめっちゃ出てきた! OSTの原則として、 💡 OSTの原則の一部 いつ始まろうと、始まったときが適切なときである いつ終わろうと、終わったときが終わりのときである があるので、1枠の長さはそこまで気にしなくても良さそうと思いつつ、15分という時間は自分が経験してきた中で一番短い時間だったので、どうなるのかな?忙しくならないかな?と少し心配でした。 ですが、そのような心配は杞憂に終わり、15分の中でみんなポイントを絞って議論したり、時間が足りなかったら自分たちでテーブルや場を用意して議論を継続したりしていました。 全体の時間としても1.5h(マーケットプレイスを含めず)という短時間での開催でした。 そんな中、各々が自主的に話したいことを話して時間を最大限に活用したOSTらしいOSTだったと思います。 (運営されていたお二人も、ひたすら運営に集中、というわけではなく話し合いにも参加して自身でも楽しむスタイルで立ち回られていました!) どんな雰囲気だったのか? タイミーでは、プロダクト開発に関わる多くの方がフルリモートでお仕事をしています。 そのため、今回オフサイトで集まった直後は「ワイワイ!ガヤガヤ!」というわけではなく、少し緊張している空気を感じました。 しかし、オフサイトイベントへ自主的に参加されていることもあり、いざOSTが始まると自己紹介や自身が向き合っているお仕事の紹介など、積極的にコミニュケーションを取りにいっていました。 最終的には、お互いの悩みに共感する声や、笑い声が多く飛び交い、一体感を感じられるイベントになっていたと思います。 「初めまして」とか、「社内イベントへの参加が初めてなんです」という声も多かった中、2hもしないうちに熱量の高い場になっており、人見知りな私も最後はすごく楽しませて頂きました!笑 今後のイベントにも期待をしていきたいプロダクト開発組織 私は社外のオフサイトで開催されるコミュニティイベントへの参加が好きで、よく参加しています。 社内でも、何かイベントが開催できないかな?もしくは開かれたら参加できないかな?と考えていたところ、今回のOSTイベントへ参加しました。 初めての参加でしたが、熱い議論をしたり、同じ組織の人との繋がりができたりと、とても有意義な時間を過ごすことができました。 普段からリモートでお仕事しているからこそ、オフラインで集まれた時はコミュニケーションを重視する時間に対しての熱量は高くなるのかなーと思いました! 今後も、社内のオフサイトイベントが立ち上がれば積極的に参加していきたいですし、自分でも何か開催してみたいと思います! あわせて、主催であるりっきーさん、razさんのご感想です! OSTにかける思い スクラムマスターをやっているりっきーです。 弊社はフルリモートの環境のため、会議の集中力を阻害する要因(Slackの通知だったり、会議とは関係ないことを勧めたり)が多いと感じています。そこで、どのような設計であれば参加者が集中できるかを模索した結果、OSTに辿り着きました。 OSTが優れている点は「自分自身で興味あるテーマを公募する / 自分自身で興味あるテーマにサインアップする」に集約されています。誰かに呼ばれて会議に参加するのではなく、”自分自身”でアクションを起こさなければ会議がうまくいかないので、参加者としても集中して会議に参加できる環境になると考えています。 今回は最もやりやすい環境であることと、全社のイベントで出社する機会があったのでオフラインで開催しましたが、オンライン環境ではより効果が発揮できると思うので、今後は回数を増やしていければと思います。 初参加でもあり、初運営のOST、うまくいってよかった どうもスクラムマスターやったりエンジニアやったりしてるrazです。今はエンジニアをしております。思いつきでやってみたいと言ったところ、2人や参加者の協力のもとOSTを開催できました。僕の記憶してる範囲では社内で実施するのは初めてだったと思います。 OST開催の動機 今回、全社のイベントがありオフサイトで集まる機会がありました。その会までの時間の使い方として提案させてもらいました。普段フルリモートだったり、違うチームだったりであまり会話する機会のない人も参加してくださったのはとても嬉しかったです。 なぜOSTなのか?の理由は3つあります。 実は自分が未経験で興味があった 毎週開催してるアジャイル相談会の様子から、普段話せてないことが色々ありそうだった 雑に大人数集めて開催してもなんとかなりそうだから 1と3はあまり説明することもないので省略しますが、2のアジャイル相談会について補足します。 アジャイル相談会とは、毎週水曜の夕方ごろにりっきーさんが主催してくれてるその名の通りアジャイルとかスクラムに関して気軽に相談していい会です。ただ、実際にはアジャイルとかスクラムの枠にとらわれず、組織論やマネジメントといった様々な内容で会話しています。その中でも、組織全体に関わることは、話す機会が少ないんだろうと感じていました。そこで、熱量のある人たちが集まって会話できるOSTをやれたらいいのではと思って開催しました。 開催してみた感想 一応運営ではあるので、全体の様子をみながらではありますが、極力会話に参加してテーマを盛り上げていきました。経験者が1/3程度いましたが、半数以上は未経験で何もわからない状態での参加でした。しかし、みんなの自主性の高さがあってか、初回セッションから盛り上がっていて安心しました。自分もその様子に安心できたので、2セッション目以降はより会話に集中できたと思います。途中会話に夢中になりすぎてタイムキープを忘れていたほどです笑。 みなさん初めての開催にもかかわらず適応能力が高いので、事前に出したテーマじゃないことに転換していて、会話を楽しんでいると感じました。個人的にはもう少し「どうしよ?」感に包まれるんかな?と心配してたのですが、杞憂でした笑 次回開催なるか? 今回、準備期間も会自体も短い中での開催でしたが、思ったよりも盛り上がってましたし、個人的には成功したんじゃないかと思ってます。いい成功体験になったと思うので、次回開催に向けて思っていること3つあげて、僕の感想を終わりにしようと思います。 フルリモートへの適応 多種多様な役職や職種の人の参加 組織を変革していくアクションを生み出せる会へ フルリモートへの適応 : タイミーのプロダクト開発組織は、フルリモートなのでリモートでも開催してみてもいいかなとは思いました。ただ、実際に運営してみてオフサイト環境だからこそ成り立っているものがありそうとも感じました。四半期に1度くらいのペースで、オンラインとオフラインを交互で開催できても面白そうです。 多種多様な役職や職種の人の参加 : 急な呼びかけなのもあって参加できたメンバーは限られていました。組織に存在している様々な役職や職種の人が充足できてない状態でしたので、次回はより多種多様な人に参加してもらえるような会を開催できたらなと思います。 組織を変革していくアクションを生み出せる会へ : 会話メインで会は終わりました。しかし、もう少し時間を長くとれれば、会話も深まりますし、具体的なアクションを生み出したりして、組織やチームに変化をもたらす会にもしていけると思うので、そういう会を目指せればと思います! 以上レポートでした! ハピネスドアもハッピー感想めっちゃ多かったです!
こんにちは、タイミーのデータアナリティクス部でデータアナリストをしている夏目です。普段は主にタイミーのプロダクトに関する分析業務に従事しています。 本日はタイミーにおいて、効果検証設計を施策前に正しく行える仕組みづくりと効果検証設計・結果を一元的に管理できるデータベースについてご紹介します。 解決したかった課題 タイミーでは、プロダクト、マーケティング、営業組織などで様々な施策が行われています。しかしながら、それらの施策の結果を判断する効果検証には課題も多く存在しています。今回は以下の2つの課題にフォーカスしてブログを書きます。 効果検証設計が事前になされていない施策があった 効果検証設計や検証結果がバラバラに保管され、会社として知見が溜まっていなかった まず1つ目の「効果検証設計が事前になされていない施策があった」に関してです。タイミーではアナリストの数も限られており、事前に全ての施策に目を通すことは難しいです。施策によっては事前に効果検証設計がされておらず、必要なログが取れていなかったり、検証に必要なサンプルサイズが担保されていなかったりと、正確な効果検証ができないケースが存在しました。 次に2つ目の「効果検証設計や検証結果がバラバラに保管され、全体として知見が溜まっていなかった」に関してです。タイミーでは様々なチームが施策を行っています。基本的に効果検証の結果はチームごとに管理されており、別のチームの人がその結果を探すことが難しいケースもありました。 取ったアプローチ 以上の2つの課題を解決するために、行ったことは主に以下の3つです。それぞれをこの項では説明していきます。 各チームが効果検証の設計と結果を記入できるNotion上のデータベースを作成 効果検証設計と結果を記入するテンプレートを作成 他アナリストや他チームへの説明の実施 1. 各チームが効果検証設計・結果を記入できるNotion上のデータベースを作成 1つ目のデータベースの作成に関しては、正確にはあるチームがすでに使用しているデータベースを少し改変し別チームにも展開しました。 イメージとしては、ダミーですが以下の画像で、行の一つ一つが効果検証設計と結果をまとめるドキュメントとなっています。チーム横断で1つのデータベースにまとめることにより、別チームの検証結果や検証方法を簡単に参照できるようになっています。 ダミーデータベース 2. 効果検証設計・結果を記入するテンプレートを作成 次は2つ目のテンプレートに関してです。データベースから効果検証ドキュメントを作る際に利用するテンプレートを作成しました。 テンプレートには、大きく効果検証設計と検証結果を書くパートの2つを用意しています。以下の画像は効果検証設計パートのテンプレートの一部です。 比較の手法には、A/Bテスト、DID、目標値との比較といった手法が入ることを想定しています。最後のScenarioは、設定したMetricsの動きによって施策担当チームの次のアクションがどう変わるのかを記入します。 このScenarioを事前に書くことによって、どのようなMetricを見るべきかが明らかになり、またそれらのMetricを計測するための手段が逆算されるはずです。 テンプレートの一部 3. 他アナリストや他チームへの説明の実施 最後に「他アナリストや他チームへの説明の実施」に関してです。作ったデータベースやテンプレートを展開するため、他のアナリストや、マーケティング担当の部署などに資料を作って説明を行いました。概ね好評で受け入れられるまでのハードルは少なかったです。 結果 データベースを作って3ヶ月ほど経ちました。現状約10個ほどの施策チームがこのデータベース・テンプレートを利用して効果検証の設計を行っています。またアナリストからも、効果検証の設計をPdMとやりやすくなったといった声をもらっています。 残課題 残課題は2つほど明確なものがあると思っています。 1つ目は、効果検証設計のテンプレートの不十分さです。現状は受け入れやすさを重視し、意図的に効果検証のテンプレートをシンプルにしています。しかしながら、A/Bテストなどでは他にも設定をしないといけない項目はまだまだあるはずです。 2つ目は、検証結果を横断したメタ分析ができる体制になっていないことです。検証結果をチーム横断でまとめているので、過去どういった施策が当たりやすかったのかといったメタ的な分析もやりやすくなるはずです。しかしながら現状こういった分析に耐えうる設計はデータベースに表現されていません。 最後に 今回は、タイミーにおける効果検証設計に関して記載しました。弊社では分析自体だけではなく、今回のような分析をより活用するための仕組みづくりも沢山行っております。 We’re Hiring! タイミーでは、一緒に働くメンバーを募集しています。 https://hrmos.co/pages/timee/jobs カジュアル面談 も行っていますので、少しでも興味がありましたら、気軽にご連絡ください。 Reference A/Bテスト実践ガイド 真のデータドリブンへ至る信用できる実験とは
タイミーでバックエンドのテックリードをしている新谷( @euglena1215 )です。 この記事は先日公開した「前編:YARD から rbs-inline に移行しました」の後編となっています。前編では rbs-inline の紹介、移行の目的などをまとめています。前編を読んでいない方はぜひ読んでみてください。 tech.timee.co.jp 後編では実際の移行の流れや詰まったポイント、今後の展望について紹介します。 移行の流れ 1. 型をやっていくことを表明する 2. rbs-inline のセットアップを行う 3. YARD から rbs-inline への移行を進める 4. 後片付け sord gem の削除 rbs subtract をやめる 今後の展望 型検査を通す リアルタイムな実装へのフィードバック まとめ 移行の流れ YARD が日常的に書かれている状況から YARD がほとんど rbs-inline になり、YARD 関連のツールが削除されるまでの流れを紹介します。 1. 型をやっていくことを表明する まずはバックエンド開発者に対してやっていく気持ちを表明しました。 YARD から rbs-inline への移行は自分1人で進めるよりは誰かに手伝ってもらったほうが自分ごとに感じられる方が増えると思い、表明と同時に手伝ってもらえる方を募集しました。 ここで、 @Juju_62q @dak2 の2名に立候補いただきました。ありがたい。 こんな感じで表明しました 2. rbs-inline のセットアップを行う 移行を段階的に進めるためにも、まずは rbs-inlne コメントを書いたらきちんと反映されるような状況を作ります。 タイミーでの型生成は pocke さんが作った便利 Rake Task rbs:setup をアレンジして使っています。そのため、 rbs:setup を実行することで rbs-inline による型定義も生成されるように変更しました。 また、タイミーでは sord gem という YARD コメントから RBS を生成するツールも使っています。rbs-inline はアノテーションがないメソッドにも RBS を生成するため、ただ生成しただけでは RBS が重複してしまいエラーになってしまいます。且つ、rbs-inline コメントが少なく YARD コメントが多い現段階においては YARD コメントによる RBS は捨てずに互換性を維持する必要がありました。 これらは sord gem の --exclude-untyped オプションを設定しつつ、 rbs subtract コマンドによって YARD → rbs-inline の優先順位で重複を削除することで解決しました。 --exclude-untyped オプションは名前の通り、YARD コメントがなく untyped にせざるを得ない RBS を生成結果から除外できます。ですが、 --exclude-untyped オプションもユースケースに完全に一致するものではなく、YARD コメントがない定数や initialize メソッドが untyped として生成されるので rbs-inline での記述が反映されない形になっていました。 定数が untyped になる問題は最終的に sord gem を削除するまでは解決しませんでしたが、YARD コメントがない initialize メソッドも生成されてしまう問題は以下のように生成されたファイルの中身を書き換えることで生成結果から強引に除外する対応を行いました。 task sord : :environment do sord_path = ' sig/sord/generated.rbs ' sh ' sord ' , ' --exclude-untyped ' , ' --rbs ' , sord_path Rails .root.join(sord_path).then do |f| content = f.read # sord は --exclude-untyped をつけていても initialize メソッドの型を出力するが、 # rbs-inline を優先したいので削除する。 content.gsub!( / def initialize: .*?(\n\s+.*?)* -> void / , '' ) f.write(content) end end また、rbs-inline をセットアップした時点の rbs-inline 0.4.0 では ActiveAdmin 用のいくつかの実装にて rbs-inline コマンドがクラッシュする事象を観測していたので、rbs-inline の変換対象から除外していました。この辺りを soutaro さんにフィードバックしたところ、0.5.0 に入った この変更 で修正してもらえました 🎉 社内でバグ報告できて便利 結果として、タイミーの RBS 生成ステップは以下のように変化しました。 Before rbs prototype で untyped ながらも全体の型を生成 rbs_rails:all で Rails によって生成されたメソッドの型を生成 sord で YARD から型を生成 rbs subtract で 1. で生成された型に対して重複した型定義を除外 After rbs-inline で全体の型を生成 rbs_rails:all で Rails によって生成されたメソッドの型を生成 sord で YARD から型を生成 rbs subtract で 1. で生成された型に対して重複した型定義を除外 rbs-inline が全体のRBSファイルを生成をしてくれるので、 rbs prototype によって型定義を生成するステップを削除しました。そのため、実質的に rbs prototype の上位互換として扱っています。rbs-inline に興味があって rbs prototype コマンドを使っているプロジェクトがあれば、とりあえず rbs prototype コマンドを rbs-inline コマンドに置き換えてみても良いのではないでしょうか。 また、このタイミングで開発ルールにも変更を加えています。 「新規で型アノテーションするときは YARD よりも rbs-inline を使うことを推奨する」ルールを追加しました。この時点ではコードベース上に rbs-inline によるアノテーションはほとんどなく、サンプルコードが少ないので義務ではなく推奨という形に留めています。(学んでみてほしくはありつつも、書き方分からないので rbs-inline ではなく YARD を書くことは許容する形) 3. YARD から rbs-inline への移行を進める 2.でrbs-inline 書き始められる状況を作れたので、ようやく YARD から rbs-inline への移行を進めていきます。 1.で手伝ってくれると立候補してもらった2名と一緒に方針を以下のように決めました。 「機械的に変換できる部分は機械的に変換していくが、自動変換を頑張りすぎない。手動で手直しした方が早い部分は手動で書き換える。コスパの良い方法を適宜選んで置き換えを進めていく」 また、変換スクリプトはメソッドに対するコメントを完全に変換可能なもののみ変換するという方針を取りました。 例えば以下のような YARD コメントが書かれたメソッドがあったとして # @param x [String] # @return [String] def foo (x) x end YARD コメントの @return タグのみ変換できるスクリプトがあったとすると以下のように変換されます。 # @param x [String] # @rbs return: String def foo (x) x end この状態だと、YARD コメントから生成された RBS は (String) -> untyped になり、rbs-inline コメントから生成された RBS は (untyped) -> String になります。 今の rbs-inline コメントによる RBS の生成結果と YARD コメントによる RBS の生成結果だと YARD コメントが優先されるため、結果として (String) -> untyped が最終的な型になります。 元々 YARD コメントだけで記述していた際は (String) -> String と正しい型が定義できていたのに、YARD と rbs-inline が混在することで型情報が落ちてしまうことは意図したものではないため、完全に変換できるもののみ変換対象としました。 前述した方針より、YARD タグの全てをサポートしているわけではないためライブラリとしての公開は控えたいと思いますが、興味ある方向けにソースコードは公開します。興味があればご覧ください。 yard-rbs-inline-sample/tasks/yard_to_rbs_inline.rake at main · euglena1215/yard-rbs-inline-sample · GitHub また、実行例として YARD アノテーションが多く記述されている yard gem に対して変換スクリプトを実行した結果も載せておきます。 github.com コードを書かずにエディタの一括置換機能を使って移行したものもいくつか存在します。 yard-sig の記法 @!sig を @rbs に置換 @example に対応する rbs-inline はないので NOTE: に置換 @see に対応する rbs-inline はないので refs に置換 @raise に対応する rbs-inline はないので Raises に置換 @deprecated に対応する rbs-inline はないので Deprecated に置換 数が少なく手動で手直しした例も挙げておきます。 YARD のタプル(e.g. Array(String, Integer) )を使っている箇所を rbs-inline に修正 YARD の @option タグを rbs-inline で Hash のリテラルに修正 # @param [Hash] h # @option h arg1 [String] # @option h arg2 [Integer] # @return [Integer] def foo (h) = h.size ↓ # @rbs h: { arg1: String, arg2: Integer } # @rbs return: Integer def foo (h) = h.size これらの作業でコードベースからほとんどの YARD が rbs-inline のコメントに移行が完了しました 🎉 移行期間としてはサブタスクとして取り組んで1ヶ月半ほどでした。このタスクに集中すれば1~2週間で終わったのではないかと思います。 4. 後片付け YARD コメントがほとんどなくなったので YARD 関連のツールを削除を始めとする後片付けを進めていきました。基本的にはスムーズに進んだのですが、進めていく中でハードルとなった点をいくつか紹介します。 sord gem の削除 YARD コメントから RBS を生成する sord gem を削除しようとしたところ、Data, Struct の型定義が見つからなくてエラーが発生しました。 sord が Data.define 、 Struct.new に対応する型定義を生成していたのに対し、rbs-inline は生成しておらず、sord 削除のタイミングでその問題が健在化しました。 対応としては、以下のように @rbs! を使って直接 RBS をコードベース上に手書きしました。 # @rbs! # class Foo # attr_reader bar: String # attr_reader baz: Integer # end # @rbs skip Foo = Data .define( :bar , :baz ) さすがにこれはちょっと辛いですという話を soutaro さんにしたところ、rbs-inline 0.6.0 で Data, Struct がサポートされました 🎉 github.com Data, Struct が面倒なことをsoutaroさんに共有している図 Data, Struct がサポートされたことによって @rbs! を使う必要がなくなり、以下のようにシュッと書けるようになりました。めっちゃ便利…! Foo = Data .define( :bar , #: String :baz #: Integer ) rbs subtract をやめる sord gem が削除されたことでアプリケーションコードから RBS を自動生成する方法が rbs-rails と rbs-inline のみになりました。rbs-rails は Rails 側が自動的に生成するメソッドに対して RBS 生成を行うのが目的で、rbs-inline は開発者が実装したメソッドに対して RBS 生成するのが目的です。 それぞれ RBS の生成対象が異なることから重複を吸収する必要はないだろうと考え、 rbs subtract をやめました。 rbs subtract をやめてみたところ、以下のコードでエラーが出るようになりました。 class User < ApplicationRecord has_one :profile after_create :create_profile private # create_profile メソッドの型定義が重複しているエラーが発生 def create_profile ... end end ActiveRecord はアソシエーションで関連付けたモデルに対して create_xxx メソッドを動的に定義します。動的に定義されたメソッドとアプリケーションコードで定義したメソッドによる型定義が重複したことによるエラーでした。今回のケースでは意図的にメソッドの上書きをしていたわけではなかったため、本来は別名をつけることが望ましいパターンでした。 rbs subtract をやめたことで、意図せずメソッドを上書きしていた場合はエラーによって別名で定義できるようになりますし、意図的にメソッドを上書きしていた場合は @rbs override を記載することでその意図をコード上に残せるようになります。 必要なくなったから rbs subtract をやめようくらいの気持ちで消していましたが、これは思いがけない発見でした。 さらに細かい話になりますが、上記の重複エラーの参照先が rbs-inline ではなく rbs-rails 側になっていることに気付きました。なんでだろうと思って調べてみると、rbs-inline は sig/rbs_inline/ に格納していて rbs-rails は sig/rbs_rails/ に格納していたのですが、RBS はアルファベット順でファイルを読み込んでいくため sig/rbs_inline/ → sig/rbs_rails/ という順番に RBS を読み込んでいたことに起因するものでした。 なので、rbs-inline は sig/rbs_inline ディレクトリではなく、sig/z_rbs_inline ディレクトリに格納するように変更し、必ず rbs-rails の後に読み込まれるようにしました。 これらの取り組みによって、最終的に RBS 生成のステップが以下のようにシンプルになりました。 Before rbs-inline で全体の型を生成 rbs_rails:all で Rails によって生成されたメソッドの型を生成 sord で YARD から型を生成 rbs subtract で 1. で生成された型に対して重複した型定義を除外 After rbs-inline で全体の型を生成 rbs_rails:all で Rails によって生成されたメソッドの型を生成 今後の展望 やりたいと思っているものの、やりきれていないいくつかの点について共有します。 型検査を通す 今回の取り組みで rbs-inline を書いていく土壌は整いましたし、実際に書かれるようになってきましたが、型検査(steep check)を通すところまでは進められていません。これから始まる長い旅のスタート地点に立ったかなという気持ちです。 また、Rails プロジェクトに対して全てのディレクトリに対して型検査を通すようにすべきなのか、それとも特定のディレクトリだけで実施するのが妥当なのかの整理はできていません。これから検証含め進めていく必要があります。 リアルタイムな実装へのフィードバック 前編「RBS 活用推進の背景」で説明したように、実装のフィードバックサイクルを早めるためには rbs-inline のコメントを更新したらリアルタイムに RBS に反映され、その RBS を元にした型検査がエディタ上ですぐに走るのが理想だと考えています。 上記の型検査を通すだけでは理想の状況は実現できません。コーディング環境のセットアップを含む包括的な開発環境の提供を推進していく必要があります。 まとめ RBS 導入の背景から YARD から rbs-inline への移行理由、移行方法、これからの展望について紹介しました。rbs-inline は experimental ではあるものの本番運用している会社がある事実があなたの会社 rbs-inline 導入への後押しになれば幸いです。 この辺りについてもっと話したい方はカジュアル面談でお話ししましょう! product-recruit.timee.co.jp
タイミーでバックエンドのテックリードをしている新谷( @euglena1215 )です。 タイミーのバックエンドはモノリスの Rails を中心に構成されています。そのモノリスな Rails に書かれていた YARD を rbs-inline に一通り移行した事例を紹介します。 前編では、rbs-inline の紹介と rbs-inline への移行理由について触れ、後編では実際の移行の流れや詰まったポイント、今後の展望について触れる予定です。 rbs-inline とは RBS 活用推進の背景 移行理由 1. YARD(sord) よりも rbs-inline の方が表現力が高い 2. YARD は書いていたが yardoc は使っていなかった 3. rbs-inline が今後言語標準の機能になっていく rbs-inline とは まずは rbs-inline について簡単に紹介します。 rbs-inline とは Ruby コードにコメントの形式で RBS を記述することで、対応する RBS ファイルを自動生成してくれるツールです。 github.com README にあるサンプルコードを引用するだけになってしまいますが、以下の Ruby コードに対して rbs-inline コマンドを実行すると # rbs_inline: enabled class Person attr_reader :name #: String attr_reader :addresses #: Array[String] # @rbs name: String # @rbs addresses: Array[String] # @rbs return: void def initialize ( name :, addresses :) @name = name @addresses = addresses end def to_s #: String " Person(name = #{ name } , addresses = #{ addresses.join( " , " ) } ) " end # @rbs &block: (String) -> void def each_address (&block) #:: void addresses.each(&block) end end 以下の RBS ファイルが生成されるようになっています。 class Person attr_reader name: String attr_reader addresses: Array [ String ] def initialize : (name: String , addresses: Array [ String ]) -> void def to_s : () -> String def each_address : () { ( String ) -> void } -> void end サポートしている構文はこちらにまとまっています。 Syntax guide · soutaro/rbs-inline Wiki · GitHub rbs-inline が作られた動機に関しては RubyKaigi 2024 での発表スライドを見てもらうのが一番かなと思います。 speakerdeck.com RBS 活用推進の背景 まずは rbs-inline の前段階である RBS 活用推進の背景について紹介します。 メルカリがメルカリハロをリリースし、リクルートもスポットワーク業界への参入を表明するなどタイミーを取り巻く環境は激化の一途をたどっています。まさに戦国時代です。競合サービスと切磋琢磨し勝ち抜いていくために我々は1段階ギアを上げた開発をしていく必要があります。 そして、開発速度を高める方法はいくつかありますが、その中でも実装のフィードバックサイクルの高速化は良いアイデアの1つだと考えています。 タイミーを含む一般的な Rails アプリケーションの開発では、実装の検証はテストコードを用いた自動テストもしくは開発・検証環境での手動テストがほとんどなのではないかと思います。手動テストに一定の時間がかかるのは当然として、自動テストもそこそこのサイズの Rails アプリケーションでは数秒かかることは少なくありません。 それが仮にエディタ上でリアルタイムに静的な型検査という形でフィードバックが返ってくるとなるとどうでしょうか。もちろん手動・自動テストほど詳細なロジックミスは検知できませんが、多くのミスはちょっとした NoMethodError など型検査で気付けるものが大半です。 これまで数秒かけて気付いていたことにリアルタイムで気付けるようになれば、開発速度の改善には間違いなく寄与するはずです。 また、前提としてタイミーは元々 YARD コメントを書く文化があり、YARD コメントから RBS を生成する sord gem を使って RBS を補完用途で導入していました。詳しくは以下の資料をご覧ください。 tech.timee.co.jp 移行理由 RBS を活用し、型検査をしていくぞ!というのは前述の通りです。 一方、sord gem を使うことで YARD から RBS の生成はできていました。それでも rbs-inline に移行した理由は以下の通りです。 1. YARD(sord) よりも rbs-inline の方が表現力が高い YARD(sord) では interface や type などの表現ができません。rbs-inline では @rbs! を使えば記述できます。 # RBS class X type name = String | Symbol def foo: () -> name end # Ruby with YARD class X # (String | Symbol) は表現できるが type を使った alias は表現できない # @return [String, Symbol] def foo = [ ' foo ' , :foo ].sample end # Ruby with rbs-inline class X # @rbs! # type name = String | Symbol # @rbs () -> name def foo = [ ' foo ' , :foo ].sample end 2. YARD は書いていたが yardoc は使っていなかった これは社内の事情ですが、YARD をコードリーディングを手助けするドキュメンテーションツールとしてしか使っておらず yardoc を用いてドキュメントページの生成はしていませんでした。(正確には一時期生成していましたが、誰も見ていなかったので生成をストップしました。) 同様の YARD を使っているプロジェクトであっても、yardoc を活用しているプロジェクトは yardoc 相当の挙動を rbs-inline で実現するツールを自作するか、yardoc によるドキュメント生成を諦めるかの判断を迫られることになります。 3. rbs-inline が今後言語標準の機能になっていく rbs-inline gem は 今後廃止されて rbs gem に統合される予定 です。 rbs gem は Ruby 標準の機能なので rbs-inline も Ruby 標準の機能になるはずです。なので、今のうちに乗り換えておいて損はないだろう、という魂胆です。 また、開発者が社内にいる soutaro さんというのも大きなポイントでした。 productpr.timee.co.jp rbs-inline はまだ experimental なので仕様が不安定という側面はあるものの、逆に考えるとフィードバックをすれば受け入れてもらう可能性が高いということでもあります。標準機能になるのなら、社内にいる soutaro さんに今のうちにフィードバックしておくことで我々のユースケースで困りにくい形で仕様が確定するといいなと思っています。 *1 前編では rbs-inline の紹介、移行の目的などを紹介しました。このまま肝心の「実際どうだったのか」もお伝えしたいところですが、長くなったので一旦ここで一区切り。後編では実際の移行の流れや詰まったポイント、今後の展望をまとめます。お楽しみに! 今回は RBS 活用によって開発速度を向上させる作戦を取りましたが、開発速度向上には色んな方法があると思っています。各社がどんなアプローチで取り組まれているのかはとても興味があります。カジュアル面談でお待ちしています! product-recruit.timee.co.jp 追記:後編を公開しました。 tech.timee.co.jp *1 : 事実として我々のフィードバックによってバグ修正や新たな構文サポートが行われました。詳しくは後編で紹介します
読んで欲しいと思っている人 POやステークホルダーと品質について共通言語や目標が欲しい開発者 開発者と品質について共通言語や目標が欲しいPO スクラムで品質について困っている人 読むとわかること 完成の定義(Definition of Done)とはどんなものか スクラムと非機能的な品質の関係性 タイミーのWorkingRelationsSquadでどんな完成の定義を作り、活用していきたいと思っているか 完成の定義(Definition of Done)とは インクリメントが 常に 守るべき状態のことです。スクラムガイド 1 では以下のように説明されています。 完成の定義とは、プロダクトの品質基準を満たすインクリメントの状態を⽰した正式な記述である。 プロダクトバックログアイテムが完成の定義を満たしたときにインクリメントが誕⽣する。 つまり完成の定義を満たしていない成果物は、いかに優れた機能であってもインクリメントとなることはありません。スクラムの原理原則として、完成の定義で合意した事柄は後回しにされることはありません。 また、完成の定義の対象はインクリメント全体です。プロダクトバックログアイテムによって生まれる差分ではありません。プロダクトは常に全ての完成の定義を満たすことが要求されます。 なぜ完成の定義を作ったのか スクラムの原理原則に則るのであれば、そもそも必要だから作ります。もっと言えば、完成の定義の作成はありとあらゆるプロダクトバックログアイテムの完了よりも優先されます。完成の定義がないとプロダクトバックログアイテムが完成した状態がわからないためです。 しかし今回はそのような教科書的な話ではなく、もっと現実で発生した問題に基づいて作成理由をお伝えできればと思います。大きく3つの問題を解決したかったために作成しました。 プロダクトが抱えている課題を明らかにしたい スクラムチーム内の開発者が非機能的な品質を改善をするために発生する説明責任を緩和したい 技術改善Weekをやめたい 1つずつ解説したいと思います。 プロダクトが抱えている課題を明らかにしたい さて、この説明をするためにはタイミーの組織構造を説明する必要があります。タイミーの組織は一部Spotifyモデルを参考にした概念を採用しています。 タイミーの組織構造と名称 普段共に活動するチームのことをSquadと呼び、同じ技術領域を担当しているエンジニアの集合をChapterと呼びます。 タイミーでは基本的に開発を行なっているチームが運用も行います。一方でそうは言ってもSquadが生んだ問題をChapterが改善を試みているという場合があります。また、特定技術において生まれた新技術などはChapterの単位で取り組まれることがほとんどです。本質的にはプロダクトが開発速度向上やメンテナンス性を保つためにやらなければならないことであるのに、その管理をChapterに負わせてしまっているという状況になっています。 結果として、プロダクトの課題であるはずなのにスクラムチームの関心から外れてしまっています。これをなんとかするために今回作成した完成の定義ではこれまでChapterの関心事とされていたことも明確に記述しています。下記はその一例です。 Android Chapterの関心ごとだったもの これまではChapterというところで隠されていた課題がスクラムチームに対して透明になっています。これによって今のプロダクトはどんな課題を抱えているのかが明らかになりました。 スクラムチーム内の開発者が非機能的な品質を改善をするために発生する説明責任を緩和したい 先ほどの話につながっているのですが、Chapterは課題は扱っているもののそれを解決する人は基本的にSquadに所属しています。結果としてChapterで対応するとなった課題はSquadに持ち帰られ、POに交渉を行い着手するという状況になっています。 交渉するというのは基本的にコストです。また、POがプロダクトバックログに対して説明責任を果たせば果たすほどその並び順は開発者にとって納得のいくものとなっていきます。結果としてChapterに持ってきた課題は対応が後回しにされることがしばしばありました。 スクラムの基本的な考え方としては品質が基準に達していないインクリメントはリリースできないはずなのに、品質に対する対応が劣後していく特殊な事態となっています。 ここに対して完成の定義としてチームで合意してしまうことで果たすべき説明責任は少なくなっていき、品質の改善に対応しやすくなるのではないかと考えています。 技術改善Weekをやめたい WorkingRelationsSquadでは技術改善Weekという取り組みを実施しています。6週間に1度プロダクトバックログアイテムではなく、各々好きな技術的な課題に取り組むという時間です。 開発者が交渉を行うことなく非機能的な品質を維持するために始めた取り組みです。 改めて考えるとこれはスクラムチームの課題を覆い隠すことに一役買っています。なぜならスクラムチームが品質に一部問題があるようなリリースをしたとしても後日対応できてしまい、スクラムチームに対応しなければならないこととしてFBが蓄積していかないためです。 常に品質の高い開発をしていれば技術改善Weekは必要ないはずなのに、なぜか品質が技術改善Weekに依存する形になり、スクラムチームは問題を解決するきっかけを失っています。ただし、現時点では技術改善Weekは日々開発を続ける中で対応できない課題に対応するために存在しています。やめるためには日常的により品質の高いインクリメントを作り続ける必要があります。技術改善Weekは直ちに止める話になるわけではありませんが、通常のスプリントで品質の高いプロダクトが開発できるようになれば不要になると考えています。 技術改善Weekは本質的には リリーススプリント と大差ありません。この存在によって意味のあるインクリメントをスプリントレビューで提供し続けることができない瞬間があるというのも大きな問題だと考えています。 成果物 上記を考えながらWorkingRelationsSquadでは下記のような完成の定義を作ってみました(画像は一部抜粋)。 作成した完成の定義(一部抜粋) 現状Doneとなっているものも、Undoneとなっているものもあります。Undoneの中でも開発対象の差分だけであれば守れるものもあれば、そうでないものもあるというのが現状です。全てをDoneにできるように頑張りたいと思っています。 余談 チームメンバーの感想 完成の定義を作った時に下記のような感想がありました。 理想はありつつ、やっていないことが可視化されて厳しい気持ちにもなった これをみた時はかなり嬉しかったです。 スクラムはできていること、できていないことを全て透明にしていくフレームワークです。スクラムの取り組みによって、できていないことが明らかになったのであればそれはスクラムの考え方ではとてもポジティブで大いなる第一歩だと感じています。 こう言った感想を素直に伝えてくれるチームメンバーには感謝の気持ちでいっぱいです。 完成の定義とプラットフォームエンジニアリング 完成の定義はプラットフォームエンジニアリングをやっていく上でかなり面白いツールだと思っています。なぜならUndoneが多ければ多いほど、チームは認知負荷が高いと考えることができるためです。本来やらねばならないことが頑張らないとできないという状態ということもできます。また、複数のチームで運用して共通してUndoneなのであれば、そこにプラットフォームチームが介入する余地があるでしょう。うまくゴールデンパスを作れれば課題内在性負荷や課題外在性負荷が下がり、1つDoneが増えるのかもしれません。同じ期間のスプリントでやれることが増えたとしたら、それはプラットフォームエンジニアリングの大きな成果になると思っています。 また、場合によってはプラットフォームチームから何かを強制するためにも活用できると考えています。これはプラットフォームチームがセキュリティやコストなどの側面から何かを依頼する際に良いインターフェースになると考えています。 タイミーではそれぞれのストリームアラインドチームの認知負荷が高いという話があって2年くらい経っていますが、いろんなツールや考え方を駆使して下げていければ良いと考えています。 終わりに 僕自身としては非機能的な品質と機能開発のバランスを取る活動を続けています。 振り返ってみると、2021年の発表でも似たようなこと話をしていました。ソフトウェアエンジニアとしては常に向き合っていかなければならない課題なのかなと強く思っています。 speakerdeck.com 辿り着くまでかなり時間がかかってしまいましたが、完成の定義はそういう意味ではかなり強力なツールになると思っています。今後うまく活用できればと思っています。 追記 完成の定義の画像を定義の内容に絞った画像に更新しました。 スクラムガイド 2020年版, https://scrumguides.org/docs/scrumguide/v2020/2020-Scrum-Guide-Japanese.pdf ↩
こんにちは、タイミーでデータサイエンティストとして働いている小栗です。 今回は、機械学習バッチ予測およびA/BテストをVertex AI PipelinesとCloud Run jobsを使ってシンプルに実現した話をご紹介します。 経緯 タイミーのサービスのユーザーは2種類に大別されます。お仕事内容を掲載して働く人を募集する「事業者」と、お仕事に申し込んで働く「働き手」です。 今回、事業者を対象に機械学習を用いた予測を行い、予測結果を元にWebアプリケーション上で特定の処理を行う機能を開発することになりました。 要件としては以下を実現する必要がありました。 定期的なバッチ処理でのMLモデルの学習・予測 MLモデルのA/Bテスト 最終的に、Vertex AI PipelinesとCloud Run jobsを活用したシンプルな構成でバッチ予測とA/Bテストを実現することにしました。 本記事では主に構成とA/B割り当ての仕組みをご紹介します。 構成 まず、全体構成とその構成要素についてご紹介します。 構成図 Webアプリケーション側の構成・実装についてもご紹介したいところですが、今回は機械学習に関係する部分に絞ってお話しします。 前提として、データサイエンス(以下DS)グループは Google CloudをベースとしたML基盤 を構築しています。MLパイプライン等はCloud Composerに載せて統一的に管理しており、今回も例に漏れずワークフロー管理ツールとして採用しています。 MLパイプラインはVertex AI Pipelinesで実装しています。MLモデルのA/Bテストを実現するため、MLモデルごとにパイプラインを構築し、並行で稼働させています。同時に、それぞれのMLモデルの予測値と付随情報をBigQueryの予測結果テーブルに蓄積する責務もMLパイプラインに持たせています。 もちろんそれだけでは予測結果がテーブルに蓄積されるだけでA/Bテストは実現できないので、各事業者に対する予測結果のA/B割り当てをCloud Run jobsの責務とし、MLパイプライン実行の後段タスクとして実行しています。同時に、割り当て結果をBigQueryテーブルに出力する処理も実施します。 当初はA/B割り当てを含めたすべての責務をVertex AI Pipelinesに集約する案も議論の中で出たのですが、将来的に類似の取り組みにて実装や思想を使い回せそう等の理由から、取り回しのしやすいCloud Run jobsを採用しました。 Cloud Run jobsの利用はDSグループ内でも初めてではありましたが、グループ内のMLOpsエンジニアに相談・依頼してCloud Run jobs用CI/CDの導入などML基盤のアップデートを並行して進めていただくことで、スムーズに開発を進められました。 今回、モジュール間のデータやり取りのIFとしてBigQueryを採用していますが、読み込み・書き込みの操作に関しては、 以前ご紹介した社内ライブラリ を活用することでサクッと実装できました。 また、各処理には実行日時などの情報が必要なため、Cloud Composerのオペレータからパラメータを渡してキックする形にしています。 例えば、Cloud Run jobsは2023年のアップデートから ジョブ構成のオーバーライド が可能になっており、それに併せてCloud Composer側でも CloudRunExecuteJobOperator を介したオーバーライドが可能になったため、そちらを利用して必要なパラメータを実行時に渡しています。 さて、A/B割り当ての結果が出力されたのち、Webアプリケーション側はデータ連携用テーブルを参照して、事業者に対してバッチ処理を行います。残念ながら機能や施策の具体についてはご紹介できないのですが、機械学習の予測結果を元に事業者ごとに特定の処理を行う仕組みになっています。 A/B割り当ての仕組み 次に、Cloud Run jobsの中身で実施しているA/B割り当てについて、より具体的にご紹介します。 どう設定を管理するか A/B割り当てに必要なパラメータはyamlファイルで指定する形にしています。例えば、実験期間や各MLモデルへの割り当て割合などです。 - experiment_name : str # 実験名。割り当てに用いるキーも兼ねる e.g. 'experiment_1' start_date : str # A/Bテストの開始日 e.g. '2024-08-01' end_date : str # A/Bテストの終了日 e.g. '2024-08-31' groups : - model_name : str # MLモデルの名前 e.g. 'model_1' weight : float # このMLモデルに割り当てる割合 e.g. 0.5 - model_name : ... - experiment_name : ... ... この方法を用いる問題点として、”PyYAML”というライブラリを使えばyamlを読み込むこと自体は可能なのですが、開発者が想定していない形式でyamlが記述されるとエラーや予期せぬ挙動に繋がります。 当初の開発者以外がyamlファイルを更新することを見越して、ファイルの中身をバリデーションすることが望ましいと考えました。そこで、型・データのバリデーションが可能なライブラリである” Pydantic ”を活用することにしました。 上記の形式のyamlファイルを安全にパースするために、以下のようなPydanticモデルクラスを定義しています。 # コードの一部を抜粋・簡略化して記載しています import datetime from pydantic import BaseModel, Field, field_validator class ABTestGroup (BaseModel): model_name: str weight: float = Field(..., ge= 0.0 , le= 1.0 ) class ABTestExperiment (BaseModel): experiment_name: str start_date: datetime.date end_date: datetime.date groups: list [ABTestGroup] @ field_validator ( 'groups' ) @ classmethod def validate_total_weight (cls, v: list [ABTestGroup]) -> list [ABTestGroup]: """ 各groupのweightの合計が1.0であることを確認する。 """ total_weight = sum (group.weight for group in v) if not math.isclose(total_weight, 1.0 , rel_tol= 1e-9 ): raise ValueError ( 'Total weight of groups must be 1.0' ) return v class ABTestConfig (BaseModel): experiments: list [ABTestExperiment] 例えば、 weight (各グループに割り当てる事業者の割合)に対しては、型アノテーションと Field を使って型と数値の範囲のバリデーションを実施しています。 加えて、各グループの割合の合計が1.0を超えることを避けるため、フィールドごとのカスタムバリデーションを定義可能な field_validator を使用し、独自のロジックでバリデーションを実装しました。 このようなPydanticを使ったバリデーション処理をML基盤のCIを通して呼び出すことにより、不適切なyamlファイルを事前に検知できるようにしています。 どう割り当てるか A/B割り当てに関しては、事業者を適切にバケット(=グループ)に割り当てるために、事業者のIDとキーを使用しています。 # コードの一部を抜粋・簡略化して記載しています import hashlib def _compute_allocation_bucket (company_id: int , key: str , bucket_size: int ) -> int : """ company_idとkeyに基づき、company_idに対してバケットを割り当てる。 keyはexperiment_nameなどを想定。 """ hash_key = key + str (company_id) hash_str = hashlib.sha256(hash_key.encode( "utf-8" )).hexdigest() return int (hash_str, 16 ) % bucket_size + 1 まず、実験名をkeyとして事業者のIDと結合し、ハッシュ化の元となる文字列を生成します。次に文字列をハッシュ化し、16進数の文字列を取得します。ハッシュ値を整数に戻した後、バケットサイズで割った余りをバケット番号とします。 その後、各MLモデルに対して weight に基づいてバケット範囲を割り当てることでMLモデルと事業者を紐付けます(コードは省略します)。 ややこしい点はありつつも上記のロジックにより、同じ事業者とキーの組み合わせに対して一貫して同じバケットを割り当てることができます。 実行のタイミングによって割り当てが変化する等の問題が生じず、A/Bテストの管理が容易になります。また、ハッシュ関数を使うことで入力値をほぼ均等に分散させることができ、実質的にランダムにグループ分けすることができます。 *1 おわりに 機械学習バッチ予測とA/Bテストをシンプルに実現した話をご紹介しました。 今回、データサイエンティストとバックエンドエンジニアの共同開発については黎明期といった状況での開発でしたが(そこがまた楽しいのですが)、主にPdM含めた3人で協力しながら手探りで設計・実装を進めていきました。 その他にもMLOpsエンジニア、データエンジニアなど多くのポジションの方々に協力いただいており、部門を跨いでスムーズに協業できる組織体制が整ってきたことを開発を通して感じました。 今回ご紹介した構成にはやや課題が残っていたりするのですが、部門を横断しつつ解決を図っていけるのではと考えています。 We’re Hiring! タイミーのデータエンジニアリング部・データアナリティクス部では、ともに働くメンバーを募集しています!! 現在募集中のポジションは こちら です! *2 「話を聞きたい」と思われた方は、是非一度 カジュアル面談 でお話ししましょう! *1 : ハッシュ関数を活用したA/B割り当てに関しては Gunosyさんのブログ記事 が分かりやすいです。 *2 : 募集中のエンジニア系のポジションは こちら です!
OGP 2024/07/09 に Platform Engineering Kaigi 2024(PEK2024) が docomo R&D OPENLAB ODAIBA で開催されました。 www.cnia.io タイミーは Platinum スポンサーとして協賛させていただき、プラットフォームエンジニアリンググループ グループマネージャーの恩田が 「タイミーを支えるプラットフォームエンジニアリング・成果指標設計から考える組織作り事例の紹介」 を発表しました。 タイミーには世界中で開催されるすべての技術系カンファレンスに無制限で参加できる「Kaigi Pass」という制度があります。この制度を活用してタイミーから4名のエンジニアがオフライン参加しました。 productpr.timee.co.jp 各エンジニアが印象に残ったセッションの感想を参加レポートとしてお届けします。 What is Platform as a Product and Why Should You Care What is Platform as a Product and Why Should You Care speakerdeck.com チームトポロジーの共著者であるマニュエルさんによる Platform as a Product という考え方がなぜ重要なのか、Platform as a Product を実現するためには特に何を念頭においてプラットフォームを構築すべきなのかを紹介するセッションでした。 価値あるプラットフォームとはストリームアラインドチームの認知負荷を下げるものであり、価値あるプロダクトとは顧客の何らかの仕事を簡単・楽にするものという話がありました。 私は元々 Platform as a Product という単語を知らない状態でこのセッションを聞いていたのですが、顧客をストリームアラインドチームと置き換えるとプラットフォームとプロダクトが同一視でき、価値あるプロダクトを生み出すためのアプローチをプラットフォームに応用というのはなるほど一理あると感じました。 プラットフォームをプロダクトと同一視すると、色々と伸び代が見えてきます。 プラットフォームは作って終わりではなく Go To Market まで考える プラットフォームは足し算で機能を足していくのではなく引き算も考える必要がある このあたりに関してはエンジニアリングというよりもプロダクトマネジメントの領域です。この発表を聞いてプラットフォームチームにもプロダクトマネージャーを配置する重要性を強く感じました。 もう一つ印象的だったのが、ストリームアラインドチームからプラットフォームチームを信頼してもらうことが重要という点です。チームトポロジーを踏まえた組織設計を考える上で、ストリームアラインドチーム・プラットフォームチームは認知していたのですが、あくまで構造として認知をしていました。ですが、チームを構成するのは人です。ストリームアラインドチームがプラットフォームチームを信頼していなければ、プラットフォームチームが作ったプラットフォームは信頼されないし、信頼されないと諸々デバフがかかった状態で物事を進める必要が出てくるため、プラットフォームチームが価値あるプラットフォームを生み出していたとしても浸透に時間がかかるようになります。この点は認識できていなかったのでハッとしたポイントでした。 (@euglena1215) Platform Engineering at Mercari Platform Engineering at Mercari speakerdeck.com Mercai deeeetさんの講演です。個人的にはSRE(Site Reliability Engineering)の導入やGoogle CloudでのGKE運用の方法など様々な情報発信をされていて、ありがたく参考にさせていただいています。このセッションでは、deeeetさんが入社して7年、立ち上げ当初から関わっている MercariにおけるPlatformEngineeringの歴史を振り返ってお話をされるという内容でした。 MercariのPlatform Engineeringのミッションは「メルカリグループの開発者がメルカリのお客様に対して新しい価値やより良い体験を早く安全に届けることができるようにサポートするインフラ、ツール、ワークフローを提供すること」とありました。今まで別のmeetupなどでもお話をされていますし、さらっと冒頭にあった言葉ですが、改めて目にするとシンプルで分かりやすく過不足なく定義されているなという印象を受けました。 さて、Platform Engineeringの具体的な実施事項(どういう組織・どういうツールセットか)などは是非セッション動画を見ていただく方が良いのでここでは記述を割愛いたしますが、”コラボレーションから始める”という言葉がすべてを物語っていました。 Platform Engineeringの歴史と、どのように作り上げていったかについての説明がなされました。それはモノリス・レガシーインフラからマイクロサービス・Kubernetes環境へのマイグレーションを開発者とコラボレーションしながら設計したりツールセットを整えたりしてきた、とのことでした。 マイグレーション当時はPlatform Engineeringチームメンバーが開発者の席に散っていって隣に座って一緒に会話をしながら設計やツールセットを一緒に作ったりしていたそうです。これは狙ってやったわけではなく、目的を達成するためにやっていたことが結果的に後から振り返って良い戦略だったなと思ったとのこと。泥臭いけれど、近くで会話するというのは本当に良いことです。現在はリモートワークが主流のところも(弊社も含め)多く、物理的に離れた場所にいる組織ではどのようにデザインするか工夫が必要そうだなと思いました。 以上までは立ち上げフェーズでの話でしたが、2020年ごろから現在まではPlatform Engineering組織のアップデートをしているとのこと。一つはメンバーが増えてきて(10名→15名程度→さらに増やす)、かつ見るべき領域が多くなり認知負荷が高まってきたため分割を考えたとのこと。この中でも印象的なのはProductチームとPlatformチーム間のInterfaceとなるPlatform DX(Developer Exprerience)チームを配置したとのこと。今まで1つの塊だったPlatformチームが細分化されるにあたって、Productチームが「これはこのチーム、あれはあのチーム…」と細かな事情を抑えてコミュニケーションをする認知負荷を軽減する目的のようでした。 まだいくつか気になった点はありますが、長くなりそうですのでこのあたりで留めておこうと思います。もし興味を持たれましたら是非資料やアーカイブをご覧ください!(@橋本和宏) タイミーを支えるプラットフォームエンジニアリング・成果指標設計から考える組織作り事例の紹介 タイミーを支えるプラットフォームエンジニアリング・成果指標設計から考える組織作り事例の紹介 speakerdeck.com 弊社 恩田からの講演となります。直近1年での元々あったチーム課題をどのように解決してきたのか、また取り組んだ内容の反省点などが示されています。 ここでは詳細をあえて書かず、是非アーカイブ動画もしくは下記のスライドをご覧いただき、皆様の参考になると幸いと考えています。是非ご覧くださいませ! (@橋本和宏) プラットフォームエンジニアリングの功罪 プラットフォームエンジニアリングの功罪 speakerdeck.com DMMさんにおけるプラットフォームエンジニアリングについて、題名にもあるとおり”功罪”という側面で実例を交えてお話をされていました。 マルチクラウド”k8s”の功罪 話を聞いている途中で私自身が”マルチクラウド”の功罪だと聞いてしまっていた節があったので、”k8s”と強調した題名にしました。あくまでkubernetes環境が異なるプラットフォーム上に存在して両方の足並みを揃えて運用するところにツラみがあったということになります。 さまざまなビジネス要件があり、それらのシステムが個別に作られるにあたってAWSとGCP、異なる技術スタックなどがあり大変だったところが出発点だったとのこと。これらをまずはkubernetesと周辺エコシステム(ArgoCDなど)に揃えることは良かった(功)とのこと。 対して、EKS(AWS)とGKE(GCP)という同じマネージドkubernetesであるものの、細かな違いに翻弄されたり、エコシステムの選定が両環境で動くことに引きづられてしまったりと運用の大変である(罪)であるとのことでした。マルチクラウド”k8s”は手を出すときには覚悟がいるものだなということを実例を持って示していただけて大変参考になりました。 セルフサービスの功罪 多くの(15のサービス)の開発者からの依頼を4人のプラットフォームエンジニアがレビューすることは捌けなくはなかったが、規模の拡大に伴いボトルネックになりうることを懸念してセルフサービス化を推進したとのことです。この点は他の会社等の事例でも語られている通り正しい選択で良かった(功)であったとのこと。 対して、レビューの人的コストを削減することは達成できたものの、その後表出した課題はレビューそのものを少なくしたことによるもの(罪)であった点はとても学びがありました。podの割当リソース最適化ができていないことや(request/limitが同じ値でかなり余裕をもった値で開発者が設定してしまっていると想像)、セキュリティリスク(ACLが適切でないingressが作成できてしまった)などは確かに通常はインフラが分かる人のレビューを持って担保しているものです。 これらの課題に対しては仕組みによる担保(Policy as Codeなどによる機械的なチェック)をすることとして、セルフサービス化の恩恵獲得に倒しているという決断も共感できるものでした。 プラットフォームチームの功罪 共通化されたプラットフォームシステムを作って移行することで、組織としてもプラットフォームチームというものが組成され、インフラ(プラットフォーム)エンジニアがボトルネックになることなく開発者が開発に集中できるようになったのは良かった(功)とのことです。 罪の部分に対してどのようなものだろう?と聞いている途中で興味深かったのですが、プラットフォーム化やセルフサービス化を進めても、開発者側で対応可能な問い合わせが結構な割合で来てしまい、対応コストが依然としてかかる = プラットフォームチームがボトルネックになってしまっている部分がまだまだあるとのことでした。 やはり「実際にやってみたら色々と大変だった」という話はとても価値があります。使っている技術スタックが異なることがあっても行き着く先にあるのはヒトや組織にあり、ある程度通底するものがあるのだなと改めて痛感しました。 (@橋本 和宏 ) いつPlatform Engineeringを始めるべきか?〜レバテックのケーススタディ〜 いつPlatform Engineeringを始めるべきか?〜レバテックのケーススタディ〜 speakerdeck.com レバテックさんの基盤システムグループという Platform Engineering の流れ以前に作られたチームが機能不全になっていることに気付き、棚卸しを行なって基盤システムグループを大幅に縮小するまでの流れを紹介しながら、今あなたの会社は Platform Engineering を始めるべきなのか?に対して示唆を与えるセッションでした。 基盤グループメンバーの声、ストリームアラインドチームの声から現状を深掘っていくスタイルはその頃の状況がとてもイメージがしやすく分かりやすかったです。個人的には聞いたセッションの中ではマニュエルさんの keynote の次に良かったセッションです。 タイミーにはプラットフォームチームが既に存在しているため、いつ Platform Engineering を始めるべきか?に悩むことはないですが、ユーザーの声を元に仮説を立てて仮説を検証し次の洞察につなげていく進め方・考え方はとても参考になりました。 発表内容からは少し逸れるのですが、基盤システムグループを大幅に縮小するという決断をグループリーダーが行えたことは素晴らしいと感じました。リーダーの影響力はチームメンバー数に比例すると思っていて、チームの縮小とはリーダーの影響力の縮小を意味すると思っています。ここに対してしっかりとアプローチを行なっていたのは素晴らしいなと思いました。 (@euglena1215) Platform Engineering Kaigi 2024 トラックB マルチクラスタの認知負荷に立ち向かう!Ubieのプラットフォームエンジニアリング マルチクラスタの認知負荷に立ち向かう!Ubieのプラットフォームエンジニアリング | Platform Engineering Kaigi 2024 speakerdeck.com Ubieのセッションでは、アプリケーションエンジニアやSREが直面する設定作業や問い合わせ対応の負担を軽減するための取り組みが紹介されました。 Ubieの講演会では、アプリケーションエンジニアとSREが直面する課題と、それに対するソリューションについての興味深い話がありました。特に、設定作業や問い合わせ対応の負担が大きい現状に対して、マルチクラスター移行を検討する必要性が述べられました。しかし、クラスター間の通信やデプロイメントの課題が浮上するため、簡単ではないとのことでした。 その解決策として、Ubieは「ubieform」と「ubieHub」を開発しました。 ubieform これは、GKEクラスターやBackStageなどを自動で作成し、k8sの設定からアクションの設定、GCPの設定までを出力してくれるツールです。このツールを導入することで、認知負荷を減らし、エンジニアの作業を大幅に効率化できるとされています。しかし、ある程度の完成度になるまで展開しない方針を取っており、質の確保に注力している点が印象的でした。 ubieHub 情報を取得するためのハブとなるもので、基本的にはBackStageをベースにしているようです。新しいツールやポータルの導入は、小規模に始めて早期にフィードバックを得ることが重要とされています。また、リンク切れなどの問題が発生しやすいため、定期的に情報を更新し、最新の状態を保つ必要があります。 新しいツールの導入時には、「ドッグフーディング」つまり、自社内で試用することが必須とされており、初期のバグ対応やチーム内でのコミュニケーションが重要であることが強調されました。 全体として、Ubieはツールの開発と導入において非常に戦略的かつ実践的なアプローチを取っており、その姿勢が印象深かったです。ツールの完成度と認知負荷のバランスを取りながら、効率的な業務環境の構築を目指す姿勢に感銘を受けました。 (@hiroshi tokudomi ) 最後に Platform Engineering という言葉は概念を表すものであり、細かな実装は各社各様さまざまなものがあります。大事なのは言葉の定義そのものではなく、何に対して価値提供するか・できているかを考え続けることだと考えます。 PEK2024は様々な環境・会社における具体的な課題やその解決のための実装を知ることで、自社における課題解決へとつながるきっかけが多くありました。 様々なセッションの聴講を通じて共通のキーワードとして以下の3点があることに気づきました。 知ってもらう 頼ってもらう 評価してもらう 何を提供しているか知ってもらって使ってもらわなければ価値提供できているとは言えない。頼ってもらう存在にならなければ、そもそも価値提供につなげることができない。また、提供したものが評価されるものでなければ、価値提供できているとは言えない。ということになります。 これらのことは何か特別な定義でもなく、プロダクトを提供する立場の人であれば当たり前のことであると感じられると思います。この当たり前のことを当たり前のこととして”やっていく”ことがプラットフォームエンジニアリングを担うものとして意識すべきことだと強く意識した良い機会となりました。 (@橋本 和宏)
こんにちは、タイミーのデータエンジニアリング部 データサイエンス(以下DS)グループ所属のYukitomoです。 今回はPythonのLinterとしてメジャーなflake8のプラグインの作り方を紹介したいと思います。 コードの記述形式やフォーマットを一定に保つため、black/isort/flake8などのformat/lintツールを使うことはpythonに限らずよく行われていますが、より細部のクラス名や変数名を細かく規制したい(例:このモジュールのクラスはこういう名前付けルールを設定したい等)、けれどコードレビューでそんな細かい部分を目視で指摘するのは効率的でない、といったケースはありませんか?そんな時、flake8のプラグインを用意して自動検出できるようにしておくと便利です。 ネット上には公式サイトを含めいくつかプラグイン作成の記事があるのですが、我々の想定ケースと微妙に異なる部分がありそのままでは利用できなかったため、 最新のflake8(2024/7現在, 7.1.0)を用い 比較的新しいパッケージマネージャーであるpoetry(1.8.3を想定)を利用して 2種類のプラグインのそれぞれの作り方 を改めてここにまとめます。 準備するもの Python: Versionは特に問いませんが、3.11.9で動作確認しています。 Poetry: 1.8 以上 (後述しますが1.8より導入されたpackage-mode = falseを指定しているため)。この記述を変えることで1.8以前のバージョンでも動くとは思いますが、この記事では1.8を前提としています。 上記が利用可能な環境をvenvやコンテナを利用して作成しておいてください。flake8本体はpyproject.tomlの依存モジュールとして導入されるため事前に準備する必要はありません(3.8以降で動作するはずですが、本記事では7.1を利用します)。 全体の構成 サンプルで利用するファイル群は以下の通りです。構文木を利用するタイプと1行ずつ読み込んでいくタイプと2種類あるため、それぞれをtype_a、type_bとしてサンプルを用意し、それら2つのサンプルを束ねる上位のプロジェクトを一つ用意しています。本来なら各プラグイン毎にユニットテスト等も実装すべきですが、本記事ではプラグインの書き方自体の紹介が目的のため割愛しています。なお、type A, type Bの呼称はflake8プラグインにおいて一般的な呼び名ではなく、本記事の中で2つのタイプを識別するために利用しているだけなので注意してください。 # poetry.lock 等本記事の本質と関係のないものは省略しています (.venv) % tree . # この位置を$REPOSITORY_ROOTとします。 . ├── pyproject.toml └── plugins ├── type_a │ ├── pyproject.toml │ └── type_a.py └── type_b ├── pyproject.toml └── type_b.py ${REPOSITORY_ROOT}/pyproject.tomlは以下の通り。 # cat ${REPOSITORY_ROOT}/pyproject.toml [tool.poetry] name = "flake8 plugin samples" version = "0.0.1" description = "A sample project to demonstrate flake8 plugins" authors = [ "timee-datascientists" ] package-mode = false # この記述を外せばきっとpoetry 1.8より前でも動くはず [tool.poetry.dependencies] python = ">=3.11.9" [[tool.poetry.source]] name = "PyPI" priority = "primary" [tool.poetry.group.dev.dependencies] # flake8を利用するので一緒によく利用されるblack/isortも導入 flake8 = "~7.1.0" isort = "~5.13.2" black = "~24.4.0" # プラグインはローカルからeditable modeで登録 type_a = { path= "./plugins/type_a" , develop = true} type_b = { path= "./plugins/type_b" , develop = true} [build-system] requires = [ "poetry>=1.8" ] build-backend = "poetry.masonry.api" Type A: AST Treeを利用する場合 Python codeの1ファイルをparseして抽象構文木(AST)として渡すタイプのプラグインです。ネットでflake8のプラグインを検索した時、こちらのタイプの実装例が出てくることが多く、また、構文木の処理が実装できるなら、こちらの方が使いやすいです。 構文木で渡されたpython ファイルを巡回し、その過程で違反を発見するとエラーを報告しますが、本記事のサンプルでは構文木の巡回結果は無視し、巡回後必ずエラーを報告しています。詳細はast.NodeVisitorを参照いただきたいのですが、各ノードを巡回する際に呼ばれるvisit()だけでなく、visit_FunctionDef() などファイル内で関数定義された場合、など個別の関数が用意されているので、これらを適切に上書きすることで、目的の処理を実現していくことになります。 なお、プラグインのコンストラクタには抽象構文木(ast)の他、lines, total_lines等公式ドキュメントの ここ に記述されているものを追加することができます。 以下にサンプルの実装(type_a/type_a.py)とプロジェクトの定義ファイル(type_a/pyproject.toml)を示します。 # type_a/type_a.py import ast from typing import Generator, List, Tuple # プラグインの本体 class TypeAPluginSample : def __init__ ( self, tree: ast.AST #, lines, total_lines: int = 0 ) -> None : self.tree = tree def run (self) -> Generator[Tuple[ int , int , str , None ], None , None ]: visitor = MyVisitor() visitor.visit(self.tree) # サンプルでは常にエラーを報告するが本来ならvisitorに結果を溜め込んで # 結果に応じてエラーをレポート if True : yield 0 , 0 , "DSG001 sample error message" , None # プラグインから利用する構文木の巡回機 class MyVisitor (ast.NodeVisitor): # visit() やvisit_FunctionDef()を目的に応じて上書き pass # 他のサンプルでは必須っぽく書いてあるが、pyproject.tomlのentry-points # の指定と被ってるなぁと思ってコメントアウトしても動いたので今はいらない気がする。 # def get_parser(): # return TypeAPluginSample (.venv) % cat plugins/type_a/pyproject.toml # 親プロジェクトから直接ロードするため [project]の記述もしていますが # プラグイン単体で独立したプロジェクトとするなら不要。 [project] name = "type_a" version = "0.1.0" description = "Sample type a plugin" authors = [{name = "timee-datascientists", email = "your.email@example.com"}] [tool.poetry] name = "type_a" version = "0.1.0" description = "Sample type-a plugin" authors = ["timee-datascientists"] [build-system] requires = ["setuptools", "wheel", "poetry>=1.8.3"] build-backend = "setuptools.build_meta" [tool.poetry.dependencies] python = ">=3.11.9" flake8 = ">=7.1.0" # ここでプラグインのクラス名を登録 [project.entry-points."flake8.extension"] DSG = "type_a:TypeAPluginSample" Type B: 1行ずつ処理する場合 対象となるpython ファイルを1行ずつ処理していくタイプのプラグインです。 公式ドキュメント にある通り、歴史的な経緯で2種類あるようですが、こちらの1行ずつ処理するタイプを使ったサンプルを見かけたことがありません。特に非推奨とされているわけでもないですし、実装したいルール自体がシンプルであればこちらの方法で実装するのもありだと私は思います。physical_lineもしくはlogical_lineを第一引数に設定し、physical_lineの場合はファイルに書かれている1行ずつ、logical_lineの場合はpython の論理行の単位で指定した関数が呼ばれます。physical_line, logical_lineの両方を同時に指定することはできず、他の変数を追加する場合もphysical_line/logical_lineは第一引数とする必要があります。 以下にサンプルの実装(type_b/type_b.py)とプロジェクトの定義ファイル(type_b/pyproject.toml)を示します。 # type_b/type_b.py from typing import Optional # プラグイン本体 def plugin_physical_lines( physical_line: Optional[str] = None, line_number: Optional[int] = None, filename: Optional[str] = None, ): if line_number == 2: yield line_number, "DSG002 sample error message" (.venv) % cat plugins/type_b/pyproject.toml # type_aのものとほぼ同じ。project.nameおよびtool.poetry.nameをtype_bに書き換えた後、 # 差分は以下。プラグイン本体の関数を指定してやれば良い。 : [project.entry-points. "flake8.extension" ] DSG = "type_b:plugin_physical_lines" 実行結果 以下のようなサンプルファイルを用意し、flake8を実行した結果を示します。 # sample.py def main (): print ( 'Hello, World!' ) if __name__ == '__main__' : main() 実行結果 % flake8 sample.py sample.py: 0 : 1 : DSG001 sample error message sample.py: 2 : 3 : DSG002 sample error message 注意点 Type A, Type B両方とも 公式ドキュメント に書いてある変数は全てコンストラクタに追加できるのですが、それぞれのタイプにおいて意味のあるものは限られるため、必要なもののみを追加すれば良いです。 まとめ flake8 のプラグインの定義方法を2通りご紹介しました。 タイミーのデータサイエンスグループでは通常のformat/lintだけでカバーできない(けれど少しの工夫により機械作業で抽出できる)運用ルールを本記事のようなflake8プラグインを用いてCIで事前に検出することで、コードレビューはできるだけ本質的な部分に集中できるよう取り組んでいます。 We’re Hiring! タイミーのデータエンジニアリング部・データアナリティクス部では、ともに働くメンバーを募集しています!! 現在募集中のポジションは こちら です! 「話を聞きたい」と思われた方は、是非一度 カジュアル面談 でお話ししましょう! References flake8 Pluginの書き方公式ドキュメント: https://flake8.pycqa.org/en/latest/plugin-development/index.html Pluginの入力として利用できる変数一覧: https://flake8.pycqa.org/en/latest/plugin-development/plugin-parameters.html flake8 Version 2.5.4を用いた参考記事(本記事でType Aと呼んでいるのと同じタイプのもの) https://blog.amedama.jp/entry/2016/04/12/063359#google_vignette さらに別な本記事でType Aと呼んでいるタイプのプラグインの作成記事: https://qiita.com/misohagi/items/756954d7f4315cea0230
タイミー QA Enabling Teamのyajiriです。 去る6月28日〜29日の2日間、ファインディ様主催の「開発生産性カンファレンス2024」に参加してきました。 (タイミーには世界中で開催されるすべての技術系カンファレンスに無制限で参加できる「Kaigi Pass」という制度があり、今回もこれを利用して新潟からはるばる参加してきました。) productpr.timee.co.jp タイミーでは弊社VPoE(VP of ええやん Engineering)の赤澤の登壇でもご紹介した通り、チームトポロジーを組織に適用し、プロダクト組織の強化と改善にチャレンジしています。 speakerdeck.com この登壇でも紹介されておりますが、私自身もイネイブリングチームの一員として、プロダクト組織全体のQA(品質保証)ケイパビリティの向上や、障害予防プロセスの改善に取り組んでいます。 開発生産性の観点から考える自動テスト まずQAの視点で最も印象に残ったのは、皆さんもご存知のt_wadaさんによる「開発生産性の観点から考える自動テスト(2024/06版)」です。 speakerdeck.com なぜ自動テストを書くのか? この問いに対してt_wadaさんは 「コストを削減するためではなく、素早く躊躇なく変化し続ける力を得るため」 そして 「信頼性の高い実行結果に短い時間で到達する状態を保つことで、開発者に根拠ある自信を与え、ソフトウェアの成長を持続可能にすること」 と表現されていました。 (ここまで一言一句に無駄のない文章は久々に見た気がします) タイミーでもアジャイル開発の中で高速なテストとフィードバックのサイクルを意識し、自動テストを含むテストアーキテクチャの強化に取り組んでいます。しかし、活動がスケールすると共にテストの信頼不能性(Flakiness)や実行時間の肥大化、費用対効果などの問題が発生します。 これらの問題に対する合理的な対応策を検討する上で、各々のテストの責務(タイプ)や粒度(レベル)を分類し、費用対効果と合目的性の高いものから重点的に対応していく必要があります。 そのためのツールとして「アジャイルテストの四象限」や「テストピラミッド」「テスティングトロフィー」などを活用し、テストレベルを整理し、テストのポートフォリオを最適化するアプローチを取っていましたが、具体的なアーキテクチャに落とし込んだ際に「これってどのテストレベルなんだっけ?」といった想定と実態の乖離がしばしば発生していました。 サイズで分類しテストダブルでテスト容易性を向上する それを解決する手段として、テストレベルではなくテスト「サイズ」で整理する方法が提唱されました。 テストサイズの概念は古くは「 テストから見えてくる グーグルのソフトウェア開発 」、最近では「 Googleのソフトウェアエンジニアリング 」で紹介されていました。今回紹介されたのは、テストピラミッドにおいても具体的なテストタイプではなく「サイズ」で分類し、テストダブル(実際のコンポーネントの代わりに使用される模擬オブジェクト)を積極的に利用することでテスタビリティを向上させ、テストサイズを下げ、速度と決定性の高いテストが多く実装される状態を作るというアプローチです。 このアプローチは、タイミーのDevOpsカルチャーにも親和性が高く、ぜひ自動テスト戦略に取り入れたいと感じました。 おわりに 他にも魅力的で参考になる登壇が盛りだくさんで、丸々2日間の日程があっという間に過ぎる素晴らしいイベントでした。 主催のファインディ様やスポンサー、登壇者の皆さまに感謝するとともに、来年の開催も心より楽しみにしています。
こんにちは、タイミーのデータエンジニアリング部データサイエンス(以下DS)グループ所属の菊地です。 今回は、 H3 を使用したBigQueryでの空間クラスタリングについて検証した内容を紹介したいと思います! BigQueryでの空間クラスタリングとは BigQueryにはクラスタリングという機能があり、うまく活用すると、クエリのパフォーマンスを向上させ、クエリ費用を削減できます。 クラスタリングは空間データにも適用でき、BigQuery がデフォルトで使用するS2インデックス システムを使用して、空間クラスタリングを行うことができます。 また、H3やGeohashなどの他の空間インデックスに対しても空間クラスタリングを行うことができ、今回はタイミーでも良く使用している H3 を使用して、空間クラスタリングを行う方法を検証してみました。 BigQueryでのクラスタリング及び空間クラスタリングについては、下記の記事が参考になるかと思います。 cloud.google.com cloud.google.com H3を使用した BigQueryでの空間クラスタリングの検証 上記の参考記事でも挙げましたが、基本的にこちら記事の内容に沿いつつ、一部具体の実装が記載されていない箇所を補完しながら検証を行いました。 cloud.google.com 1. 検証用のテーブル作成 検証用のテーブルとして、経度と緯度のランダムポイントを、H3セルID(解像度13)に変換したテーブルを作成します。 DECLARE H3_INDEX_RESOLUTION INT64 DEFAULT 13 ; -- 連番を格納しておくためだけのテーブル -- CTEだと後続のテーブル作成が遅かったので実テーブルにしてます CREATE OR REPLACE TABLE `tmp.tmprows` as SELECT x FROM UNNEST(GENERATE_ARRAY( 1 , 10000 )) AS x; -- 経度と緯度のランダムポイントを、H3セルID(解像度13)に変換したテーブル DROP TABLE IF EXISTS `tmp.h3_points`; CREATE OR REPLACE TABLE `tmp.h3_points` CLUSTER BY h3_index AS WITH points AS ( SELECT `carto-os`.carto.H3_FROMLONGLAT(RAND() * 360 - 180 , RAND() * 180 - 90 , H3_INDEX_RESOLUTION) AS h3_index -- 後の検証のために追加 , RAND() AS amount FROM `tmp.tmprows` AS _a CROSS JOIN `tmp.tmprows` AS _b ) select h3_index , amount FROM points テーブルのストレージ情報と内容は以下のようになります。 2. クラスタリングによる絞り込みが効かないクエリ例 次に、 参考記事 で紹介されているように、親セルID(今回は解像度7)をWHERE句で指定してクエリを実行してみましたが、このクエリはテーブルをフルスキャンしてしまいます。 DECLARE PARENT_CELL_ID STRING DEFAULT ' 870000000ffffff ' ; -- H3解像度7のセルID SELECT ROUND ( SUM (amount), 6 ) AS sum_amount FROM `tmp.h3_points` WHERE `carto-os`.carto.H3_TOPARENT(h3_index, 7 ) = PARENT_CELL_ID ジョブ情報と結果 H3インデックスでクラスタリングを行っているにもかかわらず、テーブルをフルスキャンしてしまう理由としては、 H3_ToParentにはビット演算が関係し、複雑すぎて BigQuery のクエリアナライザが、クエリの結果がクラスタ境界にどのように関連しているかを把握できないために発生します。 と 参考記事 では言及されています。 3. クラスタリングによる絞り込みが効くクエリ例 次に、クラスタリングによる絞り込みが適用されるクエリを検証してみます。 「2. クラスタリングによる絞り込みが効かないクエリ例」との違いとしては、低解像度の親セルに含まれる、高解像度セルの開始IDと終了IDを取得し、WHERE句で指定していることです。 DECLARE H3_PARENT_ID STRING DEFAULT ' 870000000ffffff ' ; -- H3解像度7のセルID DECLARE H3_INDEX_RESOLUTION INT64 DEFAULT 13 ; DECLARE RANGE_START STRING; DECLARE RANGE_END STRING; -- 低解像度の親セルに含まれる、高解像度セルの開始IDと終了IDを取得しセットする SET (RANGE_START, RANGE_END) = ( SELECT AS STRUCT `carto-os`.carto.H3_TOCHILDREN(H3_PARENT_ID, H3_INDEX_RESOLUTION)[ 0 ], ARRAY_REVERSE(`carto-os`.carto.H3_TOCHILDREN(H3_PARENT_ID, H3_INDEX_RESOLUTION))[ 0 ] ); SELECT ROUND ( SUM (amount), 6 ) AS sum_amount FROM `tmp.h3_points` WHERE h3_index BETWEEN RANGE_START AND RANGE_END ジョブ情報と結果は以下のようになっており、スキャン量が削減され、クエリのパフォーマンスも向上しています。クエリ結果も「2. クラスタリングによる絞り込みが効かないクエリ例」の結果と合致しています。 ジョブ情報と結果 まとめ H3を使用した BigQueryでの空間クラスタリングについて検証してきました。 タイミーでは位置情報を活用した分析を行うシーンが多く、うまく活用することで機械学習時の特徴量生成や、BIツールからのクエリ最適化に繋げることができる可能性があるので、今後のデータ分析に活かしていきたいと思います。 We’re Hiring! タイミーのデータエンジニアリング部・データアナリティクス部では、ともに働くメンバーを募集しています!! 現在募集中のポジションは こちら です! 「話を聞きたい」と思われた方は、是非一度 カジュアル面談 でお話ししましょう!
2024年6月22日(土)に Kotlin Fest 2024 が開催されました。Kotlin Festは「Kotlinを愛でる」というビジョンを掲げた技術カンファレンスです。タイミーのAndroidエンジニアはエンジニアの成長を支援する制度の一つである Kaigi Pass を利用して参加しました。 本投稿では、Kotlin Fest 2024に参加したメンバー(中川、haru、みかみ、 しゃむ 、 むらた 、 tick-tack )が気になったセッションや感想のレポートします! メンバーによるレポート 中川編 効果的なComposable関数のAPI設計 私が気になったセッションは、haru067さんによる「効果的なComposable関数のAPI設計」です。このセッションでは、Composable関数を書くときに引数をどのように定義すべきかという、現場で直面する具体的な疑問に対して、様々なケーススタディを通じて考察が行われました。 セッションでは以下のプラクティスに触れられました: State hoisting Slot API DSLでのslot APIの活用 デフォルト引数 Property drilling 特に印象的だったのは、これらのプラクティスが常に最適な解決策とは限らないという点が強調されていたことです。むやみに使うのではなく、適切な場面で使うことが重要であるという、現場での経験に裏打ちされた具体的なアドバイスが参考になりました。 haru編 Kotlinで愉しむクリエイティブコーディング まず最初にご紹介するのは、 畠山 創太 さんによる Kotlinで愉しむクリエイティブコーディング です。 私はクラブイベントにたまに行くので、VJさんという存在を元々知っていたのですが、そんなVJさんの中でもジェネ系と呼ばれる画面をリアルタイムに生成するライブコーディング的なアプローチのVJさんとプライベートで繋がりがあり、それに利用されているフレームワークなどを知っていました。 そんな中、このセッションではKotlinでリアルタイムにグラフィックスを処理できて、ジェネ系VJにも使えそうな OPENRNDR が紹介されていました。 OPENRNDRはProcessingやTouch Designerなどのジェネ系VJで使われるフレームワークとよく似たフレームワークで、KotlinベースのDSLでグラフィックス処理を記述することができます。 このセッションでは、OPENRNDRで書かれたいくつかのデモ(Boidsなど)が紹介され、OPENRNDRでできることの自由度や簡単に記述できることを紹介していました。 OpenGLベースのグラフィックスバックエンドをもち、RealSense, Kinect, TensorFlow, DMXなど多種多様な連携先が存在しており、これらを使えばセッションで紹介されていた以上のこともできそうだなと感じました。 Okioに愛を込めて 次にご紹介するのは、RyuNen344さんによる Okioに愛を込めて です。 OkioはBlock社が開発しているKotlin向けのI/O ライブラリで、OkHttpやMoshiのベースにも使われているライブラリです。 まず、Kotlinの標準ライブラリが充実しているのに、なぜOkioを採用するのかという話から始まりました。 いくつかの理由を紹介されていましたが、地味に落とし穴だなと思ったのは、Kotlinが元々JVMをターゲットとした言語としてスタートしているが故にJava標準ライブラリを呼び出しているところが多々あったり、それをKMPから使えなかったりするというところでした。 そんな中、OkioはJava標準ライブラリなどへの依存がなく、それでいて使い勝手の良いI/Oライブラリになっているということで、これから直接的・間接的問わず利用する頻度は増えていきそうでした。 これからKotlin向けのライブラリを作る上では、JVM以外のターゲットで使われることも前提として考えないといけないと思いました。 そして、綺麗なダジャレでセッションは終了。お見事でした。 みかみ編 例外設計について考えて Kotlin(Spring Boot&Arrow)で実践する 「例外設計について考えて Kotlin(Spring Boot&Arrow)で実践する」というセッションを紹介します。例外設計の重要性とプロダクト開発に与える影響について深く掘り下げ、KotlinとArrowライブラリを活用した柔軟な例外設計の実践方法が詳しく説明されていた発表でした。 特に印象的だったのは「例外設計とモデリング」についてです。このセッションでは、例外を「技術的例外とビジネス例外」および「予期する例外と予期しない例外」の組み合わせで大きく4つに分類できるという説明がありました。そしてそれぞれの例外に対して、ドメイン駆動設計(DDD)の考え方を基に、具体的にどのようにコードに反映させるかが紹介されました。例外をドメインに結びつけて考えることにより、プロダクト開発に良い影響を与える例外設計を行うことができると感じました。 例外自体は普段の実装でも意識しますが、その複雑さのため設計に関しては深く意識できていないことが多いと感じています。本セッション内容を通してプロダクト開発をより良くしていくための例外設計の考えた方と実践に挑戦していきたいと感じました。 しゃむ編 しゃむ( @arus4869 )です。FF16を最近ようやくクリアできたので、FFVIIリバースやり始めました。最高ですね。 KotlinのLinterまなびなおし2024 私が気になったセッションは「 KotlinのLinterまなびなおし2024 」です。このセッションでは、各種Lintツールの紹介だけでなく、Lintツールを効果的に活用するための実践的なアドバイスも多数紹介されました。 中でも特に気になったのはkonsistです。konsistは、標準セットルールがなく、各プロジェクトの特性に合わせたルール設定が可能である点が魅力的でした。また、テスト環境やユニットテストでの動作が主な特徴で、アノテーションを活用することで特定の用途に応じたルール設定ができる点も興味深かったです。 またセッションの中で、Lintルールを段階的に導入することでチームの負担を軽減しつつ、徐々にコード品質を向上させるアプローチも印象的でした。 このセッションを通じて、KotlinのLintの効果的な使い方について多くの知見を得ることができ、学び直しの良い機会になりました。ありがとうございました。 むらた編 むらた( @orerus )です。最近夫婦でカイロソフトさんのアプリにハマっています。 withContextってスレッド切り替え以外にも使えるって知ってた? さて、早速ですが私が気付きを得たセッションとしてT45Kさんによる「withContextってスレッド切り替え以外にも使えるって知ってた?」 を紹介させていただきます。 スライド も公開されています。 Kotlin coroutinesを使っていると頻繁に登場する withContext ですが、セッションタイトルでズバリ指摘されている通り、私もスレッドの切り替え用関数であるかのように意識してしまっていたことに気づきました。 使い方が間違っているわけではありませんが、セッションで紹介されている通り、withContextの挙動は正確にはスレッド切り替えではなく「CoroutineContextを切り替える」(厳密には既存のCoroutineContextと引数で渡されたCoroutineContextをマージする)ことにあります。そのうえで、渡されたブロックをcoroutineContextで指定されているcoroutineDispatcherにて実行するという形になります。(詳細については是非T45Kさんのスライド資料を参照ください) そのため、 withContext(Dispatchers.IO) のように切り替え先のスレッド (厳密には CoroutineDispatcher ) を指定するだけでなく、 withContext(Job() + Dispatchers.Default + CoroutineName("BackgroundCoroutine")) のように、複数のCoroutineContextを合成する形で引数を指定することができるんですね。( CoroutineContextの要素についてはこちらを参照ください ) なお、 withContext 以外のコルーチンビルダー( launch や async など)についても、引数で指定されたCoroutineContextと既存のCoroutineContextをマージして用いる挙動は同じです。 今回のセッションを通じて、Kotlin coroutinesへの理解がさらに深まりました。とても良いセッションをありがとうございました! tick-tack編 まだ JUnit を使ってるの? kotest を使って快適にテストを書こう Kotest についての HowTo を熱く語っておられるセッションで Kotest への愛を感じました。最近よく名前を聞くライブラリな気がします。 タイミーでも hamcrest を採用していますが Java 向けのテストライブラリは Kotlin の予約語が使われていてエスケープしないととても見づらいことがあります。やっぱり Kotlin first に書けるのは非常に気持ちがいいですね。Kotest は Runner が JUnit で安定した環境で動かせるのもグッド。 個人的にセッション内で刺さったポイントとしては Eventually と Property Based Testing です。 Eventually 内部で非同期処理を実行するメソッドのテストを書くときに実行しても assertion のタイミングが変更前で失敗するといったケースはよくあります。そういう時に eventually を使うと一定時間評価しつづけ期待する結果に変わったら成功と見なしてループを抜けてくれます。めちゃめちゃかしこい。逆に一定時間変更がないことを評価する continually もあるそうです。 Property Based Testing 都度実行する度に自前で用意しなくても、ランダムに自動生成された property を利用して複数回テストするといったことができます。境界値テストを用意する場合に役立ちそうです。 さっそく assertion だけですが触ってみました。 記述方法だけでも inifix で name shouldBe "tick-taku" みたいに書けて最高にワクワクします。楽しくテストが書けそうですね。 触ってみていいなと思ったのが、例えばインスタンスが別だけど中の property が同じな事だけ確認したい場合はこんな感じに書けました。1つずつ取り出して equals とかしなくてもスッキリしていいですね。 data class User( val id: Long , val name: String , val age: Int ) checkAll( iterations = 3 , Arb.long(), Arb.string( 1 .. 10 , Codepoint.katakana()), Arb.int( 1 .. 100 ) ) { id, name, age -> val user = User(id = id, name = name, age = age) repository.save(user) repository.getUser() shouldBeEqualToComparingFields user } 一応あまり有用な例ではないですが上で紹介した property testing の checkAll や property のランダム生成もせっかくなので書いてみました。 個人的には Google の Truth が好きでしたが推し変しそうです。Android プロジェクトに導入するのもよさそうでした。 まとめ Kotlin Fest 2024はKotlinという言語の可能性を改めて再認識するとともに熱意と活気に満ちたイベントでした。また、普段リモートワークで働くタイミーのエンジニアにとってもチームメンバーと対面で交流する貴重な機会でした。今回得られた知見を活かして今後のプロダクト開発にもさらに力を入れていきたいと思います、次回のKotlin Festも楽しみにしています!