トラフィック管理の問題

リクエストがEnvoyによって拒否される

リクエストは様々な理由で拒否される可能性があります。リクエストが拒否される理由を理解する最善の方法は、Envoyのアクセスログを検査することです。デフォルトでは、アクセスログはコンテナの標準出力に出力されます。次のコマンドを実行してログを確認してください。

$ kubectl logs PODNAME -c istio-proxy -n NAMESPACE

デフォルトのアクセスログフォーマットでは、Envoyレスポンスフラグはレスポンスコードの後に配置されます。カスタムログフォーマットを使用している場合は、%RESPONSE_FLAGS%を含めるようにしてください。

レスポンスフラグの詳細については、Envoyレスポンスフラグを参照してください。

一般的なレスポンスフラグは次のとおりです。

  • NR: ルートが構成されていません。DestinationRuleまたはVirtualServiceを確認してください。
  • UO: サーキットブレーキングによるアップストリームオーバーフロー。DestinationRuleのサーキットブレーカー構成を確認してください。
  • UF: 上流への接続に失敗しました。Istio認証を使用している場合は、相互TLS設定の競合を確認してください。

ルーティングルールがトラフィックフローに影響しないように見える

現在のEnvoyサイドカー実装では、重み付けされたバージョン配布が適用されるまでに、最大100個の要求が必要になる場合があります。

Bookinfoサンプルでルートルールが完全に機能しているのに、同様のバージョンルーティングルールが独自のアプリケーションに影響を与えない場合は、Kubernetesサービスを少し変更する必要がある可能性があります。Kubernetesサービスは、IstioのL7ルーティング機能を利用するために、特定の制限に従う必要があります。詳細については、Podとサービスの要件を参照してください。

もう1つの潜在的な問題は、ルートルールが単に適用されるのが遅い可能性があることです。Kubernetes上のIstio実装は、最終的に整合性のあるアルゴリズムを使用して、すべてのEnvoyサイドカーがすべてのルートルールを含む正しい構成を持っていることを保証します。構成の変更は、すべてのサイドカーに伝播するまでに時間がかかります。大規模なデプロイメントでは、伝播に時間がかかり、数秒程度の遅延が発生する可能性があります。

デスティネーションルールの設定後の503エラー

DestinationRuleを適用した後、サービスへの要求がすぐにHTTP 503エラーを生成し始め、DestinationRuleを削除または元に戻すまでエラーが続く場合は、DestinationRuleがサービスのTLS競合を引き起こしている可能性があります。

たとえば、クラスタ全体で相互TLSを設定する場合は、DestinationRuleに次のtrafficPolicyを含める必要があります。

trafficPolicy:
  tls:
    mode: ISTIO_MUTUAL

それ以外の場合は、モードはDISABLEにデフォルトになり、クライアントプロキシサイドカーはTLS暗号化されたリクエストではなく、プレーンHTTPリクエストを行うようになります。そのため、サーバープロキシが暗号化されたリクエストを期待しているため、リクエストはサーバープロキシと競合します。

DestinationRuleを適用する際は、常にtrafficPolicyのTLSモードがグローバルサーバー構成と一致していることを確認してください。

ルーティングルールがイングレスゲートウェイのリクエストに影響しない

内部サービスにアクセスするために、イングレスGatewayと対応するVirtualServiceを使用していると仮定しましょう。たとえば、あなたのVirtualServiceは次のようになります。

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: myapp
spec:
  hosts:
  - "myapp.com" # or maybe "*" if you are testing without DNS using the ingress-gateway IP (e.g., http://1.2.3.4/hello)
  gateways:
  - myapp-gateway
  http:
  - match:
    - uri:
        prefix: /hello
    route:
    - destination:
        host: helloworld.default.svc.cluster.local
  - match:
    ...

また、helloworldサービスのトラフィックを特定のサブセットにルーティングするVirtualServiceもあります。

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: helloworld
spec:
  hosts:
  - helloworld.default.svc.cluster.local
  http:
  - route:
    - destination:
        host: helloworld.default.svc.cluster.local
        subset: v1

