När man bygger programvara baserat på mikrotjänster som är experter på en och endast en sak så behöver man sätt att kommunicera mellan tjänsterna där man kan säkerställa att det går så snabbt som möjligt. Här ska vi titta på ett sätt att skicka ett anrop från en tjänst till en annan där vi behöver vänta på svaret. En av teknikerna man kan använda då är gRPC som står för gRPC Remote Procedure Call.
RPC Remote procedure call
Det finns olika anledningar för tjänster att kommunicera med varandra och vissa av dessa anrop är synkrona, dvs man behöver vänta på ett svar och vill att svaret ska komma så snabbt som möjligt. Att göra anropet från en tjänst till en annan brukar vi kalla för RPC eller Remote Procedure Call.
sequenceDiagram
Client->>WeatherService: What was the temperatur like in Boden at noon
activate WeatherService
WeatherService-->>Client: It was +4C with a wind from the north at 2m/s
deactivate WeatherService
När använder köer i stället?
Det finns situationer som liknar RPC man där man inte kommer få ett svar direkt utan lägger något på kö och sedan kan hämta svaret vid ett senare tillfälle om man alls behöver ett svar på en fråga, så om frågan är asynkron så är kanske inte RPC den första tekniken man tänker på för att kommunicera mellan tjänster, då kan det vara mer lämpligt med att använda köhantering.
sequenceDiagram
Client->>Queue: Temperature in Boden at 2023-04-01 18.01 is +7C ...
activate Queue
Queue->>Client: Ok, message added to queue
deactivate Queue
Queue->>WeatherService: Deliver message
activate WeatherService
WeatherService->>WeatherService: Store message
WeatherService->>Queue: Acknowledge message
Queue->>Queue: Remove message from queue
Vad är då gRPC
Det är en teknik som är släppt som open source, ursprungligen framtagen av Google. Tanken med gRPC är att det ska finnas ett effektivt sätt att skicka ett anrop från en tjänst till en annan med så liten overhead som möjligt oberoende av vilken teknik som använts för att skapa den anropande rutinen och den som svarar på frågan.
gRPC stödjer strömmande hantering, vilket gör att man kan påbörja att skicka data och låta gRPC hantera svaret under tiden man väntar på nästa post som ska hanteras. Det gör att man kan använda mönstret med strömmande programmering där man inte behöver köra klart varje steg innan man kan skicka vidare till nästa.
För sin kommunikation använder gRPC HTTP/2 vilket är betydligt snabbare än det klassiska HTTP/1-protokollet, men man ställer inte frågan till en webbserver utan direkt till tjänsten som publicerars.
En del av det som tar mest tid när det gäller kommunikation mellan tjänster är att omvandla data i fråga och svar till binärt data i systemen som ska kommunicera med varandra. Json är väldigt populärt som textformat som är relativt kompakt, men det tar ändå en hel del kraft av systemen att skapa och tolka Json till en objektmodell. Därför använder gRPC ett protokoll som heter Protocol Buffers som också är tillgängligt som open source.
Med Protocol Buffer (protbuf3) definierar man då i en gRPC-lösning:
- Hur meddelandeformaten ser ut för förfrågning och svar
- Vad tjänsterna heter och vad de har för inkommande data och utgående svar
Man använder sedan ett kommando för att skapa kod för klientsidan och serversidan där man får välja vilket (eller vilka) språk som koden skapas för.
proto-filen för tjänsten och dess meddelanden
Man skapar sin fil för att definiera tjänsterna i en fil som man sedan använder för att generera den kod som behövs för att bygga en server och en klient.
// The definition for sending weather requests
service Weather {
rpc AskAboutWeather (WeatherRequest) returns (WeatherResponse) {}
}
// The request message containing the city and date and time
// The definition uses field numbers to identify the fields and
// a set of scalar types
message WeatherRequest {
string city = 1;
string date_and_time = 2;
}
// The response message containing the greetings
message WeatherResponse {
string city = 1;
string date_and_time = 2;
double temperature = 3;
double wind_speed = 4;
string wind_direction = 5;
}
Klientsidan för att skicka en förfrågan, här väljer jag att använda golang. Här är en del av koden som gör själva uppkopplingen till tjänsten. För enkelhetens skull används inte variabler och sånt för att hålla nere antal rader i koden.
- pb är protbuf som skapats av kodgenereringen där man kommer åt modeller och metoder
- connection är uppkopplingen som man kan använda för att skapa nya klienter mot tjänster där vi i det här fallet kopplar upp med WeatherClient
- client är instansen av en klient som kan kommunicera med WeatherService och som är själva WeatherClient
För er som inte är så vana vid golang använder man defer för att utföra något på ett objekt när man avslutar en metod, dvs när main är slutförd.
func main() {
// Connect to the RCP Server in the cluster
connection, err := grpc.Dial("weather-srv:5000",
grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("Connection was unsuccessful: %v", err)
}
defer connection.Close()
client := pb.NewWeatherClient(connection)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
response, err := client.AskAboutWeather(ctx, &pb.WeatherRequest{
City: "Boden",
DateAndTime: "2023-04-01 12:00:00"
})
log.Printf("The temperature is %.1f with a wind speed of %.0f",
response.Temperature, response.WindSpeed)
Själva servern som då ska svara på förfrågan gör det väldigt enkelt genom att vi definierar en typ som är den oimplementerade WeatherServer som vi sedan skapar implementationer för de metoder som finns där, den som är AskAboutWeather här.
Svaret blir hårdkodat för att inte lägga på mer komplexitet i exemplet.
type weatherservice struct {
pb.UnimplementedWeatherServer
}
func (s *weatherservice) AskAboutWeather(ctx context.Context,
in *pb.WeatherRequest) (*pb.WeatherResponse, error) {
return &pb.WeatherResponse{
City: in.City,
DateAndTime: in.DateAndTime,
WindSpeed: 1.0,
Temperature: 6,
}, nil
}
Att skapa upp själva servern som lyssnar på port 5000 med en TCP-ansluting. Det är till denna man kommer prata HTTP/2 men det är inte en webbserver som svarar på något annat än bara just det vi låter den hantera.
func main() {
listener, err := net.Listen("tcp", ":5000")
if err != nil {
log.Fatalf("Unable to crete TCP server: %v", err)
}
server := grpc.NewServer()
pb.RegisterWeatherServer(server, &weatherservice{})
if err := s.Serve(listener); err != nil {
log.Fatalf("Unable to initialize the gRCP server: %v", err)
}
}
Strömmande requests
Den riktigt stora nyttan med gRPC kommer när man inte behöver vänta på att allt ska ske i steg, man kan helt enkelt utföra dem med strömmande teknik där man arbetar med datat allt eftersom det blir tillgängligt, t.ex. genom att fråga från en databas och hämta poster samtidigt som man låter RPC-tjänsten jobba med sina funktioner medan databasen letar efter nästa post, allt detta utom att behöva göra en ny uppkoppling.
sequenceDiagram
participant Client
participant DBServer
participant RPCServer
participant WeatherService
participant TimeSeriesDB
Client->>RPCServer: Connect to server
Client->>DBServer: Query for cities
loop For each city in result
Client->>DBServer: Fetch next city
Client->>RPCServer: Query temperature for city N
RPCServer->>WeatherService: Invoke handler
WeatherService->>TimeSeriesDB: Find the data
TimeSeriesDB->>WeatherService: Weather time series data
WeatherService->>RPCServer: Create the response
RPCServer->>Client: Add response to result
end
Hur går man vidare?
Det är egentligen bara att komma igång. Just att skapa en gRPC-tjänst har en liten tröskel i och med att man först ska skapa en protbuf-fil, generera kod i de språk man ska använda och sedan helst då jobba i ett kubernetes-kluster där man kan skapa flera replikor av samma tjänst för att kunna uppgradera dem online.
Tänk på att protokollet är binärt och man behöver därför se till att uppgradera både klient och server samtidigt om kontraktet ändras. Det gör att den är avsevärt klurigare att deploya jämfört med en REST/JSON-baserad tjänst.
Om du verkligen behöver hastighet i kommunikationen mellan tjänster däremot, då är gRPC helt rätt val för det går riktigt fort och går inte att jämföra med att t.ex. använda rabbit MQ för RPC eller att köra HTTP/1 mot ett REST-gränssnitt.
För mig själv så använder vi detta i owlstreet när det gäller strömmande hantering med flera tjänster inblandade.