热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

MIT6.824lab2PartB

引言感谢MIT为全球有此兴趣的人提供如此高质量的课程,这门课程让我感受到了分布式的美妙与深邃,做lab1的时候还感觉云里雾里,因为有

引言

感谢MIT为全球有此兴趣的人提供如此高质量的课程,这门课程让我感受到了分布式的美妙与深邃,做lab1的时候还感觉云里雾里,因为有一点点兴趣,遂也继续下去,做完lab2,终于明白了我不是干程序员的料,已成功转行,现在已经把电脑砸了,正在市中心推销游泳健身,一月加提成2W+,感谢这个实验为我带来的一切。


简单发一个小小的牢骚,看官一笑即可~~ 不过有一说一,这个课程的part B非常有难度,如果说lab1,和lab2 PartA只是小打小闹的话,PartB就是正儿八经的大刀阔斧了。PartB引入了很多容错问题,这些问题有一些是在paper中有所提及的,但很多其实并没有在paper中提到,这导致在过不了测试样例的时候我们需要根据测试代码去找是哪里出了问题,我是一个分布式小白,同时也是一个Go小白,对于我来说分布式代码最好调试方式就是去打印(也许叫日志不那么low),去分析是哪里出现了问题,同时还需要结合paper才能分析出一些问题的原因所在。

这篇文章的主体思路是先分析下PartB的逻辑过程,然后说几点我在写的时候遇到的错误,最后逐函数说一下作用,最后附上全部的代码,也就是raft.go这个文件。

逻辑过程

首先PartB部分我们需要完成的是”实现领导者和随从者代码达到追加新的日志节点的目标“,也就是Raft算法的第二部分,即附加日志,这里我们需要考虑容错问题,即leader宕机,小部分follower宕机可以完成协议,大部分follower宕机无法完成协议,并发写入等各种情况。因为框架中只给我们了一个start函数,所以RPC部分也需要我们自己补充,不过这个并不困难。想要理解start函数干了什么,我们还是得看看测试代码,我们看看最简单的测试函数TestBasicAgree

func TestBasicAgree(t *testing.T) {
servers := 5
cfg := make_config(t, servers, false) // 创建5个节点,并互相连接
defer cfg.cleanup()
fmt.Printf("Test: basic agreement ...\n")
iters := 3
for index := 1; index nd, _ := cfg.nCommitted(index) //有多少个节点认为下标为index的日志已经提交 返回{已提交数,命令}
if nd > 0 {
// 这里注意index始终大于正常的日志 所以这里不能大于0
t.Fatalf("some have committed before Start()")
}
xindex := cfg.one(index*100, servers) //实际上就是完成了一次协议 即向Leader提交一个命令
if xindex != index {
t.Fatalf("got index %v but expected %v", xindex, index)
}
}
fmt.Printf(" ... Passed\n")
}

func (cfg *config) one(cmd int, expectedServers int) int {
t0 := time.Now()
starts := 0
for time.Since(t0).Seconds() <10 {
// try all the servers, maybe one is the leader.
index :&#61; -1
for si :&#61; 0; si starts &#61; (starts &#43; 1) % cfg.n
var rf *Raft
cfg.mu.Lock()
if cfg.connected[starts] {
rf &#61; cfg.rafts[starts]
}
cfg.mu.Unlock()
if rf !&#61; nil {
index1, _, ok :&#61; rf.Start(cmd) //这个函数使我们要填写的函数 返回写入日志的index
if ok {
//返回ok证明下表是Leader
index &#61; index1
break
}
}
}
if index !&#61; -1 {
//找到了一个Leader
// somebody claimed to be the leader and to have
// submitted our command; wait a while for agreement.
t1 :&#61; time.Now()
for time.Since(t1).Seconds() <2 {
//循环两秒
nd, cmd1 :&#61; cfg.nCommitted(index) //查看start函数返回值index位置上的日志是否是cmd
if nd > 0 && nd >&#61; expectedServers {
//已经提交的日志数大于expectedServers
// committed
if cmd2, ok :&#61; cmd1.(int); ok && cmd2 &#61;&#61; cmd {
// and it was the command we submitted.
return index
}
}
time.Sleep(20 * time.Millisecond)
}
} else {
time.Sleep(50 * time.Millisecond)
}
}
cfg.t.Fatalf("one(%v) failed to reach agreement", cmd)
return -1
}

