複雜業務需求下,我們爲什麼選擇Akka作爲異步通信框架?

Akka是Scala語言實現的一套基於Actor模型的異步通信框架,可用於構建高併發、分佈式、可容錯、事件驅動的基於JVM的應用,在Spark中曾被用於實現進程、節點間通信,在實際項目中協助我們成功搭建了滿足業務需求的模型部署平臺。

項目背景

某國內大型連鎖餐飲企業旗下擁有大量門店。餐廳門店的每日生產、訂貨、排班都依賴於每日客單量預估的合理性,其內部數據團隊實現了一套預估模型,需要TalkingData幫助構建一個工程化平臺以支撐模型的訓練和部署,從而將模型真正地應用到實際生產環節中。

經過交流,我們發現在實際生產環境中,在各方面存在一些問題:

  • 異步:所有門店的前日銷售、業務等數據均由各自門店的店長負責整合上傳。上傳的開始時間、結束時間、數據的完整性等均不確定。而模型訓練和預測均依賴這部分數據,這就意味這無法爲模型訓練和預測設置統一的開始入口。

  • 高併發:除了一些特殊類型的門店,絕大多數門店的營業時間相對固定,從店長決定整理上傳銷售數據,到準備物料、排班準備次日營業,留給模型訓練和模型預測回吐預測結果的時間大概爲3小時。如果每個門店的預測指標有2至3項,那麼需要有足夠的調度能力在規定時間內完成大概2萬次模型訓練加預測流程。

  • 容錯:由於門店數量衆多且情況各不相同,仍然有很多潛在的因素可能導致流程出錯或失敗。原則上,某次流程的失敗不應該對其他流程造成任何影響,每個流程在平臺層面應該成爲互相獨立的任務。

因此,我們需要一套輕量化的分佈式服務框架,來實現滿足上述需求的模型訓練預測平臺,並在一定程度上保證平臺的可拓展性。結合此前團隊內的技術積累,最終選擇了Akka框架用於實現平臺的內部通信。

選型過程

###消息驅動方式——流程異步化

一次完整的預測任務包括:訓練數據準備→模型訓練→模型結果導出→預測數據準備→預測結果導出,其中數據準備步驟在時間上不確定,模型相關步驟在執行結果上不確定,如果採用同步模型,將會產生大量的等待線程,佔用浪費大量資源。在Actor模型中,每個Actor作爲一個基本計算單元,迴應接收到的消息,同時並行的:

  • 發送有限數量的消息給其他Actor
  • 創建有限數量的新Actor
  • 指定接受到下一個消息時的行爲

上述操作沒有順序執行的假設,因此可以並行進行。發送者與已經發送的消息解耦,可以進行無需等待的異步通信。

image

Actor模型通信方式

Akka中的Actor本質上就是接收消息並採取行動處理消息的對象,是封裝狀態和行爲的對象,它們唯一的通信方式是交換消息——把消息存放在接收方的郵箱裏。Actor自然形成樹形結構,這種結構的精髓在於任務被拆開、委託,直到任務小到可以被完整地處理。因此,我們將預測任務的各個步驟拆分抽象,並創建類型消息與步驟對應,將每個步驟交給線程級別的Actor執行處理,通過發送不同類型的消息來觸發創建不同操作的Actor,讓整個預測流程無需等待。

結構——應對高併發

由於絕大多數門店的營業時間大致相同,平臺在流量上會有明顯的峯值和低谷,在低谷期間平臺需要儘可能減少資源佔有量,而在流量峯值來臨時平臺要能夠及時響應,保證足夠的可用性。
經過討論,我們確定了採用Master-Worker模式的平臺結構,Master負責接收與分配任務,Worker負責處理執行具體的模型任務。

Master和Worker均爲獨立的ActorSystem,管理內部不不同操作邏輯的Actor,在空閒狀態下佔有資源很小。Actor爲線程級別,同樣僅佔用極少量資源,生命週期由ActorSystem統一管理。少量請求時,Actor線程具有很高的複用率,請求併發高時,ActorSystem會創建大量的Actor線程用來承接請求,保證可用性。

image

Akka中Actor的生命週期

子Actor——模塊化提高容錯

每個預測任務的模型相關步驟均存在失敗的可能性,此外,數據準備過程中的網絡波動、內容校驗出錯等情況,都會導致當前預測任務的失敗。對於失敗的任務,我們希望能夠儘可能記錄錯誤信息,爲重跑提供先決條件。

