開源軟件近年來已變為構建一些大型網站的基礎組件。并且伴隨著網站的成長,圍繞著它們架構的最佳實踐和指導準則已經顯露。這篇文章旨在涉及一些在設計大型網站時需要考慮的關鍵問題和一些為達到這些目標所使用的組件。上篇文章介紹了Web分布式系統設計準則和基本原理,本文介紹構建快速、可伸縮數據訪問的組件。
(上文)談及了在設計分布式系統中需要考慮的一些核心問題,現在讓我們來聊聊(比較)困難的部分:訪問數據的可伸縮性。大多數簡單的web應用,例如LAMP棧應用,看上去如圖1.5
隨著它們的成長,會有兩個主要的挑戰:訪問應用服務器和數據庫的可伸縮性。在一個高可伸縮的應用設計中,應用(或者web)服務器通常會最小化(minimized)并通常表現為一個非共享(無狀態)架構。這樣使得系統的應用服務層能夠很好地進行伸縮。這樣數據的結果是,壓力被向下推到了數據庫服務器和相關(底層)支持服務;真正的伸縮和性能挑戰就在這一層起到作用。本章余下部分致力于(介紹)一些更加通用的策略和方法,通過更快的數據訪問使得這些類型的服務更加快速和可伸縮。
大多數系統可以極度簡化為像圖1.6這樣的。這是一個很好的開始。如果你有大量的數據且希望快速、簡單地訪問,就像你把糖果藏在你桌子第一個抽屜里。雖然被極度簡化,前面觀點仍暗示著兩個難題:存儲的可伸縮性和數據的快速訪問。
為了本節,我們假設你有數以TB計的數據并且希望能讓用戶隨機訪問這些數據的一小部分。(見圖1.7)這就類似于在圖片應用例子里定位文件服務器上一個圖片文件的位置。
由于很難將TB級的數據加載到內存,所以這會使得事情變得非常有挑戰性;這(種訪問)將直接變為磁盤IO操作。從磁盤讀取會比從內存要慢得多——訪問內存就像Chuck Norris一樣快,然而訪問磁盤比DMV線還要慢。這樣的速度差異對于大數據來說比較客觀(This speed difference really adds up for large data sets);順序讀方面訪問內存的速度是訪問磁盤的6倍,而在隨機讀方面,前者是后者的十萬倍(參見”The Pathologies of Big Data”, http://queue.acm.org/detail.cfm?id=1563874)。而且,即使有唯一ID,從哪里能夠找到這樣一小塊數據仍然是一項艱巨的任務。這就好比從你藏糖果的地方不看一眼地想拿到最后一塊Jolly Rancher。
幸運的是,你有很多能把事情變得更加容易的選擇;其中重要的有如下4個:緩存、代理、索引、負載均衡。本節剩余部分將會討論每個用于加速數據訪問的概念。
緩存
緩存利用了本地引用原則的好處:最近訪問的數據可能被再次訪問。緩存幾乎被用在計算機運行的各層:硬件,操作系統,web瀏覽器,web應用等等。緩存就像短期的內存:有著限定大小的空間,但通常比訪問原始數據源更快,并且包含有最近最多被訪問過的(數據)項。緩存可以存在于架構的各個層次,但會發現到經常更靠近前端(非web前端界面,架構上層),這樣就可盡快返回數據而不用經過繁重的下層(處理)了。
在我們的API例子中,如何使用一個緩存來加速你的數據訪問速度呢?在這個場景下,你可以在很多地方插入一個緩存。選擇之一是在你的請求層節點中插入一個緩存,如圖1.8.
將緩存直接放置在請求層節點中讓本地存儲響應數據變為可能。每次對于一個服務的請求,節點將立即返回存在的本地、緩存的數據。如果(對應的)緩存不存在,請求節點將會從磁盤中查詢數據。請求層節點的緩存既可以放置在內存(更快)也可以在節點本地磁盤(比通過網絡快)上。
當你擴展到多個節點時,會發生什么呢?正如你看到的圖1.9,如果請求曾擴展到多個節點,那么每個節點都可以擁有它自身的緩存。但是,如果你的負載均衡器將請求隨機分發到這些節點上,同樣的請求會到達不同的節點,就會提高緩存miss率。兩種克服這種困難的方法是:全局緩存和分布式緩存。
全局緩存
正如聽起來的一樣,全局緩存是指:所有節點使用同一緩存空間。這包括增加一臺服務器或是某種類型的文件存儲,比從你原始存儲地方(訪問)更快,并且所有請求層的節點均可以訪問(全局緩存)。所有請求節點統一像訪問其本地緩存般訪問(全局)緩存。這種類型的緩存機制可能會變得比較復雜,因為隨著客戶端和請求數量的增加,單個緩存(服務器)很容易被壓垮,但是在一些架構中非常有效(特別是有專門定制的硬件使得訪問全局緩存非常快速,或者需要緩存的數據集是固定的)。
通常有兩種形式的全局緩存,如下圖。圖1.10中,如果緩存中找不到對應的響應,那緩存自身會去從下層存儲中獲取丟失的數據。在圖1.11中,當緩存中找不到相應數據時,需要請求節點自己去獲取數據。
【譯者注】第一種方式相當于是全局緩存將查詢緩存、底層獲取數據、填充緩存這些操作一并做掉,理想情況下對于上層應用應該只需要提供一個獲取數據的API,上層應用無需關心所請求的數據是已存在于緩存中的還是從底層存儲中獲取的,能夠更專注于上層業務邏輯,但這就可能需要這種全局緩存設計成能夠根據傳入API接口的參數去獲取底層存儲的數據,譯者認為接口簽名可以簡化為Object getData(String uniqueId, DataRetrieveCallback callback),第一個參數代表與緩存約定的唯一標示一個數據的ID,第二個是一個獲取數據回調接口,具體實現由調用該接口的業務端來實現,即當全局緩存中未找到uniqueId對應的緩存數據時,那就會以該callback去獲取數據,并以uniqueId為key、callback獲取數據為value放入全局緩存中。第二種方式相對來說自由一些。請求節點自行根據業務場景需求來決定查詢數據的方式,以及查數據后的處理(比如緩存回收策略),全局緩存只作為一個基礎組件讓請求節點能夠在其中存取數據。
大多數應用傾向于通過第一種方式使用全局緩存,由緩存自身來管理回收、獲取數據,來應對從客戶端發起的對同一數據的眾多請求。但是,對于一些場景來說,第二種實現就比較有意義。比如,如果是用來緩存大型文件,那緩存低命中率將會導致緩存緩沖區被緩存miss給壓垮;在這種情況下,緩存中緩存大部分數據集(或熱門數據)將會有助解決這個問題。另一個例子是,一個架構中緩存的文件是靜態、不應回收的。(這可能跟應用對于數據延遲的需求有關——對于大數據集來說,某些數據段需要被快速訪問——這時應用的業務邏輯會比緩存更懂得回收策略或熱點處理。)
分布式緩存
在一個分布式緩存中(如圖1.12),沒個節點擁有部分緩存的數據,如果將雜貨店里的冰箱比作一個緩存,那么一個分布式緩存好比是將你的食物放在幾個不同的地方——你的冰箱、食物柜、午餐飯盒里——非常便于取到快餐的地方而無需跑一趟商店。通常這類緩存使用一致性Hash算法進行切分,這樣一個請求節點在查詢指定數據時,可以很快知道去哪里查詢,并通過分布式緩存來判斷數據可用性。這種場景下,每個節點都會擁有一部分緩存,并且會將請求傳遞到其他節點來獲取數據,最后才到原始地方查詢數據。因此,分布式緩存的一個優勢就是通過往請求池里增加節點來擴大緩存空間。
分布式緩存的一個缺點在于節點丟失糾正問題。一些分布式緩存通過將復制數據多份存放在不同的節點來解決這個問題;但是,你可以想象到這樣做會讓邏輯迅速變得復雜,特別是當你向請求層增加或減少節點的時候。雖然一個節點丟失并且緩存失效,但請求仍然可以從源頭來獲取(數據)——所以這不一定是最悲劇的。
緩存的偉大之處在于它們讓事情進行的更快(當然需要執行正確)。你所選擇的方法只是讓你能夠更快處理更多的請求。但是,這些緩存是以需要維護更多存儲空間為代價的,特別是昂貴的內存方式;天下沒有免費的午餐。緩存讓事情變得更快,同時還保證了高負載條件下系統的功能,否則(系統)服務可能早已降級。
一個非常受歡迎的開源緩存叫做Memcached(http://memcached.org/)(既可以是本地又可以是分布式緩存);但是,還有很多其他選擇(包括許多語言/框架特定選擇)。Memcached被應用于許多大型web網站,縱然它功能強大,但它簡單來說就是一個內存key-value存儲,對任意數據存儲和快速查找做了優化(時間復雜度O(1))。
Facebook使用了若干種不同類型的緩存以達到他們網站的性能(要求,參加see “Facebook caching and performance“)。他們在語言層面使用$GLOBALS和APC緩存(在PHP中提供的函數調用)使得中間功能調用和(得到)結果更加快速。(大多數語言都有這種類型的類庫來提高web性能,應該經常去使用。)Facebook使用一種全局緩存,分布在多臺服務器上(參見”Scaling memcached at Facebook“),這樣一個訪問緩存的函數調用就會產生很多并行請求來從Memcached服務器(集群)獲取數據。這使得他們能夠在用戶概況數據上獲得更高的性能和吞吐量,并且有一個集中的地方去更新數據(當你運行著數以千計的服務器時,緩存失效、管理一致性都將變得很有挑戰,所以這是很重要的)。
現在讓我們來聊聊當數據不存在于緩存的時候應該做什么。
代理
從基本層面來看,代理服務器是硬件/軟件的一個中間層,用于接收從客戶端發起的請求并傳遞到后端服務器。通常來說,代理是用來過濾請求、記錄請求日志或者有時對請求進行轉換(增加/去除頭文件,加密/解密或者進行壓縮)。
代理同樣能夠極大幫助協調多個服務器的請求,有機會從系統的角度來優化請求流量。使用代理來加快數據訪問速度的方式之一是將多個同種請求集中放到一個請求中,然后將單個結果返回到請求客戶端。這就叫做壓縮轉發(原文叫做collapsed forwarding)。
假設在幾個節點上存在對同樣數據的請求(我們叫它littleB),并且這份數據不在緩存里。如果請求通過代理路由,那么這些請求可以被壓縮為一個,就意味著我們只需要從磁盤讀取一次littleB即可。(見圖1.14)這種設計是會帶來一定的開銷,因為每個請求都會產生更高的延遲(跟不用代理相比),并且一些請求會因為要與相同請求合并而產生一些延遲。但這種做法在高負載的情況下提高系統性能,特別是當相同的數據重復被請求。這很像緩存,但不用像緩存那樣存儲數據/文件,而是優化了對那些文件的請求或調用,并且充當那些客戶端的代理。
例如,在局域網(LAN)代理中,客戶端不需有自己的IP來連接互聯網,而局域網會將對同樣內容的客戶端請求進行壓縮。這里可能很容易產生困惑,因為許多代理同樣也是緩存(因為在這里放一個緩存很合理),但不是所有緩存都能充當代理。
另一個使用代理的好方法是,不單把代理用來壓縮對同樣數據的請求,還可以用來壓縮對那些在原始存儲中空間上緊密聯系的數據(磁盤連續塊)的請求。使用這一策略最大化(利用)所請求數據的本地性,可以減少請求延遲。例如,我們假設一群節點請求B的部分(數據):B1, B2,等。我們可以對代理進行設置使其能夠識別出不同請求的空間局部性,將它們壓縮為單個請求并且只返回bigB,最小化對原始數據的讀取操作。(見圖1.15)當你隨機訪問TB級的數據時,這樣會大幅改變(降低)請求時間。在高負載情況下或者當你只有有限的緩存,代理是非常有幫助的,因為代理可以從根本上將若干個請求合并為一個。
你完全可以一并使用代理和緩存,但通常最好將緩存放在代理之前使用,正如在馬拉松賽跑中最好讓跑得快的選手跑在前面。這是因為緩存通過內存來提供數據非??焖伲⑶宜膊魂P心多個對同樣結果的請求。但如果緩存被放在代理服務器的另一邊(后面),那在每個請求訪問緩存前就會有額外的延遲,這會阻礙系統性能。
如果你在尋找一款代理想要加入到你的系統中,那有很多選擇可供考慮;Squid和Varnish都是經過路演并廣泛應用于很多網站的生產環境中。這些代理方案做了很多優化來充分使用客戶端與服務端的通信。安裝其中之一并在web服務器層將其作為一個反向代理(將在下面的負載均衡小節解釋)可以提高web服務器相當大的性能,降低處理來自客戶端的請求所消耗的工作量。
索引
使用索引來加快訪問數據已經是優化數據訪問性能眾所周知的策略;可能更多來自數據庫。索引是以增加存儲開銷和減慢寫入速度(因為你必須同時寫入數據并更新索引)的代價來得到更快讀取的好處。
就像對于傳統的關系數據庫,你同樣可以將這種概念應用到大數據集上。索引的訣竅在于你必須仔細考慮你的用戶會如何使用你的數據。對于TB級但單項數據比較小(比如1KB,原文這里寫的是small payload)的數據集,索引是優化數據訪問非常必要的方式。在一個大數據集中尋找一個小單元是非常困難的,因為你不可能在一個可接受的時間里遍歷這么大的數據。并且,像這么一個大數據集很有可能是分布在幾個(或更多)物理設備上——這就意味著你需要有方法能夠找到所要數據正確的物理位置。索引是達到這個的最好方法。
索引可以像一張可以引導你至所要數據位置的表格來使用。例如,我們假設你在尋找B的part2數據——你將如何知道到哪去找到它?如果你有一個按照數據類型(如A,B,C)排序好的索引,它會告訴你數據B在哪里。然后你查找到位置,然后讀取你所要的部分。(見圖1.16)這些索引通常存放在內存中,或者在更靠近客戶端請求的地方。伯克利數據庫(BDBs)和樹形數據結構經常用來有序地存儲數據,非常適合通過索引來訪問。
索引經常會有很多層,類似一個map,將你從一個地方引導至另一個,以此類推,直到你獲取到你所要的那份數據。(見圖1.17)
索引也可以用來對同樣的數據創建出一些不同的視圖。對于大數據集來說,通過定義不同的過濾器和排序是一個很好的方式,而不需要創建很多額外數據拷貝。
例如,假設之前的圖片托管系統就是在管理書頁上的圖片,并且服務能夠允許客戶端查詢圖片中的文字,按照標題搜索整本書的內容,就像搜索引擎允許你搜索HTML內容一樣。這種場景下,所有書中的圖片需要很多很多的服務器去存儲文件,查找到其中一頁渲染給用戶將會是比較復雜的。首先,對需要易于查詢的任意單詞、詞組進行倒排索引;然后挑戰在于導航至那本書具體的頁面、位置并獲取到正確的圖片。所以,在這一場景,倒排索引將會映射到一個位置(比如B書),然后B可能會包含每個部分的所有單詞、位置、出現次數的索引。倒排索引可能如同下圖——每個單詞或詞組會提供一個哪些書包含它的索引。
這種中間索引看上去都類似,僅會包含單詞、位置和B的一些信息。這種嵌套索引的架構允許每個索引占用更少的空間而非將所有的信息存放在一個巨大的倒排索引中。
在大型可伸縮的系統中,即使索引已被壓縮但仍會變得很大,不易存儲。在這個系統里,我們假設世界上有很多書——100,000,000本——并且每本書僅有10頁(為了便于計算),每頁有250個單詞,這就意味著一共有2500億個單詞。如果我們假設平均每個單詞有5個字符,每個字符占用8個比特,每個單詞5個字節,那么對于僅包含每個單詞的索引的大小就達到TB級。所以你會發現創建像一些如詞組、數據位置、出現次數之類的其他信息的索引將會增長得更快。
創建這些中間索引并且以更小的方式表達數據,將大數據的問題變得易于處理。數據可以分布在多臺服務器但仍可以快速訪問。索引是信息獲取的基石,也是當今現代搜索引擎的基礎。當然,這一小節僅僅是揭開表面,為了把索引變得更小、更快、包含更多信息(比如關聯)、無縫更新,還有大量的研究工作要做。(還有一些可管理性方面的挑戰,比如競爭條件、增加或修改數據所帶來的更新操作,特別是再加上關聯、scoring)
能夠快速、簡單地找到你的數據非常重要;索引是達到這一目標非常有效、簡單的工具。
負載均衡
另一個任何分布式系統的關鍵組件是負載均衡器。負載均衡器是任何架構的關鍵部分,用于將負載分攤在一些列負責服務請求的節點上。這使得一個系統的多個節點能夠為相同功能提供服務。(見圖1.18)它們主要目的是處理許多同時進行的連接并將這些連接路由到其中的一個請求節點上,使得系統能夠可伸縮地通過增加節點來服務更多請求。
有很多不同的用于服務請求的算法,包括隨機挑選一個節點、循環(round robin)或給予某些標準如內存/CPU使用率選取節點。一個廣泛使用的開源軟件級負載均衡器是HAProxy。
在一個分布式系統中,負責均衡器通常是放置在系統很前端的地方,這樣就能路由所有進入(系統)的請求。在一個復雜的分布式系統中,一個請求被多個負載均衡器路由也不是不可能。(見圖1.19)
如同代理一般,一些負載均衡器也能根據不同類型的請求進行路由。(從技術上來說,就是所謂的反向代理。)
負載均衡器的挑戰之一在于(如何)管理用戶session數據。在一個電子商務網站,當你只有一個客戶端時很容易讓用戶把東西放到他們的購物車并且在不同的訪問間保存(這是很重要的,因為當用戶回來時很有可能買放在購物車里的產品)。但是,如果一個用戶先被路由到一個session節點,然后在他們下次訪問時路由到另一個不同的節點,那將會因為新節點可能丟失用戶購物車里的東西而產生不一致。(如果你精心挑選了6包Mountain Dew放到購物車,但當你回來的時候發現購物車清空了,你會不會很沮喪?)解決辦法之一通過粘性session機制總是將用戶路由到同一節點,但這樣既很難享受到一些像自動failover的可靠機制了。在這一場景下,用戶的購物車總是會有東西的,如果他們所對應的粘性節點不可用了,那么就會是一個特殊情況對于(保存)在那里的東西的假設就無效了(當然我們希望這種假設不會出現在應用里)。當然,這個問題可以通過本章中的一些其他策略或者工具來解決,比如服務,還有一些沒有提到的(如瀏覽器緩存、cookie、URL地址重寫)。
【譯者注】上段中提到的用戶session問題,實際上在很多大型網站如淘寶、支付寶,都是通過一個分布式session的中間件來解決的。原理其實很簡單,比如用戶登錄了支付寶,那么系統會給當前用戶分配一個全局唯一的sessionId并寫入到瀏覽器的cookie中,在后臺服務端也會有專門的一個分布式存儲以sessionId為key開辟一個空間存放該用戶session數據。雖然應用都是集群部署方式,但每個無狀態應用節點都會統一連接到該分布式存儲。由于用戶session數據是統一保存在分布式存儲上,即對session數據的存取都是發生在同一個地方,而非各個節點內部,所以不會因為不同的請求路由到不同的應用節點上導致session數據不一致的情況。同時,這一方法不會像sticky session機制那樣限制了系統的可伸縮性。如果出現session存取的性能問題,那只需通過擴展后端分布式存儲即可解決。如果系統只是由少數節點構成的,那么像Round Robin DNS那樣的系統就更加明智,因為負責均衡器很貴而且增加了一層不必要的復雜度。當然在大型系統里有各種各樣的調度和負載均衡算法,包括簡單的像隨機選擇或循環方式,還有更加復雜的機制如考慮(系統)使用率和容量的。所有這些算法都分布化了流量和請求,并且提供像自動failover或者自動去除壞節點(當該節點失去響應后)這類對可靠性非常有幫助的工具。但是,這些先進特性也會使得問題診斷變得復雜化。比如,在一個高負載情況下,負載均衡器會去除掉那些變慢或者超時(由于請求過多)的節點,但這樣反而加重了其他節點的(惡劣)處境。在這些情況下,全面監控變得很重要,因為從全局來看系統的流量和吞吐量正在下降(由于各節點服務請求越來越少),但從節點個體來看正在達到極限。
負載均衡器是一個非常簡單能讓你提高系統容量的方法,并且像本文其他的技術一樣,在分布式系統架構中扮演者重要角色。負載均衡器還能用來判斷一個節點的健康度,這樣當一個節點失去響應或者過載時,得益于系統不同節點的冗余性,可以將其從請求處理池中去除。
至此,我們已經覆蓋了很多用于加快數據讀取的方法,另一個擴展數據層的重要部分是有效管理寫入操作。當系統比較簡單,系統處理負載很低,數據庫也很小,可以預見寫入操作是很快的;但是,在更加復雜的系統中,寫入操作的時間可能無法確定。例如,數據需要被寫入到不同服務器或索引的多個地方,或者系統負載很高。這些情況下,由于上面的原因,寫操作或者任何任務都會花費很長的時間,這時需要異步化系統才能提高系統的性能和可靠性;通常的方法之一是使用隊列。
假設在一個系統中,每個客戶端在請求遠程服務來處理任務。每個客戶端將其請求送至服務器,服務器盡可能快地完成這些任務并返回結果給相應的客戶端。在小型系統中,當一臺服務器(或者邏輯上的一個服務)可以盡快地服務到來的客戶端(請求),這種情況下(系統)工作會比較好。但是,當服務器接收到超過其處理能力的請求時,那每個客戶端都只能被迫等待其他客戶端請求完成才能得到響應。圖1.20描繪的就是一個同步請求的例子。
這種同步的方式將會嚴重降低客戶端性能;客戶端被強制等待,在請求被響應前什么都做不了。增加額外的服務器并不能解決這個問題;即使通過有效的負載均衡,依然難以保證最大化客戶端性能所需做的公平分配的工作。更進一步來說,當處理請求的服務器不可用或掛掉了,那么上游的客戶端同樣也會失敗。有效解決這個問題需要抽象化客戶端的請求和真正服務它所做的工作。
現在進入隊列環節。一個隊列,正如聽上去的,簡單來說就是當一個任務過來時,會被加入到隊列中,然后會有當前有能力處理(任務)的worker去取下一個任務來做。(見圖1.21。)這些任務可以是對數據庫的寫入操作,或是復雜一些的如生成文件的小型預覽圖。當一個客戶端將任務的請求提交到隊列后,它們不再需要被迫等待結果;取而代之的是,它們只需要確認請求被得到正確接收。當客戶端需要的時候,這個確認此后可以當做是任務結果的引用。
隊列使得客戶端能夠以異步的方式進行工作,至關重要地抽象了一個客戶端請求及其響應。另一方面,一個同步化系統不會區分請求和響應,因此就無法分開管理。在一個異步化系統里,客戶端提交任務請求,后端服務反饋一個收到任務的確認信息,并且客戶端可以定期地查看任務的狀態,一旦完成即可取得任務結果。在客戶端等待一個異步請求完成時,它可以自由地處理其他的工作,即使是發起對其他服務的異步請求。上面第二個就是分布式系統中采用隊列和消息的例子。
隊列還能提供對服務斷供/失敗的保護措施。比如,很容易創建一個健壯的隊列來重試那些由于服務器短暫失敗的服務請求。更好的是通過使用隊列來確保服務品質,而非將客戶端直接面對斷斷續續的服務,因為那樣會需要客戶端復雜且經常不一致的錯誤處理。
隊列是管理大型可伸縮分布式應用不同部分間通信的基礎,可以通過很多方式來實現。有一些開源的隊列如RabbitMQ, ActiveMQ, BeanstalkD,也有一些使用像Zookeeper的服務,還有像Redis那樣的數據存儲。
【譯者注】隊列是分布式系統異步化的一個關鍵基礎組件。在淘寶、支付寶這類大型分布式網站中應用廣泛。正如大家所知的雙十一、雙十二,這兩天用戶的請求可謂超級海量。拿支付寶來說,核心系統如支付、賬務,即使使用了很多技術方案來確保高性能、高可用,但面對數倍、數十倍于平時的請求量,依然捉急。在開發了一套分布式隊列基礎中間件后,網站的吞吐量、可用性得到了很大的提高。同時,對于隊列來說,除了將客戶端請求與服務端處理分離外,通過對隊列加上額外的一些特性,能夠起到非常大的作用。比如,在隊列上加入限流特性,當請求量大大超過后端服務處理能力時,可以采取丟棄請求的方式來保證系統、隊列不至于被海量請求壓垮;當請求量回到一定水平,再將限流放開。這種做法,正好滿足了系統對可用性、性能、可伸縮性、可管理性的要求。
總結
設計出能夠快速訪問大量數據的高效系統(的方法)是存在的,并且又很多非常棒的工具來幫助各種各樣的新應用來達到這一點。本章只覆蓋了少量例子,僅僅是掀開了面紗,但其實還有更多,并將繼續保持創新。