从负载均衡的这种模式下其实有两个主要的问题: 一是中心化,整个系统都基于负载均衡器,负载均衡就至关于整个业务的中心,虽然咱们能够经过一些高可用手段来保证,但其实内部流量一般是巨大的,很容易出现性能瓶颈 二是增长了一次TCP交互java
固然也有不少好处,好比能够作一些负载均衡、长连接维护、分布式跟踪等,这不是本文重点node
事件队列
中,后续每次客户端拉取只返回增量数据,这样服务端的忘了压力就会小不少
服务端注册表结构体Registry主要包含三部分信息: lock(读写锁)、apps(应用对应信息)、eventQueue(事件队列) Lock: 注册中心是典型的读多写少的应用,server端注册表可能同时提供给N个服务进行读取,因此这里采用读写锁 apps: 保存应用对应的信息, 其实后面写完发现,不必使用,只使用基础的map就能够搞定 eventQueue: 每次注册表变动都写入事件到里面windows
// Registry 注册表
type Registry struct {
lock sync.RWMutex
apps sync.Map
duration time.Duration
eventQueue *EventQueue
}
复制代码
// Registr 注册服务
func (r *Registry) Registr(name, node string) bool {
r.lock.Lock()
defer r.lock.Unlock()
app := r.getApp(name)
if app == nil {
app = NewApplication(name)
r.apps.Store(name, app)
}
if lease, ok := app.add(node, r.duration); ok {
r.eventQueue.Push(&Event{lease: lease, action: ADD})
return true
}
return false
}
复制代码
全量拉取经过all接口拉取全量的返回的是服务对应的节点切片 增量拉取经过details接口返回增量的变动事件和服务端注册表的hashcode缓存
// all 全量拉取
func (r *Registry) all() map[string][]string {
r.lock.RLock()
defer r.lock.RUnlock()
apps := make(map[string][]string)
r.apps.Range(func(k, v interface{}) bool {
name, app := k.(string), v.(*Application)
nodes := []string{}
for key := range app.Node {
nodes = append(nodes, key)
}
apps[name] = nodes
return true
})
return apps
}
// details 增量拉取
func (r *Registry) details() []*Event {
r.lock.RLock()
defer r.lock.RUnlock()
events := []*Event{}
for {
event := r.eventQueue.Pop()
if event == nil {
break
}
events = append(events, event)
}
return events
}
复制代码
hashcode是一个一致性的保证,eureka里面主要是经过拼接全部的服务名称和节点的个数来生成的一个字符串,这里咱们也采用这种方式,bash
func (r *Registry) hashCode() string {
r.lock.RLock()
defer r.lock.RUnlock()
hashCodes := []string{}
r.apps.Range(func(_, value interface{}) bool {
app := value.(*Application)
hashCodes = append(hashCodes, app.HashCode())
return true
})
sort.Sort(sort.StringSlice(hashCodes))
return strings.Join(hashCodes, "|")
}
复制代码
客户端本地注册表其实就比较简单了,只须要存储服务和节点的对应信息便可数据结构
// LocalRegistry 本地注册表
type LocalRegistry struct {
lock sync.RWMutex
apps map[string][]string
}
复制代码
func (c *Client) start() {
c.wg.Add(1)
c.registr()
c.poll()
go c.loop()
}
复制代码
func (c *Client) loop() {
timer := time.NewTimer(time.Second)
for {
// 从服务的拉取增量事件,details内部会直接应用,而后返回服务端返回的注册表的hashcode
respHashCode := c.details()
localHashCode := c.registry.hashCode()
// 若是发现本地和服务的的注册表的hashcode不一样,则全量拉取
if respHashCode != localHashCode {
fmt.Printf("client app %s node %s poll hashcode: %s\n", c.App, c.Name, respHashCode)
c.poll()
}
select {
case <-timer.C:
timer.Reset(time.Second)
case <-c.done:
c.wg.Done()
return
}
}
}
复制代码
func main() {
// 生成服务端
server := NewServer("aliyun", time.Second)
// 注册两个test服务的节点
clientOne := NewClient("test", "1.1.1.1:9090", server)
clientOne.start()
clientTwo := NewClient("test", "1.1.1.2:9090", server)
clientTwo.start()
// 注册两个hello服务的节点
clientThree := NewClient("hello", "1.1.1.3:9090", server)
clientThree.start()
clientFour := NewClient("hello", "1.1.1.4:9090", server)
clientFour.start()
time.Sleep(time.Second * 3)
// 验证每一个服务节点的注册表的hashcode是否一致
println(clientOne.details())
println(clientTwo.details())
println(clientThree.details())
println(clientFour.details())
println(clientTwo.details() == clientOne.details())
println(clientThree.details() == clientFour.details())
println(clientOne.details() == clientFour.details())
clientOne.stop()
clientTwo.stop()
clientThree.stop()
clientFour.stop()
}
复制代码
经过结果咱们能够看出,节点增量拉取了注册表,同时若是发现与本地的hashcode不一样就进行全量拉取,并最终达成一致架构
lr event add 1.1.1.3:9090 hello
lr event add 1.1.1.4:9090 hello
lr event add client app hello node 1.1.1.4:9090 poll hashcode: hello_2|test_2
1.1.1.1:9090 test
lr event add 1.1.1.2:9090 test
client app test node 1.1.1.1:9090 poll hashcode: hello_2|test_2
client app test node 1.1.1.2:9090 poll hashcode: hello_2|test_2
client app hello node 1.1.1.3:9090 poll hashcode: hello_2|test_2
hello_2|test_2
hello_2|test_2
hello_2|test_2
hello_2|test_2
true
true
true
复制代码
微服务注册中心注册表的这种实现机制,到这基本上就明白了,注册中心 经过增量、全量、hashcode三种机制来保证客户端与注册中心的注册表的同步app
其实一个工业级的注册中心仍是很麻烦的,好比注册表中那个事件队列,我如今的实现只有一个节点能获取增量,其余的都会经过hashcode来触发全量拉取,后续文章里面会相信介绍下,这块缓存和定时器来实现增量数据的打包负载均衡
其实在go里面你们注册中心都是基于etcd、consul直接watch去作的,基本上能够完成eureka服务的8/9十的功能,可是当须要与公司现有的java作集成,可能就须要eureaka这种注册中心了分布式
未完待续 关注公共号: 布衣码农
更多精彩内容能够查看www.sreguide.com