我们可以看到Start函数实际的语义其实就是提交一个日志&#xff0c;而之后如何把它同步到follower&#xff0c;那就是我们的问题了&#xff0c;这里我的方法就是在心跳包中进行同步&#xff0c;不做额外的逻辑&#xff0c;在应该发送心跳包的时候如果有额外的日志就附带上日志&#xff0c;如果没有的话就是一个简单的心跳包。

这里我们也可以看出其实心跳包和一般的附加日志的结构体其实在逻辑上是差不多的&#xff0c;只不过heartbeat没有日志项而已&#xff0c;然后的问题是这个结构体该如何设计&#xff0c;我们使用论文中结构&#xff0c;如下&#xff1a;

type AppendEntryArgs struct {
Termint // leader的任期号
LeaderId int // leaderID 便于进行重定向
PrevLogIndex int // 新日志之前日志的索引值
PrevLogTerm int // 新日志之前日志的Term
Entries[]LogEntry // 存储的日志条目 为空时是心跳包
LeaderCommit int // leader已经提交的日志的索引
}
type AppendEntryReply struct {
Term int // 用于更新leader本身 因为leader可能会出现分区
Success bool // follower如果跟上了PrevLogIndex,PrevLogTerm的话为true,否则的话需要与leader同步日志
CommitIndex int // 用于返回与leader.Term的匹配项,方便同步日志
}

如果你看过Raft动画演示[1]的话&#xff0c;你就会知道在选举阶段有两个超时时间&#xff0c;一个是electionTimeoutheartbeatTimeout&#xff0c;这是非常重要的一件事情&#xff0c;我们上面说道&#xff0c;日志同步的过程其实和心跳包的过程差不多&#xff0c;唯一的区别就是一个日志项不为空&#xff0c;一个为空而已。所以当有一个日志项在leader中被添加的时候我们的处理逻辑是什么呢&#xff1f;也就是Start函数逻辑&#xff0c;我们只需要加入日志项&#xff0c;等到heartbeatTimeout的时候会自动发送出附加日志消息&#xff0c;这样看来PartB我们首先需要写一个heartbeat超时事件&#xff0c;其中当然涉及到RPC&#xff0c;然后还需要写一个RPC的处理函数&#xff0c;随后再补充一个处理RPC处理函数的函数&#xff08;好像有一点绕&#xff09;。

易错点

实验中也明确提到这一部分最重要的部分就是容错部分&#xff0c;而且有些错误是论文中也没有提到的&#xff0c;所以需要我们以身试错&#xff0c;以下的一些点希望大家能在完成实验的时候注意&#xff1a;

  1. 可能出现小部分follower出现网络分区&#xff0c;如果我们不加以判断&#xff0c;就会出现这部分follower不停的进行选举&#xff0c;然后选举失败&#xff0c;由进行选举&#xff0c;这样它们的Term会暴增&#xff0c;远大于没有出现分区的节点的Term&#xff0c;导致重连以后丢失大量的日志。
  2. 并行添加日志的时候各节点收到的顺序可能不同&#xff0c;需要我们使用PrevlogIndex进行维护。
  3. 重设时钟的时候一定要记得先关闭&#xff0c;因为对Go不熟&#xff0c;这样导致Term瞬间突破天际。
  4. 在发送leader发送完心跳包&#xff0c;leader端的处理函数中一定要重置时间&#xff0c;否则会导致不停的进行选举。
  5. 在发送日志项和leader处理日志项的时候一定要判断当前节点是否还是leader&#xff0c;因为可能有一些消息会延迟到来&#xff0c;导致这种情况&#xff0c;这是真实会发生的。
  6. 再就是paper中所强调的了。

正文

首先是RPC和leader在成功选举的时候需要广播选举信息的函数SendAppendEntriesToAllFollwer&#xff0c;

