BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル API GatewayサービスをClojureからGo言語に書き直す - AppsFlyerによる実例報告

API GatewayサービスをClojureからGo言語に書き直す - AppsFlyerによる実例報告

原文(投稿日:2019/01/31)へのリンク

モバイルアトリビューションおよびマーケティング分析プラットフォームのリーダであるAppsFlyerでは,1日に700億近いHTTPリクエスト(毎分約5,000万)の処理に,マイクロサービスアーキテクチャ方式を採用しています。すべてのフロントエンドサービスをラップし、システムへのエントリポイントとなるのは,API Gatewayと呼ばれるミッションクリティカルな(非マイクロ)サービスです。このサービスは事実上,ユーザからバックエンドサービスへのトラフィックをルーティングする単一点として機能しており,クライアントに対する認証と承認処理を単純化する一方で,単一障害点(a single point of failure)となる可能性も持っています。

この記事では,同社のエンジニアリングチームがClojureベースのAPIゲートウェイ実装からGoベースの実装に移行した理由と,その方法について説明します。

API Gatewayにおける技術的負債の蓄積

私たちのAPI Gatewayで起きたような技術的負債がいかにして生まれるのか,いかに多く生まれているのか、については,以前にもお話しています。

当初のAppsFlyerのサービスはPythonによるモノリスであったため,そのモノリス自身の一部として,認証および承認の単一ソリューションが必要でした。時が経つにつれてトラフィックが増加し、複雑性も増したことから,私たちはマイクロサービスアーキレクチャへの移行を実施しました。そのため,認証および承認プロバイダとして機能する,統一的なAPIゲートウェイソリューションの開発が必要になったのです。

私たちは腕まくりをして,これをClojureで書き始めました。設計フェーズは省略し,ほぼ概念実証のモードでサービスを構築したのです。私たちの会社は,EMEA最大のClojureショップのひとつなので,目前のプロジェクトに対しては,十分な検討をすることなく、Clojureが既定の選択言語となっている場合が非常に多くあります。開発速度の面や"仕事を完遂する"上では適切なのですが,プロジェクトの長期的なメンテナンスという点では,これは理想的とは言えません。トラフィックが増加すると間もなく,新たにロールアウトされるAPIゲートウェイのコードが複雑過ぎるようになり,必要なスループットを得るために毎回リファクタリングが必要な状況に陥ったのです。

最終的にサービスの不安定さが限界を越えたことから,Clojure(今度はもっとよい設計で)にせよ,あるいは他の選択肢を検討するにせよ,プロジェクトを完全に書き直す必要性があると認識するに至りました。今回のイテレーションでは,自らの認知バイアスは受け入れず,安息の地であるClojureに戻らないことを決めていました。すでに存在するサービスに手を入れるのではなく,私たちが必要とするサービスを開発するための設計作業を行うことにしたのです。

最終的に今回のAPI Gatewayサービスでは,Clojureとベンチマークを行う言語としてGo言語を選択しました。この決定は同時に,言語面における多様性を実現するとともに,新しい構文を習得することで私たちのコードクラフトマンシップ精神にもプラスになりました。

同時に私たちは,スタックに新たなプログラミング言語を追加することの裏側について理解しました。私たちはCI/CDの考え方を強く支持していたため,(Clojureのような)JVMベースではない新言語を導入するには運用面でのコストが必要だったのです。ただしこの問題は、短い期間で解決することができました。

新しい言語のマスタには学習曲線も当然ありましたし、長期使用に耐えるコード品質や堅牢性も必要でした。こういった点は、最初のプロジェクトで実際に言語を使ってコードを書き、運用時のパフォーマンスを確認するまでは分からないものです。

今回のサービスでGo言語を選択した理由について、いくつかの面から簡単に説明しておきましょう。手続き型の非同期言語であるGoは、私たちがすでに社内で使用していた関数型プログラムと同時に、オブジェクト指向の機能、スケーラビリティの向上を実現してくれました。型付けされた言語であるという事実は、メンテナンス性の改善と、車輪を再発明する必要なく前進することを可能にしてくれます。特に,私たちが書き換えを予定していたサービスには、実戦で証明済みのリバースプロキシ組み込みで備えている、というアドバンテージがありました。同期型言語としてマルチスレッドや並列性に強みを持つClojureには、このサービスではI/Oオーバヘッドが非常に大きいというデメリットもあったのです。

選択肢の評価

