博客 / 詳情

返回

無服務器開發實例|微服務向無服務器架構演進的探索

在當今的技術環境中,許多組織已經從構建單一的應用程序轉變為採用微服務架構。微服務架構是將服務分解成多個較小的應用程序,這些應用程序可以獨立開發、設計和運行。這些被拆分的小的應用程序相互協作和通信,為用户提供全面的服務。在設計和部署微服務應用時利用無服務器計算和無服務器架構,可有效解決微服務架構本身存在的複雜性、模塊間過度依賴以及系統可擴展性有限等難題。本文將以 FreeWheel 的 AD Debug Service 為例,探索從現有微服務架構過渡到無服務器架構過程中解決問題的技術實踐。AD Debug Service 是為用户提供的對 FreeWheel 廣告服務器決策進行診斷和深入瞭解的工具。

亞馬遜雲科技開發者社區為開發者們提供全球的開發技術資源。這裏有技術文檔、開發案例、技術專欄、培訓視頻、活動與競賽等。幫助中國開發者對接世界最前沿技術,觀點,和項目,並將中國優秀開發者或技術推薦給全球雲社區。如果你還沒有關注/收藏,看到這裏請一定不要匆匆劃過,點這裏讓它成為你的技術寶庫!

如下圖所示,Freewheel 的核心業務團隊放棄了最初構建單一、龐大的單體應用程序的方法,轉而採用微服務架構。

圖片
與 Freewheel 的眾多微服務一樣,AD Debug Service 也是採用 gRPC 框架創建 gRPC API,並通過 gRPC-Gateway 同時對外提供 RESTful API。AD Debug Service 的整體流程大致如下圖所示。為了更容易理解後續的遷移步驟,我們先仔細瞭解一下 AD Debug Service 的工作流程 :

  1. 當用户的診斷請求到達 AD Debug Service 的 gRPC-Gateway 時,它將 JSON 數據解析為 Protobuf 消息;
  1. gRPC-Gateway 使用解析的 Protobuf 消息發出正常的 gRPC 客户端請求;
  1. gRPC 客户端將二進制格式的 Protobuf 發送到 gRPC 服務器;
  1. gRPC 服務器處理請求,此時會真正執行業務代碼,並且通過 gRPC 或 HTTP 模式調用其他微服務(其中包括調用 Freewheel 的 AD Debug Service 並獲取廣告決策結果);
  1. gRPC 服務器以 Protobuf 二進制格式返回響應給 gRPC 客户端,客户端將其解析為 Protobuf 消息並返回到 gRPC-Gateway;
  1. 最後 gRPC-Gateway 將 Protobuf 消息編碼為 JSON 並將其返回給原始客户端。

圖片

 https://grpc.io/blog/coreos/?trk=cndc-detail

通過上面的流程圖我們可以看出來,AD Debug Service 與其他微服務不同之處在於,它實際上只有 RESTful API 會被訪問,gRPC API 則只用於處理 HTTP 請求,因此從這裏我們也可以看出來,AD Debug Service 的遷移不會影響到別的 gRPC 的微服務。

1 向無服務器改造和遷移的原因

1.1 現有服務存在的問題

  • 伸縮粒度粗: 微服務中各個子業務的不同接口,QPS 差距較大,對擴展的訴求完全不同,升級的頻率也可能不同;進一步拆分的話則會使微服務數量提升一個數量級,進一步增加基礎設施管理的負擔;
  • 成本高: 每個微服務都要考慮冗餘,保證高可用。隨着微服務數量的增加,基礎設施的數量會呈現指數級增長,但云服務的基礎設施收費方式沒有改變,依然採用按照資源大小及以小時為單位計費的方式;以容器為基礎的微服務基礎設施在彈性等方面仍有不足。

1.2 Serverless 可以幫助解決現有問題

開發者實現的服務器端應用邏輯(微服務甚至粒度更小的服務)以事件驅動的方式運行在無狀態的臨時容器中,這些容器和計算資源完全由雲提供商管理,開發者只需關心和維護業務層面的正常運行,其他部分如運行時、容器、操作系統、硬件等,都由雲提供商來解決。