この状況では、イングレスゲートウェイを介したhelloworldサービスへの要求はサブセットv1に送信されず、代わりにデフォルトのラウンドロビンルーティングを引き続き使用することに気付くでしょう。

イングレスリクエストはゲートウェイホスト(例:myapp.com)を使用しており、helloworldサービスの任意のエンドポイントにルーティングするmyappVirtualService内のルールをアクティブ化します。ホストhelloworld.default.svc.cluster.localを持つ内部リクエストのみが、トラフィックをサブセットv1のみに送信するhelloworldVirtualServiceを使用します。

ゲートウェイからのトラフィックを制御するには、myappVirtualServiceにもサブセットルールを含める必要があります。

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: myapp
spec:
  hosts:
  - "myapp.com" # or maybe "*" if you are testing without DNS using the ingress-gateway IP (e.g., http://1.2.3.4/hello)
  gateways:
  - myapp-gateway
  http:
  - match:
    - uri:
        prefix: /hello
    route:
    - destination:
        host: helloworld.default.svc.cluster.local
        subset: v1
  - match:
    ...

あるいは、可能であれば、2つのVirtualServiceを1つのユニットに結合することもできます。

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: myapp
spec:
  hosts:
  - myapp.com # cannot use "*" here since this is being combined with the mesh services
  - helloworld.default.svc.cluster.local
  gateways:
  - mesh # applies internally as well as externally
  - myapp-gateway
  http:
  - match:
    - uri:
        prefix: /hello
      gateways:
      - myapp-gateway #restricts this rule to apply only to ingress gateway
    route:
    - destination:
        host: helloworld.default.svc.cluster.local
        subset: v1
  - match:
    - gateways:
      - mesh # applies to all services inside the mesh
    route:
    - destination:
        host: helloworld.default.svc.cluster.local
        subset: v1

Envoyが負荷下でクラッシュする

ulimit -aを確認してください。多くのシステムでは、デフォルトで1024のファイル記述子上限があり、これによりEnvoyがアサートしてクラッシュします。

[2017-05-17 03:00:52.735][14236][critical][assert] assert failure: fd_ != -1: external/envoy/source/common/network/connection_impl.cc:58

ulimitを上げるようにしてください。例:ulimit -n 16384

Envoyが私のHTTP/1.0サービスに接続しない

Envoyは、上流サービスにHTTP/1.1またはHTTP/2トラフィックを必要とします。たとえば、NGINXをEnvoyの背後でトラフィックを提供するために使用する場合、NGINXのデフォルトは1.0であるため、NGINX構成でproxy_http_versionディレクティブを「1.1」に設定する必要があります。

構成例

upstream http_backend {
    server 127.0.0.1:8080;

    keepalive 16;
}

server {
    ...

    location /http/ {
        proxy_pass http://http_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        ...
    }
}

ヘッドレスサービスへのアクセス時の503エラー

Istioが次の構成でインストールされていると仮定します。

  • メッシュ内でmTLSモードSTRICTに設定されています。
  • meshConfig.outboundTrafficPolicy.modeALLOW_ANYに設定されています。

nginxがデフォルト名前空間にStatefulSetとしてデプロイされ、以下に示すように対応するHeadless Serviceが定義されているとします。

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: http-web  # Explicitly defining an http port
  clusterIP: None   # Creates a Headless Service
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  selector:
    matchLabels:
      app: nginx
  serviceName: "nginx"
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: registry.k8s.io/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web

Service定義のポート名http-webは、そのポートのプロトコルとしてhttpを明示的に指定しています。

デフォルト名前空間にcurl pod Deploymentもあると仮定します。このcurl podからPod IPを使用してnginxにアクセスする場合(これはheadlessサービスにアクセスする一般的な方法の1つです)、リクエストはPassthroughClusterを介してサーバーサイドに渡されますが、サーバーサイドのサイドカープロキシはnginxへのルートエントリが見つからず、HTTP 503 UCで失敗します。

