本文使用 Zhihu On VSCode 创作并发布

本文是本人学习MIT 6.824 Lab 2A的笔记,包含了我自己的实现和理解。本系列其它文章、及本系列详细说明,请看:MIT 6.824 分布式系统 | 材料准备和环境搭建

本文md源码:AnBlog

Lab 2A实现著名的Raft算法。在对应论文中,作者们不断强调,Raft算法相较于Paxos等其它共识算法Consensus的优势在于可解释性Understandable,方便教学。即便如此,这个实现对我来说算是非常复杂了。我花了一天多,从早到晚不停研究,才终于让代码通过了Lab 2A的测试。

Debug多线程非常困难,多线程和系统调度自带的随机性,让代码有时能通过测试,有时不可以。每次不通过的原因还可能不同,又令我们雪上加霜。当然,正确的代码应该在任何情况下都能够通过。我跑二十多次Lab 2A测试,全部幸运地通过了,即便如此,我依然不能保证代码的正确性,更不必说效率优雅了。我能保证,我的代码大致正确。如果你发现了我的错误,请务必提醒我!

我将配合代码、文字、和简单的示意图讲解,整个算法非常复杂,我不能把所有代码全搬到文章里面。文章中呈现的代码应看做伪代码,而不是可以直接运行的代码。其中很多部分不会详细展开讲解,一方面是没有必要、避免啰嗦,另一方面要尽量缩减篇幅,你不必过于刨根问底

完整的、可以直接拿来抄的代码,请看我的GitHub仓库。当然,真的直接抄就没意思了,不自己操作一遍,你的损失特别大!我不想成为剥夺你学习机会的带恶人。

为了给你提供尽量多的帮助,同时保持你的学习机会,我将给你提供充分的准备工作指引,为你增加效率。同时,我尽量减少剧透,有些思考和尝试你最好亲自执行,在讲解这些部分之前,我用Darth Vader的注视提醒你,防止你直接划到答案上、丢掉了宝贵的学习机会。

别偷看哦

当然,如果你执意要看答案,不想自己思考,你也可以直接看,虽然少了很多乐趣。

本文篇幅较长,可以配合穿插的各个思维导图辅助理解。

准备工作

在开始写代码之前,请务必做好充分准备。这些准备工作很多,请你尽量做充分,准备中的工作量能顶好几倍之后的代码工作量,减少bug出现概率。做Lab 2A的准备工作尤为重要,Lab 2的其它部分也依赖这部分的知识,我们需要一个好的开始。

准备工作主要包括两部分,先理解Raft算法,再看看给好代码的结构。

思维导图

学习Raft

讲义中给了很多参考资料,Raft算法本身非常复杂,需要花较多精力理解。推荐你按如下顺序阅读这些材料:

  1. 官网简单介绍,只需要看到Raft Visualization标题之前。
  2. 一个很棒的可视化,慢慢把所有动画放完。
  3. Raft论文,主要看Figure 2Section 5
  4. 6.824助教写的注意事项

还有一些材料可以选择性阅读:

  • 6.824课程视频
  • Raft作者博士论文

如果你需要中文材料,可以参考知乎上的这些文章:

  • 本文

  • https://www.zhihu.com/question/29597104

  • https://zhuanlan.zhihu.com/p/110168818

请你花长时间,仔细研究以上材料,确保你能独立地在脑海中放出像一个很棒的可视化那样的动画,充分理解Raft算法。讲解Raft算法不是本文的主要内容,如果以后有兴致,再写一篇文章,下次一定!

几句话说说Raft原理

还是简单说一些。

Raft的目的是在多个机器上维持相同状态。客户端对Raft集群发送改变状态的请求,Raft算法保证,集群中所有机器的状态保持一致。

Raft通过维持一个操作记录结构抱保证一致性。通过机器之前交换信息,在所有机器中维护了一个log结构。这个结构按顺序记录了客户端向集群发送的所有操作请求,保证这个结构在所有机器上保持一致,就可以保持所有机器进行一致操作,进而保证广泛的一致性

