background

Recently, in the process of business development, it has been found that maintaining business communication between multiple services at the same time has high development costs. After peeking into some of my colleagues’ code, I found that they were using RPC to build the service in general, so I decided to try refactoring the service using gRPC. Here are some basic information about the project:

  • Maintain multiple independent services simultaneously.
  • The services are deployed on multiple machines.
  • There is business communication between services (http).
  • Project directory
│ ├ ─ ─ controller │ ├ ─ ─ service │ ├ ─ ─ the route ├ ─ ─ app. The tsCopy the code

Problem analysis

Every time I try to add a new service, I need to ensure that the field types and names match exactly through routing, I need to write API documentation for constraints, and THEN I need to write additional code for input and output of the data format. Finally, when the response from the API is received, the service does the whole process in reverse again. The whole process: slow development.

Demand analysis

  • Refactoring without breaking the original project structure and business code as much as possible.
  • Write aAPIMultiple service invocations can be constrained simultaneously.
  • No additional code needs to be written for input and output.

The implementation process

The following uses the server as an example. First, replace the service entry with gRPC.

import service from 'controller';

// Load the proto file
const grpcPkg = protoLoader.loadSync(file, loader);

function main() {
    const server = new grpc.Server();
    // Go through grpcPkg and load the corresponding service method
    bindServices(server, service, grpcPkg);
  
    server.bind('0.0.0.0:50051', grpc.ServerCredentials.createInsecure());
    server.start();
}
Copy the code

According to RPC fundamentals, we need to write a.proto file to describe the API exposed by the service:

syntax = "proto3";

package test;

service Api {
  rpc index (Query) returns (Response) {};
  rpc show (Param) returns (Response) {};
  rpc update (Body) returns (Response) {};
  rpc create (Body) returns (Response) {};
  rpc del (Param) returns (Response) {};
}

message Response {
  int32 code = 1;
  string data = 2;
}
Copy the code

Here I keep the method declared by the service in the file consistent with the controller of the HTTP service

class Api {
  public index() {}
  public show() {}
  public update() {}
  public create() {}
  public del() {}
}
Copy the code

In bindService, we can walk through the loaded grpcPkg and match the service declared in it to the Controller.

  // Initialize method
    for (const definition of getServiceNames(grpcPkg)) {
      const service = (service as any)[definition.name.toLowerCase()]; !!!!! service && service.addService(// Proto file parses the service
          definition.service.service,
          // Package corresponding to service
          await createService(service)
        );
    }
Copy the code

To ensure that methods in each controller accurately correspond to methods loaded in grpcPkg and are common to all controllers, we can use reflect-Metadata to collect methods in controller. Try not to intrude into the original business.

// decorator.ts
export const ROUTE_MAPPING_METADATA = 'MODULE_ROUTE_MAPPING';
export const ROUTE_METADATA = 'MODULE_ROUTE';
export const ROUTE_METADATA_PARAMS = 'MODULE_ROUTE_PARAMS';

export function Handler(params: any = {}) {
  return (target: any, methodName: string, descriptor: PropertyDescriptor) = > {
    // Collect method name
    const methods = Reflect.getMetadata(ROUTE_MAPPING_METADATA, target) || new Set<string> (); methods.add(methodName);// Collect method references
    Reflect.defineMetadata(ROUTE_METADATA, descriptor.value, target, methodName);
    // Collect method extra parameters
    Reflect.defineMetadata(ROUTE_MAPPING_METADATA, methods, target);
    Reflect.defineMetadata(
      ROUTE_METADATA_PARAMS,
      params,
      target,
      methodName
    );
    return descriptor;
  };
}

// controller.ts
class Api {
  // rpc method
  @Handler(a)public index() {}
  
  // http method
  public show() {}
}
Copy the code

Then, in addService, we can get the method corresponding to the grpcPkg method and wrap it. Achieve the purpose of unified input and output.

function createService(controller: any) {
    const handlers: any = {};
    // Get the method set of the current controller
    const methods: Set<string> = Reflect.getMetadata(ROUTE_MAPPING_METADATA, controller);
    for (const method of Array.from(methods)) {
      const handler = Reflect.getMetadata(ROUTE_METADATA, controller, method);
      if(! handler)continue;
      / / packing method
      const enhancer = createServiceMethod(
        handler
      );
      handlers[method] = enhancer;
    }
    // Return a set of methods
    return handlers;
  }
Copy the code

Finally, there is the issue of maintaining the PROTO file, since multiple services may have to share the same ProTO file. Here we can use git subtree to extract proto as a subproject for maintenance.

The resources

  • gRPC vs. REST: Performance Simplified
  • Official document of gRPC
  • Talk about Node.js RPC (1) — Protocol