總體來説,Serverless 有以下優勢:

  • 免運維: 不需要管理服務器主機或者服務器進程;監控以確保服務仍然運行良好;自動系統升級,包括安全修補;
  • 彈性伸縮: 雲平台負責負載平衡和請求路由以有效利用資源,並且根據負載進行自動規模伸縮與自動配置;
  • 按需付費: 根據使用情況決定實際成本;
  • 高可用: 可用性冗餘,以便單個機器故障不會導致服務中斷;具備隱含的高可用性。

1.3 選擇 AD Debug Service 實現無服務器架構改造和遷移

在我們現有的業務場景中,AD Debug Service 是一個典型的用完即走的業務場景,而且其工作內容主要是與 交互,獲取廣告投放的決策信息,並調用多個其他微服務獲取相關業務數據信息,整合好返回給前端客户用於展示,每個月接收的請求不到 10 萬條,並且,用户對於請求的響應速度需求不高,也不對外暴露 因此對比於部署在 EKS 的 Cluster 中,分配固定的 pod 資源,Serverless 中的 FaaS 是一種更適合它的模式。

2 技術方案選型

Amazon Lambda 是亞馬遜雲科技在 2014 年推出的 Serverless 計算服務, 允許開發人員構建和運行應用程序而無需管理服務器。在 Lambda 裏,使用函數(Function)來存儲代碼,供 Lambda 調用,因此,Lambda 類的無服務器計算服務也被稱作函數即服務(Function-as-a-Service / FaaS)。

Freewheel 一直以來都是使用的亞馬遜雲科技提供的雲服務,所以後續我會以亞馬遜雲科技提供的服務為例來介紹我們從現有微服務架構遷移到 Serverless 的過程。Amazon Lambda 因為與其他雲上服務深度集成而更快速並且更安全,因此我們選用 FaaS 模式以 Amazon Lambda 作為核心來遷移我們的微服務。

亞馬遜雲科技為 Lambda 提供高可用性的計算基礎設施以及所有的所有管理工作,我們只需將我們的業務代碼使用 Lambda 支持的語言運行系統(我們選擇的是 Go 語言)組織到 Lambda 函數即可。

亞馬遜雲科技目前為止提供了 3 種可以同步調用 Lambda 的方式:

方式1:採用 Amazon API Gateway 與 Lambda 集成的方式

方式2:採用 Application Load Balancer 與 Lambda 集成的方式

方式3:直接使用 Lambda Function URL

2.1 Amazon API Gateway + Lambda

亞馬遜雲科技支持使用 API Gateway 為 Lambda 函數創建帶有 HTTP 端點的 Web API。Amazon API Gateway 是一項完全託管的服務,可幫助開發人員輕鬆創建、發佈、維護、監控和保護任何規模的 API。使用 API  Gateway 可以構建 REST API 和 HTTP API 兩種類型的 RESTful API,同時也支持創建 WebSocket API,使得客户端可以通過 HTTP 和 WebSocket 協議連接到應用程序。API Gateway 還提供了一些高級功能,例如授權和訪問控制、緩存、請求轉發等服務。API Gateway 的架構圖如下所示:

圖片

2.2 Application Load Balancer + Lambda

Elastic Load Balancing 支持 Lambda 函數作為 Application Load Balancer 的目標。使用負載均衡器規則,基於路徑或標頭值將 HTTP 請求路由到一個函數。處理請求並從 Lambda 函數返回 HTTP 響應。

我們需要將 Lambda 函數註冊到 Target Group,並在 ALB 中將偵聽器規則配置為將請求轉發到 Lambda 函數的 Target Group。當負載均衡器將請求轉發到 Target Group 並使用 Lambda 函數作為目標時,它會調用 Lambda 函數並以 JSON 格式將請求內容傳遞到 Lambda 函數。

ALB 在應用層進行操作,將 HTTP 請求路由到多個後端資源,這些後端資源包括 Amazon Lambda 函數。ALB 通常用於暴露一個公共的 Web 地址端點,其資源則是託管在 AmazonVPC 的私有子網中;同時也支持只能從組織內部網絡訪問的私有 ALB。

圖片

