分类: hadoop

  • Scala编码规范

    格式与命名

    1) 代码格式用两个空格缩进。避免每行长度超过100列。在两个方法、类、对象定义之间使用一个空白行。

    2) 优先考虑使用val,而非var。

    3) 当引入多个包时,使用花括号:

    import jxl.write.{WritableCell, Number, Label}

    当引入的包超过6个时,应使用通配符_:

    import org.scalatest.events._

    4)若方法暴露为接口,则返回类型应该显式声明。例如:

    def execute(conn: Connection) : Boolean =
    {
          executeCommand(conn, sqlStatement) match
          {
            case Right(result) => result
            case Left(_) => false
          }
    }

    5) 集合的命名规范

    xs, ys, as, bs等作为某种Sequence对象的名称;

    x, y, z, a, b作为sequence元素的名称。

    h作为head的名称,t作为tail的名称。

    6)避免对简单的表达式采用花括号;

    //suggestion
    def square(x: Int) = x * x
    
    //avoid
    def square(x: Int) =
    {
      x * x
    }

     

    7) 泛型类型参数的命名虽然没有限制,但建议遵循如下规则:

    A 代表一个简单的类型,例如List[A]

    B, C, D 用于第2、第3、第4等类型。例如:

    class List[A] {

    def mapB: List[B] = …

    }

    N 代表数值类型

    注意:在Java中,通常以K、V代表Map的key与value,但是在Scala中,更倾向于使用A、B代表Map的key与value。

    8)数值类型变量

    scala有7种数值类型:Byte、Char、Short、Int、Long、Float和Double,以及2种非数值类型:Boolean和Unit(只有一个值“()”,相当于java和c++中的void,即空值)。

    这些类型都是抽象的final类(不能使用new新建,也不能被继承),在scala包中定义,是对java基本数据类型的包装,因此与java基本数据类型有相同的长度。

    同时,scala还提供了RichInt、RichChar等等,它们分别提供Int、Char等所不具备的便捷方法。

    另外,scala沿用了java.lang包中的String。在scala中,常量也称作字面量,字符串字面量由双引号包含的字符组成,同时scala提供了另一种定义字符串常量的语法——原始字符串,它以三个双引号作为开始和结束,字符串内部可以包含无论何种任意字符。

    在scala中,我们使用方法,而不是强制类型转换,来做数值类型之间的转换,如99.44.toInt、97.toChar。另外也可以参见显式类型转换和隐式转换。

     

    1.1.    命名规则

    1.1.1.      程序文件

    采用unix标准,比如user_info.scala,而不使用win32标准的UserInfo.scala。

    文件后缀说明

    后缀

    说明

    .scala

    scala源代码文件

    .py

    python源代码文件

    .java

    java源代码文件

    .r

    r源代码文件

    .proto protobuf协议文件

    .iml 、.xml

    配置文件

    .htm、.html、.shtml

    页面文件

    .sql

    SQL脚本、hivesql脚本

    .sh

    /bin/sh脚本

    Makefile

    make文件,无后缀

    1.1.2.      变量、函数、类

    个体类型前缀,驼峰式命名方法。

    前缀

    基本数据类型

    说明

    举例

    b

    Boolean

    Boolean bStatus

    ch

    Char

    Char chStx

    l

    Long

    *因为32位操作系统和64操作系统在数据长度上的问题,强烈要求不要使用

    Long lTimeValue

    i

    Int

    Int iFunReturn

    sh

    Short

    多用于表示尺寸

    Short shCodeLen

    s

    java.lang包中的String。

    String sUsrName

    f

    Float

    Float fMoney

    d

    Double

    Double dMoney

    o

    scala类实例对象

    Vector

    List

    Queue

    Array

    HashMap

    HashSet

    Map

    通常命名时4个字母一下
    全名,非时采用缩进剪短命名
    在加变量本身含义
    比如:

    Vector oVecSeq

    List oListVal
    HashMap oHMapVal

     

    生存周期前缀列表

    生命周期范围

    前缀

    备注

    全局变量

    g_

    String g_sProgramName

    类生存周期变量

    m_

    String CClass::m_sName

    语法特性

    1) 定义隐式类时,应该将构造函数的参数声明为val。

    2)使用for表达式;如果需要条件表达式,应将条件表达式写到for comprehension中:

    //not good
    for (file <- files) {
      if (hasSoundFileExtension(file) && !soundFileIsLong(file)) {
        soundFiles += file
      }
    }
    
    //better
    for {
      file <- files
      if hasSoundFileExtension(file)
      if !soundFileIsLong(file)
    } yield file

     

    通常情况下,我们应优先考虑filter, map, flatMap等操作,而非for comprehension:

    //best
    files.filter(hasSourceFileExtension).filterNot(soundFileIsLong)

    3) 避免使用isInstanceOf,而是使用模式匹配,尤其是在处理比较复杂的类型判断时,使用模式匹配的可读性更好。

    //avoid
    if (x.isInstanceOf[Foo]) { do something …
    
    //suggest
    def isPerson(x: Any) :  Boolean = x match {
        case p: Person => true
     case _ => false
    }

     

    4)以下情况使用abstract class,而不是trait:

    • 想要创建一个需要构造函数参数的基类
    • 代码可能会被Java代码调用

    5) 如果希望trait只能被某个类(及其子类)extend,应该使用self type:

    trait MyTrait { this: BaseType => }   

    如果希望对扩展trait的类做更多限制,可以在self type后增加更多对trait的混入:

    trait WarpCore {
     this: Starship with WarpCoreEjector with FireExtinguisher =>
    }
    // this works
    class Enterprise extends Starship 
    with WarpCore 
    with WarpCoreEjector 
    with FireExtinguisher
     
    // won't compile 
    class Enterprise extends Starship  
    with WarpCore  
    with WarpCoreEjector 
     

    如果要限制扩展trait的类必须定义相关的方法,可以在self type中定义方法,这称之为structural type(类似动态语言的鸭子类型):

    trait WarpCore {
       this: {
        def ejectWarpCore(password: String): Boolean
        def startWarpCore: Unit
       } =>
    }
    class Starship class Enterprise extends Starship with WarpCore { 
       def ejectWarpCore(password: String): Boolean = {
        if (password == "password") { println("core ejected"); true } else false }
       def startWarpCore { println("core started") }
    }
    

    6) 对于较长的类型名称,在特定上下文中,以不影响阅读性和表达设计意图为前提,建议使用类型别名,它可以帮助程序变得更简短。例如:

    class ConcurrentPool[K, V] { 
       type Queue = ConcurrentLinkedQueue[V]
       type Map   = ConcurrentHashMap[K, Queue]  
    }

    7) 如果要使用隐式参数,应尽量使用自定义类型作为隐式参数的类型,而避免过于宽泛的类型,如String,Int,Boolean等。

    //suggestion
    def maxOfList[T](elements: List[T])
       (implicit orderer: T => Ordered[T]): T =
     elements match {
      case List() =>
       throw new IllegalArgumentException("empty list!")
      case List(x) => x
      case x :: rest =>
       val maxRest = maxListImpParm(rest)(orderer)
       if (orderer(x) > maxRest) x
       else maxRest
     }
    //avoid
    def maxOfListPoorStyle[T](elements: List[T])
        (implicit orderer: (T, T) => Boolean): T
    

    8) 对于异常的处理,Scala除了提供Java风格的try…catch…finally之外,还提供了allCatch.opt、Try…Success…Failure以及Either…Right…Left等风格的处理方式。其中,Try是2.10提供的语法。根据不同的场景选择不同风格:

    优先选择Try风格。Try很好地支持模式匹配,它兼具Option与Either的特点,因而既提供了集合的语义,又支持模式匹配,又提供了getOrElse()方法。同时,它还可以组合多个Try,并支持运用for combination。

    val z = for {
        a <- Try(x.toInt ) b <- Try(y.toInt ) } yield a * b val answer = z.getOrElse (0) * 2 

    如果希望清楚的表现非此即彼的特性,应考虑使用Either。

    注意,约定成俗下,我们习惯将正确的结果放在Either的右边(Right既表示右边,又表示正确)

    如果希望将异常情况处理为None,则应考虑使用allCatch.opt。

    import scala.util.control.Exception._
    
    def readTextFile(f: String) :  Option[List[String]] =     
        allCatch.opt(Source.fromFile(f).getLines.toList)

    如果希望在执行后释放资源,从而需要使用finally时,考虑try…catch…finally,或者结合try…catch…finally与Either。

    private def executeQuery(conn: Connection, sql: String) :  Either[SQLException, ResultSet] = {
        var stmt: Statement = null
        var rs: ResultSet = null
        try {
          stmt = conn.createStatement()
          rs = stmt.executeQuery(sql)
          Right(rs)
        } catch {
          case e: SQLException => {
            e.printStackTrace()
            Left(e)
          }
        } finally {
          try {
            if (rs != null) rs.close()
            if (stmt != null) stmt.close()
          } catch {
            case e: SQLException => e.printStackTrace()
          }
        }
      }

    为避免重复,还应考虑引入Load Pattern。

    编码风格

    1) 尽可能直接在函数定义的地方使用模式匹配。例如,在下面的写法中,match应该被折叠起来(collapse):

    list map { item =>   
         item match {     
              case Some(x) => x     
              case None => default   
         } 
    }

    用下面的写法替代:

    list map {
       case Some(x) => x
       case None => default 
    }

    它很清晰的表达了 list中的元素都被映射,间接的方式让人不容易明白。此时,传入map的函数实则为partial function。

    2)避免使用null,而应该使用Option的None。

    import java.io._
    object CopyBytes extends App {
     var in = None: Option[FileInputStream]
     var out = None: Option[FileOutputStream]
     try {
      in = Some(new FileInputStream("/tmp/Test.class"))
      out = Some(new FileOutputStream("/tmp/Test.class.copy"))
      var c = 0
      while ({c = in.get.read; c != 1}) {
         out.get.write(c)
        }
     } catch {
      case e: IOException => e.printStackTrace
     } finally {
      println("entered finally ...")
      if (in.isDefined) in.get.close
      if (out.isDefined) out.get.close
     }
    }
    

    方法的返回值也要避免返回Null。应考虑返回Option,Either,或者Try。例如:

    import scala.util.{Try, Success, Failure} 
    def readTextFile(filename: String) :  Try[List[String]] = { 
     Try(io.Source.fromFile(filename).getLines.toList
    )
    val filename = "/etc/passwd" 
    readTextFile(filename) match {
     case Success(lines) => lines.foreach(println)
     case Failure(f) => println(f) 
    }
    

    3)若在Class中需要定义常量,应将其定义为val,并将其放在该类的伴生对象中:

    class Pizza (var crustSize: Int, var crustType: String) { 
     def this(crustSize:  Int)  {
      this(crustSize, Pizza.DEFAULT_CRUST_TYPE)
     }
     def this(crustType:  String)  {
      this(Pizza.DEFAULT_CRUST_SIZE, crustType)
     }
     def this() {
      this(Pizza.DEFAULT_CRUST_SIZE, Pizza.DEFAULT_CRUST_TYPE)
     }
     override def toString = s"A $crustSize inch pizza with a $crustType crust"
    }
    object Pizza {
     val DEFAULT_CRUST_SIZE = 12
     val DEFAULT_CRUST_TYPE = "THIN"
    }
    

    4)合理为构造函数或方法提供默认值。例如:

    class Socket (val timeout: Int = 10000)  

    5)如果需要返回多个值时,应返回tuple。

    def getStockInfo = {
         //
         ("NFLX", 100.00, 101.00)
    }

    6) 作为访问器的方法,如果没有副作用,在声明时建议定义为没有括号。

    例如,Scala集合库提供的scala.collection.immutable.Queue中,dequeue方法没有副作用,声明时就没有括号:

    import scala.collection.immutable.Queue
    
    val q = Queue(1, 2, 3, 4)
    val value = q.dequeue

    7) 将包的公有代码(常量、枚举、类型定义、隐式转换等)放到package object中。

    package com.agiledon.myapp
    package object model {
      // field
     val MAGIC_NUM = 42 182 | Chapter 6: Objects
      
     // method
     def echo(a: Any) { println(a) }
      // enumeration
     object Margin extends Enumeration {
        type Margin = Value
        val TOP, BOTTOM, LEFT, RIGHT = Value
      }
      // type definition
     type MutableMap[K, V] = scala.collection.mutable.Map[K, V]
      val MutableMap = scala.collection.mutable.Map
    }
    
    

    8) 建议将package object放到与包对象命名空间一致的目录下,并命名为package.scala。以model为例,package.scala文件应放在:

    +– com

    +– agiledon

    +– myapp

    +– model

    +– package.scala

    9) 若有多个样例类属于同一类型,应共同继承自一个sealed trait。

    sealed trait Message
    case class GetCustomers extends Message case class GetOrders extends Message 

    注:这里的sealed,表示trait的所有实现都必须声明在定义trait的文件中。

    10) 考虑使用renaming clause来简化代码。例如,替换被频繁使用的长名称方法:

    import System.out.{println => p}
    
    p("hallo scala")
    p("input")

    11) 在遍历Map对象或者Tuple的List时,且需要访问map的key和value值时,优先考虑采用Partial Function,而非使用_1和_2的形式。例如:

    val dollar = Map("China" -> "CNY", "US" -> "DOL")
    
    //perfer
    dollar.foreach {
         case (country, currency) => println(s"$country -> $currency")
    }
    
    //avoid
    dollar.foreach ( x => println(s"$x._1 -> $x._2") )

    或者,考虑使用for comprehension:

    for ((country, currency) <- dollar) println(s"$country -> $currency")

    12) 遍历集合对象时,如果需要获得并操作集合对象的下标,不要使用如下方式:

    val l = List("zero", "one", "two", "three")
    
    for (i <- 0 until l.length) yield (i, l(i))

    而应该使用zipWithIndex方法:

    for ((number, index) <- l.zipWithIndex ) yield (index, number) 

    或者:

    l.zipWithIndex.map(x => (x._2, x._1))

    当然,如果需要将索引值放在Tuple的第二个元素,就更方便了。直接使用zipWithIndex即可。

    zipWithIndex的索引初始值为0,如果想指定索引的初始值,可以使用zip:

    l.zip(Stream from 1)

    13) 应尽量定义小粒度的trait,然后再以混入的方式继承多个trait。例如ScalaTest中的FlatSpec:

    class FlatSpec extends FlatSpecLike ... trait FlatSpecLike extends Suite with ShouldVerb with MustVerb with CanVerb with Informing  

    小粒度的trait既有利于重用,同时还有利于对业务逻辑进行单元测试,尤其是当一部分逻辑需要依赖外部环境时,可以运用“关注点分离”的原则,将不依赖于外部环境的逻辑分离到单独的trait中。

    14) 优先使用不可变集合。如果确定要使用可变集合,应明确的引用可变集合的命名空间。不要用使用import scala.collection.mutable._;然后引用 Set,应该用下面的方式替代:

    import scala.collections.mutable
    val set = mutable.Set() 

    这样更明确在使用一个可变集合。

    15) 在自己定义的方法和构造函数里,应适当的接受最宽泛的集合类型。通常可以归结为一个: Iterable, Seq, Set, 或 Map。如果你的方法需要一个 sequence,使用 Seq[T],而不是List[T]。这样可以分离集合与它的实现,从而达成更好的可扩展性。

    16) 应谨慎使用流水线转换的形式。当流水线转换的逻辑比较复杂时,应充分考虑代码的可读性,准确地表达开发者的意图,而不过分追求函数式编程的流水线转换风格。例如,我们想要从一组投票结果(语言,票数)中统计不同程序语言的票数并按照得票的顺序显示:

    val votes = Seq(("scala", 1), ("java", 4), ("scala", 10), ("scala", 1), ("python", 10))
    val orderedVotes = votes
       .groupBy(_._1)
       .map { case (which, counts) =>
         (which, counts.foldLeft(0)(_ + _._2))
       }.toSeq
       .sortBy(_._2)
       .reverse

    上面的代码简洁并且正确,但几乎每个读者都不好理解作者的原本意图。一个策略是声明中间结果和参数:

    val votesByLang = votes groupBy { case (lang, _) => lang }
    val sumByLang = votesByLang map {
      case (lang, counts) =>
        val countsOnly = counts map { case (_, count) => count }
        (lang, countsOnly.sum)
    }
    val orderedVotes = sumByLang.toSeq
      .sortBy { case (_, count) => count }
      .reverse
    
    

    代码也同样简洁,但更清晰的表达了转换的发生(通过命名中间值),和正在操作的数据的结构(通过命名参数)。

    17) 对于Options对象,如果getOrElse能够表达业务逻辑,就应避免对其使用模式匹配。许多集合的操作都提供了返回Options的方法。例如headOption等。

    val x = list.headOption getOrElse 0

    这要比模式匹配更清楚:

    val x = list match 
         case head::_ => head
         case Nil: => 0

    18) 当需要对两个或两个以上的集合进行操作时,应优先考虑使用for表达式,而非map,flatMap等操作。此时,for comprehension会更简洁易读。例如,获取两个字符的所有排列,相同的字符不能出现两次。使用flatMap的代码为:

    val chars = 'a' to 'z' 
    val perms = chars flatMap { a => 
       chars flatMap { b => 
         if (a != b) Seq("%c%c".format(a, b))
         else Seq() 
       }
     }

    使用for comprehension会更易懂:

    val perms = for {
       a <- chars
       b <- chars
       if a != b
     } yield "%c%c".format(a, b)

    高效编码

    1) 应尽量避免让trait去extend一个class。因为这种做法可能会导致间接的继承多个类,从而产生编译错误。同时,还会导致继承体系的复杂度。

    class StarfleetComponent trait StarfleetWarpCore extends StarfleetComponent class Starship extends StarfleetComponent with StarfleetWarpCore class RomulanStuff // won't compile class Warbird extends RomulanStuff with StarfleetWarpCore 

    2) 选择使用Seq时,若需要索引下标功能,优先考虑选择Vector,若需要Mutable的集合,则选择ArrayBuffer;若要选择Linear集合,优先选择List,若需要Mutable的集合,则选择ListBuffer。

    3) 如果需要快速、通用、不变、带顺序的集合,应优先考虑使用Vector。Vector很好地平衡了快速的随机选择和快速的随机更新(函数式)操作。Vector是Scala集合库中最灵活的高效集合。一个原则是:当你对选择集合类型犹疑不定时,就应选择使用Vector。

    需要注意的是:当我们创建了一个IndexSeq时,Scala实际上会创建Vector对象:

    scala> val x = IndexedSeq(1,2,3) 
    x: IndexedSeq[Int]  = Vector(1, 2, 3)

    4) 如果需要选择通用的可变集合,应优先考虑使用ArrayBuffer。尤其面对一个大的集合,且新元素总是要添加到集合末尾时,就可以选择ArrayBuffer。如果使用的可变集合特性更近似于List这样的线性集合,则考虑使用ListBuffer。

    5) 如果需要将大量数据添加到集合中,建议选择使用List的prepend操作,将这些数据添加到List头部,最后做一次reverse操作。例如:

    var l = List[Int]()
    (1 to max).foreach {
         i => i +: l
    }
    l.reverse

    6) 当一个类的某个字段在获取值时需要耗费资源,并且,该字段的值并非一开始就需要使用。则应将该字段声明为lazy。

    lazy val field = computation()

    7) 在使用Future进行并发处理时,应使用回调的方式,而非阻塞:

    //avoid
    val f = Future {
      //executing long time
    }
    val result = Await.result(f, 5 second)
    //suggesion
    val f = Future {
      //executing long time
    }
    f.onComplete {
      case Success(result) => //handle result
     case Failure(e) => e.printStackTrace
    }
    
    

    8) 若有多个操作需要并行进行同步操作,可以选择使用par集合。例如:

    val urls = List("http://scala-lang.org", "http://agiledon.github.com")
    
    def fromURL(url:  String)  = scala.io.Source.fromURL(url).getLines().mkString("\n")
    
    val t = System.currentTimeMillis()
    urls.par.map(fromURL(_))
    println("time: " + (System.currentTimeMillis - t) + "ms")

    9) 若有多个操作需要并行进行异步操作,则采用for comprehension对future进行join方式的执行。例如,假设Cloud.runAlgorithm()方法返回一个Futrue[Int],可以同时执行多个runAlgorithm方法:

    val result1 = Cloud.runAlgorithm(10)
    val result2 = Cloud.runAlgorithm(20)
    val result3 = Cloud.runAlgorithm(30)
    
    val result = for {
      r1 <- result1 r2 <- result2 r3 <- result3 } yield (r1 + r2 + r3) result onSuccess { case result =>  println(s"total = $result")
    }

    编码模式

    1) Loan Pattern: 确保打开的资源(如文件、数据库连接)能够在操作完毕后被安全的释放。

    Loan Pattern的通用格式如下:

    def using[A](r : Resource)(f : Resource => A) : A =
       try {
            f(r)
       } finally {
            r.dispose()
       }

    这个格式针对Resource类型进行操作。还有一种做法是:只要实现了close方法,都可以运用Loan Pattern:

    def using[A <:  def close():Unit, B][resource: A](f: A => B): B = 
         try {
              f(resource)
         } finally {
              resource.close()
         }

    以FileSource为例:

    using(io.Source.fromFile("example.txt")) { 
        source => {
            for (line <- source.getLines ) { println(line) } } } 

    2) Cake Pattern: 利用self type实现依赖注入

    例如,对于DbAccessor而言,需要提供不同的DbConnectionFactory来创建连接,从而访问不同的Data Source。

    trait DbConnectionFactory {
         def createDbConnection:  Connection
    }
    
    trait SybaseDbConnectionFactory extends DbConnectionFactory
    trait MySQLDbConnectionFactory extends DbConnectionFactory

    运用Cake Pattern,DbAccessor的定义应该为:

    trait DbAccessor {
         this: DbConnectionFactory => 
    
         //…
    }

    由于DbAccessor使用了self type,因此可以在DbAccessor中调用DbConnectionFactory的方法createDbConnection()。客户端在创建DbAccessor时,可以根据需要选择混入的DbConnectionFactory:

    val sybaseDbAccessor = new DbAccessor with SybaseDbConnectionFactory

    当然,也可以定义object:

    object SybaseDbAccessor extends DbAccessor with SybaseDbConnectionFactory
    object MySQLDbAccessor extends DbAccessor with MySQLDbConnectionFactory

    测试

    1) 测试类应该与被测试类处于同一包下。如果使用Spec2或ScalaTest的FlatSpec等,则测试类的命名应该为:被测类名 + Spec;若使用JUnit等框架,则测试类的命名为:被测试类名 + Test

    2) 测试含有具体实现的trait时,可以让被测试类直接继承Trait。例如:

    trait RecordsGenerator {
     def generateRecords(table: List[List[String]]): List[Record] {
      //...
     }
    }
    class RecordsGeneratorSpec extends FlatSpec with ShouldMatcher with RecordGenerator { 
     val table = List(List("abc", "def"), List("aaa", "bbb"))
     it should "generate records" in {
      val records = generateRecords(table)
      records.size should be(2)
     }
    }
    

    3) 若要对文件进行测试,可以用字符串假装文件:

    type CsvLine = String
    def formatCsv(source: Source) :  List[CsvLine] = {
         source.getLines(_.replace(", ", "|"))
    }

    formatCsv需要接受一个文件源,例如Source.fromFile(“testdata.txt”)。但在测试时,可以通过Source.fromString方法来生成formatCsv需要接收的Source对象:

    it should "format csv lines" in {
         val lines = Source.fromString("abc, def, hgi\n1, 2, 3\none, two, three")
         val result = formatCsv(lines)
         result.mkString("\n") should be("abc|def|hgi\n1|2|3\none|two|three")
    }

     

    避免直接借用其他语言的编码规范

     

    本文将讨论Scala中的一些编码规范,它们有助于减少编译和运行时的错误。

    大家都有几门开发语言的编写能力,每种语言又有自己的语法格式和高质量编程要求,也有一些编码规范是在各种语言通用的,比如良好的注释。

    但Scala与我们经常碰到的Java,C,C++还是有很大的不同,在深入理解Scala之前,最好谨慎使用其他语言相关的编码规范。

    编码规范在团队开发中必须的。它帮助团队避免一些曾经出现的错误,提供代码层面交流的一致性语言。我们也许没有去看自己公司、团队的编码规范,但可以从代码中略知一二。

    建立团队的编码规范的步骤有:

    • 首先建立预防错误的一些规则。这些规则可能是来自使用相同语言的其他项目。然后自己添加一些从过去出错项目中总结的一些规则。比如C++中析构函数应该声明为虚函数。
    • 然后根据团队自己的开发环境,来发现、定义一些新的编码规范。比如包的命名方式。
    • 坚持执行前面的制定的规则。最好有一个自动化工具,能够检测我们的编码是否符合规范,然后可以自动做一些重构工作。

    核心规则8条

    大家肯定见过关于左大括号应该换行写,还是同行写的争论。当然这个争论无关痛痒,对编译器来说没有什么影响。我们看一个Scala例子:

    class FooHolder
    {
      def foo1()
      {
        println("foo1 was called")
      }
      def foo2()Unit =
      {
        println("foo2 was called")
      }
      def foo3() =
        println("foo3 was called")
    }

     

    foo1、foo2、foo3都是正确的。都是定义一个函数,然后输出一个字符串。不同的是书写风格。foo1类似于C语言风格,但是没有指定返回值;foo2是标准完整的Scala函数定义,有返回值,有表达式;foo3虽然没有返回值,但是有表达式赋值。

    需要注意的是,对于foo3,如果没有=。那么在实例化一个FooHolder的时候编译器会报错:class FooHolder needs to be abstract, since method foo3 is not defined。因为编译器会把它认为是一个抽象函数,这样的类去实例化是不允许的。虽然Scala给大家提供了一个非常宽松的环境,但为了避免类似的错误,也从人们理解上考虑,避免歧义,避免猜测,建议大家使用第二种foo2的方式。

    悬垂操作符

    dangling operator怎么翻译,没有找到一个现成的答案,那就自己定义一个名字:悬垂操作符。它指的是位于每行最后的操作符,比如+、-都可以作为悬垂操作符,它告诉Scala编译器本行还没有结束。

    在Java中我们连接一个字符串可以随便写,同行也可以,分行也可以。但是Scala中,操作符需要考虑它们所在的位置。比如下面的代码:

    val x = 5   def foo2 = "HAI"     + x
        + "ZOMG"
        + "\n"

     

    这个函数编译是会出错的:value unary_+ is not a member of String。String没有一元操作符+(Scala直接使用的Java的String)。但是x(类型为Int,Scala自己的一个类型)却是有的,所以+x没有报错。

    为了解决这个编译错误,我们有两种办法:

    一是告诉编译器+表示的是一行尚未完,即使用悬垂操作符:

      val x = 5   def foo1 = "HAI" +     x +
        "ZOMG" +
        "\n"

     

    二是加上括号:

      def foo2 = ("HAI"     + x     + "ZOMG"     + "\n")

     

    使用有意义的变量名

    一般的语言标识符只能是字母、数字和下划线,外加一些限制。相比之下,Scala提供非常灵活的命名方式。Scala有三种方法可以构造一个标识符:

    第一,首字符是字母,后续字符是任意字母和数字。这种标识符还可后接下划线‟_‟,然后是任意字母和数字。

    第二,首字符是算符字符,后续字符是任意算符字符。这两种形式是普通标识符。

    最后,标识符可以是由反引号‟`‟括起来的任意字符串(宿主系统可能会对字符串和合法性有些限制)。这种标识符可以由除了反引号的任意字符构成。

    第二条规则的存在,很容易让人回想起C++的操作符重载。Scala直接将其当作标识符来处理,应该是更进来一步。

    避免在标示符中使用$。因为编译器内部为内部类、闭包等生成的内部标示符使用了$。如果出现同名,将会导致代码奇怪的行为。有兴趣的话,可以看看编译生成的汇编代码。

    在Scala2.8引入命名参数和默认参数。命名参数会作为API的一部分,名字的改变会使客户端代码出错。因此请使用有意义的名字来对参数进行命名。这就是核心规则6。

    class Foo {
      def foo(one: Int = 1,
              two: String = "two",
              three: Double = 2.5): String =
        two + one + three
    }
    object Test extends scala.App{
      val x = new Foo
      println(x.foo())
      println(x.foo(two = "not two"))
      println(x.foo(0,"zero",0.1))
      println(x.foo(4three = 0.4))
      println(x.foo(three = 0.4one = 3two = "two here"))
    }

     

    运行结果:

    two12.5

    not two12.5

    zero00.1

    two40.4

    two here30.4

    C++也有默认参数。但是没有参数命名。这样C++就有一些限制,需要默认参数放到右边。Scala参数有了名字,调用的时候,它们的顺序就可以任意存放。但如果调用的时候,有的参数直接传值,有的使用参数名字,比如上面的x.foo(4, three = 0.4),我们就需要注意没有使用名字的参数的顺序。

    Scala使用变量的静态类型来绑定参数名字,但是缺省值是由运行时类型决定的。一句话:名字是静态的、值是动态。看一个例子:

    class Parent {
      def foo(bar: Int = 1, baz: Int = 2): Int =
        bar + baz
    }
    class Child extends Parent {
      override def foo(baz: Int = 3, bar: Int = 4): Int =
        super.foo(baz,bar)
    }
    object Test extends scala.App{
      val p = new Parent
      println(p.foo())
      val y = new Child
      println(y.foo())
      val z: Parent = new Child
      println(z.foo())
      println(y.foo(bar = 1))
      println(z.foo(bar = 1))
      println(z.foo(baz = 4))
    }

     

    输出如下:

    3

    7

    7

    4

    5

    7

    Parent定义了foo,子类Child覆盖了foo。z.foo()使用的是缺省值,缺省值是由z的运行时类型Child提供的,所以baz=3,bar=4,输出为7。y.foo(bar = 1)运行时类型和静态类型都是Child,所以baz=3,bar=1,输出为4。z.foo(bar = 1)的运行时类型是Child,静态类型是Parent,函数使用静态类型的,即Parent的def foo(bar: Int = 1, baz: Int = 2): Int。但要注意的是Child和Parent的命名参数位置是反的。缺省值使用静态类型的,所以baz=4(Parent的baz对应Child的bar),bar=1,输出为5。同样的,可以得到z.foo(baz = 4)的结果是7。

    可以看到对于交换了命名参数位置的重载,从理解上看,不直观,比较困难。所有大家应该保持重载的命名参数是一致的。不要随意交换它们的位置。

    重载带有命名参数的函数,可以修改函数的默认值。

    告诉大家这是重载函数

    核心规则7:Scala中,虽然有的时候override是可选的,但是坚持使用override是安全的。

    Trait与Java的interface类似,但是可以拥有方法体,并且可以在类实例化的时候混入。我们编写下面的代码:

    trait UserService {
      def login(credentials: Credentials): UserSession
      def logout(session: UserSession): Unit
      def isLoggedIn(session: UserSession): Boolean
      def changePassword(new_credentials: Credentials,
                         old_credentials: Credentials): Boolean
    }
    class UserServiceImpl extends UserService {
      def login(credentials: Credentials): UserSession =
        new UserSession {}
      def logout(session: UserSession): Unit //class UserServiceImpl needs to be abstract, since method logout is not defined
      def isLoggedIn(session: UserSession): Boolean = true
      def changePassword(session: UserSession,
                         credentials: Credentials): Boolean = true
    }

     

    编译失败。因为编译器发现UserServiceImpl的logout还是一个抽象函数,没有实现,这在class中是不允许的。还有一个错误是UserService的ChangePassword作为一个抽象函数也没有实现。

    如果我们给UserServiceImp加上override关键字,也可以通过编译:

    trait UserServiceImpl extends UserService {
      override def login(credentials: Credentials): UserSession =
        new UserSession {}
      override def logout(session: UserSession): Unit
      override def isLoggedIn(session: UserSession): Boolean = true
      override def changePassword(session: UserSession,                      credentials: Credentials): Boolean = true
    }

     

    这是因为Scala在实现一个抽象方法的时候,不需要override关键字。这样的设置是为了解决多重继承的菱形继承问题。举个例子:

    trait Animal {
      def talk: String
    }
    trait Cat extends Animal {
      override def talk: String = "Meow"
    }
    trait Dog extends Animal {
      override def talk: String = "Woof"
    }
    object Test extends scala.App
    {
      val kittydoggy = new Cat with Dog
      println(kittydoggy.talk) //Woof
      val kittydoggy2 = new Dog with Cat
      println(kittydoggy2.talk) //Meow
    }

     

    大家可能不理解为什么输出是这样?这是Scala的类线性化(Class linearization)决定的。当存在多个函数的多个重载实现的时候,Scala从类声明的最右边开始,在每个类中寻找函数,如果找到,就停止查找。new Cat with Dog会先看Dog有没有定义talk,Dog定义了talk,并返回Woof,所以输出是Woof。类似地,new Dog with Cat会在Cat里面找talk函数。

    如果我们去掉Dog和Cat里面的override。编译器会报下面的错误:

    anonymous class $anon inherits conflicting members:

    method talk in trait Cat of type => String and

    method talk in trait Dog of type => String

    (Note: this can be resolved by declaring an override in anonymous class $anon.)

    val kittydoggy = new Cat with Dog

    ^

    anonymous class $anon inherits conflicting members:

    method talk in trait Dog of type => String and

    method talk in trait Cat of type => String

    (Note: this can be resolved by declaring an override in anonymous class $anon.)

    val kittydoggy2 = new Dog with Cat

    ^

    如果我们只是去掉Dog的override,编译器会报:

    anonymous class $anon inherits conflicting members:

    method talk in trait Cat of type => String and

    method talk in trait Dog of type => String

    (Note: this can be resolved by declaring an override in anonymous class $anon.)

    val kittydoggy = new Cat with Dog

    ^

    在Cat中混入Dog是不允许的,因为Dog没有说它的talk方法可以被重载。反过来,Dog中混入Cat是可以的,因为Cat说明了自己的talk方法可以被重载。

    为了重载某个方法,我们可以在类实例化的时候混入trait,而不需要定义一个新的类。

    对期待的优化使用注解

    Scala编译器在生成字节码的时候做一些优化操作:

    • 优化尾递归【注1】
    • 优化模式匹配

    Scala通过将模式匹配当做switch来处理,来优化模式匹配的效率。模式匹配优化时,编译器编译生成分支表,而不是决策树。这意味着,我们不是在拿值做比较。而是用匹配的值来直接定位分支表。通过JVM的tableswitch操作码可以直接完成。

    Scala使用tableswitch进行优化,必须满足3个条件:

    1. 用俩匹配的值必须是一个已知的整数
    2. 每个匹配表达式必须足够简单:不能包含类型检查、if语句和extractors。如果是表达式需要在编译时就是可用的:它保持不变,不能在运行时求值。
    3. 至少有两个以上分支,否则优化是没有必要的。

    我们继续看个例子:

      def unannotated(x: Int) = x match {
        case 1 => "One"
        case 2 => "Two!"
        case z => z + "?"
      }

     

    使用javap -c得到汇编代码:

    public java.lang.String unannotated(int);

    Code:

    0: iload_1

    1: istore_2

    2: iload_2

    3: tableswitch{ //1 to 2

    1: 51;

    2: 46;

    default: 24 }

    24: new #12; //class scala/collection/mutable/StringBuilder

    27: dup

    28: invokespecial #16; //Method scala/collection/mutable/StringBuilder.”<init>”:()V

    31: iload_2

    32: invokevirtual #20; //Method scala/collection/mutable/StringBuilder.append:(I)Lscala/collection/mutable/StringBuilder;

    35: ldc #22; //String ?

    37: invokevirtual #25; //Method scala/collection/mutable/StringBuilder.append:(Ljava/lang/Object;)Lscala/collection/mutable/StringBuilder;

    40: invokevirtual #29; //Method scala/collection/mutable/StringBuilder.toString:()Ljava/lang/String;

    43: goto 53

    46: ldc #31; //String Two!

    48: goto 53

    51: ldc #33; //String One

    53: areturn

    在得到参数后,放到临时变量,然后调用tableswith指令,通过索引访问跳转表,并跳转。

    我们修改一下上面的代码:

    def notOptimised(x: Int) = x match {
    case 1 => "One"
    case 2 => "Two!"
    case i: Int => "Other"
    }

     

    在最后一个分支上,加上了类型检查。这样就不满足前面的条件2,Scala不会进行优化了。

    public java.lang.String notOptimised(int);

    Code:

    0: iload_1

    1: istore_2

    2: iconst_1

    3: iload_2

    4: if_icmpne 13

    7: ldc #12; //String One

    9: astore_3

    10: goto 27

    13: iconst_2

    14: iload_2

    15: if_icmpne 24

    18: ldc #14; //String Two!

    20: astore_3

    21: goto 27

    24: ldc #16; //String Other

    26: astore_3

    27: aload_3

    28: areturn

    使用if_icmpne指令来判断两个int是否相等,进而决定是不是需要进行跳转。

    我们如何知道编译器是否做了模式匹配的优化呢?Scala可以给类型表达式使用注解。@switch告诉编译器我想做tableswitch优化。如果编译器发现它做不了优化就会报错。比如下面的代码:

    import annotation.switch
    class Tableswitch{
      def annotated(x: Int @switch) = x match {
        case 1 => "One"
        case 2 => "Two!"
        case z => z + "?"
      }
      def notOptimised(x: Int) =
        (x: @switch) match {
          case 1 => "One"
          case 2 => "Two!"
          case i: Int => "Other"
        }
    }

     

    在第11行会报错:

    could not emit switch for @switch annotated match

    (x: @switch) match {

    ^

    @tailrec注解用来告诉编译器,请对尾递归进行优化。

    核心规则8:对需要优化的尾递归,加上@tailrec注解,保证我们得到期望的性能优化。

    Scala编译器进行尾递归优化也需要满足3个条件:

    1. 方法必须声明为final或private,不能是多态的。
    2. 方法必须有返回值注解。
    3. 方法必须在其一个返回分支上最后调用自己。

    我们有一个普通递归和一个尾递归:

    import annotation.tailrec
    object TailRecursion{
      def FibonacciRecursive(n: Int): Int = {
        if(n < 2)
          n
        else
          FibonacciRecursive(n-1)+FibonacciRecursive(n-2)
      }
      @tailrec
      def FibonacciTailRecursive(n: Int, ret1: Int, ret2: Int): Int = {
        if(n < 2)
          ret1
        else
          FibonacciTailRecursive(n-1, ret2, ret1 + ret2)
      }
    }

     

    它们的汇编代码:

    public int FibonacciRecursive(int);

    Code:

    0: iload_1

    1: iconst_2

    2: if_icmpge 9

    5: iload_1

    6: goto 24

    9: aload_0

    10: iload_1

    11: iconst_1

    12: isub

    13: invokevirtual #16; //Method FibonacciRecursive:(I)I

    16: aload_0

    17: iload_1

    18: iconst_2

    19: isub

    20: invokevirtual #16; //Method FibonacciRecursive:(I)I

    23: iadd

    24: ireturn

    public int FibonacciTailRecursive(int, int, int);

    Code:

    0: iload_1

    1: iconst_2

    2: if_icmpge 7

    5: iload_2

    6: ireturn

    7: iload_1

    8: iconst_1

    9: isub

    10: iload_3

    11: iload_2

    12: iload_3

    13: iadd

    14: istore_3

    15: istore_2

    16: istore_1

    17: goto 0

    可见Scala对后者进行了优化。使用的是goto循环。但是如果我们在FibonacciRecursive加上@tailrec注解,编译器就会报错:

    could not optimize @tailrec annotated method FibonacciRecursive: it contains a recursive call not in tail position

    FibonacciRecursive(n-1)+FibonacciRecursive(n-2)

    ^

    优化注解并不是要求编译器去做优化,而是要求编译或发出警告。

    【注1】尾递归指的是函数最后一句调用自身的函数。尾递归优化可以减少递归需要的栈空间。一般将其展开为循环,或者重复使用当前栈空间。尾递归会增加理解的难度,并且不少编译器是不提供尾递归优化的。

     

    参考资料

    来源于网络

    1. Scala Style Guide
    2. Programming in Scala , Martin Odersky
    3. Scala Cookbook , Alvin Alexander
    4. Effective Scala , Twitter
    5. 深入理解Scala-编码规范

  • Spark性能优化指南

    1.优化Spark

    由于大多数Spark计算的内存本质,Spark程序可能因为集群中的任何资源造成瓶颈:CPU,网络,带宽,或者内存。大多数情况下,如果数据可以容纳在内存中,性能瓶颈就是网络带宽,但是有时,你还是需要做一些调优,比如用序列化形式存储RDDs来减少内存使用。这篇指南会覆盖两个主题:数据序列化,这对良好的网络性能是非常关键的,而且也可以减少内存使用;内存调优。我们也讨论了一些小的主题。

    2.数据序列化

    序列化在分布式应用的性能中扮演了一个非常重要的角色。那些序列化对象速度很慢的格式,或者消耗大量字节的格式,会极大的降低性能。通常来说,这应该是你调优一个spark应用时要做的第一件事。spark旨在便捷性(允许你在操作中使用任何java类型)和性能之间取得一个平衡。它提供了两种序列化类库:

     

    · Java序列化:默认情况下,spark使用java的ObjectOutputStream框架来序列化对象,并且对你创建的任何实现了java.io.Serialiable接口的类都有效。你可以控制你的序列化机制的性能,只要实现java.io.Externalizable即可。java序列化机制是灵活的,但是通常是很慢的,对很多类来说,它会导致非常大的序列化格式。

     

    · Kryo序列化:Spark也可以使用Kryo类库来更快地序列化对象。Kryo极大地加快了序列化速度,并且比java序列化格式更加紧凑(通常可以达到10倍)。但是不支持所有的序列化类性,而且要求你自己预先注册在程序中使用的自定义类,来获得最佳的性能。

     

    你可以切换为使用Kryo,只要在使用SparkConf初始化你的应用时,调用conf.set(“spark.serializer”, “org.apache.spark.serializer.KryoSerializer”)。这个配置设置了序列化器,不仅仅是对于在worker node之间shuffle数据,而且也对于将RDDs序列化到磁盘上。Kryo不是默认序列化器的唯一原因是因为它的自定义类注册要求,但是我们推荐在任何网络资源紧张的应用中使用Kryo。

     

    Spark对许多常用的核心Scala类都自动包含了Kryo序列化器。

     

    要注册你自己的自定义类到Kryo上,使用registerKryoClass方法。

     

    valconf=newSparkConf().setMaster(...).setAppName(...)
    conf.registerKryoClasses(Array(classOf[MyClass1],classOf[MyClass2]))
    valsc=newSparkContext(conf)

     

    Kryo文档描述了更多高级的注册选项,比如增加自定义序列化代码。

    如果你的对象很大,你也许需要增加spark.kryoserializer.buffer.mb属性的值。默认是2MB,但是这个值需要足够大到保存你最大的对象。

     

    最后,如果你不注册你的自定义类,Kryo还是会工作,但是它就必须存储每个对象的全限定类名,很浪费内存。

    3.内存调优

    在优化内存使用的时候有三个考虑因素:你的对象使用的内存总量(你可能希望你完整的数据集都保存在内存中),访问这些对象的成本,垃圾回收的开销(如果你有大量对象的处理)。

     

    默认情况下,访问Java对象是非常快的,但是会比它们的field中的原始数据多消耗2-5倍的空间。这是因为多种原因:

     

    · 每个java对象都有一个对象头,大约是16个字节,包含了诸如指向它的类的引用这类信息。对于那些只有很少的数据的对象(比如只有一个int field),对象头会比对象本身的数据要大很多。

     

    · java字符串比原始字符串数据要多出40个字节的开销(因为它将数据存储在一个char数组中,并且还保存了额外的信息,比如字符串的长度),而且因为字符串内部使用了UTF-16编码,所以会使用2个字节存储每个字符。因此一个包含10个字符的字符串可以轻易消耗掉40个字节。

     

    · 普通的集合类,比如HashMap和LinkedList,使用了链式数据结构,对每一个entry对象都有一个包装对象(比如Map.Entry)。这个对象不仅包含对象头,而且还包含了指向列表中下一个对象的引用(通常来说是8个字节)。

     

    · 原始类型的集合通常将原始数据类型使用它们的装箱类型(比如Integer)进行存储。

     

    这个部分会讨论如何确定你的对象的内存使用,以及如何提升它——包含修改你的数据结构,或者使用序列化格式存储数据。我们接着会涵盖Spark内存调优和java垃圾回收的内容。

    3.1 确定内存消耗

    确定你的数据集的内存消耗总量的最佳办法就是,创建一个RDD,将其缓存在内存中,然后查看你的驱动程序的SparkContext日志。日志会告诉你每个分区消耗了多少内存,然后你可以聚合起来计算出RDD的总大小。你可以看见如下的日志信息:

     

    INFO BlockManagerMasterActor: Added rdd_0_1 in memory on mbk.local:50311 (size: 717.5 KB, free: 332.3 MB)

     

    该行日志意味着,RDD 0的分区1消耗了717.5 KB。

    3.2 优化数据结构

    减少内存消耗的第一种方法就是避免会增加内存消耗的java特性,比如基于指针的数据结构(对象)和包装对象。有几种方法来实现它:

     

    1. 在你的数据结构中,优先使用对象和原始数据类型的数组,而不是标准的java集合类(比如HashMap)。fastutil类库为原始数据类型提供了方便的集合类,与java标准类库完全兼容。

    2. 尽可能避免包含大量小对象以及对应的指针的嵌套对象。

    3. 对于key,考虑使用数字类型的ID或者枚举对象,来替代字符串。

    4. 如果你只有小于32G的内存,设置JVM参数-XX:+useCompressedOops,来让指针使用4个字节来替代默认的8个字节。可以在spark-env.sh中增加这些选项。

    3.3 序列化的RDD存储

    如果尽管使用了上述优化技巧,但是你的对象太大了,无法有效地进行存储,一个更简单得减少内存使用的方法就是使用序列化的方式存储它们,在RDD持久化API中使用序列化的存储级别,比如MEMORY_ONLY_SER。Spark会将每个RDD的分区作为一个超大的字节数组进行存储。使用序列化格式进行存储的唯一缺点就是,更慢的访问时间,因为在访问时不得不反序列化每个对象。如果你想使用序列化格式缓存数据,我们强力建议使用Kryo,因为它比java序列化对象可以大量减少空间占用。

    3.4 垃圾回收调优

    JVM垃圾回收也许会成为一个大问题,如果你的程序要存储非常大的RDDs的话。(但是如果只是读取一次RDD,然后对它做很多操作,则这不会成为一个问题)java需要移除旧的对象来为新的对象腾出空间,因此需要追踪所有的java对象,并找出其中不在使用的那些。这里需要记住的一点就是,垃圾回收的消耗是与java对象的数量成正相关的,所以使用包含更少对象的数据结构(比如用int数组替代Integer LinkedList),可以极大地极少这方面的开销。一个更好的方法就是使用序列化格式存储对象,如上所述:现在一个RDD分区仅有一个对象(byte数组)。在尝试其他技巧之前,如果GC是一个问题,则需要尝试的第一个技巧就是使用序列化缓存技术。

     

    GC也可能因为你的任务的工作内存和节点上缓存的RDDs之间的冲突而变为一个问题。我们会讨论如何控制分配给RDD缓存的空间来解决这个问题。

    3.4.1 测量GC的影响

    GC调优的第一个步骤就是收集统计信息——垃圾回收发生的频率以及占用的总时间。这可以通过增加-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps来完成。下次你的spark job运行的时候,你就会看见每次发生垃圾回收时打印在worker日志里的消息。注意这些日志是在你的worker节点上,不在驱动程序中。

    3.4.2 优化缓存大小

    对GC来说,一个重要的配置参数就是用于缓存RDDs的内存量。默认情况下,Spark使用每个executor的内存的60%来缓存RDDs。这意味着只有40%的内存用来容纳任务执行期间创建的对象。

     

    在你的任务执行变慢的情况下,并且你发现你的JVM正在频繁的进行垃圾回收,或者几乎用光所有内存,把这个值降低可以帮助减小内存消耗。如果要改变这个值,比如50%,可以在你的SparkConf上调用conf.set(“spark.storage.memoryFraction”, “0.5”)。结合序列化缓存的使用,使用一个更小的缓存就可以缓和大多数的垃圾回收问题。

    3.4.3 高级GC调优

    为了进一步进行垃圾回收调优,我们首先需要理解JVM中内存管理的一些基本信息:

     

    · Java堆空间被划分成了两个区域,新生代和老年代。新生代用于存储短暂活跃的对象,老年代用于存储长时间存活的对象。

     

    · 新生代进一步划分为了三个区域,Eden,Survivor1,Survivor2.

     

    · 一个简化的垃圾回收过程的描述如下:当Eden区域满了之后,minor GC会在Eden区域和一个Survivor1区域进行,在Eden区域和Survivor1区域中还存活的对象会被拷贝到Survivor2区域中。Survisor区域被交换。如果一个对象年龄足够大,或者拷贝时发现Survivor2区域也满了,则会将该对象移动到老年代。最后,如果老年代满了,就会发生Full GC。

     

    在Spark中GC调优的目的是确保,只有长期存活的对象进入了老年代中,并且新生代是足够大来容纳所有短暂活跃对象的。这可以避免去通过full GC回收任务执行期间创建的临时对象。一些可能有用的操作如下:

     

    · 通过收集GC统计信息来检查是否发生了太多的垃圾回收。如果在一个task完成之前,full GC执行了多次,那么就意味着没有足够的内存来执行任务。

     

    · 在打印出来的GC统计信息中,如果老年代已经接近于满了,减少用于缓存的空间。这可以通过spark.storage.memroyFraction属性来配置。缓存更少的对象比降低任务执行速度更好。

     

    · 如果有很多minor GC,但是没有很多major GC,分配更多的空间给Eden区域。你可以将Eden区域的大小设置为比预估的每个任务要占用的内存更大。如果Eden区域的大小是E,那么你可以使用选项-Xmn=4/3*E来设置年轻代的大小。

     

    · 举例来说,如果你的任务正在从HDFS中读取数据,任务使用的内存总量可以通过使用从HDFS读取出来的数据块的大小来估算。注意,一个解压缩的块的大小通常是块的2-3倍。所以如果我们希望有3个或4个任务的工作空间,并且hdfs的块大小是64MB,我们可以估算Eden区域的大小为4 * 3 * 64 MB。

     

    · 监控随着新的设置,垃圾回收的发生频率以及执行时间。

     

    我们的经验表明,GC调优的影响依赖于你的应用程序以及可用的内存量。有许多调优方法,但是站在一个高的角度来看,管理GC发生的频率可以减少开销。

    4.其他考虑

    4.1 并行级别

    除非你为每个操作设置的并行级别足够高,否则集群是不会被充分使用的。对于每个文件来说,Spark会根据它的大小自动设置map任务的数量。对于分布式的reduce操作,比如groupByKey和reduceByKey,它使用了最大的父RDD的分去数量。你可以传递并行级别作为第二个参数,或者设置spark.default.parallelism。通常我们建议为每个CPU core设置2-3个任务。

    4.2 reduce任务的内存使用

    有时,你遇到一个OutOfMemory异常,不是因为你的RDDs无法在内存中容纳,你的任务的working set,比如groupByKey的reduce任务,太大了。Spark的shuffle操作(sortByKey,groupByKey,reduceByKey,等等)都会在每个任务中构建一个hash table来执行分组,通常hash table是非常大的。最简单的方式就是提高任务的并行度,来让每个任务的输入变小。Spark可以支持200ms的任务,因为它会对多个任务重用一个executor的JVM,因此由非常小的任务启动开销,所以你可以放心提高并行度,哪怕超过了你的应用的CPU core数量。

    4.3 广播大变量

    使用SparkContext提供的广播功能可以大幅度减少每个序列化任务的大小,以及在一个集群上启动一个作业的开销。如果你的任务在内部使用了驱动程序中的大对象,考虑将它变为一个广播变量。Spark会在master上打印每个序列化任务的大小,你可以看它来判断你的任务是不是太大了;通常来说,任务如果超过了20 KB都是值得优化的。

    4.4 数据本地化

    数据本地化会对Spark作业的性能有极大的影响。如果数据和代码都在一起,那么计算是非常快的。但是如果数据和代码是分离的,那么其中之一必须移动到另外一个所在的节点上去。通常来说,将序列化的代码从一个地方移动到另一个地方是比移动一段数据更快的,因为代码的大小比数据的大小更小。Spark针对数据本地化的原则构建了它的调度。

     

    数据本地化是指,数据距离处理它的代码有多近。基于数据当前的位置,有很多种数据本地化级别:

     

    · PROCESS_LOCAL 数据和运行的代码在同一个JVM中。这是最好的本地化。

    · NODE_LOCAL 数据与代码在同一个节点上。例如,数据在同一个节点上的HDFS中,或者在同一个节点的其他executor中。这比PROCESS_LOCAL会稍慢一点,因为需要在进程间移动数据。

    · NO_PREF 数据从哪里获取都是一样快的,没有数据本地化的偏好。

    · RACK_LOCAL 数据与代码在一个机架上。数据在同一个机架的不同服务器上,因此需要通过网络来发送。

    · ANY 数据和代码在网络中的任何地方,并且不在一个机架上。

     

    Spark希望使用最佳的本地化级别来调度所有的任务,但是这是不可能的。在没有未处理的数据在任何空闲的executor中的情况下,Spark会切换到更慢的本地化级别上。有两个选项:a) 等待,直到一个繁忙的CPU空闲下来,可以在有数据的服务器上启动一个任务;b) 立即在任何一个节点上启动任务,然后需要对数据进行移动。

     

    Spark最常见的是等待一下,期望一个CPU空闲下来。但是如果超时以后,它就会开始移动数据到任意一个空闲的CPU上。

  • 分布式序列号生成方案-待完善

    一、概述。

    1、应用场景。

    消息标识。

    订单标识。

    运单标识。

    帖子标识。

    2、核心问题。

    保证全局唯一。

    满足一定规则。

    位数固定,统一前缀,或者后缀。

    趋势有序,时间相关。

    有校验位,防止推断。

    高性能、高可用、吞吐量、易用性。

    二、依赖数据库(MySQL)。

    1、单点单次。

    2、单点批量。

    3、主备批量。

    优点:

    缺点:

    优化:

    三、依赖缓存(Redis/JimDB)。

    INCR、INCRBY、DECR、DECRBY。

    优点:

    缺点:

    优化:

    四、依赖ZooKeeper ZNode。

    优点:

    缺点:

    优化:

    五、UUID及其变种。

    折半UUID。

    优点:

    缺点:

    优化:

    六、snowflake算法。。

    优点:

    缺点:

    优化:

    七、总结。

    自动故障迁移

    一致性哈希

    CPU寄存器

    L1高速缓存

    内存

    磁盘

    外部缓存

  • 解决eclipse hadoop1.2.1 插件 new hadoop location无反应

    用eclipse4.3编译的hadoop1.2.1插件,在点击“new hadoop location”时,会出现没有反应,不出现对话框的情况。
    原因就是eclipse的版本不匹配。

    需要用eclipse4.2编译hadoop插件后,然后安装到eclipse4.3或者eclipse4.2中,就可以正常了

  • hadoop2x-eclipse-plugin编译过程

    1.下载插件:hadoop2x-eclipse-plugin
    2.因为工程目录及插件目录都不一样,因此,先说明一下本地的目录结构:
        eclipse目录: /home/hadoop/software/eclipse
        hadoop2x-eclipse-plugin目录:/home/hadoop/software/hadoop2x-eclipse-plugin/src/contrib/eclipse-plugin/
    3.利用Ant进行编译,前提是java环境、ant环境、eclipse及hadoop2.2都已经配置安装完成:
    [hadoop@localhost ~]$ cd software/hadoop2x-eclipse-plugin/src/contrib/eclipse-plugin/
    [hadoop@localhost eclipse-plugin]$ ant jar -Dversion=2.2.0 -Declipse.home=/home/hadoop/software/eclipse  -Dhadoop.home=/home/hadoop/hadoop
    ant编译时的参数说明:
      -Declipse.home:指eclipse的根路径
      -Dhadoop.home:指hadoop2.2.0的根路径
     
    4.编译
       [jar] Building jar: /home/hadoop/software/hadoop2x-eclipse-plugin/build/contrib/eclipse-plugin/hadoop-eclipse-plugin-2.2.0.jar
    BUILD SUCCESSFUL
    Total time: 5 minutes 43 seconds
    最后,可以在相应的目录下找到已经编译成功的插件jar包
Copyright © 2014-2025 奋奋的愤愤 | 京ICP备14029030号-1