クラウドワークス エンジニアブログ

日本最大級のクラウドソーシング「クラウドワークス」の開発の裏側をお届けするエンジニアブログ

チーム横断でプロダクト改善!「合同スプリント」での取り組みについて紹介します

クラウドソーシングサービス「クラウドワークス」(以下crowdworks.jp)のエンジニアをしている原です。2024年4月に新卒で株式会社クラウドワークスに入社し、今年で2年目です。

はじめに

crowdworks.jpの開発現場では、昨年より半期に一回のペースで「合同スプリント」という取り組みを行っています。

これは、まとまった時間がないとできないタスクやチーム外の知見が必要なタスクなどをチーム横断で行う活動です。合同スプリント期間中は普段のチームの施策は一旦ストップし、取り組むテーマごとにチームを新しく編成します。

今期も4/7から4/18にかけての2週間行われました。

今回の合同スプリントの取り組み

今回の合同スプリントのタスクは、以下のようにさまざまでした。

  • 技術的負債解消(Gemの削除、リファクタリングなど)
  • CIの高速化
  • フロントエンド開発ツールのアップデートや剪定
  • フロントエンドの機能剪定に向けた調査
  • 検索エンジンOpenSearch)のパフォーマンスチューニング
  • Rollbarから飛んでくるアラート通知の精査
  • 仕事検索機能の改善に伴う調査

今回の合同スプリントには私が普段所属しているデリバードチーム※のエンジニア4人も全員参加したので、各メンバーに自身の取り組みの内容について振り返ってもらおうと思います。

デリバードチームに関しては下記の記事をご覧ください!

engineer.crowdworks.jp

.rubocop_todo.ymlの消化(エンジニア原)

まず私は.rubocop_todo.ymlの消化を行いました。

これは、.rubocop_todo.ymlで無視(Exclude)するよう設定してあるファイルについて、RuboCopで警告されなくなるよう修正し、最終的に.rubocop_todo.ymlから削除するというものです。

ただし、プロダクションコードを修正するとサービスに影響が出てしまう可能性があるので、今回はテストコードのみの修正にとどめました。なおかつ、そこから比較的簡単に修正できるものを先輩エンジニアがピックアップしてくださり、ひたすらそれを消化していきました。

今回はこの.rubocop_todo.ymlの消化に数名で取り組み、期間中に約700行ほど削除することができました!

全体的にかなり地道な作業ではあったものの、RSpecの書き方を学べたり、知らないメソッドや記法に出会ったり、普段の業務で触らない領域のコードを読めたり、学びのある時間になったと感じます。

合同スプリントでは普段一緒に仕事をする機会のない方からレビューをもらったり、また自分も色々な方のプルリクエストを見たりするので、新たな知見が得られとても勉強になります。

後半はRubyKaigiと日程が重なったため、合同スプリントへの参加は実質1週間のみとなってしまいました。来期も開催されるようであれば、フルで参加したいと思います。

engineer.crowdworks.jp

OpenSearchのパフォーマンスチューニングをやってみた(エンジニアkeigo0331)

エンジニアのkeigo0331です。合同スプリントでは、OpenSearchのパフォーマンスチューニングに取り組みました。

きっかけは前期に実施したElasticsearchからOpenSearchへの移行対応で、OpenSearchへのデータインポート処理に非常に時間がかかっておりました。ただ、普段の業務でOpenSearchに十分な時間を割くのは難しかったため、今回は合同スプリントを活用して集中的に取り組むことにしました。

OpenSearchへのインポート処理は以下のような構成です。

  1. MySQLからデータを取得
  2. Railsでインデックスのマッピングに合わせてデータ整形
  3. OpenSearchのBulk API形式でHTTPリクエストを送信

対象データ数は、最大で1インデックスあたり450万件超。本来であれば「そもそもそこまで必要なのか?」という要求レベルから見直すのが理想ですが、今回は諸事情もあり、パフォーマンス改善に絞って対応しました。

ボトルネックの特定

