成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

一篇文章帶給你Etcd-Raft學習

開發 前端
raft 算法本質上是一個大的狀態機,任何的操作例如選舉、提交數據等,最后都被封裝成一個消息結構體,輸入到 raft 算法庫的狀態機中。raft 算法其實由好幾個協議組成,etcd-raft 將其統一定義在了 Message 結構體之中。

[[405925]]

從本質上說,Raft 算法是通過一切以領導者為準的方式,實現一系列值的共識和各節點日志的一致

  • Leader 選舉,Leader 故障后集群能快速選出新 Leader;
  • 日志復制, 集群只有 Leader 能寫入日志, Leader 負責復制日志到 Follower 節點,并強制 Follower 節點與自己保持相同;
  • 安全性,成員變更,一個任期內集群只能產生一個 Leader、已提交的日志條目在發生 Leader 選舉時,一定會存在更高任期的新 Leader 日志中、各個節點的狀態機應用的任意位置的日志條目內容應一樣等。

Leader 選舉

raft 算法本質上是一個大的狀態機,任何的操作例如選舉、提交數據等,最后都被封裝成一個消息結構體,輸入到 raft 算法庫的狀態機中。raft 算法其實由好幾個協議組成,etcd-raft 將其統一定義在了 Message 結構體之中,以下總結了該結構體的成員用途:

  1. type Message struct { 
  2. Type             MessageType `protobuf:"varint,1,opt,name=type,enum=raftpb.MessageType" json:"type"` // 消息類型 
  3. To               uint64      `protobuf:"varint,2,opt,name=to" json:"to"` // 消息接收者的節點ID 
  4. From             uint64      `protobuf:"varint,3,opt,name=from" json:"from"` // 消息發送者的節點 ID 
  5. Term             uint64      `protobuf:"varint,4,opt,name=term" json:"term"` // 發送消息的節點的Term值。如果Term值為0,則為本地消息,在etcd-raft模塊的實現中,對本地消息進行特殊處理。 
  6. LogTerm          uint64      `protobuf:"varint,5,opt,name=logTerm" json:"logTerm"` // 該消息攜帶的第一條Entry記錄的Term值,日志所處的任期ID 
  7. Index            uint64      `protobuf:"varint,6,opt,name=index" json:"index"` // 日志索引ID,用于節點向 Leader 匯報自己已經commit的日志數據ID 
  8. Entries          []Entry     `protobuf:"bytes,7,rep,name=entries" json:"entries"` // 如果是MsgApp類型的消息,則該字段中保存了Leader節點復制到Follower節點的Entry記錄 
  9. Commit           uint64      `protobuf:"varint,8,opt,name=commit" json:"commit"` // 消息發送節點提交日志索引 
  10. Snapshot         Snapshot    `protobuf:"bytes,9,opt,name=snapshot" json:"snapshot"` // 在傳輸快照時,該字段保存了快照數據 
  11. Reject           bool        `protobuf:"varint,10,opt,name=reject" json:"reject"` // 主要用于響應類型的消息,表示是否拒絕收到的消息 
  12. RejectHint       uint64      `protobuf:"varint,11,opt,name=rejectHint" json:"rejectHint"` //在Follower節點拒絕Leader節點的消息之后,會在該字段記錄一個Entry索引值供Leader節點 
  13. Context          []byte      `protobuf:"bytes,12,opt,name=context" json:"context,omitempty"` // 消息攜帶的一些上下文信息。例如,該消息是否與Leader節點轉移相關 
  14. XXX_unrecognized []byte      `json:"-"

Message結構體相關的數據類型為 MessageType,MessageType 有 19 種。當然,并不是所有的消息類型都會用到上面定義的Message結構體中的所有字段,因此其中有些字段是Optinal的。

  1.    MsgHup            MessageType = 0  //當Follower節點的選舉計時器超時,會發送MsgHup消息 
  2. MsgBeat           MessageType = 1  //Leader發送心跳,主要作用是探活,Follower接收到MsgBeat會重置選舉計時器,防止Follower發起新一輪選舉 
  3. MsgProp           MessageType = 2  //客戶端發往到集群的寫請求是通過MsgProp消息表示的 
  4. MsgApp            MessageType = 3  //當一個節點通過選舉成為Leader時,會發送MsgApp消息 
  5. MsgAppResp        MessageType = 4  //MsgApp的響應消息 
  6. MsgVote           MessageType = 5  //當PreCandidate狀態節點收到半數以上的投票之后,會發起新一輪的選舉,即向集群中的其他節點發送MsgVote消息 
  7. MsgVoteResp       MessageType = 6  //MsgVote選舉消息響應的消息 
  8. MsgSnap           MessageType = 7  //Leader向Follower發送快照信息 
  9. MsgHeartbeat      MessageType = 8  //Leader發送的心跳消息 
  10. MsgHeartbeatResp  MessageType = 9  //Follower處理心跳回復返回的消息類型 
  11. MsgUnreachable    MessageType = 10 //Follower消息不可達 
  12. MsgSnapStatus     MessageType = 11 //如果Leader發送MsgSnap消息時出現異常,則會調用Raft接口發送MsgUnreachable和MsgSnapStatus消息 
  13. MsgCheckQuorum    MessageType = 12 //Leader檢測是否保持半數以上的連接 
  14. MsgTransferLeader MessageType = 13 //Leader節點轉移時使用,本地消息 
  15. MsgTimeoutNow     MessageType = 14 //Leader節點轉移超時,會發該類型的消息,使Follower的選舉計時器立即過期,并發起新一輪的選舉 
  16. MsgReadIndex      MessageType = 15 //客戶端發往集群的只讀消息使用MsgReadIndex消息(只讀的兩種模式:ReadOnlySafe和ReadOnlyLeaseBased) 
  17. MsgReadIndexResp  MessageType = 16 //MsgReadIndex消息的響應消息 
  18. MsgPreVote        MessageType = 17 //PreCandidate狀態下的節點發送的消息 
  19. MsgPreVoteResp    MessageType = 18 //預選節點收到的響應消息   

然后是 raft 算法的實現,node 結構體實現了 Node 接口,對etcd-raft模塊具體實現的一層封裝,方便上層模塊使用etcd-raft模塊。其定義如下:

  1. type node struct { 
  2.  
  3. propc      chan msgWithResult      //該通道用于接收MsgProp類型的消息 
  4.  
  5. recvc      chan pb.Message         //除MsgProp外的其他類型的消息都是由該通道接收的 
  6.  
  7. confc      chan pb.ConfChangeV2    //當節點收到EntryConfChange類型的Entry記錄時,會轉換成ConfChange,并寫入該通道中等待處理。在ConfChange中封裝了其唯一 ID、待處理的節點 ID (NodeID 字段)及處理類型(Type 字段,例如,ConfChangeAddNode類型表示添加節點)等信息 
  8. confstatec chan pb.ConfState       //在ConfState中封裝了當前集群中所有節點的ID,該通道用于向上層模塊返回ConfState實例 
  9.  
  10. readyc     chan Ready              //Ready結構體的功能在上一小節已經介紹過了,該通道用于向上層模塊返回Ready實例,即node.Ready()方法的返回值 
  11.  
  12. advancec   chan struct{}           //當上層模塊處理完通過上述readyc通道獲取到的Ready實例之后,會通過node.Advance()方法向該通道寫入信號,從而通知底層raft實例 
  13.  
  14. tickc      chan struct{}                //用來接收邏輯時鐘發出的信號,之后會根據當前節點的角色推進選舉計時器和心跳計時器 
  15.  
  16. done       chan struct{}           //當檢測到done通道關閉后,在其上阻塞的goroutine會繼續執行,并進行相應的關閉操作 
  17.  
  18. stop       chan struct{}           //當node.Stop()方法被調用時,會向該通道發送信號,在后續介紹中會提到,有另一個goroutine會嘗試讀取該通道中的內容,當讀取到信息之后,會關閉done通道。 
  19.  
  20. status     chan chan Status        //注意該通道的類型,其中傳遞的元素也是Channel類型,即node.Status()方法的返回值 
  21.  
  22.  rn        *RawNode 
  23.  

下面我們來看看 raft StateMachine 的狀態機轉換,實際上就是 raft 算法中各種角色的轉換。每個 raft 節點,可能具有以下三種狀態中的一種。

  • Candidate:候選人狀態,該狀態意味著將進行一次新的選舉。
  • Follower:跟隨者狀態,該狀態意味著選舉結束。
  • Leader:領導者狀態,選舉出來的節點,所有數據提交都必須先提交到 Leader 上。

每一個狀態都有其對應的狀態機,每次收到一條提交的數據時,都會根據其不同的狀態將消息輸入到不同狀態的狀態機中。同時,在進行 tick 操作時,每種狀態對應的處理函數也是不一樣的。因此 raft 結構體中將不同的狀態及其不同的處理函數,獨立出來幾個成員變量:

  • state,保存當前節點狀態;
  • tick 函數,每個狀態對應的 tick 函數不同;
  • step,狀態機函數,同樣每個狀態對應的狀態機也不相同

我們接著看 etcd-raft 狀態轉換。etcd-raft StateMachine 封裝在 raft機構體中,etcd為了不讓entry落后的太多的直接進行選舉,多了一個其PreCandidate狀態,轉換如下圖:

raft 狀態轉換的接口都在 raft.go 中,其定義如下:

  1. //在newRaft()函數中完成初始化之后,會調用 becomeFollower()方法將節點切換成 Follower狀態,其中會設置raft實例的多個字段 
  2. func (r *raft) becomeFollower(term uint64, lead uint64) { 
  3.  r.step = stepFollower //設置函數處理Follower節點處理消息的行為 
  4.  r.reset(term) //在reset()方法中會重置raft實例的多個字段 
  5.  r.tick = r.tickElection //將tick字段設置成tickElection函數 
  6.  r.lead = lead //設置當前節點的leader節點 
  7.     //修改當前節點的角色 
  8.  r.state = StateFollower 
  9.  
  10. //如果當前集群開啟了 PreVote 模式,當 Follower 節點的選舉計時器超時時,會先調用becomePreCandidate()方法切換到PreCandidate狀態,becomePreCandidate() 
  11. func (r *raft) becomePreCandidate() { 
  12.     //檢查當前節點的狀態,禁止leader直接切換到PreCandidate狀態 
  13.  if r.state == StateLeader { 
  14.   panic("invalid transition [leader -> pre-candidate]"
  15.  } 
  16.     //設置函數處理Candidate節點處理消息的行為 
  17.  r.step = stepCandidate  
  18.  r.prs.ResetVotes() 
  19.  r.tick = r.tickElection 
  20.  r.lead = None 
  21.     //修改當前節點的角色 
  22.  r.state = StatePreCandidate  
  23. //當節點可以連接到集群中半數以上的節點時,會調用 becomeCandidate()方法切換到Candidate狀態,becomeCandidate() 
  24. func (r *raft) becomeCandidate() { 
  25.  // TODO(xiangli) remove the panic when the raft implementation is stable 
  26.  if r.state == StateLeader { 
  27.   panic("invalid transition [leader -> candidate]"
  28.  } 
  29.     //在reset()方法中會重置raft實例的多個字段 
  30.  r.step = stepCandidate 
  31.  r.reset(r.Term + 1) //在reset()方法中會重置raft實例的多個字段 
  32.  r.tick = r.tickElection 
  33.  r.Vote = r.id //在此次的選舉中,Candidate節點會將選票投給自己 
  34.     //修改當前節點的角色 
  35.  r.state = StateCandidate 
  36.  
  37. //當 Candidate 節點得到集群中半數以上節點的選票時,會調用 becomeLeader()方法切換成Leader狀態,becomeLeader() 
  38. func (r *raft) becomeLeader() { 
  39.     //檢查當前節點的狀態,機制從follower直接切換成leader狀態 
  40.  if r.state == StateFollower { 
  41.   panic("invalid transition [follower -> leader]"
  42.  } 
  43.  r.step = stepLeader 
  44.  r.reset(r.Term) //在reset()方法中會重置raft實例的多個字段 
  45.  r.tick = r.tickHeartbeat 
  46.  r.lead = r.id //將leader字段設置成當前節點的id 
  47.  r.state = StateLeader //更新當前節點的角色 
  48.     //檢查未提交的記錄中是否存在多條集群配置變更的Entry記錄 
  49.  r.prs.Progress[r.id].BecomeReplicate() 
  50.  r.pendingConfIndex = r.raftLog.lastIndex() 
  51.  emptyEnt := pb.Entry{Data: nil} 
  52.     //向當前節點的raftLog中追加一條空的Entry記錄 
  53.  if !r.appendEntry(emptyEnt) { 
  54.     } 
  55.  r.reduceUncommittedSize([]pb.Entry{emptyEnt}) 

tick 函數,每個狀態對應的 tick 函數不同,下面分析兩個tick:

  1. func (r *raft) tickElection() { 
  2.  r.electionElapsed++ //遞增electionElapsed計時器 
  3.  
  4.  if r.promotable() && r.pastElectionTimeout() { //檢查是否在集群中與檢查單簽的選舉計時器是否超時 
  5.   r.electionElapsed = 0 
  6.   r.Step(pb.Message{From: r.id, Type: pb.MsgHup}) //發起step處理pb.MsgHup類型消息。 
  7.  } 
  8.  
  9. func (r *raft) tickHeartbeat() { 
  10.  r.heartbeatElapsed++ //遞增heartbeatElapsed計時器 
  11.  r.electionElapsed++ //遞增electionElapsed計時器 
  12.  if r.electionElapsed >= r.electionTimeout { 
  13.   r.electionElapsed = 0 //重置選舉計時器,leader節點不會主動發起選舉 
  14.   if r.checkQuorum { //進行多數檢查 
  15.    r.Step(pb.Message{From: r.id, Type: pb.MsgCheckQuorum}) //發起大多數檢查。 
  16.   } 
  17.         //選舉計時器處于electionElapsed~randomizedElectionTimeout時段之間時,不能進行leader轉移 
  18.   if r.state == StateLeader && r.leadTransferee != None { 
  19.    r.abortLeaderTransfer() //清空raft.leadTransferee字段,放棄轉移 
  20.   } 
  21.  } 
  22.  if r.state != StateLeader { //只有laeder能發送tickHeartbeat 
  23.   return 
  24.  } 
  25.  if r.heartbeatElapsed >= r.heartbeatTimeout { //心跳計時器超時 
  26.   r.heartbeatElapsed = 0 //重置心跳計時器 
  27.   r.Step(pb.Message{From: r.id, Type: pb.MsgBeat}) //發起step處理MsgBeat類型消息 
  28.  } 

跟隨者、預選候選人、候選人、領導者 4 種節點狀態都有分別對應的功能函數,當需要查看各節點狀態相關的功能實現時(比如,跟隨者如何接收和處理日志),都可以將對應的函數作為入口函數,來閱讀代碼和研究功能實現。

日志復制

這里重點看一下raft.appendEntry()方法,它的主要操作步驟如下:(1)設置待追加的Entry記錄的Term值和Index值。

(2)向當前節點的raftLog中追加Entry記錄。

(3)更新當前節點對應的Progress實例。

(4)嘗試提交Entry記錄,即修改raftLog.committed字段的值。

raft.appendEntry()方法的具體實現如下:

  1. func (r *raft) appendEntry(es ...pb.Entry) (accepted bool) { 
  2.  li := r.raftLog.lastIndex()//獲取raftLog中最后一條記錄的索引值 
  3.  for i := range es {//更新待追加記錄的Term值和索引值 
  4.   es[i].Term = r.Term//Entry記錄的Term指定為當前leader節點的任期號 
  5.   es[i].Index = li + 1 + uint64(i) //為日志記錄指定的Index 
  6.  } 
  7.  li = r.raftLog.append(es...)//向raft中追加記錄 
  8.     //更新當前節點對應的Progress,主要是更新Next和Match 
  9.  r.prs.Progress[r.id].MaybeUpdate(li) 
  10.     //嘗試提交Entry記錄 
  11.  r.maybeCommit() 
  12.  return true 

在Progress.mayUpdate()方法中,會嘗試修改Match字段和Next字段,用來標識對應節點Entry記錄復制的情況。Leader節點除了在向自身raftLog中追加記錄時(即appendEntry()方法)會調用該方法,當Leader節點收到Follower節點的MsgAppResp消息(即MsgApp消息的響應消息)時,也會調用該方法嘗試修改Follower節點對應的Progress實例。Progress.MayUpdate()方法的具體實現如下:

  1. func (pr *Progress) MaybeUpdate(n uint64) bool { 
  2.  var updated bool 
  3.  if pr.Match < n { 
  4.   pr.Match = n //n之前所有的Entry記錄都已經寫入對應節點的raftLog中 
  5.   updated = true 
  6.         //下面將Progress.paused設置為false,表示leader節點可以繼續向對應Follower 
  7.         //節點發送MsgApp消息 
  8.   pr.ProbeAcked() 
  9.  } 
  10.  pr.Next = max(pr.Next, n+1)//將Next值加一,下一次復制Entry記錄開始的位置 
  11.  return updated 

如果該Entry記錄已經復制到了半數以上的節點中,則在raft.maybeCommit()方法中會嘗試將其提交。除了 appendEntry()方法,在 Leader 節點每次收到 MsgAppResp 消息時也會調用maybeCommit()方法,maybeCommit()方法的具體實現如下:

  1. func (r *raft) maybeCommit() bool { 
  2.  mci := r.prs.Committed() 
  3.  return r.raftLog.maybeCommit(mci, r.Term) 
  4.  
  5. func (p *ProgressTracker) Committed() uint64 { 
  6.  return uint64(p.Voters.CommittedIndex(matchAckIndexer(p.Progress))) 
  7. //將node分兩個組,JointConfig是大多數的組,有興趣的看一看quorum包的實現 
  8. func (c JointConfig) CommittedIndex(l AckedIndexer) Index {//比較大多數的node的前倆個Index,返回Match的值。 
  9.  idx0 := c[0].CommittedIndex(l) 
  10.  idx1 := c[1].CommittedIndex(l) 
  11.  if idx0 < idx1 { 
  12.   return idx0 
  13.  } 
  14.  return idx1 
  15. //更新raftLog.committed字段,完成提交 
  16. func (l *raftLog) maybeCommit(maxIndex, term uint64) bool { 
  17.  if maxIndex > l.committed && l.zeroTermOnErrCompacted(l.term(maxIndex)) == term { 
  18.   l.commitTo(maxIndex) 
  19.   return true 
  20.  } 
  21.  return false 

etcd 將 raft 相關的所有處理都抽象為了 Message,通過 Step 接口處理各類消息的入口,首先根據Term"值"對消息進行分類處理,再根據消息的"類型"進行分類處理:

  1. func (r *raft) Step(m pb.Message) error { 
  2.  switch {//首先根據消息的Term值進行分類處理 
  3.  case m.Term == 0://本地消息不做處理。MsgHup,MsgProp和MsgReadIndex是本地消息 
  4.  case m.Term > r.Term: 
  5.  case m.Term < r.Term://細節部分,可以自己研究源碼 
  6.  } 
  7.  switch m.Type {//根據Message的Type進行分類處理 
  8.  case pb.MsgHup://這里針對MsgHup類型的消息進行處理。 
  9.   if r.preVote {//檢查是不是開啟了preVote,如果是開啟了先調用raft.hup方法,發起preVote。 
  10.   } else { 
  11.    r.hup(campaignElection)//下面講述 
  12.   } 
  13.  case pb.MsgVote, pb.MsgPreVote: //對MsgVote,MsgPreVote類型的消息進行處理。 
  14.   canVote := r.Vote == m.From || 
  15.    (r.Vote == None && r.lead == None) || 
  16.    (m.Type == pb.MsgPreVote && m.Term > r.Term) 
  17.   if canVote && r.raftLog.isUpToDate(m.Index, m.LogTerm) { 
  18.    r.send(pb.Message{To: m.From, Term: m.Term, Type: voteRespMsgType(m.Type)}) 
  19.    if m.Type == pb.MsgVote { 
  20.     r.electionElapsed = 0 
  21.     r.Vote = m.From 
  22.    } 
  23.   } else { 
  24.    r.send(pb.Message{To: m.From, Term: r.Term, Type: voteRespMsgType(m.Type), Reject: true}) 
  25.   } 
  26.  default://對于其他類型的消息處理,對應的node的step函數處理 
  27.   err := r.step(r, m) 
  28.   if err != nil { 
  29.    return err 
  30.   } 
  31.  } 
  32.  return nil 

這里主要使用hup函數對Message來做處理,在raft.campaign()方法中,除了完成狀態切換,還會向集群中的其他節點發送相應類型的消息,例如,如果當前 Follower 節點要切換成 PreCandidate 狀態,則會發送 MsgPreVote 消息:

  1. func (r *raft) hup(t CampaignType) { 
  2.  if r.state == StateLeader {//忽略leader 
  3.   return 
  4.  } 
  5.     //方法會檢查prs字段中是否還存在當前節點對應的Progress實例,這是為了監測當前節點是否被從集群中移除了 
  6.     if !r.promotable() { 
  7.   return 
  8.  } 
  9.     //獲取raftLog中已提交但未應用的Entry記錄,異常處理 
  10.  ents, err := r.raftLog.slice(r.raftLog.applied+1, r.raftLog.committed+1, noLimit) 
  11.  r.campaign(t) 
  12. func (r *raft) campaign(t CampaignType) { 
  13.     //該方法的會發送一條包含Term值和類型 
  14.  var term uint64 
  15.  var voteMsg pb.MessageType 
  16.  if t == campaignPreElection {//切換的目標狀態是Precandidate 
  17.   r.becomePreCandidate() 
  18.   voteMsg = pb.MsgPreVote 
  19.         //確定要發送的Term值,這里只是增加了消息的Term值,并未增加raft.term字段的值 
  20.   term = r.Term + 1 
  21.  } else {//切換的目標狀態是Candidate 
  22.   r.becomeCandidate() 
  23.   voteMsg = pb.MsgVote 
  24.         //給raft.Term字段的值,并將當前節點的選票投給自身 
  25.   term = r.Term 
  26.  } 
  27.  if _, _, res := r.poll(r.id, voteRespMsgType(voteMsg), true); res == quorum.VoteWon { 
  28.         //當得到足夠的選票時,則將PreCandidate狀態的節點切換成Candidate狀態 
  29.         //Candidate狀態的節點則切換成Leader狀態 
  30.   if t == campaignPreElection { 
  31.    r.campaign(campaignElection) 
  32.   } else { 
  33.    r.becomeLeader() 
  34.   } 
  35.   return 
  36.  } 
  37.  var ids []uint64 
  38.  { 
  39.   idMap := r.prs.Voters.IDs() 
  40.   ids = make([]uint64, 0, len(idMap)) 
  41.   for id := range idMap { 
  42.    ids = append(ids, id) 
  43.   } 
  44.   sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) 
  45.  } 
  46.  for _, id := range ids {//狀態切換完成之后,當前節點會向集群中所有節點發送指定類型的消息 
  47.   if id == r.id { //跳過當前節點自身 
  48.    continue 
  49.   } 
  50.         var ctx []byte 
  51.         //在進行Leader節點轉移時,MsgPreVote或MsgVote消息會在Context字段中設置該特殊值 
  52.   if t == campaignTransfer { 
  53.    ctx = []byte(t) 
  54.   } 
  55.         //發送指定類型的消息,其中Index和LogTerm分別是當前節點的raftLog 
  56.         //最后一條消息的Index值和Term值 
  57.   r.send(pb.Message{Term: term, To: id, Type: voteMsg, Index: r.raftLog.lastIndex(), LogTerm: r.raftLog.lastTerm(), Context: ctx}) 
  58.  } 

Follower 節點在選舉計時器超時的行為:首先它會通過 tickElection()創建MsgHup消息并將其交給raft.Step()方法進行處理;raft.Step()方法會將當前Follower節點切換成PreCandidate狀態,然后創建MsgPreVote類型的消息,最后將該消息追加到raft.msgs字段中,等待上層模塊將其發送出去。

本文轉載自微信公眾號「運維開發故事」,可以通過以下二維碼關注。轉載本文請聯系運維開發故事公眾號。

 

責任編輯:姜華 來源: 運維開發故事
相關推薦

2021-07-21 09:48:20

etcd-wal模塊解析數據庫

2023-04-13 08:21:38

DevOpsAPI管理平臺

2021-01-28 08:55:48

Elasticsear數據庫數據存儲

2021-12-28 17:52:29

Android 動畫估值器

2021-02-20 11:20:21

Zabbix 5.4Zabbix運維

2021-05-19 08:12:39

etcd分布式鎖分布式系統

2021-10-27 09:38:40

JVM 虛擬機Java

2021-07-12 06:11:14

SkyWalking 儀表板UI篇

2021-07-01 11:56:04

etcd-wal模塊解析數據庫

2021-06-21 14:36:46

Vite 前端工程化工具

2023-03-29 07:45:58

VS編輯區編程工具

2021-04-14 14:16:58

HttpHttp協議網絡協議

2021-04-08 11:00:56

CountDownLaJava進階開發

2022-03-22 09:09:17

HookReact前端

2021-04-01 10:51:55

MySQL鎖機制數據庫

2021-03-12 09:21:31

MySQL數據庫邏輯架構

2022-02-17 08:53:38

ElasticSea集群部署

2022-04-29 14:38:49

class文件結構分析

2024-06-13 08:34:48

2022-02-25 15:50:05

OpenHarmonToggle組件鴻蒙
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产精品综合色区在线观看 | 亚洲不卡在线观看 | 精品国产乱码久久久久久图片 | 日韩在线高清 | 国产一区二区三区四区五区加勒比 | 国产一区二区三区网站 | 日韩在线一区二区 | 日韩精品一区二区三区在线播放 | 色伊人 | 久久99久久| 亚洲狠狠爱 | 久久久久久国产精品免费免费狐狸 | 久久久精品 | 欧美中文一区 | 99视频在线播放 | 91麻豆精品国产91久久久资源速度 | 国产午夜在线 | a在线视频观看 | 一级黄色片网址 | 狠狠干av | 你懂的在线视频播放 | 国产乱码一二三区精品 | 91精品综合久久久久久五月天 | 国产精品久久久久久久久久久免费看 | 99热精品在线观看 | 久久精品一区二区 | 欧美三级在线 | 97超碰在线免费 | 亚洲欧美aⅴ | 色综合网站 | 国产精品久久久久久久久免费软件 | 国产精品综合视频 | 亚洲三级在线观看 | 日韩精品 电影一区 亚洲 | 欧美精品一区二区三区蜜桃视频 | 91视频网址 | 国产玖玖| 天天摸天天干 | 亚洲综合一区二区三区 | 精品在线免费观看视频 | 国产日韩一区二区 |