java version of gRPC practice 6: the client dynamically obtains the server address

Links to the full series of "java version gRPC actual combat"

  1. Generate code with proto
  2. Service publishing and invocation
  3. Server stream
  4. Client stream
  5. Bidirectional flow
  6. The client dynamically obtains the server address
  7. Registration discovery based on eureka

Why does the client get the server address dynamically

This article is the sixth in the series of gRPC practice in java. When we develop client applications, the required server addresses are set according to the following steps:

  • Configure in application.yml, as shown in the following figure:
  • In the bean using gRPC, the Stub class can be injected into the member variable by using the annotation GrpcClient:
  • The advantages of the above operation methods are easy to use and good configuration, and the disadvantages are also obvious: once the IP address or port of the server changes, you must modify application.yml and restart the client application;

Why not use a registry

  • You will think that the simplest way to solve the above problems is to use the registry, such as nacos and eureka. In fact, I think so too. Until one day, due to work reasons, I will deploy my own application in an existing GRC micro service environment. This micro service environment is not a java technology stack, but based on golang. They all use the go zero framework (I'm worried). This go zero framework does not provide an SDK for the java language. Therefore, I can only follow the rules of the go zero framework and obtain the address information of other microservices from etcd before calling other gRPC servers, as shown in the following figure:
  • In this way, our previous method of configuring server information in application.yml will not work. In this article, we will develop a new gRPC client application to meet the following requirements:
  1. When creating a Stub object, the information on the server is no longer from the annotation GrpcClient, but from the result of querying etcd;
  2. When the server information on etcd changes, the client can update it in time without restarting the application;

Overview of this article

  • This article will develop the springboot application named get-service-addr-from-etcd. The application gets the IP and port of local-server application from etcd, then calls the sayHello interface of local-server.
  1. Develop client applications;
  2. Deploy gRPC server application;
  3. Deploy etcd;
  4. Simulate the rules of go zero and write the IP address and port of the server application into etcd;
  5. Start the client application to verify whether the service of the server can be called normally;
  6. Restart the server and modify the port when restarting;
  7. Modify the port information of the server in etcd;
  8. Call the interface to trigger the client to re instantiate the Stub object;
  9. Verify whether the client can normally call the server service with the port modified;

Source download

  • The complete source code in this actual combat can be downloaded from GitHub. The address and link information are shown in the table below( https://github.com/zq2599/blog_demos):

name

link

remarks

Project Home

https://github.com/zq2599/blog_demos

The project is on the GitHub home page

git warehouse address (https)

https://github.com/zq2599/blog_demos.git

The warehouse address of the source code of the project, https protocol

git warehouse address (ssh)

git@github.com:zq2599/blog_demos.git

The project source code warehouse address, ssh protocol

  • There are multiple folders in the git project. The source code of the gRPC practical combat series for java is in the gRPC tutorials folder, as shown in the red box below:
  • There are multiple directories in the grpc tutorials folder. The corresponding client code of this article is in the get service addr from etcd directory, as shown in the following figure:

Developing client applications

  • Add a new line in the build.gradle file of the parent project, which is the library related to etcd, as shown in the red box below:
  • Create a new module named get service addr from etcd under the parent project grpc tutorials, and its build.gradle content is as follows:
plugins {
    id 'org.springframework.boot'
}

dependencies {
    implementation 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'net.devh:grpc-client-spring-boot-starter'
    implementation 'io.etcd:jetcd-core'
    implementation project(':grpc-lib')
}
  • Configure the file application.yml to set your own web port number and application name. In addition, grpc.etcdendpoints is the address information of etcd cluster:
server:
  port: 8084
spring:
  application:
    name: get-service-addr-from-etcd

grpc:
  # The address of etcd, and obtain the IP and port of gRPC server from here
  etcdendpoints: 'http://192.168.72.128:2379,http://192.168.50.239:2380,http://192.168.50.239:2381'
  • The code of the startup class dynamicserveriaddressdemoapplication.java will not be pasted. It is just an ordinary springboot startup class;
  • A new stubbwrapper.java file is added, which is a spring bean. The simpleblockingsub method should be focused on. When the bean is registered in spring, the simpleblockingsub method will be executed. In this way, whenever the bean is registered in spring, the gRPC server information will be queried from etcd, and then the simpleblockingsub object will be created:
package com.bolingcavalry.dynamicrpcaddr;

import com.bolingcavalry.grpctutorials.lib.SimpleGrpc;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Client;
import io.etcd.jetcd.KV;
import io.etcd.jetcd.kv.GetResponse;
import io.grpc.Channel;
import io.grpc.ManagedChannelBuilder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Arrays;
import static com.google.common.base.Charsets.UTF_8;

/**
 * @author will (zq2599@gmail.com)
 * @version 1.0
 * @description: The class that wraps the simpleblockingsub instance needs to use the simpleblockingsub instance when initiating gRPC requests
 * @date 2021/5/8 19:34
 */
@Component("stubWrapper")
@Data
@Slf4j
@ConfigurationProperties(prefix = "grpc")
public class StubWrapper {

    /**
     * This is a key in etcd. The corresponding value of this key is the address information of grpc server
     */
    private static final String GRPC_SERVER_INFO_KEY = "/grpc/local-server";

    /**
     * etcd address written in the configuration file
     */
    private String etcdendpoints;

    private SimpleGrpc.SimpleBlockingStub simpleBlockingStub;

    /**
     * Query the address of gRPC server from etcd
     * @return
     */
    public String[] getGrpcServerInfo() {
        // Create client class
        KV kvClient = Client.builder().endpoints(etcdendpoints.split(",")).build().getKVClient();

        GetResponse response = null;

        // Go to etcd to query the value of / grpc / local server
        try {
            response = kvClient.get(ByteSequence.from(GRPC_SERVER_INFO_KEY, UTF_8)).get();
        } catch (Exception exception) {
            log.error("get grpc key from etcd error", exception);
        }

        if (null==response || response.getKvs().isEmpty()) {
            log.error("empty value of key [{}]", GRPC_SERVER_INFO_KEY);
            return null;
        }

        // Get value from response
        String rawAddrInfo = response.getKvs().get(0).getValue().toString(UTF_8);

        // rawAddrInfo is a string such as "192.169.0.1:8080", that is, an IP and a port, separated by ":,
        // Here, use ":" to split into an array and return
        return null==rawAddrInfo ? null : rawAddrInfo.split(":");
    }

    /**
     * The method that will be executed every time the bean is registered,
     * This method obtains the gRPC server address from etcd,
     * Used to instantiate the member variable simpleblockingsub
     */
    @PostConstruct
    public void simpleBlockingStub() {
        // Get address information from etcd
        String[] array = getGrpcServerInfo();

        log.info("create stub bean, array info from etcd {}", Arrays.toString(array));

        // The first element of the array is the IP address of the gRPC server, and the second element is the port
        if (null==array || array.length<2) {
            log.error("can not get valid grpc address from etcd");
            return;
        }

        // The first element of the array is the IP address of the gRPC server
        String addr = array[0];
        // The second element of the array is the port
        int port = Integer.parseInt(array[1]);

        // Create a channel according to the address and port of the gRPC server just obtained
        Channel channel = ManagedChannelBuilder
                .forAddress(addr, port)
                .usePlaintext()
                .build();

        // Create stub based on channel
        simpleBlockingStub = SimpleGrpc.newBlockingStub(channel);
    }
}
  • GrpcClientService is a service class that encapsulates StubWrapper:
package com.bolingcavalry.dynamicrpcaddr;

import com.bolingcavalry.grpctutorials.lib.HelloReply;
import com.bolingcavalry.grpctutorials.lib.HelloRequest;
import com.bolingcavalry.grpctutorials.lib.SimpleGrpc;
import io.grpc.StatusRuntimeException;
import lombok.Setter;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class GrpcClientService {

    @Autowired(required = false)
    @Setter
    private StubWrapper stubWrapper;

    public String sendMessage(final String name) {
        // Most likely, the simplestab object is null
        if (null==stubWrapper) {
            return "invalid SimpleBlockingStub, please check etcd configuration";
        }

        try {
            final HelloReply response = stubWrapper.getSimpleBlockingStub().sayHello(HelloRequest.newBuilder().setName(name).build());
            return response.getMessage();
        } catch (final StatusRuntimeException e) {
            return "FAILED with " + e.getStatus().getCode().name();
        }
    }
}
  • A controller class GrpcClientController is added to provide an http interface, which will call the method of GrpcClientService and finally complete the remote gRPC call:
package com.bolingcavalry.dynamicrpcaddr;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GrpcClientController {

    @Autowired
    private GrpcClientService grpcClientService;

    @RequestMapping("/")
    public String printMessage(@RequestParam(defaultValue = "will") String name) {
        return grpcClientService.sendMessage(name);
    }
}
  • Next, add a controller class refreshstub instancecontroller, which provides an http interface refreshhub externally. The function is to delete the bean of stubWrapper and re register it. In this way, whenever the refreshhub interface is called externally, you can obtain the server information from etcd and re instantiate the simpleblockingsub member variable, so that the client can obtain the service dynamically Effect of end address:
package com.bolingcavalry.dynamicrpcaddr;

import com.bolingcavalry.grpctutorials.lib.SimpleGrpc;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RefreshStubInstanceController implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Autowired
    private GrpcClientService grpcClientService;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @RequestMapping("/refreshstub")
    public String refreshstub() {

        String beanName = "stubWrapper";

        //Get BeanFactory
        DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();

        // Delete existing bean s
        defaultListableBeanFactory.removeBeanDefinition(beanName);

        //Create bean information
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(StubWrapper.class);

        //Dynamically register bean s
        defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getBeanDefinition());

        // Update the reference relationship (note that the applicationContext.getBean method is very important and will trigger the stubbwrapper instantiation operation)
        grpcClientService.setStubWrapper(applicationContext.getBean(StubWrapper.class));

        return "Refresh success";
    }
}
  • After coding, start verification;

