先理解一下gossip协议:在一个有界网络中,每个节点都随机地与其他节点通信,经过一番杂乱无章的通信,最终所有节点的状态都会达成一致。每个节点可能知道所有其他节点,也可能仅知道几个邻居节点,只要这些节可以通过网络连通,最终他们的状态都是一致的,当然这也是疫情传播的特点。
简单的描述下这个协议,首先要传播谣言就要有种子节点。种子节点每秒都会随机向其他节点发送自己所拥有的节点列表,以及需要传播的消息。任何新加入的节点,就在这种传播方式下很快地被全网所知道。这个协议的神奇就在于它从设计开始就没想到信息一定要传递给所有的节点,但是随着时间的增长,在最终的某一时刻,全网会得到相同的信息。当然这个时刻可能仅仅存在于理论,永远不可达。
这个算法已经有大神实现了,参见memberlist
我这里先通过一个demo演示一下,怎么使用
package main
import (
"encoding/json"
"flag"
"fmt"
"net/http"
"os"
"strings"
"sync"
"github.com/hashicorp/memberlist"
"github.com/pborman/uuid"
)
var (
mtx sync.RWMutex
members = flag.String("members", "", "comma seperated list of members")
port = flag.Int("port", 4001, "http port")
items = map[string]string{}
broadcasts *memberlist.TransmitLimitedQueue
)
type broadcast struct {
msg []byte
notify chan<- struct{}
}
type delegate struct{}
type update struct {
Action string // add, del
Data map[string]string
}
func init() {
flag.Parse()
}
func (b *broadcast) Invalidates(other memberlist.Broadcast) bool {
return false
}
func (b *broadcast) Message() []byte {
return b.msg
}
func (b *broadcast) Finished() {
if b.notify != nil {
close(b.notify)
}
}
func (d *delegate) NodeMeta(limit int) []byte {
return []byte{}
}
func (d *delegate) NotifyMsg(b []byte) {
if len(b) == 0 {
return
}
switch b[0] {
case 'd': // data
var updates []*update
if err := json.Unmarshal(b[1:], &updates); err != nil {
return
}
mtx.Lock()
for _, u := range updates {
for k, v := range u.Data {
switch u.Action {
case "add":
items[k] = v
case "del":
delete(items, k)
}
}
}
mtx.Unlock()
}
}
func (d *delegate) GetBroadcasts(overhead, limit int) [][]byte {
return broadcasts.GetBroadcasts(overhead, limit)
}
func (d *delegate) LocalState(join bool) []byte {
mtx.RLock()
m := items
mtx.RUnlock()
b, _ := json.Marshal(m)
return b
}
func (d *delegate) MergeRemoteState(buf []byte, join bool) {
if len(buf) == 0 {
return
}
if !join {
return
}
var m map[string]string
if err := json.Unmarshal(buf, &m); err != nil {
return
}
mtx.Lock()
for k, v := range m {
items[k] = v
}
mtx.Unlock()
}
func addHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
key := r.Form.Get("key")
val := r.Form.Get("val")
mtx.Lock()
items[key] = val
mtx.Unlock()
b, err := json.Marshal([]*update{
&update{
Action: "add",
Data: map[string]string{
key: val,
},
},
})
if err != nil {
http.Error(w, err.Error(), 500)
return
}
//广播数据
broadcasts.QueueBroadcast(&broadcast{
msg: append([]byte("d"), b...),
notify: nil,
})
}
func delHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
key := r.Form.Get("key")
mtx.Lock()
delete(items, key)
mtx.Unlock()
b, err := json.Marshal([]*update{
&update{
Action: "del",
Data: map[string]string{
key: "",
},
},
})
if err != nil {
http.Error(w, err.Error(), 500)
return
}
broadcasts.QueueBroadcast(&broadcast{
msg: append([]byte("d"), b...),
notify: nil,
})
}
func getHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
key := r.Form.Get("key")
mtx.RLock()
val := items[key]
mtx.RUnlock()
w.Write([]byte(val))
}
func start() error {
hostname, _ := os.Hostname()
c := memberlist.DefaultLocalConfig()
c.Delegate = &delegate{}
c.BindPort = 0
c.Name = hostname + "-" + uuid.NewUUID().String()
//创建gossip网络
m, err := memberlist.Create(c)
if err != nil {
return err
}
//第一个节点没有member,但从第二个开始就有member了
if len(*members) > 0 {
parts := strings.Split(*members, ",")
_, err := m.Join(parts)
if err != nil {
return err
}
}
broadcasts = &memberlist.TransmitLimitedQueue{
NumNodes: func() int {
return m.NumMembers()
},
RetransmitMult: 3,
}
node := m.LocalNode()
fmt.Printf("Local member %s:%d\n", node.Addr, node.Port)
return nil
}
func main() {
if err := start(); err != nil {
fmt.Println(err)
}
http.HandleFunc("/add", addHandler)
http.HandleFunc("/del", delHandler)
http.HandleFunc("/get", getHandler)
fmt.Printf("Listening on :%d\n", *port)
if err := http.ListenAndServe(fmt.Sprintf(":%d", *port), nil); err != nil {
fmt.Println(err)
}
}
这里通过一个简单的http服务查询和插入数据,找两台机器,第一台执行
memberlist
会生成gossip监听的服务ip和端口
使用上面的ip和端口在第二台执行
memberlist --members=xxx.xxx.xxx.xxx:xxxx
那么一个gossip的网络就搭建完成了。
# add
curl "http://localhost:4001/add?key=foo&val=bar"
# get
curl "http://另一台机器:4001/get?key=foo"
# delete
curl "http://localhost:4001/del?key=foo"