チームでは「DMMポイントクラブ」と「通知配信機能」を運用

佐々木勝春氏:本日は「Goを活用したサービス開発・運用の話」という内容で、お話ししたいと思います。はじめに自己紹介をします。名前は佐々木と申します。経歴としては、2017年4月に一般社団法人DMMアカデミーに入社して2年間活動したのちに、合同会社DMM.comに転籍しました。

以降は、プラットフォーム事業本部のエンジニアとして、DMMサイト内のレビューサービスのレビューリプレイスであったり、このあとお話しします、お知らせ機能の開発や、「DMMポイントクラブ」というスマートフォン向けのアプリのサーバーサイドの開発をしてきました。

今回は、これら複数のサービスの開発と、安定的なサービス運用を行うために実施しているモニタリングについてお話ししたいと思います。

これらの話の前提として、まず私たちのチームが現在運用しているサービスについて紹介したいと思います。

1つ目は、iOSとAndroid向けのスマートフォンアプリの「DMMポイントクラブ」です。DMMポイントをメインコンテンツとして、くじ機能やポイント管理の機能、アプリ内のDMMポイントのチャージと連動したお得なキャンペーンの実施などを行っています。

2つ目が、「通知配信機能」です。以前DMMサイト内には、ユーザー向けに定期的にお知らせを配信する機能はなかったのですが、今回、新たにそのためのページとサーバーサイドの開発を行いました。また、管理機能として、通知の内容を登録する処理も実装しています。

私たちは、これらのサービスを提供していくにあたって、サービスの開発とそれにあたる運用業務はもちろんですが、ここに書いてあるように、それ以外にもさまざまな業務を行っています。

これらの多様な業務を行っていくにあたって、開発の工数をなるべく効率化していきたい、複数のサービスを安定的に運用していくために、モニタリングをしっかりと行っていきたいという思いがありました。

デプロイや共通モジュールの利用の観点で「monorepo」を採用

これらを踏まえて、今回話す内容の1点目が、monorepo(モノレポ)と共通アーキテクチャを使った複数サービスの開発です。その前提として、インフラアーキテクチャを最初に見ていきます。インフラの基盤として、オンプレミスではなく、AWSのクラウドサービスを活用しています。

APIのサーバーは、このような構成になっていて、Amazon ECSの「Fargate」を使っています。ポイントクラブと通知配信も、構成は同じになっています。API以外のインフラの仕組みですが、画像処理の部分でGCPのサービスを使っていて、キューの処理で「Amazon SQS」、バッチの段階的な実施のために「Step Functions」などを使っています。

先ほどのインフラ構成を踏まえて、ポイントクラブが提供している機能群を見ていきます。クライアント向けのサービスを提供するための、APIのエンドポイントとユーザーセグメントごとのお知らせ登録や、Push通知配信などの管理機能の2点です。

同じく通知配信に関しても、クライアント向けサービスのためのAPIと、管理機能の2点を予定していました。

これらの実装は、APIの開発を先行して、あとから管理機能を開発する際にAPIで開発したリソースを有効活用して、工数を減らせたらいいなと考えていました。

その中で同じチームのメンバーが、monorepoを提案してくれました。monorepoに関しては、「DMM.go」の第1回で、前CTOの松本さんがお話ししてくれて、その資料があるので割愛します。そこで言われているメリットのうち、今回はデプロイや共通モジュールの利用といった観点で、monorepoを採用しました。

プラスアルファとして、インフラ面でも私たちは共通化を図っています。バッチは、先ほどお話ししたとおり、APIサーバーはAmazon ECSのFargateで稼働しているのですが、バッチも同じくECSタスクを活用することで、CFn(CloudFormation)のタスク的なものを有効活用しています。

バッチ実装には「cobra」を採用

次に、実際のソースコードのアーキテクチャについて見ていきます。アーキテクチャとしては、レイヤードとDDD(ドメイン駆動設計)とCQRSを採用しています。クエリ層におけるデータ層へのアクセスと並行して、DMM内部の外部APIにアクセスして、データを取ってくる処理も行っています。

