-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontent.json
1 lines (1 loc) · 672 KB
/
content.json
1
[{"title":"SQLServer中文乱码及查询异常","date":"2024-08-21T05:08:06.550Z","path":"日常问题/49955695.html","text":"记录一次工作中SQLServer查询异常的经历。 由于直连供应商那边要求使用sqlserver接收数据,故我们又在207安装了SQLServer数据库,我这边正常建表,然后导入数据,刚开始客户类型,厂家等字段使用常用字符串类型varchar,但是导入发现除数字和字母外,中文部分乱码,后来网上了解到,中文需用到Unicode字符集,故如果含有中文需要用nvarchar,以下是网上查找到varchar与nvarchar的区别: 改成nvarchar后,立马就正常了。 但是今天,同事突然找我说,这几个中文字段有些问题,无论是精准查询还是模糊匹配都查不到。以为是同步问题,我去海豚后台去看,同步条数都正常。 Navicat查询结果: 海豚后台同步日志: 然后直接去数据库中查找也的确有这个厂家的数据。这就很奇怪,后来在网上查到了原因,原来是SQLServer 排序规则的问题,以下是网上给出的解释: 然后我在字符串前加上N测试,果然是这个原因: 将数据库排序规则更改为 Chinese_PRC_CI_AS 后,不加N也可以查询的到了: 最后附上更改数据库排序规则的SQL语句: alter database [CustomerDB] collate Chinese_PRC_CI_AS;","tags":[{"name":"SQLServer","slug":"SQLServer","permalink":"http://www.seanxia.cn/tags/SQLServer/"},{"name":"数据库","slug":"数据库","permalink":"http://www.seanxia.cn/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"},{"name":"异常","slug":"异常","permalink":"http://www.seanxia.cn/tags/%E5%BC%82%E5%B8%B8/"}]},{"title":"记录一次生产数据库紧急恢复经历","date":"2024-07-19T16:00:00.000Z","path":"日常问题/f3ddb040.html","text":"事情经过 前天晚上(2024年3月31日)20点21分,钉钉突然报警,一看是任务调度平台海豚挂了一个节点,因为是集群部署有三个节点,想着挂一个也不影响,但快10点的时候突然又崩出大量报警信息,一看同步到StarRocks的任务全部报错,这个问题明显就严重很多,自从StarRocks迁移到公司集群后,已经稳定运行了七个多月,从来没有发生过这种情况。 海豚集群钉钉报警截图: StarRocks同步任务钉钉报警截图: StarRocks报错日志截图: Linux后台查看StarRocks集群进程: **结论:**这里基本确定,StarRocks集群225和226两台节点都挂了。 处理方式 起初,我尝试使用常规的方式,重启三台StarRocks节点的FE和BE进程,但225和226两台节点FE启不起来,起来秒挂,于是紧急联系了线上技术支持。 于是,在线上技术的指导下,我按照文档,先将三天节点的元数据进行备份,然后找出最新的元数据节点,再以最新的这台节点作为 LEADER 节点,将另外两台节点踢出集群,并清空这两台节点元数据,然后重新将这两台节点再加进集群。 步骤一:寻找元数据最新的FE节点 需要先备份所有 FE 节点的元数据路径。该路径存储在 ${STARROCKS_HOME}/fe/meta 下。 然后执行以下操作获取 lastVLSN,该值越大则该节点元数据越新。jar包路径:/opt/soft/StarRocks-2.5.7/fe/lib java -jar lib/starrocks-bdb-je-18.3.16.jar DbPrintLog -h meta/bdb/ -vd 步骤二:确认恢复节点角色 进入元数据最新的节点的 image 目录查看 role 文件,确认当前节点的角色。路径为 ${STARROCKS_HOME}/fe/meta/image。推荐选择FOLLOWER节点进行恢复。 步骤三:基于FOLLOWER节点进行恢复 在当前节点的 fe.conf中添加以下配置。 metadata_failure_recovery = true 启动当前 FE 节点。 sh bin/start_fe.sh --daemon 启动完成后,通过 MySQL 连接至当前 FE节点,执行查询导入操作,检查集群是否能够正常访问;如果无法正常访问,您需要查看 FE 节点的启动日志,排查问题后重新启动 FE 节点。 确认启动成功后,连接StarRocks客户端,检查当前 FE 节点角色。 # 连接客户端mysql -hhost -P9030 -uxxx -pxxx# 查看FE状态show frontends;# 查看FE状态(格式化表格)show proc '/frontends'\\G; 【重要】将当前节点 fe.conf中的 metadata_failure_recovery=true 配置项删除,或者设置为false ,然后重启当前 FE 节点。 步骤四:重新部署集群 在得到了一个存活的 FOLLOWER 角色的 LEADER 后,需要将之前的其他的 FE 从元数据删除。 ALTER SYSTEM DROP FOLLOWER; 然后您需要将恢复节点的 /fe/meta/备份,然后清空该目录。 rm -rf meta/* 最后在224和225两台节点执行以下命令,加入到新集群。 alter system add follower 10.0.1.226:9010 客户端查看最终的三台FE状态,显示已经可以了,StarRocks元数据恢复完毕,新集群启动成功。 异常原因排查 通过初步排查,显示节点225和226这两台虚拟机所在的服务器在2023年3月31日晚20点21分有自动故障重启的事件,具体原因不详,后续继续进行跟踪!","tags":[{"name":"数据库","slug":"数据库","permalink":"http://www.seanxia.cn/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"},{"name":"StarRocks","slug":"StarRocks","permalink":"http://www.seanxia.cn/tags/StarRocks/"},{"name":"恢复","slug":"恢复","permalink":"http://www.seanxia.cn/tags/%E6%81%A2%E5%A4%8D/"}]},{"title":"Linux定时备份异常处理","date":"2024-07-16T16:00:00.000Z","path":"日常问题/64d019ba.html","text":"问题场景 为了FineReport数据决策平台服务器的安全性,前段时间做了定时备份,使用Linux的crontab定时任务每天凌晨将整个FineReport项目打包然后备份一份到挂载的公司共享 NAS 1.3 云盘。 # Crontab定时任务列表查询crontab -l# Crontab定时任务编辑crontab -e 处理过程 今天突然想起来去看看备份的情况,发现挂载的 1.3 云盘没有备份文件,赶忙去看了下crontab执行日志: cat /var/log/cron | grep 'webroot' 感觉是打包有点问题,接着看了下系统邮件检查异常信息: cat /var/spool/mail/root 看起来是打包的后缀在加date拼接的问题,网上找了找,看到一个解决方式: 加上之后,果然可以了。重新修改定时任务: 0 3 * * * tar -cf - /usr/local/tomcat-8.5.55/webapps/webroot | pigz > /data/FineReport/webroot_$(date +'\\%Y-\\%m-\\%d').tar.gz 但还是会一直报一个警告: 去ChatGPT上查了一下,是因为打包使用了绝对路径的问题: 然后我采用了第一种方式: 0 3 * * * cd /usr/local/tomcat-8.5.55/webapps/ && tar -cf - webroot | pigz > /data/FineReport/webroot_$(date +'\\%Y-\\%m-\\%d').tar.gz 最后问题得到圆满解决,以下是挂载盘在 Linux1.7 和 win1.3 上的路径: 拓展 为了防止备份数据冗余,我写了一个bash脚本,每天定时删除三天前的备份(FineReport 已经默认保存了最近7天的数据) 最后,贴上NAS远程挂载命令: mount.cifs //10.0.1.3/soft/FineReport/backup/ /data/FineReport -o user="xxx",password="xxx",vers=3.0","tags":[{"name":"Linux","slug":"Linux","permalink":"http://www.seanxia.cn/tags/Linux/"},{"name":"定时备份","slug":"定时备份","permalink":"http://www.seanxia.cn/tags/%E5%AE%9A%E6%97%B6%E5%A4%87%E4%BB%BD/"}]},{"title":"使用Git系统搭建GitLab","date":"2019-08-23T16:00:00.000Z","path":"其他/6d60df94.html","text":"Git、GitHub与GitLab区别 首先我们要知道的是,Git与GitLab不是一个东西,git是一个可以进行版本控制的操作工具,而GitLab则是一个用来托管文件的远程仓库。 GitLab与GitHub最大的区别就是可以使用自己的服务器进行托管,相比GitHub来说更安全高效,适合团队内部开发。 具体步骤 第一步:新建小组以及项目 首先登录GitLab官网:http://git.lzops.com,输入账号密码然后开始进入。初始的有两个框框,新建项目和新建小组。 我已经建好了小组:lz_bigdata,还有一个项目:lzjr_first。 第二步:进行远程仓库GitLab与Git版本控制系统的关联 在GitLab空的项目中我们会发现一些系统中给出的提示操作,例如本地Git如何连接远程仓库的一些命令 第三步:安装Git分布式版本控制系统,与远程仓库GitLab进行连接 去Git官网:https://git-scm.com 下载Window版64位的安装包,然后安装成功后,在桌面点击鼠标右键会发现多出两行命令。 点击Git Bash Here,会出来一个黑窗口,便是Git的命令界面。 分别输入之前远程仓库GitLab中的命令提示,按回车执行 git config --global user.name "bigdata"git config --global user.email "[email protected]" 第四步:生成公钥并在远程仓库添加本地密匙 找到GithLab右上角的头像下拉菜单,点击Settings 在Profile选项中的Email中找到账户的邮箱,其实邮箱之前在空仓库的提示命令中已经有了。这里多说是因为一会儿我们要把生成的公钥填到SSH Keys中。 在Git系统中输入如下命令,大家可以换成自己的邮箱即可 ssh-keygen -t rsa -C "[email protected]" 按下回车后,会出来几次信息确认,账号、密码和确认密码,这里可以直接回车过掉,如果做了校验,那后面每次提交文件到远程仓库或者克隆远程仓库文件到本地都会要求输入密码,很麻烦,所以如果不是非常机密,不建议这么做。 确认之后,会出来一串看不懂的密匙图案,说明生成密匙成功。 生成的秘钥一般会在根目录的 .ssh 目录下,目录下有两个密匙,id_rsa和id_rsa.pub,分别是私钥和公钥。 这里如果电脑中文件过多,秘钥不好找的话给大家推荐一种小软件:everything,找文件比系统的全盘搜索快了N倍。强烈推荐使用!!! 然后我们用日记本或者其他编辑器打开公钥:id_rsa.pub文件,切记,是公钥id_rsa.pub文件!!! 打开后我们会发现很长一串子字母符号 我们全选然后复制,打开上面中提到的,点击GitLab的右上角头像中的Settings,里面有一个 SSH Keys选项,点开后将刚才的公钥粘贴到Key的框框中,然后取一个标题。点击Add key,就OK了。 如下图,说明本地与远程的密匙连接就成功了。 第五步:使用Git在本地Clone远程项目,并上传本地文件到远程项目中 在Git中输入下面的命令 git clone [email protected]:lz_bigdata/lzjr_first.git 后面是远程仓库的SSH地址或者HTTP地址,在如图中可以直接复制拿到 输入上面命令回车后,会有提示,直接输入yes回车即可 根据提示我们能看到,已经把远程仓库的空项目拉到本地了,在本地也可以看到名字为远程项目名的文件夹lzjr_first 打开会发现有个名为 .git 的文件夹,其实这是个隐藏文件,保存一些需要上传到远程的副本。关于如何显示隐藏文件以及文件后缀名,可以打开电脑上面的查看,在右边会有文件拓展名和隐藏的文件选项,勾上就好了。 这时候我们就可以上传本地的文件到远程仓库GitLab了。 例如我新建了如下几个文件夹,用来存放后期需要整理的文档需求,然后还要新建一个名为 README.md 的说明文件,用来说明这个项目的作用,此文件必须是MarkDown文档格式。(关于MarkDown文档大家可以自行百度,MarkDown文档编辑器强烈推荐使用Typora,毕竟好用颜值也高。现在的很多笔记工具也都支持了MD的格式,如:有道笔记、印象笔记、简书、CSDN等) 然后我们在此处重新打开Git系统,输入下面的命令 git add . ## 添加除了删除之外的变化git add -U ## 只添加增加的也就是更新的那部分变化git add -A ## 添加所有的变化 这个命令是将未提交内容添加到暂存区,关于git add . 与 git add -U 还有git add -A 的区别,大家可以去百度看详细资料,推荐使用 git add . !!! 接下来是将这些变化提交到本地仓库,这里的 -m 建议加上,不然后面做了什么修改会不清楚 git commit -m '解释说明' 在提交前和提交后我们都可以用如下命令来监视 目前文件的提交状态 git status 最终是将本地仓库的文件推送到远程仓库 git push -u origin master 看到如上的提示语,说明提交到远程就完成了。 最后回到远程GitLab中,会看到 README.md 这个文件。并且在GitLab中是支持md格式的文档的。 这里会发现一个问题,我明明还创建了5个文件夹,这里怎么没推送过来呢? 其实这是因为在Git中不支持推送空的文件夹,这个也没事,等以后有了文件之后,再推一下就会过来啦。 今天的教程就到这里,谢谢大家!!! 分支的新建与合并 在Git版本控制中,有个主分支master与其他分支的概念,分支的好处在于每个分支都是对主项目的映射,在不影响项目正常运行的前提下也起到了安全备份的作用,使丢失率大大降低,这也对应了分布式的理念和思想。 注意:分支名称推荐使用自己的名字首字母缩写,如我的:xss 一、创建并切换分支 首先,我们假设你正在你的项目上工作,并且已经有一些提交。 现在,新建一个分支并同时切换到那个分支上,你可以运行一个带有 -b 参数的 git checkout 命令: $ git checkout -b xss git checkout命令加上-b参数表示创建并切换,相当于以下两条命令: $ git branch xss$ git checkout xss 根据箭头所指可以看到当前分支名称已经切换。 二、查看分支 然后,用git branch命令查看当前分支: $ git branch git branch命令会列出所有分支,当前分支前面会标一个*号。 然后,我们就可以在xss分支上正常提交,比如对README.md做个修改,然后提交: $ git add README.md ## 加上文件名则只提交此文件,.和-A则提交全部$ git commit -m "branch test" 你继续在分支xss上工作,并且做了一些提交。 在此过程中,xss 分支在不断的向前推进,因为你已经检出到该分支(也就是说,你的 HEAD 指针指向了 xss 分支) 在版本回退里,你已经知道,每次提交,Git都把它们串成一条时间线,这条时间线就是一个分支。截止到目前,只有一条时间线,在Git里,这个分支叫主分支,即master分支。HEAD严格来说不是指向提交,而是指向master,master才是指向提交的,所以,HEAD指向的就是当前分支。 现在,xss分支的工作完成,存在两种情况: 1、将本地分支推送到远程,直接在远程页面合并 直接将分支xss推送到远程: $ git push origin xss 推送成功后,在远程GitLab可以看到已经新建了一个xss分支,以及分支下刚刚更新的文件 而主分支中没有更新的文件!!! 去远程GitLab中的项目中点击右上角的Merge request图标,选择你要合并的项目 然后如图选择你要进行合并的分支来源和分支目标。我们更新的内容在xss分支上,要将它合并到master主分支中。 然后提交合并,通过即可。 这时到远程GitLab中查看会发现,xss分支中的更新内容已经合并到master主分支中了,而且xss分支中的内容也还在,可见此合并过程实际是个拷贝覆盖的过程。 2、将本地分支直接合并到主分支,然后推送到远程 先切换回master分支: $ git checkout master 切换回master分支后,再查看一个README.md文件,刚才添加的内容不见了!因为那个提交是在xss分支上,而master分支此刻的提交点并没有变: >$ git log ##查看历史提交记录> 合并分支到主分支 现在,我们把xss分支的工作成果合并到master分支上: $ git merge xss git merge命令用于合并指定分支到当前分支。合并后,再查看README.md的内容,就可以看到,和dev分支的最新提交是完全一样的。 注意到上面的Fast-forward信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master指向dev的当前提交,所以合并速度非常快。 如图所示,分支的C3版本被合并到主分支master上。 提交结果到远程 最后将本地提交到远程GitLab即可 $ git push origin mater 这两种合并的方法都可以,这里暂时为了简单推荐第一种。大家在自己的分支中区操作整理,然后定期更新合并到主分支master中就可以了!!! 关于合并中的冲突解决可以去看廖雪峰老师的教学,很不错。(最简单的方法就是使用get pull命令,将远程的部分拉过来并与现在的合并后,再推送回远程即可) 分支常用命令 查看分支:git branch 创建分支:git branch <name> 切换分支:git checkout <name> 创建+切换分支:git checkout -b <name> 合并某分支到当前分支:git merge <name> 删除分支:git branch -d <name>","tags":[{"name":"Git","slug":"Git","permalink":"http://www.seanxia.cn/tags/Git/"},{"name":"GitLab","slug":"GitLab","permalink":"http://www.seanxia.cn/tags/GitLab/"}]},{"title":"新浪微博图床迁移","date":"2019-08-10T16:00:00.000Z","path":"其他/7fb68dad.html","text":"前不久,微博图床挂了,这对于众多使用 Markdown 写技术博客的人简直太残忍了!看来,图片迁移真的是刻不容缓了,在我准备迁移图片的时候,发现了几个平台对图片不同的处理方式,觉得很有意思,所以记录一下。 本人强烈推荐使用方法五,亲测有效。 补救措施 一、在标头添加代码 <meta name="referrer" content="no-referrer" /> 头部添加以上代码,防止获取敏感信息,这个作用是:在页面引入图片、JS 等资源,或者从一个页面跳到另一个页面,都会产生新的 HTTP 请求,浏览器一般都会给这些请求头加上表示来源的 Referrer 字段。Referrer 在分析用户来源时很有用,有着广泛的使用。但 URL 可能包含用户敏感信息,如果被第三方网站拿到很不安全(例如之前不少 Wap 站把用户 SESSION ID 放在 URL 中传递,第三方拿到 URL 就可以看到别人登录后的页面)。之前浏览器会按自己的默认规则来决定是否加上 Referrer。 二、利用大佬写的工具进行迁移 php写的小程序,扫描网站目录提取所有微博图床并下载到本地。 地址→ 三、开源项目 微博图床一键迁移到阿里云 OSS 四、代码实现,比保存本地再上传好一些 沈唁大佬 五、使用picGo插件pic-migrater进行快速迁移 目前如果你是用markdown写博客的话,可以用PicGo的pic-migrater插件来快速将markdown里的图片地址迁移到另外的一家服务商去(迁移到你当前设置的PicGo默认图床)。 PicGo地址:PicGo picgo-plugin-pic-migrater: 直接在PicGo的「插件设置」页面安装即可使用,每个迁移过的markdown文件会生成一个新的markdown文件,防止图片迁移失败导致的原图损失。 具体步骤: 一、首先要有新的图床,这里以阿里云OSS为例 先开通阿里云对象存储OSS,进入OSS控制台,创建一个存储空间Bucket,如图我的是:seanxia 然后到文件管理中新建目录,看自己情况,我这里创建了二级目录 二、下载picGo,并配置好图床 1、KeyId和KeySecret都可以在Access Key中找到 先打开OSS概况,点击右边的Access Key 然后就可以看到KeyId和KeySecret,KeySecret需要手机验证,Access Key没有的直接创建就好了 2、存储空间名 存储空间名其实就是Bucket的名字,一开始创建的名字 3、存储区域 这个在创建的时候就有,如果忘记,可以点击Bucket查看概览中的访问域名就有 4、指定存储路径 存储路径就是你在Bucket的文件管理中的目录,比如我创建的是两级目录:img/hexo/ 切记每级目录都要加上 / ,不然就会被当做前缀而无法生效 配置好图床后就是下载插件pic-migrater了 在插件设置中搜索pic-migrater,安装完成点击配置,部分配置在pic-migrater的github上也有说明 将这一切配置好后,点击选择文件或者文件夹,比如之前写的Hexo文档放在_posts中,直接选中整个文件夹,即可自动将这个文件夹中所有Markdown文档中的图片链接进行迁移到我现在默认的阿里云OSS中。 等待一会儿,picGo会显示迁移成功多少张的提示,再去看文档中的图片链接,已经换到了阿里云OSS的连接了 再去查看阿里云OSS中img/hexo/ 下,图片已经都上传成功 至此,图床迁移已完成,最后在hexo进行重新发布一遍即可! 此方法适用于所有markdown文档格式的图片链接,html或者URL此插件目前不支持,已亲测。如果以后需要迁移到其他的图床,此方式也非常实用。 然后非常推荐大家使用picGo图床工具,这个开源项目真心不错,最后希望此篇文章能对大家有所帮助,谢谢!","tags":[{"name":"图床","slug":"图床","permalink":"http://www.seanxia.cn/tags/%E5%9B%BE%E5%BA%8A/"},{"name":"迁移","slug":"迁移","permalink":"http://www.seanxia.cn/tags/%E8%BF%81%E7%A7%BB/"}]},{"title":"流式框架Flink(一)","date":"2019-01-01T16:00:00.000Z","path":"大数据/31afedf9.html","text":"Apache Flink是一个用于对无边界和有边界数据流进行有状态计算的框架和分布式处理引擎。Flink设计为运行在所有常见的集群环境中,并且以内存速度和任意规模执行计算。 官网:https://flink.apache.org Flink体系架构 Flink 是一个 Stateful Computations Over Streams,即数据流上的有状态的计算。 这里面有两个关键字,一个是Streams,Flink认为有界数据集是无界数据流的一种特例,所以说有界数据集也是一种数据流,事件流也是一种数据流。Everything is streams,即Flink可以用来处理任何的数据,可以支持批处理、流处理、AI、MachineLearning等等。 另外一个关键词是Stateful,即有状态计算。有状态计算是最近几年来越来越被用户需求的一个功能。举例说明状态的含义,比如说一个网站一天内访问UV数,那么这个UV数便为状态。Flink提供了内置的对状态的一致性的处理,即如果任务发生了Failover,其状态不会丢失、不会被多算少算,同时提供了非常高的性能。 处理无边界和有边界数据 任何类型的数据都是作为事件流产生的。信用卡交易事务,传感器测量,机器日志以及网站或移动应用程序上的用户交互行为,所有这些数据都生成流。 数据可以作为无边界或有边界流处理。 无边界流 定义了开始但没有定义结束。它们不会在生成时终止提供数据。必须持续地处理无边界流,即必须在拉取到事件后立即处理它。无法等待所有输入数据到达后处理,因为输入是无边界的,并且在任何时间点都不会完成。处理无边界数据通常要求以特定顺序(例如事件发生的顺序)拉取事件,以便能够推断结果完整性。 有边界流 定义了开始和结束。可以在执行任何计算之前通过拉取到所有数据后处理有界流。处理有界流不需要有序拉取,因为可以随时对有界数据集进行排序。有边界流的处理也称为批处理。 **Apache Flink擅长处理无边界和有边界数据集。**在事件和状态上的精确控制使得Flink运行时能在无边界流上运行任意类型的应用程序。有界流由算法和数据结构内部处理,这些算法和数据结构专门针对固定大小的数据集而设计,从而获得优秀的性能。 随处部署应用程序 Apache Flink是一个分布式系统,需要计算资源才能执行应用程序。Flink与所有常见的集群资源管理器(如Hadoop YARN,Apache Mesos和Kubernetes)集成,但也可以作为独立集群运行。 Flink旨在很好地适用于之前列出的每个资源管理器。这是通过特定于资源管理器的部署模式实现的,这些模式允许Flink以其惯用的方式与每个资源管理器进行交互。 部署Flink应用程序时,Flink会根据应用程序配置的并行度自动识别所需资源,并从资源管理器请求它们。如果发生故障,Flink会通过请求新的资源来替换发生故障的容器。提交或控制应用程序的所有通信都通过REST调用进行。这简化了Flink在许多环境中的集成。 任意规模运行应用程序 Flink旨在以任意规模运行有状态流式应用程序。应用程序可以并行化为数千个在集群中分布和同时执行的任务。因此,应用程序可以利用几乎无限量的CPU,内存,磁盘和网络IO。而且,Flink可以轻松维护非常大的应用程序的状态。其异步和增量检查点算法确保对延迟处理的影响最小,同时保证精确一次的状态一致性。 用户报告了在其生产环境中运行的Flink应用程序的扩展数字令人印象十分深刻,例如: 应用程序每天处理数万亿个事件 应用程序维护数个TB的状态 应用程序在数千个CPU核上运行 利用内存的性能 有状态的Flink应用程序针对本地状态访问进行了优化。任务状态始终驻留在内存中,或者,如果状态大小超过可用内存,则保存在访问高效的磁盘上的数据结构中。因此,任务通过访问本地(通常是内存中)状态来执行所有计算,从而产生非常低的处理延迟。Flink通过定期和异步检查点将本地状态到持久存储来保证在出现故障时的精确一次的状态一致性。 应用 Apache Flink是一个用于对无边界和有边界数据流进行有状态计算的框架。 Flink 在不同的抽象级别提供多个API,并为常见用例提供专用库。 为流应用程序构建块 流式计算框架构建和运行的应用程序的类型,由框架控制流(events)、状态(state)以及时间(time)的程度来定义。在下文中,我们描述了流处理应用程序的这些构建块,并解释了Flink处理他们的方法。 流(events) 显然,流是流式处理的一个基本方面。然而,流可以有不同的特征,这些特征会影响流的处理方式。Flink 是一个多功能的处理框架,它可以处理任意类型的流。 有边界和无边界的流:流可以是无边界或是有边界的,如固定大小的数据集。Flink 具有处理无边界流的复杂功能,但也有专用的运算符来有效地处理有边界流。 实时和记录的流:所有数据都作为流生成,有两种方法可以处理数据。在生成时实时处理它或者将流持久保存到存储系统(例如文件系统或对象存储),并在之后对其进行处理。Flink 应用程序可以处理记录或实时流。 状态(state) 每个非凡的流式应用都是有状态的。只有对个别事件应用转换的应用程序才不需要状态。运行基本业务逻辑的任何应用程序都需要记住事件或中间结果,以便在之后的时间点访问它们,例如在收到下一个事件时或在特定持续时间之后。 应用程序的状态在 Flink 中是一等公民。您可以通过查看 Flink 在状态处理环境(上下文context)中提供的所有功能(函数)来查看。 多状态原语:Flink为不同的数据结构提供了状态原语,如原子值(value),列表(list)或映射(map)。开发人员可以根据函数的访问模式选择最有效的状态原语。 可插拔状态后端:应用程序状态由可插拔状态后端管理以及检查(checkpointed)。Flink有不同的状态后端,可以在内存或RocksDB中存储状态,RocksDB(KV DB)是一种高效的嵌入式磁盘数据存储。也可以插入自定义状态后端。 精确一次的状态一致性:Flink的检查点和恢复算法可确保在发生故障时应用程序状态的一致性。因此,故障是透明处理的,不会影响应用程序的正确性。 非常大的状态:由于其异步和增量检查点算法,Flink能够维持几个TB的应用程序状态。 可扩展的应用程序: Flink通过将状态重新分配给更多或更少的Worker节点来支持有状态应用程序的扩展。 时间(time) 时间是流式应用的另一个重要组成成分。大多数事件流都具有固定的时间语义,因为每个事件都是在特定的时间点生成的。此外,许多常见的流计算基于时间,例如窗口聚合、会话化、模式监测和基于时间的连接。流处理的一个重要方面是应用程序如何测量时间,即时间时间和处理时间之间的差异。 Flink 提供了一组丰富的与时间相关的功能。 事件时间模式:使用事件时间语义处理流的应用程序根据时间的时间戳计算结果。因此,无论是否处理记录或实时的时间,事件时间处理都是准确和一致的结果。 水印支持:Flink使用水印来推断事件时间应用中的时间。水印也是一种灵活的机制,可以权衡取舍延迟数据和结果的完整性。 延迟数据处理:当在事件时间模式下使用水印处理流时,可能会发生在所有相关事件到达之前已完成计算的情况。这类事件被称为延迟事件。Flink具有多种处理延迟事件的选项,例如通过边输出重新路由它们以及更新之前已经完成的结果。 处理时间模式:除了事件时间模式以外,Flink还支持处理时间语义,处理时间语义的执行由处理机器的挂钟(系统)时间来触发计算。处理时间模式适用于某些具有严格的低延迟要求的应用,这些要求同时可以容忍近似结果。 分层接口API Flink提供三层API。每个API在简洁性和表达性之间提供不同的权衡,并针对不同的用例。 最底层是ProcessFunction,它能够提供非常灵活的功能,它能够访问各种各样的State,用来注册一些timer,利用timer回调的机制能够实现一些基于事件驱动的一些应用。 之上是DataStream API,最上层是SQL/Table API的一种High-level API。 ProcessFunctions ProcessFunctions 是Flink提供的最具表现力的功能接口。Flink 提供 ProcessFunctions 来处理来自一个或两个输入流中的单个事件或分组到一个窗口的事件。ProcessFunctions 提供对时间和状态的细粒度控制。ProcessFunction 可以任意修改其状态并注册将在未来触发回调函数的定时器。因此,ProcessFunctions 可以实现许多有状态事件驱动应用程序所需的复杂的每个事件业务逻辑。 以下示例显示了 KeyedProcessFunction 对 KeyedStream,匹配 START 以及 END 事件进行操作的示例。当一个START 事件被接收时,该函数在记住其状态时间戳和并且注册四个小时的计时器。如果在计时器触发之前收到END 事件,则该函数计算事件 END 和 START 事件之间的持续时间,清除状态并返回值。否则,计时器只会触发并清除状态。 /** * Matches keyed START and END events and computes the difference between * both elements' timestamps. The first String field is the key attribute, * the second String attribute marks START and END events. */public static class StartEndDuration extends KeyedProcessFunction<String, Tuple2<String, String>, Tuple2<String, Long>> { private ValueState<Long> startTime; @Override public void open(Configuration conf) { // obtain state handle startTime = getRuntimeContext() .getState(new ValueStateDescriptor<Long>("startTime", Long.class)); } /** Called for each processed event. */ @Override public void processElement( Tuple2<String, String> in, Context ctx, Collector<Tuple2<String, Long>> out) throws Exception { switch (in.f1) { case "START": // set the start time if we receive a start event. startTime.update(ctx.timestamp()); // register a timer in four hours from the start event. ctx.timerService() .registerEventTimeTimer(ctx.timestamp() + 4 * 60 * 60 * 1000); break; case "END": // emit the duration between start and end event Long sTime = startTime.value(); if (sTime != null) { out.collect(Tuple2.of(in.f0, ctx.timestamp() - sTime)); // clear the state startTime.clear(); } default: // do nothing } } /** Called when a timer fires. */ @Override public void onTimer( long timestamp, OnTimerContext ctx, Collector<Tuple2<String, Long>> out) { // Timeout interval exceeded. Cleaning up the state. startTime.clear(); }} 这个例子说明了KeyedProcessFunction的表达能力,但也强调了它是一个相当冗长的接口。 DataStream API DataStream API 提供了许多通用流处理操作原语。如窗口,record-at-a-time转换,查询外部数据存储丰富事件原语。DataStream API 可用于 Java 和 Scala 且它是基于函数的,如 map()、reduce() 以及 aggregate() 。可以通过扩展接口或 lambda 函数来定义函数参数。 以下示例展示如何对点击流进行会话化以及记录每个session的点击次数。 // a stream of website clicksDataStream<Click> clicks = ...DataStream<Tuple2<String, Long>> result = clicks // project clicks to userId and add a 1 for counting .map( // define function by implementing the MapFunction interface. new MapFunction<Click, Tuple2<String, Long>>() { @Override public Tuple2<String, Long> map(Click click) { return Tuple2.of(click.userId, 1L); } }) // key by userId (field 0) .keyBy(0) // define session window with 30 minute gap .window(EventTimeSessionWindows.withGap(Time.minutes(30L))) // count clicks per session. Define function as lambda function. .reduce((a, b) -> Tuple2.of(a.f0, a.f1 + b.f1)); SQL & Table API Flink 有两种关系化 API 特性, Table API 和 SQL。这两个 API 都是用于批处理和流处理的统一API,即,在无边界的实时流或有边界的记录流上以相同的语义执行查询,并产生相同的结果。Table API 和 SQL 利用Apache Calicite来解析,校验以及查询优化。它们可以与 DataStream 和 DataSet API 无缝集成,并支持用户定义的标量,聚合以及表值函数。 Flink的关系化API旨在简化数据分析,数据流水线和ETL应用程序的定义。 以下示例展示如何对点击流进行会话化以及记录每个session的点击次数。与DataStream API中的示例是相同的用例。 SELECT userId, COUNT(*)FROM clicksGROUP BY SESSION(clicktime, INTERVAL '30' MINUTE), userId 库(Libraries) Flink 具有几个用于常见数据处理用例的库。这些库通常嵌入在API中,而不是完全独立的。因此,它们可以从API的所有特性中受益,并与其他库集成。 复杂事件处理(CEP): 模式检测是事件流处理中的一个非常常见的用例。Flink的CEP库提供了一个API来指定事件模式(如正则表达式或状态机)。CEP库与Flink的DataStream API集成,以便在DataStream上评估模式。CEP库的应用包括网络***检测,业务流程监控和欺诈检测。 DataSet API:DataSet API是Flink用于批处理应用程序的核心API。DataSet API的原语包括 map,reduce,(outer)join,co-group和iterate。所有操作都由算法和数据结构支持,这些算法和数据结构对内存中的序列化数据进行进行操作,如果数据大小超过内存预算则溢出到磁盘。Flink的DataSet API的数据处理算法收到传统数据库运算符的启发,例如混合散列连接或外部合并排序( hybrid hash-join or external merge-sort)。 Gelly:Gelly是一个可扩展的图形处理和分析库。Gelly是在DataSet API之上实现的,并与DataSet API集成在一起。因此,它受益于其可扩展且强大的操作符。Gelly具有内置算法,如label propagation(标签传播), triangle enumeration, and page rank, 但也提供了一个自定义图算法实现的简化Graph API。 操作 由于许多流应用程序设计为以最短的停机时间连续运行,因此流处理器必须提供出色的故障恢复,以及在应用程序运行时监控和维护应用程序的工具。 Apache Flink 非常关注流处理的操作方面。在这里,我们将解释 Flink 的故障恢复机制,并介绍其管理和监督正在运行的应用程序的特性。 全天候运行应用程序 机器和处理故障在分布式系统中无处不在。像Flink这样的分布式流处理器必须从故障中恢复,以便能够全天候运行流应用程序。显然,这不仅意味着在故障发生后重新启动应用程序,而且还要确保其内部状态保持一致,以便应用程序可以继续处理,就像从未发生过故障一样。 Flink提供了多种特性,以确保应用程序保持运行并保持一致: 一致的检查点:Flink的恢复机制基于应用程序状态的一致性检查点。如果发生故障,将重新启动应用程序并从最新检查点加载其状态。结合可重置的流源,此特性可以保证精确一次的状态一致性。 高效的检查点:如果应用程序保持TB级的状态,则检查应用程序的状态可能非常昂贵。Flink可以执行异步和增量检查点,以便将检查点对应用程序的延迟SLAs的影响保持在非常小的水平。 End-to-End精确一次:Flink为特定存储系统提供事务接收(sink)器,保证数据只写出一次,即使出现故障。 与集群管理器集成:Flink与集群管理器紧密集成,例如Hadoop YARN,Mesos或Kubernetes。当进程失败时,将自动启动一个新进程来接管它的工作。 高可用性设置:Flink具有高可用性模式特性,可消除所有单点故障。HA模式基于Apache ZooKeeper–是一种经过验证的可靠分布式协调服务。 更新、迁移、暂停和恢复应用程序 需要维护为关键业务服务提供支持的流应用程序。需要修复错误,并且需要实现改进或新功能特性。但是,更新有状态流应用程序并非易事。通常,我们不能简单地停止应用程序并重新启动固定版本或改进版本,因为无法承受丢失应用程序的状态。 Flink 的 Savepoints 是一个独特而强大的功能特性,可以解决更新有状态应用程序和许多其他相关挑战的问题。保存点是应用程序状态的一致快照,因此它与检查点非常相似。但是,与检查点相比,需要手动触发保存点,并且在应用程序停止时不会自动删除保存点。保存点可用于启动状态兼容的应用程序并初始化其状态。保存点可启用以下功能: 应用程序演变:保存点可用于发展应用程序。可以从从先前版本的应用程序中获取的保存点重新启动应用程序的固定或改进版本。也可以从较早的时间点(假设存在这样的保存点)启动应用程序,以修复由有缺陷的版本产生的错误结果。 集群迁移:使用保存点,可以将应用程序迁移(或克隆)到不同的集群。 Flink版本更新:可以使用保存点迁移应用程序在Flink的新版本上运行。 应用程序扩展:保存点可用于增加或减少应用程序的并行性。 A / B测试和假设情景:通过在同一保存点启动应用程序的所有版本,可以比较两个(或更多)不同版本的应用程序的性能或质量。 暂停和恢复:可以通过获取保存点来暂停应用程序并停止它。在以后的任何时间点,都可以从保存点恢复应用程序。 归档:保存点可以存档,以便能够将应用程序的状态重置为较早的时间点。 监控和控制应用程序 与任何其他服务一样,持续运行的流应用程序需要受到监督并集成到组织的运营(operations)基础架构(即监控和日志记录服务)中。监控有助于预测问题并提前做出反应。日志记录让我们可以依据根原因分析来调查故障。最后,控制运行应用程序的易于访问的界面也是一个重要特性。 Flink 与许多常见的日志记录和监视服务已经很好地集成,并提供 REST API 来控制应用程序和查询信息。 Web UI:Flink拥有Web UI功能特性,可以检查,监视和调试正在运行的应用程序。它还可用于提交执行或取消执行。 Logging: Flink实现了流行的slf4j日志记录接口,并与日志框架log4j或logback集成。 Metrics:Flink具有复杂的度量标准系统,用于收集和报告系统和用户定义的度量标准。度量标准可以导出到几个reporters,包括JMX,Ganglia,Graphite,Prometheus,StatsD,Datadog和Slf4j。 REST API:Flink暴露公开提交新应用程序,获取正在运行的应用程序的保存点或取消应用程序的REST API。REST API还公开元数据、收集到的正在运行的或已完成应用程序的指标。","tags":[{"name":"流式处理","slug":"流式处理","permalink":"http://www.seanxia.cn/tags/%E6%B5%81%E5%BC%8F%E5%A4%84%E7%90%86/"},{"name":"flink","slug":"flink","permalink":"http://www.seanxia.cn/tags/flink/"}]},{"title":"流式框架Flink(二)","date":"2019-01-01T16:00:00.000Z","path":"大数据/1b90121.html","text":"Apache Flink是一个用于对无边界和有边界数据流进行有状态计算的框架。 我们来谈谈 Flink 的编程模型与部署。 第一部分:Flink 编程模型 Flink 分层架构 Flink提供不同级别的抽象来开发流/批处理应用程序。 Stateful Stream Processing 位于最底层, 只提供有状态流,是 core API 的底层实现。 通过 Process Function 嵌入到 DataStream API中。 利用低阶,构建一些新的组件(比如:利用其定时做一定情况下的匹配和缓存)。 灵活性高,但开发比较复杂。 Core APIs(DataStream API/DataSet API) 在实践中,大多数应用程序不需要上述低级抽象,而是针对Core API 编程,如 DataStream API (有界/无界流)和 DataSet API(有界数据集)。这些流畅的API提供了用于数据处理的通用构建块,例如各种形式的用户指定的转换,连接,聚合,窗口,状态等。在这些 API 中处理的数据类型在相应的编程语言中表示为类。 DataStream:流处理 DataSet:批处理 Table & SQL SQL 构建在 Table 之上,都需要构建 Table 环境。 不同的类型的 Table 构建不同的 Table 环境。 Table 可以与 DataStream 或者 DataSet 进行相互转换。 Streaming SQL 不同于存储的 SQL, 最终会转化为流式执行计划。 Flink 构建的流程 构建计算环境(决定采用哪种计算执行方式) 创建Source(可以多个数据源) 对数据进行不同方式的转换(提供了丰富的算子) 对结果的数据进行Sink(可以输出到多个地方) Flink程序的基本构建块是流和转换。(请注意,Flink的 DataSet API 中使用的 DataSet 也是内部流)从概念上讲,流是(可能永无止境的)数据记录流,而转换是将一个或多个流作为一个或多个流的操作。输入,并产生一个或多个输出流。 执行时,Flink程序映射到流数据流,由流和转换运算符组成。每个数据流都以一个或多个源开头,并以一个或多个接收器结束。数据流类似于任意有向无环图 (DAG)。尽管通过迭代结构允许特殊形式的循环 ,但为了简单起见,我们将在大多数情况下对其进行掩饰。 通常,程序中的转换与数据流中的运算符之间存在一对一的对应关系。但是,有时一个转换可能包含多个转换运算符。 并行数据流 Flink中的程序本质上是并行和分布式的。在执行期间,流具有一个或多个流分区,并且每个运算符具有一个或多个运算符子任务。运算符子任务彼此独立,并且可以在不同的线程中执行,并且可能在不同的机器或容器上执行。 运算符子任务的数量是该特定运算符的并行度。流的并行性始终是其生成运算符的并行性。同一程序的不同运算符可能具有不同的并行级别。 流可以以一对一(或转发)模式或以重新分发模式在两个运营商之间传输数据: 一对一流(例如,在上图中的Source和map()运算符之间)保留元素的分区和排序。这意味着map()运算符的subtask [1] 将看到与Source运算符的subtask [1]生成的顺序相同的元素。 重新分配流(在上面的map()和keyBy / window之间,以及 keyBy / window和Sink之间)重新分配流。每个运算符子任务将数据发送到不同的目标子任务,具体取决于所选的转换。实例是 keyBy() (其通过散列密钥重新分区),广播() ,或重新平衡() (其重新分区随机地)。在重新分配交换中,元素之间的排序仅保留在每对发送和接收子任务中(例如,map()的子任务[1] 和子任务[2]keyBy / window)。因此,在此示例中,保留了每个密钥内的排序,但并行性确实引入了关于不同密钥的聚合结果到达接收器的顺序的非确定性。 window 什么是 window 聚合事件(例如,计数,总和)在流上的工作方式与批处理方式不同。例如,不可能计算流中的所有元素,因为流通常是无限的(无界)。相反,流上的聚合(计数,总和等)由window限定,例如*“在最后5分钟内计数”* 或*“最后100个元素的总和”*。 Windows可以是时间驱动的(例如:每30秒)或数据驱动(例如:每100个元素)。人们通常区分不同类型的窗口,例如翻滚窗口(没有重叠), 滑动窗口(具有重叠)和会话窗口(由不活动间隙打断)。 Window 类型 Count Window Time Window 自定义window Window 聚合日常会遇到的问题(数据过热,延迟数据丢弃, 反压等问题) Time 当在流程序中引用时间(例如定义窗口)时,可以参考不同的时间概念: Event Time:事件时间是创建事件的时间。它通常由事件中的时间戳描述,例如由生产传感器或生产服务附加。Flink通过时间戳分配器访问事件时间戳。 Ingestion Time:是事件在源操作员处输入Flink数据流的时间。 Processing Time:是执行基于时间的操作的每个操作员的本地时间。 例如:一条日志进入 Flink 的时间为2017-11-12 10:00:00,123, 到达 window 的系统时间为 2017-11-12 10:00:01,234. 日志的内容如下: 2017-11-02 18:37:15,624 INFO org.apache.hadoop.yarn.client.ConfiguredRMFailoverProxyProvider - Failing over to rm2 State 虽然数据流中的许多操作只是一次查看一个单独的事件(例如事件解析器),但某些操作会记住多个事件(例如窗口操作符)的信息。这些操作称为有状态。 状态操作的状态保持在可以被认为是嵌入式键/值存储的状态中。状态被分区并严格地与有状态运营商读取的流一起分发。因此,只有在keyBy()函数之后才能在键控流上访问键/值状态,并且限制为与当前事件的键相关联的值。对齐流和状态的密钥可确保所有状态更新都是本地操作,从而保证一致性而无需事务开销。此对齐还允许Flink重新分配状态并透明地调整流分区。 checkpoint Flink 使用流重放和检查点的组合实现容错。检查点与每个输入流中的特定点以及每个操作符的对应状态相关。通过恢复运算符的状态并从检查点重放事件,可以从检查点恢复流数据流,同时保持一致性*(恰好一次处理语义)*。 检查点间隔是在执行期间用恢复时间(需要重放的事件的数量)来折衷容错开销的手段。 轻量级容错机制(全局异步,局部同步) 保证exactly-once 语义 用于内部失败的恢复 基本原理 1、通过往 source 注入 barrier 2、barrier 作为 checkpoint 的标志 流处理过程中的状态历史版本 具有可以replay的功能 外部恢复(应用重启和升级) 两种方式触发 1、Cancel with savepoint 2、手动主动触发 Batch on Streaming Flink 执行批处理程序作为流程序的特殊情况,其中流是有界的(有限数量的元素)。甲数据集在内部视为数据流。因此,上述概念以相同的方式应用于批处理程序,并且它们适用于流程序,除了少数例外: 批处理程序的容错不使用检查点。通过完全重放流来进行恢复。这是可能的,因为输入有限。这会使成本更多地用于恢复,但使常规处理更便宜,因为它避免了检查点。 DataSet API 中的有状态操作使用简化的内存/核外数据结构,而不是键/值索引。 DataSet API 引入了特殊的同步(超级步骤)迭代,这些迭代只能在有界流上进行。 第二部分:Flink 运行时 运行时架构 Client JobManager TaskManger 角色间的通信(Akka) 数据的传输(Netty) Flink运行时包含两种类型的进程: 该 JobManagers(也称为Masters)协调分布式执行。他们安排任务,协调检查点,协调故障恢复等。 总是至少有一个Job Manager。高可用性设置将具有多个JobManagers,其中一个始终是领导者,其他人处于待机状态。 该 TaskManagers(也叫workers)执行任务(或者更具体地说,子任务)的数据流,以及缓冲器和交换数据流。 必须始终至少有一个 TaskManager。 JobManagers和TaskManagers可以通过多种方式启动:作为独立集群直接在计算机上,在容器中,或由 YARN 或Mesos 等资源框架管理。TaskManagers 连接到 JobManagers,宣布自己可用,并被分配工作。 该客户端是不运行时和程序执行的一部分,而是被用来准备和发送的数据流的 JobManager。之后,客户端可以断开连接或保持连接以接收进度报告。客户端既可以作为触发执行的 Java / Scala 程序的一部分运行,也可以在命令行进程中运行./bin/flink run ...。 Task Slots and Resources 每个worker(TaskManager)都是一个JVM进程,可以在不同的线程中执行一个或多个子任务。为了控制worker接受的任务数量,worker有所谓的任务槽(至少一个)。 每个任务槽代表TaskManager的固定资源子集。例如,具有三个插槽的TaskManager将其1/3的托管内存专用于每个插槽。切换资源意味着子任务不会与来自其他作业的子任务竞争托管内存,而是具有一定量的保留托管内存。请注意,这里没有CPU隔离; 当前插槽只分离任务的托管内存。 通过调整任务槽的数量,用户可以定义子任务如何相互隔离。每个TaskManager有一个插槽意味着每个任务组在一个单独的JVM中运行(例如,可以在一个单独的容器中启动)。拥有多个插槽意味着更多子任务共享同一个JVM。同一JVM中的任务共享TCP连接(通过多路复用)和心跳消息。它们还可以共享数据集和数据结构,从而减少每任务开销。 默认情况下,Flink允许子任务共享插槽,即使它们是不同任务的子任务,只要它们来自同一个作业。结果是一个槽可以保存作业的整个管道。允许此插槽共享有两个主要好处: Flink集群需要与作业中使用的最高并行度一样多的任务槽。无需计算程序总共包含多少任务(具有不同的并行性)。 更容易获得更好的资源利用率。没有插槽共享,非密集 源/ map()子任务将阻止与资源密集型窗口子任务一样多的资源。通过插槽共享,将示例中的基本并行性从2增加到6可以充分利用时隙资源,同时确保繁重的子任务在TaskManagers之间公平分配。 CoLocationGroup 保证所有的i-th 的sub-tasks 在同一个slots 主要用于迭代流 SlotSharingGroup 保证同一个group的i-th 的sub-tasks 共享同一个slots 算子的默认 group 为 default 怎么确定一个算子的 SlotSharingGroup(根据input的group 和自身是否设置group共同确定) 适当设置可以减少每个slot运行的线程数,从而整体上减少机器的负载 Slots & parallelism 一个应用需要多少Slots 不设置SlotSharingGroup(应用的最大并行度) 设置SlotSharingGroup (所有SlotSharingGroup中的最大并行度之和) Operator Chains and Task 对于分布式执行,Flink 链接 operator 和子任务一起放入任务。每个任务由一个线程执行。将 operators 链接到任务是一项有用的优化:它可以减少线程到线程切换和缓冲的开销,并在降低延迟的同时提高整体吞吐量。可以配置链接行为。 下图中的示例数据流由五个子任务执行,因此具有五个并行线程。 OperatorChain OperatorChain的优点 减少线程切换 减少序列化与反序列化 减少延迟并且提高吞吐能力 OperatorChain 组成条件 没有禁用Chain 上下游算子并行度一致 下游算子的出度为1 上下游算子在同一个 slot group 上下游算子之间没有数据 shuffle 第三部分:Flink on yarn原理,部署与生产 Flink 部署方式 Local Standalone Yarn Mesos Docker Kubernetes AWS Flink On Yarn ResourceManager NodeManager AppMaster( jobmanger 运行在其上) Container(taskamanager 运行在其上) YarnSession 选择on-Yarn 的理由 提高机器的利用率 Hadoop 开源活跃,且成熟 Flink job启动方式 提交 job(submitJob) 方式, 由开发者提交应用时候触发。 恢复 job 方式(recoverJob),由 Flink 系统内部触发,在 JobManager crash 情况下触发。 理解Job 的启动过程 Graph 不同阶段的转换 Scheduler Blob 服务 HA 保证 Graph StreamGraph JobGraph ExecuteGraph 物理执行图 HA服务 选举leader 持久化checkpoint 元数据 持久化最近成功的checkpoint 持久化提交的JobGraph(SubmittedJobGraph) 持久化BlobStore(用于保存应用的Jar) Flink On Yarn 部署 Zookeeper (HA) HDFS(checkpoint) YARN(资源调度) Flink Flink-conf 配置 Akka 方面配置 Checkpoint 方面配置 HA配置 内存配置 MetricReporter Yarn方面的配置 YarnSession 启动命令 -n (container) taskmanager的数量, 怎么确定? -s (slot) 每个taskmanager的slot 数量,默认一个slot 对应一个vcore -jm jobmanager的内存大小, 怎么设置 -tm 每个taskmanager的内存大小, 怎么设置 -qu Yarn的队列名字 -nm Yarn的应用名字 -d 后台执行 ./yarn-session.sh –n 2 -s 10 –jm 2048 –tm 10240 –qu root.default –nm test -d 应用启动命令 -j 应用的jar包 -a 应用的运行参数 -c 应用的主类 -p 应用的并行度 -yid yarnSession对应的appId -nm 应用名字, 在flink-ui 显示 -d 后台执行 ./flink run –j test.jar –a “test” –p 20 –yid appId –nm flink-test -d Flink On Yarn 部署总结 一个 YarnSession 一个应用,方便管理, 减少没必要应用之间的干扰。 Flink 提交平台化,支持 HDFS 的 Jar 包提交。 Flink On Yarn 的日志滚动, 以及改善 Flink UI 查看日志。","tags":[{"name":"流式处理","slug":"流式处理","permalink":"http://www.seanxia.cn/tags/%E6%B5%81%E5%BC%8F%E5%A4%84%E7%90%86/"},{"name":"flink","slug":"flink","permalink":"http://www.seanxia.cn/tags/flink/"}]},{"title":"SparkMLlib 随机森林","date":"2018-05-21T16:00:00.000Z","path":"大数据/1ca1f555.html","text":"一种非线性有监督分类模型 一种非线性有监督分类模型 随机森林是一种非线性有监督的分类模型。随机森林的决策树的升级版,由多个随机数据集的决策树组合而成。 决策树 决策树是一个预测模型,他代表的是对象属性与对象值之间的一种映射关系。 决策树是一种树形结构,其中每个内部节点表示一个属性上的测试,每个分支代表一个测试输出,每个叶节点代表一种类别。 决策树算法目的: 利用数据的一些规则来尽可能的降低数据集的不确定性,即给定一些特征来降低数据的不确定性。 决策树的核心思想: 就是在一个数据集中找到一个最优特征,根据这个最优特征将数据集分为多个子数据集,然后递归操作,直到满足指定条件为止。 案例:判断一个人是否有偿还能力 决策树是一种非线性有监督的分类模型。 线性分类模型比如说逻辑回归,可能会存在不可分问题,但是非线性分类就不存在这个问题。 如下图,决策树的非线性分割: 决策树案例分析 例如:我们来判断车祸与天气的关系。 数据离散化: 决策树是通过固定的条件来对类别进行判断: 决策树的生成: 数据不断分裂的递归过程,每一次分裂,尽可能让类别一样的数据在树的一边,当树的叶子节点的数据都是一类的时候,则停止分类。(相当于if else 语句) 决策树最优特征选取 决策树的生成最关键的条件是树的头部,也就是第一次分裂特征的选取,选取的好坏直接影响到分裂的效率。 由上图可看出,我们首先选取的天气进行分类,那为什么先选取天气这个特征呢。 假如我们首先选取的是湿度这个特征: 下面再选取天气这个特征: 我们会发现,只有选天气的这个特征,才能第一时间得到当天气为overcast时,是肯定发生车祸的。这样,我们后面就不用再分裂,结果直接就出来了。 分割方式 假如如下图,数据集有两种分割方式,你会选择哪种? 结论是:方式2会更好,因为分的会更彻底。方式1分完之后,彼此之间还是没有分开,效果不大。 评判准则 树的每一次分类,都有很多种选择标准,每种标准产生不同的分类结果,因此我们需要一个评判指标,看看哪种选择最合适。 评判标准是每一个叶子里面的类别尽可能一致。 幸运的是,Spark已经将评价标准作了很好的封装,用户只需调用API即可。Spark MLlib会自动帮我们去做。 最优特征怎么找? 这里就引入两个词,信息熵和条件熵。 信息熵条件熵 **信息熵 H(X)H(X)H(X):**数据集不确定性的比例。熵的定义如下: 其中 D 表示训练数据集,c 表示数据类别数,Pi 表示类别 i 样本数量占所有样本的比例 **条件熵 H(X,Y)H(X,Y)H(X,Y):**类似于条件概率,在知道 Y 的情况下,X 的不确定性 **信息增益:**代表熵的变化程度 特征 Y 对训练集 D 的信息增益:g(D,Y)=H(X)−H(X,Y)g (D,Y)=H(X)-H(X,Y)g(D,Y)=H(X)−H(X,Y) 决策树 API 演示 得到一颗树之后,我们就可以用这棵树来做预测了。 训练集: 汽车数据样本.txt 下面是Spark MLlib 代码: import org.apache.spark.mllib.tree.DecisionTreeimport org.apache.spark.mllib.tree.model.DecisionTreeModelimport org.apache.spark.mllib.util.MLUtilsimport org.apache.spark.rdd.RDDimport org.apache.spark.{SparkConf, SparkContext}object ClassificationDecisionTree { val conf = new SparkConf() conf.setAppName("analysItem") conf.setMaster("local[3]") val sc = new SparkContext(conf) def main(args: Array[String]): Unit = { val data = MLUtils.loadLibSVMFile(sc, "汽车数据样本.txt") // Split the data into training and test sets (30% held out for testing) val splits = data.randomSplit(Array(0.7, 0.3)) val (trainingData, testData) = (splits(0), splits(1)) //指明类别 val numClasses = 2 //指定离散变量,未指明的都当作连续变量处理 //1,2,3,4维度进来就变成了0,1,2,3 //这里天气维度有3类,但是要指明4,这里是个坑,后面以此类推 val categoricalFeaturesInfo = Map[Int, Int](0 -> 4, 1 -> 4, 2 -> 3, 3 -> 3) //设定评判标准 val impurity = "entropy" //树的最大深度,太深运算量大也没有必要 剪枝 val maxDepth = 3 //设置离散化程度,连续数据需要离散化,分成32个区间,默认其实就是32,分割的区间保证数量差不多 这个参数也可以进行剪枝 val maxBins =10 //生成模型 val model: DecisionTreeModel = DecisionTree.trainClassifier(trainingData, numClasses, categoricalFeaturesInfo, impurity, maxDepth, maxBins) //测试 val labelAndPreds: RDD[(Double, Double)] = testData.map { point => val prediction = model.predict(point.features) (point.label, prediction) } val testErr = labelAndPreds.filter(r => r._1 != r._2).count().toDouble / testData.count() println("Test Error = " + testErr) println("Learned classification tree model:\\n" + model.toDebugString) }} 单颗决策树的缺点 1、运算量大,需要一次加载所有数据进内存。并且找寻分割条件是一个极耗资源的工作。 2、训练样本中出现异常数据时,将会对决策树产生很大影响。抗干扰能力差,逻辑回归怎么解决抗干扰能力的? 解决方法: 1、减少决策树所需训练样本(不可取) 2、随机采样,降低异常数据的影响。和逻辑回归比,逻辑回归可以告诉我们概率,而决策树只能取 0 或 1 于是随机森林就出来了!!! 随机森林 随机森林是一种非线性有监督分类模型。 森林:由树组成 随机:生成树的数据都是从数据集中随机选取的。 生成方式 当数据集很大的时候,我们随机选取数据集的一部分,生成一棵树,重复上述过程,我们可以生成一堆形态各异的树,这些树放在一起就叫森林。 随机森林将数据放入模型,产生的结果少数服从多数!!! 分割方式 随机森林跟决策树一样,是非线性,不存在不可分割的问题。 随机森林VS逻辑回归 逻辑回归 随机森林 软分类 硬分类 线性模型 非线性模型 输出有概率意义 输出无概率意义 抗干扰能力强 抗干扰能力弱 森林的生成 森林是由树组成的,这里的树是决策树,训练的过程就是利用数据集生成决策树的过程。 随机森林案例分析 例如:我们来判断对收入的关系。 数据的三种类型 1、连续型可比较:收入,身高 2、离散型可半比较:学历 3、离散型无比较:行业 随机森林的优点 1、分布式,速度快 2、可以有效避开异常数据(少数服从多数) 随机森林 API 训练集还是使用决策树的训练集。 训练集: 汽车数据样本.txt 下面是随机森林 API 代码: import org.apache.spark.{SparkContext, SparkConf}import org.apache.spark.mllib.util.MLUtilsimport org.apache.spark.mllib.tree.RandomForestobject ClassificationRandomForest { val conf = new SparkConf() conf.setAppName("analysItem") conf.setMaster("local[3]") val sc = new SparkContext(conf) def main(args: Array[String]): Unit = { //读取数据 val data = MLUtils.loadLibSVMFile(sc, "汽车数据样本.txt") //将样本按7:3的比例分成 val splits = data.randomSplit(Array(0.7, 0.3)) val (trainingData, testData) = (splits(0), splits(1)) //分类数 val numClasses = 2 // categoricalFeaturesInfo 为空,意味着所有的特征为连续型变量 val categoricalFeaturesInfo = Map[Int, Int](0 -> 4, 1 -> 4, 2 -> 3, 3 -> 3) //树的个数 val numTrees = 3 //特征子集采样策略,auto 表示算法自主选取 //"auto"根据特征数量在4个中进行选择 // 1,all 全部特征 2,sqrt 把特征数量开根号后随机选择的 3,log2 取对数个 4,onethird 三分之一 val featureSubsetStrategy = "auto" //纯度计算 val impurity = "entropy" //树的最大层次 val maxDepth = 3 //特征最大装箱数,即连续数据离散化的区间 val maxBins = 32 //训练随机森林分类器,trainClassifier 返回的是 RandomForestModel 对象 val model = RandomForest.trainClassifier(trainingData, numClasses, categoricalFeaturesInfo, numTrees, featureSubsetStrategy, impurity, maxDepth, maxBins) //打印模型 println(model.toDebugString) //保存模型 //model.save(sc,"汽车保险") //在测试集上进行测试 val count = testData.map { point => val prediction = model.predict(point.features) // Math.abs(prediction-point.label) (prediction, point.label) }.filter(r => r._1 != r._2).count() println("Test Error = " + count.toDouble / testData.count().toDouble) }}","tags":[{"name":"机器学习","slug":"机器学习","permalink":"http://www.seanxia.cn/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"},{"name":"随机森林","slug":"随机森林","permalink":"http://www.seanxia.cn/tags/%E9%9A%8F%E6%9C%BA%E6%A3%AE%E6%9E%97/"}]},{"title":"SparkMLlib 逻辑回归","date":"2018-05-14T16:00:00.000Z","path":"大数据/a4a0dc78.html","text":"逻辑回归是预测分类响应的常用方法。这是广义线性模型的一个特例,可以预测结果的概率。 在spark.ml逻辑回归中,可以使用二项逻辑回归来预测二元结果,或者可以使用多项逻辑回归来预测多类结果。使用该family 参数在这两种算法之间进行选择,或者保持不设置,Spark将推断出正确的变量。 逻辑回归的基本概念 逻辑斯蒂回归(logistic regression)是统计学习中的经典分类方法,属于对数线性模型。logistic回归的因变量可以是二分类的,也可以是多分类的。 逻辑回归与线性回归的区别: 线性回归中 y 的值域在[-∞,+∞],不能很好的表示; 逻辑回归通过 sigmod 函数将线性回归作为一个系数传进来,值域被映射为[0,1],然后比如大于0.5为一类,小于0.5为一类 补充: 1、逻辑回归本质是求解二分类问题,一般所谓的预测就是分类; 2、所有的多分类的问题都可以转化为多个二分类的问题; 3、在Spark MLlib中二分类的话,1为正例,0为负例。 归结一句话就是:逻辑回归是一种线性有监督分类模型。 逻辑回归的公式: f(z)=11+e−zf(z)=\\frac{1}{1+e^{-z}} f(z)=1+e−z1 其中的 z=w_1x_1+w_2x_2+w_3x_3+…+w_nx_n+w_0,相当于多元线性回归。 使用案例 在医学界,广泛应用于流行病学中,比如探索某个疾病的危险因素,根据危险因素预测疾病是否发生,与发生的概率。比如探讨胃癌,可以选择两组人群,一组是胃癌患者,一组是非胃癌患者。因变量是“是否胃癌”,这里“是”与“否”就是要研究的两个分类类别。自变量是两组人群的年龄,性别,饮食习惯,等等许多(可以根据经验假设),自变量可以是连续的,也可以是分类的。 在金融界,较为常见的是使用逻辑回归去预测贷款是否会违约,或放贷之前去估计贷款者未来是否会违约或违约的概率。 在消费行业中,也可以被用于预测某个消费者是否会购买某个商品,是否会购买会员卡,从而针对性得对购买概率大的用户发放广告,或代金券等等,进行精准营销。 前面我们说到过逻辑回归是一种用于分类的模型,就相当于y=f(x),表明输入与输出(类别)的关系。最常见问题有如医生治病时的望、闻、问、切,之后判定病人是否生病或生了什么病,其中的望闻问切就是输入,即特征数据,判断是否生病就相当于获取因变量y,即分类结果。 案例:逻辑回归应用于二分类问题 个人信用预测 保险公司在卖保险时,根据个人基本信息判断获赔概率 二分类: 要么A类,要么B类 比如人的身体状况为2中:1.健康;2生病 健康的可能性是1,生病的可能性就是0; 健康的可能性是0.8,生病的可能性是0.2; 健康的可能性是0.3,生病的可能性是0.7。 逻辑回归二分类:测试结果>0.5为正例----获赔,测试结果y<0.5为负例—不获赔 训练集:健康状况训练集 健康状况训练集.txt 在案例问题中: 训练: 确定w的过程,就是训练过程,spark的mllib已经做好了封装,只需调用即可 **singmod函数:**singmod函数为将线性变为非线性 函数图 如何判定结果: Spark MLlib中应用逻辑回归解决二分类问题的代码: import org.apache.spark.mllib.classification.{LogisticRegressionWithLBFGS}import org.apache.spark.mllib.util.MLUtilsimport org.apache.spark.{SparkConf, SparkContext}object LogisticRegression1 { def main(args: Array[String]) { val conf = new SparkConf().setAppName("spark").setMaster("local[3]") val sc = new SparkContext(conf) //加载基于SVM(支持向量机)算法的数据格式的文本文件: //底层将类似1 1:57 2:0 3:0 4:5 5:3 6:5转化为LabeledPoint val inputData:RDD[LabeledPoint] = MLUtils.loadLibSVMFile(sc, "健康状况训练集.txt") //随机的将数据集切分为70%的训练集和30%的测试集, //训练集和测试集的选择:一般为训练集70%~80%,测试集为20%~30% //seed为随机种子,随机种子固定,则每次切分的训练集和测试集一样, //不信请使用下面的TestRandomSplit方法测试 //seed :随机种子的作用:测试,固定每次切分相同的数据集和测试集,方便调试代码 val splits:Array[RDD] = inputData.randomSplit(Array(0.7, 0.3), seed = 1L) val (trainingData, testData) = (splits(0), splits(1)) //使用LBFGS的优化方式创建一个逻辑回归的模型 //也可以使用SGD的优化方式,一般来说LBFGS的优化方式更好 val lr = new LogisticRegressionWithLBFGS() //训练模型:确定W的过程,即求方程组: val model = lr.run(trainingData) //testData为LabeledPoint:(标签,特征向量) val result = testData //把特征放入模型中测试:predict()方法底层封装了求解z=w(训练出来的模型)*x(特征)的过程 //误差=真实y值-测试出的y值 取绝对值:测试正确为0,错误为1 .map{point=>Math.abs(point.label-model.predict(point.features)) } //错误率:result.mean():将错误的数量求均值即错误率,正确率只是评价模型的一种指标 println("正确率="+(1.0-result.mean())) //将模型参数取出变为数组打印输出 println(model.weights.toArray.mkString(" ")) //打印输出截距w0,打印出来的结果全部会是0.0 println(model.intercept) }}//测试方法import org.apache.spark.{SparkConf, SparkContext}object TestRandomSplit { def main(args: Array[String]): Unit = { val conf = new SparkConf().setMaster("local").setAppName("TestRandomSplit") val sc = new SparkContext(config = conf) val arr = Array(1 ,2 ,3, 4, 5, 6, 7, 8, 9, 10) val rdd = sc.parallelize(arr) val temp = rdd.randomSplit(Array(0.7,0.3), seed = 10L) val (trainSet, testSet) = (temp(0), temp(1)) trainSet.foreach(println(_)) testSet.foreach(println(_)) }} 那么,上面的代码是中所谓的训练模型到底是怎么回事呢? 请看图: 何为训练模型: 从历史数据中,我们可以知道,个人的健康状况和用户的特征息息相关(x0...x6x_0...x_6x0...x6),因此通过 xxx 和初始 www(初始值一般不全为0),通过逻辑回归将 xxx 带入 z=w1x1+w2x2+w3x3+w4x4+w5x5+w6x6z=w_1x_1+w_2x_2+w_3x_3+w_4x_4+w_5x_5+w_6x_6z=w1x1+w2x2+w3x3+w4x4+w5x5+w6x6 中,从而得到一个预测的健康状况。然后将真实的健康状况和预测的健康状况进行对比求错误,然后通过错误,不断的调整 www 的值,反复迭代。直到错误接近于 0 的最优解,即错误最小的时刻。那么对应的模型 www 即为最后的模型。 如何最快的求错误最小的时刻?-----调优,后面详细讲解 有了训练出来的模型后就要测试模型的准确度,将测试数据带入模型即可求解: 换一种角度理解逻辑回归:逻辑回归分界线 既然逻辑回归是线性有监督的分类模型,那么对于分类,分类的分界线在哪里呢? 对于二分类来说那肯定是0.5: 很明显,求解分界线就是求解方程组: 那么就有: 那么 w1x1+w2x2+w0=0w_1x_1+w_2x_2+w_0=0w1x1+w2x2+w0=0,对应于平面的一条直线,这条分解线就是分类线,将类别划分开来: 对应当其类比到高维空间,也是同样的原理。 三维的需要一个平面作为分界线,四维的需要立体空间作为分界线 . . . 总结一句就是: **求模型就是根据已知的数据集寻找分界线,平面,立体空间 . . . ** 对于二分类即找到一条直线将数据切分开,那么有无W0W_0W0,对构成什么样的直线就很重要了 我们知道直线的公式是:y=kx+w0y=kx+w_0y=kx+w0,有无 w0w_0w0,即如图: 那么总结一句:逻辑回归的本质:就是有w0 vs 无w0 现在我们用代码来测试一下: import org.apache.spark.mllib.classification.{LogisticRegressionWithLBFGS, LogisticRegressionWithSGD}import org.apache.spark.mllib.regression.LabeledPointimport org.apache.spark.mllib.util.MLUtilsimport org.apache.spark.rdd.RDDimport org.apache.spark.{SparkConf, SparkContext}object LogisticRegression2 { def main(args: Array[String]) { val conf = new SparkConf().setAppName("spark").setMaster("local[3]") val sc = new SparkContext(conf) val inputData: RDD[LabeledPoint] = MLUtils.loadLibSVMFile(sc, "w0测试数据.txt") val splits = inputData.randomSplit(Array(0.7, 0.3)) val (trainingData, testData) = (splits(0), splits(1)) val lr = new LogisticRegressionWithLBFGS() // Spark Mllib封装的W0的设置,true为要有w0 lr.setIntercept(true) val model=lr.run(trainingData) val result=testData .map{point=>Math.abs(point.label-model.predict(point.features)) } println("正确率="+(1.0-result.mean())) println(model.weights.toArray.mkString(" ")) println(model.intercept) }} 是否应该有w0的测试数据: w0测试数据.txt 测试过后你会明显的发现,有无正确率差异比较大。 逻辑回归遇到线性不可分 刚刚还说逻辑回归是线性有监督的分类模型,既然是线性的,那么就会遇到线性不可分的问题: 上面图中,你找不到一条线可以将其数据分开的,这个就是线性不可分问题,那么怎么解决呢? 升维解决线性不可分的问题: 一般是将已知的维度两两相乘,将其映射到高维的空间,那么就能找到一个平面将其分开。 Spark Mllib中代码演示 import org.apache.spark.mllib.classification.{LogisticRegressionWithLBFGS, LogisticRegressionWithSGD}import org.apache.spark.mllib.linalg.Vectorsimport org.apache.spark.mllib.regression.LabeledPointimport org.apache.spark.mllib.util.MLUtilsimport org.apache.spark.{SparkConf, SparkContext}object LogisticRegression3 { def main(args: Array[String]) { val conf = new SparkConf().setAppName("spark").setMaster("local[3]") val sc = new SparkContext(conf) // 解决线性不可分我们来升维,升维有代价,计算复杂度变大了 val inputData = MLUtils.loadLibSVMFile(sc, "线性不可分数据集.txt") .map { //将标注点取出,取出标签和特征 labelpoint => //标签 val label = labelpoint.label //特征 val feature = labelpoint.features //升维 val array = Array(feature(0), feature(1), feature(0) * feature(1)) //特征转为向量 val convertFeature = Vectors.dense(array) //转为新的标注点 new LabeledPoint(label, convertFeature) } val splits = inputData.randomSplit(Array(0.7, 0.3)) val (trainingData, testData) = (splits(0), splits(1)) val lr = new LogisticRegressionWithLBFGS() lr.setIntercept(true) val model = lr.run(trainingData) val result = testData .map { point => Math.abs(point.label - model.predict(point.features)) } println("正确率=" + (1.0 - result.mean())) println(model.weights.toArray.mkString(" ")) println(model.intercept) }} 线性不可分数据集: 线性不可分数据集.txt 测试过后你会明显的发现,升维过后正确率有明显的上升。 在真实的生产中,轻易间一般不会出现线性不可分的升维问题,只有当出现准确率大幅度下降的时候猜测应该是线性不可分的问题时候,才特征两两组合升维,测试看效果。 总结: 逻辑回归是一种线性 有监督分类模型,既然是有监督的,那么数据集中就应该有要预测的真实数据 y,既然是分类问题,那么有无 W0W_0W0 就很影响正确率,既然是线性模型,那么就会遇到线性不可分的问题,轻易间一般不会出现线性不可分的升维问题,当出现准确率大幅度下降的时候猜测应该是线性不可分的问题时候,才特征两两组合升维,测试看效果。 关于阈值分析 逻辑回归是求解二分类问题,那么分类的阈值是:结果>0.5为正例,结果<0.5为负例 首先,先来思考一个问题?判断一个病人是否患癌症,判断情况(1)的风险大,还是判断情况(2)的风险大? (1): 假如病人是癌症: – 判断成不是癌症<0.5 (2)假如病人是非癌症: – 判断是癌症>0.5 当然,很多人认为是第二种情况是糟糕的,但是第一种情况的后果更加的严重,病人承受的风险会加大,那么我们怎么通过调优模型来规避这样的风险呢? 我们可以不使用逻辑回归的默认的分类阈值0.5,去除固定的阈值0.5,根据业务场景调整相应的情况调整阈值,比如说0.3, 虽然整体的错误率变大了,但是规避了一些不能接受的风险。 很明显,当你规避了一些不能接受的的风险以后,相应的模型的正确率下降了,错误率提高了。 好,现在使用之前的健康状况数据集,使用Spark MLlib的封装函数测试: import org.apache.spark.mllib.classification.{LogisticRegressionWithLBFGS, LogisticRegressionWithSGD}import org.apache.spark.mllib.util.MLUtilsimport org.apache.spark.{SparkConf, SparkContext}object LogisticRegression4 { def main(args: Array[String]) { //去除阈值,规避风险 val conf = new SparkConf().setAppName("spark").setMaster("local[3]") val sc = new SparkContext(conf) val inputData = MLUtils.loadLibSVMFile(sc, "健康状况训练集.txt") val splits = inputData.randomSplit(Array(0.7, 0.3)) val (trainingData, testData) = (splits(0), splits(1)) val lr = new LogisticRegressionWithLBFGS() lr.setIntercept(true)//没去除固定的阈值的测试代码// val model = lr.run(trainingData)// val result = testData// .map{point=>Math.abs(point.label-model.predict(point.features)) }// println("正确率="+(1.0-result.mean()))// println(model.weights.toArray.mkString(" "))// println(model.intercept)//去除固定的阈值的代码//Spark MLlib封装的去除固定的阈值的方法:.clearThreshold() val model = lr.run(trainingData).clearThreshold() val errorRate = testData.map{p=> val score = model.predict(p.features) // 去除了固定的阈值以后,需要设置阈值,然后进行比较//癌症病人宁愿判断出得癌症也别错过一个得癌症的病人 val result = score>0.4 match {case true => 1 ; case false => 0} Math.abs(result-p.label) }.mean() println(1-errorRate) }} 误差的优化:梯度下降分析 前面提到了训练模型的过程,现在再来思考一下: 图中的计算误差是一个反复迭代的过程,相应的调整模型W,那么怎么样才能求得最小的误差,获得最好的模型呢?其中我们可以使用一种叫梯度下降的方法(GD–gradient descent) 先看逻辑回归的误差函数: 这个是其中的一个数据的误差函数: 参数解释: T为线性代数中的矩阵转置 那么,逻辑回归的 singmod 函数就变成了: 我们将类比下图看误差函数: 对于一组参数: 对应误差: 那么对于所有的参数,总误差就是: 很明显,找到参数对应的最矮的那堵墙的时候,就找到了最小的误差,上图最小的误差是参数4对应的误差,那么怎么才能以最快的速度求解最小的误差呢?在无数的参数对应的误差有点类似于一座山,现在考虑的就是怎么以最快的速度下山的问题? 怎么以最快的速度下山呢?-----随机梯度下降法 最快的下山的方式是每次都走 “最陡峭的道路”,最陡峭的道路很明显是最 “斜率” 的绝对值最大的,当走完上次最陡峭的路后,然后再找到最陡峭的路,直到走到谷底,这个过程就是随机梯度下降法。 SGD(随机梯度下降法)就是一种优化误差函数的方法:以最快的速度求解最小误差。 所谓的梯度就是斜率,每次都找到斜率的绝对值最大的,然后就能最快的求解最小的误差。 说白了就是求误差函数斜率为 0 的时刻!!! 那么训练的目的就是找到合适的 θ ,使得整体误差最小,即达到山底。 即最小化下式: 上面的误差函数被证明是凹函数(有极小值,无极大值),存在全局最优解,那么求解最小值的过程就是求解极值的过程。求极值就是让导数等于0,求解 θ ,也就是 w_0,w_1,w_2,…,w_n 但是从上函数可以看到几乎无法求解! 对于误差函数求导是比较困难,我们可以逆向思维,带入不同的 w0,w1,w2,...,wnw_0,w_1,w_2,...,w_nw0,w1,w2,...,wn,反复的迭代直到找到斜率为0的时刻。 那么就有: 误差 J 随 θ变化图: 把误差函数当成一座山,梯度就是往前走时陡峭程度的数字化表现。 绝对值越大,此时山越陡峭 梯度>0,往前走是上山 梯度<0,往前走是下山 梯度=0,山谷或者山峰 总结: 梯度下降法就是一个找寻一座山最低谷的过程: 如果当前往前走是上山,那么就后退; 如果当前往前走是下山,那么就前进; 不停的走,每走一步看下当前路况,决定下一步是前进还是后退,如此反复。 公式表示为: 梯度下降示意图: 鲁棒性调优 鲁棒性出现的原因和目的: 原始误差过于拟合,牺牲一些正确率来提高推广能力,防止过拟合!!! 鲁棒性调优的作用: 让你的模型更加的健壮,提高模型的通用性,推广能力、泛化能力。 对于表达式:w_1x_1+w_2x_2+w_0=0,对应于平面一条直线 下述两个公式描述同一条直线,哪个好? 在求 z 的过程中:z=w_1x_1+w_2x_2+w_3x_3+…+w_nx_n,如果 w 越大,求得的 z 对逻辑回归的 sigmoid 函数影响就越大,抗干扰的能力就越小,小小的变化就会影响到最后的分类结果。 总结:W 越小,模型的抗干扰能力越强。 肯定,不是说 w 越小就越好,那怎么找到一组适合的最小W呢? 加上正则提高你模型的推广能力-----设置lambda系数 首先来看正则化公式: L1正则>0: L2正则>0: 为了提高模型的泛化能力(推广能力—不过拟合----适应更多的未来的新的数据,做到举一反三),需要重写误差函数,加入认为的惩罚系数 λ\\lambdaλ: lambda( λ )是系数,区间在[0,1]: lambda是 w 的权重,因此我们可以通过调整 lambda 的大小来决定是更加的看重模型的准确率还是更加的看重模型的推广能力,一般情况从经验来看,会把 lambda 设置为0.4 重写误差函数必定会牺牲一定的正确率,但是能提高模型的推广能力。牺牲正确率来提高推广能力!!! 正则 L1 和L2 怎么选择呢 – 区别? L1更加的倾向于使得 www 要么取1,要么取0。也称为(lasso回归)稀疏编码,可用来降维。 L2更加的倾向于使得 www 整体偏小。也称为岭回归(rige回归) 一般L2更常用 关于回归技术,请参考:http://www.csdn.net/article/2015-08-19/2825492 Spark MLlib中的实现L1或者L2代码测试: import org.apache.spark.mllib.classification.{LogisticRegressionWithLBFGS, LogisticRegressionWithSGD}import org.apache.spark.mllib.optimization.{L1Updater, SquaredL2Updater}import org.apache.spark.mllib.util.MLUtilsimport org.apache.spark.{SparkConf, SparkContext}object LogisticRegression5 { def main(args: Array[String]) { //构建L1,L2 val conf = new SparkConf().setAppName("spark").setMaster("local[3]") val sc = new SparkContext(conf) val inputData = MLUtils.loadLibSVMFile(sc, "健康状况训练集.txt") val splits = inputData.randomSplit(Array(0.7, 0.3)) val (trainingData, testData) = (splits(0), splits(1)) //只支持L2,LBFGS:每次调整考虑所有的数据 //SGD:每次抽取一部分数据 -> 随机梯度下降算法 //LBFGS每次训练的时候取全数据进行 -> 拟牛顿法 val lr = new LogisticRegressionWithLBFGS() //SGD---随机梯度下降法,随机的提取一部分数据,L1和L2都支持 //val lr1 = new LogisticRegressionWithSGD() lr.setIntercept(true) //设置L1或者L2,使用L1正则和L2正则的用处是便于提高模型的推广能力,相当于重写了误差函数 // lr.optimizer.setUpdater(new L1Updater) //设置正则化L2,倾向于将W整体下降 lr.optimizer.setUpdater(new SquaredL2Updater) // 这块设置的是我们的lambda(惩罚系数),越大越看重这个模型的推广能力,一般不会超过1,0.4是个比较好的值 lr.optimizer.setRegParam(0.4) val model = lr.run(trainingData) val result=testData .map{point=>Math.abs(point.label-model.predict(point.features)) } println("正确率="+(1.0-result.mean())) println(model.weights.toArray.mkString(" ")) println(model.intercept) }} 训练方法优化 在 Spark MLlib 中 SGD 和 LBFGS 实现的区别: 拟牛顿法(LBFGS):val lr = new LogisticRegressionWithLBFGS,求二阶导数,相比SGD能更快更准确的收敛,每次迭代用到的训练集里面全部的数据,还可以做多分类。没有L1正则化,只有L2正则化(当不需要L1正则化时,强烈建议使用LBFGS) 随机梯度下降算法(SGD):val lr = new LogisticRegressWithSDG,求一阶导数,相比基础梯度下降算法,它无需环顾四周360°,每次迭代随机抽取一部分训练集数据,求导,只能做二分类。L1和L2正则化都有。 L- BFGS 为 SGD 的优化方法,它的训练速度比 SGD 快。 数值优化 数值优化不会影响正确率,只会提升求解模型的速度! 数值优化一 案例:某个地区的生态环境和动物数量的关系 观察上面的老虎的数量和麻雀的数量,可以知道老虎的数量和麻雀的数量差别很大,那么这样的输入数据会造成什么样的后果呢? 论点A:各个维度的输入如果在数值上差异很大,那么会引起正确的 w 在各个维度上数值差异很大。 论点B:找寻 w 的时候,对各个维度的调整基本上是按照同一个数量级来进行调整的(随机梯度下降的步长是一样的)。 发现 A 和 B 两条结论互相矛盾!!! 因为:0=w_1x_1+w_2x_2+w_3x_3+…+w_nx_n,当 x 的值差异很大的时候,www 的值也差异很大。 解决X的值差异很大的解决方案?------归一化 归一化的方法: (1)最大值最小值法 min-max标准化(Min-Max Normalization) 公式: **优点:**归一化的值一定在0~1之间 **缺点:**缺点是抗干扰能力弱,受离群值得影响比较大,导致0~1之间分布不均匀,中间容易 没有数据。 (2)方差归一化 **优点:**抗干扰能力强,和所有数据都有关,求标准差需要所有值的介入,重要有离群值的话,会被抑制下来。 **缺点:**是最终未必会落到0到1之间,牺牲归一化结果为代价提高稳定。 优化后: 总结: 归一化的目的:归一化的目的是消除 x 之间的过大差异,从而消除 w 之间的过大差异,从而使得在训练(梯度下降法)的时候,使得 w 之间的变化是同步、均匀的,从而使得你求解机器学习模型的速度加快! 数值优化二 在训练模型,不断的调整 w 的时候: 参数解释: x_{i1}和 x_{i2} 是历史数据 α 是调整的步长 A 是斜率 wtw^twt 是 t 时刻的 www,wt+1w^{t+1}wt+1是 t+1 时刻的 www,在不断的调整 www 的时候,wt+1w^{t+1}wt+1是随着 xxx 同时变化的 问题:w1w_1w1 和 w2w_2w2 只能朝一个方向变化。要么同时变大,要么同时变小 如上图:以最快的速度从WtW_tWt 到 W∗W_*W∗时刻,直线是最快的,调整 W 的时候,沿着蓝线的方向调整,WtW_tWt 减小,W∗W_*W∗ 变大。 从上面的公式,我们知道数据 xxx 决定了 www 的调整方向,让 xxx 有正有负就能调整 www 的方向。 因此,解决方法:尽可能让 x 的各个维度取值上有正有负! 均值归一化—每个数量减去平均值 方差归一化,均值归一化后: 均值归一化后,沿着正确的方法调整 www 的可能性更大,求解 www 的速度更快了 测试数据集: 环境分类数据.txt Spark MLlib实现数值优化代码: import org.apache.spark.mllib.classification.{LogisticRegressionWithLBFGS, LogisticRegressionWithSGD}import org.apache.spark.mllib.feature.StandardScalerimport org.apache.spark.mllib.regression.LabeledPointimport org.apache.spark.mllib.util.MLUtilsimport org.apache.spark.{SparkConf, SparkContext}object LogisticRegression6 { def main(args: Array[String]) { val conf = new SparkConf().setAppName("spark").setMaster("local[3]") val sc = new SparkContext(conf) val inputData = MLUtils.loadLibSVMFile(sc, "环境分类数据.txt") //将特征值从源数据中抽取出来 val vectors = inputData.map(_.features) //new一个标准归一化的实例,withMean=true(均值归一化), withStd=true(方差归一化) val scalerModel = new StandardScaler(withMean=true, withStd=true).fit(vectors) val normalizeInputData = inputData.map{point => //将标签取出来 val label = point.label //transform(稠密的向量),得到归一化的features val features = scalerModel.transform(point.features.toDense) //返回归一化的LabeledPoint new LabeledPoint(label,features) } val splits = normalizeInputData.randomSplit(Array(0.7, 0.3)) val (trainingData, testData) = (splits(0), splits(1)) val lr=new LogisticRegressionWithLBFGS() lr.setIntercept(true) val model = lr.run(trainingData) val result=testData .map{point=>Math.abs(point.label-model.predict(point.features)) } println("正确率="+(1.0-result.mean())) println(model.weights.toArray.mkString(" ")) println(model.intercept) }} 逻辑回归总结 Logistic Regression:是一种线性有监督的分类模型。 1、设置 w0w_0w0 因为如果不设置 w0w_0w0 就会使得分界一定会穿过原点,这样就会使得计算的模型受到很大的局限性,设置方法:lr.setIntercept(true) 2、线性不可分 遇到线性不可分的问题,需要升高维度,方法是让已有的维度俩俩相乘来构建更多的维度。 .map { labelpoint => val label = labelpoint.label val feature = labelpoint.features val array = Array(feature(0), feature(1), feature(0) * feature(1)) val convertFeature = Vectors.dense(array) new LabeledPoint(label, convertFeature)} 3、Threshold 阈值 Threshold 阈值在 LR 里面默认是根据 0.5 来进行二分类的,为了去规避一些风险,我们可以去掉阈值,这样最后 LR 给的结果就是0到1之间的一个分值,我们可以根据分值,自己来定到底属于哪个类别 TradeOff:人为的去调整阈值,最后的正确率一定会下降 val model = lr.run(trainingData).clearThreshold() 4、鲁棒性调优 鲁棒性:模型的通用性,举一反三的能力,推广能力,泛化能力 尽可能在保证正确率的情况下,使得W越小越好! 好到什么程度? 定义 L1 正则化和 L2 正则化,然后重写误差函数让算法去减小误差的同时减小 L1 或 L2。 L1:有的趋近于1,有的趋近于0,稀疏编码 L2:整体的W同时变小,岭回归 TradeOff: 人为的去重写了误差函数,会导致最后的正确率一定会下降 lr.optimizer.setUpdater(new SquaredL2Updater)lr.optimizer.setRegParam(0.4) 5、数值优化 (1)方差归一化:会考虑到一组数里面的所有数据,就是每个数去除以方差 优点:就是会使得各个W基本数量级一致 缺点:所有的值未必都会落到0到1之间 (2)均值归一化:让找到最优的速度变快,让各个维度的数据有正有负,就会使得各个W在调整的时候有的变大有的变小 val scalerModel = new StandardScaler(withMean=true, withStd=true).fit(vectors) 6、SGD 和 LBFGS的区别 SGD:随机梯度下降法,每次迭代随机抽取一部分训练集数据,求导,只能做二分类。 LBFGS:拟牛顿法,速度比较快求二阶导数,每次迭代用到训练集里面所有的数据,还可能做多分类。 公司使用:直接使用 LBFGS val lr=new LogisticRegressionWithSGD()val lr=new LogisticRegressionWithLBFGS()","tags":[{"name":"机器学习","slug":"机器学习","permalink":"http://www.seanxia.cn/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"},{"name":"逻辑回归","slug":"逻辑回归","permalink":"http://www.seanxia.cn/tags/%E9%80%BB%E8%BE%91%E5%9B%9E%E5%BD%92/"}]},{"title":"SparkMLlib线性回归","date":"2018-05-11T16:00:00.000Z","path":"大数据/30277e10.html","text":"关于机器学习,Spark MLlib中也对相关算法有API的讲解,本章介绍线性回归算法。 线性回归简介 什么是回归 回归问题主要关注确定一个唯一的因变量(dependent variable)(需要预测的值)和一个或多个数值型的自变量(independent variables)(预测变量)之间的关系。 广义线性回归,GLM。比如,逻辑回归,泊松回归。 对于简单线性回归问题,也就是小学大家就都会了的解应用题y=a+bx。 线性回归用来做预测,是一种线型有监督的预测模型。 线性回归模型保存的是权重系数 w,它是用历史数据找出规律用于预测未来。 **模型:**模型就是 y=ax1+bx2+…+d 这个公式 **建模/训练模型:**求出这个 y=ax+d 公式的过程,即用训练集数据求出 a、b 的过程。 **训练集:**参与求出 y=ax+b 公式过程的数据就是训练集,即历史数据(100万个点) **测试集:**用于检测模型准确度的数据,应当额外寻找一些数据来测试,将训练集的数据排除在外。 损失函数(误差函数): J(θ0,θ1)=12m∑i=1m(hθ(xi)−yi)2J(\\theta_0,\\theta_1)=\\frac{1}{2m}\\sum_{i=1} ^m(h_\\theta(x^i) - y^i)^2 J(θ0,θ1)=2m1i=1∑m(hθ(xi)−yi)2 **拟合函数:**y = ax + b 最小二乘法 一个点 (x1,y1) 误差:(ax1 + b - y1) ² 多个点的误差,又叫损失函数: 二维: 三维: 梯度下降法 用于快速找到误差最小值。 梯度下降法就是一个寻找一座山最低谷的过程。 1、如果当前往前走是上山,那么就后退; 2、如果当前往前走是下山,那么就前进; 3、不停的走,每走一步看下当前路况,决定下一步是前进还是后退,如此反复。 基本步骤: 先确定向下一步的步伐大小,我们称为Learning rate; 任意给定一个初始值; 确定一个向下的方向,并向下走预先规定的步伐,并更新; 当下降的高度小于某个定义的值,则停止下降。 问题 如果初始值就在local minimum的位置,则会如何变化? 如果取到一个正确的值,则cost function应该越来越小,怎么取值@ @过大会有什么问题? 过拟合怎么解决? 多元线性回归 大多数现实世界的分析不止一个自变量,大多数情况下,很有可能使用多元线性回归。 相关系数 两个变量之间的相关系数是一个数,它表示两个变量服从一条直线的关系有多么紧密。 关系数就是指Pearson相关系数,它是数学家Pearson提出来的,相关系数的范围是[-1,1]之间,两端的值表示一个完美的线性关系。","tags":[{"name":"机器学习","slug":"机器学习","permalink":"http://www.seanxia.cn/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"},{"name":"线性回归","slug":"线性回归","permalink":"http://www.seanxia.cn/tags/%E7%BA%BF%E6%80%A7%E5%9B%9E%E5%BD%92/"}]},{"title":"SparkMLlib Kmeans聚类","date":"2018-05-09T16:00:00.000Z","path":"大数据/31efd837.html","text":"Kmeans聚类算法又叫K均值聚类。 聚类:给事物打标签,寻找同一组内的个体之间的一些潜在的相似模式。力图找到数据的自然分组 kmeans。 理解Kmeans聚类算法 聚类是一种无监督的机器学习任务,它可以自动将数据划分成类 cluster。因此聚类分组不需要提前被告知所划分的组应该是什么样的。因为我们甚至可能都不知道我们再寻找什么,所以聚类是用于知识发现而不是预测。 聚类原则是一个组内的记录彼此必须非常相似,而与该组之外的记录截然不同。所有聚类做的就是遍历所有数据然后找到这些相似性。 使用距离来分配和更新类 探究距离测度 欧氏距离测度(EuclideanDistanceMeasure) 平方欧氏距离测度(SquaredEuclideanDistanceMeasure) 曼哈顿距离测度(ManhattanDistanceMeasure) 图中红线代表曼哈顿距离,绿色代表欧氏距离,也就是直线距离,而蓝色和黄色代表等价的曼哈顿距离。曼哈顿距离——两点在南北方向上的距离加上在东西方向上的距离,即d(i,j)=|xi-xj|+|yi-yj|。对于一个具有正南正北、正东正西方向规则布局的城镇街道,从一点到达另一点的距离正是在南北方向上旅行的距离加上在东西方向上旅行的距离,因此,曼哈顿距离又称为出租车距离。 余弦距离测度(CosineDistanceMeasure) 谷本距离测度(TanimotoDistanceMeasure) 同时表现夹角和距离的距离测度。 加权距离测度(WeightedDistanceMeasure) 选择适当的聚类数 肘部法 Kmeans算法 以空间中K个点为中心进行聚类,对最靠近他们的对象归类,通过迭代的方法,逐次更新各聚类中心的值,直到得到最好的聚类结果。 聚类的结果跟开始的选点有关。 Kmeans 流程 1、适当选择c个类的初始中心。 2、在第K次迭代中,对任意一个样本,求其到c各中心的距离,将该样本归到距离最短的中心所在的类。 3、利用均值等方法更新该类的中心值。 4、对于多有的c个聚类中心,如果利用2,3的迭代法更新后,值保持不变,则迭代结束,否则继续迭代。 Kmeans动图演示: 上选点: 下选点: 左选点: 右选点: Kmeans 算法缺陷 聚类中心的个数K 需要事先给定,但在实际中这个 K 值的选定是非常难以估计的,很多时候,事先并不知道给定的数据集应该分成多少个类别才最合适。 Kmeans需要人为地确定初始聚类中心,不同的初始聚类中心可能导致完全不同的聚类结果。(可以使用Kmeans++算法来解决)。 Kmeans++算法 Kmeans++在开始的选点上进行了优化,减少了人为选点的影响,同时类与类之间的差异也比较大。 Kmeans++ 流程 1、从输入的数据点集合中随机选择一个点作为第一个聚类中心; 2、对于数据集中的每一个点x,计算它与最近聚类中心(指已选择的聚类中心)的距离D(x); 3、选择一个新的数据点作为新的聚类中心,选择的原则是:D(x)较大的点,被选取作为聚类中心的概率较大; 4、重复2和3直到k个聚类中心被选出来; 5、利用这k个初始的聚类中心来运行标准的k-means算法。 Kmeans++演示动图: 聚类的原则 1、类的成员之间越相似越好。 2、类之间的成员差异越大越好。","tags":[{"name":"机器学习","slug":"机器学习","permalink":"http://www.seanxia.cn/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"},{"name":"Kmeans","slug":"Kmeans","permalink":"http://www.seanxia.cn/tags/Kmeans/"}]},{"title":"SparkMLlib贝叶斯分类","date":"2018-05-05T16:00:00.000Z","path":"大数据/800c7c16.html","text":"机器学习算法中,有种依据概率原则进行分类的朴素贝叶斯算法,正如气象学家预测天气一样,朴素贝叶斯算法就是应用先前事件的有关数据来估计未来事件发生的概率。如:70%降水概率。 贝叶斯分类算法是一个非线性有监督的分类算法。 贝叶斯分类用于做概率分类。二分类、正负例。 贝叶斯条件概率 思考: 一所学校里面有 60% 的男生,40% 的女生。男生总是穿长裤,女生则一半穿长裤一半穿裙子。假设你走在校园中,迎面走来一个穿长裤的学生(很不幸的是你高度近视,你只看得见他(她)穿的是否长裤,而无法确定他(她)的性别),你能够推断出他(她)是男生的概率是多大吗? 基于贝叶斯定理的条件概率 通过此公式可以算出上题: 理解朴素贝叶斯 如果我们知道P(垃圾邮件)和P(Viagra)是相互独立的,则容易计算P(垃圾邮件&Viagra),即这两个事件同时发生的概率。 20% * 5% = 1% 对于我们垃圾邮件来说 计算贝叶斯定理中每一个组成部分的概率,我们必须构造一个频率表 P(垃圾邮件∣Viagra)=P(Viagra∣垃圾邮件)∗P(垃圾邮件)/P(Viagra)P(垃圾邮件|Viagra) = P(Viagra|垃圾邮件) * P(垃圾邮件) / P(Viagra)P(垃圾邮件∣Viagra)=P(Viagra∣垃圾邮件)∗P(垃圾邮件)/P(Viagra) =(4/20)∗(20/100)/(5/100)=0.8= (4/20) * (20/100) / (5/100) = 0.8=(4/20)∗(20/100)/(5/100)=0.8 因此,如果电子邮件含有单词Viagra,那么该电子邮件是垃圾邮件的概率为80%。所以,任何含有单词Viagra的消息都需要被过滤掉。 当有额外更多的特征是,这一概念如何被使用 利用贝叶斯公式,我们得到概率如下: 分母可以先忽略它,垃圾邮件的总似然为: (4/20)∗(10/20)∗(20/20)∗(12/20)∗(20/100)=0.012(4/20) * (10/20) * (20/20) * (12/20) * (20/100) = 0.012(4/20)∗(10/20)∗(20/20)∗(12/20)∗(20/100)=0.012 非垃圾邮件的总似然为: (1/80)∗(66/80)∗(71/80)∗(23/80)∗(80/100)=0.002(1/80) * (66/80) * (71/80) * (23/80) * (80/100) = 0.002(1/80)∗(66/80)∗(71/80)∗(23/80)∗(80/100)=0.002 将这些值转换成概率,我们只需要一步得到垃圾邮件概率为85.7% 问题 现在有一封邮件,它包含了这四个单词,那么这封邮件是垃圾邮件的概率多大呢 可以计算垃圾邮件的似然如下: (4/20)∗(10/20)∗(0/20)∗(12/20)∗(20/100)=0(4/20) * (10/20) * (0/20) * (12/20) * (20/100) = 0(4/20)∗(10/20)∗(0/20)∗(12/20)∗(20/100)=0 非垃圾邮件的似然为: (1/80)∗(14/80)∗(8/80)∗(23/80)∗(80/100)=0.00005(1/80) * (14/80) * (8/80) * (23/80) * (80/100) = 0.00005(1/80)∗(14/80)∗(8/80)∗(23/80)∗(80/100)=0.00005 该消息是垃圾邮件的概率为 0/(0+0.00005)=00 / (0+0.00005) = 00/(0+0.00005)=0 该消息是非垃圾邮件的概率 0.00005/(0+0.00005)=10.00005/(0+0.00005)=10.00005/(0+0.00005)=1 问题出在Groceries这个单词,单词Grogeries有效抵消或否决了所有其他的证据。 拉普拉斯估计 拉普拉斯估计解决了每个特征的概率非零。 拉普拉斯估计本质上是给频率表中的每个计数加上一个较小的数,这样就保证了每一类中每个特征发生概率非零。 通常情况下,拉普拉斯估计中加上的数值设定为1,这样就保证每一个特征至少在数据中出现一次。 然后,我们得到垃圾邮件的似然为: (5/24)∗(11/24)∗(1/24)∗(13/24)∗(20/100)=0.0004(5/24) * (11/24) * (1/24) * (13/24) * (20/100) = 0.0004(5/24)∗(11/24)∗(1/24)∗(13/24)∗(20/100)=0.0004 非垃圾邮件的似然为: (2/84)∗(15/84)∗(9/84)∗(24/84)∗(80/100)=0.0001(2/84) * (15/84) * (9/84) * (24/84) * (80/100) = 0.0001(2/84)∗(15/84)∗(9/84)∗(24/84)∗(80/100)=0.0001 这表明该消息是垃圾邮件的概率为80%,是非垃圾邮件的概率为20%。 实例: 利用普氏贝叶斯对邮件进行分类(正常邮件和垃圾邮件) 1、构建词袋:采用稀疏向量 2、文本向量化 3、输入文本","tags":[{"name":"机器学习","slug":"机器学习","permalink":"http://www.seanxia.cn/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"},{"name":"贝叶斯分类","slug":"贝叶斯分类","permalink":"http://www.seanxia.cn/tags/%E8%B4%9D%E5%8F%B6%E6%96%AF%E5%88%86%E7%B1%BB/"}]},{"title":"Spark性能优化","date":"2018-02-19T16:00:00.000Z","path":"大数据/91aae295.html","text":"关于Spark,在实际工作中有很多需要去优化的地方。本篇文章将给出一些需要手动去调整的配置供大家参考。 资源调优 1、在部署 spark 集群中指定资源分配的默认参数 在 spark 安装包的 conf 下 spark-env.sh SPARK_WORKER_CORES #每个worker用到的核数SPARK_WORKER_MEMORY #每个worker用到的内存SPARK_WORKER_INSTANCES #每台机器启动worker数 2、在提交 Application 的时候给当前的 Application 分配更多的资源 提交命令选项:(在提交 Application 的时候使用选项) --executor-cores #每个executor用到的核数--executor-memory #每个executor用到的内存--total-executor-cores #所有的executor需要用到的核数 配置信息:(Application 的代码中设置或在 Spark-default.conf 中设置) spark.executor.coresspark.executor.memoryspark.max.cores //意思同上 动态分配资源 spark.shuffle.service.enabled true //启用 External shuffle Service 服务spark.shuffle.service.port 7337 //Shuffle Service 服务端口,必须和yarn-site中的一致spark.dynamicAllocation.enabled true //开启动态资源分配spark.dynamicAllocation.minExecutors 1 //每个Application最小分配的executor数spark.dynamicAllocation.maxExecutors 30 //每个Application最大并发分配的executor数spark.dynamicAllocation.schedulerBacklogTimeout 1sspark.dynamicAllocation.sustainedSchedulerBacklogTimeout 5s 并行度调优 1、如果读取的数据在 HDFS 中,降低 block 大小,相当于提高了 RDD 中 partition 个数** sc.textFile(xx,numPartitions) 2、sc.parallelize(xxx, numPartitions) 3、sc.makeRDD(xxx, numPartitions) 4、sc.parallelizePairs(xxx, numPartitions) 5、repartions/coalesce 6、redecByKey/groupByKey/join —(xxx, numPartitions) 7、spark.default.parallelism net set 8、spark.sql.shuffle.partitions—200 9、自定义分区器 10、如果读取数据是在 SparkStreaming 中 Receiver::spark.streaming.blockInterval—200ms Direct:读取的 topic 的分区数 代码调优 1、避免创建重复的 RDD val rdd1 = sc.textFile(path1)val rdd2 = sc.textFile(path1) // 这就是创建了重复的 RDD 有什么问题? 对于执行性能来说没有问题,但是呢,代码乱。 2、复用同一个 RDD val rdd1 = RDD<String,String>val rdd2 = rdd.map(_._2) 这样的话 rdd2 是 rdd1 的子集。 rdd2 执行了一个操作: filter rdd2.filter() = rdd1.map((_._2)).filter() 3、对多次使用的 RDD 进行持久化 如何选择一种最合适的持久化策略? 默认情况下,性能最高的当然是 MEMORY_ONLY,但前提是你的内存必须足够足够大,可以绰绰有余地存放下整个 RDD 的所有数据。因为不进行序列化与反序列化操作,就避免了这部分的性能开销;对这个 RDD 的后续算子操作,都是基于纯内存中的数据的操作,不需要从磁盘文件中读取数据,性能也很高;而且不需要复制一份数据副本,并远程传送到其他节点上。但是这里必须要注意的是,在实际的生产环境中,恐怕能够直接用这种策略的场景还是有限的,如果 RDD 中数据比较多时(比如几十亿),直接用这种持久化级别,会导致 JVM 的 OOM 内存溢出异常。 如果使用 MEMORY_ONLY 级别时发生了内存溢出,那么建议尝试使用 MEMORY_ONLY_SER 级别。该级别会将 RDD 数据序列化后再保存在内存中,此时每个 partition 仅仅是一个字节数组而已,大大减少了对象数量,并降低了内存占用。这种级别比 MEMORY_ONLY 多出来的性能开销,主要就是序列化与反序列化的开销。但是后续算子可以基于纯内存进行操作,因此性能总体还是比较高的。此外,可能发生的问题同上,如果RDD 中的数据量过多的话,还是可能会导致 OOM 内存溢出的异常。 如果纯内存的级别都无法使用,那么建议使用 MEMORY_AND_DISK_SER 策略,而不是 MEMORY_AND_DISK 策略。因为既然到了这一步,就说明 RDD 的数据量很大,内存无法完全放下。序列化后的数据比较少,可以节省内存和磁盘的空间开销。同时该策略会优先尽量尝试将数据缓存在内存中,内存缓存不下才会写入磁盘。 通常不建议使用 DISK_ONLY 和后缀为 _2 的级别:因为完全基于磁盘文件进行数据的读写,会导致性能急剧降低,有时还不如重新计算一次所有 RDD。后缀为_2 的级别,必须将所有数据都复制一份副本,并发送到其他节点上,数据复制以及网络传输会导致较大的性能开销,除非是要求作业的高可用性,否则不建议使用。 持久化算子: cache: MEMORY_ONLY persist: MEMORY_ONLYMEMORY_ONLY_SERMEMORY_AND_DISK_SER# 一般不要选择带有_2 的持久化级别。 checkpoint: ① 如果一个 RDD 的计算时间比较长或者计算起来比较复杂,一般将这个 RDD 的计算结果保存到 HDFS 上,这样数据会更加安全。 ② 如果一个 RDD 的依赖关系非常长,也会使用 checkpoint,会切断依赖关系,提高容错的效率。 4、尽量避免使用shuffle类的算子 使用广播变量来模拟使用 join。 使用情况:一个 RDD 比较大,一个 RDD比较小。 join 算子 = 广播变量+filter、广播变量+map、广播变量+flatMap 5、使用 map-side 预聚合的 shuffle 操作 即尽量使用有 combiner 的 shuffle 类算子。 combiner 概念: 在 map 端,每一个 map task 计算完毕后进行的局部聚合。 combiner 好处: 降低 shuffle write 写磁盘的数据量。 降低 shuffle read 拉取数据量的大小。 降低 reduce 端聚合的次数。 有 combiner 的 shuffle 类算子: reduceByKey:这个算子在 map 端是有 combiner 的,在一些场景中可以使用 reduceByKey 代替 groupByKey aggregateByKey combineByKey 6、尽量使用高性能的算子 使用 reduceByKey 替代 groupByKey 使用 mapPartition 替代 map 使用 foreachPartition 替代 foreach filter 后使用 coalesce 减少分区数 使用 repartitionAndSortWithinPartitions 替代 repartition 与 sort 类操作 使用 repartition 和 coalesce 算子操作分区。 7、使用广播变量 开发过程中,会遇到需要在算子函数中使用外部变量的场景(尤其是大变量,比如 100M 以上的大集合),那么此时就应该使用 Spark 的广播 (Broadcast)功能来提升性能,函数中使用到外部变量时,默认情况下,Spark 会将该变量复制多个副本,通过网络传输到 task 中,此时每个 task都有一个变量副本。如果变量本身比较大的话(比如 100M,甚至 1G),那么大量的变量副本在网络中传输的性能开销,以及在各个节点的 Executor 中占用过多内存导致的频繁 GC,都会极大地影响性能。如果使用的外部变量比较大,建议使用 Spark 的广播功能,对该变量进行广播。广播后的变量,会保证每个 Executor 的内存中,只驻留一份变量副本,而 Executor 中的 task 执行时共享该 Executor 中的那份变量副本。这样的话,可以大大减少变量副本的数量,从而减少网络传输的性能开销,并减少对 Executor 内存的占用开销,降低 GC 的频率。 广播大变量发送方式: Executor 一开始并没有广播变量,而是 task 运行需 要 用 到 广 播 变 量 , 会 找 executor 的 blockManager 要 ,bloackManager 找 Driver 里面的 blockManagerMaster 要。 使用广播变量可以大大降低集群中变量的副本数。不使用广播变量,变量的副本数和 task 数一致。使用广播变量变量的副本和 Executor 数一致。 8、使用 Kryo 优化序列化性能 在 Spark 中,主要有三个地方涉及到了序列化: 在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输。 将自定义的类型作为 RDD 的泛型类型时(比如 JavaRDD,SXT 是自定义类型),所有自定义类型对象,都会进行序列化。因此这种情况下,也要求自定义的类必须实现 Serializable 接口。 使用可序列化的持久化策略时(比如 MEMORY_ONLY_SER),Spark 会将 RDD 中的每个 partition 都序列化成一个大的字节数组。 Kryo 序列化器介绍: Spark 支持使用 Kryo 序列化机制。Kryo 序列化机制,比默认的 Java 序列化机制,速度要快,序列化后的数据要更小,大概是 Java 序列化机制的 1/10。所以 Kryo 序列化优化以后,可以让网络传输的数据变少;在集群中耗费的内存资源大大减少。 对于这三种出现序列化的地方,我们都可以通过使用 Kryo 序列化类库,来优化序列化和反序列化的性能。Spark 默认使用的是 Java 的序列化机制,也就是 ObjectOutputStream/ObjectInputStream API 来进行序列化和反序列化。但是 Spark 同时支持使用 Kryo 序列化库,Kryo 序列化类库的性能比 Java 序列化类库的性能要高很多。官方介绍,Kryo 序列化机制比 Java 序列化机制,性能高 10 倍左右。Spark 之所以默认没有使用 Kryo 作为序列化类库,是因为 Kryo 要求最好要注册所有需要进行序列化的自定义类型,因此对于开发者来说,这种方式比较麻烦。 Spark 中使用 Kryo: Sparkconf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer").registerKryoClasses(new Class[]{SpeedSortKey.class}) 9、优化数据结构 Java 中有三种类型比较消耗内存: 对象,每个 Java 对象都有对象头、引用等额外的信息,因此比较占用内存空间。 字符串,每个字符串内部都有一个字符数组以及长度等额外信息。 集合类型,比如 HashMap、LinkedList 等,因为集合类型内部通常会使用一些内部类来封装集合元素,比如 Map.Entry。 因此 Spark 官方建议,在 Spark 编码实现中,特别是对于算子函数中的代码,尽量不要使用上述三种数据结构,尽量使用字符串替代对象,使用原始类型(比如 Int、Long)替代字符串,使用数组替代集合类型,这样 尽可能地减少内存占用,从而降低 GC 频率,提升性能。 10、使用高性能的库 fastutil fasteutil 介绍: fastutil 是扩展了 Java 标准集合框架(Map、List、Set;HashMap、ArrayList、HashSet)的类库,提供了特殊类型的 map、set、list 和 queue;fastutil 能够提供更小的内存占用,更快的存取速度;我们使用 fastutil 提供的集合类,来替代自己平时使用的 JDK 的原生的 Map、List、Set,好处在于,fastutil 集合类,可以减小内存的占用,并且在进行集合的遍历、根据索引(或者 key)获取元素的值和设置元素的值的时候,提供更快的存取速度。fastutil 的每一种集合类型,都实现了对应的 Java 中的标准接口(比如 fastutil 的 map,实现了 Java 的 Map 接口),因此可以直接放入已有系统的任何代码中。 astutil 最新版本要求 Java 7 以及以上版本。 fasteutil 使用:请见 fasteutil 使用示例 数据本地化 1、数据本地化的级别: PROCESS_LOCAL task 要计算的数据在本进程(Executor)的内存中。 NODE_LOCAL ① task 所计算的数据在本节点所在的磁盘上。 ② task 所计算的数据在本节点其他 Executor 进程的内存中。 NO_PREF task 所计算的数据在关系型数据库中,如 mysql。 RACK_LOCAL task所计算的数据在同机架的不同节点的磁盘或者Executor进程的内存中。 ANY 跨机架。 2、Spark 数据本地化调优: Spark 中任务调度时,TaskScheduler 在分发之前需要依据数据的位置来分发,最好将 task 分发到数据所在的节点上,如果 TaskScheduler 分发的 task 在默认 3s 依然无法执行的话,TaskScheduler 会重新发送这个 task 到相同的 Executor 中去执行,会重试 5 次,如果依然无法执行,那么 TaskScheduler 会降低一级数据本地化的级别再次发送 task。 如上图中,会先尝试 1,PROCESS_LOCAL 数据本地化级别,如果重试 5 次每次等待 3s,会默认这个 Executor 计算资源满了,那么会降低一级数据本地化级别到 2,NODE_LOCAL,如果还是重试 5 次每次等待 3s 还是失败,那么还是会降低一级数据本地化级别到 3,RACK_LOCAL。这样数据就会有网络传输,降低了执行效率。 如何提高数据本地化的级别? 可以增加每次发送 task 的等待时间(默认都是 3s),将 3s 倍数调大,结合 WEBUI 来调节: spark.locality.wait spark.locality.wait.process spark.locality.wait.node spark.locality.wait.rack 注意:等待时间不能调大很大,调整数据本地化的级别不要本末倒置,虽然每一个 task 的本地化级别是最高了,但整个 Application 的执行时间反而加长。 如何查看数据本地化的级别? 通过日志或者 WEBUI。 内存调优 JVM堆内存分为一块较大的Eden和两块较小的Survivor,每次只使用Eden和其中一块 Survivor,当回收时将 Eden 和 Survivor 中还存活着的对象一次性复制到另外一块Survivor上,最后清理掉Eden和刚才用过的Survivor。 也就是说当 task 创建出来对象会首先往 Eden 和 survivor1 中存放,survivor2是空闲的,当Eden和survivor1区域放满以后就会触发minor gc小型垃圾回收,清理掉不再使用的对象。会将存活下来的对象放入 survivor2 中。 如果存活下来的对象大小大于 survivor2 的大小,那么 JVM 就会将多余的对象直接放入到老年代中。 如果这个时候年轻代的内存不是很大的话,就会经常的进行 minor gc,频繁的 minor gc 会导致短时间内有些存活的对象(多次垃圾回收都没有回收掉,一直在用的又不能被释放,这种对象每经过一次 minor gc 都存活下来)频繁的倒来倒去,会导致这些短生命周期的对象(不一定长期使用)每进行一次垃圾回收就会长一岁。年龄过大,默认 15 岁,垃圾回收还是没有回收回去就会跑到老年代里面去了。 这样会导致在老年代中存放大量的短生命周期的对象,老年代应该存放的是数量比较少并且会长期使用的对象,比如数据库连接池对象。这样的话,老年代就会满溢(full gc 因为本来老年代中的对象很少,很少进行 full gc 因此采取了不太复杂但是消耗性能和时间的垃圾回收算法)。不管 minor gc 还是 full gc 都会导致 JVM 的工作线程停止。 总结-堆内存不足造成的影响: 频繁的 minor gc。 老年代中大量的短声明周期的对象会导致 full gc。 gc 多了就会影响 Spark 的性能和运行的速度。 Spark JVM 调优主要是降低 gc时间,可以修改 Executor 内存的比例参数。RDD 缓存、task 定义运行的算子函数,可能会创建很多对象,这样会占用大量的堆内存。堆内存满了之后会频繁的 GC,如果 GC 还不能够满足内存 的需要的话就会报 OOM。比如一个 task 在运行的时候会创建 N 个对象,这些对象首先要放入到 JVM 年轻代中。比如在存数据的时候我们使用了 foreach 来将数据写入到内存,每条数据都会封装到一个对象中存入数据库中,那么有多少条数据就会在 JVM 中创建多少个对象。 Spark 中如何内存调优? Spark Executor 堆内存中存放(以静态内存管理为例):RDD 的缓存数据和广播变量(spark.storage.memoryFraction 0.6),shuffle 聚合内存 (spark.shuffle.memoryFraction 0.2),task 的运行(0.2)。那么如何调优呢? 提高 Executor 总体内存的大小。 降低储存内存比例或者降低聚合内存比例。 如何查看 gc? Spark WEBUI 中 job —> stage —> task Spark Shuffle 调优 buffer 大小——32KB shuffle read 拉取数据量的大小——48M shuffle 聚合内存的比例——20% 拉取数据重试次数——5 次 重试间隔时间 60s Spark Shuffle 的种类 HashShuffle 合并机制 SortShuffle bypass 机制 200 次 关于Spark Shuffle具体的调优配置请见:SparkShuffle调优 调节 Executor 的堆外内存 Spark 底层 shuffle 的传输方式是使用 netty 传输,netty 在进行网络传输的过程会申请堆外内存(netty 是零拷贝),所以使用了堆外内存。默认情况下,这个堆外内存上限默认是每一个 executor 的内存大小的 10%;真正处理大数据的时候,这里都会出现问题,导致 spark 作业反复崩溃,无法运行;此时就会去调节这个参数,到至少 1G(1024M),甚至说 2G、4G。 executor 在进行 shuffle write,优先从自己本地关联的 mapOutPutWorker 中获取某份数据,如果本地 mapOutPutWorker 没有的话,那么会通过 TransferService 去远程连接其他节点上 executor 的 block manager 去获取。频繁创建对象让 JVM 堆内存满溢,进行垃圾回收。正好碰到那个 exeuctor 的 JVM 在垃圾回收。处于垃圾回过程中,所有的工作线程全部停止;相当于只要一旦进行垃圾回收,spark / executor 停止工作,无法提供响应,spark 默认的网络连接的超时时长是 60s;如果卡住 60s 都无法建立连接的话,那么这个 task 就失败了。task 失败了就会出现 "shuffle file cannot find" 的错误。 那么如何调节等待的时长呢? 在./spark-submit 提交任务的脚本里面添加: --conf spark.core.connection.ack.wait.timeout=300 Executor 由于内存不足或者堆外内存不足了,挂掉了,对应的 Executor 上面的 block manager 也挂掉了,找不到对应的 shuffle map output 文件,Reducer 端不能够拉取数据。 我们可以调节堆外内存的大小,如何调节? 在./spark-submit 提交任务的脚本里面添加: yarn 模式下: --conf spark.yarn.executor.memoryOverhead=2048 #单位 M standalone 模式下: --conf spark.executor.memoryOverhead=2048 #单位 M 解决数据倾斜 1、使用 Hive ETL 预处理数据 方案适用场景: 如果导致数据倾斜的是 Hive 表。如果该 Hive 表中的数据本身很不均匀(比如某个 key 对应了 100 万数据,其他 key 才对应了 10 条数据),而且业务场景需要频繁使用 Spark 对 Hive 表执行某个分析操作,那么比较 适合使用这种技术方案。 方案实现思路: 此时可以评估一下,是否可以通过 Hive 来进行数据预处理(即通过 Hive ETL 预先对数据按照 key 进行聚合,或者是预先和其他表进行 join),然后在 Spark 作业中针对的数据源就不是原来的 Hive 表了,而是预处理后的 Hive 表。此时由于数据已经预先进行过聚合或 join 操作了,那么在 Spark 作业中也就不需要使用原先的 shuffle 类算子执行这类操作了。 方案实现原理: 这种方案从根源上解决了数据倾斜,因为彻底避免了在 Spark 中执行 shuffle 类算子,那么肯定就不会有数据倾斜的问题了。但是这里也要提醒一下大家,这种方式属于治标不治本。因为毕竟数据本身就存在分布不均匀的问题,所以 Hive ETL 中进行 group by 或者 join 等 shuffle 操作时,还是会出现数据倾斜,导致 Hive ETL 的速度很慢。我们只是把数据倾斜的发生提前到了 Hive ETL 中,避免 Spark 程序发生数据倾斜而已。 2、过滤少数导致倾斜的 Key 方案适用场景: 如果发现导致倾斜的 key 就少数几个,而且对计算本身的影响并不大的话,那么很适合使用这种方案。比如 99%的 key 就对应 10 条数据,但是只有一个 key 对应了 100 万数据,从而导致了数据倾斜。 方案实现思路: 如果我们判断那少数几个数据量特别多的 key,对作业的执行和计算结果不是特别重要的话,那么干脆就直接过滤掉那少数几个 key。比如,在 Spark SQL 中可以使用 where 子句过滤掉这些 key 或者在 Spark Core 中对 RDD 执行 filter 算子过滤掉这些 key。如果需要每次作业执行时,动态判定哪些 key 的数据量最多然后再进行过滤,那么可以使用 sample 算子对 RDD 进行采样,然后计算出每个 key 的数量,取数据量最多的 key 过滤掉即可。 方案实现原理: 将导致数据倾斜的 key 给过滤掉之后,这些 key 就不会参与计算了,自然不可能产生数据倾斜。 3、提高 shuffle 操作的并行度 方案实现思路: 在对 RDD 执行 shuffle 算子时,给 shuffle 算子传入一个参数,比如 reduceByKey(1000),该参数就设置了这个 shuffle 算子执行时 shuffle read task 的数量。对于Spark SQL中的shuffle类语句,比如group by、join 等,需要设置一个参数,即 spark.sql.shuffle.partitions,该参数代表了 shuffle read task 的并行度,该值默认是 200,对于很多场景来说都有点过小。 方案实现原理: 增加 shuffle read task 的数量,可以让原本分配给一个 task 的多个 key 分配给多个 task,从而让每个 task 处理比原来更少的数据。举例来说,如果原本有5个不同的 key,每个 key 对应10条数据,这5个 key 都是分配给一个task的,那么这个task就要处理50条数据。而增加了 shuffle read task 以后,每个 task 就分配到个 key,即每个 task 就处理10条数据,那么自然每个 task 的执行时间都会变短了。 4、双重聚合 方案适用场景: 对 RDD 执行 reduceByKey 等聚合类 shuffle 算子或者在 Spark SQL 中使用 group by 语句进行分组聚合时,比较适用这种方案。 方案实现思路: 这个方案的核心实现思路就是进行两阶段聚合。第一次是局部聚合,先给每个 key 都打上一个随机数,比如 10 以内的随机数,此时原先一样的 key 就变成不一样的了,比如(hello, 1) (hello, 1) (hello, 1) (hello, 1),就会变成(1_hello, 1) (1_hello, 1) (2_hello, 1) (2_hello, 1)。接着对打上随机数后的数据,执行 reduceByKey 等聚合操作,进行局部聚合,那么局部聚合结果,就会变成了(1_hello, 2) (2_hello, 2)。然后将各个 key 的前缀 给去掉,就会变成(hello,2)(hello,2),再次进行全局聚合操作,就可以得到最终结果了,比如(hello, 4)。 方案实现原理: 将原本相同的 key 通过附加随机前缀的方式,变成多个不同的 key,就可以让原本被一个 task 处理的数据分散到多个 task 上去做局部聚合,进而解决单个 task 处理数据量过多的问题。接着去除掉随机前缀,再次进行 全局聚合,就可以得到最终的结果。 如果一个 RDD 中有一个 key 导致数据倾斜,同时还有其他的 key,那么一般先对数据集进行抽样,然后找出倾斜的 key,再使用 filter 对原始的 RDD 进行分离为两个 RDD,一个是由倾斜的 key 组成的 RDD1,一个是由其他的 key 组成的 RDD2,那么对于 RDD1 可以使用加随机前缀进行多分区多 task 计算,对于另一个 RDD2 正常聚合计算,最后将结果再合并起来。 5、将 reduce join 转为 map join BroadCast + filter(或者 map) 方案适用场景: 在对 RDD 使用 join 类操作,或者是在 Spark SQL 中使用 join 语句时,而且 join 操作中的一个 RDD 或表的数据量比较小(比如几百 M 或者一两 G),比较适用此方案。 方案实现思路: 不使用 join 算子进行连接操作,而使用 Broadcast 变量与 map 类算子实现 join 操作,进而完全规避掉 shuffle 类的操作,彻底避免数据倾斜的发生和出现。将较小 RDD 中的数据直接通过 collect 算子拉取到 Driver 端的内存中来,然后对其创建一个 Broadcast 变量;接着对另外一个 RDD 执行 map 类算子,在算子函数内,从 Broadcast 变量中获取较小 RDD 的全量数据,与当前 RDD 的每一条数据按照连接 key 进行比对,如果连接 key 相同的话,那么就将两个 RDD 的数据用你需要的方式连接起来。 方案实现原理: 普通的 join 是会走 shuffle 过程的,而一旦 shuffle,就相当于会将相同 key 的数据拉取到一个shuffle read task 中再进行join,此时就是 reduce join。但是如果一个 RDD 是比较小的,则可以采用广播小 RDD 全量数 据+map 算子来实现与 join 同样的效果,也就是 map join,此时就不会发生 shuffle 操作,也就不会发生数据倾斜。 6、采样倾斜 key 并分拆 join 操作 方案适用场景: 两个 RDD/Hive 表进行 join 的时候,如果数据量都比较大,无法采用“解决方案五”,那么此时可以看一下两个 RDD/Hive 表中的 key 分布情况。如果出现数据倾斜,是因为其中某一个 RDD/Hive 表中的少数几个 key 的数据量过大,而另一个 RDD/Hive 表中的所有 key 都分布比较均匀,那么采用这个解决方案是比较合适的。 方案实现思路: 对包含少数几个数据量过大的 key 的那个 RDD,通过 sample 算子采样出一份样本来,然后统计一下每个 key 的数量,计算出来数据量最大的是哪几个key。然后将这几个key对应的数据从原来的RDD中拆分出来, 形成一个单独的 RDD,并给每个 key 都打上 n 以内的随机数作为前缀,而不会导致倾斜的大部分 key 形成另外一个 RDD。接着将需要 join 的另一个 RDD,也过滤出来那几个倾斜 key 对应的数据并形成一个单独的RDD,将每条数据膨胀成 n 条数据,这 n 条数据都按顺序附加一个 0~n 的前缀,不会导致倾斜的大部分 key 也形成另外一个 RDD。再将附加了随机前缀的独立 RDD 与另一个膨胀 n 倍的独立 RDD 进行 join,此时就可以将原先相同的 key 打散成 n 份,分散到多个 task 中去进行 join 了。而另外两个普通的 RDD 就照常 join 即可。最后将两次 join 的结果使用 union 算子合并起来即可,就是最终的 join 结果 。 7、使用随机前缀和扩容 RDD 进行 join 方案适用场景: 如果在进行 join 操作时,RDD 中有大量的 key 导致数据倾斜,那么进行分拆 key 也没什么意义,此时就只能使用最后一种方案来解决问题了。 方案实现思路: 该方案的实现思路基本和“解决方案六”类似,首先查看 RDD/Hive 表中的数据分布情况,找到那个造成数据倾斜的 RDD/Hive 表,比如有多个 key 都对应了超过 1 万条数据。然后将该 RDD 的每条数据都打上一个 n 以内的随机前缀。同时对另外一个正常的 RDD 进行扩容,将每条数据都扩容成 n 条数据,扩容出来的每条数据都依次打上一个 0~n 的前缀。最后将两个处理后的 RDD 进行 join 即可。 Spark 故障解决(troubleshooting) 1、shuffle file cannot find:磁盘小文件找不到。 connection timeout ----shuffle file cannot find 提高建立连接的超时时间,或者降低 gc,降低 gc 了那么 spark 不能对外提供服务的时间就少了,那么超时的可能就会降低。 fetch data fail ---- shuffle file cannot find 提高拉取数据的重试次数以及间隔时间。 OOM/executor lost ---- shuffle file cannot find 提高堆外内存大小,提高堆内内存大小。 2、reduce OOM BlockManager 拉取的数据量大,reduce task 处理的数据量小。 解决方法: 降低每次拉取的数据量。 提高 shuffle 聚合的内存比例。 提高 Executor 的内存比例。 3、序列化问题 Java中不能被序列化的几种情况: 反序列化时serializable 版本号不一致时会导致不能反序列化。 子类中实现了serializable接口,父类中没有实现,父类中的变量不能被序列化,序列化后父类中的变量会得到null。 注意:父类实现serializable接口,子类没有实现serializable接口时,子类可以正常序列化 被关键字transient修饰的变量不能被序列化。 静态变量不能被序列化,属于类,不属于方法和对象,所以不能被序列化。 4、Null 值问题 举个例子: val rdd = rdd.map{x=>{ x+”~”;W}}rdd.foreach{x=>{ System.out.println(x.getName())}}","tags":[{"name":"优化","slug":"优化","permalink":"http://www.seanxia.cn/tags/%E4%BC%98%E5%8C%96/"},{"name":"Spark","slug":"Spark","permalink":"http://www.seanxia.cn/tags/Spark/"}]},{"title":"Spark计算框架(六)","date":"2018-02-02T16:00:00.000Z","path":"大数据/2e9b7b1c.html","text":"SparkStreaming 是流式处理框架,是 Spark API 的扩展,支持可扩展、高吞吐、容错的实时数据流处理。 实时数据的来源可以是:Kafka,Flume,Twitter,ZeroMQ 或者 TCP sockets,并且可以使用高级功能的复杂算子来处理流数据。例如:map,reduce,join,window 。最终,处理后的数据可以存放在文件系统,数据库等,方便实时展现。 SparkStreaming 与 Storm 的区别 1、Storm 是纯实时的流式处理框架,SparkStreaming 是准实时的处理框架(微批处理)。因为微批处理,SparkStreaming 的吞吐量比 Storm 要高。 2、Storm 的事务机制比 SparkStreaming 的要完善。 3、Storm 支持动态资源调度。(spark1.2 开始和之后也支持) 4、SparkStreaming 擅长复杂的业务处理,Storm 不擅长复杂的业务处理,擅长简单的汇总型计算。 SparkStreaming 初始 1、SparkStreaming 初始理解 注意: receiver task 是 7*24 小时一直在执行,一直接受数据,将一段时间内接收来的数据保存到 batch 中。假设 batchInterval 为 5s,那么会将接收来的数据每隔 5 秒封装到一个 batch 中,batch 没有分布式计算特性,这一个 batch 的数据又被封装到一个 RDD 中,RDD 最终封装到一个 DStream 中。 例如:假设 batchInterval 为 5 秒,每隔 5 秒通过 SparkStreamin 将得到一个 DStream,在第 6 秒的时候计算这 5 秒的数据,假设执行任务的时间是 3 秒,那么第 6~9 秒一边在接收数据,一边在计算任务,9~10 秒只是在接收数据。然后在第 11 秒的时候重复上面的操作。 如果 job 执行的时间大于 batchInterval 会有什么样的问题? 如果接受过来的数据设置的级别是仅内存,接收来的数据会越堆积越多,最后可能会导致 OOM(如果设置 StorageLevel 包含 disk, 则内存存放不下的数据会溢写至 disk, 加大延迟 )。 2、SparkStreaming 代码 注意事项: 启动 socket server 服务器:nc –lk 9999 eceiver 模式下接受数据,local 的模拟线程必须大于等于 2,一个线程用来 receiver 用来接受数据,另一个线程用来执行 job。 Durations 时间设置就是我们能接收的延迟度。这个需要根据集群的资源情况以及任务的执行情况来调节。 创建 JavaStreamingContext 有两种方式(SparkConf,SparkContext) 所有的代码逻辑完成后要有一个 output operation 类算子。 JavaStreamingContext.start() Streaming 框架启动后不能再次添加业务逻辑。 JavaStreamingContext.stop() 无参的 stop 方法将 SparkContext一同关闭,stop(false),不会关闭 SparkContext。 JavaStreamingContext.stop()停止之后不能再调用 start。 public class SparkStreamingTest { public static void main(String[] args){ SparkConf conf = new SparkConf(); conf.setMaster("local[2]").setAppName("sparkStreaming");// JavaSparkContext sparkContext = new JavaSparkContext(conf); JavaStreamingContext streamingContext = new JavaStreamingContext(conf, Durations.seconds(5)); JavaReceiverInputDStream<String> dStream = streamingContext.socketTextStream("sean01", 8888); JavaDStream<String> wordDStream = dStream.flatMap( new FlatMapFunction<String, String>() { private static final long serialVersionUID = 5302655187358849615L; @Override public Iterable<String> call(String s) throws Exception { String[] split = s.split(" "); return Arrays.asList(split); } }); JavaPairDStream<String, Integer> pairDStream = wordDStream.mapToPair( new PairFunction<String, String, Integer>() { private static final long serialVersionUID = 1285374334768880064L; @Override public Tuple2<String, Integer> call(String word) throws Exception { return new Tuple2<>(word, 1); } }); JavaPairDStream<String, Integer> resultDStream = pairDStream.reduceByKey( new Function2<Integer, Integer, Integer>() { private static final long serialVersionUID = 5889114600370649292L; @Override public Integer call(Integer v1, Integer v2) throws Exception { return v1 + v2; } }); // action类算子 resultDStream.print(); streamingContext.start(); streamingContext.awaitTermination(); streamingContext.stop(); }} SparkStreaming 算子操作 关于几种算子的详细API,请见Github:SparkStreamingAPI 1、foreachRDD、print output operator 类算子,必须对抽取出来的 RDD 执行 action 类算子,代码才能执行。 2、transform transformation 类算子 可以通过 transform 算子,对Dstream做RDD到RDD的任意操作。 3、updateStateByKey transformation 算子 updateStateByKey 作用: 1)为 SparkStreaming 中每一个 Key 维护一份 state 状态,state 类型可以是任意类型的,可以是一个自定义的对象,更新函数也可以是自定义的。 2)通过更新函数对该 key 的状态不断更新,对于每个新的 batch 而言,SparkStreaming 会在使用 updateStateByKey 的时候为已经存在的 key 进行 state 的状态更新。 使用到 updateStateByKey 要开启 checkpoint 机制和功能。 多久会将内存中的数据写入到磁盘一份? 1)如果 batchInterval 设置的时间小于10秒,那么10秒写入磁盘一份。 2)如果 batchInterval 设置的时间大于 10 秒,那么就会 batchInterval 时间间隔写入磁盘一份。 4、窗口操作 窗口操作理解图: 假设每隔 5s 1 个 batch,上图中窗口长度为 15s,窗口滑动间隔 10s。 窗口长度和滑动间隔必须是 batchInterval 的整数倍。如果不是整数倍会检测报错。 优化后的 window 窗口操作示意图: 优化后的 window 操作要保存状态所以要设置 checkpoint 路径,没有优化的 window 操作可以不设置 checkpoint 路径。 Driver HA(Standalone或Mesos) 因为 SparkStreaming 是 7*24 小时运行,Driver 只是一个简单的进程,有可能挂掉,所以实现 Driver 的 HA 就有必要(如果使用的 Client 模式就无法实现 Driver HA ,这里针对的是 cluster 模式)。 Yarn 平台 cluster 模式提交任务,AM(AplicationMaster)相当于 Driver,如果挂掉会自动启动 AM。这里所说的 DriverHA 针对的是 Spark standalone 和 Mesos 资源调度的情况下。 实现 Driver 的高可用有两个步骤: 第一:提交任务层面,在提交任务的时候加上选项 --supervise,当 Driver挂掉的时候会自动重启 Driver。 第二:代码层面,使用 JavaStreamingContext.getOrCreate(checkpoint 路径,JavaStreamingContextFactory) Driver 中元数据包括: 1、创建应用程序的配置信息。 2、DStream 的操作逻辑。 3、job 中没有完成的批次数据,也就是 job 的执行进度。 SparkStreaming+Kafka Receiver 模式 receiver模式原理图 receiver模式理解 在 SparkSteaming 程序运行起来后,Executor 中会有 receiver tasks 接收 kafka 推送过来的数据。数据会被持久化,默认级别为 MEMORY_AND_DISK_SER_2,这个级别也可以修改。receiver task 对接收过来的数据进行存储和备份,这个过程会有节点之间的传输。备份完成后去 Zookeeper 中更新消费偏移量,然后向 Driver 中的 receiver tracker 汇报数据的位置。最后 Driver 根据数据本地化将 task 分发到不同节点上执行。 receiver模式中存在的问题: 当 Driver 进程挂掉后,Driver 下的 Executor 都会被杀掉,当更新完 zookeeper 消费偏移量的时候,Driver 如果挂掉了,就会存在找不到数据的问题,相当于丢失数据。 如何解决这个问题? 开启WAL(write ahead log)预写日志机制,在接受过来数据备份到其他节点的时候,同时备份到 HDFS 上一份(我们需要将接收来的数据的持久化级别降级到 MEMORY_AND_DISK),这样就能保证数据的安全性。 不过,因为写 HDFS 比较消耗性能,要在备份完数据之后才能进行更新 zookeeper 以及汇报位置等,这样会增加 job 的执行时间,这样对于任务的执行提高了延迟度。 Receiver可能会造成重复消费 合理假设一个场景,假如当前zookeeper中记录的偏移量是50,本次接收的数据为51~100,当数据备份之后,同时也放到 HDFS 了,此时准备去zookeeper中更新偏移量时,服务器挂掉了,这时zookeeper中的偏移量没有更新还是50。重启之后会去HDFS中检查数据,发现51~100的数据未计算(一般计算的话需要更新完偏移量才计算),这时开始计算这部分数据。紧接着Kafka就会那这zookeeper中的50继续往下读,这样一来就造成了重复消费。这就是Receiver模式只能保证至少消费一次(at-least),但不能保证有且只会消费一次(exactly-once)。 receiver 的并行度设置 receiver 的并行度是由 spark.streaming.blockInterval 来决定的,默认为200ms。 假设 batchInterval 为 5s,那么每隔 blockInterval 就会产生一个 block,这里就对应每批次产生 RDD 的 partition,这样 5 秒产生的这个 Dstream 中的这个 RDD 的 partition 为 25 个,并行度就是25。 如果想提高并行度可以减少 blockInterval 的数值,但是最好不要低于 50ms。 receiver 模式代码 先准备一个MyProducer类用于产生数据: public class MyProducer extends Thread { // sparkstreaming storm flink 两三年后变成主流 流式处理,可能更复杂,数据处理性能要非常好 private String topic; //发送给Kafka的数据,topic private Producer<Integer, String> producerForKafka; public MyProducer(String topic) { this.topic = topic; Properties conf = new Properties(); conf.put("metadata.broker.list", "sean01:9092,sean02:9092,sean03:9092"); conf.put("serializer.class", StringEncoder.class.getName()); conf.put("acks",1); producerForKafka = new Producer<Integer, String>(new ProducerConfig(conf)); } @Override public void run() { int counter = 0; while (true) { counter++; String value = "seanxia"; KeyedMessage<Integer, String> message = new KeyedMessage<>(topic, value); producerForKafka.send(message); System.out.println(value + " : " + counter + " --------------"); //hash partitioner 当有key时,则默认通过key 取hash后 ,对partition_number 取余数// producerForKafka.send(new KeyedMessage<Integer, String>(topic,22,userLog));// 每2条数据暂停1秒 if (0 == counter % 2) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { new MyProducer("sk1").start(); new MyProducer("sk2").start(); }} 执行以下代码: public class SparkStreamingOnKafkaReceiver { public static void main(String[] args) { SparkConf conf = new SparkConf().setMaster("local[2]"). setAppName("SparkStreamingOnKafkaReceiver"); //开启预写日志 WAL机制 conf.set("spark.streaming.receiver.writeAheadLog.enable", "true"); JavaStreamingContext jsc = new JavaStreamingContext(conf, Durations.seconds(10)); jsc.checkpoint("./checkpoint"); Map<String, Integer> topicConsumerConcurrency = new HashMap<String, Integer>(); /** * 设置读取的topic和接受数据的线程数 */ topicConsumerConcurrency.put("sk1", 1); topicConsumerConcurrency.put("sk2", 1); /** * 第一个参数是StreamingContext * 第二个参数是ZooKeeper集群信息(接受Kafka数据的时候会从Zookeeper中获得Offset等元数据信息) * 第三个参数是Consumer Group 消费者组 * 第四个参数是消费的Topic以及并发读取Topic中Partition的线程数 * 注意: * KafkaUtils.createStream 使用五个参数的方法,设置receiver的存储级别 */// JavaPairReceiverInputDStream<String,String> lines = KafkaUtils.createStream(// jsc,// "sean01:2181,sean02:2181,sean03:2181",// "MyFirstConsumerGroup", // topicConsumerConcurrency,// StorageLevel.MEMORY_AND_DISK()); JavaPairReceiverInputDStream<String, String> lines = KafkaUtils.createStream( jsc, "sean01:2181,sean02:2181,sean03:2181", "MyFirstConsumerGroup", topicConsumerConcurrency); JavaDStream<String> words = lines.flatMap( new FlatMapFunction<Tuple2<String, String>, String>() { private static final long serialVersionUID = 1L; public Iterable<String> call(Tuple2<String, String> tuple) throws Exception { return Arrays.asList(tuple._2.split("\\t")); } }); JavaPairDStream<String, Integer> pairs = words.mapToPair( new PairFunction<String, String, Integer>() { private static final long serialVersionUID = 1L; public Tuple2<String, Integer> call(String word) throws Exception { return new Tuple2<String, Integer>(word, 1); } }); JavaPairDStream<String, Integer> wordsCount = pairs.reduceByKey( new Function2<Integer, Integer, Integer>() { private static final long serialVersionUID = 1L; //对相同的Key,进行Value的累计(包括Local和Reducer级别同时Reduce) public Integer call(Integer v1, Integer v2) throws Exception { return v1 + v2; } }); wordsCount.print(); jsc.start(); jsc.awaitTermination(); jsc.close(); }} 注意: Receiver模式:只能保证至少被消费一次(at-least),但是不能保证有且只会消费一次(exactly-once)。 direct模式:可以保证exactly-once,能保证任务失败重读数据,但是不能保证任务中的输出数据有且只有一次。 Direct 模式 direct 模式理解 SparkStreaming+kafka 的 Driect 模式就是将 kafka 看成存数据的一方,不是被动接收数据,而是主动去取数据。 消费者偏移量也不是用 zookeeper 来管理,而是 SparkStreaming 内部对消费者偏移量自动来维护。默认消费偏移量是在内存中,当然如果设置了checkpoint 目录,那么消费偏移量也会保存在 checkpoint 中。当然也可以实现用 zookeeper 来管理。 direct 模式并行度设置 Direct 模式的并行度是由读取的 kafka 中 topic 的 partition 数决定的。 direct 模式代码 public class SparkStreamingOnKafkaDirected { public static void main(String[] args) { SparkConf conf = new SparkConf().setMaster("local[2]") .setAppName("SparkStreamingOnKafkaDirected");// conf.set("spark.streaming.backpressure.enabled", "false");// conf.set("spark.streaming.kafka.maxRatePerPartition", "100"); conf.set("spark.streaming.stopGracefullyOnShutdown","true"); JavaStreamingContext jsc = new JavaStreamingContext(conf,Durations.seconds(5)); /** * 可以不设置checkpoint 不设置不保存offset,offset默认在内存中有一份, * 如果设置checkpoint在checkpoint也有一份offset, 一般要设置。 */ jsc.checkpoint("./checkpoint"); Map<String, String> kafkaParameters = new HashMap<String, String>(); kafkaParameters.put("metadata.broker.list", "sean01:9092,sean02:9092,sean03:9092");// kafkaParameters.put("auto.offset.reset", "smallest"); HashSet<String> topics = new HashSet<String>(); topics.add("sk1"); topics.add("sk2"); JavaPairInputDStream<String,String> lines = KafkaUtils.createDirectStream( jsc, String.class, String.class, StringDecoder.class, StringDecoder.class, kafkaParameters, topics); JavaDStream<String> words = lines.flatMap( new FlatMapFunction<Tuple2<String,String>, String>() { private static final long serialVersionUID = 1L; public Iterable<String> call(Tuple2<String,String> tuple) throws Exception { return Arrays.asList(tuple._2.split("\\t")); } }); JavaPairDStream<String, Integer> pairs = words.mapToPair( new PairFunction<String, String, Integer>() { private static final long serialVersionUID = 1L; public Tuple2<String, Integer> call(String word) throws Exception { return new Tuple2<String, Integer>(word, 1); } }); JavaPairDStream<String, Integer> wordsCount = pairs.reduceByKey( new Function2<Integer, Integer, Integer>() { //对相同的Key,进行Value的累计(包括Local和Reducer级别同时Reduce) private static final long serialVersionUID = 1L; public Integer call(Integer v1, Integer v2) throws Exception { return v1 + v2; } },3); wordsCount.print(); jsc.start(); jsc.awaitTermination(); jsc.close(); }} Web 监控页面 由于在本地运行,输入:localhost:4040,即可查看 SparkStreaming 在kafka集群中的运行状态 相关配置 预写日志: 用于优化 receiver 模式中,Driver进程挂掉,找不到数据的问题。 spark.streaming.receiver.writeAheadLog.enable #默认false 没有开启 blockInterval: spark.streaming.blockInterval #默认200ms 反压机制: 用于解决由于job执行时间大于batchInterval,接收数据内存级别为仅内存时。引起的数据堆积问题 sparkStreaming在1.5版本之后引入反压机制(back-pressure),通过动态控制数据接收速率来适配集群数据处理能力! spark.streaming.backpressure.enabled 设置为true #默认false 数据接收速率: sparkStreaming在1.5版本之前通过控制接收数据的速率来解决数据堆积问题。 设置静态配置参数: # Receiver模式spark.streaming.receiver.maxRate #默认没有设置# Direct模式spark.streaming.kafka.maxRatePerPartition #默认没有设置 如何优雅的关闭SparkStreaming spark.streaming.stopGracefullyOnShutdown 设置为true #默认falsekill -15 进程号","tags":[{"name":"SparkStreaming","slug":"SparkStreaming","permalink":"http://www.seanxia.cn/tags/SparkStreaming/"},{"name":"流式处理","slug":"流式处理","permalink":"http://www.seanxia.cn/tags/%E6%B5%81%E5%BC%8F%E5%A4%84%E7%90%86/"}]},{"title":"Spark计算框架(五)","date":"2018-01-20T16:00:00.000Z","path":"大数据/4b9530c5.html","text":"Spark SQL 是 Spark 处理数据的一个模块,跟基本的 Spark RDD 的API不同,Spark SQL中提供的接口将会提供给Spark 更多关于结构化数据和计算的信息。其本质是,Spark SQL使用这些额外的信息去执行额外的优化。 Shark Shark 是基于 Spark 计算框架之上且兼容 Hive 语法的 SQL 执行引擎,由于底层的计算采用了 Spark,性能比 MapReduce 的 Hive 普遍快 2 倍以上,当数据全部 load 在内存的话,将快 10 倍以上,因此 Shark 可以作为交互式查询应用服务来使用。 除了基于 Spark 的特性外,Shark 是完全兼容 Hive 的语法,表结构以及UDF函数等,已有的 HiveSql 可以直接进行迁移至 Shark 上 Shark 底层依赖于 Hive 的解析器,查询优化器,但正是由于 SHark 的整体设计架构对 Hive 的依赖性太强,难以支持其长远发展,比如不能和 Spark 的其他组件进行很好的集成,无法满足 Spark 的一栈式解决大数据处理的需求。 SparkSQL 1、SparkSQL 介绍 Hive 是 Shark 的前身,Shark 是 SparkSQL 的前身,SparkSQL 产生的根本原因是其完全脱离了 Hive 的限制。 SparkSQL支持查询原生的RDD。 RDD是Spark平台的核心概念,是 Spark 能够高效的处理大数据的各种场景的基础。 能够在 Scala 中写 SQL 语句。支持简单的 SQL 语法检查,能够在Scala中写Hive语句访问Hive数据,并将结果取回作为RDD使用。 2、Spark on Hive 和 Hive on Spark Spark on Hive: Hive 只作为储存角色,Spark 负责 sql 解析优化,执行。 Hive on Spark:Hive 即作为存储又负责 sql 的解析优化,Spark 负责执行。 3、DataFrame DataFrame 也是一个分布式数据容器。与 RDD 类似,然而 DataFrame 更像传统数据库的二维表格,除了数据以外,还掌握数据的结构信息(元数据信息),即 schema。 同时,与 Hive 类似,DataFrame 也支持嵌套数据类型(struct、array 和 map)。从 API 易用性的角度上 看, DataFrame API 提供的是一套高层的关系操作,比函数式的 RDD API 要更加友好,门槛更低。 DataFrame 的底层封装的是 RDD,只不过 RDD 的泛型是 Row 类型。 4、SparkSQL 的数据源 SparkSQL 的数据源可以是 JSON 类型的字符串,JDBC,Parquent,Hive,HDFS 等。 5、SparkSQL 底层架构 首先拿到 sql 后解析一批未被解决的逻辑计划,再经过分析得到分析后的逻辑计划,再经过一批优化规则转换成一批最佳优化的逻辑计划,再经过 SparkPlanner 的策略转化成一批物理计划,随后经过消费模型转换成一个个的 Spark 任务执行。 6、谓词下推(predicate Pushdown) 我们知道,可以通过封装SparkSql的Data Source API完成各类数据源的查询,那么如果底层数据源无法高效完成数据的过滤,就会执行直接的全局扫描,把每条相关的数据都交给SparkSql的Filter操作符完成过滤,虽然SparkSql使用的Code Generation技术极大的提高了数据过滤的效率,但是这个过程无法避免大量数据的磁盘读取,甚至在某些情况下会涉及网络IO(例如数据非本地化时);如果底层数据源在进行扫描时能非常快速的完成数据的过滤,那么就会把过滤交给底层数据源来完成,这就是SparkSql中的谓词下推。 创建 DataFrame 的几种方式 1. 读取 Json 格式的文件创建 DataFrame 注意: Json 文件中的 json 数据不能嵌套 json 格式数据。 DataFrame 是一个一个 Row 类型的 RDD,df.rdd()/df.javaRdd()。 可以两种方式读取 json 格式的文件。 df.show()默认显示前 20 行数据。 DataFrame 原生 API 可以操作 DataFrame(不方便)。 注册成临时表时,表中的列默认按 ASCII 顺序显示列。 Java: public class CreateDFFromJosonFile { public static void main(String[] args){ // 配置上下文环境 SparkConf conf = new SparkConf(); conf.setMaster("local").setAppName("jsonFile"); SparkContext sc = new SparkContext(conf); // 创建sqlContext SQLContext sqlContext = new SQLContext(sc); /** * DataFrame的底层是一个一个的RDD RDD的泛型是Row类型。 * 以下两种方式都可以读取json格式的文件 */ DataFrame df = sqlContext.read().format("json").load("./data/json");// DataFrame df = sqlContext.read().json("./data/json"); /** * 显示 DataFrame中的内容,默认显示前20行。如果现实多行要指定多少行show(行数) * 注意:当有多个列时,显示的列先后顺序是按列的ascii码先后显示。 */// df.show(); // dataFrame转换成RDD,两种方式 RDD<Row> rdd = df.rdd();// JavaRDD<Row> rowJavaRDD = df.javaRDD(); // 树形形式显示scahema信息// df.printSchema(); /** * dataFram自带的API 操作DataFrame */ // select name ,age from table where age>18 df.select(df.col("name"),df.col("age")).where(df.col("age").gt(18)).show(); //select count(*) from table group by age df.groupBy(df.col("age")).count().show(); /** * 将DataFrame注册成临时的一张表, * 这张表临时注册到内存中,是逻辑上的表,不会雾化到磁盘 */ df.registerTempTable("jtable"); DataFrame result = sqlContext.sql("select age, count(1) from jtable group by age"); result.show(); sc.stop(); }} Scala: object CreateDFFromJsonFile { def main(args: Array[String]): Unit = { val conf = new SparkConf() conf.setMaster("local").setAppName("jsonFile") val sc = new SparkContext(conf) val sqlContext = new SQLContext(sc) val df = sqlContext.read.json("./data/json")// val df = sqlContext.read.format("json").load("./data/json") df.show() df.printSchema() // select * from table df.select(df.col("name")).show() // select name from table where age>19 df.select(df.col("name"), df.col("age")).where(df.col("age").gt(19)).show() // 注册临时表 df.registerTempTable("jtable") val result = sqlContext.sql("select * from jtable") result.show() sc.stop() }} 2、通过 json 格式的 RDD 创建 DataFrame Java: public class CreateDFFromJsonRDD { public static void main(String[] args){ SparkConf conf = new SparkConf(); conf.setMaster("local").setAppName("jsonRDD"); JavaSparkContext sc = new JavaSparkContext(conf); SQLContext sqlContext = new SQLContext(sc); JavaRDD<String> nameRDD = sc.parallelize(Arrays.asList( "{\\"name\\":\\"zhangsan\\",\\"age\\":\\"18\\"}", "{\\"name\\":\\"lisi\\",\\"age\\":\\"19\\"}", "{\\"name\\":\\"wangwu\\",\\"age\\":\\"20\\"}" )); JavaRDD<String> scoreRDD = sc.parallelize(Arrays.asList( "{\\"name\\":\\"zhangsan\\",\\"score\\":\\"100\\"}", "{\\"name\\":\\"lisi\\",\\"score\\":\\"200\\"}", "{\\"name\\":\\"wangwu\\",\\"score\\":\\"300\\"}" )); DataFrame namedf = sqlContext.read().json(nameRDD); DataFrame scoredf = sqlContext.read().json(scoreRDD); namedf.registerTempTable("t1"); scoredf.registerTempTable("t2"); DataFrame result = sqlContext.sql("select t1.name, t1.age, t2.score from t1, t2 where t1.name=t2.name "); result.show(); sc.stop(); }} Scala: object CreateDFFromJsonRDD { def main(args: Array[String]): Unit = { val conf = new SparkConf() conf.setMaster("local").setAppName("jsonRDD") val sc = new SparkContext(conf) val sqlContext = new SQLContext(sc) val nameRDD = sc.makeRDD(Array( "{\\"name\\":\\"zhangsan\\",\\"age\\":18}", "{\\"name\\":\\"lisi\\",\\"age\\":19}", "{\\"name\\":\\"wangwu\\",\\"age\\":20}" )) val scoreRDD = sc.makeRDD(Array( "{\\"name\\":\\"zhangsan\\",\\"score\\":100}", "{\\"name\\":\\"lisi\\",\\"score\\":200}", "{\\"name\\":\\"wangwu\\",\\"score\\":300}" )) val nameDF = sqlContext.read.json(nameRDD) val scoreDF = sqlContext.read.json(scoreRDD) // 创建临时表t1、t2 nameDF.registerTempTable("t1") scoreDF.registerTempTable("t2") val result = sqlContext.sql("select t1.name, t1.age, t2.score from t1, t2 where t1.name = t2.name") result.show() sc.stop() }} 3、非 json 格式的 RDD 创建 DataFrame (1)通过反射的方式将非 json 格式的 RDD 转换成 DataFrame(不建议使用) 自定义类要可序列化 自定义类的访问级别是 Public RDD 转成 DataFrame 后会根据映射将字段按 Assci 码排序 将 DataFrame 转换成 RDD 时获取字段两种方式,一种是df.getInt(0)下标获取(不推荐使用),另一种是 df.getAs(“列名”)获取(推荐使用) Java: public class CreateDFFromRDDWithReflect { public static void main(String[] args){ SparkConf conf = new SparkConf(); conf.setMaster("local").setAppName("RDD"); JavaSparkContext sc = new JavaSparkContext(conf); SQLContext sqlContext = new SQLContext(sc); JavaRDD<String> lineRDD = sc.textFile("./data/person.txt"); JavaRDD<Person> personRDD = lineRDD.map(new Function<String, Person>() { @Override public Person call(String s) throws Exception { Person p = new Person(); p.setId(s.split(",")[0]); p.setName(s.split(",")[1]); p.setAge(Integer.valueOf(s.split(",")[2])); return p; } }); /** * 传入进去Person.class的时候,sqlContext是通过反射的方式创建DataFrame * 在底层通过反射的方式获得Person的所有field,结合RDD本身,就生成了DataFrame */ DataFrame df = sqlContext.createDataFrame(personRDD, Person.class); df.show(); df.registerTempTable("person"); sqlContext.sql("select name from person where id = 2").show(); /** * 将DataFrame转成JavaRDD * 注意: * 1.可以使用row.getInt(0),row.getString(1)... * 通过下标获取返回Row类型的数据,但是要注意列顺序问题---不常用 * 2.可以使用row.getAs("列名")来获取对应的列值 */ JavaRDD<Row> javaRDD = df.javaRDD(); JavaRDD<Person> map = javaRDD.map(new Function<Row, Person>() { @Override public Person call(Row row) throws Exception { Person p = new Person(); //p.setId(row.getString(1)); //p.setName(row.getString(2)); //p.setAge(row.getInt(0)); p.setId((String) row.getAs("id")); p.setName((String) row.getAs("name")); p.setAge((Integer) row.getAs("age")); return p; } }); map.foreach(new VoidFunction<Person>() { @Override public void call(Person person) throws Exception { System.out.println(person); } }); sc.stop(); }} Scala: object CreateDFFromRDDWithReflect { def main(args: Array[String]): Unit = { val conf = new SparkConf() conf.setMaster("local").setAppName("reflectRDD") val sc = new SparkContext(conf) val sqlContext = new SQLContext(sc) val lineRDD = sc.textFile("./data/person.txt") // 将RDD隐式转换成DataFrame import sqlContext.implicits._ val personRDD = lineRDD.map(x => { val person = Person( x.split(",")(0), x.split(",")(1), Integer.valueOf(x.split(",")(2) )) person }) val df = personRDD.toDF() df.show() /** * 将DataFrame转换成PersonRDD */ val rdd = df.rdd val result = rdd.map(x => { Person(x.getAs("id"), x.getAs("name"), x.getAs("age")) }) result.foreach(println) sc.stop() } } (2)动态创建 Schema 将非 json 格式的 RDD 转换成 DataFrame Java: public class CreateDFFromRDDWithStruct { public static void main(String[] args){ SparkConf conf = new SparkConf(); conf.setMaster("local").setAppName("rddStruct"); JavaSparkContext sc = new JavaSparkContext(conf); SQLContext sqlContext = new SQLContext(sc); JavaRDD<String> lineRDD = sc.textFile("./data/person.txt"); /** * 转换成Row类型的RDD */ JavaRDD<Row> rowRDD = lineRDD.map(new Function<String, Row>() { @Override public Row call(String s) throws Exception { return RowFactory.create( s.split(",")[0], s.split(",")[1], Integer.valueOf(s.split(",")[0]) ); } }); /** * 动态构建DataFrame中的元数据, * 一般来说这里的字段可以来源自字符串,也可以来源于外部数据库 */ List<StructField> structFields = Arrays.asList( DataTypes.createStructField("id", DataTypes.StringType, true), DataTypes.createStructField("aname", DataTypes.StringType, true), DataTypes.createStructField("age", DataTypes.IntegerType, true) ); StructType schema = DataTypes.createStructType(structFields); DataFrame df = sqlContext.createDataFrame(rowRDD, schema); df.printSchema(); df.show(); sc.stop(); }} Scala: object CreateDFFromRDDWithStruct { def main(args: Array[String]): Unit = { val conf = new SparkConf() conf.setMaster("local").setAppName("rddStruct") val sc = new SparkContext(conf) val sqlContext = new SQLContext(sc) val lineRDD = sc.textFile("./data/person.txt") val rowRDD = lineRDD.map(x => { val split = x.split(",") RowFactory.create(split(0),split(1),Integer.valueOf(split(2))) }) val schema = StructType(List( StructField("id", StringType, true), StructField("name", StringType, true), StructField("age", IntegerType, true) )) val df = sqlContext.createDataFrame(rowRDD, schema) df.show() df.printSchema() sc.stop() }} 4、读取 parquet 文件创建 DataFrame 注意: 可以将 DataFrame 存储成 parquet 文件。保存成 parquet 文件的方式有两种 df.write().mode(SaveMode.Overwrite)format("parquet").save("./sparksql/parquet");df.write().mode(SaveMode.Overwrite).parquet("./sparksql/parquet"); SaveMode 指定文件保存时的模式。 Overwrite:覆盖 Append:追加 ErrorIfExists:如果存在就报错 Ignore:如果存在就忽略 Java: public class CreateDFFromParquet { public static void main(String[] args){ SparkConf conf = new SparkConf(); conf.setMaster("local").setAppName("parquet"); JavaSparkContext sc = new JavaSparkContext(conf); SQLContext sqlContext = new SQLContext(sc); JavaRDD<String> jsonRDD = sc.textFile("./data/json"); DataFrame df = sqlContext.read().json(jsonRDD); /** * 将DataFrame保存成parquet文件,SaveMode指定存储文件时的保存模式 * 保存成parquet文件有以下两种方式: */ df.write().mode(SaveMode.Overwrite).format("parquet").save("./data/parquet"); df.write().mode(SaveMode.Overwrite).parquet("./data/parquet"); df.show(); /** * 加载parquet文件成DataFrame * 加载parquet文件有以下两种方式: */ DataFrame parquet = sqlContext.read().format("parquet").load("./data/parquet");// DataFrame parquet = sqlContext.read().parquet("./data/parquet"); parquet.show(); sc.stop(); }} Scala: object CreateDFFromParquet { def main(args: Array[String]): Unit = { val conf = new SparkConf() conf.setMaster("local").setAppName("parquet") val sc = new SparkContext(conf) val sqlContext = new SQLContext(sc) val jsonRDD = sc.textFile("./data/json") val df = sqlContext.read.json(jsonRDD) df.show() /** * 将DF保存为parquet文件 */ df.write.mode(SaveMode.Overwrite).format("parquet").save("./data/parquet")// df.write.mode(SaveMode.Overwrite).parquet("./data/parquet") /** * 读取parquet文件 */ val result = sqlContext.read.parquet("./data/parquet")// val result = sqlContext.read.format("parquet").load("./data/parquet") result.show() sc.stop() }} 5、读取 JDBC 中的数据创建 DataFrame(MySql 为例) Java: public class CreateDFFromMysql { public static void main(String[] args){ SparkConf conf = new SparkConf(); conf.setMaster("local").setAppName("mysql"); JavaSparkContext sc = new JavaSparkContext(conf); SQLContext sqlContext = new SQLContext(sc); /** * 第一种方式读取MySql数据库表,加载为DataFrame */ HashMap<String, String> options = new HashMap<>(); options.put("url", "jdbc:mysql://127.0.0.1:3306/spark"); options.put("driver", "com.mysql.jdbc.Driver"); options.put("user", "root"); options.put("password", "123456"); options.put("dbtable", "person"); DataFrame person = sqlContext.read().format("jdbc").options(options).load();// DataFrame person = sqlContext.read().jdbc(); person.show(); // 创建临时表t1 person.registerTempTable("t1"); /** * 第二种方式读取MySql数据表加载为DataFrame */ DataFrameReader reader = sqlContext.read().format("jdbc"); reader.option("url", "jdbc:mysql://127.0.0.1:3306/spark"); reader.option("driver", "com.mysql.jdbc.Driver"); reader.option("user", "root"); reader.option("password", "123456"); reader.option("dbtable", "score"); DataFrame score = reader.load(); score.show(); // 创建临时表t2 score.registerTempTable("t2"); DataFrame result = sqlContext.sql("select t1.id, t1.name, t2.score from t1, t2 where person.name = score.name"); result.show(); /** * 将DataFrame结果保存到Mysql中 */ Properties properties = new Properties(); properties.setProperty("user", "root"); properties.setProperty("password", "123456"); result.write().mode(SaveMode.Overwrite). jdbc("jdbc:mysql://127.0.0.1:3306/spark", "result", properties); sc.stop(); }} Scala: object CreateDFFromMysql { def main(args: Array[String]): Unit = { val conf = new SparkConf() conf.setMaster("local").setAppName("mysql") val sc = new SparkContext(conf) val sqlContext = new SQLContext(sc) /** * 第一种方式读取Mysql数据库表创建DF */ val options = new HashMap[String,String](); options.put("url", "jdbc:mysql://127.0.0.1:3306/spark") options.put("driver","com.mysql.jdbc.Driver") options.put("user","root") options.put("password", "123456") options.put("dbtable","person") val person = sqlContext.read.format("jdbc").options(options).load() person.show() // 创建临时表t1 person.registerTempTable("t1") val reader = sqlContext.read.format("jdbc") reader.option("url", "jdbc:mysql://127.0.0.1:3306/spark") reader.option("driver","com.mysql.jdbc.Driver") reader.option("user","root") reader.option("password","123456") reader.option("dbtable", "score") val score = reader.load() score.show() // 创建临时表t2 score.registerTempTable("t2") val result = sqlContext.sql("select t1.id, t1.name, t2.score from t1, t2 where t1.name = t2.name") result.show() sc.stop() }} 6、读取 Hive 中的数据加载成 DataFrame HiveContext 是 SQLContext 的子类,连接 Hive 建议使用 HiveContext。 由于本地没有 Hive 环境,要提交到集群运行,提交命令: ./spark-submit--master spark://sean01:7077,sean02:7077--executor-cores 1--executor-memory 1G--total-executor-cores 1--class com.seanxia.spark.java.sql.dataframe.CreateDFFromHiveCluster/root/test/HiveTest.jar Java: public class CreateDFFromHiveCluster { public static void main(String[] args){ SparkConf conf = new SparkConf().setAppName("hive"); JavaSparkContext sc = new JavaSparkContext(conf); // HiveContext是SQLContext的子类 HiveContext hiveContext = new HiveContext(sc); // 使用spark实例库 hiveContext.sql("USE spark"); // 表student_infos如果存在就删除 hiveContext.sql("DROP TABLE IF EXISTS student_infos"); // 在hive中创建student_infos表 hiveContext.sql("CREATE TABLE IF NOT EXISTS student_infos " + "(name STRING,age INT) row format delimited fields terminated by '\\t' "); // 加载数据 hiveContext.sql("load data local inpath '/root/test/student_infos' into table student_infos"); //第二种读取Hive表加载DF方式// hiveContext.table("student_infos"); hiveContext.sql("DROP TABLE IF EXISTS student_scores"); // 在hive中创建student_scores表 hiveContext.sql("CREATE TABLE IF NOT EXISTS student_scores " + "(name STRING, score INT) row format delimited fields terminated by '\\t'"); hiveContext.sql("LOAD DATA LOCAL INPATH '/root/test/student_scores' INTO TABLE student_scores"); /** * 查询表生成DataFrame */ DataFrame goodStudentsDF = hiveContext.sql("SELECT si.name, si.age, ss.score " + "FROM student_infos si " + "JOIN student_scores ss " + "ON si.name=ss.name " + "WHERE ss.score>=80"); // 注册临时表 goodStudentsDF.registerTempTable("goodStudent"); DataFrame result = hiveContext.sql("select * from goodStudent"); result.show(); /** * 将结果保存到hive表,good_student_infos */ hiveContext.sql("DROP TABLE IF EXISTS good_student_infos"); goodStudentsDF.write().mode(SaveMode.Overwrite) .saveAsTable("good_student_infos"); DataFrame table = hiveContext.table("good_student_infos"); Row[] goodStudentRows = table.collect(); for (Row goodStudentRow : goodStudentRows) { System.out.println(goodStudentRow); } sc.stop(); }} Scala: object CreateDFFromHiveCluster { def main(args: Array[String]): Unit = { val conf = new SparkConf().setAppName("hive") val sc = new SparkContext(conf) // HiveContext是SQLContext的子类 val hiveContext = new HiveContext(sc) // 使用spark实例库 hiveContext.sql("use spark") // 表student_infos如果存在就删除 hiveContext.sql("drop table if exists student_infos") // 在hive中创建student_infos表 hiveContext.sql("create table if not exists student_infos (name string,age int) " + "row format delimited fields terminated by '\\t'") // 加载数据 hiveContext.sql("load data local inpath '/root/test/student_infos' into table student_infos") //第二种读取Hive表加载DF方式// hiveContext.table("student_infos") hiveContext.sql("drop table if exists student_scores") hiveContext.sql("create table if not exists student_scores (name string,score int) " + "row format delimited fields terminated by '\\t'") hiveContext.sql("load data local inpath '/root/test/student_scores' into tablestudent_scores") /** * 查询表生成DataFrame */ val df = hiveContext.sql("select si.name,si.age,ss.score from student_infos si," + "student_scores ss where si.name = ss.name") hiveContext.sql("drop table if exists good_student_infos") /** * 将结果写入到hive表中 */ df.write.mode(SaveMode.Overwrite).saveAsTable("good_student_infos") sc.stop() }} 序列化问题 Java中不能被序列化的几种情况: 1、反序列化时serializable 版本号不一致时会导致不能反序列化。 2、子类中实现了serializable接口,父类中没有实现,父类中的变量不能被序列化,序列化后父类中的变量会得到null。 注意:父类实现serializable接口,子类没有实现serializable接口时,子类可以正常序列化 3、被关键字transient修饰的变量不能被序列化。 4、静态变量不能被序列化,属于类,不属于方法和对象,所以不能被序列化。 存储 DataFrame 1、将 DataFrame 存储为 parquet 文件。 2、将 DataFrame 存储到 JDBC 数据库。 3、将 DataFrame 存储到 Hive 表。 自定义函数 UDF 和 UDAF 1、UDF:用户自定义函数 Java: public class UDF { public static void main(String[] args) { SparkConf conf = new SparkConf(); conf.setMaster("local").setAppName("UDF"); JavaSparkContext sc = new JavaSparkContext(conf); SQLContext sqlContext = new SQLContext(sc); JavaRDD<String> parallelize = sc.parallelize(Arrays.asList("zhangsan","lisi","wangwu")); JavaRDD<Row> rowRDD = parallelize.map(new Function<String, Row>() { private static final long serialVersionUID = 1L; @Override public Row call(String s) throws Exception { return RowFactory.create(s); } }); /** * 动态创建Schema方式加载DF */ List<StructField> fields = new ArrayList<StructField>(); fields.add(DataTypes.createStructField("name", DataTypes.StringType,true)); StructType schema = DataTypes.createStructType(fields); DataFrame df = sqlContext.createDataFrame(rowRDD,schema); df.registerTempTable("user"); /** * 根据UDF函数参数的个数来决定是实现哪一个UDF UDF1,UDF2。。。。UDF1xxx */ sqlContext.udf().register("StrLen",new UDF2<String, Integer, Integer>() { private static final long serialVersionUID = 1L; @Override public Integer call(String t1, Integer t2) throws Exception { return t1.length() + t2; } } , DataTypes.IntegerType ); sqlContext.sql("select name ,StrLen(name,100) as length from user").show(); sc.stop(); }} Scala: object UDF { def main(args: Array[String]): Unit = { val conf = new SparkConf() conf.setMaster("local").setAppName("udf") val sc = new SparkContext(conf) val sqlContext = new SQLContext(sc); val rdd = sc.makeRDD(Array("zhansan","lisi","wangwu")) val rowRDD = rdd.map { x => { RowFactory.create(x) } } val schema = DataTypes.createStructType(Array(StructField("name",StringType,true))) val df = sqlContext.createDataFrame(rowRDD, schema) df.registerTempTable("user") //sqlContext.udf.register("StrLen",(s : String)=>{s.length()}) //sqlContext.sql("select name ,StrLen(name) as length from user").show sqlContext.udf.register("StrLen",(s : String,i:Int)=>{s.length()+i}) sqlContext.sql("select name ,StrLen(name,10) as length from user").show sc.stop() }} 2、UDAF:用户自定义聚合函数 实现 UDAF 函数如果要自定义类要实现 UserDefinedAggregateFunction 类。 Java: public class UDAF { public static void main(String[] args) { SparkConf conf = new SparkConf(); conf.setMaster("local").setAppName("UDAF"); conf.set("spark.sql.shuffle.partitions", "1"); JavaSparkContext sc = new JavaSparkContext(conf); SQLContext sqlContext = new SQLContext(sc); JavaRDD<String> parallelize = sc.parallelize( Arrays.asList("zhangsan", "lisi", "wangwu", "zhangsan", "zhangsan", "lisi", "zhangsan", "lisi", "wangwu", "zhangsan", "zhangsan", "lisi"),2); JavaRDD<Row> rowRDD = parallelize.map(new Function<String, Row>() { private static final long serialVersionUID = 1L; @Override public Row call(String s) throws Exception { return RowFactory.create(s); } }); List<StructField> fields = new ArrayList<StructField>(); fields.add(DataTypes.createStructField("name", DataTypes.StringType, true)); StructType schema = DataTypes.createStructType(fields); DataFrame df = sqlContext.createDataFrame(rowRDD, schema); df.registerTempTable("user"); /** * 注册一个UDAF函数,实现统计相同值得个数 * 注意:这里可以自定义一个类继承UserDefinedAggregateFunction类也是可以的 */ sqlContext.udf().register("StringCount", new UserDefinedAggregateFunction() { private static final long serialVersionUID = 1L; /** * 初始化一个内部的自己定义的值,在Aggregate之前每组数据的初始化结果 */ @Override public void initialize(MutableAggregationBuffer buffer) { buffer.update(0, 0); System.out.println("init ....." + buffer.get(0)); } /** * 更新 可以认为一个一个地将组内的字段值传递进来 实现拼接的逻辑 * buffer.getInt(0)获取的是上一次聚合后的值 * 相当于map端的combiner,combiner就是对每一个map task的处理结果进行一次小聚合 * 大聚和发生在reduce端. * 这里即是:在进行聚合的时候,每当有新的值进来,对分组后的聚合如何进行计算 */ @Override public void update(MutableAggregationBuffer buffer, Row arg1) { System.out.println(buffer.getClass() + "-----------------------"); buffer.update(0, buffer.getInt(0) + 1); System.out.println("update.....buffer" + buffer.toString() + " | row" + arg1.toString() ); } /** * 合并 update操作,可能是针对一个分组内的部分数据,在某个节点上发生的 但是可能一个分组内的数据,会分布在多个节点上处理 * 此时就要用merge操作,将各个节点上分布式拼接好的串,合并起来 * buffer1.getInt(0) : 大聚合的时候 上一次聚合后的值 * buffer2.getInt(0) : 这次计算传入进来的update的结果 * 这里即是:最后在分布式节点完成后需要进行全局级别的Merge操作 */ public void merge(MutableAggregationBuffer buffer1, Row arg1) { buffer1.update(0, buffer1.getInt(0) + arg1.getInt(0)); System.out.println("merge.....buffer : " + buffer1.toString() + "| row" + arg1.toString() ); } /** * 在进行聚合操作的时候所要处理的数据的结果的类型 */ @Override public StructType bufferSchema() { return DataTypes.createStructType(Arrays.asList( DataTypes.createStructField("bffer", DataTypes.IntegerType, true))); } /** * 最后返回一个和DataType的类型要一致的类型,返回UDAF最后的计算结果 */ @Override public Object evaluate(Row row) { return row.getInt(0); } /** * 指定UDAF函数计算后返回的结果类型 */ @Override public DataType dataType() { return DataTypes.IntegerType; } /** * 指定输入字段的字段及类型 */ @Override public StructType inputSchema() { return DataTypes.createStructType(Arrays.asList( DataTypes.createStructField("name", DataTypes.StringType, true))); } /** * 确保一致性 一般用true,用以标记针对给定的一组输入,UDAF是否总是生成相同的结果。 */ @Override public boolean deterministic() { return true; } }); sqlContext.sql("select name ,StringCount(name) as number from user group by name").show(); sc.stop(); }} Scala: class MyUDAF extends UserDefinedAggregateFunction { // 聚合操作时,所处理的数据的类型 def bufferSchema: StructType = { DataTypes.createStructType(Array(DataTypes.createStructField("aaa", IntegerType, true))) } // 最终函数返回值的类型 def dataType: DataType = { DataTypes.IntegerType } def deterministic: Boolean = { true } // 最后返回一个最终的聚合值 要和dataType的类型一一对应 def evaluate(buffer: Row): Any = { buffer.getAs[Int](0) } // 为每个分组的数据执行初始化值 def initialize(buffer: MutableAggregationBuffer): Unit = { buffer(0) = 0 } //输入数据的类型 def inputSchema: StructType = { DataTypes.createStructType(Array(DataTypes.createStructField("input", StringType, true))) } // 最后merger的时候,在各个节点上的聚合值,要进行merge,也就是合并 def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = { buffer1(0) = buffer1.getAs[Int](0)+buffer2.getAs[Int](0) } // 每个组,有新的值进来的时候,进行分组对应的聚合值的计算 def update(buffer: MutableAggregationBuffer, input: Row): Unit = { buffer(0) = buffer.getAs[Int](0)+1 }}object UDAF { def main(args: Array[String]): Unit = { val conf = new SparkConf() conf.setMaster("local").setAppName("udaf") val sc = new SparkContext(conf) val sqlContext = new SQLContext(sc) val rdd = sc.makeRDD(Array("zhangsan","lisi","wangwu","zhangsan","lisi")) val rowRDD = rdd.map { x => {RowFactory.create(x)} } val schema = DataTypes.createStructType(Array( DataTypes.createStructField("name", StringType, true))) val df = sqlContext.createDataFrame(rowRDD, schema) df.show() df.registerTempTable("user") /** * 注册一个udaf函数 */ sqlContext.udf.register("StringCount", new MyUDAF()) sqlContext.sql("select name ,StringCount(name) from user group byname").show() sc.stop() }} 开窗函数 注意: row_number() 开窗函数是按照某个字段分组,然后取另一字段的前几个的值,相当于分组取 topN。 如果 SQL 语句里面使用到了开窗函数,那么这个 SQL 语句必须使用 HiveContext 来执行,HiveContext 默认情况下在本地无法创建。 开窗函数格式: row_number() over (partition by XXX order by XXX); 开窗函数语义说明: 1、首先在 select 查询时,使用 row_number() 函数,其次,row_number() 函数后面先跟上 over 关键字; 2、然后括号中,是 partition by,也就是说根据哪个字段进行分组; 3、其次是可以用 order by 进行组内排序; 4、这样 row_number() 就可以给每个组内的行,打上一个组内行号。 开窗函数的作用,其实就是,给每个分组的数据,按照排序顺序,打上分组内的行号。 OOM异常解决: 使用开窗函数由于会消耗大量内存空间,容易报OOM异常,出现此异常时,只需调整当前程序配置。步骤和配置如下: 只需在Configration中的VM options填入如下配置即可。 -Xms800m -Xmx800m -XX:PermSize=64M -XX:MaxNewSize=256m -XX:MaxPermSize=128m 代码展示: Java: public class RowNumberWindowFun { public static void main(String[] args) { SparkConf conf = new SparkConf(); conf.setAppName("windowfun").setMaster("local"); JavaSparkContext sc = new JavaSparkContext(conf); conf.set("spark.sql.shuffle.partitions", "1"); HiveContext hiveContext = new HiveContext(sc); hiveContext.sql("use spark"); hiveContext.sql("drop table if exists sales"); hiveContext.sql("create table if not exists sales (riqi string,leibie string,jine Int) " + "row format delimited fields terminated by '\\t'"); hiveContext.sql("load data local inpath './data/sales.txt' into table sales"); /** * 开窗函数格式: * 【 row_number() over (partition by XXX order by XXX) as rank】 * 注意:rank 从1开始 */ /** * 以类别分组,按每种类别金额降序排序,显示 【日期,种类,金额】 结果,如: * * 1 A 100 * 2 B 200 * 3 A 300 * 4 B 400 * 5 A 500 * 6 B 600 * 排序后: * 5 A 500 --rank 1 * 3 A 300 --rank 2 * 1 A 100 --rank 3 * 6 B 600 --rank 1 * 4 B 400 --rank 2 * 2 B 200 --rank 3 */ DataFrame result = hiveContext.sql("select riqi,leibie,jine,rank " + "from (select riqi,leibie,jine, " + "row_number() over (partition by leibie order by jine desc) rank " + "where t.rank<=3"); result.show(100); /** * 将结果保存到hive表sales_result */ result.write().mode(SaveMode.Overwrite).saveAsTable("sales_result"); sc.stop(); }} Scala: object RowNumberWindowFun { def main(args: Array[String]): Unit = { val conf = new SparkConf() conf.setAppName("windowfun") val sc = new SparkContext(conf) val hiveContext = new HiveContext(sc) hiveContext.sql("use spark"); hiveContext.sql("drop table if exists sales"); hiveContext.sql("create table if not exists sales (riqi string,leibie string,jine Int) " + "row format delimited fields terminated by '\\t'"); hiveContext.sql("load data local inpath '/root/test/sales' into table sales"); /** * 开窗函数格式: * 【 rou_number() over (partitin by XXX order by XXX) 】 */ val result = hiveContext.sql("select riqi,leibie,jine " + "from (select riqi,leibie,jine," + "row_number() over (partition by leibie order by jine desc) rank " + "from sales) t " + "where t.rank<=3"); result.show(); sc.stop() }}","tags":[{"name":"SparkSQL","slug":"SparkSQL","permalink":"http://www.seanxia.cn/tags/SparkSQL/"},{"name":"DataFrame","slug":"DataFrame","permalink":"http://www.seanxia.cn/tags/DataFrame/"}]},{"title":"SparkShuffle调优","date":"2018-01-15T16:00:00.000Z","path":"大数据/3243dc0c.html","text":"SparkShuffle在使用时,默认的配置中有些配置可能不适合实际中的业务处理,需要我们手动进行调整优化。这里列举了一些常用的 SparkShuffle 调优策略和建议。 1、spark.shuffle.file.buffer 默认值:32k 参数说明:该参数用于设置shuffle write task的BufferedOutputStream的buffer缓冲大小。将数据写到磁盘文件之前,会先写入buffer缓冲中,待缓冲写满之后,才会溢写到磁盘。 调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如64k),从而减少shuffle write过程中溢写磁盘文件的次数,也就可以减少磁盘IO次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。 2、spark.reducer.maxSizeInFlight 默认值:48m 参数说明:该参数用于设置shuffle read task的buffer缓冲大小,而这个buffer缓冲决定了每次能够拉取多少数据。 调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如96m),从而减少拉取数据的次数,也就可以减少网络传输的次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。 3、spark.shuffle.io.maxRetries 默认值:3 参数说明:shuffle read task从shuffle write task所在节点拉取属于自己的数据时,如果因为网络异常导致拉取失败,是会自动进行重试的。该参数就代表了可以重试的最大次数。如果在指定次数之内拉取还是没有成功,就可能会导致作业执行失败。 调优建议:对于那些包含了特别耗时的shuffle操作的作业,建议增加重试最大次数(比如60次),以避免由于JVM的full gc或者网络不稳定等因素导致的数据拉取失败。在实践中发现,对于针对超大数据量(数十亿~上百亿)的shuffle过程,调节该参数可以大幅度提升稳定性。 shuffle file not find taskScheduler不负责重试task,由DAGScheduler负责重试stage 4、spark.shuffle.io.retryWait 默认值:5s 参数说明:具体解释同上,该参数代表了每次重试拉取数据的等待间隔,默认是5s。 调优建议:建议加大间隔时长(比如60s),以增加shuffle操作的稳定性。 5、spark.shuffle.memoryFraction 默认值:0.2 参数说明:该参数代表了Executor内存中,分配给shuffle read task进行聚合操作的内存比例,默认是20%。 调优建议:在资源参数调优中讲解过这个参数。如果内存充足,而且很少使用持久化操作,建议调高这个比例,给shuffle read的聚合操作更多内存,以避免由于内存不足导致聚合过程中频繁读写磁盘。在实践中发现,合理调节该参数可以将性能提升10%左右。 6、spark.shuffle.manager 默认值:sort|hash 参数说明:该参数用于设置ShuffleManager的类型。Spark 1.5以后,有三个可选项:hash、sort和tungsten-sort。HashShuffleManager是Spark 1.2以前的默认选项,但是Spark 1.2以及之后的版本默认都是SortShuffleManager了。tungsten-sort与sort类似,但是使用了tungsten计划中的堆外内存管理机制,内存使用效率更高。 调优建议:由于SortShuffleManager默认会对数据进行排序,因此如果你的业务逻辑中需要该排序机制的话,则使用默认的SortShuffleManager就可以;而如果你的业务逻辑不需要对数据进行排序,那么建议参考后面的几个参数调优,通过bypass机制或优化的HashShuffleManager来避免排序操作,同时提供较好的磁盘读写性能。这里要注意的是,tungsten-sort要慎用,因为之前发现了一些相应的bug。 7、spark.shuffle.sort.bypassMergeThreshold 默认值:200 参数说明:当ShuffleManager为SortShuffleManager时,如果shuffle read task的数量小于这个阈值(默认是200),则shuffle write过程中不会进行排序操作,而是直接按照未经优化的HashShuffleManager的方式去写数据,但是最后会将每个task产生的所有临时磁盘文件都合并成一个文件,并会创建单独的索引文件。 调优建议:当你使用SortShuffleManager时,如果的确不需要排序操作,那么建议将这个参数调大一些,大于shuffle read task的数量。那么此时就会自动启用bypass机制,map-side就不会进行排序了,减少了排序的性能开销。但是这种方式下,依然会产生大量的磁盘文件,因此shuffle write性能有待提高。 8、spark.shuffle.consolidateFiles 默认值:false 参数说明:如果使用HashShuffleManager,该参数有效。如果设置为true,那么就会开启consolidate机制,会大幅度合并shuffle write的输出文件,对于shuffle read task数量特别多的情况下,这种方法可以极大地减少磁盘IO开销,提升性能。 调优建议:如果的确不需要SortShuffleManager的排序机制,那么除了使用bypass机制,还可以尝试将spark.shffle.manager参数手动指定为hash,使用HashShuffleManager,同时开启consolidate机制。在实践中尝试过,发现其性能比开启了bypass机制的SortShuffleManager要高出10%~30%。","tags":[{"name":"SparkShuffle","slug":"SparkShuffle","permalink":"http://www.seanxia.cn/tags/SparkShuffle/"},{"name":"调优","slug":"调优","permalink":"http://www.seanxia.cn/tags/%E8%B0%83%E4%BC%98/"}]},{"title":"Spark计算框架(四)","date":"2018-01-15T16:00:00.000Z","path":"大数据/240bf582.html","text":"我们知道 Spark 的基本架构是 Master 和 Worker 组成的,Task 的分配和执行又是由 Driver 进程和 Excutor 进程 去配合完成的,那关于他们内部是怎么运行的,有哪些对象呢,这里将 做详细阐述。 广播变量和累加器 广播变量 广播变量理解图 广播变量使用 context.broadCast val conf = new SparkConf()conf.setMaster("local").setAppName("brocast")val sc = new SparkContext(conf)val list = List("hello xasxt")val broadCast = sc.broadcast(list)val lineRDD = sc.textFile("./words.txt")lineRDD.filter { x => broadCast.value.contains(x) }.foreach { println}sc.stop() 注意事项 能不能将一个 RDD 使用广播变量广播出去? 不能,因为RDD是不存储数据的。可以将RDD的结果广播出去。 广播变量只能在 Driver 端定义,Executor 端使用。 在 Driver 端可以修改广播变量的值,在 Executor 端无法修改广播变量的值。 目的 节省内存不必要的额外开销,提高性能(网络传递的次数就少了)。 累加器 累加器理解图 累加器的使用 context.accumulator(0) val conf = new SparkConf()conf.setMaster("local").setAppName("accumulator")val sc = new SparkContext(conf)val accumulator = sc.accumulator(0)sc.textFile("./words.txt").foreach { x =>{accumulator.add(1)}}println(accumulator.value)sc.stop() 注意事项 累加器只能在Driver端定义赋初始值,Excutor 端使用。 目的 事件全局统计。 资源调度源码分析 资源请求简图 资源调度 Master 路径: #路径:spark-1.6.0/core/src/main/scala/org.apache.spark/deploy/Master/Master.scala 提交应用程序,submit 的路径: #路径:spark-1.6.0/core/src/main/scala/org.apache.spark/deploy/SparkSubmit.scala 总结: 1、Executor 在集群中分散启动,有利于 task 计算的数据本地化。 2、默认情况下(提交任务的时候没有设置 --executor-cores 选项),每一个 Worker 为当前的 Application 启动一个 Executor,这个Executor 会使用这个 Worker 的所有的 cores 和 内存。 3、如果想在 Worker 上启动多个 Executor,提交 Application 的时候要加 --executor-cores 这个选项。 4、默认情况下没有设置 --total-executor-cores,一个 Application 会使用 Spark 集群中所有的 cores。 结论演示: 使用 Spark-submit 提交任务演示。也可以使用 spark-shell。 1、默认情况每个 worker 为当前的 Application 启动一个 Executor,这个 Executor 使用集群中所有的 cores 和 1G 内存。 ./spark-submit--master spark://sean01:7077--class org.apache.spark.examples.SparkPi../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100 2、在 workr 上启动多个 Executor,设置–executor-cores 参数指定每个 executor 使用的 core 数量。 ./spark-submit--master spark://sean01:7077--executor-cores 1--class org.apache.spark.examples.SparkPi../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100 3、内存不足的情况下启动 core 的情况。Spark 启动是不仅看 core 配置参数,也要看配置的 core 的内存是否够用。 ./spark-submit--master spark://sean01:7077--executor-cores 1--executor-memory 3g--class org.apache.spark.examples.SparkPi../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100 4、–total-executor-cores 集群中共使用多少 cores 注意:一个进程不能让集群多个节点共同启动。 ./spark-submit--master spark://sean01:7077--executor-cores 1--executor-memory 2g--total-executor-cores 3--class org.apache.spark.examples.SparkPi../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100 任务调度源码分析 Action 算子开始分析 任务调度可以从一个 Action 类算子开始。因为 Action 类算子会触发一个 job 的执行。 划分 stage,以 taskSet 形式提交任务 DAGScheduler 类中 getMessingParentStages() 方法是切割 job 划分 stage 。 可以结合以下这张图来分析: 源码: 源码中采用了递归的思想,把 Stage 和 RDD 分别放入两个 Set 集合中,对集合中的 RDD 进行先压栈再弹出,最后递归循环,追溯下一个 Stage。 SparkShuffle 概念 Shuffle 描述着数据从 map task 输出到 reduce task 输入的这段过程。在分布式情况下,reduce task 需要跨节点去拉取其它节点上的 map task 结果。这一过程将会产生网络资源消耗和内存,磁盘 IO 的消耗。 reduceByKey 会将上一个 RDD 中的每一个 key 对应的所有 value 聚合成一个 value,然后生成一个新的 RDD,元素类型是<key,value>对的形式,这样每一个 key 对应一个聚合起来的 value。 **问题:**聚合之前,每一个 key 对应的 value 不一定都是在一个 partition中,也不太可能在同一个节点上,因为 RDD 是分布式的弹性的数据集,RDD 的 partition 极有可能分布在各个节点上。 如何聚合? Shuffle Write: 上一个 stage 的每个 map task 就必须保证将自己处理的当前分区的数据相同的 key 写入一个分区文件中,可能会写入多个不同的分区文件中。 **Shuffle Read:**reduce task 就会从上一个 stage 的所有 task 所在的机器上寻找属于己的那些分区文件,这样就可以保证每一个 key 所对应的 value 都会汇聚到同一个节点上去处理和聚合。 Spark 中有两种 Shuffle 类型,HashShuffle 和 SortShuffle,Spark1.2 之前是 HashShuffle 默认的分区器是 HashPartitioner,Spark1.2 引入 SortShuffle 默认的分区器是 RangePartitioner。 HashShuffle 1、普通机制 普通机制示意图 执行流程 a) 每一个 map task 将不同结果写到不同的 buffer 中,每个 buffer 的大小为 32K。buffer 起到数据缓存的作用。 b) 每个 buffer 文件最后对应一个磁盘小文件。 c) reduce task 来拉取对应的磁盘小文件。 总结 ① map task 的计算结果会根据分区器(默认是 hashPartitioner)来决定写入到哪一个磁盘小文件中去。ReduceTask 会去 Map 端拉取相应的磁盘小文件。 ② 产生的磁盘小文件的个数:M(map task 的个数)*R(reduce task 的个数) 存在的问题 产生的磁盘小文件过多,会导致以下问题: a) 在 Shuffle Write 过程中会产生很多写磁盘小文件的对象。 b) 在 Shuffle Read 过程中会产生很多读取磁盘小文件的对象。 c) 在 JVM 堆内存中对象过多会造成频繁的gc(垃圾回收),gc还无法解决运行所需要的内存的话,就会 OOM(内存溢出)。 d) 在数据传输过程中会有频繁的网络通信,频繁的网络通信使得出现通信故障的可能性大大增加,一旦网络通信出现了故障会导致 shuffle file cannot find,由于这个错误导致的 task 失败,TaskScheduler 不负责重试,由 DAGScheduler 负责重试 Stage。 2、合并机制 合并机制示意图 总结 产生磁盘小文件的个数:C(core 的个数)* R(reduce task的个数)。 这种方式使得在同一个核里,所有的 map task 可以共享 buffer,也大大减少 buffer 个数,从而减少维护 buffer 对象所需的内存。 SortShuffle 1、普通机制 普通机制示意图 执行流程 a) map task 的计算结果会写入到一个内存数据结构里面,内存数据结构默认是 5M b) 在 shuffle 的时候会有一个定时器,不定期的去估算这个内存结构的大小,当内存结构中的数据超过 5M 时,比如现在内存结构中的数据为 5.01M,那么他会申请 5.01*2-5=5.02M 内存给内存数据结构。 c) 如果申请成功不会进行溢写,如果申请失败,这时候会溢写到磁盘。 d) 在溢写之前内存结构中的数据会进行排序分区 e) 然后开始溢写磁盘,写磁盘是以batch的形式去写,一个batch是 1 万条数据, f) map task 执行完成后,会将这些磁盘小文件合并成一个大的磁盘文件,同时生成一个索引文件。 g) reduce task 去 map 端拉取数据的时候,首先解析索引文件,根据索引文件再去拉取对应的数据。 总结 产生磁盘小文件的个数: 2*M(map task 的个数)。 2、bypass机制 bypass 机制示意图 总结 ① bypass 运行机制的触发条件如下:shuffle reduce task 的数量小于 spark.shuffle.sort.bypassMergeThreshold 的参数值。这个值默认是 200。 ② 产生的磁盘小文件为:2*M(map task 的个数)。 Shuffle 文件寻址 1、MapOutputTracker MapOutputTracker 是Spark架构中的一个模块,是一个主从架构。管理磁盘小文件的地址。 MapOutputTrackerMaster:主对象,存在于 Driver 中。 MapOutputTrackerWorker:从对象,存在于 Excutor 中。 2、BlockManager BlockManager 块管理者,是 Spark 架构中的一个模块,也是一个主从架构。 BlockManagerMaster:主对象,存在于 Driver 中。 BlockManagerMaster 会在集群中有用到广播变量和缓存数据或者删除缓存数据的时候,通知 BlockManagerSlave 传输或者删除数据。 BlockManagerWorker:从对象,存在于 Excutor 中。 BlockManagerWorker 会与 BlockManagerWorker 之间通信。 无论在 Driver 端的 BlockManager 还是在 Excutor 端的 BlockManager 都含有四个对象: ① DiskStore:负责磁盘的管理。 ② MemoryStore:负责内存的管理。 ③ ConnectionManager:负责连接其他的 BlockManagerWorker。 ④ BlockTransferService:负责数据的传输。 3、Shuffle 文件寻址图 4、Shuffle 文件寻址流程 a) 当 map task 执行完成后,会将 task 的执行情况和磁盘小文件的地址封装到 MpStatus 对象中,通过MapOutputTrackerWorker 对象向 Driver 中的 MapOutputTrackerMaster 汇报。 b) 在所有的 map task 执行完毕后,Driver 中就掌握了所有的磁盘小文件的地址。 c) 在 reduce task 执行之前,先在本地的 MapOutPutTrackerWorker 对象中获取,如果没有就去 Driver 端的MapOutputTrackerMaster 获取磁盘小文件的地址。 d) 获取到磁盘小文件的地址后,会通过 BlockManager 中的 ConnectionManager 连接数据所在节点上的 ConnectionManager,然后通过 BlockTransferService 进行数据的传输。 e) BlockTransferService 默认启动5个task 去节点拉取数据。默认情况下,5 个 task 一次拉取数据量不能超过 48M。拉取过来的数据放在Executor端的shuffle聚合内存中(spark.shuffle.memeoryFraction 0.2), 如果5个task一次拉取的数据放不下 shuffle 内存中会有 OOM。 5、reduce task 中 OOM 如何处理? 减少每次拉取的数据量。 提高 shuffle 聚合的内存比例。 提高 Excutor 的总内存。 Spark 内存管理 Spark 执行应用程序时,Spark 集群会启动 Driver 和 Executor 两种 JVM 进程,Driver 负责创建 SparkContext 上下文,提交任务,task 的分发等。Executor 负责 task 的计算任务,并将结果返回给 Driver。同时需要为需要持久化的 RDD 提供储存。Driver 端的内存管理比较简单,这里所说的 Spark 内存管理针对 Executor 端的内存管理。 Excutor 负责的是算子内部的逻辑处理。 Spark 内存管理分为静态内存管理和统一内存管理。Spark1.6 之前使用的是静态内存管理,Spark1.6 开始引入了统一内存管理。 静态内存管理: 存储内存、执行内存和其他内存的大小在 Spark 应用程序运行期间均为固定的,但用户可以应用程序启动前进行配置。 统一内存管理: 与静态内存管理的区别在于储存内存和执行内存共享同一块空间,可以互相借用对方的空间。 Spark1.6以上版本默认使用的是统一内存管理,当然也可以通过参数spark.memory.useLegacyMode 设置为true(默认为false)使用静态内存管理。 1、静态内存管理分布图 2、统一内存管理分布图 Shuffle 调优 SparkShuffle 调优配置项如何使用? 1、在代码中,不推荐使用,硬编码 // 例如new SparkConf().set(“spark.shuffle.file.buffer”,”64”) 2、在提交 spark 任务的时候,推荐使用。 spark-submit --conf spark.shuffle.file.buffer=64 –conf ... 3、在 conf 下的 spark-default.conf 配置文件中,不推荐,因为是写死后所有应用程序都要用。 Shuffle 调优具体策略 关于 Shuffle 调优的具体策略请见:Shuffle调优策略","tags":[{"name":"SparkShuffle","slug":"SparkShuffle","permalink":"http://www.seanxia.cn/tags/SparkShuffle/"},{"name":"广播变量","slug":"广播变量","permalink":"http://www.seanxia.cn/tags/%E5%B9%BF%E6%92%AD%E5%8F%98%E9%87%8F/"}]},{"title":"Spark计算框架(三)","date":"2018-01-07T16:00:00.000Z","path":"大数据/9326ece7.html","text":"关于 Spark 算子的应用案例有很多,这里介绍一些一些不常见但是很有用的算子,以及几个小案例的源码介绍。 补充算子 transformations mapPartitionWithIndex 类似于 mapPartitions,除此之外还会携带分区的索引值。 repartition 增加或减少分区。会产生shuffle。 coalesce coalesce 常用来减少分区,第二个参数是减少分区的过程中是否产生 shuffle。 true 为产生 shuffle,false 不产生 shuffle。默认是 false。 如果 coalesce 设置的分区数比原来的 RDD 的分区数还多的话,第二个参数设置为 false 不会起作用,如果设置成 true,效果和 repartition 一样。即 repartition(numPartitions) = coalesce(numPartitions,true) groupBykey 作用在 K,V 格式的 RDD 上。根据 Key 进行分组。作用在(K,V),返回(K,Iterable )。 zip 将两个 RDD 中的元素(KV 格式/非 KV 格式)变成一个 KV 格式的 RDD。、 两个 RDD 的个数必须相同。 zipWithIndex 该函数将 RDD 中的元素和这个元素在 RDD 中的索引号(从 0 开始)组合成(K,V)对。 Action countBykey 作用到 K,V 格式的 RDD 上,根据 Key 计数相同 Key 的数据集元素。 countByValue 根据数据集每个元素相同的内容来计数。返回相同内容的元素对应的条数。 reduce 根据聚合逻辑聚合数据集中的每个元素。 案例分析 这里举例 PV&PU、二次排序、分组取topN。更多详细源码及测试文件请见 个人github PV&PU PV 是网站分析的一个术语,用以衡量网站用户访问的网页的数量。对于广告主,PV 值可预期它可以带来多少广告收入。一般来说,PV 与来访者的数量成正比,但是 PV 并不直接决定页面的真实来访者数量,如同一个来访 者通过不断的刷新页面,也可以制造出非常高的 PV。 1、什么是 PV 值 PV (page view )即页面浏览量或点击量,是衡量一个网站或网页用户访问量。具体的说,PV 值就是所有访问者在 24 小时(0 点到 24 点)内看了某个网站多少个页面或某个网页多少次。PV 是指页面刷新的次数,每一次页面刷新,就算做一次 PV 流量。 2、什么是 UV 值 UV (unique visitor )即独立访客数,指访问某个站点或点击某个网页的不同 IP 地址的人数。在同一天内,UV 只记录第一次进入网站的具有独立 IP 的访问者,在同一天内再次访问该网站则不计数。UV 提供了一定时间内不同观众数量的统计指标,而没有反应出网站的全面活动。 3、代码 // PV,页面浏览量public class PV { public static void main(String[] args) { SparkConf conf = new SparkConf(); conf.setMaster("local").setAppName("pv"); JavaSparkContext context = new JavaSparkContext(conf); JavaRDD<String> lineRDD = context.textFile("./data/pvuvdata"); lineRDD.mapToPair(new PairFunction<String, String, Integer>() { @Override public Tuple2<String, Integer> call(String s) throws Exception { return new Tuple2<>(s.split("\\t")[5],1); } }).reduceByKey(new Function2<Integer, Integer, Integer>() { @Override public Integer call(Integer v1, Integer v2) throws Exception { return v1 + v2; } }).mapToPair(new PairFunction<Tuple2<String,Integer>, Integer, String>() { @Override public Tuple2<Integer, String> call(Tuple2<String, Integer> tuple2) throws Exception { return new Tuple2<>(tuple2._2,tuple2._1); } }).sortByKey(false).mapToPair( new PairFunction<Tuple2<Integer,String>, String, Integer>() { @Override public Tuple2<String, Integer> call(Tuple2<Integer, String> tuple2) throws Exception { return new Tuple2<>(tuple2._2,tuple2._1); } }).foreach(new VoidFunction<Tuple2<String, Integer>>() { @Override public void call(Tuple2<String, Integer> tuple2) throws Exception { System.out.println(tuple2); } }); context.stop(); }}// UV,访客数public class UV { public static void main(String[] args) { SparkConf conf = new SparkConf(); conf.setMaster("local").setAppName("pv"); JavaSparkContext context = new JavaSparkContext(conf); JavaRDD<String> lineRDD = context.textFile("./data/pvuvdata"); JavaPairRDD<String, String> urlipRDD = lineRDD.mapToPair( new PairFunction<String, String, String>() { @Override public Tuple2<String, String> call(String s) throws Exception { return new Tuple2<>(s.split("\\t")[5], s.split("\\t")[0]); } }); JavaPairRDD<String, Iterable<String>> groupByKeyRDD = urlipRDD.groupByKey(); groupByKeyRDD.foreach(new VoidFunction<Tuple2<String, Iterable<String>>>() { @Override public void call(Tuple2<String, Iterable<String>> tuple2) throws Exception { HashSet<Object> set = new HashSet<>(); Iterator<String> iterator = tuple2._2.iterator(); while(iterator.hasNext()){ set.add(iterator.next()); } System.out.println("url : " + tuple2._1 + " value: " + set.size()); } }); context.stop(); }} 二次排序 public class SecondSortApp { public static void main(String[] args) { SparkConf sparkConf = new SparkConf(); sparkConf.setMaster("local").setAppName("SecondarySortTest"); final JavaSparkContext sc = new JavaSparkContext(sparkConf); JavaRDD<String> secondRDD = sc.textFile("data/secondSort.txt"); JavaPairRDD<SecondSortKey, String> pairSecondRDD = secondRDD.mapToPair( new PairFunction<String, SecondSortKey, String>() { private static final long serialVersionUID = 1L; @Override public Tuple2<SecondSortKey, String> call(String line) throws Exception { String[] splited = line.split(" "); int first = Integer.valueOf(splited[0]); int second = Integer.valueOf(splited[1]); // 这里自己写一个排序的类:SecondSorkey,使key实现Comparable接口 SecondSortKey secondSortKey = new SecondSortKey(first, second); return new Tuple2<SecondSortKey, String>(secondSortKey, line); } }); pairSecondRDD.sortByKey(false).foreach( new VoidFunction<Tuple2<SecondSortKey, String>>() { private static final long serialVersionUID = 1L; @Override public void call(Tuple2<SecondSortKey, String> tuple) throws Exception { System.out.println(tuple + "------" + tuple._2); } }); }}// 类SecondSorkeypublic class SecondSortKey implements Serializable , Comparable<SecondSortKey>{ private static final long serialVersionUID = 1L; private int first; private int second; public int getFirst() { return first; } public void setFirst(int first) { this.first = first; } public int getSecond() { return second; } public void setSecond(int second) { this.second = second; } public SecondSortKey(int first, int second) { super(); this.first = first; this.second = second; } @Override public int compareTo(SecondSortKey o1) { if (getFirst() - o1.getFirst() == 0) { return o1.getSecond() - getSecond(); } else { return o1.getFirst() - getFirst(); } }} 分组取topN public class TopN { public static void main(String[] args) { SparkConf conf = new SparkConf(); conf.setMaster("local").setAppName("TopOps"); JavaSparkContext sc = new JavaSparkContext(conf); //hdfs://seanxia/wc.txt JavaRDD<String> linesRDD = sc.textFile("./data/scores.txt",5); JavaPairRDD<String, Integer> pairRDD = linesRDD.mapToPair( new PairFunction<String, String, Integer>() { private static final long serialVersionUID = 1L; @Override public Tuple2<String, Integer> call(String str) throws Exception { String[] splited = str.split(" "); String clazzName = splited[0]; Integer score = Integer.valueOf(splited[1]); return new Tuple2<String, Integer>(clazzName, score); } }); JavaPairRDD<String, Iterable<Integer>> groupByKeyRDD = pairRDD.groupByKey(); groupByKeyRDD.foreach( new VoidFunction<Tuple2<String, Iterable<Integer>>>() { private static final long serialVersionUID = 1L; @Override public void call(Tuple2<String, Iterable<Integer>> tuple) throws Exception { String clazzName = tuple._1; Iterator<Integer> iterator = tuple._2.iterator(); System.out.println(tuple); // 优化:定义一个长度为3的数组,每次只取前3个最大的,无需进行全排序 Integer[] top3 = new Integer[3]; while (iterator.hasNext()) { Integer score = iterator.next(); for (int i = 0; i < top3.length; i++) { if (top3[i] == null) { top3[i] = score; break; } else if (score > top3[i]) { for (int j = 2; j > i; j--) { top3[j] = top3[j - 1]; } top3[i] = score; break; } } } System.out.println("class Name:" + clazzName); for (Integer sscore : top3) { System.out.println(sscore); } } }); }} Spark-Submit 提交参数 Options: –master MASTER_URL, 可 以 是 spark://host:port,mesos://host:port,yarn,yarn-cluster,yarn-client,local –deploy-mode DEPLOY_MODE, Driver 程序运行的地方,client 或者 cluster,默认是client。 –class CLASS_NAME,主类名称,含包名 –jars 逗号分隔的本地 JARS,Driver 和 executor 依赖的第三方 jar 包 –files 用逗号隔开的文件列表,会放置在每个 executor 工作目录中 –conf spark的配置属性 –driver-memory Driver 程序使用内存大小(例如:1000M,5G),默认 1024M –executor-memory 每个 executor 内存大小(如:1000M,2G),默认 1G Spark standalone with cluster deploy mode only: –driver-cores Driver 程序的使用 core 个数(默认为 1),仅限于 Spark standalone 模式 Spark standalone or Mesos with cluster deploy mode only: –supervise 失败后是否重启 Driver,仅限于 Spark alone 或者 Mesos 模式 Spark standalone and Mesos only: –total-executor-cores executor 使用的总核数,仅限于 SparkStandalone、Spark on Mesos 模式 Spark standalone and YARN only: –executor-cores 每个 executor 使用的 core 数,Spark on Yarn 默认为 1,standalone 默认为 worker 上所有可用的 core。 YARN-only: –driver-cores driver 使用的 core,仅在 cluster 模式下,默认为 1。 –queue QUEUE_NAME 指定资源队列的名称,默认:default。 –num-executors 一共启动的 executor 数量,默认是 2 个。 SparkShell 概念: SparkShell 是 Spark 自带的一个快速原型开发工具,也可以说是 Spark 的 scala REPL(Read-Eval-Print-Loop),即交互式 shell。支持使用 scala 语言来进行 Spark 的交互式编程。 使用: 1、启动 Standalone 集群(sbin目录下):./start-all.sh 2、在客户端上启动 spark-shell(bin目录下): ./spark-shell --master spark://sean01:7077 3、启动 HDFS,创建目录 spark/test,上传文件 wc.txt: #启动 hdfs 集群:start-all.sh#创建目录:hdfs dfs -mkdir -p /spark/test#上传 wc.txthdfs dfs -put /root/test/wc.txt /spark/test 在 root/test 目录下随机准备一份数据 wc.txt 4、测试运行 WordCount sc.textFile("hdfs://Xss/spark/test/wc.txt").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).foreach(println) 5、通过 Web 端查看 SparkUI SparkUI 界面介绍 可以指定提交 Application 的名称。 添加参数: --name 自定义名称 ./spark-shell --master spark://sean01:7077 --name myapp 注意:无论名称还是配置都是以代码写定的为准。 优先级为:代码 > shell > conf 1、配置临时 historyServer 临时配置,对本次提交的应用程序起作用。 ./spark-shell --master spark://sean01:7077--name WC--conf spark.eventLog.enabled=true--conf spark.eventLog.dir=hdfs://Xss/spark/test 作用:停止程序,在 Web Ui 中 Completed Applications 对应的 ApplicationID 中能查看 history。 2、spark-default.conf 配置文件中配置 HistoryServer,对所有提交的 Application 都起作用 在客户端节点,进入 …/spark-1.6.0/conf/spark-defaults.conf 最后加入(3台都配): # 开启记录事件日志的功能spark.eventLog.enabled true# 设置事件日志存储的目录spark.eventLog.dir hdfs://Xss/spark/test# 设置 HistoryServer 加载事件日志的位置spark.history.fs.logDirectory hdfs://Xss/spark/test# 日志优化选项, 压缩日志spark.eventLog.compress true 重启 spark 集群 #sbin目录下:./stop-all.sh./start-all.sh 查看 Web 端 启动 HistoryServer: #sbin目录下:./start-history-server.sh 访问 HistoryServer: sean01:18080,之前所有提交的应用程序运行状况都会被记录。包括之前的都有。 Master HA 1、Master 的高可用原理 Standalone 集群只有一个 Master,如果 Master 挂了就无法提交应用程序,需要给 Master 进行高可用配置,Master 的高可用可以使用 fileSystem(文件系统)和 zookeeper(分布式协调服务)。 fileSystem fileSystem 只有存储功能,可以存储 Master 的元数据信息,用 fileSystem 搭建的 Master 高可用,在 Master 失败时,需要我们手动启动另外的备用 Master,这种方式不推荐使用。 zookeeper zookeeper 有选举和存储功能,可以存储 Master 的元素据信息,使用 zookeeper 搭建的 Master 高可用,当 Master 挂掉时,备用的 Master会自动切换,推荐使用这种方式搭建 Master 的 HA。 2、Master 高可用搭建 在 Spark Master 节点上配置主 Master,配置 spark-env.sh export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=ZOOKEEPER-Dspark.deploy.zookeeper.url=sean01:2181,sean02:2181,sean03:2181-Dspark.deploy.zookeeper.dir=/sparkmaster0821" 发送到其他 worker 节点上 找一台节点(非主 Master 节点)配置备用 Master,修改 spark-env.sh 配置节点上的 MasterIP 启动集群之前启动 zookeeper 集群 启动 spark Standalone 集群,在备用节点 sean02 启动备用 Master #sbin目录下./start-master.sh 打开主 Master 和备用 Master WebUI 页面,观察状态。 3、注意点 主备切换过程中不能提交 Application。 主备切换过程中不影响已经在集群中运行的 Application。因为 Spark 是粗粒度资源调度。 4、测试验证 提交 SparkPi 程序,kill 主 Master 观察现象 #bin目录下./spark-submit--master spark://sean01:7077,sean02:7077--class org.apache.spark.examples.SparkPi../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100 kill 掉主节点 sean01,查看 Web 端备用节点 sean02 是否进行了切换 成功切换成主节点!","tags":[{"name":"spark","slug":"spark","permalink":"http://www.seanxia.cn/tags/spark/"},{"name":"PV","slug":"PV","permalink":"http://www.seanxia.cn/tags/PV/"},{"name":"PU","slug":"PU","permalink":"http://www.seanxia.cn/tags/PU/"}]},{"title":"Spark计算框架(二)","date":"2017-12-18T16:00:00.000Z","path":"大数据/de384fb5.html","text":"关于Spark的任务提交方式,总的分为 Client 提交和 Cluster 提交两种。这里以 Standalone 和 Yarn 为例详细阐述他们在 Spark 中提交任务的流程。 Spark任务提交方式 注意:使用Standalone-Cluster方式提交任务时,必须保证所有的节点上(放在Spark/lib/ 下)都有这个jar包,示例中使用的jar包系统默认都有。但使用Yarn方式时只需提交的节点存在jar包即可,因为在Yarn中会共享这个jar包!!! Standalone方式 1、Standalone-Client 提交 提交命令 ./spark-submit --master spark://sean01:7077 --class org.apache.spark.examples.SparkPi ../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100 或者 ./spark-submit --master spark://sean01:7077 --deploy-mode client --class org.apache.spark.examples.SparkPi ../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100## 默认就是 --deploy-mode client 执行原理图解 执行流程 client 模式提交任务后,并会在客户端启动 Driver 进程。 Driver 会向 Master 申请用于启动 Application 的资源。 资源申请成功,Driver 端将 task 发送到 worker 端的 executor 执行。 worker 将 task 执行结果返回到 Driver 端。 总结: client 模式适用于测试调试程序。Driver 进程是在客户端启动的,这里的客户端就是指提交应用程序的当前节点。在 Driver 端可以看到 task 执行的情况。生产环境下不能使用 client 模式,是因为:假设要提交 100 个 application 到集群运行,Driver 每次都会在 client 端启动,那么就会导致客户端 100 次网卡流量暴增的问题。 2、Standalone-Cluster 提交 提交命令 ./spark-submit --master spark://sean01:7077 --deploy-mode cluster --class org.apache.spark.examples.SparkPi ../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100 注意: Standalone-cluster 提交方式,应用程序使用的所有 jar 包和文件,必须保证所有的 worker 节点都要有,因为此种方式,spark 不会自动上传包。 解决方式: 将所有的依赖包和文件打到同一个包中,然后放在 hdfs 上。 将所有的依赖包和文件各放一份在 worker 节点上。 执行原理图解 执行流程 cluster 模式提交应用程序后,会向 Master 请求启动 Driver. Master 接受请求,随机在集群一台节点启动 Driver 进程。 Driver 启动后为当前的应用程序申请资源。 Driver 端发送 task 到 worker 节点的 executor 上执行。 worker 将执行情况和执行结果返回给 Driver 端。 总结: Driver 进程是在集群某一台 Worker 上启动的,在客户端是无法查看 task 的执行情况的。假设要提交 100 个 application 到集群运行,每次 Driver 会随机在集群中某一台 Worker 上启动,那么这 100 次网卡流量暴增的问题就散布在集群上。 总结 Standalone 两种方式提交任务,Driver 与集群的通信包括: Driver 负责应用程序资源的申请 任务的分发。 结果的回收。 监控 task 执行情况。 Yarn方式 1、yarn-client 提交 提交命令 ./spark-submit --master yarn --class org.apache.spark.examples.SparkPi ../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100 或者 ./spark-submit --master yarn-client --class org.apache.spark.examples.SparkPi ../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100 或者 ./spark-submit --master yarn --deploy-mode client --class org.apache.spark.examples.SparkPi ../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100 执行原理图解 执行流程 客户端提交一个 Application,并启动一个 Driver 进程。 应用程序启动后会向 RS(ResourceManager)发送请求,启动AM(ApplicationMaster)的资源。 RS 收到请求,随机选择一台 NM(NodeManager)启动 AM。这里的 NM 相当于 Standalone 中的 Worker 节点。 AM启动后,会向RS请求一批container资源,用于启动Executor. RS 会找到一批 NM 返回给 AM,用于启动 Executor。 AM 会向 NM 发送命令启动 Executor。 Executor 启动后,会反向注册给 Driver,Driver 发送 task 到 Executor,并回收执行情况和结果。 总结 Yarn-client 模式同样是适用于测试,因为 Driver 运行在本地,Driver会与 yarn 集群中的 Executor 进行大量的通信,会造成客户机网卡流量的大量增加. ApplicationMaster 的作用: 为当前的 Application 申请资源 给 NodeManager 发送消息启动 Executor。 注意:ApplicationMaster 有 launchExecutor 和申请资源的功能,并没有作业调度的功能。 2、yarn-cluster 提交 提交任务 ./spark-submit --master yarn --deploy-mode cluster --class org.apache.spark.examples.SparkPi ../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100 或者 ./spark-submit --master yarn-cluster --class org.apache.spark.examples.SparkPi ../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100 执行原理图 执行流程 客户机提交 Application 应用程序,发送请求到 RS(ResourceManager),请求启动 AM(ApplicationMaster),Driver在AM中启动运行。 RS 收到请求后随机在一台 NM(NodeManager)上启动 AM(相当于 Driver 端)。 AM 启动,AM 发送请求到 RS,请求一批 container 用于启动 Excutor。 RS 返回一批 NM 节点给 AM。 AM 连接到 NM,发送请求到 NM 启动 Excutor。 Excutor 反向注册到 AM 所在的节点的 Driver。Driver 发送 task 到 Excutor并回收结果。 总结 Yarn-Cluster 主要用于生产环境中,因为 Driver 运行在 Yarn 集群中某一台 nodeManager 中,每次提交任务的 Driver 所在的机器都是随机的,不会产生某一台机器网卡流量激增的现象,缺点是任务提交后不能看到日志。只能通过 yarn 查看日志。 ApplicationMaster 的作用: 为当前的 Application 申请资源 给 NodeManger 发送消息启动 Excutor。 任务调度。 补充部分算子 transformation join、leftOuterJoin、rightOuterJoin、fullOuterJoin 作用在 K,V 格式的 RDD 上。根据 K 进行连接,对(K,V)join(K,W)返回(K,(V,W)) join 后的分区数与父 RDD 分区数多的那一个相同。 union 合并两个数据集。两个数据集的类型要一致。 返回新的 RDD 的分区数是合并 RDD 分区数的总和。 intersection 取两个数据集的交集 subtract 取两个数据集的差集 mapPartition 与 map 类似,遍历的单位是每个 partition 上的数据。 distinct(map+reduceByKey+map) 对相同的值进行去重 cogroup 当调用类型(K,V)和(K,W)的数据上时,返回一个数据集(K,(Iterable < V >,Iterable < W >)) action foreachPartition 遍历的数据是每个 partition 的数据。 术语解释 窄依赖和宽依赖 RDD 之间有一系列的依赖关系,依赖关系又分为窄依赖和宽依赖。 窄依赖 父 RDD 和子 RDD partition 之间的关系是一对一或多对一。不会有 shuffle 的产生。 宽依赖 父RDD与子RDD partition之间的关系是一对多。会有shuffle的产生。 宽窄依赖图理解 Stage Spark 任务会根据 RDD 之间的依赖关系,形成一个 DAG 有向无环图,DAG 会提交给 DAGScheduler,DAGScheduler 会把 DAG 划分成相互依赖的多个 stage,划分 stage 的依据就是 RDD 之间的宽窄依赖。遇到宽依赖就划分 stage,每个 stage 包含一个或多个 task 任务。然后将这些 task 以 taskSet 的形式提交给 TaskScheduler 运行。 stage 是由一组并行的 task 组成。 stage 切割规则 从后往前追溯,遇到宽依赖就切割 stage。 计算模式 pipeline 管道计算模式,pipeline 只是一种计算思想,模式。 1、数据一直在管道里面什么时候数据会落地? 对 RDD 进行持久化(cache,persist,checkPoint)。 shuffle write 的时候。 2、Stage 的 的 task 并行度是由 stage 的最后一个 RDD 的分区数来决定的。 3、如何改变 RDD 的分区数? 例如:textFile(path, numPartitons),reduceByKey(XXX, 3),GroupByKey(4) 测试验证 pipeline 计算模式 val conf = new SparkConf()conf.setMaster("local").setAppName("pipeline");val sc = new SparkContext(conf)val rdd = sc.parallelize(Array(1,2,3,4))val rdd1 = rdd.map { x => {println("map--------"+x)x}}val rdd2 = rdd1.filter { x => {println("fliter********"+x)true} }rdd2.collect()sc.stop() Spark资源划分和任务调度 Spark 资源划分和任务调度的流程: 1、启动集群后,Worker 节点会向 Master 节点汇报资源情况,Master 掌握了集群资源情况。 2、当 Spark 提交一个 Application 后,根据 RDD 之间的依赖关系将 Application 形成一个 DAG 有向无环图。 3、任务提交后,Spark 会在Driver 端创建两个对象:DAGScheduler 和 TaskScheduler,DAGScheduler是任务调度的高层调度器,是一个对象。DAGScheduler 的主要作用就是将DAG 根据 RDD 之间的宽窄依赖关系划分为一个个的 Stage,然后将这些 Stage 以 TaskSet 的形式提交给 TaskScheduler(TaskScheduler 是任务调度的低层调度器,这里 TaskSet 其实就是一个集合,里面封装的就是一个个的 task 任务,也就是 stage 中的并行度 task 任务),TaskScheduler 会遍历 TaskSet 集合,拿到每个 task 后会将 task 发送到计算节点 Executor 中去执行(其实就是发送到 Executor 中的线程池 ThreadPool 去执行)。 4、task 在 Executor 线程池中的运行情况会向 TaskScheduler 反馈,当 task 执行失败时,则由 TaskScheduler 负责重试,将 task 重新发送给 Executor 去执行,默认重试 3 次。 5、如果重试 3 次依然失败,那么这个 task 所在的 stage 就失败了。stage 失败了则由 DAGScheduler 来负责重试,重新发送 TaskSet 到TaskSchdeuler,默认重试 4 次。如果重试 4 次以后依然失败,那么这个 job 就失败了。job 失败了,Application 就失败了。 6、TaskScheduler 不仅能重试失败的 task,还会重试 straggling(落后,缓慢)task(也就是执行速度比其他 task 慢太多的 task)。如果有运行缓慢的 task那么 TaskScheduler 会启动一个新的 task 来与这个运行缓慢的 task 执行相同的处理逻辑。两个 task 哪个先执行完,就以哪个 task 的执行结果为准。这就是 Spark 的推测执行机制。在 Spark 中推测执行默认是关闭的。推测执行可以通过 spark.speculation 属性来配置。 注意: 对于 ETL 类型要入数据库的业务要关闭推测执行机制,这样就不会有重复的数据入库。 如果遇到数据倾斜的情况,开启推测执行则有可能导致一直会有 task 重新启动处理相同的逻辑,任务可能一直处于处理不完的状态。 图解 Spark 资源调度和任务调度的流程 粗粒度资源申请和细粒度资源申请 粗粒度资源申请(Spark) 在 Application 执行之前,将所有的资源申请完毕,当资源申请成功后,才会进行任务的调度,当所有的 task 执行完成后,才会释放这部分资源。 优点:每个 task 不需要在执行前自己去申请资源,直接使用就可以了。这样 task 启动就快了,task 执行快了,stage 执行就快了,job 就快了,application 执行就快了。 缺点:直到最后一个 task 执行完成才会释放资源,集群的资源无法充分利用。 细粒度资源申请 Application 执行之前不需要先去申请资源,而是直接执行,让 job中的每个 task 在执行前自己去申请资源,task 执行完成就释放资源。 优点:集群的资源可以充分利用。 缺点:task 自己去申请资源,task 启动变慢,Application 的运行就相应的变慢了。 如何设置粗细粒度? 通过设置 JVM 插槽个数来调节 task 使用的资源。 set mapred.job.reuse.jvm.num.tasks=n;(n为 task 插槽个数)","tags":[{"name":"spark","slug":"spark","permalink":"http://www.seanxia.cn/tags/spark/"},{"name":"任务提交","slug":"任务提交","permalink":"http://www.seanxia.cn/tags/%E4%BB%BB%E5%8A%A1%E6%8F%90%E4%BA%A4/"}]},{"title":"Spark计算框架(一)","date":"2017-12-15T16:00:00.000Z","path":"大数据/53ca074f.html","text":"Apache Spark 是专为大规模数据处理而设计的快速通用的计算引擎。 Spark是UC Berkeley AMP lab (加州大学伯克利分校的AMP实验室)所开源的类Hadoop MapReduce的通用并行框架。 Spark介绍 Spark 基于 MapReduce 算法实现的分布式计算,拥有Hadoop MapReduce 所具有的优点;但不同于 MapReduce 的是 Job中间输出和结果可以保存在内存中,从而不再需要读写 HDFS,因此 Spark能更好地适用于数据挖掘与机器学习等需要迭代的 MapReduce 的算法。Spark 是 Scala 编写,方便快速编程。 spark相比MapReduce重要的点: spark的中间结果依然保存在内存中。 Apache Spark使用最先进的DAG(有向无环图)调度程序。 这两个特点使得 spark 非常适用于迭代计算,在迭代计算中比MR快100倍。 Spark的四大特性 1、高效性 在迭代计算中运行速度提高100倍。 Apache Spark使用最先进的DAG调度程序,查询优化程序和物理执行引擎,实现批量和流式数据的高性能。 2、易用性 Spark支持Java、Python和Scala的API,还支持超过80种高级算法,使用户可以快速构建不同的应用。而且Spark支持交互式的Python和Scala的shell,可以非常方便地在这些shell中使用Spark集群来验证解决问题的方法。 3、通用性 Spark提供了统一的解决方案。Spark可以用于批处理、交互式查询(Spark SQL)、实时流处理(Spark Streaming)、机器学习(Spark MLlib)和图计算(GraphX)。这些不同类型的处理都可以在同一个应用中无缝使用。Spark统一的解决方案非常具有吸引力,毕竟任何公司都想用统一的平台去处理遇到的问题,减少开发和维护的人力成本和部署平台的物力成本。 4、兼容性 Spark可以非常方便地与其他的开源产品进行融合。比如,Spark可以使用Hadoop的YARN和Apache Mesos作为它的资源管理和调度器,器,并且可以处理所有Hadoop支持的数据,包括HDFS、HBase和Cassandra等。这对于已经部署Hadoop集群的用户特别重要,因为不需要做任何数据迁移就可以使用Spark的强大处理能力。Spark也可以不依赖于第三方的资源管理和调度器,它实现了Standalone作为其内置的资源管理和调度框架,这样进一步降低了Spark的使用门槛,使得所有人都可以非常容易地部署和使用Spark。此外,Spark还提供了在EC2上部署Standalone的Spark集群的工具。 Spark运行模式 Local(本地) 多用于本地测试,如在 eclipse,idea 中写程序测试等。 standalone(spark自带) Spark自己可以给自己分配资源(master,worker)。 Yarn Spark可以运行在yarn上面。 注意:要基于 Yarn 来进行资源调度,必须实现AppalicationMaster 接口,Spark 实现了这个接口,所以可以基于 Yarn。 Mesos Spark可以运行在Mesos里面(Mesos 类似于yarn的一个资源调度框架)。 Spark的组成 Spark组成(BDAS):全称伯克利数据分析栈,通过大规模集成算法、机器、人之间展现大数据应用的一个平台。也是处理大数据、云计算、通信的技术解决方案。 它的主要组件有: SparkCore:将分布式数据抽象为弹性分布式数据集(RDD),实现了应用任务调度、RPC、序列化和压缩,并为运行在其上的上层组件提供API。 SparkSQL:Spark Sql 是Spark来操作结构化数据的程序包,可以让我使用SQL语句的方式来查询数据,Spark支持 多种数据源,包含Hive表,parquest以及JSON等内容。 SparkStreaming: 是Spark提供的实时数据进行流式计算的组件。 MLlib:提供常用机器学习算法的实现库。 GraphX:提供一个分布式图计算框架,能高效进行图计算。 BlinkDB:用于在海量数据上进行交互式SQL的近似查询引擎。 Tachyon:以内存为中心高容错的的分布式文件系统。 应用场景 Yahoo将Spark用在Audience Expansion中的应用,进行点击预测和即席查询等 淘宝技术团队使用了Spark来解决多次迭代的机器学习算法、高计算复杂度的算法等。应用于内容推荐、社区发现等 腾讯大数据精准推荐借助Spark快速迭代的优势,实现了在“数据实时采集、算法实时训练、系统实时预测”全流程实时并行高维算法,最终成功应用于广点通pCTR投放系统上。 优酷土豆将Spark应用于视频推荐(图计算)、广告业务,主要实现机器学习、图计算等迭代计算。 SparkCore RDD 1、概念 RDD(Resilient Distributed Dateset),弹性分布式数据集,是Spark中最基本的数据抽象。 2、RDD 的五大特性 RDD 是由一系列的 partition 组成的。 函数是作用在每一个 partition(split)上的。 RDD 之间有一系列的依赖关系。 分区器是作用在 K,V 格式的 RDD 上。 RDD 提供一系列最佳的计算位置。 3、RDD 理解图 注意: 1、textFile 方法底层封装的是读取 MR 读取文件的方式,读取文件之前先 split,默认 split 大小是一个 block 大小。 2、RDD 实际上不存储数据,这里方便理解,暂时理解为存储数据。 3、什么是 K,V 格式的 RDD? 如果 RDD 里面存储的数据都是二元组对象,那么这个 RDD 我们 就叫做 K,V 格式的 RDD。 4、哪里体现 RDD 的弹性(容错)? partition 数量,大小没有限制,体现了 RDD 的弹性。 RDD 之间依赖关系,可以基于上一个 RDD 重新计算出 RDD,体现了容错。 5、哪里体现 RDD 的分布式? RDD 是由 Partition 组成,partition 是分布在不同节点上的。 6、RDD 提供计算最佳位置,体现了数据本地化。体现了大数据中 “计算移动数据不移动” 的理念。 Spark任务执行原理 以上图中有四个机器节点,Driver 和 Worker 是启动在节点上的进程,运行在 JVM 中的进程。 Driver 与集群节点之间有频繁的通信。 Driver 负责任务(tasks)的分发和结果的回收。任务的调度。如果 task 的计算结果非常大就不要回收了。会造成 oom。 Worker 是 Standalone 资源调度框架里面资源管理的从节点。也是 JVM 进程。 Master 是 Standalone 资源调度框架里面资源管理的主节点。也是 JVM 进程。 Spark代码流程 1、创建 SparkConf 对象 可以设置 Application name。 可以设置运行模式及资源需求。 2、创建 SparkContext 对象 3、基于 Spark 的上下文创建一个 RDD,对 RDD 进行处理。 4、应用程序中要有 Action 类算子来触发 Transformation 类算子执行。 5、关闭 Spark 上下文对象 SparkContext。 Transformations 转换算子 1、概念 Transformations 类算子是一类算子(函数)叫做转换算子,如map,flatMap,reduceByKey 等。Transformations 算子是延迟执行,也叫懒加载执行。 2、Transformation 类算子 filter 过滤符合条件的记录数,true 保留,false 过滤掉。 map 将一个 RDD 中的每个数据项,通过 map 中的函数映射变为一个新的元素。 特点:输入一条,输出一条数据。 flatMap 先 map 后 flat。与 map 类似,每个输入项可以映射为 0 到多个输出项。 sample 随机抽样算子,根据传进去的小数按比例进行又放回或者无放回的抽样。 reduceByKey 将相同的 Key 根据相应的逻辑进行处理。 sortByKey/sortBy 作用在 K,V 格式的 RDD 上,对 key 进行升序或者降序排序。 Action 触发算子 1、概念 Action 类算子也是一类算子(函数)叫做行动算子,如foreach,collect,count 等。Transformations 类算子是延迟执行,Action 类算子是触发执行。一个 application 应用程序中有几个 Action 类算子执行,就有几个 job 运行。 2、Action 类算子 count 返回数据集中的元素数。会在结果计算完成后回收到 Driver 端。 take(n) 返回一个包含数据集前 n 个元素的集合。 first first=take(1),返回数据集中的第一个元素。 foreach 循环遍历数据集中的每个元素,运行相应的逻辑。 collect 将计算结果转成集合回收到 Driver 端。 Demo01(WordCount) Scala实现 object WordCount { def main(args: Array[String]): Unit = { val conf = new SparkConf() /** * 几种运行方式: * 1.本地运行 * 2.yarn * 3.standalone * 4.mesos */ conf.setMaster("local").setAppName("wc") val context = new SparkContext(conf) val lineRDD = context.textFile("./wc.txt") val wordRDD = lineRDD.flatMap(x => {x.split(" ")}) val KVRDD = wordRDD.map(x => { println("=================") (x,1) }) val resultRDD = KVRDD.reduceByKey((x,y) => {x+y}) val sortRDD = resultRDD.sortBy(_._2,false) sortRDD.foreach(println) } } Java实现 public class WordCount { public static void main(String[] args){ SparkConf conf = new SparkConf(); conf.setMaster("local").setAppName("wc"); JavaSparkContext context = new JavaSparkContext(conf); JavaRDD<String> javaRDD = context.textFile("./wc.txt");// long count = javaRDD.count();// List<String> collect = javaRDD.collect();// List<String> take = javaRDD.take(5);// String first = javaRDD.first(); JavaRDD<String> wordRDD = javaRDD .flatMap(new FlatMapFunction<String, String>() { @Override public Iterable<String> call(String line) throws Exception { String[] split = line.split(" "); List<String> list = Arrays.asList(split); return list; } }); JavaPairRDD<String, Integer> pairRDD = wordRDD .mapToPair(new PairFunction<String, String, Integer>() { @Override public Tuple2<String, Integer> call(String word) throws Exception { return new Tuple2(word, 1); } }); JavaPairRDD<String, Integer> resultRDD = pairRDD .reduceByKey(new Function2<Integer, Integer, Integer>() { @Override public Integer call(Integer v1, Integer v2) throws Exception { return v1 + v2; } }); JavaPairRDD<Integer, String> reverseRDD = resultRDD .mapToPair(new PairFunction<Tuple2<String, Integer>, Integer, String>() { @Override public Tuple2<Integer, String> call(Tuple2<String, Integer> tuple2) throws Exception { return new Tuple2<>(tuple2._2, tuple2._1); } }); JavaPairRDD<Integer, String> sortByKey = reverseRDD.sortByKey(false); JavaPairRDD<String, Integer> result = sortByKey .mapToPair(new PairFunction<Tuple2<Integer, String>, String, Integer>() { @Override public Tuple2<String, Integer> call(Tuple2<Integer, String> tuple2) throws Exception { return new Tuple2<>(tuple2._2, tuple2._1); } }); result.foreach(new VoidFunction<Tuple2<String, Integer>>() { @Override public void call(Tuple2<String, Integer> tuple2) throws Exception { System.out.println(tuple2); } }); }} 控制算子 控制算子有三种,cache、persist、checkpoint,以上算子都可以将 RDD 持久化,持久化的单位是 partition。cache 和 persist 都是懒执行的,必须有一个 action 类算子触发执行。checkpoint 算子不仅能将 RDD 持久化到磁盘,还能切断 RDD 之间的依赖关系。 1、cache 默认将 RDD 的数据持久化到内存中。cache 是懒执行。 注意:cache() = persist()=persist(StorageLevel.Memory_Only) 测试 cache 文件: 随机生成一个数据量比较大的测试文件:Test.txt SparkConf conf = new SparkConf();conf.setMaster("local").setAppName("CacheTest");JavaSparkContext jsc = new JavaSparkContext(conf);JavaRDD<String> lines = jsc.textFile("./Test.txt");lines = lines.cache();long startTime = System.currentTimeMillis();long count = lines.count();long endTime = System.currentTimeMillis();System.out.println("共"+count+ "条数据,"+"初始化时间+cache时间+计算时间="+(endTime-startTime));long countStartTime = System.currentTimeMillis();long countrResult = lines.count();long countEndTime = System.currentTimeMillis();System.out.println("共"+countrResult+ "条数据,"+"计算时间="+ (countEndTime-countStartTime));jsc.stop(); 2、persist 可以指定持久化的级别。最常用的是 MEMORY_ONLY 和 MEMORY_AND_DISK。”_2”表示有副本数。 持久化级别如下: cache 和 和 persist 的注意事项: cache 和 persist 都是懒执行,必须有一个 action 类算子触发执行。 cache 和 persist 算子的返回值可以赋值给一个变量,在其他 job 中直接使用这个变量就是使用持久化的数据了。持久化的单位是 partition。 cache 和 persist 算子后不能立即紧跟 action 算子。 错误:rdd.cache().count() 返回的不是持久化的 RDD,而是一个数值了。 3、checkpoint checkpoint 将 RDD 持久化到磁盘,还可以切断 RDD 之间的依赖关系。 checkpoint 的执行原理: 当 RDD 的 job 执行完毕后,会从 finalRDD 从后往前回溯。 当回溯到某一个 RDD 调用了 checkpoint 方法,会对当前的 RDD 做一个标记。 Spark 框架会重新启动一个新的 job,从头开始计算到这个 RDD 的数据,将数据持久化到 HDFS 上。 优化:对 RDD 执行 checkpoint 之前,最好对这个 RDD 先执行 cache,这样新启动的 job 只需要将内存中的数据拷贝到 HDFS 上就可以,省去了重新计算这一步。 使用样例: SparkConf conf = new SparkConf();conf.setMaster("local").setAppName("checkpoint");JavaSparkContext sc = new JavaSparkContext(conf);sc.setCheckpointDir("./checkpoint");JavaRDD<Integer> parallelize = sc.parallelize(Arrays.asList(1,2,3));parallelize.checkpoint();parallelize.count();sc.stop(); Spark集群搭建 Standalone 1、下载安装包,解压 2、改名 3、进入安装包的conf目录下,修改slaves.template文件,添加从节点。保存。 4、修改 spark-env.sh JAVA_HOME:配置 java_home 路径SPARK_MASTER_IP:master 的 ipSPARK_MASTER_PORT:提交任务的端口,默认是 7077SPARK_WORKER_CORES:每个 worker 从节点能够支配的 core 的个数SPARK_WORKER_MEMORY:每个 worker 从节点能够支配的内存数 5、同步到其他节点上 6、启动集群 进入 sbin 目录下,执行当前目录下的./start-all.sh 查看各节点进程: 主节点 从节点 7、启动Spark WEB 界面 使用提交任务的节点。 注意: 8080 是 Spark WEBUI 界面的端口,7077 是 Spark 任务提交的端口。 Yarn 1、1,2,3,4,5,7 步同 standalone。 2、在客户端中配置: HADOOP_CONF_DIR=/usr/soft/hadoop-2.6.5 测试 进到spark安装包的bin目录下,里面又一个脚本:spark-submit 然后执行以下命令提交测试任务。 Standalone 提交命令: ./spark-submit --master spark://sean01:7077 --class org.apache.spark.examples.SparkPi ../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100 从日志中可以查看到结果: 通过访问WEB端也能看到 >> sean01:8080 YARN 提交命令: ./spark-submit --master yarn --class org.apache.spark.examples.SparkPi ../lib/spark-examples-1.6.0-hadoop2.6.0.jar 10 提交之前要先关闭Standalone模式 进入 sbin 目录下,执行./stop-all.sh 然后启动Hadoop集群 先启动Zookeeper:zkServer.sh start再启动Hadoop:start-all.sh 最后提交spark任务:","tags":[{"name":"spark","slug":"spark","permalink":"http://www.seanxia.cn/tags/spark/"},{"name":"分布式计算","slug":"分布式计算","permalink":"http://www.seanxia.cn/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E8%AE%A1%E7%AE%97/"}]},{"title":"流式处理Storm","date":"2017-11-01T16:00:00.000Z","path":"大数据/b6b65a25.html","text":"Storm是一个免费开源、分布式、高容错的实时计算系统。Storm令持续不断的流计算变得容易,弥补了Hadoop批处理所不能满足的实时要求。 官网 http://storm.apache.org Storm简介 Storm进程常驻内存,数据不经过磁盘,在内存中处理。 流式处理 1、流式处理(异步) 客户端提交数据进行结算,并不会等待数据计算结果。 2、逐条处理 例:ETL 3、统计分析 例:计算PV、UV、访问热点 以及 某些数据的聚合、加和、平均等 客户端提交数据之后,计算完成结果存储到Redis、HBase、MySQL或者其他MQ当中, 客户端并不关心最终结果是多少。 实时请求 1、实时请求应答服务(同步) 客户端提交数据请求之后,立刻取得计算结果并返回给客户端 2、Drpc 3、实时请求处理 例:图片特征提取 高可靠性 1、异常处理 2、消息可靠性保障机制。 可维护性 StormUI 图形化监控接口 Storm与MR、Spark对比 Storm对比Mapreduce Storm:进程、线程常驻内存运行,数据不进入磁盘,数据通过网络传递。 MapReduce:为TB、PB级别数据设计的批处理计算框架。 Storm对比Spark Streaming Storm:纯流式处理 专门为流式处理设计 数据传输模式更为简单,很多地方也更为高效 并不是不能做批处理,它也可以来做微批处理,来提高吞吐 Spark Streaming:微批处理 将RDD做的很小来用小的批处理来接近流式处理 基于内存和DAG Storm架构设计 Nimbus 资源调度 任务分配 接收jar包 Supervisor 接收nimbus分配的任务。 启动、停止自己管理的worker进程(当前supervisor上worker数量由配置文件设定) Worker 运行具体处理运算组件的进程(每个Worker对应执行一个Topology的子集)。 worker任务类型,即spout任务、bolt任务两种。 启动executor(executor即worker JVM进程中的一个java线程,一般默认每个executor负责执行一个task任务,一个worker可以有多个executor,每个executor又可以执行多个task)。 Zookeeper Storm重点依赖的外部资源。 Nimbus和Supervisor甚至实际运行的Worker都是把心跳保存在Zookeeper上的。 Nimbus也是根据Zookeerper上的心跳和任务运行状况,进行调度和任务分配的。 Storm 架构设计与Hadoop架构对比 Storm任务提交流程 提交流程图 下图是Storm的数据交互图。可以看出两个模块Nimbus和Supervisor之间没有直接交互。状态都是保存在Zookeeper上。Worker之间通过ZeroMQ传送数据。 提交流程叙述 Client: 1、client提交topology 到Nimbus; Nimbus: 2、提交的jar包会被上传到nimbus服务器的nimbus/inbox目录下; 3、submitTopology方法对这个topology进行处理: 它首先要对storm本身,以及topology进行一些校验; 它要检查storm的状态是否是active的 它要检查是否已经有同名的topology在storm里面运行了 因为我们会在代码里面给spout、bolt指定id,storm会检查是否有两个spout和bolt使用了相同的id。 任何一个id都不能以 “_” 开头,这种命名方式是系统保留的。 4、建立topology的本地目录: nimbus/stormdist/topology-uuid,该目录包含三个文件: stormjar.jar – 包含这个topology所有代码的jar包(从nimbus/index里面挪过来的) stormcode.ser – 这个topology对象的序列化 stormconf.ser – 运行这个topology的配置 5、nimbus分配任务,根据topology定义中给定的参数,给spout/bolt设定task数据,分配对应的task-id,最后把分配好task的信息写入到zookeeper的/task目录; 6、nimbus在zookeeper上创建taskbeats目录,要求每个task定时发送心跳信息; 7、将分配好的任务,写入zookeeper,任务提交完毕; 8、将topology的信息写入到zookeeper/storms目录; Supervisor: 1、定期扫描zookeeper上的storms目录,查看是否有新的任务,有就下载下来; 2、删除本地不再运行的topology的代码; 3、根据nimbus指定的任务信息启动worker进行工作; Worker: 1、查看需要执行哪些任务; 2、根据taskid分辨出spout、bolt; 3、计算出所代表的的spout/bolt会给哪些task发送消息; 4、根据ip和端口号创建响应的网络连接用来发送消息。 Storm计算模型 Topology - DAG有向无环图的实现 Storm提交运行的程序称为Topology。 由一系列通过数据流相互关联的Spout、Bolt所组成的拓扑结构。 生命周期:此拓扑只要启动就会一直在集群中运行,直到手动将其kill,否则不会终止(区别于MapReduce当中的Job,MR当中的Job在计算执行完成就会终止) Spout – 数据源 拓扑中数据流的来源。一般会从指定外部的数据源读取元组(Tuple)发送到拓扑(Topology)中,一个Spout可以发送多个数据流(Stream)。 可先通过OutputFieldsDeclarer中的declare方法声明定义的不同数据流,发送数据时通过SpoutOutputCollector中的emit方法指定数据流Id(streamId)参数将数据发送出去。 Spout中最核心的方法是nextTuple,该方法会被Storm线程不断调用、主动从数据源拉取数据,再通过emit 方法将数据生成元组(Tuple)发送给之后的Bolt计算。 Bolt – 数据流处理组件 拓扑中数据处理均有Bolt完成。对于简单的任务或者数据流转换,单个Bolt可以简单实现;更加复杂场景往往 需要多个Bolt分多个步骤完成。 一个Bolt可以发送多个数据流(Stream)。 可先通过OutputFieldsDeclarer中的declare方法声明定义的不同数据流,发送数据时通过SpoutOutputCollector中的emit方法指定数据流Id(streamId)参数将数据发送出去。 Bolt中最核心的方法是execute方法,该方法负责接收到一个元组(Tuple)数据、真正实现核心的业务逻辑。 Tuple – 元组 Stream中最小数据组成单元。 Stream – 数据流 从Spout中源源不断传递数据给Bolt、以及上一个Bolt传递数据给下一个Bolt,所形成的这些数据通道即叫做 Stream。 Stream声明时需给其指定一个Id(默认为Default) 实际开发场景中,多使用单一数据流,此时不需要单独指定StreamId demo API Storm 数据累加 MyTopology类(main方法) public class MyTopology { public static void main(String[] args){ // 数据累加... spout bolt TopologyBuilder topologyBuilder = new TopologyBuilder(); topologyBuilder.setSpout("myspout", new MySpout()); topologyBuilder.setBolt("mybolt", new MyBolt()).shuffleGrouping("myspout"); // 定义配置 Config config = new Config(); StormTopology topology = topologyBuilder.createTopology(); //本地提交 LocalCluster localCluster = new LocalCluster(); localCluster.submitTopology("sum", config, topology) }} MySpout类:发送数据 public class MySpout extends BaseRichSpout { SpoutOutputCollector collector; int i = 0; /** * 初始化方法。框架在执行任务的时候会先执行此方法 * @param conf 可以得到spout的配置 * @param context 上下文环境 * @param collector 往下游发送数据 */ @Override public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) { this.collector = collector; } /** * 此方法是spout的核心方法。 * 框架会一直(无限)调用此方法,每当调用时,我们应该往下游发送数据 */ @Override public void nextTuple() { i++; collector.emit(new Values(i)); //底层不定参 try { //发送一条数据睡眠一下,降低发送频率 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("spout 发送。。。" + i); } /** * 当需要往下游发送数据时,就要声明字段个数和名称 * @param declarer */ @Override public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(new Fields("number")); //与Values方法传入的参数保持一致 }} MyBolt类:处理数据 public class MyBolt extends BaseRichBolt{ int sum; /** * bolt初始化方法 * @param stormConf * @param context * @param collector */ @Override public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) { } /** * bolt中最核心的方法。框架会一只调用此方法,每次调用就传一个数据进来 * @param input */ @Override public void execute(Tuple input) { Integer integer = input.getInteger(0);// input.getIntegerByField("number"); sum += integer; System.out.println("execute : " + integer + " sum : " + sum); } @Override public void declareOutputFields(OutputFieldsDeclarer declarer) { //这里不需要发送数据 }} 运行代码,查看打印信息: 从打印信息看出,累加操作完成。 Storm WordCount WordCountToplogy类(main方法) public class WordCountToplogy { /** * 一对一 线程与task * @param args */ public static void main(String[] args) { TopologyBuilder topologyBuilder = new TopologyBuilder(); // 设置并行度为3即task topologyBuilder.setSpout("wcspout",new WordCountSpout(),3); // 设置并行度为5即task topologyBuilder.setBolt("splitbolt",new SplitBolt(),5) .shuffleGrouping("wcspout"); // 随机分组 // 按字段分组,每个单词累加个数 topologyBuilder.setBolt("countbolt",new CountBolt(),6) .fieldsGrouping("splitbolt",new Fields("word")); Config config = new Config();// config.setNumWorkers(3); //设置worker进程个数 if (args.length > 0) { try { // 集群运行方式 StormSubmitter.submitTopology(args[0], config, topologyBuilder.createTopology()); } catch (AlreadyAliveException e) { e.printStackTrace(); } catch (InvalidTopologyException e) { e.printStackTrace(); } } else { // 本地运行方式 LocalCluster localCluster = new LocalCluster(); localCluster.submitTopology("mytopology", config, topologyBuilder.createTopology()); } }} WordCountSpout类:发送数据 public class WordCountSpout extends BaseRichSpout { SpoutOutputCollector collector; String[] lines = new String[]{ "i like learning", "i miss you ", "good good study day day up" }; @Override public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) { this.collector = collector; } @Override public void nextTuple() { Random random = new Random(); int index = random.nextInt(lines.length); String line = lines[index]; System.out.println("spout : " + line); collector.emit(new Values(line)); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } @Override public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(new Fields("line")); }} SplitBolt类:切分单词 public class SplitBolt extends BaseRichBolt { OutputCollector collector; @Override public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) { this.collector = collector; } @Override public void execute(Tuple input) { String line = input.getString(0); String[] words = line.split(" "); for(String word : words){ collector.emit(new Values(word)); } } @Override public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(new Fields("word")); }} CountBolt类:统计单词个数 public class CountBolt extends BaseRichBolt { Map<String,Integer> resultMap = new HashMap<>(); @Override public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) { } @Override public void execute(Tuple input) { String word = input.getStringByField("word"); //判断是否已包含 if(resultMap.containsKey(word)){ Integer integer = resultMap.get(word); integer++; resultMap.put(word,integer); }else{ resultMap.put(word,1); //初始值为1 } System.out.println("word :" + word + " : " + resultMap.get(word)); } @Override public void declareOutputFields(OutputFieldsDeclarer declarer) { }} 运行,查看打印结果: 由打印结果可以看出,统计单词完成。 Storm Grouping Shuffle Grouping 随机分组,随机派发stream里面的tuple,保证每个bolt task接收到的tuple数目大致相同。 Fields Grouping 按字段分组,比如,按"user-id"这个字段来分组,那么具有同样"user-id"的 tuple 会被分到相同的Bolt里的一 个task, 而不同的"user-id"则可能会被分配到不同的task。 All Grouping 广播发送,对于每一个tuple,所有的bolts都会收到。 Global Grouping 全局分组,把tuple分配给task id最低的task 。 None Grouping 不分组,这个分组的意思是说stream不关心到底怎样分组。目前这种分组和Shuffle grouping是一样的效果。 有一点不同的是storm会把使用none grouping的这个bolt放到这个bolt的订阅者同一个线程里面去执行(未 来Storm如果可能的话会这样设计)。 Direct Grouping 指向型分组, 这是一种比较特别的分组方法,用这种分组意味着消息(tuple)的发送者指定由消息接收者的 哪个task处理这个消息。只有被声明为 Direct Stream 的消息流可以声明这种分组方法。而且这种消息tuple必 须使用 emitDirect 方法来发射。消息处理者可以通过 TopologyContext 来获取处理它的消息的task的id (OutputCollector.emit方法也会返回task的id)。 Local or shuffle grouping 本地或随机分组。如果目标bolt有一个或者多个task与源bolt的task在同一个工作进程中,tuple将会被随机发 送给这些同进程中的tasks。否则,和普通的Shuffle Grouping行为一致。 customGrouping 自定义,相当于mapreduce那里自己去实现一个partition一样。 并发机制 Topology参数设置 Worker – 进程 一个Topology拓扑会包含一个或多个Worker(每个Worker进程只能从属于一个特定的Topology)。 这些Worker进程会并行跑在集群中不同的服务器上,即一个Topology拓扑其实是由并行运行在Storm集群中 多台服务器上的进程所组成。 Executor – 线程 Executor是由Worker进程中生成的一个线程。 每个Worker进程中会运行拓扑当中的一个或多个Executor线程。 一个Executor线程中可以执行一个或多个Task任务(默认每个Executor只执行一个Task任务),但是这些 Task任务都是对应着同一个组件(Spout、Bolt)。 Task 实际执行数据处理的最小单元。 每个task即为一个Spout或者一个Bolt。 Task数量在整个Topology生命周期中保持不变,Executor数量可以变化或手动调整。 默认情况下,Task数量和Executor是相同的,即每个Executor线程中默认运行一个Task任务。 设置Worker进程数 Config.setNumWorkers(int workers) 设置Executor线程数 TopologyBuilder.setSpout(String id, IRichSpout spout, Number parallelism_hint)TopologyBuilder.setBolt(String id, IRichBolt bolt, Number parallelism_hint) 其中, parallelism_hint即为executor线程数。 设置Task数量 ComponentConfigurationDeclarer.setNumTasks(Number val) 例: Config conf = new Config() ;conf.setNumWorkers(2);TopologyBuilder topologyBuilder = new TopologyBuilder();topologyBuilder.setSpout("spout", new MySpout(), 1);topologyBuilder.setBolt("green-bolt", new GreenBolt(), 2).setNumTasks(4).shuffleGrouping("blue-spout); 负载均衡 Rebalance – 再平衡 动态调整Topology拓扑的Worker进程数量、以及Executor线程数量。 支持两种调整方式 通过Storm UI 通过Storm CLI 通过Storm CLI动态调整 例:storm rebalance mytopology -n 5 -e blue-spout=3 -e yellow-bolt=10 将 mytopology 拓扑 worker 进程数量调整为5个 “ blue-spout ” 所使用的线程数量调整为3个 “ yellow-bolt ”所使用的线程数量调整为10个 通信机制 Worker进程间的数据通信 ZMQ ZeroMQ 开源的消息传递框架,并不是一个MessageQueue。 Netty Netty是基于NIO的网络框架,更加高效。(之所以Storm 0.9版本之后使用Netty,是因为ZMQ的license和Storm的license不兼容。) Worker内部的数据通信 Disruptor 实现了“队列”的功能。 可以理解为一种事件监听或者消息处理机制,即在队列当中一边由生产者放入消息数据,另一边消费者 并行取出消息数据处理。 Worker内部的消息传递机制: 容错机制 集群节点宕机 Nimbus服务器 单点故障。 非Nimbus服务器 故障时,该节点上所有Task任务都会超时,Nimbus会将这些Task任务重新分配到其他服务器上运行。 进程挂掉 Worker 挂掉时,Supervisor会重新启动这个进程。如果启动过程中仍然一直失败,并且无法向Nimbus发送心跳,Nimbus会将该Worker重新分配到其他服务器上。 Supervisor supervisor挂掉,不影响正在运行的worker,但也不会在这台机器上面启动新的worker。 supervion和worker都挂了,这个worker会转移到其它正常运行的supervisor节点上面。 Nimbus nimbus挂掉,不影响正在运行的任务,但也不会接受新的Topology。 消息的完整性 从Spout中发出的Tuple,以及基于他所产生Tuple(例如上个例子当中Spout发出的句子, 以及句子当中单词的tuple等)。 由这些消息就构成了一棵tuple树。 当这棵tuple树发送完成,并且树当中每一条消息都被正确处理,就表明spout发送消息被“ 完整处理”,即消息的完整性。 Acker – 消息完整性的实现机制 Storm的拓扑当中特殊的一些任务。 负责跟踪每个Spout发出的Tuple的DAG(有向无环图)。 Storm 完全分布式部署 部署ZooKeeper 版本3.4.5+ (高版本Zookeeper实现了对于自身持久化数据的定期删除功能) (autopurge.purgeInterval; autopurge.snapRetainCount) 上传、解压安装包 在storm目录中创建logs目录 $ mkdir logs 修改配置文件 storm.yaml 配置文件内容: storm.zookeeper.servers: - "sean01" - "sean02" - "sean03"nimbus.host: “sean01" 由于每个节点都需要配置,将当前节点storm远程拷贝到其他的节点上: scp -r apache-storm-0.10.0 sean02:`pwd`scp -r apache-storm-0.10.0 sean03:`pwd` 启动Zookeeper集群 zkServer.sh start 在sean01上启动Nimbus #将nimbus的进程日志标准、错误输出重定向追加加到/logs/nimbus.out文件中,后面的&符代表linux后台运行./bin/storm nimbus >> ./logs/nimbus.out 2>&1 &#将ui的进程日志标准、错误输出重定向追加加到/logs/ui.out文件中,后台运行./bin/storm ui >> ./logs/ui.out 2>&1 & 查看linux进程: 注:core 进程为Storm Ui的进程。 在sean02、sean03上启动Supervisor (按照配置每个Supervisor上启动4个slots) ./bin/storm supervisor >> ./logs/supervisor.out 2>&1 & 启动Storm UI ./storm ui >> ./logs/ui.out 2>&1 & #Web Ui的进程 通过http://sean01:8080/访问","tags":[{"name":"流式处理","slug":"流式处理","permalink":"http://www.seanxia.cn/tags/%E6%B5%81%E5%BC%8F%E5%A4%84%E7%90%86/"},{"name":"Storm","slug":"Storm","permalink":"http://www.seanxia.cn/tags/Storm/"}]},{"title":"消息队列Kafka","date":"2017-10-27T16:00:00.000Z","path":"大数据/efe707af.html","text":"Kafka 是一个高吞吐、低延迟分布式的消息队列系统。kafka 每秒可以处理几十万条消息,它的延迟最低只有几毫秒。 官网:https://kafka.apache.org Kafka 简介 Kafka 架构 kafka 集群有多个 Broker 服务器组成,每个类型的消息被定义为 topic。 同一 topic 内部的消息按照一定的 key 和算法被分区(partition)存储在不同的 Broker 上。 消息生产者 producer 和消费者 consumer 可以在多个 Broker 上生产/消费 topic。 Broker:服务器节点 消息中间件处理节点,一个Kafka节点就是一个broker,一个或者多个Broker可以组成一个Kafka集群; Topic:消息主题(类型) 主题是对一组消息的抽象分类,比如例如page view日志、click日志等都可以以topic的形式进行抽象划分类别。在物理上,不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可使得数据的生产者或消费者不必关心数据存于何处; Topic 即为每条发布到 Kafka 集群的消息的类别,topic 在 Kafka 中可以由多个消费者订阅、消费。 一个 topic 可以有多个 partition,分布在不同的 broker 服务器上。 Partiton:分区 每个主题又被分成一个或者若干个分区(Partition)。每个分区在本地磁盘上对应一个文件夹,分区命名规则为主题名称后接 “ - ” 连接符,之后再接分区编号,分区编号从0开始至分区总数减-1。 强一致性:Kafka 只保证一个分区内的消息有序,不能保证一个主题的不同分区之间的消息有序。如果你想要保证所有的消息都绝对有序可以只为一个主题分配一个分区。 Producers :生产者 消息生产者,负责发布消息到 Kafka broker。 Consumers:消费者 消息消费者,向 Kafka broker 读取消息的客户端。 Kafka 中其他关键词 LogSegment:日志分段 在Kafka中,每个分区又被划分为多个日志分段(LogSegment)组成,日志分段是Kafka日志对象分片的最小单位 LogSegment 算是一个逻辑概念,对应一个具体的日志文件(“.log”的数据文件)和两个索引文件(“.index”和“.timeindex”,分别表示偏移量索引文件和消息时间戳索引文件)组成。 Offset:消息偏移量 每个 partition 中都由一系列有序的、不可变的消息组成,这些消息被顺序地追加到partition中。每个消息都有一个连续的序列号称之为 offset 消息偏移量,用于在 partition 内唯一标识消息(并不表示消息在磁盘上的物理位置)。 分区会给每个消息记录分配一个顺序 ID 号(偏移量offset), 能够唯一地标识该分区中的每个记录。Kafka 集群保留所有发布的记录,不管这个记录有没有被消费过,Kafka 提供相应策略通过配置从而对旧数据处理。 实际上,每个消费者唯一保存的元数据信息就是消费者当前消费日志的位移位置。位移位置是由消费者控制,即、消费者可以通过修改偏移量读取任何位置的数据。 Messeage:消息 消息是Kafka中存储的最小最基本的单位,即为一个commit log,由一个固定长度的消息头和一个可变长度的消息体组成。 Kafka 的使用场景 日志收集: 一个公司可以用Kafka可以收集各种服务的log,通过 kafka 以统一接口服务的方式开放给各种 consumer,例如hadoop、Hbase、Solr 等。 消息系统: 解耦和生产者和消费者、缓存消息等。 用户活动跟踪: Kafka 经常被用来记录 web 用户或者 app 用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到 kafka 的 topic 中,然后订阅者通过订阅这 topic 来做实时的监控分析,或者装载到 hadoop、数据仓库中做离线分析和挖掘。 运营指标: Kafka 也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告。 流式处理: 比如 spark streaming 和 storm。 Kafka 集群部署 集群规划: Zookeeper 集群共三台服务器,分别为:sean01、sean02、sean03。 Kafka 集群共三台服务器,分别为:sean01、sean02、sean03。 1、Zookeeper 集群准备 kafka 是一个分布式消息队列,需要依赖 ZooKeeper,请先安装好 ZooKeeper 集群。 2、安装 Kafka (1)下载压缩包(官网地址:http://kafka.apache.org/downloads.html) (2)解压: tar -zxvf kafka_2.10-0.9.0.1.tgz -C ../ # "-C"的作用是解压到指定路径 (3)修改配置文件:config/server.properties 核心配置参数说明: **broker.id:**broker 集群中唯一标识 id,0、1、2、3 依次增长(broker 即 Kafka 集群中的一台服务器)。 注:当前 Kafka 集群共三台节点,分别为:sean01、sean02、sean03。对应的 broker.id 分别为 0、1、2。 **zookeeper.connect:**zookeeper 集群地址列表。 (4)最后将当前服务器上的 Kafka 目录同步到其他服务器节点上。 3、启动 Kafka 集群 启动 Zookeeper 集群。 启动 Kafka 集群。 分别在三台服务器上执行以下命令启动: bin/kafka-server-start.sh config/server.properties 4、测试 (1)创建 topic: bin/kafka-topics.sh --zookeeper sean01:2181,sean02:2181,sean03:2181 --create --replication-factor 2 --partitions 3 --topic test 参数说明: replication-factor:指定每个分区的复制因子个数,默认 1 个 partitions:指定当前创建的 kafka 分区数量,默认为 1 个 topic:指定新建 topic 的名称 (2)查看 topic 列表: bin/kafka-topics.sh --zookeeper sean01:2181,sean02:2181,sean03:2181 --list (3)查看 “test” topic 描述: bin/kafka-topics.sh --zookeeper sean01:2181,sean02:2181,sean03:2181 --describe --topic test (4)创建生产者:sean03 bin/kafka-console-producer.sh --broker-list sean01:9092,sean02:9092,sean03:9092 --topic test (5)创建消费者:sean02 bin/kafka-console-consumer.sh --zookeeper sean01:2181,sean02:2181,sean03:2181 --from-beginning --topic test 注: 查看帮助手册: bin/kafka-console-consumer.sh help (6)查看结果: 先在生产者节点sean03中输入几句话 然后到消费者节点sean02中查看 Flume与Kafka 整合 1、Flume 安装 Flume 详细的安装流程详见:[Flume框架][https://www.seanxia.cn/大数据/93326c6d.html] 2、Flume + Kafka 启动 Kafka 集群。 bin/kafka-server-start.sh config/server.properties 配置 Flume 集群,并启动 Flume 集群。 bin/flume-ng agent -n a1 -c conf -f conf/fk.conf -Dflume.root.logger=DEBUG,console 其中,Flume 配置文件 fk.conf 内容如下: a1.sources = r1a1.sinks = k1a1.channels = c1# Describe/configure the sourcea1.sources.r1.type = avroa1.sources.r1.bind = sean03a1.sources.r1.port = 41414# Describe the sinka1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSinka1.sinks.k1.topic = testflumea1.sinks.k1.brokerList = sean01:9092,sean02:9092,sean03:9092a1.sinks.k1.requiredAcks = 1a1.sinks.k1.batchSize = 20a1.sinks.k1.channel = c1# Use a channel which buffers events in memorya1.channels.c1.type = memorya1.channels.c1.capacity = 1000000a1.channels.c1.transactionCapacity = 10000# Bind the source and sink to the channela1.sources.r1.channels = c1a1.sinks.k1.channel = c1 3、测试 分别启动 Zookeeper、Kafka、Flume 集群。 创建 topic: # 使用JavaAPI插入数据时可以不创建,系统会自动创建一个分区和副本都为1的指定Topicbin/kafka-topics.sh --zookeeper sean01:9092,sean02:9092,sean03:9092 --create --replication-factor 2 --partitions 3 --topic testflume 启动消费者: # 这里我们也可以使用JavaAPI操作,不用shell操作bin/kafka-console-consumer.sh --zookeeper sean01:9092,sean02:9092,sean03:9092 --from-beginning --topic testflume 运行 “RpcClientDemo” 代码,通过 RPC 请求发送数据到 Flume 集群。 Flume 中 source 类型为 AVRO 类型,此时通过 Java 发送 rpc 请求,测试数据是否传入 Kafka。 相关Demo可以参考 Flume 官方文档:http://flume.apache.org/FlumeDeveloperGuide.html 先定义生产者:RpcClientDemo类 public class RpcClientDemo { public static void main(String[] args) throws InterruptedException { MyRpcClientFacade client = new MyRpcClientFacade(); // Initialize client with the remote Flume agent's host and port // 使用远程Flume代理主机和端口初始化客户端 client.init("sean03", 41414); // Send 10 events to the remote Flume agent. That agent should be // configured to listen with an AvroSource. for (int i =0; i < 300; i++) { int number = new Random().nextInt(3); String sampleData ; if(number == 0){ sampleData = "Hello Flume! ERROR " + i; }else if(number==1){ sampleData = "Hello Flume! INFO " + i; }else { sampleData = "Hello Flume! WARNING " + i; } client.sendDataToFlume(sampleData); Thread.sleep(500); } client.cleanUp(); }}class MyRpcClientFacade { private RpcClient client; private String hostname; private int port; public void init(String hostname, int port) { // Setup the RPC connection this.hostname = hostname; this.port = port; this.client = RpcClientFactory.getDefaultInstance(hostname, port); // Use the following method to create a thrift client (instead of the // above line): // this.client = RpcClientFactory.getThriftInstance(hostname, port); } public void sendDataToFlume(String data) { // Create a Flume Event object that encapsulates the sample data Event event = EventBuilder.withBody(data, Charset.forName("UTF-8")); // Send the event try { client.append(event); } catch (EventDeliveryException e) { // clean up and recreate the client client.close(); client = null; client = RpcClientFactory.getDefaultInstance(hostname, port); // Use the following method to create a thrift client (instead of // the above line): // this.client = RpcClientFactory.getThriftInstance(hostname, port); } } public void cleanUp() { // Close the RPC connection client.close(); }} 定义消费者:MyConsumer public class MyConsumer extends Thread { private final ConsumerConnector consumer; private final String topic; public MyConsumer(String topic) { consumer = Consumer.createJavaConsumerConnector(createConsumerConfig()); this.topic = topic; } private static ConsumerConfig createConsumerConfig() { Properties props = new Properties(); props.put("zookeeper.connect", "sean01:2181,sean02:2181,sean03:2181"); props.put("group.id", "xss01"); props.put("zookeeper.session.timeout.ms", "400"); props.put("auto.commit.interval.ms", "100"); props.put("auto.offset.reset","smallest");// props.put("auto.commit.enable","false"); // 关闭自动提交,开启手动提交 return new ConsumerConfig(props); } // push消费方式,服务端推送过来。主动方式是pull public void run() { Map<String, Integer> topicCountMap = new HashMap<String, Integer>(); //mytopic2 topicCountMap.put(topic, 1); // 描述读取哪个topic,需要几个线程读 Map<String, List<KafkaStream<byte[], byte[]>>> consumerMap = consumer .createMessageStreams(topicCountMap); // 每个线程对应于一个KafkaStream List<KafkaStream<byte[], byte[]>> list = consumerMap.get(topic); KafkaStream stream = list.get(0); ConsumerIterator<byte[], byte[]> it = stream.iterator(); System.out.println("xixii................"); while (it.hasNext()){ String data = new String(it.next().message()); System.out.println("开始处理数据 ...:"+ data); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }// System.out.println("数据处理中..." + data);// try {// Thread.sleep(2000);// } catch (InterruptedException e) {// e.printStackTrace();// }// System.out.println("处理完数据..." + data);// consumer.commitOffsets(); } } public static void main(String[] args) { MyConsumer consumerThread = new MyConsumer("testflume"); consumerThread.start(); }} 先启动 RpcClientDemo 再启动 MyConsumer 说明数据已经传入Kafka中,Flume和Kafka整合成功。 Flume & Kafka & Storm(Spark) 利用Flume和Kafka,我们还可以跟流式计算框架Storm或Spark进行整合,来处理工作中的业务需求。 大概流程如下: Kafka 对比 HDFS 1、Kafka 分区里的数据是有序的,读写更快。 2、Kafka 写数据先写入内存中,来保证高吞吐量。 吞吐量:单位时间内读写的数据量大小。 3、因为Kafka的数据先写入内存,一旦在写入过程中服务器宕机,会有丢失数据的风险。 可以通过增加备份来降低数据丢失的风险(副本机制)。 设置 acks 的值。 acks = 0:tomcat客户端只需发送给leader服务器,无需返回的响应信息。速度最快 acks = 1:tomcat客户端需要收到leader服务器返回的响应信息。默认此配置 acks = -1 或 all:tomcat客户端需要收到所有副本返回的响应信息。速度最慢 ISR(in syncronized replication): leader候选人机制 所有的副本都是在 ISR 列表中的节点上,来保证数据的完整性。 当 leader 服务器宕机时,Zookeeper 会从 ISR 列表中去选举新的 leader,每个副本节点在同步数据时,由于每个节点负载消耗不同,如果某个节点负载过高,数据无法在短时间内同步,就会从 isr 列表中暂时移除,等它同步正常之后在添加会 isr 列表。 Kafka的数据丢失和重复消费 发生原因 原因都起始于偏移量 offset 的提交周期问题。系统默认1分钟。 数据丢失 生产者 producer 当默认状态下(acks=1),客户端提交数据到leader服务器,收到leader的响应信息后结束发送。然后在 leader 进行同步备份节点数据时,leader 服务器宕机,而数据还未备份成功,数据就会丢失。下次客户端来访问就访问不到。 消费者 consumer 客户端提交的间隔比较频繁,数据未处理成功叫提交偏移量,此时服务器宕机,数据就会丢失。 试验一下:API中把自动提交的间隔调小 然后把API中的提交数据过程打开 启动消费端,查看控制台:在xss5未处理完成的时候断开 再次重启,查看:发现从 xss6 开始,xss5被忽略了,但xss5上次的偏移量并未提交完成,数据丢失! 重复消费 重复消费存在于消费者中。 客户端自动提交的时间间隔太久,在还未提交前服务器宕机,下次重启后只会从之前记录的偏移量开始消费,就会造成重复消费。 试验一下:API中把自动提交的间隔调的大一些 看消费端,当小费到 xss15 的时候关掉服务 然后重新启动消费端,发现还是从 xss1 开始,由于未到一分钟还未提交偏移量,形成重复消费。 解决办法 1、调整生产者的 acks 状态 改变acks的值,设置为 -1 或 all acks = -1 或 all:tomcat客户端需要收到所以副本返回的响应信息。速度最慢 2、关闭自动提交,使用手动提交方式。 auto.commit.enable","false" 默认配置为打开:true 其他详细配置可以参照官网:http://kafka.apache.org/090/documentation.html#highlevelconsumerapi 注:手动提交虽然能解决 Kafka 的数据丢失和重复消费问题,但是效率速度上相对自动提交降低很多,所以具体问题还需具体分析,应该是在实际业务中去取舍。(比如人口统计重复消费问题可以忽略少量,但是效率第一;再如银行业务是绝对不允许重复消费和数据丢失的,对精确度要求更高) Kafka 副本机制 Kafka的副本机制指的是多个服务端节点对其他节点的 topic 分区的日志进行复制。 当集群中的某个节点出现故障,访问故障节点的请求就会被转移到其他正常节点(这个过程叫做 Reblance)。 Kafka每个主题的每个分区都有一个主副本以及0或多个从副本,从副本保持与主副本的数据同步,当主副本出故障时就会被从副本替代。 在Kafka中,并不是所有的副本都能拿来替代主副本,所以在Kafka的Leader节点中维护着一个ISR列表,候选人机制。 当Leader服务器宕机时,Zookeeper会从ISR列表中去选举新的Leader。每个副本节点在同步数据时,由于负载消耗不同,如果某个节点负载较高,数据无法在短时间内同步,就会从ISR列表中暂时移除,等它正常同步之后,再添加到ISR列表中。 Kafka 数据存储 Kafka中的消息是以主题(Topic)为基本单位进行组织的,各个主题之间相互独立。在这里主题只是一个逻辑上的抽象概念,而在实际数据文件的存储中,Kafka中的消息存储在物理上是以一个或多个分区(Partition)构成,每个分区对应本地磁盘上的一个文件夹,每个文件夹内包含了日志索引文件(“.index”和“.timeindex”)和日志数据文件(“.log”)两部分。分区数量可以在创建主题时指定,也可以在创建Topic后进行修改。 同时,Kafka为了实现集群的高可用性,在每个Partition中可以设置有一个或者多个副本(Replica),分区的副本分布在不同的Broker节点上。同时,从副本中会选出一个副本作为Leader,Leader副本负责与客户端进行读写操作。而其他副本作为Follower会从Leader副本上进行数据同步。 Kafka中分区/副本的日志文件存储分析 每个分区又有1至多个副本,分区的副本分布在集群的不同代理上,以提高可用性。从存储的角度上来说,分区的每个副本在逻辑上可以抽象为一个日志(Log)对象,即分区副本与日志对象是相对应的。下图是在三个Kafka Broker节点所组成的集群中分区的主/备份副本的物理分布情况图: Kafka中日志索引和数据文件的存储结构 在Kafka中,每个 Log 对象又可以划分为多个 LogSegment 文件,每个 LogSegment 文件包括一个日志数据文件和两个索引文件(偏移量索引文件和消息时间戳索引文件)。 其中,每个 LogSegment 中的日志数据文件大小均相等(该日志数据文件的大小可以通过在Kafka Broker的config/server.properties配置文件的中的**“log.segment.bytes”**进行设置,默认为1G大小(1073741824字节),在顺序写入消息时如果超出该设定的阈值,将会创建一组新的日志数据和索引文件)。 Kafka将日志文件封装成一个FileMessageSet对象,将偏移量索引文件和消息时间戳索引文件分别封装成OffsetIndex 和 TimerIndex 对象。Log和LogSegment均为逻辑概念,Log是对副本在Broker上存储文件的抽象,而 LogSegment 是对副本存储下每个日志分段的抽象,日志与索引文件才与磁盘上的物理存储相对应;下图为Kafka日志存储结构中的对象之间的对应关系图: Kafka中Message的存储和查找过程 Message是按照topic来组织,每个topic可以分成多个的partition。比如:有5个partition的名为为page_visits的topic的目录结构为: partition是分段的,每个段叫LogSegment,包括了一个数据文件和一个索引文件。下图是某个partition目录下的文件: 可以看到,这个partition有4个LogSegment。 查找Message原理图: 比如,要查找绝对offset为7的Message: 首先是用二分查找确定它是在哪个LogSegment中,自然是在第一个Segment中。 打开这个Segment的index文件,也是用二分查找找到offset小于或者等于指定offset的索引条目中最大的那个offset。自然offset为6的那个索引是我们要找的,通过索引文件我们知道offset为6的Message在数据文件中的位置为9807。 打开数据文件,从位置为9807的那个地方开始顺序扫描直到找到offset为7的那条Message。 这套机制是建立在offset是有序的。索引文件被映射到内存中,所以查找的速度还是很快的。 一句话,Kafka的Message存储采用了分区(partition),分段(LogSegment)和稀疏索引这几个手段来达到了高效性。 思考拓展 Kafka使用磁盘也可以高效读写的原因? 1、顺序写入 因为硬盘是机械结构,每次读写都会寻址->写入,其中寻址是一个“机械动作”,它是最耗时的。所以硬盘最“讨厌”随机I/O,最喜欢顺序I/O。为了提高读写硬盘的速度,Kafka就是使用顺序I/O。 上图就展示了Kafka是如何写入数据的, 每一个Partition其实都是一个文件 ,收到消息后Kafka会把数据插入到文件末尾(虚框部分)。 这种方法有一个缺陷—— 没有办法删除数据 ,所以Kafka是不会删除数据的,它会把所有的数据都保留下来,每个消费者(Consumer)对每个Topic都有一个offset用来表示 读取到了第几条数据 。 上图中有两个消费者,Consumer1有两个offset分别对应Partition0、Partition1(假设每一个Topic一个Partition);Consumer2有一个offset对应Partition2。这个offset是由客户端SDK负责保存的,Kafka的Broker完全无视这个东西的存在;一般情况下SDK会把它保存到zookeeper里面。(所以需要给Consumer提供zookeeper的地址)。 如果不删除硬盘肯定会被撑满,所以Kakfa提供了两种策略来删除数据。一是基于时间,二是基于partition文件大小。具体配置可以参看它的配置文档。 2、Memory Mapped Files(内存映射文件) 即便是顺序写入硬盘,硬盘的访问速度还是不可能追上内存。所以Kafka的数据并不是实时的写入硬盘 ,它充分利用了现代操作系统分页存储来利用内存提高I/O效率。 Memory Mapped Files(后面简称mmap)也被翻译成 内存映射文件 ,在64位操作系统中一般可以表示20G的数据文件,它的工作原理是直接利用操作系统的Page来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上(操作系统在适当的时候)。 使用这种方式可以获取很大的I/O提升, 省去了用户空间到内核空间 复制的开销(调用文件的read会把数据先放到内核空间的内存中,然后再复制到用户空间的内存中。)也有一个很明显的缺陷——不可靠, 写到mmap中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘。 Kafka提供了一个参数——producer.type来控制是不是主动flush,如果Kafka写入到mmap之后就立即flush然后再返回Producer叫 同步 (sync);写入mmap之后立即返回Producer不调用flush叫 异步 (async)。 3、Kafka高效文件存储设计特点 Kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易根据偏移量查找消息、定期清除和删除已经消费完成的数据文件,减少磁盘容量的占用; 采用稀疏索引存储的方式构建日志的偏移量索引文件,并将其映射至内存中,提高查找消息的效率,同时减少磁盘IO操作;并大幅降低index文件元数据占用空间大小。 Kafka将消息追加的操作逻辑变成为日志数据文件的顺序写入,极大的提高了磁盘IO的性能; 关于Kafka的经典面试知识点 请参考:消息中间件如何实现每秒几十万的高并发写入? 1、页缓存技术 + 磁盘顺序写 2、零拷贝技术","tags":[{"name":"Kafka","slug":"Kafka","permalink":"http://www.seanxia.cn/tags/Kafka/"},{"name":"消息队列","slug":"消息队列","permalink":"http://www.seanxia.cn/tags/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"}]},{"title":"spring框架总结","date":"2017-10-20T15:32:11.000Z","path":"Java/4ddce68d.html","text":"Spring 简介 什么是 Spring Spring 是一个轻量级的 DI / IOC 和 AOP 容器的开源框架,来源于 Rod Johnson 在其著作**《Expert one on one J2EE design and development》**中阐述的部分理念和原型衍生而来。 Spring 提倡以**“最少侵入”**的方式来管理应用中的代码,这意味着我们可以随时安装或者卸载 Spring。 适用范围:任何 Java 应用 Spring 的根本使命:简化 Java 开发 框架中常用术语 框架: 是能完成一定功能的半成品。 框架能够帮助我们完成的是:项目的整体结构布局、一些基础功能、规定了类和对象如何创建,如何协作等,当我们开发一个项目时,框架帮助我们完成了一部分功能,我们自己再完成一部分,那这个项目就完成了。 非侵入式设计: 从框架的角度可以理解为:无需继承框架提供的任何类 这样我们在更换框架时,之前写过的代码几乎可以继续使用。 轻量级和重量级: 轻量级是相对于重量级而言的,轻量级一般就是非入侵性的、所依赖的东西非常少、资源占用非常少、部署简单等等,其实就是比较容易使用,而重量级正好相反。 JavaBean: 即符合 JavaBean 规范的 Java 类 POJO: 即 Plain Old Java Objects,简单老式 Java 对象 它可以包含业务逻辑或持久化逻辑,但不担当任何特殊角色且不继承或不实现任何其它Java框架的类或接口。 容器: 在日常生活中容器就是一种盛放东西的器具,从程序设计角度看就是装对象的的对象,因为存在放入、拿出等操作,所以容器还要管理对象的生命周期。 Spring 的优势 低侵入 / 低耦合 (降低组件之间的耦合度,实现软件各层之间的解耦) 声明式事务管理(基于切面和惯例) 方便集成其他框架(如MyBatis、Hibernate) 降低 Java 开发难度 Spring 框架中包括了 J2EE 三层的每一层的解决方案(一站式) Spring 能帮我们做什么 Spring 能帮我们根据配置文件创建及组件对象之间的依赖关系。 Spring 面向切面编程能帮助我们无耦合的实现日志记录,性能统计,安全控制。 Spring 能非常简单的帮我们管理数据库事务。 Spring 还提供了与第三方数据访问框架(如 Hibernate、JPA)无缝集成,而且自己也提供了一套 JDBC访问模板 来方便数据库访问。 Spring 还提供与第三方Web(如 Struts1/2、JSF)框架无缝集成,而且自己也提供了一套 SpringMVC 框架,来方便 web 层搭建。 Spring 还能方便的与JavaEE(如Java Mail、任务调度)整合,与更多技术整合(如缓存框架)。 Spring 的框架结构 Data Access/Integration层包含有JDBC、ORM、OXM、JMS(java邮件服务)和Transaction模块。 **Web层:**包含了Web、Web-Servlet、WebSocket、Web-Porlet模块。 **AOP模块:**提供了一个符合AOP联盟标准的面向切面编程的实现。 **Core Container(核心容器):**包含有Beans(Bean工厂,创建对象)、Core(一切的基础)、Context(上下文)和SpEL(Spring的表达式语言)模块。 **Test模块:**支持使用JUnit和TestNG对Spring组件进行测试。 Spring IOC 和 DI 简介 IOC:Inverse of Control(控制反转) 不是什么技术,而是一种设计思想,就是将原本在程序中动手创建对象的控制权,交由 Spring 框架来管理。 正控:若要使用某个对象,需要自己去负责对象的创建。 反控:若要使用某个对象,只需要从 Spring 容器中获取需要使用的对象,不关心对象的创建过程,也就是把创建对象的控制权反转给了 Spring 框架。 **好莱坞法则:**Don’t call me,I’ll call you. 举个例子 在现实生活中,人们要用到一样东西的时候,第一反应就是去找到这件东西,比如想喝新鲜橙汁,在没有饮品店的日子里,最直观的做法就是:买果汁机、买橙子,然后准备开水。值得注意的是:这些都是你自己**“主动”创造**的过程,也就是说一杯橙汁需要你自己创造。 然而到了今时今日,由于饮品店的盛行,当我们想喝橙汁时,第一想法就转换成了找到饮品店的联系方式,通过电话等渠道描述你的需要、地址、联系方式等,下订单等待,过一会儿就会有人送来橙汁了。 请注意你并没有“主动”去创造橙汁,橙汁是由饮品店创造的,而不是你,然而也完全达到了你的要求,甚至比你创造的要好上那么一些。 以上这个简单的例子就可以抽象出控制反转的概念,就是把创建对象就相当于做橙汁,饮品店就相当于框架,我们把做橙汁这个控制权交给饮品店,我们直接享用成果就可以了。 编写第一个 Spring 程序 新建一个普通的 maven 项目,命名为【spring】 在【po】包下新建一个实体类 Source 新建【resource】资源文件夹,并创建配置文件 applicationContext.xml,通过 xml 文件配置的方式装配我们的 bean <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean name="source" class="pojo.Source"> <property name="fruit" value="橙子"/> <property name="sugar" value="多糖"/> <property name="size" value="超大杯"/> </bean></beans> 在 Packge【test】下新建一个【TestSpring】类: public class TestSpring { @Test public void test(){ /** * 1、加载配置文件 * 2、获取bean对象 * 3、执行bean方法 */ ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); Source source = (Source) context.getBean("source"); System.out.println(source.getFruit()); System.out.println(source.getSugar()); System.out.println(source.getSize()); }} 运行测试代码,可以正常拿到 xml 配置的 bean 总结 传统方式:通过new关键字主动创建一个对象 **IOC 方式:**对象的生命周期由 Spring 来管理,直接从 Spring 那里去获取一个对象。IOC 是反转控制(Inversion Of Control)的缩写,就像控制权从本来在自己手里,交给了 Spring。 Spring Ioc 实例化 bean 对象的三种方式 默认构造方法 最常用的初始化bean方式,必须提供默认构造方法 public class Person { private String name; private Integer age; public Person() { System.out.println("这是一个无参构造函数"); } public Person(String name) { this.name = name; System.out.println("带有名字="+name+"参数的构造函数"); } public Person(String name, Integer age) { this.name = name; this.age = age; System.out.println("带有名字="+name+"和年龄="+age+"参数的构造函数"); }} bean.xml <bean id="person" class="com.maple.Person"></bean><bean id="personWithParam" class="com.maple.Person"> <constructor-arg name="name" value="枫叶"/></bean><bean id="personWirhParams" class="com.maple.Person"> <constructor-arg name="name" value="枫叶"/> <constructor-arg name="age" value="23"/></bean> 静态工厂方法 当采用静态工厂方法创建bean时,除了需要指定class属性外,还需要通过factory-method属性来指定创建bean实例的工厂方法。Spring将调用此方法返回实例对象,就此而言,跟通过普通构造器创建类实例没什么两样。 public class MyBeanFactory { /** * 创建实例 * @return */ public static UserService createService(){ return new UserServiceImpl(); } public static UserService createService(String name){ return new UserServiceImpl(name); } public static UserService createService(String name,int age){ return new UserServiceImpl(name,age); }} bean.xml <bean id="userServiceId" class="com.maple.MyBeanFactory" factory-method="createService"></bean><bean id="userServiceId" class="com.maple.MyBeanFactory" factory-method="createService"> <constructor-arg name="name" value="枫叶"/></bean><bean id="userServiceId" class="com.maple.MyBeanFactory" factory-method="createService"> <constructor-arg name="name" value="枫叶"/> <constructor-arg name="age" value="23"/></bean> 实例工厂方法 需要配置工厂 bean,并在业务 bean 中配置 factory-bean,factory-method 属性实例化工厂定义。 必须先有工厂实例对象,通过实例对象创建对象。提供所有的方法都是“非静态”的。 /** * 实例工厂,所有方法非静态 * */public class MyBeanFactory { /** * 实例化工厂 */ public UserService createService(){ return new UserServiceImpl(); } /** * 实例化工厂1 */ public static UserService createService(String name){ return new UserServiceImpl(name); } /** * 实例化工厂2 */ public static UserService createService(String name,int age){ return new UserServiceImpl(name,age); } } bean.xml <!-- 创建工厂实例 --> <bean id="myBeanFactoryId" class="com.maple.MyBeanFactory"></bean> <!-- 获得userservice * factory-bean 确定工厂实例 * factory-method 确定普通方法 --> <bean id="userServiceId" factory-bean="myBeanFactoryId" factory-method="createService"></bean> <bean id="userServiceId" factory-bean="myBeanFactoryId" factory-method="createService"> <constructor-arg name="name" value="枫叶"/> </bean> <bean id="userServiceId" factory-bean="myBeanFactoryId" factory-method="createService"> <constructor-arg name="name" value="枫叶"/> <constructor-arg name="age" value="23"/> </bean> DI:Dependency Injection(依赖注入) 指 Spring 创建对象的过程中,将对象依赖属性(简单值,集合,对象)通过配置设值给该对象 继续上个例子 在【po】包下新建一个【JuiceMaker】类: public class JuiceMaker { // 唯一关联了一个 Source 对象 // @Resource @Autowired private Source source; public String makeJuice(){ String juice = "XXX用户点了一杯" + source.getFruit() + source.getSugar() + source.getSize(); return juice; }} 注入Source对象 方式一:采用注解方式注入bean,spring 对于注解有专门的解释器,对定义的注解 进行解析,实现对应 bean 对象的注入,反射技术实现 加入 spring-aop jar 包 spring-aop-4.3.2.RELEASE.jar Xml 配置: 加入 context 命名空间 和 xsd 地址 添加**context:annotation-config/**配置 使用 @Autowired 或 @Resource 在属性字段或 set 方法上(如上) @Autowired 默认按 bean 的类型匹配,可以修改按名称匹配,与 @Qualifier 配合使用 @Resource 默认按名称进行装配,名称可以通过 name 属性进行指定,如果没 有指定 name 属性,当注解写在字段上时,默认取字段名进行匹配注入 ,如果 注解写在 setter 方法上默认取属性名进行装配。当找不到与名称匹配的 bean 时才按照类型进行装配。 推荐使用 @Resource 注解,它是属于 J2EE 的,减少了与 spring 的耦合。 <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 开启注解 --> <context:annotation-config/> <bean name="source" class="cn.spring.po.Source"> <property name="fruit" value="橙子"/> <property name="sugar" value="多糖"/> <property name="size" value="超大杯"/> </bean> <!--<bean name="juiceMaker" class="cn.spring.service.JuiceMaker">--> <!--<property name="source" ref="source"/>--> <!--</bean>--></beans> 方式二:在 xml 文件中配置 JuiceMaker 对象 <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean name="source" class="pojo.Source"> <property name="fruit" value="橙子"/> <property name="sugar" value="多糖"/> <property name="size" value="超大杯"/> </bean> <bean name="juickMaker" class="pojo.JuiceMaker"> <property name="source" ref="source" /> </bean></beans> 在 【TestSpring】 中添加如下代码: public class TestSpring { @Test public void test(){ /** * 1、加载配置文件 * 2、获取bean对象 * 3、执行bean方法 */ ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); Source source = (Source) context.getBean("source"); System.out.println(source.getFruit()); System.out.println(source.getSugar()); System.out.println(source.getSize()); JuiceMaker juickMaker = (JuiceMaker) context.getBean("juickMaker"); System.out.println(juickMaker.makeJuice()); }} 运行测试代码: 总结 IoC 和 DI 其实是同一个概念的不同角度描述,DI 相对 IOC 而言,明确描述了 “被注入对象依赖 IOC 容器配置依赖对象”。 Spring 支持的四种注入方式 Set 注入 xml 配置(同时 spring 也提供了对于基本数据类型的 set 注入方式) <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="userServiceImpl" class="com.shsxt.service.impl.UserServiceImpl"> <property name="userDao" ref="userDao"/></bean><bean id="userDao" class="com.shsxt.dao.UserDao"/> </beans> Java 类 public class UserServiceImpl { private UserDao userDao; public void setUserDao(UserDao userDao) { this.userDao = userDao; } public UserDao getUserDao() { return userDao; } public void saveUser(User user){ System.out.println("userName:"+userName+"price:"+price); userDao.add(user); }} 基本数据类型 set 注入 <bean id="userServiceImpl" class="com.shsxt.service.impl.UserServiceImpl"> <property name="userDao" ref="userDao"/> <property name="userName" value="sxt"/> <property name="price" value="123"/></bean>同时对应 Service 提供对应属性字段 以及 get 、set 方法即可 构造器注入 xml 配置(也提供对于基本数据类型、字符串等值的注入) <bean id="userDao" class="com.shsxt.dao.UserDao"></bean><bean id="userServiceImpl2" class="com.shsxt.service.impl.UserServiceImpl2"> <constructor-arg ref="userDao"/></bean> Java 类 提供构造函数 public class UserServiceImpl2 { private UserDao userDao; public UserServiceImpl2(UserDao userDao) { this.userDao = userDao; } public void saveUser(User user){ userDao.add(user); }} 构造器注入字符串值,Index 属性为参数顺序 如果只有一个参数 index 可以不设置。 <bean id="userServiceImpl" class="com.shsxt.service.impl.UserServiceImpl"> <constructor-arg name="userName" index="0" value="123"/> <constructor-arg name="userPwd" index="1" value="321"/></bean> 静态工厂注入 xml 配置 <bean id="userDao" class="com.shsxt.factory.StaticFactory" factory-method="createUserDao"></bean><bean id="userService" class="com.shsxt.service.UserService"> <property name="userDao" ref="userDao"/></bean> 静态工厂类 package com.shsxt.factory; import com.shsxt.dao.UserDao; public class StaticFactory { public static UserDao createUserDao(){ return new UserDao(); }} 实例化工厂 xml 配置 <bean id="sxtBeanFactory" class="com.shsxt.factory.SxtBeanFactory"/><bean id="instanceFactory" class="com.shsxt.factory.InstanceFactory"/><bean id="userDao" factory-bean="instanceFactory" factory-method="createUserDao"/><bean id="userService" class="com.shsxt.service.UserService"> <property name="userDao" ref="userDao"></property></bean> 实际开发中基本使用 set 方式注入 bean Spring AOP 简介 如果说 IoC 是 Spring 的核心,那么面向切面编程就是 Spring 最为重要的功能之一了,在数据库事务中切面编程被广泛使用。 AOP:Aspect Oriented Program 面向切面编程 功能划分 核心业务:比如登录、增加数据、删除数据 周边功能:比如性能统计、日志、事务管理等等 周边功能在 Spring 的面向切面编程AOP思想里,即被定义为切面。 在面向切面编程AOP的思想里面,核心业务功能和切面功能分别独立进行开发,然后把切面功能和核心业务功能 “编织” 在一起,这就叫AOP。 AOP 的作用 AOP 能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(比如事务处理、日志管理、权限控制)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。 AOP 当中的概念 连接点(Joinpoint) 被拦截到的每个点,spring 中指被拦截到的每一个方法,spring aop 一个连接点即代表一个方法的执行。 切入点(Pointcut) 在哪些类,哪些方法上切入(where),spring 这块有专门的表达式语言定义。 通知(Advice) 在方法执行的什么时间(when:方法前/方法后/方法前后)做什么(what:增强的功能) 前置通知 (前置增强) – before() 执行方法前通知 返回通知(返回增强)-- afterReturn 方法正常结束返回后的通知 异常抛出通知(异常抛出增强)–afetrThrow() 最终通知—after 无论方法是否发生异常,均会执行该通知。 环绕通知—around 包围一个连接点(join point)的通知,如方法调用。这是最强大的一种通知类型。 环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。 切面(Aspect) 切面 = 切入点 + 通知,通俗点就是:在什么时间,什么地方,做什么增强 目标对象(Target) 被代理的目标对象 引入(Introduction) 引入允许我们向现有的类添加新方法或属性 织入(Weaving) 把切面加入到对象,并创建出代理对象的过程。(由 Spring 来完成) AOP 编程解决日志处理问题 Aop 配置有两种方式 注解方式 与 xml 方式 注解方式 jar 包坐标引入 l<!-- aop 注解核心包--><dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.9</version></dependency> beans.xml 配置 添加命名空间 xmlns:aop="http://www.springframework.org/schema/aop" http://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop.xsd 配置 aop 代理 <aop:aspectj-autoproxy/> 编写业务方法 新建 package【task】,创建 UserService 类 @Servicepublic class UserService { public void addUser(){ System.out.println("UserService addUser..."); }} 编写 aop 实现类 /** * 声明切面组件 * 1、连接点 * 2、切入点 * 3、通知 */@Component@Aspectpublic class LogCut { /** * 定义切入点 匹配方法规则定义 * 匹配规则表达式含义,拦截 com.shsxt.service 包下以及子包下所有类的所有方法 */ @Pointcut("execution(* com.shsxt.service..*.*(..))") public void cut(){} /** * 声明前置通知 并将通知应用到定义的切入点上 * 目标泪方法执行前执行该通知 */ @Before(value = "cut()") public void before(){ System.out.println("前置通知。。。"); } /** * 声明返回通知 并将通知应用到切入点上 * 目标类方法执行完毕执行该通知 */ @AfterReturning(value = "cut()") public void returnValue(){ System.out.println("返回通知。。。"); } /** * 声明最终通知 并将通知应用到切入点上 * 目标类方法执行过程中是否发生异常 均会执行该通知相当于异常中的finally */ @After(value = "cut()") public void after(){ System.out.println("最终通知。。。"); } /** * 声明异常通知 并将通知应用到切入点上 * 目标类方法执行时发生异常 执行该通知 * @param e */ @AfterThrowing(value = "cut()", throwing = "e") public void exception(Exception e){ System.out.println("异常通知...方法执行异常时执行:"+e"); } /** * 声明环绕通知,并将通知应用到切入点上 * 方法执行前后,通过环绕通知定义相应处理 * @param pjp * @return * @throws Throwable */ @Around(value = "cut()") public Object around(ProceedingJoinPoint pjp) throws Throwable { System.out.println("环绕前置。。。"); System.out.println("环绕通知"); System.out.println(pjp.getTarget() + " - " + pjp.getSignature()); Object proceed = pjp.proceed(); // 程序继续执行 System.out.println("环绕后置。。。"); return proceed; }} xml 方式 声明 aop 代理(略) 配置切面、切入点、通知 <!--xml 配置 aop--><aop:config> <!-- aop 切面配置 --> <aop:aspect ref="logCut2"> <!-- 定义 aop 切入点 --> <aop:pointcut id="cut" expression="execution(* com.shsxt.service..*.*(..))"/> <!-- 配置前置通知 指定前置通知方法名 并引用切入点定义 --> <aop:before method="before" pointcut-ref="cut"/> <!-- 配置最终通知 指定最终通知方法名 并引用切入点定义 --> <aop:after method="after" pointcut-ref="cut"/> <!-- 配置返回通知 指定返回通知方法名 并引用切入点定义 --> <aop:after-returning method="returnValue" pointcut-ref="cut"/> <!-- 配置异常通知 指定异常通知方法名 并引用切入点定义 --> <aop:after-throwing method="exception" throwing="e" pointcut-ref="cut"/> <!-- 配置环绕通知 指定环绕通知方法名 并引用切入点定义 --> <aop:around method="around" pointcut-ref="cut"/> </aop:aspect></aop:config> 编写业务方法(略) 定义 bean @Componentpublic class LogCut2 { public void before(){ System.out.println("前置通知。。。222"); } public void returnValue(){ System.out.println("返回通知。。。222"); } public void after(){ System.out.println("最终通知。。。222"); } public void exception(Exception e){ System.out.println("异常通知。。。222 方法执行异常时执行:"+e); } /** * 声明环绕通知,并将通知应用到切入点上 * 方法执行前后,通过环绕通知定义相应处理 * @param pjp * @return * @throws Throwable */ public Object around(ProceedingJoinPoint pjp) throws Throwable { System.out.println("环绕前置。。。222"); System.out.println("环绕通知。。。222"); System.out.println(pjp.getTarget() + " - " + pjp.getSignature()); Object proceed = pjp.proceed(); // 程序继续执行 System.out.println("环绕后置。。。222"); return proceed; }} 编写测试类 AopTest: public class AopTest { @Test public void test01() { ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml"); UserService userService = (UserService) context.getBean("userService"); userService.addUser(); }} 运行测试结果 总结 通过AOP我们可以对程序运行中的日志进行监控,可以通过底层反射机制拿到对象所有的方法属性,包括注解。。","tags":[{"name":"spring","slug":"spring","permalink":"http://www.seanxia.cn/tags/spring/"},{"name":"框架","slug":"框架","permalink":"http://www.seanxia.cn/tags/%E6%A1%86%E6%9E%B6/"}]},{"title":"关于Scala中的方法详解","date":"2017-10-15T16:00:00.000Z","path":"大数据/b468623c.html","text":"在编程语言 Scala 中有很多常用的方法。这里做逐一讲解,仅供参考! String方法 char charAt(int index)返回指定位置的字符 从0开始 int compareTo(Object o)比较字符串与对象 int compareTo(String anotherString)按字典顺序比较两个字符串 int compareToIgnoreCase(String str)按字典顺序比较两个字符串,不考虑大小写 String concat(String str)将指定字符串连接到此字符串的结尾 boolean contentEquals(StringBuffer sb)将此字符串与指定的 StringBuffer 比较。 static String copyValueOf(char[] data)返回指定数组中表示该字符序列的 String static String copyValueOf(char[] data, int offset, int count)返回指定数组中表示该字符序列的 String boolean endsWith(String suffix)测试此字符串是否以指定的后缀结束 boolean equals(Object anObject)将此字符串与指定的对象比较 boolean equalsIgnoreCase(String anotherString)将此 String 与另一个 String 比较,不考虑大小写 byte getBytes()使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中 byte[] getBytes(String charsetName使用指定的字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中 void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)将字符从此字符串复制到目标字符数组 int hashCode()返回此字符串的哈希码16 int indexOf(int ch)返回指定字符在此字符串中第一次出现处的索引(输入的是ascii码值) int indexOf(int ch, int fromIndex)返返回在此字符串中第一次出现指定字符处的索引,从指定的索引开始搜索 int indexOf(String str)返回指定子字符串在此字符串中第一次出现处的索引 int indexOf(String str, int fromIndex)返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始 String intern()返回字符串对象的规范化表示形式 int lastIndexOf(int ch)返回指定字符在此字符串中最后一次出现处的索引 int lastIndexOf(int ch, int fromIndex)返回指定字符在此字符串中最后一次出现处的索引,从指定的索引处开始进行反向搜索 int lastIndexOf(String str)返回指定子字符串在此字符串中最右边出现处的索引 int lastIndexOf(String str, int fromIndex)返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索 int length()返回此字符串的长度 boolean matches(String regex)告知此字符串是否匹配给定的正则表达式 boolean regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len)测试两个字符串区域是否相等28 boolean regionMatches(int toffset, String other, int ooffset, int len)测试两个字符串区域是否相等 String replace(char oldChar, char newChar)返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的 String replaceAll(String regex, String replacement使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串 String replaceFirst(String regex, String replacement)使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串 String[] split(String regex)根据给定正则表达式的匹配拆分此字符串 String[] split(String regex, int limit)根据匹配给定的正则表达式来拆分此字符串 boolean startsWith(String prefix)测试此字符串是否以指定的前缀开始 boolean startsWith(String prefix, int toffset)测试此字符串从指定索引开始的子字符串是否以指定前缀开始。 CharSequence subSequence(int beginIndex, int endIndex)返回一个新的字符序列,它是此序列的一个子序列 String substring(int beginIndex)返回一个新的字符串,它是此字符串的一个子字符串 String substring(int beginIndex, int endIndex)返回一个新字符串,它是此字符串的一个子字符串 char[] toCharArray()将此字符串转换为一个新的字符数组 String toLowerCase()使用默认语言环境的规则将此 String 中的所有字符都转换为小写 String toLowerCase(Locale locale)使用给定 Locale 的规则将此 String 中的所有字符都转换为小写 String toString()返回此对象本身(它已经是一个字符串!) String toUpperCase()使用默认语言环境的规则将此 String 中的所有字符都转换为大写 String toUpperCase(Locale locale)使用给定 Locale 的规则将此 String 中的所有字符都转换为大写 String trim()删除指定字符串的首尾空白符 static String valueOf(primitive data type x)返回指定类型参数的字符串表示形式 数组方法 序号 方法和描述1 def apply( x: T, xs: T* ): Array[T]创建指定对象 T 的数组, T 的值可以是 Unit, Double, Float, Long, Int, Char, Short, Byte, Boolean。2 def concat[T]( xss: Array[T]* ): Array[T]合并数组3 def copy( src: AnyRef, srcPos: Int, dest: AnyRef, destPos: Int, length: Int ): Unit复制一个数组到另一个数组上。相等于 Java's System.arraycopy(src, srcPos, dest, destPos, length)。4 def empty[T]: Array[T]返回长度为 0 的数组5 def iterate[T]( start: T, len: Int )( f: (T) => T ): Array[T]返回指定长度数组,每个数组元素为指定函数的返回值。以上实例数组初始值为 0,长度为 3,计算函数为a=>a+1:scala> Array.iterate(0,3)(a=>a+1)res1: Array[Int] = Array(0, 1, 2)6 def fill[T]( n: Int )(elem: => T): Array[T]返回数组,长度为第一个参数指定,同时每个元素使用第二个参数进行填充。7 def fill[T]( n1: Int, n2: Int )( elem: => T ): Array[Array[T]]返回二数组,长度为第一个参数指定,同时每个元素使用第二个参数进行填充。8 def ofDim[T]( n1: Int ): Array[T]创建指定长度的数组9 def ofDim[T]( n1: Int, n2: Int ): Array[Array[T]]创建二维数组10 def ofDim[T]( n1: Int, n2: Int, n3: Int ): Array[Array[Array[T]]]创建三维数组11 def range( start: Int, end: Int, step: Int ): Array[Int]创建指定区间内的数组,step 为每个元素间的步长12 def range( start: Int, end: Int ): Array[Int]创建指定区间内的数组13 def tabulate[T]( n: Int )(f: (Int)=> T): Array[T]返回指定长度数组,每个数组元素为指定函数的返回值,默认从 0 开始。以上实例返回 3 个元素:scala> Array.tabulate(3)(a => a + 5)res0: Array[Int] = Array(5, 6, 7)14 def tabulate[T]( n1: Int, n2: Int )( f: (Int, Int ) => T): Array[Array[T]]返回指定长度的二维数组,每个数组元素为指定函数的返回值,默认从 0 开始。 List方法 序号 方法和描述1 def +(elem: A): List[A]前置一个元素列表2 def ::(x: A): List[A]在这个列表的开头添加的元素。3 def :::(prefix: List[A]): List[A]增加了一个给定列表中该列表前面的元素。4 def ::(x: A): List[A]增加了一个元素x在列表的开头5 def addString(b: StringBuilder): StringBuilder追加列表的一个字符串生成器的所有元素。6 def addString(b: StringBuilder, sep: String): StringBuilder追加列表的使用分隔字符串一个字符串生成器的所有元素。7 def apply(n: Int): A选择通过其在列表中索引的元素8 def contains(elem: Any): Boolean测试该列表中是否包含一个给定值作为元素。9 def copyToArray(xs: Array[A], start: Int, len: Int): Unit列表的副本元件阵列。填充给定的数组xs与此列表中最多len个元素,在位置开始。10 def distinct: List[A]建立从列表中没有任何重复的元素的新列表。11 def drop(n: Int): List[A]返回除了第n个的所有元素。12 def dropRight(n: Int): List[A]返回除了最后的n个的元素13 def dropWhile(p: (A) => Boolean): List[A]丢弃满足谓词的元素最长前缀。14 def endsWith[B](that: Seq[B]): Boolean测试列表是否使用给定序列结束。15 def equals(that: Any): Booleanequals方法的任意序列。比较该序列到某些其他对象。16 def exists(p: (A) => Boolean): Boolean测试谓词是否持有一些列表的元素。17 def filter(p: (A) => Boolean): List[A]返回列表满足谓词的所有元素。18 def forall(p: (A) => Boolean): Boolean测试谓词是否持有该列表中的所有元素。19 def foreach(f: (A) => Unit): Unit应用一个函数f以列表的所有元素。20 def head: A选择列表的第一个元素21 def indexOf(elem: A, from: Int): Int经过或在某些起始索引查找列表中的一些值第一次出现的索引。22 def init: List[A]返回除了最后的所有元素23 def intersect(that: Seq[A]): List[A]计算列表和另一序列之间的多重集交集。24 def isEmpty: Boolean测试列表是否为空25 def iterator: Iterator[A]创建一个新的迭代器中包含的可迭代对象中的所有元素26 def last: A返回最后一个元素27 def lastIndexOf(elem: A, end: Int): Int之前或在一个给定的最终指数查找的列表中的一些值最后一次出现的索引28 def length: Int返回列表的长度29 def map[B](f: (A) => B): List[B]通过应用函数以g这个列表中的所有元素构建一个新的集合30 def max: A查找最大的元素31 def min: A查找最小元素32 def mkString: String显示列表的字符串中的所有元素33 def mkString(sep: String): String显示的列表中的字符串中使用分隔串的所有元素34 def reverse: List[A]返回新列表,在相反的顺序元素35 def sorted[B >: A]: List[A]根据排序对列表进行排序36 def startsWith[B](that: Seq[B], offset: Int): Boolean测试该列表中是否包含给定的索引处的给定的序列37 def sum: A概括这个集合的元素38 def tail: List[A]返回除了第一的所有元素39 def take(n: Int): List[A]返回前n个元素40 def takeRight(n: Int): List[A]返回最后n个元素41 def toArray: Array[A]列表以一个数组变换42 def toBuffer[B >: A]: Buffer[B]列表以一个可变缓冲器转换43 def toMap[T, U]: Map[T, U]此列表的映射转换44 def toSeq: Seq[A]列表的序列转换45 def toSet[B >: A]: Set[B]列表到集合变换46 def toString(): String列表转换为字符串 Set方法 序号 方法及描述1 def +(elem: A): Set[A]为集合添加新元素,x并创建一个新的集合,除非元素已存在2 def -(elem: A): Set[A]移除集合中的元素,并创建一个新的集合3 def contains(elem: A): Boolean如果元素在集合中存在,返回 true,否则返回 false。4 def &(that: Set[A]): Set[A]返回两个集合的交集5 def &~(that: Set[A]): Set[A]返回两个集合的差集6 def +(elem1: A, elem2: A, elems: A*): Set[A]通过添加传入指定集合的元素创建一个新的不可变集合7 def ++(elems: A): Set[A]合并两个集合8 def -(elem1: A, elem2: A, elems: A*): Set[A]通过移除传入指定集合的元素创建一个新的不可变集合9 def addString(b: StringBuilder): StringBuilder将不可变集合的所有元素添加到字符串缓冲区10 def addString(b: StringBuilder, sep: String): StringBuilder将不可变集合的所有元素添加到字符串缓冲区,并使用指定的分隔符11 def apply(elem: A)检测集合中是否包含指定元素12 def count(p: (A) => Boolean): Int计算满足指定条件的集合元素个数13 def copyToArray(xs: Array[A], start: Int, len: Int): Unit复制不可变集合元素到数组14 def diff(that: Set[A]): Set[A]比较两个集合的差集15 def drop(n: Int): Set[A]]返回丢弃前n个元素新集合16 def dropRight(n: Int): Set[A]返回丢弃最后n个元素新集合17 def dropWhile(p: (A) => Boolean): Set[A]从左向右丢弃元素,直到条件p不成立18 def equals(that: Any): Booleanequals 方法可用于任意序列。用于比较系列是否相等。19 def exists(p: (A) => Boolean): Boolean判断不可变集合中指定条件的元素是否存在。20 def filter(p: (A) => Boolean): Set[A]输出符合指定条件的所有不可变集合元素。21 def find(p: (A) => Boolean): Option[A]查找不可变集合中满足指定条件的第一个元素22 def forall(p: (A) => Boolean): Boolean查找不可变集合中满足指定条件的所有元素23 def foreach(f: (A) => Unit): Unit将函数应用到不可变集合的所有元素24 def head: A获取不可变集合的第一个元素25 def init: Set[A]返回所有元素,除了最后一个26 def intersect(that: Set[A]): Set[A]计算两个集合的交集27 def isEmpty: Boolean判断集合是否为空28 def iterator: Iterator[A]创建一个新的迭代器来迭代元素29 def last: A返回最后一个元素30 def map[B](f: (A) => B): immutable.Set[B]通过给定的方法将所有元素重新计算31 def max: A查找最大元素32 def min: A查找最小元素33 def mkString: String集合所有元素作为字符串显示34 def mkString(sep: String): String使用分隔符将集合所有元素作为字符串显示35 def product: A返回不可变集合中数字元素的积。36 def size: Int返回不可变集合元素的数量37 def splitAt(n: Int): (Set[A], Set[A])把不可变集合拆分为两个容器,第一个由前 n 个元素组成,第二个由剩下的元素组成38 def subsetOf(that: Set[A]): Boolean如果集合A中含有子集B返回 true,否则返回false39 def sum: A返回不可变集合中所有数字元素之和40 def tail: Set[A]返回一个不可变集合中除了第一元素之外的其他元素41 def take(n: Int): Set[A]返回前 n 个元素42 def takeRight(n: Int):Set[A]返回后 n 个元素43 def toArray: Array[A]将集合转换为数组44 def toBuffer[B >: A]: Buffer[B]返回缓冲区,包含了不可变集合的所有元素45 def toList: List[A]返回 List,包含了不可变集合的所有元素46 def toMap[T, U]: Map[T, U]返回 Map,包含了不可变集合的所有元素47 def toSeq: Seq[A]返回 Seq,包含了不可变集合的所有元素48 def toString(): String返回一个字符串,以对象来表示 Map方法 序号 方法及描述1 def ++(xs: Map[(A, B)]): Map[A, B]返回一个新的 Map,新的 Map xs 组成2 def -(elem1: A, elem2: A, elems: A*): Map[A, B]返回一个新的 Map, 移除 key 为 elem1, elem2 或其他 elems。3 def --(xs: GTO[A]): Map[A, B]返回一个新的 Map, 移除 xs 对象中对应的 key4 def get(key: A): Option[B]返回指定 key 的值5 def iterator: Iterator[(A, B)]创建新的迭代器,并输出 key/value 对6 def addString(b: StringBuilder): StringBuilder将 Map 中的所有元素附加到StringBuilder,可加入分隔符7 def addString(b: StringBuilder, sep: String): StringBuilder将 Map 中的所有元素附加到StringBuilder,可加入分隔符8 def apply(key: A): B返回指定键的值,如果不存在返回 Map 的默认方法10 def clone(): Map[A, B]从一个 Map 复制到另一个 Map11 def contains(key: A): Boolean如果 Map 中存在指定 key,返回 true,否则返回 false。12 def copyToArray(xs: Array[(A, B)]): Unit复制集合到数组13 def count(p: ((A, B)) => Boolean): Int计算满足指定条件的集合元素数量14 def default(key: A): B定义 Map 的默认值,在 key 不存在时返回。15 def drop(n: Int): Map[A, B]返回丢弃前n个元素新集合16 def dropRight(n: Int): Map[A, B]返回丢弃最后n个元素新集合17 def dropWhile(p: ((A, B)) => Boolean): Map[A, B]从左向右丢弃元素,直到条件p不成立18 def empty: Map[A, B]返回相同类型的空 Map19 def equals(that: Any): Boolean如果两个 Map 相等(key/value 均相等),返回true,否则返回false20 def exists(p: ((A, B)) => Boolean): Boolean判断集合中指定条件的元素是否存在21 def filter(p: ((A, B))=> Boolean): Map[A, B]返回满足指定条件的所有集合22 def filterKeys(p: (A) => Boolean): Map[A, B]返回符合指定条件的的不可变 Map23 def find(p: ((A, B)) => Boolean): Option[(A, B)]查找集合中满足指定条件的第一个元素24 def foreach(f: ((A, B)) => Unit): Unit将函数应用到集合的所有元素25 def init: Map[A, B]返回所有元素,除了最后一个26 def isEmpty: Boolean检测 Map 是否为空27 def keys: Iterable[A]返回所有的key/p>28 def last: (A, B)返回最后一个元素29 def max: (A, B)查找最大元素30 def min: (A, B)查找最小元素31 def mkString: String集合所有元素作为字符串显示32 def product: (A, B)返回集合中数字元素的积。33 def remove(key: A): Option[B]移除指定 key34 def retain(p: (A, B) => Boolean): Map.this.type如果符合满足条件的返回 true35 def size: Int返回 Map 元素的个数36 def sum: (A, B)返回集合中所有数字元素之和37 def tail: Map[A, B]返回一个集合中除了第一元素之外的其他元素38 def take(n: Int): Map[A, B]返回前 n 个元素39 def takeRight(n: Int): Map[A, B]返回后 n 个元素40 def takeWhile(p: ((A, B)) => Boolean): Map[A, B]返回满足指定条件的元素41 def toArray: Array[(A, B)]集合转数组42 def toBuffer[B >: A]: Buffer[B]返回缓冲区,包含了 Map 的所有元素43 def toList: List[A]返回 List,包含了 Map 的所有元素44 def toSeq: Seq[A]返回 Seq,包含了 Map 的所有元素45 def toSet: Set[A]返回 Set,包含了 Map 的所有元素46 def toString(): String返回字符串对象","tags":[{"name":"scala","slug":"scala","permalink":"http://www.seanxia.cn/tags/scala/"},{"name":"方法","slug":"方法","permalink":"http://www.seanxia.cn/tags/%E6%96%B9%E6%B3%95/"}]},{"title":"Scala编程语言","date":"2017-10-14T16:00:00.000Z","path":"大数据/dd52d826.html","text":"Scala是一门多范式的静态编程语言,一种类似 Java 的编程语言,设计初衷是实现可伸缩的语言、并集成面向对象编程和函数式编程的各种特性。 Scala介绍 Scala的6大特性 1、Java 和 scala 可以混编 2、类型推测(自动推测类型) 3、并发和分布式(Actor) 4、特质,特征(类似 java 中 interfaces 和 abstract结合) 5、模式匹配(类似 java switch) 6、高阶函数 Scala安装使用 1、windows安装,配置环境变量 官网下载 scala2.10:http://www.scala-lang.org/download/2.10.4.html 下载好后安装。双击msi包安装,记住安装的路径。 配置环境变量(和配置jdk一样) 新建SCALA_HOME 上个步骤完成后,编辑Path变量,在后面追加:;%SCALA_HOME%\\bin 打开cmd,输入:scala 看是否显示版本号,确定是否安装成功 2、eclipse 配置scala插件 下载插件(一定要对应eclipse版本下载) http://scala-ide.org/download/prev-stable.html 下载好zip包后,解压如下: 将features和plugins两个文件夹拷贝到eclipse安装目录中的” dropins/scala”目录下。进入dropins,新建scala文件夹,将两个文件夹拷贝到“dropins/scala”下 3、 scala ide 下载网址:http://scala-ide.org/download/sdk.html 4、idea 中配置scala插件(推荐使用) 打开idea,close项目后,点击Configure->Plugins 搜索scala,点击Install安装 设置jdk,打开Project Structure,点击new 选择安装好的jdk路径 创建scala项目,配置scala sdk(Software Development Kit) Scala基础语法 数据类型 Scala数据类型结构图 几个特殊的数据类型 变量和常量的声明 定义变量或者常量的时候,也可以写上返回的类型,一般省略,如:val a:Int = 10 常量不可再赋值 /** * 定义变量和常量 * 变量 :用 var 定义 ,可修改 * 常量 :用 val 定义,不可修改 */ var name = "zhangsan" println(name) name ="lisi" println(name) val gender = "m"// gender = "m"//错误,不能给常量再赋值 类和对象 注意点: 一行结束,不需要分号。如果一行里有多个语句,则之间用分号隔开。 var 修饰变量,val修饰常量 ,完整写法: var a:Int = 1 object 可以理解为是单例,相当于java的工具类,它里面的方法都是static静态的。 object 不可传参, class可以传参,相当于默认构造器。 重写构造器,一定得先调用默认构造器 class 自动实现了 getter ,setter方法 new 对象时,除了class里的方法不执行,其他的都执行。 在scala的同一个文件里,class和object名字一样的时候,则它们互为伴生类和伴生对象。class称为object对象的伴生类,object 称为class类的伴生对象,他们可以直接访问对方的私有变量 创建类 class Person{ val name = "zhangsan" val age = 18 def sayName() = { "my name is "+ name }} 创建对象 class Person{ val name = "zhangsan" val age = 18 def sayName() = { "my name is "+ name }} 伴生类和伴生对象 class Person(xname :String , xage :Int){ var name = Person.name val age = xage var gender = "m" def this(name:String,age:Int,g:String){ this(name,age) gender = g } def sayName() = { "my name is "+ name }}object Person { val name = "zhangsanfeng" def main(args: Array[String]): Unit = { val person = new Person("wagnwu",10,"f") println(person.age); println(person.sayName()) println(person.gender) }} 条件判断和循环 If else /** * if else */val age =18 if (age < 18 ){ println("no allow")}else if (18<=age&&age<=20){ println("allow with other")}else{ println("allow self")} for, while, do…while 1、to和until 的用法(不带步长,带步长区别) /** * to和until * 例: * 1 to 10 返回1到10的Range数组,包含10 * 1 until 10 返回1到10 Range数组 ,不包含10 */ println(1 to 10 )//打印 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 println(1.to(10))//与上面等价,打印 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 println(1 to (10 ,2))//步长为2,从1开始打印 ,1,3,5,7,9 println(1.to(10, 2)) println(1 until 10 ) //不包含最后一个数,打印 1,2,3,4,5,6,7,8,9 println(1.until(10))//与上面等价 println(1 until (10 ,3 ))//步长为2,从1开始打印,打印1,4,7 2、创建for循环 /** * for 循环 */ for( i <- 1 to 10 ){ println(i) } 3、创建多层for循环 //可以分号隔开,写入多个list赋值的变量,构成多层for循环 //scala中 不能写count++ count-- 只能写count+ var count = 0; for(i <- 1 to 10; j <- 1 until 10){ println("i="+ i +", j="+j) count += 1 } println(count);//例子: 打印九九乘法表 for (i <- 1 to 9){ for (j <- 1 to i){ print(j + "*" + i + "=" + j * i + "\\t") } println() } 4、for循环中可以加条件判断,分号隔开 //可以在for循环中加入条件判断 for(i<- 1 to 10 ;if (i%2) == 0 ;if (i == 4) ){ println(i)} 5、while循环,while(){},do {}while() //将for中的符合条件的元素通过yield关键字返回成一个集合 val list = for(i <- 1 to 10 ; if(i > 5 )) yield i for( w <- list ){ println(w)} /** * while 循环 */ var index = 0 while(index < 100 ){ println("第"+index+"次while 循环") index += 1 } index = 0 do{ index +=1 println("第"+index+"次do while 循环")}while(index <100 ) Scala函数 注意点: 函数定义语法 用def来定义 省略return的时候,函数自动回将最后一行的表达式的值,作为返回值 函数的返回类型可以省略,因为scala可以类型自动推断 如果函数有retrun,则必须写返回类型。 函数定义的时候,如果去掉 = ,则相当于,函数就将返回值去掉,既无返回值。 {}里的代码,如果只有一行,则可以省略{} 函数的参数传进来的时候,是一个常量val,不能修改。 Scala函数的定义 有参函数 无参函数 def fun (a: Int , b: Int ) : Unit = { println(a+b) }fun(1,1) def fun1 (a : Int , b : Int)= a+b println(fun1(1,2)) 递归函数 /** * 递归函数 * 5的阶乘 */ def fun2(num :Int) :Int= { if(num ==1) num else num * fun2(num-1) } print(fun2(5)) 包含参数默认值的函数 默认值的函数中,如果传入的参数个数与函数定义相同,则传入的数值会覆盖默认值。 如果不想覆盖默认值,传入的参数个数小于定义的函数的参数,则需要指定参数名称。 /** * 包含默认参数值的函数 * 注意: * 1.默认值的函数中,如果传入的参数个数与函数定义相同,则传入的数值会覆盖默认值 * 2.如果不想覆盖默认值,传入的参数个数小于定义的函数的参数,则需要指定参数名称 */ def fun3(a :Int = 10,b:Int) = { println(a+b) } fun3(b=2) 可变参数个数的函数 多个参数用逗号分开 /** * 可变参数个数的函数 * 注意:多个参数逗号分开 */def fun4(elements :Int*)={ var sum = 0; for(elem <- elements){ sum += elem } sum}println(fun4(1,2,3,4)) 匿名函数 1、有参匿名函数 2、无参匿名函数 3、有返回值的匿名函数 可以将匿名函数返回给val定义的值 匿名函数不能显式声明函数的返回类型 /** * 匿名函数 * 1.有参数匿名函数 * 2.无参数匿名函数 * 3.有返回值的匿名函数 * 注意: * 可以将匿名函数返回给定义的一个变量 *///有参数匿名函数val value1 = (a : Int) => { println(a)}value1(1)//无参数匿名函数val value2 = ()=>{ println("我爱尚学堂")}value2()//有返回值的匿名函数val value3 = (a:Int,b:Int) =>{ a+b}println(value3(4,4)) 嵌套函数 /** * 嵌套函数 * 例如:嵌套函数求5的阶乘 */def fun5(num:Int)={ def fun6(a:Int,b:Int):Int={ if(a == 1){ b }else{ fun6(a-1,a*b) } } fun6(num,1)}println(fun5(5)) 偏应用函数 偏应用函数是一种表达式,不需要提供函数需要的所有参数,只需要提供部分,或不提供所需参数。 /** * 偏应用函数 */def log(date :Date, s :String)= { println("date is "+ date +",log is "+ s)}val date = new Date()log(date ,"log1")log(date ,"log2")log(date ,"log3")//想要调用log,以上变化的是第二个参数,可以用偏应用函数处理val logWithDate = log(date,_:String)logWithDate("log11")logWithDate("log22")logWithDate("log33") 高阶函数 函数的参数是函数,或者函数的返回类型是函数,或者函数的参数和函数的返回类型是函数的函数。 函数的参数是函数 函数的返回是函数 函数的参数和函数的返回是函数 /** * 高阶函数 * 函数的参数是函数/或者函数的返回是函数/或者函数的参数和返回都是函数 *///函数的参数是函数def hightFun(f : (Int,Int) =>Int, a:Int ) : Int = { f(a,100)}def f(v1 :Int,v2: Int):Int = { v1+v2}println(hightFun(f, 1))//函数的返回是函数//1,2,3,4相加def hightFun2(a : Int,b:Int) : (Int,Int)=>Int = { def f2 (v1: Int,v2:Int) :Int = { v1+v2+a+b } f2}println(hightFun2(1,2)(3,4))//函数的参数是函数,函数的返回是函数def hightFun3(f : (Int ,Int) => Int) : (Int,Int) => Int = { f} println(hightFun3(f)(100,200))println(hightFun3((a,b) =>{a+b})(200,200))//以上这句话还可以写成这样//如果函数的参数在方法体中只使用了一次 那么可以写成_表示println(hightFun3(_+_)(200,200)) 柯里化函数 可以理解为高阶函数的简化 /** * 柯里化函数 */ def fun7(a :Int,b:Int)(c:Int,d:Int) = { a+b+c+d } println(fun7(1,2)(3,4)) Scala字符串 1、String 2、StringBuilder 可变 3、string操作方法举例 比较:equals 比较忽略大小写:equalsIgnoreCase indexOf:如果字符串中有传入的assci码对应的值,返回下标 /** * String && StringBuilder */ val str = "abcd" val str1 = "ABCD" println(str.indexOf(97)) println(str.indexOf("b")) println(str==str1) /** * compareToIgnoreCase * * 如果参数字符串等于此字符串,则返回值 0; * 如果此字符串小于字符串参数,则返回一个小于 0 的值; * 如果此字符串大于字符串参数,则返回一个大于 0 的值。 * */ println(str.compareToIgnoreCase(str1)) val strBuilder = new StringBuilder strBuilder.append("abc")// strBuilder.+('d') strBuilder+ 'd'// strBuilder.++=("efg") strBuilder++= "efg" // strBuilder.+=('h') strBuilder+= 'h' strBuilder.append(1.0) strBuilder.append(18f) println(strBuilder) 4、Scala的string方法总结 其他详细的String方法:请见关于Scala中的方法详解 Scala集合 数组 1、创建数组 new Array [Int] (10) 赋值:arr(0) = xxx Array [String] (“s1”,“s2”,“s3”) 2、数组遍历 for foreach 3、创建一维数组和二维数组 4、数组中方法举例 Array.concat:合并数组 Array.fill(5)(“shsxt”):创建初始值的定长数组 创建两种方式: /** * 创建数组两种方式: * 1.new Array[String](3) * 2.直接Array */ //创建类型为Int 长度为3的数组 val arr1 = new Array[Int](3) //创建String 类型的数组,直接赋值 val arr2 = Array[String]("s100","s200","s300") //赋值 arr1(0) = 100 arr1(1) = 200 arr1(2) = 300 遍历两种方式: /** * 遍历两种方式 */for(i <- arr1){ println(i)}arr1.foreach(i => { println(i)})for(s <- arr2){ println(s)}arr2.foreach { x => println(x) } 5、创建二维数组 /** * 创建二维数组和遍历 */val arr3 = new Array[Array[String]](3)arr3(0)=Array("1","2","3")arr3(1)=Array("4","5","6")arr3(2)=Array("7","8","9")for(i <- 0 until arr3.length){ for(j <- 0 until arr3(i).length){ print(arr3(i)(j)+" ") } println()}var count = 0for(arr <- arr3 ;i <- arr){ if(count%3 == 0){ println() } print(i+" ") count +=1 }arr3.foreach { arr => { arr.foreach { println }}}val arr4 = Array[Array[Int]](Array(1,2,3),Array(4,5,6))arr4.foreach { arr => { arr.foreach(i => { println(i) })}}println("-------")for(arr <- arr4;i <- arr){ println(i)} 6、数组方法总结 其他有关数组中的方法请见:关于Scala中的方法详解 List 1、创建List val list = List(1,2,3,4) Nil长度为0的list 2、List遍历 for、foreach 3、List方法举例 filter:过滤元素 count:计算符合条件的元素个数 map:对元素操作 flatmap:压扁扁平,先map再flat //创建 val list = List(1,2,3,4,5) //遍历 list.foreach { x => println(x)}// list.foreach { println} //filter val list1 = list.filter { x => x>3 } list1.foreach { println} //count val value = list1.count { x => x>3 } println(value) //map val nameList = List( "hello shsxt", "hello xasxt", "hello shsxt" ) val mapResult:List[Array[String]] = nameList.map{ x => x.split(" ") } mapResult.foreach{println} //flatmap val flatMapResult : List[String] = nameList.flatMap{ x => x.split(" ") } flatMapResult.foreach { println } 4、List方法总结 其他有关List中的方法请见:关于Scala中的方法详解 Set 1、创建Set 注意:set集合会自动去重 2、Set遍历 for、foreach 3、Set方法举例 交集:intersect 、& 差集:diff、&~ 子集:subsetOf 最大:max 最小:min 转成数组:toList 转成字符串:mkString(“~”) //创建 val set1 = Set(1,2,3,4,4) val set2 = Set(1,2,5) //遍历 //注意:set会自动去重 set1.foreach { println}for(s <- set1){ println(s) } println("*******")/** * 方法举例 */ //交集val set3 = set1.intersect(set2)set3.foreach{println}val set4 = set1.&(set2)set4.foreach{println}println("*******")//差集set1.diff(set2).foreach { println }set1.&~(set2).foreach { println }//子集set1.subsetOf(set2)//最大值println(set1.max)//最小值println(set1.min)println("****")//转成数组,listset1.toArray.foreach{println}println("****")set1.toList.foreach{println}//mkStringprintln(set1.mkString)println(set1.mkString("\\t")) 4、Set方法举例 其他有关Set中的方法请见:关于Scala中的方法详解 Map 1、Map创建 Map(1 -> “seanxia”) Map(1,“seanxia”) 注意:创建map时,相同的key被后面的相同的key顶替掉,只保留一个。 val map = Map( "1" -> "seanxia", 2 -> "seanxia", (3,"seanxia")) 2、获取Map的值 map.get(“1“).get map.getOrElse(100, “no value”):如果map中没有对应项,赋值为getOrElse传的值。 //获取值 println(map.get("1").get)// val result = map.get(1).getOrElse("no value") val result = map.getOrElse(100, "no value") println(result) 3、遍历Map for,foreach //map遍历for(x <- map){ println("====key:"+x._1+",value:"+x._2)}map.foreach(f => { println("key:"+ f._1+" ,value:"+f._2)}) 4、遍历key map.keys //遍历keyval keyIterable = map.keyskeyIterable.foreach { key => { println("key:"+key+", value:"+map.get(key).get)} } 5、遍历value map.value //遍历valueval valueIterable = map.valuesvalueIterable.foreach { value => { println("value: "+ value)} } 6、合并map ++ 例:map1.++(map2) 注意:合并map会将map中的相同key的value替换 //合并mapval map1 = Map( (1,"a"), (2,"b"), (3,"c") )val map2 = Map( (1,"aa"), (2,"bb"), (2,90), (4,22), (4,"dd"))map1.++(map2).foreach(println) 7、map中的方法举例 filter:过滤,留下符合条件的记录 count:统计符合条件的记录数 contains:map中是否包含某个key exist:符合条件的记录存不存在 /** * map方法 *///countval countResult = map.count(p => { p._2.equals("shsxt")})println(countResult)//filtermap.filter(_._2.equals("shsxt")).foreach(println)//containsprintln(map.contains(2)) //existprintln(map.exists(f =>{ f._2.equals("xasxt") })) 8、map方法总结 其他有关Map中的方法请见:关于Scala中的方法详解 元组 1、元组定义 与列表一样,与列表不同的是元组可以包含不同类型的元素。元组的值是通过将单个的值包含在圆括号中构成的。 2、创建元组与取值 val tuple = new Tuple (1) 可以使用new val tuple2 = Tuple (1,2) 可以不使用new,也可以直接写成val tuple3 = (1,2,3) 取值用”._XX” 可以获取元组中的值 注意:tuple最多支持22个参数 //创建,最多支持22个val tuple = new Tuple1(1)val tuple2 = Tuple2("zhangsan",2)val tuple3 = Tuple3(1,2,3)val tuple4 = (1,2,3,4)val tuple18 = Tuple18(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18)val tuple22 = new Tuple22(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)//使用println(tuple2._1 + "\\t"+tuple2._2)val t = Tuple2((1,2),("zhangsan","lisi"))println(t._1._2) 3、元组的遍历 tuble.productIterator 得到迭代器,进而遍历 //遍历val tupleIterator = tuple22.productIteratorwhile(tupleIterator.hasNext){ println(tupleIterator.next())} 4、swap,toString方法 注意:swap元素翻转,只针对二元组 /** * 方法 *///翻转,只针对二元组println(tuple2.swap)//toStringprintln(tuple3.toString()) trait 特性 1、概念理解 Scala Trait(特征) 相当于 Java 的接口,实际上它比接口还功能强大。 与接口不同的是,它还可以定义属性和方法的实现 一般情况下Scala的类可以继承多个Trait,从结果来看就是实现了多重继承。Trait(特征) 定义的方式与类类似,但它使用的关键字是 trait。 2、trait中带属性带方法实现 注意: 继承的多个trait中如果有同名的方法和属性,必须要在类中使用“override”重新定义。 trait中不可以传参数 trait Read { val readType = "Read" val gender = "m" def read(name:String){ println(name +" is reading") }}trait Listen { val listenType = "Listen" val gender = "m" def listen(name:String){ println(name + " is listenning") }}class Person() extends Read with Listen{ override val gender = "f"}object test { def main(args: Array[String]): Unit = { val person = new Person() person.read("zhangsan") person.listen("lisi") println(person.listenType) println(person.readType) println(person.gender) }} 3、trait中带方法不实现 object Lesson_Trait2 { def main(args: Array[String]): Unit = { val p1 = new Point(1,2) val p2 = new Point(1,3) println(p1.isEqule(p2)) println(p1.isNotEqule(p2)) }}trait Equle{ def isEqule(x:Any) :Boolean def isNotEqule(x : Any) = { !isEqule(x) }}class Point(x:Int, y:Int) extends Equle { val xx = x val yy = y def isEqule(p:Any) = { p.isInstanceOf[Point] && p.asInstanceOf[Point].xx==xx } } 模式匹配match 1、概念理解 Scala 提供了强大的模式匹配机制,应用也非常广泛。一个模式匹配包含了一系列备选项,每个都开始于关键字 case。 每个备选项都包含了一个模式及一到多个表达式。箭头符号 => 隔开了模式和表达式。 2、代码及注意点 模式匹配不仅可以匹配值还可以匹配类型 从上到下顺序匹配,如果匹配到则不再往下匹配 都匹配不上时,会匹配到case _ ,相当于default match 的最外面的”{ }”可以去掉看成一个语句 object Lesson_Match { def main(args: Array[String]): Unit = { val tuple = Tuple6(1,2,3f,4,"abc",55d) val tupleIterator = tuple.productIterator while(tupleIterator.hasNext){ matchTest(tupleIterator.next()) } } /** * 注意点: * 1.模式匹配不仅可以匹配值,还可以匹配类型 * 2.模式匹配中,如果匹配到对应的类型或值,就不再继续往下匹配 * 3.模式匹配中,都匹配不上时,会匹配到 case _ ,相当于default */ def matchTest(x:Any) ={ x match { case x:Int=> println("type is Int") case 1 => println("result is 1") case 2 => println("result is 2") case 3=> println("result is 3") case 4 => println("result is 4") case x:String => println("type is String")// case x :Double => println("type is Double") case _ => println("no match") } } } 样例类(case classes) 1、概念理解 使用了case关键字的类定义就是样例类(case classes),样例类是种特殊的类。 实现了类构造参数的getter方法(构造参数默认被声明为val),当构造参数是声明为var类型的,它将帮你实现setter和getter方法。 样例类默认帮你实现了toString,equals,copy和hashCode等方法。 样例类可以new, 也可以不用new 2、例子:结合模式匹配的代码 case class Person1(name:String,age:Int)object Lesson_CaseClass { def main(args: Array[String]): Unit = { val p1 = new Person1("zhangsan",10) val p2 = Person1("lisi",20) val p3 = Person1("wangwu",30) val list = List(p1,p2,p3) list.foreach { x => { x match { case Person1("zhangsan",10) => println("zhangsan") case Person1("lisi",20) => println("lisi") case _ => println("no match") } } } }} Actor Model 1、概念理解 Actor Model是用来编写并行计算或分布式系统的高层次抽象(类似java中的Thread)让程序员不必为多线程模式下共享锁而烦恼。 2、Actor的特征 ActorModel是消息传递模型,基本特征就是消息传递。 消息发送是异步的,非阻塞的。 消息一旦发送成功,不能修改。 Actor之间传递时,自己决定决定去检查消息,而不是一直等待,是异步非阻塞的。 3、例:Actor简单例子发送接收消息 import scala.actors.Actorclass myActor extends Actor{ def act(){ while(true){ receive { case x:String => println("save String ="+ x) case x:Int => println("save Int") case _ => println("save default") } } }}object Lesson_Actor { def main(args: Array[String]): Unit = { //创建actor的消息接收和传递 val actor =new myActor() //启动 actor.start() //发送消息写法 actor ! "I love you !" }} 4、例:Actor与Actor之间通信 case class Message(actor:Actor,msg:Any)class Actor1 extends Actor{ def act(){ while(true){ receive{ case msg :Message => { println("I sava msg! = "+ msg.msg) msg.actor!"I love you too !" } case msg :String => println(msg) case _ => println("default msg!") } } }}class Actor2(actor :Actor) extends Actor{ actor ! Message(this,"I love you !") def act(){ while(true){ receive{ case msg :String => { if(msg.equals("I love you too !")){ println(msg) actor! "could we have a date !" } } case _ => println("default msg!") } } }}object Lesson_Actor2 { def main(args: Array[String]): Unit = { val actor1 = new Actor1() actor1.start() val actor2 = new Actor2(actor1) actor2.start() }} Scala Demo 单词统计:WordCount import org.apache.spark.SparkConfimport org.apache.spark.SparkContextimport org.apache.spark.rdd.RDDimport org.apache.spark.rdd.RDD.rddToPairRDDFunctionsobject WordCount { def main(args: Array[String]): Unit = { val conf = new SparkConf() conf.setMaster("local").setAppName("WC") val sc = new SparkContext(conf) val lines :RDD[String] = sc.textFile("./words.txt") val word :RDD[String] = lines.flatMap{lines => { lines.split(" ") }} val pairs : RDD[(String,Int)] = word.map{ x => (x,1) } val result = pairs.reduceByKey{(a,b)=> {a+b}} result.sortBy(_._2,false).foreach(println) //简化写法 lines.flatMap { _.split(" ")}.map { (_,1)}.reduceByKey(_+_).foreach(println) }}","tags":[{"name":"scala","slug":"scala","permalink":"http://www.seanxia.cn/tags/scala/"},{"name":"静态语言","slug":"静态语言","permalink":"http://www.seanxia.cn/tags/%E9%9D%99%E6%80%81%E8%AF%AD%E8%A8%80/"}]},{"title":"Redis存储系统","date":"2017-10-13T16:00:00.000Z","path":"大数据/890ab287.html","text":"Redis是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。拥有丰富的支持主流语言的客户端,C、C++、Python、Erlang、R、C#、Java、PHP、Objective\u0002C、Perl、Ruby、Scala、Go、JavaScript。 Redis的特点 1、数据结构丰富 Redis 虽然也是键值对数据库,但是和 Memcached 不同的是,Redis 的值支持多种类型的数据结构,不仅可以是字符串,同时还提供散列(hash),列表(list),集合(sets),有序集合(sorted sets)等数据结构。 通过选用不同的数据结构,用户可以使用 Redis 解决各式各样的问题。 数据库有两种,一种硬盘数据库,一种内存数据库。 硬盘是把值储存在硬盘,典型的是 SQL 数据库。在内存里面就存储一下索引,当硬盘数据库想访问硬盘的值 时,它先在内存里面找到索引,然后在找值,问题是什么,在读取和写入硬盘的时候,如果读写比较多的时候,它会把硬盘 IO 堵死。 至于内存数据库,它会直接把值放到内存里面,内存数据库就直接把值取到,风一样的感觉,读写数据的时候都不会受到硬盘 IO 速度的限制,所以速度极快。 2、数据的持久化 Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。 3、数据的备份 Redis 支持数据的备份,即 master-slave 模式的数据备份。 Redis的安装 下载安装包,redis-3.2.9.tar.gz 网址:http://www.redis.cn/download.html 第一步:依赖软件安装 yum -y install gcc tcl -y 第二步: 进入解压 redis 的文件夹 第三步: 执行 mkae 命令 第四步: 执行 make install 第五步: 修改 redis 的配置文件 redis.config ( 先备份一个原厂配置文件) 进入 myredis 文件中修改运行模式为后台运行,daemonize 修改成 yes 第六步:启动 redis 服务器,使用修改后配置文件的位置 命令: redis-server 配置文件的地址 如果修改 redis.conf,采用 redis 默认的 redis.conf 文件,redis 默认只能通过 127.0.0.1:6379 这个地址访问,这样就只能在本机上操作了,如果想要远程操作就不可行了。 这里需要修改 redis.conf 这个配置文件,在配置文件中添加相应的 ip 地址,这里假如添加 ip 地址:192.168.110.4,只需在 redis.conf 这个配置文件中 bind 127.0.0.1 后面追加: bind 127.0.0.1 192.168.110.4 第七步:客户端连接:在命令行 输入 cd /usr/local/bin 进入这个目录 命令行输入: ./redis-cli 客户端命令格式:redis-cli –h host –p port Redis的使用 Redis key 值是二进制安全的,这意味着可以用任何二进制序列作为 key值,从形如”foo”的简单字符串到一个 JPEG 文件的内容都可以。空字符串也是有效 key 值。 切换数据库 select databaseid 默认共有 16 个实例库,登录时是 ID 为 0 的数据库,总共有 16 个 Key操作 keys pattern 查找所有符合给定模式 pattern 的 key 。 exists key 检查给定 key 是否存在。 expire key seconds 为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除。 move key db 将当前数据库的 key 移动到给定的数据库 db 当中。如果当前数据库(源数据库)和给定数据库(目标数据库)有相同名字的给定 key ,或者 key 不存在于当前数据库,那么 MOVE 没有任何效果。 ttl key 以秒为单位,返回给定 key 的剩余生存时间 type key 返回 key 所储存的值的类型。 del key [key …] 删除给定的一个或多个 key 。不存在的 key 会被忽略。 String操作 字符串是一种最基本的 Redis 值类型。Redis 字符串是二进制安全的,这意味着一个 Redis 字符串能包含任意类型的数据。 set key value [EX seconds] [PX milliseconds] [NX|XX] EX 设置过期时间,秒,等同于 SETEX key seconds value PX 设置过期时间,毫秒,等同于 PSETEX key milliseconds value NX 键不存在,才能设置,等同于 SETNX key value XX 键存在时,才能设置 将字符串值 value 关联到 key 。 如果 key 已经持有其他值, SET 就覆写旧值,无视类型。 对于某个原本带有生存时间(TTL)的键来说, 当 SET 命令成功在这个键上执行时, 这个键原有的 TTL 将被清除。 get key 返回 key 所关联的字符串值。如果 key 不存在那么返回特殊值 nil 。 假如 key 储存的值不是字符串类型,返回一个错误,因为 GET 只能用于处理字符串值。 append key value 如果 key 已经存在并且是一个字符串, APPEND 命令将 value 追加到 key 原来的值的末尾。 如果 key 不存在, APPEND 就简单地将给定 key 设为 value ,就像执行 SET key value 一样。 strlen key 返回 key 所储存的字符串值的长度。 当 key 储存的不是字符串值时,返回一个错误。 incr key 将 key 中储存的数字值增一。 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行INCR 操作。 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 incrby key increment 将 key 所储存的值加上增量 increment 。 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行INCRBY 命令。 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 decr key 将 key 中储存的数字值减一。 decrby key decrement 将 key 所储存的值减去减量 decrement 。 getrange key start end 返回 key 中字符串值的子字符串。 字符串的截取范围由 start 和end 两个偏移量决定(包括 start 和 end 在内)。 负数偏移量表示从字符串最后开始计数, -1 表示最后一个字符, -2 表示倒数第二个,以此类推。 setrange key offset value 用 value 参数覆写(overwrite)给定 key 所储存的字符串值,从偏移量 offset 开始。 不存在的 key 当作空白字符串处理。 setex key seconds value 将值 value 关联到 key ,并将 key 的生存时间设为 seconds。 如果 key 已经存在, SETEX 命令将覆写旧值。 这个命令类似于以下两个命令:SET key value expire key seconds # 设置生存时间 不同之处是, SETEX 是一个原子性(atomic)操作,关联值和设置生存时间两个动作会在同一时间内完成,该命令在 Redis 用作缓存时,非常实用。 setnx key value 将 key 的值设为 value ,当且仅当 key 不存在。 若给定的 key 已经存在,则 SETNX 不做任何动作。 SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。 mget key [key …] 返回所有(一个或多个)给定 key 的值。 如果给定的 key 里面,有某个 key 不存在,那么这个 key 返回特殊值 nil 。因此,该命令永不失败。 mset key value [key value …] 同时设置一个或多个 key-value 对。 如果某个给定 key 已经存在,那么 MSET 会用新值覆盖原来的旧值,如果这不是你所希望的效果,请考虑使用 MSETNX 命令:它只会在所有给定 key 都不存在的情况下进行设置操作。 msetnx key value [key value …] 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。 即使只有一个给定 key 已存在, MSETNX 也会拒绝执行所有给定 key 的设置操作。 MSETNX 是原子性的,因此它可以用作设置多个不同 key 表示不同字段(field)的唯一性逻辑对象(unique logic object),所有字段要么全被设置,要么全不被设置。 List操作 lpush key value [value …] 将一个或多个值 value 插入到列表 key 的表头 如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表头 : 比如说 , 对空列表 mylist 执行命令 LPUSH mylist a b c ,列表的值将是 c b a ,这等同于原子性地 执行 LPUSH mylist a 、 LPUSH mylist b 和 LPUSH mylist c 三个命令。 rpush key value [value …] 将一个或多个值 value 插入到列表 key 的表尾(最右边)。 如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表尾 : 比如对一个空列表 mylist 执行 RPUSH mylist a b c ,得出的结果列表为 a b c ,等同于执行命令 RPUSH mylist a 、 RPUSH mylist b 、 RPUSH mylist c 。 lrange key start stop 返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定。 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。 lpop key 移除并返回列表 key 的头元素。 rpop key 移除并返回列表 key 的尾元素。 lindex key index 返回列表 key 中,下标为 index 的元素。 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 llen key 返回列表 key 的长度。 如果 key 不存在,则 key 被解释为一个空列表,返回 0 . lrem key count value 根据参数 count 的值,移除列表中与参数 value 相等的元素。 count 的值可以是以下几种: count > 0 : 从表头开始向表尾搜索,移除与 value 相等的元素,数量为 count 。 count < 0 : 从表尾开始向表头搜索,移除与 value 相等的元素,数量为 count 的绝对值。 count = 0 : 移除表中所有与 value 相等的值。 ltrim key start stop 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。 举个例子 , 执行命令 LTRIM list 0 2 , 表示只保留列表 list 的前三个元素,其余元素全部删除。 rpoplpush source destination 命令 RPOPLPUSH 在一个原子时间内,执行以下两个动作: 将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端。 将 source 弹出的元素插入到列表 destination,作为 destination 列表的的头元素。 举个例子,你有两个列表 source 和 destination,source 列表有元素 a, b, c , destination 列表有元素 x, y, z ,执行 RPOPLPUSH sourcedestination 之后,source 列表包含元素 a, b , destination 列表包含元素 c, x, y, z ,并且元素 c 会被返回给客户端。 lset key index value 将列表 key 下标为 index 的元素的值设置为 value 。 当 index 参数超出范围,或对一个空列表( key 不存在)进行 LSET 时,返回一个错误。 linsert key BEFORE|AFTER pivot value 将值 value 插入到列表 key 当中,位于值 pivot 之前或之后。 当 pivot 不存在于列表 key 时,不执行任何操作。 当 key 不存在时, key 被视为空列表,不执行任何操作。 Set操作 sadd key member [member …] 将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。 假如 key 不存在,则创建一个只包含 member 元素作成员的集合。 smembers key 返回集合 key 中的所有成员。 不存在的 key 被视为空集合。 sismember key member 判断 member 元素是否集合 key 的成员。 scard key 返回集合 key 的基数(集合中元素的数量)。 srem key member [member …] 移除集合 key 中的一个或多个 member 元素 , 不存在的 member 元素会被忽略。 spop key (抽奖场景) 移除并返回集合中的一个随机元素。 如果只想获取一个随机元素,但不想该元素从集合中被移除的话,可以使用 SRANDMEMBER 命令。 smove source destination member 将 member 元素从 source 集合移动到 destination 集合。 SMOVE 是原子性操作。 如果 source 集合不存在或不包含指定的 member 元素,则 SMOVE 命令不执行任何操作,仅返回 0 。否则, member 元素从 source 集合中被移除,并添加到 destination 集合中去。 当 destination 集合已经包含 member 元素时,SMOVE命令只是简单地将 source 集合中的 member 元素删除。 当 source 或 destination 不是集合类型时,返回一个错误。 sdiff key [key …] 求差集:从第一个 key 的集合中去除其他集合和自己的交集部分 sinter key [key …] (微博求共同关注场景) 返回一个集合的全部成员,该集合是所有给定集合的交集。 不存在的 key 被视为空集。 当给定集合当中有一个空集时,结果也为空集(根据集合运算定律)。 sunion key [key …] 返回一个集合的全部成员,该集合是所有给定集合的并集。 不存在的 key 被视为空集。 Sorted set 操作 类似 Sets,但是每个字符串元素都关联到一个叫 score 浮动数值。里面的元素总是通过 score 进行着排序,所以不同的是,它是可以检索的一系列元素。注意:在 set 基础上,加上 score 值,之前 set 是key value1 value2…. 现在 Zset 是 key score1 value1 score2 value2 zadd key score member [[score member] [score member] …] 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。 zrange key start stop [WITHSCORES] 返回有序集 key 中,指定区间内的成员。 其中成员的位置按 score 值递减(从小到大)来排列。 zrevrange key start stop [WITHSCORES]( 音乐排行榜场景) 返回有序集 key 中,指定区间内的成员。 其中成员的位置按 score 值递减(从大到小)来排列。 zrem key member [member …] 移除有序集 key 中的一个或多个成员,不存在的成员将被忽略。 zremrangebyscore key min max 移除有序集 key 中,所有 score 值介于 min 和 max 之间,(包括等于 min 或 max)的成员。 zscore key member 返回有序集 key 中,成员 member 的 score 值。 zcard key 返回有序集 key 的基数。 zcount key min max 返回有序集 key 中, score 值在 min 和 max 之间(默认包括 score 值等于 min 或 max)的成员的数量。 zrank key member 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递增(从小到大)顺序排列。 Hash操作(散列) KV 模式不变,但是 V 是一个键值对。 hset key field value 将哈希表 key 中的域 field 的值设为 value 。 hget key field 返回哈希表 key 中给定域 field 的值。 hmset key field value [field value …] 同时将多个 field-value (域-值)对设置到哈希表 key 中。 此命令会覆盖哈希表中已存在的域。 hmget key field [field …] 返回哈希表 key 中,一个或多个给定域的值。 hgetall key 返回哈希表 key 中,所有的域和值。 hkeys key 返回哈希表 key 中的所有域。 hvals key 返回哈希表 key 中所有域的值。 hsetnx key field value 将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在。 若域 field 已经存在,该操作无效。 hexists key field 查看哈希表 key 中,给定域 field 是否存在。 hdel key field [field …] 删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。 hincrby key field increment 为哈希表 key 中的域 field 的值加上增量 increment 。 增量也可以为负数,相当于对给定域进行减法操作。 hincrbyfloat key field increment 增加浮点数。 场景:用户维度统计 统计数包括:关注数、粉丝数、喜欢商品数、发帖数 用户为 Key,不同维度为 Field,Value 为统计数。 Redis的持久化 Redis 提供了多种不同级别的持久化方式。 RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot)。 AOF 持久化记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。 AOF 文件中的命令全部以 Redis 协议的格式来保存,新命令会被追加到文件的末尾。 Redis 还可以在后台对 AOF 文件进行重写(rewrite),使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小。 Redis 还可以同时使用 AOF 持久化和 RDB 持久化。 在这种情况下, 当 Redis 重启时, 它会优先使用 AOF 文件来还原数据集, 因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。 你甚至可以关闭持久化功能,让数据只在服务器运行时存在。 RDB(Redis DataBase) Rdb:在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的 snapshot 快照,它恢复时就是将快照文件直接读到内存里。 Redis 会单独的创建(fork) 一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程结束了,再用这个临时文件替换上次持久化的文件。整个过程中,主进程是不进行任何 IO 操作,这就确保了极高的性能,如果需要进行大规模的数据恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方法要比 AOF 方式更加的高效。RDB 的缺点是最后一次持久化后的数据可能丢失。 Fork 的作用是复制一个与当前进程一样的进程,新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。 Rdb 保存的是 dump.rdb 文件 RDB save 操作 Rdb 是整个内存的压缩的 snapshot,RDB 的数据结构,可以配置符合快照触发条件,默认的是 1 分钟内改动 1 万次,或者 5 分钟改动 10 次,或者是 15 分钟改动一次。 Save 禁用:如果想禁用 RDB 持久化的策略,只要不设置任何save 指令,或者是给 save 传入一个空字符串参数也可以。 save 指令:即刻保存操作对象 手动触发 RDB 快照 Save:save 时只管保存,其他不管,全部阻塞。 Bgsave:redis 会在后台进行快照操作,快照操作的同时还可以响应客户端的请求,可以通过 lastsave 命令获取最后一次成功执行快照的时间。 如何停止 静态停止:将配置文件里的 RDB 保存规则改为 save “” 动态停止 : AOF(Append Only File) 以日志的形式来记录每个写操作,将 redis 执行过的所有写指令记录下来(读操作不记录)。只许追加文件但不可以改写文件,redis 启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次完成数据恢复工作。 === APPEND ONLY MODE == 开启 aof :appendonly yes (默认是 no) AOF 策略 Appendfsync 参数: **Always: **每次发生数据变更会被立即记录到磁盘,性能较差但数据完整性较好。 Everysec: 出厂默认推荐,异步操作,每秒记录,不到一秒宕机,有数据丢失 **No:**从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择。因为操作系统只会等内存满了才会溢写,如果没满宕机,数据就会丢失。 Rewrite概念:AOF 采用文件追加方式,文件会越来越来大为避免出现此种情况,新增了重写机制,aof 文件的大小超过所设定的阈值时,redis 就会自动 aof 文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令 bgrewirteaof。 **重写原理:**aof 文件持续增长而大时,会 fork 出一条新进程来将文件重写(也就是先写临时文件最后再 rename),遍历新进程的内存中的数据,每条记录有一条 set 语句,重写 aof 文件的操作,并没有读取旧的的 aof 文件,而是将整个内存的数据库内容用命令的方式重写了一个新的 aof 文件,这点和快照有点类似。 **触发机制:**redis 会记录上次重写的 aof 的大小,默认的配置当 aof 文件为上次 rewrite 后大小的一倍且文件大于 64M 触发。 no-appendfsync-on-rewrite no : 重写时是否可以运用 Appendfsync 用默认no 即可,保证数据安全 auto-aof-rewrite-percentage: 倍数 设置基准值 auto-aof-rewrite-min-size: 设置基准值大小 AOF 优点 使用 AOF 持久化会让 Redis 变得非常耐久:你可以设置不同的 fsync 策略,比如无 fsync ,每秒钟一次 fsync ,或者每次执行写入命令时 fsync 。AOF 的默认策略为每秒钟 fsync 一次,在这种配置下,Redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据( fsync 会在后台线程执行,所以主线程可以继续努力地处理命令请求)。 AOF 文件是一个只进行追加操作的日志文件(append only log), 因此对AOF 文件的写入不需要进行 seek , 即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机,等),redis-check-aof 工具也可以轻易地修复这种问题。 Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。 AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写,那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。 AOF 缺点 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。 备份 Redis 数据 一定要备份你的数据库! 磁盘故障, 节点失效, 诸如此类的问题都可能让你的数据消失不见, 不进行备份是非常危险的。 Redis 对于数据备份是非常友好的, 因为你可以在服务器运行的时候对RDB 文件进行复制: RDB 文件一旦被创建, 就不会进行任何修改。 当服务器要创建一个新的 RDB 文件时, 它先将文件的内容保存在一个临时文件里 面, 当临时文件写入完毕时, 程序才使用 rename(2) 原子地用临时文件替换原来的 RDB 文件。 这也就是说, 无论何时, 复制 RDB 文件都是绝对安全的。 建议: 创建一个定期任务(cron job), 每小时将一个 RDB 文件备份到一个文件夹, 并且每天将一个 RDB 文件备份到另一个文件夹。确保快照的备份都带有相应的日期和时间信息, 每次执行定期任务脚本时, 使用 find 命令来删除过期的快照: 比如说, 你可以保留最近 48 小时内的每小时快照, 还可以保留最近一两个月的每日快照。 至少每天一次, 将 RDB 备份到你的数据中心之外, 或者至少是备份到你运行 Redis 服务器的物理机器之外。 Redis主从复制 Redis 支持简单且易用的主从复制(master-slave replication)功能, 该功能可以让从服务器(slave server)成为主服务器(masterserver)的精确复制品 。 以下是关于 Redis 复制功能的几个重要方面: Redis 使用异步复制。 从 Redis 2.8 开始, 从服务器会以每秒一次的频率向主服务器报告复制流(replication stream)的处理进度。 一个主服务器可以有多个从服务器。 不仅主服务器可以有从服务器, 从服务器也可以有自己的从服务器,多个从服务器之间可以构成一个图状结构。 复制功能不会阻塞主服务器: 即使有一个或多个从服务器正在进行初次同步, 主服务器也可以继续处理命令请求。 复制功能也不会阻塞从服务器: 只要在 redis.conf 文件中进行了相应的设置, 即使从服务器正在进行初次同步, 服务器也可以使用旧版本的数据集来处理命令查询。 不过, 在从服务器删除旧版本数据集并载入新版本数据集的那段时间内, 连接请求会被阻塞。 你还可以配置从服务器, 让它在与主服务器之间的连接断开时, 向客户端发送一个错误。 复制功能可以单纯地用于数据冗余(data redundancy), 也可以通过让多个从服务器处理只读命令请求来提升扩展性(scalability): 比如说, 繁重的 SORT 命令可以交给附属节点去运行。 从服务器配置 配置一个从服务器非常简单, 只要在配置文件中增加以下的这一行就可以了: slaveof 192.168.110.4 6379 另外一种方法是调用 SLAVEOF 命令,输入主服务器的 IP 和端口,然后同步就会开始 127.0.0.1:6379> SLAVEOF 192.168.110.4 6379 只读从服务器 从 Redis 2.6 开始, 从服务器支持只读模式, 并且该模式为从服务器的默认模式。 只读模式由 redis.conf 文件中的 slave-read-only 选项控制, 也可以通过 CONFIG SET 命令来开启或关闭这个模式。 只读从服务器会拒绝执行任何写命令, 所以不会出现因为操作失误而将数据不小心写入到了从服务器的情况。 另外,对一个从属服务器执行命令 SLAVEOF NO ONE 将使得这个从属服务器关闭复制功能,并从从属服务器转变回主服务器,原来同步所得的数据集不会被丢弃。 利用『 SLAVEOF NO ONE 不会丢弃同步所得数据集』这个特性,可以在主服务器失败的时候,将从属服务器用作新的主服务器,从而实现无间断运行。 从服务器相关配置: 如果主服务器通过 requirepass 选项设置了密码, 那么为了让从服务器的同步操作可以顺利进行, 我们也必须为从服务器进行相应的身份验证设置。 对于一个正在运行的服务器, 可以使用客户端输入以下命令: config set masterauth < password > 要永久地设置这个密码, 那么可以将它加入到配置文件中: masterauth < password > 主服务器只在有至少 N 个从服务器的情况下,才执行写操作 从 Redis 2.8 开始, 为了保证数据的安全性,可以通过配置, 让主服务器只在有至少 N 个当前已连接从服务器的情况下, 才执行写命令。 不过, 因为 Redis 使用异步复制, 所以主服务器发送的写数据并不一定会被从服务器接收到, 因此, 数据丢失的可能性仍然是存在的。 以下是这个特性的运作原理: 从服务器以每秒一次的频率 PING 主服务器一次, 并报告复制流的处理情况。 主服务器会记录各个从服务器最后一次向它发送 PING 的时间。 用户可以通过配置, 指定网络延迟的最大值 min-slaves-max-lag , 以及执行写操作所需的至少从服务器数量 min-slaves-to-write 。 如果至少有 min-slaves-to-write 个从服务器, 并且这些服务器的延迟值都少于 min-slaves-max-lag 秒, 那么主服务器就会执行客户端请求的写操作。 另一方面, 如果条件达不到 min-slaves-to-write 和 min-slaves-max-lag 所指定的条件, 那么写操作就不会被执行, 主服务器会向请求执行写操作的客户端返回一个错误。 以下是这个特性的两个选项和它们所需的参数: min-slaves-to-write < number of slaves > min-slaves-max-lag < number of seconds > Redis-sentinel( 哨兵) Redis 的 Sentinel 系统用于管理多个 Redis 服务器,该系统执行以下三个任务: 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时,Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为新主服务器的从服务器; 当客户端试图连接失效的主服务器时,集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效的主服务器。 Redis Sentinel 是一个分布式系统, 你可以在一个架构中运行多个 Sentinel 进程, 这些进程使用流言协议(gossipprotocols) 来接收关于主服务器是否下线的信息, 并使用投票协议(agreement protocols)来决定是否执行自动故障迁移,以及选择哪个从服务器作为新的主服务器。 虽然 Redis Sentinel 释出为一个单独的可执行文件 redis-sentinel , 但实际上它只是一个运行在特殊模式下的 Redis 服务器, 你可以在启动一个普通 Redis 服务器时通过给定 --sentinel 选项来启动 Redis Sentinel 。 启动 Sentinel 对于 redis-sentinel 程序, 可以用以下命令来启动Sentinel 系统: redis-sentinel /path/sentinel.conf 对于 redis-server 程序, 你可以用以下命令来启动一个运行在 Sentinel 模式下的 Redis 服务器: redis-server /path/sentinel.conf --sentinel 两种方法都可以启动一个 Sentinel 实例。 启动 Sentinel 实例必须指定相应的配置文件, 系统会使用配置文件来保存 Sentinel 的当前状态, 并在 Sentinel 重启时通过载入配置文件来进行状态还原。如果启动 Sentinel 时没有指定相应的配置文件, 或者指定的配置文件不可写(not writable), 那么 Sentinel 会拒绝启动。 配置 Sentinel Redis 源码中包含了一个名为 sentinel.conf 的文件,这个文件是一个带有详细注释的 Sentinel 配置文件示例。 运行一个 Sentinel 所需的最少配置如下所示: # 制定了 Sentinel 去监视的主服务器名称、IP、端口、Sentinel投票个数sentinel monitor mymaster 192.168.110.4 6379 2# 指定了 Sentinel 认为服务器已经断线所需的毫秒数sentinel down-after-milliseconds mymaster 30000# 故障迁移的时间,默认180ssentinel failover-timeout mymaster 180000# 指定了在执行故障转移时,最多可以有多少个从服务器同时对新的主服务器进行同步sentinel parallel-syncs mymaster 1# 主服务器无密码时,关闭保护模式或者配上本机ipprotected-mode no / bind 本机 注意:主服务器无密码时,记得在 sentinel 配置里配上 bind 本机 ip ,或者关掉保护模式 protected-mode no 第一行配置指示 Sentinel 去监视一个名为 mymaster 的主服务器, 这个主服务器的 IP 地址为 127.0.0.1 , 端口 号为 6379 , 而将这个主服务器判断为失效至少需要 2 个Sentinel 同意 (只要同意 Sentinel 的数量不达标,自动故障迁移就不会执行)。 不过要注意, 无论你设置要多少个 Sentinel 同意才能判断一个服务器失效, 一个 Sentinel 都需要获得系统中多数(majority) Sentinel 的支持, 才能发起一次自动故障迁移。换句话说, 在只有少数(minority) Sentinel 进程正常运作的情况下, Sentinel 是不能执行自动故障迁移的。 其他选项的基本格式如下: sentinel <选项的名字> <主服务器的名字> <选项的值> 各个选项的功能如下: down-after-milliseconds 选项指定了 Sentinel 认为服务器已经断线所需的毫秒数。如果服务器在给定的毫秒数之内, 没有返回 Sentinel 发送的 PING 命令的回复, 或者返回一个错误, 那么 Sentinel 将这个服务器标记为主观下线(subjectively down,简称 SDOWN )。不过只有一个 Sentinel 将服务器标记为主观下线并不一定会引起服务器的自动故障迁移: 只有在足够数量的 Sentinel 都将一个服务器标记为主观下线之后, 服务器才会被标记为客观下线,这时自动故障迁移才会执行。将服务器标记为客观下线所需的 Sentinel 数量由对主服务器的配置决定。 parallel-syncs 选项指定了在执行故障转移时, 最多可以有多少个从服务器同时对新的主服务器进行同步, 这个数字越小, 完成故障转移所需的时间就越长。如果从服务器被设置为允许使用过期数据集(参见对 redis.conf 文件中对 slave-serve-stale-data 选项的说明),那么你可能不希望所有从服务器都在同一时间向新的主服务器发送同步请求, 因为尽管复制过程的绝大部分步骤都不会阻塞从服务器, 但从服务器在载入主服务器发来的 RDB 文件时, 仍然会造成从服务器在一段时间内不能处理命令请求: 如果全部从服务器一起对新的主服务器进行同步, 那么就可能会造成所有从服务器在短时间内全部不可用的情况出现。你可以通过将这个值设为 1 来保证每次只有一个从服务器处于不能处理命令请求的状态。 测试效果: 1、3台服务器已经都安装好Redis,并且做好主从复制。 2、分别在3台服务器中redis安装包中新建文件 mysentinel.conf,填入如上的 Sentinel 配置。 这里注意:守护进程一定要关掉。(3台都关) 3、分别启动3台服务器的redis-server,以及redis-sentinel。 4、关掉主服务器,查看其它服务的监听状态。 会发现30秒后,哨兵sentinel会重新选举新的master 192.168.110.5节点成为新的master。 注意:如果重新启动之前的主节点 192.168.110.4,那它依旧是slave,无法还原到master! Redis集群 集群简介 Redis 集群可以在多个 Redis 节点之间进行数据共享。 Redis 集群不支持那些需要同时处理多个键的 Redis 命令,因为执行这些命令需要在多个 Redis 节点之间移动数据, 并且在高负载的情况下, 这些命令将降低 Redis 集群的性能, 并导致不可预测的行为。比如:有 name1 和 name2 两个节点,命令 del name1 name2 要同时删除这个 key,就不能写在一起,得分开写。 Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。 Redis 集群提供了以下两个好处: 将数据自动切分(split)到多个节点的能力。 当集群中的一部分节点失效或者无法进行通讯时, 仍然可以继续处理命令请求的能力。 集群数据共享 Redis 集群使用**数据分片(sharding)**而非一致性哈希来实现: 一个 Redis 集群包含 16384 个哈希槽(hash slot),数据库中的每个键都属于这 16384 个哈希槽的其中一个, 集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16校验和 。 集群中的每个节点负责处理一部分哈希槽。 举个例子, 一个集群可以有三个哈希槽, 其中 节点 A 负责处理 0 号至 5500 号哈希槽。 节点 B 负责处理 5501 号至 11000 号哈希槽。 节点 C 负责处理 11001 号至 16384 号哈希槽。 这种将哈希槽分布到不同节点的做法使得用户可以很容易地向集群中添加或者删除节点。 比如说: 如果用户将新节点 D 添加到集群中, 那么集群只需要将节点A 、B 、 C 中的某些槽移动到节点 D 就可以了。 与此类似, 如果用户要从集群中移除节点 A , 那么集群只需要将节点 A 中的所有哈希槽移动到节点 B 和节点 C , 然后再移除空白(不包含任何哈希槽)的节点 A 就可以了。 因为将一个哈希槽从一个节点移动到另一个节点不会造成节点阻塞, 所以无论是添加新节点还是移除已存在节点, 又或者改变某个节点包含的哈希槽数量, 都不会造成集群下线。 集群的主从复制 为了使得集群在一部分节点下线或者无法与集群的大多数节点进行通讯的情况下, 仍然可以正常运作, Redis 集群对节点使用了主从复制功能: 集群中的每个节点都有 1 个至 N 个复制品,其中一个复制品为主节点, 而其余的 N-1 个复制品为从节点。 在之前列举的节点 A 、B 、C 的例子中, 如果节点 B 下线了, 那么集群将无法正常运行, 因为集群找不到节点来处理 5501 号至 11000 号的哈希槽。 另一方面, 假如在创建集群的时候(或者至少在节点 B 下线之前), 我们为主节点 B 添加了从节点 B1 , 那么当主节点 B 下线的时候, 集群就会将 B1 设置为新的主节点, 并让它代替下线的主节点 B , 继续处理 5501 号至 11000 号的哈希槽, 这样集群就不会因为主节点 B 的下线而无法正常运作了。 不过如果节点 B 和 B1 都下线的话, Redis 集群还是会停止运作。 集群的一致性保证 Redis 集群不保证数据的强一致性(strong consistency):在特定条件下, Redis 集群可能会丢失已经被执行过的写命令。 使用异步复制是 Redis 集群可能会丢失写命令的其中一个原因。 考虑以下这个写命令的例子: 客户端向主节点 B 发送一条写命令。 主节点 B 执行写命令,并向客户端返回命令回复。 主节点 B 将刚刚执行的写命令复制给它的从节点 B1 、 B2 和B3 。 如你所见, 主节点对命令的复制工作发生在返回命令回复之后, 因为如果每次处理命令请求都需要等待复制操作完成的话, 那么主节点处理命令请求的速度将极大地降低 —— 我们必须在性能和一致性之间做出权衡。 Redis 集群另外一种可能会丢失命令的情况是, 集群出现网络分裂(network partition), 并且一个客户端与至少包括一个主节点在内的少数(minority)实例被孤立。 举个例子, 假设集群包含 A 、 B 、 C 、 A1 、 B1 、 C1六个节点, 其中 A 、B 、C 为主节点, 而 A1 、B1 、C1 分别为三个主节点的从节点, 另外还有一个客户端 Z1 。 假设集群中发生网络分裂, 那么集群可能会分裂为两方,大多数(majority)的一方包含节点 A 、C 、A1 、B1 和C1 , 而少数(minority)的一方则包含节点 B 和客户端Z1 。 在网络分裂期间, 主节点 B 仍然会接受 Z1 发送的写命令: 如果网络分裂出现的时间很短, 那么集群会继续正常运行; 但是, 如果网络分裂出现的时间足够长, 使得大多数一方将从节点 B1 设置为新的主节点, 并使用 B1 来代替原来的主节点 B ,那么 Z1 发送给主节点 B 的写命令将丢失。 注意, 在网络分裂出现期间, 客户端 Z1 可以向主节点 B发送写命令的最大时间是有限制的, 这一时间限制称为节点超时时间(node timeout), 是 Redis 集群的一个重要的配置选项: 对于大多数一方来说, 如果一个主节点未能在节点超时时间所设定的时限内重新联系上集群, 那么集群会将这个主节点视为下线, 并使用从节点来代替这个主节点继续工作。 对于少数一方, 如果一个主节点未能在节点超时时间所设定的时限内重新联系上集群, 那么它将停止处理写命令, 并向客户端报告错误。 集群搭建 要让集群正常运作至少需要 3 个主节点, 不过在刚开始试用集群功能时, 强烈建议使用六个节点: 其中三个为主节点,而其余三个则是各个主节点的从节点。 1、集群规划: 3 个主节点,3 个从节点。 2、新建配置文件(3台都要) 在redis安装包中新建文件夹 redis-cluster 在 redis-cluster 中新建分别以端口7000和7001命名新建两个文件夹 分别在7000和7001两个文件夹中新建配置文件 redis.conf(端口号要与文件夹对应) 配置文件 redis.conf port 7000/7001cluster-enabled yescluster-config-file nodes.confcluster-node-timeout 5000appendonly yesdaemonize yesprotected-mode no 3、启动 redis 服务 在每台机器的 7000 和 7001 文件夹下,使用命令 redis-server redis.conf 启动实例 4、安装 ruby 相关的程序 yum install rubyyum install rubygemsgem install redis-3.2.1.gem 5、执行命令创建集群 在主服务器 redis 的安装包的 src 目录下执行以下命令: ./redis-trib.rb create --replicas 1 ip1:7000 ip1:7001 Ip2:7000 ip2:7001 ip3:7000 ip3:7001 可以看到,集群为每个节点分配了相应的插槽。输入 yes 即可完成集群搭建。 6、测试结果 输入命令:redis-cli,发现无法连接 这是因为 redis 客户端默认的连接端口是 6379,但现在集群用的是 7000 和 7001 所以我们加上端口,如:redis-cli -p 7000 但是当我们给 redis 设置值的时候会发现有的值会报错 由图中可以看出 name 这个 key 被移动到了 ip 为192.168.110.5的节点上,这是因为 name 在进行 hash 之后取到的插槽值就处于192.168.110.5这个节点。 所以我们可以到192.168.110.5这个节点去设置 key 为 name 的值。 但是这样切来切去有没有觉得很麻烦。Redis为我们提供了一种可以自动进行切换节点的方法,只需要在连接客户端时加上参数:-c 我们会发现节点自动切换到了192.168.110.5这个节点上,是不是很方便!","tags":[{"name":"内存","slug":"内存","permalink":"http://www.seanxia.cn/tags/%E5%86%85%E5%AD%98/"},{"name":"redis","slug":"redis","permalink":"http://www.seanxia.cn/tags/redis/"}]},{"title":"关于ElasticSearch的优化方案","date":"2017-09-29T16:00:00.000Z","path":"大数据/53bfabca.html","text":"关于ElasticSearch的应用方案有很多,下面具体列举一些工作中常用到的几种。 1、调大系统的"最大打开文件数",建议 32K 甚至是 64K ulimit -a #查看ulimit -n 32000 #设置 2、修改配置文件调整 ES 的 JVM 内存大小 修改 bin/elasticsearch.in.sh 中 ES_MIN_MEM 和 ES_MAX_MEM 的大小,建议设置一样大,避免频繁的分配内存,根据服务器内存大小,一般分配 60%左右(默认 256M)。 3、设置 mlockall 来锁定进程的物理内存地址 避免交换(swapped)来提高性能 修改文件 conf/elasticsearch.yml bootstrap.mlockall: true 4、分片的设置,5-20 个比较合适 如果分片数过少或过多,都会导致检索比较慢。 分片数过多会导致检索时打开比较多的文件,另外也会导致多台服务器之间通讯。 分片数过少会导至单个分片索引过大,所以检索速度慢。 建议单个分片最多存储 20G 左右的索引数据,所以,分片数量=数据总量/20G。 5、副本的设置,2-3个比较合适 副本多的话,可以提升搜索的能力,但是如果设置很多副本的话也会对服务器造成额外的压力,因为需要同步数据。所以建议设置 2-3个即可。 如果在项目开始的时候需要批量入库大量数据的话,建议将副本数设置为 0 – 因为 es 在索引数据的时候,如果有副本存在,数据也会马上同步到副本中,这样会对 es 增加压力。待索引完成后将副本按需要改回来。这样可以提高索引效率 6、要定时对索引进行优化,不然 segment 越多,查询的性能就越差 索引量不是很大的话情况下可以将 segment 设置为 1。 shell操作:curl -XPOST 'http://192.168.110.4:9200/xss/_optimize?max_num_segments=1' java代码:client.admin().indices().prepareOptimize(“shsxt").setMaxNumSegments(1).get(); 7、删除文档 在 Lucene 中删除文档,数据不会马上在硬盘上除去,而是在 lucene 索引中产生一个.del 的文件,而在检索过程中这部分数据也会参与检索,lucene 在检索过程会判断是否删除了,如果删除了在过滤掉。这样也会降低检索效率。 所以可以执行清除删除文档: shell操作:curl -XPOST 'http://192.168.110.4:9200/elasticsearch/_optimize?only_expunge_deletes=true' java代码:client.admin().indices().prepareOptimize("elasticsearch").setOnlyExpungeDeletes(true).get(); 8、去掉 mapping 中_all 域 ElasticSearch 默认为每个被索引的文档都定义了一个特殊的域 ‘_all’,它自动包含被索引文档中一个或者多个域中的内容, 在进行搜索时,如果不指明要搜索的文档的域,ElasticSearch 则会去搜索_all 域。_all 带来搜索方便,其代价是增加了系统在索引阶段对 CPU 和存储空间资源的开销。 可以使用"_all" : {“enabled”:false} 开关禁用它。 Elasticsearch 使用经验之谈: 1、在使用 java 代码操作 es 集群的时候要保证本地使用的 es 的版本和集群上 es 的版本保持一致。 2、保证集群中每个节点的 JDK 版本和 es 配置一致。 3、Elasticsearch 的分片规则 elasticsearch 在建立索引时,根据 id 或 id,类型进行 hash, 得到 hash 值与该索引库的分片数量取余,取余的值即为存入的分片 ID。 具体源码为:根据 OperationRouting 类 generateShardId 方法进行分片。","tags":[{"name":"ElasticSearch","slug":"ElasticSearch","permalink":"http://www.seanxia.cn/tags/ElasticSearch/"},{"name":"优化","slug":"优化","permalink":"http://www.seanxia.cn/tags/%E4%BC%98%E5%8C%96/"}]},{"title":"Elasticsearch分布式搜索引擎","date":"2017-09-25T16:00:00.000Z","path":"大数据/f050b84d.html","text":"ES简介 Elasticsearch 是一个基于 Lucene 的实时的分布式搜索和分析 引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠, 快速,安装使用方便。基于 RESTful 接口。 Lucene 与 ES 关系 1、Lucene 只是一个库。想要使用它,你必须使用 Java 来作为开发语言并将其直接集成到你的应用中,更糟糕的是,Lucene 非常复杂,你需要深入了解检索的相关知识来理解它是如何工作的。 2、Elasticsearch 也使用 Java 开发并使用 Lucene 作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的 RESTful API 来隐藏 Lucene 的复杂性,从而让全文搜索变得简单。 Elasticsearch 与 Solr 对比 1、ElasticSearch查询速度更快。 2、Elasticsearch是分布式的。不需要其他组件,分发是实时的。 3、Elasticsearch 完全支持 Apache Lucene 的接近实时的搜索,处理多租户(multitenancy)不需要特殊配置,而 Solr 则需要更多的高级设置。 4、Elasticsearch 采用 Gateway 的概念,使得备份更加简单。 Elasticsearch 与关系型数据库对比 ES 与关系型数据库非常类似: REST 简介 REST 全称是 Resource Representational State Transfer,通俗来讲就是:资源在网络中以某种表现形式进行状态转移。分解开来: Resource:资源,即数据。比如 news,friends 等; Representational:某种表现形式,比如用 JSON,XML,JPEG 等; State Transfer:状态变化。通过 HTTP 动词实现。 它一种软件架构风格,而不是标准,只是提供了一组设计原则和约束条件。 它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。 **** Rest 操作 REST 的操作分为以下几种: GET:获取对象的当前状态; PUT:改变对象的状态; POST:创建对象; DELETE:删除对象; HEAD:获取头信息。 ES 内置的 REST 接口 CURL 命令 常见参数 -X:指定http请求的方法 -HEAD/GET/POST/PUT/DELETE -d :指定要传输的数据 常用操作 1、索引库的创建与删除 创建索引库 url -XPUT http://192.168.110.4:9200/xss/ #xss为创建索引库的名称 删除索引库 curl -XDELETE http://192.168.110.4:9200/xss/ 2、创建Document -d 后跟 Json 格式 pretty:把返回结果转换成便于识别的 Json 格式 curl -XPUT http://192.168.110.4:9200/xss/employee/1?pretty -d '{ "first_name" : "john", "last_name" : "smith", "age" : 25, "about" : "I love to go rock climbing", "address": "shanghai"}' 3、更新Document 局部更新,可以添加新字段或者更新已有字段(必须使用 POST) curl -XPOST http://192.168.110.4:9200/xss/employee/1/_update -d '{ "doc":{ "city":"beijing", "sex":"male" }}' 全局更新(使用PUT,与创建Document相同) curl -XPUT http://192.168.110.4:9200/xss/employee/1 -d '{"city":"beijing","car":"BMW"}' 注意:PUT 和 POST 都是新增,修改。PUT必须指定ID,所以PUT一般数据更新 。POST 可以指定ID,也可以不指定 做新增比较好。 4、普通查询索引 根据ID查询 curl -XGET http://192.168.110.4:9200/xss/employee/1?pretty curl后添加 -i 参数,能得到反馈头文件 curl -i XGET http://192.168.110.4:9200/xss/employee/1?pretty 检索文档中的一部分,只需要显示指定字段 curl -XGET http://192.168.110.4:9200/xss/employee/1?_source=name,age 查询所有 curl -XGET http://192.168.110.4:9200/xss/employee/_search?pretty 根据条件进行查询 curl -XGET http://192.168.110.4:9200/xss/employee/_search?q=last_name:smith 5、DSL 查询 DSL:Domain Specific Language – 领域特定语言 # 对单个field发起查询:matchcurl -XGET http://192.168.110.4:9200/xss/employee/_search?pretty -d '{"query": {"match": {"last_name":"smith"} }}'#对多个 field 发起查询:multi_matchcurl -XGET http://192.168.110.4:9200/xss/employee/_search?pretty -d '{"query": {"multi_match": { "query":"bin", "fields":["last_name","first_name"], "operator":"and" } }}'# 复合查询,must,must_not, shouldmust: ANDmust_not:NOTshould:ORcurl -XGET http://192.168.110.4:9200/xss/employee/_search?pretty -d '{"query": {"bool" : { "must" : {"match": {"first_name":"bin"} }, "must" : {"match": {"age":37} } } }}' – 举例:查询 first_name=bin 的,并且年龄不在 20 岁到 30 岁之间的 curl -XGET http://192.168.110.4:9200/xss/employee/_search -d '{"query": {"bool" : { "must" : {"term" : { "first_name" : "bin" } } , "must" : {"range": {"age" : { "from" : 30, "to" : 40 } } } } }}' 6、删除索引 curl -XDELETE http://192.168.110.4:9200/xss/employee/1?pretty 如果文档存在,es 会返回 200 ok 的状态码,found 属性值为 true,_version 属性的值+1 found 属性值为 false,但是_version 属性的值依然会+1,这个就是内部管理的一部分,它保证了我们在多个节点间的不同操作的顺序都被正确标记了。 注意:删除一个文档也不会立即生效,它只是被标记成已删除。 Elasticsearch 将会在你之后添加更多索引的时候才会在后台进行删除内容的清理。 ES安装部署 下载解压ES包 所有集群节点都要安装。 修改配置 config/elasticsearch.yml 文件 a)Cluster.name: my_cluster (同一集群要一样)b)Node.name:node-1 (同一集群要不一样)c)Network.Host: 192.168.1.194 (这里不能写127.0.0.1)d)防止脑裂的配置: discovery.zen.ping.multicast.enabled: false discovery.zen.ping_timeout: 120s client.transport.ping_timeout: 60s discovery.zen.ping.unicast.hosts: ["192.168.110.4","192.168.110.5","192.168.110.6"] 启动 ES_HOME/bin/elasticsearchES_HOME/bin/elasticsearch -d #后台运行 访问(默认端口9200) http://192.168.110.4:9200 ElasticSearch插件安装 head 通过 rest 请求,未免太过麻烦,而且也不够人性化。head能后帮助我们快捷查看 es 的运行状态以及数据的可视化工具。 1、安装:在线安装 bin目录下执行./plugin install mobz/elasticsearch-head 2、启动elasticsearch bin目录下执行./elasticsearch 3、访问(_plugin/head目录) 注意:访问的节点必须安装head,只需要安装一台即可。 http://192.168.110.6:9200/_plugin/head Kibana Kibana 是一个基于浏览器页面的ES前端展示工具,是为ES提供日志分析的web接口,可用它对日志进行高效的搜索、可视化、分析等操作。 1、解压安装 2、修改配置文件 config/kibana.yml的elasticsearch.url 属性,elasticsearch.url 指定任意节点都可以,他会自动进行内部广播。 Marvel Marvel 插件可以帮助使用者监控 elasticsearch 的运行状态,不过这个插件需要 license。安装完license后可以安装 marvel 的 agent,agent 会收集elasticsearch 的运行状态。 1、安装license和marvel-agent(3台都装) bin目录下执行./plugin install licensebin目录下执行./plugin install marvel-agent 2、安装Marvel(在kibana机器上安装) Kibana_home/bin/kibana plugin --install elasticsearch/marvel/late #远程安装,速度慢Kibana_home/bin/kibana plugin --install marvel --url file://Path #离线安装(推荐使用) 3、重启ES,启动Kibana bin/elasticsearchbin/kibana 4、页面访问(默认端口5601) 注意:与head插件相同,访问的节点必须安装Kibana,只需要安装一台即可。 http://192.168.110.6:5601 分词器集成 这里使用的是 IK 中文分词器。 1、下载IK分词器 https://github.com/medcl/elasticsearch-analysis-ik 2、安装步骤 在 elasticsearch 的 plugins目录下新建ik目录,将分词器包拷贝到ik目录下 通过unzip命令解压缩(第一次需要yum安装unzip) unzip elasticsearch-analysis-ik-1.8.0.zip 修改文件权限为普通用户(否则无法启动ES) 3、重启集群 注意:3台机器都要安装(因为添加数据和查询数据都要分词) 4、使用操作 创建索引库 curl -XPUT http://192.168.110.4:9200/ik 设置 mapping curl -XPOST http://192.168.110.4:9200/ik/ikType/_mapping -d'{ "properties": { "content": { "type": "string", "index":"analyzed", "analyzer": "ik_max_word", "search_analyzer": "ik_max_word" } }}' 插入数据 curl -XPOST http://192.168.110.4:9200/ik/ikType/1 -d'{"content":"美国留给伊拉克的是个烂摊子吗"}'curl -XPOST http://192.168.110.4:9200/ik/ikType/2 -d'{"content":"公安部:各地校车将享最高路权"}'curl -XPOST http://192.168.110.4:9200/ik/ikType/3 -d'{"content":"中韩渔警冲突调查:韩警平均每天扣1艘中国渔船"}'curl -XPOST http://192.168.110.4:9200/ik/ikType/4 -d'{"content":"中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首"}' 查询 curl -XGET http://192.168.110.4:9200/ik/ikType/_search?pretty -d'{ "query" : { "term" : { "content" : "中国" }} }' Elasticsearch 核心概念 cluster 代表一个集群,集群中有多个节点,其中有一个为主节点,这个主节点是可以通过选举产生的,主从节点是对于集群内部来说的。 es 的一个重要概念就是去中心化,字面上理解就是无中心节点,这是对于集群外部来说的,因为从外部来看 es 集群,在逻辑上是个整体,你与任何一 个节点的通信和与整个 es 集群通信是等价的。 主节点的职责是负责管理集群状态,包括管理分片的状态和副本的状态,以及节点的发现和删除。 只需要在同一个网段之内启动多个 es 节点,就可以自动组成一个集群。 默认情况下 es 会自动发现同一网段内的节点,自动组成集群。 集群状态查看 – http://192.168.110.4:9200/_cluster/health?pretty shards 代表索引分片,es 可以把一个完整的索引分成多个分片,这样的好处是可以把一个大的索引拆分成多个,分布到不同的节点上。构成分布式搜索。 分片的数量只能在索引创建前指定,并且索引创建后不能更改。 分片的数量可以在创建索引库的时候指定。 curl -XPUT '192.168.110.4:9200/test1/' -d'{"settings":{"number_of_shards":3}}' 默认是一个索引库有 5 个分片:number_of_shards: 5 replicas 代表索引副本,es 可以给索引设置副本,副本的作用一是提高系统的容错性,当某个节点某个分片损坏或丢失时可以从副本中恢复。二是提高 es 的查询效率,es 会自动对搜索请求进行负载均衡。 可以在创建索引库的时候指定。 curl -XPUT '192.168.110.4:9200/test2/' -d'{"settings":{"number_of_replicas":2}}' 默认是一个分片有 1 个副本 (总共有两片):number_of_replicas: 1 recovery 代表数据恢复或叫数据重新分布,es 在有节点加入或退出时会根据机器的负载对索引分片进行重新分配,挂掉的节点重新启动时也会进行数据恢复。 gateway 代表 es 索引的持久化存储方式,es 默认是先把索引存放到内存中,当内存满了时再持久化到硬盘。当这个 es 集群关闭再重新启动时就会从 gateway 中读取索引数据。es 支持多种类型的 gateway,有本地文件系统(默认),分布式文件系统,Hadoop 的 HDFS 和 amazon 的s3 云存储服务。 如果需要将数据落地到 hadoop 的 hdfs 需要先安装插件 elasticsearch/elasticsearch-hadoop。 discovery.zen 代表 es 的自动发现节点机制,es 是一个基于 p2p 的系统,它先通过广播寻找存在的节点,再通过多播协议来进行节点之间的通信,同时也支持点对点的交互。 **思考:**如果是不同网段的节点如何组成 es 集群 禁用自动发现机制 discovery.zen.ping.multicast.enabled: false 设置新节点被启动时能够发现的主节点列表 discovery.zen.ping.unicast.hosts: [“192.168.110.4", " 192.168.110.5", " 192.168.110.6"] Transport 代表 es 内部节点或集群与客户端的交互方式,默认内部是使用 tcp 协议进行交互,同时它支持 http 协议(json 格式)、thrift、servlet、 memcached、zeroMQ 等的传输协议(通过插件方式集成)。 ES 中的 settings 和 mappings settings:修改索引库默认配置 例如:分片数量,副本数量 curl -XGET http://192.168.110.4:9200/xss/_settings?pretty #获取当前分片及副本数量 curl -XPUT http://192.168.110.4:9200/helloword/ -d '{"settings": { "number_of_shards":3, #分片数量 "number_of_replicas":2 #副本数量 }}' Mapping:就是对索引库中索引的字段名称及其数据类型进行定义 类似于关系数据库中表建立时要定义字段名及其数据类型那样,不过 es 的 mapping 比数据库灵活很多,它可以动态添加字段。一般不需要指定 mapping 都可以,因为 es 会自动根据数据格式定义它的类型,如果你需要对某些字段添加特殊属性(如:定义使用其它分词器、是否分词、是否存储等),就必须手动添加 mapping。 查询索引库的 mapping 信息 curl -XGET http://192.168.110.4:9200/xss/employee/_mapping?pretty mappings 修改字段相关属性 例如:字段类型,使用哪种分词工具 Elasticsearch 的 java API 1、通过 TransportClient 这个接口,我们可以不启动节点就可以和es 集群进行通信, 它需要指定 es 集群中其中一台或多台机的 ip 地址和端口。 TransportClient client = new TransportClient()client.addTransportAddress(new InetSocketTransportAddress("host1", 9300))client.addTransportAddress(new InetSocketTransportAddress("host2", 9300)); 如果需要使用其他名称的集群(默认是 elasticsearch),需要如下设置: Settings settings = ImmutableSettings.settingsBuilder().put("cluster.name", "myClusterName").build(); TransportClientclient = new TransportClient(settings).addTransportAddress(new InetSocketTransportAddress("host1", 9300)); 2、通过 TransportClient 这个接口,自动嗅探整个集群的状态,es 会自动把集群中其它机器的 ip 地址 加到客户端中。 Settings settings = ImmutableSettings.settingsBuilder().put("client.transport.sniff", true).build(); TransportClient client = new TransportClient(settings).addTransportAddress(new InetSocketTransportAddress("host1", 9300)); 索引 index(四种 json,map,bean,es helpers) IndexResponse response = client.prepareIndex(“shsxt", "emp", "1").setSource().execute().actionGet(); 查询 get GetResponse response = client.prepareGet(“shsxt", "emp","1").execute().actionGet(); 更新 update 更新或者插入 upsert 删除 delete DeleteResponse response = client.prepareDelete(“shsxt", "employee", "1").execute().actionGet(); 总数 count long count = client.prepareCount(“shsxt").execute().get().getCount(); 3、批量操作 bulk 查询 search(SearchType) Elasticsearch 的查询 ES的搜索类型(四种): 1、query and fetch(速度最快,返回 N 倍数据量) 向索引的所有分片(shard)都发出查询请求,各分片返回的时候把元素文档(document)和计算后的排名信息一起返回。这种搜索方式是最快的。因为相比下面的几种搜索方式,这种查询方法只需要去shard 查询一次。但各个 shard 返回的结果的数量之和可能是用户要求的 size 的 n 倍。 2、query then fetch(默认的搜索方式) 如果你搜索时,没有指定搜索方式,就是使用的这种搜索方式。这种搜索方式,大概分两个步骤,第一步,先向所有的 shard 发出请求,各分片只返回排序和排名相关的信息(注意,不包括文档 document),然后按照各分片返回的分数进行重新排序和排名,取前 size个文档。然后进行第二步,去相关的 shard 取 document。这种方式返回的 document 与用户要求的 size 是相等的。 3、DFS query and fetch(更精确控制搜索打分) 这种方式比第一种方式多了一个初始化散发(initial scatter)步骤,有这一步可以更精确控制搜索打分和排名。 4、DFS query then fetch (最慢) 初始化散发其实就是在进行真正的查询之前,先把各个分片的词频率和文档频率收集一下,然后进行词搜索的时候,各分片依据全局的词频率和文档频率进行搜索和排名。显然如果使用 DFS_QUERY_THEN_FETCH 这种查询方式,效率是最低的,因为一个搜索,可能要请求3 次分片。但使用 DFS 方法,搜索精度应该是最高的。 总结: 从性能考虑 QUERY_AND_FETCH 是最快的, DFS_QUERY_THEN_FETCH 是最慢的。从搜索的准确度来说,DFS 要比非 DFS 的准确度更高。 查询: query – builder.setQuery(QueryBuilders.matchQuery("name", "test")) 分页: from/size – builder.setFrom(0).setSize(1) 排序: sort – builder.addSort("age", SortOrder.DESC) 过滤: filter – builder.setPostFilter(QueryBuilders.rangeQuery("age").from(1).to(19)) **高亮:**highlight 统计: facet(已废弃)使用 aggregations 替代– 根据字段进行分组统计– 根据字段分组,统计其他字段的值– size 设置为 0,会获取所有数据,否则,只会返回 10 条 Elasticsearch 的分页 与 SQL 使用 LIMIT 来控制单“页”数量类似,Elasticsearch 使用的是 from 以及 size 两个参数: size:每次返回多少个结果,默认值为 10 from:从哪条结果开始,默认值为 0 假设每页显示 5 条结果,那么 1 至 3 页的请求就是: GET /_search?size=5 GET /_search?size=5&from=5 GET /_search?size=5&from=10 **注意:**不要一次请求过多或者页码过大的结果,这么会对服务器造成很大的压力。因为它们会在返回前排序。一个请求会经过多个分片。每个分片都会生成自己的排序结果。然后再进行集中整理,以确保最终结果的正确性。 **timed_out:**告诉了我们查询是否超时。 curl -XGET http://localhost:9200/_search?timeout=10ms #es会在10ms之内返回查询内容 **注意:**timeout 并不会终止查询,它只是会在你指定的时间内返回当时已经查询到的数据,然后关闭连接。在后台,其他的查询可能会依旧继续, 尽管查询结果已经被返回了。 Elasticsearch 分片查询 **默认是 randomize across shards:**随机选取,表示随机的从分片中取数据。 **_local:**指查询操作会优先在本地节点有的分片中查询,没有的话再在其它节点查询。 **_primary:**指查询只在主分片中查询。 **_primary_first:**指查询会先在主分片中查询,如果主分片找不到(挂了), 就会在副本中查询。 **_only_node:**指在指定 id 的节点里面进行查询,如果该节点只有查询索引的部分分片,就只在这部分分片中查找,所以查询结果可能不完整。如 _only_node:123 在节点 id 为 123 的节点中查询。 **_prefer_node:nodeid:**优先在指定的节点上执行查询。 **_shards:0, 1, 2, 3, 4:**查询指定分片的数据。 Elasticsearch 中脑裂问题 所谓脑裂问题(类似于精神分裂),就是同一个集群中的不同节点,对于集群的状态有了不一样的理解。 **discovery.zen.minimum_master_nodes:**用于控制选举行为发生的最小集群节点数量。推荐设为大于1 的数值, 因为只有在 2 个以上节点的集群中,主节点才是有意义的。 正常情况下,集群中的所有的节点,应该对集群中 master 的选择是一致的,这样获得的状态信息也应该是一致的,不一致的状态信息,说明不同的节点对 maste 节点的选择出现了异常——也就是所谓的脑裂问题。这样的脑裂状态直接让节点失去了集群的正确状态,导致集群不能正常工作。 Elasticsearch 中脑裂产生的原因: 网络:由于是内网通信,网络通信问题造成某些节点认为 master 死掉,而另选 master 的可能性较小。 节点负载:由于 master 节点与 data 节点都是混合在一起的,所以当工作节点的负载较大时,导致对应的 ES 实例停止响应, 而这台服务器如果正充当着 master 节点的身份,那么一部分 节点就会认为这个 master 节点失效了,故重新选举新的节点, 这时就出现了脑裂;同时由于 data 节点上 ES 进程占用的内存较大,较大规模的内存回收操作也能造成 ES 进程失去响应。 Elasticsearch 中脑裂的解决方案: 主节点 node.master: truenode.data: false 从节点 node.master: falsenode.data: true 所有节点 discovery.zen.ping.multicast.enabled: false #关闭自动自动广播discovery.zen.ping.unicast.hosts: [“slave1”, “master” ,“slave2"] #指定广播的节点 Elasticsearch 的优化 关于 ElasticSearch 的优化详见:[关于ElasticSearch的优化方案][https://www.seanxia.cn/大数据/53bfabca.html]","tags":[{"name":"搜索引擎","slug":"搜索引擎","permalink":"http://www.seanxia.cn/tags/%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E/"},{"name":"分布式","slug":"分布式","permalink":"http://www.seanxia.cn/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"}]},{"title":"Flume框架","date":"2017-09-21T16:00:00.000Z","path":"大数据/93326c6d.html","text":"Flume简介 Flume 是一个分布式、可扩展、可靠、高可用的海量日志有效聚合及移动的框架。 官网:http://flume.apache.org/ 它通常用于log数据,支持在系统中定制各类数据发送方,用于收集数据。它具有可靠性和容错可调机制和许多故障转移和恢复机制。 Flume通常用于收集日志数据,但不限于收集日志,还可以收集其他的数据信息。 Flume组织架构 Flume的版本分为 OG 组织架构和 NG 组织架构,但原来的Flume OG到现在的Flume NG,进行了架构重构,并且现在NG版本完全不兼容原来的OG版本。 OG组织架构 Flume OG:Flume original generation 即 Flume 0.9.x版本,它采用三层架构,分别为agent,Collector和Master,所有的 agent 和 Collector 由 master 统一管理,master允许有多个(使用Zookeeper进行管理和负载均衡),避免单点故障问题。 NG组织架构 Flume NG:Flume next generation ,即Flume 1.x版本,只有 agent,由Source、Channel和 Sink组成。 Flume 经过架构重构后,Flume NG更像是一个轻量的小工具,非常简单,容易适应各种方式日志收集,并支持 failover和负载均衡。 Agent Agent 由source、channel、sink三大组件组成,如图: 1、Source Source负责接收event或通过特殊机制产生event,并将events批量的放到一个或多个Channel。 Source包含event驱动和轮询两种类型。 Source 有不同的类型,接受不同的数据格式。 1)与系统集成的Source:netcat、syslog。 2)自动生成事件的Source:exec 3)用于Agent和Agent之间的通信的IPC Source:avro、thrift。 Source必须至少和一个Channel关联。 2、Channel Channel位于Source和Sink之间,用于缓存进来的event。 当Sink成功的将event发送到下一跳的Channel或最终目的地,event才Channel中移除。 不同的Channel提供的持久化水平也是不一样的: 1)Memory Channel:volatile。 2)File Channel:基于WAL实现。 3)JDBC Channel:基于嵌入Database实现。 Channel支持事物,提供较弱的顺序保证。 Channel可以和任何数量的Source和Sink工作。 3、Sink Sink负责将event传输到下一跳或最终目的,成功完成后将event从Channel移除。 有不同类型的Sink: 1)存储event到最终目的的终端Sink。比如Logger,HDFS,HBase,Hive。 2)自动消耗的Sink。比如:Kafka。 3)用于Agent间通信的IPC sink:Avro。 Sink必须作用于一个确切的Channel。 4、Agent之Channel与Sink关系图 Flume的特性 1、数据可靠性(内部实现) 当节点出现故障时,日志能够被传送到其他节点上而不会丢失。 FFlume提供了三种级别的可靠性保障,所有的数据以event为单位传输,从强到弱依次分别为: **end-to-end:**收到数据agent首先将event写到磁盘上,当数据传送成功后,再删除;如果数据发送失败,可以重新发送。 **Store on failure:**这也是scribe采用的策略,当数据接收方crash(崩溃)时,将数据写到本地,待恢复后,继续发送。 **Best effort:**数据发送到接收方后,不会进行确认。 2、自身可扩展性 Flume采用了三层架构,分别为agent,collector和master,每一层均可以水平扩展。所有agent和 collector由master统一管理,使得系统容易监控和维护。master允许有多个(使用ZooKeeper进行管理和负载均衡),避免单点故障问题。【1.0自身agent实现扩展】 3、功能可扩展性 用户可以根据需要添加自己的agent。 Flume自带了很多组件,包括各种agent(file,syslog,HDFS等)。 Flume安装及使用 Flume的安装 1、下载flume安装包并解压。 2、修改 flume-env.sh 配置文件,主要是 JAVA_HOME 设置 3、配置环境变量[可选局部环境变量设置] 4、将当前节点的环境变量及 flume 安装包发送给其他节点 scp -r ~/.bash_profile sean02:/usr/soft/scp -r apache-flume-1.6.0-bin sean02:/usr/soft/ 5、验证是否安装成功 flume-ng version Flume的使用操作 网络传输格式 **exec:**查看追踪。Unix 等操作系统执行命令行 ,如:tail cat **netcat:**来监听一个指定端口,并将接收到的数据的每一行转换为一个event事件。 **avro:**是序列化的一种,实现了RPC(Remote Procedure Call),RPC是一种远程调用协议 。监听AVRO端口来接受来自外部AVRO客户端的事件流。 配置 flume 详细参数可以参照官网,1.4X以上的版本可以使用1.8的文档。 http://flume.apache.org/releases/content/1.8.0/FlumeUserGuide.html 一、netcat --> logger 1、安装telnet telnet 命令通常用来远程登录。telnet程序是基于TELNET协议的远程登录客户端程序。Telnet协议是TCP/IP协议族中的一员,是Internet远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。在终端使用者的电脑上使用telnet程序,用它连接到服务器。终端使用者可以在telnet程序中输入命令,这些命令会在服务器上运行,就像直接在服务器的控制台上输入一样。可以在本地就能控制服务器。要开始一个 telnet会话,必须输入用户名和密码来登录服务器。 yum list telnet* #查看telnet相关的安装包yum -y install telnet #安装telnet环境yum -y install telnet-server #安装telnet服务yum -y install telnet-client #安装telnet客户端(大部分系统默认安装) 2、配置参数: # example.conf: A single-node Flume configuration# Name the components on this agenta1.sources = r1a1.sinks = k1a1.channels = c1# Describe/configure the sourcea1.sources.r1.type = netcat #r1的类型a1.sources.r1.bind = localhost #ip地址a1.sources.r1.port = 44444 #r1的端口号# Describe the sinka1.sinks.k1.type = logger #输出到本地控制台# Use a channel which buffers events in memorya1.channels.c1.type = memory #基于内存存储a1.channels.c1.capacity = 1000 #Channel中最大可以存储的event数量是1000,可以不写a1.channels.c1.transactionCapacity = 100 #每次从source穿送过来或者sink过来消费的event数量是100,可以不写# Bind the source and sink to the channela1.sources.r1.channels = c1 #r1输送到c1中a1.sinks.k1.channel = c1 #k1去c1中消费数据 示意图: 3、运行flume (1)在 root目录下新建文件夹 flume,新建配置文件 netcat_logger,参数如上。 (2)运行命令: #使用绝对路径,可以在任意路径启动flume-ng agent --conf-file /root/flume/netcat_logger --name a1 -Dflume.root.logger=INFO,console #使用相对路径,只能在 flume当前路径下启动flume-ng agent --conf ./ --conf-file netcat_logger --name a1 -Dflume.root.logger=INFO,console# 同样生效flume-ng agent --conf-file netcat_logger --name a1 -Dflume.root.logger=INFO,console 启动成功! (3)在当前节点另开窗口输入命令 telnet localhost 44444 (4)在telnet中source端输入任意数据,然后去sink端去查看数据 注意:中文不显示 二、exec --> looger 1、配置参数: # example.conf: A single-node Flume configuration# Name the components on this agenta1.sources = r1a1.sinks = k1a1.channels = c1# Describe/configure the sourcea1.sources.r1.type = execa1.sources.r1.command = tail -F /root/flume.log# Describe the sinka1.sinks.k1.type = logger# Use a channel which buffers events in memorya1.channels.c1.type = memorya1.channels.c1.capacity = 1000 #默认,可以不写a1.channels.c1.transactionCapacity = 100 #默认,可以不写# Bind the source and sink to the channela1.sources.r1.channels = c1a1.sinks.k1.channel = c1 示意图 2、运行flume (1)在文件夹 flume,新建配置文件 exec_logger,参数如上。 (2)运行命令: flume-ng agent --conf-file exec_logger --name a1 -Dflume.root.logger=INFO,console (3)启动成功后在当前节点另开窗口输入命令 echo hello world >> flume.log #将“hello world”追加到flume.log文件中echo nihaoya >> flume.logecho woshixiaoming >> flume.logecho 34325435435 >> flume.logecho fdsf985843543 >> flume.log (4)去sink端查看数据 三、avro --> logger 1、配置参数 a1.sources=r1a1.channels=c1a1.sinks=k1a1.sources.r1.type = avro a1.sources.r1.bind=192.168.110.4 #source的ipa1.sources.r1.port=55555a1.sinks.k1.type=loggera1.channels.c1.type = memorya1.channels.c1.capacity = 1000 #默认,可以不写a1.channels.c1.transactionCapacity = 100 #默认,可以不写a1.sources.r1.channels=c1a1.sinks.k1.channel=c1 示意图 2、运行flume (1)在文件夹 flume,新建配置文件 avro_logger,参数如上。 (2)运行命令: flume-ng agent --conf-file avro_logger --name a1 -Dflume.root.logger=INFO,console (3)启动成功后在其他节点输入命令 ##其他flume节点执行:flume-ng avro-client --conf ./ -H 192.168.110.4 -p 55555 -F ./test.txt 例如:在 sean02 节点 root 下有个 test.txt 文件,发送到sean01,查看sink端 sean02上的test.txt 文件 在sean01上成功拿到 四、netcat --> hdfs 1、配置参数 # a1 which ones we want to activate.a1.channels = c1a1.sources = r1a1.sinks = k1a1.sources.r1.type = netcata1.sources.r1.bind = sean01a1.sources.r1.port = 41414a1.sinks.k1.type = hdfsa1.sinks.k1.hdfs.path = hdfs://Xss/myflume/%y-%m-%da1.sinks.k1.hdfs.useLocalTimeStamp=true# Define a memory channel called c1 on a1a1.channels.c1.type = memorya1.channels.c1.capacity = 1000 #默认,可以不写a1.channels.c1.transactionCapacity = 100 #默认,可以不写# Define an Avro source called r1 on a1 and tell ita1.sources.r1.channels = c1a1.sinks.k1.channel = c1 示意图 2、运行flume (1)在文件夹 flume,新建配置文件 netcat_hdfs,参数如上。 (2)当前 sink 端运行命令: flume-ng agent --conf-file netcat_hdfs --name a1 -Dflume.root.logger=INFO,console (3)启动成功后在当前节点输入命令,进入sink端 telnet sean01 41414 成功进入后,在source端输入数据。 然后去HDFS web端看目录 myflume/%y-%m-%d 下的数据 说明成功获取到! 注意:这样上传到会形成很多小文件,对HDFS来说很不友好,这就需要我们去调整一下参数,扩大等待时间或者每个文件的大小,尽量以大文件的形式来存储到HDFS中。 相关参数官网上都有详细介绍: http://flume.apache.org/releases/content/1.8.0/FlumeUserGuide.html **hdfs.rollInterval:**默认等待30秒数据落地到磁盘。如果不想基于时间的话,设为0。 **hdfs.rollSize:**默认1024个字节落地到磁盘。 **hdfs. rollCount:**默认统计10个事件就写入磁盘 **hdfs.idleTimeout:**操作时间 **hdfs.batchSize:**默认每一批100个事件写入 HDFS 中","tags":[{"name":"分布式","slug":"分布式","permalink":"http://www.seanxia.cn/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"},{"name":"flume","slug":"flume","permalink":"http://www.seanxia.cn/tags/flume/"},{"name":"日志收集","slug":"日志收集","permalink":"http://www.seanxia.cn/tags/%E6%97%A5%E5%BF%97%E6%94%B6%E9%9B%86/"}]},{"title":"Sqoop架构","date":"2017-09-14T16:00:00.000Z","path":"大数据/9bf90fb6.html","text":"Sqoop 简介 Sqoop是将关系数据库(oracle、mysql、postgresql等)数据与hadoop数据进行转换的工具。 官网:http://sqoop.apache.org/ 版本:(两个版本完全不兼容,sqoop1使用最多) sqoop1:1.4.x sqoop2:1.99.x 同类产品: DataX:阿里顶级数据交换工具 Sqoop 架构 sqoop 架构非常简单,是 hadoop 生态系统的架构最简单的框架,它主要由三个部分组成:Sqoop client、HDFS/HBase/Hive、Database。 sqoop1 由 client 端直接接入 hadoop,任务通过解析生成对应的 maprecue 执行。 Sqoop 导入 Sqoop 导出 Sqoop 安装使用 Sqoop 安装 1、下载解压 Sqoop 可以到apache基金sqoop官网 http://hive.apache.org/,选择镜像下载地址:http://mirror.bit.edu.cn/apache/sqoop/下载一个稳定版本。 **2、配置环境变量 ** vim ~/.bash_profile #这里配置局部环境变量 source ~/.bash_profile #刷新环境变量 3、添加数据库驱动包 cp mysql-connector-java-5.1.10.jar /sqoop-install-path/lib 4、重命名配置文件 mv sqoop-env-template.sh sqoop-env.sh 5、修改配置 configure-sqoop 在 sqoop 安装包的 bin 目录下 注释掉未安装服务相关内容; 例如(HCatalog、Accumulo): ## Moved to be a runtime check in sqoop.#if [ ! -d "${HCAT_HOME}" ]; then# echo "Warning: $HCAT_HOME does not exist! HCatalog jobs will fail."# echo 'Please set $HCAT_HOME to the root of your HCatalog installation.'#fi#if [ ! -d "${ACCUMULO_HOME}" ]; then# echo "Warning: $ACCUMULO_HOME does not exist! Accumulo imports will fail."# echo 'Please set $ACCUMULO_HOME to the root of your Accumulo installation.'#fi 6、测试 sqoop version #测试版本号 注意:我这里报了一个错 Error: /usr/soft/sqoop-1.4.6.bin/bin/…/…/hadoop does not exist! Please set $HADOOP_COMMON_HOME to the root of your Hadoop installation. 根据错误提示缺少的 hadoop 安装路径配置,需修改 sqoop 配置文件。 网上给出的办法是: 进入sqoop下的conf目录编辑 sqoop-env.sh 文件,给 HADOOP_COMMON_HOME 和 HADOOP_MAPRED_HOME 添加 hadoop 安装路径,注意去掉#号 [ # 是注释 ] 重新检测,发现还有警告,需要添加 ZOOKEEPER_HOME,填上 Zookeeper 的安装路径。 重新回到 sqoop-env.sh 文件中,添加如下一行: 再次检测,完美! 7、连接数据库 sqoop list-databases -connect jdbc:mysql://sean01:3306/ -username root -password 123456 连接之前记得一定要先启动 mysql 数据库 Sqoop 参数 Sqoop 连接参数 Sqoop 导入参数 Sqoop 导出参数 Sqoop 操作 将关系型数据库中的数据导入到 HDFS/Hive/Hbase。 mysql导入到HDFS/Hive/HBase 导入到 HDFS 1、先准备数据,在 mysql 中建表 stu 2、在 sqoop 上执行语句 sqoop import --connect jdbc:mysql://localhost:3306/sqoop --username root --password 123456 --table stu -m 1 -target-dir hdfs://Xss/mysqoop #指明集群名称和导入的路径 从日志打印中可以得知只运行了 Map,不改变结果。 3、在HDFS Web端上查看 4、下载打开得到数据 导入到 Hive 1、先启动 Hive 这里注意:Hive不同的配置模式启动也不同,但要保证一点,Hive的客户端必须跟Sqoop在一个节点上,否则报错! 我这里3个节点用的是远程分开模式,所以需要分别启动服务端 sean02 和客户端 sean03 2、在 sqoop 上执行语句 sqoop import --connect jdbc:mysql://sean01:3306/sqoop --username root --password 123456 --table stu -m 1 --hive-import 这里最终导入 Hive 的地址,我们在远程一体模式中配置文件中已经指定好了路径,在服务端的Hive安装包的 conf目录下的 hive-site.xml 文件中 执行完毕我们来HDFS(因为Hive的数据也是存储到HDFS中)查看 3、HDFS Web端查看Hive数据 4、下载查看 HDFS/Hive/HBase导出到mysql 1、清空 mysql 的表数据 清空表 stu 为导出做准备。 2、在 Sqoop 上执行语句 #从HDFS中导出sqoop export --connect jdbc:mysql://sean01:3306/sqoop --username root --password 123456 --table stu -export-dir /mysqoop/part-m-00000 #指明集群名称和表数据路径 3、查看 Navivat中的mysql 可以看出,数据已经成功导出 4、从Hive中导出同理 #从Hive中导出sqoop export --connect jdbc:mysql://sean01:3306/sqoop --username root --password 123456 --table stu -export-dir /user/hive/warehouse/stu/part-m-00000 --input-fields-terminated-by '\\001' 注意:这里的\\001是Hive默认的分隔符,属于不可见字符,这个字符在vi里是^A 我们可以在 vim 中查看一下 hdfs dfs -cat /user/hive/warehouse/stu/part-m-00000 会发现分隔符并不可见 但是我们下载下载使用 vim 查看呢 hdfs dfs -get /user/hive/warehouse/stu/part-m-00000vim part-m-00000 这个字符在 vi 里是 ^A 5、再次清空mysql中的stu表,执行语句从Hive导出,查看Navcat 表stu数据再次回来,表示从 Hive 中完美导出","tags":[{"name":"sqoop","slug":"sqoop","permalink":"http://www.seanxia.cn/tags/sqoop/"},{"name":"数据转换","slug":"数据转换","permalink":"http://www.seanxia.cn/tags/%E6%95%B0%E6%8D%AE%E8%BD%AC%E6%8D%A2/"}]},{"title":"HBase性能优化","date":"2017-08-09T16:00:00.000Z","path":"大数据/2220e2b4.html","text":"关于HBase的性能优化方法很多种,现在说说常用的几种,主要分为表设计、写表和读表操作。 HBase表的设计 Pre-Creating Regions(预分区) 默认情况下,在创建 HBase 表的时候会自动创建一个 region 分区,当导入数据的时候,所有的 HBase 客户端都向这一个 region 写数据,直到这个 region 足够大了才进行切分。如果在创建 HBase 的时候就进行预分区,则会减少当数据量猛增时由于 region split 带来的资源消耗,从而加快批量写入速度。这样当数据写入 HBase 时,会按照 region 分区情况,在集群内做数据的负载均衡。 HBase 表的预分区需要紧密结合业务场景来选择分区的 key 值,每个 region 都有一个 startKey 和一个 endKey 来表示该 region 存储的 rowKey 范围。 举例说明: 1、创建一个表 fq,列族为 cf1,被 splits 切成4个 region 方法一: create 'fq', 'cf1', SPLITS => ['10', '20', '30'] 方法二: create 'fq', 'cf1', SPLITS => '/home/hbase/splitfile.txt'# /home/hbase/splitfile.txt中存储了:10 20 30 逐行写出 2、然后查看HBase web端,默认端口:60010(注意安装的节点位置) 每个 region 的命名方式:[table],[region start key],[region id] Row Key HBase 中 row key 用来检索表中的记录,支持以下三种方式: 通过单个row key访问:即按照某个row key键值进行get操作。 通过row key的range进行scan:即通过设置startRowKey和endRowKey,在这个范围内进行扫描;过滤器。 全表扫描:即直接扫描整张表中所有行记录。 在 HBase 中,row key 可以是任意字符串,最大长度64KB,实际应用中一般为10~100bytes,存为 byte[] 字节数组,一般设计成定长的。 row key 是按照字典序存储,因此,设计 row key 时,要充分利用这个排序特点,将经常一起读取的数据存储到一块,将最近可能会被访问的数据放在一块。 举个例子:如果最近写入 HBase 表中的数据是最可能被访问的,可以考虑将时间戳作为 row key 的一部分,由于是字典序排序,所以可以使用 Long.MAX_VALUE - timestamp 作为 row key ,这样能保证新写入的数据在读取时可以被快速命中。 Rowkey 设计规则: 1、 定长 越小越好 2、 Rowkey 的设计是要根据实际业务来 3、 散列性 取反,如:001 002 100 200 Hash Column Family(列族) 不要在一张表里定义太多的column family。 目前 Hbase 并不能很好的处理超过2~3个 column family 的表。因为某个 column family 在 flush 的时候,它邻近的 column family 也会因关联效应被触发 flush,最终导致系统产生更多的 I/O,从而降低运行效率。 In Memory(内存缓存的第三级别) 在BlockCache缓存里面,有三个级别的缓存数据,前两个先清空,In Memory是第三个级别最高,缓存腾出空间时一定不会清空第三个,会去清除一和二级别的数据。 创建表的时候,可以通过 HColumnDescriptor.setInMemory(true) 将表放到 RegionServer 的缓存中,保证在读取的时候被 cache 命中。 Max Version (最大版本) 创建表的时候,可以通过 HColumnDescriptor.setMaxVersions(int maxVersions) 设置表中数据的最大版本,如果只需要保存最新版本的数据,那么可以设置 setMaxVersions(1)。 Time To Live(生命周期) 创建表的时候,可以通过 HColumnDescriptor.setTimeToLive(int timeToLive) 设置表中数据的存储生命期,过期数据将自动被删除,例如如果只需要存储最近两天的数据,那么可以设置:setTimeToLive(2*24*60*60)。 Compact(合并) & Split(裂变) 在 HBase 中,数据在更新时首先写入 WAL 日志 (HLog) 和内存 (MemStore) 中,MemStore 中的数据是排序的,当 MemStore 累计到一定阈值时,就会创建一个新的 MemStore,并且将老的 MemStore 添加到 flush 队列,由单独的线程 flush 到磁盘上,成为一个 StoreFile。于此同时, 系统会在 zookeeper 中记录一个 redo point,表示这个时刻之前的变更已经持久化了**(minor compact)**。 StoreFile 是只读的,一旦创建后就不可以再修改。因此 Hbase 的更新其实是不断追加的操作。当一个 Store 中的StoreFile 数量达到一定的阈值后,就会进行一次合并 (major compact),将对同一个 key 的修改合并到一起,形成一个大的 StoreFile,当 StoreFile 的大小达到一定阈值后,又会对 StoreFile 进行分割 (split),等分为两个 StoreFile。 由于对表的更新是不断追加的,处理读请求时,需要访问 Store 中全部的 StoreFile 和 MemStore,将它们按照row key 进行合并,由于 StoreFile 和 MemStore 都是经过排序的,并且 StoreFile 带有内存中索引,通常合并过程还是比较快的。 实际应用中,可以考虑必要时手动进行 major compact,将同一个 row key 的修改进行合并形成一个大的 StoreFile。同时,可以将 StoreFile 设置大些,减少 split 的发生。 hbase 为了防止小文件(被刷到磁盘的 menstore)过多,以保证保证查询效率,hbase 需要在必要的时候将这些小的 store file 合并成相对较大的 store file,这个过程就称之为 compaction。 在 hbase 中,主要存在两种类型的compaction:minor compaction 和 major compaction。 minor compaction: 较小、很少文件的合并。 major compaction: 功能是将同一个 store 里所有的 store file 合并成一个,触发 major 。 major compaction 的可能条件有: 手动 major_compact 命令 majorCompact( ) API 自动 region server自动运行需要的相关参数: hbase.hregion.major compaction 默认为24 小时(有浮动)。把24改成0就可以关闭自动合并 hbase.hregion.majorcompaction.jetter 默认值为 0.2 。 hbase.hregion.majorcompaction.jetter:浮动参数,防止region server在同一时间进行major compaction hbase.hregion.majorcompaction:规定的值起到浮动的作用,避免多个 region 在同一时间合并争抢资源。假如两个参数都为默认值24和0.2,那么 major compact 最终使用的数值为:19.2~28.8 这个范围。 minor compaction 的运行机制要复杂一些,它由一下几个参数共同决定: hbase.hstore.compaction.min:默认值为 3,表示一次 minor compaction 中最少选取3个 store file,minor compaction 才会启动。 hbase.hstore.compaction.max 默认值为10,表示一次 minor compaction 中最多选取10个 store file。 hbase.hstore.compaction.min.size 表示文件大小小于该值的 store file 一定会加入到 minor compaction 的 store file 中。 hbase.hstore.compaction.max.size 表示文件大小大于该值的 store file 一定会被 minor compaction 排除。 hbase.hstore.compaction.ratio 将 store file 按照文件年龄排序(older to younger),minor compaction 总是从 older store file 开始选择。 写表操作 多HTable并发写 创建多个 HTable 客户端用于写操作,提高写数据的吞吐量,一个例子: static final Configuration conf = HBaseConfiguration.create();static final String table_log_name = “user_log”;wTableLog = new HTable[tableN];for (int i = 0; i < tableN; i++) { wTableLog[i] = new HTable(conf, table_log_name); wTableLog[i].setWriteBufferSize(5 * 1024 * 1024); //5MB wTableLog[i].setAutoFlush(false);} 此方式如果表过多,Java API 可能无法运行,因为实际上还是一个线程在跑,是个伪多线程。 HTable参数设置 Auto Flush 通过调用 HTable.setAutoFlush(false) 方法可以将 HTable 写客户端的自动 flush 关闭,这样可以批量写入数据到HBase,而不是有一条 put 就执行一次更新,只有当 put 填满客户端写缓存时,才实际向 HBase 服务端发起写请求。默认情况下 auto flush 是开启的。 Write Buffer 通过调用 HTable.setWriteBufferSize(writeBufferSize) 方法可以设置 HTable 客户端的写 buffer 大小,如果新设置的 buffer 小于当前写 buffer 中的数据时,buffer 将会被 flush 到服务端。其中,writeBufferSize 的单位是 byte 字节数,可以根据实际写入数据量的多少来设置该值。 WAL Flag 在 HBase 中,客户端向集群中的 RegionServer 提交数据时(Put/Delete操作),首先会先写 WAL(Write Ahead Log)日志(即HLog,一个RegionServer上的所有Region共享一个HLog),只有当 WAL 日志写成功后,再接着写 MemStore,然后客户端被通知提交数据成功;如果写 WAL 日志失败,客户端则被通知提交失败。这样做的好处是可以做到 RegionServer 宕机后的数据恢复。 因此,对于相对不太重要的数据,可以在 Put/Delete 操作时,通过调用 Put.setWriteToWAL(false) 或Delete.setWriteToWAL(false) 函数,放弃写 WAL 日志,从而提高数据写入的性能。 值得注意的是:谨慎选择关闭WAL日志,因为这样的话,一旦RegionServer宕机,Put/Delete的数据将会无法根据WAL日志进行恢复。 批量写 通过调用 HTable.put(Put) 方法可以将一个指定的 row key 记录写入 HBase,同样 HBase 提供了另一个方法:通过调用 HTable.put(List) 方法可以将指定的 row key 列表,批量写入多行记录,这样做的好处是批量执行,只需要一次网络 I/O 开销,这对于对数据实时性要求高,网络传输 RTT 高的情景下可能带来明显的性能提升。这个方法与关闭 Auto Flush 的方法类似。 多线程并发写 在客户端开启多个 HTable 写线程,每个写线程负责一个 HTable 对象的 flush 操作,这样结合定时 flush 和写 buffer(writeBufferSize),可以既保证在数据量小的时候,数据可以在较短时间内被 flush(如1秒内),同时又保证在数据量大的时候,写 buffer 一满就及时进行 flush。下面给个具体的例子: for (int i = 0; i < threadN; i++) { Thread th = new Thread() { public void run() { while (true) { try { sleep(1000); //1 second } catch (InterruptedException e) { e.printStackTrace(); }synchronized (wTableLog[i]) { try { wTableLog[i].flushCommits(); } catch (IOException e) { e.printStackTrace(); } } }} }; th.setDaemon(true); th.start();} 这个方法跟”多HTable并发写“类似,也是一个伪多线程。 最终的方法我们还是要通过MR+HBase的整合来进行分布式的写操作。 MR+HBase 测试前先保证Hadoop集群和HBase已启动 以 word count 单词统计为例: 1、API 源码 Job 类: public class WCJob { static String TN = null; static Configuration config = null; static HBaseAdmin hBaseAdmin = null; public static void main(String[] args) throws IOException { // 1、环境变量 config = new Configuration(); //加载配置文件 config.set("fs.defaultFS", "hdfs://sean02:8020"); //NameNode节点 config.set("yarn.resourcemanager.hostname", "sean03:8088"); //Yarn节点 config.set("hbase.zookeeper.quorum", "sean01,sean02,sean03"); //Zookeeper节点 // 2.设置job任务的相关信息 Job job = Job.getInstance(config); job.setJarByClass(WCJob.class); // 设置任务类Mapper job.setMapperClass(WCMapper.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(IntWritable.class); // 3、输入数据文件(读取HDFS上的文件) FileInputFormat.addInputPath(job, new Path("/wc/input/wc.txt")); // 4、设定任务类Reducer并写入表wc(在此之前要先建表) createHtable(); //建表 TableMapReduceUtil.initTableReducerJob("wc", WCReducer.class, job); // 5、打印结果 try { // 等待任务是否成功完成 if (job.waitForCompletion(true)) { System.out.println("job success ..."); } } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } // 创建表 private static void createHtable() throws IOException { TN = "wc"; //表名 hBaseAdmin = new HBaseAdmin(config); //加载配置 if (hBaseAdmin.tableExists(TN)) { // 表存在先屏蔽掉再删除 hBaseAdmin.disableTable(TN); hBaseAdmin.deleteTable(TN); System.out.println("-----------表存在,已删除-------"); } HTableDescriptor desc = new HTableDescriptor(TableName.valueOf(TN)); HColumnDescriptor family = new HColumnDescriptor("cf1"); //设定列族 family.setMaxVersions(1); //保留的版本 family.setInMemory(true); //是否加载到内存 family.setBlockCacheEnabled(true); //是否开启BlockCache// family.setTimeToLive(1*24*60*60); //设置数据的有效期 desc.addFamily(family); hBaseAdmin.createTable(desc); System.out.println("------------表创建完成---------------"); } } Mapper 类: public class WCMapper extends Mapper<LongWritable, Text, Text, IntWritable> { @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String lines = value.toString(); StringTokenizer words = new StringTokenizer(lines); while (words.hasMoreTokens()) { context.write(new Text(words.nextToken()), new IntWritable(1)); } }} Reducer 类: public class WCReducer extends TableReducer<Text, IntWritable, ImmutableBytesWritable>{ @Override protected void reduce(Text text, Iterable<IntWritable> iterable, Context context) throws IOException, InterruptedException { int sum = 0; for (IntWritable i : iterable) { sum =+ i.get(); } System.out.println("============> "+text.toString()); Put put = new Put(text.toString().getBytes()); put.add("cf1".getBytes(), "count".getBytes(), (sum+"").getBytes()); context.write(null, put); }} 2、查看Eclipse打印结果 可看出,MR任务执行成功! 3、去HBase客户端查看 可以查看一下表数据(如果表数据过大,慎用 scan 查询全表),但是可以用RowKey来控制范围 scan 'wc', {COLUMNS => 'cf1', LIMIT => 10, STARTROW => 'with'}# 表示在表wc中查询:列族为‘cf1’,从‘with’开始的10条数据 或者可以直接使用 get 查看一下 “with” 这个单词有多少个。 get 'wc','with','cf1:count' #查询表wc中RowKey为‘with’,列族为‘cf1’的‘count’字段的值 读表操作 多HTable并发读 与 “多HTable并发写” 类似。创建多个 HTable 客户端用于读操作,提高读数据的吞吐量,一个例子: static final Configuration conf = HBaseConfiguration.create();static final String table_log_name = “user_log”;rTableLog = new HTable[tableN];for (int i = 0; i < tableN; i++) { rTableLog[i] = new HTable(conf, table_log_name); rTableLog[i].setScannerCaching(50);} HTable参数设置 Scanner Caching hbase.client.scanner.caching 配置项可以设置 HBase scanner 一次从服务端抓取的数据条数,默认情况下一次一条。通过将其设置成一个合理的值,可以减少 scan 过程中 next() 的时间开销,代价是 scanner 需要通过客户端的内存来维持这些被 cache 的行记录。 有三个地方可以进行配置: 1)在 HBase 的 conf 配置文件中进行配置; 2)通过调用 HTable.setScannerCaching(int scannerCaching) 进行配置; 3)通过调用 Scan.setCaching(int caching) 进行配置。 三者的优先级越来越高。 Scan Attribute Selection scan 时指定需要的 Column Family,可以减少网络传输数据量,否则默认 scan 操作会返回整行所有 Column Family 的数据。 scan.addColumn("cf1".getBytes(), "name".getBytes()); Close ResultScanner 通过 scan 取完数据后,记得要关闭 ResultScanner,否则 RegionServer 可能会出现问题(对应的Server资源无法释放)。 批量读 与 “批量写类似”。通过调用 HTable.get(Get) 方法可以根据一个指定的 row key 获取一行记录,同样 HBase 提供了另一个方法:通过调用 HTable.get(List) 方法可以根据一个指定的 row key 列表,批量获取多行记录,这样做的好处是批量执行,只需要一次网络 I/O 开销,这对于对数据实时性要求高而且网络传输 RTT 高的情景下可能带来明显的性能提升。 多线程并发读 “多线程并发读” 类似。 在客户端开启多个 HTable 读线程,每个读线程负责通过 HTable 对象进行 get 操作。 缓存查询结果 对于频繁查询 HBase 的应用场景,可以考虑在应用程序中做缓存,当有新的查询请求时,首先在缓存中查找,如果存在则直接返回,不再查询 HBase;否则对 HBase 发起读请求查询,然后在应用程序中将查询结果缓存起来。至于缓存的替换策略,可以考虑 LRU 等常用的策略。 BlockCache HBase 上 Regionserver 的内存分为两个部分,一部分作为 Memstore,主要用来写;另外一部分作为 BlockCache,主要用于读。 写请求会先写入 Memstore,Regionserver 会给每个 region 提供一个 Memstore,当 Memstore 满64MB以后,会启动 flush 刷新到磁盘。当 Memstore 的总大小超过限制时(heapsize*hbase.regionserver.global. memstore .upperLimit*0.9),会强行启动 flush 进程,从最大的 Memstore 开始flush 直到低于限制。 heapsize:RegionServer的内存大小。 hbase.regionserver.global.memstore.upperLimit:表示RegionServer进程block进行flush触发条件,默认配置:0.4 第一次读请求先到 Memstore 中查数据,查不到就到 BlockCache 中查,再查不到就会到磁盘上读,并把读的结果放入 BlockCache。由于 BlockCache 采用的是LRU策略,因此 BlockCache 达到上限 (heapsize*hfile.block. cache.size*0.85)后,会启动淘汰机制,淘汰掉最老的一批数据。 hfile.block.cache.size:BlockCache的大小 一个 Regionserver 上有一个 BlockCache 和N个 Memstore,它们的大小之和必须小于 heapsize*0.8,否则 HBase 不能启动。默认 BlockCache = heapsize*0.8*0.2,而 Memstore = heapsize*0.8*0.4。对于注重读响应时间的系统,可以将 BlockCache 设大些,比如设置 BlockCache=0.4,Memstore=0.39,以加大缓存的命中率。 HBase与DBMS比较 DBMS:关系型数据库。 查询数据不灵活: 1、不能使用column之间过滤查询 2、不支持全文索引。使用ElasticSearch(solr)和HBase整合完成全文索引。 使用RM批量读取HBase中的数据,在ES中建立索引(no store)保存rowkey的值。 根据关键词从索引中搜索到rowkey(分页查询)。 根据rowkey从HBase查询到所有数据。 HBase与ES的整合: 举个例子:假如在HBase中有一个表,表的rowkey为name,然后还有age、class两个字段,里面有上百万上千万的数据,实现一个功能找出名字中包含国强的,普通方法只能全表扫描,效率太低。 我们使用ES来建立索引,把name这个值放到ES的document中,检索的时候就可以先去ES中去找,因为ES使用的是倒排索引,分词建索引了,所以速度很快。找到之后假如总共100个,然后拿着这些name再去HBase中查询所有数据。 另外一种,假如HBase中的rowkey不是name,是其他的比如 id 什么的,这个时候 ES 的document除了要保存name之外,还要保存一个rowkey的字段,这样根据name就能找到对应的rowkey,检索到想要查找的所有数据。 总结一点,在进行 HBase 与 ES 的整合时,ES 的 document 中一定要包含 HBase 的rowkey,否则无法关联到 ES。","tags":[{"name":"优化","slug":"优化","permalink":"http://www.seanxia.cn/tags/%E4%BC%98%E5%8C%96/"},{"name":"HBase","slug":"HBase","permalink":"http://www.seanxia.cn/tags/HBase/"}]},{"title":"分布式数据库HBase","date":"2017-08-05T16:00:00.000Z","path":"大数据/f0a46e55.html","text":"Hbase简介对比 HBase的概念 HBase-Hadoop Database,是一个高可靠性、高性能、面向列、可伸缩、实时读写的分布式数据库。 在Hadoop生态圈中,它是其中一部分且利用Hadoop HDFS作为其文件存储系统,利用Hadoop MapReduce来处理HBase中的海量数据,利用Zookeeper作为其分布式协同服务,主要用来存储非结构化和半结构化的松散数据(NoSQL非关系型数据库有redis、MongoDB等)。 而我们的HBase就是这样一个非关系型数据库。 简单来说,关系模型指的就是二维表格模型,而一个关系型数据库就是由二维表及其之间的联系所组成的一个数据组织。 关系型与非关系型数据库对比 关系型数据库 1、关系型数据库的3大优点 **容易理解:**二维表结构是非常贴近逻辑世界的一个概念,关系模型相对网状、层次等其他模型来说更容易理解。 **使用方便:**通用的SQL语言使得操作关系型数据库非常方便。 **易于维护:**丰富的完整性(实体完整性、参照完整性和用户定义的完整性)大大减低了数据冗余和数据不一致的概率。 2、关系型数据库的3大瓶颈 (1)高并发读写需求 网站的用户并发性非常高,往往达到每秒上万次读写请求,对于传统关系型数据库来说,硬盘I/O是一个很大的瓶颈,并且很难能做到数据的强一致性。 (2)海量数据的读写性能低: 网站每天产生的数据量是巨大的,对于关系型数据库来说,在一张包含海量数据的表中查询,效率是非常低的。 (3)扩展性和可用性差 在基于web的结构当中,数据库是最难(但是可以)进行横向扩展的,当一个应用系统的用户量和访问量与日俱增的时候,数据库却没有办法像web server和app server那样简单的通过添加更多的硬件和服务节点来扩展性能和负载能力。对于很多需要提供24小时不间断服务的网站来说,对数据库系统进行升级和扩展是非常痛苦的事情,往往需要停机维护和数据迁移。 当然,对网站来说,关系型数据库的很多特性不再需要了,比如:事务一致性、读写实时性 。在关系型数据库中,导致性能欠佳的最主要原因是多表的关联查询,以及复杂的数据分析类型的复杂SQL报表查询。为了保证数据库的ACID特性,我们必须尽量按照其要求的范式进行设计,关系型数据库中的表都是存储一个格式化的数据结构。 非关系型数据库 1、非关系型数据库的理念 非关系型数据库提出另一种理念,例如:以 key-value 键值对存储,且结构不固定,每一个元组可以有不一样的字段,每个元组可以根据需要增加一些自己的键值对,这样就不会局限于固定的结构,可以减少一些时间和空间的开销。 使用这种方式,用户可以根据需要去添加自己需要的字段,这样,为了获取用户的不同信息,不需要像关系型数据库中,要对多表进行关联查询。仅需要根据 id 取出相应的 value 就可以完成查询。 2、非关系型数据库特点 (1)一般不支持ACID特性,无需经过SQL解析,读写性能高。 ACID,指数据库事务正确执行的四个基本要素的缩写。包含:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。一个支持事务(Transaction)的数据库,必须要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性,交易过程极可能达不到交易方的要求。 (2)存储格式:key-value,文档,图片等等。 (3)数据没有耦合性,容易扩展。 但非关系型数据库由于很少的约束,他也不能够提供像SQL所提供的where这种对于字段属性值情况的查询。并且难以体现设计的完整性。他只适合存储一些较为简单的数据,对于需要进行较复杂查询的数据,SQL数据库显的更为合适。 (4)多样性且开源。 由于非关系型数据库本身天然的多样性,以及出现的时间较短,因此,不像关系型数据库,有几种数据库能够一统江山,非关系型数据库非常多,并且大部分都是开源的。 (5)特定的应用需求 这些数据库中,其实实现大部分都比较简单,除了一些共性外,很大一部分都是针对某些特定的应用需求出现的,因此,对于该类应用,具有极高的性能。 3、非关系型数据库的分类 (1)**面向高性能并发读写的key-value数据库:**Redis,Tokyo Cabinet,Flare。 (2)**面向海量数据访问的文档数据库:**MongoDB以及CouchDB,可以在海量的数据中快速的查询数据。 (3)**面向可扩展性的分布式数据库:**这类数据库想解决的问题就是传统数据库存在可扩展性上的缺陷,这类数据库可以适应数据量的增加以及数据结构的变化。 二者优缺点对比 HBase数据模型 我们先大概看一下传统数据库存储方式及非关系型数据库的存储方式。 非关系型数据库表 Row Key 1、决定一行数据,具有唯一性。 2、按照字典顺序排序。 3、Row Key最大只能存储64K的字节数据。 Column Family列族 & qualifier列 1、HBase表中的每个列都归属于某个列族,列族必须作为表模式(schema)定义的一部分预先给出。如 create ‘test’, ‘course’; 2、列名以列族作为前缀,每个“列族”都可以有多个列成员(column);如 course:math, course:english, 新的列可以随后按需、动态加入;权限控制、存储以及调优都是在列族层面进行的; 3、HBase把同一列族里面的数据存储在同一目录下,由几个文件保存。 Cell 单元格 1、由行和列的坐标交叉决定; 单元格是有版本的; 2、单元格的内容是未解析的字节数组; 3、由 {row key, column( = < family > + < qualifier >), version} 唯一确定的单元。cell中的数据是没有类型的,全部是字节码形式存贮。 Timestamp 时间戳 1、在HBase每个cell存储单元对同一份数据有多个版本,根据唯一的时间戳来区分每个版本之间的差异,不同版本的数据按照时间倒序排序,最新的数据版本排在最前面。 2、时间戳的类型是 64位整型。 3、时间戳可以由HBase(在数据写入时自动)赋值,此时时间戳是精确到毫秒的当前系统时间。 时间戳也可以由客户显式赋值,如果应用程序要避免数据版本冲突,就必须自己生成具有唯一性的时间戳。 HLog(WAL log) 1、HLog文件就是一个普通的Hadoop SequenceFile,Sequence File的Key是HLogKey对象,HLogKey中记录了写入数据的归属信息,除了table和region名字外,同时还包括 sequence number和timestamp,timestamp是”写入时间”,sequence number的起始值为0,或者是最近一次存入文件系统中sequence number。 sequenceFile文件是Hadoop用来存储二进制形式的[Key,Value]对而设计的一种平面文件(Flat File),它提供key-value的存储,但与传统key-value存储(比如hash表,btree)不同的是,它是append only的,于是你不能对已存在的key进行写操作。 2、HLog SequeceFile的Value是HBase的KeyValue对象,即对应HFile中的KeyValue。存储Hbase表的操作记录,K-V数据信息。 HBase 体系架构 Client 包含访问HBase的接口并维护cache来加快对HBase的访问。 Zookeeper 1、保证任何时候,集群中只有一个master。 2、存贮所有Region的寻址入口。 3、实时监控Region server的上线和下线信息,并实时通知Master。 4、存储HBase的schema(数据库对象的集合)和table元数据。 方案(Schema)为数据库对象的集合,为了区分各个集合,我们需要给这个集合起个名字,这些名字就是我们在企业管理器的方案下看到的许多类似用户名的节点,这些类似用户名的节点其实就是一个schema,schema里面包含了各种对象如tables, views, sequences, stored procedures, synonyms, indexes, clusters, and database links。 一个用户一般对应一个schema,该用户的schema名等于用户名,并作为该用户缺省schema。这也就是我们在企业管理器的方案下看到schema名都为数据库用户名的原因。 Master 为 Region server 分配region. 1、负责Region server的负载均衡; 2、发现失效的Region server并重新分配其上的region; 3、管理用户对table的操作,对表的创建、删除、修改(DDL)。 RegionServer 1、维护region,处理对这些region的IO请求; 2、负责切分在运行过程中变得过大的region。 Region 1、HBase自动把表水平划分成多个区域(region),每个region会保存一个表里面某段连续的数据;每个表一开始只有一个region,随着数据不断插入表,region不断增大,当增大到一个阀值的时候,region就会等分成两个新的region(裂变); 2、当table中的行不断增多,就会有越来越多的region。这样一张完整的表被保存在多个Regionserver 上。 3、HRegion是HBase中分布式存储和负载均衡的最小单元,最小单元就表示不同的HRegion可以分布在不同的 HRegion server上。 MemStore与StoreFile 1、一个Region由多个Store组成,一个Store对应一个CF(列族)。 2、Store包括位于内存中的MemStore和位于磁盘的StoreFile。写操作先写入MemStore,当MemStore中的数据达到某个阈值,HRegionServer会启动flashcache进程溢写到磁盘中的Storefile,每次写入形成单独的一个StoreFile; 3、当StoreFile文件的数量增长到一定阈值后,系统会进行合并(minor、major compaction)。 先抽取一部分小的StoreFile文件进行小合并(minor compaction),小的StoreFile文件指的是刚溢写形成的最大64M,小合并速度比较快,IO开销比较小。然后当这些小合并的StoreFile越来越多,会进行大合并(major compaction),在合并过程中会进行版本合并和删除工作(major),形成更大的StoreFile。大合并速度比较慢,IO开销也比较大。 注意:合并只能是同一个store里的合并,也就是同一个列族里的合并。不然数据会包含多个列族,造成数据混乱。 4、当一个Region所有StoreFile已经合并且大小和数量超过一定阈值后,会把当前的Region分割为两个(裂变),并由HMaster分配到相应的HRegionServer服务器,实现负载均衡。 StoreFile文件的大小取决于内存大小,当它的数量达到峰值后根据相同的key进行合并(HBase的小合并)。 当MemStore中的数据溢写到磁盘后,系统会在内存中开辟一个新的MemStore去接收数据写入,老的MemStore在落地到磁盘后被释放掉。永远只有一个MemStore去接收服务。还有一种,当服务器宕机或者异常,尽管MemStore还未满,数据也会被落地到磁盘,也就是HDFS上,避免数据的丢失。 客户端检索数据,先在MemStore找,找不到再找StoreFile。(也就是先会到内存中找,找不到再去磁盘中找) HRegion由一个或者多个Store组成,每个store保存一个columns family,每个Strore又由一个memStore和0至多个StoreFile组成。StoreFile以HFile格式保存在HDFS上。 HBase的安装部署 1、上传tar安装包并解压 官网下载地址:http://hbase.apache.org/downloads.html 在使用的时候注意版本的兼容问题,我这里使用的是1.2.1的版本。 2、修改hbase-env.sh配置JAVA_HOME 在 hbase的安装包下的conf目录下,打开hbase-env.sh,填入JAVA_HOME路径 注意:不使用HBase的默认zookeeper配置:HBASE_MANAGES_ZK=false 3、配置hbase-site.xml,也是在conf目录下 配置参数: <property> <!--集群名称和路径--> <name>hbase.rootdir</name> <value>hdfs://Xss/hbase</value></property><property> <!--开启分布式--> <name>hbase.cluster.distributed</name> <value>true</value></property><property> <!--Zookeeper的节点--> <name>hbase.zookeeper.quorum</name> <value>sean01,sean02,sean03</value></property> 4、配置regionservers添加你配置的regionservers 的主机名 还是到conf目录下新建regionservers文件 5、vi并配置backup-masters 添加你配置的master备份的主机名 6、拷贝Hadoop的下配置文件hdfs-site.xml到当前conf下 cp -r /usr/soft/hadoop-2.6.5/etc/hadoop/hdfs-site.xml ./ 7、将配置好的环境发送给其他的服务器节点 scp -r hbase-0.98.12.1-hadoop2 sean02:/usr/soft/scp -r hbase-0.98.12.1-hadoop2 sean03:/usr/soft/ 8、配置HBase的环境变量 配置局部的环境变量 ~/.bash_profile 然后不要忘记发送给其他节点 scp -r ~/.bash_profile sean02:/root/scp -r ~/.bash_profile sean03:/root/ 最后记得刷新一下环境变量:source ~/.bash_profile 当我们在任意的节点上输入hbase,tab键一下,出现下图说明环境变量已配置成功。 9、启动:Zookeeper集群主机 zkServer.sh start 10、启动hadoop start-all.sh 11、启动hbase :因为HBase依赖于Hadoop和zookeeper之上的所以要Hadoop集群启动正常和Zookeeper集群启动正常。因为用不到yarn,所以不需要启动。 start-hbase.sh 12、启动后查看集群进程 13、查看Web端 默认端口:60010 注意:有时查看web端,我们找不到另外两个节点的服务。再次查看进程,发现节点sean02和sean03已经挂掉 这时需要做时间同步。停掉hbase的进程 stop-hbase.sh #停止hbase的进程ntpdate cn.ntp.org.cn #时间同步 做完时间同步后再次启动hbase,再查看web端 结果如图上所示,说明启动正常,搭建成功! HBase Shell & API HBase Shell 1、进入shell命令行 通过 hbase shell 命令进入HBase 命令行接口 2、hbase shell中的操作参数 通过help可查看所有命令的支持以及帮助手册 名称 Shell命令 创建表 create ‘表名’, ‘列族名1’[,…] 添加记录 put ‘表名’, ‘RowKey’, ‘列族名称:列名’, ‘值’ 查看记录 get ‘表名’, ‘RowKey’, ‘列族名称:列名’ 查看表中的记录总数 count ‘表名’ 删除记录 delete ‘表名’ , ‘RowKey’, ‘列族名称:列名’ 删除一张表 先要屏蔽该表,才能对该表进行删除。第一步 disable ‘表名称’ 第二步 drop ‘表名称’ 查看所有记录 scan ‘表名’ 设置表的过期时间 alter ’表名‘ , {NAME => ‘f’, TTL => 7776000} 注意:在hbase命令行中,常规的删除无法正常使用,需要按 ctrl+删除键 组合使用 这是因为Xshell的设置中默认的。在节点属性中的键盘设置中可以查看 3、具体操作 (1)建表:表名为t1,列族为cf1 create 't1','cf1' (2)查看当前表,使用命令 list (3)添加表记录: # 表名t1,rowkey为0001,列族cf1,列名name,Value为xiaomingput 't1','0001','cf1:name','xiaoming'# 表名t1,rowkey为0001,列族cf1,列名age,Value为18put 't1','0001','cf1:age','18'...put 't1','0002','cf1:name','zhangsan'put 't1','0002','cf1:age','22'put 't1','0002','cf1:sex','man' (4)查看表所有记录 scan 't1' 注意:因为scan是整个表的全盘扫描,如果表数据过大,慎用! (5)查看某个表的指定条件的记录 get 't1','0001' #表示查看t1表中rowkey为0001的表记录get 't1','0002' #表示查看t1表中rowkey为0002的表记录get 't1','0001','cf1:name' #表示查看t1表中rowkey为0001的name为xiaoming的表记录 (6)设定表的保留版本(在创建表初期定义) create 't2',{NAME =>'cf1',VERSIONS => 2} #表名t2,列族cf1,保留2个版本 put数据进去 put 't2','0001','cf1:name','lisi'put 't2','0001','cf1:name','lisi'put 't2','0001','cf1:age','26'put 't2','0001','cf1:age','18'put 't2','0001','cf1:age','16'put 't2','0001','cf1:name','xiaoli'put 't2','0001','cf1:name','xiaozhu'put 't2','0001','cf1:name','meina' 然后命令scan查看表数据 然而我们创建表的时候明明保留了3个版本,为什么这里只显示一个最新的版本呢?是因为我们没有加版本号 scan 't2',{RAW => true,VERSIONS => 2} #取两个版本 那当我们取三个或者四个、五个版本呢 scan 't2',{RAW => true,VERSIONS => 3} #取三个版本scan 't2',{RAW => true,VERSIONS => 4} #取四个版本scan 't2',{RAW => true,VERSIONS => 5} #取五个版本 我们会惊奇的发现,本来我们创建的时候定义保留两个版本,但是这里却留下了我们所有的版本。这是为什么? 这里发现RAM这个字段设为true的时候,意思是未经处理的都显示出来,也就是说:当写入内存中的数据较少时,内存中的数据还没有达到阈值,所以并未溢写到磁盘,也就是未处理。 如果去掉字段RAW,就只显示我们保留的两个版本 scan 't2',{VERSIONS => 5} #取五个版本 这里我们可以验证一下: ① 在处理之前,我们到HDFS web端看到,hbase目录下有三个文件夹WALs、oldWALs和data,WALs中存着表的元数据信息,data中存放表的真实数据,oldWALs中存放之前已经经过处理的元数据信息。 打开data文件夹会发现是空的,根本没有数据,也就是说还没有落地到磁盘 ② 我们知道如果服务器发生意外宕机(也可以使用flush命令),内存中的数据就自动会落地到磁盘中。所以我们关掉hbase的服务 stop-hbase.sh 注:如果关闭hbase的过程中发生异常无法关闭,ctrl+c暂停,换另外一个HMaster去关,如果还是无法关闭,ctrl+c暂停,查看进程kill调其中的一个,kill尽量少用,会对服务器有损耗。 ③ 重新开启hbase服务,查看HDFS数据 start-hbase.sh oldWALs目录 data目录 此时说明,t2表的数据已经落地到磁盘中 总结:客户端检索数据,先会到内存中找,找不到再去磁盘中找。 ④ 进入hbase中查询表数据 此时发现由于数据已经落地到磁盘,所以无论怎么取都会保留在建表初期设定的保留两个版本 (7)删除表 drop 't2' 删表前我们要先进行disable(屏蔽掉)这个表再删除 如图显示已删除成功! Hbase 建表设置数据的有效期 create 't1', {NAME => 'f', TTL => 7776000,VERSIONS => 1} 如果在已存在的表上加有效期,要先disable disable 'tableName'alter 'tableName',{NAME => 'f', TTL => 7776000}enable 'tabName' HBase API 详细 API 请见我的 github 仓库:Hbase API 总结思考 1、HBase中写入数据什么情况会落地到磁盘 当MemStore中的数据达到某个阈值,HRegionServer会启动flashcache进程溢写到磁盘中的Storefile。 当前HRegionServer服务器异常宕机。 使用命令flush强制刷到磁盘中。","tags":[{"name":"HBase","slug":"HBase","permalink":"http://www.seanxia.cn/tags/HBase/"},{"name":"非关系型","slug":"非关系型","permalink":"http://www.seanxia.cn/tags/%E9%9D%9E%E5%85%B3%E7%B3%BB%E5%9E%8B/"}]},{"title":"Hive中的优化策略","date":"2017-07-14T16:00:00.000Z","path":"大数据/9018532c.html","text":"Hive优化核心思想 把 Hive SQL 当做 MapReduce 程序去优化。 注:以下 SQL 不会转为 MapReduce 来执行: select 仅查询本表字段 where 仅对本表字段做条件过滤 几种常用的优化策略 1、Explain 显示执行计划 在 sql 前添加关键字 explain,不会产生 MapReduce 任务,但会预先分析,程序大概需要执行的步骤。 例如:对表 person 进行统计 explain select count(*) from person; 从表中我们可以看出,执行这个语句程序大概要运行的步骤。 2、Hive 运行方式 Hive 默认的运行方式为集群模式,当使用集群模式的时候程序会把任务提交到服务器去运行。 举例:对表 cell_monitor 进行统计查询操作 select count(*) from cell_monitor; 从图中可以看出,MR任务被提交到服务器运行,时间为101秒,少量数据就提交到服务器,会严重消耗工作时间和增加服务器压力。 本地模式 开启本地模式 set hive.exec.mode.local.auto=true; #默认为false 再次进行查询 可以发现,运行时间大大缩短,同时节省资源,减轻服务器的压力。 集群模式 默认为 false 本地加载文件的最大值 hive.exec.mode.local.auto.inputbytes.max #默认值为128M(134217728) 注意:若加载文件的最大值大于该配置,仍会以集群方式来运行! 3、严格模式 通过设置以下参数开启严格模式[防止误操作] set hive.mapred.mode=strict;(默认为:nonstrict非严格模式) 查询限制: 1、对分区表查询时,必须添加 where 对于分区字段的条件过滤; 2、order by 语句必须包含 limit 输出限制; 3、限制执行笛卡尔积的查询 如不满足,则会报错! 4、Hive 排序 Order By - 对于查询结果做全排序,只允许有一个 reduce 处理 (当数据量较大时,应慎用。严格模式下,必须结合 limit 来使用,限制查询的数量) Sort By - 对于单个 reduce 的数据进行排序(分布式处理) Distribute By - 分区排序,经常和 Sort By 结合使用(对多个区进行排序) Cluster By - 相当于 Sort By + Distribute By (Cluster By 不能通过 asc、desc 的方式指定排序规则;可通过 distribute by column sort by column asc|desc 的方式) 5、Hive Join Join 计算时,将小表(驱动表,数据量较少的表)放在 join 的左边。 Map Join:在 Map 端完成 Join 两种实现方式: (1)SQL 方式,在 SQL 语句中添加 MapJoin 标记(mapjoin hint) 语法: SELECT /*+ MAPJOIN(smallTable) */ smallTable.key, bigTable.value FROM smallTable JOIN bigTable ON smallTable.key = bigTable.key; (2)开启自动的 MapJoin 通过修改以下配置启用自动的 mapjoin: set hive.auto.convert.join = true;(该参数为true时,Hive自动对左边的表统计量,如果是小表就加入内存,即对小表使用Map join) 其他相关配置参数: hive.mapjoin.smalltable.filesize; (大表小表判断的阈值25MB左右,如果表的大小小于该值则会被加载到内存中运行)hive.ignore.mapjoin.hint;(默认值:true;是否忽略mapjoin hint 即map join标记)hive.auto.convert.join.noconditionaltask;(默认值:true;将普通的join转化为普通的map join时,是否将多个map join转化为一个map join)hive.auto.convert.join.noconditionaltask.size;(将多个map join转化为一个map join时,其表的最大值(默认10MB)) 注意:如果两个表都是小表,尽量把真正较小的表放到左边,提高性能。 6、Map-Side聚合 如 count() 等聚合函数 通过设置以下参数开启在Map端的聚合: set hive.map.aggr=true; 相关配置参数: hive.groupby.mapaggr.checkinterval;map端group by执行聚合时处理的多少行数据(默认:100000)hive.map.aggr.hash.min.reduction: 进行聚合的最小比例(预先对100000条数据做聚合,若 聚合的数据量/100000 的值小于该配置0.5,则不会聚合)hive.map.aggr.hash.percentmemory:map端聚合使用的内存的最大值(默认0.5)hive.map.aggr.hash.force.flush.memory.threshold: map端做聚合操作是hash表的最大可用内容,大于该值则会触发flush(默认0.9)hive.groupby.skewindata是否对Group By产生的数据倾斜做优化,默认为false 6、控制Hive中Map以及Reduce的数量 Map数量相关的参数 mapred.max.split.size一个split的最大值,即每个map处理文件的最大值(默认256MB)mapred.min.split.size.per.node一个节点上split的最小值(默认1个)mapred.min.split.size.per.rack一个机架上split的最小值(默认1个) Reduce数量相关的参数 mapred.reduce.tasks强制指定reduce任务的数量(默认-1)hive.exec.reducers.bytes.per.reducer每个reduce任务处理的数据量(默认256MB)hive.exec.reducers.max每个任务最大的reduce数(默认1009个) [ Map数量 >= Reduce数量 ] 7、Hive - JVM重用 用来设置粗细粒度。 适用场景: (1)小文件个数过多 (2)task 个数过多 通过下列参数来设置 set mapred.job.reuse.jvm.num.tasks=n;(n为 task 插槽个数) **插槽个数:**是 JVM 中的一个计量单位。举个例子:我们常用的 window 系统,有32位和64位的,我们会发现32位的操作系统上不能运行64位的软件程序,但是64位操作系统上却可以运行32位的软件,这是因为64位就等于两个32位,我们把一个32位比作一个插槽,那64位就是2个插槽了。 缺点: 设置开启之后,task插槽会一直占用资源,不论是否有task运行,直到所有的 task 即整个 job 全部执行完成时,才会释放所有的task插槽资源!","tags":[{"name":"优化","slug":"优化","permalink":"http://www.seanxia.cn/tags/%E4%BC%98%E5%8C%96/"},{"name":"Hive","slug":"Hive","permalink":"http://www.seanxia.cn/tags/Hive/"}]},{"title":"Hadoop之数据仓库Hive","date":"2017-07-06T16:00:00.000Z","path":"大数据/6cc88fdb.html","text":"Hive 及数据仓库简介 基本概念 Hive 是基于 Hadoop 的一个【数据仓库工具】,可以将结构化的数据文件映射为一张 hive 数据库表,并提供简单的 sql 查询功能,可以将 sql 语句转换为 MapReduce 任务进行运行。 【数据仓库】英文名称为 Data Warehouse,可简写为DW或 DWH。数据仓库,是为企业所有级别的决策制定过程,提供所有类型数据支持的战略集合。它是单个数据存储,出于分析性报告和决策支持目的而创建。为需要业务智能的企业,提供指导业务流程改进、监视时间、成本、质量以及控制。 **Hive 的表其实就是HDFS的目录/文件夹,hive表中的数据 就是hdfs目录中的文件,并按表名把文件夹分开。**如果是分区表,则分区值是子文件夹,可以直接在M/R job里使用这些数据。 Hive 的优缺点 **优点:**使用SQL来快速实现简单的 MapReduce 统计,不必开发专门的 MapReduce 应用,学习成本低,十分适合数据仓库的统计分析。 **缺点:**只适合离线的数据处理,不支持实时查询。 Hive与HBase的关系与区别 关系: Hive与HBase其实没有必然的关系,但是由于HBase的查询语句很不好用,所以可以通过整合Hive,对存储在Hadoop群上的数据提供类SQL的接口进行操作,也就是利用Hive来操作HBase数据库。 他们的共同点是数据都存储在HDFS,也就是说他们都是建立于Hadoop之上。 区别: 如果你有数据仓库的需求并且你擅长写SQL并且不想写MapReduce jobs就可以用Hive代替。用 HiveQL进行select,join,等等操作。但是Hive只适合做离线的批处理,实时性不高。 HBase是一个分布式的NoSql数据库,像其他数据库一样提供随即读写功能。如果你需要实时访问一些数据,就把它存入HBase。你可以用Hadoop作为静态数据仓库,HBase作为数据存储,放那些进行一些操作会改变的数据。 数据处理的分类 1. 联机事务处理OLTP(on-line transaction processing) OLTP 是传统的关系型数据库的主要应用,主要是基本的、日常的事务处理,例如银行交易。 OLTP 系统强调数据库内存效率,强调内存各种指标的命令率,强调绑定变量,强调并发操作。 2. 联机分析处理OLAP(On-Line Analytical Processing) OLAP 是数据仓库系统的主要应用,支持复杂的分析操作,侧重决策支持,并且提供直观易懂的查询结果。 OLAP 系统则强调数据分析,强调 SQL 执行市场,强调磁盘 I/O,强调分区等。 简而言之,数据仓库是用来做查询分析的数据库,基本不用来做插入,修改,删除操作。 Hive:数据仓库。 Hive:解释器,编译器,优化器等。 Hive 运行时,元数据存储在关系型数据库里面。 编译器将一个Hive SQL转换操作符,操作符是Hive的最小的处理单元 每个操作符代表HDFS的一个操作或者一道MapReduce作业。 Hive 架构原理 1. 用户接口 用户接口主要有三个:Client CLI、JDBC/ODBC 和 WEBUI。 **Client CLI :**Hive shell 命令行,最常用。Client 是 Hive 的客户端,用户连接至 Hive Server。在启动 Client 模式的时候,需要指出 Hive Server 所在节点,并且在该节点启动 Hive Server。 **JDBC/ODBC:**Hive的 Java 实现,与传统数据库 JDBC 类似。 **WEBUI:**通过浏览器访问 Hive。 2. 元数据存储 Hive 将元数据存储在关系型数据库中,如 mysql、oracle、derby 。 Hive 中的元数据包括表的名字,表的列和分区及其属性,表的属性(是否为外部表等),表的数据所在目录等。 3. 驱动器(Driver) 解释器、编译器、优化器完成 HQL 查询语句从词法分析、语法分析、编译、优化以及查询计划的生成。生成的查询计划存储在 HDFS 中,并在随后有 MapReduce调用执行。 4. 数据存储 Hive 的数据存储在 HDFS 中。大部分的查询、计算由 MapReduce 完成(包含 * 的查询,比如 select * from tb 不会生成 MapRedcue 任务) Hive 搭建及三种模式 Hive 的安装配置 1、安装Hive 安装环境以及前提说明:首先,Hive 是依赖于 hadoop 系统的,因此在运行 Hive 之前需要保证已经搭建好 hadoop 集群环境。 并安装好一个关系型数据:如 mysql 2、配置环境变量 – HADOOP_HOME=/xxx – HIVE_HOME=/xxx 这里选择配置局部的环境变量,编辑 ~/.bash_profile,具体操作参考我的另一篇文章 Linux常用操作个人整理 3、替换和添加相关jar包 (1) 修改 Hadoop安装包下 /share/hadoop/yarn/lib 目录下的 jline-*.jar 将其替换成 HIVE_HOME/lib 下的 jline-2.12.jar (2) 将 hive 连接 mysql 的jar包:mysql-connector-java-5.1.32-bin.jar 拷贝到hive解压目录的lib目录下 4、修改配置文件(选择3种模式里的一种)见三种安装模式 5、启动 hive 配置完环境变量,可以在任意客户端的节点上直接输入命令:hive 即可启动。 Hive 配置的三种模式 新建配置文件 hive-site.xml 在 Hive 安装包下的 conf 目录中。 A、内嵌模式(元数据保存在内嵌的derby中,允许一个会话链接,尝试多个会话链接时会报错)【不推荐】 B、本地模式(本地安装mysql 替代derby存储元数据)【推荐】 C、远程模式(远程安装mysql 替代derby存储元数据)【推荐】 1. 内嵌Derby单用户模式 这种安装模式的元数据是内嵌在Derby数据库中的,只能允许一个会话连接,数据会存放到HDFS上。 这种方式是最简单的存储方式,只需要hive-site.xml 做如下配置便可(注:使用 derby 存储方式时,运行 hive 会在当前目录生成一个 derby 文件和一个 metastore_db)。 <!-- 内嵌模式 --><?xml version="1.0"?><?xml-stylesheet type="text/xsl" href="configuration.xsl"?><configuration> <property> <name>javax.jdo.option.ConnectionURL</name> <value>jdbc:derby:;databaseName=metastore_db;create=true</value> </property> <property> <name>javax.jdo.option.ConnectionDriverName</name> <value>org.apache.derby.jdbc.EmbeddedDriver</value> </property> <property> <name>hive.metastore.local</name> <value>true</value> </property> <property> <name>hive.metastore.warehouse.dir</name> <value>/user/hive/warehouse</value> </property></configuration> 2. 本地用户模式 这种安装方式和嵌入式的区别在于:不再使用内嵌的Derby作为元数据的存储介质,而是使用其他数据库比如 MySQL 来存储元数据且是一个多用户的模式,运行多个用户 client 连接到一个数据库中。这种方式一般作为公司内部同时使用 Hive。这里有一个前提,每一个用户必须要有对MySQL的访问权利,即每一个客户端使用者需要知道MySQL的用户名和密码才行。 这种存储方式需要在本地运行一个 mysql 服务器,并作如下配置(下面两种使用 mysql 的方式,需要将 mysql 的 jar 包拷贝到 Hive 安装包下的 lib 目录下。 hive-site.xml 配置如下: <!-- 1.本地模式 --><?xml version="1.0"?><?xml-stylesheet type="text/xsl" href="configuration.xsl"?><configuration> <property> <name>hive.metastore.warehouse.dir</name> <value>/user/hive_local/warehouse</value> </property> <property> <name>hive.metastore.local</name> <value>true</value> </property> <property> <name>javax.jdo.option.ConnectionURL</name> <value>jdbc:mysql://sean01/hive_remote?createDatabaseIfNotExist=true</value> </property> <property> <name>javax.jdo.option.ConnectionDriverName</name> <value>com.mysql.jdbc.Driver</value> </property> <property> <name>javax.jdo.option.ConnectionUserName</name> <value>root</value> </property> <property> <name>javax.jdo.option.ConnectionPassword</name> <value>123456</value> </property></configuration> 注意:hive和MySQL不用做HA,单节点就可以。通过设置其他节点可以访问得到数据库。 3. 远程模式 远程模式又分为两种:远程一体和远程分开 (1) 远程一体模式 这种存储方式需要在远端服务器运行一个 mysql 服务器,并且需要在 Hive 服务器启动 meta服务。 <!-- 远程一体模式 --><?xml version="1.0"?><?xml-stylesheet type="text/xsl" href="configuration.xsl"?><configuration> <property> <name>hive.metastore.warehouse.dir</name> <value>/user/hive/warehouse2</value> </property> <property> <name>javax.jdo.option.ConnectionURL</name> <value>jdbc:mysql://sean01:3306/hive?createDatabaseIfNotExist=true</value> </property> <property> <name>javax.jdo.option.ConnectionDriverName</name> <value>com.mysql.jdbc.Driver</value> </property> <property> <name>javax.jdo.option.ConnectionUserName</name> <value>root</value> </property> <property> <name>javax.jdo.option.ConnectionPassword</name> <value>123456</value> </property> <property> <name>hive.metastore.local</name> <value>false</value> </property></configuration> 注:这里把 hive 的服务端和客户端都放在同一台服务器上了。 (2) 远程分开模式 所谓远程分开模式就是在远程的前提下,把 Hive 的服务端和客户端分开放到两个不同的服务器节点上。 服务端 <?xml version="1.0"?><?xml-stylesheet type="text/xsl" href="configuration.xsl"?><configuration> <property> <name>hive.metastore.warehouse.dir</name> <value>/user/hive/warehouse</value> </property> <property> <name>javax.jdo.option.ConnectionURL</name> <value>jdbc:mysql://sean01:3306/hive?createDatabaseIfNotExist=true</value> </property> <property> <name>javax.jdo.option.ConnectionDriverName</name> <value>com.mysql.jdbc.Driver</value> </property> <property> <name>javax.jdo.option.ConnectionUserName</name> <value>root</value> </property> <property> <name>javax.jdo.option.ConnectionPassword</name> <value>123456</value> </property></configuration> 客户端 <?xml version="1.0"?><?xml-stylesheet type="text/xsl" href="configuration.xsl"?> <configuration> <property> <name>hive.metastore.warehouse.dir</name> <value>/user/hive/warehouse</value> </property> <property> <name>hive.metastore.local</name> <value>false</value> </property> <property> <name>hive.metastore.uris</name> <value>thrift://sean02:9083</value> #这里填mysql数据库的服务器地址 </property></configuration> **注意启动:**必须先启动服务端,再启动客户端,否则会报错! 服务端: hive --service metastore 客户端:直接敲 hive 即可启动 HQL 操作 DDL 语句 Hive 的数据定义语言 ([LanguageManual DDL](javascript:changelink(‘https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL’,'EN2ZH_CN’);)) 具体参见:https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL 重点是 hive 的建表语句和分区。 创建/删除/修改/使用数据库 1、创建数据库(常用) CREATE (DATABASE|SCHEMA) [IF NOT EXISTS] database_name [COMMENT database_comment]; 如图:创建了一个名叫 sss 的数据库 查看:show databases; 2、删除数据库(不常用) DROP (DATABASE|SCHEMA) [IF EXISTS] database_name; 删除一个名叫 www 的数据库 3、修改表数据库(不常用) ALTER (DATABASE|SCHEMA) database_name SET DBPROPERTIES (property_name=property_value, ...);ALTER (DATABASE|SCHEMA) database_name SET OWNER [USER|ROLE] user_or_role; 4、使用数据库 use database_name;Use default; 创建、删除表 1、创建表(常用) (1) 数据类型(data_type) primitive_type 原始数据类型 array_type 数组 map_type map struct_type union_type – (Note: Available in Hive 0.7.0 and later) 原始数据类型 primitive_type TINYINT SMALLINT INT BIGINT BOOLEAN FLOAT DOUBLE DOUBLE PRECISION STRING 基本可以搞定一切 BINARY TIMESTAMP DECIMAL DECIMAL(precision, scale) DATE VARCHAR CHAR 数组(array_type) ARRAY < data_type > map(map_type) MAP < primitive_type, data_type > struct_type STRUCT < col_name : data_type [COMMENT col_comment], …> union_type UNIONTYPE < data_type, data_type, … > (2) 完整的建表语句 (3) 建表实例: create table abc( id int, name string, age int, likes array<string>, address map<string,string> ) row format delimited fields terminated by ',' COLLECTION ITEMS TERMINATED by '-' map keys terminated by ':' lines terminated by '\\n'; #可以不写,默认按换行符 同时在 NameNode 的 HDFS 上也可以看到创建的表路径 (4) 导入数据(属于DML但是为了演示需要在此应用): LOAD DATA [LOCAL] INPATH 'filepath' [OVERWRITE] INTO TABLE tablename [PARTITION (partcol1=val1, partcol2=val2 ...)] 数据如下: 1,zshang,18,game-girl-book,stu_addr:beijing-work_addr:shanghai2,lishi,16,shop-boy-book,stu_addr:hunan-work_addr:shanghai3,wang2mazi,20,fangniu-eat,stu_addr:shanghai-work_addr:tianjing4,wxiaokun,18,fangniu-book-game,stu_addr:wuhan-work_addr:nanjing5,xshufan,20,boy-girl-book,stu_addr:tokyo-work_addr:hangzhou 首先在本地新建一个文件来装这些数据 :vim hivedata 然后加载到 abc 这张表中 load data local inpath '/root/hivedata' overwrite into table abc; 注意:如果不加 local 加载的就不是本地,是加载 HDFS 上的文件 在之前的版本,需要在路径中写全 hdfs 的路径(加上集群的名称),现在新的版本已经优化了,不加 local 就是在 hdfs 上找,加了就会到本地服务器上找。 load data inpath 'hdfs://Xss/hivedata' overwrite into table abc; 2、删除表 (1) 使用 Hive 删除表 DROP TABLE [IF EXISTS] table_name [PURGE]; 比如删除刚刚创建的表 abc (2) 使用 hdfs 删除表 因为 Hive 的元数据信息存储在 mysql 上,而数据存储在 HDFS 上,所以如果用 hdfs 的命令来删除表,会删除表里的数据,并不能删除元数据信息。 试验一下:我们用 hdfs 删除 abc 里的数据,然后重新把数据加载回来 hdfs dfs -rm -r /user/hive_local/warehouse/sss.db/abc/hivedata 删除之后 hive 中就无法查找到结果 使用 hdfs 把数据 put 回来 hdfs dfs -put hivedata /user/hive_local/warehouse/sss.db/abc 再次查看时,数据重新回来! 注意区别: 1、HDFS 删除的是表里的数据,不会删除表的元数据信息; 2、Hive删除普通表会把数据连带元数据信息一并删除; 3、Hive删除外部表只会删除元数据信息,不会删除数据。 因为 HDFS 删除和 Hive 删除普通表直接会删除数据,数据很重要,所以慎用!!! 使用 hive 删除普通表就会连带表的元数据信息彻底的删除。所以为了防止使用 hive 时避免误删,我们引入外部表来控制 hive 删除表的操作。 3、外部表 外部关键字 EXTERNAL 允许您创建一个表,并提供一个位置,以便 hive 不使用这个表的默认位置。这方便如果你已经生成的数据。当删除一个外部表,表中的数据不是从文件系统中删除。外部表指向任何 HDFS 的存储位置,而不是存储在配置属性指定的文件夹 hive.metastore.warehouse.dir 中。 开始创建外部表,需要在create 前加上关键字 external create external table person( id int, name string, age int, likes array<string>, address map<string,string> ) row format delimited fields terminated by ',' COLLECTION ITEMS TERMINATED by '-' map keys terminated by ':'; 加载数据 load data local inpath '/root/hivedata' overwrite into table abc; 然后使用 hive 删除表 abc 查询 mysql 数据库,发现元数据信息已经没有了 再看 HDFS 中,数据却还在 因为数据还在,于是只需要重新创建一下这个表就可以了 修改、更新表,删除表数据 这些一般工作中很少用 重命名表 ALTER TABLE table_name RENAME TO new_table_name;Eg: alter table meninem rename to sean; 更新数据 UPDATE table_name SET column = value [, column = value ...] [WHERE expression] 删除表数据 DELETE FROM table_name [WHERE expression] # 需要配置权限 DML 语句 Hive数据操作语言([LanguageManual DML](javascript:changelink(‘https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DML’,'EN2ZH_CN’);)) 具体参见:https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DML 重点是数据加载和查询插入语法 四种插入/导入数据 Hive 不能很好的支持用 insert 语句一条一条的进行插入操作,不支持 update 。数据是以 load 的方式加载到建立好的表中。数据一旦导入就不可以修改。 注意区分:在load数据时使用的是[overwrite] into,但是插入数据时是insert overwrite/into 第一种 LOAD DATA [LOCAL] INPATH 'filepath' [OVERWRITE] INTO TABLE table_name 第二种 INSERT OVERWRITE TABLE person SELECT id,name,age,likes,address From abc; 先创建一个 person 表 create external table person( id int, name string, age int, likes array<string>, address map<string,string> ) row format delimited fields terminated by ',' COLLECTION ITEMS TERMINATED by '-' map keys terminated by ':'; 再把 abc 表里的数据插入到 person 表中 第三种 FROM person t1INSERT OVERWRITE TABLE person2 SELECT t1.id, t1.name, t1.age ; 先创建 person2 这个表 create table person2( id int, name string, age int ) row format delimited fields terminated by ','; 再插入 person 表中所需的三个字段的数据,得到数据。 【from放前面好处就是后面可以插入多条语句 】 第四种 拓展:本地load数据和从HDFS上load数据的过程有什么区别? 本地: load 会自动复制到 HDFS 上的 hive 的 ** 目录下 HDFS:load 会移动数据到 HDFS 上的 hive 的 ** 目录下,原来的就没有了。 查询数据并保存 举例:我们用 person2 这张表作为查询对象 先查看一下 person2 表的数据信息 A、保存数据到本地 将 person2 表的查询信息保存到本地目录的 /root/hive_exp 下 insert overwrite local directory '/root/hive_exp'ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' #以','隔开select * from person2; 然后查看本地 B、保存到 HDFS 上 将 person2 表的查询信息保存到 HDFS 目录的 /user/hive/hive_exp下 insert overwrite directory '/user/hive/hive_exp'ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' #以','隔开select * from person2; 然后查看 HDFS C、在外部 shell 中将数据重定向到文件中 有时我们在外部想利用 sql 查询表数据信息,但是又不想进到 hive 里面去,就可以用到外部 shell 的操作 比如:把 abc 这张表的数据标准输出重定向到本地 root 目录下的 abc.txt 文件中 hive -e "select * from sss.abc;" > /root/abc.txt #注意表明前一定要带上数据库的名称,否则会找不到而报错! 备份数据或还原数据 1、备份数据 备份使用关键字export 举例:将person2表中的数据备份到 HDFS 中的目录 /user/hadoop/hive/datas 下 EXPORT TABLE person2 TO '/user/hadoop/hive/datas'; 2、删除再还原数据 先删除表 drop table person2; 再还原数据 IMPORT FROM '/user/hadoop/hive/datas'; Hive SerDe Hive SerDe(Serializer and Deserializer),SerDe 用于做序列化和反序列化。 构建在数据存储和执行引擎之间,对两者实现解耦。 Hive 通过 ROW FORMAT DELIMITED 以及 SERDE 进行内容的读写。 1、格式: row_format: DELIMITED [FIELDS TERMINATED BY char [ESCAPED BY char]] [COLLECTION ITEMS TERMINATED BY char] [MAP KEYS TERMINATED BY char] [LINES TERMINATED BY char] : SERDE serde_name [WITH SERDEPROPERTIES (property_name=property_value, property_name=property_value, ...)] 2、举例:处理一些请求数据,对数据做固定格式的清洗 表数据文件:localhost_access_log.2016-02-29.txt 192.168.57.4 - - [29/Feb/2016:18:14:35 +0800] "GET /bg-upper.png HTTP/1.1" 304 -192.168.57.4 - - [29/Feb/2016:18:14:35 +0800] "GET /bg-nav.png HTTP/1.1" 304 -192.168.57.4 - - [29/Feb/2016:18:14:35 +0800] "GET /asf-logo.png HTTP/1.1" 304 -192.168.57.4 - - [29/Feb/2016:18:14:35 +0800] "GET /bg-button.png HTTP/1.1" 304 -192.168.57.4 - - [29/Feb/2016:18:14:35 +0800] "GET /bg-middle.png HTTP/1.1" 304 -192.168.57.4 - - [29/Feb/2016:18:14:36 +0800] "GET / HTTP/1.1" 200 11217192.168.57.4 - - [29/Feb/2016:18:14:36 +0800] "GET / HTTP/1.1" 200 11217192.168.57.4 - - [29/Feb/2016:18:14:36 +0800] "GET /tomcat.css HTTP/1.1" 304 -192.168.57.4 - - [29/Feb/2016:18:14:36 +0800] "GET /tomcat.png HTTP/1.1" 304 -192.168.57.4 - - [29/Feb/2016:18:14:36 +0800] "GET /asf-logo.png HTTP/1.1" 304 -192.168.57.4 - - [29/Feb/2016:18:14:36 +0800] "GET /bg-middle.png HTTP/1.1" 304 -192.168.57.4 - - [29/Feb/2016:18:14:36 +0800] "GET /bg-button.png HTTP/1.1" 304 -192.168.57.4 - - [29/Feb/2016:18:14:36 +0800] "GET /bg-nav.png HTTP/1.1" 304 -192.168.57.4 - - [29/Feb/2016:18:14:36 +0800] "GET /bg-upper.png HTTP/1.1" 304 -192.168.57.4 - - [29/Feb/2016:18:14:36 +0800] "GET / HTTP/1.1" 200 11217192.168.57.4 - - [29/Feb/2016:18:14:36 +0800] "GET /tomcat.css HTTP/1.1" 304 -192.168.57.4 - - [29/Feb/2016:18:14:36 +0800] "GET /tomcat.png HTTP/1.1" 304 -192.168.57.4 - - [29/Feb/2016:18:14:36 +0800] "GET / HTTP/1.1" 200 11217192.168.57.4 - - [29/Feb/2016:18:14:36 +0800] "GET /tomcat.css HTTP/1.1" 304 -192.168.57.4 - - [29/Feb/2016:18:14:36 +0800] "GET /tomcat.png HTTP/1.1" 304 -192.168.57.4 - - [29/Feb/2016:18:14:36 +0800] "GET /bg-button.png HTTP/1.1" 304 -192.168.57.4 - - [29/Feb/2016:18:14:36 +0800] "GET /bg-upper.png HTTP/1.1" 304 - 创建表 Hive 正则匹配(去掉中括号和 “ ” 号) CREATE TABLE logtbl ( host STRING, identity STRING, //身份 t_user STRING, time STRING, request STRING, referer STRING, //来路 agent STRING) //代理 ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.RegexSerDe' //继承类 WITH SERDEPROPERTIES ( "input.regex" = "([^ ]*) ([^ ]*) ([^ ]*) \\\\[(.*)\\\\] \\"(.*)\\" (-|[0-9]*) (-|[0-9]*)" ) //正则 STORED AS TEXTFILE; //存储为text文件 把数据文件上传数据到服务器,然后加载到表中 load data local inpath '/root/localhost_access_log.2016-02-29.txt' into table logtbl; Beeline 和 Hiveser2 Hiveserver2 是 Hive server的第二个版本。 1、Hiveserver2 启动。 在服务端直接使用命令 hiveserver2 即可 Beeline 是 在 Hive 里的,他在 Hive 安装包下的 bin 目录下 2、启动 Beeline。 所以只要有 Hive 就能启动 Beeline,无论是分开模式还是一体模式。 3、Beeline 连接 hiveserver2 !connect jdbc:hive2://sean01:10000 root 123456 #连接mysql数据库 4、使用 Beeline 查询 查看效果:会发现 Beeline 在格式上进行了美化,也算是优化 注意:在实际工作中,Beeline并不怎么用,因为非常格式上要进行切分,非常消耗资源。 Hive 的 JDBC 一般是平台使用展示或接口,服务端启动hiveserver2后,在java代码中通过调用hive的jdbc访问默认端口10000进行连接、访问。(这种格式使用的比较少) Hive 分区与自定义函数 Hive 的分区(partiton) 假如现在我们公司一天产生3亿的数据量,那么为了方便管理和查询,此时可以建立分区(可按日期 部门等具体业务分区)。分门别类的管理。 注意:必须在表定义时创建 partition ! 分区分为:单分区和多分区 分区分为:静态分区和动态分区 创建分区 A、单分区建表语句 create table day_table(id int, content string) partitioned by (dt string) row format delimited fields terminated by ','; 【单分区表,按天分区,在表结构中存在id,content,dt三列;以dt为文件夹区分】 新建数据文件 hivedata2 在本地 root 下 加载数据到分区表中 load data local inpath '/root/hivedata2' into table day_table partition(dt='2008-08-08'); 查看表信息 查看表结构(使用关键字 desc) 查看 HDFS 注意:在 HDFS 中不能用‘/’和‘-’连接字段,否则会乱码。 B、双分区建表语句 create table day_hour_table (id int, content string) partitioned by (dt string, hour string) row format delimited fields terminated by ','; 【双分区表,按天和小时分区,在表结构中新增加了dt和hour两列;先以dt为文件夹,再以hour子文件夹区分】 加载数据到双分区表中 load data local inpath '/root/hivedata2' into table day_hour_table partition(dt='2008-08-08',hour='01-02'); 查看表信息 查看 HDFS 注意:在创建 删除多分区等操作时一定要注意分区的先后顺序,他们是父子节点的关系。分区字段不要和表字段相同 添加分区表的分区字段 表分区已创建,在此基础上添加具体分区。 比如在建表的时候就已经定义了分区:partitioned by(dt string),我们可以根据这个类型新建多个分区字段,比如按天去分区,每天就是一个文件夹。 语法格式 ALTER TABLE table_nameADD partition_spec [ LOCATION 'location1' ] partition_spec [ LOCATION 'location2' ] ... 例: alter table day_table add partition(dt='2008-08-08');alter table day_table add partition(dt='2008-08-09');alter table day_table add partition(dt='2008-08-10'); 添加的前提是表在创建时已经定义了相应的分区。比如:添加一个单分区字段,前提是在创建这张表时已经定义了这个单分区,多分区同理。 删除分区 语法格式 ALTER TABLE table_name DROP partition_spec, partition_spec,... 用户可以用 ALTER TABLE DROP PARTITION 来删除分区。分区的元数据和数据将被一并删除。 例: alter table day_hour_table drop partition(dt='2008-08-08'); 数据加载进分区表中 语法格式 LOAD DATA [LOCAL] INPATH 'filepath' [OVERWRITE] INTO TABLE tablename [PARTITION (partcol1=val1,partcol2=val2 ...)] 例如 load data local inpath '/root/hivedata2' into table day_table partition(dt='2008-08-08'); 当数据被加载至表中时,不会对数据进行任何转换,Load操作只是将数据复制至Hive表对应的位置。 数据加载时在表下自动创建一个目录基于分区的查询的语句: select * from day_table where day_table.dt >= '2008-08-08' 查看分区语句 例如: hive> show partitions day_hour_table; 重命名分区 语法格式 ALTER TABLE table_name PARTITION partition_spec RENAME TO PARTITION partition_spec; 例如 alter table day_table partition (tian='2018-05-01') rename to partition (tain='2018-06-01'); 动态分区 1、创建数据 在本地文件 /root/hivedata3 中写入以下4行数据 aaa,US,CAaaa,US,CBbbb,CA,BBbbb,CA,BC 2、建立非分区表并加载数据 创建表 t1 CREATE TABLE t1 (name STRING, cty STRING, st STRING) ROW FORMAT DELIMITED FIELDS TERMINATED BY ','; 加载数据 hivedata3 LOAD DATA LOCAL INPATH '/root/hivedata3' INTO TABLE t1; 查询 t1 3、创建外部分区表(普通分区表也可以) CREATE EXTERNAL TABLE t2 (name STRING) PARTITIONED BY (country STRING, state STRING); 这时就需要使用动态分区来实现。 使用动态分区需要注意设定以下参数: - hive.exec.dynamic.partition默认值:true是否开启动态分区功能,默认true开启。使用动态分区时候,该参数必须设置成true;- hive.exec.dynamic.partition.mode默认值:strict动态分区的模式,默认strict,表示必须指定至少一个分区为静态分区,nonstrict模式表示允许所有的分区字段都可以使用动态分区。一般需要设置为nonstrict- hive.exec.max.dynamic.partitions.pernode默认值:100在每个执行MR的节点上,最大可以创建多少个动态分区。该参数需要根据实际的数据来设定。比如:源数据中包含了一年的数据,即day字段有365个值,那么该参数就需要设置成大于365,如果使用默认值100,则会报错。- hive.exec.max.dynamic.partitions默认值:1000在所有执行MR的节点上,最大一共可以创建多少个动态分区。同上参数解释。- hive.exec.max.created.files默认值:100000整个MR Job中,最大可以创建多少个HDFS文件。一般默认值足够了,除非你的数据量非常大,需要创建的文件数大于100000,可根据实际情况加以调整。- hive.error.on.empty.partition默认值:false当有空分区生成时,是否抛出异常。一般不需要设置。 比如设定 当前动态分区的模式为非严格模式 nonstrict 4、插入数据 将 t1 表的数据动态插入到 t2 表中 INSERT INTO TABLE t2 PARTITION (country, state) SELECT name, cty, st FROM t1; 查询 t2 自定义函数 常用函数无法满足日常需求,比如脱敏:去掉敏感数据 自定义函数包括三种:UDF、UDAF、UDTF UDF:一进一出 UDAF:聚集函数,多进一出。如:Count/max/min UDTF:一进多出,如 lateralview 侧视图 explore() 使用方式 :在HIVE 会话中 add 自定义函数的 jar 文件,然后创建 function 继而使用函数。 UDF 开发(常用) 1、UDF 函数可以直接应用于 select 语句,对查询结构做格式化处理后,再输出内容。 2、编写 UDF 函数的时候需要注意一下几点: a)自定义 UDF 需要继承类 org.apache.hadoop.hive.ql.UDF。 b)需要实现 evaluate 函数,evaluate 函数支持重载。 源码: package com.hive.udf;import org.apache.hadoop.hive.ql.exec.UDF;import org.apache.hadoop.io.Text;public class TuoMin extends UDF{ private Text res = new Text(); public Text evaluate(String str){ if (str == null) { return null; }// 1***0 String first = str.substring(0, 1); String last = str.substring(str.length()-1, str.length()); res.set(first + "***" + last); return res; }} 3、步骤 a)把程序打包放到目标机器上去; b)进入 hive 客户端,添加 jar 包: 因为 jar 包添加到环境变量中,所以不用的时候要删除 hive > add jar /root/TuoMin.jar;(清除缓存时记得删除jar包 delete jar /root/TuoMin.jar;) c)创建临时函数: hive > create temporary function tuomin as 'com.hive.udf.TuoMin'; d)查询 HQL 语句: 使用自定义函数 tuomin 来查询表 abc 的 name 字段 表 abc 原数据 进行脱敏 select tuomin(name) from abc; e)销毁临时函数: hive > drop temporary function tuomin; UDAF 自定义集函数(不用) 多行进一行出,如 sum()、min(),用在 group by 时 1、必须继承org.apache.hadoop.hive.ql.exec.UDAF(函数类继承) org.apache.hadoop.hive.ql.exec.UDAFEvaluator(内部类 Eval uator 实现 UDAFEvaluator 接口) 2、Evaluator 需要实现 init、iterate、terminatePartial、merge、t erminate 这几个函数 init():类似于构造函数,用于 UDAF 的初始化 iterate():接收传入的参数,并进行内部的轮转,返回 boolean terminatePartial():无参数,其为 iterate 函数轮转结束后,返回轮转数据,类似于 hadoop 的 Combinermerge(),用于接收 terminatePartial 的返回结果,进行数据 merge 操作,其返回类型为 boolean terminate():返回最终的聚集函数结果 UDTF (不常用) 一进多出,如 lateral view explode() 案例实战 基站掉话率 1、创建原始表 create table cell_monitor(record_time string,imei string,cell string,ph_num int,call_num int,drop_num int,duration int,drop_rate DOUBLE,net_type string,erl string)ROW FORMAT DELIMITED FIELDS TERMINATED BY ','STORED AS TEXTFILE; 2、结果表 create table cell_drop_monitor(imei string,total_call_num int,total_drop_num int,d_rate DOUBLE) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\\t'STORED AS TEXTFILE; 3、加载原始数据 LOAD DATA LOCAL INPATH '/root/cdr_summ_imei_cell_info.csv' OVERWRITE INTO TABLE cell_monitor; 4、找出掉线率最高的基站 from cell_monitor cm insert overwrite table cell_drop_monitor select cm.imei ,sum(cm.drop_num),sum(cm.duration),sum(cm.drop_num)/sum(cm.duration) d_rate group by cm.imei sort by d_rate desc; 5、查询结果表 比如查询前十条数据 select * from cell_drop_monitor limit 10; WC 单词统计 案例:做一个单词统计 例如:新建一个叫 wc 的文件 1、建表 create table docs(line string); #原表create table wc(word string, totalword int); #统计结果表 2、加载原始数据 load data local inpath '/root/wc' into table docs; 3、统计 from (select explode(split(line, ' ')) as word from docs) w insert into table wc select word, count(1) as totalword group by word order by word; 4、查询结果 select * from wc; 分桶 分桶表及应用场景 分桶表是对列值取哈希值求余的方式,将不同数据放到不同文件中存储。 对于hive中每一个表、分区都可以进一步进行分桶。由列的哈希值除以桶的个数来决定每条数据划分在哪个桶中。 适用场景: 数据抽样( sampling )、map-join 开启支持分桶 set hive.enforce.bucketing=true; 默认:false;设置为 true 之后,mr 运行时会根据bucket的个数自动分配 reduce task 个数。(用户也可以通过mapred.reduce.tasks 自己设置 reduce 任务个数,但分桶时不推荐使用) 注意:一次作业产生的桶(文件数量)和 reduce task 个数一致。 往分桶表中加载数据 insert into table bucket_table select columns from tbl;insert overwrite table bucket_table select columns from tbl; #二者选其一 桶表 抽样查询 select * from bucket_table tablesample(bucket 1 out of 4 on columns); #抽样函数 tablesample 语法: tablesample(bucket x out of y) x:表示从哪个bucket开始抽取数据 y:必须为该表总bucket数的倍数或因子 1、创建普通表 例: CREATE TABLE mm( id INT, name STRING, age INT)ROW FORMAT DELIMITED FIELDS TERMINATED BY ','; 测试数据: 1,tom,112,cat,223,dog,334,hive,445,hbase,556,mr,667,alice,778,scala,88 在本地 root 下创建一个 hivedata4 的文件 2、创建分桶表 create table psnbucket( id int, name string, age int)clustered by (age) into 4 buckets row format delimited fields terminated by ','; 3、加载数据到普通表 load data local inpath '/root/hivedata4' into table mm; 4、插入数据到分桶表中 insert into table psnbucket select id, name, age from mm; 5、抽样 select id, name, age from psnbucket tablesample(bucket 1 out of 4 on age);select id, name, age from psnbucket tablesample(bucket 2 out of 4 on age);select id, name, age from psnbucket tablesample(bucket 3 out of 4 on age);select id, name, age from psnbucket tablesample(bucket 4 out of 4 on age); 从第一个桶开始抽一直到第四个 总结:我们互发现,在分桶抽样中,抽取数据会尽量分散去取 分析图: Hive Lateral View(视图) Lateral View 用于和 UDTF 函数(explode、split)结合来使用。 首先通过 UDTF 函数把一个语句拆分成多行,然后通过 Lateral View 将多行结果组合成一个支持别名的虚拟表,也就是合并为一个字段。 主要解决在select使用UDTF做查询过程中,查询只能包含单个UDTF,不能包含其他字段、以及多个UDTF的问题。 简单说:UDTF 先一进多出,Lateral View 拿到多条数据后多进一出。 语法: lateral view udtf(expression) tableAlias as columnAlias (',' columnAlias) 例: 统计人员表中共有多少种爱好、多少个城市? 数据来自表 abc HQL语句: select count(distinct(myCol1)), count(distinct(myCol2))) from abc lateral view explode(likes) myTable1 as myCol1 lateral view explode(address) myTable2 as myCol2, myCol3; #切记。虽然myCol3没用到,但是这里不能删,否则会因为数据个数不匹配报错,因为数据中有两种地址:学习地址和工作地址。 显示结果:一共7种爱好、2个学习地址。 运行方式 **1、命令行方式 Cli:**控制台模式 与 hdfs 交互 执行 dfs 命令:例如 dfs -ls / 与 Linux 交互 ! 开头:例如 !pwd **2、脚本运行方式:**实际生产环境中使用最多 hive -e " SQL 语句 " hive -e " " > aaa hive -S -e “” > aaa -S意思是静默模式,不打印 log 日志信息 hive -f file(file里可以放很多SQL语句) hive -i /home/my/hive-init.sql hive > source file (在 hive cli 中运行) 3、JDBC 方式 hiveserver2 4、Web GUI 接口 hwi、hue等 Hive Web GUI接口 web界面安装: 1、下载源码包apache-hive-*-src.tar.gz 2、将hwi war包放在$HIVE_HOME/lib/ 制作方法:将hwi/web/*里面所有的文件打成war包 cd apache-hive-1.2.1-src/hwi/web jar -cvf hive-hwi.war * 3、复制tools.jar(在jdk的lib目录下)到$HIVE_HOME/lib下 4、修改hive-site.xml <property> <name>hive.hwi.listen.host</name> <value>0.0.0.0</value> </property> <property> <name>hive.hwi.listen.port</name> <value>9999</value> </property> <property> <name>hive.hwi.war.file</name> <value>lib/hive-hwi.war</value> </property> 5、启动hwi服务(端口号9999) hive --service hwi 6、浏览器通过以下链接来访问 http://sean03:9999/hwi/ Hive参数与变量 Hive当中的参数、变量 hive当中的参数、变量,都是以命名空间开头 通过${}方式进行引用,其中system、env下的变量必须以前缀开头。 Hive 参数设置方式 1、修改配置文件 ${HIVE_HOME}/conf/hive-site.xml 2、启动hive cli时,通过–hiveconf key=value的方式进行设置 例:hive --hiveconf hive.cli.print.header=true 3、进入cli之后,通过使用set命令设置 Hive set命令 在 hive CLI 控制台可以通过set对hive中的参数进行查询、设置。 set 设置: set hive.cli.print.header=true; set 查看: set hive.cli.print.header hive参数初始化配置 当前用户家目录下的.hiverc文件,如: ~/.hiverc 如果没有,可直接创建该文件,将需要设置的参数写到该文件中,hive启动运行时,会加载改文件中的配置。 hive历史操作命令集 ~/.hivehistory 权限管理 权限类别 1、Storage Based Authorization in the Metastore Server基于存储的授权 可以对Metastore中的元数据进行保护,但是没有提供更加细粒度的访问控制(例如:列级别、行级别)。 2、Default Hive Authorization (Legacy Mode) hive默认授权 设计目的仅仅只是为了防止用户产生误操作,而不是防止恶意用户访问未经授权的数据。 3、SQL Standards Based Authorization in HiveServer2基于SQL标准的Hive授权 完全兼容SQL的授权模型,推荐使用该模式。 基于SQL标准Hive授权模型,除支持对于用户的授权认证,还支持角色role的授权认证,role可理解为是一组权限的集合,通过role为用户授权。一个用户可以具有一个或多个角色,默认包含俩种角色:public、admin 授权限制 1、启用当前认证方式之后,dfs, add, delete, compile, and reset等命令被禁用。 2、通过set命令设置hive configuration的方式被限制某些用户使用。 (可通过修改配置文件hive-site.xml中hive.security.authorization.sqlstd.confwhitelist进行配置) 3、添加、删除函数以及宏(批量规模)的操作,仅为具有admin的用户开放。 4、用户自定义函数(开放支持永久的自定义函数),可通过具有admin角色的用户创建,其他用户都可以使用。 5、Transform功能被禁用 在 hive 服务端修改配置文件 hive-site.xml 添加以下配置内容: <property> <name>hive.security.authorization.enabled</name> <value>true</value> //开启用户权限认证</property><property> <name>hive.server2.enable.doAs</name> <value>false</value> //默认true,设为false查询将以运行hiveserver2进程的用户运行</property><property> <name>hive.users.in.admin.role</name> <value>root</value> //超级管理员角色用户</property><property> <name>hive.security.authorization.manager</name><value>org.apache.hadoop.hive.ql.security.authorization.plugin.sqlstd.SQLStdHiveAuthorizerFactory</value></property><property> <name>hive.security.authenticator.manager</name> <value>org.apache.hadoop.hive.ql.security.SessionStateUserAuthenticator</value></property> 服务端启动hiveserver2;客户端通过beeline进行连接 角色的添加、删除、查看、设置: CREATE ROLE role_name; – 创建角色 DROP ROLE role_name; – 删除角色 SET ROLE (role_name|ALL|NONE); – 设置角色 SHOW CURRENT ROLES; – 查看当前具有的角色 SHOW ROLES; – 查看所有存在的角色 思考总结 1、Hive中的自定义函数的jar包放置 Hive自定义的函数jar包最好放置到分布式存储系统HDFS上,这样可以有效的防止当前节点服务器发生意外宕机,jar 包丢失。","tags":[{"name":"Hive","slug":"Hive","permalink":"http://www.seanxia.cn/tags/Hive/"},{"name":"数据仓库","slug":"数据仓库","permalink":"http://www.seanxia.cn/tags/%E6%95%B0%E6%8D%AE%E4%BB%93%E5%BA%93/"}]},{"title":"深入Zookeeper","date":"2017-06-26T16:00:00.000Z","path":"大数据/9acfc12.html","text":"什么是 Zookeeper Zookeeper 简介 ZooKeeper 是一个开源的、分布式应用程序协调服务,是 Google 的 Chubby 一个开源的实现,是 Hadoop 和 Hbase 的重要组件。 它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。 **ZooKeeper:**提供通用的分布式锁服务,用以协调分布式应用。 **Keepalived:**监控节点不好管理,采用优先级监控。但是没有协同工作;功能单一;可扩展性差。 Zookeeper 的角色 领导者(leader),负责进行投票的发起和决议,更新系统状态。 学习者(learner),包括跟随者(follower)和观察者(observer), follower 用于接受客户端请求并想客户端返回结果,在选主过程中参与投票。 **Observer **可以接受客户端连接,将写请求转发给leader,但observer不参加投票过程,只同步 leader 的状态,observer 的目的是为了扩展系统,提高读取速度。 Zookeeper 需保证高可用和强一致性;为了支持更多的客户端,需要增加更多Server;Server增多,投票阶段延迟增大,影响性能; (1) 权衡伸缩性和高吞吐率,引入 Observer。 (2) Observers 接受客户端的连接,并将写请求转发给 leader 节点;加入更多Observer 节点,提高伸缩性,同时不影响吞吐率。 Zookeeper 的特点 Zookeeper 的安装配置 深入 Zookeeper 原理,Paxos 算法 Zookeeper 的节点及工作原理 Zookeeper节点 1. 节点分类 (1) 短暂的(ephemeral) 短暂 znode 的客户端会话结束时,zookeeper 会将该短暂 znode 删除,短暂 znode 不可以有子节点。 (2) 持久的(persistent) 持久 znode 不依赖于客户端会话,只有当客户端明确要删除该持久 znode 时才会被删除。 2. 目录节点 Znode又有四种形式的目录节点: PERSISTENT 持久的 EPHEMERAL 短暂的 PERSISTENT_SEQUENTIAL 持久且有序的 EPHEMERAL_SEQUENTIAL 短暂且有序的 注意:Znode 的类型在创建时确定并且之后不能再修改 Zookeeper工作原理 (1) 每个Server在内存中存储了一份数据; (2) Zookeeper启动时,将从实例中选举一个leader(Paxos协议) (3) Leader负责处理数据更新等操作 (4) 一个更新操作成功,当且仅当大多数Server在内存中成功修改数据。 Zookeeper的核心是原子广播,这个机制保证了各个server之间的同步。实现这个机制的协议叫做Zab协议。 Zab协议有两种模式,它们分别是恢复模式和广播模式。 当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数server的完成了和leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和server具有相同的系统状态。一旦leader已经和多数的follower进行了状态同步后,他就可以开始广播消息了,即进入广播状态。这时候当一个server加入zookeeper服务中,它会在恢复模式下启动,发现leader,并和leader进行状态同步。待到同步结束,它也参与消息广播。Zookeeper服务一直维持在Broadcast状态,直到leader崩溃了或者leader失去了大部分的followers支持. 广播模式需要保证proposal被按顺序处理,因此zk采用了**递增的事务id号(zxid)**来保证。所有的提议(proposal)都在被提出的时候加上了zxid。实现中zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch。低32位是个递增计数。 当leader崩溃或者leader失去大多数的follower,这时候zk进入恢复模式,恢复模式需要重新选举出一个新的leader,让所有的server都恢复到一个正确的状态。 Zookeeper 的方法(API)","tags":[{"name":"Zookeeper","slug":"Zookeeper","permalink":"http://www.seanxia.cn/tags/Zookeeper/"},{"name":"角色","slug":"角色","permalink":"http://www.seanxia.cn/tags/%E8%A7%92%E8%89%B2/"},{"name":"paxos","slug":"paxos","permalink":"http://www.seanxia.cn/tags/paxos/"}]},{"title":"分布式计算框架MapReduce","date":"2017-06-18T16:00:00.000Z","path":"大数据/f3219d4.html","text":"MapReduce 是什么 1. 概念 MapReduce 是一种分布式的离线计算框架,是一种编程模型,用于大规模数据集(大于1TB)的并行运算。将自己的程序运行在分布式系统上。 **概念是:"Map(映射)“和"Reduce(归约)”。**指定一个Map(映射)函数,用来把一组键值对映射成一组新的键值对,指定并发的Reduce(归约)函数,用来保证所有映射的键值对中的每一个共享相同的键组。 2. 应用场景 可应用于大规模的算法图形处理、文字处理。 MapReduce 的设计理念 1. 分布式计算 分布式计算将该应用分解成许多小的部分,分配给多台计算机节点进行处理。这样可以节约整体计算时间,大大提高计算效率。 2. 移动计算,而不是移动数据 移动计算是随着移动通信、互联网、数据库、分布式计算等技术的发展而兴起的新技术。移动计算它的作用是将有用、准确、及时的信息提供给任何时间、任何地点的任何客户(这里我们说的是将计算程序应用移动到具有数据的集群计算机节点之上进行计算操作,也就是:计算向数据靠拢)。 MapReduce 计算框架的组成 Mapper(分) Map-Reduce的思想就好比太极又好比天下大势。合久必分,分久必合。 1. Mapper 负责“分”,即把得到的复杂的任务分解为若干个“简单的任务”执行。 “简单的任务”有几个含义: 1、数据或计算规模相对于原任务要大大缩小; 2、就近计算,即会被分配到存放了所需数据的节点进行计算; 3、这些小任务可以并行计算,彼此间几乎没有依赖关系 Split规则: – max.split(100M) – min.split(10M) – block(128M) max(min.split,min(max.split,block)) split实际 = block大小 2. Map 的数目通常是由输入数据的大小决定的,一般就是所有输入文件的总块(block)数。 Reduce(合) 1. Reduce的任务是对map阶段的结果进行“汇总”并输出。 Reducer 的数目由 mapred-site.xml 配置文件里的项目mapred.reduce.tasks决定。缺省值为1,用户可以覆盖之。 Shuffle过程 在 Mapper 和 Reducer 中间的一个步骤,就是 Shuffle。 shuffle的作用: (1) Shuffle 可以把 mapper 的输出按照某种 key 值重新切分和组合成 n 份,把 key 值符合某种范围的输出送到特定的 reducer 那里去处理。 (2) 可以简化 reducer 过程。 2. Shuffle的过程理解 Map 端: 1、Collec 阶段数据放在环形缓冲区,环形缓冲区分为数据区和索引区 2、Sort 阶段对在同一 partition 内的索引按照 key 值排序。 3、Spill(溢写)阶段根据拍好序的索引将数据按顺序写到文件中。 4、Merge 阶段将 Spill 生成的小文件分批合并排序(二次排序)成一个大文件。 Reduce 端: 1、Copy 阶段将Map段的数据分批拷贝到Reduce的缓冲区。 2、Spill 阶段将内存缓冲区的数据按照顺序写到文件中。 3、Merge 阶段将溢出文件合并成一个排好序的数据集。 Combine优化 整个过程中可以提前对聚合好的value值进行计算,这个过程就叫Combine。 map端 在数据排序后,溢写到磁盘前,相同key值的value是紧挨在一起的,可以进行聚合运算,运行一次combiner。 再合并溢出文件输出到磁盘前,如果存在至少3个溢出文件,则运行combiner,可以通过min.num.spills.for.combine设置阈值。 Reduce端 在合并溢出文件输出到磁盘前,运行combiner。 Combiner不是任何情况下都适用的,需要根据业务需要进行设置。 MR 框架计算流程 MR的具体执行步骤: 1、一个文件分成多个split数据片。 2、每个split由多一个map进行处理。 3、Map处理完一个数据就把处理结果放到一个环形缓冲区内存中。 4、环形缓冲区满后里面的数据会被溢写到一个个小文件中。 5、小文件会被合并成一个大文件,大文件会按照partition进行排序。 6、reduce节点将所有属于自己的数据从partition中拷贝到自己的缓冲区中,并进行合并。 7、最后合并后的数据交给reduce处理程序进行处理。处理后的结果存放到HDFS上。 **注意:**在 Map 阶段 如果存在大量重复的统计工作,就需要 Combiner了。 Combiner 继承的是 Reduce 类:作用是对 Map 的阶段聚合统计操作,减小后期 Reduce 的重复统计压力。 MR 架构 MapReduce 采用主从架构的架构思想,包括主(JobTracker)和从(TaskTracker)。 1. 主 JobTracker:RM(ResourceManager) 负责调度分配每一个子任务 task 运行于 TaskTracker上,如果发现有失败的task就重新分配其任务到其他节点。每一个 hadoop 集群中只一个 JobTracker,一般它运行在 Master 节点上。 2. 从 TaskTracker:NM(NodeManager) TaskTracker 主动与 JobTracker 通信,接收作业,并负责直接执行每一个任务,为了减少网络带宽 TaskTracker 最好运行在 HDFS 的 DataNode 上。 MapReduce 环境配置及安装 1. 项目部署规划 2. 具体步骤 1、关于Hadoop2.X集群的配置前面在前一期已做详细讲解,这里步骤相同。在保证 Hadoop 集群正常启动之后停掉 HA 集群。 2、配置 mapred-site.xml 和 yarn-site.xml 文件 mapred-site.xml <configuration> <property> <name>mapreduce.framework.name</name> <value>yarn</value> </property></configuration> yarn-site.xml <configuration> <property> <name>yarn.nodemanager.aux-services</name> <value>mapreduce_shuffle</value> </property> <property> <name>yarn.resourcemanager.ha.enabled</name> <value>true</value> </property> <property> <name>yarn.resourcemanager.cluster-id</name> <value>mr_xss</value> </property> <property> <name>yarn.resourcemanager.ha.rm-ids</name> <value>rm1,rm2</value> </property> <property> <name>yarn.resourcemanager.hostname.rm1</name> <value>sean02</value> </property> <property> <name>yarn.resourcemanager.hostname.rm2</name> <value>sean03</value> </property> <property> <name>yarn.resourcemanager.zk-address</name> <value>sean01:2181,sean02:2181,sean03:2181</value> </property></configuration> 3、将节点 sean01 配置发送到另外两个节点 scp -r mapred-site.xml yarn-site.xml sean02:`pwd` 4、分别启动3个节点的 Zookeeper zkServer.sh start 5、启动三个节点的 Hadoop start-all.sh #这个命令相当于 start-dfs.sh + start-yarn.sh 使用命令 jps 查看当前进程 这里会发现,虽然日志显示启动了ResourceManager,但进程中并没有,所以需要手动启动 6、手动启动节点 sean02 和 sean03 的 ResourceManager yarn-daemon.sh start resourcemanager 7、去浏览器web端查看 查看NameNode sean01 节点 sean02 节点 可以看出节点 sean02 为激活状态。 查看 ResourceManager 默认端口:8088 sean03节点 sean02节点 会发现由于节点 sean03 已经启动了 SM,所以后启动的节点 sean02 就成了备用。并且在片刻之后会跳转至具有 SM 的节点sean3。 8、关闭 MR 集群 注意关闭顺序: 先关Yarn再关hadoop再关Zookeeper,注意:在NN的节点上执行 stop-all.sh #代表关闭Hadoop和yarn的所有进程 再手动关闭Yarn的ResourceManager yarn-daemon.sh stop resourcemanager 再关掉三台几点的 Zookeeper zkServer.sh stop 最后查看进程确保全部关闭成功","tags":[{"name":"MapReduce","slug":"MapReduce","permalink":"http://www.seanxia.cn/tags/MapReduce/"},{"name":"离线计算","slug":"离线计算","permalink":"http://www.seanxia.cn/tags/%E7%A6%BB%E7%BA%BF%E8%AE%A1%E7%AE%97/"}]},{"title":"Hadoop2.X","date":"2017-05-26T16:00:00.000Z","path":"大数据/205c61f9.html","text":"Hadoop 2.0 产生背景 根本原因:Hadoop 1.0 中 HDFS 和 MapReduce 在高可用、扩展性等方面存在问题。 1. HDFS 存在的问题 NameNode 单点故障,难以应用于在线场景。 NameNode 压力过大,且内存受限,影响系统扩展性 2. MapReduce 存在的问题 JobTracker 访问压力大,影响系统扩展性。 难以支持除 MapReduce 之外的计算框架,比如 Spark、Storm 等。 Hadoop 2.X 1. 系统架构 Hadoop 2.x 由 HDFS、MapReduce 和 YARN 三个分支构: HDFS:分布式文件存储系统 YARN:资源管理系统 MapReduce:运行在YARN上的MR 2. 解决的问题 解决单点故障问题 HDFS HA:通过主备 NameNode 解决,如果主 NameNode 发生故障,则切换到备 NameNode 上。 解决内存受限问题 HDFS Federation(联邦机制)、HA 3. 新的变化 2.x 支持2个 NN 节点的 HA,3.0实现了 NN 一主多从。 水平扩展,支持多个 NameNode 每个 NameNode 分管一部分目录 所有 NameNode 共享所有 DataNode 存储资源 注意:2.x仅是架构上发生了变化,使用方式不变 HDFS 2.0 HA高可用 NameNode的HA 1、实现原理 配置的HA的HDFS,所有NameSpace的NameNode节点在启动加载完元数据之后都处于Standby状态,之后被手动或自动的选择一个NameNode节点作为Active节点而开始正常工作。HA的自动方式是通过在每一个NameNode的本地启动一个守护进程 ZKFailoverController 来竞争Active NameNode的,ZKFailoverController除了为本地的NameNode争取Active角色之外,还负责监控本地的NN节点当前是否正常的,一旦它发现本地的NN不正常,就会主动替当前的Active NN退出Active角色或退出Active的竞争。 2、JN (JournalNode) 实现主备 NN(NameNode) 间的数据共享 主备 NameNode 解决单点故障 主 NameNode 对外提供服务,备 NameNode 同步主 NameNode 元数据,以待切换,所有 DataNode 同时向两个 NameNode 汇报数据块信息。 两种切换选择 手动切换:通过命令实现主备之间的切换,可以用 HDFS 升级等场合。 自动切换:基于 Zookeeper 实现。 基于 **Zookeeper **自动切换方案 Federation联邦机制 在Hadoop2.0之前,HDFS的单NameNode设计带来诸多问题: 单点故障、内存受限,制约集群扩展性和缺乏隔离机制(不同业务使用同一个NameNode导致业务相互影响)等。为了解决这些问题,除了用基于共享存储的HA解决方案我们还可以用HDFS的Federation机制来解决这个问题。【单机namenode的瓶颈大约是在4000台集群,而后则需要使用联邦机制】 1、什么是Federation机制 Federation是指HDFS集群可使用多个独立的NameSpace(NameNode节点管理)来满足HDFS命名空间的水平扩展 这些NameNode分别管理一部分数据,且共享所有DataNode的存储资源。 NameSpace之间在逻辑上是完全相互独立的(即任意两个NameSpace可以有完全相同的文件名)。在物理上可以完全独立(每个NameNode节点管理不同的DataNode)也可以有联系(共享存储节点DataNode)。一个NameNode节点只能管理一个Namespace。 2、Federation机制解决单NameNode存在的以下几个问题 (1)HDFS集群扩展性。每个NameNode分管一部分namespace,相当于namenode是一个分布式的。 (2)性能更高效。多个NameNode同时对外提供服务,提供更高的读写吞吐率。 (3)良好的隔离性。用户可根据需要将不同业务数据交由不同NameNode管理,这样不同业务之间影响很小。 (4)Federation良好的向后兼容性,已有的单Namenode的部署配置不需要任何改变就可以继续工作。 3、Federation不足之处 HDFS Federation并没有完全解决单点故障问题。虽然namenode/namespace存在多个,但是从单个namenode/namespace看,仍然存在单点故障。因此 Federation中每个namenode配置成HA高可用集群,以便主namenode挂掉一下,用于快速恢复服务。 4、Federation架构 (1)为了水平扩展namenode,federation使用了多个独立的namenode/namespace。 这些namenode之间相互独立且不需要互相协调,各自分工,管理自己的区域。分布式的datanode被用作通用的数据块存储存储设备。每个datanode要向集群中所有的namenode注册,且周期性地向所有namenode发送心跳和块报告,并执行来自所有namenode的命令。 (2)每个namenode维护一个命名空间卷(namespace volume)。 由命名空间的元数据和一个数据块池组成,数据块池(block pool)包含该命名空间下文件的所有数据块。 (3)命名空间卷之间相互独立 两两之间并不互相通信,甚至其中一个namenode的失效也不会影响由其他namenode维护的命名空间的可用性。 (4)一个namespace和它的blockpool作为一个管理单·元(称为namespace volume) 数据块池不再切分,则集群中的DataNode需要注册到每个namenode,并且存储着来自多个数据块池中的数据块。当namenode/namespace被删除后,其所有datanode上对应的block pool也会被删除。集群升级时,这个管理单元也独立升级。 HDFS-2.0 HA+Federation的总体设计架构图 YARN介绍 核心思想与架构 将MRv1中JobTracker的资源管理和任务调度两个功能分开,分别由ResourceManager和ApplicationMaster进程实现。 1. ResourceManager 负责整个集群的资源管理和调度。 2. ApplicationMaster 负责应用程序相关的事务,比如任务调度、任务监控和容错等。 3. YARN 的基本架构图 4. 以YARN为核心的生态系统 YARN 的作用 YARN的引入,使得多个计算框架可运行在一个集群中 每个应用程序对应一个 ApplicationMaster(应用程序控制-主人),目前多个计算框架可以运行在YARN上,比如 MapReduce、Spark、Storm 等。 YARN 资源管理任务调度 yarn-Client提交任务方式 1. 配置 在client节点配置中spark-env.sh添加Hadoop_HOME的配置目录即可提交yarn 任务。 注意client只需要有Spark的安装包即可提交任务,不需要其他配置(比如slaves)!!! 2. 执行流程(重要理解) (1) 客户端启动一个Driver进程来提交一个 Application。 (2) Driver 进程会向 RS(ResourceManager) 发送请求,启动 AM (AplicationMaster) 的资源。 (3) RS收到请求,随机选择一台 NM(NodeManager) 启动AM。这里的 NM 相当于Standalone 中的 Worker 节点。 (4) AM 启动后,会向RS请求一批 container 资源,用于启动 Executor。 (5) RS 会找到一批 NM 返回给 AM,用于启动 Executor。 (6) AM 会向 所有的 NM 发送命令启动 Executor。 (7) Executor 启动后,会反向注册给 Driver,Driver 发送 task 到 Executor,执行情况和结果返回给 Driver 端。 3. 总结 (1) Yarn-client 模式同样是适用于测试,因为 Driver 运行在本地,Driver 会与yarn 集群中的 Executor 进行大量的通信,会造成客户机网卡流量的大量增加。 (2) ApplicationMaster 的作用: 为当前的 Application 申请资源 给 NodeManager 发送消息启动 Executor。 注意:ApplicationMaster 有 launchExecutor 和申请资源的功能,并没有作业调度的功能。 yarn-Cluster提交任务方式 1. 执行流程 其实 yarn-Cluster 的提交方式跟 yarn-Client 基本相同,不同的只是 Driver 进程不在客户端启动,而是在ApplicationMaster 中,此时 AM 就相当于 Driver 端。 (1) 客户机提交 Application 应用程序,发送请求到 RS (ResourceManager),请求启动 AM (ApplicationMaster)。 (2) RS 收到请求后随机在一台 NM (NodeManager) 上启动 AM(相当于 Driver端)。 (3) AM 启动,**AM 发送请求到 RS,**请求一批 container 用于启动 Executor。 (4) RS 返回一批 NM 节点给 AM。 (5) AM 连接到 NM,发送请求到 NM 启动 Executor。 (6) **Executor 反向注册到 AM **所在的节点的Driver。Driver发送task到Executor。 2. 总结 (1) Yarn-Cluster主要用于生产环境中,因为Driver运行在Yarn集群中某一台nodeManager中,每次提交任务的Driver 所在的机器都是随机的**,不会产生某一台机器网卡流量激增的现象,**缺点是任务提交后不能看到日志。只能通过 yarn 查看日志。 (2) ApplicationMaster的作用: 为当前的Application申请资源 给nodemanager发送消息 启动Excutor。 任务调度。(这里和client模式的区别是AM具有调度能力,因为其就是Driver端,包含Driver进程) (3) 停止集群任务命令:yarn application -kill applicationID **自我思考:**stand-alone模式中Master发送对应的命令启动Worker上的executor进程,而yarn模式中的applimaster也是负责启动worker中的Driver进程,可见都是master负责发送消息,然后再对应的节点上启动executor进程。 MapReduce On YARN MapReduce On YARN 是在 Hadoop2.0 版本开始构建在 MRv2 系统上。 将 MapReduce 作业直接运行在YARN上,而不是由 JobTracker 和 TaskTracker 构建的 MRv1 系统中。 1. 基本功能模块 **YARN:**负责资源管理和调度 **MRAppMaster:**负责任务切分、任务调度、任务监控和容错等 **MapTask/ReduceTask:**任务驱动引擎,与 MRv1 一致 2 . 每个 MapRduce 作业对应一个 MRAppMaster 任务调度 YARN 将资源分配给 MRAppMaster MRAppMaster进一步将资源分配给内部的任务 3. MRAppMaster 容错 失败后,由 YARN 重新启动 任务失败后,MRAppMaster 重新申请资源 Zookeeper ZooKeeper 是一个开源的、分布式应用程序协调服务,是 Google 的 Chubby 一个开源的实现,是 Hadoop 和 Hbase 的重要组件。 关于Zookeeper详细的原理讲解请参照 深入Zookeeper Hadoop2.X 集群搭建 集群规划 NN: NameNode JN: JournalNode ZK: Zookeeper DN: DateNode ZKFC: Zookeeper Failover Controller ==>监控 NameNode 健康状态,并向Zookeeper 注册 NameNode,NameNode 挂掉后,ZKFC 为 NameNode 竞争锁,获得 ZKFC 锁的 NameNode 变为 active。 具体搭建步骤 1、下载上传Hadoop压缩包,并配置环境变量 vim Hadoop-env.sh 2、配置核心xml文件和hdfs文件 core-site.xml <configuration> <property> <name>fs.defaultFS</name> <value>hdfs://Xss</value> </property> <property> <name>ha.zookeeper.quorum</name> <value>sean01:2181,sean02:2181,sean03:2181</value> </property> <property> <name>hadoop.tmp.dir</name> <value>/opt/hadoop2.6.5</value> </property></configuration> hdfs-site.xml <configuration> <property> <name>dfs.nameservices</name> <value>Xss</value> </property> <property> <name>dfs.ha.namenodes.Xss</name> <value>nn1,nn2</value> </property> <property> <name>dfs.namenode.rpc-address.Xss.nn1</name> <value>sean01:8020</value> </property> <property> <name>dfs.namenode.rpc-address.Xss.nn2</name> <value>sean02:8020</value> </property> <property> <name>dfs.namenode.http-address.Xss.nn1</name> <value>sean01:50070</value> </property> <property> <name>dfs.namenode.http-address.Xss.nn2</name> <value>sean02:50070</value> </property> <property> <!-- 指定namenode元数据存储在journalnode中的路径 --> <name>dfs.namenode.shared.edits.dir</name> <value>qjournal://sean01:8485;sean02:8485;sean03:8485/Xss</value> </property> <property> <!-- 指定HDFS客户端连接active namenode的java类 --> <name>dfs.client.failover.proxy.provider.Xss</name> <value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value> </property> <property> <!-- 配置隔离机制为ssh 防止脑裂 --> <name>dfs.ha.fencing.methods</name> <value>sshfence</value> </property> <property> <!-- 指定秘钥的位置 --> <name>dfs.ha.fencing.ssh.private-key-files</name> <value>/root/.ssh/id_rsa</value> </property> <property> <!-- 指定journalnode日志文件存储的路径 --> <name>dfs.journalnode.edits.dir</name> <value>/opt/hadoop2.6.5/data</value> </property> <property> <!-- 开启自动故障转移 --> <name>dfs.ha.automatic-failover.enabled</name> <value>true</value> </property></configuration> 3、准备 zookeeper a) 三台zookeeper:sean01,sean02,sean03 b) 编辑zoo.cfg配置文件 i. 修改dataDir=/opt/zookeeper ii. 加上下面3行 server.1=sean01:2888:3888server.2=sean02:2888:3888server.3=sean03:2888:3888 c) 在 dataDir 目录中创建一个myid的文件,文件内容分别为1,2,3 4、配置 hadoop 中的 slaves 5、发送其他节点服务器 环境变量配置 6、启动三个zookeeper(启动前一定要关闭防火墙,否则失败): zkServer.sh start 7、启动三个JournalNode: hadoop-daemon.sh start journalnode 8、在其中一个namenode上格式化: hdfs namenode -format 9、把刚刚格式化之后的元数据拷贝到另外一个namenode上 a) 启动刚刚格式化的 namenode : hadoop-daemon.sh start namenode b) 在没有格式化的 namenode上执行:hdfs namenode -bootstrapStandby c) 启动第二个namenode:hadoop-daemon.sh start namenode 10、在其中一个namenode上初始化zkfc:hdfs zkfc -formatZK 11、停止上面节点:stop-dfs.sh 12、全面启动:start-dfs.sh 浏览器打开两个 NamwNode Web端 sean01 sean02 查看进程此时在两个 NN 下会多出 ZKFC 文件,就是竞争锁 13、关闭此时处于 Active 状态的 NN 节点 sean02 hadoop-daemon.sh stop namenode 由于此时之前处于active的 NN 节点sean02已经挂掉,sean01迅速拿到竞争锁ZKFC,状态由standby变为active 再重新启动 sean02 节点查看状态。 由于此时的竞争锁在节点sean01手中,所以sean02 节点无法恢复到active状态,只能为standby状态 14、查看 MapReduce 计算框架的状态。 yarn-daemon.sh start resourcemanager (启动资源管理器) 可能出错的地方 1、确认每台机器防火墙均关掉 2、确认每台机器的时间是一致的 3、确认配置文件无误,并且确认每台机器上面的配置文件一样 4、如果还有问题想重新格式化,那么先把所有节点的进程关掉 5、删除之前格式化的数据目录hadoop.tmp.dir属性对应的目录,所有节点同步都删掉,别单删掉之前的一个,删掉三台JN节点中dfs.journalnode.edits.dir属性所对应的目录 6、接上面的第6步又可以重新格式化已经启动了 7、最终 Active Namenode 停掉的时候,StandBy 可以自动接管! Hadoop集群脑裂问题 什么是脑裂 脑裂(brain-split):脑裂是指在主备切换时,由于切换不彻底或其他原因,导致客户端和 Slave 误以为出现两个active master,最终使得整个集群处于混乱状态。 如何解决集群脑裂问题 解决脑裂问题,通常采用隔离(Fencing)机制,包括三个方面: 共享存储fencing:确保只有一个Master往共享存储中写数据。 客户端fencing:确保只有一个Master可以响应客户端的请求。 Slave fencing:确保只有一个Master可以向Slave下发命令。 Hadoop 公共库中对外提供了两种 fencing 实现,分别是 sshfence 和 shellfence(缺省实现),其中 sshfence 是指通过 ssh 登陆目标 Master 节点上,使用命令 fuser 将进程杀死(通过 tcp 端口号定位进程 pid,该方法比 jps 命令更准确),shellfence 是指执行一个用户事先定义的 shell 命令(脚本)完成隔离。 解决脑裂的两种方法: 1、通过 ssh 发送 kill 命令 2、调用用户自定义的脚本程序","tags":[{"name":"Hadoop","slug":"Hadoop","permalink":"http://www.seanxia.cn/tags/Hadoop/"},{"name":"高可用HA","slug":"高可用HA","permalink":"http://www.seanxia.cn/tags/%E9%AB%98%E5%8F%AF%E7%94%A8HA/"}]},{"title":"分布式文件存储系统HDFS","date":"2017-05-11T16:00:00.000Z","path":"大数据/31482ca.html","text":"Hadoop 分布式简介 1. Hadoop 是分布式的系统架构,是Apache基金会顶级金牌项目 注:在Apache中,凡是域名以某个项目名字作为开头的,都是Apache基金会中的顶级金牌项目。 2. Hadoop 的思想之源 来自于Google 03年发布3大论文, GFS、mapreduce、 Bigtable ;Dougcutting用Java实现)。 3. Hadoop 创始人 Hadoop作者Doug cutting,就职Yahoo期间开发了Hadoop项目,目前在Cloudera 公司从事架构工作。 2003-2004年,Google公开了部分GFS和Mapreduce思想的细节,以此为基础Doug Cutting等人用了2年业余时间实现了DFS和Mapreduce机制,一个微缩版:Nutch Hadoop 于 2005 年秋天作为 Lucene的子项目 Nutch的一部分正式引入Apache基金会。 2006 年 3 月份,Map-Reduce分布式离线计算 和 Nutch Distributed File System (NDFS) nutch分布式文件系统分别被纳入称为 Hadoop 的项目中。 分布式文件存储系统 HDFS HDFS 是什么? 面对的大量的数据和如何计算的难题 例如:大量【pb级以上】的网页怎么存储问题 分布式存储系统HDFS (Hadoop Distributed File System)主要解决大数据的存储问题。 HDFS的优缺点 1. 优点 (1) 分布式的特性: 适合大数据处理:GB 、TB 、甚至 PB 级及以上的数据 百万规模以上的文件数量:10K+ 节点。 适合批处理:移动计算而非数据(MR),数据位置暴露给计算框架 (2) 自身特性: 可构建在廉价机器上 高可靠性:通过多副本提高可靠性 高容错性:数据自动保存多个副本;副本丢失后,自动恢复,提供了恢复机制 2. 缺点 (1) 低延迟高数据吞吐访问问题 比如不支持毫秒级 吞吐量大但有限制于其延迟 (2) 小文件存取占用NameNode大量内存(寻道时间超过读取时间(99%)) (3) 不支持文件修改:一个文件只能有一个写入者(深入) 仅支持append不支持修改(其实本身是支持的,主要为了空间换时间,节约成本) HDFS的架构 HDFS的功能模块及原理详解 HDFS 数据存储模型(block) 1. 文件被线性切分成固定大小的数据块 block 通过偏移量offset(单位:byte)标记 默认数据块大小为64MB (hadoop1.x),可自定义配置 若文件大小不到64MB ,则单独存成一个block 2. 一个文件存储方式 按大小被切分成若干个block ,存储到不同节点上 默认情况下每个block都有2个副本,共3个副本 副本数不大于节点数 3. Block大小和副本数通过Client端上传文件时设置,文件上传成功后副本数可以变更,Block Size大小不可变更。 NameNode(简称NN) 1. NameNode主要功能: 接受客户端的读/写服务。 接受 DateNode(DN) 汇报的 block 位置信息。 2. NameNode 保存 metadata 元信息。 metadata 基于内存存储 :不会和磁盘发生交换; metadata元数据信息包括: 文件owership(归属)和 permissions(权限) 文件大小、时间 Block列表[偏移量]:即一个完整文件有哪些block 位置信息 = Block 每个副本保存在哪个 DataNode 中(由DataNode启动时上报给 NN 因为会随时变化,不保存在磁盘)-- 动态的 3. NameNode 的 metadata 信息在启动后会加载到内存 metadata存储到磁盘文件名为”fsimage”的镜像文件 Block的位置信息不会保存到fsimage edits记录对metadata的操作日志 SecondaryNameNode(SNN) 1. 它的主要工作是帮助NN合并edits log文件,减少NN启动时间,它不是NN的备份(但可以做备份)。 2. SNN执行合并时间和机制 根据配置文件设置的时间间隔fs.checkpoint.period 默认3600秒 根据配置文件设置 edits log 大小 fs.checkpoint.size 规定 edits 文件的最大值默认是 64MB 3. SecondaryNameNode SNN合并流程 DataNode(DN) – 存储数据(Block) – 启动DN线程的时候会向 NameNode 汇报 block 位置信息 – 通过向 NN 发送心跳保持与其联系(3秒一次),如果 NN 10分钟没有收到 DN的心跳,则认为其已经 lost,并 copy 其上的 block 到其它 DN Block的副本放置策略 1. 第一个副本: 如果是集群内提交,放置在上传文件的DN;如果是集群外提交,则随机挑选一台磁盘不太满,CPU不太忙的节点。 2. 第二个副本: 放置在于第一个副本不同的机架的节点上。 3. 第三个副本: 与第二个副本相同机架的不同节点。 更多副本:随机节点 注意:空白处为客户端。 HDFS 读写流程 FileSystem是一个通用文件系统的抽象基类,可以被分布式文件系统继承,所有可能使用Hadoop文件系统的代码,都要使用这个类。 Hadoop为FileSystem这个抽象类提供了多种具体实现。 DistributedFileSystem就是FileSystem在HDFS文件系统中的具体实现。 FileSystem的open()方法返回的是一个输入流FSDataInputStream对象,在HDFS文件系统中,具体的输入流就是DFSInputStream;FileSystem中的create()方法返回的是一个输出流FSDataOutputStream对象,在HDFS文件系统中,具体的输出流就是DFSOutputStream。 Configuration conf = new Configuration(); //环境配置FileSystem fs = FileSystem.get(conf); //声明实例FSDataInputStream in = fs.open(new Path(uri)); //声明要打开文件的路径FSDataOutputStream out = fs.create(new Path(uri)); //声明要写出去的文件路径 **备注:**创建一个Configuration对象时,其构造方法会默认加载工程项目下两个配置文件,分别是hdfs-site.xml以及core-site.xml,这两个文件中会有访问HDFS所需的参数值,主要是fs.defaultFS,指定了HDFS的地址(比如hdfs://localhost:9000),有了这个地址客户端就可以通过这个地址访问HDFS了。 1. 读文件过程 (1) 首先客户端通过基类 FileSystem 的 open( ) 方法,其实获取的是它的一个实例 DistributedFileSystem 。 (2) DistributedFileSystem 通过 **RPC协议 **调用 NameNode 来获得文件的第一批 block 的 locations 地址,(同一个 block 按照重复数会返回多个 locations,因为同一文件的 block 分布式存储在不同节点上),这些 locations 按照 hadoop 拓扑结构排序,距离客户端近的排在前面(就近原则选择)。 (3) 前两步会向客户端返回一个输入流对象 FSDataInputStream ,用于给客户端读取数据。FSDataInputStream里面会封装一个DFSInputStream对象(封装在里面看不到),DFSInputStream是专门针对HDFS的实现。DFSInputStream 可以方便的管理 datanode 和 namenode数据流。 (4) 以上完成后,客户端便会在这个输入流之上调用 read( ) 方法,DFSInputStream 最会找出距离客户端最近的datanode 并连接。数据从 datanode 源源不断的流向客户端。 (5) 如果第一个block块的数据读完了,就会关闭指向第一个block块的datanode连接,接着读取下一个block块。这些操作对客户端来说是透明的,从客户端的角度来看只是读一个持续不断的流。 (6) 如果第一批 block 都读完了, DFSInputStream 就会去 namenode 拿下一批 block 的 locations,然后继续读,如果所有的块都读完,客户端调用 close( ) 方法关闭掉所有的流。 读文件的异常情况处理 如果在读数据的时候, DFSInputStream 和 datanode 的通讯发生异常,就会尝试正在读的 block 的排序第二近的 datanode,并且会记录哪个 datanode 发生错误,剩余的 blocks 读的时候就会直接跳过该 datanode。 DFSInputStream 也会检查block 数据校验和,如果发现一个坏的 block,就会先报告到 namenode 节点,然后 DFSInputStream 在其他的 datanode上读该 block 的镜像。 RPC协议 RPC(Remote Procedure Call Protocol)——远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层**。**RPC使得开发包括网络分布式多程序在内的应用程序更加容易。 2. 写文件过程(流水线的复制方式) (1) 客户端通过调用基类 FileSystem 的 create() 方法,获取它的一个实例 DistributedFileSystem 。 (2) DistributedFileSystem 通过 **RPC协议 **调用 namenode,在文件系统的命名空间中创建一个新的文件,创建前, namenode 会做各种校验,比如文件是否存在,客户端有无权限去创建等。如果校验通过, namenode 就会记录下新文件,否则就会抛出 IO 异常。 (3) 前两步结束后,会返回一个输出流 FSDataOutputStream,用于客户端写数据。与读文件的时候相似,FSDataOutputStream里面又封装了一个DFSOutputStream对象。DFSOutputStream也是专门针对HDFS的实现。DFSOutputStream可以协调 namenode 和 datanode。 (4) 以上完成后,客户端调用write( )方法,DFSOutputStream会把数据分成一个个小的 packet,写入到内部队列write quene 。 (5) DataStreamer 会去处理接收 write quene,它先询问 namenode 这个新的 block最适合存储的在哪几个 datanode 里(比如重复数是3,那么就找到3个最适合的datanode),把他们排成一个管道 pipeline 输出。DataStreamer 把 packet 按队列输出到管道的第一个 datanode 中,第一个 datanode 又把 packet 输出到第二个datanode 中,以此类推。 (6) DFSOutputStream 还有一个对列叫 ack quene,也是由 packet 组成,用于等待datanode 的响应。当 pipeline 中的 datanode 都表示已经收到数据的时,ack quene 才会把对应的 packet 包移除掉。 如果在写的过程中某个 datanode 发生错误,会采取以下几步: pipeline被关闭掉; 为了防止防止丢包。ack quene 里的 packet 会同步到 data quene 里;创建新的 pipeline 管道到其他正常 DN 上; 剩下的部分被写到剩下的两个正常的 datanode 中; namenode 找到另外的 datanode 去创建这个块的复制。当然,这些操作对客户端来说是无感知的。 (6) 客户端完成写数据后调用close方法关闭写入流。 完全分布式搭建 完全分布式搭建 1. 环境的准备 Linux 系统 JDK 环境 准备至少3台机器(通过克隆虚拟机;配置好网络JDK 时间 hosts,保证节点间能互ping通) 时间同步 ( ntpdate cn.ntp.org.cn ) ssh免秘钥登录 (两两互通免秘钥) 2. 搭建步骤 (1) 下载解压缩 Hadoop (2) 找到 hadoop 安装包下配置 etc/hadoop/hadoop-env.sh export JAVA_HOME=/usr/java/latest #跟上自己实际的 JDK 路径 (3) 核心配置 core-site.xml (同上都在hadoop安装包的 /etc/hadoop/ 下) fs.defaultFS 默认的服务端口 NameNode URI hadoop.tmp.dir 是hadoop文件系统依赖的基础配置,很多路径都依赖它。如果 hdfs-site.xml 中不配置 namenode 和 datanode 的存放位置,默认就放在这个路径中。 <configuration> <property> <name>fs.defaultFS</name> <value>hdfs://sean01:9000</value> </property> <property> <name>hadoop.tmp.dir</name> <value>/opt/hadoop-2.6.1</value> </property></configuration> (4) HDFS 配置 hdfs-site.xml(同上都在hadoop安装包的 /etc/hadoop/ 下) dfs.datanode.https.address https服务的端口 <configuration> <property> <name>dfs.replication</name> <value>1</value> #备份数量,不写默认3个 </property> <property> <name>dfs.namenode.secondary.http-address</name> <value>sean02:50090</value> </property> <property> <name>dfs.namenode.secondary.https-address</name> <value>sean02:50091</value> </property></configuration> (5) Masters: master 可以做主备的 SNN 在/hadoop-2.6.5/etc/hadoop新建 masters 文件 写上 SNN 节点名: sean02 (6) Slaves: slave 干活的节点 在/home/hadoop-2.5.1/etc/hadoop/slaves 文件中填写 DN 节点名:sean01 sean02 sean03 [ 注意:每行写一个 写成3行 ] (7) 最后将配置好的 Hadoop 通过 SCP 命令发送都其他节点。 scp -r hadoop-2.6.5 sean02:/usr/soft scp -r hadoop-2.6.5 sean03:/usr/soft (8) 配置Hadoop的环境变量 配置局部环境变量:vi ~/.bash_profile export HADOOP_HOME/home/hadoop-2.6.5export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin 然后不要忘记把环境变量发送到另外两个节点 (9) 一定要执行一下(所有节点): source ~/.bash_profile (10) 回到跟目录下对NN进行格式化: hdfs namenode -format (11) 启动HDFS: start-dfs.sh (12) 不要忘记关闭防火墙:service iptables stop (13) 在浏览器输入 sean01:50070 出现以下界面成功(此处输入sean01的前提是windows端已经配置了hosts) 此时会发现 /opt/hadoop-2.6/dfs/ 下会多出一个 data 文件夹 里面存放的就是 DN 的数据 而 sean02 节点的 /opt/hadoop-2.6/dfs/namesecondary/current/ 下存放的就是元数据镜像文件和操作记录 edits HDFS 命令 1. 常用命令 hdfs dfs -du 显示文件(夹)大小 hdfs dfs -mkdir 创建文件夹 hdfs dfs -rm -r path 删除 如果想查看其他命令,可以通过命令 hdfs dfs 直接查看相关参数即可。 2. 举例说明 新建一个文件夹 myhdfs 到 HDFS 根目录,并且当前 root 目录下的 test.txt 文件到 myhdfs 中 然后我们来web 端查看是否成功。 如上显示说明已经创建文件夹并且上传文件成功!","tags":[{"name":"HDFS","slug":"HDFS","permalink":"http://www.seanxia.cn/tags/HDFS/"},{"name":"分布式存储","slug":"分布式存储","permalink":"http://www.seanxia.cn/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E5%AD%98%E5%82%A8/"}]},{"title":"Shell脚本编程","date":"2017-03-21T16:00:00.000Z","path":"大数据/c18cd364.html","text":"Shell 脚本是一种动态语言,编程跟 python、php 编程一样,只要有一个能编写代码的文本编辑器和一个能解释执行的脚本解释器就可以了。 Shell简介 Shell 首先是 UNIX/Linux 下的脚本编程语言,它是解释执行的,无需提前编译。 Shell 同时也是一个程序,它的一端连接着 UNIX/Linux 内核,另一端连接着用户和其它应用程序;换句话说,Shell 是用户和应用程序与内核沟通的桥梁。 Shell 脚本功能非常强大,完全能够胜任 Linux 的日常管理工作,如文本或字符串检索、文件的查找或创建、大规模软件的自动部署、更改系统设置、监控服务器性能、发送报警邮件、抓取网页内容、压缩文件等。 shell分类 Linux 的 Shell 种类众多,常见的有: Bourne Shell(/usr/bin/sh或/bin/sh) Bourne Again Shell(/bin/bash) C Shell(/usr/bin/csh) K Shell(/usr/bin/ksh) Shell for Root(/sbin/sh) 我们关注的是 Bash,也就是 Bourne Again Shell,由于易用和免费,Bash 在日常工作中被广泛使用。同时,Bash 也是大多数Linux 系统默认的 Shell。 在一般情况下,人们并不区分 Bourne Shell 和 Bourne Again Shell,所以,像 #!/bin/sh,它同样也可以改为 #!/bin/bash。 shell运行方式 作为可执行程序 ./test.sh #执行脚本 作为解释器参数 ./bin/sh test.sh 变量 定义变量 your_name="seanxia" 引用变量 name=$your_name 传参 编写接收参数的脚本:vim test.sh #!/bin/bash #!是一种约定标记, 它可以告诉系统这个脚本需要什么样的解释器来执行;echo "第一个参数:$1";echo "第二个参数:$2"; 写完脚本后,要给它赋予可执行权限 chmod 700 test.sh 执行脚本 ./test.sh 1 2 数组 #!/bin/bashmy_array=(A B "C" D)echo "第一个元素为: ${my_array[0]}"echo "第二个元素为: ${my_array[1]}"echo "第三个元素为: ${my_array[2]}"echo "第四个元素为: ${my_array[3]}" 运算符 布尔运算符 假定变量 a 为 10,变量 b 为 20: 运算符 说明 举例 + 加法 expr $a + $b 结果为 30。 - 减法 expr $a - $b 结果为 -10。 * 乘法 expr $a \\* $b 结果为 200。 / 除法 expr $b / $a 结果为 2。 % 取余 expr $b % $a 结果为 0。 = 赋值 a=$b 将把变量 b 的值赋给 a。 == 相等。用于比较两个数字,相同则返回 true。 [ $a == $b ] 返回 false。 != 不相等。用于比较两个数字,不相同则返回 true。 [ $a != $b ] 返回 true。 表达式和运算符之间要有空格,例如 2+2 是不对的,必须写成 2 + 2,这与我们熟悉的大多数编程语言不一样。 完整的表达式要被 `` 包含,注意这个字符不是常用的单引号,在 Esc 键下边。 #!/bin/basha=10b=20val=`expr $a + $b`echo "a + b : $val"val=`expr $a - $b`echo "a - b : $val"val=`expr $a \\* $b`echo "a * b : $val"val=`expr $b / $a`echo "b / a : $val"val=`expr $b % $a`echo "b % a : $val"if [ $a == $b ]then echo "a 等于 b"fiif [ $a != $b ]then echo "a 不等于 b"fi 执行脚本,输出结果如下所示: a + b : 30a - b : -10a * b : 200b / a : 2b % a : 0a 不等于 b 关系运算符 关系运算符只支持数字,不支持字符串,除非字符串的值是数字。 下表列出了常用的关系运算符,假定变量 a 为 10,变量 b 为 20: 运算符 说明 举例 -eq 检测两个数是否相等,相等返回 true。 [ $a -eq $b ] 返回 false。 -ne 检测两个数是否相等,不相等返回 true。 [ $a -ne $b ] 返回 true。 -gt 检测左边的数是否大于右边的,如果是,则返回 true。 [ $a -gt $b ] 返回 false。 -lt 检测左边的数是否小于右边的,如果是,则返回 true。 [ $a -lt $b ] 返回 true。 -ge 检测左边的数是否大于等于右边的,如果是,则返回 true。 [ $a -ge $b ] 返回 false。 -le 检测左边的数是否小于等于右边的,如果是,则返回 true。 [ $a -le $b ] 返回 true。 #!/bin/basha=10b=20if [ $a -eq $b ]then echo "$a -eq $b : a 等于 b"else echo "$a -eq $b: a 不等于 b"fiif [ $a -ne $b ]then echo "$a -ne $b: a 不等于 b"else echo "$a -ne $b : a 等于 b"fiif [ $a -gt $b ]then echo "$a -gt $b: a 大于 b"else echo "$a -gt $b: a 不大于 b"fiif [ $a -lt $b ]then echo "$a -lt $b: a 小于 b"else echo "$a -lt $b: a 不小于 b"fiif [ $a -ge $b ]then echo "$a -ge $b: a 大于或等于 b"else echo "$a -ge $b: a 小于 b"fiif [ $a -le $b ]then echo "$a -le $b: a 小于或等于 b"else echo "$a -le $b: a 大于 b"fi 执行脚本,输出结果如下所示: 10 -eq 20: a 不等于 b10 -ne 20: a 不等于 b10 -gt 20: a 不大于 b10 -lt 20: a 小于 b10 -ge 20: a 小于 b10 -le 20: a 小于或等于 b #!/bin/basha=10b=20if [ $a != $b ]then echo "$a != $b : a 不等于 b"else echo "$a != $b: a 等于 b"fiif [ $a -lt 100 -a $b -gt 15 ]then echo "$a -lt 100 -a $b -gt 15 : 返回 true"else echo "$a -lt 100 -a $b -gt 15 : 返回 false"fiif [ $a -lt 100 -o $b -gt 100 ]then echo "$a -lt 100 -o $b -gt 100 : 返回 true"else echo "$a -lt 100 -o $b -gt 100 : 返回 false"fiif [ $a -lt 5 -o $b -gt 100 ]then echo "$a -lt 100 -o $b -gt 100 : 返回 true"else echo "$a -lt 100 -o $b -gt 100 : 返回 false"fi 执行脚本,输出结果如下所示: 10 != 20 : a 不等于 b10 -lt 100 -a 20 -gt 15 : 返回 true10 -lt 100 -o 20 -gt 100 : 返回 true10 -lt 100 -o 20 -gt 100 : 返回 false 运算符 说明 举例 && 逻辑的 AND [[ $a -lt 100 && $b -gt 100 ]] 返回 false || 逻辑的 OR [[ $a -lt 100 || $b -gt 100 ]] 返回 true #!/bin/basha=10b=20if [[ $a -lt 100 && $b -gt 100 ]]then echo "返回 true"else echo "返回 false"fiif [[ $a -lt 100 || $b -gt 100 ]]then echo "返回 true"else echo "返回 false"fi 字符串运算符 下表列出了常用的字符串运算符,假定变量 a 为 “abc”,变量 b 为 “efg”: 运算符 说明 举例 = 检测两个字符串是否相等,相等返回 true。 [ $a = $b ] 返回 false。 != 检测两个字符串是否相等,不相等返回 true。 [ $a != $b ] 返回 true。 -z 检测字符串长度是否为0,为0返回 true。 [ -z $a ] 返回 false。 -n 检测字符串长度是否为0,不为0返回 true。 [ -n $a ] 返回 true。 str 检测字符串是否为空,不为空返回 true。 [ $a ] 返回 true。 #!/bin/basha="abc"b="efg"if [ $a = $b ]then echo "$a = $b : a 等于 b"else echo "$a = $b: a 不等于 b"fiif [ $a != $b ]then echo "$a != $b : a 不等于 b"else echo "$a != $b: a 等于 b"fiif [ -z $a ]then echo "-z $a : 字符串长度为 0"else echo "-z $a : 字符串长度不为 0"fiif [ -n $a ]then echo "-n $a : 字符串长度不为 0"else echo "-n $a : 字符串长度为 0"fiif [ $a ]then echo "$a : 字符串不为空"else echo "$a : 字符串为空"fi 执行脚本,输出结果如下所示: abc = efg: a 不等于 babc != efg : a 不等于 b-z abc : 字符串长度不为 0-n abc : 字符串长度不为 0abc : 字符串不为空 流程控制 if if else if else-if else a=1if [ $a == 1]then echo 1elif [ $a == 2]then echo 2else echo 3fi for for var in 1 2 3 4 5do echo $vardone while #!/bin/basha=1while (( $a<5 ))do echo $a let "a++"done 函数 linux shell 可以用户定义函数,然后在shell脚本中可以随便调用。 shell 中函数的定义格式如下: [ function ] funname [()]{ action; [return int;]} 说明: 1、可以带function fun() 定义,也可以直接fun() 定义,不带任何参数。 2、参数返回,可以显示加:return 返回,如果不加,将以最后一条命令运行结果,作为返回值。 return后跟数值n(0-255)。 #!/bin/bashdemoFun(){ echo "这是我的第一个 shell 函数!"}echo "-----函数开始执行-----"demoFunecho "-----函数执行完毕-----" 输出结果: -----函数开始执行-----这是我的第一个 shell 函数!-----函数执行完毕----- 带有return语句的函数: #!/bin/bashfunWithReturn(){ echo "这个函数会对输入的两个数字进行相加运算..." echo "输入第一个数字: " read aNum echo "输入第二个数字: " read anotherNum echo "两个数字分别为 $aNum 和 $anotherNum !" return $(($aNum+$anotherNum))}funWithReturnecho "输入的两个数字之和为 $? !" 函数返回值在调用该函数后通过 $? 来获得。 注意:所有函数在使用前必须定义。这意味着必须将函数放在脚本开始部分,直至shell解释器首次发现它时,才可以使用。调用函数仅使用其函数名即可。 函数参数 在Shell中,调用函数时可以向其传递参数。在函数体内部,通过 $n 的形式来获取参数的值,例如,$1表示第一个参数,$2表示第二个参数… #!/bin/bashfunWithParam(){ echo "第一个参数为 $1 !"echo "第二个参数为 $2 !"echo "第十个参数为 $10 !"echo "第十个参数为 ${10} !" echo "第十一个参数为 ${11} !" echo "参数总数有 $# 个!" echo "作为一个字符串输出所有参数 $* !"}funWithParam 1 2 3 4 5 6 7 8 9 34 73 输出结果: 第一个参数为 1 !第二个参数为 2 !第十个参数为 10 !第十个参数为 34 !第十一个参数为 73 !参数总数有 11 个!作为一个字符串输出所有参数 1 2 3 4 5 6 7 8 9 34 73 ! 注意,10不能获取第十个参数,获取第十个参数需要10 不能获取第十个参数,获取第十个参数需要10不能获取第十个参数,获取第十个参数需要{10}。当n>=10时,需要使用${n}来获取参数。","tags":[{"name":"shell","slug":"shell","permalink":"http://www.seanxia.cn/tags/shell/"},{"name":"动态语言","slug":"动态语言","permalink":"http://www.seanxia.cn/tags/%E5%8A%A8%E6%80%81%E8%AF%AD%E8%A8%80/"}]},{"title":"Linux常用操作整理","date":"2017-03-15T16:00:00.000Z","path":"操作系统/575d59a4.html","text":"Linux 是一个自由的,免费的,源码开放的操作系统。也是开源软件中最著名的例子。其最主要的目的就是为了建立不受任何商品化软件版权制约的,全世界都能使用的类 Unix 兼容产品。而我们将服务器部署在 Linux 将会更加的稳定、安全、高效以及出色的性能这是 Windows 无法比的。 Linux 系统简介 Linux 作者 纳斯·本纳第克特·托瓦兹(Linus Benedict Torvalds, 1969年~ ),著名的电脑程序员、黑客。Linux内核的发明人及该计划的合作者。托瓦兹利用个人时间及器材创造出了这套当今全球最流行的操作系统(作业系统)内核之一。现受聘于开放源代码开发实验室(OSDL:Open Source Development Labs, Inc),全力开发Linux内核。 Linux 发行版 发行版是基于 Linux 内核的一个操作系统。它带有用户可以使用的软件集合。更多的,它还包含系统管理包。 目前有许多 Linux 发行版。一些有名的版本如: CentOS、Ubuntu、 Redhat等是几个非常受欢迎的 Linux 发行版。 Linux 特点 开放性,多用户,多任务,丰富的网络功能,可靠的系统安全,良好的可移植性,具有标准兼容性。 环境准备 Vmware 大多数服务器的容量(CPU,内存,磁盘等)利用率不足 30%,这不仅导致了资源浪费,也加大了服务器的数量。实现服务器虚拟化后,多个操作系统可以作为虚拟机在单台物理服务器上运行,并且每个操作系统都可以访问底层服务器的计算资源,从而解决效率低下问题。 Vmware虚拟机化技术由此诞生,它可以将一台服务器虚拟化出多台虚拟机,供多人同时使用,提高资源利用率。 关于 Vmware 的安装这里不做详细叙述,网上都可以搜到。 Linux 安装 关于 Linux 的安装可以参考其他文章。 网络配置 1、查看网关 2、配置静态IP(NAT模式) 编辑配置文件,添加修改以下内容 vi /etc/sysconfig/network-scripts/ifcfg-eth0 按 i 进入文本编辑模式,出现游标,左下角会出现INSERT,即可以编辑。 应包含以下配置,除此之外的可以删除掉。 DEVICE=eth0 #网卡设备名,请勿修改名字TYPE=Ethernet #网络类型,以太网BOOTPROTO=static #启用静态IP地址ONBOOT=yes #开启自动启用网络连接 IPADDR=192.168.110.4 #设置IP地址NETMASK=255.255.255.0 #设置子网掩码GATEWAY=192.168.110.2 #设置网关 修改完后执行以下命令 service network restart #重启网络连接 ifconfig #查看IP地址 验证是否配置成功 虚拟机能 ping 通虚拟网关 虚拟机与物理机(笔记本)相互可ping通 虚拟机与公网上的百度网址相互可ping通(此步ping通,才说明网络配置成功) 注意: a.保证VMware的虚拟网卡没有被禁用 b.网关IP不能被占用 3、配置 DNS vim /etc/resolv.conf 配置如下: nameserver 192.168.110.2 #网关 桥接和NAT区别 桥接: 结构:网络与物理机同一个网段(会占用外部IP) 特点:外网能够访问进来;同时也能够访问外网。 注意:桥接模式下的虚拟机网关必须改为与物理机网关一致 NAT: 结构:构成一个以物理机为网关的子网。 特点:子网的所有的服务器对外不可见;同时子网能够正常访问外网。 所以 NAT 模式下安全,还能节省IP资源。 Host-only:主要应用于多台虚拟机组成一个封闭的网络 例如:在做windows域相关实验时,多台虚拟机构成的客户端和服务端处在一个封闭的网络中。在Host-only状态下虚拟机如果需要上网需要另外配置一台具有双网卡的虚拟机充当上网代理的角色。 XShell安装与使用 Xshell 是一个强大的安全终端模拟软件,它支持SSH1, SSH2, 以及Microsoft Windows 平台的TELNET 协议。使用它可以在 Windows 界面下用来访问远端不同系统下的服务器,从而比较好的达到远程控制终端的目的。除此之外,其还有丰富的外观配色方案以及样式选择。 关于Xshell的安装网上有相关的教程。使用命令时与Linux中操作相同。 文件传输 远程拷贝 1. 将本地文件复制到远程机器 scp local_file remote_username@remote_ip:remote_folder 比如把现在 sean02 节点的 root 目录下 test.txt 文件复制到远程 sean03 节点上相同路径下 2. 将本地目录复制到远程机器 scp -r local_folder remote_username@remote_ip:remote_folder 复制目录与文件不同的地方在于需要用到 -r,意思是循环遍历文件夹里的文件,这里 我们把 sean02 节点 root 目录下的 mytask 文件夹复制到 sean03 节点的同路径下,这样mytask文件夹下的 yy.txt 文件也会一并复制过来。 3. 将远程文件或者目录复制到本地道理相同 文件上传 **1. 需先安装好 lrzsz ** yum install lrzsz -y 2. 安装好后,输入上传的命令 rz,弹出一下界面 3. 选择一个windows系统里的文件上传至虚拟机的当前目录下,然后 ll 命令,查看结果 文件下载 下载跟上传相反,是把虚拟机上的文件下载到window系统,使用命令 sz ,后面要跟上需要下载的文件名称。 Xftp的使用 Xftp 跟 Xshell 出自同一家公司,与Xshell相同,灵活轻巧。当我们使用 rz 和 sz 来进行上传下载时,只能传输单个文件,不能传输文件夹,而且当文件过大时,速度会慢很多,使用Xftp传输可以轻松解决这些问题。打开Xftp,左边是window系统,右边是虚拟机系统文件。 磁盘指令 查看硬盘信息 1. 命令:df 2. 默认硬盘分区的大小以kb显示 3. 可以在 df 后面加参数-m mb单位, -k kb单位 , -h 更易于阅读 查看文件目录的大小 1. 命令:du 文件名字/目录名字 ,默认单位为 kb -k kb单位 -m mb单位 -a 所有文件和目录 -h 更易于阅读 –max-depth=0 目录深度 2. 例如,查看/etc目录下,所有文件的大小 查看/etc目录大小,并且目录大小的单位根据实际大小,自动选择。 网络指令 查看网络配置信息 命令:ifconfig 注:箭头1指向的是本机IP,箭头2为广播地址,箭头3为子网掩码。 测试与目标主机的连通性 命令:ping remote_ip 输入 ping 192.168.1.26 代表测试本机和 26 主机的网络情况,箭头1表示一共接收到了3个包,箭头2表示丢包率为0,表示两者之间的网络顺畅。注意:linux系统的ping命令会一直发送数据包,进行测试,除非认为的按ctrl + c停止掉,windows系统默认只会发4个包进行测试,以下为windows的dos命令。 显示各种网络相关信息 1. 命令:netstat -a (all)显示所有选项,默认不显示LISTEN相关 -t (tcp)仅显示tcp相关选项 -u (udp)仅显示udp相关选项 -n 拒绝显示别名,能显示数字的全部转化成数字。 -l 仅列出有在 Listen (监听) 的服務状态 -p 显示建立相关链接的程序名 -r 显示路由信息,路由表 -e 显示扩展信息,例如uid等 -s 按各个协议进行统计 -c 每隔一个固定时间,执行该netstat命令。 提示:LISTEN 和 LISTENING 的状态只有用-a或者-l才能看到 2. 查看端口号(是否被占用) lsof -i 端口号 netstat -tunlp|grep 端口号 测试远程主机的网络端口 1. 需要安装 telnet yum install telnet -y 2. 命令: telnet ip port 查看本机能否连上远程主机的端口号 上图说明,192远程主机的22端口,我们本机是可以连的上的。 测试成功后,按ctrl + ] 键,然后弹出telnet>时,再按q退出。 http 请求模拟 1. 命令: curl 【option】【url】 2. 用法 -X/–request [GET|POST|PUT|DELETE|…] 使用指定的http method发出 http request -H/–header 设定request里的header -i/–include 显示response的header -d/–data 设定 http parameters -v/–verbose 输出比较多的信息 -u/–user 使用者账号,密码 -b/–cookie cookie 参数 -X 跟 --request 两个功能是一样的 example: curl -X GET http://www.baidu.com/ curl --request GET http://www.baidu.com/ curl -X GET “http://www.rest.com/api/users” curl -X POST “http://www.rest.com/api/users” curl -X PUT “http://www.rest.com/api/users” curl -X DELETE “http://www.rest.com/api/users” 系统管理 用户操作 1. 创建用户 useradd/adduser usernamepasswd username 修改密码 这样就创建了一个名叫 qq 的普通用户了。 2. 删除用户 userdel -r username 3. 修改用户 命令:usermod usermod 不允许你改变正在线上的使用者帐号名称。当usermod用来改变user时,必须确认这名user没在电脑上执行任何程序。 3.1 修改用户名 usermod -l new_name old_name 这样就把 qq 用户改为 mytest 用户了。 3.2 锁定账号 usermod -L username 账号锁定期间,用户输入的命令无论正确与否都提示密码错误,登录不了,即冻结了账号。 注意:慎用,禁止锁定root用户,因为root用户是超级管理员,一旦锁定将无法解除。 3.2 解除锁定 usermod -U mytest 普通用户被锁定以后可以通过管理员用户给予解除。 4. 查看用户 whoami 查看当前登录用户名 普通用户可查看 /etc/passwd 文件,得出系统一个有多少个用户。 除此之外,root 用户还可以查看 /etc/shadow 文件,来得出系统一个有多少用户。 用户组操作 1. 创建用户组 groupadd groupname 2. 删除用户组 groupdel groupname 3. 修改用户组 groupmod -n new_name old_name 4. 查看用户组 命令:groups 查看当前登录用户的组内成员 即当前的root用户所属的组为root组。 命令:groups username 查看指定用户所在的组。 即 mytest 用户所属的组为 qq 组,这是由于我第一次创建的 qq 用户,然后给它改名字了,但它的主组没有改。 注意:创建用户时,系统默认会创建一个和用户名字一样的主组。 usermod -g 组名 用户 —— 把用户的主组改为其他的组 usermod -G 组名 用户 —— 把用户添加到附加组当中 将 mytest 用户添加到附加组 testgroup 里,然后查看 mytest 用户属于哪些组。注意:testgroup 组我在上面已经创建好了。 如果想查看一个组下面一共有多少用户,可以通过查看/etc/group文件的内容来得到。 将 root 用户也添加到附加组group里。然后一起查看。 此时发现,group组里的成员一共有mytest用户和root用户。 系统权限 查看/usr 目录下的每个文件或目录的权限 命令: ll /usr 1. 权限类别 Linux中,每个文件或目录都拥有三种权限 权限 对文件的影响 对目录的影响 r(读取) 可读取文件 可列出目录内容 w(写入) 可修改文件内容 可在目录中创建删除文件 x(执行) 可以修改文件内容 可访问目录内容 2. UGO 模型 Linux权限基于UGO模型进行控制。 U代表User, G代表Group, O代表other。 权限三个一组(rwx), 对应UGO分别设置。 每个文件都有一个拥有者/用户(User), 用户的所属组即(Group), 不属于上面的都是other。 3. 修改权限 3.1 修改文件/目录的拥有者 命令:chown 【选项】… 【所有者】【:【组】】 文件… 只修改所有者 chown username 文件/目录 如图,将将属于test.txt文件的所有者root用户改为了mytest用户。 同时修改一个文件或目录的所有者和属组。 chown username:groupname 文件/目录 如果要递归修改整个目录下的所有者或属组,加参数-R. 如:chown -R mytest:test 目录名字 3.2 修改文件/目录的权限 命令:chmod ugo+rwx name 其中ugo代表的是要对谁进行权限操作,rwx代表进行怎样的权限操作,+代表的授权,-代表的取消权限。 这个命令相当于把myfile的读/写/执行三个权限对所有人都开放了。 chmod o-rwx myfile 表示将其他人对myfile的读/写/执行权限都取消了。 权限的另外一种修改方式: 将rwx rwx rwx 三组权限的读写执行权限分别用0和1代替,1代表有权限,0代表没权限,最后将三组二进制转化成十进制。 命令:chmod 700 test.txt 系统配置 用户组信息配置 cat /etc/group 用户信息配置 cat /etc/shadow和cd /etc/passwd系统存在的所有用户名 系统服务初始化配置 0:停机状态[工作中实际生产环境慎用!] 1:单用户模式,root账户进行操作 2:多用户,不能使用net file system,一般很少用 3:默认的完全多用户,一部分启动,一部分不启动,命令行界面 4:未使用、未定义的保留模式 5:图形化,3级别中启动的进程都启动,并且会启动一部分图形界面进程。 6:停止所有进程,卸载文件系统,重新启动 (reboot) 这些级别中1、2、4很少用,相对而言0、3、5、6用的会较多,3级别和5级别除了桌面相关的进程外没有什么区别。为了减少资源占用,推荐都用3级别。 注意 :**linux***默认级别为3**,不要把initdefault 设置为0 和 6 * 主机名配置 若要修改主机名字,可在/etc/sysconfig/network文件里修改. vim etc/sysconfig/network 机器重启才能生效。 DNS 配置 hosts 文件的作用相当于DNS,提供IP地址hostname的对应,可在这个文件里添加映射。 /etc/resolv.conf 为DNS服务器的地址文件 sudo 权限配置 使用Linux系统时,经常会被要求使用超级权限,如果拥有root账户那还好,可以直接进行任何操作,但是这并不一个好方法,也不推荐使用。root的权限太过大了,慎用!!! 对于普通用户来说,一个简单的sudo即可解决大部分问题。 vim /etc/sudoers(只读) 格式:授权用户 主机=【(切换到哪些用户或用户组)】【是否需要密码验证】命令 编辑sudoers ,用 visudo 命令,进行编辑 接下来mytest用户就可以用yum 和 service 命令了。 注意:在使用命令时,需要加sudo 然后在敲命令,且第一次使用时需要mytest用户密码,接下来每隔15分钟需要一次密码验证。 如果不需要密码直接运行命令的,应该加NOPASSWD:参数 sudo -l 列出该用户所有sudo权限。 如果要将权限赋予某一个组,则需要在组名前加%, 系统时间 1. 查看系统时间 命令:date cal 查看日历 cal 8 2018 cal 2018 2. 更新系统时间 date -s 2012-08-02只修改系统的日期 date -s 10:08:00 修改时间不修改日期 date -s “2018-01-01 04:53:00” 同时修改日期和时间 为了能让修改的时间更精确,可以用ntp来做时间同步,它会到时间服务器里去同步时间,保证了时间的准确度。 时间同步 需要事先安装ntp服务: yum install ntpdate -y 命令:ntpdate cn.ntp.org.cn 该命令表示为:到域名为cn.ntp.org.cn的时间服务器上同步时间。 注:全球的时间服务器有很多个,可以到百度或谷歌上搜,不一定用cn.ntp.org.cn的时间服务器。 环境变量 Linux系统的全局环境变量是在/etc/profile文件里配置。 但是使用配置全局变量需要重启服务器才会生效,而配置局部变量会达到同样的效果,因此我们一般采用的是配置局部变量。 /root/ .bash_profile -- 局部的环境变量 首先考虑一个问题,问什么我们先前敲的yum, service,date,useradd等等,可以直接使用,系统怎么知道这些命令对应的程序是放在哪里的呢? 这是由于无论是windows系统还是linux系统,都有一个叫做path的系统环境变量,当我们在敲命令时,系统会到path对应的目录下寻找,找到的话就会执行,找不到就会报没有这个命令。 我们可以查看一下,系统一共在哪些目录里寻找命令对应的程序。 命令:echo $PATH 可以看到path里有很多路径,路径之间有冒号隔开。当用户敲命令时,系统会从左往右依次寻找对应的程序,有的话则运行该程序,没有的就报错,command not found. 重定向与管道 重定向管道 1. 输出重定向 > 输出重定向到一个文件或设备 覆盖原来的文件 >> 输出重定向到一个文件或设备 追加原来的文件 ls > xss,这个命令会将ls的查看结果输出到shsxt这个文件里,不再将内容打印到屏幕上 命令:echo “shsxt is good” >> shsxt 将“shsxt is good”追加到shsxt文件里。 2. 输入重定向 < 输入重定向到一个程序 cat < xss ,将xss文件里的内容当作是cat 命令的输入,其实同 cat xss 的效果是一样的。 标准/错误输出重定向 1. 标准输出重定向 符号为: 1> 该符号含义为:输出重定向时,只有正确的输出才会重定向到指定文件,否则如果是错误的输出则不会。输出重定向默认是1。即符号“1>”等价于”>” 两种写法都一样。 如果此时,我们的命令发生错误,输出错误的日志,则不会重定向到指定文件。 2. 错误输出重定向 符号为: 2> 该符号含义为:把错误的输出日志重定向到指定文件里,正确的日志则不会。 如上图,命令cat sss 的输出是一个错误的日志(因为我没有sss这个文件),此时并没有打印到屏幕上,而是把日志重定向到了xss4文件里,当我用cat命令去查xss4文件的内容时,才看到了这句错误的日志。 3. 结合使用 符号“2>&1” 2>&1 将一个标准错误输出重定向到标准输出 ,即无论是正确的输出还是错误的输出都重定向到指定文件里。 注意:以上重定向符号都是覆盖的,若想追加则用”>>” 管道符 | 命令 “|” 表示把前一个输出当做后一个输入 介绍这个命令之前,先介绍grep命令。grep命令是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配到的行打印出来。 |和grep命令结合使用: netstat -anp | grep 22 命令含义:把netstat –anp 命令的输出 当做是grep 命令的输入。 上面的命令就是:先用netstat –anp 命令查出本机的端口信息,然后把netstat输出的端口信息,用作grep命令的输入进行匹配搜索,并且匹配的是22 ,即查看22端口是否开着。 命令执行控制 1. 命令:&& 前一个命令执行成功才会执行后一个命令 该命令,先执行cat shsxt命令,当cat shsxt 命令执行成功后,才执行ping 命令。 2. 命令:|| 前一个命令执行失败才会执行后一个命令 第一个命令cat sss执行失败了,然后才执行ping 命令。 信息黑洞 写入它的内容都会永远丢失,说白了就是不显示任何信息 位于根目录的 /dev/null ls > /dev/null 服务操作 列出所有服务 命令:chkconfig 查询操作系统在每一个执行等级中会执行哪些系统服务,其中包括各类常驻服务。 操作服务 1. 命令:service 服务名 start/stop/status/restart 例子:对防火墙服务进行操作,防火墙的服务名为:iptables. 查看防火墙服务运行状态。 开启防火墙。 关闭防火墙。 2. 永久关闭(启动后生效): chkconfig iptables on/off chkconfig 添加服务 /etc/rc.d/init.d 目录包含许多系统各种服务的启动和停止脚本 /etc/rc.d/目录下rc0.d-rc6.d子目录里分别放的是系统对应执行级别的服务软连接。 如下图所示,每个软连接前面都有一个以S或K开头+数字的前缀名,代表了这个脚本在开机时的启动顺序,或关机时的杀死顺序。(S为启动,K为杀死,在rc3.d里就是系统以3级别运行时的执行情况) 1. 若要添加自己写的服务,则要在脚本前面加以下两句: #chkconfig: 2345 80 90#description:auto_run 2. 编写自己的服务脚本,例如:开机时同步时间:vim myservice.sh 3. 写完之后修改权限,让它拥有可执行权限。 4. 接下来将脚本拷贝到/etc/init.d目录下,然后加入到服务里。 命令:chkconfig --add myservice.sh 5. 最后重启下服务器,验证一下。系统时间成功修改,且在/usr目录下有ntpdate.log产生。 删除服务 chkconfig --del name 服务初执行等级更改 chkconfig --level 2345 name off|on 定时调度 crond 是 linux 下用来周期性的执行某种任务或等待处理某些事件的一个守护进程,与 windows 下的计划任务类似,当安装完成操作系统后,默认会安装此服务工具,并且会自动启动 crond 进程,crond 进程每分钟会定期检查是否有要执行的任务,如果有要执行的任务,则自动执行该任务。 格式 minute(分钟) hour(小时) day(天) month(月) dayofweek(周) command(命令) minute:从0到59的整数 hour:从0到23的整数 day:从1到31的整数 (必须是指定月份的有效日期) month:从1到12的整数 (或如Jan或Feb简写的月份) dayofweek:从0到7的整数,0或7用来描述周日 (或用Sun或Mon简写来表示) command:需要执行的命令 编辑的内容如下图,表示每分钟执行一次 echo “hello”命令。 只要时间一到,触发定时任务,系统就会出现以下语句进行提示: 该提示只有我们操作其他的命令后才会出现,如果一直放着是不会有此提示 然后我们可以去/var/spool/mail/root文件里查看root用户定时任务的执行情况。 vim /var/spool/mail/root 设置方式 设置定时调度有两种方式: 1、在命令行输入: crontab -e 然后添加相应的任务,wq存盘退出。 2、直接编辑 /etc/crontab 文件,即vim /etc/crontab,添加相应的任务。 查看调度任务 crontab -l #列出当前的所有调度任务crontab -l -u jp #列出用户jp的所有调度任务 删除任务调度工作 crontab -r #删除所有任务调度工作 Linux 安全 防火墙 1. 临时性设置(无需重启服务器) service iptables start/stop/status 2. 永久性设置(需要重启服务器) chkconfig iptables on/off seLinux Selinux是Linux的一个安全策略DAC–MAC,但是,实际应用中,很多人会遇到这样那样的问题。很多编译安装软件的文档,也特意注明了,建议关闭SeLinux。 enforcing:强制模式,代表 SELinux 运作中,且已经正确的开始限制 domain/type 了; permissive:宽容模式:代表 SELinux 运作中,不过仅会有警告讯息并不会实际限制。这种模式可以运来作为 SELinux 的 debug 之用; disabled:关闭,SELinux 并没有实际运作。 1. 查看SELinux状态: sestatus -v ##如果SELinux status参数为enabled即为开启状态 2. 关闭SELinux: (1)临时关闭(不用重启机器): setenforce 0 设置SELinux 成为permissive模式#setenforce 1 设置SELinux 成为enforcing模式 (2)修改配置文件需要重启机器: 修改 /etc/selinux/config 文件,将 selinux 关闭掉。将 SELINUX=enforcing 改为 SELINUX=disabled 然后重启机器即可。 Linux 进程操作 查看进程 1. 命令: ps -aux -a 列出所有 -u 列出用户 -x 详细列出,如cpu、内存等 -e 显示所有进程 -f 全格式 2. 命令: ps - ef | grep ssh 查看所有进程里CMD是ssh 的进程信息。 其中箭头所指的是ssh服务进程的进程号(PID) 3. 根据 CPU 使用来升序排序 ps -aux --sort -pcpu 看出来暂时CPU占用率没有,默认按照进程号进行排序。 PID: 运行着的命令(CMD)的进程编号 TTY: 命令所运行的位置(终端) TIME: 运行着的该命令所占用的CPU处理时间 CMD: 该进程所运行的命令 性能分析 top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,常用于服务端性能分析。 1. 用法 :top [-] / [d] / [p] / [q] / [c] / [C] / [s] / [S] / [n] 2. 参数说明 d 指定每两次屏幕信息刷新之间的时间间隔。当然用户可以使用s交互命令来改变之。 p 通过指定监控进程ID来仅仅监控某个进程的状态。 q 该选项将使top没有任何延迟的进行刷新。如果调用程序有超级用户权限,那么top将以尽可能高的优先级运行。 S 指定累计模式 s 使top命令在安全模式中运行。这将去除交互命令所带来的潜在危险。 i 使top不显示任何闲置或者僵死进程。 c 显示整个命令行而不只是显示命令名。 total 进程总数 running 正在运行的进程数 sleeping 睡眠的进程数 stopped 停止的进程数 zombie 僵尸进程数 Cpu(s): us 用户空间占用CPU百分比 sy 内核空间占用CPU百分比 ni 用户进程空间内改变过优先级的进程占用CPU百分比 id 空闲CPU百分比 wa 等待输入输出的CPU时间百分比 hi:硬件CPU中断占用百分比 si:软中断占用百分比 st:虚拟机占用百分比 Mem: total 物理内存总量 used 使用的物理内存总量 free 空闲内存总量 buffers 用作内核缓存的内存量 Swap: total 交换区总量 0k used 使用的交换区总量 free 空闲交换区总量 cached 缓冲的交换区总量,内存中的内容被换出到交换区,而后又被换入到内存,但使用过的交换区尚未被覆盖,该数值即为这些内容已存在于内存中的交换区的大小,相应的内存再次被换出时可不必再对交换区写入。 后台进程 若想一个程序放在后台运行,只要在命令后面加 & 符号 1. 例如: ping www.baidu.com > ping.log & 2. jobs –l 列出当前连接的所有后台进程。 注意:jobs命令只看当前终端生效的,关闭终端后,在另一个终端jobs已经无法看到后台跑的程序了。 3. 此时应该用ps -ef | grep 进程名 来查询后台进程的 ps -ef | grep ping 后台进程有时运行一段时间后,系统会自动把该进程挂起来,导致进程无法正常运行。 故后台经常一般和nohup命令结合使用,告诉系统不要把该进程挂起,这样子该命令就可以24*7小时不间断的运行了。 nohup ping www.baidu.com > ping2.log & 杀死进程 kill 命令:kill 进程号(PID) kill -9 进程号 #强制杀死 可以先用jobs –l或ps 命令先查出对应程序的PID或PPID ,然后杀死掉进程。 JDK 部署 官网下载Linux版本jar包 1. 用Xftp将jdk包上传到linux系统里,我这里上传到/usr/soft/ysb目录下。 2. 然后解压: ar -zvxf jdk-8u191-linux-x64.tar.gz -C …/ 其中 -C 意思是解压到指定的路径 …/ 就是上级目录目录 /usr/soft/ 中 配置环境变量 注意,这里有两种配置方法,一种是配置全局,在 /etc/profile 文件中 另一种是配置局部,在 /root/.bash_profile 文件中 因为配置全局后需要重启服务器,所以我们一般在开发过程中选择配置局部的环境变量。 1. 找到局部配置文件 .bash_profile,这是个隐藏文件,通过 -a 可以看到 这种带 “ . ” 前缀的文件就是隐藏文件。 2. 使用命令 vim ~/.bash_profile 编辑配置文件 把jdk安装路径加入 JAVA_HOME变量,并把JAVA_HOME变量加入到PATH中,前面加上export。 注:配置环境变量的准则是,可变的引入,不变的保留。如果单是使用jdk可以不配JAVA_HOME,但是后面使用的Tomcat等软件需要用到JAVA_HOME,不然会报错。 3. 重新加载环境变量:source /etc/profile 4. 验证:java -version Tomcat 部署 1、官网下载安装包 下载 Tomcat:http://tomcat.apache.org 2、上传并解压 我这里上传至/usr/soft/yso目录下,然后解压,过程同 jdk 雷同。 3、启动 Tomcat 在 tomcat 的 bin目录下有个startup.sh 脚本可以直接启动 tomcat 服务。 关闭tomcat服务,可以用shutdown.sh命令。 或者ps -ef | grep tomcat 查看出tomcat进程号后,用kill命令。 4、jps jps是JDK 1.5提供的一个显示当前所有java进程pid的命令,简单实用,非常适合在linux/unix平台上简单察看当前java进程的一些简单情况。 如上图所示,jps命令显示出了,系统当前运行在jvm上的进程情况。其中Bootstrap是tomcat的进程名字,2814是tomcat的PID 5、验证 Tomcat 先把防火墙关了,然后访问虚拟机IP的8080端口 免密码登录 工作原理 1、Server A向Server B发送一个连接请求。 2、Server B得到Server A的信息后,在本地的authorized_keys文件中查找A存放在B上的公钥,如果有相应的公钥,则随机生成一个字符串,并用Server A的公钥加密,接着发送给Server A。 3、Server A得到Server B发来的消息后,使用私钥进行解密,然后将解密后的字符串发送给Server B。Server B用原来随机生成的字符串和A发过来的字符串进行对比,如果一致,则允许免登录。 总结:A要免密码登录到B,B首先要拥有A的公钥,然后B要做一次加密验证。对于非对称加密,公钥加密的密文不能公钥解开,只能私钥解开。 例子:主机A对主机B对主机C进行两两免密码登录 主机A(sean02):192.168.110.4 主机B(sean03):192.168.110.5 主机C(sean04):192.168.110.6 生成公钥和密钥 每个节点执行命令,生成自己的公钥私钥 ssh-keygen -t rsa -P ‘’ -f ~/.ssh/id_rsa 自己登陆自己: cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys 登陆其他服务器节点不需要密码需要将自己的公钥发送给要登陆的节点服务器 scp ~/.ssh/id_rsa.pub root@node32:/opt/ 被登陆的节点服务器需要将发送过来的公钥追加到自己的私钥或系统里面 cat /opt/id_rsa.pub >> ~/.ssh/authorized_keys sean02 能 ssh 登陆到 seano3 和 sean04 如果 sean03 要免密码登陆到 sean02 和 sean04,重复 3 4 5 步骤两两互登!!! sean04 要免密登录到 sean02 he sean03 同样操作。 验证结果 结果中可以查看出:sean02自己登录自己,然后登录到sean03,sean03又登录到sean04,sean04又登录到sean03,sean03又登回到sean02。最后使用命令exit一步一步退回来,最终退回到sean02本身。 部署mysql 1、安装 mysql 源码安装 第一种安装方式是源码安装,由于安装过程较为繁琐,且容易出错,这里我们不做演示,也不推荐,如想了解可以自行网上查看资料。 yum 安装 yum install mysql-server mysql-devel -y 从中可以看出来使用 yum 安装 mysql 很简单,也不容易出错,推荐使用! 2、登录 mysql 安装完毕之后,要先启动 mysql 服务 service mysqld start 启动之后,第一次登录直接输入命令:mysql,即可进入 输入命令:show databases;即可展示默认的数据库 使用命令:use mysql; 改变当前数据库实例,然后 show tables;即可展示当前数据库中存在的表 3、添加用户 添加用户并设置密码 第一次进去是没有用户的,也没有设置密码,这里我们可以去添加 mysqladmin -u root password 123456 注意:添加的用户必须是存在的,否则会报错。 使用添加的用户登录,并输入密码 mysql -uroot -p 显示 msql> 说明就成功进来了,然后就可以像之前一样去操作数据库。 4、连接Window系统的 mysql 放权给Linux中的mysql链接window中的数据库 现在直接连接会发现连接不上,是因为Linux系统中host地址字段是以字符串来保存,我们在navicat中的host:192.168.110.6根本就不能与localhost匹配上。 在 Linux 中查询数据信息 把 Linux 中的 host 改成通配符 % 因为 host 改为 % 后,所有 IP 都有连接权限。 然后修改。 **注意:**最后需要刷新一下权限!!!(否则有时不生效) flush privileges; 退出重启 mysql 即可连接上 个人思考: 既然,host 指定了允许用户所使用的IP,那把host改为当前节点的IP不就可以了,但是我把通配符 % 改为 “ 192.168.110.6 ” 之后,发现根本连接不上,并且navcat 提醒网关192.168.110.1不允许访问,于是我把host改为网关之后,发现可以正常访问了,并且另外两台同网段的节点IP也都能正常连接数据库。 ===> 由此证明,如果把 Linux Mysql中的 host 设置为 网关,那么在同一个网关下的所有节点都可以去访问。","tags":[{"name":"linux","slug":"linux","permalink":"http://www.seanxia.cn/tags/linux/"},{"name":"进程","slug":"进程","permalink":"http://www.seanxia.cn/tags/%E8%BF%9B%E7%A8%8B/"}]},{"title":"大数据思维经典问题","date":"2017-02-22T16:00:00.000Z","path":"大数据/f43f9767.html","text":"大数据环境下的核心问题 海量数据 工业技术相对落后 硬件损坏是常态 分而治之 把一个复杂的问题按一定的“分解”方法分为等价的规模较小的若干部分,然后逐个解决,分别找出各部分的中间结果,把各部分的中间结果组成整个问题的最终结果。这也是著名的 Hadoop 中分布式存储系统 HDFS 的核心思想。 经典问题 需求一:海量数据找出重复的行 已知条件: I/O 速度500M/s,服务器内存为64G,txt文件大小为1TB。 找出重复行(能不能?) 思路: 首先我们可以看出,文件过大,服务器不可能一次性读入,必须把文件分块或者压缩文件的大小。 我们可以采用哈希算法得到每行数字的 hashcode 值(因为 hashcode 是固定不变的,且内容相同的哈希值肯定是相等)。 这里由于需要知道行数是多少,所以我们采用 HashMap 来接收,把每行的 hashcode 作为 key ,行号作为 value ,然后对逐行的 key 除以1024(这个值可以自定义)取模,形成一个个的 File 小文件。 因为相同的 hashcode 取模肯定相同,也就是说落在相同的 File 文件里,这样我们再读取一次就可以找出重复的行了。这次读取我们可以直接找Key相同的值就行了。最好是第一个File文件就能找到,最坏就是在最后一个File文件中找到,这样我们按照最坏计算。 也就是说:==计算时间: 两次IO,2* 30分钟 = 1小时== 需求二:海量数字的全排序 已知条件: I/O 速度 500M/s,服务器内存为 64G,一个 TB 的乱序数字文件,要求进行排序。 思路: 方案一:这里也是采用分而治之的办法。 知道这些数字大概的范围,然后将这些数字划分成多个范围,也就是分区。注意这个分区不能超过服务器的内存,比如 64G。 然后就形成了,全局有序,局部无序的状态。 最后再分别取每个分区的小部分数据进行排序,因为这个时候每个分区文件很小,可以很快的将这些局部的数字排好序,最后把每个分区放回即可。 方案二:同理,用到归并算法 先直接分区,分为很多个小文件File,这个分区必须要小于服务器的容量,也就是说 每块容量 < 64GB,否则无法进行读写。 因为每个小文件因为比较小可以直接排好序。也就形成了全局无序,局部有序的状态,与方案一相反。 这时可以采用归并算法来进行每块的排序,使局部变得有序,最后放回即可完成。 简单理解归并算法:如下图所示,因为每组数字已经是有序的,那么先拿出每组的第一个数也就是最小值,线型比较跳出最小值,比如第一次比较的是 4、3、1 挑出 1,然后后面的数接着往上补,也就是 2 上去,重新组成 4、3、2,接着又挑出 2 ,接着 5 上去,组成 4、3、5 ,挑出 3,这样循环往复,最终会使整个数排序完成。 这个算法就相当于玩拳皇 97,对打不过,下面的预选人接着上,打赢了就留下,输了就出去,一直比下去,最强最大的那个会留到最后。哈哈。。。 注: **归并排序(MERGE-SORT)**是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。","tags":[{"name":"大数据","slug":"大数据","permalink":"http://www.seanxia.cn/tags/%E5%A4%A7%E6%95%B0%E6%8D%AE/"},{"name":"思维","slug":"思维","permalink":"http://www.seanxia.cn/tags/%E6%80%9D%E7%BB%B4/"}]},{"title":"大型高并发处理Nginx","date":"2016-10-17T16:00:00.000Z","path":"大数据/72ac1227.html","text":"Nginx 产生背景 1. 日常生活中所见 大学选课时学校官网会因为访问量较大经常崩溃。 淘宝、京东等大型网站做营销活动时也会出现服务器异常。 每年十一节假日或者春运抢票时,12306网站会发生瘫痪。 2. 上述场景产生的主要2大原因: 巨大流量—海量的并发访问 单台服务器资源和能力有限 那么如何解决这些问题呢,这就要先来了解一下负载均衡。 负载均衡 先来了解一下高并发的概念。 1. 高并发 见名知意,高(大量的),并发就是可以使用多个线程或者多个进程,同时处理(就是并发)不同的操作。简而言之就是每秒内有多少个请求同时访问。 2. 负载均衡 负载均衡:将请求/数据【均匀】分摊到多个操作单元上执行,负载均衡的关键在于【均匀】,也是分布式系统架构设计中必须考虑的因素之一。 3. tomcat并发图 可以发现,当每秒300个请求同时访问tomcat时,tomcat已经开始承受不住,出现波动。那么大型网站是如何处理高并发的呢?以下是高并发场景下,实现负载均衡的一个分布式架构图。 常见互联网分布式架构,分为客户端层、反向代理nginx层、站点层、服务层、数据层。只需要实现“将请求/数据 均匀分摊到多个操作单元上执行”,就能实现负载均衡。 什么是 Nginx 1. Nginx 的特点 Nginx是一款轻量级的Web 服务器/反向代理服务器【后面有介绍】及电子邮件(IMAP/POP3)代理服务器。由俄罗斯的程序设计师Igor Sysoev所开发。 其特点是占有内存少,并发能力强,nginx的并发能力确实在同类型的网页服务器中表现非常好。 官方测试nginx能够支撑5万并发链接,并且CPU、内存等资源消耗却非常低,运行非常稳定。 2. 使用的地方 目前世界各地使用Nginx服务器的大型公司有很多,包括国内的阿里巴巴、腾讯、京东、网易、优酷等等。并且有些大型公司进行了二次开发,做出了自己的框架,如阿里巴巴的 tengine 。 3. Nginx纵深对比其优缺点(相比Apache) nginx相对于apache的优点: 轻量级,同样起web 服务,比apache 占用更少的内存及资源高并发,nginx 处理请求是异步非阻塞(如前端ajax)的,而apache 则是阻塞型的,在高并发下nginx能保持低资源低消耗高性能高度模块化的设计,编写模块相对简单还有,它社区活跃,各种高性能模块出品迅速(十几年时间发展) apache 相对于nginx 的优点: Rewrite重写 ,比nginx 的rewrite 强大模块超多,基本想到的都可以找到少bug ,nginx 的bug 相对较多。(出身好起步高) Nginx 配置简洁, Apache 复杂。 安装 Nginx(这里以tengine为例) 1. 安装依赖 依赖 gcc openssl-devel pcre-devel zlib-devel yum -y install gcc openssl-devel pcre-devel zlib-devel 2. 解压文件 tar -zxvf tengine-2.1.0.tar.gz -C ../ #这里我把它解压到上级目录,-C后接要压缩到的路径,如不写默认当前路径 3. configure 配置 进入解压后的源码目录,然后执行configure命令进行配置 ./configure --prefix=/usr/soft/nginx #其中--prefix= 后接安装的路径,可以不写,默认安装在/usr/local/ 此时发现 /usr/local/ 并没有,是因为还没有进行编译安装。 4. 编译并安装 make && make install 编译安装好后,就会默认在 /usr/local/ 下生成 nginx 文件夹。 nginx 命令 1. nginx 的启动 我们可以直接执行 /usr/local/nginx/sbin/ 下的 nginx 脚本启动。 还有一种方式就是将 nginx 添加至服务 1、在/etc/rc.d/init.d/目录中建立文本文件nginx 2、在文件中粘贴下面的内容: #!/bin/sh## nginx - this script starts and stops the nginx daemon## chkconfig: - 85 15 # description: Nginx is an HTTP(S) server, HTTP(S) reverse \\# proxy and IMAP/POP3 proxy server# processname: nginx# config: /etc/nginx/nginx.conf# config: /etc/sysconfig/nginx# pidfile: /var/run/nginx.pid # Source function library.. /etc/rc.d/init.d/functions # Source networking configuration.. /etc/sysconfig/network # Check that networking is up.[ "$NETWORKING" = "no" ] && exit 0 nginx="/usr/local/nginx/sbin/nginx"prog=$(basename $nginx) NGINX_CONF_FILE="/usr/local/nginx/conf/nginx.conf" [ -f /etc/sysconfig/nginx ] && . /etc/sysconfig/nginx lockfile=/var/lock/subsys/nginx make_dirs() { # make required directories user=`nginx -V 2>&1 | grep "configure arguments:" | sed 's/[^*]*--user=\\([^ ]*\\).*/\\1/g' -` options=`$nginx -V 2>&1 | grep 'configure arguments:'` for opt in $options; do if [ `echo $opt | grep '.*-temp-path'` ]; then value=`echo $opt | cut -d "=" -f 2` if [ ! -d "$value" ]; then # echo "creating" $value mkdir -p $value && chown -R $user $value fi fi done} start() { [ -x $nginx ] || exit 5 [ -f $NGINX_CONF_FILE ] || exit 6 make_dirs echo -n $"Starting $prog: " daemon $nginx -c $NGINX_CONF_FILE retval=$? echo [ $retval -eq 0 ] && touch $lockfile return $retval} stop() { echo -n $"Stopping $prog: " killproc $prog -QUIT retval=$? echo [ $retval -eq 0 ] && rm -f $lockfile return $retval} restart() { configtest || return $? stop sleep 1 start} reload() { configtest || return $? echo -n $"Reloading $prog: " killproc $nginx -HUP RETVAL=$? echo} force_reload() { restart} configtest() { $nginx -t -c $NGINX_CONF_FILE} rh_status() { status $prog} rh_status_q() { rh_status >/dev/null 2>&1} case "$1" in start) rh_status_q && exit 0 $1 ;; stop) rh_status_q || exit 0 $1 ;; restart|configtest) $1 ;; reload) rh_status_q || exit 7 $1 ;; force-reload) force_reload ;; status) rh_status ;; condrestart|try-restart) rh_status_q || exit 0 ;; *) echo $"Usage: $0 {start|stop|status|restart|condrestart|try-restart|reload|force-reload|configtest}" exit 2esac 修改nginx文件的执行权限 chmod +x nginx 添加该文件到系统服务中去 chkconfig --add nginx 查看是否添加成功 chkconfig --list nginx 启动,停止,重新装载 service nginx start|stop 启动后,访问虚拟机的80端口,可查看到以下界面。 当出现以上信息,说明安装启动成功。 Nginx 配置 nginx默认配置详解 #进程数,建议设置和CPU个数一样或2倍worker_processes 2;#日志级别error_log logs/error.log warning;(默认error级别)# nginx 启动后的pid 存放位置#pid logs/nginx.pid;events { #配置每个进程的连接数,总的连接数= worker_processes * worker_connections #默认1024 worker_connections 10240;}http { include mime.types; default_type application/octet-stream; sendfile on;#连接超时时间,单位秒keepalive_timeout 65; server { listen 80; server_name localhost #默认请求 location / { root html; #定义服务器的默认网站根目录位置 index index.php index.html index.htm; #定义首页索引文件的名称 } #定义错误提示页面 error_page 500 502 503 504 /50x.html; location = /50x.html { root html; }} 负载均衡配置 nginx支持以下负载均衡机制(或方法): 1. 轮询负载均衡 - 对应用程序服务器的请求以循环方式分发(默认此方式) 如果没有专门配置负载均衡方法,则默认为循环法。所有请求都被 代理到服务器组shsxt,并且nginx应用HTTP负载平衡来分发请求。 http { upstream shsxt{ server node01; server node02; server node03; } server { listen 80; server_name localhost; location / { proxy_pass http://shsxt; } } } 2. 加权负载均衡 通过使用服务器权重,还可以进一步影响nginx负载均衡算法,谁的权重越大,分发到的请求就越多。 upstream shsxt { server srv1.example.com weight=3; server srv2.example.com; server srv3.example.com; } 3. 最少连接数 - 将下一个请求分配给活动连接数最少的服务器 在连接负载最少的情况下,nginx会尽量避免将过多的请求分发给繁忙的应用程序服务器,而是将新请求分发给不太繁忙的服务器,避免服务器过载。 upstream shsxt { least_conn; server srv1.example.com; server srv2.example.com; server srv3.example.com; } 4. 会话持久性(ip-hash负载平衡机制) 如果需要将客户端绑定到特定的应用程序服务器 - 换句话说,就是始终选择相同的服务器而言,就要使客户端的会话“粘滞”或“持久” 。 ip-hash负载平衡机制就是有这种特性。使用ip-hash,客户端的IP地址将用作散列键,以确定应该为客户端的请求选择服务器组中的哪台服务器。此方法可确保来自同一客户端的请求将始终定向到同一台服务器,除非此服务器不可用。 upstream shsxt{ ip_hash; server srv1.example.com; server srv2.example.com; server srv3.example.com;} 5. Nginx的访问控制 Nginx还可以对IP的访问进行控制,allow代表允许,deny代表禁止。 location / { deny 192.168.2.180; allow 192.168.78.0/24; allow 10.1.1.0/16; allow 192.168.1.0/32; deny all; proxy_pass http://shsxt;} 从上到下的顺序,匹配到了便跳出。如上的例子先禁止了1个,接下来允许了3个网段,其中包含了一个ipv6,最后未匹配的IP全部禁止访问。 虚拟主机 1. 何为虚拟主机 虚拟主机就是把一台物理服务器划分成多个“虚拟”的服务器,各个虚拟主机之间完全独立,在外界看来,每一台虚拟主机和一台单独的主机的表现完全相同。所以这种被虚拟化的逻辑主机被形象地称为“虚拟主机”。 2. 虚拟主机的特点和优点 由于多台虚拟主机共享一台真实主机的资源,每个虚拟主机用户承受的硬件费用、网络维护费用、通信线路的费用均大幅度降低。许多企业建立网站都采用这种方法,这样不仅大大节省了购买机器和租用专线的费用,网站服务器服务器管理简单,诸如软件配置、防病毒、防攻击等安全措施都由专业服务商提供,大大简化了服务器管理的复杂性;同时也不必为使用和维护服务器的技术问题担心,更不必聘用专门的管理人员。 3. 类别 基于域名的虚拟主机,通过域名来区分虚拟主机 http { upstream shsxt{ server node01; server node02; } upstream bjsxt{ server node03; } server { listen 80; //访问sxt2.com的时候,会把请求导到bjsxt的服务器组里 server_name sxt2.com; location / { proxy_pass http://bjsxt; }} server { listen 80; //访问sxt1.com的时候,会把请求导到shsxt的服务器组里 server_name sxt1.com; location / { proxy_pass http://shsxt; }} } 基于端口的虚拟主机,通过端口来区分虚拟主机 http { upstream shsxt{ server node01; server node02; } upstream bjsxt{ server node03 } server { //当访问nginx的80端口时,将请求导给bjsxt组 listen 8080; server_name localhost; location / { proxy_pass http://bjsxt; }} server { //当访问nginx的81端口时,将请求导给shsxt组 listen 81; server_name localhost; location / { proxy_pass http://shsxt; }} } 正向代理和反向代理 正向代理 举个例子:我是一个用户,我访问不了某网站,但是我能访问一个代理服务器,这个代理服务器呢,他能访问那个我不能访问的网站,于是我先连上代理服务器,告诉他我需要那个无法访问网站的内容,代理服务器去取回来,然后返回给我。像我们经常通过vpn访问国外的网站,此时就是正向代理。 客户端必须设置正向代理服务器,当然前提是要知道正向代理服务器的IP地址,还有代理程序的端口。 反向代理 **反向代理(Reverse Proxy)**方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。 反向代理隐藏了真实的服务端,当我们请求 www.baidu.com 的时候,就像拨打 10086 一样,背后可能有成千上万台服务器为我们服务,但具体是哪一台,你不知道,也不需要知道,你只需要知道反向代理服务器是谁就好了,www.baidu.com 就是我们的反向代理服务器,反向代理服务器会帮我们把请求转发到真实的服务器那里去。 Nginx 就是性能非常好的反向代理服务器,用来做负载均衡。 关于Nginx中的session共享 http协议是无状态的,即你连续访问某个网页100次和访问1次对服务器来说是没有区别对待的,因为它记不住你。 那么,在一些场合,确实需要服务器记住当前用户怎么办?比如用户登录邮箱后,接下来要收邮件、写邮件,总不能每次操作都让用户输入用户名和密码吧,为了解决这个问题,session的方案就被提了出来,事实上它并不是什么新技术,而且也不能脱离http协议以及任何现有的web技术。 session的常见实现形式是会话cookie(session cookie),即未设置过期时间的cookie,这个cookie的默认生命周期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失了。 Session 共享 先我们应该明白,为什么要实现共享? 如果你的网站是存放在一个机器上,那么是不存在这个问题的,因为会话数据就在这台机器,但是如果你使用了负载均衡把请求分发到不同的机器呢?这个时候会话id在客户端是没有问题的,但是如果用户的两次请求到了两台不同的机器,而它的session数据可能存在其中一台机器,这个时候就会出现取不到session数据的情况,于是session的共享就成了一个问题。 Session 一致性解决方案 1. session复制 tomcat 本身带有复制session的功能。 2、共享session 需要专门管理session的软件。 memcached 缓存服务,可以和tomcat整合,帮助tomcat共享管理session。 安装使用 memcached 1. 安装 memcached 内存数据库 yum –y install memcached 2. 启动可以用 telnet localhost 11211 memcached -d -m 128m -p 11211 -l 192.168.235.113 -u root -P /tmp/ 3. web 服务器连接 memcached 的 jar 包拷贝到 tomcat 的 lib 4. 1. 配置 tomcat 的 conf 目录下的 context.xml <Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager" memcachedNodes="n1:192.168.17.9:11211" sticky="true" lockingMode="auto" sessionBackupAsync="false" requestUriIgnorePattern=".*\\.(ico|png|gif|jpg|css|js)$"sessionBackupTimeout="1000" transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory" />配置memcachedNodes属性,配置memcached数据库的ip和端口,默认11211,多个的话用逗号隔开.目的是为了让tomcat服务器从memcached缓存里面拿session或者是放session4.修改index.jsp,取sessionid看一看<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><html lang="en">SessionID:<%=session.getId()%></br>SessionIP:<%=request.getServerName()%></br><h1>tomcat1</h1></html>","tags":[{"name":"nginx","slug":"nginx","permalink":"http://www.seanxia.cn/tags/nginx/"},{"name":"高并发","slug":"高并发","permalink":"http://www.seanxia.cn/tags/%E9%AB%98%E5%B9%B6%E5%8F%91/"},{"name":"反向代理","slug":"反向代理","permalink":"http://www.seanxia.cn/tags/%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86/"}]},{"title":"Dubbo框架初识","date":"2016-10-12T16:00:00.000Z","path":"Java/a255bcda.html","text":"RPC 基本概念 RPC 协议(Remote Procedure Call Protocol) RPC 远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。在 OSI 网络通信模型中,RPC 跨越了传输层和应用层,使得开发包括网络分布式多程序在内的应用程序更加容易。 客户机(客户端)-服务器模式:当请求没有到达服务端,服务端(全天候)处于休眠状态,当请求到达时,服务端程序会被唤醒,处理客户端请求(计算,图形处理 等),将结果响应给客户端(RPC Socket)。 OSI 七层网络模型(从上至下): 应用层:Http (Https),ftp,smtp,pop3 表示层 会话层 传输层:TCP|UDP 网络层 数据链路层 物理层 RPC 框架 IPC:单机中运行的进程之间的相互通信。 RPC:可以在同一台电脑上不同进程进行,也可以在不同电脑上进行。 LPC:在windows 里面同一台电脑上不同进程间的通讯还可以采用 LPC(本地访问)。 综上:RPC 或 LPC是上层建筑,IPC 是底层基础。 RPC 与 HTTP、TCP、UDP、Socket 的区别 TCP/UDP:都是传输协议,主要区别是 TCP 协议连接需要 3 次握手,断开需要四次挥手, 是通过流来传输的,就是确定连接后,一直发送信息,传完后断开。udp 不需要进行连接, 直接把信息封装成多个报文,直接发送。所以 UDP 的速度更快写,但是不保证数据的完整 性。 HTTP:超文本传输协议,是一种应用层协议,建立在 TCP 协议之上。 Socket:是在应用程序层面上对 TCP/IP 协议的封装和应用。其实是一个调用接口,方便 程序员使用 TCP/IP 协议栈而已。程序员通过 Socket 来使用 TCP/IP 协议。但是 Socket 并不是 一定要使用 TCP/IP 协议,Socket 编程接口在设计的时候,就希望也能适应其他的网络协议。 RPC: 是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。 所以 RPC 的实现可以通过不同的协议去实现,比如可以使 HTTP、RMI 等。 RPC 的内部运行 **(1)网络通讯问题:**Socket **(2)网络寻址问题:**本地存根(字典) **(3)数据序列化:**A 服务器通过寻址和传输将序列化的二进制发送给 B 服务器。 **(4)数据反序列化:**B 服务器收到请求后,需要对参数进行反序列化(序列化的逆操作),恢复为内存中的表达方式,然后找到对应的方法(寻址的一部分)进行本地调用,然后得到返回值。 **(5)再次序列化和反序列化:**返回值还要发送回服务器 A 上的应用,也要经过序列化的方式发送,服务器 A 接到后,再反序列化,恢复为内存中的表达方式,交给 A 服务器上的应用。 RPC 基于 RMI 的简单实现 第一步:定义一个接口,必须继承 remote public interface IUserService extends Remote{ public User queryUserByUserId(Integer userId) throws Exception;} 第二步:编写一个实现类即服务端,继承 UnicastRemoteObject 实现 IUserService接口 public class UserServiceImpl extends UnicastRemoteObject implements IUserService{ private Map<Integer, User> users; public UserServiceImpl() throws Exception{ users = new HashMap<>(); users.put(1, new User(1, "admin", "上海大厦")); } @Override public User queryUserByUserId(Integer userId) throws Exception { return users.get(userId); }} 第三步:发布服务 public class Publisher { public static void main(String[] args) throws Exception{ LocateRegistry.createRegistry(8989); // 发布服务 Naming.bind("rmi://127.0.0.1:8989/userService",new UserServiceImpl()); System.out.println("服务发布成功!"); }} 第四步:客户端调用 public class Test { public static void main(String[] args) throws Exception{ IUserService userService = (IUserService) Naming.lookup("rmi://127.0.0.1:8989/userService"); System.out.println(userService.queryUserByUserId(1)); }} 实现效果: 服务端: 客户端 Dubbo Dubbo 框架基本概念 Dubbo 是一款高性能、轻量级的开源 Java RPC 框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。 结构流程 1. 服务提供方(Provider): 环境:Spring 环境 应用启动时:服务初始化,服务注册(将服务信息写入到远程注册中心:ip:port、项目名/uri?属性参数配置&) 2. 注册中心(Registry): 第三方软件实现(zookeeper) 一种树形的目录服务。(练习中可使用广播address=“multicast://224.5.6.7:1234”) 2.1: 保存服务提供方提供的服务信息 2.2: 支持服务变更推送 **3. 服务消费方(Consumer) ** 环境:Spring 环境 应用启动时:订阅注册中西提供的服务列表信息,执行远程服务调用(RPC)。 4. 监控中心(Monitor) 负责项目运行信息的监控操作(消费方运行信息 提供方运行信息)。 Dubbo 入门环境配置 dubbo_par 父工程(pom) dubbo_api 服务api定义模块(普通工程) dubbo_provider 服务提供方(普通) dubbo_consumer 服务消费方(普通) Dubbo 常用标签配置 dubbo:application :应用名称配置(与项目名一致) (提供方与消费方) <!-- 配置应用名称 --> <dubbo:application name="dubbo_provider"/> dubbo:registry 注册中心配置 可以配置多个 一般情况配置一个 通常使用zookeeper 必须配置(提供方和消费方) <!-- 配置注册中心 --> <dubbo:registry address="multicast://224.5.6.7:1234"/> dubbo:protocol 服务注册使用协议 官方建议 dubbo 默认端口:20880 (提供方) <!-- 服务注册使用协议(提供方) --> <dubbo:protocol name="dubbo" port="20880"/> dubbo:service 注册服务 配置 (提供方) <!-- 配置注册服务(提供方) --> <dubbo:service interface="com.shsxt.service.IUserService" ref="userServiceImpl"/> dubbo:reference 服务订阅 (消费方) <!-- 配置订阅的服务(消费方) --> <dubbo:reference interface="com.shsxt.service.IUserService" id="userService"/> RPC 基于 Dubbo 的简单实现 一、配置结构 二、导包 Dubbo 坐标 <dependency><groupId>com.alibaba</groupId><artifactId>dubbo</artifactId><version>2.5.6</version></dependency> 三、定义服务接口 /** * 远程定义服务:IUserService */public interface IUserService { public User queryUserByUserId(Integer userId);} 四、服务实现 /** * 服务实现类 UserServiceImpl */@Servicepublic class UserServiceImpl implements IUserService{ private Map<Integer,User> users; public UserServiceImpl() { users = new HashMap<>(); users.put(1,new User(1,"admin","绿地伯顿大厦")); } @Override public User queryUserByUserId(Integer userId) { System.out.println("客户端调用参数-->"+userId); return users.get(userId); }} 五、配置生产者 添加 dubbo_provider.xml 文件到 resources 文件下 <?xml version="1.0" encoding="UTF-8" ?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 配置扫描器 --> <context:component-scan base-package="com.shsxt.service"/> <!-- 配置应用名称 --> <dubbo:application name="dubbo_provider"/> <!-- 配置注册中心 --> <dubbo:registry address="multicast://224.5.6.7:1234"/> <!-- 服务注册使用协议(提供方) --> <dubbo:protocol name="dubbo" port="20880"/> <!-- 配置注册服务(提供方) --> <dubbo:service interface="com.shsxt.service.IUserService" ref="userServiceImpl"/></beans> 六、启动服务提供程序 /** * 发布服务请求 */public class Publisher { public static void main(String[] args) throws IOException { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("dubbo_provider.xml"); context.start(); System.in.read(); }} 七、配置服务的消费端 添加 dubbo_consumer.xml 文件到 resources 文件下 <?xml version="1.0" encoding="UTF-8" ?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 配置扫描器 --> <context:component-scan base-package="com.shsxt.controller"/> <!-- 配置应用名称 --> <dubbo:application name="dubbo_consumer"/> <!-- 配置注册中心 --> <dubbo:registry address="multicast://224.5.6.7:1234"/> <!-- 配置订阅的服务(消费方) --> <dubbo:reference interface="com.shsxt.service.IUserService" id="userService"/></beans> 八、调用服务的方法 /** * 调用服务的方法 */@Controllerpublic class UserController { @Autowired private IUserService userService; public User queryUserByUserId(Integer userId){ return userService.queryUserByUserId(userId); }} 九、启动服务消费端进行消费 /** * 接收请求返回数据 */public class Test { public static void main(String[] args){ ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("dubbo_consumer.xml"); UserController userController = (UserController) context.getBean("userController"); System.out.println(userController.queryUserByUserId(1)); }} 实现效果 服务端 客户端 因为启用客户端调用了controller层的方法,传入的id客户端就可以拿到了","tags":[{"name":"RPC","slug":"RPC","permalink":"http://www.seanxia.cn/tags/RPC/"},{"name":"Doubbo","slug":"Doubbo","permalink":"http://www.seanxia.cn/tags/Doubbo/"}]},{"title":"Java多线程笔记","date":"2016-10-11T16:00:00.000Z","path":"Java/a2c3ad19.html","text":"Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。 线程的概念 进程: 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1–n个线程。(进程是资源分配的最小单位) 线程: 同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位) 多进程是指操作系统能同时运行多个任务(程序)。 多线程是指在同一程序中有多个顺序流在执行。 每个进程至少有一个线程,若程序只有一个线程,那就是程序本身。 现代计算机支持多个线程的并发执行。 同一个进程的多个线程共享IO,内存资源。 线程是有状态的。 注意:很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即一个 cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。 创建多线程的三种方式以及区别 Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。Java可以用三种方式来创建线程,如下所示: 1)继承Thread类创建线程 2)实现Runnable接口创建线程 3)使用Callable和Future创建线程 继承Thread类创建线程 通过继承Thread类来创建并启动多线程的一般步骤如下 1】定义Thread类的子类。 2】重写该类的**run()**方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。 3】创建Thread子类的实例,也就是创建了线程对象。 4】启动线程,即调用线程的**start()**方法。 public class MyThread extends Thread{ //继承Thread类 @Override public void run(){ //重写run方法 }}public class Main { public static void main(String[] args){ new MyThread().start(); //创建并启动线程 }} 实现Runnable接口创建线程(推荐) 优点:将线程的任务从线程的子类中分离出来,进行单独的封装;面向接口编程,避免单继承局限。 步骤如下: 1】定义Runnable接口的实现类。 2】重写**run()**方法,这个 run() 方法和Thread中的run()方法一样是线程的执行体。 3】通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。(为什么?因为线程的任务都封装在Runnable接口子类的对象的run方法中,所以要在线程对象创建时就必须明确要运行的任务) 4】通过调用线程对象的**start()**方法来启动线程。 public class MyThread2 implements Runnable {//实现Runnable接口 @Override public void run(){ //重写run方法 }}public class Main { public static void main(String[] args){ //创建并启动线程 MyThread2 myThread=new MyThread2(); Thread thread=new Thread(myThread); thread().start(); //或者 new Thread(new MyThread2()).start(); }} 使用Callable和Future创建线程 和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。 call()方法可以有返回值 call()方法可以声明抛出异常 Java5 提供了 Future 接口来代表 Callable 接口里 call() 方法的返回值,并且为 Future 接口提供了一个实现类FutureTask,这个实现类既实现了 Future 接口,还实现了 Runnable 接口,因此可以作为 Thread 类的 target。在 Future 接口里定义了几个公共方法来控制它关联的 Callable 任务。 boolean cancel(boolean mayInterruptIfRunning):视图取消该Future里面关联的Callable任务 get():返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值 get(long timeout,TimeUnit unit):返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException boolean isDone():若Callable任务完成,返回True boolean isCancelled():如果在Callable任务正常完成前被取消,返回True 使用Callable和Future创建线程的步骤如下: 1】创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。 2】使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值。 3】使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口。 4】调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。 public class Main { public static void main(String[] args){ MyThread3 th=new MyThread3(); // 使用Lambda表达式创建Callable对象 // 使用FutureTask类来包装Callable对象 FutureTask<Integer> future=new FutureTask<Integer>( (Callable<Integer>)()->{ return 5; } ); new Thread(task,"有返回值的线程").start(); //实质上还是以Callable对象来创建并启动线程 try{ System.out.println("子线程的返回值:"+future.get()); //get()方法会阻塞,直到子线程执行结束才返回 }catch(Exception e){ ex.printStackTrace(); } }} 三种创建线程方法对比 实现Runnable和实现Callable接口的方式基本相同,不过是后者执行call()方法有返回值,后者线程执行体run()方法无返回值,因此可以把这两种方式归为一种,这种方式与继承Thread类的方法之间的差别如下: 1、线程只是实现Runnable或实现Callable接口,还可以继承其他类。 2、这种方式下,多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。 3、但是编程稍微复杂,如果需要访问当前线程,必须调用Thread.currentThread()方法。 4、继承Thread类的线程类不能再继承其他父类(Java单继承决定)。 注:一般推荐采用实现接口的方式来创建多线程。 线程的生命周期 线程是一个动态执行的过程,它也有一个从产生到死亡的过程。 线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。 (1)新建:即新生状态,创建Thread对象 (2)就绪:使用stard()方法 (3)运行:调用run方法 (4)阻塞:调用sleep()方法,阻塞执行后再次回到就绪状态(注意:后期加上 synchronized 的时候,使用 sleep 是不会释放权利的) (5)死亡:即程序终止,终止的方式有两种: 正常执行完毕,循环 次数已经到达 外部干涉 (设置到达时间后,让线程停止) 线程的优先级 只代表概率,不代表绝对先后顺序。 MIN_PRIORITY : 1 NORM_PRIORITY : 5 默认优先级 MAX_PRIORITY :10 线程的同步 线程安全问题产生的原因 1、多个线程在操作共享数据。 2、操作共享数据的线程的线程代码有多条。 解决思路 将多条操作共享数据的线程封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。必须要当前线程把这些代码执行完毕,其他线程参与运行。 1)同步块 synchronized +块:同步块synchronized (引用类型|对象|类.class) {} 2)同步方法 修饰符 synchronized 返回类型|void 方法签名{} 同步的好处 解决了线程安全性问题。 同步的弊端 相对降低了效率,因为同步外的线程都会判断同步锁。 同步的前提 同步中必须右多个线程并使用同一个锁。 线程池 什么线程池 线程池中,当需要使用线程时,会从线程池中获取一个空闲线程,线程完成工作时,不会直接关闭线程,而是将这个线程退回到池子,方便其它人使用。 简而言之,使用线程池后,原来创建线程变成了从线程池获得空闲线程,关闭线程变成了向池子归还线程。 java.util.concurrent.Executors 提供了一个 java.util.concurrent.Executor 接口的实现用于创建线程池。 多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。 假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。 线程池带来的好处 1、降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的性能消耗。 2、提高响应速度,当任务到达时,任务可以不需要等待线程创建,可以直接执行。 3、提高线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 线程池四个基本组成 1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务; 2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务; 3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等; 4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。 线程池技术正是关注如何缩短或调整 T1,T3 时间的技术,从而提高服务器程序性能的。它把 T1,T3 分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有 T1,T3 的开销了。 线程池不仅调整 T1,T3 产生的时间段,而且它还显著减少了创建线程的数目,看一个例子: 假设一个服务器一天要处理50000个请求,并且每个请求需要一个单独的线程完成。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池中线程的数目,而如果服务器不利用线程池来处理这些请求则线程总数为50000。一般线程池大小是远小于50000。所以利用线程池的服务器程序不会为了创建50000而在处理请求时浪费时间,从而提高效率。 线程池的常见参数 1、corePoolSize:核心线程数 * 核心线程会一直存活,及时没有任务需要执行 * 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理 * 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭 2、queueCapacity:任务队列容量(阻塞队列) * 当核心线程数达到最大时,新任务会放在队列中排队等待执行 3、maxPoolSize:最大线程数 * 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务 * 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常 4、 keepAliveTime:线程空闲时间 * 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize * 如果allowCoreThreadTimeout=true,则会直到线程数量=0 5、allowCoreThreadTimeout:允许核心线程超时 6、rejectedExecutionHandler:任务拒绝处理器 * 两种情况会拒绝处理任务: - 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务 - 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务 * 线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常 * ThreadPoolExecutor类有几个内部实现类来处理这类情况: AbortPolicy 丢弃任务,抛运行时异常 RunsPolicy 执行任务 DiscardPolicy 忽视,什么都不会发生 dOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务 * 实现RejectedExecutionHandler接口,可自定义处理器 线程池接口、类关系一览 【说明】 Executor 是一个顶级接口,它里面只声明一个方法:execute(Runnable command),用来执行传进去的任务。 ExecutorService 接口继承了 Executor 接口,并声明了一些方法:submit、shutdown、invokeAll 等。 AbstractExecutorService 抽象类实现了 ExecutorService 接口,基本实现了 ExecutorService 接口的所有方法。 ThreadPoolExecutor 继承了类 AbstractExecutorService。 常见线程池 newSingleThreadExecutor() 创建一个单线程的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 public class ExecutorDemo1 { public static void main(String[] args) { MyTask myTask = new MyTask(); //只有一个线程的线程池 ExecutorService es = Executors.newSingleThreadExecutor(); for(int i=0;i<10;i++){ es.submit(myTask); } }}class MyTask implements Runnable{ @Override public void run() { System.out.println(System.currentTimeMillis()/1000 + ":Thread ID:" + Thread.currentThread().getId()); try { Thread.sleep(1000); }catch (Exception e){ e.printStackTrace(); } }} newFixedThreadExecutor() 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 public class ExecutorDemo2 { public static void main(String[] args) { MyTask task = new MyTask(); ExecutorService es = Executors.newFixedThreadPool(5); //创建固定线程数大小为5的线程池 for(int i=0;i<10;i++){ //依次向线程池提交了10个任务 es.submit(task); } }}class MyTask implements Runnable{ @Override public void run() { System.out.println(System.currentTimeMillis() +":Thread ID:"+Thread.currentThread().getId()); try{ Thread.sleep(1000); //1秒 }catch(Exception e){ e.printStackTrace(); } } } newCacheThreadExecutor()(推荐使用) 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程(一般是60秒无执行),若无可回收,则新建线程。 public class ExecutorDemo3 { public static void main(String[] args) { MyTask myTask = new MyTask(); //可根据实际情况调整线程数量的线程池 ExecutorService es = Executors.newCachedThreadPool(); for(int i=0;i<10;i++){ es.submit(myTask); } }}class MyTask implements Runnable{ @Override public void run() { System.out.println(System.currentTimeMillis()/1000 + ":Thread ID:" + Thread.currentThread().getId()); try { Thread.sleep(1000); }catch (Exception e){ e.printStackTrace(); } }} newScheduleThreadExecutor() 创建一个定长的线程池,而且支持定时的以及周期性的任务执行。 public class ExecutorDemo4 { public static void main(String[] args) { ScheduledExecutorService ses = Executors.newScheduledThreadPool(10); /** * scheduleAtFixedRate方法 :如果前面的任务没有完成,则调度也不会执行! * scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); */ ses.scheduleAtFixedRate(new ScheduledTimeTask(), 0, 2, TimeUnit.SECONDS); //设置每定时2s执行一次 }}class ScheduledTimeTask implements Runnable { @Override public void run() { try { //修改这里的任务执行时间 Thread.sleep(1000); System.out.println( System.currentTimeMillis()/1000 + " : ThreadId = " + Thread.currentThread().getId()); } catch (Exception e) { e.printStackTrace(); } }} 【设置任务的执行时间为1s( < 定时的2s )的运行结果 【设置任务的执行时间为3s( > 定时的2s )的运行结果(即代码改成Thread.sleep(3000))】","tags":[{"name":"线程","slug":"线程","permalink":"http://www.seanxia.cn/tags/%E7%BA%BF%E7%A8%8B/"},{"name":"同步","slug":"同步","permalink":"http://www.seanxia.cn/tags/%E5%90%8C%E6%AD%A5/"}]},{"title":"Java中的内存分析","date":"2016-09-07T16:00:00.000Z","path":"Java/3474f33d.html","text":"内存分区 栈空间(stack) 由系统自动分配,遵循后进先出的原则,用于存放局部变量。 每个线程私有,不能实现线程间的共享。 速度快!栈是一个连续的内存空间。 堆空间(heap) 用于存放new出的对象,或者说是类的实例。 堆是一个不连续的内存空间,分配灵活,速度慢。 方法区(method) 在堆空间内。 被所有线程共享。 用来存放程序中永远不变或唯一的内容,如:①类的代码信息;②静态变量和方法;③常量池(字符串敞亮等,具有共享机制)。 常量池 JVM 为每个已加载的类维护一个常量池,常量池就是这个类用到的常量的一个有序集合。 包括直接常量(基本类型,String)和对其他类型、方法、字段的符号引用。 池中的数据和数组一样通过索引访问。 由于常量池包含了一个类型所有的对其他类型、方法、字段的符号引用,所以常量池在Java的动态链接中起了核心作用。 常量池存在于方法区(Method Area)。 实例分析 创建两个实体类和测试类 实体类 Student、Computer package cn.share.vo;public class Student { private Integer score; private Integer age; private String name; private Computer computer; public void study() { System.out.println("studying..."); } /*Getter和Setter方法*/ package cn.share.vo;public class Computer { private Double price; private String brand; /*Getter和Setter方法*/} 测试类 Test package cn.share; import cn.share.vo.Computer; import cn.share.vo.Student;public class Test { public static void main(String[] args){ Student stu = new Student(); stu.setName("xiaoming"); stu.setAge(10); stu.study(); Computer c = new Computer(); c.setBrand("Hasse"); System.out.println(c.getBrand()); stu.setComputer(c); System.out.println(stu.getComputer().getBrand()); System.out.println("================== 分割线1 ==================="); c.setBrand("Dell"); System.out.println(c.getBrand()); System.out.println(stu.getComputer().getBrand()); System.out.println(c.getBrand() == stu.getComputer().getBrand()); System.out.println("================== 分割线2 ==================="); String str = "Dell"; System.out.println(c.getBrand() == str); }} 运行结果 代码分析 程序的入口是main(),因而从main方法从上到下、从左到右进行分析。 Student stu = new Student(); ① 首先,Java虚拟机(JVM)去方法区寻找是否有 Test 类的代码信息,如果存在,直接调用。如果没有,通过类加载器(ClassLoader)把 .class 字节码加载到内存中,并把静态变量和方法、常量池加载(“ xiaoming ”、” Hasse ") ② 走到 Student,以同样的逻辑对 Student 类进行加载;静态成员;常量池(“studying”)。 ③ 走到 stu,stu 在 main 方法内部,因而是局部变量,存放在栈空间中。 ④ 走到 new Student,new 出的对象(实例),存放在堆空间中,以方法区的类信息为模板创建实例。 ⑤ 赋值操作,把 new Student 的地址告诉 stu 变量,stu 通过四字节的地址(十六进制),引用该实例。 stu.setName(“xiaoming”); ⑥ stu通过引用new Student实例的name属性,该name属性通过地址指向常量池的"xiaoming"常量 stu.setAge(10); ⑦ stu实例的 age 属性是基本数据类型,基本数据类型直接赋值。 stu.study(); ⑧ 调用实例的方法时,并不会在实例对象中生成一个新的方法,而是通过地址指向方法区中类信息的方法。 ⑥⑦⑧的过程如下图: Computer c = new Computer(); 同stu变量的生成过程。 c.setBrand(“Hasse”); 同 stu.setName(“xiaoming”); 过程 stu.setComputer©; ⑨ 把 c 对象对 Computer 实例的引用赋值给 Student 实例的 computer 属性。亦即:该 Student 实例的computer 属性指向该 Computer 类的实例。 如下图: 拓展 改变brand的地址指向 重新将 Computer 实例的 brand 属性指向"Dell"常量,那 stu.computer.brand 指向谁呢?Dell 还是Hasse? c.setBrand("Dell"); 根据刚才的分析可知: stu 通过地址引用 Student 实例,而该实例的 computer 的指向和 c 的指向是同一个 Computer 实例,因而改变该 Computer 实例的 brand 属性的指向,两者都会改变。 举个例子: 访问小明,和访问小明的儿子的爸爸,实质上访问的是同一个对象:小明。 因而,最终如上图测试结果是 true。 理解字符串常量及常量池 下面我们添加新的代码,如下: String str = "Dell";System.out.println(c.getBrand() == str); 根据常量池具有共享性,可知并不会生成新的常量"Dell",而是会把 str 通过地址指向原来的 "Dell",因而结果是 true。","tags":[{"name":"内存","slug":"内存","permalink":"http://www.seanxia.cn/tags/%E5%86%85%E5%AD%98/"},{"name":"堆","slug":"堆","permalink":"http://www.seanxia.cn/tags/%E5%A0%86/"},{"name":"栈","slug":"栈","permalink":"http://www.seanxia.cn/tags/%E6%A0%88/"}]},{"title":"Java面向对象","date":"2016-06-19T16:00:00.000Z","path":"Java/5f8d3e42.html","text":"由于Java是一种面向对象的语言,万事万物皆对象,用户定义一个类,这是一个广泛的定义,需要用户具体化,实例化这个广泛的类,确定这个具体的对象。在java程序中,对象可以被显式创建和隐式创建,主要说一下显式的创建对象的方式。 Java中创建对象的5种方式以及区别 1、使用new关键字 这是最常见的创建对象的方法,并且也非常简单。通过使用这种方法我们可以调用任何我们需要调用的构造函数。 Employee emp1 = new Employee(); 0: new #19 // class org/programming/mitra/exercises/Employee3: dup4: invokespecial #21 // Method org/programming/mitra/exercises/Employee."":()V 2、反射。使用class类的 newInstance() 方法 我们也可以使用class类的 newInstance() 方法来创建对象。此newInstance()方法调用无参构造函数以创建对象。 Employee emp2 = (Employee) Class.forName("org.programming.mitra.exercises.Employee").newInstance();//或者Employee emp2 = Employee.class.newInstance(); 51: invokevirtual #70 // Method java/lang/Class.newInstance:()Ljava/lang/Object; 3、反射。使用Constructor类的newInstance()方法 与使用class类的newInstance()方法相似,java.lang.reflect.Constructor类中有一个可以用来创建对象的newInstance()函数方法。通过使用这个newInstance()方法我们也可以调用参数化构造函数和私有构造函数。 Constructor<Employee> constructor = Employee.class.getConstructor();Employee emp3 = constructor.newInstance(); 111: invokevirtual #80 // Method java/lang/reflect/Constructor.newInstance:([Ljava/lang/Object;)Ljava/lang/Object; 这两种 newInstance() 的方法就是大家所说的反射,事实上Class的 newInstance() 方法内部调用Constructor 的 newInstance() 方法。这也是众多框架Spring、Hibernate、Struts等使用后者的原因。 4、使用clone方法 实际上无论何时我们调用clone() 方法,JAVA虚拟机都为我们创建了一个新的对象并且复制了之前对象的内容到这个新的对象中。使用 clone()方法创建对象不会调用任何构造函数。 要使用clone方法,我们必须先实现Cloneable接口并实现其定义的clone方法 Employee emp4 = (Employee) emp3.clone(); 162: invokevirtual #87 // Method org/programming/mitra/exercises/Employee.clone ()Ljava/lang/Object; 5、使用反序列化 当我们序列化和反序列化一个对象,JVM会给我们创建一个单独的对象,在反序列化时,JVM创建对象并不会调用任何构造函数。 为了反序列化一个对象,我们需要让我们的类实现Serializable接口。 ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.obj"));Employee emp5 = (Employee) in.readObject(); 261: invokevirtual #118 // Method java/io/ObjectInputStream.readObject:()Ljava/lang/Object; 正如我们在以上的字节代码片段中所看到的,除了使用new关键字之外的其他方法全部都是转变为invokevirtual (创建对象的直接方法),使用被new的方式转变为两个调用,new 和 invokespecial(构造函数调用)。 实例: 先准备创建对象的 Employee 类: class Employee implements Cloneable, Serializable { private static final long serialVersionUID = 1L; private String name; public Employee() { System.out.println("Employee Constructor Called..."); } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Employee other = (Employee) obj; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; } @Override public String toString() { return "Employee [name=" + name + "]"; } @Override public Object clone() { Object obj = null; try { obj = super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return obj; }} 然后用5种方式来创建 Employee对象。 public class ObjectCreation { public static void main(String... args) throws Exception { // By using new keyword Employee emp1 = new Employee(); emp1.setName("Naresh"); System.out.println(emp1 + ", hashcode : " + emp1.hashCode()); // By using Class class's newInstance() method Employee emp2 = (Employee) Class.forName("org.programming.mitra.exercises.Employee").newInstance(); // Or we can simply do this // Employee emp2 = Employee.class.newInstance(); emp2.setName("Rishi"); System.out.println(emp2 + ", hashcode : " + emp2.hashCode()); // By using Constructor class's newInstance() method Constructor<Employee> constructor = Employee.class.getConstructor(); Employee emp3 = constructor.newInstance(); emp3.setName("Yogesh"); System.out.println(emp3 + ", hashcode : " + emp3.hashCode()); // By using clone() method Employee emp4 = (Employee) emp3.clone(); emp4.setName("Atul"); System.out.println(emp4 + ", hashcode : " + emp4.hashCode()); // By using Deserialization // Serialization ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data.obj")); out.writeObject(emp4); out.close(); //Deserialization ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.obj")); Employee emp5 = (Employee) in.readObject(); in.close(); emp5.setName("Akash"); System.out.println(emp5 + ", hashcode : " + emp5.hashCode()); } } 程序输出结果如下: Employee Constructor Called...Employee [name=Naresh], hashcode : -1968815046Employee Constructor Called...Employee [name=Rishi], hashcode : 78970652Employee Constructor Called...Employee [name=Yogesh], hashcode : -1641292792Employee [name=Atul], hashcode : 2051657Employee [name=Akash], hashcode : 63313419","tags":[{"name":"java","slug":"java","permalink":"http://www.seanxia.cn/tags/java/"},{"name":"对象","slug":"对象","permalink":"http://www.seanxia.cn/tags/%E5%AF%B9%E8%B1%A1/"}]}]