地方エンジニアの学習日記

興味ある技術の雑なメモだったりを書いてくブログ。たまに日記とガジェット紹介。

【Envoy】upstream connect error or disconnect/reset before headers. reset reason: connection termination

概要

upstream connect error or disconnect/reset before headers. reset reason: connection termination

というエラーメッセージがどこから出てくるのかを追ってみる。

結論としてはUpstreamとEnvoyのTCPコネクションがclose済みな状態(harf closeも含む)の場合に起こる(メッセージそのまま)

追ってみる

void Filter::onUpstreamReset(Http::StreamResetReason reset_reason,
                             absl::string_view transport_failure_reason,
                             UpstreamRequest& upstream_request) {
  ENVOY_STREAM_LOG(debug, "upstream reset: reset reason: {}, transport failure reason: {}",
                   *callbacks_, Http::Utility::resetReasonToString(reset_reason),
                   transport_failure_reason);

  const bool dropped = reset_reason == Http::StreamResetReason::Overflow;

  // Ignore upstream reset caused by a resource overflow.
  // Currently, circuit breakers can only produce this reset reason.
  // It means that this reason is cluster-wise, not upstream-related.
  // Therefore removing an upstream in the case of an overloaded cluster
  // would make the situation even worse.
  // https://github.com/envoyproxy/envoy/issues/25487
  if (!dropped) {
    // TODO: The reset may also come from upstream over the wire. In this case it should be
    // treated as external origin error and distinguished from local origin error.
    // This matters only when running OutlierDetection with split_external_local_origin_errors
    // config param set to true.
    updateOutlierDetection(Upstream::Outlier::Result::LocalOriginConnectFailed, upstream_request,
                           absl::nullopt);
  }

  if (maybeRetryReset(reset_reason, upstream_request, TimeoutRetry::No)) {
    return;
  }

  const Http::Code error_code = (reset_reason == Http::StreamResetReason::ProtocolError)
                                    ? Http::Code::BadGateway
                                    : Http::Code::ServiceUnavailable;
  chargeUpstreamAbort(error_code, dropped, upstream_request);
  auto request_ptr = upstream_request.removeFromList(upstream_requests_);
  callbacks_->dispatcher().deferredDelete(std::move(request_ptr));

  // If there are other in-flight requests that might see an upstream response,
  // don't return anything downstream.
  if (numRequestsAwaitingHeaders() > 0 || pending_retries_ > 0) {
    return;
  }

  const StreamInfo::CoreResponseFlag response_flags = streamResetReasonToResponseFlag(reset_reason);

  const std::string body =
      absl::StrCat("upstream connect error or disconnect/reset before headers. ",
                   (is_retry_ ? "retried and the latest " : ""),
                   "reset reason: ", Http::Utility::resetReasonToString(reset_reason),
                   !transport_failure_reason.empty() ? ", transport failure reason: " : "",
                   transport_failure_reason);
  const std::string& basic_details =
      downstream_response_started_ ? StreamInfo::ResponseCodeDetails::get().LateUpstreamReset
                                   : StreamInfo::ResponseCodeDetails::get().EarlyUpstreamReset;
  const std::string details = StringUtil::replaceAllEmptySpace(absl::StrCat(
      basic_details, "{", Http::Utility::resetReasonToString(reset_reason),
      transport_failure_reason.empty() ? "" : absl::StrCat("|", transport_failure_reason), "}"));
  onUpstreamAbort(error_code, response_flags, body, dropped, details);
}

Envoy プロキシのフィルタ内で、上流サーバーのリセットイベント(接続が切断されたり、上流のエラーが発生したときなど)に対応する Filter::onUpstreamReset メソッド。メッセージの生成はこの部分。is_retry_がtrueの場合は最新のretryの情報が出力されるようになっている。

  const std::string body =
      absl::StrCat("upstream connect error or disconnect/reset before headers. ",
                   (is_retry_ ? "retried and the latest " : ""),
                   "reset reason: ", Http::Utility::resetReasonToString(reset_reason),
                   !transport_failure_reason.empty() ? ", transport failure reason: " : "",
                   transport_failure_reason);

