肝了一个月的ETCD,从Raft原理到实践(三)

简介: 高能预警,本文是我今年年初写了,当时花了一个月时间,所以内容会比较长,其中的Raft协议非常硬核,由于当时对文章排版不熟练,所以可读性不高,现在对文章重新整理,提炼了比较核心的内容。

ETCD集群部署


ETCD安装,请参考文章

《ETCD系列教程2 - ETCD体验》 吕梦楼,公众号:楼仔进阶之路ETCD教程-3.ETCD体验

下面我们可以部署一个etcd集群,我把代码还是写到文件中,第一个脚本为不支持在 Docs 外粘贴 block,内容如下(启动etcd需要很多参数,这些参数我都已经注释说明,更多参数详见:https://www.cnblogs.com/linuxws/p/11194403.html):

TOKEN=token-01
CLUSTER_STATE=new
NAME_1=etcd-01
NAME_2=etcd-02
NAME_3=etcd-03
HOST_1=127.0.0.1
HOST_2=127.0.0.1
HOST_3=127.0.0.1
PORT_API_1=2379
PORT_PEER_1=2380
PORT_API_2=2479
PORT_PEER_2=2480
PORT_API_3=2579
PORT_PEER_3=2580
CLUSTER=${NAME_1}=http://${HOST_1}:${PORT_PEER_1},${NAME_2}=http://${HOST_2}:${PORT_PEER_2},${NAME_3}=http://${HOST_3}:${PORT_PEER_3}
# For every machine
THIS_NAME=${NAME_1}
THIS_IP=${HOST_1}
THIS_PORT_API=${PORT_API_1}
THIS_PORT_PEER=${PORT_PEER_1}
# 用于杀死进程
lsof -i:2379 | awk '{print $2}' | grep -v "PID" | uniq | xargs kill -9
# --enable-v2 支持v2接口,可以省略
# --data-dir 数据存储目录,可以省略
# --name 节点名称,必须
# --initial-advertise-peer-urls  数据在集群内进行交互的url,必须
# --listen-peer-urls  集群节点之间通信监听的url,必须
# --advertise-client-urls 客户通过该地址与本member交互信息,可以省略
# --listen-client-urls 监听客户端请求的url,必须
# --initial-cluster 初始启动的集群配置,必须
# --initial-cluster-state 初始化集群状态,取值为new和existing,可以省略
# --initial-cluster-token 集群初始化token,可以省略
etcd --enable-v2=true --data-dir=data.${THIS_NAME} --name ${THIS_NAME} \
        --initial-advertise-peer-urls http://${THIS_IP}:${THIS_PORT_PEER} --listen-peer-urls http://${THIS_IP}:${THIS_PORT_PEER} \
        --advertise-client-urls http://${THIS_IP}:${THIS_PORT_API} --listen-client-urls http://${THIS_IP}:${THIS_PORT_API} \
        --initial-cluster ${CLUSTER} \
        --initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}

第二个脚本需要把里面的内容替换如下:

# For every machine
THIS_NAME=${NAME_2}
THIS_IP=${HOST_2}
THIS_PORT_API=${PORT_API_2}
THIS_PORT_PEER=${PORT_PEER_2}
# 用于杀死进程
lsof -i:2479 | awk '{print $2}' | grep -v "PID" | uniq | xargs kill -9

第三个脚本需要把里面的内容替换如下:

# For every machine
THIS_NAME=${NAME_3}
THIS_IP=${HOST_3}
THIS_PORT_API=${PORT_API_3}
THIS_PORT_PEER=${PORT_PEER_3}
# 用于杀死进程
lsof -i:2579 | awk '{print $2}' | grep -v "PID" | uniq | xargs kill -9

有了这3个脚本,分别开3个窗口,分别执行,服务启动截图如下:

