What a systems analyst needs to know about gRPC

Irina
IT’s Tinkoff
Published in
16 min readFeb 16, 2024

Hello everyone, I’m Irina, a system analyst in Tinkoff. We develop common libraries used by all mobile applications in the Tinkoff ecosystem.

I am going to talk about the gRPC protocol. There are a lot of articles on Habr about the intricacies of its implementation aimed at developers. But I would like to introduce it to my colleagues. We’ll see how the protocol works and how to write a contract so that it’s understood, but we won’t delve into the intricacies of programme implementation, we’ll broaden our horizons. Maybe gRPC will become a cool solution for some people in their work.

What is gRPC

RPC is a remote procedure call. The client sends a request to a process on the server, which constantly listens for remote procedure calls. The request contains the server function to be called and all parameters to be passed. The process catches the request and executes it. The interaction between client and server is as if the client API request is a local operation or the request is internal server code

gRPC is Google’s RPC framework. gRPC and REST are two ways to develop an API, a mechanism that allows two software components to communicate with each other using a set of definitions and protocols. Clients send information requests to a server — the server provides responses. The main difference between gRPC and REST:

  • In gRPC, one component, the client, calls certain functions in another software component, the server. In this case, the software implementation of the client and server does not matter much due to the cross-platform nature of the gRPC protocol.
  • In REST, instead of calling functions, the client requests or updates data on the server.
Independence of the interaction implementation from the chosen component implementation language. No matter what language the client and server are written in, the interaction is organised in the same way.

There are four uses of RPC in general and gRPC in particular.

Unary RPC, 1–1. A synchronous client request that is blocked until a response is received from the server. The client cannot do anything until a response is received or the request times out.

Unary RPC

Client streaming RPC, N — 1. When the server is connected, the client starts streaming messages to it. The client makes a request to the server in the form of a sequence of N messages and receives a response in the form of a single message from the server.

Client streaming

Server streaming RPC, 1 — N. When the client connects, the server opens the stream and starts sending messages. The client makes a request to the server in the form of a single message and receives a response in the form of a sequence of N messages from the server.

Server streaming

Bidirectional streaming, N — N. The client initializes the connection, two streams are created. In general, the client makes a request to the server in the form of a sequence of N messages and receives a response in the form of a sequence of N messages from the server. The server can send the initial data on connection or respond to each client request in a ping-pong type of manner. The two threads operate independently, so clients and servers can read and write in either order. For example, a server may wait until it has received all client messages before writing its responses, or it may alternately read messages and then write responses to them immediately. Some other combination of reading and writing is also possible. The order of messages in each thread is preserved.

Bidirectional streaming

Let’s see how to recognize uses from each other in a proto-file (a Protobuf IDL contract):

service Greeter{
rpc SayHello(HelloRequest) returns (HelloReply) {} // Unary
rpc GladToSeeMe(HelloRequest) returns (stream HelloReply){} // Server streaming
rpc GladToSeeYou(stream HelloRequest) returns (HelloReply){} // Client streaming
rpc BothGladToSee(stream HelloRequest) returns (stream HelloReply){} // Bidirectional streaming
}

On the web, unary implementations are more often used for front and backend communication. Streaming support has been introduced relatively recently on the front end, so it’s possible to use the others as well. In mobile apps, there is support for Kotlin out of the box and Swift in development. There are no limitations for back-to-back communication.

Writing a contract, in my subjective opinion, looks elegant and friendly. Reading a proto-file is much easier and more pleasant than a REST swagger.

A contract is a set of methods organized into services. The method description consists of a name, a request message and a response message. In the request and response you can either specify standard data types or compose your own object with the necessary filling. In the second case, you will need to give it a name and describe it with the keyword message.

Service, method and message composition