Raft算法选中集群中的一个机器作为领导leader,起到整体调控的作用。这样的中心化设计在分布式领域非常常见,让整个开发工作轻松了很多。

log结构

这是Raft论文中的图,基本解释清楚了。leader引导整个流程,将自己的log同步到所有follower上。有的follower没有特别跟上,如第三个。也有完全跟上的,如第二个。方框的颜色表示执行到什么位置,大部分follower的执行和leader一致,都在执行x<-2。第三个followerleader执行不一致,之后等它的logleader同步之后,它也会执行相同操作,进而和其它机器一致。

无论是leader还是follower,都有发生错误离线的可能。令整个系统不受错误影响,也是Raft的职责。如果leader离线,就要重新产生leader,这是本文的主要内容。为了区分不同leader对应的不同时间段,Raft引入概念term,每次leader发生更换,都是一个新term

看看代码和任务总览

Lab 2A主要代码写在文件src/raft/raft.go。目录src/raft下的其它文件都是为测试服务的,测试主要流程在test_test.go

Lab 2A的测试流程主要在test_test.go的前两个函数TestInitialElection2A, TestReElection2A,主要关于leader产生重新产生Goland有对gotest的完全支持,请看Goland官方文档。使用Goland可以方便地运行单个测试,并使用断点调试

测试流程通过一些复杂的手段启动和管理各个Raft进程,我们最好通过测试流程test_test.go调用raft.go中的代码,而不是另外写一个main.go。主要调用和管理Raft进程的代码主要在config.go,调用raft.go中提供的函数Make, Kill, Raft.GetState,分别对应创建、销毁、查看Raft进程。

我们需要在raft.go中实现选举心跳信号,具体表现为两个功能:

  1. 一系列调用Make创建Raft进程后,进程之间选举出一个leader
  2. leader相对于其它进程离线后,其它进程在它们中间重新选举一个leader

这两个功能分别对应TestInitialElection2A, TestReElection2A,前者相对容易实现,后者需要Raft之外的一些设计。

在开始写代码之前

按惯例,除了论文还要阅读Lab 2 Notes,如果你还没阅读的话。前面没有指出来,是因为Lab 2 Notes和学习Raft这件事情关系不大。现在指出来,是因为Lab 2 Notes里面讲了很多代码介绍和要求,需要你特别注意。在这里归纳出几点:

  • Lab 2A主要和leader选举机制有关。
  • 主要编程指引在Raft论文的Figure 2中,必须按照它的逻辑严格执行,最好不自己发挥。
  • 使用的RPC命名和Figure 2中相同,用RequestVote索取选票,用AppendEntries实现心跳信号
  • 有一些和等待有关的参数需要小心选取。

除此之外,基于我自己写这个Lab的经验,再提醒你一下:

  • 没有理解清楚算法流程不要硬上,多看看论文,反复琢磨总是能懂的!
  • 不要先追求效率优雅,很多错误就是这么来的,先追求正确!
  • 多线程代码不要用断点调试了,可以尝试在终端print一些信息,像这样:
[Peer 1 Leader term 2] sending heartbeat signal to peer 2
[Peer 1 Leader term 2] all heartbeats sent, sleeping for 120 then send heartbeats again
[Peer 0 Follower term 2] got heartbeat message from leader peer 1 at term 2
[Peer 1 Leader term 2] heartbeat response from peer 0 received in 138.851µs success true
[Peer 2 Leader term 1] heartbeat response from peer 0 received in 70.168149ms success true
[Peer 2 Leader term 1] found peer 0 unreachable when sending heartbeats
[Peer 2 Leader term 1] sending heartbeat signal to peer 0

  • 我的完整实现请看我的GitHub仓库,不要照抄!!!

