API GatewayによるMicroservices化

mercari.go#1

3 July 2018

Taichi Nakashima

About me

Mercari Meetup for Microservices Platform 7/19

遊びに来てくれ!

https://mercari.connpass.com/event/92168/

tl;dr

Mercari Microservices化のために開発したAPI Gatewayについて紹介する.

Background for Microservices

Monolithアーキテクチャにより高速なサービス開発をしてきた一方でコードベースの巨大化により以下が問題になってきた

今後さらなるサービスの拡大によって組織が成長しても「開発スピードを落とさない・むしろ上げる!」「個のパフォーマンスを最大限に高める」ためにMicroservicesアーキテクチャへの移行を始めた

API gateway for Microservices?

API gateway for Microservices?

MicroservicesアーキテクチャにおけるAPI Gatewayの役割

(*)共通処理

API gateway for Microservices?

MonolithへのリクエストをProxyし段階的なサービス分割を補助する.

API gateway for *Mercari* Microservices?

MercariのAPI gatewayが満たすべきこと

VS.

以下の利用を検討した

VS.

内製することに!

Implementation

Infrastructure

Infrastructure

Microservicesの基盤はCloud(GCP)上に構築してる

API GatewayもGKE上で動かしている

詳しくはMicroservices on GKE at Mercari

Technical stack

Google Load Balancer (GLB)

現在は以下を担う部分として利用している

将来的には以下での利用を検討している

Go

Why? Goは今後Mercariのメインの言語になっていく.Goさえ知っていれば誰でもAPI gatewayを拡張できるようにしたい.

以下の機能を実装

Only Go

Service mesh(Istio)の将来的な導入を考慮しNgnixなどの依存は極力減らした.

Design

Gopher friendly

Gopher friendly

「Goさえ知っていれば誰でもAPI Gatewayを拡張できる」

なるべく標準ライブラリや標準の作法を組み合わせて実装する.例えばサーバー実装は`net/http`のみを使う,テストは`testing`でTable drivenを使う,Middlewareパターン(後述)を使うなど.

Gopher friendly

某有名なスーパーGoハッカーの声

Core as a package

Core as a package

Core packageとそれを使った具体的な実装に分離した.

Core as a package

他のRegionや他サービスごとの実装を可能にする.

Core as a package

実装の責任範囲を明確に分離する.

Core packageはSREが,それを使った実装はDeveloperが責任をもつ.

Excluding business logic

Excluding business logic

API GatewayにBusiness logicを実装できると第2のMonolithになりかねない.

Core packageを使った実装には最小限のことしかできないようにしている.

Middleware driven

Middleware driven

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を書くだけでどんどん機能を拡張していける.

Middleware driven

Coreは以下のようなMiddlewareをもつ

Regionやサービス特有の機能を実装して使うこともできるようにしている.

Implementation details

Implementation details

以下の代表的な機能の実装の詳細を紹介する

Protocol transformation

Protocol transformation

やりたいこと

API Gatewayがprotocolの変換(HTTP to gRPC)を担う

Protocol transformation

なぜClientでProtocol buffer(vs. JSON)?

なぜインターネット上はover HTTP?

なぜDC内部はgRPC(vs. REST)?

Protocol transformation

開発者がやること

Protocol transformation

サービスのインターフェースを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の実装を生成する.

Protocol transformation

エンドポイントの定義を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{}, // レスポンスの型
        },
    },
}

以下のことができるようになる

Protocol transformation

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レスポンスを書き込む
    })
}

Protocol transformation

他にも以下の機能をもつ

Request buffering

Request buffering

Why?: Clientからのインターネット越しのリクエストは通信環境によって遅くなることは十分に起こりうる

API gatewayにRequest bufferingを持たせることで

GLBはRequest bufferingをしないので自分で実装する必要がある...!

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を準備する必要があった

Migration

Migration

(Clientの変更を避けるため)DNSの切り替えによりClientから直接リクエストをMonolithに投げていたのをAPI Gateway(on GKE)経由に移行した.

リクエスト経路が変わるだけだが大きな変更(Gatewayにバグがあるかもしれない)

お客様への影響を最小限に抑えるために段階的なリリースを行った.

Migration

APIのドメインはAWSのRoute53とroadworkerにより管理している.

Route53のWeighted Recordsの機能を使いDNSレコードの返答に「MonolithのIP」と「API gatewayのIP」でそれぞれ重みをつけるようにしその重みを変更することで徐々にリクエストの流し先を変更した.

Migration

(例)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

Migration

パフォーマンス修正をしながら約1ヶ月以上かけてMigrationを完了し現在は100%のリクエストがAPI Gateway経由になっている.

+ 先月よりGateway配下で新規のMicroservicesも動き始めている!

Performance

Performance

"Readable means reliable" (Rob Pike)だが...

Mercariの全リクエスト(ピーク時約56,000req/sec)を受けるためパフォーマンスには十分に注意を払う必要がある.

パフォーマンス・チューニングで最も大切なのは「可視化」

API Gatewayでは以下を行っている

さらに「可視化」してパフォーマンス・チューニングするにはProfilingが必要

Performance

Goは標準でpprofによるCPUやMemory UsageのProfilingの仕組みが提供されている.

が自分で「適切な負荷」を発生させる必要がある...

Googleは「本番環境でProfiler」を動かしている(Google-Wide Profiling: A Continuous Profiling Infrastructure for Data Centers).つまり自分で負荷を発生させるのではなくユーザが実際に利用するエンドポイントでProfilingを行っている!

-> Stackdriver Profiler

Performance

Stackdriver Profilerは以下のようにFramegraphを使いCPUレベルでどのFunctionにどれだけ時間がかかってるかを可視化できる!(+統計的な手法によりProfilingによるパフォーマンスの劣化も防いでいる)

GolangでFlame Graphを描く

Performance

API gatewayではStackdriver Profilerを使い本番でProfilerを動かしCPUレベルでパフォーマンスの劣化問題を発見できるようにしている!

Conclusion

API gatewayとは何か?とその実装思想と詳細,移行方法について紹介した.

We're hiring

参考

Appendix: DigitalOcean 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
}

Thank you

Taichi Nakashima

Use the left and right arrow keys or click the left and right edges of the page to navigate between slides.
(Press 'H' or navigate to hide this message.)