LINE株式会社は、2023年10月1日にLINEヤフー株式会社になりました。LINEヤフー株式会社の新しいブログはこちらです。 LINEヤフー Tech Blog

Blog


【インターンレポート】Istioの導入によるKubernetesクラスタの可観測性の向上

はじめまして、LINEのエンジニア就業型インターンに参加させていただいた、横浜国立大学大学院 環境情報学府 情報環境専攻 情報学プログラム 修士1年の遠藤祐輝です。普段はIoTマルウェアの通信に関する研究を行っています。今回、AI開発室のAIプロダクト開発2チームにサーバサイドエンジニアとして配属され、当チームで運用しているKubernetesクラスタへのIstioの導入およびIstioを利用した既存アプリケーションの改善に取り組みました。本レポートでは、Istioの導入背景や取り組んだ内容についてご紹介します。

背景と目的

AIプロダクト開発2チームでは、CLOVA SpeechなどのAIに関連する様々なプロダクトを開発・運用しています。提供しているプロダクトの多くはマイクロサービスアーキテクチャに近い設計となっていて、複数のサービスがネットワークを介して連携しています。そこで、当チームではインフラとしてLINEのプライベートクラウド(Verda)が提供するマネージドなKubernetesクラスタを利用しています。また、計算機資源の最適化や運用の効率化のため、1つのKubernetesクラスタ上で複数のプロダクトを運用するマルチテナントな構成を取っています。

サービスの稼働状況を効率的に監視して問題を素早く検出するには、可観測性の向上が必要です。そのため、当チームでは、サービスごとに各種ログやメトリクス、トレース情報をExportする機能を実装しています。これらの情報はThree Pillars of Observabilityとして重要視されています。しかし、同様の設定をプロダクトおよびそれを構成するサービスごとに行うのは大変です。そこで、それらの機能を共通基盤として提供可能なサービスメッシュを導入することになりました。サービスメッシュには、IstioLinkerdなどのいくつかの実装がありますが、今回はトラフィック制御に関する機能が豊富であり、より多くの使用事例が見られるIstioを選択しました。

Istioによるサービスメッシュは、以下の画像(出典)のように構成されており、サービス間の通信を制御するデータプレーンと、データプレーンの設定を管理するコントロールプレーンにより構成されます。データプレーンでは、各サービスのサイドカーコンテナとしてIstio Proxyをデプロイし、インバウンド/アウトバウンド通信においてプロキシを経由させることで通信を制御します。また、プロキシを経由させることで、トラフィックに関するログやメトリクスを自動的に収集することができます。コントロールプレーンは、サービスディスカバリやデータプレーンの設定、認証などの役割を担っています。

このように、サービスメッシュを利用することでサービスの状態の観測やトラフィック制御を透過的に(各サービスで意識せずに)行うことが可能です。

Istioの導入

Istioの導入方法として、主にistioctlを使用した方法とHelmを使用した方法の2種類があります。両者の比較結果を以下の表に示します。当チームでは自動デプロイ基盤としてArgoCDを利用しており、Helmを使用する場合、ArgoCDにより管理を自動化できるというメリットがあります。一方、推奨されているのはistioctlを用いた方法です。

istioctl Helm
サポート状況 stable alpha
ArgoCDによる自動デプロイ 未対応 対応

今回は、Istioの設定変更やアップグレードは頻繁には発生せず手動での更新で十分と考え、istioctlでの導入を選択しました。

istioctlを用いてインストールすることで、istio-systemというnamespaceが作成され、その中にコントロールプレーンの役割を果たすIstiod、メッシュに対するインバウンド/アウトバウンド通信のゲートウェイとなるIngress/EgressGatewayがデプロイされます。また、Istioと連携するアドオンとしてKialiとPrometheusをデプロイしました。

可観測性に関する設定

ログ

Istioでは、Istio Proxyが出力するログの形式や内容、出力先をカスタマイズすることができます。当チームのKubernetesクラスタでは、各コンテナの標準出力をDaemonsSetとしてデプロイされているFluentdにより収集し、ElasticSearchに保存しています。その際、FluentdではJSON形式のログをパースして、ElasticSearchで検索できるようにしています。そこで、Istio Proxyでは標準出力にJSON形式のログを出力するように設定しました。

apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
  name: istio-operator
  namespace: istio-system
