Building gRPC Services
Designing a gRPC Service
The design process was much smoother than the RESTful interface in terms of the API specification. The endpoints were quickly defined, written, and understood, all self-contained in a single protobuf file. However, there are very few rules on what these endpoints could be, which is generally the case with RPC. We needed to be strict on defining the role of the microservice, ensuring the endpoints reflected this role. We focused on making sure each endpoint was heavily commented, which helped our cross-continent teams avoid too many integration problems.
As an aside, it’s important to write documentation and communicate common use cases involving these endpoints. This is typically outside the scope of the protobuf file but has a large impact on what the endpoints should be.
Developing a gRPC Service
Implementing gRPC was initially a rough road. Our team was unfamiliar with best practices so we had to spend more time than we wanted on building tools and testing our servers. At the time, there was a lack of good tutorials and examples for us to copy from, so the first servers created were based on trial and error. gRPC could benefit from some clear documentation and examples about the concepts it uses like stubs and channels.
This page is a good start at explaining the basics; however, we would have felt more confident if we knew more. For example, knowing how channels handle connection failure without having to check the client’s source code. We also found most of the configurable options were only documented in source code and took a lot of time and effort to find. We were never really sure if the option had worked, which meant we ended up testing most options we changed. The barrier to entry for developing and testing gRPC was quite high. More intuitive documentation and tools are essential if gRPC is here to stay, and there do seem to be more and more examples coming out.
Handling Opinionated Languages
There were a few gotchas along the way, including getting familiar with how protobufs handle default values. For example, in the protobuf format, strings are primitive and have a default value of
""
. Java developers identify null
as the default value for strings. But beware, setting null
for primitive protobuf fields like strings will cause runtime exceptions in Java.
The client libraries try to protect against invalid field values before transmitting and assume you are trying to set
null
to a primitive field. These safeguards are present to protect against conflicting opinions between different language applications e.g. ""
, nil
, and null
for strings. This led us to create wrappers for these messages to avoid confusion once you were in the application’s native language. On the whole, we’ve had very little need to dive into and debug the messages themselves. Client library implementations are very reliable at encoding and decoding messages.How to Debug a gRPC Service
When we started using gRPC, the testing tools available were limited. Developers want to cURL their endpoints, but with gRPC equivalents to familiar tools like Postman either don’t exist or are not very mature. These tools need to support both encoding and decoding messages using the appropriate protobuf file, and be able to support HTTP/2. You can actually cURL a gRPC endpoint directly, but this is far from a streamlined process. Some useful tools we came across were:
- protoc-gen-lint, linting for protobufs — This tool checks for any deviations from Google’s Protocol Buffer style guide. We use this as part of our build process to enforce coding standards and catch basic errors. It’s good for spotting invalid message structures and typos.
- grpcc, CLI for a gRPC Server — This uses Node REPL to interact with a gRPC service via its protobuf file, and is very useful for quickly testing an endpoint. It’s a little rough around the edges, but looks promising for a standalone tool to hit endpoints.
- omgrpc, GUI client — Described as Postman for gRPC endpoints, this tool provides a visual way to interact with your gRPC services.
- awesome gRPC — A great collection of resources currently available for gRPC
Let Me cURL My gRPC Endpoint
In addition to these tools, we managed to re-enable our existing REST tools by using Envoy and JSON transcoding. This works by sending HTTP/1.1 requests with a JSON payload to an Envoy proxy configured as a gRPC-JSON transcoder. Envoy will translate the request into the corresponding gRPC call, with the response message translated back into JSON.
Step 1: Annotate the service protobuf file with Google APIs. This is an example of a service with an endpoint that has been annotated so it can be invoked with a POST request to
/errorclass
.
Step 2: Generate a proto descriptor set that describes the gRPC service. This requires the protocol compiler, or protoc installed (how to install it can be found here). Follow this guide on generating a proto descriptor setwith protoc.
Step 3: Run Envoy with a JSON transcoder, configured to use the proto descriptor set. Here is an example of an Envoy configuration file with the gRPC server listening on port 4000.
Step 4: cURL the gRPC service via the proxy. In this example, we set up the proxy to listen to port 3000.
Although this technique can be very useful, it does require us to “muddy up” our protobuf files with additional dependencies, and manage the Envoy configurations to talk to these services. To streamline the process, we scripted the steps and ran an Envoy instance inside a Docker container, taking a protobuf file as a parameter. This allowed us to quickly set a JSON transcoding proxy for any gRPC services in seconds.
Running the gRPC Ruby Client Library on Alpine
We did encounter some trouble running the Ruby version of a gRPC client. When we came to build the applications container, we got the error:
Most gRPC client libraries are written on top of a shared core library, written in C. The issue was due to using the an alpine version of Ruby with a precompiled version of the gRPC library requiring
glibc
. This was solved by setting BUNDLE_FORCE_RUBY_PLATFORM=1
in the environment when running bundle install
which will build the gems from source rather than using the precompiled version.
Source
Comments
Post a Comment