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.