【CTO協会研修記録】 未経験エンジニアがISUCONで圧倒優勝するまでの話

はじめに

こんにちは、2024年4月に株式会社プレックスに新卒入社した佐藤祐飛です。現在は建設業界向けSaaSプロダクト「サクミル」の開発に携わっています。

2024年7月31日に、日本CTO協会主催の新卒合同研修でISUCON研修が開催され、50万点を超えるスコアで優勝することができました。

ISUCONは若手エンジニアにとってハードルが高いコンテストです。私自身、エンジニアとしての経験がまだ1年に満たず、初めはISUCON関連の書籍やブログを読んでも理解が難しかったです。しかし、約2ヶ月間の準備を経て、なんとかISUCONに挑戦できるレベルに達することができました。

本ブログでは、私の体験を通じて「ISUCONに取り組むべき理由」「ISUCONに向けた対策」を共有したいと思います。

ISUCONとは?

ISUCON*1とは「いい感じにスピードアップコンテスト」の略で、LINEヤフー株式会社が運営するWebサービスのチューニングコンテストです。

ISUCONとはLINEヤフー株式会社が運営窓口となって開催している、お題となるWebサービスを決められたレギュレーションの中で限界まで高速化を図るチューニングバトルです。 引用: ISUCON公式Blog

3人以下のチームで8時間以内にチューニングを行い、スコアを競います。基本的にWebサービスがより多くのリクエストを処理するほどスコアが上がります。禁止事項でない限り、DBのスキーマ変更やミドルウェア導入によるキャッシュの利用など、自由にチューニングを行えます。実際のWebサービスと同様に様々なアプローチを取ることができるのがISUCONの魅力となっております。

ISUCONに取り組むべき理由

ISUCONはハードルが高いですが、若手エンジニアこそISUCONに挑戦するべきだと考えています。その理由は大きく3つあります。

ISUCONは意思決定の連続

ソフトウェア開発において、意思決定は大きな責任が伴い、最も成長に直結するプロセスです。ISUCONでは、限られた時間内で優先順位を決め、多くの選択肢から最適なものを選ぶ意思決定を何度も行います。このプロセスがスコアとしてフィードバックされるため、迅速な意思決定とフィードバックのサイクルを経験できます。意思決定とフィードバックのサイクルがここまで短いソフトウェア開発は、ISUCON以外ではなかなか経験できません。また、ISUCONは参加者のブログが数多く投稿されているので強いエンジニアの意志決定をトレースすることも容易です。

研修中、PR TIMESの金子さん(@catatsuy)やLayerXの松本さん(@y_matsuwitter)は「ISUCONと障害対応を通じてエンジニアとして成長した」と語っていました。障害対応もISUCONと同様に、プレッシャーの中で重要な意思決定を繰り返す必要があります。若いエンジニアが早いうちに意思決定の経験を積むことは、非常に重要だと思います。

普段の業務では味わえない経験

ISUCONは若手エンジニアが普段経験できないことに挑戦することができます。具体例として以下のような技術的課題に取り組むことができます。

  • 枯渇するCPUリソース・メモリへの対応
  • CPUプロファイリング
  • スロークエリログの分析
  • DBスキーマの変更
  • 空間インデックスの利用
  • SQLiteからMySQLへの移行
  • 複数インスタンスを利用したアーキテクチャへの移行
  • 並行処理による最適化
  • N+1の解消
  • Redisやmemcachedなどのキャッシュ利用
  • NginxやVarnishなどのリバースプロキシ設定
  • HTTPキャッシュ設定

実践を積まなければ身につかないスキルや、クラウド環境が主流となった現代では見過ごされがちな技術が多くあります。ISUCONでは、多くの選択肢を持って意思決定を行うことが重要であり、そのために日々の技術研鑽がまだまだ必要だと痛感させられます。

面白いアプリケーション

ISUCONで出題されるアプリケーションは毎年工夫が凝らされており、学習教材としても非常に有用です。特に普段業務で触れない技術やコードに触れる良い機会となります。私自身もGoの学習教材として利用しています。

以下にISUCONのイントロ動画を貼ってみます。めっちゃ面白いです。

youtu.be

youtu.be

ISUCONに向けた対策

ここからは私がISUCON研修に向けて行った具体的な準備を紹介します。

OS・CPU

OS・CPUの基礎知識がないと計測結果やMySQL・Nginxのパラメータなどの意味を理解することができません。「[試して理解]Linuxのしくみ」ではこれらの基礎知識をわかりやすく図解しています。スレッド、スケジューリング、メモリ、ファイルディスクリプタ、ディスクI/Oなどに不安がある方は一読してみることをお勧めします。

gihyo.jp

DB

ISUCONではスコアを上げるためにDBの理解が必須です。特に役に立った記事と書籍を紹介します。

MySQLのパフォーマンスについて解説されています。トランプの話はとてもわかりやすかったです。 www.slideshare.net

InnoDBのインデックスを詳しく図解しており、Bツリーインデックスやインデックスを張った際の振る舞いをイメージすることができるようになります。 techlife.cookpad.com

