`
October 23, 2018 本文阅读量

etcd与service-registration-discovery

本文对etcd的原理,实现细节,性能等均不考虑,仅将etcd作为一个分布式的K-V存储组件。

声明:本文对etcd的原理,实现细节,性能等均不考虑,仅将etcd作为一个分布式的K-V存储组件。本文提价代码均在: github.com/yeqown/server-common/tree/master/framework/etcd

一个核心

etcd, 分布式Key-Value存储工具。详细资料由此去

两个对象

  • 服务提供者(在测试环境中,我定义为单独的服务实例),也就是服务的提供者,需要向其他服务暴露自己的ip和端口,方便调用。
  • 服务调用者(同样地,在测试环境中我定义为反向代理网关程序),也就是服务的调用者,需要获取到 可使用 地服务地址并调用。

关于服务注册与发现

就具体场景而言:我们的生产环境中使用了一个代理网关服务器,用于转发移动端和PC端的API请求,并完成其他功能。所有的服务实例配置都是硬编码在网关程序中,顶多就是抽离出来成了一个配置文件。这样做的缺点很明显:“非动态”。也就意味着,一旦有服务Down掉,那么用户访问则可能异常,甚至导致整个服务的崩溃;其次,需要对服务进行扩容的情况下,则需要先进行服务部署再更新网关程序,步骤繁琐且容易出错。

那么如果我们设计成为如下图的样子: etcd-service-regisration-discovery 对于新添加的服务实例,只需要启动新的服务,并注册到etcd相应的路径下就行了。

注册:对于同一组服务,配置一个统一的前缀(如图上的"/specServer"),不同实例使用ID加以区分。

将现行服务改造成为上述模式需要解决的问题:

  • etcd 配置安装
  • 网关程序改造(监听etcd的节点夹子/prefix;适配动态的服务实例调用)
  • 服务实例改造(注册服务实例到etcd;心跳更新;其他配套设施,异常退出删除注册信息)

etcd安装配置在github.com已经非常详细了。在这里贴一下我在本地测试时候启动的脚本(这部分是从etcd-demo获取到的,做了针对端口的改动):

#!/bin/bash

# For each machine
TOKEN=token-01
CLUSTER_STATE=new
NAME_1=machine1
NAME_2=machine2
NAME_3=machine3
HOST_1=127.0.0.1
HOST_2=127.0.0.1
HOST_3=127.0.0.1
CLUSTER=${NAME_1}=http://${HOST_1}:2380,${NAME_2}=http://${HOST_2}:2381,${NAME_3}=http://${HOST_3}:2382

# For machine 1
THIS_NAME=${NAME_1}
THIS_IP=${HOST_1}
etcd --data-dir=machine1.etcd --name ${THIS_NAME} \
	--initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://${THIS_IP}:2380 \
	--advertise-client-urls http://${THIS_IP}:2377 --listen-client-urls http://${THIS_IP}:2377 \
	--initial-cluster ${CLUSTER} \
	--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN} &

# For machine 2
THIS_NAME=${NAME_2}
THIS_IP=${HOST_2}
etcd --data-dir=machine2.etcd --name ${THIS_NAME} \
	--initial-advertise-peer-urls http://${THIS_IP}:2381 --listen-peer-urls http://${THIS_IP}:2381 \
	--advertise-client-urls http://${THIS_IP}:2378 --listen-client-urls http://${THIS_IP}:2378 \
	--initial-cluster ${CLUSTER} \
	--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN} & 

# For machine 3
THIS_NAME=${NAME_3}
THIS_IP=${HOST_3}
etcd --data-dir=machine3.etcd --name ${THIS_NAME} \
	--initial-advertise-peer-urls http://${THIS_IP}:2382 --listen-peer-urls http://${THIS_IP}:2382 \
	--advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://${THIS_IP}:2379 \
	--initial-cluster ${CLUSTER} \
	--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN} &

对于程序的改造,鉴于服务较多且etcd操作流程大体一致,便简单包装了一下,项目地址见文首位置。

1.对于调用方使用示例如下:

// etcdtest/gw.go

func main() {
    // ...
    endpoints := []string{
        "http://127.0.0.1:2377",
        "http://127.0.0.1:2379",
        "http://127.0.0.1:2378",
    }
    // 连接etcd获取KeysAPI
    kapi, err := etcd.Connect(endpoints...)
    if err != nil {
        fmt.Println(err)
        os.Exit(2)
    }

    // debug more, more log ~
    etcd.OpenDebug(true)

    // etcd watch, 监听/prefix目录下的改动(“expire;set;update;delete”)
    // 如:set {Key: /prefix/srv_3457, CreatedIndex: 1155, ModifiedIndex: 1155, TTL: 12}
    // 并更新watcher.members, 维持最新的节点状态和数量
    watcher = etcd.NewWatcher(kapi, "prefix")
    go watcher.Watch()
    // ...
}

func ServeHTTP() {
    // ...
    srvs := watcher.RangeMember() // 获取所有可用的服务节点
    // ...
}

2.对于请求提供方,使用示例如下:

// etcdtest/server.go

func main() {
    // ...
    endpoints := []string{
        "http://127.0.0.1:2377",
        "http://127.0.0.1:2379",
        "http://127.0.0.1:2378",
    }
    etcd.OpenDebug(true)
    kapi, err := etcd.Connect(endpoints...)
    if err != nil {
        fmt.Errorf(err.Error())
        os.Exit(2)
    }

    // 根据服务生成一个provider, 用于生成K:V
    provider := etcd.NewProvider(
        fmt.Sprintf("srv_%d", *port),              // name
        fmt.Sprintf("http://127.0.0.1:%d", *port), // addr
    )

    ctx, cancel := context.WithCancel(context.Background())
    // 每10s设置一个TTL=12s的 “/prefix/id”:“http://host:port” 的的键值对
    // 10s和12s是写死的,没有考虑动态~~,后续考虑升级,目前仅仅是测试。
    go provider.Heartbeat(ctx, kapi, &etcd.ProvideOptions{
        NamePrefix: "prefix",
        SetOpts:    nil,
    })
    //...
}

关于详细的代码,可以参见:

测试

dplayer "url=/mov/etcd-example-video.mov" "loop=no" "theme=#FADFA3" "autoplay=false" "token=tokendemo"

总结

代码包装得比较粗糙,视频演示还没有包含到服务异常(退出)之后网关程序的应对(这部分是已经完成,只是没有演示)。