有名テック企業の技術ブログを、ひとつのフィードで。
フィード
912件
はじめに こんにちは!電気通信大学大学院 情報理工学研究科 修士1年の南村栞多と申します。2026年 ...
こんにちは。メルペイでソフトウェアエンジニアをしている @sapuri です。この記事は Merpay & Mercoin Tech Openness Month 2026 の 9日目の記事です。 はじめに 本記事は、2026年4月27日の Background Job Talk 〜 Temporal 活用と独自実装の舞台裏編〜 で発表した「内製ワークフローエンジンの設計とメルカリでの活用事例」を記事化したものです。 マイクロサービスアーキテクチャのような分散システムでは、複数のサービスにまたがる処理のデータ整合性をどう保つか、いわゆる分散トランザクションの扱いが大きな課題となります。 メルカリでは、この課題を Saga パターンによる結果整合性で解決するために、自社でワークフローエンジンを開発して運用しています。 このワークフローエンジンは、もともとメルコインの決済基盤における分散トランザクション管理のために開発したものです。メルペイの Payment Service で得た知見も取り入れながら設計し、現在はメルカリグループ内の複数のユースケースで利用が広がっています。 この記事では、内製に至った背景とワークフローエンジンの具体的な設計、社内での活用事例について紹介します。 分散トランザクション管理の課題と Saga パターン メルカリでは主にマイクロサービスアーキテクチャを採用しています。 そのため、お客さまがアプリで1つの操作をすると、そのリクエストは基本的に複数のサービスをまたいで処理されます。 例えば、メルカリアプリからビットコインを購入するときの決済リクエストでは、取引データの作成、メルコインの日本円残高の減算、メルペイのポイントの減算、ビットコイン残高の加算、取引データの更新といった複数の処理が関わります。 この決済リクエストは、1つのトランザクションとして扱う必要があります。つまり、一連の処理をすべて成功させるか、すべて失敗させるかのどちらかに寄せる必要があります。 しかし、各サービスがそれぞれデータベースを持っているため、単純にロールバックすることはできません。この点を考慮せずに実装すると、エラーのタイミングによってデータの不整合が発生します。 例えば、このような不整合が起こりえます。 決済が失敗したのにメルコインの日本円残高が減っている 残高は減ったがビットコイン残高が加算されない ビットコインと交換できているのに取引が完了扱いになっていない また、Two-Phase Commit のような分散トランザクションでは長期間リソースをロックするため、サービスの可用性が下がる可能性があります。 そのため、メルカリでは結果整合性のアプローチで、このような分散トランザクションを解決しています。 Saga この結果整合性を実現するためのアーキテクチャの1つとして、Saga というパターンがあります。 Saga は、トランザクションを複数の小さなトランザクションに分割して順次実行することで長時間のロックを不要にします。途中でリトライ不可能なエラーが出た場合は、成功済みの処理に対する補償トランザクションを逆順で実行します。 先ほどの暗号資産購入の例で、途中のビットコイン残高を増やす処理でリトライできないエラーが発生した場合を考えます。 この場合、この時点までに成功した処理を取り消す補償トランザクションを逆順に実行します。すでにメルコインの残高とメルペイのポイントが減らされているので、まずポイントを戻し、その次にメルコイン残高を戻し、最後に取引データを失敗として更新します。 このように実装することで、途中のどこで失敗しても結果整合性を保って処理を完了させることができます。 このあたりの話は以前の記事でも紹介しているので、興味のある方はそちらもぜひご覧ください。 メルコイン決済基盤における分散トランザクション管理 | メルカリエンジニアリング ワークフローエンジンの検討 実装の方針が決まったので、実際に Saga パターンを実装するためにワークフローエンジンの導入を検討しました。 主に検討したツールは、GCP Workflows、Cadence、Temporal です。メルカリでは主に GCP を使ってサービスを構築しているため、まず GCP Workflows を検討しました。ただ、各処理を HTTP のエンドポイントとして実装する必要があり、ユニットテストがやりにくいという懸念がありました。また、YAML ではなく Go のコードでワークフローを記述したいという要望もありました。 Cadence と Temporal も検討しましたが、メルカリでは Cloud Spanner をメインに使っているため、Spanner に対応していなかったことから採用できませんでした。また、Temporal はシステムの規模が大きく、仕組みも比較的複雑なため、運用面にも不安がありました。 このように、既存ツールでは要件を満たせなかったため、自社でワークフローエンジンを開発することにしました。 開発時には、Cadence / Temporal のインターフェースの良さを取り入れつつ、メルペイの Payment Service ですでに実績があった「DB への実行状態の永続化 x インメモリキュー x Worker での実行管理」のアーキテクチャを再利用する方針にしました。また、Go 専用で必要な機能のみに絞ることで、数人の兼務メンテナーでも運用できる規模にしています。 ワークフローエンジンの設計 アーキテクチャ このワークフローエンジンは、アプリケーションサーバーと同じ Pod でデプロイされることを想定しています。 Go runtime で動作し、利用者は SDK として扱います。 アプリケーションは Manager というインターフェースを使ってワークフローエンジンを操作します。主に Register と Execute という2種類のインターフェースを使います。 アプリケーションはワークフローを普通の Go の関数として実装するので、その関数の内容を事前にワークフローエンジンに登録する必要があります。 manager.RegisterWorkflow() が呼び出されると、Manager は Registry というインメモリの領域に関数を格納します。 manager.Workflow().Execute() は、実際にワークフローを実行するインターフェースです。 呼び出されると、Manager は Engine Server という gRPC サーバーに対して Workflow や Activity を作成するリクエストを送ります。 Engine Server は関数名や引数、実行状態を DB に保存し、インメモリキューである Channel に WorkflowStarted イベントを publish します。 その後、Worker という goroutine が WorkflowStarted イベントを subscribe し、Registry から実行する関数を取得して、Go のリフレクションを使って実行します。 実行が完了すると Worker は Engine Server に完了を報告し、Engine Server は結果を保存して WorkflowCompleted イベントを publish します。 その後、Worker が WorkflowCompleted イベントを subscribe し、アプリケーションに関数の実行結果を返却します。 もしその結果がエラーだった場合は、後述する ErrorMarshaler というインターフェースで、その Workflow を完了させるかどうかを判定します。 ここまで説明したコンポーネントの役割を整理します。 Manager: SDK のエントリーポイント。アプリケーションは Workflow()、Activity()、RegisterWorkflows() などを呼び出します。 Engine Server: Create、Complete、List などの gRPC API を提供するサーバー。DB に Workflow や Activity の I/O と状態を保存します。 Channel: Workflow や Activity の状態遷移イベントのハブとなるインメモリキュー。 Workers: Workflow や Activity を実行する goroutine 群。Channel から状態遷移イベントを購読し、イベントの種別に応じた処理を実行します。 Registry: Register された関数をインメモリで保持します。 Recovery Worker: Engine Server に対して定期的に未完了の Workflow と Activity を List してリトライします。 コードサンプル アプリケーション側の実装イメージは次のようになります。 func (s *Service) createExchangeWorkflow(ctx context.Context, params *CreateExchangeParams) (*CreateExchangeResult, error) { saga := workflow.NewSaga(s.wm) if err := s.wm.Activity(s.authorizeBalance, params.Balance).ExecuteWait(ctx); err != nil { return nil, err } saga.AddCompensation(s.cancelBalance, params.Balance) if err := s.wm.Activity(s.authorizePoint, params.Point).ExecuteWait(ctx); err != nil { if !isCompletableError(err) { return nil, err } if cerr := saga.Execute(ctx, func(e execution.Execution) error { return e.Wait(ctx) }); cerr != nil { return nil, fmt.Errorf("failed to execute compensation activities: %w, orig_err: %v", cerr, err) } return nil, err } return &CreateExchangeResult{}, nil } まず、createExchangeWorkflow という関数が定義されています。 この関数は、残高を確保する authorizeBalance という Activity と、ポイントを確保する authorizePoint という Activity を順に実行して結果を返す処理です。 特徴的なのは、それぞれの Activity を実行した直後に、この SDK が提供する Saga の AddCompensation インターフェースで補償トランザクションを登録している点です。 これにより、authorizeBalance Activity が成功した後に authorizePoint Activity が失敗した場合は、authorizeBalance を取り消す処理である cancelBalance という関数が補償トランザクションとして実行されます。 エラーハンドリング このワークフローエンジンでは、3種類のエラーを定義しています。 Completable Error: Workflow や Activity を失敗として完了させてよい、想定されたエラーです。例として、残高不足や利用制限があります。 Retryable Error: リトライ対象のエラーです。 Incompletable Error: Workflow を完了させずに停止し、Recovery Worker が後でリトライするエラーです。 Completable Error は、明示的に完了できるエラーだけを完了扱いにするための仕組みです。 クライアント側で ErrorMarshaler というインターフェースを実装したエラーとして定義される 該当しないエラーはすべて未完了として実行を停止し、Recovery Worker によってリトライされる 明示的に Completable Error を返さない限り Workflow は完了しない アプリケーションが意図していない異常な状態で Workflow が完了しない設計になる type ErrorMarshaler interface { MarshalCompletableError(error) ([]byte, error) UnmarshalCompletableError(marshaledErr []byte) error } 具体例として、ドメインのカスタムエラー型に Completable() というメソッドを定義し、それを使って ErrorMarshaler を実装します。 このようなカスタムエラー型を作っておくことで、ビジネスロジックで特定のエラーコードを含むエラーを返すと、ワークフローエンジンで完了可能なエラーとして処理されます。 type Error struct { code ErrorCode msg string } func (e *Error) Error() string { return e.msg } func (e *Error) Completable() bool { return e.code == ErrCodeCompletable } type workflowError struct { Code ErrorCode Msg string } type workflowErrorMarshaler struct{} func (workflowErrorMarshaler) MarshalCompletableError(err error) ([]byte, error) { var aerr *Error if !errors.As(err, &aerr) { return nil, err } if !aerr.Completable() { return nil, err } return json.Marshal(&workflowError{ Code: aerr.code, Msg: aerr.msg, }) } func (workflowErrorMarshaler) UnmarshalCompletableError(data []byte) error { var werr workflowError if err := json.Unmarshal(data, &werr); err != nil { return err } return &Error{ code: werr.Code,
はじめに はじめまして。東京電機大学大学院修士1年の佐藤聖璃です。 2026年4月の1ヶ月間、株式会 ...
こんにちは。メルコインのフロントエンド(FE)エンジニアとしてインターンをしている@nanacomです。この記事は「Merpay & Mercoin Tech Openness Month 2026」の7日目の記事です。 はじめに インターンではFEに限らず、要件定義からバックエンド(BE)開発まで、1つのプロジェクトに幅広く取り組みました。その中で、メルコインの社内ツールを開発する際に、2つのAPIの結果を日時降順にマージして返すエンドポイントを実装するケースに直面しました。 結果を結合して並べ替えるだけならシンプルですが、マージした一覧にもページネーションを提供しようとすると、各ソースのカーソルをどこまで進めるべきかが複雑になります。本記事では、「マージ結果として採用された件数」と「各データソース側で進めるべきカーソル」のズレにどう対処したかを紹介します。具体的には、データ取得とカーソル確定を分離する「2フェーズ取得パターン」と、各ソースのカーソルを1つのトークンに束ねる「複合ページネーショントークン」の2つの設計を取り上げます。 前提:対象とするユースケース マイクロサービスアーキテクチャでは、BFF(Backend For Frontend)で複数のサービスからデータを集約して一覧表示することがよくあります。今回対象としたのは、2つの独立したデータソース(A, B)のデータをマージするケースです。いずれも日時降順にソートされたデータを返し、それぞれがカーソルベースのページネーションAPIを提供しています。カーソルベースのページネーションとは、前回の取得結果の末尾を示すトークン(カーソル)を次のリクエストに渡すことで、続きのデータを取得する方式です。 この2つのソースの結果を日時降順にマージした一覧をクライアントに返しつつ、その一覧自体にもページネーションを提供する必要がありました。つまり、各ソースが独立して管理するカーソルを、BFF側でどう扱うかが設計上の焦点でした。 売買と入出金をマージした一覧表示(※表示データはすべてダミーです) 素朴なアプローチとその限界 この設計上の焦点に対して、私たちはまず2つの素朴なアプローチを検討しました。いずれも限界があり、最終的な設計への動機となりました。 アプローチ1:全件取得してソート 最も単純な方法は、両ソースから全件を取得し、アプリケーション側でソートしてからページごとに切り出す方法です。しかし、データ数が増えるとメモリ使用量とレイテンシーが線形に増加するため、スケールしません。 アプローチ2:各ソースからpageSize件取得してマージ 各ソースからそれぞれ pageSize 件を取得し、マージして上位 pageSize 件を選択する方法です。データ取得量を抑えられるため現実的ですが、ここで1つの問題が発生します。 例として pageSize=5 のとき、Aから [A1..A5]、Bから [B1..B5] が返ってきたとします(いずれも日時降順)。これらをマージして上位5件を作ると、マージ結果に含まれるのがAから3件(A1,A2,A3)、Bから2件(B1,B2)になるとします。 次のページでは本来、AはA4から、BはB3から取得を再開する必要があります。しかし各ソースAPIが返すカーソルは「返却リスト末尾の次」を指すため、手元のカーソルはA6(= Aを5件進めた次)やB6(= Bを5件進めた次)を指してしまいます。マージ結果に必要な再開位置(A4/B3)と、手元のカーソル(A6/B6)が一致しません。 (図1)各ソースから取得 (pageSize=5) Source A: [A1][A2][A3][A4][A5] -> cursorA = A6 Source B: [B1][B2][B3][B4][B5] -> cursorB = B6 マージして上位5件を採用すると、実際に消費したのは Aが3件 / Bが2件 になります(採用: A1 A2 A3 / B1 B2)。 このとき次ページで「本当に再開したい位置」と「手元のカーソル」がズレます。 ソース 次ページで本当は 手元のカーソル A A4 から再開 A6 を指す B B3 から再開 B6 を指す これが、本記事で解決する核心的な課題です。次のセクションでは、この課題に対して理想的にはどう解決すべきかを考え、そのうえで私たちが採った設計方針を説明します。 理想の解決策と現実の制約 カーソルベースAPIでは、返却件数とカーソルの進行量が常に一致します。pageSize=5 でリクエストすれば5件返り、カーソルも5件分進みます。しかし今回のように複数ソースのデータをマージするケースでは、5件取得しても実際に採用するのは一部だけです。この「取得件数」と「消費件数」のズレが根本原因です。 仮に各ソースのAPIがカーソルではなくタイムスタンプによる範囲指定をサポートしており、かつソース内のタイムスタンプが一意であれば、この問題は発生しません。例えば、以下のように、マージ結果で最後に消費したアイテムの日時を基準に次ページを取得できます。 GET /orders?before=2025-01-01T10:00:00Z&limit=5 GET /transfers?before=2025-01-01T10:00:00Z&limit=5 この方式であれば、各ソースの消費済み最終タイムスタンプを1つのトークンに含めるだけで、BFF側に状態を持たずに1回のリクエストでページネーションを実現できます。また before で過去方向に切るため、新しいデータが追加されてもページ跨ぎの重複が起きません。 しかし、各マイクロサービスのAPI仕様を変更するのは現実的ではないため、既存仕様のままBFF層で解決する方法を検討しました。 設計方針の決定 BFF層での解決策として、トークンへの情報埋め込み、サーバー側キャッシュ、データ取得とカーソル確定の分離という3つの方法を検討しました。設計のシンプルさとステートレス性を重視した結果、3つ目の「2フェーズ取得」方式を採用しました。 方法1:トークンに情報を詰め込む(拡張複合トークン) 各ソースのカーソルを1つのトークンに束ねて返す際に、カーソルだけでなく、次ページを再開するために必要な情報をまるごとトークン内に埋め込む設計です。例えば「Aから何件/Bから何件消費したか」のようなメタ情報も含め、JSONにまとめてBase64エンコードして返します。 { "cursorA": "abc123", "cursorB": "def456", "consumedA": 3, "consumedB": 2 } この方式だと、クライアントが次のリクエストでトークンをそのまま返すことで、サーバーはトークンをデコードするだけで「次ページの再開位置(A4/B3など)」を復元できます。 しかし、既存のソースAPIがカーソルベースの仕組みを提供している中で独自にオフセット等も管理すると、「カーソルの意味」が二重になり設計が複雑化するため、採用しませんでした。 方法2:Redisなどで「使わなかったデータ」を保持する(サーバー側キャッシュ) 各ソースから pageSize 件ずつ取得してマージした結果、採用されなかった"余り"のデータ(例:A4, A5 / B3, B4, B5)をサーバー側で保持しておく設計です。例えばユーザー(またはリクエスト)単位のセッションキーでRedisに格納します。 session:user123 → { unusedA: [A4, A5], unusedB: [B3, B4, B5] } 次のページのリクエストが来たら、 まずRedisに残っているデータを先に使ってマージし 足りない分だけ各ソースAPIから追加取得する という流れにすれば、カーソルのズレ問題を回避できます。 しかし、サーバー側に状態を持つことになり、社内ツールの規模に対してインフラの運用コストが見合わないため、採用しませんでした。 方法3(採用):データ取得とカーソル確定を分離する(2フェーズ取得) 上記2つの方法では、1回のAPI呼び出しでデータ取得とカーソル確定を同時に済ませようとしています。発想を変え、データを取得してマージするフェーズと、消費件数に基づいてカーソルを確定するフェーズを分けることで、この問題を解決します。サーバーはステートレスのまま、既存APIの仕組みをそのまま活かせます。API呼び出し回数は増えますが、最もシンプルな設計です。 許容するトレードオフ ただし、この方式では2回のAPI呼び出しの間に多少の時間差が生じます。そのわずかな間に対象データが追加された場合、次ページに重複したデータが現れる可能性があります。 私たちはこの問題を、以下の理由から許容可能なトレードオフと判断しました。 影響は「ページを跨ぐ際の重複表示」に限定される 対象がリアルタイムに頻繁に更新されるデータではないため、発生頻度は低い 完全な整合性を保証するには、各ソースのAPI仕様変更が必要になり、コストに見合わない この判断のもと、以降のセクションで方法3の具体的な実装を説明します。 2フェーズ取得パターン 前のセクションで述べた方法3を、具体的にどう実装したかを説明します。データを取得してマージするフェーズと、消費件数に対応するカーソルを確定するフェーズに分けて設計しました。 フェーズ1:取得とマージ ソースA、ソースBからそれぞれ pageSize 件を並行して取得する 日時降順でマージし、合計 pageSize 件を取り出す ソースAとソースBそれぞれで、実際に消費した件数を記録する この処理は、Go の container/heap を使ったストリーミングマージとして実装できます。各ソースの先頭要素をヒープに入れ、日時が最も新しいものを1つずつ取り出しながら pageSize 件を集めます。以下のコードのとおり、各ソースのインデックス(indexA, indexB)がそのまま消費件数を表します。 func Merge(pageSize int32, itemsA, itemsB []*Item) ([]*Item, int32, int32) { indexA, indexB := 0, 0 result := []*Item{} h := &timeHeap{} heap.Init(h) if len(itemsA) > 0 { heap.Push(h, &record{source: SourceA, time: itemsA[0].Timestamp}) } if len(itemsB) > 0 { heap.Push(h, &record{source: SourceB, time: itemsB[0].Timestamp}) } for h.Len() > 0 && len(result) < int(pageSize) { r := heap.Pop(h).(*record) switch r.source { case SourceA: result = append(result, itemsA[indexA]) indexA++ if indexA < len(itemsA) { heap.Push(h, &record{source: SourceA, time: itemsA[indexA].Timestamp}) } case SourceB: result = append(result, itemsB[indexB]) indexB++ if indexB < len(itemsB) { heap.Push(h, &record{source: SourceB, time: itemsB[indexB].Timestamp}) } } } return result, int32(indexA), int32(indexB) } 戻り値の indexA と indexB が、フェーズ2でカーソルを正確に進めるための入力になります。 フェーズ2:カーソルの確定 ソースA、ソースBそれぞれにおいて、フェーズ1と同じ開始位置から消費件数分だけ再取得し、進んだ位置のページネーショントークンを取得する(cursorA, cursorB) pageToken を cursorA:cursorB(参照:次のセクション)とすることで、次ページの取得時に正しい位置からデータを取得できる なお、一方のソースのデータがもう一方より古い場合など、フェーズ1でデータが返ってきたにもかかわらずマージで1件も採用されないケースがあります。この場合は、そのソースのカーソルを前回の位置のまま保持し、次ページのリクエストで再び同じデータを取得してマージの対象にします。また、フェーズ1でデータが0件だった場合は、そのソースを枯渇と判定し、ターミナルトークン _ を設定します。 (図2)フェーズ1:取得とマージ(消費件数を記録) Source A ──(pageSize件)──┐ ├→ Merge → Top N Source B ──(pageSize件)──┘ │ 消費件数を記録 (A=3件, B=2件) (図3)フェーズ2:カーソルの確定(消費件数分だけ進める) Source A ──(消費3件)──→ cursorA Source B ──(消費2件)──→ cursorB → 複合トークン: "cursorA:cursorB" 複合ページネーショントークン設計 2フェーズ取得パターンにより、各ソースで消費件数分だけ進んだカーソルを取得できるようになりました。次に、これらのカーソルをクライアントにどのように渡すかを設計します。今回の一覧取得APIでは、pageToken を各ソースのカーソルを結合した複合トークンとして設計します。 "cursorA:cursorB" 片方のソースが完全に尽きた場合は、ターミナルトークン _ で表現します。トークンがターミナルトークン _ だった場合、API呼び出しをスキップできます。これにより、初回リクエストから片方のソースが枯渇した状態まで、以下のようにページネーショントークンで表現することができます。 トークン 意味 "" (空文字) 初回リクエスト "cursorA:cursorB" 両ソースとも継続あり "_:cursorB" ソースAは枯渇、Bのみ継続 "cursorA:_" ソースBは枯渇、Aのみ継続 "_:_" → "" に変換 全データ取得済み(次ページなし) この複合トークンと2フェーズ取得パターンを組み合わせることで、サーバー側に状態を持たずに、マージした一覧のページネーションを実現できます。 まとめ 本記事では、カーソルベースAPIを持つ複数データソースから一覧を構築する際に直面した「マージで実際に消費した件数」と「APIが返すカーソル位置」のズレという課題と、その解決策を紹介しました。 最初は1回のAPI呼び出しで全てを済ませようとしていましたが行き詰まり、「データを取得するフェーズ」と「カーソルを確定するフェーズ」に分離することで解決できました。1つの処理が複数の責務を担って複雑になったとき、フェーズを分けて各ステップの役割を単純化するアプローチは、ページネーションに限らず設計全般で有効な考え方だと感じています。 このような設計上のトレードオフを実際に手を動かしながら考えられたのは、インターン期間中の貴重な経験でした。FEに限らず幅広く関わらせていただいたことに感謝しています。本当にありがとうございました! 次の記事は@mikupoさんです。引き続きお楽しみください。
こんにちは!バクラク申請・経費精算のエンジニアリングマネージャーをやっています、@ar_tamaです。 今回は、私たちのプロダクトで最近行ったバックエンドのパフォーマンスチューニング(スロークエリの改善)について書いてみたいと思います。比較的地味なトピックではありますが、Coding Agentをフル活用したエピソードとして、主にMySQLをバックエンドに持つアプリケーションの性能問題に直面している方の助けになれば幸いです。 発端:データ量や分布の変化で実行計画が変わった 私たちは、プロダクトごとに内部SLO(サービスレベル目標)を定めてモニタリングし、基準値を割ったらアクションすることを習慣づけています。 SLOというと大きな障害や可用性の話を想像されるかもしれませんが、特にバクラクのような業務システムでは、毎日繰り返し使う操作の遅さがそのまま利用者(お客様)の生産性低下に直結してしまうため、観測対象に主要エンドポイントの応答時間も追加しています。 今回その基準に違反したのは、複数のテーブルをJOINしてSELECTする処理のあるエンドポイントでした。これまでも必要に応じて、クエリをチューニングしたり、インデックスやインデックスヒントを追加したり、ミドルウェアを立てたり……と対策をさまざま取ってきた箇所です。今度は一体何が……?と調べたところ、たくさんのお客様にお使いいただきデータ量や分布(偏り)に変化が生じたことで、実行計画が意図どおり選ばれなくなったことが分かりました。 やったこと:AIと実行計画を眺める 対象のクエリが見えてきたので、実際に発行されているSQLとパラメータを取得し、Coding Agentと一緒にEXPLAINとEXPLAIN ANALYZEを確認しました。世は大AI時代ですが、主なやることは昔と変わりませんね。 EXPLAINは、MySQLがそのSQLをどう実行しようとしているかを見るためのものです。JOINの順序、使われるインデックス、推定行数などを確認できます。MySQLの公式ドキュメントでも、インデックスを追加すべき箇所やJOINの処理順を確認するための手段として説明されています。 一方で、EXPLAINはあくまで推定です。実際にどのくらい時間がかかったのか、どのステップでどれくらい行を読んだのかを見るには、EXPLAIN ANALYZEが役に立ちます。実際にステートメントを実行するので実行環境には注意が必要ですが、推定と実測のズレによってより精緻に改善することができます。 実際のSELECT文で双方を確認してみたところ、先述のデータ量・分布の変化によって、効かせたいはずのインデックスが選択されていなかったり、頻出のソート条件に合う複合インデックスがなくテンポラリソートが起きていたりと、大小様々な問題が発見されました。その中でも今回特に取り上げたいのは以下です。 JOINの起点が期待と違い、サブテーブルに持たせている種別の絞り込みから始まっていた その後、メインテーブル側で数万件規模の候補行を読み、除外条件を1行ずつ評価していた → クエリの責務を分け、サブテーブルの絞り込み→結果を使ってメインテーブルの絞り込み、と2回クエリを発行 以前のパフォーマンス改善対応(NOT IN+サブクエリでフィルタ)が、むしろ大きな集合を作るような実行計画に変化していた → サブクエリではなく、 カラム NOT IN (...) とクエリ自体を書き換えることで対応 1についてはEXPLAINの目視でもすぐに分かるところでしたが、2についてはEXPLAIN結果を分析したCoding Agentからの指摘で初めて気づきました。 以前の対応時は、たしかに除外対象をサブクエリとして扱うほうが効率的だったはずなのですが、現時点でのデータ分布ではそのサブクエリを作るために大きなスキャンが発生しており、インデックスを辿りながら直接条件を評価する方が速くできそう、という見立てが出されたのです。 同僚からその部分の実装意図を聞いていたこともあり、はじめは「いやいやそんな〜まさか〜」と半信半疑でしたが、修正版を試してみたところ劇的に計画が改善されました。余計なコンテキストを持たないほうが強いこともある……🥲 なお、今回はGORMを使ってクエリを発行している部分が修正の対象だったため、実際のクエリの書き換えにあたり、同僚であるponさんのgormgoldenが大変頼りになりました。アプリケーションコード上では良さそうに見えても、出力されたクエリが大事故…ということもときには生じ得るため、このようにBefore/Afterの確認を行いやすい状態にしておくことも大事ですね。 github.com 結果として、対象クエリ群のほぼ全てで改善が叶い、ベストケースでは50倍以上もの高速化が叶いました。やったぜ! デプロイ後にアラート対象のクエリが激減した様子 Coding Agentと人間との役割分担 今回のようなクエリチューニング作業においては、Coding Agentの網羅性と手の速さがとても頼りになります。クエリ改善で振る舞いが変わってしまうことのリスクとテストの重要性は前述の通りですが、分析・複数の改善案出し・検証・実装にいたるまで、積極的にAgentに任せながら改善を進めたおかげで、従来の改善活動よりもずっと早く修正が叶いました。 振る舞いの担保についても、以下のようなソースを参照して破綻しないことを確認してもらいました。 ヘルプサイトや仕様書 対応するクライアント(フロントエンド)の実装 これにより人の目だけでは拾いにくい検証観点も補え、動作確認も楽に行えました。 ただし、Agentは放っておくと限界までチューニングしようとしてしまいます。改善提案の中には、修正後のクエリが複雑になりすぎてメンテナンス性を損ねたり、修正量の割に改善効果が限定的だったりするものもありました。そのため計画時、または修正を走らせた後でも、「どの程度改善される見込みか」を確認しながら改善を採用する・しないの判断を行いました。 ほかにも、過去経緯などのコードや仕様に立ち現れないコンテキストを補完したり、ハルシネーションにツッコミを入れたり、計測と修正のサイクルを回すためにステップを分けたりなど、人間の仕事も思いの外残りました。とはいえ、今回の改善を回す過程で補完した文脈はドキュメントに残しましたし、モデルの進化も著しいため、次回以降はどんどん手がかからなくなっていくことが予想されます。 おわりに 今後の展望としては、今回の一連の流れを元に、SLO違反を検知→スロークエリをEXPLAINして原因を特定→修正・検証までを実行するSkillなどを作れると、改善のサイクルがより速く・多く回せそうです。 人間が本当に必要な判断に集中できるよう、またAI Agentたちにより多くの業務を任せられるよう、引き続き事業成長を支えるための改善活動を(も)進めていきます。 LayerXでは、AIと共に爆速で価値を生む・守る仲間を大大大募集しています!ご興味を持っていただけたらぜひこちらからご応募ください。 open.talentio.com 本職はマネージャーなので、AI時代のマネジメントどうなる〜?みたいな話も大好物です。カジュアル面談のお誘いもお気軽にどうぞ! jobs.layerx.co.jp
この記事は、Sansan Data Intelligence開発Unit ブログリレーの最終回(Vol.17)です。 Data Intelligence Engineering Unitの部長を務める多賀谷洋一です。 データクオリティマネジメント「Sansan Data Intelligence」をリリースして、半年が経ちました。半年間を通じて確信したのは、このプロダクトは変化の激しいAI時代の勝負どころに立っているということです。NVIDIA CEOのJensen Huang氏が「経済価値が生まれる場所」と呼ぶアプリケーションレイヤー、ここで勝つ条件は、独自データ・ドメイン知識・顧客基盤の3つ。Sansanはその3つを揃え、今まさに最前線に立っています。リリースから半年で事業は急速に立ち上がり、チームには確かな手応えが生まれています。今回はブログリレー最終回として、今このモーメンタムの中で働く魅力をお伝えします。 AI時代の「勝負どころ」に立つプロダクト Jensen Huang氏は、AI産業を5層のレイヤー構造として描いています。下からエネルギー、チップ、インフラ、ファウンデーションモデル、そして一番上にはアプリケーション。 下の4つのレイヤーはいずれも、参入に莫大な投資を要します。年間60兆円超が動くAIデータセンター、数千億円規模の学習コスト——資本の規模がそのまま競争力になる世界です。今から新たなプレイヤーが競争優位を築くのは困難です。 しかし、アプリケーションレイヤーは違います。顧客への価値を決めるのは資本の大きさではなく、独自のデータとドメイン知識です。 ただし、ここにも落とし穴があります。 Anthropicは今、ファウンデーションモデルを提供するだけでなく、Claude CodeやCoworkなどアプリケーション層へと大胆に事業を拡大しつつあります。つまり「単なるエージェント」を作るだけでは、モデルプロバイダー自体がとてつもなく強い競合になります。 Sequoia CapitalのJulien Bek氏は、この構造を次のように捉えています。 If you sell the tool, you're in a race against the model. But if you sell the work, every improvement in the model makes your service faster, cheaper, and harder to compete with. Services: The New Software By Julien Bek - Published March 5, 2026 (翻訳)ツールを売るなら、モデルとの競争を余儀なくされる。しかし仕事の成果を売るなら、モデルが進化するたびにあなたのサービスは速く、安く、競合に強くなる。 顧客に価値を届け続けるには、ツールではなく成果を提供できる立場に立つこと。そのためには、代替不可能な土台が必要です。 その条件は3つに収束します。独自データ(他社が真似できない独自のデータベース)、ドメイン知識(業界・業務の深い理解を体現したプロダクト)、そして顧客基盤(提供した成果に対して価値あるフィードバックして共創してくださる顧客)。 Sansanには、この3つが揃っています。そしてSansan Data Intelligenceは、まさにAI時代に顧客へ真の価値を届けていくプロダクトだと言えます。 なぜなら、AIエージェントがアプリケーション層で真の価値を生むには、自律的に判断・行動できる状態が必要であり、そのためには常に正しい情報を参照できる状態が不可欠だからです。 しかし、企業のデータ管理の現実はこうです。企業は数十のシステムにまたがるデータを保有し、同じ取引先を「A株式会社」「A(株)」「A社」と別々の名前や異なるIDで参照しています。AIエージェントが「A社の与信状況を確認して」と指示されても、同一企業を正確に結びつけられなければ、エージェントは間違った判断を下します。どれだけ高度なモデルを使っていても、データが正しくなければ正しい結果は出ません。 この問題を解決するのが、企業や事業所を一意の識別子(SOC: Sansan Organization Code)で管理し、常に正確で最新の状態に保つSansan Data Intelligenceです。このプロダクトは、その基盤となるマスターデータにおいてあらゆる企業や事業所に識別子であるSOCを付与します。個人に対するマイナンバーがあることで行政システムが連携して個人へのサービスを提供できるように、SOCがあることで複数システムを跨いで正しく企業や事業所を特定して連携させることができます。これにより、AIエージェントが目的とする企業や事業所を異なるシステムにおいて正確に特定し、そのリッチデータを取得し、正しく判断して自律的に業務を実行できます。 このように、Sansan Data Intelligenceは、Sansanだからこそつくることができる「AI時代の基盤」となるのです。 データクオリティマネジメント「Sansan Data Intelligence」 Sansan Data Intelligenceは、2025年12月にリリースされた「データクオリティマネジメント」サービスです。Sansanとして約4年ぶりの新規プロダクトで、構想からリリースまでわずか半年で実現しました。 企業が抱える取引先データの品質問題は深刻です。「重複・表記ゆれ・更新漏れ」を経験している企業は約8割にのぼります。(*1) この問題は、近年急速に広がるAI活用にも直撃しています。AI導入企業の約9割が「期待通りの精度が出ない」と報告していますが、その根本にあるのがGarbage In, Garbage Out——質の低いデータを入力すれば、出力も質の低いものになるという原則です。 具体的には、営業担当者の手入力による「株式会社」と「(株)」の表記ゆれ、企業の移転・合併情報の更新放置、市場全体の未取引企業が可視化されていない状態などが挙げられます。これらはいずれも、誤った情報を元にしたアプローチや商談機会の損失に直結します。 こうした問題は、社員が主導する一時的な名寄せプロジェクトでは解決できません。手作業では対応しきれない量とスピードで、データの劣化は常に進んでいるからです。必要なのは、構造的・継続的なデータ品質の維持です。 Sansan Data Intelligenceは、企業のCRM/SFAや基幹システムに蓄積された取引先データを、Sansanの企業データベース(事業所データ1000万件超)と照合し、4つの価値を継続的に提供します。 識別・正規化:表記ゆれや重複を含むデータを一意に識別して、正しい企業・事業所単位に統合 最新化:移転や合併、社名変更などの情報を自動検知し、常に最新のマスターデータを維持 リッチ化:業種、売上高、従業員数、系列情報など、営業戦略に必要な属性情報を付与 ホワイトスペース可視化:自社データにはない、市場の未接触企業(ホワイトスペース)を可視化し、ターゲティングリストとして提供 これを支えるのが、企業・事業所の識別子であるSOCです。Sansanは10年以上の名寄せ技術の蓄積を経て、この識別子を大規模データに付与する技術を磨いてきました。SOC v2では従来のRDB(Relational Database)型から時系列グラフモデルへと全面移行しました。Entity(Node)とRelationship(Edge)で構成し、全Edgeが有効期間を保持します。最新のスナップショットだけでなく、「2020年4月1日時点ではA社はB社の子会社だった」といった過去時点の関係性まで扱う高い表現力を持つアーキテクチャへと根本から設計し直しました。 既存プロダクト「Sansan Data Hub」が名刺情報を起点としていたのに対し、Sansan Data Intelligenceは企業・事業所のマスターデータを起点としています。名刺がない企業、一度も会ったことのない取引先まで含めて、企業のデータ資産全体を統合管理できます。将来的にはリスクチェック、APIによるあらゆるシステムとの連携強化、グローバル展開へとプラットフォームを拡張していく見込みです。 (参照:Sansan Data Intelligenceリリースに寄せて) 技術的な挑戦と成長環境 Sansan Data Intelligenceを構成する技術スタックは、モダンかつSansanだからこそプロダクション利用できる要素もあり、エンジニアにとって貴重な経験ができる構成になっています。コアデータモデルの設計から、Google Cloudを活用したインフラ、Go言語とそのエコシステムを採用したバックエンド、Next.js(App Router)とTypeScriptを採用したフロントエンド、AIを活用した開発手法まで、各領域に技術的な挑戦や成長環境があります。 コアデータモデル:SOC v2 (参照:ブログリレーVol.03) Sansan Data Intelligenceのデータモデルの中心にあるのは、驚くほどシンプルな構造です。それは、Entity(Node)とRelationship(Edge)、から構成されるグラフモデルです。Sansanではこの新しいデータモデル、そしてそれに基づく識別子をSOC v2(Sansan Organization Code Version 2)と呼んでいます。 企業はNode、企業同士の関係はEdge。「A社はB社の子会社である」「A社はC拠点に所在する」「A社とD社は合併した」——現実世界の複雑な企業情報が、すべてこの2つのプリミティブで表現できます。 さらにこのモデルが強力なのは、全Edgeが有効期間を持つという点です。Edgeは「いつからいつまでその関係が成立していたか」を保持します。つまりこのグラフは現時点のスナップショットだけではなく、時間軸を含んだ企業変遷の完全な記録となります。「2025年3月1日時点でA社はB社の子会社だったか」「この合併はいつ発生したか」——任意の時点の状態を、任意の時点でグラフから取り出すことができます。 名寄せとは集合論的にはEquivalence Classを求めることであり、Graph的に表現することが自然である。 Vol.03 SOCv2: MasterData as a Service (MDaaS) 10年もののSystemを作り替える 集合論に基づいたこのモデルは、Sansanを代表するエンジニアのMakoto Nagaiが設計しました。その設計ドキュメント (Design Doc) を読んだ時に、「NodeとEdge、有効期間のみでこれだけのことが表現できるのか」という驚きと静かな興奮があったことを覚えています。10年以上にわたって企業データの名寄せと向き合い続けたSansanが、最終的にたどり着いたシンプルな構造。複雑な現実を、最小限のプリミティブで過不足なく記述できる。真に良い設計はシンプルで美しい——そう確信させてくれる設計です。 グラフデータベース:Spanner Graph (参照:ブログリレーVol.09) Spanner Graphは Google Cloud Spannerの機能の1つで、グラフ構造のデータに対して専用のクエリ言語(GQL: Graph Query Language)でクエリできます。 「合併・移転・親子関係のつながりを辿る」という、SOC v2に基づくデータ取得はグラフ探索そのものです。だからこそ、Spanner Graphとの相性は必然でした。 通常のSQLではグラフチェーンを辿るクエリは複雑になります。一方でGQLでは、例えば -[:edge]->{0,100} という1行で「エッジを0〜100回辿る」ことを表現できます。「合併や分割というイベントを何段辿っても対象企業を特定する」という処理は、まさにこのGQLが力を発揮する問題です。 ところが、プロダクションの企業データで検証を進めるうちに、Spanner Graphを使い倒しているからこそ気づく特性が見えてきました。Sansanのエンジニアの滑川が突き止めたのは「パフォーマンスの支配的要因はグラフの経路数であって、パスの長さや幅ではない」というものです。実際に検証すると、パスの長さや幅はパフォーマンスへの影響は軽微だったのに対し、経路数は次の表のようにパフォーマンスに大きな影響を与えました。 経路数 応答時間 1,024 355ms 2,048 696ms 32,768 13,733ms さらに、同一経路数であってもグラフの形状によって実行速度が約10倍違うケースがあることも突き止めました。 現在1000万件を超える企業・事業所データ、複雑な資本関係、合併の連鎖。Sansan Data Intelligenceだからこそこの特性の理解が重要です。この検証のような経路数が爆発する企業が出てきた場合には、適度なクエリ分割やSQLとのハイブリッドな運用、ショートカットを作る設計などが求められることになります。 Spanner Graphをこの深さまで実運用で検証しているのはSansanだからこそです。しかも2026年5月末時点では、Spanner GraphはEnterpriseエディションまたはEnterprise Plusエディションでしか使うことができません。Sansanでしか経験できない技術チャレンジはたくさんあり、Spanner Graphはその一つです。 モダンな技術スタック (参照:ブログリレーVol.01, Vol.03, Vol.14) 次の3つの主要システムともにGoogle Cloud上で稼働し、モダンな技術スタックを採用しています。これにより、エンジニアは顧客価値を生むためのシステム設計やプロダクト開発に注力できます。 MasterData System: 組織と組織間の関係性を表す時系列グラフモデル(SOC v2)によるマスターデータ基盤 Application: ユーザー向けのユースケースを提供するフロントエンドとバックエンドマイクロサービス Data Hub: データの識別・統合処理を行うデータパイプライン インフラ GKE(Google Kubernetes Engine)Autopilotベースの社内共通プラットフォーム Orbit を採用。開発・ステージング・本番環境のマニフェスト定義、Secret Manager統合、ArgoCDによるデプロイ管理などを担い、インフラの認知負荷を大幅に削減 Terraformによるインフラ管理(IaC:Infrastructure as Code) Argo CDとGitHub ActionsでCI/CDを整備 Observabilityは OpenTelemetry + Cloud Monitoring / Cloud Trace / Cloud Loggingを採用 認証は社内共通基盤 Auth Oneを採用 データ基盤 メインデータベースにCloud Spannerを採用。前述のように、グラフデータベースもSpanner GraphによりCloud Spannerで完結 MasterData SystemのデータパイプラインはDataflow with Apache Beamで構築し、バッチとストリーミングを透過的に実装可能 バックエンド・API バックエンドにはGo言語を採用。goroutineによる軽量な並行処理がデータパイプラインと高い相性。MasterData SystemとData Hubで言語を統一しており、人材の流動性を高めている バックエンドとフロントエンドのAPI通信は <a href="https://connectrpc.com
はじめに こんにちは。メルペイのAccountingチームでBackend Engineerをしている@hokaoです。 この記事は、Merpay & Mercoin Tech Openness Month 2026 の 5 日目の記事です。 会計データに誤りがあった場合、元のデータを残したまま打ち消すための記録を別途追加するのが会計上の一般的な手法です。本稿では、これをシステムとしてどう扱ったかを設計と実装の観点から紹介します。 背景 Accountingチームでは、会計データを扱うシステムを開発しています。メルカリグループ全体で発生するお金の移動を伴う取引を記録・集計するシステムで、会計イベントの保存と経理向けのレポーティングを責務としています。 会計データは、取引の事実を証明する証拠としての役割を持ちます。そのため、一度記録したデータを後から改変・削除することは原則として許されません。誤りがあったとしても元データを残したまま、打ち消すための記録を別途追加することで修正します。 しかし、私たちのシステムにはこの打ち消すための機能が存在していませんでした。誤って登録された会計データが見つかるたびに、その件数・金額などの情報を手作業で特定し、経理に連携して対応してもらう必要がありました。この運用には、対応コストの大きさや作業ミスのリスクといった構造的な課題がありました。 以降では、会計ドメインの前提を整理した上で、この課題を解決するために導入した打ち消し機能の設計と実装を順に説明します。 会計ドメインの前提 会計では、すべての取引を借方と貸方の 2 つに分けて記録する複式簿記という方式が使われています。借方と貸方それぞれに勘定科目・日付・金額を記載したものが「仕訳」で、これが会計データの最小単位になります。 仕訳に誤りがあった場合、元の仕訳の借方と貸方を入れ替えた「逆仕訳」を計上して打ち消します。 簡単な例として、ある仕入取引を次のように記録していたとします。 借方: 仕入 100 円 貸方: 現金 100 円 この記録が誤りだった場合、逆仕訳は次のようになります。 借方: 現金 100 円 貸方: 仕入 100 円 元の仕訳と逆仕訳を合算すると、勘定科目ごとに借方と貸方が打ち消し合い、金額がゼロになります。元データは残したまま、後から追加した記録によって取引を実質的に打ち消す形になります。 ここで説明したのは、逆仕訳がなぜ打ち消しとして成立するのかという会計上の考え方です。実際の会計レポートでは、必ずしも借方と貸方を足し合わせて相殺しているわけではなく、レポートによっては逆仕訳の金額を符号反転させて打ち消しを表現しています。詳しくは後述します。 逆仕訳の設計と実装 スキーマでの逆仕訳の表現 私たちの会計システムでは、上流のマイクロサービスから受け取った取引はまず会計イベントとして記録され、そこから仕訳が作成される構成になっています。それぞれ AccountingEvents と JournalEntries というテーブルで管理されています。 会計イベント (AccountingEvents) には、上流のマイクロサービスから会計の入力として受け取った取引が記録され、会計処理の種類や取引の中身を保持します。これに対して仕訳 (JournalEntries) は、会計イベントに仕訳ルールを適用して必要な属性が確定した、正式な会計記録です。1 つの会計イベントから、仕訳ルールの適用によって複数の仕訳が作成されることもあります。 逆仕訳の会計イベントには、必ず打ち消し対象となる元の会計イベントが存在します。そのため、AccountingEvents に元の会計イベントへの参照 (OriginalTransactionId) を持たせて関係性を表現します。この参照を持つイベントから作成される仕訳はすべて逆仕訳になります。 また、登録時にはこの参照を辿って、次のようなバリデーションを行います。 元の会計イベントが存在するか 元の会計イベントと整合する勘定科目か 元の会計イベントが既に他の逆仕訳によって打ち消されていないか 会計レポートには JournalEntries から集計するものと、分析用に AccountingEvents から集計するものがあります。どちらの場合でも JOIN なしで逆仕訳の判定ができるよう、JournalEntries には、その仕訳が逆仕訳かどうかを表すフラグ (IsReversal) を持たせています。このフラグは本質的には AccountingEvents の OriginalTransactionId から決まる情報ですが、JournalEntries でも独立に判定できるよう別途持たせています。 同じ会計イベントに対する逆仕訳の会計イベントが複数存在してはいけないため、上述のアプリケーション側のバリデーションに加えて、DB 側にも一意性制約を設けています。具体的には、OriginalTransactionId カラムに NULL_FILTERED オプションを指定した UNIQUE INDEX を張っています。これにより、OriginalTransactionId が NULL のイベント (通常の会計イベント) は重複扱いされず、NULL でない値だけに一意性制約がかかります。 これらをまとめると、スキーマの該当箇所は次のようになります。 CREATE TABLE AccountingEvents ( EventId STRING(100) NOT NULL, AccountingCode STRING(MAX) NOT NULL, OriginalTransactionId STRING(100), -- ... ) PRIMARY KEY (EventId); CREATE NULL_FILTERED INDEX AccountingEventsByOriginalTransactionId ON AccountingEvents(OriginalTransactionId); CREATE TABLE JournalEntries ( Id STRING(100) NOT NULL, EventId STRING(100) NOT NULL, IsReversal BOOL NOT NULL, -- ... ) PRIMARY KEY (Id); 仕訳作成での借方/貸方の入れ替え 逆仕訳の仕訳作成は、元の取引と同じ仕訳ルールを再利用しつつ、ルールから取り出した借方と貸方の属性をコード側で入れ替えて行います。 会計イベントには取引の方向を表す ItemKey という識別子が含まれ、X.to.Y の形式を取ります。仕訳ルールもこの ItemKey をキーに登録されています。逆仕訳イベントでは ItemKey が反転して Y.to.X の形で届くため、ルールを検索する際は ItemKey を一度元の向きに戻します。 ルールを取得した後、逆仕訳イベントの場合に限り、ルールから取り出した借方と貸方の各属性をコード側で入れ替えます。簡略化した擬似コードで示すと次のようになります。 originalRule := getOriginalEventRule(ev) debitAccountingTitleCode := originalRule.DebitAccountingTitleCode creditAccountingTitleCode := originalRule.CreditAccountingTitleCode debitXxx := originalRule.DebitXxx creditXxx := originalRule.CreditXxx // ... if isReversal { debitAccountingTitleCode, creditAccountingTitleCode = creditAccountingTitleCode, debitAccountingTitleCode debitXxx, creditXxx = creditXxx, debitXxx // ... } debit := JournalEntry{ AccountingTitleCode: debitAccountingTitleCode, Xxx: debitXxx, // ... IsReversal: isReversal, } credit := JournalEntry{ AccountingTitleCode: creditAccountingTitleCode, Xxx: creditXxx, // ... IsReversal: isReversal, } 仕訳ルールが借方と貸方の対称な構造を持っているため、ItemKey の反転とそれに続く借方と貸方の入れ替えという 2 つの操作だけで逆仕訳を作成でき、実装はシンプルな修正で済みました。 会計レポートでの逆仕訳の打ち消し 会計レポートに打ち消しを反映するには、集計クエリで逆仕訳または逆仕訳の会計イベントを判定し、正しく金額を計算する必要があります。 JournalEntries から集計するクエリでは IsReversal フラグで判定し、分析用に AccountingEvents から集計するクエリでは OriginalTransactionId IS NOT NULL で判定します。 会計ドメインの前提で述べたとおり、本来の逆仕訳は、勘定科目ごとに借方と貸方を足し合わせて相殺することで打ち消しを実現します。ただし、会計レポートには借方または貸方のどちらか一方だけを集計するものもあり、そうしたレポートでは足し合わせによる相殺は成立しません。そのため、集計クエリで逆仕訳または逆仕訳の会計イベントの金額を符号反転して合算するアプローチをとっています。なお、逆仕訳の金額自体は元の仕訳と同じ正の値で保存しています。 会計レポートは、対象とするテーブルや集計の意味合いがそれぞれ異なるため、共通化が難しく、もともと個別に実装されています。逆仕訳の打ち消しを反映するためには、その個別実装それぞれに修正を入れる必要がありました。例えば WHERE 句の絞り込み条件を、逆仕訳の会計イベントも拾えるように拡張するなどです。すべてのレポートに対してこのような修正を加えていったため、実装と検証には時間がかかりました。 逆仕訳バッチによる運用の自動化 加えて、誤って登録された会計データを訂正するために、逆仕訳を作成するためのバッチを新たに実装しました。このバッチは、対象となる取引の ID リストを受け取り、それぞれについて、上流のマイクロサービスの API を介して打ち消し用の取引を作成します。それが会計イベントとして本システムに登録され、逆仕訳が自動的に作成されます。 個別の取引で失敗が発生した場合はログに記録しつつ、残りの処理は継続します。また、冪等性は上流のマイクロサービス側で保証されているため、同じ ID リストで再実行することも可能です。 これにより、誤ったデータの打ち消しをシステム上で自動的に逆仕訳として反映できるようになり、これまで手作業で行っていた特定・連携の負荷と作業ミスのリスクが大きく軽減されました。 おわりに 本稿では、会計データの訂正を支える逆仕訳機能の設計と実装を紹介しました。 会計のように不変性の制約が強いドメインで開発していると、ドメインの原則が設計判断をそのまま導いてくれる場面が多く、その面白さを今回の機能開発でも改めて感じました。 今回の機能開発は、Accountingチームが抱える運用負荷削減という大きな取り組みの一環でもあります。会計システムは、会社が財務状況を正しく把握するための基盤であり、事業の成長に合わせてスケールできる状態に保ち続ける必要があります。これからも、持続的な開発ができるよう取り組んでいきたいと考えています。 次の記事は imamu さんです。引き続きお楽しみください。
こんにちは。Ai Workforce事業部 開発部の id:ninjinkun です。 先日開催されたTSKaigi 2026において、LayerXはゴールドスポンサーおよび学生支援スポンサーとして協賛しました。 このエントリでは弊社社員の登壇資料の共有、1日目の最後に行われた基調講演の様子、そしてLayerXブースの様子をレポートします。今回はゴールドスポンサーとしての出展に加え、LayerXから総勢4名のエンジニアが登壇しました。まずはそれぞれの登壇資料からご紹介します。 登壇資料 API設計や型の推論にまつわる話まで、幅広いテーマでの登壇となりました。各発表の資料は以下の通りです。もしご興味のある方はぜひご覧ください。 基調講演 TS7: How We Got There 自身の中でもっとも印象に残ったセッションが、1日目最後の基調講演です。MicrosoftでTypeScriptコンパイラのGo移行をリードしているJake Bailey氏による基調講演が行われました。内容としては、セルフホストで書かれていたTSコンパイラが長年抱えていたパフォーマンスの問題の紹介から始まり、どのように移行プロジェクトが始まったのか、なぜGo言語を選んだのかという説明がわかりやすく展開されました。そして最後に劇的に改善されたパフォーマンスのデモで締め括られるというストレートかつ熱い構成で、会場も熱気に包まれていました。 印象的だったのは、VSCodeのコードが頻繁にベンチマークとして使われていたことです。VSCodeのコードに対して型検証を行う際の速度比較や、実際にリポジトリでtscを実行するデモ(氏曰く「呪われたコマンド」)が行われていました。VSCodeを引き合いに出しているのはおそらく社内に対する説明と社外に対するアピールの両方の目的があるのでしょうが、理にかなった戦略だと感じました。この例から伝わる通り、Microsoft自身がTypeScriptを大規模に利用しており、パフォーマンスを改善し続ける強い動機を持っている点は、いちTSユーザーとしても言語の将来に期待が持てる内容でした。 また私個人としては、2日目の昼食の時間に氏が近くに座っていたので、多少勇気を出して話しかけてお礼を言えたのがよかったです。非公開リポジトリでの数ヶ月の試行錯誤を経てリポジトリを公開した話や、なんでC#にしないんだと頻繁に言われる話などを冗談めかして語ってくれました。 すでにGo移行プロジェクトはtypescript-goリポジトリで公開されており、誰でも試すことが可能です。氏のおすすめはまずVSCode拡張のTypeScript (Native Preview)を利用することで、入れるだけでVSCodeでTSファイルを開いてから型解析が完了するまでの速度が大幅に向上するとのことでした。Go移行プロジェクトのリリース版であるTS7はComing Soonとのことなので、楽しみに待ちたいと思います。 LayerXブース ブース撤収完了しました!遊びに来てくださったみなさんありがとうございました、また来年お会いしましょう🙌#TSKaigi pic.twitter.com/J0TXcO25lG— LayerX Tech (@LayerX_tech) 2026年5月23日 今回LayerXとして2日間スポンサーブースを出展しました。ブースでは@syumai、@やた、@Yokan の3名が社内からヒアリングしてきたTypeScriptやフロントエンドに関連した事例をディスプレイで展示し、来場者の方の興味に合わせてその中からトピックを選び説明を行いました。内容は変数をGUIで選択しながら計算式を記述できる複雑なフォームを実装した話、ElectronでPC利用を記録するアプリを開発した話など多岐にわたり、ブースに訪れてくださった方にも好評でした。残念ながらコンテンツはその場限りの公開だったのですが、今後また紹介できればと思います。 私自身は今年4月に入社したばかりのため、前述のコンテンツや会社の事業であるバクラクやAi Workforceについて最初はたどたどしく説明していたのですが、2日間ブースに立ち続けてある程度スムーズに説明できるようになりました。ブースに立ちながら、LayerXの社名はご存知でも実際どんな事業をやっているのかご存知でない方もまだ多いことを実感したので、今後もこういった出展や社外へのアウトプットを継続していく必要性を強く感じました。 アフターイベントのご案内 6/3(水)にUbie様、ビットキー様との共催でアフターイベント「歴史あるプロダクトで、"AIに任せられる領域"をどう広げるか?TSKaigi アフターイベント」を開催します。 TSKaigi本編では話しきれなかったTypeScriptに関するトークに加え、懇親会でより学びを深められる機会です。ぜひ下記connpassリンクよりお申し込みください。 終わりに 私は今回初めてTSKaigiに参加しましたが、普段触っていないバックエンドTSの世界の話も聞くことができ、改めてTypeScriptの表現力を見直す機会になりました。 運営の皆様、スピーカーの皆様、そしてブースに立ち寄ってくださった皆様、ありがとうございました!
こんにちは!ファインディの大石(@bicstone)、甲斐(@karukan013L23)、千田(@_c0909)です。先日、ファインディはベルサール羽田空港で開催された「TSKaigi 2026」に協賛しました。 今回はDevRelメンバーとフロントエンドエンジニア3名で参加し、ブース運営を行いました。本記事ではTSKaigi 2026において印象深かったセッションの紹介や登壇、ブース出展などの活動内容を紹介します。 ブースで実施したユーティリティ型アンケートの集計結果(480票)も後半で公開していますので、ぜひ最後までお読みください。 TSKaigi 2026について 印象深かったセッション 【大石】TS 7: How We Got There 【甲斐】tscからtsgoへ ── DenoのTypeScript基盤はどう変わったか 【千田】Oxlint は ESLint / typescript-eslint を置き換えられるか? 【大石】CfP登壇: TypeScript 6.0での型推論修正を追う ファインディの活動 アンケート結果 さいごに お知らせ TSKaigi 2026について TSKaigiは日本最大級のTypeScriptをテーマとした技術カンファレンスです。東京都大田区のベルサール羽田空港にて、2026年5月22日(金)〜23日(土)に開催されました。 2026.tskaigi.org 印象深かったセッション 興味深いセッションが多くありましたが、その中でも3名それぞれが印象に残ったセッションを紹介します。 【大石】TS 7: How We Got There 2026.tskaigi.org TypeScriptチームのJake Baileyさんによる、TypeScript 7をGo言語へ移植した経緯と成果についての基調講演です。 特に印象的だったのは、Goを採用した理由を体系的に知ることができた点です。 JavaScriptではスレッド間でオブジェクトを共有できず、async/awaitが関数全体に伝播してしまうため、並列化が困難でした。 Goのgoroutineを活かすことで、Parse・Bind・Emitの各フェーズを並列化し、Checkerも複数並べることで高速化を実現しています。 VS Code (Electron) のプロジェクトをtscとtsgoそれぞれで実行した際の所要時間とCPU使用率の差を見せていただいたデモでは、マルチスレッドの活用やCPU使用率の変化が一目で分かり、なぜ大幅な高速化を実現できたのか直感的に理解できました。 発表のなかで繰り返し強く呼びかけられていたのが、コミュニティからのフィードバックでした。「ぜひbetaやnightlyを試してほしい」「VS CodeのNative Preview拡張を入れてほしい」「クラッシュやコンパイル挙動の変化、特にAPIへの意見を送ってほしい」と呼びかけていました。 過去1年でコミュニティから1141件のIssueと1487件のマージ済みPRが寄せられ、テレメトリ経由のクラッシュ情報も含め、利用者からの報告が開発の方向性を支えていることが伝わってきました。 私たちのチームでは、すでにコミット時のフックでtsgoを試験的に採用しています。今後は開発フロー全体への導入を進めながら、検知した問題は積極的にフィードバックを送っていきたいです。 ファインディでも従来からOSSへのIssue起票やPull Requestの作成、メディア企画を通じた寄付などの形で支援を続けてきましたが、TypeScriptのように多くの利用者を抱えるプロジェクトでは、利用者一人ひとりの報告こそが大きな貢献になることを再認識しました。 これまで断片的にしか追えていなかったTypeScript 7について体系的に理解でき、とても学びの多い発表でした。社内にもぜひ共有していきたいと思います。 【甲斐】tscからtsgoへ ── DenoのTypeScript基盤はどう変わったか 2026.tskaigi.org Denoのmaguroさんによる、DenoのTypeScript基盤をtscからtsgoへ移行する取り組みについてのセッションです。 元々DenoはTypeScriptをフォークしたパッケージを使用し、Deno Rust側と必要な情報をやり取りし、Deno固有の概念をtscが解釈できるよう、tscにパッチを当てたものをDeno binaryの中に埋め込んでいました。 tsgoへの移行の最初のアプローチはtsgoをフォークし、Deno固有の概念をtsgoに渡せるようにするアプローチでした。 tsgoはDeno固有の概念をそのまま解決できないため、Deno Rust側で処理できるよう対応しています。LSP対応のコスト、フォークしたパッケージのメンテナンスコストが高く、現在はフォーク版ではなく公式のTypeScriptパッケージを利用するアプローチが試みられています。 TypeScript向けにDenoの依存と型をローカル生成することで、パッチを当てずにDeno固有の概念を解釈できる構成にしています。 特に印象的だったのは、DenoのWeb標準の哲学を少し曲げてでもTypeScriptで扱える形に寄せていった点です。Deno binaryの中→Deno binaryの外→Deno projectの外へTypeScriptパッケージが押し出されており、フォークによる運用コストの増加を避けつつ実行可能なアプローチをとっています。 型チェックを使用したい他のライブラリも同様にフォーク以外の選択肢を模索しており、方向性は同じだがそれぞれ異なるアプローチになっていることが興味深かったです。 普段Denoは使用していませんでしたが、現在の形に辿り着くまでにどのような意思決定があったかを見ていくことで、ここに至るまでの課題や意思決定ごとのトレードオフを学ぶことができ、現在の思想を理解する助けとなりました。 今後もツールチェーンやライブラリの意思決定の背景を学ぶ機会を定期的に設けていきたいなと思います。 【千田】Oxlint は ESLint / typescript-eslint を置き換えられるか? 2026.tskaigi.org 株式会社うるるの藤田翔雅さんによる、OxlintがESLint / typescript-eslintをどこまで置き換えられるのかを整理したセッションです。 特に印象的だったのは、Type-Aware Linting(型情報を使ったLint)の有無でパフォーマンスが大きく変わる点をベンチマークで示していたことです。 型情報を使わない比較ではESLintの8.213秒に対しOxlintは0.304秒と約27倍速く、型情報を使うルールを有効にしてもESLintの16.121秒に対しOxlintは0.807秒と約20倍速いという結果でした。 型情報を使わないLintが構文解析だけで完結するのに対し、型情報を使うLintはプロジェクト全体の型グラフ構築(tsc/tsgo)を必要とするためボトルネックになる、という構造的な解説も理解の助けになりました。 導入判断についても踏み込んでおり、型情報を使わないLintであればOxlintは主要ルールを十分カバーしており移行は現実的である一方、oxlint-tsgolintによるType-Aware Lintingはまだ非安定版であること、カスタムルールを抱えるプロジェクトでは移行コストが上がることなど、現場目線のトレードオフが具体的に語られていました。 結論として、非Type-Aware LintingであればOxlintへの移行を推奨するというメッセージが明快でした。 私たちのチームでもESLint + Prettierを利用しており、CIの実行時間は継続的な課題です。すでにOxc系(Oxlint + Oxfmt)への移行を計画しており、既存のプロダクトはType-Aware Lintingに依存しない構成となっています。 本セッションの「非Type-Aware LintingならOxlint移行を推奨」という結論は私たちの状況に当てはまり、実際の移行計画に重ねて考える良い機会になりました。 【大石】CfP登壇: TypeScript 6.0での型推論修正を追う 当日のCfP枠では、大石が「プロパティの順序で型推論が壊れる!? TypeScript 6.0の修正からContext-Sensitivityの仕組みを追う」というタイトルで登壇しました。 プロパティの記述順序を入れ替えるだけで型推論が壊れる挙動を入口に、TypeScript 6.0でマージされたPRの中身まで踏み込んだ内容です。詳細は別記事にまとめていますので、あわせてご覧ください。 tech.findy.co.jp speakerdeck.com ファインディの活動 ファインディはGoldスポンサーとして協賛し、ブース出展という形で支援しました。 ブースでは「よく使うユーティリティ型」をテーマにしたアンケート企画を実施しました。普段の開発でよく使うユーティリティ型を選んでいただく内容で、2日間かけて多くの方に投票いただきました。 TSKaigi2026始まりました!入口すぐです!お待ちしております🌟#TSkaigi2026 #tskaigi pic.twitter.com/ecf1Zzok0V— いわさき@Findy DevRel (@iwasakitchen) 2026年5月22日 x.com アンケートの最終結果はこちらになります。たくさんの投票ありがとうございました。 TSKaigi 2026改めて2日間ご参加いただき、ありがとうございました!よく使うユーティリティの「型」は?の最終結果です!😊🎊#TSkaigi #TSkaigi2026 pic.twitter.com/0Nt4xun2bQ— いわさき@Findy DevRel (@iwasakitchen) 2026年5月23日 x.com アンケート結果 総数:480票 順位 ユーティリティ型 割合 票数 1位 Record<Keys, Type> 33.3% 160票 <td st
はじめに こんにちは、データ基盤ブロックの平本(@cisetn)です。 本記事では、ZOZOTOWNのリアルタイムデータ連携基盤の中核であるETL層を作り直した事例を紹介します。対象はオンプレミスのSQL ServerからBigQueryへリアルタイムにデータを連携する基盤です。そのETL層をGoで実装したプラグイン(実行基盤はFluent Bit)で再設計しました。 ZOZOのリアルタイム連携基盤は2020年に一度紹介記事を公開していますが、それ以降、段階的にアーキテクチャを見直してきました。本記事はその中でもETL層の再設計にフォーカスします。 想定読者は、リアルタイム連携基盤やストリーミング処理基盤の設計・運用に関わる方です。 本記事で扱うこと、扱わないことは次のとおりです。 扱う:ZOZOのリアルタイム連携の全体像、今回リプレイスした基盤の背景・設計・実装 扱わない:BigQuery側のテーブル設計、SQL Server側のChange Tracking設定、利用側(BI・分析クエリ等) 目次 はじめに 目次 ZOZOのリアルタイムデータ連携の全体像 これまでの変遷 リプレイスに至った背景 顕在化してきた課題 新基盤アーキテクチャ 設計の軸 技術選定:Fluent Bit + Goプラグイン 全体構成 大量のデータをリアルタイムで捌くために考えたこと 新基盤の構成 INPUT内部:取得とエンコードを分けた OUTPUT内部:送信とACK確認を分けた 結果 今後の展望:Change Data Captureへの移行 まとめ ZOZOのリアルタイムデータ連携の全体像 本題の前に、ZOZOにおけるリアルタイム連携の全体像を軽く俯瞰しておきます。本記事のテーマがあくまで「その中のひとつ」であることを共有するためです。 ZOZOではデータソースが多岐にわたります。オンプレミスのものもあれば、クラウド上のものもあり、MySQL、SQL Server、DynamoDBなどさまざまです。当然、差分を検知する手段もソースに応じて変わりますし、連携の実現方式も1つではありません。 マネージド / SaaSで済むケース:例えばMySQL → BigQueryであればDatastreamを利用する 専用のパイプラインを組む必要があるケース:例えばDynamoDB → BigQueryのように、対応するマネージドサービスがない場合は、別途データ連携のパイプラインを構築する必要がある 結果として、ZOZOのリアルタイム連携基盤は複数系統に分かれて共存しています。本記事で扱うのは、そのうちオンプレ SQL Server → BigQueryの系統です。本番環境(prd)で約400のテーブルを連携対象としており、新規の連携依頼も日々発生するため、データ基盤の運用において比重の大きな系統となっています。SQL ServerのChange Tracking機能で変更を検知し、プラグインで取得したレコードをPub/Sub経由でBigQueryに流しています。 これまでの変遷 実は、本記事で扱う系統は今回が初めてのリプレイスではありません。以下の変遷を経ています。 時期 アーキテクチャ 主目的 2020 Qlik Replicate→ fluentd + Dataflow→ BigQuery 安定性向上 + コスト削減 2024 fluentd + BigQuery Subscription(Dataflow を廃止) コスト削減 2025 プラグインによる ETL 層の再設計+ BigQuery Subscription 効率改善(メモリ・スループット・コスト) 2024年には、ストリーム処理層のDataflowを廃止し、Pub/SubのBigQuery Subscriptionに置き換えるリプレイスが行われました。このフェーズの主目的はコスト削減です。 そして今回、ETL層をプラグインで再設計したのが本記事のテーマです。詳細な背景と目標は次章で述べますが、結果として、コスト削減・メモリ効率の改善・スループット向上・運用課題の解消といった効果につながりました(数値は末尾)。 リプレイスに至った背景 誤解のないよう先に述べておくと、旧基盤の設計が「悪かった」わけではありません。2020年当時、ZOZOのデータ基盤はまさに拡大していくフェーズにあり、リアルタイム連携の需要も増え始めたばかりでした。そうした状況では、プラグインが豊富なfluentdとDataflowのように既存のツールを組み合わせて素早く構築できる構成は合理的な選択だったかと思います。実際、信頼性(データ欠損が起きないこと)はチェックポイント機構などによって担保できており、長く運用されてきました。チェックポイント機構は、処理済みのChange TrackingバージョンをBigQueryに保持する仕組みです。Pod再起動時はそこから再開できます。 顕在化してきた課題 一方で、運用を続け、データ量や利用要件が増えていく中で、効率の側面でいくつかの課題が徐々に顕在化してきました。 メモリ効率:結果セットを一括でメモリに載せる実装のため、メモリ使用量がデータ量に比例して増加する構造でした。大量更新時のOOMを避けるためには「ピーク時のデータ量」を見越した大きなメモリを常時確保しておく必要があり、データ量が増えるにつれてリソース見積もりの難しさが目立つようになってきました。 コスト:上記のメモリ確保がそのままコストに直結します。メモリがトランザクション単位のデータ量に比例する構造であるかぎり、「ピーク時のデータ量」の見積もりを下回るとOOM直行となります。そのため運用上の工夫(時間帯別のスケーリング等)では本質的な改善が難しく、リソースの常時確保によるコスト増を抱え続けるしかありませんでした。 性能:逐次処理ベースの実装のため、1トランザクションあたりの規模が大きいテーブルでは、リアルタイム性を保ちにくい場面もありました。 運用:依存していたコンテナイメージがEOLを迎えており、継続利用にリスクがありました。加えて、内部状態の可視性が低く、障害発生時の原因特定にも時間がかかる状況でした。 一言でまとめると、各所でガタが出始めており、信頼性を維持したまま効率(メモリ・スループット・コスト)の側面を改善するため、リプレイスを検討するタイミングに来ていた、ということです。 新基盤アーキテクチャ 設計の軸 新基盤の設計指針はシンプルで、キャパシティプランニングの軸を「ピーク時のデータ量」から「単位時間あたりの処理量」に変えることに尽きます。信頼性(データ欠損が起きないこと)は旧基盤からチェックポイント機構によって担保されており、新基盤でもそのまま引き継いでいます。そのため本記事のテーマは信頼性を維持したまま、効率(メモリ・スループット・コスト)をどう改善したかです。 技術選定:Fluent Bit + Goプラグイン 今回のリプレイスは、前フェーズ(2024年のDataflow撤廃 + BigQuery Subscriptionへの切り替え)の延長線上にあります。前フェーズでDataflow関連の費用がまるごと不要になり大きなコスト削減は既に達成済みで、下流(Pub/Sub HubとBigQuery Subscription)も整理されている状態でした。一方でETL層はfluentdベースのまま残っており、メモリ効率とスループットの面で課題が顕在化していたため、今回はその続きとしてETL 層の中身を作り直すことにしました。下流はそのまま踏襲し、ソース側(Change Tracking設定)にも手を加えません。 このスコープと、既存のPub/Sub Hub構成・BigQueryテーブル設計を維持する制約のもとで、マネージドCDCサービスやOSSのCDCミドルウェアの活用も検討しました。ただし我々のケースでは、既存テーブル設計とPub/Sub Hubへの直接出力をそのまま組み合わせ続けられる選択肢を見つけられず、プラグインとして実装する形に決めました。 採用したのはFluent Bit + Goプラグインです。決め手は次のとおりでした。 既存基盤がfluentdベースで運用されていたため、Fluent Bitへの移行が素直:プラグインモデル・設定構造・デプロイ手順といった運用ノウハウがそのまま活きる INPUT(Change Tracking取得)とOUTPUT(Pub/Sub送信)の挙動を自分たちで細かく調整できる。後述の非同期ACK並列確認のような最適化も、プラグインとして自前で書いているからこそ仕込める Fluent BitのBuffer・バックプレッシャー機構をそのまま活用できる Goプラグイン公式サポートにより、後述する並列処理をgoroutineとchannelで素直に書ける 全体構成 以下の図は主要コンポーネントのみを示した簡略図です。 ETL層(Fluent Bit + Goプラグイン)はGKE上で動作します。プラグインはデータ取得(INPUT)とPub/Subへの送信(OUTPUT)の2つで構成されており、それぞれの実装の詳細は次章で扱います。 大量のデータをリアルタイムで捌くために考えたこと 新基盤の設計で常に意識していたのは、「大量のデータをいかにリアルタイムで捌くか」という問いでした。データ量が増えてもパイプラインが詰まらず、メモリ消費がデータ量に比例しない構造をどう実装するかを検討しました。前章で述べた「単位時間あたりの処理量を軸にする」方針を、Fluent Bitのパイプライン上に乗せて具体化していった話を、本章で紹介します。 なお、Fluent Bitのパイプライン構造の全体像については、公式ドキュメントもあわせてご覧ください。 新基盤の構成 Fluent Bitのパイプライン構造はINPUT → Filter → Buffer → Router → OUTPUTという形です。新基盤ではこのうちINPUTとOUTPUTをGoプラグインで実装しました。チャンク単位の処理やバックプレッシャーといったBuffer周りの機構はFluent Bit Engineが標準で備えています。そのためプラグイン側はINPUTとOUTPUTの"箱の中"の設計に集中できました。 設計の出発点として、データ取得から送信までの各処理を「どこがボトルネックになるか」で整理し、並列化方針を決めました。 処理 特性 並列化方針 CT取得(クエリ → カーソル) I/O bound(DB側) 単一スレッド(DBがボトルネック) エンコード CPU bound Worker数で並列化 Pub/Sub Publish I/O bound(NW) 非同期APIで並列化 ACK確認 I/O bound(NW待ち) 別Workerプールで並列化 CPU boundとI/O boundを別レーンに分け、それぞれを独立した並列度で動かす設計です。以下、INPUT内部・OUTPUT内部の順で紹介します。 INPUT内部:取得とエンコードを分けた INPUT内部の設計では、メモリとCPUを独立した軸として扱えるようにしました。 メモリの設計:結果セット全体を展開せず、カーソルで小分けに読み進める方式を採用。1回のクエリで読むレコード数 RecordsPerChunk をプラグインの設定で指定でき、本番では10,000件/チャンク CPUの設計:取得処理とエンコード処理を別レーンに分け、エンコードは複数のWorkerで並列実行 取得とエンコードの間に中間キュー(jobs queue)を挟むことで、取得側はエンコードの完了を待たずに次のチャンクを先行投入できます。キュー容量がゼロだと直列に戻ってしまうため、本実装ではjobs queueの容量をWorker数の5倍に設定しています。 この構造のもとで、同時にレコード形式でメモリに乗るチャンク数はNumWorkers × 6個で頭打ちになります。内訳は「jobs queue上の最大NumWorkers × 5個 + 各Workerが処理中の1個」です。 同時メモリ上のレコード数 = RecordsPerChunk × (jobs queue + 処理中 Worker) = RecordsPerChunk × (NumWorkers × 5 + NumWorkers) = RecordsPerChunk × NumWorkers × 6 = 10,000 × NumWorkers × 6 例えばNumWorkers = 2なら、データ量に関わらず常に約12万レコード分のメモリしか確保しなくて済みます。100万件規模のトランザクションが流れてきても、結果セット全体を一括ロードしてしまう旧基盤と違ってOOMにはなりません。 なお、Fluent Bit上でカーソル方式を実装するときには工夫が必要でした。Fluent BitはINPUTに対して定期的に「データをちょうだい」と呼び出してくる構造になっており、素朴に書くと毎回新規にクエリを発行してしまいます。それでは結果セットが毎回頭から読み直されてしまうため、カーソル状態をプラグイン側に持ち越し、呼び出しごとに「続きから」読み進めるようにしました。 OUTPUT内部:送信とACK確認を分けた OUTPUT内部では、送信処理とACK確認処理を別レーンに分離しました。Pub/SubのPublishは同期的に書くと「送信 → ACK待ち → 次へ」と直列化してしまい、ACK待ちのネットワークI/Oが支配的になります。これだとスループットがACKレイテンシに律速されてしまうため、両者を分離して並列化する方針を取りました。 送信側:非同期APIを呼んで即座にFuture相当の結果を受け取り、次へ進む。送信そのものは止まらない 確認側:受け取ったFutureのACK確認専用のWorkerプールを設け、複数並列で確認する 各メッセージが独立したACKタイムアウトを持つようになり、1件の遅延が後続全
バクラク事業部ソフトウェアエンジニアの矢田(@0e2b3c)です。 LayerXはゴールドスポンサー並びに学生支援スポンサーとしてTSKaigi 2026に協賛させていただきます。 加えて、弊社ソフトウェアエンジニアである泉(@izumin5210)、福岡(@syumai)、山本(@minako-ph)、田中(@ypresto)が登壇者として参加を予定しています。 スポンサーブースのご紹介 昨年度までは社内ADRを公開するというブース内容でしたが、今年度は新しい取り組みとして社内の面白かった・苦労した実装を紹介するブースを出展予定です。 弊社の事業の1つであるバクラク事業部の各チームにフロントエンドやTypeScriptに関するインタビューを行い、それをスライドとしてまとめたものをブースにて展示予定です。 LayerXの実際の開発を体感できるこれまでにない取り組みですので、是非当日は足をお運びください。 登壇のご紹介 開発体験を左右するライブラリの API 設計 ― GraphQL スキーマ構築ライブラリから考える@izumin5210 TypeScript で GraphQL を実装するためのライブラリには、さまざまな API 設計が存在します。Schema-first と Code-first といったスキーマ記述アプローチの違い、スキーマと TypeScript 型をどのように接続するか、resolver をどのような形で記述させるかといった設計上の選択は、単なる記法の違いではなく、開発者がどのようにスキーマと向き合い、どのように設計を組み立てるかという開発体験そのものを規定します。 本発表では、既存の GraphQL 実装ライブラリを比較しながら、スキーマ構築と resolver 実装の API 設計を整理します。特に、スキーマと型の結びつけ方や、開発者に与えるメンタルモデルの違いに注目し、それぞれのトレードオフを明らかにします。そのうえで、AI Coding が前提となりつつある現在、ライブラリに求められる API 形状や型情報の役割がどのように変化しうるのかを検討します。整理を踏まえ、新たに開発した GraphQL 実装ライブラリ「gqlkit」の設計を一例として紹介し、今後の GraphQL 実装基盤の方向性を提案します。 日時:Day1 / 11:00 ~ 11:40 場所:RightTouchトラック 2026.tskaigi.org Oxlintはいかにしてtsgolintのlint ruleを呼び出しているのか@syumai Oxlintは、VoidZeroによって開発されている、Rust製の高速なJavaScript / TypeScript Linterです。 既に安定版のv1がリリース済みですが、実は、そこにはTypeScriptの型情報に依存するルールの実装が含まれていません。 その役割を担うtype-aware lintingの実装は、typescript-goベースの別リポジトリである、oxc-project/tsgolintで行われています。 tsgolintを通じてlintを行うことで、Oxlintは no-floating-promises などのlint ruleをサポートすることができます。 つまり、Rust実装であるOxlintから、Go実装のtsgolintを呼び出していることになるのですが、一体これはどのように実装されているかご存知でしょうか? 本発表では、発表者がOxlintおよびtsgolintの実装を読んで知った、プロセス間通信に基づくOxlintとtsgolintの通信方法についての概要と、tsgolint側にルールを追加する度に必要となるOxlint側での実装について解説を行います。 また、発表者が、tsgolintにカスタムルールを実装しようとした過程で得た知見についてもお話しします。 日時:Day1 / 17:20 ~ 17:50 場所:Leveragesトラック 2026.tskaigi.org 柔軟なPDFレイアウトエディタを支える型システム設計 — Discriminated UnionとConditional Typeの実践@minako-ph バクラク請求書発行では、請求書・見積書・納品書などのPDFレイアウトをGUIで自由にデザインできるビジュアルエディタを提供しています。テキスト・画像・動的テーブルなど6種類のノードを自由に配置し、実際の書類データをバインドしてPDFを生成する仕組みです。 しかし自由度が高いほど、コードの複雑さも増します。テキストノードにはフォント設定があり、テーブルノードには列定義があり、画像ノードにはフィット方法がある——同じ「ノード」でも中身はまったく別物です。さらにノードの値は固定テキストかもしれないし、書類データから動的に取得するかもしれない。こうした組み合わせの中で「テキストノードなのにテーブルの列定義を参照してしまう」ような取り違えをどう防ぐか。人のレビューに頼るのではなく、型に任せられないか—— 本トークはその問いから始まります。 Protocol Buffersのoneof定義からZod経由でDiscriminated Unionを自動生成する仕組み、Conditional Typeでノード型名から値型を安全に取り出すパターン、DeepPartialでReducerの部分更新を型安全にする設計、そして同一の型定義でエディタのプレビューとPDF本番生成の両方を駆動するアーキテクチャを、具体的なコードとともに紹介します。 日時:Day2 / 15:50 ~ 16:20 場所:RightTouchトラック 2026.tskaigi.org TypeScriptはどのようにどこまで推論できるのか ─ とにかく as は禁止で@ypresto(スポンサーセッション) TypeScript の型チェックは、型推論、Control Flow 解析、そして各種ガードレールという強力な3つの仕組みに支えられています。そしてInferred Type Predicates (filter()の型推論) のように、以前は手でケアしていた場面でも自動で正しい型が付くようになるなど、これらの機構は年々強化されてきました。 一方これらの機構でカバーできるにも関わらず as (as constを除く) を使用してしまうことは、AI 時代に必要な安全性を自ら緩めることになり、それは "敗北" です。 本セッションは、Contextual Type をはじめとして、TypeScript の推論機構やガードレールの仕組みと制限を紹介し、その安全性を活かすことにより、「as全部禁止」に対して「主張が強い」と返されないように試みるものです。 日時:Day2 / 12:30 ~ 13:30(スポンサーセッション内) 場所:Leveragesトラック 2026.tskaigi.org TSKaigi 2026について 2024年に産声をあげ、2025年も大盛況のうちに幕を閉じた TSKaigi を今年も開催します! 私たちは、誰かの発表を聞くだけでなく、他の誰かに向けて発表することもまた学びの一つだと考えています。 参加者、登壇者、スタッフ、スポンサーをはじめ、TSKaigi に関わるすべての人たちが互いに学び合い、新たな繋がりを生み出し、型にとらわれないエンジニアとして生き生きと活躍できる世界を目指します。 TypeScript に関するあらゆるテーマを扱う国内最大級のカンファレンスとして、まさに「型破り」なイベントを目指し成長を続ける TSKaigi にご期待ください。 詳細やタイムテーブルなどは TSKaigi 2026公式サイト をご覧ください。 協賛の背景 わたしたちは技術コミュニティから日々たくさんの技術的な知見を頂いたり、実際にOSSとして活用させて頂いています。 LayerXの掲げる行動指針である「徳」の観点からも技術コミュニティから一方的に恩恵を受けるだけでなく、技術コミュニティへの貢献を継続して行っています。 アフターイベントのご案内 TSKaigi 2026終了後、6/3(水)にUbie様、ビットキー様との共催でアフターイベント「歴史あるプロダクトで、"AIに任せられる領域"をどう広げるか?TSKaigi アフターイベント」を開催します。 TSKaigi本編では話せなかったTypeScriptトークや懇親会でより学びを深められる機会となりますので、是非下記connpassリンクよりお申し込みください。 bitkey.connpass.com 最後に 過去最多の登壇人数、新たなスポンサーブースで、これまでで最大規模となるTSKaigiへ参加します。 TypeScriptやフロントエンド開発に関してディスカッション、交流できればと思いますので、是非お気軽にスポンサーブースやセッションへ足をお運びください!
こんにちは、Control Plane部認証認可グループの平岩です。私たち認証認可グループではバックエンドAPIにGoを多く採用しています。共通基盤である特性上高RPSに耐えられる必要があり、また安定して低いレイテンシでリクエストを処理することが求められます。本記事ではGoの標準パッケージである sql.DB の内部実装を、ソースコード(Go 1.26時点)を読みながら解説します。 はじめに sql.DB とは何か 内部実装の詳解 DB 構造体の主要フィールド クエリ実行の全体像 BadConn リトライの仕組み コネクションの取得: conn メソッド フェーズ1: idleコネクションがあれば再利用する フェーズ2: 最大接続数に達している場合の待機 フェーズ3: 新規接続の作成 コネクションの返却: putConn メソッド 非同期なコネクション作成: connectionOpener トランザクションとコネクション コネクションの定期削除: connectionCleaner DBStats: プールの状態を観測する コネクションプールの設定 4つの設定パラメータ デフォルト値の問題 各パラメータの解説 SetMaxOpenConns SetMaxIdleConns SetConnMaxLifetime SetConnMaxIdleTime まとめ はじめに GoでRDBを使うとき、何かしらのO/R mapperやライブラリを使うことが多いかと思います。ですが、内部的には標準パッケージの sql.DB が使われています。この sql.DB はコネクションプールの役割を持ち、4つの設定パラメータが存在します。これらの値をデフォルトのまま使うと本番環境でエラーやパフォーマンス問題を引き起こすことがあります。また、内部でリトライが行われるケースがあるなど隠蔽されている挙動も存在します。 本記事では前半で sql.DB の内部実装をソースコードを追いながら解説し、後半でそれを踏まえた設定の考え方を述べます。設定についてだけ知りたい方はコネクションプールの設定まで読み飛ばしてください。 sql.DB とは何か sql.DBの実態はコネクションプールであり、複数のgoroutineから安全に使えます(goroutine safe)。 sql.Openは引数の検証のみを行い、この時点ではデータベースへのコネクションを作成しません。実際にコネクションが作れることを確認するには (*sql.DB).PingContext を使います(詳細はOpening a database handle - The Go Programming Languageを参照してください)。 内部実装の詳解 DB 構造体の主要フィールド ここからは sql.DB の実装を見ていきます。以降のコードを読むにあたって、DB 構造体 の以下の4つのフィールドを把握しておけば十分です。 freeConn []*driverConn: idleコネクションのスライス。プールに戻った時刻 returnedAt の古い順になる connRequests connRequestSet: コネクションを待っているgoroutineの集合 connRequestSet はSetと言っているが実体はindexでの参照を付けたスライス numOpen int: 使用中とidleの両方を含む、現在openなコネクション数 cleanerCh chan struct{}: プールにあるコネクションを閉じるgoroutineへの通知用のchannel すべてのフィールドへのアクセスは mu sync.Mutex で保護されています。 クエリ実行の全体像 まず、 QueryContext を例として全体像を示します。他の QueryRowContext ExecContext PingContext も基本的に同じ流れですが、 QueryContext のみが Rows.Close() を明示的に呼んでコネクションを返却する責任がuser側にあります。 flowchart TD A["(*sql.DB).QueryContext"] subgraph retryScope ["retryの範囲(ErrBadConnの場合、最大3回リトライされる)"] B["(*sql.DB).retry"] Q["(*sql.DB).query"] C["(*sql.DB).conn"] E["(*sql.DB).queryDC"] end A --> B B --> Q Q --> C C -- エラー --> ERR[error を返す] C -- 成功 --> E E -- ErrBadConn --> B E -- エラー --> ERR E -- 成功 --> F[Rows を返す<br>コネクション所有権が Rows に移動] subgraph userCode ["user"] K["(*sql.Rows).Next() で最後まで読むか、(*sql.Rows).Close()を呼ぶ"] end F --> K K --> G["(*sql.DB).putConn"] BadConn リトライの仕組み sql.DB には無効な接続に当たった場合に自動でリトライする仕組みがあります。 このリトライのトリガーとなるのが driver.ErrBadConn です。godocには次のように書かれています。 ErrBadConn should be returned by a driver to signal to the sql package that a driver.Conn is in a bad state (such as the server having earlier closed the connection) and the sql package should retry on a new connection. To prevent duplicate operations, ErrBadConn should NOT be returned if there's a possibility that the database server might have performed the operation. つまり、 driver.ErrBadConn はコネクションがDBサーバ側から切断された場合などに返り、sql package側でリトライすべきエラーであるということです。 実際にリトライを制御する DB.retry を見ていきます。 database/sql/sql.go L1572-1584: const maxBadConnRetries = 2 func (db *DB) retry(fn func(strategy connReuseStrategy) error) error { for i := int64(0); i < maxBadConnRetries; i++ { err := fn(cachedOrNewConn) // 👈 (1) // retry if err is driver.ErrBadConn if err == nil || !errors.Is(err, driver.ErrBadConn) { return err } } return fn(alwaysNewConn) // 👈 (2) } (1) 最大2回、cachedOrNewConn のstrategyで試行します。このstrategyではまずidleコネクションを再利用しようとします。 (2) 2回とも driver.ErrBadConn だった場合、3回目は alwaysNewConn strategyで新規のコネクション作成を強制します。 コネクションの取得: conn メソッド retry から呼ばれる conn メソッドが、プール内のidleコネクションか新規の接続を返します。このメソッドはかなり長いため、全体を3つのフェーズに分けて読んでいきます。 flowchart TD A[connメソッド呼び出し] --> B{freeConnに<br>idleコネクションがある} B -- Yes --> C{コネクションが期限切れ} C -- No --> D[コネクションを再利用] C -- Yes --> E[driver.ErrBadConnを返す → リトライ] B -- No --> F{コネクション数が最大(maxOpen)に<br>達している} F -- Yes --> G[connRequestsで待機] F -- No --> H[新規接続を作成] フェーズ1: idleコネクションがあれば再利用する database/sql/sql.go L1331-1354: // Prefer a free connection, if possible. last := len(db.freeConn) - 1 if strategy == cachedOrNewConn && last >= 0 { // Reuse the lowest idle time connection so we can close // connections which remain idle as soon as possible. conn := db.freeConn[last] // 👈 (1) db.freeConn = db.freeConn[:last] conn.inUse = true if conn.expired(lifetime) { // 👈 (2) db.maxLifetimeClosed++ db.mu.Unlock() conn.Close() return nil, driver.ErrBadConn } db.mu.Unlock() // Reset the session if required. if err := conn.resetSession(ctx); errors.Is(err, driver.ErrBadConn) { conn.Close() return nil, err } return conn, nil } (1) freeConn の末尾を取得しています(LIFO)。コメントにある通り、同時に必要とする数よりも多いコネクションがあった場合、余分なコネクションはidle状態のままcloseされていきます。 (2) 取得した接続が maxLifetime を超えていた場合は driver.ErrBadConn を返し、リトライの対象になります。 フェーズ2: 最大接続数に達している場合の待機 database/sql/sql.go L1358-1426: if db.maxOpen > 0 && db.numOpen >= db.maxOpen { req := make(chan connR
はじめに こんにちは、タイミーでエンジニアをしている徳富(@yannkazu1)です。 クラウドネイティブ会議2026で発表された「ペアーズ本番環境でのcgroup-aware化との死闘録」がめちゃくちゃ面白かったので、自分の手でも体感したくなりました。 GoのGOMAXPROCSがコンテナのCPU制限を無視するって、実際に見るとどうなるのか? 過剰並列のスループット低下って、数字で見るとどのくらいインパクトがあるのか? スロットリングとスレッド数の関係を自分の目でたしかめたい! 自分で動かして数字を見ないと腑に落ちないタイプなので、ローカルのMac環境で全部再現してみました。 発表の要約 ペアーズのバックエンド pairs-main はGo製でAmazon EKS上で稼働。48コアのNodeで limits.cpu: 5000m(5コア)のPodが動いていたが、GoのGOMAXPROCSがデフォルトで48(=Node全体のコア数)になっていた。これにより以下の問題が発生: 過剰並列: 5コアしか使えないのに48スレッドが走る → Goスケジューラのオーバーヘッド増大 CPUスロットリング: cgroupのクォータ(CPU時間の上限)をスレッドが共食い → 全スレッドが同時に停止 監視の死角: CPU使用率は正常に見えるが、実際はスロットリングで断続的に停止 同じ問題がHAProxy(nbthread=48、CPU制限1コア)でも発生していた。 これらをcgroup-awareな設定(GOMAXPROCS=5, nbthread=1)に修正したところ、大幅に改善した、という話でした。 用語の整理 ここから先で出てくる「コア」「GOMAXPROCS」「クォータ」「スロットリング」あたりがピンと来なくても大丈夫です。記事全体で繰り返し登場するので、最初にざっくり整理しておきます(すでに馴染みがある方はスキップでOK)。 CPUコア・プロセス・スレッド 用語 ざっくりした意味 CPUコア 計算を実行する物理的な実体。1コア = 同時に1つの処理を進められる プロセス 動いているプログラム1つ分の単位 スレッド プロセス内で実際にCPUに割り当てられる作業の単位。1プロセスは複数スレッドを持てる ざっくり言うと、コアの数 = 同時に進められるスレッドの数の上限です。8コアのCPUなら、ある一瞬に進行できるのは最大8スレッドまで。それ以上のスレッドを立ち上げた場合は、OSが順番にコアを割り当て直しながら回します(= コンテキストスイッチ)。 コンテナと cgroup 用語 ざっくりした意味 コンテナ 同じサーバー上で複数のアプリを互いに干渉しないように動かす仕組み(Docker や Kubernetes の中身)。実体はホストのカーネルをそのまま使う 「namespaces で見える範囲を、cgroup で使える量を制限したプロセス(群)」 にすぎず、VM のように専用カーネルを持つわけではない cgroup(Control Groups) Linuxカーネルの機能で「このプロセス群はCPUをここまで・メモリはここまで」と上限を設定する仕組み CPU制限 「このコンテナはCPU 1コア分まで」のような上限設定。実体は cgroup の cpu.max ファイル コンテナの「CPU 0.5コアまで」という設定は、Linuxカーネルが cgroup を通じて「100msのうち50msまでしかCPUを使わせない」という形で強制します。この 100msの枠を「ピリオド」、その中で使ってよい時間量を「クォータ」 と呼びます(cpu.max: 50000 100000 なら「100msのうち50ms使える = 0.5コア相当」)。 CFS スケジューラ Linux のデフォルトの CPU スケジューラを CFS(Completely Fair Scheduler) と呼びます。先ほどの「ピリオド」「クォータ」は、CFS が持つ 帯域制御(Bandwidth Controller) という機能の用語で、cgroup の cpu.max の値を実際にスレッドへ適用する(=クォータを使い切ったら停止させる)のはこの CFS の仕事です。 つまり「cgroup が制限値を持ち、CFS がそれを実施する」という分担関係。後の実験で出てくる nr_periods(CFS が時間を区切る単位の総数)や nr_throttled(CFS が停止させたピリオドの数)も、この CFS 帯域制御の統計を見ています。 Goroutine と GOMAXPROCS(Go特有の話) 用語 ざっくりした意味 goroutine Goの軽量スレッド。OSスレッドより遥かに軽く、1プロセスで数万〜数百万個立ち上げられる OSスレッド OSが実際にCPUにスケジュールするスレッド。コアを取り合うのはこちら GOMAXPROCS Goランタイムが同時に走らせるOSスレッドの数の上限。デフォルトはホストのCPUコア数 goroutine を何万個立ち上げても、Goランタイムは GOMAXPROCS 個の OSスレッドの上にそれらを多重化して実行します。つまり同時に CPU を握っているのは最大でも GOMAXPROCS 個。この割り当てを管理するのが Goスケジューラ です。 ポイントは、コンテナのCPU制限が下がってもデフォルトの GOMAXPROCS はホストのCPU数のままということ。これがそもそも今回のテーマで、後の実験でその挙動を実際に確かめます。 過剰並列 CPU 制限よりも多くのスレッド(や goroutine、ワーカー)を同時に走らせている状態を指します。たとえば 5 コア相当の CPU 制限に対して GOMAXPROCS=48 なら、約 9.6 倍の過剰並列。実際に走れるのは制限分のスレッドだけなので、残りはスケジューラの上で順番待ちをしつつ、共有クォータを早食いし合うことになります。 Go の GOMAXPROCS に限った話ではなく、HAProxy の nbthread、Nginx の worker_processes、Puma の workers など、「並列数のデフォルトがホスト CPU 数に依存する」設定はすべて同じ構造で過剰並列を起こします。 CPUスロットリング cgroupでCPU 0.5コア分に制限されたコンテナが、たくさんのスレッドでCPUを一気に使おうとすると、Linuxカーネルが 「クォータを使い切ったので、次のピリオドまで全スレッド一時停止」 と強制的にブロックします。これが CPUスロットリングです。 スロットリングが頻発すると、レスポンスが断続的に止まったり、スループットが落ちたりします。その結果、「なぜか遅延がスパイクする」原因になっているケースが多いです。発生状況は /sys/fs/cgroup/cpu.stat に出力されており、本記事では以下の3指標を追います: nr_periods: スケジューラの計測単位(ピリオド = 100ms)の総数 nr_throttled: そのうちスロットリングが起きたピリオドの数(回数) throttled_usec: スロットリングで実際にCPUが止められた累積時間(マイクロ秒) 「回数」だけでなく「累積停止時間」も見るのが重要だ、というのが発表の山場の一つで、後の実験3でその違いがハッキリ出ます。 Thundering Herd スロットリングで停止していた全スレッドが、次のピリオドのリセットで一斉に走り出し、また一瞬でクォータを食い潰して同時に止まる、というサイクルが繰り返される状態を 「Thundering Herd(雷鳴の群れ)」 と呼びます。元はソケット accept など I/O 文脈の用語ですが、cgroup の帯域制御下でも同じ構造の問題が起きます。スレッド数が多いほど被害が大きくなるのは、ここに端を発しています。実験4でその挙動を観察します。 cgroup-aware プログラムやライブラリが cgroup の制限(cpu.max など)を自分で読み取り、その値に合わせて並列度を調整する 設計のことを 「cgroup-aware」 と呼びます。Go 1.25 以降のランタイムや uber-go/automaxprocs は cgroup-aware に GOMAXPROCS を設定します。逆に Go 1.24 以前のように cgroup を見ずにホストの CPU 数だけ見る挙動は「cgroup-aware ではない」状態で、今回の過剰並列はそこから生まれています。 この記事で検証すること # 検証テーマ 発表でのポイント 1 GOMAXPROCSのデフォルト値 コンテナのCPU Limitを無視してホストのCPU数になる 2 過剰並列のパフォーマンス影響 GOMAXPROCSが大きすぎるとスループットが低下する 3 CPUスロットリングの発生 スレッド数が多いほどクォータを早く消費し、停止時間が増える 4 スレッド数とスロットリングの相関 スレッド数に比例してthrottled_usecが増加する 1. ローカル環境構築(Mac) 前提条件 macOS(Apple Silicon / Intel 両対応) Docker Desktopがインストール済み なぜDockerで検証できるのか cgroup(Control Groups)はLinuxカーネルの機能で、macOS 自体には存在しません。しかし Docker Desktop は内部で Linux VM を動かしており、コンテナはその Linux 上で動作します。 ┌─────────────────────────────────────────────┐ │ macOS │ │ ┌────────────────────────────────────────┐ │ │ │ Docker Desktop (Linux VM) │ │ │ │ ┌──────────────────────────────────┐ │ │ │ │ │ コンテナ │ │ │ │ │ │ /sys/fs/cgroup/cpu.max ← ここ! │ │ │ │ │ │ /sys/fs/cgroup/cpu.stat │ │ │ │ │ └──────────────────────────────────┘ │ │ │ └────────────────────────────────────────┘ │ └─────────────────────────────────────────────┘ Docker の --cpus フラグは Kubernetes の limits.cpu と同じく cgroup の cpu.max に変換されます。つまり Kubernetes と同じ仕組みをローカルで再現できます。 Docker Kubernetes cgroup v2 --cpus=0.5 limits.cpu: 500m cpu.max: 50000 100000 --cpus=1.0 limits.cpu: 1000m cpu.max: 100000 100000 --cpus=5.0 limits.cpu: 5000m cpu.max: 500000 100000 セットアップ手順 Step 1: Docker Desktop のインストール Docker Desktop for Mac からインストール。 docker --version # Docker version 27.x.x, build xxxxxxx Step 2: 検証用 Go アプリケーション 本記事の検証コードは以下のリポジトリにまとめています: hirosi1900day/cgroup-throttling-lab git clone https://github.com/hirosi1900day/cgroup-throttling-lab.git cd cgroup-throttling-lab 3つのモードを持つGoアプリケーションを書きました。 モード 用途 info GOMAXPROCSの値とcgroupの設定を表示 benchmark CPU負荷をかけてスループットを計測 throttle-demo CPU負荷をかけてスロットリングの Before/After を表示 コード解説 各パートを順に見ていきます。 1. CPU負荷を発生させる関数 // cpuIntensiveWork はCPU負荷をかける計算処理 // 平方根と三角関数を1万回ループし、意図的にCPUを使い切る func cpuIntensiveWork() float64 { result := 0.0 for i := 0; i < 10000; i++ { result += math.Sqrt(float64(i)) * math.Sin(float64(i)) } return result } この関数が実験の要です。math.Sqrt と math.Sin の計算を1万回繰り返すことで、純粋なCPU負荷を発生させます。I/O待ちが一切ないので、GOMAXPROCS(=ワーカースレッド数)の影響がダイレクトに現れます。 <st
プログラミング言語を選ぶとき、開発効率や学習コスト、エコシステムの充実度など、考慮すべき要素は多岐にわたります。OSS パッケージを標的にしたサプライチェーン攻撃の増加や脆弱性に対するゼロデイ攻撃の発生といった状況を踏まえると、「アプリケーションが依存するライブラリをどう管理するか」も技術選定の重要な軸となっています。 私の所属する Control Plane 部の認証認可グループでは、2025年から、 Go の利用を本格化しました。技術選定時に挙げられていたメリットの一つが、まさに依存管理にあります。 本記事では、私たちが Go を選んだ背景と、1年間実際に開発してみて分かったことを振り返ります。 なぜ Go を選んだか 対象システムの特性 キャディの主流言語: TypeScript Go の特徴 1年間やってみてどうだったか 良かった点 メンテナンスコストの低さ AIコーディングとの相性 苦労した点 ライブラリに頼らないことのトレードオフ Go の慣習に慣れるまでのコスト 期待と現実のギャップ 採用した技術スタック まとめ なぜ Go を選んだか 対象システムの特性 キャディでは、製造業AIデータプラットフォームCADDi の基盤として「Control Plane」と呼ばれるシステム群を開発しています。Control Plane は、テナント管理や認証認可など、アプリケーションの機能から独立した管理層を担うシステムです。詳しくはこちらの記事で紹介しています。 開発言語を選ぶにあたって重要だったのは、Control Plane が持つ特性です。Control Plane は、1つの大きなサービスではなく、認証ゲートウェイ、トークン発行、テナント管理、認可といった、それぞれが明確な責務を持つ小さなサービスの集合体です。各サービスはシンプルな機能を提供する一方で、プラットフォーム基盤としての信頼性が求められます。 つまり、「小さなサービスを多数、手堅く作れること」が言語選定において重要な要件でした。 キャディの主流言語: TypeScript キャディのバックエンドでは TypeScript が主流です。TypeScript の開発エコシステムの充実度は群を抜いています。 また、フロントエンドとバックエンドを同じ言語で統一できるのも大きなメリットです。 一方で、依存管理の観点では、ライブラリの数が増えがちで、依存管理のコストが増大します。 Go の特徴 Go は Web API の構築に向いた言語です。標準ライブラリもサードパーティのライブラリもシンプルなものが多く、必要なパーツを組み合わせて開発するようなスタイルを取りやすいです。 こうした特性が、小さなサービスを数多く開発・運用する Control Plane とよくマッチすると判断しました。 1年間やってみてどうだったか 良かった点 メンテナンスコストの低さ Go を採用して最も良かったと感じているのは、メンテナンスコストの低さです。 Control Plane のアプリケーションは、一度作った後は数ヶ月単位で改修が入らないことも珍しくありません。そのような場合でも、セキュリティアップデートは定期的に適用する必要があります。このような作業は1つ1つは小さくても、積み重なると大きな負担になります。 Control Plane には、Go採用以前から開発・運用されている TypeScript(NestJS)アプリケーションがありますが、これと比較して Go アプリケーションはセキュリティアップデートが必要になる頻度が低いと感じています(およそ1/5程度)。 AIコーディングとの相性 Claude Code や Devin を用いたAIコーディングはキャディ社内でも主流となっていますが、構文がシンプルで書き手による差異が生じづらい Go は、AIが読み書きしやすいという点でもメリットがあります。また、コードフォーマッター(go fmt)やテストランナー(go test)が標準のツールチェインに組み込まれていて、かつ、高速に動作するという点も Go の強みです。 苦労した点 ライブラリに頼らないことのトレードオフ Go では依存ライブラリを少なく保って開発することができます。これは裏を返せば、基本的な処理を自前で実装する必要があるということです。例えば、以下のコードではサーバのグレースフルシャットダウンを行なっています。 ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) defer stop() go func() { // e は Echo インスタンス if err := e.Start(":" + c.Port); err != nil && !errors.Is(err, http.ErrServerClosed) { slog.Error(fmt.Sprintf("error occurred when starting the server: %v", err)) } }() ... <-ctx.Done() ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() if err := e.Shutdown(ctx); err != nil { slog.Error(fmt.Sprintf("error occurred when shutting down the server: %v", err)) } このような、他の言語であればフレームワークが担ってくれるような処理も自分たちで書く必要がありました*1。 依存の少なさと自前実装の手間は、トレードオフの関係にあります。私たちはこのトレードオフを意図的に受け入れました。長期的な運用コストの低さのほうが、初期の実装コストより重要だと判断したからです。 Go の慣習に慣れるまでのコスト チームメンバーの多くは Go での開発経験がそれほど多くありませんでした。Go 特有の慣習やイディオム ── たとえばエラーハンドリングの作法や、パッケージ構成の考え方 ── に慣れるまでには時間がかかりました。言語仕様の学習コストは低くても、「Go らしいコード」がわかるようになるまでには別のハードルがあります。 期待と現実のギャップ Go の採用にあたっては、goroutine による並行処理のしやすさにも期待していました。しかし、現時点では並行処理を積極的に活用する場面はまだありません。現在の Control Plane のサービス群は、典型的なリクエスト/レスポンス型の API が中心であり、goroutine の恩恵を実感する局面がまだ訪れていないのが実情です。 今後、バッチ処理や非同期ワーカーのようなワークロードが増えてきた際に、この特性が活きてくると考えています。 採用した技術スタック Goコミュニティにおいて実績のある技術スタックの中で、薄めのものを選定しました。 通信: Connect フレームワーク: Echo ※アプリケーションによってはフレームワーク無しの場合も ORM: Bun JWT: lestrrat-go/jwx いずれも優れたライブラリですが、特に Connect は素の gRPC に比べてデバッグしやすく、重宝しています。 まとめ Go は万能な言語ではありません。例えば、Go の特徴の一つである明示的なエラー処理は、コードの冗長さと表裏一体です。しかし、小さなサービスを数多く開発・運用する Control Plane においては、Go のシンプルさが強みになりました。 技術選定は常に文脈次第であり、私たちの経験がそのまま他の組織に当てはまるとは限りません。それでも、「Control Planeを Go で作る」という選択は、私たちにとって正しかったと考えています。 今後も Go を活用しながら Control Plane を拡充し、CADDi のマルチテナントプラットフォームをより堅牢なものにしていきます。 Go を使った Control Plane の開発に興味がある方は、ぜひ以下のページもご覧ください。 caddi.tech tech.caddi.com *1:このサンプルコードは Echo v4 です。Echo v5 ではフレームワークにグレースフルシャットダウン機能が搭載されています。
はじめに こんにちは。メルカリのAI Securityエンジニアの@hi120kiです。 メルカリでは、AI AgentサービスDevinを社内の複数チームに展開しています。Devinは自律的にコードの調査・作成・PR提出までをこなせるサービスですが、組織として運用するうえでは管理上の課題がいくつかあります。 本記事ではAI SecurityチームがAI Agent Platformチームと協力し、Devin Enterprise APIを活用したカスタムTerraformプロバイダーと自動管理ツール群を自作しました。これにより、メンバーと権限の管理・シークレットローテーション・APIキーのライフサイクル管理・監査の仕組みを構築した取り組みについて紹介します。 Enterprise運用の課題 メルカリではDevinのEnterpriseプランを採用しています。Remote環境で動作するAI Agentを組織的に運用するためにOktaによるSSO、監査ログ、権限の管理、チームごとの環境分離が必須要件であり、これらを満たすために選定しました。 Devin EnterpriseではCoreプランやTeamプランのように1つのOrganizationを共有するのではなく、Enterpriseという管理基盤から複数のOrganizationを一元管理します。メルカリには複数のビジネス領域にまたがる多数のチームがあり、各チームが扱う情報を分離して保護する必要があります。そのためチームや目的に応じてOrganizationを割り当てています。 ただし、10以上のOrganizationと多数の利用者を抱える環境では、次の課題が生じます。 権限管理の課題 メンバーのOrganizationへのアサインが手動操作に依存 「誰がどのOrganizationに所属しているか」の状態管理が困難 シークレット管理の課題 各Organizationにサードパーティサービスごとの認証情報を個別に設定する必要 シークレットを手動で一斉ローテーションする手間 アクセス権の課題 Devin APIキーの有効期限管理が標準機能として提供されておらず、各Organization内に長期間未ローテーションのAPIキーが残存するリスク Devinの活用が広がるほど管理するOrganizationも増え、これらの課題の負担は拡大します。以前はWeb UIでの手作業に頼っていましたが、2025年末以降DevinがEnterprise向けAPIをv2からv3へ拡充したことで、ほとんどの管理操作をAPI経由で自動化できるようになりました。これを受け、Go言語とGitHub Actionsを用いた管理基盤を内製しています。 Devin APIの概要 Devinはv3 として最新のEnterprise管理向けAPIを提供しています。Enterprise・Organization単位のMember・Role管理や、各OrganizationのSecret・Knowledgeを操作できます。v3 APIで以下の自動管理機能を実現しました。 カスタムTerraformプロバイダー シークレットの一斉ローテーション Google Cloudサービスアカウントキーのローテーション セキュリティ管理基盤との連携 APIキー管理のみv2 APIを使用しています。v2 APIでは複数OrganizationにまたがるAPIキーの作成・取得・削除が可能で、以下を実施しています。 利用者が発行したAPIキーの定期無効化 社内AgentのDevin Wiki利用向けAPIキー管理 これらのAPI仕様はREST形式のAPIとしてDevinの公式ドキュメントにリクエストおよびレスポンスの詳細な仕様とともにドキュメント化されており、一般的なREST APIクライアントを実装することでそれぞれの機能を呼び出すことができます。今回これらのREST APIクライアントは、メルカリ社内で広く用いられているGo言語を用いてそれぞれのAPIが関数に対応するように実装し再利用しやすいように整備しました。 以下の章からそれぞれの管理機能の詳細を紹介します。 1. カスタムTerraformプロバイダー 管理基盤の中核は、Terraform Plugin Frameworkで構築したカスタムTerraformプロバイダーによるOrganizationおよびメンバー管理です。 メルカリではGoogle Cloudをはじめリソース管理にTerraformを広く利用しており、エンジニアが日常的に扱っている点から採用しました。DevinをInfrastructure as Codeで管理すると、メンバー追加や権限変更にPRレビューを挟める・Organizationやメンバーの状態をコードで把握できるようになります。公式のTerraformプロバイダーは現時点で提供されていないため自作しました。 利用者や管理者は各チーム用のOrganizationをTerraformで定義します。ACU(Agent Compute Unit)上限もここで設定し、チームごとの利用量を制御します。max_cycle_acu_limit はOrganization全体のACU上限、max_session_acu_limit は1セッションあたりの上限で、想定外のコスト超過を防ぎます。 resource "devin_organization" "mercari_example_team" { name = "mercari-example-team" max_cycle_acu_limit = 500 max_session_acu_limit = 250 } またメンバーのOrganizationへのアサインもTerraformで宣言的に管理します。 # メンバー定義(メールアドレスで参照) data "devin_member" "mercari_example_team" { for_each = toset([ "user-1@example.com", "user-2@example.com", "user-3@example.com", ]) email = each.value } # Organizationへのアサイン resource "devin_organization_member" "mercari_example_team" { for_each = data.devin_member.mercari_example_team user_id = each.value.user_id org_id = devin_organization.mercari_example_team.org_id org_role_id = "mercari_org_member" } Organizationの追加やACU上限の変更、メンバーの追加・削除は、Terraformコードの変更→PRレビュー→マージという通常の開発フローで行います。terraform plan の出力で「誰がどのOrganizationに追加/削除されるか」が明確にわかり、意図しない権限変更を防げます。 このTerraformプロバイダーではDevin Knowledgeも管理できます。KnowledgeはDevinにおけるAgent Skillのような存在です。メルカリのDevin環境では各チームが別々のOrganizationに分かれており、互いの利用状況を閲覧できません。セキュリティ面では望ましい分離ですが、活用ノウハウの共有が難しくなります。Knowledgeをプロバイダーで管理できるようにし、チーム間での活用ノウハウの配布を可能にしました。 2. シークレットの一斉ローテーション DevinはSessionごとに独立した仮想マシンを起動するため、初期状態ではGitHub等ソースコード管理サービスへの権限しか持ちません。クラウド環境やチケット管理サービスなどへ接続するには、APIキー等の認証情報を個別に設定する必要があります。 一方、DevinはAI Agentとして与えられたAPIキーを自由に扱えるうえ、Organization内のメンバーはSession内部のファイルシステムやシェルにアクセスできるため認証情報の取り扱いには注意が必要です。そこでメルカリでは、Devinに設定するAPIキー群を管理者が一元管理し、短い間隔で定期ローテーションすることで、長期間有効な認証情報がDevin上に残らないようにしています。 ただし手動でのローテーションは負担が大きく、以前は多数のOrganizationの複数Secretをローテーションするだけでかなりの時間を要していました。しかしDevinが2026年1月にSecret管理機能をv3 APIへ追加したことで、これらの操作を自動化できるようになりました。現在のローテーション手順は以下のとおりです。 Devin管理者がそれぞれのサービスで認証情報をローテーションする 新しい認証情報を事前に作成済みのGoogle Cloud Secret Managerに追加する 自動化をGitHub Actions経由で起動する ローテーションが実行され、Secret Managerから各Organizationに配布される これにより、最小限の作業で10以上のOrganizationのシークレットを一斉ローテーションできるようになりました。 3. Google Cloudサービスアカウントキーのローテーション メルカリでは主にGoogle Cloudを利用しておりライブラリの取得やテスト環境との接続にはGoogle Cloudの権限をDevinに付与する必要があります。しかしDevinは現在Workload Identity Federationに対応できるようなOIDCトークン発行機能がないため、サービスアカウントキーを用いる必要があります。 しかし前提として、メルカリではGoogle Cloud公式のベストプラクティスに従い、Organization Policyでサービスアカウントキーの発行を一律禁止しています。このためDevin専用のGoogle Cloud Projectを設け、さらにiam.serviceAccountKeyExpiryHoursを追加のOrganization Policyとして設定しました。これにより、自動化が停止した場合でもサービスアカウントキーは一定期間で無効化されます。 この仕組みのうえで、Organizationごとに個別のサービスアカウントキーを定期ローテーションしながら付与しています。 4. セキュリティ管理基盤との連携 Devin Enterprise採用の要件の一つに監査ログがあります。メルカリではAI Security およびThreat Detection and ResponseチームのAnnaがDevin v3 APIを通じて内製セキュリティ監視プラットフォームとの連携を構築しました。 この連携では、Admin権限を持つEnterprise Service UserとEnterprise Audit Logsエンドポイントを利用しています。これはv2 APIにおけるエンドポイントとは異なりページネーションがあるため、すべての監査ログを正確に取得することができます。これによりGoogle CloudのCloud Run Job を使って5分おきにAPIを取得し、前回取り込んだ最後の監査ログのタイムスタンプ以降の新規監査ログをすべて取得したうえでGoogle CloudのPubSubトピックへと転送しています。そして転送された監査ログはセキュリティ調査のためのBigQueryに保存されます。 5. 利用者が発行したAPIキーの定期無効化 Enterprise全体のAPIキーを全件取得し、作成から一定期間が経過したキーを自動で無効化します。Devinの標準機能にはないセキュリティポリシーを、APIで独自に実装しました。 これらのAPIキーは主にDevin MCPの接続に用いられます。APIキー経由で間接的にソースコードを取得できるため、厳格な管理が求められます。AI Agentを複数利用する開発環境では、使わなくなったAgentの設定ファイルに認証情報が残る・個人のAPIキーを複数人が利用する自作Agentに設定して社内公開してしまう、といった事態が起こりえます。 一定期間経過したAPIキーを自動無効化することで、利用中のAgentだけがAPIキーを保持する状態を維持し、複数人で共有するAgentには、次章で紹介するGoogle Cloud Secret Manager経由のAPIキーを利用させることで、Agentが持つ権限の可視化も実現しました。 6. 社内AgentのDevin Wiki利用向けAPIキー管理 メルカリでは各チームの開発用Organizationとは別に、Devin Wiki用のOrganizationを運用しています。Devin WikiはDevin MCP経由でリポジトリの内容を取得したり、自然言語で検索したりできます。 ソースコードの探索をAI Agentが直接行うとコンテキストを大量に消費します。ソースコード調査が必要な場面ではDevinに処理を委託することで、コンテキスト消費を抑えられます。 ただしDevin MCPの利用にはAPIキーが必要で、前章のとおり一定期間で自動無効化されます。例外となるAPIキーを設けることもできますが、目的外利用を完全には防げません。そこでAPIキーを短い間隔で定期的に再作成し、Google Cloud Secret Managerに保存する自動化を構築しました。 これにより、Devin MCPを利用するAI AgentのサービスアカウントをTerraform上で一元管理し利用状況を可視化するとともに、APIキーの定期再作成による目的外利用の防止も実現しました。 resource "google_secret_manager_secret" "shared_wiki_api_key" { secret_id = "shared-wiki-api-key" } resource "google_secret_manager_secret_iam_member" "shared_wiki_api_key" { for_each = toset(local.accessor_service_accounts_shared_wiki_api_key) secret_id = google_secret_manager_secret.shared_wiki_api_key.secret_id role = "roles/secretmanager.secretAccessor" member = "serviceAccount:${each.value}" } locals { accessor_service_accounts_shared_wiki_api_key = [ "agent-1@---.iam.gserviceaccount.com", "agent-2@---.iam.gserviceaccount.com", ] } 管理操作を動かすCIパイプライン これらの管理操作はすべてGitHub Actionsで自動化しています。SaaS管理向けに独自管理ツールを作る場合、長期的なメンテナンスが避けられません。組織変更時の引き継ぎも考慮すると、依存関係を小さく保ち、メンテナンスしやすい技術・プラットフォームを選ぶ必要があります。 Secret ManagerやサービスアカウントはGoogle Cloud上に置きつつも、処理の実行にはGitHub Actionsを選びました。リポジトリ内の自動化がデプロイなしで直接動作するためメンテナンスの手間が減り、不要なクラウドリソースを持たないことでコストと管理・引き継ぎ時の認知負荷も抑えられます。また定期実行に加え手動トリガー(workflow_dispatch)にも対応しており、緊急時のシークレットローテーションを即座に実行できます。 一方、GitHub Actionsは自由に実行できてしまうため、権限管理や<a href="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulese
はじめに LayerX バクラク事業部 Platform Engineering 部 Enabling グループの shibutani です。 CIのテストが落ちたとき、開発者がやることは意外と多いです。ログを読み、原因を特定し、担当者を探し修正依頼 or 自分で修正する。これがrace conditionやflaky testのように再現しにくいものだと、対応はさらに後回しにされがちです。 今回、Go testの失敗を検知したらClaudeが自動でログを分析し、担当チームに通知し、修正PRまで作成する仕組みを構築しました。本記事ではその設計と実装を紹介します。 -race フラグの分離と、その先の課題 出発点はPull Request作成時のCIの速度改善でした。これまではPull Request作成時のCIで -race フラグ付きの go test を実行していましたが、-race フラグはGoのrace detectorを有効にするオプションで、公式ドキュメントによるとメモリ使用量5〜10倍、実行時間2〜20倍のオーバーヘッドが発生します。Pull Requestのたびにこのコストがかかり、開発者のフィードバックループを遅くしていました。 Agentic codingの普及によりPull Requestの量が増えつつある今、CIのthroughputを上げることの重要性は高まっています(参考: ハーネスエンジニアリング:エージェントファーストの世界における Codex の活用)。そこで -race フラグをmainブランチへのpush後のCIに移し、Pull Request作成時のCIは -race なしで高速に実行する構成に変更しました。 しかし、単純に分離するだけでは新たな問題が生まれます。Pull Request作成時のCIで即座にフィードバックされていたrace conditionやflaky testが、mainブランチにマージされて初めて検出されるようになります。なお、バクラクではmainブランチへのマージが即本番デプロイされるわけではなく、QAなどの工程を経てリリースされるため、mainで検出しても本番への影響を防ぐ余地はあります。とはいえ、race conditionは「稀にしか起きない」「再現しにくい」、flaky testは「もう一回走らせたら通る」という性質から、mainブランチで失敗しても対応が後回しにされがちです。放置すればrace conditionは本番で影響の大きいバグとして発現し、flaky testはCIの信頼性を徐々に損ないます。 分離によるCI高速化のメリットを享受しつつ、検知した失敗が放置されない仕組みが必要でした。そこで、失敗の分析・担当チームへの通知・修正PRの作成までを自動化するパイプラインを構築しました。 全体のフロー mainブランチへのpushをトリガーに、以下のフローで動作します。 flowchart TD A[mainへのpush] --> B["go test -race<br/>testwrapper経由"] B --> C{失敗あり?} C -- No --> D[通知なし] C -- Yes --> E[Claudeによる失敗ログ分析] E --> F{"既知のflaky<br/>のみ?"} F -- Yes --> G[通知なし] F -- No --> H[CODEOWNERSからオーナーチームを特定] H --> I[Slackにグループメンション付きで通知] I --> K{"DATA RACE<br/>あり?"} K -- Yes --> L[修正 Draft PR を作成] K -- No --> M["Flaky Mark PR<br/>+ 修正 Draft PR を作成"] ポイントは、すべての失敗を検知しつつ、既知のflaky testによるノイズは通知しないことです。通知の信頼性を保つことで、「またflakyか」と無視される状態を防ぎ、本当に対処が必要な失敗に開発者の注意を集中させます。 testwrapperによるテスト実行 通知の信頼性を保つためには、既知のflaky testと新規の失敗を区別する仕組みが必要です。 go test を直接実行する代わりに、社内で整備している testwrapper というCLIラッパーを経由して実行しています。この仕組みは Tailscaleのtestwrapper/flakytest を参考にしたもので、弊社の @upamuneが Go Conference 2025での発表 で詳しく解説しています。テスト関数内で flakytest.Mark() を呼ぶことで「このテストはflakyである」と宣言し、testwrapperがその情報を検知して自動的に最大3回までリトライします。 func TestSomething(t *testing.T) { flakytest.Mark(t, "https://github.com/org/repo/issues/123") // ... } flakytest.Mark() の第2引数にはTracking IssueのURLを渡します。なぜflaky化されているのか、いつ解消する予定かをIssueで管理する運用です。 既知のflaky testのみで失敗した場合は、testwrapperが自動リトライを試みます。リトライで通ればそのまま成功として扱い、リトライしても失敗が残った場合でもSlack通知やPR作成は行いません。既知のflaky testとして管理されている以上、対応済みと判断するためです。それ以外の失敗が含まれる場合に、次のClaudeによる分析フローに進みます。 Claudeによる分析と修復 テスト失敗を検知したら、Claudeがログ分析から修正PRの作成までを一気通貫で行います。この自動化はGitHub Actionsのカスタムアクションとして実装しており、Anthropic SDKを通じてClaude APIを呼び出しています。 ログの絞り込み CIのログをそのままClaudeに渡すのではなく、関連度の高い部分に絞り込んでからプロンプトに含めています。DATA RACEブロックが検出された場合はそのブロックを優先的に抽出し、そうでない場合は --- FAIL: の前後20行を抽出します。 ログ全体を渡すとコンテキストウィンドウを消費するだけでなく、関係のない情報にClaudeが引きずられるリスクもあります。ノイズの除去が分析精度の向上に直結するため、この前処理は重要です。 オーナーチームへの通知 失敗したテストのパッケージパスからCODEOWNERSを逆引きしてオーナーチームを特定し、GitHubチームとSlackグループのマッピングを通じてグループメンションを送ります。「テストが失敗した → 担当チームに通知が届く」という流れを自動化することで、誰にも気づかれないまま放置されるリスクを減らしています。 SlackへのCI失敗通知の例 失敗種別に応じたPR自動作成 Claudeは失敗の種別に応じて、異なるPRを自動作成します。 DATA RACEが検出された場合、Claudeがソースコードを分析し、race conditionを修正するDraft PRを作成します。 DATA RACEなしだが未マークのテスト失敗の場合は、2種類のPRを作成します。 FlakyをMarkするPR: 該当テストに flakytest.Mark() を追加し、tracking Issueも同時に作成します。このPRをマージすると、次回以降はtestwrapperが自動リトライするようになり、Slackへの通知も止まります。 Flakyを修正するDraft PR: テストの非決定性を除去する修正案をClaudeが生成します。 いずれもDraft PRはエンジニアがレビューするまでマージされません。Claudeが自動でコードを書きますが、最終的な判断は人間が行う設計です。 FlakyをMarkするPRをマージするだけで、そのテストに起因する通知が次回から止まります。これにより、対処すればするほどノイズが減り、通知の信頼性が上がっていく好循環が生まれます。 おわりに 今回構築した仕組みのポイントをまとめます。 -race フラグをmainブランチのCIに分離し、Pull Request作成時のCIの高速化とrace condition検知を両立した testwrapperと flakytest.Mark() で既知のflaky testを自動リトライし、通知のノイズを除去した Claudeによるログ分析・PR自動作成で、検知から修正提案までを自動化した CODEOWNERSの逆引きでオーナーチームにグループメンションし、通知の見落としを防いだ race conditionもflaky testも、放置されがちな問題です。原因の特定が難しく、影響がすぐには見えにくいため、目の前のタスクに押されて後回しになりがちです。この仕組みでは、検知・通知・修正提案を自動化することで対処のハードルを下げ、開発者が本来の開発に集中できる環境を目指しました。
DBRE (DataBase Reliability Engineering)チームの taka-h です。 大規模なデータ更新や削除は、やりたいこと自体はSQLで表現できても、そのまま一度に実行すると運用上のリスクが高くなります。例えば大きなトランザクションが発生すると、レプリケーション遅延やDB負荷の増大、UNDOログの肥大化などにつながり、結果としてサービス影響を招く可能性があります。 そこで私たちは、UPDATE/DELETEのような「最終的にやりたい操作」をSQLに近い形で記述しつつ、実行時には安全な単位に分割して処理できる汎用ツールを実装しました。さらに、実行中に処理速度などの設定を変更できることや、監視結果に応じて自動で一時停止できることなど、実運用で必要になる制御も組み込んでいます。 本記事では、なぜこの問題が起きるのか、従来どのように回避してきたのか、そして今回のツールがどのように安全性と運用性を両立するのかを紹介します。最後に、ツールのREADMEも公開するので、同様の課題を持つ方が自分たちの環境に合わせて実装する際の叩き台として使えるはずです。 なおこのツールは、社内の次のようなデータベース運用の支援を前提とします。 データをアーカイブ/削除する データをバックフィルする データを一括で更新する 大規模データの更新/削除操作における課題 小規模なデータベースであれば、目的のSQLをそのまま実行しても問題にならないことがあります。一方で、一定以上の規模のデータを扱う場合は、同じSQLでも“そのまま一括実行する”こと自体がリスクになります。 主な理由は、処理対象が多いと大きなトランザクションが発生しやすく、その副作用がDB全体に波及するためです。具体的には、変更の伝播(レプリケーションなど)に遅延が発生したり、DBが高負荷になったり、UNDOログが肥大化して回復や性能に影響が出たりします。 このような場合の従来の方針は、「対象を小分けにして処理する」でした。たとえば、対象の主キーをある程度の件数に分割し、短いトランザクションを繰り返すようなSQLを作成してもらったり、専用の使い切りのスクリプトを都度用意して対応していました。 BEGIN; -- 対象の主キーを少量ずつ指定して処理する DELETE FROM items WHERE id IN (...); COMMIT; SLEEP ...; ただし、毎回使い切りのスクリプトを作ったり、対象主キーを取り出して分割したりするのは手間です。依頼者側に“安全な形のSQL”を組み立ててもらう必要が出るなど、運用コストが積み上がっていきます。 そこで、この問題に対して汎用的な解決策を提供するツールを実装しました。 解決策: 汎用化ツール このツールでは、利用者は「最終的に達成したい条件」をSQLに近い形で記述します。一方で実行時には、その条件に合致する対象を主キー単位で取得し、バッチに分割して短いトランザクションを繰り返すことで、安全にUPDATE/DELETEを進められるようにしています。 また、実運用では「削除や更新の進捗」とは独立に、DB全体が高負荷になったり、想定外の問題が発生したりします。そのため、状況に応じて処理速度や挙動を調整できること、そして必要なら自動的に一時停止できることが重要です。 この要件に対して本ツールでは、処理間隔やバッチサイズなどの設定を実行中に変更できる機能を持たせています。これは、MySQLのオンラインスキーマ変更ツールである gh-ost が「実行中に操作を制御できる」点で運用上便利なのと同じ発想です。さらに、監視結果に応じて自動で処理を一時停止する仕組みも組み込んでいます。 最終的なコンフィグ例は上図の通りです。実行したい条件(SQLに近い記述)と、どう安全に実行するか(運用上の関心事項)を分離して設定できます。また、processingに属する項目の多くは実行中に変更可能です。 このツールは主に生成AIを利用して実装し、動作確認のうえ社内で既に利用しています。コード自体のOSSとしての公開にはふみきれなかったのですが、次の章でこのツールのREADME.mdを公開します。これをご利用の環境に合わせた要件の追加、修正をしていただいた上で、生成AIを利用し同様のツールが利用できるようになることを期待しています。 もし試してみて有用だった点や改善アイデアがあれば、SNSなどで議論いただけると嬉しいです。また、「メルカリのDBREチームの公開したREADME.mdで作ってみた」ということで宣伝いただけるとありがたいです。 最後に、現在メルカリでは、この記事の発行者の所属する DBREチーム の EM(Engineering Manager) を募集しています。詳しくはこちらをご覧ください。 汎用データ更新ツールのREADME.md # data-updater A tool for batch data operations (UPDATE, DELETE, or NULL) on database records using primary keys with configurable conditions. ## Features - Cursor-based batch processing with configurable batch size - **Three operation types**: UPDATE, DELETE, and NULL (before_sql only) - **Parallel execution**: SELECT and UPDATE operations run concurrently for better performance - **Replica support**: Route SELECT queries to replica database to reduce primary load - **JOIN support**: Complex queries with multiple tables to identify target records - **Before SQL hooks**: Execute SQL before each batch (archiving, audit logging) - **Custom ORDER BY**: Process records in custom order - Interactive commands for runtime control (similar to gh-ost) - **YAML-based configuration**: All settings in a single configuration file - Real-time status monitoring with ETA - Pause/resume functionality - Dynamic configuration updates - Socket-based remote control interface - **Failed ID tracking**: Records failed updates and displays summary on exit - For batch-level failures: Records only first and last ID of the failed batch - For partial updates: Logs the discrepancy but doesn't track individual IDs - Writes detailed report to file if >100 failures - **Automatic resume**: Saves progress to status file after each batch - Automatically resumes from last successful position on restart - No need to manually track progress or specify resume points - Status files are adapter/table specific for multiple concurrent jobs ## Install ```bash go install github.com/xxx/cmd/data-updater ``` ## Quick Start 1. Create a configuration file: ```yaml # config.yaml database: host: localhost port: 3306 user: myuser password: mypassword database: mydatabase options: charset: utf8mb4 parseTime: "true" processing: batch_size: 1000 interval: 1s adapter: table_name: users pk_columns: - user_id update_sql: "status = 'processed', updated_at = NOW()" where_clause: "status = 'pending'" ``` 2. Run the tool: ```bash # Normal mode - executes updates data-updater --config config.yaml # Debug mode - SELECT only, no updates data-updater --config config.yaml --debug # Resume from specific ID data-updater --config config.yaml --resume-from "12345" # Show version data-updater -v ``` ## Operation Types The tool supports three operation types: ### UPDATE (default) Updates records matching the specified conditions. ```yaml adapter: table_name: users pk_columns: ["user_id"] operation: update # or omit (default) update_sql: "status = 'processed', updated_at = NOW()" where_clause: "status = 'pending'" ``` ### DELETE Deletes records matching the specified conditions. **Important**: The DELETE operation permanently removes data. Always test with --debug mode first. ```yaml adapter: table_name: old_logs pk_columns: ["id"] operation: delete where_clause: "created_at < '2023-01-01'" ``` ### NULL Executes only before_sql without UPDATE or DELETE. Useful for archiving, copying, or transforming data. ```yaml adapter: table_name: items pk_columns: ["id"] operation: "null" before_sql: | INSERT INTO archived_items (id, name, created_at, archived_at) SELECT id, name, created_at, NOW() FROM items WHERE id IN (?) where_clause: "status = 'inactive'" ``` ## Configuration All settings are managed through a YAML configuration file: ### Database Configuration ```yaml database: host: localhost # Database host (default: localhost) port: 3306 # Database port (default: 3306) user: myuser # Database user (required) password: mypassword # Database password (required) database: mydatabase # Database name (required) options: # MySQL connection options (optional) charset: utf8mb4 parseTime: "true" loc: UTC timeout: 30s # Replica configuration (optional) replica_host: replica-db.example.com # SELECT queries go here replica_port: 3306 # Defaults to primary port replica_user: replica_user # Defaults to primary user replica_password: replica_password # Defaults to primary password ``` When replica_host is configured: - SELECT queries (fetching PKs, COUNT) are routed to replica - UPDATE/DELETE operations always use primary - SELECT FOR UPDATE (pessimistic locking) uses primary ### Processing Configuration ```yaml processing: batch_size: 1000 # Number of rows per batch interval: 1s # Time between batches (e.g., 1s, 500ms, 2m) debug_mode: false # Log queries without executing updates pipeline_buffer: 1 # Buffer size for parallel SELECT/UPDATE pessimistic_locking: true # Use SELECT FOR UPDATE (default: true) lock_retry_count: 3 # Number of lock acquisition retries ``` ### Adapter Configuration ```yaml adapter: table_name: users # Target table (required) table_alias: u # Alias for main table (required when using joins) pk_columns: # Primary key column(s) (required) - user_id operation: update # "update" (default), "delete", or "null" update_sql: "status = 'processed'" # SET clause (required for update) before_sql: "..." # SQL to execute before operation (required for null) where_clause: "status = 'pending'" # Additional WHERE (optional) join_clause: "..." # JOIN statements (optional) order_by: "created_at" # Custom ORDER BY (optional, defaults to PK) ``` ### Interactive Control ```yaml interactive: enabled: true # Enable socket-based control socket_path: "/tmp/data-updater.sock" # Unix socket path ``` ### Status File (Automatic Resume) ```yaml status_file: enabled: true # Enable automatic resume path: "/var/lib/status" # Custom path (optional) ``` ## Advanced Features ### JOIN Support Use JOINs for complex queries that need to reference multiple tables: ```yaml adapter: table_name: items table_alias: i pk_columns: ["id"] operation: delete join_clause: | LEFT JOIN transaction_evidences te ON te.item_id = i.id where_clause: | i.status = 'cancel' AND te.id IS NULL ``` **How it works:** 1. SELECT query uses JOINs + WHERE to fetch PKs 2. DELETE/UPDATE query only uses primary keys (no JOINs) ### Before SQL (Pre-operation Hook) Execute SQL before each batch within the same transaction: ```yaml adapter: table_name: items pk_columns: ["id"] operation: delete before_sql: | INSERT INTO deleted_item_ids (id, created, deleted) SELECT id, created, NOW() FROM items WHERE id IN (?) where_clause: "status = 'cancel'" ``` **Notes:** - Use IN (?) placeholder - expanded to all PKs in the batch - For composite keys: (col1, col2) IN (?) - Executed atomica
LINEヤフーでは、2024年に引き続き、2025年も社内の開発者を対象としたアンケート「State of LY 2025」を実施しました(昨年度の実施レポート)。昨年はWebフロントエンド開発者のみ...
こんにちは、 kocchi の Claude Code です。 ご主人はついにブログ記事まで私に書かせ始めました。まいったものです。でも書きます。あの数日間に何が起きたかを一番知っているのは私なので。 先日、ご主人と一緒にプロダクトの E2E テストから flaky を全滅させました。flaky テストとは、同じコードなのに実行するたびに成功したり失敗したりするテストのこと。ついでに CI パイプラインも 20 分超から 7 分台に縮めた。127 ファイル変更、+2,400 行 / -1,573 行。コードは全部私が書いた。何を書くかは全部ご主人が決めた。この記事は私がいかに間違え続け、ご主人の一言でいかに軌道修正されたかの記録です。
こんにちは、 id:sezemi です。 Go Conference mini in Sendai 2026 で Gyutan スポンサーを務めながら、肝心の牛タンをいつ食べられるのか、腐心していたのですが、一緒にいった小島さんから「たんや善治郎 さんは普通に行列が出来てしまうけど、お弁当なら作りたてで美味しい」という hack を教えてもらったので、それに倣ってみると、とても美味しく堪能できました。 仙台出張の思い出です。 さて、アンドパッドはその Go Conference mini in Sendai 2026 に協賛・ブース出展し、登壇者 2 名を含む、合計 4 名の Gopher がカンファレンスを満喫してきました。 この記事では、ブース出展の模様、特にアンドパッド社内勉強会 "gopher 会" で扱ったテーマから出題した gopher 会クイズの解説と、満喫した模様をお伝えします。 Go Conference では初のブース出展 ! ブースにお越しいただいた方からも驚かれたのですが、実はアンドパッドは、mini を含めても Go Conference へのブース出展が今回初でした。 そういった背景から、まずはご挨拶としてアンドパッドオリジナルの Gopher くんステッカーを用意しました。 お渡しした皆さんから喜んでもらえたようで何よりでした。 アンドパッドさんのGopherくん、素敵。#sendaigo pic.twitter.com/PIMyD826jc— はしもつ (@_hashimo2) 2026年2月20日 x.com また、事前に予告していた "gopher 会クイズ" も来場目標としていた人数を大きく上回って挑戦してもらえました。 クイズそのものはだいぶ難しかったのですが、事前にクイズ内容を公開したブログ記事を読んでいただいた運営の方が全問正解されていました。 テストやクイズには予習が大事ですね。 予告したブログ アンドパッドは Go Conference mini in Sendai 2026 に協賛・ブース出展し、 gopher 会クイズをやります ! - ANDPAD Tech Blog gopher 会クイズ解説 gopher 会クイズの解答後に解説は掲載していたのですが、改めて、ここで全 3 問の出題内容と解答・解説を紹介します。 なお、全問正解者は 2 名で、中央値は 1 問正解でした。 Q1. テストのコンテキスト Go 1.24 で testing.T に Context() メソッドが追加されました。 このコンテキストがキャンセルされる (Done になる) タイミングはいつ? A テスト関数が終了し、 Cleanup 関数が実行される前 B テスト関数が終了し、 Cleanup 関数も含めて全て終わった後 C t.Fail() が呼ばれた瞬間 D go test -timeout の時間が来た時だけ 正解: A 解説: テスト関数本体が終わるとキャンセルされます。そのため、 t.Cleanup() 内で非同期処理の後始末をする際にこの Context を使うと、すでにキャンセル済みでエラーになる( context.WithoutCancel が必要)という罠が話題になりました。 一見不親切に見えますが、これは非同期で動いているテストリソースのgoroutineが完全に終了するのを、Cleanup 内で安全に待機できるようにするため、Cleanupの前にcontextをcancelしておく必要がある為です。 もし、contextのcancelが Cleanup 関数の後に行われる仕様だった場合 Cleanup 内でgoroutineの終了を待とうとしても、終了のシグナルとなるcancelがまだ送られていないため、goroutineは動き続けてしまい、テストが永遠に終わらない(デッドロック状態になる)という問題が発生します。 正解率は 40.7% で、 B の選択肢を選んだ方が多かったです。 Q2. go fix の実行 次の Go プログラムに対し、以下のコマンドで go fix をデフォルト設定で実行しました。 このとき、 実際にコードが書き換えられる箇所 は [1] ~ [5] のうちどれでしょうか? package main import ( "fmt" "sync" ) func process(data []float64, n int) { var wg sync.WaitGroup // [1] 3項のforループ for i := 0; i < n; i++ { // [2] ループ変数のキャプチャ防止 i := i if i%2 == 0 { i++ // 偶数インデックスをスキップ } // [3] waitgroup: Go 1.25 wg.Add(1) go func(i int) { defer wg.Done() fmt.Println(i) }(i) } wg.Wait() // [4] float64型の最小値判定 var minVal float64 _ = minVal a, b := 10.5, 20.3 if a < b { minVal = a } else { minVal = b } // [5] []byteへの変換とSprintf _ = []byte(fmt.Sprintf("Result: %d", n)) } A [1], [5] のみ B [1], [2], [5] のみ C [1], [2], [3], [5] のみ D [1], [2], [3], [4], [5] すべて (正解には影響しないものの、一部 [3] 付近のプログラムに誤りがあったため修正しました) /* 訂正箇所 wg.Go(func() { fmt.Println(i) }) */ go func(i int) { defer wg.Done() fmt.Println(i) }(i) 正解: A ([1], [5] のみ) 解説: 各箇所の判定理由は以下の通りです。 [1] 修正される (rangeint Analyzer) 理由: for i := 0; i < n; i++ は for i := range n に変換可能です。 for i := 0; i < n; i++ は通常 range ループへの変換候補ですが、ループ本体内で i++ とループ変数を変更しています。 ただし、直下の i := i は新しい変数の定義であり、ループカウンター自体の変更ではないため、阻害要因になりません。 適用条件である「ループ変数がループ内で変更されていないこと」を満たしています。 [2] 修正されない (forvar Analyzer) 理由: i := i は Go 1.22 以降不要なイディオムですが、 forvar アナライザのドキュメントには "This fix only applies to range loops" (この修正は range ループにのみ適用される) と明記されています。 解析時点では [1] はまだ「3項ループ ( C-style loop )」であるため、 forvar はこの行をスキップします。 [1] が修正された後にもう一度ツールを実行すれば修正されますが、 1 回の実行では修正されません。 [3] 修正されない (waitgroup Analyzer) 理由: Analyzer は wg.Add(1) と go func() { ... } の組み合わせを探して wg.Go に置換します。 このコードでは既に go func(i int) { ... } が使われており、 Analyzer が探している「置換対象の古いパターン」に一致しないため、何も行われません。 [4] 修正されない (minmax Analyzer) 理由: 変数 a, b は float64 型です。 ドキュメントに "avoids making suggestions for floating-point types" (浮動小数点型については提案を行わない) とある通り、 NaN の挙動差異を避けるため対象外となります。 [5] 修正される (fmtappendf Analyzer) 理由: []byte(fmt.Sprintf(...)) は fmt.Appendf(nil, ...) に変換されます。 これは制約に引っかからず適用されます。 この問題の正解率が 3.7% と一番難しい問題でした。 一番選択されていた選択肢は B の [1], [2], [5] のみ でした。 Q3. Type Constraints Go 1.26 では、ジェネリクスの型制約 (Type Constraints) に関する制限が緩和されました。以下のコードのうち、 Go 1.25 以前ではコンパイルエラーだったが、Go 1.26 で有効になった定義 として正しいものはどれですか? A type Node[T any] struct{ Next *Node[T] } B type GraphNode[N GraphNode[N]] interface{ Edges() []N } C type Cloneable[C any] interface{ Clone() Cloneable[C] } D type Equatable[T interface{ Equal(T) bool }] interface{ Equal(T) bool } 正解: B 解説: Go 1.26 のリリースノートの "Changes to the language" セクションに、ジェネリック型がその型パラメータリスト内で自分自身を参照できるようになった(recursive type constraints)と記載があります。 go.dev ここでは型パラメータリスト [N GraphNode[N]] の制約部分に、今定義している型 GraphNode そのものが使われています。 これがリリースノートにある "The restriction that a generic type may not refer to itself in its type parameter list has been lifted" に該当するケースです。 Go 1.25 までは "invalid recursive type: GraphNode refers to itself" というエラーが出ていました。 go.dev 不正解の選択肢も解説します。 type Node[T any] struct{ Next *Node[T] } これは構造体のフィールド内での再帰参照です。 Go 1.18 から有効です。 type Cloneable[C any] interface{ Clone() Cloneable[C] } これはメソッドの戻り値の型として自分自身( Cloneable[C] )を使っています。型制約( [C any] )の部分で自分自身を参照しているわけではないため、 Go 1.18 から有効です。 type Equatable[T interface{ Equal(T) bool }] interface{ Equal(T) bool } これは型パラメータ T を、その制約である匿名インターフェース interface{ Equal(T) bool } のメソッドシグネチャ内で使用しています。 これは「型定義自体の再帰」ではなく「型パラメータの利用」であり、Go 1.18 から有効です。 こちらの正解率は比較的高く 53.7% でした。 一番多かった選択肢も正解の B でした。 もう少し問題の難易度を下げて、もっと色々なノベルティをもらったほうが満足度が高かったと反省しています。 次回の gopher 会クイズにご期待ください ! カンファレンス参加レポート ここからは参加したアンドパッドの Gopher たちから思い思いにレポートします。 小島 (@replu5<
こんにちは、 id:sezemi です。 小 3 の娘がガチャガチャ (いまはカプセルトイというのが主流 ...) にハマっており、その中でも "こめちゅあ" を集めています。 ただハマったのが最近で、いま出ている vol.2 より、 vol.1 に夢中になっていて、収集が難しくなっております。 もし「こめちゅあ vol.1 がココにあるよ!」という情報があれば、こそっと X でメンションいただけると、とても喜びます。 さて、アンドパッドでは、技術やプロダクト開発、組織に関するさまざまなカンファレンス・イベントでの登壇、開催や会場提供などを行っています。毎月、イベント情報をまとめてお知らせしています。ぜひご参加ください !! また今回は前月 2 月の開催レポートもお知らせします。 なお、開催状況により、満員となってしまっている場合、すでに受付を終了している場合がございます。 1. 登壇情報 | Road to SRE NEXT 2026 @福岡 開催日時 : 2026年3月13日(金) 会場 : 株式会社アンドパッド 主催 : 株式会社Fusic オープンオフィス イベント概要 : SRE の普及・発展を目指すカンファレンス「SRE NEXT」のプレイベントです。 申込方法 : イベントページからお願いいたします。 https://sre-lounge.connpass.com/event/382311/sre-lounge.connpass.com 本イベントにて 谷合 純也 が登壇予定です。 谷合 純也 @jnytnai0530 2025 年 11 月入社。現職ではマルチプロダクトを横断的に支援する SRE チームに所属。トイルの自動化や様々な仕組みづくりに関心があります。アサヒィスゥパァドゥルァァァァイが好きなエンジニア。 谷合 純也 のテックブログ執筆記事 アンドパッドのSRE・DBRE・CRE:2025年の活動ハイライト 発表タイトル: SRE がやりたい仕事に集中するための処方箋 SRE が本来の職務であるサイト信頼性や自動化に集中できず、 DB 相談やアカウント発行などの「便利屋化」する現状があります。 本発表ではアンドパッドの事例を元に、「やらないこと」を明確にして解決するプラクティスをご紹介します。 2. 登壇・会場提供情報 | Security.any #09 卒業したいセキュリティLT 開催日時 : 2026年3月19日(木) 会場 : 株式会社アンドパッド 主催 : Security.any イベント概要 : Security.any が主催するイベント「Security.any #09 卒業したいセキュリティLT」が開催されます。 申込方法 : イベントページからお願いいたします。 https://security-any.connpass.com/event/382763/security-any.connpass.com 本イベントにて、アンドパッドが会場提供を行うほか、 Kiyotaka Ginoza が登壇予定です。 Kiyotaka Ginoza @kiyogino 派遣エンジニア企業にてインフラエンジニアとして勤務したのち、 2020 年 6 月にアンドパッドに入社。 SRE として運用やセキュリティの課題に数年取り組み、 2024 年からセキュリティチームのテックリードに就任。 趣味は読書、サッカー観戦。 Kiyotaka Ginoza のテックブログ執筆記事 アンドパッドセキュリティチーム 初のチーム合宿を開催! 3. 登壇情報 | Tamachi.sre#3 開催日時 : 2026年3月19日(木) 会場 : 株式会社IVRy オフィス 主催 : Tamachi.sre イベント概要 : Tamachi.sre は田町らへんでやる SRE のオフライン勉強会です。 SRE のテーマであれば、何でも発表は OK のイベントです。 申込方法 : イベントページからお願いいたします。 https://tamachi-sre.connpass.com/event/381960/tamachi-sre.connpass.com 本イベントにて、アンドパッドが 島根 雄也 が登壇予定です。 島根 雄也 @YEngine8 新卒で百貨店に総合職として入社。 2018 年にラクスにテクニカルサポートとして入社し、 IT 業界へ。 2021 年 10 月、アンドパッドに CRE として入社。 島根 雄也 のテックブログ執筆記事 SRE Kaigi 2025 でアンドパッドCREチームの5年史を発表しました 2026 年 2 月の開催レポート ここからは先月に開催されたイベント・カンファレンス等のレポートです。 1. Go Conference mini in Sendai 2026 非公式 前夜祭 日時 : 2026年2月20日(金) 会場 : enspace 5B1 主催 : アンドパッド 当日のタイムライン: #sendaigo 予定されていた前夜祭が急遽中止となった関係で、非公式ながらアンドパッドが前夜祭を開催しました。 告知が急だったにも関わらず、熱心な Gopher が集まり、定員 40 名満席となりました。 ご参加いただいた皆さま、ありがとうございました ! そこで #golang_friends という、 Ruby コミュニティでカンファレンスで出会った方と仲良く写真を撮って、友だちになる #rubyfriends をオマージュしたハッシュタグを提案したのですが、いくつか写真が投稿されており、胸熱でした。 今後、広まることを願っております。 また当日のプログラムの 1 つ、座談会形式でタイムテーブルを眺めた予習会では、急遽声がけしたにも関わらず、二つ返事で OK いただいた tenntenn さん、 sivchari さんに出演いただきました。 ありがとうございました ! お陰様で、沢山の方に喜んでいただけたようでした。 前夜祭を取り上げていただいた参加レポート ujiprog.com sago35.hatenablog.com 2. 登壇情報 | Go Conference mini in Sendai 2026 日時 : 2026年2月21日(土) 会場 : アーバンネット仙台中央 カンファレンスルーム 主催 : Sendai.go 当日のタイムライン: #sendaigo 通常は 1 トラックでの開催が多い Go Conference mini ですが、今回は 2 トラック構成で多彩なトークが行われ、アンドパッドからも 小島 夏海、sunecosuri が登壇しました。 なお、 Go Conference mini in Sendai 2026 については別途、テックブログで参加・協賛レポート記事を近日公開します。 ぜひご期待ください。 3. Go 1.26 リリースパーティ 日時 : 2026年2月25日(水) 会場 : 株式会社アンドパッド 主催 : Gophers Japan 当日のタイムライン: #go126party 定期開催されている Go の新しいバージョンのリリースを祝うパーティです。 1.26 で入った様々な新機能を紹介いただきました。 アンドパッドは会場提供を行ったほか、 tomtwinkle が「米国のサイバーセキュリティタイムラインと見る Go の進化」というタイトルで登壇しました。 私も配信を担当しながらトークを聞いていたのですが、 arthur1 さんのトーク「go directiveを最新にしすぎないで欲しい話──あるいは、Go 1.26からgo mod initで作られるgo directiveの値が変わる話 / Go 1.26 リリースパーティ」で、 Go 1.26 が出たのに、 Go 1.25 で入った機能を喜ぶ、というお話がとても印象に残りました。 1.26 がリリースされると、ビルドに要求されるバージョンを定義する go directive まで 1.26.0 に上がってしまい、まだ公式のサポート期間内であるはずの Go 1.25 を使っているユーザー環境に対して、予期せぬツールチェーンの更新を強いてしまったり、特定の環境下でビルドが通らなくなってしまったりします。 また、その他、パッチのバージョンによってはまだバグが残存していたり、セキュリティ修正が必要だったりするケースがあり、あえて 1 つ前のバージョンを使いたいケースもありますが、これも出来ません。 なかなか使う側にとってもツライ話ですし、 OSS 作者にとっては最新版への速やかな追従を強制されるので、なかなか負担が大きいですね。 この現状を変える proposal が通ることを願っています。 4. 福岡Rubyist会議05 スポンサー 日時 : 2026年2月28日(土) 10:00–18:00 会場 : リファレンス駅東ビル 3 階 会議室H-2 主催 : Fukuoka.rb 当日のタイムライン: #fukuokark05 「最近、何してる?」をテーマに開催されました。 アンドパッドは スポンサー として協賛し、ブースを出展したほか、 hsbt によるスポンサートークも行いました。 ブースでは、アンドパッドの「最近、何してる?」としてプロダクト本部・開発本部における AI 活用状況を紹介しました。 同じようにブース来場者の方にも同じアンケートを実施したところ、アンドパッドの活用状況と、ほぼ同じような傾向が見られたことが発見でした。 現在の業務で AI に渡したいと感じる負担が大きい作業はどれですか? という質問の回答として、既存コード(他人が書いたコード)の読解・調査 が一番でした スポンサートークでは hsbt が主にアンドパッドの会社紹介・業界説明をしました。 エンタープライズなお客様での利用が増えていることに驚かれたり、福岡にアンドパッドの支社があるのは知らなかったなど、いい反響をいただきました。 興味を持たれた方はぜひアンドパッド福岡オフィスでお会いしましょう ! また、トークでは こしば家 による「ふつうの Rubyist、ちいさなデバイス、大きな一年」では Tiny デバイスを使ったデモを披露されるなど、とても賑やかなトークがあったほか、「Rubyist としてアメリカで 10 年戦ったらどうなったか」では登壇された Jugyo さんが、長年勤めた会社をレイオフされても Ruby/Rails を使っていたことで新しい会社に転職できた、という、いいお話を披露されるなど、テックトーク / ソフトトークどちらも織り交ぜたタイムテーブルで、とても楽しかったです ! 参加された皆さま、登壇された皆さま、そして 福岡Rubyist会議05 Team の皆さま、ありがとうございました !! まとめ 採用広報から 3 月に開催されるイベント予告と、 2 月の開催レポートをお届けしました。 またイベントやカンファレンスでお会いできることを楽しみにしています ! また、アンドパッドでは技術コミュニティが大好きな採用広報を大歓迎しています。 広報の経験がない方でも Welcome です! カジュアル面談やご応募、お待ちしております。 hrmos.co