本文是 Uber 的客戶端工程師團隊講述瞭如何開發最新版本司機端系列文章中的第三篇,該系列代號 Carbon ,是我們共享出行業務的核心。包括其它功能在內,Uber 司機端使得超過 300 萬名司機可以檢視費用、里程以及收益情況。2017 年我們結合司機的反饋開始對司機端進行重新設計,並在 2018 年 9 月份啟動了該專案。
城市建築和無線資料技術的競爭意味著在城市中存在一些手機沒有訊號的黑色區域。這種黑色區域景區更為常見,導致網路質量和阻塞程度頻繁的變化。這些問題尤其影響著那些接送乘客的司機們。
可以舉一個合適的例子來說明這種問題。假設一個司機到達了非常擁擠的班加羅爾機場終點。乘客想支付現金,司機需要在應用裡面操作完成訂單來檢視最終的金額。把車停在路邊,司機端卻無法聯網。乘客匆忙趕飛機,不能聯網就意味著司機就不能結束行程並檢視最終的金額。司機可能會繼續開下去,增加了額外的時間,也可能增加了行程花費,給司機和乘客都帶來了不便。
為了處理這種網路覆蓋漏洞和預防這類事件的發生,我們提出了 —— 樂觀模式。新版本的司機端可以離線操作,這樣司機就可以在沒有網路的情況下用最後一次服務端的預估資料來結束行程。樂觀模式下司機端可以任何網路下正常工作,極大的提高了司機和乘客的體驗。
樂觀模式元件
我們之前的司機端版本中支援一些離線能力來收集失敗的請求,一旦網路恢復就會上傳到伺服器進行整理。雖然這種功能有助於預防一些顯示錯誤,但是不能智慧的更新應用狀態,不能將多個功能堆積在一起,也不能誇會話持久化狀態。我們為新版本的司機端開發了下面這個元件來處理這些問題。
樂觀請求
司機端的任何元件都可以通過提交一個樂觀請求來開始流轉。一個樂觀請求能夠序列化儲存到磁碟,對於一個普通的網路請求來說佔用的記憶體非常小,並且每一個樂觀請求都對應一個樂觀轉換。
樂觀轉換
樂觀模式的核心是轉換,換句話說,操作轉換一個物件從當前狀態到樂觀狀態,也就是,從服務返回的預期結果。轉換還能夠堆積,一個物件可以經過多次轉換。舉一個例子來理解下轉換:想象一個類Counter
有一個屬性count
。我們可以實現一個轉換來增加count
屬性的值。
圖一:在這個簡單的例子中,Counter
物件每經過一次增加轉換,count
屬性值就會增加一。
根據業務需求轉換既可以是簡單的也可以是複雜的。每一個樂觀請求都關聯一個轉換,轉換會根據樂觀請求返回一個最終的_樂觀狀態_。當資料從服務端返回時使用者是無感知的,這種方式提供了一種平滑的過渡方案。
當客戶端提交一個樂觀請求時,關聯在請求上的轉換就會立馬生效,應用進入樂觀狀態,從而完成請求。樂觀狀態會一直被保持直到收到服務端的真實狀態,然後同步應用和服務端。
圖 2-1: 普通的計數請求失敗
圖 2-2: 在無網路的情況下樂觀模式使用轉換及時更新資料狀態,將來有網路的情況下和服務端進行同步。
樂觀流
我們整個應用都在使用 RX streams 傳遞資料。應用的每個功能都會隨著已釋出資料流的狀態改變作出響應。這種機制使我們能夠使用相同的流輕鬆地將樂觀變換應用於物件的最新狀態。為了獲得樂觀狀態,我們結合了資料最後的狀態和可用的轉換。在將資料釋出迴流並由功能使用之前,資料已經應用了每個轉換。隨後業務只需簡單的根據資料的樂觀狀態作出響應。
依賴請求
同時也存在一些請求依賴於樂觀請求的完成。例如,甚至在後端不知道行程已經開始的情況下傳送一個結束行程的請求是不合理的。當我們在等待樂觀請求完成的時候,這樣的依賴請求將會被放入佇列一段時間。如果週期過長,我們會結束這個請求,通知使用者網路錯誤。
設計挑戰
我們在這個設計中遇到了一些挑戰。我們想要支援多個堆疊的樂觀請求,允許在沒有網路的情況下完成多個步驟。由於和伺服器不同步,我們還需要處理錯誤地進入樂觀狀態並且必須回滾到先前狀態的情況。確保我們可靠地向司機展示最準確的狀態需要進行多次迭代,並持續優化。
兜底轉換
樂觀模式開啟的情況下,應用程式可能會在樂觀請求完成之前收到其他的網路資料。
圖 3: 在這個場景中,我們在收到伺服器最新的狀態之後又進行了樂觀轉換。
我們繼續拿上面用到的計數器的例子來說。應用程式使用增加變換把最終的值變成了 2。然而,這個值還沒有和服務端同步。在這期間,收到的其他的網路響應可能還是舊的值 1。樂觀模式使用轉換更新了這個舊值並且維護這個樂觀狀態。這就確保了應用程式不會在兩種狀態之前來回切換,避免給使用者產生混亂的體驗。
應用重啟時如何存活
所有的樂觀請求和最新的樂觀狀態一起被儲存在磁碟裡,所以它們能夠在應用重啟的時候得以保留。考慮這麼一種情況,一些請求正在排隊和伺服器同步,但是使用者卻殺死了應用。在重新啟動的時候,樂觀請求和狀態會從磁碟中載入。這允許使用者在重新啟動應用時處於相同的狀態。樂觀請求排隊和伺服器同步。
顯示錯誤
我們遇到的這個新功能的一個特殊問題是它如何顯示錯誤。樂觀模式的請求只應該由於後端中斷而失敗,並且結果應該是可預測的便於模擬。然而,實踐中會出現錯誤。由於我們使用樂觀的流程服務使用者,所以一個小錯就可能帶來很不好的體驗。首先,應用程式的狀態回滾到之前的樂觀狀態,不是使用者所期望的狀態,下個動作可能不太明顯。其次,即使之前的狀態可能已經無效了,我們也需要用它來接收錯誤原因來展示。為了處理這些問題,我們在司機端里加入了一個全域性處理錯誤資訊的框架,它可以呼叫內部彈窗框架。
請求出錯的情況總是很少見的。對於經常發生的錯誤,比如行程太短,我們在手機端上實現了檢查,以便更好的處理。
節省時間
對司機來說,我們在開始和結束行程上利用樂觀模式節省了大量的時間。我們經常可以看到在真實的網路請求完成之前行程已經開始幾分鐘的情況。截至 2018 年 11 月,我們注意到平均每個樂觀操作節省大約 13.5 秒的時間。即使在新司機端的早期階段,我們每天累計節省司機的時間也超過了一年。
樂觀模式的發展
在無網路的情況下能夠正常執行的能力在 Ubers 的其他應用程式上面使用的也非常好。設計之初是為了加速開始和結束行程的速度,它還被整合到 Uber Eats 中功能中,當使用現金結算時,可以更快的結束。它還能用到類似於這種可以快速響應後續同步到服務端的業務中,比如對乘客或司機的評價,標記訊息已讀,和收集交付的指紋。