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?
- What does chassis do when it runs
- The hide operation at chassis runtime.
- 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:
- Why is a message indicating that the configuration cannot be found after a configuration is added?
- 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:
- Find the service according to the schema and encapsulate the corresponding Handle func with handler chain
- Start the service and register the service with the service center
- 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:
- Fetch a ResourceFunc from Route (real handler func)
- Convert HttpRequest to Chassis Invocation,
- Add the Invocation back to request to the handler chain
- 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.
- After the protocol request is received, the Protocol Servers are converted to a unified Invocation model
- Invocation into the processing chain
- 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
- Build microservices using the Service ecomb Go-chassis
- Define “Communication protocol” for Go language cloud application development
- Pattern: Microservice chassis
- Signal processing in Linux Signal and Golang
- Golang HTTP shutdown graceful exit principle
- 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