最近尝试了一下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
   }
 }