Organizational Approach of Microservice Infrastructure Components

Keywords: Programming Kubernetes codec socket github

brief introduction

Micro is a micro-service framework implemented in go language, which realizes several common elements of services, such as gateway, proxy, registry, messaging, and also supports pluggable extension. This article explores how the project implements these components and organizes them to work together through a core object in micro.

Notice: go code is sometimes cumbersome. When intercepting the source code, deleting parts of the code that do not affect the mind will be marked as...

Core services

Microcreates a service instance through micro.NewService, through which all microservice instances (including gateways, proxies, etc.) need to interact with other services.

    type service struct {
        opts Options

        once sync.Once
    }


There is very little code, but all the micro service elements in the microbe will eventually converge on this type, because option is all-encompassing (^^).

    type Options struct {
        Broker    broker.Broker
        Cmd       cmd.Cmd
        Client    client.Client
        Server    server.Server
        Registry  registry.Registry
        Transport transport.Transport

        // Before and After funcs
        BeforeStart []func() error
        BeforeStop  []func() error
        AfterStart  []func() error
        AfterStop   []func() error

        // Other options for implementations of the interface
        // can be stored in a context
        Context context.Context
    }


Here you can see the common components of micro services. When micro.Server is initialized, it will set the corresponding components for this object. The components are set by default, cli, env reading.

func (s *service) Init(opts ...Option) {
	// process options
	for _, o := range opts {
		o(&s.opts)
	}

	s.once.Do(func() {
		// Initialise the command flags, overriding new service
		_ = s.opts.Cmd.Init(
			cmd.Broker(&s.opts.Broker),
			cmd.Registry(&s.opts.Registry),
			cmd.Transport(&s.opts.Transport),
			cmd.Client(&s.opts.Client),
			cmd.Server(&s.opts.Server),
		)
	})
}


Server is the ultimate behavior entity, which monitors ports and provides business services, registering local services to the registry. Broker asynchronous message, mq and other methods can replace this type of Client service call Client Registry registry Cmd Client Transport similar to socket, message synchronous communication, service monitoring, etc.

Service type

The types of services currently supported are rpc and grpc. If there are two different rpc protocols in the service, the protocol conversion will be carried out when the message is delivered. The default rpc service is described in detail here. rpc service is based on HTTP POST protocol. When the service starts, it will try to connect Broker, then register the service to the registry, and finally listen to the service port. Simply put forward here is how to achieve protocol conversion. If the message from http is to be delivered to a grpc protocol service, the corresponding purpose needs to be set in Content-Type. Service protocol type application/grpc, SerConn reads here in Content-Type, gets the corresponding Codec for protocol conversion, and finally delivers it to the corresponding service. The return content of the service also needs to be processed before returning.

    type Service interface {
        Init(...Option)
        Options() Options
        Client() client.Client
        Server() server.Server
        Run() error
        String() string
    }


rpc server

func (s *rpcServer) Start() error {
...
	ts, err := config.Transport.Listen(config.Address)
	// swap address
...
	// connect to the broker
	if err := config.Broker.Connect(); err != nil {
		return err
	}
...
	// use RegisterCheck func before register
	if err = s.opts.RegisterCheck(s.opts.Context); err != nil {
		log.Logf("Server %s-%s register check error: %s", config.Name, config.Id, err)
	} else {
		// announce self to the world
		if err = s.Register(); err != nil {
			log.Logf("Server %s-%s register error: %s", config.Name, config.Id, err)
		}
	}
...
	go func() {
		for {
			// listen for connections
			err := ts.Accept(s.ServeConn)
...
		}
	}()
..
	return nil
}


protocol conversion

    func (s *rpcServer) ServeConn(sock transport.Socket) {
...
        for {
            var msg transport.Message
            if err := sock.Recv(&msg); err != nil {
                return
            }
...
            // we use this Content-Type header to identify the codec needed
            ct := msg.Header["Content-Type"]

            // strip our headers
            hdr := make(map[string]string)
            for k, v := range msg.Header {
                hdr[k] = v
            }

            // set local/remote ips
            hdr["Local"] = sock.Local()
            hdr["Remote"] = sock.Remote()
...
            // TODO: needs better error handling
            var err error
            if cf, err = s.newCodec(ct); err != nil {  //Request Protocol Converter
...Return error
                return
            }
...
            rcodec := newRpcCodec(&msg, sock, cf)   //Return converter

            // internal request
            request := &rpcRequest{
                service:     getHeader("Micro-Service", msg.Header),
                method:      getHeader("Micro-Method", msg.Header),
                endpoint:    getHeader("Micro-Endpoint", msg.Header),
                contentType: ct,
                codec:       rcodec,
                header:      msg.Header,
                body:        msg.Body,
                socket:      sock,
                stream:      true,
            }

            // internal response
            response := &rpcResponse{
                header: make(map[string]string),
                socket: sock,
                codec:  rcodec,
            }

            // set router
            r := Router(s.router)
...
            // serve the actual request using the request router
            if err := r.ServeRequest(ctx, request, response); err != nil {
                // write an error response
                err = rcodec.Write(&codec.Message{
                    Header: msg.Header,
                    Error:  err.Error(),
                    Type:   codec.Error,
                }, nil)
                // could not write the error response
                if err != nil {
                    log.Logf("rpc: unable to write error response: %v", err)
                }
                if s.wg != nil {
                    s.wg.Done()
                }
                return
            }
...
        }
    }


