エンジニアの産休〜育休復帰〜その後8ヶ月を振り返る!

こんにちは、プレックスのtmmfireです。 私事ですが、2024年6月に第一子が生まれました。2024年5月から産休を取得し、そのまま育休に入り、2025年4月にフルタイムで復帰しました。 妊娠発覚から今に至るまでは大変だったことや不安に思うこともありましたが、組織とチームの理解、そして家族の支えがあって、どうにかやっていけている状況です。

そしてどうやら私はプレックスで最初に産休を取得して戻ってきた社員のようで、また、女性エンジニアの育休復帰エントリは数が多くなさそうで、ママエンジニアの一つの事例を残せたらという気持ちで、産休〜復帰〜現在に至るまでを簡単に振り返ります。

産休まで

妊娠報告と変則勤務のお願い

産休は法定で認められている出産予定日の6週前から取りました。

安定期に入った頃にチームとマネージャーには妊娠したことを共有して、お腹が目立ち始めてからは満員電車を避けられるように時差出勤をさせてもらっていました。始業時間を遅らせていたため、朝礼として行っていたデイリースタンドアップも開催時間を変更してもらいました。

また、産休入直前の1ヶ月間は特例でフルリモートによる勤務を認めてもらっていました。(マネージャーからは、それ以前から「必要があればフルリモート勤務に切り替えて問題ない」と提案をもらっていました)

これらはチーム全体へ影響のある働き方であるため、理解と協力がなければ成り立たちませんでした。私からの申し入れを心良く受け入れてくれたチームへ感謝の気持ちでいっぱいです。

引き継ぎ事項など

当時、PLEXJOB開発チームはマネージャーを含めて社員3名(+業務委託/インターン1〜3名)体制でした。小規模であったというのもあり、属人的なタスクというのはあまりなく、日々の業務での長期休業に向けた特別な引き継ぎ事項というものはほとんどありませんでした。直近の自分の実装したタスクの注意点を共有する時間等は設けましたが、これらは通常業務の中でも必要があれば行われることでした。

通常業務からやや外れたところで、プロダクトの月次振り返りについて、私が手集計していたものがあったため、それらはETLのような仕組みを作って自動集計されたものをダッシュボードで確認できるようにしておきました。

そして産休へ

産休に入ってから予定日までは1ヶ月ほどだったのですがいよいよお腹も大きくなり、あまりアクティブに何かをするという感じにもなれずにのんびりとした日々を過ごしました。計画無痛分娩の予定だったのでいつ産まれるのだろうかという不安もなく過ごしていたのですが、なんと我が子はなんの前触れもなく予定日より10日も早く生まれてきたので、私は突然の陣痛、突然の破水といったそこそこドラマチックな体験をきちんとこなすことになりました。

産休〜育休期間中の過ごし方

慌ただしいが穏やかな日々

子どもが1ヶ月〜3ヶ月の間は、今思い返すと子育て生活が始まってから一番穏やかだった時期です。我が家は夫も私と同程度の期間育休を取得したのである程度夫婦でローテーションしながら育児を進めることができました。

もちろん新生児は3時間に1度はミルクをあげる必要があったり、私自身の産後の体調トラブルもなんだかんだ3ヶ月程度は続きました(そしてその後もマイナートラブルは続きます)。けれどいつもうっすら睡眠不足ではあるものの、そもそも3ヶ月未満の子どもは自分で寝返りもうてないような存在なので、うつ伏せ寝さえ気をつければ誤飲やその他の事故は起こりづらい時期でもありました。体調は本調子ではないものの、そもそもそこまでハードに動く必要のある時期ではない為、家族でのんびりと過ごす事ができました。産休育休中は家計簿をnotionに記録していたので、表示の為のダッシュボードを構築したりしていました。

4ヶ月を過ぎて体調が産前に近づいて来たなーと思う頃に子どもの活動量も徐々に増え始め、人の身体って良く出来てるのだなと思ったりしました。もちろん、母体にも子どもにも個人差はあると思います。

保活について

私の住んでいる地域は実質待機児童0名に近いので、4月入園を狙えば現実的な登園圏内の園へ入園できる可能性が高そうでした。よって、6月生まれの娘は生後10ヶ月の保育園デビューを目指して保活を始めました。保活とは、保育園入園に向けた活動のことを指します。

保活の大まかな流れは保育園見学〜入園申し込み〜入園可否の結果発表となります。

保育園見学はしなくても申し込みはできますが、パンフレットやWEBページに記載される"スペック"だけでは分からない点も多く、園によってカラーも違う為、申し込みを行う園へは一度足を運んだ方が良さそうでした。というか、何園か見学に行ってあまりにもカラーが異なった為、一度も見学していない園に申し込む気持ちにはなりませんでした。また、送り迎えは毎日のことになるので、通勤動線から大きく外れていないかなど実際自宅〜保育園〜最寄り駅のルートを確認しておくと良いと思います。保育園の見学は通年行っている園もあれば時期が限られる園もある為、早めに情報を集めておく必要があります。

4月入園希望者は地域によって差はありますがおおよそ前年の9月〜11月頃申し込みを行い、翌年2月ごろに入園可否の発表があります。つまり会社にも復帰予定2ヶ月を切っているタイミングでしか復帰確定の連絡ができません。保育園申し込みの頃に会社の人事総務と自部署へ4月復帰の希望は伝えていましたが、実際に復帰確定〜復帰までは驚くほど時間がありません。

育休復帰直後

コードを眺めて目が滑る、しかしAIの進化に助けられる

そして仕事復帰の日がやってきます。約1年間コードを書かず、読まずに近い状況であった為、復帰最初の3日間は正直キツかったです(笑)。 画面に模様が書いてあるなという感覚。Gitコマンドも思い出せません。というかGitとかCLIベースのツールは頭で覚えるというより体が覚えているという感覚だったので、考えても何も出てきません(だって皆、タッチタイピングはできてもアルファベットをキーボードの配置で並べ直すのは難しいでしょ?)。

そこでお久しぶりです、ChatGPT君。最近はClaude Codeなるツールも登場しているそうですね。産休前もやれGPT3.5から4系の進化は凄まじいぞとかありましたけど、生成AI業界の1年は今までの人の歴史の数十倍の速度で進みます。幸い何が分からないかくらいは分かっていたのでChatGPTにどんどん聞きます。これ、ググってると間に合わない。基礎的な開発ツールのレクチャーは彼らの得意領域です。そしていつの間にかAIエージェント(と呼ばれるようになってた)はインターネットも使いこなしていました。

そんな感じでAIエージェントに伴走してもらいつつ、半月も経つと画面に広がっていた模様が意味のあるものに見えてくるのでした。

母が頑張る間、子も頑張る

一方我が子ですが、通常保育園に入園して1ヶ月程度は「慣れ保育」期間となり、一日1時間からの保育時間からのスタートになります。我が家の場合は私の復帰初月の4月中は夫に育休を取ってもらいました。私は慣れ保育期間の面倒を見てもらえたので仕事に専念できましたが、入園後しばらくは慣れ保育期間があるため入園即仕事復帰というのは通常難しいと思います。

母が仕事で悲鳴を上げている間、我が子は社会生活の大いなる一歩として保育園生活を始めていました。母とは異なり0歳児の社会適応能力は大したもので、毎日マイペースに通っているようでした。

パワーアップしたチーム体制

産休前は自分も含めて社員3名体制だったチームも、復帰後は倍の6名体制となっていました。

平均年齢が下がりましたが若いメンバーは良い意味で遠慮がなく、以前よりもチーム内の意見交換が盛んに行われるようになっていました。チームは通常の開発タスクの他に、各エンジニアが通常の開発業務の中で感じた課題(非機能要件の改善や社内システムの改善が多い)を起票して棚卸しを行う定例会を週次で行っています。定例では常に複数の課題が起票されており、3名体制の頃とは明らかに違うスピード感でプロダクトが成長しているのだと感じました。

ジェネレーションギャップにビビりながらも若いメンバーから学べる事は多く、そのエネルギーは復帰後の大きな励みとなりました。

5月、本当のワーキングママライフが始まる

保育園の洗礼

そんな言葉がありますが、保育園に通い始めると子どもはとにかく病気をもらってきます。

そしてそれが子どもだけであればまだしも大人にも容赦なく感染します。コロナ禍以降、それなりに神経質に手洗いをするようになり風邪って本当に引かなくなったよなあなんて思っていたのですが、子どもの風邪は本当に感染ります。子どもと触れ合うたびに手を洗うなんて不可能ですし、彼らの距離感は、大人の感覚とは大きく異なります。お客様〜口に手を入れるのはおやめになってください〜。といった具合です。

5月に入った途端、我が子は月の半分ほど保育園を休みました。しかも熱があれば登園はできないのですが、本人は至って元気だったりします。こうなっては大人も休んでそばにいる他ありません。どうにか夫とローテーションしながら仕事を進めますが、休みを取らざるを得ない日も多く思ったように仕事が進まない事も多かったです。 不在で受けられない問い合わせ対応や遅れた一部のタスクをチームで巻き取ってもらう事も多く、皆に申し訳なさを感じるのと同時に、働ける時にきちんとチームへ還元したいという気持ちも強くなりました。

そして今に至る

現在、復帰から約7ヶ月半が経過しました。子どもは1歳6ヶ月です。

相変わらず突然の体調不良はありますし(先月は一家全員でインフルエンザにかかりました)、フルタイム復帰ではあるものの定時即帰宅でないと家庭が回らない為、他のメンバーに比べるとどうしても時間的制約が大きい働き方になります。また、今のところ我が子は一般的な範疇の体調不良くらいしかトラブルがありませんし、トラブルの多くを夫がカバーしてくれている状況ですが、今後の子どもの成長や夫の職場環境の変化によっては私自身の働き方を変えていく必要があるかもしれません。

しかし現時点ではどうにかリズムを持って働く事ができており、ある程度規模の大きいタスクもより緊張感を持ってスケジュール管理できるようになったりと、自身の仕事の組み立て方に寄与している部分もあると思っています。この先今の働き方が永久に続けられるとは思っていないのですが、今はただ私の仕事のスケジュールの相談に乗って裁量を与えてくれるマネージャーとトラブルがあったときにフォローしてくれる周りのメンバーに感謝しております。


そんな私たちのPLEXJOB開発チームを含め、株式会社プレックスでは一緒に働くエンジニアを募集しております! 興味を持っていただいた方はぜひご連絡ください!

エンジニア募集

100兆円規模のインフラ産業の課題解決に挑戦|業務支援SaaSのテックリード - 株式会社プレックス

急成長する業務支援SaaSのソフトウェアエンジニア・リードエンジニア - 株式会社プレックス

インフラ領域で日本を動かす仕組みを作るスタートアップのエンジニア - 株式会社プレックス

オペレーションの効率化によって事業成長に貢献するコーポレートエンジニア - 株式会社プレックス

