記一次redis命令異常:參數截斷

  • 2019 年 12 月 10 日
  • 筆記

# 問題背景

最近項目在使用linux平台c++做開發,redis用到了hiredis庫。項目中用到redis list結構作為隊列,生產者和消費者模式解耦非同步任務:

生產者:

1. 將業務pb結構序列化為字元串 pbstr

2. 將字元串通過 rpush list-queue pbstr

消費者:

1. 從list-queue獲取任務:lpop list-queue 獲得字元串 pbstr

2. 將pbstr反向序列化為pb結構,執行業務邏輯

遇到問題:

消費者在步驟2中,獲取到的pbstr反序列化為pb結構失敗了!!!導致消費者後續的業務邏輯無法處理。

# 排查思路

1. 懷疑序列化問題,單獨從業務層面對pb結構進行序列pbstr,然後在將pbstr反向序列化為pb結構,沒有遇到問題,排除pb的問題。

2. 懷疑redis隊列除了問題。有一下幾個排查思路:

a. 系統多執行緒,比較難調試。

b. strace 對進程進行跟蹤,比較容易,本文採用這種方法。

工具:strace -p [pid] -s 1024 -o s.out

# 發現問題

圖1是pb轉為一個pbstr字元串:m_msgBody, 可見序列化後的長度是1029

圖1

圖2是執行的redis命令,這裡說一下redis命令的協議格式:

*[命令行參數個數]rn$[參數1長度]rn[參數1字元串]rn$[參數2長度]rn[參數2字元串]rn

例如:

RPUSH mylist Lippman

redis網路傳輸的命令傳如下:

"*3rn$5rnRPUSHrn$6rnmylistrn$7rnLippmanrn"

圖2

從圖2看出,我們的1029長度的消息,莫名其妙變為了97!!!

# 問題解決

結合程式碼層面的命令行拼接方式是基於字元串的fmt方式,懷疑是業務pb本身某些欄位含有, 導致序列化後的字元串被截斷了。

做個c預研字元串fmt遇到/0的實驗:實驗可以驗證,

字元串 s = 「abcdednxxxxxxxxxxxxx」

s.length=21

s.size=21。因為C++類中的字元串長度是記錄buffer使用的實際位元組長度。

strlen(s.c_str())=7。 因為C語言以作為字元串結束符。

字元串通過printf("%s", s.c_str) 結果只列印了 abcdedn。因為遇到被截斷了

## hiredis的兩種命令行形式

方式1:redisvFormatCommand

從如下程式碼可看出,字元串的結束判定是

“`

  1. int redisvFormatCommand(char **target, const char *format, va_list ap) {
  2. const char *c = format;
  3. ...
  4. while(*c != '') {
  5. if (*c != '%' || c[1] == '') {
  6. ...
  7. switch(c[1]) {
  8. case 's':
  9. arg = va_arg(ap,char*);
  10. size = strlen(arg); // strlen 以判定字元串結束,所以如果字元串亂碼,可能被判定為
  11. if (size > 0)
  12. newarg = sdscatlen(curarg,arg,size);
  13. break;
  14. case 'b':
  15. arg = va_arg(ap,char*);
  16. size = va_arg(ap,size_t);
  17. if (size > 0)
  18. newarg = sdscatlen(curarg,arg,size);
  19. break;
  20. case '%':
  21. newarg = sdscat(curarg,"%");
  22. break;
  23. ...
  24. }

“`

方式2 redisFormatSdsCommandArgv

從如下程式碼可看出,字元串的拼接使用的是strcat+字元串實際長度。

“`

  1. /* Format a command according to the Redis protocol using an sds string and
  2. * sdscatfmt for the processing of arguments. This function takes the
  3. * number of arguments, an array with arguments and an array with their
  4. * lengths. If the latter is set to NULL, strlen will be used to compute the
  5. * argument lengths.
  6. */
  7. int redisFormatSdsCommandArgv(sds *target, int argc, const char **argv,
  8. const size_t *argvlen)
  9. {
  10. sds cmd;
  11. unsigned long long totlen;
  12. int j;
  13. size_t len;
  14. /* Abort on a NULL target */
  15. if (target == NULL)
  16. return -1;
  17. /* Calculate our total size */
  18. totlen = 1+countDigits(argc)+2;
  19. for (j = 0; j < argc; j++) {
  20. len = argvlen ? argvlen[j] : strlen(argv[j]); // ------ 確定這個是否用的strlen
  21. totlen += bulklen(len);
  22. }
  23. /* Use an SDS string for command construction */
  24. cmd = sdsempty();
  25. if (cmd == NULL)
  26. return -1;
  27. /* We already know how much storage we need */
  28. cmd = sdsMakeRoomFor(cmd, totlen);
  29. if (cmd == NULL)
  30. return -1;
  31. /* Construct command */
  32. cmd = sdscatfmt(cmd, "*%irn", argc);
  33. for (j=0; j < argc; j++) {
  34. len = argvlen ? argvlen[j] : strlen(argv[j]); // --------確定這裡是不是錯用了strlen
  35. cmd = sdscatfmt(cmd, "$%Trn", len);
  36. cmd = sdscatlen(cmd, argv[j], len);
  37. cmd = sdscatlen(cmd, "rn", sizeof("rn")-1);
  38. }
  39. assert(sdslen(cmd)==totlen);
  40. *target = cmd;
  41. return totlen;
  42. }

“`

## 解決方法:

業務程式碼切換為第二種方式進行命令拼接,如下所示:

# 總結

1。 業務在做redis命令拼接的時候,盡量避免%s形式,除非能保證字元串不會被截斷。

2。業務程式碼抓包可以使用strace,方便快捷。