What is the chassis?

Chassis is a microservice model. In this pattern, the user does not have to handle the external configuration, logging, health checks, distributed tracking, and so on in the process of building microservices themselves, but hands them over to a specialized framework. Users can focus more on the business logic itself and develop microservices simply and quickly.

What do you get from reading this article?

  1. What does chassis do when it runs
  2. The hide operation at chassis runtime.
  3. Some understanding of the chassis design idea

What is a Go-chassis?

Go-chassis is a micro-service development framework based on the Go language. It adopts plug-in design and provides components such as pluggable registration discovery, encryption and decryption, and call chain tracing. The protocol is also plug-in, supports HTTP and GRPC, and allows developers to customize proprietary protocols. Developers only need to focus on implementing cloud native applications.

Cloud native applications, applications developed based on or deployed for cloud services.

The diagram above shows the architecture of Go-Chassis. It can be seen that configuration management (Archaius), service registration (Registry), Metrics and Logger are all independent components. Distributed tracing, load balancing and traffic limiting are all implemented by middleware (Handler Chain). After a request comes in, it will first be converted into chassis Invoker through server, then go through Handler Chain, and finally be converted into response of corresponding protocol by Transport.

For the design concept of Chassis, you can read the article written by chassis author [Second-hand Lion] to define the “Communication protocol” of Go language cloud application development.

This article focuses on what is done during the Go-Chassis startup process and how it is used.

A case in point

Starting with Hello World, the directory structure looks like this:

.├ ─ conf conf, must │ ├─ chassis.yaml # │ ├─ microsystem.yaml # ├─ ├─ ├─ ├.goCopy the code

Chassis. yaml contains:

   ---
   cse:
     protocols:
       rest:
         listenAddress: "127.0.0.1:5001"
     transport:
       timeout:
         rest: 1
     handler:
       chain:
         Provider:
           default: tracing-provider
Copy the code

The contents of microservice.yaml are:

   cse:
     service:
       registry:
           address: http://127.0.0.1:30100
   
   service_description:
     name: test-rest-server
Copy the code

main.go

package main
   
import (
       rf "github.com/go-chassis/go-chassis/v2/server/restful"
       "log"
       "net/http"
   
       "github.com/go-chassis/go-chassis/v2"
   )
   
//RestFulHello is a struct used for implementation of restfull hello program
type RestFulHello struct{}//Sayhi is a method used to reply user with hello world text
func (r *RestFulHello) Sayhi(b *rf.Context) {
       b.Write([]byte( "hello world"))
       return
}
   
//URLPatterns helps to respond for corresponding API calls
func (r *RestFulHello) URLPatterns(a) []rf.Route {
       return []rf.Route{
           {Method: http.MethodGet, Path: "/sayhi", ResourceFunc: r.Sayhi,
               Returns: []*rf.Returns{{Code: 200}}}}},func main(a) {
   chassis.RegisterSchema("rest", &RestFulHello{})
   iferr := chassis.Init(); err ! =nil {
       log.Fatal("Init failed." + err.Error())
       return
   }
    chassis.Run()
}
Copy the code

Let’s take a look at what this code does.

  • 11 to 27 declare a RestFulHello struct that has two methods Sayhi and URLPatterns, where URLPatterns returns a list of routes. This code declares an HTTP handler and the corresponding route, but I’ll explain why in a second.
type Schema struct {
    serverName string
    schema     interface{}
    opts       []server.RegisterOption
}
Copy the code
  • 30 line chassiss. RegisterSchema(“rest”, &RestFulHello{}) registers the previously declared RestFulHello to the “rest” service.

    Internally, it is simply a matter of creating a chassiss.schema using the parameters passed in and appending it to chassiss.schemas.

  • Initialization of line 31 Chassis before running.

  • 35 lines to run the Chassis service.

