如何使用Java可觀察性進行有效編碼
譯文譯者 | 李睿
審校 | 重樓
多年來,在試圖使可觀察性計劃取得成功的過程中,許多企業(yè)犯了一些常見的錯誤。然而,這些企業(yè)的失誤中最關(guān)鍵和最根本的問題是對技術(shù)和工具本身的不可抗拒的迷戀。
這應該讓人感到不意外。許多“讓我們添加可觀察性平臺X”的項目在開始時通常都是大張旗鼓,但其方向感非常模糊,并且成功的標準也非常混亂。對于有效的可觀察性可以做些什么來幫助開發(fā)人員更好地工作,許多供應商和預言者對于這一愿景的宣傳卻令人懷疑地缺失了。開發(fā)人員需要問問自己:有多少次發(fā)現(xiàn)自己會把目光從集成開發(fā)環(huán)境(IDE)中的代碼上移開,發(fā)現(xiàn)可以從執(zhí)行數(shù)據(jù)中學到什么?
不要誤解,開發(fā)人員要相信可觀察性在軟件開發(fā)中可以發(fā)揮重要作用。OpenTelemetry發(fā)揮了巨大的作用,可以清楚地看到它如何幫助開發(fā)人員編寫更好的代碼,引入新的范例,并加快開發(fā)周期。它可以啟發(fā)開發(fā)人員提出他們甚至還沒有考慮到的問題。然而,無論人們在網(wǎng)上看到什么,其重點似乎仍然是可觀察性本身,如何啟用它,以及如何開始。盡管有著炫酷圖形的儀表板非常棒,但許多開發(fā)團隊都不知道該從哪里入手。

本文將討論一個更有趣的話題:對于使用可觀察性的開發(fā)者來說,成功是什么樣子的?開發(fā)團隊如何期望使用豐富的代碼運行時數(shù)據(jù)更好地編碼和發(fā)布?更重要的是,現(xiàn)在有哪些可觀察性可以告訴開發(fā)人員關(guān)于代碼的事情,以及它如何幫助開發(fā)人員改進?可以通過具體的代碼示例來了解如何利用可觀察性作為編碼實踐。
超越監(jiān)控:縮短開發(fā)過程中的反饋循環(huán)
可觀察性最大的希望在于提供真實和客觀的反饋,不受單元測試的一些偏差和偏見的影響。想象一下,當開發(fā)人員還在處理代碼更改時,就會收到有關(guān)任何回歸或問題的警報。或者,始終了解代碼的哪些部分在生產(chǎn)中實際使用,并根據(jù)集成測試結(jié)果輕松識別需要注意的薄弱環(huán)節(jié)。
這可能是開發(fā)人員可觀察性的真正潛力,而不是作為“監(jiān)視”解決方案的傳統(tǒng)角色。監(jiān)視器和警報至關(guān)重要,但不幸的是,它們的重點始終是報告已經(jīng)發(fā)生的問題。也許是因為該技術(shù)主要由DevOps/SRE/IT團隊使用,他們主要關(guān)心生產(chǎn)的穩(wěn)定性。
本文作者表示,有一次在發(fā)布一個產(chǎn)品的階段,他和團隊中的其他開發(fā)人員感覺他們的作用更像是消防隊而不是開發(fā)團隊,他開玩笑地將匆忙修復漏洞稱為BDD——不是行為驅(qū)動設(shè)計,而是漏洞驅(qū)動開發(fā)。然而,這種描述并非完全不準確。開發(fā)人員沒有積極主動地改進代碼,而是極其被動地追逐一個又一個問題,這很快就變得不可持續(xù)。
例舉更實際的例子
為了說明開發(fā)人員如何利用可觀察性來改進的開發(fā)周期,在此例舉一個現(xiàn)實場景的更實際的例子:團隊中的高級開發(fā)人員Bob被要求向Spring PetClinic示例添加一些功能。跟蹤寵物的疫苗接種記錄似乎非常重要,Bob被要求與外部數(shù)據(jù)源集成以實現(xiàn)這一目標。對于最初的最小可行產(chǎn)品(MVP),Bob為此創(chuàng)建一個功能分支,并繼續(xù)實現(xiàn)一些新的功能。
在閱讀了許多關(guān)于如何從Java應用程序中收集可觀察性數(shù)據(jù)的教程之后,Bob在后臺運行了幾個OSS和免費工具來幫助他完成任務。這篇文章并不會詳細介紹如何設(shè)置整個堆棧(因為它也有廣泛的文檔記錄)。但是,可以依docker_compose文件的形式找到整個堆棧。
Bob的基本可觀測性堆棧:
- 用于跟蹤的OpenTelemetry;他還在本地運行了一個OTEL收集器容器,將數(shù)據(jù)路由到各種工具。
- 用于顯示跟蹤的Jaeger。
- 用于采集指標的Micrometer。
- 用于保存矩陣的Prometheus和用于可視化它們的開源版本Grafana。
需要注意的是,要開始使用OTEL收集代碼數(shù)據(jù),Bob不需要進行任何代碼更改。在本地運行時,他可以安全地使用OTEL代理。在他的例子中,他只是在IDE的運行配置中引用代理,以便在本地運行/調(diào)試時可以引用代理。他還添加了一個docker-compose.override文件,用于使用docker/Podman啟動應用程序(這也不需要更改源docker-compose文件)。