但是使用 Target Group 本身存在一些限制:

  • Lambda 函數和目標組必須位於同一賬户中,且位於同一 Region 中;
  • 可以發送到 Lambda 函數的請求正文的最大大小為 1 MB;
  • Lambda 函數可以發送的響應 JSON 的最大大小為 1 MB;
  • 不支持 WebSocket;
  • 不支持 Local Zones(Amazon Web Services Local Zones 是亞馬遜雲科技提供的基礎設施部署的一種形式,可將計算、存儲、數據庫和其他某些初級服務放置在更靠近大量人口聚居的位置)。

2.3 Lambda Function URL

Lambda Function URL 是 Lambda 函數的專用 HTTP(S) 端點。創建函數 URL 時,Lambda 會自動生成唯一的 URL 端點。創建函數 URL 後,其 URL 端點永遠不會改變。函數 URL 支持跨源資源共享(CORS)配置選項。並且 Function URL 可以支持兩種方式返回響應體:BUFFERED 和 RESPONSE_STREAM。

BUFFERED 是一種默認的響應方式:當選擇這種方式返回響應時,只有當負載完成時,調用結果才會被返回,並且響應負載的限制為 6 MB。

RESPONSE_STREAM 方式則會將響應負載流式傳輸回客户端。響應流式處理可在部分響應可用時就將其發送回客户端。因此,我們可以使用 RESPONSE_STREAM 方式處理來構建返回較大負載的函數。響應流負載的軟限制為 20MB。響應的前 6MB 的帶寬無上限,超過 6M 之後的最大處理速率是 2MBps,並且流式處理響應會產生費用,超過 6M 的部分按照 0.008USD/GB 計算。

值得注意的是,Lambda Function URL 只能通過公共網絡訪問,截止到 2023 年 9 月,它並不支持 Amazon PrivateLink,保護 Lambda 函數 URL 的方式是選擇 Amazon IAM 授權,或者使用基於資源的策略進行安全和訪問控制。

2.4 三種方式優缺點對比

我們從以下幾個方面簡單對比一下 API GW,ALB 和 Function URL:

圖片

從上對比來看,如果有負載的硬性要求(大於 6M),那最好選擇 Function URL 的 RESPONSE_STREAM 響應方式;如果需要支持私有訪問,那最好選擇 API GW 或者 ALB 的方式。

因為 Freewheel 對於安全的監管嚴格,我們需要使用統一的登錄鑑權方式訪問公司服務,所以不推薦使用 Function URL 的方式;同時鑑於公司已經有實例在使用 ALB,如果我們共享這個 ALB,只在該 ALB 上加入 AD Debug Service 的 Target Group,那麼我們可以將 ALB 本身按時間的費用忽略不計。

所以針對 AD Debug Service,我們選擇通過 ALB + Lambda 的方式實現從微服務到 Serverless 的遷移。

3 代碼遷移的過程

AD Debug Service 的調用流程如圖:用户端的 RESTful 請求經過 gRPC-Gateway 代理轉化為 gRPC 請求,調用 gRPC 客户端,客户端調用業務方法最終返回響應。

在改造的過程,我們可以保證進行最少的改動,保持其中業務邏輯部分不變,將下圖藍色部分全部替換。

圖片

雖然 Freewheel 已經支持通過內部的 Serverless LCDP 平台創建 ALB+Lambda,但是本文中,我將通過控制枱操作的方式演示所有流程。

圖片

3.1 Step1 創建 Lambda

  1. 打開 Lambda 控制枱的 Functions page
  1. 選擇 Create function
  1. 選擇 Author from scratch
  1. 輸入函數名稱: ad_debug
  1. Runtime 選擇 Go 1.x
  1. 在 Execution Role(執行角色)中,選擇 Create a new role with basic Lambda permissions(創建具有基本 Lambda 權限的新角色)

創建完空的 Lambda 之後,我們需要將我們的包上傳並部署。上面説到我們的服務是基於 gRPC 的,而且保持業務處理部分的代碼不變,我們的業務處理 Handler 基本上類似於:

func (h *BizHandler) BizExec(ctx context.Context, req *proto.BizRequest) (*proto.BizRequestResponse, error) {   
    return h.bizDomain.BizExec(ctx, req)
}

