Site icon Vinsguru

gRPC On Kubernetes With Linkerd

grpc on kubernetes

Overview:

In this tutorial, I would like to show you gRPC On Kubernetes with Linkerd – to demonstrate how we could load balance the gRPC requests on Kubernetes.

If you are new to gRPC, take a look at these articles.

gRPC On Kubernetes:

gRPC is a great choice for client/server application development or inter-Microservices communication. It depends on HTTP/2 by default which maintains a persistent connection between the client and the server. Even though gRPC provides very good performance, load balancing gRPC requests becomes a problem because of the persistent connection.

To understand the issue better, Let’s consider a simple client / server application. We have 3 instances of the server application (Pods) running on a Kubernetes cluster. When the client sends a request to the Kubernetes service we can see that the load is never distributed across all the Pods. It will always go to 1 single pod. Kubernetes service is connection oriented. So it can NOT balance the gRPC requests.

Lets see how we could solve this problem by using Linkerd Service Mesh.

Sample Application:

Lets consider an application in which the user wants to find the squares up to N number.

We will have 2 services.

For ex: If we send a request for 3 to the aggregator-service, we will get result as shown here. The hostname field will contain the value of the actual pod instance which processed the request.

[
   {
      "input":1,
      "result":1,
      "hostname":"hostname"
   },
   {
      "input":2,
      "result":4,
      "hostname":"hostname"
   },
   {
      "input":3,
      "result":9,
      "hostname":"hostname"
   }
]

Project Setup:

message Input {
  int32 number = 1;
}

message Output {
  int32 number = 1;
  int32 result = 2;
  string host = 3;
}

service SquareService {
  rpc findSquareUnary(Input) returns (Output) {};
}

gRPC Service:

@GrpcService
public class SquareService extends SquareServiceGrpc.SquareServiceImplBase {

    @Override
    public void findSquareUnary(Input request, StreamObserver<Output> responseObserver) {
        var number = request.getNumber();
        Output output = Output.newBuilder()
                .setNumber(number)
                .setResult(number * number)
                .setHost(getHostName())
                .build();
        responseObserver.onNext(output);
        responseObserver.onCompleted();
    }

    private String getHostName(){
        try {
            return InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
        return null;
    }

}

Aggregator:

@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class ResultDto {

    private int input;
    private int result;
    private String hostname;

}
@Service
public class GrpcSquareService {

    @GrpcClient("square")
    private SquareServiceGrpc.SquareServiceBlockingStub blockingStub;

    public Flux<ResultDto> getSquareResponseUnary(int number){
        return Flux.range(1, number)
                .map(i -> Input.newBuilder().setNumber(i).build())
                .map(i -> this.blockingStub.findSquareUnary(i))
                .map(o -> ResultDto.of(o.getNumber(), o.getResult(), o.getHost()))
                .subscribeOn(Schedulers.boundedElastic());
    }

}
@RestController
@RequestMapping("grpc")
public class SquareServiceController {

    @Autowired
    private GrpcSquareService service;

    @GetMapping("/{number}")
    public Flux<ResultDto> getResponseUnary(@PathVariable int number){
        return this.service.getSquareResponseUnary(number);
    }

}

gRPC On Kubernetes – Deployment:

Once the above set up is done, I dockerized the applications. I built 2 docker images and I have pushed them into docker hub. (You can directly pull them to play with this.)

Now lets deploy this app on a Kubernetes cluster.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: square-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: square-app
  template:
    metadata:
      labels:
        app: square-app
    spec:
      containers:
        - name: square-app
          image: vinsdocker/square-app
---
apiVersion: v1
kind: Service
metadata:
  name: square-service
spec:
  selector:
    app: square-app
  ports:
    - port: 6565
      protocol: TCP
      targetPort: 6565
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: aggregator-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: aggregator-app
  template:
    metadata:
      labels:
        app: aggregator-app
    spec:
      containers:
        - name: aggregator-app
          image: vinsdocker/aggregator-app
          env:
            - name: GRPC_CLIENT_SQUARE_ADDRESS
              value: static://square-service:6565
---
apiVersion: v1
kind: Service
metadata:
  name: aggregator-service
spec:
  selector:
    app: aggregator-app
  ports:
    - port: 8080
      protocol: TCP
      targetPort: 8080

Save the above content in a deployment.yaml file. Run this command to deploy.

kubectl apply -f deployment.yaml

As you see we are running 3 instances of the square app.

Lets expose the aggregator service outside Kubernetes cluster to access.

kubectl port-forward service/aggregator-service 8080

Lets send a request.

curl http://localhost:8080/grpc/5

This is the output I get. (Send the curl request multiple times. We will still see same output)

[
   {
      "input":1,
      "result":1,
      "hostname":"square-app-7cfffb48d9-r77vg"
   },
   {
      "input":2,
      "result":4,
      "hostname":"square-app-7cfffb48d9-r77vg"
   },
   {
      "input":3,
      "result":9,
      "hostname":"square-app-7cfffb48d9-r77vg"
   },
   {
      "input":4,
      "result":16,
      "hostname":"square-app-7cfffb48d9-r77vg"
   },
   {
      "input":5,
      "result":25,
      "hostname":"square-app-7cfffb48d9-r77vg"
   }
]

The gRPC requests are always routed to the same pod!

gRPC On Kubernetes With Linkerd:

Linkerd is a light weight service mesh. It is gRPC aware and it could load balance gRPC requests just out of the box without any change in the app / deployment yaml.

Once you have Linkerd setup in your cluster, Run this command.

linkerd inject deployment.yaml| kubectl apply -f -

Now if we send the same CURL request, we could see the requests are being balanced automatically by Linkerd proxy side car containers.

[
   {
      "input":1,
      "result":1,
      "hostname":"square-app-6b8449f5b4-hdbj5"
   },
   {
      "input":2,
      "result":4,
      "hostname":"square-app-6b8449f5b4-857px"
   },
   {
      "input":3,
      "result":9,
      "hostname":"square-app-6b8449f5b4-lvht9"
   },
   {
      "input":4,
      "result":16,
      "hostname":"square-app-6b8449f5b4-857px"
   },
   {
      "input":5,
      "result":25,
      "hostname":"square-app-6b8449f5b4-hdbj5"
   }
]

Linkerd adds lightweight side-car proxy containers for every pod in our app as shown below which distribute the gRPC requests.

Summary:

We were able to successfully demonstrate load balancing gRPC Requests On Kubernetes with Linkerd.

Read more about gRPC / Kubernetes.

The source code is available here.

Happy learning 🙂

Share This:

Exit mobile version