strict weak ordering導致公司級故障

高效能架構探索發表於2022-01-06

大家好,我是雨樂!

前段時間,某個同事找我傾訴,說是因為strict weak ordering導致程式coredump,給公司造成數百萬損失,最終評級故障為P0級,年終獎都有點不保了,聽完不禁一陣唏噓。

在之前的文章中,我們分析了std::sort的原始碼實現_,在資料量大時候,採用快排,分段遞迴排序。一旦分段後的資料量小於某個閾值,為了避免快排的遞迴呼叫引起的額外開銷,此時就採用插入排序。如果遞迴層次過深,還會採用堆排序。_

今天,藉助本文,我們分析下這次故障的原因,避免後面的開發過程中出現類似的問題。

背景

流量經過召回、過濾等一系列操作後,得到最終的廣告候選集,需要根據相應的策略,進行排序,最終返回首位最優廣告。

struct AdItem {
  std::string ad_id;
  int priority;
  int score;
};

現在有一個AdItem型別的verctor,要求對其排序,排序規則如下:

  • 按照priority升序排列

  • 如果priority一樣大,則按照score降序排列

需求還是比較簡單吧,當時線上程式碼如下:

void AdSort(std::vector<AdItem> &ad_items) {
	std::sort(ad_items.begin(), ad_items.end(), [](const AdItem &item1, const AdItem &item2) {
  	if (item1.priority < item2.priority) {
      return true;
    } else if (item1.priority > item2.priority) {
      return false;
    }

    return item1.score >= item2.score;
	} );
}

測試環境構造測試case,符合預期,上線。

恐怖的事情來了,上線不久後,程式直接coredump,然後自動重啟,接著有coredump,當時心情是這樣的。

定位

第一件事,登入線上伺服器,通過gdb檢視堆疊資訊

由於線上是release版的,看不了堆疊資訊,將其編譯成debug版,在某臺線上進行灰度,不出意料,仍然崩潰,檢視堆疊資訊。

通過堆疊資訊,這塊的崩潰恰好是在AdSort函式執行完,析構std::vector的時候發生,看來就是因為此次上線導致,於是程式碼回滾,重新分析原因。

原因

為了儘快定位原因,將這塊程式碼和線上的vector值獲取出來,在本地構建一個小範圍測試,基本程式碼如下:

oid AdSort(std::vector<AdItem> &ad_items) {
std::sort(ad_items.begin(), ad_items.end(), [](const AdItem &item1, const AdItem &item2) {
  if (item1.priority < item2.priority) {
      return true;
    } else if (item1.priority > item2.priority) {
      return false;
    }

    return item1.score >= item2.score;
} );
}

int main() {
  std::vector<AdItem> v;
  /*
  給v進行賦值操作
  */

  AdSort(v);

  return 0;
}

執行下面命令進行編譯,並執行:

g++ -g test.cc -o test
./test

執行報錯,如下:

通過gdb檢視堆疊資訊

線上問題復現,基本能夠確認coredump原因就是因為AdSort導致,但是在AdSort中,就一個簡單的排序,sort不可能出現崩潰,唯一的原因,就是寫的lambda函式有問題。

利用_逐步定位排除法_,重新修改lambda函式,執行,執行正常。

void AdSort(std::vector<AdItem> &ad_items) {
	std::sort(ad_items.begin(), ad_items.end(), [](const AdItem &item1, const AdItem &item2) {
  	if (item1.priority < item2.priority) {
      return true;
    } else if (item1.priority > item2.priority) {
      return false;
    }
    
    if (item1.score > item2.score) {
      return true;
    }

    return false;
	} );
}

執行正常,那麼就是因為lambda比較函式有問題,那麼為什麼這樣就沒問題了呢?

想起之前在<>中看到一句話_第21條:總是讓比較函式在等值情況下返回false。應該就是沒有遵循這個原則,才導致的coredump。

那麼為什麼要遵循這個原則呢?開啟Google,輸入std::sort coredump,看到了一句話

Having a non-circular relationship is called non-transitivity for the < operator. It’s not too hard to realise that if your relationships are circular then you won’t be getting reasonable results. In fact there is a very strict set of rules that a data type and its comparators must abide by in order to get correct results from C++ STL algorithms, that is strict weak ordering.

從上面的意思看,在STL中,對於sort函式中的排序演算法,需要遵循嚴格弱序(strict weak ordering)的原則。

嚴格弱序

什麼是嚴格弱序呢?摘抄下來自wikipedia的定義:

A strict weak ordering is a binary relation < on a set S that is a strict partial order (a transitive relation that is irreflexive, or equivalently,[5] that is asymmetric) in which the relation "neither a < b nor b < a" is transitive.[1] Therefore, a strict weak ordering has the following properties:

  • For all x in S, it is not the case that x < x (irreflexivity).
  • For all x, y in S, if x < y then it is not the case that y < x (asymmetry).
  • For all x, y, z in S, if x < y and y < z then x < z (transitivity).
  • For all x, y, z in S, if x is incomparable with y (neither x < y nor y < x hold), and y is incomparable with z, then x is incomparable with z (transitivity of incomparability).

上面概念,總結下就是,存在兩個變數x和y:

  • x > y 等同於 y < x
  • x == y 等同於 !(x < y) && !(x > y)

要想嚴格弱序,就需要遵循如下規則:

  • 對於所有的x:x < x永遠不能為true,每個變數值必須等於其本身
  • 如果x < y,那麼y < x就不能為true
  • 如果x < y 並且y < z,那麼x < z,也就是說有序性必須可傳遞性
  • 如果x == y並且y == z,那麼x == z,也就是說值相同也必須具有可傳遞性

那麼,為什麼不遵循嚴格弱序的規則,就會導致coredump呢?

對於std::sort(),當容器裡面元素的個數大於_S_threshold的列舉常量值時,會使用快速排序

我們先看下sort的函式呼叫鏈(去掉了不會導致coredump的部分):

sort
-> __introsort_loop
--> __unguarded_partition

我們看下__unguarded_partition函式的定義:

template<typename _RandomAccessIterator, typename _Tp, typename _Compare>
     _RandomAccessIterator
     __unguarded_partition(_RandomAccessIterator __first,
               _RandomAccessIterator __last,
               _Tp __pivot, _Compare __comp)
     {
       while (true)
     {
       while (__comp(*__first, __pivot))
         ++__first;
       --__last;
       while (__comp(__pivot, *__last))
         --__last;
       if (!(__first < __last))
         return __first;
       std::iter_swap(__first, __last);
       ++__first;
     }
     }

在上面程式碼中,有下面一段:

while (__comp(*__first, __pivot))
         ++__first;

其中,first為迭代器,pivot為中間值,comp為傳入的比較函式。

如果傳入的vector中,後面的元素完全相等,那麼comp比較函式一直是true,那麼後面++__first,最終就會使得迭代器失效,從而導致coredump。

好了,截止到此,此次線上故障原因分析完畢。

結語

這個故障,說真的,無話可說,只能怪自己學藝不精,心服口服,也算是給自己一個教訓,後面test case儘可能跟線上一致,把問題儘早暴露在測試階段。

這次把這個故障原因分享出來,希望大家在後面的開發過程中,能避免採坑。

好了,本期的文章就到這,我們下期見。

作者:高效能架構探索
掃描下方二維碼關注公眾號【高效能架構探索】,回覆【pdf】免費獲取計算機必備經典書籍

相關文章