簡體   English   中英

Scala,讀取文件,處理行並使用並發(akka),異步API(nio2)將輸出寫入新文件

[英]Scala, read file, process lines and write output to a new file using concurrent (akka), asynchronous APIs (nio2)

1:我遇到了一個試圖處理大文本文件的問題 - 10Gigs +

單線程解決方案如下:

val writer = new PrintWriter(new File(output.getOrElse("output.txt")));
for(line <- scala.io.Source.fromFile(file.getOrElse("data.txt")).getLines())
{
  writer.println(DigestUtils.HMAC_SHA_256(line))
}
writer.close()

2:我嘗試使用並發處理

val futures = scala.io.Source.fromFile(file.getOrElse("data.txt")).getLines
               .map{ s => Future{ DigestUtils.HMAC_SHA_256(s) } }.to
val results = futures.map{ Await.result(_, 10000 seconds) }

這會導致GC開銷限制超出異常(有關stacktrace,請參閱附錄A)

3:我嘗試使用Akka IO和AsynchronousFileChannel的組合以下https://github.com/drexin/akka-io-file我能夠使用FileSlurp以字節塊的形式讀取文件但是找不到要讀取的解決方案按行要求的文件。

任何幫助將不勝感激。 謝謝。

附錄A.

[error] (run-main) java.lang.OutOfMemoryError: GC overhead limit exceeded
java.lang.OutOfMemoryError: GC overhead limit exceeded
        at java.nio.CharBuffer.wrap(Unknown Source)
        at sun.nio.cs.StreamDecoder.implRead(Unknown Source)
        at sun.nio.cs.StreamDecoder.read(Unknown Source)
        at java.io.InputStreamReader.read(Unknown Source)
        at java.io.BufferedReader.fill(Unknown Source)
        at java.io.BufferedReader.readLine(Unknown Source)
        at java.io.BufferedReader.readLine(Unknown Source)
        at scala.io.BufferedSource$BufferedLineIterator.hasNext(BufferedSource.s
cala:67)
        at scala.collection.Iterator$$anon$11.hasNext(Iterator.scala:327)
        at scala.collection.Iterator$class.foreach(Iterator.scala:727)
        at scala.collection.AbstractIterator.foreach(Iterator.scala:1157)
        at scala.collection.generic.Growable$class.$plus$plus$eq(Growable.scala:
48)
        at scala.collection.immutable.VectorBuilder.$plus$plus$eq(Vector.scala:7
16)
        at scala.collection.immutable.VectorBuilder.$plus$plus$eq(Vector.scala:6
92)
        at scala.collection.TraversableOnce$class.to(TraversableOnce.scala:273)
        at scala.collection.AbstractIterator.to(Iterator.scala:1157)
        at com.test.Twitterhashconcurrentcli$.doConcurrent(Twitterhashconcu
rrentcli.scala:35)
        at com.test.Twitterhashconcurrentcli$delayedInit$body.apply(Twitter
hashconcurrentcli.scala:62)
        at scala.Function0$class.apply$mcV$sp(Function0.scala:40)
        at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:
12)
        at scala.App$$anonfun$main$1.apply(App.scala:71)
        at scala.App$$anonfun$main$1.apply(App.scala:71)
        at scala.collection.immutable.List.foreach(List.scala:318)
        at scala.collection.generic.TraversableForwarder$class.foreach(Traversab
leForwarder.scala:32)
        at scala.App$class.main(App.scala:71)

這里的訣竅是避免將所有數據一次性讀入內存。 如果您迭代並向工作人員發送行,則會冒這個風險,因為發送給actor是異步的,因此您可能會將所有數據讀入內存,並且它將位於actor的郵箱中,可能會導致OOM異常。 更好的高級方法是使用單個主操作員和下面的子工作池來進行處理。 這里的技巧是在主文件中使用一個惰性流(如scala.io.Source.fromX返回的Iterator ),然后在scala.io.Source.fromX中使用工作拉動模式來防止他們的郵箱填滿數據。 然后,當迭代器不再有任何行時,主服務器會自行停止,這將停止工作者(如果需要,您也可以使用此點關閉actor系統,如果這是你真正想做的事情)。

這是一個非常粗略的輪廓。 請注意,我還沒有測試過這個:

import akka.actor._
import akka.routing.RoundRobinLike
import akka.routing.RoundRobinRouter
import scala.io.Source
import akka.routing.Broadcast

object FileReadMaster{
  case class ProcessFile(filePath:String)
  case class ProcessLines(lines:List[String], last:Boolean = false)
  case class LinesProcessed(lines:List[String], last:Boolean = false)

