我们定义好的服务,如何进行方便的使用,而不是直接指定特定的地址进行调用。这就需要使用额外的服务发现组件,在产线上,可能大家比较熟悉的有etcd
,consul
,zookeeper
,这些都是功能比较完备的服务发现,各有各的优势和特点,但是在micro
默认的服务发现中,通常使用的是mdns
.大家可以从在上一篇文章中讲的micro server
命令的输出中,看到它
2020-09-2702:52:28file=grpc/grpc.go:732level=infoRegistry[mdns]Registeringnode:server-bf8d0391-bd46-4aec-8455-3ea686463aee
那么什么是mdns?以及他是如何被作为服务发现的呢?
在计算机网络中,多播DNS (mDNS)协议在不包括本地名称服务器的小网络中将主机名解析为IP地址。它是一个零配置服务,使用与单播域名系统(DNS)基本相同的编程接口、包格式和操作语义。虽然将mDNS设计成一个独立的协议,但它可以与标准的DNS服务器协同工作。
mDNS协议发布为RFC 6762,使用IP多播用户数据报协议(UDP)包,由Apple Bonjour和开源的Avahi软件包实现,大多数Linux发行版都包含这些软件包。在Windows 10中也实现了mDNS,最初仅限于发现联网打印机,后来也能够解析主机名。
协议简介:当mDNS客户机需要解析主机名时,它发送一个IP多播查询消息,要求具有该名称的主机标识自己。目标机器然后多播包含其IP地址的消息。然后,该子网中的所有机器都可以使用这些信息来更新它们的mDNS缓存。任何主机都可以通过发送一个活时间(TTL)等于零的响应包来放弃对某个名称的声明。默认情况下,mDNS只解析以.local
顶级域名结尾的主机名。
如果.local
包含不实现mDNS但可以通过传统的单播DNS服务器找到的主机,则会导致问题。解决此类冲突需要更改网络配置,而mDNS旨在避免这种更改。
包结构:mDNS消息是使用以下地址发送的多播UDP包
IPv4 address 224.0.0.251 or IPv6 address ff02::fb
UDP port 5353
当使用以太网帧时,标准的IP多播MAC地址01:00:5E:00:00:FB (IPv4)或33:33:00:00:FB (IPv6)
有效负载结构基于单播DNS包格式,由头和数据两部分组成。
header与单播DNS中的相同,数据部分中的子部分也是如此:查询、答案,权威名称服务器和其他记录。每个子节中的记录数与标题中对应的*COUNT
字段的值相匹配。
queries:
查询部分中记录的有线格式比单播DNS中的略微修改,增加了UNICAST-RESPONSE
字段。
|Field|Description|Lengthbits||-----|---------|--------|-----||QNAME|Nameofthenodetowhichthequerypertains|Variable||QTYPE|Thetypeofthequery,i.e.thetypeofRRwhichshouldbereturnedinresponses.|16|UNICAST-RESPONSE|Booleanflagindicatingwhetheraunicast-responseisdesired|1|QCLASS|Classcode,1a.k.a."IN"fortheInternetandIPnetworks|15
与单播DNS中一样,QNAME字段由一系列称为“标签”的长度/值子字段组成。 每个标签代表完全限定域名(FQDN)中点分隔的子字符串之一。该列表以单个空字节终止,表示DNS的“根”。UNICAST-RESPONSE
字段用于最小化网络上不必要的广播.如果设置了位,应答者应该直接向查询节点发送定向单播响应,而不是向整个网络广播响应。QCLASS
字段与单播dns中的相同。
答案,权威名称服务器和其他记录部分中的所有记录都具有相同的格式,统称为资源记录(RR)。与单播DNS相比,mDNS中的资源记录的一般格式也略有修改
|Field|Description|Lengthbits||-----|---------|--------|-----||RRNAME|Nameofthenodetowhichtherecordpertains|Variable||RRTYPE|ThetypeoftheResourceRecord|16||CACHE-FLUSH|Booleanflagindicatingwhetheroutdatedcachedrecordsshouldbepurged|1||RRCLASS|Classcode,1a.k.a."IN"fortheInternetandIPnetworks|15||TTL|Timeinterval(inseconds)thattheRRshouldbecached|32|RDLENGTH|Integerrepresentingthelength(inoctets)oftheRDATAfield|16|RDATA|Resourcedata;internalstructurevariesbyRRTYPE|Variable|
CACHE-FLUSH
位用于指示邻居节点记录应覆盖此RRNAME
和RRTYPE
的任何现有缓存条目,而不是附加到该条目。RDATA
字段的格式与单播DNS中的格式相同。然而,DNS服务发现(dn-sd
), mDNS最常见的用例,指定了对其某些格式(特别是TXT记录)的轻微修改。
go-micro
包中,提供了一个套默认的mDNS实现,
//Zone是一个接口,用于与服务器集成并动态提供记录的接口typeZoneinterface{//Records在一个DNS请求中返回DNS记录Records(qdns.Question)[]dns.RR}//MDNSService通过实现Zone接口用于导出命名服务typeMDNSServicestruct{Instancestring//实例名称(e.g."hostServicename")Servicestring//服务名称(e.g."_http._tcp.")Domainstring//如果为空,假设为"local"HostNamestring//主机的DNS名(e.g."mymachine.net.")Portint//服务端口IPs[]net.IP//服务的主机IP地址列表TXT[]string//ServiceTXTrecordsTTLuint32serviceAddrstring//完全限定的服务地址instanceAddrstring//完全限定的实例地址enumAddrstring//_services._dns-sd._udp.<domain>}
接着看看DNSSDService
,DNSSDService
是一项符合DNS-SD(RFC 6762)和MDNS(RFC 6762)规范的服务,用于基于本地、多播DNS的发现。DNSSDService
实现了Zone
接口并包裹了一个MDNSService
实例。
typeDNSSDServicestruct{MDNSService*MDNSService}//Records返回一个DNS记录用于响应DNS请求。这个函数返回底层MDNSService实例的DNS响应。//在请求`_services._dns-sd._udp.<Domain>`时他还返回一个PTR记录,用于浏览底层的MDNSService实例func(s*DNSSDService)Records(qdns.Question)[]dns.RR{varrecs[]dns.RRifq.Name=="_services._dns-sd._udp."+s.MDNSService.Domain+"."{recs=s.dnssdMetaQueryRecords(q)}returnappend(recs,s.MDNSService.Records(q)...)}//dnssdMetaQueryRecords返回DNS记录在一次meta-query查询中,一个meta-query查询形如`_services._dns-sd._udp.<Domain>`func(s*DNSSDService)dnssdMetaQueryRecords(qdns.Question)[]dns.RR{//预期行为,就像RFC文档中描述的那样://...对于网络管理员来说,在网络上查找已播发服务类型的列表可能会很有用,//即使这些服务名称只是不透明的标识符,并且孤立地提供的信息也不多。////为了这个目的,一个指定的元查询据定义了,一个DNS查询PTR记录通常有一个名字//"_services._dns-sd._udp.<Domain>"将返回一系列PTR记录。//wheretherdataofeachPTRrecordisthetwo-abel//每个PTR记录的rdata都是一个双标签<Service>名称,加上相同的domain.例如`_http._tcp.<Domain>`//在PTRrdata中包含域可以在单播DNS响应中更好地进行名称压缩,//但是对于服务类型枚举而言,只有前两个标签是相关的。//然后,可以使用这两个标签的服务类型在此<Domain>或其他域中构造后续的服务实例枚举PTR查询,以发现该服务类型的实例。return[]dns.RR{&dns.PTR{Hdr:dns.RR_Header{Name:q.Name,Rrtype:dns.TypePTR,Class:dns.ClassINET,Ttl:defaultTTL,},Ptr:s.MDNSService.serviceAddr,},}}
为了部署一个符合DNS-SD
的mDNS服务,建议只注册被包裹的实例,例如
service:=&mdns.DNSSDService{MDNSService:&mdns.MDNSService{Instance:"MyFoobarService",Service:"_foobar._tcp",Port:8000,}}server,err:=mdns.NewServer(&mdns.Config{Zone:service})iferr!=nil{log.Fatalf("Errorcreatingserver:%v",err)}deferserver.Shutdown()
再来看看如何实现一个mDNS服务器的
//Config用于配置mDNS服务器。typeConfigstruct{//Zone必须提供支持查询响应ZoneZone//Iface如果提供了,绑定多播监听。如果没提供,使用系统默认的多播接口Iface*net.Interface//Port如果不是0,使用它代替5353Portint//GetMachineIP是一个函数,用于返回本地机器的IP地址GetMachineIPGetMachineIP//LocalhostChecking如果设置了,请求服务,并发送响应到0.0.0.0,如果目标IP是这个主机//如果机器位于VPN上,该VPN阻止非标准端口上的通信,则很有用LocalhostCheckingbool}//Server是一个mDNS服务器,用于监听mDNS查询并响应如果有一个存在的本地记录。typeServerstruct{config*Configipv4List*net.UDPConnipv6List*net.UDPConnshutdownboolshutdownChchanstruct{}shutdownLocksync.Mutexwgsync.WaitGroupoutboundIPnet.IP}//NewServer用于根据一个config创建一个mDNS服务器,然后再goroutine中开启监听funcNewServer(config*Config)(*Server,error){setCustomPort(config.Port)//Createthelisteners//Createwildcardconnections(because:5353canbealreadytakenbyotherapps)ipv4List,_:=net.ListenUDP("udp4",mdnsWildcardAddrIPv4)ipv6List,_:=net.ListenUDP("udp6",mdnsWildcardAddrIPv6)ifipv4List==nil&&ipv6List==nil{returnnil,fmt.Errorf("[ERR]mdns:Failedtobindtoanyudpport!")}ifipv4List==nil{ipv4List=&net.UDPConn{}}ifipv6List==nil{ipv6List=&net.UDPConn{}}//Joinmulticastgroupstoreceiveannouncementsp1:=ipv4.NewPacketConn(ipv4List)p2:=ipv6.NewPacketConn(ipv6List)p1.SetMulticastLoopback(true)p2.SetMulticastLoopback(true)ifconfig.Iface!=nil{iferr:=p1.JoinGroup(config.Iface,&net.UDPAddr{IP:mdnsGroupIPv4});err!=nil{returnnil,err}iferr:=p2.JoinGroup(config.Iface,&net.UDPAddr{IP:mdnsGroupIPv6});err!=nil{returnnil,err}}else{ifaces,err:=net.Interfaces()iferr!=nil{returnnil,err}errCount1,errCount2:=0,0for_,iface:=rangeifaces{iferr:=p1.JoinGroup(&iface,&net.UDPAddr{IP:mdnsGroupIPv4});err!=nil{errCount1++}iferr:=p2.JoinGroup(&iface,&net.UDPAddr{IP:mdnsGroupIPv6});err!=nil{errCount2++}}iflen(ifaces)==errCount1&&len(ifaces)==errCount2{returnnil,fmt.Errorf("Failedtojoinmulticastgrouponallinterfaces!")}}ipFunc:=getOutboundIPifconfig.GetMachineIP!=nil{ipFunc=config.GetMachineIP}s:=&Server{config:config,ipv4List:ipv4List,ipv6List:ipv6List,shutdownCh:make(chanstruct{}),outboundIP:ipFunc(),}gos.recv(s.ipv4List)gos.recv(s.ipv6List)s.wg.Add(1)//probe广播或者单播返回响应gos.probe()returns,nil}
实现了一个mDNS服务器之后,就可以进行服务查询了。那么mdns又是如何作为一个注册中心来作为服务发现使用的呢?
packageregistryimport("errors")const(//WildcardDomainindicatesanydomainWildcardDomain="*"//DefaultDomaintouseifnonewasprovidedinoptionsDefaultDomain="micro")var(//NotfounderrorwhenGetServiceiscalledErrNotFound=errors.New("servicenotfound")//WatcherstoppederrorwhenwatcherisstoppedErrWatcherStopped=errors.New("watcherstopped"))//注册中心提供了一个接口用于服务发现,以及给广泛实现({consul,etcd,zookeeper,...})提供了一个抽象typeRegistryinterface{Init(...Option)errorOptions()OptionsRegister(*Service,...RegisterOption)errorDeregister(*Service,...DeregisterOption)errorGetService(string,...GetOption)([]*Service,error)ListServices(...ListOption)([]*Service,error)Watch(...WatchOption)(Watcher,error)String()string}typeServicestruct{Namestring`json:"name"`Versionstring`json:"version"`Metadatamap[string]string`json:"metadata"`Endpoints[]*Endpoint`json:"endpoints"`Nodes[]*Node`json:"nodes"`}typeNodestruct{Idstring`json:"id"`Addressstring`json:"address"`Metadatamap[string]string`json:"metadata"`}typeEndpointstruct{Namestring`json:"name"`Request*Value`json:"request"`Response*Value`json:"response"`Metadatamap[string]string`json:"metadata"`}typeValuestruct{Namestring`json:"name"`Typestring`json:"type"`Values[]*Value`json:"values"`}typeOptionfunc(*Options)typeRegisterOptionfunc(*RegisterOptions)typeWatchOptionfunc(*WatchOptions)typeDeregisterOptionfunc(*DeregisterOptions)typeGetOptionfunc(*GetOptions)typeListOptionfunc(*ListOptions)
监控功能接口
packageregistryimport"time"//Watcher是一个接口类型,可以返回注册中心中服务的变更typeWatcherinterface{//Next是一个阻塞调用Next()(*Result,error)Stop()}//Result是调用watcher上Next方法的返回值。action可以是创建,更新,删除typeResultstruct{ActionstringService*Service}//EventType定义了注册中心的事件类型typeEventTypeintconst(//Create事件是在注册新服务时发生的CreateEventType=iota//Delete事件是在取消注册时发生的Delete//Update事件是在服务发生变更时发生的Update)//String返回事件类型的字符串形式func(tEventType)String()string{switcht{caseCreate:return"create"caseDelete:return"delete"caseUpdate:return"update"default:return"unknown"}}//Event是注册中心事件typeEventstruct{//IdisregistryidIdstring//TypedefinestypeofeventTypeEventType//TimestampiseventtimestampTimestamptime.Time//ServiceisregistryserviceService*Service}
我们看看,micro server
是如何一步步的使用mdns
的作为注册中心的。我们知道,micro server
默认使用的服务器是grpc Server
.
funcnewGRPCServer(opts...server.Option)server.Server{options:=newOptions(opts...)//createagrpcserversrv:=&grpcServer{opts:options,rpc:&rServer{serviceMap:make(map[string]*service),},handlers:make(map[string]server.Handler),subscribers:make(map[*subscriber][]broker.Subscriber),exit:make(chanchanerror),wg:wait(options.Context),}//configurethegrpcserversrv.configure()returnsrv}
而grpc server
在创建Server
时,创建的选项就默认使用了我们的mdns
.
|Field|Description|Lengthbits||-----|---------|--------|-----||QNAME|Nameofthenodetowhichthequerypertains|Variable||QTYPE|Thetypeofthequery,i.e.thetypeofRRwhichshouldbereturnedinresponses.|16|UNICAST-RESPONSE|Booleanflagindicatingwhetheraunicast-responseisdesired|1|QCLASS|Classcode,1a.k.a."IN"fortheInternetandIPnetworks|150
mdns.NewRegistry
方法返回的就是默认的注册中心。
|Field|Description|Lengthbits||-----|---------|--------|-----||QNAME|Nameofthenodetowhichthequerypertains|Variable||QTYPE|Thetypeofthequery,i.e.thetypeofRRwhichshouldbereturnedinresponses.|16|UNICAST-RESPONSE|Booleanflagindicatingwhetheraunicast-responseisdesired|1|QCLASS|Classcode,1a.k.a."IN"fortheInternetandIPnetworks|151
默认的注册中心的Init
方法主要用途是进行选项的赋值。将参数附加到注册中心的选项集合上。
|Field|Description|Lengthbits||-----|---------|--------|-----||QNAME|Nameofthenodetowhichthequerypertains|Variable||QTYPE|Thetypeofthequery,i.e.thetypeofRRwhichshouldbereturnedinresponses.|16|UNICAST-RESPONSE|Booleanflagindicatingwhetheraunicast-responseisdesired|1|QCLASS|Classcode,1a.k.a."IN"fortheInternetandIPnetworks|152
而真正的注册动作为Register
,该方法不仅将服务注册到指定的域名,而且还会将其注册到全局的域中,用于查询使用。如果在注册选项中没有给出Domain,那么默认的domian就是micro
.
|Field|Description|Lengthbits||-----|---------|--------|-----||QNAME|Nameofthenodetowhichthequerypertains|Variable||QTYPE|Thetypeofthequery,i.e.thetypeofRRwhichshouldbereturnedinresponses.|16|UNICAST-RESPONSE|Booleanflagindicatingwhetheraunicast-responseisdesired|1|QCLASS|Classcode,1a.k.a."IN"fortheInternetandIPnetworks|153
这个方法的内容比较多,为了更详细的理解其中的内容,我们分拆其中的功能,一步一步的看,首先
createMDNSEntries
通过提供的域名,以及服务名,创建运行服务的节点条目。如果在m.domains
中已经存在对应的域名和服务名,则直接返回。
|Field|Description|Lengthbits||-----|---------|--------|-----||QNAME|Nameofthenodetowhichthequerypertains|Variable||QTYPE|Thetypeofthequery,i.e.thetypeofRRwhichshouldbereturnedinresponses.|16|UNICAST-RESPONSE|Booleanflagindicatingwhetheraunicast-responseisdesired|1|QCLASS|Classcode,1a.k.a."IN"fortheInternetandIPnetworks|154
创建完了服务名针对的节点条目之后,就可以进行注册了。如下方法是服务注册功能的方法。第一个参数为要注册的服务,第二个参数为该服务针对的服务节点条目列表,第三个为注册选项。
该方法首先进行service.Nodes
遍历来判断服务条目是否已经注册到每个node
里。如果在某个节点中并没有注册。那么就在没创建服务的节点上创建服务并追加到服务条目列表中
这一通操作就是为了将所有的节点上都创建了服务节点条目。
|Field|Description|Lengthbits||-----|---------|--------|-----||QNAME|Nameofthenodetowhichthequerypertains|Variable||QTYPE|Thetypeofthequery,i.e.thetypeofRRwhichshouldbereturnedinresponses.|16|UNICAST-RESPONSE|Booleanflagindicatingwhetheraunicast-responseisdesired|1|QCLASS|Classcode,1a.k.a."IN"fortheInternetandIPnetworks|155
在将服务注册到给定的域中后,还要将服务注册到全局的域中,以便可以查询,而createGlobalDomainService
就是用来创建全局的服务的。参数为创建的服务,以及注册选项。
|Field|Description|Lengthbits||-----|---------|--------|-----||QNAME|Nameofthenodetowhichthequerypertains|Variable||QTYPE|Thetypeofthequery,i.e.thetypeofRRwhichshouldbereturnedinresponses.|16|UNICAST-RESPONSE|Booleanflagindicatingwhetheraunicast-responseisdesired|1|QCLASS|Classcode,1a.k.a."IN"fortheInternetandIPnetworks|156
至此,我们看完了Register
的所有内容,Deregister
和Register
是一个逆过程。
|Field|Description|Lengthbits||-----|---------|--------|-----||QNAME|Nameofthenodetowhichthequerypertains|Variable||QTYPE|Thetypeofthequery,i.e.thetypeofRRwhichshouldbereturnedinresponses.|16|UNICAST-RESPONSE|Booleanflagindicatingwhetheraunicast-responseisdesired|1|QCLASS|Classcode,1a.k.a."IN"fortheInternetandIPnetworks|157
通过给定字符串参数,以及选项,获取对应的服务和选项。
|Field|Description|Lengthbits||-----|---------|--------|-----||QNAME|Nameofthenodetowhichthequerypertains|Variable||QTYPE|Thetypeofthequery,i.e.thetypeofRRwhichshouldbereturnedinresponses.|16|UNICAST-RESPONSE|Booleanflagindicatingwhetheraunicast-responseisdesired|1|QCLASS|Classcode,1a.k.a."IN"fortheInternetandIPnetworks|158
ListServices
用于查询所有的服务列表
最后一部分内容关于监控的部分。其中的Watch
方法返回的是一个实现了Watcher
接口的监控器mdnsWatcher
。
|Field|Description|Lengthbits||-----|---------|--------|-----||QNAME|Nameofthenodetowhichthequerypertains|Variable||QTYPE|Thetypeofthequery,i.e.thetypeofRRwhichshouldbereturnedinresponses.|16|UNICAST-RESPONSE|Booleanflagindicatingwhetheraunicast-responseisdesired|1|QCLASS|Classcode,1a.k.a."IN"fortheInternetandIPnetworks|159
mdnsWatcher
监控器的Stop方法很简单,就是将监听器从注册中心的监听器记录中移除。Next方法从注册中心中接收事件并解析。
|Field|Description|Lengthbits||-----|---------|--------|-----||RRNAME|Nameofthenodetowhichtherecordpertains|Variable||RRTYPE|ThetypeoftheResourceRecord|16||CACHE-FLUSH|Booleanflagindicatingwhetheroutdatedcachedrecordsshouldbepurged|1||RRCLASS|Classcode,1a.k.a."IN"fortheInternetandIPnetworks|15||TTL|Timeinterval(inseconds)thattheRRshouldbecached|32|RDLENGTH|Integerrepresentingthelength(inoctets)oftheRDATAfield|16|RDATA|Resourcedata;internalstructurevariesbyRRTYPE|Variable|0