如果怎样都调不通,可以依次尝试:

  1. 调整等待时间参数
  2. 检查GetState方法是否正确
  3. 阅读终端输出信息
    1. 检查是否出现死锁
    2. 其它就先不剧透啦

说了这么多,你可以开始写代码了。在你写好之前,请不要继续阅读本文。后文中的一些结论,我花了很长时间才发现,本来一起列在上面了,后来还是觉得不要剧透太多,给你留多一点发挥的空间,才能学到更多,不应该把这个机会就这样抢走。我也不想成为剥夺你学习机会的带恶人。

图片之后正式开始。

别偷看哦
实现思维导图

Leader选举机制

看到这里,你应该对Raftleader选举机制有了自己的理解。如果你不能在脑海里大概地模拟整个过程,强烈建议你回到上文列出的材料中,再仔细体会。如果你觉得自己准备好了,就来看看我的理解。

只看单个进程的选举非常简单。一个在Candidate状态下的进程,等待一定时间之后,如果自己还是Candidate,就发起选举,尝试让自己成为leader。进程向所有其它进程拉票,也就是发动RequestVote RPC。根据其它进程发回的信息,判断是否成功获得选票。所有选票收集得到之后,如果选票个数超过了进程个数,则认为自己成功当选。如果没有成功当选,就维持candidate状态,等待一定时间之后再发动选举。

共识不是单方面的,进程认为自己当选了,其它进程也要这么想才是。一个认为自己是leader的进程,开始向其它进程发送心跳信号heartbeat,也就是发送AppendEntries RPC。若其它进程没有认可这个心跳信号,进程自觉退回follower状态。

注意到,进程发起选举、发送心跳信号都是主动的,主动尝试成为leader,主动验证是否成功。其它进程都是被动行动,接收到拉票才决定是否给出选票,接收到心跳信号才决定是否承认leader地位。

对于发起选举的进程,成功则成为leader,失败则维持candidate。对于参与选举的其它进程,选出leader则成为follower,未选出则维持原状。可以看到,发起选举参与其它选举并不矛盾,可以同时进行。当其中一边完成之后,另一边马上退出,也能得到正确结果。如果你愿意的话,这段短短的描述就是leader选举机制的正确性证明

复现Raft论文

有了Raft论文的Figure 2,我们可以依样画葫芦,亦步亦趋地按照论文中的要求来实现。简单实现之后,你应该可以让代码通过第一个测试TestInitialElection2A

进程状态和交换信息

Figure 2 State栏包含了一个Raft进程具有的所有信息,我们把它们全部加入Raft类中,部分属性还暂时用不到。

myState     int
currentTerm int
votedFor    intcommitIndex int
lastApplied int
nextIndex   int
matchIndex  int

两个RPC需要交换一些信息,分别列在ArgumentsResults下,我们把它们全部加入到对应ArgsReply类中。

type RequestVoteArgs struct {Term         intCandidateId  intLastLogIndex intLastLogTerm  int
}type RequestVoteReply struct {GrantVote boolTerm      int
}type AppendEntriesArgs struct {// machine stateTerm     intLeaderId int// log statePrevLogIndex intPrevLogTerm  int//entriesLeaderCommit int
}type AppendEntriesReply struct {Term    intSuccess bool
}

两个RPC

Figure 2详细描述了两个RPCAppendEntries, RequestVote的流程。我们需要定义这两个RPC,并按照Figure 2的指引正确执行。除了这两个RPC之外,还有一些行为,根据进程处在的状态而定,我们后面再讨论。

RequestVote有两部分,忽略旧term有条件给选票

当进程收到来自term比自己小的进程的拉票RequestVote,进程将自己的term值告诉它,并忽略。

if rf.tryDiscardOldTerm(args.CandidateId, args.Term) {reply.Term = rf.currentTermreply.GrantVote = falsereturn
}

当进程收到拉票RequestVote,自己的log满足一定条件,自己的投票状态votedFor也满足一定条件,则给出选票。votedFor指的是当前认定的leader。和log有关的判断暂时不关心。