The rules by which the query is built:

  • The method should receive something as input and return something as output — HelloRequest and HelloResponse. If you don’t need to receive or send any data, you can replace it with an empty value google.protobuf.Empty. Then no data will be sent in response to the request or in the request, but a response code will be sent. The response is 2xx if everything is successful, or 4xx/5xx if there are problems. This reduces the load on the system and increases security by sending only the essentials.
  • The method must specify the data types it operates on. In the example above, these are string for name and message, and HelloRequest and HelloResponse for the request itself. If the data type is not known in advance, you can use google.protobuf.Any, which replaces any data type.
  • The field in the message must have a non-repeating sequence number. If a field has been used before and deleted, that number cannot be reused. Such fields can be reserved with the reserved keyword or by leaving comments.

To describe the contract, keywords are used:

  • ‘syntax’ — the current version of the syntax. Now, as a rule, new services are usually written in proto3.
  • ‘import’ — to import standard packages. For example, “google/protobuf/timestamp.proto” will load the timestamp data type.
  • ‘service’ — for declaring a service. GRPC methods are combined into a service
  • ‘rpc’ — to declare the method: its name and request message.
  • ‘returns’ — for declaring a method’s response, response-message.
  • ‘message’ — for declaring an object.
  • ‘enum’ — to declare an enumeration.
  • ‘repeated’ — to declare a repeated field.
  • ‘reserved’ — to reserve a field.
  • ‘optional’ or ‘required’; — to declare an optional or required field. This functionality is removed in proto3.
  • ‘oneof’ — to declare a complex field that can have one of several values. This is a rather heavy and difficult to handle construct that can weigh a lot and waste a lot of resources, so it is better not to use it, but replace it with something else. For example, for a string that will actually come with a json.
  • A whole set of standard data types like bool, string, int64 and others.

We have figured out how to read proto-contracts, now let’s look at an example service:

syntax = "proto3";
import "google/protobuf/any.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";

service ProductService{
// method of adding a book to the catalog
rpc AddProduct(AddProductRequest) returns (google.protobuf.Empty) {}
// method for retrieving a book by ID
rpc GetProductById(GetProductByIdRequest) returns (GetProductByIdResponse) {}
// method of retrieving all the books
rpc GetProductsList(GetProductsListRequest) returns (GetProductsListResponse) {}
}

message AddProductRequest{
BookInfo add_book_info = 1;
}

message BookInfo{
// these fields can't be used
reserved 6, 15, 9 to 11;
// author data is not uniquely defined, and any type (string or array, for example) can come in
google.protobuf.Any author = 1;
string name = 2;
int32 price = 3;
Type type = 4;
bool in_store = 5;
// you can transfer files in the bytes type, but it is better to replace it with a reference in s3
bytes book_cover = 7;
// here we will use only one of the listed fields, for the user it looks like, for example, a dynamic input form
oneof additional_fields{
// use with TYPE_UNDEFINED - it will come in handy when adding new values to enum Type: created BookInfo objects will accept this type by default
AdditionalFieldsUndefined additional_fields_undefined = 8;
AdditionalFieldsDetective additional_fields_detective = 12;
}
}

enum Type{
TYPE_UNDEFINED = 0;
TYPE_DETECTIVE = 1;
}

message AdditionalFieldsUndefined{
string description = 1;
}

message AdditionalFieldsDetective{
string description = 1;
string period = 2;
}

message GetProductByIdRequest{
// protobuf doesn't know what uuid is yet, so it can be passed by string, bytes or a custom type
string id = 1;
}

message GetProductByIdResponse{
string id = 1;
BookInfo get_book_info = 2;
google.protobuf.Timestamp created_at = 3;
}

// limit and offset are needed for pagination on the backend. If we have an infinite feed, we can pass google.protobuf.Empty instead of the GetProductsListRequest object.
message GetProductsListRequest{
int32 limit = 1;
int32 offset = 2;
}

message GetProductsListResponse{
repeated BookInfo get_book_list_info = 1;
}

