羅輯思維首席架構師:Go微服務改造實踐

Java小鋪發表於2018-08-23

一、改造的背景

得到最早的APP就是一個單體的PHP的應用,就是圖中最大的黃色塊,中間藍色塊代表不同模組。下面的黃色部分代表passport 和支付系統,這個是在做得到之前就存在的系統,因為公司早期有微信裡的電商業務。

後來發現有一些業務邏輯並不需要從得到走,還有一些資料格式轉換的工作也不需要跟業務完全耦合,所以加了一層PHP的閘道器就是下圖看到的V3那部分。但是這樣做也有一些問題,PHP後端是FPM,一旦後端的介面響應較慢,就需要啟動大量FPM保證併發訪問,從而導致作業系統負載較高,從這一點上來說,使用PHP做這部分工作並不合適。

屋漏偏逢連夜雨

案例一:8/31大故障:2017年8月31日的時候,老闆做活動,導致流量超過預期很多,系統掛了兩個小時。

案例二:羅老師要跨年

每年羅老師都要跨年演講,第一年是在優酷,有200多萬人的線上觀看,第二年是同時和優酷等視訊網站再加上深圳衛視一起合作直播,2016年深圳衛視的收視率是地方第一。2017年的老闆當時想要送東西,送東西的這個場景比較恐怖,二維碼一放出來,就會有大量使用者同時請求。

最恐怖的事情是,老闆要送的東西8月31日的時候還沒有,要在後面2個月期間把東西開發出來。一方面業務迭代不能停,一方面需要扛過跨年,所以就需要我們對業務系統進行改造。

改造目標

高效能:首先是效能要高,如果你單臺機器跑幾十QPS,那麼堆機器也很難滿足要求。

服務化:服務化實際上在故障之前就已經開始了,並且由於我們不同的業務團隊已經在負責不同的業務,實際上也是需要服務化繼續做下去。

資源拆分隔離:隨著服務化過程,就需要對資源進行拆分,需要每個服務提供相應的介面,服務之間不能直接訪問其他服務的資料庫或者快取。

高可用:當時定的目標是99.9的可用性。

Go的好處很多,最重要的還是對PHP程式設計師來說,上手更容易,而且效能好很多

二、改造的過程

首先有一個系統架構圖

對於系統改造來說,首先需要知道,系統需要改成什麼樣子。因此我們需要一個架構的藍圖。上面就是我們的架構藍圖。首先需要的是一個統一對外的API GATEWAY,圖中最上層的黃色部分。 中間淡紫色的部分是對外的業務服務。淺綠色部分是基礎資源服務,比如音訊文稿資訊,加密服務。下面紅色部分是支付和passport等公用服務,最右側是一些通用的框架和中介軟體。最下層是一些基礎設施。

我們的框架跟基礎設施的完善和系統重構是交織進行的,不是說一開始就有一個完全沒問題的設計,隨著業務的改造,會有很多新的功能加進來。

框架和基礎設施完善

我不講應用系統怎麼拆分,因為每個公司業務系統都不一樣,我講一下我們在框架和中介軟體這部分事情。

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裡面引入兩種限流的策略:

滑動視窗限流

為什麼會根據滑動視窗限流呢?因為線上介面太多,我們也不知道到底是限100好200好還是限10000好,除非每一個都進行壓測。用滑動視窗來統計一個時間視窗之內,響應時間,成功和失敗的數量,根絕這個統計資料對下一個時間視窗是否要進行限流做判斷。

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是一兩百,改成Go之後,也不強多少,後面Go和big cache的大概到兩千左右的,但是有一些問題,後面會講當問題。後面基於物件的cache,把物件快取起來,我們跑測試的機器是在八核,達到這樣的結果還可以接受。

熔斷降級

介面同時請求內部服務,service7、8、9不一樣,service5是掛掉的狀態,但是對外的服務還在每次呼叫,我們需要減少呼叫,讓service5恢復過來。