インフラ産業の人材課題を解決 | フロントエンドの技術を牽引するテックリード - 株式会社プレックス

弊社について

plex.co.jp

「ログ」から学んだ PostgreSQL のアーキテクチャの基本

こんにちは、Plex Job 開発チームの高岡です。

先日 PLEX TechCon 2025 が開催されました。
惜しくも登壇機会を得られなかったため、本記事にて発表する予定だった内容をまとめてみました。
来年こそは登壇を勝ち取ります 🔥

▼ 当日の様子はこちら
PLEX TechCon 2025 レポート - PLEX Product Team Blog

はじめに

以前のプロジェクトで、本番環境の一部の処理だけがやたらと遅くなるスロークエリに遭遇しました。
ボトルネックの特定と改善に取り組み、実行計画を確認したのですが、決定的な原因までは絞り込めていませんでした。 行き詰まってしまったため、切り口を変えて Cloud SQLPostgreSQL)のログから、スロークエリが発生している時点の DB の状態を見ることにしました。
ログには、パフォーマンスに関係しそうなメッセージがいくつか出力されており、1つずつ調査していく中で、後述する 3 つのログが特にパフォーマンスへの影響が大きそうだと分かってきました。
これらのログが PostgreSQL のどの仕組みと結びついているのかを把握するために、PostgreSQLアーキテクチャについて理解する必要がありました。

本記事では、当時実際に確認していた次の3種類のログメッセージを題材に、ログが出力されたときに PostgreSQL の内部で何が起きているのかを整理します。

  1. チェックポイント処理のログ
  2. 自動バキューム処理のログ
  3. TEMP 落ちのログ

また、特に「3.TEMP落ちのログ」からスロークエリのパフォーマンス改善につなげることができたので、その事例も合わせて紹介します。

ログと PostgreSQLアーキテクチャを手がかりに、結果として「実行計画だけを見ていたときには分からなかったボトルネック」を発見できた、という経験をもとにまとめています。
普段なんとなく PostgreSQL を使っているものの、内部構造はいまいちピンときていない……という方の、理解を一歩進めるきっかけになればうれしいです。

PostgreSQLの構成

まずはじめに、先ほど挙げた 3 つのログがそれぞれ PostgreSQL のどの構成要素と関係しているのかをイメージしやすくするために、全体の構成図を簡単に見ていきます。

出典 : PostgreSQLのアーキテクチャー概要|PostgreSQLインサイド : 富士通

プロセス構成(全体図の赤の要素)

PostgreSQLはマルチプロセス構成で、

  • ① リスナープロセス
  • ② サーバープロセス
  • ③ ワーカープロセス

の主要な3つのプロセスが存在します。
リスナープロセスは、PostgreSQL を制御するさまざまなプロセスをフォークして起動する親プロセスです。
サーバープロセスは、リスナープロセスの子プロセスとして、クライアントとのセッションごとに作成されます。
ワーカープロセスは、本記事でも説明する「チェックポインター」や「自動バキュームランチャー」などの PostgreSQL の重要な作業を担うプロセス群です。
MySQLでは、mysqldと呼ばれるプロセスの中にいくつかのスレッドを起動させるマルチスレッド構成にて動作しています。

メモリ構成(全体図の黄色の要素)

PostgreSQLのメモリは、PostgreSQL 全体が使用する領域で共有される共有メモリ域と、サーバープロセスごとに確保されるプロセスメモリ域の大きく2つに区別されます。
それぞれ以下のようなメモリの分類があります。

▼ 共有メモリ域

  • ④ 共有バッファー(shared_buffers)
    • テーブルやインデックスのデータをキャッシュする領域
  • トランザクションログバッファー(wal_buffers)

▼ プロセスメモリ域

  • ⑥ 作業メモリ(work_mem)
    • クエリ実行時に行われる、並び替えとハッシュテーブル操作のために使われる領域
  • ⑦ メンテナンス用作業メモリ(maintenance_work_mem)
    • データメンテナンスの操作で使用する領域
  • ⑧ 一時バッファ(temp_buffers)
    • サーバープロセスごとに作成される一時テーブルにアクセスするときに用いられる領域

MySQLでも名称は異なりますが、似たような用途のメモリ領域が用意されています。

今回見ていくログは、この構成の「どこで」「何を行っているか」をそれぞれ示すものになります。

調査した3つのログ

先ほど見た構成図と対応させながら、実際に Cloud SQL のログに出ていたメッセージを 3 種類取り上げていきます。
実際に次のようなメッセージが定期的に流れていました。

  • checkpoint starting / checkpoint complete
    • チェックポイント処理の開始と完了
  • automatic vacuum of table ...
    • autovacuum による VACUUM 実行
  • temporary file: path ...
    • 一時ファイルの作成(= TEMP 落ち)

どれも それ自体がDBサーバーの動作上の不具合 ではありません。
ただ、内容を理解しておくと、これらがどの構成要素で何をしているのかが分かり、ボトルネックとなっているクエリのパフォーマンスチューニングに活かせるようになります。

1. チェックポイント処理のログ📝

2025-07-07 18:20:47.160 UTC [31]: [5831-1] db=,user= LOG:  checkpoint starting: time
2025-07-07 18:20:47.509 UTC [31]: [5832-1] db=,user= LOG:  
checkpoint complete: wrote 5 buffers (0.0%); 0 WAL file(s) added, 0 removed, 0 recycled; 
write=0.318 s, sync=0.008 s, total=0.350 s; sync files=3, longest=0.005 s, average=0.003 s; 
distance=23 kB, estimate=363 kB; lsn=DE/2F0C6B78, redo lsn=DE/2F0C6B40

※ 数値は当時のログと異なります。

概要

PostgreSQL では、テーブルやインデックスを更新するとき、まずは共有メモリ域の共有バッファー上のページを書き換えつつ、更新内容だけを トランザクションログ(WAL)バッファーに順次書き込む構成になっています。 WAL(Write Ahead Logging)は、PostgreSQL の更新内容を順次記録していくログで、障害復旧やストリーミングレプリケーションで利用される非常に重要な情報です。 COMMIT 時にディスクへ書き出されるのは、基本的にはこの WAL だけであり、データファイル本体への書き込みはあとからまとめて行われます。

これにより更新処理自体は「メモリと WALへの追記」が中心になるため高速に処理できるようになります。 しかしそのまま放置すると、

  • 共有バッファー上に「ディスクへまだ書き出していない更新済みページ(ダーティページ)」が増えていく
  • 障害復旧に必要な WAL ファイルもどんどん溜まっていく

という問題が発生します。
そのために必要なのがチェックポイント処理で、一定間隔ごとに、

  • 共有バッファー上の更新済みページ(ダーティページ)をデータファイルへ書き出す
  • その時点の WAL 位置に「チェックポイントレコード」を記録し、それ以前の WAL セグメントを削除・再利用できる状態にする

という役割を担っています。 これを実際に行っているのは、前半の「PostgreSQLの構成」の章で触れた、ワーカープロセスの1つである「チェックポインタープロセス」が動作しています。

全体の構成とデータの流れのイメージ図は下記になります。

出典 : パフォーマンスチューニング9つの技 ~「書き」について~|PostgreSQLインサイド : 富士通

このような全体の仕組みによって、「データベースの更新処理の高速化」と「障害発生時のデータ保証」を両立して実現しています。

チェックポイントは、通常以下のような条件のいずれかを満たすと実行されます。

  • checkpoint_timeout で設定された時間が経過したとき
  • 直近のチェックポイントから生成された WAL の量が、max_wal_size に近づいたとき
  • 手動で CHECKPOINT コマンドを発行したとき、など

今回のログの checkpoint complete 行からは、少なくとも次のようなことが分かります。

  • wrote 5 buffers (0.0%) … チェックポイント中に 5 ページだけ書き出されている
  • write=0.318 s, sync=0.008 s, total=0.350 s … トータルで0.35 秒でチェックポイント処理を完了
  • distance=23 kB, estimate=363 kB … 前回チェックポイントから今回までに発生した WAL が 23kB 、見積もり値(estimate)も数百 kB

このログだけを見る限り、チェックポイントそのものは比較的軽く、ボトルネックにはなっていない状態だと読み取れます。

チューニングポイント

  • チェックポイント間隔を 短く すると
    • リカバリ時間は短くなる
    • その代わりチェックポイント処理(バッファ書き出し)の頻度が上がり、書き込み I/O のオーバーヘッドが増える
  • チェックポイント間隔を 長く すると
    • リカバリには時間がかかる
    • 代わりに I/O のオーバーヘッドは減り、平常時のスループットが上がる可能性がある

というトレードオフがあることを認識しておく必要があります。

チェックポイント処理を行う間隔を時間で指定するcheckpoint_timeout パラメータがあります。 このパラメータのデフォルト値は5分ですが、こちらは間隔が短くI/Oのオーバーヘッドが増えるため、PostgreSQL のチューニング記事などでは、30 分前後に調整していくケースがよく紹介されています。 もちろんアプリケーションの特性にもよるので、更新処理が多いアプリケーションの場合は、トランザクションログ量が多くなるため、checkpoint_timeout を長めに設定すると、その分リカバリー時間も増加する問題が発生します。

28.5. WALの設定

2. 自動バキューム処理のログ 🧹

2025-07-07 03:03:26.892 UTC [236223]: [1-1] db=,user= 
LOG:  automatic vacuum of table "postgres.public.filtered_job_salaries": index scans: 1
pages: 0 removed, 1169 remain, 1169 scanned (100.00% of total)
tuples: 48028 removed, 50752 remain, 0 are dead but not yet removable
removable cutoff: 95920778, which was 15 XIDs old when operation ended
new relfrozenxid: 95917748, which is 127978 XIDs ahead of previous value
new relminmxid: 83348, which is 1 MXIDs ahead of previous value
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
index scan needed: 601 pages from table (51.41% of total) had 50756 dead item identifiers removed
index "filtered_job_salaries_pkey": pages: 425 in total, 
139 newly deleted, 284 currently deleted, 145 reusable
....
I/O timings: read: 138.823 ms, write: 0.000 ms
avg read rate: 2.515 MB/s, avg write rate: 2.503 MB/s
buffer usage: 4975 hits, 208 misses, 207 dirtied
WAL usage: 3179 records, 205 full page images, 2084441 bytes
system usage: CPU: user: 0.13 s, system: 0.01 s, elapsed: 0.64 s

※ 数値は当時のログと異なります。

概要

PostgreSQL は MVCC(Multi-Version Concurrency Control)という方式で同時実行制御を行っています。
UPDATE / DELETE を実行すると「古い行」はすぐには物理削除されず、「新しいバージョン」が別の行として追加されます。 このため、何もしないとテーブルの中には「使用されない古い行」が溜まっていき、テーブルサイズの肥大(bloat)や統計情報の劣化を招きます。 これを定期的に掃除してくれるのが VACUUM であり、その自動実行を担うのが autovacuum の機構です。