$ export SOURCE_POD=$(kubectl get pod -l app=curl -o jsonpath='{.items..metadata.name}')
$ kubectl exec -it $SOURCE_POD -c curl -- curl 10.1.1.171 -s -o /dev/null -w "%{http_code}"
  503

10.1.1.171nginxのレプリカの1つのPod IPであり、サービスはcontainerPort 80でアクセスされます。

この503エラーを回避する方法はいくつかあります。

  1. 正しいHostヘッダーを指定する

    上記のcurlリクエストのHostヘッダーは、デフォルトでPod IPになります。nginxへのリクエストでHostヘッダーをnginx.defaultとして指定すると、HTTP 200 OKが正常に返されます。

    $ export SOURCE_POD=$(kubectl get pod -l app=curl -o jsonpath='{.items..metadata.name}')
    $ kubectl exec -it $SOURCE_POD -c curl -- curl -H "Host: nginx.default" 10.1.1.171 -s -o /dev/null -w "%{http_code}"
      200
    
  2. ポート名をtcpまたはtcp-webまたはtcp-<custom_name>に設定する

    ここでは、プロトコルがtcpとして明示的に指定されています。この場合、クライアント側とサーバー側の両方で、サイドカープロキシのTCP Proxyネットワークフィルターのみが使用されます。HTTP Connection Managerはまったく使用されないため、リクエストにはヘッダーは必要ありません。

    Hostヘッダーを明示的に設定する場合としない場合でも、nginxへのリクエストはHTTP 200 OKを正常に返します。

    これは、クライアントがリクエストにヘッダー情報を含めることができない特定のシナリオで役立ちます。

    $ export SOURCE_POD=$(kubectl get pod -l app=curl -o jsonpath='{.items..metadata.name}')
    $ kubectl exec -it $SOURCE_POD -c curl -- curl 10.1.1.171 -s -o /dev/null -w "%{http_code}"
      200
    
    $ kubectl exec -it $SOURCE_POD -c curl -- curl -H "Host: nginx.default" 10.1.1.171 -s -o /dev/null -w "%{http_code}"
      200
    
  3. Pod IPの代わりにドメイン名を使用する

    headlessサービスの特定のインスタンスには、ドメイン名だけでアクセスすることもできます。

    $ export SOURCE_POD=$(kubectl get pod -l app=curl -o jsonpath='{.items..metadata.name}')
    $ kubectl exec -it $SOURCE_POD -c curl -- curl web-0.nginx.default -s -o /dev/null -w "%{http_code}"
      200
    

    ここでは、web-0nginxの3つのレプリカの1つのpod名です。

headlessサービスとさまざまなプロトコルのトラフィックルーティング動作に関する追加情報については、このトラフィックルーティングページを参照してください。

TLS構成ミス

多くのトラフィック管理の問題は、不適切なTLS設定によって発生します。次のセクションでは、最も一般的な誤設定の一部について説明します。

HTTPポートへのHTTPSの送信

アプリケーションがHTTPとして宣言されたサービスにHTTPSリクエストを送信すると、Envoyサイドカーはリクエストを転送中にHTTPとして解析しようとしますが、HTTPが予期せず暗号化されているため失敗します。

apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: httpbin
spec:
  hosts:
  - httpbin.org
  ports:
  - number: 443
    name: http
    protocol: HTTP
  resolution: DNS

