声明:本文对etcd的原理,实现细节,性能等均不考虑,仅将etcd作为一个分布式的K-V存储组件。本文提价代码均在: github.com/yeqown/server-common/tree/master/framework/etcd
一个核心 #
etcd, 分布式Key-Value存储工具。详细资料由此去
两个对象 #
- 服务提供者(在测试环境中,我定义为单独的服务实例),也就是服务的提供者,需要向其他服务暴露自己的ip和端口,方便调用。
- 服务调用者(同样地,在测试环境中我定义为反向代理网关程序),也就是服务的调用者,需要获取到 可使用 地服务地址并调用。
关于服务注册与发现 #
就具体场景而言:我们的生产环境中使用了一个代理网关服务器,用于转发移动端和PC端的API请求,并完成其他功能。所有的服务实例配置都是硬编码在网关程序中,顶多就是抽离出来成了一个配置文件。这样做的缺点很明显:“非动态”。也就意味着,一旦有服务Down掉,那么用户访问则可能异常,甚至导致整个服务的崩溃;其次,需要对服务进行扩容的情况下,则需要先进行服务部署再更新网关程序,步骤繁琐且容易出错。
那么如果我们设计成为如下图的样子: 对于新添加的服务实例,只需要启动新的服务,并注册到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"
总结 #
代码包装得比较粗糙,视频演示还没有包含到服务异常(退出)之后网关程序的应对(这部分是已经完成,只是没有演示)。