autovacuumはワーカープロセスの1つである、「自動バキュームランチャープロセス」で制御/実行されます。

デフォルトでは、「総行数の 20% + 50 行」程度の更新があると autovacuum が走る、といった設定になっています(autovacuum_vacuum_scale_factor / autovacuum_vacuum_threshold などで調整可能)。

上記の autovacuum ログからは、テーブル "postgres.public.filtered_job_salaries" に対する自動 VACUUM の内容が記載されています。

  • tuples: 48028 removed, 50752 remain
    • 約 4.8 万行の dead tuple が物理的に削除
  • index "filtered_job_salaries_pkey": pages: 425 in total, 139 newly deleted, 284 currently deleted, 145 reusable
    • 主キーインデックスは 425 ページ分
    • 139 ページ分で今回新たに削除
    • 合計 284 ページが「削除済み」としてマーク
    • 145 ページは再利用可能な領域として扱える状態になっている
  • system usage: CPU: user: 0.13 s, system: 0.01 s, elapsed: 0.64 s
    • 所要時間は0.64 秒

といったことが分かります。

チューニングポイント

autovacuum のログ自体が出ていることは問題ありません。 一方で、以下のような場合はチューニングを検討する価値があります。

  • 特定の大きなテーブルに対して autovacuum が頻繁に走っている
    • autovacuum_vacuum_scale_factor / autovacuum_analyze_scale_factor をテーブルごとに調整し、
      • 更新が多いテーブルはしきい値を下げて「こまめに掃除」
      • 更新が少ない大きなテーブルはしきい値を上げて「頻度を下げる」
  • 1 回あたりの autovacuum が何十秒〜数分単位で長時間化している
    • テーブルの肥大化が進みすぎていないか確認
    • 必要に応じて VACUUM (FULL)REINDEX などのメンテナンスを検討

まずは「どのテーブルに」「どのくらいの時間」「どれくらいの頻度で」autovacuum が走っているかを可視化するところから始めると、チューニングすべきテーブルの当たりをつけやすくなるかと思います。

24.1. 定常的なバキューム作業

3. TEMP落ちのログ 💣

INFO 2025-07-07T06:02:38.305257Z 2025-07-07 06:02:38.304 UTC [2212337]: [5-1] 
db=XXX,user=XXX LOG: temporary file: 
path "base/pgsql_tmp/pgsql_tmp2212337.6", size 3427592

※ 数値は当時のログと異なります。

概要

