服务发现
服务发现是微服务开发的核心。当服务 A 需要与服务 B 通话时,它需要该服务的位置,go-micro中的registry组件定义了一套实现服务发现的接口,默认发现机制是多播 DNS (mdns),通常在实际项目中一般是使用etcd,consul,zookeeper等来实现服务发现的能力,当然,现在的云原生能力也已经有服务发现的能力的。
服务发现接口
// 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
}
go-micro之所以说是可插拔的,就是定义了一套接口,只要实现接口,就可以插拔式的使用我们自定义的组件来完成相对应的功能。
默认的服务发现mdns
我们在micro中的mdns介绍过什么是mdns,你完全可以把它当成跟consul一样的工具,只是实现的方式不同罢了,下面我们就一起来通过Mdns来学习一下如何实现一套服务发现能力
// NewRegistry returns a new default registry which is mdns
func NewRegistry(opts ...Option) Registry {
return newRegistry(opts...)
}
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),
}
}
mdnsRegistry就是一个注册中心的实例,他的原型为:
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
}
针对Registry接口方法,我们简单一起过一遍
- Init方法
将option选项加到mdnsRegisty实例的opts上。
- Register方法
将服务注册到注册中心去,
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
}
首先根据参数服务的名称,在mdnsRegistry上的services字典中查看是否存在对应的服务,如果不存在就New一个,最终实现一个mdnsEntry实例,然后放到entries列表中。这个列表的用途是,在遍历服务的节点列表时,会比对entries列表中的某一个mdnsEntry实例,看看节点id和mdnsEntry的id是否一直,如果一致的话,说明该服务已经注册过了,不用重复注册了。否则就实现一个新的mdnsEntry实例,并将它放到注册中心的services字典中去。
- GetService和ListServices
这两个方法就是通过mdns中的QueryParam,实时的查询entries管道里的ServiceEntry,拿到后就进行信息比对,把匹配到的services放到列表中返回给调用方。
- Watch
实现监听者,负责注册中心的服务变更,具体实现是,实例化一个mdnsWatcher,然后把它放到mdnsRegistry实例的watchs里面。
如果mdnsRegistry的listener不存在,就启动一个listener
在watch方法了,for持续监听mdns.ServiceEntry的管道,如果有新的ServiceEntry就取出来广播给mdnsRegistry.watchers里面所有的watcher。
我们知道,Micro工具集使用的命令行工具是cli,这个工具的具体内容,我在之前的文章中有提到,你只要知道,当我们运行一个App的时候,调用app.Run的时候,针对用户不同的选项,会执行对应的action.不仅如此,在action执行前后,会根据是否有Before方法和After方法来进行对应的操作。
在Before方法里,会做各项初始化过程中,注册中心这块,会根据注册option来初始化默认的DefaultRegistry
if err := muregistry.DefaultRegistry.Init(registryOpts...); err != nil {
logger.Fatalf("Error configuring registry: %v", err)
}
在action里,首先会检查子命令是否存在,如果存在就直接运行子命令,如果不存在就会lookupService获取指定的服务列表。找到之后就可以调用相应的服务了callService。
lookupService里面调用serviceWithName来找到对应的服务,而serviceWithName就是
// 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
}