在Miniredis上进行Dapr State的异常情况验证
太长不看
-
- 特定条件でリクエストが失敗するStateを、Miniredisで作成
-
- 異常時の動作を簡単に検証可能!
-
- サンプル:11回目のリクエストでエラーが起こるState
dapr-state-samples/miniredis at main · Syuparn/dapr-state-samples · GitHub
首先
Dapr以”组件”形式将持久化和发布订阅机制进行了抽象化,使得使用者无需关注基础设施即可使用。此外,只需将请求作为应用的辅助组件,就可以将其转发到任意喜欢的组件上。
非常方便!…但是,由于其抽象化程度较高,很难进行异常情况的测试/操作确认。
如果有一个可以验证异常情况的组件,那将会加快验证的进程…
そこで、本記事ではRedisのモックMiniredisを使って、特定条件下で壊れるState(永続化) componentを作ってみたいと思います。
示例代码 lì
我创建了一个只能发起10次请求的State。它考虑到在处理过程中突然断开连接的情况。
- dapr-state-samples/miniredis at main · Syuparn/dapr-state-samples · GitHub
我正在使用docker-compose搭建一个简单的REST API。
Miniredis是什么?
Miniredis是一个使用Golang编写的Redis测试服务器。
GitHub – alicebob/miniredis: Go语言单元测试的纯Go Redis服务器。
这个库是为了在Redis客户端的单元测试中使用,正如Readme中所述,它使用实际的TCP通信和RESP,因此可以接收外部请求。
package main
import (
"log"
"time"
miniredis "github.com/alicebob/miniredis/v2"
)
func main() {
s := miniredis.NewMiniRedis()
// メソッドでRedis操作が可能
s.Set("foo", "bar")
s.HSet("some", "other", "key")
if err := s.StartAddr("localhost:6379"); err != nil {
panic(err)
}
defer s.Close()
log.Printf("miniredis serves on %s\n", s.Addr())
// NOTE: sleepしないとリクエストを待ち受けられないので注意
time.Sleep(999999 * time.Hour)
}
$ go run main.go
2021/04/11 12:16:27 miniredis serves on 127.0.0.1:6379
# redis-cliで疎通可能!
# さっき作ったキーが確認できる
$ redis-cli keys '*'
1) "foo"
2) "some"
$ redis-cli get foo
"bar"
$ redis-cli hgetall some
1) "other"
2) "key"
此外,由于我们在内存中进行状态管理,因此只要更新数据,就会正确地反映出来。
$ redis-cli set newkey 1
OK
$ redis-cli get newkey
"1"
フックで異常な状態を再現
これだけではただのインメモリ専用Redisですが、フックを仕掛けることで動作を制御することができます。
服务器 · pkg.go.dev
在每个命令执行之前,钩子函数都会运行。如果返回true,则不执行命令,直接退出。
测试用例:发送10次请求会导致崩溃。
让我们尝试创建一个Redis,当尝试发送10个请求时,就无法再发送请求了。虽然这只需要在之前的代码中添加一个钩子函数就可以实现。
func main() {
s := miniredis.NewMiniRedis()
if err := s.StartAddr("localhost:6379"); err != nil {
panic(err)
}
defer s.Close()
done := make(chan struct{})
defer close(done)
log.Printf("miniredis serves on %s\n", s.Addr())
// フックを追加(サーバー起動後じゃないとnil pointer dereferenceになるので注意)
s.Server().SetPreHook(
limitRequestHook(done),
)
// 時間切れかdoneの早い方で抜ける
for {
select {
case <-time.After(999999 * time.Hour):
return
case <-done:
return
}
}
}
func limitRequestHook(done chan struct{}) server.Hook {
// クロージャを使いリクエスト数を記録
nReq := 0
return func(c *server.Peer, cmd string, args ...string) bool {
nReq++
if nReq > maxReq {
c.WriteError("MiniRedis went home.")
log.Println("Bye!")
done <- struct{}{} // 終了通知
return true
}
log.Printf("Request: %d/%d\n", nReq, maxReq)
return false
}
}
在通道处理中,当请求数量达到上限时,程序将终止。
当执行时,确实可以在第11次请求时确认崩溃。
$ go run main.go
2021/04/11 12:40:02 miniredis serves on 127.0.0.1:6379
2021/04/11 12:40:10 Request: 1/10
...
2021/04/11 12:40:15 Request: 10/10
2021/04/11 12:40:16 Bye!
$ redis-cli keys '*'
(empty list or set)
# ...
# 11回目
$ redis-cli keys '*'
(error) MiniRedis went home.
$ redis-cli keys '*'
Could not connect to Redis at 127.0.0.1:6379: Connection refused
请使用Dapr进行功能验证。
在将此Miniredis集成到Dapr中之前,有两点额外的准备工作需要完成。
更改IP
为了使别的容器(Dapr Sidecar)能够向Miniredis发送请求,将服务器的IP地址从localhost更改为0.0.0.0 。
(Miniredis无法使用conf文件)
if err := s.StartAddr("0.0.0.0:6379"); err != nil {
panic(err)
}
处理 INFO 复制
Dapr边车在启动时会请求Redis的副本数量,以获取 INFO replication。然而,Miniredis不支持 INFO 命令。
在GitHub上的dapr/components-contrib的redis.go文件中。
GitHub – alicebob/miniredis: 不受支持
因此,我们将使用钩子,以便对 INFO replication 请求返回一个模拟响应。
当我在bitnami的Redis镜像上尝试时,收到了以下请求的回复。
$ redis-cli INFO replication
"# Replication",
"role:master",
"connected_slaves:0",
"master_failover_state:no-failover",
"master_replid:b9bca6c53f5f6e52047e05566897add8b3f3c662",
"master_replid2:0000000000000000000000000000000000000000",
"master_repl_offset:0",
"second_repl_offset:-1",
"repl_backlog_active:0",
"repl_backlog_size:1048576",
"repl_backlog_first_byte_offset:0",
"repl_backlog_histlen:0",
GitHub – bitnami/bitnami-docker-redis:Bitnami Redis Docker映像
我会把这个结果复制粘贴到钩子里供以后使用。
func mockInfoHook(c *server.Peer, cmd string, args ...string) bool {
// INFO replication 以外は何もしない
if cmd != "INFO" || len(args) < 1 {
return false
}
if args[0] != "replication" {
return false
}
mockLines := []string{
"# Replication",
"role:master",
"connected_slaves:0",
"master_failover_state:no-failover",
"master_replid:b9bca6c53f5f6e52047e05566897add8b3f3c662",
"master_replid2:0000000000000000000000000000000000000000",
"master_repl_offset:0",
"second_repl_offset:-1",
"repl_backlog_active:0",
"repl_backlog_size:1048576",
"repl_backlog_first_byte_offset:0",
"repl_backlog_histlen:0",
"",
}
// マルチバルクリプライ
c.WriteLen(len(mockLines))
for _, l := range mockLines {
c.WriteBulk(l)
}
c.Flush()
return true
}
在公式参考文献中,只有写着INFO将返回一个Bulk string Reply,但是只是简单地叠加Bulk string的话,使用redis-cli只能显示第一行,因此我使用了Multi Bulk Reply。
参考:Redis 2.0.3 文档 – 协议规范
此外,由于服务器只能设置一个钩子,我们还准备了一个钩子合成函数,以便使用请求限制钩子合成函数。
func main() {
// ...
s.Server().SetPreHook(mergeHooks(
mockInfoHook,
limitRequestHook(done),
))
// ...
}
func mergeHooks(hooks ...server.Hook) server.Hook {
return func(c *server.Peer, cmd string, args ...string) bool {
for _, h := range hooks {
// trueの場合コマンド実行に進む必要が無いので打ち切り、falseなら次のフックに進む
if h(c, cmd, args...) {
return true
}
}
return false
}
}
确认动作
在示例应用程序中,每次进行POST请求时,REST API会将状态持久化到状态中。
在第11次请求时,应用程序出现了服务器错误。
# 最初は成功
$ curl -X POST localhost:8080/messages -H "Content-Type: application/json" -d '{"message": "hello, state!"}'
{"id":"01F2ZBX621SVTPYJWTKVYC1JNA","message":"hello, state!"}
# 11回目のリクエスト (※HTTPリクエストではなくRedisリクエストの回数)
$ curl -X POST localhost:8080/messages -H "Content-Type: application/json" -d '{"message": "hello, state!"}'
{"message":"Internal Server Error"}
ログを見てみると、確かにStateへの疎通失敗が確認できます。
$ docker-compose logs --tail 1 goapp
Attaching to miniredis_goapp_1
goapp_1 | {"time":"2021-04-11T02:27:46.768353137Z","id":"","remote_ip":"172.27.0.1","host":"localhost:8080","method":"POST","uri":"/messages","user_agent":"curl/7.68.0","status":500,"error":"failed to save message: failed to save message: error saving state: rpc error: code = Internal desc = failed saving state in state store redis-state: failed to set key goapp||01F2ZC3AYERA1CYBY6XS612ZM5: MiniRedis went home.","latency":1422117,"latency_human":"1.422117ms","bytes_in":28,"bytes_out":36}
$ docker-compose logs redis
Attaching to miniredis_redis_1
redis_1 | 2021/04/11 02:17:34 miniredis serves on [::]:6379
redis_1 | 2021/04/11 02:17:36 Request: 1/10
...
redis_1 | 2021/04/11 02:24:46 Request: 10/10
redis_1 | 2021/04/11 02:24:46 Bye!
仍有其他异常测试的选项。
只有DEL失败。
func failDeleteHook(c *server.Peer, cmd string, args ...string) bool {
if cmd != "DEL" {
return false
}
c.WriteError("This object is designated as a world heritage site.")
return true
}
# setはできるので保存成功
$ curl -X POST localhost:8080/messages -H "Content-Type: application/json" -d '{"message": "hello, state!"}'
{"id":"01F2ZS7D6G5BDZ7KRAP5SQRYAV","message":"hello, state!"}
# 削除は失敗
$ docker-compose exec goapp curl -X DELETE http://localhost:3500/v1.0/state/redis-state/01F2ZS7D6G5BDZ7KRAP5SQRYAV
{"errorCode":"ERR_STATE_DELETE","message":"failed deleting state with key 01F2ZS7D6G5BDZ7KRAP5SQRYAV: possible etag mismatch. error from state store: ERR Error compiling script (new function): \u003cstring\u003e:1: This object is designated as a world heritage site. stack traceback: [G]: in function 'call' \u003cstring\u003e:1: in main chunk [G]: ?"}
只能与特定的应用程序无法进行状态通信
State每个应用程序都有自己的命名空间,并以||的格式保存。
状态管理 API 参考 | Dapr 文档
利用这个规范,可以创建一个只有特定应用程序无法与状态进行通信的情况。
func failGoappHook(c *server.Peer, cmd string, args ...string) bool {
if len(args) < 1 {
return false
}
// 操作対象keyの名前空間がgoappの場合(=goappからのリクエスト)は失敗
if strings.HasPrefix(args[0], "goapp||") {
c.WriteError("It's none of your business!")
return true
}
return false
}
$ curl -X POST localhost:8080/messages -H "Content-Type: application/json" -d '{"message": "hello, state!"}'
{"message":"Internal Server Error"}
$ docker-compose logs --tail 1 goapp
Attaching to miniredis_goapp_1
goapp_1 | {"time":"2021-04-11T06:27:26.778520964Z","id":"","remote_ip":"172.31.0.1","host":"localhost:8080","method":"POST","uri":"/messages","user_agent":"curl/7.68.0","status":500,"error":"failed to save message: failed to save message: error saving state: rpc error: code = Internal desc = failed saving state in state store redis-state: failed to set key goapp||01F2ZST5XNXHZP6MVNEHAV4NEG: ERR Error compiling script (new function): <string>:1: It's none of your business! stack traceback: [G]: in function 'call' <string>:1: in main chunk [G]: ?","latency":4940434,"latency_human":"4.940434ms","bytes_in":28,"bytes_out":36}
# 他のアプリケーション(goapp2)からはアクセス可能
$ curl -X POST localhost:8081/messages -H "Content-Type: application/json" -d '{"message": "hello, state!"}'
{"id":"01F2ZTAGY6PQFPNR84ADQ93EPR","message":"hello, state!"}
$ docker-compose exec redis redis-cli keys '*'
1) "goapp2||01F2ZTAGY6PQFPNR84ADQ93EPR"
最后
最后
以上是关于Dapr State异常操作确认的介绍。Redis既可以用于Pubsub,也可以用于Bindings,所以异常操作的验证应该会很顺利。