TSKaigiを立ち上げて、登壇もしてきました

はじめに

TSKaigiプラチナスポンサーであるレバレジーズ株式会社のテクノロジー戦略室室長 & 一般社団法人TSKaigi Association 代表理事 & TSKaigi2024 Speakerの竹下です。
TypeScriptの言語カンファレンスであるTSKaigi2024が5月11日(土)に中野セントラルパークで開催されました。基調講演にはMicrosoftのTypeScript Product ManagerであるDaniel Rosenwasser様を迎え、37名の登壇者、会場での参加者約400名、オンライン参加約2000名の、大規模イベントになりました。

この記事では、私がTSKaigiを立ち上げた経緯と、登壇者として発表してきた「Step by Stepで学ぶ、ADT(代数的データ型)、モナドからEffect-TSまで」について、紹介したいと思います。

TSKaigiとは

TSKaigiは、TypeScriptをテーマにした、技術カンファレンスです。俗に言語カンファレンスと呼ばれる、特定のプログラミング言語を扱ったカンファレンスで、有名なものではRubyKaigiやPyCon、PHP Conference、ScalaMatsuriなどがあります。(ちなみに私はScalaMatsuriの開催にも理事として関わっていたりします)
運営は、私を含む理事4人を中心に、総勢45名ものボランティアスタッフによって行われました。

ここがすごかったよTSKaigi

初開催でいきなり2400名規模!

技術カンファレンスとしては、日本でも大規模に分類されるイベントです。会場には約400名、オンラインで約2000名の方にご参加いただきました。TypeScriptのイベントとしては現時点で日本一の規模だと思います。初開催でノウハウがない状態から、大きなトラブルもなく無事にイベントを開催できたのは素晴らしいことで、運営を手伝ってくださったスタッフの方々の頑張りの賜物です。感謝の気持ちでいっぱいです。

基調講演はMicrosoft TypeScriptのProductManagerのDaniel Rosenwasserlさん!

言語の開発者というものは普段あまり意識しないかもしれませんが、DanielさんはTypeScript自体を長年にわたって開発してくださっている方です。また、JavaScriptの標準仕様を策定する団体であるTC39にも関わっておられます。TypeScriptやJavaScriptを使うエンジニアにとっては、足を向けて寝られない方です。そのDanielさんが、TSKaigiの基調講演を行ってくれました。

レバレジーズ株式会社を始め38社が協賛!

レバレジーズも最上位のプラチナスポンサーとして協賛しましたが、プラチナスポンサー、ゴールドスポンサー、シルバースポンサー、ブロンズスポンサーをはじめ、コーヒー、ビール、リフレッシュメント、イベントツールなどの特殊スポンサーを含め、合計38社に協賛していただきました。スポンサー募集期限後に開催を知って協賛できなかった企業様も多数おられたので、来年は周知を徹底し、TypeScriptを盛り上げてくださる企業様を増やしていきたいです。

立ち上げ経緯

コミュニティの盛り上がりが、人気に比例していない

TypeScriptの人気はコロナ禍中にもどんどん高まり、開発者の数も増え、使用範囲もフロントエンドに留まらず、バックエンド、インフラ、AI/ML、IoTと非常に幅広くなっています。しかし、私個人の感想にはなりますが、TypeScriptといえばこの人という存在や、日本発のOSSライブラリは、人気や開発者数に対して少ないと感じていました。また、コミュニティ活動も行われている方は数多くおられましたが、その方々が大々的に知られる機会もあまり無いと感じていました。

コミュニティの核となるイベントを作る

TypeScriptの言語カンファレンスは、コロナ禍前にTSConf JPが開催されていましたが、その後中断してしまい、日本では大規模なイベントが開催されていない状態でした。コミュニティが盛り上がらない原因として、コロナ禍も影響し、中核となるイベントやコミュニティがなく、交流の輪が広がっていかないことが挙げられます。
そのような状況の中で、TSKaigiを開催することで、TypeScript開発者に成果を発表してもらう場を提供するとともに、TypeScriptコミュニティの皆さんにどんな人がいて、どんな使い方をされているかを知ってもらう場を提供できると考えました。また、TSKaigiに参加してもらうことで交流が生まれ、そこから交流が続くことでTypeScriptコミュニティ全体の活性化につながると考えました。

団体としてコミュニティの活性化を継続

また、単発イベントではなく継続開催することが、TypeScriptコミュニティの継続した発展に繋がると考え、同時期に同じことを考えていた他の理事と共同で一般社団法人TSKaigi Associationも設立しています。来年以降の開催や、サブイベントの開催も行っていこうと思っていますので、是非この記事をお読みの皆さんご参加下さい。

登壇内容

また、賑やかしのためにセッションの応募したら当選していたので発表も行ってきました。以下が発表スライドです。

スライド

おまけ

この発表では、Effectの導入する意義に焦点を当てて解説していますが、実際の運用では一歩踏み込み、Effectによる関数の細粒度化と副作用分離によるアーキテクチャーレベルの設計も行っています。機会があればそちらも発表したいと思います。

おまけのおまけ

代表理事として、開幕の挨拶もしたのですが、演台の前からはなれて動きながら喋ってたら演台にもどれって怒られました(笑)

オープニングトーク

P.S. 書いててレバレジーズ株式会社のエンジニアとしてなのか、TSKaigiの代表理事としてなのかよくわからなくなってきて、ちょっと混乱した文章になっていますがご容赦下さい。また、TSKaigiの代表理事としての思いなどは、改めてTSKaigiのブログで書きたいと思います。

We are hiring!!!

レバレジーズ株式会社では一緒にサービスを開発してくれるエンジニアを幅広く募集中です。
TypeScriptで開発するだけでなく、コミュニティ貢献もしていきたいという人もウェルカムですし、TypeScriptに触ってないけど興味あるよという人もウェルカムです。 もしご興味ありましたら、こちらのページからご応募ください。

株式会社ビザスク様と設計合同勉強会を開催しました

レバレジーズ株式会社 レバウェル開発部 SREの中村です。
2024/03/13に、設計をテーマに合同勉強会を開催しました。 領域を絞らずに発表いただいたことで、設計について幅広く知見を得られた勉強会になりました。
また、弊社が東京の渋谷に物理出社がメインで、ビザスク社がフルリモートということで Zoomを使用してオンラインでの開催になりました。 この記事では、弊社のエンジニアの発表内容を簡単にご紹介したいと思います。

発表内容

弊社からは、二人が発表しました。


テクノロジー戦略室 室長 竹下

speakerdeck.com

EffectというTypeScriptのライブラリを活用して、IOに関する副作用を分離した設計を実現する方法について発表されています。また、その際にEffectを導入する利点についても紹介されています。


レバテック開発部ITSプロダクト開発グループ PdM兼テックリード 古庄

speakerdeck.com

レバテックにて、新たに構築中のデザインシステム「VoLT」について発表されています。構築プロセス、システム設計や実装例について紹介されています。


We are hiring

レバレジーズ株式会社ではどの領域のエンジニアも幅広く募集中です。 もし今回の勉強会の発表を見てご興味を持っていただけたなら、こちらのページからご応募ください。

勉強会の開催も随時受け付け中です

今回は設計をテーマに合同勉強会を開催しましたが、工程を絞ったものだけでなく、フロントエンド、バックエンド、インフラ、機械学習といったように、様々なテーマで弊社と合同勉強会を開催してくれる企業様を募集中です。ご興味ある企業様がいらっしゃいましたらご連絡いただけると嬉しいです。

LangChain最新バージョンで三大クラウドのLLMをサクッと実装してみる

DALL·Eで生成したLangChainのイメージ画像

  • LangChainの動作確認済みバージョン(2024/3/26時点では最新)
    • langchain: 0.1.13
    • langchain-community: 0.0.29
    • langchain-openai: 0.1.0
    • langchain-google-vertexai: 0.1.1


はじめに

テクノロジー戦略室 AI/MLエンジニアリングチームの稲垣です。
AI/MLエンジニアリングチームでは、レコメンドエンジンの MLOps 基盤の構築や、AWS Personalize などの ML サービスや LLM など生成 AI 活用を、企画から構築まで進めています。


今回は、LangChain の最新バージョン(2024/3/26時点)を使った複数モデルの実装方法について紹介します。

このような実装ニーズは良くあるかと思いますが、案外記事がまだ落ちていないようでしたので記事にしてみました。


現在、社内で LLM を使ったツール開発を行っており、LLM は以下のような複数ベンダー&複数モデルの API を使用しています。

  • GPT-3.5-turbo, GPT-4 (Azure OpenAI Service)
  • Gemini Pro (Google Cloud VertexAI)
  • Claude 2, Claude3 (AWS Bedrock)

それぞれのモデルに対して別個に実装していくことももちろん可能ですが、
API 入力/出力のフォーマット、良くある前処理や後処理、ストリーミングによる出力などの実装方法がモデルごとに異なるケースが多く、
それらを別個に実装していく手間はモデルの数が増えるほど大きくなっていきます。

(実際、anthropic-sdk-pythonによる Claude モデル実装と、google-cloud-aiplatformによる Gemini モデル実装は根本的な箇所でいくつか大きな差異があります。)

このようなモデルごとの実装の差異を吸収してくれるライブラリとしてLangChainがあり、 社内ツールの実装でも LangChain を利用しています。

LangChain:github.com


また、LangChain について最近の動向としては、
2024/1/6 にバージョン 0.0.354 から 0.1.0 へのアップデートがあり、LangChain 初の安定バージョンとなった、というニュースがありました。
(これ以前は破壊的な変更が頻繁に起こったり、公式ドキュメントのクイックスタートの内容が見るたびに変わっていたりしました、、、)

ただし、この 0.1 系へのアップデートにより、LangChain のパッケージの構成について大きな変更があったため、この点についても後半で補足しつつ説明をしていきたいと思います。


複数 LLM の切り替え実装

先に補足事項を記載します。

  • チャットツール等のユースケースではストリーミングによる出力を行うケースが多いかと思いますが、その場合は別途 CallBack 関数を渡す実装が必要になるケースがあるため、今回はストリーミングをオフにしています。
  • 下記のコード例では分かりやすさ重視のためシークレット情報をベタ書きしています。(実際の自社のコードではシークレット情報は AWS Secrets Manager で管理しています。)

AzureChatOpenAI

