mirror of https://github.com/k3s-io/k3s
Proposal for introducing Protobuf serialization
parent
791c694c21
commit
95cf60be23
|
@ -0,0 +1,509 @@
|
||||||
|
<!-- BEGIN MUNGE: UNVERSIONED_WARNING -->
|
||||||
|
|
||||||
|
<!-- BEGIN STRIP_FOR_RELEASE -->
|
||||||
|
|
||||||
|
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
|
||||||
|
width="25" height="25">
|
||||||
|
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
|
||||||
|
width="25" height="25">
|
||||||
|
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
|
||||||
|
width="25" height="25">
|
||||||
|
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
|
||||||
|
width="25" height="25">
|
||||||
|
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
|
||||||
|
width="25" height="25">
|
||||||
|
|
||||||
|
<h2>PLEASE NOTE: This document applies to the HEAD of the source tree</h2>
|
||||||
|
|
||||||
|
If you are using a released version of Kubernetes, you should
|
||||||
|
refer to the docs that go with that version.
|
||||||
|
|
||||||
|
Documentation for other releases can be found at
|
||||||
|
[releases.k8s.io](http://releases.k8s.io).
|
||||||
|
</strong>
|
||||||
|
--
|
||||||
|
|
||||||
|
<!-- END STRIP_FOR_RELEASE -->
|
||||||
|
|
||||||
|
<!-- END MUNGE: UNVERSIONED_WARNING -->
|
||||||
|
|
||||||
|
# Protobuf serialization and internal storage
|
||||||
|
|
||||||
|
@smarterclayton
|
||||||
|
|
||||||
|
March 2016
|
||||||
|
|
||||||
|
## Proposal and Motivation
|
||||||
|
|
||||||
|
The Kubernetes API server is a "dumb server" which offers storage, versioning,
|
||||||
|
validation, update, and watch semantics on API resources. In a large cluster
|
||||||
|
the API server must efficiently retrieve, store, and deliver large numbers
|
||||||
|
of coarse-grained objects to many clients. In addition, Kubernetes traffic is
|
||||||
|
heavily biased towards intra-cluster traffic - as much as 90% of the requests
|
||||||
|
served by the APIs are for internal cluster components like nodes, controllers,
|
||||||
|
and proxies. The primary format for intercluster API communication is JSON
|
||||||
|
today for ease of client construction.
|
||||||
|
|
||||||
|
At the current time, the latency of reaction to change in the cluster is
|
||||||
|
dominated by the time required to load objects from persistent store (etcd),
|
||||||
|
convert them to an output version, serialize them JSON over the network, and
|
||||||
|
then perform the reverse operation in clients. The cost of
|
||||||
|
serialization/deserialization and the size of the bytes on the wire, as well
|
||||||
|
as the memory garbage created during those operations, dominate the CPU and
|
||||||
|
network usage of the API servers.
|
||||||
|
|
||||||
|
In order to reach clusters of 10k nodes, we need roughly an order of magnitude
|
||||||
|
efficiency improvement in a number of areas of the cluster, starting with the
|
||||||
|
masters but also including API clients like controllers, kubelets, and node
|
||||||
|
proxies.
|
||||||
|
|
||||||
|
We propose to introduce a Protobuf serialization for all common API objects
|
||||||
|
that can optionally be used by intra-cluster components. Experiments have
|
||||||
|
demonstrated a 10x reduction in CPU use during serialization and deserialization,
|
||||||
|
a 2x reduction in size in bytes on the wire, and a 6-9x reduction in the amount
|
||||||
|
of objects created on the heap during serialization. The Protobuf schema
|
||||||
|
for each object will be automatically generated from the external API Go structs
|
||||||
|
we use to serialize to JSON.
|
||||||
|
|
||||||
|
Benchmarking showed that the time spent on the server in a typical GET
|
||||||
|
resembles:
|
||||||
|
|
||||||
|
etcd -> decode -> defaulting -> convert to internal ->
|
||||||
|
JSON 50us 5us 15us
|
||||||
|
Proto 5us
|
||||||
|
JSON 150allocs 80allocs
|
||||||
|
Proto 100allocs
|
||||||
|
|
||||||
|
process -> convert to external -> encode -> client
|
||||||
|
JSON 15us 40us
|
||||||
|
Proto 5us
|
||||||
|
JSON 80allocs 100allocs
|
||||||
|
Proto 4allocs
|
||||||
|
|
||||||
|
Protobuf has a huge benefit on encoding because it does not need to allocate
|
||||||
|
temporary objects, just one large buffer. Changing to protobuf moves our
|
||||||
|
hotspot back to conversion, not serialization.
|
||||||
|
|
||||||
|
|
||||||
|
## Design Points
|
||||||
|
|
||||||
|
* Generate Protobuf schema from Go structs (like we do for JSON) to avoid
|
||||||
|
manual schema update and drift
|
||||||
|
* Generate Protobuf schema that is field equivalent to the JSON fields (no
|
||||||
|
special types or enumerations), reducing drift for clients across formats.
|
||||||
|
* Follow our existing API versioning rules (backwards compatible in major
|
||||||
|
API versions, breaking changes across major versions) by creating one
|
||||||
|
Protobuf schema per API type.
|
||||||
|
* Continue to use the existing REST API patterns but offer an alternative
|
||||||
|
serialization, which means existing client and server tooling can remain
|
||||||
|
the same while benefiting from faster decoding.
|
||||||
|
* Protobuf objects on disk or in etcd will need to be self identifying at
|
||||||
|
rest, like JSON, in order for backwards compatibility in storage to work,
|
||||||
|
so we must add an envelope with apiVersion and kind to wrap the nested
|
||||||
|
object, and make the data format recognizable to clients.
|
||||||
|
* Use the [gogo-protobuf](https://github.com/gogo/protobuf) Golang library to generate marshal/unmarshal
|
||||||
|
operations, allowing us to bypass the expensive reflection used by the
|
||||||
|
golang JSOn operation
|
||||||
|
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
|
||||||
|
* We considered JSON compression to reduce size on wire, but that does not
|
||||||
|
reduce the amount of memory garbage created during serialization and
|
||||||
|
deserialization.
|
||||||
|
* More efficient formats like Msgpack were considered, but they only offer
|
||||||
|
2x speed up vs the 10x observed for Protobuf
|
||||||
|
* gRPC was considered, but is a larger change that requires more core
|
||||||
|
refactoring. This approach does not eliminate the possibility of switching
|
||||||
|
to gRPC in the future.
|
||||||
|
* We considered attempting to improve JSON serialization, but the cost of
|
||||||
|
implementing a more efficient serializer library than ugorji is
|
||||||
|
significantly higher than creating a protobuf schema from our Go structs.
|
||||||
|
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
The Protobuf schema for each API group and version will be generated from
|
||||||
|
the objects in that API group and version. The schema will be named using
|
||||||
|
the package identifier of the Go package, i.e.
|
||||||
|
|
||||||
|
k8s.io/kubernetes/pkg/api/v1
|
||||||
|
|
||||||
|
Each top level object will be generated as a Protobuf message, i.e.:
|
||||||
|
|
||||||
|
type Pod struct { ... }
|
||||||
|
|
||||||
|
message Pod {}
|
||||||
|
|
||||||
|
Since the Go structs are designed to be serialized to JSON (with only the
|
||||||
|
int, string, bool, map, and array primitive types), we will use the
|
||||||
|
canonical JSON serialization as the protobuf field type wherever possible,
|
||||||
|
i.e.:
|
||||||
|
|
||||||
|
JSON Protobuf
|
||||||
|
string -> string
|
||||||
|
int -> varint
|
||||||
|
bool -> bool
|
||||||
|
array -> repeating message|primitive
|
||||||
|
|
||||||
|
We disallow the use of the Go `int` type in external fields because it is
|
||||||
|
ambiguous depending on compiler platform, and instead always use `int32` or
|
||||||
|
`int64`.
|
||||||
|
|
||||||
|
We will use maps (a protobuf 3 extension that can serialize to protobuf 2)
|
||||||
|
to represent JSON maps:
|
||||||
|
|
||||||
|
JSON Protobuf Wire (proto2)
|
||||||
|
map -> map<string, ...> -> repeated Message { key string; value bytes }
|
||||||
|
|
||||||
|
We will not convert known string constants to enumerations, since that
|
||||||
|
would require extra logic we do not already have in JSOn.
|
||||||
|
|
||||||
|
To begin with, we will use Protobuf 3 to generate a Protobuf 2 schema, and
|
||||||
|
in the future investigate a Protobuf 3 serialization. We will introduce
|
||||||
|
abstractions that let us have more than a single protobuf serialization if
|
||||||
|
necessary. Protobuf 3 would require us to support message types for
|
||||||
|
pointer primitive (nullable) fields, which is more complex than Protobuf 2's
|
||||||
|
support for pointers.
|
||||||
|
|
||||||
|
### Example of generated proto IDL
|
||||||
|
|
||||||
|
Without gogo extensions:
|
||||||
|
|
||||||
|
```
|
||||||
|
syntax = 'proto2';
|
||||||
|
|
||||||
|
package k8s.io.kubernetes.pkg.api.v1;
|
||||||
|
|
||||||
|
import "k8s.io/kubernetes/pkg/api/resource/generated.proto";
|
||||||
|
import "k8s.io/kubernetes/pkg/api/unversioned/generated.proto";
|
||||||
|
import "k8s.io/kubernetes/pkg/runtime/generated.proto";
|
||||||
|
import "k8s.io/kubernetes/pkg/util/intstr/generated.proto";
|
||||||
|
|
||||||
|
// Package-wide variables from generator "generated".
|
||||||
|
option go_package = "v1";
|
||||||
|
|
||||||
|
// Represents a Persistent Disk resource in AWS.
|
||||||
|
//
|
||||||
|
// An AWS EBS disk must exist before mounting to a container. The disk
|
||||||
|
// must also be in the same AWS zone as the kubelet. An AWS EBS disk
|
||||||
|
// can only be mounted as read/write once. AWS EBS volumes support
|
||||||
|
// ownership management and SELinux relabeling.
|
||||||
|
message AWSElasticBlockStoreVolumeSource {
|
||||||
|
// Unique ID of the persistent disk resource in AWS (Amazon EBS volume).
|
||||||
|
// More info: http://releases.k8s.io/HEAD/docs/user-guide/volumes.md#awselasticblockstore
|
||||||
|
optional string volumeID = 1;
|
||||||
|
|
||||||
|
// Filesystem type of the volume that you want to mount.
|
||||||
|
// Tip: Ensure that the filesystem type is supported by the host operating system.
|
||||||
|
// Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified.
|
||||||
|
// More info: http://releases.k8s.io/HEAD/docs/user-guide/volumes.md#awselasticblockstore
|
||||||
|
// TODO: how do we prevent errors in the filesystem from compromising the machine
|
||||||
|
optional string fsType = 2;
|
||||||
|
|
||||||
|
// The partition in the volume that you want to mount.
|
||||||
|
// If omitted, the default is to mount by volume name.
|
||||||
|
// Examples: For volume /dev/sda1, you specify the partition as "1".
|
||||||
|
// Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty).
|
||||||
|
optional int32 partition = 3;
|
||||||
|
|
||||||
|
// Specify "true" to force and set the ReadOnly property in VolumeMounts to "true".
|
||||||
|
// If omitted, the default is "false".
|
||||||
|
// More info: http://releases.k8s.io/HEAD/docs/user-guide/volumes.md#awselasticblockstore
|
||||||
|
optional bool readOnly = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Affinity is a group of affinity scheduling rules, currently
|
||||||
|
// only node affinity, but in the future also inter-pod affinity.
|
||||||
|
message Affinity {
|
||||||
|
// Describes node affinity scheduling rules for the pod.
|
||||||
|
optional NodeAffinity nodeAffinity = 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With extensions:
|
||||||
|
|
||||||
|
```
|
||||||
|
syntax = 'proto2';
|
||||||
|
|
||||||
|
package k8s.io.kubernetes.pkg.api.v1;
|
||||||
|
|
||||||
|
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
|
||||||
|
import "k8s.io/kubernetes/pkg/api/resource/generated.proto";
|
||||||
|
import "k8s.io/kubernetes/pkg/api/unversioned/generated.proto";
|
||||||
|
import "k8s.io/kubernetes/pkg/runtime/generated.proto";
|
||||||
|
import "k8s.io/kubernetes/pkg/util/intstr/generated.proto";
|
||||||
|
|
||||||
|
// Package-wide variables from generator "generated".
|
||||||
|
option (gogoproto.marshaler_all) = true;
|
||||||
|
option (gogoproto.sizer_all) = true;
|
||||||
|
option (gogoproto.unmarshaler_all) = true;
|
||||||
|
option (gogoproto.goproto_unrecognized_all) = false;
|
||||||
|
option (gogoproto.goproto_enum_prefix_all) = false;
|
||||||
|
option (gogoproto.goproto_getters_all) = false;
|
||||||
|
option go_package = "v1";
|
||||||
|
|
||||||
|
// Represents a Persistent Disk resource in AWS.
|
||||||
|
//
|
||||||
|
// An AWS EBS disk must exist before mounting to a container. The disk
|
||||||
|
// must also be in the same AWS zone as the kubelet. An AWS EBS disk
|
||||||
|
// can only be mounted as read/write once. AWS EBS volumes support
|
||||||
|
// ownership management and SELinux relabeling.
|
||||||
|
message AWSElasticBlockStoreVolumeSource {
|
||||||
|
// Unique ID of the persistent disk resource in AWS (Amazon EBS volume).
|
||||||
|
// More info: http://releases.k8s.io/HEAD/docs/user-guide/volumes.md#awselasticblockstore
|
||||||
|
optional string volumeID = 1 [(gogoproto.customname) = "VolumeID", (gogoproto.nullable) = false];
|
||||||
|
|
||||||
|
// Filesystem type of the volume that you want to mount.
|
||||||
|
// Tip: Ensure that the filesystem type is supported by the host operating system.
|
||||||
|
// Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified.
|
||||||
|
// More info: http://releases.k8s.io/HEAD/docs/user-guide/volumes.md#awselasticblockstore
|
||||||
|
// TODO: how do we prevent errors in the filesystem from compromising the machine
|
||||||
|
optional string fsType = 2 [(gogoproto.customname) = "FSType", (gogoproto.nullable) = false];
|
||||||
|
|
||||||
|
// The partition in the volume that you want to mount.
|
||||||
|
// If omitted, the default is to mount by volume name.
|
||||||
|
// Examples: For volume /dev/sda1, you specify the partition as "1".
|
||||||
|
// Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty).
|
||||||
|
optional int32 partition = 3 [(gogoproto.customname) = "Partition", (gogoproto.nullable) = false];
|
||||||
|
|
||||||
|
// Specify "true" to force and set the ReadOnly property in VolumeMounts to "true".
|
||||||
|
// If omitted, the default is "false".
|
||||||
|
// More info: http://releases.k8s.io/HEAD/docs/user-guide/volumes.md#awselasticblockstore
|
||||||
|
optional bool readOnly = 4 [(gogoproto.customname) = "ReadOnly", (gogoproto.nullable) = false];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Affinity is a group of affinity scheduling rules, currently
|
||||||
|
// only node affinity, but in the future also inter-pod affinity.
|
||||||
|
message Affinity {
|
||||||
|
// Describes node affinity scheduling rules for the pod.
|
||||||
|
optional NodeAffinity nodeAffinity = 1 [(gogoproto.customname) = "NodeAffinity"];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wire format
|
||||||
|
|
||||||
|
In order to make Protobuf serialized objects recognizable in a binary form,
|
||||||
|
the encoded object must be prefixed by a magic number, and then wrap the
|
||||||
|
non-self-describing Protobuf object in a Protobuf object that contains
|
||||||
|
schema information. The protobuf object is referred to as the `raw` object
|
||||||
|
and the encapsulation is referred to as `wrapper` object.
|
||||||
|
|
||||||
|
The simplest serialization is the raw Protobuf object with no identifying
|
||||||
|
information. In some use cases, we may wish to have the server identify the
|
||||||
|
raw object type on the wire using a protocol dependent format (gRPC uses
|
||||||
|
a type HTTP header). This works when all objects are of the same type, but
|
||||||
|
we occasionally have reasons to encode different object types in the same
|
||||||
|
context (watches, lists of objects on disk, and API calls that may return
|
||||||
|
errors).
|
||||||
|
|
||||||
|
To identify the type of a wrapped Protobuf object, we wrap it in a message
|
||||||
|
in package `k8s.io/kubernetes/pkg/runtime` with message name `Unknown`
|
||||||
|
having the following schema:
|
||||||
|
|
||||||
|
message Unknown {
|
||||||
|
optional TypeMeta typeMeta = 1;
|
||||||
|
optional bytes value = 2;
|
||||||
|
optional string contentEncoding = 3;
|
||||||
|
optional string contentType = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TypeMeta {
|
||||||
|
optional string apiVersion = 1;
|
||||||
|
optional string kind = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
The `value` field is an encoded protobuf object that matches the schema
|
||||||
|
defined in `typeMeta` and has optional `contentType` and `contentEncoding`
|
||||||
|
fields. `contentType` and `contentEncoding` have the same meaning as in
|
||||||
|
HTTP, if unspecified `contentType` means "raw protobuf object", and
|
||||||
|
`contentEncoding` defaults to no encoding. If `contentEncoding` is
|
||||||
|
specified, the defined transformation should be applied to `value` before
|
||||||
|
attempting to decode the value.
|
||||||
|
|
||||||
|
The `contentType` field is required to support objects without a defined
|
||||||
|
protobuf schema, like the ThirdPartyResource or templates. Those objects
|
||||||
|
would have to be encoded as JSON or another structure compatible form
|
||||||
|
when used with Protobuf. Generic clients must deal with the possibility
|
||||||
|
that the returned value is not in the known type.
|
||||||
|
|
||||||
|
We add the `contentEncoding` field here to preserve room for future
|
||||||
|
optimizations like encryption-at-rest or compression of the nested content.
|
||||||
|
Clients should error when receiving an encoding they do not support.
|
||||||
|
Negotioting encoding is not defined here, but introducing new encodings
|
||||||
|
is similar to introducing a schema change or new API version.
|
||||||
|
|
||||||
|
A client should use the `kind` and `apiVersion` fields to identify the
|
||||||
|
correct protobuf IDL for that message and version, and then decode the
|
||||||
|
`bytes` field into that Protobuf message.
|
||||||
|
|
||||||
|
Any Unknown value written to stable storage will be given a 4 byte prefix
|
||||||
|
`0x6b, 0x38, 0x73, 0x00`, which correspond to `k8s` followed by a zero byte.
|
||||||
|
The content-type `application/vnd.kubernetes.protobuf` is defined as
|
||||||
|
representing the following schema:
|
||||||
|
|
||||||
|
MESSAGE = '0x6b 0x38 0x73 0x00' UNKNOWN
|
||||||
|
UNKNOWN = <protobuf serialization of k8s.io/kubernetes/pkg/runtime#Unknown>
|
||||||
|
|
||||||
|
A client should check for the first four bytes, then perform a protobuf
|
||||||
|
deserialization of the remaining bytes into the `runtime.Unknown` type.
|
||||||
|
|
||||||
|
## Streaming wire format
|
||||||
|
|
||||||
|
While the majority of Kubernetes APIs return single objects that can vary
|
||||||
|
in type (Pod vs Status, PodList vs Status), the watch APIs return a stream
|
||||||
|
of identical objects (Events). At the time of this writing, this is the only
|
||||||
|
current or anticipated streaming RESTful protocol (logging, port-forwarding,
|
||||||
|
and exec protocols use a binary protocol over Websockets or SPDY).
|
||||||
|
|
||||||
|
In JSON, this API is implemented as a stream of JSON objects that are
|
||||||
|
separated by their syntax (the closing `}` brace is followed by whitespace
|
||||||
|
and the opening `{` brace starts the next object). There is no formal
|
||||||
|
specification covering this pattern, nor a unique content-type. Each object
|
||||||
|
is expected to be of type `watch.Event`, and is currently not self describing.
|
||||||
|
|
||||||
|
For expediency and consistency, we define a format for Protobuf watch Events
|
||||||
|
that is similar. Since protobuf messages are not self describing, we must
|
||||||
|
identify the boundaries between Events (a `frame`). We do that by prefixing
|
||||||
|
each frame of N bytes with a 4-byte, big-endian, unsigned integer with the
|
||||||
|
value N.
|
||||||
|
|
||||||
|
frame = length body
|
||||||
|
length = 32-bit unsigned integer in big-endian order, denoting length of
|
||||||
|
bytes of body
|
||||||
|
body = <bytes>
|
||||||
|
|
||||||
|
# frame containing a single byte 0a
|
||||||
|
frame = 01 00 00 00 0a
|
||||||
|
|
||||||
|
# equivalent JSON
|
||||||
|
frame = {"type": "added", ...}
|
||||||
|
|
||||||
|
The body of each frame is a serialized Protobuf message `Event` in package
|
||||||
|
`k8s.io/kubernetes/pkg/watch/versioned`. The content type used for this
|
||||||
|
format is `application/vnd.kubernetes.protobuf;type=watch`.
|
||||||
|
|
||||||
|
## Negotiation
|
||||||
|
|
||||||
|
To allow clients to request protobuf serialization optionally, the `Accept`
|
||||||
|
HTTP header is used by callers to indicate which serialization they wish
|
||||||
|
returned in the response, and the `Content-Type` header is used to tell the
|
||||||
|
server how to decode the bytes sent in the request (for DELETE/POST/PUT/PATCH
|
||||||
|
requests). The server will return 406 if the `Accept` header is not
|
||||||
|
recognized or 415 if the `Content-Type` is not recognized (as defined in
|
||||||
|
RFC2616).
|
||||||
|
|
||||||
|
To be backwards compatible, clients must consider that the server does not
|
||||||
|
support protobuf serialization. A number of options are possible:
|
||||||
|
|
||||||
|
### Preconfigured
|
||||||
|
|
||||||
|
Clients can have a configuration setting that instructs them which version
|
||||||
|
to use. This is the simplest option, but requires intervention when the
|
||||||
|
component upgrades to protobuf.
|
||||||
|
|
||||||
|
### Include serialization information in api-discovery
|
||||||
|
|
||||||
|
Servers can define the list of content types they accept and return in
|
||||||
|
their API discovery docs, and clients can use protobuf if they support it.
|
||||||
|
Allows dynamic configuration during upgrade if the client is already using
|
||||||
|
API-discovery.
|
||||||
|
|
||||||
|
### Optimistically attempt to send and receive requests using protobuf
|
||||||
|
|
||||||
|
Using multiple `Accept` values:
|
||||||
|
|
||||||
|
Accept: application/vnd.kubernetes.protobuf, application/json
|
||||||
|
|
||||||
|
clients can indicate their preferences and handle the returned
|
||||||
|
`Content-Type` using whatever the server responds. On update operations,
|
||||||
|
clients can try protobuf and if they receive a 415 error, record that and
|
||||||
|
fall back to JSON. Allows the client to be backwards compatible with
|
||||||
|
any server, but comes at the cost of some implementation complexity.
|
||||||
|
|
||||||
|
|
||||||
|
## Generation process
|
||||||
|
|
||||||
|
Generation proceeds in five phases:
|
||||||
|
|
||||||
|
1. Generate a gogo-protobuf annotated IDL from the source Go struct.
|
||||||
|
2. Generate temporary Go structs from the IDL using gogo-protobuf.
|
||||||
|
3. Generate marshaller/unmarshallers based on the IDL using gogo-protobuf.
|
||||||
|
4. Take all tag numbers generated for the IDL and apply them as struct tags
|
||||||
|
to the original Go types.
|
||||||
|
5. Generate a final IDL without gogo-protobuf annotations as the canonical IDL.
|
||||||
|
|
||||||
|
The output is a `generated.proto` file in each package containing a standard
|
||||||
|
proto2 IDL, and a `generated.pb.go` file in each package that contains the
|
||||||
|
generated marshal/unmarshallers.
|
||||||
|
|
||||||
|
The Go struct generated by gogo-protobuf from the first IDL must be identical
|
||||||
|
to the origin struct - a number of changes have been made to gogo-protobuf
|
||||||
|
to ensure exact 1-1 conversion. A small number of additions may be necessary
|
||||||
|
in the future if we introduce more exotic field types (Go type aliases, maps
|
||||||
|
with aliased Go types, and embedded fields were fixed). If they are identical,
|
||||||
|
the output marshallers/unmarshallers can then work on the origin struct.
|
||||||
|
|
||||||
|
Whenever a new field is added, generation will assign that field a unique tag
|
||||||
|
and the 4th phase will write that tag back to the origin Go struct as a `protobuf`
|
||||||
|
struct tag. This ensures subsequent generation passes are stable, even in the
|
||||||
|
face of internal refactors. The first time a field is added, the author will
|
||||||
|
need to check in both the new IDL AND the protobuf struct tag changes.
|
||||||
|
|
||||||
|
The second IDL is generated without gogo-protobuf annotations to allow clients
|
||||||
|
in other languages to generate easily.
|
||||||
|
|
||||||
|
Any errors in the generation process are considered fatal and must be resolved
|
||||||
|
early (being unable to identify a field type for conversion, duplicate fields,
|
||||||
|
duplicate tags, protoc errors, etc). The conversion fuzzer is used to ensure
|
||||||
|
that a Go struct can be round-tripped to protobuf and back, as we do for JSON
|
||||||
|
and conversion testing.
|
||||||
|
|
||||||
|
|
||||||
|
## Changes to development process
|
||||||
|
|
||||||
|
All existing API change rules would still apply. New fields added would be
|
||||||
|
automatically assigned a tag by the generation process. New API versions will
|
||||||
|
have a new proto IDL, and field name and changes across API versions would be
|
||||||
|
handled using our existing API change rules. Tags cannot change within an
|
||||||
|
API version.
|
||||||
|
|
||||||
|
Generation would be done by developers and then checked into source control,
|
||||||
|
like conversions and ugorji JSON codecs.
|
||||||
|
|
||||||
|
Because protoc is not packaged well across all platforms, we will add it to
|
||||||
|
the `kube-cross` Docker image and developers can use that to generate
|
||||||
|
updated protobufs. Protobuf 3 beta is required.
|
||||||
|
|
||||||
|
The generated protobuf will be checked with a verify script before merging.
|
||||||
|
|
||||||
|
|
||||||
|
## Implications
|
||||||
|
|
||||||
|
* The generated marshal code is large and will increase build times and binary
|
||||||
|
size. We may be able to remove ugorji after protobuf is added, since the
|
||||||
|
bulk of our decoding would switch to protobuf.
|
||||||
|
* The protobuf schema is naive, which means it may not be as a minimal as
|
||||||
|
possible.
|
||||||
|
* Debugging of protobuf related errors is harder due to the binary nature of
|
||||||
|
the format.
|
||||||
|
* Migrating API object storage from JSON to protobuf will require that all
|
||||||
|
API servers are upgraded before beginning to write protobuf to disk, since
|
||||||
|
old servers won't recognize protobuf.
|
||||||
|
* Transport of protobuf between etcd and the api server will be less efficient
|
||||||
|
in etcd2 than etcd3 (since etcd2 must encode binary values returned as JSON).
|
||||||
|
Should still be smaller than current JSON request.
|
||||||
|
* Third-party API objects must be stored as JSON inside of a protobuf wrapper
|
||||||
|
in etcd, and the API endpoints will not benefit from clients that speak
|
||||||
|
protobuf. Clients will have to deal with some API objects not supporting
|
||||||
|
protobuf.
|
||||||
|
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
* Is supporting stored protobuf files on disk in the kubectl client worth it?
|
||||||
|
|
||||||
|
|
||||||
|
<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
|
||||||
|
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/proposals/protobuf.md?pixel)]()
|
||||||
|
<!-- END MUNGE: GENERATED_ANALYTICS -->
|
Loading…
Reference in New Issue