rqlite 使用及注意事项
介绍
rqlite 是一款轻量级且分布式的关系型数据库, 实际上它是在 SQLite 的基础上实现的关系型数据库, 加上 raft 协议实现了一致性分布式. 另外它还提供了 http 相关的接口. 不同于 etcd 和 consul 这两款键值数据库, rqlite 是真正的关系型数据库, 也支持事务等. 从这方面看, 在对数据有分布式一致性且支持事务功能的需求中, rqlite 非常适合, 而且很轻量, 方便部署, 不像 MySQL, PostgrelSQL 等显得有些笨重. 当然和传统数据库比起来 rqlite 也有自身的缺点. 下面则着重介绍 rqlite 的使用及注意事项.
环境:
ip | os | hostname |
---|---|---|
10.0.21.5 | centos 6.5 | cz-test1 |
10.0.21.7 | centos 6.5 | cz-test2 |
10.0.21.17 | centos 6.5 | cz-test3 |
启动:
cz-test1 主机:
rqlited -http-addr "10.0.21.5:4001" -raft-addr "10.0.21.5:4002" /web/rqlite/
cz-test2 主机:
rqlited -http-addr "10.0.21.7:4001" -raft-addr "10.0.21.7:4002" -join "http://10.0.21.5:4001" /web/rqlite/
cz-test3 主机:
rqlited -http-addr "10.0.21.17:4001" -raft-addr "10.0.21.17:4002" -join "http://10.0.21.5:4001" /web/rqlite/
数据更新及查询的实现问题
DATA_API 中规定了读写数据需要遵照以下原则:
All write-requests must be sent to the leader of the cluster. Queries, however, may be sent to any node,
depending on the read-consistency requirements. But, by default, queries must also be sent to the leader.
实际上 raft 协议本身就要求了必须通过 leader 节点才能写入.
Read Consistency 对读取数据提供了三种级别: None, Weak 和 Strong, 如果对一致性要求不高可以考虑使用 None 级别, 默认情况为 Weak 级别, rqlite 会检查当前节点是否为 leader, 不是则将请求转发到 leader 中; Strong 则比较苛刻, 通过 raft 协议获取一致性数据, 该级别至少需要超过半数的节点确认才会返回数据, 如果网络延迟较大, strong 级别是性能最差的.
另外通过阅读源文件 cmd/rqlite/main.go 的 sendRequest
函数可知, client 端发送的所有更新和查询的 sql 语句都会先进行重定向判断处理, 如果当前节点为 follower 节点, 则将请求转发到集群中的 leader 节点处理, 如下所示:
// Check for redirect.
if resp.StatusCode == http.StatusMovedPermanently {
nRedirect++
if nRedirect > maxRedirect {
return fmt.Errorf("maximum leader redirect limit exceeded")
}
url = resp.Header["Location"][0]
continue
}
所以对于下文的几部分, 由于实现的原因, 在操作的时候会有一些单独说明, 不过具体的都依据下面的规则(当前 1.4.0 版本, 最新的版本可能会有所不同):
curl 直连操作
1. 如果是更新数据, curl 必须连接集群中的 leader 节点;
2. 如果是读取数据, curl 如果连接的是 follower, 可以增加 -L 参数获取重定向后的数据信息, 或指定 None 级别直连本地;
语言驱动
如果使用的编程语言驱动包做了重定向处理, 则可以连接任何节点操作数据, 如果没有处理重定向则必须指定 leader 节点操作数据;
其它操作
下文的备份
和回复
两部分, 由于代码没有做重定向处理, 如果连接的节点不是 leader, 则返回错误(可通过 curl -I 查看 http 返回码), 所以必须连接 leader 节点才能操作成功.
使用:
使用 http 接口操作, 可以在进群中的任意节点进行读写操作, 节点之间的一致性通过 raft 协议实现. 更多操作见 DATA_API, 各语言 client 端见 rqlite-client.
更新数据的时候, curl 连接的节点必须为集群中的 leader, rqlite
命令行工具之所以能多点更新, 是因为命令行自己做了重定向的处理, 所有的读写查询都会转发到 leader 节点.
curl -XPOST 'http://10.0.21.17:4001/db/execute?pretty&timings' -H "Content-Type: application/json" -d '["INSERT INTO foo VALUES(5, \"cztest7\")"]'
{
"results": [
{
"last_insert_id": 5,
"rows_affected": 1,
"time": 0.000191104
}
],
"time": 0.004461812
}
查询数据, 如果 curl 连接的节点不是 leader, 则可以增加 -L 参数获取重定向之后的内容:
# curl -L -G '10.0.21.17:4001/db/query?pretty&timing' --data-urlencode 'q=SELECT * FROM foo'
{
"results": [
{
"columns": [
"id",
"name"
],
"types": [
"integer",
"text"
],
"values": [
[
1,
"asff"
],
[
2,
"fiona2222"
]
]
}
]
状态信息
可以通过以下方式查看各 node 节点的状态信息:
status 集群状态信息
1. curl 请求
# curl 10.0.21.5:4001/status?pretty
{
"build": {
"branch": "master",
"build_time": "2017-11-20T10:08:20+0000",
"commit": "fb3cc196802a87edb0f2b5fbee383502cd207051",
"version": "4"
},
"http": {
"addr": "10.0.21.5:4001",
"auth": "disabled",
"redirect": "10.0.21.17:4001"
},
......
2. rqlite 命令
# rqlite -H 10.0.21.5
10.0.21.5:4001> .status
runtime:
GOARCH: amd64
GOMAXPROCS: 1
GOOS: linux
......
expvar 节点全局变量信息
1. curl 请求
curl 10.0.21.7:4001/debug/vars
2. rqlite 命令
10.0.21.5:4001> .expvar
cmdline: [./rqlited data]
db:
execute_transactions: 0
execution_errors: 1
executions: 1
queries: 0
query_transactions: 0
......
memstats:
Mallocs: 8950
HeapSys: 2.588672e+06
StackInuse: 557056
......
如何监控
我们可以从 status 接口获取比较详细的信息, 比如集群中的 leader 和 follower, 从 raft 相关的信息中可以监控到集群状态的变化; 如果要监控 qps 相关的信息, 可以通过 expvar 接口获取, 结果示例见[状态信息]部分, 其中 db 部分为更新值为 executtions
参数, 查询值为 queries
参数, 我们可以依据这些信息进行 qps 相关的监控; memstats 部分则为当前内存的占用信息, 也包括 GC 的信息. 更多参数信息参见 godoc expvar
, memstats 信息可以参考 godoc runtime
的 MemStats 部分, 其它参数值的单位也可以从这里获取.
也可以通过 expvarmon 工具连接 expvar 接口直接查看监控的数据, 不过统计到的数据都是从节点启动到当前时间的值, qps 等相关的指标需要我们单独计算. 如下所示:
# expvarmon -ports "http://10.0.21.7:4001" -i 3s -vars "mem:memstats.Alloc,mem:memstats.Sys,mem:memstats.HeapAlloc,mem:memstats.HeapInuse,queries:db.queries,execution:db.executions,transaction:db.query_transactions" -dummy
14:03:44 27/11
rqlited: Alloc: 1.4MB, Sys: 7.3MB, HeapAlloc: 1.4MB, HeapInuse: 2.3MB, queries: 7, executions: 27, query_transactions: 0,
14:03:47 27/11
rqlited: Alloc: 1.7MB, Sys: 7.3MB, HeapAlloc: 1.7MB, HeapInuse: 2.5MB, queries: 7, executions: 27, query_transactions: 0,
14:03:50 27/11
rqlited: Alloc: 1.9MB, Sys: 7.3MB, HeapAlloc: 1.9MB, HeapInuse: 2.6MB, queries: 7, executions: 27, query_transactions: 0,
如何备份
rqlited 支持热备, 使用 curl 命令即可备份:
curl 10.0.21.5:4001/db/backup -o bak.sqlite3
注意:目前使用中只有在 leader 节点才能备份成功
如何恢复
rqlited 备份的数据格式同 sqlite3 格式, 可以通过 sqlite3 将备份的格式转换为文本形式再进行恢复:
先删除现有的数据表 foo 当做数据丢失:
# rqlite -H 10.0.21.5
10.0.21.5:4001> .tables
+------+
| name |
+------+
| foo |
+------+
10.0.21.5:4001> drop table foo;
3 rows affected (0.000000 sec)
10.0.21.5:4001> .tables
+------+
| name |
+------+
将上述备份的 bak.sqlite3 转为文本格式:
# echo ".dump" | sqlite3 bak.sqlite3 > restore.dump
恢复 restore.dump 到 leader 中:
curl -XPOST 10.0.21.5:4001/db/load -H "Content-type: text/plain" --data-binary @restore.dump
注意: 同备份一样, 只能在 leader 角色中恢复成功.
安全问题
rqlite 也考虑到了安全方面, 可以指定 auth
选项设置以用户和密码才能访问 rqlited, 另外集群之间的通信可以使用 https 接口进行加密. 如果开启了 -on-disk
选项, 节点中的文件仍旧以 sqlite 的格式存储, 所以不会对数据文件加密. 所以总体上看 rqlite 只是考虑到了如何访问和通信方面的安全.实际上很多数据库软件都没有考虑对数据文件进行加密.
性能
rqlite 基于 raft 协议实现数据的一致性和冗余性, 所以本质上是以牺牲性能达到目标的. 实际使用中性能问题也会因网络问题而受到影响. 如果想提高性能, 可以考虑使用批量操作或事务操作一次处理多个记录以提高吞吐量.
另外 rqlite 默认使用 sqlite 的特性in-memory SQLite database 来尽可能的增加性能. 如果觉得性能不是那么重要, 可以在 rqlited 启动的时候增加 on-disk
选项使 rqlite 使用 file-based SQLite database
特性.
同样 rqlite 使用 in-memory 特性不会使我们的数据有丢失的风险, 因为每个节点上的 raft 日志已经保存了相关的数据.
如果应用程序使用 http 接口访问数据, 过快频率的操作可能会引起本地端口的耗尽, 如果 client 端实现了长连接则 qps 相关的性能会更好.
在本文 [数据更新及查询的实现问题] 部分, 我们提到了查询和更新数据的实现问题, 所以从这方面来看, rqlite 集群并不支持多点写操作, 所有的写压力都在 leader 节点中.
限制
已知的限制包括:
1. 不安全语句
不要执行不安全的语句, 这点同 MySQL 中的 unsafe 问题. rqlite 使用类似 基于语句(statement-based)的复制格式, 所以应该避免使用不安全类的函数, 比如下面的函数多个节点的值肯定不相同:
INSERT INTO foo(n) VALUES(random());
2. SQLite 文件
从技术上讲, 如果使用了 on-disk
选项, 你可以在任何时间读取任意节点的 SQLite 文件. 但 rqlite 不保证集群中的变化已经保存到了 SQLite 文件里, 痴肥集群已经收到并应用了保存了这些变更.
3. 直接操作 SQLite 文件
开启 on-disk
选项的时候, 直接操作 SQLite 文件并不会复制到其它的节点, 相反可能会引起 rqlite 的崩溃.
错误汇总:
maximum leader redirect 问题
func query(ctx *cli.Context, cmd, line string, argv *argT) error {
urlStr := fmt.Sprintf("%s://%s:%d%sdb/query", argv.Protocol, argv.Host, argv.Port, argv.Prefix)
......
......
}
如果 -http-addr 0.0.0.0:4001
, 则需要使用 -http-adv-addr
宣告集群的地址, 指定 -raft-addr 0.0.0.0:4002
时同理, 需要设置 -raft-adv-addr
宣告通信的地址, 详见 listen-on-all-interface, 否则代码中的 urlStr 为地址 http://0.0.0.0:4001/db/query
, rqlite 客户端连接的时候, 会进行重定向处理, 超过默认的27次则返回错误maximum leader redirect limit exceeded
:
func sendRequest(ctx *cli.Context, urlStr string, line string, argv *argT, ret interface{}) error {
......
// Check for redirect.
if resp.StatusCode == http.StatusMovedPermanently {
nRedirect++
if nRedirect > maxRedirect {
return fmt.Errorf("maximum leader redirect limit exceeded")
}
url = resp.Header["Location"][0]
continue
}
......
如下所示增加以下参数监听所有网卡:
rqlited -http-addr "0.0.0.0:4001" -http-adv-addr "10.0.21.5:4001" -raft-addr "0.0.0.0:4002" --raft-adv-addr "10.0.21.5:4002" --on-disk /web/rqlite/
恢复问题
恢复数据的时候, 最好清除掉以前集群中的数据, 如果没有完全清除请确保集群中没有未完成的事务. 详见 issue-382 了解详细的错误信息以及避免 transaction error occur
产生的方法. 后续版本中作者可能会解决这个问题.
内存问题
rqlite 本身没有限制内存的功能, 尤其是没有开启 on-disk
选项的情况下内存占用会很多, 所以最好不要往 rqlite 中存很多数据, 以免造成 OOM 问题. 另外数据过多, 备份恢复的时间也会很长, 如果数据很多, 可以考虑在程序端做分区设计.