rqlite 使用及注意事项

介绍

rqlite 是一款轻量级且分布式的关系型数据库, 实际上它是在 SQLite 的基础上实现的关系型数据库, 加上 raft 协议实现了一致性分布式. 另外它还提供了 http 相关的接口. 不同于 etcdconsul 这两款键值数据库, 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.gosendRequest 函数可知, 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 问题. 另外数据过多, 备份恢复的时间也会很长, 如果数据很多, 可以考虑在程序端做分区设计.