このアーキテクチャをポイントクラブと通知配信の両サービスで採用することで、学習コストを抑えて、仕様さえ把握すればすぐに開発に着手できる状況になっています。

次に、具体的なディレクトリ構成について見ていきます。ルートディレクトリ配下にappディレクトリを置いて、その配下に、先ほどのアーキテクチャで説明したui層、usecase層、domain層、infra層の各ディレクトリを置いています。

さらにui層配下には、バッチ用のディレクトリや、API用のhttpディレクトリを置いています。またmain関数は、ルートディレクトリ直下のコマンドディレクトリにおいて、apiとbatchのディレクトリを分けて、それぞれに対して置いています。

バッチの実装は、今回CLI(コマンドラインインターフェイス)で提供しているのですが、そのCLIの実装には「cobra」というツールを使いました。

このツールは、サブコマンド型のCLIツール作成に特化していて、POSIX標準のフラグ機能であったり、CLIアプリケーション作成のためのひな型を作る機能であったり、多様な機能が特徴的です。

そもそも標準ライブラリで実装するという選択肢もあったのですが、開発工数を削減しようという理由があり、今回は「cobra」を採用しました。さらに、cobraはサブコマンドのCLIツールの作成に特化しているので、今回の要件にも合っていました。

また、今後の拡張性や、cobra自体の採用実績なども見て、今回はcobraを採用することにしました。

CLIの具体的な実装

次に具体的な実装を見ていきます。main関数内で、このようにcobraのmainコマンドに対して、サブコマンドの追加とCLIの実行時のエラーハンドリングをする処理を書いています。

こちらが「app/ui/batch/root.go」ファイルで、先ほど紹介したルートコマンドを初期化している部分になります。

こちらが先ほど紹介した、app/ui/batch配下のファイルです。apiと同じ構成で、CLI用の処理をハンドラーとして定義していて、この赤線部分が、あとで紹介するサブコマンドを実行する処理になっています。そのほかに、ハンドラーを生成するnew関数などをここで定義しています。

先ほどのファイルの続きですが、こちらでサブコマンドを生成する関数を定義していて、こちらのcobraのコマンドの構造体のランエグゼ(RunE)というフィールドで、ハンドラーのインターフェイスで定義した関数を呼ぶように渡しています。

こちらはさらに先ほどのファイルの続きですが、ハンドラーのインターフェイスを満たしたメソッドを実際に定義したものです。赤線部分で、usecase層において定義したメソッドを呼ぶようにしています。usecase層以降は省略しますが、このようにハンドラーのインターフェイスを定義してメソッドを実装するような方針になっていて、apiと同じ構成を取っています。

以上で、CLIの作成についての説明を終わりますが、そのほかAPIの開発に関しても、フレームワークを活用することで開発コストの削減を行ってきました。

各サービスで活用しているフレームワーク

ポイントクラブは「Goa」というフレームワークを活用しています。選定理由としては、APIの仕様書と実際のコードの状態を乖離させないために、Goaを活用しています。こちらにリンクを貼っているので、よければ参照してください。

また、通知配信は「Echo」というフレームワークを採用しています。以前レビューサービスのリプレイスをする際に、Echoを使っていて採用実績があったこと、今回はポイントクラブよりも小規模なサービスだったので、Echoなどの薄いフレームワークがいいだろうと話し合いがあったこと、さらに、APIの仕様書に関して、今回の通知配信のAPIを使うユーザーは、ほぼ自分たちだけだったので、問題ないだろうという理由でEchoを採用しました。

これまでのまとめですが、monorepo、共通アーキテクチャ、ライブラリ、フレームワークを使って、私たちは複数サービスの運営を効率的に行っていきました。

モニタリングはAPM・Profiler機能が使いやすい「Datadog」を活用

次に、これらの安定的なサービス運用のために行っているモニタリングについて、お話ししたいと思います。

安定的なサービスを運用するにあたって、モニタリングは必須だと思いますが、それに加えて、インフラのリソースの最適化という観点で、モニタリングを導入しようという思いがありました。

