基於SpringBoot的微服務架構與K8S容器部署實踐(精簡版)

前不久作為架構師完成了某知名快消企業的一個業務中臺建設。系統上線後,經歷了雙十一活動的流量高峰,整體執行穩定。最近有空,便將此次架構的思路,心得稍作整理在這篇部落格中分享一下。不會深入每一個技術細節,而是把用到的技術、框架、工具做一個簡單的回顧,作為日後的參考。

業務架構

業務架構方面,該系統作為業務中臺,主要負責客戶資產管理,包括客戶的卡、券以及其他虛擬資產。透過對外暴露標準restful介面的方式提供服務。服務的呼叫方包括自有渠道的app、小程式,以及合作伙伴渠道,包括招行、阿里等。而系統本身也會透過服務閘道器去呼叫公司內部的其他業務系統介面,如透過客戶中心介面同步會員資訊等。

基於SpringBoot的微服務架構與K8S容器部署實踐(精簡版)

根據目前的統計,這個業務中臺,每日的服務呼叫量在700萬次左右,有活動時也會超過1000萬次。而大部分交易,發生在上班、午休以及下午3點左右(下午茶)的時間段內。

基於SpringBoot的微服務架構與K8S容器部署實踐(精簡版)

由於涉及到客戶業務細節,這裡對業務架構就不做詳細說明了。

技術架構

這個案例中採用了基於SpringBoot的微服務架構。結合企業自身的基礎架構設施,進行K8S容器化部署,並採用Kong API Gateway對各業務中臺暴露的API介面進行統一管理。

Kong API Gateway

隨著微服務架構在企業中的流行,原來大而全的系統被拆分為粒度較小的中臺,而系統中的大部分功能則被以restful API形式提供的服務所取代,這使得IT系統能夠更加快速地響應業務變化帶來的挑戰,但同時隨著服務的增加,如何有效管理這些服務卻成為難題。

基於SpringBoot的微服務架構與K8S容器部署實踐(精簡版)

在一些中小型專案中,我們一般都會採用Spring Cloud的技術棧,並選擇Spring Cloud Gateway來作服務閘道器。然而,對於一些大型企業,則需要全域性考慮服務的治理,閘道器效能,以及其他擴充套件功能。

在這個案例中,企業使用了

Kong

作為API閘道器。中臺將需要開放外部使用的API,透過閘道器控制檯進行註冊,新增證書,生成Auth Key供關聯方使用。

Kong具有以下一些特性,能夠很好地滿足大型組織對於服務閘道器的需求:

開源(本案例中使用的是Kong的企業版,提供了原廠服務)

亞毫秒級的響應延遲,得益於基於Nginx與OpenResty帶來的超高效能

單節點25K TPS

認證、授權、限流、資料轉換(此案例中會員ID被新增到請求頭中)、日誌、統計分析

基於SpringBoot的微服務架構與K8S容器部署實踐(精簡版)

應用架構

整個系統採用java開發後端以及vue開發前端,應用部分共分為4個服務元件,全部進行容器化部署,並透過Ingress Controller負載均衡對外暴露服務:

資產服務:提供客戶資產相關的服務介面

資產消費者服務:MQ監聽服務,非同步處理資產相關請求

控制檯服務:資產管理運維類服務介面,供控制檯前端使用

控制檯前端服務:使用Vue開發的控制檯前端應用(如下圖)

基於SpringBoot的微服務架構與K8S容器部署實踐(精簡版)

SpringBoot

除控制檯前端外,其他三個元件均採用目前主流的java微服務框架SpringBoot 2。3。4開發(考慮到穩定性,未使用最新的2。4版本)。

本案例中,透過開發應用框架,實現了系統中資料表達形式的統一,以及標準的據轉換、校驗、訊息繫結、錯誤處理等功能。架構師需要對應用框架負責,簡明、高效、統一的應用框架,能夠提升開發效率,產出標準一致的程式碼,保證交付質量。

應用框架不在本文的討論範圍內,而以下一些技巧或第三方包,卻在我們構建大多數SpringBoot應用中得到使用。

定製MyBatis

資料層框架採用MyBatis,在大型應用中MyBatis能夠幫助程式設計師更好地控制資料層互動,並進行調優。一般可以在applicaion。yml中配置MyBatis,但當我們需要讓MyBatis支援更多定製特性(如:多資料庫支援)時,可以透過定義SqlSessionFactory bean來實現。

@Bean

public

SqlSessionFactory

sqlSessionFactory

DataSource

dataSource

throws

Exception