まずは処理の全体像を把握し、ボトルネックの洗い出しから始めました。使用した手法は以下の通りです。

  • SQLログを確認し、スロークエリやN+1の有無などをチェック
  • EXPLAINでSQLの実行計画を確認
  • Stackprofを使ってプロファイリング
  • OpenSearchリクエストのサイズや処理量を検証

その結果、以下のような課題(一部抜粋)が見つかりました。

  • インデックスが貼られていても実際には使われず、フルスキャンされているSQLがあった
  • メモリを意識して1000件ずつループ処理していたが、該当のフルスキャンされるSQLが毎回実行されていた
  • ActiveRecordのPreloadやデータ処理が意外に重かった
  • 各サーバー側のリソースにはかなり余裕があった

実施したチューニング

上記のボトルネックをもとに、以下のような対応を行いました。

  • フルスキャンSQLの改善
    • インデックスを正しく活用するなど見直し、30秒かかっていた処理が約1秒に短縮
  • バッチサイズの最適化
    • サーバーリソースに余裕があったため、段階的にバッチサイズを拡大。最終的に4000件が最もスループットが高かった
  • 並列処理の導入
    • ActiveRecord処理が遅かったため、余っているサーバーのリソースを活かすために並列数を徐々に増加。最終的に並列数4で最大効率を確認
  • MySQLクライアントによる直接SQL実行(試験)
    • 試験的にActiveRecordを使わず直接クエリを投げたところ、スループットが5倍に
    • 保守性・可読性の観点から採用は見送ったが、今後ユーザー数が増え細部のチューニングでは太刀打ちできなくなったときに有効な選択肢になりそう

個人的な学びと感想

日頃からパフォーマンスも意識してコードを書いているものの、実際には保守性や可読性を優先してコードを書くことが多く、ここまでパフォーマンス改善に取り組む機会はありませんでした。

今回チューニングしたコードは今から7年前に書かれたもので、当時と比べるとユーザー数は一桁以上違います。正直当時に書かれたコードが今の環境に合っていないだけで、むしろ当時においては最適だったと思います。

こうした「ユーザー数の増加に伴って、コードの最適解が変わる」ことを体感できたのは、非常に貴重な経験でした。スケールするサービスを扱うことでしか得られない学びだと感じます。あらためて、合同スプリントは素晴らしい取り組みだと思いますし、今後も積極的に活用していきたいです。

「仕事のおすすめ機能」のロジック調査とドキュメント整備(エンジニア駒井)

エンジニアの駒井です。合同スプリントでは主に2つのタスクを進めました。1つが「仕事のおすすめ機能」のロジック調査とドキュメント整備です。

このタスクは、プロダクトオーナー(PO)から「仕事のおすすめ機能の改善を検討したいが、現状の仕様が不明確」と相談を受けたことがきっかけです。

調査対象となった「仕事のおすすめ機能」は、ユーザーの仕事の閲覧・応募履歴やプロフィール情報を元に仕事をスコアリング・並び替えして表示する仕組みですが、実装当時から何度か改修が加えられており、現在のロジックがブラックボックス化していました。

具体的には以下のようなことを行いました:

  • レコメンドロジックのコードを追跡し、どのような入力(ユーザー属性、過去の閲覧・応募履歴など)が使われ、どのようにスコアが計算されているかを整理
  • 既存のドキュメントが数年前の状態で止まっていたため、現行仕様に合わせて更新

この調査により、今後の改善施策の方針検討をより精度高く行う土台を整えることができました。また合同スプリントのようにまとまった時間があると、ドキュメント整備もしやすくてありがたいと改めて感じました。

Storybook上のフォント指定漏れの修正

加えてStorybook上のフォント指定漏れの修正も行いました。

私たちのプロジェクトでは、Vue.js + TypeScriptで構築されたUIコンポーネントをStorybookで管理・ドキュメント化しています。

