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

注:本文收錄於《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排查,有以下幾點心得:

  • 中間件升級要慎重,一定要在所有使用到該中間件的系統的負責人得知的的情況下再升級。
  • 第三方程序集的版本升級要慎重,一定要保證項目中的程序集版本一致,並進行迴歸測試。
  • 對於系統中所使用到的中間件,一定要持續關注新版本的發佈,瞭解其基本原理,這樣才能快速定位問題所在。
  • 加強不同團隊之間的溝通,思考問題儘量全面。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章