我們現在要解決的問題主要有兩個:如何進行內部路由,以及怎麼處理 TargetGroup event 與 proto 結構體的相互轉化。這兩部分所解決的就是上圖藍色框內的內容。類似於 gRPC-Gateway 代理,我們針對每個 BizHandler 在外部做一個 Wrapper,其中 Wrapper 做的事情包括,註冊 Handler,轉化請求體。

內部的路由

我們通過 Register 方法,把所有方法的 Method,Pattern 和 HanlerFunc 註冊到數組裏,當請求來時,我們通過匹配數組裏的 Method 和 Pattern 來決定調用哪個 Pattern。其中 PathMatchAndParse 會根據正則匹配 path,可以根據自己的需求定製,這裏不展開贅述。

// Register
func (controller *Controller) RegisterEndpoint(method string, pattern string, handleFunc func(ctx context.Context, req *event.ALBTargetGroupRequest) (events.ALBTargetGroupResponse,error)) {
    controller.routes = append(controller.routes, &route{
        pattern:    pattern, 
        method:     method,   
        handleFunc: handleFunc,   
    })
}


// Handle
func (controller *Controller) Handle(ctx context.Context, req *events.ALBTargetGroupRequest) (resp events.ALBTargetGroupResponse, err error) { 
    var isMatch = false  
    var fn DomainFunc  
    for _, route := range controller.routes {  
        m, vars, err := PathMatchAndParse(req.Path, route.pattern)    
        if err != nil {    
            return ResponseInternalServerError()  
        }    
        if m && req.HTTPMethod == route.method { 
            isMatch = true  
            fn = route.handleFunc  
            break    
        } 
    }  
    if !isMatch {   
        return ResponseMethodNotAllowed() 
    }  
    return fn(ctx, req)
}

TargetGroup event 與 proto 結構體的轉化

這部分比較簡單,可以直接使用 grpc-gateway 包裏的 JSONPb。但是處理把 proto 結構體轉化到 json 作為響應體這部分,我們可以抽取到一個公共的 interceptor:

var jsonPb = &runtime.JSONPb{}
func Interceptor(ctx context.Context, req *events.ALBTargetGroupRequest, bizHandler func(ctx context.Context, req *events.ALBTargetGroupRequest) (interface{}, error)) (resp events.ALBTargetGroupResponse, err error) {  
    res, err := bizHandler(ctx, req) 
    if err != nil { 
        return   
    }  
    resBytes, err := jsonPbEmitDefaults.Marshal(res)  
    if err != nil {   
        return  
    }   
    resp = events.ALBTargetGroupResponse{   
        StatusCode: 200,   
        Body:       string(resBytes), 
        Headers: map[string]string{   
            "Content-Type": "application/json",    
        }, 
    } 
    return
}


func BizExec(ctx context.Context, req *events.ALBTargetGroupRequest) (resp interface{}, err error) {   
    var adReq proto.BizRequest 
    err = jsonPb.Unmarshal([]byte(req.Body), &adReq) 
    if err != nil {  
        return events.ALBTargetGroupResponse{}, err 
    }   
    res, err := bizHandler.BizExec(ctx, &adReq) 
    if err != nil {  
        return events.ALBTargetGroupResponse{}, err  
    }  
    return bizHandler.BizExec(ctx, &adReq)
}

所以,最終 Lambda 的 main 函數類似於:

var controller *Controller
func init() { 
    controller = NewController()   
    
    // Register endpoints 
    controller.RegisterEndpoint("POST", "/exec", Interceptor(BizExec)) 
    controller.RegisterEndpoint(...) 
    ...
}


func main() { 
    lambda.Start(controller.Handle)
}

完成 Lambda 的函數體之後,我們可以通過以下命令創建 Lambda 的 zip 包,選擇上面創建好的 Lambda,點擊 Code 頁面,選擇 Upload from.zip file 部署到 Amazon Web Service 上:

GOOS=linux GOARCH=amd64 go build -o bin/ad_debug_service main.go
cd bin && zip -r ad_debug_service.zip .

3.2 Step2 創建 Target Group

  1. 通過以下網址打開 Amazon EC2 控制枱:
    https://console.aws.amazon.com/ec2/?trk=cndc-detail
  1. 在導航窗格上的 LOAD BALANCING(負載均衡) 下,選擇 Target Groups(目標組)
  1. 選擇 Create target group(創建目標組)
  1. 選擇目標類型,選擇 Lambda 函數
  1. Target group name,鍵入目標組的名稱
  1. 選擇 Next(下一步)
  1. 指定單個 Lambda 函數為上面我們創建的 Lambda
  1. 選擇創建目標組

