一、关于Yum网络版仓库和scp命令确实的问题
- 每一台机器都配置一个本地文件系统上的Yum仓库。
- 在局域网内配置一个节点(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的特征
- 一个leader,多个follower组成的集群
- 全局数据一致,每个server保存一份相同的数据副本,client无论连接到哪个server,数据都是一致的
- 分布式读写,更新请求转发,由leader实施
- 更新请求顺序执行,来自同一个client的更新请求,按其发送顺序依次执行
- 数据更新原子性,一次数据更新,要么成功,要么失败
- 实时性,在一定时间范围内,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);
}
}