環境変数AZURE_OPENAI_ENDPOINTOPENAI_API_KEYに対しては、 Azure OpenAI Service から取得した値を設定しています。(参考

import os

from langchain_openai import AzureChatOpenAI


os.environ["AZURE_OPENAI_ENDPOINT"] = https://xxxxxxxxxxxxxx.openai.azure.com/
os.environ["OPENAI_API_KEY"] = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
os.environ["OPENAI_API_VERSION"] = "2023-05-15"


chat_model = AzureChatOpenAI(
    streaming=False,
    deployment_name="gpt-4",
    # 必要であれば
    # temperature=0.9,
    # model_kwargs={
    #     "top_p": 0.9,
    # },
)

ChatVertexAI

Google Cloud への認証は下記2種類の方法があります。(参考

  • gcloud や workload identity により実行環境に認証情報を紐づける方法
  • 認証情報の json ファイルの格納パスを環境変数GOOGLE_APPLICATION_CREDENTIALSに設定する方法


今回は後者の方法で実装を行いました。
認証情報の json ファイルの作成方法はこちらのリンクが参考になります。

import os
from langchain_google_vertexai import (
    ChatVertexAI,
    HarmBlockThreshold,
    HarmCategory,
)

# 環境変数に認証情報が入った json ファイルのパスを設定
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "./credential_file_name.json"

chat_model = ChatVertexAI(
    model_name="gemini-1.0-pro",
    streaming=False,
    # Geminiはシステムプロンプトに対応していないため、システムプロンプトを入れる場合はここをTrueにする
    convert_system_message_to_human=True,
    # 適宜要件に合わせて設定してください。今回はブロックが起こりにくい設定(BLOCK_ONLY_HIGH)にしています
    safety_settings={
        HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_ONLY_HIGH,
        HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_ONLY_HIGH,
        HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_ONLY_HIGH,
        HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_ONLY_HIGH,
    },
    # 必要であれば
    # temperature=0.9,
    # top_k=0.9,
    # top_p=32,
)

BedrockChat

Amazon Bedrock へのアクセス権限を持つ IAM ロールが付与された環境で実行するか、
アクセスキー ID とシークレットアクセスキーを環境変数に設定して実行してください。

from boto3.session import Session
from langchain_community.chat_models import BedrockChat

BEDROCK_REGION = "ap-northeast-1"
session = Session()
bedrock_runtime = session.client("bedrock-runtime", region_name=BEDROCK_REGION)

chat_model = BedrockChat(
    streaming=False,
    client=bedrock_runtime,
    model_id="anthropic.claude-v2:1",
    # 必要であれば
    # model_kwargs={"temperature": 0.1, "top_p": 0.9, "top_k": 250},
)

チャットモデルから出力を受け取る

from langchain.schema import (
    AIMessage,
    HumanMessage,
    SystemMessage,
)

messages = [
    SystemMessage(
        content="あなたは江戸時代から続く老舗寿司屋の職人です。質問に対してぶっきらぼうに回答してください。"
    ),
    HumanMessage(content="オススメの寿司ネタは?"),
    AIMessage(content="まぁ、季節によるな。今なら鯖が旨い。"),
    HumanMessage(content="その理由は?"),
]

response = chat_model.invoke(messages)

print(response.content)
# > 鯖はこの時期に脂がのって味が濃くなるんだよ。


やっていることとしては、
使用したい LLM が指定できるクラスAzureChatOpenAI, ChatVertexAI, BedrockChatをそれぞれ import し、それらのクラスのインスタンス化の際に必要な引数や環境変数を設定すれば OK です。

ここまで出来れば、後はモデルごとの実装方法の差異を LangChain がうまく隠蔽してくれるため、

  • API の入力/出力のフォーマット
  • 良くある前処理や後処理
  • ストリーミング時の処理

などを共通化することができ、LangChain の恩恵を受けることができます。


また、パッケージのインストールについての補足事項としては、
LangChain のバージョン 0.1 系へのアップデートに従い、LLM の外部プロバイダーとの統合部分の処理はlangchainパッケージからlangchain_communityパッケージもしくは独立した新パッケージに分離がなされました。

こちらは LangChain 公式の以下の記事でも紹介されています。

blog.langchain.dev

Specifically we made two large architectural changes: separating out langchain-core and separating out partner packages (either into langchain-community or standalone partner packages) from langchain.

(意訳):
具体的には、大きな構造変更を二つ行いました:langchain-core パッケージの分離と、パートナーパッケージの langchain からの分離(langchain-community パッケージに組み込むか、独立したパートナーパッケージとして分離)です。


そのため、上記のコードでは、

  • AzureChatOpenAI: langchain_openaiからインポート
  • ChatVertexAI: langchain_google_vertexaiからインポート
  • BedrockChat: langchain_communityからインポート

としています。

※ 補足:
0.1 系へのバージョンアップでは後方互換性が担保されているので、例えばChatVertexAIであれば以下でも import が可能です。

from langchain.chat_models.vertexai import ChatVertexAI
from langchain_community.chat_models.vertexai import ChatVertexAI

ただし非推奨の warning が出るので、上述のとおり適切なパッケージからの import をおすすめします。


おまけ:
AzureChatOpenAI, ChatVertexAI, BedrockChatともBaseChatModelというクラスのサブクラスになっています。
そしてBaseChatModellangchain_coreパッケージに実装されているため、 ここからも LangChain のコア機能はlangchainlangchain_coreで開発し、外部プロバイダーに依存する機能はlangchain_communityや独自パッケージで開発する、という意図が汲み取れます。



まとめ

LangChain を使った複数 LLM の実装を紹介しました。
レバレジーズでは MLOps 基盤や ML 活用を企画から考え構築までやりたいエンジニアを募集しています! ご興味のある方はぜひ採用サイトをご覧ください!

レバウェルのCM放送後の急増ユーザーを受け止めろ!~API処理速度82倍向上の秘訣~

はじめに

レバレジーズ株式会社 レバウェル開発部 バックエンドチームサブリーダーの山口です。

総合転職サービス「レバウェル」は

  • 転職アドバイザー(転職エージェント)
  • スカウトサービス
  • 求人サイト

を1つのサービスで利用できるアプリケーションです。

2024年2月よりCMを放送中です!

youtu.be

このマス広告を打つために、広告効果によって増加するユーザーとその負荷に耐えることができるかの検討と対策を行いました。

本記事では、私たちがどのような視点で速度改善に取り組んだのか、そしてその結果として特定のAPIの処理速度を82倍向上させた経緯をご紹介します。

レバウェルについて

はじめにレバウェルの基本情報と今回取り組む課題の背景について簡単にご説明します。

技術スタック

今回の内容に関係するレバウェルの技術スタックをご紹介します。

  • 言語
    • TypeScript
  • フレームワーク
    • Next.js, NestJS
  • ライブラリ
    • Apollo Server
  • インフラストラクチャ
    • AWS(ECS, RDS)
    • Vercel
  • API規格
    • GraphQL

導入済みの性能対策

ISR(Incremental Static Regeneration)

レバウェルでは初期リリース時にISR(Incremental Static Regeneration)を採用しました。 対象はSEOの観点で重要かつリアルタイム性が高くないページです。

これによりアプリ全体を再ビルドすることなくデータの変化を定期的にページに反映することができます。

DataLoader

GraphQLはクライアントが必要とするデータを必要な分だけ一度に取得できるという強みがあります。 しかしその一方で、データを取得する際にN+1問題というパフォーマンスに関する問題も抱えがちです。今回はN+1問題に関する詳細な説明は省略しますが、この問題を解決するためにDataLoaderを導入しています。 DataLoaderを用いて各リレーションごとのバッチ処理を行い、SQLの実行回数を削減しています。

増加するユーザーとその負荷に耐えられるか

webアプリケーションにおいて表示速度は離脱率と密接な関係にあり、性能を評価する上で重要な指標となります。 どんなにコンテンツが素晴らしくとも、表示速度が遅いと見てもらうことすらできません。

参考:https://www.thinkwithgoogle.com/marketing-strategies/app-and-mobile/mobile-page-speed-new-industry-benchmarks/

マス広告を打つということは、必然的にユーザー数が短期間で増加します。 この負荷によって性能が悪化すると、ユーザーにプロダクトの価値を提供することができなくなってしまいます。 そのような事態を防ぐべく、速度改善の必要性を判断し、必要な対策を講じることとしました。

速度改善の必要性はどう判断する?

速度改善の必要性があるかどうかの判断基準として以下を定めました。

予想されるAPI実行頻度において性能目標を達成しているか

これを判断するために、以下の2点を定義する必要があります。

  • 予想されるAPI実行頻度
  • 性能目標

これらの数値に基づいて負荷テストを行い、実行結果が性能目標を満たしていなければ速度改善の必要があると判断することができます。

API実行頻度の見積もり

今回はマス広告の効果によるリクエスト数の増加を考慮する必要があります。

実際の計算の流れは以下の通りです。*1

とある集計期間における1分間あたりのリクエストレート(単位:RPM)が5 RPM であった時について考える。

1時間あたりのリクエスト数は以下で表すことができる。
TotalRPH = RPM * 60
つまり300 RPHとなる。

また、集計期間におけるログインユーザー数が50人だった時、 ログインしていないユーザー数をX人とする。 このとき、一人当たりに発生する1時間のリクエスト数は
RPH-per-person = TotalRPH / (50 + X)

と表すことができる。 ただし、今回はより余裕を持った対策を行いたいため、ログインユーザーを分母としてリクエスト数を計算することとする。 つまり一人当たりに発生する1時間のリクエスト数は
RPH-per-person = TotalRPH / 50
となり6 RPHとなる。

この時、広告の掲載によって大量のユーザー増加が見込まれる場合について考える。 TVCMを放映した場合、「CMを認知してもらう」という目的では個人全体視聴率でおよそ500GRPが目安とされている。 *2
GRP500の場合3000万人前後に視聴されるというシミュレーション結果があるため、本検証でもこの数値を使用する。*3

次にCTRが仮に0.2%だとすると約6万人前後がTVCM放映期間中に流入することになる。
TVCM放映期間がスポットCMの契約最短期間である一週間と仮定すると、
1時間当たりの新規流入数 = 60000 / (24 x 7) ≒ 357
増加するリクエスト数(毎分) = (RPH-per-person * 357) / 60
と表すことができる。
この結果 35.7 RPMとなる。

既存のリクエストレートと合算し、性能を担保をすべき最低限のリクエスト数は
41.7 RPM ≒ 0.7 RPS
となる。

性能目標

今回レバウェルでは、以下のことから改善対象APIをp95 1秒以下でレスポンスすることを性能目標として定めました。

  • 主要機能を改善対象とする(後述)
  • 本サイトにメディア的性質がある
  • SEO要件からより速い描画速度が求められる

API実行レートはTVCM配信後に波のようなスパイクが来ることを想定し、見積もりよりかなり余裕を持たせて5 RPS でテストすることにします。

どこを改善する?

主要機能の分析

さて、ここまでで速度改善の必要性を判断するための基準は準備できました。 次にやるべきことは、その基準に基づいて必要性の可否を判断することです。 速度改善の取り組みは時間がかかることが予想されるため、全ての機能について改善することは費用対効果的によろしくありません。 そこでレバウェルでは、まずAPIの実行量を観測し、速度改善に値するものを抽出することにしました。

API実行量の観測

今回はApollo StudioのAPI統計情報から最も実行されているGraphQL Operationを探しました。

Insightsから統計情報を確認します。 リクエストレートを確認してみると、上位2つが多く実行されていることがわかります。 対応する画面を確認したところレバウェルの中核となる部分と対応しており、システム的にこれらの二つが主要機能と言えます。

本記事では最もレートが高かったリクエスト名をtarget(仮)、 それに対応するAPI名をgetTargetInfo(仮)として説明します。

負荷テストによる速度調査

特定した画面およびAPIについて速度改善の必要性があるかを判断します。 判断基準は決定しているため、そちらに従って負荷テストを行います。テストランナーはk6を使用します。

まず先ほど特定したリクエストレートの高いGraphQL Operationを使用している画面について試験を行います。 対象画面を含めた合計4つの画面に仮想ユーザー数100で繰り返しアクセスすることとします。

http_req_duration(p95)が12秒超という非常によろしくない結果です。

次にバックエンドサーバーに対し、画面で要求しているフィールドと全く同一な状態でAPIを実行します。

getTargetInfo(仮)を5 RPSで実行した結果

avg=7.44s min=430.08ms med=8.62s max=9.73s p(90)=9.47s p(95)=9.53s 

これはp95 1秒以下でレスポンスするという性能目標を満たしていないため、改善する必要があると判断しました。

どうやって改善する?

基本的なwebアプリケーションであればSQLが最もボトルネックになりやすい箇所です。 そのためまずSQLに注目することにしました。

以下の流れに従って調査します。

1. ボトルネックとなっているSQLを見つける.
2. そのSQLが遅い原因を理解する.
3. 改善する.
4. 改善したSQLを測定する.

どういったSQLが実行されているか

対象のAPIを実行した時、どのようなSQLが実行されるのかをチェックします。 ここで明らかな問題(N+1問題や初歩的なWHEREでの絞り忘れなど)があれば、まずはそれを解決できるからです。

今回は以下のようなテーブル構成とGraphQLのqueryを例とします。

query target($getTagetInfoId: Int!) {
 getTagetInfo(id: $getTagetInfoId) {
   id
   name
   status
   targetSkills {
     skillId
     level
     skill{
       id
       name
     }
   }
 }
}

targetテーブルに連なるリレーションはTypeORMのLazy Relationsによって解決します。

この場合、要求されたリレーションのフィールド1つにつき1件のSQLが実行されます。 この例の場合、以下のように少なくとも3回実行されるイメージです。

1. 指定されたIDを持つターゲット情報を取得する。
これはgetTagetInfoフィールドに対応します。

SELECT * FROM target WHERE id = $1;

2. そのターゲットに関連するtargetSkillsを取得する。
これはtargetSkillsフィールドに対応します。

SELECT * FROM target_skills WHERE target_id = $1;

3. 各targetSkillsエントリに関連するskillを取得する。
これはskillフィールドに対応します。 targetSkillsの各エントリに対して1つのクエリが実行されるため、このステップでのクエリ数はtargetSkillsのエントリ数に依存します。

SELECT * FROM skills WHERE id = $1;

実際にレバウェルの該当ページを開いてGraphQLリクエストを発生させたところ、およそ100件を超えるSQLが実行されていました。

これ自体はGraphQLの設計思想上、「要求されたものだけを取得する」ということで間違っていません。また、前述(導入済みの性能対策)の通りDataLoaderを用いた各リレーションごとのバッチ処理は行っています。 しかし該当ページは描画要件の都合により、取得しなければならないフィールドが多く、DataLoaderによる性能改善には限界がありました。

約100テーブルに及ぶ膨大なリレーション情報が必要となると、データ解決手法によってはパフォーマンス問題が発生する恐れがあります。 またコネクションプールの枯渇、ネットワークコストの増加による負荷増の問題も考えられます。

Slow Queryはあるのか

次にSlow Queryがあるかを調査します。 仮に実行時間の大半を占めるようなSlow Queryがあり、それを解消すれば良いなら100件のSQLを実行する形のままでも目下の障害にはならないからです。

今回はpostgresのpg_stat_statementsモジュールを使って分析しました。
対象APIを呼ぶページを開き、以下のSQLでSlow Queryを抽出します

SELECT * FROM pg_stat_statements order by total_time desc limit 100;

今回は個別のクエリは軽いようで、 一番かかっているもので26msほどでした。 これはindexや個別のSQL最適化が意味をなさず、100件の分割されたクエリを何らかの方法でまとめなければならないことを意味しています。

改善方針決定

次は100件のSQLを1つのSQLにまとめるなら、どのようにすれば良いのか考えます。 もちろん単純に100テーブルをJOINしてしまうと結果セットがデカルト積となり、爆発的な量となってしまうので実現不可能です。

そこで今回は以下の点に注目しました。

  • それぞれのSQLには複雑な条件がない。単純な外部キー結合がメイン
  • 結果セットの多さが問題

ここから考えられる対処法には以下の2つがあると考えました。

1. テーブル設計を変える
2. SQL上のテクニックで結果セットの数を削減する

1の方法は最も高品質なものができる解決法ですが、今回対象となるテーブルはレバウェルの根本となっているため、リプレイスに近い形になってしまうことから見送りました。2の方法はあまり見ない方法ですが、今回のケースではJSONB_AGG集約関数とJSONB_BUILD_OBJECT関数を用いることで結果セットを減らせそうです。 一方でSQLが複雑なものとなり保守性が低下してしまうのがデメリットです。
今回はメリット・デメリットを総合的に判断し2の方法を採用することにしました。

実装

100件のSQLを1つのSQLにまとめる

結果セット数を抑えつつ100件のSQLを1つにまとめるために、大きく4つのSQLの文法を使用しました。

  • Common Table Expressions

postgresではWITH ASの構文で表されます。一時的な名前付き結果セット、つまりサブクエリの結果を生成するために使用されます。主な利点は、複雑なクエリをより読みやすくかつ管理しやすくすることです。また、同じサブクエリを一度書いてそれを再利用することができます。
参考:https://www.postgresql.jp/docs/9.5/queries-with.html

  • LATERAL

LATERALを付与したサブクエリはそれより前のFROM項目を参照できるようになります。今回はLEFT JOIN LATERALの形で使用しました。これによってサブクエリが外部クエリの行に依存する場合に、それぞれの行に対して異なる結果セットを生成できます。そして、LEFT JOINの性質上サブクエリが結果を返さない場合、nullとして処理されます。
参考:https://www.postgresql.jp/document/9.6/html/queries-table-expressions.html

  • JSONB_BUILD_OBJECT

キーと値のペアからJSONBオブジェクトを作成するための関数です。この関数は任意の数の引数を取り、それらを交互にキーと値として扱います。行のデータをJSON形式で出力する必要がある場合など、データを柔軟に扱いたい時に便利です。
参考:https://www.postgresql.jp/document/13/html/functions-json.html

  • JSONB_AGG

引数を受け取り、JSONB配列に集約する関数です。
参考:https://www.postgresql.jp/docs/9.5/functions-aggregate.html

詳細な内容はPostgreSQL公式をご参考ください。

実装SQLはこのような形になります。

WITH
r_target AS (
   SELECT * FROM target t
   WHERE t.id = $1 AND t.status = 'active'
),
s AS (
   SELECT
       ts.target_id,
       JSONB_AGG(
           JSONB_BUILD_OBJECT(
               'skill', s.obj,
               'level', ts.level
           )
       ) as target_skills_info
   FROM target_skills ts
       INNER JOIN LATERAL(
           SELECT
               JSONB_BUILD_OBJECT(
                   'id', s.id,
                   'name', s.name
               ) AS obj
           FROM skill s WHERE s.id = ts.skill_id
       ) s ON true
   WHERE ts.target_id = (SELECT id FROM r_target)
   GROUP BY ts.target_id
)
SELECT * FROM r_target
LEFT JOIN s ON s.target_id = r_target.id

実行結果イメージ

id name status target_id target_skills_info
1 hoge active 1 [{‘skill’: {‘id’: 1 ,‘name’: ‘skill1’}, ‘level’: 5}, … ]

実際には上記の対応を全てのリレーションに適用し、そのSQLを用いることで効果の検証を行います。

補足

nullハンドリング

上述のSQLでLATERALを使用した理由としてネストしたオブジェクトのnullハンドリングがあります。 例えば、

-- v.hogeは存在しない
SELECT JSONB_BUILD_OBJECT(
    'v_id', v.id, 'hoge',
     JSONB_BUILD_OBJECT('name', h.name)
 ) FROM v LEFT JOIN hoge h ON h.id = v.hoge_id

この結果は{v_id: x, hoge: {name: null}} となります。

この時に、GraphQLで「v.hogeはnullableだが、hoge.nameはnot nullable」のような設定をしている場合は少々困ってしまいます。

そのため以下のようにしてnullハンドリングを行いました。

-- v.hogeは存在しない
SELECT JSONB_BUILD_OBJECT(
    'v_id', v.id, 'hoge', h.obj
) FROM v LEFT JOIN LATERAL(
    SELECT JSONB_BUILD_OBJECT('name', h.name) AS obj
    FROM hoge h 
    WHERE h.id = v.hoge_id
) AS h ON true

LATERALを使用することで、サブクエリが各vの行に対して独立して評価されます。そしてLEFT JOINであるため、h.id = v.hoge_id を満たさない場合はobjがnullとして結合されます。 従って{v_id: x, hoge: null}となり、一般的にイメージされるnullableなオブジェクトになります。

設計方針

今回選択した手法はGraphQLの大きな強みである「不要なデータフェッチを防ぐ」にやや反するものとなっています。 つまりGraphQLとSQLが分離できておらず、オーバーフェッチが発生し得る設計となっています。 その上で本方針を選択した理由として以下があります。

  • 該当APIの使用目的が限定されており、実際にはオーバーフェッチが発生しない
  • 工数の都合によりEager Loading的な設計を選択せざるを得なかった

プロダクト全体としてはLazy Loadingを使用し「クライアントが必要とするデータを必要な分だけ取得する」という設計方針で実装されています。

対策後の計測

API処理速度の検証

対策の効果を検証するため、上記のSQLを適用した新しいAPIに対して負荷テストを行います。 条件は前回と同様に5 RPS, 同一要求フィールド、同一のレコードです。

avg=59.98ms min=36.26ms med=50.89ms max=201.09ms p(90)=96.25ms p(95)=115.55ms

以前の結果

avg=7.44s min=430.08ms med=8.62s max=9.73s p(90)=9.47s p(95)=9.53s

レスポンスタイム(p95)で約82倍の高速化に成功しており、かなり改善をすることができました。

おわりに

以上がレバウェルにおける速度改善の取り組みとなります。 このような性能改善の取り組みは、コストなどの兼ね合いから最も理想的な改善策を選択できないこともあると思います。 しかしながら、状況に応じて最適な対策を打つことで高い効果を得ることができます。

本記事が少しでも参考となれば幸いです! 最後までお読みいただきありがとうございました。

現在、私たちと一緒に社会課題に挑戦するエンジニアを募集中です! ご興味のある方はぜひ採用サイトをご覧ください!

*1:実際に使用したリクエストレートなどの数値は計測値を使用しています。 今回は説明の都合上、仮の値を使用しています。

*2:GRPとは「Gross Rating Point(グロス・レイティング・ポイント)」の略語 一定期間に放送されたテレビCMの視聴率を合計したもの≒何回視聴されるか

*3:500GRPもしくは1,000GRPのCM出稿をしたら、何人に見られるのか?

【TECH PLAY Data Conference 2024】営業部門と挑むデータの民主化について発表しました

レバレジーズ株式会社 データ戦略室の森下です。

2024/01/25にTECH PLAY Data Conference 2024にて『営業部門と挑むデータの民主化』について発表したので、そのスライドを共有します。

営業人員が多い当社において、データの民主化にどのように取り組み、データ分析の専門家だけでなく、全社員がデータに基づいた意思決定を行い、成果を向上させた取り組みについて紹介しているので、是非ご覧ください。

イベント概要: techplay.jp

We are hiring

現在レバレジーズでは一緒に働いてくれる仲間を募集しています。ご興味のある方は、以下のリンクからご応募ください。

エンジニア採用ページ: recruit.leverages.jp

データエンジニア採用ページ: hrmos.co

第一回24時間AIハッカソンで準優勝しました

はじめに

テクノロジー戦略室クロスエンジニアリングチームの小林京輔です。業務では主にフロントエンドエンジニアとして、複数チームでフロントエンドのレビュー、ペアプロをしてます。

先日、社内のエンジニアと一緒に参加した(株)サードウェーブ主催第一回24時間AIハッカソン(2023年11月3・4日)で準優勝することができたので、ハッカソンで作成したアプリについて紹介したいと思います。

ハッカソン:connpass.com 記事:forest.watch.impress.co.jp

スライドp1

スライドp2

スライドp3

開発したアプリ

せっかく作るなら社会課題を解決するようなアプリケーションを作ろうという話になり、最終的に日常生活におけるストレスや精神的な負担を軽減し、誰もがいつでもどこでも気楽にアクセスできるパーソナルヘルスケアAIと銘打って「Kiraku」というアプリを24時間で作りました。

以前、看護ケアと現象学的研究を読んだことがあり、現象学的ケアという概念が新鮮に感じられました。

症状について対処するのではなく、患者さんの意向に沿った形でケアを行うこと(現象学的ケア)は、現象学的な文脈における「志向性」に着目した方法だと考え、現象学の流れを汲んだ概念を社会に実装したいという思いがありました。

上記を踏まえて、作成したアプリには大きく分けて2つの画面があり、問診画面、チャット画面があります。

問診画面では、いくつかの設問に答えてもらう形で、ユーザーの傾向を判定しました。

まずは精神的な悩みを抱えたユーザーに対する理解が必要だと考え、直面している問題に対して寄り添いを期待するタイプと、具体的な課題解決を求めるタイプの2つを想定し、プロンプトデザインをすることで問診を行います。

チャット画面では、各タイプに応じた応答をするようにプロンプトを書き、応答させるように設計しました。 寄り添いを期待するタイプのユーザーに対しては、常に共感とカジュアルな会話で悩みを和らげるように応答させ、具体的な解決を求めるユーザーに対しては問題解決に焦点を当てた直接的な質問とアドバイスをするようにし、ユーザーの傾向に合わせてパーソナライズされたチャットボットを作りました。

chat

さいごに

今回、人生で初めて参加したハッカソンでしたが、24時間という短時間で動くものが作れ、また準優勝という結果に結びついたことは素直に嬉しかったです。 また、深夜まで残ってくれたメンバーと一緒に開発して、組織として一つの目標に夢中になれたことは貴重な体験になりました。

レバレジーズでは社会課題を技術で解決していきたいエンジニアを募集しています。 興味のある方のご応募をお待ちしています!

開発者の、開発者による、開発者のためのインフラ監視への取り組み

こんにちは!レバレジーズ株式会社テクノロジー戦略室SREチームの蒲生です。 Datadog導入と監視のイネイブリングによってインフラ監視、アラート運用を開発者向けに民主化した事例を紹介します!!

この記事で伝えたいこと

  • インフラ監視をみんなでやるのいいぞ
    • サービスのスケールに柔軟に対応できるぞ
    • インフラのことまで考えて開発ができるぞ

↑の状態を達成するためにやったこと

  • 監視のイネイブリング
  • 監視による開発者への負荷軽減

インフラ構成が簡単に肥大化、複雑化する時代になってきました。 日々成長する事業を支えるために増えていく新規のマイクロサービスやスケールアウトされるサーバーに対して監視の設定をしていくことに悩んだ経験はないでしょうか。 私が主に担当しているレバテックの開発では、インフラ監視に開発者が携われる体制にすることで高速に変化していくインフラ構成に対して柔軟に対応できるようになりました。

当時の課題

当時の開発では以下のような前提と課題を抱えていました。

前提

アーキテクチャ

レバテックではAWSのEC2のモノリスアプリケーションからECSなどサーバーレス環境にマイクロサービスとして切り出していくリプレイスプロジェクトが進行中でした。

監視体制

当時の監視ツールはAWS CloudWatchを使用していて、CloudWatch AlarmやLambdaからメトリクスのアラートやエラーログをSlackに流すようにしていました。

アーキテクチャ図

課題

開発スピードに対して監視の対応が追いついていない

前提にある通りECSやLambdaを利用してマイクロサービスに切り出していくため インフラのリソースがどんどん増えていく状態に監視の設定や体制が追いついておらず、計測したいデータが取得できていませんでした。

インフラ関係のインシデントの多さ

アクセス負荷やデータ量の変化、不適切な実装によるインフラリソースへの負荷が原因でインシデントが発生してしまうケースが散見されていました。

インシデント発生時にかかる調査工数の肥大化

インシデントが発生したときにログやメトリクスを利用して調査を開始するのですが 先に述べたように取得できていないデータがあったり監視体制が整っていないこともあり 調査工数が肥大化して解決までに時間がかかっていました。

課題分析

当時の課題を以下のようにそれぞれ原因分析してみました。

監視対応の属人化

監視の設定や運用ができる人が社内で限られていて 追加されていくインフラの多さに対してボトルネックになっていました。 設定にかかる負荷が高いため設定漏れなどで取得していないデータがありました。 実際の作業に関する暗黙知だけでなくどの項目に監視の設定をすればいいのか、メトリクスの相関をどう見ればいいのかなどの知見の共有が足りていない状態だったと思います。

インフラリソースの使用状況の曖昧さ

実装者からみてリソースの使用状況が把握しづらい体制でした。 インフラのリソースが普段どのくらい使用されているのかを意識するタイミングが少なく、リリース後しばらく経って変化するメトリクスなどには気づきにくい状態だったと思います。 また当時のCloudWatchだと使い慣れている人でないとデータやメトリクスへのアクセス性が悪く、使用状況をぱっと確認できる手段がなかったのも曖昧になった原因となっていました。

調査時に発生するコミュニケーションコストの肥大化

当時監視設定やメトリクスのモニタリングをSREチームが担当することが多かったのですが、実際に開発をしているのは開発チームというサイロ化した状態でした。 インシデントが発生した際に異変に気づき調査を開始するのはSREチームだけど 何がリリースされたのか、コードがどう実装されているのかまで深く把握していないので開発チームと連携を取る必要があり、そこでコミュニケーションコストが発生していました。

解決策

当時知識があったわけではないですが今から振り返るとDevOpsのCALMSフレームワークでいうC(Culture)、A(Automation)、S(Sharing)を特に意識して解決策を考えたように思えます。

  • 監視設定、運用の負荷軽減

インフラ監視設定の自動/半自動化を進めることで設定漏れと全体の工数を減らし、インフラ監視の知識がない開発者でも設定ができるような状態にすることで属人化の解消を目指しました。 またDatadogの導入によってメトリクスやログの可視性を上げてデータへのアクセス性を向上させました。

  • 監視のイネイブリング

開発チームと一緒にメトリクスやアラートを確認する機会を定期的に作って、開発してリリースしたものがインフラやサービスにどう影響を与えているかを把握してもらうようにしました。 それによって開発者が監視設定や普段のモニタリングを行えるようにして、インシデント発生時に開発者の中である程度対応が完結できるようにしました。

具体的な施策

Datadog導入による監視設定、運用の負荷軽減

普段の設定、運用にかかる負荷を軽減するために 監視SaaSツールであるDatadogの導入を行うことでインフラ監視設定の自動/半自動化による設定工数の削減とメトリクス、ログの可視性向上を進めました。 それまで使用していたCloudWatchでは当時だと取得したいメトリクスの不足や統一的なアラート設定がしにくい、ダッシュボードのUIの使いにくさなどの課題があり、自動/半自動化を進めにくい状態だったのでこのタイミングでツールの切り替えを決断しました。

Datadogのワンちゃん

クラウドのリソースに監視用のタグをつけるだけで自動でDatadogにデータを送れるようにし、 新規のサーバーやマイクロサービスに対してはIaCツールによってコード化することで設定にかかる負荷を軽減して開発者によってインフラ監視設定を自走できるようにしました。 それによってSREチームやインフラに詳しい人がいなくても監視設定が進むようになりました。 また監視用のダッシュボードとログ検索機能を用意して、開発者が確認したいタイミングでぱっと見れるようにデータへのアクセス性と可視性を向上させました。

定期的なメトリクス確認会実施による監視のイネイブリング

開発者に監視の知見を深めてもらって運用できる状態にするために メトリクスダッシュボードを使って開発チームと一緒にメトリクス確認会を週次で実施しました。

まず先述したDatadogを使って各サービスごとにダッシュボードを作成し、開発者がいつでもインフラのメトリクスやログを確認できる状態にして そのダッシュボードを利用して週次で開発チームと週次でメトリクスやアラートを確認するメトリクス確認会を実施しました。

ダッシュボード

はじめはファシリテーションを私が行ってメトリクスの異常値や状態化したアラートなど会の中で確認したほうがいいものをシェアして、それらに対する調査や改善などのネクストアクションを出すまでを話す時間を作りました。 それによってリリースされたどのコードがインフラやユーザーに影響を及ぼしたかメトリクスやアラートの確認を通して開発者が把握して異変が見つかったら修正行動に移せるようになりました。

Slackでメトリクスについて話されているスレッド

開発者がある程度慣れてきたらファシリテーションをだんだん委譲させることで、SREチーム抜きで会が回るようになっていきました。 今ではインシデント発生時のアラートへの1次アクションは開発チームが行っています。

まとめ

監視設定の負荷が軽減されて開発者が運用に関わりやすい状態になり、サービスのスケールにインフラ監視が追従できるようになりました。 さらに、開発者が週次でメトリクスやアラートを確認する機会を作ることで、開発者が普段の開発がインフラレイヤに及ぼしている影響を把握し、アラートへのアクションを開発チーム内である程度完結できるようになりました。

インフラ監視の民主化の重要性を示すひとつの例となったのではないでしょうか。 なによりSREチームの私としては開発チームの方と共通言語を使ってサービスの課題について話し合えるようになって仕事が楽しくなりましたし 開発チームが独自に監視やログの設計を見直している活動を見たときは嬉しくなりました。

改めてここで伝えたいことのおさらい

  • インフラ監視をみんなでやるのいいぞ
    • サービスのスケールに柔軟に対応できるぞ
    • インフラのことまで考えて開発ができるぞ

今回は監視、アラートに関する取り組みでしたが他にも色々な取り組みを通じてSREチームとしてプロダクトの信頼性を保ちながらより質の高いリリースを可能な限り早くできる状態を目指して頑張っていきます!!

レバレジーズではサービスの信頼性を高める活動をしていきたいエンジニアを募集しています。 興味のある方のご応募をお待ちしています!

AWS re:Invent 2023 グルメ紀行 後編

前日 ~ 2日目は前編に書いています。 こちらの記事は、3日目 ~ 帰国までの記事です。 技術的な記事などはこちらから見れます。

3日目

3日目の会場は、MGM Grandで参加しました。

朝食

相変わらずの甘そうなパンとフルーツに加えて、スクランブルエッグ、ベーコン、ソーセージなどが揃ってました。ベーコンはかりかりでした。

ランチ

本日のランチは中華っぽい感じでした。白いお米もあったので日本人的にも良かったです。

デザートもかなり充実してました

おやつ?

昼過ぎから、ピンチョスみたいなのやお菓子も会場に出てました。

ナイトパーティ

3日目は、AWS Japanが主催してくれた、日本人向けのClosedパーティーに参加しました。2日目は独り言以外で日本語を話さなかったので、かなり日本語話せました。 料理は、話しているウェイターさんが「一つどう?」って声かけてきてもらって食べるという形でした。

二次会

一次会近く地元の飲み屋街でお互い初対面同士で二次会しました。 お酒を1本買うと1本ついてくるという謎システムと、謎のAsada Friesをつまみに飲みました。

4日目

4日目の会場は、Mandalay bayで参加しました。

朝食

甘いパンが割りと控えめでした。その代わりジャムとかが大量にあるパターン。

ランチ

ランチは自分で作るホットドッグ。

そしてなぜかポテチも

re:Play

4日目の夜は、AWSが主催のクラブイベント。 ステージが3つあり、フードコートなども併設。飲んで踊っての大宴会でした。

5日目

最終日はVenetianにて参加しました。

朝食

安定の甘いパンとフルーツ。後はマフィンバーガー?的なのと、re:Partyでも出てたPupusaがありました。今回のre:Inventで初めて食べましたが、エルサルバドルやホンジュラスの郷土料理だそうです。

昼食

re:Inventは5日目の昼に終了なので、ランチは出ませんでした。なので個人でShake Shackに行ってきました。 バーガーのコンボと、赤ワイン頼みました。お酒を頼んでいるとはいえ、$30超えで日本円で5000円以上の贅沢ランチ。

カジノにて

昼のハンバーガーがお腹に残っていて、お腹減らなかったのでビールだけ。

負けました。

帰国

機内食

よくわからない料理でした。(ラスベガスで食べてたもの、大半名前分からず食べてはいましたがw)

さいごに

いかがでしたでしょうか?ひたすら食べたもの貼り続けただけのブログなのでなんじゃこりゃって感じかもしれませんが、真面目な記事も書いているのでぜひこちらからご覧ください。

現在、私たちと一緒に挑戦してくださるグルメなエンジニアを募集しています。記事など読んでご興味持たれた方おられたら、ぜひご応募ください。(グルメじゃなくてもご応募ください)

recruit.leverages.jp

AWS re:Invent 2023 グルメ紀行 前編

はじめに

レバレジーズ株式会社 テクノロジー戦略室室長の竹下です。技術的なことだけでなく、ゆるっとした内容もレポートしたいと思ったので、現地の食べ物をレポートしようと思います。AWS re:Inventでは開催期間の朝食、昼食が全部出て、夜もパーティーなどが開催されているため、イベントの料理を中心にご紹介していきます。

AWS re:Invent前日以前

まだ始まっていないので前日は自分で店などを探して食べに行きました

飛行機

2食出ました。Chicken or Beafを期待してましたが、Chicken or PastaとYakisoba or Omletでした。残念です。

Chicken

と、Yakisobaを選びました

ランチ

Outback Steakでランチしました。さすがステーキの国。超肉厚

おやつ?

食べられません(バスソルト)

晩酌

さて突然のクイズです。ビールとビーフジャーキーでいくらでしょう?

答えはこちら(選択反転で見れます) => 約3300円(22$)インフレと円安やばい

AWS re:Invent 1日目

朝食

アメリカンな感じで、甘いパンしか無い。。。ヨーグルトとフルーツもありました。

ランチ

ランチはメキシコ風でタコスなどがありました。デザートも有り、かなり満足度が高い。 会場はVenetianで食べましたが、昼食会場もめちゃくちゃ広かったです。

あと、Wynnではコーラなどのドリンクも置いてました

晩ごはん?

エキスポ(スポンサーブース)で様々な料理とお酒が食べ飲み放題でした。皆酒を片手にスポンサーブースを回ってました(もちろん私も)

2日目

朝食

朝食は、相変わらずフルーツと甘いパンが有りましたが、追加で数種類のソーセージだったり、ポテト炒めたものや、卵白を固めたなにかなどがありました(egg white frittataという料理かも?) 甘いもの以外が出たので食べごたえ有りました。

ランチ

2日目は、セッションの間があまり無かったのでサンドイッチのLanch Boxにしました。りんごが丸々1個入っているあたりすごくアメリカを感じました。

ナイトパーティー

夜はDatadogなど4社がスポンサードしているナイトパーティーにお邪魔しました。
料理に関して、ウェイターが机を回っててピンチョスみたいなのを「ひとつどう?」と進めてくる形でした。そのため、会話に夢中で食事の方は撮り逃してしまいました。すみません。(3日目ではちゃんと写真撮れたのでご容赦を。似た感じでした)
ちなみに、お酒でワインを頼むと、white/red以外にぶどうの品種の確認されます。「ピノ・ノワールだけどいい?」とか。日本だとぶどうの品種を聞かれることなんて無いので新鮮でした。

3日目~

AWS re:Invent 2023 グルメ紀行 後編に続きます。

最後に

来年は自分もAWS re:Inventに出て美味しいごはんを食べたい!という方は、ぜひ下のリンクからご応募ください。

recruit.leverages.jp

AWS re:Invent 2023 - LLMOps: The lifecycle of an LLM の(おそらく)世界最速レポート

はじめに

レバレジーズ株式会社 テクノロジー戦略室室長の竹下です。 現在 AWS re:Invent 2023に現地ラスベガスで参加しています。皆さんに少しでも早くラスベガスの風を感じてもらうために、月曜午前の”LLMOps: The lifecycle of an LLM”セッションのレポートを、あの会社や、あのブログより最速でお届けしたいと思います。

他の記事も見たい方はこちらの目次からお辿りください。

セッション概要

LLMOpsを進めるに当たって、E-mailのサマリーを自動で作成するシステムを例に出しながら、どのような観点でどのように意思決定をするかをシミュレーション。また、AWSのサービスをどのように組み合わせてシステムを実現できるかの構成の紹介もあり

セッションの内容

今回は、Toolingと、LLMOpsの構築プロセスについての話をしていく。 E-mailのサマリーを自動で行うシステムを例にして話をしていく。 LLMOpsにおいても、「Start small, think big」は大事。 まずは、ただサマリーを作る部分から始めるものの、最終的には各人のドメインを理解したシステムを考えて作る必要がある

MLOpsとLLMOpsまたFMOpsの違いはなにか? どちらもほぼオーバーラップしている

LLMOpsには、3タイプのユーザーがいる。 Provider: Tuners, Customer それぞれ必要なスキル、注目点が違っている

LLMOps構築までのプロセスと、構築後の改善プロセスがある。 まずは構築段階の話をしていく

まずはUseCaseの分析が必要。Criticality, Scale, Task Type, Eloquence, ROIの観点が大きく考えられる。今回のE-mailのサマリーシステムの場合は、 Criticality: Low 内容多少違っていたとしても、メールの本文を読めば良いので、優先度は低い Scale: Low 自分のメールの入力だけで良く、出力もそのユーザーに向けてだけなのでデータ的にスケール要件は低い Task Type: Specific メールの要約というSpecificな範囲 Eloquence: Medium 文章的に自然である必要はある程度ある ROI: 重要。生産性を高めることがちゃんと出来る必要がある

LLMモデルを選択するときの判断軸としては以下のような物がある。

この中でUseCaseで重要な項目と優先度を見積もっていく 今回のE-mailのサマリーシステムの場合は Cost > Precision > Speed となる。

また、通常のDeepLearningなどはハイパーパラメーターなどの調整となるが、LLM、GenAIの場合は、プロンプトの調整となる

モデルが決定し、調整も出来たら、評価が必要。 モデルは完璧では無いため多層的に、図のようなチーズモデルによって評価し、致命的な問題が無いかを適正にチェックする必要がある。

構築が完了したら、あとは改善のループを回す必要がある。最初の構築はS3がらSageMakerのNoteBookなどを利用し、BedRokSummaryを投げて結果を返す。

システム化する、受け取ったメールのデータを転送、バッチを定期実行、出力された結果をメールする、QuickSightにつなぎこんで評価を可視化するなどをするため、以下のような構成になる

更に進化させるには、Orchestrationも必要となってくる

最後に

現地時間 2023/11/27 12:01 書き上げました。セッション聞きながらリアルタイムでまとめてリリースしたので、きっと世界最速レポートになっていると思います。 聞き漏らしたところもあるので完璧な内容ではない部分もありますが、ご了承ください。

現在、私たちと一緒に挑戦してくださるエンジニアを募集しています。ご興味のある方はぜひ採用サイトをご覧ください!

AWS re:Invent 2023 レポートまとめ

AWS re:Invent 2023に弊社エンジニアが現地ラスベガスで参加しました。 セッションなどのレポート等を随時更新していきます。 こちらはそのレポートなどへのリンクのまとめ記事です。

技術記事

おもしろ記事

さいごに

現在、私たちと一緒に挑戦してくださるエンジニアを募集しています。SREの募集もありますので、記事など読んでご興味持たれた方おられたら、ぜひご応募ください。

recruit.leverages.jp

家がクリニックに!? レバレジーズが挑んだ新規事業「レバクリ」開発の裏側を大公開

はじめに

 こんにちは。レバレジーズ株式会社システム本部の田中です。

 2022年11月から新規事業の立ち上げメンバーとして開発業務に携わっており、2023年6月にレバクリをリリースしました。社内では初となる大規模かつ決済を伴うtoCサービスであり、様々な挑戦をしながら開発を進めてきました。

 レバクリは「あなたの自宅をクリニックにする」をコンセプトに、オンラインで診療予約、診察、決済が完結し、自宅に薬が届くオンライン診療プラットフォームサービスです。「医療のあり方を変え、日本の医療における問題解決の主体者となる」ことをミッションとし、提携している医療機関と連携しながら、患者と医療機関の双方への最適な体験を提供できるサービスの実現を目指しています。

 レバレジーズではエンジニア一人ひとりが要件定義などの上流から実装まで幅広く担当することはもちろん、マーケティングやデザイン、オペレーションなど様々な領域の業務に関わることがあります。特に新規事業の開発においてはエンジニアに限らず少人数チームで始まることが多いため、エンジニアとしてではなく立ち上げメンバーとして様々なことに主体的に取り組むことができます。この記事では新規事業『レバクリ』の開発にあたって私が取り組んだことについて、以下二つの観点で紹介していきたいと思います。

  • 技術的側面
  • プロダクトマネージメント的側面

 レバレジーズでの新規開発でエンジニアがどのように活躍、成長できるのか、どんな形で貢献できるのか、少しでもイメージが鮮明になると幸いです。

サービスやチームの特徴をベースとした自由な技術選定

 技術選定といえば、新規事業のプロダクト開発の際の醍醐味の1つだと思います。レバレジーズでは技術選定は各チームに任されているので、事業の特性やチーム構成、メンバーの経験などをベースに行うことができます。

 今回、レバクリ開発チームでは「最速でのリリース」と「開発者体験」を考えた技術選定を行いました。チーム発足からリリース予定まで7ヶ月あまりと短かったことから、できるだけ開発速度が出せるように、またチームメンバーの開発体験をできるだけ高めることができるようにという意図でした。

言語

 言語はフロントエンド、バックエンドともにTypeScriptを採用しました。

 選定理由はかなり一般的な部分になりますが、

  • 社内でも標準技術となりつつある
  • 実装時に型チェックが効いてエラーを未然に防げる
  • フロントエンド、バックエンドで同じ言語が使える

 ことが挙げられます。

フロントエンド

 TypeScriptで開発できるFWとしてNext.jsを採用しました。

 Next.jsはFWの大きな特徴として

  • ReactベースでTypeScriptとの相性がいい
  • アップデートやリリースが高頻度でコミュニティも活発
  • キャッシュ戦略やReact Server Component対応など、高パフォーマンスのアプリケーションを実装するためのベストプラクティスが用意されている

 ことが挙げられます。

 また、私自身入社してから1年半Next.jsを使ったサービスの開発をしていたため、新規プロジェクトへのスムーズな導入が期待できたことがありました。他にNext.jsの経験があるチームメンバーはいませんでしたが、VueやNuxtの経験はあったため順応にさほどコストがかからないと判断しました。

技術面の特徴としては

  • bulletproof-reactベースのディレクトリ構成
    • featuresディレクトリ内で機能ごとに分けることで、機能間の結合を避けて無駄な共通化を考えずに開発速度を高めることができる
  • GraphQL Code GeneratorApollo Clientを組み合わせてデータ取得を実装
    • バックエンドのスキーマ定義から型を生成できる
    • 自動生成されたApollo ClientのHooksを使ってデータ取得処理やキャッシュ管理の実装工数を削減
  • App Routerを一部導入
    • v13.4でstableになったことを受け、一部の機能でApp Routerを採用

といった具合です。

バックエンド/API

 TypeScriptで開発できるNestJS、APIはGraphQLを採用しました。

 NestJSは

  • TypeScriptで開発されたFW
  • GraphQLと相性がいい
  • Express(もしくはFastify)がベースで実装手法が近い
  • 拡張性を残しつつ、FWとしてほしい機能が一通り揃っている

 といった特徴があります。私自身はTypeScriptでのバックエンド開発経験がほとんどなく、Expressを少し触ったことがある程度でしたが、ドキュメントが充実していて実装方法に癖があまりないのでスムーズに実装に入ることができました。

 GraphQLは

  • 単一のエンドポイントで1リクエストから複数リソースを取得できる
  • 取得する内容をフロントエンドで指定できる
  • バックエンドで定義したスキーマを型としてそのままフロントエンドで使うことができる

 特徴があり、ページ数が多く、同じデータを複数箇所で使ったり少し形を変えて取得したりするプロダクトの特性にフィットしていると判断しました。また、バックエンドとフロントエンドで型が一致することは開発者体験を非常に向上させるものとなりました。

技術面の特徴としては

  • オニオンアーキテクチャベース
  • NeverThrowを使って型安全にエラーハンドリング
    • try-catchでエラーに型がつかない問題をResult型を返すことで解消する
  • ORMとしてPrismaを採用
    • 多機能ではないが、直感的で実装が簡単なNode.js用のORM
    • GraphQLのN+1問題も解消してくれる

といった具合です。

社内スタッフ向けCMS

 記事などの動的コンテンツの管理にはStrapiというHeadless CMSを採用しました。

 Headless CMSというとあまり聞いたことがなかったり、知っていても使ったことはない方も多いかもしれませんが、表示部分(Head)以外の管理画面機能やデータ配信機能(API)を備えたものです。SEO流入獲得のために記事ページを展開する上でCMSの開発は必須ですが、リソースや保守を考えて0からの自作は避けたかったため、カスタマイズ性は確保しつつ実装を最小限に抑えられるCMSを探していました。

 Strapiは

  • TypeScriptに対応している
  • オープンソースなので無料で構築できる
  • REST apiもGraphQL apiもどちらも利用可能

 という特徴があります。自前で環境構築が必要ではありますが、料金がかからずコードを書けば簡単にカスタマイズや拡張ができる点からStrapiを選択しました。

 Strapiについて詳しく書くと長くなるので省略しますが、社内利用に制限されていて機能も最低限の管理画面とAPIが必要な程度だったため、想定よりも手軽に運用に持っていくことができました。

外部サービス

 認証に関しては、有名なIDaaSとしてCognitoAuth0Firebase Authenticationがありますが

  • 料金が比較的安い
    • Firebase Authentication < Cognito < Auth0
  • インフラをAWSで構築しているので親和性が高い
  • セキュリティ的な懸念があまりない

といった特徴からCognitoを採用しました。フロントエンドはAmplifyと組み合わせることで簡単に実装できるのと、サーバーの処理もAWS Admin SDKを使ってシンプルに実装が可能でした。また、AWSでインフラを構築しているので権限周りの設定も楽に行うことができました。CognitoとAmplifyを合わせた実装については別のスライドで簡単に説明しているのでそちらも合わせてご覧ください。

 決済プラットフォームはStripeを採用しました。実装するサービスやプロダクトの特徴によって大きく変わってくるので一概にはいえませんが、StripeはAPIが充実しており開発者向けのドキュメントも豊富なため、高いカスタマイズ性を求める場合に非常に相性がいいです。

ユーザー体験を第一に考えてサービス設計を主導

 技術選定はもちろんですが、新規事業の立ち上げメンバーとしてサービス設計も主導しました。オンライン診療事業としては大きな競合が2社ありました。それらのサービスの機能やフローを分析した上で必要な機能を洗い出し、より良いユーザー体験を届けるためにどうしたらいいか、どの機能がクリティカルで優先すべきかを整理しました。そして、最適なユーザー体験を第一にサービス設計を主導しました。

 ここでは特に設計に工夫をしたtoCのメールアドレス認証とtoBの診療画面について紹介します。

メールアドレス認証

 メールアドレス認証自体は機能として一般的ですが、社内の他サービスでは導入しているものが少なく、導入してCVRが下がった例もあったため、メールアドレス認証を入れるべきかどうかの議論からスタートしました。

 レバクリは基本的に登録したメールアドレスを使ってユーザーとサービス側がコミュニケーションをとる場面が多く、ログイン時のIDとしても使用する場合があるのでメールアドレスが存在しなかったり誤って入力されてしまうことはサービス運営上大きな障壁となります。また、セキュリティ面でもなりすましや第三者にメールが届いてしまうリスクにつながります。医療に関係するサービスであるため、受診歴や問診の回答内容が流出してしまうと大きな問題になってしまいます。ユーザーに安全にサービスを使用してもらうためにもメールアドレス認証は有効な手段でした。

 エンジニアとしてはセキュリティリスクを最小限に抑えるためにメールアドレス認証が必要と判断しましたが、事業サイドのCVRにおける懸念がある以上、セキュリティリスクを説明して理解を得る必要があります。以下のようなフローチャートを作成し、リスクを最小限に抑えるためのパターンについて提示した上で、認証を挟むポイントとしてどこがベストか議論をしました。

 レバクリには複数の機能がありますが全て診療の予約が起点となります。言い換えると診療を予約するまでは流出がクリティカルになるような情報やなりすます機会はないため、診療予約が完了するまでにメールアドレスが認証できていれば最低限のセキュリティ要件を満たすことができます。ユーザー登録に必要な情報を登録してもらい、最後に予約を確定するタイミングにメールアドレス認証を要求するステップに工夫しました。

toB診察画面

 診察を行う医師やクリニックの事務作業を行うスタッフが使用する画面をtoB画面と呼んでいます。toB画面も診察の一覧表示や診察、薬の発送に関する画面など複数の機能が存在していますが、事業の状況やチーム構成によってユーザーが変化する可能性があります。例えば一人で診察から発送まで行う場合と、作業を複数人で分担する場合では導線はもちろん必要な画面の種類や数まで変わってきます。そのため、集客やサービス運営が安定してくるまでに細かい変更が簡単にできるように、あまり作り込まずに最低限の機能を用意してリリースすることを目指しました。

 初期のスコープは医師が全て対応するのを基本線として、提携クリニックの医師にヒアリングをしながら設計しました。医師が普段の診察時に使っているシステムについて知見のあるメンバーがほとんどおらず、リファレンスもなかなか見つからずに難しいタスクでしたが、適宜画面の構成や機能についてすり合わせたり、プロトタイプを触ってもらってフィードバックを受けながら進めました。

 現在はCSチームが設置されたり、作業者の分担があったりと事業の状況も変わってきていて、初期はスコープから外していた検索機能を利用シーンにあった形で実装したりと日々アップデートが行われています。

今後の課題

 最後に今後の課題として取り組んでいきたいことについてご紹介します。

開発組織の体制整備

 チーム発足からエンジニア正社員2名+業務委託数名で開発を続けており、初期スコープではあらかじめ全て設計してから進めるウォーターフォールの形で開発していました。現在はチームメンバーも徐々に増え、複数機能を並行して設計、開発、リリースするアジャイル開発に近い形になっていますが、整備されているわけではないのでタスクの管理工数の増大やチームとしてパフォーマンスを最大化できていません。社内で導入しているチームが多いスクラム開発を含めて、機能開発を進めながら開発組織の体制も整備していきたいと考えています。

プロダクトとして競合に対する優位性の確立

 レバクリはオンライン診療サービスの中では後発になるため、現在は競合のレベルに追いつくための機能開発が中心になっています。着実に機能開発が進んできており、年内には機能として競合に遜色ない状態になる見通しとなっているため、今後は追いついた先で市場1位をとるためにプロダクトの強みを伸ばしていく必要があります。具体的には、予約やオンラインでの診察といった基本機能だけでなく、医薬品の服用をリマインドしたり服用の中での副作用への不安をサポートできるような機能や、診察の結果や自らの健康状態を確認できる機能など、プロダクトとして何度も使ってもらえるような仕組みを作っていくために、ビジネスサイドに食い込んで事業成長に貢献していけたらと考えています。

さいごに

 最後までお読みいただきありがとうございました。レバクリを一番選ばれる最高のサービスにしていくことを目指して、これらの課題に取り組み、より強い開発組織を作っていきたいと考えています。現在、私たちと一緒に挑戦してくださるエンジニアを募集しています。ご興味のある方はぜひ採用サイトをご覧ください!

MLOps1年目 - SageMakerを使う中で苦労した課題解決とこれからの展望

はじめに

 テクノロジー戦略室MLOpsチームの古賀です。MLOpsチームでは、レコメンドエンジンのMLOps基盤の構築や、AWS PersonalizeなどのMLサービスやLLMなどML活用を、企画から構築まで進めています。本テックブログでは前者のMLOps基盤構築の取り組みを紹介します。

 以前投稿したテックブログにあるように、AWS SageMakerとStep Functions Data Science SDKを導入し、データサイエンティスト主導のMLモデル改善フローを構築できました。しかし、ゴールというわけではなく、まだまだ改善の余地がありました。

 そこで、本テックブログでは、課題の洗い出しや分析、解決のための取り組みを紹介します。そもそもMLOpsを行う背景は以前投稿したテックブログの「背景」に記載しているので、興味ある方はご確認ください。

以前のテックブログまでにやったことと課題

 まず前提として、今までのMLOpsシステム構成を知る必要があります。下記のような構成になっています。説明の都合上、図を書き直していますが、構成は一緒です。

 一連のMLプロセスがパイプライン化されているため、データサイエンティスト主導でモデル改善フローを実行できます。また、学習-推論時で前処理を共通化しているため、Training-Serving Skewを改善出来ています。

 一方で課題がいくつかありました。

  • 特徴量作成
    • 特徴量を再利用できない
  • 学習:開発時、学習パイプライン(前処理から評価まで)の実行時間が長い
  • デプロイ:単一モデルのデプロイフローしか整備されていない
  • 全体
    • パイプライン実行の複雑化
    • PoCからシステムへの初期導入のリードタイムが長い

各課題とその解決策の分析

 各課題を分析し、必要な解決策を説明します。

特徴量の再利用性の低さ

 現状S3に特徴量を出力しています。S3に特徴量を格納している場合、作成者以外の人が特徴量の利用を判断するのが難しくなります。理由は、S3には特徴量のメタデータ管理やバージョン管理の機能がないからです。これらの機能がない状態で作成者以外の人が利用判断するためには、その特徴量が信頼できるデータソースから正しい変換処理により作られているかをソースコードから読み解き確認し、その結果作られる特徴量が正しいかも確認する必要があります。

 そこで、特徴量のメタデータ管理機能やバージョン管理機能を持ったデータストアに格納する必要があります。

学習にかかる時間の長期化

 学習パイプラインの実行による動作確認に時間がかかっていました。理由は2つあります。

  • 不要なステップも実行されるため。例えば、評価の処理を変更した場合、評価ステップのみ実行すべきですが、前処理ステップから実行してしまいます。
  • SageMaker Processingの起動に時間がかかるため。1つあたり5分程度かかるため、全て実行すると起動だけで15分程度かかります。

 これらの課題を解決するためには、必要なステップだけ実行し、起動時間を減らす必要があります。

単一モデルデプロイフローしか整備されていない

 単一モデルのデプロイのみ実装しており、複数モデルのオンライン評価の仕組みが用意されておりませんでした。そのため、複数モデルのデプロイとそれらのオンライン評価の仕組みが必要です。

パイプライン実行の煩雑化

 デプロイパイプライン以外の動作確認手順が煩雑でした。例えば、ライブラリをインストールし特徴量変換処理を変更した場合、下記の作業が必要でした。

  • 動作確認用のAWSリソースを作成(データ取得パイプライン、S3バケット)
  • Dockerイメージのbuildとpush
  • 動作確認用のデータ取得パイプライン実行notebookを実行

変更するたびに、動作確認手順が変わるのは認知負荷が高い上、対応漏れも発生します。加えて、変更箇所によっては、AWSリソースは他のパイプラインやLambdaの作成も必要になります。これらの課題を解決するには、変更箇所によらない動作確認手段が必要になります。

PoCからシステムへの初期導入のリードタイムの長期化

 レコメンドプロジェクトを下記のように進めていました。

 MLOpsとして最優先で解決したいのは、SageMakerへの載せ替えの長期化です。長期化している理由は2つあります。

  • プロジェクトごとに一からインフラ構築しているため
  • PoC後にMLエンジニアが構築したパイプラインに載せ替えているため

 特に後者の工数が大きく、PoCではPoC後の移植を想定したコードを書いておらず、スパゲッティコードのような状態で移行が大変でした。加えて、載せ替えにあたり、データサイエンティストとMLエンジニアのコミュニケーションコストもかかっていました。

 これらの課題を解決するためには、データサイエンティストの開発スピードを落とさず、作ったモデルをそのままシステムに組み込めるような基盤が必要になります。

これまでの取り組み

 上記課題の中で実際に取り組んだことを紹介します。特徴量の再利用性についてはFeature Store + Dataflowの検証までになりましたが、他の課題はある程度解決できました。Feature Store + Dataflowの検証についても紹介します。

特徴量の再利用性の低さ

 AWS SageMaker Feature StoreとGoogle Cloud Dataflowを選択し、検証を進めています。AWS SageMaker Feature Storeを採用した理由は下記の通りです。

  • 特徴量のバージョン管理機能やメタデータ機能があり、特徴量の再利用性を高められるため

Google Cloud Dataflowを採用した理由は下記の通りです。

  • データ変更後、リアルタイムに特徴量に変換して格納するため
  • pandas互換のAPIをサポートしており、データサイエンティストも扱えるため
  • 他チームでRDSからBigQueryにETL処理するためにGoogle Cloud Dataflowを導入予定で技術を標準化するため

 現在は下記の構成の検証を進めています。

白抜き部分の実装は概ね完了したので、これから検証 / 本番環境にデプロイし問題がないか確認していきます。問題がなければ、稼働中のレコメンドプロジェクトにも導入していく予定です。

学習にかかる時間の長期化

 SageMaker Pipelinesを導入し解決しました。導入した理由は下記の通りです。

  • キャッシュモードが存在し、不要なステップの実行をスキップできるため
  • ローカルモードが存在し、SageMaker Processing Jobの起動時間を短縮できるため

処理を変更したステップのみ実行できるので、最小限の実行時間にできました。

単一モデルのデプロイフローしか整備されていない

 複数モデルをデプロイできるようにし、推論エンドポイントであるSageMaker Endpointの前段に配置しているキャッシュサーバーにトラフィックを振り分ける処理を実装しました。なお、トラフィックを振り分ける際は、ユーザーに対して推論するモデルを固定しました。

 まず、ユーザーごとにモデルを固定した理由は下記の通りです。

  • ユーザーを困惑させないため。レコメンド機能を使うたびに結果が変わると、ユーザーが混乱するため。
  • 分析を簡単にするため。もしトラフィックをランダムに振り分けると、どのモデルが推薦したか追う必要があります。この方法よりは、ABテスト期間内でユーザーごとにモデルを固定した方がシンプルで分析しやすいと判断しました。

 次に、キャッシュサーバーに実装した理由は、SageMaker Endpointのトラフィック振り分け機能はランダムな振り分けしかなく、ユーザーごとにモデルを固定できないためです。そのため、SageMaker Endpointの前段のサーバーに振り分けロジックを実装しました。

パイプライン実行の煩雑化

 どのファイルを変更しても、Github Actionsワークフローを動かせば動作確認できるようにしました。加えて、Model Registryに登録された評価結果を承認し、対象のモデルをデプロイできるようにしました。

 Github ActionsワークフローはDockerイメージのbuildとpush、Lambda関数の作成 / 更新、データ取得と学習パイプラインを作成 / 更新し実行します。どこを変更しても、このワークフローを動かせば確認できるため、認知負荷を減らせました。加えて、必要な時のみDockerイメージの構築やLambda関数の更新を実行しているため、不要な実行時間はありません。

 Model Registryに登録されたモデルを承認し、デプロイパイプラインを実行します。このようなフローにしてる理由は、人間が評価結果を確認してからデプロイしたいためです。ただし、この判断基準を明確なルールにできるならば、Github Actionsワークフローでデプロイまで実行しても良いと考えています。

これからやりたいこと

 短期的には、レコメンドプロジェクトの開発効率を高めるために、解決できてない課題を解決したいです。

  • 特徴量を他プロジェクトで再利用できない
    • Feature Store + Dataflowを本格導入したいです。パフォーマンスに問題が無いことを確認する、他の機能との優先度の兼ね合いなどありますが、実現していきたいです。
  • PoCからシステムへの初期導入のリードタイムの長期化
    • MLOpsフレームワークの構築、または、標準構成のテンプレート化などで短縮させたいです。PoCする段階からSageMaker上で開発することで、SageMaker移行の工数を無くすためです。

 長期的には、開発効率だけでなく、ビジネスサイドがレコメンドが事業に与える影響を直感的に分かるようにし、ビジネスサイドとデータサイエンティストのコミュニケーションも効率化させ、レコメンドの精度改善スピードを向上させたいです。

まとめ

 以前のテックブログ執筆時の課題と解決のために、提案し実行した取り組みを紹介しました。まだGoogleが提唱する MLOps レベル1に達してないですし、使い勝手も改善していく必要があります。加えて、今回は紹介できませんでしたが、ML活用も推進しています。レバレジーズではMLOps基盤やML活用を企画から考え構築までやりたいエンジニアを募集しています。興味のある方の応募をお待ちしています。

「日本を、IT先進国に。」に向けて、レバテックCTO室を設立。「日本一のデータとシステムを持つ事業とそれを支えるアーキテクチャ」を目指して、組織・システムの改革を進める。

はじめに

こんにちは、レバテック開発部でテックリードを担当している河村です。 私はレバテック全体のシステム設計を担当しており、今後の事業拡大に向けて、理想のシステムを目指して、技術的負債の解消などの推進を行っています。

レバテックはこれまで、マイクロサービス化を主体においた技術スタックの刷新を行ってきました。これからはユーザー体験、業務プロセス、技術的負債を含めて「痛み」となっている部分の解消を進めていき、プロダクトやサービスとしての最適解を探索していきます。 そこで、今回はレバテックのシステム課題である「分散されたモノリス」の状態から「日本一のデータとシステムを持つ事業とそれを支えるアーキテクチャ」を目指してどのようなことを行おうとしているのか、そこに合わせて新設されるCTO室がどのような活動を行うとしているのか、レバテックの現状と課題を踏まえてご紹介します。

CTO室が設立された背景

レバテック - 事業ポートフォリオ

レバテックは現在、フリーランス・派遣・就転職、新卒の各領域を軸に様々なサービス・事業を展開しています。

※ 主なサービス、関わるシステム

  • レバテックフリーランス、レバテッククリエイター
    • ITフリーランスとしてのキャリアの可能性を広げるエージェントサービス
  • レバテックキャリア、レバテックダイレクト
    • 正社員としてのキャリアの可能性を広げる転職エージェント・スカウトサービス
  • レバテックルーキー
    • ITエンジニアを目指す学生の可能性を広げる就活エージェント・スカウトサービス
  • レバテックプラットフォーム
    • レバテックに登録いただいたITフリーランスのスカウトから、参画後の契約管理までを実現するプラットフォームサービス
  • レバテックID
  • 上記レバテックのサービスを支えるマイクロサービス群

レバテック - 事業進捗・サービス展望

レバテックは5年後、10年後の事業拡大に向けて、様々な事業展開を計画しております。

今までは営業とマーケティングの強みを生かして事業展開を行ってきましたが、今後は開発とデータを活用して事業展開を計画しており、開発とデータが強い組織・システムを構築しなければなりません。そのためにも、「日本最大級のIT人材プラットフォームの進化を支える」組織やシステムを構築する必要があり、ユーザー体験、業務プロセス、技術的負債の改善に向けて取り組んでいます。

レバテック - 現状のシステム全体像

レバテックの各サービス・事業を支えるシステムは上記のようになっています。 各システム間の連携や営業支援システム、外部サービスとの連携があり、レバテックに関連したレポジトリだけでも200個以上あり、大規模なシステムになりつつあります。

その中でもシステムとして最も課題としているのが「分散されたモノリス」のような状態(参考記事:What is 分散モノリス(Distributed Monolith))です。すべての機能がマイクロサービス化されているわけではないので、一般的な「分散されたモノリス」の状態というわけではないのですが、発生している問題としては近い状態です。

上記の図のようにシステムは各サービス・事業ごとに分散されています。ですが、この分散されたシステムは理想的な分割ではなく、既存システムの複製で作られたシステムであったり、人が増えたことにより出来上がったシステム、システムの本来の責務を超えた機能をもっていたりと理想のシステムとかなりギャップがある状態です。

そこで、「分散されたモノリス」となってしまっている原因として以下の問題があります。

  • 密結合なシステム
    • 人材や企業のデータも重複して保持している状態でかつDB直参照やバッチサーバ連携で行っているため、システムの追加や変更を意識して開発しなければならない
  • 非構造化データの存在
    • 文字列やEAVパターンを利用した暗黙的なリレーション構造が存在し、DBに格納される文字列を変更するだけでシステムの障害のきっかけとなり得る
  • マスタデータ(職種やスキルなどのリファレンスデータ)の分散
    • 各システムでマスタデータのIDや項目が異なり、マッピングをして連携や分析を行っている

上記の問題は2、3年前から顕在化していました。ですが、いきなりすべての問題に取り組むのは難しく、また、当時の技術スタックも古い状態で稼働していました。なので、まずは技術スタックの刷新やマイクロサービス化の導入から進めてきました(参考記事:マイクロサービス化を中心においた技術刷新とその狙いレバテックの未来に向けた開発組織の取り組み)。技術スタックの刷新により、静的型付け言語であるTypeScriptをベースとしたシステムにすることができ、負債を貯めにくい・解消しやすい設計手法であるクリーンアーキテクチャやDDD(ドメイン駆動設計)の導入が行いやすくなり、計画的な負債の解消を進めやすい状態にすることができました。

今後は「分散されたモノリス」から「それぞれのシステムとデータが”独立化”し”疎結合化”された状態」を目指して、現在のTypeScriptをベースとしたシステムのリニューアルを行い、今後のレバテックを支える組織・システムを構築していきます。

つまり、今まで行ってきた「リプレース」を生かして、今後は「それぞれのシステムとデータが”独立化”し”疎結合化”された状態」に向けて「リファクタリング」「リプレース」「リニューアル」を計画的に進めていきます。

※ リファクタリング・リプレース・リニューアルの定義

  • リファクタリング
    • 現在のシステムをI/F(IN/OUT)やふるまいを変えずにコードを変更すること
      • 例:MVCからレイヤードアーキテクチャに移行する
  • リプレース
    • 現在のシステムをI/F(IN/OUT)やふるまいを変えずに別のシステムに載せ替えること
      • 例:PHPがベースで動いているシステムをTypeScriptベースのシステムに載せ替える
  • リニューアル
    • 現在のシステムをI/F(IN/OUT)やふるまいを変えて、新しい価値を生み出すこと
      • 例:システム・データの拡張性を向上させるため、ユーザー登録の仕組みや業務プロセスから改善を行う

CTO室で実現したいこと

レバテック - CTO室ミッション

上記のレバテックにおけるシステム・データの課題を解決をリードしていくため、この度、CTO室を立ち上げることになりました。

大規模なシステムになってきたレバテックの関連サービスを支え、今後の事業拡大に向けて、総合的なアーキテクチャや設計が求められることになります。特に、最も難しいと考えているのが、今のレバテックにおけるユーザー体験、業務プロセス、技術的負債の中で最も「痛み」となっている部分の特定とその「痛み」の解消です。 すべての課題をいきなりすべて解消できる規模ではなく、今後の事業拡大を鑑みて、最も「痛み」となる部分を分析・特定し、経営や他職種を巻き込んで、この「痛み」を計画的に解消を進めていく必要があります。なので、ただ単純に技術的負債を解消を進めていくということではありません。現状のユーザー体験や業務プロセスを含めて最適解を探索する必要があります。

レバテック - CTO室が目指す先

レバテックは現在、登録者数50万人、契約社数1万社を突破し、IT人材の2.5人の1人が登録するサービスになっております。上記の「分散されたモノリス」な状態を脱却し、レバテックにおけるユーザー体験、業務プロセス、技術的負債の中で最も「痛み」となっている部分を解消し、目指す先は「日本一のデータとシステムを持つ事業とそれを支えるアーキテクチャ」を作ることです。

レバテック - コア要素『D-POS』のAsis-Tobe

「日本一のデータとシステムを持つ事業とそれを支えるアーキテクチャ」になるために上記のTobeのような状態が必要だと定義しています。つまり、データを軸にプロダクト/オペレーション/システム(Data→Product/Operation/System)を再設計していく必要があると考えています。

ここまでをまとめると、

  • レバテックは5年後、10年後の事業拡大に向けて、様々な事業展開を計画している
  • そのためには、「日本最大級のIT人材プラットフォームの進化を支える」組織やシステムを構築する必要がある
  • 現在、組織・システムとして「分散されたモノリス」という状態の課題を抱えている
  • 理想を「日本一のデータとシステムを持つ事業とそれを支えるアーキテクチャ」と定義
  • そこに紐づくTobeを目指してレバテックを再設計していく

CTO室の今後

今後、CTO室は上記の課題から以下の軸をベースに強化していきます。まだまだ具体的な目標などは決められていませんが、以下を軸にCTO室の役割を拡大していけたらなと考えております。

  • 事業戦略に紐づいたシステム戦略のリード
  • テクニカルとドメインを掛け合わせたドメインの最適化
  • 事業横断した組織・システムの最適化
  • 開発者生産性の向上

事業戦略に紐づいたシステム戦略のリード

CTO室は、今後ますます事業戦略に連動したシステム戦略を主導する役割を果たします。この取り組みにより、技術戦略がビジネスの長期的な目標と一致し、我々の競争力を向上させ、新たな市場機会を探求します。「日本最大級のIT人材プラットフォームの進化を支える」組織やシステムを構築し、戦略的な成長を支えることを目標に進んでいきます。

テクニカルとドメインを掛け合わせたドメインの最適化

テクニカルな専門知識とビジネスドメイン知識を融合させて、ドメインの最適化に取り組みます。このアプローチにより、特定のドメインに特有の課題に対処するための優れたソリューションを提供します。そこで、レバテックの事業/開発双方の戦略及び戦術に基づく特定のテクニカルドメイン領域における信頼されるスペシャリストを多く輩出していき、新たな可能性を切り拓きます。

事業横断した組織・システムの最適化

CTO室は、組織とシステムを結びつけ、情報共有と協力を促進し、事業全体での最適化を実現します。これにはイネイブリング、SRE、QA、アジャイルCoEによる事業を横断した組織・システムの強化が必要です。冗長性の排除やリソースの最適利用、可観測性の向上により、より効果的に運営し、ビジネス目標の達成を目指します。

開発者生産性の向上

CTO室は開発者生産性の向上に注力します。新しいツール、プロセス、トレーニングを提供し、開発者がより効果的に作業できる環境を整備していきます。これにより、イノベーションが促進され、プロダクト開発をより支援できたらと考えています。 ゆくゆくはエンジニアが選ぶ「開発者体験が良い」イメージのある企業「Developer eXperience AWARD 2023」ランキング上位5にランクインに向けて取り組みを進めていきたいと考えております。

さいごに

これらの目標を達成するために、CTO室は積極的な取り組みを進め、テックカンパニーとしてより高いレベルの開発組織にしていきたいと考えています。現在、一緒にCTO室を推進していただけるエンジニアを募集しています!ご興味のある方はぜひこちらからご連絡ください!

アプリケーションの処理を40倍高速化!効果的な最適化手法と実践事例の紹介

レバレジーズ株式会社 HRテック事業部の桐生です。

アプリケーション開発において、重たい処理の高速化は避けては通れない課題の一つですが、なんとなくで取り組んであまり良い結果が得られなかったり、そもそもどこから手をつけていいか分からなかったりすることもあるかと思います。

本記事では、処理の高速化を上手に行うための流れと、各ステップで抑えるべきポイントをご紹介します。

実際に私が携わっていたプロダクトでも、今回ご紹介する流れに則って高速化に取り組み、最終的に処理時間を40倍以上高速化することに成功しました。こちらの具体的な事例も含めて詳しくご紹介しますので、ぜひ最後までお読みいただければと思います!

なお、こちらは6月に開催されたレバレジーズ テックフェスにて発表させていただいた内容と同じものです。

大まかな流れ

以下の流れで処理の高速化を行っていきます。

  1. 無駄を含む処理を見つける
  2. 処理の遅い原因を特定する(計測)
  3. 高速化のための手段を考え、実装する
  4. 実装した高速化の効果を測定する

「無駄」を含む処理を見つけるには?

遅い処理を目の前にした時に、まず考える必要があるのは「この処理に削れる無駄は残っているか?」ということです。当たり前の話ではあるのですが、既にリソースを十分に使い切っている処理を高速化するのは難しいためです。

「無駄」が存在していることを判断するのに重要なのが、処理内容から考えてどのくらいの時間がかかりそうかを予測し、それを実際の処理時間と比較することです。「こういう処理だからこれくらいの時間がかかりそう」という予測を精度良く行うことで、無駄を見つけることができるようになります。

処理の性質ごとにかかる時間を計算する

処理時間を見積もる際には、「どんなリソースで」「どれくらいの量の」処理を行っているかに着目します。例えば、多くの処理で使われるであろう「CPU」「ストレージ」「ネットワーク通信」は、以下に示すように1回の操作にかかる時間が非常に大きく異なります。

このため、処理の性質によって以下のように見積もりを変える必要があります。

計算処理が中心の処理の場合、時間がかかると感じるには少なくとも数百万回〜数千万回程度の計算ステップが必要になります。ソート処理や大規模な文字列処理、および画像処理等はこれに該当することがありますが、一般的な業務アプリケーションでこのレベルの計算負荷を求められる場面は比較的まれといえます。 逆に、ストレージ・ネットワークアクセスが中心の場合、もっと小さな数字でも時間がかかる場合があります。

処理時間を概算してみる

上記を踏まえて、実際に処理時間を見積もってみましょう。例として、以下のような処理を想定します。

  • 1000件/ユーザーのデータを、100ユーザー分集計する
  • 集計は単純な平均・合計等
  • データはDBサーバーから取得する

この場合、行われる処理は以下のように分類できます。

  • CPUでの計算: 1000×100 = 10万のオーダー → 1秒は決して超えない
  • データ取得: NW通信1回 + 読み出しに1秒〜数秒?

これらのことから、どんなに長くても数秒〜10秒程度で完了することが予想できます。よって、これよりも遥かに長い時間がかかっている場合はほぼ確実に無駄が潜んでいると考えられるわけです。

このように、処理内容から想定の処理時間を見積もることで、無駄を含んでいる処理を見分けることができます。

計測せよ!

無駄を含んでいそうな処理を特定できたところで、次はこの処理を速くしようという話になるわけですが、ここでやってしまいがちなのが「何となくここが遅そう」という推測だけで高速化に手をつけてしまうことです。しかし、これは次に示すように、可能な限り避けるべきです。

ロブ・パイクのプログラミング5カ条

プログラミング界隈で有名なロブ・パイクの「プログラミング5カ条」より、処理時間に関する2つのルールをご紹介します。

  • ルール1: プログラムがどこで時間を消費することになるか知ることはできない。ボトルネックは驚くべき箇所で起こるものである。したがって、どこがボトルネックなのかをはっきりさせるまでは、推測を行ったり、スピードハックをしてはならない。
  • ルール2: 計測すべし。計測するまでは速度のための調整をしてはならない。コードの一部が残りを圧倒しないのであれば、なおさらである。

(引用元: http://www.lysator.liu.se/c/pikestyle.html )

ここで述べられている通り、処理の重さが具体的に何に起因するのか、推測によって特定するのは非常に困難です。このため、推測によって作業に手をつけると、全く遅くない箇所を一生懸命高速化するという事態に陥る可能性があるのです。

このように、高速化を行う際には「どこが遅いのか」を計測して特定し、処理時間の占める割合が大きいところを削っていく必要があるのです。

処理時間の計測のための手法はいろいろと存在しますが、ここでは代表的なものを2つご紹介します。

タイマーによる計測

一つ目は非常に愚直な方法で、処理の前後の時刻(タイマーの値)を記録することで処理時間を計測する方法です。

例として、JavaScriptのDateクラスを使って処理時間を計測する方法を示します。

function run() {
  // 開始時の時刻を記録
  const startTime = Date.now();

  someHeavyWork();

  // 終了時の時刻を記録
  const endTime = Date.now();

  // 経過時間は、終了時時刻 - 開始時時刻 で求められる
  const elapsedTime = endTime - startTime;

  console.log(‘someHeavyWork:’, elapsedTime, ‘ms’);
}

Date.now()を呼ぶことで、その時点での時刻(ミリ秒単位)を記録することができます。今回は例としてJavaScriptの機能を用いましたが、他の言語でも現在時刻のタイムスタンプを取得する関数(PHPのmicrotimeやRubyのTime.now等)を用いることで同様に計測できます。これを利用して処理の開始時・終了時の時刻を計測し、その差を取ることで、間に挟んだ処理にどのくらいの時間がかかったかを知ることができます。

※なお、JavaScriptの場合はconsole.timeという関数を用いることでより簡単に時間を計測することができます。詳しくはMDNの該当ドキュメントをご参照ください。

このような時間の記録処理を、重い可能性のある処理の前後に挟むことで、実際に重い部分を絞り込んでいきます。(最初は大きめの範囲を挟んで、徐々にその範囲を狭めていく)

この方法は非常に原始的なので、どんな処理にでも適用できるという良さはあるのですが、その反面、計測処理を手動で挟んでいく必要があるため手間がかかります。次に紹介するプロファイルを用いると、手動で処理を挟まずに時間を計測することができます。

プロファイラの活用

プロファイラとは、関数ごとの処理時間を自動で記録してくれるツールです。プロファイラを用いると、プログラム実行中の関数呼び出しを追跡して、時間のかかっている部分を特定することができます。大抵のプラットフォームではそれぞれのプラットフォームごとに専用のプロファイラが存在しており、例えばNode.jsプロファイラを使って計測を行うと以下のような出力を得ることができます。

各関数の中でかかった時間が帯状に表示されています。また、グラフの上下関係は関数の呼び出し関係を表しています。下側にいくほど関数の呼び出し階層が深くなっていっています。

今回用いたNode.jsプロファイラの使い方の参考記事: Node.js の CPU プロファイリングでボトルネックを特定する

–-

今回は処理時間を計測する方法を2つご紹介しました。これらを用いて実際に遅い箇所を特定したら、ついに実際の高速化作業に取り組むことになります。

高速化に王道なし

いよいよ実際の高速化作業を行うわけですが、プログラムが遅い原因というのは実に様々で、そのため、高速化のための普遍的な手法も存在しません。ただ、それだけで説明を済ませてしまうのも寂しいので、今回は私が実際に関わった高速化の事例をご紹介して、高速化作業のイメージを掴んでいただこうと思います。

事例:月次処理の高速化

1つ目にご紹介する事例、給与計算ソフトにおける月次の勤怠締め操作の高速化です。こちらは会社ごとの全社員の勤怠データを集計して給与額を計算する処理なのですが、会社あたりのユーザー数が増加すると、計算完了まで何分もかかってしまうという問題がありました。

処理時間の見積もり

まずは、本来の処理時間を見積もってみます。具体的にかかっていた時間ですが、例えば300人程度の会社ですと7分以上かかっていました。ただ、実際に処理するデータの量を考えると以下のようになり、大きな乖離が生じています。

  • 300人 * 30日 = 9000個の勤怠データの集計 + 過去の有給使用データ(300人 * 数十件 = 数千〜1万件)の集計
    • 集計は基本的には時間を累積していくのみ
    • 実際は営業日は30日もないので多めに見積もっている
  • データの取得等含めて考えても何十秒もかかるのですらおかしい
    • 数万件のデータの取得(数秒) + データ処理(1秒以内) + 数万件のデータの書き戻し(数秒)にしかならないはず

これを踏まえると、この処理はまだ高速化の余地があると判断できます。

遅い箇所の特定

次に、実際に何が遅い原因となっているかを計測して調査します。処理部分にログを仕込んで計測した結果、DBのデータ読み書きに大半の時間を費やしていることがわかりました。

さらに詳しくコードを調査した結果、以下の2つの原因が判明しました。

  • 特定のテーブル(大きめ)にインデックスが張られていなかった
  • 1ユーザーの処理ごとに大量のクエリを発行していた

それぞれ詳しく説明します。

テーブルにインデックスが張られていなかった

今回の遅かった箇所の一つが、勤怠集計データを保持するRDBテーブルからのデータ読み込みです。読み取っているデータはユーザーごとに数十件程度なのですが(1ヶ月分のデータしか読み取らないため)、読み取りに異常に時間がかかっていました。これは当該テーブルに必要なインデックスが張られていなかったことが原因です。

データベースから条件で絞ってデータを取得する場合、何も設定しないとテーブル全体を検索するため、テーブル全体のデータ量に比例した時間がかかってしまいます。このため、通常は検索したいカラムにインデックスを張ることでこの時間を一定に抑えるのですが、今回操作していたテーブルでは検索対象のカラムにインデックスが張られていませんでした。このテーブルには過去の全ての勤怠データが保持されており、その総レコード数は数十万にも及んでいたため、不要なデータに対して検索をかけており時間がかかっていたのです。

対処法としては、単純に検索対象のカラムに有効なインデックスを張るだけでした。この対処だけで処理時間が元々の3分の1程度にまで減少しました。

1ユーザーごとに多数のクエリを発行していた

もう一つの原因が、1ユーザーごとにデータの取得・書き出しクエリを発行していたことです。今回の実装においては、1ユーザーごとに必要なデータを全てデータベースから取得しており、また結果データの書き込みも1行ずつ行っていたため、結果的にユーザーあたり100以上のクエリを発行しており、トータルで何万件ものクエリを発行してしまっていました。これによりネットワーク通信待ちとクエリ実行のオーバーヘッドで大きく時間がかかってしまい、何分も処理に時間がかかってしまっていました。

対処法としては、予め処理対象のユーザーのデータをまとめて取得することによりクエリ発行回数を減らしました。また、書き戻しの際もある程度まとめて書き込みクエリを発行するようにしました。


上記2つの施策を行ったことにより、計算アルゴリズムには一切手を加えませんでしたが、最終的に10秒程度まで処理時間を短縮することに成功しました。

事後の計測を忘れずに

高速化作業を行った後は、それが本当に高速化に寄与したかどうかを必ずチェックするようにしましょう。特に、2つ以上の変更を行った場合は、両方の変更に効果があったかどうかをそれぞれ調べないと、効果のない変更を入れ込んでしまうことがあります。実際に自分が高速化を行った際にも、片方の変更は高速化に寄与していたが、もう一方の変更はむしろプログラムを遅くしてしまっていた、という場合がありました。

高速化のためのコード変更というのは一般的にコードをより複雑にしてしまうものなので、効果のないコードはなるべく排除するようにしましょう。

まとめ

ここまでで、処理高速化のための大まかな流れを一通りご紹介しました。見積もりと計測に基づいて高速化作業に取り組むことで、より高い精度で作業を行うことができるようになるはずです。

本記事の内容が少しでも皆様のお役に立てば幸いです。最後までお読みいただきありがとうございました!