3.3 創建 ALB 並設置 Listener

  1. 通過以下網址打開 Amazon EC2 控制枱:
    https://console.aws.amazon.com/ec2/?trk=cndc-detail
  1. 在導航窗格中,選擇負載均衡器
  1. 選擇創建負載均衡器並選擇類型為 Application Load Balancer
  1. Scheme 選擇 internal,並且 sg 和 VPC 保持跟 Lambda 的一致
  1. 在 Listeners 選項卡上,選擇 Add listener(添加偵聽器)
  1. 對於協議:端口,選擇 HTTP 並保留默認端口
  1. 對於 Default actions (默認操作),選擇轉發,然後選擇上面創建的目標組
  1. 選擇 Add

3.4 Step4 將 ALB 註冊到 Gateway

因為我們創建的 ALB 是私有的,如果想要外部用户訪問的話,那我們需要將這個 ALB 的 DNS 註冊到我們對外的 gateway 上,這個 Gateway 也幫我們完成用户登錄驗證的工作。

4 代碼改造過程中解決的問題

4.1 超大負載處理

上述提到 AD Debug Service 會請求 AD Decision Service 來獲取廣告投放的相關調試信息,所以返回的數據量會比較大(一般會超過 1M),而在上面的介紹裏我們也提到了使用 TargetGroup 會有 1M 負載的限制,並且,我們瞭解到響應體的大小也會影響到最後費用的計算,所以針對怎麼去處理這些超大的負載(大於 1M),我們探討了幾個方案。

壓縮

對於如何減少負載的大小,壓縮是一個很好的方式,針對這個方法,我們考慮了從輸入和輸出兩個方向上的負載的壓縮。

壓縮影響體

針對輸出做壓縮的方案很常見,我們要做的是讓其適配 ALB+Lambda 的這種模式。我們依然採用 go 自帶的 compression/gzip 包,稍微做了一些調整。

因為 TargetGroup 的響應只支持 base64 的壓縮,所以我們使用 encoding/base64 包作為 gzip 的寫入,並且切記需要將響應體的 IsBase64Encoded 設置為 true。壓縮流程可以概括為:

圖片

我們可以簡單看一下應用到 AD Debug Service 的壓縮效果:

圖片

壓縮請求體

除了需要關注輸出之外,我們同樣需要關注輸入也就是請求體的大小。特別是在 AD Debug Service 中存在需要上傳文件的場景,所以針對上傳文件的請求體,我們也做了壓縮處理。

在前端,我們支持上傳 csv 文件,所以在上傳文件的時候,我們選擇去壓縮文件的內容,壓縮使用到的包是 react-zlib-js,在上傳文件的時候,創建一個 FileReader 讀取文件內容,然後通過壓縮包裏的 gzipSync 方法將文件內容進行 gzip 壓縮,最後將壓縮後的文件內容上傳到後端:

import { gzipSync } from 'react-zlib-js';


const processFileInCompress = (uploadFile File) => {  
    const rd = new FileReader(); 
    rd.readAsBinaryString(uploadFile);  
    rd.onload = content => { 
        const conRes = content?.target?.result;  
        const bf = gzipSync(conRes);   
        const gzc = new File([bf], uploadFile.name);  
        uploadCompressedFile(gzc); 
    };
};

在後端,我們接收到壓縮後的文件內容,需要進行解壓縮。需要注意的是,通過 ALB 傳輸的數據如果 Content-Type 是 multipart/form-data 類型,那麼都會自動進行 base64 編碼處理,因此我們在獲取響應體之後,需要先進行 base64 解碼,獲得解碼之後的 body,然後進行 gzip 解壓縮。

上傳 S3

壓縮的方式在很大程度上解決了 1M 的限制,但是並不能從根本上解決問題。其實,除了上述所説的壓縮的方式來解決 ALB 對於負載大小的限制之外,我們也可以通過將請求體上傳 S3,後端從 S3 下載請求體,以及將響應體上傳 S3,前端通過 S3 下載響應體的方式來解決這個問題。

