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

原创 2013年12月04日 10:26:42

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

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

package com.newegg.demo

import scala.concurrent.duration._
import scala.collection.mutable.ListBuffer

object PerfectNumber {
  def sumOfFactors(number: Int) = {
    (1 /: (2 until number)) { (sum, i) => if (number % i == 0) sum + i else sum }
  }

  def isPerfect(num: Int): Boolean = {
    num == sumOfFactors(num)
  }

  def findPerfectNumbers(start: Int, end: Int) = {
    require(start > 1 && end >= start)
    val perfectNumbers = new ListBuffer[Int]
    (start to end).foreach(num => if (isPerfect(num)) perfectNumbers += num)
    perfectNumbers.toList
  }

  def main(args: Array[String]): Unit = {
    val list = findPerfectNumbers(2, 100)
    println("\nFound Perfect Numbers:" + list.mkString(","))
  }
}

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

package com.newegg.demo

import akka.actor.ActorRef

sealed trait Message
case class StartFind(start: Int, end: Int, replyTo: ActorRef) extends Message
case class Work(num: Int, replyTo: ActorRef) extends Message
case class Result(num: Int, isPerfect: Boolean) extends Message
case class PerfectNumbers(list: List[Int]) extends Message
    用面向对象的方式把程序改造一下,把PerfectNumber.scala其中的部分代码抽取到一个单独的Worker.scala文件中:
package com.newegg.demo

import akka.actor.Actor
import akka.actor.ActorRef
class Worker extends Actor {
  private def sumOfFactors(number: Int) = {
    (1 /: (2 until number)) { (sum, i) => if (number % i == 0) sum + i else sum }
  }

  private def isPerfect(num: Int): Boolean = {
    num == sumOfFactors(num)
  }

  def receive = {
    case Work(num: Int, replyTo: ActorRef) =>
      replyTo ! Result(num, isPerfect(num))
      print("[" + num + "] ")
  }
}
一部分代码抽取到Master.scala文件中:

package com.newegg.demo

import scala.collection.mutable.ListBuffer
import akka.actor.Actor
import akka.actor.ActorRef
import akka.actor.Props
import akka.routing.FromConfig
import akka.routing.ConsistentHashingRouter.ConsistentHashableEnvelope

sealed class Helper(count: Int, replyTo: ActorRef) extends Actor {
  val perfectNumbers = new ListBuffer[Int]
  var nrOfResult = 0

  def receive = {
    case Result(num: Int, isPerfect: Boolean) =>
      nrOfResult += 1
      if (isPerfect)
        perfectNumbers += num
      if (nrOfResult == count)
        replyTo ! PerfectNumbers(perfectNumbers.toList)
  }
}

class Master extends Actor {
  val worker = context.actorOf(Props[Worker].withRouter(FromConfig()), "workerRouter")

  def receive = {
    case StartFind(start: Int, end: Int, replyTo: ActorRef) if (start > 1 && end >= start) =>
      val count = end - start + 1
      val helper = context.actorOf(Props(new Helper(count, replyTo)))
      (start to end).foreach(num => worker ! Work(num, helper))
  }
}
    这里用到了一个“可变”的变量nrOfResult,有时候,要完全不用“可变”的变量是相当难以做到的,只要将“可变”的副作用很好地进行“隔离”还是可以的。Scala语言既提倡使用“不变”变量,也容忍使用“可变”变量,既提倡“函数式”编程风格,也兼容面向对象编程,它并不强迫你一开始就完全放弃你所熟悉的编程习惯,我很喜欢这种比较中庸的语言。

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

package com.newegg.demo

import scala.concurrent.duration._
import scala.collection.mutable.ListBuffer
import akka.actor.ActorSystem
import akka.actor.Props
import akka.actor.Actor
import com.typesafe.config.ConfigFactory
import akka.routing.FromConfig

object PerfectNumber {

