最近尝试了一下etcd来做服务的注册发现
【etcd服务】
从etcd官网下载二进制文件即可,分配了三台机器做集群
10.0.1.98 etcd-001
10.0.1.205 etcd-002
10.0.1.182 etcd-003
然后用脚本启动服务
etcd --name etcd-002 --initial-advertise-peer-urls http://10.0.1.205:2380 --listen-peer-urls http://10.0.1.205:2380 --listen-client-urls http://10.0.1.205:2379,http://127.0.0.1:2379 --advertise-client-urls http://10.0.1.205:2379 --initial-cluster-token etcd-cluster --initial-cluster etcd-001=http://10.0.1.98:2380,etcd-002=http://10.0.1.205:2380,etcd-003=http://10.0.1.182:2380 --initial-cluster-state new
etcd --name etcd-001 --initial-advertise-peer-urls http://10.0.1.98:2380 --listen-peer-urls http://10.0.1.98:2380 --listen-client-urls http://10.0.1.98:2379,http://127.0.0.1:2379 --advertise-client-urls http://10.0.1.98:2379 --initial-cluster-token etcd-cluster --initial-cluster etcd-001=http://10.0.1.98:2380,etcd-002=http://10.0.1.205:2380,etcd-003=http://10.0.1.182:2380 --initial-cluster-state new
etcd --name etcd-003 --initial-advertise-peer-urls http://10.0.1.182:2380 --listen-peer-urls http://10.0.1.182:2380 --listen-client-urls http://10.0.1.182:2379,http://127.0.0.1:2379 --advertise-client-urls http://10.0.1.182:2379 --initial-cluster-token etcd-cluster --initial-cluster etcd-001=http://10.0.1.98:2380,etcd-002=http://10.0.1.205:2380,etcd-003=http://10.0.1.182:2380 --initial-cluster-state new
即可。
【服务发布】
etcd和zk不一样,他自身没有临时节点,需要客户端自己来实现。实现的大概逻辑是这样的:
设置一个一段时间超时的节点,比如60秒超时,如果超时了etcd上就找不到这个节点,
然后客户端用一个更小的时间间隔刷新这个节点的超时时间,比如每隔40秒刷新一次,重新把ttl设置成60秒。这样就可以保证在etcd上只要服务存活节点就一定存在,当服务关掉的时候,节点过一阵就消失了。
当然,如果能探测到服务关闭发一个del给etcd主动删除节点就更完美了。
在maven中添加依赖
<dependency>
<groupId>org.mousio</groupId>
<artifactId>etcd4j</artifactId>
<version>2.13.0</version>
</dependency>
以spring boot上为例
package com.seeplant.etcd;
import java.io.IOException;
import java.net.URI;
import java.util.concurrent.TimeoutException;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import com.seeplant.util.Property;
import com.seeplant.util.ServerLogger;
import mousio.client.retry.RetryOnce;
import mousio.etcd4j.EtcdClient;
import mousio.etcd4j.promises.EtcdResponsePromise;
import mousio.etcd4j.responses.EtcdAuthenticationException;
import mousio.etcd4j.responses.EtcdException;
import mousio.etcd4j.responses.EtcdKeysResponse;
@Component
@Scope("singleton")
public class EtcdUtil {
private EtcdClient client;
private final String serverName = Property.getProperty("serverName"); // 自定义的服务名字,我定义成roomServer
private final String dirString = "/roomServerList";
private final String zoneId = Property.getProperty("zeonId"); // 自定义的一个标识,我定义成1
private final String etcdKey = dirString + "/" + zoneId + "/" + serverName; // 这里就是发布的节点
public EtcdUtil() {
int nodeCount = Integer.parseInt(Property.getProperty("etcdGroupNodeCount"));
URI[] uris = new URI[nodeCount]; // 对于集群,把所有集群节点地址加进来,etcd的代码里会轮询这些地址来发布节点,直到成功
for (int iter = 0; iter < nodeCount; iter++) {
String urlString = Property.getProperty("etcdHost" + new Integer(iter).toString());
System.out.println(urlString);
uris[iter] = URI.create(urlString);
}
client = new EtcdClient(uris);
client.setRetryHandler(new RetryOnce(20)); //retry策略
}
public void regist() { // 注册节点,放在程序启动的入口
try { // 用put方法发布一个节点
EtcdResponsePromise<EtcdKeysResponse> p = client
.putDir(etcdKey + "_" + Property.getProperty("serverIp") + "_" + Property.getProperty("serverPort"))
.ttl(60).send();
p.get(); // 加上这个get()用来保证设置完成,走下一步,get会阻塞,由上面client的retry策略决定阻塞的方式
new Thread(new GuardEtcd()).start(); // 启动一个守护线程来定时刷新节点
} catch (Exception e) {
// TODO: handle exception
ServerLogger.log("etcd Server not available.");
}
}
public void destory() {
try {
EtcdResponsePromise<EtcdKeysResponse> p = client
.deleteDir(
etcdKey + "_" + Property.getProperty("serverIp") + "_" + Property.getProperty("serverPort"))
.recursive().send();
p.get();
client.close();
} catch (IOException | EtcdException | EtcdAuthenticationException | TimeoutException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private class GuardEtcd implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
try {
Thread.sleep(40*1000l);
client.refresh(
etcdKey + "_" + Property.getProperty("serverIp") + "_" + Property.getProperty("serverPort"),
60).send();
} catch (IOException | InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
对于spring boot框架来说,可以实现CommandLineRunner来完成加载和销毁的主动调用
package com.seeplant;
import javax.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import com.seeplant.etcd.EtcdUtil;
import com.seeplant.netty.RoomServer;
import com.seeplant.util.Property;
import io.netty.channel.ChannelFuture;
@SpringBootApplication
public class App implements CommandLineRunner{
@Autowired
private RoomServer roomServer;
@Autowired
private EtcdUtil etcdUtil;
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
@Bean
public RoomServer roomServer() {
return new RoomServer(Integer.parseInt(Property.getProperty("serverPort")));
}
@Override
public void run(String... args) throws Exception {
etcdUtil.regist();
ChannelFuture future = roomServer.start();
Runtime.getRuntime().addShutdownHook(new Thread(){
@Override
public void run() {
roomServer.destroy();
}
});
future.channel().closeFuture().syncUninterruptibly();
}
@PreDestroy
public void destory() {
etcdUtil.destory();
}
}
以上的retry策略是尝试一次就放弃,另外还写了一个retry策略是等待时间线性增长策略,仅供参考
package com.seeplant.etcd;
import com.seeplant.util.ServerLogger;
import mousio.client.ConnectionState;
import mousio.client.retry.RetryPolicy;
/**
* 为Etcd提供倍增的重试策略
* @author yuantao
*
*/
public class RetryDoublePolicy extends RetryPolicy {
private final int timeSlot; // 步长
private int stepTimes = 1; // 倍数
public RetryDoublePolicy(int startRetryTime) {
super(startRetryTime);
// TODO Auto-generated constructor stub
this.timeSlot = startRetryTime;
}
@Override
public boolean shouldRetry(ConnectionState connectionState) {
// TODO Auto-generated method stub
stepTimes *= 2;
connectionState.msBeforeRetry = stepTimes*timeSlot; // 重设时间
System.out.println("try " + stepTimes);
if (stepTimes > 128) {
ServerLogger.log("etcd connection failed");
stepTimes = 1;
}
return true;
}
}
以上是注册服务,对于服务管理、负载均衡方,只需要按照http://127.0.0.1:2379/v2/keys/roomServerList/1/ 这种方式get就可以得到节点内容。比如我的服务发布的节点通过get返回的内容如下:
{
"action": "get",
"node": {
"key": "/roomServerList/1",
"dir": true,
"nodes": [
{
"key": "/roomServerList/1/roomServer0_127.0.0.1_9090",
"dir": true,
"expiration": "2017-09-04T05:10:21.214199005Z",
"ttl": 42,
"modifiedIndex": 202,
"createdIndex": 202
}
],
"modifiedIndex": 36,
"createdIndex": 36
}
}