BUG—Nuget包版本不一致導致程式行為與預期不符

xiaoxiaotank發表於2022-01-06

注:本文收錄於《Bug集錦》,請點選此處檢視全文目錄

BUG起因

先介紹一下背景:

數週前的一個極其平常的下午,完成了本次迭代的開發工作,釋出到QA提測,然後開始摸魚。沒幾分鐘,測試就來找我“麻煩”了:生產者的訊息沒有傳送到RocketMQ的佇列中

“簡單,看下日誌就能定位原因了”,心想著,隨即開啟日誌,果然,報錯了,可是,這個錯誤訊息,嘖嘖嘖,看不懂啊:

NewLife.RocketMQ.Protocol.ResponseException: 1: the custom field <c> is null   
在 NewLife.RocketMQ.ClusterClient.Invoke(RequestCode request, Object body, Object extFields, Boolean ignoreError) 
在 NewLife.RocketMQ.Producer.Publish(Message msg, Int32 timeout)
......

c是個什麼玩意?我不記得有這東西啊!

BUG排查

問題沒有頭緒,只能先在本地環境測試一下了。先使用本地的生產者生產一條訊息,傳送到Dev環境的RocketMQ佇列中,結果訊息順利到達,無異常。然後,將本地RocketMQ配置修改為QA環境配置,並生產一條訊息,意外的是,訊息也順利到達了,並未出現異常。

這就奇怪了,既然無法快速解決,先啟用“重啟大法”,重新發個版本試試吧。發版後,果不其然,測試那邊說沒問題了。

可是,還沒摸魚一分鐘,測試又來“找麻煩”了,問題又出現了:生產者的訊息時而能夠傳送成功,時而傳送失敗。哎,最擔心的問題還是出現了,這種時而報錯時而正常的問題最難處理了。

RocketMQ版本升級了

不得不說,.NET對RocketMQ的支援確實不太好,NewLife.RocketMQ是為數不多的能用於生產環境的開源庫,去github碰碰運氣吧,萬一其他人也遇到這個問題了呢。

運氣不錯!其中一條Issue引起了我的注意——呼叫出錯,似乎版本不相容 rocketmq v4.9.1

哦?難不成是有人偷偷的把QA環境的RocketMQ升級了?通過RocketMQ控制檯看看:

靠!有一臺broker版本升級為v4.9.2了,QA環境的Topic是不久前建立的,其中部分佇列就分配到這臺機器上了。

找到問題原因了,而且也有人提交pr 修復了這個bug:

var smrh = new SendMessageRequestHeader
{
    ProducerGroup = Group,
    Topic = Topic,
    QueueId = mq.QueueId,
    SysFlag = 0,
    BornTimestamp = (Int64)ts.TotalMilliseconds,
    Flag = msg.Flag,
    Properties = msg.GetProperties(),
    ReconsumeTimes = 0,
    UnitMode = UnitMode,
    DefaultTopic = "TBW102"  // 增加了該行
};

現在,我只需要通過Nuget升級一下包的版本就好了,當時的最新版本是1.5.2021.1204:

重新發版,準備繼續摸魚!可是,還沒來得及高興,測試說問題依然存在,錯誤仍舊是之前的錯誤。這不就見鬼了嗎!

這一天,一直忙活到晚上十點多,問題終究還是沒有解決...

程式集版本不一致

有時候,腦子長時間思考同一個問題,往往會陷入一個封閉的環境,導致思維會越來越狹隘。

第二天,準備換個思路解決問題——將QA的dll複製到本地來除錯。不過,在準備複製dll的時候,發現了一個問題:NewLife.RocketMQ.dll的修改日期竟然還是6月份,也就是說實際上它還是老版本,不出問題才怪。

