共計 6713 個字符,預計需要花費 17 分鐘才能閱讀完成。
如何進行邏輯思維 Go 語言微服務改造,針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
1. 改造的背景
得到最早的 APP 就是一個單體的 PHP 的應用,就是圖中最大的黃色塊,中間藍色塊代表不同模塊。下面的黃色部分代表 passport 和支付系統,這個是在做得到之前就存在的系統,因為公司早期有微信里的電商業務。
后來發現有一些業務邏輯并不需要從得到走,還有一些數據格式轉換的工作也不需要跟業務完全耦合,所以加了一層 PHP 的網關就是下圖看到的 V3 那部分。但是這樣做也有一些問題,PHP 后端是 FPM,一旦后端的接口響應較慢,就需要啟動大量 FPM 保證并發訪問,從而導致操作系統負載較高,從這一點上來說,使用 PHP 做這部分工作并不合適。
1.1 屋漏偏逢連夜雨
1.2 改造目標
高性能
首先是性能要高,如果你單臺機器跑幾十 QPS,那么堆機器也很難滿足要求。
服務化
服務化實際上在故障之前就已經開始了,并且由于我們不同的業務團隊已經在負責不同的業務,實際上也是需要服務化繼續做下去。
資源拆分隔離
隨著服務化過程,就需要對資源進行拆分,需要每個服務提供相應的接口,服務之間不能直接訪問其他服務的數據庫或者緩存。
高可用
當時定的目標是 99.9 的可用性。
1.3 為什么選擇 Go
Go 的好處很多,最重要的還是對 PHP 程序員來說,上手更容易,而且性能好很多。
2. 改造的過程
2.1 首先有一個系統架構圖
對于系統改造來說,首先需要知道,系統需要改成什么樣子。因此我們需要一個架構的藍圖。上面就是我們的架構藍圖。首先需要的是一個統一對外的 API GATEWAY,圖中最上層的黃色部分。中間淡紫色的部分是對外的業務服務。淺綠色部分是基礎資源服務,比如音頻文稿信息,加密服務。下面紅色部分是支付和 passport 等公用服務,最右側是一些通用的框架和中間件。最下層是一些基礎設施。
我們的框架跟基礎設施的完善和系統重構是交織進行的,不是說一開始就有一個完全沒問題的設計,隨著業務的改造,會有很多新的功能加進來。
2.2 框架和基礎設施完善
我不講應用系統怎么拆分,因為每個公司業務系統都不一樣,我講一下我們在框架和中間件這部分事情。
API gateway
API gateway 是我們和陳皓(著名的左耳朵耗子)團隊合作研發的。他們團隊對于我們成功跨年幫助很大,在此先感謝一下。
目 的限流
API gateway 主要的目的就是限流,改造過程當中,我們線上有 400 多個接口,經常加新功能。我們可以保證新接口的性能,但是總有在改造過程中疏忽的老接口,通過 API gateway 限流可以保證在流量大的時候,老接口也有部分用戶可用。
升級 API
大部分的 API 升級都是跟客戶端解決的,但是我們不太強制用戶升級,導致線上老接口存在很長時間,我們需要在 API gateway 這一層做一些把新接口數據格式轉成老接口數據格式的工作。
鑒權
在拆分服務之后,需要統一對接口進行鑒權和訪問控制,業界的做法通常都是在網關這一層來做,我們也不例外。
接下來看一下 API gateway 的架構
API gateway 由一個 write 節點和多個 read 節點,節點之間通過 gossip 協議通信。每個節點最上層有一個 CLI 的命令行,可以用來調用 Gateway 的 API。下層的 HTTPServer 等都是一個 plugin,由多個 plugin 組成不同的 pipeline 來處理不同的請求。在后面我會介紹這個的設計。每個節點都有一個統計模塊來做一些統計信息,這個統計信息主要是接口平均響應時間,QPS 等。修改配置之后,write 節點會把配置信息同步到 read 節點上,并且通過 model 模塊持久化到本地磁盤上。
請求經過了兩段 pipeline,第一段 pipeline 基于請求的 url。可以在不同的 pipeline 上面組合不同的 plugin。假設一個接口不需要限流,只需要在接口的配置里頭不加 limiter plugin 就可以了。第二段 pipeline 基于后端的 Server 配置,做一些負載均衡的工作。
接下來我們看整個 API gateway 啟動的流程和調度方面
啟動是比較簡單的,去加載 plugin,然后再去加載相應的配置文件,根據配置文件把 plugin 和 pipeline 做對應。右上角的這個調度器分為靜態調度和動態調度。靜態調度是假設分配 5 個 go routine 來做處理,始終都有 5 個 go routine 來處理對應的請求。動態調度器是根據請求繁忙程度,在一個 go routine 最大值和最小值之間變化。
API gateway 鑒權方面比較簡單,客戶端調用登錄接口,passport 會把 token 和 userid,傳到 API gateway,API gateway 再把相應的 token 傳到這個 APP 端。客戶端下次請求就拿 token 請求,如果 token 驗證不過,就返回客戶端。如果驗證通過再調用后端不同的服務獲取結果,最后返回結果給客戶端。
最后再強調一下 API gateway 如何進行
我們在 API gateway 里面引入兩種限流的策略
1. 滑動窗口限流
為什么會根據滑動窗口限流呢?因為線上接口太多,我們也不知道到底是限 100 好 200 好還是限 10000 好,除非每一個都進行壓測。用滑動窗口來統計一個時間窗口之內,響應時間,成功和失敗的數量,根絕這個統計數據對下一個時間窗口是否要進行限流做判斷。
2.QPS 的限流
為什么還會留一個 QPS 的限流呢?因為要做活動,滑動窗口是一個時間窗口,做活動的時候,客戶拿起手機掃二維碼,流量瞬間就進來了,滑動窗口在這種情況下很難起到作用。
服務框架
目的簡化應用開發服務注冊發現方便配置管理服務框架的常用架構
第一種方式是做成一個庫,把相關功能編譯進服務本身。這里有兩個問題,第一個是我們兼容好幾種語言,開發量比較大。還有一個是一旦客戶端跟隨服務調用方發布到生產環境中,后續如果要對客戶庫進行升級,勢必要求服務調用方修改代碼并重新發布,所以該方案的升級推廣有不小的阻力。在業界來說,spring cloud,dubbo,motan 都是用這樣的機制。
還有一種方案是把 Lord Balancing 的功能拿出來做成一個 agent,跟 consumer 單獨跑,每次 consumer 請求的時候是通過 agent 拿到 Service Provder 的地址,然后再調用 Service Provder。好處是簡化了服務調用方,不需要為不同語言開發客戶庫,LB 的升級不需要服務調用方改代碼。缺點也很明顯,部署比較復雜;還有可用性檢測會更麻煩一點,這個 agent 也可能會掛。如果 agent 掛掉,整個服務也要摘下來。百度內部的 BNS 和 Airbnb 的 SmartStack 服務發現框架也是這種做法。由于我們內部語言較多,因此選擇了第二種做法。
在 Consul 集群中,每個提供服務的節點上都要部署和運行 Consul 的 agent,所有運行 Consul agent 節點的集合構成 Consul Cluster。Consul agent 有兩種運行模式:Server 和 Client。這里的 Server 和 Client 只是 Consul 集群層面的區分,與搭建在 Cluster 之上 的應用服務無關。以 Server 模式運行的 Consul agent 節點用于維護 Consul 集群的狀態,官方建議每個 Consul Cluster 至少有 3 個或以上的運行在 Server mode 的 Agent,Client 節點不限。
Client 和 Server 的角色在 DDNS 是沒有嚴格區分的,請求服務時該服務就是 Client,提供服務時候就是 Server。
NNDS 提供出來的是一個 SDK 可以很容易的集成和擴展為一個獨立的服務并且集成更多的功能。采用 agent 方式,將在每一個服務器部署安裝得到的 agent,支持使用 HTTP 和 grpc 進行請求。
服務完成啟動并可以可以對外提供服務之后,請求 agent 的接口 v1/service/register 將其注冊的進入 DDNS;
注冊成功則其他客戶端可以通過 DDNS 發現接口獲取到該 APP 節點信息;
如果注冊失敗,APP 會重復嘗試重新注冊,重試三次失敗則報警;
假設服務 A 需要請求服務 B,服務名稱為 bbb,直接請求本機的 agent 接口 v1/service/getservice,獲取到 bbb 的服務節點信息。
對于 agent 而言,如果服務 bbb 是第一次被請求,則會請求 Consul 集群,獲取到服務 bbb 的數據之后進行本地從 cache 并對服務 bbb 的節點進行 watch 監控,并定時更新本地的 service 信息;
如果獲取失敗,給出原因,如果是系統錯誤則報警;
這是服務框架基本的接口
這個就是客戶端調用的封裝,可以同時支持 HTTP 和 JRTC,在這個之后我們還做了 RBAC 的權限控制,我們希望能調哪些服務都是可以做權限控制的。
多機緩存
client 請求到 server,server 先在緩存里找,找到就返回,沒有就數據庫找,如果找到就回設到緩存然后返回客戶端。這里是一個比較簡單的模型。只有一級 cache,但是一級 cache 有可能不夠用,比如說壓測的時候我們發現,一個 redis 在我們的業務情況下支撐到接口的 QPS 就是一萬左右,QPS 高一點怎么辦呢?我們引入多級緩存。
越靠近上面的緩存就越小,一級就是服務 local cache,如果命中就返回數據,如果沒有就去 L1 查,如果查到就更新 local cache, 并且返回數據。如果 L1 級也沒有就去
L2 級查,如果查到數據就更新 L1 cache/local cache,并返回數據
我們上面看到的是針對單條內容本身的緩存,在整個棧上來看,gateway 也可以緩存一部分數據,不用請求透穿。這個 5 的虛線是什么意思呢?因為數據修改后需要更新,在應用層做有時候會有失敗,所以讀取數據庫 binlog 來補漏,減少數據不一致的情況。
我一直覺得如果有泛型代碼好寫很多,沒有泛型框架里面就要大量的反射來代替泛型。
php with redis cache
go with redis cache
go with big cache
go with object cache
免費專欄首頁
100+
600+
2000+
12000+
付費專欄首頁
200+
900+
2400+
14000+
多級緩存開始加了之后整個性能的對比,最早 PHP 是一兩百,改成 Go 之后,也不強多少,后面 Go 和 big cache 的大概到兩千左右的,但是有一些問題,后面會講當問題。后面基于對象的 cache,把對象緩存起來,我們跑測試的機器是在八核,達到這樣的結果還可以接受。
熔斷降級
接口同時請求內部服務,service7、8、9 不一樣,service5 是掛掉的狀態,但是對外的服務還在每次調用,我們需要減少調用,讓 service5 恢復過來。
打開的狀態下,失敗達到一定的閾值就關起來,等熔斷的窗口結束,達到一個半開的狀態接受一部分的請求。如果失敗的閾值很高就回到關閉的狀態。這個統計的做法就是我們之前提到的滑動窗口算法。
這里是移植了 JAVA hystrix 的庫,JAVA 里面有很多做得很不錯的框架和庫,值得我們借鑒。
3. 經驗總結
3.1 通用基礎庫非常重要
剛才講的性能提升部分,QPS 從 600 提升到 12000,我們只用了一天,主要原因就在于我們通過基礎庫做了大量優化,而且基礎庫做的提升,所有服務都會受益。
3.2 善用工具
?generate + framework 提升開發效率
?pprof+trace+go-torch 確定性能問題
比如說我們大量的用 generate + framework,通過 generate 和模板生成很多代碼。查性能的時候,pprof+trace+go-torch 可以幫你節省很多工作。Go-torch 是做火焰圖的,Go 新版本已經內置了火焰圖的功能。
這是根據我們的表結構生成相應的數據庫訪問代碼,多級緩存是把所有的訪問都要抽象成 K -V,K-LIST 等訪問模式,每次這么做的時候手動去寫太繁瑣,我們就做了一個工具,你用哪一個表,工具就生成好,你只需要把它組裝一下。
定位性能問題的時候,火焰圖一定要用
比如說定位性能問題就要看最長的地方在哪里,著力優化這個熱點的 code,壓測的時候發現,大家 600、900 的火火焰圖這里有問題,優化完成后如下圖
3.3 其他經驗總結
針對熱點代碼做優化
合理復用對象
盡量避免反射
合理的序列化和反序列化方式
GC 開銷
舉例來說我們之前有一個服務會從緩存里面拿到很多 ID 的 list,數據是存成 json 格式 [1,2,3] 這樣,發現 json 的序列化和反序列化性能開銷非常大,基本上會占到 50% 以上的開銷。早上滴滴講他們的 json 庫,可以提升 10 倍性能,實際上在我們的場景下提升不了那么多,大概只能提升一倍,當然提升一倍也是很大的提升(因為你只用改一行代碼就能提升這么多)。其次 json 飯序列化導致的 GC 的問題也很厲害,最猛的時候能夠達到 20%CPU,即使是在 Go 的算法也做得很不錯的情況下。最終解決的辦法就是在這里引入 PB 替代 json。PB 反序列化性能(在我們的情況下)確實比 json 好 10 倍,并且分配的臨時對象少多了,從而也降低了 GC 開銷。
為什么要避免反射呢?我們在本地建了 local cache,緩存整個對象就要求你不能在緩存之外修改這個對象,但是實際業務上有這個需求。我們出現過這樣的情況后就用反射來做 deep copy。JAVA 反射還可以用,原因是 jvm 會將反射代碼生成 JAVA 代碼,實際上調用的是生成的代碼。但是在 Go 里面不是,本來 Go 的性能是和 C 接近的,大量用了反射之后,性能就跟 python 接近額。后來我們就定義一個 cloneable 的接口,讓程序員手動來做這個 clone 工作。
壓力測試
我們主要用的就是 ab 和 Siege,這兩個通常是針對單個系統的壓力測試。實際上用戶在使用的過程當中,調用鏈上每一個地方都可能出現問題。所以在微服務的情況下,單個系統的壓力測試,雖然很重要,但是不足以完全消除我們系統的所有問題。
舉一個例子,跨年的時候羅老板要送東西,首先要領東西,領東西是一個接口,接下來通常用戶會再刷一下已購列表看看在不在,最后再確認一下他領到的東西對不對。因此你需要對整個鏈路進行壓測,不能只壓測一下領取接口,這樣可能是有問題的。假設你已購列表接口比較慢,用戶領了以后就再刷一下看一看有沒有,沒有的情況下,一般用戶會持續的刷,導致越慢的接口越容易成為瓶頸。因此需要合理的規劃訪問路徑,對鏈路上的所有服務進行壓測,不能只關注一個服務。
我們直接買了阿里云 PTS 的服務,他們做法就是在 CDN 節點上模擬請求,可以對整個訪問路徑進行模擬。
4. 正在做什么
4.1 分庫分表和分布式事務
選擇一個數據庫跟你公司相關的運維是相關的。分布式事務在我這里比較重要,我們有很多購買的環節,一旦拆了微服務之后,只要有一個地方錯,就需要對整個進行回滾。我們現在的做法是手動控制,但是隨著你后面的業務越來越多,不可能所有的都手動控制,這時就需要有一個分布式事務框架,所以我們現在基于 TCC 的方式正在做自己的分布式事務框架。
分庫分表也是一個硬性的需求,我們在這里暫時沒有上 tidb 的原因主要是 DBA 團隊對 tidb 不熟悉。我們之前的分庫分表也是程序員自己來處理,現在正在做一個框架能同時支持分庫和分表,同時支持 hash 和 range 兩種方式。
4.2API gateway
API gateway 上面有很多事情可以做,我們在熔斷和降級做了一些事情。現在一些 Service mesh 做的很多事情是把很多工作放在內部 API gateway 上,是做控制的事情,實際上不應該是業務邏輯關心的事情。我們也在考慮怎么把 API gateway 和 SM 做結合。
4.3 APM
拆了微服務之后,最大的問題是不方便定位具體問題在哪里。我們有時候出問題,我叫好幾個人看看各自負責的系統對不對,大家人肉看出問題的地方在哪,這是個比較蛋疼的做法。因入 APM+tracing 之后,就方便我們來追蹤問題在哪里。
4.4 容器化
我們現在的線上環境,還是在用虛擬機。仿真環境和測試環境已經是容器,使用容器有很多好處,我就不一一列舉了。這也是我們下半年要做的重點工作。
4.5 緩存服務化
我們現在有多級緩存的實現,但是多級緩存還是一個庫的形式來實現的。如果把緩存抽出來,使用 memcached 或者 redis 的協議,抽出來成為一個獨立的服務。后面的業務系統迭代的時候不用關心緩存本身的擴容縮容策略。
關于如何進行邏輯思維 Go 語言微服務改造問題的解答就分享到這里了,希望以上內容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關注丸趣 TV 行業資訊頻道了解更多相關知識。