Skip to content
275 changes: 275 additions & 0 deletions A40-csds-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
xDS Configuration Dump via Client Status Discovery Service in gRPC
----
* Author(s): lidizheng
* Approver: markdroth
* Status: In-Review
* Implemented in: TBD
* Last updated: 2021-02-26
* Discussion at: https://groups.google.com/g/grpc-io/c/zL45YyxtJ08

## Abstract

[Client Status Discovery
Service](https://github.com/envoyproxy/envoy/blob/main/api/envoy/service/status/v3/csds.proto)
(CSDS) is a service that exposes xDS config of a given client. It’s commonly
used to query control planes for the synced xDS config of a particular sidecar
proxy. However, it can also be used to query an xDS-compliant application for
its received xDS configuration. This doc proposes a solution to implement a CSDS
service in gRPC, so our users can debug their service mesh easily.


## Background

Envoy [started](https://github.com/envoyproxy/envoy/pull/9383) the CSDS
development process in Dec 2019. The CSDS API has been available since xDS v2,
and it’s under active development. But, Envoy proxies do not support or
understand this service, only the control plane does. Envoy already provides
config dump via its [admin
interface](https://www.envoyproxy.io/docs/envoy/latest/operations/admin) for
years. There is no visible plan for Envoy to support serving CSDS directly as a
Comment thread
markdroth marked this conversation as resolved.
proxy (see [envoy#13181](https://github.com/envoyproxy/envoy/issues/13181)).

The CSDS service accepts two methods, one for unary, one for streaming. The
expected behavior of the two methods is the same, but the streaming method can
reuse the stream. According to the service description below, this is not a
watch-style API.

```proto
// CSDS is Client Status Discovery Service. It can be used to get the status of
// an xDS-compliant client from the management server's point of view. It can
// also be used to get the current xDS states directly from the client.
service ClientStatusDiscoveryService {
rpc StreamClientStatus(stream ClientStatusRequest) returns (stream ClientStatusResponse) {
}

rpc FetchClientStatus(ClientStatusRequest) returns (ClientStatusResponse) {
}
}
```

### Related Proposals:
* [A27 - xDS-Based Global Load
Balancing](https://github.com/grpc/proposal/blob/master/A27-xds-global-load-balancing.md)
* [A38 - Admin Interface API](https://github.com/grpc/proposal/pull/218)
Comment thread
sergiitk marked this conversation as resolved.


## Proposal

### CSDS in gRPC

The goal of adding a CSDS service to gRPC is to enable programmatic access to
the operating xDS configs of a running gRPC application. To make this more
clear, an xDS-compliant application receives many xDS configs, which may be
rejected or ignored or obsoleted. **The gRPC CSDS service should always return
the currently accepted xDS configs.**


### ADS Parsing Logic Update: Continue After First Error

Current gRPC ADS parsing logic is when the response parser observes an error in
the ADS response, it fails the entire message and aborts parsing.

To improve the debuggability of gRPC, gRPC needs to populate **the reason** and
**affected resources** when rejecting an update response. This information
requires the ADS parser to continue parsing past the first error. Here are the
expected behavior under scenarios:

* If the entire message won’t parse: no need to record anything in CSDS, since
we don’t know what type of xDS config or what resources are being updated;
* If one resource won’t parse: don’t abort the parsing, record the error in the

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.

The handling is still a bit ambiguous. I guess we would only record the first error, so the parsing does not need to continue. But would attach the error message to all previously accepted resources that the current response is attempting to update.

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.

Can you specify the cases that are ambiguous here? I guess the wording here is not accurate enough, if one resource (e.g., a cluster) has validation error, the parsing should continue; or one of its field has parsing error, e.g. a config in Any failed to deserialize, the parsing should continue. In other word, the parsing should continue when possible.

If you need a reference PR, please see grpc/grpc#25329.

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.

Looks C-core is concatenating errors together if there are more than one error resources. I don't see much benefit of doing so, other than showing a giant error message while may still not showing all errors (e.g., multiple validation errors for a single resource). Anyway, I am fine with what's being implemented in C-core.

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.

I think the important part is peeking into the ADS response to see what set of resources is being updated, then pasting the error details to their error_state field. I'm fine with one error, as long as they are logged in the right place in the CSDS response, so it's actionable for users.

`error_state.details` field. The error string will be attached to other
resources that are successfully recognized;
* If one resource has validation error: record the error, and attach the error
string to all resources in the ADS update including itself.

Here is an example of NACK information handling with new ADS changes:

```
# Imagine we have endpoint A, B, C
EDS -> {A, B, C}, version 1
gRPC -> ACK
CSDS -> [
{Endpoint A, ACK, version 1},
{Endpoint B, ACK, version 1},
{Endpoint C, ACK, version 1}
]

# The newer endpoint B contains parsing error
EDS -> {A, B}, version 2
gRPC -> NACK, Failed to parse endpoint B
CSDS -> [
{Endpoint A, NACK, version 1, rejected version 2, rejected reason: Failed to parse endpoint B},
{Endpoint B, NACK, version 1, rejected version 2, rejected reason: Failed to parse endpoint B},
{Endpoint C, ACK, version 1}
]

# Accepted update will clean error states
EDS -> {B, C} version 3
gRPC -> ACK
CSDS -> [
{Endpoint A, NACK, version 1, rejected version 2, rejected reason: Failed to parse endpoint B},
{Endpoint B, ACK, version 3},
{Endpoint C, ACK, version 3}
]
```


### xDS Config Error State

When an ADS response is rejected, gRPC should provide debug information via
CSDS. This is done via `UpdateFailureState` within the `config_dump.proto`,
Comment thread
lidizheng marked this conversation as resolved.
which includes the version, timestamp, and the reason of the rejected update
(see [proto
definition](https://github.com/envoyproxy/envoy/blob/a4024a578b3f2611fe26229f5d0de99eb0c56895/api/envoy/admin/v3/config_dump.proto#L72)).


### CSDS Service Design

Ideally, the CSDS service should be reusable by our users, instead of just an
internal tool for the gRPC library. So, the CSDS service should be implemented
in wrapper languages and Java/Go. There are three major parts in the service
design:

* Cache resources in the global XdsClient;
* Collect additional metadata about the cached resource (update timestamp,
version, etc.);
* Assemble cached resources into the gigantic config dump response.
Comment thread
sergiitk marked this conversation as resolved.

Note that xDS config should include the `config.core.v3.Node` information in the
Comment thread
lidizheng marked this conversation as resolved.
`envoy.service.status.v3.ClientConfig.node` field. Unlike other xDS configs,
this information is static and may require extra logic to integrate into the
CSDS service.

For Java and Go, it’s straightforward that this feature can be implemented
bottom-up in the same language, and it’s possible to avoid an additional copy of
the message itself. But Core needs to transport the collected xDS configs across
the language boundary. The API needs to convert proto messages into C-compatible
types (e.g. `char *`, either bytes or JSON).


#### Detail: No xDS v2 Support

CSDS was meant to be a RPC service made for xDS management servers before this
proposal. But the service itself has the potential to serve client status on xDS
clients. However, during development, we found several constraints about the
existing service protocol, hence we merged several updates (see above) to
improve the CSDS service to meet our standard. The updates are made to xDS v3
and xDS v4 (alpha) only, since xDS v2 is in deprecated state. This doc proposes
to not support xDS v2 for CSDS.

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.

What to do if the underlying client is speaking v2?
Return an error? What error code to use? FailedPrecondition?

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.

By underlying client, do you mean the gPRC application? I thought gRPC processes all xDS config into v3, so the dumped xDS config is agnostic to v2/v3. If the external CSDS client is only speaking v2, they won't be able to invoke the v3 method, because the fully-qualified-method-name includes the service name which has "v3" in it. So, the v2 external CSDS client will get UNIMPLEMENTED by default, in theory.

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.

Here's what TD does for v3 (currently only v2 supported):

❯ csds-client -request_file request.yaml -api_version v3
2021/03/01 18:20:55 rpc error: code = Unimplemented desc = The GRPC target is not implemented on the server, host: trafficdirector.googleapis.com, method: /envoy.service.status.v3.ClientStatusDiscoveryService/StreamClientStatus.

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.

Nice, so in practicce, the old client will also get UNIMPLEMENTED.

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.

Note that the CSDS RPC service name is different between v2 and v3, because the major version is part of the proto package name. So I think all we need to do here is to implement only the v3 service, not the v2 service. Then clients will get UNIMPLEMENTED if they try to use the v2 service, just like they would for any other unimplemented RPC service.

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.

I think it's fine to send v2 protos inside of the Any fields in v3 CSDS. In xDS terms, the transport protocol version and the data model version do not have to be the same. I don't think we need to do any conversion here.

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.

Do you have an example that where the v2 resources might slip through

It's not one type of resource. All the resources in xds v2 are v2. E.g. v2.Cluster vs v3.Cluster, v2.Endpoint vs v3.Endpoint.

In xDS terms, the transport protocol version and the data model version do not have to be the same.

That's right.
The difference would be on the csds client side. If we send v2 protos as Any in the csds responses, the csds client needs to depend on v2 protos (not that they will be used, but their types need to be registered) so the client can unmarshal those Any.

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.

I think it's fine for CSDS client to need v2 protos if the client is using v2. That's independent of the fact that we're only supporting v3 of the CSDS RPC service itself.

This is no different than ADS -- it's possible for a client to use v3 of ADS but v2 of the resource protos, or vice versa. In fact, in Envoy, those are two independent knobs. In gRPC, we just simplified it and avoided one of the knobs by depending on the fact that we didn't happen to use any fields in v2 that were being removed in v3, so we could just unconditionally treat all of the resource protos as v3.

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 SGTM (and is actually easier to do :))

Let's update the gRFC to clarify this? This doesn't sound accurate:

This doc proposes to not support xDS v2 for CSDS.

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.

Updated to

For xDS CSDS v2 (the RPC service itself), this doc proposes to not support, so
we only provide v3 CSDS service and CSDS client requesting for v2 service will
get UNIMPLEMENTED error.

There are edge cases that v2 and v3 xDS resources are mixed together. This doc
proposes to keep them as-is in the CSDS responses. So, the CSDS service can
accurately reflect the xDS configs received from the control plane.

The section title also updated to xDS v2/v3/v4, since it's hard to describe our handling of v2 in the title.


#### Detail: Expose xDS Config As Is

To date, gRPC supports the majority of xDS config but not all. Alternatively, we
could only dump the config that gRPC understands. However, doing so will create
a behavior difference between the gRPC and the control plane. It might be
confusing to users that some fields are missing. Also, during the configuration
dump, Envoy also dumps its entire configuration even if it doesn’t understand
some of the configuration fields (think of fields for Envoy extensions). So, for
correctness and simplicity, we should **cache and expose the xDS config the way
gRPC receives them**.

#### Detail: Cache Lifecycle

gRPC doesn't use xDS messages directly, but interprets them into language-native
class/struct. The lifecycle of the cached xDS messages should be identical to
the interpreted structure in each stack, so there is no memory management logic
change needed.

#### Detail: Generated Node Information

gRPC loads Node information from a [bootstrap
file](https://github.com/grpc/proposal/blob/master/A27-xds-global-load-balancing.md#xdsclient-and-bootstrap-file),
which includes the identification that will be used in the control plane. gRPC's
xDS client also generates `user_agent_name`, `user_agent_version`, and
`client_features` before sending the `Node` information to the control plane.
This information can be very helpful to locate the culprit release and
debugging. This doc recommends the implementation to provide the
`ClientStatusResponse.ClientConfig.node` field with the `Node` information that
the control plane will receive. Here is an example of a filled `Node`:

```json
{
"node": {
"id": "c591240a-b3b6-4761-aada-3fa43ec6a852",
"cluster": "cluster",
"metadata": {
"...": "..."
},
"locality": {
"zone": "zone"
},
"userAgentName": "gRPC Java",
"userAgentVersion": "1.35.1-SNAPSHOT",
"clientFeatures": ["envoy.lb.does_not_support_overprovisioning"]
}
}
```

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.

The requirement of Node information is updated.
CC @sergiitk @menghanl

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.

Thanks for capturing this! Glad we've figured it out.

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.

As I mentioned above, I don't see any compelling reason to bother with populating this field.

@sergiitk sergiitk Feb 26, 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.

@markdroth: Earlier last week @lidizheng @menghanl and I discussed this in a chat. To summarize, arguments for populating node info:

  1. Parity with istioctl proxy-config bootstrap
  2. Some fields are generated after on-the-fly, and sent to the control plane in discovery requests: user_agent_name, user_agent_version, and, more importantly clientFeatures. Exposing them can be helpful for debugging. Note this is different from regular CSDS behavior to dump configs as is. The argument for treating the node info differently is that it's loaded from bootstrap and sent to the control plane, as opposed to it's received from the control plane

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.

If there's desire to expose the client's node information for debugging purposes, then that's a perfectly reasonable use-case, and we should design something to address it. However, CSDS was not designed to solve that problem, and IMHO it would be a very clunky way to solve that problem. If the mechanism we provide for accessing the bootstrap info is for a client to query CSDS, that means that a client that wants only the bootstrap info has no way to get it that does not also send the entire set of known xDS resources. That's sending a potentially very large amount of completely unnecessary data.

If we want a way to expose the client's bootstrap info for debugging purposes, let's add a separate RPC service for that. I don't think it should be part of this design.

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.

I think I might be missing your point. Could you please clarify what data (if any) should be in ClientStatusResponse. ClientConfig.node?

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.

it would be a very clunky way to solve that problem

I agree the Node information will be sent along other xDS config dumps. And users might not need them all the time. But the same argument could apply to all xDS configs, like users just updated listeners and would like to see the listeners, and we are sending routes, clusters, endpoints in the CSDS response.

When the xDS implements get popularity soon, we might have users posting issues, and with the node information we could tell which gRPC language/release it is using, and (possibly) which control plane it is trying to talk to.

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.

@sergiitk

Could you please clarify what data (if any) should be in ClientStatusResponse. ClientConfig.node?

What I am proposing is that we do not populate that field at all.

@lidizheng

But the same argument could apply to all xDS configs, like users just updated listeners and would like to see the listeners, and we are sending routes, clusters, endpoints in the CSDS response.

I don't see what that statement has to do with the argument I'm making here. It's true that CSDS does not provide the ability to filter which set of configs are returned, but that's equally true whether CSDS is being served by the control plane or the client. (And this would be trivial to add if someone needed it just by adding a resource type filter to the CSDS request proto.)

The argument I am making here is that the reason that the node field exists in the CSDS response is that CSDS was designed to be served by control planes, and the control plane will be returning data for multiple clients and needs a way to indicate which client each set of data is relevant to. But when CSDS is sent on a client, all of the data is by definition for the same client, so there's no reason to populate this field. In other words, the node field needs to be populated only when the CSDS server is a control plane; it is simply not relevant when the CSDS server is a client.

When the xDS implements get popularity soon, we might have users posting issues, and with the node information we could tell which gRPC language/release it is using, and (possibly) which control plane it is trying to talk to.

IMHO, the fact that this could conceivably be useful to someone someday is not enough justification to add it. If that was a good enough justification, why isn't it standard practice for every API to include the gRPC language and release? Why doesn't channelz include this info? Why doesn't every single RPC service designed by applications provide this info?

In case it's not clear, the answer is that if it's not what a given API is designed to do, it shouldn't be part of that API. The microservices approach means that each API should provide only the data that is necessary for the problem that service is designed to solve. If that wasn't the case, then all APIs would be huge, because everyone would throw everything they could think of into every one of them.

CSDS is designed to show the set of resources seen by the client, not the client's node information. If we want something to expose the client's node information, that's IMHO a separate use-case, and we should design a separate service to solve that problem.

In any case, I don't think this is important enough to justify spending more time debating it, so I'll leave it up to you.


#### Detail: Node Matching

The node matching mechanism is not designed for gRPC’s use case, but for
querying the control plane. Technically, gRPC application only serves as one xDS
node, so there is no need to differentiate with xDS client's status to reply.
However, gRPC should reject the request with Node Matcher that failed to match
with the Node information of the gRPC application. This feature has lower
priority. To preserve forward compatibility, this doc proposes to return an
[INVALID_ARGUMENT](https://github.com/grpc/grpc/blob/master/doc/statuscodes.md)
if the CSDS implementation observe an non-empty Node Matcher.
Comment thread
sergiitk marked this conversation as resolved.


## Alternatives

### Solution: Config Dump By Attaching An HTTP Server

Although it is the most straightforward way of implementing an admin interface,
Java doesn’t have a good enough built-in web server and it is challenging to
introduce yet another dependency into Core. Exposing application states via HTTP
will be challenging from engineering perspective.

### Solution: Config Dump via Channelz

Though only a subset of xDS resources is applicable to a channel, xDS configs
(listeners, routes, clusters, endpoints) are global resources to a gRPC
application. One unsolved problem is that if we choose to use CSDS to expose xDS
configs, **the users won’t have the ability to query the set of effective xDS
configs for a channel**. The CSDS is not designed for finer granularity than
individual processes.

gRPC supports sharing XdsClient across multiple channels, the boundary of each
channel’s xDS config will be blurry. Injecting xDS info into a channel tracing
service won’t be the right direction in the long term.

### Solution: Config Dump via File

This approach saves the active xDS configuration onto a memory-based file system
location, like `sysctl`, or Envoy Runtime. However, due to the lack of two-way
communication and the complexity of the new machinery, this solution could cost
more engineering resources but yield less functionality.


## Implementation

### CSDS Proto Updates

* [envoy#13121]: the config synchronization status was defined from the xDS
management server point of view, this PR adds a set of ENUM to present the
config synchronization status from a client-side view.
* [envoy#14689]: the CSDS focused on dumping the in-effective xDS configs, but
we can do better to improve gRPC’s debuggability. This PR adds fields to
config dump protos to allow CSDS to return information about the rejected
update.
* [envoy#14900]: this PR adds two additional status to client configs, the
REQUESTED and the DOES_NOT_EXIST ([read
more](https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol#knowing-when-a-requested-resource-does-not-exist)).

### CSDS Implementation

* Core: https://github.com/grpc/grpc/pull/25038
* Golang: To be linked
* Java: To be linked

[envoy#13121]: https://github.com/envoyproxy/envoy/pull/13121

[envoy#14689]: https://github.com/envoyproxy/envoy/pull/14689

[envoy#14900]: https://github.com/envoyproxy/envoy/pull/14900