大数据day03 集群部署ZooKeeper

一、关于Yum网络版仓库和scp命令确实的问题

  1. 每一台机器都配置一个本地文件系统上的Yum仓库。
  2. 在局域网内配置一个节点(server-base)的本地文件系统Yum库,发布到web服务器中,其他节点通过http://server-base/+路径就可以来访问了。具体方法:先挑选一台机器,挂载一个系统光盘到本地目录/mnt/cdrom,然后启动一个httpd服务器,先cd到/var/www/html目录下,再使用命令ln -s /mnt/cdrom ./centos将/mnt/cdrom软链接到httpd服务器的/var/www/html目录下的centos,通过网页访问测试就可以看到光盘的内容。

无论使用哪种方式都要先将光盘挂载到本地文件目录中,使用命令mnt -t iso9660 /dev/cdrom /mnt/cdrom,为了避免每次重启后都要手动挂载,可以在/etc/fstab中加入一行自动挂载配置:/dev/cdrom /mnt/cdrom iso9660 default 0 0即可实现自动挂载。

如果没有scp命令,使用命令yum install -y openssh-clients.x86_64来安装scp即可。

二、自动化部署脚本

目前有n台虚拟机,其中一台虚拟机里放着一些安装包的压缩包,先在这台虚拟机上安装htppd服务,就使用命令yum install -y httpd.x86_64安装httpd服务,因为安装了httpd之后,其他机器都可以访问这台机器下载需要安装的压缩包,配置免密登陆,在配置免密登陆第一次的时候是需要输入密码的,我们使用boot.sh来解决,使用install.sh来完成安装。

这一块内容主要是运维的工作,开发了解即可,目前不做深入。

boot.sh文件:

#!/bin/bash

SERVERS="192.168.156.51 192.168.156.52" # 需要安装软件的虚拟机列表
PASSWORD=123456 # 虚拟机的密码
BASE_SERVER=172.16.203.100  # 主虚拟机的地址

auto_ssh_copy_id() {    # 这是一个函数,根据脚本在console执行的返回的结果不同,执行不同的内容
    # 9:模拟输入ssh-copy 变量1->第23行第一个参数是$SERVER
    # 11:如果提示信息结尾是yes/no的形式,发送yes
    # 12:如果提示信息结尾是assword,发送变量2->第23行第二个参数是$PASSWORD
    expect -c "set timeout -1;
        spawn ssh-copy-id $1;
        expect {
            *(yes/no)* {send -- yes\r;exp_continue;}
            *assword:* {send -- $2\r;exp_continue;}
            eof        {exit 0;}
        }";
}

ssh_copy_id_to_all() {
    for SERVER in $SERVERS  # 遍历$SERVERS,依次执行auto_ssh_copy_id方法
    do
        auto_ssh_copy_id $SERVER $PASSWORD
    done
}

ssh_copy_id_to_all  # 执行第20行的方法


for SERVER in $SERVERS  # 遍历$SERVERS
do
    scp install.sh root@$SERVER:/root  # 将install.sh复制到root@$SERVER:/root下
    ssh root@$SERVER /root/install.sh  # 登录到@SERVER下,执行install.sh脚本
done

install.sh文件:

#!/bin/bash

BASE_SERVER=192.168.156.50	# 主虚拟机的地址
yum install -y wget	# 执行命令安装wget
wget $BASE_SERVER/soft/jdk-7u45-linux-x64.tar.gz	# 从$BASE_SERVER获取jdk的压缩包
tar -zxvf jdk-7u45-linux-x64.tar.gz -C /usr/local	#解压缩到/usr/local下
# 向/etc/profile中写入内容,通常我们的写法是cat a.txt >> /etc/profile,意思是追加a.txt中的内容到/etc/profile下,这里没有写,后面用了一个<<符号,后面跟着EOF+字符串+EOF
# 意思是创建一个临时文件,文件内容是EOF+字符串+EOF,将它的内容追加到/etc/profile中
cat >> /etc/profile << EOF
export JAVA_HOME=/usr/local/jdk1.7.0_45
export PATH=\$PATH:\$JAVA_HOME/bin
EOF

