【网络编程】KV存储项目中的测试与测压:从功能校验到简单 QPS 统计

在 KV 存储这类网络项目里,服务端功能写出来只是第一步,真正能帮助定位问题、验证逻辑、观察性能的,往往是测试代码。一个完整的测试程序,通常不只是"连上服务端发几条消息",而是要把功能测试、结果校验和简单压测串成一条完整链路。本文就围绕一个简易 TCP 测试程序,梳理 KV 存储项目中测试与测压的实现思路,包括如何建立连接、如何封装单次测试、如何组织一整组功能测试,以及如何做一个最基础的 QPS 统计。

一、KV 存储里的测试,到底在测什么

很多时候一提"测试",容易想到的只是发一条 SET 命令,看看服务端回不回 OK

但在 KV 存储项目里,真正有意义的测试不是单条命令,而是一整条数据生命周期。

一个典型的测试流程通常是这样的:

  1. 先写入一条键值对
  2. 再把它读出来
  3. 再修改它的值
  4. 再次读取确认修改成功
  5. 判断这个 key 是否存在
  6. 删除这条数据
  7. 删除后再次读取,确认已经不存在
  8. 删除后再修改,确认服务端能返回"对象不存在"
  9. 再次判断是否存在

也就是说,测试程序不是在"乱发命令",而是在模拟一个 key 从创建到删除的完整过程。

这种测试方式的价值很大,因为它不仅能验证某个接口是否可用,还能验证多个接口串起来之后,整个状态流转是不是正确的。KV 存储这种项目,最怕的恰恰不是命令不能发,而是"前面能写,后面不能改""删完以后状态还不对"这类链路问题。当前测试程序里,就是按固定顺序执行 SET、GET、MOD、GET、EXIST、DEL、GET、MOD、EXIST 这一组操作来完成验证。

二、测试程序的核心,不是发消息,而是"带着答案去校验"

在网络项目里,测试程序本质上就是一个 TCP 客户端。

它做的事情可以概括成 4 步:

  1. 和服务端建立连接
  2. 发送一条请求
  3. 接收服务端响应
  4. 把"实际响应"和"期望响应"做比较

真正关键的地方,不是 send()recv() 本身,而是把一次测试封装成统一格式

这样每一条测试项都能写得很清楚,也很容易定位错误。

先看建立连接这一部分:

cpp 复制代码
#include <arpa/inet.h>

/* 连接服务端 */
int connect_tcpserver(const char *ip, unsigned short port){

    int connfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(struct sockaddr_in));

    server_addr.sin_family = AF_INET;              // IPv4
    server_addr.sin_addr.s_addr = inet_addr(ip);   // IP地址转换
    server_addr.sin_port = htons(port);            // 端口号转网络字节序

    if(0 != connect(connfd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr_in))){
        perror("connect");
        return -1;
    }

    return connfd;
}

这一段代码的作用很直接:

给测试程序一个 IP 和端口,它就主动去连接服务端。连接成功后,后面的所有请求和响应都走这条 TCP 连接。

再看发送和接收的简单封装:

cpp 复制代码
/* 发送请求 */
int send_msg(int connfd, char *msg, int length){
    int res = send(connfd, msg, length, 0);
    if(res < 0){
        perror("send");
        exit(1);
    }
    return res;
}

/* 接收响应 */
int recv_msg(int connfd, char *msg, int length){
    int res = recv(connfd, msg, length, 0);
    if(res < 0){
        perror("recv");
        exit(1);
    }
    return res;
}

单独把 sendrecv 包一下,最大的好处就是后面写测试逻辑时更清楚,不用每次都重复写错误处理。

接下来才是测试代码里最关键的部分,也就是"单条测试用例"的封装:

cpp 复制代码
#define MAX_MSG_LENGTH 1024

/* 单条测试:发请求、收响应、校验结果 */
void testcase(int connfd, char *msg, char *pattern, char *casename){
    if(!msg || !pattern || !casename) return;

    /* 1. 发送请求 */
    send_msg(connfd, msg, strlen(msg));

    /* 2. 接收服务端返回值 */
    char result[MAX_MSG_LENGTH] = {0};
    recv_msg(connfd, result, MAX_MSG_LENGTH);

    /* 3. 比较实际结果和期望结果 */
    if(strcmp(result, pattern) == 0){
        // printf("==> PASS -> %s\n", casename);
    }else{
        printf("==> FAILED -> %s, '%s' != '%s'\n", casename, result, pattern);
        exit(1);
    }
}