Registry

The registry includes service discovery and service registration. Each registry type in micro implements registry interface

    type Registry interface {
        Init(...Option) error
        Options() Options
        Register(*Service, ...RegisterOption) error
        Deregister(*Service) error
        GetService(string) ([]*Service, error)
        ListServices() ([]*Service, error)
        Watch(...WatchOption) (Watcher, error)
        String() string
    }


The default registration center is mdns, which listens locally to a multicast address and receives all the information in the network and broadcasts it. At the same time, the information sent can be found by all other machines. Every time the program starts, it broadcasts its own service information. Other nodes receive this information and add it to their service list. When the service closes, it sends a shutdown message. MDNS itself does not have the functions of health check and fuse, and its starting point is only easy to test and use, so it is not recommended to use in production environment.

Resolve

Search service needs to get service name according to url or content information. After searching service name to registry and getting service, a node is delivered randomly.

    type Resolver interface {
        Resolve(r *http.Request) (*Endpoint, error)
        String() string
    }
    //The default api resolve instance, in addition to host, path, grpc three resolves, can be specified or set in the environment variable at startup time as required
    //
    func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
        var name, method string

        switch r.Options.Handler {
        // internal handlers
        case "meta", "api", "rpc", "micro":
        /// foo/bar/zool=> go.micro.api.foo method: Bar.Zool
            /// foo/bar=> go.micro.api.foo method: Foo.Bar
            name, method = apiRoute(req.URL.Path)
        default:
            //If handler is web, it will come here   
            ///foo/bar/zool => go.micro.api.foo  method bar/zool 
            // 1/foo/bar/ => go.micro.api.1.foo  method bar 
            method = req.Method
            name = proxyRoute(req.URL.Path)
        }

        return &resolver.Endpoint{
            Name:   name,
            Method: method,
        }, nil
    }



Plug-in unit

A plugin is defined in the code. However, this plugin is not a function of introducing components. It adds a layer to the pipeline of requests, which makes it easier to add new logic. However, what needs to be emphasized is the way in which micro introduces new components. Several important members of micro service have their corresponding interface specifications. As long as the interface is implemented correctly, new components can be easily accessed. Here is an example of introducing kubernetes as a registry.

The kubernetes component is located at github.com \ In microgo-plugins

    go get -u github.com\micro\go-plugins\kubernetes


Because the dynamic loading function of java/c# package can not be implemented in go, the language itself does not provide global type, function scanning. Often only through some tortuous ways to achieve. Microis, by importing packages into the entry file, uses init functions to write functional components into a map at startup. Add plug-ins to import

    _ "github.com/micro/go-plugins/registry/kubernetes"


   micro api --registry=kubernetes  --registry_address=yourAddress


Working principle: At github.com \ There is a map in micro go-micro config cmd cmd. go to save all registry creation functions

	DefaultRegistries = map[string]func(...registry.Option) registry.Registry{
		"consul": consul.NewRegistry,
		"gossip": gossip.NewRegistry,
		"mdns":   mdns.NewRegistry,
		"memory": rmem.NewRegistry,
	}


kubernetes writes the corresponding creation function in the package init

    func init() {
        cmd.DefaultRegistries["kubernetes"] = NewRegistry
    }


Effective Position

    if name := ctx.String("registry"); len(name) > 0 && (*c.opts.Registry).String() != name {
		r, ok := c.opts.Registries[name]
		if !ok {
			return fmt.Errorf("Registry %s not found", name)
		}

		*c.opts.Registry = r()
		serverOpts = append(serverOpts, server.Registry(*c.opts.Registry))
		clientOpts = append(clientOpts, client.Registry(*c.opts.Registry))

		if err := (*c.opts.Selector).Init(selector.Registry(*c.opts.Registry)); err != nil {
			log.Fatalf("Error configuring registry: %v", err)
		}

		clientOpts = append(clientOpts, client.Selector(*c.opts.Selector))

		if err := (*c.opts.Broker).Init(broker.Registry(*c.opts.Registry)); err != nil {
			log.Fatalf("Error configuring broker: %v", err)
		}
	}


Finally, a diagram is attached to illustrate the reference relationship of this core object. The component reference only draws the registry. Broker and Server are similar principles.

Posted by RedMist on Sat, 20 Jul 2019 07:32:03 -0700