spec:
  meshConfig:
    defaultProviders:
      accessLogging:
        - envoy-json
    extensionProviders:
      - name: envoy-json
        envoyFileAccessLog:
          path: /dev/stdout
          logFormat:
            labels: {} # JSON format

メトリクス

Istioでは、コントロールプレーンや各サービスにデプロイしたプロキシからメトリクスを収集することができます。それらの情報から、コントロールプレーンの状態や各サービスのリクエスト数、エラー率、レイテンシを把握することができます。当チームでは、社内共通基盤のPrometheusを利用してメトリクスを収集しています。Prometheusは通常pull型のモニタリングシステムであるため、クラスタ外からアクセスできるように設定する必要があります。そのため、専用のプロキシサーバをクラスタ内に設置し、これを経由することでクラスタ外からメトリクスを収集できるようにしています。

ここで、上記のプロキシサーバ経由でメトリクスを収集するには、対象のPodのアノテーションとして、収集用のポートやパスを設定する必要があります。そこで、IstiodやIngress/EgressGatewayに対しては直接アノテーションを付与し、Istio ProxyについてはSidecarInjectorWebhookを利用して自動的に必要なアノテーションを付与するように設定しました。設定のイメージは以下の通りです。

apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
  name: istio-operator
  namespace: istio-system
spec:
  components:
    egressGateways:
      - enabled: true
        name: istio-egressgateway
        k8s:
          podAnnotations:
            proxyJob: istio-egressgateway
            proxyPort: "15020"
            proxyPath: /stats/prometheus
    ingressGateways:
      - name: istio-ingressgateway
        k8s:
          podAnnotations:
            proxyJob: istio-ingressgateway
            proxyPort: "15020"
            proxyPath: /stats/prometheus
    pilot:
      k8s:
        podAnnotations:
          proxyJob: istiod
          proxyPort: "15014"
          proxyPath: /metrics
  values:
    sidecarInjectorWebhook:
      injectedAnnotations:
        proxyJob: istio-proxy
        proxyPort: "15020"
        proxyPath: /stats/prometheus

また、Prometheusで収集したメトリクスをGrafanaで可視化して。以下の画像の1枚目ではコントロールプレーンのリソースの使用状況を、2枚目ではサービスに流れているトラフィックの状況を可視化しています。

分散トレーシング

Istioには、Istio Proxyを経由するサービス間の通信からトレース情報を収集する仕組みがあります。社内には、ZipkinやJaegerと互換性のあるトレース情報のCollectorが共通基盤として用意されています。そこで、Istioから上記Collectorにトレース情報を送信するためにProviderやTelemetryリソースを用意しました。ProviderではCollectorのポートや対応するServiceEntry(後述)などの情報を設定しており、Telemetryリソースでは上記Collectorを利用するために必要なタグやサンプリングレートについて設定しました。

しかし、Istio単体では完全な分散トレーシングを実現できるわけではなく、アプリケーション側ではトレース情報を含むHTTPヘッダを伝播させる処理の実装が必要です。こちらについては今後対応してく予定です。

CLOVA Speechにおける活用

今回はCLOVA Speechを構成するサービスに対してIstio Proxyを有効化し、トラフィック制御やモニタリングを改善しました。その内容についてご紹介します。

Istio Proxyの導入によるgRPCの負荷分散

CLOVA Speechでは、一部のサービス間の通信でgRPCを利用しています。gRPCはHTTP/2ベースのプロトコルであり、TCPコネクションを使いまわすため、負荷分散はL7レベルで行う必要があります。しかし、KubernetesのServiceはL4レベルの負荷分散しかできません。L7の負荷分散を行うために、従来はgRPCのクライアント側のサービスごとにサイドカーとしてEnvoyをデプロイしていました。今回、Envoyの設定の管理コストを減らすため、これをIstio Proxyに置き換えました。これにより、サイドカーを一括で設定できるようになりました。

Istio Proxyを導入する際、コンテナを起動/停止する順番の問題が発生しました。サービスの起動時にIstio Proxyよりも先にサービスのコンテナが起動した場合や、サービスのGraceful Shutdownの最中にIstio Proxyが先に停止した場合、一時的にサービスへの疎通ができなくなってしまいます。そこで、Istio Proxyがサービスより先に起動し、サービスより後に停止するための設定を以下のように追加し、上記問題を解決しました。