func (rf *Raft) SendAppendEntryToFollower(server int, args AppendEntryArgs, reply *AppendEntryReply) bool {
ok :&#61; rf.peers[server].Call("Raft.AppendEntries", args, reply)
return ok
}
// 用于发送附加日志项给其他服务器 也就是心跳包 超时时间为heartbeatTimeout
func (rf *Raft) SendAppendEntriesToAllFollwer() {
for i :&#61; 0; i if i &#61;&#61; rf.me {
continue
}
var args AppendEntryArgs
args.Term &#61; rf.currentTerm
args.LeaderId &#61; rf.me
args.PrevLogIndex &#61; rf.nextIndex[i] - 1 // 当前最新的索引
if args.PrevLogIndex >&#61; 0 {
//fmt.Printf("%d %d\n",args.PrevLogIndex,len(rf.logs))
args.PrevLogTerm &#61; rf.logs[args.PrevLogIndex].Term
}
// 当我们在Start中加入一条新日志的时候这里会在心跳包中发送出去
if rf.nextIndex[i] // 刚成为leader的时候更新过 所以第一次entry为空
args.Entries &#61; rf.logs[rf.nextIndex[i]:] //如果日志小于leader的日志的话直接拷贝日志
}
args.LeaderCommit &#61; rf.commitIndex
go func(servernumber int, args AppendEntryArgs, rf *Raft) {
var reply AppendEntryReply
retry :
if rf.current_state !&#61; "LEADER"{
return
}
ok :&#61; rf.SendAppendEntryToFollower(servernumber, args, &reply)
if ok {
rf.handleAppendEntries(servernumber, reply)
}else {
goto retry //附加日志失败的时候重新附加 这是很重要的 follower中对于附加日志项是幂等的
}
}(i, args, rf)
}
}

然后是follower中日志的处理函数AppendEntries&#xff0c;具体的逻辑是这样的

  1. 判断当前Term和leaderTerm的大小 前者大于后者的话拒绝 小于的话改变节点状态
  2. 进行一个错误判断,Leader节点保存的nextIndex为leader节点日志的总长度&#xff0c;而Follwer节点的日志数目可能不大于nextIndex,原因是可能这个follower原来可能是leader,一部分数据还没有提交,或者原来就是follower,但是有一些数据丢失,此时要使leader减少PrevLogIndex来寻找来年各个节点相同的日志。论文[5.3]
  3. Follwer节点的日志数目比Leader节点记录的NextIndex多&#xff0c;则说明存在冲突&#xff0c;则保留PrevLogIndex前面的日志,在尾部添加RPC请求中的日志项并提交日志
  4. 如果RPC请求中的日志项为空&#xff0c;则说明该RPC请求为Heartbeat&#xff0c;改变当前节点状态,因为可能此节点当前还是CANDIDATE,并提交未提交的日志

func (rf *Raft) AppendEntries(args AppendEntryArgs, reply *AppendEntryReply) {
rf.mu.Lock()
defer rf.mu.Unlock()
if rf.currentTerm > args.Term {
reply.Success &#61; false
reply.Term &#61; rf.currentTerm
// 其实没必要重置时钟 因为出现一个节点收到落后于自己的Term我认为只可能在分区的时候,
// 这个时候的这个leader其实没有什么意义&#xff0c;但是设置了也无妨
rf.resetTimer()
return
} else {
rf.current_state &#61; "FOLLOWER" // 改变当前状态
rf.currentTerm &#61; args.Term // 落后于leader的时候更新Term
rf.votedFor &#61; -1 // 更新voteFor 否则在下一轮选举中可能出现两个Leader&#xff0c;在选举的时候初始化也可以
reply.Term &#61; rf.currentTerm
if args.PrevLogIndex >&#61; 0 && // 首先leader有日志
(len(rf.logs)-1 rf.logs[args.PrevLogIndex].Term !&#61; args.PrevLogTerm) {
// 或者在相同index上日志不同
reply.CommitIndex &#61; len(rf.logs) - 1
if reply.CommitIndex > args.PrevLogIndex {
reply.CommitIndex &#61; args.PrevLogIndex //多出的日志一定会被舍弃掉 要和leader同步
}
for reply.CommitIndex >&#61; 0 {
if rf.logs[reply.CommitIndex].Term !&#61; args.Term {
reply.CommitIndex--
} else {
break
}
}
//返回false说明要此节点日志并没有更上leader,或者有多余或者不一样的地方
//出现的原因是这个节点以前可能是leader,在一些日志并没有提交之前就宕机了
reply.Success &#61; false
} else if args.Entries &#61;&#61; nil {
// 心跳包 用于更新状态
if rf.lastApplied&#43;1 <&#61; args.LeaderCommit {
//TODO len(rf.logs)-1 改为 rf.lastApplied&#43;1
rf.commitIndex &#61; args.LeaderCommit
go rf.commitLogs() // 可能提交的日志落后与leader 同步一下日志
}
reply.CommitIndex &#61; len(rf.logs) - 1
reply.Success &#61; true
} else {
//日志项不为空 与leader同步日志
rf.logs &#61; rf.logs[:args.PrevLogIndex&#43;1] // debug: 第一次调用PrevLogIndex为-1
rf.logs &#61; append(rf.logs, args.Entries...)
if rf.lastApplied&#43;1 <&#61; args.LeaderCommit {
rf.commitIndex &#61; args.LeaderCommit // 与leader同步信息
go rf.commitLogs()
}
// 如果 leaderCommit > commitIndex&#xff0c;令 commitIndex 等于 leaderCommit 和 新日志条目索引值中较小的一个
reply.CommitIndex &#61; len(rf.logs) - 1
if args.LeaderCommit > rf.commitIndex{
if(args.LeaderCommit reply.CommitIndex &#61; args.LeaderCommit
}
}
reply.Success &#61; true
}
rf.resetTimer() // 很重要
}
}

