Skip to content

Commit

Permalink
ready: Add option to enable envoy readiness endpoint from worker
Browse files Browse the repository at this point in the history
/ready endpoint used by emissary is using the admin port (8001 by
default).
This generates a problem during config reloads with large configs as the
admin thread is blocking so the /ready endpoint can be very slow to
answer (in the order of several seconds, even more).

The problem is described in this envoy issue:
envoyproxy/envoy#16425

This change is trying to fix the /ready endpoint problem.
The /ready endpoint can be exposed in the worker pool by adding a
listener+ health check http filter.
This way, the /ready endpoint is fast and it is not blocked by any
config reload or blocking admin operation as it depends on the worker
pool.

Future changes will allow to use this endpoint with diagd and the go
code as well so they get a fast /ready endpoint and they do not use the
admin port.

This listener is disabled by default. the config "read_port" can be used
to set the port and enable this new listener on envoy.

Signed-off-by: Fabrice Rabaute <[email protected]>
  • Loading branch information
jfrabaute authored and Lance Austin committed Feb 3, 2023
1 parent 7a6edf7 commit a419ad3
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 0 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ it will be removed; but as it won't be user-visible this isn't considered a brea
`Zipkin` and use the [Open Telemetry Collector](https://opentelemetry.io/docs/collector/) to
collect and forward Observabity data to LightStep.

- Feature: /ready endpoint used by emissary is using the admin port (8001 by default). This
generates a problem during config reloads with large configs as the admin thread is blocking so
the /ready endpoint can be very slow to answer (in the order of several seconds, even more). The
new feature allows to enable a specific envoy listener that can answer /ready calls from the
workers so the endpoint is always fast and it does not suffers from single threaded admin thread
slowness on config reloads and other slow endpoints handled by the admin thread Configure the
listener port using AMBASSADOR_READY_PORT and enable access log using AMBASSADOR_READY_LOG
environment variables.

## [3.3.0] November 02, 2022
[3.3.0]: https://github.com/emissary-ingress/emissary/compare/v3.2.0...v3.3.0

Expand Down
14 changes: 14 additions & 0 deletions docs/releaseNotes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,20 @@ items:
and use the [Open Telemetry Collector](https://opentelemetry.io/docs/collector/) to
collect and forward Observabity data to LightStep.
- title: Add option to enable envoy readiness endpoint from worker
type: feature
body: >-
/ready endpoint used by emissary is using the admin port (8001 by default).
This generates a problem during config reloads with large configs as the
admin thread is blocking so the /ready endpoint can be very slow to
answer (in the order of several seconds, even more).
The new feature allows to enable a specific envoy listener that can answer /ready calls
from the workers so the endpoint is always fast and it does not suffers from single threaded
admin thread slowness on config reloads and other slow endpoints handled by the admin thread
Configure the listener port using AMBASSADOR_READY_PORT and enable access log using
AMBASSADOR_READY_LOG environment variables.
docs: https://www.getambassador.io/docs/emissary/latest/topics/running/environment/

- version: 3.3.0
prevVersion: 3.2.0
date: '2022-11-02'
Expand Down
1 change: 1 addition & 0 deletions pkg/ambex/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import (
_ "github.com/emissary-ingress/emissary/v3/pkg/api/envoy/extensions/filters/http/ext_authz/v3"
_ "github.com/emissary-ingress/emissary/v3/pkg/api/envoy/extensions/filters/http/grpc_stats/v3"
_ "github.com/emissary-ingress/emissary/v3/pkg/api/envoy/extensions/filters/http/gzip/v3"
_ "github.com/emissary-ingress/emissary/v3/pkg/api/envoy/extensions/filters/http/health_check/v3"
_ "github.com/emissary-ingress/emissary/v3/pkg/api/envoy/extensions/filters/http/lua/v3"
_ "github.com/emissary-ingress/emissary/v3/pkg/api/envoy/extensions/filters/http/ratelimit/v3"
_ "github.com/emissary-ingress/emissary/v3/pkg/api/envoy/extensions/filters/http/rbac/v3"
Expand Down
1 change: 1 addition & 0 deletions python/ambassador/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ class Constants:
SERVICE_PORT_HTTP = 8080
SERVICE_PORT_HTTPS = 8443
ADMIN_PORT = 8001
READY_PORT = -1
DIAG_PORT = 8877
DIAG_PORT_ALT = 8004
SERVICE_PORT_AGENT = 9900
2 changes: 2 additions & 0 deletions python/ambassador/envoy/v3/v3config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from ..common import EnvoyConfig
from .v3_static_resources import V3StaticResources
from .v3admin import V3Admin
from .v3ready import V3Ready
from .v3bootstrap import V3Bootstrap
from .v3cluster import V3Cluster
from .v3listener import V3Listener
Expand Down Expand Up @@ -64,6 +65,7 @@ def __init__(self, ir: "IR", cache: Optional[Cache] = None) -> None:
V3Cluster.generate(self)
V3StaticResources.generate(self)
V3Bootstrap.generate(self)
V3Ready.generate(self)

def has_listeners(self) -> bool:
return len(self.listeners) > 0
Expand Down
156 changes: 156 additions & 0 deletions python/ambassador/envoy/v3/v3ready.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Copyright 2021 Datawire. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License

from typing import List, TYPE_CHECKING

from .v3listener import V3Listener

if TYPE_CHECKING:
from . import V3Config # pragma: no cover


class V3Ready(dict):

@classmethod
def generate(cls, config: 'V3Config') -> None:
# Inject the ready listener to the list of listeners if enabled
rport = config.ir.aconf.module_lookup('ambassador', 'ready_port', -1)
if rport <= 0:
config.ir.logger.info(f"V3Ready: ==== disabled")
return

rip = config.ir.aconf.module_lookup('ambassador', 'ready_ip', '127.0.0.1')
rlog = config.ir.aconf.module_lookup('ambassador', 'ready_log', True)

config.ir.logger.info(f"V3Ready: ==== listen on %s:%s" % (rip, rport))

typed_config = {
'@type': 'type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager',
'stat_prefix': 'ready_http',
'route_config': {
'name': 'local_route'
},
'http_filters': [
{
'name': 'envoy.filters.http.health_check',
'typed_config': {
'@type': 'type.googleapis.com/envoy.extensions.filters.http.health_check.v3.HealthCheck',
'pass_through_mode': False,
'headers': [
{
'name': ':path',
'exact_match': '/ready'
}
]
}
},
{
'name': 'envoy.filters.http.router'
}
]
}
if rlog:
typed_config['access_log'] = cls.access_log(config)

ready_listener = {
'name': 'ambassador-listener-ready-%s-%s' % (rip, rport),
'address': {
'socket_address': {
'address': rip,
'port_value': rport,
'protocol': 'TCP'
}
},
'filter_chains': [
{
'filters': [
{
'name': 'envoy.filters.network.http_connection_manager',
'typed_config': typed_config,
}
]
}
]
}

config.static_resources['listeners'].append(ready_listener)

# access_log constructs the access_log configuration for this V3Listener
@classmethod
def access_log(cls, config: 'V3Config') -> List[dict]:
access_log: List[dict] = []

# Use sane access log spec in JSON
if config.ir.ambassador_module.envoy_log_type.lower() == "json":
log_format = config.ir.ambassador_module.get('envoy_log_format', None)
if log_format is None:
log_format = {
'start_time': '%START_TIME%',
'method': '%REQ(:METHOD)%',
'path': '%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%',
'protocol': '%PROTOCOL%',
'response_code': '%RESPONSE_CODE%',
'response_flags': '%RESPONSE_FLAGS%',
'bytes_received': '%BYTES_RECEIVED%',
'bytes_sent': '%BYTES_SENT%',
'duration': '%DURATION%',
'upstream_service_time': '%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%',
'x_forwarded_for': '%REQ(X-FORWARDED-FOR)%',
'user_agent': '%REQ(USER-AGENT)%',
'request_id': '%REQ(X-REQUEST-ID)%',
'authority': '%REQ(:AUTHORITY)%',
'upstream_host': '%UPSTREAM_HOST%',
'upstream_cluster': '%UPSTREAM_CLUSTER%',
'upstream_local_address': '%UPSTREAM_LOCAL_ADDRESS%',
'downstream_local_address': '%DOWNSTREAM_LOCAL_ADDRESS%',
'downstream_remote_address': '%DOWNSTREAM_REMOTE_ADDRESS%',
'requested_server_name': '%REQUESTED_SERVER_NAME%',
'istio_policy_status': '%DYNAMIC_METADATA(istio.mixer:status)%',
'upstream_transport_failure_reason': '%UPSTREAM_TRANSPORT_FAILURE_REASON%'
}

tracing_config = config.ir.tracing
if tracing_config and tracing_config.driver == 'envoy.tracers.datadog':
log_format['dd.trace_id'] = '%REQ(X-DATADOG-TRACE-ID)%'
log_format['dd.span_id'] = '%REQ(X-DATADOG-PARENT-ID)%'

access_log.append({
'name': 'envoy.access_loggers.file',
'typed_config': {
'@type': 'type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog',
'path': config.ir.ambassador_module.envoy_log_path,
'json_format': log_format
}
})
else:
# Use a sane access log spec
log_format = config.ir.ambassador_module.get('envoy_log_format', None)

if not log_format:
log_format = 'ACCESS [%START_TIME%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% \"%REQ(X-FORWARDED-FOR)%\" \"%REQ(USER-AGENT)%\" \"%REQ(X-REQUEST-ID)%\" \"%REQ(:AUTHORITY)%\" \"%UPSTREAM_HOST%\"'

access_log.append({
'name': 'envoy.access_loggers.file',
'typed_config': {
'@type': 'type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog',
'path': config.ir.ambassador_module.envoy_log_path,
'log_format': {
'text_format_source': {
'inline_string': log_format + '\n'
}
}
}
})

return access_log
2 changes: 2 additions & 0 deletions python/ambassador/ir/irambassador.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class IRAmbassador(IRResource):
"proper_case",
"prune_unreachable_routes",
"readiness_probe",
"ready_port",
"regex_max_size",
"regex_type",
"resolver",
Expand Down Expand Up @@ -124,6 +125,7 @@ def __init__(
name=name,
service_port=Constants.SERVICE_PORT_HTTP,
admin_port=Constants.ADMIN_PORT,
ready_port=Constants.READY_PORT,
auth_enabled=None,
enable_ipv6=False,
envoy_log_type="text",
Expand Down

0 comments on commit a419ad3

Please sign in to comment.