天天看點

阿裡雲PolarDB及其共享存儲PolarFS技術實作分析(下)

上篇介紹了PolarDB資料庫及其後端共享存儲PolarFS系統的基本架構群組成子產品,是最基礎的部分。本篇重點分析PolarFS的資料IO流程,中繼資料更新流程,以及PolarDB資料庫節點如何适配PolarFS這樣的共享存儲系統。

PolarFS的資料IO操作

寫操作

阿裡雲PolarDB及其共享存儲PolarFS技術實作分析(下)
一般情況下,寫操作不會涉及到卷上檔案系統的中繼資料更新,因為在寫之前就已經通過libpfs的pfs_posix_fallocate()這個API将Block預配置設定給檔案,這就避免在讀寫IO路徑上出現代價較高的檔案系統中繼資料同步過程。上圖是PolarFS的寫操作流程圖,每步操作解釋如下:

  1. POLARDB通過libpfs發送一個寫請求Request1,經由ring buffer發送到PolarSwitch;
  2. PolarSwitch根據本地緩存的中繼資料,将Request1發送至對應Chunk的Leader節點(ChunkServer1);
  3. Request1到達ChunkServer1後,節點上的RDMA NIC将Request1放到一個預配置設定好的記憶體buffer中,基于Request1構造一個請求對象,并将該對象加到請求隊列中。一個IO輪詢線程不斷輪詢這個請求隊列,一旦發現有新請求則立即開始處理;
  4. IO處理線程通過異步調用将Request1通過SPDK寫到Chunk對應的WAL日志塊上,同時将請求通過RDMA異步發向給Chunk的Follower節點(ChunkServer2、ChunkServer3)。由于都是異步調用,是以資料傳輸是并發進行的;
  5. 當Request1請求到達ChunkServer2、ChunkServer3後,同樣通過RDMA NIC将其放到預配置設定好的記憶體buffer并加入到複制隊列中;
  6. Follower節點上的IO輪詢線程被觸發,Request1通過SPDK異步地寫入該節點的Chunk副本對應的WAL日志塊上;
  7. 當Follower節點的寫請求成功後,會在回調函數中通過RDMA向Leader節點發送一個應答響應;
  8. Leader節點收到ChunkServer2、ChunkServer3任一節點成功的應答後,即形成Raft組的majority。主節點通過SPDK将Request1寫到請求中指定的資料塊上;
  9. 随後,Leader節點通過RDMA NIC向PolarSwitch傳回請求處理結果;
  10. PolarSwitch标記請求成功并通知上層的POLARDB。

讀請求無需這麼複雜的步驟,lipfs發起的讀請求直接通過PolarSwitch路由到資料對應Chunk的Leader節點(ChunkServer1),從其中讀取對應的資料傳回即可。需要說明的是,在ChunkServer上有個子子產品叫IoScheduler,用于保證發生并發讀寫通路時,讀操作能夠讀到最新的已送出資料。

基于使用者态的網絡和IO路徑

在本地IO處理上,PolarFS基于預配置設定的記憶體buffer來處理請求,将buffer中的内容直接使用SPDK寫入WAL日志和資料塊中。PolarFS讀寫資料基于SPDK套件直接通過DMA操作硬體裝置(SSD卡)而不是作業系統核心IO協定棧,解決了核心IO協定棧慢的問題;通過輪詢的方式監聽硬體裝置IO完成事件,消除了上下文切換和中斷的開銷。還可以将IO處理線程和CPU進行一一映射,每個IO處理線程獨占CPU,互相之間處理不同的IO請求,綁定不同的IO裝置硬體隊列,一個IO請求生命周期從頭到尾都在一個線程一顆CPU上處理,不需要鎖進行互斥。這種技術實作最大化的和高速裝置進行性能互動,實作一顆CPU達每秒約20萬次IO處理的能力,并且保持線性的擴充能力,也就意味着4顆CPU可以達到每秒80萬次IO處理的能力,在性能和經濟型上遠高于核心。

網絡也是類似的情況。過去傳統的以太網,網卡發一個封包到另一台機器,中間通過一跳交換機,大概需要一百到兩百微秒。POLARDB支援ROCE以太網,通過RDMA網絡,直接将本機的記憶體寫入另一台機器的記憶體位址,或者從另一台機器的記憶體讀一塊資料到本機,中間的通訊協定編解碼、重傳機制都由RDMA網卡來完成,不需要CPU參與,使性能獲得極大提升,傳輸一個4K大小封包隻需要6、7微秒的時間。