さまざまな言語の適合性を適切に評価するためには、いくつかの側面(パフォーマンスに加えて、実施すべきタスクに対してそれぞれの言語が持つメリット)を評価する必要があることを理解しました。パフォーマンスを測定するためには、実運用のシミュレーションに可能な限り近いベンチマークで、ClojureとGo言語をっ比較する必要があることも分かりました。

そのため、Go言語とClosureに加えて、オプションとしてNGINX(Luaによって拡張されたもの)を使用したストレステストを最初に実施しました。その結果、Go言語のスループットがClosureよりも勝っていたのです。

テストの基本的な統計は以下のとおりです。

  • ベンチマークツールとしてWRKを使用
  • 3分間バースト
  • 64スレッド
  • 1000接続プール
  • 2分の要求タイムアウト
  • リクエスト毎に500kbの静的ファイルを返送
  • ネットワークノイズを軽減するため、すべてのトラフィックは、c4 xlargeインスタンスを使用して同じAZから送信する

プロキシソリューション

要求/秒

トランザクション/秒

総要求数

総トランザクションサイズ

不正要求

平均待ち時間

Direct

190

72 MB

34500

12.8 GB

~ 400 (drop:200)

4.41 Sec

NGINX

185

73 MB

33486

12.7 GB

~ 300 (drop:37)

7.95 Sec

Clojure (basic Http-Kit implementation)

190

72 MB

34412

12.8 GB

~ 100 (drop:600)

8.48 Sec

Golang (native reverse proxy & http layer)

185

73 MB

33443

12.7 GB

~ 200 (drop: 0)

5.42 Sec


これらの結果に加えて,Go言語には、構文の観点からは更新やイテレーションの容易な型付き言語であるということや、既存のパッケージやライブラリを使って(最初から記述することなく)容易に拡張できるというメリットがありました。さらに機能の観点から、リバースプロキシコンポーネントが言語コアで組み込みサポートされているという点も、重要な利点になりました。

古いコードを複製するようなやり方を回避するためにも、私たちは、Clojureでサービスを書き直すという安易な方法ではなく、型付きの考え方を実践することにしました。

設計フェーズはまず、サービスに必要とする機能の概要をまとめることから始めました。その上で基本概念を確認し、プロダクションユーザを新たなサービスに移行する上で必要な下位互換性や潜在的な落とし穴について検討しました。すべてのベースをカバーできたことを確認した後、アーキテクトと開発者をプロジェクトにアサインして、作業を開始したのです。

コンセプトからデリバリへ

プロジェクトのコーディング部分が非常に早く、わずか2ヶ月程で完了したことは驚きでした。社内でGoを導入したのは初めてのことだったので、プロジェクトのコーディング部分には細心の注意を払いました。各機能について2回のイテレーションを実行して正しい作業ができていることを確認し、コードレビューを何回も実施しました。今後実施されるGoプロジェクトのソースとして利用するためには、コードが正確かつクリーンであることが必要だと分かっていたからです。

Goを導入したプロジェクトは今回が初めてだったのですが,言語とそのコア機能を十分に理解することができました。というのも,Redis(DDoSやbotsを防ぐために,ユーザのログインカウンタ状態を保持する)やKafka(ログインの成功ないし失敗を含む,ドメインイベントのCQRSを管理する)といったスタックの追加部分と通信するために,Clojureで使用されているライブラリを補う必要があったためです。ClojureはアプリケーションをJavaバイトコードにコンパイルして,マシンに予めデプロイしたJVMを実行することを前提とした言語であるのに対して,Goは静的にコンパイルされる言語であり,マシン上で動作するためにVMを必要としません。

CPUやメモリ使用量といったメトリクス,ビジネスロジックカウンタといった,すぐに使用可能な機能を実現するためには,スタック全体をスクラッチから書き直す必要があったのですが,この作業を通じて,Go言語の内情をより早く,深く知ることができました。

基本的な機能を一通りテストして,約2ヶ月後には基本的なマイグレーションの準備が整ったので,親グループ(ドメイングループ)内の管理下において,新たなAPI Gatewayへのサービス移行のイテレーションを,基本的にカナリアリリースとして開始しました。

最初の数週間で,いくつかのサービスを管理しながらロールアウトすることにしました。運用時にバグや欠陥を発見した場合には,それらを修正した上でサービス全体をロースアウトできるようにするためです。最初のAPIソリューションがあまりにも拙速で,結果的に低品質なものを提供することになった失敗を教訓にしたいとも思っていました。

準備万端となり,すべての問題が解決したと思えた時点で,サービス全体の移行プランに着手しました。この中には,あらなたサービスに移行するための正確な手順,移行によるメリット,特定のスタックと依存関係に基づいて移行を行うための最善の方法などを記載した,各サービス用の移行ガイドPDFも含まれていました。

