重構了後端服務,我學到了這些東西

2021-09-17 19:01:41 字數 3814 閱讀 6373

我是kurio(來自印度尼西亞的一款新聞聚合器)的軟體工程師。kurio是一款聚合器應用程式,我們的主要工作是:收集發布合作夥伴**上的新聞或文章,並通過我們的應用程式將其提供給使用者。

與其他新聞聚合器一樣,我們為使用者提供了多種新聞內容,例如按我們的top_stories邏輯進行排序的新聞、按照趨勢進行分類的新聞以及來自特定發布商的新聞。

移動端的kurio新聞布局

feed的構建過程由我們的feed服務負責處理。

這個服務是kurio的三大主要專案之一,之前的版本已經執行了很長一段時間。因此,它變得非常複雜,有時也會難以理解。這也使得新增新功能變得非常困難。因此,我們決定重建我們的feed服務。希望通過這個新版本的feed服務,我們可以輕鬆新增新功能或者使其更易於維護。

首先我們需要了解以前的系統是如何工作的。從編譯、測試和部署開始,直到收到使用者請求,我們需要知道整個過程的工作原理。

因為這是乙個核心的服務,而我剛剛來這裡一年,我真的不知道它是如何運作的,尤其是多年來整個系統新增了很多額外的功能和補丁,很難通過閱讀**來了解它。所以,我們需要了解流程和規則,然後基於這些流程和規則構建新的流程和規則。

例如,當使用者開啟應用程式時,會得到由這項服務提供的top_stories新聞源。或者是一些規則,例如:在top_stories新聞源中向使用者顯示的內容是有限制的。或者類似於:不要向使用者展示他們不關注的主題,或者根據使用者的屬性(性別、年齡等)顯示新聞。

列出這些規則和流程是一件簡單的事情,難就難在如何將其轉換為**。一般來說,我們的流程非常簡單,如下所示。

使用者獲取新聞的流程

基本上主要是兩個大功能,獲取個性化新聞和獲取預設新聞。最難的是獲取個性化新聞,因為我們必須將它與個性化引擎相結合。此外,我們必須遵循一些與上面提到的個性化內容相關的規則(提取使用者興趣和屬性,然後根據使用者的興趣構建新聞源)。

我們之所以要重構這個服務,是因為當我們要新增新功能時,之前系統的**架構無法很好地擴充套件。如果要在未來開發新功能會非常痛苦,因為我們不得不重構很多東西。

所以我們真正需要的是修復架構。設計乙個新的架構真的很難。我們需要問自己很多問題,比如:「這樣做會怎樣?」、「為什麼要這樣?」、「為什麼不是這樣?」我們希望新架構能夠解決「未來」的問題,並提供向後相容性。為此,我們進行了大約乙個月的討論,針對每個大功能進行了技術棧和流程方面的討論。

最終,我們決定嘗試一些函式式的開發方式。我們放棄了之前使用的**架構,發明了一種新的**架構,帶有函式式程式設計(使用高階函式模式)的味道,但又不像lisp或clojure那麼動態。

因此,在我們的**中可以找到很多hof(高階函式)模式,如下所示:

func something(params, func(params)) (func(params)){}
但因為我們使用的是go語言,一種靜態型別的程式語言,所以當建立了很多函式時就會有很多痛點,必須進行大量的型別檢查和轉換,而這耗費了大量時間。

因此,我們意識到go語言不適合用來解決我們的問題,但在我們這10個後端工程師當中,只有乙個人了解clojure(函式式程式設計),而學習新的程式語言就意味著我們需要額外的時間。經過長時間的討論,我們決定繼續使用go語言,不僅是因為我們所有的後端工程師都很了解go語言,也是因為go語言已經在很多微服務中得到驗證。

在將流程和設計轉換為**時,我意識到我們必須對基礎有乙個真正的了解。一開始,我並沒有真正理解高階函式的工作原理。在閱讀**時感到很困惑,怎麼總是乙個函式接收乙個函式作為引數然後再返回乙個函式呢?不過要感謝谷歌,我現在終於明白了。

我們還需要了解go語言本身的基礎知識,比如使用指標作為函式接收器、datetime的基礎知識,以及很多其他基礎的東西。如果我們對這些東西不了解,只會增加完成這個專案的時間。

優化的第一條規則——不要優化

優化的第二條規則——還不到優化的時候

優化前先分析

因此,在開發這個服務時,我們的第乙個目標是確保至少可以執行它。我們沒有去考慮效能問題,並試圖忽略任何有關優化的事情,例如使用go例程。

在開發完**後,我們就可以編譯並執行它,所有請求都能被正常處理,響應也很正常。當然,初始版本速度非常慢。與之前的系統相比,它慢了十倍。以前的系統在使用staging伺服器時單個api請求大約需要500毫秒,而新版本需要50000毫秒(約50秒)或更久。

優化**也是我們最重要的任務之一。為了優化我們的**,我們遵循了以下步驟:

找出需要長時間處理的迴圈**,將其轉換為使用go例程,提高並行性或使用管道。

分析系統並檢測所有速度慢的功能,對其進行優化。所幸的是,在go語言中進行分析很容易。借助pprof(工具,我們可以對系統進行分析並檢測所有速度慢的功能。我們甚至可以檢測出我們所使用的哪個庫最慢,這樣我們就可以使用具有類似功能的另乙個庫替換它們。

如果有必要,增加快取。

構建服務時,我們的規則是只在確實需要使用快取的情況下使用快取。快取就像一種藥物,它會讓我們上癮,因為當我們的系統看起來很慢時,會把快取看成是解決問題的靈丹妙藥。通常,在開發大型併發專案時沉迷於使用「快取」的人,首先想到的是「快取」,而不是先考慮優化(基準測試、分析)功能(邏輯/演算法)。

對於我們的情況,我們通過兩種方式來使用快取:

通過這種優化,我們至少可以像在以前的系統中那樣改進新系統的效能(staging伺服器的響應時間約為400毫秒,生產伺服器的響應時間約為180毫秒)。

基於語義版本控制,在不新增新特性和不破壞api的情況下進行重新構建就不算是乙個新的版本。基本上,在這個新重建的系統中,我們的目標是改變架構,而不是api規範。因此,無論我們在系統中進行做出哪些變更,都不能更改api。因為即使是非常微小的變化也會影響到所有相關的服務。

為了讓它成為乙個新版本,我們對錯誤響應訊息正文進行了一些修改。

原始錯誤響應訊息正文:

新版本的錯誤響應訊息正文:

}
因為進行了這些變更,我們還需要處理其他使用了我們api服務的相關服務。所幸的是,只有兩種服務使用了我們的api服務,所以我們只需要更新兩個應用程式:儀錶盤應用程式和移動閘道器api。此外,因為只有響應錯誤發生了重大變更,所以只需要修改應用程式的一小部分即可。

在重新構建這個服務時,我們至少進行了三次測試,然後才發布到生產環境中:單元測試、整合測試和負載測試。

在所有這些型別的測試中,單元測試是最小的測試。有些人似乎低估了單元測試的重要性,因為它只是乙個單元,乙個小功能。但是,在重建這個新服務時,我體會到了單元測試的重要性。

在sprint開始時,我們忽略了單元測試,因為我們希望專注在**架構的設計上。所以我們開發了一些沒有任何測試的功能。我們這樣做是因為我們仍然在構建一些實驗性的**架構,為了避免進行不必要的單元測試重構,我們在這個時候沒有建立任何單元測試。

但是,在完成**架構設計之後,我們忘了為在sprint開始時建立的功能新增單元測試。直到我們將它部署到staging伺服器並與另乙個真實服務進行了整合測試。我們在應用程式中發現了很多錯誤。然後我們檢視了源**,發現我們的功能有很多條件都沒有覆蓋到。

知道了這個問題後,我們意識到我們還沒有測試過這個功能。它還沒有通過單元測試。如果我們從一開始就進行單元測試,那麼修復這個問題並重新部署它就不需要做額外的工作。在進行單元測試時,我們可以考慮很多不同情況,並在部署應用程式之前修復它們。

雖然我們做的是幕後工作,並且對我們的使用者沒有可見的影響,但我們確實學到了很多東西。我學到了很多關於如何從頭開始構建高併發系統的知識。完成這項任務後,我知道了為什麼當我們在面試後端開發職位時,總會被問及邏輯和演算法問題。這是因為在構建高併發系統時,效能是非常重要的方面,任何演算法都會影響到系統的響應時間。

英文原文:

重構了後端服務,我學到了這些東西

我是kurio 來自印度尼西亞的一款新聞聚合器 的軟體工程師。kurio是一款聚合器應用程式,我們的主要工作是 收集發布合作夥伴 上的新聞或文章,並通過我們的應用程式將其提供給使用者。與其他新聞聚合器一樣,我們為使用者提供了多種新聞內容,例如按我們的top stories邏輯進行排序的新聞 按照趨勢...

重構了後端服務,我學到了這些東西

我是kurio 來自印度尼西亞的一款新聞聚合器 的軟體工程師。kurio是一款聚合器應用程式,我們的主要工作是 收集發布合作夥伴 上的新聞或文章,並通過我們的應用程式將其提供給使用者。與其他新聞聚合器一樣,我們為使用者提供了多種新聞內容,例如按我們的top stories邏輯進行排序的新聞 按照趨勢...

我學到了什麼 我思考了什麼

經理是個搞開發搞了十多年的人,甚至自己開發了框架並投入使用了.搞得外包,全國各地都跑過.可以自豪的說哪哪公司什麼系統參與搞過,人生閱歷非常豐富.我思考最多的應該是我哥跟我說後來經理也說過的話,我哥說時候我不太有感覺,經理說時候還舉了個例子.意思就是人在三十前都不是掙錢的時候.是投資自己的時候.去給自...