├── .travis.yml ├── CHANGELOG ├── etcd_test.go ├── example ├── discovery_example │ └── service_discovery_example.go └── register_example │ └── service_register_example.go ├── ProcFile ├── LICENSE ├── README.md └── etcd.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.12.9 4 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | # Change log 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [0.0.1] - 2019-08-23 5 | ### Added 6 | - 发布 7 | -------------------------------------------------------------------------------- /etcd_test.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "testing" 5 | //"time" 6 | ) 7 | 8 | func TestRegister(t *testing.T) { 9 | serviceName := "s-test" 10 | serviceInfo := ServiceMeta{IP: "127.0.0.1", Ext: "test"} 11 | _, err := Register(serviceName, serviceInfo, []string{ 12 | "http://127.0.0.1:12379", 13 | "http://127.0.0.1:22379", 14 | "http://127.0.0.1:32379", 15 | }) 16 | 17 | if err != nil { 18 | t.Errorf("err:%s", err.Error()) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /example/discovery_example/service_discovery_example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/zheng-ji/discover-etcd" 6 | "log" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | 12 | m, err := etcd.OnWatch([]string{ 13 | "http://127.0.0.1:12379", 14 | "http://127.0.0.1:22379", 15 | "http://127.0.0.1:32379"}, "services/") 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | 20 | for { 21 | m.Nodes.Range(func(k, v interface{}) bool { 22 | fmt.Printf("node:%s, ip=%s ext=%s\n", k, v.(*etcd.Node).Meta.IP, v.(*etcd.Node).Meta.Ext) 23 | return true 24 | }) 25 | time.Sleep(time.Second * 5) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/register_example/service_register_example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/zheng-ji/discover-etcd" 6 | "log" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | 12 | serviceName := "s-test" 13 | serviceInfo := etcd.ServiceMeta{IP: "127.0.0.1", Ext: "test"} 14 | s, err := etcd.Register(serviceName, serviceInfo, []string{ 15 | "http://127.0.0.1:12379", 16 | "http://127.0.0.1:22379", 17 | "http://127.0.0.1:32379", 18 | }) 19 | 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | fmt.Printf("name:%s, ip:%s\n", s.Name, s.Meta.IP) 25 | 26 | go func() { 27 | time.Sleep(time.Second * 20) 28 | s.Stop() 29 | }() 30 | 31 | s.Start() 32 | } 33 | -------------------------------------------------------------------------------- /ProcFile: -------------------------------------------------------------------------------- 1 | # Use goreman to run `go get github.com/mattn/goreman` 2 | etcd1: ./etcd --name infra1 --listen-client-urls http://127.0.0.1:12379 --advertise-client-urls http://127.0.0.1:12379 --listen-peer-urls http://127.0.0.1:12380 --initial-advertise-peer-urls http://127.0.0.1:12380 --initial-cluster-token etcd-cluster-1 --initial-cluster 'infra1=http://127.0.0.1:12380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380' --initial-cluster-state new 3 | 4 | etcd2: ./etcd --name infra2 --listen-client-urls http://127.0.0.1:22379 --advertise-client-urls http://127.0.0.1:22379 --listen-peer-urls http://127.0.0.1:22380 --initial-advertise-peer-urls http://127.0.0.1:22380 --initial-cluster-token etcd-cluster-1 --initial-cluster 'infra1=http://127.0.0.1:12380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380' --initial-cluster-state new 5 | 6 | etcd3: ./etcd --name infra3 --listen-client-urls http://127.0.0.1:32379 --advertise-client-urls http://127.0.0.1:32379 --listen-peer-urls http://127.0.0.1:32380 --initial-advertise-peer-urls http://127.0.0.1:32380 --initial-cluster-token etcd-cluster-1 --initial-cluster 'infra1=http://127.0.0.1:12380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380' --initial-cluster-state new 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, zheng-ji All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 5 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the 6 | documentation and/or other materials provided with the distribution. 7 | 8 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 9 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 10 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 11 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 12 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 13 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Introduction 2 | ======= 3 | 4 | [![Build Status](https://travis-ci.org/zheng-ji/discover-etcd.svg)](https://travis-ci.org/zheng-ji/discover-etcd) 5 | 6 | Implement service discovery based on etcd By Go 7 | 8 | Quick Start Etcd 9 | ----------------- 10 | 11 | ``` 12 | $ wget https://github.com/coreos/etcd/releases/download/v3.1.5/etcd-v3.1.5-linux-amd64.tar.gz 13 | $ tar xzvf etcd-v3.1.5-linux-amd64.tar.gz 14 | $ mv etcd-v3.1.5-linux-amd64 /opt/etcd 15 | 16 | $ go get github.com/mattn/goreman 17 | $ goreman -f ProcFile (本仓库提供) start 18 | ``` 19 | 20 | 21 | Quick-start 22 | ----------------- 23 | 24 | ``` 25 | go get github.com/zheng-ji/discover-etcd 26 | ``` 27 | 28 | Example 29 | ---------------- 30 | 31 | * [Service Discovery Link](https://github.com/zheng-ji/discover-etcd/blob/master/example/discovery_example/service_discovery_example.go) 32 | * [Service Register Link](https://github.com/zheng-ji/discover-etcd/blob/master/example/register_example/service_register_example.go) 33 | 34 | ```go 35 | # 服务发现 36 | package main 37 | 38 | import ( 39 | "fmt" 40 | "github.com/zheng-ji/discover-etcd" 41 | "log" 42 | "time" 43 | ) 44 | 45 | func main() { 46 | m, err := etcd.OnWatch([]string{ 47 | "http://127.0.0.1:12379", 48 | "http://127.0.0.1:22379", 49 | "http://127.0.0.1:32379"}, "services/") 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | for { 55 | m.Nodes.Range(func(k, v interface{}) bool { 56 | fmt.Printf("node:%s, ip=%s ext=%s\n", 57 | k, v.(*etcd.Node).Meta.IP, v.(*etcd.Node).Meta.Ext) 58 | return true 59 | }) 60 | time.Sleep(time.Second * 5) 61 | } 62 | } 63 | ``` 64 | 65 | ```go 66 | # 服务注册 67 | package main 68 | 69 | import ( 70 | "fmt" 71 | "github.com/zheng-ji/discover-etcd" 72 | "log" 73 | "time" 74 | ) 75 | 76 | func main() { 77 | 78 | serviceName := "s-test" 79 | serviceInfo := etcd.ServiceMeta{IP: "127.0.0.1", Ext: "Your Custome Data"} 80 | s, err := etcd.Register(serviceName, serviceInfo, []string{ 81 | "http://127.0.0.1:12379", 82 | "http://127.0.0.1:22379", 83 | "http://127.0.0.1:32379", 84 | }) 85 | 86 | if err != nil { 87 | log.Fatal(err) 88 | } 89 | 90 | fmt.Printf("name:%s, ip:%s\n", s.Name, s.Meta.IP) 91 | 92 | go func() { 93 | time.Sleep(time.Second * 20) 94 | s.Stop() 95 | }() 96 | 97 | s.Start() 98 | } 99 | ``` 100 | -------------------------------------------------------------------------------- /etcd.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "go.etcd.io/etcd/clientv3" 8 | "log" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | type Master struct { 14 | Path string 15 | Nodes *sync.Map 16 | Client *clientv3.Client 17 | } 18 | 19 | //node is a client 20 | type Node struct { 21 | Key string 22 | Meta ServiceMeta 23 | } 24 | 25 | //the detail of service 26 | type ServiceMeta struct { 27 | IP string 28 | Ext interface{} 29 | } 30 | 31 | type Service struct { 32 | Name string 33 | Meta ServiceMeta 34 | stop chan error 35 | leaseid clientv3.LeaseID 36 | client *clientv3.Client 37 | } 38 | 39 | //func NewMaster(endpoints []string, watchPath string) (*Master, error) { 40 | // Monitor Nodes 41 | func OnWatch(endpoints []string, watchPath string) (*Master, error) { 42 | cli, err := clientv3.New(clientv3.Config{ 43 | Endpoints: endpoints, 44 | DialTimeout: time.Second, 45 | }) 46 | 47 | if err != nil { 48 | log.Fatal(err) 49 | return nil, err 50 | } 51 | 52 | master := &Master{ 53 | Path: watchPath, 54 | Nodes: new(sync.Map), 55 | Client: cli, 56 | } 57 | 58 | go master.WatchNodes() 59 | return master, err 60 | } 61 | 62 | func (m *Master) AddNode(key string, info *ServiceMeta) { 63 | 64 | node := &Node{ 65 | Key: key, 66 | Meta: *info, 67 | } 68 | 69 | m.Nodes.Store(node.Key, node) 70 | } 71 | 72 | func GetServiceMeta(ev *clientv3.Event) *ServiceMeta { 73 | info := &ServiceMeta{} 74 | err := json.Unmarshal([]byte(ev.Kv.Value), info) 75 | if err != nil { 76 | log.Println(err) 77 | } 78 | return info 79 | } 80 | 81 | func (m *Master) WatchNodes() { 82 | 83 | rch := m.Client.Watch(context.Background(), m.Path, clientv3.WithPrefix()) 84 | 85 | for wresp := range rch { 86 | 87 | for _, ev := range wresp.Events { 88 | 89 | switch ev.Type { 90 | case clientv3.EventTypePut: 91 | info := GetServiceMeta(ev) 92 | m.AddNode(string(ev.Kv.Key), info) 93 | 94 | case clientv3.EventTypeDelete: 95 | m.Nodes.Delete(string(ev.Kv.Key)) 96 | } 97 | } 98 | } 99 | } 100 | 101 | // Register Service 102 | func Register(name string, info ServiceMeta, endpoints []string) (*Service, error) { 103 | 104 | cli, err := clientv3.New(clientv3.Config{ 105 | Endpoints: endpoints, 106 | DialTimeout: 2 * time.Second, 107 | }) 108 | 109 | if err != nil { 110 | log.Fatal(err) 111 | return nil, err 112 | } 113 | 114 | return &Service{ 115 | Name: name, 116 | Meta: info, 117 | stop: make(chan error), 118 | client: cli, 119 | }, err 120 | } 121 | 122 | func (s *Service) Start() error { 123 | 124 | ch, err := s.keepAlive() 125 | if err != nil { 126 | log.Fatal(err) 127 | return err 128 | } 129 | 130 | for { 131 | select { 132 | case err := <-s.stop: 133 | s.revoke() 134 | return err 135 | case <-s.client.Ctx().Done(): 136 | return errors.New("server closed") 137 | case ka, ok := <-ch: 138 | if !ok { 139 | log.Println("keep alive channel closed") 140 | s.revoke() 141 | return nil 142 | } else { 143 | log.Printf("Recv reply from service: %s, ttl:%d", s.Name, ka.TTL) 144 | } 145 | } 146 | } 147 | } 148 | 149 | func (s *Service) Stop() { 150 | s.stop <- nil 151 | } 152 | 153 | func (s *Service) keepAlive() (<-chan *clientv3.LeaseKeepAliveResponse, error) { 154 | 155 | info := &s.Meta 156 | 157 | key := "services/" + s.Name 158 | value, _ := json.Marshal(info) 159 | 160 | // minimum lease TTL is 5-second 161 | resp, err := s.client.Grant(context.TODO(), 5) 162 | if err != nil { 163 | log.Fatal(err) 164 | return nil, err 165 | } 166 | 167 | _, err = s.client.Put(context.TODO(), key, string(value), clientv3.WithLease(resp.ID)) 168 | if err != nil { 169 | log.Fatal(err) 170 | return nil, err 171 | } 172 | s.leaseid = resp.ID 173 | 174 | return s.client.KeepAlive(context.TODO(), resp.ID) 175 | } 176 | 177 | func (s *Service) revoke() error { 178 | 179 | _, err := s.client.Revoke(context.TODO(), s.leaseid) 180 | if err != nil { 181 | log.Fatal(err) 182 | } 183 | 184 | //log.Printf("servide:%s stop\n", s.Name) 185 | return err 186 | } 187 | --------------------------------------------------------------------------------