開啟的狀態下,失敗達到一定的閾值就關起來,等熔斷的視窗結束,達到一個半開的狀態接受一部分的請求。如果失敗的閾值很高就回到關閉的狀態。這個統計的做法就是我們之前提到的滑動視窗演算法。

這裡是移植了JAVA hystrix的庫,JAVA裡面有很多做得很不錯的框架和庫,值得我們借鑑。

經驗總結

通用基礎庫非常重要

剛才講的效能提升部分,QPS 從600提升到12000,我們只用了一天,主要原因就在於我們通過基礎庫做了大量優化,而且基礎庫做的提升,所有服務都會受益。

善用工具

generate + framework提升開發效率

pprof+trace+go-torch確定效能問題

比如說我們大量的用generate + framework,通過generate和模板生成很多程式碼。查效能的時候,pprof+trace+go-torch可以幫你節省很多工作。Go-torch是做火焰圖的,Go新版本已經內建了火焰圖的功能。

這是根據我們的表結構生成相應的資料庫訪問程式碼,多級快取是把所有的訪問都要抽象成K-V,K-LIST等訪問模式,每次這麼做的時候手動去寫太繁瑣,我們就做了一個工具,你用哪一個表,工具就生成好,你只需要把它組裝一下。

定位效能問題的時候,火焰圖一定要用

比如說定位效能問題就要看最長的地方在哪裡,著力優化這個熱點的code,壓測的時候發現,大家600、900的火火焰圖這裡有問題,優化完成後如下圖

其他經驗總結

例如:

針對熱點程式碼做優化

合理複用物件

儘量避免反射

合理的序列化和反序列化方式

接下來重點講幾個操作:

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節點上模擬請求,可以對整個訪問路徑進行模擬。

正在做什麼

分庫分表和分散式事務

選擇一個資料庫跟你公司相關的運維是相關的。分散式事務在我這裡比較重要,我們有很多購買的環節,一旦拆了微服務之後,只要有一個地方錯,就需要對整個進行回滾。我們現在的做法是手動控制,但是隨著你後面的業務越來越多,不可能所有的都手動控制,這時就需要有一個分散式事務框架,所以我們現在基於TCC的方式正在做自己的分散式事務框架。

分庫分表也是一個硬性的需求,我們在這裡暫時沒有上tidb的原因主要是DBA團隊對tidb不熟悉。我們之前的分庫分表也是程式設計師自己來處理,現在正在做一個框架能同時支援分庫和分表,同時支援hash和range兩種方式。

API gateway

API gateway上面有很多事情可以做,我們在熔斷和降級做了一些事情。現在一些Service mesh做的很多事情是把很多工作放在內部API gateway上,是做控制的事情,實際上不應該是業務邏輯關心的事情。我們也在考慮怎麼把API gateway和SM做結合。

APM

拆了微服務之後,最大的問題是不方便定位具體問題在哪裡。我們有時候出問題,我叫好幾個人看看各自負責的系統對不對,大家人肉看出問題的地方在哪,這是個比較蛋疼的做法。因入APM+tracing之後,就方便我們來追蹤問題在哪裡。

容器化

我們現在的線上環境,還是在用虛擬機器。模擬環境和測試環境已經是容器,使用容器有很多好處,我就不一一列舉了。這也是我們下半年要做的重點工作。

快取服務化

我們現在有多級快取的實現,但是多級快取還是一個庫的形式來實現的。如果把快取抽出來,使用memcached或者redis的協議,抽出來成為一個獨立的服務。後面的業務系統迭代的時候不用關心快取本身的擴容縮容策略。在這裡順便給大家推薦一個架構交流群:617434785,裡面會分享一些資深架構師錄製的視訊錄影:有Spring,MyBatis,Netty原始碼分析,高併發、高效能、分散式、微服務架構的原理,JVM效能優化這些成為架構師必備的知識體系。

相關文章