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 问题. 另外数据过多, 备份恢复的时间也会很长, 如果数据很多, 可以考虑在程序端做分区设计.