<!--StartFragment-->
**作者:邓楠 & 张旭**
<!--EndFragment-->
<!--StartFragment-->
上篇文章([**点击查看**](http://mp.weixin.qq.com/s?\\__biz=Mzg2NjU2ODUwMA==\\&mid=2247486064\\&idx=1\\&sn=74e290df9a84cfcb1a9826cf910868a4\\&chksm=ce499a2bf93e133dc18b32bd9bbeb166df9c7db84cd0936599637043e327803c1c6a7bc5f14d\\&scene=21#wechat_redirect))详细地介绍了MatrixCube的功能与架构,MatrixCube是MatrixOne数据库获得分布式能力的重要组件。
今天我们将通过一个简单的分布式存储demo实验来完整地体验下MatrixCube的功能。
此demo项目叫做MatrixKV,Github仓库地址如下:
*https\://github.com/matrixorigin/matrixkv*\
****
**MatrixKV是一个简单的分布式强一致KV存储系统,采用Pebble作为底层的存储引擎,MatrixCube作为分布式组件,以及自定义了最简单的读写请求接口。用户可以非常简单的在任意一个节点发起读写数据的请求,也可以从任意一个节点读到需要的数据。**
如果对TiDB架构比较熟悉的同学可以把MatrixKV等同于一个TiKV+PD,而MatrixKV其中使用的RocksDB换成了Pebble。
**本次实验以Docker模拟一个小型MatrixKV集群的形式,来进一步说明MatrixCube的功能与运作机制。**
## **Step 1**
### **环境准备**
<!--StartFragment-->
###### 工具准备
<!--EndFragment-->
我们这个实验需要用到docker与docker-compose工具,因此需要安装好docker与docker-compose。一般来说可以直接安装Docker-desktop,里面自带了docker引擎,CLI工具及Compose插件。
官方提供了各种操作系统的完整安装包:https\://www\.docker.com/products/docker-desktop/
<!--EndFragment-->
<!--StartFragment-->
```
docker -vdocker-compose -v
```
<!--EndFragment-->
<!--StartFragment-->
######
###### ** Clone代码 **
将MatrixKV代码Clone到本地。
<!--EndFragment-->
<!--StartFragment-->
```
git clone https\\://github.com/matrixorigin/matrixkv
```
<!--EndFragment-->
## **Step 2**
### **MatrixKV集群配置**
在上一篇文章中,我们提到过MatrixCube基于Raft构建分布式共识协议,因此需要至少三个节点来作为最小部署规模,而最初的三个节点都属于调度用的Prophet节点。我们这次实验准备的这个小型集群有四个节点,其中三个为Prophet节点, 一个为数据节点。我们以docker进行容器包装的形式来在单机上进行模拟。
### ** Prophet节点设置 **
我们可以看到在/cfg文件夹中有node0-node3的配置文件,其中Node0-Node2均为Prophet节点,Node3为数据节点。Prophet节点的配置以Node0举例如下如下:
<!--StartFragment-->
```js
#raft-group的RPC通信地址,节点之间通过这个地址来发送raft message和snapshot。
addr-raft = "node0:8081"
#对客户端开放的地址,客户通过这个端口和cube来交互自定义的业务请求。
addr-client = "node0:8082"
#Cube的数据存放目录,每个节点会根据这个目录所在的磁盘统计存储的使用情况,上报给调度节点。
dir-data = "/tmp/matrixkv"
[raft]
#Cube会对Raft的写请求做batch,会把多个写请求合并到一个Raft-Log,只做一次Raft的Proposal,这个配置指定一个Proposal的大小,这个#配置取决于应用的具体情况
max-entry-bytes = "200MB"
[replication]
# 1. 一个raft-group的副本最大的down time,当一个副本的down time超过这个值,调度节点就会认为这个副本用久的故障了
# 然后会在集群中选择一个合适的节点来重新创建这个副本。如果后面这个节点又重新启动了,那么调度节点会通知这个副本
# 销毁自己。
# 2. 这里的默认设置一般是30分钟,这个时间我们认为是设备一般出现故障可以在30分钟内完成故障处理恢复,如果超过这个时间说明已经无法 #恢复。在这里我们为了做实验的方便,设置成15秒。
max-peer-down-time = "15s"
[prophet]
#该Prophet调度节点的名称
name = "node0"
#该Prophet调度节点对外的RPC地址
rpc-addr = "node0:8083"#指定该节点为Prophet节点prophet-node = true
[prophet.schedule]
# Cube集群中的所有节点都会定期发送心跳到调度的Leader节点,当一个节点超过一定的时间都没有发送心跳,
# 那么调度节点会把这个节点的状态修改为Down,并且会把这个节点上,所有的Shard在集群其他节点来重建,
# 当这个节点恢复后,这个节点上的所有Shard都会收到销毁的调度消息。
# 这里也是为了实验方便设置成10秒,默认也是30分钟。max-container-down-time = "10s"
#Prophet中内嵌一个ETCD作为存储元数据的组件
[prophet.embed-etcd]
#Cube的Prophet调度节点会先后启动, 假设我们有node0, node1, node2三个调度节点, 第一个启动的是node0节点, 那么node0节点就会
#组成一个只有一个副本的etcd, 对于node0而言, `join`参数不需要填写, 后面的node1, node1启动的时候, `join`设置为node1
#的Etcd的Peer addressjoin = ""
#内嵌Etcd的client addressclient-urls = "http://0.0.0.0:8084"
#内嵌Etcd的advertise client address, 不填写, 默认和`client-urls`一致advertise-client-urls = "http://node0:8084"
#内嵌Etcd的peer address
peer-urls = "http://0.0.0.0:8085"
#内嵌Etcd的advertise peer address, 不填写, 默认和`peer-urls`一致
advertise-peer-urls = "http://node0:8085"
[prophet.replication]
#每个Shard最多多少个副本,当Cube的调度节点周期性巡检的时候,发现Shard的副本个数和这个值不匹配的时候,会执行创建副本或者删除副本的调#度操作。
max-replicas = 3
```
<!--EndFragment-->
Node1与Node2的配置除了需要在ETCD配置部分中join前面的节点,其他的几乎与Node0没有差别。
<!--EndFragment-->
<!--StartFragment-->
### **数据节点设置 **
<!--EndFragment-->
<!--StartFragment-->
而Node3作为数据节点,则配置相对比较简单,除了prophet-node设置成false以外,其他没有需要额外配置的部分。
<!--EndFragment-->
<!--StartFragment-->
```
addr-raft = "node3:8081"
addr-client = "node3:8082"
dir-data = "/tmp/matrixkv"
[raft]
max-entry-bytes = "200MB"
[prophet]
name = "node3
"rpc-addr = "node3:8083"
prophet-node = false
external-etcd = [
"http://node0:8084",
"http://node1:8084",
"http://node2:8084",
]
```
<!--EndFragment-->
### **Docker-Compose设置 **
<!--EndFragment-->
<!--StartFragment-->
Docker-compose将根据docker-compose.yml中的配置来进行容器启动,其中我们需要将每个节点的数据目录改成自己指定的目录。我们以Node0为例。
<!--EndFragment-->

<!--StartFragment-->
## **Step 3**
###
### **集群启动**
###
<!--EndFragment-->
<!--StartFragment-->
配置好这些选项后,在MatrixKV代码库中,我们已经写好了构建镜像的dockerfile及启动构建流程的Makefile。
我们直接在MatrixKV的路径下运行make docker命令,它会将MatrixKV整体打包成镜像。
<!--EndFragment-->
<!--StartFragment-->
```
#如果是MAC X86架构平台或者Linux,
可以直接运行以下命令(make docker)
#如果是MAC的ARM版本,则需要将Makefile中的docker build -t matrixkv -f Dockerfile .改成docker buildx build --platform linux/amd64 -t matrixkv -f Dockerfile .
make docker
```
<!--EndFragment-->
另外注意国内用户如果可能碰到go源站速度极慢无法下载依赖库的情况,可以在Dockerfile中增加go的中国源站设置:
<!--EndFragment-->
<!--StartFragment-->
```
RUN go env -w GOPROXY=https\\://goproxy.cn,direct
```
<!--EndFragment-->
然后通过docker-compose up命令将MatrixKV的镜像分别根据不同的节点配置启动四份,从而形成我们的Node0到Node3的四节点集群。
<!--EndFragment-->

<!--StartFragment-->
在docker desktop中我们应该就可以看到我们的4个MatrixKV的节点都以镜像的形式启动了。

<!--StartFragment-->
在看到如下日志中出现各个节点启动监听8080端口的时候,就代表集群已经启动完成。
<!--EndFragment-->

<!--StartFragment-->
同时可以看到在我们指定的数据目录中已经开始生成了很多存储数据的文件夹以及一些初始文件。
<!--EndFragment-->

<!--StartFragment-->
关闭集群的话可以在启动的命令行中停止进程即可,或者也可以在Docker desktop中以图形化界面方式停止任意节点。
<!--EndFragment-->
<!--StartFragment-->
## **Step 4**
### **读写请求接口与路由**
<!--EndFragment-->
<!--StartFragment-->
在启动好集群之后,我们就可以对集群进行读写数据的请求。MatrixKV包装了几个非常简单的数据读写接口:
* 数据写入SET
<!--EndFragment-->
<!--StartFragment-->
```
curl -X POST -H 'Content-Type: application/json' -d '{"key":"k1","value":"v1"}' http\\://127.0.0.1:8080/set
```
<!--EndFragment-->
<!--StartFragment-->
* 数据读取GET
<!--EndFragment-->
<!--StartFragment-->
```
\\
curl http\\://127.0.0.1:8080/get?key=k1
```
<!--EndFragment-->
<!--StartFragment-->
* 数据删除DELETE
<!--EndFragment-->
<!--StartFragment-->
```
curl -X POST -H 'Content-Type: application/json' -d '{"key":"k1"}' http\\://127.0.0.1:8080/delete
```
<!--EndFragment-->
<!--StartFragment-->
上一篇文章中介绍了MatrixCube中的Shard Proxy,这个组件可以使得我们可以从集群的任意一个节点发起请求,不管是写入,读取还是删除的请求,Shard Proxy都会自动将请求路由到相应的处理节点上。
比如我们可以在node0上写入数据,而在node0到node3上都可以进行读取,是完全一样的。
<!--EndFragment-->
<!--StartFragment-->
```
//向node0发起写入请求curl -X POST -H 'Content-Type: application/json' -d '{"key":"k1","value":"v1"}' http://127.0.0.1:8080/set//从node0-node3进行读取curl http://127.0.0.1:8080/get?key=k1curl http://127.0.0.1:8081/get?key=k1curl http://127.0.0.1:8082/get?key=k1curl http://127.0.0.1:8083/get?key=k1
```
<!--EndFragment-->

<!--StartFragment-->
这里如果实验的系统配置及写入读取数据规模更大一些的话,大家也可以验证一些更极端的场景,比如有多个客户端在快速的读取各个节点的数据,而每次写入的数据在客户端读到的时候都可以保证是最新的以及一致的,通过这种方式可以验证MatrixCube的强一致性,保证任何时刻从任何节点读到的数据都是最新的以及一致的。
<!--EndFragment-->
<!--StartFragment-->
## **Step 5**
### **数据分片查询与分裂**
MatrixCube会在写入的数据量达到一定级别的时候产生Shard分裂,在MatrixKV中,我们将Shard的大小设置成了1024Byte。因此写入数据超过这个尺寸的数据会产生分裂。MatrixKV提供了一个简单的查询当前集群或者当前节点中有多少个Shard的接口。
<!--EndFragment-->
<!--StartFragment-->
```
#当前集群中的Shard情况curl http://127.0.0.1:8080/shards#当前节点中的Shard情况curl http://127.0.0.1:8080/shards?local=true
```
<!--EndFragment-->
<!--StartFragment-->
我们启动集群后可以看到初始状态下集群只有3个Shard,id分别为4, 6, 8, 而他们实际存储的节点在node0,node2与node3中。
<!--EndFragment-->

<!--StartFragment-->
而在我们通过以下命令写入一个超过1024Byte的数据之后,我们可以看到node0,node2与node3中的Shard全部进行了分裂,每个原来的Shard都形成了两个新的Shard,初始状态下的3个Shard变成了11,12,13,15,16,17六个Shard。
<!--EndFragment-->
<!--StartFragment-->
```
docker -vdocker-compose -v#test.json是测试数据,数据内容需要严格按照Key,Value格式规范,比如{"key":"item0","value":"XXXXXXXXXXX"}curl -X POST -H 'Content-Type: application/json' -d@test.json http://127.0.0.1:8083/set
```
<!--EndFragment-->

<!--StartFragment-->
同时我们仍然可以在任意一个节点访问到我们写入的数据。
<!--EndFragment-->
<!--StartFragment-->
## **Step 6**
### **节点变化与副本生成**
接下来我们再来看下MatrixCube的高可用保障的功能。我们可以通过Docker desktop来手动关停单个容器,以此来模拟真实环境中的机器故障情况。
在第五步中我们输入一个较大数据之后系统整体存在6个Shard,每个Shard有3个Replica。我们现在将node3手动关掉。
<!--EndFragment-->
**Step 6**
**节点变化与副本生成**\
接下来我们再来看下MatrixCube的高可用保障的功能。我们可以通过Docker desktop来手动关停单个容器,以此来模拟真实环境中的机器故障情况。
在第五步中我们输入一个较大数据之后系统整体存在6个Shard,每个Shard有3个Replica。我们现在将node3手动关掉。
<!--StartFragment-->
尝试再访问node3的命令均以失败告终。
<!--EndFragment-->

<!--StartFragment-->
但是从其他节点发起读请求,数据仍然都可以读取,这也就是分布式系统对整体高可用性的体现。
<!--EndFragment-->

<!--StartFragment-->
按照前面我们的设置,store3的心跳10秒内没有发到Prophet,Prophet会认为这个Store已经下线,而通过查看目前副本情况发现,所有的Shard都只有两个Replica,为了满足3副本的要求,Prophet会开始自动去寻找空闲节点,将Shard复制到上面,在我们这里也就是node1,那么我们再来看下每个节点Shard的情况。
可以看到node1中以前是没有Shard的,现在也与node0和node2一样都有6个shard。这也就是Prophet自动的副本生成功能,始终保证系统中有三份副本来保证高可用性。
<!--EndFragment-->

<!--StartFragment-->
**Final**
**MatrixKV代码扫描**
通过整个实验我们已经完整体验了在MatrixCube帮助下将一个单机的KV存储引擎Pebble变成了一个分布式的KV存储。而其中需要MatrixKV本身实现的代码是非常简单的。总的来说就只有4个go文件,不到300行代码就可以完成MatrixKV的全部搭建。
1\. /cmd/matrixkv.go: 整体程序启动的入口,进行最基本的初始化并启动服务。
2\. /pkg/config/config.go: 定义了一个MatrixKV整体配置的数据结构。
3\. /pkg/metadata/metadata.go:定义了用户与MatrixKV读写交互请求的数据结构。
4\. /pkg/server/server.go:这是MatrixKV的最主体功能,其中主要做了三件事:
* 定义MatrixKV server的数据结构。
* 定义Set/Get/Delete等相关请求的Executor具体实现。
* 调用Pebble库作为单机存储引擎,实现MatrixCube指定的DataStorage接口,将MatrixCube的Config项设置到相应方法上。
<!--EndFragment-->