  case object WorkAvailable
  case object GimmeeWork
}

class FileReadMaster extends Actor{
  import FileReadMaster._

  val workChunkSize = 10
  val workersCount = 10

  def receive = waitingToProcess

  def waitingToProcess:Receive = {
    case ProcessFile(path) =>
      val workers = (for(i <- 1 to workersCount) yield context.actorOf(Props[FileReadWorker])).toList
      val workersPool = context.actorOf(Props.empty.withRouter(RoundRobinRouter(routees = workers)))
      val it = Source.fromFile(path).getLines
      workersPool ! Broadcast(WorkAvailable)
      context.become(processing(it, workersPool, workers.size))

      //Setup deathwatch on all
      workers foreach (context watch _)
  }

  def processing(it:Iterator[String], workers:ActorRef, workersRunning:Int):Receive = {
    case ProcessFile(path) => 
      sender ! Status.Failure(new Exception("already processing!!!"))


    case GimmeeWork if it.hasNext =>
      val lines = List.fill(workChunkSize){
        if (it.hasNext) Some(it.next)
        else None
      }.flatten

      sender ! ProcessLines(lines, it.hasNext)

      //If no more lines, broadcast poison pill
      if (!it.hasNext) workers ! Broadcast(PoisonPill)

    case GimmeeWork =>
      //get here if no more work left

    case LinesProcessed(lines, last) =>
      //Do something with the lines

    //Termination for last worker
    case Terminated(ref)  if workersRunning == 1 =>
      //Done with all work, do what you gotta do when done here

    //Terminared for non-last worker
    case Terminated(ref) =>
      context.become(processing(it, workers, workersRunning - 1))

  }
}

class FileReadWorker extends Actor{
  import FileReadMaster._

  def receive = {
    case ProcessLines(lines, last) => 
      sender ! LinesProcessed(lines.map(_.reverse), last)
      sender ! GimmeeWork

    case WorkAvailable =>
      sender ! GimmeeWork
  }
}

這個想法是主人迭代文件的內容並將一大堆工作發送給一個童工池。 文件處理開始時,主人告訴所有孩子工作可用。 然后每個孩子繼續請求工作,直到不再有工作為止。 當主人檢測到文件被讀完時,它會向孩子們播放一個毒丸,讓他們完成任何未完成的工作,然后停止。 當所有孩子都停下來時,主人可以完成所需的任何清理工作。

同樣,根據我的想法,這是非常粗略的。 如果我在任何地區離開,請告訴我,我可以修改答案。

實際上,在並行變體中,您試圖首先將所有文件讀入內存,作為行列表,然后進行復制(使用方法List.to)。 顯然,這導致了OOME。

要並行化,首先要確定它值得做。 您不應該從順序文件(以及寫入)並行讀取:這只會導致磁頭過度移動並使速度變慢。 如果DigestUtils.HMAC_SHA_256(s)花費的時間與讀取行相當或更長,則並行化才有意義。 制作基准來衡量兩次。 然后,如果你決定哈希碼計算的並行化是值得做的,找出工作線程的數量:想法是經過的計算時間大致等於讀取時間。 讓一個線程讀取行,批量打包(比如一批1000行),並將批量放入固定大小的ArrayBlockingQueue (比如1000)。 需要進行批處理,因為隊列太多,隊列上的同步操作太多,導致爭用。 讓工作線程使用方法take從該隊列中讀取批次。

另一個線程應該將結果寫入"output.txt" ,也與阻塞隊列連接。 如果必須在輸出文件中保持行的順序,則應使用更復雜的通信工具而不是第二個隊列,但這是另一個故事。

以下代碼未經過測試:)

映射到期貨絕對不是一個好主意。
相反,當你已經使用Akka時,我會引入一個特殊的LineProcessor actor然后向它發送行:

val processor = system.actorOf(Props(new LineProcessor))

val src = scala.io.Source.fromFile(file.getOrElse("data.txt"))

src.getLines.foreach(line => processor ! line)  

在LineProcessor中,您可以封裝邏輯來處理該行:

class LineProcessor extends Actor {
  def receive {
    case line => // process the line
  }
}    

這里的訣竅是演員可以很容易地水平縮放。 只需將LineProcessor actor包裝在路由器中......

// this will create 10 workers to process your lines simultaneously
val processor = system.actorOf(Props(new LineProcessor).withRouter(RoundRobinRouter(10))

值得一提的是,如果你需要在保留順序的地方寫行,那就變得有點棘手了。 =)(當從文件中讀取一行時,你需要捕獲它的數字,當寫回來時你需要協調所有工人)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM