首页 文章 精选 留言 我的

精选列表

搜索[官方文档],共10012篇文章
优秀的个人博客,低调大师

CentOS6,CentOS7官方镜像安装Oracle11G

安装准备 1.操作系统 CentOS6或CentOS7 2.数据库版本 linux.x64_11gR2_database_1of2.zip linux.x64_11gR2_database_2of2.zip 3.下载Oracle11G 下载地址:https://www.oracle.com/technetwork/database/enterprise-edition/downloads/index.html 4.修改hosts文件 sh 复制代码 # 编辑hosts文件 vi /etc/hosts # 添加 10.0.0.206 work006 注意: 10.0.0.206为该电脑IP可用ip addr查看 work006为主机名 5.安装一些必要的软件 sh 复制代码 yum install wget unzip net-tools -y wget:下载用的 unzip:解压oracle安装文件 net-tools:查看本机网络情况 比如netstat 6.系统必须设置SWAP分区 安装 1.使用oracle提供的环境配置工具 CentOS7: sh 复制代码 wget http://public-yum.oracle.com/public-yum-ol7.repo -O /etc/yum.repos.d/public-yum-ol7.repo && wget http://public-yum.oracle.com/RPM-GPG-KEY-oracle-ol7 -O /etc/pki/rpm-gpg/RPM-GPG-KEY-oracle && yum install oracle-rdbms-server-11gR2-preinstall -y CentOS6: sh 复制代码 wget http://public-yum.oracle.com/public-yum-ol6.repo -O /etc/yum.repos.d/public-yum-ol6.repo && wget http://public-yum.oracle.com/RPM-GPG-KEY-oracle-ol6 -O /etc/pki/rpm-gpg/RPM-GPG-KEY-oracle && yum install oracle-rdbms-server-11gR2-preinstall -y 注意: 这个工具会调整内核参数,建立一些必要的linux用户&组 可能网络不好会安装不成功,多install一下 完成后备份一下这个目录的文件到其他目录(该文件夹是修改系统后日志和原本的内核配置备份) /var/log/oracle-rdbms-server-11gR2-preinstall 查看内核参数 sysctl -f 2.创建一些目录和配置 配置oracle系统配置文件 sh 复制代码 cat >> /etc/oraInst.loc <<EOF inventory_loc=/home/oracle/ora11g/oraInventory inst_group=oinstall EOF 授权 sh 复制代码 chmod 664 /etc/oraInst.loc 创建oracle安装的目录&授权 sh 复制代码 mkdir -p /data0/oracle/app/ && mkdir /data0/oracle/tmp/ && chown -R oracle:oinstall /data0/oracle/app/ && chmod -R 775 /data0/oracle/app/ && chmod a+wr /data0/oracle/tmp/ 3.设置oracle用户密码 sh 复制代码 passwd oracle 注意:oracle用户是安装工具自己创建的 4.切换到oracle用户 新开一个ssh窗口,以oracle用户登录 5.修改环境变量 打开文件 sh 复制代码 vi .bash_profile 在文件末尾添加 sh 复制代码 TMP=/data0/oracle/tmp TMPDIR=/data0/oracle/tmp export TMP TMPDIR ORACLE_BASE=/data0/oracle/app/oracle ORACLE_HOME=/data0/oracle/app/oracle/product/11.2.0/dbhome_1 ORACLE_SID=orcl PATH=$ORACLE_HOME/bin:$PATH export ORACLE_BASE ORACLE_SID ORACLE_HOME PATH 保存 sh 复制代码 :x 刷新配置文件 sh 复制代码 source .bash_profile 6.上传安装文件到/home/oracle目录并解压,然后授予权限 sh 复制代码 cd /home/oracle && unzip linux.x64_11gR2_database_1of2.zip && unzip linux.x64_11gR2_database_2of2.zip && chown -R oracle:oinstall /home/oracle/database 7.配置db_install.rsp 备份到/home/oracle/rsp/ sh 复制代码 cp -r /home/oracle/database/response /home/oracle/rsp 配置安装响应文件db_install.rsp文件,修改为以下内容 sh 复制代码 # 我的/home/oracle/rsp/db_install.rsp oracle.install.responseFileVersion=/oracle/install/rspfmt_dbinstall_response_schema_v11_2_0 # INSTALL_DB_AND_CONFIG安装并自动配置数据库实例和监听 建议首次安装用这个 # 不然配置另外两个文件,新建实例和监听 oracle.install.option=INSTALL_DB_AND_CONFIG ORACLE_HOSTNAME=localhost UNIX_GROUP_NAME=oinstall INVENTORY_LOCATION=/home/oracle/ora11g/oraInventory SELECTED_LANGUAGES=zh_CN,en ORACLE_HOME=/data0/oracle/app/oracle/product/11.2.0/dbhome_1 ORACLE_BASE=/data0/oracle/app/oracle oracle.install.db.InstallEdition=EE oracle.install.db.isCustomInstall=true oracle.install.db.customComponents=oracle.server:11.2.0.1.0,oracle.sysman.ccr:10.2.7.0.0,oracle.xdk:11.2.0.1.0,oracle.rdbms.oci:11.2.0.1.0,oracle.network:11.2.0.1.0,oracle.network.listener:11.2.0.1.0,oracle.rdbms:11.2.0.1.0,oracle.options:11.2.0.1.0,oracle.rdbms.partitioning:11.2.0.1.0,oracle.oraolap:11.2.0.1.0,oracle.rdbms.dm:11.2.0.1.0,oracle.rdbms.dv:11.2.0.1.0,orcle.rdbms.lbac:11.2.0.1.0,oracle.rdbms.rat:11.2.0.1.0 oracle.install.db.DBA_GROUP=dba oracle.install.db.OPER_GROUP=oinstall oracle.install.db.config.starterdb.type=GENERAL_PURPOSE # 这个是服务名 oracle.install.db.config.starterdb.globalDBName=orcl # 实例sid oracle.install.db.config.starterdb.SID=orcl oracle.install.db.config.starterdb.characterSet=AL32UTF8 oracle.install.db.config.starterdb.memoryOption=true # 最小256M oracle.install.db.config.starterdb.memoryLimit=1024 # 是否安装学习的scott和hr(我就知道这两个) oracle.install.db.config.starterdb.installExampleSchemas=true oracle.install.db.config.starterdb.enableSecuritySettings=true # 密码全设置成oracle(安装时会提示,个人学习忽略) oracle.install.db.config.starterdb.password.ALL=OracleSys654321 oracle.install.db.config.starterdb.control=DB_CONTROL oracle.install.db.config.starterdb.dbcontrol.enableEmailNotification=false oracle.install.db.config.starterdb.automatedBackup.enable=false oracle.install.db.config.starterdb.storageType=FILE_SYSTEM_STORAGE oracle.install.db.config.starterdb.fileSystemStorage.dataLocation=/data0/oracle/app/oracle/oradata # true DECLINE_SECURITY_UPDATES=true 8.安装oracle(安装过程出现警告忽略,不操作即可) sh 复制代码 /home/oracle/database/runInstaller -silent -ignorePrereq -responseFile /home/oracle/rsp/db_install.rsp 安装过程可以另开一个shell查看日志 sh 复制代码 tail -f /home/oracle/ora11g/oraInventory/logs/installActions2020-03-12_08-04-30PM.log installActions2020-03-12_08-04-30PM.log为屏幕上显示的日志文件 日志文件中看到Exit Status is 3即为安装完成 9.切换root用户执行脚本 sh 复制代码 /data0/oracle/app/oracle/product/11.2.0/dbhome_1/root.sh 10.切换oracle用户查看安装情况 sqlplus登录 sh 复制代码 sqlplus / as sysdba 查看状态 sh 复制代码 select status from v$instance; 11.切换root用户重启系统 sh 复制代码 reboot 12.配置防火墙并开放1521端口 CentOS6 打开文件 sh 复制代码 vi /etc/sysconfig/iptables 在( -A INPUT -m state --state NEW -m tcp -p tcp --dport 22 -j ACCEPT ) 下添加 sh 复制代码 -A INPUT -m state --state NEW -m tcp -p tcp --dport 1521-j ACCEPT 保存并退出 sh 复制代码 :x 重启防火墙 sh 复制代码 service iptables restart CentOS7 sh 复制代码 firewall-cmd --add-port=1521/tcp --permanent && firewall-cmd --add-port=1521/tcp 13.修改监听文件 切换目录 sh 复制代码 cd /data0/oracle/app/oracle/product/11.2.0/dbhome_1/network/admin 修改listener.ora和tnsnames.ora这两个文件把主机名换成本机IP即可 14.启动数据库 启动监听 sh 复制代码 lsnrctl start sqlplus登录 sh 复制代码 sqlplus / as sysdba 启动 sh 复制代码 startup 15.修改oracle服务启动配置 sh 复制代码 vi /etc/oratab 将 sh 复制代码 orcl:/data0/oracle/app/oracle/product/11.2.0/dbhome_1:N 改 sh 复制代码 orcl:/data0/oracle/app/oracle/product/11.2.0/dbhome_1:Y 16.外网连接数据库并测试 用户名:sys 密码:oracle 服务名:orcl 角色:SYSDBA 17.常用命令 sh 复制代码 # oracle用户下启动|关闭数据库 # 启动 dbstart $ORACLE_HOME # 关闭 dbshut $ORACLE_HOME # oracle用户下启动|关闭数据库监听 # 启动 lsnrctl start # 关闭 lsnrctl stop # sqlplus下启动|关闭数据库 # sqlplus登录 sqlplus / as sysdba # 启动 startup # 关闭 shutdown 18.切换root配置oracle服务为开机自启 编辑文件 sh 复制代码 vi /etc/rc.local 添加命令 sh 复制代码 su - oracle -c 'dbstart $ORACLE_HOME' 修改权限 sh 复制代码 chmod a+x /etc/rc.local 19.清除安装文件 /home/oracle目录下的安装oracle的文件均可清除(慎用下面那个命令) sh 复制代码 rm -rf /home/oracle/* 20.修改oracle端口(11015) 停止监听 sh 复制代码 lsnrctl stop 修改端口 sh 复制代码 cd /data0/oracle/app/oracle/product/11.2.0/dbhome_1/network/admin 修改listener.ora和tnsnames.ora这两个文件把端口1521修改成11015即可 查看参数 sqlplus登录 sh 复制代码 sqlplus / as sysdba 查看 sh 复制代码 show parameter local_listener 若为第一次修改VALUE为空 sh 复制代码 alter system set local_listener="(address = (protocol = tcp)(host = izuf68yfs65r3cmglfxf2pz)(port = 11015))"; 注意: izuf68yfs65r3cmglfxf2pz为主机名 11015为端口 启动监听 sh 复制代码 lsnrctl start 查看监听 sh 复制代码 netstat -an|grep 11015

优秀的个人博客,低调大师

官方文档 Upgrading Elasticsearch