Deploy gRPC server application

Deploying the gRPC server application is very simple. Start the local server application:

Deploy etcd

  • In order to simplify the operation, the etcd cluster here is deployed with docker, and the corresponding docker-compose.yml file is as follows:
version: '3'
services:
  etcd1:
    image: "quay.io/coreos/etcd:v3.4.7"
    entrypoint: /usr/local/bin/etcd
    command:
      - '--name=etcd1'
      - '--data-dir=/etcd_data'
      - '--initial-advertise-peer-urls=http://etcd1:2380'
      - '--listen-peer-urls=http://0.0.0.0:2380'
      - '--listen-client-urls=http://0.0.0.0:2379'
      - '--advertise-client-urls=http://etcd1:2379'
      - '--initial-cluster-token=etcd-cluster'
      - '--heartbeat-interval=250'
      - '--election-timeout=1250'
      - '--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380'
      - '--initial-cluster-state=new'
    ports:
      - 2379:2379
    volumes:
      - ./store/etcd1/data:/etcd_data
  etcd2:
    image: "quay.io/coreos/etcd:v3.4.7"
    entrypoint: /usr/local/bin/etcd
    command:
      - '--name=etcd2'
      - '--data-dir=/etcd_data'
      - '--initial-advertise-peer-urls=http://etcd2:2380'
      - '--listen-peer-urls=http://0.0.0.0:2380'
      - '--listen-client-urls=http://0.0.0.0:2379'
      - '--advertise-client-urls=http://etcd2:2379'
      - '--initial-cluster-token=etcd-cluster'
      - '--heartbeat-interval=250'
      - '--election-timeout=1250'
      - '--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380'
      - '--initial-cluster-state=new'
    ports:
      - 2380:2379
    volumes:
      - ./store/etcd2/data:/etcd_data
  etcd3:
    image: "quay.io/coreos/etcd:v3.4.7"
    entrypoint: /usr/local/bin/etcd
    command:
      - '--name=etcd3'
      - '--data-dir=/etcd_data'
      - '--initial-advertise-peer-urls=http://etcd3:2380'
      - '--listen-peer-urls=http://0.0.0.0:2380'
      - '--listen-client-urls=http://0.0.0.0:2379'
      - '--advertise-client-urls=http://etcd3:2379'
      - '--initial-cluster-token=etcd-cluster'
      - '--heartbeat-interval=250'
      - '--election-timeout=1250'
      - '--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380'
      - '--initial-cluster-state=new'
    ports:
      - 2381:2379
    volumes:
      - ./store/etcd3/data:/etcd_data
  • After preparing the above files, execute docker compose up - D to create a cluster;