然后是处理心跳包返回值的函数handleAppendEntries,以下为处理逻辑&#xff1a;

  1. 如果返回值中的Term大于leader的Term&#xff0c;证明出现了分区&#xff0c;节点状态转换为follower
  2. 如果RPC成功的话更新leader对于各个服务器的状态
  3. 如果RPC失败的话证明两边日志不一样&#xff0c;使用前面提到的reply。CommitIndex作为nextIndex&#xff0c;用于请求参数中的PrevLogIndex

func (rf *Raft) handleAppendEntries(server int, reply AppendEntryReply) {
rf.mu.Lock()
defer rf.mu.Unlock()
if rf.current_state !&#61; "LEADER" {
//log.Fatal("Error in handleAppendEntries, receive a heartbeat reply, but not a leader.")
return
}
if reply.Term > rf.currentTerm {
// 出现网络分区 这是一个落后的leader
rf.current_state &#61; "FOLLOWER"
rf.currentTerm &#61; reply.Term
rf.votedFor &#61; -1
rf.resetTimer()
return
}
if reply.Success {
rf.nextIndex[server] &#61; reply.CommitIndex &#43; 1 //CommitIndex为对端确定两边相同的index 加上1就是下一个需要发送的日志
rf.matchIndex[server] &#61; reply.CommitIndex
if rf.nextIndex[server] > len(rf.logs){
//debug
rf.nextIndex[server] &#61; len(rf.logs)
rf.matchIndex[server] &#61; rf.nextIndex[server] - 1
//log.Fatal("ERROR : rf.nextIndex[server] > len(rf.logs)\n")
}
commit_count :&#61; 1
for i :&#61; 0; i if i &#61;&#61; rf.me {
continue
}
// 这里可以和其他服务器比较matchIndex 当到大多数的时候就可以提交这个值
if rf.matchIndex[i] >&#61; rf.matchIndex[server] {
//matchIndex 对于每一个服务器&#xff0c;已经复制给他的日志的最高索引值
commit_count&#43;&#43;
}
}
if commit_count >&#61; len(rf.peers)/2&#43;1 &&
rf.commitIndex rf.logs[rf.matchIndex[server]].Term &#61;&#61; rf.currentTerm{
rf.commitIndex &#61; rf.matchIndex[server]
go rf.commitLogs() //提交日志 下次心跳的时候会提交follower中的日志
}
} else {
//rf.nextIndex[server] &#61; int(math.Min(float64(reply.CommitIndex &#43; 1),float64(len(rf.logs)-1)))
rf.nextIndex[server] &#61; reply.CommitIndex &#43; 1
if rf.nextIndex[server] > len(rf.logs){
//debug
rf.nextIndex[server] &#61; len(rf.logs)
//log.Fatal("ERROR : rf.nextIndex[server] > len(rf.logs)\n")
}
rf.SendAppendEntriesToAllFollwer() //TODO 发送心跳包 其实发送单个人即可 有问题后面再改 先用已有函数
}
rf.resetTimer() // TODO 很重要 要不后面不发心跳包 导致不停的选举 Term往上飙
}