In the sample library description I tried to collect the most common (good and not so good) contract implementation patterns. The code implements methods of adding a book to the catalog, getting a specific book by ID and the full list of books. The syntax version is proto3, importing data types — any, empty, timestamp and ProductService of three methods: AddProduct, GetProductById, GetProductsList.

An important feature of gRPC is strict typing. When gRPC generates a file, a certain amount of bytes and position in the future base64 is allocated for each field. It means that for field data with type bool and sequence number 1 a chunk will be allocated at the beginning of the file, and field data with type string and number 2 will be located right after it. These fields will be allocated as much memory as is required for bool and string, and their position in the file will be clearly fixed. Even if the fields are deleted, the space will remain reserved and these bytes will be filled with empty values. Therefore, if you change the type or number of a field, this typing feature will cause errors when compiling the contract. To avoid bloating the final file to huge sizes, it is important to initially clearly understand what data should be passed in the object.

Example of data substitution into a contract and a base64 string:

  • Contract
syntax = "proto3";

message exampleMessage{
string exampleString = 1;
bytes exampleBytes = 2;
uint32 exampleInt = 3;
repeated uint32 repeatedInt = 4;
}
  • Substituted data
{
"exampleString": "test",
"exampleBytes": [255, 15],
"exampleInt": 2,
"repeatedInt": [2, 4]
}
  • Encoded data
Hex: 0a04746573741202ff0f180222020204
Base64: CgR0ZXN0EgL/DxgCIgICBA==

REST API and SOAP API features

I’m not interested in talking about a spherical horse in a vacuum, so I suggest comparing the characteristics of REST API, SOAP API and gRPC API.

SOAP API Features

Framework: HTTP1.1.

The Approach: service-oriented design. A client requests a service or function from the server that may affect server resources.

Contract: WSDL schemas are mandatory.

Format of data to be transmitted:

Request: XML.

Response: XML.

A client and server using SOAP always exchange standardized data

How data is transmitted: uses only HTTP-POST requests.

Number of endpoints (application entry points): 1.

Web operation: no tolerance.

Documentation: WSDL schemas are difficult to write and maintain.

For what architecture: complex architecture beyond CRUD. SOAP is used by many banks.

Weight: XML weighs more than its JSON and base64 counterparts and is mostly used in legacy systems that were developed in the late 1990s and early 2000s.

Advantages:

  • Language-independent.
  • Built-in error handling.
  • Built-in security protocol.
  • Self-documenting.

Disadvantages:

  • Heavy XML.
  • Complex set of rules for contract description.
  • Long time to update messages — features of the schema.
  • Constant need to encode data on the server before transmission over communication channels and its subsequent decoding on the client. Physical layer of exchange protocols understands only sequences of binary data, this leads to increased transmission time, complexity of information framing and risks of loss of individual data packets.

Why use: Fintech and other long massive projects with complex architecture, legacy from the 90s-00s, or historically chosen SOAP that can’t be abandoned. For a new project, it may be worth looking at alternatives.

REST API Features

Framework: HTTP1.1.

Server connection: 1–1.

The Approach: object-oriented design. The client requests the server to create, share, or modify resources.

Contract: not necessarily OpenAPI, endpoints may not be documented.

How data is transmitted:

Request: mostly JSON.

Response: json with all data found on the server for this endpoint.

The client and server using REST do not standardize data

How data is transmitted: uses HTTP as a transport protocol, creates a one-time connection between two points: create connection, send and close. The client sends messages to the API and receives a response immediately or waits for a response to be generated. The client and server don’t need to know about internal data. Uses four basic HTTP methods: GET, POST, PUT, DELETE.

Number of endpoints: can be many — either one or many more, without limit.

Web work: without additional effort

Documentation: in JSON, you need to document the fields it contains and their types. Often the information may be inaccurate, incomplete or outdated.

For which architecture: predominantly CRUD. The most popular API architecture for web services and microservice architectures.