在一切就緒并運行之后,Bob創(chuàng)建了一個新的功能分支,并開始開發(fā)新功能。
疫苗API外觀組件
非常幸運的是,有人已經(jīng)編寫了一個Spring組件來與另一個模塊的模擬API進行通信。Bob的工作很簡單:將組件注入PetController,并在添加寵物時使用它檢索數(shù)據(jù)。該組件非常簡單,使用OKHttp庫實現(xiàn)一個基本的REST調(diào)用,以獲取JSON形式的數(shù)據(jù)。
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);
} 更新寵物模型
接下來,為了保存疫苗接種數(shù)據(jù),而不是每次都檢索它,必須更新模型和數(shù)據(jù)庫結(jié)構(gòu)。這涉及到很多樣板文件,但為了保存每只寵物的疫苗接種信息,這是必要的措施。Bob適時地添加了一個新表,對類中的關(guān)系進行建模,還更新了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創(chuàng)建了一個簡單的域服務,該服務將被注入PetController中。新服務編排域邏輯,以便從外部API檢索新寵物的疫苗記錄,并用最新日期更新模型。不幸的是,這也是Bob犯了幾個錯誤的地方,其中一些錯誤與facade抽象的泄漏有關(guān),這掩蓋了成本昂貴的HTTP調(diào)用。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甚至編寫了一些測試,他對快速的進展感到滿意,并對本地測試時沒有發(fā)生意外的代碼充滿信心,他轉(zhuǎn)向收集的運行時數(shù)據(jù),看看它能揭示他的更改。他決定擴展“完成”的定義,并花費額外的精力來檢查與他的更改相關(guān)的數(shù)據(jù)。
使用可觀察性
首先,參考某種基準是很重要的。有兩個API操作受到了更改的影響,Bob希望了解更改之前和之后它們是如何執(zhí)行的。作為可觀察性設(shè)置的一部分,Bob還配置了Micrometer和Actuator,以提供有關(guān)API的有用指標。在這個案例中,這些可以通過執(zhí)行器URL直接訪問http://localhost:8082/actuator/metrics。然而,為了更好的可視化和更多的繪圖選項,Bob將在他的堆棧中使用本地運行的Prometheus和Grafana OSS。
查看一些常見的Grafana儀表板,令人驚訝的是,沒有用于跟蹤API響應時間的默認圖表。也許是因為大多數(shù)儀表板都與Ops相關(guān),關(guān)注CPU/內(nèi)存和堆棧大小,而不是日常開發(fā)人員的見解。幸運的是,使用Actuator度量很容易配置這樣的儀表板。可以使用以下查詢創(chuàng)建一個以創(chuàng)建新寵物的API為重點的圖表:
HTTP
1 http_server_requests_seconds{uri="/owners/{ownerId}/pets/new", quantile="0.5", method="POST", outcome="REDIRECTION"} != 0
2然后可以在代碼更改之前和之后檢查圖。
代碼更改之前:

代碼更改之后:

毫無疑問,這些更改導致了嚴重的性能問題。可以通過查看指標立即發(fā)現(xiàn)問題,但跟蹤可以揭示更多關(guān)于根本原因和潛在問題的信息。現(xiàn)在是調(diào)用Jaeger的時候了,這是可觀察性堆棧的另一個組件。Jaeger習慣于可視化捕獲的跟蹤,并為Bob提供了一個機會,讓他在忙于添加更多邏輯和功能的同時,調(diào)查他的代碼在做什么:

因此,在不添加單個斷點的情況下,已經(jīng)可以在這個請求中了解到許多關(guān)于這一代碼的內(nèi)容。到目前為止,Bob還完全沒有注意到這些信息。雖然他在嘗試新請求時確實注意到了一些延遲,但他并沒有太在意。也許外部API太慢了?既然他已經(jīng)訪問了跟蹤,他就可以重新審視引入的代碼了。
精選豐富的語句
第一個突出的問題是作為findById存儲庫方法的一部分觸發(fā)的許多SQL語句。Spring Data會自動檢測到這一點,并提供一些關(guān)于正在發(fā)生的事情的場景。更仔細地檢查查詢會發(fā)現(xiàn)一個熟悉的Hibernate陷阱:
看起來訪問關(guān)系是通過通常稱為N+1選擇的方式為每個寵物獲取的。有趣的是,這個問題似乎是PetClinic應用程序特有的,而且似乎早于Bob的更改。實際上,雖然這會導致一些放緩,但它并不像其他一些問題那樣重要,這一點在Bob進一步檢查跟蹤時變得明顯。
HTTP請求聊天
性能回歸的真正原因似乎與Bob的誤解有關(guān),可能是由于VaccineServiceFacade方法的命名不明確。他似乎不太清楚,每次調(diào)用VaccineRecord函數(shù)時在后臺執(zhí)行API調(diào)用。使用更好的命名約定可以緩解這種抽象漏洞,強調(diào)這實際上是長時間同步操作的執(zhí)行。


隱藏的錯誤
HTTP請求中還發(fā)生了其他事情。當向下滾動請求列表時,Bob注意到其中一些請求以錯誤結(jié)束,然后在嘗試序列化不存在的響應時出現(xiàn)異常。基于HTTP錯誤代碼的根本原因與速率限制或節(jié)流或外部API有關(guān)。這個問題可以通過優(yōu)化調(diào)用的數(shù)量來暫時解決,但是隨著越來越多的用戶開始同時使用這個組件,這個問題可能會重新出現(xiàn)。此外,這段代碼中的異常處理肯定是錯誤的,也許需要一種重試機制。

就在Bob開始糾正通過檢查可觀測性工件發(fā)現(xiàn)的許多問題之前,他決定快速查看他修改的另一個API。在這種情況下,性能似乎沒有顯著下降,但是檢查跟蹤仍然發(fā)現(xiàn)至少有一個問題需要修復。
將會出現(xiàn)哪些問題?
數(shù)據(jù)中還可以識別出其他問題,但是回顧一下場景,考慮一下如果Bob在合并更改之前沒有對其進行分析,會發(fā)生什么情況:代碼最終被部署。有些問題在CR或后期測試階段被發(fā)現(xiàn),導致更多的更改、額外的延遲和痛苦的合并,因為在此期間會出現(xiàn)更多的更改。其他問題也會轉(zhuǎn)移到生產(chǎn)中,導致進一步的問題:延遲發(fā)布、匆忙修復、增加團隊的焦慮和沮喪等等。毫無疑問,可以發(fā)現(xiàn)縮短反饋循環(huán)有很多好處。
勝利了嗎?不完全是
在這個有點幼稚的例子中,能夠演示如何簡單地打開OTEL并通過一些OSS工具流式傳輸數(shù)據(jù),有可能為Bob和其他開發(fā)人員提供額外的保護。然而,現(xiàn)實情況是,Bob的團隊很可能無法以可持續(xù)的方式繼續(xù)應用此類反饋。之所以會出現(xiàn)這種情況,有幾個關(guān)鍵原因:
(1)不連續(xù)的人工過程:整個實驗依賴于Bob的奉獻精神、紀律和意志來仔細檢查他的代碼。隨著釋放壓力的增加,他這么做的可能性越來越小。特別是如果在相當多的情況下,他將花費時間調(diào)查數(shù)據(jù)而沒有提出任何重要的提示。與測試類似,除非它是連續(xù)的和自動的,否則它可能不會大規(guī)模地發(fā)生。
(2)專家需求:如上所述,這個例子在強調(diào)一些明確的場景時有些動作。在現(xiàn)實中,如果沒有統(tǒng)計學、回歸甚至基本的機器學習知識,以這種方式處理數(shù)據(jù)以理解代碼更改的影響是非常困難的。以研究的第一個圖為例,即“之前”狀態(tài)。這些值之間的差異是否代表僥幸、某種上升成本或其他什么?