  def main(args: Array[String]): Unit = {
    val system = ActorSystem("MasterApp", ConfigFactory.load.getConfig("multiThread"))
    system.actorOf(Props(new Actor() {
      val master = context.system.actorOf(Props[Master], "master")
      master ! StartFind(2, 100, self)
      def receive = {
        case PerfectNumbers(list: List[Int]) =>
          println("\nFound Perfect Numbers:" + list.mkString(","))
          system.shutdown()
      }
    }))
  }
}
    程序中用到的配置application.conf文件中的内容如下:

multiThread{
  akka.actor.deployment./master/workerRouter{
    router="round-robin"
    nr-of-instances=10
  }
}
    这样,单线程程序就完全改造成了一个可以充分利用计算机上所有的CPU核的多线程程序,根据计算机的硬件能力只需调整nr-of-instances配置参数就可以调整并发的能力。

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

新建一个MasterApp.scala文件:

package com.newegg.demo

import com.typesafe.config.ConfigFactory
import akka.actor.Actor
import akka.actor.ActorRef
import akka.actor.ActorSelection.toScala
import akka.actor.ActorSystem
import akka.actor.Props
import akka.kernel.Bootable
import akka.cluster.Cluster

class Agent extends Actor {
  var master = context.system.actorSelection("/user/master")

  def receive = {
    case StartFind(start: Int, end: Int, replyTo: ActorRef) if (start > 1 && end >= start) =>
      master ! StartFind(start, end, sender)
  }
}

class MasterDaemon extends Bootable {
  val system = ActorSystem("MasterApp", ConfigFactory.load.getConfig("remote"))
  val master = system.actorOf(Props[Master], "master")

  def startup = {}
  def shutdown = {
    system.shutdown()
  }
}

object MasterApp {
  def main(args: Array[String]) {
    new MasterDaemon()
  }
}
application.conf文件中加入一个remote的section配置块:

akka {
  actor {
    provider = "akka.remote.RemoteActorRefProvider"
    deployment{
      /remoteMaster{
        router="round-robin"
        nr-of-instances=10
        target{
          nodes=[
            "akka.tcp://MasterApp@127.0.0.1:2551",
            "akka.tcp://MasterApp@127.0.0.1:2552"
          ]
        }
      }
      /master/workerRouter{
        router="round-robin"
        nr-of-instances=10
      }
    }
  }

  remote {
    transport = "akka.remote.netty.NettyRemoteTransport"
    netty.tcp {
      hostname = "127.0.0.1"
      port = 2551
    }
  }
}
    在Terminal中运行命令java -cp ".:../../lib/*" com.newegg.demo.MasterApp,可以看一个守护程序正在监听2551端口,修改上述配置端口为2552,在另一个Terminal中运行同样的命令,另一个守护程序正在监听2552端口。
    修改PerfectNumber.scala中的main函数为:

 def main(args: Array[String]): Unit = {
    val system = ActorSystem("MasterApp", ConfigFactory.load.getConfig("remote"))
    system.actorOf(Props(new Actor() {
      val agent = context.system.actorOf(Props(new Agent()).withRouter(FromConfig()), "remoteMaster")
      dispatch

  private def dispatch = {
    val remotePaths = context.system.settings.config.getList("akka.actor.deployment./remoteMaster.target.nodes")
    val count = end - start + 1
    val piece = Math.round(count.toDouble / remotePaths.size()).toInt
    println("%s pieces per node".format(piece))
    var s = start
    while (end >= s) {
      var e = s + piece - 1
      if (e > end)
        e = end
      agent ! StartFind(s, e, self)
      s = e + 1
    }
    println(agent.path)
  }

      def receive = {
        case PerfectNumbers(list: List[Int]) =>
          println("\nFound Perfect Numbers:" + list.mkString(","))
          system.shutdown()
      }
    }))
  }