三、ZooKeeper的功能和应用场景

ZooKeeper是一个分布式协调服务,为用户的分布式应用程序提供协调服务。

  • ZooKeeper是为别的分布式程序服务的
  • ZooKeeper本身就是一个分布式程序,只要有半数以上节点存活,ZooKeeper就能正常服务
  • ZooKeeper提供的服务:主从协调、服务器节点动态上下线、统一配置管理、分布式共享锁、统一名称服务……
  • ZooKeeper在底层实现的两个功能:管理(存储,读取)状态用户提交的数据、为用户提供数据节点监听服务

四、ZooKeeper的集群角色分配原理

ZooKeeper是一个Java程序,需要先安装和配置JDK。详见JDK的安装及配置第三部分。

ZooKeeper至少配置3个节点,因为它有投票选举机制,超过半数的节点就作为leader,其余的作为follower。

ZooKeeper的特征

  1. 一个leader,多个follower组成的集群
  2. 全局数据一致,每个server保存一份相同的数据副本,client无论连接到哪个server,数据都是一致的
  3. 分布式读写,更新请求转发,由leader实施
  4. 更新请求顺序执行,来自同一个client的更新请求,按其发送顺序依次执行
  5. 数据更新原子性,一次数据更新,要么成功,要么失败
  6. 实时性,在一定时间范围内,client能读到最新数据

五、ZooKeeper的命令行客户端及Znode数据结构类型监听等功能

在虚拟机中开启3台ZooKeeper之后,使用命令./zkCli.sh启动bin下的客户端。

zkClient命令概述

h:列出帮助
ls:列出某一节点下的子节点信息
stat:查看节点的状态信息
get:获取当前节点存储的数据内容
create:创建节点
set:修改节点数据
delete:删除没有子节点的节点
rmr:递归删除节点(含子节点)

Znode节点分为两类,短暂(ephemeral)(断开连接自己删除)和持久(persistent)(断开连接不删除)。

watch是用来监听节点变化的,watch是一个一次性的触发器,当触发之后,当节点再次发生变化,不会触发监听器,必须重新设置才可以触发。常用的事件类型:数据节点发生变化,子节点发生变化。

监听器的注册是在获取数据的操作中实现的:

getData(path,watch?)监听事件是:节点数据变化事件

getChildren(path,watch?)监听事件是:节点下子节点增减变化事件

六、ZooKeeper集群自动启动脚本及export变量作用域的解析

因为启动3台ZooKeeper需要输入3次命令,比较麻烦,所以可以写一个shell脚本一次性执行启动3台ZooKeeper,首先在zookeeper01,zookeeper02,zookeeper03的同级目录下,使用命令vim startZooKeeper.sh创建一个脚本文件,具体的脚本内容如下。接着使用命令chmod +x startZooKeeper.sh给这个shell脚本添加可执行权限。

#!/bin/sh
./zookeeper01/bin/zkServer.sh start
./zookeeper02/bin/zkServer.sh start
./zookeeper03/bin/zkServer.sh start

目前,就可以直接使用命令./startZooKeeper.sh来一次性启动3台ZooKeeper了。同理,可以写一个脚本一次性关闭所有ZooKeeper,也就是把脚本文件中start改为stop即可。

使用命令export A=1定义一个变量A,那么A的作用域为所在Shell脚本进程及其子进程;使用命令B=1定义一个变量,B的作用域为所在Shell脚本进程;如果希望当前登录的shell中,可以使用a.sh中的变量,可以使用source a.sh,这时候a.sh中的变量就会进入到当前shell进程中了。

七、ZooKeeper的Java客户端API

package com.wsy;

import java.util.List;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import org.junit.Before;
import org.junit.Test;

public class ZkClient {
    private static final String connectString = "192.168.156.50:2181,192.168.156.50:2182,192.168.156.50:2183";
    private static final int sessionTimeout = 2000;
    ZooKeeper zkClient = null;