例如在 AD Debug Service 中,我們將響應體中佔比較大的 AD Decision 的結果,上傳到 S3,然後將 S3 的下載鏈接返回,同時,在前端接收到響應體之後,再根據響應體中的 S3 的鏈接去下載相應的內容。

這個時候需要注意的一點是,一定要允許 S3 的跨域訪問(CORS)。

[   
    {     
        "AllowedHeaders": [    
            "*"     
        ],    
        "AllowedMethods": [      
            "GET"    
        ],      
        "AllowedOrigins": [      
            "*"   
        ],      
        "ExposeHeaders": []  
    }
]

4.2 處理 IncomingContext

在使用 gRPC 架構中 garpc-gatway 代理會通過 AnnotateContext 的方法把請求頭裏的 key/value 對轉化成 context 裏的 metadata,然後透傳到整個服務的各個函數中,而 ALB 並不會特殊處理請求,所以我們需要在 Lambda 裏提供一個 interceptor,能夠幫我們根據請求頭生成新的 context。

func WithIncomingHeaderMatcher(next ALBFunc) ALBFunc {  
    return func(ctx context.Context, req events.ALBTargetGroupRequest) (resp events.ALBTargetGroupResponse, err error) {   
        m := map[string]string{}   
        for k, v := range req.Headers {   
            key, t := matcher(k)      
            if t {       
                m[key] = v    
            }  
        }     
        md := metadata.New(m)    
        ctx = metadata.NewIncomingContext(ctx, md)   
        return next(ctx, req)  
    }
}

4.3 處理 URL 中查詢字符串的解碼

同樣的,我們也希望可以通過 interceptor 的方式幫我們統一處理 URL 中查詢字符串的轉譯問題。需要 url.QueryUnescape() 方法對請求裏的 QueryStringParameters 進行轉義操作。

4.4 個性化處理響應 Error

如果我們不對 bizHandler 的 error 做處理的話,最終通過 ALB 的響應體返回的 error 將會是個普通文本的類型。但是,原先我們基於 grpc-gateway 的模式產生的 error,都是做過了處理的,也就是最後的返回 error 也會是 json 格式,方便前端處理,因此,為了減少改動,也為了可讀性,我們可以將 error 做個性化處理。

errMsg := fmt.Sprintf("{\"message\": \"%s\"}", err.Error())
return events.ALBTargetGroupResponse{ 
    StatusCode: 500,  
    Headers: map[string]string{  
        header.ContentType: "application/json", 
    },  
    Body: errMsg,
}

4.5 配置其他依賴的 Endpoint

因為我們將原本屬於某個集羣的微服務遷移到了 Lambda 上,而這個服務還需要通過 RESTful 方式或者 gRPC 的方式調用原來集羣中的其他微服務,那麼我們必須配置被調用服務的 Endpoint,並且需要保證這些微服務可以允許集羣外的訪問。

5 流量遷移

5.1 Metrics 收集

如果服務中涉及到 Prometheus Metrics 的收集,並且在原來 gRPC 服務中採取的是 pull 的方式拉取 metrics 的值,那現在我們必須更新到採用 push 的方式將 metrics 推送到 Pushgateway,然後 Prometheus 通過 pull 的方式去 Pushgateway 拉取 metrics 的值,這是因為 Lambda 是一個用完即銷燬的運行模式,我們不能保證 Prometheus 能夠在 Lambda shutdown 之前來拉取 metrics,所以我們必須採用主動推送的方式更新。

我們使用 push 方式收集 metrics 的具體步驟如下:

  • 首先我們必須有自己運行的 Pushgateway,暴露一個 Lambda 可以訪問的 URL
  • 然後在 Lambda 中我們定義一個 prometheus 包裏的 pusher,使用 Pushgateway 的 URL,一個唯一的名字,以及訪問這個 URL 使用到的用户名和密碼,初始化這個 pusher
  • 初始化一個 Collector,例如 CounterVec
  • 將該 Collector 添加到 pusher 中
  • 最後,可以在 Lambda 銷燬之前,調用 Push() 方法將 metrics 推送到 gateway 上
