ProgressTracker

tracker是etcd数据库raft使用的单独一个包(raft/tracker),其核心类是ProgressTracker。从类名上看是Progress的Tracker,所以Progresstracker是用来跟踪Progress的,理解了什么是Progress才能明白ProgressTracker的是干什么的,但是这里先介绍一些简单的函数来看看ProgressTracker提供的功能。
ProgressTracker跟踪当前活动的配置以及其中的节点和learner的已知信息。 它跟踪每个对等点的匹配索引,这反过来又允许对提交的索引进行推理。ProgressTracker tracks the currently active configuration and the information known about the nodes and learners in it. In particular, it tracks the match index for each peer which in turn allows reasoning about the committed index.

// 代码源自raft/tracker/tracker.go
type ProgressTracker struct { // ProgressTracker是Progress的管理者,可以理解为Leader跟踪所有Peer的Progress    Voters   quorum.JointConfig  // 这个会在后面的章节说明,此处现忽略    Learners map[uint64]struct{}   // 所有的learners    Progress map[uint64]*Progress  // 所有的peer的Progress,map<node id,Progress>    Votes map[uint64]bool          // 对应node id的节点是否为自己投票,map<node id,bool>   MaxInflight int // 这个就是Inflights的容量
}

Progress代表了Leader视角的Follower进度,Leader拥有所有Follower的进度,并根据其进度向Follower发送日志。从官方注释来应该能看出一些端倪,笔者一句话概括:Progress是Follower追随Leader状态的进度,此处的状态主要指定的是日志的状态。对于raft来说系统状态的决策者是Leader,其他Follower都是从Leader同步的,确切的说是Leader发送给Follower的。作为Leader,需要知道所有Peer已经同步到什么程度了,所以就有了Progress。比如Leader已经有10条日志,Leader需要把这10条日志发给所有的Peer,那么Leader就需要记录Peer1已经发送到了第3条,Peer2已经发送到了第4条,以此类推。当然,Leader不可能就这么简单的跟踪每个peer的当前日志的进度,接下来看看Progress对于上述功能的定义:

