ZOZOTOWNにおける段階的なIstioサービスメッシュ化戦略

ogp

はじめに

こんにちは、SRE部 ECプラットフォーム基盤SREブロックの亀井です。

ZOZOTOWNのマイクロサービスプラットフォーム基盤(以下、プラットフォーム基盤)ではサービス間通信におけるトラフィック制御・カナリアリリース実装のため、Istioによるサービスメッシュを導入しました。現在は初期段階としてBFF機能を司るZOZO Aggregation APIとその通信先サービス間へ部分的に導入しています。

ZOZO Aggregation APIについては、以前に三神が紹介しているので、そちらの記事をご参照ください。

techblog.zozo.com

その後、Istioによる一貫したトラフィック制御・カナリアリリース実装を目的とし、プラットフォーム基盤全体へサービスメッシュを拡大しました。本記事ではその取り組みを紹介します。

なお、本記事はプロダクション運用中サービスのサービスメッシュ移行という運用目線の内容です。Istioの概要や選定理由などサービスメッシュ導入の背景にご興味がある方は、以前川崎が執筆した記事をご参照ください。

techblog.zozo.com

サービスメッシュ導入後の課題

ZOZO Aggregation APIと通信先サービスが部分的にサービスメッシュ化された状態を下図に示します。

ZOZO Aggregation APIと通信先サービスがメッシュ化された状態

「ZOZO Aggregation API → サービス」間はサービスメッシュ化され、Istioによるトラフィック制御・カナリアリリースが実装されました。しかし、プラットフォーム基盤全体ではサービスメッシュの導入は部分的であり、下図の様にサービスによってトラフィック制御・カナリアリリース手法に差異が生まれていました。サービスによって「設定が異なる」または「複数の設定を持つ」状態となっており、運用負荷が高く、二重にリトライが行われるなどの設定不備によるミスが起きやすい状況にありました。

トラフィック制御・カナリアリリース手法の差異

この状況の解消に向け、プラットフォーム基盤全体へサービスメッシュを拡大し、Istioによる一貫したトラフィック制御・カナリアリリース実装の展開を進めました。

プロダクション運用中サービスのサービスメッシュ化方針

プロダクション運用中サービスのサービスメッシュ化では、大きく以下の2点を実施しました。

  • ZOZO API GatewayとIstioの責務整理と機能分担
  • 段階的な移行

以降で、具体的な内容を順に説明していきます。

ZOZO API GatewayとIstioの責務整理と機能分担

ZOZOTOWNはストラングラーパターンでレガシシステムの段階的なリプレイスを行っています。ZOZO API Gatewayは、この中でストラングラーファサードという役割を担っており、ルーティングや認証、トラフィック制御などの機能を持つリバースプロキシとして動作しています。なお、ZOZO API Gatewayは、独自要件に対し柔軟に対応出来るよう独自実装しています。

詳細は、旗野の記事をご参照ください。

techblog.zozo.com

一方、Istioはトラフィック制御、セキュリティ、可観測性の機能を持ちます。つまり、ZOZO API GatewayとIstioでタイムアウト・リトライなどのトラフィック制御機能が重複しています。そこで、ZOZO API GatewayとIstioの責務を明確にし、重複する機能を分担する必要がありました。

まず、下図の様に責務を整理しました。

ZOZO API GatewayとIstioの責務整理 (画像が小さい場合は拡大してご覧ください)

そして、下図の様に機能を分担しました。

ZOZO API GatewayとIstioの機能分担

このような責務整理と機能分担の結果、プラットフォーム基盤全体に対し、サービスメッシュの拡大を滞りなく進める事が出来ました。

段階的な移行

ZOZOTOWNを停止させずにサービスメッシュへ移行するため、下記の様に段階的な移行方針を取りました。

  • 優先度の高いサービスから段階的にサービスメッシュ化
    1. ZOZO API Gateway
    2. その他マイクロサービス
  • 無停止を前提としたサービス単位でカナリアリリース

一斉にプラットフォーム基盤全体をサービスメッシュ化せず、優先度の高いサービスから下図の様に10%、100%とカナリアリリースし、無停止で移行しました。

カナリアリリース

ZOZO API Gatewayサービスメッシュ化における考慮点

ZOZO API Gatewayは責務整理と機能分担の他にも考慮した点があります。みなさまの参考になるであろう、大きな考慮点なので、その内容をご紹介します。

「ALB → ZOZO API Gateway」のトラフィックはサービスメッシュ外から中への通信(Ingress Traffic)です。Ingress Trafficにおいても、サービスメッシュ間のトラフィック同様にIstioによる一貫したトラフィック制御が求められていました。

そこで、IngressGatewayを使う事で上記の課題を解決しました。サービスメッシュの境界にIngressGateway(実態はistio-proxy)を追加する事で、Ingress TrafficもIstioによるトラフィック制御が可能となります。

なお、k8sマニフェストは下記の通りです。IstioOperatorにてIngressGatewayコンポーネントを作成し、ZOZO API Gateway用のIstioカスタムリソースを設定します。

apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
  namespace: istio-system
  name: istio-control-plane
spec:
  components:
    ingressGateways: # IngressGatewayコンポーネントを追加
    - name: ingressgateway
---
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - hosts:
    - zozo-api-gateway.example.com
    port:
      name: http
      number: 80
      protocol: HTTP
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: virtualservice
spec:
  gateways:
  - gateway
  hosts:
  - zozo-api-gateway.example.com
  http:
  - route:
    - destination:
        host: zozo-api-gateway.ns.svc.cluster.local
        subset: primary
      weight: 100
    - destination:
        host: zozo-api-gateway.ns.svc.cluster.local
        subset: canary
      weight: 0
    timeout: 10s
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: destinationrule
spec:
  host: zozo-api-gateway.ns.svc.cluster.local
  subsets:
  - name: primary
    labels:
      version: primary
  - name: canary
    labels:
      version: canary

Amazon EKS上にIngressGatewayをデプロイすると、デフォルトではClassic Load Balancer(CLB)が作成され、サービスが外部に公開されます。しかし、ZOZOTOWNではセキュリティ要件により、AWS WAFのアタッチされたApplication Load Balancer(ALB)を使っています。そのため、サービスメッシュ化も同様のセキュリティレベルを保つため、下図の様にIngressGatewayはCLBで公開せず、既存のALB配下で公開する構成にしました。

IngressGateway

そして、CLBはセキュリティホールとなり得るため、削除しています。下記の様にIstioOperatorのIngressGatewayコンポーネントを設定する事でCLBを作成しない事が可能です。

apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
  namespace: istio-system
  name: istio-control-plane
spec:
  components:
    ingressGateways:
    - name: ingressgateway
      k8s:
        service:
          type: NodePort # CLBを作成しない

ZOZOTOWNへの導入効果

ZOZO API GatewayとIstioの責務整理と機能分担を行い、サービス単位での段階的な移行をしました。その結果、ZOZOTOWNを停止することなく、下図の様にプラットフォーム基盤全体をサービスメッシュ化することが出来ました。

結果

そして、プラットフォーム基盤全体がサービスメッシュ化された事で下記の様な事が可能となっています。

  • 一貫したトラフィック制御
  • カナリアリリース手法の統一
  • 基盤全体でのIstio活用

2つ目に挙げた「カナリアリリース手法の統一」は、ZOZO API Gatewayの場合、下図の様に変更されIstioによる加重ルーティングを用いてカナリアリリースが可能になりました。

サービスメッシュ化後のZOZO API Gatewayカナリアリリース

次に、Istio Virtual Service、Destination Ruleリソースのマニフェスト設定例を紹介します。

まず、Destination Ruleでsubsetにprimary、canaryを登録します。合わせて、Virtual Serviceのroute部分に先程のsubsetを指定し宛先を登録します。そして、weightを更新してクラスタに適応すると、istiodにより自動的にistio-proxyのconfigが更新され、ZOZO API Gatewayへのトラフィック加重率が変更されます。

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: virtualservice
spec:
  hosts:
    - zozo-api-gateway.example.com
  gateways:
    - ingressgateway
  http:
    - route:
        - destination:
            host: zozo-api-gateway.ns.svc.cluster.local
            subset: primary
          weight: 90
        - destination:
            host: zozo-api-gateway.ns.svc.cluster.local
            subset: canary
          weight: 10
      retries:
        attempts: 1
        perTryTimeout: 3s
        retryOn: 5xx
      timeout: 6s
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: destinationrule
spec:
  host: zozo-api-gateway.ns.svc.cluster.local
  subsets:
  - name: primary
    labels:
      version: zozo-api-gateway
  - name: canary
    labels:
      version: zozo-api-gateway-canary

以上の流れで、プラットフォーム基盤全体のカナリアリリース手法が上記に統一されました。

また、基盤全体でIstioの活用も行っており、直近ではサーキットブレーカーを導入しマイクロサービスの連鎖障害に備える取り組みを行いました。詳細は大澤の記事で解説しているので、併せてご参照ください。

techblog.zozo.com

今後の課題

さらなる改善のため、大きく下記2つの課題に取り組んでいく予定です。

  • k8sクラスタを跨ぐIstioサービスメッシュの拡大
  • カナリアリリースの自動化

k8sクラスタを跨ぐIstioサービスメッシュの拡大

ECプラットフォーム基盤SREブロックでは、認証サービス基盤というもう1つの基盤・k8sクラスタが存在します。個人情報などのセキュリティ要件の高い情報を取り扱うサービスが稼働する基盤です。プラットフォーム基盤から認証サービス基盤間の通信は現状サービスメッシュ化出来ておらず、下図の様にZOZO API Gatewayによるトラフィック制御が行われています。

今後の課題

k8sクラスタを跨ぐサービスメッシュの構築を今後の課題としています。

カナリアリリースの自動化

プラットフォーム基盤全体のサービスメッシュ化により、障害を軽減し無停止で進行するカナリアリリース手法が統一されました。しかし、カナリアリリースの進行における判断コストや加重ルーティングを進行、もしくは切り戻す設定変更コストは依然高い状況にあります。一方、カナリアリリース手法が統一されたことで、判断の自動化・設定変更の自動化がしやすくなりました。そこで、Progressive Deliveryの導入など更なるリリーススピードの向上、運用負荷の削減も今後の課題としています。

さいごに

ZOZOでは、一緒にサービスを作り上げてくれるSREエンジニアを募集中です。ご興味のある方は以下のリンクからぜひご応募ください。

https://hrmos.co/pages/zozo/jobs/0000010hrmos.co

Istioサーキットブレーカーで備えるマイクロサービスの連鎖障害

image1

はじめに

こんにちは。SRE部 ECプラットフォームSREチームの大澤です。

先日、SREチームにてBFF機能を司る「ZOZO Aggregation API」の導入について紹介しました。

techblog.zozo.com

BFFは複数のバックエンドと通信するアーキテクチャであるため、通信先のバックエンド障害に大きな影響を受けてしまいます。そのため、ZOZO Aggregation APIでは、各バックエンド間の通信障害をIstioによるタイムアウトとリトライ制御で可用性を担保していました。

今回は、新たにIstioサーキットブレーカーを導入することで、さらなる安定性・回復性の向上を果たした取り組みを紹介します。

サーキットブレーカーとは