最近、意図しないスタイル継承(例:親コンポーネントのfont-familyやcolorがそのまま適用されてしまうなど)を検知するための仕組みとして、適切にfont指定がされていないコンポーネントに対してStorybookのプレビュー上で文字色をピンク色にする設定を行いました。

技術的にいうとbodyタグに対して任意のfont-familyやcolorなどのstyleを指定したCSSファイルを用意し、.storybook/preview-head.htmlにてlinkタグを配置して読みこませることで実現しています。

// .storybook/preview-head.html

<link href="css/storybook-settings.css" media="all" rel="stylesheet" type="text/css" />
/* css/storybook-settings.css */

@font-face {
  font-family: StorybookFont;
  src: url('/font/k6x8_gothic.woff2');
}

body {
  /* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
  font-family: StorybookFont;
  color: #f0f;
}

サブタイトルが入るコンポーネントの例でみると、fontが適切に指定されていない場合、下記のようにStorybook上ではピンク色になっていることがわかります。

具体的な対応内容は以下の通りです:

  • font-familyやcolorが未指定のケースに対して明示的に指定を追加
  • 同時に、意図的に親からフォントを継承させたいケース(例:子コンポーネントが汎用的なContainerである場合など)は除外するよう調整

この修正により、プロジェクト全体でのUIの一貫性を向上することができました。

Storybookのアップデート(エンジニア服部)

エンジニアの服部です。私は今回Storybookを現行バージョンであるv8.2.9から、可能であれば最新のv8.6.12へのアップデートするための対応を行なっていました。

結論から述べると最新のv8.6.12まですぐに上げることはできず、一旦v8.4系の最新であるv8.4.7へのアップデートを行いました。また弊社ではStorybookを使っている箇所が3箇所あるため、3箇所全てでアップデートの対応を行うようにしました。

バージョンアップの対応に際し、とりあえずいきなりバージョンを上げられたら助かると思い$npx storybook@8.6.12 upgradeを実行すると、

そう簡単にはいきませんね。エラーが出たわけではありませんでしたが、CIを回してみるとVRTで差分がたくさん生まれているようでした。

弊社では、マージ先のStorybookのスクリーンショット画像とプルリクエストで実行されたStorybookのスクリーンショット画像の差分をチェックするためにビジュアルリグレッションテスト(VRT)を利用しているのですが、このVRTにおいて意図しない差分が発生しているようでした。

どの段階から差分が生まれているのかStorybookのバージョンを一つ一つ上げながら確認すると、v8.5.0から発生していることがわかりました。差分が生まれた原因をすぐに特定することはできなかったのですが、v8.5.0のCHANGELOGを見ると

  • A11yアドオンのリファクタによりDOM構造や描画タイミングに依存するVRTに差分が生まれた
  • 実行環境(テストの初期化順序やモジュールの解決)の変化によりレンダリングのタイミングやスタイル反映の差異がVRTに影響

この辺でVRTの差分に影響が生まれていそうな感じはしたので、A11yアドオンの設定を一時的に無効にしてみるなどして引き続き調査をしたいと思います。

おわりに

合同スプリントの雰囲気が伝わったでしょうか。このように、普段同じチームで活動するメンバーも合同スプリントでは別チームに分かれて多種多様な取り組みを行っています。

合同スプリント終了後に行われたふりかえりのミーティングでは、以下のようにポジティブな意見が多く出ていました。

  • モブ作業が楽しかった
  • 普段触らない領域に関われた
  • 違うチームの人の意見ややり方を知れた

モブは楽しい 違うチームの方の意見ややり方を知れた 普段触らない部分を覗くことができた 細かいタスクとはいえ、普段あまり触れないフロントエンドをさわれて楽しかった〜

次回開催についても、「希望する」という声が多く聞かれました。

次回の開催は未定ですが、次はなにができそうか、時間があったらどんなことをしたいか、普段からアンテナを張って考えておこうと思います。

We're hiring!

株式会社クラウドワークスでは、エンジニアメンバーを募集しています!

herp.careers

© 2016 CrowdWorks, Inc., All rights reserved.