導讀:Uber的Greenlight Hubs是其全球支援系統,為合作車主提供從賬戶和支付到車輛檢查和車主註冊等各方面的人工支援。本文作者簡單介紹了該系統的實現。對於構建類似人工支援系統有借鑒意義,並且特別對如何處理全球化帶來的時區問題做了很好的經驗總結。
Uber的Greenlight Hubs(GLH)在全球擁有超過700個分支機構,為合作車主提供從賬戶和支付到車輛檢查和車主註冊等各方面的人工支援。為了給合作車主創造更好的體驗並提高客戶滿意度,Uber的客戶優先工程團隊開發的內部客戶支援系統,是一個透過GLH實現了更加簡化和快速的支援申請的解決方案。
客戶支援系統包含兩個主要功能:為我們的服務專家提供的登記佇列系統,以跟蹤合作車主進入GLH的情況; 和一個預約系統,讓合作車主可以透過Uber合作車主APP安排人工支援的預約。 這些工具自從2017年3月推出以來,已經改善了全球合作車主的的支援服務體驗。
向內部解決方案的過渡
隨著Uber的發展,我們之前的客戶支援技術在為合作車主提供最佳體驗上不能很好的擴充套件。透過開發我們自己的GLH客戶支援系統,我們提出了一個既適合我們的可擴充套件性和定製需求,又改進了現有基礎架構以支援新功能的解決方案。
開發我們自己的工具意味著我們可以:
-
方便獲得客戶支援需要的資訊:我們的登記系統可以讓客戶支援代表更加方便獲得那些解決合作車主關心的問題所需要的相關資訊。這種整合有助於減少支援服務的解決時間和改善合作車主使用GLHs的體驗。
-
合作車主交流渠道的聚合:Uber各種支援渠道(包括應用內訊息,GLH自身和電話支援)的集中化意味著GLH專家擁有額外的背景關係資訊,在一個地方統一的解決合作車主的問題。
-
為合作車主在GLH縮短等待時間:使用我們升級後的系統,合作車主可以透過安排預約來避免在高峰時段發生不必要的等待時間。
為了實現這些標的,為我們的內部客戶支援平臺開發了兩個新工具:登記佇列和預約系統。
更加無縫的登記體驗
透過在我們的客戶支援平臺之上設計和實現的實時登記系統,為合作車主提供了更加無縫的支援體驗。使用此係統,合作車主會與禮賓人員登記,然後禮賓人員會根據與其帳戶關聯的電話號碼或電子郵件地址找到合作車主的個人資料。
一旦合作車主登記,GLH專家會從該網站的佇列中選擇他們。合作車主隨後會在手機和GLH內的監視器上收到推送訊息,告知他們已與專家配對。一旦合作車主與支援站在通知中指定的專家會面,合作車主就會退出登記佇列。
我們的實時登記系統還彙總了客戶資訊,例如過去的旅行和支援資訊,使我們的專家能夠盡可能有效地解決問題。
圖1: 在GLHs, 與專家配對時監控提醒使用者
提供實時專家佇列
建立這個實時登記解決方案時遇到一些困難。我們面臨的一個挑戰是在一個專家宣告一個合作車主已經得到協助的場景下,防止專家衝突。為了實現這一標的,我們的系統需要提供一個等待支援服務的合作車主的佇列(稱為我們的GLH站點佇列),透過它,專家可以與等待中的合作車主配對,併在合作車主被選中時實時通知他。
由於WebSocket協議支援低延遲的長連線,所以我們利用它透過後端傳送佇列更新。Go,Uber許多後端服務選擇的語言,透過讓我們使用管道和協程技術更加方便的將實時更新傳輸給web客戶端。
儘管如此,在使用WebSocket過程中我們遇到了一些有意思的挑戰。為了使我們的站點佇列能夠實時工作,我們決定將特定站點的所有WebSocket連線和佇列寫入維持在一個固定的主機。這樣,當佇列中的一個登記或者預約被更新,所有相關的連線客戶端也會被更新。 在我們寫入並將WebSocket連線到主機之前,使用單個主機處理這些請求需要在應用程式層上進行分片。
我們使用了Ringpop-go,我們的開源可擴充套件和容錯應用層分片用於Go應用,這有助於配置分片金鑰,以便具有相同金鑰的所有請求都將路由到同一主機。對於我們的分片金鑰,我們使用了GLH站點ID,因此在同一個GLH上發生的所有登記都會轉到同一主機,並更新相關客戶端上的所有站點佇列。
圖2: 我們的面對面支援體系結構利用擁有特定GLH的主機的前端WebSocket連線。 來自活動資料中心的GLH專家前端和移動客戶端的請求透過Ringpop進行分割,並分配給擁有給定GLH的主機。 來自非活動資料中心的請求會重定向到活動資料中心。 與個人支援相關的資料儲存在優步內部資料儲存的Schemaless中
實現跨資料中心的高可靠性
為確保我們的GLH軟體平穩執行,我們需要保證高可用性。為了做到這一點,我們的服務執行在多個資料中心,處理來自全球的請求。如果某個資料中心由於某種不可預知的原因(如中斷)宕機,該服務將自行恢復並繼續從其他資料中心執行。
鑒於我們使用WebSocket,在多個資料中心中執行該服務帶來了一系列困難。 如果資料中心出現故障,我們不得不重新考慮如何正確處理WebSocket。雖然Ringpop分片在跨資料中心執行良好,但由於每次主機離開或進入環時都會傳送跨資料中心的請求,因此會增加延遲。
為解決WebSocket降級問題,我們配置了我們的系統,以便每個資料中心都有一個環; 這樣,如果具有相同唯一GLH ID的兩個請求命中兩個不同的資料中心,它只會更新我們承載站點佇列的資料中心中的站點佇列。 無論請求來自哪個資料中心,我們都會將所有請求轉發給固定的資料中心。如果資料中心發生故障,我們會將請求轉發給其他資料中心。 我們同時也會將與出現故障的資料中心建立的所有WebSocket連線殺掉,並與新的資料中心重新建立連線。
增加預約
為了減少在GLH的等待時間並確保我們在高峰時段提供充足的支援,我們推出了一項新功能,讓我們的合作車主提前安排GLH預約,只需在UberAPP上輕鬆點選幾下即可。
圖3: 我們的面對面支援預約安排流程使合作車主
可以輕鬆安排我們的Greenlight中心的預約
圖4: 當合作伙伴的應用程式到達Greenlight Hub時,合作伙伴會在Uber合作伙伴應用程式中收到簽入通知.
儘管合作車主的預約安排很簡單,但是幕後還有大量的工作來保證流程盡可能的無縫。例如,GLH管理者可以隨時指定有多少專家在其中心工作,以確保他們的團隊不會超額預訂; 那麼當合作車主進入應用程式時,他們只能看到基於專家數量的可用預約數。例如,如果週二早上9點在某個的GLH只有四名專家正在工作,那麼該中心的管理者當時可以設定四個預約的能力,從而限制可用預約的數量。
當合作車主安排預約時,他們會出現在GLH的當天預約串列中。當合作車主到達預定的預約時間時,他們可以透過他們的app輕鬆登記,並通知分配給他們的專家,他們已經到達。構建我們的預約系統包括在後端實施排程系統,在移動裝置上新增預約功能,併為我們的GLH管理者開發基於瀏覽器的日曆介面。
建立全球排程系統
受Martin Fowler關於經常性日曆事件的論文的啟發,我們決定使用核心日曆服務構建我們的日程安排系統,具體實現可用的時間間隔(簡化為日曆間隔),系統將這些時間間隔視為規則來處理這些規範。
在Fowler模型中,這些規則可以由GLH管理者指定和修改,從而允許更靈活的排程。由於排程系統通常有許多需要考慮的邊界情況,因此我們逐步構建排程系統以避免範圍模糊,併為每一步提供一個功能正常的系統:
-
我們的第一次迭代使用GLH管理者最初設定的營業時間,併為每個站點指定了全球三名專家的容量,使我們能夠慢慢推出測試版本的軟體。
-
我們的第二次迭代使用由GLH管理者設定的日曆間隔,允許他們間隔多久設定一次專家池容量。
-
我們的第三次迭代結合了現有的日曆時間間隔,但也允許GLH管理者設定GLH關閉時間(即非營業時間和假日)。
然而,由於Uber的國際影響力,我們很快遇到了時區相關的問題,並且由於系統的各個元件需要協調正在使用哪個時區的環境而加劇了這一問題,例如, GLH時區或合作車主的時區。 另外,我們需要考慮夏令時的變化。 為瞭解決這些需求,我們採用了以下規則:
-
與主要後端服務API互動的所有客戶端均採用其所選GLH的時區。
-
所有預約時間在我們的資料庫中都會儲存為UTC +0時區時間。
-
主要的後端服務有一個內部層來處理持久層和API層之間的所有時區轉換。這使我們能夠抽象出日曆邏輯並呼叫與日曆相關的內部方法,而無需擔心時區問題。
重要的是要註意時區,即UTC偏移,不作為GLH物件的屬性儲存。 如果是這種情況,那麼夏令時改變會導致先前安排的預約時間在任一方向偏移一小時。為了正確處理這個問題,UTC偏移量將根據每個GLH的物理坐標進行動態計算。
時區邊緣的情況
在構建我們的排程系統時,我們遇到了一些關於時區的特殊案例。當我們的系統將“日曆間隔”轉換為當地時區時,出現了一個問題。 由於UTC和當地時間之間的時區變化(取決於相關網站的時區),日期可能不正確。 例如,11月20日5:00 am UTC時間實際上是太平洋標準時間11月19日的下午9:00。因此,重要的是我們不要對相關時間段的日期做出假設,並且在時區跨越多天時進行測試。
另外,當我們將GLH營業時間從UTC時間轉換為當地時間時,我們遇到了類似的時區問題。 我們在當地時間節省了我們的工作時間,因為沒有日期,我們沒有足夠的背景關係讓我們將其用UTC儲存。 例如,一個GLH在星期一從上午9點到下午9點可能導致UTC營業時間從週一下午5:00開始週二早上5點結束。 由於沒有日期,不清楚這些當地時間提到的小時是一週中的哪天。 因此,每當建立新的日曆時間間隔,我們都必須將開放時間從已儲存的本地時間轉換成UTC時間。 根據業務邏輯所在的位置,這些場景可能需要在Web和移動客戶端以及伺服器端進行廣泛的測試。
在移動裝置上使用日期時間庫
對於合作車主實際使用我們的排程系統,我們需要為移動裝置構建新的UX。 這涉及到修改支援表單螢幕給合作伙伴除提交按鈕之外的選項以獲得幫助,以及幫助主螢幕顯示他們可能會有的任何即將到來的預約。
還有一些與特定活動相關的新螢幕:選擇附近的GLH預約會面,根據該會場的可用選項選擇預約的特定日期和時間,確認選擇以建立預約,檢視詳細資訊 預訂預約並取消預訂,並檢視有關該網站的詳細資訊,例如地址。
由於我們與日期和時間打交道,並且因為我們希望我們的伺服器API傳回結構化的資料(例如ISO 8601),而不是預先格式化的本地化字串(即使用者的偏好語言中的日期)供我們展示,所以我們假設將使用java.util.Date標準。在這個標準中,Date和相應的日曆類在處理時區時有許多已知的問題,所以我們想要探索一下其他選項是否能工作的更好。 例如,Joda-Time標準(一種Java 8 API)聽起來很有趣,但它還不相容Android系統這種廣泛用於合作車主的裝置的系統。
我們最終發現了ThreeTenBP– Joda-Time的後繼者,它將Java 8的時間和日期API引入Java 6和7。然而,以前在Android上使用ThreeTenBP的嘗試遭遇啟動問題。在啟動時,這些庫從磁碟載入時區資料庫資訊,對其進行分析並將其註冊到庫中以供稍後使用。這個庫的特定於Android的包裝器以更友好的方式載入資料,但仍然存在阻礙應用程式啟動的非平凡磁碟操作。在低至中檔裝置上進行測試時,這會使Uber合作伙伴應用程式的啟動速度減慢超過200毫秒。
我們嘗試以多種方式最佳化ThreeTenBP,例如,透過在不同的執行緒上執行實際的磁碟操作,以便Application.onCreate的其餘部分可以並行發生,併在最後加入執行緒,從而確保Uber合作伙伴應用程式可以安全地使用 庫。 我們也嘗試使用其他類似的庫,它們在啟動時嘗試少做或沒有IO,但是不能將啟動時間降低到合理的延遲。
我們嘗試使用方法分析器,讓我們驚訝的是,透過解析程式碼,我們看到在啟動過程中大量時間花費在常見的字串方法中像string.split。根據我們閱讀原始碼,甚至是來自Application.onCreate的步除錯器,似乎都沒有發生這種情況。 在探查器中,重量級操作彙總到ZoneRulesProvider類中的靜態初始化程式中,其中(理論上)懶惰時區資料庫提供程式程式碼正在註冊。 由於這個類正在被載入進行註冊,即使被註冊的物件完全是懶惰的,並且在註冊時沒有執行任何I / O,也會執行靜態初始化塊以試圖從ServiceLoader / META載入時區資料庫META-INF。這是Java伺服器中典型的樣式,而不是Android。它用了我們避免使用的同樣資源下載,由於它在Android上的效能很差。
我們最終修改了ThreeTenBP本身,以便可以輕鬆重寫此靜態初始化塊的行為。預設實現將保持不變,但會被抽象化在新的ZoneRulesInitializer類後。Android應用程式或庫將能夠提供自己的實現,以便在庫的第一次使用時透過Android資產載入時區資料庫。
我們更新了另一個面向Android的ThreeTenBP封裝器lazythreetenbp,以利用這個新介面,相當於ThreeTenABP有待更新。使用這個庫的啟動延遲是零,導致低延遲。但是,在靜態模組初始化中有時區資料庫的載入的發生,意味著在需要時區資料之前不需要進行任何操作,這在典型的使用者會話期間甚至可能不會發生。(Uber App非常大,很少有功能需要操縱日期和時間會用到時區)。
圖5: GLH管理者的日曆UI指定在任何給定時間段內,特定站點上有多少專家可用。
我們還為GLH管理者構建了一個日曆應用程式,可以輕鬆靈活地配置其網站的營業時間,可用的預約時間以及任意給定小時內的可用專家池。可用時間只能在營業時間內建立。日曆中的休息時間變灰。日曆還顯示當前已安排的預約。
在日曆的周檢視中,網站管理員可以從開始時間拖放到結束時間以建立可用時間。此外,他們還可以在移動應用程式中新增假期和午餐時間等關閉專案,從而防止網站管理員在現場關閉期間意外增加可用時間。
為了設計這個介面,我們使用Node.js,React / Redux,Styletron進行行內樣式,ES2017(ES8)用於JavaScript,Lerna用於儲存monorepo的可重用元件,以及其他一些Uber類庫/框架,如Bedrock和Superfine。設計能夠提供卓越使用者體驗的日曆功能非常複雜,因此建立一個並保持高效能是一項重大挑戰。然而,我們並不想妥協我們的簡單,可讀和可擴充套件的程式碼庫。另外,我們希望建立一些可重用的React元件,以適應將使用這些元件的其他前端專案。
在我們的軟體測試版中,每當日曆被拖動時,日曆中的許多元素都會被重新渲染。因此,小時範圍是動態顯示的,即使大多數這些元素沒有視覺更新。由於渲染日曆中的許多DOM元素,我們透過調整shouldComponentUpdate()生命週期方法來減少需要渲染的元素數量,從而利用React的虛擬DOM。
然後,我們透過使用react-dnd的拖動源來檢查日曆中的元素是否在開始時間和結束時間的範圍內,並僅重新呈現那些元素。另外,我們使得閉包和可用時間的DOM元素不可更新,因為它們不允許重疊,略微提高了效能。結果,在拖放過程中由更新引起的200毫秒延遲減少,使其接近於0。
由於日曆應用程式對伺服器進行了大量呼叫,並且包含許多效能調整,所以自開始以來,程式碼複雜度顯著增加。為了保持程式碼清潔和簡單,我們將程式碼抽取到可重用元件和HOC以及一些環境設定中,並將其轉換為前端monorepo。我們將Lerna用於monorepo併發布軟體包。透過使用monorepo,幾個軟體包被儲存在一個回購站中,這樣可以節省引導新專案的時間,並且可以一次更新多個元件,從而更容易新增跨元件功能或修複錯誤。另外,為了增強React元件的可重用性,我們使用Styletron代替CSS來進行行內樣式。這確保了其他開發人員不需要自己新增CSS,從而避免考慮樣式衝突,因為所有樣式都直接應用於JavaScript程式碼中。
Uber的面對面支援工程的未來
開發此產品有助於提高合作車主在GLH上的體驗,從而提高客戶滿意度。遷移到新系統已經將等待時間平均縮短了15%以上,並且一旦與客戶支援專家匹配,問題解決時間減少了25%。最重要的是,這些新功能讓那些在GLH安排預約的合作車主幾乎不需要等待時間。
這隻是為我們來自全球的合作車主和客戶支援專家準備的眾多產品的一小部分。我們一直持續在探索新技術以改善我們使用者的GLH體驗,從改進我們的分析到合作車主提交申請前主動提供支援服務。
相關閱讀:
高可用架構
改變網際網路的構建方式
長按二維碼 關註「高可用架構」公眾號