Repository: Bingjian-Zhu/etcd-example
Branch: master
Commit: 1005223ede7f
Files: 36
Total size: 124.1 KB
Directory structure:
gitextract_8iqpzcz_/
├── .vscode/
│ └── settings.json
├── 1-etcd环境安装与使用/
│ └── README.md
├── 2-etcd-go-client/
│ ├── README.md
│ ├── example_auth_test.go
│ ├── example_cluster_test.go
│ ├── example_kv_test.go
│ ├── example_lease_test.go
│ ├── example_maintenence_test.go
│ ├── example_metrics_test.go
│ ├── example_test.go
│ └── example_watch_test.go
├── 3-etcd-service-discovery/
│ ├── .vscode/
│ │ └── launch.json
│ ├── README.md
│ ├── discovery/
│ │ └── discovery.go
│ └── register/
│ └── register.go
├── 4-etcd-grpclb/
│ ├── README.md
│ ├── client/
│ │ └── client.go
│ ├── etcdv3/
│ │ ├── discovery.go
│ │ └── register.go
│ ├── proto/
│ │ ├── simple.pb.go
│ │ └── simple.proto
│ └── server/
│ └── server.go
├── 5-etcd-grpclb-balancer/
│ ├── README.md
│ ├── balancer/
│ │ └── weight/
│ │ └── weight.go
│ ├── client/
│ │ └── client.go
│ ├── etcdv3/
│ │ ├── discovery.go
│ │ └── register.go
│ ├── proto/
│ │ ├── simple.pb.go
│ │ └── simple.proto
│ └── server/
│ └── server.go
├── 6-etcd-mutex/
│ ├── README.md
│ └── example_mutex_test.go
├── LICENSE
├── README.md
├── go.mod
└── go.sum
================================================
FILE CONTENTS
================================================
================================================
FILE: .vscode/settings.json
================================================
{
"go.inferGopath": false
}
================================================
FILE: 1-etcd环境安装与使用/README.md
================================================
### etcd简介
[etcd](https://github.com/etcd-io/etcd)是开源的、高可用的分布式key-value存储系统,可用于配置共享和服务的注册和发现,它专注于:
* 简单:定义清晰、面向用户的API(gRPC)
* 安全:可选的客户端TLS证书自动认证
* 快速:支持每秒10,000次写入
* 可靠:基于Raft算法确保强一致性
##### etcd与redis差异
etcd和redis都支持键值存储,也支持分布式特性,redis支持的数据格式更加丰富,但是他们两个定位和应用场景不一样,关键差异如下:
* redis在分布式环境下不是强一致性的,可能会丢失数据,或者读取不到最新数据
* redis的数据变化监听机制没有etcd完善
* etcd强一致性保证数据可靠性,导致性能上要低于redis
* etcd和ZooKeeper是定位类似的项目,跟redis定位不一样
##### 为什么用 etcd 而不用ZooKeeper?
相较之下,ZooKeeper有如下缺点:
* `复杂`:ZooKeeper的部署维护复杂,管理员需要掌握一系列的知识和技能;而 Paxos 强一致性算法也是素来以复杂难懂而闻名于世;另外,ZooKeeper的使用也比较复杂,需要安装客户端,官方只提供了 Java 和 C 两种语言的接口。
* `难以维护`:Java 编写。这里不是对 Java 有偏见,而是 Java 本身就偏向于重型应用,它会引入大量的依赖。而运维人员则普遍希望保持强一致、高可用的机器集群尽可能简单,维护起来也不易出错。
* `发展缓慢`:Apache 基金会项目特有的“Apache Way”在开源界饱受争议,其中一大原因就是由于基金会庞大的结构以及松散的管理导致项目发展缓慢。
而 etcd 作为一个后起之秀,其优点也很明显。
* `简单`:使用 Go 语言编写部署简单;使用 HTTP 作为接口使用简单;使用 Raft 算法保证强一致性让用户易于理解。
* `数据持久化`:tcd 默认数据一更新就进行持久化。
* `安全`:etcd 支持 SSL 客户端安全认证。
### 单机部署
(1)到etcd的github地址,下载最新的安装包(目前最新版本:v3.4.7)
下载地址:https://github.com/etcd-io/etcd/releases/
(2)解压,把`etcd`和`etcdctl`文件复制到已经配置了环境变量的目录中
* 方法一:把`etcd`和`etcdctl`文件复制到`GOBIN`目录下。
* 方法二:在环境变量里添加`etcd`和`etcdctl`文件所在的目录。
(3)验证是否安装成功
```
$ etcd --version
etcd Version: 3.4.7
Git SHA: e694b7bb0
Go Version: go1.12.17
Go OS/Arch: linux/amd64
```
正常显示etcd版本信息,则证明安装成功。
### API学习
`etcdctl`用于与`etcd`交互的控制台程序。`API`版本可以通过`ETCDCTL_API`环境变量设置为2或3版本。默认情况下,`v3.4`以上的`etcdctl`使用`v3 API`,`v3.3`及更早的版本默认使用`v2 API`。
> 注意:用`v2 API`创建的任何key将不能通过`v3 API`查询。同样,用`v3 API`创建的任何key将不能通过`v2 API`查询。
运行etcd,在终端输入:`etcd`
在另一个终端运行ctcdctl测试。
```
#查看默认API版本
$ etcdctl version
etcdctl version: 3.4.7
API version: 3.4 #v3 API
#写入key:/test/foo value:hello etcd (双引号可去掉)
$ etcdctl put /test/foo "hello etcd"
OK
$ etcdctl get /test/foo
/test/foo
hello etcd
#手动切换到v2 API
$ export ETCDCTL_API=2
$ etcdctl --version
etcdctl version: 3.4.7
API version: 2
$ etcdctl get /test/foo
Error: client: response is invalid json. The endpoint is probably not valid etcd cluster endpoint #查询不到/test/foo的值
```
#### 写入key
```
$ etcdctl put foo bar
OK
```
#### 读取key值
```
$ etcdctl get foo
foo
bar
#只是获取值
$ etcdctl get foo --print-value-only
bar
```
```
$ etcdctl put foo1 bar1
$ etcdctl put foo2 bar2
$ etcdctl put foo3 bar3
#获取从foo到foo3的值,不包括foo3
$ etcdctl get foo foo3 --print-value-only
bar
bar1
bar2
# 获取前缀为foo的值
$ etcdctl get --prefix foo --print-value-only
bar
bar1
bar2
bar3
#获取符合前缀的前两个值
$ etcdctl get --prefix --limit=2 foo --print-value-only
bar
bar1
```
#### 删除key
```
#删除foo
$ etcdctl del foo
1
#删除foo到foo2,不包括foo2
$ etcdctl del foo foo2
1
#删除key前缀为foo的
$ etcdctl del --prefix foo
2
```
#### 监视值变化
```
#监视foo单个key
$ etcdctl watch foo
#另一个控制台执行: etcdctl put foo bar
PUT
foo
bar
#同时监视多个值
$ etcdctl watch -i
$ watch foo
$ watch zoo
# 另一个控制台执行: etcdctl put foo bar
PUT
foo
bar
# 另一个控制台执行: etcdctl put zoo val
PUT
zoo
val
#监视foo前缀的key
$ etcdctl watch --prefix foo
#另一个控制台执行: etcdctl put foo1 bar1
PUT
foo1
bar1
#另一个控制台执行: etcdctl put fooz1 barz1
PUT
fooz1
barz1
```
#### 设置租约(Grant leases)
当一个key被绑定到一个租约上时,它的生命周期与租约的生命周期绑定。
```
#设置60秒后过期时间
$ etcdctl lease grant 60
lease 32695410dcc0ca06 granted with TTL(60s)
#把foo和租约绑定,设置成60秒后过期
$ etcdctl put --lease=32695410dcc0ca06 foo bar
OK
$ etcdctl get foo
foo
bar
#60秒后,获取不到foo
$ etcdctl get foo
#返回空
```
#### 主动撤销租约(Revoke leases)
通过租赁ID(此处指:`32695410dcc0ca06`)撤销租约。`撤销租约将删除其所有绑定的key`。
```
$ etcdctl lease grant 60
lease 32695410dcc0ca06 granted with TTL(60s)
$ etcdctl put foo bar --lease=32695410dcc0ca06
OK
#主动撤销租约
$ etcdctl lease revoke 32695410dcc0ca06
lease 32695410dcc0ca06 revoked
$ etcdctl get foo
#返回空
```
#### 续租约(Keep leases alive)
通过刷新其TTL来保持租约的有效,使其不会过期。
```
#设置60秒后过期租约
$ etcdctl lease grant 60
lease 32695410dcc0ca06 granted with TTL(60s)
#把foo和租约绑定,设置成60秒后过期
$ etcdctl put foo bar --lease=32695410dcc0ca06
#续租约,自动定时执行续租约,续约成功后每次租约为60秒
$ etcdctl lease keep-alive 32695410dcc0ca06
lease 32695410dcc0ca06 keepalived with TTL(60)
lease 32695410dcc0ca06 keepalived with TTL(60)
lease 32695410dcc0ca06 keepalived with TTL(60)
...
```
#### 获取租约信息(Get lease information)
获取租约信息,以便续租或查看租约是否仍然存在或已过期
```
#设置500秒TTL
$ etcdctl lease grant 500
lease 694d5765fc71500b granted with TTL(500s)
#keyzoo1绑定694d5765fc71500b租约
$ etcdctl put zoo1 val1 --lease=694d5765fc71500b
OK
#查看租约信息,remaining(132s)剩余有效时间132秒;--keys获取租约绑定的key
$ etcdctl lease timetolive --keys 694d5765fc71500b
lease 694d5765fc71500b granted with TTL(500s), remaining(132s), attached keys([zoo1])
```
值得注意的地方,一个租约可以绑定多个`key`
```
$ etcdctl lease grant 500
lease 694d5765fc71500b granted with TTL(500s)
$ etcdctl put zoo1 val1 --lease=694d5765fc71500b
OK
$ etcdctl put zoo2 val2 --lease=694d5765fc71500b
OK
```
当租约过期后,所有key值会被删除。
当一个租约只绑定了一个`key`时,想删除这个`key`,最好的办法是撤销它的租约,而不是直接删除这个`key`。
看下面这个例子:
```
#方法一:直接删除`key`
#设置租约并绑定zoo1
$ etcdctl lease grant 60
lease 694d71f80ed8bf1e granted with TTL(60s)
$ etcdctl put zoo1 val1 --lease=694d71f80ed8bf1e
OK
#续租约
$ etcdctl lease keep-alive 694d71f80ed8bf1e
lease 694d71f80ed8bf1e keepalived with TTL(60)
#另一个控制台执行:etcdctl del zoo1
#单纯删除key后,续约操作还会一直进行,造成内存泄露
lease 694d71f80ed8bf1e keepalived with TTL(60)
lease 694d71f80ed8bf1e keepalived with TTL(60)
lease 694d71f80ed8bf1e keepalived with TTL(60)
...
```
```
方法二:撤销`key`的租约
#设置租约并绑定zoo1
$ etcdctl lease grant 60
lease 694d71f80ed8bf1e granted with TTL(60s)
$ etcdctl put zoo1 val1 --lease=694d71f80ed8bf1e
OK
#续租约
$ etcdctl lease keep-alive 694d71f80ed8bf1e
lease 694d71f80ed8bf1e keepalived with TTL(60)
lease 694d71f80ed8bf1e keepalived with TTL(60)
#另一个控制台执行:etcdctl lease revoke 694d71f80ed8bf1e
#续约操作并退出
lease 694d71f80ed8bf1e expired or revoked.
```
当租约没有绑定`key`时,应主动把它撤销掉。
### 应用场景
根据以上特性和API,etcd有应用场景以下应用场景:
#### 场景一:服务发现
服务发现要解决的也是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务,要如何才能找到对方并建立连接。本质上来说,服务发现就是想要了解集群中是否有进程在监听 udp 或 tcp 端口,并且通过名字就可以查找和连接。
#### 场景二:配置中心
etcd的应用场景优化都是围绕存储的东西是“配置” 来设定的。
* 配置的数据量通常都不大,所以默认etcd的存储上限是1GB
* 配置通常对历史版本信息是比较关心的,所以etcd会保存 版本(revision) 信息
* 配置变更是比较常见的,并且业务程序会需要实时知道,所以etcd提供了watch机制,基本就是实时通知配置变化
* 配置的准确性一致性极其重要,所以etcd采用raft算法,保证系统的CP
* 同一份配置通常会被大量客户端同时访问,针对这个做了grpc proxy对同一个key的watcher做了优化
* 配置会被不同的业务部门使用,提供了权限控制和namespace机制
#### 场景三:负载均衡
此处指的负载均衡均为软负载均衡,分布式系统中,为了保证服务的高可用以及数据的一致性,通常都会把数据和服务部署多份,以此达到对等服务,即使其中的某一个服务失效了,也不影响使用。由此带来的坏处是数据写入性能下降,而好处则是数据访问时的负载均衡。因为每个对等服务节点上都存有完整的数据,所以用户的访问流量就可以分流到不同的机器上。
#### 场景四:分布式锁
因为 etcd 使用 Raft 算法保持了数据的强一致性,某次操作存储到集群中的值必然是全局一致的,所以很容易实现分布式锁。
#### 场景五:集群监控与 Leader 竞选
通过 etcd 来进行监控实现起来非常简单并且实时性强。
* 前面几个场景已经提到 Watcher 机制,当某个节点消失或有变动时,Watcher 会第一时间发现并告知用户。
* 节点可以设置TTL key,比如每隔 30s 发送一次心跳使代表该机器存活的节点继续存在,否则节点消失。
这样就可以第一时间检测到各节点的健康状态,以完成集群的监控要求。
另外,使用分布式锁,可以完成 Leader 竞选。这种场景通常是一些长时间 CPU 计算或者使用 IO 操作的机器,只需要竞选出的 Leader 计算或处理一次,就可以把结果复制给其他的 Follower。从而避免重复劳动,节省计算资源。
参考:
* https://github.com/etcd-io/etcd
* https://etcd.io/docs/v3.4.0/dev-guide/interacting_v3/
* https://www.infoq.cn/article/etcd-interpretation-application-scenario-implement-principle/
================================================
FILE: 2-etcd-go-client/README.md
================================================
代码来源:https://github.com/etcd-io/etcd/tree/master/clientv3
================================================
FILE: 2-etcd-go-client/example_auth_test.go
================================================
// Copyright 2016 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clientv3_test
import (
"context"
"fmt"
"log"
"github.com/coreos/etcd/clientv3"
)
func ExampleAuth() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
if _, err = cli.RoleAdd(context.TODO(), "root"); err != nil {
log.Fatal(err)
}
if _, err = cli.UserAdd(context.TODO(), "root", "123"); err != nil {
log.Fatal(err)
}
if _, err = cli.UserGrantRole(context.TODO(), "root", "root"); err != nil {
log.Fatal(err)
}
if _, err = cli.RoleAdd(context.TODO(), "r"); err != nil {
log.Fatal(err)
}
if _, err = cli.RoleGrantPermission(
context.TODO(),
"r", // role name
"foo", // key
"zoo", // range end
clientv3.PermissionType(clientv3.PermReadWrite),
); err != nil {
log.Fatal(err)
}
if _, err = cli.UserAdd(context.TODO(), "u", "123"); err != nil {
log.Fatal(err)
}
if _, err = cli.UserGrantRole(context.TODO(), "u", "r"); err != nil {
log.Fatal(err)
}
if _, err = cli.AuthEnable(context.TODO()); err != nil {
log.Fatal(err)
}
cliAuth, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
Username: "u",
Password: "123",
})
if err != nil {
log.Fatal(err)
}
defer cliAuth.Close()
if _, err = cliAuth.Put(context.TODO(), "foo1", "bar"); err != nil {
log.Fatal(err)
}
_, err = cliAuth.Txn(context.TODO()).
If(clientv3.Compare(clientv3.Value("zoo1"), ">", "abc")).
Then(clientv3.OpPut("zoo1", "XYZ")).
Else(clientv3.OpPut("zoo1", "ABC")).
Commit()
fmt.Println(err)
// now check the permission with the root account
rootCli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
Username: "root",
Password: "123",
})
if err != nil {
log.Fatal(err)
}
defer rootCli.Close()
resp, err := rootCli.RoleGet(context.TODO(), "r")
if err != nil {
log.Fatal(err)
}
fmt.Printf("user u permission: key %q, range end %q\n", resp.Perm[0].Key, resp.Perm[0].RangeEnd)
if _, err = rootCli.AuthDisable(context.TODO()); err != nil {
log.Fatal(err)
}
// Output: etcdserver: permission denied
// user u permission: key "foo", range end "zoo"
}
================================================
FILE: 2-etcd-go-client/example_cluster_test.go
================================================
// Copyright 2016 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clientv3_test
import (
"context"
"fmt"
"log"
"github.com/coreos/etcd/clientv3"
)
func ExampleCluster_memberList() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
resp, err := cli.MemberList(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println("members:", len(resp.Members))
// Output: members: 3
}
func ExampleCluster_memberAdd() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints[:2],
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
peerURLs := endpoints[2:]
mresp, err := cli.MemberAdd(context.Background(), peerURLs)
if err != nil {
log.Fatal(err)
}
fmt.Println("added member.PeerURLs:", mresp.Member.PeerURLs)
// added member.PeerURLs: [http://localhost:32380]
}
func ExampleCluster_memberRemove() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints[1:],
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
resp, err := cli.MemberList(context.Background())
if err != nil {
log.Fatal(err)
}
_, err = cli.MemberRemove(context.Background(), resp.Members[0].ID)
if err != nil {
log.Fatal(err)
}
}
func ExampleCluster_memberUpdate() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
resp, err := cli.MemberList(context.Background())
if err != nil {
log.Fatal(err)
}
peerURLs := []string{"http://localhost:12380"}
_, err = cli.MemberUpdate(context.Background(), resp.Members[0].ID, peerURLs)
if err != nil {
log.Fatal(err)
}
}
================================================
FILE: 2-etcd-go-client/example_kv_test.go
================================================
// Copyright 2016 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clientv3_test
import (
"context"
"fmt"
"log"
"github.com/coreos/etcd/clientv3"
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
)
func ExampleKV_put() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
_, err = cli.Put(ctx, "sample_key", "sample_value")
cancel()
if err != nil {
log.Fatal(err)
}
}
func ExampleKV_putErrorHandling() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
_, err = cli.Put(ctx, "", "sample_value")
cancel()
if err != nil {
switch err {
case context.Canceled:
fmt.Printf("ctx is canceled by another routine: %v\n", err)
case context.DeadlineExceeded:
fmt.Printf("ctx is attached with a deadline is exceeded: %v\n", err)
case rpctypes.ErrEmptyKey:
fmt.Printf("client-side error: %v\n", err)
default:
fmt.Printf("bad cluster endpoints, which are not etcd servers: %v\n", err)
}
}
// Output: client-side error: etcdserver: key is not provided
}
func ExampleKV_get() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
_, err = cli.Put(context.TODO(), "foo", "bar")
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
resp, err := cli.Get(ctx, "foo")
cancel()
if err != nil {
log.Fatal(err)
}
for _, ev := range resp.Kvs {
fmt.Printf("%s : %s\n", ev.Key, ev.Value)
}
// Output: foo : bar
}
func ExampleKV_getWithRev() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
presp, err := cli.Put(context.TODO(), "foo", "bar1")
if err != nil {
log.Fatal(err)
}
_, err = cli.Put(context.TODO(), "foo", "bar2")
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
resp, err := cli.Get(ctx, "foo", clientv3.WithRev(presp.Header.Revision))
cancel()
if err != nil {
log.Fatal(err)
}
for _, ev := range resp.Kvs {
fmt.Printf("%s : %s\n", ev.Key, ev.Value)
}
// Output: foo : bar1
}
func ExampleKV_getSortedPrefix() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
for i := range make([]int, 3) {
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
_, err = cli.Put(ctx, fmt.Sprintf("key_%d", i), "value")
cancel()
if err != nil {
log.Fatal(err)
}
}
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
resp, err := cli.Get(ctx, "key", clientv3.WithPrefix(), clientv3.WithSort(clientv3.SortByKey, clientv3.SortDescend))
cancel()
if err != nil {
log.Fatal(err)
}
for _, ev := range resp.Kvs {
fmt.Printf("%s : %s\n", ev.Key, ev.Value)
}
// Output:
// key_2 : value
// key_1 : value
// key_0 : value
}
func ExampleKV_delete() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
defer cancel()
// count keys about to be deleted
gresp, err := cli.Get(ctx, "key", clientv3.WithPrefix())
if err != nil {
log.Fatal(err)
}
// delete the keys
dresp, err := cli.Delete(ctx, "key", clientv3.WithPrefix())
if err != nil {
log.Fatal(err)
}
fmt.Println("Deleted all keys:", int64(len(gresp.Kvs)) == dresp.Deleted)
// Output:
// Deleted all keys: true
}
func ExampleKV_compact() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
resp, err := cli.Get(ctx, "foo")
cancel()
if err != nil {
log.Fatal(err)
}
compRev := resp.Header.Revision // specify compact revision of your choice
ctx, cancel = context.WithTimeout(context.Background(), requestTimeout)
_, err = cli.Compact(ctx, compRev)
cancel()
if err != nil {
log.Fatal(err)
}
}
func ExampleKV_txn() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
kvc := clientv3.NewKV(cli)
_, err = kvc.Put(context.TODO(), "key", "xyz")
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
_, err = kvc.Txn(ctx).
// txn value comparisons are lexical
If(clientv3.Compare(clientv3.Value("key"), ">", "abc")).
// the "Then" runs, since "xyz" > "abc"
Then(clientv3.OpPut("key", "XYZ")).
// the "Else" does not run
Else(clientv3.OpPut("key", "ABC")).
Commit()
cancel()
if err != nil {
log.Fatal(err)
}
gresp, err := kvc.Get(context.TODO(), "key")
cancel()
if err != nil {
log.Fatal(err)
}
for _, ev := range gresp.Kvs {
fmt.Printf("%s : %s\n", ev.Key, ev.Value)
}
// Output: key : XYZ
}
func ExampleKV_do() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
ops := []clientv3.Op{
clientv3.OpPut("put-key", "123"),
clientv3.OpGet("put-key"),
clientv3.OpPut("put-key", "456")}
for _, op := range ops {
if _, err := cli.Do(context.TODO(), op); err != nil {
log.Fatal(err)
}
}
}
================================================
FILE: 2-etcd-go-client/example_lease_test.go
================================================
// Copyright 2016 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clientv3_test
import (
"context"
"fmt"
"log"
"github.com/coreos/etcd/clientv3"
)
func ExampleLease_grant() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
// minimum lease TTL is 5-second
resp, err := cli.Grant(context.TODO(), 5)
if err != nil {
log.Fatal(err)
}
// after 5 seconds, the key 'foo' will be removed
_, err = cli.Put(context.TODO(), "foo", "bar", clientv3.WithLease(resp.ID))
if err != nil {
log.Fatal(err)
}
}
func ExampleLease_revoke() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
resp, err := cli.Grant(context.TODO(), 5)
if err != nil {
log.Fatal(err)
}
_, err = cli.Put(context.TODO(), "foo", "bar", clientv3.WithLease(resp.ID))
if err != nil {
log.Fatal(err)
}
// revoking lease expires the key attached to its lease ID
_, err = cli.Revoke(context.TODO(), resp.ID)
if err != nil {
log.Fatal(err)
}
gresp, err := cli.Get(context.TODO(), "foo")
if err != nil {
log.Fatal(err)
}
fmt.Println("number of keys:", len(gresp.Kvs))
// Output: number of keys: 0
}
func ExampleLease_keepAlive() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
resp, err := cli.Grant(context.TODO(), 5)
if err != nil {
log.Fatal(err)
}
_, err = cli.Put(context.TODO(), "foo", "bar", clientv3.WithLease(resp.ID))
if err != nil {
log.Fatal(err)
}
// the key 'foo' will be kept forever
ch, kaerr := cli.KeepAlive(context.TODO(), resp.ID)
if kaerr != nil {
log.Fatal(kaerr)
}
ka := <-ch
fmt.Println("ttl:", ka.TTL)
// Output: ttl: 5
}
func ExampleLease_keepAliveOnce() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
resp, err := cli.Grant(context.TODO(), 5)
if err != nil {
log.Fatal(err)
}
_, err = cli.Put(context.TODO(), "foo", "bar", clientv3.WithLease(resp.ID))
if err != nil {
log.Fatal(err)
}
// to renew the lease only once
ka, kaerr := cli.KeepAliveOnce(context.TODO(), resp.ID)
if kaerr != nil {
log.Fatal(kaerr)
}
fmt.Println("ttl:", ka.TTL)
// Output: ttl: 5
}
================================================
FILE: 2-etcd-go-client/example_maintenence_test.go
================================================
// Copyright 2016 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clientv3_test
import (
"context"
"fmt"
"log"
"github.com/coreos/etcd/clientv3"
)
func ExampleMaintenance_status() {
for _, ep := range endpoints {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{ep},
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
resp, err := cli.Status(context.Background(), ep)
if err != nil {
log.Fatal(err)
}
fmt.Printf("endpoint: %s / Leader: %v\n", ep, resp.Header.MemberId == resp.Leader)
}
// endpoint: localhost:2379 / Leader: false
// endpoint: localhost:22379 / Leader: false
// endpoint: localhost:32379 / Leader: true
}
func ExampleMaintenance_defragment() {
for _, ep := range endpoints {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{ep},
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
if _, err = cli.Defragment(context.TODO(), ep); err != nil {
log.Fatal(err)
}
}
}
================================================
FILE: 2-etcd-go-client/example_metrics_test.go
================================================
// Copyright 2016 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clientv3_test
import (
"context"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"strings"
"github.com/coreos/etcd/clientv3"
grpcprom "github.com/grpc-ecosystem/go-grpc-prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"google.golang.org/grpc"
)
func ExampleClient_metrics() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialOptions: []grpc.DialOption{
grpc.WithUnaryInterceptor(grpcprom.UnaryClientInterceptor),
grpc.WithStreamInterceptor(grpcprom.StreamClientInterceptor),
},
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
// get a key so it shows up in the metrics as a range RPC
cli.Get(context.TODO(), "test_key")
// listen for all Prometheus metrics
ln, err := net.Listen("tcp", ":0")
if err != nil {
log.Fatal(err)
}
donec := make(chan struct{})
go func() {
defer close(donec)
http.Serve(ln, promhttp.Handler())
}()
defer func() {
ln.Close()
<-donec
}()
// make an http request to fetch all Prometheus metrics
url := "http://" + ln.Addr().String() + "/metrics"
resp, err := http.Get(url)
if err != nil {
log.Fatalf("fetch error: %v", err)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
log.Fatalf("fetch error: reading %s: %v", url, err)
}
// confirm range request in metrics
for _, l := range strings.Split(string(b), "\n") {
if strings.Contains(l, `grpc_client_started_total{grpc_method="Range"`) {
fmt.Println(l)
break
}
}
// Output:
// grpc_client_started_total{grpc_method="Range",grpc_service="etcdserverpb.KV",grpc_type="unary"} 1
}
================================================
FILE: 2-etcd-go-client/example_test.go
================================================
// Copyright 2016 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clientv3_test
import (
"context"
"log"
"os"
"time"
"github.com/coreos/etcd/clientv3"
"github.com/coreos/etcd/pkg/transport"
"google.golang.org/grpc/grpclog"
)
var (
dialTimeout = 5 * time.Second
requestTimeout = 10 * time.Second
endpoints = []string{"localhost:2379"}
//endpoints = []string{"localhost:2379", "localhost:22379", "localhost:32379"}
)
func Example() {
clientv3.SetLogger(grpclog.NewLoggerV2(os.Stderr, os.Stderr, os.Stderr))
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close() // make sure to close the client
_, err = cli.Put(context.TODO(), "foo", "bar")
if err != nil {
log.Fatal(err)
}
}
func ExampleConfig_withTLS() {
tlsInfo := transport.TLSInfo{
CertFile: "/tmp/test-certs/test-name-1.pem",
KeyFile: "/tmp/test-certs/test-name-1-key.pem",
TrustedCAFile: "/tmp/test-certs/trusted-ca.pem",
}
tlsConfig, err := tlsInfo.ClientConfig()
if err != nil {
log.Fatal(err)
}
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
TLS: tlsConfig,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close() // make sure to close the client
_, err = cli.Put(context.TODO(), "foo", "bar")
if err != nil {
log.Fatal(err)
}
}
================================================
FILE: 2-etcd-go-client/example_watch_test.go
================================================
// Copyright 2016 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clientv3_test
import (
"context"
"fmt"
"log"
"github.com/coreos/etcd/clientv3"
)
func ExampleWatcher_watch() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
rch := cli.Watch(context.Background(), "foo")
for wresp := range rch {
for _, ev := range wresp.Events {
fmt.Printf("%s %q : %q\n", ev.Type, ev.Kv.Key, ev.Kv.Value)
}
}
// PUT "foo" : "bar"
}
func ExampleWatcher_watchWithPrefix() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
rch := cli.Watch(context.Background(), "foo", clientv3.WithPrefix())
for wresp := range rch {
for _, ev := range wresp.Events {
fmt.Printf("%s %q : %q\n", ev.Type, ev.Kv.Key, ev.Kv.Value)
}
}
// PUT "foo1" : "bar"
}
func ExampleWatcher_watchWithRange() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
// watches within ['foo1', 'foo4'), in lexicographical order
rch := cli.Watch(context.Background(), "foo1", clientv3.WithRange("foo4"))
for wresp := range rch {
for _, ev := range wresp.Events {
fmt.Printf("%s %q : %q\n", ev.Type, ev.Kv.Key, ev.Kv.Value)
}
}
// PUT "foo1" : "bar"
// PUT "foo2" : "bar"
// PUT "foo3" : "bar"
}
func ExampleWatcher_watchWithProgressNotify() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
rch := cli.Watch(context.Background(), "foo", clientv3.WithProgressNotify())
wresp := <-rch
fmt.Printf("wresp.Header.Revision: %d\n", wresp.Header.Revision)
fmt.Println("wresp.IsProgressNotify:", wresp.IsProgressNotify())
// wresp.Header.Revision: 0
// wresp.IsProgressNotify: true
}
================================================
FILE: 3-etcd-service-discovery/.vscode/launch.json
================================================
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${file}",
"env": {},
"args": []
}
]
}
================================================
FILE: 3-etcd-service-discovery/README.md
================================================
### etcd实现服务发现
### 前言
[etcd环境安装与使用](https://bingjian-zhu.github.io/2020/05/09/etcd%E7%8E%AF%E5%A2%83%E5%AE%89%E8%A3%85%E4%B8%8E%E4%BD%BF%E7%94%A8/)文章中介绍了etcd的安装及`v3 API`使用,本篇将介绍如何使用etcd实现服务发现功能。
### 服务发现介绍
服务发现要解决的也是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务,要如何才能找到对方并建立连接。本质上来说,服务发现就是想要了解集群中是否有进程在监听 udp 或 tcp 端口,并且通过名字就可以查找和连接。

服务发现需要实现一下基本功能:
* `服务注册`:同一service的所有节点注册到相同目录下,节点启动后将自己的信息注册到所属服务的目录中。
* `健康检查`:服务节点定时进行健康检查。注册到服务目录中的信息设置一个较短的TTL,运行正常的服务节点每隔一段时间会去更新信息的TTL ,从而达到健康检查效果。
* `服务发现`:通过服务节点能查询到服务提供外部访问的 IP 和端口号。比如网关代理服务时能够及时的发现服务中新增节点、丢弃不可用的服务节点。
接下来介绍如何使用etcd实现服务发现。
### 服务注册及健康检查
根据etcd的`v3 API`,当启动一个服务时候,我们把服务的地址写进etcd,注册服务。同时绑定租约(lease),并以续租约(keep leases alive)的方式检测服务是否正常运行,从而实现健康检查。
go代码实现:
```go
package main
import (
"context"
"log"
"time"
"go.etcd.io/etcd/clientv3"
)
//ServiceRegister 创建租约注册服务
type ServiceRegister struct {
cli *clientv3.Client //etcd client
leaseID clientv3.LeaseID //租约ID
//租约keepalieve相应chan
keepAliveChan <-chan *clientv3.LeaseKeepAliveResponse
key string //key
val string //value
}
//NewServiceRegister 新建注册服务
func NewServiceRegister(endpoints []string, key, val string, lease int64) (*ServiceRegister, error) {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
}
ser := &ServiceRegister{
cli: cli,
key: key,
val: val,
}
//申请租约设置时间keepalive
if err := ser.putKeyWithLease(lease); err != nil {
return nil, err
}
return ser, nil
}
//设置租约
func (s *ServiceRegister) putKeyWithLease(lease int64) error {
//设置租约时间
resp, err := s.cli.Grant(context.Background(), lease)
if err != nil {
return err
}
//注册服务并绑定租约
_, err = s.cli.Put(context.Background(), s.key, s.val, clientv3.WithLease(resp.ID))
if err != nil {
return err
}
//设置续租 定期发送需求请求
leaseRespChan, err := s.cli.KeepAlive(context.Background(), resp.ID)
if err != nil {
return err
}
s.leaseID = resp.ID
log.Println(s.leaseID)
s.keepAliveChan = leaseRespChan
log.Printf("Put key:%s val:%s success!", s.key, s.val)
return nil
}
//ListenLeaseRespChan 监听 续租情况
func (s *ServiceRegister) ListenLeaseRespChan() {
for leaseKeepResp := range s.keepAliveChan {
log.Println("续约成功", leaseKeepResp)
}
log.Println("关闭续租")
}
// Close 注销服务
func (s *ServiceRegister) Close() error {
//撤销租约
if _, err := s.cli.Revoke(context.Background(), s.leaseID); err != nil {
return err
}
log.Println("撤销租约")
return s.cli.Close()
}
func main() {
var endpoints = []string{"localhost:2379"}
ser, err := NewServiceRegister(endpoints, "/web/node1", "localhost:8000", 5)
if err != nil {
log.Fatalln(err)
}
//监听续租相应chan
go ser.ListenLeaseRespChan()
select {
// case <-time.After(20 * time.Second):
// ser.Close()
}
}
```
主动退出服务时,可以调用Close()方法,撤销租约,从而注销服务。
### 服务发现
根据etcd的`v3 API`,很容易想到使用`Watch`监视某类服务,通过`Watch`感知服务的`添加`,`修改`或`删除`操作,修改服务列表。
```go
package main
import (
"context"
"log"
"sync"
"time"
"github.com/coreos/etcd/mvcc/mvccpb"
"go.etcd.io/etcd/clientv3"
)
//ServiceDiscovery 服务发现
type ServiceDiscovery struct {
cli *clientv3.Client //etcd client
serverList map[string]string //服务列表
lock sync.Mutex
}
//NewServiceDiscovery 新建发现服务
func NewServiceDiscovery(endpoints []string) *ServiceDiscovery {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
}
return &ServiceDiscovery{
cli: cli,
serverList: make(map[string]string),
}
}
//WatchService 初始化服务列表和监视
func (s *ServiceDiscovery) WatchService(prefix string) error {
//根据前缀获取现有的key
resp, err := s.cli.Get(context.Background(), prefix, clientv3.WithPrefix())
if err != nil {
return err
}
for _, ev := range resp.Kvs {
s.SetServiceList(string(ev.Key), string(ev.Value))
}
//监视前缀,修改变更的server
go s.watcher(prefix)
return nil
}
//watcher 监听前缀
func (s *ServiceDiscovery) watcher(prefix string) {
rch := s.cli.Watch(context.Background(), prefix, clientv3.WithPrefix())
log.Printf("watching prefix:%s now...", prefix)
for wresp := range rch {
for _, ev := range wresp.Events {
switch ev.Type {
case mvccpb.PUT: //修改或者新增
s.SetServiceList(string(ev.Kv.Key), string(ev.Kv.Value))
case mvccpb.DELETE: //删除
s.DelServiceList(string(ev.Kv.Key))
}
}
}
}
//SetServiceList 新增服务地址
func (s *ServiceDiscovery) SetServiceList(key, val string) {
s.lock.Lock()
defer s.lock.Unlock()
s.serverList[key] = string(val)
log.Println("put key :", key, "val:", val)
}
//DelServiceList 删除服务地址
func (s *ServiceDiscovery) DelServiceList(key string) {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.serverList, key)
log.Println("del key:", key)
}
//GetServices 获取服务地址
func (s *ServiceDiscovery) GetServices() []string {
s.lock.Lock()
defer s.lock.Unlock()
addrs := make([]string, 0)
for _, v := range s.serverList {
addrs = append(addrs, v)
}
return addrs
}
//Close 关闭服务
func (s *ServiceDiscovery) Close() error {
return s.cli.Close()
}
func main() {
var endpoints = []string{"localhost:2379"}
ser := NewServiceDiscovery(endpoints)
defer ser.Close()
ser.WatchService("/web/")
ser.WatchService("/gRPC/")
for {
select {
case <-time.Tick(10 * time.Second):
log.Println(ser.GetServices())
}
}
}
```
运行:
```
#运行服务发现
$go run discovery.go
watching prefix:/web/ now...
put key : /web/node1 val:localhost:8000
[localhost:8000]
#另一个终端运行服务注册
$go run register.go
Put key:/web/node1 val:localhost:8000 success!
续约成功 cluster_id:14841639068965178418 member_id:10276657743932975437 revision:29 raft_term:7
续约成功 cluster_id:14841639068965178418 member_id:10276657743932975437 revision:29 raft_term:7
...
```
### 总结
基于 Raft 算法的 etcd 天生是一个强一致性高可用的服务存储目录,用户可以在 etcd 中注册服务,并且对注册的服务设置key TTL,定时保持服务的心跳以达到监控健康状态的效果。通过在 etcd 指定的主题下注册的服务也能在对应的主题下查找到。
为了确保连接,我们可以在每个服务机器上都部署一个 Proxy 模式的 etcd,这样就可以确保能访问 etcd 集群的服务都能互相连接。
参考:
* https://segmentfault.com/a/1190000020944777
* https://blog.csdn.net/blogsun/article/details/102861648
* https://www.infoq.cn/article/etcd-interpretation-application-scenario-implement-principle/
================================================
FILE: 3-etcd-service-discovery/discovery/discovery.go
================================================
package main
import (
"context"
"log"
"sync"
"time"
"github.com/coreos/etcd/mvcc/mvccpb"
"go.etcd.io/etcd/clientv3"
)
//ServiceDiscovery 服务发现
type ServiceDiscovery struct {
cli *clientv3.Client //etcd client
serverList sync.Map
}
//NewServiceDiscovery 新建发现服务
func NewServiceDiscovery(endpoints []string) *ServiceDiscovery {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
}
return &ServiceDiscovery{
cli: cli,
}
}
//WatchService 初始化服务列表和监视
func (s *ServiceDiscovery) WatchService(prefix string) error {
//根据前缀获取现有的key
resp, err := s.cli.Get(context.Background(), prefix, clientv3.WithPrefix())
if err != nil {
return err
}
for _, ev := range resp.Kvs {
s.SetServiceList(string(ev.Key), string(ev.Value))
}
//监视前缀,修改变更的server
go s.watcher(prefix)
return nil
}
//watcher 监听前缀
func (s *ServiceDiscovery) watcher(prefix string) {
rch := s.cli.Watch(context.Background(), prefix, clientv3.WithPrefix())
log.Printf("watching prefix:%s now...", prefix)
for wresp := range rch {
for _, ev := range wresp.Events {
switch ev.Type {
case mvccpb.PUT: //修改或者新增
s.SetServiceList(string(ev.Kv.Key), string(ev.Kv.Value))
case mvccpb.DELETE: //删除
s.DelServiceList(string(ev.Kv.Key))
}
}
}
}
//SetServiceList 新增服务地址
func (s *ServiceDiscovery) SetServiceList(key, val string) {
s.serverList.Store(key, val)
log.Println("put key :", key, "val:", val)
}
//DelServiceList 删除服务地址
func (s *ServiceDiscovery) DelServiceList(key string) {
s.serverList.Delete(key)
log.Println("del key:", key)
}
//GetServices 获取服务地址
func (s *ServiceDiscovery) GetServices() []string {
addrs := make([]string, 0, 10)
s.serverList.Range(func(k, v interface{}) bool {
addrs = append(addrs, v.(string))
return true
})
return addrs
}
//Close 关闭服务
func (s *ServiceDiscovery) Close() error {
return s.cli.Close()
}
func main() {
var endpoints = []string{"localhost:2379"}
ser := NewServiceDiscovery(endpoints)
defer ser.Close()
ser.WatchService("/web/")
ser.WatchService("/gRPC/")
for {
select {
case <-time.Tick(10 * time.Second):
log.Println(ser.GetServices())
}
}
}
================================================
FILE: 3-etcd-service-discovery/register/register.go
================================================
package main
import (
"context"
"log"
"time"
"go.etcd.io/etcd/clientv3"
)
//ServiceRegister 创建租约注册服务
type ServiceRegister struct {
cli *clientv3.Client //etcd client
leaseID clientv3.LeaseID //租约ID
//租约keepalieve相应chan
keepAliveChan <-chan *clientv3.LeaseKeepAliveResponse
key string //key
val string //value
}
//NewServiceRegister 新建注册服务
func NewServiceRegister(endpoints []string, key, val string, lease int64) (*ServiceRegister, error) {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
}
ser := &ServiceRegister{
cli: cli,
key: key,
val: val,
}
//申请租约设置时间keepalive
if err := ser.putKeyWithLease(lease); err != nil {
return nil, err
}
return ser, nil
}
//设置租约
func (s *ServiceRegister) putKeyWithLease(lease int64) error {
//设置租约时间
resp, err := s.cli.Grant(context.Background(), lease)
if err != nil {
return err
}
//注册服务并绑定租约
_, err = s.cli.Put(context.Background(), s.key, s.val, clientv3.WithLease(resp.ID))
if err != nil {
return err
}
//设置续租 定期发送需求请求
leaseRespChan, err := s.cli.KeepAlive(context.Background(), resp.ID)
if err != nil {
return err
}
s.leaseID = resp.ID
s.keepAliveChan = leaseRespChan
log.Printf("Put key:%s val:%s success!", s.key, s.val)
return nil
}
//ListenLeaseRespChan 监听 续租情况
func (s *ServiceRegister) ListenLeaseRespChan() {
for leaseKeepResp := range s.keepAliveChan {
log.Println("续约成功", leaseKeepResp)
}
log.Println("关闭续租")
}
// Close 注销服务
func (s *ServiceRegister) Close() error {
//撤销租约
if _, err := s.cli.Revoke(context.Background(), s.leaseID); err != nil {
return err
}
log.Println("撤销租约")
return s.cli.Close()
}
func main() {
var endpoints = []string{"localhost:2379"}
ser, err := NewServiceRegister(endpoints, "/web/node1", "localhost:8000", 5)
if err != nil {
log.Fatalln(err)
}
//监听续租相应chan
go ser.ListenLeaseRespChan()
select {
// case <-time.After(20 * time.Second):
// ser.Close()
}
}
================================================
FILE: 4-etcd-grpclb/README.md
================================================
### gRPC负载均衡(客户端负载均衡)
### 前言
[上篇](https://bingjian-zhu.github.io/2020/05/14/etcd%E5%AE%9E%E7%8E%B0%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0/)介绍了如何使用`etcd`实现服务发现,本篇将基于etcd的服务发现前提下,介绍如何实现gRPC客户端负载均衡。
### gRPC负载均衡
gRPC官方文档提供了关于gRPC负载均衡方案[Load Balancing in gRPC](https://github.com/grpc/grpc/blob/master/doc/load-balancing.md),此方案是为gRPC设计的,下面我们对此进行分析。
#### 1、对每次调用进行负载均衡
gRPC中的负载平衡是以每次调用为基础,而不是以每个连接为基础。换句话说,即使所有的请求都来自一个客户端,我们仍希望它们在所有的服务器上实现负载平衡。
#### 2、负载均衡的方法
* `集中式`(Proxy Model)

在服务消费者和服务提供者之间有一个独立的负载均衡(LB),通常是专门的硬件设备如 F5,或者基于软件如 LVS,HAproxy等实现。LB上有所有服务的地址映射表,通常由运维配置注册,当服务消费方调用某个目标服务时,它向LB发起请求,由LB以某种策略,比如轮询(Round-Robin)做负载均衡后将请求转发到目标服务。LB一般具备健康检查能力,能自动摘除不健康的服务实例。
该方案主要问题:服务消费方、提供方之间增加了一级,有一定性能开销,请求量大时,效率较低。
> 可能有读者会认为集中式负载均衡存在这样的问题,一旦负载均衡服务挂掉,那整个系统将不能使用。
> 解决方案:可以对负载均衡服务进行DNS负载均衡,通过对一个域名设置多个IP地址,每次DNS解析时轮询返回负载均衡服务地址,从而实现简单的DNS负载均衡。
* `客户端负载`(Balancing-aware Client)

针对第一个方案的不足,此方案将LB的功能集成到服务消费方进程里,也被称为软负载或者客户端负载方案。服务提供方启动时,首先将服务地址注册到服务注册表,同时定期报心跳到服务注册表以表明服务的存活状态,相当于健康检查,服务消费方要访问某个服务时,它通过内置的LB组件向服务注册表查询,同时缓存并定期刷新目标服务地址列表,然后以某种负载均衡策略选择一个目标服务地址,最后向目标服务发起请求。LB和服务发现能力被分散到每一个服务消费者的进程内部,同时服务消费方和服务提供方之间是直接调用,没有额外开销,性能比较好。
该方案主要问题:要用多种语言、多个版本的客户端编写和维护负载均衡策略,使客户端的代码大大复杂化。
* `独立LB服务`(External Load Balancing Service)

该方案是针对第二种方案的不足而提出的一种折中方案,原理和第二种方案基本类似。
不同之处是将LB和服务发现功能从进程内移出来,变成主机上的一个独立进程。主机上的一个或者多个服务要访问目标服务时,他们都通过同一主机上的独立LB进程做服务发现和负载均衡。该方案也是一种分布式方案没有单点问题,服务调用方和LB之间是进程内调用性能好,同时该方案还简化了服务调用方,不需要为不同语言开发客户库。
本篇将介绍第二种负载均衡方法,客户端负载均衡。
### 实现gRPC客户端负载均衡
gRPC已提供了简单的负载均衡策略(如:Round Robin),我们只需实现它提供的`Builder`和`Resolver`接口,就能完成gRPC客户端负载均衡。
```go
type Builder interface {
Build(target Target, cc ClientConn, opts BuildOption) (Resolver, error)
Scheme() string
}
```
`Builder`接口:创建一个`resolver`(本文称之服务发现),用于监视名称解析更新。
`Build`方法:为给定目标创建一个新的`resolver`,当调用`grpc.Dial()`时执行。
`Scheme`方法:返回此`resolver`支持的方案,`Scheme`定义可参考:https://github.com/grpc/grpc/blob/master/doc/naming.md
```go
type Resolver interface {
ResolveNow(ResolveNowOption)
Close()
}
```
`Resolver`接口:监视指定目标的更新,包括地址更新和服务配置更新。
`ResolveNow`方法:被 gRPC 调用,以尝试再次解析目标名称。只用于提示,可忽略该方法。
`Close`方法:关闭`resolver`
根据以上两个接口,我们把服务发现的功能写在`Build`方法中,把获取到的负载均衡服务地址返回到客户端,并监视服务更新情况,以修改客户端连接。
修改服务发现代码,`discovery.go`
```go
package etcdv3
import (
"context"
"log"
"sync"
"time"
"github.com/coreos/etcd/mvcc/mvccpb"
"go.etcd.io/etcd/clientv3"
"google.golang.org/grpc/resolver"
)
const schema = "grpclb"
//ServiceDiscovery 服务发现
type ServiceDiscovery struct {
cli *clientv3.Client //etcd client
cc resolver.ClientConn
serverList map[string]resolver.Address //服务列表
lock sync.Mutex
}
//NewServiceDiscovery 新建发现服务
func NewServiceDiscovery(endpoints []string) resolver.Builder {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
}
return &ServiceDiscovery{
cli: cli,
}
}
//Build 为给定目标创建一个新的`resolver`,当调用`grpc.Dial()`时执行
func (s *ServiceDiscovery) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOption) (resolver.Resolver, error) {
log.Println("Build")
s.cc = cc
s.serverList = make(map[string]resolver.Address)
prefix := "/" + target.Scheme + "/" + target.Endpoint + "/"
//根据前缀获取现有的key
resp, err := s.cli.Get(context.Background(), prefix, clientv3.WithPrefix())
if err != nil {
return nil, err
}
for _, ev := range resp.Kvs {
s.SetServiceList(string(ev.Key), string(ev.Value))
}
s.cc.NewAddress(s.getServices())
//监视前缀,修改变更的server
go s.watcher(prefix)
return s, nil
}
// ResolveNow 监视目标更新
func (s *ServiceDiscovery) ResolveNow(rn resolver.ResolveNowOption) {
log.Println("ResolveNow")
}
//Scheme return schema
func (s *ServiceDiscovery) Scheme() string {
return schema
}
//Close 关闭
func (s *ServiceDiscovery) Close() {
log.Println("Close")
s.cli.Close()
}
//watcher 监听前缀
func (s *ServiceDiscovery) watcher(prefix string) {
rch := s.cli.Watch(context.Background(), prefix, clientv3.WithPrefix())
log.Printf("watching prefix:%s now...", prefix)
for wresp := range rch {
for _, ev := range wresp.Events {
switch ev.Type {
case mvccpb.PUT: //新增或修改
s.SetServiceList(string(ev.Kv.Key), string(ev.Kv.Value))
case mvccpb.DELETE: //删除
s.DelServiceList(string(ev.Kv.Key))
}
}
}
}
//SetServiceList 新增服务地址
func (s *ServiceDiscovery) SetServiceList(key, val string) {
s.lock.Lock()
defer s.lock.Unlock()
s.serverList[key] = resolver.Address{Addr: val}
s.cc.NewAddress(s.getServices())
log.Println("put key :", key, "val:", val)
}
//DelServiceList 删除服务地址
func (s *ServiceDiscovery) DelServiceList(key string) {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.serverList, key)
s.cc.NewAddress(s.getServices())
log.Println("del key:", key)
}
//GetServices 获取服务地址
func (s *ServiceDiscovery) getServices() []resolver.Address {
addrs := make([]resolver.Address, 0, len(s.serverList))
for _, v := range s.serverList {
addrs = append(addrs, v)
}
return addrs
}
```
代码主要修改以下地方:
1. 把获取的服务地址转成`resolver.Address`,供gRPC客户端连接。
2. 根据`schema`的定义规则,修改`key`格式。
服务注册主要修改`key`存储格式,`register.go`
```go
package etcdv3
import (
"context"
"log"
"time"
"go.etcd.io/etcd/clientv3"
)
//ServiceRegister 创建租约注册服务
type ServiceRegister struct {
cli *clientv3.Client //etcd client
leaseID clientv3.LeaseID //租约ID
//租约keepalieve相应chan
keepAliveChan <-chan *clientv3.LeaseKeepAliveResponse
key string //key
val string //value
}
//NewServiceRegister 新建注册服务
func NewServiceRegister(endpoints []string, serName, addr string, lease int64) (*ServiceRegister, error) {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
}
ser := &ServiceRegister{
cli: cli,
key: "/" + schema + "/" + serName + "/" + addr,
val: addr,
}
//申请租约设置时间keepalive
if err := ser.putKeyWithLease(lease); err != nil {
return nil, err
}
return ser, nil
}
//设置租约
func (s *ServiceRegister) putKeyWithLease(lease int64) error {
//设置租约时间
resp, err := s.cli.Grant(context.Background(), lease)
if err != nil {
return err
}
//注册服务并绑定租约
_, err = s.cli.Put(context.Background(), s.key, s.val, clientv3.WithLease(resp.ID))
if err != nil {
return err
}
//设置续租 定期发送需求请求
leaseRespChan, err := s.cli.KeepAlive(context.Background(), resp.ID)
if err != nil {
return err
}
s.leaseID = resp.ID
s.keepAliveChan = leaseRespChan
log.Printf("Put key:%s val:%s success!", s.key, s.val)
return nil
}
//ListenLeaseRespChan 监听 续租情况
func (s *ServiceRegister) ListenLeaseRespChan() {
for leaseKeepResp := range s.keepAliveChan {
log.Println("续约成功", leaseKeepResp)
}
log.Println("关闭续租")
}
// Close 注销服务
func (s *ServiceRegister) Close() error {
//撤销租约
if _, err := s.cli.Revoke(context.Background(), s.leaseID); err != nil {
return err
}
log.Println("撤销租约")
return s.cli.Close()
}
```
客户端修改gRPC连接服务的部分代码即可:
```go
func main() {
r := etcdv3.NewServiceDiscovery(EtcdEndpoints)
resolver.Register(r)
// 连接服务器
conn, err := grpc.Dial(r.Scheme()+"://8.8.8.8/simple_grpc", grpc.WithBalancerName("round_robin"), grpc.WithInsecure())
if err != nil {
log.Fatalf("net.Connect err: %v", err)
}
defer conn.Close()
// 建立gRPC连接
grpcClient = pb.NewSimpleClient(conn)
```
gRPC内置了简单的负载均衡策略`round_robin`,根据负载均衡地址,以轮询的方式进行调用服务。
服务端启动时,把服务地址注册到`etcd`中即可:
```go
func main() {
// 监听本地端口
listener, err := net.Listen(Network, Address)
if err != nil {
log.Fatalf("net.Listen err: %v", err)
}
log.Println(Address + " net.Listing...")
// 新建gRPC服务器实例
grpcServer := grpc.NewServer()
// 在gRPC服务器注册我们的服务
pb.RegisterSimpleServer(grpcServer, &SimpleService{})
//把服务注册到etcd
ser, err := etcdv3.NewServiceRegister(EtcdEndpoints, SerName, Address, 5)
if err != nil {
log.Fatalf("register service err: %v", err)
}
defer ser.Close()
//用服务器 Serve() 方法以及我们的端口信息区实现阻塞等待,直到进程被杀死或者 Stop() 被调用
err = grpcServer.Serve(listener)
if err != nil {
log.Fatalf("grpcServer.Serve err: %v", err)
}
}
```
### 运行效果
我们先启动并注册三个服务



然后客户端进行调用

看服务端接收到的请求



关闭`localhost:8000`服务,剩余`localhost:8001`和`localhost:8002`服务接收请求


重新打开`localhost:8000`服务



可以看到,gRPC客户端负载均衡运行良好。
### 总结
本文介绍了gRPC客户端负载均衡的实现,它简单实现了gRPC负载均衡的功能。但在对接其他语言时候比较麻烦,需要每种语言都实现一套服务发现和负载策略,且如果要较为复杂的负载策略,需要修改客户端代码才能完成。
下篇将介绍如何实现官方推荐的负载均衡策略(`External Load Balancing Service`)。
源码地址:https://github.com/Bingjian-Zhu/etcd-example
参考:
* https://segmentfault.com/a/1190000008672912
* https://github.com/wothing/wonaming
================================================
FILE: 4-etcd-grpclb/client/client.go
================================================
package main
import (
"context"
"fmt"
"log"
"strconv"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/resolver"
"etcd-example/4-etcd-grpclb/etcdv3"
pb "etcd-example/4-etcd-grpclb/proto"
)
var (
// EtcdEndpoints etcd地址
EtcdEndpoints = []string{"localhost:2379"}
// SerName 服务名称
SerName = "simple_grpc"
grpcClient pb.SimpleClient
)
func main() {
r := etcdv3.NewServiceDiscovery(EtcdEndpoints)
resolver.Register(r)
// 连接服务器
conn, err := grpc.Dial(
fmt.Sprintf("%s:///%s", r.Scheme(), SerName),
grpc.WithBalancerName("round_robin"),
grpc.WithInsecure(),
)
if err != nil {
log.Fatalf("net.Connect err: %v", err)
}
defer conn.Close()
// 建立gRPC连接
grpcClient = pb.NewSimpleClient(conn)
for i := 0; i < 100; i++ {
route(i)
time.Sleep(1 * time.Second)
}
}
// route 调用服务端Route方法
func route(i int) {
// 创建发送结构体
req := pb.SimpleRequest{
Data: "grpc " + strconv.Itoa(i),
}
// 调用我们的服务(Route方法)
// 同时传入了一个 context.Context ,在有需要时可以让我们改变RPC的行为,比如超时/取消一个正在运行的RPC
res, err := grpcClient.Route(context.Background(), &req)
if err != nil {
log.Fatalf("Call Route err: %v", err)
}
// 打印返回值
log.Println(res)
}
================================================
FILE: 4-etcd-grpclb/etcdv3/discovery.go
================================================
package etcdv3
import (
"context"
"log"
"sync"
"time"
"github.com/coreos/etcd/mvcc/mvccpb"
"go.etcd.io/etcd/clientv3"
"google.golang.org/grpc/resolver"
)
const schema = "grpclb"
//ServiceDiscovery 服务发现
type ServiceDiscovery struct {
cli *clientv3.Client //etcd client
cc resolver.ClientConn
serverList sync.Map //服务列表
}
//NewServiceDiscovery 新建发现服务
func NewServiceDiscovery(endpoints []string) resolver.Builder {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
}
return &ServiceDiscovery{
cli: cli,
}
}
//Build 为给定目标创建一个新的`resolver`,当调用`grpc.Dial()`时执行
func (s *ServiceDiscovery) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOption) (resolver.Resolver, error) {
log.Println("Build")
s.cc = cc
prefix := "/" + target.Scheme + "/" + target.Endpoint + "/"
//根据前缀获取现有的key
resp, err := s.cli.Get(context.Background(), prefix, clientv3.WithPrefix())
if err != nil {
return nil, err
}
for _, ev := range resp.Kvs {
s.SetServiceList(string(ev.Key), string(ev.Value))
}
s.cc.UpdateState(resolver.State{Addresses: s.getServices()})
//监视前缀,修改变更的server
go s.watcher(prefix)
return s, nil
}
// ResolveNow 监视目标更新
func (s *ServiceDiscovery) ResolveNow(rn resolver.ResolveNowOption) {
log.Println("ResolveNow")
}
//Scheme return schema
func (s *ServiceDiscovery) Scheme() string {
return schema
}
//Close 关闭
func (s *ServiceDiscovery) Close() {
log.Println("Close")
s.cli.Close()
}
//watcher 监听前缀
func (s *ServiceDiscovery) watcher(prefix string) {
rch := s.cli.Watch(context.Background(), prefix, clientv3.WithPrefix())
log.Printf("watching prefix:%s now...", prefix)
for wresp := range rch {
for _, ev := range wresp.Events {
switch ev.Type {
case mvccpb.PUT: //新增或修改
s.SetServiceList(string(ev.Kv.Key), string(ev.Kv.Value))
case mvccpb.DELETE: //删除
s.DelServiceList(string(ev.Kv.Key))
}
}
}
}
//SetServiceList 新增服务地址
func (s *ServiceDiscovery) SetServiceList(key, val string) {
s.serverList.Store(key, resolver.Address{Addr: val})
s.cc.UpdateState(resolver.State{Addresses: s.getServices()})
log.Println("put key :", key, "val:", val)
}
//DelServiceList 删除服务地址
func (s *ServiceDiscovery) DelServiceList(key string) {
s.serverList.Delete(key)
s.cc.UpdateState(resolver.State{Addresses: s.getServices()})
log.Println("del key:", key)
}
//GetServices 获取服务地址
func (s *ServiceDiscovery) getServices() []resolver.Address {
addrs := make([]resolver.Address, 0, 10)
s.serverList.Range(func(k, v interface{}) bool {
addrs = append(addrs, v.(resolver.Address))
return true
})
return addrs
}
================================================
FILE: 4-etcd-grpclb/etcdv3/register.go
================================================
package etcdv3
import (
"context"
"log"
"time"
"go.etcd.io/etcd/clientv3"
)
//ServiceRegister 创建租约注册服务
type ServiceRegister struct {
cli *clientv3.Client //etcd client
leaseID clientv3.LeaseID //租约ID
//租约keepalieve相应chan
keepAliveChan <-chan *clientv3.LeaseKeepAliveResponse
key string //key
val string //value
}
//NewServiceRegister 新建注册服务
func NewServiceRegister(endpoints []string, serName, addr string, lease int64) (*ServiceRegister, error) {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
}
ser := &ServiceRegister{
cli: cli,
key: "/" + schema + "/" + serName + "/" + addr,
val: addr,
}
//申请租约设置时间keepalive
if err := ser.putKeyWithLease(lease); err != nil {
return nil, err
}
return ser, nil
}
//设置租约
func (s *ServiceRegister) putKeyWithLease(lease int64) error {
//设置租约时间
resp, err := s.cli.Grant(context.Background(), lease)
if err != nil {
return err
}
//注册服务并绑定租约
_, err = s.cli.Put(context.Background(), s.key, s.val, clientv3.WithLease(resp.ID))
if err != nil {
return err
}
//设置续租 定期发送需求请求
leaseRespChan, err := s.cli.KeepAlive(context.Background(), resp.ID)
if err != nil {
return err
}
s.leaseID = resp.ID
s.keepAliveChan = leaseRespChan
log.Printf("Put key:%s val:%s success!", s.key, s.val)
return nil
}
//ListenLeaseRespChan 监听 续租情况
func (s *ServiceRegister) ListenLeaseRespChan() {
for leaseKeepResp := range s.keepAliveChan {
log.Println("续约成功", leaseKeepResp)
}
log.Println("关闭续租")
}
// Close 注销服务
func (s *ServiceRegister) Close() error {
//撤销租约
if _, err := s.cli.Revoke(context.Background(), s.leaseID); err != nil {
return err
}
log.Println("撤销租约")
return s.cli.Close()
}
================================================
FILE: 4-etcd-grpclb/proto/simple.pb.go
================================================
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: 2-simple_rpc/proto/simple.proto
package proto
import (
context "context"
fmt "fmt"
proto "github.com/golang/protobuf/proto"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
// 定义发送请求信息
type SimpleRequest struct {
// 定义发送的参数,采用驼峰命名方式,小写加下划线,如:student_name
// 参数类型 参数名 标识号(不可重复)
Data string `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *SimpleRequest) Reset() { *m = SimpleRequest{} }
func (m *SimpleRequest) String() string { return proto.CompactTextString(m) }
func (*SimpleRequest) ProtoMessage() {}
func (*SimpleRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_31047b63fe44dee8, []int{0}
}
func (m *SimpleRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SimpleRequest.Unmarshal(m, b)
}
func (m *SimpleRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_SimpleRequest.Marshal(b, m, deterministic)
}
func (m *SimpleRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_SimpleRequest.Merge(m, src)
}
func (m *SimpleRequest) XXX_Size() int {
return xxx_messageInfo_SimpleRequest.Size(m)
}
func (m *SimpleRequest) XXX_DiscardUnknown() {
xxx_messageInfo_SimpleRequest.DiscardUnknown(m)
}
var xxx_messageInfo_SimpleRequest proto.InternalMessageInfo
func (m *SimpleRequest) GetData() string {
if m != nil {
return m.Data
}
return ""
}
// 定义响应信息
type SimpleResponse struct {
// 定义接收的参数
// 参数类型 参数名 标识号(不可重复)
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *SimpleResponse) Reset() { *m = SimpleResponse{} }
func (m *SimpleResponse) String() string { return proto.CompactTextString(m) }
func (*SimpleResponse) ProtoMessage() {}
func (*SimpleResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_31047b63fe44dee8, []int{1}
}
func (m *SimpleResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SimpleResponse.Unmarshal(m, b)
}
func (m *SimpleResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_SimpleResponse.Marshal(b, m, deterministic)
}
func (m *SimpleResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_SimpleResponse.Merge(m, src)
}
func (m *SimpleResponse) XXX_Size() int {
return xxx_messageInfo_SimpleResponse.Size(m)
}
func (m *SimpleResponse) XXX_DiscardUnknown() {
xxx_messageInfo_SimpleResponse.DiscardUnknown(m)
}
var xxx_messageInfo_SimpleResponse proto.InternalMessageInfo
func (m *SimpleResponse) GetCode() int32 {
if m != nil {
return m.Code
}
return 0
}
func (m *SimpleResponse) GetValue() string {
if m != nil {
return m.Value
}
return ""
}
func init() {
proto.RegisterType((*SimpleRequest)(nil), "proto.SimpleRequest")
proto.RegisterType((*SimpleResponse)(nil), "proto.SimpleResponse")
}
func init() { proto.RegisterFile("2-simple_rpc/proto/simple.proto", fileDescriptor_31047b63fe44dee8) }
var fileDescriptor_31047b63fe44dee8 = []byte{
// 158 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x37, 0xd2, 0x2d, 0xce,
0xcc, 0x2d, 0xc8, 0x49, 0x8d, 0x2f, 0x2a, 0x48, 0xd6, 0x2f, 0x28, 0xca, 0x2f, 0xc9, 0xd7, 0x87,
0x08, 0xe8, 0x81, 0x39, 0x42, 0xac, 0x60, 0x4a, 0x49, 0x99, 0x8b, 0x37, 0x18, 0x2c, 0x1c, 0x94,
0x5a, 0x58, 0x9a, 0x5a, 0x5c, 0x22, 0x24, 0xc4, 0xc5, 0x92, 0x92, 0x58, 0x92, 0x28, 0xc1, 0xa8,
0xc0, 0xa8, 0xc1, 0x19, 0x04, 0x66, 0x2b, 0x59, 0x71, 0xf1, 0xc1, 0x14, 0x15, 0x17, 0xe4, 0xe7,
0x15, 0xa7, 0x82, 0x54, 0x25, 0xe7, 0xa7, 0xa4, 0x82, 0x55, 0xb1, 0x06, 0x81, 0xd9, 0x42, 0x22,
0x5c, 0xac, 0x65, 0x89, 0x39, 0xa5, 0xa9, 0x12, 0x4c, 0x60, 0xad, 0x10, 0x8e, 0x91, 0x03, 0x17,
0x1b, 0x44, 0xaf, 0x90, 0x19, 0x17, 0x6b, 0x50, 0x7e, 0x69, 0x49, 0xaa, 0x90, 0x08, 0xc4, 0x09,
0x7a, 0x28, 0x16, 0x4b, 0x89, 0xa2, 0x89, 0x42, 0x6c, 0x52, 0x62, 0x48, 0x62, 0x03, 0x8b, 0x1b,
0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0x50, 0xd7, 0x51, 0x6d, 0xd3, 0x00, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// SimpleClient is the client API for Simple service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type SimpleClient interface {
Route(ctx context.Context, in *SimpleRequest, opts ...grpc.CallOption) (*SimpleResponse, error)
}
type simpleClient struct {
cc *grpc.ClientConn
}
func NewSimpleClient(cc *grpc.ClientConn) SimpleClient {
return &simpleClient{cc}
}
func (c *simpleClient) Route(ctx context.Context, in *SimpleRequest, opts ...grpc.CallOption) (*SimpleResponse, error) {
out := new(SimpleResponse)
err := c.cc.Invoke(ctx, "/proto.Simple/Route", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// SimpleServer is the server API for Simple service.
type SimpleServer interface {
Route(context.Context, *SimpleRequest) (*SimpleResponse, error)
}
// UnimplementedSimpleServer can be embedded to have forward compatible implementations.
type UnimplementedSimpleServer struct {
}
func (*UnimplementedSimpleServer) Route(ctx context.Context, req *SimpleRequest) (*SimpleResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Route not implemented")
}
func RegisterSimpleServer(s *grpc.Server, srv SimpleServer) {
s.RegisterService(&_Simple_serviceDesc, srv)
}
func _Simple_Route_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SimpleRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SimpleServer).Route(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/proto.Simple/Route",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SimpleServer).Route(ctx, req.(*SimpleRequest))
}
return interceptor(ctx, in, info, handler)
}
var _Simple_serviceDesc = grpc.ServiceDesc{
ServiceName: "proto.Simple",
HandlerType: (*SimpleServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Route",
Handler: _Simple_Route_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "2-simple_rpc/proto/simple.proto",
}
================================================
FILE: 4-etcd-grpclb/proto/simple.proto
================================================
syntax = "proto3";// 协议为proto3
package proto;
// 定义发送请求信息
message SimpleRequest{
// 定义发送的参数,采用驼峰命名方式,小写加下划线,如:student_name
// 参数类型 参数名 标识号(不可重复)
string data = 1;
}
// 定义响应信息
message SimpleResponse{
// 定义接收的参数
// 参数类型 参数名 标识号(不可重复)
int32 code = 1;
string value = 2;
}
// 定义我们的服务(可定义多个服务,每个服务可定义多个接口)
service Simple{
rpc Route (SimpleRequest) returns (SimpleResponse){};
}
================================================
FILE: 4-etcd-grpclb/server/server.go
================================================
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
"etcd-example/4-etcd-grpclb/etcdv3"
pb "etcd-example/4-etcd-grpclb/proto"
)
// SimpleService 定义我们的服务
type SimpleService struct{}
const (
// Address 监听地址
Address string = "localhost:8000"
// Network 网络通信协议
Network string = "tcp"
// SerName 服务名称
SerName string = "simple_grpc"
)
// EtcdEndpoints etcd地址
var EtcdEndpoints = []string{"localhost:2379"}
func main() {
// 监听本地端口
listener, err := net.Listen(Network, Address)
if err != nil {
log.Fatalf("net.Listen err: %v", err)
}
log.Println(Address + " net.Listing...")
// 新建gRPC服务器实例
grpcServer := grpc.NewServer()
// 在gRPC服务器注册我们的服务
pb.RegisterSimpleServer(grpcServer, &SimpleService{})
//把服务注册到etcd
ser, err := etcdv3.NewServiceRegister(EtcdEndpoints, SerName, Address, 5)
if err != nil {
log.Fatalf("register service err: %v", err)
}
defer ser.Close()
//用服务器 Serve() 方法以及我们的端口信息区实现阻塞等待,直到进程被杀死或者 Stop() 被调用
err = grpcServer.Serve(listener)
if err != nil {
log.Fatalf("grpcServer.Serve err: %v", err)
}
}
// Route 实现Route方法
func (s *SimpleService) Route(ctx context.Context, req *pb.SimpleRequest) (*pb.SimpleResponse, error) {
log.Println("receive: " + req.Data)
res := pb.SimpleResponse{
Code: 200,
Value: "hello " + req.Data,
}
return &res, nil
}
================================================
FILE: 5-etcd-grpclb-balancer/README.md
================================================
### gRPC负载均衡(自定义负载均衡策略)
### 前言
上篇文章介绍了如何实现gRPC负载均衡,但目前官方只提供了`pick_first`和`round_robin`两种负载均衡策略,轮询法`round_robin`不能满足因服务器配置不同而承担不同负载量,这篇文章将介绍如何实现自定义负载均衡策略--`加权随机法`。
`加权随机法`可以根据服务器的处理能力而分配不同的权重,从而实现处理能力高的服务器可承担更多的请求,处理能力低的服务器少承担请求。
### 自定义负载均衡策略
gRPC提供了`V2PickerBuilder`和`V2Picker`接口让我们实现自己的负载均衡策略。
```go
type V2PickerBuilder interface {
Build(info PickerBuildInfo) balancer.V2Picker
}
```
`V2PickerBuilder`接口:创建V2版本的子连接选择器。
`Build`方法:返回一个V2选择器,将用于gRPC选择子连接。
```go
type V2Picker interface {
Pick(info PickInfo) (PickResult, error)
}
```
`V2Picker `接口:用于gRPC选择子连接去发送请求。
`Pick`方法:子连接选择
问题来了,我们需要把服务器地址的权重添加进去,但是地址`resolver.Address`并没有提供权重的属性。官方给的答复是:把权重存储到地址的元数据`metadata`中。
```go
// attributeKey is the type used as the key to store AddrInfo in the Attributes
// field of resolver.Address.
type attributeKey struct{}
// AddrInfo will be stored inside Address metadata in order to use weighted balancer.
type AddrInfo struct {
Weight int
}
// SetAddrInfo returns a copy of addr in which the Attributes field is updated
// with addrInfo.
func SetAddrInfo(addr resolver.Address, addrInfo AddrInfo) resolver.Address {
addr.Attributes = attributes.New()
addr.Attributes = addr.Attributes.WithValues(attributeKey{}, addrInfo)
return addr
}
// GetAddrInfo returns the AddrInfo stored in the Attributes fields of addr.
func GetAddrInfo(addr resolver.Address) AddrInfo {
v := addr.Attributes.Value(attributeKey{})
ai, _ := v.(AddrInfo)
return ai
}
```
定义`AddrInfo`结构体并添加权重`Weight`属性,`Set`方法把`Weight`存储到`resolver.Address`中,`Get`方法从`resolver.Address`获取`Weight`。
解决权重存储问题后,接下来我们实现加权随机法负载均衡策略。
首先实现`V2PickerBuilder`接口,返回子连接选择器。
```go
func (*rrPickerBuilder) Build(info base.PickerBuildInfo) balancer.V2Picker {
grpclog.Infof("weightPicker: newPicker called with info: %v", info)
if len(info.ReadySCs) == 0 {
return base.NewErrPickerV2(balancer.ErrNoSubConnAvailable)
}
var scs []balancer.SubConn
for subConn, addr := range info.ReadySCs {
node := GetAddrInfo(addr.Address)
if node.Weight <= 0 {
node.Weight = minWeight
} else if node.Weight > 5 {
node.Weight = maxWeight
}
for i := 0; i < node.Weight; i++ {
scs = append(scs, subConn)
}
}
return &rrPicker{
subConns: scs,
}
}
```
`加权随机法`中,我使用空间换时间的方式,把权重转成地址个数(例如`addr1`的权重是`3`,那么添加`3`个子连接到切片中;`addr2`权重为`1`,则添加`1`个子连接;选择子连接时候,按子连接切片长度生成随机数,以随机数作为下标就是选中的子连接),避免重复计算权重。考虑到内存占用,权重定义从`1`到`5`权重。
接下来实现子连接的选择,获取随机数,选择子连接
```go
type rrPicker struct {
subConns []balancer.SubConn
mu sync.Mutex
}
func (p *rrPicker) Pick(balancer.PickInfo) (balancer.PickResult, error) {
p.mu.Lock()
index := rand.Intn(len(p.subConns))
sc := p.subConns[index]
p.mu.Unlock()
return balancer.PickResult{SubConn: sc}, nil
}
```
关键代码完成后,我们把加权随机法负载均衡策略命名为`weight`,并注册到gRPC的负载均衡策略中。
```go
// Name is the name of weight balancer.
const Name = "weight"
// NewBuilder creates a new weight balancer builder.
func newBuilder() balancer.Builder {
return base.NewBalancerBuilderV2(Name, &rrPickerBuilder{}, base.Config{HealthCheck: false})
}
func init() {
balancer.Register(newBuilder())
}
```
完整代码[weight.go](https://github.com/Bingjian-Zhu/etcd-example/blob/master/5-etcd-grpclb-balancer/balancer/weight/weight.go)
最后,我们只需要在服务端注册服务时候附带权重,然后客户端在服务发现时把权重`Set`到`resolver.Address`中,最后客户端把负载论衡策略改成`weight`就完成了。
```go
//SetServiceList 设置服务地址
func (s *ServiceDiscovery) SetServiceList(key, val string) {
s.lock.Lock()
defer s.lock.Unlock()
//获取服务地址
addr := resolver.Address{Addr: strings.TrimPrefix(key, s.prefix)}
//获取服务地址权重
nodeWeight, err := strconv.Atoi(val)
if err != nil {
//非数字字符默认权重为1
nodeWeight = 1
}
//把服务地址权重存储到resolver.Address的元数据中
addr = weight.SetAddrInfo(addr, weight.AddrInfo{Weight: nodeWeight})
s.serverList[key] = addr
s.cc.UpdateState(resolver.State{Addresses: s.getServices()})
log.Println("put key :", key, "wieght:", val)
}
```
客户端使用`weight`负载均衡策略
```go
func main() {
r := etcdv3.NewServiceDiscovery(EtcdEndpoints)
resolver.Register(r)
// 连接服务器
conn, err := grpc.Dial(
fmt.Sprintf("%s:///%s", r.Scheme(), SerName),
grpc.WithBalancerName("weight"),
grpc.WithInsecure(),
)
if err != nil {
log.Fatalf("net.Connect err: %v", err)
}
defer conn.Close()
```
运行效果:
运行`服务1`,权重为`1`

运行`服务2`,权重为`4`

运行客户端

查看前50次请求在`服务1`和`服务器2`的负载情况。`服务1`分配了`9`次请求,`服务2`分配了`41`次请求,接近权重比值。


断开`服务2`,所有请求流向`服务1`

以权重为`4`,重启`服务2`,请求以加权随机法流向两个服务器

### 总结
本篇文章以加权随机法为例,介绍了如何实现gRPC自定义负载均衡策略,以满足我们的需求。
源码地址:https://github.com/Bingjian-Zhu/etcd-example
================================================
FILE: 5-etcd-grpclb-balancer/balancer/weight/weight.go
================================================
package weight
import (
"math/rand"
"sync"
"google.golang.org/grpc/attributes"
"google.golang.org/grpc/balancer"
"google.golang.org/grpc/balancer/base"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/resolver"
)
// Name is the name of weight balancer.
const Name = "weight"
var (
minWeight = 1
maxWeight = 5
)
// attributeKey is the type used as the key to store AddrInfo in the Attributes
// field of resolver.Address.
type attributeKey struct{}
// AddrInfo will be stored inside Address metadata in order to use weighted balancer.
type AddrInfo struct {
Weight int
}
// SetAddrInfo returns a copy of addr in which the Attributes field is updated
// with addrInfo.
func SetAddrInfo(addr resolver.Address, addrInfo AddrInfo) resolver.Address {
addr.Attributes = attributes.New()
addr.Attributes = addr.Attributes.WithValues(attributeKey{}, addrInfo)
return addr
}
// GetAddrInfo returns the AddrInfo stored in the Attributes fields of addr.
func GetAddrInfo(addr resolver.Address) AddrInfo {
v := addr.Attributes.Value(attributeKey{})
ai, _ := v.(AddrInfo)
return ai
}
// NewBuilder creates a new weight balancer builder.
func newBuilder() balancer.Builder {
return base.NewBalancerBuilderV2(Name, &rrPickerBuilder{}, base.Config{HealthCheck: false})
}
func init() {
balancer.Register(newBuilder())
}
type rrPickerBuilder struct{}
func (*rrPickerBuilder) Build(info base.PickerBuildInfo) balancer.V2Picker {
grpclog.Infof("weightPicker: newPicker called with info: %v", info)
if len(info.ReadySCs) == 0 {
return base.NewErrPickerV2(balancer.ErrNoSubConnAvailable)
}
var scs []balancer.SubConn
for subConn, addr := range info.ReadySCs {
node := GetAddrInfo(addr.Address)
if node.Weight <= 0 {
node.Weight = minWeight
} else if node.Weight > 5 {
node.Weight = maxWeight
}
for i := 0; i < node.Weight; i++ {
scs = append(scs, subConn)
}
}
return &rrPicker{
subConns: scs,
}
}
type rrPicker struct {
// subConns is the snapshot of the roundrobin balancer when this picker was
// created. The slice is immutable. Each Get() will do a round robin
// selection from it and return the selected SubConn.
subConns []balancer.SubConn
mu sync.Mutex
}
func (p *rrPicker) Pick(balancer.PickInfo) (balancer.PickResult, error) {
p.mu.Lock()
index := rand.Intn(len(p.subConns))
sc := p.subConns[index]
p.mu.Unlock()
return balancer.PickResult{SubConn: sc}, nil
}
================================================
FILE: 5-etcd-grpclb-balancer/client/client.go
================================================
package main
import (
"context"
"fmt"
"log"
"strconv"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/resolver"
"etcd-example/5-etcd-grpclb-balancer/etcdv3"
pb "etcd-example/5-etcd-grpclb-balancer/proto"
)
var (
// EtcdEndpoints etcd地址
EtcdEndpoints = []string{"localhost:2379"}
// SerName 服务名称
SerName = "simple_grpc"
grpcClient pb.SimpleClient
)
func main() {
r := etcdv3.NewServiceDiscovery(EtcdEndpoints)
resolver.Register(r)
// 连接服务器
conn, err := grpc.Dial(
fmt.Sprintf("%s:///%s", r.Scheme(), SerName),
grpc.WithBalancerName("weight"),
grpc.WithInsecure(),
)
if err != nil {
log.Fatalf("net.Connect err: %v", err)
}
defer conn.Close()
// 建立gRPC连接
grpcClient = pb.NewSimpleClient(conn)
for i := 0; i < 100; i++ {
route(i)
time.Sleep(1 * time.Second)
}
}
// route 调用服务端Route方法
func route(i int) {
// 创建发送结构体
req := pb.SimpleRequest{
Data: "grpc " + strconv.Itoa(i),
}
// 调用我们的服务(Route方法)
// 同时传入了一个 context.Context ,在有需要时可以让我们改变RPC的行为,比如超时/取消一个正在运行的RPC
res, err := grpcClient.Route(context.Background(), &req)
if err != nil {
log.Fatalf("Call Route err: %v", err)
}
// 打印返回值
log.Println(res)
}
================================================
FILE: 5-etcd-grpclb-balancer/etcdv3/discovery.go
================================================
package etcdv3
import (
"context"
"log"
"strconv"
"strings"
"sync"
"time"
"etcd-example/5-etcd-grpclb-balancer/balancer/weight"
"github.com/coreos/etcd/mvcc/mvccpb"
"go.etcd.io/etcd/clientv3"
"google.golang.org/grpc/resolver"
)
const schema = "grpclb"
//ServiceDiscovery 服务发现
type ServiceDiscovery struct {
cli *clientv3.Client //etcd client
cc resolver.ClientConn
serverList sync.Map //服务列表
prefix string //监视的前缀
}
//NewServiceDiscovery 新建发现服务
func NewServiceDiscovery(endpoints []string) resolver.Builder {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
}
return &ServiceDiscovery{
cli: cli,
}
}
//Build 为给定目标创建一个新的`resolver`,当调用`grpc.Dial()`时执行
func (s *ServiceDiscovery) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOption) (resolver.Resolver, error) {
log.Println("Build")
s.cc = cc
s.prefix = "/" + target.Scheme + "/" + target.Endpoint + "/"
//根据前缀获取现有的key
resp, err := s.cli.Get(context.Background(), s.prefix, clientv3.WithPrefix())
if err != nil {
return nil, err
}
for _, ev := range resp.Kvs {
s.SetServiceList(string(ev.Key), string(ev.Value))
}
s.cc.UpdateState(resolver.State{Addresses: s.getServices()})
//监视前缀,修改变更的server
go s.watcher()
return s, nil
}
// ResolveNow 监视目标更新
func (s *ServiceDiscovery) ResolveNow(rn resolver.ResolveNowOption) {
log.Println("ResolveNow")
}
//Scheme return schema
func (s *ServiceDiscovery) Scheme() string {
return schema
}
//Close 关闭
func (s *ServiceDiscovery) Close() {
log.Println("Close")
s.cli.Close()
}
//watcher 监听前缀
func (s *ServiceDiscovery) watcher() {
rch := s.cli.Watch(context.Background(), s.prefix, clientv3.WithPrefix())
log.Printf("watching prefix:%s now...", s.prefix)
for wresp := range rch {
for _, ev := range wresp.Events {
switch ev.Type {
case mvccpb.PUT: //新增或修改
s.SetServiceList(string(ev.Kv.Key), string(ev.Kv.Value))
case mvccpb.DELETE: //删除
s.DelServiceList(string(ev.Kv.Key))
}
}
}
}
//SetServiceList 设置服务地址
func (s *ServiceDiscovery) SetServiceList(key, val string) {
//获取服务地址
addr := resolver.Address{Addr: strings.TrimPrefix(key, s.prefix)}
//获取服务地址权重
nodeWeight, err := strconv.Atoi(val)
if err != nil {
//非数字字符默认权重为1
nodeWeight = 1
}
//把服务地址权重存储到resolver.Address的元数据中
addr = weight.SetAddrInfo(addr, weight.AddrInfo{Weight: nodeWeight})
s.serverList.Store(key, addr)
s.cc.UpdateState(resolver.State{Addresses: s.getServices()})
log.Println("put key :", key, "wieght:", val)
}
//DelServiceList 删除服务地址
func (s *ServiceDiscovery) DelServiceList(key string) {
s.serverList.Delete(key)
s.cc.UpdateState(resolver.State{Addresses: s.getServices()})
log.Println("del key:", key)
}
//GetServices 获取服务地址
func (s *ServiceDiscovery) getServices() []resolver.Address {
addrs := make([]resolver.Address, 0, 10)
s.serverList.Range(func(k, v interface{}) bool {
addrs = append(addrs, v.(resolver.Address))
return true
})
return addrs
}
================================================
FILE: 5-etcd-grpclb-balancer/etcdv3/register.go
================================================
package etcdv3
import (
"context"
"log"
"time"
"go.etcd.io/etcd/clientv3"
)
//ServiceRegister 创建租约注册服务
type ServiceRegister struct {
cli *clientv3.Client //etcd client
leaseID clientv3.LeaseID //租约ID
//租约keepalieve相应chan
keepAliveChan <-chan *clientv3.LeaseKeepAliveResponse
key string //key
weight string //value
}
//NewServiceRegister 新建注册服务
func NewServiceRegister(endpoints []string, addr, weigit string, lease int64) (*ServiceRegister, error) {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
}
ser := &ServiceRegister{
cli: cli,
key: "/" + schema + "/" + addr,
weight: weigit,
}
//申请租约设置时间keepalive
if err := ser.putKeyWithLease(lease); err != nil {
return nil, err
}
return ser, nil
}
//设置租约
func (s *ServiceRegister) putKeyWithLease(lease int64) error {
//设置租约时间
resp, err := s.cli.Grant(context.Background(), lease)
if err != nil {
return err
}
//注册服务并绑定租约
_, err = s.cli.Put(context.Background(), s.key, s.weight, clientv3.WithLease(resp.ID))
if err != nil {
return err
}
//设置续租 定期发送需求请求
leaseRespChan, err := s.cli.KeepAlive(context.Background(), resp.ID)
if err != nil {
return err
}
s.leaseID = resp.ID
s.keepAliveChan = leaseRespChan
log.Printf("Put key:%s weight:%s success!", s.key, s.weight)
return nil
}
//ListenLeaseRespChan 监听 续租情况
func (s *ServiceRegister) ListenLeaseRespChan() {
for leaseKeepResp := range s.keepAliveChan {
log.Println("续约成功", leaseKeepResp)
}
log.Println("关闭续租")
}
// Close 注销服务
func (s *ServiceRegister) Close() error {
//撤销租约
if _, err := s.cli.Revoke(context.Background(), s.leaseID); err != nil {
return err
}
log.Println("撤销租约")
return s.cli.Close()
}
================================================
FILE: 5-etcd-grpclb-balancer/proto/simple.pb.go
================================================
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: 2-simple_rpc/proto/simple.proto
package proto
import (
context "context"
fmt "fmt"
proto "github.com/golang/protobuf/proto"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
// 定义发送请求信息
type SimpleRequest struct {
// 定义发送的参数,采用驼峰命名方式,小写加下划线,如:student_name
// 参数类型 参数名 标识号(不可重复)
Data string `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *SimpleRequest) Reset() { *m = SimpleRequest{} }
func (m *SimpleRequest) String() string { return proto.CompactTextString(m) }
func (*SimpleRequest) ProtoMessage() {}
func (*SimpleRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_31047b63fe44dee8, []int{0}
}
func (m *SimpleRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SimpleRequest.Unmarshal(m, b)
}
func (m *SimpleRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_SimpleRequest.Marshal(b, m, deterministic)
}
func (m *SimpleRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_SimpleRequest.Merge(m, src)
}
func (m *SimpleRequest) XXX_Size() int {
return xxx_messageInfo_SimpleRequest.Size(m)
}
func (m *SimpleRequest) XXX_DiscardUnknown() {
xxx_messageInfo_SimpleRequest.DiscardUnknown(m)
}
var xxx_messageInfo_SimpleRequest proto.InternalMessageInfo
func (m *SimpleRequest) GetData() string {
if m != nil {
return m.Data
}
return ""
}
// 定义响应信息
type SimpleResponse struct {
// 定义接收的参数
// 参数类型 参数名 标识号(不可重复)
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *SimpleResponse) Reset() { *m = SimpleResponse{} }
func (m *SimpleResponse) String() string { return proto.CompactTextString(m) }
func (*SimpleResponse) ProtoMessage() {}
func (*SimpleResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_31047b63fe44dee8, []int{1}
}
func (m *SimpleResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SimpleResponse.Unmarshal(m, b)
}
func (m *SimpleResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_SimpleResponse.Marshal(b, m, deterministic)
}
func (m *SimpleResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_SimpleResponse.Merge(m, src)
}
func (m *SimpleResponse) XXX_Size() int {
return xxx_messageInfo_SimpleResponse.Size(m)
}
func (m *SimpleResponse) XXX_DiscardUnknown() {
xxx_messageInfo_SimpleResponse.DiscardUnknown(m)
}
var xxx_messageInfo_SimpleResponse proto.InternalMessageInfo
func (m *SimpleResponse) GetCode() int32 {
if m != nil {
return m.Code
}
return 0
}
func (m *SimpleResponse) GetValue() string {
if m != nil {
return m.Value
}
return ""
}
func init() {
proto.RegisterType((*SimpleRequest)(nil), "proto.SimpleRequest")
proto.RegisterType((*SimpleResponse)(nil), "proto.SimpleResponse")
}
func init() { proto.RegisterFile("2-simple_rpc/proto/simple.proto", fileDescriptor_31047b63fe44dee8) }
var fileDescriptor_31047b63fe44dee8 = []byte{
// 158 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x37, 0xd2, 0x2d, 0xce,
0xcc, 0x2d, 0xc8, 0x49, 0x8d, 0x2f, 0x2a, 0x48, 0xd6, 0x2f, 0x28, 0xca, 0x2f, 0xc9, 0xd7, 0x87,
0x08, 0xe8, 0x81, 0x39, 0x42, 0xac, 0x60, 0x4a, 0x49, 0x99, 0x8b, 0x37, 0x18, 0x2c, 0x1c, 0x94,
0x5a, 0x58, 0x9a, 0x5a, 0x5c, 0x22, 0x24, 0xc4, 0xc5, 0x92, 0x92, 0x58, 0x92, 0x28, 0xc1, 0xa8,
0xc0, 0xa8, 0xc1, 0x19, 0x04, 0x66, 0x2b, 0x59, 0x71, 0xf1, 0xc1, 0x14, 0x15, 0x17, 0xe4, 0xe7,
0x15, 0xa7, 0x82, 0x54, 0x25, 0xe7, 0xa7, 0xa4, 0x82, 0x55, 0xb1, 0x06, 0x81, 0xd9, 0x42, 0x22,
0x5c, 0xac, 0x65, 0x89, 0x39, 0xa5, 0xa9, 0x12, 0x4c, 0x60, 0xad, 0x10, 0x8e, 0x91, 0x03, 0x17,
0x1b, 0x44, 0xaf, 0x90, 0x19, 0x17, 0x6b, 0x50, 0x7e, 0x69, 0x49, 0xaa, 0x90, 0x08, 0xc4, 0x09,
0x7a, 0x28, 0x16, 0x4b, 0x89, 0xa2, 0x89, 0x42, 0x6c, 0x52, 0x62, 0x48, 0x62, 0x03, 0x8b, 0x1b,
0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0x50, 0xd7, 0x51, 0x6d, 0xd3, 0x00, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// SimpleClient is the client API for Simple service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type SimpleClient interface {
Route(ctx context.Context, in *SimpleRequest, opts ...grpc.CallOption) (*SimpleResponse, error)
}
type simpleClient struct {
cc *grpc.ClientConn
}
func NewSimpleClient(cc *grpc.ClientConn) SimpleClient {
return &simpleClient{cc}
}
func (c *simpleClient) Route(ctx context.Context, in *SimpleRequest, opts ...grpc.CallOption) (*SimpleResponse, error) {
out := new(SimpleResponse)
err := c.cc.Invoke(ctx, "/proto.Simple/Route", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// SimpleServer is the server API for Simple service.
type SimpleServer interface {
Route(context.Context, *SimpleRequest) (*SimpleResponse, error)
}
// UnimplementedSimpleServer can be embedded to have forward compatible implementations.
type UnimplementedSimpleServer struct {
}
func (*UnimplementedSimpleServer) Route(ctx context.Context, req *SimpleRequest) (*SimpleResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Route not implemented")
}
func RegisterSimpleServer(s *grpc.Server, srv SimpleServer) {
s.RegisterService(&_Simple_serviceDesc, srv)
}
func _Simple_Route_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SimpleRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SimpleServer).Route(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/proto.Simple/Route",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SimpleServer).Route(ctx, req.(*SimpleRequest))
}
return interceptor(ctx, in, info, handler)
}
var _Simple_serviceDesc = grpc.ServiceDesc{
ServiceName: "proto.Simple",
HandlerType: (*SimpleServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Route",
Handler: _Simple_Route_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "2-simple_rpc/proto/simple.proto",
}
================================================
FILE: 5-etcd-grpclb-balancer/proto/simple.proto
================================================
syntax = "proto3";// 协议为proto3
package proto;
// 定义发送请求信息
message SimpleRequest{
// 定义发送的参数,采用驼峰命名方式,小写加下划线,如:student_name
// 参数类型 参数名 标识号(不可重复)
string data = 1;
}
// 定义响应信息
message SimpleResponse{
// 定义接收的参数
// 参数类型 参数名 标识号(不可重复)
int32 code = 1;
string value = 2;
}
// 定义我们的服务(可定义多个服务,每个服务可定义多个接口)
service Simple{
rpc Route (SimpleRequest) returns (SimpleResponse){};
}
================================================
FILE: 5-etcd-grpclb-balancer/server/server.go
================================================
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
"etcd-example/5-etcd-grpclb-balancer/etcdv3"
pb "etcd-example/5-etcd-grpclb-balancer/proto"
)
// SimpleService 定义我们的服务
type SimpleService struct{}
const (
// Address 监听地址
Address string = "localhost:8000"
// Network 网络通信协议
Network string = "tcp"
// SerName 服务名称
SerName string = "simple_grpc"
)
// EtcdEndpoints etcd地址
var EtcdEndpoints = []string{"localhost:2379"}
func main() {
// 监听本地端口
listener, err := net.Listen(Network, Address)
if err != nil {
log.Fatalf("net.Listen err: %v", err)
}
log.Println(Address + " net.Listing...")
// 新建gRPC服务器实例
grpcServer := grpc.NewServer()
// 在gRPC服务器注册我们的服务
pb.RegisterSimpleServer(grpcServer, &SimpleService{})
//把服务注册到etcd
ser, err := etcdv3.NewServiceRegister(EtcdEndpoints, SerName+"/"+Address, "1", 5)
if err != nil {
log.Fatalf("register service err: %v", err)
}
defer ser.Close()
//用服务器 Serve() 方法以及我们的端口信息区实现阻塞等待,直到进程被杀死或者 Stop() 被调用
err = grpcServer.Serve(listener)
if err != nil {
log.Fatalf("grpcServer.Serve err: %v", err)
}
}
// Route 实现Route方法
func (s *SimpleService) Route(ctx context.Context, req *pb.SimpleRequest) (*pb.SimpleResponse, error) {
log.Println("receive: " + req.Data)
res := pb.SimpleResponse{
Code: 200,
Value: "hello " + req.Data,
}
return &res, nil
}
================================================
FILE: 6-etcd-mutex/README.md
================================================
### etcd分布式锁及事务
### 前言
`分布式锁`是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。
### etcd分布式锁设计
1. `排他性`:任意时刻,只能有一个机器的一个线程能获取到锁。
通过在etcd中存入key值来实现上锁,删除key实现解锁,参考下面伪代码:
```go
func Lock(key string, cli *clientv3.Client) error {
//获取key,判断是否存在锁
resp, err := cli.Get(context.Background(), key)
if err != nil {
return err
}
//锁存在,返回上锁失败
if len(resp.Kvs) > 0 {
return errors.New("lock fail")
}
_, err = cli.Put(context.Background(), key, "lock")
if err != nil {
return err
}
return nil
}
//删除key,解锁
func UnLock(key string, cli *clientv3.Client) error {
_, err := cli.Delete(context.Background(), key)
return err
}
```
当发现已上锁时,直接返回lock fail。也可以处理成等待解锁,解锁后竞争锁。
```go
//等待key删除后再竞争锁
func waitDelete(key string, cli *clientv3.Client) {
rch := cli.Watch(context.Background(), key)
for wresp := range rch {
for _, ev := range wresp.Events {
switch ev.Type {
case mvccpb.DELETE: //删除
return
}
}
}
}
```
2. `容错性`:只要分布式锁服务集群节点大部分存活,client就可以进行加锁解锁操作。
`etcd`基于`Raft`算法,确保集群中数据一致性。
3. `避免死锁`:分布式锁一定能得到释放,即使client在释放之前崩溃。
上面分布式锁设计有缺陷,假如client获取到锁后程序直接崩了,没有解锁,那其他线程也无法拿到锁,导致死锁出现。
通过给key设定`leases`来避免死锁,但是`leases`过期时间设多长呢?假如设了30秒,而上锁后的操作比30秒大,会导致以下问题:
* 操作没完成,锁被别人占用了,不安全
* 操作完成后,进行解锁,这时候把别人占用的锁解开了
`解决方案`:给key添加过期时间后,以`Keep leases alive`方式延续`leases`,当client正常持有锁时,锁不会过期;当client程序崩掉后,程序不能执行`Keep leases alive`,从而让锁过期,避免死锁。看以下伪代码:
```go
//上锁
func Lock(key string, cli *clientv3.Client) error {
//获取key,判断是否存在锁
resp, err := cli.Get(context.Background(), key)
if err != nil {
return err
}
//锁存在,等待解锁后再竞争锁
if len(resp.Kvs) > 0 {
waitDelete(key, cli)
return Lock(key)
}
//设置key过期时间
resp, err := cli.Grant(context.TODO(), 30)
if err != nil {
return err
}
//设置key并绑定过期时间
_, err = cli.Put(context.Background(), key, "lock", clientv3.WithLease(resp.ID))
if err != nil {
return err
}
//延续key的过期时间
_, err = cli.KeepAlive(context.TODO(), resp.ID)
if err != nil {
return err
}
return nil
}
//通过让key值过期来解锁
func UnLock(resp *clientv3.LeaseGrantResponse, cli *clientv3.Client) error {
_, err := cli.Revoke(context.TODO(), resp.ID)
return err
}
```
经过以上步骤,我们初步完成了分布式锁设计。其实官方已经实现了分布式锁,它大致原理和上述有出入,接下来我们看下如何使用官方的分布式锁。
### etcd分布式锁使用
```go
func ExampleMutex_Lock() {
cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
// create two separate sessions for lock competition
s1, err := concurrency.NewSession(cli)
if err != nil {
log.Fatal(err)
}
defer s1.Close()
m1 := concurrency.NewMutex(s1, "/my-lock/")
s2, err := concurrency.NewSession(cli)
if err != nil {
log.Fatal(err)
}
defer s2.Close()
m2 := concurrency.NewMutex(s2, "/my-lock/")
// acquire lock for s1
if err := m1.Lock(context.TODO()); err != nil {
log.Fatal(err)
}
fmt.Println("acquired lock for s1")
m2Locked := make(chan struct{})
go func() {
defer close(m2Locked)
// wait until s1 is locks /my-lock/
if err := m2.Lock(context.TODO()); err != nil {
log.Fatal(err)
}
}()
if err := m1.Unlock(context.TODO()); err != nil {
log.Fatal(err)
}
fmt.Println("released lock for s1")
<-m2Locked
fmt.Println("acquired lock for s2")
// Output:
// acquired lock for s1
// released lock for s1
// acquired lock for s2
}
```
此代码来源于[官方文档](https://github.com/etcd-io/etcd/blob/master/clientv3/concurrency/example_mutex_test.go),etcd分布式锁使用起来很方便。
### etcd事务
顺便介绍一下etcd事务,先看这段伪代码:
```go
Txn(context.TODO()).If(//如果以下判断条件成立
Compare(Value(k1), "<", v1),
Compare(Version(k1), "=", 2)
).Then(//则执行Then代码段
OpPut(k2,v2), OpPut(k3,v3)
).Else(//否则执行Else代码段
OpPut(k4,v4), OpPut(k5,v5)
).Commit()//最后提交事务
```
使用例子,代码来自[官方文档](https://github.com/etcd-io/etcd/blob/master/clientv3/example_kv_test.go):
```go
func ExampleKV_txn() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
kvc := clientv3.NewKV(cli)
_, err = kvc.Put(context.TODO(), "key", "xyz")
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
_, err = kvc.Txn(ctx).
// txn value comparisons are lexical
If(clientv3.Compare(clientv3.Value("key"), ">", "abc")).
// the "Then" runs, since "xyz" > "abc"
Then(clientv3.OpPut("key", "XYZ")).
// the "Else" does not run
Else(clientv3.OpPut("key", "ABC")).
Commit()
cancel()
if err != nil {
log.Fatal(err)
}
gresp, err := kvc.Get(context.TODO(), "key")
cancel()
if err != nil {
log.Fatal(err)
}
for _, ev := range gresp.Kvs {
fmt.Printf("%s : %s\n", ev.Key, ev.Value)
}
// Output: key : XYZ
}
```
### 总结
如果发展到分布式服务阶段,且对数据的可靠性要求很高,选`etcd`实现分布式锁不会错。介于对`ZooKeeper`好感度不强,这里就不介绍`ZooKeeper`分布式锁了。一般的`Redis`分布式锁,可能出现锁丢失的情况(如果你是Java开发者,可以使用Redisson客户端实现分布式锁,据说不会出现锁丢失的情况)。
================================================
FILE: 6-etcd-mutex/example_mutex_test.go
================================================
// Copyright 2017 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package concurrency_test
import (
"context"
"fmt"
"log"
"github.com/coreos/etcd/clientv3"
"github.com/coreos/etcd/clientv3/concurrency"
)
func ExampleMutex_Lock() {
cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
// create two separate sessions for lock competition
s1, err := concurrency.NewSession(cli)
if err != nil {
log.Fatal(err)
}
defer s1.Close()
m1 := concurrency.NewMutex(s1, "/my-lock/")
s2, err := concurrency.NewSession(cli)
if err != nil {
log.Fatal(err)
}
defer s2.Close()
m2 := concurrency.NewMutex(s2, "/my-lock/")
// acquire lock for s1
if err := m1.Lock(context.TODO()); err != nil {
log.Fatal(err)
}
fmt.Println("acquired lock for s1")
m2Locked := make(chan struct{})
go func() {
defer close(m2Locked)
// wait until s1 is locks /my-lock/
if err := m2.Lock(context.TODO()); err != nil {
log.Fatal(err)
}
}()
if err := m1.Unlock(context.TODO()); err != nil {
log.Fatal(err)
}
fmt.Println("released lock for s1")
<-m2Locked
fmt.Println("acquired lock for s2")
// Output:
// acquired lock for s1
// released lock for s1
// acquired lock for s2
}
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
### 教程
* [etcd环境安装与使用](https://www.cnblogs.com/FireworksEasyCool/p/12858570.html)
* [etcd实现服务发现](https://www.cnblogs.com/FireworksEasyCool/p/12890649.html)
* [gRPC负载均衡(客户端负载均衡)--基于etcd服务发现](https://www.cnblogs.com/FireworksEasyCool/p/12912839.html)
* [gRPC负载均衡(自定义负载均衡策略)--基于etcd服务发现](https://www.cnblogs.com/FireworksEasyCool/p/12924701.html)
* [etcd分布式锁及事务](https://www.cnblogs.com/FireworksEasyCool/p/12937882.html)
================================================
FILE: go.mod
================================================
module etcd-example
go 1.13
require (
github.com/coreos/etcd v3.3.20+incompatible
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect
github.com/gogo/protobuf v1.3.1 // indirect
github.com/golang/protobuf v1.4.1
github.com/google/uuid v1.1.1 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/prometheus/client_golang v1.6.0
go.etcd.io/etcd v3.3.20+incompatible
go.uber.org/zap v1.15.0 // indirect
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f // indirect
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25 // indirect
golang.org/x/text v0.3.2 // indirect
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380 // indirect
google.golang.org/grpc v1.29.1
)
replace google.golang.org/grpc => google.golang.org/grpc v1.26.0
================================================
FILE: go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bingjian-zhu/etcd-example v0.0.0-20200509084147-ecf5c76cccde h1:sa9ttAnwsJ+GgzqpHQSs4Iq7h/BJgrOxQAEJweaw4Tc=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/etcd v3.3.20+incompatible h1:jIrdkuJDHmyh6VZsxQQ3LQGfOrwgJx6sILz/lxzXsGw=
github.com/coreos/etcd v3.3.20+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.6.0 h1:YVPodQOcK15POxhgARIvnDRVpLcuK8mglnMrWfyrw6A=
github.com/prometheus/client_golang v1.6.0/go.mod h1:ZLOG9ck3JLRdB5MgO8f+lLTe83AXG6ro35rLTxvnIl4=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI=
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
go.etcd.io/etcd v3.3.20+incompatible h1:EyOVslCepyFB2JcbYXvqcYdBTh7cyBKU2NYdKfgTSC0=
go.etcd.io/etcd v3.3.20+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI=
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM=
go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f h1:QBjCr1Fz5kw158VqdE9JfI9cJnl/ymnJWAdMuinqL7Y=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25 h1:OKbAoGs4fGM5cPLlVQLZGYkFC8OnOfgo6tt0Smf9XhM=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380 h1:xriR1EgvKfkKxIoU2uUvrMVl+H26359loFFUleSMXFo=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0 h1:cJv5/xdbk1NnMPR1VP9+HU6gupuG9MLBoH1r6RHZ2MY=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
gitextract_8iqpzcz_/ ├── .vscode/ │ └── settings.json ├── 1-etcd环境安装与使用/ │ └── README.md ├── 2-etcd-go-client/ │ ├── README.md │ ├── example_auth_test.go │ ├── example_cluster_test.go │ ├── example_kv_test.go │ ├── example_lease_test.go │ ├── example_maintenence_test.go │ ├── example_metrics_test.go │ ├── example_test.go │ └── example_watch_test.go ├── 3-etcd-service-discovery/ │ ├── .vscode/ │ │ └── launch.json │ ├── README.md │ ├── discovery/ │ │ └── discovery.go │ └── register/ │ └── register.go ├── 4-etcd-grpclb/ │ ├── README.md │ ├── client/ │ │ └── client.go │ ├── etcdv3/ │ │ ├── discovery.go │ │ └── register.go │ ├── proto/ │ │ ├── simple.pb.go │ │ └── simple.proto │ └── server/ │ └── server.go ├── 5-etcd-grpclb-balancer/ │ ├── README.md │ ├── balancer/ │ │ └── weight/ │ │ └── weight.go │ ├── client/ │ │ └── client.go │ ├── etcdv3/ │ │ ├── discovery.go │ │ └── register.go │ ├── proto/ │ │ ├── simple.pb.go │ │ └── simple.proto │ └── server/ │ └── server.go ├── 6-etcd-mutex/ │ ├── README.md │ └── example_mutex_test.go ├── LICENSE ├── README.md ├── go.mod └── go.sum
SYMBOL INDEX (174 symbols across 22 files)
FILE: 2-etcd-go-client/example_auth_test.go
function ExampleAuth (line 25) | func ExampleAuth() {
FILE: 2-etcd-go-client/example_cluster_test.go
function ExampleCluster_memberList (line 25) | func ExampleCluster_memberList() {
function ExampleCluster_memberAdd (line 43) | func ExampleCluster_memberAdd() {
function ExampleCluster_memberRemove (line 62) | func ExampleCluster_memberRemove() {
function ExampleCluster_memberUpdate (line 83) | func ExampleCluster_memberUpdate() {
FILE: 2-etcd-go-client/example_kv_test.go
function ExampleKV_put (line 26) | func ExampleKV_put() {
function ExampleKV_putErrorHandling (line 44) | func ExampleKV_putErrorHandling() {
function ExampleKV_get (line 72) | func ExampleKV_get() {
function ExampleKV_getWithRev (line 99) | func ExampleKV_getWithRev() {
function ExampleKV_getSortedPrefix (line 130) | func ExampleKV_getSortedPrefix() {
function ExampleKV_delete (line 164) | func ExampleKV_delete() {
function ExampleKV_compact (line 194) | func ExampleKV_compact() {
function ExampleKV_txn (line 220) | func ExampleKV_txn() {
function ExampleKV_do (line 262) | func ExampleKV_do() {
FILE: 2-etcd-go-client/example_lease_test.go
function ExampleLease_grant (line 25) | func ExampleLease_grant() {
function ExampleLease_revoke (line 48) | func ExampleLease_revoke() {
function ExampleLease_keepAlive (line 82) | func ExampleLease_keepAlive() {
function ExampleLease_keepAliveOnce (line 113) | func ExampleLease_keepAliveOnce() {
FILE: 2-etcd-go-client/example_maintenence_test.go
function ExampleMaintenance_status (line 25) | func ExampleMaintenance_status() {
function ExampleMaintenance_defragment (line 47) | func ExampleMaintenance_defragment() {
FILE: 2-etcd-go-client/example_metrics_test.go
function ExampleClient_metrics (line 33) | func ExampleClient_metrics() {
FILE: 2-etcd-go-client/example_test.go
function Example (line 36) | func Example() {
function ExampleConfig_withTLS (line 54) | func ExampleConfig_withTLS() {
FILE: 2-etcd-go-client/example_watch_test.go
function ExampleWatcher_watch (line 25) | func ExampleWatcher_watch() {
function ExampleWatcher_watchWithPrefix (line 44) | func ExampleWatcher_watchWithPrefix() {
function ExampleWatcher_watchWithRange (line 63) | func ExampleWatcher_watchWithRange() {
function ExampleWatcher_watchWithProgressNotify (line 85) | func ExampleWatcher_watchWithProgressNotify() {
FILE: 3-etcd-service-discovery/discovery/discovery.go
type ServiceDiscovery (line 14) | type ServiceDiscovery struct
method WatchService (line 35) | func (s *ServiceDiscovery) WatchService(prefix string) error {
method watcher (line 52) | func (s *ServiceDiscovery) watcher(prefix string) {
method SetServiceList (line 68) | func (s *ServiceDiscovery) SetServiceList(key, val string) {
method DelServiceList (line 74) | func (s *ServiceDiscovery) DelServiceList(key string) {
method GetServices (line 80) | func (s *ServiceDiscovery) GetServices() []string {
method Close (line 90) | func (s *ServiceDiscovery) Close() error {
function NewServiceDiscovery (line 20) | func NewServiceDiscovery(endpoints []string) *ServiceDiscovery {
function main (line 94) | func main() {
FILE: 3-etcd-service-discovery/register/register.go
type ServiceRegister (line 12) | type ServiceRegister struct
method putKeyWithLease (line 46) | func (s *ServiceRegister) putKeyWithLease(lease int64) error {
method ListenLeaseRespChan (line 70) | func (s *ServiceRegister) ListenLeaseRespChan() {
method Close (line 78) | func (s *ServiceRegister) Close() error {
function NewServiceRegister (line 22) | func NewServiceRegister(endpoints []string, key, val string, lease int64...
function main (line 87) | func main() {
FILE: 4-etcd-grpclb/client/client.go
function main (line 25) | func main() {
function route (line 49) | func route(i int) {
FILE: 4-etcd-grpclb/etcdv3/discovery.go
constant schema (line 14) | schema = "grpclb"
type ServiceDiscovery (line 17) | type ServiceDiscovery struct
method Build (line 39) | func (s *ServiceDiscovery) Build(target resolver.Target, cc resolver.C...
method ResolveNow (line 59) | func (s *ServiceDiscovery) ResolveNow(rn resolver.ResolveNowOption) {
method Scheme (line 64) | func (s *ServiceDiscovery) Scheme() string {
method Close (line 69) | func (s *ServiceDiscovery) Close() {
method watcher (line 75) | func (s *ServiceDiscovery) watcher(prefix string) {
method SetServiceList (line 91) | func (s *ServiceDiscovery) SetServiceList(key, val string) {
method DelServiceList (line 98) | func (s *ServiceDiscovery) DelServiceList(key string) {
method getServices (line 105) | func (s *ServiceDiscovery) getServices() []resolver.Address {
function NewServiceDiscovery (line 24) | func NewServiceDiscovery(endpoints []string) resolver.Builder {
FILE: 4-etcd-grpclb/etcdv3/register.go
type ServiceRegister (line 12) | type ServiceRegister struct
method putKeyWithLease (line 46) | func (s *ServiceRegister) putKeyWithLease(lease int64) error {
method ListenLeaseRespChan (line 70) | func (s *ServiceRegister) ListenLeaseRespChan() {
method Close (line 78) | func (s *ServiceRegister) Close() error {
function NewServiceRegister (line 22) | func NewServiceRegister(endpoints []string, serName, addr string, lease ...
FILE: 4-etcd-grpclb/proto/simple.pb.go
constant _ (line 25) | _ = proto.ProtoPackageIsVersion3
type SimpleRequest (line 28) | type SimpleRequest struct
method Reset (line 37) | func (m *SimpleRequest) Reset() { *m = SimpleRequest{} }
method String (line 38) | func (m *SimpleRequest) String() string { return proto.CompactTextStri...
method ProtoMessage (line 39) | func (*SimpleRequest) ProtoMessage() {}
method Descriptor (line 40) | func (*SimpleRequest) Descriptor() ([]byte, []int) {
method XXX_Unmarshal (line 44) | func (m *SimpleRequest) XXX_Unmarshal(b []byte) error {
method XXX_Marshal (line 47) | func (m *SimpleRequest) XXX_Marshal(b []byte, deterministic bool) ([]b...
method XXX_Merge (line 50) | func (m *SimpleRequest) XXX_Merge(src proto.Message) {
method XXX_Size (line 53) | func (m *SimpleRequest) XXX_Size() int {
method XXX_DiscardUnknown (line 56) | func (m *SimpleRequest) XXX_DiscardUnknown() {
method GetData (line 62) | func (m *SimpleRequest) GetData() string {
type SimpleResponse (line 70) | type SimpleResponse struct
method Reset (line 80) | func (m *SimpleResponse) Reset() { *m = SimpleResponse{} }
method String (line 81) | func (m *SimpleResponse) String() string { return proto.CompactTextStr...
method ProtoMessage (line 82) | func (*SimpleResponse) ProtoMessage() {}
method Descriptor (line 83) | func (*SimpleResponse) Descriptor() ([]byte, []int) {
method XXX_Unmarshal (line 87) | func (m *SimpleResponse) XXX_Unmarshal(b []byte) error {
method XXX_Marshal (line 90) | func (m *SimpleResponse) XXX_Marshal(b []byte, deterministic bool) ([]...
method XXX_Merge (line 93) | func (m *SimpleResponse) XXX_Merge(src proto.Message) {
method XXX_Size (line 96) | func (m *SimpleResponse) XXX_Size() int {
method XXX_DiscardUnknown (line 99) | func (m *SimpleResponse) XXX_DiscardUnknown() {
method GetCode (line 105) | func (m *SimpleResponse) GetCode() int32 {
method GetValue (line 112) | func (m *SimpleResponse) GetValue() string {
function init (line 119) | func init() {
function init (line 124) | func init() { proto.RegisterFile("2-simple_rpc/proto/simple.proto", file...
constant _ (line 146) | _ = grpc.SupportPackageIsVersion4
type SimpleClient (line 151) | type SimpleClient interface
type simpleClient (line 155) | type simpleClient struct
method Route (line 163) | func (c *simpleClient) Route(ctx context.Context, in *SimpleRequest, o...
function NewSimpleClient (line 159) | func NewSimpleClient(cc *grpc.ClientConn) SimpleClient {
type SimpleServer (line 173) | type SimpleServer interface
type UnimplementedSimpleServer (line 178) | type UnimplementedSimpleServer struct
method Route (line 181) | func (*UnimplementedSimpleServer) Route(ctx context.Context, req *Simp...
function RegisterSimpleServer (line 185) | func RegisterSimpleServer(s *grpc.Server, srv SimpleServer) {
function _Simple_Route_Handler (line 189) | func _Simple_Route_Handler(srv interface{}, ctx context.Context, dec fun...
FILE: 4-etcd-grpclb/server/server.go
type SimpleService (line 15) | type SimpleService struct
method Route (line 54) | func (s *SimpleService) Route(ctx context.Context, req *pb.SimpleReque...
constant Address (line 19) | Address string = "localhost:8000"
constant Network (line 21) | Network string = "tcp"
constant SerName (line 23) | SerName string = "simple_grpc"
function main (line 29) | func main() {
FILE: 5-etcd-grpclb-balancer/balancer/weight/weight.go
constant Name (line 15) | Name = "weight"
type attributeKey (line 24) | type attributeKey struct
type AddrInfo (line 27) | type AddrInfo struct
function SetAddrInfo (line 33) | func SetAddrInfo(addr resolver.Address, addrInfo AddrInfo) resolver.Addr...
function GetAddrInfo (line 40) | func GetAddrInfo(addr resolver.Address) AddrInfo {
function newBuilder (line 47) | func newBuilder() balancer.Builder {
function init (line 51) | func init() {
type rrPickerBuilder (line 55) | type rrPickerBuilder struct
method Build (line 57) | func (*rrPickerBuilder) Build(info base.PickerBuildInfo) balancer.V2Pi...
type rrPicker (line 79) | type rrPicker struct
method Pick (line 88) | func (p *rrPicker) Pick(balancer.PickInfo) (balancer.PickResult, error) {
FILE: 5-etcd-grpclb-balancer/client/client.go
function main (line 25) | func main() {
function route (line 49) | func route(i int) {
FILE: 5-etcd-grpclb-balancer/etcdv3/discovery.go
constant schema (line 18) | schema = "grpclb"
type ServiceDiscovery (line 21) | type ServiceDiscovery struct
method Build (line 44) | func (s *ServiceDiscovery) Build(target resolver.Target, cc resolver.C...
method ResolveNow (line 64) | func (s *ServiceDiscovery) ResolveNow(rn resolver.ResolveNowOption) {
method Scheme (line 69) | func (s *ServiceDiscovery) Scheme() string {
method Close (line 74) | func (s *ServiceDiscovery) Close() {
method watcher (line 80) | func (s *ServiceDiscovery) watcher() {
method SetServiceList (line 96) | func (s *ServiceDiscovery) SetServiceList(key, val string) {
method DelServiceList (line 113) | func (s *ServiceDiscovery) DelServiceList(key string) {
method getServices (line 120) | func (s *ServiceDiscovery) getServices() []resolver.Address {
function NewServiceDiscovery (line 29) | func NewServiceDiscovery(endpoints []string) resolver.Builder {
FILE: 5-etcd-grpclb-balancer/etcdv3/register.go
type ServiceRegister (line 12) | type ServiceRegister struct
method putKeyWithLease (line 46) | func (s *ServiceRegister) putKeyWithLease(lease int64) error {
method ListenLeaseRespChan (line 70) | func (s *ServiceRegister) ListenLeaseRespChan() {
method Close (line 78) | func (s *ServiceRegister) Close() error {
function NewServiceRegister (line 22) | func NewServiceRegister(endpoints []string, addr, weigit string, lease i...
FILE: 5-etcd-grpclb-balancer/proto/simple.pb.go
constant _ (line 25) | _ = proto.ProtoPackageIsVersion3
type SimpleRequest (line 28) | type SimpleRequest struct
method Reset (line 37) | func (m *SimpleRequest) Reset() { *m = SimpleRequest{} }
method String (line 38) | func (m *SimpleRequest) String() string { return proto.CompactTextStri...
method ProtoMessage (line 39) | func (*SimpleRequest) ProtoMessage() {}
method Descriptor (line 40) | func (*SimpleRequest) Descriptor() ([]byte, []int) {
method XXX_Unmarshal (line 44) | func (m *SimpleRequest) XXX_Unmarshal(b []byte) error {
method XXX_Marshal (line 47) | func (m *SimpleRequest) XXX_Marshal(b []byte, deterministic bool) ([]b...
method XXX_Merge (line 50) | func (m *SimpleRequest) XXX_Merge(src proto.Message) {
method XXX_Size (line 53) | func (m *SimpleRequest) XXX_Size() int {
method XXX_DiscardUnknown (line 56) | func (m *SimpleRequest) XXX_DiscardUnknown() {
method GetData (line 62) | func (m *SimpleRequest) GetData() string {
type SimpleResponse (line 70) | type SimpleResponse struct
method Reset (line 80) | func (m *SimpleResponse) Reset() { *m = SimpleResponse{} }
method String (line 81) | func (m *SimpleResponse) String() string { return proto.CompactTextStr...
method ProtoMessage (line 82) | func (*SimpleResponse) ProtoMessage() {}
method Descriptor (line 83) | func (*SimpleResponse) Descriptor() ([]byte, []int) {
method XXX_Unmarshal (line 87) | func (m *SimpleResponse) XXX_Unmarshal(b []byte) error {
method XXX_Marshal (line 90) | func (m *SimpleResponse) XXX_Marshal(b []byte, deterministic bool) ([]...
method XXX_Merge (line 93) | func (m *SimpleResponse) XXX_Merge(src proto.Message) {
method XXX_Size (line 96) | func (m *SimpleResponse) XXX_Size() int {
method XXX_DiscardUnknown (line 99) | func (m *SimpleResponse) XXX_DiscardUnknown() {
method GetCode (line 105) | func (m *SimpleResponse) GetCode() int32 {
method GetValue (line 112) | func (m *SimpleResponse) GetValue() string {
function init (line 119) | func init() {
function init (line 124) | func init() { proto.RegisterFile("2-simple_rpc/proto/simple.proto", file...
constant _ (line 146) | _ = grpc.SupportPackageIsVersion4
type SimpleClient (line 151) | type SimpleClient interface
type simpleClient (line 155) | type simpleClient struct
method Route (line 163) | func (c *simpleClient) Route(ctx context.Context, in *SimpleRequest, o...
function NewSimpleClient (line 159) | func NewSimpleClient(cc *grpc.ClientConn) SimpleClient {
type SimpleServer (line 173) | type SimpleServer interface
type UnimplementedSimpleServer (line 178) | type UnimplementedSimpleServer struct
method Route (line 181) | func (*UnimplementedSimpleServer) Route(ctx context.Context, req *Simp...
function RegisterSimpleServer (line 185) | func RegisterSimpleServer(s *grpc.Server, srv SimpleServer) {
function _Simple_Route_Handler (line 189) | func _Simple_Route_Handler(srv interface{}, ctx context.Context, dec fun...
FILE: 5-etcd-grpclb-balancer/server/server.go
type SimpleService (line 15) | type SimpleService struct
method Route (line 54) | func (s *SimpleService) Route(ctx context.Context, req *pb.SimpleReque...
constant Address (line 19) | Address string = "localhost:8000"
constant Network (line 21) | Network string = "tcp"
constant SerName (line 23) | SerName string = "simple_grpc"
function main (line 29) | func main() {
FILE: 6-etcd-mutex/example_mutex_test.go
function ExampleMutex_Lock (line 26) | func ExampleMutex_Lock() {
Condensed preview — 36 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (153K chars).
[
{
"path": ".vscode/settings.json",
"chars": 31,
"preview": "{\n \"go.inferGopath\": false\n}"
},
{
"path": "1-etcd环境安装与使用/README.md",
"chars": 6890,
"preview": "### etcd简介\n[etcd](https://github.com/etcd-io/etcd)是开源的、高可用的分布式key-value存储系统,可用于配置共享和服务的注册和发现,它专注于:\n\n* 简单:定义清晰、面向用户的API(g"
},
{
"path": "2-etcd-go-client/README.md",
"chars": 57,
"preview": "代码来源:https://github.com/etcd-io/etcd/tree/master/clientv3"
},
{
"path": "2-etcd-go-client/example_auth_test.go",
"chars": 2814,
"preview": "// Copyright 2016 The etcd Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not "
},
{
"path": "2-etcd-go-client/example_cluster_test.go",
"chars": 2326,
"preview": "// Copyright 2016 The etcd Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not "
},
{
"path": "2-etcd-go-client/example_kv_test.go",
"chars": 6405,
"preview": "// Copyright 2016 The etcd Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not "
},
{
"path": "2-etcd-go-client/example_lease_test.go",
"chars": 3005,
"preview": "// Copyright 2016 The etcd Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not "
},
{
"path": "2-etcd-go-client/example_maintenence_test.go",
"chars": 1575,
"preview": "// Copyright 2016 The etcd Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not "
},
{
"path": "2-etcd-go-client/example_metrics_test.go",
"chars": 2201,
"preview": "// Copyright 2016 The etcd Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not "
},
{
"path": "2-etcd-go-client/example_test.go",
"chars": 1958,
"preview": "// Copyright 2016 The etcd Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not "
},
{
"path": "2-etcd-go-client/example_watch_test.go",
"chars": 2524,
"preview": "// Copyright 2016 The etcd Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not "
},
{
"path": "3-etcd-service-discovery/.vscode/launch.json",
"chars": 394,
"preview": "{\n // 使用 IntelliSense 了解相关属性。 \n // 悬停以查看现有属性的描述。\n // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=83038"
},
{
"path": "3-etcd-service-discovery/README.md",
"chars": 6187,
"preview": "### etcd实现服务发现\n\n### 前言\n\n[etcd环境安装与使用](https://bingjian-zhu.github.io/2020/05/09/etcd%E7%8E%AF%E5%A2%83%E5%AE%89%E8%A3%85"
},
{
"path": "3-etcd-service-discovery/discovery/discovery.go",
"chars": 2215,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/coreos/etcd/mvcc/mvccpb\"\n\t\"go.etcd.io/etcd/client"
},
{
"path": "3-etcd-service-discovery/register/register.go",
"chars": 2032,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"time\"\n\n\t\"go.etcd.io/etcd/clientv3\"\n)\n\n//ServiceRegister 创建租约注册服务\ntype Service"
},
{
"path": "4-etcd-grpclb/README.md",
"chars": 9819,
"preview": "### gRPC负载均衡(客户端负载均衡)\n\n### 前言\n[上篇](https://bingjian-zhu.github.io/2020/05/14/etcd%E5%AE%9E%E7%8E%B0%E6%9C%8D%E5%8A%A1%E5"
},
{
"path": "4-etcd-grpclb/client/client.go",
"chars": 1156,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/r"
},
{
"path": "4-etcd-grpclb/etcdv3/discovery.go",
"chars": 2696,
"preview": "package etcdv3\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/coreos/etcd/mvcc/mvccpb\"\n\t\"go.etcd.io/etcd/clie"
},
{
"path": "4-etcd-grpclb/etcdv3/register.go",
"chars": 1785,
"preview": "package etcdv3\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"time\"\n\n\t\"go.etcd.io/etcd/clientv3\"\n)\n\n//ServiceRegister 创建租约注册服务\ntype Servi"
},
{
"path": "4-etcd-grpclb/proto/simple.pb.go",
"chars": 7418,
"preview": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// source: 2-simple_rpc/proto/simple.proto\n\npackage proto\n\nimport (\n\tco"
},
{
"path": "4-etcd-grpclb/proto/simple.proto",
"chars": 406,
"preview": "syntax = \"proto3\";// 协议为proto3\n\npackage proto;\n\n// 定义发送请求信息\nmessage SimpleRequest{\n // 定义发送的参数,采用驼峰命名方式,小写加下划线,如:stud"
},
{
"path": "4-etcd-grpclb/server/server.go",
"chars": 1320,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"net\"\n\n\t\"google.golang.org/grpc\"\n\n\t\"etcd-example/4-etcd-grpclb/etcdv3\"\n\tpb \"et"
},
{
"path": "5-etcd-grpclb-balancer/README.md",
"chars": 5103,
"preview": "### gRPC负载均衡(自定义负载均衡策略)\n\n### 前言\n上篇文章介绍了如何实现gRPC负载均衡,但目前官方只提供了`pick_first`和`round_robin`两种负载均衡策略,轮询法`round_robin`不能满足因服务器"
},
{
"path": "5-etcd-grpclb-balancer/balancer/weight/weight.go",
"chars": 2431,
"preview": "package weight\n\nimport (\n\t\"math/rand\"\n\t\"sync\"\n\n\t\"google.golang.org/grpc/attributes\"\n\t\"google.golang.org/grpc/balancer\"\n\t"
},
{
"path": "5-etcd-grpclb-balancer/client/client.go",
"chars": 1169,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/r"
},
{
"path": "5-etcd-grpclb-balancer/etcdv3/discovery.go",
"chars": 3056,
"preview": "package etcdv3\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"etcd-example/5-etcd-grpclb-balancer/"
},
{
"path": "5-etcd-grpclb-balancer/etcdv3/register.go",
"chars": 1788,
"preview": "package etcdv3\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"time\"\n\n\t\"go.etcd.io/etcd/clientv3\"\n)\n\n//ServiceRegister 创建租约注册服务\ntype Servi"
},
{
"path": "5-etcd-grpclb-balancer/proto/simple.pb.go",
"chars": 7418,
"preview": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// source: 2-simple_rpc/proto/simple.proto\n\npackage proto\n\nimport (\n\tco"
},
{
"path": "5-etcd-grpclb-balancer/proto/simple.proto",
"chars": 406,
"preview": "syntax = \"proto3\";// 协议为proto3\n\npackage proto;\n\n// 定义发送请求信息\nmessage SimpleRequest{\n // 定义发送的参数,采用驼峰命名方式,小写加下划线,如:stud"
},
{
"path": "5-etcd-grpclb-balancer/server/server.go",
"chars": 1346,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"net\"\n\n\t\"google.golang.org/grpc\"\n\n\t\"etcd-example/5-etcd-grpclb-balancer/etcdv3"
},
{
"path": "6-etcd-mutex/README.md",
"chars": 4868,
"preview": "### etcd分布式锁及事务\n\n### 前言\n\n`分布式锁`是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往"
},
{
"path": "6-etcd-mutex/example_mutex_test.go",
"chars": 1790,
"preview": "// Copyright 2017 The etcd Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not "
},
{
"path": "LICENSE",
"chars": 11357,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 423,
"preview": "### 教程\n\n* [etcd环境安装与使用](https://www.cnblogs.com/FireworksEasyCool/p/12858570.html)\n\n* [etcd实现服务发现](https://www.cnblogs.c"
},
{
"path": "go.mod",
"chars": 918,
"preview": "module etcd-example\n\ngo 1.13\n\nrequire (\n\tgithub.com/coreos/etcd v3.3.20+incompatible\n\tgithub.com/coreos/go-semver v0.3.0"
},
{
"path": "go.sum",
"chars": 19748,
"preview": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ngithub.com/BurntSushi/toml v0.3.1/go."
}
]
About this extraction
This page contains the full source code of the Bingjian-Zhu/etcd-example GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 36 files (124.1 KB), approximately 46.5k tokens, and a symbol index with 174 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.