Irbisa · Шпаргалка
gRPC и микросервисы
gRPC и эволюция protobuf, дедлайны, синхронные и асинхронные вызовы, outbox и саги, ретраи и circuit breaker
Что такое gRPC и Protocol Buffers и когда вы выберете gRPC вместо REST с JSON?
// orders/v1/orders.proto//// syntax = "proto3";// package orders.v1;// option go_package = "example.com/gen/orders/v1;ordersv1";//…Каковы компромиссы между монолитом и микросервисами и что вы выберете для стартапа из пяти человек?
Когда сервисам общаться синхронно через REST или gRPC, а когда через брокер сообщений? Чем платят за асинхронность?
Какие четыре вида вызовов есть в gRPC и когда стриминг лучше унарного вызова?
// service Catalog {// rpc GetItem(GetItemRequest) returns (Item); // unary// rpc ListItems(ListRequest) returns (stream Item); // server streaming// rpc Upload(stream Item) returns (UploadSummary); // client streaming// rpc Sync(stream SyncEvent) returns (stream SyncEvent); // bidirectional// }…Этот хендлер возвращает ошибку из
fmt.Errorfдля несуществующего заказа. Какой код статуса получит gRPC-клиент и как хендлер должен сообщать «не найдено»?func (s *server) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.Order, error) { o, err := s.repo.Find(ctx, req.GetId()) if errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("order %s: %w", req.GetId(), err) } if err != nil { return nil, err } return o, nil}Сколько должен жить
grpc.ClientConn(один на вызов или один на процесс) и во что обходится новое соединение на каждый вызов?type Payments struct{ client pb.PaymentsClient } // Built once in main; conn.Close is returned for shutdown.func NewPayments(target string) (*Payments, func() error, error) { conn, err := grpc.NewClient(target, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})),…Что делают API gateway, service discovery и service mesh и какие задачи они забирают из вашего Go-кода?
Какие изменения
.protoбезопасны, когда клиенты уже в проде, почему номер удалённого поля нужно пометитьreservedи что даётoptionalв proto3?// user.proto//// message UpdateUserRequest {// reserved 4; // was bool is_trial// reserved "is_trial"; // and the name cannot come back either// string id = 1;…Чем дедлайн gRPC отличается от таймаута HTTP-клиента и что видит сервер, когда дедлайн клиента истёк?
func quote(ctx context.Context, c pb.PricingClient, sku string) (*pb.Quote, error) { // no default deadline in gRPC: without this a stuck server holds the call forever ctx, cancel := context.WithTimeout(ctx, 300*time.Millisecond) defer cancel() q, err := c.Quote(ctx, &pb.QuoteRequest{Sku: sku}) // remaining time sent as grpc-timeout if status.Code(err) == codes.DeadlineExceeded {…Что такое gRPC-интерцептор и почему интерцептор аутентификации, который работает для всех унарных методов, не срабатывает для стриминговых?
func UnaryAuth(ctx context.Context, req any, _ *grpc.UnaryServerInfo, h grpc.UnaryHandler) (any, error) { ctx, err := authenticate(ctx) if err != nil { return nil, err } return h(ctx, req)…Найдите баг: запрос одного клиента перезапускает под заказов, и все остальные вызовы в процессе падают с
Unavailable.func (s *server) ListOrders(ctx context.Context, req *pb.ListOrdersRequest) (*pb.ListOrdersResponse, error) { limit := int(req.Page.Size) if limit <= 0 || limit > 100 { limit = 20 } orders, next, err := s.repo.List(ctx, req.GetCustomerId(), limit, req.Page.Token) if err != nil { return nil, status.Error(codes.Internal, "list orders failed") } return &pb.ListOrdersResponse{Orders: orders, NextPageToken: next}, nil}Этот клиент возвращает первый товар в наличии из server-стрима. Поиски без совпадений пишут в лог
search failed: EOF, а горутины копятся. Какие тут две ошибки?func findFirst(ctx context.Context, c pb.CatalogClient, q string) (*pb.Item, error) { stream, err := c.Search(ctx, &pb.SearchRequest{Query: q}) if err != nil { return nil, err } for { item, err := stream.Recv() if err != nil { log.Printf("search failed: %v", err) return nil, err } if item.GetInStock() { return item, nil } }}Как работают встроенные ретраи gRPC, какие коды статуса стоит указывать в
retryableStatusCodesи почему метод должен быть идемпотентным до включения ретраев?// Passed with grpc.WithDefaultServiceConfig. Charge is listed only because it carries// an idempotency key; a write without one must never be retried by the transport.const svcConfig = `{ "methodConfig": [{ "name": [{"service": "payments.v1.Payments", "method": "Charge"}], "retryPolicy": {…Что делает circuit breaker, какие у него три состояния и когда он должен срабатывать?
// github.com/sony/gobreaker/v2var pricingCB = gobreaker.NewCircuitBreaker[*pb.Quote](gobreaker.Settings{ Name: "pricing", MaxRequests: 3, // trial calls while half-open Interval: 30 * time.Second, // closed: reset the counts every 30 s Timeout: 10 * time.Second, // open -> half-open after 10 s…Найдите баг: через неделю после деплоя этот circuit breaker то и дело открывается на 30 с, хотя зависимость падает на одном вызове из тысячи.
func (b *Breaker) Do(fn func() error) error { b.mu.Lock() if b.failures >= 5 && time.Since(b.lastFail) < 30*time.Second { b.mu.Unlock() return ErrOpen } b.mu.Unlock() err := fn() b.mu.Lock() if err != nil { b.failures++ b.lastFail = time.Now() } b.mu.Unlock() return err}Десять реплик стоят за Kubernetes-сервисом типа
ClusterIP. REST-трафик распределяется ровно, а gRPC шлёт около 90% вызовов в один под. Почему и как это исправить?func dial() (*grpc.ClientConn, error) { return grpc.NewClient( // headless Service: DNS returns every pod IP, not one virtual IP "dns:///orders.prod.svc.cluster.local:8080", grpc.WithTransportCredentials(insecure.NewCredentials()), // the default policy is pick_first: one pod gets everything…p99 одной зависимости вырос с 400 мс до 6 с, и вместе с ней легли четыре сервиса за общим вызывающим. Клиенты ретраят 3 раза с таймаутом 30 с. Что усилило сбой и что его останавливает?
// One Bulkhead per dependency: at most cap(slot) calls in flight to it.type Bulkhead struct{ slot chan struct{} } func NewBulkhead(n int) *Bulkhead { return &Bulkhead{slot: make(chan struct{}, n)} } func (b *Bulkhead) Unary(ctx context.Context, method string, req, reply any,…У части заказов нет события
OrderCreated, а если публиковать до коммита, появляются события для откатившихся заказов. Какой паттерн исправляет оба случая?func CreateOrder(ctx context.Context, tx *sql.Tx, o Order, payload []byte) error { _, err := tx.ExecContext(ctx, `INSERT INTO orders (id, total) VALUES ($1, $2)`, o.ID, o.Total) if err == nil { // same transaction: the event exists if and only if the order does _, err = tx.ExecContext(ctx, `INSERT INTO outbox (aggregate_id, type, payload) VALUES ($1, $2, $3)`, o.ID, "OrderCreated", payload)…Оформление заказа должно зарезервировать товар, списать деньги и вызвать курьера, и каждый шаг выполняется в своём сервисе. Почему не распределённая транзакция и как вместо неё работает сага?
Как построить распределённую блокировку на Redis и почему блокировка без fencing token небезопасна?
func acquire(ctx context.Context, rdb *redis.Client, key string, ttl time.Duration) (string, bool, error) { token := uuid.NewString() // random: only the owner can release it ok, err := rdb.SetNX(ctx, "lock:"+key, token, ttl).Result() return token, ok, err}…