[etcd]etcd源码

1.etcd存储结构

 

etcd存储结构:

       内存:treeIndex,存储key对应的reversion信息.一个key可能对应多个reversion.(etcd以此实现的mvcc)

       磁盘:boltdb.这个是开源的kv存储引擎.此处存储reversion对应的value.

       因此一个读请求需要先从treeIndex查找到key对应的reversion,再从boltdb中查找value,写请求需要更新treeIndex,以及持久化到boltdb.

       1.boltdb中的数据通过mmap机制,映射到内存中.加速查询

       2.boltdb中的部分数据会缓存到内存中(上图中readTxBuffer).每个读事务开始时,第一步都会把缓存数据拷贝一份(concurrentReadTxBuffer),注意这里不是拷贝的引用,而是深拷贝.在qa测试时,可以看到并发查询,内存线性增长,直到撑爆内存.

        思考:eadTxBuffer中的数据什么时候插入的呢?是boltdb的拷贝吗?

              原以为在查询一个数据没有命中时,会把查到的数据缓存到readTxBuffer,但这样会导致可能最终所有数据都缓存了.之后考虑是lru.但是看etcd实现,只是一个map.只有在更改数据时,会回写readTxBuffer.

        也就是.一个数据只有在修改后,才会缓存到ReadTxBuffer.比如etcd重启了,那么缓存就不存在了

             

 

2.读操作原理

 

 

读操作大概流程:

         1.client通过grpc协议请求server.

         2.server首先确定自己机器的日志是否是最新的.(3.2的优化readIndex机制,之前每个读请求也需要请求到leader执行.3.2对读请求做了优化,当任意机器的committedId>=leader的committedId,就可以处理请求.因此分为两种情况

               2.1该机器是leader,那么需要确定自己是leader,怎么确定呢?给所有机器发送心跳,当得到大多数回应即可

               2.2该机器是follower,请求leader获取committedId.之后是2.1逻辑

              疑问:每个读请求都会请求leader获取committedId吗?

              不会, readIndex逻辑里一共有两个channel,一个协程.  其中两个channel分别为readWaitC(rc),notifierc(nc). 如图.前两个读,会插入rc.并且不断监听nc.etcd启动时会有一个协程不断监听rc这个channel,当监听到后,会去请求readIndex.将结果写入nc.此时read1,2就可以继续处理了.如果read3插入不了,就会直接拿nc,等待其他read事务的readIndex的返回.

      

 

 

        3.构建读事务,拷贝ReadTxBuffer,此处对ReadTxBuffer加读锁.

        4.从内存中的IndexTree查找key对应的reversion.(注意这里会加读锁)

        5.通过reversion查找value.

             5.1首先查询buffer,如果命中,直接返回,不命中,走5.2

             5.2从boltdb查找数据. 这里会查磁盘吗?no, mmap机制,把boltdb数据缓存到pageCache中,只有内存不够了,才会再去读磁盘.

由此可见:在读请求时,有这么几点会影响读效率

         a.readIndex需要请求leader或者如果leader处理,需要发送心跳.这里会阻塞

         b.构建buffer时,需要拷贝readTxBuffer,如果这里有写,会阻塞(因为会上写锁)

         c.查询indexBuffer时,会上读锁,如果这里有写,会阻塞(因为会上写锁)

         d.查询value时.readTxBuffer这个结构,之后更新数据才会更新这个buffer,不更新,就永远不命中.

       //  e.不命中buffer时,会查询boltdb,boltdb底层是b+树的存储.查询时,是通过mmap,把磁盘数据映射到内存.且通过cow机制.实现写时复制,这里不会有锁(还需要细看下.)

3.写逻辑

简单逻辑总结:

1.接受请求

2.将请求包装为一个任务(propc).放到channel中

3.node协程,会不断监听propc.当监听到数据,会调用reftNode方法,走raft协议

4.当可以commit的时候,会写入wal日志.之后会更新boltdb,更新内存indexTree.可能会重建snapshot

写写之间是串行的.

代码逻辑如下:

 

4.验证写阻塞读 

4.1 indexTree update

 

当commit时,会修改内存中的treeIndex.此时对treeIndex加写锁,而读操作,会加读锁.此时会有阻塞发生.

在put中加入断点或者sleep一段时间,可以观察到读操作超时

 

4.2 commit to boltdb

向boltdb 提交数据修改时,会申请ReadTx的写锁.

 

在构建一个读事务的时候,第一步,就是将ReadTx中的数据拷贝出来一份,赋值给concurrentReadBuffer,类似一个缓存.之后读数据都是从该buffer中获取.

当commit时候,如果有大量的读操作,那么就会频繁加读锁,导致写锁一直阻塞.同时,如果commit期间,因为写锁导致读事务无法开始.

同样.通过加sleep模拟或者打断点.可以复现.在commit的时候,会阻塞读.

 

 

 

4.3 读读不阻塞.

因为读直接都是加的读锁,因此不会阻塞

4.4 写写阻塞

串行处理

 

4.5 读事务未结束,会不会影响写?

阿里的commitor说会阻塞,但是我实际操作,不会阻塞.需要再细看下代码,因为如果读事务未结束,就影响写的话,没有必要.

 

TODO 目前生产是3.2.25.  其中3.2版本最大是3.2.31分支.需要对比下分支间的差别

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值