(3)場景切換和工具過載——切換場景很難。為了使這種編程范例能夠工作,它必須是工程團隊可以擁有的解決方案。它不可能是開發(fā)人員需要掌握并知道如何正確閱讀的一堆指示板和工具。而需要的認知努力減少得越多,這些信息就越有可能被使用。

未來是持續(xù)的反饋
持續(xù)反饋是一種新的開發(fā)實踐,旨在彌合已經(jīng)確定的差距:擁有大量易于收集的關(guān)于代碼運行時的數(shù)據(jù),但需要人工工作、專業(yè)知識和時間來處理成實際和可操作的提示。有三個要素可以使其發(fā)揮作用:持續(xù)管道(反向持續(xù)集成管道)、集成工具和自動化數(shù)據(jù)分析的機器學習/數(shù)據(jù)科學。
注:本文作者表示,作為Digma的構(gòu)建者,這是他創(chuàng)建的一個免費的持續(xù)反饋插件,因為這個無法解釋的鴻溝阻止開發(fā)人員使用代碼數(shù)據(jù),這讓他感到非常沮喪。他不止一次遇到“Bob”的情況,所有的信息都在公開的地方。它可以在調(diào)試/測試數(shù)據(jù)中找到,甚至可以在關(guān)于代碼的生產(chǎn)數(shù)據(jù)中找到,只是沒有人會或無法檢查它。
這里設(shè)想的是流水線自動化,它可以發(fā)現(xiàn)Bob最終發(fā)現(xiàn)的所有不同的問題,并使其持續(xù)-只是正常開發(fā)周期的一部分。實際上,從等式中刪除了整個OTEL配置、樣板文件和工具。將“打開”所需的工作減少到一個簡單的按鈕切換。通過這種方式,整個項目現(xiàn)在只需要Bob做兩件事——啟用可觀察性,并運行他的代碼。這意味著更多的開發(fā)人員將能夠開始探索代碼運行時數(shù)據(jù)的潛力,而不僅僅是像Bob這樣的頑固派。

啟用了可觀察性收集之后,以下是Bob在調(diào)試和本地運行時使用Digma插件時看到的IDE視圖:

從視圖中的會話反模式、N+1查詢、檢測速度變慢到隱藏錯誤,所有這些都成為了開發(fā)人員視圖的一部分。當Bob繼續(xù)編碼、運行和調(diào)試時,它會不斷地從收集的大量數(shù)據(jù)中解鎖和破譯。
通過這種方式,類似于測試,最終可以使可觀察性透明——不需要有意識的努力。就像管道一樣,可觀察性的作用應該是融入背景。不管數(shù)據(jù)是如何收集的,也不管它是OTEL還是其他技術(shù)。更重要的是扭轉(zhuǎn)了這個過程。Bob沒有在與代碼相關(guān)的指標和跟蹤中搜索問題,而是從查看代碼問題開始,這些代碼問題本身包含到相關(guān)指標和跟蹤的鏈接,以便進行進一步研究。

在考慮持續(xù)反饋時,最讓人大開眼界的方法就是把它關(guān)掉。知道所有的問題仍然存在,除了完全看不見之外,這讓人抓狂,這感覺就像在黑暗中編碼。
許多開發(fā)人員評論說,與采用測試類似,轉(zhuǎn)換部分是技術(shù)上的,部分是文化上的。誰知道如果用基于證據(jù)的指標來檢驗它們,會有什么編碼恐怖事件出現(xiàn),或者會有多少假設(shè)被推翻?也許有些人更喜歡在黑暗中編碼?
在作者看來,它只會給代碼庫帶來問題:技術(shù)債務提供更多的形式和方法。了解延遲代碼更改的差距、影響和系統(tǒng)范圍的后果,將有望幫助推動更改,并消除許多企業(yè)所遭受的一些前瞻性偏見。
還有更多的例子和細微差別可以作為未來博客文章的素材,這里幾乎沒有觸及使用CI/Prod數(shù)據(jù)的主題,這可能會產(chǎn)生巨大的影響。
原文標題:Effective Coding With Java Observability,作者:Roni Dover

