Run the go run rest/main.go command to run the code. The system finds that the startup fails and the log output is as follows:

   INFO: Install client plugin, protocol: rest
   INFO: Install Provider Plugin, name: default
   INFO: Installed Server Plugin, protocol:rest
   
   ERROR: add file sourceerror [[/var/folders/rr/rzqnl9h10y577rch1nsx_jww0000gp/T/go-build725280265/b001/exe/conf/chassis.yaml] file not exist]. File: [email protected] / chassis_init. Go: 106, MSG: failed to initialize the conf: [/var/folders/rr/rzqnl9h10y577rch1nsx_jww0000gp/T/go-build725280265/b001/exe/conf/chassis.yaml] file not exist init chassis fail: [/var/folders/rr/rzqnl9h10y577rch1nsx_jww0000gp/T/go-build725280265/b001/exe/conf/chassis.yaml] file not exist Init failed.[/var/folders/rr/rzqnl9h10y577rch1nsx_jww0000gp/T/go-build725280265/b001/exe/conf/chassis.yaml] file not existCopy the code

From the logs, you can see two problems:

  1. Why is a message indicating that the configuration cannot be found after a configuration is added?
  2. Why can the plug-in be successfully installed when the configuration is not successfully loaded?

chassis init

The following is the execution flow of Chassis Init:

Enter [chassis_init] to obtain the original image

Configuration initialization

First take a look at how the configuration is loaded during chassis initialization.

Check the config. The Init () code can see configuration directory is through fileutil RouterConfigPath () to obtain, directory initialization method is:

   func initDir(a) {
       ifh := os.Getenv(ChassisHome); h ! ="" {
           homeDir = h
       } else {
           wd, err := GetWorkDir()
           iferr ! =nil {
               panic(err)
           }
           homeDir = wd
       }
   
       // set conf dir, CHASSIS_CONF_DIR has highest priority
       ifconfDir := os.Getenv(ChassisConfDir); confDir ! ="" {
           configDir = confDir
       } else {
           // CHASSIS_HOME has second most high priority
           configDir = filepath.Join(homeDir, "conf")}}Copy the code

If you specify the application directory using the ChassisHome environment variable, the configuration is read from the ChassisHome/conf/ directory under that directory at chassis runtime

You can also directly specify the configuration directory by using ChassisConfDir, which has a higher priority than ChassisHome/conf