新たなリバースプロキシの段階的なロールアウトにはアプリケーションロードバランサ(ALB)を使用して,新旧APIそれぞれのゲートウェイを介して公開したいサービスを事前に定義したURLセットに基づくトラッフィクのルーティングを行いました。

これによって,最小限の労力とリスクでトラフィックをルーティングするという,非常によく管理されたアプローチが可能になりました。その後は時間を掛けて,ユーザ向けサービスを担当する他のチームすべてと密接に協力しながら,移行した個々のサービスをテストしました。これには6ヶ月を要したのですが,40に及ぶマイクロサービスをダウンタイムゼロで,新APIゲートウェイに移行することができました。

結果

最終的には,Clojureコードを実行する25インスタンス(c4 xlarge)で60要求を同時処理可能であったものが,Goコードを実行する2インスタンス(c3.2xlarge)で毎分~5,000要求を並行サポート可能になるという,大幅な改善が実現できました。新たなアーキテクチャ設計は,手続的アプローチによって規模の拡大やビジネスの複雑化にも容易に対応可能であると同時に,大規模処理に対処するための新たな言語がレパートリに加わったという意味においても,当社の次フェースの成長にも十分耐え得るソリューションとなりました。

今回実現したリバースプロキシソリューションについて,ClojureとGoの場合での例を挙げてみましょう。

Clojureの場合:

;; Creating a connection manager


(let [cm (clj-http.conn-mgr/make-reusable-conn-manager {:timeout 1 :threads 20 :default-per-route 10})])


;; Creating a proxy server using cm (connection manager)
 (client/request {:method	:get
                           :url	(service/service-uri service-spec uri-match)
                           :headers	(dissoc (into {} (:headers req)) "content-length")
                           :body	(when-let [len (get-in req [:headers "content-length"])]
                                                     (bs/to-byte-array (:body req)))
                           :follow-redirects   false
                           :throw-exceptions   false
                           :connection-manager cm
                           :as	:stream}))

Go言語の場合:

func NewProxy(spec *serviceSpec.ServiceSpec, director func(*http.Request), respDirector func(*http.Response) error, dialTimeout, dialKAlive, transTLSHTimeout, transRHTimeout time.Duration) *MultiReverseProxy {
	return &MultiReverseProxy{
		proxy: &httputil.ReverseProxy{
			Director:       director, //Request director function
			ModifyResponse: respDirector,
			Transport: &http.Transport{
				Dial: (&net.Dialer{
					Timeout:   dialTimeout, //limits the time spent establishing a TCP connection (if a new one is needed).
					KeepAlive: dialKAlive,  //limits idle keep a live connection.
				}).Dial,
				TLSHandshakeTimeout:   transTLSHTimeout, //limits the time spent performing the TLS handshake.
				ResponseHeaderTimeout: transRHTimeout,   //limits the time spent reading the headers of the response.
			},
		},

Go言語版では,コネクションプール管理の改善とリバースプロキシ機能に向けた機能の多くが,コアクラスに組み込まれたものである点に注目してください。

まとめ

型付き言語で開発されたという事実によって,さまざまな機能のプラグインや,Go言語のライブラリサポートやコミュニティによる新たなテクノロジの導入が容易にできるようになりました。しかし,私たちにとって何よりも重要なのは,パフォーマンスとスループットの面で,ユーザによりよいエクスペリエンスを提供可能になったことです。新たにデプロイされたソリューションでは,現在よりも飛躍的に多くのトラフィックを処理可能です。10倍スケールで増加する当社のトラフィックやリクエストにおいて,将来的な意味からも,これは非常に重要なことなのです。

著者について

Asaf Yonay氏はAppsFlyerのR&Dグループマネージャで,管理上および技術上の問題を解決し,それを人的要素を含めた成功事例とすることに情熱を注いでいます。氏はまた,R&Dチームがそのスピードを失うことなく成長し拡大するためのプロセス定義,問題対処のための実践的なフルスタックアプローチといったものを信奉し,それがマネージャをリーダに進化させるものであると確信しています。氏は所属するスタートアップにおいて,サポートからQA,さまざまなR&D的役割, ClosureやGo言語,Node.js,Pythonによるスケーラブルで堅牢なシステムの構築,ReactやAngularサービスのパワーアップなど,多くの立場で活動する一方で,KafkaやAerospike,Neo4Jによる大規模ないし複雑なビジネスロジック処理にも携わっています。

この記事に星をつける

おすすめ度
スタイル

BT