Weight: JSON is smaller than XML, but larger than protobuf.

Advantages:

  • The client is separated from the server.
  • No long connection with status tracking → resource saving.
  • Scalability.
  • Easy to use and understand, large community.
  • There is a standard list of error codes, but everyone uses it in their own way.
  • Can be implemented in many different formats without standard software.
  • Caching at HTTP level without additional modules.

Disadvantages:

  • Excessive load on the network.
  • Excessive or insufficient data sampling.
  • No documentation and standardization.
  • No standard for the use of response codes, so errors can often be transmitted in successful codes.
  • Constant need to encode data on the server before transmission over communication channels and then decode it on the client. The physical layer of communication protocols only understands sequences of binary data. This leads to increased transmission time, complexity of information framing and increased probability of loss of individual data packets.

Why to use: due to its simple implementation and display of data structure, readability, it is easy to work with for beginner programmers.

REST API use cases:

  • Web architectures.
  • Public APIs for easy understanding by external users.
  • Easy data exchange.

gRPC API features

RPC and REST are two different design approaches. REST was started as an alternative to RPC to solve the main problem it had — the difficulty of integration due to the dependence on the development language and the risk of exposing the internal features of the system. REST was no longer as lightweight as RPC and created a lot of metadata in its messages. This most likely led to the second birth of RPC in the form of Facebook’s GraphQL and Google’s gRPC.

Google developed its framework for internal microservices needs, but eventually opened up its source code for widespread use. Now gRPC is still a fairly new protocol and not on everyone’s radar. But it is already used by companies with highly loaded systems, such as Google, IBM, Netflix, Twitter and others. Below are its characteristics.

Framework: HTTP2 (works in two directions and is faster than HTTP1.1).

Server connection: 1–1, 1 — N, N — N.

The Approach: service-oriented design. A client requests a service or function from the server that may affect server resources.

The contract: is necessarily written according to the Protocol Buffers standard, compiled by the internal protoc compiler, which generates the necessary source code of classes from definitions in the proto-file.

How data is transmitted:

Request: binary file — protobuf.

Answer: binary file — protobuf.

Client and server using gRPC always exchange standardized data

How data is transmitted: creates a permanent connection — a socket — between two points, through which it transfers a binary file and calls a function remotely by passing parameters to it.

Sends messages both ways: gRPC provides bidirectional data streaming — both client and server can simultaneously send and receive multiple requests and responses within a single connection. REST can’t do that. Both client and server need the same Protocol Buffer file that defines the data format. Uses only HTTP-POST requests.

Number of endpoints: 1.

Web work with a little extra effort:

  • gRPC works on HTTP2 and transfers a binary file, while JS in the browser works on HTTP1 and interacts only with text files. That’s why there is gRPC-WEB, which can put base64 in the body of a text message, and then JS translates base64 into JSON by a separate library. gRPC-WEB is a protocol separate from gRPC, exists only in the browser and acts as a translation layer between gRPC and the application in the browser.
  • Front-end codgen does not know where a field is mandatory and where it is not. All non-basic types are generated as optional.
  • Streams for frontend became available not so long ago. It used to be that you had to write a rest point to send a file.

Documentation: A well-defined and self-documenting schema. The Protobuf API generates code, the code will not be unsynchronized with the documentation. When generating code from Protobuf, a basic check is passed — the generated code does not accept fields of the wrong type.

For what architecture: mostly CRUD.

Weight: less than JSON.

Advantages:

  • High performance and low network load.
  • Holds connection, no need to waste time on connection.
  • You can put client timeout and thus save resources.
  • Strict specification of data types. Under each field allocates a set of bits in order.
  • Standardization of error codes embedded in protobuf.
  • Self-generates source code by proto.
  • Self-documenting.
  • Language-independent, the contract is the same everywhere.
  • Can be used to manage containers in k8s and storage systems.

