シンクロ・フード エンジニアブログ

飲食店ドットコムを運営する、株式会社シンクロ・フードの技術ブログです

Aurora MySQL v3 (MySQL 8.0 互換) へのアップグレード対応

はじめまして、SRE チームの村山です。

今回、弊社の主要サービスで使用している Aurora MySQL のアップグレードを行いましたので、対応内容や得られた知見についてご紹介したいと思います。

背景

弊社で使用している Aurora は、2022 年 10 月に Aurora MySQL v1 (MySQL 5.6 互換) から Aurora MySQL v2 (MySQL 5.7 互換) へとアップグレードしております。その際の対応については、以下の記事をご覧ください。

tech.synchro-food.co.jp

その後も v2 を使用していましたが、2024 年 10 月 31 日 をもって v2 の標準サポートが終了になるため、Aurora MySQL v3 (MySQL 8.0 互換) へのアップグレードが必要になりました。

docs.aws.amazon.com

変更点の影響調査

アップグレードに向けて、まずはドキュメントを中心に変更点と影響範囲の調査を進めました。 これは SRE チームではなく、Web サービスの基盤部分を横断的に見るアプリケーション基盤チームを中心に実施して頂きました。

この中で対応が必要だと判断し、実施した主な内容を以下に記載します。

デフォルトの COLLATION が utf8mb4_0900_ai_ci になる

MySQL 8.0 では、utf8mb4 のデフォルトの COLLATION が utf8mb4_general_ci から utf8mb4_0900_ai_ci に変更になりました。

Each character set has a default collation. For example, the default collations for utf8mb4 and latin1 are utf8mb4_0900_ai_ci and latin1_swedish_ci, respectively.

これまで DB のテーブル定義 (マイグレーションファイル) には明示的に COLLATION を設定していなかったので、今後新規にテーブルを作成する際は COLLATE=utf8mb4_general_ci を設定するように開発ルールを変更しました。
DB アップグレードのタイミングに合わせ、既存のテーブルも COLLATE を設定するように変更しています。

FLOAT, DOUBLE, DECIMAL タイプのカラムに対して UNSIGNED 属性が非推奨になる

MySQL 8.0.17 で非推奨になったため、ALTER TABLE で対象のカラムから UNSIGNED 属性を削除するようにしました。対象のカラムが多いため、稼働中の DB には ALTER TABLE を適用せず、後述するクローンの DB にだけ適用するようにしています。詳しくは「アップグレード方法」の項に記載します。

As of MySQL 8.0.17, the UNSIGNED attribute is deprecated for columns of type FLOAT, DOUBLE, and DECIMAL (and any synonyms);

SQL_CALC_FOUND_ROWS, FOUND_ROWS() が非推奨になる

MySQL 8.0.17 で非推奨になったため、使用箇所を修正しました。アップグレード前に修正できる内容でしたので、事前に対応しています。

The SQL_CALC_FOUND_ROWS query modifier and accompanying FOUND_ROWS() function are deprecated as of MySQL 8.0.17;

utf8mb3 が非推奨になる

MySQL 8.0 で非推奨になったため、ALTER TABLE で対象のテーブル・カラムを utf8mb4 に変換しました。こちらも後述するクローンの DB にだけ適用するようにしています。

The utf8mb3 character set is deprecated. utf8mb3 remains supported for the lifetimes of the MySQL 8.0.x and following LTS release series, as well as in MySQL 8.0.

予約語利用箇所の対処

MySQL 8.0 より RANK や LEAD の単語が予約語に追加されました。

Reserved words are permitted as identifiers if you quote them as described in Section 11.2, “Schema Object Names”:

既存のテーブルに同名のカラムがあることを下記クエリで確認しましたが、アプリのライブラリにより引用符で囲む処理をするため、対応は不要の想定でした。

select table_schema, table_name, column_name 
from information_schema.columns 
where table_schema not in ('information_schema', 'mysql', 'sys') 
  and column_name in (select WORD from information_schema.KEYWORDS where reserved = 1)
and TABLE_NAME NOT IN ('ar_internal_metadata');

しかし、後の段階で一部のアプリではその処理がされず、エラーになることが判明したため、最終的にはカラム名を変更する対応をとりました。

GROUP BY 句での ASC/DESC の廃止

MySQL 8.0 より GROUP BY 句で ASC/DESC をつけることができなくなりました。後述する「テスト」の段階で、各サービスの開発担当者にて使用箇所を修正して頂きました。(昔からある一部の機能で利用されていました)

The deprecated ASC or DESC qualifiers for GROUP BY clauses are removed. Queries that previously relied on GROUP BY sorting may produce results that differ from previous MySQL versions. To produce a given sort order, provide an ORDER BY clause.

上記のほか、アップグレードにより変更されたパラメータグループの設定値の調査や検討なども行いました。

アップグレード方法

アップグレードの方法は、前回のメジャーバージョンアップ時と同じく、binlog レプリケーションを使った Blue-Green Deployment を採用しました。採用の理由は、実績があり手順が確立できていること、リリース当日のメンテナンス作業の時間を短くできること、切り戻しを容易にできることです。

具体的な手順は以下の記事を参考に検討しました。

aws.amazon.com

最初に検討した手順の大まかな流れは以下のとおりです。
※ テストで不備がみつかり修正したため、実際の手順とは異なります。詳しくは「アップグレード手順のテスト」の項に記載します

検討時のアップグレード手順

  1. 既存の Aurora MySQL v2 からクローンを作成する
  2. クローンを v3 へインプレースアップグレードする
  3. v2 から v3 へレプリケーションする
  4. v3 へ ALTER TABLE を実行する
  5. v2 から v3 へのレプリケーションを停止する
  6. アプリケーションの接続先を v3 へ切り替える
  7. v3 から v2 へレプリケーションする

手順 1 ~ 4 は事前に実施し、5 ~ 7 をリリース当日のメンテナンス作業時に実施します。そうすることで、リリース当日の作業時間を短くすることができます。
「変更点の影響調査」の項に記載した ALTER TABLE は、手順 4 のタイミングで v3 にだけ実行します。この時点でアプリケーションは v2 へ接続しているため、ALTER TABLE を安全に適用できます。
手順 7 は、アップグレード後に v2 へ切り戻せるようにするため実施します。

なお、RDS がサポートする Blue-Green Deployment の方法も候補にありましたが、切り戻しの容易さから前述の方法をとることにしました。

docs.aws.amazon.com

テスト

開発環境にてアップグレード手順のテストと、アップグレード後のアプリケーションのテストを行いました。

アップグレード手順のテスト

「アップグレード方法」の項で述べた手順で進めましたが、手順 4 の ALTER TABLE 実行後、v2 から v3 へのレプリケーションでエラーが発生してしまいました。

Column 17 of table 'テーブル名' cannot be converted from type 'varchar(765(bytes))' to type 'varchar(1020(bytes) utf8mb4)'

utf8mb4 へ変換する ALTER TABLE を v3 側だけに実行していたため、データ型の不一致が原因のようです。これを許容してレプリケーションできるようにするため、v3 側のパラメータグループで replica_type_conversions を設定するようにしました。
今回は source (v2) よりも target (v3) 側の型が大きいため、非不可逆変換を許可する ALL_NON_LOSSY の値を設定します。設定値の詳細は以下をご参照ください。

設定後、v2 から v3 へのレプリケーションがエラーにならないことを確認できました。
ただここで、手順 7 の v3 から v2 へのレプリケーションにおいても同じ問題に遭遇することが予想できました。型の大小関係が逆になるため、不可逆変換を許可する ALL_LOSSY を設定する必要があります。しかし、値が切り捨てられデータの不整合が生じる可能性があるため、他の方法を検討することにしました。

考えた方法は、v2 切り戻し用のクラスターを別に作成し、クラスター 3 台でレプリケーションする構成です。

実際のアップグレード手順

具体的な手順は以下になります。

  1. 既存の Aurora MySQL v2 からクローンを 2 つ作成する (v3 用と切り戻し用)
  2. 片方のクローンを v3 へインプレースアップグレードする
  3. v3 から切り戻し用の v2 へレプリケーションする
  4. v2 から v3 へレプリケーションする
  5. v3 へ ALTER TABLE を実行する
  6. v2 から v3 へのレプリケーションを停止する
  7. アプリケーションの接続先を v3 へ切り替える

切り戻し用の v2 には、v3 からのレプリケーションにより ALTER TABLE が反映されているため、型の不一致が起こりません。そのため、ALL_LOSSY を設定することなくレプリケーションすることができました。
また副産物として、v3 から切り戻し用の v2 へのレプリケーションを事前に実施できるため、リリース当日の作業を 1 つ減らすこともできています。

アプリケーションのテスト

上記の手順で開発環境を v3 へアップグレードした後、各サービスの開発担当者にてアプリケーションのテストを実施して頂きました。テストコードや、前回のバージョンアップ時に作成した実行計画を比較する機能を活用してテストをしています。