PostgreSQLの構成」の章で説明したとおり、PostgreSQL のメモリ構成は大きく「共有メモリ域」と「プロセスメモリ域」に分けられます。
このうち 作業メモリ(work_mem は「プロセスメモリ域」の 1 つで、各クライアントセッションごとに確保され、主に次のような操作で使用されます。

  • ソート操作:ORDER BYDISTINCT、マージ結合
  • ハッシュテーブル:ハッシュ結合、ハッシュ集約、メモ化(memoize)ノード、IN 副問い合わせのハッシュ処理

これらの処理が work_mem の上限を超えるデータ量 を扱うと、その処理単位ごとに一時ファイルが作成され、メモリ内ではなくディスク上でソートやハッシュが行われます。
この「メモリからディスクにあふれて一時ファイルに逃がす」状態を「TEMP落ち」と呼びます。 ディスク I/O はメモリアクセスに比べてはるかに遅いため、TEMP落ちが多発すると、クエリ全体のレスポンスが大きく低下します。

上記のログメッセージからは、少なくとも 約 3.3 MB の一時ファイルが作成された ことが分かります。
1 回あたりのサイズとしてはそこまで大きくはありませんが、同様のログが大量に出ている場合は、ソート/ハッシュの対象になっているデータ量が work_mem に対して多すぎる可能性を疑うきっかけになります。

チューニングポイント

TEMP落ちを抑えるための基本的なアプローチは、work_mem の上限を調整することです。 ただし、work_mem の値を大きくしすぎると、OS 側で利用できるメモリが減り、その結果としてシステム全体の遅延やメモリ不足(OOM)の発生につながるリスクがあります。

ざっくりとした上限の目安としては、

  • (物理メモリ - shared_buffersの値) / max_connectionsの値

で算出された値です。 また PGTune の設定値を算出してくれるツールを使用して上限値の目安を把握することを推奨します。

実際には、これらの算出した上限を参考にしつつ、

  • グローバルな work_mem を少しずつ上げてみる
  • 特定の重い処理だけ、トランザクション内で SET LOCAL work_mem = '64MB'; のように一時的に増やす

といった運用で調整するのが安全かと思います。

19.4. 資源の消費

work_memを増強してみた

Railsによる実装

今回はソート処理を行なっていたスロークエリがあったので、work_mem を増加させる検証を行いました。 work_mem の増加方法は、対象の重いクエリだけ、トランザクション内での設定値を一時的に上げる方針としました。

まず初めに現在の PostgreSQL の設定値を確認し、

  • メモリ: 16GB(16,384 MiB)
  • work_mem : 4MB
  • max_connections:500
  • shared_buffers:5,460 MB(5,332 MiB)

上記の設定値からざっくり許容できる work_mem の値を計算します。

(物理メモリ - shared_buffersの値) /  max_connectionsの値

(16,384 MiB − 5,332 MiB)/ 500 ≒ 22.1 MiB

この例では、1 セッションあたり 20MB 程度の上限になります。 そして、実際にRuby on Rails で対象のクエリだけトランザクション内で20MBに増加させてみました。

ActiveRecord::Base.transaction do
  ActiveRecord::Base.connection.execute("SET LOCAL work_mem = '20MB'")
  Model.heavy_query
end

この結果、一部のクエリで発生していた TEMP落ちのログが消え、全体で数分かかっていたクエリが 数十秒前後に収まるようになりました 🎉

実行計画で見てみる

簡単なSQLで実際にどのような実行計画の違いが出るのかを見てみます。
実行するSQLは下記になります。

EXPLAIN
ANALYZE
SELECT
    *
FROM
    table_1
    JOIN table_2 ON table_1.id = table_2.id
ORDER BY
    table_1.id

まずは、デフォルト値の work_mem (4MB)のまま実行します。 結果は下記のようなプランになりました。

QUERY PLAN
Gather Merge  (cost=31095.13..40212.57 rows=78144 width=745) (actual time=441.110..721.817 rows=70571 loops=1)
  Workers Planned: 2
  Workers Launched: 2
  ->  Sort  (cost=30095.11..30192.79 rows=39072 width=745) (actual time=417.967..433.232 rows=23524 loops=3)
        Sort Key: table_1.id
        Sort Method: external merge  Disk: 18776kB 👈👈 注目すべき箇所 
        Worker 0:  Sort Method: external merge  Disk: 15096kB 
        Worker 1:  Sort Method: external merge  Disk: 12280kB
        ->  Parallel Hash Join  (cost=1790.14..14158.12 rows=39072 width=745) (actual time=44.093..279.160 rows=23524 loops=3)
              Hash Cond: ((table_2.id)::text = (table_1.id)::text)
              ->  Parallel Seq Scan on table_2  (cost=0.00..12180.64 rows=71364 width=543) (actual time=0.045..154.770 rows=57091 loops=3)
              ->  Parallel Hash  (cost=1405.06..1405.06 rows=30806 width=202) (actual time=43.465..43.467 rows=17457 loops=3)
                    Buckets: 65536  Batches: 1  Memory Usage: 9408kB
                    ->  Parallel Seq Scan on table_1  (cost=0.00..1405.06 rows=30806 width=202) (actual time=0.022..15.490 rows=17457 loops=3)
Planning Time: 0.521 ms
Execution Time: 729.775 ms

注目すべき箇所は、「Sort Method: external merge Disk: 18776kB」 です。 この「Disk: 18776kB」が、約 18MB 分がメモリからディスクにあふれている(= TEMP落ちしている) ことを表しています。

次に、work_mem を増加させて再度実行計画を取得します。
Rails の実装と同じく、トランザクション内で一時的に work_mem を上げます。

BEGIN;
SET LOCAL work_mem = '20MB';

EXPLAIN
ANALYZE
SELECT
    *
FROM
    table_1
    JOIN table_2 ON table_1.id = table_2.id
ORDER BY
    table_1.id

COMMIT;

結果は下記のようなプランになりました。

QUERY PLAN
Gather Merge  (cost=31095.13..40212.57 rows=78144 width=745) (actual time=410.376..494.698 rows=70571 loops=1)
  Workers Planned: 2
  Workers Launched: 2
  ->  Sort  (cost=30095.11..30192.79 rows=39072 width=745) (actual time=388.172..398.945 rows=23524 loops=3)
        Sort Key: table_1.id
        Sort Method: quicksort  Memory: 17883kB 👈👈 注目すべき箇所 
        Worker 0:  Sort Method: quicksort  Memory: 13548kB
        Worker 1:  Sort Method: quicksort  Memory: 17818kB
        ->  Parallel Hash Join  (cost=1790.14..14158.12 rows=39072 width=745) (actual time=47.379..289.016 rows=23524 loops=3)
              Hash Cond: ((table_2.id)::text = (table_1.id)::text)
              ->  Parallel Seq Scan on table_2  (cost=0.00..12180.64 rows=71364 width=543) (actual time=0.316..140.737 rows=57091 loops=3)
              ->  Parallel Hash  (cost=1405.06..1405.06 rows=30806 width=202) (actual time=46.256..46.258 rows=17457 loops=3)
                    Buckets: 65536  Batches: 1  Memory Usage: 9408kB
                    ->  Parallel Seq Scan on table_1  (cost=0.00..1405.06 rows=30806 width=202) (actual time=0.032..15.850 rows=17457 loops=3)
Planning Time: 0.487 ms
Execution Time: 498.724 ms

今度は 「Sort Method: quicksort Memory: 17883kB」 となっており、ソート処理がディスクではなくメモリ上だけで完結していることが分かります。

もちろん work_mem を無闇に上げると、「3. TEMP落ちのログ 💣」の章で記載したとおり、

OS 側で利用できるメモリが減り、その結果としてシステム全体の遅延やメモリ不足(OOM)の発生につながるリスク

があるので、

  1. まずはクエリ自体の見直し(不要なソート・巨大な IN・複雑なJOIN など)
  2. それでも厳しいところにだけ、SET LOCAL work_mem をピンポイントで適用

といった順番で検討するのが安全だと思います。

まとめ

本記事では、

  • チェックポイント処理のログ
  • 自動バキューム処理のログ
  • TEMP落ちのログ

の3つのログを入り口に、PostgreSQLアーキテクチャの基本を整理して見ました。
PostgreSQL には他にもたくさんのパラメータがあり、他の RDB製品(MySQL / SQL Server / Oracle など)と比較しながら読むと、設計思想の違いが見えてきて面白いです。

来年の TechCon では、さらに PostgreSQL の内部構造を深掘り、理解しているからこそわかるチューニング事例を紹介したいと思います 🔥

参考

19.4. 資源の消費
24.1. 定常的なバキューム作業
28.5. WALの設定
PostgreSQLのアーキテクチャー概要|PostgreSQLインサイド : 富士通
パフォーマンスチューニング9つの技 ~「書き」について~|PostgreSQLインサイド : 富士通
パフォーマンスチューニング9つの技 ~「基盤」について~|PostgreSQLインサイド : 富士通
[改訂3版]内部構造から学ぶPostgreSQL―設計・運用計画の鉄則 (Software Design plus) | 上原 一樹, 勝俣 智成, 佐伯 昌樹, 原田 登志 |本 | 通販 | Amazon

PLEX TechCon 2025 レポート

こんにちは。今回は11月7日に開催されたPLEX TechConについて、入社4ヶ月の開発部コーポレートチームの岡田がレポートさせていただきたいと思います。

まず、PLEX TechCon(プレックス テックコン)について軽く説明させていただくと、去年から年1の頻度で開催している株式会社プレックスのエンジニア全員集合の技術カンファレンスです。参加者は雇用形態に関わらず、プレックスで働いたことがある人限定のクローズドなイベントです。

なぜ TechCon を開催するのか?

プレックスではエンジニア組織が拡大しており、隣の人や部署が日々どのような業務に向き合っているかを知る機会は決して多くありません。そこで、お互いの事業や組織でどんな取り組みをしているかを知るための相互理解の場として、また、発表テーマを幅広く設定することで、TSKaigi や Kaigi on Rails に登壇するメンバーが出てきたように登壇練習の機会としても活用してほしいという思いから開催されました。

product.plex.co.jp

product.plex.co.jp

具体的な業務内容には触れられませんが、各チームが直面した課題や苦労、技術的な議論の雰囲気をお伝えできればと思います。

タイムテーブル

チーム 発表者 タイトル
14:00 - 14:25 プレックスジョブ 池川 倍倍NESTED!
14:25 - 14:50 プレックスジョブ 栃川 私の個人生産性向上プロジェクト~序章~
14:50 - 15:15 プレックスジョブ 小松 運用まで行くぜ!AIがE2Eテストを代行する時代はすでにやってきている?!
15:15 - 15:40 サクミル 毛利 フロントエンド データモデリング -クソデカ画面でデータを扱う流儀-
15:40 - 16:05 サクミル 荒木 0→1から1→10へ:ユーザー行動データから"使われ方"を可視化する
16:05 - 16:20 休憩
16:20 - 16:45 コーポレート yumazak コーポレートチームの AI アプリケーション開発への取り組み
16:45 - 17:10 コーポレート 金山 SAML認証の仕組みと p-search への導入事例
17:10 - 17:35 コーポレート 宮森 気づける安心、直せる体制。Sentry × Kintoneで広がるエラーモニタリング
17:35 - 18:00 コーポレート 石塚 Claude Codeによる開発効率化
19:00 - 21:00 懇親会 19:00開始

TechCon開催の様子

前半(プレックスジョブ、サクミルチーム)

倍倍NESTED!

最初の発表は、プレックスジョブチーム池川さんによる、クエリのパフォーマンスチューニングについての発表です!

池川さん発表

事の発端として、とあるスカウト対象者のユーザーを抽出する処理が45秒のリクエストタイムアウトに引っかかっておりエラーアラートが上がっていました。

クエリの内容としては10以上ある複雑な条件のクエリで、実際に該当クエリを実行したところ約1分30秒かかる事が判明。実行計画を確認すると、Nested Loopの中にさらにNested Loopがネストしており、まさに「倍倍NESTED!」状態というところから話が始まりました。

実行計画を確認すると「7.2千件 × 13.3万件 = 約9億6千万件」という膨大な「Rows Removed by Join Filter」が発生しており調査の結果、原因は複数の要素にありました。

  • 実行計画の見積もりと実際の行数に乖離があるにも関わらずNested Loopが採用され、大量のNested Loopが発生していた。
  • CTEの元テーブルにはインデックスが貼られていたが、MATERIALIZEDされることで元のインデックスが引き継がれていなかった。

これら二重の落とし穴が引き金となってスロークエリが発生していましたが、最終的にはJOINを使う形に改修することで、1分半かかっていたクエリを数秒まで改善できたというお話でした。

Nested Loopとは

SQLの結合アルゴリズムに「Nested Loop」「Merge」「Hash」の3種類があることや、CTEがMATERIALIZEDされるかどうかで挙動が大きく変わることなど、非常に学びの多い発表で大変勉強になりました!

今回初めてCANDY TUNEの「倍倍FIGHT!」という曲を知ったのですが、池川さんのスライドサムネが「倍倍NESTED!」になっているので、どうやってサムネを作ったんだろうと気になっています

私の個人生産性向上プロジェクト~序章~

同じくプレックスジョブチームの栃川さんからは、生産性向上についての発表です。

栃川さん発表

開発時の姿勢から整理整頓、便利ツール、ショートカットまで多岐にわたる内容でとても面白かったです!

よく開きっぱなしにしがちなブラウザのタブを自動で削除できる「Tab Wrangler」のようなツールや、「Shift + Command + (N or G or S)」でNotion、Google、Slackを開けるショートカットコマンドは、早速試してみたいと思いました。

開発時の姿勢について

私も普段、知っているコマンドだけで作業しがちなので、常にショートカットを意識していきたいです!

運用まで行くぜ!AIがE2Eテストを代行する時代はすでにやってきている?!

続いてプレックスジョブチームの小松さんの発表です。

小松さんの発表

前回はDevinを使ったE2Eテストに挑戦したものの、燃費が悪く運用には至らなかったというお話でした

前回の話

product.plex.co.jp

今回はその続きで、固有のエージェントに囚われず「テスト仕様書」を資産として活用し、AIによるE2Eテストの代行と運用を目指すといった内容です!

PJ(プレックスジョブ)チームでは、売上に直結する機能のリグレッションテストやシナリオがあり、繰り返し実行する必要があるものの、運用が形骸化しがちという課題がありました。これを解消するためにE2Eテストを自動化したいというのが今回の事の背景となります。

話の内容としてはまず前回Devinに作成させたテストガイドやテスト仕様書を再度メンテナンス。WEBページの操作にChromeDevToolsを使用させ、テスト仕様書を基にテストを実施。Claude Code(Claude Sonnet 4)、Codex(GPT-5-codex)、Copilot(Claude Sonnet 4)の各エージェントの違いを比較してみたといった内容です。

結論として「誰かができたことは誰でもできる。誰かが躓いたところはみんな躓く」ということで、概ねAIエージェント間のレベル差はなくなっているようです!

もろもろの所感

さらに複雑性の高いテストを実施したところ、ChromeDevToolsからは操作できないUI要素の存在や、ローカルマシンのリソースに依存するため並列でのテストの難しさといった課題があるものの、ローカルAIエージェントを使ったE2Eテストは現実的な段階まで来ているとのことで、自分も簡単なテスト仕様書を作ってみてトライしてみたいです!

フロントエンド データモデリング -クソデカ画面でデータを扱う流儀-

続いてサクミルのtettyさんによる、フロントエンドのパフォーマンスチューニングについての発表です。

tettyさんの発表

サクミルについて

皆さん「トリレンマ」をご存知でしょうか?三つのうちどれか一つを選ばなければならない状況を指すそうなのですが、「パフォーマンス」「拡張性」「可読性」の3つを同時に改善するのは難しいですよね?そんな「トリレンマ」を改善したお話です。

プレックスでは建設業界向けの現場管理ツールとして「サクミル」というサービスを提供しています。直近、サクミルではこれ一つでサービスが完結するほどの規模感の見積もり機能をリリースしました。

しばらくして、ユーザーさんから「見積もり作成時に項目が増えると重くなる」「見積もり入力が重くなった場合の改善策はありますか?」といった問い合わせが来るようになりました。そこから、計測、分析、データモデリング、改善までの一連の流れが紹介されました。

まずは計測

Chrome DevtoolのPerformanceタブを使って、JSの処理(Scripting)が原因か、DOMの描画(Rendering)が原因かのアタリの付け方や、Performance APIを使って各処理の実行時間を計測するなど、まさにお手本のような調査手順でした!

根本的な原因として、見積もり項目や明細のデータの持ち方が階層構造のままであったため、親要素や子要素が変更されるたびに再描画・再マウントが発生していた点や、見積もり項目の計算効率や探索方法が非効率だった点にありました。

そこで改めてデータモデリングを行い、フラットなデータ構造に変える事で、階層構造時に存在したUIとロジックの共存によって引き起こされていた不要な計算処理によるパフォーマンス悪化や、コンポーネントのmemo化による拡張性低下の問題を無事解決したという話です。

DOM構造Before・After

実際に開発・リリースしてみると、想定外の使い方をされていたり、想定以上のデータが登録されていたりすることがあるので、私も常に「パフォーマンス」「拡張性」「可読性」の3つを意識して実装していきたいなと感じました!

0→1から1→10へ:ユーザー行動データから"使われ方"を可視化する

サクミルチームからはもう一人、荒木さんからサービスリリース後の「1→10」フェーズにおけるデータ活用の取り組みについての発表です。

荒木さんの発表

Firebase AnalyticsやBigQuery、dbtといったツールを活用し、イベントごとのタイムスタンプを計測することで、「誰が」「どのページを」「どれくらいの時間見ていたか」をデータとして割り出し、ページ導線の最適化や離脱パターンの把握を通じてKPI改善に繋げたり、オンボーディング完了率の改善に繋げたという内容です。

Firebase Analyticsを使ったイベント送信

それに至るまでの地道な計測や集計と計算が必要であったり、データをただ計測するだけでなく、それをいかに事業改善に「活用」するかという一連の流れがよく分かる、非常に学びになる発表でした!

ゲスト

休憩時間には、ゲストとして駆けつけてくださった元インターン生の3名に軽く自己紹介をしていただきました。

現プレックスの主要メンバーの元同期の方や、CEOの黒崎さんとシェアハウスをしていた時代を知るOBの方など、豪華なメンバーから貴重なお話を伺うことができました。鈴木さん、豊田さん、内藤さん、お忙しい中お越しいただきありがとうございました。

ゲスト

休憩の様子①

休憩の様子②

後半(コーポレート、マーケティングチーム)

コーポレートチームの AI アプリケーション開発への取り組み

コーポレートチームのyumazakさんからは、実際の業務へのAI活用に関する取り組みの発表です。具体的な事例として

  • ①フォーマットがバラバラな求人情報を構造化して保存
  • ②架電内容(音声ファイル)を要約する
  • ③要約した架電を元にメールを作成

といった3つの具体例を元に、どのようにAIを取り入れていったかが紹介されました。

yumazakさんの発表

当初は多機能なAIアプリケーションフレームワーク「Mastra」を利用して開発していましたが、様々なAI活用に取り組む中で、最終的に必要だったのは出力の構造を定義できるStructured Outputs機能でした。

これはMastraを使わずともどのAIモデルも対応しており、シンプルなOpenAI Agents SDKだけでも必要な要件を満たせるというお話でした。

AI活用の仕組みとしてはシンプル

私もMastraについては聞いたことがあり気になっていましたが、今回の発表で具体的にどのような使い方ができ、何が実現できるのかを知ることができ、非常に学びになりました!

その他にも、AI導入におけるコスト最適化の難しさ(安いモデルで5割の精度を目指すか、高いモデルで8割の精度を目指すか)や、ロジックを含めたテスト、不確実性のある出力データをシステムにどう組み込むかといったリアルな課題を聞けて良かったです!

SAML認証の仕組みと p-search への導入事例

続いてコーポレートチームの金山さんからは、社内システムへのSAML認証導入についての発表です。

金山さんの発表

現在プレックスでは、自社で開発している「p-search」というM&Aに関する社内システムが存在します。M&Aシステムであるため機密情報が多く、従来のID・パスワード認証ではID/PWさえ知っていればどこからでも誰でもログインできてしまうというセキュリティ上の課題がありました。

そこで、端末認証や社内からのアクセス制限が可能なCloudGateというSSOプラットフォームを導入し、セキュリティを強化したという内容です。

SAML認証が使われているサービス例

SAML認証の基本的な仕組みや、元々利用していたFirebase Authenticationと連携することで意外と簡単に導入できた点など、SAML認証は敷居が高いイメージがありましたが、今回のスライドで認証周りを触れることへのハードルが一段下がりました!

SAML認証の課題と解決策

金山さんのスライドは、デザイナーである奥様も手伝ってくださったそうで、非常に読みやすくまとまっていました。デザインの力はやはりすごいですね。

気づける安心、直せる体制。Sentry × Kintoneで広がるエラーモニタリング

続いてコーポレートチームの宮森さんからはSentry導入に関する発表です。

宮森さんの発表

なぜkintoneエラーモニタリングが必要か?

プレックスではKintoneアプリを使った業務フローが数多く存在し、現状は問い合わせベースで対応しているという課題がありました。そんな「見えないエラー」をいかに検知し、修正・改善できる体制を整えるかという課題に取り組んだ内容です。

実際にSentryを導入してみると、Kintoneアプリ以外のGoogle APIライブラリなどのエラーも検知してしまったり、無料枠のイベントが5,000件であるため、いかにサンプリングやフィルタ設計を行うかといった具体的な話も聞けました。

現在私が関わっているp-searchにもぜひ導入してみたいと思いました!

Claude Codeによる開発効率化

最後の発表はコーポレートチームの石塚さんによる、Claude Codeを活用した開発効率化に関する取り組みです。

石塚さん

話の内容はあまり細かくお伝えする事はできませんが、Claude.mdの作成から、ガイドラインやドキュメントの整備、Claude Codeのカスタムコマンドを取り入れることで大幅に開発効率をUPさせることに成功したというお話でした。

AIを活用した効率化に取り組んでみて

特に、定型作業のためのプロンプトやPull Requestを集めたナレッジ集を作成することで開発効率をさらにUPさせたりと、すぐにでも真似できる実践的なアイデアが多く、非常に参考になりました!

懇親会

カンファレンスの後は、神田のおしゃれなバルで懇親会です

tabelog.com

普段なかなか話す機会のない他チームのメンバーと、それぞれのサービスならではの苦労話や面白いエピソードを聞けて楽しかったです!

最後に、本イベントを企画してくださった石塚さん、懇親会の幹事を務めてくださった高岡さん、そしてこの日のためにスライドを作成して発表してくださった皆さん、本当にありがとうございました。

delayed_jobが遅延する問題を解決しました

こんにちは、Plex Job開発チームの種井です。

Plex Jobでは、バックエンドシステム(Railsアプリケーション)での非同期処理にdelayed_jobを使用しています。 運用する中で、とあるジョブが数時間遅れて実行されてしまう問題が発生しました。調査したところdelayed_jobraise_signal_exceptionsの設定値に原因がありました。

今回は、delayed_jobraise_signal_exceptionsパラメーターの設定が非同期処理の遅延につながってしまう事象と、対応した内容について紹介します。

前提および今回の問題について

前提

問題発生時のPlex Jobのバックエンドシステムの構成は以下のような形となります。

簡略版構成図

また、ジョブについては再試行されても問題ないように冪等性が担保されていることを前提とします。

今回の問題について

delayed_jobが受け取った一部のジョブについて、処理が完了するまでに数時間かかっていました。

今回発生した問題①

時間をかけても処理さえ完了すればよいものについては問題ないかもしれません。しかし、アプリケーションで発生したイベントに応じて通知を送信するような処理など、ユーザー体験やサービス上の機会損失を生むような処理の遅延は避けたいです。

調査と仮説

今回、登録されたジョブの大半が遅延しているわけではなく、ごく一部について遅延することが厄介な点でした。

まずは、ログからわかることがないか調査してみることにしました。 数時間遅延したジョブの実行ログを確認したところ、ジョブの実行中にWorkerプロセスがSIGTERMシグナルを受け取った後に終了していることが分かりました。 また、そのちょうど4時間後の再実行により成功しているようです。

※ 該当ログのローデータが残っていなかったので、以下はイメージとなります。

Oct 27 10:01:02.643 Performing SampleJob
...
Oct 27 10:02:02.643 SampleJob SignalException: SIGTERM (SignalException)
...
Oct 27 14:02:02.643 Performing SampleJob
...
Oct 27 14:03:02.643 Performed SampleJob

同じ現象が発生した他のログを確認してみたところ、いずれも同じ挙動でジョブが実行されていました。

今回発生した問題②

どうやらWorkerプロセスがジョブの処理中に終了すると、ジョブの遅延が発生するようです。

なぜWorkerプロセスにSIGTERMシグナルが送信されているのか?

Plex Jobでは、Railsアプリケーションのインフラ環境にk8s (GKE)を使用しています。 k8sは、Nodeに配置されるリソースの最適化や可用性を担保する機能が組み込まれている関係で、Podを強制的に終了させることがあります。 詳しい仕組みについては、以下の記事を参照いただくことにして、ここでは割愛します。

Podの終了にあたっては、アプリケーションに停止までの猶予を与えるGraceful Shutdownという仕組みが使われており、Podを終了する際は以下のような順番でシグナルを送信するような仕組みになっています。

Graceful Shutdown

参考: Pod終了時のライフサイクル

今回はこのGraceful Shutdownの停止サイクルにおいて、タイムアウト判定となった場合の挙動であると仮定し、実際に再現してみることにしました。

挙動の検証

Workerプロセスの終了時の挙動について、実際に再現してみます。

適当なジョブを定義しましょう。app/jobs/sample_job.rbを作成します。

class SampleJob < ApplicationJob
  def perform
    sleep 300 # ジョブをしばらく動かしておく
  end
end

delayed_jobの設定値は、今回問題が発生したアプリケーションの設定と同じにします。

config/initializers/delayed_job.rb

Delayed::Worker.destroy_failed_jobs = false
Delayed::Worker.max_attempts = 3

Workerプロセスを起動します。

$ rails jobs:work

ジョブを作成します。

$ rails c

> SampleJob.perform_later

しばらくジョブは内部でSleep処理しているため、その間にk8sのPod終了時の挙動と同じく、WorkerプロセスにSIGTERMを送信した後で、SIGKILLシグナルを送信してみます。

$ kill -15 WorkerのプロセスID
$ kill -9 WorkerのプロセスID

Workerプロセスはkillされました。ジョブキューがどのような状態になっているかを確認しましょう。 delayed_jobsテーブルの中身を確認します。

| id | priority | attempts | handler | last_error | run_at | locked_at | failed_at | locked_by | queue | created_at | updated_at |
|----|-----------|-----------|----------|-------------|---------------------|---------------------|-----------|---------------------------|---------|---------------------|---------------------|
| 6 | 0 | 0 | `--- !ruby/object:ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper¶job_data:¶  job_class: SampleJob¶  job_id: 0f01af35-1341-4165-b100-b37ed777e397¶  provider_job_id:¶  queue_name: default¶  priority:¶  arguments: []¶  executions: 0¶  exception_execu` |  | 2025-10-27 05:32:06.656 | 2025-10-27 05:32:08.964 |  | host:b4b6d783ad99 pid:29768 | default | 2025-10-27 05:32:06.656 | 2025-10-27 05:32:06.656 |

すると、locked_atlocked_byにロック日時とロックしたプロセス情報が入ったまま、キューに残り続けています。 再度Workerプロセスを起動してみます。

$ rails jobs:work

別のWorkerプロセスを起動しても、キューの中のジョブは実行されません。

今回のようにWorkerプロセスが強制終了されると、locked_atlocked_byが残り、その他のWorkerプロセスはロックが解除されるまで対象のジョブを取得できません。ロックはDelayed::Worker.max_run_time(デフォルト: 4時間)を過ぎると無効化され、別のWorkerがジョブを取得して実行する可能性があります。

delayed_jobのREADME.mdより

Otherwise the lock on the job would expire and another worker would start the working on the in progress job.

ロック中のジョブは他のWorkerから実行されない。

The default Worker.max_run_time is 4.hours. If your job takes longer than that, another computer could pick it up. It's up to you to make sure your job doesn't exceed this time. You should set this to the longest time you think the job could take.

ジョブの最大実行時間は4時間で、それを超える場合は他のWorkerプロセスからジョブが実行される。

上記をまとめると以下のような流れとなります。

delayed_jobの挙動

対応

問題となっている現象を再現できたので、対応していきます。

ジョブの処理途中でWorkerプロセスが終了した場合はDelayed::Worker.max_run_timeを待たずに、再実行されてほしいです。 delayed_jobには、raise_signal_exceptionsというパラメーターが用意されています。SIGKILLは捕捉できないため、終了までの猶予を示すSIGTERMイベントを捕捉するようになっています。 こちらを有効化することでSIGTERMを受け取った場合に例外を発生させることができます。

  • false: デフォルト。SIGTERMを受け取っても例外を発生させない
  • term: SIGTERMを受け取ると例外を発生させる
  • true: SIGTERM, SIGINTを受け取った場合に例外を発生させる
Delayed::Worker.raise_signal_exceptions = :term

例外が発生すると、ジョブは失敗扱いとなり再試行の対象となります。

さきほどの挙動の検証のコードに設定を加えて確認してみましょう。

config/initializers/delayed_job.rb

Delayed::Worker.destroy_failed_jobs = false
Delayed::Worker.max_attempts = 3
Delayed::Worker.raise_signal_exceptions = :term

同じ手順で、SIGTERMSIGKILLシグナルを送信してプロセスを終了させ、Workerプロセスを再度立ち上げます。 しばらく待つと、ジョブが処理されていました。

| id | priority | attempts | handler | last_error | run_at | locked_at | failed_at | locked_by | queue | created_at | updated_at |
|----|-----------|-----------|----------|-------------|---------------------|---------------------|-----------|---------------------------|---------|---------------------|---------------------|

補足

delayed_jobにはいくつかパラメーターの設定方法があり、config/initializers/delayed_job.rbイニシャライザへ定義もその一つとして、アプリケーション全体で共通のパラメーターを指定できます。 README.mdではそれ以外にも

  • 環境変数の指定
  • ジョブオブジェクトでの属性のoverride

が紹介されています。

例: 起動コマンド(rails jobs:work)の引数

対象のWorkerプロセス毎にパラメーターを指定できる

$ QUEUE=tracking rails jobs:work

例: ジョブオブジェクトでの属性のoverride

ジョブ毎にパラメーターを指定できる

NewsletterJob = Struct.new(:text, :emails) do
  def perform
    emails.each { |e| NewsletterMailer.deliver_text_to_email(text, e) }
  end

  def max_attempts
    3
  end
end

当初は設定の変更による影響範囲を絞るため「ジョブオブジェクトでの属性のoverride」する方法を検討しましたが、挙動についても検証済であったため、config/initializers/delayed_job.rbに設定を追加する方法を採用しました。

ということで、Delayed::Worker.raise_signal_exceptions = :termを設定した結果、今回の非同期処理が数時間遅延する問題を解消することができました。

まとめ | アプリケーションの終了フローを制御するということ

ここまで、delayed_jobを使った非同期処理が遅延する問題について、調査・対応までの流れを紹介してきました。

途中k8sdelayed_jobの機能について触れましたが、今回の問題はGraceful Shutdownという仕組みを理解せずに、Workerプロセスを扱っていたことに根本的な原因がありました。 delayed_jobにはraise_signal_exceptionsというオプションが用意されています。前提知識があれば適切に設定を行い、今回の問題に至ることもありませんでした。

k8sやPaaSなどのプラットフォームでは、リソースの最適化や可用性の担保を目的としたプロセスの再起動が実施されることが多いため、アプリケーションはプロセスの終了に対しての振る舞いについて予め想定しておく必要があります。 今回、delayed_jobのパラメーターの設定を一つ見直すだけではありましたが、調査や検証を通じてGraceful Shutdownをはじめとしたプロセスのライフサイクルを理解した上で、システム設計を行うことの重要性に気付かされる良い機会でした。

エンジニア募集

100兆円規模のインフラ産業の課題解決に挑戦|業務支援SaaSのテックリード - 株式会社プレックス

急成長する業務支援SaaSのソフトウェアエンジニア・リードエンジニア - 株式会社プレックス

インフラ領域で日本を動かす仕組みを作るスタートアップのエンジニア - 株式会社プレックス

オペレーションの効率化によって事業成長に貢献するコーポレートエンジニア - 株式会社プレックス

インフラ産業の人材課題を解決 | フロントエンドの技術を牽引するテックリード - 株式会社プレックス

弊社について

plex.co.jp

プロポーザルのコツ ~ Kaigi on Rails 2025 初参加で3名の登壇を実現 ~

SaaS事業 テックリードの石見です。

Kaigi on Rails 2025で登壇してきました。弊社は今年が初参加にもかかわらず3名が登壇の機会をいただきました。

product.plex.co.jp

社内ではプロポーザルの添削会を行いました。その際に話したプロポーザルのコツをまとめます。

プロポーザルは企画書

プロポーザルは話したいことを書くのではありません。コンテキストを共有していない選考者に1枠を使うだけの意義(コスパ)を説明する企画を書きます。

  • なぜこの内容なのか?
  • なぜ今なのか?
  • どのような成果を想定しているか?

自分が話す意味や自分へのリターンは重視してはいけません。ただ、内容と事実がカンファレンスやコミュニティに対してどのようなメリットがあるか、それだけを考えればよいです。
有料のカンファレンスに登壇する。この時点で我々はプロです。

主張は1つ、情報は削ぎ落とす

ソフトウェア開発と同じで、関心事に集中できるように主張は1つにした方が良いです。

伝えられることは限られます。また、聴講者は当日は多くの話をずっっっと聞かされます。脳みそは疲れている前提で考えましょう。

タイトルと最後の着地が同じであるように、思考と主張の視点は固定であるように、意識しましょう。

概要(Abstract)で勝て

選考者は多くのプロポーザルを見ます。似た内容も多いでしょう。脳みそは疲れている前提で考えましょう。

概要(Abstract)で興味を得て、詳細(Details)で選ばれようとしないでください。概要(Abstract)で選ばれないと駄目です。

概要(Abstract)で選ばれてスタートラインで、詳細(Details)をしっかり見てもらえます。

概要(Abstract)で選ばれて、詳細(Details)で確実にしてください。

大事な3要素

Kaigi on Railsのような特定の技術のコミュニティでは、次の3つが大事だと考えています。

  • あなたが対象だ!
  • これは我々の敵だ!
  • プレゼントがあるぞ!

自分事になってもらうこと、課題が一般的であること、それの対応策などの利を提供すること。
だから、お金を払って聞く価値がある。インターネットに残す価値がある。

先人の成功を参考にする

多くの方が記事を書いてくれています。参考にしましょう。

自分と似た領域や内容の登壇が過去にあるか、動機づけや構成はどうか、調べてみましょう。

実際に提出したプロポーザル

 ### Title
 
 「技術負債にならない・間違えない」権限管理の設計と実装
 
 ### 概要
 
 Webサービスの開発と運用に関わる方なら、権限管理の重要性については深く認識していることでしょう。運営アカウント、役割などの条件によって表示や操作を変えたい際に必要となります。権限は非常に繊細であり、1つの実装ミスがサービスや事業の大きな損失に繋がります。
 
 しかし、条件をそのまま表現した`admin?`のような簡易的な分岐による実装が多く見られます。そして、成長と共に`admin?``manager?`など権限の種類が増え、条件が複雑になり「レビュワーも条件を把握しきれない。」「実装を触るのが怖い。」「不具合の温床だ。」と技術負債になっているケースを多く目にしてきました。
 
 このセッションでは、まず「なぜ権限管理は複雑になり、技術負債を生むのか」という根本原因を紐解きます。そして、その失敗から学んだ「実装・利用・理解、その全てで**間違えない**」というたった一つの原則に光を当てます。
 
 私が辿り着いたのは、Rails Wayに沿ってModelとPolicyを1対1で対応させ「**更に1つ軸を設ける**」アプローチです。この設計によって、コードは驚くほど見通しが良くなり、エンジニア以外も誰が何をできるのかを正確に理解できるようになりました。
 
 `admin?`を超えた先にある、明日からあなたのチームで実践できる「技術負債にならない・間違えない」権限管理の設計と実装。その全てをお伝えします。
 
 ### 詳細
 
 この発表では、以下に該当する聞き手に対して、権限管理の設計と実装に関する具体的な解決策を提供します。さらに、具体的なアンチパターンとその理由、そして良い設計がもたらすサービスと事業における利点についても共有します。
 
 #### 想定する聞き手
 
 このセッションでは、初級から上級の開発者を対象としています。特に以下のような聞き手には有益な内容となるでしょう。
 
 - Webサービスの開発や運用に関与している方
 - 権限管理の設計と実装に関心がある方
 - 技術負債を避ける設計と実装に関心がある方
 
 #### 話そうと考えていること
 
 1. 権限管理のユースケース / 繊細さ / 技術負債 の具体例の説明(5分)
     - 何を実現したい際に登場するか
     - どのようなミスが起きうるか、サービスと事業に損失があるか
     - 技術負債の具体例
         - 実装の多くの場所で`admin?`があるが、`admin?`が何ができるかわかない。と思ったら`super_admin?`が出てきたが!?
         - PunditなどGemはあるが条件がメンバー or 作成者 or 管理者 and ...と複雑でパッと理解できない!
     - アンチパターンの紹介
         - 役割(`admin?`)でなく権限(`can_create?`)に依存した実装が望ましい
         - adminが作成をできるという暗黙的な仕様の上で成り立つ実装になってしまっているため、adminが何をできるかが変わる際に全ての`admin?`の使用箇所を確認しないといけない
     - → 聞き手に問題提起に対してあるあるの共感や危機感を与えて、解決策への意欲を引き出す
 2. なぜ権限管理が複雑になるか(3分)
     - 権限の種類が増える
     - 権限の定義が変わる
     - → サービスや事業の成長に伴い、想定ユーザーが変わることや機能が増えることで当時の設計(線引)が最適ではなくなるという前提を聞き手と共有する
 3. 権限管理で大事なこと(2分)
     - 間違えない
         - 実装で間違えない(追加する時)
         - 利用で間違えない(処理中に判定する時)
         - 理解で間違えない(問い合わせの回答の時)
 4. Rails Wayに沿ってModelとPolicyを1対1で対応させ、「**更に1つ軸を設ける**」アプローチのModuleの詳説(15分)
     - Rails Wayに沿って権限を再整理
         - 例として「プロジェクトの更新は、管理者か担当者ならできる」を考える
         - 抽象化すると、「対象の、操作は、役割か、条件ならできる」となります
         - Rails は schema・model・controller と対象を意識させるフレームワークのため、権限も対象を軸として整理する
             - ユーザー -> 役割 -> 対象 -> 条件 -> 操作(CRUD) -> ※属性(white list / カラム)
     - 更に1つ軸を設けるアプローチのModule
         - Punditなどで実装が複雑になるのは対象と操作の2つしか軸がないため、役割という軸を設けることで実装を簡潔にする
             - ※独自Moduleの実装は1番最後に載せています
     - 結果、エンジニア以外も誰が何をできるのかを正確に理解できるようになった
         - 実際に実装を提示して聞き手に「技術負債にならない・間違えない」を実感してもらいます
 5. 良い設計がもたらすサービスと事業における利点(5分)
     - 不具合が0になりサービスの信用が上がった
     - 社内からのお問い合せが0になり開発生産性が上がった
         - エンジニアは聞かれたらコードを確認して回答するので10分は使ってしまう
         - CSやPdMが実装をみて回答できるため、エンジニアまで質問が来なくなった
     - 権限の一覧をクライアントに返す設計によって、Railsだけでなくクライアントのコードの技術負債も防げている
         - クライアントのコードも役割でなく権限に依存する実装に自然となった
 
 #### 参加者が得られる成果
 
 - 権限管理の実装に対する具体的な解決策の理解
 - 課題の整理と解決策(リファクタリング)の模索の思考方法
 - 既に権限管理があるサービスに関わっている参加者も同様の工夫で改善が可能
 - 良い設計がサービスや事業にもたらす具体的なメリットの理解
 
 #### 独自Moduleの実装
 
 ##### ディレクトリ構成
 
 ```
 app
 └── policies
     ├── policy
     │   ├── project           # Project が対象の権限について
     │   │   ├── base.rb       # Project の共通基底クラス
     │   │   └── roles
     │   │       ├── admin.rb  # Project が対象の 管理者 の権限
     │   │       └── normal.rb # Project が対象の 通常  の権限
     │   ├── event
     │   │   ├── base.rb
     │   │   └── roles
     │   │       ├── admin.rb
     │   │       └── normal.rb
     │   ├── base.rb
     │   └── context.rb
     └── policy.rb
 ```
 
 権限の対象ごとにディレクトリを分けています。
 model と対応させることで権限の対象が何かを明確にします。
 
 また、対象ごとに base.rb を用意することで、ドメイン特有の権限や処理の拡張を実現しています。
 
 そして、役割ごとにファイルを分けることでどんな境界(役割)があるのかを明確にします。
 基本は同じ数のファイルを持つため、役割の定義漏れに気づきやすくなります。
 
 ファイルに権限をすべて記述することで対象にはどの権限があるかを明確にします。
 
 ##### policies/policy.rb
 
 ```
 module Policy
   class Error < StandardError; end
 
   class NotAuthorizedError < Error
     attr_reader :policy, :action
 
     def initialize(options = {})
       @policy = options[:policy]
       @action = options[:action]
 
       super("not allowed to #{policy.class.name}##{action}")
     end
   end
 
   class NotDefinedError < Error
     attr_reader :record_class, :role
 
     def initialize(options = {})
       @record_class = options[:record_class]
       @role = options[:role]
 
       super("unable to find #{record_class} policy for #{role}")
     end
   end
 
   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
 ```
 
 利用方法は 3 つあります。
 
 ###### 1. 権限の判定を行う
 
 ```
 project = Project.find(1)
 
 Policy.authorize(user, project, :update)
 ```
 
 ###### 2. 権限があるものだけに絞り込む
 
 ```
 scope = Project.all
 
 Policy.authorize_scope(user, scope, :update)
 ```
 
 ###### 3. 権限の一覧を取得する
 
 ```
 Policy.permissions(user)
 ```
 
 ##### policies/policy/context.rb
 
 ```
 module Policy
   class Context
     attr_reader :user
 
     def initialize(user:)
       @user = user
     end
 
     def authorize(record, action)
       raise(ArgumentError, 'record cannot be nil') unless record
 
       policy = policy_class(user, record.class).new(user:, record:, mode: :record)
 
       raise(NotAuthorizedError, policy:, action:) unless policy.public_send(action.to_sym)
 
       policy.record
     end
 
     def authorize_scope(scope, action)
       raise(ArgumentError, 'scope cannot be nil') unless scope
       raise(ArgumentError, 'scope must be ActiveRecord::Relation') unless scope.is_a?(ActiveRecord::Relation)
 
       policy = policy_class(user, scope.klass).new(user:, scope:, mode: :scope)
 
       policy.public_send(action.to_sym)
     end
 
     def permissions
       list = {}
 
       policy_constants.each do |constant|
         klass = policy_class(user, constant)
         policy = klass.new(user:, mode: :list)
 
         constant_result = klass.public_instance_methods(false).each_with_object({}) do |method, result|
           result[method.to_sym] = policy.public_send(method)
         end
 
         list[constant.to_s.underscore.to_sym] = constant_result
       end
 
       list
     end
 
     private
 
     def policy_constants
       reject_constants = [:Base, :Context, :Error, :NotAuthorizedError, :NotDefinedError]
 
       Policy.constants.reject { |constant| reject_constants.include?(constant) }
     end
 
     def policy_class(user, record_class)
       role = user.role.camelize
 
       "Policy::#{record_class}::Roles::#{role}".safe_constantize || raise(NotDefinedError, record_class:, role:)
     end
   end
 end
 ```
 
 `user``role` と対象の `record` から、権限のクラスを取得しています。
 メタプログラミングを利用することで、クラスを追加するだけで権限を追加できるようにしています。
 
 `permissions` は、`user``role` の権限を一覧で取得しています。
 この一覧をクライアントに提供することで、役割ではなく権限に依存した細かい制御を実現しています。
 
 ```json:jsonの例
 {
   "permissions": {
     "project": {
       "read": true,
       "create": true,
       "update": ["assignee"],
       "delete": false,
       "invite": false
     }
   }
 }
 ```
 
 ##### policies/policy/base.rb
 
 ```
 module Policy
   class Base
     attr_reader :user, :record, :scope, :mode
 
     # NOTE: mode: :list, :record, :scope
     def initialize(user:, record: nil, scope: nil, mode: nil)
       @user = user
       @record = record
       @scope = scope
       @mode = mode
     end
 
     def read
       raise(NotImplementedError)
     end
 
     def create
       raise(NotImplementedError)
     end
 
     def update
       raise(NotImplementedError)
     end
 
     def delete
       raise(NotImplementedError)
     end
   end
 end
 ```
 
 ##### policies/policy/project/base.rb
 
 ```
 module Policy
   module Project
     class Base < Policy::Base
       def assignee?
         record.assignees.exists?(id: user)
       end
 
       def assignee_scope
         scope.left_joins(:assignees).where(assignees: { id: user }).distinct
       end
 
       def invite
         raise(NotImplementedError)
       end
     end
   end
 end
 ```
 
 各対象の `base.rb` には、権限の具体的な処理とドメイン特有の権限を定義します。
 例として「プロジェクトに招待できるか」の権限を `invite` として定義しています。
 
 `ApplicationRecord` に対しての処理と `ActiveRecord::Relation` に対しての処理を分けています。
 
 ##### policies/policy/project/roles/admin.rb, normal.rb
 
 ```
 module Policy
   module Project
     module Roles
       class Admin < Base
         def read
           case mode
           when :list
             true
           when :record
             true
           when :scope
             scope
           end
         end
 
         def create
           case mode
           when :list
             true
           when :record
             true
           when :scope
             scope
           end
         end
 
         def update
           case mode
           when :list
             true
           when :record
             true
           when :scope
             scope
           end
         end
 
         def delete
           case mode
           when :list
             true
           when :record
             true
           when :scope
             scope
           end
         end
 
         def invite
           case mode
           when :list
             true
           when :record
             true
           when :scope
             scope
           end
         end
       end
     end
   end
 end
 ```
 
 ```
 module Policy
   module Project
     module Roles
       class Normal < Base
         def read
           case mode
           when :list
             true
           when :record
             true
           when :scope
             scope
           end
         end
 
         def create
           case mode
           when :list
             true
           when :record
             true
           when :scope
             scope
           end
         end
 
         def update
           case mode
           when :list
             [:assignee]
           when :record
             assignee?
           when :scope
             assignee_scope
           end
         end
 
         def delete
           case mode
           when :list
             false
           when :record
             false
           when :scope
             scope.none
           end
         end
 
         def invite
           case mode
           when :list
             false
           when :record
             false
           when :scope
             scope.none
           end
         end
       end
     end
   end
 end
 ```
 
 役割ごとにファイルを分けることで、コードが複雑になるのを防いでいます。
 
 対象の基底クラスの関数を利用して、権限を表現しています。
 例えば、メンバー かつ 担当者 または 作成者 のような判定もシンプルに表現できます。
 
 ```
 when :record
   member? && (assignee? || author?)
 ```
 
 また、一覧の場合は具体的なデータが必要な判定はできないため、key を返すようにしています。
 
 ### ピッチ
 
 #### このプロポーザルを採択すべき理由
 
 以下に、このプロポーザルを採択すべき4つの理由を述べます。
 
 1. **幅広い層に対応**: 権限管理は個人開発から大規模サービスまで利用される機能です。Kaigi on Railsの参加者の大半が関わったことのある権限管理を題材として課題の整理と具体的な解決策を伝えることで、初級者から上級者までに強い関心と学びを与えられると期待しています。
 
 2. **独自性**: Railsにおいて、権限管理を実装する際はPunditやCanCanCanなどのGemを利用することが多いです。これらの成熟した方法ではなく、権限というものを見直すことで辿り着いた思想と独自Moduleを提示することで、新たな視点と議論の機会を提供します。
 
 3. **実践的な内容**: 本セッションでは、課題の整理と思想だけでなく、それを実現する独自Moduleの実装も含みます。実際にコードを見ることで「技術負債にならない・間違えない」を実感でき、参加者の今後の開発にとって有用なものになると考えています。
 
 4. **権限管理の設計の定番になれる**: Railsにおいて、権限管理を実装する際はPunditやCanCanCanなどのGemを利用することが多いです。Webを検索してもGemの利用以上の解説をしているサイトは少ないです。権限管理が繊細な実装であるが故に発信が少ないと考えられます。権限管理の設計と実装の定番として本セッション及びKaigi on Railsが認知されることを期待しています。
 
 #### テーマで講演する資格
 
 私はRuby on Railsを用いた開発を行っており、特に権限管理が重要とされる業務支援SaaSで、本セッションの主題である「技術負債にならない・間違えない」権限管理の設計と実装と運用を経験してきました。そのため、このテーマについて深く語るための十分な知識と経験を有しています。
 例えば「自分が担当している案件だけ見れる。」「管理者だけが会計情報を見れる。」「協力会社は、顧客情報が見れない。」のような個社ごとに異なる要望の権限管理を本セッションで紹介する独自Moduleで実現しています。また、独自Moduleの導入後1年が経過していますが権限に関するお問い合わせ・不具合は1件も発生しておりません。
 
 ### Bio
 
 株式会社プレックスで「サクミル」の開発をしています。新卒からRailsでご飯を食べています。

添削会のSlackとNotionのコメント

▼ 主張は1つ、情報は削ぎ落とすについて

▼ yuhiの階層構造の初期プロポーザルへの添削

▼ 概要(Abstract)で勝てについて

まとめ

プロポーザルの提出は初めてでしたが、社内で知見を共有しながら進めることができて楽しかったです。
結果として、初参加にもかかわらず3名が登壇の機会をいただくことができました。

おまけ:登壇で意識したこと

私の登壇は2日目の最後でした。

聴講者は本当に疲れていると思ったため、気軽に聞けるポップな構成にしました。その上で「技術負債にならない・間違えない」を実感してもらえることを重視しました。

極論「なんとなく分かる人🙋」で手が上がれば良い発表!の認識で挑みました。

kaigionrails.org

speakerdeck.com

おまけ:Kaigi on Rails 2025 感想

テックリードであり、プロダクトチームの責任者という立場のため、普段は孤独な闘いが多いです。
カンファレンスに参加して、同じ技術を使い、同じように現実と闘う方々と話すことで、元気をいただいて気が引き締まりました。

本当にRubyRailsのコミュニティが暖かく好きです。

事業とサービスがある程度の規模になり、発表できる内容が蓄えられているため、今後もコミュニティに少しでも還元できたらと考えています。

運営の方、聴講者の方、登壇者の方、RubyRailsのみなさま、本当にありがとうございました!

【Kaigi on Rails 2025】プレックスから「3名」が登壇!!!

こんにちは、プロダクトブログ編集長の栃川です。

いよいよ 9月26日・27日 に 「Kaigi on Rails 2025」 が開催されますね!

そして今年は、Day2(9月27日土曜日)に、なんとプレックスから3名のエンジニアが登壇することになりました!

それぞれの発表テーマや意気込みを伺いましたので、参加予定の方もそうでない方もぜひチェックしてみてください!

誰が登壇するの?

弊社からは以下の3名が登壇します。


kakudoooさんの発表について

Q. 発表タイトルは?

小規模から中規模開発へ、構造化ログからはじめる信頼性の担保」です。

Q. いつ登壇しますか?

Day2 10:40〜10:55 に発表します。

Q. どんな内容ですか?

サービスの成長による不具合時の損失や運用コストの高まりをきっかけに、ログの収集と活用の方法を見直しました。 社内にSREチームのような専門性のあるチームがない中、アプリケーションエンジニア起点でできるRailsのロギングと監視の仕組みづくりについて、試行錯誤をしながら取り組んできました。 今回はその取り組みを事例として交えつつ、ログの活用どころや構造化にあたっての観点についてお話しする予定です。

Q. どんな人におすすめ?

  • 小規模チームで立ち上げから運用に関わる方
  • 構造化ログ導入のステップや課題を知りたい方
  • ログは何となく収集しているが活用まで踏み込めていない方

Q. 登壇に向けたコメントをどうぞ!

成長期のプロダクトにおいて、ログを通じたアプリケーションの状態把握の要求は高まります。 一方、そのようなフェーズにおいて、私たちのように効果的なログ収集と活用を意識して運用できていないこともあるのではないかと思っています。 次のフェーズへの橋渡しとして、小規模チームのアプリケーションエンジニアからでもはじめられるログを通じた信頼性の担保について、観点を持ち帰っていただけるととても嬉しいです。


Yuhiさんの発表について

Q. 発表タイトルは?

階層構造を表現するデータ構造とリファクタリング」です。

Q. いつ登壇しますか?

Day2 11:05 〜 11:35 に発表します。

Q. どんな内容ですか?

階層構造を表現するデータ構造について整理し、急成長中のプロダクトにおけるリファクタリングについて解説します。

RDBにおいて階層構造をどのように表現しますか?という問いに対して、初見で答えられる方はなかなかいないと思います。また、データ構造を知っていたとしても、普段SQLをあまり書かないRails開発者が階層構造を操作することも難しいと思います。本発表では階層構造を表現するデータ構造についてSQLクイズを交えながら解説し、皆さんに階層構造への理解を深めていただきます。また、プロダクトが直面したパフォーマンス課題について20倍高速化したリファクタリングについてRailsのコードベースで紹介したいと思います。

Q. どんな人におすすめ?

  • 階層構造を持つ機能のデータ設計に興味がある方
  • 急成長しているプロダクトにおける課題とそれに対するリファクタリングについて興味がある方

Q. 登壇に向けたコメントをどうぞ!

30分と長時間の登壇が初めてでめちゃくちゃ緊張しますが、楽しんで発表できればなと思います!


naro143さんの発表内容について

Q. 発表タイトルは?

「技術負債にならない・間違えない」権限管理の設計と実装」です。

Q. いつ登壇しますか?

Day2 16:30〜17:00 に発表します。

Q. どんな内容ですか?

Webサービスにおいて重要な権限管理について話します。 「なぜ権限管理は複雑になり、技術負債を生むのか」を紐解き、間違えない権限管理の原則を説明します。そして、それを実現したModuleの実装解説をします。最後に良い権限管理を実現した結果、事業やクライアント(Next.js)へどのような影響があったのかを話します。

Q. どんな人におすすめ?

  • Webサービスの開発や運用に関与している方
  • 権限管理の設計と実装に関心がある方
  • 技術負債を避ける設計と実装に関心がある方

Q. 登壇に向けたコメントをどうぞ!

最後の枠の登壇のため、ポップな構成にしました。気軽に聞けるが役に立つ内容になったと思います。 ぜひ当日は「技術負債にならない・間違えない権限管理」を実感していただけたら嬉しいです。


事業の成長を支える3人の発表をお楽しみ!

本日は登壇予定の3人からコメントを紹介しました。 弊社プレックスは、急激な事業成長を継続的に実現しており、その背景には日々進化し続ける技術の力があります。 Kaigi on Rails 2025での発表を通じて、私たちが直面し、そして乗り越えてきた実践知をお届けします。

そして、そんな事業成長を続ける弊社では、たくさんの仲間を募集しています! ご興味のある方はぜひご連絡ください!!

エンジニア募集

100兆円規模のインフラ産業の課題解決に挑戦|業務支援SaaSのテックリード - 株式会社プレックス

急成長する業務支援SaaSのソフトウェアエンジニア・リードエンジニア - 株式会社プレックス

インフラ領域で日本を動かす仕組みを作るスタートアップのエンジニア - 株式会社プレックス

オペレーションの効率化によって事業成長に貢献するコーポレートエンジニア - 株式会社プレックス

インフラ産業の人材課題を解決 | フロントエンドの技術を牽引するテックリード - 株式会社プレックス

弊社について

plex.co.jp

サクミルチームの技術スタックと開発フローの紹介

はじめに

こんにちは。

株式会社プレックスSaaS事業部(サクミル)でエンジニアをしている荒木です。

弊社では現在、複数の事業を展開しており、それぞれの事業部にエンジニアが所属しています。チームによっては異なる技術スタックや開発フローを採用しているため、今回チームごとの特徴や取り組みをまとめることにしました。

本記事は第三弾の記事で、サクミルチームにおける技術スタックや開発フローを紹介します!

サクミルとは

サクミルは、建設業のDXを推進し、業務効率化を実現するバーティカルSaaSです。現場から経営までを一元管理できるのが大きな特徴で、建設業に必要な機能を幅広く提供しています。

2023年7月に正式リリースされて以来、建設業界のDX化ニーズの高まりを背景に急速に導入が進み、2025年3月時点で正式利用企業数は1,000社を突破しました。リリースからわずか1年9ヶ月での達成となり、多くの企業様にご活用いただいております。

sakumiru.jp

prtimes.jp

開発チームについて

それでは、現在のサクミルの開発体制について紹介していきます!

チーム体制

サクミルの開発チームは、2025年8月時点で正社員エンジニア4名、インターン生2名、プロダクトマネージャー(以下PdM)2名で構成されています。

エンジニア全員がフルスタックで開発に取り組み、1人が1つの機能に責任を持ってフロントエンドからバックエンドまで一貫して担当しています。

それぞれが大きな裁量を持ち開発を行うため、少人数ながらも、お客さんの課題を最速で解決し、スピード感を持って価値を届けています。

各メンバーの紹介は弊社の採用ページに載せてありますので、そちらも併せてご覧ください!

dev.plex.co.jp

技術スタック

2025年8月現在の技術スタックは次のとおりです。

フロントエンド
バックエンド
データベース
インフラ・開発環境
開発支援ツール
  • ソース管理: GitHub
  • ドキュメント管理: Notion
  • コミュニケーション: Slack
  • デザイン: Figma
データ基盤・監視
  • データ分析: BigQuery, dbt
  • 監視 / ログ管理: Datadog
  • その他: Firebase Authentication
AI活用
  • Claude Code
  • GitHub Copilot
  • ChatGPT

開発フロー

続いて開発フローについての紹介です!

スプリントの単位は1週間で、毎週月曜に開催されるPdMを含めたスプリント会で、前スプリントで対応した開発タスクの内容確認と進捗、翌スプリントで進めるタスクの確認などを実施しています。

開発の流れは次のとおりです:

  1. 要望の収集
    ビジネスサイドからお客様の声や改善要望が挙がります。
  2. 仕様策定
    PdMが要望を整理し、仕様をまとめて新規機能や改善タスクに落とし込みます。
  3. スプリント会(週1回)
    チーム全体でその週に取り組むタスクを確認。
    エンジニアにタスクを割り当て、進捗も合わせてチェックします。
  4. 実装
    エンジニアは担当したタスクをフロントからバックまで一貫して実装。
    1人が1つの機能に責任を持つスタイルを取っています。

チーム内での取り組み

ジュニア勉強会

直近、私たちのチームではインターン生を2人迎え入れたこともあり、ジュニアレベルのメンバーが増えました。そこで新しく始めた取り組みが「ジュニア勉強会」です。

この勉強会は、シニアのメンバーに遠慮したりせず、純粋にジュニア同士で技術について議論できる機会です。テーマは毎回メンバーで「学びたいもの」を決め、それぞれが事前に勉強してきた内容を持ち寄ります。そのうえで、わからなかったことわかったこと、その背景にある思想などを、ジュニアなりに議論するという形で進めています。

直近ではGoFデザインパターンのAdapterパターンについて勉強しました。勉強会ではただ学んだことをシェアするだけでなく、実際にOSSを見に行き「この部分にこういうデザインパターンが使われているね」と確認したり、自分たちのチームのコードの中で「ここはこのパターンが使われているね」といった発見を共有したりしています。AdapterパターンではRuby on Railsのコードベースを見にいきました!

Adapterパターンのブレスト↓

昼休みの時間を利用して、わいわい楽しい雰囲気で取り組んでいます!

勉強会の様子↓

ナレッジ共有

サクミルのエンジニアチームでは、開発の中での学びや気づきをチーム全体で共有する機会があります。

各メンバーにはSlack上に「times」があり、そこで「わからなかったこと」や「理解できたこと」、最新の技術情報や勉強中の内容などを自由に投稿しています。投稿には他のメンバーがコメントやリアクションを返し合い、お互いの理解を深めたり、解釈のズレを早めに確認したりできる場になっています。

加えて、週に一度の「開発定例」では、その週に学んだ技術的な内容や、話題となっている技術トピックについて共有し合う時間を設け、自分の言葉で共有し合うことで、チーム全体の知見を広げています。

おわりに

弊社では各事業部でエンジニアを募集中です!! 気になるポジションがあればお気軽にお問い合わせください。

SaaS

100兆円規模のインフラ産業の課題解決に挑戦|業務支援SaaSのテックリード - 株式会社プレックス

急成長する業務支援SaaSのソフトウェアエンジニア・リードエンジニア - 株式会社プレックス

PLEX JOB

インフラ産業の人材課題を解決 | フロントエンドの技術を牽引するテックリード - 株式会社プレックス

インフラ領域で日本を動かす仕組みを作るスタートアップのエンジニア - 株式会社プレックス

コーポレート

オペレーションの効率化によって事業成長に貢献するコーポレートエンジニア - 株式会社プレックス