API GatewayによるMicroservices化
mercari.go#1
3 July 2018
Taichi Nakashima
Taichi Nakashima
遊びに来てくれ!
Mercari Microservices化のために開発したAPI Gatewayについて紹介する.
Monolithアーキテクチャにより高速なサービス開発をしてきた一方でコードベースの巨大化により以下が問題になってきた
今後さらなるサービスの拡大によって組織が成長しても「開発スピードを落とさない・むしろ上げる!」「個のパフォーマンスを最大限に高める」ためにMicroservicesアーキテクチャへの移行を始めた
MicroservicesアーキテクチャにおけるAPI Gatewayの役割
(*)共通処理
MonolithへのリクエストをProxyし段階的なサービス分割を補助する.
MercariのAPI gatewayが満たすべきこと
以下の利用を検討した
内製することに!
Microservicesの基盤はCloud(GCP)上に構築してる
API GatewayもGKE上で動かしている
現在は以下を担う部分として利用している
将来的には以下での利用を検討している
Why? Goは今後Mercariのメインの言語になっていく.Goさえ知っていれば誰でもAPI gatewayを拡張できるようにしたい.
以下の機能を実装
Service mesh(Istio)の将来的な導入を考慮しNgnixなどの依存は極力減らした.
「Goさえ知っていれば誰でもAPI Gatewayを拡張できる」
なるべく標準ライブラリや標準の作法を組み合わせて実装する.例えばサーバー実装は`net/http`のみを使う,テストは`testing`でTable drivenを使う,Middlewareパターン(後述)を使うなど.
某有名なスーパーGoハッカーの声
Core packageとそれを使った具体的な実装に分離した.
他のRegionや他サービスごとの実装を可能にする.
実装の責任範囲を明確に分離する.
Core packageはSREが,それを使った実装はDeveloperが責任をもつ.
API GatewayにBusiness logicを実装できると第2のMonolithになりかねない.
Core packageを使った実装には最小限のことしかできないようにしている.
API Gatewayでの共通処理は全てMiddleware(Adapter)で実装している.
e.g., アクセスログを記録するMiddleware
func withLog(logger *zap.Logger) adapter { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { d := newDelegator(w) next.ServeHTTP(d, r) logger.Info("request", zap.String("host", r.Host), zap.String("path", r.URL.Path), zap.Int("status", d.status), zap.String("request_id", requestID(r.Context())), ) }) } }
Middlewareを書くだけでどんどん機能を拡張していける.
Coreは以下のようなMiddlewareをもつ
withAuth
- 内製のAuthorityサービスによるAuthN/AuthZwithRequestBuffering
- リクエストのBufferingwithLog
- アクセスログの記録 (Observability)withDDInstruments
- Datadogへのメトリクスの送信 (Observability)withDDTrace
- Datadogによる分散TracingのSpanの生成 (Observability)withRecover
- panicのrecover+Sentryへの通知Regionやサービス特有の機能を実装して使うこともできるようにしている.
以下の代表的な機能の実装の詳細を紹介する
やりたいこと
API Gatewayがprotocolの変換(HTTP to gRPC)を担う
なぜClientでProtocol buffer(vs. JSON)?
なぜインターネット上はover HTTP?
なぜDC内部はgRPC(vs. REST)?
開発者がやること
サービスのインターフェースをProtocol bufferで定義する(e.g., Echo service)
service Echo { rpc Say(SayRequest) returns (SayResponse) {}; } message SayRequest { string message_body = 1; } message SayResponse { string message_body = 1; }
Protoの定義から各言語のClientとServerの実装を生成する.
エンドポイントの定義をAPI Gatewayの実装に追加する(e.g., Echo service)
{ Name: "mercari.platform.echo.v1.Echo", Endpoint: regionConfig.EchoServiceEndpoint, MethodMap: map[string]*gateway.GRPCMethod{ "/services/echo/say": &gateway.GRPCMethod{ // エンドポイント(Path) Name: "Say", // gRPCメソッドの名前 RequestMessage: &echo_pb.SayRequest{}, // リクエストの型 ResponseMessage: &echo_pb.SayRequest{}, // レスポンスの型 }, }, }
以下のことができるようになる
API Gateway(core package)は何をしているか?
開発者によるエンドポイント定義を基に`http.Handler`を生成する.
(省略版)
func GRPCHandler(name string, conn *grpc.ClientConn, reqMsg, respMsg proto.Message) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { unmarshalRequest(r, reqMsg) // リクエストを与えられたリクエスト型にUnmarshalする conn.Invoke(r.Context(), name, reqMsg, respMsg) // gRPCリクエストを実行する buf, _ := proto.Marshal(respMsg) // レスポンスをProtobufferにMarshalする w.Write(buf) // HTTPレスポンスを書き込む }) }
他にも以下の機能をもつ
Why?: Clientからのインターネット越しのリクエストは通信環境によって遅くなることは十分に起こりうる
API gatewayにRequest bufferingを持たせることで
GLBはRequest bufferingをしないので自分で実装する必要がある...!
vulcand/oxyを利用した.
`vulcand/oxy`はRequest bufferingだけでなくRate limitなど標準パッケージが提供していない便利機能を提供している.
GoのReverseProxyとうまく動かない(ReverseProxyがTransfer-EncodingとContent-Lengthヘッダーを消すのでoxyがResponse bodyを読めない...)+Package内でのObservabilityを高めるためにForkして実装した.
動作
(*) GKEにSSDを準備する必要があった
(Clientの変更を避けるため)DNSの切り替えによりClientから直接リクエストをMonolithに投げていたのをAPI Gateway(on GKE)経由に移行した.
リクエスト経路が変わるだけだが大きな変更(Gatewayにバグがあるかもしれない)
お客様への影響を最小限に抑えるために段階的なリリースを行った.
APIのドメインはAWSのRoute53とroadworkerにより管理している.
Route53のWeighted Recordsの機能を使いDNSレコードの返答に「MonolithのIP」と「API gatewayのIP」でそれぞれ重みをつけるようにしその重みを変更することで徐々にリクエストの流し先を変更した.
(例)roadworkerの設定.「MonolithのIP」: 「API gatewayのIP」= 255:1
rrset "api.example.com", "A" do set_identifier "Monolith" weight 255 resource_records( "x.x.x.x", ... ) end rrset "api.example.com", "A" do set_identifier "Gateway" weight 1 resource_records( "y.y.y.y", ... ) end
パフォーマンス修正をしながら約1ヶ月以上かけてMigrationを完了し現在は100%のリクエストがAPI Gateway経由になっている.
+ 先月よりGateway配下で新規のMicroservicesも動き始めている!
"Readable means reliable" (Rob Pike)だが...
Mercariの全リクエスト(ピーク時約56,000req/sec)を受けるためパフォーマンスには十分に注意を払う必要がある.
パフォーマンス・チューニングで最も大切なのは「可視化」
API Gatewayでは以下を行っている
さらに「可視化」してパフォーマンス・チューニングするにはProfilingが必要
Goは標準でpprofによるCPUやMemory UsageのProfilingの仕組みが提供されている.
が自分で「適切な負荷」を発生させる必要がある...
Googleは「本番環境でProfiler」を動かしている(Google-Wide Profiling: A Continuous Profiling Infrastructure for Data Centers).つまり自分で負荷を発生させるのではなくユーザが実際に利用するエンドポイントでProfilingを行っている!
Stackdriver Profilerは以下のようにFramegraphを使いCPUレベルでどのFunctionにどれだけ時間がかかってるかを可視化できる!(+統計的な手法によりProfilingによるパフォーマンスの劣化も防いでいる)
API gatewayではStackdriver Profilerを使い本番でProfilerを動かしCPUレベルでパフォーマンスの劣化問題を発見できるようにしている!
API gatewayとは何か?とその実装思想と詳細,移行方法について紹介した.
Building a proxy server in Golang
type Filter interface { Name() string } type BeforeFilter interface { Filter BlacklistedHeaders() []string DoBefore(context context.Context) BeforeFilterError } type AfterFilter interface { Filter DoAfter(context context.Context) AfterFilterError }
Taichi Nakashima