用Scala语言轻松开发多线程、分布式以及集群式程序

原文:http://blog.csdn.net/pengyanhong/article/details/17112177


Akka framework现在已经是Scala语言的一部分了,用它编写分布式程序是相当简单的,本文将一步一步地讲解如何做到scale up & scale out。

  • 简单的单线程程序
    先从一个简单的单线程程序PerfectNumber.scala开始,这个程序是找出2到100范围内所有的“完美数”(真约数之和恰好等于此数自身)

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. package com.newegg.demo  
  2.   
  3. import scala.concurrent.duration._  
  4. import scala.collection.mutable.ListBuffer  
  5.   
  6. object PerfectNumber {  
  7.   def sumOfFactors(number: Int) = {  
  8.     (1 /: (2 until number)) { (sum, i) => if (number % i == 0) sum + i else sum }  
  9.   }  
  10.   
  11.   def isPerfect(num: Int): Boolean = {  
  12.     num == sumOfFactors(num)  
  13.   }  
  14.   
  15.   def findPerfectNumbers(start: Int, end: Int) = {  
  16.     require(start > 1 && end >= start)  
  17.     val perfectNumbers = new ListBuffer[Int]  
  18.     (start to end).foreach(num => if (isPerfect(num)) perfectNumbers += num)  
  19.     perfectNumbers.toList  
  20.   }  
  21.   
  22.   def main(args: Array[String]): Unit = {  
  23.     val list = findPerfectNumbers(2100)  
  24.     println("\nFound Perfect Numbers:" + list.mkString(","))  
  25.   }  
  26. }  

  • 多线程程序
    Scala编写并发程序的基础是Actor模型,与Actor交互的唯一途径是“消息传递”,你根本不用考虑“进程”,“线程”,“同步”,“锁”等等一些冷冰冰的概念,你可以把Actor看做是一个“人”,你的程序是一个“组织”内的一群“人”之间以“消息传递”的方式在协作。
    这个示例中要用到的“消息”定义在Data.scala文件中,内容如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. package com.newegg.demo  
  2.   
  3. import akka.actor.ActorRef  
  4.   
  5. sealed trait Message  
  6. case class StartFind(start: Int, end: Int, replyTo: ActorRef) extends Message  
  7. case class Work(num: Int, replyTo: ActorRef) extends Message  
  8. case class Result(num: Int, isPerfect: Boolean) extends Message  
  9. case class PerfectNumbers(list: List[Int]) extends Message  
    用面向对象的方式把程序改造一下,把PerfectNumber.scala其中的部分代码抽取到一个单独的Worker.scala文件中:
[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. package com.newegg.demo  
  2.   
  3. import akka.actor.Actor  
  4. import akka.actor.ActorRef  
  5. class Worker extends Actor {  
  6.   private def sumOfFactors(number: Int) = {  
  7.     (1 /: (2 until number)) { (sum, i) => if (number % i == 0) sum + i else sum }  
  8.   }  
  9.   
  10.   private def isPerfect(num: Int): Boolean = {  
  11.     num == sumOfFactors(num)  
  12.   }  
  13.   
  14.   def receive = {  
  15.     case Work(num: Int, replyTo: ActorRef) =>  
  16.       replyTo ! Result(num, isPerfect(num))  
  17.       print("[" + num + "] ")  
  18.   }  
  19. }  
一部分代码抽取到Master.scala文件中:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. package com.newegg.demo  
  2.   
  3. import scala.collection.mutable.ListBuffer  
  4. import akka.actor.Actor  
  5. import akka.actor.ActorRef  
  6. import akka.actor.Props  
  7. import akka.routing.FromConfig  
  8. import akka.routing.ConsistentHashingRouter.ConsistentHashableEnvelope  
  9.   
  10. sealed class Helper(count: Int, replyTo: ActorRef) extends Actor {  
  11.   val perfectNumbers = new ListBuffer[Int]  
  12.   var nrOfResult = 0  
  13.   
  14.   def receive = {  
  15.     case Result(num: Int, isPerfect: Boolean) =>  
  16.       nrOfResult += 1  
  17.       if (isPerfect)  
  18.         perfectNumbers += num  
  19.       if (nrOfResult == count)  
  20.         replyTo ! PerfectNumbers(perfectNumbers.toList)  
  21.   }  
  22. }  
  23.   
  24. class Master extends Actor {  
  25.   val worker = context.actorOf(Props[Worker].withRouter(FromConfig()), "workerRouter")  
  26.   
  27.   def receive = {  
  28.     case StartFind(start: Int, end: Int, replyTo: ActorRef) if (start > 1 && end >= start) =>  
  29.       val count = end - start + 1  
  30.       val helper = context.actorOf(Props(new Helper(count, replyTo)))  
  31.       (start to end).foreach(num => worker ! Work(num, helper))  
  32.   }  
  33. }  
    这里用到了一个“可变”的变量nrOfResult,有时候,要完全不用“可变”的变量是相当难以做到的,只要将“可变”的副作用很好地进行“隔离”还是可以的。Scala语言既提倡使用“不变”变量,也容忍使用“可变”变量,既提倡“函数式”编程风格,也兼容面向对象编程,它并不强迫你一开始就完全放弃你所熟悉的编程习惯,我很喜欢这种比较中庸的语言。

    那个单线程程序的主函数改造如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. package com.newegg.demo  
  2.   
  3. import scala.concurrent.duration._  
  4. import scala.collection.mutable.ListBuffer  
  5. import akka.actor.ActorSystem  
  6. import akka.actor.Props  
  7. import akka.actor.Actor  
  8. import com.typesafe.config.ConfigFactory  
  9. import akka.routing.FromConfig  
  10.   
  11. object PerfectNumber {  
  12.   
  13.   def main(args: Array[String]): Unit = {  
  14.     val system = ActorSystem("MasterApp", ConfigFactory.load.getConfig("multiThread"))  
  15.     system.actorOf(Props(new Actor() {  
  16.       val master = context.system.actorOf(Props[Master], "master")  
  17.       master ! StartFind(2100, self)  
  18.       def receive = {  
  19.         case PerfectNumbers(list: List[Int]) =>  
  20.           println("\nFound Perfect Numbers:" + list.mkString(","))  
  21.           system.shutdown()  
  22.       }  
  23.     }))  
  24.   }  
  25. }  
    程序中用到的配置application.conf文件中的内容如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. multiThread{  
  2.   akka.actor.deployment./master/workerRouter{  
  3.     router="round-robin"  
  4.     nr-of-instances=10  
  5.   }  
  6. }  
    这样,单线程程序就完全改造成了一个可以充分利用计算机上所有的CPU核的多线程程序,根据计算机的硬件能力只需调整nr-of-instances配置参数就可以调整并发的能力。

  • 分布式程序
   现在,我们进一步改造,把它变成一个可以跨JVM,或者说跨计算机运行的分布式程序。

新建一个MasterApp.scala文件:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. package com.newegg.demo  
  2.   
  3. import com.typesafe.config.ConfigFactory  
  4. import akka.actor.Actor  
  5. import akka.actor.ActorRef  
  6. import akka.actor.ActorSelection.toScala  
  7. import akka.actor.ActorSystem  
  8. import akka.actor.Props  
  9. import akka.kernel.Bootable  
  10. import akka.cluster.Cluster  
  11.   
  12. class Agent extends Actor {  
  13.   var master = context.system.actorSelection("/user/master")  
  14.   
  15.   def receive = {  
  16.     case StartFind(start: Int, end: Int, replyTo: ActorRef) if (start > 1 && end >= start) =>  
  17.       master ! StartFind(start, end, sender)  
  18.   }  
  19. }  
  20.   
  21. class MasterDaemon extends Bootable {  
  22.   val system = ActorSystem("MasterApp", ConfigFactory.load.getConfig("remote"))  
  23.   val master = system.actorOf(Props[Master], "master")  
  24.   
  25.   def startup = {}  
  26.   def shutdown = {  
  27.     system.shutdown()  
  28.   }  
  29. }  
  30.   
  31. object MasterApp {  
  32.   def main(args: Array[String]) {  
  33.     new MasterDaemon()  
  34.   }  
  35. }  
application.conf文件中加入一个remote的section配置块:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. akka {  
  2.   actor {  
  3.     provider = "akka.remote.RemoteActorRefProvider"  
  4.     deployment{  
  5.       /remoteMaster{  
  6.         router="round-robin"  
  7.         nr-of-instances=10  
  8.         target{  
  9.           nodes=[  
  10.             "akka.tcp://MasterApp@127.0.0.1:2551",  
  11.             "akka.tcp://MasterApp@127.0.0.1:2552"  
  12.           ]  
  13.         }  
  14.       }  
  15.       /master/workerRouter{  
  16.         router="round-robin"  
  17.         nr-of-instances=10  
  18.       }  
  19.     }  
  20.   }  
  21.   
  22.   remote {  
  23.     transport = "akka.remote.netty.NettyRemoteTransport"  
  24.     netty.tcp {  
  25.       hostname = "127.0.0.1"  
  26.       port = 2551  
  27.     }  
  28.   }  
  29. }  
    在Terminal中运行命令java -cp ".:../../lib/*" com.newegg.demo.MasterApp,可以看一个守护程序正在监听2551端口,修改上述配置端口为2552,在另一个Terminal中运行同样的命令,另一个守护程序正在监听2552端口。
    修改PerfectNumber.scala中的main函数为:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. def main(args: Array[String]): Unit = {  
  2.    val system = ActorSystem("MasterApp", ConfigFactory.load.getConfig("remote"))  
  3.    system.actorOf(Props(new Actor() {  
  4.      val agent = context.system.actorOf(Props(new Agent()).withRouter(FromConfig()), "remoteMaster")  
  5.      dispatch  
  6.   
  7.  private def dispatch = {  
  8.    val remotePaths = context.system.settings.config.getList("akka.actor.deployment./remoteMaster.target.nodes")  
  9.    val count = end - start + 1  
  10.    val piece = Math.round(count.toDouble / remotePaths.size()).toInt  
  11.    println("%s pieces per node".format(piece))  
  12.    var s = start  
  13.    while (end >= s) {  
  14.      var e = s + piece - 1  
  15.      if (e > end)  
  16.        e = end  
  17.      agent ! StartFind(s, e, self)  
  18.      s = e + 1  
  19.    }  
  20.    println(agent.path)  
  21.  }  
  22.   
  23.      def receive = {  
  24.        case PerfectNumbers(list: List[Int]) =>  
  25.          println("\nFound Perfect Numbers:" + list.mkString(","))  
  26.          system.shutdown()  
  27.      }  
  28.    }))  
  29.  }  
修改配置端口为2553,在Terminal中运行命令java -cp ".:../../lib/*" com.newegg.demo.PerfectNumber,可以看到两个守护程序中均有Worker在工作,总的计算任务得到了分担。
    这种分布式程序实现起来简单,但是有个缺点:参与分担任务的守护程序的地址必须全部在target.nodes配置中列出,且一旦其中有守护程序宕掉,整体将不能正确地对外服务。
    因此,我们需要有一个有更具扩展能力和高容错能力的集群式应用。

  • 集群式应用
    在application.conf文件中新加一个cluser配置区块:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. akka {  
  2.   actor {  
  3.     provider = "akka.cluster.ClusterActorRefProvider"  
  4.     deployment {  
  5.       /master/workerRouter {  
  6.         router = "consistent-hashing"  
  7.         nr-of-instances = 10  
  8.             cluster {  
  9.               enabled = on  
  10.               max-nr-of-instances-per-node = 3  
  11.               allow-local-routees = on  
  12.             }  
  13.           }  
  14.         }  
  15.   }  
  16.   remote {  
  17.     log-remote-lifecycle-events = off  
  18.     netty.tcp {  
  19.       hostname = "127.0.0.1"  
  20.       port = 2551  
  21.     }  
  22.   }  
  23.   cluster {  
  24.     min-nr-of-members = 2  
  25.     seed-nodes = [  
  26.       "akka.tcp://MasterApp@127.0.0.1:2551",  
  27.       "akka.tcp://MasterApp@127.0.0.1:2552"]  
  28.   
  29.     auto-down=on  
  30.   }  
  31. }  
注意其中有两个seed-nodes,是集群的“首脑”,某节点会加入的是哪个集群,正是因为参照这个seed-nodes来的。
    这里采用的是consistent-hashing Router,集群节点之间有心跳检测,集群实现中内部采用的是与Cassandra一样的Gossip协议,用一致性哈希来维护集群节点“环”。
    改造Master.scala文件中其中一行代码,将

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. worker ! Work(num, helper)  
改为:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. worker.tell(ConsistentHashableEnvelope(Work(num,helper), num), helper)  
这样,发送到集群中的消息支持一致性哈希,在整个集群节点中分散任务。 
    改造上面的MasterApp.scala,在其中代码行val master = system.actorOf(Props[Master], "master")下面增加两行代码:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. val agent = system.actorOf(Props(new Agent), "agent")  
  2.  Cluster(system).registerOnMemberUp(agent)  
将分布式的守护程序改成了集群式的守护程序(其实这段代码没必要放到Bootable类中),以上述同样的方式运行MasterApp,以不同的端口,跑起任意个守护程序,它们均会join到同一集群中,只要有一个seed-nodes存在,集群就能正常对外提供服务。
    新建一个ClusterClient.scala文件,内容如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. package com.newegg.demo  
  2.   
  3. import scala.concurrent.forkjoin.ThreadLocalRandom  
  4.   
  5. import akka.actor.Actor  
  6. import akka.actor.ActorRef  
  7. import akka.actor.ActorSelection.toScala  
  8. import akka.actor.Address  
  9. import akka.actor.RelativeActorPath  
  10. import akka.actor.RootActorPath  
  11. import akka.cluster.Cluster  
  12. import akka.cluster.ClusterEvent.CurrentClusterState  
  13. import akka.cluster.ClusterEvent.MemberEvent  
  14. import akka.cluster.ClusterEvent.MemberRemoved  
  15. import akka.cluster.ClusterEvent.MemberUp  
  16. import akka.cluster.MemberStatus  
  17.   
  18. class ClusterClient extends Actor {  
  19.   val cluster = Cluster(context.system)  
  20.   override def preStart(): Unit = cluster.subscribe(self, classOf[MemberEvent])  
  21.   override def postStop(): Unit = cluster unsubscribe self  
  22.   
  23.   var nodes = Set.empty[Address]  
  24.   
  25.   val servicePath = "/user/agent"  
  26.   val servicePathElements = servicePath match {  
  27.     case RelativeActorPath(elements) => elements  
  28.     case _ => throw new IllegalArgumentException(  
  29.       "servicePath [%s] is not a valid relative actor path" format servicePath)  
  30.   }  
  31.   
  32.   def receive = {  
  33.     case state: CurrentClusterState =>  
  34.       nodes = state.members.collect {  
  35.         case m if m.status == MemberStatus.Up => m.address  
  36.       }  
  37.     case MemberUp(member) =>  
  38.       nodes += member.address  
  39.     case MemberRemoved(member, _) =>  
  40.       nodes -= member.address  
  41.     case _: MemberEvent => // ignore  
  42.     case PerfectNumbers(list: List[Int]) =>  
  43.       println("\nFound Perfect Numbers:" + list.mkString(","))  
  44.       cluster.down(self.path.address)  
  45.       context.system.shutdown()  
  46.     case StartFind(start: Int, end: Int, resultTo: ActorRef) =>  
  47.       println("node size:" + nodes.size)  
  48.       nodes.size match {  
  49.         case x: Int if x < 1 =>  
  50.           Thread.sleep(1000)  
  51.           self ! StartFind(start, end, resultTo)  
  52.         case _ =>  
  53.           val address = nodes.toIndexedSeq(ThreadLocalRandom.current.nextInt(nodes.size))  
  54.           val service = context.actorSelection(RootActorPath(address) / servicePathElements)  
  55.           service ! StartFind(start, end, resultTo)  
  56.           println("send to :" + address)  
  57.       }  
  58.   }  
  59. }  
上面的代码是比较固定的写法,此Actor的作用是:加入集群,订阅集群中有关节点增减变化的消息,维护一个集群中存活节点的地址列表,将任务消息发到集群节点中。

    改造上述main函数如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. val system = ActorSystem("MasterApp", ConfigFactory.load.getConfig("cluster"))  
  2.    system.actorOf(Props(new Actor() {  
  3.      context.system.actorOf(Props[ClusterClient], "remoteMaster") ! StartFind(2100, self)  
  4. ...  

运行此集群客户端程序,可以看到客户端也join到集群中,集群中所有的节点都在分担计算任务,任意增减集群节点数目都是如此。
posted on 2015-01-30 10:36  shenlanzifa  阅读(846)  评论(0编辑  收藏  举报