分散システムにおけるレプリケーション、パーティショニングなどについて詳しく書かれています。ISUCONでは複数台アーキテクチャを利用できるため、DB分割なども打ち手として持てるようになります。 www.oreilly.co.jp

Go

普段の実務ではRuby及びTypeScriptを扱っているのですが、ISUCONにおいてはGoの使用率が高いため、Goについて一からキャッチアップしました。

基本文法はお馴染みの「A Tour Of Go」で学習しました。 go-tour-jp.appspot.com

メモリを中心としたGoの低レイヤーについて詳しく書かれています。正直、ISUCONで低レイヤーまでチューニングをほとんどしませんが、読んで損はないと思います。 www.oreilly.co.jp

並行処理を扱ったことがあまりなく、ピンと来なかったので読んでみました。 www.oreilly.co.jp

リバースプロキシ・HTTPキャッシュ

Nginxを詳しく学びたい方におすすめです。 tatsu-zine.com

HTTPキャッシュはヘッダーやキャッシュ条件が複雑で奥が深いので一読してみることをお勧めします。 gihyo.jp

ISUCON本

ISUCON関連の本の中では一番有名な本で、private-isuという問題を題材にチューニングの基礎が解説されています。エンジニアになりたての昨年は、難しく読み進めるのが大変でしたが、上記のインプットを入れた後だとスッと頭に入ってきました。

私はprivate-isu関連のブログを読みこみ、最終的にprivate-isuの点数を約91万点まで伸ばすことができました。

gihyo.jp

giarrium.hatenablog.com

Makefile

ISUCONでは計測と改善のサイクルを高速に回すことが大切です。そのために、Makefileスクリプトを事前に作り込んでおくことが重要です。私は東京工業大学デジタル創作同好会traPの@_oribe1115さんのMakefileを参考にさせていただきました。

また、計測結果をチームメンバーと素早く共有するため、notify_slackを利用してSlackに通知を送るようにしました。

github.com

ISUCON過去問

ISUCON11予選は出題の意図にもある通り「教科書的な問題でありつつも、解きごたえのある問題」でprivate-isuの次に解く問題として丁度良い難易度だと思いました。 isucon.net

本番を想定した練習

時間制限がある中で解くISUCONは無制限で解くISUCONと全くの別物です。可能であれば、多くの人を巻き込み、本番と同じ条件で練習することを強くお勧めします。私はISUCON研修の週に上司に頼んで社内ISUCONを開催していただきました。また、社内ISUCONの翌日にはCTO協会の研修メンバー5人でチーム対抗戦を行いました。

それぞれ、ISUCON13とISUCON9予選の問題を解きました。

isucon.net

isucon.net

ISUCON研修本番

研修本番は二人一組でチームを組み、全31チームの対抗戦でした。ここまでは初見の問題を解く想定で準備をしていましたが、本番の問題はまさかのprivate-isuでした。private-isuは既に4回解いていたので、さくさく計測と改善を回して、15:30頃には目立ったボトルネックを潰すことができていました。

15:30頃のスコアボード

最後はhtmlファイルのExecuteが一番のボトルネックとなっていたのでhtmlを全てバイト列で持ち、レスポンスに直接書き込むという黒魔術を使って50万点越えを達成しました。

github.com

最後に

このような素晴らしい研修を開催していただいたCTO協会とその関係者の皆様、本当にありがとうございました。

私は弊社で初めての新卒エンジニアとして入社し、同じ立場で話せる同期エンジニアがいないことに寂しさを感じていました。そんな時に、松本さんの新卒研修を開催するというXの投稿を目にし、弊社のCTOに参加をお願いして実現に至りました。研修後の懇親会も毎回楽しみで、絶対に良い仲間をたくさん作ろうという気持ちで臨んでいました。

新卒研修終了後も継続的に交流するために勉強会のグループを立ち上げました。月一ペースで各メンバーのオフィスで輪読会を開催する予定です。僕の今の夢は10年経ってもこのメンバーで組織を語り合いながらお酒を飲むことです笑。24卒で参加したい方がいらっしゃればXのDMで是非連絡ください!

最後にプレックスではエンジニアとプロダクトデザイナーを募集しております。ご興味ある方はご連絡していただけると嬉しいです! dev.plex.co.jp

plex.co.jp

*1:「ISUCON」は、LINEヤフー株式会社の商標または登録商標です。

【入社エントリ】フロントエンドからバックエンドへ、さらなる高みを目指すために入社しました!🔥

入社エントリーアイキャッチ画像

はじめまして、プレックスの高岡と申します。

2024年4月に株式会社プレックス(以下、プレックス)にエンジニアとして入社しました。

今回、入社して4ヶ月程経ちましたので、入社経緯や入社してからの感じていることをまとめておきたいと思います。
一人でも多くの方にプレックスを知ってもらえると嬉しいです。

自己紹介

現在25歳で社会人4年目になります。

新卒では、地元の受託開発の会社に就職しました。
大学時代に海外留学をしていて、新卒で就職するつもりはなく2,3年は海外で暮らそうと考えていました。 ただコロナ禍の影響もあり帰国することを決めたので、地元で就活を行い、その会社に新卒入社いたしました。