然后就是提交日志&#xff0c;刚开始的时候其实比较懵逼&#xff0c;当我们去查看测试代码中的start1的时候我们会发现这里面对每一个Raft节点都会跑一个goroutine&#xff0c;里面有一个channel&#xff0c;不停的往cfg.logs里面写日志&#xff0c;而在检查已提交日志的时候就在cfg.logs里面检查&#xff0c;所以提交日志其实就是往每个raft节点初始化的时候传入的那个channel里面写数据就可以了。所以提交日志的过程不难&#xff1a;

func (rf *Raft) commitLogs() {
rf.mu.Lock()
defer rf.mu.Unlock()
if rf.commitIndex > len(rf.logs)-1 {
log.Fatal("出现错误 : raft.go commitlogs()")
}
// 初始化是-1
for i :&#61; rf.lastApplied &#43; 1; i <&#61; rf.commitIndex; i&#43;&#43; {
//commit日志到与Leader相同
// listen to messages from Raft indicating newly committed messages.
// 调用过程才test_test.go -> start1函数中
// 很重要的是要index要加1 因为计算的过程start返回的下标不是以0开始的
rf.applyCh <- ApplyMsg{
Index: i &#43; 1, Command: rf.logs[i].Command}
}
rf.lastApplied &#61; rf.commitIndex
}

最后附上我们的start函数

func (rf *Raft) Start(command interface{
}) (int, int, bool) {
index :&#61; -1
term :&#61; -1
isLeader :&#61; false
rf.mu.Lock()
defer rf.mu.Unlock()
if rf.current_state !&#61; "LEADER"{
// 不是leader拒绝即可 测试代码用这个判断哪一个节点是leader
return index, term ,isLeader
}
nlog :&#61; LogEntry{
command, rf.currentTerm}
isLeader &#61; (rf.current_state &#61;&#61; "LEADER")
rf.logs &#61; append(rf.logs, nlog) // 提交一个命令其实就是向日志里面添加一项 在心跳包的时候同步
//fmt.Printf("leader append log [leader&#61;%d], [term&#61;%d], [command&#61;%v]\n",
//rf.me, rf.currentTerm, command)
index &#61; len(rf.logs)
term &#61; rf.currentTerm
return index, term, isLeader
}

这样&#xff0c;lab2 PartB就完成了&#xff0c;下面是过了测试的图示&#xff1a;

在这之前&#xff0c;经历了不少的失败&#xff0c;

总结

对于Lab2来说&#xff0c;当出现bug的时候可以说唯一的方法就是把想测试的样例以外的测试函数都注释&#xff0c;然后加日志&#xff0c;一点一点分析&#xff0c;完成以后还是很开心的&#xff0c;但是也有一点累&#xff0c;因为分布式的代码实在是太难排错了&#xff0c;比C&#43;&#43;还难。其实最后版本也可能还是有问题的&#xff0c;因为测试代码中是有随机性的&#xff0c;所以后面有心的话可以在修改下代码。现在也才知道为什么这门课开始选择C&#43;&#43;&#xff0c;后来改为Go了。快开学了&#xff0c;开学前不想再像这样一样费脑子了&#xff0c;已经两天没看海贼王&#xff0c;而且今天还是母亲节&#xff0c;明天就不碰电脑了&#xff0c;休息一下&#xff0c;后天再写PartC吧&#xff0c;好在看起来C并不难。附上全部的代码链接。

2020.8.1 &#xff1a;
更新了一个6.824每一课后面的问题解答&#xff0c;有很多确实很有意思的问题&#xff0c;有兴趣的朋友可以看看

参考&#xff1a;

  • [1] 《Raft动画演示》
  • [2] 《MIT-6.824-lab2-raft&#xff08;实验内容&#43;测试用例讲解&#xff09;》
  • [3] 《论文翻译》

