A couple of years ago we fairly quietly worked with our small team and did hosting. It turned out that each service in the system had its own unique and unique API. But then it became a problem and it was decided to redo everything.
We will talk about how to combine the external API with the internal one and what to do if you have a lot of PHP code, but you want to take advantage of gRPC.
Now they talk a lot about microservices and SOA in general. Our infrastructure is no exception: after all, we are engaged in hosting and our services allow us to manage almost a thousand servers.
Over time, services in our system began to appear more and more: have become a domain registrar - we register in a separate service; there are a lot of server metrics - we are writing a service that does samples from ClickHouse / InfluxDB; You need to make an emulator of launching tasks “like through Crontab”; for users - we write service. It will probably be familiar to many.
Inbound tasks in developing a lot. The number of different services is growing smoothly and, seemingly, unnoticed. It is impossible to take into account all the future nuances in advance, therefore one API was replaced by other, better ones . But the day came when it became apparent that too many protocols were divorced :
The more services and connections between them became, the longer and more difficult it was to solve problems: it was necessary to study several different APIs, write clients to them and only then begin the real work.
Oh, yes ... because the documentation is also needed. Otherwise the following dialogues take place in chat:
- Guys, how can I get user balance from billing?
- Make a call to billing / getBalance (customerId)
- How to get a list of services?
- I do not remember, look for the desired controller in
In short, the dream of a magical single standard and technology for creating a network API, which will solve all the problems and save us time, has arisen.
A little thought, we made a small list of requirements:
As a result of searches, evaluations and small tests, we stopped at gRPC . This framework is not new and has already been written about it on Habré .
Out of the box, he met almost all of our requirements. If in a nutshell:
However, the ideal technology does not exist. For us, there were several stumbling blocks:
Fortunately, we solved these problems quite easily. Further I will assume that the reader is familiar with gRPC. If not, it is better to first refer to the article mentioned above.
Yes indeed. OpenAPI and the toolbox provided by Swagger look enticing.
Immediately I must say that comparing OpenAPI and gRPC is not entirely correct. gRPC is primarily a framework that solves the technical problem of RPC interaction. It offers its own protocol, serialization method, service description language, and some tuling.
OpenAPI is primarily a specification trying to become a single standard for describing interfaces. Let it now be strongly oriented towards REST, but there are proposals to add support for RPC . Perhaps in the future, OpenAPI will become the modern equivalent of web services and WSDL.
Nevertheless, our team found several arguments in favor of using gRPC instead of OpenAPI:
doSomethingVerySpecialShit
into the right resource available at its URL. Well, or rather, of course you can (converting the verb-method into a noun), but it will look very alien. There are no such problems with gRPC;If we sum up a little, then we can say that OpenAPI turned out to be too large for us and did not always offer adequate solutions. I recall that our ultimate goal - to move to a single technical and organizational standard for RPC. Implementing REST principles would require rethinking and refactoring many things, which would be a task from a completely different weight category.
You can also read, for example, with this article-comparison . In general, we agree with her.
As I said above, we have a lot of business logic written in PHP. If we turn to the documentation , then we will have a bummer: due to the peculiarities of the code execution model, it cannot act as a server (various reactphp does not count). But it works well as a client and, if you feed the proto-file with the service description to the code generator, it will honestly generate classes for all structures (request and response). So, the problem is completely solved.
All that we found on the topic of how PHP works as a server is a discussion on this topic in Google Groups . In this discussion, one of the participants said that they are working on the possibility of proxying gRPC in FastCGI (PHP-FPM wants to see it used by us). This is exactly what we were looking for. Unfortunately, we were unable to contact, find out the status of this project and participate in it.
In this regard, it was decided to write a small proxy, which could accept requests and convert them to FactCGI. Since gRPC runs on top of HTTP / 2 and the method call in it is in fact a regular HTTP request, the task is not complicated.
As a result, we quickly made such a proxy in the Go language. For its work, it requires only a small config with information about where to proxy. We have published its code for everyone .
The scheme of work is as follows:
Thus, the principle of processing a request in PHP is very simple:
php://input
stream);r
(we use Yii2 and its router wants to see the route in this parameter) syntax = 'proto3'; package api.customer; service CustomerService { rpc getSomeInfo(GetSomeInfoRequest) returns (GetSomeInfoResponse) {} } message GetSomeInfoRequest { string login = 1; } message GetSomeInfoResponse { string first_name = 2; string second_name = 3; }
When getSomeInfo
requested, the r
parameter will contain api.customer.customer-service/get-some-info
.
Concentrated example of request processing in the application:
<?php // , gRPC : // package.service-name/method-name $route = $_GET['r']; // , protobuf $body = file_get_contents("php://input"); try { // 50% if (rand(0, 1)) { throw new \RuntimeException("Some error happened!"); } // , // -, , - $request = new GetSomeInfoRequest; $request->parse($body); $customer = findCustomer($request->getLogin()); $response = (new GetSomeInfoResponse) ->setFirstName($customer->getFirstName()); echo $response->serialize(); } catch (\Throwable $e) { // -. // , : // https://github.com/grpc/grpc-go/blob/master/codes/codes.go $errorCode = 13; header("X-Grpc-Status: ERROR"); header("X-Grpc-Error-Code: {$errorCode}"); header("X-Grpc-Error-Description: {$e->getMessage()}"); }
As you can see, everything is quite simple. You can implement this logic in any popular framework and make a convenient layer of controllers in which all serialization / deserialization will occur automatically.
<?php namespace app\api\controllers\customer\actions; use app\api\base\Action; // protobuf use app\generated\api\customer\GetSomeInfoRequest; use app\generated\api\customer\GetSomeInfoResponse; class GetSomeInfoAction extends Action { public function run(GetSomeInfoRequest $request): GetSomeInfoResponse { // $customer = findCustomer($request->getLogin()); // return (new GetSomeInfoResponse) ->setFirstName($customer->getFirstName()) ->setSecondName($customer->getSecondName()); } }
Exception handling and converting them into the corresponding gRPC statuscodes is implemented at the application level and occurs automatically.
All the developer needs is to create an Action and specify the expected types for the request and response in the signature of the run
method.
To generate code in PHP
Benefits
disadvantages
If we have more or less solved the issues with internal messaging, then the frontend still remains. Ideally, I would like to strive for using a single stack of technologies for both backend and frontend: it's easier and cheaper. Therefore, we began to study this issue.
Almost immediately, we found the grpc-gateway project , which allows us to generate proxies for converting gRPC / Protobuf to HTTP / JSON. It seems that this is a good solution for returning the API to the frontend and for those customers who do not want or cannot use gRPC (for example, if you need to write some one-time bash script quickly).
About this project there is also an article on Habré , so literally in two words: a plugin for protoc
based on the transmitted .proto files with a description of the service and special meta-information about the HTTP routes in which they generate code for reverse proxy. Next, the main file is written with hands, in which the generated proxy server is simply started (the grpc-gateway authors describe in detail all the actions in README.md ).
True, out of the box for us there were a couple of inconveniences:
In short, you need to make changes often and quickly. We'll have to work a little on convenience.
The grpc-gateway itself is a protoc plugin that generates a proxy and is written in Go. Based on this decision, it begs itself: write a generator that generates a plugin that generates a proxy =) Well, automate the launch and deployment of all this in our Gitlab CI.
The result is a generator generator, which takes a simple config to the input:
- url: git.repository.com/api0/example-service # .proto ref: c4d0504f690ee66349306f66578cb15787eefe72 # target: grpc-external.example.service.consul:50051 # - ...
After changing it and starting the build, in our CI, a generator is launched that downloads all the necessary repositories based on the config, generates code for the main file, wraps the proxy itself with various middleware, and the output is a ready-made binary, which is then deployed to production.
Benefits
disadvantages
As a result of all these technical perturbations, we were able to quickly prepare our infrastructure for the transition to a single messaging protocol. Thus, we were able to simplify and speed up the exchange of information between developers, added strictness to our interfaces in the issue of types, moved to the Design First design principle and preliminary Code Review at the level of interservice interfaces.
It turned out something like this:
At the moment we have transferred most of the internal messaging to gRPC and are working on a new public API. All this happens in the background as far as possible. However, this process is not as complicated as it seemed before. Instead, we were able to quickly and consistently design our APIs, share them between developers, conduct a Code Review, and generate clients. For cases when HTTP is needed (for example, for internal web interfaces), we simply add a few annotations, add a couple of lines to the grpc-gateway config and get a ready endpoint.
We would also like to make it possible in the future to use gRPC or a similar protocol directly on the frontend =)
It cannot be said that there were no jambs and difficulties. Among the interesting problems characteristic of gRPC, we have identified the following:
grpc.max_receive_message_length
parameter is responsible for it. It was possible to find it only by digging in the client’s source code;protoc
. True, you need to do this only once;public
or private
). Now it is almost completely fixed;uint32
, then on the server we will receive 0
, but not null
. This may be unusual, but, on the other hand, it turned out to be quite convenient;In addition to the technical aspects described in this article, there were other things that need to be given close attention when switching to a new API:
We hope that our experience will be useful to teams working on long-lived projects and ready to deal with what is usually called the term "historically."
Source: https://habr.com/ru/post/348008/