Write the IP address and port of the server application to etcd

  • The IP address of my local server is 192.168.50.5 and port is 9898, so execute the following command to write the local server information to etcd:
docker exec 08_etcd2_1 /usr/local/bin/etcdctl put /grpc/local-server 192.168.50.5:9898

Start client application

  • Open dynamicservertaddressdemoapplication.java and click the red box in the figure below to start the client application:
  • Note the log in the red box below, which proves that the client application successfully obtains the server information from etcd:
  • The browser accesses the http interface of the application get service addr from etcd and successfully receives a response, which proves that the gRPC call is successful:
  • Go to the local server console, as shown in the red box below, to prove that the remote call is indeed executed:

Restart the server and modify the port when restarting

  • In order to verify whether the dynamic acquisition of server information is effective, let's first change the port of the local server application, as shown in the red box below, to 9899:
  • Restart the local server after the change, as shown in the red box below, it can be seen that the gRPC port has been changed to 9899:
  • At this time, access the http interface of get service addr from etcd. Because get service addr from etcd didn't know that the listening port of local server had changed, it still accessed port 9898 and returned a failure without accident:

Modify the port information of the server in etcd

Now execute the following command to change the server information in etcd to correct:

docker exec 08_etcd2_1 /usr/local/bin/etcdctl put /grpc/local-server 192.168.50.5:9899

