TECH PLAY

タイミー

タイミー の技術ブログ

264

みんなでパシャリ タイミーの新谷、神山です。 東京Ruby会議12 が1月18日に開催されました。タイミーは Gold Sponsor として協賛をさせていたただき、ブースを出展していました。ブースに来ていただいたみなさんありがとうございます! 盛況なブースの様子 タイミーからは @ryopeko が「functionalなアプローチで動的要素を排除する」というタイトルで登壇しました。 speakerdeck.com また、タイミーには世界中で開催されているすべての技術カンファレンスに無制限で参加できる「Kaigi Pass」という制度があります。 productpr.timee.co.jp この制度を使って2名のエンジニアが参加しました。 参加して聞いたセッションのうち印象に残ったいくつかをピックアップしてご紹介します。 Keynote "Scaling Ruby @ GitHub" GitHub 社の John Hawthorn さんによる GitHub というプロダクトがどのようにして規模を拡大し、高可用性とパフォーマンスを維持しながら、アプリケーションを効率的に運用しているかについて紹介するセッションでした。 セッションの中ではたくさんのノウハウの紹介がありました。 大量の Pull Request のデプロイを効率的に管理するためのマージキュー 問題が発生した際に素早く切り戻すための Feature Flag flipper gem パフォーマンスチューニングを行う際の性能比較を行いやすくするためのツール scientist gem データベースレプリケーションを最適化するためのツール freno gem DB再接続の機構と過度に再接続を行わないようにするための Circuit Breaker の仕組み 発表の中で GitHub はモノリスな Rails で運用されていて、コード行数は380万行弱、テストコードは200万行弱という話がありました。単純な引き算をするとテストを抜いた実装に関するコードは180万行弱ということになります。これは現在のタイミーのモノリスの10倍以上ものコード行数です。 モノリスであること自体がスケールのボトルネックになるわけではなく、モノリスを取り巻く周辺環境の整備さえできればこれだけ組織がスケールするという点は大きな励みになりました。 また、発表の中での「GitHub 社に入社後、GitHub が普通の Rails アプリケーションだったことに驚いた」という話も印象的でした。飛び道具を使うわけではなく、実直に目の前の課題に対処することへの重要さを再認識しました。 発表の中でいくつかの GitHub 社のテックブログが引用されていたと思うので、こちらを参考にさせていただこうと思います。 github.blog 心のどこかで遠い存在のように感じていた GitHub が我々の開発の延長線の先にいるように感じられた、そんな発表でした。 (@euglena1215) 新谷が書いているように、GitHub が弊社の開発の延長線上にいるように感じました。 特に scientist の gem は変更に対して堅牢さをもたらしてくれるなと感じているので、結構気になっています。 スライドが公開されたら見返したい。 (@dak2) Writing PDFs in Ruby DSL Cookpad Inc. の Hiromi Ogawa さんによる Ruby DSL で PDF 書こうぜというセッションでした。 github.com 私は前職で thinreports で PDF に出力する項目を修正していたことがありました。 github.com そのため、個人的にセッションの内容が気になっていました。 PDF の構造は知らなかったので、自分にとっては知見だなあと思いつつ自分だったらどう書くんだろうかと思いながら楽しく拝見しました。 業務で使っているツールなどをハックして再実装するなどの試みは、そのツールとの機能差分が比較できてより理解が深まりますよね。 何か Ruby で再実装できないかなあと意識する良いきっかけになりました。 セッションのスライドの PDF 自体も Ruby DSL で自作されていたという最後の伏線回収まで綺麗でお見事だなと思いました(笑)。 (@dak2) Simple組み合わせ村から大都会Railsにやってきた俺は これまで軽量なライブラリの組み合わせによって Web サービスを開発してきた(≒ シンプル組み合わせ村に住んでいた)ところから、フルスタックな Rails を書くようになった(≒ 大都会Railsに移り住んできた) moznion さんによるシンプル組み合わせと大都会Railsの比較を行うセッションでした。 speakerdeck.com 私は業務でちゃんと使ったことがあるのが Rails のみでSimple組み合わせ村に住んだことがないシティーボーイだったということもあり、興味深く拝見しました。 Simple組み合わせ村も大都会も「どちらも正解だよね」と結論だったのですが、Simple組み合わせ村に住んだことがない自分としては適材適所の具体例を一歩踏み込んで聞けると尚良かったなと感じました。 また、これは発表と直接関係ないのですが、Simple組み合わせ村が発達している言語においてはDI(Dependency Injection)も同様に発達しているような印象があり、それは組み合わせの差し替えを容易にするためだったりするんだろうか…と聞きながら考えていました。 (@euglena1215) 私も production 利用のコードベースで触ったことがあるのは Rails のみな都会っ子です。 セッション内容を聞きながら、20年経っても Rails が当初の価値観を崩さず、Rails でいつづけられるのは、Easy であるからこそなのではないかなと思っていました。 Simple 組み合わせ村は「俺の考えた最強のxxxx」が乱立するんだろうなと思いますし、すべてのプロジェクトでそれが通用するかと言われると微妙なところがあると思っています。 Rails は Rails Way という言葉があるようにレールに乗った開発を推奨しているので、同じコンテキストの情報が Web にたくさん落ちていますし、そういった面からもクイックにやりたいことを実現できる素養があって、ここまで使われているんじゃないかなあと考えていました。 (@dak2) Regional.rb and the Tokyo Metropolis 広義の “Tokyo” の地域.rbのオーガナイザーが一堂に会し、色々なことに対してガヤガヤと話す RubyKaigi の Ruby Committers and the World 的な会でした。 東京近辺にこんなに地域.rbがあるのかという驚きが第一印象でした。他の言語のコミュニティにあまり参加したことがないのですが、ここまで地域コミュニティが発展している言語はあまり多くないのではと予想します。Asakusa.rb のようなOSS活動などでアウトプットすることを目的とした地域.rbもあれば、しんめ.rb のような初学者向けの地域.rbがあったりと裾野が広さを改めて感じることができました。 たくさんの地域.rb オーガナイザーたち 私は普段 omotesando.rb に参加することが多いのですが、地域.rbがあることで普段の業務や趣味で取り組んでいることを対外的に発表する機会が生まれ、登壇資料を作る中で自分の中でも理解の整理が進み、発表した内容を元に懇親会で興味ある人とざっくばらんに話すという一粒で三度美味しい経験をしているので、地域.rbのオーガナイザーには感謝してもしきれません。本当にありがとうございます。 (@euglena1215) 三浦半島.rb が立ち上がったのを聞いて Kaigi Effect を感じました! 私は自宅近くの地域.rbにしか参加していなかったので、これを機に他の地域.rbにも顔を出してみようかなあと思いました。私なりの Kaigi Effect です(笑) (@dak2) Keynote “Ruby と Rust と私” Cookpad Inc. の Suzuki Kohei さんによる Ruby と Rust の実装を比較しながら感想を述べていくセッションでした。個人的には最近趣味で Rust を触ってみているので、どういう違いがあるのだろうかと気になっていました。 speakerdeck.com 並行処理の文脈で「Ruby だと工夫が必要なところが、Rust では普通に書くだけで達成できる」という言葉が印象に残っています。メンテなども考えると、この間の距離って強い気持ちがないと埋めづらいなと聞きながら思っていました。 Result 型の question operator によるエラーハンドリング便利ですよねとか、SQLx で DB の row をデータ型にマッピングできるのかとか共感や発見が得られて面白かったです。 余談ですが @euglena1215 が Suzuki さんと ISUCON の Ruby 実装に型を付けたいという話をされたようで、ISUCON の Ruby 実装にも型がつくかもしれません。 (@dak2) 今回のコンセプトは「Ruby と Class」ではなく「Rubyと暮らす」でした。 発表としても業務で Ruby を使っている、というよりももう一歩生活の中に Ruby が溶け込んでいるような印象を受ける発表が多かったような気がします。 RubyKaigi とも Kaigi on Rails とも異なる、アットホームな味のある楽しいカンファレンスでした。東京Ruby会議12のオーガナイザー、ヘルパーのみなさんありがとうございました!
アバター
「エンドユーザーのためのデータ品質向上への取り組みと展望」 というタイトルでファインディさんとクローズドな合同勉強会を実施しましたので、タイミー目線でのレポート記事をお届けします。 きっかけ 以前同じ企業に所属していた両社の社員の間で合同勉強会の話が持ち上がったのがきっかけです。 両社共にデータ品質向上のための取り組みを進めており、参考になることが多いのではということで合同勉強会を開催する運びとなりました。 勉強会当日の流れ 勉強会はファインディさんのオフィスにて開催いただき、タイミーからはデータアナリスト、アナリティクスエンジニア、データエンジニア等のメンバーでお邪魔しました。 各社でLTを行った後、データに関連する話題を中心とした懇親会を行うという流れです。 LTの発表内容 各社からの発表のタイトルと概要についてご紹介します。 ファインディ マルチプロダクトのデータ基盤にデータメッシュを採用した話 / ファインディ ひらきさん データ基盤チームの発足から、データメッシュを採用するに至った経緯や運用についてお話いただきました。特にマルチプロダクトにおけるデータ基盤の運用の難しさや、データメッシュにおけるデータプロダクトをどのように定義するかについて、興味深く拝聴しました。 PII保護のためのDLP運用 / ファインディ tagashiraさん データ権限の管理方針や、DLP(Cloud Data Loss Prevention)を利用した個人情報保護に関する取り組みについて共有していただきました。DLPを使い込んでいないと出てこない課題感が多く含まれており、大変勉強になりました。 タイミー 各発表者からコメントを貰いましたので、合わせてご紹介します。 ユースケースに合わせたデータ品質 / タイミー atshsy タイミーでは、2024年3月にデータ品質の向上を目的として、RDBからデータ基盤へのパイプラインを刷新しました。刷新からしばらく経ち、データ品質について改めて評価したところ、完全性の観点で新たな課題が見えてきました。この発表では、改善に向けて取り組んでいる内容と、ユースケースごとに求められるデータ品質の違いについて共有いたしました。 ユーザーニーズに合わせたデータ鮮度の提供 / タイミー okodoon アナリティクスエンジニアのokodoonです。 複数レイヤーを保持しているデータ基盤の更新を、ユーザーの求める更新頻度で更新されるように全体の更新戦略を構築していく話をしました。レイヤーごとに求められる鮮度の概念が異なることを説明しつつ、dbtコマンドを用いて「どのように最適な更新頻度を実現するのか」現時点で弊社が考えているデザインを発表した形です。 基盤の信頼性を活かした 全社データ活用推進の取り組み / タイミー kuritama データアナリストのkuritamaです。 私からは、データ基盤の信頼性を活かして、全社のデータ活用推進に取り組んでいる話をしました。タイミーでは ディメンショナルモデリング を運用し安定したDWH・データマートを提供しており、そのデータマートへのアクセス手段として全社的にlookerを導入しています。データアナリストは分析業務と並行してlookerの全社活用推進を担っており、勉強会ではその活動内容の一部を紹介いたしました。 懇親会の様子 ファインディさんに食事をご用意いただき、大変美味しくいただきました! 各所で小さなグループが自然発生的に出来上がり、LTの内容に限らずデータに関する課題についての議論が行われていたのが印象的でした。 振り返って 初の取り組みということもあって緊張の面持ちでお伺いしましたが、蓋を開けてみると終了時間を過ぎても話題が尽きず、和やかな勉強会となりました。ファインディの皆様が暖かく迎えてくださったお陰と感じています。 開催をリードしてくださったひらきさん、ファインディの皆様、本当にありがとうございました! 勉強会の内容については、事前に想像していたよりも両社が似た課題を感じていることに驚きました。データ品質に関する課題について、他社がどのように捉え対応しているのかを知る機会は多くありませんので、とても貴重な機会となりました。 他のタイミーのメンバーからは、オープンな勉強会と比較して話しやすかったという感想がありました。共通の課題について率直に意見交換できるのは、クローズドな勉強会ならではの良さがあったように感じています。 終わりに タイミーでは、今後もこうした形式で他社のデータ関係者と交流を図っていければと考えています。ご興味がありましたら、気軽にお声がけください!
アバター
はい、亀井です。 yykamei という名前でインターネット上ではやらせてもらっています。所属はタイミーです。 今回、 Kaigi Pass という制度を利用して Regional Scrum Gathering Tokyo 2025 (RSGT 2025) にボランティアスタッフとして参加させていただきました。 ShinoP さんと takakazu さんに「RSGT 2025 でボランティアスタッフをやるのですが、スタッフでも Kaigi Pass 使えます?」と相談したところ「いいね!」というお声がけをもらい、晴れて Kaigi Pass を利用して参加することができました。お二人に感謝。 Day 0 RSGT への参加自体は今回が2回目です。2回目の今回はスタッフとしての参加で「なんもわからん」という状態で若干の不安を抱えながら Day 0 を迎えました。 この Day 0 というのは、つまり「前日」の意味ですね。 RSGT 自体は 2025 年 1 月 8 日に始まりますが、その前日からいろいろと活動をするわけです。 Day 0 の日に会場についたところ、誰かから「ここはふわっと始まるんで」と言われ、たしかに、「ふわっと」作業が始まりました。到着時点ですでになんらかの作業をしていたメンバーが数人いたのですが、やることがわからないので観察するしかありません。観察していると、だんだんと「なるほど、これをここに持っていくのね」「ブースの荷物を持っていくのね」などがわかってきますし、「誰か◯◯をやってー」という感じのヘルプ(指示ではない)が発生するので慣れないながらもそういう作業をします。 とはいえ、今回のボランティアスタッフはそれなりの人数がいらっしゃったようで、すぐに誰かがなにかしらの作業をしてくれるので、やはり観察する時間が長かったです。 Day 0 のメイン作業といえばノベルティーの準備でしょうか。参加した方はわかるかもしれませんが、トートバッグみたいなものがそれぞれに配られます。 その中にステッカーだったりペンだったりが含まれていますが、そうした内容物をトートバッグに入れる、という単純な作業を行なっていました。これ、それなりに大変でして、現地参加者の数が結構いらっしゃいますので単純に量が多くて大変です。 ただ、そこはさすがというべきか、スタッフがこの単純作業の中で自分の役割を見出し、途中から流れるようにノベルティーが完成していきました。まさしく自己組織的な何かを垣間見たような気がします。 ノベルティー準備の様子 この単純作業の最中にトラブルもありまして、作業がいい感じにスムーズに進み始めたあとに「これもトートバッグに入れる必要がでましたー」ということになり、その時点まで完成させていたノベルティーをもう一度点検してやり直す羽目になりました。こういうスクラム関連のワークショップ、ありますよね。もう擦られ続けた VUCA というワードですが、こんなところにもその片鱗が見えました。 Day 0 では、夕方以降に Speakers Dinner というものが開催されました。こちらは登壇者とスポンサーの方々、そしてスタッフが和気藹々と話す場です。やはりここも「ふわっと」始まります。ただし、会場を借りている時間の関係もあるので終了時間はきっちり守ります。それが終わったら各々二次会などに行っているようでした。 Day 1 そして、いよいよ Day 1 を迎えます。ここからが本番ですね。 Day 1 での主な私の役割は Room B での部屋付きです。 The way through data to quality “Measuring Quality” と Untangle your team with a new approach. (プロポーザルの段階では "A new innovative way to manage the complexity of a team.” でしたが、タイトルを変更したようです))という二つの登壇を担当しました。 これらの登壇は海外から来てくれたスピーカーがやってくれたので、メイン言語は英語でそれを日本語に通訳してくれます。質疑応答についても日本語で質問をすれば英語に通訳され、英語で質問されれば日本語に通訳される、という感じで通訳の方々が大活躍する現場です。 また、通訳の皆さんは別の部屋で行うので、もしスピーカーや質問者がマイクを使わずに話してしまうと通訳の方々にオリジナルの音声を届けられず、部屋付きは意外と気を抜けません。当初は「のんびり登壇を聞いていようか」ぐらいに思っていたのですが、部屋付きに入ると「これは参加者や登壇者の満足度に影響しそうだ」ということに気づきました。 そのため、マイクに音が入っているか確認しながら会場の様子に気を配る、ということをしたので、登壇内容はほとんど理解できませんでした。あとで録画を見ようと思います。 Untangle your team with a new approach. という登壇をしてくれた Teemu に同じ部屋付きだった松下さんが「Last name はどうやって発音するの?」と聞いていてさすがだな、と思いました。その問いに対して Teemu は「そうなんだよ。みんな、このスペルからは発音の仕方がわかる人はなかなかいないよね」的なことを回答していました。部屋付きだと登壇者とこういう会話ができていいですよね。 Day 2 Day 2 も引き続き部屋付きでした。この日の担当は Cowtopia: Designing Agility Through Playful Innovation で、こちらはワークショップになります。 1 時間 40 分の時間を使ったワークショップでクネビンフレームワークを体験してチームを改善していくためにはどうすればいいのか?というような内容だったように思えます。部屋付きで見ていると参加者同士で交流をされていてとてもよかったですね。登壇を聞くのもいいですが、ワークショップに行くと半ば強制的に人と交流することになってそれはそれで Gathering ができてよいのではないでしょうか。 Day 2 にもなってくるとスタッフ業もだんだん慣れてきてようやく廊下を楽しむ、ということができるようになってきました。ただ、人と話している時にもトランシーバーの音声に注意していたので 100% 会話を楽しめたか?というとそこは課題がありそうです。 次回、チャンスがあればいかにうまくトランシーバーを扱っていくか?ということを研究したいと思います。そういえば、 Day 2 で正義さんに「30 秒ピッチやりたいんですけどどこに行けばいいですか?」と質問され「え!なにそれ!?」という感じで Confengine を見たところたしかに “Lunch Time (with 30 seconds pitch from anyone @ Terrace Room)” というのがあり、あわてて実行委員の永瀬さんに聞きに行きました。 そしたら永瀬さんも「え!なにそれ!?」という感じで急遽連携を取り合ってなんとか 30 秒ピッチを開催することになりました。スタッフの新さんがきっちり 30 秒はかって、肉声でドラを叩くということをやってくれて急ごしらえの割には面白いコンテンツになったのかもしれません。 Day 3 Day 3 はメインホールで Open Space Technology (OST))を行い、事前に募集している人たちは別の部屋でワークショップに参加する感じです。この日は特に私に役割があったわけではないのですが、とりあえず、 OST の部屋で入り口付近にみなさん座りがちだったので、奥のほうに誘導していました。 OSTが始まるときの様子 — 円形に並んで座ります OST は始まるタイミングでは全員が円形に並んで座ってインストラクションを聞いて、各自持ち寄りたいトピックを書いていくのですが、その円形の並びがどうしても手前に偏ってしまったんですね。その偏りの修正をやっていました。 OST が始まる前に OST の寸劇があったのですが、私は廊下にいたので見られませんでした。あとで録画を見て楽しもうと思います。 OST では相変わらず熱量を持った参加者がテーマを持ち寄り、いたるところで活発な議論が行われていたようです。熱量が凄すぎてその日は寒かったはずですが、会場内は熱気が凄かったですね。私はなんとなくフラフラしていたのですが、途中から furoshiki.fm のテーマについてのテーブルにお邪魔しました。実はリスナーなのですが、ここぞとばかりに「お世話になっております」というフィードバックを出させていただきました。これが役に立つのかわかりませんがこれからもファンでいさせていただこうと思います。 Day 3 の OST とワークショップが終わるといよいよクロージングキーノートです。本間さんのお話です。最初の導入部で、現在、本間さんがされている子供たちとのお仕事の話をします。「ホンダの話じゃなかったっけ?」と思ったのですが、この導入部がないと実はホンダでのワイガヤの話がすっと入ってこないんです。ある意味焦らすような感じなのですが、全体を通して振り返ってみると「めちゃくちゃいい話だな!」という感想になります。さすがです。 ホンダの話になってくると City の開発の裏話?的なお話がされますが、とにかく「ワイガヤ」というのがキーワードですね。そして、本間さんは最後のほうで RSGT の OST に「ワイガヤ」を見出していただいたようで、ちょっと嬉しいですよね。そういえば、先日 Slack の Huddles を使ったプラクティスとその背後にある考え という記事を書きましたが、このワイガヤにも通じるな、と一人でにっこりしておりました。 まとめ ということで、つらつらとボランティアスタッフとしての RSGT 2025 を書いてみました。ボランティアスタッフだとあまり RSGT を楽しめないかも?などと思っていたのですが終わってみるともう一度やりたいな、という気持ちになっています。 スタッフとしての仕事に慣れてくるとだんだんカンファレンス全体を俯瞰して見られるようになりわずか 4 日ながら自分の成長を実感しました。こんな感じで正統的周辺参加をしてコミュニティーに関わっていくのか、と実感しております。 オーガナイザーとスタッフの皆さんで最後に記念撮影
アバター
こんにちは。タイミーのデータアナリティクス部でデータアナリストをしている亀山です。担当業務としては、主に営業部門のデータ分析を行っています。 今日はA/Bテストと※DIDの効果検証がより信用できるように仕組みづくりをした話を紹介したいと思います。 ※DID(Difference in Differences、差分の差分法)は、ある施策の実施前後で、影響を受けたグループ(比較群)と影響を受けなかったグループ(対照群)の変化を比較することで、介入の効果を推定する手法です。 取り組んだ背景 以前同じ部署の夏目さんがブログで取り上げている通り 、データアナリティクス部では過去に、効果検証の事前設計と結果を管理できるような仕組みを作成しました。その後、効果検証のテンプレートは多く使用されるようになり、知見も貯まってきましたが、残課題としては以下がありました。 汎用性の高いシンプルなテンプレートを作成したため、A/Bテストなど特定の手法を駆使した効果検証では入力項目に不足がある 特定の効果検証の手法も積極的に使っていきたいが、手法に関する知識が人によって差があるため、効果検証のクオリティにばらつきが出てきてしまう やったこと 今回は、これらの課題を解消するために、よく使われる効果検証の方法論のドキュメント化とその手法に特化したテンプレートの作成を行いました。 よく使われる効果検証の方法論のドキュメント化 社内でよく使われる効果検証としてA/BテストとDIDがあげられたため、それらの方法論のドキュメント化を行いました。 ドキュメントは以下のようにNotion上にデータベースを作成して、A/BテストとDIDが一覧ですぐに参照できる形式にしました。 A/BテストとDIDの方法論のデータベースの一部 これらのドキュメント整備で工夫した点は2点あります。一つ目は概論や設計方法だけでなく、社内事例を参照した解説まで記載したことです。ただの効果検証手法の説明であれば、ネット検索をすれば参照できますが、社内事例も含めることで、タイミーで行う効果検証だからこそ、考慮するポイントなどを記載しました。 二つ目は社内事例の解説の箇所で、Pythonなどのサンプルコードも記載したことです。説明だけでなくサンプルコードも載せることで、コードの転用につなげ、作業効率の向上を図りました。 特定の手法に特化したテンプレートの作成 A/BテストとDIDのそれぞれの手法に特化したテンプレートも作成しました。背景でもお話しした通り、それまでのテンプレートは汎用性が高くシンプルな形式でしたが、今回作成したテンプレートはそれぞれの手法ならではの入力項目を追加しました。 例えば以下の画像のように、A/Bテストならば「Randomizeの単位」や「割り当てタイミング」などA/Bテストで考慮すべきポイントをテンプレに含ませるようにしました。 まとめ 新しいテンプレートを作成してから2ヶ月弱経ちましたが、従来のテンプレートに加えて、今回作成したテンプレートも使ってもらえているようです。今後の展望としては、他の効果検証の方法の追加や既存のドキュメントのブラッシュアップも行っていき、より信用できる効果検証を行える仕組みを作っていきたいです。 また、個人的な感想ですが、私はDIDを担当することで、今までの知識の整理ができてとても良かったです。知識は入れるだけでなく、外に出すことも必要だなあと思います。今後もインプット・アウトプットを繰り返すことで、自身の分析スキルも引き上げていきたいです! We’re Hiring! タイミーでは、一緒に働くメンバーを募集しています。 https://hrmos.co/pages/timee/jobs カジュアル面談も実施していますので、少しでも興味を持っていただけましたら気軽にお申し込みください!
アバター
タイミーのプロダクトマネージャー(以下、PM)の 柿谷( @_kacky )、 高石( @tktktks10 )、吉池、大歳 です。 今回は、タイミーの「 Kaigi Pass 」制度を利用し、12/6にオンサイト開催された プロダクトマネージャーカンファレンス (以下pmconf)のDAY2に参加してきました。 この制度を通じて、非常に有意義な学びを得ることができましたので、その内容を共有します。 2024.pmconf.jp productpr.timee.co.jp プログラム概要 パネルディスカッション テーマ:「プロダクトマネージャーと仮説/戦略」 登壇者:FoundX 馬田さん、Zen and Company 宮田さん OST(Open Space Technology) 参加者持ち込みテーマでのディスカッション 懇親会 他社のPMと交流するネットワーキングの場 パネルディスカッションからの学び 「プロダクトマネージャーと仮説/戦略」というテーマで行われたセッションでは、戦略の現状や仮説構築におけるポイントが深く掘り下げられました。 戦略のコモディティ化 宮田さんが指摘した「戦略のコモディティ化」という現状に対し、差別化の重要性が議論されました。タイミーにおいては、プロダクトマネジメントや戦略を担う立場でもあるため、プロダクトマネージャーがどこにエッジを立てるべきかを再考するきっかけとなりました。 実はSaaSって、国内だと取れる戦略少なくて、4パターンぐらいに集約されるんじゃないか。 ・会計、人事や契約レビューや契約書管理などは垣根を超えて、Multi-Horizontal化 ・Verticalは参入障壁高くて、じっくりAll-in-Oneを作り込む… — Yoshitaka Miyata / 宮田善孝 (@zenkou_1211) 2024年7月22日 仮説構築の重要性 宮田さんは、仮説は地道なインプットを重ねた結果として生まれるものであり、見栄えの良さを求めるべきではないと強調していました。また、日本のプロダクトマネージャーが海外事例を十分に収集せず、国内のプラクティスに偏っていることや、議論テーマが数年間変化していない点に警鐘を鳴らしていました。 近年は、ChatGPTのようなツールやX(Twitter)を活用して、海外の著名なPMの投稿や事例を簡単に収集できる環境が整っています。 私自身も、海外事例を意識的に取り入れる姿勢を持ちたいと感じました。 AI経済の構造改革 馬田さんからは、書籍 『AI経済の勝者』 のフレームワークを用い、AIを活用した3つのソリューションレイヤー(ポイントソリューション、アプリケーションソリューション、システムソリューション)についての説明がありました。特に、リスクを取りながらシステムソリューションレイヤーに挑戦し、構造的な変革を進めることの重要性が強調されていました。 AIを前提とした新しいルールや戦略については、まだ理解が浅い部分も多いため、該当の書籍を読み、知識を深めたいと思います。 productpr.timee.co.jp OSTでのディスカッション OSTセッションの開始に先立ち、pmconfスタッフから進行方法の説明がありました。その後、会場から話したいテーマを募り、16の分科会に分かれて合計3回のディスカッションが行われました。 それでは、実際に、我々が参加したOSTで取り上げられたトピックに触れたいと思います。 プロダクトマネジメントに関する海外事例や書籍について知りたい このテーマでは、パネルディスカッションで取り上げられた海外事例をもとに議論が行われました。プロダクトマネジメントにおけるプロセスや役割の定義など、海外事例をどのように参考にし、実践しているのかが話題に上りました。しかし、参加者の環境や事業フェーズ、提供するサービス内容が異なるため、議論は各自の事例紹介にとどまりました。 さらに、プロダクトの海外展開時におけるPMの役割や、プロダクト開発体制、国内と海外のカルチャーの違いへの対応についても議論が広がり、非常に興味深い内容となりました。 LLMの活用事例 「LLMの活用事例」では、以下の点について議論が行われました。 AI投資の方向性 : トップダウンで進める企業が多い一方、エンジニア主導でボトムアップに進めるケースも見られました。特にアーリーフェーズでは投資家の影響が強く、AIへの投資は手段が目的化しているように見える場合もありました。 ROIの課題 : ROIの可視化が難しい中でも、AIへの投資は不可欠であるという意見が多く出ました。 個人的には、どんなユースケースで活用できるのか考える営みは、プロダクトマネージャーとして最低限要求されるべきなのかなと思いました。改めて、自社のプロダクトマネージャーや戦略部門のメンバーとも議論してみたいなと思いました。 リテンション施策の効果検証 「リテンション施策の効果検証がしづらい」というテーマでは、多くのPMが同じ課題を抱えていることが分かりました。他社のPMからは、データアナリストが充実している弊社の環境について羨ましいという声をいただき、自社の強みを改めて実感しました。 一方で、効果が測れない施策については、測定の努力を続けながらも「やるべきことを決める仕組み」が重要だと考えています。 BtoBtoCのプロダクトはCにどのように向き合うか 多くのBtoBtoCプロダクトにおいては、キャッシュポイントがBにあるためにCに対する体験改善がどうしても後回しになってしまう課題感について各社の意見交換が行われました。 各社の知見を寄せ集める中で「Cの声を現場までインタビューしにいく」ようなすぐできるアプローチから「Cの体験が良くなることでKPIが達成され、売上があがるようなビジネスモデルに変更する」といった根源的なアプローチまで、さまざまなアイデアが生まれました。 個人的には、顧客が満足するポイントとキャッシュポイントの距離の近さは、優秀なビジネスモデルを構築する上で重要な要素の一つだと考えています。今後自分がプライシングに関わることがあれば、意識したいですね。 メンバーへのスケジュール意識を高める PMとしてはスピード感を持ってプロジェクトを進めていきたいと考えているが、その温度感・危機感のようなものがどうもチームメンバーに伝わらない….。そんな課題について掘り下げるグループでした。 会話の中では「そもそもどうしてスケジュール感が必要なのか」といった課題の発端について意見交換が行われ、 経営陣がプロジェクトの進捗に不透明さを感じており状況を把握したいため 顧客の不便を早期に解消したいため 確定申告など、1年に1度しか訪れない外部イベントに間に合わせる必要があるため など、多種多様なきっかけがあることが分かりました。 基本的なアプローチとしては、PMが情報を包み隠さずチーム全体で情報の非対称性を減らしていくことが大事そうです。しかし、元の課題によっては単にチームの計画をオープンにしたり、開発する機能のスコープを削ったりと、スケジュール意識を高める以外の解決方法もありそうだと感じています。 非エンジニアPMの生存戦略 「非エンジニアPMの生存戦略」では、以下の点について議論が行われました。 活躍するために必要なケイパビリティについて まずは圧倒的に自身の強みであると言えるだけのスキルや経験、ドメイン知識を獲得すること。その自身の強み=ケイパビリティが顧客価値の拡大に寄与する実績を積み上げて深さを出すこと。そのあとに、抽象化と転用でケイパビリティをさらに広げていくと良い、という意見がその場の総論となりました。 結局、エンジニア経験を積むべきか? 主張の一つとしては、見積もりに対する妥当性評価をするためにも一定のエンジニア経験は積むべきという意見がありました。 しかし、その意見は内製の開発組織ではなく、発注元企業の企画担当と請負または準委任の受託企業の関係性を前提としているものでした。ビジネスパートナー関係による利害関係が発生する場合についてであったため、PM とエンジニアが同じ顧客に向かって利害関係なく動く組織においては、必ずしもエンジニア経験は必要ではないといった対比の意見も出ており、開発体制や文化、事業フェーズなどによるという結論となりました。 PM組織の構造 「PM組織の構造」では、以下の点について議論が行われました。 事業部制における PM 組織のあり方 事業部長がトップにいる中での PM 組織におけるレポートラインの妥当性 (CPO といった機能職種としてのトップより PL 責任を持つ事業部長のキャリアしか道がなさそう)と、その組織のマネージャーの振る舞いをどうするかといった議論がなされました。 事業フェーズや経営上、事業上の課題により適切な組織デザインが思考されるべきで、一義的なものはないという意見が多く出ました。 懇親会での交流 懇親会では、他社のPMとリアルに交流する機会がありました。Xで知っていた方々と直接話せたのは特に有意義でした。数年ぶりに再会する方もちらほらいたりして、楽しい時間でした。 異なる環境や課題を持つ企業の話を聞くことで、自社の環境や課題を相対的に捉え、視野を広げる良い機会となりました。 最後に 今回のカンファレンスを通じて、多くの刺激を受けると同時に、自分自身のキャリアや現在の課題について深く考える時間を持つことができました。特に、大量のインプットを通じて質の高い仮説を構築する重要性や、AI時代におけるプロダクトマネージャーの役割について再認識しました。 Kaigi Passを活用して得たこの学びを、今後の業務にしっかり活かしていきたいと思います。
アバター
イベント概要 12月17日(火)にpmconf 2024のサイドイベントとして「Re:cycle〜pmconf 2024編〜」を開催しました! このイベントはReject Conライクなイベントとして通過しなかったプロポーザルをアップデートして発表することをコンセプトにIVRyさん、DMMさんとの共催で開催しました。 今回はこの勉強会からタイミーのプロダクトマネージャーである大嶋さん( @ta0o_o0821 )の発表を書き起こしイベントレポート形式でお伝えします。 オープニング お品書き 自己紹介 はい、それでは私のほうから15分ほどお時間をいただいて、「Go See! で見つけるプロダクト開発の突破口とその実践法」というテーマでお話しします。まずは自己紹介から入ります。 私は大嶋泰斗と申します。株式会社タイミーでプロダクトマネージャーをしています。入社は昨年の6月頃で、今ちょうど1年半ほどPdMとして関わっているところです。 バックグラウンドや職歴についてお伝えすると、これまでLINE、リクルート、そして現在はタイミーと、ずっとtoCサービスに携わってきました。個人的に、身近なユーザーや想像しやすい人たちの幸せや喜びにつながる体験をつくることが好きで、学生時代のインターンも含め、一貫してtoC領域でやってきました。キャリアの中ではデザイナーをしていた時期もありますが、基本的にはプロダクトマネージャーとして積み上げてきた形です。趣味はカメラ、漫画、料理などです。 会社紹介 会社紹介は簡単にしておきます。タイミーは、「働きたい時間」と「働いてほしい時間」をマッチングするスキマバイトサービスです。一般的な求人媒体型サービスと異なり、実際の稼働や労務管理、企業への支払いまでプロダクト上で完結します。単にマッチングで終わらず、その後のワーカー行動や稼働データ、時給とマッチング率の相関など、多面的なデータ分析や改善が可能である点が面白いところです。 データだけに頼った意思決定の失敗 では本題に入りましょう。先ほど別のセッションで「徹底的にやる」という話が出ていましたが、私がお伝えしたいのは「Go See!」、すなわち現場に足を運んで顧客について学び、プロダクト開発を前進させるアプローチです。 いきなりですが「顧客を完全に理解している」という方はなかなかいないと思います。データ分析が一般的となり、ファネル改善や需要予測、バナー最適化などはデータドリブンで効果を出しやすい領域です。しかし、アナログな現場や複雑なオペレーションが絡むと、データ分析だけでは行き詰まってしまうことがあります。 例として、マクドナルドの「サラダマック」を挙げました。当時、健康志向が高まっているデータから発想された商品ですが、実際の顧客はマクドナルドに来たらビッグマックやポテトといったジャンクなものを求めることが多かったため、売れ行きは伸びず撤退する結果となりました。データは有用ですが、それだけでは顧客心理を完全には掴めないケースがあるわけです。 出勤簿プロジェクト このような状況はタイミーでも起きました。その一つが「出勤簿プロジェクト」です。 物流企業の倉庫などでは1日に50~100人規模でワーカーが来ることがあります。誰がどんなスキルで、何回目の勤務なのか、持ち物は何が必要かなどを把握し、スムーズに受け入れたい。 しかし従来は情報が断片的で、それらを手作業で集約し、1日30分ほど準備に時間をかけていました。この「受け入れコスト」が利用拡大のボトルネックになっていたのです。 一見すると、管理画面で情報をまとめれば解決しそうに思えます。しかし社内外でヒアリングをすると、「現場にPCを持ち込めない」「自作ツールを使いこなしている拠点がある」「拠点ごとにオペレーションが異なる」「そもそも1日の定義が違う拠点もある」といった複雑性が次々と判明しました。「管理画面で見せればいいのか? 印刷したほうがいいのか?」といった基本的な方針すら分からなくなり、ソリューションが見えなくなってしまったのです。 Go see で得られたインサイト そこで活用したのが「Go See!」、つまり現場へ足を運んで直接観察することです。 東京、名古屋、栃木、群馬など各地の物流倉庫を回り、ワーカー受け入れの様子や顧客とのやりとり、働く現場を半日から1日かけて観察しました。 センター長、人事部長、現場責任者、受け入れ担当者などにも直接ヒアリングを行い、実態を把握します。 その結果わかったことが、やはり「紙が必須」という点でした。 拠点によっては様々な派遣媒体からワーカーを呼び、テーブル上に各媒体の出勤簿を並べて即時にメモを書き込む必要があります。 ロッカーキー番号や体調不良、直前キャンセルなど、想定外の事態が常に起きる中、PCを開いて操作する余裕はありません。紙ならその場で書き込みが可能で、柔軟な対応ができるのです。 また、とある別のPoC検証をしていた拠点はなかなか利用に至っておらず、ヒアリングしてみるとCSVの列の追加や削除の作業をなくしたいとか フィルターする手間をなくしたいとか、色々なお声をいただきました。 元々30分掛かっていた作業が5分程度で完了する様になったので、それで良いだろうと思ってましたし、何故、5分掛かることで利用に至らないのか理解が出来ていませんでした。 でも、実際に現地で作業しているのを観察してみると色々なことが分かりました。 朝8時から8時45分までの間に、電話対応、他社派遣スタッフへの対応、書類分け、上司の依頼業務、さらには100人規模で来るタイミーワーカーの点呼準備まで、1人でマルチタスクをこなしていました。たとえCSV整理が数分短縮できても、その数分すら大きな負担になるわけです。こうした状況を見なければ「なぜ利用されないのか」が分からなかったでしょう。 小さな一歩で大きな成果を得る 現地訪問をしなければ、未利用の原因は不明なままで、開発は進まず手詰まりになっていた可能性があります。 表面的な対応に終始し、ギャンブル的な改善を繰り返すことになったかもしれません。しかし、実際に現場を見て理解を深めることで、真の課題が発見でき、確かな改善へとつなげることができました。 ここで改めて指摘したいのは、ユーザー行動はあらゆる要因に左右されているということです。 プロダクト上のデータはクリックや検索、閲覧履歴といった動作しか示しませんが、その背後には割引キャンペーンや地域の習慣、友人からのおすすめなど、データ化されない影響要因が無数に存在します。 現地訪問をすることで、そうした背景に目を向けられます。 まとめとして、「Go See!」は小さな手間で大きな成果を得られるアプローチだと言えます。数字やデータでは見落としがちな現場固有の環境や不便さを直接観察することで、プロダクト開発の盲点を補完し、成功へと近づけます。また、チーム全体で顧客理解を共有すれば、その後の開発スピードや質が大幅に向上します。私たちのチームでは、エンジニアやデザイナー、入社直後のメンバーにも必ず現場に行ってもらい、全員が同じ高い顧客解像度を得るようにしています。その結果、スクラム開発でプロダクトオーナーとしての私が手離れできるほど、チーム自律的に開発が進むようになりました。 注意点として、Go See! は定性的アプローチなので、N=1的なバイアスをはらみますし、顧客特有の問題に左右されがちです。 そのため得られたインサイトは、小さなPOCを通じて検証し、さらにデータ分析を組み合わせることで、より確度を高めることが大切です。 最終的なリリース後はデータ分析を最大限活用して、価値の最大化をスピーディに図ることが望ましいでしょう。 以上で、「Go See! で見つけるプロダクト開発の突破口とその実践法」のお話を終わります。 ご清聴、ありがとうございました。 現場に足を運ぶプロダクト開発に共感する人は是非お話しましょう! product-recruit.timee.co.jp 求人はこちら! hrmos.co
アバター
目次 目次 はじめに RBS について rbs-inline と Steep について RBS に出会ってからの Ruby への向き合い方 単一の型を返す意識がついた メソッドの戻り値の型だけを見て実装する機会が増えた Ruby で型を書くのも良いなと思った はじめに こちらは Timee Product Advent Calendar 2024 の24日目の記事です。 前日は @beryu の iOSの職能チームが存在しない組織で、WWDCハッカソンを企画・開催しました でした。 こんにちは。バックエンドエンジニアの @dak2 です。 タイミーではバックエンドの Ruby on Rails アプリケーションに型定義情報(RBS)を記述して運用しています。 もう少し具体的に話すと、rbs-inline を利用して RBS ファイルを生成し、Steep によって型検査しています。後半で詳しく説明します。 自分はタイミーに Join するまで RBS に触れたことがなかったので、Ruby で型を記述するという体験が新鮮でした。 型を記述して運用する中で Ruby への向き合い方が変わってきたなあと感じているので、今回はそれを文字に起こしてみたいと思います。 RBS について RBS とは Ruby のプログラム構造、いわゆる型を記述する言語のことを指します。 Ruby ファイルとは別に .rbs という拡張子のファイルにクラスやモジュールの型を記述します。 class Foo def bar (str) "#{ str }" end end class Foo def bar: ( String ) -> String end 簡単な例ですが、上記のように foo.rb のクラスに対して、 foo.rbs ファイルに RBS を書いて型定義ができます。 *詳細は 公式リポジトリ を参照してください。 rbs-inline と Steep について rbs-inline & Steep は、弊社のフルタイム Ruby コミッタである soutaro が開発している gem です。 rbs-inline はコメントとして型を記述できます。このコメントをもとに RBS ファイルが生成されます。 下記左のようにメソッド上部にコメントを追記して、 rbs-inline コマンドを実行すると右のような RBS ファイルが生成されます。 class Foo # @rbs (String) -> String def bar (str) "#{ str }" end end class Foo def bar: ( String ) -> String end *詳細は 公式リポジトリ を参照してください。 Steep は Ruby の型検査器であり、実装と型宣言に矛盾がないかをチェックします。 上記のような RBS を記述した上で steep check コマンドを実行すると型チェックをします。 Steep は VSCode での拡張機能もあり、関数ホバー時に型定義を教えてくれたり、メソッドの補完や型エラーになっているコードを赤の下線で示したりしてくれます。 メソッド補完 未定義メソッドのエラー *詳細は 公式リポジトリ を参照してください。 ちなみに、弊社バックエンドのテックリードである shintani が Steep のエラーリファレンスをまとめた 記事 があるので、興味がある方はご覧ください。(自分もちょくちょく見ています) RBS に出会ってからの Ruby への向き合い方 自分の所属している Working Relations Squad というチームでは Done の定義の一環として、インクリメンタルな差分に対しては必ず rbs-inline を記述するようにしています。 *Done の定義の話について詳しく知りたい方は、 こちら の記事を参照してください。 rbs-inline を記述して型を意識することで、Ruby への向き合い方が変わったなあと思う点を下記に挙げてみました。 単一の型を返す意識がついた メソッドの戻り値の型だけを見て実装する機会が増えた Ruby で型を書くのも良いなと思った 単一の型を返す意識がついた 自分は戻り値の型が単一の方が扱いやすいと考えています。 その方が呼び出し側で戻り値の型ごとにハンドリングしなくて良いし、メソッド自体の再利用性も高まると思っています。 rbs-inilne を書くまではぼんやりとそういった意識はあったものの、あまり意識できていなかったように思います。 rbs-inline を書くことで、自分が追加したメソッドの型を定義するようになり、自然と戻り値の型がどうあるべきかに対して明確に思考が向くようになりました。 メソッドの戻り値の型だけを見て実装する機会が増えた Ruby を記述していると、このレシーバってこのメソッド使えるんだっけ?と思ってメソッドの中身を再度読みに戻った経験はありませんか?もしくはテストを見に行ったり、場合によってはコンソールで Object#class を実行して確かめたりなど。自分は何度もあります。 既存メソッドの中身を確認することはあるのですが、詳細まで深く確認するというケースは少し減ったかなと思っています。 メソッドなどの型情報を主なインターフェースとして捉え、呼び出し側で利用するみたいなケースが増えたなと。 Ruby で型を書くのも良いなと思った 良いか悪いかは別として、「メソッドの戻り値の型だけを見て実装する」ことで、余計な情報を知らずに済むようになってきて、本当に実現したい処理に集中しやすくなりつつあるなと思っています。 この状態は理想的で、やりたいことにフォーカスできると Ruby の高い表現力を活かして素早く価値提供できるんじゃないかと思います。 過去に TypeScript などで型を書いてはいましたが、型の意義が自分の中で腹落ちしていませんでした。デフォルトで型付けしないといけない世界線だとそれが当たり前だったので、Ruby の世界との差分が大きくてうまく解釈できていなかった感覚があります。 ですので、それまでは型を書くというのがただ退屈な作業に感じていましたが、“型のある” Ruby を書くことと、”型のない” Ruby を書く体験差分を通じて、上述したように型の意義が自分の中で腹落ちしてきたなと思っています。 「Ruby に型なんかいらないんじゃないかな?」と思ってた自分が、型の恩恵を受けた世界線はより Ruby の表現力にフォーカスできるんじゃないかと思い直していて、Ruby への向き合い方がまた一つ変わったような気がします。面白いなあ。 これからも型やっていき!の精神で書いていこうと思います。 明日は @naoya の 「【iOS】Live Textを活用した画像解析 です。」お楽しみに!
アバター
これは Timee Product Advent Calendar 2024 の23日目の記事です。 こんにちは。タイミーでiOSアプリを作っている岐部( @beryu ) です。 もうすぐクリスマスですね!月初から我が家のリビングに置いてあるクリスマスツリーもだいぶ見慣れてきました。 さて、今年も例年通りApple社による開発者カンファレンス「 WWDC24 」が夏に開催されましたね。 弊社の体制としては”iOSチーム”という単位のチームが存在せず、プロダクトを機能領域ごとに分割した職種横断チームで機能開発をしています。そのため、普通に業務をする上ではWWDCで得た知識についてiOSエンジニアが積極的に試したりする場はそれほど多くなく、得た知識を非エンジニアの社員に展開するかどうかも含めて個々人に任せられていました。 今年、初めてそれを社内ハッカソンという形で社内にアウトプットする機会を作ったので、それについてまとめたいと思います。 企画したきっかけ WWDC開催期間中、弊社のiOSエンジニアは普段通り業務にあたりつつ、スキマ時間でセッションをキャッチアップする日々を送っていました。 そんな中、ふと思いつきでSlackに以下のつぶやきをしたのがことの始まりでした。 各チームのiOSエンジニアが参加しているSlackチャンネルの実際の書き込み この時点で参加者数の見通しは私を含んで4名。小規模なハッカソンを開催するには十分な人数だと判断し、準備に取り掛かりました。 準備 前述した通り、弊社にはiOSチームという組織は無く、プロダクトを機能領域ごとに分割したチームで働いています。 したがって、一介のエンジニアがハッカソン用にリソースを自由に割けるような体制ではなく、ハッカソンに参加する社員が所属するチーム全てから許可を得る必要がありました。 今回のハッカソンは遊びではなく、魅力的なプロダクトを開発するために有効な 仕事 です。 しかし、万が一文脈を知らない方がこの活動を見て「遊んでいるのでは?」と思われたとしたら、それは誰の得にもならない悲しい誤解なので、そのような事態は必ず防がなければなりません。 この準備フェーズでは、そんな事態を防ぐために出来ることを考えながら動くことにしました。 非エンジニア向けの資料作成 まず、ハッカソンにかけたコストに見合うリターンが得られる論理を明文化しました。 誰かに資料作成を命じられたわけではありませんが、業務に割く時間を削って行う活動ではあるので「納得してもらえる材料があるに越したことはないだろう」という考えで用意しました。 ドキュメントには社外秘の情報を含んでいるので詳細はお見せできないのですが、主に以下のような内容を含めました。 目的 タイミーが今WWDCハッカソンをやる意義 期待されるアウトプット ハッカソンのレギュレーション(ここ3〜4年のWWDCで発表された技術であれば何でも使って良い) ハッカソンで消費するリソース量 iOS Chapterでの相談 弊社にはiOSチームが無い代わりに、各チームのiOSエンジニアが横軸で繋がるコミュニティのような存在として”iOS Chapter”という仮想組織が定義されています。 iOS Chapterでは毎週定例会議を開催しており、その場でハッカソンの開催形式や所要期間について相談しました。 各々が別チームに所属しているのでそれぞれに事情があり、直近での同期形式での開催は現実味が薄いと判断し、以下の枠組みで合意しました。 1ヶ月弱の期間中で2営業日を割く 任意参加扱いとする マネージャーとの交渉 エンジニアリングマネージャー陣の定例会議の場で時間を拝借して、上述の資料をひっさげて開催させてほしい旨を交渉しました。 交渉とは言っても、私がこの話題を出した時点でその場が応援ムードになり、特に咎められることもなくスムーズにOKを頂けました。 自部署の全体定例での展開 共に現場で働くエンジニア・デザイナーの皆さんにも納得してもらった上で開催したかったので、対象のメンバーがほぼ全員集まるAll Handsという定例会議でも上述の資料を用いながら告知も兼ねて紹介しました。 ここでのリアクションも”わいわい”という形容がピッタリの、良い盛り上がりを感じるものでした。 ハッカソンの開発期間 参加者は定められた期間中の2営業日を使って、ハッカソンのための調査・開発を行いました。 このタイミングでも参加者はFixさせていなかったので、参加予定だった社員が途中で業務都合で参加できなくなったり、逆にこれまで参加表明していなかった社員が参加してくれたりすることもありました。 あくまで通常業務が優先であることを鑑みてこの形にしたのですが、結果的にはこの形にして良かったなと思っています。 成果発表会 弊社は(私も含め)リモートワークのメンバーが多く所属している事情もあり、成果発表会はオンラインで行いました。 発表順はその場でシャッフル関数で決めました 成果物 App Intentsの概要・実装方法の調査報告 発表の場では、WWDC24で発表されたApple Intelligenceとの統合を見据えながらApp Intentsの概要を説明しつつ、iOS版タイミーアプリにApp Intentsを組み込んだ例を実際のソースコードを添えて紹介していました。 タイミーのアプリ内部の構造が深く絡む内容だったのでここでは資料をお見せできないのが残念なのですが、App Intentsの実装の手軽さ、身近さを感じられる発表でした。 Live Activityによるリッチな通知機能の実現 これが私の発表です。 WWDC23で発表されたプッシュ通知経由での利用例を中心に、iOS版タイミーアプリにLive Activityを組み込んだらどんな体験になるかを紹介しました。 実際にデモで使用した動画は以下からご覧頂けます。 ※ターミナル画面にはぼかし加工を加えています www.youtube.com パスキーによる認証の導入 現在タイミーで採用している電話番号認証とは別の認証方法として、パスキーを紹介していました。 認証はセキュリティが重要な要素になるので技術的な話題も多い発表で、正直なところ聞くだけでもやや難易度が高い内容でしたが、一方で動作デモの場面ではシンプルでわかりやすいUXに聴衆が感動している様子が印象的でした。 この日の発表の中で唯一、実際に操作できるデモアプリを配布できていたことでも大きなインパクトを残していました。 TipKitによるアプリ内ヒントの提供 WWDC23で発表された、機能のヒントを表示するためのフレームワークの紹介でした。 恥ずかしながら私はTipKitの存在を知らなかったのですが、画面の使い方を教えるようなUIを低い実装コストで実現できる、実用的なフレームワークでした。 動作デモでは、普段の開発で「この画面のUI、少しわかりにくいかもしれないね」と話に挙がっていつつ実装リソースを割くことが出来ないままになっていた箇所に、シンプルなコードでヒントを実装する様子が披露され、目立たない存在ながらその有用性を強く実感できる内容でした。 社内からの反応 成果発表会の最中にGoogle Meetのコメント機能で感想を述べて頂いたり、終了後にアンケートにご協力頂いたりして社内の反応を集めたのですが、非常に好評でした! 例えば、以下のような声が挙がっていました。 よかったですーで終わらせたくない内容でした R&Dみが強い感じかと思ってたんですが、すぐにでも導入できそうな形に仕上がっていてすごい iOSを業務で触っていない方がハッカソンに出られるところがスキルとマインドのダブルですごい ※App Intentsについて発表したのは普段アナリストとして業務にあたっている社員でした 取り組みに再現性持たせたいですね 楽しかった! 今日発表された機能のリリースはいつですか? リリースされた機能 嬉しいことに、このハッカソンで披露された成果物のうち実際にリリースされた機能も存在します。 今年タイミーアプリに新設されたものの一つである”バッジ”という機能があります。ワーカー(タイミーを通して働くひと)が働き先で認定されると業務ごとのバッジを獲得でき、そのバッジ情報をもとに更にさらに自分のスキルに合った募集を受けることが出来るようになる機能です。 この機能で獲得したバッジはバッジ一覧という画面で一覧できるのですが、この画面にあるバッジ画像をタップ出来る事に気付けないという声が度々挙がっていた背景があり、そこに今回のハッカソンの成果物の一つだったTipKitを導入することになりました。 この機能はすでにリリースされており、iOS17以上のiPhone上で動作するタイミーアプリでご利用頂けます。 実際のバッジ一覧画面 また、他の発表された成果物に用いられていた機能についても実装に向けてPdMと相談し、優先度を付けて日々の開発に用いているバックログに追加しています。 やってみた感想 ハッカソンを実施するきっかけはSlackでの些細なつぶやきでしたが、周囲の応援もあってどんどん話が進んでいきました。賛同した社員が各々の発想と技術力をもって成果物を作成・発表し、非iOSエンジニアの興味を誘いつつ一部はスピーディにリリースまでされるという、理想的なハッカソンになったと感じています。 実施するにあたって社内のステークホルダーと丁寧に条件を握るステップも踏みましたが、その過程が終始ポジティブな空気だったことから、少なくとも弊社はこのような取り組みを快く思う人間性の方が多く所属しているのだと感じられて嬉しくもなりました。 弊社は組織の人数が日々増えているので開発力の高まりを感じる機会が多いですが、今回のように個の力を感じられる機会も非常に魅力的だったなと感じています。 こういった場を作ることを検討されている方にとって、この記事が少しでも参考になっていれば幸いです。
アバター
Timee Advent Calendar 2024 23日目の記事です。 こんにちは、タイミーでデータアナリストをしている yuzuka です。 先日、統計検定準1級に合格し、最優秀成績賞をいただきました🌸 弊社からは別のアナリストがすでに 統計検定準1級の合格体験記 を出してくれていますが、合格体験記はあればあるだけ良いと思うので、私も書こうと思います。 受験の動機 社会人7年目で文系職からデータアナリストに転向しましたが、やはり専門性で周囲に後れをとっていると感じ、統計の知識を早急に身につけたいと考えていました。 取得した資格は履歴書などにも書けるため、アナリストに転向してから早い段階で取得しておけば、自身のLearnabilityを証明する一助になる、という打算的な動機もありました。 学習開始時の状況 文系職からデータアナリストに転向して2年目 1年ほど前に統計検定2級を取得済み 一応理系の学部卒だが、数学が苦手で、積分や行列の計算はほとんどできない状態に戻っていた 今回は、そんな自分の統計検定準1級対策についてご紹介します。 数学に苦手意識のある方の参考になれば幸いです。 具体的な学習方法 1. ワークブックを写経する(80時間程度) 統計検定準1級の勉強は「日本統計学会公式認定 統計検定準1級対応 統計学実践ワークブック」が主軸になると思いますが、こちらの内容をすべてノートにまとめ直しました(まとめ直すといっても、ほぼ丸写しです)。 ワークブックの内容は、数学や統計の知識が浅い自分には少しハードルが高く、軽く目を通しただけでは内容があまり入ってきませんでした。 このままワークブックを読むだけでは理解が深まらないと判断し、とりあえず手を動かしながら理解を深めることにしました。 ワークブックは300ページ以上あり、大変そうに思われるかもしれませんが、トータルで見るとコスパは良かったと思います。 1日1時間でワークブック3〜4ページ分を目安に、約3ヶ月かけてまとめ終えました。 あくまで自分が手を動かして理解することが目的であり、ノートを見返す必要はないと割り切って、綺麗さよりもスピード重視で進めました。 写経だけで内容を完全にものにできるわけではありませんが、この後の理解の進み方が大きく違ったように思います。 実際のノートの一部です。 2. 統計検定準1級に必要な前提知識のおさらい(10〜20時間程度) 部分積分や行列式、固有値など、ワークブックに出てきたものでわからないものがあれば、ネットで例題を探して解いていきました。 ワークブックに取り掛かる前に予習しておく必要はなく、ワークブックの問題を解くうえでわからないものがあれば、その都度調べる程度で良いと思いました。 最初はギリシャ文字の読み書きも怪しかったため、スマホの待ち受け画面をギリシャ文字の一覧表にしていました。 読み方のわからない文字があると、内容もなかなか頭に入ってこないため、放置しない方が良いと思いました。 3. 問題集を解く(50時間程度) 公式問題集(過去問)とワークブックの問題を2〜3周しました。 YouTubeにワークブックの解説動画を上げてくださっている方がいらっしゃり、大変理解が捗りました( URL )。 試験では1問あたり3〜4分しかかけられないため、普段からスピーディーに解くことを意識しました。 まず問題を見て、どのジャンルからの出題なのか、計算量が多い問題なのかをぱっと見定めることが重要そうです。 本番で使う電卓を問題演習中にも使用し、メモリ機能も含めて使い慣れておきました。 私は こちら の電卓を使っていました。 問題集に対して過学習が起きないよう、ときどきワークブック全体を読み返すようにしていました。 おわりに ここまで、私の統計検定準1級の勉強法をご紹介しました。 公式問題集やワークブックの問題を解くだけでなく、ワークブック全体の理解に力を入れたことが、好成績に繋がったのかなと思います。 試験当日は、なるべく早い時間帯で受験した方がパフォーマンスが良いと思い、11:00開始の枠で受験することにしましたが、これも良かったのかもしれません。 当日の問題群がたまたま自分に合っていた可能性もあります。 1週間空ければ再受験できるそうなので、何回か受験してみるのも手かと思います。 何か一つでも、参考になっていれば幸いです。 ちなみに、弊社では合否に関わらず資格の受験費用を補助してもらえるため、思い切って受験しやすかったです。 We’re Hiring! 私たちは、ともに働くメンバーを募集しています! カジュアル面談も行っていますので、お気軽にお申し込みください 。 タイミー データ職種 採用ページ 個人的にもアナリストやデータ関連職の方と繋がりたいと思っているので、よろしければ X のフォローもよろしくお願いします。(弊社データメンバーとランチ会や合同勉強会のお誘いも……お待ちしております!)
アバター
本記事は、  Timee Advent Calendar 2024  の 12/22 公開分の記事になります。 はじめに 前提:チームトポロジーとは? 組織規模とフェーズにおけるチームトポロジーへの誤解 備えとしてのチームトポロジー グロース期の組織への段階的なチームトポロジーの適用 チーム組成のトリガーを見極める 組成に備えてチームメンバーの志向を理解しておく 最後に はじめに 株式会社タイミーでVPoEをつとめております、赤澤 a.k.a chango( @go0517go )です! 2024年2月に私がタイミーにジョインして以来、タイミーのプロダクト開発組織全体で適用・実践している「チームトポロジー」について、開発生産性Conference 2024をはじめ、様々な場で関連するトピックをお話しする機会がありました。 そういった場でいただく反応やご相談の中に、「タイミーはチームトポロジーを適用できる組織規模で羨ましい」「うちは開発組織を立ち上げてこれから組織をグロースしていくタイミングなので、まだチームトポロジーは取り入れていない」といったお声がいくつかいただきました。 確かに、チームトポロジーは認知負荷への対処方法などを例にとっても、一定以上の規模や複雑性を持つプロダクト開発組織で効果が顕著に現れやすいフレームワークです。しかし、「チームトポロジーを取り入れる=定義された全てのチームタイプを組織内に揃えなければならない」というわけでは決してありません。また、前職のユーザベース、そして現職のタイミーでの経験を通じて、チームトポロジーは今後の事業・組織の成長期に生じる混乱や生産性低下を軽減するための“備え”として非常に有益な概念であると実感しています。 このアドベントカレンダーの記事では、そうしたプロダクト開発組織の立ち上げ期や成長期におけるチームトポロジーに対する誤解を解き、組織の初期段階からチームトポロジーを活用するためのTIPSについて書きたいと思います。 前提:チームトポロジーとは? この記事の前提となるチームトポロジー自体の説明は省略させていただきますが、当該書籍自体はもちろんのこと、本書の共訳者である吉羽 龍太郎(@ryuzee)さんの記事や、弊社CTOの山口(@ZIGOROu)や私の開発生産性Conferenceでの登壇資料などもご参照ください。 【資料公開】30分で分かった気になるチームトポロジー(吉羽龍太郎@ryuzee) 組織をスケールさせるための Four Keys とチームトポロジー(山口徹@ZIGOROu) 実践チームトポロジー:プラットフォーム性とイネーブリング性の戦略(赤澤剛@chango) 組織規模とフェーズにおけるチームトポロジーへの誤解 前述のような場でいただいたご質問やご相談を、規模やフェーズに関する誤解として整理すると、概ね以下の点に集約されると考えています。 エンジニアリング組織が立ち上げ期で小規模なため、チームトポロジーを適用できない(と考えている)。 ストリームアラインドチーム以外のチームタイプ、特にイネーブリングチームやプラットフォームチームを検討できるほどの規模や余裕がない(と考えている)。 冒頭でも記載した通り、チームトポロジーは一定の規模や複雑性を有するプロダクト開発組織で、その効果がより顕著に現れるフレームワークです。しかし、「一定の組織規模がなければチームトポロジーを取り入れられない」と解釈することは本質的ではありません。また、4つのチームタイプ(ストリームアラインド、プラットフォーム、イネーブリング、コンプリケイティッド・サブシステム)を全て明確に揃えることが、チームトポロジーの適用を意味するわけでもありません。 むしろ、組織がまだ小規模な段階から、将来的に必要となるチームの性質を内包しつつ、ストリームアラインドチームを中心に据えた思考で価値創出に集中することこそが、チームトポロジーの有用性を最大限に活かす鍵だと考えています。 参考:チームトポロジーで表現したタイミーのプロダクト開発組織 備えとしてのチームトポロジー 「チームトポロジーが適切に実施できる組織規模で羨ましい!」と言っていただいたことがあり、非常にありがたくも恐縮なのですが、実際のところ私の認識では、一定の規模になったからチームトポロジーが実践できるようになったのではなく、一定の規模と複雑性でも顧客への価値提供を継続強化するために、チームトポロジーなどを取り込む必要性が生じた結果、実践せざるを得なくなったというのが正しい表現かと考えています(私自身は前職・現職を通じてチームトポロジーが好きで、決して否定的な意図はないことをお伝えしておきます)。 顧客価値とプロダクトに向き合い、主に機能開発をリードするストリームアラインドチームのみでビジネス要求に十分応えられるうちは、わざわざ複雑なチーム構造や追加の支援チームを設ける必要はありません。組織がまだ比較的シンプルな状態で、エンジニアリングやオペレーション、デリバリープロセスに関わる領域が一つのチームで把握できる範囲に収まっているのであれば、そのまま突き進んで問題はないでしょう。むしろ、その段階で「チームトポロジーを取り入れなければ」「無理に他チームを設置しなければ」と考えてしまうと、まだ不要な段階で組織構造の複雑化や権限移譲のコストが増大し、スピードや柔軟性を損ねる可能性があります。 問題は、プロダクトや組織が成長し、技術的・ビジネス的な複雑性が増大したときに表面化します。たとえば、以下のようなケースが想定されます。 ストリームアラインドチームの責務や負荷が増大 当初は1つ、あるいは少数のストリームアラインドチームでエンドツーエンドの開発・運用が可能だったが、新機能の追加や顧客数の増加、利用技術の多様化によって、本来集中すべき領域以外への対応(インフラ構築、技術調査、セキュリティ対応など)が増え、コア業務に影響が出始める。 領域の専門性が著しく向上 ストリームアラインドチームが扱う技術スタックやドメインが増え、メンバー全員がすべてを理解し追従することが難しくなっている。高度な機械学習アルゴリズムや極めて複雑なインフラ設定、厳格な法規制対応が求められるセキュリティ領域など、専門性を強く求められる分野が増え、従来の体制では対応しきれなくなる。 新技術・新手法の導入が困難 担当範囲の拡大に伴い、各ストリームアラインドチームが自前で新技術を調査・学習・導入することが難しくなり、新たなスキルやプラクティスの普及が停滞する。 共通基盤整備の必要性 アプリケーションやサービス群が増え、それぞれがインフラやCI/CDパイプラインを独自に整える状況が続くと、重複した作業や不整合が発生し、共通基盤・共通サービスを整備するチームの必要性が高まってくる。 重要なのは、これらのチームタイプの追加や新たなトポロジーは、これまで維持できていた単純性が崩れつつある状態を補完するために必要となるもので、組織規模が大きくなったからといって自動的に投入されるべきものではないという点です。あくまでも顧客への価値提供を止めない、もしくは加速させるための“備え”として、チームトポロジーを「引き出し」に用意しておくという発想が求められます。 言い換えれば、今はまだモノリシックなチーム構造で回せているのなら、それは望ましい状態です。同時に、将来的な成長過程で複雑性が増し、上記のような課題に直面する可能性を見越して「いざ必要になったとき、どう変化できるか」「どのようなチーム間関係を設計すれば、スケール時にも価値提供を継続できるか」といったシナリオを事前に想定しておくことが、長期的な競争力や組織の持続可能性を高める鍵となるのです。 グロース期の組織への段階的なチームトポロジーの適用 ここまでのパートで、発生しうる技術的・組織的な「備え」としてチームトポロジーを活用する方針について記載しましたが、後半ではエンジニアリング組織の立ち上げから拡大していくフェーズにおいてどう段階的に適用してきたか、そしてその際の留意事項や反省点についても書きたいと思います。 チーム組成のトリガーを見極める これは前職でエンジニアリング組織を立ち上げ、人数がおおよそ30名を超えた段階までの話ですが、チームトポロジーの適用、特にイネーブリングチームやプラットフォームチームをどのタイミングで組成するかについては、あらかじめトリガー(条件)を設けていました。 わかりやすい例として、イネーブリング的支援とプラットフォーム基盤整備の両方を担うSREチームを挙げてみます。以下は、覚えている範囲で抽出した定性的な条件例です。 複数のバリューストリームが生まれ、共通的な信頼性向上施策(CI/CDパイプラインの標準化、モニタリング基盤の強化、インシデントレスポンス手順の標準化)が求められる段階になっている。 利用ユーザ数やトランザクション量が増加し、現行体制下でSLO/SLAを維持するための継続的改善・強化が不可欠になっている。結果として、ストリームアラインドチームが本来の機能開発領域以外のシステム運用、トラブルシューティング、パフォーマンス改善などに多くの時間を割く状態が生じている。 組織内に、信頼性向上やオペレーション自動化に強い関心を持つエンジニアが育ちつつあり、共通基盤整備やシステム品質改善に特化して注力できる人材が揃ってきた。 SREチーム組成後、仮にSREチームメンバーが一時的に不在であっても、ストリームアラインドチームのメンバーのみでプロダクトの継続的デリバリーが(1や2のような課題を抱えつつも)自走可能な状態である。 事業やプロダクト面では1や2が特に重要な判断軸となりますが、私自身がSREチーム組成の可否を見極める上で“ノックアウトファクター”と捉えていたのは、4の条件でした。過去に別の組織で、CI/CDやOps領域においてSREチームへの依存が過度に高まり、ストリームアラインドチームが自前で運用しきれない状況に直面した経験がありました。その反省から、「顧客への価値提供をストリームアラインドチームが自走完結できること」を前提とし、その状態を強化・加速するために「Center of Practice」としてSREチームが存在する、という関係性を実現できることが、SREチームを組成する上での重要な前提条件、すなわちトリガーだったのです。 組成に備えてチームメンバーの志向を理解しておく 開発生産性Conference 2024の発表では、 イネイブリングやプラットフォームを「性質」として捉える。 タイミーにおいては、イネイブリングチームやプラットフォームチームを定義しつつも、内部ではQA領域、SREおよびプラットフォームエンジニアリング領域を中心に、イネイブリング性とプラットフォーム性の両面を兼ね備えたチームが存在している(メインの性質がそのチームタイプとなる)。 といった点に触れました。セッションの際には言及しませんでしたが、こうした性質や振る舞いは、当然ながら各メンバーにも志向や特性として内在しています。そして、ここでは「イネイブリング志向」や「プラットフォーム志向」と呼べる個々人の志向は、キャリア開発の観点も含め、チーム組成において重要な判断材料としています。 エンジニアにとって、「仕組みや方法によって再現性を高める」「自分以外の人でも運用可能にする」といった点は、共通して大切にされる価値観だと私自身も考えています。ただ、その中でも特に、インフラやプラットフォーム領域に強みと志向を持ち、複数のプロダクトに対して共通基盤の整備や最適化を行うことに喜びを感じるエンジニアもいます。 チームトポロジーで提唱される「タイプ」や「モード」はあくまでフレームワークであり、最終的には「誰が、どのような価値創出に楽しさや挑戦を感じるのか」という個々の志向やキャリアパスと、組織が目指すチーム構造を結びつけることで、中長期的に継続的な顧客価値提供が可能な組織を形作ることができると考えています。 また、私の過去の経験からの反省点として、イネイブリングチームやプラットフォームチームを立ち上げる際には、ストリームアラインドチームでの所属経験を一定割合以上持つ人材をアサインすることが重要だと強く感じています。そうすることで、ドメイン知識や現場課題の理解度が高まり、表層的なアドバイスではなく、実質的なサポートが可能になります。 具体的な防止策として、タイミーでは次のような点に留意して取り組んでいます。 イネイブリングチームは、ファシリテーションモードでの支援だけでなく、コラボレーションモードを意図的に発生させ、ストリームアラインドチーム内で密に課題解決を行う。 転職入社のエンジニアにイネイブリングやプラットフォーム領域への強い志向がある場合でも、まずはストリームアラインドチームで経験を積んでもらい、現場への理解を深めた上で、本人のキャリア意向と合意を得た上でイネイブリングまたはプラットフォーム領域へシフトしていく。 これらのトピックについては、開発生産性Conference 2024の中でも各領域の事例を交えて言及しています。ご興味がありましたら、ぜひ以下の資料をご覧ください。 開発生産性Conference 2024:組織全体の成長や成熟の過程でインタラクションモードを変化させている事例 最後に 本記事で述べた立ち上げ期でのチームトポロジーの適用は、択一的な正解を示唆するものでは全くありません。その上で私自身の経験として、チームトポロジーを組織の立ち上げ期から理解し、今後発生しうる課題と共に適用パターンを思考し始めることが非常に有用だと感じています。 ストリームアラインドチーム以外のチームタイプが組成されていることが重要なのではなく、自組織が携わる事業やプロダクトの特性を踏まえた上で、今後の課題と組織的な対応策をチームトポロジーに従って想定しておくことがチームトポロジーの組織適用の第一歩だと考えています。 これを読んでくださった皆様に、事例の1つとして参考になる部分があれば幸いでございます。
アバター
この記事は Timee Product Advent Calendar 2024 シリーズ2の21日目の記事です。 はじめに タイミーでフロントエンドエンジニアをしている大川です。 PlaywrightでのUI自動テストを運用してきた中で学んだ、PlaywrightとMock Service Worker(以降、MSW)を組み合わせたAPIのモック方法を紹介します。 Playwrightのテスト中にモックレスポンスを定義する バックエンドと連携しているシステム全体をテストすることで正常に動作しているか確認していくことは大切ですが、普段のUI開発やUIで利用しているライブラリアップデート後の動作確認時などはAPIリクエストをモックしてPlaywrightのテストを実行するのも有効な場合があると思います。 APIリクエストをモックするためのライブラリにMSWを利用している場合は、開発中に定義したモックハンドラーを再利用してテスト実装を効率化できます。 PlaywrightとMSWを組み合わせる場合には、テストシナリオ内でレスポンスを定義するための工夫が必要です。以下にいくつかの方法をまとめました。 テスト対象のUIでMSWが動作している場合 MSWをwindowオブジェクト経由で操作可能にする 参考: Overriding MSW handlers in Playwright (GitHubディスカッションへのリンク) テスト対象のUIがバックエンドと結合している場合 playwright-msw を利用する 参考: valendres/playwright-msw (Next.js App Routerの場合)experimentalとして実装されているtest modeを利用する 参考: Experimental test mode for Playwright MSWのgetResponseを利用する(後述) 自前のフィクスチャを定義する playwright-mswでは、最後に追加されたハンドラーが最初に実行されるよう、すべてのハンドラー定義順序をリバースしている箇所があります。 参考: ハンドラーをリバースしているコード MSWにも実行順序のルールがあり、それを考慮してハンドラーを用意していると意図しないレスポンスになる場合がありました。 参考: Request handler | Execution order playwright-mswを利用せず、MSWのハンドラーをベースにモック定義するために getRespose 関数が利用できます。 参考: getResponse 以下のコードは getResponse を利用してAPIリクエストをモックしているフィクスチャと、フィクスチャの利用例としてのテストになります。 (フィクスチャの実装例) import { test as base, expect } from '@playwright/test'; import { http, HttpResponse, RequestHandler, getResponse } from 'msw'; const apiEndpoint = process.env.API_ENDPOINT; // NOTE: MSWのモックハンドラー定義の例(実際は別ファイルで管理されている想定) const initialHandlers = [ http.get(`${apiEndpoint}/user/:userId`, () => { return HttpResponse.json({ name: 'John' }); }), ]; // NOTE: テストコード中でもモック定義するためのビルダーを定義 class MockBuilder { private handlers: RequestHandler[] = initialHandlers; constructor(...overrides: RequestHandler[]) { this.handlers = [...overrides, ...initialHandlers]; } use(...additionalHandlers: RequestHandler[]) { this.handlers = [...additionalHandlers, ...this.handlers]; } reset() { this.handlers = initialHandlers; } getHandlers() { return this.handlers; } } const test = base.extend<{ mockBuilder: MockBuilder }>({ mockBuilder: [ async ({ page }, use) => { const builder = new MockBuilder(); await page.route(`${apiEndpoint}/**`, async (route) => { const actualRequest = route.request(); // NOTE: MSWとPlaywrightでRequest型が違うので再定義している const request = new Request(actualRequest.url(), { method: actualRequest.method(), headers: actualRequest.headers(), body: actualRequest.postData(), }); const response = await getResponse(builder.getHandlers(), request); if (!response) { await route.fulfill({ status: 404, }); return; } const json = await response.json(); await route.fulfill({ json, status: response.status }); }); await use(builder); // NOTE: テストコード中に上書きしたハンドラーをリセットする builder.reset(); }, // NOTE: auto: trueでmockBuilderをテストコード中に呼び出さない場合もセットアップしておく { auto: true }, ] }); export { expect, test }; (テストの実装例) test('ユーザーIDを編集できる', async ({ page, mockBuilder }) => { // NOTE: 定義済みのユーザー情報取得に加えてユーザーID編集APIをモックする mockBuilder.use( http.patch(`${apiEndpoint}/user/:userId`, () => { return HttpResponse.json({ result: 'OK' }); }), ); // NOTE: テスト開始 const newUserId = 'new_user_id'; await expect(page).toHaveURL('/user/123/edit'); const input = page.getByRole('textbox', { name: 'ユーザーID' }); await input.fill(newUserId); const button = page.getByRole('button', { name: '保存' }); await button.click(); await expect(page).toHaveURL('/user/123'); const userId = page.getByText(newUserId); await expect(userId).toBeVisible(); }); さいごに 今回はPlaywrightとMSWを組み合わせてAPIレスポンスをモックする方法を紹介しました。 PlaywrightもMSWもAPIが充実しており、いろんなテストシナリオに対応できて便利です。 弊社ではPlaywrightでのテスト以外にも静的解析、コンポーネントやフックの単体テスト、ビジュアルリグレッションテストなどを取り入れています。 自動テストを整備することで機能開発とその動作確認、ライブラリアップデートなどの運用をバランスよく実施していける仕組みをつくっていき、お客さまによりよいUI・UXをより早く提供していくことにつなげていければと考えています。
アバター
こんにちは。エンジニアリング本部 プラットフォームエンジニアリングチームの徳富です。 この記事は、 Timee Product Advent Calendar 2024 の 20 日目として、EXPLAINを使用した実行計画の見方についてご紹介します。 背景 タイミーでは、会社の成長に伴い、パフォーマンスチューニングが喫緊に求められています。このような課題に対処するため、クエリのパフォーマンスチューニングには EXPLAIN を使用した実行計画の確認が非常に重要です。しかし、実行計画の解釈には社内でばらつきがありました。この問題を解消するために、実行計画の見方を社内でまとめ、共有することにしました。ただし、この情報を社内だけに留めておくのはもったいないと考え、テックブログを通じて広く公開することに決めました。 実行計画の基本的な見方 MySQLの EXPLAIN 文は、SQLクエリがどのように実行されるかの詳細を提供します。実行計画には、以下のような重要な情報が含まれています: 項目 説明 id SELECT識別子で、クエリが複数の部分から構成されている場合(例えば、サブクエリやUNIONが使われている場合)に重要です。同一のidを持つ行は同じSELECT文に属しています。 select_type クエリのタイプを示します。例えば、 SIMPLE (単一のSELECT)、 SUBQUERY (サブクエリ内のSELECT)、 UNION (UNIONの一部)などがあります。 table クエリが参照するテーブル名。複数のテーブルが結合されるクエリでは、どのテーブルがどの順番で処理されるかを示します。 partitions クエリが参照するパーティション。指定されたパーティションを明確にすることで、クエリの実行効率が向上します。 type データへのアクセス方法のタイプ。 ALL (フルスキャン)、 index (インデックススキャン)、 range (範囲スキャン)などがあります。 possible_keys このクエリで使用可能なインデックスのリスト。適切なインデックスが選択されるかどうかの手がかりになります。 key 実際に使用されるインデックス。このインデックスがクエリのパフォーマンスに大きく影響します。 key_len 使用されるキーの長さをバイト数で示します。キーの長さは、インデックスを使用する効率に影響します。 ref インデックスに使用される列や定数。外部キーの結合や、定数との比較で使用されます。 rows 読み込まれる行数の推定値。クエリのコスト評価やパフォーマンスチューニングの際に重要です。 filtered フィルタ条件によって行がどれだけ絞り込まれるかのパーセンテージ。100%に近いほど、フィルタ条件によるデータの絞り込みが効果的です。 Extra クエリの実行に関する追加情報。例えば「Using index」はインデックスのみでデータが解決されていることを、また「Using temporary」は一時テーブルを使用していることを示します。 これらの情報を基に、実行計画を読み解き、クエリのパフォーマンスを最適化する方法を理解します。 各項目の重要な部分の説明( type, filtered, Extra ) type type はクエリがどのように実行されるかを示すもので、パフォーマンスの観点から重要です。以下は、最も効率的な順に並べた type の種類とその説明です。 項目 内容 const 単一の比較によるレコードの検索で、結果が1行だけに限定される場合に使用されます。通常、主キーやユニークキーの等価比較で見られ、非常に高速です。 eq_ref 主キーまたはユニークキーに基づくジョインで一つのレコードだけを指し示す場合に使用されます。各ジョイン段階で1行のみが処理されるため、効率的です。 ref インデックスを使用して複数の行がマッチする可能性がある検索です。非ユニークインデックスが使われることが多く、キーに基づく絞り込みが行われますが、constやeq_refほどには効率的ではありません。 range インデックスを利用した範囲検索です。特定の範囲内の値を持つ行を効率的に検索しますが、スキャンする範囲によっては処理が重くなる可能性があります。 index インデックス全体をスキャンしますが、テーブル自体は読み込まれません。これは特定のケースでは効率的ですが、全エントリの検査が必要な場合はコストが高くなります。 ALL すべての行をスキャンする必要があるテーブルスキャンです。インデックスがない場合や適切なインデックスが利用されない場合に使用され、パフォーマンスが最も悪いタイプです。避けるべきです。 filtered filtered の値が100に近いほど、行をフェッチした後に絞り込んだ量が少なかったことを意味します。一方で、この値が 100 ではない場合、インデックスが適切に設定されていない可能性があり、不要な行が多くフェッチされている可能性があります。この場合、クエリやインデックス設計の見直しが必要です。 補足情報 : インデックスが存在しないカラムの場合、MySQLのオプティマイザは統計情報を持たないため、 filtered の値は以下のように固定されることが一般的です: 等価検索( = value )の場合、 filtered は 10% と固定されます。 もしカラムがenum型の場合、 filtered の値は取りうる値の逆数(1/enumの値の総数)に基づいて計算され、それを100倍してパーセンテージで表示します。 より大きい( > value )の場合、 filtered は 33.33% と固定されます。 範囲検索( BETWEEN start AND end )の場合、 filtered は 11.11% と固定されます。 これらの固定値は、統計情報がない場合のオプティマイザの仮定に基づいています。したがって、適切なインデックス設計を行い、オプティマイザが正確な統計情報を基に、インデックススキャンなど適切なクエリの実行方法を選択できるようにすることが、パフォーマンスの改善につながります。 Extra 説明 : クエリの実行に関する追加情報を提供します。ここに表示される内容は、パフォーマンスのボトルネックを特定するのに役立つことがあります。 パフォーマンスが良い順に並べた内容(若干条件により前後する) : Using index : インデックスだけを使用してデータを取得し、テーブルへのアクセスを避けます。(カバリングインデックス)これはクエリが効率的であることを示す良い兆候です。 Using index condition : インデックスコンディションプッシュダウン(ICP)が使用されていることを示します。これはインデックス内でWHERE条件の一部を評価することにより、不要な行の読み込みを減少させ、クエリの全体的な実行時間を短縮します。 Using where : データを取得した後に追加の絞り込みを行っている状態を示します。この状況は、インデックスがWHERE条件をすべてカバーしていない場合や、適切なインデックス自体が存在しないに発生します。 Using filesort : MySQLが結果をソートするために一時ファイルを使用します。これはクエリのパフォーマンスに影響を及ぼす可能性がありますが、正しい結果を得るために必要な場合があります。 Using temporary : クエリ処理のために一時テーブルが使用されます。これはGROUP BYやORDER BYの処理で見られ、大量のデータを扱う際にパフォーマンスに影響を与える可能性があります。 Full scan on NULL key : ジョインやサブクエリでNULL値を持つキーをフルスキャンしています。このプロセスは非常にコストが高く、パフォーマンスに大きく影響を及ぼします。 チューニングの具体的な例 具体的なSQLのチューニング方法をご紹介します。使用する環境はこちらのGitHubリポジトリで公開されています: https://github.com/hirosi1900day/tech-blog-for-mysql レベル1: whereとgroup byを使ったクエリのチューニング 次のSQLクエリは、特定の日付における商品名ごとの総数量を求めるものです。 SELECT product_name, SUM (quantity) as total_quantity FROM orders WHERE order_date = ' 2021-01-02 ' GROUP BY product_name まず、このクエリの実行計画を確認してみましょう。 EXPLAIN SELECT product_name, SUM (quantity) as total_quantity FROM orders WHERE order_date = ' 2021-01-02 ' GROUP BY product_name; 実行計画の結果は以下の通りです: + ----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + ----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+------------------------------+ | 1 | SIMPLE | orders | NULL | ALL | NULL | NULL | NULL | NULL | 12 | 10 . 00 | Using where ; Using temporary | + ----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+------------------------------+ 実行計画からわかるように、 ALL (全スキャン)が使用されており、インデックスが利用されていないことが明らかです。これはデータベースがテーブルの全行をスキャンしているため、データ量が多くなるとパフォーマンスが大きく低下する可能性があります。さらに、 Extra 列に Using where; Using temporary と表示されています。これは、 WHERE 句での絞り込み後に一時テーブルを使用して GROUP BY 処理が行われていることを示しており、効率的ではありません。 チューニングの実施 このクエリのパフォーマンスを向上させるためには、 order_date と product_name に複合インデックスを作成することが効果的です。複合インデックスでは、インデックスの列の順序が重要です。このケースでは、 WHERE 句で order_date を使用してデータを絞り込んでから、 GROUP BY で product_name を使うため、 order_date を第一引数に設定するのが適切です。 インデックスを追加します: CREATE INDEX idx_date_product ON orders(order_date, product_name); インデックスを追加した後の実行計画は以下のように改善されるはずです: EXPLAIN SELECT product_name, SUM (quantity) as total_quantity FROM orders WHERE order_date = ' 2021-01-02 ' GROUP BY product_name; 結果: + ----+-------------+--------+------------+------+------------------+------------------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + ----+-------------+--------+------------+------+------------------+------------------+---------+-------+------+----------+-------+ | 1 | SIMPLE | orders | NULL | ref | idx_date_product | idx_date_product | 6 | const | 2 | 100 . 00 | NULL | + ----+-------------+--------+------------+------+------------------+------------------+---------+-------+------+----------+-------+ この改善により、 type が ref に変わり、クエリはインデックスを使用してより効率的に実行されます。また、 Extra 情報もクリアされ、一時テーブルを使用することなく処理が行われていることが確認できます。 インデックスの順序の影響 SQLクエリのパフォーマンスを向上させるためには、インデックスの構成とその順序が重要な役割を果たします。ここでは、 product_name を第一引数とし、 order_date を第二引数とする複合インデックスの影響を考察します。 CREATE INDEX idx_product_date ON orders(product_name, order_date); この複合インデックスを用いたクエリの実行計画を見てみましょう: EXPLAIN SELECT product_name, SUM (quantity) as total_quantity FROM orders WHERE order_date = ' 2021-01-02 ' GROUP BY product_name; 実行計画は以下のようになります: + ----+-------------+--------+------------+-------+------------------+------------------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + ----+-------------+--------+------------+-------+------------------+------------------+---------+------+------+----------+-------------+ | 1 | SIMPLE | orders | NULL | index | idx_product_date | idx_product_date | 1028 | NULL | 1 | 100 . 00 | Using where | + ----+-------------+--------+------------+-------+------------------+------------------+---------+------+------+----------+-------------+ ここで、 type index となっているため idx_product_date インデックスがフルスキャンされ、 Extra 列に Using where が表示されている点に注目します。これは、 order_date での絞り込みがインデックスを最適に活用していないことを示しています。また、 Using temporary が表示されないことから、 GROUP BY product_name 処理で複合インデックスが効果的に機能し、一時テーブルが不要になっていることがわかります。このことから、複合インデックスの第一引数の product_name だけが利用され、第二引数以降の order_date は単独でインデックスとしての機能を果たしていません。 さらに、同じ条件で product_name のみにインデックスを設定した場合の実行計画を見てみましょう: CREATE INDEX idx_product_name ON orders(product_name); EXPLAIN SELECT product_name, SUM (quantity) as total_quantity FROM orders WHERE order_date = ' 2021-01-02 ' GROUP BY product_name; 実行計画は以下の通りです: + ----+-------------+--------+------------+-------+------------------+------------------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + ----+-------------+--------+------------+-------+------------------+------------------+---------+------+------+----------+-------------+ | 1 | SIMPLE | orders | NULL | index | idx_product_name | idx_product_name | 1022 | NULL | 1 | 100 . 00 | Using where | + ----+-------------+--------+------------+-------+------------------+------------------+---------+------+------+----------+-------------+ この結果から、 product_name のみのインデックスでも同じ実行計画になることがわかります。 つまり、複合インデックスを使用している場合でも、第一引数( product_name )が単独で使用されることがある点に注意が必要です。 レベル2: GROUP BYとHAVING、ORDER BY を使ったクエリ 次のSQLクエリは、商品がどれだけ頻繁に複数購入されるかを調べるためのものです。具体的には、商品名ごとに注文総数と平均購入数量を集計します。 SELECT product_name, COUNT (*) AS total_orders, AVG (quantity) AS average_quantity FROM orders GROUP BY product_name HAVING AVG (quantity) > 1 ORDER BY average_quantity DESC ; このクエリの実行計画を EXPLAIN を使って確認すると、以下のような結果が得られました。 + ----+-------------+--------+------------+------+------------------+------+---------+------+------+----------+---------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + ----+-------------+--------+------------+------+------------------+------+---------+------+------+----------+---------------------------------+ | 1 | SIMPLE | orders | NULL | ALL | idx_date_product | NULL | NULL | NULL | 1 | 100 . 00 | Using temporary; Using filesort | + ----+-------------+--------+------------+------+------------------+------+---------+------+------+----------+---------------------------------+ 全スキャンが行われており、インデックスが利用されていません。 Using temporary; Using filesort は、 GROUP BY と ORDER BY によって一時テーブルが使用され、結果がファイルソートされていることを示しています。これによりデータが多い場合、クエリのパフォーマンスが低下する可能性があります。 チューニングの実施 このクエリは product_name でグルーピングした後に quantity のカラムを使って HAVING や ORDER BY を行うため、 product_name と quantity に複合インデックスを追加することでデータそのものにアクセスすることなく、インデックスだけでデータの取得が行えそうです(カバリングインデックス)。これにより、データベースが効率的にデータにアクセスできるようになります。 インデックスを追加するSQLは次の通りです: CREATE INDEX idx_product_name_quantity ON orders(product_name, quantity); インデックス追加後、実行計画を再び確認すると、次のように改善されるはずです。 + ----+-------------+--------+------------+-------+--------------------------------------------+---------------------------+---------+------+------+----------+----------------------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + ----+-------------+--------+------------+-------+--------------------------------------------+---------------------------+---------+------+------+----------+----------------------------------------------+ | 1 | SIMPLE | orders | NULL | index | idx_date_product,idx_product_name_quantity | idx_product_name_quantity | 1027 | NULL | 1 | 100 . 00 | Using index ; Using temporary; Using filesort | + ----+-------------+--------+------------+-------+--------------------------------------------+---------------------------+---------+------+------+----------+----------------------------------------------+ クエリの最適化は行ったものの、 Using temporary; Using filesort がExtra情報から消えない理由は、 GROUP BY によるグループ化と ORDER BY によるソート処理が原因です。カバリングインデックスが追加されたことにより Using index が表示され、インデックスから直接必要なデータを取得しているため、全行スキャンのコストは削減されましたが、集計とソートには依然として追加のリソースが必要です。 SQLクエリでは、 GROUP BY でグルーピングした結果を ORDER BY でソートする際には、MySQLが内部的にテンポラリーテーブルを作成し、そのテーブルにデータを格納後、ソート処理を行います。このプロセスはメモリやディスクスペースを消費するため、扱うデータ量が増えるとパフォーマンスの低下が発生します。 そのため、データベースでの処理負荷を軽減するために、クエリをさらに絞り込むか、あるいはアプリケーションレベルでデータを取得後に集計やソートを行うアプローチが考慮されるべきです。(その場合アプリケーションサーバーに負荷が発生することになるのでバランスが重要) レベル3: Joinとサブクエリを使ったクエリ 次のSQLクエリは、各ユーザーが注文した商品の中で、そのユーザーが注文した商品の平均数量を超える数量を持つ商品名を取得します。 SELECT users.username, orders.product_name, orders.quantity FROM users JOIN orders ON users.id = orders.user_id WHERE orders.quantity > ( SELECT AVG (orders.quantity) FROM orders WHERE orders.user_id = users.id ); このクエリを EXPLAIN で実行計画を確認した結果、以下のような内容が得られました。 + ----+--------------------+--------+------------+--------+---------------+---------+---------+-----------------------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + ----+--------------------+--------+------------+--------+---------------+---------+---------+-----------------------+------+----------+-------------+ | 1 | PRIMARY | orders | NULL | ALL | user_id | NULL | NULL | NULL | 1 | 100 . 00 | NULL | | 1 | PRIMARY | users | NULL | eq_ref | PRIMARY | PRIMARY | 4 | testdb.orders.user_id | 1 | 100 . 00 | Using where | | 2 | DEPENDENT SUBQUERY | orders | NULL | ref | user_id | user_id | 4 | testdb.users.id | 1 | 100 . 00 | NULL | + ----+--------------------+--------+------------+--------+---------------+---------+---------+-----------------------+------+----------+-------------+ orders テーブルに対してフルスキャンが発生していますが、これはデータ量が少ない場合にインデックスを利用するよりも効率的だと判断されているケースです。特に注目すべきは DEPENDENT SUBQUERY という select_type で、これは外部クエリの結果に基づいて行ごとにサブクエリを繰り返し実行することを意味します。 例えば、100人のユーザーがそれぞれ5件の注文を持つ場合、サブクエリは500回実行されるため、効率が低下します。この問題を改善するためには、共通テーブル式(CTE)を使用し、ユーザーごとの平均数量を事前に計算してからメインクエリで利用する方法が有効です。 以下は、CTEを使用したクエリ例です。 WITH UserAverages AS ( SELECT user_id, AVG (quantity) AS avg_quantity FROM orders GROUP BY user_id ) SELECT u.username, o.product_name, o.quantity FROM users u JOIN orders o ON u.id = o.user_id JOIN UserAverages ua ON u.id = ua.user_id WHERE o.quantity > ua.avg_quantity; このクエリの実行計画は以下のようになります。 + ----+-------------+------------+------------+--------+---------------+-------------+---------+-----------------------+------+----------+--------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + ----+-------------+------------+------------+--------+---------------+-------------+---------+-----------------------+------+----------+--------------------------+ | 1 | PRIMARY | orders | NULL | ALL | user_id | NULL | NULL | NULL | 1 | 100 . 00 | NULL | | 1 | PRIMARY | <derived2> | NULL | ref | <auto_key0> | <auto_key0> | 4 | testdb.orders.user_id | 2 | 50 . 00 | Using where ; Using index | | 1 | PRIMARY | users | NULL | eq_ref | PRIMARY | PRIMARY | 4 | testdb.orders.user_id | 1 | 100 . 00 | NULL | | 2 | DERIVED | orders | NULL | index | user_id | user_id | 4 | NULL | 1 | 100 . 00 | NULL | + ----+-------------+------------+------------+--------+---------------+-------------+---------+-----------------------+------+----------+--------------------------+ CTEを使用することで、サブクエリの繰り返し実行を防ぎ、一度の計算で済ませるようになりました。ただし、CTEは派生テーブルとして扱われ、実行計画では DERIVED と表示されます。これにより、一部のデータがテンポラリーテーブルとして実体化されるため、必ずしも常にサブクエリより高速になるわけではないので注意してください。 DEPENDENT SUBQUERY が実行計画に現れた場合には、データ量が増えるとパフォーマンスが大幅に劣化する可能性があるため、注意が必要です。このような場合には、クエリの設計を見直すことを検討してください。 まとめ MySQLの実行計画を利用することで、クエリのパフォーマンスを把握し、最適化の方向性を明確にできます。今回は、具体的な例を通じて、インデックスを追加する方法とクエリを変更することでの改善方法を紹介しました。しかし、クエリのチューニングは状況に応じて最適な手法が異なるため、これらの方法以外にも様々なアプローチが考えられます。これからも実際の問題に遭遇した際の解決策をブログで積極的に共有していくことで、より多くの技術者がデータベースのパフォーマンス問題に対処できるよう支援していきたいと思います。 参考 この記事の内容に関連するさらなる情報は、以下の書籍で詳しく学べます。 書籍名 : MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ 出版社 : 技術評論社 出版日 : 2024年5月22日 この書籍は、MySQLの内部構造や動作原理について深く掘り下げており、実際の運用や管理におけるパフォーマンスチューニングの実践的なアプローチが学べます。
アバター
はい、亀井です。 yykamei という名前でインターネット上では活動しています。所属はタイミーです。 今回は Timee Advent Calendar 2024 の 19 日目の記事として、Slack の Huddles を使ったプラクティスとその背後にある考え、というタイトルで、筆を取らせていただきました。 仕事におけるコミュニケーションツールはいろいろありますが、その中でも Slack を使っている組織は多いのかなと思います。その Slack の機能の一つとして Huddles が 2021 年にリリースされました。当時はコロナ禍ということもあり、リモートワークが浸透してきた中でどのようにしてオンラインでのコミュニケーションを充実させるか?をまだまだ模索している中だったと思います。そんな中、 Discord のようにクイックに会話を開始できて、かつ、他のメンバーもリアルタイムで会話に参加できる機能として Huddles がリリースされ、当時の私は少し驚きました。 これまでも Zoom だったり、 Google Meet だったりでビデオチャットは可能でしたし、それこそ Slack Apps などを使えば、簡単にそれらのビデオチャットを呼び出してインスタントに会話が可能でした。しかし、 Slack の Huddles はそれが普段使っているチャットツールに組み込まれていると言うところに大きなアドバンテージがあると思います。そして、誰がどこのチャンネルで会話しているのか?が見える、というのも魅力的なポイントかと思います。 組織のカルチャーにもよりますが、 Slack を使っていてよく Huddles を利用している環境だと、自分が join しているチャンネルのいずれかで Huddles による会話が複数並行して行われることになります。実際のオフィスにおいても会議室は複数あって同じ時間帯に複数の会議室でミーティングが行われることは普通のことですが、それが可視化され、それぞれのミーティングに誰がいるのか?を知ることができるのは意外とメリットが大きいと感じます。どういうメリットかというと「賑やかでいいなー」というメリットです。はい、実利的なメリットというよりも感情面でのメリットです。 Slack を起動すると一目で賑やかさが可視化されるわけですよね。実際に賑やかなのかどうかはわかりませんが、人と人とが会話しているのだからおそらく賑やかなはずです。 Slack Huddles を使ったプラクティスは世の中にすでにたくさんあると思いますが、ここでは私が考えるもの、またなぜそれらを使うのか?を列挙してみようと思います。 Huddles を開いて作業する Communication は大事ですよね。その Communication を extreme にやるためには会話をする機会が必要です。では会話をする機会を増やすにはどうすればよいのか?という問いに対する答えがこのプラクティスです。リモートワークが主流ではなくメンバーの多くがオフィスで働いている場合は「ちょっといいですか?」という会話ができました。とはいえ「ちょっといいですか?」という声がけ自体、ハードルが高いと感じる場合もありますよね。私はありました。そして、リモートワークが中心の働き方になるとそのハードルの高さ自体感じることなく会話する機会は失われました。特にコロナ禍でこの問題が顕著になりましたね。この問題に対して、多くの組織があの手この手でバーチャルオフィスのツールを導入したり「雑談をする」という時間をカレンダー上でセットしたりして工夫をしてきました。そうした工夫のうちの一つが Huddles を開いて作業する、です。 誰かが Huddles を開いて何かをしていると「なんだなんだ?」という感じで興味を持ってくれた人が Huddles に参加してくれます。そうなったら雑談をしてもよいですし、軽く挨拶をかわして作業を続けてもいいでしょう。 このプラクティスは、チームが継続的に会話をすることに慣れていない、あるいは、会話をすることに対して価値を感じていない場合に有効です。 Communication が大事、という価値観をチームに押し付けてもよいのですが、そうするとチームからは信頼されなくなるでしょう。 Communication も大事ですがチームからの信頼も大事です。もしかしたら、誰もその Huddles に来ないこともあるかもしれません。それでもいいのです。ただそこにいるという状態をまず作り出すこと、それ自体に価値があります。そして、何かのきっかけでチームの誰かが話しかけてくれるかもしれません。そうしたときにその会話を楽しみましょう。そのうち「ここに来れば仕事上の相談ができるかもしれない」という雰囲気を作り出せるかもしれません。そこまでいけばチームが Communication の価値に気づいている可能性があります。 注意点としては、このプラクティスを行うと、ずっと Huddles にいる、という状態になってしまいがちということです。 Communication は大事ですが、人間性も大事です。休みましょう。8時間ずっとオンラインというのはどう考えても非人間的です。無理のない範囲で実施するようにしましょう。 また、もう一つ注意点があります。 Huddles から抜けることを促す、あるいはいつでも Huddles から抜けることを奨励しましょう。というのも、一度会話に入ったら途中で抜けることに躊躇する人もいるからです。そうなると、「抜けたいのに抜けられない」というふうになってしまってせっかくの Huddles の場がその人にとっては好ましくないものに変わる可能性があります。 Huddles というのは出たり入ったりが自由なものなのだ、という雰囲気を普段から醸成しておくとよいでしょう。 他のチームの Huddles に飛び込んでみる 他のチームが複数人で楽しそうに会話しているのをみると思わず飛び込んでみたくなりますよね。飛び込みましょう。わざわざ会議用のビデオチャットではなくあえて Slack で Huddles をしているのですから、これは誘われていると言っても過言ではありません。 なぜこれをするのでしょうか?自分のチーム外のことは普段の業務に関係することは少ないでしょう。その時間を使うぐらいなら普段の業務に時間を割り当てて締め切りが迫っている自分の作業に集中したくなります。もちろん、自分のタスクが逼迫しているなら無理をしてはいけません。ただ、もし自分の作業に余裕があるのであれば、他のチームの働き方を覗いてみると意外と発見があるものです。私が最近発見したことは次のようなものです。 あるチームのリファインメントのやり方が勉強になった あるチームのタスクの切り方が勉強になった 「モブプロをやる」と言っても「モブプロ」の定義はチームによって異なるようだ スプリントレビューに対する態度が勉強になった あるチームが適用したコードが自分のチームに開発に影響することがわかった 同じ組織とはいえ隣のチームは仕事の仕方が異なります。もしかしたら文化も異なるかもしれません。同じ言葉なのに異なる意味で会話していることもあります。そうした「異なるもの」に触れることは新たな洞察を得ることにもつながり、学びになります。自分のチームをもっと効果的にアウトカムを生み出すチームにするにあたって「これなら自分のチームに取り入れるとうまくいくのではないか?」という発見があるかもしれません。もちろん、こうした学びは書籍やインターネット上の情報を活用しても得られなくはないでしょう。ただ、そうした学びの実践を目の当たりにできる機会は少ないです。 Huddles でのリアルな会話を目撃することは、そうした体系立った知識がどのように活用されているかを発見する場として活用できるのではないかと思うのです。 加えて、他のチームの Huddles に入ることで単純にいろいろな人と知り合いになれます。やはり知り合いが社内に多いと困った時に頼りやすいですよね。実際、最近 SQL のパフォーマンスについて相談させてもらって非常に助かりました。「ちょっと話したことがあるから頼ったら助けてくれるかもしれない」というぐらいには簡単に知り合いになれるのがこのプラクティスの魅力です。 とはいえ、私も嫌われたくはないので空気を読んで Huddles に入るかどうかは見ます。たとえば、 Huddles を始める前後のやりとりで何かしら深刻そうな会話がテキストで行われていたり、なんとなく 1on1 のような空気感の会話なのであれば入らないようにしています。この読みが当たっているかどうかはわかりません。もしかしたらあまり深く考えずに飛び込んでもよいのかもしれませんが、そのあたりはもともとあった人間関係も影響すると思います。 Huddles にトピックを設定する Slack Huddles でトピックをセットできるのはご存じでしょうか?このトピックをセットすると Huddles に入っている人からも入っていない人からもどのような会話が行われているのか?がわかって便利です。トピックをセットする目的は、雑談ではなく「今これについて話している」というのを宣言し、 Huddles に入っていないがそのトピックに対して興味がある人を誘い込むことにあります。このプラクティスは、特定の誰かをメンションしたいわけではないが特定のトピックについて話したい、そして、そのトピックに関心のある人を呼びたいときに有効です。 プロジェクトやタスクなどすでに実施することが決まっている業務の場合、関係者は決まっているでしょう。しかし、次に何をするか?という探索などでは、柔軟な考えを取り入れたいですし様々な人の意見を聞きたいでしょう。多種多様な職種の人たちを集めてざっくばらんに話すことができればもしかしたら新しいアイディアが生まれるかもしれません。そうしたときにどうやって人を集めるか?というのは意外と難しいです。そこで Huddles のトピックを広告のように掲げることで関心のある人を呼び込むのです。 これは、「Huddles を開いて作業する」というプラクティスと組み合わせて使うこともできるでしょう。たとえば、パフォーマンスチューニングに取り組んでいるとします。その取り組み内容である「パフォーマンスチューニング」をトピックにセットするとどうでしょうか?チューニングが好きそうな人たちを呼べそうな気がしませんか? このように Huddles のトピックは人を呼び込むことを目的としています。呼び込んだ後に途中から雑談に変わったらトピックは変更すればよいでしょう。 最後に ここまで Slack Huddles のプラクティスをみてきました。どれも基本的なものですが、それを使う背景を考えてみると案外面白いですね。実はこれまでほとんど何も考えずにこうしたプラクティスを行ってきたのですが、こうしてアドベントカレンダーを書く機会をいただいたことでそのプラクティスの背景が言語化されました。この場を借りてアドベントカレンダーの機会をいただいたタイミーのプロダクト組織に感謝いたします。
アバター
こちらは Timee Product Advent Calendar2024 の18日目の記事です。前日は @ryopeko による「 RubyWorld Conference 2024に参加してきた 」でした。 こんにちは。タイミーでバックエンドのテックリードをしている @euglena1215 です。 タイミーではモノリスな Ruby on Rails アプリケーションに一定の規律を設けるために Packwerk を導入しています。 A Packwerk Retrospective であったように、Packwerk はあくまでツールであり鋭いナイフです。ツールは使い手が意図を持って扱わないとそれに振り回されて怪我をしてしまいます。 この記事では、それぞれのチェッカーがどんな目的を達成するために使えるものなのかを自分なりに整理してまとめてみます。   Packwerk 自体はあくまで依存グラフを作成するだけであり、どんな検査を行うかは決定しません。どんな検査を行うかを決定するのはパッケージごとのチェッカーの設定です。この記事を通して「Packwerk での議論が行われる際にはチェッカーの設定もセットで行われるようになるといいな」という微かな期待もあります。 前提 タイミーでは packwerk , packwerk-extensions で提供されているチェッカーのうち、Dependency Checker と Privacy Checker を利用しています。packwerk-extensions には他に Visibility Checker, Folder-Visibility Checker, Layer Checker が存在しますが、今回は省略させてください。 Dependency Checker Dependency Checkerの概要図 パッケージ間の依存関係を管理・検査するためのチェッカーです。これが唯一 Packwerk 本体が提供しているチェッカーになります。 機能の詳細は https://github.com/Shopify/packwerk/blob/main/USAGE.md#types-of-boundary-checks をご覧ください。 主な使い道 CI 上で実行するテストの枝刈りを行いテストの実行時間を削減する Dependency Checker はパッケージ間の依存関係を管理するため、全てのパッケージの依存関係を厳密に管理すれば、ある変更に対して影響を受けた可能性のあるパッケージを特定できます。影響範囲外のパッケージはテストの実行を省略可能なのでテストの実行時間を削減することが可能です。 これは Shopify のブログにもモチベーションの1つとして記述されています。 Instead of running the test suite on the whole application, we can run it on the smaller subset of components affected by a change, making the test suite faster and more stable. ref. https://shopify.engineering/shopify-monolith この用途で使うためには、全てのパッケージの enforce_dependencies を基本的には strict に設定する必要があります。 # packs/foo/package.yml enforce_dependencies : strict dependencies : - bar # bar パッケージで変更があれば foo パッケージにも影響を与える - baz # baz パッケージで変更があれば foo パッケージにも影響を与える しかし、この使い道で Dependency Checker を利用する際にはいくつかの注意点が存在します。 パッケージの循環依存をなくす必要がある 循環依存があると、全てのパッケージが影響を受ける可能性ありとしてマークされます。 例えば、A → B → C → D → A という依存があると、どのパッケージを変更しても全てのパッケージのテストを実行する必要があるため実行すべきテストを削減することができません。 循環依存があると全てのパッケージに影響が波及する図 そのため、どうやって循環依存を減らすかが重要です。 Shopify では、依存の方向を変え循環依存を減らすために ActiveSupport::Notifications の pub/sub のような機構を導入したと2020年の記事には記述されていました。 Inversion of control means to invert a dependency in such a way that control flow and source code dependency are opposed. This can be done for example through a publish/subscribe mechanism like  ActiveSupport::Notifications . ref. https://shopify.engineering/shopify-monolith が、2024年の記事では「結果としてコードが理解しにくくなったこともしばしばあった」と記述されていました。 We relied heavily on inversion of control, for example, to extract package references out of base layer code. These changes introduced indirection that, while resolving the violations, often made code harder to understand. ref. https://shopify.engineering/a-packwerk-retrospective ActiveSupport::Notifications をアプリケーションロジックで多用している Rails アプリケーションに出会ったことがないのでどのような書き味・読み味になるのかは私には分かりません。 しかし、同じように依存方向を反転させる ActiveRecord の Callback は馴染み深いのではないでしょうか。ActiveRecord の Callback は適切に使えば便利ですが、使いすぎると苦しめられることは Rails エンジニアのみなさんはよくご存知だと思います。同様のことが起きていないといいのですが果たして…。 Sorbet の利用が実質的には必須 テストの実行を省略するためには厳密な依存チェックが行えている必要があります。feature branch では CI が通っていてマージをしたのに、実は失敗するテストケースが存在していたなんて考えなくもないですよね? Packwerk はパッケージ間のクラス・定数の呼び出しを検出するツールです。そのため、引数として他パッケージで管理しているクラスのインスタンスが渡された場合、コード上にはクラスが登場しないので検知することができません。 # packs/foo/app/models/foo.rb class Foo # 引数 bar は bar パッケージで管理している Bar クラスのインスタンス def foo (bar) # bar.bar_method を呼びだすことで bar パッケージへの依存が発生しているが # packwerk は `Bar` が登場しない限り依存を検知できない! bar.bar_method end end Sorbet のような引数のクラスを評価するような実装があることで初めて検知することができます。 # packs/foo/app/models/foo.rb class Foo # sorbet ではランタイムで `Bar` を評価するので packwerk が依存を検知できる! sig { params( bar : Bar ).void } def foo (bar) bar.bar_method end end これは Shopify の Rails アプリケーションでの Sorbet による型アノテーションのカバレッジが十分に高いことが前提になっているからだと思われます。 こちらに関しては実質的に Sorbet に依存せずとも厳密なチェックができるようにカスタマイズするための機能提案が行われていますが、1年以上前から動きがありません。 github.com Shopify からすると今のままで困ってないわけですし、新たな機能を追加することで保守が大変になるので困るのは理解できます。fork して自分で保守を行う覚悟がないのであれば、この用途で使うためには Sorbet の利用が実質的に必須である状態は避けられなさそうです。 基盤パッケージが他パッケージに依存していないことを維持したい 全てのパッケージに関してパッケージ間の依存管理するのは諦めて、最低限基盤パッケージのような他パッケージに依存しないパッケージのみ検査を行うという使い道もあると思います。 この使い道で Dependency Checker を利用するなら以下のような設定になります。 # packs/no_dependency_package/package.yml # 他パッケージに依存しない基盤的なパッケージ enforce_dependencies : true | strict dependencies : [] # packs/normal/package.yml # 他パッケージに依存して動作する一般的なパッケージ enforce_dependencies : false dependencies : [] タイミーでは、 ApplicationRecord や ApplicationController など Rails が提供している基盤クラスを rails_shims パッケージとしてまとめています。このパッケージが具体の機能に紐づく module を include しているのは好ましくありません。 この使い方をすることで、明らかに依存してはいけない依存を検知することができます。 どの使い道が良いと考えているのか 完全に個人的な意見にはなりますが、Shopify の状況を見るに依存グラフを作成することによるテストの実行時間の削減は現実的ではないと考えています。 そのため、基盤パッケージの意図しない他パッケージへの依存の検出など本当にパッケージ間の依存を管理したい一部のパッケージのみ Dependency Checker を有効にし、一般的なその他パッケージは無効にしておくのが良いのではないかと考えています。 Privacy Checker Privacy Checker の概要図 packwerk の文脈での private なクラス・定数への参照を管理・検査するためのチェッカーです。元々 packwerk 本体が提供するチェッカーでしたが、packwerk-extensions に切り出されました。 機能の詳細は https://github.com/rubyatscale/packwerk-extensions?tab=readme-ov-file#privacy-checker をご覧ください。 主な使い道 パッケージの利用方法を絞りたいとき パッケージで扱っているロジックが複雑で「ここを変更した際は一緒にここも変更しないと不整合が発生する」といった暗黙的な依存が発生しているときは、安全な利用方法を Public API として提供しておくのが安心です。 この使い道で Privacy Checker を利用するなら以下のような設定になります。 # Public API を提供して利用方法を絞りたいパッケージ enforce_privacy : true | strict # パッケージ外からどのクラスを参照しても構わないパッケージ enforce_privacy : false 一方、packwerk の開発元である Shopify では Privacy Checker をうまく活用できなかったために packwerk 本体からは削除され、packwerk-extensions に切り出されたのも事実です。何が起きていたのかを分かる範囲でまとめておきたいと思います。 Under Deconstruction: The State of Shopify’s Monolith では「Privacy Checker を守ることだけを目的としたほとんど意味を持たない Public ラッパークラスが量産されていたことに気付いた」との記述がありました。 When we ignored the dependency graph, in large parts of the codebase the public interface turned out to just be an added layer of indirection in the existing control flows. This made it harder to refactor these control flows because it added additional pieces that needed to be changed. It also didn’t make it a lot easier to reason about parts of the system in isolation. ref. https://shopify.engineering/shopify-monolith また、Packwerk から Privacy Checker を削除し、packwerk-extensions に切り出すことが決まった GitHub discussion での議論 では「Privacy Check に違反したコードはただ public ディレクトリに移動されるだけ、もしくは package_todo.yml に記録されて忘れ去られるだけ。これらを対処するためにより悪いコードが生み出される」との記述がありました。ここでの「悪いコード」とは上記のほとんど意味を持たない Public ラッパークラスだと予想しています。 So far at Shopify, we didn't see much value in the privacy checks. Most of our packages have this option disabled and when we do have them enabled, people mostly fix this kind of violations mostly by moving files around, without improving the APIs, or, even worse, people just record the deprecations and forget about them. Those ways to solve this violation are actually creating worse code, instead of improving it. The public folders are full of different concepts mixed together, and people are getting annoyed with "Packwerk failing on me with something I can't fix" and just avoiding the tool all together. ref. https://github.com/Shopify/packwerk/discussions/219 このような状況になった実際の原因は Shopify に直接聞かないと分かりませんが、自分なりに予想するに Public API を定義することに価値を感じていない開発者に対しても Public API の提供および運用を強制させてしまったことにあるのではないかと考えています。 投資対効果に対して効果を感じられていない場合は、投資を最小化させるのが最も効果的な行動です。 どの使い道が良いと考えているか やはり他パッケージからの利用方法を絞りたいときに活用するのが良いと考えています。しかし、Shopify の事例を踏まえるとまずは Public API を提供することに価値があると感じられるパッケージでのみ有効にするのが良いのではないでしょうか。 新規で作ったパッケージに対して Privacy Checker を有効にし、Public API の定義を頑張ってみるのも良いと思います。 最初は全てを無効にした状態から始めてもいいじゃないか、気楽に行こう 個人的には最初は全てを無効にした状態で導入を行い、機能・ドメインごとでディレクトリが分かれているだけで他は特に何もしない Rails アプリケーションとして運用してみるのも悪くないと思います。 社内でこのような運用をしているパッケージが存在しますが、「触るファイルがまとまっているのでコードリーディングがしやすくなった」とのポジティブな声もありました。一方「普通の Rails のディレクトリ構造と違うのでギョッとする」という声もあります。 ただし、チェッカーを有効にする際は意図を持って有効にしましょう。繰り返しますが、Packerk はあくまでツールであり鋭いナイフです。ツールは利用者が意図を持って扱わないとツールに振り回されて怪我をしてしまいます。 全てを有効にした状態から始めると大量の package_todo.yml の荒波に飲み込まれてしまい、気付くと package_todo.yml を空っぽにすることが目的になることがあります。何度でも言いますが Packwerk はあくまでツールです。自分たちの開発スタイルを Packwerk に合わせるのではなく、Packwerk の設定を自分たちの開発スタイルに合わせましょう。   明日は yykamei の「Slack の Huddles を使ったプラクティスとその背後にある考え」です。お楽しみに!
アバター
モノリス特有の運用課題 こんにちは。バックエンドエンジニアの須貝です。 タイミーのバックエンドAPIはモノリスなRuby on Railsアプリケーションです。2024年12月現在、このリポジトリ上で10程度のチームが開発しています。 モノリスは利点も多いのですが、チームが増加するにつれて運用面でモノリス特有の難しさを感じることも増えてきました。例えば、SentryやDatadogで何かエラーや問題を検知しても「これはどこのチームの持ち物なのか」という責任があいまいになってしまい改善がなかなか進まない、基盤的なチームがエラーのトリアージをするにしても調査の負担が大きい、といった課題がありました。 SentryとDatadogにコードオーナーを送る 「まずどこのチームの持ち物なのかわかりやすくしよう」ということで、SentryとDatadogにコードオーナー(リポジトリ内の特定のファイルやディレクトリに責任を持つチーム)の情報を送信するようにしました。すでにバックエンドのリポジトリではGitHubのCODEOWNERSを導入していたのでこちらを利用しました。 なお、CODEOWNERSファイルのパースと、ファイル・クラスなどからコードオーナーを取得する処理ではCodeOwnershipというgemを活用しています。 github.com やることはシンプルでSentryとDatadogに情報を送信する際に、CodeOwnership gemを使ってコードオーナー名を取得し、tagのような形で付与してあげるだけです。以下、それぞれの実装例を紹介します。 Sentry編 まず、Sentryにコードオーナーを送信するところから見ていきましょう。 CodeOwnershipはbacktraceを渡してコードオーナーを返すこともできるので、Sentryにコードオーナー情報を送信する実装も簡潔です。以下の実装例のようにsentry-ruby gemの設定に数行追加するだけで済みます。 Sentry .init do |config| # ...諸々省略 config.before_send = ->(event, hint) do if hint[ :exception ] # コードオーナー名を取得 code_owner = CodeOwnership .for_backtrace(hint[ :exception ].backtrace)&.name || ' unknown ' # code_ownerというtagでSentryに情報を送信する event.tags[ :code_owner ] = code_owner end end end CodeOwnership.for_backtrace は渡したbacktraceを先頭から見ていき、コードオーナーが見つかった場合はそのコードオーナーを返す便利なメソッドです。 これでSentry側にtagとしてコードオーナー情報を送信するようになったので、Sentryの管理画面上でも確認できるようになっています。下記の画像のようにIssueの画面のTagsにコードオーナー名(この例ではWorking Relationsというチーム)が表示されています。 Sentryの画面で見た様子 さらに便利にするためにSentryから飛んでくるSlack通知でもコードオーナー情報を見られるようにしました。SentryのAlertルールの編集で「Set conditions」の「THEN」の「and show tags」の箇所に code_owner (上記のコード例で設定したtag名)を追加するだけです。 SentryのAlertの編集画面 これでSlack通知で下記のようにコードオーナー名(この例ではIronBankというチーム)が見られるようになりました。 SentryからのSlack通知 Datadog編 続いてDatadogのAPMでもコードオーナー情報を見られるようにしていきます。Controller(APIリクエスト)とSidekiqのjob(非同期処理)の2パターンに対応します。 まずControllerです。下記のようなModuleを書いてControllerでincludeします。 module DatadogTaggable extend ActiveSupport :: Concern included do before_action :set_datadog_tags end private def set_datadog_tags span = Datadog :: Tracing .active_span return unless span code_owner = CodeOwnership .for_class( self .class)&.name || ' unknown ' span.set_tag( ' code_owner ' , code_owner) rescue StandardError => e Rails .logger.warn( " Failed to set code_owner tag to span: #{ e.message } ( #{ e.class } ) " ) end end すると下記のようにDatadogのAPMでコードオーナー名(この例ではWork Wellというチーム)が確認できるようになります。 Datadog APMのTrace 次はSidekiqです。下記のようなMiddlewareを書いて上記のModule同様にDatadogのspanにtagをセットしてあげましょう。 class SidekiqDatadogMiddleware include Sidekiq :: ServerMiddleware # loggerを使用するため def call (worker, _job, _queue) span = Datadog :: Tracing .active_span if span code_owner = CodeOwnership .for_class(worker.class)&.name || ' unknown ' span.set_tag( ' code_owner ' , code_owner) end rescue StandardError => e logger.warn( " Failed to set code_owner tag to span: #{ e.message } ( #{ e.class } ) " ) ensure yield end end あとはSidekiqの設定で上記のMiddlewareを追加してあげれば完了です。 Sidekiq .configure_server do |config| # ...略 config.server_middleware do |chain| chain.add SidekiqDatadogMiddleware end # ...略 end おわりに 実際に上記の仕組みを導入してみて、自分がSentryのエラー通知をよく見ているというのもあり、ぱっと見で担当チームがわかるのはだいぶ助かるなあと日々感じています。主要なエンドポイントやJobは自分の中に脳内マッピングができている(気がする)ものの、さすがに限界を感じ始めていたのでこれ無しではもう生きていけません。後はSentryは最終的にはSlackでメンションまでできるようにしたいのですけど、あまり賢い方法が思いつかずに困っているので良い案のある方は教えてください。 また、上記では紹介しきれなかったのですが、ログにもコードオーナー名を付与しているのでDatadog Logsでもコードオーナーを見られるようにしています。コードオーナーを条件に入れてアラートを設定したり、ダッシュボードを作ることも可能になっているので、チーム単位でのオブザーバビリティの向上にも寄与するのではないかと考えています。 最後に自分が所属しているRailway(Rails WayではなくRailway)というチームではこんな感じでバックエンドアプリケーションの横断的な課題を解いたり、他のバックエンドエンジニアの開発生産性を上げたりするような活動を行っています。なんと、いま絶賛一緒に働いてくれる方を探しているので、カジュアル面談でお話しましょう。 product-recruit.timee.co.jp
アバター
英語YATTEIKI
Timee Advent Calendar 2024 13日目の記事です。 こんにちは!バックエンド・エンジニアの松岡です。 僕は2024年3月にタイミーに入社して、オフショア開発チームでブリッジ・エンジニアをしています。 チームメンバーの多くはオフショア先のベトナムの方々で、僕はそんなみんなと一緒にわいわい開発しています。 そんなチームではコミュニケーションの多くが英語で行われていますが、僕の英語力は、、、、 ということで今回は、英語学習について僕の取り組みを紹介します! 目次 過去の英語力 学習をはじめるきっかけ 目指すゴール CEFRとは B2を選ぶ理由 ゴールまでの学習量 学習方法 Speak 英語コーチ-イングリッシュおさる NHKゴガク 学習を継続する方法 現在の英語力 最後に 過去の英語力 まず以前の僕の英語力は中学生レベルでした。 どのように中学生レベルかというと、英語を学習したのが中学時代だけだったということです。最後の学習機会はきっと高校受験だったと思います。 社会人になってから何度か英語を使う機会がありましたが、今のように学習することはありませんでした。きっと当時の自分には英語学習は優先度の低いことだったのでしょう。 学習をはじめるきっかけ 僕はソフトウェア・エンジニアを20年以上やっています。 その過程でそこそこに技術力を成長させてきましたが、だんだんと自分に伸びしろを感じなくなっていました。 そこで、自分の成長に伸びしろがありそうなものを見渡したときに気になったのが英語でした。 そして次のような意欲や好奇心をもったことがはじまりでした。 伸びしろがあるとはいえ、若くない自分がいまから成長できるのか!?!?やってみるしかない! 英語を覚えたら僕の人生の可能性はどれくらい広がるだろうか?気になる!やるしかない! 一方で、それまで趣味としてやってきた技術の勉強をいったんやめる決断もしました。両立できるほど時間がないと感じたからで、それくらい真剣に取り組むことにしました。 目指すゴール 目指すゴールはCEFRのB2で、そのためにやることは600時間の学習です。 CEFRとは CEFRとはCommon European Framework of Reference for Languageの略で、外国語の習熟度を測る国際基準です。セファールと呼ぶようです。 ja.wikipedia.org A1からC2まで等級が6個あり、僕が目指すB2はその真ん中あたりです。 出典:「 各資格・検定試験とCEFRとの対照表 」(文部科学省) B2を選ぶ理由 B2を選んだ理由には エンジニア組織の英語化変革 EX という書籍の内容を参考にしています。 この書籍には 開発チームのすべてのコミュニケーションの英語化はB2が目安 であると書かれており、自分が目指すところはそこかなと思ったのでした。 エンジニア組織の英語化変革 EX[English Transformation]~グローバル時代に生き残る強い組織作りの鉄則~ 作者: 手島 拓也 , Marc Anderson , 水畑 建一 技術評論社 Amazon ゴールまでの学習量 次の解説によると、 英語初心者がB2を目指す場合は600時間の学習時間が必要 なようです。 prontest.co.jp 600時間は1日に1時間の学習で1.7年、1日に1時間半の学習で1.1年の長さです。 僕には適度かなという印象です! 学習方法 いろいろな方法をためしていますが、僕がお世話になった教材を3個紹介します! Speak www.speak.com 一番はじめに取り組んだのがSpeakというサービスでした。2023年4月 (約1年半前) にサブスクリプション契約していて、現在も続いています。 僕は勉強方法に無知だったんですが、その当時話題になっていたことが選んだきっかけでした。 diamond.jp 学習内容はAIと行う英会話レッスンです。 会話の相手がAIなのでとても気軽にできる ことがよいところです。 英語コーチ-イングリッシュおさる www.youtube.com 中学生のときは小笠原先生から英語を学びましたが、この度はイングリッシュおさる先生から英語を学びました。 下記の動画は先生の5時間の大作です。これを見て基礎を学びことによりその後の学習がより効果的になった気がします。 www.youtube.com NHKゴガク いまもっとも学習している教材はNHKゴガクです。次の2個の番組を聴いています。 ラジオ英会話 www.nhk.jp ラジオビジネス英語 www.nhk.jp 学習方法は次のとおりです。 リスニングとスピーキングはアプリ www.nhk.or.jp リーディングとライティングはNHKテキスト (本) www.nhk-book.co.jp よいところは3点です。 毎週新しいエピソードが配信されるため、学習する教材探しに困らないこと 各講座はそれぞれに毎週5個の新しいエピソードが配信されます。僕の場合は2個の番組を聴いているのでエピソードの数は合計10個です。 1個のエピソードの長さが短いため、続けやすいこと 1個のエピソードの長さは15分です。 重要な4個のスキルを学習できること 上記のとおり、リスニング、スピーキング、リーディング、ライティングの4つを学習できます。 学習を継続する方法 何事も継続することが一番難しいですよね。 僕のおすすめは 基本的に毎日やる です。 やる日とやらない日があると、やらない日の楽さを知っているのでやる日が少し辛く、よしやるぞ!と心を整えるまでにも時間がかかります。 一方で毎日にやる(やるしかない!)ということにすると上記は不要になります。 とはいえ、疲れていたり、予定があって時間がないときもあり、そんな日にはお休みします。 現在の英語力 さてさて、こんな僕ですが2024年10月に初TOEIC L&Rを受験してきました。 受験の目的は現在地を知るためです。 結果は590点で、CEFRで表すとA1です。目標は600点以上でしたのでくやしい結果でした泣。 TOEICの受験は今回で終わり、次回からはDuolingo English Testを受ける予定です。 englishtest.duolingo.com これを選ぶ理由は次のとおりです。 はやい インターネットで受験できる。 48時間以内に結果が出る。 やすい 試験料がTOEICやTOEFLより安い。 うまい 世界中の4500以上の教育機関で認められている。 最後に 現在までの学習時間はおよそ200時間でした。 ゴールまであと400時間、来年末までに達成したい! YATTEIKI!!! 次回のAdvent Calendarで結果発表できるかもしれません。 ということで来年またお会いしましょう〜!
アバター
Timee Product Advent Calendar 2024 13日目の記事です。 MLOpsエンジニアとして10月にタイミーにジョインした、 ともっぴ です。 データエンジニアリング部 データサイエンス(以下DS)グループに所属し、ML基盤の構築・改善に取り組んでいます。 概要 本記事では、Vertex AI Pipelinesを効率的に開発するために行った 「Vertex AI Pipelinesテンプレートを管理するArtifact Registryの導入」 の取り組みを紹介します。 過去、DSグループが取り組んできたVertex AI Pipelinesの開発効率化は、以下の記事を参照ください。 tech.timee.co.jp tech.timee.co.jp 背景と課題 背景 前提としてタイミーのML基盤では、サービスレベルに応じた複数のGoogle Projectが存在し、その中で複数のMLパイプラインが動いています。( 参考記事 ) Artifact Repositoryを導入する以前は、GCSにMLパイプラインテンプレートを格納していました。 下図は簡易的に表現したその時のアーキテクチャです。 Artifact Registry導入前のアーキテクチャ 課題 この状態での課題は2点ありました。 純粋な管理の煩雑さ 複数の環境のGCSに、以下のような構成でMLパイプラインテンプレートが存在していました。 ./project-internal ├── ML_Project_A_Bucket │   └── pipeline │   ├── v0.1.0 │   │   └── pipeline-config │   │   └── pipeline.json │   └── v0.2.0 │   └── pipeline-config │   └── pipeline.json └── ML_Project_B_Bucket └── pipeline ├── v1.0.0 │   └── pipeline-config │   └── pipeline.json └── v1.1.0 └── pipeline-config └── pipeline.json こうした状況下で、パイプラインやバージョンが増えていくにつれ、見通しが悪くなっていました。 ライフサイクル管理の煩雑さ MLパイプラインテンプレートすべてのファイルを保存し続ける必要はなく、開発用は一定の期間で削除、逆に本番用はバージョン管理して一定の世代分は必ず保持する、という管理が求められます。 GCSでもオブジェクトライフサイクルが設定できるので、n世代前は削除する、n日前のパイプラインは削除する、等のルールは設定可能です。 しかし従来の構成では、一つのMLプロジェクトにつき一つのバケットを作成していたので、MLプロジェクトが増えるたびに、ライフサイクル設定が必要になってしまい、手間が増える状況でした。 GCSに集約するという方法も考えられますが、Artifact Registryの方が各種ポリシーを柔軟に設定できることから、Artifact Registryの採用に至りました。また、Artifact RegistryのKFP対応自体が最近 *1 *2 のことなので、これまで採用を見送ってきた背景もあります。 ソリューション 改善後のアーキテクチャ Artifact RegistryにKFP formatのリポジトリを作成し、そのリポジトリに対してCDでMLパイプラインテンプレートをアップロードする構成となりました。 Artifact Registry導入後のアーキテクチャ その構成にしたことで、前項で書いたような複数プロジェクト・複数のバケットで管理してきたMLパイプラインテンプレートを、一つのリポジトリの中で管理できるようになりました。 (以下のディレクトリ構成のイメージ) ./project-central-repository └── kfp-repository ├── pipeline-1 │   ├── version1.yaml │   └── version2.yaml └── pipeline-2 └── version1.yaml 実装サマリー 具体的にどのような実装を行なったのか、概要を紹介します。 CDスクリプトの変更 CDで行うパイプラインテンプレートのアップロードは、 kfpライブラリのRegistryClient を利用しました。KFP CLIから利用できたら便利なのですが、 こちらのissue を読む限り現在サポートはされていないようでした。 そのため、簡易なPythonスクリプトを用意して、 GitHub Actions上で呼び出すことにしました。 cleanup_policyを設定しパイプラインのライフサイクルを管理 Artifact Registryでは cleanup_policy を設定することで、ライフサイクルを設定できます。Artifact Registryで一元管理することで、統一的なルールでライフサイクルを、各MLパイプラインテンプレートに適用でき、管理の煩雑さが解消できました。 実装途中で詰まったところ 実装をする上で、いくつか直面した問題があったので、その概要と解決方法を紹介します。 1) GitHub Actions上でkfp.RegistryClient.upload_pipelineが通らない CDで呼び出しているアップロード用のPythonスクリプトですが、ローカルでは正常終了するにもかかわらず、GitHub Actions上では認証エラーになる現象に遭遇しました。 原因を探っていると、 LayerXさんの記事 に行き当たり、RegistryClientがWorkload Identityに対応していないことが原因と判明しました。 LayerXさんの記事の通り、 kfp.registry.ApiAuth を利用することで問題は解決しました。 2) jsonではアップロードできず、yamlが必要になる 改善前の構成では、パイプラインはjsonでGCSに格納していました。 しかし、 kfp.RegistryClient.upload_pipeline(…) を利用すると、jsonでは invalid extension type pipeline.json というエラーでBad Requestとなりました。 ドキュメントを確認すると、 Artifact Registry REST APIを利用する場合、yamlである必要があると記載 がありました。 KFP SDK v2ではyamlフォーマットが推奨 されており、かつ KFP SDK v1のサポートは2024/12/20に切れるとされている ため、徐々にyamlに移行していくこととなりそうです。 3) Cloud Composer(Airflow)のRunPipelineJobOperatorからArtifact Registryのパイプラインを利用する際にタグ指定だと権限エラーとなる 最初、以下のようなコードで動作すると思っていたところ、権限エラーとなりました。 from airflow.decorators import dag from airflow.providers.google.cloud.operators.vertex_ai.pipeline_job import RunPipelineJobOperator @ dag (...) def main (): TEMPLATE_REPOSITORY_URL= "https://asia-northeast1-kfp.pkg.dev/project-central-repository/pipelines/ml-project/v1.0.0" vertexai_pipeline = RunPipelineJobOperator( task_id=task_id, project_id=gcp_project_id, region=gcp_location, display_name=display_name, template_path=TEMPLATE_REPOSITORY_URL, # KFP Repository上のテンプレートのURL service_account=pipeline_service_account ) 発生したエラーは以下です。 Service account XXX does not have permission to get ArtifactRegistry tag projects/project-central-repository/locations/asia-northeast1/repositories/pipelines/packages/ml-project/tags/v1.0.0 in region asia-northeast1. タグを取得する権限がないと言われています。 ここで、Artifact Registryにおけるタグとバージョンの違いについて確認します。 タグとは、MLパイプラインテンプレートをアップロードする際に、任意で付与できるものです。一方バージョンは、アップロード時に自動で付与される、 sha256: で始まるハッシュ値です。 MLテンプレートのタグとバージョン 関係するサービスアカウントと、Artifact RegistryへのREAD権限は以下の通りです。 サービスアカウント Artifact RegistryへのREAD権限 Cloud Composer用のサービスアカウント あり MLパイプライン用のサービスアカウント (pipeline_service_account) なし 一時的なエラー解消方法は、2通りあります。 タグではなくバージョンハッシュを指定する MLパイプライン用のサービスアカウントにArtifact RegistryへのREAD権限を付与する。 しかし、それぞれデメリットがあり、 1.の方法では、MLパイプライン更新のたびにバージョンハッシュを指定し直す運用となり、意図せぬバージョン指定のミスや、手間がかかることがデメリットです。 2.の方法でも、既存のMLパイプライン用サービスアカウントのIAMを変更し、新規のパイプラインを作る際も権限を毎度付与する手間が発生します。タイミーのML基盤では、 MLパイプラインごとに柔軟に権限付与するために、MLパイプラインごとにサービスアカウントを作成する運用になっているため、その工数は無視できないものになります。 Airflowのソースコードを確認したところ、RunPipelineJobOperatorの内部では、 PipelineJob初期化時にArtifact Registryからyamlを取得 し、 Vertex AI Pipelinesのジョブを作成するリクエスト を送っている流れになっていることがわかりました。 上述のエラーは、 ジョブを作成するリクエストをまさに送っている部分 で発生しており、この部分でMLパイプライン用のサービスアカウントから共有リポジトリへのアクセスが発生していると判断できました。 原因を踏まえると、RunPipelineJobOperatorにバージョン付きのMLパイプラインテンプレートを渡せるようにする方法が良いと考えました。 タグからバージョンを取得するには、 kfp.RegistryClient.get_tag(…) を利用すれば良いので、最終的に以下のような実装となりました。 from airflow.decorators import dag from airflow.providers.google.cloud.operators.vertex_ai.pipeline_job import RunPipelineJobOperator @ dag (...) def main (): TEMPLATE_REPOSITORY_URL= "https://asia-northeast1-kfp.pkg.dev/project-central-repository/pipelines/ml-project/v1.0.0" # kfp.RegistryClient.get_tag(...)をもとに、タグ付きのURLからバージョン付きのURLを取得するメソッド template_url_with_version = resoleve_template_version(TEMPLATE_REPOSITORY_URL) vertexai_pipeline = RunPipelineJobOperator( task_id=task_id, project_id=gcp_project_id, region=gcp_location, display_name=display_name, template_path=template_url_with_version, # KFP Repository上のテンプレートのURL w/ version service_account=pipeline_service_account ) まとめ 取り組みの内容自体はちょっとしたものですが、ML基盤の拡張性や利便性を高められるものかと思っています。一方、当初想定したよりも特に権限周りで詰まりどころが多く、この記事の内容が誰かの参考になれば嬉しいです。 入社2ヶ月ということもあり最初は入社エントリでも書こうと思っていたのですが、せっかくなら直近取り組んできたことを書きたいと思い、本記事の執筆に至りました。 最後に、入社エントリっぽいことを簡単に書いておきたいと思います。 タイミーではデータを活用した施策が活発に行われています。 MLOpsエンジニアとして、より広く、安全で使い勝手の良いML基盤にするため、日々ワクワクしながら取り組んでいます。 We’re Hiring! タイミーのデータエンジニアリング部・データアナリティクス部では、ともに働くメンバーを募集しています!! 現在募集中のポジションは こちら です! 「話を聞きたい」と思われた方は、是非一度 カジュアル面談 でお話ししましょう! *1 : Vertex AI PipelinesテンプレートがArtifact Registryに対応したのは、2022年12月です。 Release Note: https://cloud.google.com/vertex-ai/docs/release-notes#December_05_2022 *2 : kfp.RegistryClientのリリースはKFP SDK 2.0.0からで、2023年6月です。Release Note: https://github.com/kubeflow/pipelines/blob/master/sdk/RELEASE.md#200
アバター
こんにちは! タイミーでPlatform Engineerをしている @MoneyForest です。 こちらは Timee Product Advent Calendar 2024 の10日目の記事です。 2024年8月に入社して、幸いにもチームメンバーにも恵まれて楽しく働いています。 個人的にキャッチアップがあまりできていなかった OpenTelemetry を題材にして実装をしてみたので、ここから得られた気づきや知見を共有したいと思います。 はじめに アプリケーションの可観測性(Observability)を担保する上で、APM(Application Performance Monitoring)は重要です。 現在、JaegerやPrometheusのような非商用のものから、DatadogやNew RelicやSplunkなど様々なAPMを利用できるツールが存在します。(本記事ではこれらを「Observability Backend」と呼称します。) これらのObservability BackendでAPMを利用するには、各々が提供する独自のSDKを使用してTraceやSpanを送信する必要があり、以下のような課題がありました。 アプリケーションの実装に各SDKの仕様が滲み出る Observability Backendの切り替えが困難 OpenTelemetryの登場により、オブザーバビリティに関する様々な概念が抽象化され、統一された仕様が提供されるようになりました。 これにより、アプリケーションコードとObservability Backendの実装を分離し、状況に応じて適切なツールを選択できるようになりました。 つまり、ローカルではJaegerのような非商用のツールで開発中にAPMを利用し、デプロイしたアプリケーションではDatadogのような商用ツールでリッチなUIでAPMを利用したり、モニタリングアラートを組んだりなどができるということです。 本記事では、Go言語でOpenTelemetryを活用して、環境ごとにObservability Backend(Jaeger、Datadog)を切り替える実装を行うことで、これらのメリットを体感していきたいと思います。 実装してみる 実装の全量はこちらにあります。 https://github.com/MoneyForest/advent-calender-2024 共通インターフェースの定義・初期化処理 OpenTelemetryに準拠した共通インターフェースを定義することで、各バックエンドの実装を抽象化します。(この実装では infrastructure/tracer.go に配置しています。) 共通インターフェースはOpenTelemetryのSDKの仕様に則してインターフェースを定義します。 例えばShutdownメソッドは、OpenTelemetryの仕様に基づき定められているものです。トレースデータの欠損を防ぐために必要な標準仕様となっています。 参照: https://opentelemetry.io/docs/specs/otel/trace/sdk/#shutdown また、今回は環境に応じてObservability Backendを切り替える実装のため、envを条件に初期化するProviderを切り替えています。 // infrastructure/tracer.go package infrastructure import "context" type TracerProviderWrapper interface { Shutdown(context.Context) error } func InitTracer(env string ) (TracerProviderWrapper, error ) { switch env { case "dev" : return InitDatadog() default : return InitJaeger() } } Observability Backend固有の初期化処理 次に各Observability Backendごとの初期化処理を書いていきます。 Datadog Datadogは2つの方法で送信できます。 ddotelアダプター経由 参照: https://docs.datadoghq.com/ja/tracing/trace_collection/custom_instrumentation/go/otel/ こちらではDatadogのSDKであるdd-trace-goからddotelというアダプターを通じてOtelに準拠したTraceProviderを生成します。 送信するポートはDatadog Agentのデフォルトポートである8126になります。 // infrastructure/datadog.go package infrastructure import ( "context" "os" "go.opentelemetry.io/otel" ddotel "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/opentelemetry" ddtracer "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" ) type ddTracerProvider struct { provider *ddotel.TracerProvider } func InitDatadog() (*ddTracerProvider, error ) { tp := ddotel.NewTracerProvider( ddtracer.WithService(os.Getenv( "SERVICE" )), // MoneyForest ddtracer.WithEnv(os.Getenv( "ENV" )), ddtracer.WithServiceVersion( "1.0.0" ), ddtracer.WithAgentAddr( "localhost:8126" ), ) otel.SetTracerProvider(tp) return &ddTracerProvider{provider: tp}, nil } func (p *ddTracerProvider) Shutdown(ctx context.Context) error { return p.provider.Shutdown() } Otel SDK経由 こちらではOtel SDKを使用してTraceProviderを生成します。 参照: https://docs.datadoghq.com/ja/opentelemetry/interoperability/otlp_ingest_in_the_agent/?tab=host#enabling-otlp-ingestion-on-the-datadog-agent 送信するポートはOTLP HTTPのデフォルトポートである4318になります。 // infrastructure/datadog.go package infrastructure import ( "context" "os" "go.opentelemetry.io/otel" ddotel "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/opentelemetry" ddtracer "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" ) type ddTracerProvider struct { provider *sdktrace.TracerProvider } func InitDatadog() (*ddTracerProvider, error ) { exporter, err := otlptracehttp.New( context.Background(), otlptracehttp.WithEndpoint( "localhost:4318" ), otlptracehttp.WithInsecure(), otlptracehttp.WithURLPath( "/v1/traces" ), ) if err != nil { return nil , err } tp := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exporter), sdktrace.WithResource(resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceNameKey.String(os.Getenv( "SERVICE" )), // MoneyForest semconv.DeploymentEnvironmentKey.String(os.Getenv( "ENV" )), )), ) otel.SetTracerProvider(tp) return &ddTracerProvider{provider: tp}, nil } func (p *ddTracerProvider) Shutdown(ctx context.Context) error { return p.provider.Shutdown(ctx) } Jaeger Jaegerは jaegar-client-go のSDKがありましたが、2024/5に非推奨となっています。 今はOpenTelemetryに準拠することが推奨されているため、Otel SDKによる実装を行います。 // infrastructure/jaeger.go package infrastructure import ( "context" "os" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.4.0" ) type jaegerTracerProvider struct { provider *sdktrace.TracerProvider } func InitJaeger() (jaegerTracerProvider, error ) { exporter, err := otlptracehttp.New( context.Background(), otlptracehttp.WithEndpoint( "localhost:4318" ), otlptracehttp.WithInsecure(), ) if err != nil { return jaegerTracerProvider{}, err } tp := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exporter), sdktrace.WithResource(resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceNameKey.String(os.Getenv( "SERVICE" )), semconv.DeploymentEnvironmentKey.String(os.Getenv( "ENV" )), )), ) otel.SetTracerProvider(tp) return jaegerTracerProvider{provider: tp}, nil } func (p jaegerTracerProvider) Shutdown(ctx context.Context) error { return p.provider.Shutdown(ctx) } アプリケーションでの利用 アプリケーションでは、OpenTelemetryの標準APIを通じてTracerを使用することで、Observability Backendに依存しない一貫した実装が可能になります。 package main import ( "context" "crypto/rand" "fmt" "log" "os" _ "github.com/go-sql-driver/mysql" "github.com/MoneyForest/timee-advent-calender-2024/internal" "github.com/MoneyForest/timee-advent-calender-2024/internal/handler" "github.com/MoneyForest/timee-advent-calender-2024/internal/infrastructure" ) func main() { // Initialize tracer tp, err := infrastructure.InitTracer(os.Getenv( "ENV" )) if err != nil { log.Fatal(err) } defer tp.Shutdown(context.Background()) // ... } func (h *UserHandler) CreateUser(ctx context.Context, email string ) error { tracer := otel.Tracer(os.Getenv( "SERVICE" )) ctx, span := tracer.Start(ctx, "UserHandler.CreateUser" ) defer span.End() span.SetAttributes( attribute.String( "email" , email), attribute.String( "handler" , "UserHandler" ), attribute.String( "method" , "CreateUser" ), ) return h.userUsecase.CreateUser(ctx, email) } 動かしてみる これまでの実装がうまくいっていれば、JaegerとDatadogで同じようなAPMを見ることができる想定です。確認してみましょう。 Docker Composeで gcr.io/datadoghq/agent と jaegertracing/all-in-one のサイドカーを立てます。 本来はdatadog-agentはdev環境以上に必要で、ECSのサイドカーなどに定義すればよいです。 (リポジトリではMySQLやRedisのコンテナも建てていますが省略します。) JaegarはUIまでパッケージングされたイメージが公式から提供されており、こちらにトレースを送信することで、localhostのUIからAPMを確認できます。 datadog-agent : image : gcr.io/datadoghq/agent:latest environment : - DD_API_KEY=${DD_API_KEY} - DD_SITE=datadoghq.com - DD_APM_ENABLED= true - DD_APM_NON_LOCAL_TRAFFIC= true - DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_ENDPOINT=0.0.0.0:4318 ports : - "8126:8126" - "4319:4318" # OTLP HTTP (ローカルマシンのポートはJaegarと被るのでずらしている) volumes : - /var/run/docker.sock:/var/run/docker.sock - /proc/:/host/proc/:ro - /sys/fs/cgroup:/host/sys/fs/cgroup:ro jaeger : image : jaegertracing/all-in-one:latest ports : - "16686:16686" # UI - "14250:14250" - "4318:4318" # OTLP HTTP environment : - COLLECTOR_OTLP_ENABLED= true dev環境(Datadog) https://app.datadoghq.com/apm/traces でトレース情報を見ることができます。 以下はddotelを使用して送信したメトリクスです。 以下はOtel SDKを使用して送信したメトリクスです。 1. と異なる点として、SpanのAttributeに otel の構造体があります。また、 process_id や runtime-id は存在しません。 local環境(Jaegar) http://localhost:16686/search でトレース情報を見ることができます。Datadogと一部のメタデータが異なっていたりと、完全に同一ではないものの、概ね同じ内容を見ることができます。 Datadogのトレースと異なる点としては、サービス名として与えていたMoneyForestがotel.libraryにマッピングされていましたが、こちらではotel.scopeにマッピングされているという点です。 実装してわかったこと GoのOtel SDKから生成される sdktrace.TracerProvider では Shutdown はcontextを引数に取るものの、 ddotel.TracerProvider の Shutdown はcontentを引数に取らない、などの細かな差異があり、共通インターフェースの方で吸収する必要がありました。 Datadogの実装でOtel SDKを直接使う方を採用すれば、共通インターフェースを用意する必要がなく、以下のように InitTracer は初期化の条件分岐のみを書いた実装が可能です。 func InitTracer(env string ) (*sdktrace.TracerProvider, error ) { switch env { case "dev" : return initDatadog() default : return initJaeger() } } またddotelのライブラリには added to the OpenTelemetry API will default to being a no-op until implemented by this library. のコメントがあり、新機能の利用にはGoのOtel SDKではなくDatadog Agent側での実装を待つ必要がありそうでした。 そのため、Datadogの独自機能を重視する場合はddotelを採用し、Otelへの準拠を重視する場合はOtel SDKで実装するなど、実装の際に意思決定が必要になりそうです。 また、細かなマッピングの違いも存在しそうなため、Traceを使ってモニタリングアラートを組んでいる場合は注意して切り替える必要がありそうです。(例えばDatadogからNew Relicに変更する場合など) 商用のツールでは料金体系なども異なるため、仕様が同じでもサンプリングレートの値の調整が必要になることもあるでしょう。 まとめ OpenTelemetryを活用することで、トレーシング実装でより一貫性のあるコードを実現できることがわかりました。 これにより、環境に応じて適切なObservability Backendを利用したり、移行することが容易になったりするのは間違いなさそうです。 一方で標準仕様についてはまだまだ理解が浅いので、OpenTelemetry自体の理解も深めて、さらに楽しいエンジニアライフを送りたいと思います!またね〜
アバター
こちらは Timee Product Advent Calendar 2024 の9日目の記事です。前日は平岡の スクラムマスターが常に意識するべき重要なこと でした。 タイミーでテックリードをしている @euglena1215 です。 最近、Majestic Monolith と Citadel というアーキテクチャ・考え方を知ったのですが、あまり国内では認知度が高くないように感じたので紹介してみたいと思います。 自分が見つけた日本語での記事は モノリス亜種のアーキテクチャ(Modular MonolithとかMajestic MonolithとかCitadel Architectureとか) と Rails: AppSignalが採用する「シタデルアーキテクチャ」(翻訳)|TechRacho by BPS株式会社 の2記事のみでした(※記事執筆時点)。 Majestic Monolith Photo by Gary Walker-Jones on Unsplash signalvnoise.com Majestic Monolith の出典は Ruby on Rails の作者である DHH のブログです。2016年に書かれていてすごい。 正確な内容は出典を参照していただくとして、ここでは自分の解釈をまとめてみます。 世間では「モノリスアーキテクチャ」は劣ったアーキテクチャのような評価を受け、「マイクロサービスアーキテクチャ」が優れているような評価を受ける。 Amazon や Google、あるいは何千人もの開発者がいるような会社にとってはマイクロサービスアーキテクチャは素晴らしいアーキテクチャだが、「成功している企業で効果があったのだから、我々にも効果があるはずだ」と思い込んで小さな会社がマイクロサービスアーキテクチャを目指すのは間違い。 巨大な一握りの会社以外はモノリスを受け入れ、意図を持ってモノリスを雄大(majestic)に設計しよう。 モノリス自体が悪いと捉えてしまうと、それを解決するためにはモノリスから脱する以外の道はありません。モノリスをまず肯定することによって、初めて自分たちが抱えている様々な技術的課題の輪郭が見えてきます。 もちろんその中にはモノリスを分けなければ解決できない課題もあると思いますが、そうでないものも含まれていると思います。まずモノリスを肯定することによって、様々な技術的課題の解像度を高めることができる示唆に富んだ考え方だと感じています。 Citadel Photo by K. Mitch Hodge on Unsplash signalvnoise.com こちらも出典は Ruby on Rails の作者である DHH のブログです。2020年に書かれています。 多くのケースで Majestic Monolith はうまく機能するが、パフォーマンスや可用性の問題でモノリスでは対処が難しい課題が出てくることもある。 その際の次のステップは Citadel。Citadel とは、モノリス(Citadel = 城塞)を中央に据えて周辺に必要に応じてモノリスを補完するようなサービス(Outpost = 前哨基地)を配置する考え方。 Outpost としてモノリスとは異なる特性(パフォーマンス上の問題を対処、組織上、実装上など理由は様々)のサービスが提供できれば、アプリケーションの残りの部分は Majestic Monolith として提供し続けられる。 計らずもこの構成になっているプロダクトは多いのではないのでしょうか。タイミーのそのうちの1つです。 Citadel という名前をつけたことで、別物だと感じていた他プロダクトの構成にも共通点を見出せるようになった気がします。DHH の命名の妙だと言わざるを得ません。 状態ではなく、事象に注目している ソフトウェアアーキテクチャの文脈でよく登場するモノリス・マイクロサービス・モジュラモノリスといった分類は、あくまで「こういった状態のシステムはこのような特性を持っている」というカタログでしかありません。 理解し把握しておく分にはとても有用ですが、「(ここに任意のアーキテクチャ名が入る)化」をしたから我々が直面している様々な技術的課題が解決できるかというと、なかなか難しい面もあるかと思います。 ですが、DHH の提唱する Majestic Monolith にも Citadel にも、現場で起きている事象(問題)に対してどのように対処をしていくかという地に足のついた指針のようなものを感じました。 アーキテクチャに囚われず、きちんと目の前で起きている技術的課題を見定めよう。 目の前で起きている技術的課題を見定めたら、必要十分な対処をしよう。 隣の芝生は青いということわざがあるように、自分たちが選択していないアーキテクチャが良く見えてしまうのは人間の性だと思います。Majestic Monolith, Citadel を胸に誘惑に負けないよう努力し続けたいと思いました。   明日は @MoneyForest の「OpenTelemetryで環境ごとにObservability Backend(Jaeger、Datadog)を切り替えてエンジョイしてみたよ」です。お楽しみに!
アバター
Timee Advent Calendar 2024 6日目の記事です。 タイミーでスクラムマスター(以下、SM)/アジャイルコーチを担当している正義です! この記事では 学習したことや学んだことは、どんどんアウトプットするといいよ! どのようなアウトプット方法があるのか? というお話をします。 なぜアウトプットをするのか? 1. 自身への定着 それは「学習に対する能動性を向上し、自身の記憶としてより定着させるため」です。 よく見かけるラーニングピラミッドのように、能動的な活動になるにつれて定着しやすくなります。 また、活動によって学習に対するアプローチの深さが変わってきます。 https://www.mext.go.jp/b_menu/shingi/chukyo/chukyo3/004/siryo/__icsFiles/afieldfile/2015/09/04/1361407_2_4.pdf 必ずしも一定の深いアプローチをしていれば身に付くわけではなく、学習する内容によってアプローチの手法は変わってきますし、効果的に定着するかはアプローチとの相性によっても異なります。 学習を定着させるために重要なのは「多角的に、能動的に学ぶこと」です。 そのため、色々な学習に対するアプローチを積極的にしてみることで、自身への学びをより効果的にすることができます。 2. 自身の活動のログとなる 自分の学習・経験をログとなる形で残すことで、自分自身で振り返ることができます。 また、その内容は他の人にシェアできたり、自身の今後の評価材料にもできます。 どのような学びのアウトプット方法があるのか? 1. ブログでの発信 ブログでの発信はじっくり考えを整理でき、任意のボリュームでの記述が可能です。 また、図・表・写真など、視覚的な情報も載せられます。 私の場合、ブログの記事を最後まで書くのは意外とパワーが必要となってしまうので、「やるぞ!」という気合がないとどうしても後回しにになって、投稿が遅れがちになります。 同じような状態に陥りがちな方は、期日を設ける/体験した翌日には投稿するなどの個人的なルールを設けることをおすすめします。 https://note.com/rakuraku_justice/ 2. Xでの発信 Xでの発信は文字数が限られているため、知見や考えたことの要点のみをまとめることに役立ちます(課金していない人限定)。文量が少ない分、ブログよりも気持ちのハードルを下げて投稿できます。 また、図・表・写真など、視覚的な情報も載せられます。 私の場合はカンファレンスなどで得た情報をサクッとメモがわりに投稿しています。 その情報を元に、あとあと見返してブログなど他のアウトプットにつなげたりしています。 3. カンファレンスプロポーザルでの発信 主にRSGTやスクラムフェスなど、特定のカンファレンスに限られるかもしれませんが、プロポーザルを募集していたりするので、テーマに沿った内容を記載して発信します。 ブログと違う点は、完全にフリーな場ではなくカンファレンスごとに、ある程度テーマや書き方が定まっていることです。 私の場合は、カンファレンスを一つの区切りとして、そこまでに得た知見と関連する経験を記載して投稿してみることが多いです。 (登壇につながれば、さらなるアウトプットもできる!) いざ登壇したいとなっても、うまく書けなかったり、書く内容が思い浮かばなかったりするので、ひとまずカンファレンスプロポーザルは書けそうなら書いてみることをおすすめします。 https://confengine.com/conferences/scrum-fest-osaka-2022/proposal/16594 4. 登壇する カンファレンスやコミュニティイベントでのLT会などの場で、登壇できるならば登壇してみましょう! 登壇による学習の効果は高いと考えています。資料をまとめるだけでも一つのアウトプットですし、それを自身の言葉で発表することは更なるアウトプットや情報の整理につながります。 また、Q&Aの時間で質問してもらえたらラッキーで、瞬時に色々と考えた上で自分の言葉にする経験を得ることができます。 このように登壇では複数のアウトプットを体験できるので、機会はどんどん獲得してたくさん登壇しましょう! 5. Podcastなどのラジオ形式での配信 1人もしくは数名でラジオ形式の収録を行い、編集と配信をします。 個人的には、一番アウトプットが楽に感じています。一緒に収録してくれる人がいれば、テーマに対し議論しているだけでアウトプットにつながります。 編集は少し時間がかかります(30minのラジオに対して1hほど)。しかし、失敗したりおかしいと感じたりする部分はカットできるので、安心して収録できます。 ブログを書くよりも準備や考えることが少なく始められるので、おすすめです! https://creators.spotify.com/pod/show/yoriyokufm 他にも色々なアウトプット方法がある エンジニアやデザイナーであれば業務外において趣味で創作をする 仕事で得た知識を別の場で活用してみる 1on1や数名の場で議論をしてみる 一つの形に絞るのではなく、色々な形式を試してみることで自分に合う形式を見つけられるかもしれません。 最後に 臆せず、アウトプットすることが大切です。 「自分が話してもすごい経験があるわけでもないし…」 「誰かがすでに記事にしていそうだし…」 と、考えてしましい、アウトプットをしないのはもったいないです。 あなたの経験には、必ず価値があり、誰かの役に立ちます。 まずはどんな形でもよいので、発信してみて自分の学習のためのアウトプットにチャレンジしてみましょう! いつかそれが誰かの役に立っていると気づく日がきっときます! 私の Timee Advent Calendar 2024 Day 6は終わりです! 引き続き、明日の記事も是非読んでみてください!
アバター