Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1565ef0
Add retryable http health check statuses.
wez470 Sep 1, 2021
f631c7f
Replace retryable with retriable.
wez470 Sep 1, 2021
981b5f2
Fix typo.
wez470 Sep 1, 2021
edbe062
Add http unhealthy threshold integration test.
wez470 Sep 2, 2021
9646866
Fix api docs references.
wez470 Sep 2, 2021
3f7db44
Add version history doc.
wez470 Sep 2, 2021
f4295fb
Add in http health check range tests.
wez470 Sep 2, 2021
ab37ada
Wait for counter/guages in test.
wez470 Sep 3, 2021
239f73a
Move API field.
wez470 Sep 7, 2021
4a27d0a
Kick CI
wez470 Sep 7, 2021
0366c98
Update docs. Add integration test.
wez470 Sep 7, 2021
1176ebc
Kick CI
wez470 Sep 8, 2021
66d4129
Merge remote-tracking branch 'upstream/main' into retryable-http-heal…
wez470 Sep 8, 2021
c7e875e
Refactor range check funcs.
wez470 Sep 9, 2021
889adec
Pass proper range to in ranges func.
wez470 Sep 9, 2021
70714be
Remove uneeded include.
wez470 Sep 9, 2021
431ec59
Bring back public range funcs.
wez470 Sep 10, 2021
6a5ebf4
Add comment to boolean param usage.
wez470 Sep 10, 2021
d8a8f30
Kick CI
wez470 Sep 10, 2021
94d04d2
Kick CI
wez470 Sep 10, 2021
fdc5eae
Merge remote-tracking branch 'upstream/main' into retryable-http-heal…
wez470 Sep 13, 2021
565e42e
Refactor range validation checks.
wez470 Sep 14, 2021
e3e5aae
Kick CI
wez470 Sep 14, 2021
eefbf9f
Kick CI
wez470 Sep 14, 2021
2c70a2a
Kick CI
wez470 Sep 14, 2021
2cada4d
Merge remote-tracking branch 'upstream/main' into retryable-http-heal…
wez470 Sep 14, 2021
52bcc5f
Update docs.
wez470 Sep 21, 2021
37c41e8
Upgrade const methods to static.
wez470 Sep 24, 2021
6d80517
Merge remote-tracking branch 'upstream/main' into retryable-http-heal…
wez470 Sep 24, 2021
6a5269e
Kick CI
wez470 Sep 24, 2021
5599a92
Merge remote-tracking branch 'upstream/main' into retryable-http-heal…
wez470 Oct 6, 2021
974191a
Fix merge of version history.
wez470 Oct 6, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions api/envoy/config/core/v3/health_check.proto
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ message HealthCheck {
}
}

