依赖分为两种,本地的lib依赖,远程的服务依赖。java
本地的依赖实际上是很复杂的问题。从操做系统的apt-get,到各类语言的pip, npm。包管理是无穷无尽的问题。可是全部的本地依赖已经被docker终结了。不管是依赖了什么,所有给你打包起来,从操做系统开始。除了你依赖的cpu指令集无法给你打包成镜像了,其余都给打包了。node
docker以后,依赖问题就只剩远程服务依赖的问题。这个问题就是服务注册发现与调度须要解决的问题。从软件工程的角度来讲,全部的解耦问题均可以经过抽取lib的方式解决。lib也能够实现独立的发布周期,良好定义的IDL接口。因此若是非必要,请不要把lib依赖升级成网络服务依赖的角度。除非是从非功能性需求的角度,好比独立的扩缩容,支持scale out这些。不少时候微服务是由于基于lib的工具链支持不全,使得你们义无反顾地走上了拆分网络服务的不归路。mysql
服务名又称之为Service Qualifier,是一我的类可理解的英文标识。所谓的服务注册和发现就是在一个Service Qualifier下注册一堆Endpoint。一个Endpoint就是一个ip+端口的网络服务。就是一个很是相似DNS的名字服务,其实DNS自己就能够作服务的注册和发现,用SRV类型记录。nginx
名字服务的存在乎义是简化服务的使用方,也就是主调方。过去在使用方的代码里须要填入一堆ip加端口的配置,如今有了名字服务就能够只填一个服务名,实际在运行时用服务名找到那一堆endpoint。git
从名字服务的角度来说并不比DNS要强多少。可能也就是经过“服务发现的lib”帮你把ip和端口都得到了。而DNS默认lib(也就是libc的getHostByName)只支持host获取,并不能得到port。固然既然你都外挂了一个服务发现的lib了,和libc作对比也就优点公平了。github
lib提供的接口相似golang
$endpoints = listServiceEnpoints('redis'); echo($endpoints[0]['ip]);
甚至能够直接提供拼接url的接口web
$url = getServiceUrl('order', '/newOrder'); # http://xxx:yyy/newOrder
传统DNS的服务发现机制是缓存加上TTL过时时间,新的endpoint要传播到使用方须要各级缓存的刷新。并且即使endpoint没有更新,由于TTL到期了也要去上游刷新。为了减小网络间定时刷新endpoint的流量,通常TTL都设得比较长。redis
而另一个极端是gossip协议。全部人链接到全部人。一个服务的endpoint注册了,能够经过gossip协议很快广播到所有的节点上去。可是gossip的缺点是不基于订阅的。不管我是否是使用这个服务,我都会被动地被gossip这个服务的endpoint。这样就形成了无谓的网络间带宽的开销。算法
比较理想的更新方式是基于订阅的。若是业务对某个服务进行了发现,那么缓存服务器就保持一个订阅关系得到最新的endpoint。这样能够比定时刷新更及时,也消耗更小。这个方面要黑一下etcd 2.0,它的基于http链接的watch方案要求每一个watch独占一个tcp链接,严重限制了watch的数量。而etcd 3.0基于gRPC的实现就修复了这个问题。而consul的msgpack rpc从一开始就是复用tcp链接的。
图中的observer是相似的zookeeper的observer角色,是为了帮权威服务器分担watch压力的存在。也就是说服务发现的核心实际上是一个基于订阅的层级消息网络。服务注册和发现并不承诺任何的一致性,它只是尽力地进行分发,并不保证全部的节点对一个服务的endpoint是哪些有一致的view,由于这并无价值。由于一个qualifier下的多个endpoint by design 就是等价的,只要有足够的endpint可以承担负载,对于abc三个endpoint具体是让ab可见,仍是bc可见,并没有任何影响。
DNS的方案是在每台机器上装一个dnsmasq作为缓存服务器。服务发现也是相似的,在每台机器上有一个agent进程。若是dnsmasq挂了,dns域名就会解析失败,这样的可用性是不够的。服务发现的agent会把服务的配置和endpoint dump一份成本机的文件,服务发现的lib在没法访问agent的时候会降级去读取本机的文件,从而保证足够的可用性。固然你要愿意搞什么共享内存,也没人阻拦。
没法实现对dns服务器的降级。由于哪怕是降级到 /etc/hosts 的实现,其一个巨大的缺陷是 /etc/hosts 对于一个域名只能填一个ip,没法知足扩展性。而若是这一个ip填的是代理服务器的话,则失去了作服务发现的意义,都有代理了那就让代理去发现服务好了。
更进一步,不少基于zk的方案是把服务发现的agent和业务进程作到一个进程里去了。因此就不须要担忧外挂的进程是否还存活的问题了。
这点上和DNS是相似的。理论来讲ttl设置为0的DNS服务器也能够起到负载均衡的做用。经过把权重分发到服务发现的agent上,可让业务“每次发现”的endpoint都不同,从而达到均衡负载的做用。权重的实现经过简单的随机算法就能够实现。
经过软负载均衡理论上能够实现小流量,灰度地让一个新的endpoint加入集群。也能够实现某一些endpoint承担更大的调用量,以达到在线压测的目的。
不要小瞧了这么一点调权的功能。可以中央调度,智能调度流量,是很是有用的。
故障检测实际上是好作的。无非就是一个qualifier下挂了不少个endpoint,根据某种探活机制摘掉其中已经没法提供正常服务的endpoint。摘除最好是软摘除,这样不会出现一个闪失把全部endpoint全摘掉的问题。好比zookeeper的临时节点就是硬摘除,不可取。
在业务拿到endpoint以后,作完了rpc能够知道这个endpoint是否可用。这个时候对endpoint的健康状态本地作一个投票累积。若是endpoint连续不可用则标记为故障,被临时摘除。过一段时间以后再从新放出小黑屋,进行探活。这个过程和nginx对upstream的被动探活是很是相似的。
被动探活的好处是很是敏感并且真实可信(不可用就是我不能调你,就是不可用),本地投票完了当即就能够断定故障。缺陷是每一个主调方都须要独立去进行重复的断定。对于故障的endpoint,为了探活其是否存活须要以latency作为代价。
被动探活不会和具体的rpc机制绑定。不管是http仍是thrift,不管是redis仍是mysql,只要是网络调用均可以经过rpc后投票的方式实现被动探活。
主动探活比较难作,并且效果也未必好:
全部的主动探活的问题都在于须要指定如何去探测。不是tcp链接得上就算是能提供服务的。
主动探活受到网络路由的影响,a能够访问b,并不带表c也能够访问b
主动探测带来额外的网络开销,探测不能过于频繁
主动探测的发起者过少则容易对发起者产生很大的探活压力,须要很高的性能
consul 的本机主动探活是一个颇有意思的组合。避免了主动探活的一些缺点,能够是被动探活的一些补充。
不管是zookeeper那样一来tcp链接的心跳(tcp链接的保持其实也是定时ttl发ip包保持的)。仍是etcd,consul支持的基于ttl的心跳。都是相似的。
改进版本的心跳。减小总体的网络间通讯量。
服务endpoint注册比endpoint摘除要可贵多。
无状态服务的注册没有任何约束。无论是中央管理服务注册表,用web界面注册。仍是和部署系统联动,在进程启动时自动注册均可以作。
有状态服务,好比redis的某个分片的master。其有两个约束:
一致性:同一个分片不能有两个master
可用性:分片不能没有master,当master挂了,要自发选举出新的master
除非是在数据层协议上作ack(paxos,raft)或者协议自己支持冲突解决(crdt),不然基于服务注册来实现的分布式要么牺牲一致性,要么牺牲可用性。
有状态服务的注册需求,和普通的注册发现需求是本质不一样的。有状态服务须要的是一个一致性决策机制,在consistency和availability之间取平衡。这个机制能够是外挂一个zookeeper,也能够是集群的数据节点自身作一个gossip的投票机制。
而普通的注册和发现就是要给广播渠道,提供visibility。尽量地让endpoint曝光到其使用方那。不一样的问题须要的解决方案是不一样的。对于有状态服务的注册表须要很是可靠的故障检测机制,不能随意摘除master。而用于广播的服务注册表则很随意,故障检测机制也能够作到尽量错杀三千不放过一个。广播的机制须要解决的问题是大集群,怎么让服务可见。而数据节点的选主要解决的是相对小的集群,怎么保持一致地状况下尽可能可用。拿zookeeper的临时节点这样的机制放在大集群背景下,去作无状态节点探活就是技术用错了地方。
好比kafka,其有状态服务部分的注册和发现是用zookeeper实现的。而无状态服务的注册与发现是用data node自身提供集群的metadata来实现的。也就是消费者和生产者是不须要从zookeeper里去集群分片信息的(也就是服务注册表),而是从data node拿。这个时候data node其是充当了一个服务发现的agent的做用。若是不用data node干这个活,咱们把data node的内容放到DNS里去,其实也是能够work的。只是这些存储的给业务使用的客户端lib已经把这些逻辑写好了,没有人会去修改这个默认行为了。
可是广播用途的服务注册和发现,好比DNS不是只提供visibility而不能保证任何consistency吗?那我读到分片信息是旧的,把slave当master用了怎么办呢?全部作得好的存储分片选主方案,在data node上本身是知道本身的角色的。若是你使用错了,像redis cluster会回一个move指令,至关于http 302让你去别的地方作这个操做。kafka也是相似的。
libc只支持getHostByName,任何更高级的服务发现都须要挖空心思想怎么简化接入。反正操做系统和语言自身的工具链上是没有标准的支持的。每一个公司都有一套本身的玩法。行业严重缺少标准。
不管哪一种方式都是要修改业务代码的。即使是用proxy方式接入,业务代码里也得写死固定的proxy ip才行。从可读性的角度来讲,固定proxy ip的可读性是最差的,而用服务名或者域名是可读性最好的。
最笨拙的方法,也是最保险的。业务代码直接写服务名,得到endpoint。
探活也就是硬改各类rpc的lib,在调用后面加上投票的代码。
外挂式的服务发现。在配置文件中写变量引用服务,运行时把endpoint取出来生成并替换。大部分通用工具都是这么实现,好比consul-template。可是维护模板配置文件是很大的一个负担。
由于全部的语言基本上都支持DNS域名解析。利用这一层的接口,用钩子换掉lib的实际实现。业务代码里写域名,端口固定。
socket的钩子要难作得多,并且仅仅tcp4层探活也是不够的(http 500了每每也要认为对方是挂了的)。
实际上考虑golang这种没有libc的,java这种本身缓存域名结果的,钩子的方案其实没有想得那么美好。
proxy实际上是一种简化服务发现接入方式的手段。业务能够不用知道服务名,而是使用固定的ip和端口访问。由proxy去作服务发现,把请求转给对方。
http的proxy也很成熟,在proxy里对rpc结果进行跳票也有现成的工具(好比nginx)。不少公司都是这种本地proxy的架构,好比airbnb,yelp,eleme,uber。当用lib方式接业务接不动的时候,你们都会往这条路上转的。
远程proxy的缺陷是固定ip致使了路由是固定的。这条路由上的全部路由器和交换机都是故障点。没法作到多条网络路由冗余容错。并且须要用lvs作虚ip,也引入了运维成本。
并且远程proxy没法支持分区部署多套环境。除非引入bgp anycast这样妖孽的实现。让同一个ip在不一样的idc里路由到不一样的服务器。
国内大部分的网游都是分区分服的。这种架构就是一种简化的存储层数据分片。存储层的数据分片通常都作得很是完善,能够作到key级别的搬迁(当你访问key的时候告诉你我能够响应,仍是告诉你搬迁到哪里去了),能够作到访问错了shard告诉你正确的shard在哪里。而分区部署每每是没有这么完善的。
因此为了支持分区部署。每每是给不一样分区的服务区不一样的服务名。好比模块叫 chat,那么给hb_set(华北大区)的chat模块就命名为hb_set.chat,给hn_set(华南大区)的chat模块就命名为hn_set.chat。当时若是咱们是gamesvr模块,须要访问chat模块,代码都是同一份,我怎么知道应该访问hn_set.chat仍是hb_set.chat呢?这个就须要让gamesvr先知道本身所在的set,而后去访问同set下的其余模块。
again,这种分法也就是由于分区部署作为一个大的组合系统无法像一个孤立地存储作得那么好。像kafka的broker,哪怕你访问的不是它的本地分片,它能够帮你去作proxy链接到正确的分片上。而咱们无法要求一个组合出来的业务系统也作到这么完备地程度。因此凑合着用吧。
可是这种分法也有问题。有一些模块若是不是分区的,是全局的怎么办?这个时候服务发现就得起一个路由表的做用,把不一样分区的服务经过路由串起来。