如何使用Java可觀察性進行有效編碼
譯文譯者 | 李睿
審校 | 重樓
多年來,在試圖使可觀察性計劃取得成功的過程中,許多企業犯了一些常見的錯誤。然而,這些企業的失誤中最關鍵和最根本的問題是對技術和工具本身的不可抗拒的迷戀。
這應該讓人感到不意外。許多“讓我們添加可觀察性平臺X”的項目在開始時通常都是大張旗鼓,但其方向感非常模糊,并且成功的標準也非?;靵y。對于有效的可觀察性可以做些什么來幫助開發人員更好地工作,許多供應商和預言者對于這一愿景的宣傳卻令人懷疑地缺失了。開發人員需要問問自己:有多少次發現自己會把目光從集成開發環境(IDE)中的代碼上移開,發現可以從執行數據中學到什么?
不要誤解,開發人員要相信可觀察性在軟件開發中可以發揮重要作用。OpenTelemetry發揮了巨大的作用,可以清楚地看到它如何幫助開發人員編寫更好的代碼,引入新的范例,并加快開發周期。它可以啟發開發人員提出他們甚至還沒有考慮到的問題。然而,無論人們在網上看到什么,其重點似乎仍然是可觀察性本身,如何啟用它,以及如何開始。盡管有著炫酷圖形的儀表板非常棒,但許多開發團隊都不知道該從哪里入手。
本文將討論一個更有趣的話題:對于使用可觀察性的開發者來說,成功是什么樣子的?開發團隊如何期望使用豐富的代碼運行時數據更好地編碼和發布?更重要的是,現在有哪些可觀察性可以告訴開發人員關于代碼的事情,以及它如何幫助開發人員改進?可以通過具體的代碼示例來了解如何利用可觀察性作為編碼實踐。
超越監控:縮短開發過程中的反饋循環
可觀察性最大的希望在于提供真實和客觀的反饋,不受單元測試的一些偏差和偏見的影響。想象一下,當開發人員還在處理代碼更改時,就會收到有關任何回歸或問題的警報。或者,始終了解代碼的哪些部分在生產中實際使用,并根據集成測試結果輕松識別需要注意的薄弱環節。
這可能是開發人員可觀察性的真正潛力,而不是作為“監視”解決方案的傳統角色。監視器和警報至關重要,但不幸的是,它們的重點始終是報告已經發生的問題。也許是因為該技術主要由DevOps/SRE/IT團隊使用,他們主要關心生產的穩定性。
本文作者表示,有一次在發布一個產品的階段,他和團隊中的其他開發人員感覺他們的作用更像是消防隊而不是開發團隊,他開玩笑地將匆忙修復漏洞稱為BDD——不是行為驅動設計,而是漏洞驅動開發。然而,這種描述并非完全不準確。開發人員沒有積極主動地改進代碼,而是極其被動地追逐一個又一個問題,這很快就變得不可持續。
例舉更實際的例子
為了說明開發人員如何利用可觀察性來改進的開發周期,在此例舉一個現實場景的更實際的例子:團隊中的高級開發人員Bob被要求向Spring PetClinic示例添加一些功能。跟蹤寵物的疫苗接種記錄似乎非常重要,Bob被要求與外部數據源集成以實現這一目標。對于最初的最小可行產品(MVP),Bob為此創建一個功能分支,并繼續實現一些新的功能。
在閱讀了許多關于如何從Java應用程序中收集可觀察性數據的教程之后,Bob在后臺運行了幾個OSS和免費工具來幫助他完成任務。這篇文章并不會詳細介紹如何設置整個堆棧(因為它也有廣泛的文檔記錄)。但是,可以依docker_compose文件的形式找到整個堆棧。
Bob的基本可觀測性堆棧:
- 用于跟蹤的OpenTelemetry;他還在本地運行了一個OTEL收集器容器,將數據路由到各種工具。
- 用于顯示跟蹤的Jaeger。
- 用于采集指標的Micrometer。
- 用于保存矩陣的Prometheus和用于可視化它們的開源版本Grafana。
需要注意的是,要開始使用OTEL收集代碼數據,Bob不需要進行任何代碼更改。在本地運行時,他可以安全地使用OTEL代理。在他的例子中,他只是在IDE的運行配置中引用代理,以便在本地運行/調試時可以引用代理。他還添加了一個docker-compose.override文件,用于使用docker/Podman啟動應用程序(這也不需要更改源docker-compose文件)。
在一切就緒并運行之后,Bob創建了一個新的功能分支,并開始開發新功能。
疫苗API外觀組件
非常幸運的是,有人已經編寫了一個Spring組件來與另一個模塊的模擬API進行通信。Bob的工作很簡單:將組件注入PetController,并在添加寵物時使用它檢索數據。該組件非常簡單,使用OKHttp庫實現一個基本的REST調用,以獲取JSON形式的數據。
Java
@WithSpan
public VaccinnationRecord[] AllVaccines() throws JSONException, IOException {
var vaccineListString = MakeHttpCall(VACCINES_RECORDS_URL);
JSONArray jArr = new JSONArray(vaccineListString);
var vaccinnationRecords =
new ArrayList<VaccinnationRecord>();
for (int i = 0; i < jArr.length(); i++) {
VaccinnationRecord record = parseVaccinationRecord(jArr.getJSONObject(i));
vaccinnationRecords.add(record);
}
return vaccinnationRecords.toArray(VaccinnationRecord[]::new);
}
@WithSpan
public VaccinnationRecord VaccineRecord(int vaccinationRecordId) throws JSONException, IOException {
var idUrl = VACCINES_RECORDS_URL + "/" + vaccinationRecordId;
var vaccineListString = MakeHttpCall(idUrl);
JSONObject vaccineJson = new JSONObject(vaccineListString);
return parseVaccinationRecord(vaccineJson);
}
更新寵物模型
接下來,為了保存疫苗接種數據,而不是每次都檢索它,必須更新模型和數據庫結構。這涉及到很多樣板文件,但為了保存每只寵物的疫苗接種信息,這是必要的措施。Bob適時地添加了一個新表,對類中的關系進行建模,還更新了DDL腳本。
Java
@Entity
@Table(name = "pet_vaccines")
public class PetVaccine extends BaseEntity {
@Column(name = "vaccine_date")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate date;
/**
* Creates a new instance of Visit for the current date
*/
public PetVaccine() {
}
public LocalDate getDate() {
return this.date;
}
public void setDate(LocalDate date) {
this.date = date;
}
}
添加用于檢索和更新新的寵物接種日期字段的域服務
遵循最佳實踐,Bob創建了一個簡單的域服務,該服務將被注入PetController中。新服務編排域邏輯,以便從外部API檢索新寵物的疫苗記錄,并用最新日期更新模型。不幸的是,這也是Bob犯了幾個錯誤的地方,其中一些錯誤與facade抽象的泄漏有關,這掩蓋了成本昂貴的HTTP調用。Bob也沒有注意到很多邏輯是多余的。
Java
@Component
public class PetVaccinationStatusService {
@Autowired
private PetVaccinationService adapter;
public void UpdateVaccinationStatus(Pet[] pets){
for (Pet pet: pets){
try {
var vaccinationRecords = this.adapter.AllVaccines();
for (VaccinnationRecord record : vaccinationRecords){
var recordInfo = this.adapter.VaccineRecord(record.recordId());
if (recordInfo.petId()==pet.getId()){
PetVaccine petVaccine = new PetVaccine();
petVaccine.setDate(recordInfo.vaccineDate());
pet.addVaccine(petVaccine);
}
}
} catch (JSONException |IOException e) {
//Fail silently
Span.current().recordException(e);
}
}
}
}
更新視圖模板
最后,Bob添加了一個新字段,用于指示寵物疫苗是否過期。
HTML
..
<table class="table table-striped" th:object="${owner}">
<tr>
<th>Name</th>
<td><b th:text="*{firstName + ' ' + lastName}"></b></td>
</tr>
<tr>
<th>Address</th>
<td th:text="*{address}"></td>
</tr>
<tr>
<th>City</th>
<td th:text="*{city}"></td>
</tr>
<tr>
<th>Telephone</th>
<td th:text="*{telephone}"></td>
</tr>
<tr>
<th>Needs Vaccine</th>
<td th:text="*{isVaccineExpired()}"></td>
</tr>
</table>
...
就是這樣! 更改已準備就緒。Bob甚至編寫了一些測試,他對快速的進展感到滿意,并對本地測試時沒有發生意外的代碼充滿信心,他轉向收集的運行時數據,看看它能揭示他的更改。他決定擴展“完成”的定義,并花費額外的精力來檢查與他的更改相關的數據。
使用可觀察性
首先,參考某種基準是很重要的。有兩個API操作受到了更改的影響,Bob希望了解更改之前和之后它們是如何執行的。作為可觀察性設置的一部分,Bob還配置了Micrometer和Actuator,以提供有關API的有用指標。在這個案例中,這些可以通過執行器URL直接訪問http://localhost:8082/actuator/metrics。然而,為了更好的可視化和更多的繪圖選項,Bob將在他的堆棧中使用本地運行的Prometheus和Grafana OSS。
查看一些常見的Grafana儀表板,令人驚訝的是,沒有用于跟蹤API響應時間的默認圖表。也許是因為大多數儀表板都與Ops相關,關注CPU/內存和堆棧大小,而不是日常開發人員的見解。幸運的是,使用Actuator度量很容易配置這樣的儀表板??梢允褂靡韵虏樵儎摻ㄒ粋€以創建新寵物的API為重點的圖表:
HTTP
1 http_server_requests_seconds{uri="/owners/{ownerId}/pets/new", quantile="0.5", method="POST", outcome="REDIRECTION"} != 0
2
然后可以在代碼更改之前和之后檢查圖。
代碼更改之前:
代碼更改之后:
毫無疑問,這些更改導致了嚴重的性能問題。可以通過查看指標立即發現問題,但跟蹤可以揭示更多關于根本原因和潛在問題的信息?,F在是調用Jaeger的時候了,這是可觀察性堆棧的另一個組件。Jaeger習慣于可視化捕獲的跟蹤,并為Bob提供了一個機會,讓他在忙于添加更多邏輯和功能的同時,調查他的代碼在做什么:
因此,在不添加單個斷點的情況下,已經可以在這個請求中了解到許多關于這一代碼的內容。到目前為止,Bob還完全沒有注意到這些信息。雖然他在嘗試新請求時確實注意到了一些延遲,但他并沒有太在意。也許外部API太慢了?既然他已經訪問了跟蹤,他就可以重新審視引入的代碼了。
精選豐富的語句
第一個突出的問題是作為findById存儲庫方法的一部分觸發的許多SQL語句。Spring Data會自動檢測到這一點,并提供一些關于正在發生的事情的場景。更仔細地檢查查詢會發現一個熟悉的Hibernate陷阱:
看起來訪問關系是通過通常稱為N+1選擇的方式為每個寵物獲取的。有趣的是,這個問題似乎是PetClinic應用程序特有的,而且似乎早于Bob的更改。實際上,雖然這會導致一些放緩,但它并不像其他一些問題那樣重要,這一點在Bob進一步檢查跟蹤時變得明顯。
HTTP請求聊天
性能回歸的真正原因似乎與Bob的誤解有關,可能是由于VaccineServiceFacade方法的命名不明確。他似乎不太清楚,每次調用VaccineRecord函數時在后臺執行API調用。使用更好的命名約定可以緩解這種抽象漏洞,強調這實際上是長時間同步操作的執行。
隱藏的錯誤
HTTP請求中還發生了其他事情。當向下滾動請求列表時,Bob注意到其中一些請求以錯誤結束,然后在嘗試序列化不存在的響應時出現異常?;贖TTP錯誤代碼的根本原因與速率限制或節流或外部API有關。這個問題可以通過優化調用的數量來暫時解決,但是隨著越來越多的用戶開始同時使用這個組件,這個問題可能會重新出現。此外,這段代碼中的異常處理肯定是錯誤的,也許需要一種重試機制。
就在Bob開始糾正通過檢查可觀測性工件發現的許多問題之前,他決定快速查看他修改的另一個API。在這種情況下,性能似乎沒有顯著下降,但是檢查跟蹤仍然發現至少有一個問題需要修復。
將會出現哪些問題?
數據中還可以識別出其他問題,但是回顧一下場景,考慮一下如果Bob在合并更改之前沒有對其進行分析,會發生什么情況:代碼最終被部署。有些問題在CR或后期測試階段被發現,導致更多的更改、額外的延遲和痛苦的合并,因為在此期間會出現更多的更改。其他問題也會轉移到生產中,導致進一步的問題:延遲發布、匆忙修復、增加團隊的焦慮和沮喪等等。毫無疑問,可以發現縮短反饋循環有很多好處。
勝利了嗎?不完全是
在這個有點幼稚的例子中,能夠演示如何簡單地打開OTEL并通過一些OSS工具流式傳輸數據,有可能為Bob和其他開發人員提供額外的保護。然而,現實情況是,Bob的團隊很可能無法以可持續的方式繼續應用此類反饋。之所以會出現這種情況,有幾個關鍵原因:
(1)不連續的人工過程:整個實驗依賴于Bob的奉獻精神、紀律和意志來仔細檢查他的代碼。隨著釋放壓力的增加,他這么做的可能性越來越小。特別是如果在相當多的情況下,他將花費時間調查數據而沒有提出任何重要的提示。與測試類似,除非它是連續的和自動的,否則它可能不會大規模地發生。
(2)專家需求:如上所述,這個例子在強調一些明確的場景時有些動作。在現實中,如果沒有統計學、回歸甚至基本的機器學習知識,以這種方式處理數據以理解代碼更改的影響是非常困難的。以研究的第一個圖為例,即“之前”狀態。這些值之間的差異是否代表僥幸、某種上升成本或其他什么?
(3)場景切換和工具過載——切換場景很難。為了使這種編程范例能夠工作,它必須是工程團隊可以擁有的解決方案。它不可能是開發人員需要掌握并知道如何正確閱讀的一堆指示板和工具。而需要的認知努力減少得越多,這些信息就越有可能被使用。
未來是持續的反饋
持續反饋是一種新的開發實踐,旨在彌合已經確定的差距:擁有大量易于收集的關于代碼運行時的數據,但需要人工工作、專業知識和時間來處理成實際和可操作的提示。有三個要素可以使其發揮作用:持續管道(反向持續集成管道)、集成工具和自動化數據分析的機器學習/數據科學。
注:本文作者表示,作為Digma的構建者,這是他創建的一個免費的持續反饋插件,因為這個無法解釋的鴻溝阻止開發人員使用代碼數據,這讓他感到非常沮喪。他不止一次遇到“Bob”的情況,所有的信息都在公開的地方。它可以在調試/測試數據中找到,甚至可以在關于代碼的生產數據中找到,只是沒有人會或無法檢查它。
這里設想的是流水線自動化,它可以發現Bob最終發現的所有不同的問題,并使其持續-只是正常開發周期的一部分。實際上,從等式中刪除了整個OTEL配置、樣板文件和工具。將“打開”所需的工作減少到一個簡單的按鈕切換。通過這種方式,整個項目現在只需要Bob做兩件事——啟用可觀察性,并運行他的代碼。這意味著更多的開發人員將能夠開始探索代碼運行時數據的潛力,而不僅僅是像Bob這樣的頑固派。
啟用了可觀察性收集之后,以下是Bob在調試和本地運行時使用Digma插件時看到的IDE視圖:
從視圖中的會話反模式、N+1查詢、檢測速度變慢到隱藏錯誤,所有這些都成為了開發人員視圖的一部分。當Bob繼續編碼、運行和調試時,它會不斷地從收集的大量數據中解鎖和破譯。
通過這種方式,類似于測試,最終可以使可觀察性透明——不需要有意識的努力。就像管道一樣,可觀察性的作用應該是融入背景。不管數據是如何收集的,也不管它是OTEL還是其他技術。更重要的是扭轉了這個過程。Bob沒有在與代碼相關的指標和跟蹤中搜索問題,而是從查看代碼問題開始,這些代碼問題本身包含到相關指標和跟蹤的鏈接,以便進行進一步研究。
在考慮持續反饋時,最讓人大開眼界的方法就是把它關掉。知道所有的問題仍然存在,除了完全看不見之外,這讓人抓狂,這感覺就像在黑暗中編碼。
許多開發人員評論說,與采用測試類似,轉換部分是技術上的,部分是文化上的。誰知道如果用基于證據的指標來檢驗它們,會有什么編碼恐怖事件出現,或者會有多少假設被推翻?也許有些人更喜歡在黑暗中編碼?
在作者看來,它只會給代碼庫帶來問題:技術債務提供更多的形式和方法。了解延遲代碼更改的差距、影響和系統范圍的后果,將有望幫助推動更改,并消除許多企業所遭受的一些前瞻性偏見。
還有更多的例子和細微差別可以作為未來博客文章的素材,這里幾乎沒有觸及使用CI/Prod數據的主題,這可能會產生巨大的影響。
原文標題:Effective Coding With Java Observability,作者:Roni Dover