Upgrading Elasticsearch Before upgrading Elasticsearch: Consult thebreaking changesdocs. Use theElasticsearch Migration Pluginto detect potential issues before upgrading. Test upgrades in a dev environment before upgrading your production cluster. Alwaysback up your databefore upgrading. Youcannot roll backto an earlier version unless you have a backup of your data. If you are using custom plugins, check that a compatible version is available. Elasticsearch can usually be upgraded using a rolling upgrade process, resulting in no interruption of service. This section details how to perform both rolling upgrades and upgrades with full cluster restarts. To determine whether a rolling upgrade is supported for your release, please consult this table: Upgrade From Upgrade To Supported Upgrade Type 1.x 5.x Reindex to upgrade 2.x 2.y Rolling upgrade(wherey > x) 2.x 5.x Full cluster restart 5.0.0 pre GA 5.x Full cluster restart 5.x 5.y Rolling upgrade(wherey > x) Indices created in Elasticsearch 1.x or before Elasticsearch is able to read indices created in theprevious major version only. For instance, Elasticsearch 5.x can use indices created in Elasticsearch 2.x, but not those created in Elasticsearch 1.x or before. This condition also applies to indices backed up withsnapshot and restore. If an index was originally created in 1.x, it cannot be restored into a 5.x cluster even if the snapshot was made by a 2.x cluster. Elasticsearch 5.x nodes will fail to start in the presence of too old indices. SeeReindex to upgradefor more information about how to upgrade old indices. !!!回滚升级就是一次升级一个节点!!!! A rolling upgrade allows the Elasticsearch cluster to be upgraded one node at a time, with no downtime for end users. Running multiple versions of Elasticsearch in the same cluster for any length of time beyond that required for an upgrade is not supported, as shards will not be replicated from the more recent version to the older version. Reindex to upgrade Elasticsearch is able to use indices created in the previous major version only. For instance, Elasticsearch 5.x can use indices created in Elasticsearch 2.x, but not those created in Elasticsearch 1.x or before. Elasticsearch 5.x nodes will fail to start in the presence of too old indices. If you are running an Elasticsearch 2.x cluster which contains indices that were created before 2.x, you will either need to delete those old indices or to reindex them before upgrading to 5.x. SeeReindex in place. If you are running an Elasticsearch 1.x cluster, you have two options: First upgrade to Elasticsearch 2.4.x, reindex the old indices, then upgrade to 5.x. SeeReindex in place. Create a new 5.x cluster and use reindex-from-remote to import indices directly from the 1.x cluster. SeeUpgrading with reindex-from-remote. Reindex in place The easiest way to reindex old (1.x) indices in place is to use theElasticsearch Migration Plugin. You will need to upgrade to Elasticsearch 2.3.x or 2.4.x first. Upgrading with reindex-from-remote If you are running a 1.x cluster and would like to migrate directly to 5.x without first migrating to 2.x, you can do so usingreindex-from-remote. 转自:https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-upgrade.html#setup-upgrade 本文转自张昺华-sky博客园博客,原文链接:http://www.cnblogs.com/bonelee/p/7443844.html,如需转载请自行联系原作者

优秀的个人博客,低调大师

Apache Storm 官方文档 —— Metrics

Storm 提供了一个可以获取整个拓扑中所有的统计信息的度量接口。Storm 内部通过该接口可以跟踪各类统计数字:executor 和 acker 的数量、每个 bolt 的平均处理时延、worker 使用的最大堆容量等等,这些信息都可以在 Nimbus 的 UI 界面中看到。 Metric 类型 使用 Metrics 只需要实现一个接口方法:getValueAndReset,在方法中可以查找汇总值、并将该值复位为初始值。例如,在 MeanReducer 中就实现了通过运行总数除以对应的运行计数的方式来求取均值,然后将两个值都重新设置为 0。 Storm 提供了以下几种 metric 类型: AssignableMetric— 将 metric 设置为指定值。此类型在两种情况下有用:1. metric 本身为外部设置的值;2. 你已经另外计算出了汇总的统计值。 CombinedMetric— 可以对 metric 进行关联更新的通用接口。 CountMetric— 返回 metric 的汇总结果。可以调用incr()方法来将结果加一;调用incrBy(n)方法来将结果加上给定值。 MultiCountMetric— 返回包含一组 CountMetric 的 HashMap ReducedMetric MeanReducer— 跟踪由它的reduce()方法提供的运行状态均值结果(可以接受Double、Integer、Long等类型,内置的均值结果是Double类型)。MeanReducer 确实是一个相当棒的家伙。 MultiReducedMetric— 返回包含一组 ReducedMetric 的 HashMap Metric Consumer 构建自定义 metric 内建的 Metric builtin_metrics.clj为内建的 metrics 设置了数据结构,以及其他框架组件可以用于更新的虚拟方法。metrics 本身是在回调代码中实现计算的 — 请参考executor.clj中的ack-spout-msg的例子。 转载自并发编程网 - ifeve.com

优秀的个人博客,低调大师

Apache Storm 官方文档 —— Hooks

Storm 提供了一种 hooks 机制,可以实现在 Storm 的各种事件流中运行自定义代码的功能。可以通过继承BaseTaskHook类来创建 hook,还可以根据需要在继承的子类中覆写适当的方法来跟踪相关事件。 注册 hook 有两种方法: 在 spout 的 open 方法或者 bolt 的 prepare 方法中使用TopologyContext#addTaskHook方法; 使用 Storm 配置表中的topology.auto.task.hooks配置项。之后这些 hook 会自动注册到每个 spout 和 bolt 中,这样就可以很方便地处理例如集成自定义的系统监控代码之类的事情了。 转载自并发编程网 - ifeve.com

优秀的个人博客,低调大师

Apache Storm 官方文档 —— FAQ