サーキットブレーカーとは、あるサービスの障害を検知した場合には通信を遮断、その後サービスの復旧を検知すると通信を復旧させる仕組みです。

複数のマイクロサービスが連動するサービスの場合、一部のマイクロサービスの障害が連鎖的な障害に繋がるカスケード障害を発生させる可能性があります。

以下はカスケード障害の例です。Service Cが応答不能となると、Servie BService Cからのレスポンスを待ち続けるため、不安定な状態となります。この状況が続くとService Bが応答不能となり、連動するService Aへと障害が連鎖します。

image1

サーキットブレーカーは、このようなマイクロサービスアーキテクチャ特有の課題に対するデザインパターンの1つです。

以下の図は、先程の例にサーキットブレーカーを導入した場合の流れです。Service Cで発生した障害を検知するとServie Bはリクエストを遮断します。リクエストを遮断することでレスポンスを待ち続ける状況やスレッドプールの枯渇を防ぎ、Service Bと連動するService Aを保護します。

image1

サーキットブレーカーパターンの詳細についてはこちらを参照ください。 docs.microsoft.com

ここで紹介した例は非常にシンプルなカスケード障害の場合ですが、ZOZOTOWNのプラットフォーム基盤はマイクロサービスアーキテクチャを採用しており、より複雑なサービス間連携が発生しています。そのため、連鎖による大規模な障害に発展しないよう、カスケード障害への対策の必要性が増していました。

既存のタイムアウト・リトライ制御の問題点

ZOZO Aggregation APIでは、Istioによるタイムアウト・リトライ制御設定を通信先のバックエンド毎に入れています。設定したタイムアウト・リトライ試行内でバックエンドからレスポンスが得られない場合には、それ以外のバックエンドから取得できたモジュールのみでレスポンスし、サービスを継続しています。

image1

以下の図は、リトライでサービスを救える場合の処理の流れの例です。商品詳細API呼出処理は、リトライを含めて130msで完了しています。

image1

この様に、すぐに復旧が見込まれる様な一時的なネットワークの瞬断などの不具合であれば、リトライ機能により適切にサービスを救うことができます。

しかし、バックエンドとの通信のエラー状況によってはタイムアウト・リトライ制御が必ずしも適切に働くわけではありません。通信先のバックエンドが不安定になって直ちにエラーが返ってこない場合、Istioによるタイムアウトまでバックエンドからのレスポンスを待つことになります。

以下の例は、Istioで10sのタイムアウト、かつ1回のリトライを設定していた場合です。最終的に商品詳細API呼出処理は20s待つことになります。ZOZO Aggregation APIとしては、該当のAPI以外のバックエンドから取得したモジュールで正常ステータスを返却できます。ただし商品詳細APIの回復までレイテンシーは増加し続けてしまいます。

image1

この様なレイテンシーの増加を防ぐために、異常なバックエンドをサービスアウトし、ZOZO Aggregation APIからリクエストしない状態にするのが理想的です。

以下の例はサーキットブレーカーを導入した場合に期待される動作例です。商品詳細APIに障害が発生している場合、API呼出を行わずに処理を完了できます。

image1

この様に障害を検知し、リクエストを遮断するサーキットブレーカーは有効な手段です。

サーキットブレーカーの導入方法

サーキットブレーカーを導入するには、大きく分けて以下の2つのアプローチが考えられます。

  • 各マイクロサービスにサーキットブレーカーが実装されたライブラリを組み込むアプローチ
  • Istioやnginxのサービスメッシュなど、ネットワーク機能として導入するアプローチ

弊社は後者のアプローチを採用しました。なぜIstioサービスメッシュによる導入を選択したのか、どのようにZOZO Aggregation APIにサーキットブレーカーを導入していったのかを本章で紹介します。

Istioサーキットブレーカーを導入した理由

サーキットブレーカーが実装されたライブラリを各マイクロサービスに組み込んでいく場合、以下の課題がありました。

  • マイクロサービスへの組込やアップグレードの際に、アプリケーション開発者とSRE間でコミュニケーションが多く発生し、コミュニケーションコストが増加する
  • マイクロサービス毎に異なるアーキテクチャー・言語を採用しているため、ライブラリ・組込方法も異なり一貫性の担保が困難になる

こういった点を考慮し、SREチームでは以下の理由でIstioサービスメッシュによるアプローチを選択しました。

  • アプリケーションコードを変更する必要がなく、インフラコードの改修のみでサーキットブレーカーの機能追加が実現でき、かつサービスメッシュ全体で一貫した制御が可能
  • 既にマイクロサービスプラットフォーム基盤にIstioサービスメッシュを活用していたので、サーキットブレーカー導入の敷居が低い
  • Istioサーキットブレーカーは、外れ値検出(エラー検出)だけではなく、接続要求(接続数上限など)によるサーキットブレーカーも提供しており機能要件に適していた

Istioサーキットブレーカーの組込

Istioサーキットブレーカーの設定項目の理解は、サーキットブレーカー自体の振る舞いを把握しているとより容易になります。

そのため、まずはサーキットブレーカーパターンの動作原理を説明します。

サーキットブレーカーは動作原理として以下の状態を持ちます。

  • Closed
    • 遮断機がOFFの状態
    • リモートのサービスにリクエストを要求可能となる
    • リクエストが失敗した場合、エラー数をカウントし、エラー数が閾値に達するとOpen状態へと移行する
  • Open
    • 遮断機がONの状態
    • リモートのサービスへのリクエストは直ちに失敗となる
    • Open状態へ遷移した時間をカウントし、時間経過カウントが閾値に達するとHalf Open状態へ移行する
  • Half Open
    • 障害が解決したか確認する状態
    • リモートのサービスに少数の限られたリクエストを要求可能となる
    • リクエストが成功した場合にはエラーカウントをリセットしClosed状態へ、リクエストが失敗した場合にはOpen状態へと移行する

image1

また、外れ値検出によるIstioサーキットブレーカーの組込は、カスタムリソースであるDestinationRuleへOutlierDetectionを設定することで実現できます。

以下のサンプルコードは、外れ値検出による基本的なサーキットブレーカーを組込む場合の例です。

apiVersion: networking.Istio.io/v1beta1
kind: DestinationRule
metadata:
  name: test-api
spec:
  host: test-api.test-api.svc.cluster.local
  trafficPolicy:
    outlierDetection:
      consecutive5xxErrors: 10
      interval: 10s
      baseEjectionTime: 1m

OutlierDetectionの設定項目は以下の通りです。

設定項目 説明
consecutive5xxErrors Open状態に遷移する5xxエラー閾値
interval 5xxエラー検出の間隔
baseEjectionTime Open状態からHalf Open状態に移行する時間

Istioサーキットブレーカーには、上記以外にも様々な設定値が存在します。詳細はDestinationRuleの公式リファレンスをご参照ください。 istio.io

上記のサンプルの設定では「10秒間で10回の5xxエラーを検知すると、1分間Open状態とするサーキットブレーカー」として動作します。

よって、ZOZO Aggregation APIへのサーキットブレーカー組込は、既存のDestinationRuleにOutlierDetectionを設定するのみです。ただし、サーキットブレーカーを適切に稼働させるためにはバックエンドをOpen状態へ遷移させるための閾値を決定する必要があります。

閾値の決定

閾値を決めるには、以下の2つのアプローチがあります。

image1

  • クライアント側のサービス要件で閾値を決める
    • Service Aは1sに、1回のエラー発生でService Dへのリクエスト前に遮断したい
    • Service Bは1sに、2回のエラー発生でService Dへのリクエスト前に遮断したい
    • Service Cは1sに、3回のエラー発生でService Dへのリクエスト前に遮断したい
  • リモート側のサービス要件で閾値を決める
    • Service Dは1sに、4回のエラー発生で受付けるリクエストを遮断したい

以下に示すのは、クライアント側の要件で閾値を設定する場合の例です。DestinationRuleのサンプル同様に、どのサービスからのリクエストであるのかを個別に定義する必要があります。

apiVersion: networking.Istio.io/v1beta1
kind: DestinationRule
metadata:
  name: service-d-api
spec:
  host: service-d-api.service-d-api.svc.cluster.local
  # Service AからService Dへの設定
  - name: service-a-api-to-service-d-api
    trafficPolicy:
      outlierDetection:
        consecutive5xxErrors: 1
        interval: 1s
        baseEjectionTime: 1m
  # Service BからService Dへの設定
  - name: service-b-api-to-service-d-api
    trafficPolicy:
      outlierDetection:
        consecutive5xxErrors: 2
        interval: 1s
        baseEjectionTime: 1m
  # Service CからService Dへの設定
  - name: service-c-api-to-service-d-api
    trafficPolicy:
      outlierDetection:
        consecutive5xxErrors: 3
        interval: 1s
        baseEjectionTime: 1m

SREチームではZOZOTOWNのセールなどのイベントに合わせて随時Pod数を調節しています。仮にService Dの管理者がPod数を2倍にした場合、Service A〜Cの管理者は個別にエラー閾値を調節しなければならず、運用が複雑になります。また、本記事では省略していますが、VirtualServiceにも同様に、どのサービスからのどのサービスへのルーティングであるか個別に定義する必要があり、複雑さがさらに増します。

そのため、SREチームではリモート側のサービス要件で閾値を決定する方法を採用しました。

サーキットブレーカー導入の効果

安定性・回復性向上のために導入したサーキットブレーカーですが、そのような機能が実際に使われることなく安定してサービスが運用されることが望ましいです。

幸いにもサーキットブレーカー導入後、実際の障害によってサーキットブレーカーが発動されたことはありません。そのため、今回は開発環境に用意したmockアプリで、擬似的に障害状態を再現した事例を紹介します。

以下の図は、サーキットブレーカー導入前のアプリケーショントレーシングの結果です。バックエンドサービスAPIのタイムアウトに影響を受け、ZOZO Aggregation APIのレイテンシーが増加していることが確認できます。

image1

一方、以下の図は、サーキットブレーカー導入後のアプリケーショントレーシングの結果です。サーキットブレーカーによりバックエンドサービスAPIへのリクエストが直ちに遮断されていることが確認できます。また、サーキットブレーカー導入前にはバックエンドサービスAPIのパフォーマンス劣化の影響を受け、9.08sで返却していたレスポンスタイムが136msへ改善していることも確認できます。

image1

サーキットブレーカー導入後の課題

サーキットブレーカーを導入したことにより回復性は高まりました。しかし、バックエンドが障害から復旧したと判断する時間設定によっては、サービス復旧までにタイムラグが生じてしまいます。ユーザ体験を損なわないためにも、「全てのモジュール情報が揃った正しいレスポンス」をタイムラグなく返却することが重要です。障害のパターンは様々なため、運用しながら最適値を見極めていく必要があります。

また、現状はサーキットブレーカーによって通信がOpen状態へ移行したことを検出しておらず、バックエンド自体のサービス稼働状況で通信状況が問題ないか判断しています。もし、通信がOpen状態に移行したことを検知できれば早期にサービス稼働状況が危険な状態であることを発見できるため、サーキットブレーカー検出も今後導入していく予定です。

まとめ