{

SqlSessionFactoryBean

sfb

=

new

();

sfb

setDataSource

dataSource

);

sfb

setVfs

SpringBootVFS

class

);

Properties

props

=

new

Properties

();

props

setProperty

“dialect”

dataConfiguration

getDialect

());

props

setProperty

“reasonable”

String

valueOf

dataConfiguration

isPageReasonable

()));

PageHelper

pagePlugin

=

new

PageHelper

();

pagePlugin

setProperties

props

);

Interceptor

[]

plugins

=

{

pagePlugin

};

sfb

setPlugins

plugins

);

ResourcePatternResolver

resolver

=

new

PathMatchingResourcePatternResolver

();

sfb

setMapperLocations

resolver

getResources

“classpath*:mappers/”

+

dataConfiguration

getDialect

()

+

“/*。xml”

));

sfb

setTypeAliasesPackage

“com。xxx。bl。core。data。model”

);

SqlSessionFactory

factory

=

sfb

getObject

();

factory

getConfiguration

()。

setMapUnderscoreToCamelCase

true

);

// factory。getConfiguration()。addInterceptor(new CoreResultSetHandler());

factory

getConfiguration

()。

setCallSettersOnNulls

dataConfiguration

isCallSettersOnNulls

());

return

factory

}

使用logback日誌元件

採用logback日誌框架,可以在logback配置檔案中指定針對不同的Spring profile在不同的環境中採用不同的日誌級別,並採用不同的appender。同時引入spring-cloud-starter-sleuth依賴,透過設定traceId,使整個請求全鏈路上的所有日誌打印出一致的traceId,大大方便了各系統間生產問題的協同排查。另外,採用非同步方式記錄日誌,也有利於降低IO阻塞。

<

springProfile

name

=

“stg”

>

<

root

level

=

“error”

>

<

appender-ref

ref

=

“STDOUT”

/>

<

ref

=

“SAVE-ERROR-TO-FILE-STG”

/>

root

>

<

logger

name

=

“org。xxx”

level

=

additivity

=

“false”

>

<

ref

=

/>

<

ref

=

“ASYNC-SAVE-TO-FILE-STG”

/>

logger

>

springProfile

>

<

springProfile

name

=

“prod”

>

<

root

level

=

>

<

ref

=

/>

<

ref

=

“SAVE-ERROR-TO-FILE-PROD”

/>

root

>

<

logger

name

=

level

=

additivity

=

>

<

ref

=

“ASYNC-SAVE-TO-FILE-PROD”

/>

logger

>

springProfile

>

SSL加密及密碼安全

全鏈路傳輸加密已成為企業安全中必不可少的措施。透過在classpath中引入CA頒發(也可以使用自籤)的jks證書,並在application配置檔案中進行簡單配置,便可實現SpringBoot應用的SSL加密。

ssl

enabled

true

key-store

classpath

xxx。net。jks

key-store-type

JKS

key-store-password

RUIEIoUD

key-password

RUIEIoUD

require-ssl

true

密碼以明文形式存放在配置檔案中,也是不安全的。你可以jasypt加密配置檔案中使用到的密碼,或者直接使用Key-Vault方案,比如本案例中會分別在微軟雲環境中使用Azure Key Vault或本地IDC中使用Cyberark Conjur方案。

同步與非同步服務

我們並沒有使用Spring Webflux來支援reactive特性,因為,這會增加開發複雜度,並且Webflux雖然改善了Web容器阻塞機制,但並不能從根本上解決高併發請求到來時的阻塞問題。

在這個案例中,透過搭建了3個節點的RabbitMq映象叢集,作為訊息中介軟體,並透過應用框架的支援,實現了服務的同步非同步切換功能。我們將對外提供的服務註冊到資料庫中,在應用啟動時,讀入redis快取。當請求到來時,透過API code判斷該請求的響應模式:同步或非同步。如果是同步請求則直接處理,而如果是非同步請求,則傳送到RabbitMq中,再由經過封裝的消費者元件進行非同步消費,最終達到削峰的目的。

基於SpringBoot的微服務架構與K8S容器部署實踐(精簡版)

對於開發人員來說,他們只需要關注服務的業務邏輯開發,由應用框架統一處理服務的同步,非同步切換,訊息傳送或失敗時的異常處理,以及死信佇列的維護等工作。

Dockerfile

案例中的四個元件需要實現容器化部署,分別為SpringBoot應用與Vue應用建立Dockerfile。

典型的SpringBoot應用Dockerfile如下,一般情況下大型組織會構建私有映象倉庫,透過私有倉庫拉取映象的速度更快,能夠節省CICD的時間。

FROM openjdk:11-jre

#FROM cargo。xxx。net/library/openjdk:11-jre

ARG JAR_FILE=console-service/build/libs/*。jar

COPY ${JAR_FILE} app。jar

EXPOSE 9002

EXPOSE 9003

ENTRYPOINT [ “java”, “-jar”, “/app。jar” ]

vue應用的Dockerfile如下,同樣添加了SSL證書,進行傳輸加密:

FROM cargo。xxx。net/library/nginx:stable-alpine

COPY /dist /usr/share/nginx/html/console

COPY nginx。conf /etc/nginx/nginx。conf

ARG KEY_FILE=stg。xxx。net。key

ARG PEM_FILE=stg。xxx。net。pem

COPY ${KEY_FILE} /etc/ssl/certs/cert。key

COPY ${PEM_FILE} /etc/ssl/certs/cert。pem

EXPOSE 80

CMD [ “nginx”, “-c”, “/etc/nginx/nginx。conf”, “-g”, “daemon off;” ]

編寫dockerfile時有以下一些注意事項:

基礎映象:儘可能推薦選擇官方映象

選擇大小適中的版本:如果選擇的基礎映象過大,啟動後需要消耗更多的資源,影響系統性能。如果太小,則可能缺失關鍵功能。

利用快取:將dockerfile中不易變動的內容寫在dockerfile最前。

資料庫架構

在賬戶資料上億,交易資料幾百億的系統,需要採用分庫分表方案。本案例中,採用了MyCat+MySQL的資料庫架構方案。採用mycat代理Master與Slave,可靈活進行主從切換。Slave可作為Master熱備,也同時可作為讀庫,實現讀寫分離。備庫除作為準實時的備份外,也可作為運維庫或提供大資料平臺數據抽取。

同時採用1主2從1備的雙機房設計

Master到Slave使用半同步方案,保證從庫資料一致性。

Master異常時,透過mycat切換至Slave,Slave轉換為新Master

Master異常恢復後,先將原Master設定為Slave,資料同步完成後,再切換回正式Master

mycat高可用

mycat採用k8s容器化執行,使用k8s service來實現mycat的負載均衡,達到mycat的叢集的高可用。若mycat容器節點異常,應用自動連線到另外的mycat節點上。

對資料庫的大量操作是讀操作,一般佔到所有操作70%以上。所以做讀寫分離還是很有必要的,如果不做讀寫分離,那麼從庫也是一種很大的浪費。 mycat透過配置很容易做到讀寫分離,在從庫進行讀操作,提升資源利用率,在主庫進行寫操作,減低主庫壓力。

基於SpringBoot的微服務架構與K8S容器部署實踐(精簡版)

分庫分表

垂直分庫:按照功能劃分,把資料分別放到不同的資料庫和伺服器。例如:賬戶、資產、交易等業務領域不同的資料分別放在不同的庫中,分散壓力、減少相互影響、降低耦合,獨立模組獨立釋出

水平分庫:在垂直分庫不能滿足要求時,再對模型進行水平的 切分,將同一實體,不同範圍的資料分散到不同庫中,保持單庫數量和壓力,提升連線數,達到橫向擴充套件的目的。

基於SpringBoot的微服務架構與K8S容器部署實踐(精簡版)

冷熱資料方案

熱資料快取

對於高頻使用的熱資料,如經常使用App的客戶資訊等,適當增加資料庫query cache,提升資料庫查詢效能。

在應用層使用redis等記憶體快取部分高頻使用資料,降低請求響應時間,增加系統流暢度,提升客戶體驗。

進行讀寫分離,使用從庫提供資料查詢的服務,提升從庫硬體資源利用率,降低主庫讀壓力,增加主庫寫效能。提升整體效率。

冷資料歸檔

對於使用頻率很低或基本不使用的冷資料,如歷史交易、歷史卡券等,進行資料的歸檔,提升資料庫的效能。

也可提供使用頻率較低的歷史交易查詢功能,使用備庫提供服務。

對於交易類資料建議按日期進行分庫分表,每日交易分為一片或多片,對於歷史交易如1年前交易進行定期遷移和歸檔,提升資料庫效能。

DEVOPS與K8S容器化部署

DEVOPS流水線

本案例中,透過基於jenkins的CICD平臺,將應用程式碼從github程式碼庫獲取,使用gradle進行構建(前端使用npm構建),透過dockerfile打成映象後,部署到K8S容器平臺。

基於SpringBoot的微服務架構與K8S容器部署實踐(精簡版)

在進行持續整合的過程中,同時加入了安全檢查,合規檢查以及單元測試(SpringBoot應用使用JUnit,Vue前端應用使用Jest測試框架)的步驟,以保證每一次釋出的質量。

ConfigMap

ConfigMap用於將應用的配置資訊與程式的分離,這種方式不僅可以實現應用程式被的複用,而且還可以透過不同的配置實現更靈活的功能。本案例中,SpringBoot應用在K8S部署時,便將application。yml檔案以ConfigMap檔案的形式進行掛載。需要注意,SpringBoot會優先讀取classpath下的配置檔案,因此需要在打出springboot應用jar包時,先將配置檔案排除,並透過容器啟動命令引數來制定掛載的應用配置檔案。

-

spring

profiles

active

=

prod

-

spring

config

location

=

/config/

application

yml

K8S容器部署

在K8S部署平臺,可以為每一個服務指定初始的資源,以及節點數量配置。比如我們為SpringBoot應用初始配置,2core 4g的資源配置,節點數量則為20個。

基於SpringBoot的微服務架構與K8S容器部署實踐(精簡版)

根據需要我們可以採用滾動方式對pod數量進行伸縮。而不會引起服務不可用的情況。

基於SpringBoot的微服務架構與K8S容器部署實踐(精簡版)

另外,我們也可以利用彈性伸縮,基於某些關鍵指標,如容器的CPU使用量作為閾值,來觸發容器進行彈性伸縮。在這個案例中,透過彈性伸縮機制,在上班以及中午業務高峰時間段內,將更多pod提供給業務服務元件,而在晚上,則會將pod從業務元件收回,提供給需要跑批處理以及非同步消費的服務元件。

基於SpringBoot的微服務架構與K8S容器部署實踐(精簡版)

運維與監控

ELK

ELK是一套解決方案而不是一款軟體, 三個字母分別是三個軟體產品的縮寫。 E代表Elasticsearch,負責日誌的儲存和檢索; L代表Logstash, 負責日誌的收集,過濾和格式化;K代表Kibana,負責日誌的展示統計和資料視覺化。

基於SpringBoot的微服務架構與K8S容器部署實踐(精簡版)

Dynatrace

Dynatrace可能是目前最優秀的應用效能管理工具(APM),它既能監控基礎設施如伺服器,K8S容器,又能自動發現並監控在容器內執行的動態微服務,瞭解它們如何執行、相互之間如何通訊,還能立即檢測出效能不佳的微服務。在我們的案例中,透過定製dashboard新增我們所需要關注的監控資料。

基於SpringBoot的微服務架構與K8S容器部署實踐(精簡版)

Dynatrace還能自動識別服務,並提供更精細的檢測資料,為開發或運維人員定位問題,帶來了極大的幫助。

基於SpringBoot的微服務架構與K8S容器部署實踐(精簡版)

一些思考

資料庫分庫分表方案帶來的程式碼侵入問題:MyCat+MySQL雖然在物理上實現了分庫分表,但對於開發來說帶來了侵入性問題,需要為分片鍵進行特殊的表結構設計,在進行查詢時也需要額外考慮分片鍵的使用,以提升查詢效率。其他的如事務的處理,由於分庫的關係,我們不再依賴事務,而是透過資料最終一致性,以及錯誤補償等方式進行處理。

未來資料庫的選型:MyCat+MySQL給資料庫運維增加了複雜性,而未來針對超大資料量級的應用,在硬體資源允許的情況下,可以考慮轉向如:TiDB這樣的NewSQL方案進行替代。

JVM最佳化:應用上線後,在高併發情況下曾偶發Long GC問題,透過分析dump檔案,最佳化記憶體使用,進行了解決。另外,對於記憶體變化較大的應用,也可以考慮使用jdk13,並開啟ZGC。

案例中透過redis快取服務配置資訊,每次服務響應時都需要讀取redis,這給redis造成了不小的壓力,透過引入Guava cache,在本地建立快取副本,並設定合理的失效時間,能夠顯著降低對redis的壓力。

透過應用框架實現低程式碼:在應用框架上的投資是非常值得的,透過將共性問題集中在應用框架中解決,可以在一定程度上實現低程式碼平臺的特性。開發人員也能更專注於業務邏輯的實現。

開發管理:透過讓每位開發人員充分理解應用框架,並形成解決同類問題的統一Pattern,能夠明顯提高開發效率,減少低質量程式碼的產生。