阿裡雲PolarDB及其共享存儲PolarFS技術實作分析(下)

如同核心的IO協定棧跟不上高速儲存設備能力,核心的TCP/IP協定棧跟不上高速網絡裝置能力,也被POLARDB的使用者态網絡協定棧代替。這樣就解決了HDFS和Ceph等目前的分布式檔案系統存在的性能差、延遲大的問題。

基于ParallelRaft的資料可靠性保證

在PolarFS中,位于不同ChunkServer上的3個Chunk資料副本使用改進型Raft協定ParallelRaft來保障可靠性,通過快速主從切換和majority機制確定能夠容忍少部分Chunk副本離線時仍能夠持續提供線上讀寫服務,即資料的高可用。

在标準的Raft協定中,raft日志是按序被Follower節點确認,按序被Leader節點送出的。這是因為Raft協定不允許出現空洞,一條raft日志被送出,意味着它之前的所有raft日志都已經被送出。在資料庫系統中,對不同資料的并發更新是常态,也正因為這點,才有了事務的組送出技術,但如果引入Raft協定,意味着組送出技術在PolarFS資料多副本可靠性保障這一層退化為串行送出,對于性能會産生很大影響。通過将多個事務batch成一個raft日志,通過在一個Raft Group的Leader和Follower間建立多個連接配接來同時處理多個raft日志這兩種方式(batching&pipelining)能夠緩解性能退化。但batch會導緻額外延遲,batch也不能過大。pipelining由于Raft協定的束縛,仍然需要保證按序确認和送出,如果出現由于網絡等原因導緻前後pipeline上的raft日志發送往follow或回複leader時亂序,那麼就不可避免得出現等待。

為了進一步優化性能,PolarFS對Raft協定進行了改進。核心思想就是解除按序确認,按序送出的束縛。将其變為亂序确認,亂序送出和亂序應用。首先看看這樣做的可行性,假設每個raft日志代表一個事務,多個事務能夠并行送出說明其不存在沖突,對應到存儲層往往意味着沒有修改相同的資料,比如事務T1修改File1的Block1,事務T2修改File1的Block2。顯然,先修改Block1還是Block2對于存儲層還是資料庫層都沒有影響。這真是能夠亂序的基礎。下圖為優化前後的性能表現:

阿裡雲PolarDB及其共享存儲PolarFS技術實作分析(下)

但T1和T2都修改了同一個表的資料,導緻表的統計資訊發生了變化,比如T1執行後表中有10條記錄,T2執行後變為15條(舉例而已,不一定正确)。是以,他們都需要更新存儲層的相同BlockX,該更新操作就不能亂序了。

為了解決上述所說的問題,ParallelRaft協定引入look behind buffer(LBB)。每個raft日志都有個LBB,緩存了它之前的N個raft日志所修改的LBA資訊。LBA即Logical Block Address,表示該Block在Chunk中的偏移位置,從0到10GB。通過判斷不同的raft日志所包含的LBA是否有重合來決定能否進行亂序/并行應用,比如上面的例子,先後修改了BlockX的raft日志就可以通過LBB發現,如果T2對BlockX的更新先完成了确認和送出,在應用前通過LBB發現所依賴的T1對BlockX的修改還沒有應用。那麼就會進入pending隊列,直到T1對BlockX完成應用。

另外,亂序意味着日志會有空洞。是以,Leader選舉階段額外引入了一個Merge階段,填補Leader中raft日志的空洞,能夠有效保障協定的Leader日志的完整性。

阿裡雲PolarDB及其共享存儲PolarFS技術實作分析(下)

PolarFS中繼資料管理與更新

PolarFS各節點中繼資料維護

libpfs僅維護檔案塊(塊在檔案中的偏移位置)到卷塊(塊在卷中的偏移位置)的映射關系,并未涉及到卷中Chunk跟ChunkServer間的關系(Chunk的實體位置資訊),這樣libpfs就跟存儲層解耦,為Chunk配置設定實際實體空間時無需更新libpfs層的中繼資料。而Chunk到ChunkServer的映射關系,也就是實體存儲空間到卷的配置設定行為由PolarCtrl元件負責,PolarCtrl完成配置設定後會更新PolarSwitch上的緩存,確定libpfs到ChunkServer的IO路徑是正确的。