本記事では、Istioサーキットブレーカー導入の事例を紹介しました。本記事により、サーキットブレーカーの有効性、Istioサービスメッシュ環境下であれば簡単に導入可能であることをご理解いただけたら幸いです。また新たな知見が得られた際には、紹介したいと思います。

さいごに

ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。

tech.zozo.com

【オンラインMeetup イベントレポート】ZOZOTOWNアーキテクトナイト

ogp

こんにちは、ZOZOテクノロジーズ技術戦略室の光野(@kotatsu360)です。

ZOZOテクノロジーズでは、9/9にZOZO Tech Meetup〜ZOZOTOWNアーキテクトナイト〜を開催しました。

zozotech-inc.connpass.com

このイベントでは、ZOZOTOWNの開発においてアーキテクトとして活躍しているメンバーから、「アーキテクチャ設計」にフォーカスして技術選定や設計手法、設計時の考え方などについて具体的な事例を交えながらお伝えしました。

登壇内容まとめ

弊社の社員4名が登壇しました。

  • これからのZOZOTOWNを支えるログ収集プラットフォームを設計した話(SRE部 データ基盤 / 塩崎 健弘)
  • ZOZOTOWNマイクロサービス基盤のService Meshアーキテクチャへの移行(SRE部 ECプラットフォームSRE / 川﨑 庸市)
  • ZOZOTOWNマイクロサービス化に向けたサービス粒度の話(ECプラットフォーム部 / 高橋 智也)
  • ZOZOTOWNのアーキテクトという役割を紹介します(アーキテクト部 アーキテクト / 岡 大勝)

最後に

ZOZOテクノロジーズでは、プロダクト開発以外にも今回のようなイベントの開催など、外部への発信も積極的に取り組んでいます。

一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください!

tech.zozo.com

【オンラインMeetup イベントレポート】マイクロサービス化に取り組む、16年目のZOZOTOWN

マイクロサービス化に取り組む、16年目のZOZOTOWN

こんにちは、ZOZOテクノロジーズ 技術戦略室の池田(@ikenyal)です。

ZOZOテクノロジーズでは、7/28にZOZO Tech Meetup〜マイクロサービス化に取り組む、16年目のZOZOTOWN〜を開催しました。 zozotech-inc.connpass.com

本イベントでは、ZOZOテクノロジーズが進めてきたリプレイスプロジェクトの中で、特に「マイクロサービス化」にフォーカスし、各担当者からお伝えしました。

登壇内容 まとめ

弊社の社員5名が登壇しました。

  • ZOZOTOWN(16歳)の悩みをSREが赤裸々に語る (SRE部 ECプラットフォームSRE / 髙塚 大暉)
  • Backends For Frontends(BFF)をプロダクションレディするまでの取り組み (SRE部 ECプラットフォームSRE / 三神 拓哉)
  • ZOZOTOWNトップページの裏側 (ECプラットフォーム部 カート決済 / 高橋 和太郎)
  • ZOZOTOWN 検索機能のマイクロサービス化への取り組みについて (検索基盤部 検索基盤チーム / 可児 友裕)
  • リプレイスを通して実現した、より高度なサービス改善 (検索基盤部 検索基盤チーム / 有村 和真)

最後に

ZOZOテクノロジーズでは、プロダクト開発以外にも、今回のようなイベントの開催など、外部への発信も積極的に取り組んでいます。

一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください!

tech.zozo.com

ZOZOTOWN検索機能のマイクロサービス化への取り組み

header

はじめに

こんにちは、検索基盤部 検索基盤チームの可児(@KanixT)です。以前は通勤に片道2時間ほどかかっていましたが、フルリモートワークの環境になり空いた時間で生後4か月の娘の子育てに奮闘中です。

本記事では、検索基盤チームが取り組んだZOZOTOWN検索機能のマイクロサービス化の事例・工夫点を紹介します。これから検索機能のマイクロサービス化にチャレンジする方の参考になれば幸いです。

目次

背景と課題

ZOZOTOWNでは、ASPからJavaへのリプレイスプロジェクトが数年前より実施されており、これまで多くのAPIを改善・改修してきました。一方、そのリプレイスされた環境には、1つのマイクロサービスに非常に多数のAPIが存在している状態にもなっていました。検索基盤チームが管理する検索APIも、このマイクロサービス(以下、既存マイクロサービス)の中にありました。

既存マイクロサービスは別チームが主管のため、機能追加や改修の際は別チームにレビュー・リリース依頼をしていました。

そのため、改修した内容のマージや、リリースのタイミング等を検索基盤チームが自由にハンドリングできない状態でした。また、把握していないAPIや共通処理等も多数ある状況故に、開発難度が高くなってしまうという課題もありました。

既存マイクロサービスはSQL ServerとElasticsearchを参照しており、1つのマイクロサービスとしては責任が大きく、障害発生時は複数チームが原因特定に動く状態でした。そのため、「リクエスト数が非常に多い検索機能に特化したElasticsearchのみを参照するマイクロサービス」を構築したいという思いがありました。

検索機能に特化したマイクロサービスの構築

検索基盤チームが主管である検索APIのみのマイクロサービスを構築することで、別チームへの依頼事項は無くなり、精通したAPIの開発に集中できます。そのため、開発・改修・リリースに掛かるサイクルの短縮が見込まれます。また、チャレンジングな実装の場合でも、スムーズな意思決定が可能となると考えました。

そこで既存マイクロサービスから検索APIを切り出し、検索機能に特化したマイクロサービスを構築するに際し、下記の目的を定めました。

  • 開発速度を向上させる
  • 検索機能に特化したマイクロサービスを開発する
    • バックエンドはElasticsearchのみとする

既存マイクロサービスから検索APIを切り出すイメージは下図の通りです。この図はZOZOTOWNのシステム概要図であり、青色の部分が今回構築した検索機能のマイクロサービスです。なお、詳細は一部省略しています。

既存マイクロサービス

検索機能のマイクロサービス実装後

検索APIで利用する技術スタックは以下の表の通りです。

技術スタック
言語 Java
フレームワーク Spring Boot
データベース Elasticsearch

どのように構築したか

分割する方針はいろいろと考えられますが、下記の2案で検討しました。

  1. 既存マイクロサービスのリポジトリをコピーする

    既存のリポジトリを丸々コピーした別のマイクロサービスを構築し、検索機能のリクエストのみを受け付け、不要なAPIは後々消す方式。

    • メリット
      • リポジトリをコピーするため少ない作業量で短期間の本番リリースが可能
    • デメリット
      • 不要なAPIのコードが丸々残る
      • SQL Serverの参照が残る
      • 古くなっているライブラリ等もそのまま残る
  2. 既存マイクロサービスから検索APIのみを切り出す

    検索APIのコードのみをコピー・リファクタリングし、別のマイクロサービスを構築する方式。

    • メリット
      • 検索APIのみのマイクロサービスが構築できる
      • Elasticsearchのみの参照にできる
      • コードのコピー・リファクタリングのタイミングで各種のバージョンアップが可能
    • デメリット
      • 1のパターンより作業量が多い

検討の結果、2. の方針を採用し、検索機能に特化したマイクロサービスを構築することにしました。

選定の主な理由は、2. は 1. より実装コストがかかりますが、このタイミングで不要なAPIを取り除いたマイクロサービスを構築することで負債を抱えずに今後の検索機能の開発に集中できると考えたためです。また、このタイミングで、各種バージョンアップや不要なライブラリを削除することでアプリケーション全体の整理整頓ができるメリットもありました。

構築時にやったこと

APIの実装

既存マイクロサービスを分割して切り出す対象となるAPIは全部で4本でした。

一からすべてのコードを書き直す余裕はなかったため、既存マイクロサービスの検索APIのコードを移植し、必要に応じて部分的に再実装しました。その際、ユニットテストが十分に書いてあったおかげで安心して移植と再実装ができました。ユニットテストを書くことは安定した品質につながり、コードを変更する作業が非常に容易になると再認識できる良い経験でした。

静的解析

コードの静的解析ツールとしてSonarCloudを利用し、コードの状態を可視化しています。チームの取り組みとして、ユニットテストガイドラインを作成し、テストカバレッジを毎週確認することで、コードの品質を保つようにしています。

また、GitHubリポジトリへのPull Request単位でユニットテストのカバレッジが確認できるため、レビュー依頼時には開発者自身でテスト不足がないかを確認してもらうようにしています。

なお、現在のカバレッジは86%です。

この程度のカバレッジになると、実装時にはユニットテストを書くことが当たり前になっているため、「テストを書くこと」が浸透していると実感できます。1

ヘルスチェックの実装

アプリケーションのヘルスチェックにSpring Boot ActuatorのElasticsearch用ヘルスチェックの利用を検討しました。Actuatorは単一のElasticsearchエンドポイントのみに対応しており、複数エンドポイントで運用している弊社には対応できないため独自のヘルスチェックを実装しました。

複数エンドポイントのヘルスチェック

ヘルスチェックの独自実装の方法は非常に単純で、各Elasticsearchエンドポイントに対してindexの存在有無を確認するAPIを実装しました。indexの確認方法は次の通りです。

boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);

ElasticsearchのIndex Exists APIより引用

単一エンドポイントのヘルスチェック

複数エンドポイントに比べ、単一のElasticsearchヘルスチェックを行う場合はさらに簡単で、実装は不要で設定のみで実現できます。

まず、pom.xmlに依存関係を追加します。なお、以下に示すXMLはビルドツールにMavenを利用している場合の例です。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
</dependencies>

次に、Elasticsearchに対するヘルスチェックを有効にします。Spring Boot Actuatorの公式ドキュメントも併せてご覧ください。

management:
  health:
    elasticsearch:
      enabled: true

Java(Spring Boot)とElasticsearchを組み合わせたアプリケーションの運用ノウハウにご興味ある方は、こちらの記事も是非ご覧ください。 techblog.zozo.com

各種バージョンアップ

検索機能のマイクロサービス化の際に行った大きなバージョンアップ作業は、JavaとSwaggerのバージョンアップです。なおバージョンアップ後のバージョンは非公表とさせていただきます。

対象 バージョンアップ前
Java 8
Swagger 2.x

Java

バージョンアップに伴うコード修正はなく、スムーズにバージョンアップできました。しかし、GCをCMSからG1へ変更したため負荷テストとメモリサイズのチューニングを実施しました。

Swagger

Swagger 2.xは古くなっていたため、RESTful APIの標準規格と言われるOpen APIへ変更しました。また、今までは非常に大きな1つのyamlファイルに定義が集約されており、開発し辛い状況でした。そのため、yamlファイルを分割し開発をやり易くしました。

OSSのライセンスチェック

アプリケーション内では様々なライブラリを利用しているため、OSSライセンスのチェックを行いました。ここでは、そのチェック方法を紹介します。

具体的には、Spring Bootの依存関係からライセンスの一覧を作成し、各ライブラリのライセンスが社内のOSS利用ガイドラインを順守しているかを目視で確認していきました。なお、OSS利用ガイドラインに関する情報は以下の記事に書かれています。 techblog.zozo.com

ライセンス一覧は下記コマンドで出力できます。

$ mvn license:add-third-party -D license.excludedScopes=test
$ cat ./target/generated-sources/license/THIRD-PARTY.txt | sort > license.txt

参考:License Maven Plugin license:add-third-party