推荐阅读
  • Nginx使用AWStats日志分析的步骤及注意事项
    本文介绍了在Centos7操作系统上使用Nginx和AWStats进行日志分析的步骤和注意事项。通过AWStats可以统计网站的访问量、IP地址、操作系统、浏览器等信息,并提供精确到每月、每日、每小时的数据。在部署AWStats之前需要确认服务器上已经安装了Perl环境,并进行DNS解析。 ... [详细]
  • Linux服务器密码过期策略、登录次数限制、私钥登录等配置方法
    本文介绍了在Linux服务器上进行密码过期策略、登录次数限制、私钥登录等配置的方法。通过修改配置文件中的参数,可以设置密码的有效期、最小间隔时间、最小长度,并在密码过期前进行提示。同时还介绍了如何进行公钥登录和修改默认账户用户名的操作。详细步骤和注意事项可参考本文内容。 ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • 本文介绍了高校天文共享平台的开发过程中的思考和规划。该平台旨在为高校学生提供天象预报、科普知识、观测活动、图片分享等功能。文章分析了项目的技术栈选择、网站前端布局、业务流程、数据库结构等方面,并总结了项目存在的问题,如前后端未分离、代码混乱等。作者表示希望通过记录和规划,能够理清思路,进一步完善该平台。 ... [详细]
  • 集成电路企业在进行跨隔离网数据交换时面临着安全性问题,传统的数据交换方式存在安全性堪忧、效率低下等问题。本文以《Ftrans跨网文件安全交换系统》为例,介绍了如何通过丰富的审批流程来满足企业的合规要求,保障数据交换的安全性。 ... [详细]
  • 本文介绍了H5游戏性能优化和调试技巧,包括从问题表象出发进行优化、排除外部问题导致的卡顿、帧率设定、减少drawcall的方法、UI优化和图集渲染等八个理念。对于游戏程序员来说,解决游戏性能问题是一个关键的任务,本文提供了一些有用的参考价值。摘要长度为183字。 ... [详细]
  • ejava,刘聪dejava
    本文目录一览:1、什么是Java?2、java ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 在重复造轮子的情况下用ProxyServlet反向代理来减少工作量
    像不少公司内部不同团队都会自己研发自己工具产品,当各个产品逐渐成熟,到达了一定的发展瓶颈,同时每个产品都有着自己的入口,用户 ... [详细]
  • 开发笔记:计网局域网:NAT 是如何工作的?
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了计网-局域网:NAT是如何工作的?相关的知识,希望对你有一定的参考价值。 ... [详细]
  • Linux如何安装Mongodb的详细步骤和注意事项
    本文介绍了Linux如何安装Mongodb的详细步骤和注意事项,同时介绍了Mongodb的特点和优势。Mongodb是一个开源的数据库,适用于各种规模的企业和各类应用程序。它具有灵活的数据模式和高性能的数据读写操作,能够提高企业的敏捷性和可扩展性。文章还提供了Mongodb的下载安装包地址。 ... [详细]
  • mac php错误日志配置方法及错误级别修改
    本文介绍了在mac环境下配置php错误日志的方法,包括修改php.ini文件和httpd.conf文件的操作步骤。同时还介绍了如何修改错误级别,以及相应的错误级别参考链接。 ... [详细]
  • 从Oracle安全移植到国产达梦数据库的DBA实践与攻略
    随着我国对信息安全和自主可控技术的重视,国产数据库在党政机关、军队和大型央企等行业中得到了快速应用。本文介绍了如何降低从Oracle到国产达梦数据库的技术门槛,保障用户现有业务系统投资。具体包括分析待移植系统、确定移植对象、数据迁移、PL/SQL移植、校验移植结果以及应用系统的测试和优化等步骤。同时提供了移植攻略,包括待移植系统分析和准备移植环境的方法。通过本文的实践与攻略,DBA可以更好地完成Oracle安全移植到国产达梦数据库的工作。 ... [详细]
  • 本文介绍了在Android设备上使用命令行来抓取log文件的方法,包括检查设备连接、清除log缓存、选择存放目录、运行程序等步骤,最后可以在桌面上生成log文件。 ... [详细]
author-avatar
-微城市-互粉-
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有