Chassis uses Archaius to manage configuration. Archaius initializes configuration from files, environment variables, command lines, and memory.

   // InitArchaius initialize the archaius
   func InitArchaius(a) error {
       var err error
   
       requiredFiles := []string{
           fileutil.GlobalConfigPath(),
           fileutil.MicroServiceConfigPath(),
       }
       optionalFiles := []string{
           fileutil.CircuitBreakerConfigPath(),
           fileutil.LoadBalancingConfigPath(),
           fileutil.RateLimitingFile(),
           fileutil.TLSConfigPath(),
           fileutil.MonitoringConfigPath(),
           fileutil.AuthConfigPath(),
           fileutil.TracingPath(),
           fileutil.LogConfigPath(),
           fileutil.RouterConfigPath(),
       }
   
       err = archaius.Init( // Initialize the configuration
           archaius.WithCommandLineSource(),
           archaius.WithMemorySource(),
           archaius.WithENVSource(),
           archaius.WithRequiredFiles(requiredFiles),
           archaius.WithOptionalFiles(optionalFiles))
   
       return err
Copy the code

As you can see from the code, global config and MicroService config are required,

Global config corresponds to conf_PATH/chassism.yaml

Microservice config corresponds to conf_PATH /microservice.yaml

Next read the configuration and give the value of the initialization Runtime:

The data in the Runtime can be considered global variables at runtime

.// The runtime data can be considered global variables at runtime
       runtime.ServiceName = MicroserviceDefinition.ServiceDescription.Name
       runtime.Version = MicroserviceDefinition.ServiceDescription.Version
       runtime.Environment = MicroserviceDefinition.ServiceDescription.Environment
       runtime.MD = MicroserviceDefinition.ServiceDescription.Properties
       ifMicroserviceDefinition.AppID ! ="" { //microservice.yaml has first priority
           runtime.App = MicroserviceDefinition.AppID
       } else ifGlobalDefinition.AppID ! ="" { //chassis.yaml has second priority
           runtime.App = GlobalDefinition.AppID
       }
       if runtime.App == "" {
           runtime.App = common.DefaultApp
       }
   
       runtime.HostName = MicroserviceDefinition.ServiceDescription.Hostname
   ...
Copy the code

Archaius also supports reading configuration from the configuration center, and by doing so, Chassis also provides a run-time configuration hot loading capability.

For the second question, why is the plug-in installed before the configuration?

Plug-in initialization

As you can see from the figure, init pre-initializes many plug-ins, such as client, provider, server, log, router rule, Register, Load Balance, Service Discover, Treporter, etc. And there is no explicit initialization call in the Chassis Init method. A look at the code shows that this step is done automatically using the respective init methods, like this:

// restful server
func init(a) {
    server.InstallPlugin(Name, newRestfulServer)
}

// route rule plugin
func init(a) { 
    router.InstallRouterService("cse", newRouter)
}

// init initialize the plugin of service center registry
func init(a) {
    registry.InstallRegistrator(ServiceCenter, NewRegistrator)
    registry.InstallServiceDiscovery(ServiceCenter, NewServiceDiscovery)
    registry.InstallContractDiscovery(ServiceCenter, newContractDiscovery)

}

// init install plugin of new file registry
func init(a) {
    registry.InstallRegistrator(Name, newFileRegistry)
    registry.InstallServiceDiscovery(Name, newDiscovery)
}
Copy the code

The reason for implicit loading is that chassis is a plug-in design. The plug-in can be inserted and played by loading the plug-in in init mode. The plug-in to be used only needs to add the package import in the code, for example, GRPC plug-in can be added in main.go

import _ "github.com/go-chassis/go-chassis-extension/protocol/grpc/server"
Copy the code

This series of plug-in installation methods can also be seen that for Chassis, the registry, protocol and load balancing are all plug-ins, which means that these plug-ins are replaceable and convenient for secondary development.

Both of the above problems are now resolved, and the following command is now used to run the service:

CHASSIS_CONF_DIR=`pwd`/conf go run rest/main.go
Copy the code

Example Initialize the handler chain

Handler is the smallest processing unit at the framework level during the microserver operation. Go Chassis realizes componentized running model architecture through handler and the assembly of handler. Its basic use is to implement interface, registration logic:

The Handler definition is very simple, implementing the Handler interface is considered to have created a Handler.

// Handler interface for handlers
type Handler interface {
    // handle invocation transportation,and tr response
    Handle(*Chain, *invocation.Invocation, invocation.ResponseCallBack)
    Name() string
}
Copy the code

Using the RegisterHandler function will be added to HandlerFuncMap to be used when CreateHandler is called.

// RegisterHandler Let developer custom handler
func RegisterHandler(name string, f func(a) Handler) error {
    if stringutil.StringInSlice(name, buildIn) {
        return errViolateBuildIn
    }
    _, ok := HandlerFuncMap[name]
    if ok {
        return ErrDuplicatedHandler
    }
    HandlerFuncMap[name] = f
    return nil
}
Copy the code

For Chassis, protocol transformation, permission verification, and full link tracing can all be considered as a handler (middleware), which reads the declared handler from the configuration and initializes it. When a request is invoked, it goes to handler for processing in the order defined in the configuration file.

During service initialization, go-chassis loads required handlers according to the definition in the configuration file, including Provider, Consumer, and Default. The following is an example:

handler:
    chain:
      Provider:
        default: tracing-provider
        rest: jwt
Copy the code

If a non-default type is configured, only this particular handler will be executed when the service is started. For example, in this configuration, the handler will only execute JWT and ignore the tracing provider

This is because a chassis uses map to store handler chain. The key of map is chainType+chainName, and default is also a chainType. If name(chain type) has a value, the corresponding chain is used; otherwise, default is used.

type Chain struct {
    ServiceType string
    Name string
    Handlers []Handler
}

// GetChain is to get chain
func GetChain(serviceType string, name string) (*Chain, error) {
    if name == "" {
        name = common.DefaultChainName
    }
    origin, ok := ChainMap[serviceType+name]
    if! ok {return nil, fmt.Errorf("get chain [%s] failed", serviceType+name)
    }
    return origin, nil
}

// 
chainMap := chaninMap[strint]*Chain{
    "Provider+rest":  &Chain{
          ServiceType: "Provider",
          Name: "rest",
          Handlers: []Handler{jwt},},
    "Provider+default": &Chain{
          ServiceType: "Provider",
          Name: "default",
          Handlers: []Handler{tracing-provider}},,
}
Copy the code

Initialize the server

The premise of initialization is that the service has already been loaded, and the loading steps are already loaded through the init method before init.

//Init initializes
func Init(a) error {
    var err error
    for k, v := range config.GlobalDefinition.Cse.Protocols {
        iferr = initialServer(config.GlobalDefinition.Cse.Handler.Chain.Provider, v, k); err ! =nil {
            log.Println(err)
            return err
        }
    }
    return nil
}
Copy the code

This initializes the service specified by protocols in the configuration file.

// How to get the service
func GetServerFunc(protocol string) (NewFunc, error) {
    f, ok := serverPlugins[protocol]
    if! ok {return nil, fmt.Errorf("unknown protocol server [%s]", protocol)
    }
    return f, nil
}
Copy the code

*var* serverPlugins = make(*map*[string]NewFunc)

Chassis will have the REST plug-in installed by default, which needs to be specified first for GRPC

// p corresponds to the configuration in protocal
if p.Listen == "" {
        ifp.Advertise ! ="" {
            p.Listen = p.Advertise
        } else {
            p.Listen = iputil.DefaultEndpoint4Protocol(name)
        }
    }
Copy the code

Listen Advertise of the service has the highest priority. If neither Advertise nor Listen is configured, the default configuration is used.

Initialize server Options, where chainName Is used if the Provider is configured with a protocol name value.

chainName := common.DefaultChainName
    if _, ok := providerMap[name]; ok {
        chainName = name
    }

o := Options{
        Address:            p.Listen,  // The port to listen on in the configuration
        ProtocolServerName: name,      // The name of the Protocal provider, such as Rest GRPC
        ChainName:          chainName, // The name of the Protocal provider, such as Rest GRPC
        TLSConfig:          tlsConfig,
        BodyLimit:          config.GlobalDefinition.Cse.Transport.MaxBodyBytes["rest"],}Copy the code

other

Init also includes register, ConfigCenter, Router, Contorl, tracing, metric, Reporter, fuse, event listener, and so on.

At this point, the initialization steps required for the Chassis are complete, and the next step is to run the service.

chassis run

Take a look at the overall flow of chassiss.run () starting

Enter [chassis_run] to get the original image

Chassis operation is mainly divided into three actions:

  1. Find the service according to the schema and encapsulate the corresponding Handle func with handler chain
  2. Start the service and register the service with the service center
  3. Monitor exit signal

Here we use the REST service as an example to see what happens when a Chassis starts the service.

The service registry

First, review the Hello World code:

//RestFulHello is a struct used for implementation of restfull hello program
type RestFulHello struct{}//Sayhi is a method used to reply user with hello world text
func (r *RestFulHello) Sayhi(b *rf.Context) {
    b.Write([]byte( "hello world"))
    return
}

//URLPatterns helps to respond for corresponding API calls
func (r *RestFulHello) URLPatterns(a) []rf.Route {
    return []rf.Route{
        {Method: http.MethodGet, Path: "/sayhi", ResourceFunc: r.Sayhi,
            Returns: []*rf.Returns{{Code: 200}}},
    }
}

chassis.RegisterSchema("rest", &RestFulHello{}) // The first parameter is the service name, and the second parameter is Router
Copy the code

RestFulHello, which has an URLPatterns() []Route method that implements the Router interface.

The Router is defined

//Router is to define how route the request
type Router interface {
    //URLPatterns returns route
    URLPatterns() []Route
}
Copy the code

// HTTPRequest2Invocation convert http request to uniform invocation data format
func HTTPRequest2Invocation(req *restful.Request, schema, operation string, resp *restful.Response) (*invocation.Invocation, error) {
    inv := &invocation.Invocation{
        MicroServiceName:   runtime.ServiceName,
        SourceMicroService: common.GetXCSEContext(common.HeaderSourceName, req.Request),
        Args:               req,
        Reply:              resp,
        Protocol:           common.ProtocolRest,
        SchemaID:           schema,
        OperationID:        operation,
        URLPathFormat:      req.Request.URL.Path,
        Metadata: map[string]interface{}{
            common.RestMethod: req.Request.Method,
        },
    }
    //set headers to Ctx, then user do not need to consider about protocol in handlers
    m := make(map[string]string)
    inv.Ctx = context.WithValue(context.Background(), common.ContextHeaderKey{}, m)
    for k := range req.Request.Header {
        m[k] = req.Request.Header.Get(k)
    }
    return inv, nil
}
Copy the code

The service registration process started consists of taking all the routers from the Schemas and traversing them, calling the WrapHandlerChain() function. This function does the following:

  1. Fetch a ResourceFunc from Route (real handler func)
  2. Convert HttpRequest to Chassis Invocation,
  3. Add the Invocation back to request to the handler chain
  4. Returns a closure function.

Finally, the handler wrapped with the WrapHandlerChain is registered with the Go-restful framework.

In response to a request, the invocation relationship looks like the following:

func handle(a){
    func handle1(a){
        func handle2(a){
            func handle3(a){
                real_handle_func()
            }()
        }()
    }()
}
Copy the code

Why convert to uniform Invocation?

The Server translates the protocol request to the Invocation model and passes it to the Handler chain. Because the Handler handles the Invocation according to the Unified model, it does not need to develop a set of governance for each protocol developed. The processing chain can be configured to update and then go to the Transport Handler, which is transmitted to the target using the protocol client of the target microservice.

In this way, each server plug-in actually provides business processing, while chassis is just a middleman, which can handle request and response as it wants, such as flow limiting, fusing, routing update, etc.

  1. After the protocol request is received, the Protocol Servers are converted to a unified Invocation model
  2. Invocation into the processing chain
  3. After the processing is complete, the service processing logic is entered

Signal to monitor

When a service needs to be shut down or restarted, it should finish processing the current request or set it to timeout instead of brutally disconnecting. Chassis uses signal listening to process the shut down signal.

func waitingSignal(a) {
    //Graceful shutdown
    c := make(chan os.Signal) // Create an OS.signal channel
    // Register the signal to receive
    signal.Notify(c, syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGILL, syscall.SIGTRAP, syscall.SIGABRT)
    select {
    case s := <-c:
        openlogging.Info("got os signal " + s.String())
    case err := <-server.ErrRuntime:
        openlogging.Info("got server error " + err.Error())
    }

    // Check whether the service is registered
    if! config.GetRegistratorDisable() { registry.HBService.Stop()// Stop the heartbeat service
        openlogging.Info("unregister servers ...")
        // Exit the server Center
        iferr := server.UnRegistrySelfInstances(); err ! =nil {
            openlogging.GetLogger().Warnf("servers failed to unregister: %s", err)
        }
    }

    for name, s := range server.GetServers() {
        // Iterate over the service and call the stop method of the service
        openlogging.Info("stopping server " + name + "...")
        err := s.Stop()
        iferr ! =nil {
            openlogging.GetLogger().Warnf("servers failed to stop: %s", err)
        }
        openlogging.Info(name + " server stop success")
    }

    openlogging.Info("go chassis server gracefully shutdown")}Copy the code

This is done by sending os.signal to a channel using the GO Signal notification mechanism. Create an OS.signal channel and register signals to receive using signal.notify. Chassis focuses on the following signals:

signal value action instructions
SIGHUP 1 Term The terminal control process ends (the terminal is disconnected)
SIGINT 2 Term Triggered when the user sends INTR character (Ctrl+C)
SIGQUIT 3 Core The user sends the QUIT character (Ctrl+/)
SIGILL 4 Core Illegal instructions (program error, attempted execution of data segment, stack overflow, etc.)
SIGTRAP 5 Core Trap instruction firing (e.g., breakpoints, used in debuggers)
SIGABRT 6 Core Call abort to trigger
SIGTERM 15 Term End the program (can be caught, blocked, or ignored)

Upon receiving the signal, first determine whether to register with the service center, if so, stop heartbeat sending, exit the registration, and then call server.shutdown () to gracefully exit.

Go HTTP Server supports graceful exit after 1.8.

Cc /archives/58…

conclusion

This article introduces the startup process of chassis service, mainly introduces the initialization process of configuration, plug-in, handler chain and server in Init, and then analyzes the operations done when the service is started and the handling of service exit.

Refer to the link

  1. Build microservices using the Service ecomb Go-chassis
  2. Define “Communication protocol” for Go language cloud application development
  3. Pattern: Microservice chassis
  4. Signal processing in Linux Signal and Golang
  5. Golang HTTP shutdown graceful exit principle
  6. Go Language Microservices development framework practice – Go Chassis

Finally, thanks to the girlfriend support and tolerance, than ❤️

Around the male can also enter the keywords for historical article: male number & small program | | concurrent design mode & coroutines