前文回顧
前文《Spark Streaming 新手指南》介紹了 Spark Streaming 的基本工作原理,並以 WordCount 示例進行解釋。此外,針對 Spark Streaming 的優缺點也做了一些描述。
本文重點主要是解釋流式處理架構的工作原理,讓讀者對 Spark Streaming 的整體設計原理及應用場景有所瞭解。
流式處理框架特徵
流式處理框架的特徵主要有以下五個方面。
1. 強實時處理
流式處理需要確保資料的實時產生、實時計算,此外,也需要確保處理結果的實時傳送。大多數流式處理架構多采用記憶體計算方式,即當資料到達後直接在記憶體中計算,只有少量資料會被儲存到硬碟,或者乾脆不儲存資料。這樣的系統架構可以確保我們能夠提供低延遲計算能力,可以快速地進行資料計算,在資料較短的時間內完成計算,體現資料的有用性。對於時效性特別短、潛在價值又很大的資料可以優先計算。
2. 高容錯能力
由於資料很容易丟失,這就需要系統具有一定的容錯能力,要充分地利用好僅有的一次資料計算機會,儘可能全面、準確、有效地從資料流中得出有價值的資訊。
3. 動態變化
一般採用流式處理架構的應用場景都存在資料速率不固定的情況,即可能存在前一時刻資料速率和後一時刻資料速率有較大的差異。這樣的需求要求系統具有很好的可伸縮性,能夠動態適應流入的資料流,具有很強的系統計算能力和大資料流量動態匹配的能力。一方面,在高資料流速的情況下,保證不丟棄資料,或者識別並選擇性地丟棄部分不重要的資料;另一方面,在低資料速率的情況下,保證不會太久或過多地佔用系統資源。
4. 多資料來源
由於可能存在很多的資料來源,而且各資料來源、資料流之間又可能是相互獨立的,所以無法保證資料是有序的,這就需要系統在資料計算過程中具有很好的資料分析和發現規律的能力,不能過多地依賴資料流間的內在邏輯或者資料流內部的內在邏輯。
5. 高可擴充套件
由於資料是實時產生、動態增加的,即只要資料來源處於活動狀態,資料就會一直產生和持續增加下去。可以說,潛在的資料量是無限的,無法用一個具體確定的資料實現對其進行量化。系統在資料計算過程中,無法儲存全部資料。由於硬體中沒有足夠大的空間來儲存這些無限增長的資料,也沒有合適的軟體來有效地管理這麼多資料。
流式處理框架技術需求
針對具有強實時處理、高容錯能力、動態變化、多資料來源、高可擴充套件等特徵的流式處理框架需求,那麼理想的流式處理框架應該表現出低延遲、高吞吐、持續穩定執行和彈性可伸縮等特性,這需要系統設計架構、任務執行方式、高可用性技術等關鍵技術的合理規劃和良好設計。
- 系統設計架構
系統架構是系統中各子系統間的組合方式,流式處理框架需要選擇特定的系統架構進行流式計算任務的部署。當前,針對流式處理框架較為流行的系統架構主要有無中心節點的 point-point 架構和有中心節點的 Master-Slaves 架構兩種。
(1) 對稱式架構。如圖 1 所示,系統中各個節點的作用是完全相同的,即所有節點之間互相可以做備份,這樣整個系統具有良好的可伸縮性。但是由於不存在中心節點,因此在資源排程、系統容錯、負載均衡等方面需要通過分散式協議幫助實現。目前商業產品 S4、Puma 屬於這類架構,S4 通過 Zookeeper 實現系統容錯、負載均衡等功能。
圖 1. 無中心節點架構
(2) 主從式系統架構。如圖 2 所示,系統存在一個主節點和多個從節點。主節點負責系統資源的管理和任務的協調,並完成系統容錯、負載均衡等方面的工作,從節點負責接收來自於主節點的任務,並在計算完成後進行反饋。各從節點間可以選擇是否資料往來,但是系統的整體執行狀態依賴主節點控制。Storm、Spark Streaming 屬於這種架構。
圖 2. 有中心節點架構
- 任務執行方式
任務執行方式是指完成有向任務圖到物理計算節點的部署之後,各個計算節點之間的資料傳輸方式。資料的傳輸方式分為主動推送方式和被動拉取方式兩種。
(1) 主動推送方式。在上游節點產生或計算完資料後,主動將資料傳送到相應的下游節點,其本質是讓相關資料主動尋找下游的計算節點,當下遊節點報告發生故障或負載過重時,將後續資料流推送到其他相應節點。主動推送方式的優勢在於資料計算的主動性和及時性,但由於資料是主動推送到下游節點,往往不會過多地考慮到下游節點的負載狀態、工作狀態等因素,可能會導致下游部分節點負載不夠均衡;
(2) 被動拉取方式。只有下游節點顯式進行資料請求,上游節點才會將資料傳輸到下游節點,其本質是讓相關資料被動地傳輸到下游計算節點。被動拉取方式的優勢在於下游節點可以根據自身的負載狀態、工作狀態適時地進行資料請求,但上游節點的資料可能未必得到及時的計算。
大資料流式計算的實時性要求較高,資料需要得到及時處理,往往選擇主動推送的資料傳輸方式。當然,主動推送方式和被動拉取方式不是完全對立的,也可以將兩者進行融合,從而在一定程度上實現更好的效果。
- 高可用性技術
流式計算框架的高可用性是通過狀態備份和故障恢復策略實現的。當故障發生後,系統根據預先定義的策略進行資料的重放和恢復。按照實現策略,可以被細分為被動等待 (passive standby)、主動等待 (active standby) 和上游備份 (upstream backup) 這 3 種策略。
(1) 被動等待策略
圖 3 所示,主節點 B 進行資料計算,副本節點 B’處於待命狀態,系統會定期地將主節點 B 上的最新的狀態備份到副本節點 B’上。出現故障時,系統從備份資料中進行狀態恢復。被動等待策略支援資料負載較高、吞吐量較大的場景,但故障恢復時間較長,可以通過對備份資料的分散式儲存縮短恢復時間。該方式更適合於精確式資料恢復,可以很好地支援不確定性應用計算,在當前流式資料計算中應用最為廣泛。
圖 3. 被動等待策略
(2) 主動等待策略
圖 4 所示,系統在為主節點 B 傳輸資料的同時,也為副本節點 B’傳輸一份資料副本。以主節點 B 為主進行資料計算,當主節點 B 出現故障時,副本節點 B’完全接管主節點 B 的工作,主副節點需要分配同樣的系統資源。該種方式故障恢復時間最短,但資料吞吐量較小,也浪費了較多的系統資源。在廣域網環境中,系統負載往往不是過大時,主動等待策略是一個比較好的選擇,可以在較短的時間內實現系統恢復。
圖 4. 主動等待策略
(3) 上游備份策略
每個主節點均記錄其自身的狀態和輸出資料到日誌檔案,當某個主節點 B 出現故障後,上游主節點會重放日誌檔案中的資料到相應副本節點 B’中進行資料的重新計算。上游備份策略所佔用的系統資源最小,在無故障期間,由於副本節點 B’保持空閒狀態,資料的執行效率很高。但由於其需要較長的時間進行恢復狀態的重構,故障的恢復時間往往較長,如需要恢復時間視窗為 30 分鐘的聚類計算,就需要重放該 30 分鐘內的所有元組。可見,於系統資源比較稀缺、運算元狀態較少的情況,上游備份策略是一個比較好的選擇方案。如圖 5 和圖 6 所示。
圖 5. 上游備份策略 1
圖 6. 上游備份策略 2
Spark Streaming 所處地位
Spark Streaming 是 Spark 的擴充套件,專門用來實現流式分析方式處理資料。Spark Streaming 支援 Kafka、Flume、Twitter、ZeroMQ、Kinesis、TCP Sockets 等多種資料來源。此外,也可以使用一個複雜的演算法,如 map、reduce、join、window,這些來處理資料。處理完的資料可以被髮送給檔案系統、資料庫、其他第三方。圖 7 引用自 Spark Streaming 官網,比較好地描述了 Spark Streaming 的地位。
圖 7. Spark Streaming 地位
Spark Streaming 接收輸出資料流,然後將這些資料分割後放入批處理流程 (batches),Spark 引擎稍後會處理這些資料,最終生成計算結果併傳送到外部系統。
筆者的前一篇文章已經詳細地通過 WordCount 示例介紹了 Spark Streaming 的執行次序、基本架構、RDD 概念,請讀者參閱文章《Spark Streaming 新手指南》。
Spark Streaming 應用例項
我們以一個流式處理圖片的例子作為本文的例項。我們把圖片檔案通過基於 Spark Streaming 的程式讀取成資料流,重新將資料流寫成圖片檔案並儲存在檔案系統上。
整個程式的流程圖如圖 8 所示。
圖 8. 圖片處理程式流程圖
如圖 8 所示,第一步我們需要實現一個服務,該服務不停地向 HDFS 檔案系統裡寫入圖片檔案,這些圖片檔案後續會被用來當作資料來源的原始資料,並被進行處理。程式碼如清單 1 所示。
清單 1. 迴圈寫入圖片檔案程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
public ServerSocket getServerSocket(int port){ ServerSocket server=null; try { server = new ServerSocket(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return server; } public void sendData(String path,ServerSocket server){ OutputStream out=null; FileInputStream in=null; BufferedOutputStream bf =null; try { out = server.accept().getOutputStream(); File file = new File(path); in = new FileInputStream(file); bf = new BufferedOutputStream(out); byte[] bt = new byte[(int)file.length()]; in.read(bt); bf.write(bt); } catch (IOException e) { e.printStackTrace(); }finally{ if(in!=null){ try { in.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(bf!=null){ try { bf.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(out!=null){ try { out.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(!server.isClosed()){ try { server.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } public static void main(String[] args) { if(args.length<4){ System.err.println("Usage:server3 <port> <file or dir> <send-times> <sleep-time(ms)>"); System.exit(1); } Map<Integer, String> fileMap = null; Server s = new Server(); for (int i = 0; i < Integer.parseInt(args[2]) ; i++) { ServerSocket server =null; while(server==null){ server = s.getServerSocket(Integer.parseInt(args[0])); try { Thread.sleep(Integer.parseInt(args[3])); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } while(!server.isBound()){ try { server.bind(new InetSocketAddress(Integer.parseInt(args[0]))); System.out.println("第"+(i+1)+"個服務端繫結成功"); Thread.sleep(Integer.parseInt(args[3])); } catch (NumberFormatException | IOException | InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } fileMap = s.getFileMap(args[1]); System.out.println("fileMap.size="+fileMap.size()); //System.out.println("fileMap="+fileMap); s.sendData(fileMap.get(s.getNum(0, fileMap.size()-1)), server); //s.sendData(args[1], server); } } public Map<Integer, String> getMap(String dir,Map<Integer, String> fileMap){ File file = new File(dir); if(file.isFile()){ if(file.getName().endsWith(".jpg")||file.getName().endsWith(".bmp")|file.getName(). endsWith(".JPG")||file.getName().endsWith(".BMP")){ if(file.length()<1024*1024*2){ fileMap.put(fileMap.size(),file.getAbsolutePath()); } }else{ } } if(file.isDirectory()){ File[] files = file.listFiles(); for (int j = 0; j < files.length; j++) { getMap(files[j].getAbsolutePath(), fileMap); } } return fileMap; } public Map<Integer, String> getFileMap(String dir){ Map<Integer, String> fileMap = new HashMap<Integer, String>(); return getMap(dir, fileMap); } public int getNum(int offset,int max){ int i = offset+(int)(Math.random()*max); if(i>max){ return i-offset; }else{ return i; } } |
接下來開啟一個程式,實現開啟 Socket 監聽,從指定埠讀取圖片檔案,這裡使用的是 Spark Streaming 的 socketStream 方法獲取資料流。程式程式碼是用 Scala 語言編寫的,如清單 4 所示。
清單 2. 讀取檔案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
val s = new SparkConf().setAppName("face") val sc = new SparkContext(s) val ssc = new StreamingContext(sc, Seconds(args(0).toInt)) val img = new ImageInputDStream(ssc, args(1), args(2).toInt, StorageLevel.MEMORY_AND_DISK_SER)//呼叫重寫的 ImageInputDStream 方法讀取圖片 val imgMap = img.map(x => (new Text(System.currentTimeMillis().toString), x)) imgMap.saveAsNewAPIHadoopFiles("hdfs://spark:9000/image/receiver/img", "", classOf[Text], classOf[BytesWritable], classOf[ImageFileOutputFormat], ssc.sparkContext.hadoopConfiguration)//呼叫 ImageFileOutputFormat 方法寫入圖片 imgMap.map(x => (x._1, { if (x._2.getLength > 0) imageModel(x._2) else "-1" }))//獲取 key 的值,即圖片 .filter(x => x._2 != "0" && x._2 != "-1") .map(x => "{time:" + x._1.toString +","+ x._2 + "},").print() ssc.start() ssc.awaitTermination() |
清單 2 程式碼設定 Spark 上下文環境,設定了每隔多少時間 (使用者輸入的第一個引數,單位:秒) 讀取一次資料來源,然後開始呼叫重寫的方法讀入圖片,我們需要對圖片進行分析,分析過程不是本程式關注的重點,這裡忽略,讀者可以自己網上搜尋圖片分析的開源庫,匯入即可實現圖片分析功能。
清單 3 當中自己定義了一個 Scala 類 ImageInputDStream,用於載入 Java 的讀入圖片類。
清單 3. Scala 實現讀取檔案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
class ImageInputDStream(@transient ssc_ : StreamingContext,host: String,port: Int,storageLevel: StorageLevel) extends ReceiverInputDStream[BytesWritable](ssc_) with Logging{ override def getReceiver(): Receiver[BytesWritable] = { new ImageRecevier(host,port,storageLevel) } } class ImageRecevier(host: String,port: Int,storageLevel: StorageLevel) extends Receiver[BytesWritable](storageLevel) with Logging{ override def onStart(): Unit = { new Thread("Image Socket"){ setDaemon(true) override def run(): Unit = { receive() } }.start() } override def onStop(): Unit = { } def receive(): Unit ={ var socket:Socket=null var in:InputStream =null try{ logInfo("Connecting to " + host + ":" + port) socket = new Socket(host, port) logInfo("Connected to " + host + ":" + port) in= socket.getInputStream val buf = new ArrayBuffer[Byte]() var bytes = new Array[Byte](1024) var len = 0 while(-1 < len){ len=in.read(bytes) if(len > 0){ buf ++=bytes } } val bw = new BytesWritable(buf.toArray) logError("byte:::::"+ bw.getLength) store(bw) logInfo("Stopped receiving") restart("Retrying connecting to " + host + ":" + port) }catch { case e: java.net.ConnectException => restart("Error connecting to " + host + ":" + port, e) case t: Throwable => restart("Error receiving data", t) }finally { if(in!=null){ in.close() } if (socket != null) { socket.close() logInfo("Closed socket to " + host + ":" + port) } } } |
清單 2 裡面定義了寫回圖片檔案時需要呼叫 ImageFileOutputFormat 類,這個類繼承了 org.apache.hadoop.mapreduce.lib.output.FileOutputFormat 類,通過緩衝讀取的方式加快資料讀取。程式碼如清單 4 所示。
清單 4. 寫入檔案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
public class ImageFileOutFormat extends FileOutputFormat<Text,BytesWritable> { @Override public RecordWriter<Text, BytesWritable> getRecordWriter(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException { Configuration configuration = taskAttemptContext.getConfiguration(); Path path = getDefaultWorkFile(taskAttemptContext, ""); FileSystem fileSystem = path.getFileSystem(configuration); FSDataOutputStream out = fileSystem.create(path,false); return new ImageFileRecordWriter(out); } protected class ImageFileRecordWriter extends RecordWriter<Text, BytesWritable>{ protected DataOutputStream out; private final byte[] keyValueSeparator; private static final String colon=","; public ImageFileRecordWriter(DataOutputStream out){ this(colon,out); } public ImageFileRecordWriter(String keyValueSeparator,DataOutputStream out) { this.out=out; this.keyValueSeparator = keyValueSeparator.getBytes(); } @Override public void write(Text text, BytesWritable bytesWritable) throws IOException, InterruptedException { if(bytesWritable!=null){ out.write(bytesWritable.getBytes()); } } @Override public void close(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException { out.close(); } } } |
通過清單 1-4 的程式,我們可以實現讀入圖片檔案->對圖片進行一些業務處理->寫回分析成果物 (文字資訊、圖片)。
結束語
通過本文的學習,讀者可以大致瞭解流式處理框架的設計原理、Spark Streaming 的工作原理,並通過一個讀取、分析、寫入圖片的示例幫助讀者進行加深瞭解。目前市面上釋出的 Spark 中文書籍對於初學者來說大多較為難讀懂,更沒有專門針對 Spark Streaming 的文章。作者力求推出一系列 Spark 文章,讓讀者能夠從實際入手的角度來了解 Spark Streaming。後續除了應用之外的文章,還會致力於基於 Spark 及 Spark Streaming 的系統架構、原始碼解釋等方面的文章釋出。