在Akka中,構建了父子Actor的樹形監督結構,提供Actor的監督機制以保證容錯性,把處理響應錯誤的責任交給出錯對象以外的實體。父Actor創建子Actor來委託處理子任務,同時便會自動地監管它們。子Actor列表維護在父Actor的上下文中,父Actor可以訪問它。

image

Akka中的Actor結構

通過更進一步的拆分細化,我們將Worker端的Actor分爲Prepare和Executor兩種,Prepare爲主要負責數據準備步驟,Executor負責模型相關步驟,統一由Worker端的父Actor管理,錯誤和異常均向上層拋出,由Worker端的父Actor記錄併發送給的錯誤收集模塊統一處理。

實踐應用

ActorSystem

創建ActorSystem時,默認將在classpath中尋找application.conf、 application.json和application.properties,並自動加載:

val system=ActorSystem("RsModelActorSystem")
val system=ActorSystem("RsModelActorSystem", ConfigFactory.load()) //同上

如果想要使用自己的配置文件,可以通過ConfigFactory來配置加載:

		val system = ActorSystem("UniversityMessageSystem", 
           ConfigFactory.load("own-application.conf")) 


		val config = ConfigFactory.parseString(
      		s"""
        	|akka.remote.netty.tcp.hostname = $host
        	|akka.actor.provider = akka.remote.RemoteActorRefProvider
        	|akka.remote.enabled-transport = akka.remote.netty.tcp
        	|akka.remote.netty.tcp.port = 2445
      		""".stripMargin)
    	val system = ActorSystem("RsModelActorSystem",
    		config.withFallback(ConfigFactory.load())) //同上

ActorSystem的配置參數中有大量參數可以自定義,需要根據實際需要修改,例如在該項目中,後期單個算法任務對象大小超過了Akka remote默認包大小128000 bytes,需要修改參數 akka.remote.netty.tcp.maximum-frame-size

Actor

一個Actor包含了狀態、行爲、一個郵箱、子Actor和一個監管策略,所有這些封裝在一個Actor引用裏。Actor對象通常包含一些變量來反映其所處的可能狀態,Akka-actor自身的輕量線程與系統的其他部分完全隔離,因此無須擔心併發問題。每當一個消息被處理,它會與Actor的當前行爲進行匹配。行爲是一個函數,它定義了在某個時間點處理當前消息所要採取的動作,需要結合實際需求編寫具體邏輯。Actor的郵箱是連接發送者與接收者的紐帶,每個Actor有且僅有一個郵箱,所有的發來的消息都在郵箱裏排隊。可以有不同策略的郵箱實現供選擇,缺省時爲FIFO。

編寫邏輯

在Actor類中,主要邏輯均在receive方法中實現,通過偏函數方法,執行並返回對應的邏輯:

		//ActorLogging提供Actor內部的日誌輸出
		class RsActor extends Actor with ActorLogging {
  			override def receive: Receive = {
    			case MapMessage(parameters) =>
      				println(parameters.get("code"))
	
    			case MapKeyMessage(parameters, key) =>
      				println(parameters.get(key))

    			case StringMessage(msg) =>
      				println(msg.getBytes().length)

    			case o: Object =>
      				println(o.getClass)

    			case _: AnyRef =>
      				println("233")
  			}
		}

生成引用

生成一個可以接收消息的Actor實例主要有兩個方法:

		//生成一個基於本地類的Actor實例
		val rsActor = system.actorOf(Props[RsActor], "rsActor")
		//生成一個基於遠程地址的Actor實例
		val rmActor = 
			system.actorSelection("akka.tcp://[email protected]:2445/user/rsActor")
		
		//	使用!向對應的Actor實例發送消息
		rsActor ! StringMessage("test")
		rmActor ! MapMessage(Map("code"->"233"))

Message

Akka中對傳遞的消息內容並沒有太嚴格要求,可以是基本數據類型,也可以是支持序列化的對象:

		//scala的case class便於簡潔地創建消息類
		case class StringMessage(msg: String) extends Serializable
		case class MapMessage(parameters: Map[String, String]) extends Serializable
		case class MapKeyMessage(parameters: Map[String, String], key: String) extends Serializable

其他

Akka作爲一款被廣泛使用的開源工具,在實際項目中體現出了很多的優勢,異步的消息驅動方式也給我們提供了一套新的思路和實現方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章