雲端服務與經驗談 [2] 雲端服務與架構演化 by qrtt1 | CodeData
top

雲端服務與經驗談 [2] 雲端服務與架構演化

分享:

雲端服務與經驗談 [1] 由於需求的變更,意外進入雲端服務的世界 << 前情

先前文章提到的「工作分派器」與「執行者」,這個架構簡單地將分配工作的角色 Web 化,並記錄目前任務的進度(已完成、待執行或執行中的工作)。在執行者來詢問是否有需要執行的工作時,送出指定數量的待執行工作,並將標示為執行中的工作;執行者完成工作後,將工作分派器更新結果,有結果的工作會標示為「已完成」,若執行時間太久則由工作分派器定期回收將工作重設為「待執行」:

experience-about-cloud-computing-service-2-1

這個初級的分工實作有幾個問題是需要面對的:

  • 為了精確管理工作狀態,使用同步化區塊(Java 的 synchronized block)造成效能瓶項
  • 為特定資料內容實作,當有不同的工作目標時不易改變

避免同步設計

實際測試工作分派器能同時間承受多少個執行者,大致心理有譜,設計雖然滿足了需求,但實作方法的選擇造成效能的瓶頸:當執行者增加到一個門檻時,getJob() 的缺點顯露無疑,為了精確管理工作狀態,getJob() 與 update() 不能同時發生,所以我們在程裡使用同步化區塊(Java 的 synchronized block)。它的效果就是多個執行者來同時呼叫 getJob() 或 update() 同一個時間內只會有一個人可以執行。在執行者數量小時,不會明顯感覺到需要等待其他執行者呼叫 getJob() 或 update()。當數量多的時候,執行都一旦做完工作或是需重新取得待作工作時,就會造成多個執行者等待一個的現象。在良好設計的前提下,執行者數量越多執行效能應該會越好,如下圖左方的曲線。由於實作的問題,實際執行起來的效果成了右方的曲線。因為不良的實作、選擇了不當的策略、架構組合都可能讓實際的結果與設計時產生落差:

experience-about-cloud-computing-service-2-2

題問如上述所分析的,但是在面臨更大多的工作分派任務前得盡可能改善它。瓶頸出現在多執行緒的並行管理,只要解開這個問題它的效能曲線就有機會接近等比例發展。面臨二選一的情境:

  • 設法減少(甚至去除)lock 的情況下,滿足執行者仍不會重複取得工作
  • 重新設計工作分派器

思考現行的架構為何需要鎖定呢?原因是:我們不希望同一份工作被多個執行者重複取得,這會照成運算資源的浪費。由於 Web Server 的特性,讓執行者主動去要資料是較合理的方式,這情況使得工作分派器需透過消極的鎖定避免重複。假設我們今天不是用 Web Server 為基礎去設計它,讓它有機會變成執行者是被動被分配工作呢?在這個情境下,執行者沒有索取工作項目的功能,只能等待工作,接著後執行它。工作分派器就佔據了主導的位置,沒有其他執行緒共用資料,因此再無鎖定的需求。

若是要沿用 Web 應用程式的架構,有幾種可能的技術是讓 HTTP Server 取回主導權:

  • Long Polling,採用長時間等待的方式讓 Server 想回應時才回應。Server 端就以簡單地統一派送工作,沒有輪到的就等待著。
  • WebSocket,HTML 5 開始提供的技術,能答到如同一般應用程式建立 Socket 的雙向溝通。

若是決定採用不同的架構,那麼適於分散式且非同步的訊息傳遞技術來說,採用訊息佇列(Message Queue)是很常見的做法,這類產品的 API 通常提供主動取得訊息或事件驅動型式的工作方式。若採用訊息佇列的產品通常需要架設一台訊息傳遞伺服器(或是一整個叢集),像是 RabbitMQ。也有不同思維,不需訊息傳遞伺服器的產品,例如:ZeroMQ。這類的產品也有直接的雲端服務,例如:Amazon Simple Queue Service (Amazon SQS)。

採用訊息佇列的新架構如下圖所示。工作項目仍來自於資料庫,但傳遞的媒界已經轉變為訊息佇列。所有的工作會保留在標為 JOBs 的佇列,執行者需要向 JOBs 佇列訂閱(Subscribe)訊息,一旦完成訂閱的動作,訊息伺服器就會開始將訊息發送(Publish)給訂閱者。由於執行者訂閱 JOBs 佇列的消息,它會自動收到下一項待執行的工作,執行完畢後將結果傳自 RESULTs 佇列,由 Server 端進更新資料庫:

experience-about-cloud-computing-service-2-3

由特定對象轉向泛化設計

消去「同步化」對程式擴展性的影響後,仍需考慮後續這份設計是否容易被反覆使用的問題。思考的時間點不用太早,因為沒有實際的需求是摸不清新的議題是如何衝擊現有實作的。

回歸到原本的需求是為了進行網路媒體資料偵測設計,並支援簡單地擴展。它實際的作法是,將原始資料庫內的清單,拉進 Web Server 內的嵌入式資料庫記錄著,並提供 CRUD 的相關動作將偵測結果寫回資料庫。在設計上不夠抽象之處是依賴特定的資料型態,以網路串流描述資訊(metadata)偵測為例,我們需要偵測的資料項目為:

  • 傳輸協定:http、rtmp、mms、shoutcast 等
  • 影像 codec:h264、vc1 等
  • 音訊 codec:aac、mp3 等
  • 影像大小
  • 聲道數量