meshConfig:
  defaultConfig:
    holdApplicationUntilProxyStarts: true  # Istio Proxyがスタートするまで、サービスのコンテナの起動を止める
    proxyMetadata:
      EXIT_ON_ZERO_ACTIVE_CONNECTIONS: "true"  # サービスからIstio ProxyへのコネクションがなくなったらIstio Proxyコンテナを停止させる

ServiceEntryの導入による外部サービスへの通信の監視

ServiceEntryはIstio内部のサービスレジストリにエントリを追加するためのリソースです。例えば、クラスタ外のサービスに関するエントリを作成することで、クラスタ外のサービスに対しても

  • Virtual ServiceやDestination Ruleを用いたトラフィック制御
  • トラフィックの監視

などを行うことができます。

CLOVA Speechでは、クラスタ外のサービスとしてRedisや複数のMySQL等を利用しています。これらに対するトラフィックを制御・監視できるようにするため、それぞれに対応するServiceEntryを作成しました。その際、複数のMySQLに対するトラフィックをIstioが区別できないという問題が発生しました。通信プロトコルがHTTPやHTTPSの場合、HostヘッダやSNIに基づいて宛先を区別することができます。一方、MySQLの場合はそれらの情報が含まれないため、IstioはポートやIPアドレスに基づいて区別しようとします。しかし、今回の場合、複数のMySQLで同じポートを使用しており、宛先を正しく区別できていませんでした。Service EntryにおいてIPアドレスを指定すれば区別は可能になりますが、MySQLのフェイルオーバー時にIPアドレスが変化する可能性があるため、これは避けたいです。

そこで、IstioのDNS Proxyという機能を利用することで、複数のMySQLに対するトラフィックを区別できるようにしました。DNS Proxyは、アプリケーションからのDNSリクエストをサイドカーにリダイレクトして、ドメインとIPアドレスのマッピング情報が存在すればそれを返すという機能です。これを利用することで、

  • kube-dnsの負荷を軽減
  • 固定のIPアドレスを持つServiceEntryの名前解決

が可能になります。さらに、DNS Proxyには、ServiceEntryに対してIstio独自の仮想的なIPアドレスを割り当てて、名前解決時にそれを返す機能があります。これにより、Istio ProxyはIPアドレスから対応するServiceEntryを特定できるようになります。DNS Proxyおよび仮想IPアドレスの自動割り当ては以下の設定で有効化しました。

meshConfig:
  defaultConfig:
    proxyMetadata:
      ISTIO_META_DNS_CAPTURE: "true"
      ISTIO_META_DNS_AUTO_ALLOCATE: "true"

今回はこの機能を有効化することで、複数のMySQLへのトラフィックを区別し、下図のようにKiali上でサービスグラフを正しく表示できるようになりました。

まとめ

今回、KubernetesクラスタにIstioを導入することで、可観測性の向上やトラフィック管理のための設定を透過的に行うことができました。期待通りの挙動とならないことも多くありましたが、ドキュメントや他の実装などを参考にしながら設定を見直すことで一つ一つ解決することができました。今回のインターン期間中では、Istioについては活用しきれていない機能も多く、一つのプロダクトに対して導入したのみでしたが、今後さらに導入を進める際の参考となればうれしいです。

本インターンに参加し、6週間という期間でたくさんの学びを得ることができました。今までは、ソースコードのコメントやGitのCommitメッセージ、ドキュメンテーションなどにはあまり意識を向けていませんでしたが、メンターやチームの方から指摘をいただいて、チーム開発での大切さを改めて認識することができました。また、Kubernetesを利用した経験がなかったため、Kubernetes自体やIstio、Prometheus等の様々な関連ツールについての学習からスタートしましたが、公式や社内のドキュメントを参照し、わからない点があってもメンターの方に相談することでスムーズに実際の導入へと移ることができました。チームでの歓迎会も開いていただき、おいしい食事をいただきながら、実際にLINEで働いている方と話すことで、経験談や考え方などを聞くことができたのも貴重な経験となりました。本インターンでの経験を今後の開発にも活かしていきたいと思います。

最後になりますが、メンターの谷川さんには、リモート勤務の中でもZoomやSlackを通じて未熟な私をサポートしていただき、大変お世話になりました。

本当にありがとうございました。