修改配置端口为2553,在Terminal中运行命令java -cp ".:../../lib/*" com.newegg.demo.PerfectNumber,可以看到两个守护程序中均有Worker在工作,总的计算任务得到了分担。
    这种分布式程序实现起来简单,但是有个缺点:参与分担任务的守护程序的地址必须全部在target.nodes配置中列出,且一旦其中有守护程序宕掉,整体将不能正确地对外服务。
    因此,我们需要有一个有更具扩展能力和高容错能力的集群式应用。

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

akka {
  actor {
    provider = "akka.cluster.ClusterActorRefProvider"
    deployment {
      /master/workerRouter {
        router = "consistent-hashing"
        nr-of-instances = 10
            cluster {
              enabled = on
              max-nr-of-instances-per-node = 3
              allow-local-routees = on
            }
          }
        }
  }
  remote {
    log-remote-lifecycle-events = off
    netty.tcp {
      hostname = "127.0.0.1"
      port = 2551
    }
  }
  cluster {
    min-nr-of-members = 2
    seed-nodes = [
      "akka.tcp://MasterApp@127.0.0.1:2551",
      "akka.tcp://MasterApp@127.0.0.1:2552"]

    auto-down=on
  }
}
注意其中有两个seed-nodes,是集群的“首脑”,某节点会加入的是哪个集群,正是因为参照这个seed-nodes来的。
    这里采用的是consistent-hashing Router,集群节点之间有心跳检测,集群实现中内部采用的是与Cassandra一样的Gossip协议,用一致性哈希来维护集群节点“环”。
    改造Master.scala文件中其中一行代码,将

worker ! Work(num, helper)
改为:

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

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

package com.newegg.demo

import scala.concurrent.forkjoin.ThreadLocalRandom

import akka.actor.Actor
import akka.actor.ActorRef
import akka.actor.ActorSelection.toScala
import akka.actor.Address
import akka.actor.RelativeActorPath
import akka.actor.RootActorPath
import akka.cluster.Cluster
import akka.cluster.ClusterEvent.CurrentClusterState
import akka.cluster.ClusterEvent.MemberEvent
import akka.cluster.ClusterEvent.MemberRemoved
import akka.cluster.ClusterEvent.MemberUp
import akka.cluster.MemberStatus

class ClusterClient extends Actor {
  val cluster = Cluster(context.system)
  override def preStart(): Unit = cluster.subscribe(self, classOf[MemberEvent])
  override def postStop(): Unit = cluster unsubscribe self

  var nodes = Set.empty[Address]

  val servicePath = "/user/agent"
  val servicePathElements = servicePath match {
    case RelativeActorPath(elements) => elements
    case _ => throw new IllegalArgumentException(
      "servicePath [%s] is not a valid relative actor path" format servicePath)
  }

