ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
官方下载
目前最新的稳定版本是3.4.10,压缩包解压后会出现如下目录。
bin目录是存放脚本的目录,其中包括windows与linux两个平台的启动与CLI客户端脚本;
conf目录是存放配置文件的目录,其中包括日志配置、zoo_sample.cfg样例配置;
contrib目录是其它语言对zk支持的工具包;
lib目录是zk的依赖包;
recipes目录是zk某些用法的示例代码;
src源代码目录
config目录存放了一个名为zoo_sample.cfg
的样例配置文件,将它重命名为zoo.cfg
,并修改其中的配置选项,配置选项以键值对的形式存在。
tickTime=2000 // 时长单位为毫秒,是后续配置的基本单位,1 * tickTime是客户端与zk服务端的心跳时间,2 * tickTime是客户端会话的超时时间
clientPort=2181 // zk服务进程监听的TCP端口,默认情况下,服务端会监听2181端口
dataDir=F://ideaWorkspace//zookeeper-3.4.6//data
dataLogDir=F://ideaWorkspace//zookeeper-3.4.6//logs // 配置存储快照与事务日志的文件目录
进入到bin目录,windows环境下直接双击zkServer.cmd
,linux环境下执行命令./zkServer.sh start
将服务启动。另外还提供了stop/status/restart
参数。
CLI是指zookeeper提供的类似命令行的维护接口。
./zkCli.sh
即可-server
参数,例如./zkCli.sh -server 10.10.107.26:2181
help
回车,会将支持的命令列表列出来ls /
或ls2 /
create /test "hello"
或create /test ""
delete /test
或rmr /test
,表示删除节点或子节点get /test
set /test "ohmygod"
当在没有CLI的情况下,可以通过nc或telnet的方式从zk那里获取相关的信息。为什么叫四字命令是由于输入的指令为四个字符,命令例如echo conf | nc 127.0.0.1 2181
。
分布式是多个服务跑在不同的机器上,伪分布式是指在资源有限的情况下,多个服务跑在一台机器上。
zoo.cfg
中增加如下配置,且每个服务的clientPort要选择不一样。// initLimit配置follower与leader之间建立连接后进行同步的最长时间为initLimit*tickTime
initLimit=10
// 配置follower和leader之间发送消息,请求和应答的最大时间长度为syncLimit*tickTime
syncLimit=5
// server.id=host:port1:port2
// id为手动给zk服务的编号,port1表示follower和leader交换消息所使用的端口,port2表示选举leader所使用的端口
server.1=10.10.107.104:2887:3887
server.2=10.10.107.104:2888:3888
server.3=10.10.107.104:2889:3889
myid
配置,如果是分布式部署,需要在data
目录下增加一个myid的配置文件,里面分别填好该进程服务在zoo.cfg
文件中分配的编号./zkCli.sh -server 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
,它会随机选择一个服务,从日志中可以具体看出来当前连在哪个服务上在分布式系统开发过程中,服务与服务这间的调用是跨进程或跨机器的,前面讲到的RPC或RESTFul也好,均需要有一个统一的服务管理中心,因为每个服务都存在多个服务实例,也就是常说的服务治理模块,主要负责服务的注册与发现,另外还有配置管理、负载均衡等功能。主要的示意图如下。
各系统组件角色如下:
这在内部系统比较少,访问量比较小的情况下,解决了服务的注册,发现与负载均衡等问题。但是,随着内部服务越来愈多,访问量越来越大的情况下,该架构的隐患逐渐暴露出来:
解决的思路如下:
在此架构中分为服务消费者、服务提供者及服务注册中心三个角色。当然这种架构目前有挺多成熟实现,如阿里的Dubbo及Spring Cloud组件中的Eureka与Consul及etcd等。
服务提供者作为服务的提供方在启动时将自身的服务信息注册到服务注册中心中去,服务信息一般包括但不限于隶属于哪个系统、服务的IP及端口、服务请求的URL、服务的权重等等。
服务注册中心主要提供所有服务注册信息的中心存储,解决了早期nginx单点问题,实现服务名与服务实现端点的对应,同时负责将服务注册信息的更新通知实时的Push给服务消费者(主要是通过Zookeeper的Watcher机制来实现的)。
zookeeper本身提供的API接口太底层,使用不方便。有对API进一步封装的ZK客户端或框架,目前开源之中用得比较多的为zkclient与Curator。两者分别有其优缺点。
原生API
zkclient
Curator
服务的定义是指将服务对外提供的功能接口以某种方式发布出来,在此例中打算定义一个用户的服务,通过服务消费者传入用户ID,来获取该用户的相关信息的功能,利用protobuff来进行服务接口的定义如下,相关语法上一节RPC使用中己讲到,不再详述。
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.comba.zookeeper.rpc";
option java_outer_classname = "UserProto";
package rpc;
// 定义用户接口
service User {
// 获取用户信息
rpc GetUser (UserRequest) returns (UserReply) {}
}
message UserRequest {
int32 id = 1;
}
message UserReply {
int32 id = 1;
string name = 2;
int32 age = 3;
}
<dependency>
<groupId>com.101tecgroupId>
<artifactId>zkclientartifactId>
<version>${zkclient.version}version>
dependency>
<dependency>
<groupId>io.grpcgroupId>
<artifactId>grpc-allartifactId>
<version>${grpc.version}version>
dependency>
<build>
<extensions>
<extension>
<groupId>kr.motd.mavengroupId>
<artifactId>os-maven-pluginartifactId>
<version>1.4.1.Finalversion>
extension>
extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.pluginsgroupId>
<artifactId>protobuf-maven-pluginartifactId>
<version>0.5.0version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.0.0:exe:${os.detected.classifier}protocArtifact>
<pluginId>grpc-javapluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.0.0:exe:${os.detected.classifier}pluginArtifact>
configuration>
<executions>
<execution>
<goals>
<goal>compilegoal>
<goal>compile-customgoal>
goals>
execution>
executions>
plugin>
plugins>
build>
服务提供者在启动时,需要向注册中心注册服务信息,在此实现服务注册的通用方法。
/**
* 服务注册
* @author doublerabbit
* @date 2017年11月2日 上午9:32:30
*/
public class ServiceRegister {
private ZkClient zkClient;
private String host;
private String path;
public ServiceRegister(String host){
this.host = host;
init();
}
public void init(){
zkClient = new ZkClient(host, SESSION_TIMEOUT, CONNECTION_TIMEOUT);
}
public void addNode(String nodePath){
// 验证节点路径的合法性
if (!nodePath.startsWith("/")) {
System.out.println("nodePath must be start with /");
return;
}
// 如果不存在节点,就新创建一个
if (!zkClient.exists(nodePath)) {
zkClient.createPersistent(nodePath,true);
path = nodePath;
}else {
System.out.println(nodePath + "is exists.");
}
}
public void updateData(Object data){
if (StringUtils.isNotBlank(path)) {
updateData(path, data);
}
}
public void updateData(String nodePath,Object data){
// 验证节点路径的合法性
if (!nodePath.startsWith("/")) {
System.out.println("nodePath must be start with /");
return;
}
if (zkClient.exists(nodePath)) {
zkClient.writeData(nodePath, data);
}else {
System.out.println(nodePath + "is not exists.");
}
}
public void deleteNode(String nodePath){
// 验证节点路径的合法性
if (!nodePath.startsWith("/")) {
System.out.println("nodePath must be start with /");
return;
}
if (zkClient.exists(nodePath)) {
zkClient.deleteRecursive(nodePath);
}else {
System.out.println(nodePath + "is not exists.");
}
}
}
服务接口实现利用GRPC方式。
public class ServiceProvider {
private int port = 50051;
private Server server;
private String host = "10.10.107.104:2181,10.10.107.104:2182,10.10.107.104:2183";
private String path = "/servers/user1";
private ServiceRegister register;
public ServiceProvider(){
register = new ServiceRegister(host);
register.addNode(path);
}
private void start() throws IOException {
server = ServerBuilder.forPort(port)
.addService(new UserImpl())
.build()
.start();
System.out.println("service start...");
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
System.err.println("*** shutting down gRPC server since JVM is shutting down");
ServiceProvider.this.stop();
System.err.println("*** server shut down");
System.err.println("*** service deregister");
register.deleteNode(path);
}
});
}
private void stop() {
if (server != null) {
server.shutdown();
}
}
// block 一直到退出程序
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
public ServiceRegister getRegister(){
return this.register;
}
public static void main(String[] args) throws IOException, InterruptedException {
final ServiceProvider server = new ServiceProvider();
server.start();
server.getRegister().updateData("127.0.0.1:50051");
//server.blockUntilShutdown();
Thread.sleep(2*60*1000L);
}
// 实现 定义一个实现服务接口的类
private class UserImpl extends UserGrpc.UserImplBase {
public void getUser(UserRequest req, StreamObserver responseObserver) {
System.out.println("service:"+req.getId());
UserReply reply = UserReply.newBuilder().setId(req.getId())
.setName("zhangsan-service1")
.setAge(20).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
}
消费者侧与服务提供者的依赖引入一致。
服务发现是指消费者在己知服务名的前提下,从注册中将可用的服务信息拉取的本地缓存,待需要使用时从本地缓存中随机选取某个实例。
/**
* 服务消费者进行服务发现,并包含负载均衡功能简略实现
* @author doublerabbit
* @date 2017年11月2日 下午3:07:13
*/
public class ServiceDiscovery {
private ZkClient zkClient;
// 服务实例
private Map instanceMap;
public ServiceDiscovery(){
init();
}
public void init(){
zkClient = new ZkClient(ZK_HOSTS, SESSION_TIMEOUT, CONNECTION_TIMEOUT);
instanceMap = new ConcurrentHashMap();
}
public void subDataChange(String path){
zkClient.subscribeDataChanges(path, new IZkDataListener() {
public void handleDataDeleted(String dataPath) throws Exception {
System.out.println(dataPath + " delete noitfy====");
zkClient.unsubscribeDataChanges(dataPath, this);
}
public void handleDataChange(String dataPath, Object data) throws Exception {
System.out.println(dataPath + " notify,value=" + data);
instanceMap.put(dataPath, (String)data);
}
});
}
public void subChildChange(){
zkClient.subscribeChildChanges(ZNODE_PATH, new IZkChildListener() {
public void handleChildChange(String parentPath, List currentChilds) throws Exception {
System.out.println("parentPath=" + parentPath);
for (String str : currentChilds) {
System.out.println("service path=" + str);
String path = parentPath + "/" + str;
Object data = zkClient.readData(path);
if (data == null) {
subDataChange(path);
}
if (!instanceMap.containsKey(path)) {
System.out.println("path=" + path + ",data=" + (String)data);
instanceMap.put(path, (String)data);
System.out.println("map.size=" + instanceMap.size());
subDataChange(path);
}
}
}
});
}
public Map getServers(){
return instanceMap;
}
/**
* 随机获取一个可用的服务地址,作负载均衡使用
* @return
*/
public Optional getServer(){
int size = instanceMap.size();
if (size == 0) {
System.err.println("does not have available server.");
return Optional.ofNullable(null);
}
int rand = new Random().nextInt(size);
System.out.println("size=" + size + ",rand=" + rand);;
String server = (String)instanceMap.values().toArray()[rand];
return Optional.ofNullable(server);
}
public static void main(String[] args) throws Exception {
ServiceDiscovery consumer = new ServiceDiscovery();
consumer.subChildChange();
Thread.sleep(20*60*1000L);
for (Map.Entry entry : consumer.getServers().entrySet()) {
System.out.println("key=" + entry.getKey() + ",value=" + entry.getValue());
}
}
public static class DataListener implements IZkDataListener{
public void handleDataChange(String dataPath, Object data) throws Exception {
System.out.println(dataPath + "notify,value=" + data);
}
public void handleDataDeleted(String dataPath) throws Exception {
System.out.println(dataPath + " delete noitfy====");
}
}
}
客户端负载均衡是指客户端从本地缓存中选取服务实例的策略,本例中实现简单,采用随机数方式,不过该功能就类似于spring cloud组件中的Ribbon。
public Optional getServer(){
int size = instanceMap.size();
if (size == 0) {
System.err.println("does not have available server.");
return Optional.ofNullable(null);
}
int rand = new Random().nextInt(size);
System.out.println("size=" + size + ",rand=" + rand);;
String server = (String)instanceMap.values().toArray()[rand];
return Optional.ofNullable(server);
}
服务消费实现从注册中心发现服务,并解析服务地址,从而达到调用服务的目的。
/**
* 服务消费者
* @author doublerabbit
* @date 2017年11月3日 下午12:18:57
*/
public class ServiceConsumer {
private final ManagedChannel channel;
private final UserGrpc.UserBlockingStub blockingStub;
public ServiceConsumer(String host,int port){
channel = ManagedChannelBuilder.forAddress(host,port)
.usePlaintext(true)
.build();
blockingStub = UserGrpc.newBlockingStub(channel);
}
public void shutdown() throws InterruptedException {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
}
public void greet(){
UserRequest request = UserRequest.newBuilder().setId(10).build();
UserReply response = blockingStub.getUser(request);
System.out.println("id="+response.getId()+",name="+response.getName()+",age="+response.getAge());
}
public static void main(String[] args) throws InterruptedException {
ServiceDiscovery discovery = new ServiceDiscovery();
discovery.subChildChange();
Thread.sleep(30*1000L);
for (int i = 0; i < 5; i++) {
Optional server = discovery.getServer();
if (server.isPresent()) {
String[] array = server.get().split(":");
System.out.println("server host=" + array[0] + ",port=" + array[1]);
ServiceConsumer client = new ServiceConsumer(array[0],Integer.parseInt(array[1]));
client.greet();
client.shutdown();
}else {
System.err.println("not find avaliable server====");
}
}
}
}
启动服务消费者与服务提供者,然后查看消费者与提供者的控制台输出如下。
// 消费者启动后控制台打印,可能看出消费者侧均感知到服务提供信息
parentPath=/servers
service path=user1
path=/servers/user1,data=null
/servers/user1 notify,value=127.0.0.1:50051
parentPath=/servers
service path=user1
service path=user2
path=/servers/user2,data=null
/servers/user2 notify,value=127.0.0.1:50052
parentPath=/servers
service path=user1
service path=user2
service path=user3
path=/servers/user3,data=null
/servers/user3 notify,value=127.0.0.1:50053
// 消费者进行消费,以10次为例,可以看出在随机选择服务实例进行调用
size=3,rand=2
server host=127.0.0.1,port=50053
id=10,name=zhangsan-service3,age=20
size=3,rand=2
server host=127.0.0.1,port=50053
id=10,name=zhangsan-service3,age=20
size=3,rand=2
server host=127.0.0.1,port=50053
id=10,name=zhangsan-service3,age=20
size=3,rand=2
server host=127.0.0.1,port=50053
id=10,name=zhangsan-service3,age=20
size=3,rand=1
server host=127.0.0.1,port=50052
id=10,name=zhangsan-service2,age=20
size=3,rand=1
server host=127.0.0.1,port=50052
id=10,name=zhangsan-service2,age=20
size=3,rand=0
server host=127.0.0.1,port=50051
id=10,name=zhangsan-service1,age=20
size=3,rand=2
server host=127.0.0.1,port=50053
id=10,name=zhangsan-service3,age=20
size=3,rand=0
server host=127.0.0.1,port=50051
id=10,name=zhangsan-service1,age=20
size=3,rand=1
server host=127.0.0.1,port=50052
id=10,name=zhangsan-service2,age=20
CAP原理,Eureka是基于AP原则构建,ZK是基于CP原则来构建,相对于一个服务访问来讲,在出现问题时能够访问是最重要的,可能访问的数据出现不一致现象,但总比访问不到报错更合理。