談談從CAP定理到Lambda架構的演化
CAP 定理指出資料庫不能同時保證一致性、可用性和分割槽容錯性。但是我們不能犧牲分割槽容錯性,因此必須在可用性和一致性之間做出權衡。管理這種權衡是 NoSQL 執行的核心焦點。
一致性意味著在成功寫入之後,以後的讀取將始終考慮該寫入。可用性意味著可以隨時讀取和寫入系統。在分割槽期間,只能擁有這些屬性之一。
選擇一致性而不是可用性的系統必須處理一些棘手的問題。當資料庫不可用時怎麼做?可以嘗試緩衝寫入以備後用,但是如果丟失了帶有緩衝區的機器,就有丟失這些寫入的風險。此外,緩衝寫入可能是一種不一致的形式,因為客戶端認為寫入已成功但寫入尚未在資料庫中。或者,可以在資料庫不可用時將錯誤返回給客戶端。但是,如果曾經使用過一種告訴“稍後再試”的產品,就會知道這會是多麼令人惱火。
另一種選擇是選擇可用性而不是一致性。這些系統所能提供的最好的一致性保證就是“最終一致性”。如果使用最終一致的資料庫,那麼有時會讀取到與剛剛寫入的結果不同的結果。有時多個訪問者同時讀取同一個金鑰會得到不同的結果。更新可能不會傳遞到一個值的所有副本,因此最終會得到一些副本獲得一些更新而其他副本獲得不同的更新。一旦檢測到值出現差異,就可以修復該值。這需要使用向量時鐘追溯歷史並將更新合併在一起,稱為“讀取修復”。
在應用層維護最終一致性對開發人員來說負擔太重。讀取修復程式碼極易受到開發人員錯誤的影響;如果犯了錯誤,錯誤的讀取修復將給資料庫帶來不可逆轉的損壞。因此犧牲可用性是有問題的,最終一致性太複雜以至於無法合理地構建應用程式。然而,這是唯一的兩個選擇, CAP 定理是自然界的事實,那麼還有什麼替代方案呢?還有另一種方法。你無法避免 CAP 定理,但你可以隔離它的複雜性並防止它破壞你對系統進行推理的能力。CAP 定理引起的複雜性是我們如何構建資料系統的基本問題。有兩個問題特別突出:在資料庫中使用可變狀態以及使用增量演算法來更新該狀態。正是這些問題與 CAP 定理之間的相互作用導致了複雜性。
在這篇文章中將展示一個系統的設計,該系統透過防止 CAP 定理通常引起的複雜性來突破它。CAP 定理是關於資料系統對機器故障的容錯程度的結果。然而,有一種比機器容錯更重要的容錯形式:人為容錯。如果軟體開發有任何確定性,那就是開發人員並不完美,錯誤將不可避免地影響生產。我們的資料系統必須對寫入錯誤資料的錯誤程式具有彈性,而下面將要展示的系統具有儘可能多的人為容錯能力。
這篇文章將挑戰對如何構建資料系統的基本假設。但是,透過打破我們當前的思維方式並重新想象應該如何構建資料系統,出現的是一種比想象的更好的、可擴充套件和健壯的架構。
什麼是資料系統
在我們談論系統設計之前,讓我們首先定義我們試圖解決的問題。資料系統的目的是什麼?什麼是資料?除非我們可以用清楚地封裝每個資料應用程式的定義來回答這些問題,否則我們甚至不需要接近 CAP 定理。
資料應用範圍從儲存和檢索物件、連線、聚合、流處理、連續計算、機器學習等等。目前尚不清楚是否存在如此簡單的資料系統定義——似乎我們對資料所做的事情範圍太廣,無法用單一定義來概括。
但是,有這麼一個簡單的定義。就是這個:
Query = Function(All Data)
而已。這個等式總結了資料庫和資料系統的整個領域。該領域的一切——過去 50 年的 RDBMS、索引、OLAP、OLTP、MapReduce、ETL、分散式檔案系統、流處理器、NoSQL 等——都以這種或那種方式總結為該等式。
資料系統回答有關資料集的問題。這些問題稱為“查詢”。這個等式表明查詢只是擁有的所有資料的函式。
這個等式可能看起來過於籠統而無用。它似乎沒有捕捉到資料系統設計的任何複雜性。但重要的是每個資料系統都屬於這個等式。該等式是我們探索資料系統的起點,該等式最終將導致一種突破 CAP 定理的方法。
這個等式中有兩個概念:“資料”和“查詢”。這些是在資料庫領域中經常混淆的不同概念,因此讓我們嚴格瞭解這些概念的含義。
資料
讓我們從“資料”開始。一條資料是一個不可分割的單元,你認為它是真實的,除了它存在之外沒有其他原因。它就像數學中的公理。
關於資料,有兩個重要的屬性需要注意。首先,資料本質上是基於時間的。一條資料是知道在某個時刻是真實的事實。例如,假設張梓涵在她的社交網路資料中輸入她住在北京。從該輸入中獲取的資料是,截至她將該資訊輸入她的個人資料的特定時刻,她住在北京。假設張梓涵稍後將她的個人資料位置更新為上海。然後你知道她在那段時間住在上海。她現在住在上海的事實並沒有改變她曾經住在北京的事實。這兩個資料都是真實的。
資料的第二個屬性緊隨第一個屬性:資料本質上是不可變的。由於它與時間點的聯絡,一條資料的真實性永遠不會改變。人們無法回到過去來改變資料的真實性。這意味著只能對資料執行兩個主要操作:讀取現有資料和新增更多資料。CRUD變成了 CR。我省略了“更新”操作。這是因為更新對不可變資料沒有意義。例如,“更新”張梓涵的位置實際上意味著正在新增一條新資料,表明她最近住在一個新位置。我也省略了“刪除”操作。同樣,大多數刪除情況更好地表示為建立新資料。例如,如果張三停止在微博上關注李四,這不會改變他曾經關注過她的事實。因此,與其刪除表示他關注她的資料,不如新增一條新的資料記錄,說明他在某個時刻取消了對她的關注。
在某些情況下,確實希望永久刪除資料,例如要求在一定時間後清除資料的法規。將要展示的資料系統設計很容易支援這些情況,因此為了簡單起見,我們可以忽略這些情況。
這種資料定義幾乎肯定與習慣的不同,特別是如果來自以更新為常態的關聯式資料庫世界。有兩個原因。首先,這個資料定義非常通用:很難想出一種資料不符合這個定義。其次,資料的不變性是我們在設計一個戰勝 CAP 定理的人類容錯資料系統時要利用的關鍵屬性。
查詢
等式中的第二個概念是“查詢”。查詢是一組資料的推導。從這個意義上說,查詢就像數學中的定理。例如,“張梓涵目前的位置是什麼?” 是一個查詢。可以透過返回有關張梓涵 位置的最新資料記錄來計算此查詢。查詢是完整資料集的函式,因此它們可以做任何事情:聚合、將不同型別的資料連線在一起等等。因此,可能會查詢服務的女性使用者數量,或者可能會查詢推文資料集,瞭解過去幾個小時的熱門話題。
我們已將查詢定義為完整資料集上的函式。當然,許多查詢不需要執行完整的資料集——它們只需要資料集的一個子集。但重要的是我們的定義封裝了所有可能的查詢,如果我們要突破 CAP 定理,我們必須能夠對任何查詢做到這一點。
突破 CAP 定理
計算查詢的最簡單方法是在完整資料集上逐字執行函式。如果可以在延遲限制內執行此操作,那麼就完成了。沒有別的東西可以建造了。
當然,期望一個函式在一個完整的資料集上快速完成是不可行的。許多查詢,例如為網站提供服務的查詢,需要毫秒級的響應時間。但是,讓我們假設可以快速計算這些函式,讓我們看看這樣的系統如何與 CAP 定理互動。正如即將看到的,像這樣的系統不僅突破了 CAP 定理,而且還消滅了它。
CAP 定理仍然適用,因此需要在一致性和可用性之間做出選擇。關鍵之處在於,一旦決定了要做出的權衡,就完成了。透過使用不可變資料和從頭開始計算查詢,避免了 CAP 定理通常導致的複雜性。
如果選擇一致性而不是可用性,那麼與以前相比不會有太大變化。有時將無法讀取或寫入資料,因為犧牲了可用性。但對於需要嚴格一致性的情況,這是一種選擇。
當選擇可用性而不是一致性時,事情會變得更加有趣。在這種情況下,系統是最終一致的,沒有任何最終一致性的複雜性。由於系統具有高可用性,可以隨時編寫新資料和計算查詢。在失敗的情況下,查詢將返回不包含以前寫入的資料的結果。最終,這些資料將是一致的,並且查詢會將這些資料合併到它們的計算中。
關鍵是資料是不可變的。不可變資料意味著沒有更新這樣的東西,因此一條資料的不同副本不可能變得不一致。這意味著沒有不同的值、向量時鐘或讀取修復。從查詢的角度來看,一條資料要麼存在,要麼不存在。該資料上只有資料和功能。無需執行任何操作來強制執行最終一致性,並且最終一致性不會妨礙對系統進行推理。
之前導致複雜的是增量更新和CAP定理的互動。增量更新和 CAP 定理真的不能很好地結合在一起;可變值需要在最終一致的系統中進行讀取修復。透過拒絕增量更新、接受不可變資料以及每次都從頭開始計算查詢,可以避免這種複雜性。CAP 定理已被突破。
當然,我們剛剛經歷的是一個思想實驗。雖然我們希望每次都能從頭開始計算查詢,但這是不可行的。然而,我們已經瞭解了真實解決方案的一些關鍵屬性:
1.該系統使儲存和擴充套件不可變、不斷增長的資料集變得容易
2.主要的寫操作是新增新的不可變資料事實
3.系統透過從原始資料重新計算查詢來避免 CAP 定理的複雜性
4.系統使用增量演算法將查詢延遲降低到可接受的水平
讓我們開始探索這樣一個系統是什麼樣的。請注意,從這裡開始的一切都是最佳化。資料庫、索引、ETL、批處理計算、流處理——這些都是最佳化查詢功能並將延遲降低到可接受水平的技術。這是一個簡單而深刻的認識。資料庫通常被認為是資料管理的核心,但實際上它們是更大範圍的一部分。
批次計算
弄清楚如何在任意資料集上快速執行任意函式是一個令人生畏的問題。所以讓我們稍微放鬆一下這個問題。讓我們假設查詢過時幾個小時是可以的。以這種方式放鬆問題會導致構建資料系統的簡單、優雅和通用的解決方案。之後,我們將擴充套件解決方案,使問題不再寬鬆。
由於查詢是所有資料的函式,因此使查詢快速執行的最簡單方法是預先計算它們。每當有新資料時,只需重新計算所有內容。這是可行的,因為我們放寬了問題,允許查詢過時幾個小時。這是此工作流程的示例:
要構建它,需要一個系統:
1.可以輕鬆儲存龐大且不斷增長的資料集
2.可以以可擴充套件的方式計算該資料集上的函式
這樣的系統是存在的。它成熟,經過數百個組織的實戰測試,並且擁有龐大的工具生態系統。它叫做Hadoop。Hadoop並不完美,但它是進行批處理的合適工具。
很多人會說 Hadoop 只適用於“非結構化”資料。這是完全錯誤的。Hadoop 非常適合結構化資料。使用Thrift或Protocol Buffers等工具,可以使用豐富的、可演化的模式來儲存資料。
Hadoop 由兩部分組成:分散式檔案系統 (HDFS) 和批處理框架 (MapReduce)。HDFS 擅長以可擴充套件的方式跨檔案儲存大量資料。MapReduce 擅長以可擴充套件的方式對該資料執行計算。這些系統完全符合我們的需求。
我們會將資料儲存在 HDFS 上的平面檔案中。檔案將包含一系列資料記錄。要新增新資料,只需將包含新資料記錄的新檔案附加到包含所有資料的資料夾即可。在 HDFS 上儲存這樣的資料解決了“儲存一個龐大且不斷增長的資料集”的需求。
對該資料進行預計算查詢同樣簡單明瞭。MapReduce 是一種具有足夠表現力的範例,幾乎任何功能都可以作為一系列 MapReduce 作業來實現。Cascalog、Cascading和Pig等工具使實現這些功能變得更加容易。
最後,需要為預計算的結果編制索引,以便應用程式可以快速訪問結果。有一類資料庫非常擅長於此。ElephantDB和Voldemort read-only專注於從 Hadoop 中匯出鍵/值資料以進行快速查詢。這些資料庫支援批次寫入和隨機讀取,不支援隨機寫入。隨機寫入導致資料庫的大部分複雜性,因此由於不支援隨機寫入,這些資料庫非常簡單。例如,ElephantDB 只有幾千行程式碼。這種簡單性導致這些資料庫非常健壯。
讓我們看一個批處理系統如何組合在一起的例子。假設正在構建一個跟蹤頁面瀏覽量的 Web 分析應用程式,並且希望能夠查詢任何時間段內的頁面瀏覽量,精確到一小時。
實現這個很容易。每個資料記錄都包含一個頁面檢視。這些資料記錄儲存在 HDFS 上的檔案中。按小時彙總每個 URL 的頁面瀏覽量的功能是作為一系列 MapReduce 作業實現的。該函式發出鍵/值對,其中每個鍵都是一[URL, hour]對,每個值都是頁面瀏覽量的計數。這些鍵/值對被匯出到 ElephantDB 資料庫中,以便應用程式可以快速獲取任何[URL, hour]對的值。當應用程式想知道某個時間範圍內的頁面瀏覽量時,它會向 ElephantDB 查詢該時間範圍內每小時的頁面瀏覽量,並將它們相加得到最終結果。
批處理可以計算任意資料上的任意函式,缺點是查詢會過時幾個小時。這種系統的“任意性”意味著它可以應用於任何問題。更重要的是,它簡單、易於理解並且完全可擴充套件。只需要從資料和功能的角度考慮,Hadoop 負責並行化。
批處理系統、CAP 和人為容錯
那麼批處理系統如何與 CAP 保持一致,它是否滿足我們的人類容錯目標?
讓我們從 CAP 開始。批處理系統以最極端的方式實現最終一致性:寫入總是需要幾個小時才能合併到查詢中。但這是一種易於推理的最終一致性形式,因為只需考慮資料和資料上的函式。無需考慮讀取修復、併發或其他複雜問題。
接下來,我們來看看批處理系統的人為容錯能力。批處理系統的人為容錯能力是你能得到的最好的。在這樣的系統中,人類只會犯兩個錯誤:部署有缺陷的查詢實現或寫入錯誤資料。
如果你部署了一個有問題的查詢實現,你要做的就是修復這個問題,部署修復後的版本,然後從主資料集中重新計算所有內容。這是可行的,因為查詢是純函式。
同樣,寫入壞資料有一條清晰的恢復路徑:刪除壞資料並再次預計算查詢。由於資料是不可變的並且主資料集是僅附加的,因此寫入錯誤資料不會覆蓋或以其他方式破壞良好資料。這與幾乎所有傳統資料庫形成鮮明對比,在傳統資料庫中,如果更新金鑰,就會丟失舊值。
請注意,MVCC和類似 HBase 的行版本控制並沒有接近這種水平的人為容錯。MVCC 和 HBase 行版本控制不會永遠保留資料:一旦資料庫壓縮行,舊值就消失了。只有不可變的資料集才能保證在寫入錯誤資料時有恢復路徑。
實時層
批處理解決方案几乎解決了實時計算任意資料的任意函式的完整問題。任何早於幾個小時的資料都已合併到批處理檢視中,因此剩下要做的就是補償最後幾個小時的資料。弄清楚如何對幾個小時的資料進行實時查詢比對完整資料集進行實時查詢要容易得多。這是一個重要的見解。
為了補償那幾個小時的資料,需要一個與批處理系統並行執行的實時系統。實時系統針對最近幾個小時的資料預先計算每個查詢函式。要解決查詢功能,查詢批處理檢視和實時檢視並將結果合併在一起以獲得最終答案。
實時層是使用讀/寫資料庫(如 Riak 或 Cassandra)的地方,實時層依賴於增量演算法來更新這些資料庫中的狀態。
用於實時計算的 Hadoop 模擬是Storm。Storm 是為了以一種可擴充套件且健壯的方式進行大量實時資料處理。Storm 對資料流進行無限計算,併為資料處理提供強有力的保證。
讓我們透過返回查詢某個時間範圍內 URL 的頁面瀏覽量的執行示例來檢視實時層的示例。
批處理系統與以前相同:基於 Hadoop 和 ElephantDB 的批處理工作流預先計算除最近幾個小時資料之外的所有內容的查詢。剩下的就是構建實時系統來補償最後幾個小時的資料。
我們會將過去幾個小時的統計資料彙總到 Cassandra 中,我們將使用 Storm 處理頁面瀏覽流並將更新並行化到資料庫中。[URL, hour]在 Cassandra 中,每次頁面瀏覽都會導致一個金鑰計數器遞增。這就是它的全部——Storm 使這些事情變得非常簡單。
批處理層 + 實時層、CAP 定理和人類容錯
在某些方面,我們似乎又回到了起點。實現實時查詢需要我們使用 NoSQL 資料庫和增量演算法。這意味著我們回到了不同值、向量時鐘和讀取修復的複雜世界。
但是有一個關鍵的區別。由於實時層僅補償最後幾個小時的資料,實時層計算的所有內容最終都會被批處理層覆蓋。因此,如果在實時層中犯了錯誤或出了什麼問題,批處理層會糾正它。所有這些複雜性都是短暫的。
這並不意味著不應該關心實時層中的讀取修復或最終一致性。仍然希望實時層儘可能保持一致。但是,當犯錯時,不會永久損壞資料。這減輕了巨大的複雜性負擔。
在批處理層,你只需要考慮資料和資料上的函式。批處理層的推理非常簡單。另一方面,在實時層,必須使用增量演算法和極其複雜的 NoSQL 資料庫。將所有這些複雜性隔離到實時層中,對於構建健壯、可靠的系統有很大的不同。
此外,實時層不會影響系統的人為容錯能力。批處理層中的 append-only 不可變資料集仍然是系統的核心,因此任何錯誤都可以像以前一樣從中恢復。
讓我們看一個關於在實時層中隔離複雜性的案例。有一個與這裡描述的系統非常相似的系統:用於批處理層的 Hadoop 和 ElephantDB,以及用於實時層的 Storm 和 Cassandra。由於監控不力,有一天發現 Cassandra 空間不足並且每次請求都超時。這導致 Storm 拓撲失敗,資料流在佇列中備份。相同的訊息不斷被重播並不斷失敗。
如果沒有批處理層,將不得不擴充套件和恢復 Cassandra。這很重要。更糟糕的是,由於多次重播相同的訊息,許多資料庫可能不準確。
幸運的是,所有這些複雜性都隔離在實時層中。將備份的佇列重新整理到批處理層並建立了一個新的 Cassandra 叢集。批處理層像發條一樣執行,幾個小時內一切恢復正常。沒有資料丟失,查詢也沒有不準確之處。
垃圾收集
我們描述的一切都建立在一個不變的、不斷增長的資料集的基礎上。那麼,如果的資料集太大以至於無法一直儲存所有資料,即使使用水平可擴充套件儲存,會怎麼做?這個用例是否破壞了所描述的一切?你應該回去使用可變資料庫嗎?
不。很容易用“垃圾收集”擴充套件基本模型來處理這個用例。垃圾收集只是一個函式,它接受主資料集並返回主資料集的過濾版本。垃圾收集擺脫了低價值的資料。可以使用任何想要的策略來進行垃圾回收。可以透過僅保留實體的最後一個值來模擬可變性,或者可以保留每個實體的歷史記錄。例如,如果要處理位置資料,可能希望每年為每個人保留一個位置以及當前位置。可變性實際上只是一種不靈活的垃圾收集形式,它與 CAP 定理的互動也很差。
垃圾收集是作為批處理任務實現的。這是偶爾執行的東西,也許每月一次。由於垃圾收集作為離線批處理任務執行,因此它不會影響系統與 CAP 定理的互動方式。
小結
使可伸縮資料系統變得困難的不是 CAP 定理。正是對增量演算法和可變狀態的依賴導致了我們系統的複雜性。隨著分散式資料庫的興起,這種複雜性才開始減弱。但這種複雜性一直存在。
批處理/實時架構還有許多其他功能。現在值得總結其中的一些:
演算法靈活性:一些演算法難以增量計算。例如,如果唯一值集變大,計算唯一值可能會很困難。批處理/實時拆分使可以靈活地在批處理層上使用精確演算法,在實時層上使用近似演算法。批處理層不斷覆蓋實時層,因此近似值得到糾正,系統表現出“最終準確性”的特性。
模式遷移很容易:由於批處理計算是系統的核心,因此很容易在完整的資料集上執行函式。這使得更改資料或檢視的模式變得容易。
輕鬆的臨時分析:批處理層的任意性意味著可以對資料執行任何喜歡的查詢。由於所有資料都可以在一個位置訪問,因此簡單方便。
自審計:透過將資料視為不可變的,可以獲得自審計資料集。資料集記錄了它自己的歷史。這對於人類容錯非常重要,它對於進行分析也非常有用。
批處理/實時架構具有很高的通用性,可以應用於任何資料系統。要提高我們解決大資料問題的集體能力,還有很多工作要做。以下是一些關鍵的改進領域:
批次可寫、隨機讀取資料庫的擴充套件資料模型:並非每個應用程式都受鍵/值資料模型支援。這就是為什麼我的團隊正在投資擴充套件 ElephantDB 以支援搜尋、文件資料庫、範圍查詢等。
更好的批處理原語:Hadoop 不是批處理計算的終極目標。對於某些型別的計算,它可能效率低下。Spark是一個重要的專案,在擴充套件 MapReduce 範例方面做了有趣的工作。
改進的讀/寫 NoSQL 資料庫:有更多具有不同資料模型的資料庫的空間,這些專案通常會從更成熟的過程中受益。
高階抽象:未來工作中最有趣的領域之一是對映到批處理元件和實時處理元件的高階抽象。沒有理由不讓宣告性語言的簡潔性和批處理/實時架構的健壯性結合起來。
很多人都想要一個可擴充套件的關聯式資料庫。大資料和 NoSQL 運動似乎使資料管理比 RDBMS 更復雜,但這只是因為我們試影像對待 RDBMS 資料一樣對待“大資料”:透過合併資料和檢視並依賴關於增量演算法。大資料的規模讓能夠以完全不同的方式構建系統。透過將資料儲存為一組不斷擴充套件的不可變事實並將重新計算構建到核心中,大資料系統實際上比關係系統更容易推理。
以上便是Lambda 架構的想法,架構如下圖所示:
它的工作方式是捕獲不可變的記錄序列並將其並行輸入到批處理系統和流處理系統中。實現轉換邏輯兩次,一次在批處理系統中,一次在流處理系統中。在查詢時將兩個系統的結果拼接在一起以產生完整的結果。
來自 “ 資料驅動智慧 ”, 原文作者:曉曉;原文連結:http://server.it168.com/a2022/1214/6780/000006780642.shtml,如有侵權,請聯絡管理員刪除。
相關文章
- 從 Spring Cloud 開始談一談微服務架構的實踐之路SpringCloud微服務架構
- 談談如何從資料湖(Data Lake)架構轉向資料網格(Data Mesh)架構架構
- 架構演化架構
- 淺談架構-從傳統走向分散式架構分散式
- iOS架構淺談從 MVC、MVP 到 MVVMiOS架構MVCMVPMVVM
- 談談關於 iOS 的架構以及應用iOS架構
- 阿里支付寶架構師:談談我眼中的高併發架構【好文】阿里架構
- 架構雜談《九》架構
- 架構雜談《八》架構
- 架構雜談《七》架構
- 架構雜談《六》架構
- 架構雜談《五》架構
- 架構雜談《二》架構
- 架構雜談《三》架構
- 架構雜談《四》架構
- InnoDB架構淺談架構
- 談談對資料架構的幾點認識架構
- 架構之:軟體架構漫談架構
- 架構之:微服務架構漫談架構微服務
- 2020 年,從架構談起,到 Mesh 結束架構
- 談談中臺架構之交易中臺架構
- 架構雜談(Android、Web)架構AndroidWeb
- 微服務架構詳談微服務架構
- iOS APP 架構漫談iOSAPP架構
- 淺談lambda表示式
- 視覺目標跟蹤漫談:從原理到應用視覺
- MVC、MVP、MVVM,談談我對Android應用架構的理解MVCMVPMVVMAndroid應用架構
- UI架構設計的演化UI架構
- 漫談Web快取架構Web快取架構
- 漫談計算機架構計算機架構
- 自營商城架構雜談架構
- 從點線面體談開發到架構師的轉型架構
- 談談人工智慧和機器學習的資料架構人工智慧機器學習架構
- 【淺談架構14/100】架構的緣起與目標架構
- 架構思想之CAP原理架構
- 微服務架構在阿里的演化微服務架構阿里
- 從一個優秀開源專案來談前端架構前端架構
- 淺談Android os體系架構Android架構