こちらが、実際にモニタリングを導入したことによる効果の事例です。例えばAPIサーバーでメモリリークしているのに気づけたり、実際のAPIサーバーへの負荷状況とサーバーのリソースの使用状況を考慮して、コンテナ数を減らしてインフラ費用を抑えたりできました。

私たちは、モニタリングを行うにあたって、SaaS型のサービスである「Datadog」を活用しています。ここに書いてあるように、APM(Application Performance Management)やProfiler(性能解析)やネットワークモニタリングといった機能や、Syntheticモニタリングといって、APIのhealthチェックや、フロントエンド側のエンドツーエンドを行うための機能など、さまざまな機能を提供しています。DMMのプラットフォーム事業部としても、標準のモニタリングツールとしてDatadogを活用しています。

対応状況なのですが、Go以外の多様な言語に対応していて、ドキュメントも充実していて、機能も頻繁にアップデートしているので、とても便利で使いやすい内容になっています。これらのうち、今回はアプリケーションレイヤーに関係のある、APMとProfilingについて見ていきたいと思います。

まずAPMについてです。このあと詳細な実装を見ていきますが、こちらがAPMのUI画面になります。リクエストの開始から終了までのトレース内容と、自分たちでトレースしたい関数にトレース用の処理を埋め込むことで、このように部分的にメソッドのトレース状況を確認できます。

こちらが実際の、トレースを開始するためのコードになっています。main関数内でトレースの開始と終了を呼ぶだけになっています。こちらが呼んでいる関数で、Datadogのライブラリを利用して、タグなどの設定を行っています。

さらに、トレースを行いたい任意の関数に、このようにトレースの開始と終了処理を呼ぶよう実装しています。こちらが先ほどのトレース開始用の処理なのですが、こちらもDatadogのライブラリをコールして実装する内容になっています。

以上でAPMについての説明は終わりで、続いてProfilingについて見ていきます。

Profilingの特徴ですが、本番環境において、最小限の負荷で、外部ライブラリも含めたコードレベルでパフォーマンスを詳細に分析できます。

Goでは、標準ライブラリとして「pprof」などのライブラリもありますが、Datadogを活用すると、これらのProfilingをAPIのリリースバージョンごとの比較したり、Datadogのほかのサービスで取得したメトリクスの相関関係を見たりできる特徴があります。

こちらが実際のコードの内容です。main関数内で、DatadogのProfilingを開始する処理を呼ぶだけとなっています。必要に応じて、タグや、Profilingする項目の設定などを行っています。

こちらが、実際のProfiling結果を表示したDatadogのUI画面になります。このように、処理の開始から終了までが上から下に並んでいて、フレームの横幅のサイズが処理の大きさになっています。

例えばCPUのタイムを表示する場合であれば、このフレームの横幅が大きいほどCPUの消費時間が大きいということになります。その部分を優先的に修正していけば、ボトルネックを改善できるので、パフォーマンスが改善されます。

Profilingとメトリクスの比較になりますが、Profilingを取得できる項目が限定されているのですが、スタックトレースとしてランタイムレベルで詳細に、処理の実行順に逐次確認できるので、ボトルネックを比較的確認しやすいのが特徴となっています。

以上で、アプリケーションレイヤーに関連する説明は終わりになりますが、Datadogに関しては、そのほかにもAWSやGCPなどの外部サービスと連携するインテグレーション機能や、それらのモニタリング結果を確認するためのダッシュボードなどが提供されています。

これらのまとめですが、継続的なモニタリングでDatadogを活用することで、パフォーマンスの確認などを行ってきました。現在もパフォーマンスで一部問題があるので、引き続き改善していきたいと思っています。

全体的な話のまとめになります。私たちのチームでは、monorepoと共通のアーキテクチャを使ったり、外部ライブラリや必要に応じてフレームワークなどを活用することにより、開発の効率化をしてきました。また、継続的なサービス運用のためにモニタリングを活用して、リソースの監視や、インフラサービスのコストの削減化などを行っています。

今後もよりよいサービスを提供していけるように、日々改善しながらやっていきたいと思っています。本日の発表は以上になります。ありがとうございました。