QA環境的RocketMQ是以叢集方式部署的,共有三臺Broker,其中一臺是高版本v4.9.2,其它兩臺是低版本v4.4.0。QA環境的Topic也是今天剛剛建立的,這三臺Broker上都有屬於該Topic的佇列。當生產者生產訊息時,可能會傳送到高版本的Broker,也可能會傳送到低版本的Broker上,所以,才導致了“時而成功,時而失敗”情況的發生。

可是,我明明已經升級版本了啊!

突然驚醒!其他Service的NewLife.RocketMQ版本並未同步升級, 導致出現了程式集版本不一致的問題!(PS:該專案是一個.net4.5的老專案,其他團隊的許多Service也在該解決方案中,遇到該問題時未及時通知其他團隊,這也讓我充分意識到團隊間溝通的重要性!)

檢視程式集的生成順序:右擊“解決方案”,選擇“專案生成順序”即可檢視。

知道問題原因了,現在有兩種解決方式:

  1. 升級專案中所有程式集的NewLife.RocketMQ版本,代價是需要測試走一遍迴歸。
  2. 要求運維團隊暫時不要升級RocketMQ版本,待研發、測試團隊準備好後再升級。

因為迭代急著上線,其他團隊也來不及迴歸測試,且RocketMQ的升級僅僅是為了適用一下新特性,並非強制要求,所以選擇了穩妥的方案2。至此,問題終於解決完畢!

其他

1. 當專案中出現Nuget包版本不一致的情況時,最終生成的程式集版本是哪個呢?

參考nuget使用經驗:複雜依賴關係下的包版本問題,並親測無問題,總結如下:

  • 引用層級不同時,NuGet 將選擇最接近入口程式集的版本
  • 引用層級相同時,NuGet 將選擇滿足所有版本要求的最低版本

建議一定要保證專案內引用包版本一致,儘量避免引用多版本的情況

2. 為什麼我本地生產者生產訊息到QA環境RocketMQ沒出問題?

經過詳細排查,我發現並非僅僅是版本不一致導致的問題。實際上,雖然專案中同時引用了多個版本的NewLife.RocketMQ,不過當專案編譯後,入口程式集的bin目錄下的NewLife.RocketMQ.dll實際上已經是最新的了,所以也就不會出問題。然而,由於QA的釋出指令碼有問題,導致QA伺服器上的NewLife.RocketMQ.dll並未更新到,仍在使用老版本。

3. the custom field <c> is null 到底是什麼意思?

事實上,這個c指的是生產者傳送訊息請求頭中的Default Topic,對於不瞭解RocketMQ通訊協議的我,是從Newlife.RocketMQ原始碼中得知的:

/// <summary>傳送訊息請求頭</summary>
public class SendMessageRequestHeader
{
    /// <summary>預設主題</summary>
    [XmlElement("c")]
    public String DefaultTopic { get; set; }
    
    // ...
}

至於Default Topic的空校驗,是由於在RocketMQ v4.9.1 中增加了decodeSendMessageHeaderV2方法,如果有興趣,可以點選此pr檢視完整提交記錄。

static SendMessageRequestHeaderV2 decodeSendMessageHeaderV2(RemotingCommand request)
        throws RemotingCommandException {
    SendMessageRequestHeaderV2 r = new SendMessageRequestHeaderV2();
    HashMap<String, String> fields = request.getExtFields();
    if (fields == null) {
        throw new RemotingCommandException("the ext fields is null");
    }

    // ...

    s = fields.get("c");
    checkNotNull(s, "the custom field <c> is null");
    r.setC(s);
    
    // ...
}

總結

經過這次bug排查,有以下幾點心得:

  • 中介軟體升級要慎重,一定要在所有使用到該中介軟體的系統的負責人得知的的情況下再升級。
  • 第三方程式集的版本升級要慎重,一定要保證專案中的程式集版本一致,並進行迴歸測試。
  • 對於系統中所使用到的中介軟體,一定要持續關注新版本的釋出,瞭解其基本原理,這樣才能快速定位問題所在。
  • 加強不同團隊之間的溝通,思考問題儘量全面。

相關文章