这一段特别值得学习,因为它把一次测试统一成了三个输入:

  • msg:要发给服务端的命令
  • pattern:希望服务端返回什么
  • casename:这条测试项的名字

比如下面这句:

cpp 复制代码
testcase(connfd, "SET Teacher Charon", "OK\r\n", "SET-Teacher");

它的意思就是:

  • 给服务端发:SET Teacher Charon
  • 期望服务端回:OK\r\n
  • 如果返回值不是这个,就当成测试失败

也就是说,测试程序不是"试试看能不能发出去",而是先把正确答案写出来,再让程序自动对照检查

这就是功能测试真正有用的地方。

三、KV 存储功能测试怎么组织,才算完整

有了 testcase() 这种单条测试封装以后,剩下的事情就简单很多了。

只需要把一组业务相关的命令按顺序串起来,就能形成一整套功能测试。

例如下面这组测试:

cpp 复制代码
/* 一组完整的 KV 功能测试 */
void array_testcase(int connfd){
    testcase(connfd, "SET Teacher Charon", "OK\r\n", "SET-Teacher");
    testcase(connfd, "GET Teacher", "Charon\r\n", "GET-Teacher");
    testcase(connfd, "MOD Teacher King", "OK\r\n", "MOD-Teacher");
    testcase(connfd, "GET Teacher", "King\r\n", "GET-Teacher");
    testcase(connfd, "EXIST Teacher King", "EXIST\r\n", "MOD-Teacher");
    testcase(connfd, "DEL Teacher", "OK\r\n", "DEL-Teacher");
    testcase(connfd, "GET Teacher", "NO EXIST\r\n", "GET-Teacher");
    testcase(connfd, "MOD Teacher Charon", "NO EXIST\r\n", "MOD-Teacher");
    testcase(connfd, "EXIST Teacher", "NO EXIST\r\n", "GET-Teacher");
}

这一组命令本质上是在做下面几件事:

1)验证最基础的写入和读取

SET,再 GET,确认写进去的值确实能读出来。

2)验证修改逻辑

MOD,再 GET,确认旧值已经被替换成新值。

3)验证存在性判断

调用 EXIST,确认 key 仍然在存储里。

4)验证删除后的状态

DEL,再 GET、再 MOD、再 EXIST,确认删除以后服务端不会还把它当成存在对象。

这几步看起来简单,但其实已经把一个 KV 存储里最基本的状态流转都覆盖到了。

测试代码写到这一步,才算真正开始具备"帮忙发现 bug"的能力,而不是只做表面验证。当前服务端协议层也正是根据命令类型,把 SET/GET/DEL/MOD/EXIST 分发到对应的处理逻辑,并返回 "OK\r\n""NO EXIST\r\n""EXIST\r\n" 这类响应字符串,所以客户端才能拿这些字符串做自动校验。

四、功能测通以后,为什么还要测压

功能测试解决的是"对不对",

压测解决的是"快不快"。

一个 KV 存储项目,如果只是偶尔发一两条命令,很多问题根本暴露不出来。

真正循环跑起来以后,才会发现一些隐藏问题,比如:

  • 状态计数是否维护正确
  • 删除后是否能继续插入
  • 某个接口在大量调用下是否会返回异常结果
  • 性能大概在什么数量级

所以,最简单的做法就是把前面那组功能测试放进循环里,连续跑很多轮,再统计总耗时。

当前测试程序里,压测逻辑大概是这样写的:

cpp 复制代码
#include <sys/time.h>

/* 计算两次时间的毫秒差 */
#define TIME_SUB_MS(tv1, tv2) \
    ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)

/* 简单压测:循环执行固定测试集 */
void array_testcase_10w(int connfd){

    int count = 1000;

    struct timeval tv_begin;
    gettimeofday(&tv_begin, NULL);

    for(int i = 0; i < count; ++i){
        testcase(connfd, "SET Teacher Charon", "OK\r\n", "SET-Teacher");
        testcase(connfd, "GET Teacher", "Charon\r\n", "GET-Teacher");
        testcase(connfd, "MOD Teacher King", "OK\r\n", "MOD-Teacher");
        testcase(connfd, "GET Teacher", "King\r\n", "GET-Teacher");
        testcase(connfd, "EXIST Teacher King", "EXIST\r\n", "MOD-Teacher");
        testcase(connfd, "DEL Teacher", "OK\r\n", "DEL-Teacher");
        testcase(connfd, "GET Teacher", "NO EXIST\r\n", "GET-Teacher");
        testcase(connfd, "MOD Teacher Charon", "NO EXIST\r\n", "MOD-Teacher");
        testcase(connfd, "EXIST Teacher", "NO EXIST\r\n", "GET-Teacher");
    }

    struct timeval tv_end;
    gettimeofday(&tv_end, NULL);

    int time_used = TIME_SUB_MS(tv_end, tv_begin);

    printf("array testcase --> time_used: %d, qps: %d\n",
           time_used, 9000 * 1000 / time_used);
}

