Service discovery

Service discovery is the core of microservice development. When service A needs to talk to service B, it needs the location of the service. The Registry component in Go-Micro defines A set of interfaces to realize service discovery. The default discovery mechanism is multicast DNS (MDNS), and ETCD is usually used in real projects. Consul, Zookeeper, etc. to implement service discovery capabilities, of course, the current cloud native capabilities also have service discovery capabilities.

Service discovery Interface

// The registry provides an interface for service discovery // and an abstraction over varying implementations // {consul, etcd, zookeeper, ... } type Registry interface { Init(... Option) error Options() Options Register(*Service, ... RegisterOption) error Deregister(*Service, ... DeregisterOption) error GetService(string, ... GetOption) ([]*Service, error) ListServices(... ListOption) ([]*Service, error) Watch(... WatchOption) (Watcher, error) String() string }Copy the code

The reason why GO-Micro is pluggable is that it defines a set of interfaces. As long as the interfaces are implemented, we can plug and remove the components we define to complete the corresponding functions.

Default service discovery MDNS

We have introduced MDNS in Micro. You can use MDNS as a tool similar to Consul, but in a different way. Let’s learn how to implement service discovery capabilities using MDNS

// NewRegistry returns a new default registry which is mdns func NewRegistry(opts ... Option) Registry { return newRegistry(opts...) }Copy the code
func newRegistry(opts ... Option) Registry { options := Options{ Context: context.Background(), Timeout: time.Millisecond * 100, } for _, o := range opts { o(&options) } // set the domain domain := mdnsDomain d, ok := options.Context.Value("mdns.domain").(string) if ok { domain = d } return &mdnsRegistry{ opts: options, domain: domain, services: make(map[string][]*mdnsEntry), watchers: make(map[string]*mdnsWatcher), } }Copy the code

MdnsRegistry is an instance of a registry whose prototype is:

type mdnsRegistry struct { opts Options // the mdns domain domain string sync.Mutex services map[string][]*mdnsEntry mtx  sync.RWMutex // watchers watchers map[string]*mdnsWatcher // listener listener chan *mdns.ServiceEntry }Copy the code

Let’s walk through the Registry interface methods briefly

  • The Init method

Add the option option to the OPTS of the mdnsRegisty instance.

  • The Register method

Register a service with a registry,

func (m *mdnsRegistry) Register(service *Service, opts ... RegisterOption) error { m.Lock() defer m.Unlock() entries, ok := m.services[service.Name] // first entry, create wildcard used for list queries if ! ok { s, err := mdns.NewMDNSService( service.Name, "_services", m.domain+".", "", 9999, [] net.ip {net.parseip ("0.0.0.0")}, nil,) if err! = nil { return err } srv, err := mdns.NewServer(&mdns.Config{Zone: &mdns.DNSSDService{MDNSService: s}}) if err ! = nil { return err } // append the wildcard entry entries = append(entries, &mdnsEntry{id: "*", node: srv}) } var gerr error for _, node := range service.Nodes { var seen bool var e *mdnsEntry for _, entry := range entries { if node.Id == entry.id { seen = true e = entry break } } // already registered, continue if seen { continue // doesn't exist } else { e = &mdnsEntry{} } txt, err := encode(&mdnsTxt{ Service: service.Name, Version: service.Version, Endpoints: service.Endpoints, Metadata: node.Metadata, }) if err ! = nil { gerr = err continue } host, pt, err := net.SplitHostPort(node.Address) if err ! = nil { gerr = err continue } port, _ := strconv.Atoi(pt) if logger.V(logger.DebugLevel, logger.DefaultLogger) { logger.Debugf("[mdns] registry create new service with ip: %s for: %s", net.ParseIP(host).String(), host) } // we got here, new node s, err := mdns.NewMDNSService( node.Id, service.Name, m.domain+".", "", port, []net.IP{net.ParseIP(host)}, txt, ) if err ! = nil { gerr = err continue } srv, err := mdns.NewServer(&mdns.Config{Zone: s, LocalhostChecking: true}) if err ! = nil { gerr = err continue } e.id = node.Id e.node = srv entries = append(entries, e) } // save m.services[service.Name] = entries return gerr }Copy the code

According to the name of the parameter service, check whether the corresponding service exists in the Services dictionary of mdnsRegistry, if not, create a New one, and finally implement a mdnsEntry instance, and then put it in the entries list. The purpose of this list is to compare an mdnsEntry instance in the entries list when traversing the node list of the service to see if the node ID and mdnsEntry ID are consistent. If they are, the service has been registered and there is no need to register them again. Otherwise, implement a new mdnsEntry instance and place it in the Services dictionary of the registry.

  • GetService and ListServices

The two methods are to query ServiceEntry in the entries pipeline in real time through QueryParam in MDNS, compare the information after getting it, and put the matched services in the list and return it to the caller.

  • Watch

Implement the listener, responsible for service changes to the registry, by instantiating an mdnsWatcher and placing it in watchs of the mdnsRegistry instance.

If the mdnsRegistry listener does not exist, start a listener

In the watch method, for keeps listening on the mdns.ServiceEntry pipe, and if there is a new ServiceEntry, it broadcasts it to all the Watcher in mdnsRegistry.

As we know, the Micro toolset uses cli as the command line tool. The details of this tool, as I mentioned in the previous article, just know that when we Run an App, when we call app.run, the corresponding action will be executed according to the user’s different options. In addition, Before and After the action is executed, actions are performed based on whether there are Before and After methods.

In the Before method, all of the initialization is done, and the registry section initializes the default DefaultRegistry based on the registration option

if err := muregistry.DefaultRegistry.Init(registryOpts...) ; err ! = nil { logger.Fatalf("Error configuring registry: %v", err) }Copy the code

In action, the subcommand is first checked to see if it exists. If it does, the subcommand is run directly, and if not, lookupService gets the specified list of services. Once found, the corresponding service can be called callService.

LookupService calls serviceWithName to find the corresponding service, which serviceWithName is

// find a service in a domain matching the name func serviceWithName(name, domain string) (*goregistry.Service, error) { srvs, err := registry.DefaultRegistry.GetService(name, goregistry.GetDomain(domain)) if err == goregistry.ErrNotFound { return nil, nil } else if err ! = nil { return nil, err } if len(srvs) == 0 { return nil, nil } return srvs[0], nil }Copy the code