主に、フロントエンドチームに所属して、Webアプリケーションの開発・運用を行っていました。海外拠点への出張やチームリーダーなどの経験を積むことができ、とても刺激のある会社で働くことができたと実感しています。
約3年間お世話になった後に、プレックスへの転職をする運びになりました。

プレックスではドライバー向けの求人サービスであるプレックスジョブの開発にフロントエンド・バックエンドの両面で携わっています。

プレックスへ入社した理由

エンジニアとして成長できる環境が一番整っていると感じたからです。
それと前職の先輩であり、今は同じチームで働く、師匠かつ飲み友の池川さんよりお誘いいただいたことがきっかけです🙏

まだ経験の浅いバックエンドの開発に携われること、主体的に動ける(良い意味で組織・チームの環境が整いすぎていない)ことなどが成長できる環境と定義していました。
前職でマネジメントを行う中で、エンジニアとしての技術力が足りないことを痛感していましたので、技術力を上げることを最優先にしていました。
幸い、池川さんより社内の様子やエンジニアチームの雰囲気などを聞けて、イメージが湧いていたのがよかったなと思っています。

ただイメージは湧いていたものの、経験が浅いのでついていけるか・ドメイン知識がないが大丈夫かなど不安は多くありました...

入社して感じたところ

そんな不安を抱えつつも、入社してからはあっという間に4ヶ月が経過しました。
転びながらも前に進めているのではと思っています。この機会に改めて振り返りたいと思います 🏃‍♂️

やっぱり求められる基準が高い

長く運用できるプロダクトを開発することを意識して、全メンバーのコードの質技術負債の解消に対する積極性などの基準が高いです。

コードレビューでは一切の妥協なくコードの質をあげるためのフィードバックを常にいただいています。
プロダクトも成長していく中で、データの複雑化やビジネスサイドのオペレーションの変化もあります。なので要件の背景を正確に理解して、なぜそのコードがベストなのかを常に考える必要があります。

技術負債に関しても、開発のサイクル内で継続的に取り組むよう、洗い出しを行なった上で優先度をつけて対応をしています。

上記の基準で常にパフォーマンス高く開発を行っているメンバーが多く在籍しています。
私も上記の基準が当たり前になった時にどのような世界が見えるのか楽しみです!

他のエンジニアチーム・メンバーの色や個性が多様でおもしろい

現在のエンジニア組織は、プレックスジョブ、サクミル、コーポレート、マーケの4つの事業部に分かれています。
入社後すぐにそれぞれの事業部に色の違いを感じることができるぐらい、雰囲気の違いがあります。 詳細を書くと長くなりますので、ぜひ入社後に感じていただければと思っています!

個々のメンバーでも強みやこだわりを持っているメンバーが多いです。
例えば、バックエンドが好きでデータベースに関わる技術を極めようとするメンバーやとにかく熱量が高く何でも取り組むメンバーなどがいます。

私自身はフロントエンド出身ということもあり、UIに少しこだわりがあります。 別のメンバーからUI厨と呼ばれるくらいに、使用するアプリやガジェットにこだわっています...笑

ビジネスサイドにも魅力的なメンバーが多い

ビジネスサイドにもパワーあふれるメンバーが多いので、エンジニア同士とはまた違った良い刺激を受けます。

特に毎週金曜日に事業部で自慢大会があるのですが、そこでよく目標売上を達成して拍手喝采が起こるのを目の当たりにしています。 直接的な売上ではないのですが、プロダクトが成長することで売り上げにつながるので、私も貢献するぞという気持ちでいっぱいです。

それと他のメンバーのブログでも記載がある通り、事業の売り上げが右肩上がりで、従業員数もすごい勢いで増えています。
事業が伸びているからこそ、良いメンバーが集まり、良い結果を出せるという好循環が生まれています。

最後に

バックエンドの経験がまだ少なく学ぶことがたくさんあり、パフォーマンスが良くないと落ち込む日も少なくないです。
ただ着実に前へ進んでいる実感があるので、すぐに立ち上がり走り出すことができています。
現在は周りのメンバーに支えられていることが多いですが、これからは自分自身がチームの基準を上げていきたいと思っています 💪

最後にはなりますが、現在プレックスではソフトウェアエンジニア、フロントエンドエンジニア、UIデザイナーの募集もあります。
もしこの記事を読んで、一緒に熱く働いてみたいと思った方がいましたら是非ご連絡をお待ちしています!

dev.plex.co.jp

次回予告

先月 Reactの公式ドキュメントを読み込む会を朝活のテーマとして行なっていました 📚
そちらでキャッチアップした内容を活かして、「Reactにおけるパフォーマンスの改善」をテーマに既存コードをリファクタリングしてみる内容のブログを公開予定です。

もちろんバックエンドの開発も楽しいのですが、やはりフロントエンドがもっと好きだということに最近気づきました!
ぜひ次回もお楽しみにいただければ 🤲