Chunk中Block的LBA到Block真實實體位址的映射表,以及每塊SSD盤的空閑塊位圖均全部緩存在ChunkServer的記憶體中,使得使用者資料IO通路能夠全速推進。

PolarFS中繼資料更新流程

前面我們介紹過,PolarDB為每個資料庫執行個體建立了一個volume/卷,它是一個檔案系統,建立時生成了對應的中繼資料資訊。由于PolarFS是個可多點挂載的共享通路分布式檔案系統,需要確定一個挂載點更新的中繼資料能夠及時同步到其他挂載點上。比如一個節點增加/删除了檔案,或者檔案的大小發生了變化,這些都需要持久化到PolarFS的中繼資料上并讓其他節點感覺到。下面我們來讨論PolarFS如何更新中繼資料并進行同步。

PolarFS的每個卷/檔案系統執行個體都有相應的Journal檔案和與之對應的Paxos檔案。Journal檔案記錄了檔案系統中繼資料的修改曆史,是該卷各個挂載點之間中繼資料同步的中心。Journal檔案邏輯上是一個固定大小的循環buffer,PolarFS會根據水位來回收Journal。如果一個節點希望在Journal檔案中追加項,其必需使用DiskPaxos算法來擷取Journal檔案控制權。

正常情況下,為了確定檔案系統中繼資料和資料的一緻性,PolarFS上的一個卷僅設定一個計算節點進行讀寫模式挂載,其他計算節點以隻讀形式挂載檔案系統,讀寫節點鎖會在中繼資料記錄持久化後馬上釋放鎖。但是如果該讀寫節點crash了,該鎖就不會被釋放,為此加在Journal檔案上的鎖會有過期時間,在過期後,其他節點可以通過執行DiskPaxos來重新競争對Journal檔案的控制權。當PolarFS的一個挂載節點開始同步其他節點修改的中繼資料時,它從上次掃描的位置掃描到Journal末尾,将新entry更新到節點的本地緩存中。PolarFS同時使用push和pull方式來進行節點間的中繼資料同步。

下圖展示了檔案系統中繼資料更新和同步的過程:

阿裡雲PolarDB及其共享存儲PolarFS技術實作分析(下)
  1. Node 1是讀寫挂載點,其在pfs_fallocate()調用中将卷的第201個block配置設定給FileID為316的檔案後,通過Paxos檔案請求互斥鎖,并順利獲得鎖。
  2. Node 1開始記錄事務至journal中。最後寫入項标記為pending tail。當所有的項記錄之後,pending tail變成journal的有效tail。
  3. Node1更新superblock,記錄修改的中繼資料。與此同時,node2嘗試擷取通路互斥鎖,由于此時node1擁有的互斥鎖,Node2會失敗重試。
  4. Node2在Node1釋放lock後(可能是鎖的租約到期所緻)拿到鎖,但journal中node1追加的新項決定了node2的本地中繼資料是過時的。
  5. Node2掃描新項後釋放lock。然後node2復原未記錄的事務并更新本地metadata。最後Node2進行事務重試。
  6. Node3開始自動同步中繼資料,它隻需要load增量項并在它本地重放即可。

PolarFS的元速度更新機制非常适合PolarDB一寫多讀的典型應用擴充模式。正常情況下一寫多讀模式沒有鎖争用開銷,隻讀執行個體可以通過原子IO無鎖擷取Journal資訊,進而使得PolarDB可以提供近線性的QPS性能擴充。

資料庫如何适配PolarFS

大家可能認為,如果讀寫執行個體和隻讀執行個體共享了底層的資料和日志,隻要把隻讀資料庫配置檔案中的資料目錄換成讀寫執行個體的目錄,貌似就可以直接工作了。但是這樣會遇到很多問題,MySQL适配PolarFS有很多細節問題需要處理,有些問題隻有在真正做适配的時候還能想到,下面介紹已知存在的問題并分析資料庫層是如何解決的。

資料緩存和資料一緻性

從資料庫到硬體,存在很多層緩存,對基于共享存儲的資料庫方案有影響的緩存層包括資料庫緩存,檔案系統緩存。

