TECH PLAY

株式会社スタメン

株式会社スタメン の技術ブログ

247

はじめに みなさん、2025年はどうでしたか? 存分にエンジニアリングを楽しみましたか? こんにちは!株式会社スタメンの ちぇる です。私事で恐縮ですが、先日「 俺の忘年会2025 」というイベントに参加してきました。 今年から始まった「俺の勉強会」シリーズのイベントですが、 「名古屋のエンジニアを家から出す!」 をモットーに、非常に熱量の高いコミュニティとして盛り上がりを見せています。 参考: 俺の勉強会を振り返る2025(スライド) その熱量に触れ、本記事では改めて自分の1年を振り返ってみたいと思います。少し個人的な話になりますが、お付き合いいただけると幸いです。 今年、僕のエンジニアとしての仕事への向き合い方は180度変わったといっても過言ではありません。一言で言えば、「仕事 = 苦行」だった日々が、「仕事 = 全力で向き合える趣味」に変わったのです。 2025年を振り返って:仕事が「最高の趣味」になるまで 1. 激動の時代だからこそ、問われる「楽しむ力」 今、エンジニアを取り巻く環境は激変しています。GitHub Copilotがコードを書き、Geminiが爆速でレビューを返してくれる。面倒な作業が効率化され、コーディングの基準がグッと引き上げられた「過渡期」に私たちはいます。 AIが正解を教えてくれる時代に、エンジニアとして働く喜びはどこにあるのか?そのヒントをくれたのが、今年入社した「株式会社スタメン」での環境でした。 2. 自主性が生んだ「自分で仕事を楽しくする」サイクル 弊社スタメンの Vision は 「人と組織で勝つ会社」 です。 この言葉通り、ここでは驚くほどの裁量と自主性が重んじられています。 目標は自分で決める 半期の目標は、自分が感じている課題をもとに提案します。 手段も自分で選ぶ 実装をどう進めるか、AIをどう使いこなすか、いつ誰に相談するか、時間の使い方も自分次第です。 この「自由」という名の責任を与えられたことで、僕の意識は変わりました。 「与えられた仕事をこなす」のではなく、「自分の仕事を自分で楽しくする」 という発想になったのです。 例えば、入社してから「RuboCopの警告解消プロジェクト」を自ら立ち上げたり、社内勉強会の運営メンバーとして文化醸成に携わったり、時にはメインのバックエンド領域から飛び出して、不慣れなフロントエンド領域のタスクに挑戦することもありました。 こうして仕事のハンドルを自分で握ることで、 「仕事ってこんなにも面白いものなのか」 と驚きました。 3. 「机に向かう」だけがエンジニアの成長ではない 以前の僕は、「とにかく本を読み、一人で黙々とコードを書けるようになること」こそが正義だと思っていました。もちろん自学自習は大切です。しかし、スタメンの「交流を大切にする文化」に触れ、その考えも大きく広がりました。 今年は勇気を出して、外の世界へ踏み出してみました。 岐阜:「 ながらRuby会議01 」への参加( 参加レポート ) 東京:「 Kaigi on Rails 2025 」への参加( 参加レポート ) 名古屋:先日開催された「 俺の忘年会2025 」への参加 そこで目にしたのは、心から技術を楽しみ、情報交換をし、世の中を良くしようとする仲間たちの姿でした。 これまで自分は、「ビジネス成果を出すための手段」としてのみエンジニアリングを捉えていた時期もありました。しかし、変化が激しく、新しい技術が次々と生まれ、インターネットを通じて世界中にプロダクトを届けられる。そんな恵まれた環境にいるのに、ただお金を稼ぐためだけに働くのは、あまりにももったいないと気づいたのです。 そう思うと、エンジニアが家から出る価値って、本当に大きいなと感じました。 4. 2026年、さらなる「高み」を目指して 自分の周りにいる先輩方は、自作ツールの開発やOSSへのコントリビュート、イベント登壇など、常にワクワクしながら挑戦し続けています。 僕もその背中を追い、2026年はさらに視座を高めていきたいです。今年はイベントへの参加を通じて多くの刺激をいただきましたが、来年は自らプロポーザルを提出し、コミュニティへ「還元する側」に回ることに挑戦したいです。 2025年は視野が広がり、エンジニアリングの本質的な面白さに気づけた1年でした。2026年もこの高揚感を忘れず、さらに成長していきたいと思います。 最後に この記事が、「開発にもっと向き合いたい」「環境を変えてみたい」と感じるきっかけになれば嬉しいです。スタメンでは、「人と組織」を大切にしながら開発できる仲間を募集しています。コードを書くことにとどまらない、学びと刺激のある日々を一緒に送りませんか? herp.careers
はじめに こんにちは、スタメンにてエンジニアインターンをしております中村です。 「 TUNAGチャット 」では、データベースとしてGoogle Cloud(旧GCP)のAlloyDB for PostgreSQLを利用しています。以前から平日昼間のCPU使用率は55~65%の水準でしたが、ある時期からデプロイのタイミングでDBのCPU負荷が100%に達し、一時的に障害となる事態が問題となりました。 一週間で2度、CPU使用率が100%までスパイクした様子 本記事では、以下のアプローチで行ったパフォーマンスチューニングの詳細を書きました。 DB負荷の原因を探る :Google Cloudの機能を利用した原因調査 クエリの改善 : 統計情報を阻害していたSQLアンチパターンの解消 実行回数の削減 : アプリケーションロジックの見直し 今回のDB負荷の原因は「オプティマイザの行数推定ミス」というRDBに一般的なもので、クエリ改善により15~20倍に高速化できました。 1. DB負荷の原因を探る 改善のために、「なにが」「いつどこで」「なぜ」起きているのかを特定する必要がありました。以下のステップで調査を進めました。 スロークエリの特定 「なにが」負荷原因であるかを確認するため、AlloyDBの標準機能であるQuery Insightsを使用しました。 Query Insightsは高負荷なクエリを可視化し、実行計画までWeb上で確認できる分析ツールです *1 。 順位表を確認すると、Top 5のクエリだけでDB負荷の大半を占めていることが判明しました(下図)。 Query Insightsの順位表 また通常時・スパイク時それぞれで負荷割合の時系列グラフを見ると、以下のことがわかりました。 これらのクエリは、特定の時間だけでなく昼夜問わず恒常的に実行されている スパイクが発生したリリースで「新しい高負荷クエリ」が追加されたわけではなく、既存のクエリ構成のまま負荷が急増していた 左:通常時 右:スパイク時 呼び出し元の特定 次に、Top5のクエリを発行するAPIが「いつどこで」叩かれているかを調査しました。 アプリケーションの実装から「どこ」の当たりをつけつつ、Google CloudのLog Analyticsを使用して「いつ」叩かれているか確認します。 Log Analyticsでは、Google Cloudのプロジェクト単位で保存されているログに対してSQLを実行できます。TUNAGチャットではL7のHTTPロードバランサー(LB)を使用しているので、LBのログから特定のエンドポイントのアクセス傾向を分析できます。 以下のSQLを実行し、APIごとのリクエスト数を時系列で集計しました。 SELECT TIMESTAMP_TRUNC( timestamp , HOUR) as log_hour, count (*) as request_count FROM `{PROJECT_ID}.{LOCATION}._Default._Default` WHERE resource . type = " http_load_balancer " AND log_name LIKE " %/logs/requests " AND http_request.request_url LIKE " %{任意のAPIエンドポイントの部分文字列}% " GROUP BY log_hour ORDER BY log_hour DESC Log AnalyticsでのSQL実行結果 調べたAPIからは、深夜帯にも一定のリクエストがあり、クエリ負荷が深夜帯にもあることと一致しています。API発行元は、スパイクに関連していそうな場所として「リロード時の初期情報取得」・「WebSocket再接続時の情報取得」がありました。 スパイクを引き起こした原因の予想 ここまでの調査から「なぜ」起こったかを予想すると、 DB負荷の高いクエリがプロダクトに存在し、普段は分散して実行されることで耐えていた。 デプロイによるサーバー再起動がトリガーとなり、既存のWebSocket接続が一斉に切断されてクライアントが一斉に再接続を試みた。 「リロード時」「再接続時」に含まれていた高負荷クエリが瞬間的に集中し、CPU使用率のスパイクを引き起こした。 つまり、「重いクエリを放置したまま、アクセスが一点集中する状況を作ってしまったこと」がスパイクの直接的な原因のようでした。 再発防止のためには、この「重いクエリ」自体を軽量化する必要があります。 2. クエリの改善 スパイクの原因はデプロイ時のコード変更ではなく、既存実装の「重いクエリ」にあることがわかりました。 なぜか選択されるNested loop Query Insightsで高負荷な5つのうち1つのクエリの実行計画を確認したところ、Nested Loopがボトルネックとなっていました。 Query Insightsの実行計画 Nested loopが最大レイテンシとなっている SeqScanが重いことも注目すべきですが、SeqScanで40000行返されているのにも関わらず、オプティマイザ(クエリプランナー)がNestedLoopを選択していることがわかります。 なぜNested loopが選択されたのかを確認するため、実際のDBに対して再度実行計画を取得しました。 QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------- Finalize GroupAggregate (cost= 1000 . 42 .. 6444 . 51 rows = 3 width= 56 ) Group Key: threads.threadteamid -> Gather (cost= 1000 . 42 .. 6444 . 45 rows = 3 width= 56 ) // 計2プロセスで並列処理(リーダー+ワーカー 1 人) Workers Planned: 1 -> Partial GroupAggregate (cost= 0 . 42 .. 5444 . 15 rows = 3 width= 56 ) Group Key: threads.threadteamid -> Nested Loop (cost= 0 . 42 .. 5444 . 08 rows = 5 width= 32 ) // ⬇️threadsに対して、1プロセスあたり 238 行と見積もられている -> Parallel Seq Scan on threads (cost= 0 . 00 .. 3496 . 03 rows = 238 width= 51 ) Filter: (((threadteamid)::text = ' XXXXXXXXXXXX ' ::text) AND ( COALESCE (threaddeleteat, ' 0 ' ::bigint) = 0 )) -> Index Scan using threadmemberships_pkey on threadmemberships (cost= 0 . 42 .. 8 . 19 rows = 1 width= 35 ) Index Cond: (((postid)::text = (threads.postid)::text) AND ((userid)::text = ' XXXXXXXXXXXX ' ::text)) Filter: following オプティマイザは統計情報をもとに行数推定を行い、その値をみて実行計画を最適化します *2 。 rows が各プロセスでの行数推定の値であり、値は『テーブルにWhere句を適用したときに残っている見積もり行数』を意味します。 Query Insightsの実行計画に、SeqScanで返された行数が40767とあるので、実態としては40000*2(プロセス数)= 80000行程度がthreadsテーブルから返されています *3 *4 。 しかし実行計画上の推定行数( rows )はたったの238行と、極端に過小評価されていることがわかりました。 WHERE句で関数を使うと、行数推定に統計情報が使えない 行数推定が狂った原因は、以下のようにWHERE句で関数を使用していたことでした。 WHERE (~~ AND COALESCE (Threads.ThreadDeleteAt, 0 ) = 0 ~~) 行数推定の概要については、こちらのスライド *5 が私のような初心者にもわかりやすくまとまっていました。 推定行数は以下の式で計算されます *6 推定行数=『濃度(テーブルの総行数)』×『選択度(WHERE句にマッチする割合)』 『濃度』『選択度』は、それぞれPostgreSQLが統計情報として持っています。 今回のテーブルの濃度を取得してみます *7 。 SELECT relpages, reltuples FROM pg_class WHERE relname = ' threads ' ; relpages | reltuples ----------+----------- 2701 | 90908 また『選択度』はオプティマイザーがWHERE句の条件文を見て、統計情報を使える場合は使い、使えない場合は以下のようなデフォルト値 *8 を使用します。 /* default selectivity estimate for equalities such as "A = b" */ #define DEFAULT_EQ_SEL 0.005 /* default selectivity estimate for inequalities such as "A < b" */ #define DEFAULT_INEQ_SEL 0.3333333333333333 /* default selectivity estimate for range inequalities "A > b AND A < c" */ #define DEFAULT_RANGE_INEQ_SEL 0.005 /* default selectivity estimate for multirange inequalities "A > b AND A < c" */ #define DEFAULT_MULTIRANGE_INEQ_SEL 0.005 /* default selectivity estimate for pattern-match operators such as LIKE */ #define DEFAULT_MATCH_SEL 0.005 /* default selectivity estimate for other matching operators */ #define DEFAULT_MATCHING_SEL 0.010 ... 値を比較する際、 COALESCE(Threads.ThreadDeleteAt, 0) = 0 のように関数を通すとオプティマイザが出力を予想できず、『選択度』に最頻値などの統計情報が使えなくなります。その結果、今回はイコール式が条件文のため、オプティマイザは『選択度』として最初の行にある0.005を使用します。 今回の並列処理(2プロセス)に当てはめてざっくりと行数推定してみると、『濃度』*『選択度』=90908*0.005=454.54、1プロセスあたり454.54÷2(プロセス数)=227.27と、オプティマイザが計算した238に近い値になりました。 つまり、データの実態を無視して、Where句を適用した後の行は「全体の0.5%しかない」と見積もられた結果、4万行のループ処理が走っていたようです。 実行計画が実態を反映するようになった 負荷Top5のクエリ全てで、WHERE句に COALESCE が使用されていたため、統計情報が正しく機能するシンプルな比較式に書き換えました。 WHERE (~~ AND (Threads.ThreadDeleteAt = 0 OR Threads.ThreadDeleteAt IS NULL )) ~~) この修正により、オプティマイザは行数を約8万件と正しく認識するようになり、Nested Loopから Hash Join を選択するようになりました。 QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------------- GroupAggregate (cost= 4047 . 87 .. 8320 . 14 rows = 3 width= 56 ) Group Key: threads.threadteamid -> Hash Join (cost= 4047 . 87 .. 8312 . 34 rows = 1554 width= 32 ) Hash Cond: ((threads.postid)::text = (threadmemberships.postid)::text)     // ⬇️実態が反映されている -> Seq Scan on threads (cost= 0 . 00 .. 4052 . 55 rows = 80727 width= 51 ) Filter: (((threaddeleteat = 0 ) OR (threaddeleteat IS NULL )) AND ((threadteamid)::text = ' XXXXXXXXXXXX ' ::text)) -> Hash (cost= 4026 . 18 .. 4026 . 18 rows = 1735 width= 35 ) -> Bitmap Heap Scan on threadmemberships (cost= 34 . 63 .. 4026 . 18 rows = 1735 width= 35 ) Recheck Cond: ((userid)::text = ' XXXXXXXXXXXX ' ::text) Filter: following -> Bitmap Index Scan on idx_thread_memberships_user_id (cost= 0 . 00 .. 34 . 19 rows = 1836 width= 0 ) Index Cond: ((userid)::text = ' XXXXXXXXXXXX ' ::text) 左:クエリ変更前 右:クエリ変更後 3. 実行回数の削減 クエリ単体の軽量化に加え、アプリケーション層でも「そもそもそのクエリを投げる必要があるか」を見直しました。 調査の結果、5つの高負荷クエリのうち一部は、特定の画面状態によっては実行が不要なものであることが判明しました。特にデプロイ後に負荷が集中する「リロード時」や「WebSocket再接続時」において、ロジック上不要なクエリの発行をフラグ制御でスキップするように修正しました。 エンドポイントのフラグ置換(青→緑)によりクエリ発行数を削減 改善結果 CPU使用率 赤棒は左から、応急措置として行なったスケールアップ(左の赤線)、元スペックへのスケールダウン(右の赤線)を示しています。対策適用を行なった以降は、インスタンススペックを元に戻した後も、 CPU使用率20〜30%程度 で安定して推移しています。 右の赤線以降、20-30%の使用率で落ち着いている クエリ実行速度 実行計画が適正化されたことで、レイテンシが劇的に改善しました。 平均実行時間は 約400msから20ms台 へと短縮され、 15~20倍高速化されました 。 番号 修正前 修正後 改善倍率 ① 436.71 ms 20.87 ms 約 20.9倍 ② 435.86 ms 20.75 ms 約 21.0倍 ③ 440.62 ms 29.06 ms 約 15.2倍 ④ 369.46 ms 25.20 ms 約 14.7倍 ⑤ 369.16 ms 24.99 ms 約 14.8倍 まとめ Where句で安易に関数を使うと、オプティマイザが行数推定を誤り、データ量に見合わない非効率な実行計画が選択される原因になります。 またスロークエリの原因を探る際は、単に「インデックスがあるか」だけでなく、オプティマイザの実行計画が正しいかどうかに注意する必要があることが、今回の調査で身にしみてわかりました。 最後に 今回、Google Cloudを触るのもDBチューニングも初めてでしたが、DB負荷改善という明確な目標に取り組みながら学ぶことで、自分にとって良い経験になりました。 まだまだGC,DBともに知らないことが多くあるので、業務に活かせるよう引き続き学んでいきたいと思います。 本記事は、会社で契約しているGeminiに校正をしてもらいました。 スタメンには、新人のうちから色々なことを経験させてもらえる文化があります。またエンジニアだけでなく全部署でAI活用を推進しています。少しでも興味を持たれましたら、以下フォームからぜひご応募ください! herp.careers *1 : Query Insights を使用してクエリのパフォーマンスを向上させる *2 : PostgreSQL 17.6文書 パート VII. 内部情報 第68章 プランナは統計情報をどのように使用するか *3 : パラレルクエリにおけるプロセス数について *4 : 上の補足 *5 : PostgreSQL:行数推定を読み解く/row-estimation *6 : PostgreSQLにおける行数推定の計算方法 *7 : オプティマイザが濃度として参照する値 *8 : GitHubで確認できる、PostgreSQLの選択度デフォルト値
はじめに こんにちは!株式会社スタメンの ちぇる です! 長らく動画変換を支えてきた Amazon Elastic Transcoder(以下 Elastic Transcoder)の EOL(サポート終了)が発表されました。AWS 公式でも AWS Elemental MediaConvert(以下 MediaConvert)への移行 が強く推奨されています。 弊社が運営する「TUNAG」でも、これまで Elastic Transcoder と MediaConvert が混在していましたが、この度 MediaConvert への全面移行を完了しました。本記事では、この移行プロジェクトを通じて学んだ「変換システムの思想の違い」と「設計の健全化」についてご紹介します。 動画変換システムの仕組み:思想の違い 両サービスを比較すると、動画変換に対する根本的な設計思想が異なることに気づきます。 Elastic Transcoder:Pipeline 中心設計 Elastic Transcoder の設計思想は、一言で言えば 「固定された変換ライン(Pipeline)に Job を流し込む」 方式です。 Pipeline: 入出力バケットや IAM ロール、SNS 通知などを定義する。 Job / Preset: 解像度やコーデックを定義し、Pipeline にキューイングする。 あらかじめ定義された「変換ライン」の処理能力に応じて順次実行されるため、管理はシンプルですが、入力動画の特性に合わせた柔軟な調整には限界がありました。 MediaConvert:Job 中心設計 対する MediaConvert の思想は 「動画 1 本ごとに詳細な注文票(Job)を作り、Queue でさばく」 方式です。 Job: 入出力設定、動画・音声の細かなパラメータ、加工設定など全ての情報を持たせる。 Queue: 並列実行数や優先度を制御する。 MediaConvert には Pipeline という概念がなく、動画ごとに Job を定義します。そのため、「入力動画の特性に合わせて解像度・ビットレートを動的に最適化する」 といった、より高度でプロフェッショナルな制御が可能になります。 「Pipeline(変換ライン)中心」から、より柔軟な「Job(注文票)中心」への転換が、このリプレイスの本質といえます。 移行で直面した「厳格さ」という注意点 移行を進める中で、特に注意が必要だったのが 「 音声のみ(映像トラックなし)のデータ 」 の扱いです。 「よしなに」の Elastic Transcoder、「厳格」な MediaConvert Elastic Transcoder: 映像+音声の設定になっているプリセットに「音声のみ」のファイルを投げても、足りない要素(映像)を自動でスキップして「音声のみ MP4」をよしなに出力してくれます。 MediaConvert: 商用レベルの品質を担保するため、バリデーションが非常に厳格です。映像設定があるのにソースに映像がない場合、「設定と入力が一致しない」としてエラーになります。(実装側で、入力に応じて Video Selector を動的に除外するといった制御が必要となります。) そのため、Elastic Transcoder では「よしなに」処理されていたファイルが、MediaConvert ではエラーとなり、「以前は投稿できていたファイルがアップロード(変換)できなくなる」という問題が発生する可能性があります。 厳格さが生む、運用の健全化 一見すると MediaConvert は手間がかかるように見えますが、これはサービス運営において大きなメリットになります。 例えば、これまでは動画アップロード導線に誤って音声データが混入した場合でも、「映像のない真っ暗な動画」として変換されてしまい、タイムラインに黒いフレームが表示されるといった状況が起こり得ました。今回の移行に伴い、「仕様の曖昧さ」を排除することで、コンテンツの種別に応じた最適な見せ方を強制できるようになったのです。 具体的には、音声データについては「音声専用のアップロード導線を用意する」か、「サムネイルを付与したうえで動画としてアップロードする」かの、いずれかの方法を取ることになります。 こういった仕様は一般的で、例えば YouTube でも音声データをそのまま動画としてアップロードすることはできず、公開には映像トラック(静止画を含む)が必須となっています。 まとめ TUNAG には複数の動画投稿導線が存在しており、新旧のシステムが混在していましたが、今回のリプレイスを通じて MediaConvert への一本化が完了しました。 サービスを運用する中で、EOL によるリプレイスを迫られるのは避けられない宿命です。しかしそれは同時に、 「システムの曖昧さを整理し、設計を健全化する絶好の機会」 でもあります。 今回の移行を経て、TUNAG の動画基盤はより堅牢で、今後の進化に耐えうるものになりました。本記事が、同じく動画変換基盤の運用やリプレイスに取り組んでいる方々の参考となれば幸いです。 最後に 株式会社スタメンでは、エンジニアを絶賛大募集中です! 技術の力でサービスをより良くしていきたい方、ぜひ一度お話ししましょう。詳しくは以下をご覧ください! herp.careers
目次 目次 はじめに 移行背景 Web Push通知の仕組み バックエンド側の実装 フロントエンド側の実装 トークンのライフサイクル管理 移行工程 最後に はじめに こんにちは。スタメンでTUNAGのバックエンド開発を行なっている きいろ です。 TUNAGは組織活動を支援するサービスで、Webアプリとモバイルアプリの両方で提供しており、Webアプリではユーザーアクションをリアルタイムに届けるためのWeb Push通知機能を備えています。 TUNAGの代表的な機能として「制度」があり、そこから社内報など情報共有のための各種コンテンツ投稿が行えます。 それら投稿のリアルタイム通知を行うなど、Web Push通知機能はプロダクトのユーザー体験に直結する重要な仕組みとなっています。 今回、そのWeb Push通知機能について、Firebase Cloud Messaging(以下、FCM)をベースに移行を行いました。 この記事では、移行当時の調査内容や、バックエンドおよびフロントエンドの両面で直面した課題、そこから得られた知見を紹介します。 同様にFCMを用いたWeb Push通知の導入や移行を検討されている方にとって、何かしら参考になれば嬉しいです。 移行背景 TUNAGでは長らくPushCrew(現 VWO Engage)を利用し、Web Push通知機能を提供していました。 PushCrewはWeb Push通知機能を手軽にサービスに導入できる一方、長期運用をする中で通知用トークン管理や料金面などの課題が上がっていました。 また、通知設定をPushCrewから提供されるサブドメイン上で行うため、通知の再設定フローがやや複雑になりやすいなど、運用面での細かい課題も表面化していました。 移行先としてFCMを選んだ理由は、すでにモバイルアプリの通知基盤として活用しており、開発環境ごとにFirebaseプロジェクトが存在していたこと、認証まわりの仕組みも整っていたことが大きいです。 新規構築する部分が比較的少なく、既存リソースを活かしながらWeb側の通知基盤も統合できる点が、移行先への決め手となりました。 Web Push通知の仕組み PushCrewからFCMへの移行にあたり、最も大変だったものがFCMの導入設計です。 当時の自分はWeb Push通知に関する知識が充分とは言えず、既存機能の連携構造も把握しきれていなかったため、Web Push通知送信に関する既存実装、PushCrew APIの仕様と利用形式を主軸に、地道にFCMの導入方針を定めていきました。 そもそもとしてのFCMベースでのWeb Push通知の仕組みについてですが、ブラウザ含めて以下4つの主要コンポーネントによって成立します。 コンポーネント 役割 Browser Web Push通知の受信 Push Service 通知の中継サービスその1。通知先エンドポイント管理。ブラウザに直接Web Push通知を届ける FCM 通知の中継サービスその2。アプリケーション側のリクエスト起点でWeb Push通知を送信する Application Web Push通知の送信 実際にWeb Push通知を行う場合は、(1)トークン作成→(2)Push通知送信の2ステップで行います。トークンとはWeb Push通知配信サービスを利用する際に、サービス事業者側でクライアント管理するために個別に発行するIDのことを表現しています。ID名称は通知配信サービス毎に異なりますが、この記事ではまとめて「トークン」と表記します。 Web Push通知を行う際の、具体的なコンポーネント間の連携フローは以下の通りです。 青線:トークン作成 赤線:Push通知送信 Web Push通知の処理フロー Push通知先のエンドポイントの発行管理はPush Service(Push Subscription)で行い、FCMはdevice tokenを通じてPush Subscriptionを管理します。 実際の通知送信時は、アプリケーション側はFCM APIを通じてdevice tokenをFCM側に連携し、FCMはdevice tokenをもとに対応するPush Subscriptionを内部で参照、その後Push Serviceを経由してWeb Push通知の送受信が行われます。 ちなみにですが、FCMを通知配信基盤として利用する場合、Firebase SDKがこれらを抽象化してくれるため、Web Push通知の内部構造やPush Serviceの存在を強く意識する必要はありません。 実際の実装手順としては以下のようなシンプルなものになります。 Service Workerの登録 Firebase SDKから提供される getToken() を用いてdevice tokenを取得 device tokenの保存 send API 呼び出し時に保存したdevice tokenをpayloadに含めて通知実行 一方で、実装を進める中で「Push通知時の送信先エンドポイントはどのように管理されているのか」といった点がブラックボックス的に映り、実装方針の構築に時間を要した部分もありました。 そのため通常の実装では深く意識しなくてもよいものの、背景として知っておくと挙動の理解がしやすくなるWeb Push通知の仕組みについて、整理の意味も兼ねて簡単にまとめました。 バックエンド側の実装 バックエンド側の実装設計を行うにあたり、軸にしたものはPushCrew APIとFCM APIの仕様差分でした。移行時点でTUNAGで行うWeb Push通知の利用形式として、以下3つのパターンがありました。 単一トークン通知 複数トークン同時通知 時刻指定通知 PushCrewはこれらの通知形式をAPIとして個別に提供してくれていたため、バックエンドではトークンを用意して用途に応じたAPIを呼び出すだけで通知制御が完結していました。 しかし、FCMが提供するものは単一トークン通知用API(先のsend API)のみです。そのため、複数トークン同時通知・時刻指定通知の制御はアプリケーション側で構築する必要がありました。 また、トークンはユーザーに紐づくものですが、厳密にはユーザーのクライアント環境に応じて発行されます。同一ユーザーであっても、複数のデバイスからログインした場合はそれぞれ別IDとしてトークン発行が行われます。 そのため、アプリケーション側では ユーザー:トークン = 1:Nの対応関係を保った管理 が必要となります。 このデータ構造を考慮した上で、複数トークン同時通知および時刻指定通知の制御は、Sidekiqを用いて以下のような処理フローを設計しました。 イベント発火時に通知対象ユーザーを決定 ユーザー単位で通知ジョブを発行 各ユーザーに紐づくトークンを取得 トークン単位でFCM API呼び出しジョブを発行 %%{init: {'theme': 'dark'} }%% sequenceDiagram participant caller as caller participant worker1 as UserWorker participant worker2 as TokenWorker caller->>worker1: perform(user_id,msg) activate worker1 worker1->>DB: user取得 activate DB DB->>worker1: user deactivate DB worker1->>DB: token取得 activate DB DB->>worker1: token deactivate DB loop token_id worker1->>worker2: perform(token_id,msg) deactivate worker1 Note over worker2: Web Push通知実行 end 複数トークンへの同時通知については通知時に並列でジョブを走らせることで対応し、時刻指定通知においてはジョブそのものの実行時間指定で制御しています。 Web Push通知は複数の機能から利用される前提の機能であるため、通知内容と対象ユーザーを指定するだけで利用できるインターフェースに統一し、内部処理はSidekiqで吸収する方針にしました。 通知処理中の失敗に対するリトライ制御についてもアプリケーション側で個別に持たず、Sidekiqの標準機能に集約することで、通知処理全体の見通しが良くなるようにしました。 手探りの実装でしたが、他機能との連携や拡張性を見据えた構成にできたかなと思っています。 フロントエンド側の実装 バックエンド側のコアロジックが固まった後は、フロントエンド側の移行を行います。フロントエンド側で移行が必要な箇所は以下2つです。 トークン発行処理 通知表示処理(フォアグラウンド通知、バックグラウンド通知) まず、トークン発行処理ですが、ここでもPushCrewとFCMの仕様の違いが大きく影響しました。 FCMでのトークン発行処理はFirebase SDKが提供する getToken() 関数を用いて行う想定で、その前段としてService Workerの登録が必要となります。 Service Workerは、ブラウザに登録されるJavaScriptスクリプトで、Web Push通知機能に関してはプッシュ通知イベントが発生した際(Web Push通知を受信した際)にブラウザがワーカーとして実行し通知表示の処理を行います。 PushCrewでは、Web Push通知に必要となるService Workerが公式CDNから提供されていたため、トークン発行時にはそのエンドポイントの呼び出し、取得したスクリプトをService Workerとして登録をするだけでWeb Push通知表示が可能でした。一方で、FCMを用いたWeb Push通知では、Service Worker向けのスクリプト配信自体をサービス事業者側で担う必要があり、登録用のエンドポイントを含めた配信設計から検討する必要がありました。 このWeb Push通知処理に向けたService Worker取得用のエンドポイントを実装する上で、Web Push通知を受け取るWebサービスのドメイン直下で取得する、ということが重要になります。(例: https://service-domain/service-worker.js など) これはFCM公式でも推奨されているプラクティスになります。 というのも、Service Workerがイベントハンドリングにおいて制御可能な範囲は、スクリプトを取得したURLのパスに依存しており、その配下にあるページやリクエストのみが対象となるからです。 例えば、Web Push通知を受けとり、そこに仕込まれたURLリンクに直接遷移する際、遷移先で特定要素にフォーカスを当てるなどの挙動が可能ですが、そのフォーカスを当てられるページは、Service Workerが取得したURLパスに依存します。 ドメイン直下で取得しない場合、先の例だと遷移先ページによってはフォーカス処理がうまく動作しない、などの通知挙動の違いが外部起因によって生まれてしまうことになります。 そのため、Web Push通知に関するService Workerの取得エンドポイントはドメイン直下で取得することが推奨されています。 TUNAGでもこのプラクティスに沿ってエンドポイントを実装しようとしたのですが、既存のアプリケーション構成との兼ね合いから、ドメイン直下でのスクリプト配布ができない状況でした。 この制約により、Service Workerの配置パスやスコープ設計を検討する必要が生じ、一部のページが制御対象外となるリスクも含めて実装判断を行うことになりました。 最終的に、先のService Workerスコープ仕様と既存の通知仕様を踏まえ、直近は問題にはならないと判断し、ドメイン直下ではなくサブパスを挟んでのService Worker登録を行う運用になりました。 通知表示処理の移行ですが、こちらは公式プラクティス通りの実装を行ったため割愛します。 トークンのライフサイクル管理 Web Push通知機能は通知先クライアントの識別用にトークンを利用しますが、これはクライアントの持ち物であり、Service Workerのライフサイクルと連動します。 これまで利用していたPushCrewでは、通知用Service Workerが専用ドメインで管理されており、トークンの有効期間についても長期利用を前提とした仕様でした。 そのため、クライアント側でトークンを定期的に再取得する必要性は高くありませんでした。 一方で、FCMではトークンは長期利用を想定したものではなく、トークンそのものが不意に利用出来なくなることを前提としたリソースとして扱っています。 そのため、今回のFCM移行において、適当なタイミングでの再取得処理を実装する必要が出てきました。 これらを踏まえ、最終的に実装した一連のトークン管理フローは以下の通りです。 %%{init: {'theme': 'dark'} }%% stateDiagram-v2 direction LR state トークン管理 { 取得 --> 保存 : 差分あり 取得 --> 破棄 : 差分なし 保存 --> 取得済み 破棄 --> 取得済み } [*] --> 取得可否を判定 取得可否を判定 --> 取得 : true 取得可否を判定 --> 取得不可 : false 取得済み --> 取得可否を判定 : 特定導線を踏む 取得不可 --> [*] TUNAGでは通知の安定性を維持するため、ユーザーアクションに紐づけてトークンの再取得を行い、トークンの鮮度を保つことにしました。 また、バックエンド側で実際に通知を行う際は通知APIのレスポンスを元に、利用期限に達したトークンについてはその時点で破棄する処理も組み込み、不要なトークンがアプリケーション上で残り続けないようにしています。 移行工程 実際の基盤移行については、TUNAGの既存ユーザーへの影響が避けられないため、以下4つのフェーズを策定して進めました。 フェーズ 通知状況 FCM移行前 PushCrew APIを用いて通知 FCM移行中 ユーザーのトークン状況に応じてPushCrew APIとFCM APIを使い分け FCM移行後 FCM APIを用いて通知 PushCrewトークン削除 FCM APIを用いて通知 FCM移行中のフェーズにおいてはTUNAGユーザーを以下の3パターンで分類し、移行の前後で互換性を持たせることでトークン保持状況の移行期間を設けました。 ユーザーパターン 通知形式 PushCrewトークンのみ保持するユーザー PushCrew APIを用いて通知 両トークンを保持するユーザー FCM APIを用いて通知 FCMトークンのみ保持するユーザー FCM APIを用いて通知 以上を踏まえ、移行に関する全体工程は以下の通りとなりました。 通知基盤の移行フロー PushCrew API切り離し時期の策定において、どのタイミングで行ったとしても一部の移行前ユーザーへの影響が避けられない懸念がありましたが、そこは十分な移行期間を設けることで影響の最小化を行いました。 移行期間の区切りについては、FCMトークン総数の増加を時系列でモニタリングしつつ、その傾向が安定化する時期までとしました。 その時点で移行が済んでいないユーザーについてはWeb版を積極的に利用していない層と扱いリスク許容をした上で、最終的なPushCrew APIの切り離しに至りました。 最後に 今回の記事では、TUNAGにおけるFCMへのWeb Push通知基盤移行で直面したポイントを紹介しました。 自分自身、Web Push通知の実装に本格的に関わるのは初めてで、知識のない状態から手探りで進めた部分も多かったのですが、社内のエンジニアの方々に細かく相談しつつ、最終的には無事移行することができました。 設計や実装の節目ごとに意見をもらえたことで、現在は大きな問題もなく運用できています。 今後も安定した通知体験を提供できるよう引き続き改善していく予定です。 FCMはメジャーなサービスではありますが、Web Push通知の仕組みなど前提知識が求められるケースも多く、必要最小限のAPIのみが提供されているため、移行元サービスとのギャップに戸惑う場面もあるかと思います。 この記事が同じようにFCMの導入を検討されている方々の手助けになれば嬉しいです。 スタメンでは、Rubyエンジニアに限らず全技術領域で、共にTUNAGを成長させていきたいエンジニアを募集しています。 ご興味いただけましたら、ぜひまずはカジュアル面談からご応募いただけると嬉しいです。皆さんとお会いできることを楽しみにしています。 herp.careers
はじめに こんにちは、プロダクト開発部の 勝間田 です。 非同期処理は、即時の応答が不要な処理をバックグラウンドで並行処理することでユーザー体験を向上させるものであり、私たちのサービス TUNAG(ツナグ) では主にSidekiqを利用しております。 即時の応答が不要な処理とはいえ、そこで大きな 遅延(Latency) が発生してしまえば、ユーザー体験を損ねることにつながってしまいます。 Sidekiqの設定でキューの重みづけを行い、ユーザーへのインパクトが大きいジョブが優先的に処理されるよう工夫はしていましたが、優先度の低いキューとはいえ、その遅延が大きくなることがありUXに少なからず影響が出始めていました。 これまでスケーリングについては ECSタスクのCPU使用率ベースのオートスケーリング に任せていました。 それとは別で定期実行によりSidekiqのキューが一定数以上滞留してしまった場合はSlackにアラートを通知し、エンジニアがそれに気づいて手動でECSのタスク数を増やす、という運用を行なっていました。 こうした属人的な対応・検知から遅延解消までのタイムラグ・休日対応が難しい点などが課題となっており、これらを解消すべく今回の仕組みを導入するに至りました。 また この記事では下記のことについては触れませんので、あらかじめご了承ください 。 CloudWatchカスタムメトリクス送信の具体的な実装 各AWSリソースの具体的な設定手順 余談ですが、最近サブサービスで Solid Queue を導入しました。導入の背景や手順について別のブログで紹介しておりますので、ご興味のある方はぜひご覧ください。 tech.stmn.co.jp 対応内容 これらの課題を解決するため、監視する指標をCPU使用率から SidekiqのキューLatency(遅延時間) そのものに変更することにしました。 やりたいこと:キューのLatencyが一定の閾値を超えたら、エンジニアの手を介さず自動でECSタスクをスケールアウトさせる 具体的な対応内容は以下の通りです。 Napkinを使ってみました 1. LambdaでアプリのAPIよりSidekiqのLatencyを取得 Sidekiqには、キューやワーカーの状態を取得するためのAPIがあります。 github.com 以下のようなコードで簡単にキューの情報を取得することができます。 stats = Sidekiq :: Stats .new stats.queues.keys.map do |key| queue = Sidekiq :: Queue .new(key) { name : queue.name, size : queue.size, latency : queue.latency } end このSidekiq APIを利用してLatencyを取得する方法として、「LambdaからSidekiqのデータストア(Redis)に直接接続する」のではなく、 「アプリケーション(Rails)側に、Sidekiq APIを呼び出して結果をJSONで返す専用のエンドポイントを実装する」 ようにしています。 このようにすることで以下のメリットがあると思います。 Lambda側でRedisとの接続が不要になるので、あらゆる設定をする必要がない。 将来SidekiqのバージョンアップなどでAPIの仕様が変わったとしても修正はアプリケーションのみとなり、Lambda側のコードは一切変更する必要がなく返却されるLatencyに集中すればよくなる。 2. CloudWatchカスタムメトリクスにQueueのLatencyを送信 Latency情報をオートスケーリングのトリガーとして使えるよう、CloudWatchに送信します。 TUNAG(ツナグ) では、処理する機能が大きく異なる非同期処理についてそれぞれ異なるECS Serviceで動かしているため、個別にオートスケーリングさせる必要がありました。 そのためメトリクス名は同じ Latency としつつ、CloudWatchの ディメンション を利用してECS Serviceごとに設定することで、CloudWatchアラームがLatencyを個別に監視し、対応が必要なECS Serviceのみをスケールアウトさせることができます。 3. EventBridgeをLambda実行のトリガーに設定 用意したLambdaを定期実行するために、トリガーとしてEventBridgeのスケジュールを設定しました。 スケールアウトが後手後手にならないようある程度短めにLambdaを実行するようルールを設定しています。 4. CloudWatch アラームによる監視と発火 CloudWatchに Latency メトリクスが送信されるようになったので、次はこのメトリクスを監視するCloudWatchアラーム を作成します。 アラームも個別に作成することができ、サービスごとに独立したスケーリングができるようにしています。 下の画像のように、ECS Serviceで分けたディメンション単位でアラームを設定しています。 CloudWatchメトリクスの参照タブ抜粋 5. ECSオートスケーリングポリシーの実行 作成したCloudWatchアラームが ALARM 状態になることをトリガー として、ECSタスクを増やすスケーリングポリシーを設定します。これにより、Latencyが高くなったらECSタスクが設定した数だけ自動でスケールアウトするようになります。 ここまででやりたいことの「キューのLatencyが一定の閾値を超えたら、エンジニアの手を介さず自動でECSタスクをスケールアウトさせる」が実現できます。 ただスケーリングポリシーを設定する上で、コストとパフォーマンスのバランス調整に悩みました。「いつ、どのくらいタスクを増減させるか」という判断には、2つの課題があります。 スケールアウトから解消までのタイムラグによる「過剰スケールアウト」のリスク ECSタスクを増やしてから、そのタスクが起動し、キューのLatencyが実際に解消されるまでにはタイムラグが発生します。 もし、アラームが発火し続ける間タスクを増やし続ける設定にすると、本来なら(起動中のタスクで)捌ききれる量だったにも関わらず、過剰にタスクを増やしてしまい、無駄なコストが発生する恐れがあります。 スケールインによる「ピーク時パフォーマンス低下」のリスク 非同期処理の特性上、平常時はLatencyが0に近い状態が続きます。もしLatencyの値だけを見てタスクを減らすと、ピーク時以外の時間帯でタスクが必要最低限まで減りすぎてしまいます。 その結果、次のジョブのピークが来た際に、スタート時点のタスク数が少なすぎるため、そこからスケールアウトを始めても処理が間に合わなくなる恐れがあります。 上記のような課題があったため、現状は下記のような対策によりバランスをとっています。 スケーリングポリシーに 適度なクールダウン期間を設定 ECSタスクをスケールアウトさせ、新しいタスクが起動してジョブを処理し始めてもすぐにLatencyが解消されるわけではありません。このタイムラグを考慮しないと、Latencyがまだ高い状態を見てアラームが発火し続けることで必要以上にタスクを増やしてしまい、過剰なスケーリングが発生する可能性があります。 そのためスケーリングポリシー側に適切なクールダウン期間を設けることで、「一度スケールアウトしたら、次のスケーリング判断まで一定時間待機する」ように設定し、過剰なスケールアウトを防ぐようにしました。ただこの期間についても増やすタスクの数と同様でサービスによって適切な値が異なってくると思います。最初は短めに設定しつつ、Latencyの解消具合を見ながら微調整していくと良さそうに思いました。 キューのLatencyによる「スケールイン」をあえて設定しない 非同期処理の特性上、アラームがOK(Latencyが低い)状態の時間帯のほうが多いため、OK状態をトリガーにタスク数を減らしてしまうと、次のジョブのピーク(エンキューが急増する時間帯)が来た際に、タスク数が少ない状態からスケールアウトをすることになります。 Latencyによりスケールアウトした時間帯は、継続してジョブがエンキューされる可能性が高い時間帯です。クールダウン期間もあるため、少ないタスク数からのリカバリーでは間に合わなくなる恐れがあります。 そのため、Latencyベースでは「増やす」ことだけを担当させ、タスクを「減らす」処理については、従来から設定してあるCPU使用率ベースのスケーリング(CPU使用率が低ければ減らす)に引き続き任せる構成としました。 現状はこのような設定で運用しております。 これらの設定についてもサービスの特性やピーク帯の有無等で変わりそうです。より良い設定があればぜひ知りたいです。 結果 Latencyベースのオートスケーリングを導入した結果、これまでキューの滞留件数が増えるたびに飛んでいたSlackアラートが、ほとんど飛ばなくなりました。 手動対応についても導入後は一度も行っておりません。 これによりエンジニアの手動対応コストを削除し、処理遅延によるUXの悪化を未然に防ぐことができました。 まとめ 今回ご紹介したSidekiqのLatencyをトリガーにしたオートスケーリングは、AWSの標準的な機能(Lambda, EventBridge, CloudWatch, ECS Auto Scaling)を組み合わせることで実現できるため、導入自体のハードルは比較的低いと感じました。 今後はより細かなチューニングもしていければと考えています。現状では閾値を超えた際に決まったタスク数を増やしていますが、Latencyの値やキューの種類によって増やす数を変えたり、監視するキューをより厳選したりなどコストパフォーマンスとのバランスを考えながら改善していきたいです。 最後に スタメンでは、Rubyエンジニアに限らず全技術領域で、プロダクトを成長させていくエンジニア、デザイナー、プロダクトマネージャーの方を募集しています。 ご興味いただけましたらぜひご応募いただけますと嬉しいです。 皆さんとお話できることを楽しみにしています。 herp.careers
🏁 はじめに 株式会社スタメンにてプラットフォーム部で SRE / DevEx などに取り組んでいるもりしたです。今回は Ruby on Rails アプリケーションに Solid Queue を導入したお話を書こうと思います。 こんな人に読んでもらえるとうれしく思います。 Solid Queue に興味がある Ruby on Rails アプリケーションで手軽に定期実行ジョブを作りたい すでに稼働している定期実行ジョブの Solid Queue へのリプレイスを検討している 💡 Solid Queueとは? DB完結型ジョブキューの基礎 Solid Queue は Active Job 向けに設計された、DBベースのキューイングバックエンドです。 Sidekiq や Resque のように Redis などの外部サービスが不要 Recurring Tasks 機能を利用することで Cronのように、設定されたスケジュールに従ってジョブを自動的に定期実行できる 利用するDBは既存アプリケーションと共存することも Solid Queue 独自で分離することもできる 📌 導入の背景 ジョブを定期実行したかったためです。 スタメンの主力事業 TUNAG(ツナグ) は TUNAGのコア部分(以降、TUNAG本体)と機能単位で切り出した複数サービスが連携して動作していますが、機能単位で切り出したタスク機能サービスで定期的に実行したい処理がありました。 しかしながら、タスク機能サービスにはジョブを定期実行する基盤がなかったため、基盤構築から始めることとなりました。 ⚙️ 既存の定期実行システム:AWS/ECS連携のアーキテクチャ まずは既存の定期実行がどのように動いているかを確認しました。 TUNAG本体では elastic_whenever gem を利用して ECS Scheduled Task にて rake タスクを実行しています。 rake タスクを追加して定期実行するまでの流れは下記となります。 プロダクトコードにて管理するスケジュールファイル config/schedule.rb で下記のように定義する every ' */5 * * * * ' do # 5 分おき rake ' scheduled:foo:bar_baz ' end デプロイ時に定義内容から Amazon EventBridge ルールを作成する Amazon EventBridge ルールのイベントスケジュールでECSタスクが起動して rake タスクが実行される ECS Scheduled Task を利用した定期処理については 2021年のテックブログでも紹介しています tech.stmn.co.jp このアーキテクチャの場合、ジョブを動かすためにAWS側の設定を行う必要がありました。 どのように実現しているのか全体像の理解がやや難しいと感じています。 ✅ Solid Queueを選んだ理由:Redis/AWSからの脱却 既存の定期実行の仕組みを把握したうえで、新しくタスク機能サービスに導入する定期実行の基盤として Solid Queue を選びました。 複雑なインフラ環境の設定はなるべく行いたくない なるべく標準以外の新しいライブラリは増やしたくなかった Solid Queue を小さいところから導入を試してみたかった ECS Scheduled Task を利用した仕組みは実績がありますが、AWS側の設定を行うなどの準備が多く、今回はなるべくその手間を減らしたかった。 スタメンでは利用ライブラリのアップデートを毎週行っており、その対象が増えるため、elastic_whenever のような gem をなるべく増やしたくなかった。一方で Solid Queue は、Ruby on Rails の一部であるため、gem が増えても許容できると考えました。 以上が理由の2つですが、結局は、 Solid Queue を試してみたい が一番の理由です。 タスク機能サービスは影響範囲が絞られた小さいサービスなので導入を試してみるには最適でした。 🖥️ Mission Control — Jobs:稼働状況の確認 後述の導入手順で設定が完了し、ジョブが動くようになったら Mission Control — Jobs で実行状況が確認できます。 Sidekiq のダッシュボードと比べると非常にシンプルな印象です。 本番で動いている Mission Control — Jobs の画面をピックアップしてご紹介します。 Recurring tasks タブ:スケジュールの一元管理 ここが Solid Queue による定期実行の最大のメリットを示す画面です。 従来、 config/schedule.rb ファイルや Amazon EventBridge ルールに分散していたスケジュール定義が、リポジトリ内の config/recurring.yml ファイルに集約され、その実行状況がこのダッシュボードで一元管理できます。 Recurring(=繰り返し)のタスクとして登録したジョブを確認できます 1つ目のジョブであれば、月〜金の 10:00 に繰り返し実行します Run now を押下するとジョブを即時実行できます Finished jobs タブ 完了したジョブが一覧で確認できます Finished jobs 詳細 実行にかかった時間などが確認できます 🛠️ 導入手順 今回導入のために行った手順です。 Solid Queue と Mission Control — Jobs を導入します。 やりたいことによっては設定が異なる可能性があるので、あくまで参考として読んでください。 前提条件 Active Record マイグレーションでデータベーススキーマのマイグレーションを行っている Solid Queue のテーブルは既存のDBに投入する。独自のDBは用意しない Solid Queue 主には Solid Queue の README および README の翻訳記事を参考にしました github.com techracho.bpsinc.jp Gemfile に gem "solid_queue" を追加する bundle install で依存する gem をインストールし、Gemfile.lock を更新する bin/rails solid_queue:install を実行する # bin/rails solid_queue:install create config/queue.yml create config/recurring.yml create db/queue_schema.rb create bin/jobs gsub config/environments/production.rb Solid Queue 用のマイグレーションファイルを作成する 前手順で Solid Queue で利用するテーブルのスキーマ定義が db/queue_schema.rb ファイルとして作られるが、このファイルができただけでは Active Record マイグレーションでDBにテーブルは作られない マイグレーションを実行するためにファイルを作成する # bundle exec rails generate migration CreateSolidQueueTables invoke active_record create db/migrate/20250819042726_create_solid_queue_tables.rb 作成した Solid Queue 用のマイグレーションファイルに db/queue_schema.rb ファイルの内容を反映する db/queue_schema.rb のテーブル名やカラム名は文字列で指定されているので文字列を Symbol に置き換え これはマイグレーションファイルは基本、 Symbol で定義しているための対応 正規表現にて文字列を置き換えた マイグレーションを実行 Solid Queue 用テーブルと外部キーがDBに反映され、合わせて db/schema.rb が更新される # bin/rails db:migrate == 20250819042726 CreateSolidQueueTables: migrating =========================== -- create_table(:solid_queue_blocked_executions, {force: :cascade}) -> 0.0317s ... 省略 ... == 20250819042726 CreateSolidQueueTables: migrated (0.3559s) ================== -- execute("SELECT extversion FROM pg_extension WHERE extname = 'citus'") Model files unchanged. 動作確認用ジョブを bin/rails generate job で作成する はじめてジョブを生成した場合、 app/jobs/application_job.rb が作られる # bin/rails generate job FooBar::BazQuxJob invoke rspec create spec/jobs/foo_bar/baz_ qux_job_spec.rb create app/jobs/foo_bar/baz_qux_job.rb create app/jobs/application_job.rb 動作確認用ジョブを修正する queue_as で指定する queue を任意で変更する 実行したことが確認できるようにログ出力だけ実装する class FooBar :: BazQuxJob < ApplicationJob queue_as :background # generate したときは default となっている def perform (*args) Rails .logger.info( " FooBar::BazQuxJob called with args: #{ args.inspect }" ) end end 自動生成された ApplicationJob を確認する self.queue_adapter = :solid_queue であること これにより、 ApplicationJob を継承したクラスが Solid Queue にて実行される class ApplicationJob < ActiveJob :: Base self .queue_adapter = :solid_queue # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked # Most jobs are safe to ignore if the underlying records are no longer available # discard_on ActiveJob::DeserializationError end config/recurring.yml にて動作確認用ジョブをスケジューリングする queue は 動作確認用ジョブ の queue_as で指定した queue と一致させる 動作確認しやすいように短い周期の1分ごとに実行する default : &default foo_bar_baz_qux : class : FooBar::BazQuxJob queue : background schedule : every 1 minutes development : <<: *default production : <<: *default config/application.rb を修正 # require "active_job/railtie" のコメントアウトを外して、ActiveJob を有効化 死活監視のために supervisor のPIDファイルを作成するようにする config.solid_queue.supervisor_pidfile = Rails .application.root.join( " tmp/pids/solid_queue_supervisor.pid " ) config/queue.yml を必要に応じて修正する 並列でジョブを実行する場合、threads、processes を調整する 以上で 動作確認用ジョブが定期実行できる状態となったかと思います。 次は動かしてみます。 動かしてみる bin/jobs で起動します。 Dockerで起動する際、以下のコマンドにて、pid ファイルを削除してから起動するようにしました。 bash -c " rm -f tmp/pids/solid_queue_supervisor.pid && bin/jobs " 実行中のアプリケーションログ アプリケーション起動〜動作確認用ジョブが2回実行されるまでのログです。 下記の3つのログが2回出力されていることから、ジョブがスケジューリングの周期でエンキューされて実行していることが分かります Enqueued FooBar::BazQuxJob Performing FooBar::BazQuxJob Performed FooBar::BazQuxJob # tail -1000f log/development.log | grep -E "SolidQueue-1.2.2|ActiveJob" SolidQueue-1.2.2 Register Supervisor (31.3ms) pid: 1, hostname: "b9fb9f17bddb", process_id: 27, name: "supervisor-b90acad5c891087512fb" SolidQueue-1.2.2 Started Supervisor (136.9ms) pid: 1, hostname: "b9fb9f17bddb", process_id: 27, name: "supervisor-b90acad5c891087512fb" SolidQueue-1.2.2 Prune dead processes (9.2ms) size: 0 SolidQueue-1.2.2 Register Dispatcher (47.7ms) pid: 17, hostname: "b9fb9f17bddb", process_id: 28, name: "dispatcher-fd60cfd986a70188ddad" SolidQueue-1.2.2 Started Dispatcher (51.5ms) pid: 17, hostname: "b9fb9f17bddb", process_id: 28, name: "dispatcher-fd60cfd986a70188ddad", polling_interval: 1, batch_size: 500, concurrency_maintenance_interval: 600 SolidQueue-1.2.2 Register Worker (50.4ms) pid: 21, hostname: "b9fb9f17bddb", process_id: 29, name: "worker-7b1d65c64db3d68e456a" SolidQueue-1.2.2 Register Scheduler (49.5ms) pid: 25, hostname: "b9fb9f17bddb", process_id: 30, name: "scheduler-0aa92e643ddec8a6b637" SolidQueue-1.2.2 Started Worker (54.6ms) pid: 21, hostname: "b9fb9f17bddb", process_id: 29, name: "worker-7b1d65c64db3d68e456a", polling_interval: 0.1, queues: "*", thread_pool_size: 1 SolidQueue-1.2.2 Started Scheduler (74.4ms) pid: 25, hostname: "b9fb9f17bddb", process_id: 30, name: "scheduler-0aa92e643ddec8a6b637", recurring_schedule: ["foo_bar_baz_qux"] SolidQueue-1.2.2 Unblock jobs (17.6ms) limit: 500, size: 0 [ActiveJob] TRANSACTION (0.6ms) BEGIN /*application='****'*/ [ActiveJob] SolidQueue::Job Create (1.9ms) INSERT INTO `solid_queue_jobs` (`active_job_id`, `arguments`, `class_name`, `concurrency_key`, `created_at`, `finished_at`, `priority`, `queue_name`, `scheduled_at`, `updated_at`) VALUES ('f061f654-51e9-425d-aa84-2100f4e559dd', '{\"job_class\":\"FooBar::BazQuxJob\",\"job_id\":\"f061f654-51e9-425d-aa84-2100f4e559dd\",\"provider_job_id\":null,\"queue_name\":\"background\",\"priority\":null,\"arguments\":[],\"executions\":0,\"exception_executions\":{},\"locale\":\"en\",\"timezone\":\"Tokyo\",\"enqueued_at\":\"2025-10-26T01:20:00.038662472Z\",\"scheduled_at\":\"2025-10-26T01:20:00.038481305Z\"}', 'FooBar::BazQuxJob', NULL, '2025-10-26 10:20:00.070323', NULL, 0, 'background', '2025-10-26 10:20:00.038481', '2025-10-26 10:20:00.070323') /*application='****'*/ [ActiveJob] TRANSACTION (0.1ms) SAVEPOINT active_record_1 /*application='****'*/ [ActiveJob] SolidQueue::Job Load (0.2ms) SELECT `solid_queue_jobs`.* FROM `solid_queue_jobs` WHERE `solid_queue_jobs`.`id` = 15 LIMIT 1 /*application='****'*/ [ActiveJob] SolidQueue::ReadyExecution Create (0.2ms) INSERT INTO `solid_queue_ready_executions` (`created_at`, `job_id`, `priority`, `queue_name`) VALUES ('2025-10-26 10:20:00.106445', 15, 0, 'background') /*application='****'*/ [ActiveJob] TRANSACTION (0.1ms) RELEASE SAVEPOINT active_record_1 /*application='****'*/ [ActiveJob] Enqueued FooBar::BazQuxJob (Job ID: f061f654-51e9-425d-aa84-2100f4e559dd) to SolidQueue(background) with arguments: SolidQueue-1.2.2 Enqueued recurring task (117.1ms) task: "foo_bar_baz_qux", active_job_id: "f061f654-51e9-425d-aa84-2100f4e559dd", at: "2025-10-26T01:20:00Z" [ActiveJob] [FooBar::BazQuxJob] [f061f654-51e9-425d-aa84-2100f4e559dd] Performing FooBar::BazQuxJob (Job ID: f061f654-51e9-425d-aa84-2100f4e559dd) from (background) enqueued at 2025-10-26T01:20:00.038662472Z with arguments: [ActiveJob] [FooBar::BazQuxJob] [f061f654-51e9-425d-aa84-2100f4e559dd] FooBar::BazQuxJob called with args: [] [ActiveJob] [FooBar::BazQuxJob] [f061f654-51e9-425d-aa84-2100f4e559dd] Performed FooBar::BazQuxJob (Job ID: f061f654-51e9-425d-aa84-2100f4e559dd) from SolidQueue(background) in 2.35ms [ActiveJob] TRANSACTION (0.2ms) BEGIN /*application='****'*/ [ActiveJob] SolidQueue::Job Create (1.3ms) INSERT INTO `solid_queue_jobs` (`active_job_id`, `arguments`, `class_name`, `concurrency_key`, `created_at`, `finished_at`, `priority`, `queue_name`, `scheduled_at`, `updated_at`) VALUES ('bb5b5ceb-ce02-48d4-9a96-3e9dc91c9bf5', '{\"job_class\":\"FooBar::BazQuxJob\",\"job_id\":\"bb5b5ceb-ce02-48d4-9a96-3e9dc91c9bf5\",\"provider_job_id\":null,\"queue_name\":\"background\",\"priority\":null,\"arguments\":[],\"executions\":0,\"exception_executions\":{},\"locale\":\"en\",\"timezone\":\"Tokyo\",\"enqueued_at\":\"2025-10-26T01:21:00.007415430Z\",\"scheduled_at\":\"2025-10-26T01:21:00.007366555Z\"}', 'FooBar::BazQuxJob', NULL, '2025-10-26 10:21:00.009421', NULL, 0, 'background', '2025-10-26 10:21:00.007366', '2025-10-26 10:21:00.009421') /*application='****'*/ [ActiveJob] TRANSACTION (0.2ms) SAVEPOINT active_record_1 /*application='****'*/ [ActiveJob] SolidQueue::Job Load (0.6ms) SELECT `solid_queue_jobs`.* FROM `solid_queue_jobs` WHERE `solid_queue_jobs`.`id` = 16 LIMIT 1 /*application='****'*/ [ActiveJob] SolidQueue::ReadyExecution Create (0.2ms) INSERT INTO `solid_queue_ready_executions` (`created_at`, `job_id`, `priority`, `queue_name`) VALUES ('2025-10-26 10:21:00.024004', 16, 0, 'background') /*application='****'*/ [ActiveJob] TRANSACTION (0.1ms) RELEASE SAVEPOINT active_record_1 /*application='****'*/ [ActiveJob] Enqueued FooBar::BazQuxJob (Job ID: bb5b5ceb-ce02-48d4-9a96-3e9dc91c9bf5) to SolidQueue(background) with arguments: SolidQueue-1.2.2 Enqueued recurring task (34.3ms) task: "foo_bar_baz_qux", active_job_id: "bb5b5ceb-ce02-48d4-9a96-3e9dc91c9bf5", at: "2025-10-26T01:21:00Z" [ActiveJob] [FooBar::BazQuxJob] [bb5b5ceb-ce02-48d4-9a96-3e9dc91c9bf5] Performing FooBar::BazQuxJob (Job ID: bb5b5ceb-ce02-48d4-9a96-3e9dc91c9bf5) from (background) enqueued at 2025-10-26T01:21:00.007415430Z with arguments: [ActiveJob] [FooBar::BazQuxJob] [bb5b5ceb-ce02-48d4-9a96-3e9dc91c9bf5] FooBar::BazQuxJob called with args: [] [ActiveJob] [FooBar::BazQuxJob] [bb5b5ceb-ce02-48d4-9a96-3e9dc91c9bf5] Performed FooBar::BazQuxJob (Job ID: bb5b5ceb-ce02-48d4-9a96-3e9dc91c9bf5) from SolidQueue(background) in 1.4ms 🧠 プロセスを確認:4つの役割と協調動作 Solid Queue は、DBベースでありながら効率的なジョブ処理を行うため、主に以下の4つのプロセスで構成されています。 これらのプロセスが、共通のデータベースを利用して協調動作することで、外部キュー(Redisなど)なしでのジョブ実行を実現しています。 supervisor : 全体の親プロセスとして、 dispatcher 、 worker 、 scheduler の各子プロセスを起動・監視します。 dispatcher : キュー内のジョブを監視し、 worker がすぐに実行できる状態(Ready)に配置する役割を担います。 worker : dispatcher によって配置された Job を実際に取得し、実行します。 scheduler : config/recurring.yml に基づき、定期実行の時刻が来たジョブを判断し、dispatcher を介してキューにエンキューします。 # ps -aux | grep solid-queue root 1 0.3 0.9 1026296 106708 ? Ssl 10:19 0:02 solid-queue-supervisor(1.2.2): supervising 17, 21, 25 root 17 0.2 1.0 974356 114088 ? Sl 10:19 0:02 solid-queue-dispatcher(1.2.2): dispatching every 1 seconds root 21 1.5 1.0 1043072 118048 ? Sl 10:19 0:12 solid-queue-worker(1.2.2): waiting for jobs in * root 25 0.0 0.9 965768 110944 ? Sl 10:19 0:00 solid-queue-scheduler(1.2.2): scheduling foo_bar_baz_qux root 58 0.0 0.0 3660 1688 pts/0 S+ 10:33 0:00 grep solid-queue Mission Control — Jobs Solid Queue は Mission Control — Jobs をダッシュボードとして利用することを推奨しています。また、Solid Queue 自体としては Dashboard UI を持っていないため、README に従って導入を進めます。 こちらも Mission Control — Jobs の README および README の翻訳記事を参考にしました。 github.com techracho.bpsinc.jp Gemfile に下記の2行を追加する propshaft は Mission Control — Jobs を導入するサービスが API-only Applications に相当するため、追加した gem " mission_control-jobs " gem " propshaft " bundle install で依存する gem をインストールし、Gemfile.lock を更新する config/application.rb に下記の2行を追加する Basic認証がデフォルトだが、別の認証方式を利用するのでOFFにする config.mission_control.jobs.adapters = [ :solid_queue ] config.mission_control.jobs.http_basic_auth_enabled = false config/environments/production.rb などの環境ごとの設定ファイルに下記の行を追加 Mission Control — Jobs のURLへのアクセスはこの Controller を経由させる 開発時は認証をスキップしたいため、 config/environments/development.rb には行を追加しなかった config.mission_control.jobs.base_controller_class = " Foo::JobsDashboardController " Foo::JobsDashboardController を実装する このクラスで認証を行い、認証に失敗した場合、401 Unauthorized のレスポンスを返却する class Foo :: JobsDashboardController < ApplicationController before_action :authenticate! private def authenticate! # Implement your authentication logic here. rescue AuthenticatorError => e head :unauthorized end end config/routes.rb を修正して Mission Control Job のエンジンにアクセスできるようにマウントする Rails .application.routes.draw do # ... mount MissionControl :: Jobs :: Engine , at : " /foo/jobs " 以上で Mission Control — Jobs が表示できるようになったかと思います。 ブラウザから config/routes.rb で指定したパスにアクセスしてみてください。 ✨ まとめ:Solid Queue導入で得られたメリット Solid Queue の導入は比較的容易に行うことができました。 ECS Scheduled Task の設定などが不要で、対象サービスのリポジトリ内のコード修正でほぼ完結するのは後から入ったメンバーの学習コスト低減に繋がり、大きなメリットだと考えます。 この記事をきっかけに Solid Queue に触れてみたという方がいたらうれしいです。 🤝 さいごに 株式会社スタメンでは、プロダクト開発に関わる全ての領域で、プロダクト職種の採用を積極的に行っています。 ご興味をお持ちいただけたなら、ぜひご応募いただけますとうれしいです。 皆さんとお話できることを楽しみにしています。 herp.careers herp.careers herp.careers
はじめに こんにちは。スタメンで Watchy というIT資産管理・操作ログ管理ツールのプロダクトエンジニアをしている yun8boo です。 スタメンでは、業務以外の場でもエンジニアの成長機会づくりを重視しており、カンファレンス参加の補助制度があります。今回はその制度を活用し、2025年10月7日–8日に開催された React Conf 2025 in Las Vegas の現地参加の機会をいただきました。 この記事では、参加の経緯からダイジェスト、印象的だったセッション、現地で得た気づきを順にまとめています。 参加の経緯 代表の大西から急遽参加の機会をいただき、即座に承諾しました。オンラインでも視聴できる時代ですが、会場の雰囲気を肌で感じ、グローバルなコミュニケーションを直接体験したいと考えていました。 初めての海外カンファレンス、しかも一人での参加ということもあり、出発前は期待と不安が入り混じっていました。 ダイジェスト 公式から「React Conf 2025 Recap」も公開されています。カンファレンス全体の熱気や雰囲気がよく伝わる内容ですので、こちらもぜひご覧になることをお勧めします。 react.dev React Conf 2025 Day 1 React Keynote: Lauren Tan氏らが登壇し、Reactエコシステムの現状と未来について語りました。 React Compiler 1.0 の正式リリースや React Foundationの設立 など、カンファレンスの目玉となる重要な発表が行われました。 View Transitions and Activity: 新しいコンポーネントである <ViewTransition> と <Activity> について深掘りされました。これらがいかにして、よりスムーズなUIアニメーションと状態管理を実現するかが解説されました。 Profiling with React Performance tracks: DevToolsに新しく搭載された「React Performance tracks」を使い、アプリケーションのパフォーマンスを詳細に分析・改善する方法が紹介されました。 Async React: 非同期処理はReactの重要なテーマです。このセッションでは、データ取得や状態更新における非同期のベストプラクティスと、今後の展望が語られました。 React and AI: ReactがAI(人工知能)とどのように連携していくか、AIが開発プロセスをどう変えるかについて、Christopher Chedeau氏らが議論しました。 Exploring React Performance: Reactのコア開発者であるJoe Savona氏が、アプリケーションのパフォーマンスを最大限に引き出すための実践的なテクニックや考え方を解説しました。 Lightning Talks & Q&A: Reactを使ったモダンなEメール開発など、多岐にわたる短いセッションが行われました。最後にはReactチームが直接コミュニティからの質問に答えるQ&Aセッションで締めくくられました。 Day 2 React Native Keynote: Hermes V1の発表や新アーキテクチャへの完全移行など、React Nativeのパフォーマンスを飛躍的に向上させる発表が行われ、コミュニティを沸かせました。 React Native, Amplified: Amazonが開発した新しいOSであるVega OSへのReact Nativeの統合と、それによって可能になった新しいクロスプラットフォーム開発体験に焦点を当てたものでした。 React Strict DOM: Webとネイティブアプリ間でのコード共有をより安全かつ容易にするための新しいアプローチ「React Strict DOM」について、Nicolas Gallagher氏が解説しました。 Reimagining Lists in React Native : React Nativeにおけるリストのパフォーマンス課題、特にブランキング現象の解消と、新しいアーキテクチャを活用したリストのための新しい基本要素の導入に焦点を当てたものでした。 React Everywhere: Bringing React Into Native Apps: 既存のネイティブアプリに、Reactを段階的に導入していく「ブラウンフィールド」開発の実践的な手法が共有されました。 フレームワーク & ツール セッション: Parcel, Vercel, Expo, React Router, RedwoodSDK, TanStack Startといった主要なフレームワークやツールの開発者が登壇しました。 What's The Framework of the React Future?: Reactの未来を担うフレームワークはどうあるべきか、業界のエキスパートたちが活発な議論を交わし、2日間のカンファレンスを締めくくりました。 なお、カンファレンス全体の詳細については、りんたろーさんのブログ記事に大変分かりやすくまとめられています。より深く理解したい方は、こちらも合わせてお読みいただくことをお勧めします。 blog.re-taro.dev 特に印象的だったセッション React Keynote: React Compiler v1 の発表: v1の発表で会場が大いに盛り上がりました。これは単なる自動最適化ツールではなく、より良いコードを書くための支援ツールでもあるとのことです。 パフォーマンスを向上させるためには useMemo や useCallback が有効ですが、どこに使うべきかの判断は簡単ではありませんでした。結果として、適切な箇所で使えていなかったり、逆に不要な場所でコードを複雑にしてしまったりすることもあったと思います。今回のv1の発表によって、プロダクションへの導入がより一層しやすくなったと感じています。 View Transitions: View Transitions APIの簡素化により、Webアプリケーションでもシームレスな画面遷移を簡単に実装できるようになったと解説されました。他のセッションでのライブコーディングも会場を盛り上げていました。 Async React: このセッションでは、 ui = f(state) というReactの従来のメンタルモデルは、現代のアプリケーションには適していないと語られました。すべてのユーザー操作を同期的に処理するのは不可能で、体験を損なう可能性があるためです。 代わりに await ui = await f(await state) という新しいメンタルモデルが提案されました。これは、ユーザーイベント、状態更新、UI更新のすべてが非同期であることを明示しています。 このセッションでもライブコーディングが行われました。ハプニングがあり時間内に話しきることができませんでしたが、会場は大いに盛り上がっていました。(2日目の最後に時間をもらい、残っていた箇所の説明が行われました) React Native Keynote: Hermes v1 (Static Hermes): JavaScriptコードを事前にネイティブコードへコンパイルすることで、アプリのパフォーマンスが劇的に向上し、アプリサイズも小さくなるとのことでした。 DevToolsの進化: React Native DevToolsに待望のパフォーマンスパネルとネットワークパネルが追加されることが発表されました。これにより、Web開発者にはおなじみの強力なデバッグ体験が、ネイティブ開発にももたらされます。 私が学生だった8年ほど前、React Nativeでアプリ開発を経験したのですが、当時はデバッグにとても苦労した記憶があります。それ以来、しばらくReact Nativeから離れており最新の情報を追えていませんでした。しかし、今回のKeynoteで発表された進化を目の当たりにして、ぜひもう一度触ってみたいと強く感じました。 現地で得た気づき コミュニケーションの壁と突破口 セッションの合間や食事の時間での会話はリアルタイム翻訳が難しく、最終的にスマートフォンの画面を見せ合いながらの会話となり、少しテンポが悪くなってしまいました。特に会場が盛り上がっている場面や複数人での会話では、リアルタイム翻訳の精度と速度が追いつかないのが現実でした。 そのため、技術的に深い話までは踏み込めませんでしたが、「日本から来た」こと自体が話題になり、会話のきっかけとして機能しました。SNSで数名とつながり、後日フォローアップできる土台を確保できたのは大きな収穫でした。(そして、日本のアニメは偉大でした!) 交流会 現地参加ならではの体験 ライブコーディングや目玉発表が成功した瞬間に会場が一体となって沸き立つ体験は、オンラインでは得難いものであり、学びへのモチベーションを一段と引き上げてくれました。どの話題で会場が沸くか、どこが強調されているのかを肌で感じられるのは大きなメリットです。また、スポンサーブースには普段使っているライブラリの開発者たちがおり、ユースケースなどを直接聞ける貴重な機会でした。 海外カンファレンス参加へのハードルが下がった 出国前は英語に自信がなく不安でしたが、実際に行ってみると「意外となんとかなる」と感じました。この経験を通じて自信がつき、海外カンファレンスへの心理的なハードルが大きく下がりました。 セッションのスライドは分かりやすくシンプルな英語で書かれており、登壇者もプレゼンテーションに慣れている方が多いため、スライドに沿って進むセッションは内容を理解しやすかったです。 終わりに 今回の一番の収穫は、「モチベーションの向上」でした。会場の一体感と熱量に触れ、日々の学びに向き合う姿勢がぐっと前向きになりました。英語は完璧でなくても会話は成立することを実感した一方で、「次はもう一段レベルを上げた状態で臨みたい」という気持ちが強くなり、英語での発信や対話への意欲が大きく高まりました。 最後に、今回の参加は社内のカンファレンス参加補助制度があってこそ実現できたものです。この貴重な機会に感謝しつつ、高まったモチベーションを今後の実装という形でチームに還元していきます。 Watchyではエンジニアを絶賛募集中です 🙌 herp.careers
導入 お久しぶりです!株式会社スタメンの ちぇる です。前回の 「ながらRuby会議01」 に続き、今回は「Kaigi on Rails 2025」に参加してきました! Kaigi on Rails とは、「初学者から上級者までが楽しめるWeb系の技術カンファレンス」です!年に一度開催され、国内外から多くの参加者やスピーカーが集まり、Railsに関するさまざまなテーマでの講演や交流が行われます。 kaigionrails.org 弊社スタメンの 福利厚生 には「まるっとカンファレンス補助」という制度が存在し、今回はこちらを利用して、名古屋から東京へ1泊2日でカンファレンスに参加させていただきました🙌 東京駅に到着!ここから徒歩圏内の会場で行われました🙆‍♂️ 弊社スタメン社員の参加者です👨 印象に残ったセッション 2日間にわたって開催された Kaigi on Rails は、新たな発見のある講演ばかりでした。その中でも、本記事では特に印象に残ったセッションに関してご紹介させていただきます🖊️ 入門 FormObject speakerdeck.com Ruby on Railsで実装していると、Rails Wayから外れることってありませんか?本セッションでは、その代表例である FormObject の使い所に関するお話を聞くことができました。僕自身も、以前に業務で FormObject を使っていたこともあり、非常に勉強になる点が多かったです。 そもそも、 FormObject の使用基準は何でしょうか?パッと考えてみると、以下のような観点が挙げられます。 バリデーションに if がついたら? メールフォームを実装するとき? accepts_nested_attributes_for の代わり? 本セッションでは、普段なんとなく使っていた FormObject の使い所を明確に言語化・整理されており、とても納得感がありました。 FormObject の特徴 ① DBに紐づかないオブジェクト ② モデルと同じインターフェースでコントローラから呼ばれる ③ 独自のライフサイクル処理をもてる ④ ビューの状態を保持できる (例) class UserNewForm # ① include ActiveModel :: Model # ② attr_accessor :email , :name validates :email , :name , presence : true # ③ before_action :email_to_lower_case # ③ def save # ... end private def email_to_lower_case # ... end end class UsersController < ApplicationController def create @form = UserNewForm .new(create_params) # ② if @form .save redirect_to new_users_path, notice : ' ユーザを作成しました ' else render :new # ④ end end end FormObject の使い所 「なぜ、 FormObject が必要になるのか?」という問いに対する結論は、以下であると述べられていました。 Rails Wayの制約があると、ユーザの目的が実現できないから Rails Wayのフォーム処理は、次のような前提のもとに成り立っています。 一度に操作するモデルの数が一つ モデルのライフサイクル処理のパターンが一つ つまり、 これら前提が壊れる時 が FormObject の使い所なのです!例えば、以下のようなケースが挙げられます。 一度に操作するモデルの数が一つではない モデルを操作しない(メールフォーム等) 複数のモデルを操作する(会社情報と個人情報の同時更新等) モデルのライフサイクル処理をアクションごとに分ける(ユーザの作成と編集でバリデーションを分ける等) 特に、現代のユーザは複雑な操作をワンタップで実現したいという要望も強く、Rails Wayから外れることがしばしばあります。しかし、こういった「レールの伸ばし方」は先人たちが用意してくれているため、巨人の肩に乗る姿勢を持つことが大切であるとお話しされていました。 今後は、 FormObject を「なんとなく使う」のではなく、「使うべき所で使う」ように注意していきたいと思いました! 2分台で1500examples完走!爆速CIを支える環境構築術 speakerdeck.com CIが高速であるということは、 ビジネスに直結 します。なぜなら、不具合の原因特定と修正のサイクルを早く回すことができ、安定したリリースを早期にすることができるからです。 本セッションでは、開発において欠かせないCIをどのように速くしたか、その体験談をお聞きすることができました。 RSpecの実行を早くするには 全体のCI処理が遅くなってしまう原因として、「直列実行でCPUを使いきれていない」ことが挙げられます。そのため、まずは並列処理にすることが、CIの実行時間を短縮する選択肢の一つと考えられます。 そこで、TwoGateさんでは parallel_tests を導入するに至ります。この gem は、マルチプロセスによる並列実行をサポートしています。 また、spec単位での実行時間にはバラツキがあります。つまり、均等に並列実行しないと求めている効果が得られません。そこで、spec単位の実行時間の履歴を記録して最適な分割をしてくれる knapsack_pro-ruby の gem も導入されていました。 これらの対応後、 8コアCPU で 8並列 で Rspec を実行したそうです。結果、本来であればCI実行時間の 1/8 程度の短縮を期待できそうですが、実際は 29分38秒 → 23分20秒 の短縮にとどまり、思うような結果が得られませんでした。なぜなら、8並列にしたことで、DB書き込みによるディスクI/Oに負荷がかかってしまったためです...。 次の改善として、ストレージに tmpfs を利用しました。 tmpfs はメモリ上に構築されるファイルシステムで、再起動時にデータが消失する点が特徴です。そして、この変更によってディスクI/Oの負荷が軽減され、CIの実行時間は 29分38秒 から 5分 へと短縮され、なんと 約6倍 のスピードアップを実現することができたそうです! さらになんと、サーバーをクラウドマシンから物理マシンにしてスペックを上げたことで、 1分59秒 までCI実行時間の削減を実現されていました。物理マシンの導入は、多くの企業で容易にできる選択ではないと思いますが、コスト観点からもTwoGateさんはメリットを享受できたそうです。すごいですね。 弊社でも、CI実行時間には一定の課題感を持っており、本セッションの内容を応用できないか、是非とも参考にさせていただきたいと感じました! 「技術負債にならない・間違えない」権限管理の設計と実装 speakerdeck.com 権限管理は、システムにおいて非常に重要な機能を果たします。これらが間違って設定されると、「給与を第三者に公開してしまった」「取引先を公開してしまった」といった事態になりかねません。それにより、サービスへの信頼が大きく下がり、事業への損失が大きくなります。つまり、 権限管理のミスは許されない のです。 そんな権限管理において、本セッションでは適切な実装方法をレクチャーしていただきました。まずは、よくある間違った実装を見てみます。 class ProjectsController < ApplicationController def create unless current_user.admin? # 役割に依存 redirect_to root_path, alert : ' 権限が必要です。 ' return end end end admin? の判定はアンチパターン です。なぜなら、役割は変わりゆくからです。また、 admin? と記載した場合、admin がどのような権限を持っているか、その先のコードを見にいかなければなりません。 役割に依存した実装は、権限が暗黙的になるので、技術的負債になりがちです。さらに、そもそもの役割の権限が変化した際、多くの判定箇所を見に行って修正する必要があります。 レビュワーは、常に役割の権限を知らないといけない。 判定箇所が散らばり、どのような権限が定義されているのかわからない。 そこで、 役割ではなく権限に依存する 必要があります。 class ProjectsController < ApplicationController def create unless current_user.can_create_project? # 権限に依存 redirect_to root_path, alert : ' 権限が必要です。 ' return end end end class User < ApplicationRecord # ... enum :role , %i(admin manager normal external) def can_create_project? admin? || manager? end end これだけでも、だいぶ見やすくなります。 さらに、権限の要素を 「対象」 ・ 「操作」 ・ 「役割」 ・ 「条件」 に分割して考えることで、権限管理をより綿密に行うことができるとお話しされていました。 【権限操作の呼び出し】 # レコードに対して権限があるかの判定 project = Project .find( 1 ) readable_project = Policy .authorize(current_user, project, :read ) # 権限があるレコードの絞り込み projects = Project .all readable_projects = Policy .authorize_scope(current_user, projects, :read ) # 権限一覧の取得(主にクライアント側で利用) permissions = Policy .permissions(current_user) #=> JSON # { # "projects": { # "read": true, # "create": false, # "update": false, # "delete": false # } # } 【権限操作のインターフェース】 module Policy def self . authorize (user, record, action) context = Context .new( user :) context.authorize(record, action) end def self . authorize_scope (user, scope, action) context = Context .new( user :) context.authorize_scope(scope, action) end def self . permissions (user) context = Context .new( user :) context.permissions end end 【上記の内部実装】 module Policy class Context def authorize (record, action) policy = policy_class(user, record.class).new( user :, record :, mode : :record ) policy.record end def authorize_scope (scope, action) policy = policy_class(user, scope.klass).new( user :, record :, mode : :scope ) policy.public_send(action.to_sym) end def permissions # ... end private def policy_class (user, record_class) role = user.role.camelize # メタプロを活用し、具体的な権限クラスを動的に生成 " Policy:: #{ record_class } ::Roles:: #{ role }" .safe_constantize end end end 【権限の基底クラス】 module Policy class Base attr_reader :user , :record , :scope , :mode def initialize ( user :, record : nil , scope : nil , mode : nil ) @user = user @record = record @scope = scope @mode = mode end end end 【具体的な権限クラス】 module Policy module Project # 対象 module Role class Manager < Base # 役割 def update # 操作 case mode when :record assignee? || author? # 条件 when :scope # ... when :list # ... end end end end end end このような設計にすることで、権限の有無が一目で分かるようになりました。 その結果、プレックスさんでは CS や PdM がソースコードの一次情報を見て理解できるように なり、開発側への問い合わせが減少。エンジニアは他の開発業務に専念できるようになりました。 さらに、Railsサーバー側からフロントに対して権限のマッピング情報を提供するようになり、フロント側の実装も自然と 役割ではなく権限に依存する ようになったそうです。 複雑な権限管理を正しく設計したことで、多くの嬉しい副次的効果が得られたんですね! まとめ Kaigi on Rails、本当に面白かったです。Railsエンジニアとして、他の活躍されているエンジニアの方々の知見を直接共有していただける機会は、非常に貴重だと感じました。 以下、カンファレンスで特に印象に残った言葉です。 ソフトウェアの実装における最適解は、常に 場合によります Rails Wayがあるとはいえ、環境や目標は企業ごとに異なります。そのため、最適解も状況によって変わります。今回のようなカンファレンスに参加し、自社の取り組みだけでなく、他社の事例も取り入れることで、視野を広げ、より柔軟に考えるきっかけになったと感じました。 今後も実装で迷うことは多々あると思いますが、どの選択が最善か、常に考え続けていきたいと思います! 最後に 株式会社スタメンでは、一緒に働く仲間を募集しています!詳しくはこちらをご覧ください🙌 herp.careers
プロダクト開発部でTUNAGの開発をしている hisa です。最近公開された「ひゃくえむ。」がとても良く、久々劇場で涙を流しました。 今回は少し前になりますが、デザイナーと協業でリリースした社内ツール開発について紹介できればと思います。 はじまりは、何気ない雑談から 「バナー、毎回同じような構成で作ってるんだけど、テンプレートにできたら楽かもね」 「Zoomの背景も、各自Googleスライドで作ってるから、ばらつき出ちゃってて…」 そんな、オフィスでの雑談からこのプロジェクトは始まりました。 デザイナーとエンジニア、それぞれの 気になっていたこと を持ち寄って、「じゃあ、作ってみようか」と動き出しました。 もともとの課題 今回取り組んだ背景には、2つの あるある課題 がありました。 ① バナー作成の手間と属人化 イベントや発表のたびに、バナーを毎回新しく作る必要がある 一定の構成パターンはあるものの、毎回手作業で時間がかかる デザイナーに依頼が集中しがちで、ちょっとした更新にも時間がかかる ② Zoom背景のバラバラ感 社内ではメンバーが各自で背景を作成(Googleスライドなど) 統一感がなく、資料やスクショで見たときに違和感が出る 「ちゃんとデザインされた感」が伝わりづらくなることも デザイナー × エンジニアでつくったもの 作ったのは、 社内向けの画像ジェネレーター です。 イベント用バナーやZoom背景などを、誰でも簡単に、統一感のあるデザインで生成できるツールです。 テンプレートに沿って、イベント名・日付・名前などを入力するだけ バナー画像やZoom背景が即座に生成される デザイナーが設計したテンプレートをベースに、エンジニアが画像生成やUI部分を構築 非エンジニアでも使えるUI、かつ実用レベルでの品質を意識 技術的な挑戦とトライ 今回の取り組みでは、いくつか新しい挑戦も行いました。 画像生成ライブラリの選定と検証 弊社主力サービス TUNAG では使っていないライブラリを選定し、初めて導入してみました。 特に文字の縦位置揃えや、Canvas描画の比率など、調整には細かな工夫が必要でした。 技術的に試したかったUI設計 普段使っている技術とは異なるUI構成で、柔軟なデザイン反映にチャレンジ。 デザイナーが意図したトンマナを壊さず、かつ汎用化できる設計に落とし込んでいます。 非エンジニアの開発参加(AIエージェント活用) Cursorなどのツールを用いて、デザイナー自身がスタイル調整や軽微な修正を担当。 「ちょっと試してみる」が現実的にできる環境があることで、参加のハードルが下がりました。 やってみてよかったこと リリース後、社内からはこんな声がありました。 「統一感が出て、資料に載せやすい」 「サクッと作れるのがありがたい」 「非デザイナーでもレイアウトが整うのは助かる」 デザイナーに依頼しなくても誰でもちょっとした画像が作れる社内ツールを開発しました! ①イベント告知バナー②名前入りZoom背景③記事のサムネイル を出力できます。 エンジニアと一緒に、自己研鑽としてCursorなどを使いながら作りました。良い学びになったし、社内のみんなにも喜んでもらえて pic.twitter.com/FbxvZ0Qf9Q — スタメン デザイナーたち✉️ (@stmn_designer) 2025年7月2日 小さな機能でも、 ちょっと便利 / ちょっと嬉しい を確実に増やすことができました。 プロダクト本体とは別文脈で技術を試し、協業の型をアップデートできたのも良い循環です。 これからやっていきたいこと 開発のハードルをさらに下げ、 誰でもツールを作れる組織 へ デザイン・エンジニアの境界をゆるやかにし、 共創を当たり前 に 雑談ベースの課題感から始まる 小さなプロジェクトを、もっと増やしていきたい おわりに 今回の社内ツールは、「ちゃんと課題を解決したい」ではなく、「これ、あったらちょっといいよね」から始まりました。 その ちょっといい を一緒に形にできたのは、職種を超えたコラボレーションのおかげです。 小さな一歩の積み重ねが、働き方や関係性そのものを変えていく気がしています。 これからも、いろんな あったらいいな を拾って、形にしていきます! このブログの サムネイル画像も、今回開発したジェネレーターで作成 しました。 弊社ではプロダクト開発に関わる全ての領域で、プロダクトを共に良くしていけるメンバーを募集しています。 私たちと一緒に最高のチームとプロダクトを作りましょう! herp.careers
こんにちは!プロダクト開発部の おしん ( @38Punkd ) です。 先日9月19日(金)から21日(日)にかけて、有明セントラルタワーホール&カンファレンスで開催された iOSDC Japan 2025 に参加しました! iosdc.jp 2016年初開催されたiOSDCは今年で遂に10回目を迎え、今年もiOSDCらしい、賑やかで学びの多い最高の3日間でした。 今年も弊社はシルバースポンサーとしてiOSDC Japan 2025に協賛しました🙌 今年はいつもとちょっと違う形のペンライト 弊社社員のとんとんぼさん(@Ktombow1110)。楽しんでました 白熱するSwiftコードバトル お題を満たすプログラムをSwiftで短く書けた方が勝ち、という1 vs 1のガチバトル。 予選を勝ち抜いた6名の選手が決勝に進出し、カンファレンス当日に優勝を目指して戦う様子を観戦しました。 コードをできるだけ短くするために普段の業務では目にしない手法が使われていて、『Swift でこんな書き方もあるのか!』と刺激を受けました。 決勝のお題 特に印象的だったセッション カスタムUIを作る覚悟 スピーカー:まつじ( @mtj_j )さん iOS 27から対応が必須とアナウンスされているLiquid Glassに向き合うため、カスタマイズされた既存のモバイルアプリのナビバーやタブバーに対して、今一度向き合う必要がありました。 まつじさんのセッションを聴いて『なぜカスタムUIは大変なのか』が自分の中で漠然とした経験則に基づく直感が、明確に言語化されました。 デザイナーとエンジニアが、Figma等の静的な画面デザインを元に協業をする場合、アニメーションやインタラクション、パフォーマンス、アクセシビリティ、ローカライゼーションといった、静的なデザインでは表現しにくい部分を決めにいく必要があり、Appleの標準コンポーネントではこれらが既に実装されています。 『標準搭載された機能を捨てて一から作り直し、かつそれをメンテナンスする覚悟はあるか』 カスタムUIを作る・作らないの議論を進める上では、標準コンポーネントに備わっている機能をきちんと理解しておくことが重要だと改めて思いました。 fortee.jp アセンブリで学ぶCPUアーキテクチャ スピーカー:akkey( @AkkeyLab )さん 過去にアセンブリ言語について学ぼうと思い本を買ったのですが、難しすぎて挫折した経験があるので、「今回こそは」と思い akkey さんのセッションを聴きました。 これまで自作メソッド以外でクラッシュが起きると思考停止していましたが、セッションでスタックトレースを辿る方法を学んだことで、難解な暗号に見えていたアセンブリ言語が、デバッグの強力な武器に変わりうると感じました。 fortee.jp 感想 私自身、iOSDCへの参加は今年で3回目を迎えました。回を重ねる毎に横の繋がりが増えている事を実感しています。 私は名古屋に住んでいるので、社外の方とは基本的にはX(Twitter)やDiscord等のオンラインのやり取りがメインです。 そういった方々とオフラインでは年に数回しか会えないので、会えるとやっぱり嬉しいですね。 iOS開発続けてて良かったなあと思います。 また、数名の方から、「名古屋で何度かモバイルの勉強会をやられてますよね」と声をかけていただけ、「Nagoya.swift、開催して良かったなあ」「mobile.stmn、継続してて良かったなあ」としみじみ思いました。 Nogoya.swift は名古屋で開催したiOSエンジニア向けのイベントですが、一緒に運営してくださったかっくんさん( @fromkk )が、Nagoya.swiftを含む (region).swift の各地域の開催者へのインタビュー記事を書いてくださり、その記事が今年のiOSDCのパンフレットに載っています。 読んでいただけると嬉しいです。 iOSDC Japan 2025のパンフレット記事を書いた方法 #iosdc https://t.co/w6q3d1Zx8y pic.twitter.com/tnSQ2ARTEG — かっくん (@fromkk) 2025年9月9日 スポンサーとして参加する以上、スポンサーとしての責務を全うする事は勿論ですが、私にとってiOSDC は、日々の業務へのエネルギーチャージをする場だと感じています。 また皆と笑顔で近況を話せるよう、普段の開発を頑張ろうと思いました。 圧巻のクロージング 最後に 弊社ではプロダクト開発に関わる全ての領域で、プロダクトを共に良くしていけるメンバーを募集しています。 私たちと一緒に最高のチームとプロダクトを作りましょう! herp.careers
はじめまして!株式会社スタメンでバックエンドエンジニアをしている ちぇる と申します。 この度、2025/9/6(土)に開催された「ながらRuby会議01」に参加しました。 会場は岐阜県の「うかいミュージアム」で、自然に囲まれた素敵な場所でした✨ 近くには金華山(きんかざん)があり、山頂に築かれた岐阜城が見えます🏯 いざ、会場へ! まず驚いたのは、 今回開催された「ながらRuby会議01」でしたが、なんと応募はほぼ満員...! 東海地方にも、これだけ多くのRubyistが集まり、熱量を持って交流できる場があることに感激しました。 ちなみに、nagara.rbは、今回の「ながらRuby会議01」で記念すべき第100回目の開催だそうです。Rubyコミュニティを作ってくれた先人たちに感謝です。 アフターパーティーでは、皆さんと鵜飼に参加しました✨ 以下、実施されたプログラム内容となります! ふむふむ。ジュニアエンジニアの自分にとって、目の前に Ruby のアタラシイ世界が広がっているぞ...🤔 regional.rubykaigi.org この記事では、特に気になったセッションについてご紹介させていただきます。 refinementsのメソッド定義を4000倍速くした話 スピーカー:alpaca さん( @alpaca_tc ) speakerdeck.com 社内で Ruby をアップデートしたところ、どうやらアプリケーションの起動やファイル変更時における反映が遅くなってしまったようです。 そのため、 vernier という gem を使って実際に速度を計測します。 すると、Ruby3.2 と比べて Ruby3.3以降では、 Module#refine を呼び出すタイミングで、なんと実行時間に4000倍以上もの差分が見られたそうです👀( 計測 ) Module#refine とは、簡単にいうと「モンキーパッチを安全に使うための仕組み」です。 例えば、以下のように書くと、String 全体に shout メソッドが定義されてしまいます。 class String def shout self .upcase + " ! " end end " hello " .shout # => "HELLO!" しかし、refine を使うことで、モジュール内での定義にスコープを限定することが可能となります。 module StringRefinement refine String do def shout self .upcase + " ! " end end end using StringRefinement # こちらを実行した場合のみ有効に! " hello " .shout # => "HELLO!" では、この refine がなぜ遅くなってしまったのでしょうか...? ここから原因調査に入ります。 調査を続けていくうちに、rubyリポジトリ内で rb_clear_all_refinement_method_cache(); の処理が追加されたことが判明します。 これが怪しいのでは...?👀 そこで、実際に Revert パッチを当てて確認すると、なんと速度が改善されました🎊 ( 再現 ) つまり、Ruby3.3以降では、refineに関連する callcache の無効化処理が追加され、それによってパフォーマンスリグレッションが起こってしまったのですね。 callcacheとは「同じメソッドを再度呼び出すときに、以前の呼び出し結果を再利用する仕組み」のことです。 PR にあるように、refineに関するキャッシュによるバグが発生してしまったために、このキャッシュを使用しない変更が反映されたんですね。 Rubyでは、メソッドを実行する際、継承チェーンに沿って順にメソッド定義を探します。 しかし、今回の alpacaさんのケースでは、 ObjectSpace になんと、70億のオブジェクトが存在したそうです...😳 そのため、refine 関連のメソッド探索がキャッシュされない場合、相当のオーバーヘッドが発生することは想像できますね...。 そこで alpacaさんは、refine callcache 専用の格納先を用意する方針で実装することにしたそうです。 しかしご存知の通り、Ruby は C言語で書かれています。そのため、上記の方針をC言語で書いて実現する必要があります。 Rubyistにとって、なかなか大変な作業だと考えられます...。 alpacaさんは案の定、C言語特有のGC(ガベージコレクション)によるメモリ管理や、セグメンテーションフォールト(不正なメモリアクセスを行ったときに OS が強制終了させるエラー)等に苦戦します。 しかし、それらにもめげず、ChatGPT や rubyhackchallenge を駆使して、ついにマージまで実現したそうです!🎉( 改善 ) すごいですよね。 該当PR: Optimize callcache invalidation for refinements ここから、alpacaさんは以下の教訓を話されてました。 パフォーマンス改善の流れは同じ(計測 → 再現 → 改善) AIやコミュニティの助けを得て貢献できる また、alpacaさんは以前に RubyKaigi にも参加されており、GC やセグフォについても学んでいました。こうした知識が、実際の現場で活かせることを実感されたそうです。 TUNAGのモノリスとアーキテクチャ また、スポンサーセッションとして、弊社の近藤( @sei_kondo97 )も登壇しました! 弊社は、現在 TUNAG というエンゲージメントプラットフォームを提供させていただいております。TUNAGはモノリス構造のシステムですが、年々サービス内容が肥大化しており、今後はサブサービスやサブアプリケーションに分割し、各ドメインごとに分けて管理していく方針となります。 最近では、「TUNAG本体」から「TUNAGチャット」が分離されました。また、「TUNAG本体」を認可サービスとして利用することで、「TUNAGチャット」へのSSOログインも可能となりました。 最後に 僕は今回、初めて地域Ruby会議に参加しました。そこで感じたのは、 自分が使っている道具の構造を知ることの重要性 です。 現実として、アプリケーション側のエンジニアであれば、道具の使い方さえ分かっていれば、内部構造を知らなくても開発は可能です。高水準言語の強みはまさにここにあります。たとえば、多くの開発者は機械語やアセンブリの仕組みを知らなくてもコードを書けますし、私自身も知りません。 コミッターの方々がRubyの内部をブラックボックス化してくれているからこそ、私たちは便利に開発できているわけです。しかしその一方で、構造を理解していないことで見落としたり、最適な使い方ができない場合もあります。 Rubyの内部やメソッドの実装を知っていれば、パフォーマンスチューニングやデバッグでより効率的で安全な判断が可能になります。つまり、「道具の使い方」と「構造の理解」は表裏一体で、両方あって初めて深い応用力が得られるのではないかと感じました。 今回の「ながらRuby会議01」における学びは、普段はブラックボックスとして使っている部分にも興味を持ち、仕組みを知ることで、開発者としての視野や判断力が広がるということです。これからは、自分のコードだけでなく、その下で動く仕組みや背景にも目を向けていきたいと思います。 株式会社スタメンでは、一緒に働く仲間を募集しています!詳しくはこちらをご覧ください🙌 herp.careers
👋 あいさつ こんにちは!株式会社スタメンのフロントエンドエンジニア、伊賀本です。 これまでフロントエンドを中心にキャリアを積んできましたが、スタメンに入社してからは バックエンド領域にも挑戦 を始めています。 今回は、2025年6月に入社して3ヶ月の節目ということで振り返りをしてみようと思います。気軽に読んでいただけると嬉しいです! 🎯 入社理由 転職を考えていたとき、私は「新しい領域に挑戦できる環境」と「仲間と成果を称賛し合える文化」を探していました。 スタメンを知ったきっかけは、元同僚が勤めていたことでした。その同僚から聞く会社の雰囲気やカルチャーが、まさに私が求めていたものと一致していました。 スタメンに惹かれたのは、行動指針「StarWay」にある Work Bravely (大胆に攻め、挑戦や失敗を讃える)という言葉です。 さらに、面接で感じたフラットな雰囲気と「挑戦を称賛する文化」が、自分の理想と重なりました。 行動指針「StarWay」にある Work Bravely(大胆に攻め、挑戦や失敗を讃える) 🛠️ 入社後のリアル体験 技術面の挑戦 入社直後から、既存機能の改善を任されるなどフロントエンドの強みを活かす一方で、サーバーサイドの開発にも挑戦しています。スタメンでは、OSSのgood first issueのようなオンボーディングタスクが用意されており、新しい技術領域に挑戦する最初のステップとして最適な環境が整っています。慣れないことに苦戦もしましたが、レビューや議論を通して学びを得られる環境がありました。 good first issueのようなオンボーディングタスク オンボーディング(ウェルカムミッション) 組織が大きくなるほど、顔と名前は知っているけれど話したことがない人が増えがちです。スタメンではその解消のため、入社から1ヶ月は所属部署や他部署へあいさつに伺い、簡単な自己紹介と写真撮影を行うオンボーディングがあります。この取り組みによりファーストコンタクトのハードルが下がり、のちに業務で関わる際も自然に声を掛け合える関係が築けました。 ウェルカムミッションで実際撮影した写真 働き方とコミュニケーション 前職はフルリモート中心の働き方でしたが、現在は週3出社・週2リモートのハイブリッドです。出社日には雑談やその場での意思決定が増え、レビューや相談も早く回るようになりました。結果としてコミュニケーション量が増え、開発はより円滑になっています。 その成果を「スタカネ(成果を称賛する文化)」で認めてもらえたとき、 挑戦を称賛してくれる会社だと実感 しました。 スタカネ(成果を称賛する文化)の投稿内容 🤝 チームの雰囲気 メインプロダクトである「TUNAG」では日常的にサンクスカードが飛び交い、小さな努力や挑戦も称賛されます。 年齢や役職に関係なく意見を交わせるため、安心して「やったことがないこと」に手を挙げられるのも魅力です。 若手でも裁量を任される環境だからこそ、自然と「一歩踏み出してみよう」と思える雰囲気があります。 実際にいただいたサンクスカード 🚀 今後の挑戦 今後はフロントエンドとバックエンドの両方を経験し、 フルスタックに活躍できるエンジニア を目指していきたいです。 さらに、得た知見をチームに還元し、スタメンのプロダクトをよりスケーラブルに進化させる挑戦に貢献していきたいと考えています。 📣 未来の仲間へのメッセージ スタメンは「挑戦を称賛する」文化が根づいている会社です。 バックエンドに挑戦したいフロントエンドエンジニア フルスタックを目指して成長したい人 仲間と成果を分かち合いながら、大胆にチャレンジしたい人 そんな方にとって、スタメンは最高の環境になるはずです。 herp.careers 私と一緒に、挑戦を楽しみながら「スターになる成長」を経験しませんか?
株式会社スタメンでAndroidアプリ開発を担当している鈴木と申します。 この記事では、2025/09/10(水) 〜 2025/09/12(金)に行われたDroidKaigi2025(以下DroidKaigi)に参加して、特に印象に残ったセッションや、カンファレンス全体を通して感じた技術トレンドなどを共有します。 株式会社スタメンはサポーターズスポンサーとしてDroidKaigi2025に協賛しました。 カンファレンスの全体像と雰囲気 今回のDroidKaigiは、代表のmhidakaさんによると過去最高の参加者数とのことでかなりの盛り上がりを見せていました。 Day0のワークショップでは、Compose MultiplatformとKotlin MultiPlatformを使用したアプリケーション開発の方法を学びました。 去年のalpha版だったFleetからAndroid Studioを使用するようになったためか、結構安定感のある実行環境になっているように感じました。 メインコンテンツであるセッションについては、公正取引委員会の方によるスマホ新法に関するものや、Googleの方からのPlayStoreに関するセッションなど例年とは少し毛色の違った登壇もあり、より強い新鮮さを味わうことができました。 アフターパーティーで行われたマグロの解体ショー 特に印象的に残ったセッション 基礎から学ぶ大画面対応 〜「Large screen differentiated」認定アプリの開発知見〜 スピーカー: tomoya0x00さん Android16から画面向きを固定化することや、アスペクト比の指定が無視されるようになったため、自身の開発にも影響のあるトピックでした。 まとめると以下の点が語られていました。 Android16からの仕様変更について 3段階のサポートレベル 実際の対応例 対応が必要ということは把握していたのですが、サポートレベルの段階が存在していることまでは調べられていなかったのでとてもためになるセッションでした。 自身のプロジェクトでは第1段階(Large screen differentiated)ではありますが、対応の方向性は合っていたことが確認できたのでホッとした面もありました。 第3段階のLarge screen readyの状態にするためにはNavigation3への置き換えが不可欠なため、大変そうだなと思いつつ指標があることを知ることができたので少しずつでもUXを向上していきたい気持ちが高まりました。 2025.droidkaigi.jp Performance for Conversion! 分散トレーシングでボトルネックを特定せよ スピーカー: andousanさん このセッションでは、分散トレーシングを用いてアプリケーションのパフォーマンスを測定する方法ついて、以下の様な点が語られました。 パフォーマンス管理の重要性 既存のツールによる限界 分散トレーシングの有効性 モバイルアプリケーションへの導入方法 私が普段開発しているプロジェクトでもWebViewをネイティブ実装に変更する利点として、パフォーマンスが向上するデータを提示するような場面があったのですが、普段からウォッチできる環境を構築できていればそのようやり取りもよりスムーズに行えていたことと思います。 まずは自身が関わっているプロジェクトから導入を進める動きをしていきたいと思います。 2025.droidkaigi.jp 最後に 今年のDroidkaigiは例年通りの雰囲気もありつつ、コーヒーの注文システムが新たに構築されていたり等新しい要素もありつつとても楽しいお祭りでした。 スタメンではエンジニアを絶賛募集中です🙌 👇👇👇気になる方はこちらからアクセスをお願いします👇👇👇 herp.careers
はじめに 株式会社スタメンでモバイルアプリの開発をしているカーキ(X: @khaki_ngy )です。 これまでJetpack Composeを採用した画面で、大量のアイテムをページング表示したい場合は、多くのケースで AndroidX Paging ライブラリ(以下、Paging3)を採用してきました。 直近で担当したタイムライン機能のリプレイスに際しても、投稿一覧を表示するために当初 Paging3 を採用しましたが、開発を進める中で、Paging3 が持つ特性と、我々の要件との間にギャップがあり、Paging3 の利用を見送る必要がある状況になりました。 今回のブログでは、Paging3 の基本的な使い方を振り返りつつ、我々が直面した課題と、それをどのように乗り越えたかについて紹介をします。 Paging3×Composeの基本的な使い方 Paging3 とは、大規模なデータセットを効率的に、少量ずつページ単位で読み込み、表示するためのGoogle公式のライブラリです。ユーザーのスクロール操作に応じて次のページを自動で読み込むことで、メモリ使用量を抑え、スムーズなユーザー体験を提供します。 ここでは、Paging3 を利用する上で必須となる登場人物と、その基本的な接続方法に絞って、シンプルなコード例を紹介します。 1. データソースを定義する ( PagingSource ) まず、データをどのように取得するかを定義する PagingSource を実装します。この例では、API通信などを模した上で、20個の文字列アイテムを擬似的に生成しています。 // PagingSourceの実装。キーはページ番号(Int)、値は取得するデータ(String) class MyPagingSource : PagingSource< Int , String >() { // ページングされたデータを読み込むための中心的な関数 override suspend fun load(params: LoadParams< Int >): LoadResult< Int , String > { val page = params.key ?: 1 // 読み込むページ番号(初回は1) // 実際にはここでAPIリクエストなどを行う // この例では、ページ番号に基づいて擬似的なデータを生成 val data = List ( 20 ) { index -> "アイテム ${ index + (page - 1 ) * 20 } " } // 読み込み結果をLoadResult.Pageとして返す return LoadResult.Page( data = data, prevKey = if (page == 1 ) null else page - 1 , // 前のページ nextKey = if (page < 5 ) page + 1 else null // 次のページ(5ページで終わり) ) } // リストが更新されたときに、新しいPagingSourceをどこから読み込み始めるかを定義する // この例では簡単のため実装を省略 override fun getRefreshKey(state: PagingState< Int , String >): Int ? { return null } } 2. ViewModelで Pager をセットアップする 次に、 ViewModel で PagingSource を利用して Pager を構築し、UIに公開するための Flow<PagingData<String>> を作成します。 class MyViewModel : ViewModel() { val items: Flow<PagingData< String >> = Pager( // PagingConfigで、ページサイズなどの設定を行う config = PagingConfig(pageSize = 20 ), // 新しいPagingSourceのインスタンスを生成するファクトリを渡す pagingSourceFactory = { MyPagingSource() } ).flow .cachedIn(viewModelScope) // Flowをキャッシュし、画面回転などの構成変更後もデータを保持 } 3. Composableでデータを表示する 最後に、Composable関数で ViewModel から Flow を受け取り、 LazyColumn で表示します。 paging-compose ライブラリが提供する専用の items 拡張関数を利用すると、より簡潔に記述できます。 @Composable fun MyPagingScreen(viewModel: MyViewModel) { // ViewModelのFlowを、Composeで扱えるLazyPagingItemsに変換 val lazyPagingItems = viewModel.items.collectAsLazyPagingItems() LazyColumn { // Paging3ライブラリ専用のitems拡張関数 // これを使うと、アイテムの取得やキーの管理などを自動で行ってくれる items( items = lazyPagingItems, key = { it } // 各アイテムの安定したキーを指定 (今回はString自体をキーに) ) { item -> // itemはnull許容型なので、nullでないことを確認 if (item != null ) { Text( text = item, modifier = Modifier .fillMaxWidth() .padding( 16 .dp) ) } } } } lazyPagingItems.loadState を監視することで、リストの末尾にローディングインジケーターやエラーメッセージを表示することも可能ですが、ここでは基本的なアイテムの表示に絞って紹介しました。 以上がPaging3をJetpack Composeで利用する際の、最もシンプルで基本的な実装の流れです。 Paging3 の「ツラミ」との遭遇 タイムラインリプレイスでのユースケースと課題 今回リプレイスしたタイムライン機能では、ユーザーが投稿に対して「リアクション」や「コメント」といったリアクションを行えます。これらの操作はAPIを通じてサーバーのデータを更新しますが、ユーザー体験を考慮し、操作後は画面全体を再読み込みするのではなく、対象の投稿アイテムの「リアクション」や「コメント数」といった情報をローカルで即時反映させたい、という要件がありました。 タイムライン上に表示される投稿の例 しかし、ここで大きな課題に直面しました。 Paging3で表示しているリスト内の特定のアイテムの状態を、ローカルで部分的に更新することは原則サポートされていませんでした。 なぜPaging3の要素は(簡単には)更新できないのか? この課題の根本原因は、Paging3の設計思想にあります。Paging3が提供する PagingData は、 その時点でのデータの不変なスナップショット として扱われます。これは、宣言的にUIを構築するJetpack Composeの考え方とも一致しています。 身近なもので例えるなら、 PagingData は「印刷済みの写真アルバム」のようなものです。アルバムに印刷された写真の一部分だけを後から修正するのは困難であり、もし修正したい場合は、元のネガフィルム(データソース)から、修正を加えた新しい写真を印刷し直し、アルバムのページごと差し替えるのが正しい手順です。 Paging3も同様に、表示されているリスト(写真)を直接操作することは推奨されていません。データの整合性を保つため、 変更は常に単一の信頼できる情報源(Single Source of Truth)、すなわち大元のデータソース(ネガフィルム)に対して行われるべき だとされています。データソースが更新されると、Paging3はそれを検知し、新しい PagingData のスナップショットを生成してUIにストリームします。この単一方向のデータの流れにより、複雑なページング処理や非同期なデータ読み込みの中でも、データの整合性が保たれるのです。 したがって、リスト内のアイテムをローカルで更新したい場合、 PagingData が参照している大元のデータソース(データベースなど)を更新し、 PagingSource の無効化(invalidation)をトリガーして、Paging3に新しいスナップショットを再生成させる のが正規のフローとなります。 最終的な回避方法 PagingDataAdapter.refresh() を呼び出してリスト全体を再取得する方法も検討しましたが、ユーザーのスクロール位置がリセットされるなど、UXの観点から今回の要件には合致しませんでした。 そこで、 更新が必要な要素のページングに関しては、Paging3を利用しない という方針を決定しました。 その代替として、 LazyColumn のスクロール状態を LazyListState で監視し、 ユーザーがリストの末尾近くまで表示したことを検知して、次のページを読み込むコールバックをトリガーする という方法を実装しました。 【ViewModel】 class TimelineViewModel( private val repository: TimelineRepository) : ViewModel() { private val _items = MutableStateFlow< List <TimelineItem>>(emptyList()) val items: StateFlow< List <TimelineItem>> = _items private val _isLoading = MutableStateFlow( false ) val isLoading: StateFlow< Boolean > = _isLoading private var currentPage = 1 private var canLoadMore = true // これ以上読み込むページがないことを示すフラグ init { loadMoreItems() // 最初のページを読み込む } fun loadMoreItems() { // ローディング中、またはこれ以上読み込むページがない場合は処理を中断 if (isLoading.value || ! canLoadMore) return viewModelScope.launch { _isLoading.value = true try { // Repositoryから次のページのデータを取得 val newItems = repository.fetchTimeline(page = currentPage) if (newItems.isNotEmpty()) { // 既存のリストに新しいアイテムを追加 _items.value + = newItems currentPage ++ } else { // 新しいアイテムがなければ、それ以上読み込まないようにフラグを更新 canLoadMore = false } } catch (e: Exception ) { // 実際にはエラー状態をUIに通知するなどの処理が必要 } finally { _isLoading.value = false } } } } 【Composable】 @Composable fun TimelineScreen(viewModel: TimelineViewModel) { val items by viewModel.items.collectAsState() val isLoading by viewModel.isLoading.collectAsState() val listState = rememberLazyListState() LazyColumn(state = listState) { items(items) { item -> TimelineItem(item = item) } // ローディング中であれば、リストの末尾にインジケーターを表示 if (isLoading) { item { Box(modifier = Modifier.fillMaxWidth().padding( 16 .dp)) { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) } } } } // LazyColumnのスクロール状態を監視 val isScrolledToEnd by remember { derivedStateOf { val layoutInfo = listState.layoutInfo val visibleItemsInfo = layoutInfo.visibleItemsInfo if (layoutInfo.totalItemsCount == 0 ) { false } else { // 最後に表示されているアイテムが、リスト全体の最後のアイテムであるか val lastVisibleItem = visibleItemsInfo.lastOrNull() lastVisibleItem?.index == layoutInfo.totalItemsCount - 1 } } } // リストの末尾までスクロールされたら、次のページを読み込む LaunchedEffect(isScrolledToEnd) { if (isScrolledToEnd) { viewModel.loadMoreItems() } } } この実装により、Paging3を使わずにページング機能を実現しつつ、 ViewModel が保持する StateFlow のリストを直接更新することで、ローカルでのデータ更新にも柔軟に対応できるようになりました。 取り得た別の選択肢 もしPaging3を選択した上で今回のケースを対応するとしたら、ローカルデータベースを管理するライブラリである Room を用いて、ローカルデータベースをデータソースとして扱ってページングする方法もありました。 Paging3 では、API から取得したデータを Room と同期をしながらページングする実装 RemoteMediator が用意されています。この記事では詳しくは述べませんが、これを利用すれば、ローカルで持っている Room に対して更新をかけることで、表示状態を更新することができます。 こちらの公式ドキュメントがよくまとまっています。 ネットワークとデータベースからページングする 今回のトレードオフ 先述の通り、 Room のようなローカルデータベースを信頼できる情報源(Single Source of Truth)として利用し、Paging3にその変更を監視させるのが、今回の要件を満たす上での最も望ましいアーキテクチャであったと認識しています。タイムラインの投稿は、ローカルにキャッシュすることのメリットも享受することができます。 しかし、今回は リリースまでの期間が短く設定されていたこと 、そして オフライン対応なども見据えたローカルデータベースのスキーマ設計には、慎重に時間をかけたかったこと から、この方法は見送りました。当時の開発の状況としては、致し方ない判断だったと思っています。 まとめ:Paging3 の注意点 今回のPaging3の一連の取り組みから、 Paging3で取得したデータは、不変なスナップショットとして扱われるため、リスト内の一要素をローカルで簡単に更新することはできない。 ということを学びました。チームとして Paging3 に今まで頼ってきていた経緯はありましたが、これを機に Paging3との付き合い方を考え直すきっかけになりました。 スタメンでは、Androidエンジニアを含めて、モバイルアプリ開発者を募集しています! herp.careers
Watchy が実践するAI活用 はじめに 👋 こんにちは。スタメンでソフトウェアエンジニアをしている yunboo です。 AI 技術の急速な発展により、プロダクト開発の現場も大きく変わろうとしています。しかし、単純に AI ツールを導入するだけでは、その真価を発揮することは難しいものです。 この記事では、私たち Watchy が実践している、AI 活用によるワークフロー改善について紹介します。プロダクト開発における根本的な課題を解決し、AI を「使用する」だけでなく「活用する」ためのアプローチをお伝えします。 プロダクト開発における根本的な課題 🤔 「なぜ作るのか」が不明確な開発要求 プロダクト開発の現場では、以下のような曖昧な要求に遭遇することがよくあります。 「これ作ったら売れる」 「お客様が欲しいと言ったから」 これらの要求は一見もっともらしく聞こえますが、詳細を聞いてみると実は作らなくても解決できることが多々あります。 「なぜ作るのか」(Why) が不明確 なままでは、真に価値のあるプロダクトを作ることはできません。 背景情報の属人化という問題 この問題の根本には、 背景情報の属人化 があります。 セールスは商談以外の業務も多く、重要な顧客の声が記録・共有されないまま失われる。 チーム全体で「なぜ作るのか」を腹落ちして開発できない状況が生まれる。 このような課題を解決するために、私たちは AI を活用したワークフロー改善に取り組んでいます。 AI を活用したワークフロー改善:フェーズ 1 🎯 目標:「なぜ作るのか」を可視化し、チーム全体で共有 フェーズ 1 では、営業・顧客対応から PRD(Product Requirements Document)作成までのプロセスを改善しました。 Step 1: "Why" を可視化する 👀 tl;dv を活用した商談の記録と分析 すべての商談を自動で録画・文字起こしし、いつでも振り返れる状態にしています。特に重要なのは、Meeting Template 機能を使って以下のようなキーワードを設定し、顧客の声を自動で抽出することです。 Current workflow(現在のワークフロー) Motivation(動機・課題感) Problems(具体的な問題) Feature requests(機能要望) これにより、「なぜその機能が必要なのか」という背景を確実にキャッチできるようになりました。 Step 2: チームに背景を共有する 🤝 tl;dv + Zapier + Notion による情報の自動蓄積 抽出した顧客の声を、チームの資産として確実に蓄積するために tl;dv で生成されたサマリーや文字起こしを Zapier 経由で Notion データベースに自動格納 重要な情報が属人化せず、フロー情報ではなくストック情報として蓄積される仕組みを構築 Step 3: 日常業務を効率化する ⚡ Notion AI によるカスタム自動入力 商談後の付帯業務を大幅に削減するために、Notion データベースへのアイテム追加をトリガーとして 商談後の御礼メール本文 を自動作成 Salesforce 用の議事録 を自動作成 これにより、営業担当者は商談そのものに集中できるようになりました。 Step 4: 今後の展開 🔮 現在計画している改善項目 Notion AI による要望の集約 : 商談 DB から要望をまとめ、要望 DB へ自動起票 PRD 作成プロセスのサポート : 要望 DB から PRD のファーストドラフトを自動作成 プロダクトビジョン、競合情報、市場分析、今期の注力ポイントなどをコンテキストとして活用 AI を活用したワークフロー改善:フェーズ 2 🚀 PRD 作成からリリースまでの自動化 AI の進化に伴い、開発プロセス全体の最適化も進めています。現在実験的に動かしているワークフローは以下の通りです。 Notion に起案アイテムを追加 優先順位を確定し、ステータスを 着手可能 に変更 ステータス変更をトリガーに GitHub Issue を自動作成 Notion + Slack + Devin Notion MCP + Cursor + GitHub MCP Zapier を使ったワークフロー構築などを検討中 Issue のレビュー Claude Code Action などで プルリクエスト を作成 プルリクエスト のレビュー・リリース このように、企画から実装まで一貫した自動化を目指しています。 その他の AI 活用事例💡 問い合わせ対応の効率化 課題 問い合わせ対応の履歴がメールやチャットに散在し、ナレッジが属人化 類似の問い合わせに毎回対応するコストが発生 解決策 全ての問い合わせ内容を Notion データベース に集約 Notion AI を使って、過去の類似問い合わせがないか検索できる仕組みを構築 情報のサイロ化を防ぐ 課題 「あのドキュメントどこだっけ?」 「〇〇機能のやり取りどこのチャンネルでしてたっけ、確認漏れないかな」 探す時間、人に聞く時間で作業が中断される 解決策 Notion AI で検索能力を強化(標準検索では見つけにくい情報も発見可能) Notion AI コネクター で Slack、GitHub、Google Drive を横断検索 スライド作成の AI 活用 課題 スライド作成ツールの操作やデザイン調整に時間を取られ、本来伝えたい内容に集中しづらい レビューや修正が煩雑になりがち 解決策 Cursor + Marp を使いスライドを作成 Markdown でスライドをテキストベースで AI(Cursor)に相談しながらブラッシュアップ Marp で Markdown から直接スライド化、見た目の調整も CSS で一括管理 テキストファイルなので Git でバージョン管理や共同編集も容易 まとめ:AI を「活用する」ために重要なこと 🎯 「解空間」を意識したアプローチ AI の能力を最大限に引き出すには、 インプットとなる情報の整理 と ワークフローの見直し が不可欠です。特に 「解空間」を意識 して業務やドキュメントを整備することが重要です。 解空間が曖昧だと AI も曖昧な答えしか返せない 解空間を明確に定義し、必要な情報を整理しておくことで、AI はより的確な提案やアウトプットができる ただし、創造力を豊かにするために意図的に限定しないケースもある Notion を組織の「脳みそ」に Watchy では、Notion を中心とした情報集約・AI 活用を進めています。 「まず Notion を見よう」「まず Notion AI に聞いてみよう」が当たり前の文化 を醸成することで、情報の活用度が大幅に向上しました。 今後の展望 AI 活用のベストプラクティスを社内で体系化し、誰でも活用できるようにしたい プロダクトの要望や問い合わせを自動で分類・優先順位付けする仕組みを作りたい tl;dv をもっと活用したい AI ファーストで業務や仕組みを設計し直すことが、組織の生産性や競争力向上につながるのでは おわりに 🌈 AI は単なるツールではなく、組織のワークフロー全体を変革する可能性を秘めています。重要なのは、AI に何を求めるのか、どんな情報を与えるのかを明確にすること。つまり「解空間」を意識した設計です。 私たち Watchy の取り組みが、皆さんのプロダクト開発にも少しでも参考になれば幸いです。AI 活用についてご質問やご意見があれば、ぜひお聞かせください。 Watchyではエンジニアを絶賛募集中です 🙌 herp.careers watchy.biz
はじめに こんにちは。スタメンでバックエンドエンジニアをしているきいろです。 今回、2025年6月28日(土)に京都の先斗町歌舞練場にて開催された関西Ruby会議08にTakeスポンサーとして出展してきましたので、その振り返りレポートを書きたいと思います。 関西Ruby会議ならではなのかスポンサーグループがMatz、Take、Umeなのは面白いですね。(最初"テイク"スポンサーと読んでいた) カンファレンス会場の先斗町歌舞練場。 軒先に並ぶ各スポンサーブースののぼり旗。壮観。 筆書きタッチのフォントで個人的には好みです。 セッション開場 開場近くの鴨川。この日は最高気温が34度まで上がった快晴でした。アツカッタ... 企業ブース この日展示していたブースの様子です。 スタメンのメインプロダクトであるTUNAGの機能 TUNAGベネフィット にちなんだ抽選を行いました。 来場者の方にその場で抽選にチャレンジしてもらい、当たった方にAnkerの充電器をプレゼントしました。また、ブースに訪問していただいた方全員にオリジナルステッカーも配布しました。 \関西Ruby会議08 #kanrk08 / スタメンブース、準備完了しました! ステッカーを無料で配布します!! 弊社プロダクトのTUNAG(ツナグ)の抽選機能を体験できます! 当たるとAnkerの充電器をゲットできます。 お気軽にお越しください! #TUNAG #kanrk08 pic.twitter.com/HQWRAINvL6 — stmn, inc. Developers (@stmn_eng) 2025年6月28日 気になったセッション Rubyを使った10年の個人開発でやってきたこと speakerdeck.com 自分が印象に残ったポイントは以下です。 自由にクラス設計がためせて楽しい 個人開発なので自分が欲しいものを作る(無理しない) 所属プロダクトチームが導入するコード規約や既存設計に縛られることなく、自由に自分のやりたい設計ができるのは個人開発の醍醐味かなとは思います。 クラス設計について、その時良いパターンだと思った設計を導入しても後から見ると...という点には自分の過去の実装でも痛いほど感じ入る部分があり、首がもげそうになりました。(自分のは設計というより実装がイケてないくらいの軽微なものですが) それでもその時に信じた設計を都度導入して振り返っていくことが大切なのだと個人的には感じました。 同じくオフライン参戦した まっきー の感想 Witchcraft for Memory speakerdeck.com memory_profiler gemではピーク時のメモリ使用量を計測することが難しく、 majo というメモリプロファイラを実装したという発表でした。 ガベージコレクションを生き延びた、寿命の長いオブジェクトを計測する機能があるとのこと。 GC周りの挙動の話を聞いて、native extensionを含むruby gemの実装の難易度を知れたことが印象に残りました。 また、C言語周りの話やコピーオンライト/プロセスfork/ソケット/パイプなど、学生時代にLinuxカーネルの授業を受けていた時によく聞いていた単語が出てきて懐かしい気持ちになりました。 おわりに 久々の京都の堪能しつつ、関西のRubyistたちと交流を深められた良いカンファレンスでした。 次の開催場所は滋賀県だそうです。次の地域交流も楽しみです。 スタメンではRubyエンジニアを絶賛募集中です🙌 herp.careers
こんにちは!プロダクト開発部のおしん( @38Punkd )です。 スタメンは、組織改善クラウドサービス「 TUNAG 」を提供しています。そのコア機能である『タイムライン』を、モバイルアプリでWebViewからSwiftUI / Jetpack Composeにリプレイスしました。今回は、その背景と技術選定、そして実現までの道のりをご紹介します。 リプレイスの背景 スタメンの主幹サービスである「TUNAG」は、『人と組織に働きがいを』提供することをミッションに掲げ、コミュニケーションの活性化、情報共有の促進、ビジョン浸透、人材定着、そして業務効率化をメインに、お客様の組織課題を解決するサービスです。 TUNAGにはWebアプリ版とモバイルアプリ版があり、ファーストリリースはWeb版が先行し、モバイルアプリ版もそれに追従する形でリリースされました。当時の組織のケイパビリティがWeb技術に寄っていたこともあり、モバイルアプリは、いわゆる ガワネイティブ の形式で開発を進めていました。 しかし、TUNAGをご利用いただくユーザー層が多様化するにつれて、アプリのパフォーマンス改善を求める声が次第に増えてきました。例えば、地下街のレストランや工場で働かれる方、あるいは社用端末を一律で配布され、最低限のスペックと弱い通信環境でTUNAGをご利用されているお客様も少なくありません。 私たちは、こういった方々のユーザー体験の課題を解決するため、モバイルアプリのパフォーマンス改善を真剣に検討し、技術選定の見直しを行うことになりました。 技術選定の見直し モバイルアプリ開発における様々なアプローチ方法の、一般的なメリット・デメリットを整理しました。 開発アプローチ メリット デメリット 1. SwiftUI / Jetpack Compose 高いパフォーマンスとUX: OSのUIコンポーネントを使用するため、高速でスムーズな動作。各プラットフォームのガイドラインに準拠したUI/UXを提供可。 フル機能へのアクセス: カメラやGPS等端末の全ての機能に直接アクセスでき、OSの最新機能やAPIにいち早く対応可。 開発コスト: iOS, Androidそれぞれ別の開発が必要になる。 2. Flutter / React Native 開発コスト削減: 一つのコードベースでiOS, Androidの両方に対応可。 ネイティブに近いパフォーマンスとUI: Flutterは独自のレンダリングエンジンを持ち、React NativeはOSのUIコンポーネントにブリッジするため、Swift / Kotlinに近いパフォーマンスとUI。 OS機能へのアクセス制限やライブラリ依存: 一部のOS機能には別途ブリッジ実装やサードパーティ製ライブラリへの依存が必要となる場合がある。 3. KMP / CMP (KMP/CMP)ビジネスロジックの共有による効率化: 共通のビジネスロジックをKotlinで記述し、iOS, Androidで共有可。 OSのUIの活用と柔軟性: UIは各OSのUIで実装するため、ガイドラインに準拠したUIとパフォーマンス。 (KMP)プレゼンテーション層の開発が二重になる可能性: UI層はiOS, Androidでそれぞれ開発する必要があるため、接続部分の工数は削減されない。 (CMP)成熟度とコミュニティの規模: 比較的新しい技術なため、利用可能なライブラリやツールが発展途上なことも。 4. WebView 開発コスト削減: 既存のWebコンテンツを流用でき、Web開発の知識を用いて開発可。 迅速なデプロイ: App StoreやGoogle Playの審査を通さずにコンテンツ更新可。 パフォーマンスの限界: WebコンテンツをWebView内で表示するため、複雑なUIやアニメーションでもたつきやカクつきが発生しやすい。 OS機能への制限とUXの低下: アクセスが制限されたり、ブリッジが必要な場合も。また、ストアの審査基準で単なるWebサイトの表示では却下の可能性も。 私たちの場合、 社内にSwift / Kotlinの経験者はそれぞれ複数名在籍しているが、FlutterやReact Nativeの経験者は不在だったこと クライアント側にそこまで複雑なロジックが多くないこと パフォーマンス課題が大きいため、UIの作り込みがしやすい技術が必要なこと という理由から、コア機能である『タイムライン』を、WebViewからSwiftUI / Jetpack Componseへリプレイスすることに踏み切りました。 ゴールと、その策定背景 実は、これまでもコア機能である『タイムライン』をネイティブ化しようという試みは、社内で何度か話題に上がっていました。 しかし、優先度の高い新機能開発や、モバイルエンジニアのリソース不足の問題で、残念ながら保留になってきた経緯があります。仮にネイティブ化に再チャレンジするとしても、中途半端にタイムラインのWebViewの一部コンポーネントだけをネイティブ化していては、かえって技術負債につながりかねません。 そこで私たちは、 画面単位でいち早く刷新しリリースすること を今回のプロジェクトのゴールとしました。 ゴール達成に向けた戦略 この大きな目標を達成するために、私たちは2つの戦略を立てました。 1. 社内からの共感を得る タイムラインは、20種類以上の項目の組み合わせからなる投稿の一覧が並ぶ、非常に複雑な仕様です。 そのため、エンジニアサイドのQAだけでは、不具合を発見しきれないと懸念していました。なので、より多くの人に触ってもらう社内リリースでの不具合の早期発見が重要と考えました。 エンジニアが長期間、技術刷新に時間を割くことの意味は、エンジニア同士なら理解しやすいかもしれません。しかし、エンジニアではない社員からは中々想像がつきにく、想像しにくいものに対しては興味や関心を持ってもらいにくいと考え、「そもそもネイティブ化とは何か」そして特に、「ネイティブ化したら何が嬉しいのか」の2点に絞って、全社への共有を徹底しました。そうしてプロジェクトへの理解と協力を得ることに努めました。 実際にプロジェクト開始前よりも多くの社員から、社内リリースされたアプリに対して、チャットでリアクションをもらったり、気づいたことを指摘したりしてくれるようになりました。 社内リリース期間中、不具合の修正内容を社内に報告した時の様子 2. 改善欲を抑える 技術刷新を進める中で、開発サイドから改善したい仕様やUI/UXが次々と出てくる可能性を考えました。 しかし、それら全てを都度議論していては、スケジュールが延びていく一方なので、あらかじめ改善したい仕様やUI/UXを、チームメンバーでスプレッドシートに洗い出し、デザイナーと徹底的に相談しました。 改善したい仕様やUIUX一覧 ここで洗い出されなかったものについては、議論が必要と判断された場合を除き、基本的に既存の仕様やUI/UXに合わせる方針を徹底しました。都度出てきた論点や改善点については、今回のリリース後の継続改善の一環として、改めて対応していくこととしました。 これにより、プロジェクトのスコープを明確にし、スピーディに進めることができ、プロジェクト開始から約8ヶ月でリリースを実限できました。 成果と今後の展望 パフォーマンス改善を検証するため、SwiftUI版とWebView版のタイムラインそれぞれで、大きな画像を含む全てのコンテンツが表示されるまでの速度を比較しました(下の図:縦軸はコンテンツが表示されるまでの相対値)。 その結果、通信環境が弱ければ弱い程、その差は大きくなる事がわかりました。 ネットワーク毎の速度比較 (※)『Very Bad Network』 は、iPhoneのデベロッパ用ネットワークリンク調節器にあるプリセットであり、3Gよりも弱い通信環境です。 今回の技術刷新によって、TUNAGをご利用いただく多様なお客様の環境に合わせたパフォーマンス改善を実現できました。 しかし、私たちのミッションは、パフォーマンスを改善するだけではありません。TUNAGを使ってもらうことで、『人と組織に働きがいを』最大限もたらせるようにすることが、私たちサービス開発者の本質的なミッションです。 そのためにも、今回のタイムラインのパフォーマンス改善を足がかりに、さらにプロダクト改善を進めていきたいと考えています。 スタメンではエンジニアを絶賛募集中です🙌 https://herp.careers/v1/stmn/requisition-groups/8d5d0858-2bda-4167-80f2-2401d8fb1891 herp.careers
目まぐるしく変化するIT業界において、企業が競争力を維持し続けるためには、常に新しい知識を取り入れ、お互いに学び合う文化が不可欠です。そこで、プロダクト部門では先日、エンジニア・プロダクトマネージャー(PdM)・デザイナーが一堂に会する社内LT会を開催しました! 開催の狙い:全員で高め合う学びの場づくり 今回のLT会は、部門全体の知識共有と交流を深め、プロダクト開発における連携を強化することを目的としています。異なる職種のメンバーがそれぞれの知見を発表し、質疑応答を通じて相互理解を深めることで、個人のスキルアップはもちろんのこと、組織全体の成長に繋がることを目指しました。 今回のLT会の発表内容 今回のLT会では、多岐にわたるテーマで発表が行われました。 タイトル 登壇者 キーノート: 「アウトプットしようね」 エンジニアリングマネージャー あさしん 「ゴルフから学ぶPdMの仕事術」 プロダクトマネージャー えんまめ 「モバイルアプリ、はじめの一歩」 モバイルアプリエンジニア とんとんぼ 「Ask Devinで変わる開発スタイル」 フロントエンドエンジニア 森 「企画を動かすデザイナーの思考!『広げて絞る』アプローチ。」 プロダクトデザイナー おたけ 「Hotwireを導入しました!」 プラットフォーム部バックエンドエンジニア 近藤 「5分でのぞく!セキュリティ窓口の舞台裏」 プロダクト開発部バックエンドエンジニア じゅけい 「生成AIでどの部署でも本業に集中できるチーム作り」 Watchy事業部 エンジニア ゆんま 通常業務がある中、クオリティの高いLTばかりで、運営チームとしては感謝してもしきれません...! 今回のLT会で行った取り組みの紹介 今回のLT会では、これまでの開催で得た学びを活かし、いくつかの新しい取り組みに挑戦しました。 事前アンケートによる発表内容の決定: 開催までの時間が限られていたため、今回は事前に振り分けた各チームで事前に「他のチームに聞きたいこと」をアンケートで収集しました。これにより、短期間でも他者に価値を提供できるGiveを意識した発表が揃い、誰かが聞きたい・興味のある学びを提供できました。 各チーム代表者からの登壇者選出: 過去は立候補形式が多かったのですが、各チームから1名ずつ登壇者を決めてもらう形式を採用しました。これにより、エンジニア、PdM、デザイナーといった様々な職種の発表をバランスよく聞くことができ、幅広い知見に触れる機会となりました。 キーノートセッションの用意: 当初予定していなかったキーノートセッションですが、プロダクト部門のEM(エンジニアリングマネージャー)との雑談の中で、アウトプットへの熱い想いを聞き、発表をお願いしました。アンケート結果では、このセッションが参加者にも刺さったことが分かり、思わぬところからよい発見がありました。 ハッシュタグを作ってXに投稿する: 社内勉強会ではあるのですが、ハッシュタグを作って公開OKな範囲についてはSNSへの投稿をOKにしました。何人かご協力いただきました!少しでも社外の皆さんに当日の雰囲気が伝わればうれしいです!「 #スタメンテック 」をご覧ください! 盛り上げグッズとしてペンライトとうちわを使う: 閲覧側もなるべく参加できる形にしたかったので、質疑応答の時間を設けたり拍手をお願いしたりはもちろん、盛り上げグッズとしてみんなにペンライトや応援うちわを渡して会場を盛り上げてもらいました。まさか登壇者も人生でペンライトで応援してもらう時が来るとは思わなかったでしょう...。(ノリのいいみんなに感謝です!) オープニングの時点でぶん回してくれています🪄 #スタメンテック はじまりましたー! pic.twitter.com/Pe2IYVHdD8 — hisayuki (@hisayuki_mori) 2025年7月3日 開催した結果と今後の開催 開催後のアンケートでは、「専門外の分野を知れる機会になってとてもありがたかった」「真似してみよう、これできそうかもといったヒントになった」「どの発表も「いつか役に立つ」ではなく「今すぐ役に立つ」だったのがよかった」といった、ポジティブな感想が多数寄せられました。いただいたご意見を参考に、今後も継続的に勉強会を企画していく予定です。 今回は運営チーム3人のデザイナーとエンジニアの意志で開催していたのですが、以下のような感想もいただけて運営チームとしても開催してよかったです。 「組織のコミュニケーションを活発にし続けるのに努力と行動をしている方がいらっしゃって、良い組織が成り立つと感じました。自分もその一員になるように頑張ろうと思いました。」 さらに嬉しいことに、今回のLT会をきっかけに運営メンバー以外からも「エンジニア向けの勉強会を定期的に開催したい」という声が上がり、現在はその実現に向けて動き出しています。 私たちは、お互いが積極的に学び合い、それを発信し共有することを大切にしています。これからも、会社全体で成長し続けられるよう、学びの機会を積極的に作っていきたいと思っています。 最後に スタメンでは、Rubyエンジニアに限らず全技術領域で、プロダクトを成長させていくエンジニア、デザイナー、プロダクトマネージャーの方を募集しています。 私たちの学び合う文化に共感し、一緒にプロダクトを創っていきたいと感じた方は、ぜひ採用情報もご覧ください! herp.careers herp.careers
こんにちは! 株式会社スタメン 、プラットフォーム部のもりしたです。 先日、7月11日(金)から12日(土)にかけて東京のTOC有明で開催された「 SRE NEXT 2025 」にオフラインで参加してきました!SRE NEXTへの参加は今回が初めてでしたが、非常に学びが多く、充実した2日間を過ごすことができました。 sre-next.dev 参加メンバー集合写真(もりしたは写真撮影係してます) なぜSRE NEXTに参加したのか? 私はSRE業務をミッションの一つとするプラットフォーム部に所属しています。日々の業務の中で「SREとして、これから何に取り組むべきか?」という問いに対し、具体的なヒントを得たいという期待を抱いていました。 今年は「 SREをはじめよう - O'Reilly Japan 」をはじめとするSRE関連書籍を多く読み込んできましたが、座学で得た知識を実際の業務でどのように実践すれば良いのか、具体的なイメージが湧かないという悩みがありました。そのため、他社事例から学ぶことが最も効果的だと考え、今回のSRE NEXTへの参加を決めました。 www.oreilly.co.jp SRE NEXT とは? SRE NEXTについてご存じない方のために、簡単にご紹介します。 SRE NEXTは、 SRE Lounge のメンバーの方々が中心となって運営・開催している、信頼性に関するプラクティスに深い関心を持つエンジニアのためのカンファレンスです。2020年に初開催されて以来、今回で5回目の開催となります。 ※ SRE : Site Reliability Engineering の略。Googleが提唱したシステム管理とサービス運用の方法論です スタメンはロゴスポンサーとしてSRE NEXTを応援! 第5回目となるSRE NEXT 2025で、なんと弊社スタメンはロゴスポンサーを務めさせていただきました!会場のTrack Aセッション会場のスクリーンでスポンサー紹介が流れていたのですが、ご覧いただけたでしょうか? ロゴスポンサー紹介。右下が弊社スタメンのロゴです 来場者数から見るSRE NEXTの熱気 閉会式で発表された情報によると、今回のSRE NEXT 2025の来場者は合計 1,272 人でした。オンラインとのハイブリッド開催でしたが、オフライン参加者が約1.4倍と多く、その内訳は以下の通りです。 合計: 1,272人 内訳: オンライン 532人 / オフライン 740人 オフライン参加者が多かったのは、やはりSpeakerや他の参加者の熱量を直接感じられたり、ブースを巡る楽しさなど、オフラインならではの魅力があったからだと感じました。 現地の雰囲気を写真でご紹介! 会場の様子を少しでも感じていただけるよう、いくつか写真を掲載します。(顔が写り込んでいる箇所はマスキングしています) SREに関するアンケートボード 会場フロアの4Fにエスカレーターで上がると、SREに関する興味深いアンケートボードがありました。 アンケートボード全体 アンケートボード左側 Q1〜Q5の回答から感じたこと: Q1(あなたのSREタイプは?): Platform Engineeringの回答が多く、現在のトレンドを反映しているように感じました。(私もプラットフォーム部なので納得です!) Q2(SLO導入レベル): まだSLOの導入が進んでいない企業が多いようですね。 Q3(SRE Practice どれが好き?): Toil削減やポストモーテムは多くの方が実践されているようです。ポストモーテムは運用が難しい側面もあるので、他社事例をさらに聞いてみたいと思いました。 Q4(あなたのSRE・エンジニア歴は?): 「 SRE サイトリライアビリティエンジニアリング - O'Reilly Japan 」が発売され、SREの存在が広く認知されてからと比較すると、10年未満のSRE歴の方が多いことがわかります。 Q5(使用している監視ツール): Datadog は弊社でも利用していますが、さらに使いこなしたいと改めて思いました。 アンケートボード右側 Q6、Q7の回答から感じたこと: Q6(SRE本読んでいる?): 「 SRE サイトリライアビリティエンジニアリング - O'Reilly Japan 」の読者数が圧倒的でした。私もかなり前に読みましたが、SREのバイブルですね。次に多い「 入門 監視 - O'Reilly Japan 」もぜひ読破したいです。 Q7(SREチームの担っている役割): SREがオンコール担当の「支援」に回ることが多いという点は興味深かったです。これは、実際に開発しているチームが主体となってオンコール対応を行い、SREがそのサポートに徹するという役割分担を示しているのかもしれません。 熱気に包まれたセッション会場 開会式前 開会式やTrack Aのセッションは広々とした会場で行われました。会場が広いため、スクリーンが2つ用意されており、どの席からも見やすい工夫がされていました。 (早い時間に着席したため、写真ではまだ参加人数は少ないです) 賑わうブース会場 ブース会場 セッションの合間には、各社のブースが並ぶ会場へ。 ガチャガチャを回せたり、美味しいコーヒーをいただけたりと、お祭り気分で楽しめました。多くの参加者で賑わっており、活気に満ちていました。 参加者の熱気が伝わる懇親会 懇親会での乾杯 閉会式後には懇親会に参加しました。 とにかく人が多く、オフライン参加者のほとんどが参加していたのではないでしょうか。 Speakerの方々は特に人気で、話を聞きたい参加者に囲まれているのが印象的でした。 特に注目したセッションとLT:SREにおけるAI活用の最前線 2日間でKeynote、セッション、LTを合わせて20近く聞きました。 全体を通して感じたのは、 AI活用 をテーマにしたセッションが多かったことです。 AIをメインテーマとしていない場合でも、内容にAIが登場する機会が多く、システム開発におけるAIの浸透を強く感じました。 今回は、AI活用に注目して特に印象に残ったセッションとLTをそれぞれ1つずつご紹介します。 SRE へのサポートケースをAIに管理させる方法 speakerdeck.com Ubie株式会社 SREのGumiさんによるセッションです。 SREチームに寄せられる問い合わせ対応による業務負担を、AIを活用して軽減した事例が紹介されました。 内製で作り込まれたAI BotやWebアプリケーションは、簡単に真似できるレベルではないと感じましたが、「こんなものを作ってみたい!」「どうやって動いているのかもっと知りたい!」と強く思いました。 特に興味深かったのは、 「AIもオンボーディングが必要」 という点です。 新しいエンジニアが入社する際と同様に、AIを立ち上げるためには、今回のケースではドキュメント整備といった準備が必要なんですね。 MCPサーバで始めたアラート整理の実験的取り組み speakerdeck.com 株式会社モニクル SREのErika TakadaさんによるLTです。 こちらもAI活用の事例ですが、 「小さいところから始められるAI活用」 という点が非常に参考になりました。 アラート通知のトリアージは経験値に大きく左右され、オンコールに加わったばかりのメンバーにとっては不安が大きい業務です。 このLTでは、MCPサーバを活用して調査のヒントとなる情報が得られるよう工夫されていました。 これにより、アラート対応の精神的な負担が大幅に軽減されるだろうと感じました。 さいごに SRE NEXT 2025に参加し、SREの最前線とAI活用の可能性を肌で感じることができました。今回の学びを日々の業務に活かし、スタメンのプラットフォームをより強固なものにしていきたいと思います。 株式会社スタメンでは、プロダクト開発に関わる全ての領域で、プロダクト職種の採用を積極的に行っています。 今回レポートしたSite Reliability Engineer (SRE)の領域にご興味をお持ちいただけたなら、ぜひプラットフォーム部にご応募ください! 皆さんとお話できることを楽しみにしています。 herp.careers herp.careers herp.careers
はじめに こんにちは、プロダクト開発部の 勝間田 です。 先日、サービスのパフォーマンス向上のため、ECSで稼働しているSidekiqコンテナのvCPUをスケールアップしました。増やしたCPUリソースを最大限に活用すべく、Sidekiqのconcurrencyも引き上げたのですが、CPU使用率が期待通りに上がらず、オートスケールが機能しないという事態に陥りました。 この出来事をきっかけに、CPU、プロセス、スレッド(concurrency)、そしてRubyのGVL(Global VM Lock)について自分の理解が不十分だと気づきましたので、これらについて調査・検証を行いました。 きっかけ:予期せぬCPU使用率の低下 私たちのサービスでは、バックグラウンドジョブの実行にSidekiqを利用しています。CPU使用率に基づいたオートスケールを設定しており、1vCPU 8GBの環境で処理していました。 ここ最近メトリクスを確認するとメモリ使用率が頻繁に100%近くに達することがあったため、安定性向上・パフォーマンス向上を目指し、コンテナのスペックを1vCPU→2vCPU、8GB → 16GBにスケールアップしました。 【当初の期待】 vCPUが2倍になったので、Sidekiqのconcurrency(同時に処理するスレッド数)も約2倍に引き上げる。 これにより、CPU使用率は以前と同水準を維持しつつ、処理スループットが2倍になるはずだった。 【実際の結果】 concurrencyを増やしたにもかかわらず、CPU使用率がスケールアップ前の半分程度に低下してしまった。 その結果、オートスケーリングの閾値にも達しない状態になってしまった。 1vCPUの時点ではCPUリソースを使い切るほどジョブを捌けていたのですが、2vCPUにしたところ増やした分のCPUが遊んでしまいました。 原因は単純で実行者であるRubyプロセスが1つのままだった点にありました。 既にご存知の方も多いかもしれませんが、Rubyには GVL という「1つのRubyプロセス内で、同時に複数のスレッドがRubyのコードを並列で実行することを防ぐ」仕組みがあります。 つまり、複数のスレッドが存在していても、実際にCPUを使ってRubyのコードを実行できるのは、常にたった1つのスレッドだけです。 参考: class Thread (Ruby 3.4 リファレンスマニュアル) そのため良かれと思ってスケールアップしたものの、結果的には手付かずの「空きCPU」を1つ作ってしまっただけでした。 concurrencyで指定した分、同時に処理されていると勘違いをしており、実際にはごく短い時間でスレッドを切り替えながら処理を進めている「見かけ上の並列処理(並行処理)」であることを理解しました。 スレッドの切り替えは以下の条件で行われます。 スレッドがCPUバウンド(処理速度がCPUの計算性能によって制限されている状態)の処理を完了させる スレッドがI/O操作を実行する(この場合自動的にGVLを解放する) GVL保持期間を超える(デフォルト100ms) 参考: スレッドの切り替え I/O待ちが発生する処理(例:データベースへのクエリ、外部API呼び出し、ファイルの読み書きなど)を行っている間は、GVLを解放する仕組みになっています。 あるスレッドがI/O待ちでブロックされると、そのスレッドはGVLを解放し、待機していた別のスレッドが実行権を得て処理を開始できます。これにより、I/Oバウンド(処理速度がInput/Outputの速度によって制限されている状態)な処理が中心のアプリケーションでは、マルチスレッドによるスループットの向上が期待できます。 コンカレンシーとパラレリズム、またGVLが行っていることをスーパーマーケットのレジに例えた解説がわかりやすかったです↓ techracho.bpsinc.jp 検証:GVLの動き 理論はわかったのですが、実際にGVLがどのように動作しているのかを確かめるため、CPUに負荷をかけるCPUバウンドなワーカーを用意して検証を行いました。 実行環境:Ruby 3.4.3 検証用ワーカー(Geminiにお願いしました) class BenchmarkWorker include Sidekiq :: Job def perform (worker_id, iterations = 50_000_000_0 ) puts " [開始] Worker #{ worker_id }" start_time = Time .now result = 1 counter = 0 while counter < iterations counter += 1 result = (result + counter + worker_id) % 1000000 # 1000万回ごとに進捗を出力 if counter % 10_000_000 == 0 progress = (counter.to_f / iterations * 100 ).round( 1 ) puts " [ #{ Time .now.strftime( ' %H:%M:%S.%3N ' ) } ] Worker #{ worker_id } : #{ progress } % " end end duration = Time .now - start_time puts " [完了] Worker #{ worker_id } : #{ duration.round( 3 ) } 秒 " return duration end end 単独でワーカーを動かしたログは以下です。 処理完了まで 約25秒 かかりました。 またputsの 間隔は約0.5秒 ほどでした。 INFO 2025-06-17T00:30:58.839Z pid=1 tid=cqx jid=bde01311593723bcb73d5bbc class=BenchmarkWorker: start [開始] Worker1 [09:31:01.059] Worker1: 2.0% (10000000/500000000) result=1 [09:31:01.552] Worker1: 4.0% (20000000/500000000) result=1 [09:31:02.089] Worker1: 6.0% (30000000/500000000) result=1 [09:31:02.606] Worker1: 8.0% (40000000/500000000) result=1 ... [09:31:24.922] Worker1: 98.0% (490000000/500000000) result=1 [09:31:25.393] Worker1: 100.0% (500000000/500000000) result=1 [完了] Worker1: 24.825秒 INFO 2025-06-17T00:31:25.401Z pid=1 tid=cqx jid=bde01311593723bcb73d5bbc class=BenchmarkWorker elapsed=26.562: done 仮説 真の並列処理の場合: 1つのワーカーを実行する時間と、2つのワーカーを同時に実行する時間は、ほぼ同じになるはず。 GVLが効いている場合: 2つのワーカーを同時に実行すると、1つのワーカーを実行する時間の約2倍の時間がかかるはず。 concurrencyが2以上のキューに対して2つ同時にエンキューしたログが以下です。 INFO 2025-06-17T00:31:32.045Z pid=1 tid=crd jid=89ef0d0e04252a9dd9dd6e12 class=BenchmarkWorker: start INFO 2025-06-17T00:31:32.045Z pid=1 tid=cpl jid=97b2acb34b6ff2c8dcf5a4fd class=BenchmarkWorker: start [開始] Worker1 [09:31:32.540] Worker1: 2.0% (10000000/500000000) result=1 [開始] Worker2 [09:31:33.200] Worker1: 4.0% (20000000/500000000) result=1 [09:31:33.932] Worker2: 2.0% (10000000/500000000) result=1 [09:31:34.103] Worker1: 6.0% (30000000/500000000) result=1 [09:31:34.980] Worker2: 4.0% (20000000/500000000) result=1 [09:31:35.104] Worker1: 8.0% (40000000/500000000) result=1 [09:31:35.929] Worker2: 6.0% (30000000/500000000) result=1 ... [09:32:19.217] Worker1: 96.0% (480000000/500000000) result=1 [09:32:19.251] Worker2: 92.0% (460000000/500000000) result=1 [09:32:20.123] Worker1: 98.0% (490000000/500000000) result=1 [09:32:20.258] Worker2: 94.0% (470000000/500000000) result=1 [09:32:21.086] Worker1: 100.0% (500000000/500000000) result=1 [完了] Worker1: 49.083秒 INFO 2025-06-17T00:32:21.159Z pid=1 tid=crd jid=89ef0d0e04252a9dd9dd6e12 class=BenchmarkWorker elapsed=49.114: done (Worker2の完了ログは割愛しますが、ほぼ同時に完了しています) ログを見ると、Worker1とWorker2の進捗が交互に出力されていることがわかります。 そして、完了までにかかった時間は 約49秒 と 単独実行のほぼ2倍 になりました。 Worker1とWorker2それぞれのputsの間隔も2倍近くの値 になっています。 2つのスレッドが同時にそれぞれ実行されているのではなく、GVLによって交互に実行されている(コンテキストスイッチを繰り返している)ことがわかりました。 続いてconcurrency: 1のSidekiqキューに対して、2つ同時にエンキューした場合の挙動についても検証しました。 INFO 2025-06-17T00:51:47.438Z pid=1 tid=cs9 jid=6f918c7413e8b90a82687d50 class=BenchmarkWorker: start [開始] Worker1 [09:51:49.372] Worker1: 2.0% (10000000/500000000) result=1 [09:51:49.867] Worker1: 4.0% (20000000/500000000) result=1 ... [09:52:14.165] Worker1: 100.0% (500000000/500000000) result=1 [完了] Worker1: 25.375秒 INFO 2025-06-17T00:52:14.174Z pid=1 tid=cs9 jid=6f918c7413e8b90a82687d50 class=BenchmarkWorker elapsed=26.737: done INFO 2025-06-17T00:52:14.176Z pid=1 tid=cs9 jid=f29879723f0eb233ea8f5c10 class=BenchmarkWorker: start [開始] Worker2 [09:52:14.663] Worker2: 2.0% (10000000/500000000) result=1 [09:52:15.146] Worker2: 4.0% (20000000/500000000) result=1 ... [09:52:38.078] Worker2: 98.0% (490000000/500000000) result=1 [09:52:38.556] Worker2: 100.0% (500000000/500000000) result=1 [完了] Worker2: 24.376秒 INFO 2025-06-17T00:52:38.556Z pid=1 tid=cs9 jid=f29879723f0eb233ea8f5c10 class=BenchmarkWorker elapsed=24.381: done ログから、Worker1が完全に終了した後にWorker2が開始されていることがわかります。 concurrencyの設定が正しく機能し、ジョブが一つずつ処理されていることが確認できました。 まとめ 今回の経験からRubyによる開発において、以下のような学びを得ることができました。 GVL(Global VM Lock)は単一のRubyプロセス内では、同時に1つのスレッドだけがCPUバウンドのコードを実行できるようにする Rubyのマルチスレッドは見かけの並列化であり、本当に並列で処理が進んでいるわけではない I/Oバウンドな処理が多い場合、I/O待ちの間にGVLが解放されるため、スレッド(concurrency)を増やすことでスループットの向上が期待できる CPUバウンドな処理をスケールさせたい場合、スレッド(concurrency)ではなくプロセスを増やす必要がある Sidekiqのconcurrencyの設定値をただ大きくするだけでは改善に繋がらない可能性がある Sidekiqのスケールアップでconcurrencyを増やした際、プロセス数自体は変わっていないため結果CPUを有効活用できないことに気づきました。この経験が、プロセス、concurrency、そしてRubyのGVLについて理解を深めるきっかけとなりました。 得た学びを生かし、効率的でパフォーマンスの高い設定を追求していきたいと思っています。 最後に スタメンでは、Rubyエンジニアに限らず全技術領域で、プロダクトを成長させていくエンジニア、デザイナー、プロダクトマネージャーの方を募集しています。 herp.careers