var Pusher *push.Pusher
func init() {  
    Pusher = push.New(pqmURL, pqmJob).BasicAuth(pqmUser, pqmPasswd).Collector(metricCounter)
}
var metricCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ 
    Namespace: nameSpace,  
    Name:      "metric_name", 
    Help:      "Total number of metrics",
},  
    []string{"metric_1", "metric_2"},
)


func CollectTotalMetrics(metric1, metric2 string) { 
    metricCounter.WithLabelValues(metric1, metric2).Inc()   
    if err := Pusher.Push(); err != nil {  
        bizlog.Errorf("Push metrics: %v error: %s", metricCounter, err) 
    }
}

5.2 日誌收集

日誌的收集可以直接使用 Amazon CloudWatch,也可以使用自定義的方式。我們可以使用 Lambda 的外部擴展的功能,將日誌收集進程運行在外部擴展中,隨着 Lambda 的運行,外部擴展也會開始運行,將日誌收集到遠端服務,隨着 Lambda 的停止,外部擴展也會隨之停止

5.3 流量遷移

上述部分都確認之後,我們可以開始流量的遷移,流量切換的安排必須保證順利上線且風險可控。

圖片

Step1 雙寫

首先,我們需要保證所有請求在 gRPC 服務和 Serverless 服務返回的結果是一致的,所以我們需要對於同一個請求獲取兩份響應結果。而 AD Debug Service 的特別之處在於對於同一個請求,不同時刻的響應結果是不一樣的,所以我們需要儘可能同一時間發出兩個請求才獲得來自兩個終端的結果才能做比對。

因此,我們上線了“雙寫“方案,也就是客户的頁面依然是 gRPC 版本的 AD Debug Service 來提供服務,但是會同時發一個請求給 Serverless 端,用來對同樣的請求在“儘可能同一時刻“提供服務,來方便校驗。

Step2 遷移測試用户

先將所有測試用户的流量切換到 Serverless 服務,觀察一段時間,可以暴露一些問題,如果運行順暢,那麼我們進行到下一階段。

Step3 遷移所有用户

將所有用户的流量遷移到 Serverless 服務,但是保留 gRPC 版本的 AD Debug Service 服務作為備選。

在這個階段,我們採用了新的 A/B 測試策略,一旦出現緊急問題,可以通過用户參數(通過 ENG 控制的)操作來切換到原來的服務。

整個頁面默認使用 Serverless 服務,但如果想切換到的原來版本的 AD Debug Service,可以通過更新用户的某個參數,然後重刷頁面即可。

Step4 gRPC 版本服務

將 gRPC 版本的 AD Debug Service 服務下線。

總結

經過上述的一系列過程之後,我們可以將 gRPC 微服務逐步遷移到 Serverless 服務,並且通過一段時間的監控,我們發現響應時間上的差異在我們可接受的範圍之內。以下是記錄的某一個用户的某一種請求在近 3 天之內兩種響應時長的對比,左邊是 Serverless 模式下的響應時間,右邊的 gRPC 模式下的響應時間:

圖片

P95 的請求響應時長如下圖所示:

圖片

可以看出,Serverless 的響應時長可能會比 gRPC 服務的響應時長多幾秒,導致這種情況的很大一部分原因在於,前面我們也提到過,我們的 AD Debug Service 會調用很多別的服務獲取相應的結果,而我們的其他服務現階段還在 EKS 的集羣中,這裏的調用時長勢必會比 gRPC 的 AD Debug Service 在 EKS 裏直接調用要更長一些,等後續別的服務陸續遷移之後,響應時長應該也會有所減少。

並且也記錄到我們每次重啓 Lambda 的時候用來初始化應用所用到時間也是很短的,基本都在 10ms 以內完成:

圖片
同時也可以監控到這段時間內的費用對比如下:

圖片

注:因為我們的 Domain Service 部署在一個 EKS 集羣內,因此沒法給每個 pod 單獨計費,所以 AD Debug Service 部分的花費是根據 pod 的平均花費做的估計。

從上面表格可以看出來,遷移到 Serverless 之後的花費減少了 99% 以上。

圖片

文章來源:
https://dev.amazoncloud.cn/column/article/6551e333a0321109d83179c3?sc_medium=regulartraffic&sc_campaign=crossplatform&sc_channel=SF

user avatar u_16213317 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.