test: 补齐 Q2 验收与本机 Q3 弱网崩溃压测
This commit is contained in:
+7
-46
@@ -1,61 +1,22 @@
|
||||
# 混沌 / 弱网测试辅助(Q 线)
|
||||
|
||||
用 toxiproxy 官方镜像做延迟和断开;Linux 丢包用容器内 netem。所有 Docker 资源名带 `q` 前缀,用完删除。
|
||||
用 toxiproxy 官方镜像做延迟和断开;Linux 丢包用容器内 netem。Q3 相关 Docker 资源名带 `q3` 前缀,用完删除。
|
||||
|
||||
## 启动 toxiproxy
|
||||
|
||||
在仓库根目录(或本目录)执行:
|
||||
|
||||
```powershell
|
||||
docker compose -p q-chaos -f test/chaos/compose.yml up -d
|
||||
```
|
||||
|
||||
API 默认映射到本机随机/固定端口见 compose 注释。查看实际端口:
|
||||
|
||||
```powershell
|
||||
docker compose -p q-chaos -f test/chaos/compose.yml port toxiproxy 8474
|
||||
docker compose -p q3-chaos -f test/chaos/compose.yml up -d
|
||||
docker compose -p q3-chaos -f test/chaos/compose.yml port toxiproxy 8474
|
||||
```
|
||||
|
||||
清理:
|
||||
|
||||
```powershell
|
||||
docker compose -p q-chaos -f test/chaos/compose.yml down -v
|
||||
docker compose -p q3-chaos -f test/chaos/compose.yml down -v
|
||||
```
|
||||
|
||||
## 对运行中的服务注入故障
|
||||
集成测 `TestQ3ToxiproxyOfflineKeepDelivered` 会自动起停上述 compose。
|
||||
|
||||
不要写死 `7443`。上游地址取自当前实例的实际监听端口(例如测试 harness 写入的 `listen.addr`,或你启动时打印的地址)。
|
||||
## netem
|
||||
|
||||
1. 本机起好 NixMsg(或任意 TCP 服务),记下 `HOST:PORT`。
|
||||
2. 在 Windows 上,容器访问本机服务用 `host.docker.internal:PORT`。
|
||||
3. 用本包客户端或 curl 建代理(listen 用 `0.0.0.0:0` 让 toxiproxy 分配端口):
|
||||
|
||||
```go
|
||||
c := chaos.NewClient("http://127.0.0.1:<api端口>")
|
||||
p, err := c.CreateProxy("q-nixmsg", "0.0.0.0:0", "host.docker.internal:"+strconv.Itoa(port))
|
||||
// 客户端改连 p.Listen(把 0.0.0.0 换成 127.0.0.1)
|
||||
_, _ = c.AddLatency("q-nixmsg", "lag", 200, 50, "")
|
||||
_, _ = c.AddResetPeer("q-nixmsg", "rst", 0, "")
|
||||
```
|
||||
|
||||
等价 curl:
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:<api>/proxies \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"name":"q-nixmsg","listen":"0.0.0.0:0","upstream":"host.docker.internal:<PORT>","enabled":true}'
|
||||
```
|
||||
|
||||
延迟:`POST .../proxies/q-nixmsg/toxics`,`type=latency`,`attributes.latency` / `jitter`(毫秒)。
|
||||
|
||||
断开:`type=reset_peer`,`attributes.timeout`(毫秒,0 表示尽快 RST)。
|
||||
|
||||
## 单元测试
|
||||
|
||||
```powershell
|
||||
go test ./test/chaos/ -count=1
|
||||
```
|
||||
|
||||
## netem 丢包
|
||||
|
||||
见 [netem/README.md](./netem/README.md)。脚本必须在 Linux 容器内执行(本机是 Windows)。
|
||||
见 [netem/README.md](./netem/README.md)。本机 Windows 上 Q3 将 netem 20% 丢包记为未测(见 `docs/DEVIATIONS.md` 测试交付 Q)。
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
# toxiproxy:延迟与断开。项目名用 -p q-chaos,容器名带 q 前缀。
|
||||
# toxiproxy:延迟与断开。项目名用 -p q3-chaos,容器名带 q3 前缀。
|
||||
# API 与代理端口映射到本机,具体宿主机端口用 `docker compose port` 查询,不要写死业务端口。
|
||||
|
||||
services:
|
||||
toxiproxy:
|
||||
image: ghcr.io/shopify/toxiproxy:2.12.0
|
||||
container_name: q-toxiproxy
|
||||
# 8474=API;8475 起可手工映射代理端口,或在 CreateProxy 时用 0.0.0.0:0 再 docker port 查询
|
||||
container_name: q3-toxiproxy
|
||||
ports:
|
||||
- "8474"
|
||||
- "8475"
|
||||
- "8476"
|
||||
# 允许代理到本机服务(Docker Desktop / Windows)
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package chaos_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.asio.asia/nixevol/NixMsg/test/accept"
|
||||
"git.asio.asia/nixevol/NixMsg/test/harness"
|
||||
)
|
||||
|
||||
// TestQ3CrashSubmitThenRestart:提交返回成功后杀进程,重启后续传(离线保留消息)。
|
||||
func TestQ3CrashSubmitThenRestart(t *testing.T) {
|
||||
srv, err := accept.StartManaged()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = srv.Cleanup() }()
|
||||
|
||||
hs := &harness.Server{
|
||||
HTTPBase: srv.HTTPBase,
|
||||
AdminHTTPBase: srv.AdminHTTPBase,
|
||||
AdminPassword: srv.AdminPassword,
|
||||
}
|
||||
ac := accept.AdminLogin(t, hs)
|
||||
accept.CreateEndpoint(t, ac, "q3crasha1", epPassword)
|
||||
accept.CreateEndpoint(t, ac, "q3crashb1", epPassword)
|
||||
|
||||
alice := accept.MQTTLogin(t, srv.HTTPBase, "q3crasha1", epPassword)
|
||||
sendOff := alice.Request(t, map[string]any{
|
||||
"v": 1, "type": "send", "rid": "cr1", "id": "q3-crash-1",
|
||||
"to": map[string]any{"kind": "endpoint", "id": "q3crashb1"},
|
||||
"body": map[string]any{"enc": "utf8", "data": "after-crash"},
|
||||
"delay_ms": int64(0),
|
||||
"offline": map[string]any{"keep": true, "ttl_seconds": int64(86400)},
|
||||
})
|
||||
if !sendOff.OK {
|
||||
t.Fatalf("submit before crash: %+v", sendOff)
|
||||
}
|
||||
alice.Close()
|
||||
|
||||
if err := srv.Kill(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
if err := srv.Restart(); err != nil {
|
||||
t.Fatalf("restart: %v", err)
|
||||
}
|
||||
|
||||
bob := accept.MQTTLogin(t, srv.HTTPBase, "q3crashb1", epPassword)
|
||||
defer bob.Close()
|
||||
msg := bob.WaitType(t, "msg", 20*time.Second)
|
||||
if msg["id"] != "q3-crash-1" {
|
||||
t.Fatalf("want q3-crash-1 after restart, got %v", msg)
|
||||
}
|
||||
bob.Request(t, map[string]any{"v": 1, "type": "ack", "rid": "cra1", "from": "q3crasha1", "id": "q3-crash-1"})
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package chaos_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.asio.asia/nixevol/NixMsg/test/accept"
|
||||
"git.asio.asia/nixevol/NixMsg/test/chaos"
|
||||
"git.asio.asia/nixevol/NixMsg/test/harness"
|
||||
)
|
||||
|
||||
const (
|
||||
q3ComposeProject = "q3-chaos"
|
||||
q3ProxyName = "q3-nixmsg"
|
||||
epPassword = "password1234"
|
||||
)
|
||||
|
||||
// TestQ3ToxiproxyOfflineKeepDelivered:延迟 + 断开后,离线保留期内消息最终送达。
|
||||
func TestQ3ToxiproxyOfflineKeepDelivered(t *testing.T) {
|
||||
if _, err := exec.LookPath("docker"); err != nil {
|
||||
t.Skip("docker 不可用,跳过 toxiproxy 弱网")
|
||||
}
|
||||
|
||||
composeFile := filepath.Join(moduleRoot(t), "test", "chaos", "compose.yml")
|
||||
down := func() {
|
||||
cmd := exec.Command("docker", "compose", "-p", q3ComposeProject, "-f", composeFile, "down", "-v", "--remove-orphans")
|
||||
_ = cmd.Run()
|
||||
}
|
||||
down()
|
||||
t.Cleanup(down)
|
||||
|
||||
up := exec.Command("docker", "compose", "-p", q3ComposeProject, "-f", composeFile, "up", "-d")
|
||||
if out, err := up.CombinedOutput(); err != nil {
|
||||
t.Fatalf("compose up: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
apiHostPort := waitComposePort(t, composeFile, "8474", 90*time.Second)
|
||||
listenHostPort := waitComposePort(t, composeFile, "8475", 90*time.Second)
|
||||
apiURL := "http://127.0.0.1:" + apiHostPort
|
||||
waitHTTP(t, apiURL+"/version", 90*time.Second)
|
||||
|
||||
srv, err := harness.Start(harness.Options{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = srv.Stop() }()
|
||||
|
||||
_, nixPort, err := net.SplitHostPort(srv.Addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c := chaos.NewClient(apiURL)
|
||||
_ = c.DeleteProxy(q3ProxyName)
|
||||
if _, err := c.CreateProxy(q3ProxyName, "0.0.0.0:8475", "host.docker.internal:"+nixPort); err != nil {
|
||||
t.Fatalf("CreateProxy: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = c.DeleteProxy(q3ProxyName) })
|
||||
|
||||
proxyBase := "http://127.0.0.1:" + listenHostPort
|
||||
if _, err := c.AddLatency(q3ProxyName, "q3-lag", 150, 50, ""); err != nil {
|
||||
t.Fatalf("AddLatency: %v", err)
|
||||
}
|
||||
|
||||
ac := accept.AdminLogin(t, srv)
|
||||
accept.CreateEndpoint(t, ac, "q3alice01", epPassword)
|
||||
accept.CreateEndpoint(t, ac, "q3bob0001", epPassword)
|
||||
|
||||
alice := accept.MQTTLogin(t, proxyBase, "q3alice01", epPassword)
|
||||
defer alice.Close()
|
||||
|
||||
sendOff := alice.Request(t, map[string]any{
|
||||
"v": 1, "type": "send", "rid": "q3s1", "id": "q3-off-keep",
|
||||
"to": map[string]any{"kind": "endpoint", "id": "q3bob0001"},
|
||||
"body": map[string]any{"enc": "utf8", "data": "keep-me"},
|
||||
"delay_ms": int64(0),
|
||||
"offline": map[string]any{"keep": true, "ttl_seconds": int64(86400)},
|
||||
})
|
||||
if !sendOff.OK {
|
||||
t.Fatalf("send offline keep: %+v", sendOff)
|
||||
}
|
||||
|
||||
if _, err := c.AddResetPeer(q3ProxyName, "q3-rst", 0, ""); err != nil {
|
||||
t.Fatalf("AddResetPeer: %v", err)
|
||||
}
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
_ = c.RemoveToxic(q3ProxyName, "q3-rst")
|
||||
_ = c.RemoveToxic(q3ProxyName, "q3-lag")
|
||||
|
||||
bob := accept.MQTTLogin(t, proxyBase, "q3bob0001", epPassword)
|
||||
defer bob.Close()
|
||||
msg := bob.WaitType(t, "msg", 20*time.Second)
|
||||
if msg["id"] != "q3-off-keep" {
|
||||
t.Fatalf("want q3-off-keep got %v", msg)
|
||||
}
|
||||
bob.Request(t, map[string]any{"v": 1, "type": "ack", "rid": "q3a1", "from": "q3alice01", "id": "q3-off-keep"})
|
||||
}
|
||||
|
||||
// TestQ3NetemUntested 记录本机 Windows 上 netem 20% 丢包未测原因。
|
||||
func TestQ3NetemUntested(t *testing.T) {
|
||||
t.Log("未测:Linux netem 20% 丢包。本机为 Windows;无法在宿主直接用 tc/netem。" +
|
||||
"若只对 toxiproxy 容器网卡挂 netshoot,不等于 NixMsg 业务端到端丢包验收。" +
|
||||
"本波用 toxiproxy 延迟/断开覆盖弱网;netem 需 Linux 宿主或把服务放进同网络 Linux 容器后再测。")
|
||||
}
|
||||
|
||||
func moduleRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
||||
return dir
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
t.Fatal("go.mod not found")
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
|
||||
func waitComposePort(t *testing.T, composeFile, containerPort string, timeout time.Duration) string {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(timeout)
|
||||
var last string
|
||||
for time.Now().Before(deadline) {
|
||||
cmd := exec.Command("docker", "compose", "-p", q3ComposeProject, "-f", composeFile, "port", "toxiproxy", containerPort)
|
||||
out, err := cmd.CombinedOutput()
|
||||
last = strings.TrimSpace(string(out))
|
||||
if err == nil && last != "" {
|
||||
_, port, perr := net.SplitHostPort(normalizeComposeAddr(last))
|
||||
if perr == nil && port != "" {
|
||||
return port
|
||||
}
|
||||
}
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("compose port %s timeout, last=%q", containerPort, last)
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeComposeAddr(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if strings.HasPrefix(s, "[::]") {
|
||||
return "127.0.0.1" + s[len("[::]"):]
|
||||
}
|
||||
if strings.HasPrefix(s, "0.0.0.0:") {
|
||||
return "127.0.0.1" + s[len("0.0.0.0"):]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func waitHTTP(t *testing.T, url string, timeout time.Duration) {
|
||||
t.Helper()
|
||||
client := &http.Client{Timeout: 2 * time.Second}
|
||||
deadline := time.Now().Add(timeout)
|
||||
var last error
|
||||
for time.Now().Before(deadline) {
|
||||
resp, err := client.Get(url)
|
||||
if err == nil {
|
||||
_ = resp.Body.Close()
|
||||
if resp.StatusCode < 500 {
|
||||
return
|
||||
}
|
||||
last = err
|
||||
} else {
|
||||
last = err
|
||||
}
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("wait HTTP %s: %v", url, last)
|
||||
}
|
||||
Reference in New Issue
Block a user