はじめに
Sansan技術本部Data Intelligence Engineering Unit Master Dataグループで1カ月間インターンを行った山口湊士です。
僕が参加したMaster Dataグループでは、既存の名寄せシステムに変わる新しい名寄せシステムを開発しています。新しい名寄せシステムは、事業所・本社・親会社といった組織構造に加え、業務連携や合併といった時間軸上の変化まで企業の情報を一元的に名寄せ・管理することを目的としたプロダクトです。このシステムは、SansanやEightなどの各種アプリで利用される基盤となるため、非常に重要なシステムです。
今回のインターンでは、この名寄せシステムの一部分のフォルダー構成、使用フレームワークの選定、デモアプリを作成し、ユーザーとサーバー間の通信、DBとの接続確認、CI/CDのセットアップ、さらには、OpenTelemetryを使用したtrace, logの収集まで幅広く担当しました。本記事では、その過程を通じて得られた知見を紹介します。
背景
このシステムは既存名寄せシステムを置き換える基幹サービスであるため、1日約4000万リクエストを処理することが想定されています。現在は要件定義の段階ですが、将来の大規模開発に備えて初期セットアップを行う必要がありました。僕がインターンで担当したのは、まさにこの初期セットアップです。
また、名寄せシステムはSansanやEightなどの複数のプロダクトで利用されるため、開発初期から障害発生時にどう検知・対応するかを考慮することが欠かせませんでした。デプロイ先は、Sansanのコンテナ基盤 Orbit に決定していました。
フレームワーク選定
使用言語はGoであると元々決められていたので、フレームワークを使用するのかしないのか、使用するのであれば、どのフレームワークを使用するのかを決定する必要がありました。結果として、フレームワークは使用せず、net/httpを使用することに決めました。 選定理由は、ginやechoのようなフレームワークを使用すれば、ロギングや認証・認可のミドルウェアを容易に実装できるが、開発の初期段階で本当に必要か不明確だったから、そしてプロジェクト初期はスモールスタートを優先したからです。
manifestファイルとフォルダー構成
orbit用のmanifestファイルはアプリと同じレポジトリで管理することにしました。ArgoCDのベストプラクティスでは、アプリケーションのソースコードとKubernetesのmanifestファイルを分けて管理することが推奨されているのですが、今回は、manifestファイルがKubernetesのmanifestファイルではなく、orbitの設定manifestファイルであり、環境ごとにファイルが1つであったため、同じレポジトリに置くことにしました。 また、manifestフォルダー以下のファイル構成については次のようにしました。理由としては、manifestフォルダーを作成することでリポジトリ内にmanifestファイルを分散させることを避け、manifestフォルダー内にアプリ用のフォルダーを作成することで、認知的負荷を下げるためです。
identification/manifest/
├── App-A/
│ ├── dev.yaml
│ ├── prod.yaml
│ └── stg.yaml
└── App-B/
├── dev.yaml
├── prod.yaml
└── stg.yaml
CI/CDの構築と動作確認
Github Actionsからworkflow_dispatchトリガーを利用し、ボタン1つでデプロイ可能な仕組みを構築しました。そして、デプロイ後、GraphQL Playgroundを通じてDBとの接続確認まで行いました。
trace, logの収集
システムの性質上、障害発生時に迅速な切り分けが求められます。主に以下2点を想定しました。
- 遅延の発生箇所を即座に特定すること
- 誤データ発生時に、DBに保存されたデータが誤っているのか、アプリサーバーでの整形作業が誤ったデータを作成しているのかを特定できるようにする
これを実現するため、trace, logを紐づけることが容易であるOpenTelemetryを使用することにしました。以下、簡単にOpenTelemetryについて説明します。
OpenTelemetryについて
OpenTelemetryとはほんの少しの設定をするだけで、トレース、メトリクス、ログを統合的に収集できるツールです。 traceを計測するための設定は、次のように簡単に設定可能です。
func newTracerProvider(ctx context.Context, res *resource.Resource) (*trace.TracerProvider, error) { client := otlptracehttp.NewClient( otlptracehttp.WithCompression(otlptracehttp.GzipCompression), ) traceExporter, err := otlptrace.New(ctx, client) if err != nil { return nil, err } tracerProvider := trace.NewTracerProvider( trace.WithBatcher(traceExporter, trace.WithBatchTimeout(time.Second)), trace.WithResource(res), ) return tracerProvider, nil }
また、関数ごとの実行時間の計測といった細かい設定のために、手動で計装するツールも用意されていて、以下のspanを関数内に記述するだけで計測が可能です。
tracer := otel.Tracer("company-usecase") ctx, span := tracer.Start(ctx, "create_company_usecase") defer span.End()
spanとは、1つの処理のまとまりを表す言葉であり、次の写真のようにspanを切ることで、各serviceや関数の実行時間を計測できます。

1に対する障害対応のフローとしては、各関数にspanを追加することで対応しました。 具体的には、
- ユーザーからのリクエストの変換(本アプリにおけるGraphQL)
- ビジネスロジックの処理(企業データの整形作業)
- DBとのやりとり
- DB内の処理
に各spanを配置することで対応いたしました。結果として、各関数の実行時間を次の画像のように視覚的に確認することができました!

また、2に対するフローとしては、log, traceの紐付けを行うことで対応しました。 実際に関数内に、log, traceを記述することで、次の画像の通り、traceに対して、logが紐づけられていることが確認できました!

今後の展望
現状、全てのリクエストでトレース計測とログ出力を行っています。しかし、この方式を本番環境で運用すると、以下の2点が大きな問題となります。第一に、traceとlogが膨大な量となり、Storageの保存コストが著しく増大します。 第二に、trace・logの出力処理自体がサーバー負荷となり、レスポンス速度の低下を招きます。これらの課題を解決するため、本番環境ではサンプリングやログ選定(例:エラー発生時のみ詳細ログを記録するなど)を行い、効率的な収集方法を設計する必要があります。
まとめ
本インターンでは、技術選定から、CI/CDの構築、監視基盤の整備まで本当にさまざまなことを経験させていただきました。 特に意思決定とその影響、また、別の選択肢を取らなかった理由についてADRにまとめることで、後から参加したメンバーが非常にキャッチアップしやすい点が印象的でした。
インターン期間中はメンターの方をはじめとして多くの方にお世話になり、感謝しかありません。本当にありがとうございました。