資料庫緩存主要是InnoDB的Buffer Pool(BP),存在2個問題:

  1. 讀寫節點的資料更改會緩存在bp上,隻有完成刷髒頁操作後polarfs才能感覺,是以如果在刷髒之前隻讀節點發起讀資料操作,讀到的資料是舊的;
  2. 就算PolarFS感覺到了,隻讀節點的已經在BP中的資料還是舊的。是以需要解決不同節點間的緩存一緻性問題。

PolarDB采用的方法是基于redolog複制的節點間資料同步。可能我們會想到Primary節點通過網絡将redo日志發送給ReadOnly/Replica節點,但其實并不是,現在采用的方案是redo采用非ring buffer模式,每個檔案固定大小,大小達到後Rotate到新的檔案,在寫模式上走Direct IO模式,確定磁盤上的redo資料是最新的,在此基礎上,Primary節點通過網絡通知其他節點可以讀取的redo檔案及偏移位置,讓這些節點自主到共享存儲上讀取所需的redo資訊,并進行回放。流程如下圖所示:

阿裡雲PolarDB及其共享存儲PolarFS技術實作分析(下)

由于StandBy節點與讀寫節點不共享底層存儲,是以需要走網絡發送redo的内容。節點在回放redo時需區分是ReadOnly節點還是StandBy節點,對于ReadOnly節點,其僅回放對應的Page頁已在BP中的redo,未在BP中的page不會主動從共享存儲上讀取,且BP中Apply過的Page也不會回刷到共享存儲。但對于StandBy節點,需要全量回放并回刷到底層存儲上。

檔案系統緩存主要是中繼資料緩存問題。檔案系統緩存包括Page Cache,Inode/Dentry Cache等,對于Page Cache,可以通過Direct IO繞過。但對于VFS(Virtual File System)層的Inode Cache,無法通過Direct IO模式而需采用o_sync的通路模式,但這樣導緻性能嚴重下降,沒有實際意義。vfs層cache無法通過direct io模式繞過是個很嚴重的問題,這就意味着讀寫節點建立的檔案,隻讀節點無法感覺,那麼針對這個新檔案的後續IO操作,隻讀節點就會報錯,如果采用核心檔案系統,不好進行改造。

PolarDB通過中繼資料同步來解決該問題,它是個使用者态檔案系統,資料的IO流程不走核心态的Page Cache,也不走VFS的Inode/Dentry Cache,完全自己掌控。共享存儲上的檔案系統中繼資料通過前述的更新流程實作即可。通過這種方式,解決了最基本的節點間資料同步問題。

事務的資料可見性問題

一、MySQL/InnoDB通過Undo日志來實作事務的MVCC,由于隻讀節點跟讀寫節點屬于不同的mysqld程序,讀寫節點在進行Undo日志Purge的時候并不會考慮此時在隻讀節點上是否還有事務要通路即将被删除的Undo Page,這就會導緻記錄舊版本被删除後,隻讀節點上事務讀取到的資料是錯誤的。

針對該問題,PolarDB提供兩種解決方式:

  • 所有ReadOnly定期向Primary彙報自己的最大能删除的Undo資料頁,Primary節點統籌安排;
  • 當Primary節點删除Undo資料頁時候,ReadOnly接收到日志後,判斷即将被删除的Page是否還在被使用,如果在使用則等待,超過一個時間後還未有結束則直接給用戶端報錯。

二、還有個問題,由于InnoDB BP刷髒頁有多種方式,其并不是嚴格按照oldest modification來的,這就會導緻有些事務未送出的頁已經寫入共享存儲,隻讀節點讀到該頁後需要通過Undo Page來重建可見的版本,但可能此時Undo Page還未刷盤,這就會出現隻讀上事務讀取資料的另一種錯誤。

針對該問題,PolarDB解決方法是:

  1. 限制讀寫節點刷髒頁機制,如果髒頁的redo還沒有被隻讀節點回放,那麼該頁不能被刷回到存儲上。這就確定隻讀節點讀取到的資料,它之前的資料鍊是完整的,或者說隻讀節點已經知道其之前的所有redo日志。這樣即使該資料的記錄版本目前的事務不可見,也可以通過undo構造出來。即使undo對應的page是舊的,可以通過redo構造出所需的undo page。
  2. replica需要緩存所有未刷盤的資料變更(即RedoLog),隻有primary節點把髒頁刷入盤後,replica緩存的日志才能被釋放。這是因為,如果資料未刷盤,那麼隻讀讀到的資料就可能是舊的,需要通過redo來重建出來,參考第一點。另外,雖然buffer pool中可能已經緩存了未刷盤的page的資料,但該page可能會被LRU替換出去,當其再次載入是以隻讀節點必須緩存這些redo。