テストにより修正した内容の 1 つに、パラメータグループの optimizer_switch の変更があります。
実行結果が変わってしまうクエリが見つかったため、実行計画を確認するなどして調査を進めました。試行錯誤の結果、セミジョイン最適化の duplicateweedout の動きが原因ではないかと考え、開発環境で optimizer_switch の duplicateweedout を off にしたところ、問題が解消しました。
バグレポートをみると、duplicateweedout 含めセミジョイン関連のバグがいくつか報告されていました。ただ今回のテストでは、セミジョインに起因する他の問題は起こらなかったので、duplicateweedout だけ off にする方針にしています。

その他にも問題のあった箇所は、各開発者にて修正をして頂きました。

リリース

テストが完了した後、リリースへ向けた対応を進めました。
まずリリースの前週に「アップグレード手順のテスト」の項に記載した手順の 5 までを実施します。v3 へのインプレースアップグレードは約 15 分、ALTER TABLE の実行は約 15 分で完了し、作業全体は 70 分程で完了しました。

そしてリリース当日のメンテナンス中に手順 6, 7 を実施しました。正確にはレプリケーションの同期確認やアプリのデプロイ作業も含むため、メンテナンス時間は 2 時間設けていましたが、ほぼ予定時間内に作業を完了することができました。
ちなみにレプリケーションの同期確認では、各 DB のレコード件数が一致することの確認をしています。テーブルごとに SELECT COUNT をしているのですが、処理を並列化することにより 10 分程で完了しました。 直列で行なうと 1 時間以上かかることがわかっていたため、作業時間の短縮に繋がりました。

リリース後の修正

v3 へのアップグレード後、v2 へ切り戻すほどの致命的なエラーはありませんでしたが、いくつか問題が発生しました。
ひとつは、パフォーマンスが極端に劣化した SQL の問題です。実行時間が著しく伸び、DB の負荷も増大したため急ぎ修正する必要がありました。
その主な原因は、「テスト」の項でも述べたセミジョインによるものでした。
MySQL 8.0 では内部のオプティマイザに変更が多く、サブクエリを使う SQL において v2 ではセミジョインが使われなくても、v3 だとセミジョインが使われるケースが多くあります。
オプティマイザの不具合なのか、セミジョインを使うと速度が大きく劣化するようなケースにもかかわらず、セミジョインを利用してしまうようなこともありました。
具体的な例を示します。以下は同一の SQL について、セミジョインを on(デフォルト) または off にしたときの、EXPLAIN FORMAT=TREE の実行結果になります。(最初の 2 行のみ抜粋)

optimizer_switch: semijoin=on のケース

-> Aggregate: avg((db_1.table_1.column_1 / (case when (((to_days(db_1.table_2.column_1) - to_days(db_1.table_2.column_2)) / 30) < 1) then 1 else round(((to_days(db_1.table_2.column_1) - to_days(db_1.table_2.column_2)) / 30),0) end)))  (cost=7839673608.28 rows=6050893371)
    -> Nested loop semijoin  (cost=7234584271.20 rows=6050893371)

optimizer_switch: semijoin=off のケース

-> Aggregate: avg((db_1.table_1.column_1 / (case when (((to_days(db_1.table_2.column_1) - to_days(db_1.table_2.column_2)) / 30) < 1) then 1 else round(((to_days(db_1.table_2.column_1) - to_days(db_1.table_2.column_2)) / 30),0) end)))  (cost=302837.05 rows=618503)
    -> Nested loop inner join  (cost=240986.72 rows=618503)

セミジョインを利用する場合だと、rows=6050893371 という内部テーブルができてしまっています。実際にこれが原因で、SQL の実行時間が著しく伸びてしまっていました。
このようなケースに対処するためには、SQL を書き換えたりして局所的にセミジョインを回避する必要がありました。オプティマイズヒントを使って無効化する方法も把握していましたが、フレームワーク経由で SQL が実行される都合上、SQL を書き換えることで対応し、解決することができました。

他には、バッチ処理で以下のエラーが発生した問題がありました。

The table '/rdsdbdata/tmp/#sqlxxx_xxx' is full

これは MySQL 8.0 から一時テーブルの動作が変更されており、それに起因するものでした。リソースの制限値を超えてしまったことによるエラーのため、パラメータグループから temptable_max_ram と temptable_max_mmap の値を調整することで解決することができました。

docs.aws.amazon.com

その他にもいくつかの問題が発生しましたが、内容に応じて各サービスの開発者や SRE チームにて修正を行っております。

リリース後のパフォーマンス

v3 へのアップグレード後、主要サービスの全体的なレスポンスタイムは大きな変化がありませんでした。
先に述べたパフォーマンスが劣化する SQL の影響があり、リリース日は悪化していましたが、修正によりその後は元に近い値となっています。
現状、v3 の仕様や追加機能にあわせたチューニングはしていないので、大きく改善することもなく変化は少なかったのかなと考えています。

レスポンスタイムの状況

まとめ

各チームの開発者とチーム横断で協力して対応した結果、無事に Aurora MySQL v3 へのアップグレードを完了することができました。
本記事が Aurora アップグレードへ取り組む方にとって、少しでもお役に立てれば幸いです。

RubyKaigi 2024に参加しました

はじめまして、開発部の大庭です。
普段はRuby on Railsを用いたアプリケーション開発を行なっています。

今回、那覇で開催されたRubyKaigi 2024に参加してきましたので、印象に残ったセッションや感想をお伝えしたいと思います。

なお、弊社はSilver SponsorとしてRubyKaigi2024に協賛させて頂きました。

rubykaigi.org

印象に残ったセッション

Day1 Keynote 「Writing Weird Code」

発表資料はこちらから確認できます。

初日の最初のセッションで、初参加の私はRubyKaigiの洗礼を受けました。
一見して複雑で実行不可能に見えるコードが、実際には文法として有効で実行可能であること、さらにはQuine(自身のプログラムを出力する性質)であるプログラムが紹介されました。

普段の業務では、読みやすく簡潔なコードを目指していますが、このセッションではその対極にある世界を垣間見ることができ、Rubyの奥深さに衝撃を受けました。

このセッションは衝撃の連続であり、正規表現を用いて連立方程式の解を返すプログラムや、Rubyのプログラムとして実行可能な画像など、Rubyの高い表現力や拡張性に大変驚きました。

帰宅してから、セッションで取り上げられていたプログラムを実行してみたのですが、実行結果を見てただただ驚くばかりでした。 改めてまじまじと見ても、何が書いてあるのか、何故動くのかがまったく理解できませんでした。

github.com

次回のRubyKaigiでは、このようなコードが集まるTRICK(超絶技巧 Ruby 意味不明コンテスト)というも催しも開催されるようで、非常に楽しみです。

Day1 「Unlocking Potential of Property Based Testing with Ractor」

発表資料はこちら確認できます。

speakerdeck.com

こちらは、Rubyの並列処理機能であるRactorを、テスト手法であるProperty Based Testingに適用し、実行速度の向上を試みたというセッションになります。

Property Based Testingとは、RSpecでテストするようなExample Based Testingに対して、ランダムな値を大量に生成しテストするという手法です。 ランダム値を大量に生成して試行するため、プログラマが予期しなかったバグを発見可能という利点があります。 この手法ではテストを大量に実行する必要があるのですが、それらは独立した処理であるということに注目し、Ractorの適用を試みたという内容でした。

RubyKaigi 2024を通して、個人的に非常に興味を惹かれたセッションでした。
Property Based Testing自体が魅力的な手法で、開発時に手軽に素早く実行できるとより良い開発体験になりそうと感じました。 Integerなどの型をもとに入力データを生成していることから、(近年のRubyのトピックの1つである)型推論と組み合わせることでさらなる手軽さを導入できるのではと思いました。

RubyKaigi 2024を通しての感想