// raft/tracker/progress.go
type Progress struct {// Leader与Follower之间的状态同步是异步的,Leader将日志发给Follower,Follower再回复接收// 到了哪些日志。出于效率考虑,Leader不会每条日志以类似同步调用的方式发送给Follower,而是// 只要Leader有新的日志就发送,Next就是用来记录下一次发送日志起始索引。换句话说就是发送给Peer// 的最大日志索引是Next-1,而Match的就是经过Follower确认接收的最大日志索引,Next-Match-1// 就是还在飞行中或者还在路上的日志数量(Inflights)。Inflights还是比较形象的,下面会有详细// 说明。Match, Next uint64

通过Progress的定义来看,总结如下,后续将讲解下面两点:

  • (0, Next)的日志已经发送给节点了,(0,Match]是节点的已经接收到的日志。
  • 探测状态通过ProbeSent控制探测消息的发送频率,复制状态下通过Inflights控制发送流量。
  • 如果Follower的返回消息中确认接收的日志索引大于Match,说明Follower开始接收日志了,那么就进入了复制状态。

Quorum机制

Quorum机制本质就是一个关于多数派的事情,这个多数派应用的有两个方面。

  • 选举过程:获得多数节点投票的节点才能获胜,成为 Leader ;
  • 运行过程:被多数节点 commit 的日志位置,这个才是被集群可靠记录的位置。被集群 commit 的日志才能被应用 apply ;
    那么这里有两个小思考问题:既然是选举过程,那怎么选举结果唱票的?既然是运行过程,那集群的这些节点怎么确认集群的 commit 位置?

选举投票箱和选举唱票

Votes的含义是真实世界的选举投票箱,ResetVotes函数用于清理选举投票箱,RecordVote函数记录具有给定 node id 的节点是否投票本Raft 实例(如果投票给该节点v == true,则设置Votes[id]为true,重复投票将不会再次设置Votes[id];如果拒绝投票给该节点v == false,则设置Votes[id]为false)。

// ResetVotes prepares for a new round of vote counting via recordVote.
func (p *ProgressTracker) ResetVotes() { p.Votes = map[uint64]bool{} }// RecordVote records that the node with the given id voted for this Raft instance if v == true (and declined it otherwise).
func (p *ProgressTracker) RecordVote(id uint64, v bool) {_, ok := p.Votes[id]if !ok { p.Votes[id] = v }
}

TallyVotes函数返回被批准和被拒绝的票数,以及选举结果是否已经出现。首先我们可以看到代码先遍历peer的进度结构体Progress,然后通过判断该peer是否参与投票或者还没有投票(Learner不参与投票、Votes投票箱里面还没有其对应的选票)剔除它们;如果已经投票给本Raft 实例,则递增granted,拒绝投票给该Raft 实例,则递增rejected;通过Config结构体中的Voters判定是否已经形成多数派表决优势。

// TallyVotes returns the number of granted and rejected Votes, and whether the election outcome is known.
func (p *ProgressTracker) TallyVotes() (granted int, rejected int, _ quorum.VoteResult) {// Make sure to populate granted/rejected correctly even if the Votes slice contains members no longer part of the configuration. This doesn't really matter in the way the numbers are used (they're informational), but might as well get it right. 确保正确填充已授予/已拒绝的成员,即使投票片段包含不再属于配置的成员。for id, pr := range p.Progress {if pr.IsLearner { continue }v, voted := p.Votes[id]if !voted { continue }if v { granted++ } else { rejected++ }}result := p.Voters.VoteResult(p.Votes)return granted, rejected, result // 返回投票和拒绝投票的节点数量以及唱票结果
}

这里我们看看Voters结构体是如何进行唱票的。ProgressTracker.Voters的类型是quorum.JointConfig,从quorum的定义可以推测这个模块做的基本都是跟法定人数,即超过一半以上的人数有关的功能,比如选举就需要超过一半以上的Peer支持才能成为Leader。这里我们需要提前知道一个和etcd向集群加入多个新节点的join consensus流程相关事实,后续博客将详细讲解:

  • 节点未发生变更时,节点信息存储在JointConfig[0]中,即incoming指向的集合中
  • 当EnterJoin时,将老节点拷贝至JointConfig[1] (outgoing)中,变更节点拷贝至JointConfig[0] (incoming)中
  • 当LeaveJoin时,删除下线节点,合并在线节点至JointConfig[0] (incoming)中,完成节点变更
    因此MajorityConfig的VoteResult函数提供的空Majority选举一定获胜的特性就体现在节点未发生变更时进行的唱票流程。也就是JointConfig[1]一定是VoteWon,这时如果JointConfig[0]是VoteWon,则返回VoteWon,也就是第一步的agreed state。如果JointConfig[1]为VoteLost,则VoteResult也是返回VoteLost,剩下就是选举结果未出的可能。进入EnterJoin和LeaveJoin时,如果JointConfig[0]和JointConfig[1]同时为VotePending、VoteLost、VoteWon时就返回相应状态;否则如果有一个是VoteLost,则整体VoteResult为VoteLost;如果一个为VoteWon,另一个为VotePending,则直接返回VotePending选举结果未出。
// VoteResult takes a mapping of voters to yes/no (true/false) votes and returns a result indicating whether the vote is pending, lost, or won. A joint quorum requires both majority quorums to vote in favor. VoteResult获取voters到是/否(真/假)投票的映射,并返回一个结果,指示投票是待定、失败还是获胜。联合法定人数要求两个多数票都投赞成票。
func (c JointConfig) VoteResult(votes map[uint64]bool) VoteResult {r1 := c[0].VoteResult(votes)  // 调用MajorityConfig的VoteResult函数,对于空的Majority选举一定获胜r2 := c[1].VoteResult(votes)  // 调用MajorityConfig的VoteResult函数,对于空的Majority选举一定获胜if r1 == r2 { return r1 } // If they agree, return the agreed state. if r1 == VoteLost || r2 == VoteLost { return VoteLost } // If either config has lost, loss is the only possible outcome.    return VotePending // One side won, the other one is pending, so the whole outcome is.
}
type VoteResult uint8
const ( // VotePending indicates that the decision of the vote depends on future    VotePending VoteResult = 1 + iota // votes, i.e. neither "yes" or "no" has reached quorum yet.    选举结果未出VoteLost // VoteLost indicates that the quorum has voted "no".  选举失败 VoteWon  // VoteWon indicates that the quorum has voted "yes".  选举成功
)

再看MajorityConfig提供的VoteResult函数,其流程很简单这里就不再详细叙述,可以直接看如下相应的描述。

// 代码源自raft/quorum/majority.go 参数votes是一个map,支持自己成为leader那么votes[peerID]=true,所以这个函数就是一个唱票的实现
func (c MajorityConfig) VoteResult(votes map[uint64]bool) VoteResult {// By convention, the elections on an empty config win. This comes in handy with joint quorums because it'll make a half-populated joint quorum behave like a majority quorum. 按照惯例,对于空的Majority选举一定获胜。这对于joint quorums很方便,因为它将使半填充的joint quorum行为类似于majority quorum。 if len(c) == 0 { return VoteWon }ny := [2]int{} // vote counts for no and yes, respectively // 统计支持者(nv[1])和反对者(nv[0])的数量var missing int //  当然还有弃权的,raft的弃权不是peer主动弃权的,而是丢包或者超时造成的for id := range c {  // 统计票数,这个也没啥好解释的了v, ok := votes[id]if !ok {missing++continue}if v { ny[1]++ } else { ny[0]++ }}q := len(c)/2 + 1  // 取半数值if ny[1] >= q { return VoteWon } // 支持者超过一半代表选举胜利if ny[1]+missing >= q { return VotePending } // 支持者和弃权数量超过一半以上选举挂起,因为可能还有一部分票还在路上~return VoteLost // 反对者超过一半以上肯定就失败了
}

怎么确认集群的 commit 位置

统计所有Peer的确认接收的最大日志索引,然后计算出超过一半以上Peer都确认接收的最大的日志索引,把这个值广播到所有Peer,让日志进一步被应用。和上述join consensus流程相关事实的原理一致,CommittedIndex函数返回给定JointConfig的最大提交索引。这里需要注意MajorityConfig.CommittedIndex函数再Peers数为0的时候返回无穷大,如果返回0,CommittedIndex函数就永远返回0了。

func (c JointConfig) CommittedIndex(l AckedIndexer) Index {idx0 := c[0].CommittedIndex(l)idx1 := c[1].CommittedIndex(l)if idx0 < idx1 { return idx0 } // 返回的是二者最小的那个,这时候可以理解MajorityConfig.CommittedIndex()为什么Peers数// 为0的时候返回无穷大了吧,如果返回0该函数就永远返回0了。return idx1
}

MajorityConfig.CommittedIndex函数根据通过提供的AckedIndex(用于active config)提供的索引计算提交的索引。这里需要注意的是AckedIndexer就是matchAckIndexer,通过matchAckIndexer可以获取所有节点Progress.Match,后面我们会提及该结构体,这里先理解它的作用。

// CommittedIndex computes the committed index from those supplied via the provided AckedIndexer (for the active config).
func (c MajorityConfig) CommittedIndex(l AckedIndexer) Index {n := len(c)if n == 0 { return math.MaxUint64 } // This plays well with joint quorums which, when one half is the zero MajorityConfig, should behave like the other half. 这里很有意思,当没有任何peer的时候返回值居然是无穷大(64位无符号范围内)// Use an on-stack slice to collect the committed indexes when n <= 7 (otherwise we alloc). The alternative is to stash a slice on MajorityConfig, but this impairs usability (as is, MajorityConfig is just a map, and that's nice). The assumption is that running with a replication factor of >7 is rare, and in cases in which it happens performance is a lesser concern (additionally the performance implications of an allocation here are far from drastic). 当n≤7时,使用堆栈切片收集提交的索引(否则我们分配)。另一种选择是在MajorityConfig上隐藏一个片段,但这会降低可用性(因为MajorityConfig只是一个地图,这很好)。假设在复制因子大于7的情况下运行是很少见的,在这种情况下,性能是一个较小的问题(此外,这里分配的性能影响远没有剧烈)。下面的代码对理解函数的实现原理没有多大影响,只是用了一个小技巧,在Peer数量不大于7个的情况下,优先用栈数组,否则通过堆申请内存。因为raft集群超过7个的概率不大,用栈效率会更高var stk [7]uint64var srt []uint64if len(stk) >= n { srt = stk[:n] } else { srt = make([]uint64, n) }{ // Fill the slice with the indexes observed. Any unused slots will be left as zero; these correspond to voters that may report in, but haven't yet. We fill from the right (since the zeroes will end up on the left after sorting below anyway). 用观察到的索引填充切片。任何未使用的插槽将保留为零;这些对应的选民可能会报告,但尚未。我们从右边填充(因为无论如何,在下面排序后,零将在左边结束)i := n - 1for id := range c {if idx, ok := l.AckedIndex(id); ok {srt[i] = uint64(idx)i--}}}// Sort by index. Use a bespoke algorithm (copied from the stdlib's sort package) to keep srt on the stack. 按索引排序。使用定制算法(从stdlib的排序包复制)将srt保留在堆栈上。insertionSort(srt) // 插入排序,这里只需要知道根据所有Peer.Progress.Match进行了排序即可,至于用什么排序并不重要// The smallest index into the array for which the value is acked by a quorum. In other words, from the end of the slice, move n/2+1 to the left (accounting for zero-indexing). 数组中的最小索引,其值由仲裁确认。换句话说,从切片的末尾向左移动n/2+1(说明索引为零)// 这句代码就是整个函数的精髓了,当前srt是按照peer.Progress.Match从小到达排好序了,此时需要 知道一个事情:Peer.Progress.Match代表了[0,Match]的日志全部被peer确认收到。有了这个前提 就非常容易理解了,可以把srt理解为按照处理速度升序排序的Peer。n - (n/2 + 1)之后的所有Peer 接收日志的速度都比它快,而在他之后包括他自己的节点数量正好超过一半,那么他的Match就是集群的 提交索引了。换句话说,有少于一半的节点的Match可能小于该节点的Match。pos := n - (n/2 + 1)return Index(srt[pos])
}

如下所示是上述函数的流程,先把集群每个节点的 commit 位置取出来(如下图所示),这里我们讲解一下AckedIndexer(matchAckIndexer通过matchAckIndexer可以获取所有节点Progress.Match)。

后来排个序是这样的,黑色的节点 commit 位置则是集群的 commit 位置,就是我们上面提的代码的精髓:

Committed返回已知的最大日志索引,该索引基于组的投票成员所确认的内容。matchAckIndexer通过AckedIndex可以获取所有节点Progress.Match。

type matchAckIndexer map[uint64]*Progress
// Committed returns the largest log index known to be committed based on what the voting members of the group have acknowledged.
func (p *ProgressTracker) Committed() uint64 { return uint64(p.Voters.CommittedIndex(matchAckIndexer(p.Progress))) }
func (l matchAckIndexer) AckedIndex(id uint64) (quorum.Index, bool) {pr, ok := l[id]  // 取出node id对应的Progressif !ok { return 0, false }return quorum.Index(pr.Match), true
}

Inflights机制

Inflights记录的是消息中最大日志的索引,所以它记录的是飞行中的消息的数量。Inflights存在于Progress结构体中,用于对peer对端节点传输的日志消息进行网络发送控制。

// 代码源自raft/tracker/inflights.go
// 在解释Inflights前先温习小学的数据题:有一个水池子,一个入水口,一个出水口,小学题一般会问什么时候
// 能把池子放满。Inflights就好像这个池子,当Progress在复制状态时,Leader向Peer发日志消息相当于
// 放水,Peer回复日志已经收到了相当于出水,当池子满了就不能放水了,也就是上面提到的暂停。作为一个
// 容量相对固定的池子,有入水口有出水口,而且需要按照进水的顺序出水,这正符合queue的特性。而raft的
// 实现没有使用queue,而是在一个内存块上采用循环方式模拟queue的特性,这样效率会更高。就这么多了干货
// 了,没有其他更有价值的内容了。
type Inflights struct {    start int // 因为是循环使用内存块,需要用起始位置+数量的方式表达Inflights中的日志,start和count就是 这两个变量。count int     size int  // size是内存块的大小  buffer []uint64 // buffer是内存块,Inflights只记录日志的索引值,而不是日志本身,有索引就足够了。
}
// 创建Inflights,需要给Inflights的容量
func NewInflights(size int) *Inflights {// 有没有注意到并没有为buffer申请内存?size是容量,但是实际运行过程中对于buffer的使用量可能// 远远低于容量,此时申请size大小的内存明显是一种浪费,所以设计者采用动态调整buffer大小的方法// 这个会在后面的函数中看到。此处来一个附加题,为什么实际运行过程中对于buffer的使用量可能远远// 低于容量?例如,容量是256,但是即使用的量可能只有16。首先,日志是以消息为粒度发送的,一个// 消息可以携带多个日志;其次,Inflights记录的是消息中最大日志的索引,所以它记录的是飞行中的// 消息的数量,那么折算成飞行中的日志数量就更多了;第三,正常情况下日志发送到节点到接收到节点// 的回复是非常快的,几毫秒到几十毫秒;第四,使用者在不频繁执行写操作的情况下节点间的IO性能基本// 能够满足写IO,Inflights的缓冲效果就不明显了。所以说,在大部分情况下,buffer的使用远到不// 了设置容量。return &Inflights{ size: size, }
}
// 向Inflights添加一个日志索引,就是向池子放水
func (in *Inflights) Add(inflight uint64) {// 如果已经满了是不能再添加的if in.Full() { panic("cannot add into a Full inflights") }// 找到新添加的日志应该在内存块的位置,因为是循环使用内存块,算法也比较简单:(count) % sizenext := in.start + in.countsize := in.sizeif next >= size { next -= size }// 这里有意思了,如果buffer大小不够了,那就再扩容。前面我们提到了,buffer不是上来就申请内存的if next >= len(in.buffer) { in.grow() }// 把日志索引存储bufferin.buffer[next] = inflightin.count++
}
// 为buffer扩容
func (in *Inflights) grow() {// 每次扩上次容量的2倍,不多解释了newSize := len(in.buffer) * 2if newSize == 0 { newSize = 1} else if newSize > in.size { newSize = in.size}// 把以前内存的内容拷贝到新内存上newBuffer := make([]uint64, newSize)copy(newBuffer, in.buffer)in.buffer = newBuffer
}
// 把小于等于to的日志全部释放,为什么不是把等于to的释放掉?这个很简单,如果节点回复的消息丢包了,那么
// 就会造成部分日志无法释放。raft里日志是有序的,搜到了节点回复消息的使用为n,那就说明节点已经收到了
// n以前的全部日志,所以可以把之前的全部释放掉。
func (in *Inflights) FreeLE(to uint64) {// 没有日志或者老旧消息则忽略if in.count == 0 || to < in.buffer[in.start] { return }// 找到第一个比to更大的日志idx := in.startvar i intfor i = 0; i < in.count; i++ {if to < in.buffer[idx] { break } // found the first large inflight// 此处还是循环使用内存的操作size := in.sizeif idx++; idx >= size { idx -= size}}// 调整start和countin.count -= iin.start = idx// 如果此时没有日志了,索性把start也调整到0的位置,我感觉这是coder的强迫症,哈哈~if in.count == 0 { in.start = 0 }
}
// 释放第一个日志
func (in *Inflights) FreeFirstOne() { in.FreeLE(in.buffer[in.start]) }
// 判断是否满了
func (in *Inflights) Full() bool { return in.count == in.size }
// 获取日志数量
func (in *Inflights) Count() int { return in.count }
// 复位
func (in *Inflights) reset() {in.count = 0in.start = 0
}

StateType

StateType有三种状态:探测(StateProbe)、复制(StateReplicate)、快照(StateSnapshot)。ProgressStateSnapshot状态表示Leader节点正在向目标节点发送快照数据。ProgressStateProbe状态表示Leader节点一次不能向目标节点发送多条消息,只能待一条消息被响应之后,才能发送下一条消息。当刚刚复制完快照数据、上次MsgApp消息被拒绝(或是发送失败)或是Leader节点初始化时,都会导致目标节点的Progress切换到该状态。ProgressStateReplicate状态表示正常的Entry记录复制状态,Leader节点向目标节点发送完消息之后,无须等待响应,即可开始后续消息的发送。

raft没有专门的探测消息,它是借助于其他消息实现的,比如心跳消息,日志消息等。任何消息的回复都算是一种探测。所以从ProgressStateProbe转为ProgressStateReplicate状态时任何消息发送成功就是其转换条件。

type Progress struct {// 此处顺便把StateType这个类型详细说明一下,StateType的代码在go.etcd.io/etcd/raft/tracker/state.go// Progress一共有三种状态,分别为探测(StateProbe)、复制(StateReplicate)、快照(StateSnapshot)// 探测:一般是系统选举完成后,Leader不知道所有Follower都是什么进度,所以需要发消息探测一下,从//    Follower的回复消息获取进度。在还没有收到回消息前都还是探测状态。因为不确定Follower是//    否活跃,所以发送太多的探测消息意义不大,只发送一个探测消息即可。// 复制:当Peer回复探测消息后,消息中有该节点接收的最大日志索引,如果回复的最大索引大于Match,//    以此索引更新Match,Progress就进入了复制状态,开启高速复制模式。复制制状态不同于//    探测状态,Leader会发送更多的日志消息来提升IO效率,就是上面提到的异步发送。这里就要引入//    Inflight概念了,飞行中的日志,意思就是已经发送给Follower还没有被确认接收的日志数据。// 快照:快照状态说明Follower正在复制Leader的快照State StateType PendingSnapshot uint64 // 在快照状态时,快照的索引值    RecentActive bool // 变量名字就能看出来,表示Follower最近是否活跃,只要Leader收到任何一个消息就表示节点是最近是活跃的。如果新一轮的选举,那么新的Leader默认为都是不活跃的。   ProbeSent bool // 探测状态时才有用,表示探测消息是否已经发送了,如果发送了就不会再发了,避免不必要的IO。// Inflight前面提到了,在复制状态有作用,后面有他的代码解析,此处只需要知道他是个限流的作用即可。// Leader不能无休止的向Follower发送日志,飞行中的日志量太大对网络和节点都是负担。而且一个日志// 丢失其后面的日志都要重发,所以过大的飞行中的日志失败后的重发成本也很大。

如下为StateType三种状态:探测(StateProbe)、复制(StateReplicate)、快照(StateSnapshot)的转换函数,我们这里详细注释一下,下篇博客详细解释该流程。

// 代码源自go.etcd.io/etcd/raft/tracker/progress.go
// Progress进入探测状态
func (pr *Progress) BecomeProbe() {// 代码注释翻译:如果原始状态是快照,说明快照已经被Peer接收了,那么Next=pendingSnapshot+1,// 意思就是从快照索引的下一个索引开始发送。if pr.State == StateSnapshot {// 此处用临时变量的原因是因为ResetState()会pr.PendingSnapshot=nilpendingSnapshot := pr.PendingSnapshotpr.ResetState(StateProbe)pr.Next = max(pr.Match+1, pendingSnapshot+1)} else {// ResetState的代码注释在下面pr.ResetState(StateProbe)// 上面的逻辑是Peer接收完快照后再探测一次才能继续发日志,而这里的逻辑是Peer从复制状态转// 到探测状态,这在Peer拒绝了日志、日志消息丢失的情况会发生,此时Leader不知道从哪里开始,// 倒不如从Match+1开始,因为Match是节点明确已经收到的。pr.Next = pr.Match + 1}
}
// Progress进入复制状态
func (pr *Progress) BecomeReplicate() {// 除了复位一下状态就是调整Next,为什么Next也是Match+1?进入复制状态肯定是收到了探测消息的// 反馈,此时Match会被更新,那从Match+1也就理所当然了。pr.ResetState(StateReplicate)pr.Next = pr.Match + 1
}
// Progress进入快照状态
func (pr *Progress) BecomeSnapshot(snapshoti uint64) {// 除了复位一下状态就是设置快照的索引,此处为什么不需要调整Next?因为这个状态无需在发日志给// peer,知道快照完成后才能继续pr.ResetState(StateSnapshot)pr.PendingSnapshot = snapshoti
}
// 复位状态
func (pr *Progress) ResetState(state StateType) {// 代码简单到无需解释pr.ProbeSent = falsepr.PendingSnapshot = 0pr.State = statepr.Inflights.reset()
}

ETCD数据库源码分析——ProgressTracker相关推荐

  1. ETCD数据库源码分析——Cluster membership changes日志

    Raft论文<CONSENSUS: BRIDGING THEORY AND PRACTICE>的第四章"集群成员变更"中,支持两种集群变更方式: 每次变更单节点,即&q ...

  2. Android面试题Service,Android面试题-IntentService源码分析

    自定义控件 联网 工具 数据库 源码分析相关面试题 Activity相关面试题 Service相关面试题 与XMPP相关面试题 与性能优化相关面试题 与登录相关面试题 与开发相关面试题 与人事相关面试 ...

  3. 《源码分析转载收藏向—数据库内核月报》

    月报原地址: 数据库内核月报 现在记录一下,我可能需要参考的几篇文章吧,不然以后还得找: MySQL · 代码阅读 · MYSQL开源软件源码阅读小技巧 MySQL · 源码分析 · 聚合函数(Agg ...

  4. 数据库中间件 MyCAT源码分析 —— XA分布式事务

    title: MyCAT 源码分析 -- XA分布式事务 date: 2017-07-15 tags: categories: MyCAT permalink: MyCAT/xa-distribute ...

  5. PG数据库内核源码分析——UPDATE

    PG中UPDATE源码分析 本文主要描述SQL中UPDATE语句的源码分析,代码为PG13.3版本. 整体流程分析 以 update dtea set id = 1;这条最简单的Update语句进行源 ...

  6. 云客Drupal源码分析之数据库Schema及创建数据表

    本主题是<云客Drupal源码分析之数据库系统及其使用>的补充,便于查询,所以独立成一个主题 讲解数据库系统如何操作Schema(创建修改数据库.数据表.字段:判断它们的存在性等等),以及 ...

  7. pbp 读取 mysql数据_SqlAlchemy 中操作数据库时session和scoped_session的区别(源码分析)...

    原生session: from sqlalchemy.orm import sessionmaker from sqlalchemy import create_engine from sqlalch ...

  8. Etcd源码分析-存储3

    Etcd是分布式存储系统,当leader有数据变化,要及时更新到其他节点,这里就涉及到数据同步. 一.数据同步 上一篇介绍,Etcd接收到客户端的请求,会把相关数据传递到Raft状态机中,那么进入状态 ...

  9. zookeeper服务发现实战及原理--spring-cloud-zookeeper源码分析

    1.为什么要服务发现? 服务实例的网络位置都是动态分配的.由于扩展.失败和升级,服务实例会经常动态改变,因此,客户端代码需要使用更加复杂的服务发现机制. 2.常见的服务发现开源组件 etcd-用于共享 ...

最新文章

  1. mysql导入数据io异常_mysql 数据同步 出现Slave_IO_Running:No问题的解决方法小结
  2. webSocket浏览器握手不成功(解决)
  3. POJ-2391 Ombrophobic Bovines 网络流-拆点构图
  4. 我是怎么通过技术白手起家创业 续2
  5. 服务器修改kb,Microsoft KB2344941:操作系统即插即用方式的改变,提高iSCSI Initiator的适应力...
  6. 一种 Android 应用内全局获取 Context 实例的装置
  7. Python习题11
  8. mysql分表方法实现
  9. Jackson用法详解
  10. 在rac集群上开启OEM
  11. [思]刻意练习是不是这样的
  12. 【算法导论】第24章迪杰斯特拉算法
  13. unity打包IOS填坑1
  14. actviti 工作流核心技术和实战-学习笔记(一)什么是工作流
  15. android 360度全景,android 360度全景展示
  16. xp信息服务器iis5.0,XP中安装iis5.0/IIS6.0的详细操作方法步骤(图文教程)
  17. html 上下左右箭头按钮,css 上下左右箭头
  18. switch vba_VBA switch
  19. 红色高端爱家Aijiacms大型房产门户系统V9网站源码+带WAP
  20. STAC: A Simple Semi-Supervised Learning Framework for Object Detection

热门文章

  1. 我就这样忍了一生——星云大师
  2. 华为HMS全球应用创新大赛启动 百万美元奖金激励开发者
  3. 13代酷睿移动端处理器:HX、H、P和U系列区别是什么?
  4. C/C++基础讲解(二十六)之数值计算与趣味数学篇(打鱼还是晒网与怎样存钱以获取最大利息)
  5. 选择防身武器,利用身边的物品才是王道
  6. 如何用uniapp+vue开发自定义相机插件——拍照+录像功能
  7. Android项目开发:指南针(两种方法实现)
  8. 使用手机软件Bluino Loader通过蓝牙编程、烧录Arduino
  9. 《创新者的基因》读书笔记
  10. Win10系统wifi图标消失无法联网怎么办