Ukratko

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
  1. Uvod u Microservices i njihovu važnost
  2. Zašto Go i gRPC za Microservices?
  3. Go programski jezik
  4. gRPC komunikacijski okvir
  5. Dizajniranje gRPC servisa s Protocol Buffers (Protobuf)
  6. Primjer .proto datoteke
  7. Generiranje Go koda iz .proto datoteke
  8. Implementacija gRPC Servera u Go-u
  9. Implementacija gRPC Klijenta u Go-u
  10. Izgradnja i pokretanje
  11. Napredne teme i razmatranja
  12. 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:

  1. productCatalogServer struct: Implementira sučelje ProductCatalogServiceServer (koje je generirano iz .proto datoteke). Sadrži mapu products za simulaciju baze podataka i sync.RWMutex za sigurnu konkurentnu manipulaciju podacima.
  2. AddProduct, GetProduct, ListProducts metode: Implementiraju poslovnu logiku za svaku RPC metodu definiranu u .proto datoteci. Koriste context.Context za upravljanje rokovima (deadlines) i otkazivanjem, te vraćaju gRPC statuse za rukovanje greškama.
  3. main funkcija:
    • net.Listen otvara TCP port na kojem će server slušati.
    • grpc.NewServer() stvara novu gRPC serversku instancu.
    • pb.RegisterProductCatalogServiceServer registrira 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:

  1. 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.
  2. pb.NewProductCatalogServiceClient(conn): Stvara klijentsku instancu koja koristi uspostavljenu vezu.
  3. context.WithTimeout: Postavlja vremensko ograničenje za RPC pozive, što je dobra praksa u distribuiranom sustavu za sprječavanje vječnog čekanja.
  4. addProduct, getProduct, listProducts funkcije: 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

  1. Strukturirajte projekt:

    my-microservice/
    ├── proto/
    │   └── productcatalog.proto
    ├── server/
    │   └── main.go
    ├── client/
    │   └── main.go
    └── go.mod
    
  2. Inicijalizirajte Go modul:

    cd my-microservice
    go mod init my-microservice
    go mod tidy
    
  3. Generirajte Protobuf kod: (Iz korijena my-microservice direktorija)

    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 proto direktorij kako bi import pb "./proto" radio ispravno)

  4. Pokrenite server:

    cd server
    go run main.go
    

    Server će početi slušati na portu 50051.

  5. Pokrenite klijenta (u zasebnom terminalu):

    cd client
    go run main.go
    

    Klijent ć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.

Izvori i dodatno čitanje

  1. gRPC – A high performance, open-source universal RPC framework
  2. Protocol Buffers
  3. The Go Programming Language
  4. Microservices.io - Pattern: Microservice Architecture
B
Uredništvo portala

BAJT

Službeni autorski profil redakcije portala BAJT. Sadržaj priprema i provjerava uredništvo portala.