if rf.votedFor < 0 || rf.votedFor == args.CandidateId {if rf.commitIndex <= args.LastLogIndex {reply.GrantVote = truerf.timerCleared = truereturn}
}

当进程接收到心跳信号AppendEntries,应该:

  1. 丢弃过期的term
  2. 暂时不关心其它和log有关的操作

几种状态的不同行为

进程可能处在三种状态,具有不同职能, Follower, Candidate, Leader。在不同状态下,进程接受/发送RequestVote, AppendEntriesRPC时的行为不同,我把和leader选举有关的列在下面。可以看做Rule for Servers的翻译。

处在Leader状态下时:

  1. 选举成为Leader成功,马上向所有其它进程发送心跳信号AppendEntries

处在Follower状态下时:

  1. 响应各种RPC
  2. 超过一段时间未接受到心跳信号,切换为candidate
  3. 接受到心跳信号,重置计时器避免切换为candidate
  4. 接受到拉票RequestVote,重置计时器避免切换为candidate

处在Candidate状态下时:

  1. 随机等待一定时间,然后发起选举尝试成为leader

    1. 增加自身term计数,进入一个新term
    2. 为自己增加一票。
    3. 向所有其它进程拉票。
  2. 收到超过半数的选票,成为leader,开始发送心跳信号
  3. 收到来自另一个进程的心跳信号AppendEntries,切换为follower
  4. 没有切换为leaderfollower,随机等待一段时间,重新发起选举

无论进程处在什么状态,具有什么职能,有些行为是共同的。我把和leader选举有关的列在下面:

  1. 接受到的RPC请求中、或得到的RPC响应中,包含的term信息,比自己的term值更大,需要切换到follower状态。

代码实现

完整代码过于冗长,机械地贴在这里只会空占篇幅。这里只抽象讲解我的实现框架,具体完整代码请看我的GitHub仓库。

Make函数中,我们初始化Raft对象,主要是设置等待参数,包括心跳信号频率等待心跳信号超时上限。除此之外,还把当前自己认定的leader设置为,用特殊值-1表达。

rf.votedFor = -1
rf.electWaitDuration = 300
rf.heartBeatWaitDuration = 120

当前状态应设置为follower

rf.myState = FollowerState

然后启动这个Raft进程,主要流程写在Run方法中。

go rf.Run()

主流程不断循环,根据进程的当前状态,执行不同的行为:

func (rf *Raft) Run() {for {switch rf.myState {case LeaderState:...breakcase FollowerState:...breakcase CandidateState:...breakdefault:panic("Invalid peer state!")}}
}

之后的就是机械执行Figure 2

完成以后,代码应该至少通过测试TestInitialElection2A

特别的设计要点

写出差不多正确的代码相对简单,机械地按照上面复现Figure 2就可以了,差不多的代码可以通过TestInitialElection2A测试。差不多的代码可能可以通过TestReElection2A测试,如果运气好的话。运气经常是时好时坏的,所以第二个测试可能有时通过,有时不通过。完全正确的代码才能总是通过,我们当然希望得到总是通过的完美结果。

如果你遇到了有时通过、有时不通过这样的情况,请你不要继续往下阅读,再多研究一下下,想想还有什么改进的办法。个人认为,这个部分是Lab 2A的精华,如果你能独立琢磨出此处玄机,一定收获巨大。如果你已经充分思考、尝试过了,图片之后正式开始。

别偷看哦

测试TestReElection2A是要看看,我们的Raft是否能在规定时间内重新选举一个leader。这就要求我们,不但要能够正确执行,还要能够在规定时间内正确执行。我们不得不进行额外设计,考虑和时间有关的参数。

和时间有关的参数大概有3个:

  1. Follower等待心跳信号的时间,超过这个时间没有收到新的心跳信号拉票,切换为Candidate
  2. Leader发送心跳信号之间的间隔时间。
  3. Candidate发起选举之前,随机等待时间的最大/最小值。