// [#next-free-field: 12]
// [#next-free-field: 13]
message HttpHealthCheck {
option (udpa.annotations.versioning).previous_message_type =
"envoy.api.v2.core.HealthCheck.HttpHealthCheck";
Expand Down Expand Up @@ -118,6 +118,17 @@ message HealthCheck {
// range are required. Only statuses in the range [100, 600) are allowed.
repeated type.v3.Int64Range expected_statuses = 9;

// Specifies a list of HTTP response statuses considered retriable. If provided, responses in this range
// will count towards the configured :ref:`unhealthy_threshold <envoy_v3_api_field_config.core.v3.HealthCheck.unhealthy_threshold>`,
// and will not result in the host being considered immediately unhealthy
Comment thread
wez470 marked this conversation as resolved.
Outdated
// (By default all responses not in :ref:`expected_statuses <envoy_v3_api_field_config.core.v3.HealthCheck.HttpHealthCheck.expected_statuses>`
Comment thread
wez470 marked this conversation as resolved.
Outdated
// will result in the host being considered immediately unhealthy). Ranges follow half-open semantics of
// :ref:`Int64Range <envoy_v3_api_msg_type.v3.Int64Range>`. The start and end of each range are required.
// Only statuses in the range [100, 600) are allowed. The :ref:`expected_statuses <envoy_v3_api_field_config.core.v3.HealthCheck.HttpHealthCheck.expected_statuses>`
// field takes precedence for any range overlaps with this field i.e. if status code 200 is both retriable and expected, a 200 response will
// be considered a successful health check.
repeated type.v3.Int64Range retriable_statuses = 12;

// Use specified application protocol for health checks.
type.v3.CodecClientType codec_client_type = 10 [(validate.rules).enum = {defined_only: true}];

Expand Down Expand Up @@ -243,8 +254,10 @@ message HealthCheck {
uint32 interval_jitter_percent = 18;

// The number of unhealthy health checks required before a host is marked
// unhealthy. Note that for *http* health checking if a host responds with 503
// this threshold is ignored and the host is considered unhealthy immediately.
// unhealthy. Note that for *http* health checking if a host responds with a code not in
// :ref:`expected_statuses <envoy_v3_api_field_config.core.v3.HealthCheck.HttpHealthCheck.expected_statuses>`
// or :ref:`retriable_statuses <envoy_v3_api_field_config.core.v3.HealthCheck.HttpHealthCheck.retriable_statuses>`,
// this threshold is ignored and the host is considered immediately unhealthy.
google.protobuf.UInt32Value unhealthy_threshold = 4 [(validate.rules).message = {required: true}];

// The number of healthy health checks required before a host is marked
Expand Down
6 changes: 3 additions & 3 deletions docs/root/intro/arch_overview/upstream/health_checking.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ checking along with various settings (check interval, failures required before m
unhealthy, successes required before marking a host healthy, etc.):

* **HTTP**: During HTTP health checking Envoy will send an HTTP request to the upstream host. By
default, it expects a 200 response if the host is healthy. Expected response codes are
default, it expects a 200 response if the host is healthy. Expected and retriable response codes are
:ref:`configurable <envoy_v3_api_msg_config.core.v3.HealthCheck.HttpHealthCheck>`. The
upstream host can return 503 if it wants to immediately notify downstream hosts to no longer
forward traffic to it.
upstream host can return a non-expected or non-retriable status code (any non-200 code by default) if
it wants to immediately notify downstream hosts to no longer forward traffic to it.
* **L3/L4**: During L3/L4 health checking, Envoy will send a configurable byte buffer to the
upstream host. It expects the byte buffer to be echoed in the response if the host is to be
considered healthy. Envoy also supports connect only L3/L4 health checking.
Expand Down
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ New Features
* grpc reverse bridge: added a new :ref:`option <envoy_v3_api_field_extensions.filters.http.grpc_http1_reverse_bridge.v3.FilterConfig.response_size_header>` to support streaming response bodies when withholding gRPC frames from the upstream.
* http: added :ref:`string_match <envoy_v3_api_field_config.route.v3.HeaderMatcher.string_match>` in the header matcher.
* http: added support for :ref:`max_requests_per_connection <envoy_v3_api_field_config.core.v3.HttpProtocolOptions.max_requests_per_connection>` for both upstream and downstream connections.
* http: added support for :ref:`retriable health check status codes <envoy_v3_api_field_config.core.v3.HealthCheck.HttpHealthCheck.retriable_statuses>`.
* http: sanitizing the referer header as documented :ref:`here <config_http_conn_man_headers_referer>`. This feature can be temporarily turned off by setting runtime guard ``envoy.reloadable_features.sanitize_http_header_referer`` to false.
* jwt_authn: added support for :ref:`Jwt Cache <envoy_v3_api_field_extensions.filters.http.jwt_authn.v3.JwtProvider.jwt_cache_config>` and its size can be specified by :ref:`jwt_cache_size <envoy_v3_api_field_extensions.filters.http.jwt_authn.v3.JwtCacheConfig.jwt_cache_size>`.
* listener: new listener metric ``downstream_cx_transport_socket_connect_timeout`` to track transport socket timeouts.
Expand Down
19 changes: 16 additions & 3 deletions generated_api_shadow/envoy/config/core/v3/health_check.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 7 additions & 6 deletions source/common/upstream/health_checker_base_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ void HealthCheckerImplBase::setUnhealthyCrossThread(const HostSharedPtr& host,
return;
}

session->second->setUnhealthy(envoy::data::core::v3::PASSIVE);
session->second->setUnhealthy(envoy::data::core::v3::PASSIVE, false);
});
}