DDL問題

如果讀寫節點把一個表删了,反映到存儲上就是把檔案删了。對于mysqld程序來說,它會確定删除期間和删除後不再有事務通路該表。但是在隻讀節點上,可能此時還有事務在通路,PolarFS在完成檔案系統中繼資料同步後,就會導緻隻讀節點的事務通路存儲出錯。

PolarDB目前的解決辦法是:如果主庫對一個表進行了表結構變更操作(需要拷表),在操作傳回成功前,必須通知到所有的ReadOnly節點(有一個最大的逾時時間),告訴他們,這個表已經被删除了,後續的請求都失敗。當然這種強同步操作會給性能帶來極大的影響,有進一步的優化的空間。

Change Buffer問題

Change Buffer本質上是為了減少二級索引帶來的IO開銷而産生的一種特殊緩存機制。當對應的二級索引頁沒有被讀入記憶體時,暫時緩存起來,當資料頁後續被讀進記憶體時,再進行應用,這個特性也帶來的一些問題,該問題僅存在于StandBy中。例如Primary節點可能因為資料頁還未讀入記憶體,相應的操作還緩存在Change Buffer中,但是StandBy節點則因為不同的查詢請求導緻這個資料頁已經讀入記憶體,可以直接将二級索引修改合并到資料頁上,無需經過Change Buffer了。但由于複制的是Primary節點的redo,且需要保證StandBy和Primary在存儲層的一緻性,是以StandBy節點還是會有Change Buffer的資料頁和其對應的redo日志,如果該髒頁回刷到存儲上,就會導緻資料不一緻。

為了解決這個問題,PolarDB引入shadow page的概念,把未修改的資料頁儲存到其中,将cChange Buffer記錄合并到原來的資料頁上,同時關閉該Mtr的redo,這樣修改後的Page就不會放到Flush List上。也就是StandBy執行個體的存儲層資料跟Primary節點保持一緻。

性能測試

性能評估不是本文重點,官方的性能結果也不一定是靠譜的,隻有真實測試過了才算數。在此僅簡單列舉阿裡雲自己的性能測試結果,權當一個參考。

PolarFS性能

不同塊大小的IO延遲

阿裡雲PolarDB及其共享存儲PolarFS技術實作分析(下)

4KB大小的不同請求類型

阿裡雲PolarDB及其共享存儲PolarFS技術實作分析(下)

PolarDB整體性能

使用不同底層存儲時性能表現

阿裡雲PolarDB及其共享存儲PolarFS技術實作分析(下)

對外展示的性能表現

阿裡雲PolarDB及其共享存儲PolarFS技術實作分析(下)

與Aurora簡單對比

阿裡雲的PolarDB和AWS Aurora雖然同為基于MySQL和共享存儲的Cloud-Native Database(雲原生資料庫)方案,很多原理是相同的,包括基于redo的實體複制和計算節點間狀态同步。但在實作上也存在很大的不同,Aurora在存儲層采用日志即資料的機制,計算節點無需再将髒頁寫入到存儲節點,大大減少了網絡IO量,但這樣的機制需要對InnoDB存儲引擎層做很大的修改,難度極大。而PolarDB基本上遵從了原有的MySQL IO路徑,通過優化網絡和IO路徑來提高網絡和IO能力,相對來說在資料庫層面并未有架構性的改動,相對容易些。個人認為Aurora在資料庫技術創新上更勝一籌,但PolarDB在資料庫系統級架構優化上做得更好,以盡可能小的代價獲得了足夠好的收益。

另附PolarFS的架構師曹偉在知乎上對PolarDB和Aurora所做的對比:

在設計方法上,阿裡雲的PolarDB和Aurora走了不一樣的路,歸根結底是我們的出發點不同。AWS的RDS一開始就是架設在它的虛拟機産品EC2之上的,使用的存儲是雲盤EBS。EC2和EBS之間通過網絡通訊,是以AWS的團隊認為“網絡成為資料庫的瓶頸”,在Aurora的論文中,他們開篇就提出“Instead, the bottleneck moves to the network between the database tier requesting I/Os and the storage tier that performs these I/Os.” Aurora設計于12到13年之際,當時網絡主流是萬兆網絡,确實容易成為瓶頸。而PolarDB是從15年開始研發的,我們見證了IDC從萬兆到25Gb RDMA網絡的飛躍。是以我們非常大膽的判斷,未來幾年主機通過高速網絡互聯,其傳輸速率會和本地PCIe總線儲存設備帶寬打平,網絡無論在延遲還是帶寬上都會接近總線,是以不再成為高性能伺服器的瓶頸。而恰恰是軟體,過去基于核心提供的syscall開發的軟體代碼,才是拖慢系統的一環。Bottleneck resides in the software.

在架構上Aurora和PolarDB各有特色。我認為PolarDB的架構和技術更勝一籌。

1)現代雲計算機型的演進和分化,計算機型向高主頻,多CPU,大記憶體的方向演進;存儲機型向高密度,低功耗方向發展。機型的分化可以大大提高機器資源的使用率,降低TCO。

是以PolarStore中大量采用OS-bypass和zero-copy的技術來節約CPU,降低處理機關I/O吞吐需要消耗的CPU資源,确儲存儲節點處理I/O請求的效率。而Aurora的存儲節點需要大量CPU做redolog到innodb page的轉換,存儲節點的效率遠不如PolarStore。

2)Aurora架構的最大亮點是,存儲節點具有将redolog轉換為innodb page的能力,這個改進看着很吸引眼球,事實上這個優化對關系資料庫的性能提升很有限,性能瓶頸真的不在這裡:),反而會拖慢關鍵路徑redolog落地的性能。btw,在PolarDB架構下,redolog離線轉換為innodb page的能力不難實作,但我們目前不認為這是高優先級要做的。

3)Aurora的存儲多副本是通過quorum機制來實作的,Aurora是六副本,也就是說,需要計算節點向六個存儲節點分别寫六次,這裡其實計算節點的網絡開銷又上去了,而且是發生在寫redolog這種關鍵路徑上。而PolarDB是采用基于RDMA實作的ParallelRaft技術來複制資料,計算節點隻要寫一次I/O請求到PolarStore的Leader節點,由Leader節點保證quorum寫入其他節點,相當于多副本replication被offload到存儲節點上。

此外,在最終一緻性上Aurora是用gossip協定來兜底的,在完備程度上沒有PolarDB使用的ParallelRaft算法有保證。

4)Aurora的改動手術切口太大,使得它很難後面持續跟進社群的新版本。這也是AWS幾個資料庫産品線的通病,例如Redshift,如何吸收PostgrelSQL 10的變更是他們的開發團隊很頭疼的問題。對新版本做到與時俱進是雲資料庫的一個樸素需求。怎麼設計這個刀口,達到effect和cost之間的平衡,是對架構師的考驗。

總得來說,PolarDB将資料庫拆分為計算節點與存儲節點2個獨立的部分,計算節點在已有的MySQL資料庫基礎上進行修改,而存儲節點基于全新的PolarFS共享存儲。PolarDB通過計算和存儲分離的方式實作提供了即時生效的可擴充能力和運維能力,同時采用RDMA和SPDK等最新的硬體來優化傳統的過時的網絡和IO協定棧,極大提升了資料庫性能,基本上解決了使用MySQL是會遇到的各種問題,除此之外本文并未展開介紹PolarDB的ParallelRaft,其依托上層資料庫邏輯實作IO亂序送出,大大提高多個Chunk資料副本達成一緻性的性能。以上這些創新和優化,都成為了未來資料庫的發展方向。

參數資料:

  • “PolarFS: An Ultra-low Latency and Failure Resilient. Distributed File System for Shared Storage Cloud Database”
  • ”深入了解阿裡雲新一代産品 POLARDB“
  • ”阿裡雲下一代資料庫PolarDB架構設計“
  • PolarDB技術深入剖析
  • 如何評價阿裡雲新一代關系型資料庫 PolarDB?

本文來自網易雲社群 ,經作者溫正湖授權釋出。

網易雲免費體驗館,0成本體驗20+款雲産品!

更多網易研發、産品、營運經驗分享請通路網易雲社群。

相關文章:

【推薦】 從加班論用戶端開發中的模組化

【推薦】 GDB抓蟲之旅(上篇)

【推薦】 談談驗證碼的工作原理

繼續閱讀