java akka_Akka系列(九):Akka分布式之Akka Remote
Akka作為一個天生用于構建分布式應用的工具,當然提供了用于分布式組件即Akka Remote,那么我們就來看看如何用Akka Remote以及Akka Serialization來構建分布式應用。
背景
很多同學在程序的開發中都會遇到一個問題,當業務需求變得越來越復雜,單機服務器已經不足以承載相應的請求的時候,我們都會考慮將服務部署到不同的服務器上,但服務器之間可能需要相互調用,那么系統必須擁有相互通信的接口,用于相應的數據交互,這時候一個好的遠程調用方案是一個絕對的利器,主流的遠程通信有以下幾種選擇:
RPC(Remote Procedure Call Protocol)
Web Service
RMI (Remote Method Invocation)
JMS(Java Messaging Service)
這幾種方式都是被采用比較廣泛的通信方案,有興趣的同學可以自己去了解一下,這里我會講一下RMI和JMS。
JAVA遠程調用
RMI和JMS相信很多寫過Java程序的同學都知道,是Java程序用來遠程通信的主要方式,那么RMI和JMS又有什么區別呢?
1.RMI
i.特征:
同步通信:在使用RMI調用遠程方法時,線程會持續等待直到結果返回,所以它是一個同步阻塞操作;
強耦合:請求的系統中需要使用的RMI服務進行接口聲明,返回的數據類型有一定的約束;
ii.優點:
實現相對簡單,方法調用形式通俗易理解,接口聲明服務功能清晰。
iii.缺點:
只局限支持JVM平臺;
對無法兼容Java語言的其他語言也不適用;
2.JMS
i.特征:
異步通信:JMS發送消息進行通信,在通信過程中,線程不會被阻塞,不必等待請求回應,所以是一個異步操作;
松耦合:不需要接口聲明,返回的數據類型可以是各種各樣,比如JSON,XML等;
ii.通信方式:
(1)點對點消息傳送模型
顧名思義,點對點可以理解為兩個服務器的定點通信,發送者和接收者都能明確知道對方是誰,大致模型如下:
(2)發布/訂閱消息傳遞模型
點對點模型有些場景并不是很適用,比如有一臺主服務器,它產生一條消息需要讓所有的從服務器都能收到,若采用點對點模型的話,那主服務器需要循環發送消息,后續若有新的從服務器增加,還要改主服務器的配置,這樣就會導致不必要的麻煩,那么發布/訂閱模型是怎么樣的呢?其實這種模式跟設計模式中的觀察者模式很相似,相信很多同學都很熟悉,它最大的特點就是較松耦合,易擴展等特點,所以發布/訂閱模型的大致結構如下:
iii.優點:
由于使用異步通信,不需要線程暫停等待,性能相對較高。
iiii.缺點:
技術實現相對復雜,并需要維護相關的消息隊列;
更通俗的說:
RMI可以看成是用打電話的方式進行信息交流,而JMS更像是發短信。
總的來說兩種方式沒有孰優孰劣,我們也不用比較到底哪種方式比較好,存在即合理,更重要的是哪種選擇可能更適合你的系統。
Akka Remote
上面講到JAVA中遠程通信的方式,但我們之前說過Akka也是基于JVM平臺的,那么它的通信方式又有什么不同呢?
在我看來,Akka的遠程通信方式更像是RMI和JMS的結合,但更偏向于JMS的方式,為什么這么說呢,我們先來看一個示例:
我們先來創建一個遠程的Actor:
class RemoteActor extends Actor {
def receive = {
case msg: String =>
println(s"RemoteActor received message '$msg'")
sender ! "Hello from the RemoteActor"
}
}
現在我們在遠程服務器上啟動這個Actor:
val system = ActorSystem("RemoteDemoSystem")
val remoteActor = system.actorOf(Props[RemoteActor], name = "RemoteActor")
那么現在我們假如有一個系統需要向這個Actor發送消息應該怎么做呢?
首先我們需要類似RMI發布自己的服務一樣,我們需要為其他系統調用遠程Actor提供消息通信的接口,在Akka中,設置非常簡單,不需要代碼侵入,只需簡單的在配置文件里配置即可:
akka {
actor {
provider = "akka.remote.RemoteActorRefProvider"
}
remote {
enabled-transports = ["akka.remote.netty.tcp"]
netty.tcp {
hostname = $localIp //比如127.0.0.1
port = $port //比如2552
}
log-sent-messages = on
log-received-messages = on
}
}
我們只需配置相應的驅動,傳輸方式,ip,端口等屬性就可簡單完成Akka Remote的配置。
當然本地服務器也需要配置這些信息,因為Akka之間是需要相互通信的,當然配置除了hostname有一定的區別外,其他配置信息可一致,本例子是在同一臺機器上,所以這里hostname是相同的。
這時候我們就可以在本地的服務器向這個Actor發送消息了,首先我們可以創建一個本地的Actor:
case object Init
case object SendNoReturn
class LocalActor extends Actor{
val path = ConfigFactory.defaultApplication().getString("remote.actor.name.test")
implicit val timeout = Timeout(4.seconds)
val remoteActor = context.actorSelection(path)
def receive: Receive = {
case Init => "init local actor"
case SendNoReturn => remoteActor ! "hello remote actor"
}
}
其中的remote.actor.name.test的值為:“akka.tcp://RemoteDemoSystem@127.0.0.1:4444/user/RemoteActor”,另外我們可以看到我們使用了context.actorSelection(path)來獲取的是一個ActorSelection對象,若是需要獲得ActorRef,我們可以調用它的resolveOne(),它返回的是是一個Future[ActorRef],這里是不是很熟悉,因為它跟本地獲取Actor方式是一樣的,因為Akka中Actor是位置透明的,獲取本地Actor和遠程Actor是一樣的。
最后我們首先啟動遠程Actor的系統:
object RemoteDemo extends App {
val system = ActorSystem("RemoteDemoSystem")
val remoteActor = system.actorOf(Props[RemoteActor], name = "RemoteActor")
remoteActor ! "The RemoteActor is alive"
}
然后我們在本地系統中啟動這個LocalActor,并向它發送消息:
object LocalDemo extends App {
implicit val system = ActorSystem("LocalDemoSystem")
val localActor = system.actorOf(Props[LocalActor], name = "LocalActor")
localActor ! Init
localActor ! SendNoReturn
}
我們可以看到RemoteActor收到了一條消息:
從以上的步驟和結果看出可以看出,Akka的遠程通信跟JMS的點對點模式似乎更相似一點,但是它有不需要我們維護消息隊列,而是使用Actor自身的郵箱,另外我們利用context.actorSelection獲取的ActorRef,可以看成遠程Actor的副本,這個又和RMI相關概念類似,所以說Akka遠程通信的形式上像是RMI和JMS的結合,當然底層還是通過TCP、UDP等相關網絡協議進行數據傳輸的,從配置文件的相應內容便可以看出。
上述例子演示的是sendNoReturn的模式,那么假如我們需要遠程Actor給我們一個回復應該怎么做呢?
首先我們創建一個消息:
case object SendHasReturn
def receive: Receive = {
case SendHasReturn =>
for {
r
} yield r
}
我們重新運行LocalActor并像RemoteActor發送一條消息:
可以看到LocalActor在發送消息后并收到了RemoteActor返回來的消息,另外我們這里設置了超時時間,若在規定的時間內沒有得到反饋,程序就會報錯。
Akka Serialization
其實這一部分本可以單獨拿出來寫,但是相信序列化這塊大家都應該有所了解了,所以就不準備講太多序列化的知識了,怕班門弄斧,主要講講Akka中的序列化。
繼續上面的例子,假如我們這時向RemoteActor發送一個自定義的對象,比如一個case class對象,但是我們這是是在網絡中傳輸這個消息,那么怎么保證這個對象類型和值呢,在同一個JVM系統中我們不需要擔心這個,因為對象就在堆中,我們只要傳遞相應的地址即可就行,但是在不同的環境中,我們并不能這么做,我們在網絡中只能傳輸字節數據,所以我們必須將對象做特殊的處理,在傳輸的時候轉化成特定的由一連串字節組成的數據,而且我們又可以根據這些數據恢復成一個相應的對象,這便是序列化。
我們先定義一個參與的case class, 并修改一下上面發送消息的語句:
case object SendSerialization
case class JoinEvt(
id: Long,
name: String
)
def receive: Receive = {
case SendSerialization =>
for {
r
} yield println(r)
}
這時我們重新啟動RemoteActor和LocalActor所在的系統,發送這條消息:
有同學可能會覺得奇怪,我們明明沒有對JoinEvt進行過任何序列化的標識和處理,為什么程序還能運行成功呢?
其實不然,只不過是有人替我們默認做了,不用說,肯定是貼心的Akka,它為我們提供了一個默認的序列化策略,那就是我們熟悉又糾結的java.io.Serializable,沉浸在它的易使用性上,又對它的性能深惡痛絕,尤其是當有大量對象需要傳輸的分布式系統,如果是小系統,當我沒說,畢竟存在即合理。
又有同學說,既然Akka是一個天生分布式組件,為什么還用低效的java.io.Serializable,你問我我也不知道,可能當時的作者偷了偷懶,當然Akka現在可能覺醒了,首先它支持第三方的序列化工具,當然如果你有特殊需求,你也可以自己實現一個,而且在最新的文檔中說明,在Akka 2.5x之后Akka內核消息全面廢棄java.io.Serializable,用戶自定義的消息暫時還是支持使用java.io.Serializable的,但是不推薦用,因為它是低效的,容易被攻擊,所以在這里我也推薦大家再Akka中盡量不要在使用了java.io.Serializable。
那么在Akka中我們如何使用第三方的序列化工具呢?
這里我推薦一個在Java社區已經久負盛名的序列化工具:kryo,有興趣的同學可以去了解一下:kryo,而且它也提供Akka使用的相關包,這里我們就使用它作為示例:
這里我貼上整個項目的build.sbt, kryo的相關依賴也在里面:
import sbt._
import sbt.Keys._
lazy val AllLibraryDependencies =
Seq(
"com.typesafe.akka" %% "akka-actor" % "2.5.3",
"com.typesafe.akka" %% "akka-remote" % "2.5.3",
"com.twitter" %% "chill-akka" % "0.8.4"
)
lazy val commonSettings = Seq(
name := "AkkaRemoting",
version := "1.0",
scalaVersion := "2.11.11",
libraryDependencies := AllLibraryDependencies
)
lazy val remote = (project in file("remote"))
.settings(commonSettings: _*)
.settings(
// other settings
)
lazy val local = (project in file("local"))
.settings(commonSettings: _*)
.settings(
// other settings
)
然后我們只需將application.conf中的actor配置替換成以下的內容:
actor {
provider = "akka.remote.RemoteActorRefProvider"
serializers {
kryo = "com.twitter.chill.akka.AkkaSerializer"
}
serialization-bindings {
"java.io.Serializable" = none
"scala.Product" = kryo
}
}
其實其中的"java.io.Serializable" = none可以省略,因為若是有其他序列化的策略則會替換掉默認的java.io.Serializable的策略,這里只是為了更加仔細的說明。
至此我們就可以使用kryo了,整個過程是不是很easy,迫不及待開始寫demo了,那就快快開始吧。
整個例子的相關的源碼已經上傳到akka-demo中:源碼鏈接
總結
以上是生活随笔為你收集整理的java akka_Akka系列(九):Akka分布式之Akka Remote的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 姜皮的功效与作用、禁忌和食用方法
- 下一篇: java乌龟_java用swing画可以