Expand Down Expand Up @@ -329,13 +329,14 @@ bool networkHealthCheckFailureType(envoy::data::core::v3::HealthCheckFailureType
} // namespace

HealthTransition HealthCheckerImplBase::ActiveHealthCheckSession::setUnhealthy(
envoy::data::core::v3::HealthCheckFailureType type) {
envoy::data::core::v3::HealthCheckFailureType type, bool retriable) {
// If we are unhealthy, reset the # of healthy to zero.
num_healthy_ = 0;

HealthTransition changed_state = HealthTransition::Unchanged;
if (!host_->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)) {
if (!networkHealthCheckFailureType(type) || ++num_unhealthy_ == parent_.unhealthy_threshold_) {
if ((!networkHealthCheckFailureType(type) && !retriable) ||
++num_unhealthy_ == parent_.unhealthy_threshold_) {
host_->healthFlagSet(Host::HealthFlag::FAILED_ACTIVE_HC);
parent_.decHealthy();
changed_state = HealthTransition::Changed;
Expand Down Expand Up @@ -376,8 +377,8 @@ HealthTransition HealthCheckerImplBase::ActiveHealthCheckSession::setUnhealthy(
}

void HealthCheckerImplBase::ActiveHealthCheckSession::handleFailure(
envoy::data::core::v3::HealthCheckFailureType type) {
HealthTransition changed_state = setUnhealthy(type);
envoy::data::core::v3::HealthCheckFailureType type, bool retriable) {
HealthTransition changed_state = setUnhealthy(type, retriable);
// It's possible that the previous call caused this session to be deferred deleted.
if (timeout_timer_ != nullptr) {
timeout_timer_->disableTimer();
Expand All @@ -392,7 +393,7 @@ HealthTransition
HealthCheckerImplBase::ActiveHealthCheckSession::clearPendingFlag(HealthTransition changed_state) {
if (host_->healthFlagGet(Host::HealthFlag::PENDING_ACTIVE_HC)) {
host_->healthFlagClear(Host::HealthFlag::PENDING_ACTIVE_HC);
// Even though the health value of the host might have not changed, we set this to Changed to
// Even though the health value of the host might have not changed, we set this to Changed so
// that the cluster can update its list of excluded hosts.
return HealthTransition::Changed;
}
Expand Down
5 changes: 3 additions & 2 deletions source/common/upstream/health_checker_base_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ class HealthCheckerImplBase : public HealthChecker,
class ActiveHealthCheckSession : public Event::DeferredDeletable {
public:
~ActiveHealthCheckSession() override;
HealthTransition setUnhealthy(envoy::data::core::v3::HealthCheckFailureType type);
HealthTransition setUnhealthy(envoy::data::core::v3::HealthCheckFailureType type,
bool retriable);
Comment on lines +79 to +80

@wbpcode wbpcode Sep 14, 2021

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compared to adding a boolean parameter, can we add a new HealthCheckFailure value Retriable? This is a sudden idea, as a reference.

@wez470 wez470 Sep 14, 2021

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could. That will also affect the failure_type displayed in the health event log (Unless we do some translation to make RETRIABLE -> ACTIVE). I had decided initially to go with the boolean to reduce the impact of this change in that regard, but we can certainly add a new type if desired.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting point. I think the boolean expresses the API nicely because the failure type is still ACTIVE, it just happens to be retriable.

void onDeferredDeleteBase();
void start() { onInitialInterval(); }

Expand All @@ -85,7 +86,7 @@ class HealthCheckerImplBase : public HealthChecker,

void handleSuccess(bool degraded = false);
void handleDegraded();
void handleFailure(envoy::data::core::v3::HealthCheckFailureType type);
void handleFailure(envoy::data::core::v3::HealthCheckFailureType type, bool retriable = false);

HostSharedPtr host_;

Expand Down
63 changes: 55 additions & 8 deletions source/common/upstream/health_checker_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ HttpHealthCheckerImpl::HttpHealthCheckerImpl(const Cluster& cluster,
Router::HeaderParser::configure(config.http_health_check().request_headers_to_add(),
config.http_health_check().request_headers_to_remove())),
http_status_checker_(config.http_health_check().expected_statuses(),
config.http_health_check().retriable_statuses(),
static_cast<uint64_t>(Http::Code::OK)),
codec_client_type_(codecClientType(config.http_health_check().codec_client_type())),
random_generator_(random) {
Expand All @@ -148,6 +149,7 @@ HttpHealthCheckerImpl::HttpHealthCheckerImpl(const Cluster& cluster,

HttpHealthCheckerImpl::HttpStatusChecker::HttpStatusChecker(
const Protobuf::RepeatedPtrField<envoy::type::v3::Int64Range>& expected_statuses,
const Protobuf::RepeatedPtrField<envoy::type::v3::Int64Range>& retriable_statuses,
uint64_t default_expected_status) {
for (const auto& status_range : expected_statuses) {
const auto start = status_range.start();
Expand All @@ -169,16 +171,53 @@ HttpHealthCheckerImpl::HttpStatusChecker::HttpStatusChecker(
fmt::format("Invalid http status range: expecting end <= 600, but found end={}", end));
}

ranges_.emplace_back(std::make_pair(static_cast<uint64_t>(start), static_cast<uint64_t>(end)));
expected_ranges_.emplace_back(
std::make_pair(static_cast<uint64_t>(start), static_cast<uint64_t>(end)));
}

if (ranges_.empty()) {
ranges_.emplace_back(std::make_pair(default_expected_status, default_expected_status + 1));
if (expected_ranges_.empty()) {
expected_ranges_.emplace_back(
std::make_pair(default_expected_status, default_expected_status + 1));
}

for (const auto& status_range : retriable_statuses) {
const auto start = status_range.start();
const auto end = status_range.end();

if (start >= end) {
throw EnvoyException(fmt::format("Invalid http retriable status range: expecting start < "
"end, but found start={} and end={}",
start, end));
}

if (start < 100) {
throw EnvoyException(fmt::format(
"Invalid http retriable status range: expecting start >= 100, but found start={}",
start));
}

if (end > 600) {
throw EnvoyException(fmt::format(
"Invalid http retriable status range: expecting end <= 600, but found end={}", end));
}

retriable_ranges_.emplace_back(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a range intersection verification against the expected_ranges_

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left this for now since I'm initially leaning towards allowing overlap for simplicity. This is now reflected in the API docs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM

@wbpcode wbpcode Sep 14, 2021

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we wrapper this redundant range-checking codes to a validate func or some thing?


    if (start >= end) {
      throw EnvoyException(fmt::format("Invalid http retriable status range: expecting start < "
                                       "end, but found start={} and end={}",
                                       start, end));
    }

    if (start < 100) {
      throw EnvoyException(fmt::format(
          "Invalid http retriable status range: expecting start >= 100, but found start={}",
          start));
    }

    if (end > 600) {
      throw EnvoyException(fmt::format(
          "Invalid http retriable status range: expecting end <= 600, but found end={}", end));
    }

std::make_pair(static_cast<uint64_t>(start), static_cast<uint64_t>(end)));
}
}

bool HttpHealthCheckerImpl::HttpStatusChecker::inRange(uint64_t http_status) const {
for (const auto& range : ranges_) {
bool HttpHealthCheckerImpl::HttpStatusChecker::inExpectedRange(uint64_t http_status) const {
for (const auto& range : expected_ranges_) {
if (http_status >= range.first && http_status < range.second) {
return true;
}
}

return false;
}

bool HttpHealthCheckerImpl::HttpStatusChecker::inRetriableRange(uint64_t http_status) const {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: avoid code duplication by refactoring inExpectedRange and inRetriableRange to a single function inRange that receives the http_status and the range (either expected_range_ or retriable_range_)

@esmet esmet Sep 10, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this but I think the two ranges fields should remain private and the callers should still use lightly wrapped functions inExpectedRange and inRetriablyRange. The inner private function would have the common factored out code. What do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in response to c7e875e

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is better, thanks. Will update

for (const auto& range : retriable_ranges_) {
if (http_status >= range.first && http_status < range.second) {
return true;
}
Expand Down Expand Up @@ -331,7 +370,7 @@ HttpHealthCheckerImpl::HttpActiveHealthCheckSession::healthCheckResult() {
ENVOY_CONN_LOG(debug, "hc response={} health_flags={}", *client_, response_code,
HostUtility::healthFlagsToString(*host_));

if (!parent_.http_status_checker_.inRange(response_code)) {
if (!parent_.http_status_checker_.inExpectedRange(response_code)) {
// If the HTTP response code would indicate failure AND the immediate health check
// failure header is set, exclude the host from LB.
// TODO(mattklein123): We could consider doing this check for any HTTP response code, but this
Expand All @@ -341,7 +380,12 @@ HttpHealthCheckerImpl::HttpActiveHealthCheckSession::healthCheckResult() {
if (response_headers_->EnvoyImmediateHealthCheckFail() != nullptr) {
host_->healthFlagSet(Host::HealthFlag::EXCLUDED_VIA_IMMEDIATE_HC_FAIL);
}
return HealthCheckResult::Failed;

if (parent_.http_status_checker_.inRetriableRange(response_code)) {
return HealthCheckResult::Retriable;
} else {
return HealthCheckResult::Failed;
}
}

const auto degraded = response_headers_->EnvoyDegraded() != nullptr;
Expand Down Expand Up @@ -374,7 +418,10 @@ void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::onResponseComplete() {
handleSuccess(true);
break;
case HealthCheckResult::Failed:
handleFailure(envoy::data::core::v3::ACTIVE);
handleFailure(envoy::data::core::v3::ACTIVE, false);
break;
case HealthCheckResult::Retriable:
handleFailure(envoy::data::core::v3::ACTIVE, true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to comment the boolean parameter for extra clarity. Something like , /*retriable=*/true);

break;
}

Expand Down
9 changes: 6 additions & 3 deletions source/common/upstream/health_checker_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,15 @@ class HttpHealthCheckerImpl : public HealthCheckerImplBase {
public:
HttpStatusChecker(
const Protobuf::RepeatedPtrField<envoy::type::v3::Int64Range>& expected_statuses,
const Protobuf::RepeatedPtrField<envoy::type::v3::Int64Range>& retriable_statuses,
uint64_t default_expected_status);

bool inRange(uint64_t http_status) const;
bool inExpectedRange(uint64_t http_status) const;
bool inRetriableRange(uint64_t http_status) const;

private:
std::vector<std::pair<uint64_t, uint64_t>> ranges_;
std::vector<std::pair<uint64_t, uint64_t>> expected_ranges_;
std::vector<std::pair<uint64_t, uint64_t>> retriable_ranges_;
};

private:
Expand All @@ -78,7 +81,7 @@ class HttpHealthCheckerImpl : public HealthCheckerImplBase {
~HttpActiveHealthCheckSession() override;

void onResponseComplete();
enum class HealthCheckResult { Succeeded, Degraded, Failed };
enum class HealthCheckResult { Succeeded, Degraded, Failed, Retriable };
HealthCheckResult healthCheckResult();
bool shouldClose() const;

Expand Down
Loading