依存関係も把握しておきたい場合、下記コマンドで出力できます。

$ mvn dependency:tree > dependency_tree.txt

外部サービス

運用・監視には下記の外部サービスを活用しています。どのサービスも運用・監視にはなくてはならないサービスです。個別の説明は省きますので、各社のWebページをご参照ください。

  • Datadog
    • マイクロサービスのモニタリングとアラート検知
  • Sentry
    • エラー通知
  • SonarCloud
    • コードの静的解析
  • PagerDuty
    • インシデントのオンコール通知

リリース

すでに本番稼働しているAPIなので、リクエスト先の切り替えは慎重に実施しました。当然のことですが、通常の開発案件も平行で動いているため、それらの開発案件のリリースの合間をみて検索機能のマイクロサービスをリリースしていきました。

リリース時にやったこと

  • 既存マイクロサービスの検索APIと新APIでの比較テスト
    • 同一のリクエストをそれぞれのAPIへリクエストし、同等の結果が得られることを確認する
  • 既存マイクロサービスの開発案件の差分取込
    • 毎週担当者を決めて差分をウォッチし、検索機能に関係する差分がある場合は内容を確認してコードの差分を取り込む
  • カナリアリリース
    • 検索APIは非常に大量のリクエストを受けるため、1度に全リクエストの切り替えず、カナリアリリースで段階的に切替える

カナリアリリースについてご興味ある方は、こちらの記事も是非ご覧ください。 techblog.zozo.com

得られた効果

検索機能に特化したマイクロサービスを構築することで、前述の下記の目的が達成できたかを検証してみます。

  • 開発速度の向上
  • 検索機能に特化したマイクロサービスの開発

開発速度の向上

定量的な測定ができていないため、定性的な評価になってしまいますが、自チームでハンドリングできるマイクロサービスは意思決定が早く、開発作業のスピードは確実に上がっていると感じています。

機能の開発だけでは無く、開発がやり易くなるような改善やリファクタリングもチームメンバーが自発的に実施しているため、チームの気持ちのこもったマイクロサービスへと着々と進化しています。

検索機能に特化したマイクロサービスの開発

本番リリース後はプログラム起因による障害は無く、ZOZOTOWNの検索機能のリクエストを日々安定して処理できています。データベースはSQL Serverを参照することは無く、Elasticsearchのみを参照しており、パフォーマンスとアーキテクチャの両面で想定通りのマイクロサービスが構築できました。

なお、既存マイクロサービスの一部APIでは、まだElasticsearchを参照しているため、完全に目的を達成したとは言えないところが残念ではあります。

まとめ

本記事では、ZOZOTOWNで本番稼働している肥大化したマイクロサービスから検索APIを切り出し、検索機能に特化したマイクロサービスを構築した事例を紹介しました。肥大化したマイクロサービスや役割が多いマイクロサービスをシンプルな形にすることで受けられる恩恵は十分にあると思いました。

ZOZOTOWNにおける検索機能は「ZOZOTOWN利用者が欲しい商品を見つける」ための重要な機能でかつ、リクエスト数も膨大です。今回ご紹介したようにシステムの改修を柔軟に対応できる形へ切り出せた事で、今では更なる検索速度や精度を改善に取り組む環境が整いました。このような検索基盤を開発する経験は個人的にも非常に良い経験でした。

おわりに

ZOZOテクノロジーズでは、検索機能を開発・改善していきたいエンジニアを全国から募集中です。ご興味のある方は、以下のリンクからぜひご応募ください!

hrmos.co


  1. ユニットテストのガイドラインを作成いただいた木目沢さん、ありがとうございました!

Istioによるサービスメッシュをどのようにプロダクションレディにするか

ogp

はじめに

SRE部 ECプラットフォームSREチームの小林 (@akitok_) です。

ZOZOTOWNでは、マイクロサービス間通信におけるトラフィック制御のために、Istioによるサービスメッシュを導入しています。本記事ではZOZOTOWNのマイクロサービスプラットフォーム基盤(以下、プラットフォーム基盤)において、Istioをいかにプロダクションレディな状態で本番に投入していったか、その取り組みを紹介します。

なお、Istioによるサービスメッシュを導入した背景については、以下の記事で紹介しています。

techblog.zozo.com

What is Istio?

Istioは、マイクロサービスの複雑性を解決する一手段である「サービスメッシュ」を実現するためのフレームワークです。サービスメッシュは、マイクロサービスの実装においてビジネスロジックに集中できることを目指して生まれた手法です。

具体的にはサービス間の通信制御をサービスごとに実装させるのではなく、すべてプロキシ経由の通信とし、ルーティングや認証などのプロキシ設定を全体に伝搬させます。プロキシ経由でサービス間に網状の構成を取ることから、サービスメッシュと呼ばれています。

Istioは以下の特徴を持ちます。

  • Google、IBM、Lyftの3社共同開発により、2017年5月にOSS化されたサービスメッシュフレームワーク
  • KubernetesのPodにプロキシ(Envoy)をサイドカーコンテナとして注入させることで、サービスのコード変更を伴わずにサービスメッシュの実現が可能

アーキテクチャは以下の通りです。

Istioのアーキテクチャは、Data PlaneとControl Planeに分割して考えることができ、それぞれ以下の特徴を持ちます。

  • Data Plane
    • サイドカーとして注入されるEnvoyプロキシ(正確にはEnvoyプロキシの拡張)のコンテナから構成される
    • このプロキシがマイクロサービス間の通信を仲介・制御する
  • Control Plane
    • Envoyプロキシコンテナのサービスへの注入や設定伝搬を司る

Istioをプロダクションレディにするまでに直面した3つの課題

ZOZOTOWNでは、これからも持続的に成長を続けていくことを目的とし、現在レガシーシステムのリプレイスを進めています。ZOZOTOWNのリプレイス戦略については、以下のスライドをご覧ください。

speakerdeck.com

その一環で、モノリシックアーキテクチャから、マイクロサービスアーキテクチャへの移行も行われています。そして、マイクロサービス化が進むにつれ、プラットフォーム基盤上で稼働する各サービス間通信に複雑性が生まれていました。

そこで、この課題を解決していくために、昨年度末にIstioの導入を推進しました。その際に、Istioをプロダクションレディな状態で導入していくために、以下3つの大きな課題に直面しました。

  1. どのようにリソース消費量を見積もるか
  2. 何を監視するか
  3. どのように可観測性を向上させるか

本記事では、それぞれどのように検討・対処を進めていったかをご紹介します。

どのようにリソース消費量を見積もるか

Istioのリソース消費量を見積もり、適切なキャパシティプランニングを行う必要があります。そのためには、アーキテクチャに基づき、Data PlaneとControl Planeをそれぞれ分けて考慮する必要があります。

Data Planeサイジング

Istioの公式ドキュメントによれば、Data Plane(Envoy)のパフォーマンスについて、以下のようにレポートされています。

  • Envoyプロキシは、プロキシを通過するリクエストにおいて、1000リクエスト/秒あたり0.35vCPUと40MBメモリを使用する
  • Envoyプロキシは、90パーセンタイルで、レイテンシに2.65ミリ秒を追加する

上記の数値は以下の前提で行われた負荷テストによる結果です。

  • Istio 1.10を使用する
  • サービスメッシュが1000個のサービスと2000個のEnvoyプロキシ(サイドカーコンテナ)で構成される
  • サービスメッシュ全体で1秒あたり70000回のリクエストがある

実際にはData Planeのパフォーマンスは、以下にあるような要素にも依存し、変動します。

  • クライアント接続数
  • 目標リクエストレート
  • リクエストサイズとレスポンスサイズ
  • プロキシワーカースレッド数
  • プロトコル
  • CPUコア数

これらの要因により、レイテンシやスループット、EnvoyプロキシのCPUやメモリのリソース消費量は変化します。そのため、Istioの公式ドキュメントを参考にしながらも、実際に負荷試験を行い、実環境で計測することが非常に重要です。

Envoyプロキシのチューニング

負荷試験の説明を進める前に、まずData Planeのチューニングポイントである、Envoyプロキシのチューニングについて説明します。

Envoyプロキシのresource設定は、Envoyプロキシを注入するリソースに対しspec.template.metadata.annotationsの指定を追加することで、チューニング可能です。

resource設定に関するannotationは以下の通りです。

annotation 説明
sidecar.istio.io/proxyCPU EnvoyプロキシのCPU Requestを指定する
sidecar.istio.io/proxyCPULimit EnvoyプロキシのCPU Limitを指定する
sidecar.istio.io/proxyMemory EnvoyプロキシのMemory Requestを指定する
sidecar.istio.io/proxyMemoryLimit EnvoyプロキシのMemory Limitを指定する

以下の例は、Deploymentリソースに注入するEnvoyプロキシのCPU Limitを500m、Memory Limitを512Miに指定する例です。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-api
spec:
  template:
    metadata:
      annotations:
        sidecar.istio.io/proxyCPULimit: 500m
        sidecar.istio.io/proxyMemoryLimit: 512Mi

その他のannotationでの設定は、公式リファレンスをご参照ください。

本記事で説明する負荷試験では、試験結果を見ながら、このannotationによるチューニングを繰り返し行いました。

負荷試験

プラットフォーム基盤では、以下の3つの負荷試験フェーズに分け、パフォーマンス測定を行い、チューニング精度を上げていくようにしました。

  1. Istioベンチマーク試験
  2. サービス単体負荷試験
  3. サービス結合負荷試験

また、プラットフォーム基盤でのIstioの導入は、BFF(Backends For Frontends)を実現するZOZO Aggregation APIがファーストターゲットとなりました。以下に示す負荷試験イメージは、このAPIの負荷試験を対象として記しています。

ZOZOTOWNのBFFへの取り組みについては、以下の記事をご参照ください。

techblog.zozo.com

Istioベンチマーク試験

Istioベンチマーク負荷試験は、以下の構成で実施しました。

この構成では、実際のマイクロサービスをData Planeに置くのではなく、Fortioという負荷試験クライアントのPodにEnvoyプロキシを注入し、Data Planeに組み込んでいます。FortioがEnvoyプロキシ経由でコールするバックエンドサービスは、httpbinというモックを水平スケールさせた状態で稼働させています。この状態でクライアントからcurlコマンドでHTTPリクエストを実行し、Fortio経由でEnvoyプロキシに負荷をかけ、検証しました。

この試験は、各マイクロサービスに注入するEnvoyプロキシの初期リソース(CPU、Memory)サイジングに役立ちました。また、マイクロサービスのリソースサイジングだけでなく、Istioのバージョンアップにおけるパフォーマンスの変化を確認できる環境としても役立っています。

サービス単体負荷試験

サービス単体負荷試験は、以下の構成で実施しました。

この構成では、試験対象であるマイクロサービスのPodにEnvoyプロキシコンテナを注入し、連携先である他サービスは、Nginxを用いて静的コンテンツを返すWebサービスモックを用意しました。さらに、負荷試験はIstioベンチマーク試験とは異なり、本番環境へのリクエストを想定したテストシナリオを作成し、Gatlingを負荷試験クライアントとして活用しています。

