在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,所以异常操作的验证应该会很顺利。

这是默认行为,但也可以设置为带有固定值前缀或无前缀。 如何:在应用程序之间共享状态 | Dapr文档 ↩
广告
将在 10 秒后关闭
bannerAds