用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 多线程

Runnable/Callable 区别:Runnable无返回值,Callable线程执行完有返回值 Runnable示例 import java.util.concurrent.{Executo...

c主函数参数问题

Spark技术内幕:究竟什么是RDD

RDD是Spark最基本,也是最根本的数据抽象。http://www.cs.berkeley.edu/~matei/papers/2012/nsdi_spark.pdf 是关于RDD的论文。如果觉得英...

Idea+Maven创建scala项目

1.选择Create New Project 2.如下图选择,然后下一步 3.一直Next,填写groupID,artifactid,projectName之后等待IDEA初始化。...

android应用开发-从设计到实现 4-6界面的整体布局

界面的整体布局从这一章节开始,我们终于可以开始使用Android Studio写代码了。天气预报这个应用虽然功能很简单,但是对于从来没有接触过安卓开发的人来讲,开发完成还是需要很多步骤。为了减小学习的...

二叉树中2个节点的最小公共父节点

关于如何求二叉树2个节点的最小公共父节点,分2种情况讨论 情况1:节点有parent节点            这种情况就直接找到节点1和节点2的根节点,然后求得2个链表的长度,再求得长度差。如果说2...

do while

Scala基础—并发编程示例

package com.spark.scala.basics import scala.actors.Actor /** * 1. */ object ActorDemo { def ...

Hive Shell常用操作

1.Hive非交互模式常用命令:   1) hive -e:从命令行执行指定的HQL,不需要分号: % hive -e 'select * from dummy' > a.txt ...

RAMCloud:内存云存储的内存分配机制

闪存速度的确就是现在存储系统的极限吗?现在有需要基于内存的数据库,比如Redis,TimesTen。也不得不提缓存系统的极佳实践memcached。spark也把操作的中间数据全都放入到内存中,避免了...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

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