複雑なサービスメッシュ構成において一気にすべてのサービスを接続し、想定したパフォーマンスが出ない、あるいはエラーが頻発するというような事象が発生した場合、問題切り分けが非常に困難になります。被疑箇所は、以下のように分割して考える必要があります。

  • 接続元サービス
  • 接続元サービスのEnvoyプロキシ
  • 接続先サービスのEnvoyプロキシ
  • 接続先サービス

そこでサービス単体試験環境を用意し、接続元サービスと接続元サービスのEnvoyプロキシのチューニングを完了させた上で、実際のマイクロサービスを連携させた負荷試験のフェーズに進むことが重要と考えました。

サービス結合負荷試験

サービス結合負荷試験は、以下の構成で実施しました。

この構成では、連携する他サービスも含め、本番環境と同等の環境を用意しています。

この試験結果が期待通りでない場合は、単体負荷試験と比較しながら切り分けを行うことで、マイクロサービス間での課題整理をスムーズに進めることができました。

Control Planeサイジング

Control Planeを構成するIstiodコンポーネントのパフォーマンスは、以下の要素に依存し、変動します。

  • Deploymentの変更頻度
  • Configurationの変更頻度
  • Istiodに接続するEnvoyプロキシ数

またIstiodは水平にスケール可能なので、CPU使用率などをトリガーとしてKubernetesのHPA(Horizontal Pod Autoscaler)設定で、オートスケールさせると良いです。

なお、本記事ではIstioの構築には深く触れていませんが、プラットフォーム基盤ではIstio Operatorを活用した構築をしています。Istio OperatorによりHPAの設定は自動生成され、IstiodのCPU使用率が80%に到達したら、オートスケールするようにしています。

何を監視するか

プラットフォーム基盤における運用監視には、Amazon CloudWatchDatadogを採用しています。特に今回、既にDatadogで取得している各サービスの監視対象メトリクスなども合わせてダッシュボード化していくことも考慮し、Istioに関するメトリクスもDatadogで取得する方針としました。

DatadogにおけるIstioインテグレーションについては、公式ドキュメントをご参照ください。

メトリクス監視

監視対象のメトリクスについても、Data PlaneとControl Planeに分けて考慮しました。

Data Planeメトリクス

プラットフォーム基盤上に稼働している各マイクロサービスは、Datadog APMを活用し、マイクロサービス単位でのメトリクス収集・監視は十分に実施できている状況でした。そのため、Data Planeの監視は、個々のマイクロサービスに着目するのではなく、Data Plane全体のエラーレートを監視するのが良いと考えました。

そこで、以下の2つのメトリクスを用いて、エラー数 / リクエスト数 = エラーレートで算出した値を監視することにしました。

メトリクス 説明
trace.envoy.proxy.hits Envoyプロキシが受け付けたリクエスト数
trace.envoy.proxy.errors Envoyプロキシが受け付けたリクエストのエラー数

Control Planeメトリクス

前述の通り、プラットフォーム基盤ではIstio Operatorを用いた構築をしています。これによりControl Planeは、Istio Operatorのマニフェストファイルに基づき、自動運用されます。

例えば、Control PlaneのPod障害などがあった場合には自動で再起動され、Podのリソース消費が大きい場合にはオートスケールされるなど、回復性および拡張性をもった構成になっています。そのため、Control Planeの単純なインフラメトリクスの変化ではなく、Control Planeが正しい挙動をしていない以下のような状態を捕捉すべきと考えました。

  • 何らかの原因でEnvoyプロキシの注入に失敗している
  • 何らかの原因でEnvoyプロキシへの設定伝搬に失敗している

それぞれ以下のメトリクスを監視することで、捕捉できます。

メトリクス 説明
istio.sidecar_injection.failure_total Envoyプロキシの注入に失敗した回数
istio.galley.validation.failed Envoyプロキシへの設定伝搬に失敗した回数

分散トレーシング

プラットフォーム基盤ではIstioサービスメッシュの導入により、マイクロサービス間の通信が透過的にルーティングされ、複雑性が増しています。あるサービスのレイテンシ遅延の原因を調査したい場合に、1つのリクエスト起点で発生する複数のマイクロサービス呼び出しをすべてトレースし、どこで何が起きているのか特定するのは至難の業です。分散トレーシングは、まさにこれらのリクエストを追跡するための技術です。Istio Data Planeの分散トレーシングについては、Istioの公式ドキュメントで、ZipkinJaegarLightstepを活用した方法が紹介されています。

前述の通りプラットフォーム基盤では、Datadog APMを活用し、各マイクロサービスの分散トレーシング情報を既に収集していました。そこで、Envoyプロキシを通過した通信も同様にDatadog APMを活用し、各マイクロサービスの通信とIstio Data Planeの通信を一気通貫でトレースできるようにしました。

Datadogを活用したIstio Data Planeのトレーシング情報の収集については、公式リファレンスをご参照ください。

以下は、実際の各マイクロサービスとEnvoyプロキシを通過する通信を含むトレーシング情報の図です。

この図の「えんじ色」の部分が、Envoyプロキシのトレーシングを示しています。他のサービスからの呼び出し関係などを含め、一気通貫したトレースが容易になっています。

どのように可観測性を向上させるか

ここまでは、Istioの監視メトリクスについて、いくつか紹介してきました。

一方で、他にも常に監視対象とする必要はないものの、運用状態として可観測性を高く保っておきたいメトリクスもありました。それらはDatadog Dashboardを使い、一箇所に情報を収集・可視化し、Istioサービスメッシュの健康状態の把握を分かりやすくしています。

以下が実際のダッシュボードです。

IstioのDatadog Dashboardの作成にあたっては、公式ドキュメントブログが非常に参考になりました。

プラットフォーム基盤上のマイクロサービスで、パフォーマンス劣化やエラーなどが観測された際に、特にウォッチしているグラフは以下のものです。

グラフ 説明
Request count by destination 宛先ごとのリクエスト数
Top request destination リクエスト数上位の宛先
Average request latency by destination 宛先ごとの平均レイテンシ
Top latency destination レイテンシ上位の宛先
Request count by resource マイクロサービスごとのリクエスト数
Top request resource リクエスト数上位のマイクロサービス
Error count by resource マイクロサービスごとのエラー数
Top error resource エラー数上位のマイクロサービス

例えば、サービスメッシュ全体のエラーレートが高騰した際に、特定のマイクロサービスに集中して発生している問題なのか、サービスメッシュ全体での問題なのか切り分ける必要があります。その際には、Top request resouceとTop error resourceのグラフを参考にしています。

具体的には、プラットフォーム基盤全体で発生している問題であれば、Top request resourceとTop error resourceのランキングは相関した動きになるはずです。一方で、特定のサービスに起因する場合は必ずしも相関せず、特定のサービスのみ大量にエラー発生している状態が読み取れるでしょう。

このように特定メトリクスを監視するだけでなく、Dashboardなどを活用した可観測性の向上も継続的に実施することが、サービスメッシュを拡大していく上では非常に重要です。

まとめ

本記事では、Istioサービスメッシュをプロダクションレディな状態で、ZOZOTOWNプラットフォーム基盤に導入してきた取り組みを紹介しました。Istioは今後ますますマイクロサービス全体に利用を拡大し、さらなる可観測性の向上や、サーキットブレーカーなどの高度な機能も取り入れていく予定です。また新たな知見が得られたら、紹介したいと思います。

終わりに

ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。

tech.zozo.com

Backends For Frontends(BFF)はじめました

image

はじめに

こんにちは。EC基盤本部SRE部プラットフォームSREの三神です。

2021年3月18日、ZOZOTOWNは大規模なリニューアルをしました。その中でも、コスメ専門モールのZOZOCOSMEと、ラグジュアリー&デザイナーズゾーンのZOZOVILLAを同時にオープンし、多くの反響をいただきました。

今回のリニューアルではBackends For Frontends(以下、BFF)にあたるZOZO Aggregation APIを構築しています。本記事ではZOZOTOWNが抱えていた課題とBFFアーキテクチャを採用した理由、またZOZO Aggregation API構築時に発生した課題と解決法についてご紹介します。

ZOZO Aggregation APIのサービスメッシュについてはこちらの記事でご紹介していますので合わせてご覧ください。

techblog.zozo.com

続きを読む

ZOZOTOWNマイクロサービスの段階的移行を支えるカナリアリリースとサービス間通信における信頼性向上の取り組み

はじめに

SRE部プラットフォームSREチームの川崎 @yokawasa です。

ZOZOTOWNではモノリシックなアーキテクチャーから、優先度と効果が高い機能から段階的にマイクロサービス化を進めています。本記事では、そのZOZOTOWNの段階的なマイクロサービス移行で実践しているカナリアリリースとサービス間通信の信頼性向上の取り組みについてご紹介します。

なお、ZOZOTOWNのリプレイス戦略ついてはこちらのスライドが参考になります。

speakerdeck.com

さて、ZOZOTOWNマイクロサービスプラットフォーム(以下、プラットフォーム)はAWS上に構築しており、コンテナーアプリ基盤にマネージドKubernetesサービスであるEKSを採用しています。また、複数サービスを単一Kubernetesクラスターで稼働させる、いわゆるマルチテナントクラスター方式を採用しています。

下記イメージは、そのマルチテナントクラスター(以下、クラスター)に展開されているマイクロサービスとクライアントからマイクロサービスへのリクエストフローを表した概念図です。本記事ではこの中の青点線で囲んだ部分にフォーカスしてその取り組みをご紹介します。

ZOZO API Gatewayを軸にした段階的なマイクロサービスへの移行

本プラットフォームでは、クライアントが直接サービスと通信するのではなく、すべてのリクエストをZOZO API Gatewayと呼ばれるアプリケーションを経由してサービスにルーティングするAPI Gatewayパターンを採用しています。

ZOZO API GatewayはURIパスベースのルーティング機能を提供し、ルーティング先であるターゲットをまとめたターゲットグループという単位でカナリアリリースの機能を提供します。また、ターゲットへのルーティングにおいてリトライ制御、タイムアウトなど通信の信頼性を高める機能を提供します。

特定のマイクロサービス移行に際して、これらの機能のおかげで古いエンドポイントから新しいものへの切り替えに対しても、クライアントがURI変更の影響を受けることなく安定的かつ段階的な切り替えが可能になります。

下図は、/searchで始まるパスのリクエストをターゲットであるZOZO Search API PrimaryとCanaryにそれぞれ90対10で加重ルーティングするイメージです。

ZOZO API GatewayはGolangで独自実装しており、アルゴリズムや細かな動作制御パラメーター、可用性の機能などZOZOTOWNのさまざまな独自要件に対して柔軟に対応が可能です。まさに、ZOZOTOWNのマイクロサービスアーキテクチャーへの段階的な移行を支える中心的なコンポーネントと言えます。

ZOZO API Gatewayについては各機能や実装レベルの詳細が書かれた人気の記事があるので、是非ご覧ください。

techblog.zozo.com techblog.zozo.com

ALB加重ルーティングによるAPI Gatewayのカナリアリリース

ZOZO API Gatewayをカナリアリリースするための手法を紹介します。

ZOZO API Gatewayの前段にはApplication Load Balancer(以下、ALB)があり、クライアントからのすべてのリクエストはALBからZOZO API Gatewayにフォワードされます。ZOZO API GatewayのカナリアリリースはこのALBが持つ加重ルーティング機能を活用して実現します。そして、このALB加重ルーティング設定の自動化を実現するのがAWS Load Balancer Controller(以下、コントローラー)です。

