visit
{
"docs": {
"name": "name_for_documents",
"department": {
"code": "uuid_code",
"time": 123123123,
"employee": {
"name": "Ivan",
"surname": "Polich",
"code": "uuidv4"
}
},
"price": {
"categoryA": "1.0",
"categoryB": "2.0",
"categoryC": "3.0"
},
"owner": {
"uuid": "uuid",
"secret": "dsfdwr32fd0fdspsod"
},
"data": {
"transaction": {
"type": "CODE",
"uuid": "df23erd0sfods0fw",
"pointCode": "01"
}
},
"delivery": {
"company": "TTC",
"address": {
"code": "01",
"country": "uk",
"street": "Main avenue",
"apartment": "1A"
}
},
"goods": [
{
"name": "toaster v12",
"amount": 15,
"code": "12312reds12313e1"
}
]
}
}
Nothing special, we are going to create a small service with Gin gonic and http lib. As a good example, “Golang RESTful API”:
Let’s code smh like this. Full code here:
const (
post = "/report"
get = "/reports"
TTL = 5
)
func main() {
router := gin.Default()
p := ginprometheus.NewPrometheus("gin")
p.Use(router)
sv := service.NewReportService()
gw := middle.NewHttpGateway(*sv)
router.POST(post, gw.Save)
router.GET(get, gw.Find)
srv := &http.Server{
Addr: "localhost:8080",
Handler: router,
}
}
// BenchmarkCreateAndMarshal-10 168706 7045 ns/op
func BenchmarkCreateAndMarshal(b *testing.B) {
for i := 0; i < b.N; i++ {
doc := createDoc()
_ = doc.Docs.Name // for tests
bt, err := json.Marshal(doc)
if err != nil {
log.Fatal("parse error")
}
parsedDoc := new(m.Document)
if json.Unmarshal(bt, parsedDoc) != nil {
log.Fatal("parse error")
}
_ = parsedDoc.Docs.Name
}
}
BenchmarkCreateAndMarshal-10
: This is the output line provided by the Go testing tool.
168706
: This is the number of iterations that were executed during the test.
7045 ns/op
: This is the average time taken for one iteration in nanoseconds. Here, ns/op
stands for nanoseconds per operation.
Thus, the result indicates that the BenchmarkCreateAndMarshal
function executes at approximately 7045 nanoseconds per operation over 168706 iterations.
Now and again, no worries if you’re new to gRPC! Taking it step by step is a great approach. I remember being in the same boat — copying and pasting from the documentation is a common practice when diving into new technologies. It’s a fantastic way to grasp the concepts and understand how things work. Keep exploring the guide, and don’t hesitate to reach out if you have any questions along the way. Happy coding! 🚀 Read more here:
syntax = "proto3";
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
}
And generate it:
protoc - python_out=. example.proto
This will create the file `example_pb2.py`, which contains the generated code for working with the data defined in `example.proto`. Usage in Python:
import example_pb2
# Create a Person object
person = example_pb2.Person()
person.name = "John"
person.id = 123
person.email = "[email protected]"
# Serialize to binary format
serialized_data = person.SerializeToString()
# Deserialize from binary format
new_person = example_pb2.Person()
new_person.ParseFromString(serialized_data)
Person person = {
name: "John Doe",
id: 123,
email: "[email protected]"
};
08 4A 6F 68 6E 20 44 6F 65 10 7B 1A 14 6A 6F 68 6E 40 65 78 61 6D 70 6C 65 2E 63 6F 6D
name
field), followed by the field’s length.id
field), followed by the value 123
in variable-length encoding (Varint).email
field), followed by the string length 20
and the ASCII codes for the string “[email protected].”syntax = "proto3";
package docs;
option go_package = "proto-docs-service/docs";
service DocumentService {
rpc GetAllByLimitAndOffset(GetAllByLimitAndOffsetRequest) returns (GetAllByLimitAndOffsetResponse) {}
rpc Save(SaveRequest) returns (SaveResponse) {}
}
message GetAllByLimitAndOffsetRequest {
int32 limit = 1;
int32 offset = 2;
}
message GetAllByLimitAndOffsetResponse {
repeated Document documents = 1;
}
message SaveRequest {
Document document = 1;
}
message SaveResponse {
string message = 1;
}
message Document {
string name = 1;
Department department = 2;
Price price = 3;
Owner owner = 4;
Data data = 5;
Delivery delivery = 6;
repeated Goods goods = 7;
}
message Department {
string code = 1;
int64 time = 2;
Employee employee = 3;
}
message Employee {
string name = 1;
string surname = 2;
string code = 3;
}
message Price {
string categoryA = 1;
string categoryB = 2;
string categoryC = 3;
}
message Owner {
string uuid = 1;
string secret = 2;
}
message Data {
Transaction transaction = 1;
}
message Transaction {
string type = 1;
string uuid = 2;
string pointCode = 3;
}
message Delivery {
string company = 1;
Address address = 2;
}
message Address {
string code = 1;
string country = 2;
string street = 3;
string apartment = 4;
}
message Goods {
string name = 1;
int32 amount = 2;
string code = 3;
}
# if it is your first downloading:
brew install protobuf
go install google.golang.org/protobuf/cmd/[email protected]
go install google.golang.org/grpc/cmd/[email protected]
export PATH="$PATH:$(go env GOPATH)/bin"
# only generator
cd .. && cd grpc
mkdir "docs"
protoc --go_out=./docs --go_opt=paths=source_relative \
--go-grpc_out=./docs --go-grpc_opt=paths=source_relative docs.proto
How do you test locally? I prefer to use BloomRpc (unfortunately, it has been deprecated :D; also, Postman can do the same). This time, I will skip the implementation details of the server and the logic related to document processing. However, once again, we will write a test. Naturally, we expect an unprecedented increase!
// BenchmarkCreateAndMarshal-10 651063 1827 ns/op
func BenchmarkCreateAndMarshal(b *testing.B) {
for i := 0; i < b.N; i++ {
doc := CreateDoc()
_ = doc.GetName()
r, e := proto.Marshal(&doc)
if e != nil {
log.Fatal("problem with marshal")
}
nd := new(docs.Document)
if proto.Unmarshal(r, nd) != nil {
log.Fatal("problem with unmarshal")
}
_ = nd.GetName()
}
}
This code represents a benchmark named BenchmarkCreateAndMarshal
, which measures the performance of creating and marshaling operations. The results show that, on average, the benchmark performs these operations in 1827 nanoseconds per iteration over 651063 iterations. So, the full code here:
And now, let’s introduce our guest among the protocols — most of you probably haven’t even heard of it — it’s .
Person person;
person.id = 123;
person.name = "John Doe";
person.age = 30;
Certainly, let’s represent the serialized bytes in hexadecimal format for the given Person
structure:
// Serialized bytes (hexadecimal representation)
// (assuming little-endian byte order)
1B 00 00 00 // Data size (including this byte)
7B 00 00 00 // ID (123 in little-endian byte order)
09 00 00 00 // Name string length (including null-terminator)
4A 6F 68 6E // Name ("John" in ASCII, including null-terminator)
20 00 00 00 // Age (30 in little-endian byte order)
In this example:
The next 4 bytes represent the id
(123 in little-endian byte order).
The last 4 bytes represent the age
(30 in little-endian byte order).
This time around, we can’t just breeze through because we’ll have to write all the serialization code ourselves. However, we’re expecting some silver linings from this. Sure, we’ll need to get our hands dirty with manual serialization coding, but the potential payoffs are worth it. It’s a bit of a trade-off — more effort upfront, but the control and potential performance boost might just make it a sweet deal in the long run. After all, sometimes you gotta get your hands deep in the code to make the magic happen, right?
// BenchmarkCreateAndMarshalBuilderPool-10 1681384 711.2 ns/op
func BenchmarkCreateAndMarshalBuilderPool(b *testing.B) {
builderPool := builder.NewBuilderPool(100)
for i := 0; i < b.N; i++ {
currentBuilder := builderPool.Get()
buf := BuildDocs(currentBuilder)
doc := sample.GetRootAsDocument(buf, 0)
_ = doc.Name()
sb := doc.Table().Bytes
cd := sample.GetRootAsDocument(sb, 0)
_ = cd.Name()
builderPool.Put(currentBuilder)
}
}
Since we’re in the “do-it-yourself optimization” mode, I decided to whip up a small pool of builders that I clear after use. This way, we can recycle them without allocating memory again and again.
const builderInitSize = 1024
// Pool - pool with builders.
type Pool struct {
mu sync.Mutex
pool chan *flatbuffers.Builder
maxCap int
}
// NewBuilderPool - create new pool with max capacity (maxCap)
func NewBuilderPool(maxCap int) *Pool {
return &Pool{
pool: make(chan *flatbuffers.Builder, maxCap),
maxCap: maxCap,
}
}
// Get - return builder or create new if it is empty
func (p *Pool) Get() *flatbuffers.Builder {
p.mu.Lock()
defer p.mu.Unlock()
select {
case builder := <-p.pool:
return builder
default:
return flatbuffers.NewBuilder(builderInitSize)
}
}
// Put return builder to the pool
func (p *Pool) Put(builder *flatbuffers.Builder) {
p.mu.Lock()
defer p.mu.Unlock()
builder.Reset()
select {
case p.pool <- builder:
// return to the pool
default:
// ignore
}
}
protocol | iterations | speed |
---|---|---|
json |
168706 |
7045 ns/op |
proto |
651063 | 1827 ns/op |
flat |
1681384 | 711.2 ns/op |
Language: Golang
http framework: Gin gonic
database: mognodb
Now it’s time to put our protocols to the real test — we’ll spin up the services, hook them up with Prometheus metrics, add MongoDB connections, and generally make them full-fledged services. We might skip tests for now, but that’s not the priority.
Save method, 1000 rps, 60 sec, profile:
rps: { duration: 60s, type: const, ops: 1000 }
Results:
JSON | first | second |
---|---|---|
99% | 1.630 | 1.260 |
98% | 1.160 | 1.070 |
95% | 1 | 0.920 |
Links:
PROTO | first | second |
---|---|---|
99% | 1.800 | 2.040 |
98% | 1.380 | 1.540 |
95% | 1.160 | 1.220 |
Links:
FLAT | first | second |
---|---|---|
99% | 3.220 | 3.010 |
98% | 2.420 | 2.490 |
95% | 1.850 | 1.840 |
Links:
Validate method, 1000 rps, 60 sec, same profile:
rps: { duration: 60s, type: const, ops: 1000 }
JSON | first | second |
---|---|---|
99% | 1.810 | 1.980 |
98% | 1.230 | 1.290 |
95% | 0.970 | 1.070 |
Links:
PROTO | first | second |
---|---|---|
99% | 1.060 | 1.010 |
98% | 0.700 | 0.660 |
95% | 0.550 | 0.530 |
Links:
FLAT | first | second |
---|---|---|
99% | 2.920 | 3.010 |
98% | 2.170 | 2.490 |
95% | 1.540 | 1.510 |
Links:
The results are quite ambiguous. Serialization with FlatBuffer, as expected, is faster than simple JSON. The case with validation was also surprising - we expected FlatBuffer to win, but gRPC came out on top. Let's delve into why we got these results. But why did another protocol win in load tests?
As examples, I wrote a couple of services that are simple enough, only processing incoming messages or "checking" them. As we can replace, gRPC and the Protocol Buffer protocol are now the winners. By the way, consider this experiment as a baseline, and if you encounter the question of accelerating serialization and message transmission down the chain, it's worth testing it on your process. Do not forget that it is also important to take into account the programming language and stack you are using. And once again, I want to underline the importance of conducting an MVP on the project if you still want to migrate to other serialization protocols.
JSON: In stress tests of the save method with a load of 1000 requests per second, JSON demonstrates stable results, with an execution time of approximately 7045 nanoseconds per operation.
Protobuf: Protobuf demonstrates high efficiency, surpassing JSON, with an execution time of about 1827 nanoseconds per operation in the same test.
FlatBuffers: FlatBuffers stands out among others, demonstrating significantly lower execution time - around 711.2 nanoseconds per operation in the same stress test.
Looking at our metrics, we're talking about 1-3 ms - just think how fast that is!
Also published .