Calling the interface triggers the client to re instantiate the Stub object

  • Smart, you must know what to do next: let the stubbwrapper bean re register in the spring environment, that is, call the http interface RefreshStubInstanceController provided by RefreshStubInstanceController:
  • Check the console of the get service addr from etcd application, as shown in the red box below. The StubWrapper has been re registered and the latest server information has been obtained from etcd:

Verify whether the client can call the server service with the port modified normally

  • Access the web interface of the get service addr from etcd application again, as shown in the following figure. gRPC call succeeds:
  • So far, without modifying the configuration and restarting the service, the client can adapt to the changes of the server. Of course, this paper only provides basic operation reference. In fact, the micro service environment will be more complex. For example, the refreshtube interface may be called by other services, so that the server can be updated more timely when there are changes, In addition, the client itself may also be a gRPC service provider, so you should also register yourself with etcd, monitor whether the specified server is always alive by using the watch function of etcd, and how to balance the load of multiple instances of the same gRPC service. These should be customized according to your actual situation;
  • There are too many contents in this article. It can be seen that for these microservice environments that are not supported by the official, it is time-consuming and laborious for us to do the registration and discovery by ourselves. If the design and selection can be made by ourselves, we prefer to use the existing registration center. In the next article, we will try to use eureka to provide registration and discovery services for gRPC;

Posted on Tue, 07 Dec 2021 01:50:26 -0500 by Jaguar