直覺、簡易的做法是用一個 POJO 寫上需要的欄位,用物件來傳遞、操作它。

public class StreamingMetadata {
    // 資料庫對應欄位
    Long id;
    String url;
    // 偵測結果
    String format;
    String videoCodec;
    String audioCodec;
    int width;
    int height;
    int channels;
}

public interface JobDispatcher {
    public void addJob(StreamingMetadata metadata);
    public StreamingMetadata[] getJobs(int maxJobs);
    public void updateJob(StreamingMetadata metadata);
}

使用 StreamingMetadata 物件存放每一個待執行工作的狀態,並作為工作分派器與執行者之間傳遞的物件:

  • 由原始資料庫取得執行清單,利用 id, url 建立出 StreamingMetadata 物件
  • 呼叫 JobDispatcher.addJob() 將新工作排入工作分派器之內(暫存於嵌入式資料庫)
  • 執行者使用 JobDispatcher.getJobs() 取得待執行的工作(StreamingMetadata 陣列)
  • 執行者使用 JobDispatcher.updateJob() 將偵測完畢的 StreamingMetadata 回報給工作分派器

由於「需求」是偵測網路串流,所以我們才將 POJO 命名為 StreamingMetadata 並依要蒐集的資料給定那些欄位。若「需求」改變了,甚至是不同的「目標」,例如偵測網路串流是否活著,那麼 POJO 依目標應修改成:

public class StreamingStatus {
    Long id;
    String url;
    boolean available;
}

思考如何兼容需求時,若沒有心力或多餘的時間大改,那也許就會是這種妥協的版本。只是單純在舊有的設計追加新需求需要的欄位,這樣 JobDispatcher 也無需更動:

public class StreamingMetadata {
    Long id;
    String url;
    String format;
    String videoCodec;
    String audioCodec;
    int width;
    int height;
    int channels;
    boolean available;
}

在偵測存活時只用 available 欄位,在偵測描述資料時用原先的欄位。接著在由工作分派器產生結果的部分用個條件判斷,分辨不同工作目標時該如何輸出結果。例如:

  • 如果是描述資料偵測,就將結果更新回原始資料庫
  • 如果是存活偵測,就將失效的連結回報給資料管理員

妥協版本的實作,並不是最適當的寫法。再來一個新的需求,又多了些欄位與條件判斷。程式會變得越來越難維護,讓後繼者覺得難以接手。

要朝著抽象化的方向發展,那就得不依賴特定需求設計的資料保存格式,最好是與特定語言無關的,例如:XML。隨著近年 Javascript 的發展與 Web App 的設計,JSON 格式也相當受歡應,而它在各種語言上 API 的操作相對於 XML 更簡單。JSON 在 Java 可以單純轉譯為 Map 物件。同時考慮在整個工作分派與執行者運作的模型下,無論是以 HTTP 協定或訊息佇列作為通訊媒界,它都得經過一層轉譯,由特定語言資料型態(例如 Java),轉換成能透過網路傳遞的資料。若以 JSON 的型式表現各種意圖的工作,會相當容易達到兼容新需求的目標。

兼容不同目標的 JobDispatcher 如下,我們去除特意圖的 StreamingMetadata,改用較中性的 JsonObject 作為工作內容的載體,並將因目標不同而變化的 update 功能獨立出來:

public interface JobDispatcher {
    public void addJob(JsonObject job);
    public JsonObject[] getJob(int maxJobs);
}

public interface UpdateStrategy {
    public void update(JsonObject job);
}

新的構想可以保持工作分派器與執行者運作的模式,同時依不同的工作需求,單獨擴充新的資料更新邏輯。如此一來,就能讓先前構思多時的擴展性架構能隨著新需求反覆使用。

更多的設計考量

本篇談及「架構演化」並不是一些通則,而是對設計反思的歷程。過程中有許多細節是需要透過 profiling 數據來驗證,即使仰賴經驗也得找到佐證的材料來辨識目前的瓶頸在哪。所提到的只是實務上的一小部分,若要面面俱到得適您發展的需求而定。

有些更具體效法的對象,例如:Hadoop 是近幾年面對巨量資料而興起的 MapReduce 解決方案。光是研究評論文章指出的優缺點的都能得到許多啟發,像是 Hadoop 在 1.x 版時常被指出的 single point of failure (SPOF) 問題,在 Hadoop 內的 Named Node 是負責管理資料儲存位置的重要元件,「它在設計上沒有避免 SPOF 的問題」,這句話的意思是說,如果 Named Node 因故消失了,其他功能也就無法正常運作,無法正確得知資料正確的位置。同樣的,回顧工作分派器的例子,單一的工作分派器設計也有 SPOF 的問題。

仔細去分析每一個設計的議題,很難找到「完美」的作品。時間有限,在能承受的範圍內,我們不必解決所有邏輯上應該排除的問題,得在需求、實際使用、實作時間三者之間取得均衡,以思考最該解決的是哪一項,同時也得誠實列出已知問題作為後續維護時的改善依據。

後續 >> 雲端服務與經驗談 [3] 虛擬機器與應用程式部署

分享:
按讚!加入 CodeData Facebook 粉絲群

留言

留言請先。還沒帳號註冊也可以使用FacebookGoogle+登錄留言

關於作者

目前在一家網路應用軟體公司擔任開發工作,對多媒體處理與雲端應用充滿興趣,工作之餘亦常整理開發經驗分享於網路或於社群活動時進行分享。

熱門論壇文章

熱門技術文章