【GraphQL Ruby】N+1問題を防ぐ GraphQL::Dataloaderのまとめ

はじめに

こんにちは、SaaS事業部(サクミル)のエンジニアの栃川です。

今回は、N+1問題を防ぐGraphQL Rubyの「GraphQL::Dataloader」について調べたことをまとめていきたいと思います。

github.com

対象とする読者

  • GraphQL Rubyについて基本的な知識があるという方
  • GraphQL::Dataloaderについて概要を知りたい方

GraphQL::Dataloaderとは

GraphQL::Dataloaderは、データベースやマイクロサービスなどの外部サービスへの効率的なバッチ アクセスを提供してくれます。

その仕組みはとてもシンプルで2つのアプローチを行います。

  1. GraphQLフィールドは、何が要求されているかをクエリパラメーターやオブジェクトIDなどをリクエストキーとして登録します。
  2. できるだけ多くのリクエストキーを収集した後に、まとめて外部サービス(例えば、DB)へのフェッチを開始する

このアプローチによって、GraphQLでは取得したいデータのノードを辿って必要なデータを一度に取得できるため、不必要にSQLが大量発行されてしまうN+1問題を防ぐことができます。

GraphQL::Dataloaderの実装例

実際の実装例を紹介します。

前提

ある会社Aの従業員は必ず1つの部署だけに属しているとします。

この場合、ある会社Aの従業員と部署名を取得する例を考えます。

query Employees (
  $companyId: ID!
) {
  company: node(id: $companyId) {
    ... onCompany {
      id
      employees {
        nodes {
          id
          department {
            id
            name
          }
        }
      }
    }
  }
}

fieldの定義

まずはfieldを定義していきます。

departmentフィールドは、dataloader.with(::Sources::Department).load(object.id)と定義します。

こうすることで、object.id(employeeのid)をリクエストキーとして、「リクエストをしたい」ということだけをGraphQL::Dataloaderに伝えます。

あとで記載しますが、これによって逐一SQLが発行されることを防ぎ、N+1問題の発生を回避できます。

module Types
  module Objects
    class Employee < Types::Bases::Object
      field :department, Types::Objects::Department, null: false

      def department
        dataloader.with(::Sources::Department).load(object.id)
      end
    end
  end  
end

sourcesの実装

次にSourcesを定義していきます。

fetchメソッドには、GraphQL::Dataloader が外部サービスからデータをフェッチするためのロジックを定義します。

引数のemployee_idsは、loadメソッドに渡されたobject.idの集まりが配列として渡されます。 つまり、要求されているリクエストの全リクエストキーがfetchメソッドの引数に渡されます。

今回の例の場合、取得するべき全ての従業員のidがemployee_idsとしてfetchメソッドに渡されます。

fetchメソッドでは、このemployee_idsを使い、@model_class.where(employee_id: employee_ids)と定義します。

# frozen_string_literal: true

module Sources
  class Department < GraphQL::Dataloader::Source
    def initialize()
      @model_class = :Department
    end

    def fetch(employee_ids)
       @model_class.where(employee_id: employee_ids)
    end
  end
end

上記のように定義することで、以下のようなSQLが発行されて、一気に部署を取得してくることができます!!

SELECT 
    "departments".* 
FROM 
    "departments" 
WHERE 
    "departments"."employee_id" IN (1, 2, 3, 4)

どんな仕組みになっているのか?

GraphQL::Dataloaderが実際にどんな仕組みで動いているのかを見ていきます。

Fiberについて

前提として、GraphQL::Dataloaderの実装は内部的にはノンプリエンプティブな軽量スレッドであるRubyのFiberを利用して、実装されています。

docs.ruby-lang.org

このFiberについては、普段からRubyを使われていたとしてもあまり知らない方もいると思うので簡単に説明します。

Fiberは、任意の場所で実行を中断して再開できる機能を提供します。

例として、非同期処理の実装例を紹介します。

# 非同期処理のシミュレーション
fiber1 = Fiber.new do
  5.times do |i|
    puts "Fiber 1 - ステップ #{i}"
    Fiber.yield
  end
end

fiber2 = Fiber.new do
  3.times do |i|
    puts "Fiber 2 -ステップ #{i}"
    Fiber.yield
  end
end

# メインループでFiberを順番に実行
loop do
  if fiber1.alive?
    fiber1.resume
  end
  if fiber2.alive?
    fiber2.resume
  end
  break unless fiber1.alive? || fiber2.alive?
end

puts "完了!"

このコードでは、fiber1fiber2を順番に実行しています。 Fiber.yieldによって処理が中断するたびにメインループに戻ります。

そして、resumeメソッドを使い、再度再開することができます。

つまり、Fiberを通じてコンテキストを切り替えることで、処理を交互に行っています。

なお、上記のコード出力結果は以下の通りです。

Fiber 1 -ステップ 0
Fiber 2 - ステップ 0
Fiber 1 - ステップ 1
Fiber 2 - ステップ 1
Fiber 1 - ステップ 2
Fiber 2 - ステップ 2
Fiber 1 - ステップ 3
Fiber 1 - ステップ 4
完了!