    @Before
    public void init() throws Exception {
        // 获取ZooKeeper实例
        zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                // 收到事件通知后的回调函数
                System.out.println(event.getType() + "---" + event.getPath());
                // 因为Watcher监听一次后就失效了,为了能够一直实现监听,就需要再次创建一个Watcher
                try {
                    // 第二个参数为true表示再次创建一个Watcher来监听
                    zkClient.getChildren("/", true);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    // 创建数据节点到ZooKeeper中
    @Test
    public void testCreate() throws Exception {
        // 参数:要创建的节点的路径,节点的数据,节点的权限,节点的类型
        // 上传的数据可以是任何类型,都要转化成byte
        zkClient.create("/eclipse", "Hello ZooKeeper".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
    }

    // 判断Znode是否存在
    @Test
    public void testExist() throws Exception {
        Stat stat = zkClient.exists("/eclipse", false);
        System.out.println(stat == null ? "not exist" : "exist");
    }

    // 获取子节点
    @Test
    public void getChilderen() throws Exception {
        List<String> children = zkClient.getChildren("/", true);
        children.forEach((child) -> {
            System.out.println(child);
        });
        // 让线程继续运行,用Watcher来监听根节点的变化
        Thread.sleep(Long.MAX_VALUE);
    }

    // 获取Znode的数据
    @Test
    public void getData() throws Exception {
        byte[] data = zkClient.getData("/eclipse", false, null);
        System.out.println(new String(data));
    }

    // 删除Znode的数据
    @Test
    public void deleteZnode() throws Exception {
        // 参数:要删除的节点的路径,要删除的版本,-1表示所有版本
        zkClient.delete("/eclipse", -1);
    }

    // 修改Znode的数据
    @Test
    public void setData() throws Exception {
        zkClient.setData("/eclipse", "Hello World".getBytes(), -1);
    }
}

八、分布式应用系统服务器上下线动态感知程序开发

一个应用系统动态上下线服务器,根据需要增加或者删除服务器,这时候,客户端访问服务器的时候,必须知道这些服务器哪些是在线的,这时候就需要使用到ZooKeeper,在服务器和客户端之间放ZooKeeper。

对于服务端,每当上线一个服务器的时候,就向ZooKeeper中去注册信息,在ZooKeeper中创建一个父目录/servers,首先执行zkCli.sh脚本文件,之后输入create /servers "test",这时候就在ZooKeeper中创建了一个父节点,服务注册信息的时候就去server是上注册。注册的节点必须是临时节点,因为短暂节点有一个功能,就是当临时服务器挂掉之后,临时节点会被删除,同时触发事件,从而通知客户端,如果是持久节点,客户端是感知不到的。并且,这些节点的类型要是序列化的,防止服务器主机名相同而发生冲突。

对于客户端,首先getChildren先去扫描一下看看有哪些服务端在线,同时注册一个监听,监听这些服务端节点。当某台服务器挂掉之后,会执行new Watch中的process方法,我们需要在这个方法里面再次通过getChildren获取服务器列表并注册监听事件。

服务端代码如下所示,首先需要配置一下Run Configurations,如图所示,修改这里的变量,启动多次,代表多个服务,这里我启动3次,分别是hostname1,hostname2,hostname3,这时候去zkCli.sh中执行命令ls /servers,可以看到节点下面已经有3个服务了,[server0000000000, server0000000002, server0000000001]。

package com.wsy;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;

public class DistributedServer {
    private static final String connectString = "192.168.156.50:2181,192.168.156.50:2182,192.168.156.50:2183";
    private static final int sessionTimeout = 2000;
    private static final String parentNode = "/servers";
    private ZooKeeper zk = null;

    // 创建到ZooKeeper客户端的连接
    public void getConnection() throws Exception {
        zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                // 收到事件通知后的回调函数
                System.out.println(event.getType() + "---" + event.getPath());
                // 因为Watcher监听一次后就失效了,为了能够一直实现监听,就需要再次创建一个Watcher
                try {
                    // 第二个参数为true表示再次创建一个Watcher来监听
                    zk.getChildren("/", true);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    // 向ZooKeeper集群服务器注册信息
    public void registerServer(String data) throws Exception {
        String path = zk.create(parentNode + "/server", data.getBytes(), Ids.OPEN_ACL_UNSAFE,
                CreateMode.EPHEMERAL_SEQUENTIAL);
        System.out.println(data + " is online,path=" + path);
    }

    // 业务功能
    public void handleBusiness(String name) throws Exception {
        System.out.println(name + " is working");
        Thread.sleep(Long.MAX_VALUE);
    }

    public static void main(String[] args) throws Exception {
        // 获取zk连接
        DistributedServer server = new DistributedServer();
        server.getConnection();
        // 利用zk连接注册服务器
        server.registerServer(args[0]);
        // 启动业务功能
        server.handleBusiness(args[0]);
    }
}

客户端的代码如下所示。

package com.wsy;

import java.util.ArrayList;
import java.util.List;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;

public class DistributedClient {
    private static final String connectString = "192.168.156.50:2181,192.168.156.50:2182,192.168.156.50:2183";
    private static final int sessionTimeout = 2000;
    private static final String parentNode = "/servers";
    // 加volatile的解释:因为main线程中的servers和getServerList中的servers有可能不一样
    // main线程和getServerList是两个线程,存在读取错误的情况,加上volatile之后,保证了不同线程对这个变量操作的可见性
    // 即一个线程修改了这个变量的值,这个新值对其他线程来说是立即可见的,这样就能保证多线程数据的一致性
    private volatile List<String> servers;
    private ZooKeeper zk = null;

    // 创建到ZooKeeper客户端的连接
    public void getConnection() throws Exception {
        zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                try {
                    // 更新服务器列表并重新注册监听
                    getServerList();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    // 获取服务器信息列表
    public void getServerList() throws Exception {
        // 获取服务器子节点信息,并且对父节点进行监听,当父节点的子节点发生变化(服务器上下线),触发监听器
        List<String> children = zk.getChildren(parentNode, true);
        servers = new ArrayList<>();
        children.forEach((child) -> {
            try {
                // 这里的监听器是false,是因为不需要监听子节点内容的变化
                // 有一种情况是需要设置为true的,负载均衡的时候,需要不断获取子节点的数据变化,此时设置为true
                byte[] data = zk.getData(parentNode + "/" + child, false, null);
                servers.add(new String(data));
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        // 打印服务器列表信息
        System.out.println(servers);
    }

    // 业务功能
    public void handleBusiness() throws Exception {
        System.out.println("client is working");
        Thread.sleep(Long.MAX_VALUE);
    }

    public static void main(String[] args) throws Exception {
        // 获取zk连接
        DistributedClient client = new DistributedClient();
        client.getConnection();
        // 获取servers的子节点信息并监听,从中获取服务器信息列表
        client.getServerList();
        // 业务线程
        client.handleBusiness();
    }
}

九、ZooKeeper客户端线程的属性——守护线程

ZooKeeper在设计的时候,把connector和listener都设置为守护线程,所以在Server中要设置Thread.sleep,否则主线程运行完之后就退出了,有可能此时业务方法还没执行完就被中断了,这不符合逻辑。

守护线程是指程序运行的时候,在后台提供服务的线程,比如垃圾回收线程就是一个守护线程,这种线程并不属于程序不可或缺的部分,因此,当所有非守护线程结束的时候,守护现场也会被终止。将普通线程转换成守护线程可以通过调用Thread 的setDaemon(true)方法来实现,且要写在thread.start()前面,否则会抛异常。在Daemon线程中产生的线程也是守护线程。守护线程应该永远不去访问固有资源,如文件、数据库等,因为它会在任何时候发生中断。

十、分布式应用系统程序效果测试

首先运行起来DistributedClient,再填写不同参数,启动多台DistributedServer,可以在DistributedClient的console中看到client获取到了Server的列表信息,当关闭某一个Server的时候,client的listener也会被触发,同时在console中看到少了一个Server。目前客户端就可以动态感知到服务端的上下线了。当某个Server掉线之后,Client端会等待一会儿才能看到结果,是因为我们在代码中设置了sessionTimeout-2000,也就是客户端等待两秒后依旧连接不上才认为服务器挂掉了。

十一、分布式共享锁的程序逻辑流程

现在有一个资源,有很多的客户程序都要访问这一个共享的资源。在资源和客户端之间加入ZooKeeper,每当客户端接入的时候,向ZooKeeper注册一个短暂的序列化的节点,并监听父节点,ZooKeeper中获取父节点下所有子节点,比较子节点的大小,每次都是序列值最小的获取到锁,访问资源,当它释放资源后,删除自己节点,相当于释放锁,并且重新注册一个新的子节点,其他的客户端会获取到监听通知,可以去ZooKeeper上获取锁,可以理解为轮询的策略。

先在zkCli.sh中创建父节点,使用命令create /locks null来创建,之后,执行程序,在console中可以看到get lock+path和finished+path,同时,如果在zkCli.sh中输入命令ls /lock可以看到当前的/lock节点内存在的节点是哪一个,/lock节点中存在的节点一定只有一个,当控制台出现get lock:/locks/sub**********的时候,去ls就可以看到/lock中真是这个sub,当控制台出现finished lock:/locks/sub********的时候,随着出现的下一个序列节点的get locks,再去查看/locks节点,会发现/locks中是下一个序列节点。所以说/locks中每次只有一个节点存在。

package com.wsy;

import java.util.Collections;
import java.util.List;
import java.util.Random;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;

public class DistributedClientLock {
    // 会话超时
    private static final int SESSION_TIMEOUT = 2000;
    // ZooKeeper集群地址
    private String hosts = "192.168.156.50:2181,192.168.156.50:2182,192.168.156.50:2183";
    private String groupNode = "locks";
    private String subNode = "sub";
    private ZooKeeper zk;
    // 记录自己创建的子节点的路径
    private volatile String thisPath;

    // 创建到ZooKeeper客户端的连接
    public void connectZooKeeper() throws Exception {
        zk = new ZooKeeper(hosts, SESSION_TIMEOUT, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                try {
                    // 判断事件类型,只处理子节点变化的事件,并且事件发生的路径是父节点
                    if (event.getType() == EventType.NodeChildrenChanged && event.getPath().equals("/" + groupNode)) {
                        // 获得子节点并对父节点进行监听
                        List<String> children = zk.getChildren("/" + groupNode, true);
                        String thisNode = thisPath.substring(("/" + groupNode + "/").length());
                        // 比较自己的id是不是最小的
                        Collections.sort(children);
                        if (children.size() > 0 && children.get(0).equals(thisNode)) {
                            // 访问共享资源处理业务,并且在完成业务处理后删除锁
                            doSomething();
                            // 重新注册一把锁
                            thisPath = zk.create("/" + groupNode + "/" + subNode, null, Ids.OPEN_ACL_UNSAFE,
                                    CreateMode.EPHEMERAL_SEQUENTIAL);
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        try {
            // 程序一进来就先注册上一把锁到ZooKeeper上
            thisPath = zk.create("/" + groupNode + "/" + subNode, null, Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);
            // wait一小会儿,便于观察
            Thread.sleep(new Random().nextInt(1000));
            // 从ZooKeeper的锁父目录下,获取所有子节点,并给注册对父节点的监听
            List<String> children = zk.getChildren("/" + groupNode, true);
            // 如果争抢资源的只有自己,则可以直接去访问共享资源
            if (children.size() == 1) {
                doSomething();
                thisPath = zk.create("/" + groupNode + "/" + subNode, null, Ids.OPEN_ACL_UNSAFE,
                        CreateMode.EPHEMERAL_SEQUENTIAL);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 处理业务逻辑,并在最后释放锁
    public void doSomething() {
        System.out.println("get lock:" + thisPath);
        try {
            Thread.sleep(2000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("finished:" + thisPath);
            try {
                // 删除thisPath节点,相当于释放锁,此时监听thisPath的client将获得通知
                zk.delete(thisPath, -1);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws Exception {
        DistributedClientLock dcl = new DistributedClientLock();
        dcl.connectZooKeeper();
        Thread.sleep(Long.MAX_VALUE);
    }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值