このコントローラーをクラスターにデプロイすると、Ingressリソースに指定するパスベースのルーティングや接続ターゲットの情報に基づきALBが作成され、ALBのTargetGroupsとしてアプリケーションPodに直接ルーティングするよう、自動的にALBリスナールールを設定します。

以下、ZOZO API GatewayにおけるIngressマニフェストの設定例を紹介します。

TargetGroups部分にカナリアリリースにおける既存のサービスのzozo-api-gateway-primaryと一部のリクエストを振り分けたい新しいサービスであるzozo-api-gateway-canaryを登録します。それぞれの比重を変更してクラスターに適用すると、Ingressリソースの更新イベントを常時モニタリングしているコントローラーにより自動的に指定された比重でALBリスナールールが更新され、ZOZO API Gatewayへのトラフィックの加重率が変更されます。

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: zozo-api-gateway-ingresss
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/actions.forward-external-traffic: |
      { 
        "Type":"forward",
        "ForwardConfig":{ 
          "TargetGroups":[ 
            { 
                "ServiceName":"zozo-api-gateway-primary",
                "ServicePort":"80",
                "Weight":90
            },
            { 
                "ServiceName":"zozo-api-gateway-canary",
                "ServicePort":"80"
                "Weight":10
            }
          ]
        }
      }
