Intermittent gRPC `UNAVAILABLE` Errors After Inactivity: Client Connection Dropped by Load Balancer/Firewall?
Intermittent gRPC UNAVAILABLE Errors After Inactivity: Client Connection Dropped by Load Balancer/Firewall?
Context:
We're developing a microservice-based e-commerce platform where services communicate primarily via gRPC. One critical interaction involves our OrderProcessingService (client) calling the InventoryService (server) to reserve stock before confirming an order. Both services are written in Go and deployed as Docker containers on Kubernetes within AWS, behind an internal AWS Network Load Balancer (NLB) for the InventoryService.
Problem Description:
The issue we're facing is intermittent UNAVAILABLE errors when the OrderProcessingService tries to communicate with InventoryService after a period of inactivity. Specifically, if there are no calls to InventoryService for about 5-10 minutes, the first subsequent RPC request from OrderProcessingService fails with an UNAVAILABLE status. All subsequent requests immediately after this initial failure succeed without any problems. This behavior is highly disruptive, as it leads to failed order attempts and a poor user experience.
We've confirmed that the InventoryService pods are healthy and running continuously. There are no restarts or crashes on the server side during these incidents. The issue consistently appears after an idle period. It seems as if the underlying TCP connection is being prematurely closed by an intermediary (like the NLB or a firewall) due to inactivity, and the gRPC client isn't gracefully handling the reconnection on the first attempt.
Environment:
- Client Service:
OrderProcessingService(Go 1.20,google.golang.org/grpcv1.53.0) - Server Service:
InventoryService(Go 1.20,google.golang.org/grpcv1.53.0) - Deployment: Kubernetes on AWS,
InventoryServiceexposed via an AWS NLB. - OS: Linux (Alpine-based Docker images)
Code Snippets:
OrderProcessingService (Client-side connection setup):
package main
import (
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "your/proto/package" // Assume pb is defined for generated gRPC client
)
var inventoryServiceClient pb.InventoryServiceClient
func init() {
// Address of the InventoryService in Kubernetes
inventoryServiceAddr := "inventory-service.ecommerce-system.svc.cluster.local:50051"
conn, err := grpc.Dial(
inventoryServiceAddr,
grpc.WithTransportCredentials(insecure.NewCredentials()), // For simplicity in internal cluster comms
)
if err != nil {
log.Fatalf("did not connect to inventory service: %v", err)
}
inventoryServiceClient = pb.NewInventoryServiceClient(conn)
log.Printf("Successfully connected to inventory service at %s", inventoryServiceAddr)
}
func main() {
// ... application logic ...
// Example RPC call later in the code (simplified):
// _, err := inventoryServiceClient.ReserveStock(context.Background(), &pb.ReserveStockRequest{ProductId: "prod1", Quantity: 1})
// if err != nil {
// log.Printf("Error reserving stock: %v", err) // This is where UNAVAILABLE errors appear
// }
}
InventoryService (Server-side setup):
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
pb "your/proto/package" // Assume pb is defined for generated gRPC server
)
// server implements your/proto/package.InventoryServiceServer
type server struct {
pb.UnimplementedInventoryServiceServer
}
func (s *server) ReserveStock(ctx context.Context, req *pb.ReserveStockRequest) (*pb.ReserveStockResponse, error) {
log.Printf("Received ReserveStock request for product %s, quantity %d", req.ProductId, req.Quantity)
// Simulate stock reservation logic
if req.ProductId == "non-existent" {
return nil, status.Errorf(codes.NotFound, "Product %s not found", req.ProductId)
}
return &pb.ReserveStockResponse{Success: true, Message: "Stock reserved"}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterInventoryServiceServer(s, &server{})
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
What I Expected:
I expected gRPC to maintain a persistent, healthy connection or, if the connection was dropped by an intermediary, to transparently and immediately re-establish it on the next RPC call without propagating an UNAVAILABLE error to the application logic. The design goal is that RPC calls should appear reliable from the client's perspective, even after periods of inactivity.
What Actually Happened:
After about 5-10 minutes of no RPC traffic between OrderProcessingService and InventoryService, the very first ReserveStock RPC call fails with an error similar to this:
rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing: read: connection reset by peer"
Subsequent calls made immediately after this failure succeed without any issues, which indicates the server is healthy and a new connection is established, but not without an initial error.
Any insights into why this is happening and how to prevent these UNAVAILABLE errors would be greatly appreciated. Thanks!
Community Insights
1Unlock Global Discussions
The TechLab Forum is reserved for our premium engineering community. Upgrade now to participate in high-level architectural debates.
Get Pro Access