image.gifZMR7GD`(LWNFVL_%610S})B.png

当这3个脚本全部启动后,集群部署完毕,我们检查一下3个节点的健康状态:

curl http://127.0.0.1:2379/health
curl http://127.0.0.1:2479/health
curl http://127.0.0.1:2579/health

如果都返回{"health":"true"},表示部署成功,下面我们查看一下部署的节点信息:

curl http://127.0.0.1:2379/v2/members

返回结果如下,其中peerURLs是节点互相通信访问的url,clientURLs是对外访问的url:

{
    "members":[
        {
            "id":"264ae6bc59e99892",
            "name":"etcd-01",
            "peerURLs":[
                "http://127.0.0.1:2380"
            ],
            "clientURLs":[
                "http://127.0.0.1:2379"
            ]
        },
        {
            "id":"dbafe5ad6b652eda",
            "name":"etcd-02",
            "peerURLs":[
                "http://127.0.0.1:2480"
            ],
            "clientURLs":[
                "http://127.0.0.1:2479"
            ]
        },
        {
            "id":"f570ae41f524bdcb",
            "name":"etcd-03",
            "peerURLs":[
                "http://127.0.0.1:2580"
            ],
            "clientURLs":[
                "http://127.0.0.1:2579"
            ]
        }
    ]
}


ETCD使用


集群管理

我们在部署集群时,用到一些方法,这里我简单汇总一下:

// 版本检查,输出{"etcdserver":"3.4.14","etcdcluster":"3.4.0"}
curl http://127.0.0.1:2379/version
// 健康检查,输出{"health":"true"}
curl http://127.0.0.1:2379/health
// 查看集群节点
curl http://127.0.0.1:2379/v2/members


键值操作

设置键的值:

curl http://127.0.0.1:2379/v2/keys/message -XPUT -d value="hello world"

返回结果:

{
    "action":"set",
    "node":{
        "key":"/message",
        "value":"hello world",
        "modifiedIndex":43,
        "createdIndex":43
    }
}

读取键的值:

curl http://127.0.0.1:2379/v2/keys/message

返回结果:

{
    "action":"get",
    "node":{
        "key":"/message",
        "value":"hello world",
        "modifiedIndex":43,
        "createdIndex":43
    }
}

给键设置10s的超时时间:

curl http://127.0.0.1:2379/v2/keys/message -XPUT -d value="hello world" -d ttl=10

返回结果(prevNode是旧值):

{
    "action":"set",
    "node":{
        "key":"/message",
        "value":"hello world",
        "expiration":"2021-01-21T00:16:13.777434Z",
        "ttl":10,
        "modifiedIndex":44,
        "createdIndex":44
    },
    "prevNode":{
        "key":"/message",
        "value":"hello world",
        "modifiedIndex":43,
        "createdIndex":43
    }
}

获取该键值,超时后,就提示“key not found”:

M$A8ZS$~EWDC6)715{SEP)R.png


watch通知

可以对key设置监听,当key的值有变化时,会通知监听的客户端,我们先在客户端A监听key:

curl http://127.0.0.1:2379/v2/keys/message?wait=true

然后在客户端B,修改该key的值:

curl http://127.0.0.1:2379/v2/keys/message -XPUT -d value="hello world2"

客户端A返回并退出,返回结果:

{
    "action":"set",
    "node":{
        "key":"/message",
        "value":"hello world2",
        "modifiedIndex":48,
        "createdIndex":48
    }
}

如果希望客户端A能持续监听,不退出,可以通过增加stream=true参数:

curl "http://127.0.0.1:2379/v2/keys/message?wait=true&stream=true"

当在客户端B执行如下时:

curl http://127.0.0.1:2379/v2/keys/message -XPUT -d value="hello world" -d ttl=10

客户端A会实时监听返回,比如当给key设置值,或者当key过期时,客户端A都会监听到:

image.gifMHFY$SA6W(TU91T4N9%V_K7.png

ETCD安装,请参考文章

《ETCD系列教程2 - ETCD体验》 吕梦楼,公众号:楼仔进阶之路ETCD教程-3.ETCD体验


初探ETCD 3.0


该部分作为扩展知识,仅供了解,更多内容请参考文章

《ETCD系列教程3 - 深入ETCD》 吕梦楼,公众号:楼仔进阶之路ETCD教程-4.深入ETCD


版本说明

目前etcd主要经历了3个大的版本,分别为etcd 0.4版本、etcd 2.0版本和etcd 3.0版本。对于etcd 2.0版本,已经可以很好满足etcd的初步需求,主要包括:

  • 专注于key-value存储,而不是一个完整的数据库;
  • 通过HTTP + JSON的方式暴露给外部API;
  • watch机制提供持续监听某个key变化的功能,以及基于TTL的key的自动过期机制。

但是在实际过程中,我们也发现了一些问题,比如客户端需要频繁地与服务端进行通信,集群在空间和时间上都需要承受较大的压力,以及垃圾回收key的时间不稳定等,同时“微服务”架构要求etcd能够单集群支撑更大规模的并发,因此诞生了etcd 3.0版本,主要对HTTP + JSON的通信方式、key的自动过期机制、watch机制、数据持久化等进行了优化,下面我们看看etcd 3.0版本对每个模块都做了哪些优化。


客户端通信方式

gRPC是Google开源的 个高性能、跨语言的RPC框架,基于HTTP/2协议实现。它使用protobuf作为序列化和反序列化协议,即基于 protobuf 来声明数据模型和RPC接口服务。protobuf的效率远高于JSON,尽管etcd v2的客户端已经对JSON的序列号和反序列化进行了大量的优化,但是etcd v3的gRPC序列号和反序列化的速度依旧是etcd v2的两倍多。

etcdv3的客户端使用gRPC与server进行通信,通信的消息协议使用protobuf进行约定,代替了v2版本的HTTP+JSON格式,使用二进制替代文本,更加节省空间。同时gRPC使用的是HTTP/2协议,同一个连接可以同时处理多个请求,不必像HTTP1.1协议中,多个请求需要建立多个连接。同时,HTTP/2会对请求的Header和请求数据进行压缩编码,常见的有Header帧,用于传输Header内容,另外就是Data帧,来传输正文实体。客户端可以将多个请求放到不同的流中,然后将这些流拆分成帧的形式进行二进制传输,传输的帧也会有一个编号,因此在一个连接中客户端可以发送多个请求,减少了连接数,降低了对服务器的压力,二进制的数据传输格式也会是传输速度更快。

总结一下,其实这里主要进行2点优化:

  • 二进制取代字符串:通过gRPC进行通信,代替了v2版本的HTTP+JSON格式;
  • 减少TCP连接:使用HTTP/2协议,同一个连接可以同时处理多个请求,摒弃多个请求需要建立多个连接的方式。


键的过期机制

etcdv2中的键的实效是使用TTL机制来实现的,每个有存活时间的键,客户端必须定期的进行刷新重新设置保证它不被自动删除,每次刷新同时还会重新建立连接去更新键。也就是说,及时整个集群都处于空闲状态,也会有很多客户端与服务器进行定期通信,以保证某个key不被自动删除。

etcdv3版本中采用了租约机制进行实现,每个租约会有一个TTL,然后将一些key附加到租约上,当租约到期后,附加到它上边的key都会被删除。利用键的过期机制可以实现服务注册功能,我们可以将一个服务的域名、IP等信息注册到etcd中,并给相应的键设置租约,并在TTL时间内定期维持一个心跳进行刷新。当服务故障后,心跳消失从而相应的键就会自动删除,从而实现了服务的注册功能和服务的健康检查功能。

总结一下,就是v2版本比较傻瓜,需要时刻维护每个key的通信,v3就比较智能,整个统一的过期key的代号,我们把代号称之为“租约”,我们只需要维护这个代号即可,避免客户端去维护所有的key。


watch机制

etcdv2中的键被废除以后,为了能够跟踪key的变化,使用了事件机制进行跟踪,维护键的状态,来防止被删除掉的后键还能恢复和watch到,但是有一个滑动窗口的大小限制,那么如果要获取1000个时间之前的键就获取不到了。因此etcdv2中通过watch来同步数据不是那么可靠,断开连接一段时间后就会导致有可能中间的键的改动获取不到了。在etcdv3中支持get和watch键的任意的历史版本记录。

另外,v2中的watch本质上还是建立很多HTTP连接,每一个watch建立一个tcp套接字连接,当watch的客户端过多的时候会大大消耗服务器的资源,如果有数千个客户端watch数千个key,那么etcd v2的服务端的socket和内存资源会很快被耗尽。v3版本中的watch可以进行连接复用,多个客户端可以共用相同的TCP连接,大大减轻了服务器的压力。

总结一下,其实这里主要进行2点优化:

  • 实时监听key的更新:解决v2中途key的数据更新,客服端不会感知的问题;
  • 多路复用:这个可以想到select和epool模型,就是一个客户之前需要建立多个TCP连接,现在只需要建立一个即可。


数据存储模型

etcd是一个key-value数据库,ectd v2只保存了key的最新的value,之前的value会被直接覆盖,如果需要知道一个key的历史记录,需要对该key维护一个历史变更的窗口,默认保存最新的1000个变更,但是当数据更新较快时,这1000个变更其实“不够用”,因为数据会被快速覆盖,之前的记录还是找不到。为了解决这个问题,etcd v3摒弃了v2不稳定的“滑动窗口”式设计,引入MVCC机制,采用从历史记录为主索引的存储结构,保存了key的所有历史记录变更,并支持数据在无锁状态下的的快速查询。etcd是一个key-value数据库,etcdv2的key是一个递归的文件目录结构,在v3版本中的键改成了扁平化的数据结构,更加简洁,并通过线段树的优化方式,支持key的快速查询。

由于etcd v3实现了MVCC,保存了每个key-value pair的历史版本,数据大了很多,不能将整个数据库都存放到内存中。因此etcd v3摒弃了内存数据库,转为磁盘数据库,即整个数据都存储在磁盘上,底层的存储引擎使用的是BoltDB。

总结一下,其实这里主要进行3点优化:

  • 保存历史数据:摈弃v2的“滑动窗口”式设计,通过MVCC机制,保存了所有的历史数据;
  • 数据落磁盘:因为要保存历史数据,数据量态度,不适合全内存存储,使用BoltDB存储;
  • 查询优化:摒弃v2的目录式层级化设计,使用线段树优化查询。
相关文章
|
5月前
|
存储 缓存 NoSQL
蚂蚁金服P7私藏的Redis原理与实践内部笔记
Redis 是完全开源免费的,是一个高性能的key-value类型的内存数据库。整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。
96 1
|
2月前
|
负载均衡 NoSQL Java
因为不懂Redis集群,我被diss了
Redis官方在官网里写着霸气的宣传语:从构建者那里获取世界上最快的内存数据库。相信国内没用Redis的科技公司也屈指可数。现在Redis已经走向了商业化,它所属的公司叫Redis Ltd。不过可惜的是Redis创始人在2020年就离开了Redis Labs,那个留着乱糟糟黑色头发的中年男人就是Redis的创始人。Redis的商业推广仍在继续着,大家要去现场看看嘛。大家好,我是南哥。一个Java学习与进阶的领路人,相信对你通关面试、拿下Offer进入心心念念的公司有所帮助。
135 4
因为不懂Redis集群,我被diss了
|
5月前
|
NoSQL Redis
【怒怼大厂面试官】听说你精通Redis?Redis数据同步懂吗
面试官:不用慌尽管说,错了也没关系。。。来说说Redis数据同步。是这样的,Redis有一个叫命令传播的概念,如果像面试官说的这种场景,再使用上面我提到的AOF缓冲区就有点浪费内存空间了。所以Redis会将主服务器的这条Del删除命令
【怒怼大厂面试官】听说你精通Redis?Redis数据同步懂吗
|
5月前
|
存储 算法 开发工具
学习分享|Etcd/Raft 原理篇
本文是根据近期对 Etcd-Raft 的学习把自己的理解做个简单整理和分享。
|
负载均衡 监控 算法
【阿里二面面试题】说说你对 Raft 算法的理解?
【阿里二面面试题】说说你对 Raft 算法的理解?
772 0
【阿里二面面试题】说说你对 Raft 算法的理解?
|
缓存 NoSQL Dubbo
京东三面惨遭被虐,关于redis,高并发,分布式,微服务一窍不通
三面大概九十分钟,问的东西很全面,需要做充足准备,就是除了概念以外问的有点懵逼了(呜呜呜~)。回来之后把这些题目做了一个分类并整理出答案(强迫症的我~狂补知识~)分为redis缓存,高并发,分布式,微服务等,接下来分享一下我的这京东面试的面经+一些我的学习笔记。
|
存储 缓存 负载均衡
Redis持久化(少年一贯快马扬帆,道阻且长不转弯)(一)
Redis持久化(少年一贯快马扬帆,道阻且长不转弯)(一)
107 0
Redis持久化(少年一贯快马扬帆,道阻且长不转弯)(一)
|
存储 缓存 NoSQL
Redis持久化(少年一贯快马扬帆,道阻且长不转弯)(二)
Redis持久化(少年一贯快马扬帆,道阻且长不转弯)(二)
105 0
Redis持久化(少年一贯快马扬帆,道阻且长不转弯)(二)
|
消息中间件 分布式计算 资源调度
|
存储 监控 算法
肝了一个月的ETCD,从Raft原理到实践(一)
高能预警,本文是我今年年初写了,当时花了一个月时间,所以内容会比较长,其中的Raft协议非常硬核,由于当时对文章排版不熟练,所以可读性不高,现在对文章重新整理,提炼了比较核心的内容。
307 0
肝了一个月的ETCD,从Raft原理到实践(一)