Kuncle

God I pray to prosper thee.

All posts in one long list


Flink基于Chandy-Lamport的分布式快照算法

痛点

 当流式系统中有多个处理节点,并且多个处理节点需要保持自己的状态信息(比如处理节点每接受到一个消息,就需要根据消息更新自己的状态,如消息记数等),那处理节点应该如何保证 failure recovery 的时候,能自动恢复节点的状态,从而不会造成数据不一致问题?
undefined  比如上图,当所有的src不再发出消息,那么最终count-1和count-2的计数必须是一样的,且print是只增的。当count-1节点挂掉后重启,如何保证它和count-2一致并保证print-1和print-2最终会打出一样的数字呢?且保证print只增呢?  一种方法是所有节点记住自己所有发出的信息,失败重发,下游记住所有收到的消息和所有每个消息所导致的最新状态,failover之后要求所有上游从失败前的消息开始重发,且只能重发给失败的节点(也就是src-1和src-2要记住不能给count-2重发),且count-1要记住所有这些重发的消息不能导致print打出老的数字(否则违反只增性),而重发结束就要立刻开始给print发出应有的消息。当系统内节点非常多和复杂的时候,记录整个图的消息流动会非常复杂和导致high cost。

Chandy-Lamport 分布式快照算法

  • 背景
    • Global Snapshot  Global Snapshot 也就是Global State(全局状态),在系统做 Failure Recovery 的时候非常有用,是分布式计算系统中的一种容错处理理论基础。
    • 有向图  有向图是由一组顶点和一组有方向的边组成。 可以把分布式系统简化成是由限个进程和进程之间的 channel 组成的有向图:节点是进程,边是 channel(包含 input channel, output channel)。在分布式系统中,这些进程是运行在不同的物理机器上的。
    • 分布式快照  分布式系统的全局状态由进程的状态和 channel 中的 message 组成,分布式快照算法记录的就是这些进程的状态( local state 和它的 input channel 中有序的 message)。每个进程的状态可以认为是一个局部快照,全局快照是通过将所有的进程的局部快照合并起来得到。
  • 算法原理 Chandy-Lamport 完成一次分布式快照需要三个步骤
    1. Initiating a snapshot: 也就是开始创建 snapshot,可以由系统中的任意一个进程发起
      • 进程 Pi 发起,Pi记录自己的进程状态,同时生产一个标识信息 marker( 和进程通信的 message 不同)
      • 将 marker 信息通过 ouput channel 发送给系统里面的其他进程
      • Pi开始记录所有 input channel 接收到的 message
    2. Propagating a snapshot: 系统中其他进程开始逐个创建 snapshot 的过程
      • 进程 Pj 从 某个input channel 接收到 marker 信息后,如果 Pj 还没有记录自己的进程状态,则Pj 记录自己的进程状态,同时将该channel 状态记录为空,然后向 output channel 发送 marker 信息
      • 如果 Pj 已经记录好自己的进程状态,则Pj需要记录自己的其他 channel 在收到 marker 之前这些channel 中收到所有 message
      • 这里的 marker 可以理解为一个分隔符,进程 local snapshot (记录进程状态)前和后的 message。比如 Pj 做完 local snapshot 之后 一个Channel 中发送过来的 message 为 [a,b,c,marker,x,y,z] 那么 a, b, c 就是进程 Pj 做 local snapshot 前的数据,Pj 对于这部分数据需要记录下来。而 marker 后面 message x,y,x 正常处理掉就可以了。
    3. Terminating a snapshot: 算法结束条件
      • 所有的进程都收到 marker 信息并且记录下自己的状态和 channel 的状态(包含的 message)
  • 约束
    • Channel 是一个 FIFO 队列
    • Message 有序且无重复
  • 总结
    • 任何节点的snapshot由本地状态snapshot和节点的input channel snapshot组成
    • 任何src可以在任意时间决定take本地状态snapshot,take完本地snapshot,广播一个marker给所有下游
    • 任意没有take本地snapshot的节点(注意这个算法里src也是可以接受别人的msg的),假设从第x个channel收到第一个marker的时候,take本地状态snapshot(且take接受到第一个marker的input channel-x的channel snapshot记为空),然后给所有output channel广播这个marker
    • 从收到第一个marker并take完本地snapshot之后,记录所有input channel的msg到log里,直到从所有的input channel都收到这个marker. 作为这些input channel的channel snapshot。
  • 扩展
    • http://lamport.azurewebsites.net/pubs/chandy.pdf
    • http://lamport.azurewebsites.net/pubs/time-clocks.pdf

      Flink异步快照

       Flink解决Exactly Once Message处理的核心思想在Lightweight Asynchronous Snapshots这篇论文里。核心思想是在 input source 端插入 barrier 来替代 Chandy-Lamport 算法中的 Marker,通过控制 barrier 的同步来实现 snapshot 的备份和 exactly-once 语义。它的优点是不需要所有计算节点记住所有发出的信息。只需要数据源可以replay就行了,超级轻量级。

  • Flink无环单向图通信
    • 原理
      • 需要所有的通信channel是先进先出(FIFO)有序
      • 数据源SRC往Output Channel发Message的同时,会有一个中心Coordinator不断广播持续增长的stage barrier到所有的src数据流里。一般是固定时间间隔,比如每5s发送一次barrier。
      • 当数据源SRC收到第n个barrier的时候:
        • SRC需要保存状态。这样可以保证当需要从任意n位置Replay消息时,可以Replay在自己收到barrier-n之后和第n个barrier之前的所有消息
        • 将barrier广播给下游
      • 当中间处理节点或最终叶子节点在某个input流收到barrier-n的时候(如果有m个input流)
        • block这个input流保证不再收取和处理
        • 当所有的m个input都收到的barrier-n的时候, – 保存本地状态(take local-snapshot-n), 保证可以从这个状态恢复。 – 向自己的下游广播barrier-n (如果是叶子节点没有下游,那么不需要广播)
      • 当所有的节点(源,中间节点,叶子节点)都处理完barrier-n且完成取快照(take snapshot)的任务之后,就组成了barrier-n的全局快照。
    • Failover
      • 当集群任意节点挂掉,可以从最近的快照来重启整个系统;即,健康的节点rollback自己的状态到 接收到barrier-n时候 的状态快照。fail掉的节点的通过jobManager用自己的local-snapshot-n重置本地状态之后,才开始接收上游的消息。
      • 可以理解为当failover的时候,全部节点的状态都回退到了barrier-n之前的数据源message所导致的全网状态,就好像数据源在barrier-n之后根本没有发过消息一样。不断发出的barrier就好像逻辑时钟一样,然而“时间”流动到不同地方的速度不同,只有当一个时间“点”全部流动到了全网,且全网把这个时间“点”的状态全部取了快照(注意当网络很大,最后一个节点取完快照,初始节点可能已经前进到n+5,n+10了,但是由于最后一个节点才刚取完快照,CompleteGlobalSnapshot-n只到n,n是全局consistent的记录点)
      • 如果正常的节点的运算可以自动忽略老的已经处理过的消息(或者说replay导致的消息),那么我们只需要重启所有从源到fail掉的节点的这条线即可。
  • Flink有环单向图通信
    • 原理
      1. 当一个节点是环的Message流动的起点时(或者说这个节点正好同时是环的起点和终点),它必定有一个Input Channel是来自自己的下游节点。
      2. 这个节点不能像其他节点一样,等待所有的Input Channel的barrier到来,才take SnapShot且广播barrier,因为它有一个或多个Input Channel的消息是被自己往“下游”发的消息所引发的。如果它自己不向下游广播barrier,那么这些回环Input Channel永远也不会有barrier发来,那么算法会永久等待。
      3. 所以这个这个节点只需要等待所有非回环input channel的barrier到了,它就知道所有可能的barrier都到齐了,那么它就可以take本地SnapShot且往“下游”广播barrier了(从而造成barrier会通过回路再次抵达这个节点)
      4. 此节点take完本地SnapShot之后,需要记录所有回环Input Channel的Message到log里,直到从此回环input channel收到自己发出的barrier,当所有回环input channel都收到barrier-n. 此时在Step3 take的本地snapshot,加上所有回环input channel的msg log一起,成为此节点在barrier-n的本地snapshot。(对于环来说,可能遇到上一个event发给本节点的需要记录的”未来状态”还未到来,但是已经有”new event”(比如从src来的新消息)来改本地状态了,所以不能等待回环的消息,而必须先把本地状态take snapshot了才行,作为”上一个event导致的msg“,只能作为”未来event“记录在stream log里了,当然flink的算法也可以设计为,即使非回环input channel的barrier都到齐了,也不unblock input channel ,而是等待所有的回环input channel的barrier也都到齐了,才take本地snapshot,且一起unblock所有的input channel;这样就不需要维护stream log了。但性能很低)
      5. Failover,failover的时候,除了从本地snapshot恢复状态之外,还需要replay所有input channel的msg。

Flink容错

Fault Tolerance 概念

容错(Fault Tolerance) 是指容忍故障,在故障发生时能够自动检测出来并使系统能够自动回复正常运行。当出现某些指定的网络故障、硬件故障、软件错误时,系统仍能执行规定的一组程序,或者说程序不会因系统中的故障而中止,并且执行结果也不包含系统中故障所引起的差错。

传统数据库Fault Tolerance

我们在 Flink流表对偶(duality)性 中介绍过mysql的主备复制时候提到了binlog,binlog是一个append only的日志文件,Mysql的主备复制是高可用的主要方式,binlog是主备复制的核心手段(当然mysql高可用细节也很复杂和多种不同的优化点,如 纯异步复制优化为半同步和同步复制以保证异步复制binlog导致的master和slave的同步时候网络坏掉,导致主备不一致问题等)。Mysql主备复制,是容错机制的一部分,在容错机制之中也包括事物控制,在传统数据库中事物可以设置不同的级别,以保证数据不同的质量,级别由低到高 如下:

  • Read uncommitted - 读未提交,就是一个事务可以读取另一个未提交事务的数据。那么这中事物控制成本最低,但是会导致另一个事物读都时候脏数据,那么怎么解决读脏数据呢?利用Read committed 级别…
  • Read committed - 读提交,就是一个事务要等另一个事务提交后才能读取数据。这种级别可以解决读脏数据的问题,那么这种级别有什么问题呢?这个级别还有一个 不能重复读的问题,即:开启一个读事物时候T1,先读取字段F1值是V1,这时候另一个事物T2可以UPDATA这个字段值V2,导致T1再次读取字段值时候获得V2了,同一个事物中的两次读取不一致了。那么如何解决不可重复读的问题呢?利用 Repeatable read 级别…
  • Repeatable read - 重复读,就是在开始读取数据(事务开启)时,不再允许修改操作。重复读模式要有事物顺序的等待,需要一定的成本达到高质量的数据信息,那么重复读还会有什么问题吗?是的,重复读级别还有一个问题就是 幻读,幻读产生的原因是INSERT,那么幻读怎么解决呢?利用Serializable级别…
  • Serializable - 序列化 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。 主备复制,事物控制都是传统数据库容错的机制。

流计算Fault Tolerance的挑战

流计算Fault Tolerance的一个很大的挑战是低延迟,很多Blink任务都是7 x 24小时不间断,端到端的秒级延迟,要想在遇上网络闪断,机器坏掉等非预期的问题时候快速恢复正常,并且不影响计算结果正确性是一件极其困难的事情。同时除了流计算的低延时要求,还有计算模式上面的挑战,在Blink中支持exactly-once和at-least-once两种计算模式,如何做到在failover时候不重复计算精准的做到exactly-once也是流计算Fault Tolerance要重点解决的问题。

Blink Fault Tolerance机制上面理论基础与flink一致都是持续创建分布式数据流及其状态的快照。这些快照在系统遇到故障时,作为一个回退点。Blink中创建快照的机制叫做Checkpointing,Checkpointing的理论基础 Stephan 在 Lightweight Asynchronous Snapshots for Distributed Dataflows 进行了细节描述,该机制源于有K. MANI CHANDY和LESLIE LAMPORT 发表的 Determining-Global-States-of-a-Distributed-System Paper,该Paper描述了在分布式系统如何解决全局状态一致性问题。我想该算法的剖析我们应该单独写一篇进行介绍。 在Blink中以checkpointing的机制进行容错,checkpointing会产生类似binlog一样的可以用来恢复的任务状态数据。Blink中也有类似于数据库事物控制(4个级别)一样的数据计算语义控制,在Blink中有另种语义模式设置,花费的成本有低到高,如下:

  • at-least-once
  • exactly-once

检查点-Checkpointing

上面我们说Checkpointing是Blink中Fault Tolerance的核心机制,我们以Checkpointing的方式创建包含timer,connector,window,user-defined state 等stateful Operator的快照。在Determining-Global-States-of-a-Distributed-System的全局状态一致性算法中重点描述了全局状态的对齐问题,在Lightweight Asynchronous Snapshots for Distributed Dataflows中核心描述了对齐的方式,在flink中采用以在流信息中插入barrier的方式完成DAG中异步快照。 如下图(from Lightweight Asynchronous Snapshots for Distributed Dataflows)描述了Asynchronous barrier snapshots for acyclic graphs。也是Blink中采用的方式。 map_shuffle 上图描述的是一个面描述 增量计算 word count的Job,上图核心说明了如下几点:

  • barrier 由source节点发出;
  • barrier会将流上event切分到不同的checkpoint中;
  • 汇聚到当前节点的多流的barrier要对齐;
  • barrier对齐之后会进行Checkpointing,生成snapshot;
  • 完成snapshot之后向下游发出barrier,继续直到Sink节点; 这样在整个流上面以barrier方式进行Checkpointing,随着时间的推移,整个流的计算过程中按时间顺序不断的进行Checkpointing,如下图: map_shuffle 生成的snapshot会存储到StateBackend中,相关State的介绍可以查阅 Flink State 。这样在进行failover时候,从最后一次成功的checkpoint进行恢复;

Checkpointing的控制

上面我们了解到整个流上面我们会随这时间推移不断的做Checkpointing,不断的产生snapshot存储到Statebackend中,那么多久进行一次Checkpointing?对产生的snapshot如何持久化的呢?带着这些疑问,我们看看Flink对于Checkpointing如何控制的?有哪些可配置的参数:(这些参数都在 CheckpointCoordinator 中进行定义)

  • checkpointMode - 检查点模式,对应作业参数flink.checkpoint.mode,我们有AT_LEAST_ONCE 或 EXACTLY_ONCE。
  • checkpointInterval - 检查点时间间隔,对应作业参数 flink.checkpoint.interval.ms,单位是毫秒。
  • checkpointTimeout - 检查点超时时间,对应作业参数 flink.checkpoint.timeout.ms, 单位毫秒。
  • 其他Flink一些默认配置;
  • 比如 externalize - 默认true,表示是否将checkpoint存储到外部存储,这样即使job被cancel掉,checkpoint信息也不会删除,当恢复job时候可以利用checkpoint进行状态恢复。Blink内部默认使用ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION模式,也就是对应了Job界面上面的 恢复 操作时候,可以复用state。

如何做到exactly-once

上面内容我们了解了Flink中exactly-once和at-least-once只是在进行checkpointing时候的配置模式,两种模式下进行checkpointing的原理是一致的,那么在实现上有什么本质区别呢?

语义
  • at-least-once - 语义是流上所有数据至少被处理过一次(不要丢数据)
  • exactly-once - 语义是流上所有数据必须被处理且只能处理一次(不丢数据,且不能重复) 从语义上面exactly-once 比 at-least-once对数据处理的要求跟严格,更精准,那么更高的要求就意味着更高的代价,这里的代价就是 延迟。(下面实现部分会介绍为啥有 延时 的代价)
实现

那在实现上面Flink中at-least-once 和 exactly-once有什么区别呢?区别体现在多路输入的时候(比如 Join),当所有输入的barrier没有完全到来的时候,早到来的event在exactly-once的情况向会进行缓存(不进行处理),而at-least-once的模式下即使所有输入的barrier没有完全到来的时候,早到来的event也会进行处理。也就是说对于at-least-once模式下,对于下游节点而言,本来数据属于checkpoint n的数据在checkpoint n-1里面也可能处理过了。 我以exactly-once为例说明exactly-once模式相对于at-least-once模式为啥会有更高的延时?如下图: map_shuffle 上图示意了某个节点进行Checkpointing的过程:

  • 当Operator接收到某个上游发下来的第barrier时候开始进行barrier的对齐阶段;
  • 在进行对齐期间早的的input的数据会被缓存到buffer中;
  • 当Operator接收到上游所有barrier的时候,当前Operator会进行checkpointing,生成snapshot并持久化;
  • 当完checkpointing时候将barrier广播给下游operator; 当多路输入的barrier没有没有对齐时候,barrier先到的输入数据会缓存在buffer中,不进行处理,这样对于下游而言buffer的数据越多就有更大的延迟。这个延时带来的好处就是相邻checkpointing所记录的数据(计算结果或event)没有重复。相对at-least-once模式数据不会被buffer,减少延时的利好是以容忍数据重复计算为代价的。 在Flink的代码实现上用CheckpointBarrierHandler类处理barrier,其核心接口是:
    public interface CheckpointBarrierHandler {
       ...
       //返回operator消费的下一个BufferOrEvent。这个调用会导致阻塞直到获取到下一个BufferOrEvent
       BufferOrEvent getNextNonBlocked() throws Exception;
       ...
    }
    

    其中BufferOrEvent,可能是正常的data event,也可能是特殊的event,比如barrier event。对应at-least-once和exactly-once有两种不同的实现,具体如下:

  • BarrierBuffer - 处理exactly-once模式; BarrierBuffer用于提供exactly-once一致性保证,其行为是:它将以barrier阻塞输入直到所有的输入都接收到基于某个检查点的barrier,也就是上面所说的对齐。为了避免backpress输入流,BarrierBuffer将从被阻塞的channel中持续地接收buffer并在内部存储它们,直到阻塞被解除。 BarrierBuffer 实现了CheckpointBarrierHandler的getNextNonBlocked, 该方法用于获取待处理的下一条记录。该方法时阻塞调用,直到获取到下一个记录。其中这里的记录包括两种,一种是来自于上游未被标记为blocked的输入,比如上图中的 event(a),;另一种是,从已blocked输入中缓冲区队列中被释放的记录,比如上图中的event(1,2,3,4)。

  • BarrierTracker - 处理at-least-once模式; BarrierTracker会对各个输入接收到的检查点的barrier进行跟踪。一旦它观察到某个检查点的所有barrier都已经到达,它将会通知监听器检查点已完成,以触发相应地回调处理。不像BarrierBuffer,BarrierTracker不阻塞已经发送了barrier的输入,也就说明不采用对齐机制,因此本检查点的数据会及时被处理,并且因此下一个检查点的数据可能会在该检查点还没有完成时就已经到来。这样在恢复时只能提供AT_LEAST_ONCE保证。 BarrierTracker也实现了CheckpointBarrierHandler的getNextNonBlocked, 该方法用于获取待处理的下一条记录。与BarrierBuffer相比它实现很简单,只是阻塞的冲输入中获取要处理的event。 如上两个CheckpointBarrierHandler实现的核心区别是BarrierBuffer会维护多路输入是否要blocked,缓存被blocked的输入的record。所谓有得必有失,有失必有得,舍得舍得在这里也略有体现哈 :)。
完整Blink任务Checkpointing过程

在 Flink State 中我们有过对Blink存储到State中的内容做过介绍,比如在connector会利用OperatorState记录读取位置offset,那么一个完整的Blink任务的执行图是一个DAG,上面我们描述了DAG中一个节点的过程,那么整体来看Checkpointing的过程是怎样的呢?在产生checkpoint并分布式持久到HDFS的过程是怎样的呢?

整体checkpoint流程

map_shuffle 上图我们看到一个完整的Blink任务进行Checkpointing的过程,JM触发Soruce发射barriers,当某个Operator接收到上游发下来的barrier,开始进行barrier的处理,整体根据DAG自上而下的逐个节点进行Checkpointing,并持久化到Statebackend。一直到DAG的sink节点。

Incremental checkpoint

对于一个流计算的任务,数据会源源不断的流入,比如要进行双流join(Flink双流Join 篇会详细介绍),由于两边的流event的到来有先后顺序问题,我们必须将left和right的数据都会在state中进行存储,Left event流入会在Right的State进行join数据,Right event流入会在LState中join数据,如下图左右两边的数据都会持久化到State中: map_shuffle 由于流上数据源源不断,随着时间的增加,每次checkpoint产生的snapshot的文件(RocksDB的sst文件)会变的非常庞大,增加网络IO,拉长checkpoint时间,最终导无法完成checkpoint,Flink失去failover的能力。为了解决checkpoint不断变大的问题,Flink内部实现了Incremental checkpoint,这种增量进行checkpoint的机制,会大大减少checkpoint时间,并且如果业务数据稳定的情况下每次checkpoint的时间是相对稳定的,根据不同的业务需求设定checkpoint的interval,稳定快速的进行checkpointing,保障Blink任务在遇到故障时候可以顺利的进行failover。Incremental checkpoint的优化对于Flink成百上千的任务节点带来的利好不言而喻。

端到端exactly-once

根据上面的介绍我们知道Flink内部支持exactly-once,要想达到端到端(Soruce到Sink)的exactly-once,需要Flink外部Soruce和Sink的支持,比如Source要支持精准的offset,Sink要支持两阶段提交,也就是继承TwoPhaseCommitSinkFunction。不过,目前没有真正意义上的exactly-once,后面会开篇文章单独讲。

Flink双流Join

JOIN概念

JOIN的本质是分别从N(N>=1)张表中获取不同的字段,进而得到最完整的记录行。比如我们有一个查询需求:在学生表(学号,姓名,性别),课程表(课程号,课程名,学分)和成绩表(学号,课程号,分数)中查询所有学生的姓名,课程名和考试分数。如下: map_shuffle

为啥需要JOIN

JOIN的本质是数据拼接,那么如果我们将所有数据列存储在一张大表中,是不是就不需要JOIN了呢?如果真的能将所需的数据都在一张表存储,我想就真的不需要JOIN的算子了,但现实业务中真的能做到将所需数据放到同一张大表里面吗?答案是否定的,核心原因有2个:

  • 产生数据的源头可能不是一个系统;
  • 产生数据的源头是同一个系统,但是数据冗余的沉重代价,迫使我们会遵循数据库范式,进行表的设计。简说NF如下:
    • 1NF - 列不可再分
    • 2NF - 符合1NF,并且非主键属性全部依赖于主键属性
    • 3NF - 符合2NF,并且传递依赖,即:即任何字段不能由其他字段派生出来
    • BCNF - 符合3NF,并且主键属性之间无依赖关系

      JOIN的种类

  • CROSS JOIN - 交叉连接,计算笛卡儿积
  • INNER JOIN - 内连接,返回满足条件的记录
  • LEFT - 返回左表所有行,右表不存在补NULL;
  • RIGHT - 返回右表所有行,左边不存在补NULL;
  • FULL - 返回左表和右表的并集,不存在一边补NULL;
  • SELF JOIN - 自连接,将表查询时候命名不同的别名;

    JOIN语法

    JOIN 在SQL89和SQL92中有不同的语法,以INNER JOIN为例说明:

  • SQL89 - 表之间用“,”逗号分割,链接条件和过滤条件都在Where子句指定
    SELECT 
    a.colA, 
    b.colA
    FROM  
    tab1 AS a , tab2 AS b
    WHERE a.id = b.id and a.other > b.other
    
  • SQL92
    SELECT 
    a.colA, 
    b.colA
    FROM 
    tab1 AS a JOIN tab2 AS b ON a.id = b.id
    WHERE 
    a.other > b.other
    

    SQL92将链接条件在ON子句指定,过滤条件在WHERE子句指定,逻辑更为清晰,本篇中的后续示例将应用SQL92语法进行SQL的编写。

    tableExpression [ LEFT|RIGHT|FULL|INNER|SELF ] JOIN tableExpression [ ON joinCondition ] [WHERE filterCondition]
    

    语义示例说明

    我们以开篇示例中的三张表学生表(学号,姓名,性别),课程表(课程号,课程名,学分)和成绩表(学号,课程号,分数)来介绍各种JOIN的语义。 map_shuffle

    CROSS JOIN

    交叉连接会对两个表进行笛卡尔积,也就是LEFT表的每一行和RIGHT表的所有行进行联接,因此生成结果表的行数是两个表行数的乘积,如student和course表的CROSS JOIN结果如下:

    mysql> SELECT * FROM student JOIN course;
    +------+-------+------+-----+-------+--------+
    | no   | name  | sex  | no  | name  | credit |
    +------+-------+------+-----+-------+--------+
    | S001 | Sunny | M    | C01 | Java  |      2 |
    | S002 | Tom   | F    | C01 | Java  |      2 |
    | S003 | Kevin | M    | C01 | Java  |      2 |
    | S001 | Sunny | M    | C02 | Blink |      3 |
    | S002 | Tom   | F    | C02 | Blink |      3 |
    | S003 | Kevin | M    | C02 | Blink |      3 |
    | S001 | Sunny | M    | C03 | Spark |      3 |
    | S002 | Tom   | F    | C03 | Spark |      3 |
    | S003 | Kevin | M    | C03 | Spark |      3 |
    +------+-------+------+-----+-------+--------+
    9 rows in set (0.00 sec)
    

    如上结果我们得到9行=student(3) x course(3)。交叉联接一般会消耗较大的资源,也被很多用户质疑交叉联接存在的意义?(任何时候我们都有质疑的权利,同时也建议我们养成自己质疑自己“质疑”的习惯,就像小时候不理解父母的“废话”一样)。我们以开篇的示例说明交叉联接的巧妙之一。 开篇中我们的查询需求是:在学生表(学号,姓名,性别),课程表(课程号,课程名,学分)和成绩表(学号,课程号,分数)中查询所有学生的姓名,课程名和考试分数。开篇中的SQL语句得到的结果如下:

    mysql> SELECT 
      ->   student.name, course.name, score 
      -> FROM student JOIN  score ON student.no = score.s_no 
      ->              JOIN course ON score.c_no = course.no;
    +-------+-------+-------+
    | name  | name  | score |
    +-------+-------+-------+
    | Sunny | Java  |    80 |
    | Sunny | Blink |    98 |
    | Sunny | Spark |    76 |
    | Kevin | Java  |    78 |
    | Kevin | Blink |    88 |
    | Kevin | Spark |    68 |
    +-------+-------+-------+
    6 rows in set (0.00 sec)
    

    如上INNER JOIN的结果我们发现少了Tom同学的成绩,原因是Tom同学没有参加考试,在score表中没有Tom的成绩,但是我们可能希望虽然Tom没有参加考试但仍然希望Tom的成绩能够在查询结果中显示(成绩 0 分),面对这样的需求,我们怎么处理呢?交叉联接可以帮助我们:

  • 第一步 student和course 进行交叉联接:
    mysql> SELECT 
      ->   stu.no, c.no, stu.name, c.name
      -> FROM student stu JOIN course c  笛卡尔积
      -> ORDER BY stu.no; -- 排序只是方便大家查看:)
    +------+-----+-------+-------+
    | no   | no  | name  | name  |
    +------+-----+-------+-------+
    | S001 | C03 | Sunny | Spark |
    | S001 | C01 | Sunny | Java  |
    | S001 | C02 | Sunny | Blink |
    | S002 | C03 | Tom   | Spark |
    | S002 | C01 | Tom   | Java  |
    | S002 | C02 | Tom   | Blink |
    | S003 | C02 | Kevin | Blink |
    | S003 | C03 | Kevin | Spark |
    | S003 | C01 | Kevin | Java  |
    +------+-----+-------+-------+
    9 rows in set (0.00 sec)
    
  • 第二步 将交叉联接的结果与score表进行左外联接,如下:
    mysql> SELECT 
      ->   stu.no, c.no, stu.name, c.name,
      ->    CASE 
      ->     WHEN s.score IS NULL THEN 0
      ->     ELSE s.score
      ->   END AS score 
      -> FROM student stu JOIN course c  -- 迪卡尔积
      -> LEFT JOIN score s ON stu.no = s.s_no and c.no = s.c_no -- LEFT OUTER JOIN
      -> ORDER BY stu.no; -- 排序只是为了大家好看一点:)
    +------+-----+-------+-------+-------+
    | no   | no  | name  | name  | score |
    +------+-----+-------+-------+-------+
    | S001 | C03 | Sunny | Spark |    76 |
    | S001 | C01 | Sunny | Java  |    80 |
    | S001 | C02 | Sunny | Blink |    98 |
    | S002 | C02 | Tom   | Blink |     0 | -- TOM 虽然没有参加考试,但是仍然看到他的信息
    | S002 | C03 | Tom   | Spark |     0 |
    | S002 | C01 | Tom   | Java  |     0 |
    | S003 | C02 | Kevin | Blink |    88 |
    | S003 | C03 | Kevin | Spark |    68 |
    | S003 | C01 | Kevin | Java  |    78 |
    +------+-----+-------+-------+-------+
    9 rows in set (0.00 sec)
    
    INNER JOIN

    内联接在SQL92中 ON 表示联接添加,可选的WHERE子句表示过滤条件,如开篇的示例就是一个多表的内联接,我们在看一个简单的示例: 查询成绩大于80分的学生学号,学生姓名和成绩:

    mysql> SELECT 
      ->   stu.no, stu.name , s.score
      -> FROM student stu JOIN score s ON  stu.no = s.s_no 
      -> WHERE s.score > 80;
    +------+-------+-------+
    | no   | name  | score |
    +------+-------+-------+
    | S001 | Sunny |    98 |
    | S003 | Kevin |    88 |
    +------+-------+-------+
    2 rows in set (0.00 sec)
    

    上面按语义的逻辑是:

  • 第一步:先进行student和score的内连接,如下:
    mysql> SELECT 
      ->   stu.no, stu.name , s.score
      -> FROM student stu JOIN score s ON  stu.no = s.s_no ;
    +------+-------+-------+
    | no   | name  | score |
    +------+-------+-------+
    | S001 | Sunny |    80 |
    | S001 | Sunny |    98 |
    | S001 | Sunny |    76 |
    | S003 | Kevin |    78 |
    | S003 | Kevin |    88 |
    | S003 | Kevin |    68 |
    +------+-------+-------+
    6 rows in set (0.00 sec)
    
  • 第二步:对内联结果进行过滤, score > 80 得到,如下最终结果:
    -> WHERE s.score > 80;
    +------+-------+-------+
    | no   | name  | score |
    +------+-------+-------+
    | S001 | Sunny |    98 |
    | S003 | Kevin |    88 |
    +------+-------+-------+
    2 rows in set (0.00 sec)
    

    上面的查询过程符合语义,但是如果在filter条件能过滤很多数据的时候,先进行数据的过滤,在进行内联接会获取更好的性能,比如我们手工写一下:

    mysql> SELECT 
     ->   no, name , score
     -> FROM student stu JOIN ( SELECT s_no, score FROM score s WHERE s.score >80) as sc ON no = s_no;
    +------+-------+-------+
    | no   | name  | score |
    +------+-------+-------+
    | S001 | Sunny |    98 |
    | S003 | Kevin |    88 |
    +------+-------+-------+
    2 rows in set (0.00 sec)
    

    上面写法语义和第一种写法语义一致,得到相同的查询结果,上面查询过程是:

  • 第一步:执行过滤子查询
    mysql> SELECT s_no, score FROM score s WHERE s.score >80;
    +------+-------+
    | s_no | score |
    +------+-------+
    | S001 |    98 |
    | S003 |    88 |
    +------+-------+
    2 rows in set (0.00 sec)
    
  • 第二步:执行内连接
    -> ON no = s_no;
    +------+-------+-------+
    | no   | name  | score |
    +------+-------+-------+
    | S001 | Sunny |    98 |
    | S003 | Kevin |    88 |
    +------+-------+-------+
    2 rows in set (0.00 sec)
    

    如上两种写法在语义上一致,但在查询性能在数量很大的情况下会有很大差距。上面为了和大家演示相同的查询语义,可以有不同的查询方式,不同的执行计划。实际上数据库本身的优化器会我们做查询优化,在内联接中ON的联接条件和WHERE的过滤条件具有相同的优先级,具体的执行顺序可以由数据库的优化器根据性能消耗决定。也就是说物理执行计划可以先执行过滤条件进行查询优化,如果细心的读者可能发现,在第二个写法中,子查询我们不但有行的过滤,也进行了列的裁剪(去除了对查询结果没有用的c_no列),这两个变化实际上对应了数据库中两个优化规则:

  • filter push down
  • project push down

    LEFT OUTER JOIN

    左外联接语义是返回左表所有行,右表不存在补NULL,为了演示作用,我们查询没有参加考试的所有学生的成绩单:

    mysql> SELECT 
      ->   no, name , s.c_no, s.score
      -> FROM student stu LEFT JOIN score s ON stu.no = s.s_no
      -> WHERE s.score is NULL;
    +------+------+------+-------+
    | no   | name | c_no | score |
    +------+------+------+-------+
    | S002 | Tom  | NULL |  NULL |
    +------+------+------+-------+
    1 row in set (0.00 sec)
    

    上面查询的执行逻辑上也是分成两步:

  • 第一步:左外联接查询
    mysql> SELECT 
      ->   no, name , s.c_no, s.score
      -> FROM student stu LEFT JOIN score s ON stu.no = s.s_no;
    +------+-------+------+-------+
    | no   | name  | c_no | score |
    +------+-------+------+-------+
    | S001 | Sunny | C01  |    80 |
    | S001 | Sunny | C02  |    98 |
    | S001 | Sunny | C03  |    76 |
    | S002 | Tom   | NULL |  NULL | -- 右表不存在的补NULL
    | S003 | Kevin | C01  |    78 |
    | S003 | Kevin | C02  |    88 |
    | S003 | Kevin | C03  |    68 |
    +------+-------+------+-------+
    7 rows in set (0.00 sec)
    
  • 第二步:过滤查询
    mysql> SELECT 
      ->   no, name , s.c_no, s.score
      -> FROM student stu LEFT JOIN score s ON stu.no = s.s_no
      -> WHERE s.score is NULL;
    +------+------+------+-------+
    | no   | name | c_no | score |
    +------+------+------+-------+
    | S002 | Tom  | NULL |  NULL |
    +------+------+------+-------+
    1 row in set (0.00 sec)
    

    这个两个过程和上面分析的INNER JOIN一样,但是这时候能否利用上面说的 filter push down的优化呢?根据LEFT OUTER JOIN的语义来讲,答案是否定的。我们手工操作看一下:

  • 第一步:先进行过滤查询(获得一个空表)
    mysql> SELECT * FROM score s WHERE s.score is NULL;
    Empty set (0.00 sec)
    
  • 第二步: 进行左外链接
    mysql> SELECT 
      ->   no, name , s.c_no, s.score
      -> FROM student stu LEFT JOIN (SELECT * FROM score s WHERE s.score is NULL) AS s ON stu.no = s.s_no;
    +------+-------+------+-------+
    | no   | name  | c_no | score |
    +------+-------+------+-------+
    | S001 | Sunny | NULL |  NULL |
    | S002 | Tom   | NULL |  NULL |
    | S003 | Kevin | NULL |  NULL |
    +------+-------+------+-------+
    3 rows in set (0.00 sec)
    

    我们发现两种写法的结果不一致,第一种写法只返回Tom没有参加考试,是我们预期的。第二种写法返回了Sunny,Tom和Kevin三名同学都没有参加考试,这明显是非预期的查询结果。所有LEFT OUTER JOIN不能利用INNER JOIN的 filter push down优化。

RIGHT OUTER JOIN

右外链接语义是返回右表所有行,左边不存在补NULL,如下:

mysql> SELECT 
    ->   s.c_no, s.score, no, name
    -> FROM score s RIGHT JOIN student stu ON stu.no = s.s_no;
+------+-------+------+-------+
| c_no | score | no   | name  |
+------+-------+------+-------+
| C01  |    80 | S001 | Sunny |
| C02  |    98 | S001 | Sunny |
| C03  |    76 | S001 | Sunny |
| NULL |  NULL | S002 | Tom   | -- 左边没有的进行补 NULL
| C01  |    78 | S003 | Kevin |
| C02  |    88 | S003 | Kevin |
| C03  |    68 | S003 | Kevin |
+------+-------+------+-------+
7 rows in set (0.00 sec)

上面右外链接我只是将上面左外链接查询的左右表交换了一下

FULL OUTER JOIN

全外链接语义返回左表和右表的并集,不存在一边补NULL,用于演示的MySql数据库不支持FULL OUTER JOIN。这里不做演示了。

SELF JOIN

上面介绍的INNER JOIN、OUTER JOIN都是不同表之间的联接查询,自联接是一张表以不同的别名做为左右两个表,可以进行如上的INNER JOIN和OUTER JOIN. 如下看一个INNER 自联接:

mysql> SELECT * FROM student l JOIN student r where l.no = r.no;
+------+-------+------+------+-------+------+
| no   | name  | sex  | no   | name  | sex  |
+------+-------+------+------+-------+------+
| S001 | Sunny | M    | S001 | Sunny | M    |
| S002 | Tom   | F    | S002 | Tom   | F    |
| S003 | Kevin | M    | S003 | Kevin | M    |
+------+-------+------+------+-------+------+
3 rows in set (0.00 sec)
不等值联接

这里说的不等值联接是SQL92语法里面的ON子句里面只有等值联接,比如:

mysql> SELECT 
    ->   s.c_no, s.score, no, name
    -> FROM score s RIGHT JOIN student stu ON stu.no != s.c_no;
+------+-------+------+-------+
| c_no | score | no   | name  |
+------+-------+------+-------+
| C01  |    80 | S001 | Sunny |
| C01  |    80 | S002 | Tom   |
| C01  |    80 | S003 | Kevin |
| C02  |    98 | S001 | Sunny |
| C02  |    98 | S002 | Tom   |
| C02  |    98 | S003 | Kevin |
| C03  |    76 | S001 | Sunny |
| C03  |    76 | S002 | Tom   |
| C03  |    76 | S003 | Kevin |
| C01  |    78 | S001 | Sunny |
| C01  |    78 | S002 | Tom   |
| C01  |    78 | S003 | Kevin |
| C02  |    88 | S001 | Sunny |
| C02  |    88 | S002 | Tom   |
| C02  |    88 | S003 | Kevin |
| C03  |    68 | S001 | Sunny |
| C03  |    68 | S002 | Tom   |
| C03  |    68 | S003 | Kevin |
+------+-------+------+-------+
18 rows in set (0.00 sec)

上面这示例,其实没有什么实际业务价值,在实际的使用场景中,不等值联接往往是结合等值联接,将不等值条件在WHERE子句指定,即, 带有WHERE子句的等值联接。

Blink双流JOIN的支持

|| CROSS JOIN | INNER JOIN | OUTER JOIN | SELF JOIN | ON(condition) | WHERE | |—|—|—|—|—|—|—| |Blink| N | Y | Y | Y | 必选 | 可选 | Blink目前支持INNER JOIN和LEFT OUTER JOIN(SELF 可以转换为普通的INNER和OUTER)。在语义上面Blink严格遵守标准SQL的语义,与上面演示的语义一致。下面我重点介绍Blink中JOIN的实现原理。

双流JOIN与传统数据库表JOIN的区别

传统数据库表的JOIN是静态两张静态表的数据联接,在流上面是 动态表(关于流与动态表的关系请查阅 Flink流表对偶(duality)性) 的JOIN,双流JOIN的数据不断流入与传统数据库表的JOIN有如下3个核心区别:

  • 左右两边的数据集合无穷 - 传统数据库左右两个表的数据集合是有限的,双流JOIN的数据会源源不断的流入;
  • JOIN的结果不断产生/更新 - 传统数据库表JOIN是一次执行产生最终结果后退出,双流JOIN会持续不断的产生新的结果。在Flink持续查询(Continuous Queries) 也有相关介绍。
  • 查询计算的双边驱动 - 双流JOIN由于左右两边的流的速度不一样,会导致左边数据到来的时候右边数据还没有到来,或者右边数据到来的时候左边数据没有到来,所以在实现中要将左右两边的流数据进行保存,以保证JOIN的语义。在Blink中会以State的方式进行数据的存储。State相关请查看 Flink State 。
数据Shuffle

分布式流计算所有数据会进行Shuffle,怎么才能保障左右两边流的要JOIN的数据会在相同的节点进行处理呢?在双流JOIN的场景,我们会利用JOIN中ON的联接key进行partition,确保两个流相同的联接key会在同一个节点处理。

数据的保存

不论是INNER JOIN还是OUTER JOIN 都需要对左右两边的流的数据进行保存,JOIN算子会开辟左右两个State进行数据存储,左右两边的数据到来时候,进行如下操作:

  • LeftEvent到来存储到LState,RightEvent到来的时候存储到RState;
  • LeftEvent会去RightState进行JOIN,并发出所有JOIN之后的Event到下游;
  • RightEvent会去LeftState进行JOIN,并发出所有JOIN之后的Event到下游 map_shuffle

简单场景介绍实现原理

INNER JOIN 实现

JOIN有很多复杂的场景,我们先以最简单的场景进行实现原理的介绍,比如:最直接的两个进行INNER JOIN,比如查询产品库存和订单数量,库存变化事件流和订单事件流进行INNER JOIN,JION条件是产品ID,具体如下: map_shuffle 双流JOIN两边事件都会存储到State里面,如上,事件流按照标号先后流入到join节点,我们假设右边流比较快,先流入了3个事件,3个事件会存储到state中,但因为左边还没有数据,所有右边前3个事件流入时候,没有join结果流出,当左边第一个事件序号为4的流入时候,先存储左边state,再与右边已经流入的3个事件进行join,join的结果如图 三行结果会流入到下游节点sink。当第5号事件流入时候,也会和左边第4号事件进行join,流出一条jion结果到下游节点。这里关于INNER JOIN的语义和大家强调两点:

  • INNER JOIN只有符合JOIN条件时候才会有JOIN结果流出到下游,比如右边最先来的1,2,3个事件,流入时候没有任何输出,因为左边还没有可以JOIN的事件;
  • INNER JOIN两边的数据不论如何乱序,都能够保证和传统数据库语义一致,因为我们保存了左右两个流的所有事件到state中。
LEFT OUTER JOIN 实现

LEFT OUTER JOIN 可以简写 LEFT JOIN,语义上和INNER JOIN的区别是不论右流是否有JOIN的事件,左流的事件都需要流入下游节点,但右流没有可以JION的事件时候,右边的事件补NULL。同样我们以最简单的场景说明LEFT JOIN的实现,比如查询产品库存和订单数量,库存变化事件流和订单事件流进行LEFT JOIN,JION条件是产品ID,具体如下: map_shuffle 下图也是表达LEFT JOIN的语义,只是展现方式不同: map_shuffle 上图主要关注点是当左边先流入1,2事件时候,右边没有可以join的事件时候会向下游发送左边事件并补NULL向下游发出,当右边第一个相同的Join key到来的时候会将左边先来的事件发出的带有NULL的事件撤回(对应上面command的-记录,+代表正向记录,-代表撤回记录)。这里强调三点:

  • 左流的事件当右边没有JOIN的事件时候,将右边事件列补NULL后流向下游;
  • 当右边事件流入发现左边已经有可以JOIN的key的时候,并且是第一个可以JOIN上的右边事件(比如上面的3事件是第一个可以和左边JOIN key P001进行JOIN的事件)需要撤回左边下发的NULL记录,并下发JOIN完整(带有右边事件列)的事件到下游。后续来的4,5,6,8等待后续P001的事件是不会产生撤回记录的。
  • 在Blink系统内部事件类型分为正向事件标记为“+”和撤回事件标记为“-”。
RIGHT OUTER JOIN 和 FULL OUTER JOIN

RIGHT JOIN内部实现与LEFT JOIN类似, FULL JOIN和LEFT JOIN的区别是左右两边都会产生补NULL和撤回的操作。对于State的使用都是相似的,这里不再重复说明了。

复杂场景介绍State结构

上面我们介绍了双流JOIN会使用State记录左右两边流的事件,同时我们示例数据的场景也是比较简单,比如流上没有更新事件(没有撤回事件),同时流上没有重复行事件。那么我们尝试思考下面的事件流在双流JOIN时候是怎么处理的? map_shuffle 上图示例是连续产生了2笔销售数量一样的订单,同时在产生一笔销售数量为5的订单之后,有将该订单取消了(或者退货了),这样在事件流上面就会是上图的示意,这种情况Blink内部如何支撑呢? 根据JOIN的语义以INNER JOIN为例,右边有两条相同的订单流入,我们就应该想下游输出两条JOIN结果,当有撤回的事件流入时候,我们也需要将已经下发下游的JOIN事件撤回,如下: map_shuffle 上面的场景以及LEFT JOIN部分介绍的撤回情况,需要Blink内部需要处理几个核心点:

  • 记录重复记录(完整记录重复记录或者记录相同记录的个数)
  • 记录正向记录和撤回记录(完整记录正向和撤回记录或者记录个数)
  • 记录那一条事件是第一个可以与左边事件进行JOIN的事件
双流JOIN的State数据结构

在Blink内部对不同的场景有特殊的数据结构优化,本篇我们只针对上面说的情况(通用设计)介绍一下双流JOIN的State的数据结构和用途:

  • 数据结构
    • Map<JoinKey, Map<rowData, count>
    • 第一级MAP的key是Join key,比如示例中的P001, value是流上面的所有完整事件
    • 第二级MAP的key是行数据,比如示例中的P001, 2,value是相同事件值的个数
  • 数据结构的利用
    • 记录重复记录 - 利用第二级MAP的value记录重复记录的个数,这样大大减少存储和读取
    • 正向记录和撤回记录 - 利用第二级MAP的value记录,当count=0时候删除该元素
    • 判断右边是否产生撤回记录 - 根据第一级MAP的value的size来判断是否产生撤回,只有size由0变成1的时候(第一条和左可以JOIN的事件)才产生撤回
双流JOIN的应用优化
  • 构造更新流 在 Flink持续查询(Continuous Queries) 的 “Blink store 类型”部分已双流JOIN为例介绍了如何构造业务上的PK source,构造PK source本质上在保证业务语义的同时也是对双流JOIN的一种优化,比如多级LEFT JOIN会让流上的数据不断膨胀,造成JOIN节点性能较慢,JOIN之后的下游节点边堵(数据量大导致,非热点)。那么减少流入JOIN的数据,比如构造PK source就会大大减少JOIN数据的膨胀。这里不再重复举例,大家可以查阅 Flink持续查询(Continuous Queries) 的双流JOIN示例部分。

  • NULL造成的热点 比如我们有A LEFT JOIN B ON A.aCol = B.bCol LEFT JOIN C ON B.cCol = C.cCol 的业务,JOB的DAG如下: map_shuffle 假设在实际业务中有这这样的特点,大部分时候当A事件流入的时候,B还没有可以JION的数据,但是B来的时候,A已经有可以JOIN的数据了,这特点就会导致,A LEFT JOIN B 会产生大量的 (A, NULL),其中包括B里面的 cCol 列也是NULL,这时候当与C进行LEFT JOIN的时候,首先Blink内部会利用cCol对AB的JOIN产生的事件流进行Shuffle, cCol是NULL进而是下游节点大量的NULL事件流入,造成热点。那么这问题如何解决呢? 我们可以改变JOIN的先后顺序,来保证A LEFT JOIN B 不会产生NULL的热点问题,如下: map_shuffle

  • 开启minibatch 在实际的业务使用中开启如下两个参数会大大提高JOIN的性能:
  • flink.miniBatch.size
  • flink.miniBatch.allowLatencyMs 如上两个参数会开启minibatch模式,关于minibatch原理和实现我想单独写一篇进行介绍,这里大家记得在数据出现性能问题时候,打开minibatch尝试调优就好!

  • JOIN ReOrder 对于JOIN算子的实现我们知道左右两边的事件都会存储到State中,在流入事件时候在从另一边读取所有事件进行JOIN计算,这样的实现逻辑在数据量很大的场景会有一定的state操作瓶颈,我们某些场景可以通过业务角度调整JOIN的顺序,来消除性能呢瓶颈,比如:A JOIN B ON A.acol = B.bcol JOIN C ON B.bcol = C.ccol. 这样的场景,如果 A与B进行JOIN产生数据量很大,但是B与C进行JOIN产生的数据量很小,那么我们可以强制调整JOIN的联接顺序,B JOIN C ON b.bcol = c.ccol JOIN A ON a.acol = b.bcol. 如下示意图: map_shuffle

小结

本篇初步向大家介绍传统数据库的JOIN的类型,语义和可以使用的查询优化,再以实际的例子介绍Blink上面的双流JOIN的实现和State数据结构设计,最后向大家介绍三个双流JION的使用优化。本篇只介绍了等值JOIN(ON 子句只有等值条件),Blink目前也支持非等值联接条件和等值联接条件相结合使用,本质是相当于添加了WHERE子句。

Flink Top N实战与原理

传统TopN

TopN语法

  • 全局TopN 最常见的TopN 的写法一般是这样的:
    SELECT column_name(s) FROM table_name WHERE condition 
    ORDER BY order_field [DESC|ASC] LIMIT number
    

    如上语法是MySQL的TopN语法,使用ORDER BY指定排序键和排序方向,使用LIMIT来指定选出前几名。不同的数据库的 TopN 语法不尽相同,比如 MS SQL Server 使用 TOP 的关键字,Oracle 使用 ROWNUM 的隐藏字段。不过几家数据库提供的 TopN 语法都是全局 TopN,也就是数据是全局进行排序的,查询的结果只有一组排行榜。比如希望对全网商家按销售额排序,计算出销售额排名前十的商家。这就是全局 TopN,范例如下:

    SELECT * FROM shop_sales ORDER BY sales DESC LIMIT 10
    
  • 分组TopN 例如对全网商家根据行业按销售额排序,计算出每个行业销售额前十名的商家。这时候,传统的 TopN 语法就无法表达这种需求了。有些 Stream SQL 系统为了解决这个问题,会 hack 一种新的 TopN 语法允许用户指定分组字段。但是 Flink SQL 是基于 ANSI SQL 标准语法的,不能加入任何非标准的语法。于是我们尝试从批处理的角度去思考这个问题,在传统批处理中常用 ROW_NUMBER 的开窗聚合函数来解决分组 TopN 的问题。语法如下所示:
    SELECT *
    FROM (
    SELECT *,
      ROW_NUMBER() OVER ([PARTITION BY col1[, col2..]]
        ORDER BY col1 [asc|desc][, col2 [asc|desc]...]) AS rownum
    FROM table_name)
    WHERE rownum <= N [AND conditions]
    参数说明:
    • ROW_NUMBER(): 是一个计算行号的OVER窗口函数,行号计算从1开始。
    • PARTITION BY col1[, col2..] : 指定分区的列,可以不指定。
    • ORDER BY col1 [asc|desc][, col2 [asc|desc]...]: 指定排序的列,可以多列不同排序方向。
    

    如上语法所示,TopN 需要两层 query,子查询中使用ROW_NUMBER()开窗函数来为每条数据标上排名,排名的计算根据PARTITION BY和ORDER BY来指定分区列和排序列,也就是说每一条数据会计算其在所属分区中,根据排序列排序得到的排名。在外层查询中,对排名进行过滤,只取出排名小于 N 的,如 N=10,那么就是取 Top 10 的数据。如果没有指定PARTITION BY那么就是一个全局 TopN 的计算,所以 ROW_NUMBER 在使用上更为灵活。

流式的TopN不同于批处理的TopN,它的特点是持续的在内存中按照某个统计指标(如出现次数)计算TopN 排行榜,然后当排行榜发生变化时,发出更新后的排行榜。 例如上文说的对全网商家根据行业按销售额排序,计算出每个行业销售额前十名的商家,SQL 范例如下。

SELECT *
FROM (
  SELECT *,
    ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) AS rownum
  FROM shop_sales)
WHERE rownum <= 10

TopN实现和优化

ROW_NUMBER 方式的 TopN 语法非常灵活,能满足全局 TopN 和分组 TopN 的需求。但是在流计算上的物理执行是一个挑战。如上文所述的每个行业销售额前十商家排行榜,经过 SQL 编译后得到的抽象语法树(AST)如下所示。 map_shuffle LogicalWindow 会对所有数据进行排名,也就是说每当到达一个数据,就要对历史数据进行重排序,并输出历史数据的新的排名,然后 LogicalCalc 节点会根据排名进行过滤。这在性能上是非常糟糕的,因为这无限放大了流量。而我们知道,最优的流式 TopN 的计算只需要维护一个 N 元素大小的小根堆,每当有数据到达时,只需要与堆顶元素比较,如果比堆顶元素还小,则直接丢弃;如果比堆顶元素大,则更新小根堆,并输出更新后的排行榜。也就是说我们不需要分为两个节点进行计算,不需要将所有数据进行排序,只需要在一个节点中就可以高效地完成计算。所以我们在查询优化器中加入了一条规则,在使用 TopN 语法时,将 LogicalWindow 和 LogicalCalc 合并成了 LogicalRank 节点。LogicalRank 在翻译成物理执行计划时,会使用一个经过特殊设计的 TopN 算子。
map_shuffle TopN 算子的实现上主要有两个数据结构,一个是 TreeMap,另一个是 MapState。TreeMap 的作用类似于上文的小根堆,有序地存放了排名前 N 的元素。但是 TreeMap 是个内存数据结构,在 failover 后会丢失,无法保证数据的一致性。因此我们还有一个 MapState 结构,MapState 是 Flink 提供的状态接口,用来存储 TopN 的数据(保证数据不丢)。当有 failover 发生后,MapState 能保证状态的恢复,而 TreeMap 会从 MapState 中重新构造出来。我们并有没有把顺序也存到状态中去,因为顺序是可以在恢复时重构的。因为每一次状态的读写操作都会涉及到序列化/反序列化,往往是性能的瓶颈,所以 TreeMap 的主要作用是降低了对 MapState 状态的读写操作。对大部分数据来说都是与 TreeMap 进行交互,不需要对 MapState 进行读写的,全是内存操作,所以 TopN 的性能是非常高的。 map_shuffle TopN 算子的主要处理流程是,每当有数据到达时,会与 TreeMap 的最小的元素比较,如果比它小,那么该数据就不可能是 TopN 的一员,直接丢弃即可。如果比它大,那么就会先更新 TreeMap,同时更新 MapState 中的存的数据。最后输出更新后的排行榜。为了减少冗余数据的输出,我们只会输出排名发生变化的数据。例如原先的第7名上升到了第六名,那么只需要输出新的第六名和第七名即可。

嵌套TopN解决热点问题

TopN 的计算与 GroupBy 的计算类似,如果数据存在倾斜,则会有计算热点的现象。比如全局 TopN,那么所有的数据只能汇集到一个节点进行 TopN 的计算,那么计算能力就会受限于单台机器,无法做到水平扩展。解决思路与 GroupBy 是类似的,就是使用嵌套 TopN,或者说两层 TopN。在原先的 TopN 前面,再加一层 TopN,用于分散热点。例如,计算全网排名前十的商铺,会导致单点的数据热点,那么可以先加一层分组 TopN,组的划分规则是根据店铺 ID 哈希取模后分成128组(并发的倍数)。第二层 TopN 与原先的写法一样,没有 PARTITION BY。第一层会计算出每一组的 TopN,而后在第二层中进行合并汇总,得到最终的全网前十。第二层虽然仍是单点,但是大量的计算量由第一层分担了,而第一层是可以水平扩展的。使用嵌套 TopN 的优化写法如下所示:

CREATE VIEW tmp_topn AS
SELECT *
FROM (
  SELECT *,
    ROW_NUMBER() OVER (PARTITION BY HASH_CODE(shop_id)%128 ORDER BY sales DESC) AS rownum
  FROM shop_sales)
WHERE rownum <= 10
SELECT *
FROM (
  SELECT shop_id, shop_name, sales,
    ROW_NUMBER() OVER (ORDER BY sales DESC) AS rownum
  FROM tmp_topn)
WHERE rownum <= 10

Flink SQL维表Join

背景

在流计算中我们会经常需要涉及数据流补齐字段。因为一条流的数据比较单一,维度有限,而在使用的时候需要更多地维度,就要先将所需的维度信息补全。比如某条流里是交易日志中商品id,但是在做业务时需要根据店铺维度或者行业纬度进行聚合,这就需要先将交易日志与商品维表进行关联,补全所需的维度信息。这里所说的维表与数据仓库中的概念类似,是维度属性的集合,比如商品维,地点维,用户维等等。

维表JOIN语法

由于维表是一张不断变化的表(静态表只是动态表的一种特例)。如果用传统的JOIN语法SELECT * FROM T JOIN dim_table on T.id = dim_table.id 来表达维表JOIN是不完整的。因为维表是一直在更新变化的,这个语法关联上的哪个时刻的维表是确定的。所以Flink SQL的维表JOIN语法引入了SQL:2011 Temporal Table的标准语法,用来声明关联的是维表哪个时刻的快照。维表 JOIN 语法/示例如下。

假设我们有一个Orders订单数据流,希望根据产品ID补全流上的产品维度信息,所以需要跟Products维度表进行关联。Orders和Products的DDL声明语句如下:

CREATE TABLE Orders (
  orderId VARCHAR,          -- 订单 id
  productId VARCHAR,        -- 产品 id
  units INT,                -- 购买数量
  orderTime TIMESTAMP       -- 下单时间
) with (
  'connector.type' = 'kafka',  -- kafka数据源
  'connector.version' = '0.11',
  'connector.topic' = 'topic_name',
)

CREATE TABLE Products (
  productId VARCHAR,        -- 产品 id
  name VARCHAR,             -- 产品名称
  unitPrice DOUBLE          -- 单价
  PERIOD FOR SYSTEM_TIME,   -- 这是一张随系统时间而变化的表,用来声明维表
  PRIMARY KEY (productId)   -- 维表必须声明主键
) with (
  'connector.type' = 'hbase', -- required: specify this table type is hbase
  'connector.version' = '1.4.3',          -- required: valid connector versions are "1.4.3"
  'connector.table-name' = 'hbase_table_name', 
  ...
)

JOIN当前维表

SELECT *
FROM Orders AS o
[LEFT] JOIN Products FOR SYSTEM_TIME AS OF PROCTIME() AS p
ON o.productId = p.productId

Flink SQL支持LEFT JOIN和INNER JOIN的维表关联。如上语法所示的,维表JOIN语法与传统的JOIN语法并无二异。 只是Products维表后面需要跟上FOR SYSTEM_TIME AS OF PROCTIME()的关键字,其含义是每条到达的数据所关联上的是到达时刻的维表快照, 也就是说,当数据到达时,我们会根据数据上的key去查询远程数据库,拿到匹配的结果后关联输出。这里的PROCTIME即processing time。 使用JOIN当前维表功能需要注意的是,如果维表插入了一条数据能匹配上之前左表的数据时,JOIN的结果流,不会发出更新的数据以弥补之前的未匹配。 JOIN行为只发生在处理时间(processing time),即使维表中的数据都被删了,之前JOIN流已经发出的关联上的数据也不会被撤回或改变。

JOIN历史维表

SELECT *
FROM Orders AS o
[LEFT] JOIN Products FOR SYSTEM_TIME AS OF o.orderTime AS p
ON o.productId = p.productId

有时候想关联上的维度数据,并不是当前时刻的值,而是某个历史时刻的值。比如,产品的价格一直在发生变化,订单流希望补全的是下单时的价格, 而不是当前的价格,那就是JOIN历史维表。语法上只需要将上文的PROCTIME()改成o.orderTime即可。含义是关联上的是下单时刻的Products维表。 Flink在获取维度数据时,会根据左流的时间去查对应时刻的快照数据。因此JOIN 历史维表需要外部存储支持多版本存储,如HBase,或者存储的数据中带有多版本信息。

AbstractList.modCount

What

Below details is copy from AbstractList.java source code.

/**
  * The number of times this list has been <i>structurally modified</i>.
  * Structural modifications are those that change the size of the
  * list, or otherwise perturb it in such a fashion that iterations in
  * progress may yield incorrect results.
  *
  * <p>This field is used by the iterator and list iterator implementation
  * returned by the {@code iterator} and {@code listIterator} methods.
  * If the value of this field changes unexpectedly, the iterator (or list
  * iterator) will throw a {@code ConcurrentModificationException} in
  * response to the {@code next}, {@code remove}, {@code previous},
  * {@code set} or {@code add} operations.  This provides
  * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
  * the face of concurrent modification during iteration.
  *
  * <p><b>Use of this field by subclasses is optional.</b> If a subclass
  * wishes to provide fail-fast iterators (and list iterators), then it
  * merely has to increment this field in its {@code add(int, E)} and
  * {@code remove(int)} methods (and any other methods that it overrides
  * that result in structural modifications to the list).  A single call to
  * {@code add(int, E)} or {@code remove(int)} must add no more than
  * one to this field, or the iterators (and list iterators) will throw
  * bogus {@code ConcurrentModificationExceptions}.  If an implementation
  * does not wish to provide fail-fast iterators, this field may be
  * ignored.
  */
 protected transient int modCount = 0;

Why

This field is used by the iterator and list iterator implementation returned by the iterator and listIterator methods. If the value of this field changes unexpectedly, the iterator (or list iterator) will throw a ConcurrentModificationException in response to the next, remove, previous, set or add operations. This provides fail-fast behavior, rather than non-deterministic behavior in the face of concurrent modification during iteration.

How

/**
    * Returns a list-iterator of the elements in this list (in proper
    * sequence), starting at the specified position in the list.
    * Obeys the general contract of {@code List.listIterator(int)}.<p>
    *
    * The list-iterator is <i>fail-fast</i>: if the list is structurally
    * modified at any time after the Iterator is created, in any way except
    * through the list-iterator's own {@code remove} or {@code add}
    * methods, the list-iterator will throw a
    * {@code ConcurrentModificationException}.  Thus, in the face of
    * concurrent modification, the iterator fails quickly and cleanly, rather
    * than risking arbitrary, non-deterministic behavior at an undetermined
    * time in the future.
    *
    * @param index index of the first element to be returned from the
    *              list-iterator (by a call to {@code next})
    * @return a ListIterator of the elements in this list (in proper
    *         sequence), starting at the specified position in the list
    * @throws IndexOutOfBoundsException {@inheritDoc}
    * @see List#listIterator(int)
    */
   public ListIterator<E> listIterator(int index) {
       checkPositionIndex(index);
       return new ListItr(index);
   }

   private class ListItr implements ListIterator<E> {
       private Node<E> lastReturned;
       private Node<E> next;
       private int nextIndex;
       private int expectedModCount = modCount;

       ......

       final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
   }

You will find that they will check the value (modCount != expectedModCount ?).

Fail Fast & Fail Safe

Iterators in java are used to iterate over the Collection objects.

  • Fail-Fast iterators immediately throw ConcurrentModificationException if there is structural modification of the collection. Structural modification means adding, removing or updating any element from collection while a thread is iterating over that collection. Iterator on ArrayList, HashMap classes are some examples of fail-fast Iterator.
  • Fail-Safe iterators don’t throw any exceptions if a collection is structurally modified while iterating over it. This is because, they operate on the clone of the collection, not on the original collection and that’s why they are called fail-safe iterators. Iterator on CopyOnWriteArrayList, ConcurrentHashMap classes are examples of fail-safe Iterator.

Gradle Avro Plugin Guide

Add below script into your build.gradle file.

apply plugin: "com.commercehub.gradle.plugin.avro-base"  
  
buildscript {  
    repositories {  
        maven {  
            url "xxx"  
            credentials{  
                username=xxx  
                password=xxx  
            }  
        }  
        maven {  
            url "xxx"  
        }  
    }  
    dependencies {  
        classpath 'com.commercehub.gradle.plugin:gradle-avro-plugin:0.16.0'  
    }  
}  
  
dependencies {  
    compile group: 'org.codehaus.jackson', name: 'jackson-mapper-asl', version: '1.9.13'  
}  
  
task generateAvro(type: com.commercehub.gradle.plugin.avro.GenerateAvroJavaTask) {  
    source("src/main/avro")  
    outputDir = file("src/main/java")  
}  

If you are using IntelliJ IDEA, you can enter the Gradle view and click the Tasks -> other -> generateAvro button to generate a Java bean from the avsc file you provided.

Kafka Cleanup Policy

Compaction

Log Compaction的大部分功能由CleanerThread完成, 核心逻辑在Cleaner的clean方法

class LogCleaner(val config: CleanerConfig,
                 val logDirs: Array[File],
                 val logs: Pool[TopicPartition, Log],
                 time: Time = Time.SYSTEM) extends Logging with KafkaMetricsGroup {
                 ......
  /**
   * The cleaner threads do the actual log cleaning. Each thread processes does its cleaning repeatedly by
   * choosing the dirtiest log, cleaning it, and then swapping in the cleaned segments.
   */
  private class CleanerThread(threadId: Int)
    extends ShutdownableThread(name = "kafka-log-cleaner-thread-" + threadId, isInterruptible = false){
    ......
    val cleaner = new Cleaner(id = threadId,
                              offsetMap = new SkimpyOffsetMap(memory = math.min(config.dedupeBufferSize / config.numThreads, Int.MaxValue).toInt, 
                                                              hashAlgorithm = config.hashAlgorithm),
                              ioBufferSize = config.ioBufferSize / config.numThreads / 2,
                              maxIoBufferSize = config.maxMessageSize,
                              dupBufferLoadFactor = config.dedupeBufferLoadFactor,
                              throttler = throttler,
                              time = time,
                              checkDone = checkDone)
    ......
  }
  ......
}

注意SkimpyOffsetMap的参数hashAlgorithm = config.hashAlgorithm. 而config为CleanerConfig.

/**
 * This class holds the actual logic for cleaning a log
 * @param id An identifier used for logging
 * @param offsetMap The map used for deduplication
 * @param ioBufferSize The size of the buffers to use. Memory usage will be 2x this number as there is a read and write buffer.
 * @param maxIoBufferSize The maximum size of a message that can appear in the log
 * @param dupBufferLoadFactor The maximum percent full for the deduplication buffer
 * @param throttler The throttler instance to use for limiting I/O rate.
 * @param time The time instance
 * @param checkDone Check if the cleaning for a partition is finished or aborted.
 */
private[log] class Cleaner(val id: Int,
                           val offsetMap: OffsetMap,
                           ioBufferSize: Int,
                           maxIoBufferSize: Int,
                           dupBufferLoadFactor: Double,
                           throttler: Throttler,
                           time: Time,
                           checkDone: (TopicPartition) => Unit) extends Logging {
                ......
  /**
   * Clean the given log
   *
   * @param cleanable The log to be cleaned
   *
   * @return The first offset not cleaned and the statistics for this round of cleaning
   */
  private[log] def clean(cleanable: LogToClean): (Long, CleanerStats) = {
    val stats = new CleanerStats()

    info("Beginning cleaning of log %s.".format(cleanable.log.name))
    val log = cleanable.log

    // build the offset map
    info("Building offset map for %s...".format(cleanable.log.name))
    val upperBoundOffset = cleanable.firstUncleanableOffset
    buildOffsetMap(log, cleanable.firstDirtyOffset, upperBoundOffset, offsetMap, stats)
    val endOffset = offsetMap.latestOffset + 1
    stats.indexDone()
    
    // figure out the timestamp below which it is safe to remove delete tombstones
    // this position is defined to be a configurable time beneath the last modified time of the last clean segment
    val deleteHorizonMs = 
      log.logSegments(0, cleanable.firstDirtyOffset).lastOption match {
        case None => 0L
        case Some(seg) => seg.lastModified - log.config.deleteRetentionMs
    }

    // determine the timestamp up to which the log will be cleaned
    // this is the lower of the last active segment and the compaction lag
    val cleanableHorizonMs = log.logSegments(0, cleanable.firstUncleanableOffset).lastOption.map(_.lastModified).getOrElse(0L)

    // group the segments and clean the groups
    info("Cleaning log %s (cleaning prior to %s, discarding tombstones prior to %s)...".format(log.name, new Date(cleanableHorizonMs), new Date(deleteHorizonMs)))
    for (group <- groupSegmentsBySize(log.logSegments(0, endOffset), log.config.segmentSize, log.config.maxIndexSize))
      cleanSegments(log, group, offsetMap, deleteHorizonMs, stats)

    // record buffer utilization
    stats.bufferUtilization = offsetMap.utilization
    
    stats.allDone()

    (endOffset, stats)
  }
  .......
}

可以发现log compaction通过两次遍历所有数据来实现, 两次遍历之间就个OffsetMap交互, 这个OffsetMap的实现是SkimpyOffsetMap.

/**
 * An hash table used for deduplicating the log. This hash table uses a cryptographicly secure hash of the key as a proxy for the key
 * for comparisons and to save space on object overhead. Collisions are resolved by probing. This hash table does not support deletes.
 * @param memory The amount of memory this map can use
 * @param hashAlgorithm The hash algorithm instance to use: MD2, MD5, SHA-1, SHA-256, SHA-384, SHA-512
 */
@nonthreadsafe
class SkimpyOffsetMap(val memory: Int, val hashAlgorithm: String = "MD5") extends OffsetMap{}

注意到hashAlgorithm默认是MD5, 前面Cleaner初始化SkimpyOffsetMap时, 这个参数是通过CleanerConfig获取.
看看CleanerConfig结构.

/**
 * Configuration parameters for the log cleaner
 * 
 * @param numThreads The number of cleaner threads to run
 * @param dedupeBufferSize The total memory used for log deduplication
 * @param dedupeBufferLoadFactor The maximum percent full for the deduplication buffer
 * @param maxMessageSize The maximum size of a message that can appear in the log
 * @param maxIoBytesPerSecond The maximum read and write I/O that all cleaner threads are allowed to do
 * @param backOffMs The amount of time to wait before rechecking if no logs are eligible for cleaning
 * @param enableCleaner Allows completely disabling the log cleaner
 * @param hashAlgorithm The hash algorithm to use in key comparison.
 */
case class CleanerConfig(numThreads: Int = 1,
                         dedupeBufferSize: Long = 4*1024*1024L,
                         dedupeBufferLoadFactor: Double = 0.9d,
                         ioBufferSize: Int = 1024*1024,
                         maxMessageSize: Int = 32*1024*1024,
                         maxIoBytesPerSecond: Double = Double.MaxValue,
                         backOffMs: Long = 15 * 1000,
                         enableCleaner: Boolean = true,
                         hashAlgorithm: String = "MD5") {
}

搜索了初始化LogCleaner的地方.

val cleaner: LogCleaner =
    if(cleanerConfig.enableCleaner)
      new LogCleaner(cleanerConfig, liveLogDirs, currentLogs, logDirFailureChannel, time = time)
    else
      null

可以认为hashAlgorithm是MD5无疑了.
顺便提一句, Kafka有个DynamicBrokerConfig的Object.

/**
  * Dynamic broker configurations are stored in ZooKeeper and may be defined at two levels:
  * <ul>
  *   <li>Per-broker configs persisted at <tt>/configs/brokers/{brokerId}</tt>: These can be described/altered
  *       using AdminClient using the resource name brokerId.</li>
  *   <li>Cluster-wide defaults persisted at <tt>/configs/brokers/&lt;default&gt;</tt>: These can be described/altered
  *       using AdminClient using an empty resource name.</li>
  * </ul>
  * The order of precedence for broker configs is:
  * <ol>
  *   <li>DYNAMIC_BROKER_CONFIG: stored in ZK at /configs/brokers/{brokerId}</li>
  *   <li>DYNAMIC_DEFAULT_BROKER_CONFIG: stored in ZK at /configs/brokers/&lt;default&gt;</li>
  *   <li>STATIC_BROKER_CONFIG: properties that broker is started up with, typically from server.properties file</li>
  *   <li>DEFAULT_CONFIG: Default configs defined in KafkaConfig</li>
  * </ol>
  * Log configs use topic config overrides if defined and fallback to broker defaults using the order of precedence above.
  * Topic config overrides may use a different config name from the default broker config.
  * See [[kafka.log.LogConfig#TopicConfigSynonyms]] for the mapping.
  * <p>
  * AdminClient returns all config synonyms in the order of precedence when configs are described with
  * <code>includeSynonyms</code>. In addition to configs that may be defined with the same name at different levels,
  * some configs have additional synonyms.
  * </p>
  * <ul>
  *   <li>Listener configs may be defined using the prefix <tt>listener.name.{listenerName}.{configName}</tt>. These may be
  *       configured as dynamic or static broker configs. Listener configs have higher precedence than the base configs
  *       that don't specify the listener name. Listeners without a listener config use the base config. Base configs
  *       may be defined only as STATIC_BROKER_CONFIG or DEFAULT_CONFIG and cannot be updated dynamically.<li>
  *   <li>Some configs may be defined using multiple properties. For example, <tt>log.roll.ms</tt> and
  *       <tt>log.roll.hours</tt> refer to the same config that may be defined in milliseconds or hours. The order of
  *       precedence of these synonyms is described in the docs of these configs in [[kafka.server.KafkaConfig]].</li>
  * </ul>
  *
  */
object DynamicBrokerConfig {
  ......
  val AllDynamicConfigs = DynamicSecurityConfigs ++
    LogCleaner.ReconfigurableConfigs ++
    DynamicLogConfig.ReconfigurableConfigs ++
    DynamicThreadPool.ReconfigurableConfigs ++
    Set(KafkaConfig.MetricReporterClassesProp) ++
    DynamicListenerConfig.ReconfigurableConfigs ++
    SocketServer.ReconfigurableConfigs
    
    .......
}

Complexity

复杂度分析定义

  • 复杂度分析包含时间复杂度和空间复杂度,描述的是算法执行时间(或占用空间)与数据规模的增长关系
  • 时间复杂度的全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系
  • 空间复杂度全称就是渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系
  • 数据结构和算法解决是“如何让计算机更快时间、更省空间的解决问题”,因此需从执行时间和占用空间两个维度来评估数据结构和算法的性能。

为什么要进行复杂度分析

  1. 和性能测试相比,复杂度分析有不依赖执行环境、成本低、效率高、易操作、指导性强的特点
  2. 复杂度分析能帮助编写出性能更优的代码,有利于降低系统开发和维护成本

如何进行复杂度分析

  1. 大O复杂度表示法
    • 来源
    • 算法的执行时间与每行代码的执行次数成正比,用T(n) = O(f(n))表示,其中T(n)表示算法执行总时间,f(n)表示每行代码执行总次数,而n往往表示数据的规模 + 特点
    • 以时间复杂度为例,由于时间复杂度描述的是算法执行时间与数据规模的增长变化趋势,所以常量阶、低阶以及系数实际上对这种增长趋势不产决定性影响,所以在做时间复杂度分析时忽略这些项
  2. 复杂度分析法则
    • 单段代码看高频:比如循环
    • 多段代码取最大:比如一段代码中有单循环和多重循环,那么取多重循环的复杂度
    • 嵌套代码求乘积:比如递归、多重循环等
    • 多个规模求加法:比如方法有两个参数控制两个循环的次数,那么这时就取二者复杂度相加

常用的复杂度级别

  • 多项式阶:随着数据规模的增长,算法的执行时间和空间占用,按照多项式的比例增长。
    • O(1)(常数阶)
      • O(1) 只是常量级时间复杂度的一种表示方法,并不是指只执行了一行代码。
      • 一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)
    • O(n)(线性阶)
      public void print(int n) {
        int i = 0;
        int[] a = new int[n];
        for (i; i <n; ++i) {
          a[i] = i * i;
        }
        for (i = n-1; i >= 0; --i) {
          System.out.println(a[i])
        }
      }
      

      跟时间复杂度分析一样,我们可以看到,第 2行代码中,我们申请了一个空间存储变量 i,但是它是常量阶的,跟数据规模n 没有关系,所以我们可以忽略。第 3行申请了一个大小为 n的 int类型数组,除此之外,剩下的代码都没有占用更多的空间,所以整段代码的空间复杂度就是O(n)

    • O(logn)(对数阶)
        i = 1;
        while(i <= n) {
          i = i * 2
        }
      

      从代码中可以看出,变量i的值从1开始取,每循环一次就乘以2。当大于n时,循环结束。实际上,变量i的取值就是一个等比数列。所以,我们只要知道x值是多少,就知道这行代码执行的次数了。x=logn,所以,这段代码的时间复杂度就是O(logn)。

    • O(nlogn)(线性对数阶)
    • O(n^2)(平方阶)
    • O(n^3)(立方阶)
  • 非多项式阶:随着数据规模的增长,算法的执行时间和空间占用暴增,这类算法性能极差。
    • O(2^n)(指数阶)
    • O(n!)(阶乘阶)

复杂度分析的4个概念

  1. 最坏情况时间复杂度:代码在最理想情况下执行的时间复杂度
  2. 最好情况时间复杂度:代码在最坏情况下执行的时间复杂度
  3. 平均时间复杂度:用代码在所有情况下执行的次数的加权平均值表示
  4. 均摊时间复杂度:在代码执行的所有复杂度情况中绝大部分是低级别的复杂度,个别情况是高级别复杂度且发生具有时序关系时,可以将个别高级别复杂度均摊到低级别复杂度上。基本上均摊结果就等于低级别复杂度

为什么要引入这4个概念

  1. 同一段代码在不同情况下时间复杂度会出现量级差异,为了更全面,更准确的描述代码的时间复杂度,所以引入这4个概念
  2. 代码复杂度在不同情况下出现量级差别时才需要区别这四种复杂度。大多数情况下,是不需要区别分析它们的。

如何分析平均、均摊时间复杂度

  1. 平均时间复杂度代码在不同情况下复杂度出现量级差别,则用代码所有可能情况下执行次数的加权平均值表示 2.均摊时间复杂度两个条件满足时使用:
    • 代码在绝大多数情况下是低级别复杂度,只有极少数情况是高级别复杂度
    • 低级别和高级别复杂度出现具有时序规律。均摊结果一般都等于低级别复杂度

如何掌握好复杂度分析方法

复杂度分析关键在于多练,所谓孰能生巧。

New Concept English - Grammar 9

  • 形容词副词比较级最高级

        大多数形容词副词有三种形式:原级,比较级,最高级
        两者进行比较时用比较级
        三者或三者以上进行比较时用最高级

    • 比较级最高级构成
      1. 一般情况词尾加er构成比较级,加est构成最高级 tall -> taller -> tallest
        hard -> harder -> hardest
        great -> greater -> greatest
      2. 以e结尾的词直接加r/st large -> larger -> largest
        nick -> nicer -> nicest
      3. 以辅音字母加y结尾的词,把y变为i再加er/est happy -> happier -> happest
        easy -> easier -> easiest
      4. 重读闭音节结尾的词双写最后一个辅音字母再加er/est hot -> hotter -> hottest
        big -> bigger -> biggest
    • 比较级最高级用法
      1. 比较级只用于两者之间 构成: 比较级 + than…
        若比较的东西比较明显,than之后可以省略 Marry is taller than Susan.

Java Foundation FAQ

What is the difference between NoClassDefFoundError and ClassNotFoundException?

  • NoClassDefFoundError

        Thrown if the Java Virtual Machine or a ClassLoader instance tries to load in the definition of a class (as part of a normal method call or as part of creating a new instance using the new expression) and no definition of the class could be found. The searched-for class definition existed when the currently executing class was compiled, but the definition can no longer be found.

  • ClassNotFoundException

        Thrown when an application tries to load in a class through its string name using:
            The forName method in class Class.
            The findSystemClass method in class ClassLoader.
            The loadClassmethod in class ClassLoader.
    but no definition for the class with the specified name could be found.
        As of release 1.4, this exception has been retrofitted to conform to the general purpose exception-chaining mechanism. The “optional exception that was raised while loading the class” that may be provided at construction time and accessed via the getException() method is now known as the cause, and may be accessed via the Throwable.getCause() method, as well as the aforementioned “legacy method.”

What is the difference between final, finally and finalize?

  • final

        final is a reserved keyword in java. We can’t use it as an identifier as it is reserved. We can use this keyword with variables, methods and also with classes. The final keyword in java has different meaning depending upon it is applied to variable, class or method.

  • final with Variables

        The value of variable cannot be changed once initialized. If we declare any variable as final, we can’t modify its contents since it is final, and if we modify it then we get Compile Time Error.

  • final with Class

        The class cannot be subclassed. Whenever we declare any class as final, it means that we can’t extend that class or that class can’t be extended or we can’t make subclass of that class.

  • final with Method

        The method cannot be overridden by a subclass. Whenever we declare any method as final, then it means that we can’t override that method.

  • finally

        The finally keyword is used in association with a try/catch block and guarantees that a section of code will be executed, even if an exception is thrown. The finally block will be executed after the try and catch blocks, but before control transfers back to its origin.

  • finalize

        It is a method that the Garbage Collector always calls just before the deletion/destroying the object which is eligible for Garbage Collection, so as to perform clean-up activity. Clean-up activity means closing the resources associated with that object like Database Connection, Network Connection or we can say resource de-allocation. Remember it is not a reserved keyword.
        Once finalize method completes immediately Garbage Collector destroy that object. finalize method is present in Object class and its syntax is:
            protected void finalize throws Throwable{}
        Since Object class contains finalize method hence finalize method is available for every java class since Object is superclass of all java classes. Since it is available for every java class hence Garbage Collector can call finalize method on any java object Now, the finalize method which is present in Object class, has empty implementation, in our class clean-up activities are there, then we have to override this method to define our own clean-up activities.
        There is no guarantee about the time when finalize is called. It may be called any time after the object is not being referred anywhere (cab be garbage collected).
        JVM does not ignore all exceptions while executing finalize method, but it ignores only Unchecked exceptions. If the corresponding catch block is there then JVM won’t ignore and corresponding catch block will be executed.
        System.gc() is just a request to JVM to execute the Garbage Collector. It’s up-to JVM to call Garbage Collector or not.Usually JVM calls Garbage Collector when there is not enough space available in the Heap area or when the memory is low.

New Concept English - Grammar 8

  • 反意疑问句
    • 结构:
      • 前一部分为陈述句,后一部分为简短问句,两部分有逗号隔开
      • 陈述句肯定则问句否定,陈述句否定则问句肯定。
    • 注意:
      1. 问句主语需要和主句主语一致,且问句用代词不用名词
      2. 前后助动词,时态须保持一致
      3. 没有助动词的,后面部分需要加上助动词(只能加do, does, did)

        eg:
            You have lived here for many years, haven’t you? 前肯后否,助动词为have,前后需保持一致
            He won’t be late, will he?
            You are tired, aren’t you?
            It isn’t going to rain tomorrow, is it?
            He went out, didn’t he?前面部分没有助动词,需要我们去添加助动词do,does,did
            You love me, don’t you?
            He doesn’t feel ill, does he?
            You can wait for me, can’t you?
            There is no water here, is there?

  • 直接引语与间接引语(引用别人的话)
    • 直接引语与间接引语都是宾语
    • 直接引语即一字不差引用别人的话,需放在单引号内
    • 间接引语则是用自己的话转述别人的话,不需要引号,而以宾语从句的形式出现。
    • 直接引语变间接引语时须注意人称和时态的变化:
      • 人称的变化:按照中文作相应改变
      • 时态的变化:
        1. 若当即转述别人的话,动词(如say,tell等)用一般现在时,则直接引语中的时态不用作任何改变放在间接引语中。

          eg:
              He says:’I am busy.’ -> He says that he is busy.
              Mr.Jones says:’I have just finished my work.’ -> Mr.Jones says that he has just finished his work.
              Sally says:’I’m sitting under the tree.’ -> Sally says that she is sitting under the tree.
              Sara says:’I broke that plate.’ -> Sara says that she broke that plate.
              Jack says:’I’ll go to England tomorrow.’ -> Jack says that he will go to England tomorrow.

        2. 若过来一段时间再转述别人的话,动词用一般过去时态(said,told等),则时态需作如下变化: 直接引语 间接引语
          一般现在时 -> 一般过去时
          一般过去时 -> 过去完成时
          现在进行时 -> 过去进行时
          一般将来时 -> 过去将来时
          现在完成时 -> 过去完成时
          情态动词 -> 对应的过去式

          eg:
              She said:’I’m hungry.’ -> She said that she was hungry.
              ‘I am having breakfast,’ he told me. -> He told me that he was having breakfast.
              He said:’I want to see you.’ -> He said that he wanted to see me.
              She said:’I’ve just made a new film.’ -> She said that she had just made a new film.
              She said:’I am going to retire.’ -> She said that she was going to retire.
              Tom said:’I broke the cup.’ -> Tom said that he had broken the cup.
              Jane said to Jack:’I love you.’ -> Jane said to Jack that she loved him.
              He said:’We will come back soon.’ -> He said that they would come back soon.

New Concept English - Grammar 7

时态

即时间和形态,不同时间所体现出来的形态不一样。英文中时态的不同靠动词来体现。

  • 一般将来时态
    • 时间:表示从现在看将来要发生的动作或情况, 常与将来的时间状语连用,如tomorrow, next week等
    • 结构: will/shall(情态动词) + 动词原形 (情态动词必须接动词原形)
      • shall只用于第一人称
      • will可用于任何人称,常和主语缩写为’ll,否定缩写won’t
      • will/shall为情态动词(情态动词当助动词来用),不随主语的改变而改变,其后必须用动词原形

        eg:
            I’ll leave Beijing tomorrow.
            It’ll snow tonight.
            It won’t snow tonight.
            I was, am and will be your friend.

      • be gong to为将来时态的一种体现形式,但如表示客观的,人为无法改变的事不能用此结构。

        eg:
            I’ll be 20 next year.
            不说,I am going to be 20 next year.因为年龄不说你说变就变的

  • 过去将来时
    • 用法: 表示从过去看将要发生的动作
    • 结构: would + 动词原形

      eg:
          He said he wouldn’t come tomorrow.

New Concept English - Grammar 6

时态

即时间和形态,不同时间所体现出来的形态不一样。英文中时态的不同靠动词来体现。

  • 现在完成时态
    • 结构 has(第三人称单数时用)/have+动词过去分词
      过去分词形式通常与过去式一样,不规则动词需单独记忆。
      has/have为助动词
      否定疑问在has/have上发变化
    • 用法
      1. 动作在过去已经发生,但对现在的影响依然存在,而且具体发生时间不明,一旦有具体发生的时间,就必须用一般过去时。
        • 与一般过去时的区分:
        • 一般过去时只表示具体过去某时发生的动作,常与以下时间状语连用:yesterday, last, year, last week, 3 days ago, in 1989等
        • 现在完成时强调一个过去发生的动作对现在造成的影响或结果。常与以下时间状语连用:just(刚刚), already(已经), yet(还,仍然), never(从不), ever(曾经)等

          eg:
              He has already left.
              I have already finished my work.
              We have alreday had breadkfast.
              I have already had 3 cups of cooffee.
              My brother has just arrived in Beijing.

      2. 表示一直延续到现在的动作
        这时句中常出现for,since,so far等词

        eg:
            I have lived in BJ for 12 years. for+时间段
            I have lived in BJ since 2002. since+时间点
            So far,we haven’t seen each other.
            I am a teacher.一般现在时态
            I was a teacher 12 years ago.一般过去时态
            I have been a teacher for 12 years.现在完成时态

High Concurrency Architecture FAQ

  1. 如何设计一个高并发系统?
  2. 消息队列
    • 为什么使用消息队列?
    • 消息队列有什么优点和缺点?
    • kafka、activemq、rabbitmq、rocketmq都有什么优点和缺点?
    • 如何保证消息队列的高可用啊?
    • 如何保证消息不被重复消费啊(如何进行消息队列的幂等性问题)?
    • 如何保证消息的可靠性传输(如何处理消息丢失的问题)?
    • 如何保证消息的顺序性?
    • 如何解决消息队列的延时以及过期失效问题?
    • 消息队列满了以后该怎么处理?
    • 有几百万消息持续积压几小时,怎么解决?
    • 如果让你写一个消息队列,该如何进行架构设计?说一下你的思路
  3. 搜索引擎
    • es的分布式架构原理能说一下么(es是如何实现分布式的)?
    • es写入数据的工作原理是什么?
    • es查询数据的工作原理是什么?
    • 底层的lucene介绍一下?
    • 倒排索引?
    • es在数据量很大的情况下(数十亿级别)如何提高查询效率?
    • es生产集群的部署架构是什么?
    • 每个索引的数据量大概有多少?
    • 每个索引大概有多少个分片?
  4. 缓存
    • 在项目中缓存是如何使用的?
    • 缓存如果使用不当会造成什么后果?
    • redis和memcached有什么区别?
    • redis的线程模型是什么?
    • 为什么单线程的redis比多线程的memcached效率要高得多?
    • redis都有哪些数据类型?
    • 分别在哪些场景下使用比较合适?
    • redis的过期策略都有哪些?
    • 手写一下LRU代码实现?
    • 如何保证Redis高并发、高可用、持久化?
    • redis的主从复制原理能介绍一下么?
    • redis的哨兵原理能介绍一下么?
    • redis的持久化有哪几种方式?
    • 不同的持久化机制都有什么优缺点?
    • 持久化机制具体底层是如何实现的?
    • redis集群模式的工作原理能说一下么?
    • 在集群模式下,redis的key是如何寻址的?
    • 分布式寻址都有哪些算法?
    • 了解一致性hash算法吗?
    • 如何动态增加和删除一个节点?
    • 了解什么是redis的雪崩和穿透?
    • redis崩溃之后会怎么样?
    • 系统该如何应对这种情况?
    • 如何处理redis的穿透?
    • 如何保证缓存与数据库的双写一致性?
    • redis的并发竞争问题是什么?
    • 如何解决这个问题?
    • 了解Redis事务的CAS方案吗?
    • 生产环境中的redis是怎么部署的?
  5. 分库分表
    • 为什么要分库分表(设计高并发系统的时候,数据库层面该如何设计)?
    • 用过哪些分库分表中间件?
    • 不同的分库分表中间件都有什么优点和缺点?
    • 你们具体是如何对数据库如何进行垂直拆分或水平拆分的?
    • 现在有一个未分库分表的系统,未来要分库分表,如何设计才可以让系统从未分库分表动态切换到分库分表上?
    • 如何设计可以动态扩容缩容的分库分表方案?
    • 分库分表之后,id主键如何处理?
  6. 读写分离
    • 如何实现mysql的读写分离?
    • MySQL主从复制原理的是啥?
    • 如何解决mysql主从同步的延时问题?

High Availability Architecture FAQ

  1. 如何设计一个高可用系统?
  2. 限流
    • 如何限流,在工作中是怎么做的,说一下具体的实现?
  3. 熔断
    • 如何进行熔断?
    • 熔断框架都有哪些?
    • 具体实现原理?
  4. 降级
    • 如何进行降级?

Distributed System FAQ

  1. 为什么要进行系统拆分?
    • 为什么要进行系统拆分?
    • 如何进行系统拆分?
    • 拆分后不用dubbo可以吗?
    • dubbo和thrift有什么区别呢?
  2. 分布式服务框架
    • 说一下的dubbo的工作原理?注册中心挂了可以继续通信吗?
    • dubbo支持哪些序列化协议?说一下hessian的数据结构?PB知道吗?为什么PB的效率是最高的?
    • dubbo负载均衡策略和高可用策略都有哪些?动态代理策略呢?
    • dubbo的spi思想是什么?
    • 如何基于dubbo进行服务治理、服务降级、失败重试以及超时重试?
    • 分布式服务接口的幂等性如何设计(比如不能重复扣款)?
    • 分布式服务接口请求的顺序性如何保证?
    • 如何自己设计一个类似dubbo的rpc框架?
  3. 分布式锁
    • 使用redis如何设计分布式锁?使用zk来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高?
  4. 分布式事务
    • 分布式事务了解吗?
    • 你们如何解决分布式事务问题的?
    • TCC如果出现网络连不通怎么办?
    • XA的一致性如何保证?
  5. 分布式会话
    • 集群部署时的分布式session如何实现?

New Concept English - Grammar 5

时态

即时间和形态,不同时间所体现出来的形态不一样。英文中时态的不同靠动词来体现。

  • 一般过去时态
    • 用法
      1. 表示过去某个特定时间点的状态或动作

        eg:
            I bought a dictionary yestday.标志词
            He was a doctor a year ago.标志词

      2. 过去某段时间内的习惯,反复发生的动作。

        eg:
            When he was a child, he ofthe wet the bed.标志词

      3. 当谈到已故的人的时候。

        eg:
            LuXun was a great writer.

    • 结构
      1. 主语+be过去时(was,were)
        • am -> was, is -> was, are -> are
        • I/He/She/It was…
        • We/You/They were…

          eg:
              Jimmy was ill last week.
              I was at the dentist’s yestderay.
              It was cold yesterday.
              We were home yesterday.     I was tired last night.

        • 否定疑问:在be上面发生变化

          eg:
              Was Jimmy ill last week?
              Where were you just now?
              Were you at the dentist’s? Yes I was./No, I wasn’t.

      2. 主语 + 动词过去式(适用于任何人称)
        • 不规则动词的过去式需单独记忆

          eg:
              say -> said
              do -> did
              go -> went
              come -> came

        • 规则动词过去式构成
        1. 一般动词词尾加-ed

          eg:
              call -> called
              answer -> answered
              finish -> finished

        2. 以e结尾的动词只加-d

          eg:
              phone -> phoned
              believe -> believed
              agree -> agreed

        3. 以辅音字母+y结尾的动词,变y为i再加-ed

          eg:
              cry -> cried
              try -> tried
              study -> studied
              enjoy -> enjoyed
              play -> played

        4. 重读闭音节(音节:元音的个数,一个元音是单音节。重读闭音节:最后三个单词辅音元音辅音的结构,并且重读)结尾的动词,双写最后一个辅音字母再加-ed

          eg:
              stop -> stopped
              beg -> begged
              fit -> fitted

      3. 动词加-d/-ed后读音
      4. 清辅音结尾读[t]

        eg:
            knocked/helped/asked

      5. 浊辅音和元音结尾读[d]

        eg:
            cleaned/tried/begged

      6. [t]/[d]后读[id]

        eg:
            wanted/minded

      7. 否定:动词前加didn’t,把 动词改回原形

        eg:
            I didn’t work yesterday.
            He didn’t come.
            We didn’t finish the work.

      8. 疑问:主语前加did, 动词改回原形,句末问号

        eg:
            Did he phone you yesterday?
            What did you day?
            What did you do yesterday?

New Concept English - Grammar 4

时态

即时间和形态,不同时间所体现出来的形态不一样。英文中时态的不同靠动词来体现。

  • 现在进行时态
    • 结构:am/is/are + doing(动词现在分词)
      • I am doing …
      • He/She/It is doing …
      • We/You/They are doing …
      • am/is/are在此为助动词,没有实际意义,帮助动词doing一起作谓语。
      • 表示“是”时为系动词
      • 但不论何种用途,主谓一致的规则不变
    • 动词现在分词构成
      1. 一般动词在词尾加-ing
        • read -> reading
        • cook -> cooking
        • climb -> climbing
      2. 以不发音的e结尾的动词,去e再加-ing
        • make -> making
        • type -> typing
        • come -> coming
      3. 重读闭音节(音节:元音的个数,一个元音是单音节。重读闭音节:最后三个单词辅音元音辅音的结构,并且重读)结尾的动词,双写最后一个辅音字母再加-ing
        • put -> putting
        • run -> running
        • begin -> beginning
        • sharpen -> sharpening(重音在前面,不在pen上)
    • 用法
      1. 表示现在正在做的事

        eg:
            Mom is cooking in the kitchen.
            Susan is reading a book.
            I am watching TV.
            They are cleaning the office.
            My dog is running after a cat.

      2. 表示现阶段正在做的事

        eg:
            I am studying English.
            Prter is running after Susan.

    • 否定:在am/is/are之后加not,缩写n’t

      eg:
          He isn’t sleeping.
          I am not watching TV.
          They aren’t running.

    • 疑问句:把am/is/are提至主语前面,句末加问号

      eg:
          Are you reading a book?
          What are you dong?
          What is Sally doing?

  • be going to do …
    1. 表示“计划”,打算做某事,是将来时态的一种体现形式
    2. be为助动词,随着主语的变化而变化
      • I am going to …
      • He/She/It is gong to …
      • We/They/You are going to …
    3. to后一定要用动词原形
    4. 否定疑问句在be上发生变化

      eg:
          I am going to cook.
          Susan is going to write a letter.
          What are you going to do?
          I am not going to cook tonight.

    5. go,come,leave等词的be going to结构用进行时态来代替。

      eg:
          I’m going. 不说I’m going to go.
          I’m coming. 不说I’m going to come.

New Concept English - Grammar 3

a/an/the

  • a/an 不定冠词
    • 只修饰可数名词单数
    • 用于第一次出现的人或物前
  • the 定冠词
    • 可修饰任何名词
    • 用于特指的人或物前
    • 用于重新提到的人或物前
    • 用于世界上独一无二的事物前

there be结构

  • there be 结构用来说明人或物的存在,译成“有…” (have表达的是拥有,和there be不同)
  • 结构本身为倒装,主语是be之后的名词或代词
  • 句末常有介词短语作状语,说明所在的地点
  • there is + 可数名词单数或不可数名词
  • there are + 可数名词复数

    eg:
        There is a dog in the graden.
        There is some bread on the table.
        There are two students in the classroom.

there be句型的疑问和否定

  • 疑问: 把be提到there前面,句末加问号
  • 否定: 在be后面加not,缩写n’t

    eg:
        Is there a pen on the desk?
        There isn’t a pen on the desk.
        Are there any pictures on the wall?
        There aren’t any pictures on the wall.

  • any / some
    • any + 可数名词的复数和不可数名词,用在否定句,疑问句中
    • some 用在肯定句中

祈使句

  • 祈使句省略主语you,所以以动词原型开头。往往表示请求,命令,建议,叮嘱等。

    eg:
        Open the door.
        Look!
        Sit down, please.
        Give me your hand.

  • 否定时在句首加don’t或 do not(语气更强)

    eg:
        Don’t do that.
        Do not do that.(语气更强)

  • 祈使句若有两个动词常用and连接

    eg:
        Come and see my new dress.
        Go and wash your hands.
        Wait and see.

New Concept English - Grammar 2

名词所有格和代词所有格

  • 一般用于有生命的人或物,翻译成“…的”
  • 名词所有格 形式:人名+’s (Tim’s)
  • 代词所有格 形式:形容词性物主代词(my, your,his等)和名词性物主代词(mine,yours等)
  • 名词所有格 作用:可相当于形容词或名词,也即可定语修饰名词也可单独用
  • 名词所有格 作用:只相当语形容词,也即只作定语修饰名词。

    eg
        Whose shirt is that?
        It’s Tim’s. (作名词,后面可接不可接名词)
        It’s Tim’s shirt. (作形容词,后面需接名词)
        It’s his shirt. (作形容词,后面需接名词)

人称代词

personal pronouns

名词变复数

  • 可数名词有单数和复数两种形式,指一个以上事物的时候用复数
  • 名词变复数的规则
    1. 一般名词词尾加 -s 构成复数

      eg:
          friend -> friends
          case -> cases

    2. 以-s,-x,-ch,-sh和部分o结尾的词词尾加-es

      eg:
          dress -> dresses
          box -> boxes
          watch -> watches
          dish ->dishes
          tomato -> tomatoes
          potato -> totatoes
          photo -> photos

    3. 以辅音字母+y结尾的单词,把变i再加-es;以元音字母+y结尾的单词复数加-s

      eg:
          baby -> baies
          city -> cities
          boy -> boys
          key -> keys

    4. 以-f,-fe结尾的名词把-f,-fe变为v再加-es

      eg:
          wife -> wives
          wolf -> wolves

    5. man,woman以及其结尾的名词把man变为men,把woman变成women

      eg:
          policeman -> policemen
          policewoman -> policewomen

    6. 不规则名词复数需单独记忆

      eg:
          tooth -> teeth
          child -> children

New Concept English - Phonogram

元音

  • 前元音:[i:] [i] [e] [æ]
    • [i:]
      音标特征: 前元音 舌位高 不圆唇 长元音
      发音要诀: 舌前部抬得最高,牙齿近乎全合。舌尖抵下齿。舌前部向硬颚尽量抬起,但比汉语普通话 “i”音稍低,没有摩擦。嘴唇向两旁伸开,成扁平行,做微笑状,发[i:]长音。
    • [i]
      音标特征: 前元音 半高音 扁平唇 短元音
      发音要诀: 舌前部比[i:]稍低,比[e]高,舌尖抵下齿,嘴唇扁平分开。牙床也开得稍大一些比[i:]稍宽,比[e]窄。上下齿之间的距离大约可以容纳一个小指尖。使下颚稍稍下垂,舌前部也随之稍稍下降,即可发出短促[i]音。
    • [e]
      音标特征: 前元音 半高音 不圆唇 短元音
      发音要诀: 舌尖抵下齿,舌前部稍抬起,舌后接近硬颚,比[i:]低。牙床也开得半开半合,比[i:]宽,整体做微笑状。上下齿之间的距离大约相当于一个食指尖。
    • [æ]
      音标特征: 前元音 低舌音 不圆唇 短元音
      发音要诀: 舌前部最低,双唇向两旁平伸,成扁平行。牙床开的最大。软颚升起,唇自然开放,上下齿之间的距离大约相当于一个食指加中指。
    • 前元音小结:
      英语中有四个前元音,即:[i:] [i] [e] [æ]
      发前元音时必须注意:
      1. 舌尖要抵住下齿
      2. 舌前部向硬颚部分抬起
      3. 双唇不要收圆,发[i:] [i] [e]时双唇平展,发[æ]时口形要张大,扁唇
      4. 唇形舌位保持不变,否则就要发成双元音
  • 中元音:[ə:] [ə]
    所谓中元音是指发音时要使用舌中部。也就是说舌中部要向硬腭抬起,舌尖要抵住上齿,口型圆。
    • [ə:]
      音标特征:中元音 半高音 不圆唇 长元音
      发音要诀:舌身平放,舌中部稍微抬起,成自然状态,口半开半闭。发长音[E:]。
    • [ə]
      音标特征:中元音 半低音 不圆唇 短元音
      发音要诀:口半开半闭,牙床较张开,舌身平放,舌中部稍微抬起,成自然状态,这个音和汉语普通话轻声说 “么”中的短促音 “e” 相似,但英语的[ə]在词末时发音比普通话的 “e”音长。
  • 后元音: [a:] [ʌ] [u:] [u] [ɔ:] [ɔ]
    • [a:]
      音标特征:后元音 低音 不圆唇 长元音
      发音要诀:牙床大开,口张大,双唇张开而不圆。舌身平放舌尖后缩,舌后微升,舌身微离下齿。注意长度,不要发的太短。
    • [ʌ]
      音标特征:后元音 半低音 不圆唇 短元音
      发音要诀:双唇平放,牙床半开,开口程度和[æ]相似,双唇向两旁平伸。舌后部的靠前部分稍稍抬起,舌尖和舌端两侧触下齿,舌尖抵住下牙龈,发短促音[Q]。从短元音[R]出发,将圆唇改为扁唇,即可发出[Q]音。
    • [u:] 音标特征:后元音 高音 圆唇 长元音
      发音要诀:双唇成圆形,收得较[u]更圆更小,双唇向前突出,牙床近于半合。舌后部比[u]抬的更高,舌尖不触下齿,发长音[u:]。注意长度,不要发的太短,口腔肌肉要始终保持紧张状态,自然而有力。
    • [u]
      音标特征:后元音 高音 圆唇 短元音
      发音要诀:双唇成圆形,稍向前突出,牙床近于半合。舌尖不触下齿,舌后部向软颚抬起,舌身后缩。舌尖离开下齿自然而不用力,发短促音[u]。
    • [ɔ:]
      音标特征:后元音 半低音 圆唇 长元音
      发音要诀:双唇向外突出成圆形,稍稍收圆,介于开闭,圆唇之间。舌后升起,比[R]略高,舌尖不触下齿。牙床半开渐至全开,舌尖卷上后在过渡微卷后。
      注意:双唇收得要更圆更小,并用力向前突出。注意长度,不要发得太短。
    • [ɔ]
      音标特征:后元音 低音 圆唇 短元音
      发音要诀:口张大,舌身尽量降低并后缩。先发[a:]音,然后将舌身稍稍后缩,双唇稍稍收圆(不要突出),即可发音。
    • 双元音的一般特点是:
      1. 由两个成分组成,发音时由一个元音向另一个元音滑动,发音过程口形有变化
      2. 前重后轻,第一个成分发音响亮清澈,第二个成分较轻弱模糊
      3. 前长后短,第一个成分发音较长,第二个成分发音较短
  • 合口双元音为: [ei] [ai] [ɔɪ] [au] [əu] [ei] 发音要诀:舌尖抵住下齿,牙床半开半合。双唇扁平,口形由[e]向[i]滑动。发音过程中下颚向上合拢,舌位也随之稍稍抬高。
    • [ai]
      发音要诀:将口张开略圆,舌后微升,舌尖向后收缩,由[a]平稳过渡到[i]音。开始部分[a]是个前元音,和普通话 “a”音相仿,但舌位更靠前,发音时舌尖必须抵住下齿。
    • [ɔɪ]
      发音要诀:双唇外突成圆形,发[R]音,逐渐过渡为双唇扁平分开,发[i]短音。
      注意:开始部分舌位在[R] 和[R:]之间,由上述部分向 [i]音滑动,由圆唇变成扁唇。
    • [au]
      发音要诀:将口张开略圆,逐渐合拢,双唇逐渐成圆形,不要一开始就把双唇收圆。开始部分和[ai]中的[a]音相同,由[a]平稳过渡到[u]音。滑动时双唇逐渐收成圆形,并把舌后部稍稍抬起。
    • [əu]
      发音要诀:口半开半圆,舌后微微上升,过渡成双唇成圆形,发英语字母” O” 的长音。
  • 集中双元音为:[iə] [ɛə] [uə]
    • [iə]
      发音要诀:双唇张开,牙床由窄至半开,舌抵下齿逐渐过渡上卷,从[i]音过渡到[E]音。发[i]时注意用扁平唇,嘴不要张得太大,以免发成[æ],[e]。
    • [ɛə]
      发音要诀:双唇张开后略圆,牙床张开一定宽度,舌尖卷上慢慢卷后。
    • [uə]
      发音要诀:双唇成圆形,牙床近于半合,舌尖不触下齿,由[u]音很快向[E]滑动。
      注意[u]不要发成[u:]音,或普通话的 “u”。

辅音

  • 爆破音: [p] [b] [t] [d] [k] [g]
    • [p]
      音标特征:双唇爆破清辅音
      发音要诀:双唇紧闭,然后突然放开,使气流突破双唇外泄。[p]是清辅音发音时声带不震动。注意在[s] 音的后边,[p]音要读成相应的浊辅音 [b]。
    • [b]
      音标特征:双唇爆破浊辅音
      发音要诀:双唇紧闭,然后突然放开,使气流突破双唇外泄。[b]是浊辅音发音时声带震动。
    • [t]
      音标特征:舌尖齿龈爆破清辅音。
      发音要诀:双唇微开,先用舌尖紧贴上齿龈,形成阻碍,然后突然下降,气流冲出口腔。[t]是清辅音,发音时声带不震动。
      注意在[s] 音的后边,[t]音要读成相应的浊辅音[d]。
    • [d]
      音标特征:舌尖齿龈爆破浊辅音。
      发音要诀:双唇微开,先用舌尖紧贴上齿龈,形成阻碍,然后突然下降,气流冲出口腔。[d]是浊辅音,发音时声带震动。
    • [k]
      音标特征:舌后软颚爆破清辅音
      发音要诀:舌后部隆起,舌根紧贴软颚,形成阻碍,然后突然张开,气流冲出口腔。声带不产生震动,属于清辅音。在[s] 音后面读相应的浊辅音[g]
    • [g]
      音标特征:舌后软颚爆破浊辅音
      发音要诀:舌后部隆起,舌根紧贴软颚,形成阻碍,然后突然张开,气流冲出口腔。声带震动,属于浊辅音。
    • 爆破音小结:
      1. [p] [t] [k] 是清辅音,发音时声带不震动,送气要强。在词末时不要加上元音[[]。
      2. [b] [d] [g] 是浊辅音,发音时声带必须震动。在词末时不要加上元音[[]。
  • 摩擦音: [f] [v] [W] [T] [F] [V] [s] [z] [h] [r]
    • [f]
      音标特征:唇齿摩擦清辅音
      发音要诀:上齿轻触下唇,用力将气流从唇齿之间吹出,引起摩擦成音。[f] 是清辅音,发音时声带不震动。
    • [v]
      音标特征:唇齿摩擦浊辅音
      发音要诀:上齿轻触下唇,用力将气流从唇齿之间吹出,引起摩擦成音。[v] 是浊辅音,发音时声带震动。
    • [θ]
      音标特征:舌尖齿背摩擦清辅音
      发音要诀:舌尖轻上齿背,气流由舌齿间窄缝泄出,摩擦成音。声带不震动,属于清辅音。
    • [ð]
      音标特征:舌尖齿背摩擦浊辅音
      发音要诀:舌尖轻触上齿背,气流由舌齿间窄缝泄出,摩擦成音。声带震动,属于浊辅音。
    • [ʃ]
      音标特征:舌端齿龈后部摩擦清辅音
      发音要诀:舌尖和舌端抬向上齿龈较后部分,整个舌身抬起靠近上颚,开成一条狭长的通道,用力将气息送出来。气流由此通道流过,引起摩擦成音。双唇微开,稍向前突出,略成长方形,注意不要扁唇,以免发成汉语里的 “徐”(太圆)和 “希”(太扁)音。[F]是清辅音,发音时声带不震动。
    • [3]
      音标特征:舌端齿龈后部摩擦浊辅音
      发音要诀:舌尖和舌端抬向上齿龈较后部分,整个舌身抬起靠近上颚,开成一条狭长的通道,用力将气息送出来。气流由此通道流过,引起摩擦成音。双唇微开,稍向前突出,略成长方形。[V]是浊辅音,发音时声带震动。
    • [s]
      音标特征:舌端齿龈摩擦清辅音
      发音要诀:双唇微开,上下齿接近于合拢状态,舌端靠近齿龈(不要贴住),气流由齿龈之间泄出,摩擦成音。发音时声带不震动,属于清辅音。
    • [z]
      音标特征:舌端齿龈摩擦浊辅音
      发音要诀:双唇微开,上下齿接近于合拢状态,舌端靠近齿龈(不要贴住),气流由齿龈之间泄出,摩擦成音。声带震动,属于浊辅音。
    • [h]
      音标特征:声门摩擦辅音
      发音要诀:发这个音时气流从两条声带间的缝隙(声门)间通过,气流不受阻碍,自由逸出口腔,只在通过气门时发出轻微的摩擦。摩擦声门而成,但是声带不振动,是个清辅音,很像喘一口气,口形不定,随后面的元音而变化。
      注意: [h] 和汉语里的 “h” 音的区别。汉语的“h”音比英语的[h]紧张有力。发英语时舌后部和软颚间不产生摩擦。
    • [r]
      音标特征:舌尖齿龈后部摩擦辅音
      发音要诀:唇形稍圆,舌尖向上齿龈后部上卷,舌前部下陷,舌身略凹,舌身两侧收拢,发音时舌端抬起,向后面的硬颚弯曲,发生磨擦而成。要注意舌的动作是由前向后弯,声带振动,是个浊辅音。
      双唇略突出,气流由舌面与硬颚间泄出,震动声带。
    • 摩擦音小结:
      英语中有十个摩擦音即: [f][v][θ][ð][ʃ][3][s][z][h][r]
      发摩擦音时必须注意:
      1. 口腔通道不完全阻塞,留有窄小空隙,气流从中泄出时摩擦或震动成音
      2. 摩擦音可以延长而发音器官位置不变。
  • 破擦音: [t] [d][tr][dr][ts][dz]
    • [t]
      音标特征:舌端齿龈破擦清辅音
      发音要诀:双唇微开,舌尖舌端抬起,先用舌尖抵上齿龈,形成阻碍,然后张开,使气流外泄而成音。气流冲破这个阻碍后,舌和齿龈间仍保持一个狭缝,发出摩擦的声音。
    • [d]
      音标特征:舌端齿龈破擦浊辅音
      发音要诀:双唇微开,舌尖舌端抬起,先用舌尖抵上齿龈,形成阻碍,然后张开,使气流外泄而成音。气流冲破这个阻碍后,舌和齿龈间仍保持一个狭缝,发出摩擦的声音。
    • [tr]
      音标特征:齿龈后部破擦清辅音
      发音要诀:舌身采取发[r] 的姿势,但舌尖上翘贴在齿龈后部,气流冲破这个阻碍发出短促的[t] 后立即发[r]。声带不震动,属于清辅音。
    • [ts]
      音标特征: 舌端齿龈破擦清辅音
      发音要诀: 舌端先贴住齿龈,堵住气流,然后略微位下降,气流随之泄出口腔。声带不震动,属于清辅音。
    • [dz]
      音标特征:舌端齿龈破擦浊辅音
      发音要诀:舌端先贴住齿龈,堵住气流,然后略微位下降,气流随之泄出口腔。声带震动,属于浊辅音。
  • 鼻辅音: [m] [n] [ŋ]
    • [m]
      音标特征:双唇鼻辅音
      发音要诀:双唇闭拢,舌放平,软颚下垂,气流从鼻腔泄出。声带震动发音。
      注意:在词末时须略微延长,以防止吞音。

    • [n]
      音标特征:舌尖齿龈鼻辅音
      发音要诀:双唇微开,舌尖紧贴上龈,形成阻碍,软颚下垂,使气流从鼻腔中发出。在词末时须略微延长,以防止吞音。
    • [ŋ]
      音标特征:舌后软颚鼻辅音
      发音要诀:舌位和[k] [g]相同,但软颚下垂,堵住口腔通道,气流从鼻腔泄出。属于浊辅音,发音时声带震动。和汉语普通话的 “ng” (“昂” “影”的尾音)相似,在词末发音时清晰有力。
    • 鼻辅音小结:
      英语中一共由三个鼻辅音,即: [m] [n] [N],发鼻辅音时必须注意:
      1. 软颚下垂,口腔通道完全阻塞,气流从鼻腔泄出。
      2. 有声带震动,都是浊辅音。
      3. 鼻辅音在词末时,发音要略微延长。
  • 舌边音: [l]
    音标特征:舌端齿龈边辅音 (也叫旁流音)
    发音要诀:双唇微开,舌端紧抵上齿龈,气流从舌的一侧泄出,属于浊辅音,震动声带。
    注意: [l]有两个发音,即清晰音[l]和含糊音[l]。发清晰音[l] 时:舌前向硬颚抬起,清晰音出现在元音前面。发含糊音[l]时:舌前下降,舌后上台,舌面形成凹槽,可以和元音一样长,含糊音出现在辅音前面和词的末尾。

  • 半元音: [w] [ j ] [w]
    音标特征:舌后软颚半元音
    发音要诀:发音部位和元音[u:]相似,即舌后部向软颚抬起,抬得很高,双唇收的很圆很小,并向前突出,成尖圆形。
    这是一个半元音,因为发[w]时气流不受发音器官的阻碍,像元音一样,只是气流通过唇角时有一些磨擦。
    半元音没有长度,只能向元音过渡且都是浊音。辅音字母w和wh组合读此音,气息流过,声带震动。
    注意:一经发出,立即向后面的元音滑动。

元音字母a、e、i、o、u在重读音节中的发音规则

  • 重读开音节:[ei] [i:] [ai] [əu] [ju:] [u:]
  • 重读闭音节:[æ] [e] [i] [ɔ] [ʌ] [u]
  • 注意:除了符合读音规则的词以外,还有一些词是例外的,需要逐个记忆。
  • 重读开音节:一个辅音加e结尾的单词,元音字母a,e,i,o,u 经常发长音
    如: cake theme bike close consume
  • 重读闭音节:元音字母a,e,i,o,u 经常发短音
    如: have bed sit love bus
  • 常见字母组合在单词中的读音:是由两个字母搭配而成的一种固定组合
    1. 两个辅音字母在一起,通常只发其中一个辅音字母的音。两个不同的辅音字母在一起时,只发其中一个辅音字母的音。
    2. 两个元音字母在一起,通常只发其中一个元音字母的基本音。两个元音字母在一起时,发第一个元音字母的基本音。如:boat people soul sea。
    3. 辅音字母+le时,辅音字母前边的元音字母经常发长音。辅音字母+le时,辅音字母前边的元音字母有时也可发短音。如:couple double possible
    4. 两个辅音前的元音字母经常发短音。两个相同的辅音字母前的元音字母发短音。如:carry little boss summer
    5. 字母c在e,i,y前边读[s],其他读作[k]。
    6. 字母g在e ,i ,y前边读[dV ],其他读作[g]。
    7. 字母组合ti si ci不在字的第一个音节时读 [F]。如:action、attention。
    8. s后的清辅音[k][p][t] 要浊化为[g][b][d]音。如:sky、skill。
    9. 字母组合ai一般发[ei] [e] [i]音。如:[ei] straight
    10. 字母组合er发长音[ə:]和短音[ə]两种情况:
      在重读音节中er一般发长音[ə:]
      在弱读音节中一般发短音[ə]
    11. 字母组合or发[ɔ:]和[ə:]两种音。
    12. 字母组合ou常发 [au] [ʌ]
    13. 字母组合tion有两种发音[tʃn],[ʃən]
    14. 字母组合ng发[η]和[ηɡ]两种音
    15. 有些单词中某些字母不发音,如:hour、climb、build等。

失音规则

  • 失音规则:某一个或者几个音脱落,把单词短语和句子快速连缀成串。
  • 单词间的失音有四种类型:
    1. 前一词以“持续音+[t] [d]结尾,后一词以辅音开头,则其中[t] [d]失音有:
    • [st]+辅音 [ft]+辅音 [nd]+辅音 [ld]+辅音 [zd]+辅音 [td]+辅音
      [wt]+辅音 [md]+辅音 [nd]+辅音 [nt]+辅音 [lt]+辅音 [vd]+辅音
      如: last class、next day、bend back、settled there、refused both。
      2. 前一词以破音/擦音+ [t] [d]结束,后一词以辅音开头,则其中[t] [d]失音。
    • [pt]+辅音 [tft]+辅音 [kt]+辅音(如完结于skt则脱k而不是t)
      [bd]+辅音 [gd]+辅音 [dvd]+辅音 [vd]+辅音
      如:kept quiet swept valley rubbed gently
      注:后一词如始于h则[t][d]很少脱落如:kept her waiting 3. 动词否定结尾的缩写形式 n’t 中的[t]失音,无论后一词以元音还是辅音开头。 4. 前一词以[t] 结束,后一词以[t] 或[d]开头,则前面的[t]往往失音。
      如:I’ve got to go。 What do you want?

不完全爆破

  • 实质上也存在不发音现象: 如果前面单词结尾和后面单词开头是两个或者同一个破擦音( [p] [b] [t] [d] [k] [g]) 则前一音保留口型和发音时间却并不发音,但是听者能感到这个音的存在。

New Concept English - Grammar 1

一般疑问句

  • 陈述句变疑问句采取句式的变化(把谓语中的动词提到句首)
  • 最基本的含有be动词(is,am,are)的句子把be提到句首,首字母大写,句末加问好
  • I做主语后面接am
  • 主语是第三人称单数(he,she,it,人名)后面接is
  • 主语是第一,二,三人称复数,第二人称单数后面接are
  • 读时用升调
  • 回答时用Yes/No

    eg:
        This is my bike.
        Is this your bike?
        Yes, it is(my bike).
        No, it isn’t(my bike).

否定句

  • 否定句通过not来体现,not需放在助动词之后。(目前学过的助动词只有be)

    eg:
        This is my umbrella.
        This isn’t my umbrella.

选择疑问句

  • 选择疑问句仅仅在一般疑问句的末尾加or…即可
  • 语调:(or)前升后调
  • 回答时不用yes或no

    eg:
        Is this a French car or a German car?
        This is a French car.
        This isn’t a Greman car.

区分a/an

  • a/an称为不定冠词,意思为“一个”,只放在可数名词单数前

    名词:表示事物的名称的词

  • 用a/an取决去后面单词的第一个音标(为了发音方便),若是元音音标用an

what引导的特殊疑问句

  • what为“特殊疑问词”,英文中特殊疑问词引导的问句为特殊疑问句。读时用降调
  • what特殊疑问句的结构:What(+名词)+助动词

    eg:
        What make is your car?
        What color is your car?
        Waht color are your shoes?
        What’s your name?

陈述句变特殊疑问句三部曲

  1. 根据意思确定特殊疑问词
  2. 特殊疑问词置于句首
  3. 把助动词提至主语前

    eg:
        My car is a Benz. -> What make is your car?
        That is a dog. -> What is that.

how引导的特殊疑问句

  • how引导的特殊疑问句经常用于询问健康状况,生活,工作等。
  • how特殊疑问句的结构:How+助动词+主语?

    eg:
        How are you?
        How is your work?

  • how之后也可跟形容词,用于询问具体的多高,多长等

    eg:
        How long is your hair?
        How is Helen’s car?

whose引导的特殊疑问句

  • whose引导的特殊疑问句用于问某物时谁的。
  • whose特殊疑问句的结构:Whose+名词+助动词+主语?

    eg:
        Whose shirt is that? / Whose is that shirt.

  • 回答时往往用名词所有格或形容词性物主代词

    eg:
        It’s Tim’s shit.

service-map

metrics-tracing-and-logging

  • I think that the defining characteristic of metrics is that they are aggregatable: they are the atoms that compose into a single logical gauge, counter, or histogram over a span of time. As examples: the current depth of a queue could be modeled as a gauge, whose updates aggregate with last-writer-win semantics; the number of incoming HTTP requests could be modeled as a counter, whose updates aggregate by simple addition; and the observed duration of a request could be modeled into a histogram, whose updates aggregate into time-buckets and yield statistical summaries.

  • I think that the defining characteristic of logging is that it deals with discrete events. As examples: application debug or error messages emitted via a rotated file descriptor through syslog to Elasticsearch (or OK Log, nudge nudge); audit-trail events pushed through Kafka to a data lake like BigTable; or request-specific metadata pulled from a service call and sent to an error tracking service like NewRelic.

  • I think that the single defining characteristic of tracing, then, is that it deals with information that is request-scoped. Any bit of data or metadata that can be bound to lifecycle of a single transactional object in the system. As examples: the duration of an outbound RPC to a remote service; the text of an actual SQL query sent to a database; or the correlation ID of an inbound HTTP request.

service map tools

  • zipkin
  • jaeger

paper

  • https://ai.google/research/pubs/pub36356
  • https://ai.google/research/pubs/pub40378

refer

  • http://peter.bourgon.org/blog/2017/02/21/metrics-tracing-and-logging.html
  • https://logz.io/blog/zipkin-vs-jaeger/

java类型擦除

概念

Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。

在代码中定义的List和List等类型,在编译后都会编程List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。

ArrayList<String> arrayList1 = new ArrayList<String>();  
ArrayList<Integer> arrayList2 = new ArrayList<Integer>();  
System.out.println(arrayList1.getClass()==arrayList2.getClass());  

结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下了原始类型。

类型擦除后保留的原始类型

原始类型(raw type)就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。无论何时定义一个泛型类型,相应的原始类型都会被自动地提供。类型变量被擦除(crased),并使用其限定类型(无限定的变量用Object)替换。

类型擦除引起的问题及解决方法

FTRL的scala实现

FTRL的scala实现

import breeze.linalg.{DenseVector, _}
import breeze.numerics._
import org.apache.spark.sql.{Dataset, SparkSession}
import org.apache.log4j.Logger

/**
  * Created by nickliu on 11/29/2017.
  */
object LR{

  val logger = Logger.getLogger(LR.getClass.getName)

  def fn(w: DenseVector[Double], x: DenseVector[Double]): Double ={
    /** 决策函数为sigmoid函数
      * exp 对矩阵a中每个元素取指数函数
      * dot 按照矩阵乘法的规则来运算的 Breeze a.t * b => python dot(a,b)
      */
    val sigmoid = 1.0 / (1.0 + exp(-w.t * x))
    return sigmoid
  }

  def loss(y: Double, y_hat: Double): Double ={
    /** 交叉熵损失函数 */
    return sum(DenseVector[Double](-y * log(y_hat) - (1 - y) * log(1 - y_hat)))
  }

  def grad(y: Double, y_hat: Double, x: DenseVector[Double]): DenseVector[Double] ={
    /** 交叉熵损失函数对权重w的一阶导数 */
    return (y_hat - y) * x
  }

}

object FTLR{

  val dim = 4
  var l1: Double = 0.0
  var l2: Double = 0.0
  var alpha: Double = 0.5
  var beta: Double = 1.0
  var z, n, w = DenseVector.zeros[Double](dim)//z更新权重用到的,存放梯度累加的n,最后的w

  private def predict(x: DenseVector[Double]): Double ={
    return LR.fn(w, x)
  }

  var correct = 0
  var wrong = 0
  def transform(dataSet: Dataset[String]): Double ={
    val dataList = dataSet.rdd.map({
      line =>
        val row = line.split("\\s+")
        (row(0).toDouble, row(1).toDouble, row(2).toDouble, row(3).toDouble, row(4).toDouble)
    }).collect().toList
    for (line <- dataList) {
      val predictValue = predict(DenseVector(line._1.toDouble, line._2.toDouble, line._3.toDouble, line._4.toDouble))
      val y_hat = if(predictValue > 0.5) 1.0 else 0.0
      val y = line._5.toDouble
      if(y == y_hat) correct += 1 else wrong += 1
    }
    val precision = 1.0 * correct / (correct + wrong)
    return precision
  }

  def update(x: DenseVector[Double], y: Double): Double ={
    for(i <- 0 until dim){
      var y: Double = 0
      if(abs(z(i)) > l1){
        /**
          * Breeze signum(a) => Numpy sign(a)
          * 大于0的返回1.0
          * 小于0的返回-1.0
          * 等于0的返回0.0
          *
          * Numpy sqrt()
          * 计算各元素的平方根
          */
        y = (signum(z(i)) * l1 - z(i)) / (l2 + (beta + sqrt(n(i))) / alpha)
      }
      w(i) = y
    }
    val y_hat = predict(x)
    val g = LR.grad(y, y_hat, x)
    val sigma = (sqrt(n + g * g) - sqrt(n)) / alpha
    z += g - sigma * w
    n += g * g
    return LR.loss(y, y_hat)
  }

  def fit(dataSet: Dataset[String], max_itr: Int = 100000000, eta: Double = 0.01, epochs: Int = 100): DenseVector[Double] ={
    var itr = 0
    var n = 0
    val dataList = dataSet.rdd.map({
      line =>
        val row = line.split("\\s+")
        (row(0).toDouble, row(1).toDouble, row(2).toDouble, row(3).toDouble, row(4).toDouble)
    }).collect().toList

    while(true) {
      for (line <- dataList) {
        val loss = update(DenseVector(line._1, line._2, line._3, line._4), line._5)
        if (loss < eta) {
          itr += 1
        } else {
          itr = 0
        }
        if (itr >= epochs) {
          // 损失函数已连续epochs次迭代小于eta
          println("loss have less than", eta, " continuously for ", itr, "iterations")
          return w
        } else {
          n += 1
          if (n >= max_itr) {
            println("reach max iteration", max_itr)
            return w
          }
        }
      }
    }
    w
  }

}

object Application{
  val warehouseLocation = "hdfs://nameservice1/user/hive/warehouse"

  val spark = SparkSession
    .builder
    .master("local")
    .appName("ALSExample")
    .config("spark.sql.warehouse.dir",warehouseLocation)
    .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
    .enableHiveSupport()
    .getOrCreate()

  val d = 4
  def main(args: Array[String]): Unit = {
    val data = spark.read.textFile("file:///D:/git_project/phoenix/data/mllib/train.txt").cache()
    val Array(trainData, testData) = data.randomSplit(Array(0.7,0.3))
    val ftlr = FTLR
    val startTime = System.currentTimeMillis
    val w = ftlr.fit(trainData, 100000, 0.01, 100)
    println(w)

    val precision = ftlr.transform(testData)
    val currentTime = System.currentTimeMillis
    val minutes = (currentTime - startTime).toDouble / 60000
    println("precision -> ", precision)
    println("minutes -> ", minutes)
  }
}

RMF分析方法--客户细分

根据美国数据库营销研究所ArthurHughes的研究,客户数据库中有三个神奇的要素,这三个要素构成了数据分析较好的指标:

  • 最近一次消费(Recency)
  • 消费频率(Frequency)
  • 消费金额(Monetary)

  RFM分析原多用于传统营销、零售业等领域,适用于拥有多种消费品或快速消费品的行业,只要任何有数据记录的消费都可以被用于分析。那么对于电子商务网站来说,网站数据库中记录的详细的交易信息,同样可以运用RFM分析模型进行数据分析,尤其对于那些已经建立起客户关系管理(CRM)系统的网站来说,其分析的结果将更具意义。 基本概念解释。
  RFM模型是衡量客户价值和客户创利能力的重要工具和手段。RFM分析模型主要由三个指标组成,下面对这三个指标的定义和作用做下简单解释:

最近一次消费(Recency)

  最近一次消费意指用户上一次购买的时间,理论上,上一次消费时间越近的顾客应该是比较好的顾客,对提供即时的商品或是服务也最有可能会有反应。因为最近一次消费指标定义的是一个时间段,并且与当前时间相关,因此是一直在变动的。最近一次消费对营销来说是一个重要指标,涉及吸引客户,保持客户,并赢得客户的忠诚度。

消费频率(Frequency)

  消费频率是顾客在一定时间段内的消费次数。最常购买的消费者,忠诚度也就较高,增加顾客购买的次数意味着从竞争对手处偷取市场占有率,由别人的手中赚取营业额。
  根据这个指标,我们又把客户分成五等分,这个五等分分析相当于是一个“忠诚度的阶梯”(loyaltyladder),其诀窍在于让消费者一直顺着阶梯往上爬,把销售想像成是要将两次购买的顾客往上推成三次购买的顾客,把一次购买者变成两次的。

消费金额(Monetary)

  消费金额是对电子商务网站产能的最直接的衡量指标,也可以验证“帕雷托法则”(Pareto’sLaw)——公司80%的收入来自20%的顾客。

数据获取与分析

  在从数据库中提取相关数据之前,首先需要确定数据的时间跨度,根据网站销售的物品的差异,确定合适的时间跨度。如果经营的是快速消费品,如日用品,可以确定时间跨度为一个季度或者一个月;如果销售的产品更替的时间相对久些,如电子产品,可以确定时间跨度为一年、半年或者一个季度。在确定时间跨度之后就可以提取相应时间区间内的数据,其中:
  最近一次消费(Recency),取出来的数据是一个时间点,需要由当前时间点-最近一次消费时间点来作为该度量的值,注意单位的选择和统一,无论以小时、天为单位;
  消费频率(Frequency),这个指标可以直接在数据库中COUNT用户的消费次数得到;
  消费金额(Monetary),可以将每位客户的所有消费的金额相加(SUM)求得。
  获取三个指标的数据以后,需要计算每个指标数据的均值,分别以AVG(R)、AVG(F)、AVG(M)来表示,最后通过将每位客户的三个指标与均值进行比较,可以将客户细分为8类: | Recency|Frequency|Monetary|客户类型 | |:——:|:——-:|:——:|:———:| | ↑ | ↑ | ↑ |重要价值客户| | ↑ | ↓ | ↑ |重要发展客户| | ↓ | ↑ | ↑ |重要保持客户| | ↓ | ↓ | ↑ |重要挽留客户| | ↑ | ↑ | ↓ |一般价值客户| | ↑ | ↓ | ↓ |一般发展客户| | ↓ | ↑ | ↓ |一般保持客户| | ↓ | ↓ | ↓ |一般挽留客户| “↑”表示大于均值,“↓”表示小于均值

结果的展示

  RFM模型包括三个指标,无法用平面坐标图来展示,所以这里使用三维坐标系进行展示,一种X轴表示Recency,Y轴表示Frequency,Z轴表示Monetary,坐标系的8个象限分别表示8类用户,根据上表中的分类,可以用如下图形进行描述:
rmf
  RFM分析也存在着一定的缺陷,它只能分析有交易行为的用户,而对访问过网站但未消费的用户由于指标的限制无法进行分析,这样就无法发现潜在的客户。所以在分析电子商务网站的用户时,由于网站数据的丰富性——不仅拥有交易数据,而且可以收集到用户的浏览访问数据,可以扩展到更广阔的角度去观察用户,这方面的定量分析会在之后的网站用户分析中进行详细叙述。

Spark标签和索引的转化

StringIndexer

​StringIndexer是指把一组字符型标签编码成一组标签索引,索引的范围为0到标签数量,索引构建的顺序为标签的频率,优先编码频率较大的标签,所以出现频率最高的标签为0号。如果输入的是数值型的,我们会把它转化成字符型,然后再对其进行编码。在pipeline组件,比如Estimator和Transformer中,想要用到字符串索引的标签的话,我们一般需要通过setInputCol来设置输入列。另外,有的时候我们通过一个数据集构建了一个StringIndexer,然后准备把它应用到另一个数据集上的时候,会遇到新数据集中有一些没有在前一个数据集中出现的标签,这时候一般有两种策略来处理:第一种是抛出一个异常(默认情况下),第二种是通过掉用 setHandleInvalid(“skip”)来彻底忽略包含这类标签的行。

import org.apache.spark.SparkConf
import org.apache.spark.SparkContext
import org.apache.spark.sql.SQLContext
import org.apache.spark.ml.feature.StringIndexer

scala> val sqlContext = new SQLContext(sc)
sqlContext: org.apache.spark.sql.SQLContext = org.apache.spark.sql.SQLContext@2869d920

scala> import sqlContext.implicits._
import sqlContext.implicits._

scala> val df1 = sqlContext.createDataFrame(
     |       Seq((0, "a"), (1, "b"), (2, "c"), (3, "a"), (4, "a"), (5, "c"))
     |     ).toDF("id", "category")
df1: org.apache.spark.sql.DataFrame = [id: int, category: string]

scala> val indexer = new StringIndexer().
     |       setInputCol("category").
     |       setOutputCol("categoryIndex")
indexer: org.apache.spark.ml.feature.StringIndexer = strIdx_95a0a5afdb8b

scala> val indexed1 = indexer.fit(df1).transform(df1)
indexed1: org.apache.spark.sql.DataFrame = [id: int, category: string, categoryIndex: double]

scala> indexed1.show()
+---+--------+-------------+
| id|category|categoryIndex|
+---+--------+-------------+
|  0|       a|          0.0|
|  1|       b|          2.0|
|  2|       c|          1.0|
|  3|       a|          0.0|
|  4|       a|          0.0|
|  5|       c|          1.0|
+---+--------+-------------+

scala> val df2 = sqlContext.createDataFrame(
     |       Seq((0, "a"), (1, "b"), (2, "c"), (3, "a"), (4, "a"), (5, "d"))
     |     ).toDF("id", "category")
df2: org.apache.spark.sql.DataFrame = [id: int, category: string]

scala> val indexed2 = indexer.fit(df1).setHandleInvalid("skip").transform(df2)
indexed2: org.apache.spark.sql.DataFrame = [id: int, category: string, categoryIndex: double]

scala> indexed2.show()
+---+--------+-------------+
| id|category|categoryIndex|
+---+--------+-------------+
|  0|       a|          0.0|
|  1|       b|          2.0|
|  2|       c|          1.0|
|  3|       a|          0.0|
|  4|       a|          0.0|
+---+--------+-------------+

scala> val indexed3 = indexer.fit(df1)transform(df2)
indexed3: org.apache.spark.sql.DataFrame = [id: int, category: string, categoryIndex: double]

scala> indexed3.show()
org.apache.spark.SparkException: Unseen label: d.

​ 在上例当中,我们首先构建了1个dataframe,然后设置了StringIndexer的输入列和输出列的名字。通过indexed1.show(),我们可以看到,StringIndexer依次按照出现频率的高低,把字符标签进行了排序,即出现最多的“a”被编号成0,“c”为1,出现最少的“b”为0。接下来,我们构建了一个新的dataframe,这个dataframe中有一个再上一个dataframe中未曾出现的标签“d”,然后我们通过设置setHandleInvalid(“skip”)来忽略标签“d”的行,结果通过indexed2.show()可以看到,含有标签“d”的行并没有出现。如果,我们没有设置的话,则会抛出异常,报出“Unseen label: d”的错误。

IndexToString

​ 对称的,IndexToString的作用是把标签索引的一列重新映射回原有的字符型标签。一般都是和StringIndexer配合,先用StringIndexer转化成标签索引,进行模型训练,然后在预测标签的时候再把标签索引转化成原有的字符标签。当然,也允许你使用自己提供的标签。

import org.apache.spark.SparkConf
import org.apache.spark.SparkContext
import org.apache.spark.sql.SQLContext
import org.apache.spark.ml.feature.{StringIndexer, IndexToString}

scala> val sqlContext = new SQLContext(sc)
sqlContext: org.apache.spark.sql.SQLContext = org.apache.spark.sql.SQLContext@2869d920

scala> import sqlContext.implicits._
import sqlContext.implicits._

scala> val df = sqlContext.createDataFrame(Seq(
     |       (0, "a"),
     |       (1, "b"),
     |       (2, "c"),
     |       (3, "a"),
     |       (4, "a"),
     |       (5, "c")
     |     )).toDF("id", "category")
df: org.apache.spark.sql.DataFrame = [id: int, category: string]

scala> val indexer = new StringIndexer().
     |       setInputCol("category").
     |       setOutputCol("categoryIndex").
     |       fit(df)
indexer: org.apache.spark.ml.feature.StringIndexerModel = strIdx_00fde0fe64d0

scala> val indexed = indexer.transform(df)
indexed: org.apache.spark.sql.DataFrame = [id: int, category: string, categoryIndex: double]

scala> val converter = new IndexToString().
     |       setInputCol("categoryIndex").
     |       setOutputCol("originalCategory")
converter: org.apache.spark.ml.feature.IndexToString = idxToStr_b95208a0e7ac

scala> val converted = converter.transform(indexed)
converted: org.apache.spark.sql.DataFrame = [id: int, category: string, categoryIndex: double, originalCategory: string]

scala> converted.select("id", "originalCategory").show()
+---+----------------+
| id|originalCategory|
+---+----------------+
|  0|               a|
|  1|               b|
|  2|               c|
|  3|               a|
|  4|               a|
|  5|               c|
+---+----------------+

​ 在上例中,我们首先用StringIndexer读取数据集中的“category”列,把字符型标签转化成标签索引,然后输出到“categoryIndex”列上。然后再用IndexToString读取“categoryIndex”上的标签索引,获得原有数据集的字符型标签,然后再输出到“originalCategory”列上。最后,通过输出“originalCategory”列,可以看到数据集中原有的字符标签。

OneHotEncoder

​独热编码是指把一列标签索引映射成一列二进制数组,且最多的时候只有一位有效。这种编码适合一些期望类别特征为连续特征的算法,比如说逻辑斯蒂回归。

import org.apache.spark.SparkConf
import org.apache.spark.SparkContext
import org.apache.spark.sql.SQLContext
import org.apache.spark.ml.feature.{OneHotEncoder, StringIndexer}

scala> val sqlContext = new SQLContext(sc)
sqlContext: org.apache.spark.sql.SQLContext = org.apache.spark.sql.SQLContext@2869d920

scala> import sqlContext.implicits._
import sqlContext.implicits._

scala> val df = sqlContext.createDataFrame(Seq(
     |       (0, "a"),
     |       (1, "b"),
     |       (2, "c"),
     |       (3, "a"),
     |       (4, "a"),
     |       (5, "c"),
     |       (6, "d"),
     |       (7, "d"),
     |       (8, "d"),
     |       (9, "d"),
     |       (10, "e"),
     |       (11, "e"),
     |       (12, "e"),
     |       (13, "e"),
     |       (14, "e")
     |     )).toDF("id", "category")
df: org.apache.spark.sql.DataFrame = [id: int, category: string]

scala> val indexer = new StringIndexer().
     |       setInputCol("category").
     |       setOutputCol("categoryIndex").
     |       fit(df)
indexer: org.apache.spark.ml.feature.StringIndexerModel = strIdx_b315cf21d22d

scala> val indexed = indexer.transform(df)
indexed: org.apache.spark.sql.DataFrame = [id: int, category: string, categoryIndex: double]

scala> val encoder = new OneHotEncoder().
     |       setInputCol("categoryIndex").
     |       setOutputCol("categoryVec")
encoder: org.apache.spark.ml.feature.OneHotEncoder = oneHot_bbf16821b33a

scala> val encoded = encoder.transform(indexed)
encoded: org.apache.spark.sql.DataFrame = [id: int, category: string, categoryIndex: double, categoryVec: vector]

scala> encoded.show()
+---+--------+-------------+-------------+
| id|category|categoryIndex|  categoryVec|
+---+--------+-------------+-------------+
|  0|       a|          2.0|(4,[2],[1.0])|
|  1|       b|          4.0|    (4,[],[])|
|  2|       c|          3.0|(4,[3],[1.0])|
|  3|       a|          2.0|(4,[2],[1.0])|
|  4|       a|          2.0|(4,[2],[1.0])|
|  5|       c|          3.0|(4,[3],[1.0])|
|  6|       d|          1.0|(4,[1],[1.0])|
|  7|       d|          1.0|(4,[1],[1.0])|
|  8|       d|          1.0|(4,[1],[1.0])|
|  9|       d|          1.0|(4,[1],[1.0])|
| 10|       e|          0.0|(4,[0],[1.0])|
| 11|       e|          0.0|(4,[0],[1.0])|
| 12|       e|          0.0|(4,[0],[1.0])|
| 13|       e|          0.0|(4,[0],[1.0])|
| 14|       e|          0.0|(4,[0],[1.0])|
+---+--------+-------------+-------------+

​ 在上例中,我们构建了一个dataframe,包含“a”,“b”,“c”,“d”,“e” 五个标签,通过调用OneHotEncoder,我们发现出现频率最高的标签“e”被编码成第0位为1,即第0位有效,出现频率第二高的标签“d”被编码成第1位有效,依次类推,“a”和“c”也被相继编码,出现频率最小的标签“b”被编码成全0。

VectorIndexer

VectorIndexer解决向量数据集中的类别特征索引。它可以自动识别哪些特征是类别型的,并且将原始值转换为类别索引。它的处理流程如下:

  1. 获得一个向量类型的输入以及maxCategories参数
  2. 基于不同特征值的数量来识别哪些特征需要被类别化,其中最多maxCategories个特征需要被类别化
  3. 对于每一个类别特征计算0-based(从0开始)类别索引
  4. 对类别特征进行索引然后将原始特征值转换为索引 索引后的类别特征可以帮助决策树等算法恰当的处理类别型特征,并得到较好结果。 在下面的例子中,我们读入一个数据集,然后使用VectorIndexer来决定哪些特征需要被作为类别特征,将类别特征转换为他们的索引。 ``` scala import org.apache.spark.SparkConf import org.apache.spark.SparkContext import org.apache.spark.sql.SQLContext import org.apache.spark.ml.feature.VectorIndexer import org.apache.spark.mllib.linalg.{Vector, Vectors}

scala> val sqlContext = new SQLContext(sc) sqlContext: org.apache.spark.sql.SQLContext = org.apache.spark.sql.SQLContext@2869d920

scala> import sqlContext.implicits._ import sqlContext.implicits._

scala> val data = Seq(Vectors.dense(-1.0, 1.0, 1.0),Vectors.dense(-1.0, 3.0, 1.0), Vectors.dense(0.0, 5.0, 1.0)) data: Seq[org.apache.spark.mllib.linalg.Vector] = List([-1.0,1.0,1.0], [-1.0,3.0,1.0], [0.0,5.0,1.0])

scala> val df = sqlContext.createDataFrame(data.map(Tuple1.apply)).toDF(“features”) df: org.apache.spark.sql.DataFrame = [features: vector]

scala> val indexer = new VectorIndexer(). | setInputCol(“features”). | setOutputCol(“indexed”). | setMaxCategories(2) indexer: org.apache.spark.ml.feature.VectorIndexer = vecIdx_abee81bafba8

scala> val indexerModel = indexer.fit(df) indexerModel: org.apache.spark.ml.feature.VectorIndexerModel = vecIdx_abee81bafba8

scala> val categoricalFeatures: Set[Int] = indexerModel.categoryMaps.keys.toSet categoricalFeatures: Set[Int] = Set(0, 2)

scala> println(s”Chose ${categoricalFeatures.size} categorical features: “ + categoricalFeatures.mkString(“, “)) Chose 2 categorical features: 0, 2

scala> val indexedData = indexerModel.transform(df) indexedData: org.apache.spark.sql.DataFrame = [features: vector, indexed: vector]

scala> indexedData.foreach { println } [[-1.0,1.0,1.0],[1.0,1.0,0.0]] [[-1.0,3.0,1.0],[1.0,3.0,0.0]] [[0.0,5.0,1.0],[0.0,5.0,0.0]] ``` ​从上例可以看到,我们设置maxCategories为2,即只有种类小于2的特征才被认为是类别型特征,否则被认为是连续型特征。其中类别型特征将被进行编号索引,为了索引的稳定性,规定如果这个特征值为0,则一定会被编号成0,这样可以保证向量的稀疏度(未来还会再维持索引的稳定性上做更多的工作,比如如果某个特征类别化后只有一个特征,则会进行警告等等,这里就不过多介绍了)。于是,我们可以看到第0类和第2类的特征由于种类数不超过2,被划分成类别型特征,并进行了索引,且为0的特征值也被编号成了0号。

Hive On MR调优

使用EXPLAIN

限制调整

很多情况下LIMIT语句还是需要执行整个查询语句,然后再返回部分结果。 开启optimize后,当使用limit时,可以对元数据进行抽样。缺点是有可能输入中的有些数据永远不会被处理到。

<property>
	<name>hive.limit.optimize.enable</name>
	<value>true</value>
</property>
<property>
	<name>hive.limit.row.max.size</name>
	<value>100000</value>
</property>
<property>
	<name>hive.limit.optimize.limit.file</name>
	<value>10</value>
</property>
num-executors/spark.executor.instances
  • 参数说明:该参数用于设置Spark作业总共要用多少个Executor进程来执行。Driver在向YARN集群管理器申请资源时,YARN集群管理器会尽可能按照你的设置来在集群的各个工作节点上,启动相应数量的Executor进程。这个参数非常之重要,如果不设置的话,默认只会给你启动少量的Executor进程,此时你的Spark作业的运行速度是非常慢的。

  • 参数调优建议:每个Spark作业的运行一般设置50~100个左右的Executor进程比较合适,设置太少或太多的Executor进程都不好。设置的太少,无法充分利用集群资源;设置的太多的话,大部分队列可能无法给予充分的资源。配置spark.executor.cores为host上CPU core的N分之一,同时也设置spark.executor.memory为host上分配给Spark计算内存的N分之一,这样这个host上就能够启动N个executor

executor-memory/spark.executor.memory
  • 参数说明:该参数用于设置每个Executor进程的内存。Executor内存的大小,很多时候直接决定了Spark作业的性能,而且跟常见的JVM OOM异常,也有直接的关联。

  • 参数调优建议:每个Executor进程的内存设置4G8G较为合适。但是这只是一个参考值,具体的设置还是得根据不同部门的资源队列来定。可以看看自己团队的资源队列的最大内存限制是多少,num-executors乘以executor-memory,是不能超过队列的最大内存量的。此外,如果你是跟团队里其他人共享这个资源队列,那么申请的内存量最好不要超过资源队列最大总内存的1/31/2,避免你自己的Spark作业占用了队列所有的资源,导致别的同学的作业无法运行。

executor-cores/spark.executor.cores
  • 参数说明:该参数用于设置每个Executor进程的CPU core数量。这个参数决定了每个Executor进程并行执行task线程的能力。因为每个CPU core同一时间只能执行一个task线程,因此每个Executor进程的CPU core数量越多,越能够快速地执行完分配给自己的所有task线程。

  • 参数调优建议:Executor的CPU core数量设置为2~4个较为合适。同样得根据不同部门的资源队列来定,可以看看自己的资源队列的最大CPU core限制是多少,再依据设置的Executor数量,来决定每个Executor进程可以分配到几个CPU core。同样建议,如果是跟他人共享这个队列,那么num-executors * executor-cores不要超过队列总CPU core的1/3~1/2左右比较合适,也是避免影响其他同学的作业运行。

driver-memory
  • 参数说明:该参数用于设置Driver进程的内存。

  • 参数调优建议:Driver的内存通常来说不设置,或者设置1G左右应该就够了。唯一需要注意的一点是,如果需要使用collect算子将RDD的数据全部拉取到Driver上进行处理,那么必须确保Driver的内存足够大,否则会出现OOM内存溢出的问题。

spark.default.parallelism
  • 参数说明:该参数用于设置每个stage的默认task数量。这个参数极为重要,如果不设置可能会直接影响你的Spark作业性能。

  • 参数调优建议:Spark作业的默认task数量为500~1000个较为合适。很多同学常犯的一个错误就是不去设置这个参数,那么此时就会导致Spark自己根据底层HDFS的block数量来设置task的数量,默认是一个HDFS block对应一个task。通常来说,Spark默认设置的数量是偏少的(比如就几十个task),如果task数量偏少的话,就会导致你前面设置好的Executor的参数都前功尽弃。试想一下,无论你的Executor进程有多少个,内存和CPU有多大,但是task只有1个或者10个,那么90%的Executor进程可能根本就没有task执行,也就是白白浪费了资源!因此Spark官网建议的设置原则是,设置该参数为num-executors * executor-cores的2~3倍较为合适,比如Executor的总CPU core数量为300个,那么设置1000个task是可以的,此时可以充分地利用Spark集群的资源。

spark.storage.memoryFraction
  • 参数说明:该参数用于设置RDD持久化数据在Executor内存中能占的比例,默认是0.6。也就是说,默认Executor 60%的内存,可以用来保存持久化的RDD数据。根据你选择的不同的持久化策略,如果内存不够时,可能数据就不会持久化,或者数据会写入磁盘。

  • 参数调优建议:如果Spark作业中,有较多的RDD持久化操作,该参数的值可以适当提高一些,保证持久化的数据能够容纳在内存中。避免内存不够缓存所有的数据,导致数据只能写入磁盘中,降低了性能。但是如果Spark作业中的shuffle类操作比较多,而持久化操作比较少,那么这个参数的值适当降低一些比较合适。此外,如果发现作业由于频繁的gc导致运行缓慢(通过spark web ui可以观察到作业的gc耗时),意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

spark.shuffle.memoryFraction
  • 参数说明:该参数用于设置shuffle过程中一个task拉取到上个stage的task的输出后,进行聚合操作时能够使用的Executor内存的比例,默认是0.2。也就是说,Executor默认只有20%的内存用来进行该操作。shuffle操作在进行聚合时,如果发现使用的内存超出了这个20%的限制,那么多余的数据就会溢写到磁盘文件中去,此时就会极大地降低性能。

  • 参数调优建议:如果Spark作业中的RDD持久化操作较少,shuffle操作较多时,建议降低持久化操作的内存占比,提高shuffle操作的内存占比比例,避免shuffle过程中数据过多时内存不够用,必须溢写到磁盘上,降低了性能。此外,如果发现作业由于频繁的gc导致运行缓慢,意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

set hive.exec.dynamic.partition.mode=nostrict; set hive.exec.max.dynamic.partitions.pernode=1000; set hive.exec.max.dynamic.partitions=10000;

a vcore & b memory 每台机器: n executor = 1/n * a cores = 1/n * b memory 4 vcore & 8g memory set spark.executor.instances=1; set spark.executor.cores=4; set spark.executor.memory=3072m; set spark.default.parallelism=3;

yarn application -kill application_id

Spark配置详解(转)

应用程序属性

这些皆可在spark-default.conf配置,或者部分可在 sparkconf().set设置

属性名称 默认值 含义
spark.app.name (none) 你的应用程序的名字。这将在UI和日志数据中出现
spark.driver.cores 1 driver程序运行需要的cpu内核数
spark.driver.maxResultSize 1g 每个Spark action(如collect)所有分区的序列化结果的总大小限制。设置的值应该不小于1m,0代表没有限制。如果总大小超过这个限制,程序将会终止。大的限制值可能导致driver出现内存溢出错误(依赖于spark.driver.memory和JVM中对象的内存消耗)。
spark.driver.memory 512m driver进程使用的内存数
spark.executor.memory 512m 每个executor进程使用的内存数。和JVM内存串拥有相同的格式(如512m,2g)
spark.extraListeners (none) 注册监听器,需要实现SparkListener
spark.local.dir /tmp Spark中暂存空间的使用目录。在Spark1.0以及更高的版本中,这个属性被SPARK_LOCAL_DIRS(Standalone, Mesos)和LOCAL_DIRS(YARN)环境变量覆盖。
spark.logConf false 当SparkContext启动时,将有效的SparkConf记录为INFO。
spark.master (none) 集群管理器连接的地方

运行环境

| 属性名称 | 默认值 | 含义| |:——————————————-:|:————-:|:————-:| | spark.driver.extraClassPath | (none) | 附加到driver的classpath的额外的classpath实体。| | spark.driver.extraJavaOptions | (none) | 传递给driver的JVM选项字符串。例如GC设置或者其它日志设置。注意,在这个选项中设置Spark属性或者堆大小是不合法的。Spark属性需要用–driver-class-path设置。| | spark.driver.extraLibraryPath | (none) | 指定启动driver的JVM时用到的库路径| | spark.driver.userClassPathFirst | false | (实验性)当在driver中加载类时,是否用户添加的jar比Spark自己的jar优先级高。这个属性可以降低Spark依赖和用户依赖的冲突。它现在还是一个实验性的特征。| | spark.executor.extraClassPath | (none) | 附加到executors的classpath的额外的classpath实体。这个设置存在的主要目的是Spark与旧版本的向后兼容问题。用户一般不用设置这个选项| | spark.executor.extraJavaOptions | (none) | 传递给executors的JVM选项字符串。例如GC设置或者其它日志设置。注意,在这个选项中设置Spark属性或者堆大小是不合法的。Spark属性需要用SparkConf对象或者spark-submit脚本用到的spark-defaults.conf文件设置。堆内存可以通过spark.executor.memory设置| | spark.executor.extraLibraryPath | (none) | 指定启动executor的JVM时用到的库路径| | spark.executor.logs.rolling.maxRetainedFiles | (none) | 设置被系统保留的最近滚动日志文件的数量。更老的日志文件将被删除。默认没有开启。| | spark.executor.logs.rolling.size.maxBytes | (none) | executor日志的最大滚动大小。默认情况下没有开启。值设置为字节| | spark.executor.logs.rolling.strategy | (none) | 设置executor日志的滚动(rolling)策略。默认情况下没有开启。可以配置为time和size。对于time,用spark.executor.logs.rolling.time.interval设置滚动间隔;对于size,用spark.executor.logs.rolling.size.maxBytes设置最大的滚动大小| | spark.executor.logs.rolling.time.interval | daily | executor日志滚动的时间间隔。默认情况下没有开启。合法的值是daily,hourly, minutely以及任意的秒。| | spark.files.userClassPathFirst | false | (实验性)当在Executors中加载类时,是否用户添加的jar比Spark自己的jar优先级高。这个属性可以降低Spark依赖和用户依赖的冲突。它现在还是一个实验性的特征。| | spark.python.worker.memory | 512m | 在聚合期间,每个python worker进程使用的内存数。在聚合期间,如果内存超过了这个限制,它将会将数据塞进磁盘中| | spark.python.profile | false | 在Python worker中开启profiling。通过sc.show_profiles()展示分析结果。或者在driver退出前展示分析结果。可以通过sc.dump_profiles(path)将结果dump到磁盘中。如果一些分析结果已经手动展示,那么在driver退出前,它们再不会自动展示| | spark.python.profile.dump | (none) | driver退出前保存分析结果的dump文件的目录。每个RDD都会分别dump一个文件。可以通过ptats.Stats()加载这些文件。如果指定了这个属性,分析结果不会自动展示| | spark.python.worker.reuse | true | 是否重用python worker。如果是,它将使用固定数量的Python workers,而不需要为每个任务fork()一个Python进程。如果有一个非常大的广播,这个设置将非常有用。因为,广播不需要为每个任务从JVM到Python worker传递一次| | spark.executorEnv.[EnvironmentVariableName] | (none) | 通过EnvironmentVariableName添加指定的环境变量到executor进程。用户可以指定多个EnvironmentVariableName,设置多个环境变量| | spark.mesos.executor.home | driver | side SPARK_HOME 设置安装在Mesos的executor上的Spark的目录。默认情况下,executors将使用driver的Spark本地(home)目录,这个目录对它们不可见。注意,如果没有通过 spark.executor.uri指定Spark的二进制包,这个设置才起作用| | spark.mesos.executor.memoryOverhead | executor | memory * 0.07, 最小384m 这个值是spark.executor.memory的补充。它用来计算mesos任务的总内存。另外,有一个7%的硬编码设置。最后的值将选择spark.mesos.executor.memoryOverhead或者spark.executor.memory的7%二者之间的大者|

Shuffle行为

| 属性名称 | 默认值 | 含义| |:—————————————:|:————-:|:————-:| | spark.reducer.maxMbInFlight | 48 | 从递归任务中同时获取的map输出数据的最大大小(mb)。因为每一个输出都需要我们创建一个缓存用来接收,这个设置代表每个任务固定的内存上限,所以除非你有更大的内存,将其设置小一点| | spark.shuffle.blockTransferService | netty | 实现用来在executor直接传递shuffle和缓存块。有两种可用的实现:netty和nio。基于netty的块传递在具有相同的效率情况下更简单| | spark.shuffle.compress | true | 是否压缩map操作的输出文件。一般情况下,这是一个好的选择。| | spark.shuffle.consolidateFiles | false | 如果设置为”true”,在shuffle期间,合并的中间文件将会被创建。创建更少的文件可以提供文件系统的shuffle的效率。这些shuffle都伴随着大量递归任务。当用ext4和dfs文件系统时,推荐设置为”true”。在ext3中,因为文件系统的限制,这个选项可能机器(大于8核)降低效率| | spark.shuffle.file.buffer.kb | 32 | 每个shuffle文件输出流内存内缓存的大小,单位是kb。这个缓存减少了创建只中间shuffle文件中磁盘搜索和系统访问的数量| | spark.shuffle.io.maxRetries | 3 | Netty only,自动重试次数| | spark.shuffle.io.numConnectionsPerPeer | 1 | Netty only| | spark.shuffle.io.preferDirectBufs | true | Netty only| | spark.shuffle.io.retryWait | 5 | Netty only| | spark.shuffle.manager | sort | 它的实现用于shuffle数据。有两种可用的实现:sort和hash。基于sort的shuffle有更高的内存使用率| | spark.shuffle.memoryFraction | 0.2 | 如果spark.shuffle.spill为true,shuffle中聚合和合并组操作使用的java堆内存占总内存的比重。在任何时候,shuffles使用的所有内存内maps的集合大小都受这个限制的约束。超过这个限制,spilling数据将会保存到磁盘上。如果spilling太过频繁,考虑增大这个值| | spark.shuffle.sort.bypassMergeThreshold | 200 | (Advanced) In the sort-based shuffle manager, avoid merge-sorting data if there is no map-side aggregation and there are at most this many reduce partitions| | spark.shuffle.spill | true | 如果设置为”true”,通过将多出的数据写入磁盘来限制内存数。通过spark.shuffle.memoryFraction来指定spilling的阈值| | spark.shuffle.spill.compress | true | 在shuffle时,是否将spilling的数据压缩。压缩算法通过spark.io.compression.codec指定。|

Spark UI

| 属性名称 | 默认值 | 含义| |:———————-:|:————-:|:————-:| | spark.eventLog.compress | false | 是否压缩事件日志。需要spark.eventLog.enabled为true| | spark.eventLog.dir | file:///tmp/spark-events | Spark事件日志记录的基本目录。在这个基本目录下,Spark为每个应用程序创建一个子目录。各个应用程序记录日志到直到的目录。用户可能想设置这为统一的地点,像HDFS一样,所以历史文件可以通过历史服务器读取| | spark.eventLog.enabled | false | 是否记录Spark的事件日志。这在应用程序完成后,重新构造web UI是有用的| | spark.ui.killEnabled | true | 运行在web UI中杀死stage和相应的job| | spark.ui.port | 4040 | 你的应用程序dashboard的端口。显示内存和工作量数据| | spark.ui.retainedJobs | 1000 | 在垃圾回收之前,Spark UI和状态API记住的job数| | spark.ui.retainedStages | 1000 | 在垃圾回收之前,Spark UI和状态API记住的stage数|

压缩和序列化

| 属性名称 | 默认值 | 含义| |:————————————–:|:————-:|:————-:| | spark.broadcast.compress | true | 在发送广播变量之前是否压缩它| | spark.closure.serializer | org.apache.spark.serializer.JavaSerializer | 闭包用到的序列化类。目前只支持java序列化器| | spark.io.compression.codec | snappy | 压缩诸如RDD分区、广播变量、shuffle输出等内部数据的编码解码器。默认情况 下,Spark提供了三种选择:lz4、lzf和snappy,你也可以用完整的类名来制定。| | spark.io.compression.lz4.block.size | 32768 | LZ4压缩中用到的块大小。降低这个块的大小也会降低shuffle内存使用率| | spark.io.compression.snappy.block.size | 32768 | Snappy压缩中用到的块大小。降低这个块的大小也会降低shuffle内存使用率| | spark.kryo.classesToRegister | (none) | 如果你用Kryo序列化,给定的用逗号分隔的自定义类名列表表示要注册的类| | spark.kryo.referenceTracking | true | 当用Kryo序列化时,跟踪是否引用同一对象。如果你的对象图有环,这是必须的设置。 如果他们包含相同对象的多个副本,这个设置对效率是有用的。如果你知道不在这两个场景,那么可以禁用它以提高效率| | spark.kryo.registrationRequired | false | 是否需要注册为Kyro可用。如果设置为true,然后如果一个没有注册的类序列化,Kyro会抛出异常。如果设置为false,Kryo将会同时写每个对象和其非注册类名。写类名可能造成显著地性能瓶颈。| | spark.kryo.registrator | (none) | 如果你用Kryo序列化,设置这个类去注册你的自定义类。如果你需要用自定义的方式注册你的类,那么这个属性是有用的。否则spark.kryo.classesToRegister会更简单。它应该设置一个继承自KryoRegistrator的类| | spark.kryoserializer.buffer.max.mb | 64 | Kryo序列化缓存允许的最大值。这个值必须大于你尝试序列化的对象| | spark.kryoserializer.buffer.mb | 0.064 | Kyro序列化缓存的大小。这样worker上的每个核都有一个缓存。如果有需要,缓存会涨到spark.kryoserializer.buffer.max.mb设置的值那么大。| | spark.rdd.compress | true | 是否压缩序列化的RDD分区。在花费一些额外的CPU时间的同时节省大量的空间| | spark.serializer | org.apache.spark.serializer.JavaSerializer | 序列化对象使用的类。默认的Java序列化类可以序列化任何可序列化的java对象但是它很慢。所有我们建议用org.apache.spark.serializer.KryoSerializer| | spark.serializer.objectStreamReset | 100 | 当用org.apache.spark.serializer.JavaSerializer序列化时,序列化器通过缓存对象防止写多余的数据,然而这会造成这些对象的垃圾回收停止。通过请求’reset’,你从序列化器中flush这些信息并允许收集老的数据。为了关闭这个周期性的reset,你可以将值设为-1。默认情况下,每一百个对象reset一次|

运行时行为

| 属性名称 | 默认值 | 含义| |:———————-:|:———————————:|:————-:| | spark.broadcast.blockSize | 4096 | TorrentBroadcastFactory传输的块大小,太大值会降低并发,太小的值会出现性能瓶颈| | spark.broadcast.factory | org.apache.spark.broadcast.TorrentBroadcastFactory | broadcast实现类| | spark.cleaner.ttl | (infinite) | spark记录任何元数据(stages生成、task生成等)的持续时间。定期清理可以确保将超期的元数据丢弃,这在运行长时间任务是很有用的,如运行7*24的sparkstreaming任务。RDD持久化在内存中的超期数据也会被清理| | spark.default.parallelism | 本地模式:机器核数;Mesos:8;其他:max(executor的core,2) | 如果用户不设置,系统使用集群中运行shuffle操作的默认任务数(groupByKey、 reduceByKey等)| | spark.executor.heartbeatInterval | 10000 | executor 向 the driver 汇报心跳的时间间隔,单位毫秒| | spark.files.fetchTimeout | 60 | driver 程序获取通过SparkContext.addFile()添加的文件\ 时的超时时间,单位秒| | spark.files.useFetchCache | true | 获取文件时是否使用本地缓存| | spark.files.overwrite | false | 调用SparkContext.addFile()时候是否覆盖文件| | spark.hadoop.cloneConf | false | 每个task是否克隆一份hadoop的配置文件| | spark.hadoop.validateOutputSpecs | true | 是否校验输出| | spark.storage.memoryFraction | 0.6 | Spark内存缓存的堆大小占用总内存比例,该值不能大于老年代内存大小,默认值为0.6,但是,如果你手动设置老年代大小,你可以增加该值| | spark.storage.memoryMapThreshold | 2097152 | 内存块大小| | spark.storage.unrollFraction | 0.2 | Fraction of spark.storage.memoryFraction to use for unrolling blocks in memory.| | spark.tachyonStore.baseDir | System.getProperty(“java.io.tmpdir”) | Tachyon File System临时目录| | spark.tachyonStore.url | tachyon://localhost:19998 | Tachyon File System URL|

网络

| 属性名称 | 默认值 | 含义| |:———————-:|:————-:|:————-:| | spark.driver.host | (local | hostname) driver监听的主机名或者IP地址。这用于和execut | | | ors以及独立的master通信| | spark.driver.port | (random) | driver监听的接口。这用于和executors以及独立的master通信| | spark.fileserver.port | (random) | driver的文件服务器监听的端口| | spark.broadcast.port | (random) | driver的HTTP广播服务器监听的端口| | spark.replClassServer.port | (random) | driver的HTTP类服务器监听的端口| | spark.blockManager.port | (random) | 块管理器监听的端口。这些同时存在于driver和executors| | spark.executor.port | (random) | executor监听的端口。用于与driver通信| | spark.port.maxRetries | 16 | 当绑定到一个端口,在放弃前重试的最大次数| | spark.akka.frameSize | 10 | 在”control plane”通信中允许的最大消息大小。如果你的任务需要发送大的结果到driver中,调大这个值| | spark.akka.threads | 4 | 通信的actor线程数。当driver有很多CPU核时,调大它是有用的| | spark.akka.timeout | 100 | Spark节点之间的通信超时。单位是秒| | spark.akka.heartbeat.pauses | 6000 | This is set to a larger value to disable failure detector that comes inbuilt akka. It can be enabled again, if you plan to use this feature (Not recommended). Acceptable heart beat pause in seconds for akka. This can be used to control sensitivity to gc pauses. Tune this in combination of spark.akka.heartbeat.interval and spark.akka.failure-detector.threshold if you need to.| | spark.akka.failure-detector.threshold | 300.0 | This is set to a larger value to disable failure detector that comes inbuilt akka. It can be enabled again, if you plan to use this feature (Not recommended). This maps to akka’s akka.remote.transport-failure-detector.threshold. Tune this in combination of spark.akka.heartbeat.pauses and spark.akka.heartbeat.interval if you need to.| | spark.akka.heartbeat.interval | 1000 | This is set to a larger value to disable failure detector that comes inbuilt akka. It can be enabled again, if you plan to use this feature (Not recommended). A larger interval value in seconds reduces network overhead and a smaller value ( ~ 1 s) might be more informative for akka’s failure detector. Tune this in combination of spark.akka.heartbeat.pauses and spark.akka.failure-detector.threshold if you need to. Only positive use case for using failure detector can be, a sensistive failure detector can help evictrogue executors really quick. However this is usually not the case as gc pauses and network lags are expected in a real Spark cluster. Apart from that enabling this leads to a lot of exchanges of heart beats between nodes leading to flooding the network with those.|

调度相关属性

| 属性名称 | 默认值 | 含义| |:——————————-:|:————-:|:————-:| | spark.task.cpus | 1 | 为每个任务分配的内核数| | spark.task.maxFailures | 4 | Task的最大重试次数| | spark.scheduler.mode | FIFO | Spark的任务调度模式,还有一种Fair模式| | spark.cores.max | 无 | 当应用程序运行在Standalone集群或者粗粒度共享模式Mesos集群时,应用程序向集群请求的最大CPU内核总数(不是指每台机器,而是整个集群)。如果不设置,对于Standalone集群将使用spark.deploy.defaultCores中数值,而Mesos将使用集群中可用的内核| | spark.mesos.coarse | False | 如果设置为true,在Mesos集群中运行时使用粗粒度共享模式| | spark.speculation | False | 以下几个参数是关于Spark推测执行机制的相关参数。此参数设定是否使用推测执行机制,如果设置为true则spark使用推测执行机制,对于Stage中拖后腿的Task在其他节点中重新启动,并将最先完成的Task的计算结果最为最终结果| | spark.speculation.interval | 100 | Spark多长时间进行检查task运行状态用以推测,以毫秒为单位| | spark.speculation.quantile | 无 | 推测启动前,Stage必须要完成总Task的百分比| | spark.speculation.multiplier | 1.5 | 比已完成Task的运行速度中位数慢多少倍才启用推测| | spark.locality.wait | 3000 | 以下几个参数是关于Spark数据本地性的。本参数是以毫秒为单位启动本地数据task的等待时间,如果超出就启动下一本地优先级别的task。该设置同样可以应用到各优先级别的本地性之间(本地进程 -> 本地节点 -> 本地机架 -> 任意节点 ),当然,也可以通过spark.locality.wait.node等参数设置不同优先级别的本地性| | spark.locality.wait.process | spark.locality.wait | 本地进程级别的本地等待时间| | spark.locality.wait.node | spark.locality.wait | 本地节点级别的本地等待时间| | spark.locality.wait.rack | spark.locality.wait | 本地机架级别的本地等待时间| | spark.scheduler.revive.interval | 1000 | 复活重新获取资源的Task的最长时间间隔(毫秒),发生在Task因为本地资源不足而将资源分配给其他Task运行后进入等待时间,如果这个等待时间内重新获取足够的资源就继续计算|

Dynamic Allocation

| 属性名称 | 默认值 | 含义| |:———————-:|:————-:|:————-:| | spark.dynamicAllocation.enabled | false | 是否开启动态资源搜集 | | spark.dynamicAllocation.executorIdleTimeout | 600 | | spark.dynamicAllocation.initialExecutors | spark.dynamicAllocation.minExecutors || | spark.dynamicAllocation.maxExecutors| Integer.MAX_VALUE || | spark.dynamicAllocation.minExecutors| 0 || | spark.dynamicAllocation.schedulerBacklogTimeout | 5 | | spark.dynamicAllocation.sustainedSchedulerBacklogTimeout | schedulerBacklogTimeout |

安全

| 属性名称 | 默认值 | 含义| |:———————-:|:————-:|:————-:| | spark.authenticate | false | 是否Spark验证其内部连接。如果不是运行在YARN上,请看spark.authenticate.secret| | spark.authenticate.secret | None | 设置Spark两个组件之间的密匙验证。如果不是运行在YARN上,但是需要验证,这个选项必须设置| | spark.core.connection.auth.wait.timeout | 30 | 连接时等待验证的实际。单位为秒| | spark.core.connection.ack.wait.timeout | 60 | 连接等待回答的时间。单位为秒。为了避免不希望的超时,你可以设置更大的值| | spark.ui.filters | None | 应用到Spark web UI的用于过滤类名的逗号分隔的列表。过滤器必须是标准的javax servlet Filter。通过设置java系统属性也可以指定每个过滤器的参数。spark..params='param1=value1,param2=value2'。例如-Dspark.ui.filters=com.test.filter1、-Dspark.com.test.filter1.params='param1=foo,param2=testing'| | spark.acls.enable | false | 是否开启Spark acls。如果开启了,它检查用户是否有权限去查看或修改job。UI利用使用过滤器验证和设置用户| | spark.ui.view.acls | empty | 逗号分隔的用户列表,列表中的用户有查看Spark web UI的权限。默认情况下,只有启动Spark job的用户有查看权限| | spark.modify.acls | empty | 逗号分隔的用户列表,列表中的用户有修改Spark job的权限。默认情况下,只有启动Spark job的用户有修改权限| | spark.admin.acls | empty | 逗号分隔的用户或者管理员列表,列表中的用户或管理员有查看和修改所有Spark job的权限。如果你运行在一个共享集群,有一组管理员或开发者帮助debug,这个选项有用|

加密

| 属性名称 | 默认值 | 含义| |:———————-:|:————-:|:————-:| | spark.ssl.enabled | false | 是否开启ssl| | spark.ssl.enabledAlgorithms | Empty | JVM支持的加密算法列表,逗号分隔| | spark.ssl.keyPassword | None || | spark.ssl.keyStore | None || | spark.ssl.keyStorePassword | None || | spark.ssl.protocol | None || | spark.ssl.trustStore | None || | spark.ssl.trustStorePassword | None ||

Spark Streaming

| 属性名称 | 默认值 | 含义| |:———————-:|:————-:|:————-:| | spark.streaming.blockInterval | 200 | 在这个时间间隔(ms)内,通过Spark Streaming receivers接收的数据在保存到Spark之前,chunk为数据块。推荐的最小值为50ms| | spark.streaming.receiver.maxRate | infinite | 每秒钟每个receiver将接收的数据的最大记录数。有效的情况下,每个流将消耗至少这个数目的记录。设置这个配置为0或者-1将会不作限制| | spark.streaming.receiver.writeAheadLogs.enable | false | Enable write ahead logs for receivers. All the input data received through receivers will be saved to write ahead logs that will allow it to be recovered after driver failures| | spark.streaming.unpersist | true | 强制通过Spark Streaming生成并持久化的RDD自动从Spark内存中非持久化。通过Spark Streaming接收的原始输入数据也将清除。设置这个属性为false允许流应用程序访问原始数据和持久化RDD,因为它们没有被自动清除。但是它会造成更高的内存花费|

集群管理

Spark On YARN

属性名称 默认值 含义
spark.yarn.am.memory 512m client 模式时,am的内存大小;cluster模式时,使用spark.driver.memory变量
spark.driver.cores 1 claster模式时,driver使用的cpu核数,这时候driver运行在am中,其实也就是am和核数;client模式时,使用spark.yarn.am.cores变量
spark.yarn.am.cores 1 client 模式时,am的cpu核数
spark.yarn.am.waitTime 100000 启动时等待时间
spark.yarn.submit.file.replication 3 应用程序上传到HDFS的文件的副本数
spark.yarn.preserve.staging.files False 若为true,在job结束后,将stage相关的文件保留而不是删除
spark.yarn.scheduler.heartbeat.interval-ms 5000 Spark AppMaster发送心跳信息给YARN RM的时间间隔
spark.yarn.max.executor.failures 2倍于executor数,最小值3 导致应用程序宣告失败的最大executor失败次数
spark.yarn.applicationMaster.waitTries 10 RM等待Spark AppMaster启动重试次数,也就是SparkContext初始化次数。超过这个数值,启动失败
spark.yarn.historyServer.address Spark history server的地址(不要加 https://)。这个地址会在Spark应用程序完成后提交给YARN RM,然后RM将信息从RM UI写到history server UI上。
spark.yarn.dist.archives (none)  
spark.yarn.dist.files (none)  
spark.executor.instances 2 executor实例个数
spark.yarn.executor.memoryOverhead executorMemory * 0.07, with minimum of 384 executor的堆内存大小设置
spark.yarn.driver.memoryOverhead driverMemory * 0.07, with minimum of 384 driver的堆内存大小设置
spark.yarn.am.memoryOverhead AM memory * 0.07, with minimum of 384 am的堆内存大小设置,在client模式时设置
spark.yarn.queue default 使用yarn的队列
spark.yarn.jar (none)  
spark.yarn.access.namenodes (none)  
spark.yarn.appMasterEnv.[EnvironmentVariableName] (none) 设置am的环境变量
spark.yarn.containerLauncherMaxThreads 25 am启动executor的最大线程数
spark.yarn.am.extraJavaOptions (none)  
spark.yarn.maxAppAttempts yarn.resourcemanager.am.max-attempts in YARN am重试次数

Spark History Server的属性

| 属性名称 | 默认 | 含义| |:———————-:|:————-:|:————-:| | spark.history.provider | org.apache.spark.deploy.history.FsHistoryProvide | 应用历史后端实现的类名。 目前只有一个实现, 由Spark提供, 它查看存储在文件系统里面的应用日志| | spark.history.fs.logDirectory | file:/tmp/spark-events || | spark.history.updateInterval | 10 | 以秒为单位,多长时间Spark history server显示的信息进行更新。每次更新都会检查持久层事件日志的任何变化。| | spark.history.retainedApplications | 50 | 在Spark history server上显示的最大应用程序数量,如果超过这个值,旧的应用程序信息将被删除。| | spark.history.ui.port | 18080 | 官方版本中,Spark history server的默认访问端口| | spark.history.kerberos.enabled | false | 是否使用kerberos方式登录访问history server,对于持久层位于安全集群的HDFS上是有用的。如果设置为true,就要配置下面的两个属性。| | spark.history.kerberos.principal | 空 | 用于Spark history server的kerberos主体名称| | spark.history.kerberos.keytab | 空 | 用于Spark history server的kerberos keytab文件位置| | spark.history.ui.acls.enable | false | 授权用户查看应用程序信息的时候是否检查acl。如果启用,只有应用程序所有者和spark.ui.view.acls指定的用户可以查看应用程序信息;如果禁用,不做任何检查。|

Hive On Spark调优

Spark作业运行原理

shuffle

详细原理见上图。我们使用spark-submit提交一个Spark作业之后,这个作业就会启动一个对应的Driver进程。根据你使用的部署模式(deploy-mode)不同,Driver进程可能在本地启动,也可能在集群中某个工作节点上启动。Driver进程本身会根据我们设置的参数,占有一定数量的内存和CPU core。而Driver进程要做的第一件事情,就是向集群管理器(可以是Spark Standalone集群,也可以是其他的资源管理集群,美团•大众点评使用的是YARN作为资源管理集群)申请运行Spark作业需要使用的资源,这里的资源指的就是Executor进程。YARN集群管理器会根据我们为Spark作业设置的资源参数,在各个工作节点上,启动一定数量的Executor进程,每个Executor进程都占有一定数量的内存和CPU core。

Spark是根据shuffle类算子来进行stage的划分。如果我们的代码中执行了某个shuffle类算子(比如reduceByKey、join等),那么就会在该算子处,划分出一个stage界限来。可以大致理解为,shuffle算子执行之前的代码会被划分为一个stage,shuffle算子执行以及之后的代码会被划分为下一个stage。因此一个stage刚开始执行的时候,它的每个task可能都会从上一个stage的task所在的节点,去通过网络传输拉取需要自己处理的所有key,然后对拉取到的所有相同的key使用我们自己编写的算子函数执行聚合操作(比如reduceByKey()算子接收的函数)。这个过程就是shuffle。

task的执行速度是跟每个Executor进程的CPU core数量有直接关系的。一个CPU core同一时间只能执行一个线程。而每个Executor进程上分配到的多个task,都是以每个task一条线程的方式,多线程并发运行的。如果CPU core数量比较充足,而且分配到的task数量比较合理,那么通常来说,可以比较快速和高效地执行完这些task线程。

以上就是Spark作业的基本运行原理的说明,大家可以结合上图来理解。理解作业基本原理,是我们进行资源参数调优的基本前提。

参数调优

了解完了Spark作业运行的基本原理之后,对资源相关的参数就容易理解了。所谓的Spark资源参数调优,其实主要就是对Spark运行过程中各个使用资源的地方,通过调节各种参数,来优化资源使用的效率,从而提升Spark作业的执行性能。以下参数就是Spark中主要的资源参数,每个参数都对应着作业运行原理中的某个部分。

num-executors/spark.executor.instances
  • 参数说明:该参数用于设置Spark作业总共要用多少个Executor进程来执行。Driver在向YARN集群管理器申请资源时,YARN集群管理器会尽可能按照你的设置来在集群的各个工作节点上,启动相应数量的Executor进程。这个参数非常之重要,如果不设置的话,默认只会给你启动少量的Executor进程,此时你的Spark作业的运行速度是非常慢的。

  • 参数调优建议:每个Spark作业的运行一般设置50~100个左右的Executor进程比较合适,设置太少或太多的Executor进程都不好。设置的太少,无法充分利用集群资源;设置的太多的话,大部分队列可能无法给予充分的资源。配置spark.executor.cores为host上CPU core的N分之一,同时也设置spark.executor.memory为host上分配给Spark计算内存的N分之一,这样这个host上就能够启动N个executor

executor-memory/spark.executor.memory
  • 参数说明:该参数用于设置每个Executor进程的内存。Executor内存的大小,很多时候直接决定了Spark作业的性能,而且跟常见的JVM OOM异常,也有直接的关联。

  • 参数调优建议:每个Executor进程的内存设置4G8G较为合适。但是这只是一个参考值,具体的设置还是得根据不同部门的资源队列来定。可以看看自己团队的资源队列的最大内存限制是多少,num-executors乘以executor-memory,是不能超过队列的最大内存量的。此外,如果你是跟团队里其他人共享这个资源队列,那么申请的内存量最好不要超过资源队列最大总内存的1/31/2,避免你自己的Spark作业占用了队列所有的资源,导致别的同学的作业无法运行。

executor-cores/spark.executor.cores
  • 参数说明:该参数用于设置每个Executor进程的CPU core数量。这个参数决定了每个Executor进程并行执行task线程的能力。因为每个CPU core同一时间只能执行一个task线程,因此每个Executor进程的CPU core数量越多,越能够快速地执行完分配给自己的所有task线程。

  • 参数调优建议:Executor的CPU core数量设置为2~4个较为合适。同样得根据不同部门的资源队列来定,可以看看自己的资源队列的最大CPU core限制是多少,再依据设置的Executor数量,来决定每个Executor进程可以分配到几个CPU core。同样建议,如果是跟他人共享这个队列,那么num-executors * executor-cores不要超过队列总CPU core的1/3~1/2左右比较合适,也是避免影响其他同学的作业运行。

driver-memory
  • 参数说明:该参数用于设置Driver进程的内存。

  • 参数调优建议:Driver的内存通常来说不设置,或者设置1G左右应该就够了。唯一需要注意的一点是,如果需要使用collect算子将RDD的数据全部拉取到Driver上进行处理,那么必须确保Driver的内存足够大,否则会出现OOM内存溢出的问题。

spark.default.parallelism
  • 参数说明:该参数用于设置每个stage的默认task数量。这个参数极为重要,如果不设置可能会直接影响你的Spark作业性能。

  • 参数调优建议:Spark作业的默认task数量为500~1000个较为合适。很多同学常犯的一个错误就是不去设置这个参数,那么此时就会导致Spark自己根据底层HDFS的block数量来设置task的数量,默认是一个HDFS block对应一个task。通常来说,Spark默认设置的数量是偏少的(比如就几十个task),如果task数量偏少的话,就会导致你前面设置好的Executor的参数都前功尽弃。试想一下,无论你的Executor进程有多少个,内存和CPU有多大,但是task只有1个或者10个,那么90%的Executor进程可能根本就没有task执行,也就是白白浪费了资源!因此Spark官网建议的设置原则是,设置该参数为num-executors * executor-cores的2~3倍较为合适,比如Executor的总CPU core数量为300个,那么设置1000个task是可以的,此时可以充分地利用Spark集群的资源。

spark.storage.memoryFraction
  • 参数说明:该参数用于设置RDD持久化数据在Executor内存中能占的比例,默认是0.6。也就是说,默认Executor 60%的内存,可以用来保存持久化的RDD数据。根据你选择的不同的持久化策略,如果内存不够时,可能数据就不会持久化,或者数据会写入磁盘。

  • 参数调优建议:如果Spark作业中,有较多的RDD持久化操作,该参数的值可以适当提高一些,保证持久化的数据能够容纳在内存中。避免内存不够缓存所有的数据,导致数据只能写入磁盘中,降低了性能。但是如果Spark作业中的shuffle类操作比较多,而持久化操作比较少,那么这个参数的值适当降低一些比较合适。此外,如果发现作业由于频繁的gc导致运行缓慢(通过spark web ui可以观察到作业的gc耗时),意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

spark.shuffle.memoryFraction
  • 参数说明:该参数用于设置shuffle过程中一个task拉取到上个stage的task的输出后,进行聚合操作时能够使用的Executor内存的比例,默认是0.2。也就是说,Executor默认只有20%的内存用来进行该操作。shuffle操作在进行聚合时,如果发现使用的内存超出了这个20%的限制,那么多余的数据就会溢写到磁盘文件中去,此时就会极大地降低性能。

  • 参数调优建议:如果Spark作业中的RDD持久化操作较少,shuffle操作较多时,建议降低持久化操作的内存占比,提高shuffle操作的内存占比比例,避免shuffle过程中数据过多时内存不够用,必须溢写到磁盘上,降低了性能。此外,如果发现作业由于频繁的gc导致运行缓慢,意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

RCFile和ORCFile(转)

RCFile

RCFile文件格式是FaceBook开源的一种Hive的文件存储格式,首先将表分为几个行组,对每个行组内的数据进行按列存储,每一列的数据都是分开存储,正是先水平划分,再垂直划分的理念。 shuffle

存储结构

如上图是HDFS内RCFile的存储结构,我们可以看到,首先对表进行行划分,分成多个行组。一个行组主要包括:16字节的HDFS同步块信息,主要是为了区分一个HDFS块上的相邻行组;元数据的头部信息主要包括该行组内的存储的行数、列的字段信息等等;数据部分我们可以看出RCFile将每一行,存储为一列,将一列存储为一行,因为当表很大,我们的字段很多的时候,我们往往只需要取出固定的一列就可以。 在一般的行存储中 select a from table,虽然只是取出一个字段的值,但是还是会遍历整个表,所以效果和select * from table 一样,在RCFile中,像前面说的情况,只会读取该行组的一行。 在一般的列存储中,会将不同的列分开存储,这样在查询的时候会跳过某些列,但是有时候存在一个表的有些列不在同一个HDFS块上(如下图),所以在查询的时候,Hive重组列的过程会浪费很多IO开销

shuffle

列存储

而RCFile由于相同的列都是在一个HDFS块上,所以相对列存储而言会节省很多资源

存储空间

RCFile采用游程编码,相同的数据不会重复存储,很大程度上节约了存储空间,尤其是字段中包含大量重复数据的时候.
注:游程编码,是一种十分简单的无损数据压缩算法,在某些情况下非常有用,该算法的实现是用当前数据元素以及该元素连续出现的次数来取代字符串中连续出现的数据部分。如aaaaaaaaaabbbaxxxxyyyzyx压缩后a10b3a1x4y3z1y1x1。

懒加载

数据存储到表中都是压缩的数据,Hive读取数据的时候会对其进行解压缩,但是会针对特定的查询跳过不需要的列,这样也就省去了无用的列解压缩

select c from table where a > 1

针对行组来说,会对一个行组的a列进行解压缩,如果当前列中有a>1的值,然后才去解压缩c。若当前行组中不存在a>1的列,那就不用解压缩c,从而跳过整个行组。

ORCFile

ORC是在一定程度上扩展了RCFile,是对RCFile的优化。 shuffle

存储结构

根据结构图,我们可以看到ORCFile在RCFile基础上引申出来Stripe和Footer等。每个ORC文件首先会被横向切分成多个Stripe,而每个Stripe内部以列存储,所有的列存储在一个文件中,而且每个stripe默认的大小是250MB,相对于RCFile默认的行组大小是4MB,所以比RCFile更高效

  1. Postscripts中存储该表的行数,压缩参数,压缩大小,列等信息
  2. Stripe Footer中包含该stripe的统计结果,包括Max,Min,count等信息
  3. FileFooter中包含该表的统计结果,以及各个Stripe的位置信息
  4. IndexData中保存了该stripe上数据的位置信息,总行数等信息
  5. RowData以stream的形式保存了数据的具体信息

Hive读取数据的时候,根据FileFooter读出Stripe的信息,根据IndexData读出数据的偏移量从而读取出数据。 网友有一幅图,形象的说明了这个问题: shuffle

存储空间

ORCFile扩展了RCFile的压缩,除了Run-length(游程编码),引入了字典编码和Bit编码。 采用字典编码,最后存储的数据便是字典中的值,每个字典值得长度以及字段在字典中的位置 至于Bit编码,对所有字段都可采用Bit编码来判断该列是否为null,如果为null则Bit值存为0,否则存为1,对于为null的字段在实际编码的时候不需要存储,也就是说字段若为null,是不占用存储空间的。

RCFile格式相比,ORC File格式的优点

  1. 每个task只输出单个文件,这样可以减少NameNode的负载;
  2. 支持各种复杂的数据类型,比如: datetime, decimal, 以及一些复杂类型(struct, list, map, and union)
  3. 在文件中存储了一些轻量级的索引数据;
  4. 基于数据类型的块模式压缩:a、integer类型的列用行程长度编码(run-length encoding);b、String类型的列用字典编码(dictionary encoding);
  5. 用多个互相独立的RecordReaders并行读相同的文件;
  6. 无需扫描markers就可以分割文件;
  7. 绑定读写所需要的内存;
  8. metadata的存储是用 Protocol Buffers的,所以它支持添加和删除一些列。

Hive On Spark

COALESCE 要用hive的这个函数路径重新注册这个函数 可能这个COALESCE在spark sql中有同名函数

exit code 143

17/12/08 10:26:59 WARN yarn.YarnAllocator: Container marked as failed: container_1512699337730_0003_01_000003 on host: slave2. Exit status: 143. Diagnostics: Container killed on request. Exit code is 143
Container exited with a non-zero exit code 143
Killed by external signal
  1. Run spark add conf bellow
    --conf 'spark.driver.extraJavaOptions=-XX:+UseCompressedOops -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps' \
    --conf 'spark.executor.extraJavaOptions=-XX:+UseCompressedOops -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC  ' \
    
  2. When jvm GC ,you will get follow message Heap after GC invocations=157 (full 98)
     PSYoungGen      total 940544K, used 853456K [0x0000000781800000, 0x00000007c0000000, 0x00000007c0000000)
      eden space 860160K, 99% used [0x0000000781800000,0x00000007b5974118,0x00000007b6000000)
      from space 80384K, 0% used [0x00000007b6000000,0x00000007b6000000,0x00000007bae80000)
      to   space 77824K, 0% used [0x00000007bb400000,0x00000007bb400000,0x00000007c0000000)
     ParOldGen       total 2048000K, used 2047964K [0x0000000704800000, 0x0000000781800000, 0x0000000781800000)
      object space 2048000K, 99% used [0x0000000704800000,0x00000007817f7148,0x0000000781800000)
     Metaspace       used 43044K, capacity 43310K, committed 44288K, reserved 1087488K
      class space    used 6618K, capacity 6701K, committed 6912K, reserved 1048576K  
    }
    
  3. Both PSYoungGen and ParOldGen are 99% ,then you will get java.lang.OutOfMemoryError: GC overhead limit exceeded if more object was created

  4. Try to add more memory for your executor or your driver when more memory resources are avaliable
    --executor-memory 10000m \
    --driver-memory 10000m \
    
  5. For my case : memory for PSYoungGen are smaller then ParOldGen which causes many young object enter into ParOldGen memory area and finaly ParOldGen are not avaliable.So java.lang.OutOfMemoryError: Java heap space error appear.

  6. Adding conf for executor
    'spark.executor.extraJavaOptions=-XX:NewRatio=1 -XX:+UseCompressedOops -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps '
    -XX:NewRatio=rate rate = ParOldGen/PSYoungGen
    

    It dependends.You can try GC strategy like

    -XX:+UseSerialGC :Serial Collector
    -XX:+UseParallelGC :Parallel Collector
    -XX:+UseParallelOldGC :Parallel Old collector
    -XX:+UseConcMarkSweepGC :Concurrent Mark Sweep
    

    Java Concurrent and Parallel GC

  7. If both step 4 and step 6 are done but still get error, you should consider change you code. For example, reduce iterator times in ML model.

Sqoop增量导入导出

export:

| 参数 | 说明 | |:————-:|:————-:| |-–direct|快速模式,利用了数据库的导入工具,如mysql的mysqlimport,可以比jdbc连接的方式更为高效的将数据导入到关系数据库中| |–-export-dir

|存放数据的HDFS的源目录| |--m,–num-mappers |启动N个map来并行导入数据,默认是4个,最好不要将数字设置为高于集群的最大Map数| |–-table|要导入到的关系数据库表| |–-update-key|后面接条件列名,通过该参数,可以将关系数据库中已经存在的数据进行更新操作,类似于关系数据库中的update操作| |–-update-mode |更新模式,有两个值updateonly和默认的allowinsert,该参数只能是在关系数据表里不存在要导入的记录时才能使用,比如要导入的hdfs中有一条id=1的记录,如果在表里已经有一条记录id=2,那么更新会失败| |–-input-null-string |可选参数,如果没有指定,则字符串null将被使用| |-–input-null-non-string |可选参数,如果没有指定,则字符串null将被使用| |-–staging-table |该参数是用来保证在数据导入关系数据库表的过程中事务安全性的,因为在导入的过程中可能会有多个事务,那么一个事务失败会影响到其它事务,比如导入的数据会出现错误或出现重复的记录等等情况,那么通过该参数可以避免这种情况。创建一个与导入目标表同样的数据结构,保留该表为空在运行数据导入前,所有事务会将结果先存放在该表中,然后最后由该表通过一次事务将结果写入到目标表中| |-–clear-staging-table|如果该staging-table非空,则通过该参数可以在运行导入前清除staging-table里的数据| |-–batch|该模式用于执行基本语句|

increment export

| 参数 | 说明 | |:————-:|:————-:| |–update-key|后面接条件列名,通过该参数,可以将关系数据库中已经存在的数据进行更新操作,类似于关系数据库中的update操作,有两种模式:updateonly(默认)、allowinsert(存在的数据更新,不存在数据插入)| |–update-mode|更新模式,有两个值updateonly和默认的allowinsert, 该参数只能是在关系数据表里不存在要导入的记录时才能使用, 比如要导入的hdfs中有一条id=1的记录,如果在表里已经有一条记录id=2,那么更新会失败|

import

| 参数 | 说明 | |:————-:|:————-:| |–append|将数据追加到hdfs中已经存在的dataset中。使用该参数,sqoop将把数据先导入到一个临时目录中,然后重新给文件命名到一个正式的目录中,以避免和该目录中已存在的文件重名| |–-as-avrodatafile|将数据导入到一个Avro数据文件中| |–-as-sequencefile|将数据导入到一个sequence文件中| |–-as-textfile|将数据导入到一个普通文本文件中,生成该文本文件后,可以在hive中通过sql语句查询出结果| |–-columns<col,col,col…>|指定要导入的字段值,格式如:–columns id,username| |–-direct|直接导入模式,使用的是关系数据库自带的导入导出工具。官网上是说这样导入会更快| |–-direct-split-size|在使用上面direct直接导入的基础上,对导入的流按字节数分块,特别是使用直连模式从PostgreSQL导入数据的时候,可以将一个到达设定大小的文件分为几个独立的文件| |–-inline-lob-limit|设定大对象数据类型的最大值| |–m,–num-mappers|启动N个map来并行导入数据,默认是4个,最好不要将数字设置为高于集群的节点数| |–-query,-e|从查询结果中导入数据,该参数使用时必须指定–target-dir、–hive-table,在查询语句中一定要有where条件且在where条件中需要包含$CONDITIONS,示例:–query ‘select * from person where $CONDITIONS ‘| |–-boundary-query |边界查询,也就是在导入前先通过SQL查询得到一个结果集,然后导入的数据就是该结果集内的数据,格式如"–boundary-query 'select id,creationdate from person where id = 3',表示导入的数据为id=3的记录,或者"select min(), max() from table-name",注意查询的字段中不能有数据类型为字符串的字段,否则会报错'java.sql.SQLException: Invalid value for getLong()'"| |--split-by|表的列名,用来切分工作单元,一般后面跟主键ID| |--table|关系数据库表名,数据从该表中获取| |--target-dir

|指定hdfs路径| |--warehouse-dir |与–target-dir不能同时使用,指定数据导入的存放目录,适用于hdfs导入,不适合导入hive目录| |--where|从关系数据库导入数据时的查询条件,示例:–where ‘id = 2′| |-z,--compress|压缩参数,默认情况下数据是没被压缩的,通过该参数可以使用gzip压缩算法对数据进行压缩,适用于SequenceFile, text文本文件, 和Avro文件| |--compression-codec|Hadoop压缩编码,默认是gzip| |--null-string |可选参数,如果没有指定,则字符串null将被使用| |--null-non-string|可选参数,如果没有指定,则字符串null将被使用|

increment import

|参数|说明| |:————-:|:————-:| |-–check-column (col)|用来作为判断的列名,如id| |-–incremental (mode)|append:追加,比如对大于last-value指定的值之后的记录进行追加导入。lastmodified:最后的修改时间,追加last-value指定的日期之后的记录,使用lastmodified模式进行增量处理要指定增量数据是以append模式(附加)还是merge-key(合并)模式添加 | |–-last-value (value)|指定自从上次导入后列的最大值(大于该指定的值),也可以自己设定某一值,对incremental参数,如果是以日期作为追加导入的依据,则使用lastmodified,否则就使用append值。| |–merge-key (column)|对修改的数据会根据column键来进行合并,更新修改|

Flume优化

  • 基础参数调优经验
    HdfsSink中默认的serializer会每写一行在行尾添加一个换行符,我们日志本身带有换行符,这样会导致每条日志后面多一个空行,修改配置不要自动添加换行符;
    lc.sinks.sink_hdfs.serializer.appendNewline = false
    调大MemoryChannel的capacity,尽量利用MemoryChannel快速的处理能力;
    调大HdfsSink的batchSize,增加吞吐量,减少hdfs的flush次数;
    适当调大HdfsSink的callTimeout,避免不必要的超时错误;

  • HdfsSink获取Filename的优化
    HdfsSink的path参数指明了日志被写到Hdfs的位置,该参数中可以引用格式化的参数,将日志写到一个动态的目录中。这方便了日志的管理。例如我们可以将日志写到category分类的目录,并且按天和按小时存放:
    lc.sinks.sink_hdfs.hdfs.path = /user/hive/work/orglog.db/%{category}/dt=%Y%m%d/hour=%H
    HdfsS ink中处理每条event时,都要根据配置获取此event应该写入的Hdfs path和filename,默认的获取方法是通过正则表达式替换配置中的变量,获取真实的path和filename。因为此过程是每条event都要做的操作,耗时很长。通过我们的测试,20万条日志,这个操作要耗时6-8s左右。
    由于我们目前的path和filename有固定的模式,可以通过字符串拼接获得。而后者比正则匹配快几十倍。拼接定符串的方式,20万条日志的操作只需要几百毫秒。

  • HdfsSink的b/m/s优化
    在我们初始的设计中,所有的日志都通过一个Channel和一个HdfsSink写到Hdfs上。我们来看一看这样做有什么问题。
    首先,我们来看一下HdfsSink在发送数据的逻辑:

    //从Channel中取batchSize大小的events
    for (txnEventCount = 0; txnEventCount < batchSize; txnEventCount++) {
      //对每条日志根据category append到相应的bucketWriter上;
      bucketWriter.append(event);
    }
    for (BucketWriter bucketWriter : writers) {
      //然后对每一个bucketWriter调用相应的flush方法将数据flush到Hdfs上
      bucketWriter.flush();
    }
    

    假设我们的系统中有100个category,batchSize大小设置为20万。则每20万条数据,就需要对100个文件进行append或者flush操作。
    其次,对于我们的日志来说,基本符合80/20原则。即20%的category产生了系统80%的日志量。这样对大部分日志来说,每20万条可能只包含几条日志,也需要往Hdfs上flush一次。
    上述的情况会导致HdfsSink写Hdfs的效率极差。
    鉴于这种实际应用场景,我们把日志进行了大小归类,分为big, middle和small三类,这样可以有效的避免小日志跟着大日志一起频繁的flush,提升效果明显。

Linux Daemontools Supervise

  • supervise是daemontools的一个工具,可以用来监控管理unix下的应用程序运行情况,在应用程序出现异常时,supervise可以重新启动指定程序。
  • 安装 ``` shell (http://cr.yp.to/daemontools/install.html) How to install daemontools

Like any other piece of software (and information generally), daemontools comes with NO WARRANTY. System requirements

daemontools works only under UNIX. Installation

Create a /package directory: mkdir -p /package chmod 1755 /package cd /package Download daemontools-0.76.tar.gz into /package. Unpack the daemontools package: gunzip daemontools-0.76.tar tar -xpf daemontools-0.76.tar rm -f daemontools-0.76.tar cd admin/daemontools-0.76 Compile and set up the daemontools programs: package/install On BSD systems, reboot to start svscan. To report success:

 mail djb-sysdeps@cr.yp.to < /package/admin/daemontools/compile/sysdeps ```

Spark Execution

  • Local Execution
    在该模式下,在一个含有多个线程的进程中执行task。
    $SPARK_HOME/bin/spark-submit --class org.apress.prospark.TranslateApp --master local[n]
    ./target/scala-2.10/FirstApp-assembly-1.0.jar <app_name> <book_path> <output_path> <language>
    
  • Standalone Cluster 在该模式下,Driver可以在提交job的机器上执行 ``` shell $SPARK_HOME/bin/spark-submit –class org.apress.prospark.TranslateApp –master ./target/scala-2.10/FirstApp-assembly-1.0.jar
``` ![standalone_cluster1](/assets/img/standalone cluster1.png) 也可以在集群中运行Driver ``` shell $SPARK_HOME/bin/spark-submit ¨Cclass org.apress.prospark.TranslateApp --master --deploy-mode cluster ./target/scala-2.10/FirstApp-assembly-1.0.jar ``` ![standalone_cluster2](/assets/img/standalone cluster2.png) 这两种方式的job都在work上执行,只是Driver的执行位置不同而已。 * Yarn 和standalone模式一样,Driver可以在Client执行 ``` shell $SPARK_HOME/bin/spark-submit --class org.apress.prospark.TranslateApp --master yarn --deploy-mode client ./target/scala-2.10/FirstApp-assembly-1.0.jar ``` 也可以在集群中执行: ``` shell $SPARK_HOME/bin/spark-submit --class org.apress.prospark.TranslateApp --master yarn --deploy-mode cluster ./target/scala-2.10/FirstApp-assembly-1.0.jar ```

聊天机器人资源合集

Awesome Chatbot
Github:https://github.com/fendouai/Awesome-Chatbot

Chatbot
ParlAI
A framework for training and evaluating AI models on a variety of openly available dialog datasets. https://github.com/facebookresearch/ParlAI

stanford-tensorflow-tutorials
A neural chatbot using sequence to sequence model with attentional decoder.
https://github.com/chiphuyen/stanford-tensorflow-tutorials/tree/master/assignments/chatbot

ChatterBot
ChatterBot is a machine learning, conversational dialog engine for creating chat bots
http://chatterbot.readthedocs.io/

DeepQA
My tensorflow implementation of “A neural conversational model”, a Deep learning based chatbot
https://github.com/Conchylicultor/DeepQA

chatbot-rnn
A toy chatbot powered by deep learning and trained on data from Reddit
https://github.com/pender/chatbot-rnn

tf_seq2seq_chatbot
tensorflow seq2seq chatbot
https://github.com/nicolas-ivanov/tf_seq2seq_chatbot

ai-chatbot-framework
A python chatbot framework with Natural Language Understanding and Artificial Intelligence.
https://github.com/alfredfrancis/ai-chatbot-framework

DeepChatModels
Conversation Models in Tensorflow
https://github.com/mckinziebrandon/DeepChatModels

Chatbot
Build your own chatbot base on IBM Watson
https://webchatbot.mybluemix.net/

Chatbot
An AI Based Chatbot
http://chatbot.sohelamin.com/

neural-chatbot
A chatbot based on seq2seq architecture done with tensorflow.
https://github.com/inikdom/neural-chatbot

Chinese_Chatbot
Seq2Seq_Chatbot_QA
使用TensorFlow实现的Sequence to Sequence的聊天机器人模型
https://github.com/qhduan/Seq2Seq_Chatbot_QA

Chatbot
基於向量匹配的情境式聊天機器人
https://github.com/zake7749/Chatbot

Corpus
Cornell Movie-Dialogs Corpus
http://www.cs.cornell.edu/~cristian/Cornell_Movie-Dialogs_Corpus.html

Dialog_Corpus
Datasets for Training Chatbot System
https://github.com/candlewill/Dialog_Corpus

OpenSubtitles
A series of scripts to download and parse the OpenSubtitles corpus.
https://github.com/AlJohri/OpenSubtitles

insuranceqa-corpus-zh
OpenData in insurance area for Machine Learning Tasks
https://github.com/Samurais/insuranceqa-corpus-zh

Papers
Sequence to Sequence Learning with Neural Networks
http://papers.nips.cc/paper/5346-sequence-to-sequence-learning-with-neural-networks.pdf

A Neural Conversational Model
http://arxiv.org/pdf/1506.05869v1.pdf

Tutorial
Deep Learning for Chatbots, Part 1 – Introduction
http://www.wildml.com/2016/04/deep-learning-for-chatbots-part-1-introduction/

Deep Learning for Chatbots, Part 2 – Implementing a Retrieval-Based Model in Tensorflow
http://www.wildml.com/2016/07/deep-learning-for-chatbots-2-retrieval-based-model-tensorflow/
由 http://www.buluo360.com 整理。

Github:https://github.com/fendouai/Awesome-Chatbot

机器学习算法选型

监督学习的用途: k-近邻算法、线性回归、朴素贝叶斯、局部加权线性回归、支持向量机、Ridge回归、决策树、Lasso最小回归系数估计

无监督学习的用途: K-均值、最大期望算法、DBSCAN、Parzen窗设计

选择合适的算法: 1,使用机器学习算法的目的
预测目标变量的值 – 监督学习算法
标变量类型
离散型(是/否、1/2/3 ..) – 分类算法
连续性数值(0.0 ~ 100.00 ..) – 回归算法
不想预测目标变量的值 – 无监督学习算法
需要将数据划分为离散的组 – 聚类算法
分组后还需要估计数据与没个分组的相似程度 – 密度估计算法

图数据库Titan安装

  • 图数据库选型

重要:在Titan的原始团队被DataStar收购后,Titan停滞在1.0.0版本后不再更新。来自Linux Foundation开源社区的爱好者在Titan基础上开发了JanusGraph.https://github.com/JanusGraph/janusgraph

目前市面上比较知名的图数据库主要是Neo4j和Titan.   这里是这两个数据库的对比:https://db-engines.com/en/system/Neo4j;Titan
至于为什么我选择Titan,主要是基于Titan是基于Apache v2 开源协议.而且对公司现有大数据平台的无缝结合.
满足常用的图操作需求,且提供Gremlin查询接口.性能良好,在超大图上的响应能够做到秒级别.

  • Titan安装
    Titan requires Java 8 (Standard Edition). Oracle Java 8 is recommended
    download url: https://github.com/thinkaurelius/titan/wiki/Downloads
    unzip titan-1.0.0-hadoop2.zip
    cd titan-1.0.0-hadoop2
    ./bin/gremlin.sh
    
  • 安装遇到问题 ``` shell Invalid import definition: ‘com.thinkaurelius.titan.hadoop.MapReduceIndexManagement’; reason: startup failed: script15060542610722042298222.groovy: 1: unable to resolve class com.thinkaurelius.titan.hadoop.MapReduceIndexManagement @ line 1, column 1. import com.thinkaurelius.titan.hadoop.MapReduceIndexManagement ^

1 error ``` 需要引入titan-hadoop-1.0.0.jar,地址:http://search.maven.org/remotecontent?filepath=com/thinkaurelius/titan/titan-hadoop/1.0.0/titan-hadoop-1.0.0.jar

Jenkins安装

  • 安装包(推荐war,更方便) wget http://mirrors.jenkins-ci.org/war/latest/jenkins.war

  • 启动 java -jar jenkins.war

  • 启动参数

    –httpPort=$HTTP_PORT	Runs Jenkins listener on port $HTTP_PORT using standard http protocol. The default is port 8080. To disable (because you’re using https), use port -1.
    –httpListenAddress=$HTTP_HOST	Binds Jenkins to the IP address represented by $HTTP_HOST. The default is 0.0.0.0 — i.e. listening on all available interfaces. For example, to only listen for requests from localhost, you could use: –httpListenAddress=127.0.0.1
    –httpsPort=$HTTP_PORT	Uses HTTPS protocol on port $HTTP_PORT
    –httpsListenAddress=$HTTPS_HOST	Binds Jenkins to listen for HTTPS requests on the IP address represented by $HTTPS_HOST.
    –prefix=$PREFIX	Runs Jenkins to include the $PREFIX at the end of the URL.For example, to make Jenkins accessible at http://myServer:8080/jenkins, set –prefix=/jenkins
    –ajp13Port=$AJP_PORT	Runs Jenkins listener on port $AJP_PORT using standard AJP13 protocol. The default is port 8009. To disable (because you’re using https), use port -1.
    –ajp13ListenAddress=$AJP_HOST	Binds Jenkins to the IP address represented by $AJP_HOST. The default is 0.0.0.0 — i.e. listening on all available interfaces.
    –argumentsRealm.passwd.$ADMIN_USER	Sets the password for user $ADMIN_USER. If Jenkins security is turned on, you must log in as the $ADMIN_USER in order to configure Jenkins or a Jenkins project. NOTE: You must also specify that this user has an admin role. (See next argument below).
    –argumentsRealm.roles.$ADMIN_USER=admin	Sets that $ADMIN_USER is an administrative user and can configure Jenkins if Jenkins’ security is turned on. See Securing Jenkins for more information.
    -Xdebug -Xrunjdwp:transport=dt_socket,
    address=$DEBUG_PORT,server=y,suspend=n	Sets debugging on and you can access debug on $DEBUG_PORT.
    -logfile=$LOG_PATH/winstone_date +”%Y</del>%m-%d_%H-%M”.log	Logging to desired file
    -XX:PermSize=512M -XX:MaxPermSize=2048M -Xmn128M -Xms1024M -Xmx2048M	referring to these options for Oracle Java
    

Flume遇到的问题

  • channel溢出
    ERROR - org.apache.flume.source.AvroSource.appendBatch(AvroSource.Java:341)]    
    Avro source seqGenSrc1: Unable to process event batch. Exception follows.    
    org.apache.flume.ChannelException: Unable to put batch on required channel:    
    org.apache.flume.channel.MemoryChannel{name: memoryChannel}
    

    一般情况下修改capacity,transactionCapacity,调大就可以解决

  • java.lang.OutOfMemoryError: Java heap space
    调整flume启动配置文件–flume-env.sh里面的Java参数,将-Xms1024m -Xmx2048m值增大,两项参数分别表示最小和最大java堆大小

  • 若输出的字符串为空(即打印到屏幕上无任何显示),flume会因读取不到数据而报错,故不要将空字符串打印输出,比如可以将其赋值为null。

Flume使用Hive Sink问题

  • Failed connecting to EndPoint
    Caused by: org.apache.hive.hcatalog.streaming.StreamingException: Cannot stream to table that has not been bucketed
    

    使用Hive做为Sink,Hive表必须cluster by bucket

  • ClassCastException
    ERROR - org.apache.flume.SinkRunner$PollingRunner.run(SinkRunner.java:160)] Unable to deliver event. Exception follows.
    org.apache.flume.EventDeliveryException: java.lang.ClassCastException:    
    org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat cannot be cast to org.apache.hadoop.hive.ql.io.AcidOutputFormat
    

    通过查看源码,接口AcidOutputFormat的实现类只有OrcOutputFormat,所以Hive表需要stored as orc

  • Column ‘CHANNEL’ not found
    Caused by: org.apache.hive.hcatalog.streaming.InvalidColumn: Column 'CHANNEL' not found in table for input field 81
    

    Flume配置的Hive 列名必须都为小写字母

Java8内存模型

根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。

  • 虚拟机栈
    每个线程有一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息。栈的大小可以固定也可以动态扩展。当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误,不过这个深度范围不是一个恒定的值,我们通过下面这段程序可以测试一下这个结果:
    public class StackErrorMock {
      private static int index = 1;
     
      public void call(){
          index++;
          call();
      }
     
      public static void main(String[] args) {
          StackErrorMock mock = new StackErrorMock();
          try {
              mock.call();
          }catch (Throwable e){
              System.out.println("Stack deep : "+index);
              e.printStackTrace();
          }
      }
    }
    

    运行三次,可以看出每次栈的深度都是不一样的,输出结果如下:

Stack deep : 11422
java.lang.StackOverflowError
	at StackErrorMock.call(StackErrorMock.java:6)
	at StackErrorMock.call(StackErrorMock.java:7)
	at StackErrorMock.call(StackErrorMock.java:7)
 
Stack deep : 21846
java.lang.StackOverflowError
	at StackErrorMock.call(StackErrorMock.java:7)
	at StackErrorMock.call(StackErrorMock.java:7)
	at StackErrorMock.call(StackErrorMock.java:7)
  
Stack deep : 11423
java.lang.StackOverflowError
	at StackErrorMock.call(StackErrorMock.java:6)
	at StackErrorMock.call(StackErrorMock.java:7)
	at StackErrorMock.call(StackErrorMock.java:7)

虚拟机栈除了上述错误外,还有另一种错误,那就是当申请不到空间时,会抛出 OutOfMemoryError。这里有一个小细节需要注意,catch 捕获的是 Throwable,而不是 Exception。因为 StackOverflowError 和 OutOfMemoryError 都不属于 Exception 的子类。

  • 本地方法栈
    这部分主要与虚拟机用到的 Native 方法相关,一般情况下, Java 应用程序员并不需要关心这部分的内容。

  • PC 寄存器
    PC 寄存器,也叫程序计数器。JVM支持多个线程同时运行,每个线程都有自己的程序计数器。倘若当前执行的是 JVM 的方法,则该寄存器中保存当前执行指令的地址;倘若执行的是native 方法,则PC寄存器中为空。

  • 堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。当申请不到空间时会抛出 OutOfMemoryError。
    下面我们简单的模拟一个堆内存溢出的情况:
import java.util.ArrayList;
import java.util.List;
 
public class HeapOomMock {
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<byte[]>();
        int i = 0;
        boolean flag = true;
        while (flag){
            try {
                i++;
                list.add(new byte[1024 * 1024]);//每次增加一个1M大小的数组对象
            }catch (Throwable e){
                e.printStackTrace();
                flag = false;
                System.out.println("count="+i);//记录运行的次数
            }
        }
    }
}

运行上述代码,输出结果如下:

java.lang.OutOfMemoryError: Java heap space
	at HeapOomMock.main(HeapOomMock.java:12)
count=1676
  • 方法区
    方法区也是所有线程共享。主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。

PermGen(永久代)

绝大部分 Java 程序员应该都见过 “java.lang.OutOfMemoryError: PermGen space “这个异常。这里的 “PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。我们现在通过动态生成类来模拟 “PermGen space”的内存溢出:

public class PermGenOomMock {
    public static void main(String[] args) {
        URL url = null;
        List<ClassLoader> classLoaderList = new ArrayList<ClassLoader>();
        try {
            url = new File("/tmp").toURI().toURL();
            URL[] urls = {url};
            while (true){
                ClassLoader loader = new URLClassLoader(urls);
                classLoaderList.add(loader);
                loader.loadClass("Test");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class Test{

}

运行结果如下:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
		at java.lang.ClassLoader.defineClass(classLoader.java:800)

本例中使用的 JDK 版本是 1.7,指定的 PermGen 区的大小为 8M。通过每次生成不同URLClassLoader对象来加载Test类,从而生成不同的类对象,这样就能看到我们熟悉的 “java.lang.OutOfMemoryError: PermGen space “ 异常了。这里之所以采用 JDK 1.7,是因为在 JDK 1.8 中, HotSpot 已经没有 “PermGen space”这个区间了,取而代之是一个叫做 Metaspace(元空间) 的东西。

Metaspace(元空间)

其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。我们可以通过一段程序来比较 JDK 1.6 与 JDK 1.7及 JDK 1.8 的区别,以字符串常量为例:

import java.util.ArrayList;
import java.util.List;
 
public class StringOomMock {
    static String  base = "string";
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        for (int i=0;i< Integer.MAX_VALUE;i++){
            String str = base + base;
            base = str;
            list.add(str.intern());
        }
    }
}

这段程序以2的指数级不断的生成新的字符串,这样可以比较快速的消耗内存。我们通过 JDK 1.6、JDK 1.7 和 JDK 1.8 分别运行:

1.6
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
		at java.lang.string.intern(Native Method)
		
1.7
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
		at java.util.Arrays.copyOf(Arrays.java:2367)

1.8
Java HoSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=8m; support was removed in 8.0
Java HoSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=8m; support was removed in 8.0			
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
		at java.util.Arrays.copyOf(Arrays.java:3332)

从上述结果可以看出,JDK 1.6下,会出现“PermGen Space”的内存溢出,而在 JDK 1.7和 JDK 1.8 中,会出现堆内存溢出,并且 JDK 1.8中 PermSize 和 MaxPermGen 已经无效。因此,可以大致验证 JDK 1.7 和 1.8 将字符串常量由永久代转移到堆中,并且 JDK 1.8 中已经不存在永久代的结论。现在我们看看元空间到底是一个什么东西?
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
现在我们在 JDK 8下重新运行一下代码段 4,不过这次不再指定 PermSize 和 MaxPermSize。而是指定 MetaSpaceSize 和 MaxMetaSpaceSize的大小。输出结果如下:
shell Exception in thread "main" java.lang.OutOfMemoryError: Metaspace 从输出结果,我们可以看出,这次不再出现永久代溢出,而是出现了元空间的溢出。

总结

通过上面分析,大家应该大致了解了 JVM 的内存划分,也清楚了 JDK 8 中永久代向元空间的转换。至于为什么要这么做,可能是以下几点原因:

  1. 字符串存在永久代中,容易出现性能问题和内存溢出
  2. 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,PermSize指定太小容易造成永久代OOM
  3. 永代会为 GC 带来不必要的复杂度,并且回收效率偏低
  4. Oracle 可能会将HotSpot 与 JRockit 合二为一

Centos配置history

history显示的信息有局限性,默认保存最近的1000条命令,从历史信息中只能看到某个命令的执行有可能导致系统出了问题,如果想定位到是哪个用户在哪个时间在哪执行的命令,需要自己配置。
在/etc/profile中加入以下脚本

#history  
USER_IP=`who -u am i 2>/dev/null| awk '{print $NF}'|sed -e 's/[()]//g'`  
HISTDIR=/usr/share/.history  
if [ -z $USER_IP ]  
then  
USER_IP=`hostname`  
fi  
if [ ! -d $HISTDIR ]  
then  
mkdir -p $HISTDIR  
chmod 777 $HISTDIR  
fi  
if [ ! -d $HISTDIR/${LOGNAME} ]  
then  
mkdir -p $HISTDIR/${LOGNAME}  
chmod 300 $HISTDIR/${LOGNAME}  
fi  
export HISTSIZE=4000  
DT=`date +%Y%m%d_%H%M%S`  
export HISTFILE="$HISTDIR/${LOGNAME}/${USER_IP}.history.$DT"  
export HISTTIMEFORMAT="[%Y.%m.%d %H:%M:%S]"  
chmod 600 $HISTDIR/${LOGNAME}/*.history* 2>/dev/null 

上面的脚本可以记录用户ip和时间,在/usr/share/.history下以用户名命名的目录下,历史记录文件名根据用户ip和时间命名。

SparkContext

SparkContext是在Driver端创建,除了和ClusterManager通信,进行资源的申请、任务的分配和监控等以外还会在创建的时候会初始化各个核心组件,包括DAGScheduler,TaskScheduler,SparkEnv等。

/**
 * Main entry point for Spark functionality. A SparkContext represents the connection to a Spark
 * cluster, and can be used to create RDDs, accumulators and broadcast variables on that cluster.
 *
 * Only one SparkContext may be active per JVM.  You must `stop()` the active SparkContext before
 * creating a new one.  This limitation may eventually be removed; see SPARK-2243 for more details.
 *  目前一个jvm只能存在一个SparkContext,未来可能会支持 可以看看https://issues.apache.org/jira/browse/SPARK-2243的讨论
 * @param config a Spark Config object describing the application configuration. Any settings in
 *   this config overrides the default configs as well as system properties.
 */
class SparkContext(config: SparkConf) extends Logging {

  // The call site where this SparkContext was constructed.
  获取当前SparkContext的当前调用堆栈,将栈里最靠近栈底的属于spark或者Scala核心的类压入callStack的栈顶,   
  并将此类的方法存入lastSparkMethod;将栈里最靠近栈顶的用户类放入callStack,将此类的行号存入firstUserLine,   
  类名存入firstUserFile,最终返回的样例类CallSite存储了最短栈和长度默认为20的最长栈的样例类
  private val creationSite: CallSite = Utils.getCallSite()

  // If true, log warnings instead of throwing exceptions when multiple SparkContexts are active
  private val allowMultipleContexts: Boolean =
    config.getBoolean("spark.driver.allowMultipleContexts", false)

接着是配置信息的获取与设置

  /**
   * Return a copy of this SparkContext's configuration. The configuration ''cannot'' be
   * changed at runtime.  运行时不能修改configuration
   */
  def getConf: SparkConf = conf.clone()

  def jars: Seq[String] = _jars
  def files: Seq[String] = _files
  def master: String = _conf.get("spark.master")
  def deployMode: String = _conf.getOption("spark.submit.deployMode").getOrElse("client")
  def appName: String = _conf.get("spark.app.name")

  private[spark] def isEventLogEnabled: Boolean = _conf.getBoolean("spark.eventLog.enabled", false)
  private[spark] def eventLogDir: Option[URI] = _eventLogDir
  private[spark] def eventLogCodec: Option[String] = _eventLogCodec
  // 是否本地运行
  def isLocal: Boolean = Utils.isLocalMaster(_conf)
  
  // Set Spark driver host and port system properties. This explicitly sets the configuration
  // instead of relying on the default value of the config constant.
  设置driver host 和 port 以及executor.id等
  _conf.set(DRIVER_HOST_ADDRESS, _conf.get(DRIVER_HOST_ADDRESS))
  _conf.setIfMissing("spark.driver.port", "0")

  _conf.set("spark.executor.id", SparkContext.DRIVER_IDENTIFIER)

  _jars = Utils.getUserJars(_conf)
  _files = _conf.getOption("spark.files").map(_.split(",")).map(_.filter(_.nonEmpty))
    .toSeq.flatten

然后比较重要的是事件监听

/**
 * Asynchronously passes SparkListenerEvents to registered SparkListeners.
 *
 * Until `start()` is called, all posted events are only buffered. Only after this listener bus
 * has started will events be actually propagated to all attached listeners. This listener bus
 * is stopped when `stop()` is called, and it will drop further events after stopping.
 */
 listenerBus里已经注册了很多监听者(listener),通常listenerBus会启动一个线程异步的调用这些listener去消费这个Event   
 (其实就是触发事先设计好的回调函数来执行譬如信息存储等动作)
  _listenerBus = new LiveListenerBus(_conf)
   
   // "_jobProgressListener" should be set up before creating SparkEnv because when creating
   // "SparkEnv", some messages will be posted to "listenerBus" and we should not miss them.
   负责监听事件并把事件消息发送给listenerBus  但是将要removed了           
   _jobProgressListener = new JobProgressListener(_conf)
  listenerBus.addListener(jobProgressListener)

接着创建SparkEnv

    // Create the Spark execution environment (cache, map output tracker, etc)
    _env = createSparkEnv(_conf, isLocal, listenerBus)
    SparkEnv.set(_env)
   ......
  // This function allows components created by SparkEnv to be mocked in unit tests:
  private[spark] def createSparkEnv(
      conf: SparkConf,
      isLocal: Boolean,
      listenerBus: LiveListenerBus): SparkEnv = {
      实际是创建的driverEnv
    SparkEnv.createDriverEnv(conf, isLocal, listenerBus, SparkContext.numDriverCores(master))
  }
  ......
  /**
   * Create a SparkEnv for the driver.
   */
  private[spark] def createDriverEnv(
      conf: SparkConf,
      isLocal: Boolean,
      listenerBus: LiveListenerBus,
      numCores: Int,
      mockOutputCommitCoordinator: Option[OutputCommitCoordinator] = None): SparkEnv = {
     断言driver host & port
    assert(conf.contains(DRIVER_HOST_ADDRESS),
      s"${DRIVER_HOST_ADDRESS.key} is not set on the driver!")
    assert(conf.contains("spark.driver.port"), "spark.driver.port is not set on the driver!")
    val bindAddress = conf.get(DRIVER_BIND_ADDRESS)
    val advertiseAddress = conf.get(DRIVER_HOST_ADDRESS)
    val port = conf.get("spark.driver.port").toInt
    是否传输加密
    val ioEncryptionKey = if (conf.get(IO_ENCRYPTION_ENABLED)) {
      Some(CryptoStreamUtils.createKey(conf))
    } else {
      None
    }
    调用SparkEnv的create
  /**
   * Helper method to create a SparkEnv for a driver or an executor.
   */
    create(
      conf,
      SparkContext.DRIVER_IDENTIFIER,
      bindAddress,
      advertiseAddress,
      Option(port),
      isLocal,
      numCores,
      ioEncryptionKey,
      listenerBus = listenerBus,
      mockOutputCommitCoordinator = mockOutputCommitCoordinator
    )
    这个create包含SecurityManager,Serializer,BroadcastManager,registerOrLookupEndpoint,
    ShuffleManager,useLegacyMemoryManager,BlockManager,MetricsSystem等的创建
   }
  

然后是低级别状态报告api,负责监听job和stage的进度

      /**
       * Low-level status reporting APIs for monitoring job and stage progress.
       *
       * These APIs intentionally provide very weak consistency semantics; consumers of these APIs should
       * be prepared to handle empty / missing information.  For example, a job's stage ids may be known
       * but the status API may not have any information about the details of those stages, so
       * `getStageInfo` could potentially return `None` for a valid stage id.
       *
       * To limit memory usage, these APIs only provide information on recent jobs / stages.  These APIs
       * will provide information for the last `spark.ui.retainedStages` stages and
       * `spark.ui.retainedJobs` jobs.
       *
       * NOTE: this class's constructor should be considered private and may be subject to change.
       */
    _statusTracker = new SparkStatusTracker(this)

接着是进度条,ui,hadoop conf,executor memory,心跳 等配置

    // We need to register "HeartbeatReceiver" before "createTaskScheduler" because Executor will
    // retrieve "HeartbeatReceiver" in the constructor. (SPARK-6640)
    _heartbeatReceiver = env.rpcEnv.setupEndpoint(
        /**
         * Retrieve the [[RpcEndpointRef]] represented by `address` and `endpointName`.
         * This is a blocking action.
         * 注册heartbeatReceiver的Endpoint到rpcEnv上面并返回他对应的Reference
         */
      HeartbeatReceiver.ENDPOINT_NAME, new HeartbeatReceiver(this))

然后最重要的TaskScheduler & DAGScheduler

    // Create and start the scheduler
    会根据master匹配对应的SchedulerBackend和TaskSchedulerImpl创建方式
    val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)
    // Create and start the scheduler
    _schedulerBackend = sched
    _taskScheduler = ts
    创建DAGScheduler
    _dagScheduler = new DAGScheduler(this)
    心跳
    _heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)

    // start TaskScheduler after taskScheduler sets DAGScheduler reference in DAGScheduler's
    // constructor
    启动TaskScheduler
    _taskScheduler.start()
    获取appid 不同模式不一样
    local模式为:"local-" + System.currentTimeMillis
    _applicationId = _taskScheduler.applicationId()
    _applicationAttemptId = taskScheduler.applicationAttemptId()
    _conf.set("spark.app.id", _applicationId)
    if (_conf.getBoolean("spark.ui.reverseProxy", false)) {
      System.setProperty("spark.ui.proxyBase", "/proxy/" + _applicationId)
    }
    _ui.foreach(_.setAppId(_applicationId))
      /**
       * Initializes the BlockManager with the given appId. This is not performed in the constructor as
       * the appId may not be known at BlockManager instantiation time (in particular for the driver,
       * where it is only learned after registration with the TaskScheduler).
       *
       * This method initializes the BlockTransferService and ShuffleClient, registers with the
       * BlockManagerMaster, starts the BlockManagerWorker endpoint, and registers with a local shuffle
       * service if configured.
       */
    _env.blockManager.initialize(_applicationId)

接下来metrics system 测量系统,提供个ui展示

    // The metrics system for Driver need to be set spark.app.id to app ID.
    // So it should start after we get app ID from the task scheduler and set spark.app.id.
    _env.metricsSystem.start()
    // Attach the driver metrics servlet handler to the web ui after the metrics system is started.
    _env.metricsSystem.getServletHandlers.foreach(handler => ui.foreach(_.attachHandler(handler)))

然后_eventLogger和动态资源分配模式

    // Optionally scale number of executors dynamically based on workload. Exposed for testing.
    通过spark.dynamicAllocation.enabled参数开启后就会启动ExecutorAllocationManager
    val dynamicAllocationEnabled = Utils.isDynamicAllocationEnabled(_conf)
    _executorAllocationManager =
      if (dynamicAllocationEnabled) {
        schedulerBackend match {
          case b: ExecutorAllocationClient =>
            // An agent that dynamically allocates and removes executors based on the workload.
            根据集群资源动态触发增加或者删除资源策略
            Some(new ExecutorAllocationManager(
              schedulerBackend.asInstanceOf[ExecutorAllocationClient], listenerBus, _conf))
          case _ =>
            None
        }
      } else {
        None
      }
    _executorAllocationManager.foreach(_.start())

然后cleaner

   _cleaner =
      if (_conf.getBoolean("spark.cleaner.referenceTracking", true)) {
      /**
       * An asynchronous cleaner for RDD, shuffle, and broadcast state.
       *
       * This maintains a weak reference for each RDD, ShuffleDependency, and Broadcast of interest,
       * to be processed when the associated object goes out of scope of the application. Actual
       * cleanup is performed in a separate daemon thread.
       */
        Some(new ContextCleaner(this))
      } else {
        None
      }
    _cleaner.foreach(_.start())

最后shutdown hook

    // Make sure the context is stopped if the user forgets about it. This avoids leaving
    // unfinished event logs around after the JVM exits cleanly. It doesn't help if the JVM
    // is killed, though.
    logDebug("Adding shutdown hook") // force eager creation of logger
    // ShutdownHookManager相比JVM本身的执行Hook方式具有如下两种特性(默认JVM执行,无序,并发)
    // 1.顺序  2.有优先级
    _shutdownHookRef = ShutdownHookManager.addShutdownHook(
      ShutdownHookManager.SPARK_CONTEXT_SHUTDOWN_PRIORITY) { () =>
      logInfo("Invoking stop() from shutdown hook")
      stop()
    }

SparkSubmit

当我们按照官网的介绍,执行

export HADOOP_CONF_DIR=XXX
./bin/spark-submit \
  --class org.apache.spark.examples.SparkPi \
  --master yarn \
  --deploy-mode cluster \  # can be client for client mode
  --executor-memory 20G \
  --num-executors 50 \
  /path/to/examples.jar \
  1000

时,Spark内部是如何提交这个job的呢?
那就看看SparkSubmit.scala做了什么

  override def main(args: Array[String]): Unit = {
    val appArgs = new SparkSubmitArguments(args)
    if (appArgs.verbose) {
      // scalastyle:off println
      printStream.println(appArgs)
      // scalastyle:on println
    }
    // 根据传入的参数匹配对应的执行方法
    appArgs.action match {
      /**
       * Submit the application using the provided parameters.
       *
       * This runs in two steps. First, we prepare the launch environment by setting up
       * the appropriate classpath, system properties, and application arguments for
       * running the child main class based on the cluster manager and the deploy mode.
       * Second, we use this launch environment to invoke the main method of the child
       * main class.
       * 二步:prepareSubmitEnvironment 和 doRunMain
       */
      case SparkSubmitAction.SUBMIT => submit(appArgs)
      // Kill an existing submission using the REST protocol. Standalone and Mesos cluster mode only.
      case SparkSubmitAction.KILL => kill(appArgs) 
      // Request the status of an existing submission using the REST protocol.
      // Standalone and Mesos cluster mode only.
      case SparkSubmitAction.REQUEST_STATUS => requestStatus(appArgs) 
    }
  }

然后看看submit方法详细内容:

 private def submit(args: SparkSubmitArguments): Unit = {
    /**
     * Prepare the environment for submitting an application.
     * This returns a 4-tuple:
     *   (1) the arguments for the child process,
     *   (2) a list of classpath entries for the child,
     *   (3) a map of system properties, and
     *   (4) the main class for the child
     * Exposed for testing.
     * 前面提交job脚本里面的master,deploy-mode等参数 全在这个方法里面会触发不同的执行操作
     */
    val (childArgs, childClasspath, sysProps, childMainClass) = prepareSubmitEnvironment(args)

    def doRunMain(): Unit = {
      if (args.proxyUser != null) {
        val proxyUser = UserGroupInformation.createProxyUser(args.proxyUser,
          UserGroupInformation.getCurrentUser())
        try {
          proxyUser.doAs(new PrivilegedExceptionAction[Unit]() {
            override def run(): Unit = {
              /**
               * Run the main method of the child class using the provided launch environment.
               *
               * Note that this main class will not be the one provided by the user if we're
               * running cluster deploy mode or python applications.
               */
              runMain(childArgs, childClasspath, sysProps, childMainClass, args.verbose)
            }
          })
        } catch {
          case e: Exception =>
            // Hadoop's AuthorizationException suppresses the exception's stack trace, which
            // makes the message printed to the output by the JVM not very helpful. Instead,
            // detect exceptions with empty stack traces here, and treat them differently.
            if (e.getStackTrace().length == 0) {
              // scalastyle:off println
              printStream.println(s"ERROR: ${e.getClass().getName()}: ${e.getMessage()}")
              // scalastyle:on println
              exitFn(1)
            } else {
              throw e
            }
        }
      } else {
          /**
           * Run the main method of the child class using the provided launch environment.
           *
           * Note that this main class will not be the one provided by the user if we're
           * running cluster deploy mode or python applications.
           */
        runMain(childArgs, childClasspath, sysProps, childMainClass, args.verbose)
      }
    }

其中prepareSubmitEnvironment最重要的代码:

    if (deployMode == CLIENT || isYarnCluster) {
      childMainClass = args.mainClass
      ...
    }
    
    if (args.isStandaloneCluster) {
      if (args.useRest) {
        childMainClass = "org.apache.spark.deploy.rest.RestSubmissionClient"
        ...
      } else {
        // In legacy standalone cluster mode, use Client as a wrapper around the user class
        childMainClass = "org.apache.spark.deploy.Client"
        ...
      }
      ...
    }
    
    if (isYarnCluster) {
      childMainClass = "org.apache.spark.deploy.yarn.Client"
      ...
    }
      
    if (isMesosCluster) {
      childMainClass = "org.apache.spark.deploy.rest.RestSubmissionClient"
      ...
    }

runMain所需的参数就是prepareSubmitEnvironment的返回值

  // runMain里面通过java的反射得到mainClass
  mainClass = Utils.classForName(childMainClass)
  // 得到main方法
  val mainMethod = mainClass.getMethod("main", new Array[String](0).getClass)
  // 执行main方法
  mainMethod.invoke(null, childArgs.toArray)

一致性Hash算法

  • 性质
    考虑到分布式系统每个节点都有可能失效,并且新的节点很可能动态的增加进来,如何保证当系统的节点数目发生变化时仍然能够对外提供良好的服务,这是值得考虑的,尤其实在设计分布式缓存系统时,如果某台服务器失效,对于整个系统来说如果不采用合适的算法来保证一致性,那么缓存于系统中的所有数据都可能会失效(即由于系统节点数目变少,客户端在请求某一对象时需要重新计算其hash值(通常与系统中的节点数目有关),由于hash值已经改变,所以很可能找不到保存该对象的服务器节点),因此一致性hash就显得至关重要,良好的分布式cahce系统中的一致性hash算法应该满足以下几个方面:
  1. 平衡性(Balance)
    平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。

  2. 单调性(Monotonicity)
    单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲区加入到系统中,那么哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲区中去,而不会被映射到旧的缓冲集合中的其他缓冲区。简单的哈希算法往往不能满足单调性的要求,如最简单的线性哈希:x = (ax + b) mod (P),在上式中,P表示全部缓冲的大小。不难看出,当缓冲大小发生变化时(从P1到P2),原来所有的哈希结果均会发生变化,从而不满足单调性的要求。哈希结果的变化意味着当缓冲空间发生变化时,所有的映射关系需要在系统内全部更新。而在P2P系统内,缓冲的变化等价于Peer加入或退出系统,这一情况在P2P系统中会频繁发生,因此会带来极大计算和传输负荷。单调性就是要求哈希算法能够应对这种情况。

  3. 分散性(Spread)
    在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。

  4. 负载(Load)
    负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

  5. 平滑性(Smoothness)
    平滑性是指缓存服务器的数目平滑改变和缓存对象的平滑改变是一致的。

  • 原理
    一致性哈希算法(Consistent Hashing)最早在论文《Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web》中被提出。简单来说,一致性哈希将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0-2^32-1(即哈希值是一个32位无符号整形),整个哈希空间环如下:
    hash_1
    整个空间按顺时针方向组织。0和232-1在零点中方向重合。
    下一步将各个服务器使用Hash进行一个哈希,具体可以选择服务器的ip或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置,这里假设将上文中四台服务器使用ip地址哈希后在环空间的位置如下: hash_2
    接下来使用如下算法定位数据访问到相应服务器:将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器。
    例如我们有Object A、Object B、Object C、Object D四个数据对象,经过哈希计算后,在环空间上的位置如下: hash_3
    根据一致性哈希算法,数据A会被定为到Node A上,B被定为到Node B上,C被定为到Node C上,D被定为到Node D上。
    下面分析一致性哈希算法的容错性和可扩展性。现假设Node C不幸宕机,可以看到此时对象A、B、D不会受到影响,只有C对象被重定位到Node D。一般的,在一致性哈希算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响。
    下面考虑另外一种情况,如果在系统中增加一台服务器Node X,如下图所示: hash_4
    此时对象Object A、B、D不受影响,只有对象C需要重定位到新的Node X 。一般的,在一致性哈希算法中,如果增加一台服务器,则受影响的数据仅仅是新服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它数据也不会受到影响。
    综上所述,一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
    另外,一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题。例如系统中只有两台服务器,其环分布如下, hash_5
    此时必然造成大量数据集中到Node A上,而只有极少量会定位到Node B上。为了解决这种数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器ip或主机名的后面增加编号来实现。例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六个虚拟节点: hash_6
    同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到“Node A#1”、“Node A#2”、“Node A#3”三个虚拟节点的数据均定位到Node A上。这样就解决了服务节点少时数据倾斜的问题。在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。

Quorum机制

  • 基于Quorum投票的冗余控制算法 Quorom 机制,是一种分布式系统中常用的,用来保证数据冗余和最终一致性的投票算法,其主要数学思想来源于鸽巢原理(若有n+1只鸽子关在n个笼子里,那么至少有一个笼子有至少2只鸽子)。
    在有冗余数据的分布式存储系统当中,冗余数据对象会在不同的机器之间存放多份拷贝。但是同一时刻一个数据对象的多份拷贝只能用于读或者用于写。
    该算法可以保证同一份数据对象的多份拷贝不会被超过两个访问对象读写。 采用Quorum机制后,写操作需要即刻完成的副本数减少,读操作需要成功读取的副本数增加,负载被间接转移了过去,一定程度上平衡了读写两种操作,系统整体性能会得到提升。 算法来源于[Gifford, 1979]。 分布式系统中的每一份数据拷贝对象都被赋予一票。每一个操作必须要获得最小的读票数(Vr)或者最小的写票数(Vw)才能读或者写。如果一个系统有V票(意味着一个数据对象有V份冗余拷贝),那么这最小读写票必须满足:
    Vr + Vw > V
    Vw > V/2
    第一条规则保证了一个数据不会被同时读写。当一个写操作请求过来的时候,它必须要获得Vw个冗余拷贝的许可。而剩下的数量是V-Vw 不够Vr,因此不能再有读请求过来了。同理,当读请求已经获得了Vr个冗余拷贝的许可时,写请求就无法获得许可了。
    第二条规则保证了数据的串行化修改。一份数据的冗余拷贝不可能同时被两个写请求修改,W> N/2, 实际上变成了一个写的锁,意味着只有写了过半数副本的才算写成功,拿不到的就返回失败,解决了竞争的问题。W> N/2,同时意味着不需要把所有的副本都写完,未完成的留给系统自己后台慢慢同步,那这个时候问题就来了,一个新的会话过来读数据的时候,分配到的副本有可能是没来得及更新的。

  • 算法的好处
    在分布式系统中,冗余数据是保证可靠性的手段,因此冗余数据的一致性维护就非常重要。一般而言,一个写操作必须要对所有的冗余数据都更新完成了,才能称为成功结束。比如一份数据在5台设备上有冗余,因为不知道读数据会落在哪一台设备上,那么一次写操作,必须5台设备都更新完成,写操作才能返回。
    对于写操作比较频繁的系统,这个操作的瓶颈非常大。Quorum算法可以让写操作只要写完3台就返回。剩下的由系统内部缓慢同步完成。而读操作,则需要也至少读3台,才能 保证至少可以读到一个最新的数据。
    Quorum的读写最小票数可以用来做为系统在读、写性能方面的一个可调节参数。写票数Vw越大,则读票数Vr越小,这时候系统写的开销就大。反之则写的开销就小。

HDFS hflush hsync和close的区别

  • hflush: 语义是保证flush的数据被新的reader读到,但是不保证数据被datanode持久化.
  • hsync: 与hflush几乎一样,不同的是hsync保证数据被datanode持久化。
  • close: 关闭文件.除了做到以上2点,还保证文件的所有block处于completed状态,并且将文件置为closed 场景是写一个字节(append或者create)
  • hflush
    涉及到几个线程,一个是调FSDataOutputStream的write的线程,它是应用程序自己的线程,write会将数据以packet的形式一个个的丢入data queue中。 另外两个是HDFS客户端代码中的线程,其中一个是DataStreamer,负责从data queue中取出一个个的packet发出去,过程是为每个block建立一个pipeline,然后发packet,最后关闭pipeline,接着下一个block。另外一个是ResponseProcessor,负责处理下游节点的ack。
    实际上,packet是由chunk组成的,每个chunk对应一个checksum,一个packet大概64KB左右,一个chunk通常512字节。通常情况下,每512字节算一个checksum,写入到packet中。但是最后一个chunk通常是不满512字节。hflush实际上,就是将最后不满一个chunk的数据算checksum,然后写入packet,最后将这个packet放入data queue队列.在我们只写一个字节的场景下,一个字节不够一个chunk,故data queue中始终每个packet,DataStreamer始终等待着没有建立pipeline,调用hflush后,往data queue塞入一个packet,DataStreamer终于从data queue中取到一个packet,然后建立pipeline,接着发送packet。调完hflush的应用程序线程一直在等待最后一个packet的ack被收到,轮到ResponseProcessor上场。他不断的处理从datanode收到的packet ack,不断更新block的长度。接着,执行hflush的应用程序线程终于等到了最后一个packet的ack,然后它告诉namenode最后一个block的长度,namenode更新内存状态,实际上是根据文件名找到INodeFile,将block长度写入,并且记一条edit log.
  • FSDataOutputStream的close
    一开始也是和hflush一样,将最后一个packet进data queue,不同的是还会生成一个特殊的packet入data queue,lastPacketInBlock标记设为true,意思是告诉datanode这是block的最后一个packet,然后等最后这个包的ack收到。接着关闭DataStreamer和ResponseProcessor线程。然后调用completeFile(),最后结束file lease.
    看看completeFile():通知namenode,namenode会做一些检查:
    根据文件名从目录树中拿出INode,检查文件是否处于under construction状态,如果不是,则complete file失败. 从INode中拿出修改这个文件的lease holder和当前completeFile()这个客户端比较,看是否是同一个client,如果不是,则complete file失败(namenode从目录树中得到当前打开文件的信息,会定期检查打开的文件的lease是否超过hard limit,默认1小时,如果超过了,会强行将文件的lease设置为namenode,这样,client 就不能向namenode commit block了。) namenode会检查文件的倒数第二个block是否已经是completed状态,如果不是客户端重试,否则,将最后一个block变成completed状态,其实就是修改一下内存中数据结构,写一条edit log。一个block是completed状态的条件是满足最低副本数要求,默认配置1,配置项DFS_NAMENODE_REPLICATION_MIN_KEY.当datanode收到一个block后,会向namenode汇报,只要有一个datanode汇报成功,namenode就将block置为completed.最后namenode将file置为closed状态。
  • hsync
    hsync()执行时,实际上会在对应Datanode的机器上产生一个fsync的系统调用,从而将内存中的相关文件的数据更新到磁盘。
    Client端执行hsync时,Datanode端会识别到Client发送过来的数据包中的syncBlock_字段为true,从而判定需要将内存中的数据更新到磁盘。此时会在BlockReceiver.java的flushOrSync()中执行如下语句:
    ((FileOutputStream)cout).getChannel().force(true);
    而FileChannel的force(boolean metadata)方法在JDK中,底层为于FileDispatcherImpl.c中调用fsync或fdatasync。metadata为true时执行fsync,为false时执行fdatasync。
    当Datanode将数据持久化到磁盘上后,会发ack响应给Client端。当收到所有Datanode的ack响应时,hsync()的调用结束。
    值得注意的是,fsync或fdatasync本身是一个非常耗时的调用,因为磁盘的读写速度远低于内存的读写速度。在不调用fsync或fdatasync的情况下,数据可能保存在各级cache中。

HDFS的读写

应用程序通过创建新文件以及向新文件写数据的方式,给HDFS系统添加数据。文件关闭以后,被写入的数据就无法再修改或者删除,只有以“追加”方式重新打开文件后,才能再次为文件添加数据。HDFS采用单线程写,多线程读的模式。

  • 读文件 hdfs_read
    1. 首先调用FileSystem对象的open方法,其实是一个DistributedFileSystem的实例
    2. DistributedFileSystem通过rpc获得文件的第一批个block的locations,同一block按照replaction会返回多个locations,这些locations按照hadoop拓扑结构排序,距离客户端近的排在前面.
    3. 前两步会返回一个FSDataInputStream对象,该对象会被封装成DFSInputStream对象,DFSInputStream可以方便的管理datanode和namenode数据流。客户端调用read方法,DFSInputStream最会找出离客户端最近的datanode并连接。
    4. 数据从datanode源源不断的流向客户端。
    5. 如果第一块的数据读完了,就会关闭指向第一块的datanode连接,接着读取下一块。这些操作对客户端来说是透明的,客户端的角度看来只是读一个持续不断的流。
    6. 如果第一批block都读完了,DFSInputStream就会去namenode拿下一批blocks的location,然后继续读,如果所有的块都读完,这时就会关闭掉所有的流。
      如果在读数据的时候,DFSInputStream和datanode的通讯发生异常,就会尝试正在读的block的排第二近的datanode,并且会记录哪个datanode发生错误,剩余的blocks读的时候就会直接跳过该datanode。DFSInputStream也会检查block数据校验和checkSum,如果发现一个坏的block,就会先报告到namenode节点,然后DFSInputStream在其他的datanode上读该block的镜像
      该设计的方向就是客户端直接连接datanode来检索数据并且namenode来负责为每一个block提供最优的datanode,namenode仅仅处理block location的请求,这些信息都加载在namenode的内存中,hdfs通过datanode集群可以承受大量客户端的并发访问。

    注:DataNode中所包含的文件块备份可能会因为内存、磁盘或者网络的错误而造成损坏。为了避免这种错误的形成,HDFS会为其文件的每个数据块生成并存储一份Checksum(总和检查)。Checksum主要供HDFS客户端在读取文件时检查客户端,DataNode以及网络等几个方面可能造成的数据块损坏。当客户端开始建立HDFS文件时,会检查文件的每个数据块的checksum序列,并将其与数据一起发送给DataNode。 DataNode则将checksum存放在文件的元数据文件里,与数据块的具体数据分开存放。当HDFS读取文件时,文件的每个块数据和checksum均被发送到客户端。客户端会即时计算出接受的块数据的checksum, 并将其与接受到的checksum进行匹配。如果不匹配,客户端会通知NameNode,表明接受到的数据块已经损坏,并尝试从其他的DataNode节点获取所需的数据块。
    HDFS允许客户端从正在进行写操作的文件中读取数据。当进行这样的操作时,目前正在被写入的数据块对于NameNode来说是未知的。在这样的情况下,客户端会从所有数据块备份中挑选一个数据块,以这个数据块的最后长度作为开始读取数据之前的数据长度。
    HDFS I/O的设计是专门针对批处理系统进行优化的,比如MapReduce系统,这类系统对顺序读写的吞吐量都有很高的要求。针对于那些需要实时数据流以及随机读写级别的应用来说,系统的读/写响应时间还有待于优化,目前正在做这方面的努力。

  • 写文件 hdfs_write
    1. 客户端通过调用DistributedFileSystem的create方法创建新文件
    2. DistributedFileSystem通过RPC调用namenode去创建一个没有blocks关联的新文件,创建前,namenode会做各种校验,比如文件是否存在,客户端有无权限去创建等。如果校验通过,namenode就会记录下新文件,否则就会抛出IO异常.
    3. 前两步结束后会返回FSDataOutputStream的对象,象读文件的时候相似,FSDataOutputStream被封装成DFSOutputStream.DFSOutputStream可以协调namenode和datanode。客户端开始写数据到DFSOutputStream,DFSOutputStream会把数据切成一个个小packet,然后排成队列data quene。
    4. DataStreamer会去处理接受data quene,他先问询namenode这个新的block最适合存储的在哪几个datanode里(参考第二小节),比如重复数是3,那么就找到3个最适合的datanode,把他们排成一个pipeline.DataStreamer把packet按队列输出到管道的第一个datanode中,第一个datanode又把packet输出到第二个datanode中,以此类推。
    5. DFSOutputStream还有一个对列叫ack quene,也是有packet组成,等待datanode的收到响应,当pipeline中的所有datanode都表示已经收到的时候,这时akc quene才会把对应的packet包移除掉。如果在写的过程中某个datanode发生错误,会采取以下几步:1) pipeline被关闭掉;2)为了防止防止丢包ack quene里的packet会同步到data quene里;3)把产生错误的datanode上当前在写但未完成的block删掉;4)block剩下的部分被写到剩下的两个正常的datanode中;5)namenode找到另外的datanode去创建这个块的复制。当然,这些操作对客户端来说是无感知的。
    6. 客户端完成写数据后调用close方法关闭写入流
    7. DataStreamer把剩余得包都刷到pipeline里然后等待ack信息,收到最后一个ack后,通知datanode把文件标示为已完成。

    注:HDFS客户端需要首先获得对文件操作的授权,然后才能对文件进行写操作。在此期间,其他的客户端都不能对该文件进行写操作。被授权的客户端通过向NameNode发送心跳信号来定期更新授权的状态。当文件关闭时,授权会被回收。文件授权期限分为软限制期和硬限制期两个等级。当处于软限制期内时,写文件的客户端独占对文件的访问权。当软限制过期后,如果客户端无法关闭文件,或没有释放对文件的授权,其他客户端即可以预定获取授权。当硬限制期过期后(一小时左右),如果此时客户端还没有更新(释放)授权,HDFS会认为原客户端已经退出,并自动终止文件的写行为,收回文件控制授权。文件的写控制授权并不会阻止其他客户端对文件进行读操作。因此一个文件可以有多个并行的客户端对其进行读取。
    客户端执行write操作后,写完的block才是可见的,正在写的block对客户端是不可见的,只有调用hsync方法,客户端才确保该文件被写操作已经全部完成,当客户端调用close方法时会默认调用hsync方法。是否需要手动调用取决你根据程序需要在数据健壮性和吞吐率之间的权衡
    HDFS文件由多个文件块组成。当需要创建一个新文件块时,NameNode会生成唯一的块ID,分配块空间,以及决定将块和块的备份副本存储到哪些DataNode节点上。DataNode节点会形成一个管道,管道中DataNode节点的顺序能够确保从客户端到上一DataNode节点的总体网络距离最小。文件的则以有序包(sequence of packets)的形式被推送到管道。应用程序客户端创建第一个缓冲区,并向其中写入字节。第一个缓冲区被填满后(一般是64 KB大小),数据会被推送到管道。后续的包随时可以推送,并不需要等前一个包发送成功并发回通知(这被称为“未答复发送”——译者注)。不过,这种未答复发送包的数目会根据客户端所限定的“未答复包窗口”(outstanding packets windows)的大小进行限制。

SELECT返回指定区间行

  • ORACLE伪列ROWNUM 中包含有当前的行号,但ROWNUM只能做 < 或者 <= 操作,而不能做 > 或者 >= 以及 BETWEEN 操作,如:
    Select * from dba_users where rownum<=10; –是可以的 Select * from dba_users where rownum>=10; –将不返回任何记录
  • 解决方案 select * from (select rownum rownumber,tn.* from tableName tn where rownum <= 3) t where t.rownumber >= 2; 子查询中的rownum必须有别名.

数据归一化处理(转)

连续型特征归一化

  • 数据的标准化(normalization)和归一化 数据的标准化(normalization)是将数据按比例缩放,使之落入一个小的特定区间。在某些比较和评价的指标处理中经常会用到,去除数据的单位限制,将其转化为无量纲的纯数值,便于不同单位或量级的指标能够进行比较和加权。其中最典型的就是数据的归一化处理,即将数据统一映射到[0,1]区间上。
    目前数据标准化方法有多种,归结起来可以分为直线型方法(如极值法、标准差法)、折线型方法(如三折线法)、曲线型方法(如半正态性分布)。不同的标准化方法,对系统的评价结果会产生不同的影响,然而不幸的是,在数据标准化方法的选择上,还没有通用的法则可以遵循。
  • 归一化的目标
    1 把数变为(0,1)之间的小数
    主要是为了数据处理方便提出来的,把数据映射到0~1范围之内处理,更加便捷快速,应该归到数字信号处理范畴之内。
    2 把有量纲表达式变为无量纲表达式
    归一化是一种简化计算的方式,即将有量纲的表达式,经过变换,化为无量纲的表达式,成为纯量。 比如,复数阻抗可以归一化书写:Z = R + jωL = R(1 + jωL/R) ,复数部分变成了纯数量了,没有量纲。
    另外,微波之中也就是电路分析、信号系统、电磁波传输等,有很多运算都可以如此处理,既保证了运算的便捷,又能凸现出物理量的本质含义。
  • 归一化后有两个好处
    1. 提升模型的收敛速度
      如下图,x1的取值为0-2000,而x2的取值为1-5,假如只有这两个特征,对其进行优化时,会得到一个窄长的椭圆形,导致在梯度下降时,梯度的方向为垂直等高线的方向而走之字形路线,这样会使迭代很慢,相比之下,右图的迭代就会很快(理解:也就是步长走多走少方向总是对的,不会走偏)
      normalization_1
    2. 提升模型的精度
      归一化的另一好处是提高精度,这在涉及到一些距离计算的算法时效果显著,比如算法要计算欧氏距离,上图中x2的取值范围比较小,涉及到距离计算时其对结果的影响远比x1带来的小,所以这就会造成精度的损失。所以归一化很有必要,他可以让各个特征对结果做出的贡献相同。
      在多指标评价体系中,由于各评价指标的性质不同,通常具有不同的量纲和数量级。当各指标间的水平相差很大时,如果直接用原始指标值进行分析,就会突出数值较高的指标在综合分析中的作用,相对削弱数值水平较低指标的作用。因此,为了保证结果的可靠性,需要对原始指标数据进行标准化处理。
      在数据分析之前,我们通常需要先将数据标准化(normalization),利用标准化后的数据进行数据分析。数据标准化也就是统计数据的指数化。数据标准化处理主要包括数据同趋化处理和无量纲化处理两个方面。数据同趋化处理主要解决不同性质数据问题,对不同性质指标直接加总不能正确反映不同作用力的综合结果,须先考虑改变逆指标数据性质,使所有指标对测评方案的作用力同趋化,再加总才能得出正确结果。数据无量纲化处理主要解决数据的可比性。经过上述标准化处理,原始数据均转换为无量纲化指标测评值,即各指标值都处于同一个数量级别上,可以进行综合测评分析。
      从经验上说,归一化是让不同维度之间的特征在数值上有一定比较性,可以大大提高分类器的准确性。 normalization_2
  • 数据需要归一化的机器学习算法
    1. 需要归一化的模型:
      有些模型在各个维度进行不均匀伸缩后,最优解与原来不等价,例如SVM(距离分界面远的也拉近了,支持向量变多?)。对于这样的模型,除非本来各维数据的分布范围就比较接近,否则必须进行标准化,以免模型参数被分布范围较大或较小的数据dominate。
      有些模型在各个维度进行不均匀伸缩后,最优解与原来等价,例如logistic regression(因为θ的大小本来就自学习出不同的feature的重要性吧?)。对于这样的模型,是否标准化理论上不会改变最优解。但是,由于实际求解往往使用迭代算法,如果目标函数的形状太“扁”,迭代算法可能收敛得很慢甚至不收敛。所以对于具有伸缩不变性的模型,最好也进行数据标准化。
    2. 不需要归一化的模型:
      ICA好像不需要归一化(因为独立成分如果归一化了就不独立了?)。
      基于平方损失的最小二乘法OLS不需要归一化。

常见的数据归一化方法

  • min-max标准化(Min-max normalization)/0-1标准化(0-1 normalization) 也叫离差标准化,是对原始数据的线性变换,使结果落到[0,1]区间,转换函数如下:
    其中max为样本数据的最大值,min为样本数据的最小值。
    def Normalization(x):
      return [(float(i)-min(x))/float(max(x)-min(x)) for i in x]
    

    如果想要将数据映射到[-1,1],则将公式换成:

    x∗=x−xmeanxmax−xmin
    

    x_mean表示数据的均值。

    def Normalization2(x):
      return [(float(i)-np.mean(x))/(max(x)-min(x)) for i in x]
    

    这种方法有一个缺陷就是当有新数据加入时,可能导致max和min的变化,需要重新定义。

  • log函数转换 通过以10为底的log函数转换的方法同样可以实现归一下,具体方法如下:
    x=log10(x)/log10(max)
    看了下网上很多介绍都是x
    =log10(x),其实是有问题的,这个结果并非一定落到[0,1]区间上,应该还要除以log10(max),max为样本数据最大值,并且所有的数据都要大于等于1。
  • atan函数转换 用反正切函数也可以实现数据的归一化。
    x=actn(x)2/π
    使用这个方法需要注意的是如果想映射的区间为[0,1],则数据都应该大于等于0,小于0的数据将被映射到[-1,0]区间上,而并非所有数据标准化的结果都映射到[0,1]区间上。
  • z-score 标准化(zero-mean normalization) 最常见的标准化方法就是Z标准化,也是SPSS中最为常用的标准化方法,spss默认的标准化方法就是z-score标准化。
    也叫标准差标准化,这种方法给予原始数据的均值(mean)和标准差(standard deviation)进行数据的标准化。
    经过处理的数据符合标准正态分布,即均值为0,标准差为1,其转化函数为:
    x∗=x−μσ
    其中μ为所有样本数据的均值,σ为所有样本数据的标准差。
    z-score标准化方法适用于属性A的最大值和最小值未知的情况,或有超出取值范围的离群数据的情况。
    标准化的公式很简单,步骤如下
    1.求出各变量(指标)的算术平均值(数学期望)xi和标准差si ;
    2.进行标准化处理:
    zij=(xij-xi)/si
    其中:zij为标准化后的变量值;xij为实际变量值。
    3.将逆指标前的正负号对调。
    标准化后的变量值围绕0上下波动,大于0说明高于平均水平,小于0说明低于平均水平。
    def z_score(x, axis):
      x = np.array(x).astype(float)
      xr = np.rollaxis(x, axis=axis)
      xr -= np.mean(x, axis=axis)
      xr /= np.std(x, axis=axis)
      # print(x)
      return x
    

    为什么z-score 标准化后的数据标准差为1?
    x-μ只改变均值,标准差不变,所以均值变为0
    (x-μ)/σ只会使标准差除以σ倍,所以标准差变为1 normalization_3

  • Decimal scaling小数定标标准化 这种方法通过移动数据的小数点位置来进行标准化。小数点移动多少位取决于属性A的取值中的最大绝对值。
    将属性A的原始值x使用decimal scaling标准化到x’的计算方法是:
    x’=x/(10^j)
    其中,j是满足条件的最小整数。
    例如 假定A的值由-986到917,A的最大绝对值为986,为使用小数定标标准化,我们用每个值除以1000(即,j=3),这样,-986被规范化为-0.986。
    注意,标准化会对原始数据做出改变,因此需要保存所使用的标准化方法的参数,以便对后续的数据进行统一的标准化。
  • Logistic/Softmax变换
    logistic函数和标准正态函数
    新数据=1/(1+e^(-原数据))
    P(i)=11+exp(−θTix)
    这个函数的作用就是使得P(i)在负无穷到0的区间趋向于0,在0到正无穷的区间趋向于1。同样,函数(包括下面的softmax)加入了e的幂函数正是为了两极化:正样本的结果将趋近于1,而负样本的结果趋近于0。这样为多类别分类提供了方便(可以把P(i)看作是样本属于类别i的概率)。
    logit(P) = log(P / (1-P)) = a + bx 以及 probit(P) = a + bx
    这两个连接函数的性质使得P的取值被放大到整个实数轴上。
    事实上可以把上面的公式改写一下:
    P = exp(a + bx) / (1 + exp(a + bx)) 或者 P = pnorm(a + b*x)(这个是标准正态分布的分布函数) normalization_4 normalization_5 Note: 上半部分图形显示了概率P随着自变量变化而变化的情况,下半部分图形显示了这种变化的速度的变化。可以看得出来,概率P与自变量仍然存在或多或少的线性关系,主要是在头尾两端被连接函数扭曲了,从而实现了[0,1]限制。同时,自变量取值靠近中间的时候,概率P变化比较快,自变量取值靠近两端的时候,概率P基本不再变化。这就跟我们的直观理解相符合了,似乎是某种边际效用递减的特点。

  • Softmax函数 是logistic函数的一种泛化,Softmax是一种形如下式的函数:
    假设我们有一个数组,V,Vi表示V中的第i个元素,那么这个元素的Softmax值就是
    也就是说,是该元素的指数,与所有元素指数和的比值
    为什么要取指数,第一个原因是要模拟 max 的行为,所以要让大的更大。第二个原因是需要一个可导的函数。
    通过softmax函数,可以使得P(i)的范围在[0,1]之间。在回归和分类问题中,通常θ是待求参数,通过寻找使得P(i)最大的θi作为最佳参数。
    此外Softmax函数同样可用于非线性估计,此时参数θ可根据现实意义使用其他列向量替代。
    Softmax函数得到的是一个[0,1]之间的值,且∑Kk=1P(i)=1,这个softmax求出的概率就是真正的概率,换句话说,这个概率等于期望。

  • 模糊量化模式 新数据=1/2+1/2sin[派3.1415/(极大值-极小值)*(X-(极大值-极小值)/2) ] X为原数据

离散型特征归一化(独热编码(One-Hot Encoding))

在很多机器学习任务中,特征并不总是连续值,而有可能是分类值.
例如,考虑一下的三个特征:

["male", "female"]
["from Europe", "from US", "from Asia"]
["uses Firefox", "uses Chrome", "uses Safari", "uses Internet Explorer"]

如果将上述特征用数字表示,效率会高很多.例如:

["male", "from US", "uses Internet Explorer"] 表示为[0, 1, 3]
["female", "from Asia", "uses Chrome"]表示为[1, 2, 1]

但是,即使转化为数字表示后,上述数据也不能直接用在我们的分类器中.因为,分类器往往默认数据数据是连续的(可以计算距离?),并且是有序的(而上面这个0并不是说比1要高级).但是,按照我们上述的表示,数字并不是有序的,而是随机分配的.

  • 独热编码 为了解决上述问题,其中一种可能的解决方法是采用独热编码(One-Hot Encoding).独热编码即 One-Hot 编码,又称一位有效编码,其方法是使用N位状态寄存器来对N个状态进行编码,每个状态都由他独立的寄存器位,并且在任意时候,其中只有一位有效.
    例如:
    自然状态码为:000,001,010,011,100,101
    独热编码为:000001,000010,000100,001000,010000,100000
    可以这样理解,对于每一个特征,如果它有m个可能值,那么经过独热编码后,就变成了m个二元特征(如成绩这个特征有好,中,差变成one-hot就是100, 010, 001).并且,这些特征互斥,每次只有一个激活.因此,数据会变成稀疏的.
    这样做的好处主要有:
    解决了分类器不好处理属性数据的问题
    在一定程度上也起到了扩充特征的作用
    举例
    基于Scikit-learn的例子:
    from sklearn import preprocessing
    enc = preprocessing.OneHotEncoder()
    enc.fit([[0, 0, 3], [1, 1, 0], [0, 2, 1], [1, 0, 2]])
    enc.transform([[0, 1, 3]]).toarray()
    

    输出结果:

    array([[ 1.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  1.]])
    

    Note: fit了4个数据3个特征,而transform了1个数据3个特征.第一个特征两种值(0: 10, 1: 01),第二个特征三种值(0: 100, 1: 010, 2: 001),第三个特征四种值(0: 1000, 1: 0100, 2: 0010, 3: 0001).所以转换[0, 1, 3]为[ 1., 0., 0., 1., 0., 0., 0., 0., 1.].

处理离散型特征和连续型特征并存的情况

  1. 离散型特征的处理方法: a) Binarize categorical/discrete features: For all categorical features, represent them as multiple boolean features. For example, instead of having one feature called marriage_status, have 3 boolean features - married_status_single, married_status_married, married_status_divorced and appropriately set these features to 1 or -1. As you can see, for every categorical feature, you are adding k binary feature where k is the number of values that the categorical feature takes.对于离散的特征基本就是按照one-hot编码,该离散特征有多少取值,就用多少维来表示该特征。
    为什么使用one-hot编码来处理离散型特征?
    1、Why do we binarize categorical features?
    We binarize the categorical input so that they can be thought of as a vector from the Euclidean space (we call this as embedding the vector in the Euclidean space).使用one-hot编码,将离散特征的取值扩展到了欧式空间,离散特征的某个取值就对应欧式空间的某个点。
    2、Why do we embed the feature vectors in the Euclidean space?
    Because many algorithms for classification/regression/clustering etc. requires computing distances between features or similarities between features. And many definitions of distances and similarities are defined over features in Euclidean space. So, we would like our features to lie in the Euclidean space as well.将离散特征通过one-hot编码映射到欧式空间,是因为,在回归,分类,聚类等机器学习算法中,特征之间距离的计算或相似度的计算是非常重要的,而我们常用的距离或相似度的计算都是在欧式空间的相似度计算,计算余弦相似性,基于的就是欧式空间。
    3、Why does embedding the feature vector in Euclidean space require us to binarize categorical features?
    Let us take an example of a dataset with just one feature (say job_type as per your example) and let us say it takes three values 1,2,3.
    Now, let us take three feature vectors x_1 = (1), x_2 = (2), x_3 = (3). What is the euclidean distance between x_1 and x_2, x_2 and x_3 & x_1 and x_3? d(x_1, x_2) = 1, d(x_2, x_3) = 1, d(x_1, x_3) = 2. This shows that distance between job type 1 and job type 2 is smaller than job type 1 and job type 3. Does this make sense? Can we even rationally define a proper distance between different job types? In many cases of categorical features, we can properly define distance between different values that the categorical feature takes. In such cases, isn’t it fair to assume that all categorical features are equally far away from each other?
    Now, let us see what happens when we binary the same feature vectors. Then, x_1 = (1, 0, 0), x_2 = (0, 1, 0), x_3 = (0, 0, 1). Now, what are the distances between them? They are sqrt(2). So, essentially, when we binarize the input, we implicitly state that all values of the categorical features are equally away from each other.
    将离散型特征使用one-hot编码,确实会让特征之间的距离计算更加合理。比如,有一个离散型特征,代表工作类型,该离散型特征,共有三个取值,不使用one-hot编码,其表示分别是x_1 = (1), x_2 = (2), x_3 = (3)。两个工作之间的距离是,(x_1, x_2) = 1, d(x_2, x_3) = 1, d(x_1, x_3) = 2。那么x_1和x_3工作之间就越不相似吗?显然这样的表示,计算出来的特征的距离是不合理。那如果使用one-hot编码,则得到x_1 = (1, 0, 0), x_2 = (0, 1, 0), x_3 = (0, 0, 1),那么两个工作之间的距离就都是sqrt(2).即每两个工作之间的距离是一样的,显得更合理。
    4、About the original question?
    Note that our reason for why binarize the categorical features is independent of the number of the values the categorical features take, so yes, even if the categorical feature takes 1000 values, we still would prefer to do binarization.
    5、Are there cases when we can avoid doing binarization?没必要用one-hot 编码的情形Yes. As we figured out earlier, the reason we binarize is because we want some meaningful distance relationship between the different values. As long as there is some meaningful distance relationship, we can avoid binarizing the categorical feature. For example, if you are building a classifier to classify a webpage as important entity page (a page important to a particular entity) or not and let us say that you have the rank of the webpage in the search result for that entity as a feature, then 1] note that the rank feature is categorical, 2] rank 1 and rank 2 are clearly closer to each other than rank 1 and rank 3, so the rank feature defines a meaningful distance relationship and so, in this case, we don’t have to binarize the categorical rank feature.
    More generally, if you can cluster the categorical values into disjoint subsets such that the subsets have meaningful distance relationship amongst them, then you don’t have binarize fully, instead you can split them only over these clusters. For example, if there is a categorical feature with 1000 values, but you can split these 1000 values into 2 groups of 400 and 600 (say) and within each group, the values have meaningful distance relationship, then instead of fully binarizing, you can just add 2 features, one for each cluster and that should be fine.
    将离散型特征进行one-hot编码的作用,是为了让距离计算更合理,但如果特征是离散的,并且不用one-hot编码就可以很合理的计算出距离,那么就没必要进行one-hot编码,比如,该离散特征共有1000个取值,我们分成两组,分别是400和600,两个小组之间的距离有合适的定义,组内的距离也有合适的定义,那就没必要用one-hot 编码。
    离散特征进行one-hot编码后,编码后的特征,其实每一维度的特征都可以看做是连续的特征。就可以跟对连续型特征的归一化方法一样,对每一维特征进行归一化。比如归一化到[-1,1]或归一化到均值为0,方差为1。
    有些情况不需要进行特征的归一化:
    It depends on your ML algorithms, some methods requires almost no efforts to normalize features or handle both continuous and discrete features, like tree based methods: c4.5, Cart, random Forrest, bagging or boosting. But most of parametric models (generalized linear models, neural network, SVM,etc) or methods using distance metrics (KNN, kernels, etc) will require careful work to achieve good results. Standard approaches including binary all features, 0 mean unit variance all continuous features, etc。
    基于树的方法是不需要进行特征的归一化,例如随机森林,bagging 和 boosting等。基于参数的模型或基于距离的模型,都是要进行特征的归一化。

one-hot编码为什么可以解决类别型数据的离散值问题

首先,one-hot编码是N位状态寄存器为N个状态进行编码的方式
eg:高、中、低不可分,→ 用0 0 0 三位编码之后变得可分了,并且成为互相独立的事件
→ 类似 SVM中,原本线性不可分的特征,经过project之后到高维之后变得可分了
GBDT处理高维稀疏矩阵的时候效果并不好,即使是低维的稀疏矩阵也未必比SVM好

  • Tree Model不太需要one-hot编码
    对于决策树来说,one-hot的本质是增加树的深度
    tree-model是在动态的过程中生成类似 One-Hot + Feature Crossing 的机制
    1. 一个特征或者多个特征最终转换成一个叶子节点作为编码 ,one-hot可以理解成三个独立事件
    2. 决策树是没有特征大小的概念的,只有特征处于他分布的哪一部分的概念
      one-hot可以解决线性可分问题 但是比不上label econding
      one-hot降维后的缺点:
      降维前可以交叉的降维后可能变得不能交叉
      树模型的训练过程:
      从根节点到叶子节点整条路中有多少个节点相当于交叉了多少次,所以树的模型是自行交叉
      eg:是否是长的 { 否(是→ 柚子,否 → 苹果) ,是 → 香蕉 } 园 cross 黄 → 形状 (圆,长) 颜色 (黄,红) one-hot度为4的样本
      使用树模型的叶子节点作为特征集交叉结果可以减少不必要的特征交叉的操作 或者减少维度和degree候选集
      eg 2 degree → 8的特征向量 树 → 3个叶子节点
      树模型:Ont-Hot + 高degree笛卡尔积 + lasso 要消耗更少的计算量和计算资源
      这就是为什么树模型之后可以stack线性模型
      nm的输入样本 → 决策树训练之后可以知道在哪一个叶子节点上 → 输出叶子节点的index → 变成一个n1的矩阵 → one-hot编码 → 可以得到一个n*o的矩阵(o是叶子节点的个数) → 训练一个线性模型
      典型的使用: GBDT + RF
      优点 : 节省做特征交叉的时间和空间
      如果只使用one-hot训练模型,特征之间是独立的
      对于现有模型的理解:(G(l(张量))):
      其中:l(·)为节点的模型
      G(·)为节点的拓扑方式
      神经网络:l(·)取逻辑回归模型
      G(·)取全连接的方式
      决策树: l(·)取LR
      G(·)取树形链接方式
      创新点: l(·)取 NB,SVM 单层NN ,等
      G(·)取怎样的信息传递方式

onehot编码实现

onehot = (np.arange(num_labels) == labels[:, None]).astype(np.float32)

from sklearn import preprocessing
enc = preprocessing.OneHotEncoder(sparse=False)
onehot = enc.fit_transform(nominal.values)

转自:http://blog.csdn.net/pipisorry/article/details/52247379

Hadoop机架感知(转)

背景

分布式的集群通常包含非常多的机器,由于受到机架槽位和交换机网口的限制,通常大型的分布式集群都会跨好几个机架,由多个机架上的机器共同组成一个分布式集群。机架内的机器之间的网络速度通常都会高于跨机架机器之间的网络速度,并且机架之间机器的网络通信通常受到上层交换机间网络带宽的限制。

</br>具体到Hadoop集群,由于hadoop的HDFS对数据文件的分布式存放是按照分块block存储,每个block会有多个副本(默认为3),并且为了数据的安全和高效,所以hadoop默认对3个副本的存放策略为:

  • 第一个block副本放在和client所在的node里(如果client不在集群范围内,则这第一个node是随机选取的)。
  • 第二个副本放置在与第一个节点不同的机架中的node中(随机选择)。
  • 第三个副本似乎放置在与第一个副本所在节点同一机架的另一个节点上 如果还有更多的副本就随机放在集群的node里。

    </br>这样的策略可以保证对该block所属文件的访问能够优先在本rack下找到,如果整个rack发生了异常,也可以在另外的rack上找到该block的副本。这样足够的高效,并且同时做到了数据的容错。

    </br>但是,hadoop对机架的感知并非是自适应的,亦即,hadoop集群分辨某台slave机器是属于哪个rack并非是只能的感知的,而是需要hadoop的管理者人为的告知hadoop哪台机器属于哪个rack,这样在hadoop的namenode启动初始化时,会将这些机器与rack的对应信息保存在内存中,用来作为对接下来所有的HDFS的写块操作分配datanode列表时(比如3个block对应三台datanode)的选择datanode策略,做到hadoop allocate block的策略:尽量将三个副本分布到不同的rack。 接下来的问题就是:通过什么方式能够告知hadoop namenode哪些slaves机器属于哪个rack?以下是配置步骤。

配置

默认情况下,hadoop的机架感知是没有被启用的。所以,在通常情况下,hadoop集群的HDFS在选机器的时候,是随机选择的,也就是说,很有可能在写数据时,hadoop将第一块数据block1写到了rack1上,然后随机的选择下将block2写入到了rack2下,此时两个rack之间产生了数据传输的流量,再接下来,在随机的情况下,又将block3重新又写回了rack1,此时,两个rack之间又产生了一次数据流量。
</br>在job处理的数据量非常的大,或者往hadoop推送的数据量非常大的时候,这种情况会造成rack之间的网络流量成倍的上升,成为性能的瓶颈,进而影响作业的性能以至于整个集群的服务。要将hadoop机架感知的功能启用,配置非常简单,在namenode所在机器的hadoop-site.xml配置文件中配置一个选项:

<property>
  <name>topology.script.file.name</name>
  <value>/path/to/RackAware.py</value>
</property>

这个配置选项的value指定为一个可执行程序,通常为一个脚本,该脚本接受一个参数,输出一个值。接受的参数通常为某台datanode机器的ip地址,而输出的值通常为该ip地址对应的datanode所在的rack,例如”/rack1”。Namenode启动时,会判断该配置选项是否为空,如果非空,则表示已经用机架感知的配置,此时namenode会根据配置寻找该脚本,并在接收到每一个datanode的heartbeat时,将该datanode的ip地址作为参数传给该脚本运行,并将得到的输出作为该datanode所属的机架,保存到内存的一个map中。

</br>至于脚本的编写,就需要将真实的网络拓朴和机架信息了解清楚后,通过该脚本能够将机器的ip地址正确的映射到相应的机架上去。一个简单的实现如下:

#!/usr/bin/python  
#-*-coding:UTF-8 -*-  
import sys  
  
rack = {"hadoopnode-176.tj":"rack1",  
        "hadoopnode-178.tj":"rack1",  
        "hadoopnode-179.tj":"rack1",  
        "hadoopnode-180.tj":"rack1",  
        "hadoopnode-186.tj":"rack2",  
        "hadoopnode-187.tj":"rack2",  
        "hadoopnode-188.tj":"rack2",  
        "hadoopnode-190.tj":"rack2",  
        "192.168.1.15":"rack1",  
        "192.168.1.17":"rack1",  
        "192.168.1.18":"rack1",  
        "192.168.1.19":"rack1",  
        "192.168.1.25":"rack2",  
        "192.168.1.26":"rack2",  
        "192.168.1.27":"rack2",  
        "192.168.1.29":"rack2",  
        }  
  
  
if __name__=="__main__":  
    print "/" + rack.get(sys.argv[1],"rack0") 

由于没有找到确切的文档说明 到底是主机名还是ip地址会被传入到脚本,所以在脚本中最好兼容主机名和ip地址,如果机房架构比较复杂的话,脚本可以返回如:/dc1/rack1 类似的字符串。

执行命令:chmod +x RackAware.py

重启namenode,如果配置成功,namenode启动日志中会输出:

2011-12-21 14:28:44,495 INFO org.apache.hadoop.net.NetworkTopology: Adding a new node: /rack1/192.168.1.15:50010  

网络拓扑机器之间的距离

这里基于一个网络拓扑案例,介绍在复杂的网络拓扑中hadoop集群每台机器之间的距离 rock 有了机架感知,NameNode就可以画出上图所示的datanode网络拓扑图。D1,R1都是交换机,最底层是datanode。则H1的rackid=/D1/R1/H1,H1的parent是R1,R1的是D1。这些rackid信息可以通过topology.script.file.name配置。有了这些rackid信息就可以计算出任意两台datanode之间的距离。

distance(/D1/R1/H1,/D1/R1/H1)=0  相同的datanode
distance(/D1/R1/H1,/D1/R1/H2)=2  同一rack下的不同datanode
distance(/D1/R1/H1,/D1/R1/H4)=4  同一IDC下的不同datanode
distance(/D1/R1/H1,/D2/R3/H7)=6  不同IDC下的datanode

转自:http://www.cnblogs.com/ggjucheng/archive/2013/01/03/2843015.html

Scala的implicit关键字

隐式参数

当我们在定义方法时,可以把最后一个参数列表标记为implicit,表示该组参数是隐式参数.一个方法只会有一个隐式参数列表,置于方法的最后一个参数列表.如果方法有多个隐式参数,只需一个implicit修饰即可.当调用包含隐式参数的方法是,如果当前上下文中有合适的隐式值,则编译器会自动为改组参数填充合适的值,如果没有编译器会抛出异常.当然,标记为隐式参数的我们也可以手动为该参数添加默认值.

隐式地转换类型

使用隐含转换将变量转换成预期的类型是编译器最先使用 implicit 的地方.这个规则非常简单,当编译器看到类型X而却需要类型Y,它就在当前作用域查找是否定义了从类型X到类型Y的隐式定义.

隐式调用函数

隐式调用函数可以转换调用方法的对象,比如但编译器看到X .method,而类型 X 没有定义 method(包括基类)方法,那么编译器就查找作用域内定义的从 X 到其它对象的类型转换,比如 Y,而类型Y定义了 method 方法,编译器就首先使用隐含类型转换把 X 转换成 Y,然后调用 Y 的 method.

Zeppelin安装并集成spark2.x

  • 下载Zeppelin:zeppelin.apache.org下载 zeppelin-0.7.2-bin-all.tgz(本次使用的是0.7.2)
  • 解压 tar zxvf zeppelin-0.7.2-bin-all.tgz
  • 修改配置文件 ``` shell cp zeppelin-env.sh.template zeppelin-env.sh cp zeppelin-site.xml.template zeppelin-site.xml 修改 zeppelin-env.sh export SPARK_HOME=/opt/cloudera/parcels/SPARK2/lib/spark2 export HADOOP_CONF_DIR=/opt/cloudera/parcels/CDH/lib/hadoop export ZEPPELIN_INTP_CLASSPATH_OVERRIDES=/etc/hive/conf export MASTER=”yarn-client” export ZEPPELIN_PID_DIR=/var/run/zeppelin export ZEPPELIN_LOG_DIR=/var/log/zeppelin export ZEPPELIN_CLASSPATH=”${SPARK_CLASSPATH}” 修改 zeppelin-site.xml
zeppelin.server.port 7080 Server port.
* 启动 ./bin/zeppelin-daemon.sh start

* 异常
``` shell
1, java.lang.NoSuchMethodError: org.apache.hadoop.io.retry.RetryPolicies.retryOtherThanRemoteException
(Lorg/apache/hadoop/io/retry/RetryPolicy;Ljava/util/Map;)Lorg/apache/hadoop/io/retry/RetryPolicy;
解决方案:替换hadoop jar
mv zeppelin-0.7.2-bin-all/lib/hadoop-annotations-*.jar /opt/zeppelin-0.7.2-bin-all/lib/hadoop-annotations-*.jar.bak
mv zeppelin-0.7.2-bin-all/lib/hadoop-auth-*.jar /opt/zeppelin-0.7.2-bin-all/lib/hadoop-auth-*.jar.bak
mv zeppelin-0.7.2-bin-all/lib/hadoop-common-*.jar /opt/zeppelin-0.7.2-bin-all/lib/hadoop-common-*.jar.bak
cp /opt/cloudera/parcels/CDH/lib/hadoop/lib/hadoop-annotations-*.jar /opt/zeppelin-0.7.2-bin-all/lib
cp /opt/cloudera/parcels/CDH/lib/hadoop/lib/hadoop-auth-*.jar /opt/zeppelin-0.7.2-bin-all/lib
cp /opt/cloudera/parcels/CDH/lib/hadoop/lib/hadoop-common-*.jar /opt/zeppelin-0.7.2-bin-all/lib

2, com.fasterxml.jackson.databind.JsonMappingException: Jackson version is too old 2.5.3
解决方案:替换Jackson jar
mv zeppelin-0.7.2-bin-all/lib/jackson-annotations-2.5.3.jar /opt/zeppelin-0.7.2-bin-all/lib/jackson-annotations-2.5.3.jar.bak
mv zeppelin-0.7.2-bin-all/lib/jackson-core-2.5.3.jar /opt/zeppelin-0.7.2-bin-all/lib/jackson-core-2.5.3.jar
mv zeppelin-0.7.2-bin-all/lib/jackson-databind-2.5.3.jar /opt/zeppelin-0.7.2-bin-all/lib/jackson-databind-2.5.3.jar.bak
cp /opt/cloudera/parcels/SPARK2/lib/spark2/jars/jackson-annotations-2.6.5.jar /opt/zeppelin-0.7.2-bin-all/lib
cp /opt/cloudera/parcels/SPARK2/lib/spark2/jars/jackson-core-2.6.5.jar /opt/zeppelin-0.7.2-bin-all/lib
cp /opt/cloudera/parcels/SPARK2/lib/spark2/jars/jackson-databind-2.6.5.jar /opt/zeppelin-0.7.2-bin-all/lib

Centos7.3无损磁盘空间调整

Centos7.3无损磁盘空间调整

  • 查看磁盘空间
    [root@slave1 ~]# df -h
    Filesystem           Size  Used Avail Use% Mounted on
    /dev/mapper/cl-root   50G   28G   23G  55% /
    devtmpfs             6.8G     0  6.8G   0% /dev
    tmpfs                6.7G   84K  6.7G   1% /dev/shm
    tmpfs                6.7G   41M  6.7G   1% /run
    tmpfs                6.7G     0  6.7G   0% /sys/fs/cgroup
    /dev/xvda1          1014M  173M  842M  17% /boot
    /dev/mapper/cl-home  343G   72M  343G   1% /home
    tmpfs                1.4G     0  1.4G   0% /run/user/0
    tmpfs                1.4G   16K  1.4G   1% /run/user/42
    cm_processes         6.7G  251M  6.5G   4% /run/cloudera-scm-agent/process
    tmpfs                1.4G     0  1.4G   0% /run/user/1000
    
  • 备份数据
    [root@slave1 ~]# tar zcvf /tmp/home.tar /home
    
  • 取消挂载
    [root@slave1 ~]# umount /home
    umount: /home: target is busy.
          (In some cases useful info about processes that use
           the device is found by lsof(8) or fuser(1))
    You have new mail in /var/spool/mail/root
    
  • 查看磁盘占用(因为占用,无法取消挂载)
    [root@slave1 ~]# fuser -m -v /home
                       USER        PID ACCESS COMMAND
    /home:               root     kernel mount /home
                       cm        23054 ..c.. sftp-server
                       cm        23071 ..c.. bash
    
  • 解除磁盘占用
    [root@slave1 ~]# fuser -m -v -i -k /home
                       USER        PID ACCESS COMMAND
    /home:               root     kernel mount /home
                       cm        23054 ..c.. sftp-server
                       cm        23071 ..c.. bash
    Kill process 23054 ? (y/N) y
    Kill process 23071 ? (y/N) y
    
  • 取消挂载
    [root@slave1 ~]# umount /home
    
  • 删除/home所在的lv
    [root@slave1 ~]# lvremove /dev/mapper/cl-home
    Do you really want to remove active logical volume cl/home? [y/n]: y
    Logical volume "home" successfully removed
    
  • 扩展/root所在的lv,增加290G
    [root@slave1 ~]# lvextend -L +290G /dev/mapper/cl-root
    Size of logical volume cl/root changed from 50.00 GiB (12800 extents) to 340.00 GiB (87040 extents).
    Logical volume cl/root successfully resized.
    
  • 扩展/root文件系统
    [root@slave1 ~]# xfs_growfs /dev/mapper/cl-root
    meta-data=/dev/mapper/cl-root    isize=512    agcount=4, agsize=3276800 blks
           =                       sectsz=512   attr=2, projid32bit=1
           =                       crc=1        finobt=0 spinodes=0
    data     =                       bsize=4096   blocks=13107200, imaxpct=25
           =                       sunit=0      swidth=0 blks
    naming   =version 2              bsize=4096   ascii-ci=0 ftype=1
    log      =internal               bsize=4096   blocks=6400, version=2
           =                       sectsz=512   sunit=0 blks, lazy-count=1
    realtime =none                   extsz=4096   blocks=0, rtextents=0
    data blocks changed from 13107200 to 89128960
    
  • 重新创建home lv (50G)
    [root@slave1 ~]# lvcreate -L 50G -n home cl
    Logical volume "home" created.
    
  • 创建文件系统
    [root@slave1 ~]# mkfs.xfs /dev/cl/home
    meta-data=/dev/cl/home           isize=512    agcount=4, agsize=3276800 blks
           =                       sectsz=512   attr=2, projid32bit=1
           =                       crc=1        finobt=0, sparse=0
    data     =                       bsize=4096   blocks=13107200, imaxpct=25
           =                       sunit=0      swidth=0 blks
    naming   =version 2              bsize=4096   ascii-ci=0 ftype=1
    log      =internal log           bsize=4096   blocks=6400, version=2
           =                       sectsz=512   sunit=0 blks, lazy-count=1
    realtime =none                   extsz=4096   blocks=0, rtextents=0
    
  • 挂载/home
    [root@slave1 ~]# mount /dev/cl/home /home
    
  • 查看磁盘空间
    [root@slave1 ~]# df -h
    Filesystem           Size  Used Avail Use% Mounted on
    /dev/mapper/cl-root  340G   28G  313G   8% /
    devtmpfs             6.8G     0  6.8G   0% /dev
    tmpfs                6.7G   84K  6.7G   1% /dev/shm
    tmpfs                6.7G   41M  6.7G   1% /run
    tmpfs                6.7G     0  6.7G   0% /sys/fs/cgroup
    /dev/xvda1          1014M  173M  842M  17% /boot
    tmpfs                1.4G     0  1.4G   0% /run/user/0
    tmpfs                1.4G   16K  1.4G   1% /run/user/42
    cm_processes         6.7G  251M  6.5G   4% /run/cloudera-scm-agent/process
    /dev/mapper/cl-home   50G   33M   50G   1% /home
    
  • 恢复文件
    [root@slave1 ~]# tar zxvf /tmp/home.tar -C /
    

Redis安装异常

jemalloc/jemalloc.h: No such file or directory

  • 异常详情
    [root@master redis-3.2.9]# make
    cd src && make all
    make[1]: Entering directory `/opt/redis-3.2.9/src'
      CC adlist.o
    In file included from adlist.c:34:0:
    zmalloc.h:50:31: fatal error: jemalloc/jemalloc.h: No such file or directory
     #include <jemalloc/jemalloc.h>
                                 ^
    compilation terminated.
    make[1]: *** [adlist.o] Error 1
    make[1]: Leaving directory `/opt/redis-3.2.9/src'
    make: *** [all] Error 2
    
  • 解决方案 ``` shell github(https://github.com/antirez/redis)的README.md

Allocator Selecting a non-default memory allocator when building Redis is done by setting the MALLOC environment variable. Redis is compiled and linked against libc malloc by default, with the exception of jemalloc being the default on Linux systems. This default was picked because jemalloc has proven to have fewer fragmentation problems than libc malloc.

To force compiling against libc malloc, use:

% make MALLOC=libc To compile against jemalloc on Mac OS X systems, use:

% make MALLOC=jemalloc

说关于分配器allocator, 如果有MALLOC 这个 环境变量,会有用这个环境变量的去建立Redis。 而且libc 并不是默认的 分配器, 默认的是 jemalloc, 因为jemalloc 被证明 有更少的 fragmentation problems 比libc。 但是如果你又没有jemalloc 而只有 libc 当然 make 出错。所以加这么一个参数。

[root@master redis-3.2.9]# make MALLOC=libc 解决 ```

Spark Serializable Exception

object not serializable (class: scala.util.matching.Regex$Match xxx)

  • 解决方案
    val spark = SparkSession
    ...
    spark.config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
    

Cloudera集成Spark2.x启动异常

使用Cloudera Manager 5.10.0集成Spark2.1.0 启动异常

  • Cloudera Manager 5.10.0集成Spark2.1.0步骤 https://www.cloudera.com/documentation/spark2/latest/topics/spark2_packaging.html
  • 安装好Spark2后/etc/spark2/conf/ 下面是空的,可以先拷贝/etc/spark/conf/下的配置文件过来
    cp -r /etc/spark/conf.cloudera.spark_on_yarn/ /etc/spark2/conf/
    
  • 修改spark2下面的配置
    vi spark-env.sh
    export SPARK_HOME=/xxx/cloudera/parcels/SPARK2-2.1.0.cloudera1-1.cdh5.7.0.p0.120904/lib/spark2
    
    vi spark-defaults.conf
    spark.yarn.jars=/xxx/cloudera/parcels/SPARK2-2.1.0.cloudera1-1.cdh5.7.0.p0.120904/lib/spark2/jars/* (这里你可以后续改为HDFS路径)
    spark.master=yarn
    

Cloudera分配Parcel卡住

  • 在Cloudera Manager5.10.0中准备取消Spark2.1.0的分配,可能由于我再一台机器上误删了部分文件,导致该机器一直无法取消分配
  • cloudera-scm-agent日志提示为:Deleting unmanaged parcel xxxxxxx
  • 解决方案:删除该机器 xxx/cloudera/parcel-repo目录下的Spark2.1.0相关文件,删除 xxx/cloudera/parcels/.flood/目录下的Spark2.1.0相关文件
  • 重启cloudera-scm-agent服务

Windows跑MR异常

java.lang.UnsatisfiedLinkError: org.apache.hadoop.io.nativeio.NativeIO$Windows.access0(Ljava/lang/String;I)Z

  • 解决方案
    Windows本地已经配好了HADOOP_HOME,也安装了hadoop windows环境运行的所需插件
    通过异常信息可以看出是NativeIO这个类报错了,又不想改源码后再次编译,可以直接在
    项目中创建org.apache.hadoop.io.nativeio包,并创建NativeIO类,修改
    return access0(path, desiredAccess.accessRight())为retuen true.
    

Spark SQL内置函数

  • 聚合函数
    approxCountDistinct, avg, count, countDistinct, first, last, max, mean, min, sum, sumDistinct
    
  • 集合函数
    array_contains, explode, size, sort_array
    
  • 日期时间转换
    unix_timestamp, from_unixtime, to_date, quarter, day, dayofyear, weekofyear, from_utc_timestamp, to_utc_timestamp 
    
  • 从日期时间中提取字段
    year, month, dayofmonth, hour, minute, second
    
  • 日期/时间计算
    datediff, date_add, date_sub, add_months, last_day, next_day, months_between 
    
  • 获取当前时间等
    current_date, current_timestamp, trunc, date_format
    
  • 数学函数
    abs, acros, asin, atan, atan2, bin, cbrt, ceil, conv, cos, sosh, exp, expm1, factorial, floor, hex, hypot, log, log10, log1p, log2, pmod, pow, rint, round, shiftLeft, shiftRight, shiftRightUnsigned, signum, sin, sinh, sqrt, tan, tanh, toDegrees, toRadians, unhex
    
  • 混合函数
    array, bitwiseNOT, callUDF, coalesce, crc32, greatest, if, inputFileName, isNaN, isnotnull, isnull, least, lit, md5, monotonicallyIncreasingId, nanvl, negate, not, rand, randn, sha, sha1, sparkPartitionId, struct, when
    
  • 字符串函数
    ascii, base64, concat, concat_ws, decode, encode, format_number, format_string, get_json_object, initcap, instr, length, levenshtein, locate, lower, lpad, ltrim, printf, regexp_extract, regexp_replace, repeat, reverse, rpad, rtrim, soundex, space, split, substring, substring_index, translate, trim, unb
    
  • 窗口函数
    cumeDist, denseRank, lag, lead, ntile, percentRank, rank, rowNumber
    ase64, upper
    

Cloudera集群IP修改

1, 首先在安装cloudera-manager的主机上,停止所有的cloudera管理进程

service cloudera-scm-agent stop
service cloudera-scm-server-db stop (我用的外部数据库mysql,所以不需要这步)
service cloudera-scm-server stop

2, 查看postgresql的scm用户的密码 (我用的外部数据库mysql,所以不需要这步)

grep password /etc/cloudera-scm-server/db.properties

3, 登录mysql / postgresql数据库

mysql: mysql -uroot -p
postgresql: psql -h localhost -p 7432 -U scm
提示你输入密码,密码就是上面第二步的密码

4, 修改mysql / postgresql数据库中的数据(即主机的ip)

查看ip
mysql: select host_id, host_identifier, name, ip_address from hosts;
postgresql: select host_id, host_identifier, name, ip_address from HOSTS;
修改各主机的ip(分别修改各主机的ip)
mysql: update HOSTS set ip_address = '新的ip' where host_id='1';
postgresql: update hosts set (ip_address) = ('新的ip') where host_id='1';
修改你需要修改的机器ip

5, 修改所有Hadoop集群机器中的cloudera-scm-agent的配置文件

vi /etc/cloudera-scm-agent/config.ini
修改host为新的ip

6, 修改各主机的/etc/hosts文件,将现在的hostname与ip地址对应上

7, 重启服务

service cloudera-scm-server-db start(我用的外部数据库mysql,所以不需要这步)
service cloudera-scm-server start
service cloudera-scm-agent start

Cloudera HUE配置自定义MySql数据库

ref:http://www.cloudera.com/documentation/enterprise/latest/topics/cm_ig_mysql.html#cmig_topic_5_5

In the Cloudera Manager Admin Console, go to the Hue service status page.
  1. Select Actions > Stop. Confirm you want to stop the service by clicking Stop.
  2. Select Actions > Dump Database. Confirm you want to dump the database by clicking Dump Database.
    Note the host to which the dump was written under Step in the Dump Database Command window. You can also find it by selecting Commands > Recent Commands > Dump Database.
    Open a terminal window for the host and go to the dump file in /tmp/hue_database_dump.json.
    Remove all JSON objects with useradmin.userprofile in the model field, for example:
    {
    "pk": 14,
    "model": "useradmin.userprofile",
    "fields":
    { "creation_method": "EXTERNAL", "user": 14, "home_directory": "/user/tuser2" }
    },
    Set strict mode in /etc/my.cnf and restart MySQL:
    [mysqld]
    sql_mode=STRICT_ALL_TABLES
    Create a new database and grant privileges to a Hue user to manage this database. For example:
    mysql> create database hue;
    Query OK, 1 row affected (0.01 sec)
    mysql> grant all on hue.* to 'hue'@'localhost' identified by 'secretpassword';
    Query OK, 0 rows affected (0.00 sec)
    
  3. In the Cloudera Manager Admin Console, click the Hue service. Click the Configuration tab.
  4. Select Scope > All.
  5. Select Category > Database.
    Specify the settings for Hue Database Type, Hue Database Hostname, Hue Database Port, Hue Database Username, Hue Database Password, and Hue Database Name. For example, for a MySQL database on the local host, you might use the following values:
    Hue Database Type = mysql
    Hue Database Hostname = host
    Hue Database Port = 3306
    Hue Database Username = hue
    Hue Database Password = secretpassword
    Hue Database Name = hue
    
  6. Optionally restore the Hue data to the new database: Select Actions > Synchronize Database.
    Determine the foreign key ID.
    $ mysql -uhue -psecretpassword
    mysql > SHOW CREATE TABLE auth_permission;
    (InnoDB only) Drop the foreign key that you retrieved in the previous step.
    mysql > ALTER TABLE auth_permission DROP FOREIGN KEY content_type_id_refs_id_XXXXXX;
    Delete the rows in the django_content_type table.
    mysql > DELETE FROM hue.django_content_type;
    
  7. In Hue service instance page, click Actions > Load Database. Confirm you want to load the database by clicking Load Database.
    (InnoDB only) Add back the foreign key.
    mysql > ALTER TABLE auth_permission ADD FOREIGN KEY (content_type_id) REFERENCES django_content_type (id);
    
  8. Start the Hue service.

Centos7安装Oracle XE

  • 下载Oracle XE http://www.oracle.com/technetwork/database/database-technologies/express-edition/downloads/index.html
  • 解压缩Oracle XE安装程序 unzip oracle-xe-11.2.0-1.0.x86_64.rpm.zip
  • 创建用户 ``` shell groupadd oinstall //创建oracle数据库安装组 groupadd dba //创建oracle数据库管理组 useradd -m -g oinstall -G dba oracle //创建oracle用户 id oracle uid=1001(oracle) gid=1001(oinstall) groups=1001(oinstall),502(dba)

passwd oracle //为Oracle用户设置密码: Changing password for user oracle. New UNIX password: BAD PASSWORD: it is based on a dictionary word Retype new UNIX password: passwd: all authentication tokens updated successfully.

* 建立安装目录
``` shell
chown -R oracle:oinstall /u01/app
chmod -R 775 /u01/app
  • 开始安装
    cd Disk1
    rpm -ivh oracle-xe-11.2.0-1.0.x86_64.rpm
    
  • 运行配置oracle xe的命令
    /etc/init.d/oracle-xe configure
    
  • 使用oracle用户修改bash_profile中环境变量 修改.bash_profile.在其中添加如下内容:
    TMP=/tmp
    TMPDIR=$TMP
    ORACLE_HOSTNAME=dbserver
    ORACLE_UNQNAME=ORADB 
    ORACLE_BASE=/u01/app/oracle 
    ORACLE_HOME=$ORACLE_BASE/product/11.2.0/xe
    ORACLE_SID=ORADB
    PATH=$ORACLE_HOME/bin:$PATH 
    LD_LIBRARY_PATH=$ORACLE_HOME/lib:/lib:/usr/lib 
    export TMP TMPDIR ORACLE_HOSTNAME ORACLE_UNQNAME ORACLE_BASE ORACLE_HOME ORACLE_SID PATH LD_LIBRARY_PATH
    
  • 测试是否成功
    echo $ORACLE_BASE
    sqlplus / as sysdba  #查看是否可以进入sql命令行
    

Cloudera中Oozie安装ExtJs

Cloudera安装Oozie过后进入Oozie的管理界面时提示要安装ExtJs才能使用.

  1. 下载 ExtJs的包wget http://archive.cloudera.com/gplextras/misc/ext-2.2.zip
  2. 解压 unzip ext-2.2.zip
  3. 移动 ext-2.2文件夹到/var/lib/oozie目录下mv ext-2.2 /var/lib/oozie/
  4. 授权 将ext-2.2文件夹拥有用户改为oozie chown -R oozie:oozie /var/lib/oozie/ext-2.2

再次进入oozie的web界面便可以正常使用了.

MySQL创建数据库与创建用户以及授权

创建数据库

create database 数据库名称 default character set utf8 collate utf8_general_ci;

创建用户

create user ‘用户名称’@’%’ identified by ‘用户密码’; (%:匹配所有主机,该地方还可以设置成’localhost’,代表只能本地访问,例如root账户默认为’localhost’)

用户授权数据库

grant select,insert,update,delete,create on 数据库名称.* to 用户名称; (*代表整个数据库)

立即启用修改

flush privileges;

取消用户所有数据库(表)的所有权限

revoke all on . from 用户名;

删除用户

delete from mysql.user where user=’用户名’;

删除数据库

drop database [schema名称|数据库名称];

Cloudera安装遇到的问题和解决方案

  • Unable to retrieve non-local non-loopback IP address. Seeing address: cm/127.0.0.1
    2017-04-18 09:40:29,308 ERROR ScmActive-0:com.cloudera.server.cmf.components.ScmActive: ScmActive: Unable to retrieve non-local non-loopback IP address. Seeing address: cm/127.0.0.1. 2017-04-18 09:40:29,308 ERROR ScmActive-0:com.cloudera.server.cmf.components.ScmActive: ScmActive failed. Bootup = false2017-04-18 09:40:29,308 ERROR ScmActive-0:com.cloudera.server.cmf.components.ScmActive: ScmActive was not able to access CM identity to validate it.2017-04-18 09:40:29,308 ERROR ScmActive-0:com.cloudera.server.cmf.components.ScmActive: ScmActive is deferring the validation to the next run in 15 seconds.2017-04-18 09:40:29,308 WARN ScmActive-0:com.cloudera.enterprise.AbstractWrappedEntityManager: Rolling back transaction that wasn't marked for rollback-only.java.lang.Exception: Non-thrown exception for stack trace.        
    at com.cloudera.enterprise.AbstractWrappedEntityManager.close(AbstractWrappedEntityManager.java:161)        
    at com.cloudera.cmf.persist.CmfEntityManager.close(CmfEntityManager.java:356)        
    at com.cloudera.server.cmf.components.ScmActive.markScmActive(ScmActive.java:224)        
    at com.cloudera.server.cmf.components.ScmActive.run(ScmActive.java:87)        
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)        
    at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:304)        
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:178)        
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)        
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)        
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)        
    at java.lang.Thread.run(Thread.java:745)
    

    解决方案: 是由于host文件导致,我的host文件多了一行127.0.0.1   cm,删除这行就OK了

  • Unable to create new directory at /var/lib/cloudera-host-monitor/ts/ts_entity_metadata
    2017-04-19 09:44:37,403 INFO com.cloudera.cmon.tstore.leveldb.LDBUtils: Creating directory /var/lib/cloudera-host-monitor/ts/ts_entity_metadata
    2017-04-19 09:44:37,404 ERROR com.cloudera.cmon.firehose.Main: Error creating LevelDB timeseries store in directory /var/lib/cloudera-host-monitor/ts
    java.io.IOException: Unable to create new directory at /var/lib/cloudera-host-monitor/ts/ts_entity_metadata
          at com.cloudera.cmon.tstore.leveldb.LDBUtils.openVersionedDB(LDBUtils.java:239)
          at com.cloudera.cmon.tstore.leveldb.LDBTimeSeriesMetadataStore.openMetadataDB(LDBTimeSeriesMetadataStore.java:148)
          at com.cloudera.cmon.tstore.leveldb.LDBTimeSeriesMetadataStore.<init>(LDBTimeSeriesMetadataStore.java:138)
          at com.cloudera.cmon.firehose.Main.main(Main.java:452)
    

    解决方案: 是由于文件夹权限导致,修改文件夹拥有人chown cloudera-scm:cloudera-scm /var/lib/cloudera-host-monitor和/var/lib/cloudera-service-monitor

  • ValueError: too many values to unpack 出现这个问题会导致parcel一直处于分发阶段
    [19/Apr/2017 14:23:20 +0000] 25170 MainThread agent        INFO     Using parcels directory from server provided value: /opt/cloudera/parcels
    [19/Apr/2017 14:23:20 +0000] 25170 MainThread parcel       INFO     Agent does create users/groups and apply file permissions
    [19/Apr/2017 14:23:20 +0000] 25170 MainThread parcel_cache INFO     Using /opt/cloudera/parcel-cache for parcel cache
    [19/Apr/2017 14:23:20 +0000] 25170 MainThread agent        ERROR    Caught unexpected exception in main loop.
    Traceback (most recent call last):
    File "/usr/lib64/cmf/agent/build/env/lib/python2.7/site-packages/cmf-5.10.0-py2.7.egg/cmf/agent.py", line 710, in __issue_heartbeat
      self._init_after_first_heartbeat_response(resp_data)
    File "/usr/lib64/cmf/agent/build/env/lib/python2.7/site-packages/cmf-5.10.0-py2.7.egg/cmf/agent.py", line 947, in _init_after_first_heartbeat_response
      self.client_configs.load()
    File "/usr/lib64/cmf/agent/build/env/lib/python2.7/site-packages/cmf-5.10.0-py2.7.egg/cmf/client_configs.py", line 682, in load
      new_deployed.update(self._lookup_alternatives(fname))
    File "/usr/lib64/cmf/agent/build/env/lib/python2.7/site-packages/cmf-5.10.0-py2.7.egg/cmf/client_configs.py", line 432, in _lookup_alternatives
      return self._parse_alternatives(alt_name, out)
    File "/usr/lib64/cmf/agent/build/env/lib/python2.7/site-packages/cmf-5.10.0-py2.7.egg/cmf/client_configs.py", line 444, in _parse_alternatives
      path, _, _, priority_str = line.rstrip().split(" ")
    ValueError: too many values to unpack
    

    解决方案: 修改/usr/lib64/cmf/agent/build/env/lib/python2.7/site-packages/cmf-5.10.0-py2.7.egg/cmf/client_configs.py脚本的第444行代码. 修改为:

    for line in output.splitlines():
        if line.startswith("/"):
          if len(line.rstrip().split(" "))<=4:
            path, _, _, priority_str = line.rstrip().split(" ")
    
            # Ignore the alternative if it's not managed by CM.
            if CM_MAGIC_PREFIX not in os.path.basename(path):
              continue
    
            try:
              priority = int(priority_str)
            except ValueError:
              THROTTLED_LOG.info("Failed to parse %s: %s", name, line)
    
            key = ClientConfigKey(name, path)
            value = ClientConfigValue(priority, self._read_generation(path))
            ret[key] = value
    
          else:
            pass
      return ret
    

    具体可以参考:http://blog.csdn.net/qq_23660243/article/details/60870527   感谢这位同学的解决方案.

  • Error, CM server guid updated
    [19/Apr/2017 14:43:41 +0000] 3700 MainThread agent        INFO     Using parcels directory from server provided value: /opt/cloudera/parcels
    [19/Apr/2017 14:43:41 +0000] 3700 MainThread agent        INFO     Using parcels directory from server provided value: /opt/cloudera/parcels
    [19/Apr/2017 14:43:41 +0000] 3700 MainThread agent        WARNING  Expected user root for /opt/cloudera/parcels but was cloudera-scm
    [19/Apr/2017 14:43:41 +0000] 3700 MainThread agent        WARNING  Expected group root for /opt/cloudera/parcels but was cloudera-scm
    [19/Apr/2017 14:43:41 +0000] 3700 MainThread parcel       INFO     Agent does create users/groups and apply file permissions
    [19/Apr/2017 14:43:41 +0000] 3700 MainThread parcel_cache INFO     Using /opt/cloudera/parcel-cache for parcel cache
    [19/Apr/2017 14:43:41 +0000] 3700 MainThread agent        ERROR    Error, CM server guid updated, expected 0fd6eac4-d1dc-4b46-90bb-b58c87fa3d1f, received c9bb909f-e7c0-4d56-b33e-26e3764adae8
    

    解决方案: rm -f /var/lib/cloudera-scm-agent/cm_guid 然后 service cloudera-scm-agent restart

  • Failed request to SCM: 302
    2017-04-19 15:01:36,610 INFO com.cloudera.cmf.BasicScmProxy: Failed request to SCM: 302
    2017-04-19 15:01:37,610 INFO com.cloudera.cmf.BasicScmProxy: Authentication to SCM required.
    2017-04-19 15:01:37,650 INFO com.cloudera.cmf.BasicScmProxy: Using encrypted credentials for SCM
    2017-04-19 15:01:37,653 INFO com.cloudera.cmf.BasicScmProxy: Authenticated to SCM.
    

    解决方案: 官方回复是

    The "Failed request to SCM: 302" message occurs when the Host Monitor attempts to communicate to Cloudera Manager but the session has expired.  The Host Monitor acts as a client that authenticates to SCM, so it is subject to session restrictions.
    The error can be ignored as the Host Monitor will re-authenticate and we do see that in your case that occurs.
    In fact, a code change to make the message an INFO message rather than ERROR is slated for Cloudera Manager 5.7.2 and 5.8.2.
    

    没啥大的影响,但我用的是5.10.0 ……

  • Unable to find the JDBC database jar on host 这个属于安装oozie和hive时用mysql作为元数据库时遇到的 解决方案:将mysql的连接驱动放到 /usr/share/java/mysql-connector-java.jar

  • 安装Kafka时java.lang.OutOfMemoryError: Java heap space
    # java.lang.OutOfMemoryError: Java heap space
    # -XX:OnOutOfMemoryError="/usr/lib64/cmf/service/common/killparent.sh"
    #   Executing /bin/sh -c "/usr/lib64/cmf/service/common/killparent.sh"...
    Wed Apr 19 16:15:33 CST 2017
    JAVA_HOME=/usr/java/jdk1.7.0_67-cloudera
    Using -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/kafka_kafka-KAFKA_BROKER-c07d94c57862124dad7ce85095d99c9f_pid22663.hprof -XX:OnOutOfMemoryError=/usr/lib64/cmf/service/common/killparent.sh as CSD_JAVA_OPTS
    Using /run/cloudera-scm-agent/process/114-kafka-KAFKA_BROKER as conf dir
    Using scripts/control.sh as process script
    CONF_DIR=/run/cloudera-scm-agent/process/114-kafka-KAFKA_BROKER
    CMF_CONF_DIR=/etc/cloudera-scm-agent
    

    解决方案: 修改Kafka的Java Heap Size of Broker 设置为broker_max_heap_size=256

  • “org.apache.log4j.RollingFileAppender” object is not assignable to a “org.apache.log4j.Appender” variable 这个是安装sqoop2时遇到的问题
    Thu Apr 20 10:29:05 CST 2017
    JAVA_HOME=/usr/java/jdk1.7.0_67-cloudera
    using 5 as CDH_VERSION
    CONF_DIR=/run/cloudera-scm-agent/process/238-sqoop-SQOOP_SERVER
    CMF_CONF_DIR=/etc/cloudera-scm-agent
    log4j:ERROR A "org.apache.log4j.RollingFileAppender" object is not assignable to a "org.apache.log4j.Appender" variable.
    log4j:ERROR The class "org.apache.log4j.Appender" was loaded by 
    log4j:ERROR [org.apache.catalina.loader.StandardClassLoader@52437b9a] whereas object of type 
    log4j:ERROR "org.apache.log4j.RollingFileAppender" was loaded by [WebappClassLoader
    context: /sqoop
    delegate: false
    repositories:
      /WEB-INF/classes/
    ----------> Parent Classloader:
    org.apache.catalina.loader.StandardClassLoader@52437b9a
    ].
    log4j:ERROR Could not instantiate appender named "RFA".
    log4j:WARN No appenders could be found for logger (org.apache.hadoop.util.NativeCodeLoader).
    log4j:WARN Please initialize the log4j system properly.
    log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
    

    解决方案:

    cd /opt/cloudera/parcels/CDH-5.10.0-1.cdh5.10.0.p0.41/lib/sqoop2/webapps/sqoop/WEB-INF/lib
    mv log4j-1.2.16.jar log4j-1.2.16.jar.bak
    刚开始我修改的/var/lib/sqoop2/tomcat-deployment/webapps/sqoop 目录下的pom文件和jar包,发现每次重启sqoop2,这个目录下的文件都会被覆盖,后来,通过 find / -name sqoop.war命令查找到/opt/cloudera/parcels/CDH-5.10.0-1.cdh5.10.0.p0.41/lib/sqoop2/ 这个目录下也存在,测试修改了下这个目录下的文件,生效了
    

Cloudera安装

安装介质准备:

  • 1,Cloudera Manager 安装包(rpm 包)
      bin文件下载地址: http://archive.cloudera.com/cm5/installer/
      rpm包下载地址: http://archive.cloudera.com/cm5/redhat/7/x86_64/cm/  (注意自己的系统版本)
    

    找到对应版本,本次安装5.10.0,将整个目录下载下来,可以使用迅雷,也可以在linux中使用wget.注意目录结构要一致.

  • 2,CDH 安装包(parcel 包) 下载地址: http://archive.cloudera.com/cdh5/parcels/ 由于我用的是centos7.3,则需要选择e17.对应的3个文件为:
       CDH-5.10.0-1.cdh5.10.0.p0.41-el7.parcel
       CDH-5.10.0-1.cdh5.10.0.p0.41-el7.parcel.sha1
       manifest.json
    
  • 3,将下载的文件上传到centos中的/var/www/html/目录下,注意目录结构和下载的路径的目录结构一致. 我的目录结构为
      /var/www/html/cm   
      /var/www/html/cdh 
      /var/www/html/cloudera-manager-installer5.10.0.bin
    

安装环境准备:

  • 1,修改hosts vi /etc/hosts 添加其他机器ip与主机名映射 如192.168.1.2 master 根据自己实际情况指定
  • 2,修改主机名 hostnamectl set-hostname 自己的hostname
  • 3,关闭防火墙
      查看状态:service firewalld status 
      临时关闭:service firewalld stop 
      查看防火墙启动级别:chkconfig firewalld --list 
      永久关闭:chkconfig firewalld off
    
  • 4,关闭selinux
      vi /etc/sysconfig/selinux 
      注释掉 SELINUX=enforcing 
      添加 SELINUX=disabled 
      查看状态:/usr/sbin/sestatus-v 
      注意:需要重启生效
    
  • 5,免key登录
      生成公钥和私钥: ssh-keygen -t rsa 
      复制公钥到免 key 主机: ssh-copy-id 指定主机地址 ip (如果有需要,自己也要和自己免 key)
    
  • 6,启动httpd服务
      查看服务状态:service httpd status 
      打开服务:service httpd start
    
  • 7,时间同步
      手动设置 查看服务器时间:date 
      设置时间:date -s "2016-01-0417:07:30" (可以单独设置日期或者时间)
      NTP 同步(需要联网) 命令:ntpdate asia.pool.ntp.org (定时同步请看时钟同步部分)
    
  • 8,安装依赖服务 yum -y install postgresql-server postgresql httpd perl bind-utils libxslt cyrus-sasl-gssapiredhat-lsb cyrus-sasl-plain portmap fuse fuse-libs nc python-setuptools openssl_devel python_psycopg2 mod_ssl MySQL_python

CM安装:

  • 1,配置本地yum源 在/etc/yum.repos.d/目录下创建文件cloudera-manager.repo 文件内容为:
      [cloudera-manager] 
      name=ClouderaManager,Version5.10.0 
      baseurl=http://自己机器ip/cm (都指定已经上传安装包的那一台机器即可) 
      gpgcheck=0
    
  • 2,验证本地yum源 将上一步baseurl地址粘贴到浏览器确认是否能正常访问到已经上传安装文件的机器的安装文件目录(注意开启httpd服务)
  • 3,开始安装
      赋予bin文件执行权限 命令:chmod u+x cloudera-manager-installer5.10.0.bin
      执行bin文件,进入/var/www/html/目录,执行如下命令 ./cloudera-manager-installer5.10.0.bin 
      .......一步步安装就好.安装完成进入ip:7180管理界面,用户名和密码都为admin
    注意:请稍等几分钟在尝试进入7180界面,如果还是无法进入,请检查防火墙是否处于关闭状态.在访问时建议使用非IE浏览器,比如chrome,火狐等浏览器.
    

    CDH安装:

    按照网页提示一步一步走就OK了.

Hive自定义UDAF详解(转)

转自:http://paddy-w.iteye.com/blog/2081409
遇到一个Hive需求:有A、B、C三列,按A列进行聚合,求出C列聚合后的最小值和最大值各自对应的B列值。这个需求用hql和内建函数也可完成,但是比较繁琐,会解析成几个MR进行执行,如果自定义UDAF便可只利用一个MR完成任务。 所用Hive为0.13.1版本。UDAF有两种,第一种是比较简单的形式,利用抽象类UDAF和UDAFEvaluator,暂不做讨论。主要说一下第二种形式,利用接口GenericUDAFResolver2(或者抽象类AbstractGenericUDAFResolver)和抽象类GenericUDAFEvaluator。

这里用AbstractGenericUDAFResolver做说明.
public abstract class AbstractGenericUDAFResolver implements GenericUDAFResolver2 {  
  
  @SuppressWarnings("deprecation")  
  @Override  
  public GenericUDAFEvaluator getEvaluator(GenericUDAFParameterInfo info)  
    throws SemanticException {  
  
    if (info.isAllColumns()) {  
      throw new SemanticException(  
          "The specified syntax for UDAF invocation is invalid.");  
    }  
  
    return getEvaluator(info.getParameters());  
  }  
  
  @Override  
  public GenericUDAFEvaluator getEvaluator(TypeInfo[] info)   
    throws SemanticException {  
    throw new SemanticException(  
          "This UDAF does not support the deprecated getEvaluator() method.");  
  }  
}  

可以看到,该抽象类有两个方法,其中一个已经被弃用,所以只需要实现参数类型为TypeInfo的getEvaluator方法即可。 该方法其实相当于一个工厂,TypeInfo表示在使用时传入该UDAF的参数的类型。该方法主要做的工作有:

  • 检查参数长度和类型
  • 根据参数返回对应的实际处理对象
返回的对象类型为GenericUDAFEvaluator,这是一个抽象类:
public abstract class GenericUDAFEvaluator implements Closeable {  
  
    ......  
  
    public ObjectInspector init(Mode m, ObjectInspector[] parameters) throws HiveException {  
        // This function should be overriden in every sub class  
        // And the sub class should call super.init(m, parameters) to get mode set.  
        mode = m;  
        return null;  
    }  
  
    public abstract AggregationBuffer getNewAggregationBuffer() throws HiveException;  
  
    public abstract void reset(AggregationBuffer agg) throws HiveException;  
  
    public abstract void iterate(AggregationBuffer agg, Object[] parameters) throws HiveException;  
  
    public abstract Object terminatePartial(AggregationBuffer agg) throws HiveException;  
  
    public abstract void merge(AggregationBuffer agg, Object partial) throws HiveException;  
  
    public abstract Object terminate(AggregationBuffer agg) throws HiveException;  
    ......  
}  

说明上述方法的之前,需要提一个GenericUDAFEvaluator的内部枚举类Mode

public static enum Mode {  
    /** 
     * 相当于map阶段,调用iterate()和terminatePartial() 
     */  
    PARTIAL1,  
    /** 
     * 相当于combiner阶段,调用merge()和terminatePartial() 
     */  
    PARTIAL2,  
    /** 
     * 相当于reduce阶段调用merge()和terminate() 
     */  
    FINAL,  
    /** 
     * COMPLETE: 相当于没有reduce阶段map,调用iterate()和terminate() 
     */  
    COMPLETE  
  };  

可以看到,UDAF将任务分成了几种类型,PARTIAL1相当于MR程序的map阶段,负责迭代处理记录并返回该阶段的中间结果。PARTIAL2相当于Combiner,对map阶段的结果进行一次聚合。FINAL是reduce阶段,进行整体聚合以及返回最终结果。COMPLETE有点特殊,是一个没有reduce阶段的map过程,所以在进行记录迭代之后,直接返回最终结果。

再来看GenericUDAFEvaluator中的各方法
public ObjectInspector init(Mode m, ObjectInspector[] parameters) throws HiveException {...} 

初始化方法,在Mode的每一个阶段启动时会执行init方法。该方法有两个参数,第一个参数是Mode,可以根据此参数判断当前执行的是哪个阶段,进行该阶段相应的初始化工作。ObjectInspector是一个抽象的类型描述,例如:当参数类型是原生类型时,可以转化为PrimitiveObjectInspector,除此之外还有StructObjectInspector等等。ObjectInspector只是描述类型,并不存储实际数据。后面的具体例子中会有一些使用说明。
ObjectInspector[]的长度不是固定的,要看当前是处于哪个阶段。如果是PARTIAL1,那么与使用时传入该UDAF的参数个数一致;如果是FINAL阶段,长度就是1了,因为map阶段返回的结果只有一个对象。

public abstract AggregationBuffer getNewAggregationBuffer() throws HiveException;  
  
public abstract void reset(AggregationBuffer agg) throws HiveException;

AggregationBuffer是一个标识接口,没有任何需要实现的方法。实现该接口的类被用于暂存中间结果。reset是为了重置AggregationBuffer,但是在实际应用场景中没有发现单独调用该方法进行重置,有可能是聚合key的数据量还不够大,在后面会再说一下这个问题。

public abstract void iterate(AggregationBuffer agg, Object[] parameters) throws HiveException;  
  
public abstract Object terminatePartial(AggregationBuffer agg) throws HiveException;  
  
public abstract void merge(AggregationBuffer agg, Object partial) throws HiveException;  
  
public abstract Object terminate(AggregationBuffer agg) throws HiveException;  
......  

iterate方法存在于MR的M阶段,用于处理每一条输入记录。Object[]作为输入传入UFAF,AggregationBuffer作为中间缓存暂存结果。需要注意的是,每次调用iterate传入的AggregationBuffer并不一定是同一个对象。Hive调用UDAF的时候会用一个Map来管理AggregationBuffer,Map的key即为需要聚合的key。就通过实际运行过程来看,在每一次iterate调用之前,会根据聚合key从Map中查找对应的AggregationBuffer,若能找到则直接返回AggregationBuffer对象,找不到则调用getNewAggregationBuffer方法新建并插入Map中并返回结果。
terminatePartial方法在iterate处理完所有输入后调用,用于返回初步的聚合结果。
merge方法存在于MR的R阶段(也同样存在于Combine阶段),用于最后的聚合。Object类型的partial参数与terminatePartial返回值一致,AggregationBuffer参数与上述一致。
terminate方法在merge方法执行完毕之后调用,用于进行最后的处理,并返回最后结果。
像上面提到的Mode一样,这些方法并不一定都会被调用,与Hive解析成的MR程序类型有关。例如解析后的MR程序只有M阶段,则只会调用iterate和terminate。实际使用过程中,由于聚合key数据量有限,内存可以承载,所以没有发现reset单独调用的情况。每次遇到一个不同的key,则新建一个AggregationBuffer,没有看源码,不知道当聚合key很大的时候,是否会调用reset进行对象重用。

Centos中的wget使用

wget

  • -c 断点续传
  • -r 递归下载,下载指定网页某一目录下(包括子目录)的所有文件
  • -nd 递归下载时不创建一层一层的目录,把所有的文件下载到当前目录
  • -np 递归下载时不搜索上层目录,如wget -c -r www.xxx.org/pub/path/ (没有加参数-np,就会同时下载path的上一级目录pub下的其它文件)
  • -k 将绝对链接转为相对链接,下载整个站点后脱机浏览网页,最好加上这个参数
  • -L 递归时不进入其它主机,如wget -c -r www.xxx.org/ (如果网站内有一个这样的链接: www.yyy.org,不加参数-L,就会像大火烧山一样,会递归下载www.yyy.org网站)
  • -p 下载网页所需的所有文件,如图片等
  • -A 指定要下载的文件样式列表,多个样式用逗号分隔
  • -i 后面跟一个文件,文件内指明要下载的URL

wget的常见用法

Usage: wget [OPTION]… [URL]…

用wget做站点镜像:
  • wget -r -p -np -k http://dsec.pku.edu.cn/~usr_name/
  • wget -m http://www.tldp.org/LDP/abs/html/
    在不稳定的网络上下载一个部分下载的文件,以及在空闲时段下载
  • wget -t 0 -w 31 -c http://dsec.pku.edu.cn/BBC.avi -o down.log &
  • 或者从filelist读入要下载的文件列表
  • wget -t 0 -w 31 -c -B ftp://dsec.pku.edu.cn/linuxsoft -i filelist.txt -o down.log & (上面的代码还可以用来在网络比较空闲的时段进行下载。我的用法是:在mozilla中将不方便当时下载的URL链接拷贝到内存中然后粘贴到文件filelist.txt中,在晚上要出去系统前执行上面代码的第二条)
使用代理下载
  • wget -Y on -p -k https://sourceforge.net/projects/wvware/ (代理可以在环境变量或wgetrc文件中设定,在环境变量中设定代理 :export PROXY=http://211.90.168.94:8080/ ; 在~/.wgetrc中设定代理 : http_proxy = http://proxy.yoyodyne.com:18023/ ftp_proxy = http://proxy.yoyodyne.com:18023/ )

wget各种选项分类列表

启动
  • -V, –version 显示wget的版本后退出
  • -h, –help 打印语法帮助
  • -b, –background 启动后转入后台执行
  • -e, –execute=COMMAND
  • 执行.wgetrc格式的命令,wgetrc格式参见/etc/wgetrc或~/.wgetrc
    记录和输入文件
  • -o, –output-file=FILE 把记录写到FILE文件中
  • -a, –append-output=FILE 把记录追加到FILE文件中
  • -d, –debug 打印调试输出
  • -q, –quiet 安静模式(没有输出)
  • -v, –verbose 冗长模式(这是缺省设置)
  • -nv, –non-verbose 关掉冗长模式,但不是安静模式
  • -i, –input-file=FILE 下载在FILE文件中出现的URLs
  • -F, –force-html 把输入文件当作HTML格式文件对待
  • -B, –base=URL 将URL作为在-F -i参数指定的文件中出现的相对链接的前缀
  • –sslcertfile=FILE 可选客户端证书
  • –sslcertkey=KEYFILE 可选客户端证书的KEYFILE
  • –egd-file=FILE 指定EGD socket的文件名
    下载
  • –bind-address=ADDRESS 指定本地使用地址(主机名或IP,当本地有多个IP或名字时使用)
  • -t, –tries=NUMBER 设定最大尝试链接次数(0 表示无限制).
  • -O –output-document=FILE 把文档写到FILE文件中
  • -nc, –no-clobber 不要覆盖存在的文件或使用.#前缀
  • -c, –continue 接着下载没下载完的文件
  • –progress=TYPE 设定进程条标记
  • -N, –timestamping 不要重新下载文件除非比本地文件新
  • -S, –server-response 打印服务器的回应
  • –spider 不下载任何东西
  • -T, –timeout=SECONDS 设定响应超时的秒数
  • -w, –wait=SECONDS 两次尝试之间间隔SECONDS秒
  • –waitretry=SECONDS 在重新链接之间等待1…SECONDS秒
  • –random-wait 在下载之间等待0…2*WAIT秒
  • -Y, –proxy=on/off 打开或关闭代理
  • -Q, –quota=NUMBER 设置下载的容量限制
  • –limit-rate=RATE 限定下载输率
    目录
  • -nd –no-directories 不创建目录
  • -x, –force-directories 强制创建目录
  • -nH, –no-host-directories 不创建主机目录
  • -P, –directory-prefix=PREFIX 将文件保存到目录 PREFIX/…
  • –cut-dirs=NUMBER 忽略 NUMBER层远程目录
    HTTP 选项
  • –http-user=USER 设定HTTP用户名为 USER.
  • –http-passwd=PASS 设定http密码为 PASS.
  • -C, –cache=on/off 允许/不允许服务器端的数据缓存 (一般情况下允许).
  • -E, –html-extension 将所有text/html文档以.html扩展名保存
  • –ignore-length 忽略 Content-Length 头域
  • –header=STRING 在headers中插入字符串 STRING
  • –proxy-user=USER 设定代理的用户名为 USER
  • –proxy-passwd=PASS 设定代理的密码为 PASS
  • –referer=URL 在HTTP请求中包含 Referer: URL头
  • -s, –save-headers 保存HTTP头到文件
  • -U, –user-agent=AGENT 设定代理的名称为 AGENT而不是 Wget/VERSION.
  • –no-http-keep-alive 关闭 HTTP活动链接 (永远链接).
  • –cookies=off 不使用 cookies.
  • –load-cookies=FILE 在开始会话前从文件 FILE中加载cookie
  • –save-cookies=FILE 在会话结束后将 cookies保存到 FILE文件中
    FTP 选项
  • -nr, –dont-remove-listing 不移走 .listing文件
  • -g, –glob=on/off 打开或关闭文件名的 globbing机制
  • –passive-ftp 使用被动传输模式 (缺省值).
  • –active-ftp 使用主动传输模式
  • –retr-symlinks 在递归的时候,将链接指向文件(而不是目录)
    递归下载
  • -r, –recursive 递归下载--慎用!
  • -l, –level=NUMBER 最大递归深度 (inf 或 0 代表无穷).
  • –delete-after 在现在完毕后局部删除文件
  • -k, –convert-links 转换非相对链接为相对链接
  • -K, –backup-converted 在转换文件X之前,将之备份为 X.orig
  • -m, –mirror 等价于 -r -N -l inf -nr.
  • -p, –page-requisites 下载显示HTML文件的所有图片
    递归下载中的包含和不包含(accept/reject)
  • -A, –accept=LIST 分号分隔的被接受扩展名的列表
  • -R, –reject=LIST 分号分隔的不被接受的扩展名的列表
  • -D, –domains=LIST 分号分隔的被接受域的列表
  • –exclude-domains=LIST 分号分隔的不被接受的域的列表
  • –follow-ftp 跟踪HTML文档中的FTP链接
  • –follow-tags=LIST 分号分隔的被跟踪的HTML标签的列表
  • -G, –ignore-tags=LIST 分号分隔的被忽略的HTML标签的列表
  • -H, –span-hosts 当递归时转到外部主机
  • -L, –relative 仅仅跟踪相对链接
  • -I, –include-directories=LIST 允许目录的列表
  • -X, –exclude-directories=LIST 不被包含目录的列表
  • -np, –no-parent 不要追溯到父目录

使用log4j向Kafka直接打数据异常

使用log4j向Kafka直接打数据异常

Exception in thread "main" java.lang.NoSuchMethodError: org.apache.log4j.spi.LoggingEvent.getTimeStamp()J
	at kafka.producer.KafkaLog4jAppender.append(KafkaLog4jAppender.scala:72)
	at org.apache.log4j.AppenderSkeleton.doAppend(AppenderSkeleton.java:230)
	at org.apache.log4j.helpers.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.java:65)
	at org.apache.log4j.Category.callAppenders(Category.java:203)
	at org.apache.log4j.Category.forcedLog(Category.java:388)
	at org.apache.log4j.Category.info(Category.java:663)
问题来源

log4j版本太低

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.12</version>
</dependency>
解决方法

升级log4j版本

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.16</version>
</dependency>

Kafka配置文件详解

Kafka配置文件详解

broker的全局配置

最为核心的三个配置 broker.id、log.dir、zookeeper.connect

------------------------------------------- 系统 相关 -------------------------------------------
\##每一个broker在集群中的唯一标示,要求是正数。在改变IP地址,不改变broker.id的话不会影响consumers
broker.id = 1
 
\##kafka数据的存放地址,多个地址的话用逗号分割 /tmp/kafka-logs-1,/tmp/kafka-logs-2
log.dirs = /tmp/kafka-logs
 
\##提供给客户端响应的端口
port = 6667
 
\##消息体的最大大小,单位是字节
message.max.bytes = 1000000
 
\## broker 处理消息的最大线程数,一般情况下不需要去修改
num.network.threads = 3
 
\## broker处理磁盘IO 的线程数 ,数值应该大于你的硬盘数
num.io.threads = 8
 
\## 一些后台任务处理的线程数,例如过期消息文件的删除等,一般情况下不需要去做修改
background.threads = 4
 
\## 等待IO线程处理的请求队列最大数,若是等待IO的请求超过这个数值,那么会停止接受外部消息,算是一种自我保护机制
queued.max.requests = 500
 
\##broker的主机地址,若是设置了,那么会绑定到这个地址上,若是没有,会绑定到所有的接口上,并将其中之一发送到ZK,一般不设置
host.name
 
\## 打广告的地址,若是设置的话,会提供给producers, consumers,其他broker连接,具体如何使用还未深究
advertised.host.name
 
\## 广告地址端口,必须不同于port中的设置
advertised.port
 
\## socket的发送缓冲区,socket的调优参数SO_SNDBUFF
socket.send.buffer.bytes = 100 * 1024
 
\## socket的接受缓冲区,socket的调优参数SO_RCVBUFF
socket.receive.buffer.bytes = 100 * 1024
 
\## socket请求的最大数值,防止serverOOM,message.max.bytes必然要小于socket.request.max.bytes,会被topic创建时的指定参数覆盖
socket.request.max.bytes = 100 * 1024 * 1024
 
------------------------------------------- LOG 相关 -------------------------------------------
\## topic的分区是以一堆segment文件存储的,这个控制每个segment的大小,会被topic创建时的指定参数覆盖
log.segment.bytes = 1024 * 1024 * 1024
 
\## 这个参数会在日志segment没有达到log.segment.bytes设置的大小,也会强制新建一个segment 会被 topic创建时的指定参数覆盖
log.roll.hours = 24*7
 
\## 日志清理策略 选择有:delete和compact 主要针对过期数据的处理,或是日志文件达到限制的额度,会被 topic创建时的指定参数覆盖
log.cleanup.policy = delete
 
\## 数据存储的最大时间 超过这个时间 会根据log.cleanup.policy设置的策略处理数据,也就是消费端能够多久去消费数据
\## log.retention.bytes和log.retention.minutes任意一个达到要求,都会执行删除,会被topic创建时的指定参数覆盖
log.retention.minutes=7 days
 
\## topic每个分区的最大文件大小,一个topic的大小限制 = 分区数*log.retention.bytes 。-1 没有大小限制
\## log.retention.bytes和log.retention.minutes任意一个达到要求,都会执行删除,会被topic创建时的指定参数覆盖
log.retention.bytes=-1
 
\## 文件大小检查的周期时间,是否处罚 log.cleanup.policy中设置的策略
log.retention.check.interval.ms=5 minutes
 
\## 是否开启日志压缩
log.cleaner.enable=false
 
\## 日志压缩运行的线程数
log.cleaner.threads =1
 
\## 日志压缩时候处理的最大大小
log.cleaner.io.max.bytes.per.second=None
 
\## 日志压缩去重时候的缓存空间 ,在空间允许的情况下,越大越好
log.cleaner.dedupe.buffer.size=500*1024*1024
 
\## 日志清理时候用到的IO块大小 一般不需要修改
log.cleaner.io.buffer.size=512*1024
 
\## 日志清理中hash表的扩大因子 一般不需要修改
log.cleaner.io.buffer.load.factor = 0.9
 
\## 检查是否处罚日志清理的间隔
log.cleaner.backoff.ms =15000
 
\## 日志清理的频率控制,越大意味着更高效的清理,同时会存在一些空间上的浪费,会被topic创建时的指定参数覆盖
log.cleaner.min.cleanable.ratio=0.5
 
\## 对于压缩的日志保留的最长时间,也是客户端消费消息的最长时间,同log.retention.minutes的区别在于一个控制未压缩数据,一个控制压缩后的数据。会被topic创建时的指定参数覆盖
log.cleaner.delete.retention.ms = 1 day
 
\## 对于segment日志的索引文件大小限制,会被topic创建时的指定参数覆盖
log.index.size.max.bytes = 10 * 1024 * 1024
 
\## 当执行一个fetch操作后,需要一定的空间来扫描最近的offset大小,设置越大,代表扫描速度越快,但是也更好内存,一般情况下不需要搭理这个参数
log.index.interval.bytes = 4096
 
\## log文件"sync"到磁盘之前累积的消息条数
\## 因为磁盘IO操作是一个慢操作,但又是一个"数据可靠性"的必要手段
\## 所以此参数的设置,需要在"数据可靠性"与"性能"之间做必要的权衡.
\## 如果此值过大,将会导致每次"fsync"的时间较长(IO阻塞)
\## 如果此值过小,将会导致"fsync"的次数较多,这也意味着整体的client请求有一定的延迟.
\## 物理server故障,将会导致没有fsync的消息丢失.
log.flush.interval.messages=None
 
\## 检查是否需要固化到硬盘的时间间隔
log.flush.scheduler.interval.ms = 3000
 
\## 仅仅通过interval来控制消息的磁盘写入时机,是不足的.
\## 此参数用于控制"fsync"的时间间隔,如果消息量始终没有达到阀值,但是离上一次磁盘同步的时间间隔
\## 达到阀值,也将触发.
log.flush.interval.ms = None
 
\## 文件在索引中清除后保留的时间 一般不需要去修改
log.delete.delay.ms = 60000
 
\## 控制上次固化硬盘的时间点,以便于数据恢复 一般不需要去修改
log.flush.offset.checkpoint.interval.ms =60000
 
------------------------------------------- TOPIC 相关 -------------------------------------------
\## 是否允许自动创建topic ,若是false,就需要通过命令创建topic
auto.create.topics.enable =true
 
\## 一个topic ,默认分区的replication个数 ,不得大于集群中broker的个数
default.replication.factor =1
 
\## 每个topic的分区个数,若是在topic创建时候没有指定的话 会被topic创建时的指定参数覆盖
num.partitions = 1
 
实例 --replication-factor 3 --partitions 1 --topic replicated-topic :名称replicated-topic有一个分区,分区被复制到三个broker上。
 
------------------------------------------- 复制(Leader、replicas) 相关 -------------------------------------------
\## partition leader与replicas之间通讯时,socket的超时时间
controller.socket.timeout.ms = 30000
 
\## partition leader与replicas数据同步时,消息的队列尺寸
controller.message.queue.size=10
 
\## replicas响应partition leader的最长等待时间,若是超过这个时间,就将replicas列入ISR(in-sync replicas),并认为它是死的,不会再加入管理中
replica.lag.time.max.ms = 10000
 
\## 如果follower落后与leader太多,将会认为此follower[或者说partition relicas]已经失效
\## 通常,在follower与leader通讯时,因为网络延迟或者链接断开,总会导致replicas中消息同步滞后
\## 如果消息之后太多,leader将认为此follower网络延迟较大或者消息吞吐能力有限,将会把此replicas迁移
\## 到其他follower中.
\## 在broker数量较少,或者网络不足的环境中,建议提高此值.
replica.lag.max.messages = 4000
 
\##follower与leader之间的socket超时时间
replica.socket.timeout.ms= 30 * 1000
 
\## leader复制时候的socket缓存大小
replica.socket.receive.buffer.bytes=64 * 1024
 
\## replicas每次获取数据的最大大小
replica.fetch.max.bytes = 1024 * 1024
 
\## replicas同leader之间通信的最大等待时间,失败了会重试
replica.fetch.wait.max.ms = 500
 
\## fetch的最小数据尺寸,如果leader中尚未同步的数据不足此值,将会阻塞,直到满足条件
replica.fetch.min.bytes =1
 
\## leader 进行复制的线程数,增大这个数值会增加follower的IO
num.replica.fetchers=1
 
\## 每个replica检查是否将最高水位进行固化的频率
replica.high.watermark.checkpoint.interval.ms = 5000
 
\## 是否允许控制器关闭broker ,若是设置为true,会关闭所有在这个broker上的leader,并转移到其他broker
controlled.shutdown.enable = false
 
\## 控制器关闭的尝试次数
controlled.shutdown.max.retries = 3
 
\## 每次关闭尝试的时间间隔
controlled.shutdown.retry.backoff.ms = 5000
 
\## 是否自动平衡broker之间的分配策略
auto.leader.rebalance.enable = false
 
\## leader的不平衡比例,若是超过这个数值,会对分区进行重新的平衡
leader.imbalance.per.broker.percentage = 10
 
\## 检查leader是否不平衡的时间间隔
leader.imbalance.check.interval.seconds = 300
 
\## 客户端保留offset信息的最大空间大小
offset.metadata.max.bytes
 
------------------------------------------- ZooKeeper 相关 -------------------------------------------
\##zookeeper集群的地址,可以是多个,多个之间用逗号分割 hostname1:port1,hostname2:port2,hostname3:port3
zookeeper.connect = localhost:2181
 
\## ZooKeeper的最大超时时间,就是心跳的间隔,若是没有反映,那么认为已经死了,不易过大
zookeeper.session.timeout.ms=6000
 
\## ZooKeeper的连接超时时间
zookeeper.connection.timeout.ms = 6000
 
\## ZooKeeper集群中leader和follower之间的同步实际那
zookeeper.sync.time.ms = 2000

配置的修改
其中一部分配置是可以被每个topic自身的配置所代替,例如
新增配置
bin/kafka-topics.sh --zookeeper localhost:2181 --create --topic my-topic --partitions 1 --replication-factor 1 --config max.message.bytes=64000 --config flush.messages=1
 
修改配置
bin/kafka-topics.sh --zookeeper localhost:2181 --alter --topic my-topic --config max.message.bytes=128000
 
删除配置 :
bin/kafka-topics.sh --zookeeper localhost:2181 --alter --topic my-topic --deleteConfig max.message.bytes
Consumer配置

最为核心的配置是group.id、zookeeper.connect

\## Consumer归属的组ID,broker是根据group.id来判断是队列模式还是发布订阅模式,非常重要
 group.id
 
\## 消费者的ID,若是没有设置的话,会自增
 consumer.id
 
\## 一个用于跟踪调查的ID ,最好同group.id相同
 client.id = group id value
 
\## 对于zookeeper集群的指定,可以是多个 hostname1:port1,hostname2:port2,hostname3:port3 必须和broker使用同样的zk配置
 zookeeper.connect=localhost:2182
 
\## zookeeper的心跳超时时间,查过这个时间就认为是dead消费者
 zookeeper.session.timeout.ms = 6000
 
\## zookeeper的等待连接时间
 zookeeper.connection.timeout.ms = 6000
 
\## zookeeper的follower同leader的同步时间
 zookeeper.sync.time.ms = 2000
 
\## 当zookeeper中没有初始的offset时候的处理方式 。smallest :重置为最小值 largest:重置为最大值 anything else:抛出异常
 auto.offset.reset = largest
 
\## socket的超时时间,实际的超时时间是:max.fetch.wait + socket.timeout.ms.
 socket.timeout.ms= 30 * 1000
 
\## socket的接受缓存空间大小
 socket.receive.buffer.bytes=64 * 1024
 
\##从每个分区获取的消息大小限制
 fetch.message.max.bytes = 1024 * 1024
 
\## 是否在消费消息后将offset同步到zookeeper,当Consumer失败后就能从zookeeper获取最新的offset
 auto.commit.enable = true
 
\## 自动提交的时间间隔
 auto.commit.interval.ms = 60 * 1000
 
\## 用来处理消费消息的块,每个块可以等同于fetch.message.max.bytes中数值
 queued.max.message.chunks = 10
 
\## 当有新的consumer加入到group时,将会reblance,此后将会有partitions的消费端迁移到新
\## 的consumer上,如果一个consumer获得了某个partition的消费权限,那么它将会向zk注册
\## "Partition Owner registry"节点信息,但是有可能此时旧的consumer尚没有释放此节点,
\## 此值用于控制,注册节点的重试次数.
 rebalance.max.retries = 4
 
\## 每次再平衡的时间间隔
 rebalance.backoff.ms = 2000
 
\## 每次重新选举leader的时间
 refresh.leader.backoff.ms
 
\## server发送到消费端的最小数据,若是不满足这个数值则会等待,知道满足数值要求
 fetch.min.bytes = 1
 
\## 若是不满足最小大小(fetch.min.bytes)的话,等待消费端请求的最长等待时间
 fetch.wait.max.ms = 100
 
\## 指定时间内没有消息到达就抛出异常,一般不需要改
 consumer.timeout.ms = -1
Producer的配置

比较核心的配置:metadata.broker.list、request.required.acks、producer.type、serializer.class

\## 消费者获取消息元信息(topics, partitions and replicas)的地址,配置格式是:host1:port1,host2:port2,也可以在外面设置一个vip
 metadata.broker.list
 
\##消息的确认模式
\ ## 0:不保证消息的到达确认,只管发送,低延迟但是会出现消息的丢失,在某个server失败的情况下,有点像TCP
\ ## 1:发送消息,并会等待leader 收到确认后,一定的可靠性
\ ## -1:发送消息,等待leader收到确认,并进行复制操作后,才返回,最高的可靠性
 request.required.acks = 0
 
\## 消息发送的最长等待时间
 request.timeout.ms = 10000
 
\## socket的缓存大小
 send.buffer.bytes=100*1024
 
\## key的序列化方式,若是没有设置,同serializer.class
 key.serializer.class
 
\## 分区的策略,默认是取模
 partitioner.class=kafka.producer.DefaultPartitioner
 
\## 消息的压缩模式,默认是none,可以有gzip和snappy
 compression.codec = none
 
\## 可以针对默写特定的topic进行压缩
 compressed.topics=null
 
\## 消息发送失败后的重试次数
 message.send.max.retries = 3
 
\## 每次失败后的间隔时间
 retry.backoff.ms = 100
 
\## 生产者定时更新topic元信息的时间间隔 ,若是设置为0,那么会在每个消息发送后都去更新数据
 topic.metadata.refresh.interval.ms = 600 * 1000
 
\## 用户随意指定,但是不能重复,主要用于跟踪记录消息
 client.id=""
 
------------------------------------------- 消息模式 相关 -------------------------------------------
 \## 生产者的类型 async:异步执行消息的发送 sync:同步执行消息的发送
 producer.type=sync
 
\## 异步模式下,那么就会在设置的时间缓存消息,并一次性发送
 queue.buffering.max.ms = 5000
 
\## 异步的模式下 最长等待的消息数
 queue.buffering.max.messages = 10000
 
\## 异步模式下,进入队列的等待时间 若是设置为0,那么要么进入队列,要么直接抛弃
 queue.enqueue.timeout.ms = -1
 
\## 异步模式下,每次发送的最大消息数,前提是触发了queue.buffering.max.messages或是queue.buffering.max.ms的限制
 batch.num.messages=200
 
\## 消息体的系列化处理类 ,转化为字节流进行传输
 serializer.class = kafka.serializer.DefaultEncoder

MapReduce的shuffle阶段问题解惑

Shuffle产生的意义是什么?

Shuffle过程的期望可以有: 完整地从map task端拉取数据到reduce 端. 在跨节点拉取数据时,尽可能地减少对带宽的不必要消耗. 减少磁盘IO对task执行的影响.

每个map task都有一个内存缓冲区,存储着map的输出结果,当缓冲区快满的时候需要将缓冲区的数据该如何处理?

当缓冲区快满的时候需要将缓冲区的数据以一个临时文件的方式存放到磁盘,当整个map task结束后再对磁盘中这个map task产生的所有临时文件做合并,生成最终的正式输出文件,然后等待reduce task来拉数据.

MapReduce提供Partitioner接口,它的作用是什么?

Partitioner它的作用就是根据key或value及reduce的数量来决定当前的这对输出数据最终应该交由哪个reduce task处理.默认对key hash后再以reduce task数量取模.默认的取模方式只是为了平均reduce的处理能力,如果用户自己对Partitioner有需求,可以订制并设置到job上.

什么是溢写?

在一定条件下将缓冲区中的数据临时写入磁盘,然后重新利用这块缓冲区/这个从内存往磁盘写数据的过程被称为Spill,中文可译为溢写.

溢写是为什么不影响往缓冲区写map结果的线程?

溢写线程启动时不应该阻止map的结果输出,所以整个缓冲区有个溢写的比例spill.percent.这个比例默认是0.8,也就是当缓冲区的数据已经达到阈值(buffer size * spill percent = 100MB * 0.8 = 80MB),溢写线程启动,锁定这80MB的内存,执行溢写过程.Map task的输出结果还可以往剩下的20MB内存中写,互不影响.

当溢写线程启动后,需要对这80MB空间内的key做排序(Sort).排序是MapReduce模型默认的行为,这里的排序也是对谁的排序?

需要对这80MB空间内的key做排序(Sort).排序是MapReduce模型默认的行为,这里的排序也是对序列化的字节做的排序.

溢写过程中如果有很多个key/value对需要发送到某个reduce端去,那么如何处理这些key/value值?

如果有很多个key/value对需要发送到某个reduce端去,那么需要将这些key/value值拼接到一块,减少与partition相关的索引记录.

哪些场景才能使用Combiner呢?

Combiner的输出是Reducer的输入,Combiner绝不能改变最终的计算结果.所以Combiner只应该用于那种Reduce的输入key/value与输出key/value类型完全一致,且不影响最终结果的场景.比如累加,最大值等.Combiner的使用一定得慎重,如果用好,它对job执行效率有帮助,反之会影响reduce的最终结果.

Merge的作用是什么?

最终磁盘中会至少有一个这样的溢写文件存在(如果map的输出结果很少,当map执行完成时,只会产生一个溢写文件),因为最终的文件只有一个,所以需要将这些溢写文件归并到一起,这个过程就叫做Merge.

每个reduce task不断的通过什么协议从JobTracker那里获取map task是否完成的信息?

RPC协议

reduce中Copy过程采用是什么协议?

HTTP协议

reduce中merge过程有几种方式?

merge有三种形式:1)内存到内存 2)内存到磁盘 3)磁盘到磁盘.

MapReduce的Shuffle

MapReduce的Shuffle过程

shuffle

这张是官方对Shuffle过程的描述.Shuffle描述着数据从map task输出到reduce task输入的这段过程. 在Hadoop集群环境中,大部分map task与reduce task的执行是在不同的节点上.很多情况下Reduce执行时需要跨节点去拉取其它节点上的map task结果.如果集群正在运行的job有很多,那么task的正常执行对集群内部的网络资源消耗会很严重.这种网络消耗是正常的,我们不能限制,能做的就是最大化地减少不必要的消耗.还有在节点内,相比于内存,磁盘IO对job完成时间的影响也是可观的.从最基本的要求来说,我们对Shuffle过程的期望可以有:

  • 完整地从map task端拉取数据到reduce 端
  • 在跨节点拉取数据时,尽可能地减少对带宽的不必要消耗
  • 减少磁盘IO对task执行的影响
以WordCount为例,并假设它有8个map task和3个reduce task

map端

map_shuffle

   每个map task都有一个环形缓冲区. map_shuffle 环形缓冲其实就是一个字节数组.存储着map的输出结果,当缓冲区快满的时候(80%)需要将缓冲区的数据以一个临时文件的方式存放到磁盘,当整个map task结束后再对磁盘中这个map task产生的所有临时文件做合并,生成最终的正式输出文件,然后等待reduce task来拉数据.

 // MapTask.java
private byte[] kvbuffer;  // main output buffer
kvbuffer = new byte[maxMemUsage - recordCapacity];

kvbuffer包含数据区和索引区,这两个区是相邻不重叠的区域,用一个分界点来标识.分界点不是永恒不变的,每次Spill之后都会更新一次.初始分界点为0,数据存储方向为向上增长,索引存储方向向下: buffer_index bufferindex一直往上增长,例如最初为0,写入一个int类型的key之后变为4,写入一个int类型的value之后变成8.kvmeta的存放指针kvindex每次都是向下跳四个”格子”.索引是对key-value在kvbuffer中的索引,是个四元组,占用四个Int长度,包括:value的起始位置,key的起始位置,partition值,value的长度.

Shuffle整个流程分了四步

  1. 在map task执行时,它的输入数据来源于HDFS的block,当然在MapReduce概念中,map task只读取split.Split与block的对应关系可能是多对一,默认是一对一.在WordCount例子里,假设map的输入数据都是像”aaa”这样的字符串.
  2. 在经过mapper的运行后,我们得知mapper的输出是这样一个key/value对:key是”aaa”,value是数值1.因为当前map端只做加1的操作,在reduce task里才去合并结果集.前面我们知道这个job有3个reduce task,到底当前的“aaa”应该交由哪个reduce去做呢,是需要现在决定的.

    MapReduce提供Partitioner接口,它的作用就是根据key或value及reduce的数量来决定当前的这对输出数据最终应该交由哪个reduce task处理.默认对key hash后再以reduce task数量取模.默认的取模方式只是为了平均reduce的处理能力,如果用户自己对Partitioner有需求,可以订制并设置到job上.在我们的例子中,”aaa”经过Partitioner后返回0,也就是这对值应当交由第一个reducer来处理.

    接下来,需要将数据写入内存缓冲区中,缓冲区的作用是批量收集map结果,减少磁盘IO的影响.我们的key/value对以及Partition的结果都会被写入缓冲区.当然写入之前,key与value值都会被序列化成字节数组.整个内存缓冲区就是一个字节数组.

  3. 这个内存缓冲区是有大小限制的,默认是100MB(通过mapreduce.task.io.sort.mb设置).当map task的输出结果很多时,就可能会撑爆内存,所以需要在一定条件下将缓冲区中的数据临时写入磁盘,然后重新利用这块缓冲区.这个从内存往磁盘写数据的过程被称为Spill,中文可译为溢写,字面意思很直观.这个溢写是由单独线程来完成,不影响往缓冲区写map结果的线程.溢写线程启动时不应该阻止map的结果输出,所以整个缓冲区有个溢写的比例spill.percent.这个比例默认是0.8,也就是当缓冲区的数据已经达到阈值(buffer size * spill percent = 100MB * 0.8 = 80MB),溢写线程启动,锁定这80MB的内存,执行溢写过程.Map task的输出结果还可以往剩下的20MB内存中写,互不影响.当溢写线程启动后,需要对这80MB空间内的key做排序(Sort).排序是MapReduce模型默认的行为,这里的排序也是对序列化的字节做的排序.

    在这里我们可以想想,因为map task的输出是需要发送到不同的reduce端去,而内存缓冲区没有对将发送到相同reduce端的数据做合并,那么这种合并应该是体现是磁盘文件中的.从官方图上也可以看到写到磁盘中的溢写文件是对不同的reduce端的数值做过合并(每个Reducer会对应到一个Partition,并且每个Partition使用快速排序算法(QuickSort)对key排序,如果设置了Combiner,则在排序的结果上运行combine).所以溢写过程一个很重要的细节在于,如果有很多个key/value对需要发送到某个reduce端去,那么需要将这些key/value值拼接到一块,减少与partition相关的索引记录. 在针对每个reduce端而合并数据时,有些数据可能像这样:”aaa”/1,”aaa”/1.对于WordCount例子,就是简单地统计单词出现的次数,如果在同一个map task的结果中有很多个像”aaa”一样出现多次的key,我们就应该把它们的值合并到一块,这个过程叫reduce也叫combine.但MapReduce的术语中,reduce只指reduce端执行从多个map task取数据做计算的过程.除reduce外,非正式地合并数据只能算做combine了.其实大家知道的,MapReduce中将Combiner等同于Reducer.

    如果client设置过Combiner,那么现在就是使用Combiner的时候了.将有相同key的key/value对的value加起来,减少溢写到磁盘的数据量.Combiner会优化MapReduce的中间结果,所以它在整个模型中会多次使用.那哪些场景才能使用Combiner呢?从这里分析,Combiner的输出是Reducer的输入,Combiner绝不能改变最终的计算结果.所以从我的想法来看,Combiner只应该用于那种Reduce的输入key/value与输出key/value类型完全一致,且不影响最终结果的场景.比如累加,最大值等.Combiner的使用一定得慎重,如果用好,它对job执行效率有帮助,反之会影响reduce的最终结果.

  4. 每次溢写会在磁盘上生成一个溢写文件(数据被写入到mapreduce.cluster.local.dir配置的目录中的其中一个,使用round robin fashion的方式轮流.注意写入的是本地文件目录,而不是HDFS.Spill文件名像sipll0.out,spill1.out等),如果map的输出结果真的很大,有多次这样的溢写发生,磁盘上相应的就会有多个溢写文件存在.当map task真正完成时,内存缓冲区中的数据也全部溢写到磁盘中形成一个溢写文件(mapreduce.task.io.sort.factor属性配置每次最多合并多少个文件,默认为10,即一次最多合并10个spill文件.spill文件数量大于mapreduce.map.combiner.minspills配置的数,则在合并文件写入之前,会再次运行combiner.如果spill文件数量太少,运行combiner的收益可能小于调用的代价).

    合并过程的简单示意: spill_merge

    最终磁盘中会至少有一个这样的溢写文件存在(如果map的输出结果很少,当map执行完成时,只会产生一个溢写文件),因为最终的文件只有一个,所以需要将这些溢写文件归并到一起,这个过程就叫做Merge.Merge是怎样的?如前面的例子,“aaa”从某个map task读取过来时值是5,从另外一个map 读取时值是8,因为它们有相同的key,所以得merge成group.什么是group.对于“aaa”就是像这样的:{“aaa”, [5, 8, 2, …]},数组中的值就是从不同溢写文件中读取出来的.请注意,因为merge是将多个溢写文件合并到一个文件,所以可能也有相同的key存在,在这个过程中如果client设置过Combiner,也会使用Combiner来合并相同的key.

    每个mapper也有对应的一个索引环形Buffer,默认为1KB,可以通过mapreduce.task.index.cache.limit.bytes来配置,索引如果足够小则存在内存中,如果内存放不下,需要写入磁盘(索引文件超过21845.3时).Spill文件索引名称类似这样spill110.out.index,spill111.out.index.Spill文件的索引事实上是 org.apache.hadoop.mapred.SpillRecord的一个数组,每个Map任务(源码中的MapTask.java类)维护一个这样的列表.

    索引及spill文件如下图示意: spill

至此,map端的所有工作都已结束,最终生成的这个文件也存放在TaskTracker够得着的某个本地目录内。每个reduce task不断地通过RPC从JobTracker那里获取map task是否完成的信息,如果reduce task得到通知,获知某台TaskTracker上的map task执行完成,Shuffle的后半段过程开始启动。

reduce端

reduce_shuffle reduce task在执行之前的工作就是不断地拉取当前job里每个map task的最终结果,然后对从不同地方拉取过来的数据不断地做merge,也最终形成一个文件作为reduce task的输入文件.

  1. Copy过程,简单地拉取数据.Reduce进程启动一些数据copy线程(Fetcher),通过HTTP方式请求map task所在的TaskTracker获取map task的输出文件.因为map task早已结束,这些文件就归TaskTracker管理在本地磁盘中.
  2. Merge阶段.这里的merge如map端的merge动作,只是数组中存放的是不同map端copy来的数值.Copy过来的数据会先放入内存缓冲区中,这里的缓冲区大小要比map端的更为灵活,它基于JVM的heap size设置,因为Shuffle阶段Reducer不运行,所以应该把绝大部分的内存都给Shuffle用.这里需要强调的是,merge有三种形式:1)内存到内存;2)内存到磁盘;3)磁盘到磁盘.默认情况下第一种形式不启用,让人比较困惑,是吧.当内存中的数据量到达一定阈值,就启动内存到磁盘的merge.与map 端类似,这也是溢写的过程,这个过程中如果你设置有Combiner,也是会启用的,然后在磁盘中生成了众多的溢写文件.第二种merge方式一直在运行,直到没有map端的数据时才结束,然后启动第三种磁盘到磁盘的merge方式生成最终的那个文件.
  3. Reducer的输入文件.不断地merge后,最后会生成一个”最终文件”.为什么加引号?因为这个文件可能存在于磁盘上,也可能存在于内存中.对我们来说,当然希望它存放于内存中,直接作为Reducer的输入,但默认情况下,这个文件是存放于磁盘中的.当Reducer的输入文件已定,整个Shuffle才最终结束.然后就是Reducer执行,把结果放到HDFS上.

Rcfile和orcfile

RCFile

RCFile文件格式是FaceBook开源的一种Hive的文件存储格式,首先将表分为几个行组,对每个行组内的数据进行按列存储,每一列的数据都是分开存储,正是先水平划分,再垂直划分的理念。 shuffle

存储结构

如上图是HDFS内RCFile的存储结构,我们可以看到,首先对表进行行划分,分成多个行组。一个行组主要包括:16字节的HDFS同步块信息,主要是为了区分一个HDFS块上的相邻行组;元数据的头部信息主要包括该行组内的存储的行数、列的字段信息等等;数据部分我们可以看出RCFile将每一行,存储为一列,将一列存储为一行,因为当表很大,我们的字段很多的时候,我们往往只需要取出固定的一列就可以。 在一般的行存储中 select a from table,虽然只是取出一个字段的值,但是还是会遍历整个表,所以效果和select * from table 一样,在RCFile中,像前面说的情况,只会读取该行组的一行。 在一般的列存储中,会将不同的列分开存储,这样在查询的时候会跳过某些列,但是有时候存在一个表的有些列不在同一个HDFS块上(如下图),所以在查询的时候,Hive重组列的过程会浪费很多IO开销

shuffle

列存储

而RCFile由于相同的列都是在一个HDFS块上,所以相对列存储而言会节省很多资源

存储空间

RCFile采用游程编码,相同的数据不会重复存储,很大程度上节约了存储空间,尤其是字段中包含大量重复数据的时候.
注:游程编码,是一种十分简单的无损数据压缩算法,在某些情况下非常有用,该算法的实现是用当前数据元素以及该元素连续出现的次数来取代字符串中连续出现的数据部分。如aaaaaaaaaabbbaxxxxyyyzyx压缩后a10b3a1x4y3z1y1x1。

懒加载

数据存储到表中都是压缩的数据,Hive读取数据的时候会对其进行解压缩,但是会针对特定的查询跳过不需要的列,这样也就省去了无用的列解压缩

select c from table where a > 1

针对行组来说,会对一个行组的a列进行解压缩,如果当前列中有a>1的值,然后才去解压缩c。若当前行组中不存在a>1的列,那就不用解压缩c,从而跳过整个行组。

ORCFile

ORC是在一定程度上扩展了RCFile,是对RCFile的优化。 shuffle

存储结构

根据结构图,我们可以看到ORCFile在RCFile基础上引申出来Stripe和Footer等。每个ORC文件首先会被横向切分成多个Stripe,而每个Stripe内部以列存储,所有的列存储在一个文件中,而且每个stripe默认的大小是250MB,相对于RCFile默认的行组大小是4MB,所以比RCFile更高效

  1. Postscripts中存储该表的行数,压缩参数,压缩大小,列等信息
  2. Stripe Footer中包含该stripe的统计结果,包括Max,Min,count等信息
  3. FileFooter中包含该表的统计结果,以及各个Stripe的位置信息
  4. IndexData中保存了该stripe上数据的位置信息,总行数等信息
  5. RowData以stream的形式保存了数据的具体信息

Hive读取数据的时候,根据FileFooter读出Stripe的信息,根据IndexData读出数据的偏移量从而读取出数据。 网友有一幅图,形象的说明了这个问题: shuffle

存储空间

ORCFile扩展了RCFile的压缩,除了Run-length(游程编码),引入了字典编码和Bit编码。 采用字典编码,最后存储的数据便是字典中的值,每个字典值得长度以及字段在字典中的位置 至于Bit编码,对所有字段都可采用Bit编码来判断该列是否为null,如果为null则Bit值存为0,否则存为1,对于为null的字段在实际编码的时候不需要存储,也就是说字段若为null,是不占用存储空间的。

MapReduce计数器

MapReduce计数器

运行MapReduce的example包下的WordCount,计数器日志信息如下:

File System Counters 文件系统计数器

  • FILE: Number of bytes read=78

    job读取本地文件系统的文件字节数.假定我们当前map的输入数据都来自于HDFS,那么在map阶段,这个数据应该是0.但reduce在执行前,它的输入数据是经过shuffle的merge后存储在reduce端本地磁盘中,所以这个数据就是所有reduce的总输入字节数

  • FILE: Number of bytes written=237975

    map的中间结果都会spill到本地磁盘中,在map执行完后,形成最终的spill文件.所以map端这里的数据就表示map task往本地磁盘中总共写了多少字节.与map端相对应的是,reduce端在shuffle时,会不断地拉取map端的中间结果,然后做merge并 不断spill到自己的本地磁盘中.最终形成一个单独文件,这个文件就是reduce的输入文件

  • FILE: Number of read operations=0
  • FILE: Number of large read operations=0
  • FILE: Number of write operations=0
  • HDFS: Number of bytes read=127

    整个job执行过程中,只有map端运行时,才从HDFS读取数据,这些数据不限于源文件内容,还包括所有map的split元数据.所以这个值应该比FileInputFormatCounters.BYTES_READ 要略大些

  • HDFS: Number of bytes written=36

    Reduce的最终结果都会写入HDFS,就是一个job执行结果的总量

  • HDFS: Number of read operations=6
  • HDFS: Number of large read operations=0
  • HDFS: Number of write operations=2

Job Counters 作业计数器

  • Launched map tasks=1

    job启动的map task个数

  • Launched reduce tasks=1

    job启动的reduce task个数

  • Data-local map tasks=1

    数据本地化map task个数

  • Total time spent by all maps in occupied slots (ms)=40326

    所有map任务在被占用的slots中所用的时间.在yarn中,程序打成jar包提交给resourcemanager,nodemanager向resourcemanager申请资源,然后在nodemanager上运行,而划分资源(cpu,io,网络,磁盘)的单位叫容器container,每个节点上资源不是无限的,因此应该将任务划分为不同的容器,job在运行的时候可以申请job的数量,之后由nodemanager确定哪些任务可以执行map,那些可以执行reduce等,从而由slot表示,表示槽的概念.任务过来就占用一个槽

  • Total time spent by all reduces in occupied slots (ms)=89505

    所有reduce任务在被占用的slots中所用的时间

  • Total time spent by all map tasks (ms)=40326 (所有map执行时间
  • Total time spent by all reduce tasks (ms)=89505 (所有reduce执行的时间
  • Total vcore-milliseconds taken by all map tasks=40326
  • Total vcore-milliseconds taken by all reduce tasks=89505
  • Total megabyte-milliseconds taken by all map tasks=41293824
  • Total megabyte-milliseconds taken by all reduce tasks=91653120

    Map-Reduce Framework MapReduce框架计数器

  • Map input records=6

    所有map task从HDFS读取的文件总行数)

  • Map output records=11

    map task的直接输出record是多少,就是在map方法中调用context.write的次数,也就是未经过Combine时的原生输出条数

  • Map output bytes=66

    Map的输出结果key/value都会被序列化到内存缓冲区中,所以这里的bytes指序列化后的最终字节之和

  • Map output materialized bytes=78
  • Input split bytes=105
  • Combine input records=11

    Combiner是为了减少尽量减少需要拉取和移动的数据,所以combine输入条数与map的输出条数是一致的

  • Combine output records=9

    经过Combiner后,相同key的数据经过压缩,在map端自己解决了很多重复数据,表示最终在map端中间文件中的所有条目数

  • Reduce input groups=9

    Reduce总共读取了多少个这样的groups

  • Reduce shuffle bytes=78

    Reduce端的copy线程总共从map端抓取了多少的中间数据,表示各个map task最终的中间文件总和

  • Reduce input records=9

    如果有Combiner的话,那么这里的数值就等于map端Combiner运算后的最后条数,如果没有,那么就应该等于map的输出条数

  • Reduce output records=9

    所有reduce执行后输出的总条目数

  • Spilled Records=18

    spill过程在map和reduce端都会发生,这里统计在总共从内存往磁盘中spill了多少条数据

  • Shuffled Maps =1

    每个reduce几乎都得从所有map端拉取数据,每个copy线程拉取成功一个map的数据,那么增1,所以它的总数基本等于 reduce number * map number

  • Failed Shuffles=0

    copy线程在抓取map端中间数据时,如果因为网络连接异常或是IO异常,所引起的shuffle错误次数

  • Merged Map outputs=1

    记录着shuffle过程中总共经历了多少次merge动作

  • GC time elapsed (ms)=172

    通过JMX获取到执行map与reduce的子JVM总共的GC时间消耗

  • CPU time spent (ms)=1600
  • Physical memory (bytes) snapshot=331804672
  • Virtual memory (bytes) snapshot=1755283456
  • Total committed heap usage (bytes)=168562688

    Shuffle Errors Shuffle错误计数器

  • BAD_ID=0

    每个map都有一个ID,如attempt_201703016150_0254_m_000000_0,如果reduce的copy线程抓取过来的元数据中这个ID不是标准格式,那么此Counter增加

  • CONNECTION=0

    表示copy线程建立到map端的连接有误的次数

  • IO_ERROR=0

    Reduce的copy线程如果在抓取map端数据时出现IOException,那么这个值相应增加

  • WRONG_LENGTH=0

    map端的那个中间结果是有压缩好的有格式数据,所以它有两个length信息:源数据大小与压缩后数据大小.如果这两个length信息传输的有误(负值),那么此Counter增加

  • WRONG_MAP=0

    每个copy线程当然是有目的:为某个reduce抓取某些map的中间结果,如果当前抓取的map数据不是copy线程之前定义好的map,那么就表示把数据拉错了

  • WRONG_REDUCE=0

    与上面描述一致,如果抓取的数据表示它不是为此reduce而准备的,那还是拉错数据了

    File Input Format Counters 文件输入格式计数器

  • Bytes Read=22

    Map task的所有输入数据(字节),等于各个map task的map方法传入的所有value值字节之和

    File Output Format Counters 文件输出格式计数器

  • Bytes Written=36

    Reduce task的所有输出数据(字节),等于各个reduce task的reduce方法传入的所有value值字节之和

virtualbox使用hostonly连外网

背景

公司机器都在内网中,访问外网需要配置代理。所以现在的问题是,virtualbox使用hostonly方式网络配置,并且需要配置代理访问外网。

配置hostonly

  1. 在客户机的Network Connections中配置Local Area Connection的sharing,勾选Allow other network user to connect…
  2. 查看VirtualBox Host-Only Network的ip(如192.168.56.1),此ip作为虚拟机的gateway,虚拟机ip设置为192.68.56.2
  3. 修改虚拟机dns,和主机中dns(ipconfig /all查看)配置一致

    配置代理

    配置代理有多种方式,此处为修改环境变量.bash_profile 在.bash_profile中加入:

    export http_proxy="http://proxy_addr:port"
    export https_proxy="http://proxy_addr:port"
    export ftp_proxy="http://proxy_addr:port"
    如果代理需要用户名和密码的话,这样设置:
    export http_proxy=”http://username:password@proxy_addr:port”
    

Spark的shuffle算子

  • 去重
    def distinct()
    def distinct(numPartitions: Int)
    
  • 聚合
    def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)]
    def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)]
    def groupBy[K](f: T => K, p: Partitioner):RDD[(K, Iterable[V])]
    def groupByKey(partitioner: Partitioner):RDD[(K, Iterable[V])]
    def aggregateByKey[U: ClassTag](zeroValue: U, partitioner: Partitioner): RDD[(K, U)]
    def aggregateByKey[U: ClassTag](zeroValue: U, numPartitions: Int): RDD[(K, U)]
    def combineByKey[C](createCombiner: V => C, mergeValue: (C, V) => C, mergeCombiners: (C, C) => C): RDD[(K, C)]
    def combineByKey[C](createCombiner: V => C, mergeValue: (C, V) => C, mergeCombiners: (C, C) => C, numPartitions: Int): RDD[(K, C)]
    def combineByKey[C](createCombiner: V => C, mergeValue: (C, V) => C, mergeCombiners: (C, C) => C, partitioner: Partitioner, mapSideCombine: Boolean = true, serializer: Serializer = null): RDD[(K, C)]
    
  • 排序
    def sortByKey(ascending: Boolean = true, numPartitions: Int = self.partitions.length): RDD[(K, V)]
    def sortBy[K](f: (T) => K, ascending: Boolean = true, numPartitions: Int = this.partitions.length)(implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T]
    

重分区

def coalesce(numPartitions: Int, shuffle: Boolean = false, partitionCoalescer: Option[PartitionCoalescer] = Option.empty)
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null)

集合或者表操作

def intersection(other: RDD[T]): RDD[T]
def intersection(other: RDD[T], partitioner: Partitioner)(implicit ord: Ordering[T] = null): RDD[T]
def intersection(other: RDD[T], numPartitions: Int): RDD[T]
def subtract(other: RDD[T], numPartitions: Int): RDD[T]
def subtract(other: RDD[T], p: Partitioner)(implicit ord: Ordering[T] = null): RDD[T]
def subtractByKey[W: ClassTag](other: RDD[(K, W)]): RDD[(K, V)]
def subtractByKey[W: ClassTag](other: RDD[(K, W)], numPartitions: Int): RDD[(K, V)]
def subtractByKey[W: ClassTag](other: RDD[(K, W)], p: Partitioner): RDD[(K, V)]
def join[W](other: RDD[(K, W)], partitioner: Partitioner): RDD[(K, (V, W))]
def join[W](other: RDD[(K, W)]): RDD[(K, (V, W))]
def join[W](other: RDD[(K, W)], numPartitions: Int): RDD[(K, (V, W))]
def leftOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (V, Option[W]))]

Scala与Java的隐式转换

Exception

Error:(32, 48) value filter is not a member of java.util.List[java.util.Map[String,AnyRef]]
    for(res: java.util.Map[String, AnyRef]  <- result){
  • 这是在scala中引用Java的集合进行迭代抛出的异常
  • 解决方案为import scala.collection.JavaConversions._ (Scala支持与Java的隐式转换)

Yarn权限配置

权限相关配置参数

  • 管理员和普通用户如何区分

    由参数yarn.admin.acl指定

  • 服务级别的权限(eg:哪些用户可以向集群提交ResourceManager提交应用程序)

    通过配置hadoop-policy.xml实现

  • 队列级别的权限(eg:哪些用户可以向队列A提交作业)

    由对应的资源调度器内部配置(eg:Fair Scheduler或者Capacity Scheduler etc.)