  def receive = {
    case state: CurrentClusterState =>
      nodes = state.members.collect {
        case m if m.status == MemberStatus.Up => m.address
      }
    case MemberUp(member) =>
      nodes += member.address
    case MemberRemoved(member, _) =>
      nodes -= member.address
    case _: MemberEvent => // ignore
    case PerfectNumbers(list: List[Int]) =>
      println("\nFound Perfect Numbers:" + list.mkString(","))
      cluster.down(self.path.address)
      context.system.shutdown()
    case StartFind(start: Int, end: Int, resultTo: ActorRef) =>
      println("node size:" + nodes.size)
      nodes.size match {
        case x: Int if x < 1 =>
          Thread.sleep(1000)
          self ! StartFind(start, end, resultTo)
        case _ =>
          val address = nodes.toIndexedSeq(ThreadLocalRandom.current.nextInt(nodes.size))
          val service = context.actorSelection(RootActorPath(address) / servicePathElements)
          service ! StartFind(start, end, resultTo)
          println("send to :" + address)
      }
  }
}
上面的代码是比较固定的写法,此Actor的作用是:加入集群,订阅集群中有关节点增减变化的消息,维护一个集群中存活节点的地址列表,将任务消息发到集群节点中。

    改造上述main函数如下:

 val system = ActorSystem("MasterApp", ConfigFactory.load.getConfig("cluster"))
    system.actorOf(Props(new Actor() {
      context.system.actorOf(Props[ClusterClient], "remoteMaster") ! StartFind(2, 100, self)
…...

运行此集群客户端程序,可以看到客户端也join到集群中,集群中所有的节点都在分担计算任务,任意增减集群节点数目都是如此。











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

Akka framework现在已经是Scala语言的一部分了,用它编写分布式程序是相当简单的,本文将一步一步地讲解如何做到scale up & scale out。 简单的单线程程序     先...
  • pengyanhong
  • pengyanhong
  • 2013年12月04日 10:26
  • 6949

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

原文:http://blog.csdn.net/pengyanhong/article/details/17112177 Akka framework现在已经是Scala语言的一部分...
  • shenlanzifa
  • shenlanzifa
  • 2015年01月30日 10:36
  • 1140

scala 并行集合在spark中的应用

一.scala并行集合现在有一个集合,对它的每个元素进行处理,比如: val arr = List[String]("a","b","c") arr.foreach(println(_)...
  • lsshlsw
  • lsshlsw
  • 2015年11月12日 00:40
  • 2392

Akka Scala编程实践,轻松开发多线程、分布式以及集群式程序

Akka基础 参照: http://www.importnew.com/16479.html  Akka笔记之Actor简介     Akka中的Actor遵循Actor模型。你可以...
  • ZYC88888
  • ZYC88888
  • 2017年04月19日 23:56
  • 1104

Hadoop伪分布式与集群式安装配置

源地址:Hadoop伪分布式与集群安装 目录: 1、配置前的说明 2、环境说明 3、修改主机名 4、SSH无密码验证配置 5、创建Hadoop用户 6、生成SSH密钥 7、JDK配置 8、Hadoop...
  • SONGCHUNHONG
  • SONGCHUNHONG
  • 2015年07月19日 10:34
  • 718

Hadoop伪分布式与集群式安装配置

配置环境                           配置前的说明 部署伪分布式与集群式Hadoop的绝大部分操作都是一样的,细节上区别在于集群式是在两台机子上部署的,两台机子都要执行下列操作...
  • SprintfWater
  • SprintfWater
  • 2013年11月25日 14:19
  • 785

Java 技术: 使您轻松地进行多线程应用程序编程——Consumer 类可以简化生产者-消费者行为的实现

生产者-消费者方案是多线程应用程序开发中最常用的构造之一 ― 因此困难也在于此。因为在一个应用程序中可以多次重复生产者-消费者行为,其代码也可以如此。软件开发人员 Ze'ev Bubis 和 Saff...
  • novelly
  • novelly
  • 2013年10月07日 22:09
  • 678

Java 技术: 使您轻松地进行多线程应用程序编程(生产者消费者模式的优化)

多线程应用程序通常利用生产者-消费者编程方案,其中由生产者线程创建重复性作业,将其传递给作业队列,然后由消费者线程处理作业。虽然这种编程方法很有用,但是它通常导致重复的代码,这对于调试和维护可能是真正...
  • dac55300424
  • dac55300424
  • 2013年07月29日 17:46
  • 532

Struts2轻松实现多文件上传(自定义多线程加速程序效率)

一、大家都知道用Struts2框架上传单个文件非常的简单,其实多文件上传也一样,只不过是更改一下表单和Action代码而已,基本配置不在展示。 关键就是在Action中,针对的File必须写成数...
  • shaopeng5211
  • shaopeng5211
  • 2013年03月22日 16:30
  • 880

集群式游戏服务器架构方案设计开发

自从2003年开发VOIP Radius Server以及修改Gnugk以来,从事服务器开发已经近五年了,对服务器开发也有一些自己独到的看法以及见解。当摆脱了技术本身的束缚之后,才理解重要的并不是某种...
  • u011676589
  • u011676589
  • 2015年01月03日 18:22
  • 1182
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:用Scala语言轻松开发多线程、分布式以及集群式程序
举报原因:
原因补充:

(最多只允许输入30个字)