Upgrade to Pro — share decks privately, control downloads, hide ads and more …

【CNDT2020】Amebaアフィリエイト基盤の GKEアーキテクチャと マイクロサービス

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for youta ogino youta ogino
September 08, 2020

【CNDT2020】Amebaアフィリエイト基盤の GKEアーキテクチャと マイクロサービス

Cloud Native Days 2020での登壇資料
https://event.cloudnativedays.jp/cndt2020/talks/59

Avatar for youta ogino

youta ogino

September 08, 2020
Tweet

More Decks by youta ogino

Other Decks in Technology

Transcript

  1. Ⅰ GKEのオートプロビジョニングとスケールアップ 負荷に応じてノードとPodのオートスケールをさせています ノードの自動プロビジョニング 必要なリソースに合わせてノードプール単位でスケーリングしてくれます HPA 人気ブログなどでアクセスのスパ イクが予想されるときは予めス ケールさせます spec:

    scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: grpcserver minReplicas: 3 maxReplicas: 40 metrics: - type: Resource resource: name: cpu targetAverageUtilization: 80 ここではCPU使用率が閾値を 超えたらPodがスケールする ように設定しています。
  2. アクセスログの設定 access_log: - name: envoy.access_loggers.file typed_config: "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog path: "/dev/stdout"

    typed_json_format: start_time: "%START_TIME%" method: "%REQ(:METHOD)%" path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%" protocol: "%PROTOCOL%" response_code: "%RESPONSE_CODE%" response_flags: "%RESPONSE_FLAGS%" bytes_rcvd: "%BYTES_RECEIVED%" bytes_snt: "%BYTES_SENT%" duration: "%DURATION%" x-envoy-upstream-svc-time: "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%" x-forwarded-for: "%REQ(X-FORWARDED-FOR)%" useragent: "%REQ(USER-AGENT)%" x-request-id: "%REQ(X-REQUEST-ID)%" backend_address: "%UPSTREAM_HOST%" client: "%DOWNSTREAM_REMOTE_ADDRESS%" referer: "%REQ(REFERER)%" response_duration: "%RESPONSE_DURATION%" upstream_transport_failure_reason: "%UPSTREAM_TRANSPORT_FAILURE_REASON%" アプリのログの他にも、 Envoyでアクセスログを取得 しています %RESPONSE_FRAGS%で RESPONSEの情報をみるぐ らいに使っています
  3. Envoyのメトリクス EnvoyのメトリクスでgRPC等 のstatusなどもとれます。 実際はDatadogAPMで取得し たメトリクスを主に使用して いますが、Envoyでのメトリ クスで監視設定している部分 もあるので、取得していま す。 template:

    metadata: annotations: ad.datadoghq.com/envoy.check_names: '["envoy"]' ad.datadoghq.com/envoy.init_configs: '[{}]' ad.datadoghq.com/envoy.instances: | [ { "stats_url": "http://%%host%%:8001/stats" } ]
  4. 共通のEnvoyイメージの用意 共通で使える設定ファイルを内包したEnvoyイメージを使用 YAMLのアンカーとエイリアスで記述量を減らすようにしています。 ゆくゆくはサービス の規模感や体制に合 わせたcontrol planeを導入したい です type: STRICT_DNS

    lb_policy: ROUND_ROBIN connect_timeout: 0.25s ignore_health_on_host_removal: true http2_protocol_options: {} health_checks: *egress_health_checks outlier_detection: *outlier_detection circuit_breakers: *circuit_breakers マイクロサービスが増え た際はclustersに書き足 していく運用です。 Envoyをv1.15.0にアッ プグレードして、data plane apiの設定をv3に 対応してあります。
  5. 実際に運用して困ったこと Pod数が増えるとヘルスチェックのgRPCアクセスが劇的に増えて、 API監視でUNKNOWNのアラートを発報してしまった。 http_filters: - name: envoy.filters.http.health_check typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.health_che

    ck.v3.HealthCheck pass_through_mode: false cluster_min_healthy_percentages: self-grpc: value: 100 headers: - name: ":path" exact_match: /healthz pass_through_mode: falseにし てヘルスチェックの状態を保持 し返すようにしました。 no_traffic_intervalを60sにし大 量のヘルスチェックを飛ばさな いようにしています。
  6. Ⅰ リクエストを取りこぼさないためにやったこと① アプリのGraceful Shutdown // Stop datadog exporter cleanly defer

    a.datadog.Stop() go func() { _ = a.server.Serve(listenPort) }() // Wait for signal sigCh := make(chan os.Signal, 1) signal.Notify( sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) log.Printf("SIGNAL %d received, then shutting down...\n", <-sigCh) a.server.GracefulStop() gRPCのGraceful Shutdownを実 装しました。 GracefulStop()を使っています。 またDatadog Exporterにメトリ クスを投げているためdeferで止 めています。 terminationGracePeriodSecon dsも指定しています。
  7. Kubernatesの監視体制 外形監視: Datadog Synthetic 内形監視: Datadog DeploymentのDesired/Available, PodのRestart回数 GSLBと各APIのレイテンシやStatusの監視, NodeのStatus,ノードのDisk使用量etc

    ログ監視: Stackdriver Logging トレース/APM: DatadogAPM/OpenCensus 通知先: Slack/メーリングリスト ドキュメント管理: esa Datadogでの監視の知見が多いの で使用していますDashboardを なるべく統一したいのもあります
  8. DatadogAPMとOpenCensusでのモニタリング OpenCensusでTrace,Stats情報を取得 Datadog Exporterへ送信 - StatsはPrometheus Exporterへ変更を対応中 - Agentのリソース負担を減らすため -

    DogStatsD経由でメトリクスを送らないため - OpenTelemetryに移行させたい 当初DatadogAPMを有効化するか、自前 でJaegerをたてるか悩み、DatadogAPM のライブラリを使わずにOpencensusで取 得している背景があります
  9. OpenCensusでのStats/Tracingでとっているもの gRPC Server/Client - StatsHandlerにocgrpcのhandlerを設定するだけ HTTP Server MySQLドライバー - driverNameの設定時にocsqlでwrapするだけ

    他にどんなものをとっ ているか教えて下さい # Server grpc.StatsHandler(&ocgrpc.ServerHandler{}) # Client grpc.WithStatsHandler(&ocgrpc.ClientHandler{}) return &http.Server{ Addr: host + port, Handler: &ochttp.Handler{ Handler: mux, }, }, listenPort ochttp.Handlerでhttp.Handlerを wrapするだけ ochttpにはHTTPヘッダやStatusを取 得してくれる機能があるので、 stats/viewなどがなくても表示可能 // OpenCensus driverName, err := ocsql.Register("mysql")
  10. Ⅰ マイクロサービス採用した理由 システム要件を洗い出したらモノリスだと辛そうだったから 必要なシステム要件 • アフィリエイト広告の計測 • 成果報酬の支払い • 成果通知の受け取り

    • アフィリエイト案件の管理 • 報酬レポート作成 • アフェリエイターの管理 社内に採用事例があ り知見もあったので 採用しやすかったで す 外部のシステム との連携が必要
  11. マイクロサービスの設計 ① コンテナオーケストレーションにはKubernatesを採用 • デファクトスタンダードなので採用しない理由がなかった マイクロサービス 間の通信にはgRPCを採用 • HTTP/2を使うので低レイテンシでマイクロサービス間の通信が行える •

    Protoocol Buffersを使ったIDLでクライアント・サーバー間でスキーマを共有 できる • IDLからクライアント・サーバーコードの自動生成ができる • エコシステムが充実している
  12. Ⅰ システム構成 ブロガー Private Cloud ブログシステム API Gateway Micro Service

    A Micro Service B Micro Service C Routing SSL終端・API認証 Micro Service内でREST to gRPCの変換をしてます REST(JSON) REST(JSON)
  13. REST to RPCの仕組み grpc-gatewayを利用している • .protoからREST to gRPCのリバースプロキシをするコードを自動生成してく れるもの •

    https://github.com/grpc-ecosystem/grpc-gateway • 自動生成されたコードが各マイクロサービスに取り込まれている rpc SearchItems (SearchItemsRequest) returns (SearchItemsResponse) { option (google.api.http) = { post: "/api/affiliate_item/search_items" body: "*" }; } RESTとgRPCのマッピングをちゃんと考 えると面倒なので以下のルールで統一 • POSTのみ • パス名は/api/マイクロサービス 名/mathod名のスネークケース
  14. Ⅰ 再掲: システム構成 ブロガー Private Cloud ブログシステム REST(JSON) API Gateway

    Micro Service A Micro Service B Micro Service C Routing SSL終端・API認証 当初はここにREST to gRPCの変換を持たせよう としていました REST(JSON)
  15. Ⅰ マイクロサービス間でのエラーハンドリング 共通のエラーコードを定義して、エラーコードを使ってエラーハンドリン グをしている type ApplicationError struct { code string

    message string gRPCStatusCode codes.Code } var ( UnknownError = newApplicationError("CM500003", "unknown error", codes.Internal) UnavailableError = newApplicationError("CM503001", "unavailable", codes.Unavailable) ) 共通のエラーコードの定義
  16. Ⅰ マイクロサービス間でのエラーの伝播① 1. カスタムエラーを定義 2. カスタムエラーをProtocol Buffersで定義 3. エラー時アプリでカスタムエラーを返す message

    ApplicationError { string code = 1; string message = 2; } type ApplicationError struct { code string message string gRPCStatusCode codes.Code } var ( UnknownError = newApplicationError("CM500003", "unknown error", codes.Internal) UnavailableError = newApplicationError("CM503001", "unavailable", codes.Unavailable) ) - エラーメッセージ - 共通のエラーコード - エラーに対応するgRPC のステータスコード を定義
  17. Ⅰ マイクロサービス間でのエラーの伝播② 4. Server InterceptorでError Detailsにカスタムエラーを突っ込む 5. func ApplicationErrorUnaryServerInterceptor() grpc.UnaryServerInterceptor

    { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { res, err := handler(ctx, req) if err == nil { return res, nil } ae, ok := err.(*ApplicationError) if !ok { ae = UnknownError.Wrap(err).(*ApplicationError) } st := status.New(ae.gRPCStatusCode, ae.Message()) if st, _ := st.WithDetails(&commonpb.ApplicationError{ Message: ae.message, Code: ae.code, }); st != nil { return nil, st.Err() } return nil, st.Err() } } エラーが起きたらエラーをProtoで シリアライズして詰めている エラーに対応するgRPCステータス コードを設定している
  18. Ⅰ マイクロサービス間でのエラーの伝播③ 5. Client InterceptorでError Detailsからカスタムエラーを取り出す func ApplicationErrorUnaryClientInterceptor() grpc.UnaryClientInterceptor {

    return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { err := invoker(ctx, method, req, reply, cc, opts...) if err == nil { return nil } st := status.Convert(err) for _, d := range st.Details() { switch t := d.(type) { case *commonpb.ApplicationError: return newApplicationError(t.GetCode(), t.GetMessage(), st.Code()) } } //不明なエラー return UnknownError.Wrap(err) } } Error DetailからProtoでシリアラ イズされたエラーを取り出してデシ リアライズしている
  19. CI/CDパイプライン GCR 本番クラスタ ステージング クラスタ ステージング 本番 PRマージ 反映 反映

    イメージ作成 タグ追加 gitタグ作成 パラメータに作成したイメージのタ グ名が渡される
  20. Manifestの管理 最初はKustomizeを使っていた... • yaml差分を書くのが面倒臭い • 使い回しがしづらい • Command Lineからパラメータを渡せないのが辛い 結局Helmを採用した

    • Kustomizeに比べて学習コストが高いが一度Chartを作れば使いまわせる! • Command Lineからパラメータを渡せる!
  21. Secretsの管理 最初はkubesecを利用 • Secret の構造を保ったまま値だけ暗号化してくれるもの • https://github.com/shyiko/kubesec 現在はhelm-secretsを採用 • HelmのPlugin

    • HelmのValuesファイルを暗号化してくれるもの • デプロイも簡単! helm install が helm secrets installになるだけ • https://github.com/zendesk/helm-secrets External Secretsを採用するのもアリだったかも...