Disadvantages:

  • Does not work without gRPC-WEB in the browser.
  • A person will not read the message without a decoder.
  • It requires gRPC software on both client and server side to work.

Why to use: is designed to enable developers to create high-performance APIs for microservice architectures in distributed data centers. This includes microservice architectures in multiple programming languages, for which APIs are unlikely to change over time. It is also well suited for back-end systems that require real-time streaming and large data loads.

gRPC API is better to use when:

  • High-performance systems are created. For example, in high-load systems where high throughput and performance are needed with low requirements for the network and hardware resources of the server and client — the IoT platform. And also in distributed computing or testing, when resource-intensive tasks are distributed among several servers or when testing on different platforms.
  • Big data is being loaded.
  • Real-time or streaming applications are being developed.
  • Remote administration is needed — managing configuration files from a single node.
  • Tunneling is required — going beyond the boundaries of the routed network.

In gRPC, you can’t:

  • Reuse field numbers. This is a peculiarity of the codgen, everything breaks.
  • Change field type. Delete and add a new one with a new number. Codgen features, everything breaks.
  • Forget to reserve deleted fields. They can be reused by someone else. So what? That’s right, everything breaks. This applies to field names and numbers as well.
  • Add mandatory fields. It’s hard to remove. If you need to make a field mandatory, make it so in the code and write a comment to the contract.
  • Add a lot of fields to an object. It will weigh a lot and in some cases won’t even compile. In Java, for example, there is a hard limit on method size.
  • Forgetting to UNDEFINED with the number 0 in the enumeration. First, for proto2 and proto3 compatibility, this is the only option where nothing will break when adding a new value to an enum. Second, by explicitly writing the value 0, we save ourselves from logical errors. In the book site example, if there is no UNDEFINED value, all books will automatically be assigned the type “detective”. But, if there is such a value, the user can be shown a mandatory field with a drop-down list with only one choice and will be forced to fill it in.
  • Reinventing the wheel. Almost all data types are either imported by default (string) or plug in (timestamp). Look at the documentation before making a custom type to save resources.
  • Use constants and language keywords in enum. Everything will break.
  • Change the default value. Backward compatibility will break, that’s why this functionality was removed in proto3.
  • It is not recommended to remove it if it was there. We lose the whole message or a specific field depending on the version. Either way it’s bad.

My fellow developers and analysts and I have identified a number of advantages and disadvantages of using gRPC. If you know any more, please add them in the comments.

Advantages:

  • Back-to-back communication. Fast and stable.
  • Proto makes life much easier for developers of the service for which the contract is written. If the answer changes, the service will know about it immediately, not when they update the documentation (if they do). Swagger is always lazy for everyone to write.
  • Fast, base64 weighs less compared to JSON. Suitable for microservice architecture.
  • Secure.
  • Contract is human-readable.
  • Typization.

Disadvantages:

  • Doesn’t work fully with the front end, so it may not work properly if not configured correctly. And gRPC-web is absolutely necessary.
  • Front-end codgen does not know where a field is mandatory and where it is not. All non-basic types are generated as optional.
  • It follows from the first and second point that it is not particularly good on the web. But it is used.
  • Codgen does not let you change the field type — only delete and add a new one with a new number (typing peculiarities that people forget about).

Instead of concluding

The choice of technology for a project is a complex issue and should be decided jointly by architects and developers. A systems analyst is more of a theorist than a practical technical specialist, but should have a good understanding of technologies and a consultative voice. We don’t usually write code, we don’t know the intricacies of using this or that technology, and the final implementation doesn’t rest on our shoulders. But we can share our experiences from other projects. And if today’s introduction to gRPC has inspired you to explore it further and present it to your team, that’s great.

For the most curious: what is g in gRPC? In each version of gRPC the value of g changes — in the current version 1.61 at the time of writing it means grand. You can see the values in previous versions on GitHub.

--

--