etcd是CoreOS团队于2013年6月发起的开源项目,它的目标是构建一个高可用的分布式键值(key-value)数据库。etcd内部采用raft
协议作为一致性算法,etcd基于Go语言实现。
etcd作为服务发现系统,有以下的特点:
etcd项目地址:https://github.com/coreos/etcd/
version: '2.2'
services:
etcd:
image: quay.io/coreos/etcd:v3.3.12
container_name: etcd
ports:
- 23791:2379
- 2380
environment:
ETCDCTL_API: 3
volumes:
- ./data/etcd/etcd-data:/etcd-data
command:
- "/usr/local/bin/etcd"
- "--name"
- "s1"
- "--data-dir"
- "/etcd-data"
- "--advertise-client-urls"
- "http://0.0.0.0:2379"
- --listen-client-urls
- "http://0.0.0.0:2379"
- "--initial-advertise-peer-urls"
- "http://0.0.0.0:2380"
- "--listen-peer-urls"
- "http://0.0.0.0:2380"
- "--initial-cluster-token"
- "tkn"
- "--initial-cluster"
- "s1=http://0.0.0.0:2380"
- "--initial-cluster-state"
- "new"
docker exec -it etcd bash
## 获取版本信息
etcdctl version
## 获取所有键值对
etcdctl get --prefix ""
## 添加键值对
etcdctl put zhangsan hello
## 删除键值对
etcdctl del zhangsan
## 添加一个过期时间为20s的租约
etcdctl lease grant 20
## 获取所有租约
etcdctl lease list
## 添加键值对,并为该键指定租约
etcdctl put lisi world --lease="3f3574057fe0e61c"
## 查看某个租约的keepalived时间
etcdctl lease keep-alive 3f3574057fe0e61c
## 续租
etcdctl lease timetolive 3f3574057fe0e61c --keys
## 回收租约
etcdctl lease revoke 3f3574057fe0e61c
## 获取版本信息
curl -L http://127.0.0.1:2379/version
## 获取健康状态
curl -L http://127.0.0.1:2379/health
## 添加键值对
curl http://127.0.0.1:2379/v2/keys/zhangsan -XPUT -d value="hello"
## 查看键值对
curl http://127.0.0.1:2379/v2/keys/zhangsan
我们试图使用go开发etcd sdk,启动两个简单的server端,向etcd分别注册自己的address,再启动一个client端,从etcd中发现服务,随机抽取一个进行访问请求。
采坑指南:go mod管理的时候,导入etcd相关sdk包会出现版本冲突问题,踩了很多坑之后,总结了一下,需要在go.mod文件中,添加如下三行replace
replace (
github.com/coreos/bbolt v1.3.4 => go.etcd.io/bbolt v1.3.4
github.com/coreos/go-systemd => github.com/coreos/go-systemd/v22 v22.0.0
google.golang.org/grpc => google.golang.org/grpc v1.26.0
)
package main
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"go.etcd.io/etcd/clientv3"
"net/http"
"sync"
"time"
)
const (
EtcdPrefix = "/test/server/"
ServerSerial = "1"
Address = "http://127.0.0.1:18081/"
)
var (
EtcdAddress = []string{
"http://127.0.0.1:23791"}
leaseTTL = 5
)
type HealthProvider struct {
etcdClient *EtcdClient
}
var (
healthProvider *HealthProvider
healthProviderOnce sync.Once
)
func GetHealthProvider() *HealthProvider {
healthProviderOnce.Do(func() {
healthProvider = &HealthProvider{
etcdClient: NewEtcdClient(),
}
})
return healthProvider
}
type EtcdClient struct {
address []string
username string
password string
kv clientv3.KV
client *clientv3.Client
ctx context.Context
lease clientv3.Lease
leaseID clientv3.LeaseID
leaseTTL int64
}
func NewEtcdClient() *EtcdClient {
var client = &EtcdClient{
ctx: context.Background(),
address: EtcdAddress,
leaseTTL: int64(leaseTTL),
}
err := client.connect()
if err != nil {
panic(err)
}
return client
}
func (etcdClient *EtcdClient) connect() (err error) {
etcdClient.client, err = clientv3.New(clientv3.Config{
Endpoints: etcdClient.address,
DialTimeout: 5 * time.Second,
TLS: nil,
Username: etcdClient.username,
Password: etcdClient.password,
})
if err != nil {
return
}
etcdClient.kv = clientv3.NewKV(etcdClient.client)
etcdClient.ctx = context.Background()
return
}
func (etcdClient *EtcdClient) Close() (err error) {
return etcdClient.client.Close()
}
func (etcdClient *EtcdClient) register(address string) (*clientv3.PutResponse, error) {
etcdClient.lease = clientv3.NewLease(etcdClient.client)
leaseResp, err := etcdClient.lease.Grant(etcdClient.ctx, etcdClient.leaseTTL)
if err != nil {
return nil, err
}
etcdClient.leaseID = leaseResp.ID
return etcdClient.kv.Put(etcdClient.ctx, EtcdPrefix+ServerSerial, address, clientv3.WithLease(leaseResp.ID))
}
func (etcdClient *EtcdClient) LeaseKeepAlive() error {
if etcdClient.lease == nil {
_, err := etcdClient.register(Address)
if err != nil {
return err
}
}
_, err := etcdClient.lease.KeepAlive(etcdClient.ctx, etcdClient.leaseID)
if err != nil {
return err
}
return nil
}
func healthCheck(provider *HealthProvider) {
var tick = time.NewTicker(time.Second)
for {
select {
case <-tick.C:
err := provider.etcdClient.LeaseKeepAlive()
if err != nil {
fmt.Println(err.Error())
return
}
}
}
}
func main() {
provider := GetHealthProvider()
go healthCheck(provider)
defer provider.etcdClient.Close()
engine := gin.Default()
engine.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, "one")
})
engine.Run(":18081")
}
package main
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"go.etcd.io/etcd/clientv3"
"net/http"
"sync"
"time"
)
const (
EtcdPrefix = "/test/server/"
ServerSerial = "2"
Address = "http://127.0.0.1:18082/"
)
var (
EtcdAddress = []string{
"http://127.0.0.1:23791"}
leaseTTL = 5
)
type HealthProvider struct {
etcdClient *EtcdClient
}
var (
healthProvider *HealthProvider
healthProviderOnce sync.Once
)
func GetHealthProvider() *HealthProvider {
healthProviderOnce.Do(func() {
healthProvider = &HealthProvider{
etcdClient: NewEtcdClient(),
}
})
return healthProvider
}
type EtcdClient struct {
address []string
username string
password string
kv clientv3.KV
client *clientv3.Client
ctx context.Context
lease clientv3.Lease
leaseID clientv3.LeaseID
leaseTTL int64
}
func NewEtcdClient() *EtcdClient {
var client = &EtcdClient{
ctx: context.Background(),
address: EtcdAddress,
leaseTTL: int64(leaseTTL),
}
err := client.connect()
if err != nil {
panic(err)
}
return client
}
func (etcdClient *EtcdClient) connect() (err error) {
etcdClient.client, err = clientv3.New(clientv3.Config{
Endpoints: etcdClient.address,
DialTimeout: 5 * time.Second,
TLS: nil,
Username: etcdClient.username,
Password: etcdClient.password,
})
if err != nil {
return
}
etcdClient.kv = clientv3.NewKV(etcdClient.client)
etcdClient.ctx = context.Background()
return
}
func (etcdClient *EtcdClient) Close() (err error) {
return etcdClient.client.Close()
}
func (etcdClient *EtcdClient) register(address string) (*clientv3.PutResponse, error) {
etcdClient.lease = clientv3.NewLease(etcdClient.client)
leaseResp, err := etcdClient.lease.Grant(etcdClient.ctx, etcdClient.leaseTTL)
if err != nil {
return nil, err
}
etcdClient.leaseID = leaseResp.ID
return etcdClient.kv.Put(etcdClient.ctx, EtcdPrefix+ServerSerial, address, clientv3.WithLease(leaseResp.ID))
}
func (etcdClient *EtcdClient) LeaseKeepAlive() error {
if etcdClient.lease == nil {
_, err := etcdClient.register(Address)
if err != nil {
return err
}
}
_, err := etcdClient.lease.KeepAlive(etcdClient.ctx, etcdClient.leaseID)
if err != nil {
return err
}
return nil
}
func healthCheck(provider *HealthProvider) {
var tick = time.NewTicker(time.Second)
for {
select {
case <-tick.C:
err := provider.etcdClient.LeaseKeepAlive()
if err != nil {
fmt.Println(err.Error())
return
}
}
}
}
func main() {
provider := GetHealthProvider()
go healthCheck(provider)
defer provider.etcdClient.Close()
engine := gin.Default()
engine.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, "two")
})
engine.Run(":18082")
}
package main
import (
"context"
"fmt"
"github.com/coreos/etcd/clientv3"
"io/ioutil"
"math/rand"
"net/http"
"time"
)
var (
EtcdAddress = []string{
"http://127.0.0.1:23791"}
ServerPrefix = "/test/server/"
)
type EtcdClient struct {
address []string
username string
password string
kv clientv3.KV
client *clientv3.Client
ctx context.Context
lease clientv3.Lease
leaseID clientv3.LeaseID
}
func newEtcdClient() *EtcdClient {
var client = &EtcdClient{
ctx: context.Background(),
address: EtcdAddress,
}
err := client.connect()
if err != nil {
panic(err)
}
return client
}
func (etcdClient *EtcdClient) connect() (err error) {
etcdClient.client, err = clientv3.New(clientv3.Config{
Endpoints: etcdClient.address,
DialTimeout: 5 * time.Second,
TLS: nil,
Username: etcdClient.username,
Password: etcdClient.password,
})
if err != nil {
return
}
etcdClient.kv = clientv3.NewKV(etcdClient.client)
etcdClient.ctx = context.Background()
return
}
func (etcdClient *EtcdClient) list(prefix string) ([]string, error) {
resp, err := etcdClient.kv.Get(etcdClient.ctx, prefix, clientv3.WithPrefix())
if err != nil {
return nil, err
}
servers := make([]string, 0)
for _, value := range resp.Kvs {
if value != nil {
servers = append(servers, string(value.Value))
}
}
return servers, nil
}
func (etcdClient *EtcdClient) close() (err error) {
return etcdClient.client.Close()
}
func genRand(num int) int {
return int(rand.Int31n(int32(num)))
}
func getServer(client *EtcdClient) (string, error) {
servers, err := client.list(ServerPrefix)
if err != nil {
return "", err
}
return servers[genRand(len(servers))], nil
}
func Get(url string) ([]byte, error) {
client := &http.Client{
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
return body, nil
}
func main() {
client := newEtcdClient()
err := client.connect()
if err != nil {
panic(err)
}
defer client.close()
for i := 0; i < 10; i++ {
address, err := getServer(client)
if err != nil {
fmt.Println(err.Error())
return
}
data, err := Get(address + "ping")
if err != nil {
fmt.Println(err.Error())
return
}
fmt.Println(string(data))
time.Sleep(2 * time.Second)
}
}
会发现请求one_server和two_server的频率慢慢趋于平均