HADOOP笔记
赵永生
2015.12.20
一、Hadoop
1. hadoop安装流程
1.1 centOS6.5mini安装
1.2修改三台节点主机名
[root@hadoop0 ~]# hostname hadoop0 (3台机子)
[root@hadoop0 ~]# vi /etc/sysconfig/network
HOSTNAME=hadoop0 1 2 (3台机子)
1.3创建用户
useradd hadoop (3台机子均hadoop)
1.4添加hosts 记录
vi /etc/hosts (3台机子)
10.2.124.220 hadoop0
10.2.124.221 hadoop1
10.2.124.222 hadoop2
完成以上重启系统
1.5建立互信
Hadoop下执行 cd
ssh-keygen -t rsa (3台机子)
生成公钥 cat ~/.ssh/id_rsa.pub>> ~/.ssh/authorized_keys (hadoop0)
授权 chmod 600 ~/.ssh/authorized_keys hadoop0
配置hadoop0中的authorized_keys3个公钥
scp ~/.ssh/id_rsa.pub hadoop@hadoop0:/home/hadoop/.ssh/id_rsa1.pub scp ~/.ssh/id_rsa.pub hadoop@hadoop0:/home/hadoop/.ssh/id_rsa2.pub cat ~/.ssh/id_rsa1.pub >> ~/.ssh/authorized_keys cat ~/.ssh/id_rsa2.pub >> ~/.ssh/authorized_keys |
将hadoop0中的authorized_keys传入hadoop1 hadoop2中
scp ~/.ssh/authorized_keys hadoop@hadoop1:/home/hadoop/.ssh/authorized_keys
scp ~/.ssh/authorized_keys hadoop@hadoop2:/home/hadoop/.ssh/authorized_keys
互访 ssh hadoop0 ssh hadoop1 ssh hadoop2
ssh-copy-id 将本机的公钥复制到远程机器的authorized_keys文件中,ssh-copy-id也能让你有到远程机器的home, ~./ssh , 和 ~/.ssh/authorized_keys的权利 $ ssh-copy-id -i ~/.ssh/id_rsa.pub hadoop@hadoop0 |
1.6安装java
查找已安装的Java $su –root
#rpm –qa|grep jdk
#yum install rsync 远程同步
安装 #yum install lrzsz
下载 #sz file 上传 #rz -y
解压jdk #tar -zxvf jdk1.7.0_79
移至 #mv jdk1.7.0_79 /usr/
#chmod 777 -R jdk1.7.0_79
添加环境变量 #vi /etc/profile
##2015-10-12 export JAVA_HOME=/usr/jdk1.7.0_79 export CLASSPATH=:$CLASSPATH:$JAVA_HOME/lib:$JAVA_HOME/jre/lib export PATH=$PATH:$JAVA_HOME/bin:$JAVA_HOME/jre/bin |
重新加载
#source/etc/profile
# java -version
#java
传入其他节点 #scp /usr/jdk1.7.0_79hadoop@hadoop1:/usr
1.8关闭防火墙
Root 下 #chkconfig iptables off
查看 #chkconfig|grep ipt
修改文件 #vi /etc/selinux/config
SELINUX=disabled 重启系统 (立刻关闭: service iptables status/stop)
1.9克隆网络问题
1、修改 /etc/udev/rules.d/70-persistent-net.rules 文件 删除掉 关于 eth0 的信息。修改 第二条 eth1 的网卡的名字为 eth0. 2、修改文件/etc/udev/rules.d/70-persistent-net.rules中的MAC地址和/etc/sysconfig/network-scripts/ifcfg-eth0中的MAC地址要与虚拟机网卡的MAC地址相对应。 |
1.10上传hadoop
$rz -y 解压 $tar -zxvf hadoop
1.11修改.sh配置文件
(hadoop-2.2.0/etc/hadoop/)
hadoop-env.sh JAVA_HOME=/usr/jdk1.7.0_79
yarn-env.sh JAVA_HOME=/usr/jdk1.7.0_79
mapred-env.sh JAVA_HOME=/usr/jdk1.7.0_79
Slaves加入 hadoop1
hadoop2
slaves中存储的datanode节点信息
masters中存储的是secondarynamenode节点信息
1.12创建目录
/home/hadoop tmp data name
mkdir tmp data name
chmod 777 tmp/ data/ name/
1.13修改.xml配置文件
core-site.xml
|
hdfs-site.xml
|
mapred-site.xml
|
yarn-site.xml
|
1.14配置环境变量
#vi/etc/profile root下
export HADOOP_HOME=/home/hadoop/hadoop-2.6.0 #export JAVA_LIBRARY_PATH=$JAVA_LIBRARY_PATH:$HADOOP_HOME/lib/native export HADOOP_COMMON_HOME=$HADOOP_HOME export HADOOP_HDFS_HOME=$HADOOP_HOME export HADOOP_MAPRED_HOME=$HADOOP_HOME export HADOOP_YARN_HOME=$HADOOP_HOME export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$HADOOP_HOME/lib export HADOOP_COMMON_LIB_NATIVE_DIR=$HADOOP_HOME/lib/native export HADOOP_OPTS="-Djava.library.path=$HADOOP_HOME/lib" |
env|grep JAVA_LIBRARY_PATH
1.15替换动态库
Lib/native下的库替换
1.16同步
scp hadoop-env.sh yarn-env.sh slavehdfs-site.xml core-site.xml mapred-site.xml yarn-site.xml hadoop@hadoop1
1.17hadoop的启动和关闭(namenode)
初始化 (/home/hadoop/hadoop-2.2.0/bin)
$hdfs namenode -format
启动 (/home/hadoop/hadoop-2.2.0/sbin)
$./start-all.sh
关闭(/home/hadoop/hadoop-2.2.0/sbin)
$./stop-all.sh
$Jps查看运行情况
如果配置文件修改完多次格式化namenode 可能起不了datanode 因为每次格式化版本号都在变 datanode的记录没有变需要删除datanode下的内容重新格式化
http://hadoop0:50070
http://hadoop0:8088
http://hadoop0:9001
http://hadoop1:19888
1.18端口无法访问问题
查看端口 #sudo netstat -tnlp|grep 19888 启动历时服务器 $mr-jobhistory-daemon.sh start historyserver $mr-jobhistory-daemon.sh stop historyserver $jps
#sudo netstat -tnlp|grep 19888
|
1.19获取配置信息
$hdfs getconf -confkey hadoop.tmp.dir $hdfs getconf -namenodes 不重启hadoop集群,而使配置生效 $hdfs dfsadmin -refreshSuperUserGroupsConfiguration $yarn rmadmin -refreshSuperUserGroupsConfiguration |
2. Hadoop2.6.0
2.1新特性
Common:键管理服务\凭据提供
HDFS:异构存储层(应用API\ 内存\SSD存储)、支持档案存储、在剩余加密中透明数据、操作安全DataNode而不需要root权限、热交换驱动、重启DataNode、支持快速的线性AES加密
YARN:支持长运行应用程序、 应用服务注册、支持回滚升级、工作保存重启ResourceManager、容器保存重启NodeManager、调度过程中支持节点标签、支持基于时间的资源预留的容量调度器、全局,共享缓存的应用程序构件、支持原生的应用程序运行在Docker容器
2.2部署
2.3架构
2.3hadoop1.X生态系统
2.4 hadoop1.X生态系统
2.5启动停止顺序
hadoop-daemons.shàhadoop-daemon.sh (namenodes) start-dfs.sh hadoop-daemons.shàhadoop-daemon.sh (datanodes) (hdfs-config.sh)hadoop-daemons.shàhadoop-daemon.sh (secondary namenodes) hadoop-daemons.shàhadoop-daemon.sh (quorumjournal nodes) hadoop-daemons.shàhadoop-daemon.sh (ZK Failover controllers) (slaves.sh、hadoop-config.sh)(hadoop-config.sh、hadoop-env.sh) start-all.sh (hadoop-config.sh)
yarn-daemon.sh (resourcemanager) start-yarn.sh yarn-daemons.shà yarn -daemon.sh (nodemanager) (yarn-config.sh)(slaves.sh、yarn-config.sh)(yarn-env.sh、yarn-config.sh) yarn-daemon.sh (proxyserver)
|
start-all.sh启动所有的Hadoop守护进程。包括NameNode、 Secondary NameNode、DataNode、JobTracker、 TaskTrack
start-dfs.sh启动Hadoop HDFS守护进程NameNode、SecondaryNameNode和DataNode
hadoop-daemons.shstart namenode 单独启动NameNode守护进程
2.6压缩格式
hadoop对每个压缩格式的支持,详细见下表:
压缩格式 |
工具 |
算法 |
文件扩展名 |
多文件 |
可分割性 |
DEFLATE |
无 |
DEFLATE |
.deflate |
不 |
不 |
gzip |
gzip |
DEFLATE |
.gz |
不 |
不 |
ZIP |
zip |
DEFLATE |
.zip |
是 |
是,在文件范围内 |
bzip2 |
bzip2 |
bzip2 |
.bz2 |
不 |
是 |
LZO |
lzop |
LZO |
.lzo |
不 |
是 |
hadoop下各种压缩算法的压缩比,压缩时间,解压时间见下表:
压缩算法 |
原始文件大小 |
压缩后的文件大小 |
压缩速度 |
解压缩速度 |
gzip |
8.3GB |
1.8GB |
17.5MB/s |
58MB/s |
bzip2 |
8.3GB |
1.1GB |
2.4MB/s |
9.5MB/s |
LZO-bset |
8.3GB |
2GB |
4MB/s |
60.6MB/s |
LZO |
8.3GB |
2.9GB |
49.3MB/S |
74.6MB/s |
3. HDFS
HDFS架构
HDFS读写流程
文件的写入剖析
写入文件的过程比读取较为复杂:
1)解释一
客户端调用create()来创建文件
DistributedFileSystem 用RPC 调用元数据节点,在文件系统的命名空间中创建一个新的文件。
元数据节点首先确定文件原来不存在,并且客户端有创建文件的权限,然后创建新文件。
DistributedFileSystem 返回DFSOutputStream,客户端用于写数据。
客户端开始写入数据,DFSOutputStream将数据分成块,写入data queue。
Data queue由Data Streamer读取,并通知元数据节点分配数据节点,用来存储数据块(每块默认复制3 块)。分配的数据节点放在一个pipeline 里。
Data Streamer 将数据块写入pipeline 中的第一个数据节点。第一个数据节点将数据块发送给第二个数据节点。第二个数据节点将数据发送给第三个数据节点。
DFSOutputStream 为发出去的数据块保存了ackqueue,等待pipeline中的数据节点告知数据已经写入成功。
如果数据节点在写入的过程中失败:
关闭pipeline,将ack queue 中的数据块放入dataqueue 的开始。
当前的数据块在已经写入的数据节点中被元数据节点赋予新的标示,则错误节点重启后能够察觉其数据块是过时的,会被删除。
失败的数据节点从pipeline中移除,另外的数据块则写入pipeline 中的另外两个数据节点。
元数据节点则被通知此数据块是复制块数不足,将来会再创建第三份备份。
当客户端结束写入数据,则调用stream 的close 函数。此操作将所有的数据块写入pipeline中的数据节点,并等待ack queue 返回成功。最后通知元数据节点写入完毕。
2)解释二
使用HDFS 提供的客户端开发库,向远程的Namenode 发起RPC 请求;
Namenode 会检查要创建的文件是否已经存在,创建者是否有权限进行操作,成功则会为文件创建一个记录,否则会让客户端抛出异常;
当客户端开始写入文件的时候,开发库会将文件切分成多个packets,并在内部以”data queue”的形式管理这些packets,并向Namenode 申请新的blocks,获取用来存储replicas的合适的datanodes 列表,列表的大小根据在Namenode 中对replication的设置而定。
开始以pipeline(管道)的形式将packet写入所有的replicas中。开发库把packet以流的方式写入第一个datanode,该datanode 把该packet 存储之后,再将其传递 给在此pipeline中的下一个datanode,直到最后一个datanode,这种写数据的方式 呈流水线的形式。
最后一个datanode 成功存储之后会返回一个ack packet,在pipeline 里传递至客户端,在客户端的开发库内部维护着”ack queue”,成功收到datanode 返回的ack packet后会从”ack queue”移除相应的packet 。
如果传输过程中,有某个datanode 出现了故障,那么当前的pipeline会被关闭,出现故障的datanode 会从当前的pipeline 中移除,剩余的block 会继续剩下的datanode 中继续以pipeline的形式传输,同时Namenode会分配一个新的datanode,保持replicas 设定的数量。
文件的读取剖析
文件读取的过程如下:
1)解释一
客户端(client)用FileSystem 的open()函数打开文件。
DistributedFileSystem 用RPC 调用元数据节点,得到文件的数据块信息。
对于每一个数据块,元数据节点返回保存数据块的数据节点的地址。
DistributedFileSystem 返回FSDataInputStream给客户端,用来读取数据。
客户端调用stream 的read()函数开始读取数据。
DFSInputStream 连接保存此文件第一个数据块的最近的数据节点。
Data 从数据节点读到客户端(client)。
当此数据块读取完毕时,DFSInputStream关闭和此数据节点的连接,然后连接此文件下一个数据块的最近的数据节点。
当客户端读取完毕数据的时候,调用FSDataInputStream的close 函数。
在读取数据的过程中,如果客户端在与数据节点通信出现错误,则尝试连接包含此数据块的下一个数据节点。失败的数据节点将被记录,以后不再连接。
2)解释二
使用HDFS 提供的客户端开发库,向远程的Namenode 发起RPC 请求;
Namenode会视情况返回文件的部分或者全部 block列表,对于每个block,Namenode 都会返回有该block 拷贝的datanode 地址;
客户端开发库会选取离客户端最接近的datanode来读取block ;
读取完当前block 的数据后,关闭与当前的datanode 连接,并为读取下一个block寻找最佳的datanode;
当读完列表的block后,且文件读取还没有结束,客户端开发库会继续向Namenode获取下一批的block列表。
读取完一个block 都会进行checksum 验证,如果读取datanode时出现错误,客户端会通知Namenode,然后再从下一个拥有该block 拷贝的datanode 继续读。
两种HDFS HA的解决方案,一种是NFS(Network File System),另一种是Quorum JournalManager (QJM)
HA与Federation
基于QJM/Qurom Journal Manager/Paxos的HDFS HA
1GB=1024MB 1TB=1024GB 1PB=1024TB 1EB=1024PB 1ZB=1024EB |
4. YARN
Yarn优点:更快地MapReduce计算、对多框架支持(MapReduce、Spark、Storm、MPI、Giraph)框架升级更容易
Yarn的核心服务:
Resource Manager:每个集群一个实例,用于管理整个集群的资源使用;
Node Manager:每个集群多个实例,用于自身Container的启动和监测(每个NodeManager上可能有多个Container)。
YARN的HA
YARN包括的组件有:ResourceManager、NodeManager、ApplicationMaster,其中ResourceManager可以分为:Scheduler、ApplicationsManager
Hadoop1.X中的JobTracker被分为两部分:ResourceManager和ApplicationMaster,前者提供集群资源给应用,后者为应用提供运行时环境;
YARN应用生命周期:
1) 客户端提交一个应用请求到ResourceManager;
2) ResourceManager中的ApplicationsManager在集群中寻找一个可用的、负载较小的NodeManager;
3) 被找到的NodeManager创建一个ApplicationMaster实例;
4) ApplicationMaster向ResourceManager发送一个资源请求,ResourceManager回复一个Container的列表,包括这些Container是在哪些NodeManager上启动的信息;
5) ApplicationMaster在ResourceManager的指导下在每个NodeManager上启动一个Container,Container在ApplicationMaster的控制下执行一个任务;
工作流程:
当用户向YARN中提交一个应用程序后,YARN将分两个阶段运行该应用程序:
第一个阶段是启动ApplicationMaster;
第二个阶段是由ApplicationMaster创建应用程序,为它申请资源,并监控它的整个运行过程,直到运行完成。
步骤1 用户向YARN中提交应用程序,包括ApplicationMaster程序、启动ApplicationMaster的命令、用户程序等。
步骤2 ResourceManager为该应用程序分配第一个Container,并与对应的Node-Manager通信,要求它在这个Container中启动应用程序的ApplicationMaster。
步骤3 ApplicationMaster首先向ResourceManager注册,这样用户可以直接通过ResourceManager查看应用程序的运行状态,然后它将为各个任务申请资源,并监控它的运行状态,直到运行结束,即重复步骤4~7。
步骤4 ApplicationMaster采用轮询的方式通过RPC协议向ResourceManager申请和领取资源。
步骤5 一旦ApplicationMaster申请到资源后,便与对应的NodeManager通信,要求它启动任务。
步骤6 NodeManager为任务设置好运行环境(包括环境变量、JAR包、二进制程序等)后,将任务启动命令写到一个脚本中,并通过运行该脚本启动任务。
步骤7 各个任务通过某个RPC协议向ApplicationMaster汇报自己的状态和进度,以让ApplicationMaster随时掌握各个任务的运行状态,从而可以在任务失败时重新启动任务。
在应用程序运行过程中,用户可随时通过RPC向ApplicationMaster查询应用程序的当前运行状态。
步骤8 应用程序运行完成后,ApplicationMaster向ResourceManager注销并关闭自己。
Yarn资源调度:
无论FifoScheduler,CapacityScheduler和FairScheduler的核心资源分配模型都是一样的。
调度器维护一群队列的信息。用户可以向一个或者多个队列提交应用。每次NM心跳的时候,调度器,根据一定的规则选择一个队列,再在队列上选择一个应用,尝试在这个应用上分配资源。不过,因为一些参数限制了分配失败,就会继续选择下一个应用。在选择了一个应用之后,这个应用对应也会有很多的资源申请的请求。调度器会优先匹配本地资源是申请请求,其次是同机架的,最后的任意机器的。
调度器 |
FifoScheduler |
CapacityScheduler |
FairScheduler |
设计目的 |
最简单的调度器,易于理解和上手 |
多用户的情况下,最大化集群的吞吐和利用率 |
多用户的情况下,强调用户公平地贡献资源 |
队列组织方式 |
单队列 |
树状组织队列。无论父队列还是子队列都会有资源参数限制,子队列的资源限制计算是基于父队列的。应用提交到叶子队列。 |
树状组织队列。但是父队列和子队列没有参数继承关系。父队列的资源限制对子队列没有影响。应用提交到叶子队列。 |
资源限制 |
无 |
父子队列之间有容量关系。每个队列限制了资源使用量,全局最大资源使用量,最大活跃应用数量等。 |
每个叶子队列有最小共享量,最大资源量和最大活跃应用数量。用户有最大活跃应用数量的全局配置。 |
队列ACL限制 |
可以限制应用提交权限 |
可以限制应用提交权限和队列开关权限,父子队列间的ACL会继承。 |
可以限制应用提交权限,父子队列间的ACL会继承。但是由于支持客户端动态创建队列,需要限制默认队列的应用数量。目前,还看不到关闭动态创建队列的选项。 |
队列排序算法 |
无 |
按照队列的资源使用量最小的优先 |
根据公平排序算法排序 |
应用选择算法 |
先进先出 |
先进先出 |
先进先出或者公平排序算法 |
本地优先分配 |
支持 |
支持 |
支持 |
延迟调度 |
不支持 |
不支持 |
支持 |
资源抢占 |
不支持 |
不支持 |
支持,看到代码中也有实现。但是,由于本特性还在开发阶段,本文没有真实试验。 |
简单总结下:
FifoScheduler:最简单的调度器,按照先进先出的方式处理应用。只有一个队列可提交应用,所有用户提交到这个队列。可以针对这个队列设置ACL。没有应用优先级可以配置。
CapacityScheduler:可以看作是FifoScheduler的多队列版本。每个队列可以限制资源使用量。但是,队列间的资源分配以使用量作排列依据,使得容量小的队列有竞争优势。集群整体吞吐较大。延迟调度机制使得应用可以放弃,夸机器或者夸机架的调度机会,争取本地调度。
FairScheduler:多队列,多用户共享资源。特有的客户端创建队列的特性,使得权限控制不太完美。根据队列设定的最小共享量或者权重等参数,按比例共享资源。延迟调度机制跟CapacityScheduler的目的类似,但是实现方式稍有不同。资源抢占特性,是指调度器能够依据公平资源共享算法,计算每个队列应得的资源,将超额资源的队列的部分容器释放掉的特性。
在hadoop集群配置中启动公平调度器之后,调度器会从classpath中加载fair-scheduler.xml和fair-allocation.xml文件,完成公平调度器的初始化。其中fair-scheduler.xml主要描述重要特性的配置,fair-allocation.xml主要描述了具体的队列及其参数配置。总结起来有如下特性:
1) 动态更新配置:公平调度器的fair-allocation.xml配置文件在运行时可以随时重新加载来调整分配参数。除非重启ResourceManager,否则队列只能添加不能删除。修改fair-allocation.xml后,使用以下命令可以刷新配置。
yarn rmadmin -refreshQueues
2) 树形组织队列:公平调度器的队列是按照树形结构组织的。根队列只有一个root队列。父子队列除了ACL参数外,其余参数都不继承。
3) 队列应用参数:应用只能提交到叶子队列。受队列最大应用数量限制。队列可以设定权重,最小共享量和最大使用量。权重和最小共享量将影响在公平排序算法中的排名,从而影响资源调度倾向。队列还可以设定最大运行的应用数量。
4) 用户参数限制:一个用户可以提交应用到多个队列。用户受全局的最大可运行数量限制。
5) 资源分配选择:资源分配的时候,使用公平排序算法选择要调度的队列,然后在队列中使用先进先出算法或者公平排序算法选择要调度的应用。
6) 延迟调度:每种资源申请的优先级都有一个资源等级标记。一开始标记都是NODE_LOCAL,只允许本地调度。如果调度机会大于NM数量乘以上界(locality.threshold.node),资源等级转变为RACK_LOCAL、重置调度机会为0、接受机架调度。如果调度机会再次大于NM数量乘以上界(locality.threshold.rack),资源等级转变为OFF_SWITCH、重置调度机会为0、接受任意调度。详情代码参看[FSSchedulerApp.getAllowedLocalityLevel:470]
7) 资源抢占:调度器会使用公平资源共享算法计算每个队列应该得到的资源总量。如果一个队列长时间得不到应得到的资源量,调度器可能会杀死占用掉该部分资源的容器。
5. MapReduce
map阶段:
1) InputFormat确定输入数据应该被分为多少个分片,并且为每个分片创建一个InputSplit实例;
2) 针对每个InputSplit实例MR框架使用一个map任务来进行处理;在InputSplit中的每个KV键值对被传送到Mapper的map函数进行处理;
3) map函数产生新的序列化后的KV键值对到一个没有排序的内存缓冲区中;
4) 当缓冲区装满或者map任务完成后,在该缓冲区的KV键值对就会被排序同时流入到磁盘中,形成spill文件,溢出文件;
5) 当有不止一个溢出文件产生后,这些文件会全部被排序,并且合并到一个文件中;
6) 文件中排序后的KV键值对等待被Reducer取走;
reduce阶段:
1) shuffle:或者称为fetch阶段(获取阶段),所有拥有相同键的记录都被合并而且发送到同一个Reducer中;
2) sort: 和shuffle同时发生,在记录被合并和发送的过程中,记录会按照key进行排序;
3) reduce:针对每个键会进行reduce函数调用;
reduce数据流:
1) 当Mapper完成map任务后,Reducer开始获取记录,同时对他们进行排序并存入自己的JVM内存中的缓冲区;
2) 当一个缓冲区数据装满,则会流入到磁盘;
3) 当所有的Mapper完成并且Reducer获取到所有和他相关的输入后,该Reducer的所有记录会被合并和排序,包括还在缓冲区中的;
4) 合并、排序完成后调用reduce方法;输出到HDFS或者根据作业配置到其他地方;
MR2的运行模式:本地(不启动yarn);yarn。
输出压缩:Gzip、Lzo、snappy
6. win7下配置eclipse
添加hadoop环境变量
HADOOP_HOME= C:\hadoop-2.6.0 path=% HADOOP_HOME %/bin |
替换Hadoop2/bin目录
hadoop-eclipse-plugin-2.6.0放入eclipse中plugins
src下添加日志信息log4j.properties
log4j.rootLogger=debug,stdout,R log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%5p - %m%n log4j.appender.R=org.apache.log4j.RollingFileAppender log4j.appender.R.File=mapreduce_test.log log4j.appender.R.MaxFileSize=1MB log4j.appender.R.MaxBackupIndex=1 log4j.appender.R.layout=org.apache.log4j.PatternLayout log4j.appender.R.layout.ConversionPattern=%p %t %c - %m%n log4j.logger.com.codefutures=DEBUG |
替换动态库
Lib/native下的库替换
C:\Windows\System32下缺少hadoop.dll
添加 mapreduce
导入lib包(没精简)
添加native
7.调优
1 Linux文件系统参数调整
(1) noatime 和 nodiratime属性
文件挂载时设置这两个属性可以明显提高性能。默认情况下,Linux ext2/ext3 文件系统在文件被访问、创建、修改时会记录下文件的时间戳。如果系统运行时要访问大量文件,关闭这些操作,可提升文件系统的性能。Linux 提供了 noatime 这个参数来禁止记录最近一次访问时间戳。
(2) readahead buffer
调整linux文件系统中预读缓冲区地大小,可以明显提高顺序读文件的性能。默认buffer大小为256 sectors,可以增大为1024或者2408 sectors(注意,并不是越大越好)。可使用blockdev命令进行调整。
(3)避免RAID和LVM操作
避免在TaskTracker和DataNode的机器上执行RAID和LVM操作,这通常会降低性能。
2 Hadoop通用参数调整
(1)dfs.namenode.handler.count或mapred.job.tracker.handler.count
namenode或者jobtracker中用于处理RPC的线程数,默认是10,较大集群,可调大些,比如64。
(2)dfs.datanode.handler.count
datanode上用于处理RPC的线程数。默认为3,较大集群,可适当调大些,比如8。需要注意的是,每添加一个线程,需要的内存增加。
(3) tasktracker.http.threads
HTTP server上的线程数。运行在每个TaskTracker上,用于处理maptask输出。大集群,可以将其设为40~50。
3 HDFS相关配置
(1)dfs.replication
文件副本数,通常设为3,不推荐修改。
(2)dfs.block.size
HDFS中数据block大小,默认为64M,对于较大集群,可设为128MB或者256MB。(也可以通过参数mapred.min.split.size配置)
(3) mapred.local.dir和dfs.data.dir
这两个参数mapred.local.dir和dfs.data.dir 配置的值应当是分布在各个磁盘上目录,这样可以充分利用节点的IO读写能力。运行 Linux sysstat包下的iostat -dx 5命令可以让每个磁盘都显示它的利用率。
4 map/reduce 相关配置
(1){map/reduce}.tasks.maximum
同时运行在TaskTracker上的最大map/reduce task数,一般设为(core_per_node)/2~2*(cores_per_node)。
(2) io.sort.factor
当一个map task执行完之后,本地磁盘上(mapred.local.dir)有若干个spill文件,map task最后做的一件事就是执行merge sort,把这些spill文件合成一个文件(partition)。执行merge sort的时候,每次同时打开多少个spill文件由该参数决定。打开的文件越多,不一定merge sort就越快,所以要根据数据情况适当的调整。
(3)mapred.child.java.opts
设置JVM堆的最大可用内存,需从应用程序角度进行配置。
5 map task相关配置
(1) io.sort.mb
Map task的输出结果和元数据在内存中所占的buffer总大小。默认为100M,对于大集群,可设为200M。当buffer达到一定阈值,会启动一个后台线程来对buffer的内容进行排序,然后写入本地磁盘(一个spill文件)。
(2)io.sort.spill.percent
这个值就是上述buffer的阈值,默认是0.8,即80%,当buffer中的数据达到这个阈值,后台线程会起来对buffer中已有的数据进行排序,然后写入磁盘。
(3) io.sort.record
Io.sort.mb中分配给元数据的内存百分比,默认是0.05。这个需要根据应用程序进行调整。
(4)mapred.compress.map.output/Mapred.output.compress
中间结果和最终结果是否要进行压缩,如果是,指定压缩方式(Mapred.compress.map.output.codec/ Mapred.output.compress.codec)。推荐使用LZO压缩。Intel内部测试表明,相比未压缩,使用LZO压缩的TeraSort作业运行时间减少60%,且明显快于Zlib压缩。
6 reduce task相关配置
(1)Mapred.reduce.parallel
Reduce shuffle阶段copier线程数。默认是5,对于较大集群,可调整为16~25。
==================================================================================
hadoop优化相关:
1:对操作系统进行参数调优
(1):打开文件描述符和网络连接参数上限(具体操作内容:使用ulimit命令讲允许同时打开的文件描述符数据上限增大至一个合适的值,同时调整内核参数net.core.somaxconn)
(2):关闭swap分区(具体操作内容是/etc/stsctl.conf中得vm.vm.swappiness参数)
(3):设置合理的预读取缓冲区大小(具体操作内容:使用linux命令blockdev设置预读取缓冲区的大小)
(4):文件系统的选择和配置
2:JVM参数的优化
3:通过hadoop的参数进行调优
(1):设置合理的槽位数目(具体配置mapred.tasktracker.map.tasks.maximum | apred.tasktracker.reduce.tasks.maximum | mapreduce.tasktracker.map.tasks.maximum |mapreduce.tasktracker.reduce.tasks.maximum)
(2):调整心跳间隔,对于300台以下的集群 可以把心跳设置成300毫秒(默认是3),mapreduce.jobtracker.hearbeat.interval.min| mapred.hearbeats.in.second | mapreduce.jobtracker.heartbeats.scaling.factor
(3):启用外心跳,为了减少任务分配延迟(比如我们的任务心跳设置为10秒钟,当有一个任务挂掉了之后,他就不能马上通知jobtracker),所以hadoop引入了外心跳,外心跳是任务运行结束或者任务运行失败的时候触发的,能够在出现空闲资源时第一时间通知jobtracker,以便他能够迅速为空闲资源分配新的任务外心跳的配置参数是 mapreduce.tasktracker.outofband.hearbeat
(4):磁盘快的配置. map task会把中间结果放到本地磁盘中,所以对于I/O密集的任务来说这部分数据会对本地磁盘造成很大的压力,我们可以配置多块可用磁盘,hadoop将采用轮训的方式将不同的maptask的中间结果写到磁盘上 maptask中间结果的配置参数是mapred.local.dir | mapreduce.cluster.local.dir
(5):配置RPC Handler的数量,jobracker需要冰法处理来自各个tasktracker的RPC请求,我们可以根据集群规模和服务器并发处理的情况调整RPC Handler的数目,以使jobtracker的服务能力最佳配置参数是 mapred.job.tracker.handler.count |mapreduce.jobtracker.handler.count (默认是10)
(6):配置HTTP线程数. 在shuffle阶段,reducetask 通过http请求从各个tasktracker上读取map task中间结果,而每个tasktracker通过jetty server处理这些http请求,所以可以适当配置调整jetty server的工作线程数配置参数是 tasktracker.http.thread | mapreduce.tasktracker.http.threads (默认是40)
(7):如果我们在运行作业的过程中发现某些机器被频繁地添加到黑名单里面,我们可以把此功能关闭
(8):使用合理调度器
(9):使用合适的压缩算法,在hadoop里面支持的压缩格式是: gzip,zip,bzip2,LZO,Snappy,LZO和Snappy的呀搜比和压缩效率都很优秀,Snappy是谷歌的开源数据压缩哭,他已经内置在hadoop1.0之后的版本,LZO得自己去编译
(10):开启预读机制. 预读机制可以有效提高磁盘I/O的读性能,目前标准版的apachehadoop不支持此功能,但是在cdh中是支持的配置参数是: mapred.tasktracker.shuffle.fadvise=true (是否启用shuffle预读取机制)mapred.tasktracker.shuffle.readahead.bytes=4MB (shuffle预读取缓冲区大小)mapreduce.ifile.readahead = true (是否启用ifile预读取机制)mapreduce.ifile.readahead.bytes = 4MB (IFile预读取缓冲区大小)
(11):启用推测执行机制
(12):map task调优: 合理调整io.sort.record.percent值,可减少中间文件数据,提高任务执行效率.(map task的输出结果将被暂时存放到一个环形缓冲区中,这个缓冲区的大小由参数"io.sort.mb"指定,单位MB,默认是100MB, 该缓冲区主要由两部分组成,索引和实际数据,默认情况下,索引占整个buffer的比例为io.sort.record.percent,默认是5%, 剩余空间存放数据,仅当满足以下任意一个条件时才会触发一次flush,生成一个临时文件,索引或者数据空间使用率达到比例为 io.sort.spill.percent的80%) 所以具体调优参数如下: io.sort.mb | io.sort.record.percent | io.sort.spill.percent
(13):reduce task调优 reduce task会启动多个拷贝线程从每个map task上读取相应的中间结果,参数是"mapred.reduce.parallel.copies"(默认是5) 原理是这样的-->对于每个待拷贝的文件,如果文件小于一定的阀值A,则将其放入到内存中,否则已文件的形式存放到磁盘上, 如果内存中文件满足一定条件D,则会将这些数据写入磁盘中,而当磁盘上文件数目达到io.sort.factor(默认是10)时, 所以如果中间结果非常大,可以适当地调节这个参数的值
(14):跳过坏记录 看具体参数说明,=号后面是默认值 mapred.skip.attempts.to.start.skipping=2 当任务失败次数达到该值时,才会进入到skipmode,即启用跳过坏记录gongnneg
mapred.skip.map.max,skip.records=0 用户可通过该参数设置最多运行跳过的记录数目
mapred.skip.reduce.max.skip.groups=0 用户可通过设置该参数设置ReduceTask最多允许跳过的记录数目 mapred.skip.out.dir =${mapred.output.dir}/logs/ 检测出得坏记录存放到目录里面(一般为HDFS路径),hadoop将坏记录保存起来以便于用户调试和跟踪
(15):使用JVM重用 :mapred.job.reuse.jvm.aum.tasks | mapreduce.job.jvm.num.tasks = -1
4:从用户角度来优化
(1)设置combiner. 在应用中尽量使用combiner可以有效地提高效率
(2)选择合适的writable
(3)设置合理的reduce数
(4)合理使用DistributedCache(建议如果需要一个外部文件引入的时候,事先把他上传到hdfs上,这样效率高,因为这样节省了客户端上传文件的时间,并且还隐含地告诉DistributedCache, 请将文件下载到各节点的public共享目录下)
(5)合理控制Reduce Task的启动时机 ,因为在执行job的时候,reduce task晚于map task启动,所以合理控制reduce task启动时机不仅可以加快作业的运行速度还可以提高资源利用率,如果reduce task启动过早,则可能由于reduce task长时间占用reduce slot资源造成slot hoarding现象,而且还会降低资源利用率反之则导致reduce task获取资源延迟,增加了作业的运行时间.hadoop配置reduce task启动时机的参数是 mapred.reduce.slowstart.completed.maps | mapreduce.job.reduce.slowstart.completed.maps (默认值是0.05,也就是map task完成数目达到5%时,开始启动reducetask)
8. hadoop部署后做些什么?
Zookeeper的基本操作
HDFS 创建删除等
测试,运行mapreduce用例
集群基准测试(HDFS、mapreduce、hbase)程序频繁写数据,反复运行
集群磁盘网络IO性能、CPU计算性能
9.hadoop安全
1). Hadoop 2.0认证机制
在Hadoop中,Client与NameNode和Client与ResourceManager之间初次通信均采用了Kerberos进行身份认证,之后便换用Delegation Token以较小开销,而DataNode与NameNode和NodeManager与ResourceManager之间的认证始终采用Kerberos机制,默认情况下,Kerberos认证机制是关闭的,管理员可通过将参数hadoop.security.authentication设为“kerberos”(默认值为“simple”)启动它。接下来重点分析Hadoop中Token的工作原理以及实现。
Hadoop中Token的定义在org.apache.hadoop.security.token.Token中,每类Token存在一个唯一TokenIdentifier标识,Token主要由下表列出的几个字段组成。
2). Hadoop 2.0授权机制
Hadoop YARN的授权机制是通过访问控制列表(ACL)实现的,按照授权实体,可分为队列访问控制列表、应用程序访问控制列表和服务访问控制列表,下面分别对其进行介绍。
在正式介绍YARN授权机制之前,先要了解HDFS的POSIX风格的文件访问控制机制,这与当前Unix的一致,即将权限授予对象分为用户、同组用户和其他用户,且可单独为每类对象设置一个文件的读、写和可执行权限。此外,用户和用户组的关系是插拔式的,默认情况下共用Unix/Linux下的用户与用户组对应关系,这与YARN是一致的。
3).如何实现hadoop的安全机制。
1.1 共享hadoop集群:
a: 管理人员把开发人员分成了若干个队列,每个队列有一定的资源,每个用户及用户组只能使用某个队列中指定资源。
b: HDFS上有各种数据,公用的,私有的,加密的。不用的用户可以访问不同的数据。
1.2 HDFS安全机制
client获取namenode的初始访问认证( 使用kerberos )后,会获取一个delegation token,这个token可以作为接下来访问HDFS或提交作业的认证。同样,读取block也是一样的。
1.3 mapreduce安全机制
所有关于作业的提交或者作业运行状态的追踪均是采用带有Kerberos认证的RPC实现的。授权用户提交作业时,JobTracker会为之生成一个delegation token,该token将被作为job的一部分存储到HDFS上并通过RPC分发给各个TaskTracker,一旦job运行结束,该token失效。
1.4 DistributedCache是安全的。
DistribuedCache分别两种,一种是shared,可以被所有作业共享,而private的只能被该用户的作业共享。
1.5 RPC安全机制
在Hadoop RP中添加了权限认证授权机制。当用户调用RPC时,用户的login name会通过RPC头部传递给RPC,之后RPC使用Simple Authentication and Security Layer(SASL)确定一个权限协议(支持Kerberos和DIGEST-MD5两种),完成RPC授权。
10. hadoop的调度策略
10.1 默认情况下hadoop使用的FIFO, 先进先出的调度策略。按照作业的优先级来处理。
10.2 计算能力调度器( CapacityScheduler ) 支持多个队列,每个队列可配置一定的资源量,每个队列采用FIFO, 为了防止同一个用户的作业独占资源,那么调度器会对同一个用户提交的作业所占资源进行限定,首先按以下策略选择一个合适队列:计算每个队列中正在运行的任务数与其应该分得的计算资源之间的比值,选择一个该比值最小的队列;然后按以下策略选择该队列中一个作业:按照作业优先级和提交时间顺序选择,同时考虑用户资源量限制和内存限制。
10.3 公平调度器( Fair Scheduler) 支持多队列多用户,每个队列中的资源量可以配置,同一队列中的作业公平共享队列中所有资源。
10.4 异构集群的调度器LATE
10.5 实时作业的调度器DeadlineScheduler和Constraint-based Scheduler
11.NN与SNN
12. 其他
Hadoop版本 (Apache、cdh(cloudera))
Hadoop集群的规模(测试机内存32G、硬盘4T,CPU8核、网卡千兆)
Hadoop安全模式
检测数据块副本数,退出安全模式后复制。
HDFS通信协议 TCP/IP
快照: 数据复制备份
Staging: 缓存本地临时文件,之后联系namenode,写入datanode
文件删除:最后副本存储在/trash中,默认6小时。
遇到的问题:
1 数据倾斜。
出现这种情况:多数是由于代码的质量写的不够健壮。查看日志:发现问题。
2 spark-出现OOM
小数据量的情况可以cache,数据量大的情况必须考虑内存使用。
状态: Hadoop dfadmin -report 安全模式: Hadoop dfadmin -saftmode enter Hadoop dfadmin -saftmode leave 负载均衡 start balancer.sh |
二、 Hive
1. hive安装
hive安装模式:嵌入(当前执行目录保存元数据信息);本地;远程
1.1 Root下安装mysql
yum-y install mysql-server
中文字符集:createdatabase xiaoyou2 character set gbk;
设置开机启动
chkconfig mysqldon
chkconfig|grepmysql
启动mysql服务
servicemysqld start
设置mysql的root用户密码
mysql-u root
>selectuser,host,password from mysql.user;
> set password for root@localhost=password('hadoop');
> set password for root@hadoop0=password('hadoop');
>exit
重新登录
mysql -u root -p
基本命令
>showdatabases;
>usedatabasesname;
>drop database databasesname;
>exit
>createdatabase test;
>showtables;
为hadoop/hive创建mysql用户
>CREATE USER 'hadoop'@'hadoop0' IDENTIFIED BY 'hadoop';
>GRANT ALL PRIVILEGES ON *.* TO 'hadoop'@'hadoop0' WITH GRANT OPTION;
>exit;
登录: su - hadoop
mysql -h hadoop0 -u hadoop -p
创建元数据库: >create databasehive;
1.2/home/hadoop/下解压hive
配置环境变量 root/etc/profile
exportHIVE_HOME=/home/hadoop/apache-hive-0.13.0-bin
exportPATH=$PATH:$JAVA_HOME/bin:$HIVE_HOME/bin:$SQOOP_HOME/bin
source/etc/profile
sqooh运行权限:先不做
chmod 777/home/hadoop/hive-0.13/bin/*
1.3配置hive
创建目录
hdfs dfs –mkdir -p /user/hive/warehouse
hdfs dfs –mkdir -p /tmp
hdfs dfs -chmod -R 777 /user
将加入hive-site.xml加入/home/hadoop/hadoop-2.6.0/etc/hadoop/目录下
|
1.4加入jar
将加入mysql-connector-java-5.1.8-bin.jar加入/home/hadoop/hive-1.1.0/lib/目录下
1.5启动
Hive
fetch task命令>set hive.fetch.task.conversion=more; >hive -- hiveconf hive.fetch.task.conversion=more; >set hive.groupby.orderby.position.alias=true; |
开启 Hive的Web 服务:$hive --service hwi 地址http://host_name:9999/hwi
单个 hbase启动 hive -hiveconf hbase.master= Hadoop0:60000 hbase 集群启动 hive -hiveconf hbase.zookeeper.quorum= Hadoop0, Hadoop1,Hadoop2 |
2.HIVE体系结构
Hive 的结构可以分为以下几部分:
·用户接口:包括 CLI, Client, WUI
·元数据存储:通常是存储在关系数据库如 mysql, derby 中
·解释器、编译器、优化器、执行器
· Hadoop:用 HDFS 进行存储,利用MapReduce 进行计算
1、用户接口主要有三个:CLI,Client和 WUI。其中最常用的是 CLI,Cli 启动的时候,会同时启动一个 Hive 副本。Client 是 Hive 的客户端,用户连接至 Hive Server。在启动 Client 模式的时候,需要指出 Hive Server 所在节点,并且在该节点启动 Hive Server。 WUI 是通过浏览器访问 Hive。
2、 Hive 将元数据存储在数据库中,如 mysql、derby。Hive 中的元数据包括表的名字,表的列和分区及其属性,表的属性(是否为外部表等),表的数据所在目录等。
3、解释器、编译器、优化器完成 HQL 查询语句从词法分析、语法分析、编译、优化以及查询计划的生成。生成的查询计划存储在 HDFS 中,并在随后有 MapReduce 调用执行。
4、Hive 的数据存储在 HDFS 中,大部分的查询由 MapReduce 完成(包含 * 的查询,比如 select * from tbl 不会生成 MapRedcue 任务)。
ThriftServer 提供了一个很简单的API用于执行HiveQL语句。Thrift框架提供多语言服务,是用一种语言(如Java)编写的客户端,也支持其它语言编写。Thrift客户端可由不同语言生成,用来构建通用驱动程序如JDBC(java),ODBC(c++)以及用php,perl,python等语言编写的脚本驱动。
Metastore是一个系统目录(system catalog)。
Driver掌管HiveQL语句的生命周期,周期包括编译,优化和执行。用于接收了来自thrift服务器或其它接口的HiveQL语句。Driver会创建一个session handle用以统计执行时间,输出行个数等信息。
Compiler由dirver调用,将接收到的HiveQL语句转换为由map-reduce任务的DAG组成的策略(plan)。
ExecutionEngine dirver提交单独的map-reduce任务到执行引擎(Execution Engine),这些任务来自DAG并以拓扑顺序被提交。目前,Hive使用Hadoop做为它的执行引擎
2.1 Metastore
Metastore是系统目录(catalog)用于保存Hive中所存储的表的元数据(metadata)信息。每次在使用HiveQL创建或使用表时均会指定元数据。Metastore是Hive被用作传统数据库解决方案(如oracle和db2)时区别其它类似系统的一个特征,这些系统如Pig和Scope也是构建在map-reduce框架上的数据处理系统。
Metastore包含如下的部分:
Database是表(table)的名字空间。默认的数据库(database)名为‘default’,提供给那些没有被指定数据库名的表使用。
Table表(table)的原数据包含信息有:列(list of columns)和它们的类型(types),拥有者(owner),存储空间(storage)和SerDei信息。还包含有任何用户提供的key和value数据;这一能力在将来还可能被用于存储表(table)的统计信息。存储空间(storage)信息包含表(table)的数据,数据格式和桶信息。SerDe元数据包含有:实现序列化和去序列化方法的类以及实现时所需要的信息。所有这些信息会在表创建的时候产生。
Partition每个分区(partition)都有自己的列(columns),SerDe和存储空间(storage)。这一特征将被用来支持Hive中的模式演变(schema evolution)
Metastore存储系统在随机访问和更新的在线事务上还需优化。像HDFS文件系统适用于序列扫描而非随机访问。这样metastore可使用传统的数据库(像MySQL,Oracle)或文件系统(如本地系统,NFS,AFS)而非HDFS。所以对于只访问元数据的HiveQL语句能被很快执行,Hive必须明确维护元数据和数据的一致性。
2.2 Compiler
Driver调用编译器(compiler)处理HiveQL字串,这些字串可能是一条DDL、DML或查询语句。编译器将字符串转化为策略(plan)。策略仅由元数据操作和HDFS操作组成,元数据操作只包含DDL语句,HDFS操作只包含LOAD语句。对插入和查询而言,策略由map-reduce任务中的具有方向的非循环图(directedacyclic graph,DAG)组成。
解析器(parser)将查询字串转换为解析树表达式
语义分析器(semanticanalyzer)将解析树表达式转换为基于块(block-based)的内部查询表达式,将输入表的模式(schema)信息从metastore中进行恢复。用这些信息验证列名,展开select *以及类型检查(固定类型转换也包含在此检查中)
逻辑策略生成器(LogicalPlan Generator)将内部查询表达式转换为逻辑策略,这些策略由逻辑操作树组成。
优化器(Optimizer)通过逻辑策略构造多途径并以不同方式重写:
将多multiplejoin合并为一个multi-way join,join,group-by和自定义的map-reduce操作重新进行划分消减不必要的列,在表扫描操作中推行使用断言(predicate) 对于已分区的表,消减不必要的分区在抽样(sampling)查询中,消减不必要的桶。外,优化器还能:
增加局部聚合操作用于处理大分组聚合(grouped aggregations)
增加再分区操作用于处理不对称(skew)的分组聚合
物理策略生成器(PhysicalPlan Generator)将逻辑策略转换为物理策略,策略由map-reduce任务的DAG组成。它为逻辑策略中的标记操作子(marker operators),重分区(repartition)或联合(union all)创建新的map-reduce任务,然后将标记间的逻辑策略的一部分分配给map-recude任务的mapper和reducer。
3.调优
1. 设置hive.map.aggr=true,提高HiveQL聚合的执行性能。
这个设置可以将顶层的聚合操作放在Map阶段执行,从而减轻清洗阶段数据传输和Reduce阶段的执行时间,提升总体性能。缺点:该设置会消耗更多的内存。
注:顶层的聚合操作(top-level aggregation operation),是指在group by语句之前执行的聚合操作。例如,
hive> SET hive.map.aggr=true;
hive> SELECTcount(*), avg(salary)FROM employees group by ** having max()>1
2. 显示数据时,使用“local mode”可避免启动MapReduce,显著提升性能。
当执行“select * from table”时,Hive会简单地仅仅从表中读取数据并将格式化的数据输出到控制端,这个过程不会生成MapReduce程序。而对于其它类型的查询,Hive会使用MapReduce执行这些查询。若想避免这些查询的MapReduce执行,可以设置hive.exec.mode.local.auto=true。
注:这种情况下,Hive查询的数据可能不全,只是一个结点上的数据,可供测试查询使用。
3. (陷阱)浮点数的比较
hive> SELECT name, salary, deductions['Federal Taxes']
> FROM employeesWHERE deductions['Federal Taxes'] >0.2;
其中deductions['FederalTaxes']为Double类型数据。
查出的数据实际上是>=0.2的记录。
原因:IEEE的浮点数表示,它影响几乎所有的程序!对于0.2这个数值的在计算机中的Float表示为0.2000001,Double值为0.200000000001,都比0.2略大。
解决方法:使用cast函数。WHEREdeductions['Federal Taxes'] > cast(0.2 AS FLOAT);
4. Hive中进行表的关联查询时,尽可能将较大的表放在Join之后。
Hive在处理表的关联查询时,会默认对最后一张表执行streaming操作,也就是将其它的表缓存起来,将最后一张表与其它的表进行关联,这个操作只会读一遍这张最大的表,反之,该表会读取多次,特别是包含多张表时。另外,由于Join之前的表会默认缓存,如果大表放在前面的位置,也会造成内存的消耗。
但可以通过指令改变这种默认行为:
SELECT /*+ STREAMTABLE(s) */ s.ymd, s.symbol, s.price_close,d.dividend
FROM stocks s JOIN dividends d ON s.ymd = d.ymd AND s.symbol =d.symbol
WHERE s.symbol ='AAPL';
STREAMTABLE会告诉Hive的查询优化器将制定的表作为最大的表来处理。
5. 在where语句中添加分区过滤条件可加速查询的执行。
6. 避免笛卡尔积!
SELECTS * FROM stocks JOIN dividends; //没有指定ON条件,Hive会对两张表执行笛卡尔积连结!
SELECT * FROM stocks JOIN dividends
WHERE stock.symbol =dividends.symbol andstock.symbol='AAPL';// 在Hive中,连结操作会在where条件之前执行,所以这条语句与上一条语句执行时间相当!
SELECT * FROM stocks JOIN dividends ON stock.symbol =dividends.symbol;//这样才能真正执行Inner Join而不是笛卡尔积!
7. Map-side Join:Map端连结
SELECT /*+ MAPJOIN(d) */ s.ymd, s.symbol, s.price_close, d.dividend
FROM stocks s JOIN dividends d ON s.ymd = d.ymd AND s.symbol =d.symbol
WHERE s.symbol ='AAPL';
在Hive v0.7之前,MAPJOIN()会将指定的表,一般是较小的表,加载到内存中,这样整个连结过程会在Map段完成。这样可以避免产生冗余的中间数据(连结产生的中间表)同时也可以免除相应的Reduce操作,进而提高整体性能。
在Hive v0.7之后,需要设置hive.auto.convert.join=true,开启MapJoin功能。
注:另外也可以进行bucketMapJoin的优化,具体理解,待调研。
8. Order by vs. Sort by vs.Distribute By vs. Cluster By
这四个语句都和排序相关,但底层的执行细节不同。
Order By:会将所有的数据在一个reducer上执行,得到的结果是整体有序的。但是由于不能并发执行,所以效率比较低。
Sort By:排序操作在多个reducer上执行,得到的结果是局部有序(一个reducer内)的,但是整体数据不一定是严格有序的。另外,这个语句还可能造成数据的重叠和丢失。由于MapReduce是采用Hash的方式来组织数据的,所以当使用Sort By时,一个reducer的输出会覆盖另一个reducer的数据。
Distribute By:为Sort By而生!它可以修正SortBy带来的负面作用,避免数据的覆盖和丢失。Distribute By将保证具有相同的指定关键字的记录进入到同一个reducer进行处理,这样就可以避免reducer在输出数据时将不同reducer的记录放到同一个位置,从而造成数据的覆盖!
SELECT s.ymd, s.symbol, s.price_close
FROM stocks s
DISTRIBUTE BY s.symbol
SORT BY s.symbol ASC, s.ymd ASC;
Cluster By = Distribute By + Sort By!
但是除了Order By之外产生的所有排序结果默认情况下(除非修改mapred.reduce.tasks的值)都不能做到结果的整体有序。
9. 抽样查询
对于大量的数据,有时会需要查看数据的状态,比如是否有记录,其中的某个字段是否有值,但是若对整张表查询可能会比较耗时,另外得出的结果也不具有随机性。
Hive支持抽样查询èTableSample
例如,有一张表numbers,其中包含一个列number
hive> SELECT * from numbers TABLESAMPLE(BUCKET 3 OUT OF 10 ONrand()) s;
2
4
rand()返回一个随机值,这里对应结果的条数。
hive> SELECT * from numbersTABLESAMPLE(BUCKET 3 OUT OF 10 ON number) s;
2
如果不用rand()而是特定的列名,那么在多次运行中,返回的结果是确定的。
在Bucket语句中,分母(eg.10)代表数据会被散列到的桶的数目,分子代表被选中的桶的编号。
除了按桶抽样,也可以按块进行抽样:
SELECT * FROM numbersflat TABLESAMPLE(0.1PERCENT) s;
注:按块抽样的最小抽样单元是一个HDFS的块,默认情况下,如果表的大小小于128MB,那么所有的列将会被抽出,无论设置的百分比是多少!
10. UNION ALL è 同时返回多张表的查询结果
SELECT log.ymd, log.level, log.message
FROM (
SELECT l1.ymd, l1.level,
l1.message, 'Log1' AS source
FROM log1 l1
UNION ALL
SELECT l2.ymd, l2.level,
l2.message, 'Log2' AS source
FROM log1 l2
) log
SORT BY log.ymd ASC;
注:要求两个表查询结果的字段的个数和类型必须一致!
11. (技巧)EXPLAIN/EXPLAIN EXTENDED è展开查询计划树,可以作为优化查询的工具。
12. Limit 优化 èhive.limit.optimize.enable=true
可以避免查询中对整张表的query,它受一下两个条件的约束:
how much size we need to guarantee each row to have at least.
maximum number of files we can sample.
13. 本地模式èlocal mode
对于输入数据比较多,但是每个文件又特别的小的情况下,这样可以避免调用task造成的开销:
set oldjobtracker=${hiveconf:mapred.job.tracker};
set mapred.job.tracker=local;
set mapred.tmp.dir=/home/edward/tmp;
SELECT * from people WHERE firstname=bob;
… …
setmapred.job.tracker=${oldjobtracker};
14. 并行执行è hive.exec.parallel=true
Hive将一个query语句转换成多阶段任务来执行,每次执行一个阶段的任务。
当被解析成的多个阶段之间不存在依赖的时候,可以让多个阶段的任务并行执行,这可以大大加快任务执行的速度,但同时也许需要更多的集群资源。
15. Mapper与Reducer数量的优化
折衷:数量太大,会导致任务的启动、调度和运行过程的开销太大;数量太小,无法很好地利用集群的并发特性。
Hive会在接收到查询任务后,根据输入数据的大小评估所需要的reducer数量,但这个过程需要时间开销。默认的hive.exec.reducers.bytes.per.reducer是1GB,也可以改变这个值。
如何自己评估输入数据的大小?
[edward@etl02 ~]$ hadoop dfs -count /user/media6/fracture/ins/* |tail -4
1 82614608737 hdfs://.../user/media6/fracture/ins/hit_date=20120118
1 72742992546 hdfs://.../user/media6/fracture/ins/hit_date=20120119
1 172656878252 hdfs://.../user/media6/fracture/ins/hit_date=20120120
1 2 362657644hdfs://.../user/media6/fracture/ins/hit_date=20120121
注:当在执行Hadoop任务时,特别是hadoop-streaming脚本,如果只有mapper而没有reducer的话,可以将reducer数量设为0,这可以作为解决数据倾斜的一种方法!
16. JVM 复用 è 在一个JVM实例上运行多个mapreduce任务,减少创建jvm实例的开销
JVM Reuse | 139
缺点:会造成潜在的集群资源的浪费。
17. 探测性执行(Speculative Execution)
所谓探测性执行,是指Hadoop会启动同一个任务的多个副本在集群上执行,但它会丢弃该阶段的产生的多个副本的数据。
这个阶段会消耗更多的集群资源,目的是为了探测执行较慢的TaskTrackers,并将它们列入黑名单,进而提升整体工作流程的性能。
may be executed in parallel.
may be executed in parallel.
18. 虚拟列
Hive提供了三个虚拟列:INPUT__FILE__NAME,BLOCK__OFFSET__INSIDE__FILE和ROW__OFFSET__INSIDE__BLOCK。但ROW__OFFSET__INSIDE__BLOCK默认是不可用的,需要设置hive.exec.rowoffset为true才可以。可以用来排查有问题的输入数据。
hive> SELECT INPUT__FILE__NAME, BLOCK__OFFSET__INSIDE__FILE, line
> FROM hive_text WHERE line LIKE '%hive%' LIMIT 2;
har://file/user/hive/warehouse/hive_text/folder=docs/
data.har/user/hive/warehouse/hive_text/folder=docs/README.txt 2243
har://file/user/hive/warehouse/hive_text/folder=docs/
data.har/user/hive/warehouse/hive_text/folder=docs/README.txt 3646
hive> set hive.exec.rowoffset=true;
hive> SELECT INPUT__FILE__NAME, BLOCK__OFFSET__INSIDE__FILE,
> ROW__OFFSET__INSIDE__BLOCK
> FROM hive_text WHERE line LIKE '%hive%' limit 2;
file:/user/hive/warehouse/hive_text/folder=docs/README.txt 2243 0
file:/user/hive/warehouse/hive_text/folder=docs/README.txt3646 0
4.参数设置
对于一般参数,有以下三种设定方式:
• 配置文件
• 命令行参数
• 参数声明
配置文件:Hive的配置文件包括
• 用户自定义配置文件:$HIVE_CONF_DIR/hive-site.xml
• 默认配置文件:$HIVE_CONF_DIR/hive-default.xml
用户自定义配置会覆盖默认配置。另外,Hive也会读入Hadoop的配置,因为Hive是作为Hadoop的客户端启动的,Hadoop的配置文件包括
• $HADOOP_CONF_DIR/hive-site.xml
• $HADOOP_CONF_DIR/hive-default.xml
Hive的配置会覆盖Hadoop的配置。
配置文件的设定对本机启动的所有Hive进程都有效。
命令行参数:启动Hive(客户端或Server方式)时,可以在命令行添加-hiveconf param=value来设定参数,例如:
bin/hive -hiveconfhive.root.logger=INFO,console
这一设定对本次启动的Session(对于Server方式启动,则是所有请求的Sessions)有效。
参数声明:可以在HQL中使用SET关键字设定参数,例如:
set mapred.reduce.tasks=100;
这一设定的作用域也是Session级的。
上述三种设定方式的优先级依次递增。即参数声明覆盖命令行参数,命令行参数覆盖配置文件设定。
5.使用HIVE注意点
5.1 字符集
Hadoop和Hive都是用UTF-8编码的,所以, 所有中文必须是UTF-8编码, 才能正常使用
5.2 压缩
hive.exec.compress.output 这个参数, 默认是 false,但是很多时候貌似要单独显式设置一遍否则会对结果做压缩的,如果你的这个文件后面还要在hadoop下直接操作, 那么就不能压缩了
5.3 count(distinct)
当前的 Hive 不支持在一条查询语句中有多 Distinct。如果要在 Hive 查询语句中实现多Distinct,需要使用至少 n+1 条查询语句(n为distinct的数目),前 n 条查询分别对 n 个列去重,最后一条查询语句对 n 个去重之后的列做 Join 操作,得到最终结果。
5.4 JOIN
只支持等值连接
5.5 DML操作
只支持INSERT/LOAD操作,无UPDATE和DELTE
5.6 HAVING
不支持HAVING操作。如果需要这个功能要嵌套一个子查询用where限制
5.7 子查询
Hive不支持where子句中的子查询
5.8 Join中处理null值的语义区别
SQL标准中,任何对null的操作(数值比较,字符串操作等)结果都为null。Hive对null值处理的逻辑和标准基本一致,除了Join时的特殊逻辑。
这里的特殊逻辑指的是,Hive的Join中,作为Join key的字段比较,null=null是有意义的,且返回值为true。
select u.uid, count(u.uid) from t_weblog ljoin t_user u on (l.uid = u.uid) group by u.uid;
查询中,t_weblog表中uid为空的记录将和t_user表中uid为空的记录做连接,即l.uid = u.uid=null成立。
如果需要与标准一致的语义,我们需要改写查询手动过滤null值的情况:
select u.uid, count(u.uid)
from t_weblog l join t_user u
on (l.uid = u.uid and l.uid is not null andu.uid is not null)
group by u.uid;
实践中,这一语义区别也是经常导致数据倾斜的原因之一。
5.9 分号字符
分号是SQL语句结束标记,在HiveQL中也是,但是在HiveQL中,对分号的识别没有那么智慧,例如:
select concat(cookie_id,concat(';',’zoo’))from c02_clickstat_fatdt1 limit 2;
FAILED: Parse Error: line 0:-1 cannotrecognize input '
可以推断,Hive解析语句的时候,只要遇到分号就认为语句结束,而无论是否用引号包含起来。
解决的办法是,使用分号的八进制的ASCII码进行转义,那么上述语句应写成:
selectconcat(cookie_id,concat('\073','zoo')) from c02_clickstat_fatdt1 limit 2;
为什么是八进制ASCII码?
我尝试用十六进制的ASCII码,但Hive会将其视为字符串处理并未转义,好像仅支持八进制,原因不详。这个规则也适用于其他非SELECT语句,如CREATE TABLE中需要定义分隔符,那么对不可见字符做分隔符就需要用八进制的ASCII码来转义。
5.10 Insert
5.10.1 新增数据
根据语法Insert必须加“OVERWRITE”关键字,也就是说每一次插入都是一次重写。那如何实现表中新增数据呢?
假设Hive中有表xiaojun1,
hive> DESCRIBE xiaojun1;
OK
id int
value int
hive> SELECT * FROM xiaojun1;
OK
3 4
1 2
2 3
现增加一条记录:
hive> INSERT OVERWRITE TABLE xiaojun1
SELECT id, value FROM (
SELECT id, value FROM xiaojun1
UNION ALL
SELECT 4 AS id, 5 AS value FROM xiaojun1limit 1
) u;
结果是:
hive>SELECT * FROM p1;
OK
3 4
4 5
2 3
1 2
其中的关键在于, 关键字UNION ALL的应用, 即将原有数据集和新增数据集进行结合, 然后重写表.
5.10.2 插入次序
INSERT OVERWRITE TABLE在插入数据时,是按照后面的SELECT语句中的字段顺序插入的. 也就说, 当id 和value 的位置互换, 那么value将被写入id, 同id被写入value.
5.10.3 初始值
INSERT OVERWRITE TABLE在插入数据时, 后面的字段的初始值应注意与表定义中的一致性. 例如, 当为一个STRING类型字段初始为NULL时:
NULL AS field_name // 这可能会被提示定义类型为STRING,但这里是void
CAST(NULL AS STRING) AS field_name // 这样是正确的
又如, 为一个BIGINT类型的字段初始为0时:
CAST(0 AS BIGINT) AS field_name
6.优化
6.1 HADOOP计算框架特性
• 数据量大不是问题,数据倾斜是个问题。
• jobs数比较多的作业运行效率相对比较低,比如即使有几百行的表,如果多次关联多次汇总,产生十几个jobs,耗时很长。原因是map reduce作业初始化的时间是比较长的。
• sum,count,max,min等UDAF,不怕数据倾斜问题,hadoop在map端的汇总合并优化,使数据倾斜不成问题。
• count(distinct ),在数据量大的情况下,效率较低,如果是多count(distinct )效率更低,因为count(distinct)是按group by 字段分组,按distinct字段排序,一般这种分布方式是很倾斜的,比如男uv,女uv,淘宝一天30亿的pv,如果按性别分组,分配2个reduce,每个reduce处理15亿数据。
6.2 优化的常用手段
• 好的模型设计事半功倍。
• 解决数据倾斜问题。
• 减少job数。
• 设置合理的mapreduce的task数,能有效提升性能。(比如,10w+级别的计算,用160个reduce,那是相当的浪费,1个足够)。
• 了解数据分布,自己动手解决数据倾斜问题是个不错的选择。set hive.groupby.skewindata=true;这是通用的算法优化,但算法优化有时不能适应特定业务背景,开发人员了解业务,了解数据,可以通过业务逻辑精确有效的解决数据倾斜问题。
• 数据量较大的情况下,慎用count(distinct),count(distinct)容易产生倾斜问题。
• 对小文件进行合并,是行至有效的提高调度效率的方法,假如所有的作业设置合理的文件数,对云梯的整体调度效率也会产生积极的正向影响。
• 优化时把握整体,单个作业最优不如整体最优。
6.3 全排序
Hive的排序关键字是SORT BY,它有意区别于传统数据库的ORDERBY也是为了强调两者的区别–SORT BY只能在单机范围内排序。
6.4 怎样做笛卡尔积
当Hive设定为严格模式(hive.mapred.mode=strict)时,不允许在HQL语句中出现笛卡尔积,这实际说明了Hive对笛卡尔积支持较弱。因为找不到Join key,Hive只能使用1个reducer来完成笛卡尔积。
当然也可以用上面说的limit的办法来减少某个表参与join的数据量,但对于需要笛卡尔积语义的需求来说,经常是一个大表和一个小表的Join操作,结果仍然很大(以至于无法用单机处理),这时MapJoin才是最好的解决办法。
MapJoin,顾名思义,会在Map端完成Join操作。这需要将Join操作的一个或多个表完全读入内存。
MapJoin的用法是在查询/子查询的SELECT关键字后面添加/*+MAPJOIN(tablelist) */提示优化器转化为MapJoin(目前Hive的优化器不能自动优化MapJoin)。其中tablelist可以是一个表,或以逗号连接的表的列表。tablelist中的表将会读入内存,应该将小表写在这里。
6.5 怎样写exist/in子句
Hive不支持where子句中的子查询,SQL常用的exist in子句需要改写。这一改写相对简单。考虑以下SQL查询语句:
SELECT a.key, a.value FROM a WHERE a.key in(SELECT b.key FROM B);
可以改写为
SELECT a.key, a.value FROM a LEFT OUTERJOIN b ON (a.key = b.key) WHERE b.key <> NULL;
一个更高效的实现是利用left semi join改写为:
SELECT a.key, a.val FROM a LEFT SEMI JOIN bon (a.key = b.key);
left semi join是0.5.0以上版本的特性。
6.6 怎样决定reducer个数
Hadoop MapReduce程序中,reducer个数的设定极大影响执行效率,这使得Hive怎样决定reducer个数成为一个关键问题。遗憾的是Hive的估计机制很弱,不指定reducer个数的情况下,Hive会猜测确定一个reducer个数,基于以下两个设定:
1. hive.exec.reducers.bytes.per.reducer(默认为1000^3)
2. hive.exec.reducers.max(默认为999)
计算reducer数的公式很简单:
N=min(参数2,总输入数据量/参数1)
通常情况下,有必要手动指定reducer个数。考虑到map阶段的输出数据量通常会比输入有大幅减少,因此即使不设定reducer个数,重设参数2还是必要的。依据Hadoop的经验,可以将参数2设定为0.95*(集群中TaskTracker个数)。
6.7 合并MapReduce操作
Multi-group by
Multi-group by是Hive的一个非常好的特性,它使得Hive中利用中间结果变得非常方便。例如,
FROM (SELECT a.status, b.school, b.gender
FROM status_updates a JOIN profiles b
ON (a.userid = b.userid and
a.ds='2009-03-20' )
) subq1
INSERT OVERWRITE TABLE gender_summary
PARTITION(ds='2009-03-20')
SELECT subq1.gender, COUNT(1) GROUP BYsubq1.gender
INSERT OVERWRITE TABLE school_summary
PARTITION(ds='2009-03-20')
SELECT subq1.school, COUNT(1) GROUP BYsubq1.school
上述查询语句使用了Multi-group by特性连续group by了2次数据,使用不同的group by key。这一特性可以减少一次MapReduce操作。
Multi-distinct
Multi-distinct是淘宝开发的另一个multi-xxx特性,使用Multi-distinct可以在同一查询/子查询中使用多个distinct,这同样减少了多次MapReduce操作
6.8 Bucket与 sampling
Bucket是指将数据以指定列的值为key进行hash,hash到指定数目的桶中。这样就可以支持高效采样了。
如下例就是以userid这一列为bucket的依据,共设置32个buckets
CREATE TABLE page_view(viewTime INT, useridBIGINT,
page_url STRING,referrer_url STRING,
ip STRING COMMENT 'IPAddress of the User')
COMMENT 'This is the page view table'
PARTITIONED BY(dt STRING, country STRING)
CLUSTERED BY(userid) SORTED BY(viewTime) INTO 32 BUCKETS
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '1'
COLLECTION ITEMS TERMINATED BY '2'
MAP KEYS TERMINATED BY '3'
STORED AS SEQUENCEFILE;
Sampling可以在全体数据上进行采样,这样效率自然就低,它还是要去访问所有数据。而如果一个表已经对某一列制作了bucket,就可以采样所有桶中指定序号的某个桶,这就减少了访问量。
如下例所示就是采样了page_view中32个桶中的第三个桶。
SELECT * FROM page_view TABLESAMPLE(BUCKET3 OUT OF 32);
6.9 Partition
Partition就是分区。分区通过在创建表时启用partitionby实现,用来partition的维度并不是实际数据的某一列,具体分区的标志是由插入内容时给定的。当要查询某一分区的内容时可以采用where语句,形似where tablename.partition_key > a来实现。
创建含分区的表
CREATE TABLE page_view(viewTime INT, userid BIGINT,
page_url STRING,referrer_url STRING,
ip STRING COMMENT 'IPAddress of the User')
PARTITIONED BY(date STRING, country STRING)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '1'
STORED AS TEXTFILE;
载入内容,并指定分区标志
LOAD DATA LOCAL INPATH`/tmp/pv_2008-06-08_us.txt` INTO TABLE page_view PARTITION(date='2008-06-08',country='US');
查询指定标志的分区内容
SELECT page_views.*
FROM page_views
WHERE page_views.date >= '2008-03-01' AND page_views.date <='2008-03-31' AND page_views.referrer_url like '%xyz.com';
6.10 JOIN
6.10.1 JOIN原则
在使用写有 Join 操作的查询语句时有一条原则:应该将条目少的表/子查询放在 Join 操作符的左边。原因是在 Join 操作的 Reduce 阶段,位于 Join 操作符左边的表的内容会被加载进内存,将条目少的表放在左边,可以有效减少发生 OOM 错误的几率。对于一条语句中有多个 Join 的情况,如果 Join 的条件相同,比如查询:
INSERT OVERWRITE TABLE pv_users SELECT pv.pageid, u.age FROM page_view p JOIN user u ON (pv.userid = u.userid) JOIN newuser x ON (u.userid = x.userid);
• 如果 Join 的 key 相同,不管有多少个表,都会则会合并为一个 Map-Reduce
• 一个 Map-Reduce任务,而不是 ‘n’ 个
• 在做 OUTER JOIN的时候也是一样
如果 Join 的条件不相同,比如:
INSERT OVERWRITE TABLE pv_users
SELECT pv.pageid, u.age FROM page_view p
JOIN user u ON (pv.userid = u.userid)
JOIN newuser x on (u.age = x.age);
Map-Reduce 的任务数目和 Join 操作的数目是对应的,上述查询和以下查询是等价的:
INSERT OVERWRITE TABLE tmptable
SELECT * FROM page_view p JOIN user u
ON(pv.userid = u.userid);
INSERT OVERWRITE TABLE pv_users
SELECT x.pageid, x.age FROM tmptable x
JOIN newuser y ON (x.age = y.age);
6.10.2 MapJoin
Join 操作在 Map 阶段完成,不再需要Reduce,前提条件是需要的数据在Map 的过程中可以访问到。比如查询:
INSERT OVERWRITE TABLE pv_users
SELECT /*+ MAPJOIN(pv) */ pv.pageid, u.age
FROM page_view pv
JOIN user u ON (pv.userid = u.userid);
可以在 Map 阶段完成 Join,如图所示:
相关的参数为:
• hive.join.emit.interval= 1000 How many rows in the right-most join operand Hive should buffer beforeemitting the join result.
• hive.mapjoin.size.key =10000
• hive.mapjoin.cache.numrows= 10000
6.11 数据倾斜
6.11.1 空值数据倾斜
场景:如日志中,常会有信息丢失的问题,比如全网日志中的user_id,如果取其中的user_id和bmw_users关联,会碰到数据倾斜的问题。
解决方法1: user_id为空的不参与关联
Select * From log a Join bmw_users b On a.user_id is not null Anda.user_id = b.user_id Union all Select * from log a where a.user_id is null;
解决方法2 :赋与空值分新的key值
Select * from log a left outer join bmw_users b on case when a.user_id is nullthen concat(‘dp_hive’,rand() ) else a.user_id end = b.user_id;
结论:方法2比方法效率更好,不但io少了,而且作业数也少了。方法1 log读取两次,jobs是2。方法2 job数是1 。这个优化适合无效id(比如-99,’’,null等)产生的倾斜问题。把空值的key变成一个字符串加上随机数,就能把倾斜的数据分到不同的reduce上 ,解决数据倾斜问题。附上hadoop通用关联的实现方法(关联通过二次排序实现的,关联的列为parition key,关联的列c1和表的tag组成排序的group key,根据parition key分配reduce。同一reduce内根据group key排序)
6.11.2 不同数据类型关联产生数据倾斜
场景:一张表s8的日志,每个商品一条记录,要和商品表关联。但关联却碰到倾斜的问题。s8的日志中有字符串商品id,也有数字的商品id,类型是string的,但商品中的数字id是bigint的。猜测问题的原因是把s8的商品id转成数字id做hash来分配reduce,所以字符串id的s8日志,都到一个reduce上了,解决的方法验证了这个猜测。
解决方法:把数字类型转换成字符串类型
Select * from s8_log a Left outer joinr_auction_auctions b On a.auction_id = cast(b.auction_id as string);
6.11.3 大表Join的数据偏斜
MapReduce编程模型下开发代码需要考虑数据偏斜的问题,Hive代码也是一样。数据偏斜的原因包括以下两点:
1. Map输出key数量极少,导致reduce端退化为单机作业。
2. Map输出key分布不均,少量key对应大量value,导致reduce端单机瓶颈。
Hive中我们使用MapJoin解决数据偏斜的问题,即将其中的某个表(全量)分发到所有Map端进行Join,从而避免了reduce。这要求分发的表可以被全量载入内存。
极限情况下,Join两边的表都是大表,就无法使用MapJoin。
这种问题最为棘手,目前已知的解决思路有两种:
1. 如果是上述情况1,考虑先对Join中的一个表去重,以此结果过滤无用信息。这样一般会将其中一个大表转化为小表,再使用MapJoin 。
一个实例是广告投放效果分析,例如将广告投放者信息表i中的信息填充到广告曝光日志表w中,使用投放者id关联。因为实际广告投放者数量很少(但是投放者信息表i很大),因此可以考虑先在w表中去重查询所有实际广告投放者id列表,以此Join过滤表i,这一结果必然是一个小表,就可以使用MapJoin。
2. 如果是上述情况2,考虑切分Join中的一个表为多片,以便将切片全部载入内存,然后采用多次MapJoin得到结果。
一个实例是商品浏览日志分析,例如将商品信息表i中的信息填充到商品浏览日志表w中,使用商品id关联。但是某些热卖商品浏览量很大,造成数据偏斜。例如,以下语句实现了一个inner join逻辑,将商品信息表拆分成2个表:
select * from
(
select w.id, w.time, w.amount, i1.name,i1.loc, i1.cat
from w left outer join i sampletable(1 outof 2 on id) i1
)
union all
(
select w.id, w.time, w.amount, i2.name,i2.loc, i2.cat
from w left outer join i sampletable(1 outof 2 on id) i2
));
以下语句实现了left outer join逻辑:
select t1.id, t1.time, t1.amount,
coalease(t1.name, t2.name),
coalease(t1.loc, t2.loc),
coalease(t1.cat, t2.cat)
from (
select w.id, w.time, w.amount, i1.name, i1.loc, i1.cat
from w left outer join i sampletable(1 out of 2 on id) i1
) t1 left outer join i sampletable(2 out of2 on id) t2;
上述语句使用Hive的sample table特性对表做切分。
6.12 合并小文件
文件数目过多,会给 HDFS 带来压力,并且会影响处理效率,可以通过合并 Map 和 Reduce 的结果文件来消除这样的影响:
hive.merge.mapfiles = true 是否和并 Map 输出文件,默认为 True
hive.merge.mapredfiles = false 是否合并 Reduce 输出文件,默认为False
hive.merge.size.per.task = 256*1000*1000 合并文件的大小
6.13 GroupBy
• Map 端部分聚合:
并不是所有的聚合操作都需要在 Reduce 端完成,很多聚合操作都可以先在 Map 端进行部分聚合,最后在 Reduce 端得出最终结果。
基于 Hash
参数包括:
hive.map.aggr= true 是否在 Map 端进行聚合,默认为 True
hive.groupby.mapaggr.checkinterval= 100000 在 Map 端进行聚合操作的条目数目
• 有数据倾斜的时候进行负载均衡
hive.groupby.skewindata = false
当选项设定为 true,生成的查询计划会有两个MR Job。第一个 MR Job 中,Map 的输出结果集合会随机分布到 Reduce 中,每个 Reduce 做部分聚合操作,并输出结果,这样处理的结果是相同的 Group By Key 有可能被分发到不同的 Reduce 中,从而达到负载均衡的目的;第二个 MR Job 根据预处理的数据结果按照 Group By Key 分布到 Reduce 中(这个过程可以保证相同的 Group By Key 被分布到同一个 Reduce 中),最后完成最终的聚合操作。
7.对比
Hive |
RDBMS |
|
查询语言 |
HQL |
SQL |
数据存储 |
HDFS |
Raw Device or Local FS |
索引 |
无 |
有 |
执行 |
MapReduce |
Excutor |
执行延迟 |
高 |
低 |
处理数据规模 |
大 |
小 |
1. 查询语言。由于 SQL 被广泛的应用在数据仓库中,因此,专门针对 Hive 的特性设计了类 SQL 的查询语言 HQL。熟悉 SQL 开发的开发者可以很方便的使用 Hive 进行开发。
2. 数据存储位置。Hive 是建立在Hadoop 之上的,所有 Hive 的数据都是存储在HDFS 中的。而数据库则可以将数据保存在块设备或者本地文件系统中。
3. 数据格式。Hive 中没有定义专门的数据格式,数据格式可以由用户指定,用户定义数据格式需要指定三个属性:列分隔符(通常为空格、”\t”、”\x001″)、行分隔符(”\n”)以及读取文件数据的方法(Hive 中默认有三个文件格式 TextFile,SequenceFile 以及 RCFile)。由于在加载数据的过程中,不需要从用户数据格式到 Hive 定义的数据格式的转换,因此,Hive 在加载的过程中不会对数据本身进行任何修改,而只是将数据内容复制或者移动到相应的 HDFS
目录中。而在数据库中,不同的数据库有不同的存储引擎,定义了自己的数据格式。所有数据都会按照一定的组织存储,因此,数据库加载数据的过程会比较耗时。
4. 数据更新。由于 Hive 是针对数据仓库应用设计的,而数据仓库的内容是读多写少的。因此,Hive 中不支持对数据的改写和添加,所有的数据都是在加载的时候中确定好的。而数据库中的数据通常是需要经常进行修改的,因此可以使用 INSERT INTO … VALUES 添加数据,使用 UPDATE… SET 修改数据。
5. 索引。之前已经说过,Hive 在加载数据的过程中不会对数据进行任何处理,甚至不会对数据进行扫描,因此也没有对数据中的某些 Key 建立索引。Hive 要访问数据中满足条件的特定值时,需要暴力扫描整个数据,因此访问延迟较高。由于 MapReduce 的引入, Hive 可以并行访问数据,因此即使没有索引,对于大数据量的访问,Hive 仍然可以体现出优势。数据库中,通常会针对一个或者几个列建立索引,因此对于少量的特定条件的数据的访问,数据库可以有很高的效率,较低的延迟。由于数据的访问延迟较高,决定了Hive 不适合在线数据查询。
6. 执行。Hive 中大多数查询的执行是通过 Hadoop 提供的 MapReduce 来实现的(类似 select * from tbl 的查询不需要 MapReduce)。而数据库通常有自己的执行引擎。
7. 执行延迟。之前提到,Hive 在查询数据的时候,由于没有索引,需要扫描整个表,因此延迟较高。另外一个导致 Hive 执行延迟高的因素是 MapReduce 框架。由于 MapReduce 本身具有较高的延迟,因此在利用 MapReduce 执行 Hive 查询时,也会有较高的延迟。相对的,数据库的执行延迟较低。当然,这个低是有条件的,即数据规模较小,当数据规模大到超过数据库的处理能力的时候,Hive 的并行计算显然能体现出优势。
8. 可扩展性。由于 Hive 是建立在 Hadoop 之上的,因此 Hive 的可扩展性是和 Hadoop 的可扩展性是一致的(世界上最大的 Hadoop 集群在 Yahoo!,2009年的规模在4000 台节点左右)。而数据库由于 ACID 语义的严格限制,扩展行非常有限。目前最先进的并行数据库 Oracle 在理论上的扩展能力也只有 100 台左右。
9. 数据规模。由于 Hive 建立在集群上并可以利用MapReduce 进行并行计算,因此可以支持很大规模的数据;对应的,数据库可以支持的数据规模较小。
8.中文乱码
mysql> create database xiaoyou2 character set gbk; 解决方案:my.cnf里直接把服务器字符集设为gbk mysql> show variables like 'character\_set\_%'; 1、然后进入数据库执行以下5条SQL语句: (1)修改表字段注解和表注解 alter table COLUMNS_V2 modify column COMMENT varchar(256) character set utf8 alter table TABLE_PARAMS modify column PARAM_VALUE varchar(4000) character set utf8 (2) 修改分区字段注解: alter table PARTITION_PARAMS modify column PARAM_VALUE varchar(4000) character set utf8 ; alter table PARTITION_KEYS modify column PKEY_COMMENT varchar(4000) character set utf8; (3)修改索引注解: alter table INDEX_PARAMS modify column PARAM_VALUE varchar(4000) character set utf8; 2、修改hive连接mysql的连接为utf-8 |
9.权限
Hive从0.10可以通过元数据控制权限。但是Hive的权限控制并不是完全安全的。基本的授权方案的目的是防止用户不小心做了不合适的事情。先决条件:为了使用Hive的授权机制,有两个参数必须在hive-site.xml中设置:
|
含义分别是开启权限验证;表的创建者对表拥有所有权限; hive.security.authorization.createtable.owner.grants默认值为NULL,所以表的创建者无法访问该表,这明显是不合理的。
用户,组,角色:
Hive授权的核心就是用户、组、角色。
Hive中的角色和平常我们认知的角色是有区别的。Hive中的角色可以理解为一部分有一些相同“属性”的用户或组或角色的集合。这里有个递归的概念,就是一个角色可以是一些角色的集合。
用户组 张三 G_db1 李四 G_db2 王五 G_bothdb
如上有三个用户分别属于G_db1、G_db2、G_alldb。G_db1、G_db2、G_ bothdb分别表示该组用户可以访问数据库1、数据库2和可以访问1、2两个数据库。现在可以创建role_db1和role_db2,分别并授予访问数据库1和数据库2的权限。这样只要将role_eb 1赋给G_db1(或者该组的所偶用户),将role_eb2赋给G_db2,就可以是实现指定用户访问指定数据库。最后创建role_bothdb指向role_db1、role_db2(role_bothdb不需要指定访问那个数据库),然后role_bothdb授予G_bothdb,则G_bothdb中的用户可以访问两个数据库。
使用和组使用的是Linux机器上的用户和组,而角色必须自己创建。
注意:如果有一个属于组bar的用户foo,他通过cli连接到远程的Server上执行操作,而远程的Server上有一个用户foo属于baz组,则在权限控制中foo是对应的baz组的。
角色的创建、删除、使用:创建和删除:
CREATE ROLE ROLE_NAME
DROP ROLE ROLE_NAME
grant/revoke:
GRANT ROLE role_name [, role_name] ... TOprincipal_specification [, principal_specification] ...
REVOKE ROLE role_name [, role_name] ...FROM principal_specification [, principal_specification] ...
principal_specification :
USER user | GROUP group | ROLE role
查看用户\组\角色的角色: SHOW ROLE GRANTprincipal_specification
示例:create role testrole;
grant role testrole to user yinxiu;
SHOW ROLE GRANT user yinxiu;
OK
role name:testrole
role name:testrole
Time taken: 0.01 seconds
revoke role testrole from user yinxiu;
HIVE支持以下权限:
权限名称 |
含义 |
ALL |
所有权限 |
ALTER |
允许修改元数据(modify metadata data of object)---表信息数据 |
UPDATE |
允许修改物理数据(modify physical data of object)---实际数据 |
CREATE |
允许进行Create操作 |
DROP |
允许进行DROP操作 |
INDEX |
允许建索引(目前还没有实现) |
LOCK |
当出现并发的使用允许用户进行LOCK和UNLOCK操作 |
SELECT |
允许用户进行SELECT操作 |
SHOW_DATABASE |
允许用户查看可用的数据库 |
查看权限: SHOW GRANT principal_specification [ON object_type priv_level[(column_list)]]
实现HIVE中的超级管理员。HIVE本身有权限管理功能,需要通过配置开启。
其中hive.security.authorization.createtable.owner.grants设置成ALL表示用户对自己创建的表是有所有权限的(这样是比较合理地)。
开启权限控制有Hive的权限功能还有一个需要完善的地方,那就是“超级管理员”。
Hive中没有超级管理员,任何用户都可以进行Grant/Revoke操作,为了完善“超级管理员”,必须添加hive.semantic.analyzer.hook配置,并实现自己的权限控制类。
package com.xxx.hive; 10 import org.apache.hadoop.hive.ql.parse.ASTNode; 11 import org.apache.hadoop.hive.ql.parse.AbstractSemanticAnalyzerHook; 12 import org.apache.hadoop.hive.ql.parse.HiveParser; 13 import org.apache.hadoop.hive.ql.parse.HiveSemanticAnalyzerHookContext; 14 import org.apache.hadoop.hive.ql.parse.SemanticException; 15 import org.apache.hadoop.hive.ql.session.SessionState; 23 public class AuthHook extends AbstractSemanticAnalyzerHook { 24 private static String admin = "xxxxxx"; 26 @Override 27 public ASTNode preAnalyze(HiveSemanticAnalyzerHookContext context, 28 ASTNode ast) throws SemanticException { 29 switch (ast.getToken().getType()) { 30 case HiveParser.TOK_CREATEDATABASE: 31 case HiveParser.TOK_DROPDATABASE: 32 case HiveParser.TOK_CREATEROLE: 33 case HiveParser.TOK_DROPROLE: 34 case HiveParser.TOK_GRANT: 35 case HiveParser.TOK_REVOKE: 36 case HiveParser.TOK_GRANT_ROLE: 37 case HiveParser.TOK_REVOKE_ROLE: 38 String userName = null; 39 if (SessionState.get() != null 40 && SessionState.get().getAuthenticator() != null) { 41 userName = SessionState.get().getAuthenticator().getUserName(); 42 } 43 if (!admin.equalsIgnoreCase(userName)) { 44 throw new SemanticException(userName 45 + " can't use ADMIN options, except " + admin + "."); 46 } 47 break; 48 default: 49 break; 50 } 51 return ast; 52 } 53 } |
添加了控制类之后还必须添加下面的配置:
(若有使用hiveserver,hiveserver必须重启)
至此,只有xxxxxx用户可以进行Grant/Revoke操作
权限操作示例:
grant select on database default to userxiaohai;
revoke all on database default from useryinxiu;
show grant user xiaohai on databasedefault;
10.HIVE HA
将若干hive 实例纳入一个资源池,然后对外提供一个唯一的接口,进行proxy relay。
对于程序开发人员,就把它认为是一台超强“hive"就可以。每次它接收到一个HIVE查询连接后,都会轮询资源池里可用的hive 资源。
这样,能充分使用每个hive server,减少压力。在拿到hive 连接后,Hive HA会首先进行逻辑可用测试,这个逻辑规则可自行配置。
如果逻辑可用,则直接把客户端的HIVE 查询连接 relay到该hive server。
若逻辑不可用,则将该hiveserver放入黑名单,然后继续读取池里其他hive server进行连接测试。
Hive Ha每隔一段时间(可配置),对黑名单中的hive server进行处理,通过和节点管理服务器通讯,重启该hive server。如果重启后可用,则将该hive从黑名单中移除,加入资源池。
Hive使用HAProxy配置HA(高可用)
HAProxy是一款提供高可用性、负载均衡以及基于TCP(第四层)和HTTP(第七层)应用的代理软件,HAProxy是完全免费的、借助HAProxy可以快速并且可靠的提供基于TCP和HTTP应用的代理解决方案。
免费开源,稳定性也是非常好,这个可通过我做的一些小项目可以看出来,单Haproxy也跑得不错,稳定性可以与硬件级的F5相媲美。根据官方文档,HAProxy可以跑满10Gbps-New benchmark of HAProxy at 10 Gbps using Myricom's 10GbENICs (Myri-10G PCI-Express),这个数值作为软件级负载均衡器是相当惊人的。
HAProxy 支持连接拒绝 : 因为维护一个连接的打开的开销是很低的,有时我们很需要限制攻击蠕虫(attack bots),也就是说限制它们的连接打开从而限制它们的危害。这个已经为一个陷于小型DDoS攻击的网站开发了而且已经拯救了很多站点,这个优点也是其它负载均衡器没有的。
HAProxy 支持全透明代理(已具备硬件防火墙的典型特点): 可以用客户端IP地址或者任何其他地址来连接后端服务器. 这个特性仅在Linux 2.4/2.6内核打了cttproxy补丁后才可以使用. 这个特性也使得为某特殊服务器处理部分流量同时又不修改服务器的地址成为可能。
HAProxy现多于线上的Mysql集群环境,我们常用于它作为MySQL(读)负载均衡;
自带强大的监控服务器状态的页面,实际环境中我们结合Nagios进行邮件或短信报警,这个也是我非常喜欢它的原因之一;
HAProxy支持虚拟主机,许多朋友说它不支持虚拟主机是错误的,通过测试我们知道,HAProxy是支持虚拟主机的。
HAProxy特别适用于那些负载特大的web站点, 这些站点通常又需要会话保持或七层处理。HAProxy运行在当前的硬件上,完全可以支持数以万计的并发连接。并且它的运行模式使得它可以很简单安全的整合进您当前的架构中,同时可以保护你的web服务器不被暴露到网络上。
安装配置
在HAProxy官网下载安装包并编译
wget http://haproxy.1wt.eu/download/1.4/src/haproxy-1.4.24.tar.gz|tarzxvf
mv haproxy-1.4.24 /opt/haproxy-1.4.24
cd /opt/haproxy-1.4.24
make TARGET=linux26
添加配置文件
在/opt/haproxy-1.4.24目录下创建一个config.cfg文件,添加如下内容:
global
daemon
nbproc 1
pidfile /var/run/haproxy.pid
ulimit-n 65535
defaults
mode tcp #mode { tcp|http|health },tcp 表示4层,http表示7层,health仅作为健康检查使用
retries 2 #尝试2次失败则从集群摘除
option redispatch #如果失效则强制转换其他服务器
option abortonclose #连接数过大自动关闭
maxconn 1024 #最大连接数
timeout connect 1d #连接超时时间,重要,hive查询数据能返回结果的保证
timeout client 1d #同上
timeout server 1d #同上
timeout check 2000 #健康检查时间
log 127.0.0.1 local0 err #[err warning info debug]
listen admin_stats #定义管理界面
bind 0.0.0.0:1090 #管理界面访问IP和端口
mode http #管理界面所使用的协议
maxconn 10 #最大连接数
stats refresh 30s #30秒自动刷新
stats uri / #访问url
stats realm Hive\ Haproxy #验证窗口提示
stats auth admin:123456 #401验证用户名密码
listen hive #hive后端定义
bind 0.0.0.0:10001 #ha作为proxy所绑定的IP和端口
mode tcp #以4层方式代理,重要
balance leastconn #调度算法'leastconn' 最少连接数分配,或者 'roundrobin',轮询分配
maxconn 1024 #最大连接数
server hive_1 192.168.1.1:10000 check inter 180000 rise 1 fall 2
server hive_2 192.168.1.2:10000 check inter 180000 rise 1 fall 2
#释义:server 主机代名(你自己能看懂就行),IP:端口每180000毫秒检查一次。也就是三分钟。
#hive每有10000端口的请求就会创建一个log,设置短了,/tmp下面会有无数个log文件,删不完。
如何启动
在HAProxy目录下执行如下命令:
haproxy -f conf.cfg
如何使用
在hive-server或者hive-server2中jdbc的连接信息修改url和port,如hive-server2:
jdbc:hive2://${haproxy.hostname}:${haproxy.hive.bind.port}/${hive.database}
上面haproxy.hostname为你安装haproxy的机器名;haproxy.hive.bind.port为conf.cfg中定义的监听hive的端口(上面中定义的为10001)
三、 Hbase
1. Hbase安装
解压# tar zxvf hbase-0.92.0.tar.gz
修改conf/hbase-env.sh
export JAVA_HOME=/usr/jdk 1.7 export HBASE_MANAGES_ZK=true #由 HBase 负责启动和关闭 ZooKeeper export HBASE_LOG_DIR=/data/logs/hbase export HBASE_CLASSPATH=/usr/hadoop/conf #HBase 类路径export HBASE_HEAPSIZE=4096 |
修改conf/hbase-site.xml
<property> <name>hbase.rootdirname> <value>hdfs://hadoop0:9000/hbasevalue> property> <property> <name>hbase.cluster.distributedname> <value>truevalue> property> <property> <name>hbase.mastername> <value>hdfs://hadoop0:60000value> property> <property> <name>hbase.zookeeper.quorumname> <value>hadoop0, hadoop1, hadoop2value> property> <property> <name>hbase.zookeeper.property.dataDirname> <value>/home/hadoop/zookeeper-3.4.5/datavalue> property> <property> <name>zookeeper.znode.parentname> <value> /hbasevalue> property> |
hbase.rootdir设置hbase在hdfs上的目录,主机名为hdfs的namenode节点所在的主机“fs.default.name”
hbase.cluster.distributed设置为true,表明是完全分布式的hbase集群
hbase.master设置hbase的master主机名和端口
hbase.zookeeper.quorum设置zookeeper的主机,建议使用单数
参数“hbase.master.maxclockskew”是用来防止“hbase 结点之间时间不一致造成regionserver启动失败”,默认的值为“30000”,现改为“180000”。
修改hadoop的目录下的conf/hdfs-site.xml
<property> <name>dfs.datanode.max.xcieversname> <value>4096value> property> |
修改conf/regionservers
将所有的datanode添加到这个文件,类似与hadoop中slaves文件拷贝hbase到所有的节点
在“/etc/profile”文件的尾部添加以下内容,并使其有效(source /etc/profile):
export HBASE_HOME=/usr/hadoop expor t PATH=$PATH :$ HBASE_HOME /bin |
启动hbase
$ ./bin/start-hbase.sh
hbase自带的web界面
http://hadoop0:60010/
测试:登录hbase客户端
1)./bin/hbase shell
2).新建数据表,并插入记录
hbase(main):003:0> create 'test', 'cf'
0 row(s) in 1.2200 seconds
hbase(main):003:0> list 'table'
test
1 row(s) in 0.0550 seconds
hbase(main):004:0> put 'test', 'row1','cf:a', 'value1'
0 row(s) in 0.0560 seconds
3).查看插入的数据
hbase(main):007:0> scan 'test'
ROW COLUMN+CELL
row1 column=cf:a, timestamp=1288380727188,value=value1
1row(s) in 0.0590 seconds
4).读取单条记录
hbase(main):008:0> get 'test', 'row1'
COLUMN CELL
cf:a timestamp=1288380727188, value=value1
1 row(s) in 0.0400 seconds
5).停用并删除数据表
hbase(main):012:0> disable 'test'
0 row(s) in 1.0930 seconds
hbase(main):013:0> drop 'test'
0 row(s) in 0.0770 seconds
6).退出
hbase(main):014:0> exit
2. 物理存储
HDFS 为HBase 提供了高可靠性的底层存储支持,Hadoop MapReduce 为HBase 提供了高性能的计算能力,Zookeeper 为HBase 提供了稳定服务和failover 机制。
此外,Pig 和Hive 还为HBase 提供了高层语言支持,使得在HBase上进行数据统计处理变的非常简单。Sqoop 则为HBase 提供了方便的RDBMS 数据导入功能,使得传统数据库数据向HBase 中迁移变的非常方便。
另外,HBase 存储的是松散型数据。具体来说,HBase 存储的数据介于映射(key/value )和关系型数据之间。进一步讲,HBase 存储的数据可以理解为一种key 和value 的映射关系,但又不是简简单单的映射关系。除此之外它还有许多其他的特性。HBase 存储的数据从逻辑上来看就像一张很大的表,并且它的数据列可以根据需要动态增加。除此之外,每个cell (由行和列所确定的位置)中的数据又可以具有多个版本(通过时间戳来区别)。
建立的hdfs之上,提供高可靠性、高性能、列存储、可伸缩、实时读写的数据库系统。它介于nosql和RDBMS之间,仅能通过主键(row key)和主键的range来检索数据,仅支持单行事务(可通过hive支持来实现多表join等复杂操作)。主要用来存储非结构化和半结构化的松散数据。
Hbase目标主要依靠横向扩展,来增加计算和存储能力。Table 在行的方向上分割为多个Hregion
region按大小分割的,每个表一开始只有一个region,随着数据不断插入表,region不断增大,当增大到一个阀值的时候,Hregion就会等分会两个新的Hregion。当table中的行不断增多,就会有越来越多的Hregion。
HRegion是Hbase中分布式存储和负载均衡的最小单元。最小单元就表示不同的Hregion可以分布在不同的HRegionserver上。但一个Hregion是不会拆分到多个server上的。
HRegion由一个或者多个Store组成,每个store保存一个columnsfamily。
每个Strore又由一个memStore和0至多个StoreFile组成。如图:
StoreFile以HFile格式保存在HDFS上。
HFile(StoreFile)分为六个部分:
Data Block 段–保存表中的数据,这部分可以被压缩
Meta Block 段 (可选的)–保存用户自定义的kv对,可以被压缩。
File Info 段–Hfile的元信息,不被压缩,用户也可以在这一部分添加自己的元信息。
Data Block Index 段–Data Block的索引。每条索引的key是被索引的block的第一条记录的key。
Meta Block Index段 (可选的)–Meta Block的索引。
Trailer–这一段是定长的。保存了每一段的偏移量,读取一个HFile时,会首先读取Trailer,Trailer保存了每个段的起始位置(段的Magic Number用来做安全check),然后,DataBlock Index会被读取到内存中,这样,当检索某个key时,不需要扫描整个HFile,而只需从内存中找到key所在的block,通过一次磁盘io将整个block读取到内存中,再找到需要的key。DataBlock Index采用LRU机制淘汰。
HFile的Data Block,Meta Block通常采用压缩方式存储,压缩之后可以大大减少网络IO和磁盘IO,随之而来的开销当然是需要花费cpu进行压缩和解压缩。
目标Hfile的压缩支持两种方式:Gzip,Lzo。
HLog(WAL log)
WAL 意为Write ahead log(http://en.wikipedia.org/wiki/Write-ahead_logging),类似mysql中的binlog,用来做灾难恢复只用,Hlog记录数据的所有变更,一旦数据修改,就可以从log中进行恢复。
每个Region Server维护一个Hlog,而不是每个Region一个。这样不同region(来自不同table)的日志会混在一起,这样做的目的是不断追加单个文件相对于同时写多个文件而言,可以减少磁盘寻址次数,因此可以提高对table的写性能。带来的麻烦是,如果一台region server下线,为了恢复其上的region,需要将region server上的log进行拆分,然后分发到其它region server上进行恢复。
HLog文件就是一个普通的Hadoop Sequence File,Sequence File 的Key是HLogKey对象,HLogKey中记录了写入数据的归属信息,除了table和region名字外,同时还包括 sequence number和timestamp,timestamp是”写入时间”,sequence number的起始值为0,或者是最近一次存入文件系统中sequence number。HLog Sequece File的Value是HBase的KeyValue对象,即对应HFile中的KeyValue,可参见上文描述
下图是HFile的存储格式:
首先HFile文件是不定长的,长度固定的只有其中的两块:Trailer和FileInfo。正如图中所示的,Trailer中有指针指向其他数据块的起始点。File Info中记录了文件的一些Meta信息,例如:AVG_KEY_LEN, AVG_VALUE_LEN, LAST_KEY, COMPARATOR, MAX_SEQ_ID_KEY等。Data Index和Meta Index块记录了每个Data块和Meta块的起始点。
Data Block是HBase I/O的基本单元,为了提高效率,HRegionServer中有基于LRU的Block Cache机制。每个Data块的大小可以在创建一个Table的时候通过参数指定,大号的Block有利于顺序Scan,小号Block利于随机查询。每个Data块除了开头的Magic以外就是一个个KeyValue对拼接而成, Magic内容就是一些随机数字,目的是防止数据损坏。后面会详细介绍每个KeyValue对的内部构造。
HFile里面的每个KeyValue对就是一个简单的byte数组。但是这个byte数组里面包含了很多项,并且有固定的结构。我们来看看里面的具体结构:
HLogFile
上图中示意了HLog文件的结构,其实HLog文件就是一个普通的Hadoop Sequence File,Sequence File 的Key是HLogKey对象,HLogKey中记录了写入数据的归属信息,除了table和region名字外,同时还包括 sequence number和timestamp,timestamp是“写入时间”,sequence number的起始值为0,或者是最近一次存入文件系统中sequence number。
HLog Sequece File的Value是HBase的KeyValue对象,即对应HFile中的KeyValue。
3. 系统架构
HBase的服务器体系结构遵循简单的主从服务器架构,它由HRegion服务器(HRegion Server)群和HBase Master服务器(HBase Master Server)构成。HBase Master服务器负责管理所有的HRegion服务器,而HBase中所有的服务器都是通过ZooKeeper来进行协调,并处理HBase服务器运行期间可能遇到的错误。HBase Master Server本身不存储HBase中的任何数据,HBase逻辑上的表可能会被划分为多个HRegion,然后存储到HRegion Server群中,HBase Master Server中存储的是从数据到HRegion Server中的映射。
Client
HBase Client使用HBase的RPC机制与HMaster和HRegionServer进行通信,对于管理类操作,Client与HMaster进行RPC;对于数据读写类操作,Client与HRegionServer进行RPC
1 包含访问hbase的接口,client维护着一些cache来加快对hbase的访问,比如regione的位置信息。
Zookeeper
Zookeeper Quorum 中除了存储了-ROOT-表的地址和HMaster 的地址,HRegionServer 也会把自己以Ephemeral方式注册到Zookeeper中,使得HMaster可以随时感知到各个 HRegionServer 的健康状态。此外,Zookeeper 也避免了HMaster 的单点问题。
1 保证任何时候,集群中只有一个master
2 存贮所有Region的寻址入口。
3 实时监控Region Server的状态,将Region server的上线和下线信息实时通知给Master
4 存储Hbase的schema,包括有哪些table,每个table有哪些column family
Master
每台 HRegion Server都会 HMaster通信,HMaster的主要任务就是要告诉每台 HRegion Server 它要维护那些 HRegion。当一台新的HRegion Server登录到 HMaster时,HMaster 会告诉它等待分配数据。而当一台 HRegion 死HMaster会把它负责的 HRegion 标记为未分配,然后再把它们分配到其他 HRegion Server中。 HMaster没有单点问题(SPFO),HBase 中可以启动多个 HMaster,通过 Zookeeper 的Master Election 机制保证总有一个Master运行,HMaster在功能上主要负责 Table和 Region的管理工作:
管理用户对 Table 的增、删、改、查操作;
管理 HRegionServer的负载均衡,调整 Region 分布;
在 RegionSplit 后,负责新 Region 的分配;
在 HRegionServer 停机后,负责失效 HRegion Server 上的 Regions迁移。
1 为Region server分配region
2 负责region server的负载均衡
3 发现失效的region server并重新分配其上的region
4 GFS上的垃圾文件回收
5 处理schema更新请求
RegionServer
所有的数据库数据一般是保存在 Hadoop HDFS分布式文件系统上面,用户通过一系列HRegion Server获取这些数据,一台机器上面一般只运行一个 HRegion Server,且每一个区段的 HRegion 也只会被一个 HRegion Server 维护。HRegion Server 主要负责响应用户 I/O 请求,向 HDFS文件系统中读写数据,是 HBase最核心的模块。 HRegion Server内部管理了一系列 HRegion 对象,每个 HRegion对应了 Table 中的一个ion, HRegion中由多个HStore组成。每个HStore对应了Table中的一个Column Family存储,可以看出每个 Column Family 其实就是一个集中的存储单元,因此最好将具备共O特性的 column 放在一个 Column Family中,这样最高效。
HStore 存储是 HBase 存储的核心了,其中由两部分组成,一部分是 MemStore,一部是StoreFiles。 MemStore是SortedMemory Buffer,用户写入的数据首先会放入MemStore,MemStore 满了以后会 Flush 成一个 StoreFile(底层实现是 HFile),当 StoreFile 文件数增长到一定阈值,会触发 Compact 合并操作,将多个 StoreFiles 合并成一个 StoreFile,并过程中会进行版本合并和数据删除,因此可以看出 HBase 其实只有增加数据,所有的新和删除操作都是在后续的 compact 过程中进行的,这使得用户的写操作只要进入内存就可以立即返回,保证了 HBase I/O的高性能。当 StoreFiles Compact后,会逐步形成越越大的 StoreFile,当单个 StoreFile大小超过一定阈值后,会触发 Split操作,同时把当前 ion Split成 2 个 Region,父 Region 会下线,新 Split 出的2 个孩子 Region 会被 HMaster配到相应的 HRegionServer 上,使得原先 1 个Region 的压力得以分流到 2 个Region 上。
图描述了 Compaction和 Split 的过程。
在理解了上述 HStore 的基本原理后,还必须了解一下 HLog的功能,因为上述的 HStore在系统正常工作的前提下是没有问题的,但是在分布式系统环境中,无法避免系统出错或者宕机,因此一旦 HRegion Server意外退出,MemStore 中的内存数据将会丢失,这就需要引入HLog了。每个HRegion Server中都有一个HLog对象, HLog是一个实现Write Ahead Log的类,在每次用户操作写入 MemStore 的同时,也会写一份数据到 HLog 文件中(HLog 文件格式见后续),HLog 文件定期会滚动出新的,并删除旧的文件(已持久化到 StoreFile 中的数据)。当HRegion Server意外终止后,HMaster 会通过 Zookeeper 感知到,HMaster 首先会处理遗留的 HLog文件,将其中不同Region的Log数据进行拆分,分别放到相应region的目录下,然后再将失效的 region 重新分配,领取到这些 region的 HRegion Server在 Load Region的过程中,会发现有历史HLog需要处理,因此会Replay HLog中的数据到MemStore
中,然后 flush 到 StoreFiles,完成数据恢复。
1 Region server维护Master分配给它的region,处理对这些region的IO请求
2 Region server负责切分在运行过程中变得过大的region
可以看到,client访问hbase上数据的过程并不需要master参与(寻址访问zookeeper和region server,数据读写访问regione server),master仅仅维护者table和region的元数据信息,负载很低。
HRegionServer主要负责响应用户I/O请求,向HDFS文件系统中读写数据,是HBase中最核心的模块。
所有的数据库数据一般是保存在Hadoop分布式文件系统上面的,用户通过一系列HRegion服务器来获取这些数据,一台机器上面一般只运行一个HRegion服务器,且每一个区段的HRegion也只会被一个HRegion服务器维护。
当用户需要更新数据的时候,他会被分配到对应的HRegion服务器上提交修改,这些修改显示被写到Hmemcache(内存中的缓存,保存最近更新的数据)缓存和服务器的Hlog(磁盘上面的记录文件,他记录着所有的更新操作)文件里面。在操作写入Hlog之后,commit()调用才会将其返回给客户端。
在读取数据的时候,HRegion服务器会先访问Hmemcache缓存,如果缓存里没有改数据,才会回到Hstores磁盘上面寻找,每一个列族都会有一个HStore集合,每一个HStore集合包含很多HstoreFile文件,如下图:
HStore存储是HBase存储的核心了,其中由两部分组成,一部分是MemStore,一部分是StoreFiles。MemStore是Sorted Memory Buffer,用户写入的数据首先会放入MemStore,当MemStore满了以后会Flush成一个StoreFile(底层实现是HFile),当StoreFile文件数量增长到一定阈值,会触发Compact合并操作,将多个StoreFiles合并成一个StoreFile,合并过程中会进行版本合并和数据删除,因此可以看出HBase其实只有增加数据,所有的更新和删除操作都是在后续的compact过程中进行的,这使得用户的写操作只要进入内存中就可以立即返回,保证了HBase I/O的高性能。当StoreFiles Compact后,会逐步形成越来越大的StoreFile,当单个StoreFile大小超过一定阈值后,会触发Split操作,同时把当前Region Split成2个Region,父Region会下线,新Split出的2个孩子Region会被HMaster分配到相应的HRegionServer上,使得原先1个Region的压力得以分流到2个Region上。
HRegion
当表的大小超过设置值的是偶,HBase会自动地将表划分为不同的区域,每个区域包含所有行的一个子集。对用户来说,每个表是一堆数据的集合,靠主键来区分。从物理上来说,一张表被拆分成了多块,每一块就是一个HRegion。我们用表名+开始/结束主键来区分每一个HRegion,一个HRegion会保存一个表里某段连续的数据,从开始主键到结束主键,一张完整的表是保存在多个HRegion上面的。
HBase存储格式
HBase中的所有数据文件都存储在HadoopHDFS文件系统上,主要包括上述提出的两种文件类型:
1). HFile, HBase中KeyValue数据的存储格式,HFile是Hadoop的二进制格式文件,实际上StoreFile就是对HFile做了轻量级包装,即StoreFile底层就是HFile
2). HLog File,HBase中WAL(Write AheadLog) 的存储格式,物理上是Hadoop的SequenceFile
4. 调优
Linux 系统最大可打开文件数一般默认的参数值是1024,如果你不进行修改并发量上来的时候会出现“Too Many Open Files”的错误,导致整个 HBase不可运行,你可以用 ulimit -n 命令进行修改,或者修改/etc/security/limits.conf 和/proc/sys/fs/file-max的参数
1、hbase.hregion.max.filesize应该设置多少合适。
默认是256,HStoreFile的最大值。如果任何一个ColumnFamily(或者说HStore)的HStoreFiles的大小超过这个值,那么,其所属的HRegion就会Split成两个。
众所周知hbase中数据一开始会写入memstore,当memstore满64MB以后,会flush到disk上而成为storefile。当storefile数量超过3时,会启动compaction过程将它们合并为一个storefile。这个过程中会删除一些timestamp过期的数据,比如update的数据。而当合并后的storefile大小大于hfile默认最大值时,会触发split动作,将它切分成两个region。
2、autoflush=false的影响
无论是官方还是很多blog都提倡为了提高hbase的写入速度而在应用代码中设置autoflush=false,然后lz认为在在线应用中应该谨慎进行该设置,原因如下:
2.1、autoflush=false的原理是当客户端提交delete或put请求时,将该请求在客户端缓存,直到数据超过2M(hbase.client.write.buffer决定)或用户执行了hbase.flushcommits()时才向regionserver提交请求。因此即使htable.put()执行返回成功,也并非说明请求真的成功了。假如还没有达到该缓存而client崩溃,该部分数据将由于未发送到regionserver而丢失。这对于零容忍的在线服务是不可接受的。
2.2、autoflush=true虽然会让写入速度下降2-3倍,但是对于很多在线应用来说这都是必须打开的,也正是hbase为什么让它默认值为true的原因。当该值为true时,每次请求都会发往regionserver,而regionserver接收到请求后第一件事就是写hlog,因此对io的要求是非常高的,为了提高hbase的写入速度,应该尽可能高地提高io吞吐量,比如增加磁盘、使用raid卡、减少replication因子数等。
从性能的角度谈table中family和qualifier的设置
3、对于传统关系型数据库中的一张table,在业务转换到hbase上建模时,从性能的角度应该如何设置family和qualifier呢?
最极端的,①每一列都设置成一个family,②一个表仅有一个family,所有列都是其中的一个qualifier,那么有什么区别呢?
从读的方面考虑:
family越多,那么获取每一个cell数据的优势越明显,因为io和网络都减少了。
如果只有一个family,那么每一次读都会读取当前rowkey的所有数据,网络和io上会有一些损失。
当然如果要获取的是固定的几列数据,那么把这几列写到一个family中比分别设置family要更好,因为只需一次请求就能拿回所有数据。
从写的角度考虑:
首先,内存方面来说,对于一个Region,会为每一个表的每一个Family分配一个Store,而每一个Store,都会分配一个MemStore,所以更多的family会消耗更多的内存。
其次,从flush和compaction方面说,目前版本的hbase,在flush和compaction都是以region为单位的,也就是说当一个family达到flush条件时,该region的所有family所属的memstore都会flush一次,即使memstore中只有很少的数据也会触发flush而生成小文件。这样就增加了compaction发生的机率,而compaction也是以region为单位的,这样就很容易发生compaction风暴从而降低系统的整体吞吐量。
第三,从split方面考虑,由于hfile是以family为单位的,因此对于多个family来说,数据被分散到了更多的hfile中,减小了split发生的机率。这是把双刃剑。更少的split会导致该region的体积比较大,由于balance是以region的数目而不是大小为单位来进行的,因此可能会导致balance失效。而从好的方面来说,更少的split会让系统提供更加稳定的在线服务。而坏处我们可以通过在请求的低谷时间进行人工的split和balance来避免掉。
因此对于写比较多的系统,如果是离线应该,我们尽量只用一个family好了,但如果是在线应用,那还是应该根据应用的情况合理地分配family
5. 读写过程
上文提到,hbase使用MemStore和StoreFile存储对表的更新。
数据在更新时首先写入Log(WAL log)和内存(MemStore)中,MemStore中的数据是排序的,当MemStore累计到一定阈值时,就会创建一个新的MemStore,并且将老的MemStore添加到flush队列,由单独的线程flush到磁盘上,成为一个StoreFile。于此同时,系统会在zookeeper中记录一个redo point,表示这个时刻之前的变更已经持久化了。(minor compact)
当系统出现意外时,可能导致内存(MemStore)中的数据丢失,此时使用Log(WAL log)来恢复checkpoint之后的数据。
前面提到过StoreFile是只读的,一旦创建后就不可以再修改。因此Hbase的更新其实是不断追加的操作。当一个Store中的StoreFile达到一定的阈值后,就会进行一次合并(major compact),将对同一个key的修改合并到一起,形成一个大的StoreFile,当StoreFile的大小达到一定阈值后,又会对StoreFile进行split,等分为两个StoreFile。
由于对表的更新是不断追加的,处理读请求时,需要访问Store中全部StoreFile和MemStore,将他们的按照row key进行合并,由于StoreFile和MemStore都是经过排序的,并且StoreFile带有内存中索引,合并的过程还是比较快。
客户端调优
AutoFlush:将 HTable 的setAutoFlush 设为 false,可以支持客户端批量更新。即当 Put 填满客户端
flush 缓存时,才发送到服务端。
默认是 true。
ScanCaching :scanner一次缓存多少数据来 scan(从服务端一次抓多少数据回来 scan)
默认值是 1,一次只取一条。
ScanAttribute Selection:scan时建议指定需要的Column Family,减少通信量,否则scan操作默认会返回整个row的所有数据(所有 Coulmn Family)。
OptimalLoading of Row Keys:当你 scan 一张表的时候,返回结果只需要 row key (不需要 CF, qualifier,values,timestaps)时,你可以在 scan 实例中添加一个filterList,并设置 MUST_PASS_ALL 操作,filterList 中add?FirstKeyOnlyFilter或KeyOnlyFilter。这样可以减少网络通信量。
Turnoff WAL on Puts :当 Put 某些非重要数据时,你可以设置 writeToWAL(false),来进一步提高写性能.writeToWAL(false)会在 Put 时放弃写 WAL log。风险是,当 RegionServer宕机时,可能你刚才 Put 的那些数据会丢失,且无法恢复。
启用 Bloom Filter :Bloom Filter通过空间换时间,提高读操作性能。
6. 参数配置
zookeeper.session.timeout
默认值:3分钟(180000ms)
说明:RegionServer与Zookeeper间的连接超时时间。当超时时间到后,ReigonServer会被Zookeeper从RS集群清单中移除,HMaster收到移除通知后,会对这台server负责的regions重新balance,让其他存活的RegionServer接管.
调优:这个timeout决定了RegionServer是否能够及时的failover。设置成1分钟或更低,可以减少因等待超时而被延长的failover时间。
不过需要注意的是,对于一些Online应用,RegionServer从宕机到恢复时间本身就很短的(网络闪断,crash等故障,运维可快速介入),如果调低timeout时间,反而会得不偿失。因为当ReigonServer被正式从RS集群中移除时,HMaster就开始做balance了(让其他RS根据故障机器记录的WAL日志进行恢复)。当故障的RS在人工介入恢复后,这个balance动作是毫无意义的,反而会使负载不均匀,给RS带来更多负担。特别是那些固定分配regions的场景。
hbase.regionserver.handler.count
默认值:10
说明:RegionServer的请求处理IO线程数。
调优:这个参数的调优与内存息息相关。
较少的IO线程,适用于处理单次请求内存消耗较高的Big PUT场景(大容量单次PUT或设置了较大cache的scan,均属于Big PUT)或ReigonServer的内存比较紧张的场景。
较多的IO线程,适用于单次请求内存消耗低,TPS要求非常高的场景。设置该值的时候,以监控内存为主要参考。
这里需要注意的是如果server的region数量很少,大量的请求都落在一个region上,因快速充满memstore触发flush导致的读写锁会影响全局TPS,不是IO线程数越高越好。
压测时,开启Enabling RPC-level logging,可以同时监控每次请求的内存消耗和GC的状况,最后通过多次压测结果来合理调节IO线程数。
这里是一个案例?Hadoop and HBase Optimization for Read Intensive SearchApplications,作者在SSD的机器上设置IO线程数为100,仅供参考。
hbase.hregion.max.filesize
默认值:256M
说明:在当前ReigonServer上单个Reigon的最大存储空间,单个Region超过该值时,这个Region会被自动split成更小的region。
调优:小region对split和compaction友好,因为拆分region或compact小region里的storefile速度很快,内存占用低。缺点是split和compaction会很频繁。
特别是数量较多的小region不停地split, compaction,会导致集群响应时间波动很大,region数量太多不仅给管理上带来麻烦,甚至会引发一些Hbase的bug。
一般512以下的都算小region。
大region,则不太适合经常split和compaction,因为做一次compact和split会产生较长时间的停顿,对应用的读写性能冲击非常大。此外,大region意味着较大的storefile,compaction时对内存也是一个挑战。
当然,大region也有其用武之地。如果你的应用场景中,某个时间点的访问量较低,那么在此时做compact和split,既能顺利完成split和compaction,又能保证绝大多数时间平稳的读写性能。
既然split和compaction如此影响性能,有没有办法去掉?
compaction是无法避免的,split倒是可以从自动调整为手动。
只要通过将这个参数值调大到某个很难达到的值,比如100G,就可以间接禁用自动split(RegionServer不会对未到达100G的region做split)。
再配合RegionSplitter这个工具,在需要split时,手动split。
手动split在灵活性和稳定性上比起自动split要高很多,相反,管理成本增加不多,比较推荐online实时系统使用。
内存方面,小region在设置memstore的大小值上比较灵活,大region则过大过小都不行,过大会导致flush时app的IO wait增高,过小则因store file过多影响读性能。
hbase.regionserver.global.memstore.upperLimit/lowerLimit
默认值:0.4/0.35
upperlimit说明:hbase.hregion.memstore.flush.size这个参数的作用是当单个Region内所有的memstore大小总和超过指定值时,flush该region的所有memstore。RegionServer的flush是通过将请求添加一个队列,模拟生产消费模式来异步处理的。那这里就有一个问题,当队列来不及消费,产生大量积压请求时,可能会导致内存陡增,最坏的情况是触发OOM。
这个参数的作用是防止内存占用过大,当ReigonServer内所有region的memstores所占用内存总和达到heap的40%时,HBase会强制block所有的更新并flush这些region以释放所有memstore占用的内存。
lowerLimit说明: 同upperLimit,只不过lowerLimit在所有region的memstores所占用内存达到Heap的35%时,不flush所有的memstore。它会找一个memstore内存占用最大的region,做个别flush,此时写更新还是会被block。lowerLimit算是一个在所有region强制flush导致性能降低前的补救措施。在日志中,表现为 “** Flushthread woke up with memory above low water.”
调优:这是一个Heap内存保护参数,默认值已经能适用大多数场景。
参数调整会影响读写,如果写的压力大导致经常超过这个阀值,则调小读缓存hfile.block.cache.size增大该阀值,或者Heap余量较多时,不修改读缓存大小。
如果在高压情况下,也没超过这个阀值,那么建议你适当调小这个阀值再做压测,确保触发次数不要太多,然后还有较多Heap余量的时候,调大hfile.block.cache.size提高读性能。
还有一种可能性是?hbase.hregion.memstore.flush.size保持不变,但RS维护了过多的region,要知道 region数量直接影响占用内存的大小。
hfile.block.cache.size
默认值:0.2
说明:storefile的读缓存占用Heap的大小百分比,0.2表示20%。该值直接影响数据读的性能。
调优:当然是越大越好,如果写比读少很多,开到0.4-0.5也没问题。如果读写较均衡,0.3左右。如果写比读多,果断默认吧。设置这个值的时候,你同时要参考?hbase.regionserver.global.memstore.upperLimit?,该值是memstore占heap的最大百分比,两个参数一个影响读,一个影响写。如果两值加起来超过80-90%,会有OOM的风险,谨慎设置。
hbase.hstore.blockingStoreFiles
默认值:7
说明:在flush时,当一个region中的Store(Coulmn Family)内有超过7个storefile时,则block所有的写请求进行compaction,以减少storefile数量。
调优:block写请求会严重影响当前regionServer的响应时间,但过多的storefile也会影响读性能。从实际应用来看,为了获取较平滑的响应时间,可将值设为无限大。如果能容忍响应时间出现较大的波峰波谷,那么默认或根据自身场景调整即可。
hbase.hregion.memstore.block.multiplier
默认值:2
说明:当一个region里的memstore占用内存大小超过hbase.hregion.memstore.flush.size两倍的大小时,block该region的所有请求,进行flush,释放内存。
虽然我们设置了region所占用的memstores总内存大小,比如64M,但想象一下,在最后63.9M的时候,我Put了一个200M的数据,此时memstore的大小会瞬间暴涨到超过预期的hbase.hregion.memstore.flush.size的几倍。这个参数的作用是当memstore的大小增至超过hbase.hregion.memstore.flush.size2倍时,block所有请求,遏制风险进一步扩大。
调优:这个参数的默认值还是比较靠谱的。如果你预估你的正常应用场景(不包括异常)不会出现突发写或写的量可控,那么保持默认值即可。如果正常情况下,你的写请求量就会经常暴长到正常的几倍,那么你应该调大这个倍数并调整其他参数值,比如hfile.block.cache.size和hbase.regionserver.global.memstore.upperLimit/lowerLimit,以预留更多内存,防止HBaseserver OOM。
hbase.hregion.memstore.mslab.enabled
默认值:true
说明:减少因内存碎片导致的Full GC,提高整体性能。
启用LZO压缩
LZO对比Hbase默认的GZip,前者性能较高,后者压缩比较高,具体参见?Using LZO Compression 。对于想提高HBase读写性能的开发者,采用LZO是比较好的选择。对于非常在乎存储空间的开发者,则建议保持默认。
不要在一张表里定义太多的Column Family
Hbase目前不能良好的处理超过包含2-3个CF的表。因为某个CF在flush发生时,它邻近的CF也会因关联效应被触发flush,最终导致系统产生更多IO。
批量导入
在批量导入数据到Hbase前,你可以通过预先创建regions,来平衡数据的负载。
避免CMS concurrent modefailure
HBase使用CMS GC。默认触发GC的时机是当年老代内存达到90%的时候,这个百分比由-XX:CMSInitiatingOccupancyFraction=N 这个参数来设置。concurrentmode failed发生在这样一个场景:
当年老代内存达到90%的时候,CMS开始进行并发垃圾收集,于此同时,新生代还在迅速不断地晋升对象到年老代。当年老代CMS还未完成并发标记时,年老代满了,悲剧就发生了。CMS因为没内存可用不得不暂停mark,并触发一次stop the world(挂起所有jvm线程),然后采用单线程拷贝方式清理所有垃圾对象。这个过程会非常漫长。为了避免出现concurrent mode failed,建议让GC在未到90%时,就触发。
通过设置?-XX:CMSInitiatingOccupancyFraction=N
这个百分比, 可以简单的这么计算。如果你的?hfile.block.cache.size和?hbase.regionserver.global.memstore.upperLimit 加起来有60%(默认),那么你可以设置70-80,一般高10%左右差不多。
Hbase客户端优化
AutoFlush
将HTable的setAutoFlush设为false,可以支持客户端批量更新。即当Put填满客户端flush缓存时才发送到服务端。
默认是true。
Scan Caching
scanner一次缓存多少数据来scan(从服务端一次抓多少数据回来scan)。
默认值是 1,一次只取一条。
Scan Attribute Selection
scan时建议指定需要的Column Family,减少通信量,否则scan操作默认会返回整个row的所有数据(所有CoulmnFamily)。
Close ResultScanners
通过scan取完数据后,记得要关闭ResultScanner,否则RegionServer可能会出现问题(对应的Server资源无法释放)。
Optimal Loading of Row Keys
当你scan一张表的时候,返回结果只需要row key(不需要CF, qualifier,values,timestaps)时,你可以在scan实例中添加一个filterList,并设置MUST_PASS_ALL操作,filterList中add?FirstKeyOnlyFilter或KeyOnlyFilter。这样可以减少网络通信量。
Turn off WAL on Puts
当Put某些非重要数据时,你可以设置writeToWAL(false),来进一步提高写性能。writeToWAL(false)会在Put时放弃写WAL log。风险是,当RegionServer宕机时,可能你刚才Put的那些数据会丢失,且无法恢复。
启用Bloom Filter
Bloom Filter通过空间换时间,提高读操作性能。
=================================================================
该文档是用hbase默认配置文件生成的,文件源是hbase-default.xml
hbase.rootdir
这 个目录是region server的共享目录,用来持久化Hbase。URL需要是'完全正确'的,还要包含文件系统的scheme。例如,要表示hdfs中的 '/hbase'目录,namenode 运行在namenode.example.org的9090端口。则需要设置为hdfs://namenode.example.org:9000/hbase。默认情况下Hbase是写到/tmp的。不改这个配置,数据会在重启的时候丢失。
默认: file:///tmp/hbase-${user.name}/hbase
hbase.master.port
Hbase的Master的端口.
默认: 60000
hbase.cluster.distribute
Hbase的运行模式。false是单机模式,true是分布式模式。若为false,Hbase和Zookeeper会运行在同一个JVM里面。
默认: false
hbase.tmp.dir
本地文件系统的临时文件夹。可以修改到一个更为持久的目录上。(/tmp会在重启时清楚)
默认:/tmp/hbase-${user.name}
hbase.master.info.port
HBase Master web 界面端口. 设置为-1 意味着你不想让他运行。
默认: 60010
hbase.master.info.bindAddress
HBase Master web 界面绑定的端口
默认: 0.0.0.0
hbase.client.write.buffer
HTable 客户端的写缓冲的默认大小。这个值越大,需要消耗的内存越大。因为缓冲在客户端和服务端都有实例,所以需要消耗客户端和服务端两个地方的内存。得到的好处是,可以减少RPC的次数。可以这样估算服务器端被占用的内存: hbase.client.write.buffer * hbase.regionserver.handler.count
默认: 2097152
hbase.regionserver.port
HBase RegionServer绑定的端口
默认: 60020
hbase.regionserver.info.port
HBase RegionServer web 界面绑定的端口 设置为 -1 意味这你不想与运行RegionServer 界面.
默认: 60030
hbase.regionserver.info.port.auto
Master或RegionServer是否要动态搜一个可以用的端口来绑定界面。当hbase.regionserver.info.port已经被占用的时候,可以搜一个空闲的端口绑定。这个功能在测试的时候很有用。默认关闭。
默认: false
hbase.regionserver.info.bindAddress
HBase RegionServer web 界面的IP地址
默认: 0.0.0.0
hbase.regionserver.class
RegionServer 使用的接口。客户端打开代理来连接region server的时候会使用到。
默认:org.apache.hadoop.hbase.ipc.HRegionInterface
hbase.client.pause
通常的客户端暂停时间。最多的用法是客户端在重试前的等待时间。比如失败的get操作和region查询操作等都很可能用到。
默认: 1000
hbase.client.retries.number
最大重试次数。例如region查询,Get操作,Update等都可能发生错误,需要重试。这是最大重试错误的值。
默认: 10
hbase.client.scanner.caching
当 调用Scanner的next方法,而值又不在缓存里的时候,从服务端一次获取的行数。越大的值意味着Scanner会快一些,但是会占用更多的内存。当缓冲被占满的时候,next方法调用会越来越慢。慢到一定程度,可能会导致超时。例如超过了 hbase.regionserver.lease.period。
默认: 1
hbase.client.keyvalue.maxsize
一 个KeyValue实例的最大size.这个是用来设置存储文件中的单个entry的大小上界。因为一个KeyValue是不能分割的,所以可以避免因为数据过大导致region不可分割。明智的做法是把它设为可以被最大region size整除的数。如果设置为0或者更小,就会禁用这个检查。默认10MB。
默认: 10485760
hbase.regionserver.lease.perio
客户端租用HRegionserver 期限,即超时阀值。单位是毫秒。默认情况下,客户端必须在这个时间内发一条信息,否则视为死掉。
默认: 60000
hbase.regionserver.handler.count
RegionServers受理的RPC Server实例数量。对于Master来说,这个属性是Master受理的handler数量
默认: 10
hbase.regionserver.msginterval
RegionServer 发消息给 Master 时间间隔,单位是毫秒
默认: 3000
hbase.regionserver.optionallogflushinterval
将Hlog同步到HDFS的间隔。如果Hlog没有积累到数量,到了时间也会触发同步。默认1秒,单位毫秒。
默认: 1000
hbase.regionserver.regionSplitLimit
region的数量到了这个值后就不会在分裂了。这不是一个region数量的硬性限制。但是起到了一定指导性的作用,到了这个值就该停止分裂了。默认是MAX_INT.就是说不阻止分裂。
默认:2147483647
hbase.regionserver.logroll.period
提交commit log的间隔,不管有没有写足够的值。
默认: 3600000
hbase.regionserver.hlog.reader.impl
HLog file reader 的实现.
默认:org.apache.hadoop.hbase.regionserver.wal.SequenceFileLogReader
hbase.regionserver.hlog.writer.impl
HLog file writer 的实现.
默认:org.apache.hadoop.hbase.regionserver.wal.SequenceFileLogWriter
hbase.regionserver.thread.splitcompactcheckfrequency
region server 多久执行一次split/compaction 检查.
默认: 20000
hbase.regionserver.nbreservationblocks
储备的内存block的数量(译者注:就像石油储备一样)。当发生out ofmemory 异常的时候,我们可以用这些内存在RegionServer停止之前做清理操作。
默认: 4
hbase.zookeeper.dns.interface
当使用DNS的时候,Zookeeper用来上报的IP地址的网络接口名字。
默认: default
hbase.zookeeper.dns.nameserver
当使用DNS,Zookeepr使用的DNS的域名或者IP 地址,Zookeeper用它来确定和master用来进行通讯的域名.
默认: default
hbase.regionserver.dns.interface
当使用DNS的时候,RegionServer用来上报的IP地址的网络接口名字。
默认: default
hbase.regionserver.dns.nameserver
当使用DNS的时候,RegionServer使用的DNS的域名或者IP 地址,RegionServer用它来确定和master用来进行通讯的域名.
默认: default
hbase.master.dns.interface
当使用DNS的时候,Master用来上报的IP地址的网络接口名字。
默认: default
hbase.master.dns.nameserver
当使用DNS的时候,RegionServer使用的DNS的域名或者IP 地址,Master用它来确定用来进行通讯的域名.
默认: default
hbase.balancer.period
Master执行region balancer的间隔。
默认: 300000
hbase.regions.slop
当任一regionserver有average +(average * slop)个region是会执行Rebalance
默认: 0
hbase.master.logcleaner.ttl
Hlog存在于.oldlogdir 文件夹的最长时间, 超过了就会被 Master 的线程清理掉.
默认: 600000
hbase.master.logcleaner.plugins
LogsCleaner 服务会执行的一组LogCleanerDelegat。值用逗号间隔的文本表示。这些WAL/HLog cleaners会按顺序调用。可以把先调用的放在前面。你可以实现自己的LogCleanerDelegat,加到Classpath下,然后在这里写下类的全称。一般都是加在默认值的前面。
默认:org.apache.hadoop.hbase.master.TimeToLiveLogCleaner
hbase.regionserver.global.memstore.upperLimit
单个regionserver的全部memtores的最大值。超过这个值,一个新update操作会被挂起,强制执行flush操作。
默认: 0.4
hbase.regionserver.global.memstore.lowerLimit
当强制执行flush操作的时候,当低于这个值的时候,flush会停止。默认是堆大小的 35% . 如果这个值和 hbase.regionserver.global.memstore.upperLimit 相同就意味着当update操作因为内存限制被挂起时,会尽量少的执行flush(译者注:一旦执行flush,值就会比下限要低,不再执行)
默认: 0.35
hbase.server.thread.wakefrequency
service工作的sleep间隔,单位毫秒。可以作为service线程的sleep间隔,比如log roller.
默认: 10000
hbase.hregion.memstore.flush.size
当memstore的大小超过这个值的时候,会flush到磁盘。这个值被一个线程每隔hbase.server.thread.wakefrequency检查一下。
默认: 67108864
hbase.hregion.preclose.flush.size
当一个region中的memstore的大小大于这个值的时候,我们又触发了close.会先运行“pre-flush”操作,清理这个需要关闭的 memstore,然后将这个region下线。当一个region下线了,我们无法再进行任何写操作。如果一个memstore很大的时候,flush 操作会消耗很多时间。"pre-flush"操作意味着在region下线之前,会先把memstore清空。这样在最终执行close操作的时候,flush操作会很快。
默认: 5242880
hbase.hregion.memstore.block.multiplier
如果memstore有hbase.hregion.memstore.block.multiplier倍数的hbase.hregion.flush.size的大小,就会阻塞update操作。这是为了预防在update高峰期会导致的失控。如果不设上界,flush的时候会花很长的时间来合并或者分割,最坏的情况就是引发out of memory异常。(译者注:内存操作的速度和磁盘不匹配,需要等一等。原文似乎有误)
默认: 2
hbase.hregion.memstore.mslab.enabled
体验特性:启用memStore分配本地缓冲区。这个特性是为了防止在大量写负载的时候堆的碎片过多。这可以减少GC操作的频率。(GC有可能会Stop the world)(译者注:实现的原理相当于预分配内存,而不是每一个值都要从堆里分配)
默认: false
hbase.hregion.max.filesize
最大HStoreFile大小。若某个Columnfamilies的HStoreFile增长达到这个值,这个Hegion会被切割成两个。 Default: 256M.
默认: 268435456
hbase.hstore.compactionThreshold
当一个HStore含有多于这个值的HStoreFiles(每一个memstoreflush产生一个HStoreFile)的时候,会执行一个合并操作,把这HStoreFiles写成一个。这个值越大,需要合并的时间就越长。
默认: 3
hbase.hstore.blockingStoreFiles
当一个HStore含有多于这个值的HStoreFiles(每一个memstoreflush产生一个HStoreFile)的时候,会执行一个合并操作,update会阻塞直到合并完成,直到超过了hbase.hstore.blockingWaitTime的值
默认: 7
hbase.hstore.blockingWaitTime
hbase.hstore.blockingStoreFiles所限制的StoreFile数量会导致update阻塞,这个时间是来限制阻塞时间的。当超过了这个时间,HRegion会停止阻塞update操作,不过合并还有没有完成。默认为90s.
默认: 90000
hbase.hstore.compaction.max
每个“小”合并的HStoreFiles最大数量。
默认: 10
hbase.hregion.majorcompaction
一个Region中的所有HStoreFile的majorcompactions的时间间隔。默认是1天。设置为0就是禁用这个功能。
默认: 86400000
hbase.mapreduce.hfileoutputformat.blocksize
MapReduce 中HFileOutputFormat可以写 storefiles/hfiles. 这个值是hfile的blocksize的最小值。通常在Hbase写Hfile的时候,bloocksize是由table schema(HColumnDescriptor)决定的,但是在mapreduce写的时候,我们无法获取schema中blocksize。这个值 越小,你的索引就越大,你随机访问需要获取的数据就越小。如果你的cell都很小,而且你需要更快的随机访问,可以把这个值调低。
默认: 65536
hfile.block.cache.size
分配给HFile/StoreFile的block cache占最大堆(-Xmxsetting)的比例。默认是20%,设置为0就是不分配。
默认: 0.2
hbase.hash.type
哈希函数使用的哈希算法。可以选择两个值:: murmur (MurmurHash) 和 jenkins(JenkinsHash). 这个哈希是给 bloom filters用的.
默认: murmur
hbase.master.keytab.file
HMaster server验证登录使用的kerberos keytab 文件路径。(译者注:Hbase使用Kerberos实现安全)
默认:
hbase.master.kerberos.principal
例"hbase/[email protected]". HMaster运行需要使用 kerberosprincipal name. principal name 可以在: user/hostname@DOMAIN 中获取. 如果"_HOST" 被用做hostname portion,需要实际运行的hostname来替代它。
默认:
hbase.regionserver.keytab.file
HRegionServer验证登录使用的kerberos keytab 文件路径。
默认:
hbase.regionserver.kerberos.principal
例如."hbase/[email protected]". HRegionServer运行需要使用kerberos principal name. principal name 可以在:user/hostname@DOMAIN 中获取. 如果 "_HOST" 被用做hostname portion,需要使用实际运行的hostname来替代它。在这个文件中必须要有一个entry来描述 hbase.regionserver.keytab.file
默认:
zookeeper.session.timeout
ZooKeeper 会话超时.Hbase把这个值传递改zk集群,向他推荐一个会话的最大超时时间。详见http://hadoop.apache.org /zookeeper/docs/current/zookeeperProgrammers.html#ch_zkSessions"The client sends a requested timeout, the server responds with thetimeout that it can give the client. "。 单位是毫秒
默认: 180000
zookeeper.znode.parent
ZooKeeper中的Hbase的根ZNode。所有的Hbase的ZooKeeper会用这个目录配置相对路径。默认情况下,所有的Hbase的ZooKeeper文件路径是用相对路径,所以他们会都去这个目录下面。
默认: /hbase
zookeeper.znode.rootserver
ZNode 保存的根region的路径. 这个值是由Master来写,client和regionserver 来读的。如果设为一个相对地址,父目录就是 ${zookeeper.znode.parent}.默认情形下,意味着根region的路径存储在/hbase/root-region-server.
默认:root-region-server
hbase.zookeeper.quorum
Zookeeper 集群的地址列表,用逗号分割。例 如:"host1.mydomain.com,host2.mydomain.com,host3.mydomain.com".默认是localhost,是给伪分布式用的。要修改才能在完全分布式的情况下使用。如果在hbase-env.sh设置了HBASE_MANAGES_ZK,这些ZooKeeper节点就会和Hbase一起启动。
默认: localhost
hbase.zookeeper.peerport
ZooKeeper节点使用的端口。详细参见:http://hadoop.apache.org/zookeeper/docs/r3.1.1/zookeeperStarted.html#sc_RunningReplicatedZooKeeper
默认: 2888
hbase.zookeeper.leaderport
ZooKeeper用来选择Leader的端口,详细参见:http://hadoop.apache.org/zookeeper/docs/r3.1.1/zookeeperStarted.html#sc_RunningReplicatedZooKeeper
默认: 3888
hbase.zookeeper.property.initLimit
ZooKeeper的zoo.conf中的配置。 初始化synchronization阶段的ticks数量限制
默认: 10
hbase.zookeeper.property.syncLimit
ZooKeeper的zoo.conf中的配置。发送一个请求到获得承认之间的ticks的数量限制
默认: 5
hbase.zookeeper.property.dataDir
ZooKeeper的zoo.conf中的配置。快照的存储位置
默认:${hbase.tmp.dir}/zookeeper
hbase.zookeeper.property.clientPort
ZooKeeper的zoo.conf中的配置。客户端连接的端口
默认: 2181
hbase.zookeeper.property.maxClientCnxns
ZooKeeper的zoo.conf中的配置。ZooKeeper集群中的单个节点接受的单个Client(以IP区分)的请求的并发数。这个值可以调高一点,防止在单机和伪分布式模式中出问题。
默认: 2000
hbase.rest.por
HBase REST server的端口
默认: 8080
hbase.rest.readonly
定义REST server的运行模式。可以设置成如下的值: false: 所有的HTTP请求都是被允许的 - GET/PUT/POST/DELETE. true:只有GET请求是被允许
默认: false
7. 命令
命令名称 |
描述 |
create |
创建表,格式为:create ‘表名称’,‘列名称1’,‘列名称N’ |
drop |
删除表,格式为:drop ‘表名称’ |
enable |
使表可用,格式为:enable ‘表名称’ |
disable |
使表不可用,格式为:disable ‘表名称’ |
alter |
修改字段,格式为:alter ‘表名称’,name=>‘字段名’,method=>‘操作类型’ |
put |
添加记录:格式为:put ‘表名称’,‘行名称’,‘列名称’,‘值’ |
scan |
扫描全表:格式为:scan ‘表名称’,[‘列名称’] |
get |
查看记录,格式为:get ‘表名称’,‘行名称’ |
count |
用于统计记录数,格式为:count ‘表名称’ |
delete |
用于删除记录,格式为:delete ‘表名’,‘行名称’,‘列名称’ |
8. hbase HA
1)、 hbase-env.sh 文件修改点:
# The java implementation to use. Java 1.6 required.
export JAVA_HOME=/usr/java/latest
# Extra Java CLASSPATH elements. Optional.
exportHBASE_CLASSPATH=/home/hadoop/hadoop/etc/hadoop
# The maximum amount of heap to use, in MB.Default is 1000.
export HBASE_HEAPSIZE=1000
export HBASE_OPTS="-XX:+UseConcMarkSweepGC"
export HBASE_LOG_DIR=${HBASE_HOME}/logs
export HBASE_MANAGES_ZK=false
2)、 hbase-site.xml文件配置:
3)、regionservers文件配置:(其实这个文件的内容比较简单,主要是几个DataNode的主机名称)
hadoop1
hadoop2
hadoop3
hadoop4
4)、这步主要做个说明:
1.以上配置在所有主机的Hbase中都是一样的。
2.Hbase安装在所有节点,包括NameNode,因为NameNode我主要是让其来管理HMaster的。
3.因为我的安装是基于Hadoop的HA,我有两个NameNode,准备让这两个NameNode都运行HMaster。
4.我有六台服务分别为hadoopm,hadoopm1这两个是NameNode,上面regionservers里配置的4个主机名是我的DataNode。
5.现在开始启动:在hadoopm上hbase目录的bin下执行:./start-hbase.sh
在hadoopm1上hbase目录的bin下执行:./hbase-daemon.shstart master
不出意外的情况下,这样就Ok了,可以试着打开浏览器看看Hbase了,然后在hadoopm上hbase目录的bin下执行:./hbase-daemon.sh stop master看看
hadoopm1是否会自动接口HMaster。提示:建议看日志,日志里说的非常清楚!我就不演示了,这是此片日志的第二次写了,现在是用txt在本地写的,写完复制
9. 权限(需补充)
四、 Zookeeper
1. 设计目标
1.1 简单
ZooKeeper让分布式进程可通过共享的、与标准文件系统类似的分层名字空间相互协调。名字空间由数据寄存器(在ZooKeeper世界中称作znode)构成,这与文件和目录类似。与用于存储设备的典型文件系统不同的是,ZooKeeper在内存中保存数据,这让其可以达到高吞吐量和低延迟。ZooKeeper的实现很重视高性能、高可用性,以及严格的顺序访问。高性能意味着可将ZooKeeper用于大的分布式系统。可靠性使之可避免单点失败。严格的顺序访问使得客户端可以实现复杂的同步原语。
1.2 自我复制
与它所协调的进程一样,ZooKeeper本身也会试图在一组主机间进行复制,这就是集群。
组成ZooKeeper服务的各个服务器必须相互知道对方。它们在内存中维护状态和事务日志,还在永久存储中维护快照。只要大部分服务器可用,ZooKeeper服务就是可用的。
客户端连接到单个ZooKeeper服务器。客户端维持一个TCP连接,通过这个连接发送请求、接收响应、获取观察事件,以及发送心跳。如果到服务器的TCP连接断开,客户端会连接到另一个服务器。
1.3 顺序访问
ZooKeeper为每次更新设置一个反映所有ZooKeeper事务顺序的序号。并发操作可使用序号来实现更高层抽象,如同步原语。
1.4 高速
在读操作为主的负载下特别快。ZooKeeper应用运行在成千上万台机器中,在读操作比写操作频繁,二者比例约为10:1的情况下,性能最好。
2.工作原理
Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。Zab协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。
为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了zxid。实现中zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于那个leader的统治时期。低32位用于递增计数。
每个Server在工作过程中有三种状态:
• LOOKING:当前Server不知道leader是谁,正在搜寻
• LEADING:当前Server即为选举出来的leader
• FOLLOWING:leader已经选举出来,当前Server与之同步
2.1 选主流程
当leader崩溃或者leader失去大多数的follower,这时候zk进入恢复模式,恢复模式需要重新选举出一个新的leader,让所有的Server都恢复到一个正确的状态。Zk的选举算法有两种:一种是基于basic paxos实现的,另外一种是基于fast paxos算法实现的。系统默认的选举算法为fast paxos。先介绍basic paxos流程:
1 .选举线程由当前Server发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的Server;
2 .选举线程首先向所有Server发起一次询问(包括自己);
3 .选举线程收到回复后,验证是否是自己发起的询问(验证zxid是否一致),然后获取对方的id(myid),并存储到当前询问对象列表中,最后获取对方提议的leader相关信息(id,zxid),并将这些信息存储到当次选举的投票记录表中;
4. 收到所有Server回复以后,就计算出zxid最大的那个Server,并将这个Server相关信息设置成下一次要投票的Server;
5. 线程将当前zxid最大的Server设置为当前Server要推荐的Leader,如果此时获胜的Server获得n/2 + 1的Server票数, 设置当前推荐的leader为获胜的Server,将根据获胜的Server相关信息设置自己的状态,否则,继续这个过程,直到leader被选举出来。
通过流程分析我们可以得出:要使Leader获得多数Server的支持,则Server总数必须是奇数2n+1,且存活的Server的数目不得少于n+1.
每个Server启动后都会重复以上流程。在恢复模式下,如果是刚从崩溃状态恢复的或者刚启动的server还会从磁盘快照中恢复数据和会话信息,zk会记录事务日志并定期进行快照,方便在恢复时进行状态恢复。选主的具体流程图如下所示:
fast paxos流程是在选举过程中,某Server首先向所有Server提议自己要成为leader,当其它Server收到提议以后,解决epoch和zxid的冲突,并接受对方的提议,然后向对方发送接受提议完成的消息,重复这个流程,最后一定能选举出Leader。其流程图如下所示:
2.2 同步流程
选完leader以后,zk就进入状态同步过程。
1. leader等待server连接;
2 .Follower连接leader,将最大的zxid发送给leader;
3 .Leader根据follower的zxid确定同步点;
4 .完成同步后通知follower已经成为uptodate状态;
5 .Follower收到uptodate消息后,又可以重新接受client的请求进行服务了。
流程图如下所示:
2.3 工作流程
2.3.1 Leader工作流程
Leader主要有三个功能:
1 .恢复数据;
2 .维持与Learner的心跳,接收Learner请求并判断Learner的请求消息类型;
3 .Learner的消息类型主要有PING消息、REQUEST消息、ACK消息、REVALIDATE消息,根据不同的消息类型,进行不同的处理。
PING消息是指Learner的心跳信息;REQUEST消息是Follower发送的提议信息,包括写请求及同步请求;ACK消息是Follower的对提议的回复,超过半数的Follower通过,则commit该提议;REVALIDATE消息是用来延长SESSION有效时间。
Leader的工作流程简图如下所示,在实际实现中,流程要比下图复杂得多,启动了三个线程来实现功能。
2.3.2 Follower工作流程
Follower主要有四个功能:
1. 向Leader发送请求(PING消息、REQUEST消息、ACK消息、REVALIDATE消息);
2 .接收Leader消息并进行处理;
3 .接收Client的请求,如果为写请求,发送给Leader进行投票;
4 .返回Client结果。
Follower的消息循环处理如下几种来自Leader的消息:
1 .PING消息: 心跳消息;
2 .PROPOSAL消息:Leader发起的提案,要求Follower投票;
3 .COMMIT消息:服务器端最新一次提案的信息;
4 .UPTODATE消息:表明同步完成;
5 .REVALIDATE消息:根据Leader的REVALIDATE结果,关闭待revalidate的session还是允许其接受消息;
6 .SYNC消息:返回SYNC结果到客户端,这个消息最初由客户端发起,用来强制得到最新的更新。
Follower的工作流程简图如下所示,在实际实现中,Follower是通过5个线程来实现功能的。
对于observer的流程不再叙述,observer流程和Follower的唯一不同的地方就是observer不会参加leader发起投票。
3.安装
3.1单机
Step1:配置JAVA环境。检验方法:执行java-version和javac -version命令。
Step2:下载并解压zookeeper。
最终生成目录类似结构:/home/hadoop/zookeeper-3.4.5/bin
Step3:重命名 zoo_sample.cfg文件
mv/home/hadoop/zookeeper-3.4.5/conf/zoo_sample.cfg zoo.cfg
Step4:vi zoo.cfg,修改
dataDir=/home/hadoop/zookeeper-3.4.5/data
Step5:创建数据目录:mkdir /home/hadoop/zookeeper-3.4.5/data
mkdir /home/hadoop/zookeeper-3.4.5/data
Step6:添加环境变量
export ZK_HOME=/home/hadoop/zookeeper-3.4.5
Step7:启动zookeeper:执行
/home/hadoop/zookeeper-3.4.5/bin/zkServer.shstart
Step8:检测是否成功启动:执行
/home/hadoop/zookeeper-3.4.5/bin/zkCli.sh 或 echostat|nc localhost 2181
3.2集群
Step1:配置JAVA环境。检验方法:执行java-version和javac -version命令。
Step2:下载并解压zookeeper。
最终生成目录类似结构:/home/ hadoop/zookeeper-3.4.5/bin
Step3:重命名 zoo_sample.cfg文件
mv/home/hadoop/zookeeper-3.4.5/conf/zoo_sample.cfg zoo.cfg
Step4:vi zoo.cfg,修改
tickTime=2000 |
这里要注意下server.1这个后缀,表示的是1.2.3.4这个机器,在机器中的server id是1
Step5:创建数据目录:mkdir /home/hadoop/zookeeper-3.4.5/data
mkdir /home/ hadoop/zookeeper-3.4.5/data
Step6:在标识Server ID.
在/home/hadoop /zookeeper-3.4.5/data目录中创建文件 myid 文件,每个文件中分别写入当前机器的server id,例如1.2.3.4这个机器,在/home/hadoop/zookeeper-3.4.5/data目录的myid文件中写入数字1.
Step7:
配置自动故障转移要求添加两个新的配置到hdfs-site.xml,如下:
这个配置需要添加到core-site.xml文件中,如下:
Step8:启动zookeeper:执行
/home/hadoop/zookeeper-3.4.5/bin/zkServer.shstart
Step9:检测是否成功启动:执行
/home/hadoop/zookeeper-3.4.5/bin/zkCli.sh 或 echostat|nc localhost 2181
1. 启动ZK服务: sh bin/zkServer.sh start 2. 查看ZK服务状态: sh bin/zkServer.sh status 3. 停止ZK服务: sh bin/zkServer.sh stop 4. 重启ZK服务: sh bin/zkServer.sh restart |
1. 可以通过命令:echo stat|nc 127.0.0.1 2181 来查看哪个节点被选择作为follower或者leader 2. 使用echo ruok|nc 127.0.0.1 2181 测试是否启动了该Server,若回复imok表示已经启动。 3. echo dump| nc 127.0.0.1 2181 ,列出未经处理的会话和临时节点。 4. echo kill | nc 127.0.0.1 2181 ,关掉server 5. echo conf | nc 127.0.0.1 2181 ,输出相关服务配置的详细信息。 6. echo cons | nc 127.0.0.1 2181 ,列出所有连接到服务器的客户端的完全的连接 / 会话的详细信息。 7. echo envi |nc 127.0.0.1 2181 ,输出关于服务环境的详细信息(区别于 conf 命令)。 8. echo reqs | nc 127.0.0.1 2181 ,列出未经处理的请求。 9. echo wchs | nc 127.0.0.1 2181 ,列出服务器 watch 的详细信息。 10. echo wchc | nc 127.0.0.1 2181 ,通过 session 列出服务器 watch 的详细信息,它的输出是一个与 watch 相关的会话的列表。 11. echo wchp | nc 127.0.0.1 2181 ,通过路径列出服务器 watch 的详细信息。它输出一个与 session 相关的路径。 |
4.应用场景
数据发布与订阅(配置中心)
发布与订阅模型,即所谓的配置中心,顾名思义就是发布者将数据发布到ZK节点上,供订阅者动态获取数据,实现配置信息的集中式管理和动态更新。例如全局的配置信息,服务式服务框架的服务地址列表等就非常适合使用。
1应用中用到的一些配置信息放到ZK上进行集中管理。这类场景通常是这样:应用在启动的时候会主动来获取一次配置,同时,在节点上注册一个Watcher,这样一来,以后每次配置有更新的时候,都会实时通知到订阅的客户端,从来达到获取最新配置信息的目的。
2分布式搜索服务中,索引的元信息和服务器集群机器的节点状态存放在ZK的一些指定节点,供各个客户端订阅使用。
3分布式日志收集系统。这个系统的核心工作是收集分布在不同机器的日志。收集器通常是按照应用来分配收集任务单元,因此需要在ZK上创建一个以应用名作为path的节点P,并将这个应用的所有机器ip,以子节点的形式注册到节点P上,这样一来就能够实现机器变动的时候,能够实时通知到收集器调整任务分配。
4系统中有些信息需要动态获取,并且还会存在人工手动去修改这个信息的发问。通常是暴露出接口,例如JMX接口,来获取一些运行时的信息。引入ZK之后,就不用自己实现一套方案了,只要将这些信息存放到指定的ZK节点上即可。
注意:在上面提到的应用场景中,有个默认前提是:数据量很小,但是数据更新可能会比较快的场景。
负载均衡
这里说的负载均衡是指软负载均衡。在分布式环境中,为了保证高可用性,通常同一个应用或同一个服务的提供方都会部署多份,达到对等服务。而消费者就须要在这些对等的服务器中选择一个来执行相关的业务逻辑,其中比较典型的是消息中间件中的生产者,消费者负载均衡。
命名服务(NamingService)
命名服务也是分布式系统中比较常见的一类场景。在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。被命名的实体通常可以是集群中的机器,提供的服务地址,远程对象等等——这些我们都可以统称他们为名字(Name)。其中较为常见的就是一些分布式服务框架中的服务地址列表。通过调用ZK提供的创建节点的API,能够很容易创建一个全局唯一的path,这个path就可以作为一个名称。
分布式通知/协调
ZooKeeper中特有watcher注册与异步通知机制,能够很好的实现分布式环境下不同系统之间的通知与协调,实现对数据变更的实时处理。使用方法通常是不同系统都对ZK上同一个znode进行注册,监听znode的变化(包括znode本身内容及子节点的),其中一个系统update了znode,那么另一个系统能够收到通知,并作出相应处理
另一种心跳检测机制:检测系统和被检测系统之间并不直接关联起来,而是通过zk上某个节点关联,大大减少系统耦合。
另一种系统调度模式:某系统有控制台和推送系统两部分组成,控制台的职责是控制推送系统进行相应的推送工作。管理人员在控制台作的一些操作,实际上是修改了ZK上某些节点的状态,而ZK就把这些变化通知给他们注册Watcher的客户端,即推送系统,于是,作出相应的推送任务。
另一种工作汇报模式:一些类似于任务分发系统,子任务启动后,到zk来注册一个临时节点,并且定时将自己的进度进行汇报(将进度写回这个临时节点),这样任务管理者就能够实时知道任务进度。
总之,使用zookeeper来进行分布式通知和协调能够大大降低系统之间的耦合
集群管理与Master选举
集群机器监控:这通常用于那种对集群中机器状态,机器在线率有较高要求的场景,能够快速对集群中机器变化作出响应。这样的场景中,往往有一个监控系统,实时检测集群机器是否存活。
利用ZooKeeper有两个特性,就可以实时另一种集群机器存活性监控系统:
1客户端在节点 x 上注册一个Watcher,那么如果 x?的子节点变化了,会通知该客户端。
2创建EPHEMERAL类型的节点,一旦客户端和服务器的会话结束或过期,那么该节点就会消失。
例如,监控系统在 /clusterServers 节点上注册一个Watcher,以后每动态加机器,那么就往 /clusterServers 下创建一个 EPHEMERAL类型的节点:/clusterServers/{hostname}. 这样,监控系统就能够实时知道机器的增减情况,至于后续处理就是监控系统的业务了。
Master选举则是zookeeper中最为经典的应用场景了。
在分布式环境中,相同的业务应用分布在不同的机器上,有些业务逻辑(例如一些耗时的计算,网络I/O处理),往往只需要让整个集群中的某一台机器进行执行,其余机器可以共享这个结果,这样可以大大减少重复劳动,提高性能,于是这个master选举便是这种场景下的碰到的主要问题。
利用ZooKeeper的强一致性,能够保证在分布式高并发情况下节点创建的全局唯一性,即:同时有多个客户端请求创建 /currentMaster 节点,最终一定只有一个客户端请求能够创建成功。利用这个特性,就能很轻易的在分布式环境中进行集群选取了。
另外,这种场景演化一下,就是动态Master选举。这就要用到?EPHEMERAL_SEQUENTIAL类型节点的特性了。
上文中提到,所有客户端创建请求,最终只有一个能够创建成功。在这里稍微变化下,就是允许所有请求都能够创建成功,但是得有个创建顺序,于是所有的请求最终在ZK上创建结果的一种可能情况是这样: /currentMaster/{sessionId}-1 ,?/currentMaster/{sessionId}-2,?/currentMaster/{sessionId}-3 ….. 每次选取序列号最小的那个机器作为Master,如果这个机器挂了,由于他创建的节点马上消失,那么之后最小的那个机器就是Master了。
在搜索系统中,如果集群中每个机器都生成一份全量索引,不仅耗时,而且不能保证彼此之间索引数据一致。因此让集群中的Master来进行全量索引的生成,然后同步到集群中其它机器。另外,Master选举的容灾措施是,可以随时进行手动指定master,就是说应用在zk在无法获取master信息时,可以通过比如http方式,向一个地方获取master。
在Hbase中,也是使用ZooKeeper来实现动态HMaster的选举。在Hbase实现中,会在ZK上存储一些ROOT表的地址和HMaster的地址,HRegionServer也会把自己以临时节点(Ephemeral)的方式注册到Zookeeper中,使得HMaster可以随时感知到各个HRegionServer的存活状态,同时,一旦HMaster出现问题,会重新选举出一个HMaster来运行,从而避免了HMaster的单点问题
分布式锁
分布式锁,这个主要得益于ZooKeeper为我们保证了数据的强一致性。锁服务可以分为两类,一个是保持独占,另一个是控制时序。
所谓保持独占,就是所有试图来获取这个锁的客户端,最终只有一个可以成功获得这把锁。通常的做法是把zk上的一个znode看作是一把锁,通过create znode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。
控制时序,就是所有视图来获取这个锁的客户端,最终都是会被安排执行,只是有个全局时序了。做法和上面基本类似,只是这里 /distribute_lock 已经预先存在,客户端在它下面创建临时有序节点(这个可以通过节点的属性控制:CreateMode.EPHEMERAL_SEQUENTIAL来指定)。Zk的父节点(/distribute_lock)维持一份sequence,保证子节点创建的时序性,从而也形成了每个客户端的全局时序。
分布式队列
队列方面,简单地讲有两种,一种是常规的先进先出队列,另一种是要等到队列成员聚齐之后的才统一按序执行。对于第一种先进先出队列,和分布式锁服务中的控制时序场景基本原理一致。
第二种队列其实是在FIFO队列的基础上作了一个增强。通常可以在 /queue 这个znode下预先建立一个/queue/num 节点,并且赋值为n(或者直接给/queue赋值n),表示队列大小,之后每次有队列成员加入后,就判断下是否已经到达队列大小,决定是否可以开始执行了。这种用法的典型场景是,分布式环境中,一个大任务Task A,需要在很多子任务完成(或条件就绪)情况下才能进行。这个时候,凡是其中一个子任务完成(就绪),那么就去 /taskList 下建立自己的临时时序节点(CreateMode.EPHEMERAL_SEQUENTIAL),当 /taskList 发现自己下面的子节点满足指定个数,就可以进行下一步按序进行处理了。
5. 开发指南
5.1 简介
本文是为想要创建使用ZooKeeper协调服务优势的分布式应用的开发者准备的。本文包含理论信息和实践信息。
5.2 ZooKeeper数据模型
ZooKeeper有一个分层的名字空间,跟分布式文件系统很相似。唯一的不同是,名字空间中的每个节点都可以有关联的数据和子节点。这就像一个允许文件也是目录的文件系统。节点路径总是表达为规则的、斜杠分隔的绝对路径,不存在相对路径。路径可以使用任何Unicode字符,但是需要遵循下列限制:
l 不能使用空字符(\\u0000)。(这在C绑定中会导致问题)
l 因为不能正确显示,或者容易弄混淆,不能使用这些字符:\\u0001 - \\u0019和\\u007F - \\u009F。
l 不允许使用这些字符:\\ud800 - uF8FFF、\\uFFF0 - uFFFF、\\uXFFFE - \\uXFFFF(X是1到E之间的一个数字)、\\uF0000 - \\uFFFFF。
l 可以使用小数点,但是不能单独使用.和..来指示路径中的节点,因为ZooKeeper不使用相对路径。/a/b/./c或者/a/b/../c是无效的。
l 记号zookeeper是保留的。
5.2.1 ZNode
ZooKeeper树中的节点称作znode。znode会维护一个包含数据修改和ACL修改版本号的Stat结构体,这个结构体还包含时间戳字段。版本号和时间戳让ZooKeeper可以校验缓存,协调更新。每次修改znode数据的时候,版本号会增加。客户端获取数据的同时,也会取得数据的版本号。执行更新或者删除操作时,客户端必须提供版本号。如果提供的版本号与数据的实际版本不匹配,则更新操作失败。(可以覆盖这个行为,更多信息请看……)
注意:
分布式应用工程中,node这个词可以指代主机、服务器、集群成员、客户端进程等等。ZooKeeper文档用znode指代数据节点;用server指代组成ZooKeeper服务的机器;用quorum peer指代组成集群的服务器;用client指代任何使用ZooKeeper服务的主机或者进程。
znode是程序员访问的主要实体,它有一些值得讨论的特征。
5.2.1.1 观察
客户端可以在znode上设置观察。对znode的修改将触发观察,然后移除观察。观察被触发时,ZooKeeper向客户端发送一个通知。关于观察的更多信息请看ZooKeeper观察。
5.2.1.2 数据存取
存储在名字空间中每个znode节点里的数据是原子地读取和写入的。读取操作获取节点的所有数据,写入操作替换所有数据。节点的访问控制列表(ACL)控制可以进行操作的用户。
ZooKeeper不是设计用来作为通用数据库或者大型对象存储的,而是用来存储协调数据的。协调数据的形式可能是配置、状态信息、聚合等等。各种形式的协调数据的一个共同特点是:它们通常比较小,以千字节来衡量。ZooKeeper客户端和服务器实现会进行检查,以保证znode数据小于1MB,但是平均的实际数据量应该远小于1MB。对较大数据的操作将导致某些操作比其他操作耗费更多时间,进而影响某些操作的延迟,因为需要额外的时间在网络和存储媒体间移动更多数据。如果需要大数据存储,通常方式是存储到块存储系统,如NFS或者HDFS中,然后在ZooKeeper中保存到存储位置的指针。
5.2.1.3 临时节点
ZooKeeper有临时节点的概念。临时节点在创建它的会话活动期间存在。会话终止的时候,临时节点被删除,所以临时节点不能有子节点。
5.2.1.4 顺序节点:唯一命名
创建znode时,可以要求ZooKeeper在路径名后增加一个单调增加的计数器部分。这个计数器相对于znode的父节点是唯一的。计数器的格式是%010d,也就是带有0填充的10个数字(这种格式是为了方便排序),比如说,
5.2.2 ZooKeeper中的时间
ZooKeeper以多种方式跟踪时间:
l zxid
每次修改ZooKeeper状态都会收到一个zxid形式的时间戳,也就是ZooKeeper事务ID。事务ID是ZooKeeper中所有修改总的次序。每个修改都有唯一的zxid,如果zxid1小于zxid2,那么zxid1在zxid2之前发生。
l 版本号
对节点的每次修改将使得节点的版本号增加一。版本号有三种:version(znode数据修改的次数)、cversion(znode子节点修改的次数),以及aversion(znode的ACL修改次数)。
l tick
多服务器ZooKeeper中,服务器使用tick来定义状态上传、会话超时、节点间连接超时等事件的时序。tick仅被最小会话超时(2倍的tick时间)间接使用:如果客户端要求小于最小会话超时的时间,服务器将告知客户端,实际使用的是最小会话超时。
l 真实时间
除了在创建和修改znode时将时间戳放入stat结构体中之外,ZooKeeper不使用真实时间,或者说时钟时间。
5.2.3 ZooKeeper的Stat结构体
ZooKeeper中每个znode的Stat结构体由下述字段构成:
l czxid:创建节点的事务的zxid
l mzxid:对znode最近修改的zxid
l ctime:以距离时间原点(epoch)的毫秒数表示的znode创建时间
l mtime:以距离时间原点(epoch)的毫秒数表示的znode最近修改时间
l version:znode数据的修改次数
l cversion:znode子节点修改次数
l aversion:znode的ACL修改次数
l ephemeralOwner:如果znode是临时节点,则指示节点所有者的会话ID;如果不是临时节点,则为零。
l dataLength:znode数据长度。
l numChildren:znode子节点个数。
5.3 ZooKeeper会话
客户端使用某种语言绑定创建一个到服务的句柄时,就建立了一个ZooKeeper会话。会话创建后,句柄处于CONNECTING状态,客户端库会试图连接到组成ZooKeeper服务的某个服务器;连接成功则进入到CONNECTED状态。通常操作中句柄将处于这两个状态之一。如果发生不可恢复的错误,如会话过期、身份鉴定失败,或者应用显式关闭,则句柄进入到CLOSED状态。下图显式了ZooKeeper客户端可能的状态转换:
要创建客户端会话,应用程序代码必须提供一个包含逗号分隔的列表的字符串,其中每个主机:端口对代表一个ZooKeeper服务器(例如,"127.0.0.1:4545"或者"127.0.0.1:3001,127.0.0.1:3002")。ZooKeeper客户端库将试图连接到任意选择的一个服务器。如果连接失败,或者到服务器的连接断开,则客户端将自动尝试连接到列表中的下一个服务器,直到连接(重新)建立。
3.2.0版新增加:可以在连接字符串后增加可选的"chroot"后缀,这让客户端命令都是相对于指定的根的(类似于Unix的chroot命令)。例如,如果使用"127.0.0.1:4545/app/a"或者"127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002/app/a",则客户端的根将是/app/a,所有路径将是相对于这个根的:获取/设置/foo/bar数据的操作将实际在/app/a/foo/bar上执行(从服务器来看)。这个特征在多用户环境中特别有用,某个特定ZooKeeper服务的每个用户可以使用不同的根。这让重用更加简单,用户应用在编码时以/为根,但实际的根位置(如/app/a)可以在部署时确定。
客户端取得ZooKeeper服务句柄时,ZooKeeper创建一个会话,由一个64位数标识,这个数将返回给客户端。如果连接到其他服务器,客户端将在连接握手时发送会话ID。出于安全考虑,服务器会为会话ID创建一个密码,ZooKeeper服务器可以校验这个密码。这个密码将在创建会话时与会话ID一同发送给客户端。与新的服务器重新建立会话的时候,客户端会和会话ID一同发送这个密码。
客户端库创建会话时需要的参数之一是毫秒表示的会话超时。客户端发送请求的超时值,服务器以可以分配给客户端的超时值回应。当前实现要求超时值最小是2倍的tickTime(在服务器配置文件中设置),最大是20倍的tickTime。客户端API可以获取商定的超时值。
从服务集群分裂开来时,客户端(会话)将搜索会话创建时给出的服务器列表。最终,客户端至少和一个服务器重新建立连接,会话再次进入“已连接”状态(如果在会话超时之前重新连接上),或者进入到“已过期”状态(如果在会话超时后才重新连接上)。不建议在断开连接时创建一个新的会话对象(即一个新的ZooKeeper.class对象,或者C绑定中的zookeeper句柄),因为客户端库会进行重新连接。特别是客户端库具有试探特征,可以处理“羊群效应”等问题。只需要在被通知会话已过期时创建新的会话(必须的)。
会话过期由ZooKeeper集群,而不是客户端来管理。客户端与集群建立会话时会提供上面讨论的超时值。集群使用这个值来确定客户端会话何时过期。集群在指定的超时时间内没有得到客户端的消息时发生会话过期。会话过期时集群将删除会话的所有临时节点,立即通知所有(观察节点的)客户端。此时已过期会话的客户端还是同集群断开连接的,不会被通知会话已经过期,直到(除非)客户端重新建立到集群的连接,这时候已过期会话的观察才会收到“会话已过期”通知。
已过期会话的观察看到的状态转换过程示例:
1.已连接:会话建立,客户端与集群通信中(客户端/服务器通信正常进行)
2.客户端从集群中分离
3.连接已断开:客户端失去同集群的连接
4.时间流逝,超时时间过后,集群让会话过期,客户端并不知道,因为它还是同集群断开连接的。
5.时间流逝,客户端与集群间的网络恢复正常。
6.已过期:最终客户端重新连接到集群,此时被通知会话已经过期。
建立会话时的另一个参数是默认观察。客户端发生状态改变时观察会被通知。比如说,客户端将在失去同服务器的连接,或者会话过期时被通知。观察应该认为初始状态是连接已经断开(在客户端库向观察发送任何状态改变事件之前)。新建连接时发送给观察的第一个事件通常是会话连接建立事件。
会话由客户端发送的请求保持为活动状态。如果要空闲一段将导致超时的时间,客户端将发送PING请求,保持会话是活动的。PING请求不仅让服务器知道客户端仍然是存活的,也让客户端可以确认,到服务器的连接依然是活动的。PING的时序足够保守,确保能够在合理的时间内检测到死掉的连接,重新连接到新的服务器。
一旦到服务器的连接成功建立,则进行同步或者异步操作时,通常有两种情况导致客户端库产生连接丢失事件(C绑定中的错误码,Java中的异常:关于绑定特定的细节,请看API文档):
1.应用程序对已经不存活/有效的会话进行操作
2.在有到服务器的未决操作(例如,有一个进行中的异步调用)时,客户端断开同服务器的连接
3.2.0版增加:SessionMovedException。有一种称作SessionMovedException的 内部异常。通常客户端看不到这个异常。在某连接上收到一个会话请求,但是这个会话已经重建到另一个服务器上的时候会发生这种异常。导致这种错误的原因通常是,客户端向服务器发送请求,但是数据分组被延迟,以致客户端超时并且连接到一个新的服务器。延迟的分组到达先前的服务器的时候,服务器检测到会话已经移走,会关闭客户端连接。客户端通常看不到这个错误,因为客户端不会从较早的连接上读取数据(通常关闭了较早的连接)。两个客户端试图使用已保存的会话ID和密码重新建立相同的连接时会看到这种错误。其中一个客户端将重新建立连接,而另一个客户端会被断开连接(导致无限次地试图重新建立连接/会话)。
5.4 ZooKeeper观察
ZooKeeper中的所有读操作:getData()、getChildren()和exists(),都有一个设置观察作为边效应的选项。ZooKeeper对观察的定义是:观察事件是在被观察数据发生变化时,发送给建立观察的客户端的一次性触发器。对于这个定义,有三点值得关注
l 一次触发
观察事件将在数据修改后发送给客户端。比如说,如果客户端执行getData("/znode1",true),然后/znode1的数据发生变化,或者被删除,则客户端将收到/znode1的观察事件。如果再次修改/znode1,则不会给客户端发送观察事件,除非客户端再执行一次读取操作,设置新的观察。
l 发送给客户端
这暗示着,在(导致观察事件被触发的)修改操作的成功返回码到达客户端之前,事件可能在去往客户端的路上,但是可能不会到达客户端。观察事件是异步地发送给观察者(客户端)的。ZooKeeper会保证次序:在收到观察事件之前,客户端不会看到已经为之设置观察的节点的改动。网络延迟或者其他因素可能会让不同的客户端在不同的时间收到观察事件和更新操作的返回码。这里的要点是:不同客户端看到的事情都有一致的次序。
l 为哪些数据设置观察
节点有不同的改动方式。可以认为ZooKeeper维护两个观察列表:数据观察和子节点观察。getData()和exists()设置数据观察。getChildren()设置子节点观察。此外,还可以认为不同的返回数据有不同的观察。getData()和exists()返回节点的数据,而getChildren()返回子节点列表。所以,setData()将为znode触发数据观察。成功的create()将为新创建的节点触发数据观察,为其父节点触发子节点观察。成功的delete()将会为被删除的节点触发数据观察以及子节点观察(因为节点不能再有子节点了),为其父节点触发子节点观察。
观察维护在客户端连接到的ZooKeeper服务器中。这让观察的设置、维护和分发是轻量级的。客户端连接到新的服务器时,所有会话事件将被触发。同服务器断开连接期间不会收到观察。客户端重新连接时,如果需要,先前已经注册的观察将被重新注册和触发。通常这都是透明的。有一种情况下观察事件将丢失:对还没有创建的节点设置存在观察,而在断开连接期间创建节点,然后删除。
5.4.1 ZooKeeper关于观察的保证
l 观察与其他事件、其他观察和异步回应是顺序的。ZooKeeper客户端库保证一切都是按顺序分发的。
l 客户端将在看到znode的新数据之前收到其观察事件。
l 观察事件的次序与ZooKeeper服务看到的更新次序一致。
5.4.2 关于观察需要记住的
l 观察是一次触发的:如果想在收到观察事件之后收到未来修改的通知,必须再次设置观察
l 因为观察是一次触发的,而收到观察事件和发送新的请求、再次建立观察之间是有延迟的,所以不能可靠地观察到节点的所有修改。应该要准备处理在收到观察事件和再次设置观察之间,节点被多次修改的情况。(可以不处理,但至少要知道这种情况是可能的)
l 一个观察对象,或者函数/上下文对,只会因为某个通知而触发一次。比如说,对同一个文件使用exists和getData调用,设置相同的观察对象,然后文件被删除,则观察对象只会被调用一次,带有文件删除通知。
l 与服务器断开连接期间(比如说,服务器故障)不能收到任何观察事件,直到连接重新建立。因此,会话事件是发送给所有未决观察处理器的。可使用会话事件进入到安全模式:断开连接期间不会收到任何事件,进程应该谨慎操作
5.5 使用ACL的访问控制
ZooKeeper使用ACL控制对节点的访问。ACL的实现同Unix文件访问权限非常相似:采用权限位来定义允许/禁止的各种节点操作,以及位应用的范围。与标准Unix权限不同的是,ZooKeeper节点不由用户(文件所有者)、组和其他这三个标准范围来限制。ZooKeeper没有节点所有者的概念。取而代之的是,ACL指定一个ID集合,以及与这些ID相关联的权限。
还要注意的是,ACL仅仅用于某特定节点。特别是,ACL不会应用到子节点。比如说,/app只能被ip:172.16.16.1读取,/app/status可以被所有用户读取。ACL不是递归的。
ZooKeeper支持可插入式鉴权模式。使用scheme:id的形式指定ID,其中scheme是id对应的鉴权模式。比如说,ip:172.16.16.1是地址为172.16.16.1的主机的ID。
客户端连接到ZooKeeper,验证自身的时候,ZooKeeper将所有对应客户端的ID都关联到客户端连接上。客户端试图存取节点的时候,ZooKeeper会在节点的ACL中校验这些ID。ACL由(scheme:expression,perms)对组成。expression的格式是特定于scheme的。比如说,(ip:19.22.0.0/16,READ)给予任何IP地址以19.22开头的客户端以READ权限。
5.5.1 ACL权限
ZooKeeper支持下述权限:
l CREATE:可创建子节点
l READ:可获取节点数据和子节点列表
l WRITE:可设置节点数据
l DELETE:可删除子节点
l ADMIN:可设置节点权限
从WRITE权限中分离出CREATE和DELETE可以取得更好的访问控制。使用CREATE和DELETE的情况:
l 希望A可以设置节点数据,但是不能CREATE或者DELETE子节点。
l 没有DELETE的CREATE权限:客户端通过在某父目录中创建节点来创建请求。此时希望所有客户端可以添加节点,但是只有请求处理器可以删除节点。(这与文件的APPEND权限类似)
此外,ADMIN权限存在的原因是,ZooKeeper没有文件所有者的概念。某些情况下ADMIN权限可以指定实体的所有者。ZooKeeper不支持LOOKUP权限(目录上的、允许进行LOOKUP的执行权限位,即使不能列出目录内容)。每个用户都隐含地拥有LOOKUP权限。这仅仅让用户可以取得节点状态。(问题是,如果想对一个不存在的节点进行zoo_exists()调用,没有权限可以检查)
5.5.1.1 内置的ACL模式
ZooKeeper内置下述ACL模式:
l world具有单独的ID,代表任何用户。
l auth不使用任何ID,代表任何已确认用户。
l digest使用username:password字符串来生成MD5散列值,用作ID。身份验证通过发送明文的username:password字符串来进行。用在ACL表达式中时将是username:base64编码的SHA1密码摘要。
l ip使用客户端主机IP作为ID。ACL表达式的形式是addr/bits,表示addr的最高bits位将与客户端主机IP的最高bits位进行匹配。
5.5.1.2 ZooKeeper C客户端API
ZooKeeper C库提供下述常量:
l const int ZOO_PERM_READ;//可读取节点的值,列出子节点
l const int ZOO_PERM_WRITE;//可设置节点数据
l const int ZOO_PERM_CREATE;//可创建子节点
l const int ZOO_PERM_DELETE;//可删除子节点
l const int ZOO_PERM_ADMIN;//可执行set_acl()
l const int ZOO_PERM_ALL;//OR连接的上述所有标志
下面是标准的ACL ID:
l struct Id ZOO_ANYONE_ID_UNSAFE;//('world','anyone')
l struct Id ZOO_AUTH_IDS;//('auth','')
空的ZOO_AUTH_IDS标识字符串应该解释为“创建者的标识”。
ZooKeeper有三种标准ACL:
l struct ACL_vectorZOO_OPEN_ACL_UNSAFE;//(ZOO_PERM_ALL,ZOO_ANYONE_ID_UNSAFE)
l struct ACL_vector ZOO_READ_ACL_UNSAFE;//(ZOO_PERM_READ,ZOO_ANYONE_ID_UNSAFE)
l struct ACL_vector ZOO_CREATOR_ALL_ACL;//(ZOO_PERM_ALL,ZOO_AUTH_IDS)
ZOO_OPEN_ACL_UNSAFE是完全开放自由的ACL:任何应用程序可以对节点进行任何操作,以及创建、列出和删除子节点。ZOO_READ_ACL_UNSAFE给予任何应用程序以只读访问权限。CREATE_ALL_ACL给予节点创建者所有权限。创建者在使用这种ACL创建节点之前,必须已经通过服务器的身份鉴别(比如说,使用"digest"模式)。
下述ZooKeeper操作用于处理ACL:
l int zoo_add_auth(zhandle_t* zh,const char* scheme,const char* cert,intcertLen,void_completion_t completion,const void* data);
应用程序使用zoo_add_auth函数向服务器验证自身。如果想使用不同的模式和/或标识来进行身份验证,可以多次调用这个函数。
l int zoo_create(zhandle_t* zh,const char* path,const char* value,intvaluelen,const struct ACL_vector* acl,int flags,char* realpath,intmax_realpath_len);
zoo_create()创建新的节点。acl是与节点相关的ACL列表。父节点必须设置了CREATE权限位。
l int zoo_get_acl(zhandle_t* zh,const char* path,struct ACL_vector*acl,struct Stat* stat);
这个函数返回节点的ACL信息。
l int zoo_set_acl(zhandle_t* zh,const char* path,int version,const structACL_vector* acl);
这个函数替换节点的ACL列表。节点必须设置了ADMIN权限。
下面是一段使用上述API来进行foo模式的身份验证,然后创建具有仅创建者可访问权限的临时节点/xyz的示例代码。
注意
这是一个展示如何与ZooKeeper ACL交互的非常简单的示例。更合适的C客户端实现示例请看../trunk/src/c/src/cli.c。
<……省略示例代码……>
5.6 插入式身份验证
ZooKeeper运行在各种使用不同身份验证模式的环境中,所以它有一个完全插入式的身份验证框架。内置的身份验证模式也是使用这个框架的。
要理解身份验证框架如何工作,首先必须理解两种主要的身份验证操作。框架首先要验证客户。这通常在客户端连接到服务器后立即进行,由验证客户端发送的信息,或者验证收集的关于客户端的信息,并且将其关联到连接两个步骤构成。框架进行的第二个操作是在ACL中找出客户端对应的实体。ACL实体就是
第一个方法,getScheme返回标识插件的字符串。因为支持多种身份验证方法,所以每个身份验证凭证,或者说idspec总是带有scheme:前缀。ZooKeeper服务器使用身份验证插件返回的模式字符串来确定将模式应用到哪些id。
handleAuthentication在客户端发送与连接相关联的身份验证信息时被调用。客户端指定身份验证信息的模式。ZooKeeper服务器将信息传递给getScheme返回值与客户端传递的模式值相匹配的身份验证插件。handleAuthentication通常在确定身份验证信息不正确时返回错误,或者使用cnxn.getAuthInfo().add(new Id(getScheme(),data))将身份验证信息关联到连接。
身份验证插件与设置和使用ACL相关。为节点设置ACL时,ZooKeeper服务器会将条目的id部分传递给isValid(String id)方法。插件必须验证id具有正确的形式。比如说,ip:172.16.0.0/16是一个有效的id,但是ip:host.com则不是。
如果新的ACL含有auth条目,则isAuthenticated用于确定与连接相关联的身份验证信息是否要添加到ACL中。某些模式不应该包含在auth中。比如说,如果指定了auth,则客户端的IP地址不会被看作是id,不应该添加到ACL中。
检查ACL时,ZooKeeper调用matches(String id,String aclExpr)。函数需要将客户端的身份验证信息与相应的ACL条目进行匹配。为找出应用到客户端的条目,ZooKeeper服务器找出每个条目的模式,如果有客户端的、这个模式的身份验证信息,则matches(String id,String aclExpr)会被调用,id设置为先前通过handleAuthentication添加到连接的身份验证信息,aclExpr设置为ACL条目的id。身份验证插件使用其逻辑进行匹配,确定id是否包含在aclExpr中。
有两个内置的身份验证插件:id和digest。可通过系统属性添加额外的插件。ZooKeeper服务器启动时会查找以zookeeper.authProvider.开头的系统属性,将这些属性的值解释为身份验证插件的类名。可使用-Dzookeeper.authProvider.X=com.f.MyAuth来设置这些属性,或者在系统配置文件中添加类似于下面的条目:
应该注意,要确保后缀是唯一的。如果有重复的,如-Dzookeeper.authProvider.X=com.f.MyAuth和-Dzookeeper.authProvider.X=com.f.MyAuth2,只会使用一个。此外,所有服务器必须定义有同样的插件,否则客户端使用插件提供的身份验证模式连接到某些服务器时会有问题。
5.7 一致性保证
ZooKeeper是高性能、可伸缩的服务。读和写操作都设计为高速操作,虽然读比写更快。原因是在读操作中,ZooKeeper可返回较老的数据,这源自ZooKeeper的一致性保证:
l 顺序一致性:一个客户端的更新将以发送的次序被应用。
l 原子性:更新要么成功,要么失败,没有部分结果。
l 单一系统镜像:无论连接到哪个服务器,客户端将看到同样的视图。
l 可靠性:一旦应用了某更新,结果将是持久的,直到客户端覆盖了更新。这个保证有两个推论:
1.如果客户端得到成功的返回码,则更新已经被应用。某些失败情况下(通信错误、超时等),客户端不知道更新是否已经应用。我们采取措施保证最小化失败,但这个保证只对成功的返回码有效。(这称作是Paxos中的单一条件)
2.服务器从失败恢复时,客户端通过读请求或者成功更新看到的任何更新,都不会回滚。
l 及时性:保证客户端的系统视图在某个时间范围(大约为十几秒)内是最新的。在此范围内,客户端要么可看到系统的修改,要么检测到服务终止。
使用这些一致性保证,就可以很容易地单独在ZooKeeper客户端构建如领导者选举、护栏、队列以及可恢复的读写锁等高层功能。更多细节请看Recipes and Solutions。
注意:有时候开发者会错误地假定一个ZooKeeper实际上没有提供的保证:
l 跨客户端视图的并发一致性
ZooKeeper并不保证在某时刻,两个不同的客户端具有一致的数据视图。因为网络延迟的原因,一个客户端可能在另一个客户端得到修改通知之前进行更新。假定有两个客户端A和B。如果客户端A将一个节点/a的值从0修改为1,然后通知客户端B读取/a,客户端B读取到的值可能还是0,这取决于它连接到了哪个服务器。如果客户端A和B读取到相同的值很重要,那么客户端B应该在执行读取之前调用sync()方法。
所以,ZooKeeper本身不保证修改在多个服务器间同步地发生,但是可以使用ZooKeeper原语来构建高层功能,提供有用的客户端同步。
5.8 绑定
ZooKeeper客户端库以两种方式提供:Java和C。下面几节描述这两种绑定。
5.8.1 Java绑定
ZooKeeper的Java绑定由两个包组成:org.apache.zookeeper和org.apache.zookeeper.data。组成ZooKeeper的其他包由内部使用或者是服务器实现的组成部分。org.apache.zookeeper.data由简单地用作容器的类构成。
ZooKeeper Java客户端使用的主要类是ZooKeeper类。这个类的两个构造函数的不同仅仅在于可选的会话ID和密码。ZooKeeper支持进程的不同实例间的会话恢复。Java程序可以将会话ID和密码保存到稳态存储中,然后重启、恢复程序先前实例使用的会话。
创建ZooKeeper对象的时候,会同时创建两个线程:一个IO线程和一个事件线程。所有IO在IO线程中发生(使用Java NIO)。所有事件回调则在事件线程中进行。重连到ZooKeeper服务器和维持心跳等会话维持活动在IO线程中进行。同步方法的回应也在IO线程中进行。所有异步方法的回应,以及观察事件则在事件线程中处理。对于这个设计,有一些事情需要注意:
l 所有同步调用和观察回调将按次序进行,一次一个。调用者可以进行任何想要的处理,但是在此期间不会处理其他回调。
l 回调不会阻塞IO线程或者同步调用的处理。
l 同步调用可能不会以正确的次序返回。比如说,假设客户端进行下述处理:提交一个watch设置为ture的、对节点/a的异步读取,然后在读取操作的完成回调中执行一个对/a的同步读取。(可能是不好的实现,但是是合法的,这只是一个简单的例子)
如果在异步读取和同步读取之间,对/a进行了修改,则客户端库将在同步读取返回之前接收到一个事件,表明/a已经被修改。但是因为完成回调阻塞了事件队列,同步读取将在观察事件被处理之前返回/a的新值。
最后,关于关闭的规则很直接:一旦被关闭或者接收到致命事件(SESSION_EXPIRED和AUTH_FAILED),ZooKeeper对象就变成无效的了。关闭后,两个线程被关闭,后续对zookeeper句柄的任何访问都将导致不确定的行为,应该避免。
在应用程序中使用ZooKeeper API时,应该记住:
1.包含ZooKeeper头文件:#include
2.如果创建多线程客户端,请使用-DTHREADED编译器标志,以启用库的多线程版本,并且链接到zookeeper_mt库。如果创建单线程客户端,不要使用-DTHREADED,并且链接到zookeeper_st库。
关于Java和C的使用示例,请看程序结构和简单示例。
5.9 创建块:ZooKeeper操作指南
本节描述开发者可对ZooKeeper服务器执行的所有操作。这些信息比本手册前面章节的内容要更底层,但是比ZooKeeper API参考的层次要高。
5.9.1 处理错误
Java和C绑定都可能报告错误。Java客户端绑定通过抛出KeeperException来报告错误,对异常对象调用code()可取得特定的错误码。C客户端绑定返回ZOO_ERRORS枚举定义的错误码。在两个语言绑定中,API回调都指示结果值。关于所有可能的错误码及其含义的详细信息,请看API文档(Java绑定的javadoc,C绑定的doxygen)。
5.9.2 连接到ZooKeeper
5.9.3 读取操作
5.9.4 写入操作
5.9.5 处理观察
5.9.6 其他ZooKeeper操作
5.10 程序结构和简单示例
5.11 转向:常见问题和解决
现在你了解ZooKeeper了,它高效、简单,你的程序可以工作,但是等等……,出了点问题了。
下面是ZooKeeper用户遇到的一些陷阱:
1.使用观察的时候,必须处理已连接的观察事件。ZooKeeper客户端同服务器断开连接期间,不会收到修改通知,直到重新连接。如果观察一个节点的出现,则断开连接期间会错过节点的创建和删除事件。
2.必须测试ZooKeeper服务失败。一旦多数服务器不活动,ZooKeeper服务会失败。问题是:你的程序可以处理这种情况吗?在真实世界中,客户端到ZooKeeper的连接可能会断开(ZooKeeper服务器失败和网络分区是连接丢失的常见原因)。ZooKeeper客户端库会处理连接恢复,并且让你知道发生了什么,但是你必须保证可以正确恢复状态和任何已失败的未决请求。在实验室确认程序是正确的,而不是在产品中:用由多个服务器组成的ZooKeeper服务进行测试,并且进行一些重启。
3.客户端和服务器使用的服务器列表应该一致。如果客户端的列表只是真正的服务器列表的一部分,程序可以工作,虽然不是最优的;但是如果客户端包含不在集群中的服务器,则不能工作。
4.注意在哪里放置事务日志。事务日志是ZooKeeper中最关乎性能的部分。返回响应之前,ZooKeeper必须将事务同步到媒体中。专用事务日志设备是取得良好性能的关键。如果只有一个存储设备,把跟踪文件放到NFS中,并且增加snapshotCount;这不能解决问题,但是有一定的改善。
5.正确设置Java的最大堆大小。避免交换是非常重要的。大多数情况下,不必要地放入磁盘肯定会降低性能到不可接受的程度。记住,在ZooKeeper中,一切都是顺序的,如果一个请求触及磁盘,其他排队的请求也会触及磁盘。
为避免交换,试试将堆大小设置为拥有的物理内存大小减去操作系统和缓存需要的大小。确定最优堆大小的最好方法是执行负载测试。如果因为一些原因而不能进行测试,请采取保守估计,选择一个小于将导致交换的值。比如说,在4GB的机器上,3GB是一个保守的开始值。
6. 常用命令
参数名 |
说明 |
conf |
输出server的详细配置信息。New in 3.3.0 $>echo conf|nc localhost 2181 |
cons |
输出指定server上所有客户端连接的详细信息,包括客户端IP,会话ID等。 $>echo cons|nc localhost 2181 |
crst |
功能性命令。重置所有连接的统计信息。New in 3.3.0 |
dump |
这个命令针对Leader执行,用于输出所有等待队列中的会话和临时节点的信息。 |
envi |
用于输出server的环境变量。包括操作系统环境和Java环境。 |
ruok |
用于测试server是否处于无错状态。如果正常,则返回“imok”,否则没有任何响应。 |
stat |
输出server简要状态和连接的客户端信息。 |
srvr |
和stat类似,New in 3.3.0 $>echo stat|nc localhost 2181 Latency min/avg/max: 0/0/1036 $>echo srvr|nc localhost 2181 |
srst |
重置server的统计信息。 |
wchs |
列出所有watcher信息概要信息,数量等:New in 3.3.0 $>echo wchs|nc localhost 2181 |
wchc |
列出所有watcher信息,以watcher的session为归组单元排列,列出该会话订阅了哪些path:New in 3.3.0 $>echo wchc|nc localhost 2181 |
wchp |
列出所有watcher信息,以watcher的path为归组单元排列,列出该path被哪些会话订阅着:New in 3.3.0 $>echo wchp|nc localhost 2181 注意,wchc和wchp这两个命令执行的输出结果都是针对session的,对于运维人员来说可视化效果并不理想,可以尝试将cons命令执行输出的信息整合起来,就可以用客户端IP来代替会话ID了,具体可以看这个实现:http://rdc.taobao.com/team/jm/archives/1450 |
mntr |
输出一些ZK运行时信息,通过对这些返回结果的解析,可以达到监控的效果。New in 3.4.0 $ echo mntr | nc localhost 2185 |
ZooKeeper 四字命令 |
功能描述 |
conf |
输出相关服务配置的详细信息。 |
cons |
列出所有连接到服务器的客户端的完全的连接 / 会话的详细信息。包括“接受 / 发送”的包数量、会话 id 、操作延迟、最后的操作执行等等信息。 |
dump |
列出未经处理的会话和临时节点。 |
envi |
输出关于服务环境的详细信息(区别于 conf 命令)。 |
reqs |
列出未经处理的请求 |
ruok |
测试服务是否处于正确状态。如果确实如此,那么服务返回“imok ”,否则不做任何相应。 |
stat |
输出关于性能和连接的客户端的列表。 |
wchs |
列出服务器 watch 的详细信息。 |
wchc |
通过 session 列出服务器 watch 的详细信息,它的输出是一个与watch 相关的会话的列表。 |
wchp |
通过路径列出服务器 watch 的详细信息。它输出一个与 session相关的路径。 |
7. 注意事项
7.1 保持Server地址列表一致
客户端使用的server地址列表必须和集群所有server的地址列表一致。(如果客户端配置了集群机器列表的子集的话,也是没有问题的,只是少了客户端的容灾。)
集群中每个server的zoo.cfg中配置机器列表必须一致。
7.2 独立的事务日志输出
对于每个更新操作,ZK都会在确保事务日志已经落盘后,才会返回客户端响应。因此事务日志的输出性能在很大程度上影响ZK的整体吞吐性能。强烈建议是给事务日志的输出分配一个单独的磁盘。
7.3 配置合理的JVM堆大小
确保设置一个合理的JVM堆大小,如果设置太大,会让内存与磁盘进行交换,这将使ZK的性能大打折扣。例如一个4G内存的机器的,如果你把JVM的堆大小设置为4G或更大,那么会使频繁发生内存与磁盘空间的交换,通常设置成3G就可以了。当然,为了获得一个最好的堆大小值,在特定的使用场景下进行一些压力测试。
7.4Watches通知是一次性的,必须重复注册.
7.5同一个ZK客户端,反复对同一个ZK节点(znode)注册相同的watcher,是无效的,最终只会有一个生效。
7.6发生CONNECTIONLOSS之后,只要在session_timeout之内再次连接上(即不发生SESSIONEXPIRED),那么这个连接注册的watches依然在。
7.7客户端会话失效之后,所有这个会话中创建的Watcher都会被移除。
7.8节点数据的版本变化会触发NodeDataChanged,注意,这里特意说明了是版本变化。存在这样的情况,只要成功执行了setData()方法,无论内容是否和之前一致,都会触发NodeDataChanged事件。
7.9对某个节点注册了watcher,但是节点被删除了,那么注册在这个节点上的watcher都会被移除。
8. 权限控制
方案一:采用ZooKeeper支持的ACL digest方式,用户自己定义节点的权限
这种方案将zookeeper的acl和digest授权认证模式相结合。具体操作流程如下:
可以把这个访问授权过程看作是用户注册,系统给你一个密码,每次操作使用这个用户名(appName)和密码. 于是就可以对应有这样权限管理系统,专门是负责进行节点的创建申请:包含“申请私有节点”和“申请公有节点”。这样一来,节点的创建都是由这个权限管理系统来负责了,每次申请完后,系统都会返回给你的一个key,格式通常是“{appName}:{password}”,以后你的任何操作都要在zk session 中携带上这个key,这样就能进行权限控制。当然,用户自己通过zk客户端进行path的创建也是可以的,只是要求他们要使用授权方式来进行zk节点的创建。(注意,如果使用zkclient,请使用 https://github.com/nileader/zkclient )
方案二、对zookeeper的AuthenticationProvider进行扩展,和内部其它系统A打通,从系统A中获取一些信息来判断权限
这个方案大致是这样:
1. A系统上有一份IP和appName对应的数据本地。
2. 将这份数据在ZK服务器上缓存一份,并定时进行缓存更新。
3. 每次客户端对服务器发起请求的时候,获取客户端ip进行查询,判断是否有对应appName的权限。限制指定ip只能操作指定 /appName znode。
4. 其它容灾措施。
个人比较两个方案:
1.方案一较方案二,用户的掌控性大,无论线上,日常,测试都可以由应用开发人员自己决定开启/关闭权限。(方案一的优势)
2.方案二较方案一,易用性强,用户的使用和无权限基本一致。 (方案二的优势)
3.方案一较方案二更为纯洁。因为我觉得zk本来就应该是一个底层组件,让他来依赖其它上层的另一个系统?权限的控制精度取决于系统A上信息的准确性。 (方案一的优势)
9.通信模型
在Zookeeper整个系统中,有3中角色的服务,client、Follower、leader。其中client负责发起应用的请求,Follower接受client发起的请求,参与事务的确认过程,在leader crash后的leader选择。而leader主要承担事务的协调,当然leader也可以承担接收客户请求的功能,为了方便描述,后面的描述都是client与Follower之间的通信,如果Zookeeper的配置支持leader接收client的请求,client与leader的通信跟client与Follower的通信模式完全一样。Follower与leader之间的角色可能在某一时刻进行转换。一个Follower在leader crash掉以后可能被集群(Quorum)的Follower选举为leader。而一个leader在crash后,再次加入集群(Quorum)将作为Follower角色存在。在一个集群(Quorum)中,除了在选举leader的过程中没有Follower和leader的区分外,其他任何时刻都只有1个leader和多个Follower。Client、Follower和leader之间的通信架构如下:
Client与Follower之间
为了使客户端具有较高的吞吐量,Client与Follower之间采用NIO的通信方式。当client需要与Zookeeper service打交道时,首先读取配置文件确定集群内的所有server列表,按照一定的load balance算法选取一个Follower作为一个通信目标。这样client和Follower之间就有了一条由NIO模式构成的通信通道。这条通道会一直保持到client关闭session或者因为client或Follower任一方因某种原因异常中断通信连接。正常情况下, client与Follower在没有请求发起的时候都有心跳检测。
Follower与leader之间
Follower与leader之间的通信主要是因为Follower接收到像(create,delete, setData, setACL, createSession, closeSession, sync)这样一些需要让leader来协调最终结果的命令,将会导致Follower与leader之间产生通信。由于leader与Follower之间的关系式一对多的关系,非常适合client/server模式,因此他们之间是采用c/s模式,由leader创建一个socket server,监听各Follower的协调请求。
集群在选择leader过程中
由于在选择leader过程中没有leader,在集群中的任何一个成员都需要与其他所有成员进行通信,当集群的成员变得很大时,这个通信量是很大的。选择leader的过程发生在Zookeeper系统刚刚启动或者是leader失去联系后,选择leader过程中将不能处理用户的请求,为了提高系统的可用性,一定要尽量减少这个过程的时间。选择哪种方式让他们可用快速得到选择结果呢?Zookeeper在这个过程中采用了策略模式,可用动态插入选择leader的算法。系统默认提供了3种选择算法,AuthFastLeaderElection,FastLeaderElection,LeaderElection。其中AuthFastLeaderElection和LeaderElection采用UDP模式进行通信,而FastLeaderElection仍然采用tcp/ip模式。在Zookeeper新的版本中,新增了一个learner角色,减少选择leader的参与人数。使得选择过程更快。一般说来Zookeeper leader的选择过程都非常快,通常<200ms。
Zookeeper的通信流程
要详细了解Zookeeper的通信流程,我们首先得了解Zookeeper提供哪些客户端的接口,我们按照具有相同的通信流程的接口进行分组:
Zookeeper系统管理命令
Zookeeper的系统管理接口是指用来查看Zookeeper运行状态的一些命令,他们都是具有4字母构成的命令格式。主要包括:
ruok:发送此命令可以测试zookeeper是否运行正常。
dump:dump server端所有存活session的Ephemeral(临时)node信息。
stat:获取连接server的服务器端的状态及连接该server的所有客服端的状态信息。
reqs: 获取当前客户端已经提交但还未返回的请求。
stmk:开启或关闭Zookeeper的trace level.
gtmk:获取当前Zookeeper的trace level是否开启。
envi: 获取Zookeeper的java相关的环境变量。
srst:重置server端的统计状态
当用户发送这些命令的到server时,由于这些请求只与连接的server相关,没有业务处理逻辑,非常简单。Zookeeper对这些命令采用最快的效率进行处理。这些命令发送到server端只占用一个4字节的int类型来表示不同命令,没有采用字符串处理。当服务器端接收到这些命令,立刻返回结果。
Session创建
任何客户端的业务请求都是基于session存在的前提下。Session是维持client与Follower之间的一条通信通道,并维持他们之间从创建开始后的所有状态。当启动一个Zookeeper client的时候,首先按照一定的算法查找出follower, 然后与Follower建立起NIO连接。当连接建立好后,发送create session的命令,让server端为该连接创建一个维护该连接状态的对象session。当server收到create session命令,先从本地的session列表中查找看是否已经存在有相同sessionId,则关闭原session重新创建新的session。创建session的过程将需要发送到Leader,再由leader通知其他follower,大部分Follower都将此操作记录到本地日志再通知leader后,leader发送commit命令给所有Follower,连接客户端的Follower返回创建成功的session响应。Leader与Follower之间的协调过程将在后面的做详细讲解。当客户端成功创建好session后,其他的业务命令就可以正常处理了。
Zookeeper查询命令
Zookeeper查询命令主要用来查询服务器端的数据,不会更改服务器端的数据。所有的查询命令都可以即刻从client连接的server立即返回,不需要leader进行协调,因此查询命令得到的数据有可能是过期数据。但由于任何数据的修改,leader都会将更改的结果发布给所有的Follower,因此一般说来,Follower的数据是可以得到及时的更新。这些查询命令包括以下这些命令:
exists:判断指定path的node是否存在,如果存在则返回true,否则返回false.
getData:从指定path获取该node的数据
getACL:获取指定path的ACL。
getChildren:获取指定path的node的所有孩子结点。
所有的查询命令都可以指定watcher,通过它来跟踪指定path的数据变化。一旦指定的数据发生变化(create,delete,modified,children_changed),服务器将会发送命令来回调注册的watcher. Watcher详细的讲解将在Zookeeper的Watcher中单独讲解。
Zookeeper修改命令
Zookeeper修改命令主要是用来修改节点数据或结构,或者权限信息。任何修改命令都需要提交到leader进行协调,协调完成后才返回。修改命令主要包括:
1. createSession:请求server创建一个session
2. create:创建一个节点
3. delete:删除一个节点
4. setData:修改一个节点的数据
5. setACL:修改一个节点的ACL
6. closeSession:请求server关闭session
我们根据前面的通信图知道,任何修改命令都需要leader协调。在leader的协调过程中,需要3次leader与Follower之间的来回请求响应。并且在此过程中还会涉及事务日志的记录,更糟糕的情况是还有take snapshot的操作。因此此过程可能比较耗时。但Zookeeper的通信中最大特点是异步的,如果请求是连续不断的,Zookeeper的处理是集中处理逻辑,然后批量发送,批量的大小也是有控制的。如果请求量不大,则即刻发送。这样当负载很大时也能保证很大的吞吐量,时效性也在一定程度上进行了保证。
zookeeper server端的业务处理-processor链
Zookeeper通过链式的processor来处理业务请求,每个processor负责处理特定的功能。不同的Zookeeper角色的服务器processor链是不一样的,以下分别介绍standaloneZookeeper server, leader和Follower不同的processor链。
Zookeeper中的processor
AckRequestProcessor:当leader从向Follower发送proposal后,Follower将发送一个Ack响应,leader收到Ack响应后,将会调用这个Processor进行处理。它主要负责检查请求是否已经达到了多数Follower的确认,如果满足条件,则提交commitProcessor进行commit处理
CommitProcessor:从commited队列中处理已经由leader协调好并commit的请求或者从请求队列中取出那些无需leader协调的请求进行下一步处理。
FinalRequestProcessor:任何请求的处理都需要经过这个processor,这是请求处理的最后一个Processor,主要负责根据不同的请求包装不同的类型的响应包。当然Follower与leader之间协调后的请求由于没有client连接,将不需要发送响应(代码体现在if (request.cnxn == null) {return;})。
FollowerRequestProcessor:Follower processor链上的第一个,主要负责修改请求和同步请求发往leader协调。
PrepRequestProcessor:在leader和standalone server上作为第一Processor,主要作用对于所有的修改命令生成changelog。
ProposalRequestProcessor:leader用来将请求包装为proposal向Follower请求确认。
SendAckRequestProcessor:Follower用来向leader发送Ack响应的处理。
SyncRequestProcessor:负责将已经commit的事务写到事务日志以及take snapshot.
ToBeAppliedRequestProcessor:负责将tobeApplied队列的中request转移到下一个请求进行处理。
10.配置参数
参数 |
说明 |
clientPort |
客户端连接server的端口,即zk对外服务端口,一般设置为2181。 |
dataDir |
就 是把内存中的数据存储成快照文件snapshot的目录,同时myid也存储在这个目录下(myid中的内容为本机server服务的标识)。写快照不需 要单独的磁盘,而且是使用后台线程进行异步写数据到磁盘,因此不会对内存数据有影响。默认情况下,事务日志也会存储在这里。建议同时配置参数 dataLogDir,事务日志的写性能直接影响zk性能。 |
tickTime |
ZK 中的一个时间单元。ZK中所有时间都是以这个时间单元为基础,进行整数倍配置的。例如,session的最小超时时间是2*tickTime。默认 3000毫秒。这个单元时间不能设置过大或过小,过大会加大超时时间,也就加大了集群检测session失效时间;设置过小会导致session很容易超 时,并且会导致网络通讯负载较重(心跳时间缩短) |
dataLogDir |
事 务日志输出目录。尽量给事务日志的输出配置单独的磁盘或是挂载点,这将极大的提升ZK性能。 由于事务日志输出时是顺序且同步写到磁盘,只有从磁盘写完日志后才会触发follower和leader发回事务日志确认消息(zk事务采用两阶段提 交),因此需要单独磁盘避免随机读写和磁盘缓存导致事务日志写入较慢或存储在缓存中没有写入。 |
globalOutstandingLimit |
最 大请求堆积数。默认是1000。ZK运行的时候, 尽管server已经没有空闲来处理更多的客户端请求了,但是还是允许客户端将请求提交到服务器上来,以提高吞吐性能。当然,为了防止Server内存溢 出,这个请求堆积数还是需要限制下的。当有非常多的客户端并且请求都比较大时,可以减少这个值,不过这种情况很少。 (Java system property:zookeeper.globalOutstandingLimit) |
preAllocSize |
预 先开辟磁盘空间,用于后续写入事务日志。默认是64M,每个事务日志大小就是64M,这个默认大小是按snapCount为100000且每个事务信息为 512b来计算的。如果ZK的快照频率较大的话,建议适当减小这个参数。(Java system property:zookeeper.preAllocSize)。当事务日志文件不会增长得太大的话,这个大小是可以减小的。比如1000次事务会新 产生一个快照(参数为snapCount),新产生快照后会用新的事务日志文件,假设一个事务信息大小100b,那么事务日志预开辟的大小为100kb会 比较好。 |
snapCount |
每 进行snapCount次事务日志输出后,触发一次快照(snapshot), 此时,ZK会生成一个snapshot.*文件,同时创建一个新的事务日志文件log.*。默认是100000.(真正的代码实现中,会进行一定的随机数 处理,以避免所有服务器在同一时间进行快照而影响性能)(Java system property:zookeeper.snapCount)。在通过快照和事务日志恢复数据时,使用的时间为读取快照时间和读取在这个快照之后产生的事 务日志的时间,因此snapCount太大会导致读取事务日志的数量较多,snapCount较小会导致产生快照文件很多。 |
traceFile |
用于记录所有请求的log,一般调试过程中可以使用,但是生产环境不建议使用,会严重影响性能。(Java system property:requestTraceFile) |
maxClientCnxns |
单 个客户端与单台服务器之间的连接数的限制,是ip级别的,默认是60,如果设置为0,那么表明不作任何限制。请注意这个限制的使用范围,仅仅是单台客户端 机器与单台ZK服务器之间的连接数限制,不是针对指定客户端IP,也不是ZK集群的连接数限制,也不是单台ZK对所有客户端的连接数限制。指定客户端IP 的限制策略,这里有一个patch,可以尝试一下:http://rdc.taobao.com/team/jm/archives/1334(No Java system property) |
clientPortAddress |
对于多网卡的机器,可以为每个IP指定不同的监听端口。默认情况是所有IP都监听clientPort指定的端口。New in 3.3.0 |
minSessionTimeoutmaxSessionTimeout |
Session超时时间限制,如果客户端设置的超时时间不在这个范围,那么会被强制设置为最大或最小时间。默认的Session超时时间是在2 * tickTime ~ 20 * tickTime这个范围 New in 3.3.0 |
fsync.warningthresholdms |
事务日志输出时,如果调用fsync方法超过指定的超时时间,那么会在日志中输出警告信息。默认是1000ms。(Java system property: fsync.warningthresholdms) New in 3.3.4 |
autopurge.purgeInterval |
在 上文中已经提到,3.4.0及之后版本,ZK提供了自动清理事务日志和快照文件的功能,这个参数指定了清理频率,单位是小时,需要配置一个1或更大的整 数,默认是0,表示不开启自动清理功能,但可以运行bin/zkCleanup.sh来手动清理zk日志。(No Java system property) New in 3.4.0 |
autopurge.snapRetainCount |
这个参数和上面的参数搭配使用,这个参数指定了需要保留的文件数目。默认是保留3个。(No Java system property) New in 3.4.0 |
electionAlg |
在 之前的版本中, 这个参数配置是允许我们选择leader选举算法,但是由于在以后的版本中,只会留下一种“TCP-based version of fast leader election”算法,所以这个参数目前看来没有用了,这里也不详细展开说了。(No Java system property) |
initLimit |
Follower 在启动过程中,会从Leader同步所有最新数据,然后确定自己能够对外服务的起始状态。Leader允许Follower在initLimit时间内完 成这个工作。通常情况下,我们不用太在意这个参数的设置。如果ZK集群的数据量确实很大了,Follower在启动的时候,从Leader上同步数据的时 间也会相应变长,因此在这种情况下,有必要适当调大这个参数了。默认值为10,即10 * tickTime (No Java system property) |
syncLimit |
在 运行过程中,Leader负责与ZK集群中所有机器进行通信,例如通过一些心跳检测机制,来检测机器的存活状态。如果Leader发出心跳包在 syncLimit之后,还没有从Follower那里收到响应,那么就认为这个Follower已经不在线了。注意:不要把这个参数设置得过大,否则可 能会掩盖一些问题,设置大小依赖与网络延迟和吞吐情况。默认为5,即5 * tickTime (No Java system property) |
leaderServes |
默 认情况下,Leader是会接受客户端连接,并提供正常的读写服务。但是,如果你想让Leader专注于集群中机器的协调,那么可以将这个参数设置为 no,这样一来,会大大提高写操作的性能。默认为yes(Java system property: zookeeper.leaderServes)。 |
server.x=[hostname]:n:n |
这 里的x是一个数字,与myid文件中的id是一致的,用来标识这个zk server,大小为1-255。右边可以配置两个端口,第一个端口用于Follower和Leader之间的数据同步和其它通信,第二个端口用于 Leader选举过程中投票通信。Zk启动时,会读取myid中的值,从而得到server.x的配置为本机配置,并且也可以通过这个id找到和其他zk 通信的地址和端口。hostname为机器ip,第一个端口n为事务发送的通信端口,第二个n为leader选举的通信端口,默认为2888:3888。 (No Java system property) |
group.x=nnnnn[:nnnnn] weight.x=nnnnn |
对机器分组和权重设置,可以 参见这里(No Java system property) |
cnxTimeout |
Leader选举过程中,打开一次连接(选举的server互相通信建立连接)的超时时间,默认是5s。(Java system property: zookeeper.cnxTimeout) |
zookeeper.DigestAuthenticationProvider.superDigest |
ZK权限设置相关,具体参见《使用super身份对有权限的节点进行操作》 和 《ZooKeeper权限控制》 |
skipACL |
对所有客户端请求都不作ACL检查。如果之前节点上设置有权限限制,一旦服务器上打开这个开头,那么也将失效。(Java system property:zookeeper.skipACL) |
forceSync |
这个参数确定了是否需要在事务日志提交的时候调用FileChannel.force来保证数据完全同步到磁盘。(Java system property:zookeeper.forceSync) |
jute.maxbuffer |
每个节点最大数据量,是默认是1M。这个限制必须在server和client端都进行设置才会生效。(Java system property:jute.maxbuffer) |
server.x:hostname:n:n:observer |
配置observer,表示本机是一个观察者(观察者不参与事务和选举,但会转发更新请求给leader)。比如:server.4:localhost:2181:3181:observer |
peerType=observer |
结合上面一条配置,表示这个zookeeper为观察者 |
五、 Pig
1.安装部署
1.安装
(1)上传pig安装包到机器上,使用root用户登陆: tar -xvf pig-0.9.2.tar.gz
(2)将解压的pig移动并改名为/usr/local/pig
rm -rf /usr/local/pig
mv pig-0.9.2 /usr/local/pig
2.配置pig
(1)修改/usr/local/pig/bin/pig,加入
JAVA_HOME=/usr/local/jdk
HADOOP_HOME=/usr/local/hadoop
(2) 根据修改conf/pig.properties文件,在文件末尾加入
fs.default.name=hdfs://namenode_server_host:namenode_ipc_port mapred.job.tracker=jobtracker_server_ip:jobtracker_port
3.改变 /usr/local/pig的目录所有者为hadoop
chown -R hadoop:hadoop/usr/local/pig
2.pig
Pig是对处理超大型数据集的抽象层,在MapReduce中的框架中有map和reduce两个函数,如果你亲手弄一个MapReduce实现从编写代码,编译,部署,放在Hadoop上执行这个MapReduce程序还是耗费你一定的时间的,有了Pig这个东东以后不仅仅可以简化你对MapReduce的开发,而且还可以对不同的数据之间进行转换,例如:包含在连接内的一些转化在MapReduce中不太容易去实现。
Apache Pig的运行可以纯本地的,解压,敲个“bin/pig -x local”命令直接运行,非常简单,这就是传说中的local模式,但是人们往往不是这样使用,都是将Pig与hdfs/hadoop集群环境进行对接,我看说白了Apache的Pig最大的作用就是对mapreduce算法(框架)实现了一套shell脚本,类似我们通常熟悉的SQL语句,在Pig中称之为Pig Latin,在这套脚本中我们可以对加载出来的数据进行排序、过滤、求和、分组(group by)、关联(Joining),Pig也可以由用户自定义一些函数对数据集进行操作,也就是传说中的UDF(user-defined functions)。
经过Pig Latin的转换后变成了一道MapReduce的作业,通过MapReduce多个线程,进程或者独立系统并行执行处理的结果集进行分类和归纳。Map() 和 Reduce() 两个函数会并行运行,即使不是在同一的系统的同一时刻也在同时运行一套任务,当所有的处理都完成之后,结果将被排序,格式化,并且保存到一个文件。Pig利用MapReduce将计算分成两个阶段,第一个阶段分解成为小块并且分布到每一个存储数据的节点上进行执行,对计算的压力进行分散,第二个阶段聚合第一个阶段执行的这些结果,这样可以达到非常高的吞吐量,通过不多的代码和工作量就能够驱动上千台机器并行计算,充分的利用计算机的资源,打消运行中的瓶颈。
所以用Pig可以对TB级别海量的数据进行查询非常轻松,并且这些海量的数据都是非结构化的数据,例如:一堆文件可能是log4j输出日志存又放于跨越多个计算机的多个磁盘上,用来记录上千台在线服务器的健康状态日志,交易日至,IP访问记录,应用服务日志等等。我们通常需要统计或者抽取这些记录,或者查询异常记录,对这些记录形成一些报表,将数据转化为有价值的信息,这样的话查询会较为复杂,此时类似MySQL这样的产品就并非能满足我们的对速度、执行效率上的需求,而用Apache的Pig就可以帮助我们去实现这样的目标。
反之,你如果在做实验的时候,把MySQL中的100行数据转换成文本文件放在在pig中进行查询,会让你非常失望,为何这短短的100行数据查询的效率极低,呵呵,因为中间有一个生成MapReduce作业的过程,这是无法避免的开销,所以小量的数据查询是不适合pig做的,就好比用关二哥的大刀切青菜一样。另外,还可以利用Pig的API在Java环境中调用,对Apache的Pig以上内容请允许我这样片面的理解,谢谢。
基本架构:
1)Pig自己实现的一套框架对输入、输出的人机交互部分的实现,就是Pig Latin 。
2)Zebra是Pig与HDFS/Hadoop的中间层、Zebra是MapReduce作业编写的客户端,Zerbra用结构化的语言实现了对hadoop物理存储元数据的管理也是对Hadoop的数据抽象层,Zebra中有2个核心的类TableStore(写)/TableLoad(读)对Hadoop上的数据进行操作。
3)Pig中的Streaming主要分为4个组件: 1. PigLatin 2. 逻辑层(Logical Layer) 3. 物理层(Physical Layer) 4. Streaming具体实现(Implementation),Streaming会创建一个Map/Reduce作业,并把它发送给合适的集群,同时监视这个作业的在集群环境中的整个执行过程。
4)MapReduce在每台机器上进行分布式计算的框架(算法)。
5)HDFS最终存储数据的部分。
与Hive对比
Language:在Hive中可以执行“插入/删除”等操作,但是Pig中我没有发现有可以“插入”数据的方法。
Schemas:Hive中至少还有一个“表”的概念,但是Pig中我认为是基本没有表的概念,所谓的表建立在Pig Latin脚本中,对与Pig更不要提metadata了。
Partitions:Pig中没有表的概念,所以说到分区对于Pig来说基本免谈,如果跟Hive说“分区”(Partition)他还是能明白的。
Server:Hive可以依托于Thrift启动一个服务器,提供远程调用。Pig没有发现有这样的功能。
Shell:在Pig 你可以执行一些个 ls 、cat 这样很经典、很cool的命令,但是在使用Hive的时候我压根就没有想过有这样的需求。
Web Interface:Hive有,Pig无
JDBC/ODBC:Pig无,Hive有
Pig的应用场景
数据查询只面向相关技术人员
即时性的数据处理需求,这样可以通过pig很快写一个脚本开始运行处理,而不需要创建表等相关的事先准备工作。
Pig包括:
Pig Latin, 类SQL数据处理语言
在Hadoop上运行的Pig Latin执行引擎,将pig脚本转换为map-reduce程序在hadoop集群运行
Pig的优点:
编码简单
对常见的操作充分优化
可扩展。自定义UDF
Pig使用:
1启动/运行
分为2台服务器,一台作为pig的服务器,一台作为hdfs的服务器。
首先需要在pig的服务器上进行配置,将pig的配置文件指向hdfs服务器,修改pig/conf目录下的
vim /work/pig/conf/pig.properties
添加以下内容:
fs.default.name=hdfs://192.168.1.201:9000/ #指向HDFS服务器
mapred.job.tracker=192.168.1.201:9001 #指向MR job服务器地址
如果是第一次运行请在Hadoop的HDFS的服务器上创建root目录,并且将etc目录下的passwd文件放在HDFS的root目录下,请执行以下两条命令。
hadoop fs -mkdir /user/root
hadoop fs -put /etc/passwd/user/root/passwd
创建运行脚本,用vim命令在pig的服务器上创建javabloger_testscript.pig 文件,内容如下:
LoadFile = load 'passwd' using PigStorage(':'); |
3.pig应该注意/避免的操作或事项
(1)CROSS操作
由于求交叉积可能会导致结果数据量暴增,因此,CROSS操作是一个“昂贵”的操作,可能会耗费Hadoop集群较多的资源,使用的时候需要评估一下数据量的大小。
(2)JOIN操作的顺序
如教程《Apache Pig中文教程(进阶)》中的第(6)条所写,当JOIN的各数据集分布严重不均时,你最好考虑一下JOIN的顺序,可以对你的job效率提升有帮助。
(3)FLATTEN一个空的bag将得不会输出任何结果
FLATTEN操作本来会将嵌套展开,生成更多行的结果,但如果被展开的bag是空的,则一行记录也不会生成,这与你想像的可能有点不同:至少也应该生成一行结果吧?不会的,就是一行也不会生成。
在这里我用一个实例来说明。假设有数据文件 1.txt:
[root@localhost ~]$ cat 1.txt 1 2 {(a,b),(c,d)} 77 88 {(p,q),(r,s)} 123 555 {(u,w),(q,t)} |
有三列,它们之间是以TAB分隔的。
以下代码:
A = LOAD '1.txt' AS (col1: int, col2: int, col3: bag{t: (first: chararray, second: chararray)}); B = FOREACH A GENERATE col1, col2, FLATTEN(col3); DUMP B; |
得到的结果是显而易见的:
(1,2,a,b)
(1,2,c,d)
(77,88,p,q)
(77,88,r,s)
(123,555,u,w)
(123,555,q,t)
可见记录被解嵌套了。
但是,如果第三列的bag是空的:
[root@localhost ~]$ cat 1.txt 1 2 {} 77 88 {} 123 555 {} |
那么,与上面同样的代码将什么也不会输出(你可以自己试验一下)。
这一点需要特别注意,所以一般来说,你在FLATTEN一个bag之前,需要判断一下它是否是空的(IsEmpty),如果你需要在FLATTEN的结果中标记空的那些bag,那么你就需要自己在FLATTEN之前将空的bag替换为自己指定的内容。
(4)SAMPLE的结果数量是不确定的
SAMPLE操作符可以对一个关系(relation)进行取样,得到其一定百分比的数据(例如随机取其中10%的数据),但是,这并不保证对同一个relation进行同样比例的SAMPLE,得到的tuple的数量就是相同的。例如,对一个有千万行的数据集,SAMPLE 0.1的结果,可能第一次会得到100万行,重做一次却得到了101万行(这里只是举一个例子,具体的数字是未知的)。
我的试验结果可以肯定地告诉大家:我拿一个含上亿条记录的数据集SAMPLE 0.1两次的结果相差了4万多条记录(指的是数据条数)。
(5)输出几个简单数据的job,没必要单独跑,使用UNION整合在一个Pig脚本中即可
假设有几个Pig job,它们输出的都是一行数据(当然,一行可能有多列),那么没必要单独跑几个job再得到所有结果,你可以用UNION把它们整合放在一个Pig脚本中,例如:
A1 = LOAD '1.txt' AS (col: chararray); A2 = LOAD 'a.txt' AS (col: chararray); A2 = LOAD 'P.txt' AS (col: chararray); B1 = GROUP A1 ALL; C1 = FOREACH B1 GENERATE COUNT(A1); B2 = GROUP A2 ALL; C2 = FOREACH B2 GENERATE COUNT(A2); B3 = GROUP A3 ALL; C3 = FOREACH B3 GENERATE COUNT(A3); U = UNION C1, C2, C3; STORE U INTO 'res'; |
有人说,为什么不把A1,A2,A3使用通配符一起加载?答:这里我假设了一种非常简单的情况:三个数据文件都只有一列,而实际中,可能三个文件完全有不同的格式,而且后面的处理针对每个job也是不同的(在这里为简单起见才写成相同的),因此,单独加载有时候是必要的。
这样做之后,会输出3个文件,每个文件中有一个数。比跑3个Pig job方便。
(6)用ORDER排序时,Pig并不遵守“相同的key的记录会被发送到同一个partition”的惯例
处理海量数据时,我们常常会遇到这样一种情况:某些key的数据远远多于其他key的数据。例如,我们要分析用户的web访问日志,会发现用户访问Google的次数远远多于其他网站的次数。
所以,如果我们要按“访问的网站”这个字段来GROUP或者ORDER的话,就会造成落入某些reducer的数据远远多于其他reducer(GROUP、ORDER都会触发reduce过程)——注意,这里说“某些reducer”,是因为在这个例子中,不仅访问Google是个大户,可能还有其他的网站访问大户。
由于这些reducer需要处理的数据量特别大,也就会导致所需的时间特别长,从而整个job所需的总时间特别长。为了解决这一弊端,Pig使用了一种聪明的方法:先对需要ORDER的数据进行采样,获知其key分布情况,然后根据此分布构造一个可以均衡全体数据的partitioner,从而将数据比较均匀地送到N个reducer上。Pig的这种算法是很有效的,它使得各个reducer之间的执行时间相差不大。
正因为Pig做了这样的工作,所以,前面例子中所说的对Google的访问记录可能会被送到多个reducer中,它们有相同的key,却没有被送到同一reducer中,这没有遵守MapReduce的惯例。如果你的数据处理流程需要遵守此惯例,那么就不能用Pig的ORDER来排序。
同时,正因为Pig在ORDER时需要对输入数据进行采样,所以,ORDER的时候Pig会为你的数据流程添加一个额外的轻量job来完成采样工作——从Pig在控制台输出的信息中,你可以看到一个Feature为“SAMPLER”的job,这个job就是采样用的。
(7)关系操作符(Relational Operator)只能对关系(relation)进行操作,而不能对表达式(expression)进行操作吗?
有人说,从这一句的陈述来看,它根本就是废话——关系操作符当然是操作关系的啊!
不过,答案是:不一定。例如,DISTINCT 是一个关系操作符,但是它却可以对表达式进行操作!
我拿一个例子来说明这个问题。有以下数据文件:
[root@localhost ~]$ cat 1.txt
1 2 3
2 5 3
2 6 7
8 6 3
1 5 7
有以下Pig代码(没什么计算上的意义,就是为了演示用):
A = LOAD '1.txt' AS (col1: int, col2: int, col3: int); B = GROUP A BY col3; C = FOREACH B { E = DISTINCT A.col3; GENERATE group, COUNT(E); }; DUMP C; |
完全可以成功执行,结果为:
(3,1)
(7,1)
从这段代码能看出什么?首先,A.col3 是一个表达式(expression),而不是一个关系(relation),但是,DISTINCT 这个操作符却应用在了它上面,这说明,关系操作符完全是有可能应用在表达式上的。
某些书/资料上有一种说法是,上面的代码是有语法错误的,你必须用以下的代码来替代:
A = LOAD '1.txt' AS (col1: int, col2: int, col3: int); B = GROUP A BY col3; C = FOREACH B { D = A.col3; E = DISTINCT D; GENERATE group, COUNT(E); }; DUMP C; |
这段代码确实没有错误,它先通过 A.col3 这个表达式,生成了一个关系(relation) D,再将 DISTINCT 关系操作符应用于其上,这样就避开了所谓的“关系操作符只能操作关系”的限制。但事实上,我们完全没有必要这样做,正如上面的代码的试验结果,你完全可以直接用 DISTINCT A.col3,并不会出错。
所以说,书本也有坑爹的时候,不可全信。
(8)嵌套的 FOREACH 中的代码是串行执行的
嵌套的 FOREACH 语句可以完成复杂的操作,例如在嵌套的 FOREACH 中对bag进行排序等。FOREACH会被并行执行,但是对嵌套在其中的子语句来说,它们却是串行执行的。
(9)PARALLEL只对强制产生reduce过程的操作符有效
通过PARALLEL,你可以控制Pig程序的并行度(说白了就是控制reducer的数量)。PARALLEL可以附加在任何关系操作符上,但是它只对reduce端的并行度起控制作用,因为MapReduce不允许用户自己控制map端的并行度,它只允许用户控制reduce端的并行度,所以,这就是PARALLEL只对强制产生reduce过程的操作符有效的原因了。
另外,在本地模式(pig -x local)下,PARALLEL是不起任何作用的(被忽略),因为在本地模式下所有操作都是串行执行的。
4.pig输出压缩格式的SequenceFile
SequenceFile是Hadoop API提供的一种二进制文件,它将数据以
如果你要用Apache Pig读取这种类型的数据,可以用 PiggyBank 中的SequenceFileLoader——我没有用过,但肯定是没问题的。
但是,如果你保存在SequenceFile中的key或value是ThriftWritable类型的数据,那么,要用Pig来 load & store 这种数据,就不那么容易了。
幸好我们有Twitter,它已经帮我们做好了这个工作。利用其开源的 Elephant Bird,我们可以轻松做到这一点。
Elephant Bird 中的SequenceFileLoader 以及 SequenceFileStorage 就是用来干这个的。
例如,load数据的做法是:
A = LOAD 'xxx' USING com.twitter.elephantbird.pig.load.SequenceFileLoader( '-c com.mediav.elephantbird.pig.util.BooleanWritableConverter', '-c com.twitter.elephantbird.pig.util.ThriftWritableConverter com.codelast.MyThriftClass'); |
其中,这份SequenceFile的key是BooleanWritable类型,value是ThriftWritable类型,它对应的Thrift类是MyThriftClass,这是一个自定义的Thrift class。
文章来源:http://www.codelast.com/
store 数据的做法是:
STORE B INTO 'xxx' USING com.twitter.elephantbird.pig.store.SequenceFileStorage( '-c com.mediav.elephantbird.pig.util.BooleanWritableConverter', '-c com.twitter.elephantbird.pig.util.ThriftWritableConverter com.codelast.MyThriftClass'); |
其中,对key和value的说明和上面一样。
这样,就可以实现加载以及存储SequenceFile了。
但是你会发现,这样输出的SequenceFile是未压缩的,所以文件体积比较大。如果要压缩,该怎么做呢?
答案就是在Pig脚本中添加以下几句话就OK了:
SET output.compression.enabled 'true'; SET mapreduce.output.fileoutputformat.compress.type 'BLOCK'; SET output.compression.codec 'org.apache.hadoop.io.compress.DefaultCodec'; |
这会使得输出的SequenceFile是BLOCK压缩类型,默认压缩编码的文件。
5.操作
pig简单操作
1.从文件导入数据
1)Mysql (Mysql需要先创建表).
CREATE TABLE TMP_TABLE(USER VARCHAR(32),AGE INT,IS_MALE BOOLEAN);
CREATE TABLE TMP_TABLE_2(AGEINT,OPTIONS VARCHAR(50)); -- 用于Join
LOAD DATA LOCAL INFILE '/tmp/data_file_1' INTO TABLE TMP_TABLE ;
LOAD DATA LOCAL INFILE '/tmp/data_file_2' INTO TABLE TMP_TABLE_2;
2)Pig
tmp_table = LOAD '/tmp/data_file_1'USING PigStorage('\t') AS (user:chararray, age:int,is_male:int);
tmp_table_2= LOAD'/tmp/data_file_2' USING PigStorage('\t') AS (age:int,options:chararray);
2.查询整张表
1)Mysql
SELECT * FROM TMP_TABLE;
2)Pig
DUMP tmp_table;
3. 查询前50行
1)Mysql
SELECT * FROM TMP_TABLE LIMIT 50;
2)Pig
tmp_table_limit = LIMIT tmp_table 50;
DUMP tmp_table_limit;
4.查询某些列
1)Mysql
SELECT USER FROM TMP_TABLE;
2)Pig
tmp_table_user = FOREACH tmp_table GENERATE user;
DUMP tmp_table_user;
5. 给列取别名
1)Mysql
SELECT USER AS USER_NAME,AGE AS USER_AGE FROM TMP_TABLE;
2)Pig
tmp_table_column_alias = FOREACH tmp_table GENERATE user ASuser_name,age AS user_age;
DUMP tmp_table_column_alias;
6.排序
1)Mysql
SELECT * FROM TMP_TABLE ORDER BY AGE;
2)Pig
tmp_table_order = ORDER tmp_table BY age ASC;
DUMP tmp_table_order;
7.条件查询
1)Mysql
SELECT * FROM TMP_TABLE WHERE AGE>20;
2) Pig
tmp_table_where = FILTER tmp_table by age > 20;
DUMP tmp_table_where;
8.内连接Inner Join
1)Mysql
SELECT * FROM TMP_TABLE A JOIN TMP_TABLE_2 B ON A.AGE=B.AGE;
2)Pig
tmp_table_inner_join = JOIN tmp_table BY age,tmp_table_2 BY age;
DUMP tmp_table_inner_join;
9.左连接Left Join
1)Mysql
SELECT * FROM TMP_TABLE A LEFT JOIN TMP_TABLE_2 B ON A.AGE=B.AGE;
2)Pig
tmp_table_left_join = JOIN tmp_table BY age LEFT OUTER,tmp_table_2 BYage;
DUMP tmp_table_left_join;
10.右连接Right Join
1)Mysql
SELECT * FROM TMP_TABLE A RIGHT JOIN TMP_TABLE_2 B ON A.AGE=B.AGE;
2)Pig
tmp_table_right_join = JOIN tmp_table BY age RIGHT OUTER,tmp_table_2 BYage;
DUMP tmp_table_right_join;
11.全连接Full Join
1)Mysql
SELECT * FROM TMP_TABLE A JOINTMP_TABLE_2 B ON A.AGE=B.AGE
UNION SELECT * FROM TMP_TABLE ALEFT JOIN TMP_TABLE_2 B ON A.AGE=B.AGE
UNION SELECT * FROM TMP_TABLE ARIGHT JOIN TMP_TABLE_2 B ON A.AGE=B.AGE;
2)Pig
tmp_table_full_join = JOIN tmp_table BY age FULL OUTER,tmp_table_2 BYage;
DUMP tmp_table_full_join;
12.同时对多张表交叉查询
1)Mysql
SELECT * FROM TMP_TABLE,TMP_TABLE_2;
2)Pig
tmp_table_cross = CROSS tmp_table,tmp_table_2;
DUMP tmp_table_cross;
13.分组GROUP BY
1)Mysql
SELECT * FROM TMP_TABLE GROUP BY IS_MALE;
2)Pig
tmp_table_group = GROUP tmp_table BY is_male;
DUMP tmp_table_group;
14.分组并统计
1)Mysql
SELECT IS_MALE,COUNT(*) FROM TMP_TABLE GROUP BY IS_MALE;
2)Pig
tmp_table_group_count = GROUPtmp_table BY is_male;
tmp_table_group_count = FOREACHtmp_table_group_count GENERATE group,COUNT($1);
DUMP tmp_table_group_count;
15.查询去重DISTINCT
1)MYSQL
SELECT DISTINCT IS_MALE FROMTMP_TABLE;
2)Pig
tmp_table_distinct = FOREACHtmp_table GENERATE is_male;
tmp_table_distinct = DISTINCTtmp_table_distinct;
DUMP tmp_table_distinct;
上面简单操作,下面需要进一步了解pig支持的内容:
pig支持数据类型
double > float > long > int > bytearray
tuple|bag|map|chararray > bytearray
double float long int chararray bytearray都相当于pig的基本类型
tuple相当于数组,但是可以类型不一,举例('dirkzhang','dallas',41)
Bag相当于tuple的一个集合,举例{('dirk',41),('kedde',2),('terre',31)},在group的时候会生成bag
Map相当于哈希表,key为chararray,value为任意类型,例如['name'#dirk,'age'#36,'num'#41
nulls 表示的不只是数据不存在,他更表示数据是unkown
pig latin语法
1:load
LOAD 'data' [USING function] [AS schema];
例如: load = LOAD 'sql://{SELECTMONTH_ID,DAY_ID,PROV_ID FROM zb_d_bidwmb05009_010}' USINGcom.xxxx.dataplatform.bbdp.geniuspig.VerticaLoader('oracle','192.168.6.5','dev','1522','vbap','vbap','1')AS (MONTH_ID:chararray,DAY_ID:chararray,PROV_ID:chararray);
Table = load ‘url’ as (id,name…..); //table和load之间除了等号外 还必须有个空格 不然会出错,url一定要带引号,且只能是单引号。
2:filter
alias = FILTER alias BY expression;
Table = filter Table1 by + A; //A可以是 id >10;not name matches ‘’,is not null 等,以用and 和or连接各条件
例如:
filter = filter load20 by ( MONTH_ID == '1210'and DAY_ID == '18' and PROV_ID == '010' );
3:group
alias = GROUP alias { ALL | BY expression} [, alias ALL | BY expression …][USING 'collected' | 'merge'] [PARTITION BY partitioner] [PARALLEL n];
pig的分组,不仅是数据上的分组,在数据的schema形式上也进行分组为groupcolumn:bag
Table3 = group Table2 by id;也可以Table3 =group Table2 by (id,name);括号必须加
可以使用ALL实现对所有字段的分组
4:foreach
alias = FOREACH alias GENERATE expression [AS schema] [expression [ASschema]….];
alias = FOREACH nested_alias {
alias = {nested_op | nested_exp}; [{alias = {nested_op | nested_exp}; …]
GENERATE expression [AS schema] [expression [AS schema]….]
};
一般跟generate一块使用
Table = foreach Table generate (id,name);括号可加可不加。
avg = foreach Table generate group, AVG(age); MAX ,MIN..
在进行数据过滤时,建议尽早使用foreach generate将多余的数据过滤掉,减少数据交换
5:join
Inner join Syntax
alias = JOIN alias BY {expression|'('expression [, expression …]')'} (, aliasBY {expression|'('expression [, expression …]')'} …) [USING 'replicated' |'skewed' | 'merge' | 'merge-sparse'] [PARTITION BY partitioner] [PARALLEL n];
Outer join Syntax
alias = JOIN left-alias BY left-alias-column [LEFT|RIGHT|FULL] [OUTER],right-alias BY right-alias-column [USING 'replicated' | 'skewed' | 'merge'][PARTITION BY partitioner] [PARALLEL n]; join/left join/ right join
daily = load 'A' as (id,name, sex);
divs = load 'B' as (id,name, sex);
join
jnd = join daily by (id, name), divs by (id, name);
left join
jnd = join daily by (id, name) left outer, divs by (id, name);
也可以同时多个变量,但只用于inner join
A = load 'input1' as (x, y);
B = load 'input2' as (u, v);
C = load 'input3' as (e, f);
alpha = join A by x, B by u, C by e;
6: union
alias = UNION [ONSCHEMA] alias, alias [, alias …];
union 相当与sql中的union,但与sql不通的是pig中的union可以针对两个不同模式的变量:如果两个变量模式相同,那么union后的变量模式与变量的模式一样;如果一个变量的模式可以由另一各变量的模式强制类型转换,那么union后的变量模式与转换后的变量模式相同;否则,union后的变量没有模式。
A = load 'input1' as (x:int, y:float);
B = load 'input2' as (x:int, y:float);
C = union A, B;
describe C;
C: {x: int,y: float}
A = load 'input1' as (x:double, y:float);
B = load 'input2' as (x:int, y:double);
C = union A, B;
describe C;
C: {x: double,y: double}
A = load 'input1' as (x:int, y:float);
B = load 'input2' as (x:int, y:chararray);
C = union A, B;
describe C;
Schema for C unknown.
注意:在pig 1.0中执行不了最后一种union。
如果需要对两个具有不通列名的变量union的话,可以使用onschema关键字
A = load 'input1' as (w: chararray, x:int, y:float);
B = load 'input2' as (x:int, y:double, z:chararray);
C = union onschema A, B;
describe C;
C: {w: chararray,x: int,y: double,z: chararray}
join和union之后alias的别名会变
7:Dump
dump alias
用于在屏幕上显示数据。
8:Order by
alias = ORDER alias BY { * [ASC|DESC] | field_alias [ASC|DESC] [, field_alias[ASC|DESC] …] } [PARALLEL n];
A = order Table by id desc;
9:distinct
A = distinct alias;
10:limit
A = limit alias 10;
11:sample
SAMPLE alias size;
随机抽取指定比例(0到1)的数据。
some = sample divs 0.1;
13:cross
alias = CROSS alias, alias [, alias …] [PARTITION BY partitioner] [PARALLEL n];
将多个数据集中的数据按照字段名进行同值组合,形成笛卡尔积。
--cross.pig
daily = load 'NYSE_daily' as (exchange:chararray,symbol:chararray,date:chararray, open:float, high:float, low:float,
close:float, volume:int, adj_close:float);
divs = load 'NYSE_dividends' as (exchange:chararray,symbol:chararray,date:chararray, dividends:float);
tonsodata = cross daily, divs parallel 10;
15:split
Syntax
SPLIT alias INTO alias IF expression, alias IF expression [, alias IFexpression …] [, alias OTHERWISE];
A = LOAD 'data' AS (f1:int,f2:int,f3:int);
DUMP A;
(1,2,3)
(4,5,6)
(7,8,9)
SPLIT A INTO X IF f1<7, Y IF f2==5, Z IF (f3<6 OR f3>6);
DUMP X;
(1,2,3)
(4,5,6)
DUMP Y;
(4,5,6)
DUMP Z;
(1,2,3)
(7,8,9)
16:store
Store … into … Using…
pig在别名维护上:
1、join
如e = join d by name,b by name;
g = foreach e generate $0 as one:chararray, $1 as two:int, $2as three:chararray,$3 asfour:int;
他生成的schemal:
e: {d::name: chararray,d::position:int,b::name: chararray,b::age: int}
g: {one: chararray,two: int,three: chararray,four: int}
2、group
B = GROUP A BY age;
----------------------------------------------------------------------
| B | group: int | A: bag({name: chararray,age:int,gpa: float}) |
----------------------------------------------------------------------
| | 18 |{(John, 18, 4.0), (Joe, 18, 3.8)} |
| | 20 | {(Bill,20, 3.9)} |
----------------------------------------------------------------------
(18,{(John,18,4.0F),(Joe,18,3.8F)})
pig udf自定义
pig支持嵌入user defined function,一个简单的udf 继承于evalFunc,通常用在filter,foreach中
6.优化
1. 尽早去除无用的数据
MapReduce Job的很大一部分开销在于磁盘IO和数据的网络传输,如果能尽早的去除无用的数据,减少数据量,会提升Pig的性能。
1). 尽早的使用Filter
使用Filter可以去除数据中无用的行(Record),尽早的Filter掉无用的数据,可以减少数据量,提升Pig性能。
2).尽早的使用Project(Foreach Generate)
使用ForeachGenerate可以去除数据中无用的列(Column),减少数据量,提升Pig性能。
2. 使用Combiner
Combiner可以对Map的结果进行combine,减少Shuffle的数据量。
在Pig中实现UDF时,应该尽可能地实现Algebraic接口(实现Algebraic接口的function可以对中间结果执行多次而不影响最终结果,比如Count, Sum等都是Algebraic function),这样的UDF就可能进行Combine,条件如下
如果在Group之后的Foreach语句中,所有投影都是针对分组(Group)的列的表达式,或者是Algebraic UDF的表达式时,就可以使用Combiner(表达式包括Sum,Count,Distinct或者其他数学表达式等)。
3. Join优化
当进行Join时,最后一个表不会放入内存,而是以stream的方式进行处理,所以最好把最大的一个表放置到Join语句的最后
Pig实现了以下三种定制的Join以进一步优化。
1) Replicated Join
当进行Join的一个表比较大,而其他的表都很小(能够放入内存)时,Replicated Join会非常高效。
Replicated Join会把所有的小表放置在内存当中,然后在Map中读取大表中的数据记录,和内存中存储的小表的数据进行Join,得到Join结果,无需Reduce。
可以在Join时使用 Using'replicated'语句来触发Replicated Join,大表放置在最左端,其余小表(可以有多个)放置在右端。
2)Skewed Join
当进行Join的两个表中,一个表数据记录针对key的分布极其不均衡的时候,简单的使用Hash来分配Reduce端的key时,可能导致某些Reducer上的数据量特别大,降低整个集群的性能。
Skewed Join可以首先对左边的表的key统计其分布,然后决定Reduce端的key的分布,尽量使得Reduce端的数据分布比较均衡。
可以在Join时使用Using'skewed'语句来触发Skewed Join,需要进行统计的表(亦即key可能分布不均衡的表)放置在左端。
3)Merge Join
当进行Join的两个表都已经是有序的时,可以使用Merge Join。
Join时,首先对右端的表进行一次采样,对采样的数据创建索引,记录(key, 文件名, 偏移[offset])。然后进行map,读取Join左边的表,对于每一条数据记录,根据前一步计算好的索引来查找数据,进行Join。
可以在Join时使用Using'merge'语句来触发Merge Join,需要创建索引的表放置在右端。
另外,在进行Join之前,首先过滤掉key为Null的数据记录可以减少Join的数据量。
4. 使用压缩来提高性能
通过压缩Map/Reduce之间的数据,以及Job之间需要传输的数据,可以显著的减少需要存储在硬盘上的和需要传输的数据,提升Pig的性能。
1) 压缩Map/Reduce之间的数据
通过设置mapred.compress.map.output= true可以对Map的结果进行压缩,压缩的方法可以通过下面的语句来进行设置:mapred.map.output.compression.codec =org.apache.hadoop.io.compress.GzipCodec / com.hadoop.compression.lzo.LzopCodec。
Gzip的压缩效率比较高,但是比较消耗CPU,所以通常情况下可以使用Lzo来进行压缩。
2) 压缩Job之间的数据
通过设置pig.tmpfilecompression= true可以对Job之间的数据进行压缩,压缩的方法可以通过pig.tmpfilecompres sion.codec =org.apache.hadoop.io.compress.GzipCodec / com.hadoop.compression.lzo.LzopCodec来进行设置。
5. 设置Reduce的并发数
可以通过PARALLEL = n 来设置Reduce的并发数(Map的并发数不可以设置),可以启动Reduce的操作包括:
COGROUP, CROSS, DISTINCT, GROUP, JOIN (inner), JOIN (outer), 和 ORDER BY。
需要注意的是,PARALLEL并不是越大越好,这需要根据集群的配置来确定,比较合理的PARALLEL数 = 集群节点数*mapred.tasktracker.reduce.tasks.maximum。后者默认为2。
六、 Sqoop
1. 安装
1 下载包
wgethttp://mirrors.ustc.edu.cn/apache/sqoop/1.4.4/sqoop-1.4.4.bin__hadoop-1.0.0.tar.gz
mv sqoop-1.4.4.bin__hadoop-1.0.0sqoop-1.4.4
2 配置环境变量
export HADOOP_COMMON_HOME=/home/ hadoop /hadoop-1.2.1
export HADOOP_MAPRED_HOME=/home/ hadoop /hadoop-1.2.1
export PATH=$PATH:/home/hadoop/sqoop-1.4.4/bin
export HBASE_HOME=/home/ hadoop /hbase-0.94.12
source /etc/profile
root@host001:/home/szy/sqoop-1.4.4/bin#sqoop help
复制hadoop-core-0.20.2-CDH3B4.jar到sqoop的lib下
将JDBC驱动mysql-connector-java-5.1.18.jar拷贝到/home/szy/sqoop-1.4.4/lib
修改configure-sqoop
注释掉hbase zookeeper检查:
#if [ ! -d "${HBASE_HOME}" ];then
# echo “Error: $HBASE_HOME does not exist!”
# echo ‘Please set $HBASE_HOME to the rootof your HBase installation.’
# exit 1
#fi
#if [ ! -d "${ZOOKEEPER_HOME}" ];then
# echo “Error: $ZOOKEEPER_HOME does not exist!”
# echo ‘Please set $ZOOKEEPER_HOME to theroot of your ZooKeeper installation.’
# exit 1
#fi
修改配置文件sqoop-env.sh
#Set path to where bin/hadoop is available export HADOOP_COMMON_HOME=/usr/local/hadoop/ #Set path to where hadoop-*-core.jar is available export HADOOP_MAPRED_HOME=/usr/local/hadoop #set the path to where bin/hbase is available export HBASE_HOME=/usr/local/hbase #Set the path to where bin/hive is available export HIVE_HOME=/usr/local/hive #Set the path for where zookeper config dir is export ZOOCFGDIR=/usr/local/zk |
这样基本就算成功了下面你可以执行一下命令测试一下就好
sqoop list-databases --connectjdbc:mysql://host001 --username root --password szy
sqoop list-tables --connect jdbc:mysql://host001/mysql--username root --password szy
sqoop import --connectjdbc:mysql://host001/test --username root --password szy --table person
sqoop import --connectjdbc:mysql://host001/test --username root --password szy --table person -m 1
sqoop import --connectjdbc:mysql://host001/test --username root --password szy --table person--direct -m 1
sqoop import-all-tables --connectjdbc:mysql://host001/test --username root --password szy --direct -m 1
sqoop export --connectjdbc:mysql://host001/test --username root --password szy --table person--export-dir person
sqoop export --connectjdbc:mysql://host001/test --username root --password szy --table animal--export-dir animal
Sqoop2:
wgethttp://mirror.bit.edu.cn/apache/sqoop/1.99.3/sqoop-1.99.3-bin-hadoop100.tar.gz
tar -xzvf sqoop-1.99.3-bin-hadoop100.tar.gz
mv sqoop-1.99.3-bin-hadoop100 sqoop-1.99.3
cd sqoop-1.99.3
sudo apt-get install zip
bin/addtowar.sh -hadoop-version 1.2.1-hadoop-path /home/szy/hadoop-1.2.1
bin/addtowar.sh -jars /home/szy/mysql-connector-java-5.1.18.jar
vi server/conf/sqoop.properties
修改org.apache.sqoop.submission.engine.mapreduce.configuration.directory=/etc/hadoop/conf/为
org.apache.sqoop.submission.engine.mapreduce.configuration.directory=/home/szy/hadoop-1.2.1/conf/
启动Sqoop 2 server:
bin/sqoop.sh server start
http://host001:12000/sqoop/
停止Sqoop 2 server:
bin/sqoop.sh server stop
客户端连接Sqoop 2 server:
客户端直接解压即可运行
MySQL准备数据库和表:
create database test;
create table history (userId int, commandvarchar(20));
insert into history values(1, 'ls');
insert into history values(1, 'dir');
insert into history values(2, 'cat');
insert into history values(5, 'vi');
交互模式:
bin/sqoop.sh client
sqoop:000> set server --host host001--port 12000 --webapp sqoop
sqoop:000> show version --all
sqoop:000> show connector --all
sqoop:000>create connection --cid 1
Name: mysql
JDBC Driver Class: com.mysql.jdbc.Driver
JDBC Connection String:
jdbc:mysql://host001:3306/test?useUnicode=true&characterEncoding=UTF-8&createDatabaseIfNotExist=true&autoReconnect=true
Username: root
Password: ***
entry#回车
Max connections:100
sqoop:000>create job --xid 1 --typeimport
Name:ImportHistory
Schema name:
Table name: history
Table SQL statement:
Table column names:
Partition column name:userId
Boundary query:
Choose:0
Choose: 0
Output directory: output-sqoop2-history
Extractors:
Loaders:
sqoop:000> submission start --jid 1
sqoop:000> submission status --jid 1
sqoop:000> submission stop --jid 1
批处理模式:
sqoop.sh client /home/szy/script.sqoop
vi /home/ysc/script.sqoop
输入:
#指定服务器信息
set server --host host001 --port 12000 --webapp sqoop
#执行JOB
submission start --jid 1
2. 结构
3. 工具
Sqoop可以在HDFS/Hive和关系型数据库之间进行数据的导入导出,其中主要使用了import和export这两个工具。
业务数据存放在关系数据库中,如果数据量达到一定规模后需要对其进行分析或同统计,单纯使用关系数据库可能会成为瓶颈,这时可以将数据从业务数据库数据导入(import)到Hadoop平台进行离线分析。
import和export工具有些通用的选项,如下表所示:
选项 |
含义说明 |
--connect |
指定JDBC连接字符串 |
--connection-manager |
指定要使用的连接管理器类 |
--driver |
指定要使用的JDBC驱动类 |
--hadoop-mapred-home |
指定$HADOOP_MAPRED_HOME路径 |
--help |
打印用法帮助信息 |
--password-file |
设置用于存放认证的密码信息文件的路径 |
-P |
从控制台读取输入的密码 |
--password |
设置认证密码 |
--username |
设置认证用户名 |
--verbose |
打印详细的运行信息 |
--connection-param-file |
可选,指定存储数据库连接参数的属性文件 |
数据导入工具import
import工具,是将HDFS平台外部的结构化存储系统中的数据导入到Hadoop平台,如下表所示:
选项 |
含义说明 |
--append |
将数据追加到HDFS上一个已存在的数据集上 |
--as-avrodatafile |
将数据导入到Avro数据文件 |
--as-sequencefile |
将数据导入到SequenceFile |
--as-textfile |
将数据导入到普通文本文件(默认) |
--boundary-query |
边界查询,用于创建分片(InputSplit) |
--columns |
从表中导出指定的一组列的数据 |
--delete-target-dir |
如果指定目录存在,则先删除掉 |
--direct |
使用直接导入模式(优化导入速度) |
--direct-split-size |
分割输入stream的字节大小(在直接导入模式下) |
--fetch-size |
从数据库中批量读取记录数 |
--inline-lob-limit |
设置内联的LOB对象的大小 |
-m,--num-mappers |
使用n个map任务并行导入数据 |
-e,--query |
导入的查询语句 |
--split-by |
指定按照哪个列去分割数据 |
--table |
导入的源表表名 |
--target-dir |
导入HDFS的目标路径 |
--warehouse-dir |
HDFS存放表的根路径 |
--where |
指定导出时所使用的查询条件 |
-z,--compress |
启用压缩 |
--compression-codec |
指定Hadoop的codec方式(默认gzip) |
--null-string |
果指定列为字符串类型,使用指定字符串替换值为null的该类列的值 |
--null-non-string |
如果指定列为非字符串类型,使用指定字符串替换值为null的该类列的值 |
将MySQL数据库中整个表数据导入到Hive表
bin/sqoopimport --connect jdbc:mysql://10.95.3.49:3306/workflow --table project--username shirdrn -P --hive-import ----default-character-set=utf-8
将MySQL数据库workflow中project表的数据导入到Hive表中。
将MySQL数据库中多表JION后的数据导入到HDFS
bin/sqoopimport --connect jdbc:mysql://10.95.3.49:3306/workflow --username shirdrn -P--query 'SELECT users.*, tags.tag FROM users JOIN tags ON (users.id =tags.user_id) WHERE $CONDITIONS' --split-by users.id --target-dir/hive/tag_db/user_tags ----default-character-set=utf-8
这里,使用了--query选项,不能同时与--table选项使用。而且,变量$CONDITIONS必须在WHERE语句之后,供Sqoop进程运行命令过程中使用。上面的--target-dir指向的其实就是Hive表存储的数据目录。
将MySQL数据库中某个表的数据增量同步到Hive表
bin/sqoop job--create your-sync-job -- import --connectjdbc:mysql://10.95.3.49:3306/workflow --table project --username shirdrn -P--hive-import --incremental append --check-column id --last-value 1 ----default-character-set=utf-8
这里,每次运行增量导入到Hive表之前,都要修改--last-value的值,否则Hive表中会出现重复记录。
将MySQL数据库中某个表的几个字段的数据导入到Hive表
bin/sqoopimport --connect jdbc:mysql://10.95.3.49:3306/workflow --username shirdrn --P--table tags --columns 'id,tag' --create-hive-table -target-dir/hive/tag_db/tags -m 1 --hive-table tags --hive-import ----default-character-set=utf-8
我们这里将MySQL数据库workflow中tags表的id和tag字段的值导入到Hive表tag_db.tags。其中--create-hive-table选项会自动创建Hive表,--hive-import选项会将选择的指定列的数据导入到Hive表。如果在Hive中通过SHOWTABLES无法看到导入的表,可以在conf/hive-site.xml中显式修改如下配置选项:
然后再重新运行,就能看到了。
使用验证配置选项
sqoop import--connect jdbc:mysql://db.foo.com/corp --table EMPLOYEES --validate --validatororg.apache.sqoop.validation.RowCountValidator --validation-thresholdorg.apache.sqoop.validation.AbsoluteValidationThreshold--validation-failurehandler org.apache.sqoop.validation.AbortOnFailureHandler
数据导出工具export
export工具,是将HDFS平台的数据,导出到外部的结构化存储系统中。如下表所示:
选项 |
含义说明 |
--validate |
启用数据副本验证功能,仅支持单表拷贝,可以指定验证使用的实现类 |
--validation-threshold |
指定验证门限所使用的类 |
--direct |
使用直接导出模式(优化速度) |
--export-dir |
导出过程中HDFS源路径 |
-m,--num-mappers |
使用n个map任务并行导出 |
--table |
导出的目的表名称 |
--call |
导出数据调用的指定存储过程名 |
--update-key |
更新参考的列名称,多个列名使用逗号分隔 |
--update-mode |
指定更新策略,包括:updateonly(默认)、allowinsert |
--input-null-string |
使用指定字符串,替换字符串类型值为null的列 |
--input-null-non-string |
使用指定字符串,替换非字符串类型值为null的列 |
--staging-table |
在数据导出到数据库之前,数据临时存放的表名称 |
--clear-staging-table |
清除工作区中临时存放的数据 |
--batch |
使用批量模式导出 |
讲解如何将Hive中的数据导入到MySQL数据库。
首先,我们准备几个表,MySQL数据库为tag_db,里面有两个表,定义如下所示:
CREATE TABLE tag_db.users (
id INT(11) NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
PRIMARY KEY (`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE tag_db.tags (
id INT(11) NOT NULL AUTO_INCREMENT,
user_id INT NOT NULL,
tag VARCHAR(100) NOT NULL,
PRIMARY KEY (`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
这两个表中存储的是基础数据,同时对应着Hive中如下两个表:
CREATE TABLE users (
id INT,
name STRING
);
CREATE TABLE tags (
id INT,
user_id INT,
tag STRING
);
我们首先在上述MySQL的两个表中插入一些测试数据:
INSERT INTO tag_db.users(name) VALUES('jeffery');
INSERT INTO tag_db.users(name) VALUES('shirdrn');
INSERT INTO tag_db.users(name) VALUES('sulee');
INSERT INTO tag_db.tags(user_id, tag) VALUES(1, 'Music');
INSERT INTO tag_db.tags(user_id, tag) VALUES(1, 'Programming');
INSERT INTO tag_db.tags(user_id, tag) VALUES(2, 'Travel');
INSERT INTO tag_db.tags(user_id, tag) VALUES(3, 'Sport');
然后,使用Sqoop的import工具,将MySQL两个表中的数据导入到Hive表,执行如下命令行:
bin/sqoop import --connect jdbc:mysql://10.95.3.49:3306/tag_db --tableusers --username shirdrn -P --hive-import -- --default-character-set=utf-8
bin/sqoopimport --connect jdbc:mysql://10.95.3.49:3306/tag_db --table tags --usernameshirdrn -P --hive-import -- --default-character-set=utf-8
导入成功以后,再在Hive中创建一个用来存储users和tags关联后数据的表:
CREATE TABLE user_tags (
id STRING,
name STRING,
tag STRING
);
执行如下HQL语句,将关联数据插入user_tags表:
FROM users u JOIN tags t ON u.id=t.user_id INSERT INTO TABLE user_tagsSELECT CONCAT(CAST(u.id AS STRING), CAST(t.id AS STRING)), u.name, t.tag;
将users.id与tags.id拼接的字符串,作为新表的唯一字段id,name是用户名,tag是标签名称。
再在MySQL中创建一个对应的user_tags表,如下所示:
CREATE TABLE tag_db.user_tags (
id varchar(200) NOT NULL,
name varchar(100) NOT NULL,
tag varchar(100) NOT NULL
);
使用Sqoop的export工具,将Hive表user_tags的数据同步到MySQL表tag_db.user_tags中,执行如下命令行:
bin/sqoop export --connect jdbc:mysql://10.95.3.49:3306/tag_db--username shirdrn --P --table user_tags --export-dir /hive/user_tags--input-fields-terminated-by '\001' -- --default-character-set=utf-8
执行导出成功后,可以在MySQL的tag_db.user_tags表中看到对应的数据。
如果在导出的时候出现类似如下的错误:
14/02/27 17:59:06 INFO mapred.JobClient: TaskId : attempt_201402260008_0057_m_000001_0, Status : FAILED
java.io.IOException: Can't export data, please check task tracker logs
atorg.apache.sqoop.mapreduce.TextExportMapper.map(TextExportMapper.java:112)
atorg.apache.sqoop.mapreduce.TextExportMapper.map(TextExportMapper.java:39)
at org.apache.hadoop.mapreduce.Mapper.run(Mapper.java:145)
at org.apache.sqoop.mapreduce.AutoProgressMapper.run(AutoProgressMapper.java:64)
at org.apache.hadoop.mapred.MapTask.runNewMapper(MapTask.java:764)
at org.apache.hadoop.mapred.MapTask.run(MapTask.java:364)
at org.apache.hadoop.mapred.Child$4.run(Child.java:255)
at java.security.AccessController.doPrivileged(Native Method)
at javax.security.auth.Subject.doAs(Subject.java:396)
atorg.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1190)
at org.apache.hadoop.mapred.Child.main(Child.java:249)
Caused by: java.util.NoSuchElementException
at java.util.AbstractList$Itr.next(AbstractList.java:350)
at user_tags.__loadFromFields(user_tags.java:225)
at user_tags.parse(user_tags.java:174)
atorg.apache.sqoop.mapreduce.TextExportMapper.map(TextExportMapper.java:83)
... 10 more
通过指定字段分隔符选项--input-fields-terminated-by,指定Hive中表字段之间使用的分隔符,供Sqoop读取解析,就不会报错了。
数据同步
我们使用的是Sqoop-1.4.4,在进行关系型数据库与Hadoop/Hive数据同步的时候,如果使用--incremental选项,如使用append模式,我们需要记录一个--last-value的值,如果每次执行同步脚本的时候,都需要从日志中解析出来这个--last-value的值,然后重新设置脚本参数,才能正确同步,保证从关系型数据库同步到Hadoop/Hive的数据不发生重复的问题。
而且,我们我们需要管理我们使用的这些脚本,每次执行之前可能要获取指定参数值,或者修改参数。Sqoop也提供了一种比较方面的方式,那就是直接创建一个Sqoop job,通过job来管理特定的同步任务。就像我们前面提到的增量同步问题,通过创建sqoop job可以保存上一次同步时记录的--last-value的值,也就不用再费劲去解析获取了,每次想要同步,这个job会自动从job保存的数据中获取到。
sqoop job命令使用
Sqoop job相关的命令有两个:
bin/sqoop job
bin/sqoop-job
使用这两个都可以。我们先看看sqoop job命令的基本用法:
创建job:--create
删除job:--delete
执行job:--exec
显示job:--show
列出job:--list
下面,我们基于增量同步数据这个应用场景,创建一个sqoop job,命令如下所示:
bin/sqoop job --create your-sync-job -- import --connectjdbc:mysql://10.95.3.49:3306/workflow --table project --username shirdrn -P--hive-import --incremental append --check-column id --last-value 1 ----default-character-set=utf-8
创建了job,id为“your-sync-job”,它是将MySQL数据库workflow中的project表同步到Hive表中,而且--incremental append选项使用append模式,--last-value为1,从MySQL表中自增主键id=1开始同步。然后我们根据这个job的id去查询job详细配置情况:
bin/sqoop job --show your-sync-job
这里由于,结果示例输出信息过多,不便贴出来,请见谅。
通过incremental.last.value = 1可以看到,通过该选项来控制增量同步开始记录。
接着,可以使用创建的这个job id来运行它,执行如下命令:
bin/sqoop job --exec your-sync-job
可以查询,MySQL数据库workflow中的project表中的数据被同步到Hive表中。
这时,可以通过bin/sqoop job --show your-sync-job命令,查看当前的sqoop job配置情况,可以看到如下变化:
incremental.last.value = 7
从MySQL表中增量同步的起始id变为7,下次同步就会把id大于7的记录同步到Hive表中。可以在MySQL表中再INSERT一条记录,再次执行your-sync-job,能够正确地进行增量同步。
Sqoop job安全配置
默认情况下,创建的每个job在运行的时候都不会进行安全的认证。如果我们希望限制指定的sqoop job的执行,只有经过认证以后才能执行,这时候可以使用sqoop job的安全选项。Sqoop安装目录下,通过修改配置文件conf/sqoop-site.xml可以对job进行更高级的配置。实际上,我们使用了Sqoop的metastore工具,它能够对Sqoop进行细粒度的配置。
我们要将MySQL数据库中的数据同步到Hive表,每次执行sqoop job都需要输入访问MySQL数据库的连接账号信息,可以设置sqoop.metastore.client.record.password的值为true。如果在conf/sqoop-site.xml中增加如下配置,会将连接账号信息存储到Sqoop的metastore中:
如果想要限制从外部调用执行Sqoop job,如将Sqoop job提交给Oozie调度程序,也会通过上面Sqoop的metastore配置的内容来进行验证。
另外,Sqoop的metastore工具,可以允许我们指定为外部,例如使用外部主机上的MySQL数据库来存储元数据,可以在conf/sqoop-site.xml配置如下:
job-management metastore. If unspecified, uses ~/.sqoop/.
You can specify a different path here.
还有一个可与选择的配置项是,可以设置是否自动连接到外部metastore数据库,通过如下配置指定:
这样,你可以通过MySQL的授权机制,来限制指定的用户和主机(或IP地址)访问Sqoop的metadata,也能起到一定的安全访问限制。
4. 调优
mapreduce job所需要的各种参数在Sqoop中的实现
1) InputFormatClass
com.cloudera.sqoop.mapreduce.db.DataDrivenDBInputFormat
2) OutputFormatClass
1)TextFile
com.cloudera.sqoop.mapreduce.RawKeyTextOutputFormat
2)SequenceFile
org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat
3)AvroDataFile
com.cloudera.sqoop.mapreduce.AvroOutputFormat
3)Mapper
1)TextFile
com.cloudera.sqoop.mapreduce.TextImportMapper
2)SequenceFile
com.cloudera.sqoop.mapreduce.SequenceFileImportMapper
3)AvroDataFile
com.cloudera.sqoop.mapreduce.AvroImportMapper
4)taskNumbers
1)mapred.map.tasks(对应num-mappers参数)
2)job.setNumReduceTasks(0);
这里以命令行:import –connectjdbc:mysql://localhost/test –usernameroot –password 123456 –query “select sqoop_1.id as foo_id, sqoop_2.id as bar_id from sqoop_1,sqoop_2 WHERE $CONDITIONS” –target-dir/user/sqoop/test -split-by sqoop_1.id –hadoop-home=/home/hdfs/hadoop-0.20.2-CDH3B3 –num-mappers 2
1)设置Input
DataDrivenImportJob.configureInputFormat(Jobjob, String tableName,String tableClassName, String splitByCol)
a)DBConfiguration.configureDB(Configurationconf, String driverClass, StringdbUrl, String userName, String passwd, Integer fetchSize)
1).mapreduce.jdbc.driver.class com.mysql.jdbc.Driver
2).mapreduce.jdbc.url jdbc:mysql://localhost/test
3).mapreduce.jdbc.username root
4).mapreduce.jdbc.password 123456
5).mapreduce.jdbc.fetchsize -2147483648
b)DataDrivenDBInputFormat.setInput(Jobjob,Class extends DBWritable> inputClass, String inputQuery, StringinputBoundingQuery)
1)job.setInputFormatClass(DBInputFormat.class);
2)mapred.jdbc.input.bounding.query SELECTMIN(sqoop_1.id), MAX(sqoop_2.id) FROM (select sqoop_1.id as foo_id, sqoop_2.idas bar_id from sqoop_1 ,sqoop_2 WHERE (1 = 1) ) AS t1
3)job.setInputFormatClass(com.cloudera.sqoop.mapreduce.db.DataDrivenDBInputFormat.class);
4)mapreduce.jdbc.input.orderby sqoop_1.id
c)mapreduce.jdbc.input.class QueryResult
d)sqoop.inline.lob.length.max 16777216
2)设置Output
ImportJobBase.configureOutputFormat(Jobjob, String tableName,String tableClassName)
a)job.setOutputFormatClass(getOutputFormatClass());
b)FileOutputFormat.setOutputCompressorClass(job,codecClass);
c)SequenceFileOutputFormat.setOutputCompressionType(job,CompressionType.BLOCK);
d)FileOutputFormat.setOutputPath(job,outputPath);
3)设置Map
DataDrivenImportJob.configureMapper(Jobjob, String tableName,String tableClassName)
a)job.setOutputKeyClass(Text.class);
b)job.setOutputValueClass(NullWritable.class);
c)job.setMapperClass(com.cloudera.sqoop.mapreduce.TextImportMapper);
4)设置task number
JobBase.configureNumTasks(Job job)
mapred.map.tasks 4
job.setNumReduceTasks(0);
这里以命令行:import –connectjdbc:mysql://localhost/test –usernameroot –password 123456 –query “select sqoop_1.id as foo_id, sqoop_2.id as bar_id from sqoop_1,sqoop_2 WHERE $CONDITIONS” –target-dir/user/sqoop/test -split-by sqoop_1.id –hadoop-home=/home/hdfs/hadoop-0.20.2-CDH3B3 –num-mappers 2
注:红色部分参数,后接根据命令衍生的参数值
1)设置Input
DataDrivenImportJob.configureInputFormat(Jobjob, String tableName,String tableClassName, String splitByCol)
a)DBConfiguration.configureDB(Configurationconf, String driverClass,
String dbUrl, String userName, String passwd, Integer fetchSize)
1).mapreduce.jdbc.driver.classcom.mysql.jdbc.Driver
2).mapreduce.jdbc.url jdbc:mysql://localhost/test
3).mapreduce.jdbc.username root
4).mapreduce.jdbc.password 123456
5).mapreduce.jdbc.fetchsize -2147483648
b)DataDrivenDBInputFormat.setInput(Jobjob,Class extends DBWritable> inputClass, String inputQuery, StringinputBoundingQuery)
1)job.setInputFormatClass(DBInputFormat.class); 2)mapred.jdbc.input.bounding.query SELECT MIN(sqoop_1.id),MAX(sqoop_2.id) FROM (select sqoop_1.id as foo_id, sqoop_2.id as bar_id fromsqoop_1 ,sqoop_2 WHERE (1 = 1) ) AS t1
3)job.setInputFormatClass(com.cloudera.sqoop.mapreduce.db.DataDrivenDBInputFormat.class);
4)mapreduce.jdbc.input.orderby sqoop_1.id
c)mapreduce.jdbc.input.class QueryResult
d)sqoop.inline.lob.length.max 16777216
2)设置Output
ImportJobBase.configureOutputFormat(Jobjob, String tableName,String tableClassName)
a)job.setOutputFormatClass(getOutputFormatClass()); b)FileOutputFormat.setOutputCompressorClass(job,codecClass);
c)SequenceFileOutputFormat.setOutputCompressionType(job,CompressionType.BLOCK);
d)FileOutputFormat.setOutputPath(job,outputPath);
3)设置Map
DataDrivenImportJob.configureMapper(Jobjob, String tableName,String tableClassName)
a)job.setOutputKeyClass(Text.class);
b)job.setOutputValueClass(NullWritable.class);
c)job.setMapperClass(com.cloudera.sqoop.mapreduce.TextImportMapper);
4)设置task number
JobBase.configureNumTasks(Job job)
mapred.map.tasks 4
job.setNumReduceTasks(0);
5. 命令
Sqoop大约有13种命令,和几种通用的参数(都支持这13种命令),这里先列出这13种命令。
接着列出Sqoop的各种通用参数,然后针对以上13个命令列出他们自己的参数。Sqoop通用参数又分Common arguments,Incremental import arguments,Output line formattingarguments,Input parsing arguments,Hive arguments,HBase arguments,Generic Hadoopcommand-line arguments
1)列出mysql数据库中的所有数据库
sqoop list-databases –connectjdbc:mysql://localhost:3306/ –username root –password 123456
2)连接mysql并列出test数据库中的表
sqoop list-tables –connectjdbc:mysql://localhost:3306/test –username root –password 123456
命令中的test为mysql数据库中的test数据库名称 username password分别为mysql数据库的用户密码
3)将关系型数据的表结构复制到hive中,只是复制表的结构,表中的内容没有复制过去。
sqoop create-hive-table –connectjdbc:mysql://localhost:3306/test
–table sqoop_test –username root –password 123456 –hive-table
test
其中–table sqoop_test为mysql中的数据库test中的表–hive-table
test 为hive中新建的表名称
4)从关系数据库导入文件到hive中
sqoop import –connectjdbc:mysql://localhost:3306/zxtest –username
root –password 123456 –table sqoop_test–hive-import –hive-table
s_test -m 1
5)将hive中的表数据导入到mysql中,在进行导入之前,mysql中的表
hive_test必须已经提起创建好了。
sqoop export –connectjdbc:mysql://localhost:3306/zxtest –username
root –password root –table hive_test–export-dir
/user/hive/warehouse/new_test_partition/dt=2012-03-05
6)从数据库导出表的数据到HDFS上文件
./sqoop import –connect
jdbc:mysql://10.28.168.109:3306/compression–username=hadoop
–password=123456 –table HADOOP_USER_INFO -m 1 –target-dir
/user/test
7)从数据库增量导入表数据到hdfs中
./sqoop import –connectjdbc:mysql://10.28.168.109:3306/compression
–username=hadoop –password=123456 –table HADOOP_USER_INFO -m 1
–target-dir /user/test –check-column id –incremental append
–last-value 3
8)将mysql中的表导入hive中:
$ sqoop import --connectjdbc:mysql://IP:PORT/DATABASE --username USERNAME --password PASSWORD --tableTABLE --hive-import
9) hive启动日志检查
hive -hiveconfhive.root.logger=DEBUG,console
10) qoop 在同步mysql表结构到hive
sqoop create-hive-table --connect jdbc:mysql://ip:3306/sampledata--table t1--username dev --password 1234 --hive-table t1;
==================================================================
1.使用sqoop导入数据
sqoop import --connect jdbc:mysql://localhost/db --username foo --tableTES
2.账号密码
sqoop import --connect jdbc:mysql://database.example.com/employees \
--username aaron --password 12345
3.驱动
sqoop import --driver com.microsoft.jdbc.sqlserver.SQLServerDriver \
--connect string> ...
4.写sql语句导入的方式
sqoop import \
--query 'SELECT a.*, b.* FROM a JOIN b on (a.id == b.id) WHERE$CONDITIONS' \
--split-by a.id --target-dir /user/foo/joinresults
如果是顺序导入的话,可以只开一个线程
sqoop import \
--query 'SELECT a.*, b.* FROM a JOIN b on (a.id == b.id) WHERE$CONDITIONS' \
-m 1 --target-dir /user/foo/joinresults
如果where语句中有要用单引号的,就像这样子写就可以啦"SELECT * FROM x WHERE a='foo' AND \$CONDITIONS"
5. 1.4.3版本的sqoop不支持复杂的sql语句,不支持or语句
6. --split-by
默认是主键,假设有100行数据,它会执行那个SELECT * FROM sometable WHERE id >= lo AND id
7. --direct 是为了利用某些数据库本身提供的快速导入导出数据的工具,比如mysql的mysqldump
性能比jdbc更好,但是不知大对象的列,使用的时候,那些快速导入的工具的客户端必须的shell脚本的目录下
8.导入数据到hdfs目录,这个命令会把数据写到/shared/foo/ 目录
sqoop import --connnect --tablefoo --warehouse-dir /shared \
或者
sqoop import --connnect --tablefoo --target-dir /dest \
9.传递参数给快速导入的工具,使用--开头,下面这句命令传递给mysql默认的字符集是latin1
sqoop import --connect jdbc:mysql://server.foo.com/db --table bar \
--direct -- --default-character-set=latin1
10.转换为对象
--map-column-java 转换为java数据类型
--map-column-hive 转转为hive数据类型
11.增加导入
--check-column(col) Specifies the column to beexamined when determining which rows to import.
--incremental(mode) Specifies how Sqoop determineswhich rows are new. Legal values for mode include append and lastmodified.
--last-value(value) Specifies the maximum value ofthe check column from the previous import.
增加导入支持两种模式append和lastmodified,用--incremental来指定
12.在导入大对象
比如BLOB和CLOB列时需要特殊处理,小于16MB的大对象可以和别的数据一起存储,超过这个值就存储在_lobs的子目录当中
它们采用的是为大对象做过优化的存储格式,最大能存储2^63字节的数据,我们可以用--inline-lob-limit参数来指定每个lob文件最大的限制是多少 如果设置为0,则大对象使用外部存储
13.分隔符、转移字符
下面的这句话
Some string, with a comma.
Another "string with quotes"
使用这句命令导入$ sqoop import --fields-terminated-by , --escaped-by \\--enclosed-by '\"' ...
会有下面这个结果
"Some string, with a comma.","1","2","3"...
"Another \"string withquotes\"","4","5","6"...
使用这句命令导入$ sqoop import --optionally-enclosed-by '\"' (the rest asabove)...
"Some string, with a comma.",1,2,3...
"Another \"string with quotes\"",4,5,6...
14.hive导入参数
--hive-home 重写$HIVE_HOME
--hive-import 插入数据到hive当中,使用hive的默认分隔符
--hive-overwrite 重写插入
--create-hive-table 建表,如果表已经存在,该操作会报错!
--hive-table [table] 设置到hive当中的表名
--hive-drop-import-delims 导入到hive时删除 \n, \r,and \01
--hive-delims-replacement 导入到hive时用自定义的字符替换掉\n, \r, and \01
--hive-partition-key hive分区的key
--hive-partition-value hive分区的值
--map-column-hive 类型匹配,sql类型对应到hive类型
15.hive空值处理
sqoop会自动把NULL转换为null处理,但是hive中默认是把\N来表示null,因为预先处理不会生效的
我们需要使用 --null-string 和 --null-non-string来处理空值把\N转为\\N
sqoop import ... --null-string'\\N' --null-non-string '\\N'
16.导入数据到hbase
导入的时候加上--hbase-table,它就会把内容导入到hbase当中,默认是用主键作为split列
也可以用--hbase-row-key来指定,列族用--column-family来指定,它不支持--direct。
如果不想手动建表或者列族,就用--hbase-create-table参数
17.代码生成参数,没看懂
--bindir Output directoryfor compiled objects
--class-name Sets thegenerated class name. This overrides --package-name. When combined with--jar-file, sets the input class.
--jar-file Disable codegeneration; use specified jar
--outdir Output directoryfor generated code
--package-name Putauto-generated classes in this package
--map-column-java Overridedefault mapping from SQL type to Java type for configured columns.
18.通过配置文件conf/sqoop-site.xml来配置常用参数
如果不在这里面配置的话,就需要像这样写命令
sqoop import -D property.name=property.value ...
19.两个特别的参数
sqoop.bigdecimal.format.string 大decimal是否保存为string,如果保存为string就是 0.0000007,否则则为1E7
sqoop.hbase.add.row.key 是否把作为rowkey的列也加到行数据当中,默认是false的
20.例子
#指定列
$sqoop import --connect jdbc:mysql://db.foo.com/corp --table EMPLOYEES \
--columns "employee_id,first_name,last_name,job_title"
#使用8个线程
$sqoop import --connect jdbc:mysql://db.foo.com/corp --table EMPLOYEES \
-m 8
#快速模式
$sqoop import --connect jdbc:mysql://db.foo.com/corp --table EMPLOYEES \
--direct
#使用sequencefile作为存储方式
$sqoop import --connect jdbc:mysql://db.foo.com/corp --table EMPLOYEES \
--class-name com.foocorp.Employee --as-sequencefile
#分隔符
$sqoop import --connect jdbc:mysql://db.foo.com/corp --table EMPLOYEES \
--fields-terminated-by '\t' --lines-terminated-by '\n' \
--optionally-enclosed-by '\"'
#导入到hive
$sqoop import --connect jdbc:mysql://db.foo.com/corp --table EMPLOYEES \
--hive-import
#条件过滤
$sqoop import --connect jdbc:mysql://db.foo.com/corp --table EMPLOYEES \
--where "start_date > '2010-01-01'"
#用dept_id作为分个字段
$sqoop import --connect jdbc:mysql://db.foo.com/corp --table EMPLOYEES \
--split-by dept_id
#追加导入
$sqoop import --connect jdbc:mysql://db.foo.com/somedb --table sometable \
--where "id > 100000" --target-dir /incremental_dataset--append
21.导入所有的表sqoop-import-all-tables
每个表都要有主键,不能使用where条件过滤
sqoop import-all-tables --connect jdbc:mysql://db.foo.com/corp
22.export
我们采用sqoop-export插入数据的时候,如果数据已经存在了,插入会失败
如果我们使用--update-key,它会认为每个数据都是更新,比如我们使用下面这条语句
sqoop-export --table foo --update-key id --export-dir /path/to/data--connect …
UPDATE fooSET msg='this is a test', bar=42 WHERE id=0;
UPDATE fooSET msg='some more data', bar=100 WHERE id=1;
...
这样即使找不到它也不会报错
23.如果存在就更新,不存在就插入
加上这个参数就可以啦--update-mode allowinsert
24.事务的处理
它会一次statement插入100条数据,然后每100个statement提交一次,所以一次就会提交10000条数据
25.例子
$sqoop export --connect jdbc:mysql://db.example.com/foo --table bar \
--export-dir /results/bar_data
$sqoop export --connect jdbc:mysql://db.example.com/foo --table bar \
--export-dir /results/bar_data --validate
$sqoop export --connect jdbc:mysql://db.example.com/foo --call barproc \
--export-dir /results/bar_data
6.问题
1)、自由查询形式导入
Sqoop还支持将任意的查询结果集导入,不使用--table、--columns和--where,使用SQL语句--query参数执行自由查询导入,但是必须指定--target-dir目录,必须指定--split-by 分隔列,同时必须使用where且在其后加个$CONDITIONS,使Sqoop进程替代为一个唯一的条件表达式达到条件查询效果。
Sqoop使用--split-by 列名,根据此分隔工作量,默认的Sqoop将表中的关键字作为分隔列,由上导入命令可知,此处我们是以“id”作为分隔列。
Sqoop从大部分的数据源并行的导入数据,我们可以使用-m参数控制Map tasks的数目,默认是4个,此处我们改成了1个Map task。Map task,根据整个范围的均衡大小进行操作。例如,你有一张表,关键字id范围是0-1000,默认Map tasks 是4个,Sqoop将会执行4个进程,每个进程以如下格式执行SELECT * FROM sometable WHERE id >= lo AND id < hi其中(lo, hi)set to (0, 250), (250, 500), (500, 750), and (750, 1001) 在不同的任务中。
注意一:如果你的表中关键字不是根据其范围均匀的分布,就可能导致不平衡的任务。这个时候你需要明确的选择一个不同的列使用--split-by指定分隔参数。目前,Sqoop,还不支持对各个列索引进行分隔,如果一个表没有索引列或者含有多个关键字列,你必须手动的指定一个分隔列。
注意二:如果SQL语句中使用双引号(“”),则必须使用$CONDITIONS代替$CONDITIONS,使你的shell不将其识别为shell自身的变量。如下示例:
错误方式:
[hadoopUser@secondmgt ~]$ sqoop import--connect jdbc:mysql://secondmgt:3306/spice --username hive
正确如下:
[hadoopUser@secondmgt ~]$ sqoop import--connect jdbc:mysql://secondmgt:3306/spice --username hive --password hive--query "select * from users where $CONDITIONS" --split-by id --target-dir /output/query/
注意三:目前版本的Sqoop中,使用自由形式查询导入,只提供简单的查询,没有复杂的和“OR”条件查询在where子句中。
2)、控制导入进程
有些数据库提供更加快捷、高效的方式用来将数据库表中的数据导入到其他的系统中,这个时候可以--direct 参数。如:mysql会调用 mysqldump和mysqlimport ,PostgreSQL 为psql。
3)、控制映射类型
Sqoop预配置了Java和Hive典型的大部分SQL类型,然而,默认的类型有时候不一定完全适合用户需求。可以使用下面两个参数根据自己的应用修改映射类型
Argument Description --map-column-java --map-column-hive |
4)、文件格式
Sqoop支持两种类型的文件格式导入:分隔符文本和序列文件(delimited text or SequenceFiles)。默认的是采用分隔符文本,由上面导入后查询的结果可知,默认采用逗号分隔的。可以使用--as-textfile参数修改默认的文件导入格式。
delimited text 是适合大多数非二进制数据类型。它也很容易支持进一步操纵其他工具,如Hive。
SequenceFiles是二进制格式以自定义记录特有的数据类型来存储个人记录的。
5)、sqoop 导入数据到HDFS注意事项
分割符的方向问题
首先sqoop的参数要小心, 从数据库导出数据,写到HDFS的文件中的时候,字段分割符号和行分割符号必须要用
--fields-terminated-by
而不能是
--input-fields-terminated-by
--input前缀的使用于读文件的分割符号,便于解析文件,所以用于从HDFS文件导出到某个数据库的场景。
两个方向不一样。
参数必须用单引号括起来
官方文档的例子是错的:
The octal representation of a UTF-8 character’s code point. This shouldbe of the form \0ooo, where ooo is the octal value. For example,--fields-terminated-by \001 would yield the ^A character.
应该写成
--fields-terminated-by '\001'
创建Hive表
CREATE EXTERNAL TABLE my_table(
id int,
...
)
PARTITIONED BY (
dt string)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\001'
LINES TERMINATED BY '\n'
STORED AS textfile;
要小心hive的bug,如果用\001, hive会友好的转换成\u0001
但是如果直接写\u0001, hive某些版本会变成u0001
STORED AS textfile 可以不用。
6)、sqoop 使用指定条件导入数据
在测试sqoop语句的时候,一定要限制记录数量,否则就像我刚才,等了1个多小时,才看到测试结果。
sqoop-import --options-filemedia_options.txt --table my_table --where "ID = 2" --target-dir/user/jenkins/bigdata/import/20140607 -m 1 --fields-terminated-by '\001'--lines-terminated-by '\n'
导入后,可以用hdfs dfs -get命令获取文件到本地目录
然后用bunzip2 命令解压,
最后用emacs的hexl-mode查看文件的16进制格式,检查分割符是否正确。
m 1代表一个mapreduce
7)、 sqoop导入时删除string类型字段的特殊字符
如果你指定了\n为sqoop导入的换行符,mysql的某个string字段的值如果包含了\n, 则会导致sqoop导入多出一行记录。
有一个选项
-hive-drop-import-delims Drops\n, \r, and \01 from string fields when importing to Hive.
8)、 sqoop导入数据时间日期类型错误
一个问题困扰了很久,用sqoop import从mysql数据库导入到HDFS中的时候一直报错,最后才发现是一个时间日期类型的非法值导致。
hive只支持timestamp类型,而mysql中的日期类型是datetime, 当datetime的值为0000-00-0000:00:00的时候,sqoop import成功,但是在hive中执行select语句查询该字段的时候报错。
解决方法是在创建hive表时用string字段类型。
9)、sqoop 从mysql导入hive的字段名称问
hive中有些关键字限制,因此有些字段名称在mysql中可用,但是到了hive就不行。
比如order必须改成order1, 下面列出了我们发现的一些不能在hive中使用的字段名称
order => order1
sort => sort1
reduce => reduce
cast => cas
directory => directory1
7. shell从关系库导入hive-hbase(整合)表
从关系库导入数据到hive-hbase表中,关系库到hbase中,可以直接由sqoop来导入,但是我们要对rowkey做特殊处理并加入更新时间,则操作步骤如下:
1、创建hive与hbase的表
1)创建hbase表
命令行输入 hbase shell 进入hbase的终端:
[Bash shell]
create 'location','cf1'
2)创建hive的外表
[Bash shell]
hive -e"
drop TABLE ods.hbase_location;
CREATE EXTERNAL TABLE ods.hbase_location(key string ,
ID int comment '唯一ID',
location1 string comment '国家代号' ,
location2 string comment '省份代号' ,
location3 string comment '城市代号',
country string comment '国家(中文)',
cn string comment '省份(中文)',
cn_city string comment '城市(中文)',
cn_district string comment '城市所属地区(中文)',
py string comment '省份缩略(拼音)',
en_city string comment '城市(英文)',
en_district string comment '城市所属地区(英文)',
en string comment '省份(英文)',
geoid INT comment '行政区划代码',
updatetime string comment '更新时间'
)
STORED BY'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES("hbase.columns.mapping" = ":key,
cf1:ID,
cf1:location1,
cf1:location2,
cf1:location3,
cf1:country,
cf1:cn,
cf1:cn_city,
cf1:cn_district,
cf1:py,
cf1:en_city,
cf1:en_district,
cf1:en,
cf1:geoid,
cf1:updatetime")
TBLPROPERTIES("hbase.table.name"= "location"); "
2、自动化的shell脚本
[Bash shell]
#!/bin/sh
# upload logs to hdfs
today=`date --date='0 days ago' +%Y-%m-%d`
hive -e"
drop table stage.location_tmp;
CREATE TABLE
stage.location_tmp
(
ID INT comment '唯一ID',
location1 string comment '国家代号' ,
location2 string comment '省份代号' ,
location3 string comment '城市代号',
country string comment '国家(中文)',
cn string comment '省份(中文)',
cn_city string comment '城市(中文)',
cn_district string comment '城市所属地区(中文)',
py string comment '省份缩略(拼音)',
en_city string comment '城市(英文)',
en_district string comment '城市所属地区(英文)',
en string comment '省份(英文)',
geoid INT comment '行政区划代码'
)
ROW FORMAT DELIMITED fields terminated by'\001'
STORED AS TEXTFILE;
"
sqoop import --connectjdbc:mysql://10.130.2.6:3306/bdc_test --username lvwenjuan --password Abcd1234 --table location --where "1=1"--columns "ID ,location1 ,location2 ,location3 ,country ,cn ,cn_city ,cn_district ,py , en_city , en_district ,en , geoid "--fields-terminated-by '\001' --hive-import --hive-drop-import-delims--hive-table stage.location_tmp -m 1
hive -e"
insert into table ods.hbase_location selectreverse(ID) ,
ID ,
location1 ,
location2 ,
location3 ,
country,
cn,
cn_city,
cn_district,
py,
en_city,
en_district,
en string,
geoid ,
udf_getbfhourstime(0) fromstage.location_tmp;"
hive -e "drop TABLEstage.location_tmp;"
3、说明
1) stage.location_tmp为临时中转表,本次ETL完后即删除。
2)--where"1=1" 可设置关系库的查询语句
3)reverse(ID) 对hbase的rowkey自动逆序处理
4)insert into 到hbase中自动根据rowkey来去重
5)udf_getbfhourstime(0)自定义函数,取的是当前时间
8. sqoop 从 hive 导到mysql问题
1、拒绝连接:mysql 用户权限问题
2. 导出数据到MySQL,当然数据库表要先存在,否则会报错
此错误的原因为sqoop解析文件的字段与MySql数据库的表的字段对应不上造成的。因此需要在执行的时候给sqoop增加参数,告诉sqoop文件的分隔符,使它能够正确的解析文件字段。hive默认的字段分隔符为’\001′
3. null字段填充符需指定
没有指定null字段分隔符,导致错位。
6. mysql字符串长度定义太短,存不下
在导入数据的过程中,如果碰到列值为null的情况,hive中为null的是以\N代替的,所以你在导入到MySql时,需要加上两个参数:--input-null-string '\\N' --input-null-non-string '\\N',多加一个'\',是为转义。如果你通过这个还不能解决字段为null的情况,还是报什么NumberFormalt异常的话,那就是比较另类的了,没有关系,我们还是要办法解决,这就是终极武器。呵呵
你应该注意到每次通过sqoop导入MySql的时,都会生成一个以MySql表命名的.java文件,然后打成JAR包,给sqoop提交给hadoop 的MR来解析Hive表中的数据。那我们可以根据报的错误,找到对应的行,改写该文件,编译,重新打包,sqoop可以通过 -jar-file ,--class-name 组合让我们指定运行自己的jar包中的某个class。来解析该hive表中的每行数据。脚本如下:一个完整的例子如下:
./bin/sqoop export --connect"jdbc:mysql://localhost/aaa?useUnicode=true&characterEncoding=utf-8"
--username aaa --password bbb --table table
--export-dir /hive/warehouse/table --input-fields-terminated-by '\t'
--input-null-string '\\N' --input-null-non-string '\\N'
--class-name com.chamago.sqoop.codegen.bi_weekly_sales_item
--jar-file /tmp/sqoop-chamago/bi_weekly_sales_item.jar
上面--jar-file 参数指定jar包的路径。--class-name 指定jar包中的class。
这样就可以解决所有解析异常了。
下面贴下sqoop经常用的命令,
导入MySQL表到Hive
./sqoop import --connectjdbc:mysql://localhost/aaa?useUnicode=true&characterEncoding=utf-8--username
aaa --password bbb --table table2 --hive-import
6.日期格式问题
mysql date日期格式,hive中字符串必须是yyyy-mm-dd,我原来使用yyyymmdd,报下面的错误。
7. 字段对不上或字段类型不一致
七、Flume
1. 安装
解压
配置环境变量
配置conf/flume-env.sh JAVA_HOME=/usr/jdk1.7.0_79
一个 example文件
agent1表示代理名称 |
一个测试的例子:
#设置配置文件 [root@cc-staging-loginmgr2 conf]# cat example.conf # example.conf: A single-node Flume configuration # Name the components on this agent a1.sources = r1 a1.sinks = k1 a1.channels = c1 # Describe/configure the source a1.sources.r1.type = netcat a1.sources.r1.bind = localhost a1.sources.r1.port = 44444 # Describe the sink a1.sinks.k1.type = logger # Use a channel which buffers events in memory a1.channels.c1.type = memory a1.channels.c1.capacity = 1000 a1.channels.c1.transactionCapacity = 100 # Bind the source and sink to the channel a1.sources.r1.channels = c1 a1.sinks.k1.channel = c1 #命令参数说明 -c conf 指定配置目录为conf -f conf/example.conf 指定配置文件为conf/example.conf -n a1 指定agent名字为a1,需要与example.conf中的一致 -Dflume.root.logger=INFO,console 指定DEBUF模式在console输出INFO信息 #启动agen cd /usr/local/apache-flume-1.3.1-bin flume-ng agent -c conf -f conf/example.conf -n a1 -Dflume.root.logger=INFO,conso 2013-05-24 00:00:09,288 (lifecycleSupervisor-1-0) [INFO - org.apache.flume.source.NetcatSource.start(NetcatSource.java:150)] Source starting 2013-05-24 00:00:09,303 (lifecycleSupervisor-1-0) [INFO - org.apache.flume.source.NetcatSource.start(NetcatSource.java:164)] Created serverSocket:sun.nio.ch.ServerSocketChannelImpl[/127.0.0.1:44444] #在另一个终端进行测试 [root@cc-staging-loginmgr2 conf]# telnet 127.0.0.1 44444 Trying 127.0.0.1... Connected to localhost.localdomain (127.0.0.1). Escape character is '^]'. hello world! OK |
测试$./bin/flume-ng version
启动/conf/example -Dflume.root.logger=DEBUG,console
2. 结构
(1)搜集信息
(2)获取记忆信息
(3)传递报告间谍信息
flume是怎么完成上面三件事情的,三个组件:
source:搜集信息
channel:传递信息
sink:存储信息
flume的特点:
flume是一个分布式、可靠、和高可用的海量日志采集、聚合和传输的系统。支持在日志系统中定制各类数据发送方,用于收集数据;同时,Flume提供对数据进行简单处理,并写到各种数据接受方(比如文本、HDFS、Hbase等)的能力。
flume的数据流由事件(Event)贯穿始终。事件是Flume的基本数据单位,它携带日志数据(字节数组形式)并且携带有头信息,这些Event由Agent外部的Source生成,当Source捕获事件后会进行特定的格式化,然后Source会把事件推入(单个或多个)Channel中。你可以把Channel看作是一个缓冲区,它将保存事件直到Sink处理完该事件。Sink负责持久化日志或者把事件推向另一个Source。
flume的可靠性
当节点出现故障时,日志能够被传送到其他节点上而不会丢失。Flume提供了三种级别的可靠性保障,从强到弱依次分别为:end-to-end(收到数据agent首先将event写到磁盘上,当数据传送成功后,再删除;如果数据发送失败,可以重新发送。),Store on failure(这也是scribe采用的策略,当数据接收方crash时,将数据写到本地,待恢复后,继续发送),Besteffort(数据发送到接收方后,不会进行确认)。
flume的可恢复性:
还是靠Channel。推荐使用FileChannel,事件持久化在本地文件系统里(性能较差)。
flume的一些核心概念:
Agent 使用JVM 运行Flume。每台机器运行一个agent,但是可以在一个agent中包含多个sources和sinks。
Client 生产数据,运行在一个独立的线程。
Source 从Client收集数据,传递给Channel。
Sink 从Channel收集数据,运行在一个独立线程。
Channel 连接 sources 和 sinks ,这个有点像一个队列。
Events 可以是日志记录、 avro对象等。
Flume以agent为最小的独立运行单位。一个agent就是一个JVM。单agent由Source、Sink和Channel三大组件构成,如下图:
值得注意的是,Flume提供了大量内置的Source、Channel和Sink类型。不同类型的Source,Channel和Sink可以自由组合。组合方式基于用户设置的配置文件,非常灵活。比如:Channel可以把事件暂存在内存里,也可以持久化到本地硬盘上。Sink可以把日志写入HDFS, HBase,甚至是另外一个Source等等。Flume支持用户建立多级流,也就是说,多个agent可以协同工作,并且支持Fan-in、Fan-out、Contextual Routing、Backup Routes,这也正是NB之处。如下图所示:
3. Flume-NG
Flume-NG的所有统计信息、监控及相关的类都在org.apache.flume.instrumentation.http、org.apache.flume.instrumentation、org.apache.flume.instrumentation.util三个包下。
上面提到了MonitoredCounterGroup,这个类是用来跟踪内部的统计指标的,注册组件的MBean并跟踪和更新统计值。需要监控的组件都要继承这个类,这个类可以跟踪flume内部的所有组件,但是目前只实现了3个。其中比较重要的方法有以下几个:
(1)、构造方法MonitoredCounterGroup(Type type, String name, String... attrs),这个方法主要是设置组件的类型、名称;然后将所有的attrs(这是设定的各个统计项)加入Map
(2)、start()方法,会先注册计数器,然后对所有统计项的统计值设为0;将开始时间设置为当前时间
(3)、register()方法,如果这个计数器还未注册,将这个计数器的MBean进行注册,就可以进行跟踪了
(4)、stop()方法,会设置结束时间为当前时间;输出各个统计项的信息。我们 Ctrl+C 结束进程时,最后显示的统计信息就是来自这里。
其它方法都是获取counterMap的中信息或者更新值等,比较简单。
接下来我们看看,三个组件中各种统计项及其含义吧:
一、SourceCounter,继承了MonitoredCounterGroup。主要统计项如下:
(1)"src.events.received",表示source接受的event个数;
(2)"src.events.accepted",表示source处理成功的event个数,和上面的区别就是上面虽然接受了可能没处理成功;
(3)"src.append.received",表示调用append次数,在avrosource和thriftsource中调用;
(4)"src.append.accepted",表示append处理成功次数;
(5)"src.append-batch.received",表示appendBatch被调用的次数,在avrosource和thriftsource中调用;
(6)"src.append-batch.accepted",表示appendBatch处理成功次数;
(7)"src.open-connection.count",用在avrosource中表示打开连接的数量;
一般source调用都集中在前俩。
二、SinkCounter,继承了MonitoredCounterGroup
(1)"sink.connection.creation.count",这个调用的地方颇多,都表示“链接”创建的数量,比如与HBase建立链接,与avrosource建立链接以及文件的打开等;
(2)"sink.connection.closed.count",对应于上面的stop操作、destroyConnection、close文件操作等。
(3)"sink.connection.failed.count",表示上面所表示“链接”时异常、失败的次数;
(4)"sink.batch.empty",表示这个批次处理的event数量为0的情况;
(5)"sink.batch.underflow",表示这个批次处理的event的数量介于0和配置的batchSize之间;
(6)"sink.batch.complete",表示这个批次处理的event数量等于设定的batchSize;
(7)"sink.event.drain.attempt",准备处理的event的个数;
(8)"sink.event.drain.sucess",这个表示处理成功的event数量,与上面不同的是上面的是还未处理的。
三、ChannelCounter,继承了MonitoredCounterGroup
(1)"channel.current.size",这个表示这个channel的当前容量
(2)"channel.event.put.attempt",一般指的是在channel的事务当中,source的put操作中记录尝试发送event的个数;
(3)"channel.event.take.attempt",一般指的是在channel的事务中,sink的take操作记录尝试拿event的个数;
(4)"channel.event.put.success",一般指的是在channel的事务中,put成功的event的数量;
(5)"channel.event.take.success",一般指的是channel事务中,take成功的event的数量;
(6)"channel.capacity",指的是channel的容量,在channel的start方法中设置。
4. 命令
5. 问题
1. OOM 问题:
flume 报错:
java.lang.OutOfMemoryError: GC overhead limit exceeded
或者:
java.lang.OutOfMemoryError: Java heap space
Exception in thread"SinkRunner-PollingRunner-DefaultSinkProcessor"java.lang.OutOfMemoryError: Java heap space
Flume 启动时的最大堆内存大小默认是 20M,线上环境很容易 OOM,因此需要你在flume-env.sh 中添加 JVM 启动参数:
JAVA_OPTS="-Xms8192m -Xmx8192m -Xss256k -Xmn2g -XX:+UseParNewGC-XX:+UseConcMarkSweepGC -XX:-UseGCOverheadLimit"
然后在启动 agent 的时候一定要带上 -c conf 选项,否则 flume-env.sh 里配置的环境变量不会被加载生效。
2 JDK 版本不兼容问题:
2014-07-07 14:44:17,902 (agent-shutdown-hook) [WARN -org.apache.flume.sink.hdfs.HDFSEventSink.stop(HDFSEventSink.java:504)]Exception while closing hdfs://192.168.1.111:8020/flumeTest/FlumeData.Exception follows.
java.lang.UnsupportedOperationException: This is supposed to beoverridden by subclasses.
at com.google.protobuf.GeneratedMessage.getUnknownFields(GeneratedMessage.java:180)
atorg.apache.hadoop.hdfs.protocol.proto.ClientNamenodeProtocolProtos$GetFileInfoRequestProto.getSerializedSize(ClientNamenodeProtocolProtos.java:30108)
at com.google.protobuf.AbstractMessageLite.toByteString(AbstractMessageLite.java:49)
atorg.apache.hadoop.ipc.ProtobufRpcEngine$Invoker.constructRpcRequest(ProtobufRpcEngine.java:149)
at org.apache.hadoop.ipc.ProtobufRpcEngine$Invoker.invoke(ProtobufRpcEngine.java:193)
把你的 jdk7 换成 jdk6 试试。
3 小文件写入 HDFS 延时的问题
其实上面 3.2 中已有说明,flume 的 sink 已经实现了几种最主要的持久化触发器:
比如按大小、按间隔时间、按消息条数等等,针对你的文件过小迟迟没法写入 HDFS 持久化的问题,
那是因为你此时还没有满足持久化的条件,比如你的行数还没有达到配置的阈值或者大小还没达到等等,
可以针对上面 3.2 小节的配置微调下,例如:
agent1.sinks.log-sink1.hdfs.rollInterval = 20
当迟迟没有新日志生成的时候,如果你想很快的 flush,那么让它每隔 20s flush 持久化一下,agent 会根据多个条件,优先执行满足条件的触发器。
下面贴一些常见的持久化触发器:
#Number of seconds to wait before rolling current file (in 600 seconds)
agent.sinks.sink.hdfs.rollInterval=600
#File size to trigger roll, in bytes (256Mb)
agent.sinks.sink.hdfs.rollSize = 268435456
#never roll based on number of events
agent.sinks.sink.hdfs.rollCount = 0
#Timeout after which inactive files get closed (in seconds)
agent.sinks.sink.hdfs.idleTimeout = 3600
agent.sinks.HDFS.hdfs.batchSize = 1000
更多关于 sink 的触发机制与参数配置请参见: http://flume.apache.org/FlumeUserGuide.html#hdfs-sink
http://stackoverflow.com/questions/20638498/flume-not-writing-to-hdfs-unless-killed
注意:对于 HDFS 来说应当竭力避免小文件问题,所以请慎重对待你配置的持久化触发机制。
4 数据重复写入、丢失问题
Flume的HDFSsink在数据写入/读出Channel时,都有Transcation的保证。当Transaction失败时,会回滚,然后重试。但由于HDFS不可修改文件的内容,假设有1万行数据要写入HDFS,而在写入5000行时,网络出现问题导致写入失败,Transaction回滚,然后重写这10000条记录成功,就会导致第一次写入的5000行重复。这些问题是 HDFS 文件系统设计上的特性缺陷,并不能通过简单的Bugfix来解决。我们只能关闭批量写入,单条事务保证,或者启用监控策略,两端对数。
Memory和exec的方式可能会有数据丢失,file是 end to end 的可靠性保证的,但是性能较前两者要差。
end to end、store on failure 方式 ACK 确认时间设置过短(特别是高峰时间)也有可能引发数据的重复写入。
5 tail 断点续传的问题:
可以在 tail 传的时候记录行号,下次再传的时候,取上次记录的位置开始传输,类似:
agent1.sources.avro-source1.command =/usr/local/bin/tail -n +$(tail -n1/home/storm/tmp/n) --max-unchanged-stats=600 -F /home/storm/tmp/id.txt | awk 'ARNGIND==1{i=$0;next}{i++; if($0~/文件已截断/)i=0;print i >> "/home/storm/tmp/n";print $1"---"i}' /home/storm/tmp/n-
需要注意如下几点:
(1)文件被 rotation 的时候,需要同步更新你的断点记录“指针”,
(2)需要按文件名来追踪文件,
(3)flume 挂掉后需要累加断点续传“指针”
(4)flume 挂掉后,如果恰好文件被 rotation,那么会有丢数据的风险,
只能监控尽快拉起或者加逻辑判断文件大小重置指针。
(5)tail 注意你的版本,请更新 coreutils 包到最新。
6 在 Flume 中如何修改、丢弃、按预定义规则分类存储数据?
这里你需要利用 Flume 提供的拦截器(Interceptor)机制来满足上述的需求了
八、 Redis
1. 安装
安装必要的库:
#yum install gcc
$tar -zxvf redis3
$ make MALLOC=libc
如果你的虚拟机的时间不对,比如说是2012年
解决: date -s ‘yyyy-mm-dd hh:mm:ss’ 重写时间
再 clock-w 写入cmos
#yum install tcl
$ make test
安装:
# make PREFIX=/usr/local/redis install
make install之后,得到如下几个文件 redis-benchmark 性能测试工具 redis-check-aof 日志文件检测工(比如断电造成日志损坏,可以检测并修复) redis-check-dump 快照文件检测工具,效果类上 redis-cli 客户端 redis-server 服务端 |
拷贝配置文件:
# cp /home/hadoop/redis-3.0.6/redis.conf/usr/redis/
启动服务:
[root@localhost redis]# ./bin/redis-server./redis.conf
连接:
[hadoop@hadoop0 redis]$ ./bin/redis-cli
让redis以后台进程的形式运行 编辑conf配置文件,修改如下内容;43行 daemonize yes |
2. 命令
Redis对于key的操作命令
del key1 key2 ... Keyn
作用: 删除1个或多个键
返回值: 不存在的key忽略掉,返回真正删除的key的数量
rename key newkey
作用: 给key赋一个新的key名
注:如果newkey已存在,则newkey的原值被覆盖
renamenx key newkey
作用: 把key改名为newkey
返回: 发生修改返回1,未发生修改返回0
注: nx--> not exists, 即, newkey不存在时,作改名动作
move key db
redis127.0.0.1:6379[1]> select 2
OK
redis127.0.0.1:6379[2]> keys *
(empty list orset)
redis127.0.0.1:6379[2]> select 0
OK
redis127.0.0.1:6379> keys *
1)"name"
2) "cc"
3) "a"
4) "b"
redis127.0.0.1:6379> move cc 2
(integer) 1
redis127.0.0.1:6379> select 2
OK
redis127.0.0.1:6379[2]> keys *
1) "cc"
redis127.0.0.1:6379[2]> get cc
"3"
(注意: 一个redis进程,打开了不止一个数据库, 默认打开16个数据库,从0到15编号,
如果想打开更多数据库,可以从配置文件修改)
keys pattern 查询相应的key
在redis里,允许模糊查询key
有3个通配符 *, ? ,[]
*: 通配任意多个字符
?: 通配单个字符
[]: 通配括号内的某1个字符
redis127.0.0.1:6379> flushdb
OK
redis127.0.0.1:6379> keys *
(empty list orset)
redis127.0.0.1:6379> mset one 1 two 2 three 3 four 4
OK
redis127.0.0.1:6379> keys o*
1) "one"
redis127.0.0.1:6379> key *o
(error) ERRunknown command 'key'
redis 127.0.0.1:6379>keys *o
1) "two"
redis127.0.0.1:6379> keys ???
1) "one"
2) "two"
redis127.0.0.1:6379> keys on?
1) "one"
redis127.0.0.1:6379> set ons yes
OK
redis127.0.0.1:6379> keys on[eaw]
1) "one"
randomkey 返回随机key
exists key
判断key是否存在,返回1/0
type key
返回key存储的值的类型
有string,link,set,order set, hash
ttl key
作用: 查询key的生命周期
返回: 秒数
注:对于不存在的key或已过期的key/不过期的key,都返回-1
Redis2.8中,对于不存在的key,返回-2
expire key 整型值
作用: 设置key的生命周期,以秒为单位
同理:
pexpire key 毫秒数, 设置生命周期
pttl key, 以毫秒返回生命周期
persist key
作用: 把指定key置为永久有效
Redis字符串类型的操作
set key value [ex 秒数] / [px 毫秒数] [nx] /[xx]
如: set a 1 ex 10 , 10秒有效
Set a 1 px9000 , 9秒有效
注: 如果ex,px同时写,以后面的有效期为准
如 set a 1 ex 100 px 9000, 实际有效期是9000毫秒
nx: 表示key不存在时,执行操作
xx: 表示key存在时,执行操作
mset multi set , 一次性设置多个键值
例: mset key1 v1 key2 v2 ....
get key
作用:获取key的值
mget key1 key2 ..keyn
作用:获取多个key的值
setrange key offset value
作用:把字符串的offset偏移字节,改成value
redis127.0.0.1:6379> set greet hello
OK
redis127.0.0.1:6379> setrange greet 2 x
(integer) 5
redis127.0.0.1:6379> get greet
"hexlo"
注意: 如果偏移量>字符长度, 该字符自动补0x00
redis127.0.0.1:6379> setrange greet 6 !
(integer) 7
redis127.0.0.1:6379> get greet
"heyyo\x00!"
append key value
作用: 把value追加到key的原值上
getrange key start stop
作用: 是获取字符串中 [start, stop]范围的值
注意: 对于字符串的下标,左数从0开始,右数从-1开始
redis 127.0.0.1:6379>set title 'chinese'
OK
redis127.0.0.1:6379> getrange title 0 3
"chin"
redis127.0.0.1:6379> getrange title 1 -2
"hines"
注意:
1:start>=length, 则返回空字符串
2:stop>=length,则截取至字符结尾
3: 如果start 所处位置在stop右边, 返回空字符串
getset key newvalue
作用: 获取并返回旧值,设置新值
redis127.0.0.1:6379> set cnt 0
OK
redis127.0.0.1:6379> getset cnt 1
"0"
redis127.0.0.1:6379> getset cnt 2
"1"
incr key
作用: 指定的key的值加1,并返回加1后的值
注意:
1:不存在的key当成0,再incr操作
2: 范围为64有符号
incrby key number
redis127.0.0.1:6379> incrby age 90
(integer) 92
incrbyfloat key floatnumber
redis127.0.0.1:6379> incrbyfloat age 3.5
"95.5"
decr key
redis127.0.0.1:6379> set age 20
OK
redis127.0.0.1:6379> decr age
(integer) 19
decrby key number
redis127.0.0.1:6379> decrby age 3
(integer) 16
getbit key offset
作用:获取值的二进制表示,对应位上的值(从左,从0编号)
redis127.0.0.1:6379> set char A
OK
redis127.0.0.1:6379> getbit char 1
(integer) 1
redis127.0.0.1:6379> getbit char 2
(integer) 0
redis127.0.0.1:6379> getbit char 7
(integer) 1
setbit key offset value
设置offset对应二进制位上的值
返回: 该位上的旧值
注意:
1:如果offset过大,则会在中间填充0,
2: offset最大大到多少
3:offset最大2^32-1,可推出最大的的字符串为512M
bitop operation destkey key1 [key2 ...]
对key1,key2..keyN作operation,并将结果保存到 destkey 上。
operation 可以是 AND 、 OR 、 NOT 、 XOR
redis127.0.0.1:6379> setbit lower 7 0
(integer) 0
redis127.0.0.1:6379> setbit lower 2 1
(integer) 0
redis127.0.0.1:6379> get lower
" "
redis127.0.0.1:6379> set char Q
OK
redis127.0.0.1:6379> get char
"Q"
redis127.0.0.1:6379> bitop or char char lower
(integer) 1
redis127.0.0.1:6379> get char
"q"
注意: 对于NOT操作, key不能多个
link 链表结构
lpush key value
作用: 把值插入到链接头部
rpop key
作用: 返回并删除链表尾元素
rpush,lpop: 不解释
lrange key start stop
作用: 返回链表中[start ,stop]中的元素
规律: 左数从0开始,右数从-1开始
lrem key count value
作用: 从key链表中删除 value值
注: 删除count的绝对值个value后结束
Count>0 从表头删除
Count<0 从表尾删除
ltrim key start stop
作用: 剪切key对应的链接,切[start,stop]一段,并把该段重新赋给key
lindex key index
作用: 返回index索引上的值,
如 lindex key 2
llen key
作用:计算链接表的元素个数
redis127.0.0.1:6379> llen task
(integer) 3
redis127.0.0.1:6379>
linsert keyafter|before search value
作用: 在key链表中寻找’search’,并在search值之前|之后,.插入value
注: 一旦找到一个search后,命令就结束了,因此不会插入多个value
rpoplpush source dest
作用: 把source的尾部拿出,放在dest的头部,
并返回 该单元值
场景: task + bak 双链表完成安全队列
Task列表 bak列表
|
|
|
|
|
|
业务逻辑:
1:Rpoplpush taskbak
2:接收返回值,并做业务处理
3:如果成功,rpop bak 清除任务. 如不成功,下次从bak表里取任务
brpop ,blpop keytimeout
作用:等待弹出key的尾/头元素,
Timeout为等待超时时间
如果timeout为0,则一直等待
场景: 长轮询Ajax,在线聊天时,能够用到
Setbit 的实际应用
场景: 1亿个用户, 每个用户登陆/做任意操作 ,记为今天活跃,否则记为不活跃
每周评出: 有奖活跃用户: 连续7天活动
每月评,等等...
思路:
Userid dt active
1 2013-07-27 1
1 2013-0726 1
如果是放在表中, 1:表急剧增大,2:要用group ,sum运算,计算较慢
用: 位图法 bit-map
Log0721: ‘011001...............0’
......
log0726 : ‘011001...............0’
Log0727 : ‘0110000.............1’
1: 记录用户登陆:
每天按日期生成一个位图, 用户登陆后,把user_id位上的bit值置为1
2: 把1周的位图 and 计算,
位上为1的,即是连续登陆的用户
redis127.0.0.1:6379> setbit mon 100000000 0
(integer) 0
redis127.0.0.1:6379> setbit mon 3 1
(integer) 0
redis127.0.0.1:6379> setbit mon 5 1
(integer) 0
redis127.0.0.1:6379> setbit mon 7 1
(integer) 0
redis 127.0.0.1:6379>setbit thur 100000000 0
(integer) 0
redis127.0.0.1:6379> setbit thur 3 1
(integer) 0
redis127.0.0.1:6379> setbit thur 5 1
(integer) 0
redis127.0.0.1:6379> setbit thur 8 1
(integer) 0
redis127.0.0.1:6379> setbit wen 100000000 0
(integer) 0
redis127.0.0.1:6379> setbit wen 3 1
(integer) 0
redis127.0.0.1:6379> setbit wen 4 1
(integer) 0
redis127.0.0.1:6379> setbit wen 6 1
(integer) 0
redis127.0.0.1:6379> bitop and res mon febwen
(integer) 12500001
如上例,优点:
1: 节约空间, 1亿人每天的登陆情况,用1亿bit,约1200WByte,约10M 的字符就能表示
2: 计算方便
集合 set 相关命令
集合的性质: 唯一性,无序性,确定性
注: 在string和link的命令中,可以通过range 来访问string中的某几个字符或某几个元素
但,因为集合的无序性,无法通过下标或范围来访问部分元素.
因此想看元素,要么随机先一个,要么全选
sadd key value1 value2
作用: 往集合key中增加元素
srem value1 value2
作用: 删除集合中集为 value1 value2的元素
返回值: 忽略不存在的元素后,真正删除掉的元素的个数
spop key
作用: 返回并删除集合中key中1个随机元素
随机--体现了无序性
srandmember key
作用: 返回集合key中,随机的1个元素.
sismember key value
作用: 判断value是否在key集合中
是返回1,否返回0
smembers key
作用: 返回集中中所有的元素
scard key
作用: 返回集合中元素的个数
smove source dest value
作用:把source中的value删除,并添加到dest集合中
sinter key1 key2 key3
作用: 求出key1 key2 key3 三个集合中的交集,并返回
redis127.0.0.1:6379> sadd s1 0 2 4 6
(integer) 4
redis127.0.0.1:6379> sadd s2 1 2 3 4
(integer) 4
redis127.0.0.1:6379> sadd s3 4 8 9 12
(integer) 4
redis127.0.0.1:6379> sinter s1 s2 s3
1) "4"
redis127.0.0.1:6379> sinter s3 s1 s2
1) "4"
sinterstore dest key1 key2 key3
作用: 求出key1 key2 key3 三个集合中的交集,并赋给dest
suion key1 key2.. Keyn
作用: 求出key1 key2 keyn的并集,并返回
sdiff key1 key2 key3
作用: 求出key1与key2 key3的差集
即key1-key2-key3
order set 有序集合
zadd key score1 value1 score2 value2 ..
添加元素
redis127.0.0.1:6379> zadd stu 18 lily 19 hmm 20 lilei 21 lilei
(integer) 3
zrem key value1 value2 ..
作用: 删除集合中的元素
zremrangebyscore key min max
作用: 按照socre来删除元素,删除score在[min,max]之间的
redis127.0.0.1:6379> zremrangebyscore stu 4 10
(integer) 2
redis127.0.0.1:6379> zrange stu 0 -1
1) "f"
zremrangebyrank key start end
作用: 按排名删除元素,删除名次在[start,end]之间的
redis127.0.0.1:6379> zremrangebyrank stu 0 1
(integer) 2
redis127.0.0.1:6379> zrange stu 0 -1
1) "c"
2) "e"
3) "f"
4) "g"
zrank key member
查询member的排名(升续 0名开始)
zrevrank key memeber
查询 member的排名(降续 0名开始)
ZRANGE key start stop [WITHSCORES]
把集合排序后,返回名次[start,stop]的元素
默认是升续排列
Withscores 是把score也打印出来
zrevrange key start stop
作用:把集合降序排列,取名字[start,stop]之间的元素
zrangebyscore key minmax [withscores] limit offset N
作用: 集合(升续)排序后,取score在[min,max]内的元素,
并跳过 offset个, 取出N个
redis127.0.0.1:6379> zadd stu 1 a 3 b 4 c 9 e 12 f 15 g
(integer) 6
redis127.0.0.1:6379> zrangebyscore stu 3 12 limit 1 2 withscores
1) "c"
2) "4"
3) "e"
4) "9"
zcard key
返回元素个数
zcount key min max
返回[min,max] 区间内元素的数量
zinterstore destination numkeys key1 [key2 ...]
[WEIGHTS weight [weight ...]]
[AGGREGATE SUM|MIN|MAX]
求key1,key2的交集,key1,key2的权重分别是 weight1,weight2
聚合方法用: sum |min|max
聚合的结果,保存在dest集合内
注意: weights ,aggregate如何理解?
答: 如果有交集, 交集元素又有socre,score怎么处理?
Aggregate sum->score相加 ,min 求最小score, max 最大score
另: 可以通过weigth设置不同key的权重, 交集时,socre * weights
详见下例
redis127.0.0.1:6379> zadd z1 2 a 3 b 4 c
(integer) 3
redis127.0.0.1:6379> zadd z2 2.5 a 1 b 8 d
(integer) 3
redis 127.0.0.1:6379>zinterstore tmp 2 z1 z2
(integer) 2
redis127.0.0.1:6379> zrange tmp 0 -1
1) "b"
2) "a"
redis127.0.0.1:6379> zrange tmp 0 -1 withscores
1) "b"
2) "4"
3) "a"
4) "4.5"
redis127.0.0.1:6379> zinterstore tmp 2 z1 z2 aggregate sum
(integer) 2
redis127.0.0.1:6379> zrange tmp 0 -1 withscores
1) "b"
2) "4"
3) "a"
4) "4.5"
redis127.0.0.1:6379> zinterstore tmp 2 z1 z2 aggregate min
(integer) 2
redis127.0.0.1:6379> zrange tmp 0 -1 withscores
1) "b"
2) "1"
3) "a"
4) "2"
redis127.0.0.1:6379> zinterstore tmp 2 z1 z2 weights 1 2
(integer) 2
redis127.0.0.1:6379> zrange tmp 0 -1 withscores
1) "b"
2) "5"
3) "a"
4) "7"
Hash 哈希数据类型相关命令
hset key field value
作用: 把key中 filed域的值设为value
注:如果没有field域,直接添加,如果有,则覆盖原field域的值
hmset key field1 value1 [field2 value2 field3 value3......fieldn valuen]
作用: 设置field1->N 个域, 对应的值是value1->N
(对应PHP理解为 $key =array(file1=>value1, field2=>value2 ....fieldN=>valueN))
hget key field
作用: 返回key中field域的值
hmget key field1 field2 fieldN
作用: 返回key中field1 field2 fieldN域的值
hgetall key
作用:返回key中,所有域与其值
hdel key field
作用: 删除key中 field域
hlen key
作用: 返回key中元素的数量
hexists key field
作用: 判断key中有没有field域
hinrby key field value
作用: 是把key中的field域的值增长整型值value
hinrby float key fieldvalue
作用: 是把key中的field域的值增长浮点值value
hkeys key
作用: 返回key中所有的field
kvals key
作用: 返回key中所有的value
Redis 中的事务
Redis支持简单的事务
Redis与 mysql事务的对比
|
Mysql |
Redis |
开启 |
start transaction |
muitl |
语句 |
普通sql |
普通命令 |
失败 |
rollback 回滚 |
discard 取消 |
成功 |
commit |
exec |
注: rollback与discard 的区别
如果已经成功执行了2条语句, 第3条语句出错.
Rollback后,前2条的语句影响消失.
Discard只是结束本次事务,前2条语句造成的影响仍然还在
注:
在mutil后面的语句中, 语句出错可能有2种情况
1: 语法就有问题,
这种,exec时,报错, 所有语句得不到执行
2: 语法本身没错,但适用对象有问题. 比如 zadd 操作list对象
Exec之后,会执行正确的语句,并跳过有不适当的语句.
(如果zadd操作list这种事怎么避免? 这一点,由程序员负责)
思考:
我正在买票
Ticket -1 , money-100
而票只有1张, 如果在我multi之后,和exec之前, 票被别人买了---即ticket变成0了.
我该如何观察这种情景,并不再提交
悲观的想法:
世界充满危险,肯定有人和我抢, 给 ticket上锁, 只有我能操作. [悲观锁]
乐观的想法:
没有那么人和我抢,因此,我只需要注意,
--有没有人更改ticket的值就可以了 [乐观锁]
Redis的事务中,启用的是乐观锁,只负责监测key没有被改动.
具体的命令---- watch命令
例:
redis127.0.0.1:6379> watch ticket
OK
redis127.0.0.1:6379> multi
OK
redis127.0.0.1:6379> decr ticket
QUEUED
redis127.0.0.1:6379> decrby money 100
QUEUED
redis127.0.0.1:6379> exec
(nil) // 返回nil,说明监视的ticket已经改变了,事务就取消了.
redis127.0.0.1:6379> get ticket
"0"
redis127.0.0.1:6379> get money
"200"
watch key1key2 ... keyN
作用:监听key1 key2..keyN有没有变化,如果有变, 则事务取消
unwatch
作用: 取消所有watch监听
消息订阅
使用办法:
订阅端: Subscribe 频道名称
发布端: publish 频道名称 发布内容
客户端例子:
redis127.0.0.1:6379> subscribe news
Readingmessages... (press Ctrl-C to quit)
1)"subscribe"
2)"news"
3) (integer) 1
1) "message"
2)"news"
3) "good goodstudy"
1)"message"
2)"news"
3) "day dayup"
服务端例子:
redis127.0.0.1:6379> publish news 'good good study'
(integer) 1
redis127.0.0.1:6379> publish news 'day day up'
(integer) 1
redis持久化配置
Redis的持久化有2种方式 1快照 2是日志
Rdb快照的配置选项
save 900 1 // 900内,有1条写入,则产生快照
save 300 1000 // 如果300秒内有1000次写入,则产生快照
save 60 10000 // 如果60秒内有10000次写入,则产生快照
(这3个选项都屏蔽,则rdb禁用)
stop-writes-on-bgsave-erroryes // 后台备份进程出错时,主进程停不停止写入?
rdbcompressionyes // 导出的rdb文件是否压缩
Rdbchecksum yes // 导入rbd恢复时数据时,要不要检验rdb的完整性
dbfilenamedump.rdb //导出来的rdb文件名
dir ./ //rdb的放置路径
Aof 的配置
appendonly no # 是否打开 aof日志功能
appendfsyncalways # 每1个命令,都立即同步到aof. 安全,速度慢
appendfsynceverysec # 折衷方案,每秒写1次
appendfsyncno # 写入工作交给操作系统,由操作系统判断缓冲区大小,统一写入到aof. 同步频率低,速度快,
no-appendfsync-on-rewrite yes: # 正在导出rdb快照的过程中,要不要停止同步aof
auto-aof-rewrite-percentage100 #aof文件大小比起上次重写时的大小,增长率100%时,重写
auto-aof-rewrite-min-size64mb #aof文件,至少超过64M时,重写
注: 在dump rdb过程中,aof如果停止同步,会不会丢失?
答: 不会,所有的操作缓存在内存的队列里, dump完成后,统一操作.
注: aof重写是指什么?
答: aof重写是指把内存中的数据,逆化成命令,写入到.aof日志里.
以解决 aof日志过大的问题.
问: 如果rdb文件,和aof文件都存在,优先用谁来恢复数据?
答: aof
问: 2种是否可以同时用?
答: 可以,而且推荐这么做
问: 恢复时rdb和aof哪个恢复的快
答: rdb快,因为其是数据的内存映射,直接载入到内存,而aof是命令,需要逐条执行
redis 服务器端命令
redis127.0.0.1:6380> time ,显示服务器时间 , 时间戳(秒), 微秒数
1)"1375270361"
2)"504511"
redis127.0.0.1:6380> dbsize // 当前数据库的key的数量
(integer) 2
redis127.0.0.1:6380> select 2
OK
redis127.0.0.1:6380[2]> dbsize
(integer) 0
redis127.0.0.1:6380[2]>
BGREWRITEAOF 后台进程重写AOF
BGSAVE 后台保存rdb快照
SAVE 保存rdb快照
LASTSAVE 上次保存时间
Slaveofmaster-Host port , 把当前实例设为master的slave
Flushall 清空所有库所有键
Flushdb 清空当前库所有键
Showdown[save/nosave]
注: 如果不小心运行了flushall,立即shutdown nosave ,关闭服务器
然后 手工编辑aof文件, 去掉文件中的 “flushall ”相关行, 然后开启服务器,就可以导入回原来数据.
如果,flushall之后,系统恰好bgrewriteaof了,那么aof就清空了,数据丢失.
Slowlog 显示慢查询
注:多慢才叫慢?
答: 由slowlog-log-slower-than10000 ,来指定,(单位是微秒)
服务器储存多少条慢查询的记录?
答: 由 slowlog-max-len 128 ,来做限制
Info[Replication/CPU/Memory..]
查看redis服务器的信息
Config get 配置项
Config set 配置项 值 (特殊的选项,不允许用此命令设置,如slave-of, 需要用单独的slaveof命令来设置)
Redis运维时需要注意的参数
1: 内存
# Memory
used_memory:859192数据结构的空间
used_memory_rss:7634944实占空间
mem_fragmentation_ratio:8.89前2者的比例,1.N为佳,如果此值过大,说明redis的内存的碎片化严重,可以导出再导入一次.
2: 主从复制
# Replication
role:slave
master_host:192.168.1.128
master_port:6379
master_link_status:up
3:持久化
# Persistence
rdb_changes_since_last_save:0
rdb_last_save_time:1375224063
4: fork耗时
#Status
latest_fork_usec:936 上次导出rdb快照,持久化花费微秒
注意: 如果某实例有10G内容,导出需要2分钟,
每分钟写入10000次,导致不断的rdb导出,磁盘始处于高IO状态.
5: 慢日志
config get/setslowlog-log-slower-than
CONFIG get/SETslowlog-max-len
slowlog get N 获取慢日志
运行时更改master-slave
修改一台slave(设为A)为new master
1) 命令该服务不做其他redis服务的slave
命令: slaveof no one
2) 修改其readonly为yes
其他的slave再指向new master A
1) 命令该服务为new master A的slave
命令格式 slaveof IP port
监控工具sentinel
Sentinel不断与master通信,获取master的slave信息.
监听master与slave的状态
如果某slave失效,直接通知master去除该slave.
如果master失效,,是按照slave优先级(可配置), 选取1个slave做 new master
,把其他slave--> new master
疑问:sentinel与master通信,如果某次因为masterIO操作频繁,导致超时,
此时,认为master失效,很武断.
解决: sentnel允许多个实例看守1个master, 当N台(N可设置)sentinel都认为master失效,才正式失效.
Sentinel选项配置
port 26379 # 端口
sentinel monitor mymaster 127.0.0.1 63792 ,
给主机起的名字(不重即可),
当2个sentinel实例都认为master失效时,正式失效
sentineldown-after-milliseconds mymaster 30000 多少毫秒后连接不到master认为断开
sentinelcan-failover mymaster yes #是否允许sentinel修改slave->master. 如为no,则只能监控,无权修改./
sentinelparallel-syncs mymaster 1 , 一次性修改几个slave指向新的new master.
sentinelclient-reconfig-script mymaster /var/redis/reconfig.sh ,# 在重新配置new master,new slave过程,可以触发的脚本
redis 与关系型数据库的适合场景
书签系统
create table book (
bookid int,
title char(20)
)engine myisam charset utf8;
insert into book values
(5 , 'PHP圣经'),
(6 , 'ruby实战'),
(7 , 'mysql运维')
(8, 'ruby服务端编程');
create table tags (
tid int,
bookid int,
content char(20)
)engine myisam charset utf8;
insert into tags values
(10 , 5 , 'PHP'),
(11 , 5 , 'WEB'),
(12 , 6 , 'WEB'),
(13 , 6 , 'ruby'),
(14 , 7 , 'database'),
(15 , 8 , 'ruby'),
(16 , 8 , 'server');
# 既有web标签,又有PHP,同时还标签的书,要用连接查询
select * from tags inner join tags as t ontags.bookid=t.bookid
where tags.content='PHP' and t.content='WEB';
换成key-value存储
用kv 来存储
set book:5:title 'PHP圣经'
set book:6:title 'ruby实战'
set book:7:title 'mysql运难'
set book:8:title ‘ruby server’
sadd tag:PHP 5
sadd tag:WEB 5 6
sadd tag:database 7
sadd tag:ruby 6 8
sadd tag:SERVER 8
查: 既有PHP,又有WEB的书
Sinter tag:PHPtag:WEB #查集合的交集
查: 有PHP或有WEB标签的书
Sunin tag:PHPtag:WEB
查:含有ruby,不含WEB标签的书
Sdiff tag:rubytag:WEB #求差集
Redis key 设计技巧
1: 把表名转换为key前缀如, tag:
2: 第2段放置用于区分区key的字段--对应mysql中的主键的列名,如userid
3: 第3段放置主键值,如2,3,4...., a , b ,c
4: 第4段,写要存储的列名
用户表 user , 转换为key-value存储 |
|||
userid |
username |
passworde |
|
9 |
Lisi |
1111111 |
set user:userid:9:username lisi
set user:userid:9:password 111111
set user:userid:9:email [email protected]
keysuser:userid:9*
2 注意:
在关系型数据中,除主键外,还有可能其他列也步骤查询,
如上表中, username 也是极频繁查询的,往往这种列也是加了索引的.
转换到k-v数据中,则也要相应的生成一条按照该列为主的key-value
Set user:username:lisi:uid 9
这样,我们可以根据username:lisi:uid ,查出userid=9,
再查user:9:password/email ...
Redis配置文件
daemonize yes # redis是否以后台进程运行
Requirepass 密码 # 配置redis连接的密码
注:配置密码后,客户端连上服务器,需要先执行授权命令
# auth 密码
3. Aa
redis分布式实现原理。如何实现读写分离,在这个过程当中使用了哪些算法,有什么好处。
memcache只能说是简单的kv内存数据结构,而redis支持的数据类型比较丰富。Redis在3.0以后实现集群机制。目前Redis实现集群的方法主要是采用一致性哈稀分片(Shard),将不同的key分配到不同的redis server上,达到横向扩展的目的。
使用了一致性哈稀进行分片,那么不同的key分布到不同的Redis-Server上,当我们需要扩容时,需要增加机器到分片列表中,这时候会使得同样的key算出来落到跟原来不同的机器上,这样如果要取某一个值,会出现取不到的情况,对于这种情况,Redis的提出了一种名为Pre-Sharding的方式:
使用了redis的集群模式:存在以下几个问题
A:扩容问题:
Pre-Sharding方法是将每一个台物理机上,运行多个不同断口的Redis实例,假如有三个物理机,每个物理机运行三个Redis实际,那么我们的分片列表中实际有9个Redis实例,当我们需要扩容时,增加一台物理机,步骤如下:
1、在新的物理机上运行Redis-Server;
2、该Redis-Server从属于(slaveof)分片列表中的某一Redis-Server(假设叫RedisA);
3、等主从复制(Replication)完成后,将客户端分片列表中RedisA的IP和端口改为新物理机上Redis-Server的IP和端口;
4、停止RedisA.
B: 单点故障问题
将一个Redis-Server转移到了另外一台上。Prd-Sharding实际上是一种在线扩容的办法,但还是很依赖Redis本身的复制功能的,如果主库快照数据文件过大,这个复制的过程也会很久,同时会给主库带来压力。
九、 Stom
1. Storm-0.9.0.1 安装部署
需要依次完成的安装步骤
1). 搭建Zookeeper集群;
2). 依赖库安装
3). 下载并解压Storm发布版本;
4). 修改storm.yaml配置文件;
5). 启动Storm各个后台进程。
1.1 搭建Zookeeper集群
Storm使用Zookeeper协调集群,由于Zookeeper并不用于消息传递,所以Storm给Zookeeper带来的压力相当低。大多数情况下,单个节点的Zookeeper集群足够胜任,不过为了确保故障恢复或者部署大规模Storm集群,可能需要更大规模节点的Zookeeper集群(对于Zookeeper集群的话,官方推荐的最小节点数为3个)。在Zookeeper集群的每台机器上完成以下安装部署步骤:
1). 下载安装Java JDK,官方下载链接为http://java.sun.com/javase/downloads/index.jsp,JDK版本为JDK 6或以上。
2). 根据Zookeeper集群的负载情况,合理设置Java堆大小,尽可能避免发生swap,导致Zookeeper性能下降。保守起见,4GB内存的机器可以为Zookeeper分配3GB最大堆空间。
3). 下载后解压安装Zookeeper包,官方下载链接为http://hadoop.apache.org/zookeeper/releases.html。
4). 根据Zookeeper集群节点情况,在conf目录下创建Zookeeper配置文件zoo.cfg:
tickTime=2000
dataDir=/var/zookeeper/
clientPort=2181
initLimit=5
syncLimit=2
server.1=zookeeper1:2888:3888
server.2=zookeeper2:2888:3888
server.3=zookeeper3:2888:3888
5). 在dataDir目录下创建myid文件,文件中只包含一行,且内容为该节点对应的server.id中的id编号。其中,dataDir指定Zookeeper的数据文件目录;其中server.id=host:port:port,id是为每个Zookeeper节点的编号,保存在dataDir目录下的myid文件中,zookeeper1~zookeeper3表示各个Zookeeper节点的hostname,第一个port是用于连接leader的端口,第二个port是用于leader选举的端口。
6). 启动Zookeeper服务:
bin/zkServer.sh start
7). 通过Zookeeper客户端测试服务是否可用:
bin/zkCli.sh -server 127.0.0.1:2181
1.2 依赖库安装
这里的Storm依赖库有python、以及JDK两个,这两个的安装相对比较简单,所以在这里就不提了!
1.3 下载并解压Storm发布版本
Storm0.9.0.1版本提供了两种形式的压缩包:zip和tar.gz
我们下载tar.gz格式的,这样可以免去uzip的安装
下载路径:https://dl.dropboxusercontent.co ... torm-0.9.0.1.tar.gz
解压命令
tar -zxvf storm-0.9.0.1.tar.gz
1.4 下载并解压Storm发布版本
Storm发行版本解压目录下有一个conf/storm.yaml文件,用于配置Storm。默认配置在这里可以查看。conf/storm.yaml中的配置选项将覆盖defaults.yaml中的默认配置。以下配置选项是必须在conf/storm.yaml中进行配置的:
1) storm.zookeeper.servers:Storm集群使用的Zookeeper集群地址,其格式如下:
storm.zookeeper.servers:
-“111.222.333.444″
-“555.666.777.888″
如果Zookeeper集群使用的不是默认端口,那么还需要storm.zookeeper.port选项。
2) storm.local.dir:Nimbus和Supervisor进程用于存储少量状态,如jars、confs等的本地磁盘目录,需要提前创建该目录并给以足够的访问权限。然后在storm.yaml中配置该目录,如:
storm.local.dir: "/home/admin/storm/workdir"
3) nimbus.host:Storm集群Nimbus机器地址,各个Supervisor工作节点需要知道哪个机器是Nimbus,以便下载Topologies的jars、confs等文件,如:
01.nimbus.host: "111.222.333.444"
4) supervisor.slots.ports: 对于每个Supervisor工作节点,需要配置该工作节点可以运行的worker数量。每个worker占用一个单独的端口用于接收消息,该配置选项即用于定义哪些端口是可被worker使用的。默认情况下,每个节点上可运行4个workers,分别在6700、6701、6702和6703端口,如:
supervisor.slots.ports:
-6700
-6701
-6702
-6703
1.5 启动Storm各个后台进程
最后一步,启动Storm的所有后台进程。和Zookeeper一样,Storm也是快速失败(fail-fast)的系统,这样Storm才能在任意时刻被停止,并且当进程重启后被正确地恢复执行。这也是为什么Storm不在进程内保存状态的原因,即使Nimbus或Supervisors被重启,运行中的Topologies不会受到影响。
以下是启动Storm各个后台进程的方式:
Nimbus: 在Storm主控节点上运行”bin/stormnimbus >/dev/null 2>&1 &”启动Nimbus后台程序,并放到后台执行;
Supervisor: 在Storm各个工作节点上运行”bin/stormsupervisor>/dev/null 2>&1 &”启动Supervisor后台程序,并放到后台执行;
UI: 在Storm主控节点上运行”bin/stormui >/dev/null 2>&1 &”启动UI后台程序,并放到后台执行,启动后可以通过http://{nimbushost}:8080观察集群的worker资源使用情况、Topologies的运行状态等信息。
logview:在Storm主节点上运行"bin/stormlogviewer > /dev/null 2>&1"启动logviewer后台程序,并放到后台执行。
注意事项:
启动Storm后台进程时,需要对conf/storm.yaml配置文件中设置的storm.local.dir目录具有写权限。
Storm后台进程被启动后,将在Storm安装部署目录下的logs/子目录下生成各个进程的日志文件。
经测试,Storm UI必须和Storm Nimbus部署在同一台机器上,否则UI无法正常工作,因为UI进程会检查本机是否存在Nimbus链接。
为了方便使用,可以将bin/storm加入到系统环境变量中。
至此,Storm集群已经部署、配置完毕,可以向集群提交拓扑运行了。
接下来我们检查下环境的运行情况:--使用jps检查守护进程运行状况
zqgame@kickseed:/data/storm/zookeeper-3.4.5/bin$ jps
20420 nimbus
20623 logviewer
20486 supervisor
20319 core
21755 Jps
查看运行页面如下
2. 结构
storm中如何实现统计uv的不重复。
storm主要是通过Transactionaltopology,确保每次tuple只被处理一次。给每个tuple按顺序加一个id,在处理过程中,将成功处理的tuple id和计算保存在数据库当中,但是这种机制使得系统一次只能处理一个tuple,无法实现分布式计算。
我们要保证一个batch只被处理一次,机制和上一节类似。只不过数据库中存储的是batch id。batch的中间计算结果先存在局部变量中,当一个batch中的所有tuple都被处理完之后,判断batch id,如果跟数据库中的id不同,则将中间计算结果更新到数据库中。
Storm提供的TransactionalTopology将batch计算分为process和commit两个阶段。Process阶段可以同时处理多个batch,不用保证顺序性;commit阶段保证batch的强顺序性,并且一次只能处理一个batch,第1个batch成功提交之前,第2个batch不能被提交。这样就保证多线程情况下值能处理一个batch。
3.Storm特点
Storm是一个开源的分布式实时计算系统,可以简单、可靠的处理大量的数据流。Storm有很多使用场景:如实时分析,在线机器学习,持续计算,分布式RPC,ETL等等。Storm支持水平扩展,具有高容错性,保证每个消息都会得到处理,而且处理速度很快(在一个小集群中,每个结点每秒可以处理数以百万计的消息)。Storm的部署和运维都很便捷,而且更为重要的是可以使用任意编程语言来开发应用。
Storm有如下特点:
编程模型简单
在大数据处理方面相信大家对hadoop已经耳熟能详,基于Google Map/Reduce来实现的Hadoop为开发者提供了map、reduce原语,使并行批处理程序变得非常地简单和优美。同样,Storm也为大数据的实时计算提供了一些简单优美的原语,这大大降低了开发并行实时处理的任务的复杂性,帮助你快速、高效的开发应用。
可扩展
在Storm集群中真正运行topology的主要有三个实体:工作进程、线程和任务。Storm集群中的每台机器上都可以运行多个工作进程,每个工作进程又可创建多个线程,每个线程可以执行多个任务,任务是真正进行数据处理的实体,我们开发的spout、bolt就是作为一个或者多个任务的方式执行的。
因此,计算任务在多个线程、进程和服务器之间并行进行,支持灵活的水平扩展。
高可靠性
Storm可以保证spout发出的每条消息都能被“完全处理”,这也是直接区别于其他实时系统的地方,如S4。
请注意,spout发出的消息后续可能会触发产生成千上万条消息,可以形象的理解为一棵消息树,其中spout发出的消息为树根,Storm会跟踪这棵消息树的处理情况,只有当这棵消息树中的所有消息都被处理了,Storm才会认为spout发出的这个消息已经被“完全处理”。如果这棵消息树中的任何一个消息处理失败了,或者整棵消息树在限定的时间内没有“完全处理”,那么spout发出的消息就会重发。
考虑到尽可能减少对内存的消耗,Storm并不会跟踪消息树中的每个消息,而是采用了一些特殊的策略,它把消息树当作一个整体来跟踪,对消息树中所有消息的唯一id进行异或计算,通过是否为零来判定spout发出的消息是否被“完全处理”,这极大的节约了内存和简化了判定逻辑,后面会对这种机制进行详细介绍。
这种模式,每发送一个消息,都会同步发送一个ack/fail,对于网络的带宽会有一定的消耗,如果对于可靠性要求不高,可通过使用不同的emit接口关闭该模式。
上面所说的,Storm保证了每个消息至少被处理一次,但是对于有些计算场合,会严格要求每个消息只被处理一次,幸而Storm的0.7.0引入了事务性拓扑,解决了这个问题,后面会有详述。
高容错性
如果在消息处理过程中出了一些异常,Storm会重新安排这个出问题的处理单元。Storm保证一个处理单元永远运行(除非你显式杀掉这个处理单元)。
当然,如果处理单元中存储了中间状态,那么当处理单元重新被Storm启动的时候,需要应用自己处理中间状态的恢复。
支持多种编程语言
除了用java实现spout和bolt,你还可以使用任何你熟悉的编程语言来完成这项工作,这一切得益于Storm所谓的多语言协议。多语言协议是Storm内部的一种特殊协议,允许spout或者bolt使用标准输入和标准输出来进行消息传递,传递的消息为单行文本或者是json编码的多行。
Storm支持多语言编程主要是通过ShellBolt,ShellSpout和ShellProcess这些类来实现的,这些类都实现了IBolt 和 ISpout接口,以及让shell通过java的ProcessBuilder类来执行脚本或者程序的协议。
可以看到,采用这种方式,每个tuple在处理的时候都需要进行json的编解码,因此在吞吐量上会有较大影响。
支持本地模式
Storm有一种“本地模式”,也就是在进程中模拟一个Storm集群的所有功能,以本地模式运行topology跟在集群上运行topology类似,这对于我们开发和测试来说非常有用。
高效
用ZeroMQ作为底层消息队列, 保证消息能快速被处理
十、 Lucene
1、lucene 简介
表示了搜索应用程序和 Lucene 之间的关系,也反映了利用 Lucene 构建搜索应用程序的流程:
1.1 什么是lucene
Lucene是一个全文搜索框架,而不是应用产品。因此它并不像www.baidu.com 或者google Desktop那么拿来就能用,它只是提供了一种工具让你能实现这些产品。
2 lucene的工作方式
lucene提供的服务实际包含两部分:一入一出。所谓入是写入,即将你提供的源(本质是字符串)写入索引或者将其从索引中删除;所谓出是读出,即向用户提供全文搜索服务,让用户可以通过关键词定位源。
2.1写入流程
源字符串首先经过analyzer处理,包括:分词,分成一个个单词;去除stopword(可选)。
将源中需要的信息加入Document的各个Field中,并把需要索引的Field索引起来,把需要存储的Field存储起来。
将索引写入存储器,存储器可以是内存或磁盘。
2.2读出流程
用户提供搜索关键词,经过analyzer处理。
对处理后的关键词搜索索引找出对应的Document。
用户根据需要从找到的Document中提取需要的Field。
3 一些需要知道的概念
3.1 analyzer
Analyzer是分析器,它的作用是把一个字符串按某种规则划分成一个个词语,并去除其中的无效词语,这里说的无效词语是指英文中的“of”、“the”,中文中的“的”、“地”等词语,这些词语在文章中大量出现,但是本身不包含什么关键信息,去掉有利于缩小索引文件、提高效率、提高命中率。
分词的规则千变万化,但目的只有一个:按语义划分。这点在英文中比较容易实现,因为英文本身就是以单词为单位的,已经用空格分开;而中文则必须以某种方法将连成一片的句子划分成一个个词语。具体划分方法下面再详细介绍,这里只需了解分析器的概念即可。
3.2 document
用户提供的源是一条条记录,它们可以是文本文件、字符串或者数据库表的一条记录等等。一条记录经过索引之后,就是以一个Document的形式存储在索引文件中的。用户进行搜索,也是以Document列表的形式返回。
3.3 field
一个Document可以包含多个信息域,例如一篇文章可以包含“标题”、“正文”、“最后修改时间”等信息域,这些信息域就是通过Field在Document中存储的。
Field有两个属性可选:存储和索引。通过存储属性你可以控制是否对这个Field进行存储;通过索引属性你可以控制是否对该Field进行索引。这看起来似乎有些废话,事实上对这两个属性的正确组合很重要,下面举例说明:还是以刚才的文章为例子,我们需要对标题和正文进行全文搜索,所以我们要把索引属性设置为真,同时我们希望能直接从搜索结果中提取文章标题,所以我们把标题域的存储属性设置为真,但是由于正文域太大了,我们为了缩小索引文件大小,将正文域的存储属性设置为假,当需要时再直接读取文件;我们只是希望能从搜索解果中提取最后修改时间,不需要对它进行搜索,所以我们把最后修改时间域的存储属性设置为真,索引属性设置为假。上面的三个域涵盖了两个属性的三种组合,还有一种全为假的没有用到,事实上Field不允许你那么设置,因为既不存储又不索引的域是没有意义的。
3.4 term
term是搜索的最小单位,它表示文档的一个词语,term由两部分组成:它表示的词语和这个词语所出现的field。
3.5 tocken
tocken是term的一次出现,它包含trem文本和相应的起止偏移,以及一个类型字符串。一句话中可以出现多次相同的词语,它们都用同一个term表示,但是用不同的tocken,每个tocken标记该词语出现的地方。
3.6 segment
添加索引时并不是每个document都马上添加到同一个索引文件,它们首先被写入到不同的小文件,然后再合并成一个大索引文件,这里每个小文件都是一个segment。
4 如何建索引
4.1 最简单的能完成索引的代码片断
IndexWriter writer = newIndexWriter(“/data/index/”, new StandardAnalyzer(), true);
Document doc = new Document();
doc.add(new Field("title","lucene introduction", Field.Store.YES, Field.Index.TOKENIZED));
doc.add(new Field("content","lucene works well", Field.Store.YES, Field.Index.TOKENIZED));
writer.addDocument(doc);
writer.optimize();
writer.close();
下面我们分析一下这段代码。
首先我们创建了一个writer,并指定存放索引的目录为“/data/index”,使用的分析器为StandardAnalyzer,第三个参数说明如果已经有索引文件在索引目录下,我们将覆盖它们。然后我们新建一个document。
我们向document添加一个field,名字是“title”,内容是“lucene introduction”,对它进行存储并索引。再添加一个名字是“content”的field,内容是“lucene works well”,也是存储并索引。
然后我们将这个文档添加到索引中,如果有多个文档,可以重复上面的操作,创建document并添加。
添加完所有document,我们对索引进行优化,优化主要是将多个segment合并到一个,有利于提高索引速度。
随后将writer关闭,这点很重要。
对,创建索引就这么简单!
当然你可能修改上面的代码获得更具个性化的服务。
4.2 索引文本文件
如果你想把纯文本文件索引起来,而不想自己将它们读入字符串创建field,你可以用下面的代码创建field:
Field field = newField("content", new FileReader(file));
这里的file就是该文本文件。该构造函数实际上是读去文件内容,并对其进行索引,但不存储。
Lucene 2 教程
Lucene是apache组织的一个用java实现全文搜索引擎的开源项目。 其功能非常的强大,api也很简单。总得来说用Lucene来进行建立 和搜索和操作数据库是差不多的(有点像),Document可以看作是 数据库的一行记录,Field可以看作是数据库的字段。用lucene实 现搜索引擎就像用JDBC实现连接数据库一样简单。
例子一:
1、在windows系统下的的C盘,建一个名叫s的文件夹,在该文件夹里面随便建三个txt文件,随便起名啦,就叫"1.txt","2.txt"和"3.txt"啦
其中1.txt的内容如下:
中华人民共和国
全国人民
2006年
而"2.txt"和"3.txt"的内容也可以随便写几写,这里懒写,就复制一个和1.txt文件的内容一样吧
2、下载lucene包,放在classpath路径中
建立索引:
package lighter.javaeye.com;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Date;
import org.apache.lucene.analysis.Analyzer;
importorg.apache.lucene.analysis.standard.StandardAnalyzer;
importorg.apache.lucene.document.Document;
importorg.apache.lucene.document.Field;
importorg.apache.lucene.index.IndexWriter;
/** */ /**
*author lighter date 2006-8-7
*/
public class TextFileIndexer {
public static void main(String[] args) throwsException {
/**/ /* 指明要索引文件夹的位置,这里是C盘的S文件夹下 */
File fileDir = new File( " c:\\s " );
/**/ /* 这里放索引文件的位置 */
File indexDir = new File( " c:\\index " );
Analyzer luceneAnalyzer = new StandardAnalyzer();
IndexWriter indexWriter = new IndexWriter(indexDir, luceneAnalyzer,
true );
File[] textFiles = fileDir.listFiles();
long startTime = newDate().getTime();
// 增加document到索引去
for ( int i = 0 ; i < textFiles.length; i ++ ) {
if (textFiles[i].isFile()
&& textFiles[i].getName().endsWith( " .txt" )) {
System.out.println("File " + textFiles[i].getCanonicalPath()
+ "正在被索引. " );
String temp = FileReaderAll(textFiles[i].getCanonicalPath(),
" GBK ");
System.out.println(temp);
Document document = new Document();
Field FieldPath = new Field( " path ", textFiles[i].getPath(),
Field.Store.YES,Field.Index.NO);
Field FieldBody = new Field( " body ", temp, Field.Store.YES,
Field.Index.TOKENIZED,
Field.TermVector.WITH_POSITIONS_OFFSETS);
document.add(FieldPath);
document.add(FieldBody);
indexWriter.addDocument(document);
}
}
// optimize()方法是对索引进行优化
indexWriter.optimize();
indexWriter.close();
// 测试一下索引的时间
long endTime = new Date().getTime();
System.out
.println(" 这花费了"
+ (endTime - startTime)
+ " 毫秒来把文档增加到索引里面去! "
+ fileDir.getPath());
}
public static StringFileReaderAll(String FileName, String charset)
throws IOException {
BufferedReader reader = new BufferedReader( new InputStreamReader(
new FileInputStream(FileName), charset));
String line = new String();
String temp = new String();
while ((line = reader.readLine()) != null) {
temp += line;
}
reader.close();
return temp;
}
}
索引的结果:
File C:\s\ 1 .txt正在被索引.
中华人民共和国全国人民2006年
File C:\s\ 2 .txt正在被索引.
中华人民共和国全国人民2006年
File C:\s\ 3 .txt正在被索引.
中华人民共和国全国人民2006年
这花费了297 毫秒来把文档增加到索引里面去 ! c:\s
3、建立了索引之后,查询啦....
package lighter.javaeye.com;
import java.io.IOException;
importorg.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.queryParser.ParseException;
importorg.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.Hits;
importorg.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
public class TestQuery {
public static void main(String[] args) throwsIOException, ParseException {
Hits hits = null ;
String queryString = " 中华";
Query query = null ;
IndexSearcher searcher = new IndexSearcher( " c:\\index " );
Analyzer analyzer = new StandardAnalyzer();
try {
QueryParser qp = new QueryParser( " body ",analyzer);
query = qp.parse(queryString);
} catch (ParseException e) {
}
if (searcher != null ) {
hits = searcher.search(query);
if (hits.length() > 0 ) {
System.out.println(" 找到:" + hits.length() + " 个结果! " );
}
}
}
}
其运行结果:
找到: 3 个结果!
Lucene 其实很简单的,它最主要就是做两件事:建立索引和进行搜索
来看一些在lucene中使用的术语,这里并不打算作详细的介绍,只是点一下而已----因为这一个世界有一种好东西,叫搜索。
IndexWriter:lucene中最重要的的类之一,它主要是用来将文档加入索引,同时控制索引过程中的一些参数使用。
Analyzer:分析器,主要用于分析搜索引擎遇到的各种文本。常用的有StandardAnalyzer分析器,StopAnalyzer分析器,WhitespaceAnalyzer分析器等。
Directory:索引存放的位置;lucene提供了两种索引存放的位置,一种是磁盘,一种是内存。一般情况将索引放在磁盘上;相应地lucene提供了FSDirectory和RAMDirectory两个类。
Document:文档;Document相当于一个要进行索引的单元,任何可以想要被索引的文件都必须转化为Document对象才能进行索引。
Field:字段。
IndexSearcher:是lucene中最基本的检索工具,所有的检索都会用到IndexSearcher工具;
Query:查询,lucene中支持模糊查询,语义查询,短语查询,组合查询等等,如有TermQuery,BooleanQuery,RangeQuery,WildcardQuery等一些类。
QueryParser: 是一个解析用户输入的工具,可以通过扫描用户输入的字符串,生成Query对象。
Hits:在搜索完成之后,需要把搜索结果返回并显示给用户,只有这样才算是完成搜索的目的。在lucene中,搜索的结果的集合是用Hits类的实例来表示的。
上面作了一大堆名词解释,下面就看几个简单的实例吧:
1、简单的的StandardAnalyzer测试例子
package lighter.javaeye.com;
import java.io.IOException;
import java.io.StringReader;
importorg.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.Token;
importorg.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
public class StandardAnalyzerTest
{
//构造函数,
public StandardAnalyzerTest()
{
}
public static void main(String[] args)
{
// 生成一个StandardAnalyzer对象
Analyzer aAnalyzer = new StandardAnalyzer();
// 测试字符串
StringReader sr = new StringReader( "lighter javaeye com isthe are on ");
// 生成TokenStream对象
TokenStream ts = aAnalyzer.tokenStream( " name ", sr);
try {
int i = 0 ;
Token t = ts.next();
while (t != null )
{
// 辅助输出时显示行号
i++ ;
// 输出处理后的字符
System.out.println(" 第 " + i+ " 行: " + t.termText());
// 取得下一个字符
t= ts.next();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
显示结果:
第1行:lighter
第2行:javaeye
第3行:com
提示一下:
StandardAnalyzer是lucene中内置的"标准分析器",可以做如下功能:
1、对原有句子按照空格进行了分词
2、所有的大写字母都可以能转换为小写的字母
3、可以去掉一些没有用处的单词,例如"is","the","are"等单词,也删除了所有的标点
查看一下结果与"newStringReader("lighter javaeye com is the areon")"作一个比较就清楚明了。
这里不对其API进行解释了,具体见lucene的官方文档。需要注意一点,这里的代码使用的是lucene2的API,与1.43版有一些明显的差别。
2、看另一个实例,简单地建立索引,进行搜索
package lighter.javaeye.com;
importorg.apache.lucene.analysis.standard.StandardAnalyzer;
importorg.apache.lucene.document.Document;
importorg.apache.lucene.document.Field;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.Hits;
importorg.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.store.FSDirectory;
public class FSDirectoryTest {
// 建立索引的路径
public static final String path = " c:\\index2 ";
public static void main(String[] args) throws Exception {
Document doc1 = new Document();
doc1.add( new Field( " name" , "lighter javaeye com" ,Field.Store.YES,Field.Index.TOKENIZED));
Document doc2 = new Document();
doc2.add(new Field( " name" , " lighter blog",Field.Store.YES,Field.Index.TOKENIZED));
IndexWriter writer = new IndexWriter(FSDirectory.getDirectory(path, true), new StandardAnalyzer(), true );
writer.setMaxFieldLength(3 );
writer.addDocument(doc1);
writer.setMaxFieldLength(3 );
writer.addDocument(doc2);
writer.close();
IndexSearcher searcher = new IndexSearcher(path);
Hits hits = null ;
Query query = null ;
QueryParser qp = new QueryParser( " name " , newStandardAnalyzer());
query = qp.parse( " lighter ");
hits = searcher.search(query);
System.out.println(" 查找\ " lighter\ " 共 " + hits.length() + " 个结果 " );
query = qp.parse( " javaeye" );
hits = searcher.search(query);
System.out.println(" 查找\ " javaeye\ " 共 " + hits.length() + " 个结果 " );
}
}
运行结果:
查找 " lighter " 共2个结果
查找 " javaeye " 共1个结果
到现在我们已经可以用lucene建立索引了
下面介绍一下几个功能来完善一下:
1.索引格式
其实索引目录有两种格式,
一种是除配置文件外,每一个Document独立成为一个文件(这种搜索起来会影响速度)。
另一种是全部的Document成一个文件,这样属于复合模式就快了。
2.索引文件可放的位置:
索引可以存放在两个地方1.硬盘,2.内存
放在硬盘上可以用FSDirectory(),放在内存的用RAMDirectory()不过一关机就没了
FSDirectory.getDirectory(File file,boolean create)
FSDirectory.getDirectory(String path,boolean create)
两个工厂方法返回目录
New RAMDirectory()就直接可以
再和
IndexWriter(Directory d, Analyzer a,boolean create)
一配合就行了
如:
IndexWrtier indexWriter = new IndexWriter(FSDirectory.getDirectory(“c:\\index”, true ), newStandardAnlyazer(), true );
IndexWrtier indexWriter = new IndexWriter( new RAMDirectory(), new StandardAnlyazer(),true );
3.索引的合并
这个可用
IndexWriter.addIndexes(Directory[] dirs)
将目录加进去
来看个例子:
public void UniteIndex() throws IOException
{
IndexWriter writerDisk = new IndexWriter(FSDirectory.getDirectory( " c:\\indexDisk" , true ), new StandardAnalyzer(), true );
Document docDisk = new Document();
docDisk.add(new Field( "name " , " 程序员之家 " ,Field.Store.YES,Field.Index.TOKENIZED));
writerDisk.addDocument(docDisk);
RAMDirectory ramDir = new RAMDirectory();
IndexWriter writerRam = new IndexWriter(ramDir, new StandardAnalyzer(), true );
Document docRam = new Document();
docRam.add(new Field( " name" , " 程序员杂志 " ,Field.Store.YES,Field.Index.TOKENIZED));
writerRam.addDocument(docRam);
writerRam.close();// 这个方法非常重要,是必须调用的
writerDisk.addIndexes(new Directory[] {ramDir} );
writerDisk.close();
}
public void UniteSearch() throws ParseException, IOException
{
QueryParser queryParser = new QueryParser( " name" , new StandardAnalyzer());
Query query = queryParser.parse(" 程序员 " );
IndexSearcher indexSearcher = new IndexSearcher( " c:\\indexDisk " );
Hits hits = indexSearcher.search(query);
System.out.println(" 找到了 " + hits.length() + " 结果 " );
for ( int i = 0 ;i
{
Document doc = hits.doc(i);
System.out.println(doc.get(" name " ));
}
}
这个例子是将内存中的索引合并到硬盘上来.
注意:合并的时候一定要将被合并的那一方的IndexWriter的close()方法调用。
4.对索引的其它操作:
IndexReader类是用来操作索引的,它有对Document,Field的删除等操作。
下面一部分的内容是:全文的搜索
全文的搜索主要是用:IndexSearcher,Query,Hits,Document(都是Query的子类),有的时候用QueryParser
主要步骤:
1 . new QueryParser(Field字段, new 分析器)
2 .Query query = QueryParser.parser(“要查询的字串”);这个地方我们可以用反射api看一下query究竟是什么类型
3 . new IndexSearcher(索引目录).search(query);返回Hits
4 .用Hits.doc(n);可以遍历出Document
5 .用Document可得到Field的具体信息了。
其实1 ,2两步就是为了弄出个Query 实例,究竟是什么类型的看分析器了。
拿以前的例子来说吧
QueryParser queryParser = new QueryParser( " name" , new StandardAnalyzer());
Query query = queryParser.parse(" 程序员 " );
/**/ /* 这里返回的就是org.apache.lucene.search.PhraseQuery*/
IndexSearcher indexSearcher = new IndexSearcher( " c:\\indexDisk " );
Hits hits = indexSearcher.search(query);
不管是什么类型,无非返回的就是Query的子类,我们完全可以不用这两步直接new个Query的子类的实例就ok了,不过一般还是用这两步因为它返回的是PhraseQuery这个是非常强大的query子类它可以进行多字搜索用QueryParser可以设置各个关键字之间的关系这个是最常用的了。
IndexSearcher:
其实IndexSearcher它内部自带了一个IndexReader用来读取索引的,IndexSearcher有个close()方法,这个方法不是用来关闭IndexSearche的是用来关闭自带的IndexReader。
QueryParser呢可以用parser.setOperator()来设置各个关键字之间的关系(与还是或)它可以自动通过空格从字串里面将关键字分离出来。
注意:用QueryParser搜索的时候分析器一定的和建立索引时候用的分析器是一样的。
Query:
可以看一个lucene2.0的帮助文档有很多的子类:
BooleanQuery, ConstantScoreQuery,ConstantScoreRangeQuery, DisjunctionMaxQuery,FilteredQuery, MatchAllDocsQuery,MultiPhraseQuery, MultiTermQuery,PhraseQuery, PrefixQuery, RangeQuery, SpanQuery,TermQuery
各自有用法看一下文档就能知道它们的用法了
下面一部分讲一下lucene的分析器:
分析器是由分词器和过滤器组成的,拿英文来说吧分词器就是通过空格把单词分开,过滤器就是把the,to,of等词去掉不被搜索和索引。
我们最常用的是StandardAnalyzer()它是lucene的标准分析器它集成了内部的许多的分析器。
最后一部分了:lucene的高级搜索了
1.排序
Lucene有内置的排序用IndexSearcher.search(query,sort)但是功能并不理想。我们需要自己实现自定义的排序。
这样的话得实现两个接口: ScoreDocComparator,SortComparatorSource
用IndexSearcher.search(query,newSort(new SortField(StringField,SortComparatorSource)));
就看个例子吧:
这是一个建立索引的例子:
public void IndexSort() throws IOException
{
IndexWriter writer = new IndexWriter( " C:\\indexStore " ,new StandardAnalyzer(), true );
Document doc = new Document()
doc.add(new Field( " sort" , " 1 ",Field.Store.YES,Field.Index.TOKENIZED));
writer.addDocument(doc);
doc = new Document();
doc.add(new Field( " sort" , " 4 ",Field.Store.YES,Field.Index.TOKENIZED));
writer.addDocument(doc);
doc = new Document();
doc.add(new Field( " sort" , " 3 ",Field.Store.YES,Field.Index.TOKENIZED));
writer.addDocument(doc);
doc = new Document();
doc.add(new Field( " sort" , " 5 ",Field.Store.YES,Field.Index.TOKENIZED));
writer.addDocument(doc);
doc = new Document();
doc.add(new Field( " sort" , " 9 ",Field.Store.YES,Field.Index.TOKENIZED));
writer.addDocument(doc);
doc = new Document();
doc.add(new Field( " sort" , " 6 " ,Field.Store.YES,Field.Index.TOKENIZED));
writer.addDocument(doc);
doc = new Document();
doc.add(new Field( " sort" , " 7 ",Field.Store.YES,Field.Index.TOKENIZED));
writer.addDocument(doc);
writer.close();
}
下面是搜索的例子:
[code]
public void SearchSort1() throwsIOException, ParseException
{
IndexSearcher indexSearcher =newIndexSearcher("C:\\indexStore");
QueryParser queryParser = newQueryParser("sort",newStandardAnalyzer());
Query query =queryParser.parse("4");
Hits hits =indexSearcher.search(query);
System.out.println("有"+hits.length()+"个结果");
Document doc = hits.doc(0);
System.out.println(doc.get("sort"));
}
public void SearchSort2() throwsIOException, ParseException
{
IndexSearcher indexSearcher = newIndexSearcher("C:\\indexStore");
Query query = newRangeQuery(newTerm("sort","1"),newTerm("sort","9"),true);//这个地方前面没有提到,它是用于范围的Query可以看一下帮助文档.
Hits hits =indexSearcher.search(query,new Sort(newSortField("sort",newMySortComparatorSource())));
System.out.println("有"+hits.length()+"个结果");
for(int i=0;i
{
Document doc= hits.doc(i);
System.out.println(doc.get("sort"));
}
}
public class MyScoreDocComparatorimplements ScoreDocComparator
{
private Integer[]sort;
public MyScoreDocComparator(String s,IndexReader reader,Stringfieldname) throws IOException
{
sort = new Integer[reader.maxDoc()];
for(int i = 0;i
{
Document doc=reader.document(i);
sort[i]=newInteger(doc.get("sort"));
}
}
public int compare(ScoreDoc i, ScoreDoc j)
{
if(sort[i.doc]>sort[j.doc])
return 1;
if(sort[i.doc]
return -1;
return 0;
}
public int sortType()
{
return SortField.INT;
}
public Comparable sortValue(ScoreDoc i)
{
// TODO 自动生成方法存根
return new Integer(sort[i.doc]);
}
}
public class MySortComparatorSourceimplements SortComparatorSource
{
private static final long serialVersionUID =-9189690812107968361L;
public ScoreDocComparator newComparator(IndexReader reader,Stringfieldname)
throwsIOException
{
if(fieldname.equals("sort"))
return newMyScoreDocComparator("sort",reader,fieldname);
return null;
}
}[/code]
SearchSort1()输出的结果没有排序,SearchSort2()就排序了。
2.多域搜索MultiFieldQueryParser
如果想输入关键字而不想关心是在哪个Field里的就可以用MultiFieldQueryParser了
用它的构造函数即可后面的和一个Field一样。
MultiFieldQueryParser. parse(String[] queries,String[] fields,BooleanClause.Occur[] flags, Analyzeranalyzer) ~~~~~~~~~~~~~~~~~
第三个参数比较特殊这里也是与以前lucene1.4.3不一样的地方
看一个例子就知道了
String[] fields = {"filename","contents", "description"};
BooleanClause.Occur[] flags ={BooleanClause.Occur.SHOULD,
BooleanClause.Occur.MUST,//在这个Field里必须出现的
BooleanClause.Occur.MUST_NOT};//在这个Field里不能出现
MultiFieldQueryParser.parse("query",fields, flags, analyzer);
1、lucene的索引不能太大,要不然效率会很低。大于1G的时候就必须考虑分布索引的问题
2、不建议用多线程来建索引,产生的互锁问题很麻烦。经常发现索引被lock,无法重新建立的情况
3、中文分词是个大问题,目前免费的分词效果都很差。如果有能力还是自己实现一个分词模块,用最短路径的切分方法,网上有教材和demo源码,可以参考。
4、建增量索引的时候很耗cpu,在访问量大的时候会导致cpu的idle为0
5、默认的评分机制不太合理,需要根据自己的业务定制
整体来说lucene要用好不容易,必须在上述方面扩充他的功能,才能作为一个商用的搜索引擎
十一、 Spark
1.介绍
Spark概览
Spark 是一个通用的大规模数据快速处理引擎。可以简单理解为 Spark 就是一个大数据分布式处理框架。
Spark是基于map reduce算法实现的分布式计算框架,但不同的是Spark的中间输出和结果输出可以保存在内存中,从而不再需要读写HDFS,因此Spark能更好地用于数据挖掘与机器学习等需要迭代的map reduce的算法中。
Spark生态系统BDAS
伯克利将Spark的整个生态系统称为伯克利数据分析栈(BDAS)。其核心框架是Spark,同时BDAS涵盖支持结构化数据SQL查询与分析的查询引擎Spark SQL,提供机器学习功能的系统MLbase及底层的分布式机器学习库MLlib、并行图计算框架GraphX,流计算框架Spark Streaming、采样近似计算查询引擎BlinkDB、内存分布式文件系统Tachyon、资源管理框架Mesos等子项目。这些子项目在Spark上层提供了更高层、更丰富的计算范式。
(1)Spark
Spark是整个BDAS的核心组件,是一个大数据分布式编程框架,不仅实现了MapReduce的算子map 函数和reduce函数及计算模型,还提供更为丰富的算子,如filter、join、groupByKey等。Spark将分布式数据抽象为弹性分布式数据集(RDD),实现了应用任务调度、RPC、序列化和压缩,并为运行在其上的上层组件提供API。其底层采用Scala这种函数式语言书写而成,并且所提供的API深度借鉴Scala函数式的编程思想,提供与Scala类似的编程接口。图1-2为Spark的处理流程(主要对象为RDD)。
Spark将数据在分布式环境下分区,然后将作业转化为有向无环图(DAG),并分阶段进行DAG的调度和任务的分布式并行处理。
(2)Shark
Shark是构建在Spark和Hive基础之上的数据仓库。目前,Shark已经完成学术使命,终止开发,但其架构和原理仍具有借鉴意义。它提供了能够查询Hive中所存储数据的一套SQL接口,兼容现有的Hive QL语法。这样,熟悉Hive QL或者SQL的用户可以基于Shark进行快速的Ad-Hoc、Reporting等类型的SQL查询。Shark底层复用Hive的解析器、优化器以及元数据存储和序列化接口。Shark会将Hive QL编译转化为一组Spark任务,进行分布式运算。
(3)Spark SQL
Spark SQL提供在大数据上的SQL查询功能,类似于Shark在整个生态系统的角色,它们可以统称为SQL on Spark。之前,Shark的查询编译和优化器依赖于Hive,使得Shark不得不维护一套Hive分支,而Spark SQL使用Catalyst做查询解析和优化器,并在底层使用Spark作为执行引擎实现SQL 的Operator。用户可以在Spark上直接书写SQL,相当于为Spark扩充了一套SQL算子,这无疑更加丰富了Spark的算子和功能,同时Spark SQL不断兼容不同的持久化存储(如HDFS、Hive等),为其发展奠定广阔的空间。
(4)Spark Streaming
Spark Streaming通过将流数据按指定时间片累积为RDD,然后将每个RDD进行批处理,进而实现大规模的流数据处理。其吞吐量能够超越现有主流流处理框架Storm,并提供丰富的API用于流数据计算。
(5)GraphX
GraphX基于BSP模型,在Spark之上封装类似Pregel的接口,进行大规模同步全局的图计算,尤其是当用户进行多轮迭代时,基于Spark内存计算的优势尤为明显。
(6)Tachyon
Tachyon是一个分布式内存文件系统,可以理解为内存中的HDFS。为了提供更高的性能,将数据存储剥离Java Heap。用户可以基于Tachyon实现RDD或者文件的跨应用共享,并提供高容错机制,保证数据的可靠性。
(7)Mesos
Mesos是一个资源管理框架,提供类似于YARN的功能。用户可以在其中插件式地运行Spark、MapReduce、Tez等计算框架的任务。Mesos会对资源和任务进行隔离,并实现高效的资源任务调度。
(8)BlinkDB
BlinkDB是一个用于在海量数据上进行交互式 SQL 的近似查询引擎。它允许用户通过在查询准确性和查询响应时间之间做出权衡,完成近似查询。其数据的精度被控制在允许的误差范围内。为了达到这个目标,BlinkDB的核心思想是:通过一个自适应优化框架,随着时间的推移,从原始数据建立并维护一组多维样本;通过一个动态样本选择策略,选择一个适当大小的示例,然后基于查询的准确性和响应时间满足用户查询需求。
Spark的依赖
(1)Map Reduce模型
作为一个分布式计算框架,Spark采用了MapReduce模型。在它身上,Google的Map Reduce和Hadoop的痕迹很重,很明显,它并非一个大的创新,而是微创新。在基础理念不变的前提下,它借鉴,模仿并依赖了先辈,加入了一点改进,极大的提升了MapReduce的效率。
使用MapReduce模型解决大数据并行计算的问题,带来的最大优势,是它和Hadoop的同属一家人。因为同属于MapReduce并行编程模型,而不是MPI和OpenMP其它模型,因此,复杂的算法,只要能够以Java算法表达,在Hadoop上运行的,就能以Scala算法表达,在Spark上运行,而速度有倍数的提升。相比之下,在MPI和Hadoop算法之间切换,难度就大多了。
(2)函数式编程
Spark由Scala写就,而支持的语言亦是Scala。其原因之一就是Scala支持函数式编程。这一来造就了Spark的代码简洁,二来使得基于Spark开发的程序,也特别的简洁。一次完整的MapReduce,Hadoop中需要创建一个Mapper类和Reduce类,而Spark只需要创建相应的一个map函数和reduce函数即可,代码量大大降低。
(3)Mesos
Spark将分布式运行的需要考虑的事情,都交给了Mesos,自己不Care,这也是它代码能够精简的原因之一。
(4)HDFS和S3
Spark支持2种分布式存储系统:HDFS和S3。应该算是目前最主流的两种了。对文件系统的读取和写入功能是Spark自己提供的,借助Mesos分布式实现。如果自己想做集群试验,又没有HDFS环境,也没有EC2环境的话,可以搞个NFS,确保所有MESOS的Slave都可以访问,也可以模拟一下。
Spark架构
Spark架构采用了分布式计算中的Master-Slave模型。Master是对应集群中的含有Master进程的节点,Slave是集群中含有Worker进程的节点。Master作为整个集群的控制器,负责整个集群的正常运行;Worker相当于计算节点,接收主节点命令与进行状态汇报;Executor负责任务的执行;Client作为用户的客户端负责提交应用,Driver负责控制一个应用的执行。
Spark集群部署后,需要在主节点和从节点分别启动Master进程和Worker进程,对整个集群进行控制。在一个Spark应用的执行过程中,Driver和Worker是两个重要角色。Driver 程序是应用逻辑执行的起点,负责作业的调度,即Task任务的分发,而多个Worker用来管理计算节点和创建Executor并行处理任务。在执行阶段,Driver会将Task和Task所依赖的file和jar序列化后传递给对应的Worker机器,同时Executor对相应数据分区的任务进行处理。
Spark的整体流程为:Client 提交应用,Master找到一个Worker启动Driver,Driver向Master或者资源管理器申请资源,之后将应用转化为RDD Graph,再由DAGScheduler将RDD Graph转化为Stage的有向无环图提交给TaskScheduler,由TaskScheduler提交任务给Executor执行。在任务执行的过程中,其他组件协同工作,确保整个应用顺利执行。
Spark架构基本组件详见该节附录。
Spark运行逻辑
对于RDD,有两种类型的动作,一种是Transformation,一种是Action。它们本质区别是:
Transformation返回值还是一个RDD。它使用了链式调用的设计模式,对一个RDD进行计算后,变换成另外一个RDD,然后这个RDD又可以进行另外一次转换。这个过程是分布式的Action返回值不是一个RDD。它要么是一个Scala的普通集合,要么是一个值,要么是空,最终或返回到Driver程序,或把RDD写入到文件系统中
上图显示,在Spark应用中,整个执行流程在逻辑上会形成有向无环图(DAG)。Action算子触发之后,将所有累积的算子形成一个有向无环图,然后由调度器调度该图上的任务进行运算。Spark的调度方式与MapReduce有所不同。Spark根据RDD之间不同的依赖关系切分形成不同的阶段(Stage),一个阶段包含一系列函数执行流水线。图中的A、B、C、D、E、F分别代表不同的RDD,RDD内的方框代表分区。数据从HDFS输入Spark,形成RDD A和RDD C,RDD C上执行map操作,转换为RDD D, RDD B和 RDD E执行join操作,转换为F,而在B和E连接转化为F的过程中又会执行Shuffle,最后RDD F 通过函数saveAsSequenceFile输出并保存到HDFS中。
Spark On Mesos
为了在Mesos框架上运行,安装Mesos的规范和设计,Spark实现两个类,一个是SparkScheduler,在Spark中类名是MesosScheduler;一个是SparkExecutor,在Spark中类名是Executor。有了这两个类,Spark就可以通过Mesos进行分布式的计算。
Spark会将RDD和MapReduce函数,进行一次转换,变成标准的Job和一系列的Task。提交给SparkScheduler,SparkScheduler会把Task提交给Mesos Master,由Master分配给不同的Slave,最终由Slave中的Spark Executor,将分配到的Task一一执行,并且返回,组成新的RDD,或者直接写入到分布式文件系统。
Spark On YARN
Spark on YARN能让Spark计算模型在云梯YARN集群上运行,直接读取云梯上的数据,并充分享受云梯YARN集群丰富的计算资源。
Spark on YARN架构解析如下:
基于YARN的Spark作业首先由客户端生成作业信息,提交给ResourceManager,ResourceManager在某一NodeManager汇报时把AppMaster分配给NodeManager,NodeManager启动SparkAppMaster,SparkAppMaster启动后初始化作业,然后向ResourceManager申请资源,申请到相应资源后,SparkAppMaster通过RPC让NodeManager启动相应的SparkExecutor,SparkExecutor向SparkAppMaster汇报并完成相应的任务。此外,SparkClient会通过AppMaster获取作业运行状态。
附录Spark架构中的基本组件
ClusterManager:在Standalone模式中即为Master(主节点),控制整个集群,监控Worker。在YARN模式中为资源管理器。
Worker:从节点,负责控制计算节点,启动Executor或Driver。在YARN模式中为NodeManager,负责计算节点的控制。
Driver:运行Application的main()函数并创建SparkContext。
Executor:执行器,在worker node上执行任务的组件、用于启动线程池运行任务。每个Application拥有独立的一组Executors。
SparkContext:整个应用的上下文,控制应用的生命周期。
RDD:Spark的基本计算单元,一组RDD可形成执行的有向无环图RDD Graph。
DAG Scheduler:根据作业(Job)构建基于Stage的DAG,并提交Stage给TaskScheduler。
TaskScheduler:将任务(Task)分发给Executor执行。
SparkEnv:线程级别的上下文,存储运行时的重要组件的引用。
SparkEnv内创建并包含如下一些重要组件的引用。
MapOutPutTracker:负责Shuffle元信息的存储。
BroadcastManager:负责广播变量的控制与元信息的存储。
BlockManager:负责存储管理、创建和查找块。
MetricsSystem:监控运行时性能指标信息。
SparkConf:负责存储配置信息。
2.参数
MapReduce重要配置参数
1. 资源相关参数
(1) mapreduce.map.memory.mb: 一个Map Task可使用的资源上限(单位:MB),默认为1024。如果Map Task实际使用的资源量超过该值,则会被强制杀死。
(2) mapreduce.reduce.memory.mb: 一个Reduce Task可使用的资源上限(单位:MB),默认为1024。如果ReduceTask实际使用的资源量超过该值,则会被强制杀死。
(3) mapreduce.map.java.opts: Map Task的JVM参数,你可以在此配置默认的javaheap size等参数, e.g.
“-Xmx1024m -verbose:gc -Xloggc:/tmp/@[email protected]” (@taskid@会被Hadoop框架自动换为相应的taskid), 默认值: “”
(4) mapreduce.reduce.java.opts: Reduce Task的JVM参数,你可以在此配置默认的javaheap size等参数, e.g.
“-Xmx1024m -verbose:gc -Xloggc:/tmp/@[email protected]”, 默认值: “”
(5) mapreduce.map.cpu.vcores: 每个Map task可使用的最多cpucore数目, 默认值: 1
(6) mapreduce.map.cpu.vcores: 每个Reduce task可使用的最多cpucore数目, 默认值: 1
2. 容错相关参数
(1) mapreduce.map.maxattempts: 每个Map Task最大重试次数,一旦重试参数超过该值,则认为Map Task运行失败,默认值:4。
(2) mapreduce.reduce.maxattempts: 每个Reduce Task最大重试次数,一旦重试参数超过该值,则认为Map Task运行失败,默认值:4。
(3) mapreduce.map.failures.maxpercent: 当失败的Map Task失败比例超过该值为,整个作业则失败,默认值为0. 如果你的应用程序允许丢弃部分输入数据,则该该值设为一个大于0的值,比如5,表示如果有低于5%的Map Task失败(如果一个Map Task重试次数超过mapreduce.map.maxattempts,则认为这个Map Task失败,其对应的输入数据将不会产生任何结果),整个作业扔认为成功。
(4) mapreduce.reduce.failures.maxpercent: 当失败的ReduceTask失败比例超过该值为,整个作业则失败,默认值为0.
(5) mapreduce.task.timeout: Task超时时间,经常需要设置的一个参数,该参数表达的意思为:如果一个task在一定时间内没有任何进入,即不会读取新的数据,也没有输出数据,则认为该task处于block状态,可能是卡住了,也许永远会卡主,为了防止因为用户程序永远block住不退出,则强制设置了一个该超时时间(单位毫秒),默认是300000。如果你的程序对每条输入数据的处理时间过长(比如会访问数据库,通过网络拉取数据等),建议将该参数调大,该参数过小常出现的错误提示是“AttemptID:attempt_14267829456721_123456_m_000224_0 Timed out after300 secsContainer killed by the ApplicationMaster.”。
3. 本地运行mapreduce作业
设置以下几个参数:
mapreduce.framework.name=local
mapreduce.jobtracker.address=local
fs.defaultFS=local
4. 效率和稳定性相关参数
(1) mapreduce.map.speculative: 是否为Map Task打开推测执行机制,默认为false
(2) mapreduce.reduce.speculative: 是否为ReduceTask打开推测执行机制,默认为false
(3) mapreduce.job.user.classpath.first& mapreduce.task.classpath.user.precedence:当同一个class同时出现在用户jar包和hadoop jar中时,优先使用哪个jar包中的class,默认为false,表示优先使用hadoopjar中的class。
(4)mapreduce.input.fileinputformat.split.minsize: 每个Map Task处理的数据量(仅针对基于文件的Inputformat有效,比如TextInputFormat,SequenceFileInputFormat),默认为一个block大小,即134217728。
HBase 相关配置参数
(1) hbase.rpc.timeout:rpc的超时时间,默认60s,不建议修改,避免影响正常的业务,在线上环境刚开始配置的是3秒,运行半天后发现了大量的timeout error,原因是有一个region出现了如下问题阻塞了写操作:“Blocking updates … memstore size 434.3m is >= than blocking 256.0m size”可见不能太低。
(2) ipc.socket.timeout:socket建立链接的超时时间,应该小于或者等于rpc的超时时间,默认为20s
(3) hbase.client.retries.number:重试次数,默认为14,可配置为3
(4) hbase.client.pause:重试的休眠时间,默认为1s,可减少,比如100ms
(5) hbase.regionserver.lease.period:scan查询时每次与server交互的超时时间,默认为60s,可不调整。
Spark 相关配置参数
1. 效率及稳定性相关参数
建议打开map(注意,在spark引擎中,也只有map和reduce两种task,spark叫ShuffleMapTask和ResultTask)中间结果合并及推测执行功能:
spark.shuffle.consolidateFiles=true
spark.speculation=trure
2. 容错相关参数
建议将这些值调大,比如:
spark.task.maxFailures=8
spark.akka.timeout=300
spark.network.timeout=300
spark.yarn.max.executor.failures=100
3.调优
1、数据序列化
(1) Spark默认是使用Java的ObjectOutputStream框架,它支持所有的继承于java.io.Serializable序列化,如果想要进行调优的话,可以通过继承java.io.Externalizable。这种格式比较大,而且速度慢。
(2)Spark还支持这种方式Kryo serialization,它的速度快,而且压缩比高于Java的序列化,但是它不支持所有的Serializable格式,并且需要在程序里面注册。它需要在实例化SparkContext之前进行注册,下面是它的使用例子:
importcom.esotericsoftware.kryo.Kryo
import org.apache.spark.serializer.KryoRegistrator
class MyRegistrator extends KryoRegistrator {
override def registerClasses(kryo: Kryo) {
kryo.register(classOf[MyClass1])
kryo.register(classOf[MyClass2])
}
}
// Make sure to set these properties *before* creating a SparkContext!
System.setProperty("spark.serializer","org.apache.spark.serializer.KryoSerializer")
System.setProperty("spark.kryo.registrator", "mypackage.MyRegistrator")
val sc = new SparkContext(...)
如果对象很大,需要设置这个参数spark.kryoserializer.buffer.mb,默认是2。
2、内存调化
这里面需要考虑3点,对象使用的内存、访问这些对象的开销、垃圾回收器的管理开销。
通常,对象访问的速度都很快,但是需要2-5x的空间来存储,因为下面的原因:
1)每一个独立的Java对象,都有一个16字节的“objectheader”和关于这个对象的信息,比如指针。
2)Java String类型有40字节的“objectheader”,然后因为Unicode,每个字符要存储2个字节,这样10个字符要消耗掉大概60个字节。
3)普通的容器类,比如HashMap和LinkedList,它们采用的是链式的数据结构,它需要封装每个实体,不仅需要头信息,还要有个指针指向下一个实体。
4)原始容器类型通常存储它们为装箱类型,比如java.lang.Integer。
下面我们就来讨论如何确定这些对象的内存开销并且如何进行调优,比如改变数据结构或者序列化存储数据。下面我们讲谈论如何调优Spark的Cache大小以及Java的垃圾回收器。
(1)确定内存使用情况
首先我们要确定内存使用情况,确定数据集的内存使用情况,最好的方法就是创建一个RDD,然后缓存它,然后查看日志,日志会记录出来它的每个分片使用的大小,然后我们可以找个这些分片的大小计算出总大小,如下:
INFO BlockManagerMasterActor: Added rdd_0_1 in memory on mbk.local:50311(size: 717.5 KB, free: 332.3 MB)
(2)数据结构调优
1) 优先使用数组和原生类型来替代容器类,或者使用fastutil找个包提供的容器类型,fastutil的官方链接是http://fastutil.di.unimi.it/。
2)避免大量的小对象的嵌套结构。
3)使用数字的ID来表示,而不是使用字符串的ID。
4)如果内存小于32GB,设置JVM参数-XX:+UseCompressedOops为4个字节而不是8个字节;在Java7或者之后的,尝试使用-XX:+UseCompressedStrings存储ASCII字符串8个比特一个字符。这些参数可以添加到spark-env.sh,根据我的观察,应该是设置到SPARK_JAVA_OPTS这个参数上。
(3)序列化RDD存储
强烈建议使用Kryo进行序列化,这也是降低内存使用最简单的方式。
(4)垃圾回收器调优
当我们只使用一次RDD的时候,不会存在这方面的问题。当java需要清除旧的对象给新的对象腾出空间的时候,它需要遍历所有对象,然后找出那些没有使用的。这里最中要的一点是记住,垃圾回收器的代价是和它里面的对象的数量相关的。查看GC是不是一个问题,第一件事就是使用序列化的缓存方式。
GC还可以出现的问题就是执行任务所需要的内存大小,下面我们讲讨论如何控制分配给RDD缓存的空间大小来减轻这个问题。
1)确定GC的影响
添加这些参数到-verbose:gc-XX:+PrintGCDetails -XX:+PrintGCTimeStamps到SPARK_JAVA_OPTS这个参数,让它出书GC的信息,然后运行任务。
2)缓存大小调优
影响GC的一个重要配置参数是分配给缓存RDD的内存大小,Spark默认是使用 66%的可配置内存大小(通过spark.executor.memory or SPARK_MEM来配置)来存储RDD,也即是说,只有33%是给任务执行过程当中执行过程当中创建的对象的。
当你的程序慢下来,你发现GC很频繁或内存不够等现象,降低它的值会起到一些效果,我们可以通过这个参数System.setProperty("spark.storage.memoryFraction","0.5")来达到这个效果。
3)高级内存调优
java的堆内存是分为两个区间,Young和Old,Young是用来存储短生命周期的对象,Old是用来存储长生命周期的对象。Young又可以进一步细分为 [Eden, Survivor1, Survivor2]。 一个简单的垃圾过程可以描述为:当Eden满的时候,一个简单的GC会运行在Eden和依赖它的对象,Survivor1被复制到Survivor2。Survivor区域进行了交换。如果一个对象足够老或者Survivor2满了,它就会被移到Old区。当Old区也满的时候,一个完整的GC就会触发。
Spark里面的GC调优目标是确保RDD存储在Old区间,并且Young区有足够的空间去存储那些短生命周期的对象。这样可以减少完全的GC去回收那些任务执行中的临时对象。下面的的这些步骤可能是有用的:
1)检查GC的统计信息,查看在任务执行完成之前是不是执行过多次的GC,这意味着内存不足以执行任务。
2)当Old区快满的时候,我们可以通过调整这个参数spark.storage.memoryFraction来减少缓存使用的内存量,少缓存一点对象比拖慢作业执行更好一些。
3)当发生了很多次小的GC,而不是重要的GC时候,我们可以考虑多分配点内存给Eden,假设一个任务需要使用E大小的内存,我们可以分配给Eden的内存大小为: -Xmn=4/3*E,这个大小同样适用于survivor区间。
4)当从HDFS上读取数据的时候,任务的所需内容可以估计为block的大小,一个反压缩的快是2-3倍的大小,我们考虑用3-4个任务来执行,这样我们可以考虑设置Eden的大小为4*3*64MB。
3、其它的考虑
(1)并行的水平
建议是1个CPU核心2-3个任务,可以通过程序的函数的时候传入numPartitions参数,或者通过系统变量spark.default.parallelism来改变。
(2)Reduce任务的内存使用情况
有时候出现OutOfMemoryError并不是因为RDD太大内存装不下,而是因为执行Reduce任务执行的groupByKey的结果太大。Spark的shuffle操作(sortByKey, groupByKey, reduceByKey, join, etc)它会为每一个任务建立一个hash表来执行grouping操作,简答的处理方式就是增加并行水平,这样每个任务的输入集变小。Spark能够支持每个任务200ms的速度,因为它在所有任务共享了JVMs,减小了发布任务的开销,所有可以安全的增加并行水平超过核心数。
(3)使用broadcast存储大的变量
使用Spark里面的broadcast的变量来存储大的变量可以大大减少每个序列化任务的大小和集群发布任务的开销。任务大对象的任务都可以考虑使用broadcast变量,Spark在master上会打印每个序列化任务的大小,当大小超过20KB的时候,可以考虑调优。
4、总结
这里简短的指出了我们调优的时候需要注意的一些重要的点,通常我们把序列化方式调整为Kryo并且缓存方式改为序列化存储方式就可以解决大部分的问题了。
4. Spark集成开发环境搭建eclipse
(1)准备工作
在正式介绍之前,先要以下软硬件准备:
软件准备:
Eclipse Juno版本(4.2版本),可以直接点击这里下载:Eclipse 4.2
Scala 2.9.3版本,Window安装程序可以直接点击这里下载:Scala 2.9.3
Eclipse Scala IDE插件,可直接点击这里下载:ScalaIDE(for Scala 2.9.x and Eclipse Juno)
硬件准备
装有Linux或者Windows操作系统的机器一台
(2)构建Spark集成开发环境
我是在windows操作系统下操作的,流程如下:步骤1:安装scala 2.9.3:直接点击安装即可。
步骤2:将Eclipse Scala IDE插件中features和plugins两个目录下的所有文件拷贝到Eclipse解压后对应的目录中
步骤3:重新启动Eclipse,点击eclipse右上角方框按钮,如下图所示,展开后,点击“Other….”,查看是否有“Scala”一项,有的话,直接点击打开,否则进行步骤4操作
步骤4:在Eclipse中,依次选择“Help”–> “Install New Software…”,在打开的卡里填入http://download.scala-ide.org/sdk/e38/scala29/stable/site,并按回车键,可看到以下内容,选择前两项进行安装即可。(由于步骤3已经将jar包拷贝到eclipse中,安装很快,只是疏通一下)安装完后,重复操作一遍步骤3便可。
(3)使用Scala语言开发Spark程序
在eclipse中,依次选择“File”–>“New”–> “Other…”–> “Scala Wizard” –> “ScalaProject”,创建一个Scala工程,并命名为“SparkScala”。
右击“SaprkScala”工程,选择“Properties”,在弹出的框中,按照下图所示,依次选择“Java Build Path”–>“Libraties”–>“Add External JARs…”,导入文章“Apache Spark学习:将Spark部署到Hadoop 2.2.0上”中给出的
assembly/target/scala-2.9.3/目录下的spark-assembly-0.8.1-incubating-hadoop2.2.0.jar,这个jar包也可以自己编译spark生成,放在spark目录下的assembly/target/scala-2.9.3/目录中。
跟创建Scala工程类似,在工程中增加一个Scala Class,命名为:WordCount,整个工程结构如下:
WordCount就是最经典的词频统计程序,它将统计输入目录中所有单词出现的总次数,Scala代码如下:
import org.apache.spark._
import SparkContext._
object WordCount {
def main(args: Array[String]) {
if (args.length != 3 ){
println("usage is org.test.WordCount
return
}
val sc = new SparkContext(args(0), "WordCount",
System.getenv("SPARK_HOME"),Seq(System.getenv("SPARK_TEST_JAR")))
val textFile = sc.textFile(args(1))
val result = textFile.flatMap(line => line.split("\\s+"))
.map(word => (word, 1)).reduceByKey(_ + _)
result.saveAsTextFile(args(2))
}
}
在Scala工程中,右击“WordCount.scala”,选择“Export”,并在弹出框中选择“Java”–> “JAR File”,进而将该程序编译成jar包,可以起名为“spark-wordcount-in-scala.jar”,我导出的jar包下载地址是spark-wordcount-in-scala.jar。
该WordCount程序接收三个参数,分别是master位置,HDFS输入目录和HDFS输出目录,为此,可编写run_spark_wordcount.sh脚本:
#配置成YARN配置文件存放目录
export YARN_CONF_DIR=/opt/hadoop/yarn-client/etc/hadoop/
SPARK_JAR=./assembly/target/scala-2.9.3/spark-assembly-0.8.1-incubating-hadoop2.2.0.jar\
./spark-class org.apache.spark.deploy.yarn.Client \
–jar spark-wordcount-in-scala.jar \
–class WordCount \
–args yarn-standalone \
–args hdfs://hadoop-test/tmp/input \
–args hdfs:/hadoop-test/tmp/output \
–num-workers 1 \
–master-memory 2g \
–worker-memory 2g \
–worker-cores 2
需要注意以下几点:WordCount程序的输入参数通过“-args”指定,每个参数依次单独指定,第二个参数是HDFS上的输入目录,需要事先创建好,并上传几个文本文件,以便统计词频,第三个参数是HDFS上的输出目录,动态创建,运行前不能存在。
直接运行run_spark_wordcount.sh脚本即可得到运算结果。
在运行过程中,发现一个bug,org.apache.spark.deploy.yarn.Client有一个参数“–name”可以指定应用程序名称:
但是使用过程中,该参数会阻塞应用程序,查看源代码发现原来是个bug,该Bug已提交到Spark jira上:
// 位置:new-yarn/src/main/scala/org/apache/spark/deploy/yarn/ClientArguments.scala
case ("--queue") :: value :: tail =>
amQueue = value
args = tail
case ("--name") ::value :: tail =>
appName = value
args = tail //漏了这行代码,导致程序阻塞
case ("--addJars") :: value :: tail =>
addJars = value
args = tail
因此,大家先不要使用“–name”这个参数,或者修复这个bug,重新编译Spark。
(4)使用Java语言开发Spark程序
方法跟普通的Java程序开发一样,只要将Spark开发程序包spark-assembly-0.8.1-incubating-hadoop2.2.0.jar作为三方依赖库即可。
(5)总结
初步试用Spark On YARN过程中,发现问题还是非常多,使用起来非常不方便,门槛还是很高,远不如Spark On Mesos成熟。
十二、 maven
1.环境配置
1.1.配置Maven2
1)将maven2从\\192.168.22.51\Group\MIA\开发环境\tool\apache-maven-2.2.1复制到本地,如复制到D:\work\apache-maven-2.2.1
2) 配置Path环境变量,指向bin目录,如图:
3)把svn上的maven2资源库http://192.168.22.53/svn/mobile_mia/mlib/check out下来,如check out 到D:\work\mlib
4)修改Maven2安装目录下的配置文件conf\settings.xml,把
关于settings.xml结构详细说明,请参考:http://maven.apache.org/settings.html
1.2. 配置Eclipse环境变量MIA_LIB
配置Eclipse环境变量MIA_LIB指向maven2的
打开菜单Window -> Preferences,然后选择如图:
1.3. 安装Maven2 的eclipseplugin
通过Eclipse update 功能安装,安装地址是:http://m2eclipse.sonatype.org/update/
2.Maven2使用说明
2.1.简单例子
1)新建工程,目录结构如下图:
更多目录结构的作用详细说明请参考:http://maven.apache.org/guides/i ... rectory-layout.html
2) 设置pom.xml,maven2 编译配置文件。
在每个使用maven2编译的项目必须有pom.xml
pom.xml文件详细结构说明请参考:http://maven.apache.org/pom.html。
此例简单的pom.xml结构如下:
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
3)运行mvn命令
在命令行进入工程目录
运行compile命令编译class
mvn compile
运行结果在工程目录下生成target目录,如图:
注:maven2默认所有运行编译等临时文件和生成的结果都在target目录下。
运行package命令打包成jar, war等,打成什么包取决于pom.xml中
mvn package
运行结果:
注:更多关于简单例子和使用请看:http://maven.apache.org/guides/getting-started/index.html
2.2. groupId, artifactId, version的作用
Maven2中groupId, artifactId,version是各种依赖jar包、plugin等调用、引用的基础标识结构。如dependency引用、plugin引用,还有将项目打包发布到Maven2 Repository上都需要是利用该结构的。
groupdId类似于java package的概念,artifactId相当于是java类名。 当工程被安装到Maven2Repository上时,被安装打包后的文件路径将是groupId/artifactId/version/artifactId-version.jar
如pom.xml是:
........
........
如本地的Maven2 Repository是在D:\work\mlib,项目被打包发布到D:\work\mlib下的com\mycompany\app\my-app\1.0-SNAPSHOT\my-app-1.0-SNAPSHOT.jar。
注:这里生成的目录为什么是com\mycompany\app,而不是com.mycompany.app?因为如果groupId是以点号隔开的,Maven2会把点号分隔生成目录,所以这里就把com.mycompany.app分隔生成com\mycompany\app了。
如果把:
改成:
结果在Maven2 Repository发布的包的路径是:com-mycompany-app\my-app\1.0-SNAPSHOT\my-app-1.0-SNAPSHOT.jar
2.3. Maven2的plugin, goal和phase的说明
Maven2的中的plugin, goal作用相当于ant中的task, phase相当于ant中的targetdepends, ant通过depends的关联,实现运行一系列task,Maven2中用phase来实现这种功能,并且它的功能更强,更简单易用。
2.3.1.plugin和goal
Maven2中所有的build的实现都是通过plugin形式,每个plugin下有多个goal,具体的build动作是通过goal来完成的。执行plugin主要有两种方式。
一、在命令行输入
mvn plugin:goal 或者mvnpluginGroupId:pluginArtifactId:pluginVersion:goal
如:
mvn compile:compile
作用等于:
mvn org.apache.maven.plugins:maven-compiler-plugin:2.0.2:compile
在pom.xml中配置plugin
配置如下:
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/maven-v4_0_0.xsd">
注:Maven2内置了很多plugin,关于它们的作用和使用方法,具体请查看:http://maven.apache.org/plugins/index.html
关于自定义plugin的开发请参考:http://maven.apache.org/plugin-developers/index.html
2.3.2.phase
Maven2 的phase是指一个build的生命周期,每个phase绑定一个或多个plugingoal(compile绑定的默认plugin goal是compile:compile),执行每一个phase时,会依次执行完这个phase之前的所有phase后再执行当前phase。
phase的执行方法,如:
mvn compile
注:这里的compile是phase,中间有冒号隔开的是goal,否则是执行一个phase。如:mvn compile:compile是执行一个goal。
下表是Mave2的默认phase:
1
|
validate |
validate the project is correct and all necessary information is available |
2 |
compile |
compile the source code of the project |
3 |
test |
test the compiled source code using a suitable unit testing framework. These tests should not require the code be packaged or deployed |
4 |
package |
take the compiled code and package it in its distributable format, such as a JAR. |
5 |
integration-test |
process and deploy the package if necessary into an environment where integration tests can be run |
6 |
verify |
run any checks to verify the package is valid and meets quality criteria |
7 |
install |
install the package into the local repository, for use as a dependency in other projects locally |
8 |
deploy |
done in an integration or release environment, copies the final package to the remote repository for sharing with other developers and projects. |
如当执行第7个phase install时,会依次执行完前面的1到6个phase后再执行install。如运行:
mvn install
会先依次按顺序调用install前面的几个phase:validate, compile, test, package, integration-test,verify,最后再调用install。
如运行:
mvnpackage
会先依次调用validate,compile, test,最后调用package。
注:关于phase的详细说明请参考:http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
1.1.2. plugin goal与phase的区别
每个plugin goal可以被设置成在指定的phase中被调用,但是在命令行下直接调用goal时不能指定到某个phase中执行,如执行goal:
mvnjar:jar
结果只是单一的调用这个动作,不会执行一系列动作,如果class没有被编译出来,直接调用mvn jar:jar结果将是失败的,因为class不存在。
每个plugin goal如何被指定绑定到一个phase上?有很多plugingoal已经被默认指定到相应的phase上了,如compile:compile被指定到compile phase上,install:install被指定到install phase上的,各phase和plugin goal的绑定关系详细说明可以参考:http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
plugin goal可以通过在pom.xml中设置到指定phase中执行。
如下面的pom.xml配置:
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/maven-v4_0_0.xsd"> ...... ......
默认的plugin goal org.apache.maven.plugins:org.apache.maven.plugins:2.2:jar(简写jar:jar)运行在phasepackage上,也就是说只有当执行“mvnpackage”时这个plugin goal才会被调用。下面进行修改这个plugin goal的默认phase:
......
......
1.1.3. 命令行一次执行多个 goal或phase
在命令行中一条命令可以同时执行多个goal或phase,如:
mvn cleancompile
先调用clean phase清除旧的编译临时文件和目录,然后再调用compile编译源代码
goal 和phase可以组合调用,如:
mvn cleancompile jar:jar
先执行清除编译后,再调用plugingoal jar:jar对编译class打成jar包。
1.2. 几个常用的phase: clean,compile, package, install
1) clean
mvn clean
清除编译生成的目录文件,如:默认是清除target目录及下面的所有文件
2) compile
mvncompile
编译java源代码
3) package
mvnpackage
对工程进行编译后打包,如打成jar包或war包等
4) install
mvninstall
将工程编译打包,最后发布到localRepository上
1.3. 工程依赖jar包的设置
1.3.1.设置说明
在ant中,工程依赖的jar包需要存放在本地,并且通过设置指定位置引用。Maven2中,jar包依赖库的引用变的很简单,只需要在pom.xml中配置好,jar包无需存放在本地,可以在网络的任何地方,maven2在运行编译时会根据pom.xml中的设置自行从网上指定的maven资源库中去下载。每一个依赖的包在pom.xml中设置
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/maven-v4_0_0.xsd"> ...
Maven2 首先根据groupId,artifactId, version发布包时生成的路径规则寻找,如找commons-codec包,将会拼成:commons-code/commons-codec/1.3/commons-codec-1.3.pom,然后从maven2资源库去下载这个pom就是maven的编译配置文件pom.xml,在发布的时候随着jar一起发布到资源库库的。如果找到这个pom文件,然后解析它,如果这个pom有相应的依赖库
注:
scope |
description |
compile |
this is the default scope, used if none is specified. Compile dependencies are available in all classpaths. Furthermore, those dependencies are propagated to dependent projects. |
provided |
this is much like compile, but indicates you expect the JDK or a container to provide it at runtime. It is only available on the compilation and test classpath, and is not transitive. |
runtime |
this scope indicates that the dependency is not required for compilation, but is for execution. It is in the runtime and test classpaths, but not the compile classpath. |
test |
this scope indicates that the dependency is not required for normal use of the application, and is only available for the test compilation and execution phases. |
system |
this scope is similar to provided except that you have to provide the JAR which contains it explicitly. The artifact is always available and is not looked up in a repository.
systemPath: |
1.3.2.关于maven2资源库
上面说的Maven2将所有jar包都是网上下载到本地资源库然后再引用进行编译,所以Maven2先会去检测该jar包在本地资源库localRepository中是否则已经有了,如果有了就直接调用,如果没有再从网上资源库中寻找下载。
Maven2的内置默认资源库是http://repo2.maven.org/maven2,也可以在pom.xml中自定义设置资源库位置,设置方法如下:
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/maven-v4_0_0.xsd"> ...... ......
1.4. 手动安装jar包到本地资源库
在2.5.1 设置说明中提到,如果一个jar包在所有提供的资源库中都找不到,就会编译失败。所以只能把这个jar包手动安装到本地资源库中。安装方法:
mvninstall:install-file -DgroupId=groupId -DartifactId=artifactId-Dversion=version-Dpackaging=jar -DgeneratePom=true -Dfile=/path/to/file
如ugc sti的nokia jar包naai-sdk.jar,在任何资源库中都找不到它,所以只有手动将naai-sdk.jar安装到本地资源库上
mvninstall:install-file-DgroupId=com.nokia.ncim -DartifactId=naai-sdk -Dversion=1.0-Dpackaging=jar-DgeneratePom=true -Dfile=D:/ttt/sti/WebContent/WEB-INF/lib/naai-sdk.jar
groupId设成com.nokia.ncim
arartifactId设成naai-sdk
version设成1.0
然后就可以在pom.xml中引用了:
1.5. 使用maven编译项目代码注意事项
在Maven中,对代码的要求比较严格,只要编译时出现警告提示,虽然在eclipse或在ant中能编译通过,
但是在Maven中无法编译通过
1. 方法返回泛型类型的需注意:
有如下代码:
public
return (T)getSqlMapClientTemplate().queryForObject(statementName,parameterObject);
}
如果该函数返回是java primitive基本类型的,返回的变量一定要要声明成它的Wrapper Class,如下:
Integer num=queryForObject(arg1, arg2)
如果以下面这种形式
int num= queryForObject(arg1, arg2)
在maven中将无法通过编译
2. 引入com.sum打头的包会编译失败,所以不要直接引用这种类
3.构建项目规范说明
3.1.项目目录结构
目录结构如图:
现有实际项目目录结构图参考:
目录作用说明:
src/main/java 存放所有java source code
src/main/resources 存放所有资源文件,如xml,properties等配置文件
src/main/webapp 存放web资源文件
src/test 存放所有测试相关的程序代码和资源文件,src/test下面的目录结构和作用与src/main下面的一样
pom.xml maven2 编译配置文件。在每个使用maven2编译的项目该文件必须有,pom.xml文件详细结构说明请参考:http://maven.apache.org/pom.html
assembly.xml maven2的 plugin assembly的配置文件,主要作用是配置相关文件如自定义配置xml, properties, lib等复制到指定的位置,关于plugin assembly的使用说明请参考:http://maven.apache.org/plugins/maven-assembly-plugin/index.html
关于assembly.xml配置文件结构说明请参考:http://maven.apache.org/plugins/... lugin/assembly.html
更多目录结构的作用详细说明请参考:http://maven.apache.org/guides/i... rectory-layout.html
注:
1) 所有的需要被打到jar包的文件或classpath里的资源配置文件除了java 文件以外的文件,必须放在src/main/resources或在pom.xml里指定resources的目录,否则将无法被打进jar包或放在class path里
2) 如果不按照maven2标准的目录结构,在pom.xml中将需要做配置指定相关的目录,如:
3.2.设置groupId, artifactId规范
规范示例:
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/maven-v4_0_0.xsd"> ......
3.3.在Eclipse要引用的jar包设置规范
在Eclipse工程中所有要引用的lib jar包,必须从上面已建的环境变量MIA_LIB库中引用。引用方法如图:
3.4. MIA_LIB维护更新规范
环境变量MIA_LIB指向的是Maven2 local repository,也是就从svn check out下来的mlib,所以维护MIA_LIA就是维护maven2本地资源库。所以必须经常从svn更新这个库,或在本地有新增加jar包,一定要提交到svn上,通过svn来保证各开发人员之间,本地资源库的统一。
我们内部所有项目如commons, nbi等,如有更新修改,也需要发布更新安装到MIA_LIB,提交svn。与其他开发人员需要引用的这些库保持同步。