印象に残ったセッション以外にも、言語処理系など普段あまり関わる機会のない分野のセッションも聞きました。 これまで勉強してこなかった分野のため、内容のほとんどは理解できなかったものの、「Rubyの1+1は常に2ではない」(Integer#+の定義を容易に上書きできる)などといった話は印象的でした。 このように、Rubyについての幅広い話題が取り上げられており、そこから新たな考えや発見を得られるのは非常に良い経験でした。

私は外部の勉強会などに参加してこなかったのですが、RubyKaigiに参加してブースや懇親会で、他社のエンジニアさんと交流することができて非常に刺激を受けた良い経験となりました。他社のエンジニアさんを勝手に畏怖していたのですが、実際に交流してみるとそんなことはなく、会社の違いはあれど同じエンジニアなんだなと感じることができました。 また、弊社に在籍していた先輩がLTに登壇されており、こちらも刺激を受けるとともに、登壇(者)を少しだけ身近に感じ、RubyKaigiに限らず機会があれば何か発表してみたいなと思いました。

さらに、各社ブースを巡ってみて、使用しているツールなどのアンケート結果に驚いたり、掲示されているプログラムに付箋でコードレビューする催しなど、セッション以外も非常に面白かったです。

おわりに

非常に熱気のある発表に対して、自分は知らないことだらけで、学習欲・プログラミング欲が刺激される3日間でした。 いつか発表の内容が理解できるように精進していきたいです。

中途入社モバイルアプリエンジニアが入社後4ヶ月で感じたこと

自己紹介

こんにちは、開発部モバイルアプリチームの横山です。

普段は求人飲食店ドットコムのAndroidアプリの開発をしている私ですが、今年の2月に中途入社しました。
社会人になって10年目になります。
新卒でIT業界とは関係のない業界に就職し、コロナの影響など紆余曲折を経て前職でエンジニアとしてのキャリアをスタートしました。
前職は会社ではWebサイト開発とFlutterを使ってiOS・Androidアプリケーションの開発をしていました。

そんな私が入社して4ヶ月を経とうとしている今、入社エントリーとして、転職活動をしていた当時や、入社してからのシンクロ・フードでの経験を振り返り、感じたことを今回お伝えできればと思っています。

なぜシンクロ・フードに転職したか

ネイティブでのアプリ開発に徐々に興味が出てきていた事と、社内でのアプリ開発者が私一人で、自分よりも知見や知識が豊富なエンジニアと共に働き、自身の技術を向上させたいと考え転職を決意しました。

転職の軸として「成長できそうか」「アプリ開発に携われるか」という観点で会社を探していました。
弊社とのカジュアル面談での説明が、他社と比べて技術スタックや会社の評価制度について詳しく説明をしていただき好印象だったことや、アプリ開発の方針や、アプリ開発をクロスプラットフォームのFlutterやReact Nativeで開発するのではなく、Kotlin、Swiftで開発を行っていることなど、今後エンジニアとして働いていくなかで技術力を伸ばしていける理想的な環境が整っていると思ったのでシンクロ・フードを選びました。

また、評価制度がしっかりしてると感じたので、納得して業務を取り組んでいける環境でもあると思いました。

入社前の不安と実際に入社した後の心境

弊社の開発部はフルリモート制度が2023年5月より採用されており、フレックスタイム制で働くことができます。
コアタイムの11:00〜16:00に出勤していれば、フレキシブルタイムの7:00〜22:00の間で好きな時間に業務を行うことができ、月の労働時間が所定の時間を満たすように気をつければ出勤時間と退勤時間は自由にすることができます。
このように、柔軟に働くことができるので、とても魅力的な環境だと思っていたのですが、フルリモート勤務、フレックタイム制どちらとも未経験の私には不安のもとでした。
仕事とプライベートの区別が難しいのではないか、オフラインで直接相談して聞けないことで、チームメンバーとのコミュニケーションや業務の進め方、社内ツールの使い方など上手くやっていけるかなど様々なことに不安を感じていました。

しかし、実際に働き始めてみると、仕事とプライベートの切り替えにはそれほど問題がなく、フルリモート勤務ならではのメリットを感じています。
腰痛があるため前職ではずっと座っていることが辛かったのですが、これを機会に自宅のデスクを昇降式デスクに変更するなど作業環境の改善に取り組み、身体的ストレスが軽減されるようになりました。
私は本社がある東京ではなく関西圏からフルリモート制度を利用して勤務をしてるので、おかげで自宅でも集中して業務に取り組めるようになり作業効率は良くなったと感じています。

現在のデスク

コミュニケーションに関しても、Slack内のチームチャンネルや1on1の場を通じて、気軽に質問や雑談ができる環境が整っているので、不安を感じることなく業務に取り組めています。週1回チームのミーティングでは、雑談の時間があり好きな雑談テーマや、近況などを話すことができます。最近あった話題は「よく見るYoutubeチャンネルやテレビ番組」、「雑学やトリビアなど、ちょっとした知識や発見、タメになったり面白い情報」など業務とは関係ないことなどを業務委託の方々含め談笑していたりします。

また、社内のドキュメントが整備されており、必要な情報をすぐに見つけることができるため、仕事に取り組む上での情報に困ることは今のところないです。あと、オンボーディング期間も十分に設けられ、社内の業務フローやツールの使い方が丁寧に教えてもらえたので、スムーズに業務に取り組むことができる環境だと感じています。

■ オンボーディング期間中の1日の流れ

  • 09:00:メール確認、昨日の復習など
  • 11:00:チームMTG
  • 12:00:お昼休憩
  • 13:00 ~ 17:00:学習、業務フローやサービスの説明
  • 18:00:1on1、メンターとの振り返り

シンクロ・フードに入って感じたこと

一番の大きな印象は、社内ドキュメントがしっかりしている整理されていることでした。
情報共有サービスなどに社内業務の進めかたや、開発のルール、仕様等がまとまっていて、大体の探したいものは検索したらすぐに見つかるので、とても便利で仕事がしていきやすいと感じています。あと入社間もない頃は、大量に覚えないといけないという気持ちが少しあったのですが、検索したら情報が出てくるので、心理的な負担が軽くなった覚えがあります。

esa環境構築などの一覧

あと、社内アシスタントbotの「ナレット」や「さえずり」の開発を行うなど、業務をより便利に、かつ効率よくしていこうという活動や、前職ではなかったドキュメントを作るだけでなく、アップデートをしていこうというチームとしての方針を感じているので、とてもいい環境だなと感じています。私も何かしらアプリ開発やその他の業務に関することを残して貢献していけたらと思っています。

業務でもネイティブでのアプリ開発がしたくて、入社したので現在の開発業務全般にやりがいを感じています。

課題・改善点について

求人飲食店ドットコムAndroidアプリには、技術的にレガシーな部分やフォルダ構成が機能によってバラバラな点があるのでその改善をしていきたいです。(フォルダ構成に関しては現在リアーキテクチャを進めているので時間の問題で解決をするのかなと思うのですが...)

あと、AndroidOS特有のナビゲーションバーの「戻るボタン」をタップした時やジェスチャーナビゲーションの戻る操作を行った際の動作について、もっとこうしたらいいのにと感じる点があるので、そのことが課題かなと思っています。感じた課題に関しては機能改善として提案していきたいです。

これからについて

まだまだ力不足なところもあり十分には貢献できてはいないのですが、求人飲食店ドットコムAndroidアプリの施策機能の実装を進めて行きたいです。求人飲食店ドットコムiOSアプリと比べて、開発が追いついていない状況なので開発を進めていき、使っていただくユーザーに便利なアプリを提供していきたいです。

機能開発以外ではiOSアプリではSwiftUIについて取り組んでいるので、Android側でも徐々にですがJetpack Composeの導入について考えていければといいなと思っています。

便利な社内アシスタントBotのシステム構成を大公開!

こんにちは、開発部モバイルアプリチームの小関です。

普段は求人飲食店ドットコムのiOS・Androidアプリの開発をしている私ですが、昨年4月に設立された「GPTプロジェクトチーム」にも参加しており、この1年でより一般的にも身近になってきた生成AIをサービスや普段の業務に使えないかと模索する仕事もする日々です。

今回は、そんな「GPTプロジェクト」の一環で作成した社内アシスタントBotによって、ナレッジの検索しづらさを解決しようとした事例をご紹介しようと思います。

GPTプロジェクトとは

弊社では2023年4月から、CTO直下に「GPTプロジェクトチーム」を新設して(*1)、ChatGPTのような生成AIを活用する取り組みを進めています。
このプロジェクトでは、弊社が運営するサービスや社内業務に対して生成AI技術を適用し、飲食店の業務効率化や負荷削減を図ることを目的として活動しています。

チーム発足まもない頃は、そもそも生成AIとはどう動いているのだろうといった部分や、ChatGPTなどの生成AIサービスはどういった形で利用でき、どのような場面に適用することができるのかを探るところから行っていました。

生成AI自体は様々な種類がありますが、手始めに取り組むものとしてはやはりChatGPTからだろうとなり、APIの利用方法やLangChainという文章生成に関わるライブラリを調べるところから行い、この1年間でいくつか、ChatGPTを組み込んだツールや機能の開発を行いました。

今回はその中から、社内アシスタントBotを作ったことでナレッジ検索がしやすくなった事例を紹介していきます。

社内アシスタントBot 第一弾「ナレット」

弊社の社内ルールや労務関連の情報をまとめたナレッジはGoogleサイトで管理をしているのですが、ナレッジが存在するかを調べるときに検索ワードが適切に選定することが難しかったり、それらしいキーワードで調べたときに本来見たいはずのナレッジが上位にでてこなかったりする課題がありました。

そんな中、他社の事例でチャットツールを経由したナレッジ検索のやり方が公開されており(*2)、弊社でもそれを参考にして作ることでこの課題をクリアすることができるのではないかという話がチーム内で挙がり、実際に開発に至りました。

そうして作られた社内アシスタントBotは「ナレット」と名付けられ、ナレットに対してメンションをつけて質問を投げかけると、質問に類似したナレッジ記事から必要な箇所を情報として引っ張ってきて、その情報を元に質問に答えてくれるといった、まさにほしかった仕組みが完成したのでした。

ナレットとのやりとり
ナレットとのやりとり

(ナレッジ+キャットからついた名前のとおりに猫キャラを付与しています)

このBotの良い点は、適切なナレッジを検索するためのキーワードがある程度曖昧でも、自然言語で質問をして検索ができるところにあります。

また、参照したナレッジがリンクで提示されるため、そのナレッジにアクセスして、より詳しい情報を得ることもできますし、正しい情報を言っているのかどうかも、参照したナレッジが適切そうかどうかである程度判断することができます。
本当に正しいのだろうか?という回答の際には、大半が関係のないナレッジを参照してしまったことによるもののため、その際には質問の聞き方を変えてみることで、本来知りたかった情報を得られるということもありました。
回答は必ずしも正しいとはいえないため、最終的には質問者がソースまで見て判断することを一応推奨しているのですが、ある程度は正しいナレッジが参照されていれば適切な回答ができるようになっています。

また、回答にフィードバックボタンをつけて意図どおり動作しているかを把握できるようにしています(適切な回答が得られなかった際にGPTプロジェクトメンバーがサポートを行い、質問の仕方をアドバイスしたり、ナレッジがないことで回答できていなければナレッジを作成・更新してもらうよう働きかける想定でつけた機能です)。

ここからはシステム的な話になりますが、実際に組んだ構成は以下のようになりました。

ナレットシステム構成
ナレットシステム構成

中心となっているのは、Slackが提供する Node.jsベースのフレームワークの Bolt for JavaScript を使って構築したアプリケーションで、実験的に動かしたいということもあり、PaaSのherokuを利用して動かしています。

また、今回はChatGPTを利用するにあたり、「LangChain」というChatGPTなどの大規模言語モデル(LLM)を使いやすくするためのフレームワークも併せて利用しています。
このLangChainはTypeScriptかPythonの形で提供されていましたが、社内的にもGPTプロジェクトメンバー的にもTypeScriptのほうが都合が良かったため、Slackアプリケーションも含めて言語はTypeScriptで統一して、このような構成になりました。

全体的な処理の流れとしては、

① Slackでナレット宛のメンションとともに質問をすると、Webhookによってアプリケーションにその情報が送られる
② 質問文をベクトル化する
③ 質問文に近い内容のナレッジがないかを、②のベクトルを使ってナレッジのベクトルDBに対して問い合わせる
④ ③で得た検索結果上位数件のナレッジの情報と質問文を組み込んだプロンプトでChatGPTに回答を作成してもらう
⑤ Slackの質問のスレッドに対して、④を返信する

となっています。

この中にでてくるナレッジが格納されたDBにはPineconeを利用しており、あらかじめGoogleサイトから取得したナレッジデータを一定の文量で区切って(チャンク化)、それをOpenAIのEmbedding APIにてベクトル化したデータを保存しています。
チャンク化するデータサイズは、大きすぎると最終的にChatGPTに渡す情報量が大きすぎることで当時は最大文字数の関係で扱いづらかったり課金額が増えてしまう課題につながり、小さすぎても情報が限定されることで質問に似ているかどうかの検索マッチ度がブレてしまって適切なデータが参照できないため、ここの調整には時間をかけて実験を重ね、ちょうどよいサイズを模索しました。
(最近では、ChatGPTに渡せる最大情報量(=トークン量)がかなり拡張され、課金額も安くなっているので昔よりもこのサイズは大きくしても問題なくなってきています)

こうして作られたナレットですが、単純にナレットを利用することでナレッジが検索しやすくなったこと以外にも、古いナレッジを放置するリスクがより高まったことで、なるべく更新をしようとする動きが働いたこともプラスの一面でした。

ちなみにChatGPTに詳しい方であればお気づきのとおり、このように大量の文章データを元にChatGPTに質問に回答させるようなことは、現在はChatGPT単体で解決できるようになっており、Assistantsという機能で提供されています。
ただし、柔軟なカスタマイズはできないので、ベクトルデータに対してタグを付与することや、引用元のURLを表示したりすることなどは工夫が必要になりそうではあり、今の構成を変えるほどではないとも考えています。

社内アシスタントBot 第二弾「さえずり」

弊社ではナレッジサービス・ツールをいくつか利用しており、部署などによっても使い分けをしています。
その中でも、エンジニア、デザイナー、ディレクターが利用するesa.ioというナレッジサービスがあり、業務に関するナレッジを保存したり、普段の会議での議事録としても利用しています。

そんなesaには、弊社が運営するサービスの知識や、開発における知見・ルールなどがまとめられており、前述のナレットのナレッジデータをesaにしたバージョンを作れないかという話が挙がったため、作成することになりました。

アプリケーションの全体的な仕組み自体は流用ができ、ナレッジを保存するベクトルDBの内容を社内ナレッジではなくesaの記事に変えるだけで済むといった具合に、比較的に容易に完成に至り、この新しいBotにはesaのサービスキャラクターの鳥から連想して「さえずり」という名前が付けられました。

さえずりとのやりとり
さえずりとのやりとり

ただ、社内ナレッジはGoogleサイトの都合上、手動でデータを定期的に更新しているのですが、社内ナレッジの更新頻度が低いためにある程度許容できた部分でした。
esaに関しては基本的には毎日誰かがいろいろなナレッジを最新化したり、新規記事作成を行っており、更新間隔は短いほうが好ましかったことと、esaそのものにWebhookの機能があったため、更新や新規作成などをリアルタイムに受け取ってDB側も更新できそうだというところで、ナレットよりも少し広がったこのようなシステム構成となりました。

さえずりシステム構成
さえずりシステム構成

やはり、労務などのルールに関する疑問よりも実業務における疑問のほうが発生しやすいこともあり、ナレット以上にさえずりは利用されているのを見かけます。
また、ナレットが任意のチャンネル内でしか利用できないのに対して、さえずりはDMでクローズドに質問ができる点も良いところのため、ナレットも同様にDMで利用できるようにすることを検討しています(DMでのやりとりの際にはログが残らないようにも設定をしています)。

おわりに

GPTプロジェクトではこの1年で他にも、求人@インテリアデザインにおける職務経歴例自動生成機能のリリース(*3)に協力したりと、ChatGPTを活用した試みをいくつか行ってきました。

最近は、Claude 3というLLMがGPT-4を超えているのではないかと話題になっていたり、まだまだこの領域は目まぐるしく進化をしている毎日ですが、プロジェクトチームではChatGPTのみに限らずに多くの生成AIの活用を模索していきたいと考えています。
この1年は小さい改善を中心に検証を行って、社内でのノウハウも少しずつ溜まってきたため、今後はLLMを利用した大きい改善に挑戦していくフェーズになっていきます。
これからも飲食に関わる方々へ、より価値を提供できるような開発を行っていきたいと思います。

Four Keys によるシンクロ・フード開発チームの生産性可視化

はじめまして、シンクロ・フード開発部の眞田です。

今回、シンクロ・フードにおける開発生産性向上活動の一環として開発部 各開発チームのFour Keysを計測しパフォーマンスレベルを比較しました。Four Keysの指標値やデータの分布などを比較していくと各開発チームごとの差異が見られ課題も見えてきましたので、ご紹介しようと思います。

Four Keysの概要

Four Keysは、GoogleのDevOps Research and Assessment(DORA)チームの研究・調査により示されたソフトウェア開発チームのパフォーマンスを表す指標で、4つの指標と指標それぞれに対する4つのレベルで開発チームのパフォーマンスレベルを分類するものです。最近の開発生産性に関する議論ではFour Keysがよく参照されており、さまざまなソフトウェア開発企業でFour Keysが開発生産性の指標として採用されています。

開発チームのパフォーマンスを見るための4つの指標は下記となります。

指標 内容
デプロイ頻度 本番環境への正常なリリースの頻度
変更リードタイム 本番環境稼働への修正着手(First Commit)からリリースまでの所要時間
変更失敗率 本番リリースの内で障害が発生する割合(%)
サービス復元時間 本番リリースで発生した障害から回復するのにかかる時間

上記の各指標の値がどの範囲に含まれるかにより組織・開発チームのパフォーマンスをElite / High / Medium / Lowというレベルに分類します。

Elite High Medium Low
デプロイ頻度 適時(1日複数) 1回/日〜1回/週 1回/月〜1回/6ヶ月 1回 / 6ヶ月以下
変更リードタイム 1時間以内 1日 〜 1週間 1ヶ月 〜 6ヶ月 6ヶ月以上
変更失敗率 0% 〜 15% 16% 〜 30% 16% 〜 30% 16% 〜 30%
サービス復元時間 1時間以内 1日以内 1日〜1週間 6ヶ月以上

Four Keysを用いる理由づけとしては一般的に下記のようなことが言われており、シンクロ・フードでの開発生産性を考えるスタート地点として適切であると判断し、今回Four Keysを計測することとしました。

  • ビジネス面への好影響・関連が示されている
    • Four Keysを提案したレポートにて企業の競争優位性とFour Keysに優れた開発チームを持つことの関連性が確認されているため、開発会社として取り組むべき意味付けや意義がビジネスサイドにも理解してもらいやすい
    • エンジニアにとっても、Four Keysは最近のソフトウェア開発におけるトレンドに沿っているので、改善に取り組むことの意味を理解してもらいやすい
  • 車輪の再発明をしなくて済む
    • 多くの関係者が長期間に携わった(過去 7 年間で3万人を超える世界中の専門家が参加している)成果である
    • Four KeysはアジャイルやDevOpsなど最近のソフトウェア開発に適用しやすい指標である
  • 社内・社外の開発チームとの比較ができる
    • Four Keysのパフォーマンスレベル「Elite」「High」「Medium」「Low」により、他の開発チームと比較して自分達がどのレベルにいるのかを客観的に把握できる
    • それぞれの組織や開発チームに適合する開発生産性指標は様々あると思われますが、まずはFour Keysから始めれば良いのではないかと思われる

今回の計測におけるFour Keys定義

インターネットで調べてもFour Keysの指標値の算出に関する厳密な定義はされておらず、Four Keysを採用する開発企業にて少しずつ異なる定義でFour Keysの計測がされているようです。今回、シンクロ・フードでのFour Keys計測における各指標値の定義は下記としました。(集計期間は2023年4月から2023年12月としています)

指標 定義
変更リードタイム 該当期間に本番リリースされたチケットに紐づく(複数の)PullRequestの中で最も古いCommit(First Commit)のタイムスタンプから本番リリースのタイムスタンプまでの営業日日数
デプロイ頻度 ある週内で、日毎に本番リリースされたチケット件数の中央値
変更失敗率 2023/4 - 2023/12に本番リリースされた全てのチケット件数中でバグ対応と分類されたチケット件数の割合
サービス復元時間 2023/4 - 2023/12に発生したバグ対応チケットについて、修正に要した日数(チケット起票日から本番リリースまでの日数)の中央値
  • ここでの変更失敗率・サービス復元時間はFour Keysとしては参考値として捉えています。 Four Keysの定義に基づくと、該当期間の本番リリースに起因して発生したバグに対する各指標値を算出すべきですが、正確な記録が現状では残っていないためチケット管理システムにバグとして記録されているものから集計できる値を算出しました。

Four Keys計測のためのデータ取得

シンクロ・フード 開発部では、ソースコード管理にはGitHub Enterprise、開発チケット管理には社内で内製した独自システムを使用しており、今回のFour Keysの算出に際してはそれぞれから必要な情報を抜き出し、各指標値を算出しました。

チケット管理システムからの情報取得

チケット管理システムには、チケット登録日、本番環境リリース日などが管理されており、Four Keys計測のためにチケット管理システムからは下記の情報を取得しました。

  • チケットの起票日
  • チケットの本番環境リリース日
  • 対象期間における本番リリースされたチケット数
  • 対象期間にバグとして登録されたチケット数

GitHubからの情報取得

GitHub Enterpriseからは、チケットに紐づけている複数のPullReqestに記録されてるCommitから最も古いものを特定し、そのタイムスタンプをチケットのFirst Commitタイムスタンプとして取得しました。(GitHubからのデータ取り出しにはGitHub API (GraphQL)を用いました)

シンクロ・フードの開発チーム

シンクロ・フードにはサービスの開発・運用保守を行っている開発チームが大きく4チームあり、各チームの概要は下記のようになります。今回のFour Keysの計測では、それぞれのチームそれぞれで指標値を算出しました。

チーム 担当サービス
チームA 求人飲食店ドットコム関連サービスの開発・運用保守
チームB 求人飲食店ドットコムサービスに関連する業務システムの開発・運用保守
チームC 飲食店ドットコムサービスの開発・運用保守
キッチンカーやアパレルカーなど移動販売事業者に、出店場所の提供や出店・運営の支援サービスであるモビマルの開発・運用保守
チームD 店舗デザイン.COMなど飲食店出店時の内装を依頼するデザイン会社を探すサービスの開発・運用保守

シンクロ・フード各開発チームのFour Keysによるパフォーマンス比較

開発チームのFour Keysパフォーマンスレベル

各開発チームのFour Keys指標値を算出した結果は下記となりました。

チーム 変更リードタイム デプロイ頻度 変更失敗率 サービス復旧時間
チームA 9 営業日 1回/日 24% 1 営業日
チームB 5 営業日 1回/日 15% 3 営業日
チームC 7 営業日 3回/日 16% 10 営業日
チームD 7 営業日 1回/日 20% 2 営業日

この各開発チームのFour Keys指標値から判別されたパフォーマンスレベルは、下記のように全開発チームHighとなりました。Four Keysのパフォーマンスレベルをみるだけでは各開発チームの違いが見られませんでした。

Four Keysの定めるEliteパフォーマンスレベル以外の各レベル間の指標値レンジ幅が大きいため、開発チーム間の違いが現れにくくなっているように感じます。加えて、レンジの指標境界に隙間があり、指標値が隙間に位置する場合にはどのレベルかの判断しにくかったです。今回、シンクロ開発チームでも指標値がレベルの隙間に位置する場合もありましたが、下位のパフォーマンスレベル境界値との差を考慮してHighと判断している箇所があります。

チーム 変更リードタイム デプロイ頻度 変更失敗率 サービス復旧時間
チームA High High High High
チームB High High High High
チームC High High High High
チームD High High High High

インターネット上には自社開発チームのFour Keysパフォーマンスを公開されている企業も多くあり、その中にはある指標値に関してはEliteパフォーマンスレベルであったという報告も散見されます。シンクロ・フードはパフォーマンスレベルとしては総じてHighであり悪い結果ではないと考えられますが、まだまだパフォーマンス改善の余地があると感じました。

とはいえ、リードタイムのEliteレベルの定義をみると、修正着手から本番リリースまでの時間が1時間以内となっており、シンクロ・フードの開発フローや開発環境を鑑みるとその実現はあまり現実的ではないと捉えています。このことから、一律にEliteレベルを目指すというのではなく、適宜Four Keysの指標値を改善していくというのがシンクロ・フードとしての適切な改善スタンスだろうという見解になっています。

Foue Keys指標値による比較

パフォーマンスレベルは全ての開発チームでHighレベルとなりましたが、各開発チームでの違いを見てみるためにFour Keys各指標値を直接比較してみました。

各チームのFour Keys指標値(再掲)

チーム 変更リードタイム デプロイ頻度 変更失敗率 サービス復旧時間
チームA 9 営業日 1回/日 24% 1 営業日
チームB 5 営業日 1回/日 15% 3 営業日
チームC 7 営業日 3回/日 16% 10 営業日
チームD 7 営業日 1回/日 20% 2 営業日

Four Keys指標値を横並びで比較すると4チームでの課題が見られます。

チーム Four Keysによる評価
チームA 4チーム中で最もパフォーマンス改善の必要性があるように見える
変更リードタイム・変更失敗率・デプロイ頻度が他のチームの中では低いが、サービス復元平均時間は最良
チームB 4チーム中ではパフォーマンスは良好
変更リードタイム・変更失敗率が4チーム中最良
チームC デプロイ頻度は4チーム中で最良、一方でサービス復元時間については最下位
サービス復元時間についてパフォーマンスが課題と見られる
チームD 4チームの中ではいずれの指標もパフォーマンスとしては中位
ただし、変更失敗率が課題と見られる

より詳細な比較

さらに、Four Keys指標値だけでなく、各開発チームのリードタイム・デプロイ頻度データの分布からチームパフォーマンスを比較しました。

変更リードタイムの度数分布

各開発チームのリードタイムの度数分布は下記のようになりました。横軸は変更リードタイム(営業日)で、縦軸は度数です。変更リードタイム度数分布の評価観点としては、原点付近に度数が集まっている方がパフォーマンスが良好(全体的に変更リードタイムが短い)であると捉えられるため、中央値と80パーセンタイル値がなるべく小さい方がパフォーマンスが良好という評価をしました。

変更リードタイムの度数分布
変更リードタイムの度数分布

デプロイ頻度の週毎の分布

各開発チームのデプロイ頻度の分布は下記のようになりました。横軸は2023年4月から12月までの各週で、縦軸は該当週の各営業日のデプロイ回数の中央値です。評価にはデプロイ頻度の平均値と標準偏差を算出し、平均値がより大きく(デプロイ頻度が多い)、標準偏差の小さい(平均値付近の頻度で安定してデプロイできている)ものをパフォーマンスが良好という評価をしました。

デプロイ頻度(週毎)
デプロイ頻度(週毎)

チーム デプロイ頻度平均値 デプロイ頻度標準偏差
チームA 0.86 0.68
チームB 0.74 0.69
チームC 3.37 1.22
チームD 1.47 0.88

このグラフ分布から変更リードタイム・デプロイ頻度の状況をまとめると下記のようになりました。

チーム 変更リードタイム評価 デプロイ頻度評価
チームA リードタイム2週間以内が全体の半数
1.5ヶ月以上要している要件が10%強
平均的に週0.2〜1.6回程度のデプロイ頻度
チームB リードタイム1週間以内が全体の半数
1.5ヶ月以上要している要件は5%強
平均的に週0〜1.4回程度のデプロイ頻度
チームC リードタイム1週間強が全体の半数
1.5ヶ月以上要している要件は5%
平均的に週2〜4回程度のデプロイ頻度
チームD リードタイム1週間強が全体の半数
1.5ヶ月以上要している要件は10%弱
平均的に週0.6〜2.4回程度のデプロイ頻度

変更リードタイムのチーム比較

変更リードタイムのデータの分布から、チームBの中央値が他のチームと比較して最も良く(80パーセンタイルも2番目に良い) 、次いでチームC、チームD、チームAというパフォーマンス順であるように見えます。

チームBの開発内容としては、比較的規模の大きな案件を定常的に数件開発しつつ細かい機能改修や不具合対応などを多く対応しており、前者はリードタイムが長い傾向にありますが後者のパフォーマンスが良好であることが現れていると考えています。チームAに関しては、サービスとしては機能も多くコードベースも大きいことから、影響範囲も大きくなりがちで改修・テストに時間がかかりリードタイム・デプロイ頻度に影響しているように思われます。チームCとチームDは中央値は同じですが、80パーセンタイルはチームCの方が小さいため、チームDよりもチームCの方パフォーマンスが良いと判断しています。

デプロイ頻度のチーム比較

デプロイ頻度からは、チームCが最もパフォーマンスが良い結果となりました、その他のチームはFour Keys指標値のみでの評価では同じように見られますが、週単位でのデプロイ回数データをみると違いが見られ、パフォーマンス順は、チームC>チームD>チームA>チームBであるように見えます。(各チームの標準偏差は 0 .7〜1.2となっており、データの分布は平均値の±1営業日程度の範囲に収まっている状況であることがわかりました)

チームCは、サービスとしては若く多くの新規機能の追加を優先的・精力的に行なっているためリリース頻度が他のチームと比較して多くなっています。また、各機能の開発規模も小さく、細かいサイクルで開発を行なっているということもデプロイ頻度に影響していると思われます(ちなみに、Four Keysのサービス復旧時間が長く(10営業日)なっており他のチームと比較してパフォーマンスとしては良くありませんが、これは影響度の小さい・緊急度の低い不具合対応よりも新機能開発を優先しているという状況からきています)。チームBは2023年度前半はデプロイのない週も多く見られましたが、10月以降デプロイ頻度が上がってきている様子が伺え、チームAと同等レベルになってきています。

注:この比較では、リードタイム・デプロイ頻度の観点のみでシンクロ開発チームのパフォーマンスレベルを考察しています。FourKeysの残りの2つの指標:変更失敗率・サービス復元時間について、今回算出した指標値はFour Keysの定義からは外れていると思われるため、今回はこれらの指標では評価しませんでした。

まとめ

シンクロ・フード開発部 各開発チームのFour Keysを計測した結果、Four Keysの定義するパフォーマンスレベルとしてはどの開発チームもHighレベルと判断されました。ただ、Four Keysの指標値やデータの分布などを細かく比較していくと、各開発チームごとの状況の違いから差異が見られ、それぞれが異なる課題を持っていることが見えてきたと思います。

この結果に基づいて、今後は下記のような活動を予定しています。これらの改善活動を通じて新しい情報・知見などが得られましたら、またこのブログで紹介したいと思います。

1. Four Keys を開発生産性指標とした改善活動

開発チームにおけるFour Keys を開発生産性指標のうち、まずは変更リードタイム・デプロイ頻度にフォーカスして改善活動の実施していく予定です。

2. 企画チームを巻き込んだ改善活動

シンクロ・フードにおける開発では、Four Keysで計測される指標値は開発チームの作業のみならずサービスの企画を立案しているチームの作業も関わってくるため、開発チーム・企画チームが協調して改善活動をしていく必要があります。変更リードタイムは、企画チームの受入テストの時間・企画チームとのコミュニケーション・チケットの作業規模などにも依存していると考えられるため、企画チームを巻き込んだ改善が必要となると考えています。

3. 各開発チームへの横展開

改善活動はまずは一つの開発チームにて実施していく予定ですが、そこで成果が得られた改善活動は他の開発チームにも横展開していきます。

4. Four Keysを継続的・自動的に集計できる仕組みの整備

今回のFour Keys計測では、継続的・自動的に集計する仕組みは用意していません。開発・企画メンバーが常にFour Keysを収集・閲覧できる仕組みを構築していく予定です。

Gmail メール送信者のガイドラインに対応した話

はじめまして。開発部の髙木です。

今回は Google が去年新たに制定したメール送信者のガイドラインに対応したので、具体的な対応方法とその結果をご紹介したいと思います。
Google が制定したメール送信者のガイドラインについて詳しく知りたい方は、公式の記事を確認していただければ理解しやすいと思います。

弊社のメール事情

弊社は飲食店に関わる多種多様なサービスを展開しています。そのため、それぞれのサービスごとにお客様へ販促・トランザクションメールを送信する必要があります。例えば、飲食店の開業を検討しているお客様におすすめの新規物件をお知らせするメールや、求職者に希望条件とマッチした求人をお知らせするメールのようにユーザーがサービスを有効活用できるメールや、飲食店に関する有益な情報を定期的にメールで送信しています。多いときには一日に数十万通の販促メールを送信することもあります。これだけの数のメールがお客様に届かなくなるとサービスに致命的な影響を与えてしまうため、メール送信者のガイドラインへの対応は迅速に行う必要がありました。

対応すべきメール送信者のガイドライン

上記のメール事情でも述べたように弊社は多くのメールを送信しており、中でも Gmail アカウント宛が約半数を占めます。つまり、メール送信者のガイドラインに記載されている Gmail アカウントに 1 日あたり 5000 件以上のメールを送信する送信者 に該当します。よって、以下に記載するガイドラインへの対応が必須という状況でした。

  • メールの暗号化・認証を適切に設定する
    • ドメインに SPF・DKIM・DMARC 認証を設定する
    • メールの送信に TLS 接続を使用する
  • 迷惑メール率を 0.1 % 未満にする
    • 0.3 % 以上に決してならないようにする
  • 販促メールの購読解除を容易にする
    • ワンクリック購読解除を提供する
    • メール本文に登録解除のリンクをわかりやすく表示する

メールの暗号化・認証

最初にメールの暗号化・認証について説明します。
設定した暗号化・認証はガイドラインに記載されている以下の通りです。要するに、送信元の偽装やメールの改ざんがないこと・認証失敗時の対応を明示していること・安全性の高いメール送信方法を適切に設定したということになります。

  • 暗号化
    • TLS
      • より安全性の高いメール送信を実現するため
  • 認証
    • SPF
      • 送信ドメインの正当性を証明するため
    • DKIM
      • 送信元の詐称やメール内容の改ざんがないことを証明するため
    • DMARC
      • SPF や DKIM の認証が失敗した場合の対応策を示すため

弊社は販促メールとトランザクションメールを送信していますが、それぞれ送信方法が異なります。販促メールは弊社で立てたメールサーバーを利用して送信し、トランザクションメールは ESP( Email Service Provider )である SendGrid を利用してメールを送信しています。そのため、暗号化・認証の設定を行うに当たり、対応漏れがないように注意しました。

加えて、今回は Google Workspace から送信しているメールに対しても DKIM の設定を行っています。弊社では独自ドメインを取得して Gmail を利用しており、Google Workspace の DKIM 設定がデフォルトのままだと DKIM アライメントに失敗してしまいます。SPF は従来から対応済みのため DMARC 認証には Pass しますが、ワンクリック購読解除の提供に合わせて DKIM アライメントにも Pass するよう対応しました。

ワンクリック購読解除の提供

続いてワンクリック購読解除の提供について説明します。 その前に、弊社の販促メールの送信方法について簡単に紹介だけしておきます。メール事情でも述べたのですが、弊社は飲食店に関わる多種多様なサービスを展開しています。これらのサービスごとに販促メールを送信するとなるとメール送信処理が複雑化してしまうので、弊社では「一括メールシステム」というものを作成して販促メール送信に利用しています。以下の図に示すように、一括メールシステムは多数のサービスで作成した販促メールデータを一元管理し、一斉送信するような機能になります。

一括メール配信システム

上記のように、弊社は販促メールの送信に SendGrid や Salesforce などの ESP を利用していません。そのため、ワンクリック購読解除の仕組みを独自に実装する必要がありました。そこで、弊社の販促メール送信フローを考慮した上で、以下のようにワンクリック購読解除を実装しました。

  1. 一括メールシステムの送信処理にワンクリック購読解除の設定を組み込む
  2. ワンクリック購読解除リクエストを受け付ける処理を実装する
  3. ワンクリック購読解除に必要な情報をメールデータに追加する

まずは、一括メールシステムの送信処理にワンクリック購読解除の設定を組み込むことから対応しました。Google の「簡単に登録解除できるようにする」や RFC 8058 に詳しく記載されていますが、ワンクリック購読解除を提供するには以下 2 つのヘッダー情報をメールに設定するだけです。

List-Unsubscribe: <https://example.com/unsubscribe.html?opaque=123456789>
List-Unsubscribe-Post: List-Unsubscribe=One-Click

List-Unsubscribe-Post が設定されている場合のみ List-Unsubscribe に記載されている url やメールアドレスに対して、ワンクリック購読解除リクエストが送信できるリンクがメール内に表示されるようになります。上記の仕様から、ワンクリック購読解除に使用できるメールデータがある場合に限り、一括メールシステムのメール送信時にメールヘッダーとして設定するような実装を行いました。

次に、ワンクリック購読解除リクエストを受け付ける処理を実装しました。再三ですが、弊社は販促メールの送信に ESP を利用していないので、ワンクリック購読解除リクエストを受け付ける機能の実装も独自に行なっています。一括メールシステムはほぼ全サービスの販促メールを一元管理して送信しているため、同じようにワンクリック購読解除リクエストを受け付ける機能も一元管理するように実装しています。具体的には、特定の url をメールデータから構築して解析することで購読解除に必要な情報を抽出して、メールを送信しないお客様として登録するような実装となっています。

最後に、ワンクリック購読解除に必要な情報をメールデータに追加しました。一括メールシステムを利用しているサービスの販促メール登録処理を洗い出し、ワンクリック購読解除に必要な情報をメールデータとして一括メールシステムへ渡せるように実装しています。

ワンクリック購読解除

まとめると、「サービス」と「一括メールシステム」でワンクリック購読解除を提供するための基盤を実装したことになります。そのため、お客様にワンクリック購読解除を提供するだけでなく、新規で販促メールを登録するような場合でも容易にワンクリック購読解除を適用できる環境の構築まで完了しています。

Gmail メール送信者のガイドラインに対応した結果

Gmail アカウント宛のメール配信状況は Google が提供する Postmaster Tools を使うことで確認できます。
この Postmaster Tools 上で確認できる情報や個別に集計した情報を用いて、メールの暗号化・認証、ワンクリック購読解除を提供した結果を共有します。

メールの暗号化・認証は今回のメール送信者のガイドラインが規定される前からある程度対応していたこともあり、迅速に対応することができました。そのため、PostmasterTools を確認すると 2023-11 月末から常に認証率 100 % となっております。もちろん、TLS による暗号化も同時期から常に 100 % の使用率です。

TLSによる暗号化

また、ドメインレピュテーションも向上しています。

ドメインレピュテーション

続いてワンクリック購読解除を提供した結果を、迷惑メール率の平均値推移・ワンクリック購読解除リクエスト数を指標として共有します。

迷惑メール率

ワンクリック購読解除を提供する以前は、弊社の迷惑メール率の月間平均値は 0.25 % 前後でした。これは月間平均値なので 0.3 % を超過することはありませんが、日単位で確認すると 0.4 % などの 0.3 % を超過する日もありました。ただ、販促メールに対するワンクリック購読解除を提供開始した 2024-02 月以降は迷惑メール率の月間平均値が 0.05 % 程度と著しく低下しており、メール送信者のガイドラインを満たすことができています。日単位で確認しても 0.0 % か 0.1 % の日が大半を占めており、0.2 % 以上になったのは一日だけでした。これは、実際にワンクリック購読解除のリクエスト数が 2024-02 月だけで 3500 件程度あることからも、販促メールを受信していただいているお客様が迷惑メールとして報告するのではなく、購読解除していただけていると捉えることができます。

ワンクリック購読解除リクエスト数

では、実際に送信されたワンクリック購読解除リクエストの推移を確認してみます。ワンクリック購読解除リクエストを送信するためには Google が提供しているメール内リンクを押下する必要があります。しかし、こちらのリンクは今までワンクリック購読解除を提供していない送信者の場合、Google の判断で表示制限が設けられてしまいます。そのため、提供開始直後はワンクリック購読解除のリンクが表示されるケースが少ない = リクエスト数が少ない( 迷惑メールとして報告される )状態であると考えられます。現状、時間の経過とともにワンクリック購読解除リクエスト数は増加傾向にあるため、ワンクリック購読解除のリンクが多くの受信者で表示されるようになっているのだと捉えています。このワンクリック購読解除のリンクがすべての受信者で表示されるようになれば、さらに迷惑メール率を低下させることが可能になり、0.1 % 未満を維持することができる想定です。

まとめ

期日までに Google が新たに制定したメール送信者のガイドラインに対応することができました。暗号化・認証はすでに設定していたものが多く、迅速に対応することができました。ワンクリック購読解除は全サービスの販促メールを網羅できるように実装しました。また、今後追加される販促メールに対しても容易にワンクリック購読解除を提供できる基盤を作ることもできました。これらの対応によって PostmasterTools で報告される平均迷惑メール率は 0.25 % 程度から 0.05 % 程度へと減少させることができました。

また、今回の対応は弊社の販促メール送信フローを再確認する良い機会だったと捉えています。今までルール化されていなかった曖昧な販促メール送信フローを見直し、全サービスの販促メール送信フローの基盤を構築できたと考えています。

現段階でもメール送信者のガイドラインに対応できてはいます。しかし、日毎の迷惑メール率が 0.1 % となる場合があること・販促メールがトランザクションメールにも影響してしまうことを考慮して、販促メール専用のドメイン分割を試みています。また、迷惑メール率が一定値以上になった場合に通知するような監視体制を整えている段階です。こちらについては、対応完了後に別記事として掲載できればと思います。

飲食店ドットコムの中で一番負荷の大きかった検索機能にOpenSearchを導入した話

はじめまして。開発部の深野です。
今回は飲食店ドットコムの中のスカウト対象者検索という、サービス内でも一番DB負荷の高かった機能にOpenSearchを導入したので、導入時に検討したことと導入結果を紹介します。

シンクロ・フードのエンジニアブログでは、以前にも求人飲食店ドットコムの求人検索という機能にOpenSearchという全文検索エンジンを導入したことを紹介したことがあります。そのため、OpenSearchについて詳しく知りたい方は是非こちらの記事を先にお読みください。

はじめに

以前、弊社で最も頻繁に利用される機能である求人サイトの求人検索機能に全文検索エンジンであるOpenSearchを導入し、その高速化を図りました。 その結果として求人検索機能の高速化とDB負荷の低減という結果を得られたので、今回は新たな課題に取り組むことにしました。
具体的には、弊社のサービスでMySQLへの負荷が一番大きかったスカウト対象者検索機能にOpenSearchを導入することで、データベースの負担を軽減し、検索処理のさらなる改善を目指しました。
多くの求人サービスにはスカウトという仕組みがあり、求職者の方から会社へ応募するだけではなく、会社の方から求職者の方へ向けて自社を受けてみないかどうか勧誘することができると思います。飲食店ドットコムのスカウト対象者検索機能もこのようなことを実現するための機能であり、求職者の方の職務経歴や希望する勤務地を検索して求人飲食店ドットコム会員をスカウトすることが可能です。

導入の背景

元々のスカウト対象者検索機能は、以下のような問題を抱えた実装になっていました。

  • 複数のテーブルにまたがる複雑な検索処理:一度のSQLクエリではなく、複数のSQL実行というステップを踏んで徐々に対象のスカウト対象者を絞り込む方式で実装されていた
  • ページング機能のためのフルスキャン:検索条件に合致する件数を取得するCOUNT(*)がフルスキャンしており、実行時間が大きくかかっていた
  • 正規化されていないテーブルとカンマ区切りのカラム:一部のテーブルでは歴史的経緯によりカンマ区切りのカラムが使用されていることから、FIND_IN_SET関数を用いた検索が行われており、パフォーマンス面の問題を起こしていた
  • フリーワード検索機能:部分一致のフリーワードが機能的に必要になったことで、LIKEによる部分一致の検索というパフォーマンス的にはあまり良くない実装が必要になっていた

これらから、アクセス量が増加するとすぐにデータベースのリソースを圧迫し、レスポンスタイムが著しく低下することがあり、最悪の場合システム障害につながることもありました。また、レスポンスタイムのさらなる劣化を避けるため、新機能の追加が一部制限されるという問題も抱えていました。過去にはOpenSearchを導入せずに高速化しようと試みたこともあったようなのですが、そのアプローチには限界がありそうなのでどうせこの問題に向き合う(=検索機能の再実装を行う)なら全文検索エンジンを利用したいという考えがあり、今回のOpenSearch導入に至りました。

MySQLからOpenSearchへのデータ同期方法

データ同期のためのシステム構成は以下の通りです:

システム構成図
システム構成図

検索のために必要なテーブルはスカウト対象者のテーブルやその関連テーブルを含めて11個ほど存在したので、それぞれにTRIGGERを設定しました。設定したテーブルにINSERT, DELETE, UPDATEがあると、キューテーブルにスカウト対象者テーブルの主キーをインサートします。rakeタスクは30秒に一度の間隔でキューテーブルをポーリングし、更新の必要のあるスカウト対象者をMySQLから取得した上でOpenSearchへの更新を行うためのJSONを作成し、OpenSearchに更新を行います。

更新待ちキューに関して、以前は各アプリケーションからAWSのSQSを経由してデータを送信し、rakeタスクでポーリングする方式を採用していました。しかし、今回はSQSは利用せず上で述べたような方法を選択しました。この方法を採用した理由は以下の通りです:

  • データ更新経路の多様性:弊社ではデータ更新経路が複数のRailsアプリケーション、複数のJavaアプリケーション、そしてトラブル発生時のデータメンテナンスのための生SQLに分かれており、各アプリケーションにキュー送信処理を組み込むのは実装のコストが大きい
  • 「枯れた」技術の利用:既存の確立された技術の使用により、新たなリスクを回避できる
  • ある程度のリアルタイム性:データ変更があった場合、即座にキューテーブルへの挿入が行われる
  • トランザクション制御:データの更新とキューテーブルへのインサート処理がトランザクション内で完結するため、整合性が保たれる

この方法を採用するにあたって受容したデメリットは以下の通りです:

  • DBシステムの複雑性の増加:トリガやコールバックを大量に導入することはDBシステムの複雑性を増加させるが、今回のトリガは変更があった時にログをインサートするだけなのでそこまで大きな問題はないと判断した
  • スケーラビリティ:トリガを利用する方法は更新が多くなるとスケールが難しいが、大量にレコードを更新するような処理が連続することは現在の弊社のサービスではあまりないので問題は起きないと判断した。
  • トランザクションによるロック:キューテーブルはINSERTとDELETEしか行わないテーブルではあるが、場合によってはテーブルにロックがかかりキューテーブルからのレコードの削除まで時間がかかることがあった(トリガによるキューのテーブルへのインサートを特定のバッチでのみ一時的に無効化することで対応できた)

今回は、他にもAWS Glueのような方法からAWS DMSやdebezeimなどのCDCまで、マネージドサービスを中心に色々な方法を検討していました。
特に注目していたのがMySQLのテーブルをキューとするのではなく、キューはAWSのSQSを利用して、Aurora MySQLの機能を利用してトリガからAWSのlambda functionをコールしてlambda functionからSQSにエンキューする方法でした。元々弊社ではキュー的なものを実装する時にはSQSを利用することが多く、この方法は有力な候補でしたが、lambda functionをコールしたトランザクションがCOMMITされた後にエンキューされるのではないことから本要件での利用は断念しました。一つのトランザクションの実行時間が長くlambda functionが実行される時に該当のトランザクションがまだCOMMITされていない場合や、該当のトランザクションが途中でロールバックした場合などに対応するのが難しいと思ったためです。

OpenSearchによる検索

スカウト対象者検索では、様々な検索条件に基づいて組み立てられたJSONを用いて、OpenSearchのsearch APIを通じて検索を行っています。具体的には、まずスカウト対象者の主キーをOpenSearchから取得し、その後、Auroraデータベースから表示に必要なデータを取得します。

元々の実装では、複数のSQLクエリを順番に実行して主キーを絞り込む実装になっていましたが、OpenSearchの導入により、検索プロセスが大幅に簡素化されました。具体的には、一回のOpenSearch検索で必要な全ての主キーを絞り込み、その結果を使用して迅速に検索対象のスカウト対象者のリストを取得できるようになりました。

今回は、既存のMySQLベースの検索機能をOpenSearchに移植し、その高速化に重点を置いています。OpenSearchの機能を最大限に活用すれば、これまで不可能だったより高度な検索機能も実現可能ですが、今回は元の機能の移植とパフォーマンスの向上に注力しました。

テスト中に発生した問題

テスト中に遭遇しましたいくつかの問題のうち、主な2つが以下になります。

1つ目はcompressionというオプションの適用問題です。
OpenSearchではデータ更新や検索リクエスト時にcompressionというオプションを使ってリクエストをg-zipで圧縮することが可能です。 今回はこれを有効にしようと思って開発を進めており、ローカル環境ではこの設定を有効にしても問題なく通信できましたが、設定の問題か何かでAWS上のテスト環境ではエラーが発生しました。
リリースの優先度を考慮し、今回はcompressionオプションの使用を諦め、圧縮なしでの通信を行うことにしましたが、将来的にはこの問題を改めて調査し、圧縮設定をオンにする予定です。

2つ目は、高負荷時の検索レスポンスタイムの劣化です。
前回のブログ記事「OpenSearch の導入による検索システム改善のための認証・認可設計と負荷検証結果」でも書いたのですが、OpenSearchではrefresh_intervalという更新内容を検索結果に反映するまでの時間を短くするほどパフォーマンスが劣化することは元々認識していました。
ただし前回の求人一覧のOpenSearch利用の際は、refresh_intervalをデフォルトの1秒のままでインスタンスサイズを増加させることでパフォーマンス問題を解決しました。 しかし、今回OpenSearchに新たなインデックスを追加し、再度負荷検証を行った結果、refresh_interval=1sで短時間に更新リクエストが大量に来る場合には一時的に検索のレスポンスタイムが著しく劣化することが分かりました。
そのため、今回新たにインデックスの更新リクエストが増えることや、今後さらに検索機能をOpenSearchに置き換えることまで考えて、既存のものも含めてrefresh_intervalを30秒以上に設定することにしました。

導入した結果

OpenSearch導入前後のスカウト対象者一覧のレスポンスタイムの日ごとの中央値の推移について紹介します。
まずは、検索条件を何も指定しなかった場合のレスポンスタイムの推移が以下になります。

レスポンスタイム
レスポンスタイム

10/18のリリース前は3秒強だったレスポンスタイムが0.5秒前後まで縮まりました。
検索条件を何も指定しなかった場合でも大きく改善しているのは、元々はページングのために検索結果にマッチする件数を取得するために数十万件のレコードがあるテーブルを複数JOINした上でCOUNT(*)を実行しており、そこがかなり負担になっていたのですがそこがOpenSearchに任せたことで数msで済むようになったのが大きいと思っています。飲食店ドットコムは20年ほど続いているサービスなので、実装時はパフォーマンスが問題にならなかったもののユーザー数の増加と共にパフォーマンス面で問題になるというケースが度々あり、今回もその一つでした。

次が、全文検索エンジンの本領発揮である、フリーワード検索を検索条件に含む検索の時のレスポンスタイムになります。

レスポンスタイム(フリーワード検索含む)
レスポンスタイム(フリーワード検索含む)

以前は1日ごとの中央値で6秒~12秒というレスポンスタイムでしたが、OpenSearch導入後は、検索条件がないときと同じ0.5秒ほどで推移しています。これはやはり元々が遅かったインデックスの効かないキーワードのLIKE検索を大きく高速化できたためだと思われます。また、フリーワード以外の検索条件を含んだものもここには入っているため、他にもFIND_IN_SETなどを利用したインデックスの効かない検索を高速化できたのも理由として大きいはずです。

導入した結論としては、以下のようになります。

  • 検索条件が何もない時でも実行時間の大きかった件数表示のためのCOUNT(*)をMySQL上で実行しなくて済むようになったことでレスポンスタイムが改善した
  • フリーワード検索を含むような複雑なリクエストは検索条件がない時に比べても大きくレスポンスタイムが改善した

まとめ

負荷軽減と高速化を目的に、スカウト対象者検索機能にもOpenSearchを導入しました。
更新方法についてはMySQLのトリガを利用して変更された内容をセミリアルタイムにOpenSearchに反映する形を取りました。
OpenSearchを導入した結果、検索クエリが単純な場合と複雑な場合で共にレスポンスタイムが大きく改善しました。
今回のスカウト対象者検索機能はMySQLでも改善が不可能ではないと思うのですが面倒な、COUNT(*)LIKEFIND_IN_SETをそれなりのレコード数のあるテーブルで利用しているページということもあり、OpenSearchの導入の効果は大きかったです。