从一个 SAP CRM 软件实际的故障处理出发,谈谈企业管理软件领域内那些很难稳定重现故障的处理技巧
目錄
企業管理軟件領域內棘手故障的一些表現形式
1. 需要復雜的流程才能重現
2. 故障橫跨企業管理軟件的多個模塊
3. 故障只能在客戶生產系統重現
4. 故障只能在后臺作業模式下重現,在 online 模式運行時一切正常
5. 故障只能在軟件正常運行模式時才能重現,單步調試時,軟件工作一切正常
一個實際故障排查過程的案例分享
1. 試圖找到穩定重現故障的辦法
2. 縮小可能引起故障的代碼排查范圍
3. 利用調試器鎖定問題
故障排查過程總結
筆者從2007年大學畢業加入 SAP 成都研究院至今,一直從事企業管理軟件領域的開發工作。
企業管理軟件面向的是企業級用戶,如果軟件出現故障(bug),在某些極端情況下,可能會讓企業蒙受巨大的經濟損失,故而對軟件開發人員在編程規范,軟件測試和軟件交付之前的驗證等各方面都提出了更高的要求。同時,由于企業管理軟件自身高度的復雜性,有些故障很難重現或者只能在運行了客戶特定業務流程的生產系統上才能重現。這些都給企業管理軟件分析和故障處理帶來了巨大的挑戰。
本文從筆者處理過的一個實際軟件故障出發,談談自己對企業管理軟件里一些棘手故障的處理體會。
在筆者看來,這些棘手故障,可以分為以下幾類。
企業管理軟件領域內棘手故障的一些表現形式
筆者在 SAP成都研究院處理過很多曾讓我頭痛的軟件故障,它們具有下列一項或幾項特征。
1. 需要復雜的流程才能重現
例如我處理過的一個客戶發票(Customer Invoice)相關的故障。這個故障只有在每次 release 發票時才能重現。為了 release 發票,我們必須先創建一個銷售訂單(Sales Order),基于該訂單創建 Customer Demand,然后創建撿貨任務(Pick Task),生成交貨單(Delivery Note),最后才能生成一張新的客戶發票。
這些復雜的流程往往也需要系統事先維護好對應的主數據(Master Data)和事務數據(Transaction Data)才能順利執行。復雜的業務流程增添了故障重現的難度。
2. 故障橫跨企業管理軟件的多個模塊
由于企業管理軟件自身的復雜度,終端用戶眼中看到的貌似簡單的一個故障,背后可能橫跨了軟件實現的多個模塊。
以上述形式1描述的故障為例,假設軟件幫助文檔上描述的支持功能為:客戶在銷售訂單界面上添加了一個新的自定義字段并維護了對應值,該值能夠從銷售訂單,經撿貨任務,交貨單,最后傳遞到客戶發票上。我們稱這種字段值從多個文檔間的傳遞稱為 data flow.?
那么如果客戶在發票頁面上,看到這個字段的值為空,客戶可能認為是發票模塊出了故障。然而,在 data flow 的每個節點對應的模塊處理,可能都是造成該故障的罪魁禍首。銷售訂單和客戶發票屬于 CRM 模塊,而撿貨任務和發貨單則歸屬 SCM 的范疇。
在實際開發工作中,這意味著分析該故障往往需要跨團隊間協作,因為 CRM 和 SCM 模塊往往分屬不同的開發團隊負責。
3. 故障只能在客戶生產系統重現
在企業管理軟件交付之前,必定在內部開發,測試和驗證系統(validation system)進行過不同層次的測試。即便如此,由于種種客觀原因,比如當應用運行在客戶生產系統上,基于某些只有該客戶才會用到的特定業務流程的配置時,故障才會暴露,而這些配置并沒有被企業管理軟件供應商的內部系統測試所涵蓋到。
這類故障因為只能在客戶生產系統重現,在分析和定位問題時更加困難重重,尤其當重現步驟會在客戶生產系統進行寫操作時,通常只能聯系客戶相關人員,采用遠程桌面+電話會議的方式,讓客戶相關人員進行操作,然后軟件供應商的支持人員在線調試。
4. 故障只能在后臺作業模式下重現,在 online 模式運行時一切正常
在企業管理軟件領域特別是 ERP 領域,后臺作業常常被用來執行一些費時的批處理工作,比如訂單批量處理,報表數據分析和聚合匯總等等。后臺作業模式不同于掛接了用戶界面的 online 模式,給單步調試也帶來了困難。
5. 故障只能在軟件正常運行模式時才能重現,單步調試時,軟件工作一切正常
當故障出現這種特征時,實際給支持人員傳遞了一個信號:該故障可能與程序特定的執行時序相關。因為程序正常運行,與處于單步調試模式下運行,執行時序顯然不同,比如在調試器單步調試時,可能會破壞多線程程序正常的執行時序。
因為缺少了調試器這一強有力的武器,分析該類故障,需要支持人員具有更強的理論分析能力和問題抽象能力。
由于篇幅限制,本文僅舉一個實際例子,對上述第五類故障的分析處理流程做一個分享。
一個實際故障排查過程的案例分享
筆者曾經負責 SAP CRM IBASE(Installed Base) 模塊。IBase 是一個抽象模型,用于描述已在客戶位置安裝的資源對象,例如設備、機器,服務或軟件。IBase 模型以樹形結構,描述了這些對象的層級結構和它們的各個組件,是服務模塊的參考基礎。
有一天,我收到一個故障報告,另一個團隊的同事,使用我所在團隊負責的 IBASE API,在同一會話過程內創建 IBASE 組件,修改,隨后刪除,然后保存,會遇到運行時錯誤(Runtime Error).
在故障描述里提到的運行時錯誤的截圖如上圖所示。
這位同事發現,這個錯誤只能在后臺作業模式下重現,并且不一定每次都能夠重現。該故障也無法在單步調試模式下重現。
并不總是能夠重現 != 不能重現。
1. 試圖找到穩定重現故障的辦法
為了分析這個問題,我得先找到能夠穩定重現的辦法。因為該故障對單步調試大法免疫,我只能另想他法。
逐字逐句死扣故障報告里的描述,發生故障之前的操作流程為:
(1) 創建 IBASE
(2) 修改 IBASE
(3) 刪除 IBASE
(4) 保存事務。
出現運行時錯誤。
因為我就是 IBASE 模塊的負責人,所以我三下五除二就寫好了一個不到 200 行的程序,在程序里依次調用 IBASE 的創建,修改和刪除 API,再保存事務。
程序源代碼如下:
REPORT zibase_create_delete.PARAMETERS: txt TYPE char40 OBLIGATORY DEFAULT 'description test',eid TYPE char30 OBLIGATORY DEFAULT 'PROGRAM',oid TYPE comm_product-product_id OBLIGATORY DEFAULT 'CHILDOBJ8',fam TYPE comm_product-object_family OBLIGATORY DEFAULT '0401',cat TYPE COMT_CATEGORY_ID OBLIGATORY DEFAULT 'OBJ_0401'.DATA: lt_param ?TYPE crmt_name_value_pair_tab,ls_param ?TYPE crmt_name_value_pair,lr_core ? TYPE REF TO cl_crm_bol_core,ls_object TYPE comm_product,lr_root ? TYPE REF TO if_bol_entity_col,entity ? ?TYPE REF TO cl_crm_bol_entity.CHECK zcl_object_generator=>create_object( iv_id = oid iv_family = fam iv_catid = cat ) = abap_true.ls_param-name ?= cl_crm_ibase_il_constant=>createparam. ls_param-value = '01'. APPEND ls_param TO lt_param.lr_core = cl_crm_bol_core=>get_instance( ). lr_core->load_component_set('IBASE_ONLY').CALL METHOD lr_core->root_createEXPORTINGiv_object_name ?= cl_crm_ibase_il_constant=>root_objectiv_create_param = lt_paramiv_number ? ? ? = 1RECEIVINGrv_result ? ? ? = lr_root.CHECK lr_root IS BOUND. entity ?= lr_root->get_current( ).CHECK entity IS BOUND. IF entity->lock( ) = abap_true.entity->switch_to_change_mode( ). ENDIF.entity->set_property_as_string( iv_attr_name = 'DESCR' iv_value = CONV #( txt ) ). entity->set_property_as_string( iv_attr_name = 'EXTID' iv_value = CONV #( eid ) ). "entity->set_property_as_string( iv_attr_name = 'IBTYP' iv_value = '01' ). lr_core->modify( ). DATA(lv_ibase_id) = entity->get_property_as_string( 'IBASE' ).DATA(component) = entity->create_related_entity( 'FirstLevelComponent' ).CHECK component IS NOT INITIAL.DATA(obj_comp) = component->create_related_entity( 'IBCompObj').CHECK obj_comp IS NOT INITIAL.obj_comp->set_property_as_string( iv_attr_name = 'OBJECT_ID' iv_value = CONV #( oid ) ).SELECT SINGLE * INTO ls_object FROM comm_product WHERE product_id = oid. ASSERT sy-subrc = 0.obj_comp->set_property_as_string( iv_attr_name = 'OBJECT_GUID' iv_value = CONV #( ls_object-product_guid ) ). obj_comp->set_property_as_string( iv_attr_name = 'OBJECT_FAMILY' iv_value = CONV #( ls_object-product_guid ) ). lr_core->modify( ).DATA(lo_message_container) = entity->get_message_container( ). CALL METHOD lo_message_container->get_messagesEXPORTINGiv_message_type = if_genil_message_container=>mt_allIMPORTINGet_messages ? ? = DATA(lt_msg1). LOOP AT lt_msg1 ASSIGNING FIELD-SYMBOL(<msg1>).WRITE:/ <msg1>-message COLOR COL_NEGATIVE. ENDLOOP.CHECK lt_msg1 IS INITIAL.DATA: ls_header ? ? ?TYPE ibap_head1,lt_struc_tab ? TYPE ibap_struc1_tab,ls_comp TYPE IBAP_DAT1. "delete component"ls_header-ibase = lv_ibase_id. CALL FUNCTION 'CRM_IBASE_GET_DETAIL'EXPORTINGi_ibase_head ? ? ?= ls_headerIMPORTINGe_struc_ibase_tab = lt_struc_tabEXCEPTIONSnot_specified ? ? = 1doesnt_exist ? ? ?= 2no_authority ? ? ?= 3.CHECK sy-subrc = 0.READ TABLE lt_struc_tab ASSIGNING FIELD-SYMBOL(<line>) INDEX 1. ls_comp-instance = <line>-instance.CALL FUNCTION 'CRM_IBASE_COMP_DELETE'EXPORTINGi_comp = ls_compEXCEPTIONSDATA_NOT_CONSISTENT = 1IBASE_LOCKED = 2NOT_SUCCESFUL = 3NO_AUTHORITY = 4.CASE sy-subrc.WHEN 1.WRITE: / 'data not consistent' COLOR COL_NEGATIVE.WHEN 2.WRITE: / 'cannot delete locked component' COLOR COL_NEGATIVE.WHEN 3.WRITE: / 'deletion not successful' COLOR COL_NEGATIVE.WHEN 4.WRITE: / 'no deletion authorization' COLOR COL_NEGATIVE.ENDCASE.DATA(lo_transaction) = lr_core->get_transaction( ). DATA(lv_changed) = lo_transaction->check_save_needed( ).CHECK lv_changed EQ abap_true.DATA(lv_success) = lo_transaction->save( ).DATA(lo_glb_msg_cont) = lr_core->get_global_message_cont( ). CALL METHOD lo_glb_msg_cont->if_genil_message_container~get_messagesEXPORTINGiv_message_type = if_genil_message_container=>mt_allIMPORTINGet_messages ? ? = DATA(lt_msg). LOOP AT lt_msg ASSIGNING FIELD-SYMBOL(<msg>).WRITE:/ <msg>-message. ENDLOOP.IF lv_success = abap_true.lo_transaction->commit( ).WRITE:/ 'IBASE Created Successfully: ', lv_ibase_id COLOR COL_NEGATIVE. ELSE.lo_transaction->rollback( ). ENDIF.執行這個報表,遇到了期望中的運行時錯誤。這是一個好兆頭,因為我現在找到了穩定重現問題的辦法。下一步,我需要縮小問題的范圍,找出我這 200 行代碼里,到底哪一行代碼的執行,引起了運行時錯誤。
筆者喜歡稱自己開發的這種專門用于分析故障,重現錯誤的程序,為“腳手架程序”或者“故障觸發器”。
2. 縮小可能引起故障的代碼排查范圍
因為這 200 行代碼是我自己編寫的,所以我可以任意修改。首先把所有代碼全部注釋掉,只留下 IBASE 創建 API 的調用。執行程序,一切正常。
再解除 IBASE 修改 API 調用代碼的注釋,讓其參與到程序執行中,一切正常。
再反注釋 IBASE 刪除 API 調用代碼,執行程序,出現了運行時錯誤!
由此說明,這個運行時錯誤和 IBASE 刪除的場景相關。
回到故障提交報告里的運行時錯誤截圖:第 103 行拋出了一個類型為 X 的錯誤,因為調用函數 CRM_IBASE_COMP_GET_DETAIL, 并沒有讀取到通過輸入參數 i_date 和 i_time 指定的時間戳對應的 IBASE 數據,因此程序決定通過拋出錯誤的方式來終止執行。
通過運行時錯誤的上下文調用棧,我找到了 CRM_IBASE_COMP_GET_DETAIL API 沒有返回任何 IBASE 數據的原因:下圖第 53 行高亮代碼的 CHECK 語句,檢查當前傳入的時間戳(默認為 IBASE 創建時的時間戳)是否小于待讀取 IBASE 抬頭的 valto(即 valid to,指 IBASE 有效截止日期的時間戳)字段。如果小于,則順序執行 CHECK 下一條即 54 行。如果大于或等于,則退出數據讀取邏輯所在的循環體。
?
在后臺作業運行模式,以及我的腳手架程序執行時,第 53 行時間戳判斷條件沒有滿足,因此退出了循環,導致 CRM_IBASE_COMP_GET_DETAIL 讀取失敗,所以引發了故障。
要想滿足 53 行的判斷條件,只有兩種可能:
- 當前時間戳 > IBASE valto 字段值
- 當前時間戳 = IBASE valto 字段值
需要強調的是,ABAP 編程語言里的時間戳字段,精確到秒,比如 20211024102424 代表 2021年10月24日10點24分24秒。
3. 利用調試器鎖定問題
雖然我的腳手架應用在單步調試模式下也無法重現故障,但是直接執行可以重現。因此,執行執行腳手架應用,在運行時故障頁面點擊工具欄的 Debugger 按鈕,能彈出調試器,查看應用程序拋出運行時錯誤的各種信息:
?
這一回,在調試器里,所有的謎題都揭曉了:當前時間戳 = IBASE valto 字段值,因此導致 API CRM_IBASE_COMP_GET_DETAIL 讀取失敗,拋出運行時錯誤。
- 在調用 IBASE 創建 API 時,會把待創建的 IBASE 抬頭的 valfr 字段,賦以系統的當前時間戳。
- 在調用 IBASE 刪除 API 時,會把該待刪除 IBASE 抬頭的 valto 字段,賦以系統的當前時間戳。
為什么在單步調試模式下,無法重現這個錯誤呢?我們來看一張簡單的時序圖。
橫軸代表時間戳。t3 代表代碼 53 行判斷語句里的 <ibinadm>-valto 字段值, t1 代表代碼 53 行判斷語句里的 lv_timestamp 字段值。
在單步調試模式下,假設我們從 IBASE 創建 API 開始依次單步執行,則由于按鍵手速原因,t3 必定大于 t1.
而在后臺作業模式以及腳手架程序正常運行情況下,如果 IBASE 創建,修改和刪除的 API 執行得足夠快速,能夠在一秒鐘之內完成,則 t3 與 t1 之差小于 1 秒,故 CHECK 語句執行失敗,直接返回。
換言之,這個故障提交時,CRM IBASE API 的開發人員,并沒有考慮到 IBASE 的創建和刪除會在`同一秒之內`完成的場景。畢竟正常情況下,客戶不可能在 1 秒鐘之內,在 UI 完成 IBASE 先創建再刪除的操作。這種場景只可能在客戶使用 IBASE API 進行一些二次開發場景下才有可能出現。
當然,最后這個問題,也絕非僅僅是把 53 行 CHECK 語句的 < 符號,改成小于等于操作那么簡單。我們仔細評估了改動可能帶來的其他副作用,并和提交該故障的團隊開發人員進行了討論,最后采取了其他的方式來避免這個故障的發生。
故障排查過程總結
回到這個故障分析過程本身,最開始接到故障時,因為單步調試無法重現,因此筆者很是一籌莫展了一陣,后來想到編寫腳手架程序來穩定重現該故障,這一步是問題分析的突破口。
有了腳手架程序之后,先注釋掉所有的 API 調用,再逐步開放 IBASE 創建,修改和刪除的代碼,最終把問題范圍縮小到 IBASE 刪除過程。
通過腳手架應用的直接執行觸發的運行時錯誤,利用調試器查看程序拋出錯誤時的變量值,將問題鎖定到時間戳的處理邏輯上,進而找出根源。
這一分析步驟有點像上世紀末本世紀初電腦 DIY 發燒友們遇到組裝機無法啟動時的排查措施。當組裝機無法啟動時,只保留電源,主板和 CPU,嘗試啟動,如果成功,再逐一添加顯卡,硬盤等其他設備。當新添加的設備導致系統重新回到無法啟動狀態,說明該設備有問題。當時發燒友們把這種方式稱為“最小系統法”。
而整個分析過程的重中之重,就是把故障報告中無法穩定重現故障的后臺作業里執行的內容,抽象成一個不到 200 行的腳手架程序。
《編程珠璣》第五章曾經分享過一個關于故障調試的有趣故事:IBM 研究中心一位程序員,新安裝了一臺工作站,發現一個故障:他只能采取坐著的姿勢登錄系統;一旦站起來,就無法登陸系統了。大家知道這個故障最后怎么定位的嗎?去讀讀原書吧!
希望本文能給大家在企業管理軟件領域內的故障排除方法帶來一些啟發,感謝閱讀。
?
總結
以上是生活随笔為你收集整理的从一个 SAP CRM 软件实际的故障处理出发,谈谈企业管理软件领域内那些很难稳定重现故障的处理技巧的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 立夏吃一蛋力气长一万!夏天模式开启:你那
- 下一篇: SAP Spartacus PageLa