整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          淺談雙十一背后的支付寶LDC架構和其CAP分析

          淺談雙十一背后的支付寶LDC架構和其CAP分析

          背景

          本文曾獲阿里內網ATA“峰云之巔”獎。未經允許,不得轉載。本文曾被大量轉載和修改文章標題,給本人帶來了極大的傷害。

          一年一度的雙十一又要來了,自2008年雙十一以來,在每年雙十一超大規模流量的沖擊上,螞蟻金服都會不斷突破現有技術的極限。2010年雙11的支付峰值為2萬筆/分鐘,全天1280萬筆支付,這個數字到2017雙11時變為了25.6萬筆/秒,全天14.8億筆。在如此之大的支付TPS背后除了削峰等錦上添花的應用級優化,最解渴最實質的招數當數基于分庫分表的單元化了,螞蟻技術稱之為LDC(邏輯數據中心)。本文不打算討論具體到代碼級的分析,而是嘗試用最簡單的描述來說明其中最大快人心的原理。我想關心分布式系統設計的人都曾被下面這些問題所困擾過:

          • 支付寶海量支付背后最解渴的設計是啥?換句話說,實現支付寶高TPS的最關鍵的設計是啥?
          • LDC是啥?LDC怎么實現異地多活和異地災備的?
          • CAP魔咒到底是啥?P到底怎么理解?
          • 什么是腦裂?跟CAP又是啥關系?
          • 什么是PAXOS,什么是Quorum法定數量,它們解決了啥問題?
          • PAXOS和CAP啥關系?PAXOS可以逃脫CAP魔咒么?
          • Oceanbase能逃脫CAP魔咒么?

          如果你對這些感興趣,不妨看一場赤裸裸的論述,拒絕使用晦澀難懂的詞匯,直面最本質的邏輯。

          本文提及的所有關于支付寶和螞蟻的技術點均為網絡采集,未經親自核實,請謹慎參考。如有涉及關鍵技術泄露請指明具體細節,并提供證據,比如到底是哪個點侵害了您的哪個權益。尤其是支付寶的公關和法務同學,不要聽風就是雨,本文曾被大量轉載和修改名稱,尤其會攜帶一些“大廠揭秘”等具有誤導性的詞匯,請保持理性閱讀,過腦分析。按照個人的理解,本文不涉及任何關鍵技術細節,也不會給貴司帶來任何損失和負面作用。

          2 LDC和單元化

          LDC(logic data center)是相對于傳統的(Internet Data Center-IDC)提出的,邏輯數據中心所表達的中心思想是無論物理結構如何的分布,整個數據中心在邏輯上是協同和統一的。這句話暗含的是強大的體系設計,分布式系統的挑戰就在于整體協同工作(可用性,分區容忍性)和統一(一致性)。

          單元化是大型互聯網系統的必然選擇趨勢,舉個最最通俗的例子來說明單元化。我們總是說TPS很難提升,確實任何一家互聯網(比如淘寶、攜程、新浪)它的交易TPS頂多以十萬計量(平均水平),很難往上串了,因為數據庫存儲層瓶頸的存在再多水平擴展的服務器都無法繞開這個瓶頸,而從整個互聯網的視角看,全世界電商的交易TPS可以輕松上億。

          這個例子帶給我們一些思考:為啥幾家互聯網的TPS之和可以那么大,服務的用戶數規模也極為嚇人,而單個互聯網的TPS卻很難提升?究其本質,每家互聯網都是一個獨立的大型單元,他們各自服務自己的用戶互不干擾。這就是單元化的基本特性,任何一家互聯網公司,其想要成倍的擴大自己系統的服務能力,都必然會走向單元化之路,它的本質是分治,我們把廣大的用戶分為若干部分,同時把系統復制多份,每一份都獨立部署,每一份系統都服務特定的一群用戶,以淘寶舉例,這樣之后,就會有很多個淘寶系統分別為不同的用戶服務,每個淘寶系統都做到十萬TPS的話,N個這樣的系統就可以輕松做到N*十萬的TPS了。

          LDC實現的關鍵就在于單元化系統架構設計,所以據說在螞蟻金服內部,LDC和單元化是不分家的,這也是很多同學比較困擾的地方,看似沒啥關系,實則是單元化體系設計成就了LDC。

          小結:分庫分表解決的最大痛點是數據庫單點瓶頸,這個瓶頸的產生是由現代二進制數據存儲體系決定的(即I/O速度)。單元化只是分庫分表后系統部署的一種方式,這種部署模式在災備方面也發揮了極大的優勢。

          2.1 系統架構演化史

          幾乎任何規模的互聯網公司,都有自己的系統架構迭代和更新,大致的演化路徑都大同小異。最早一般為了業務快速上線,所有功能都會放到一個應用里,系統架構如圖1所示。

          這樣的架構顯然是有問題的,單機有著明顯的單點效應,單機的容量和性能都是很局限的,而使用中小型機會帶來大量的浪費。 隨著業務發展,這個矛盾逐漸轉變為主要矛盾,因此工程師們采用了以下架構。

          通過對應用進行水平擴展可以大幅度提高服務器容量,這時候應用代碼還是一份,里面五臟俱全,只是應用代碼運行在多個服務器實例上。這也是整個公司第一次觸碰到分布式,它的理論基礎是將多個微機的計算能力團結起來,其效果可以完勝同等價格的中小型機器。隨著業務進一步發展,用量到達了百萬級,慢慢的大家發現,應用服務器CPU都很正常了,但是還是有很多慢請求,究其原因,是因為單點數據庫帶來了性能瓶頸。于是程序員們決定使用主從結構的數據庫集群,如下圖所示。

          其中大部分讀操作可以直接訪問從庫,從而減輕主庫的壓力。然而這種方式還是無法解決寫瓶頸,寫依舊需要主庫來處理,當業務量量級再次增高時,寫已經變成刻不容緩的待處理瓶頸。這時候,分庫分表方案出現了。

          分庫分表不僅可以對相同的庫進行更細的業務拆分,還可以進行對同一張表進行拆分,對表進行拆分的方式叫做水平拆分。原本在同一個庫中不同功能表拆分后放到不同的數據庫中,這種方式對應的是垂直拆分(按照業務功能進行拆分),此時一般還對應了微服務化。分庫分表往往按照用戶ID進行散列,不同用戶的數據訪問請求會在固定的不同數據庫實例上進行處理,這種方法做到極致基本能支撐TPS在萬級甚至更高的訪問量了。

          然而這種模式下需要每個擴展出的應用都連接所有的數據庫實例以保證任意用戶的請求在本應用都能進行處理,隨著應用擴展的越多,每個數據庫實例的連接數也成倍增長,而數據庫服務器的連接數量一般是恒定的,因此無法跟著可以無限擴容的應用服務實例增長而同比增長,這種機制是出于對數據庫的保護而設計的。

          事實上即便數據庫連接池可以無限增長,應用服務也會花費大量的性能消耗在維護與每個分庫的數據庫連接池上。比如在16個分庫200臺水平擴展應用的情況下,若每個應用與數據庫的連接池在100個,那么每臺應用上的數據庫連接數量將高達1600個,每臺數據庫服務實例上維護的連接數量達20000個。雖然對于任意一臺數據庫來說不代表同時會有20000個連接在傳輸數據,但由于系統實際運行時的各種不可預期的復雜調用(比如原先估計同時最多只有200個連接在讀寫數據——根據數據庫服務的容量進行合理計算得出,實際上一旦將最大連接數設置為20000個連接,同時讀寫的連接數超過200會變得非常常見)和請求流轉以及數據庫服務器中連接調度的性能消耗,這20000個始終保持存活的連接將逐步成為數據庫訪問的瓶頸,從而影響整個系統。

          Mysql等數據庫的主流用法中都是在數據訪問期間獨占連接,而不是像HTTP2.0那樣允許多路復用的純異步訪問,這是由于SQL語句的執行必須要符合順序性,否則一致性將被破壞。對于那些完全不需要一致性的業務(作者也舉不出來例子但應該是存在的),可能可以使用異步執行模式,在那種情況下數據庫連接瓶頸的問題或可以得到解決。

          從本質上看,這種模式的資源隔離性還不夠徹底,總會存在資源間互相污染的情況,這些污染最終會變現為系統的急劇熵增。要徹底讓資源進行隔離,就需要按照某種規則將網絡訪問用戶進行分流,讓一套服務和數據庫為某個特定群體進行服務——就像每個電商平臺一樣,這樣一來凡是來自這個用戶的所有請求都會由其對應的應用服務和數據庫實例來處理,而不會影響其他的服務和數據庫。這樣帶來另外一個變化就是識別用戶分庫的邏輯需要往上層移動——從數據庫層向上移動到路由網關層。當網關識別到A用戶時通過散列算法將其分配到對應的應用服務器(Server-A),最后數據持久化到對應的數據庫實例DB-1,因此Server-A也不再需要鏈接其他的數據庫實例了,如此一個單元化的雛形就誕生了。

          如上圖所示,但我們把整套系統打包為單元化時,每一類的數據從進單元開始就注定在這個單元被消化,由于這種徹底的隔離性,整個單元可以輕松的部署到任意機房而依然能保證邏輯上的統一。下圖為一個三地五機房的部署方式。

          2.2 支付寶單元化架構實踐

          螞蟻支付寶應該是國內最大的支付工具,其在雙十一等活動日當日的支付TPS可達幾十萬級,未來這個數字可能會更大,這決定了螞蟻單元化架構從容量要求上看必然從單機房走向多機房。另一方面,異地災備也決定了這些IDC機房必須是異地部署的。 整體上支付寶也采用了三地五中心(IDC機房)來保障系統的可用性[7],跟2.1中描述的有所不同的是,支付寶將單元分成了三類(也稱CRG架構)[8]:

          • RZone(Region Zone):直譯可能有點反而不好理解。實際上就是所有可以分庫分表的業務系統整體部署的最小單元。每個RZone連上數據庫就可以撐起一片天空,把業務跑的溜溜的。
          • GZone(Global Zone):全局單元,意味著全局只有一份。部署了不可開創副本服務和數據,比如匯率、優惠政策、商品價格、系統配置等。實際情況下,GZone異地也會部署,不過僅是用于災備,同一時刻,只有一地GZone進行全局服務。GZone一般被RZone依賴,提供的大部分是讀取服務。
          • CZone(City Zone):顧名思義,這是以城市為單位部署的單元。同樣部署了不可拆分的數據和服務,比如用戶賬號服務,客戶信息服務等。理論上CZone會被RZone以比訪問GZone高很多的頻率進行訪問。CZone是基于特定的GZone場景進行優化的一種單元,它把GZone中有些有著”寫讀時間差現象”的數據和服務進行了的單獨部署,這樣RZone只需要訪問本地的CZone即可,而不是訪問異地的GZone。

          “寫讀時間差現象”是架構師們根據實踐統計總結的,他們發現大部分情況下,一個數據被寫入后,都會過足夠長的時間后才會被訪問。生活中這種例子很常見,我們辦完銀行卡后可能很久才會存第一筆錢;我們創建微博賬號后,可能想半天才會發微博;我們下載創建淘寶賬號后,可能得瀏覽好幾分鐘才會下單買東西。當然了這些例子中的時間差遠遠超過了系統同步時間。一般來說異地的延時在100ms以內,所以只要滿足某地CZone寫入數據后100ms以后才用這個數據,這樣的數據和服務就適合放到CZone中。

          相信大家看到這都會問:為啥分這三種單元?其實其背后對應的是不同性質的數據,而服務不過是對數據的操作集。下面我們來根據數據性質的不同來解釋支付寶的CRG架構。當下幾乎所有互聯網公司的分庫分表規則都是根據用戶ID來制定的,而圍繞用戶來看整個系統的數據可以分為以下兩類:用戶專屬型數據和用戶間共享型數據。

          注:網上和支付寶內部有另外一些分法,比如流水型和狀態性,有時候還會分為三類:流水型、狀態型和配置型。

          (1)用戶專屬型數據

          代表只有用戶自己會訪問的數據,典型的有用戶的訂單、用戶發的評論、用戶的行為記錄等。這些數據都是用戶行為產生的流水型數據,具備天然的用戶隔離性,比如A用戶的App上絕對看不到B用戶的訂單列表。所以此類數據非常適合分庫分表后獨立部署服務,即按照用戶ID散列部署為RZone。

          (2)用戶共享型數據

          代表所有的用戶都可能訪問的數據。由于數據可能會被所有用戶訪問,將提供這類數據的服務部署到RZone就變得不合適了,因為RZone可能被分布在全國各地,高頻的遠程調用并不經濟。用戶共享型數據又可以進一步分為以下幾類:

          • 時間差弱感型數據:這些數據往往對一致性的實時性不敏感,比如上海的某個用戶寫了一條評論,一般并不會強調廣東深圳的另一個用戶與上海的其他用戶需要同時并立即看到這條評論。其他類似的還有賬號、個人博客、商品等。由于對時間差不敏感,這類數據的提供服務往往被部署到CZone中,
          • 時間差敏感型數據:代表這些數據對時間差很敏感,不允許出現過期副本,比如不允許超賣的庫存,公共賬戶余額等。這些數據對應的服務一般會部署為GZone服務,每一次數據訪問都會路由到全局唯一的數據庫中進行處理。GZone中的服務在多地部署的架構下調用是低效的,幸運的是這種類型的數據在所有業務數據中只占很小的一部分。
          • 低頻訪問的配置型數據:系統配置是共享型數據中比較常見的一種類型,有些配置是時間差敏感的有些則是弱感的,但即便是時間差弱感型數據,我們常常也并不會因此將其與其他配置分開部署到RZone或者CZone中(特殊情況下有必要也只能部署到RZone或CZone),因為大部分配置數據都是低頻訪問的,比如運營后臺配置的活動數據等,這些更新可能在運動開始前被RZone服務加載后就不再高頻訪問了,放在GZone更為方便和節約成本。作者在支付寶所搭建的價格工廠用于制定各項業務的收費規則,它也被部署到了GZone中,因為金融產品的收費規則的使用方往往都不是高并發入口,甚至有些是離線業務。

          以上只是從數據特性上做了分類,具體的案例場景并不具備必然的參考意義,具體使用什么方式部署,需要結合業務場景進行判斷。即便在支付寶的實際應用中,各個系統仍然存在不合理的CRG分類,尤其是CG不分的現象很常見。為了避免過度設計,作者建議共享型數據服務一開始可以優先考慮放到GZone中,當逐步發現瓶頸時再對數據做劃分,逐步引入CZone。

          3 LDC單元化的異地多活和災備

          3.1 流量挑撥技術探秘簡介

          單元化后,異地多活從未如此簡單——只需要在多地進行單元部署而已。比如上海的兩個單元負責用戶ID范圍為[00~19],[40~59]的用戶服務,而杭州的兩個單元為ID為[20~39]和[60,79]的用戶服務,這樣上海和杭州就是異地雙活的。

          支付寶對單元化的基本要求是每個單元都具備服務所有用戶的能力,以應對不可預期的天災人禍,即具體的哪個單元服務哪些用戶是可以動態配置的。所以異地雙活的這些單元還充當了彼此的備份。

          發現工作中冷備熱備已經被用的很亂了。最早冷備是指數據庫在備份數據時需要關閉后進行備份(也叫離線備份),防止數據備份過程中又修改了,不需要關閉即在運行過程中進行數據備份的方式叫做熱備(也叫在線備份)[9]。也不知道從哪一天開始,冷備在主備系統里代表了這臺備用機器是關閉狀態的,只有主服務器掛了之后,備服務器才會被啟動;而相同的熱備變成了備服務器也是啟動的,只是沒有流量而已,一旦主服務器掛了之后,流量自動打到備服務器上。本文不打算用第二種理解,因為感覺有點野、、、

          為了做到每個單元訪問哪些用戶變成可配置,支付寶要求單元化管理系統具備流量到單元的可配置以及單元到DB的可配置能力,如下圖所示:

          其中spanner是基于nginx自研的反向代理網關,也很好理解,有些請求我們希望在反向代理層就被轉發至其他IDC的spanner而無需進入后端服務,如圖箭頭2所示。那么對于應該在本IDC處理的請求,就直接映射到對應的RZ即可,如圖箭頭1。進入后端服務后,理論上如果請求只是讀取用戶專屬型數據,那么一般不會再進行路由了。然而對于有些場景來說,A用戶的一個請求可能關聯了對B用戶數據的訪問,比如A轉賬給B,A扣完錢后要調用賬務系統去增加B的余額。這時候就涉及到再次的路由,同樣有兩個結果:跳轉到其他IDC(如圖箭頭3)或是跳轉到本IDC的其他RZone(如圖箭頭4)。

          RZone到DB數據分區的訪問這是事先配置好的,上圖中RZ和DB數據分區的關系為:

          RZ0* --> a
          RZ1* --> b
          RZ2* --> c
          RZ3* --> d
          

          下面我們舉個例子來說明整個流量挑撥的過程,假設C用戶所屬的數據分區是c,而C用戶在杭州訪問了cashier.xxx.com(隨便編的)。

          (1)首先默認會按照地域來路由流量,具體的實現承載者是全局負載均衡GLSB(Global Server Load Balancing),它會根據請求者的IP,自動將cashier.xxx.com解析為杭州IDC的IP地址(或者跳轉到IDC所在的域名)。自己搞過網站的同學應該知道大部分DNS服務商的地址都是靠人去配置的,GLSB屬于動態配置域名的系統,網上也有比較火的類似產品,比如花生殼之類(建過私站的同學應該很熟悉)的。

          (2)好了,到此為止,用戶的請求來到了IDC-1的Spanner集群服務器上,Spanner從內存中讀取到了路由配置,知道了這個請求的主體用戶C所屬的RZ3*不在本IDC,于是直接轉到了IDC-2進行處理。

          (3)進入IDC-2之后,根據流量配比規則,該請求被分配到了RZ3B進行處理。

          (4)RZ3B得到請求后對數據分區c進行訪問。

          (5)處理完畢后原路返回。

          大家應該發現問題所在了,如果再來一個這樣的請求,豈不是每次都要跨地域進行調用和返回體傳遞?確實是存在這樣的問題的,對于這種問題,支付寶架構師們決定繼續把決策邏輯往用戶終端推移。比如,每個IDC機房都會有自己的域名(真實情況可能不是這樣命名的): IDC-1對應cashieridc-1.xxx.com IDC-2對應cashieridc-2.xxx.com 那么請求從IDC-1涮過一遍返回時會將前端請求跳轉到cashieridc-2.xxx.com去(如果是APP,只需要替換rest調用的接口域名),后面所有用戶的行為都會在這個域名上發生,就避免了走一遍IDC-1帶來的延時。

          3.2 災備機制

          流量挑撥是災備切換的基礎和前提條件,發生災難后的通用方法就是把陷入災難的單元的流量重新打到正常的單元上去,這個流量切換的過程俗稱切流。支付寶LDC架構下的災備有三個層次:

          • 同機房單元間災備。
          • 同城機房間災備。
          • 異地機房間災備。

          (1)同機房單元間災備

          災難發生可能性相對最高(但其實也很小)。對LDC來說,最小的災難就是某個單元由于一些原因(局部插座斷開、線路老化、人為操作失誤)宕機了。從3.1節里的圖中可以看到每組RZ都有A,B兩個單元,這就是用來做同機房災備的,并且AB之間也是雙活雙備的,正常情況下AB兩個單元共同分擔所有的請求,一旦A單元掛了,B單元將自動承擔A單元的流量份額。這個災備方案是默認的。

          (2) 同城機房間災備

          災難發生可能性相對更小。這種災難發生的原因一般是機房電線網線被挖斷,或者機房維護人員操作失誤導致的。在這種情況下,就需要人工的制定流量挑撥(切流)方案了。下面我們舉例說明這個過程,如下圖所示為上海的兩個IDC機房。

          整個切流配置過程分兩步,首先需要將陷入災難的機房中RZone對應的數據分區的訪問權配置進行修改;假設我們的方案是由IDC-2機房的RZ2和RZ3分別接管IDC-1中的RZ0和RZ1。那么首先要做的是把IDC2中的數據分區a,b對應的訪問權從RZ0和RZ1收回。分配給RZ2和RZ3。即將(如上圖所示為初始映射):

          RZ0* --> a
          RZ1* --> b
          RZ2* --> c
          RZ3* --> d
          

          變為:

          RZ0* --> /
          RZ1* --> /
          RZ2* --> a
          RZ2* --> c
          RZ3* --> b
          RZ3* --> d
          

          然后再修改用戶ID和RZ之間的映射配置。假設之前為:

          [00-24] --> RZ0A(50%),RZOB(50%)
          [25-49] --> RZ1A(50%),RZ1B(50%)
          [50-74] --> RZ2A(50%),RZ2B(50%)
          [75-99] --> RZ3A(50%),RZ3B(50%)
          

          那么按照災備方案的要求,這個映射配置將變為:

          [00-24] --> RZ2A(50%),RZ2B(50%)
          [25-49] --> RZ3A(50%),RZ3B(50%)
          [50-74] --> RZ2A(50%),RZ2B(50%)
          [75-99] --> RZ3A(50%),RZ3B(50%)
          

          這樣之后,所有流量將會被打到IDC-2中,期間部分已經向IDC-1發起請求的用戶會收到失敗并重試的提示。 實際情況中,整個過程并不是災難發生后再去做的,整個切換的流程會以預案配置的形式事先準備好,推送給每個流量挑撥客戶端(集成到了所有的服務和spanner中)。

          這里可以思考下,為何先切數據庫映射,再切流量呢?這是因為如果先切流量,意味著大量注定失敗的請求會被打到新的正常單元上去,從而影響系統的穩定性(數據庫還沒準備好)。

          (2) 異地機房間災備

          這個基本上跟同城機房間災備一致(這也是單元化的優點),不再贅述。

          4 LDC單元化架構的CAP分析

          4.1 回顧CAP

          4.1.1 CAP的定義

          CAP原則是指任意一個分布式系統,同時最多只能滿足其中的兩項,而無法同時滿足三項。所謂的分布式系統,說白了就是一件事一個人做的,現在分給好幾個人一起干。我們先簡單回顧下CAP各個維度的含義[1]:

          • Consistency(一致性),這個理解起來很簡單,就是每個操作結束后節點上的同一份數據都是一致的。保證一致性往往有兩種方法,一種是要求任何更新都是原子的,即要么全部成功,要么全部失敗。由于分布式系統之間無法做到像單機一樣對系統資源的全把控能力,保證整個分布式系統的原子性也變得十分奢侈。另外一種一致性保障方法是通過分布式仲裁的方法,比如CouchDB等數據庫,當數據更新發現沖突時,將通過預先設置的規則進行分布式仲裁,最終得到一個全局一致的數據。
          • Availability(可用性),這個可用性看起來很容易理解,但真正說清楚的不多。我更愿意把可用性解釋為:任意時刻系統都可以提供讀寫服務。那么舉個例子,當我們用事務將所有節點鎖住來進行某種寫操作時,如果某個節點發生不可用的情況,會讓整個系統不可用。對于分片式的NoSQL中間件集群(Redis,Memcached)來說,一旦一個分片歇菜了,整個系統的數據也就不完整了,讀取宕機分片的數據就會沒響應,也就是不可用了。需要說明一點,對于選擇CP的分布式系統來說并不代表可用性就完全沒有了,只是可用性沒有保障了。為了增加可用性保障,這類中間件往往都提供了”分片集群+復制集”的方案。
          • Partition tolerance(分區容忍性),這個可能也是很多文章都沒說清楚的。P并不是像CA一樣是一個獨立的性質,它依托于CA來進行討論。參考文獻[1]中解釋道:”除非整個網絡癱瘓,否則任何時刻系統都能正常工作”,言下之意是小范圍的網絡癱瘓,節點宕機,都不會影響整個系統的CA。我感覺這個解釋聽著還是有點懵逼,所以個人更愿意解釋為”當節點之間網絡不通時(出現網絡分區),可用性和一致性仍然能得到保障”。按筆者的理解,分區容忍性又分為”可用性分區容忍性”和”一致性分區容忍性”。”出現分區時會不會影響可用性”的關鍵在于”需不需要所有節點互相溝通協作來完成一次事務”,不需要的話是鐵定不影響可用性的,慶幸的是應該不太會有分布式系統會被設計成完成一次事務需要所有節點聯動,一定要舉個例子的話,全同步復制技術下的Mysql是一個典型案例[2]。”出現分區時會不會影響一致性”的關鍵則在于出現腦裂時有沒有保證一致性的方案,這對主從同步型數據庫(MySQL、SQL Server)是致命的,一旦網絡出現分區,產生腦裂,系統會出現一份數據兩個值的狀態,誰都不覺得自己是錯的。需要說明的是,正常來說同一局域網內,網絡分區的概率非常低,這也是為啥我們最熟悉的數據庫(MySQL、SQL Server等)也是不考慮P的原因。

          下圖為CAP之間的經典關系圖:

          還有個需要說明的地方,“分布式系統不可能同時滿足CAP“的前提條件是這個分布式系統一定是有讀有寫的,如果只考慮讀,那么CAP很容易都滿足,比如一個計算器服務,接受表達式請求,返回計算結果,搞成水平擴展的分布式,顯然這樣的系統沒有一致性問題,網絡分區也不怕,可用性也是很穩的,所以可以滿足CAP。而對于需要分布式讀寫的系統則符合CAP約束,分布式讀寫意味著在A點寫入的數據,在B點也可以讀取,這樣的系統無法在任一時刻同時滿足CAP。

          4.1.2 CAP分析方法

          先說下CA和P的關系,如果不考慮P的話,系統是可以輕松實現CA的。而P并不是一個單獨的性質,它代表的是目標分布式系統有沒有對網絡分區的情況做容錯處理。如果做了處理,就一定是帶有P的,接下來再考慮分區情況下到底選擇了A還是C。所以分析CAP,建議先確定有沒有對分區情況做容錯處理。 以下是個人總結的分析一個分布式系統CAP滿足情況的一般方法:

          if( 不存在分區的可能性 || 分區后不影響可用性或一致性 || 有影響但考慮了分區情況-P){
              if(可用性分區容忍性-A under P))
                return "AP";
              else if(一致性分區容忍性-C under P)
                return "CP";
          }
          else{  //分區有影響但沒考慮分區情況下的容錯
               if(具備可用性-A && 具備一致性-C){
                   return AC;
               }
          }
          
          

          這里說明下,如果考慮了分區容忍性,就不需要考慮不分區情況下的可用性和一致性了(大多是滿足的)。

          4.2 水平擴展應用+單數據庫實例的CAP分析

          讓我們再來回顧下分布式應用系統的來由,早年每個應用都是單體的,跑在一個服務器上,服務器一掛,服務就不可用了。另外一方面,單體應用由于業務功能復雜,對機器的要求也逐漸變高,普通的微機無法滿足這種性能和容量的要求。所以要拆!還在IBM大賣小型商用機的年代,阿里巴巴就提出要以分布式微機替代小型機。所以我們發現,分布式系統解決的最大的痛點,就是單體單機系統的可用性問題。要想高可用,必須分布式。 一家互聯網公司的發展之路上,第一次與分布式相遇應該都是在單體應用的水平擴展上。也就是同一個應用啟動了多個實例,連接著相同的數據庫(為了簡化問題,先不考慮數據庫是否單點),如下圖所示。

          這樣的系統天然具有的就是AP(可用性和分區容忍性),一方面解決了單點導致的低可用性問題,另一方面無論這些水平擴展的機器間網絡是否出現分區,這些服務器都可以各自提供服務,因為他們之間不需要進行溝通。然而,這樣的系統是沒有一致性可言的,當每個實例都可以往數據庫insert和update(注意這里還沒討論到事務)時,數據就亂套了。

          于是我們轉向了讓DB去做這個事,這時候”數據庫事務”就被用上了。用大部分公司會選擇的Mysql/MariaDB來舉例,用了事務之后會發現數據庫又變成了單點和瓶頸。單點就像單機一樣(本例子中不考慮從庫模式),理論上就不叫分布式了,如果一定要分析其CAP的話,根據4.1.2的步驟分析過程應該是這樣的:

          • 分區容忍性:先看有沒有考慮分區容忍性,或者分區后是否會有影響。單臺MySQL無法構成分區,要么整個系統掛了,要么就活著。
          • 可用性分區容忍性:假設單節點是分布式的一種特例,并且分區情況下恰好是該節點掛了,這時候沒有其他節點提供服務,系統也就不可用了,所以可用性分區容忍性不滿足。
          • 一致性分區容忍性:假設單節點是分布式的一種特例,并且分區情況下恰好是該節點掛了,這時候沒有其他節點提供服務,也不存在一致性的問題,單點單機的最大好處就是一致性可以得到保障。

          因此這樣的一個系統,個人認為只是滿足了CP。A有但不出色,從這點可以看出,CAP并不是非黑即白的。包括常說的BASE[3](最終一致性)方案,其實只是C不出色,但最終也是達到一致性的,BASE在一致性上選擇了退讓。

          關于分布式應用+單點數據庫的模式算不算純正的分布式系統,這個可能每個人看法有點差異,上述只是我個人的一種理解,是不是分布式系統不重要,重要的是分析過程。其實我們討論分布式,就是希望系統的可用性是多個系統多活的,一個掛了另外的也能頂上,顯然單機單點的系統不具備這樣的高可用特性。所以在我看來,廣義的說CAP也適用于單點單機系統,單機系統是CP的。說到這里,大家似乎也發現了,水平擴展的服務應用+數據庫這樣的系統的CAP魔咒主要發生在數據庫層,因為大部分這樣的服務應用都只是承擔了計算的任務(像計算器那樣),本身不需要互相協作,所有寫請求帶來的數據的一致性問題下沉到了數據庫層去解決。想象一下,如果沒有數據庫層,而是應用自己來保障數據一致性,那么這樣的應用之間就涉及到狀態的同步和交互了,分布式鎖就是一個典型的例子。

          4.3 水平擴展應用+主從數據庫集群的CAP分析

          上一節我們討論了多應用實例+單數據庫實例的模式,這種模式是分布式系統也好,不是分布式系統也罷,整體是偏CP的。現實中,技術人員們也會很快發現這種架構的不合理性——可用性太低了。于是如下圖所示的模式成為了當下大部分中小公司所使用的架構:

          從上圖我可以看到三個數據庫實例中只有一個是主庫,其他是從庫。一定程度上,這種架構極大的緩解了”讀可用性”問題,而這樣的架構一般會做讀寫分離來達到更高的”讀可用性”,幸運的是大部分互聯網場景中讀都占了80%以上,所以這樣的架構能得到較長時間的廣泛應用。對于”寫可用性”方面主備模式可以采用keepalived[4]等HA(高可用)框架來保證主庫的存活性,但實質上這種方式并沒有帶來性能上的可用性提升,只是保證了宕機情況下的可用性,至少系統不會因為某個實例掛了就都不可用了。該分布式系統可用性勉強達標了,具體的CAP分析如下:

          • 分區容忍性:依舊先看分區容忍性,主從結構的數據庫存在節點之間的通信,他們之間需要通過心跳來保證只有一個Master。然而一旦發生分區,每個分區會自己選取一個新的Master,這樣就出現了腦裂,常見的主從數據庫(MySQL,Oracle等)并沒有自帶解決腦裂的方案。所以分區容忍性是沒考慮的。
          • 一致性:不考慮分區,由于任意時刻只有一個主庫,所以一致性是滿足的。
          • 可用性:不考慮分區,HA機制的存在可以保證可用性,所以可用性顯然也是滿足的。

          所以這樣的一個系統,我們認為它是CA的。我們再深入研究下,如果發生腦裂產生數據不一致后有一種方式可以仲裁一致性問題,是不是就可以滿足P了呢。還真有嘗試通過預先設置規則來解決這種多主庫帶來的一致性問題的系統,比如CouchDB,它通過版本管理來支持多庫寫入,在其仲裁階段會通過DBA配置的仲裁規則(也就是合并規則,比如誰的時間戳最晚誰的生效)進行自動仲裁(自動合并),從而保障最終一致性(BASE),自動規則無法合并的情況則只能依賴人工決策了。

          4.4 LDC單元化架構的CAP分析

          4.4.1 戰勝分區容忍性

          在討論LDC架構的CAP之前,我們再來想想分區容忍性有啥值得一提的,為啥很多大名鼎鼎的BASE(最終一致性)體系系統都選擇損失實時一致性,而不是丟棄分區容忍性呢?

          分區的產生一般有兩種情況:

          • 某臺機器宕機了,過一會兒又重啟了,看起來就像失聯了一段時間,像是網絡不可達一樣。
          • 異地部署情況下,異地多活意味著每一地都可能會產生數據寫入,而異地之間偶爾的網絡延時尖刺(網絡延時曲線圖陡增)、網絡故障都會導致小范圍的網絡分區產生。前文也提到過,如果一個分布式系統是部署在一個局域網內的(一個物理機房內),那么個人認為分區的概率極低,即便有復雜的拓撲,也很少會有在同一個機房里出現網絡分區的情況。而異地這個概率會大大增高,所以螞蟻的三地五中心必須需要思考這樣的問題,分區容忍不能丟!同樣的情況還會發生在不同ISP的機房之間(想象一下你和朋友組隊玩DOTA,他在電信,你在聯通)。為了應對某一時刻某個機房突發的網絡延時尖刺活著間歇性失聯,一個好的分布式系統一定能處理好這種情況下的一致性問題。

          那么螞蟻是怎么解決這個問題的呢?我們在4.2的備注部分討論過,其實LDC機房的各個單元都由兩部分組成:負責業務邏輯計算的應用服務器和負責數據持久化的數據庫。大部分應用服務器就像一個個計算器,自身是不對寫一致性負責的,這個任務被下沉到了數據庫。所以螞蟻解決分布式一致性問題的關鍵就在于數據庫!

          想必有些讀者大概猜到下面的討論重點了——OceanBase(下文簡稱OB),中國第一款自主研發的分布式數據庫,一時間也確實獲得了很多光環。在討論OB前,我們先來想想Why not MySQL?

          首先,就像CAP三角圖中指出的,MySQL是一款滿足AC但不滿足P的分布式系統。試想一下,一個MySQL主從結構的數據庫集群,當出現分區時,問題分區內的Slave會認為主已經掛了,所以自己成為本分區的master(腦裂),等分區問題恢復后,會產生2個主庫的數據,而無法確定誰是正確的,也就是分區導致了一致性被破壞。這樣的結果是嚴重的,這也是螞蟻寧愿自研OceanBase的原動力之一。

          那么如何才能讓分布式系統具備分區容忍性呢?按照老慣例,我們從”可用性分區容忍”和”一致性分區容忍”兩個方面來討論。 (1) 可用性分區容忍性保障機制

          可用性分區容忍的關鍵在于別讓一個事務依賴所有節點來完成,這個很簡單,別要求所有節點共同同時參與某個事務即可。

          (2) 一致性分區容忍性保障機制

          老實說,都產生分區了,哪還可能獲得實時一致性。但要保證最終一致性也不簡單,一旦產生分區,如何在2個分區無法通信的情況下保證只有有一份正確的提議呢?究其根本是要保障所有分區種有且只能有一個大腦,下面我們來看下共識算法PAXOS的核心——Quorum思想是如何解決腦裂問題的。

          這里可以發散下,所謂的”腦”其實就是具備寫能力的系統,”非腦”就是只具備讀能力的系統,對應了MySQL集群中的從庫。

          下面是兩段摘自維基百科的PAXOS定義[5]:

          Paxos is a family of protocols for solving consensus in a network of unreliable processors (that is, processors that may fail).

          Quorums express the safety (or consistency) properties of Paxos by ensuring at least some surviving processor retains knowledge of the results.

          這段話的意思是:PAXOS是在一群不可靠的節點組成的集群中的一類共識機制,而Quorum機制則用來保證。眾所周知Paxos算法可以解決分布式系統中的腦裂問題,但其中真正起作用的本質思想是Quorum機制,翻譯過來叫做“法定數量機制”,在分布式領域,它往往要求任何一個提議在一個有N個節點的分布式系統中至少被(N/2)+1個系統節點所認可,它才被認為是可信的,這背后的理論基礎是少數服從多數——一個群體不可能存在2種對立的多數群體觀點。

          只要多數節點認可并持久化數據,即便整個系統宕機了,在重啟后該系統仍然可以通過一次互相通信知道哪個值是合法的——多數節點保留的那個值。這樣的設定也巧妙的解決了分區情況下的共識問題,因為一旦產生分區,勢必最多只有一個分區內的節點數量會大于等于(N/2)+1。也有一些HA(高可用)方案通過依托于另一個仲裁系統來避免腦裂,比如大家同時Ping一個公共的IP,先成功者繼續為腦,然而顯然這就又制造了另外一個單點——仲裁系統的高可用誰來保障?

          如果你了解過比特幣或者區塊鏈,你就知道區塊鏈的基礎理論也是Quorum法定數量機制。區塊鏈借助Quorum機制來抵御惡意篡改,分布式應用系統則是借助Quorum來解決分區腦裂問題。不過區塊鏈假設的是網絡中存在拜占庭錯誤,因而法定數量并不是(N/2)+1,具體可搜索相關技術文章。

          很多同學肯定聽說過這樣的描述——PAXOS是唯一能解決分布式一致性問題的解法。這句話越是理解越發覺得詭異,這會讓人以為PAXOS逃離于CAP約束了,所以個人更愿意理解為——PAXOS是唯一一種保障分布式系統最終一致性的共識算法(所謂共識算法,就是大家都按照這個算法來操作,大家最后的結果一定相同)。

          PAXOS并沒有逃離CAP魔咒,畢竟達成共識是(N/2)+1的節點之間的事,剩下的(N/2)-1的節點上的數據還是舊的,這時候仍然是不一致的,所以PAXOS對一致性的貢獻在于經過一次事務后,這個集群里已經有部分節點保有了本次事務正確的結果(共識的結果),這個結果隨后會被異步的同步到其他節點上,從而保證最終一致性。以下摘自維基百科[5]:

          另外PAXOS不要求對所有節點做實時同步,實質上是考慮到了分區情況下的可用性,通過減少完成一次事務需要的參與者個數,來保障系統的可用性。

          4.4.2 OceanBase的CAP分析

          上文提到過,單元化架構中的成千山萬的應用就像是計算器,本身無CAP限制,其CAP限制和挑戰下沉到了數據庫層——螞蟻自研的分布式數據庫OceanBase(本節簡稱OB)[6]。在OB體系中,每個數據庫實例都具備讀寫能力,具體是讀是寫可以動態配置(參考2.2部分)。實際情況下大部分時候,對于某一類數據(固定用戶號段的數據)任意時刻只有一個單元會負責寫入某個節點,其他節點要么是實時庫間同步,要么是異步數據同步。OB也采用了PAXOS共識協議,實時庫間同步的節點(包含自己)個數至少需要(N/2)+1個,這樣就可以解決分區容忍性問題。

          下面我們舉個馬老師改英文名的例子來說明OB設計的精妙之處。假設數據庫按照用戶ID分庫分表,馬老師的用戶ID對應的數據段在[0-9],由A單元負責數據寫入,假如馬老師(用戶ID假設為000)正在用支付寶APP修改自己的英文名,馬老師一開始打錯了,打成了Jason Ma,馬老師看到修改的按鈕還可以點擊,立馬將名字改成了Jack Ma并重新提交。當第一個請求來到A單元時,突然A單元網絡斷開了(分區產生了),系統執行了預先配置的災備策略自動將A單元對數據段[0,9]的寫入權限轉交給B單元(更改映射),這時候第二個請求便來到了B單元。由于在網絡斷開前請求已經進入了A,寫權限轉交給單元B生效后,A和B同時對[0,9]數據段進行寫入馬老師的英文名。

          假如這時候不做任何的分區容忍性保障即都允許寫入的話就會出現不一致,A單元說我看到馬老師設置了Jason Ma,B單元說我看到馬老師設置了Jack Ma。而在OB中這種情況永遠不會發生——當A單元數據庫實例向整個系統提議說我建議把馬老師的英文名設置為Jason Ma時,發現沒人回應它,由于出現了分區,其他節點對它來說都是不可達的,所以這個提議被自動丟棄,A心里也明白是自己分區了,會有主分區替自己完成寫入任務的。

          同樣的,當B單元數據庫實例提出將馬老師的英文名改成Jack Ma后,大部分節點都響應了,所以B成功將Jack Ma寫入了馬老師的賬號記錄中。假如在寫權限轉交給單元B后A突然恢復了,也沒關系,兩筆寫請求同時要求獲得(N/2)+1個節點的事務鎖,通過no-wait設計,在B獲得了鎖之后,其他爭搶該鎖的事務都會因為失敗而回滾。

          No-wait設計的思想在于當發現所訪問的資源出現競爭時,不等待資源釋放,而是直接返回失敗。

          下面我們分析下OB的CAP:

          • 分區容忍性:OB節點之間是有互相通信的(需要相互同步數據),所以存在分區問題,OB通過僅同步到部分節點來保證可用性。這一點就說明OB做了分區容錯。
          • 可用性分區容忍性:OB事務只需要同步到(N/2)+1個節點,允許其余的一小半節點分區(宕機、斷網等),只要(N/2)+1個節點活著就是可用的。極端情況下,當5個節點分成2:2:1共3個分區時整個系統就確實不可用了,只是這種情況概率比較低。
          • 一致性分區容忍性:分區情況下意味著部分節點失聯了,一致性顯然是不滿足的。但通過共識算法可以保證當下只有一個值是合法的,并且最終會通過節點間的同步達到最終一致性。

          所以OB仍然沒有逃脫CAP魔咒,產生分區的時候它變成AP+最終一致性(C)。整體來說,它是AP的,即高可用和分區容忍。

          5 結語

          個人感覺本文涉及到的知識面確實不少,每個點單獨展開都可以討論半天。回到我們緊扣的主旨來看,雙十一海量支付背后技術上大快人心的設計到底是啥?我想無非是以下幾點:

          基于用戶分庫分表的RZone設計。每個用戶群獨占一個單元給整個系統的容量帶來了爆發式增長。

          • RZone在網絡分區或災備切換時OB的防腦裂設計(PAXOS)。我們知道RZone是單腦的(讀寫都在一個單元對應的庫),而網絡分區或者災備時熱切換過程中可能會產生多個腦,OB解決了腦裂情況下的共識問題(PAXOS算法)。
          • 基于CZone的本地(地理上的本地)讀設計。這一點保證了很大一部分有著“寫讀時間差”現象的公共數據能被高速本地訪問。
          • 剩下的那一丟丟不能本地訪問只能實時遠程訪問GZone的公共配置數據,也興不起什么風,作不了什么浪。大部分瓶頸場景都可以通過優化系統設計來解決,比如對于實時庫存數據,可以通過“頁面展示查詢走應用層緩存”+“實際下單時再校驗”的方式減少其GZone調用量。

          而這就是螞蟻LDC的CRG架構,理論上TPS數字可以做到更高,但雙十一海量支付的成功不單單是這么一套設計所決定的,還有預熱削峰等運營+技術的手段,在此感謝阿里巴巴全體技術給大家帶來的一年一度的購物盛宴。

          感謝大家的閱讀,文中可能存在不足或遺漏之處,歡迎批評指正。

          本文為個人留檔使用,未經允許,不得轉載。

          6 參考文獻

          [1] Practice of Cloud System Administration, The: DevOps and SRE Practices for Web Services, Volume 2. Thomas A. Limoncelli, Strata R. Chalup, Christina J. Hogan.

          [2] MySQL 5.7半同步復制技術. https://www.cnblogs.com/zero-gg/p/9057092.html

          [3] BASE理論分析; https://www.jianshu.com/p/f6157118e54b

          [4] Keepalived; https://baike.baidu.com/item/Keepalived/10346758?fr=aladdin

          [5] PAXOS; https://en.wikipedia.org/wiki/Paxos_(computer_science)

          [6] OceanBase支撐2135億成交額背后的技術原理; https://www.cnblogs.com/antfin/articles/10299396.html

          [7] 三地五中心,螞蟻金服金融行業安全最高標準技術開放,光纜挖斷業務照樣用; https://www.sohu.com/a/255067828_114921

          [8] 阿里雙11技術詳解:容量規劃+LDC+彈性架構+大促中控等; https://my.oschina.net/u/4383176/blog/4713897

          [9] Backup; https://en.wikipedia.org/wiki/Backup


          作者:湯波

          出處:https://tbwork.org/2019/11/10/ant-ldc-arch/

          知道從什么時候起,談及夢想成了一件可笑的事情。有人說夢想總是要有的,萬一實現了呢!有人說實現的那叫目標,實現不了的才叫夢想。可能有太多的夢想沒有實現,于是我們再次談及夢想的時候覺得越來越可笑。曾幾何時我也是為了一篇文章能夠敲上幾個小時再修改無數遍的職場菜鳥,可隨著時間的推移,好像麻木了很多,忘卻了很多,直到他的故事,讓我重新審視自己。

          故事的主人公老周,上學時候追求搖滾樂近乎癡狂,曾經為了買一盒磁帶而吃了一周方便面,而這些瘋狂的事情,現在從他口中說出來不過是簡單的一句“年輕時喜歡搖滾樂,也玩過兩年吉他,也夢想過組建一支自己的樂隊”罷了。與一位執著音樂的朋友相遇讓他重新拾起年輕時候的夢想——組辦一場搖滾音樂會。老周與幾個朋友親力親為,懷著最初的夢想去努力,當四百多人擠爆現場,一起吶喊,當全場唱起《海闊天空》讓這些已過而立之年的漢子濕了眼眶時,老周用DOBBY記錄下來了這個夢想照進現實的過程。

          搖滾音樂會彩排照片,DOBBY拍攝

          如果你以為故事到這里已經是圓滿,我會說:這才剛剛開始而已。東北漢子老周與DOBBY,一起經歷了很多樸實卻感人,平凡又卓越的故事。

          老周用DOBBY在雪地上的自拍

          去年于老周老來說是實現夢想的一年,除了用DOBBY記錄了那場激動人心的搖滾音樂會,還帶著全家在春節前回到了久別的故鄉。岳母的父親罹患癌癥,臨近年根兒,二老甚是想家,臘月二十七,老周帶著一家人踏上了返鄉的路途。春節里,一家人團聚在一起,堆雪人、吃年夜飯、外出游玩合影,一切都是那么熟悉又舒適。全程,老周都在認真的用DOBBY記錄每一個溫馨時刻,從此全家福變得更加完整。回到那個屯兒,見到熟悉的人兒,雖然故土看著好似破敗,但是那里凝聚了老人們的童年記憶,一張DOBBY視角的留影,記錄下他們在老宅與鄉親們的合影,好似我們在一本書里,讀著他們的故事。

          鶴崗,東北邊陲的一座小城,老周生在這里,長在這里,也深深地熱愛著這座城市。作為DOBBY的第一批眾籌用戶,老周一直把DOBBY當做生活中的紀錄者,一篇《寒冬暮色中的東北礦山》曾經感動了很多社區朋友,老周包含深情的文字配上DOBBY的獨特視角,讓我們知道:那里曾在大躍進期間靠人工鎬刨鍬挖開采出露天礦,曾經擁有全國最大產量的礦井,這樣一個三四線的小城市也曾經為國家的建設貢獻了自己所有的力量……“為什么我的眼里飽含淚水,因為我對這片土地愛得深沉,因為這是我的家鄉,我經歷過這里的輝煌和衰落,這里也見證了我的成長。”老周深情地訴說著。

          重情又瀟灑的老周有著一撥兒經歷過風風雨雨一路走來的朋友,八年前他因為《紅河谷》而迷戀上了西藏,老阿媽的慈祥,藏族漢子的驍勇淳樸,西藏文化的神秘,他們對信仰的虔誠,都打動著老周。八年里,老周經歷了結婚,生子,父親病重去世,創業等等。半年前老周發現自己得了高血壓,然而這場西藏行卻還未開始,于是毅然決然收拾行囊與朋友出發了。這次行程,攝影設備老周只帶了DOBBY和ROLLCAP,與其他旅行者不同,DOBBY讓老周從上帝視角領略了西藏的神秘與魅力。

          “一座高原,一個西藏,十萬邊疆,五百山水,三千佛唱。”西藏獨有的美讓老周陶醉,同時也伴隨著嚴重的高原反應:頭疼,心慌,呼吸困難,每一次飛起DOBBY,老周都要小心翼翼克服身體的不適。

          DOBBY的限高海拔其實只有3000米,幾個月前有個飛友曾經在五千多米海拔處成功的飛起DOBBY拍照,創下記錄,這一次,老周不但在這里成功的飛起了DOBBY,更是一次又一次的用DOBBY記錄了他們的旅行。鶯飛草長,青天在上,白云涌動,雪山綿長,牧民喇嘛,身邊的摯友,老周用他的DOBBY,創造了一個全新的奇跡。

          視頻https://v.qq.com/x/page/v0388xk75qf.html

          DOBBY & ROLLCAP 拍攝

          幾天前與老周溝通的時候,他正在陪老婆逛街,他說:“自己的夢想實現了,現在要好好兒陪陪老婆,實現老婆的夢想。”回到的平凡世界的老周依舊是柴米油鹽,家長里短,兢兢業業的工作。

          我們曾走過無數地方和無盡歲月,每個夢想都通往不同世界,老周的故事,會一直繼續,而你的故事,現在正是起點。

          Canvas中,線的默認顏色為黑色,寬度為lpx,但我們可以使用相關屬性為線添加不同的樣式。下面我們將從寬度、描邊顏色、端點形狀3方面詳細講解線條樣式的設置方法。

          1.設置線條寬度

          使用lineWidth屬性可以定義線的寬度,該屬性的取值為數值(不帶單位),以像素為計量。設置線的寬度的示例代碼如下:

          context.lineNidth='10';

          上述代碼中設置了線的寬度為10。

          2.設置描邊顏色

          使用strokeStyle屬性可以定義線的描邊顏色,該屬性的取值為十六進制顏色值或顏色的英文名。設置描邊顏色的示例代碼如下:

          context.strokeStyle='4f00';
          context.strokeStyle='red';

          在上述代碼中,兩種方式都可以用于設置線的描邊顏色為紅色。

          3.設置端點形狀

          默認情況下,線的端點是方形的,通過lineCap屬性可以改變端點的形狀,示例代碼如下:

          context.1ineCap=,屬性值';

          lineCap屬性的取值有3個,具體如表所示。

          lineCap屬性的取值

          了解了設置線的樣式的基本方法后,下面演示如何為線設置樣式。

          (1)創建C:icodekchapter02\demol3.html,創建畫布并為線設置寬度、顏色和端點形狀,具體代碼如下:

          <!DOCTYPE html>
          <html>
          <head>
            <meta charset="UTF-8">
            <title>Document</title>
          </head>
          <body>
            <canvas id="cas" width="300" height="300">
               您的測覽器不支持Canvas標簽
            </canvas>
            <script>
             var context=document.getElementById('cas').getContext('2d');
             context.moveTo(10,10);          // 定義初始位置
             context.1ineTo(300,10);         // 定義連線端點
             context.lineWidth='10';         // 設置線的寬度
             context.strokeStyle='red';      // 設置線的顏色
             context.lineCap='round';        // 設置線的端點形狀
             context.stroke();               // 定叉描邊
            </script>
          </body>
          </html>

          上述代碼中,第15行代碼設置了線的寬度為10像素:第16行代碼設置了線的顏色為紅色;第17行代碼設置了線的端點為圓形。

          (2)保存代碼,在瀏覽器中測試,頁面效果如圖所示。

          設置線的寬度、顏色和端點形狀

          頁面顯示一條紅色的線,說明我們已經成功為線設置了樣式。


          主站蜘蛛池模板: 黑人大战亚洲人精品一区| 亚洲Av永久无码精品一区二区 | 香蕉久久一区二区不卡无毒影院| 国产自产V一区二区三区C| 国模无码视频一区| 国产日韩一区二区三区| 久久久久人妻精品一区二区三区| 国产女人乱人伦精品一区二区| 亚洲av无码一区二区三区天堂| 人妻少妇久久中文字幕一区二区 | 国产视频福利一区| 精品无码国产一区二区三区51安| 久久久久人妻一区精品色| 一区二区在线电影| 亚洲一区二区免费视频| 久久精品无码一区二区WWW| 久久精品国产AV一区二区三区| 99精品一区二区免费视频| 国产精品一区二区久久| 精品久久久久久无码中文字幕一区 | 亚洲性无码一区二区三区| 亚洲天堂一区在线| 日韩一区二区三区视频| 久久99久久无码毛片一区二区| 中文字幕乱码一区二区免费| 久久人妻av一区二区软件| 亚洲福利一区二区| 国产成人高清亚洲一区久久| 国产免费播放一区二区| 久久精品国产一区二区三| 亚洲综合一区二区精品久久| 日本内射精品一区二区视频| 成人免费视频一区二区三区| 一区二区三区在线|日本| 午夜无码一区二区三区在线观看| 精品一区二区久久| 国产精品美女一区二区| 无码一区二区三区| 中文字幕在线一区二区在线| 亚洲欧美成人一区二区三区| 亚洲综合无码一区二区痴汉|