このように、Fiberを使うことで処理を途中で中断したり再開したりすることができます。

GraphQL::Dataloaderの仕組み

では、GraphQL::DataloaderではFiberは何の用途で使われているのでしょうか?

答えは、上述した「2. できるだけ多くのリクエストキーを収集した後に、まとめて外部サービス(例えば、DB)へのフェッチを開始する」という挙動を実現するために利用されています。

GraphQLでは、クエリ全体の処理を親Fiberが担い、各フィールド処理を子Fiberが担当します。 (親Fiberと子ファイバーはGraphQL::DataLoader#runメソッドで生成されます。)

GraphQLフィールドの処理が行われる時、loadメソッドが呼び出されると、内部的には下記のsyncメソッドが呼び出されます。

syncメソッドは、子Fiberの処理を中断し、親Fiberへコンテキストを移します。

 MAX_ITERATIONS = 1000

 # Wait for a batch, if there's anything to batch.
# Then run the batch and update the cache.
# @return [void]
def sync(pending_result_keys)
  # 親Fiberにコンテキストを移す
  @dataloader.yield
  iterations = 0

  # 処理が保留されたフィールドがある場合
  while pending_result_keys.any? { |key| !@results.key?(key) }
    iterations += 1

    if iterations > MAX_ITERATIONS
      raise "#{self.class}#sync tried #{MAX_ITERATIONS} times to load pending keys (#{pending_result_keys}), but they still weren't loaded. There is likely a circular dependency."
    end

    # 親Fiberにコンテキストを移す
    @dataloader.yield
  end

  nil  # メソッドの終了
end

Github 該当箇所

こうすることで、GraphQLフィールドの処理が行われる際に、loadメソッドが呼ばれると、 そのフィールドの処理は一時中断し、次のフィールドへ処理が移ります。

この処理を兄弟フィールド全てに対して行います。

その後、親Fiberは未処理状態のフィールドを解決するため、GraphQL::DataLoader::Source#fetchメソッドに定義された処理を行います。

fetchメソッドは、実装例にあるように、データを取得するためのロジックを定義する必要があります。

      # Subclasses must implement this method to return a value for each of `keys`
      # @param keys [Array<Object>] keys passed to {#load}, {#load_all}, {#request}, or {#request_all}
      # @return [Array<Object>] A loaded value for each of `keys`. The array must match one-for-one to the list of `keys`.
      def fetch(keys)
        # somehow retrieve these from the backend
        raise "Implement `#{self.class}#fetch(#{keys.inspect}) to return a record for each of the keys"
      end

Githubの該当箇所

このように、Fiberの特性を用いることで、複数のフィールドの処理を一時停止して、最後にまとめて取得するといったことをGraphQL::Dataloaderでは行っているということがわかりました。

まとめ

今回は、簡単にGraphQL::Dataloaderについて内部実装を確認しながら自分なりにまとめてみました。

GraphQL::Dataloaderがあることで、開発時に特別意識しなくとも、 N+1問題を回避しつつ効率的にデータ取得することができると理解できました。

まだまだGraphQL::Dataloaderへの理解は浅いので、これからもさらに深掘っていきたいと思います。

さいごに

最後に、弊社では全ての事業部でエンジニア採用を積極的に行なっています。

少しでも興味を持っていただけた方は業務委託や副業からでも、ぜひご応募ください!

dev.plex.co.jp

Developer eXperience Day 2024 参加レポート

こんにちは、プレックスの石塚です。

今回は、7月16日から17日の2日間にわたって開催されたDeveloper eXperience Day 2024に参加してきたので、その感想をまとめたいと思います! Developer eXpericence DayはCTO協会が主催するイベントで、その名の通りテーマは開発者体験となっています

cto-a.org

印象に残ったセッションとその感想

簡単ではありますが、印象に残ったセッションを紹介して、感想をまとめたいと思います。

チーム安野が目指すデジタル民主主義

東京都知事選に出馬し、無所属・初出馬で5位の15万票を獲得された安野さんの発表です。都知事選が終わったばかりのこのタイミングでのイベント登壇ということで、安野さんの選挙で得た知見を世の中に還元したいという思いと主催者の本気度が伝わってきました。

発表では安野さんが15万票という成果を上げることができた仮説について、「デジタルテクノロジーを使って、双方向の選挙をできたからではないか」というお話がありました。多くの人から意見を吸い上げる仕組みやそれを議論して、取り入れて、反映するという作業を繰り返すことは政治のみならず組織運営でも通じるものがあると感じました。

またこのセッション以外でもプロダクト開発のエッセンスを組織作りに取り入れる発表などもあり、テクノロジーを使ったソフトウェアエンジニアリングのアプローチはさまざまな分野で通用するということを証明した出来事だったのではないかと思います。

さらに興味のある方は下記のnoteもぜひ読んでいただきたいです。 note.com

生成AI関連の発表

特定の発表ではなく恐縮ですが、今年の発表タイトルを見ても、生成AI系の発表が多く、自分が参加したセッションも14個中6個が生成AI関連のものでした。

以下は、自分が参加した生成AIに関連するセッションの一覧です。

  • エンジニアの才能を最大限に引き出す Googleの生成AIが実現する開発生産性の向上
  • 最先端の生成AIトレンドから先読みする これからの生成AIエンジニアに求められるスキルセット大解剖
  • 脳力とAIのアラインメント
  • Software EngineerのためのPrompt活用
  • 生成AIと自動運転開発の舞台裏
  • 生成AIに振り回された3か月間の成功と失敗

上記のセッションの中では、Googleのプロダクションコードの半分以上が生成AIによって書かれているという話や、生成AIによってライブラリを自動でアップデートしたりESLintのIgnoreを自動で解消するタスクに取り組んでいるといった話がありました。

ソフトウェアエンジニアとしていかにAIと付き合っていくかは改めて避けては通れないテーマだということ、組織としても個人としてもより真剣に生成AIに取り組んでいかなければならないということを実感しました。

まとめ

2日という短い期間でしたが、日頃の業務を離れて広く日本のソフトウェア業界全体を見渡すことができてとても有意義な2日間でした。

特にイベントを通じて最も実感できたことは、 「Software is eating the world」 の波が予想以上に大きく広がっていることでした。純粋なソフトウェアの開発から始まったIT産業ですが、ハードウェアや非IT産業のDXに広がり、今では政治や行政といった領域にも広がっていることを身を持って体感することができました。

我々プレックスもSaaSというソフトウェアを提供していますが、同時に人材紹介事業やM&A仲介事業といった非IT産業のDXにも取り組んでいます。宮坂副都知事の基調講演で行政は市場規模が大きく、まだまだITが未開拓の領域というお話がありましたが、それはプレックスが取り組むDXも同様です。

一見純粋なソフトウェア系のプロダクトと比較して面白みがないと思われてしまいがちな領域ですが、テクノロジーを活用して圧倒的に生産性の高い事業を生み出すというチャレンジは面白く、価値があると感じています。まだまだ日本ではそうした非ITの領域が巨大であり、プレックスの成功が日本の産業に対して大きなインパクトを与えられるようなモデルケースにしていきたいと思っています。

最後に、プレックスでは開発者体験を改善するソフトウェアエンジニアを募集しています。少しでも興味を持っていただけた方は業務委託や副業からでも、ぜひご応募いただけると幸いです。

dev.plex.co.jp

【インターン記録】技術と事業の両軸を目指して

はじめに

はじめまして、豊田と申します。

約半年間、エンジニアインターンとしてサクミルという建設業界向けのSaaSプロダクトの開発に携わりました。

事業が急成長する中で、様々な経験をしたので、振り返りを兼ねてブログを書くことにしました。

インターンに参加をしたきっかけ

Xでインターン募集の投稿を見て、興味を持ちました。

当時は事情があり、選考を受けませんでした。

それにもかかわらず、就活やキャリアの相談に真摯に向き合っていただきました。

お話させていただく中で、技術と事業に対する熱い思いを感じ、この方と一緒に働けたら絶対に楽しいだろうと思いました。

その後、改めて連絡をしてインターンに参加させていただくことになりました。

魅力的だった点

1. ビジネスと開発の距離の近さ

ビジネスと開発の距離間が近く、事業にも興味がある私にとって、サクミルでの開発は非常に楽しいものでした。

サクミルでは、エンジニアが技術的な観点から事業・企画・仕様に対して責任を持ちます。

そのため、企画・仕様・設計・実装の全てにおいてエンジニアが関わります。

もちろん、インターン生も同様です。

新しい開発要件が出てきた際には、お客様のユースケースを踏まえて、PdMやビジネスサイドの方と議論し、それをどのような概念としてサービスに落とし込んでいくかを考えます。

2. 事業が急成長する中での開発

インターンを始めた当初と比べて、半年間で事業も組織もプロダクトも大きく変化しました。

事業立ち上げの0から1のフェーズにおいて「0が確かな1になる」そういう期間だったと思います。

また、プロダクトが成長するにつれて、開発の内容もより面白く変化していきました。

事業が伸びているからこそ、このような刺激的な経験ができたのだと思います。

半年間で得たもの

プロとしての自覚

全てを理解しなくても良いけど、既存実装と関連する公式ドキュメントは全て読んでからがスタート」。

メンターであるテックリードの方からいただいた忘れられない言葉です。

既存実装を一通り読み、何度も公式ドキュメントを読み込むことを徹底しました。

また、メンターの方が非常にプロフェッショナルであり、その姿勢を見ているうちに「自分もプロでありたい」と思うようになりました。

フィードバック

既存実装と公式ドキュメントを読み込むことを徹底したおかげか、現状を正しく理解しようとする姿勢を評価していただけました。

一方で、新しい概念など既存の仕組みを超える必要がある際に、現状の仕組みや枠組みに囚われすぎてしまうというフィードバックをいただきました。

半年間で変わったこと

今回のインターンで得られたものとして、大きくスタンスとスキルの部分があります。

スタンス

プロとしてのエンジニアの基準を知ったことで「ここまでやらなければならない」「これくらいやらないと、プロとしてのスタートラインにすら立てない」という自覚と覚悟が芽生えました。

スキル

企画から関わり、技術と事業に向き合うことを通じてソフトウェアを捉える力が向上したと感じています。

具体的には、組織とプロダクトの変化の中で現状がどのような前提に依存しているか、その前提が崩れる境界は何か、機能をどう仕様と設計に落とし込むか、なにを意識させたくてこのインターフェースにするかなどを、考えられるようになりました。

自分の中で、良いコードとは何か、良いアーキテクチャとは何かをある程度は言語化できるようになりました。

「技術的な視点から事業を伸ばせるエンジニア」になる

今回のインターンを通じて、事業が急激に成長していくフェーズを経験することができました。

立ち上げ期においても品質と開発速度の両立が出来ることを実感し、ここまでやりきることが出来ると学びました。

メンターであるテックリードの方のように、自分も技術的な視点から事業を伸ばせるエンジニアになりたいと強く思いました。

最後に

インターン期間中にお世話になったメンターをはじめ、関わっていただいた皆様に改めてお礼を申し上げます。

技術と事業の両軸を目指しているエンジニアにとって、サクミルはこの上ない環境です。

興味を持った方は、ぜひ応募してみてください!

dev.plex.co.jp

【入社エントリ】休養を経てプレックスに入社した理由

はじめに

初めまして、エンジニアの中西と申します。
2024年1月に株式会社プレックス(以下、プレックス)にマーケティングチームのエンジニアとして入社致しました。
入社してから今日までの時間の中で感じたプレックスの良さや業務内容などをお伝えできたらと思います。

簡単な自己紹介

  • 2019年に新卒で受託,派遣(SES)の会社に入社し、web開発に従事。
  • 2020年2月ごろに転職し、受託,派遣(SES)の会社に入社。web開発に従事。
  • 2022年5月退職
  • 2024年1月プレックスに入社

2022年5月~プレックスへ入社するまでの間、詳細は割愛しますが鬱病のため療養しておりました。

プレックスへの入社理由

これまで受託や派遣(SES)が主の会社に勤めていましたので、成果物に対して明確なフィードバックがある環境に居ませんでした。
そのため、制作物へのフィードバックがCVRなどの数値となって明確に返ってくるというお話に魅力を感じました。
また、自社サービスを開発できる点にも魅力を感じたのが理由です。

所属しているマーケティングチームについて

当初は私と業務委託の方数人の体制でしたが、現在は社内エンジニア2人+業務委託の方という体制となり、主にランディングページ(以下LP)を制作しております。
またLP以外の業務であっても、やりたいことがあればやらせてもらえる環境となっています。
主にフロントエンドですが、勿論バックエンドを触ることも可能です。

入社後に感じたプレックスの魅力

妥協しないデザイン・レイアウト

1pxを妥協せず調整を行う、そのような環境に身を置けます。
例えば、css上中央であっても見た目でずれていれば1px単位で調整を行います。 時に大変ですが、出来上がりは素晴らしいものになること間違いありません。

エンジニア以外との関わり

主にマーケティングの方々と関わることが多いです。
CPC(Cost Per Click: クリック単価)*1やCVR(Conversion Rate: 獲得率)*2など、エンジニア業務だけでは聞くことのないような単語を意識して仕事に当たることができます。
またエンジニアかどうか関係なく、理解することを諦めてる方はいないので、同じ温度感で開発の相談ができます。

当事者意識

自身の業務上のことであり相手には関係なくとも、当事者であるかのように相談にのって頂けます。
解決するために何が必要かなど、一緒に考えていただける素晴らしい環境が整っています。
相談しても放置されるなんてことはまずないです。

最後に

プレックスはとても成長を促される環境だと思います。
また、休養期間がある方でも臆することなく働ける環境となっています。
随時エンジニアを募集しておりますので、ご興味持っていただけたら幸いです。

dev.plex.co.jp

*1:ユーザーによる広告のクリック1回当りに掛かる費用

*2:広告のリンクをクリックした数のうち、何割がコンバージョン(商品購入や資料請求などの最終成果)に至ったかの割合を示す指標

Firebase Authenticationにおける分散トランザクション

はじめに

2024年4月に株式会社プレックスにエンジニアとして新卒入社した佐藤祐飛と申します。現在はサクミルという建設業界向けのSaaSプロダクト開発を行っています。

sakumiru.jp

Firebase Authentication(以下Firebaseと略します)を利用した認証において、ユーザー作成時に分散トランザクションによってデータの整合性を担保する実装をRuby on Railsで行ったのでその知見について共有したいと思います。

firebase.google.com

背景

サクミルにおけるユーザー認証について

サクミルではFirebaseを活用したJWTによるユーザー認証を行なっています。ユーザー認証完了後、FirebaseのユーザーUIDを元にサクミルはDB上にあるユーザーデータを提供し、各ユーザーはサクミルにログインすることができます。

ユーザー作成方法について

サクミル管理画面ではアカウント発行機能としてFirebase上へのユーザー作成とサクミルのDB上へのユーザー作成を同時に行う機能を提供しています。サクミル管理画面のAPIにおけるユーザー作成手順を以下に示します。

ユーザー作成手順

サクミル管理画面のAPIRuby on Railsで書かれているのですが、Firebase公式からはRubySDKが提供されていないので、google-api-ruby-clientというgemを活用して、Firebase上へのユーザー作成を行なっております。

github.com

課題

ユーザーデータの不整合が生じる可能性がある

ユーザーデータがDBとFirebaseという異なる2つのノードに保存されるので、データの不整合が生じる可能性があります。

例えば、Aさんのユーザーデータを作成することを考えます。Firebase上へAさんのユーザーデータを保存することに成功したものの、その後のサクミルのDB上への保存が何らかの原因で失敗した場合、「サクミルDBにはAさんのデータがなく、FirebaseにはAさんのデータがある」という不整合が発生します。

ユーザーデータの不整合が生じる例

この不整合の状態で、再度サクミル管理画面からAさんのアカウント発行を実行するとFirebase上の一意制約によってアカウント発行の処理が失敗してしまいます。

Firebaseのコミット制御やロールバックができない

いわゆるIDaaSであるFirebaseを利用すると、認証サービスの実装コストや監視コストが下がるというメリットがある反面、デメリットとして認証サービスのカスタマイズ性が低下します。

FirebaseはFirebase内部のDBについて、コミットのタイミング制御やロールバックを行う機能を提供していないので、分散トランザクションの代表的手法である2フェーズコミット(2PC)を行うことができません。

サーガパターンによる整合性担保

上記の課題を解決するために、サーガパターンによる分散トランザクション管理を実装しました。

サーガパターンとは

サーガパターンとは、複数のサービスにまたがるビジネスプロセスを管理し、データの整合性を担保する分散トランザクション手法です。サーガパターンは、各サービスのローカルトランザクションのシーケンスであり、ローカルトランザクションはDBを更新して、次のローカルトランザクションをトリガーします。

ロールバックを実行する場合は、各ローカルトランザクションを取り消す操作として補償トランザクションを実行します。

サクミル管理画面 APIの実装

Firebaseへの操作を管理するFirebaseAdminクラスを以下に示します。

工夫したポイントは2点あります。 1点目はFirebaseへの操作を行うメソッド(今回はsign_up_user)内で補償トランザクションをスタックに積んだことです。 2点目は、補償トランザクションの処理をproc インスタンスとしてスタックに保持させ、rollbackメソッドによってロールバックを行えるようにしたことです。

これらの工夫によって、動的にUIDが変化するロールバックの処理を実現することができます。

class FirebaseAdmin
  def initialize
    # HTTP通信のclientを初期化する
    @client = Google::Apis::IdentitytoolkitV3::IdentityToolkitService.new
    
     # 認証
    ...
    # 補償トランザクションの処理を保持するスタック
    @revert_proc_stack = []
  end

  def sign_up_user(email:, password:)
    # Firebaseへユーザー作成のリクエストを送信
    request = Google::Apis::IdentitytoolkitV3::SignupNewUserRequest.new(email:, password:)
    response = @client.signup_new_user(request)

    uid = response.local_id

    # ユーザー削除の処理(補償トランザクション)をスタックにプッシュする
    @revert_proc_stack.push(proc { delete_user(uid:) })

    uid
  end

  def delete_user(uid:)
    request = Google::Apis::IdentitytoolkitV3::DeleteAccountRequest.new(local_id: uid)
    @client.delete_account(request)
  end
  ...

  def rollback
    # スタックが空になるまで補償トランザクションを実行する
    until @revert_proc_stack.empty?
      proc = @revert_proc_stack.pop
      proc.call
    end
  end

  def cleanup
    @revert_proc_stack.clear
  end
end

以下に、アカウント発行を実行した際に叩かれるcreate_user!メソッドを示します。

工夫したポイントはActiveRecordトランザクション内でFirebaseへの操作とDBへの操作を実行し、例外処理でロールバックを行ったことです。例外(FirebaseまたはDBに対する処理の失敗を想定)が発生するとFirebase上のロールバック処理とDBのロールバック処理が実行されることが保証されるため、ユーザーデータの整合性が担保されます。

def create_user!(email:, password:)
  # FirebaseAdminインスタンスを作成
  client = FirebaseAdmin.new

  begin
    User.transaction do
      # Firebase上にユーザーを作成する
      uid = client.sign_up_user(email:, password:)

      # DB上にユーザーを作成する
      user_record = User.create!(..., email:, uid:)
      ...
      save!
    end
  rescue StandardError => e
    # 例外をキャッチし、ロールバックを実行する
    client.rollback
    raise(e)
  ensure
    # スタックのクリーンナップを必ず実行する
    client.cleanup
  end
  ...
end

最後に

プレックスではエンジニアを募集しております。ご興味ある方がいらっしゃれば是非連絡をください! dev.plex.co.jp

また、入社エントリを執筆いたしましたのでご覧いただけるととても嬉しいです!

product.plex.co.jp