Najvažnije iz članka
- Mikroservisi omogućuju izgradnju skalabilnih, otpornih i održivih sustava razbijanjem monolitnih aplikacija na manje, neovisne servise.
- Go programski jezik (Golang) je idealan za mikroservise zbog svoje konkurentnosti, performansi, jednostavnosti i efikasnog razmještanja u kontejnerima.
- gRPC je visokoučinkoviti RPC okvir koji koristi Protocol Buffers (Protobuf) za serijalizaciju i HTTP/2 za transport, nudeći efikasnu komunikaciju i generiranje koda za različite jezike.
- Dizajniranje gRPC servisa započinje s `.proto` datotekom koja definira poruke i servisna sučelja, nakon čega se generira Go kod za serversku i klijentsku implementaciju.
Sadržaj članka
- Uvod u Microservices i njihovu važnost
- Zašto Go i gRPC za Microservices?
- Go programski jezik
- gRPC komunikacijski okvir
- Dizajniranje gRPC servisa s Protocol Buffers (Protobuf)
- Primjer .proto datoteke
- Generiranje Go koda iz .proto datoteke
- Implementacija gRPC Servera u Go-u
- Implementacija gRPC Klijenta u Go-u
- Izgradnja i pokretanje
- Napredne teme i razmatranja
- Zaključak
Uvod u Microservices i njihovu važnost
U današnjem svijetu softverskog inženjerstva, potreba za skalabilnim, otpornim i lako održivim sustavima nikada nije bila veća. Monolitne aplikacije, iako su imale svoje prednosti u prošlosti, često se bore s ovim izazovima. Ovdje na scenu stupaju mikroservisi. Mikroservisna arhitektura predlaže razbijanje velike aplikacije na skup malih, neovisno razmjestivih servisa koji komuniciraju laganim mehanizmima. Svaki servis je odgovoran za specifičnu poslovnu funkcionalnost i može biti razvijen, razmješten i skaliran neovisno o drugima.
Glavne prednosti mikroservisa uključuju:
- Skalabilnost: Pojedini servisi se mogu skalirati neovisno, ovisno o specifičnim potrebama opterećenja.
- Fleksibilnost: Različiti servisi mogu koristiti različite tehnologije (programski jezici, baze podataka), ovisno o najprikladnijem alatu za posao.
- Održivost: Manje kodne baze olakšavaju razumijevanje, testiranje i održavanje.
- Otpornost: Pogreška u jednom servisu ne mora nužno srušiti cijeli sustav.
- Brža isporuka: Timovi mogu raditi paralelno na različitim servisima, ubrzavajući ciklus razvoja.
Ipak, mikroservisi donose i nove izazove, poput složenosti distribuiranog sustava, konzistentnosti podataka, promatranja (observability) i komunikacije među servisima. Upravo se na komunikaciju fokusiramo u ovom vodiču, koristeći gRPC.
Zašto Go i gRPC za Microservices?
Go programski jezik
Go (ili Golang) je programski jezik koji je razvio Google, a posebno je dizajniran za izgradnju skalabilnih i visoko performansnih mrežnih servisa. Njegove ključne značajke koje ga čine idealnim izborom za mikroservise su:
- Konkurentnost: Ugrađena podrška za goroutine i kanale omogućuje jednostavno pisanje konkurentnog koda, što je ključno za I/O intenzivne operacije tipične za mrežne servise.
- Performanse: Kompajlira se u strojni kod, što rezultira izvrsnom brzinom izvršavanja i efikasnim korištenjem resursa.
- Jednostavnost: Sintaksa Go-a je jednostavna i laka za učenje, što poboljšava produktivnost i održivost koda.
- Statičko linkanje: Generira samostalne binarne datoteke bez vanjskih ovisnosti, što olakšava razmještanje (deployment) u kontejnerima poput Dockera.
- Robusni standardni library: Dolazi s bogatom standardnom bibliotekom, uključujući moćne pakete za mrežnu komunikaciju, kriptografiju i parsiranje JSON-a.
gRPC komunikacijski okvir
gRPC (Google Remote Procedure Call) je moderni open-source visokoučinkoviti RPC okvir koji može raditi u bilo kojem okruženju. Omogućuje sinkronu komunikaciju između servisa, a ključne značajke uključuju:
- Baziran na Protobufu: Koristi Protocol Buffers (Protobuf) kao format za serijalizaciju strukturiranih podataka. Protobuf je jezično neovisan, platformski neovisan i mnogo efikasniji od JSON-a ili XML-a u smislu veličine poruke i brzine parsiranja.
- HTTP/2 protokol: gRPC koristi HTTP/2 kao transportni sloj, što donosi značajne prednosti poput multipleksiranja, kompresije zaglavlja i streaminga, omogućujući efikasniju komunikaciju.
- Bilateralni streaming: Podržava četiri vrste servisa: Unary, Server Streaming, Client Streaming i Bidirectional Streaming, omogućujući fleksibilnu komunikaciju.
- Generiranje koda: Na temelju Protobuf definicija, gRPC automatski generira klijentski i serverski kod za različite programske jezike, smanjujući boilerplate kod i osiguravajući dosljednost.
- Autentikacija, load balancing i otkazivanje: Ugrađena podrška za napredne značajke distribuiranih sustava.
Kombinacija Go-a i gRPC-a rezultira moćnim ekosustavom za izgradnju performantnih, skalabilnih i jezično agnostičnih mikroservisa.
Dizajniranje gRPC servisa s Protocol Buffers (Protobuf)
Srce svakog gRPC servisa je .proto datoteka koja definira strukturu poruka i sučelje servisa. Ovo je ugovor između klijenta i servera.
Primjer .proto datoteke
Zamislite da gradimo jednostavnu "Product Catalog" mikroservis. Trebat će nam načini za dodavanje, dohvaćanje i listanje proizvoda.
syntax = "proto3";
package productcatalog;
option go_package = "./proto";
// Poruka koja predstavlja proizvod
message Product {
string id = 1;
string name = 2;
string description = 3;
double price = 4;
repeated string categories = 5;
int32 stock_quantity = 6;
}
// Zahtjev za dodavanje novog proizvoda
message AddProductRequest {
Product product = 1;
}
// Odgovor nakon dodavanja proizvoda
message AddProductResponse {
Product product = 1;
}
// Zahtjev za dohvaćanje proizvoda po ID-u
message GetProductRequest {
string id = 1;
}
// Odgovor s dohvaćenim proizvodom
message GetProductResponse {
Product product = 1;
}
// Zahtjev za listanje svih proizvoda
message ListProductsRequest {
// Npr. za paginaciju, ali za sada samo prazan zahtjev
}
// Odgovor s listom proizvoda
message ListProductsResponse {
repeated Product products = 1;
}
// Definiramo ProductCatalog servis
service ProductCatalogService {
rpc AddProduct (AddProductRequest) returns (AddProductResponse);
rpc GetProduct (GetProductRequest) returns (GetProductResponse);
rpc ListProducts (ListProductsRequest) returns (ListProductsResponse);
}
Generiranje Go koda iz .proto datoteke
Nakon što smo definirali .proto datoteku (npr. productcatalog.proto), moramo generirati Go kod koji će nam omogućiti rad s porukama i sučeljima servisa. Trebat će vam protoc kompajler i Go dodaci za protoc.
Instalacija (ako već niste):
# Instalacija protoc kompajlera (ovisno o OS-u)
sudo apt install protobuf-compiler # Za Debian/Ubuntu
brew install protobuf # Za macOS
# Instalacija Go dodataka
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
Generiranje koda (iz direktorija gdje se nalazi productcatalog.proto):
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
productcatalog.proto
Ovo će generirati datoteke poput productcatalog.pb.go koje sadrže Go strukture za poruke i productcatalog_grpc.pb.go koje sadrže serverska sučelja i klijentske stubove.
Implementacija gRPC Servera u Go-u
Sada ćemo implementirati serversku stranu ProductCatalogService.
package main
import (
"context"
"fmt"
"log"
"net"
"sync"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
pb "./proto" // Put do generiranih proto datoteka
)
type productCatalogServer struct {
pb.UnimplementedProductCatalogServiceServer
products map[string]*pb.Product
mu sync.RWMutex
}
func newProductCatalogServer() *productCatalogServer {
return &productCatalogServer{
products: make(map[string]*pb.Product),
}
}
func (s *productCatalogServer) AddProduct(ctx context.Context, req *pb.AddProductRequest) (*pb.AddProductResponse, error) {
log.Printf("Received AddProduct request: %v", req.GetProduct().GetName())
product := req.GetProduct()
if product.GetId() == "" {
return nil, status.Errorf(codes.InvalidArgument, "Product ID cannot be empty")
}
s.mu.Lock()
defer s.mu.Unlock()
// Provjeri postoji li već proizvod s istim ID-jem
if _, exists := s.products[product.GetId()]; exists {
return nil, status.Errorf(codes.AlreadyExists, "Product with ID %s already exists", product.GetId())
}
s.products[product.GetId()] = product
log.Printf("Added product: %s", product.GetId())
return &pb.AddProductResponse{Product: product}, nil
}
func (s *productCatalogServer) GetProduct(ctx context.Context, req *pb.GetProductRequest) (*pb.GetProductResponse, error) {
log.Printf("Received GetProduct request for ID: %s", req.GetId())
s.mu.RLock()
defer s.mu.RUnlock()
product, ok := s.products[req.GetId()]
if !ok {
return nil, status.Errorf(codes.NotFound, "Product with ID %s not found", req.GetId())
}
return &pb.GetProductResponse{Product: product}, nil
}
func (s *productCatalogServer) ListProducts(ctx context.Context, req *pb.ListProductsRequest) (*pb.ListProductsResponse, error) {
log.Println("Received ListProducts request")
s.mu.RLock()
defer s.mu.RUnlock()
var products []*pb.Product
for _, p := range s.products {
products = append(products, p)
}
return &pb.ListProductsResponse{Products: products}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterProductCatalogServiceServer(s, newProductCatalogServer())
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Objašnjenje koda:
productCatalogServerstruct: Implementira sučeljeProductCatalogServiceServer(koje je generirano iz.protodatoteke). Sadrži mapuproductsza simulaciju baze podataka isync.RWMutexza sigurnu konkurentnu manipulaciju podacima.AddProduct,GetProduct,ListProductsmetode: Implementiraju poslovnu logiku za svaku RPC metodu definiranu u.protodatoteci. Koristecontext.Contextza upravljanje rokovima (deadlines) i otkazivanjem, te vraćaju gRPC statuse za rukovanje greškama.mainfunkcija:net.Listenotvara TCP port na kojem će server slušati.grpc.NewServer()stvara novu gRPC serversku instancu.pb.RegisterProductCatalogServiceServerregistrira našu implementaciju servisa s gRPC serverom.s.Serve(lis)pokreće gRPC server koji počinje prihvaćati dolazne zahtjeve.
Implementacija gRPC Klijenta u Go-u
Sada ćemo napisati klijentsku aplikaciju koja će komunicirati s našim ProductCatalogService.
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "./proto" // Put do generiranih proto datoteka
)
const (
address = "localhost:50051"
)
func main() {
// Uspostavi vezu s gRPC serverom
conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewProductCatalogServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// Dodaj nekoliko proizvoda
addProduct(ctx, c, &pb.Product{
Id: "prod1",
Name: "Laptop Pro X",
Description: "High-performance laptop for professionals",
Price: 1200.00,
Categories: []string{"electronics", "computers"},
StockQuantity: 50,
})
addProduct(ctx, c, &pb.Product{
Id: "prod2",
Name: "Mechanical Keyboard",
Description: "Gaming keyboard with RGB lighting",
Price: 99.99,
Categories: []string{"accessories", "peripherals"},
StockQuantity: 150,
})
// Dohvati proizvod po ID-u
getProduct(ctx, c, "prod1")
// Pokušaj dohvatiti nepostojeći proizvod
getProduct(ctx, c, "nonexistent_id")
// Listaj sve proizvode
listProducts(ctx, c)
}
func addProduct(ctx context.Context, client pb.ProductCatalogServiceClient, product *pb.Product) {
req := &pb.AddProductRequest{Product: product}
res, err := client.AddProduct(ctx, req)
if err != nil {
log.Printf("Could not add product %s: %v", product.GetName(), err)
return
}
log.Printf("Product added: %s (ID: %s)", res.GetProduct().GetName(), res.GetProduct().GetId())
}
func getProduct(ctx context.Context, client pb.ProductCatalogServiceClient, productID string) {
req := &pb.GetProductRequest{Id: productID}
res, err := client.GetProduct(ctx, req)
if err != nil {
log.Printf("Could not get product with ID %s: %v", productID, err)
return
}
log.Printf("Product retrieved: ID: %s, Name: %s, Price: %.2f", res.GetProduct().GetId(), res.GetProduct().GetName(), res.GetProduct().GetPrice())
}
func listProducts(ctx context.Context, client pb.ProductCatalogServiceClient) {
req := &pb.ListProductsRequest{}
res, err := client.ListProducts(ctx, req)
if err != nil {
log.Fatalf("Could not list products: %v", err)
return
}
log.Println("--- All Products ---")
for _, p := range res.GetProducts() {
log.Printf("ID: %s, Name: %s, Price: %.2f, Stock: %d", p.GetId(), p.GetName(), p.GetPrice(), p.GetStockQuantity())
}
log.Println("--------------------")
}
Objašnjenje koda:
grpc.Dial: Uspostavlja sigurnu (ili neosiguranu, kao ovdje za demonstraciju) vezu s gRPC serverom.insecure.NewCredentials()se koristi samo za razvoj/testiranje; u produkciji biste koristili TLS/SSL.pb.NewProductCatalogServiceClient(conn): Stvara klijentsku instancu koja koristi uspostavljenu vezu.context.WithTimeout: Postavlja vremensko ograničenje za RPC pozive, što je dobra praksa u distribuiranom sustavu za sprječavanje vječnog čekanja.addProduct,getProduct,listProductsfunkcije: Pokazuju kako pozvati RPC metode na serveru i kako obraditi odgovore i potencijalne greške. Klijent koristi generirani Go kod za kreiranje zahtjeva i parsiranje odgovora, što eliminira ručno serijaliziranje/deserijaliziranje.
Izgradnja i pokretanje
Strukturirajte projekt:
my-microservice/ ├── proto/ │ └── productcatalog.proto ├── server/ │ └── main.go ├── client/ │ └── main.go └── go.modInicijalizirajte Go modul:
cd my-microservice go mod init my-microservice go mod tidyGenerirajte Protobuf kod: (Iz korijena
my-microservicedirektorija)protoc --go_out=./proto --go_opt=paths=source_relative \ --go-grpc_out=./proto --go-grpc_opt=paths=source_relative \ proto/productcatalog.proto(Pazite da se generirani fajlovi smjeste u
protodirektorij kako biimport pb "./proto"radio ispravno)Pokrenite server:
cd server go run main.goServer će početi slušati na portu
50051.Pokrenite klijenta (u zasebnom terminalu):
cd client go run main.goKlijent će izvršiti RPC pozive i ispisati rezultate. Trebali biste vidjeti logove na serverskoj i klijentskoj strani.
Napredne teme i razmatranja
Iako smo pokrili osnove, izgradnja produkcijskih mikroservisa zahtijeva razmatranje sljedećih naprednih tema:
- Error Handling: Koristite standardizirane gRPC statuse za greške. Razmislite o prilagođenim greškama i kako ih prenositi.
- Autentikacija i Autorizacija: Implementacija JWT-a ili OAuth-a s gRPC intercepterima.
- Observability (Promatranje): Logiranje, metrika (Prometheus), distribuirano praćenje (OpenTelemetry/Jaeger) su ključni za razumijevanje ponašanja sustava.
- Load Balancing: Korištenje klijentskog ili serverskog load balancinga za distribuiranu obradu zahtjeva.
- Service Discovery: Integracija s alatima poput Consula, Etcd-a ili Kubernetesa za automatsko pronalaženje servisa.
- Konfiguracija: Centralizirano upravljanje konfiguracijom (npr. s Vaultom, Consulom).
- Idempotencija: Osiguravanje da ponovljeni pozivi iste operacije ne uzrokuju neželjene nuspojave, pogotovo kod pisanja podataka.
- Deployment: Kontejnerizacija s Dockerom i orkestracija s Kubernetesom su de facto standardi za razmještanje mikroservisa.
- Testing: Unit, integracijski i end-to-end testovi su esencijalni.
Zaključak
Izgradnja mikroservisa s Go-om i gRPC-om nudi moćnu kombinaciju performansi, skalabilnosti i lakoće razvoja. Go-ova konkurentnost i jednostavnost u kombinaciji s gRPC-ovom efikasnošću i jezičnom agnostičnošću pružaju solidnu osnovu za modernu distribuiranu arhitekturu. Iako put do potpuno funkcionalnog produkcijskog sustava zahtijeva dodatna razmatranja, ovaj vodič pruža čvrst temelj za početak vaše avanture u svijetu mikroservisa.
Komentari