Storm 最佳实践 关于配置 Storm + Trident 的建议 worker 的数量最好是服务器数量的倍数;topology 的总并发度(parallelism)最好是 worker 数量的倍数;Kafka 的分区数(partitions)最好是 Spout(特指KafkaSpout)并发度的倍数 在每个机器(supervisor)上每个拓扑应用只配置一个 worker 在拓扑最开始运行的时候设置使用较少的大聚合器,并且最好是每个 worker 进程分配一个 使用独立的调度器(scheduler)来分配任务(关于Scheduler 的知识请参考xumingming 的博客—— 译者注) 在每个 worker 上只配置使用一个 acker —— 这是 0.9.x 版本的默认特性,不过在早期版本中有所不同 在配置文件中开启 GC 日志记录;如果一切正常,日志中记录的 major GC 应该会非常少 将 trident 的 batch interval 配置为你的集群的端到端时延的 50% 左右 开始时设置一个很小的TOPOLOGY_MAX_SPOUT_PENDING(对于 trident 可以设置为 1,对于一般的 topology 可以设置为 executor 的数量),然后逐渐增大,直到数据流不再发生变化。这时你可能会发现结果大约等于“2 × 吞吐率(每秒收到的消息数) × 端到端时延”(最小的额定容量的2倍)。 如何避免 worker 总是出现莫名其妙的故障的问题 确保 Storm 对你的日志目录有写权限 确保你的堆内存没有溢出 确保所有的 worker 上都已经正确地安装了所有的依赖库文件 确保 ZooKeeper 的 hostname 不是简单地设置为 “localhost” 确保集群中的每台机器上都配置好了正确、唯一的 hostname,并且这些 hostname 需要配置在所有机器的 Storm 配置文件中 确保 a) 不同的 worker 之间,b) 不同的 Storm 节点之间,c) Storm 与 ZooKeeper 集群之间, d) 各个 worker 与拓扑运行所需要的 Kafka/Kestrel/其他数据库等 之间没有开启防火墙或者其他安全保护机制;如果有,请使用 netstat 来为各个端口之间的通信授权 Help!Storm 使用过程中无法获取: 日志文件:日志文件默认记录在$STORM_HOME/logs目录中。请检查你对该目录是否有写权限。具体的日志配置信息位于 logback/cluster.xml 文件中(0.9 之前的版本需要在 log4j/*.properties 配置文件中进行配置。 最终输出的 JVM 设置:需要在配置文件(storm.yaml)的childopts配置项中添加-XX+PrintFlagsFinal命令选项。 最终输出的 Java 系统属性信息:需要在你构建拓扑的位置添加代码Properties props = System.getProperties(); props.list(System.out); 我应该使用多少个 worker? worker 的完整数量是由 supervisor 配置的。每个 supervisor 会分配到一定数量的 JVM slot,你在拓扑中设置的 worker number 就是以这个 slot 数量为依据进行分配的。 不建议为每个拓扑在每台机器上分配超过一个 worker。 假如有一个运行于三台 8 核服务器节点的拓扑,它的并行度为24,每个 bolt 在每台机器上分配有 8 个 executor(即每个 CPU 核心分配一个)。这种场景下,使用三个 worker (每个 worker 分配 8 个executor)相对于使用更多的 worker (比如使用 24 个 worker,为每个 executor 分别分配一个)有三点好处: 首先,在 worker 内部将数据流重新分区到不同的 executor 的操作(比如 shuffle 或者 group-by)就不会产生触发到传输 buffer 缓冲区,tuple 会直接从发送端转储到接收端的 buffer 区。这一点有很大的优势。相反,如果目标 executor 是在同一台机器的不同 worker 进程内,tuple 就需要经历“发送 -> worker 传输队列 -> 本地 socket 端口 -> 接收端 worker -> 接收端 executor”这样一个漫长的过程。虽然这个过程并不会产生网络级传输,但是在同一台机器的不同进程间的传输损耗也是很可观的。 其次,三个大的聚合器带来的大的缓存空间比 24 个小聚合器带来的小缓存空间要有用得多。因为这回降低数据倾斜造成的影响,同时提高 LRU 的性能。 最后,更少的 worker 可以有效地降低控制流的频繁变动。 拓扑 Trident 拓扑支持多数据流吗 Trident 拓扑可以设计成条件路径(if-else)的工作流形式吗?例如,bolt0 在接收 spout 的数据流时,可以根据输入 tuple 的值来选择将数据流发送到 bolt1 或者 bolt2,而不是同时向两个 bolt 发送。 Trident 的 “each” 运算符可以返回一个数据流对象,你可以将该对象存储在某个变量中,然后你可以对同一个数据流执行多个 each 操作来分解该数据流,如下述代码所示: Stream s = topology.each(...).groupBy(...).aggregate(...) Stream branch1 = s.each(..., FilterA) Stream branch2 = s.each(..., FilterB) 你可以使用 join、merge 或者 multiReduce 来联结各个数据流。 到目前为止,Trident 暂时不支持输出多个数据流。(详见STORM-68) Spout Coordinator 是什么,为什么会有很多 Coordinator? Trident spout 实际上是通过 Storm 的 bolt 运行的。MasterBatchCoordinator(MBC)封装了 Trident 拓扑的 spout,它负责整合 Trident 中的 batch,这一点对于你所使用的任何类型的 spout 而言都是一样的。Trident 的 batch 就是在 MBC 向各个 spout-coordinator 分发种子 tuple 的过程中生成的。Spout-coordinator bolt 知道你所定义的 spout 是如何互相协作的 —— 实际上,在使用 Kafka 的情况下,各个 spout 就是通过 spout-coordinator 来获取 pull 消息所需要的 partition 和 offset 信息的。 在 spout 的 metadata 记录中能够存储什么信息? 只能存储少量静态数据,而且是越少越好(尽管你确实可以向其中存储更多的信息,不过我们不推荐这样做)。 emitPartitionBatchNew函数是多久调用一次的? 由于在 Trident 中 MBC 才是实际运行的 spout,一个 batch 中的所有 tuple 都是 MBC 生成的 tuple 树的节点。也就是说,Storm 的 “max spout pending” 参数实际上定义的是可以并发运行的 batch 数量。MBC 在满足以下两个条件下会发送出一个新的 batch:首先,挂起的 tuple 数需要小于 “max pending” 参数;其次,距离上一个 batch 的发送已经过去了至少一个trident batch interval的间隔时间。 如果没有数据发送,Trident 会降低发送频率吗? 是的,Storm 中有一个可选的 “spout 等待策略”,默认配置是 sleep 一段指定的配置时间。 Trident batch interval 参数有什么用? 你知道 486 时代的计算机上面为什么有个trubo button吗?这个参数的作用和这个按钮有点像。 实际上,trident batch interval 有两个用处。首先,它可以用于减缓 spout 从远程数据源获取数据的速度,但这不会影响数据处理的效率。例如,对于一个从给定的 S3 存储区中读取批量上传文件并按行发送数据的 spout,我们就不希望它经常触发 S3 的阈值,因为文件要隔几分钟才会上传一次,而且每个 batch 也需要花费一定的时间来执行。 另一个用处是限制启动期间或者突发数据负载情况下内部消息队列的负载压力。如果 spout 突然活跃起来,并向系统中挤入了 10 个 batch 的记录,那么可能会有从 batch7 开始的大量不紧急的 tuple 堵塞住传输缓冲区,并且阻塞了从 batch3 中的 tuple(甚至可能包含 batch3 中的部分旧 tuple)的 commit 过程#。对于这种情况,我们的解决方法就是将 trident batch interval 设置为正常的端到端处理时延的一半左右 —— 也就是说如果需要花费 600 ms 的时间处理一个 batch,那么就可以每 300 ms 处理一个 batch。 注意,这个 300 ms 仅仅是一个上限值,而不是额外增加的延时时间,如果你的 batch 需要花费 258 ms 来运行,那么 Trident 就只会延时等待 42 ms。 如何设置 batch 大小? Trident 本身不会对 batch 进行限制。不过如果使用 Kafka 的相关 spout,那么就可以使用 max fetch bytes 大小除以 平均 record 大小来计算每个子 batch 分区的有效 record 大小。 怎样重新设置 batch 的大小? Trident 的 batch 在某种意义上是一种过载的设施。batch 大小与 partition 的数量均受限于或者是可以用于定义#: 事务安全单元(一段时间内存在风险的 tuple); 相对于每个 partition,一个用于窗口数据流分析的有效窗口机制; 相对于每个 partition,使用 partitionQuery,partitionPersist 等命令时能够同时进行的查询操作数量; 相对于每个 partition,spout 能够同时分配的 record 数量。 不能在 batch 生成之后更改 batch 的大小,不过可以通过 shuffle 操作以及修改并行度的方式来改变 partition 的数量。 时间相关问题 怎样基于指定时间聚合数据 对于带有固定时间戳的 records,如果需要对他们执行计数、求均值或者聚合操作,并将结果整合到离散的时间桶(time bucket)中,Trident 是一个很好的具有可扩展性的解决方案。 这种情况下可以写一个each函数来将时间戳置入一个时间桶中:如果桶的大小是以“小时”为单位的,那么时间戳2013-08-08 12:34:56就会被匹配到2013-08-08 12:00:00桶中,其他的 12 时到 13 时之间的时间也一样。然后可以使用persistentAggregate来对时间桶分组。persistentAggregate会使用一个基于数据存储的本地 cacheMap。这些包含有大量 records 的 group 会使用高效的批量读取/写入方式对数据存储区进行操作,所以并不会对数据存储区进行大量的读操作;只要你的数据传送足够快捷,Trident 就可以高效地使用内存与网络。即使某台服务器宕机了一天,需要重新快速地发送一整天的数据,旧有的结果也可以静默地获取到并进行更新,并且这并不会影响当前结果的计算过程。 怎么才能知道某个时间桶中已经收到了所有需要的 record? 很遗憾,你不会知道什么时候所有的 event 都已经采集到了 —— 这是一个认识论问题,而不是一个分布式系统的问题。你可以: 使用域相关知识来设定时间限制。 引入标记机制:对于一个指定时间窗,确定某个 record 会处在所有的 record 的最后位置。Trident 使用这个机制来判断一个 batch 是否结束。例如,你收到一组传感器采集到的 records,每个传感器都是按顺序发送数据的,那么一旦所有的传感器都发送出一个 “3:02:xx” 的数据,你就可以知道可以开始处理这个时间窗了。 如果可以的话,尽量使你的处理过程增量化:每个新来的值都会使结果越来越准确。Trident ReducerAggregator 就是一个可以通过一个旧有的结果以及一组新数据来返回一个更新的结果的运算符。这使得结果可以被缓存并序列化到一个数据库中;如果某台服务器宕机了一天,在恢复运行之后需要重新快速地发送一整天的数据,旧有的结果也可以静默地获取到并进行更新。 使用 Lambda 架构:将所有收到的事件数据归档到存储区中(S3,HBase,HDFS)。在快速处理层,一旦时间窗复位,就对对应的时间桶进行处理来获取有效结果,并且在处理过程中跳过所有比早于该时间窗的过期数据。定期地执行全局聚合操作就可以计算出一个较“正确”的结果。 附注 #此处译文可能不够准确,有疑问的读者请参考原文对应内容。 转载自并发编程网 - ifeve.com

优秀的个人博客,低调大师

Apache Storm 官方文档 —— 配置

Storm 有大量配置项用于调整 nimbus、supervisors 和拓扑的行为。有些配置项是系统级的配置项,在拓扑中不能修改,另外一些配置项则是可以在拓扑中修改的。 每一个配置项都在 Storm 代码库的defaults.yaml中有一个默认值。可以通过在 Nimbus 和 Supervisors 的环境变量中定义一个 storm.yaml 来覆盖默认值。最后,在使用StormSubmitter提交拓扑时也可以定义基于具体拓扑的配置项。但是,基于拓扑的配置项仅仅能够覆盖那些以 “TOPOLOGY” 作为前缀的配置项。 Storm 0.7.0 以上版本支持覆写每个 Bolt/Spout 的配置信息。不过,使用这种方式只能修改以下几个配置项: “topology.debug” “topology.max.spout.pending” “topology.max.task.parallelism” “topology.kryo.register”:由于序列化对拓扑中的所有组件都是可见的,这一项与其他几项稍微有一些不同,详细信息可以参考Storm 的序列化 Storm 的 Java API 支持两种自定义组件配置信息的方式: 内置型:在需要配置的 Spout/Bolt 中覆写getComponentConfiguration方法,使其返回特定组件的配置表; 外置型:TopologyBuilder中的setSpout与setBolt方法会返回一个带有addConfiguration方法的ComponentConfigurationDeclarer对象,通过addConfiguration方法就可以覆写对应组件的配置项(同时也可以添加自定义的配置信息——译者注)。 配置信息的优先级依次为:defaults.yaml < storm.yaml < 拓扑配置 < 内置型组件信息配置 < 外置型组件信息配置。 相关资料 Config:此类包含所有可配置项的列表,对于创建拓扑配置信息很有帮助 defaults.yaml:所有配置项的默认值 配置 Storm 集群:说明了如何创建、配置一个 Storm 集群 在生产环境中运行拓扑:列出了在集群中运行拓扑的一些有用的配置项 本地模式:列出了使用本地模式时比较有用的配置项 转载自并发编程网 - ifeve.com

优秀的个人博客,低调大师

《Spark 官方文档》硬件配置

硬件配置 Spark开发者们常常被问到的一个问题就是:如何为Spark配置硬件。我们通常会给出以下的建议,但具体的硬件配置还依赖于实际的使用情况。 存储系统 因为绝大多数Spark作业都很可能是从外部存储系统加载输入数据(如:HDFS或者HBase),所以最好把Spark部署在离这些存储比较近的地方。建议如下: 只要有可能,就尽量在HDFS相同的节点上部署Spark。最简单的方式就是,在HDFS相同的节点上独立部署Spark(standalone mode cluster),并配置好Spark和Hadoop的内存和CPU占用,以避免互相干扰(对Hadoop来说,相关的选项有 mapred.child.java.opts – 配置单个任务的内存,mapred.tasktracker.map.tasks.maximun和mapred.tasktracker.reduce.tasks.maximum – 配置任务个数)。当然,你也可以在一些通用的集群管理器上同时运行Hadoop和Spark,如:Mesos或Hadoop YARN。 如果不能将Spark和HDFS放在一起,那么至少要将它们部署到同一局域网的节点中。 对于像HBase这类低延迟数据存储来说,比起一味地避免存储系统的互相干扰,更需要关注的是将计算分布到不同节点上去。 本地磁盘 虽然大部分情况下,Spark都是在内存里做计算,但它仍会使用本地磁盘存储数据,如:存储无法装载进内存的RDD数据,存储各个阶段(stage)输出的临时文件。因此,我们建议每个节点上用4~8块磁盘,非磁盘阵列方式挂载(只需分开使用单独挂载点即可)。在Linux中,挂载磁盘时使用noatimeoption可以减少不必要的写操作。在Spark中,配置(configure)spark.local.dir 属性可指定Spark使用的本地磁盘目录,其值可以是逗号分隔的列表以指定多个磁盘目录。如果该节点上也有HDFS目录,可以和HDFS共用同一个块磁盘。 内存 一般来说,Spark可以在8GB~几百GB内存的机器上运行得很好。不过,我们还是建议最多给Spark分配75%的内存,剩下的内存留给操作系统和系统缓存。 每次计算具体需要多少内存,取决于你的应用程序。如需评估你的应用程序在使用某个数据集时会占用多少内存,可以尝试先加载一部分数据集,然后在Spark的监控UI(http://<driver-node>:4040)上查看其占用内存大小。需要注意的是,内存占用很大程度受存储级别和序列化格式影响 – 更多内存优化建议,请参考调优指南(tuning guide)。 最后,还需要注意的是,Java虚拟机在200GB以上内存的机器上并非总是表现良好。如果你的单机内存大于200GB,建议在单个节点上启动多个worker JVM。在Spark独立部署模式下(standalone mode),你可在conf/spark-env.sh 中设置 SPARK_WORKER_INSTANCES 来配置单节点上worker个数,而且在该文件中你还可以通过 SPARK_WORKER_CORES 设置单个worker占用的CPU core个数。 网络 以我们的经验来说,如果数据能加载进内存,那么多数Spark应用的瓶颈都是网络带宽。对这类应用,使用万兆网(10 Gigabit)或者更强的网络是最好的优化方式。对于一些包含有分布式归约相关算子(distributed reduce相关算子,如:group-by系列,reduce-by系列以及SQL join系列)的应用尤其是如此。对于任何一个应用,你可以在监控UI(http://<driver-node>:4040)上查看Spark混洗跨网络传输了多少数据量。 CPU Cores Spark在单机几十个CPU的机器上也能表现良好,因为Spark尽量减少了线程间共享的数据。但一般你至少需要单机8~16个CPU cores。当然,根据具体的计算量你可能需要更多的CPU,但是:一旦数据加载进内存,绝大多数应用的瓶颈要么是CPU,要么是网络。 转载自并发编程网 - ifeve.com

优秀的个人博客,低调大师

《Flink官方文档》示例总览

示例 Java 的示例项目和Scala 的示例项目指导了构建Maven和SBT项目,并包含了一个单词计数程序的简单实现。 监控Wikipedia编辑是一个更复杂的流式分析应用 用Apache Flink、Elasticsearch和Kibana构建实时面板应用是发布在elastic.co上的一个博客,展示了如何用Apache Flink、Elasticsearch和Kibana去构建实时面板来解决流数据分析。 捆绑示例 Flink 资源包含了很多流式(java/scala) 和批处理(java/scala)的示例。这些介绍说明了这些示例如何运行。 转载自 并发编程网 - ifeve.com

优秀的个人博客,低调大师

Redisson官方文档 - 13. 工具

13.1. 集群管理工具 Redisson集群管理工具提供了通过程序化的方式,像redis-trib.rb脚本一样方便地管理Redis集群的工具。 13.1.1 创建集群 以下范例展示了如何创建三主三从的Redis集群。 ClusterNodes clusterNodes = ClusterNodes.create() .master("127.0.0.1:7000").withSlaves("127.0.0.1:7001", "127.0.0.1:7002") .master("127.0.0.1:7003").withSlaves("127.0.0.1:7004") .master("127.0.0.1:7005"); ClusterManagementTool.createCluster(clusterNodes); 主节点127.0.0.1:7000的从节点有127.0.0.1:7001和127.0.0.1:7002。 主节点127.0.0.1:7003的从节点是127.0.0.1:7003。 主节点127.0.0.1:7005没有从节点。 13.1.2 踢出节点 以下范例展示了如何将一个节点踢出集群。 ClusterManagementTool.removeNode("127.0.0.1:7000", "127.0.0.1:7002"); // 或 redisson.getClusterNodesGroup().removeNode("127.0.0.1:7002"); 将从节点127.0.0.1:7002从其主节点127.0.0.1:7000里踢出。 13.1.3 数据槽迁移 以下范例展示了如何将数据槽在集群的主节点之间迁移。 ClusterManagementTool.moveSlots("127.0.0.1:7000", "127.0.0.1:7002", 23, 419, 4712, 8490); // 或 redisson.getClusterNodesGroup().moveSlots("127.0.0.1:7000", "127.0.0.1:7002", 23, 419, 4712, 8490); 将番号为23,419,4712和8490的数据槽从127.0.0.1:7002节点迁移至127.0.0.1:7000节点。 以下范例展示了如何将一个范围的数据槽在集群的主节点之间迁移。 ClusterManagementTool.moveSlotsRange("127.0.0.1:7000", "127.0.0.1:7002", 51, 9811); // 或 redisson.getClusterNodesGroup().moveSlotsRange("127.0.0.1:7000", "127.0.0.1:7002", 51, 9811); 将番号范围在[51, 9811](含)之间的数据槽从127.0.0.1:7002节点移动到127.0.0.1:7000节点。 13.1.4 添加从节点 以下范例展示了如何向集群中添加从节点。 ClusterManagementTool.addSlaveNode("127.0.0.1:7000", "127.0.0.1:7003"); // 或 redisson.getClusterNodesGroup().addSlaveNode("127.0.0.1:7003"); 将127.0.0.1:7003作为从节点添加至127.0.0.1:7000所在的集群里。 13.2.5 添加主节点 以下范例展示了如何向集群中添加主节点。 ClusterManagementTool.addMasterNode("127.0.0.1:7000", "127.0.0.1:7004"); // 或 redisson.getClusterNodesGroup().addMasterNode("127.0.0.1:7004"); 将127.0.0.1:7004作为主节点添加至127.0.0.1:7000所在的集群里。Adds master node 127.0.0.1:7004 to cluster where 127.0.0.1:7000 participate in 该功能仅限于Redisson PRO版本。

优秀的个人博客,低调大师

Apache Storm 官方文档 —— 常用模式

本文列出了 Storm 拓扑中使用的一些常见模式,包括: 数据流的 join 批处理 BasicBolt 内存缓存与域分组的结合 Top N 流式计算 TimeCacheMap CoordinatedBolt 与 KeyedFairBolt Joins 数据流的 join 一般指的是通过共有的域来聚合两个或多个数据流的过程。与一般的数据库中 join 操作要求有限的输入与清晰的语义不同,数据流 join 的输入往往是无限的数据集,而且并不具备明确的语义。 join 的类型一般是由应用的需求决定的。有些应用需要将两个流在某个固定时间内的所有 tuple 进行 join,另外一些应用却可能要求对每个 join 域的 join 操作过程的两侧只保留一个 tuple,而其他的应用也许还有一些其他需求。不过这些 join 类型一般都会有一个基本的模式,那就是将多个输入流进行分区。Storm 可以很容易地使用域分组的方法将多个输入流聚集到一个联结 bolt 中,比如下面这样: builder.setBolt("join", new MyJoiner(), parallelism) .fieldsGrouping("1", new Fields("joinfield1", "joinfield2")) .fieldsGrouping("2", new Fields("joinfield1", "joinfield2")) .fieldsGrouping("3", new Fields("joinfield1", "joinfield2")); 当然,上面的代码只是个例子,实际上不同的流完全可以具有不同的输入域。 批处理 通常由于效率或者其他方面的原因,你需要使用将 tuple 们组合成 batch 来处理,而不是一个个分别处理它们。比如,在做数据库更新操作或者流聚合操作时,你就会需要这样的批处理形式。 要确保数据处理的可靠性,正确的方式是在 bolt 进行批处理之前将 tuple 们缓存在一个实例变量中。在完成批处理操作之后,你就可以一起 ack 所有的缓存的 tuple 了。 如果这个批处理 bolt 还需要继续向下游发送 tuple,你可能还需要使用多锚定(multi-anchoring)来确保可靠性。具体怎么做取决于应用的需求。想要了解更多关于可靠性的工作机制的内容请参考消息的可靠性保障一文。 BasicBolt Bolt 处理 tuple 的一种基本模式是在execute方法中读取输入 tuple、发送出基于输入 tuple 的新 tuple,然后在方法末尾对 tuple 进行应答(ack)。符合这种模式的 bolt 一般是一种函数或者过滤器。对于这种基本的处理模式,Storm 提供了IBasicBolt接口来自动实现这个过程。更多内容请参考消息的可靠性保障一文。 内存缓存与域分组的结合 在 Storm 的 bolt 中保存一定的缓存也是一种比较常见的方式。尤其是在于域分组结合的时候,缓存的作用特别显著。例如,假如你有一个用于将短链接(short URLs,例如 bit.ly, t.co,等等)转化成长链接(龙 URLs)的 bolt。你可以通过一个将短链接映射到长链接的 LRU 缓存来提高系统的性能,避免反复的 HTTP 请求操作。假如现在有一个名为 “urls” 的组件用于发送短链接,另外有一个 “expand” 组件用于将短链接扩展为长链接,并且在 “expand” 内部保留一个缓存。让我们来看看下面两段代码有什么不同: builder.setBolt("expand", new ExpandUrl(), parallelism) .shuffleGrouping(1); builder.setBolt("expand", new ExpandUrl(), parallelism) .fieldsGrouping("urls", new Fields("url")); 由于域分组可以使得相同的 URL 永远被发往同一个 task,第二段代码会比第一段代码高效得多。这样可以避免在不同的 task 的缓存中的复制动作,并且看上去短 URL 可以更好地在命中缓存。 Top N Storm 中一种常见的连续计算模式是计算数据流中某种形式的 Top N 结果。假如现在有一个可以以 [“value”, “count”] 的形式发送 tuple 的 bolt,并且你需要一个可以根据 count 计算结果输出前 N 个 tuple 的 bolt。实现这个操作的最简单的方法就是使用一个对数据流进行全局分组的 bolt,并且在内存中维护一个包含 top N 结果的列表。 这种方法并不适用于大规模数据流,因为整个数据流都会发往同一个 task,会造成该 task 的内存负载过高。更好的做法是将数据流分区,同时对每个分区计算 top N 结果,然后将这些结果汇总来得到最终的全局 top N 结果。下面是这个模式的代码: builder.setBolt("rank", new RankObjects(), parallelism) .fieldsGrouping("objects", new Fields("value")); builder.setBolt("merge", new MergeObjects()) .globalGrouping("rank"); 这个方法之所以可行是因为第一个 bolt 的域分组操作确保了每个小分区在语义上的正确性。你可以在storm-starter里看到使用这个模式的一个例子。 当然,如果待处理的数据集存在较严重的数据倾斜,那么还是应该使用 partialKeyGrouping 来代替 fieldsGrouping,因为 partialKeyGrouping 可以通过两个下游 bolt 分散每个 key 的负载。 builder.setBolt("count", new CountObjects(), parallelism) .partialKeyGrouping("objects", new Fields("value")); builder.setBolt("rank" new AggregateCountsAndRank(), parallelism) .fieldsGrouping("count", new Fields("key")) builder.setBolt("merge", new MergeRanksObjects()) .globalGrouping("rank"); 这个拓扑中需要一个中间层来聚合来自上游 bolt 数据流的分区计数结果,但这一层仅仅会做一个简单的聚合处理,这样 bolt 就不会受到由于数据倾斜带来的负载压力。你可以在storm-starter中看到使用这个模式的一个例子。 支持 LRU 的 TimeCacheMap 有时候你可能会需要一个能够保留“活跃的”数据并且能够使得超时的“非活跃的”数据自动失效的缓存。TimeCacheMap是一个可以高效地实现此功能的数据结构。它还提供了一个钩子用于实现在数据失效后的回调操作。 用于分布式 RPC 的 CoordinatedBolt 与 KeyedFairBolt 在构建 Storm 上层的分布式 RPC 应用时,通常会用到两种常用的模式。现在这两种模式已经被封装为CoordinatedBolt和KeyedFairBolt,并且已经加入了 Storm 标准库中。 CoordinatedBolt将你的处理逻辑 bolt 包装起来,并且在你的 bolt 收到了指定请求的所有 tuple 之后发出通知。CoordinatedBolt中大量使用了直接数据流组来实现此功能。 KeyedFairBolt同样包装了你的处理逻辑 bolt,并且可以让你的拓扑同时处理多个 DRPC 调用,而不是每次只执行一个。 如果需要了解更多内容请参考分布式 RPC一文。 转载自并发编程网 - ifeve.com

优秀的个人博客,低调大师

Apache Storm 官方文档 —— Trident 教程

Trident 是 Storm 的一种高度抽象的实时计算模型,它可以将高吞吐量(每秒百万级)数据输入、有状态的流式处理与低延时的分布式查询无缝结合起来。如果你了解 Pig 或者 Cascading 这样的高级批处理工具,你就会发现他们和 Trident 的概念非常相似。Trident 同样有联结(join)、聚合(aggregation)、分组(grouping)、函数(function)以及过滤器(filter)这些功能。Trident 为数据库或者其他持久化存储上层的状态化、增量式处理提供了基础原语。由于 Trident 有着一致的、恰好一次的语义,因此推断出 Trident 拓扑的状态也是一件很容易的事。 使用范例 让我们先从一个使用 Trident 的例子开始。这个例子中做了两件事情: 从一个句子的输入数据流中计算出单词流的数量 实现对一个单词列表中每个单词总数的查询 为了实现这个目的,这个例子将会从下面的数据源中无限循环地读取语句数据流: FixedBatchSpout spout = new FixedBatchSpout(new Fields("sentence"), 3, new Values("the cow jumped over the moon"), new Values("the man went to the store and bought some candy"), new Values("four score and seven years ago"), new Values("how many apples can you eat")); spout.setCycle(true); 这个 Spout 会循环地访问语句集来生成语句数据流。下面的代码就是用来实现计算过程中的单词数据流统计部分: TridentTopology topology = new TridentTopology(); TridentState wordCounts = topology.newStream("spout1", spout) .each(new Fields("sentence"), new Split(), new Fields("word")) .groupBy(new Fields("word")) .persistentAggregate(new MemoryMapState.Factory(), new Count(), new Fields("count")) .parallelismHint(6); 让我们一行行地来分析上面的代码。首先我们创建了一个TridentTopology对象,这个对象提供了构造 Trident 计算过程的接口。TridentTopology有一个叫做newStream的方法,这个方法可以从一个输入数据源中读取数据创建一个新的数据流。在这个例子中,输入的数据源就是前面定义的FixedBatchSpout。输入数据源也可以是像 Kestrel 和 Kafka 这样的消息系统。Trident 会通过 ZooKeeper 一直跟踪每个输入数据源的一小部分状态(Trident 具体消费对象的相关元数据)。例如这里的 “spout1” 就对应着 ZooKeeper 中的一个节点,而 Trident 就会在该节点中存放数据源的元数据(metadata)。 Trident 会将数据流处理为很多个小块 tuple 的集合,例如,输入的句子流就会像下面这样被分割成很多个小块: 这些小块的大小主要取决于你的输入吞吐量,一般可能会在数万甚至数百万元组的级别。 Trident 为这些小块提供了一个完全成熟的批处理 API。这个 API 和你见到过的 Pig 或者 Cascading 这样的 Hadoop 的高级抽象语言很相似:你可以处理分组(group by)、联结(join)、聚合(aggregation)、函数(function)、过滤器(filter)等各种操作。当然,分别处理每个小块并不是件好事,所以,Trident 提供了适用于处理各个小块之间的聚合操作的函数,并且可以在聚合后将结果保存到持久化存储中,而且无论是内存、Memcached、Cassandra 还是其他类型的存储都可以支持。最后,Trident 还提供了用于查询实时状态结果的一级接口。而这个结果状态既可以像这个例子中演示的那样由 Trident 负责更新,也可以作为一个独立的状态数据源而存在。 再回到这个例子中,输入数据源 spout 发送出了一个名为 “sentence” 的数据流。接下来拓扑中定义了一个Split方法用于处理流中的每个 tuple,这个方法接收 “sentence” 域并将其分割成若干个单词。每个 sentence tuple 都会创建很多个单词 tuple —— 例如 “the cow jumped over the moon” 这个句子就会创建 6 个 “word” tuple,下面是Split的定义: public class Split extends BaseFunction { public void execute(TridentTuple tuple, TridentCollector collector) { String sentence = tuple.getString(0); for(String word: sentence.split(" ")) { collector.emit(new Values(word)); } } } 从上面的代码中你会发现这个过程真的很简单。这个方法中的所有操作仅仅是抓取句子、以空格分隔句子并且为每个单词发射一个 tuple。 拓扑的剩余部分负责统计单词的数量并将结果保存到持久化存储中。首先,数据流根据 “word” 域分组,然后使用Count聚合器持续聚合每个小组。persistentAggregate方法用于存储并更新 state 源中的聚合结果。在这个例子中,单词的数量结果是保存在内存中的,不过可以根据需要切换到 Memcached、Cassandra 或者其他持久化存储中。切换存储模型也非常简单,只需要像下面这样(使用trident-memcached修改persistentAggregate行中的一个参数(其中,“serverLocations” 是 Memcached 集群的地址/端口列表)即可: .persistentAggregate(MemcachedState.transactional(serverLocations), new Count(), new Fields("count")) persistentAggregate方法所存储的值就表示所有从数据流中发送出来的块的聚合结果。 Trident 的另一个很酷的特性就是它支持完全容错性和恰好一次处理的语义。如果处理过程中出现错误需要重新执行处理操作,Trident 不会向数据库中提交多次来自相同的源数据的更新操作,这就是 Trident 持久化 state 的方式。 persistentAggregate方法也可以将数据流结果传入一个TridentState对象中。这种情况下,这个TridentState就表示所有的单词统计信息。这样我们就可以使用TridentState对象来实现整个计算过程中的分布式查询部分。 接下来我们就可以在拓扑中实现 word count 的一个低延时分布式查询。这个查询接收一个由空格分隔的单词列表作为参数,然后返回这些单词的数量统计结果。这个查询看上去与普通的 RPC 调用并没有什么分别,不过在后台他们是并发执行的。下面是一个实现这种查询的例子: DRPCClient client = new DRPCClient("drpc.server.location", 3772); System.out.println(client.execute("words", "cat dog the man"); // prints the JSON-encoded result, e.g.: "[[5078]]" 如你所见,这个查询看上去只是一个普通的远程过程调用(RPC),不过在后台他是在一个 Storm 集群中并发执行的。这种查询的端到端延时一般在 10 ms 左右。当然,更大量的查询会花费更长的时间,尽管这些查询还是取决于你为这个计算过程分配了多少时间。 拓扑中的分布式查询的实现是这样的: topology.newDRPCStream("words") .each(new Fields("args"), new Split(), new Fields("word")) .groupBy(new Fields("word")) .stateQuery(wordCounts, new Fields("word"), new MapGet(), new Fields("count")) .each(new Fields("count"), new FilterNull()) .aggregate(new Fields("count"), new Sum(), new Fields("sum")); 这里还需要使用前面的TridentTopology对象来创建一个 DRPC 数据流,这个创建数据流的方法叫做 “words”。前面使用DRPCClient进行 RPC 调用的第一个参数必须与这个方法名完全相同。 在这段代码里,首先是使用Split方法来将请求的参数分割成若干个单词。这些单词构成的单词流是通过 “word” 域来分组的,而stateQuery运算符就是用来查询拓扑中第一个部分中生成的TridentState对象的。stateQuery接收一个 state(在这个例子中就是拓扑前面计算得到的单词数结果)和查询这个 state 的方法作为参数。在这个例子里,stateQuery调用了MapGet方法,用于获取每个单词的个数。由于 DRPC 数据流是和 TridentState 采用的完全相同的方式进行分组的(通过 “word” 域),每个单词查询都可以精确地定位到 TridentState 对象中的指定部分,同时 TridentState 对象中维护着对应的单词的更新状态。 接下来,个数为 0的单词会被FilterNull过滤器过滤掉,然后就可以使用Sum聚合器来获取其他的单词统计个数。接着 Trident 就会自动将结果返回给等待的客户端。 Trident 很聪明,它知道怎么以最好的性能运行拓扑。在这个拓扑中还有两个会自动发生的有趣的事: 从 state 中读取或写入的操作(例如 persistentAggregate 和 stateQuery)会自动批处理化。因此,如果当前的批处理过程需要对数据库执行 20 个更新操作,Trident 就会自动将读取或写入操作当作批处理过程,仅仅会对数据库发送一次读请求和一次写请求,而不是发送 20 次读请求和 20 次写请求(而且一般你还可以在你的 state 里使用缓存来消除读请求)。这样做就有两个方面的好处:可以按照你指定的方式来执行你的计算过程,同时还可以维持较好的性能。 Trident 的聚合器是高度优化的。在向网络中发送 tuple 之前,Trident 有时候会做部分聚合操作,而不是将一个分组的所有的 tuple 一股脑地发送到同一台机器中来执行聚合。例如,Count聚合器就是这样先计算每个小块的个数,然后向网络中发送很多个部分计数的结果,接着再将所有的部分计数结果汇总来得到最终的统计结果。这个技术与 MapReduce 的 combiner 模型很相似。 我们再来看看 Trident 的另一个例子。 Reach 这个例子是一个纯粹的 DRPC 拓扑,计算了一个指定 URL 的 Reach 数。Reach 指的是 Twitter 上能够看到一个指定的 URL 的独立用户数。要想计算 Reach,你需要先提取所有转发了该 URL 的用户,提取这些用户的关注者,将关注者放入一个 set 集合中来去除重复的关注者,然后再统计这个 set 中的数量。对于单一的一台机器来说,计算 reach 太耗时了,这个过程大概需要数千次数据库调用并生成数千万 tuple。而使用 Storm 和 Trident 就可以通过一个集群来将计算过程的每个步骤进行并行化处理。 这个拓扑会从两个 state 源中读取数据。其中一个数据库建立了 URL 和转发了该 URL 的用户列表的关联表。另一个数据库中建立了用户和用户的关注者列表的关联表。拓扑的定义是这样的: TridentState urlToTweeters = topology.newStaticState(getUrlToTweetersState()); TridentState tweetersToFollowers = topology.newStaticState(getTweeterToFollowersState()); topology.newDRPCStream("reach") .stateQuery(urlToTweeters, new Fields("args"), new MapGet(), new Fields("tweeters")) .each(new Fields("tweeters"), new ExpandList(), new Fields("tweeter")) .shuffle() .stateQuery(tweetersToFollowers, new Fields("tweeter"), new MapGet(), new Fields("followers")) .parallelismHint(200) .each(new Fields("followers"), new ExpandList(), new Fields("follower")) .groupBy(new Fields("follower")) .aggregate(new One(), new Fields("one")) .parallelismHint(20) .aggregate(new Count(), new Fields("reach")); 这个拓扑使用newStaticState方法创建了两个分别对应外部于两个外部数据库的TridentState对象。在拓扑的后续部分就可以对这两个TridentState对象执行查询操作。和 state 的所有数据源一样,为了最大程度地提升效率,对这些数据库的查询将会自动地批处理化。 拓扑的定义很直接 —— 就是一个简单的批处理 job。首先,会通过查询 urlToTweeters 数据库来获取转发了 URL 的用户列表,然后就可以调用ExpandList方法来为每个 tweeter 创建一个 tuple。 接下来必须要获取每个 tweeter 的关注者。由于需要调用 shuffle 方法将所有的 tweeter 均衡分配到拓扑的所有 worker 中,所以这个步骤必须并发进行,这一点非常重要。然后就可以查询关注者数据库来获取每个 tweeter 的关注者列表。你可能注意到了这个过程的并行度非常高,因为这是整个计算过程中复杂度最高的部分。 再接下来,关注者就会被放入一个单独的 set 集合中用于计数。这里包含两个步骤。首先,会根据 “follower” 域来执行 “group by” 分组操作,并在每个组上运行One聚合器。“One”聚合器的作用仅仅是为每个组发送一个包含数字 1 的 tuple。然后,就可以通过统计这些 one 结果来得到关注者 set 的大小,也就是真正的关注者数量。下面是 “One” 聚合器的定义: public class One implements CombinerAggregator<Integer> { public Integer init(TridentTuple tuple) { return 1; } public Integer combine(Integer val1, Integer val2) { return 1; } public Integer zero() { return 1; } } 这是一个“组合聚合器”,它知道怎样在向网络中发送 tuple 之前以最好的效率进行部分聚合操作。同样,Sum 也是一个组合聚合器,所以在拓扑结尾的全局统计操作也会有很高的效率。 下面让我们再来看看 Trident 中的一些细节。 域(Fields)与元组(tuples) Trident 的数据模型 TridentTuple 是一个指定的值列表。在一个拓扑中,tuple 是在一系列操作中不断生成的。这些操作一般会输入一个“输入域”(input fields)集合,然后发送出一个“方法域”(function fields)的集合。输入域主要用于选取一个 tuple 的子集作为操作的输入,而“方法域”主要用于为该操作的输出结果域命名。 我们来看看这样一个场景。假设你有一个名为 “stream” 的数据流,其中包含域 “x”、“y” 和 “z”。如果要运行一个接收 “y” 作为输入的过滤器 MyFilter,你可以这样写: stream.each(new Fields("y"), new MyFilter()) 再假设 MyFilter 的实现是这样的: public class MyFilter extends BaseFilter { public boolean isKeep(TridentTuple tuple) { return tuple.getInteger(0) < 10; } } 这样就会保留所有 “y” 域的值小于 10 的 tuple。MyFilter 输入的 TridentTuple 将会仅包含有 “y” 域。值得注意的是,Trident 可以在选取输入域时以一种非常高效的方式来投射 tuple 的子集:这个投射过程非常灵活。 我们再来看看 “function fields” 是怎么工作的。假设你有这样一个函数: public class AddAndMultiply extends BaseFunction { public void execute(TridentTuple tuple, TridentCollector collector) { int i1 = tuple.getInteger(0); int i2 = tuple.getInteger(1); collector.emit(new Values(i1 + i2, i1 * i2)); } } 这个函数接收两个数字作为输入,然后发送出两个新值:分别是两个数字的和和乘积。再假定你有一个包含 “x”、“y” 和 “z” 域的数据流,你可以这样使用这个函数: stream.each(new Fields("x", "y"), new AddAndMultiply(), new Fields("added", "multiplied")); 这个函数的输出增加了两个新的域。因此,这个 each 调用的输出 tuple 会包含 5 个域:“x”、“y” 、“z”、“added” 和 “multiplied”。其中 “added” 与 AddAndMultiply 的第一个输出值相对应,“multiplied” 和 AddAndMultiply 的第二个输出值相对应。 另一方面,通过聚合器,函数域也可以替换输入 tuple 的域。假如你有一个包含域 “val1” 和域 “val2” 的数据流,通过这样的操作: stream.aggregate(new Fields("val2"), new Sum(), new Fields("sum")) 就会使得输出数据流中只包含一个只带有 “sum” 的域的 tuple,这个 “sum” 域就代表了在哪个批处理块中所有的 “val2” 域的总和值。 通过数据流分组,输出就可以同时包含用于分组的域以及由聚合器发送的域。举个例子: stream.groupBy(new Fields("val1")) .aggregate(new Fields("val2"), new Sum(), new Fields("sum")) 这个操作就会使得输出同时包含域 “val1” 以及域 “sum”。 State 实时计算的一个关键问题就在于如何管理状态(state),使得在失败与重试操作之后的更新过程仍然是幂等的。错误是不可消除的,所以在出现节点故障或者其他问题发生时批处理操作还需要进行重试。不过这里最大的问题就在于怎样执行一种合适的状态更新操作(不管是针对外部数据库还是拓扑内部的状态),来使得每个消息都能够被执行且仅仅被执行一次。 这个问题很麻烦,接下来的例子里面就有这样的问题。假如你正在对你的数据流做一个计数聚合操作,并且打算将计数结果存储到一个数据库中。如果你仅仅把计数结果存到数据库里就完事了的话,那么在你继续准备更新某个块的状态的时候,你没法知道到底这个状态有没有被更新过。这个数据块有可能在更新数据库的步骤上成功了,但在后续的步骤中失败了,也有可能先失败了,没有进行更新数据库的操作。你完全不知道到底发生了什么。 Trident 通过下面两件事情解决了这个问题: 在 Trident 中为每个数据块标记了一个唯一的 id,这个 id 就叫做“事务 id”(transaction id)。如果数据块由于失败回滚了,那么它持有的事务 id 不会改变。 State 的更新操作是按照数据块的顺序进行的。也就是说,在成功执行完块 2 的更新操作之前,不会执行块 3 的更新操作。 基于这两个基础特性,你的 state 更新就可以实现恰好一次(exactly-once)的语义。与仅仅向数据库中存储计数不同,这里你可以以一个原子操作的形式把事务 id 和计数值一起存入数据库。在后续更新这个计数值的时候你就可以先比对这个数据块的事务 id。如果比对结果是相同的,那么就可以跳过更新操作 —— 由于 state 的强有序性,可以确定数据库中已经包含有当前数据库的额值。而如果比对结果不同,就可以放心地更新计数值了。 当然,你不需要在拓扑中手动进行这个操作,操作逻辑已经在 State 中封装好了,这个过程会自动进行。同样的,你的 State 对象也不一定要实现事务 id 标记:如果你不想在数据库里耗费空间存储事务 id,你就不用那么做。在这样的情况下,State 会在出现失败的情形下保持“至少处理一次”的操作语义(这样对你的应用也是一件好事)。在这篇文章里你可以了解到更多关于如何实现 State 以及各种容错性权衡技术。 你可以使用任何一种你想要的方法来实现 state 的存储操作。你可以把 state 存入外部数据库,也可以保存在内存中然后在存入 HDFS 中(有点像 HBase 的工作机制)。State 也并不需要一直保存某个状态值。比如,你可以实现一个只保存过去几个小时数据并将其余的数据删除的 State。这是一个实现 State 的例子:Memcached integration。 Trident 拓扑的运行 Trident 拓扑会被编译成一种尽可能和普通拓扑有着同样的运行效率的形式。只有在请求数据的重新分配(比如 groupBy 或者 shuffle 操作)时 tuple 才会被发送到网络中。因此,像下面这样的 Trident 拓扑: 就会被编译成若干个 spout/bolt: 总结 Trident 让实时计算变得非常简单。从上面的描述中,你已经看到了高吞吐量的数据流处理、状态操作以及低延时查询处理是怎样通过 Trident 的 API 来实现无缝结合的。总而言之,Trident 可以让你以一种更加自然,同时仍然保持着良好的性能的方式来实现实时计算。 转载自并发编程网 - ifeve.com

优秀的个人博客,低调大师

Apache Storm 官方文档 —— Trident State

Trident 中含有对状态化(stateful)的数据源进行读取和写入操作的一级抽象封装工具。这个所谓的状态(state)既可以保存在拓扑内部(保存在内存中并通过 HDFS 来实现备份),也可以存入像 Memcached 或者 Cassandra 这样的外部数据库中。而对于 Trident API 而言,这两种机制并没有任何区别。 Trident 使用一种容错性的方式实现对 state 的管理,这样,即使在发生操作失败或者重试的情况下状态的更新操作仍然是幂等的。基于这个机制,每条消息都可以看作被恰好处理了一次,然后你就可以很容易地推断出 Trident 拓扑的状态。 State 的更新过程支持多级容错性保证机制。在讨论这一点之前,我们先来看一个例子,这个例子展示了如何实现恰好一次的语义的技术。假如你正在对数据流进行一个计数聚合操作,并打算将计数结果存入数据库中。在这个例子里,你存入数据库的就是一个对应计数结果的值,每次处理新 tuple 的时候就会增加这个值。 考虑到可能存在的处理失败情况,tuple 有可能需要重新处理。这样就给 state 的更新操作带来了一个问题(或者其他的副作用)—— 你无法知道当前的这个 tuple 的更新操作是否已经处理过了。也许你之前没有处理过这个 tuple,那么你现在就需要增加计数结果;也许你之前已经处理过 tuple 了并且成功地增加了计数结果,但是在后续操作过程中 tuple 的处理失败了,并由此引发了 tuple 的重新处理操作,这时你就不能再增加计数结果了;还有可能你之前在使用这个 tuple 更新数据库的时候出错了,也就是说计数值的更新操作并未成功,此时在 tuple 的重新处理过程中你仍然需要更新数据库。 所以说,如果只是向数据库中简单地存入计数值,你确实无法知道 tuple 是否已经被处理过。因此,你需要一些更多的信息来做决定。Trident 提供了一种支持恰好一次处理的语义,如下所述: 通过小数据块(batch)的方式来处理 tuple(可以参考Trident 教程一文) 为每个 batch 提供一个唯一的 id,这个 id 称为 “事务 id”(transaction id,txid)。如果需要对 batch 重新处理,这个 batch 上仍然会赋上相同的 txid。 State 的更新操作是按照 batch 的顺序进行的。也就是说,在 batch 2 完成处理之前,batch 3 的状态更新操作不会进行。 基于这几个基本性质,你的 State 的实现就可以检测到 tuple 的 batch 是否已经被处理过,并根据检测结果选择合适的 state 更新操作。你具体采用的操作取决于你的输入 spout 提供的语义,这个语义对每个 batch 都是有效的。有三类支持容错性的 spout:“非事务型”(non-transactional)、“事务型”(transactional)以及“模糊事务型”(opaque transactional)。接下来我们来分析下每种 spout 类型的容错性语义。 事务型 spout(Transactional spouts) 记住一点,Trident 是通过小数据块(batch)的方式来处理 tuple 的,而且每个 batch 都会有一个唯一的 txid。spout 的特性是由他们所提供的容错性保证机制决定的,而且这种机制也会对每个 batch 发生作用。事务型 spout 包含以下特性: 每个 batch 的 txid 永远不会改变。对于某个特定的 txid,batch 在执行重新处理操作时所处理的 tuple 集和它的第一次处理操作完全相同。 不同 batch 中的 tuple 不会出现重复的情况(某个 tuple 只会出现在一个 batch 中,而不会同时出现在多个 batch 中)。 每个 tuple 都会放入一个 batch 中(处理操作不会遗漏任何的 tuple)。 这是一种很容易理解的 spout,其中的数据流会被分解到固定的 batches 中。Storm-contrib 项目中提供了一种基于 Kafka 的事务型 spout 实现。 看到这里,你可能会有这样的疑问:为什么不在拓扑中完全使用事务型 spout 呢?这个原因很好理解。一方面,有些时候事务型 spout 并不能提供足够可靠的容错性保障,所以不需要使用事务型 spout。比如,TransactionalTridentKafkaSpout的工作方式就是使得带有某个 txid 的 batch 中包含有来自一个 Kafka topic 的所有 partition 的 tuple。一旦一个 batch 被发送出去,在将来无论重新发送这个 batch 多少次,batch 中都会包含有完全相同的 tuple 集,这是由事务型 spout 的语义决定的。现在假设TransactionalTridentKafkaSpout发送出的某个 batch 处理失败了,而与此同时,Kafka 的某个节点因为故障下线了。这时你就无法重新处理之前的 batch 了(因为 Kafka 的节点故障,Kafka topic 必然有一部分 partition 无法获取到),这个处理过程也会因此终止。 这就是要有“模糊事务型” spout 的原因了 —— 模糊事务型 spout 支持在数据源节点丢失的情况下仍然可以实现恰好一次的处理语义。我们会在下一节讨论这类 spout。 顺便提一点,如果 Kafka 支持数据复制,那么就可以放心地使用事务型 spout 提供的容错性机制了,因为这种情况下某个节点的故障不会导致数据丢失,不过 Kafka 暂时还不支持该特性。(本文的写作时间应该较早,Kakfa 早就已经可以支持复制的机制了 —— 译者注)。 在讨论“模糊事务型” spout 之前,让我们先来看看如何为事务型 spout 设计一种支持恰好一次语义的 State。这个 State 就称为 “事务型 state”,它支持对于特定的 txid 永远只与同一组 tuple 相关联的特性。 假如你的拓扑需要计算单词数,而且你准备将计数结果存入一个 K-V 型数据库中。这里的 key 就是单词,value 对应于单词数。从上面的讨论中你应该已经明白了仅仅存储计数结果是无法确定某个 batch 中的tuple 是否已经被处理过的。所以,现在你应该将 txid 作为一种原子化的值与计数值一起存入数据库。随后,在更新计数值的时候,你就可以将数据库中的 txid 与当前处理的 batch 的 txid 进行比对。如果两者相同,你就可以跳过更新操作 —— 由于 Trident 的强有序性处理机制,可以确定数据库中的值是对应于当前的 batch 的。如果两者不同,你就可以放心地增加计数值。由于一个 batch 的 txid 永远不会改变,而且 Trident 能够保证 state 的更新操作完全是按照 batch 的顺序进行的,所以,这样的处理逻辑是完全可行的。 下面来看一个例子。假如你正在处理 txid 3,其中包含有以下几个 tuple: ["man"] ["man"] ["dog"] 假如数据库中有以下几个 key-value 对: man => [count=3, txid=1] dog => [count=4, txid=3] apple => [count=10, txid=2] 其中与 “man” 相关联的 txid 为 1。由于当前处理的 txid 为 3,你就可以确定当前处理的 batch 与数据库中存储的值无关,这样你就可以放心地将 “man” 的计数值加上 2 并更新 txid 为 3。另一方面,由于 “dog” 的 txid 与当前的 txid 相同,所以,“dog” 的计数是之前已经处理过的,现在不能再对数据库中的计数值进行更新操作。这样,在结束 txid3 的更新操作之后,数据库中的结果就会变成这样: man => [count=5, txid=3] dog => [count=4, txid=3] apple => [count=10, txid=2] 现在我们再来讨论一下“模糊事务型” spout。 模糊事务型 spout(Opaque transactional spouts) 前面已经提到过,模糊事务型 spout 不能保证一个 txid 对应的 batch 中包含的 tuple 完全一致。模糊事务型 spout 有以下的特性: 每个 tuple 都会通过某个 batch 处理完成。不过,在 tuple 处理失败的时候,tuple 有可能继续在另一个 batch 中完成处理,而不一定是在原先的 batch 中完成处理。 OpaqueTridentKafkaSpout就具有这样的特性,同时它对 Kafka 节点的丢失问题具有很好的容错性。OpaqueTridentKafkaSpout在发送一个 batch 的时候总会总上一个 batch 结束的地方开始发送新 tuple。这一点可以保证 tuple 不会被遗漏,而且也不会被多个 batch 处理。 不过,模糊事务型 spout 的缺点就在于不能通过 txid 来识别数据库中的 state 是否是已经处理过的。这是因为在 state 的更新的过程中,batch 有可能会发生变化。 在这种情况下,你应该在数据库中存储更多的 state 信息。除了一个结果值和 txid 之外,你还应该存入前一个结果值。我们再以上面的计数值的例子来分析以下这个问题。假如你的 batch 的部分计数值是 “2”,现在你需要应用一个更新操作。假定现在数据库中的值是这样的: { value = 4, prevValue = 1, txid = 2 } 情形1:假如当前处理的 txid 为 3,这与数据库中的 txid 不同。这时可以将 “prevValue” 的值设为 “value” 的值,再为 “value” 的值加上部分计数的结果并更新 txid。执行完这一系列操作之后的数据库中的值就会变成这样: { value = 6, prevValue = 4, txid = 3 } 情形2:如果当前处理的 txid 为 2,也就是和数据库中存储的 txid 一致,这种情况下的处理逻辑与上面的 txid 不一致的情况又有所不同。因为此时你会知道数据库中的更新操作是由上一个拥有相同 txid 的batch 做出的。不过那个 batch 有可能与当前的 batch 并不相同,所以你需要忽略它的操作。这个时候,你应该将 “prevValue” 加上 batch 中的部分计数值来计算新的 “value”。在这个操作之后数据库中的值就会变成这样: { value = 3, prevValue = 1, txid = 2 } 这种方法之所以可行是因为 Trident 具有强顺序性处理的特性。一旦 Trident 开始处理一个新的 batch 的状态更新操作,它永远不会回到过去的 batch 的处理上。同时,由于模糊事务型 spout 会保证 batch 之间不会存在重复 —— 每个 tuple 只会被某一个 batch 完成处理 —— 所以你可以放心地使用 prevValue 来更新 value。 非事务型 spout(Non-transactional spouts) 非事务型 spout 不能为 batch 提供任何的安全性保证。非事务型 spout 有可能提供一种“至多一次”的处理模型,在这种情况下 batch 处理失败后 tuple 并不会重新处理;也有可能提供一种“至少一次”的处理模型,在这种情况下可能会有多个 batch 分别处理某个 tuple。总之,此类 spout 不能提供“恰好一次”的语义。 不同类型的 Spout 与 State 的总结 下图显示了不同的 spout/state 的组合是否支持恰好一次的消息处理语义: 模糊事务型 state 具有最好的容错性特征,不过这是以在数据库中存储更多的内容为代价的(一个 txid 和两个 value)。事务型 state 要求的存储空间相对较小,但是它的缺点是只对事务型 spout 有效。相对的,非事务型要求的存储空间最少,但是它也不能提供任何的恰好一次的消息执行语义。 你选择 state 与 spout 的时候必须在容错性与存储空间占用之间权衡。可以根据你的应用的需求来确定哪种组合最适合你。 State API 从上文的描述中你已经了解到了恰好一次的消息执行语义的原理是多么的复杂。不过作为用户你并不需要处理这些复杂的 txid 比对、多值存储等操作,Trident 已经在 State 中封装了所有的容错性处理逻辑,你只需要像下面这样写代码即可: TridentTopology topology = new TridentTopology(); TridentState wordCounts = topology.newStream("spout1", spout) .each(new Fields("sentence"), new Split(), new Fields("word")) .groupBy(new Fields("word")) .persistentAggregate(MemcachedState.opaque(serverLocations), new Count(), new Fields("count")) .parallelismHint(6); 所有处理模糊事务型 state 的逻辑已经封装在MemcachedState.opaque的调用中了。另外,状态更新都会自动调整为批处理操作,这样可以减小与数据库的反复交互的资源损耗。 基本的State接口只有两个方法: public interface State { void beginCommit(Long txid); // 对于类似于在 DRPC 流上进行 partitionPersist 的操作,此方法可以为空 void commit(Long txid); } 前面已经说过,state 更新操作的开始时和结束时都会获取一个 txid。对于你的 state 怎么工作,你在其中使用什么样的方法执行更新操作,或者使用什么样的方法从 state 中读取数据,Trident 并不关心。 假如你有一个包含有用户的地址信息的定制数据库,你需要使用 Trident 与该数据库交互。你的 State 的实现就会包含有用于获取与设置用户信息的方法,比如下面这样: public class LocationDB implements State { public void beginCommit(Long txid) { } public void commit(Long txid) { } public void setLocation(long userId, String location) { // code to access database and set location } public String getLocation(long userId) { // code to get location from database } } 接着你就可以为 Trident 提供一个 StateFactory 来创建 Trident 任务内部的 State 对象的实例。对应于你的数据库(LocationDB)的 StateFactory 大概是这样的: public class LocationDBFactory implements StateFactory { public State makeState(Map conf, int partitionIndex, int numPartitions) { return new LocationDB(); } } Trident 提供了一个用于查询 state 数据源的QueryFunction接口,以及一个用于更新 state 数据源的StateUpdater接口。例如,我们可以写一个查询 LocationDB 中的用户地址信息的 “QueryLocation”。让我们从你在拓扑中使用这个操作的方式开始。假如在拓扑中需要读取输入流中的 userid 信息: TridentTopology topology = new TridentTopology(); TridentState locations = topology.newStaticState(new LocationDBFactory()); topology.newStream("myspout", spout) .stateQuery(locations, new Fields("userid"), new QueryLocation(), new Fields("location")) 这里的QueryLocation的实现可能是这样的: public class QueryLocation extends BaseQueryFunction<LocationDB, String> { public List<String> batchRetrieve(LocationDB state, List<TridentTuple> inputs) { List<String> ret = new ArrayList(); for(TridentTuple input: inputs) { ret.add(state.getLocation(input.getLong(0))); } return ret; } public void execute(TridentTuple tuple, String location, TridentCollector collector) { collector.emit(new Values(location)); } } QueryFunction的执行包含两个步骤。首先,Trident 会将读取的一些数据中汇总为一个 batch 传入 batchRetrieve 方法中。在这个例子中,batchRetrieve 方法会收到一些用户 id。然后 batchRetrieve 会返回一个与输入 tuple 列表大小相同的队列。结果队列的第一个元素与第一个输入 tuple 对应,第二个元素与第二个输入 tuple 相对应,以此类推。 你会发现这段代码并没有发挥出 Trident 批处理的优势,因为这段代码仅仅一次查询一下 LocationDB。所以,实现 LocationDB 的更好的方式应该是这样的: public class LocationDB implements State { public void beginCommit(Long txid) { } public void commit(Long txid) { } public void setLocationsBulk(List<Long> userIds, List<String> locations) { // set locations in bulk } public List<String> bulkGetLocations(List<Long> userIds) { // get locations in bulk } } 然后,你可以这样实现QueryLocation方法: public class QueryLocation extends BaseQueryFunction<LocationDB, String> { public List<String> batchRetrieve(LocationDB state, List<TridentTuple> inputs) { List<Long> userIds = new ArrayList<Long>(); for(TridentTuple input: inputs) { userIds.add(input.getLong(0)); } return state.bulkGetLocations(userIds); } public void execute(TridentTuple tuple, String location, TridentCollector collector) { collector.emit(new Values(location)); } } 这段代码大幅减少了域数据库的IO,具有更高的执行效率。 你需要使用StateUpdater接口来更新 state。下面是一个更新 LocationDB 的地址信息的 StateUpdater 实现: public class LocationUpdater extends BaseStateUpdater<LocationDB> { public void updateState(LocationDB state, List<TridentTuple> tuples, TridentCollector collector) { List<Long> ids = new ArrayList<Long>(); List<String> locations = new ArrayList<String>(); for(TridentTuple t: tuples) { ids.add(t.getLong(0)); locations.add(t.getString(1)); } state.setLocationsBulk(ids, locations); } } 然后你就可以在 Trident 拓扑中这样使用这个操作: TridentTopology topology = new TridentTopology(); TridentState locations = topology.newStream("locations", locationsSpout) .partitionPersist(new LocationDBFactory(), new Fields("userid", "location"), new LocationUpdater()) partitionPersist操作会更新 state 数据源。StateUpdater接收 State 和一批 tuple 作为输入,然后更新这个 State。上面的代码仅仅从输入 tuple 中抓取 userid 和 location 信息,然后对 State 执行一个批处理更新操作。 在 Trident 拓扑更新 LocationDB 之后,partitionPersist会返回一个表示更新后状态的TridentState对象。随后你就可以在拓扑的其他地方使用stateQuery方法对这个 state 执行查询操作。 你也许注意到了 StateUpdater 中有一个 TridentCollector 参数。发送到这个 collector 的 tuple 会进入一个“新的数值流”中。在这个例子里向这个新的流发送 tuple 并没有意义,不过如果你需要处理类似于更新数据库中的计数值这样的操作,你可以考虑将更新后的技术结果发送到这个流中。可以通过TridentState.newValuesStream方法来获取新的流的数据。 persistentAggregate Trident 使用一个称为persistentAggregate的方法来更新 State。你已经在前面的数据流单词统计的例子里见过了这个方法,这里再写一遍: TridentTopology topology = new TridentTopology(); TridentState wordCounts = topology.newStream("spout1", spout) .each(new Fields("sentence"), new Split(), new Fields("word")) .groupBy(new Fields("word")) .persistentAggregate(new MemoryMapState.Factory(), new Count(), new Fields("count")) partitionPersist 是一个接收 Trident 聚合器作为参数并对 state 数据源进行更新的方法,persistentAggregate 就是构建于 partitionPersist 上层的一个编程抽象。在这个例子里,由于是一个分组数据流(grouped stream),Trident 需要你提供一个实现MapState接口的 state。被分组的域就是 state 中的 key,而聚合的结果就是 state 中的 value。MapState接口是这样的: public interface MapState<T> extends State { List<T> multiGet(List<List<Object>> keys); List<T> multiUpdate(List<List<Object>> keys, List<ValueUpdater> updaters); void multiPut(List<List<Object>> keys, List<T> vals); } 而当你在非分组数据流上执行聚合操作时(全局聚合操作),Trident 需要你提供一个实现了Snapshottable接口的对象: public interface Snapshottable<T> extends State { T get(); T update(ValueUpdater updater); void set(T o); } MemoryMapState与MemcachedState都实现了上面两个接口。 实现 Map State 接口 实现MapState接口非常简单,Trident 几乎已经为你做好了所有的准备工作。OpaqueMap、TransactionalMap、与NonTransactionalMap类都分别实现了各自的容错性语义。你只需要为这些类提供一个用于对不同的 key/value 进行 multiGets 与 multiPuts 处理的 IBackingMap 实现类。IBackingMap接口是这样的: public interface IBackingMap<T> { List<T> multiGet(List<List<Object>> keys); void multiPut(List<List<Object>> keys, List<T> vals); } OpaqueMap 会使用OpaqueValue作为 vals 参数来调用 multiPut 方法,TransactionalMap 会使用TransactionalValue作为参数,而 NonTransactionalMap 则直接将拓扑中的对象传入。 Trident 也提供了一个CachedMap用于实现 K-V map 的自动 LRU 缓存功能。 最后,Trident 还提供了一个SnapshottableMap类,该类通过将全局聚合结果存入一个固定的 key 中的方法将 MapState 对象转化为一个 Snapshottable 对象。 可以参考MemcachedState的实现来了解如何将这些工具结合到一起来提供一个高性能的 MapState。MemcachedState支持选择模糊事务型、事务型或者非事务型语义。 转载自并发编程网 - ifeve.com

优秀的个人博客,低调大师

Apache Storm 官方文档 —— Trident Spouts

与一般的 Storm API 一样,spout 也是 Trident 拓扑的数据来源。不过,为了实现更复杂的功能服务,Trident Spout 在普通的 Storm Spout 之上另外提供了一些 API 接口。 数据源、数据流以及基于数据流更新 state(比如数据库)的操作,他们之间的耦合关系是不可避免的。Trident State一文中有这方面的详细解释,理解他们之间的这种联系对于理解 spout 的运作方式非常重要。 Trident 拓扑中的大部分 spout 都是非事务型 spout。在 Trident 拓扑中可以使用普通的IRichSpout接口来创建数据流: TridentTopology topology = new TridentTopology(); topology.newStream("myspoutid", new MyRichSpout()); Trident 拓扑中的所有 spout 都必须有一个唯一的标识,而且这个标识必须在整个 Storm 集群中都是唯一的。Trident 需要使用这个标识来存储 spout 从 ZooKeeper 中消费的元数据(metadata),包括 txid 以及其他相关的 spout 元数据。 你可以使用以下配置项来设置用于存储 spout 元数据的 ZooKeeper 地址(一般情况下不需要设置以下选项,因为 Storm 默认会直接使用集群的 ZooKeeper 服务器来存储数据 —— 译者注): transactional.zookeeper.servers:ZooKeeper 的服务器列表 transactional.zookeeper.port:ZooKeeper 集群的端口 transactional.zookeeper.root:元数据在 ZooKeeper 中存储的根目录。元数据会直接存储在该设置目录下。 管道 默认情况下,Trident 每次处理只一个 batch,知道该 batch 处理成功或者失败之后才会开始处理其他的 batch。你可以通过将 batch 管道化来提高吞吐率,降低每个 batch 的处理延时。同时处理的 batch 的最大数量可以通过topology.max.spout.pending来进行配置。 不过,即使在同时处理多个 batch 的情况下,Trident 也会按照 batch 的顺序来更新 state。例如,假如你正在处理一个将全局计数结果整合并更新到数据库中的任务,那么在你向数据库中更新 batch1 的计数结果时,你同时可以继续处理 batch2、batch3 甚至 batch10 的计数工作。不过,Trident 只会在 batch1 的 state 更新结束之后才会处理后续 batch 的 state 更新操作。这是实现恰好一次处理的语义的必要基础,我们已经在Trident State一文中讨论了这一点。 Trident spout 类型 下面列出了一些可用的 spout API 接口: ITridentSpout:这是最常用的 API,支持事务型和模糊事务型的语义实现。不过一般会根据需要使用它的某个已有的实现,而不是直接实现该接口。 IBatchSpout:非事务型 spout,每次会输出一个 batch 的 tuple。 IPartitionedTridentSpout:可以从分布式数据源(比如一个集群或者 Kafka 服务器)读取数据的事务型 spout。 OpaquePartitionedTridentSpout:可以从分布式数据源读取数据的模糊事务型 spout。 当然,正如这篇教程的开头提到的,除了这些 API 之外,你还可以使用普通的IRichSpout。 转载自并发编程网 - ifeve.com

优秀的个人博客,低调大师

Apache Storm 官方文档 —— 容错性

本文通过问答的形式解释了 Storm 的容错性原理。 工作进程(worker)死亡时会发生什么? 工作进程死亡的时候,supervisor 会重新启动这个进程。如果在启动过程中仍然一直失败,并且无法向 Nimbus 发送心跳,Nimbus 就会将这个 worker 重新分配到其他机器上去。 节点故障时会发生什么? 一个节点(集群中的工作节点,非 Nimbus 所在服务器)故障时,该节点上所有的任务(tasks)都会超时,然后 Nimbus 在检测到超时后会将所有的这些任务重新分配到其他机器上去。 Nimbus 或者 Supervisor 的后台进程挂掉时会发生什么 Nimbus 和 Supervisor 的后台进程本身是设计为快速失败(无论何时发生了异常情况之后都会启动自毁操作)和无状态(所有的状态由 ZooKeeper 负责管理)的。正如配置 Storm 集群这篇文章中所述,Nimbus 和 Supervisor 的后台进程实际上是在后台监控工具的监控之下运行的。所以,如果 Nimbus 或者 Supervisor 进程挂掉,他们就会静默地自动重启。 值得一提的是,Nimbus 和 Supervisor 的故障不会影响任何工作进程。这一点与 Hadoop 形成了鲜明对比,在 Hadoop 中 JobTracker 的故障会导致所有正在运行的 job 运行失败(Hadoop 2.x 中引入的 Yarn 架构已经提升了 Hadoop 系统的稳定性,目前的架构并不会如这里所说的那么不堪——译者注)。 Nimbus 是系统的单故障点1吗? 如果你的 Nimbus 节点出现故障无法访问,集群中的 worker 仍然会继续保持运行。另外,此时 Supervisor 也仍然会正常工作,在 worker 挂掉时自动重启挂掉的进程。但是,由于缺少 Nimbus 的协调,worker 就不会在必要的时候重新分配到不同的机器中(看上去好像你丢失了一个 worker)。 所以这个问题的答案是,Nimbus 确实稍微有一点像 SPOF(单故障点,Single Point of Failure)。不过在实际应用中,Nimbus 的故障从来就不是什么问题。未来的开发计划中还会考虑让 Nimbus 具备更好的可用性。 Storm 是如何保障消息的完全处理的? 对于节点故障或者消息丢失的情况,Storm 提供了一套完善的机制保障所有的消息都能够得到正确处理。消息的可靠性保障一文中解释了这方面更多的技术细节。 1单故障点是指在一个系统中的某个在失效或停止运转后会导致整个系统不能工作的部件,具体概念可以参考维基百科。 转载自并发编程网 - ifeve.com

资源下载

更多资源
Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册