这里的思路其实很清楚:

  1. 先记录开始时间
  2. 连续执行很多轮测试
  3. 再记录结束时间
  4. 用总请求数除以总耗时,得到一个最基础的吞吐量指标

为什么这里写的是 9000 * 1000 / time_used

因为当前一轮里固定执行了 9 条请求 ,而 count = 1000,所以总请求数就是:

cpp 复制代码
9 * 1000 = 9000

如果 time_used 是毫秒,那么乘以 1000 以后,算出来的就是一个简化版的 QPS。

虽然这个统计方式还比较粗糙,但对于项目初期已经很有参考价值了:至少能知道服务端大概处在什么数量级。

五、KV 存储里的测试代码,真正有价值的地方是什么

很多人一开始写测试代码,会觉得这只是"辅助代码",不重要。

但实际上,在 KV 存储这种项目里,测试代码的价值非常高,主要体现在三个方面。

1)它能快速定位功能 bug

比如返回值不对、删除后状态异常、修改逻辑有问题,这些都能通过"期望值和实际值不一致"第一时间暴露出来。

之前这套逻辑在循环测试时出现过 SET 返回 ERRORMOD 返回值和预期不一致,本质上就是因为反复测试把存储逻辑里的状态维护问题逼出来了。当前数组存储实现里,set/get/del/mod/exist 的行为都直接影响客户端看到的 "OK""ERROR""NO EXIST"

2)它能把"手工点点点"变成自动验证

原来要手动敲很多条命令,现在只要运行一次测试程序,就能自动把一整套流程跑完。

对于后面继续改服务端代码来说,这一点非常重要,因为每改一次逻辑都能很快回归测试。

3)它能顺手完成最基础的性能观察

虽然这里只是一个很简单的 QPS 统计,但已经足够用来观察优化前后性能有没有变化。

比如改了协议解析、改了数据结构、改了删除逻辑之后,跑一遍测试就能马上看到耗时和吞吐量有没有明显波动。

总结

KV 存储项目里的测试板块,真正重要的不是"能不能发一条请求",而是能不能把功能验证、状态校验和简单性能观察串成一个完整闭环。

这类测试程序通常有三个最核心的作用:

  • 验证功能链路是否正确
  • 发现服务端逻辑 bug
  • 统计一个最基础的吞吐量指标

从实现上看,它其实并不复杂:

连接服务端、发送请求、接收响应、比较结果、循环执行、统计耗时。

但恰恰是这样一个看起来不大的测试模块,往往最能帮助把 KV 存储项目真正跑通。

0voice · GitHub

相关推荐
程序猿编码2 小时前
网络数据包环形缓存捕获技术:原理、设计与实现(C/C++代码实现)
linux·c语言·网络·tcp/ip·缓存
上海云盾-小余2 小时前
黑产入侵链路拆解:从打点踩点到内网横移的完整防御思路
网络·安全·web安全
supersolon2 小时前
PVE9安装32位爱快路由(ikuai)
linux·运维·网络
123过去2 小时前
mfterm使用教程
linux·网络·测试工具·安全
123过去2 小时前
nfc-mfclassic使用教程
linux·网络·测试工具·安全
虎头金猫2 小时前
自建 GitLab 没公网?用内网穿透技术,远程开发协作超丝滑
运维·服务器·网络·开源·gitlab·开源软件·开源协议
桌面运维家13 小时前
VLAN配置进阶:抑制广播风暴,提升网络效率
开发语言·网络·php
安静轨迹13 小时前
TLS_SSL 警报码完整手册
网络·网络协议·ssl
minji...14 小时前
Linux 进程信号(二)信号的保存,sigset_t,sigprocmask,sigpending
linux·运维·服务器·网络·数据结构·c++·算法