ポート443で意図的にプレーンテキストを送信する場合(例:curl http://httpbin.org:443)、上記の構成は正しい場合がありますが、一般的にポート443はHTTPSトラフィック専用です。

デフォルトでポート443を使用するcurl https://httpbin.orgのようなHTTPSリクエストを送信すると、curl: (35) error:1408F10B:SSL routines:ssl3_get_record:wrong version numberのようなエラーが発生します。アクセスログにも400 DPEのようなエラーが表示される場合があります。

これを修正するには、ポートプロトコルをHTTPSに変更する必要があります。

spec:
  ports:
  - number: 443
    name: https
    protocol: HTTPS

ゲートウェイから仮想サービスへのTLSの不一致

仮想サービスをゲートウェイにバインドする際に発生する可能性のある2つの一般的なTLSミスマッチがあります。

  1. ゲートウェイがTLSを終了し、仮想サービスがTLSルーティングを設定します。
  2. ゲートウェイがTLSパススルーを実行し、仮想サービスがHTTPルーティングを設定します。

TLS終端付きゲートウェイ

apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 443
      name: https
      protocol: HTTPS
    hosts:
      - "*"
    tls:
      mode: SIMPLE
      credentialName: sds-credential
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: httpbin
spec:
  hosts:
  - "*.example.com"
  gateways:
  - istio-system/gateway
  tls:
  - match:
    - sniHosts:
      - "*.example.com"
    route:
    - destination:
        host: httpbin.org

この例では、ゲートウェイはTLSを終了しています(ゲートウェイのtls.mode構成はSIMPLEであり、PASSTHROUGHではありません)。仮想サービスはTLSベースのルーティングを使用しています。ルーティングルールの評価は、ゲートウェイがTLSを終了した後に発生するため、リクエストはHTTPSではなくHTTPになるため、TLSルールは効果がありません。

この誤設定では、リクエストはHTTPルーティングに送信されますが、HTTPルートが構成されていないため、404応答が返されます。これは、istioctl proxy-config routesコマンドを使用して確認できます。

この問題を解決するには、仮想サービスを切り替えてtlsではなくhttpルーティングを指定する必要があります。

spec:
  ...
  http:
  - match:
    - headers:
        ":authority":
          regex: "*.example.com"

TLSパススルー付きゲートウェイ

apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - hosts:
    - "*"
    port:
      name: https
      number: 443
      protocol: HTTPS
    tls:
      mode: PASSTHROUGH
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: virtual-service
spec:
  gateways:
  - gateway
  hosts:
  - httpbin.example.com
  http:
  - route:
    - destination:
        host: httpbin.org

この構成では、仮想サービスはゲートウェイを介して渡されたTLSトラフィックに対してHTTPトラフィックを照合しようとします。これにより、仮想サービス構成は効果がなくなります。istioctl proxy-config listenerおよびistioctl proxy-config routeコマンドを使用して、HTTPルートが適用されていないことを確認できます。

これを修正するには、仮想サービスを切り替えてtlsルーティングを設定する必要があります。

spec:
  tls:
  - match:
    - sniHosts: ["httpbin.example.com"]
    route:
    - destination:
        host: httpbin.org

あるいは、ゲートウェイのtls構成を切り替えることで、TLSをパススルーするのではなく終了させることもできます。

spec:
  ...
    tls:
      credentialName: sds-credential
      mode: SIMPLE

ダブルTLS(TLSリクエストに対するTLS発信)

TLSオリジネーションを実行するようにIstioを構成する場合は、アプリケーションがプレーンテキストのリクエストをサイドカーに送信し、サイドカーがTLSを開始することを確認する必要があります。

次のDestinationRuleは、httpbin.orgサービスへのリクエストに対してTLSを生成しますが、対応するServiceEntryはポート443のプロトコルをHTTPSとして定義しています。

apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: httpbin
spec:
  hosts:
  - httpbin.org
  ports:
  - number: 443
    name: https
    protocol: HTTPS
  resolution: DNS
---
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: originate-tls
spec:
  host: httpbin.org
  trafficPolicy:
    tls:
      mode: SIMPLE

この構成では、サイドカーはアプリケーションがポート443でTLSトラフィックを送信することを期待しています(例:curl https://httpbin.org)。しかし、リクエストを転送する前にTLSオリジネーションも行います。これにより、リクエストが二重に暗号化されます。

たとえば、curl https://httpbin.orgのようなリクエストを送信すると、エラーが発生します:(35) error:1408F10B:SSL routines:ssl3_get_record:wrong version number

この例を修正するには、ServiceEntryでポートプロトコルをHTTPに変更します。

spec:
  hosts:
  - httpbin.org
  ports:
  - number: 443
    name: http
    protocol: HTTP

この構成では、アプリケーションはポート443にプレーンテキストのリクエストを送信する必要があります(例:curl http://httpbin.org:443)。なぜなら、TLSオリジネーションはポートを変更しないからです。ただし、Istio 1.8以降では、アプリケーションにHTTPポート80を公開し(例:curl http://httpbin.org)、TLSオリジネーションのためにリクエストをtargetPort 443にリダイレクトできます。

spec:
  hosts:
  - httpbin.org
  ports:
  - number: 80
    name: http
    protocol: HTTP
    targetPort: 443

同じTLS証明書で構成された複数のゲートウェイが構成されている場合に404エラーが発生する

HTTP/2接続の再利用を利用するブラウザ(つまり、ほとんどのブラウザ)では、別のホストへの接続が既に確立された後に2番目のホストにアクセスすると、同じTLS証明書を使用して複数のゲートウェイを構成すると、404エラーが発生します。

たとえば、次のように同じTLS証明書を共有する2つのホストがあるとします。

  • ワイルドカード証明書*.test.comistio-ingressgatewayにインストールされています。
  • ホストservice1.test.com、セレクターistio: ingressgateway、およびゲートウェイのマウントされた(ワイルドカード)証明書を使用したTLSを持つGateway構成gw1
  • ホストservice2.test.com、セレクターistio: ingressgateway、およびゲートウェイのマウントされた(ワイルドカード)証明書を使用したTLSを持つGateway構成gw2
  • ホスト名service1.test.com、ゲートウェイgw1を持つVirtualService設定vs1
  • ホスト名service2.test.com、ゲートウェイgw2を持つVirtualService設定vs2

両方のゲートウェイが同じワークロード(セレクタistio: ingressgateway)によって提供されているため、両方のサービス(service1.test.comservice2.test.com)へのリクエストは同じIPアドレスに解決されます。最初にservice1.test.comにアクセスすると、ワイルドカード証明書(*.test.com)が返され、service2.test.comへの接続にも同じ証明書を使用できることが示されます。ChromeやFirefoxなどのブラウザは、その結果、service2.test.comへのリクエストに対して既存の接続を再利用します。ゲートウェイ(gw1)にservice2.test.comのルートがないため、404(Not Found)応答が返されます。

この問題を回避するには、2つのゲートウェイ(gw1gw2)ではなく、単一のワイルドカードGatewayを設定します。その後、次のように両方のVirtualServicesをそれにバインドします。

  • ホスト名*.test.com、セレクタistio: ingressgateway、ゲートウェイにマウントされた(ワイルドカード)証明書を使用したTLSを持つGateway設定gw
  • ホスト名service1.test.com、ゲートウェイgwを持つVirtualService設定vs1
  • ホスト名service2.test.com、ゲートウェイgwを持つVirtualService設定vs2

SNIを送信していない場合のSNIルーティングの構成

hostsフィールドを指定したHTTPS Gatewayは、受信リクエストに対してSNIマッチを実行します。例えば、次の設定では、SNIに*.example.comが一致するリクエストのみが許可されます。

servers:
- port:
    number: 443
    name: https
    protocol: HTTPS
  hosts:
  - "*.example.com"

これにより、一部のリクエストが失敗することがあります。

例えば、DNSを設定しておらず、代わりにホストヘッダーを直接設定している場合(例:curl 1.2.3.4 -H "Host: app.example.com")、SNIは設定されず、リクエストは失敗します。代わりに、DNSを設定するか、curl--resolveフラグを使用してください。詳細については、「セキュアなゲートウェイ」のタスクを参照してください。

もう1つのよくある問題は、Istioの前にロードバランサーがある場合です。ほとんどのクラウドロードバランサーはSNIを転送しないため、クラウドロードバランサーでTLSを終了している場合は、次のいずれかを行う必要があります。

  • クラウドロードバランサーを設定して、TLS接続をパススルーするようにします。
  • hostsフィールドを*に設定して、GatewayでのSNIマッチングを無効にします。

この一般的な症状としては、実際のトラフィックが失敗する一方で、ロードバランサーのヘルスチェックは成功することです。

変更されていないEnvoyフィルター構成が突然動作しなくなる

他のフィルタに対する挿入位置を指定するEnvoyFilter設定は、デフォルトでは評価順序がフィルタの作成時間に基づいているため、非常に脆弱になる可能性があります。次の仕様のフィルタを考えてみましょう。

spec:
  configPatches:
  - applyTo: NETWORK_FILTER
    match:
      context: SIDECAR_OUTBOUND
      listener:
        portNumber: 443
        filterChain:
          filter:
            name: istio.stats
    patch:
      operation: INSERT_BEFORE
      value:
        ...

このフィルタ設定が正しく動作するには、istio.statsフィルタの作成時間がそれよりも古い必要があります。そうでない場合、INSERT_BEFORE操作はサイレントに無視されます。このフィルタがチェーンに追加されていないことを示すエラーログは何もありません。

これは、istio.statsのような、バージョン固有の(つまり、マッチング基準にproxyVersionフィールドが含まれている)フィルタをマッチングする場合に特に問題になります。このようなフィルタは、Istioをアップグレードすると、新しいフィルタによって削除または置き換えられる可能性があります。その結果、上記のようなEnvoyFilterは最初は完璧に動作しているかもしれませんが、Istioを新しいバージョンにアップグレードした後、サイドカーのネットワークフィルタチェーンに含まれなくなります。

この問題を回避するには、他のフィルタの存在に依存しない操作(例:INSERT_FIRST)に変更するか、EnvoyFilterに明示的な優先順位を設定して、デフォルトの作成時間に基づく順序付けを上書きします。例えば、上記のフィルタにpriority: 10を追加すると、デフォルトの優先順位が0であるistio.statsフィルタの後で処理されることが保証されます。

フォールトインジェクションとリトライ/タイムアウトポリシーを持つ仮想サービスが期待通りに動作しない

現在、Istioでは、同じVirtualServiceにフォールトインジェクションとリトライまたはタイムアウトポリシーを設定することはサポートされていません。次の設定を考えてみましょう。

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: helloworld
spec:
  hosts:
    - "*"
  gateways:
  - helloworld-gateway
  http:
  - match:
    - uri:
        exact: /hello
    fault:
      abort:
        httpStatus: 500
        percentage:
          value: 50
    retries:
      attempts: 5
      retryOn: 5xx
    route:
    - destination:
        host: helloworld
        port:
          number: 5000

5回の再試行が設定されているため、ユーザーはhelloworldサービスを呼び出すときにエラーをほとんど見ないことが予想されます。しかし、フォールトと再試行の両方が同じVirtualServiceに設定されているため、再試行設定は有効にならず、失敗率は50%になります。この問題を回避するために、VirtualServiceからフォールト設定を削除し、代わりにEnvoyFilterを使用してアップストリームEnvoyプロキシにフォールトを注入することができます。

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: hello-world-filter
spec:
  workloadSelector:
    labels:
      app: helloworld
  configPatches:
  - applyTo: HTTP_FILTER
    match:
      context: SIDECAR_INBOUND # will match outbound listeners in all sidecars
      listener:
        filterChain:
          filter:
            name: "envoy.filters.network.http_connection_manager"
    patch:
      operation: INSERT_BEFORE
      value:
        name: envoy.fault
        typed_config:
          "@type": "type.googleapis.com/envoy.extensions.filters.http.fault.v3.HTTPFault"
          abort:
            http_status: 500
            percentage:
              numerator: 50
              denominator: HUNDRED

これは、このようにすることで、クライアントプロキシにリトライポリシーが設定され、アップストリームプロキシにフォールトインジェクションが設定されるためです。

この情報は役に立ちましたか?
改善のための提案はありますか?

ご意見ありがとうございます!