spec:
  rules:
    - http:
        paths:
          - path: /*
            backend:
              serviceName: forward-external-traffic
              servicePort: use-annotation

ALB Load Balancer Controllerのannnotation設定について詳しくは公式リファレンスを参照ください。

Istioを活用したサービス間通信のトラフィック制御

Istioを活用したサービス間通信におけるトラフィック制御についてご紹介します。なお、本記事ではサービスメッシュの概要や、Istioそのものに関する説明はしません。

Istioサービスメッシュの導入背景について

ZOZO API GatewayからマイクロサービスへのルーティングにおいてはZOZO API Gatewayのトラフィック制御機能が使えますが、マイクロサービスと他サービス(クラスター外のサービスを含む)間の通信に対しても一貫した機能を提供したいという思いがありました。

これを実現するために出てきた選択肢に以下の3つがありました。

  1. マイクロサービス間の通信でもZOZO API Gatewayを介し、一貫したトラフィック制御機能を提供する
  2. タイムアウトやリトライ制御などの機能を提供する共通ライブラリを各アプリケーションに組み込む
  3. サービスメッシュを活用し、ソースコードを変更することなくアプリケーションPodにSidecarパターンでプロキシを注入して、透過的に機能を追加する

1については、ZOZO API Gateway独自に設定しているクライアント認証設定の手間と、ZOZO API Gatewayへの負荷を考慮すると現実的ではありませんでした。また2は、ZOZOTOWNのように利用言語やフレームワークが統一されていない多様な環境をサポートする必要がある状況下では難しさがありました。最終的に、3のサービスメッシュがもっとも現実的であるという結論に至りました。

そして、我々は次のような理由からIstioを選定して、2020年後半から検証を進めました。

ZOZO Aggregation APIにおける設定例

3月18日にZOZOCOSMEやZOZOVILLAがリリースされましたが、この裏側で利用されているマイクロサービスではじめてIstioを導入しました。

このマイクロサービスはZOZO Aggregation APIと呼ばれ、いわゆるBackends for Frontends(BFF)層としての複数APIの結果を集約し、フロントエンドの仕様に特化したレスポンスを返却します。

ZOZO Aggregation APIでは、下図のようにSidecarプロキシでネットワーク接続されたサービスメッシュ内ネットワーク(以下、メッシュネットワーク)のサービス間の通信とメッシュネットワーク外にあるサービスとの通信の2パターンにおいてIstioによるトラフィック制御の設定をしています。

はじめに、メッシュネットワーク内のZOZO Aggregation APIと検索機能を提供するZOZO Search APIサービス間の通信の設定例を紹介します。

以下のサンプルはVirtual Serviceというルーティングの振る舞いを定義するカスタムリソースのHTTPルーティング部分ですが、ここでZOZO Search APIへの加重ルーティングの比重、タイムアウトやリトライ制御を設定します。今回の例では、上図のように新旧それぞれ90対10の加重ルーティングと、5秒タイムアウトで5xxや接続エラーに対して最大2回のリトライ制御を設定しています。なお、サービス間通信設定では他にもDestination RuleというIstioのカスタムリソースの定義が必要になりますが、ここでは省略しています。

  http:
  - route:
    - destination:
        host: zozo-search-api.searchns.svc.cluster.local
        subset: zozo-search-api-primary
      weight: 90
    - destination:
        host: zozo-search-api.searchns.svc.cluster.local
        subset: zozo-search-api-canary
      weight: 10
    retries:
      attempts: 2
      perTryTimeout: 4s
      retryOn: 5xx,connect-failure
    timeout: 5s

次に、メッシュネットワーク外にあるBackend APIサービスとの通信設定を紹介します。

以下のサンプルもメッシュネットワーク内サービス間通信と同じくVirtual ServiceのHTTPルーティング部分です。ここでは、6秒タイムアウトで5xxや接続エラーに対して最大2回のリトライ制御を設定しています。なお、メッシュネットワーク外とのサービス間通信設定では他にもService Entryというカスタムリソースの定義が必要になりますが、ここでは省略しています。

  http:
  - route:
    - destination:
        host: zozo-backend-api.zozo-sample-service.com
    retries:
      attempts: 2
      perTryTimeout: 3s
      retryOn: 5xx,connect-failure
    timeout: 6s

分散トレーシング

上述の通り、本プラットフォームでは、ALBからZOZO API Gatewayへのルーティング、そこからマイクロサービスへのルーティングという通信連携があります。さらに、Istioを導入してからはサービスメッシュプロキシを通じてサービス間通信が透過的にルーティングされるため、より一層複雑性が増しています。

こういった中で、問題の発生箇所やパフォーマンスのボトルネック、信頼性の機構が期待通りに機能しているかなどをログやメトリクスのみから追うのは大変困難であることが容易に想像できます。

このような問題の解決策として本プラットフォームでは構築初期の頃から分散トレーシングを導入しており、バックエンドサービスとしてDatadog APMを活用しています。

ここでは、先日リリースしたZOZO Aggregation APIへのリクエストの処理状況を表すフレームグラフをご紹介します。ZOZO API GatewayからZOZO Aggregation APIにルーティングされ、そこから複数サービス間との通信で集約された結果がZOZO API Gatewayにより返されるまでの処理状況が一気通貫で確認可能です。

本プラットフォームにおけるDatadogを活用した可観測性の取り組みについて詳細はこちらの発表資料を参照ください。

speakerdeck.com

構成管理とCI/CD

本プラットフォームでは、インフラからアプリまでサービス環境の構成は可能な限りIaC化しており、その構築・更新はCI/CDパイプラインから行うことを基本としています。今回ご紹介した各所のカナリアリリースや、通信の信頼性のための設定についても当然ながら下図のようにCI/CDを起点としてサービス環境にロールアウトされる流れにしています。

なお、ZOZOTOWNマイクロサービスプラットフォームのCI/CD戦略に関しては、こちらの記事で解説していますので是非ご覧ください。

techblog.zozo.com

ちなみに、Istioの構成管理ですが、Istio OperatorというKubernetes Operatorを利用して、IaC化とCI/CDを通じた自動ロールアウトを実現しています。IstioOperatorカスタムリソースに構成設定を定義してクラスターにデプロイすると、カスタムリソースの定義を元にインストールやアップグレード、Istio全体の設定やコンポーネントごとの設定を自動ロールアウトしてくれます。

まとめ

ビックバンアプローチで全体を一気にマイクロサービスアーキテクチャーとしてリリースするケースがある一方、既存機能を動かしながら多様な環境状況を考慮しつつ段階的に移行するケースがあります。本記事では後者のケースにおいてそれを支えるためにZOZOTOWNで実践しているカナリアリリースとサービス間通信の信頼性向上の取り組みについてご紹介しました。

本記事では深く紹介できませんでしたが、ZOZO Aggregation APIやIstioについてはプロダクションリリース要件をクリアするまでにさまざまなチャレンジがありました。また、Istioは今後マイクロサービス全体にその利用広げていき、サーキットブレーカーをはじめとしたより高度な機能活用を行っていく予定です。これらについては別の記事にてその詳細をご紹介できればと思っております。

さいごに

ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。

tech.zozo.com

【ZOZOTOWNマイクロサービス化】API Gatewayの可用性を高めるノウハウを惜しみなく大公開

ogp

はじめに

こんにちは。ECプラットフォーム部のAPI基盤チームに所属している籏野 @gold_kou と申します。普段は、GoでAPI GatewayやID基盤(認証マイクロサービス)の開発をしています。

先日、【ZOZOTOWNマイクロサービス化】API Gatewayを自社開発したノウハウ大公開! を公開したところ、多くの方からご好評いただきました。ありがとうございます。まだ読まれていない方はぜひご覧ください。 techblog.zozo.com

今回はその記事の続きです。API Gatewayは単にリバースプロキシの役割を担うだけでなく、ZOZOTOWN全体の可用性を高める仕組みを用意しています。本記事では、それらの中でカナリアリリース機能・リトライ機能・タイムアウト機能に関して実装レベルの紹介をします。

マイクロサービスに興味ある方や、API Gatewayを自社開発する方の参考になれば幸いです。

なお、本記事における可用性の定義はこちらを参考にしており、 成功したリクエスト数 /(成功したリクエスト数 + 失敗したリクエスト数) で計算できるものとします。

続きを読む

【ZOZOTOWNマイクロサービス化】API Gatewayを自社開発したノウハウ大公開!

ogp

はじめに

こんにちは。ECプラットフォーム部のAPI基盤チームに所属している籏野 @gold_kou と申します。普段は、GoでAPI GatewayやID基盤(認証マイクロサービス)の開発をしています。

ZOZOテクノロジーズでは、2020年11月5日にZOZO Technologies Meetup〜ZOZOTOWNシステムリプレイスの裏側〜を開催しました。その中で発表されたAPI Gatewayによるマイクロサービスへのアクセス制御に関して、当日話せなかった内容も含めて、API Gatewayについてこの記事で網羅的にまとめました。

API Gatewayやマイクロサービスに興味ある方、「API Gateway」という言葉は知っているけど中身はよく分からないという方向けの記事なので、読んでいただけると幸いです。

続きを読む

ZOZOTOWN「おすすめアイテム」を支える推薦システム基盤

ogp

はじめに

こんにちは。SRE部MLOpsチームの田島(@tap1ma)です。

現在、ZOZOTOWNの「おすすめアイテム」に使われていたアイテム推薦ロジックを刷新するプロジェクトを進めています。既に一部のユーザに向けて新しいアイテム推薦ロジックを使った「おすすめアイテム」の配信を開始しています。その刷新に伴い推薦システムのインフラ基盤から新しく構築したので、本記事ではその基盤について解説したいと思います。

目次

「おすすめアイテム」とは

この記事で扱う「おすすめアイテム」とは、ZOZOTOWNで取り扱っている各アイテムの詳細ページ内にある「おすすめアイテム」枠のことです。アイテム詳細ページのアイテムやそのページを閲覧しているユーザに合わせて、おすすめのアイテムを複数表示しています。

image

アイテムの詳細ページは、アイテムの詳細な説明やサイズ毎の在庫状況、商品画像といった情報を含み、ユーザがアイテム購入時に必ず通る重要なページです。そして、アイテム詳細ページ内に設置された「おすすめアイテム」枠もまたZOZOTOWNの重要な要素の1つです。

しかし、これまで使われていた推薦ロジックは10年以上前に開発されたもので、ストアドプロシージャとしてオンプレミスのSQL Serverに保存されているなど非常にレガシー化したシステムの上で動いていました。そのため、大きな技術的負債となっていました。

この度、より高性能な推薦ロジックの導入とそのためのシステムをインフラ基盤から新しく構築することで、推薦ロジックの性能向上と推薦システムの技術的負債の回収を同時に実現できました。

新しい推薦ロジック

推薦ロジックの刷新に際し、以下の2種類の推薦ロジックを開発しました。

  1. Recommendations AIを用いた推薦ロジック
  2. ZOZO研究所によって独自で開発された推薦ロジック

2種類の推薦ロジックの開発を並行で行い、互いに性能を競わせながら、より高性能な推薦ロジックの実現を目指して日々開発に取り組んでいます。

Recommendations AIを用いた推薦ロジック

Recommdendations AIはECサイトに特化し、ユーザにパーソナライズされた商品の推薦システムを機械学習の高度な知識を必要とせずに簡単に構築できるGCPのフルマネージドサービス(本稿執筆時点でベータ版)です。

推薦ロジックのモデル構築に必要なデータを入力することで推薦ロジックの機械学習モデルの構築から、そのモデルを使って商品の推薦結果を返す推論用のWeb APIのサービングまで自動で行ってくれます。

また、データの入力に対してリアルタイムでモデルを更新できること、推論用のWeb APIがスケーラブルであることといった特徴を持ちます。

詳しくはこちらの資料をご覧ください。

ZOZO研究所によって独自で開発された推薦ロジック

弊社が有する研究機関「ZOZO研究所」によって独自で開発された推薦ロジックです。

現在は、ランダムウォークのアルゴリズムをベースとし、高速な推論速度、アイテムのカテゴリ分布の調整が可能(=推薦アイテムの多様性を制御できる)、などの特徴を持つ推薦ロジックとなっています。

手法の詳細は本記事では割愛しますが、現在もより高性能な推薦ロジックの実現を目指して様々な手法を用いた開発が進められています。

新しい推薦システム

本章では、ユーザがアイテムの詳細ページへアクセスした際に詳細ページの「おすすめアイテム」枠に最適なアイテムを選出する新しい推薦システムについて詳しく解説します。

推薦システムの処理の流れ

新しい推薦ロジックを使った推薦システムはGCP上でVPCから新規で構築しました。

以下が新しい推薦システムのシステム構成の概略図です。

image

推薦システムはAWS上に存在するZOZOTOWNのバックエンドAPIからアクセスされ、推薦結果のアイテム情報をリストで返します。AWS→GCPのプライベート接続には、AWSではDirect Connect、GCPではDedicated Interconnectという専用線サービスを使用しオンプレ経由での専用線接続を行うことで高可用・低レイテンシーな通信を実現しています。

ZOZOTOWNバックエンドAPIからきたリクエストを推薦システムが処理して推薦結果となるアイテムのリストを返すまでの流れを、図の番号に沿って説明します。なお、登場するコンポーネントの説明は後述します。

  1. ユーザがアイテムの詳細ページを開いた時に非同期でZOZOTOWNバックエンドAPIはアイテム推薦API宛にGETリクエストを投げます。
  2. Internal Load BalancerはZOZOTOWNバックエンドAPIからのリクエストをアイテム推薦APIに振り分けます。
  3. アイテム推薦APIはまずRecommendations AI APIまたはZOZO研究所APIに対してリクエストを投げて、「おすすめアイテム」枠に表示すべきアイテムIDのリストを取得します。
  4. アイテム推薦APIは3.で取得したアイテムIDのリストに対してRedisにアクセスし、キャッシュヒットした場合は、取得したアイテムの詳細情報を推薦結果のアイテムIDのリストに付加します。
  5. Redisアクセス時にキャッシュヒットしなかった場合は、Bigtableへアクセスし取得したアイテムの詳細情報を推薦結果のアイテムIDのリストに付加します。
  6. Bigtableから取得したアイテムの詳細情報をRedisにキャッシュさせた後、推薦結果のアイテムIDのリストに付加します。
  7. 推薦結果のアイテム情報のリストをZOZOTOWNバックエンドAPIへ返します。

システム構成

以下、図の各コンポーネントを解説します。

  • Internal Load Balancer

    ZOZOTOWNのバックエンドAPIからのアクセスを捌くL7ロードバランサーです。今回GKE(Google Kubernetes Engine)クラスタを新規で構築し、後述する「アイテム推薦API」及び「ZOZO研究所API」のサーバーをGKEクラスタのPod上で稼働させています。このGKEクラスタはプライベートネットワーク内に閉じているため、GKEクラスタの受け口にL7内部負荷分散を立てる必要がありました。GKEのバージョン1.16.5-gke.10からGCPのL7内部負荷分散に対応したIngress for Internal HTTP(S) Load Balancingが使用できるようになったので、今回初めて採用しました。

  • アイテム推薦API

    アイテム推薦APIはZOZOTOWNバックエンドAPIからInternal Load Balancer経由で届いたリクエストに対して、最終的な推薦結果となるアイテムをリストで返すAPIサーバーです。ZOZOTOWNバックエンドAPIから届くリクエストのパラメータには閲覧しているアイテムの情報とユーザの情報、推薦結果として取得したいアイテムの件数が含まれています。Java製フレームワークSpring Bootを用いて作られており、GKEのPod上で稼働しています。

  • ZOZO研究所API

    ZOZO研究所が開発した推薦ロジックを用いた推論用のAPIサーバです。アイテム推薦APIからきたリクエストに対してそのアイテムの詳細ページの「おすすめアイテム」枠に最適なアイテムを推論し、アイテムIDのリストを返します。Python製フレームワークのFlaskを用いて作られており、GKEのPod上で稼働しています。

  • Recommendations AI API

    GCPのRecommendations AI上で構築したアイテム推薦の推薦ロジックの機械学習モデルによる推論を行うWeb APIです。ZOZO研究所API同様、アイテム推薦APIからきたリクエストに対して推論したアイテムIDのリストを返します。

  • アイテムデータベース

    ZOZOTOWNの最新のアイテム情報のデータが格納されているデータベースで、GCPのフルマネージドな大規模分散データベースであるCloud Bigtableを使用しています。Cloud Pub/Sub経由で送られてくるアイテムのデータの更新情報がリアルタイムで反映されるので、常に最新のアイテムデータが格納されています。

  • アイテム情報キャッシュ

    GCPのフルマネージドなRedisサービスを利用しています。Bigtableへの負荷軽減のためにアイテム推薦APIがBigtableから取得したアイテムの情報は一定期間Redisにキャッシュさせています。

新しい推薦システムで工夫したポイント

以下、工夫した点についていくつか紹介します。

Bigtableのパフォーマンス改善

ZOZOTOWNの最新のアイテムの情報を格納しておくデータベースとして以下の要件から高スループットかつ低レイテンシーなGCPのCloud Bigtableを採用しました。

  • 大量の書き込みに耐えられる
  • 高速な応答性能を持つ推薦システムを実現できる

しかし、実際にアイテム推薦APIからBigtableへアクセスしてみると期待通りの応答性能が出なかったため、パフォーマンスチューニングを行い応答性能を改善しました。ここでは、実際に行ったチューニング方法について説明します。

前提として、アイテム推薦APIは以下のような実装となっていました。

  • アイテム推薦APIではJava製のBigtableクライアントライブラリbigtable-hbase-2.xのバージョン1.12.0を使用しています。
  • アイテム推薦APIからBigtableへのアクセスは全て Multi-Get という参照系の操作です。

調査したところ、上記のBigtableクライアントライブラリによるBigtableへのアクセスエラー発生時のリトライ処理は以下のような挙動をしていることが分かりました。

  • BIGTABLE_RPC_TIMEOUT_MS_KEYで設定されたタイムアウト時間(ミリ秒)を迎えるまでリトライ処理を繰り返す。
  • 参照系処理の場合はタイムアウト後にMAX_SCAN_TIMEOUT_RETRIESの回数だけ更にリトライ処理が走る。なお、ドキュメントに記載は見当たりませんが、実装を確認するとscanだけではなくgetの処理においてもMAX_SCAN_TIMEOUT_RETRIESの回数分リトライ処理が走ることが分かっています。

今回、アイテム推薦システムのタイムアウト時間の要件は5秒であったため、元々の設定であった設定値Aとタイムアウト時間を減らしてその分タイムアウト後のリトライ回数を増やした設定値B、2つの設定値でアイテム推薦APIに負荷をかけてBigtableへのアクセス時の応答性能を測定しました。

img4

実験の結果、設定値Bの応答速度は設定値Aに比べて99パーセンタイル値の比較で平均約29%速いことが分かりました。つまり、応答が想定時間で返ってこない場合はそのまま待つよりも再度リクエストを投げた方がより速く応答を返しやすいようです。

以上の結果を踏まえてBの設定値となるように更新し、パフォーマンスを改善できました。

アイテム推薦APIのPodの安全停止

アイテム推薦APIのデプロイ時にPodがローリングアップデートされた際、Ingress for Internal HTTP(S) Load Balancing(以下、Ingress)でステータスコード503のエラーが出る事象が発生しました。調査したところ、原因はGKEのIngressのコネクションドレインのタイムアウト時間がデフォルトの0秒であったためコネクションドレインが機能せずリクエスト処理の途中でコネクションが切られてしまっていたからでした。そこで、適切なコネクションドレインのタイムアウト時間を設定したのですが、ここではその設定の際に考えたことについて説明します。

PodがIngressから登録解除されて停止する際にPodではpreStopフックの実行処理が、Ingressではコネクションドレインの処理が同時に非同期で実行されます。そして、PodではpreStopフックの実行終了後にコンテナのルートプロセスに対してSIGTERMが送られ、サーバーがGraceful Shutdownされます。実際に計測してみるとアイテム推薦APIのpreStopフックの処理が始まって10〜15秒後にコネクションドレインの処理が始まっていることが分かりました。そのため、まずはpreStopフックでは15秒のsleep処理を走らすことで、PodがIngressから登録解除されてコネクションドレイン処理開始する前にサーバーのGraceful Shutdownが始まらないように調整しました。また、サーバーのGraceful Shutdownに要する時間は約20秒だったので、この場合のコネクションドレインのタイムアウト時間はPodのpreStopフックの開始から正常にGraceful Shutdownされるまでの時間である(15秒+20秒=)35秒以上に設定すべきであることが分かります。実際には少し余裕を持ってコネクションドレインのタイムアウト時間として45秒を設定しました。

ZOZO研究所APIのキャッシュ戦略

ZOZO研究所APIに対して負荷試験を行ったところ、クエリとなるアイテムIDによって応答速度に大きなばらつきがあり、CPUスパイクも頻繁に起こしていました。

調査したところ、以下のことが判明しました。

  • 推薦ロジックのアルゴリズムの性質上、アイテム詳細ページのアクセス数が多いアイテムほど推論の計算コストが高くなるため、推論速度が遅い。
  • アイテム詳細ページのアクセス数はアイテム毎に大きな偏りがある。

上記の特性を踏まえて、アクセス数上位のアイテムに関しては推論結果をキャッシュするようにしました。ZOZO研究所APIのPod起動時に推論を行い、サーバーのメモリ上に推論結果を展開しています。キャッシュしたアイテムの数は総アイテム数のわずか0.2%ですが、システムパフォーマンスが大幅に向上し、観測されていたCPUスパイクも起きなくなり安定化できました。

推薦ロジックのモデル更新時のワークフロー

ZOZO研究所製の推薦ロジックに使用するモデルの更新は毎日1回行われています。

ここでは、そのモデル更新時のワークフローを解説したいと思います。以下が、そのワークフローの概略図です。

image

Cloud ComposerとBigQueryで日次集計された最新のアイテムデータを読み込み、Pythonスクリプトによって推薦ロジックのモデルファイルを生成します。この日次集計処理は今回のプロジェクト以前から運用されていて、かつ、異なるGCPプロジェクトで存在していました。そのため集計完了の通知をCloud Pub/Subで受け取るようにし、本プロジェクトのモデルファイル生成Jobを実行するトリガーとしています。Cloud RunはCloud Pub/Subからメッセージを受け取りGKEのJobを実行するトリガーとしてのみ利用しています。なお、Cloud Run・GKEのJobの選定に関しては後述します。

モデル更新時の流れを図の番号に対応する手順で説明します。

  1. Cloud Composerの日次集計で最新のアイテムデータをBigQueryに保存後、集計完了を意味するメッセージをCloud Pub/Subにパブリッシュします。
  2. Cloud Pub/SubはCloud Composerから飛んできたメッセージをトリガーにCloud Runのエンドポイントを叩きます。
  3. Cloud Runのエンドポイントが叩かれるとCloud Runではモデル作成用のPythonスクリプトを実行するGKEのJobをアイテム推薦APIと同じGKEクラスタ上で作成します。Cloud RunではGoで書かれたAPIサーバが動いており、そのAPIサーバのエンドポイントが叩かれるとモデル作成用のPythonスクリプトを実行するJobをアイテム推薦APIと同じGKEクラスタ上に作成します。Goの選定理由はKubernetes APIアクセス時に使用するGo言語用のKubernetesクライアントライブラリclient-goが他の言語用のクライアントライブラリに比べて開発が活発で継続的にメンテナンスされることが期待できるためです。
  4. GKEのJobではモデル構築用のPythonスクリプトを実行してBigQueryから最新のアイテムデータを読み込んでモデルファイルを作成し、Cloud Storageにアップロードします。
  5. GKEのJobはCloud Storageにモデルファイルをアップロード後、最後にZOZO研究所APIのPodを kubectl rollout restart コマンドによって再起動させて処理が終了します。
  6. ZOZO研究所APIのPodは再起動時に新しいモデルファイルをCloud Storageからダウンロードし、推論時にそのモデルを使用するようになります。

推薦ロジックのモデル更新時のワークフローで工夫したポイント

以下、推薦ロジックのモデル更新時のワークフローで工夫した点についていくつか紹介します。

Cloud Runの選定

前述の通り、既存のCloud Composerとは別のGCPプロジェクトでモデル更新のジョブを実行する必要がありました。そこで、Cloud Composerの日次集計のワークフローの最後にCloud Pub/Subへ通知を送り、その通知をトリガーに別のGCPプロジェクトでモデル更新のジョブを実行する設計としました。

1日1回Cloud Pub/SubからくるPOSTリクエストをトリガーにワークロードを実行する用途として、リクエストが実際に処理されている時間のみ課金が発生する以下の3つのサーバレスソリューションを検討しました。

ただし、現在弊チームではAnthos環境を運用していないので、Cloud Runの場合はCloud Run for Anthos on Google Cloudではなくフルマネージド版のCloud Runのみに限ります。

また、これらのメモリの上限値は以下の通りです。

App Engine(スタンダード環境) Cloud Functions Cloud Run(フルマネージド)
メモリ上限 2Gi 2Gi 4Gi

モデル更新ジョブのメモリ消費量は上記のどのソリューションにおいてもそのメモリ上限値を超えてしまうので、モデル更新処理をそれだけで完結することはできません。そこで、モデル更新ジョブをアイテム推薦APIのPodなどが稼働しているGKEクラスタ内のハイメモリなインスタンス上でKubernetes Jobとして実行することにし、そのJobの作成処理を上記のサーバレスソリューションのいずれかで実行することにしました。理想を言うと、Cloud Pub/Subへの通知をトリガーとして直接GKEのJobを作成できるようなソリューションがあったら嬉しいですね。

検討の末、他の2つに比べてワークロードのランタイムに縛りがなく、Dockerfileで管理できる開発のしやすさの点で優れたCloud Runをチームで今回初めて採用しました。また、Cloud Runでは、Cloud Pub/Subからきたリクエストのトークン認証処理を組み込みでサポートしているので、簡単な設定でCloud Pub/SubとCloud Run間を安全に通信することができます。GCPの公式ドキュメントには記載が見当たりませんでしたが、Cloud RunとCloud Pub/Subが異なるGCPプロジェクトに存在しているケースでもトークン認証処理を利用することができます。便利。

Cloud Pub/Subのat-least-once配信の考慮

Cloud Pub/Subはat-least-once配信方式を採用しているため、同一のメッセージが複数回配信される可能性があります。そのため、たとえCloud ComposerからCloud Pub/Subへのメッセージのパブリッシュは1日1件であっても、そのメッセージが複数回配信されてCloud Runのエンドポイントが1日に複数回叩かれてしまう場合を考慮しないといけません。

まず、Cloud Runで日に複数回GKEのJobの作成処理が実行される可能性があるため、GKEのJobの処理が冪等である必要があります。今回GKEのJobで実行するモデル作成処理は冪等であるため、この要件は満たしていました。

また、Cloud RunによるGKEのリソースへの操作が並列で行われる可能性も考慮する必要があります。Cloud Runの処理ではGKEのJobを作成するためにKubernetes APIを使ってGKEのリソースを操作します。実際の処理ではJobの作成処理だけでなく、昨日分のJobの設定をクリーンアップしたりJob作成時に立ち上がったPodの情報を取得したりと、Cloud Runのエンドポイントへのリクエスト毎に複数回Kubernetes APIへのアクセスが発生します。もしCloud Pub/Subからほぼ同時に複数のリクエストがきた場合、これらのKubernetes APIへの操作が並列で実行されるため、Jobの作成処理が同時に2度実行されてしまい片方の処理がエラーになるといった問題に繋がる可能性があります。一方で、Kubernetes APIを使った操作は非同期なこともあり、このようなエッジケースにも対応した並行性制御を実装するのはなかなか大変です。そこで、複数リクエストが同時にきてもCloud Runではリクエストを並列では処理しないようにすることでGKEのリソースへの一連の操作を排他制御するようにしました。具体的には、Cloud Runではコンテナあたりの最大同時リクエスト数とスケールアウト時の最大コンテナインスタンス数を簡単に設定できるので、どちらも最大値を1とすることで複数リクエストを並列で処理しないようにしました。

Cloud Monitoringを使用したGKE Jobの監視設定

GKEのJobの監視を導入時にいくつか躓いた点があるので、それらの点も踏まえてどのような設定をしたのかをここでは説明したいと思います。

以下の2種類の監視が現在設定されています。

  • JobのPodステータスがFailedになった時のアラート設定
  • 長時間経ってもJobのPodステータスがSucceededにならない時のアラート設定

JobのPodステータスがFailedになった時のアラート設定

当初はJobが異常終了した時、すなわち、Jobが作成したPodのステータスがFailedとなった時にアラートが鳴るようにCloud Monitoringで監視を導入しようと試みました。しかし、Cloud MonitoringではPodのFailedステータスを直接カウントしアラートのトリガーとするような設定方法はサポートされておりませんでした。そこで、代わりにPod内のコンテナの再起動回数であるrestart countをトリガーに使用できることを利用し、Pod内のコンテナの異常終了後の再起動処理が発生(restart count > 0)したらアラートが鳴るように設定しました。ただし、このやり方は以下の点において理想的な監視とは言えない妥協策です。

  • JobのrestartPolicyがOnFailureに設定されている場合においてのみ使える方法である
  • Podの失敗ステータスを直接監視できておらず、間接的な監視となってしまっている

もっと適切な方法ご存知の方いましたら教えてください!

長時間経過してもJobのPodステータスがSucceededにならない時のアラート設定

Jobが失敗した時の監視だけでは以下のような異常時のケースを拾うことができません。

  • そもそもJobが実行されていない場合
  • Jobの処理途中になんらかの理由でスタックしている場合

上記のケースが発生した際にアラートが飛ぶように、日次で実行されるJobがその日のうちに正常終了していない時にアラートがなるような監視も導入しました。しかし、Cloud MonitoringではPodのFailedステータスと同様Succeededステータスを直接カウントしアラートのトリガーとするような設定方法もサポートされておりませんでした。また、Cloud Monitoringでは24時間周期の監視もサポートされていませんでした。そこで、妥協策として以下の手順で監視を設定しました。

  1. Jobの処理の最後にJobの成功を意味するログをコンテナログとして出力するようにする。
  2. GKEのCronJobで毎日定時に直近24時間でCloud Loggingへ出力されたJobのコンテナログからJobの成功を意味するログを検索する。もし成功ログが見つからなかった場合は、エラーログをCronJobのコンテナログに吐くようにする。
  3. Cloud MonitoringでCronJobが吐いたエラーログをトリガーとしてアラートを鳴らすように設定する。

このやり方もPodの成功ステータスを直接監視できていない複雑な設計となってしまっています。もっと適切な方法ご存知の方いましたら教えてください!(2回目)

まとめ

本記事ではZOZOTOWNの「おすすめアイテム」枠に使われている新しい推薦システム基盤のアーキテクチャについて解説しました。私が今年入社して最初に取り組んだプロジェクトでしたが、ネットワーク設計から任せてもらい、個人的に思い入れの深いプロジェクトです。ユーザの皆さんが気に入るアイテムをより簡単に見つけやすくできるように引き続き改善に取り組んでいきます。

最後に

SRE部MLOpsチームでは、データや機械学習を用いてサービスを成長させたいエンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください!

www.wantedly.com

CloudNative Days Tokyo 2020 にてID基盤リプレイスについて技術発表をしました

ogp

こんにちはプラットフォームSREの亀井と三神です。

先日開催されましたCloudNative Days Tokyo 2020にて私達が取り組んできたID基盤リプレイスプロジェクトについて登壇してきました! ID基盤リプレイスプロジェクトはモノリスな環境をリプレイスするプロジェクトの1つであり、マイクロサービス化とそれに伴うメンバーの教育について挑戦した案件ですので是非とも御覧ください。

続きを読む
カテゴリー