またリトライ可否はこのように判定している

  // We don't retry if we already started the response, don't have a retry policy defined,
  // or if we've already retried this upstream request (currently only possible if a per
  // try timeout occurred and hedge_on_per_try_timeout is enabled).
  if (downstream_response_started_ || !retry_state_ || upstream_request.retried()) {
    return false;
  }

Envoy の Filter::maybeRetryReset メソッドで、上流のリクエストがリセットされたときに、そのリクエストをリトライするかどうかを判定し、リトライが必要ならそのリクエストを実行。retry_state_ は、Envoy のリクエストごとにリトライの状態とポリシーを管理するためのオブジェクトです。このオブジェクトは、リトライが可能かどうか、何回までリトライできるか、HTTP ステータスコードやリセット理由に基づいてリトライが必要かどうかなどの情報を持っています。

呼び出し元

void UpstreamRequest::onResetStream(Http::StreamResetReason reason,
                                    absl::string_view transport_failure_reason) {
  ScopeTrackerScopeState scope(&parent_.callbacks()->scope(), parent_.callbacks()->dispatcher());

  if (span_ != nullptr) {
    // Add tags about reset.
    span_->setTag(Tracing::Tags::get().Error, Tracing::Tags::get().True);
    span_->setTag(Tracing::Tags::get().ErrorReason, Http::Utility::resetReasonToString(reason));
  }
  clearRequestEncoder();
  awaiting_headers_ = false;

  stream_info_.setResponseFlag(Filter::streamResetReasonToResponseFlag(reason));
  parent_.onUpstreamReset(reason, transport_failure_reason, *this);
}

さらに呼び出しもと。

void TcpUpstream::onUpstreamData(Buffer::Instance& data, bool end_stream) {
  // In the TCP proxy case the filter manager used to trigger the full stream closure when the
  // upstream server half closed its end of the TCP connection. With the
  // allow_multiplexed_upstream_half_close enabled filter manager no longer closes stream that were
  // half closed by upstream before downstream. To keep the behavior the same for TCP proxy the
  // upstream force closes the connection when server half closes.
  //
  // Save the indicator to close the stream before calling the decodeData since when the
  // allow_multiplexed_upstream_half_close is false the call to decodeHeader with end_stream==true
  // will delete the TcpUpstream object.
  // NOTE: it this point Envoy can not support half closed TCP upstream as there is currently no
  // distinction between half closed vs fully closed TCP peers.
  const bool force_reset =
      force_reset_on_upstream_half_close_ && end_stream && !downstream_complete_;
  bytes_meter_->addWireBytesReceived(data.length());
  upstream_request_->decodeData(data, end_stream);
  // force_reset is true only when allow_multiplexed_upstream_half_close is true and in this case
  // the decodeData will never cause the stream to be closed and as such it safe to access
  // upstream_request_
  if (force_reset && upstream_request_) {
    upstream_request_->onResetStream(Envoy::Http::StreamResetReason::ConnectionTermination,
                                     "half_close_initiated_full_close");
  }
}

Envoy の TCP プロキシフィルタで、上流サーバーからのデータを受信したときに呼び出される TcpUpstream::onUpstreamData メソッドです。このメソッドは、上流からのデータを処理する際に接続の半閉じ状態(half close)をどのように扱うかを制御しています。

const bool force_reset =
    force_reset_on_upstream_half_close_ && end_stream && !downstream_complete_;
  • force_reset_on_upstream_half_close_ は、上流接続の半閉じ(クライアントが shutdown(SHUT_WR) で書き込みを閉じた場合など)を検出した際に接続を強制的に閉じるべきかどうかを制御するフラグ
  • end_stream が true の場合は、上流から送信されたデータの終端を示す
  • downstream_complete_ が false のとき、下流接続はまだ開かれているため、半閉じの可能性があることを意味する

コメントのあたりは要約すると

  • 通常、TCP プロキシでは、上流サーバーが半閉じ状態になると、フィルターマネージャがストリームを完全に閉じるようトリガーを発する
  • allow_multiplexed_upstream_half_close オプションが有効な場合、上流が半閉じでも下流接続を即座に閉じずにデータ転送を続行できるようにする
  • Envoy は、現在の実装では「半閉じ」と「完全閉じ」を明確に区別できないため、半閉じを検出すると接続を強制終了する