Debug了大半天,观察到一个现象:先用电脑干别的,再回来运行测试,通过的概率更大。这把我引向一个猜想:是否正确运行和操作系统线程/进程调度行为有关。不管这个猜想是否正确,这个现象让我更加相信,各种等待时间非常重要!

基于此信心,建议你先尝试在初始化函数Make中,调整各个等待时间参数的值,看看这样能否解决你的问题。这样的参数调整是治标不治本的,要真正解决问题,需要调整设计。

我将按自己的经历比较和引出新旧设计,并解释这样做的原因。这更多是我的心路历程。或许你已经采用了正确的设计,或许比我下面要说的更加优雅/高效,请你一定提醒我,我也想学习!

还是那句话,完整代码请看我的GitHub仓库。

同步异步

我一开始采用了非常原始的设计。进程向其它进程发送RPC时,一定要分别等待它们完全回复才继续执行其它流程。这是一种同步

同步阻塞等待

这样的设计可以通过测试TestInitialElection2A,因为它可以正确运行,但是不可以在规定时间内正确运行。若出现一个Peer,响应时间很长,或干脆没有响应,leader的其它流程就会被拖慢。leader本应发送心跳信号,或本应响应一个新的leader,可能因为这个同步等待而发生拖延,进而拖延整个集群。

解决同步的问题当然是采用异步机制。进程发送完心跳信号AppendEntries拉票RequestVote之后不等待其它进程的响应,而继续进行其它操作。之后收到响应,再调用一个回调函数处理响应。

举个例子,拉票发送请求如下:

for peerIndex, _ := range rf.peers {if peerIndex == rf.me {// does not send to myselfcontinue}if rf.myState != CandidateState {return}fmt.Println(rf.PrefixPrint(), "requesting vote from peer", peerIndex)rf.sendRequestVoteAsync(peerIndex, args, &replyArray[peerIndex], requestInfo)
}

方法sendRequestVoteAsync就是发送异步请求,收到请求之后调用回调函数requestVoteCallBack

go func() {if info.MustExit {return}sendBegin := time.Now()ok := rf.sendRequestVote(peerIndex, args, reply)sendEnd := time.Now()fmt.Println(rf.PrefixPrint(), "vote response from peer", peerIndex, "received in", sendEnd.Sub(sendBegin))rf.requestVoteCallBack(ok, peerIndex, args, reply, info)
}()

异步

这样的修改让测试TestReElection2A通过的概率变大了。

以上设计还有问题。

以上异步设计解决了:因一个Peer超时响应而没有及时给其它Peer发送同类请求。没有解决:因一个Peer超时响应某一类而没有及时给其它Peer发送其它类请求

如某进程发起选举,一定要等到所有进程都进行投票之后,才继续执行发送心跳信号等其它动作。其它进程不会因没有及时收到RequestVote而发生拖延,但可能因为没有及时收到AppendEntries而发生拖延。最终,整个集群固然可以正确运行,但是效率受到极大影响。

我们接着改进。

提前决策

发起选举为例。其实,进程不需要等到所有其它进程都发回投票再决定自己是否当选leader。当收到的Grant Vote数量超过集群中总的进程数量,就可以认为自己成功当选了!我们称之为提前决策

当出现一些Peer很久都没有响应,只要数量不多,对leader选举的影响就不大。

当进程发现自己已经可以成为leader时,就不再向其它进程发送新的拉票RequestVote,也不再等待其它进程的响应,可以直接继续进行。

if requestVoteInfo.SuccessCount+1 > requestVoteInfo.TotalCount/2 {// leader claimed!fmt.Println(rf.PrefixPrint(), "got", requestVoteInfo.SuccessCount, "votes in", requestVoteInfo.AliveCount, "alive peers", requestVoteInfo.TotalCount, "total peers")rf.myState = LeaderStaterf.votedFor = rf.mefmt.Println(rf.PrefixPrint(), "elected leader at term", rf.currentTerm)requestVoteInfo.SetMustExit()
}

提前决策

我花了很长时间才完善到这里,现在已经能通过测试TestReElection2A。有很多代码细节,就不在这里详细一一细展示了,你的具体实现可能和我很不一样。

更激进的Pipeline策略

本来已经完事了,写文章的时候又有了一个奇思妙想,在这里描述一下。可能可以更进一步加强效率,但难度较大,也不是那么有必要了。

还是拿发起选举举例子。进程在发动选举之后,不进行任何形式的等待,将自己的当前状态复制几份,分别以Leader, Follower, Candidate的状态同时运行。当选举完成之后,判断最终应处在哪个状态,保留那个状态,并丢弃其它状态。

可以想象,这个设计的实现是非常复杂了。我应该是没有兴致再来尝试了,如果你觉得这个猜想可行,就来试试吧。

简易定时器

比起前面讲到的,这里要说的相对没那么重要。我研究了一下Golang的原生定时器机制Timer, Ticker,看到很多评论说它们很。我没有信心完全掌握它们,不想在本来就bug百出的代码里再增加bug来源,也就没有使用它们。

这里介绍以下我自己设计的定时器机制,非常简单,非常不坑。当然,你也可以使用Golang的原生机制。

一次等待拆分成好几等分,是不是检查定时器是否被要求重置。若定时器被要求重置,则重新开始等待。

checkCount := 200
divDuration := rf.electWaitDuration / checkCount
for {for checkIndex := 0; checkIndex < checkCount; checkIndex++ {if rf.timerCleared {// restart timerbreak}if rf.votedFor == -1 {fmt.Println(rf.PrefixPrint(), "has no leader, converting to a candidate")rf.myState = CandidateStatereturn}time.Sleep(time.Millisecond * time.Duration(divDuration))if rf.myState != FollowerState {return}}rf.timerCleared = false
}

若等待完成,则说明没有收到心跳信号,需要切换为candidate

if !rf.timerCleared {// not brought here by a timeout event// timer manually resetfmt.Println(rf.PrefixPrint(), "did not receive heartbeat signal from supposed leader peer", rf.votedFor, "in", rf.electWaitDuration, "ms, converting to a candidate")// no leader nowrf.votedFor = -1rf.myState = CandidateStatereturn
}

外部流程重置定时器的方法是设置timerCleared属性。

rf.timerCleared = true

这个设计很简单,在这里只是稍微提一提,以免你可能因此对我的代码产生疑惑。

测试

第一个测试TestInitialElection2A相对确定,通过就没问题,不通过就是有错误。重点是TestReElection2A,即便代码不完美,也有可能通过。你不能满足于有时通过,应该跑很多次,保证是总是通过

希望你玩的开心。

raft算法_MIT 6.824 分布式系统 | Lab 2A:Raft选举相关推荐

  1. 分布式一致性算法——Paxos 和 Raft 算法

    写在前面 本文隶属于专栏<100个问题搞定大数据理论体系>,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢! 本专栏目录结构和参考文献请见100个问题搞定大数据理 ...

  2. Raft算法国际论文全翻译

    最近在开发强一致性的分布式算法,因此需要深入理解下Raft算法,这里对Raft论文进行了翻译,留以备用 - Sunface. 英文版论文:https://ramcloud.atlassian.net/ ...

  3. 终于搞懂Raft算法

    Raft 鱼排;木筏;木排;筏子,木筏协议. 木筏协议,一种共识算法,旨在替代PaxOS,相比容易理解许多.斯坦福大学的两个博士Diego Ongaro和John Ousterhout两个人以易懂为目 ...

  4. 【重点】一文搞懂Raft算法

    目录 raft算法概览 leader election term 选举过程详解 log replication Replicated state machines 请求完整流程 safety Elec ...

  5. 一文搞懂选举人算法(Raft算法)

    原文链接:http://www.cnblogs.com/xybaby/p/10124083.html raft是工程上使用较为广泛的强一致性.去中心化.高可用的分布式协议.在这里强调了是在工程上,因为 ...

  6. Raft算法的Leader选举和日志复制过程

    Raft 简介 Raft 是一种为了管理复制日志的一致性算法.它提供了和 Paxos 算法相同的功能和性能,但是它的算法结构和 Paxos 不同,使得Raft 算法更加容易理解并且更容易构建实际的系统 ...

  7. 【面试】Raft算法详解

    文章目录 前言 一.Raft算法概述 二.Leader选举 三.日志同步 四.安全性 五.日志压缩 六.成员变更 七.Raft与Multi-Paxos的异同 八.Raft算法总结 参考 前言 Paxo ...

  8. Nacos高级特性Raft算法以及原理和源码分析

    Nacos高级特性Raft算法以及原理和源码分析 对比springcloud-config配置中心 springcloud-config工作原理 Nacos的工作原理图 springcloud-con ...

  9. 【TIDB】拜占庭将军问题和Raft算法

    1 拜占庭将军问题(from 百度百科) 拜占庭将军问题是一个协议问题,拜占庭帝国军队的将军们必须全体一致的决定(数据的一致性)是否攻击某一支敌军.问题是这些将军在地理上是分隔开来的(分布式),并且将 ...

  10. 6.824 raft lab 2A 2B实验分析

    一.背景 为突破单机的容量和性能瓶颈,现在都采用分布式存储系统,而分布式存储系统就是采用分片扩大容量.多副本提供容错和性能的能力,可以简单.自动地横向扩展. 1 一致性算法介绍 raft就是非常著名的 ...

最新文章

  1. stm32采集脉冲信号_STM32 TIM 编码器模式采集编码器信号
  2. 成功解决xgboost\core.py, ValueError: feature_names may not contain [, ] or
  3. asp.net mvc 权限过滤和单点登录(禁止重复登录)
  4. group_concat 不是可以识别的 内置函数名称。_Python 函数库 APIs 编写指南
  5. MybatisPlus入门之快速入门
  6. 一些微服务拆分的浅见
  7. 淮安中专学计算机哪个学校好,2021淮安初中十强排名 哪些初中比较好
  8. Day05:装饰器,三元表达式,函数的递归,匿名/内置函数,迭代器,模块,开发目录...
  9. Bootstrap列表组
  10. Finally it is here - Physbam source code has been released!
  11. CUDA学习(一)之使用GPU输出HelloWorld
  12. (转)解决RabbitMQ service is already present - only updating service parameters
  13. 超级计算机的水冷散热,1U服务器集成16颗AMD 64核心霄龙:全水冷散热
  14. 动态设置全屏、取消全屏的方法,以及切换全屏保持内容位置不变的方法
  15. C++ priority_queue的使用及模拟实现
  16. 配电网PMU优化配置与状态估计(Matlab代码实现)
  17. JavaScript:实现计算二维平面上两点之间的距离算法(附完整源码)
  18. wordpress中Google Map V3 for IDN 插件的使用
  19. 高级查询(二)+php中文网,MySQL高级查询方法之记录查询
  20. mysql值locate()、position()、instr() 函数

热门文章

  1. 解决apache启动错误httpd:Could not reliably determine...
  2. 张樟兴策略分析:数据库营销顾客
  3. 55. 安全 HTTP(3)
  4. 47.使用外部 JavaScript 和 CSS(8)
  5. 5.Magento资源配置(Setup Resource)
  6. Linux下安装流量监控工具iftop
  7. C/C++ 知识点---链表操作
  8. C语言中extern关键字详解
  9. Python基础(7) - 函数
  10. 火狐—火狐浏览器中的“HttpWatch”