從一次 RPC 請求,探索 MOSN 的工作流程

SOFA 六週年,歡迎來玩

本週六 4.20 上海螞蟻集團 S 空間 掃碼免費報名活動,來與 MOSN 社區負責人線下交流~

img

MOSN 社區歡迎您的加入!
MOSN 官網:https://mosn.io/ MOSN Github:https://github.com/mosn/mosn

作者:王程銘(呈銘) 螞蟻集團技術工程師,Apache Committer

專注 RPC、Service Mesh 和雲原生等領域。

本文 7368 字,預計閱讀 15 分鐘

前言

MOSN(Modular Open Smart Network)是一款主要使用 Go 語言開發的雲原生網絡代理平臺,由螞蟻集團開源並經過了雙十一大促幾十萬容器的生產級驗證。

MOSN 爲服務提供多協議、模塊化、智能化、安全的代理能力,融合了大量雲原生通用組件,同時也可以集成 Envoy 作爲網絡庫,具備高性能、易擴展的特點。MOSN 可以和 Istio 集成構建 Service Mesh,也可以作爲獨立的四、七層負載均衡,API Gateway、雲原生 Ingress 等使用。

MOSN 作爲數據面,整體由 NET/IO、Protocol、Stream、Proxy 四個層次組成,其中:

  • NET/IO 用於底層的字節流傳輸
  • Protocol 用於協議的 decode/encode
  • Stream 用於封裝請求和響應,在一個 conn 上做連接複用
  • Proxy 做 downstream 和 upstream 之間 stream 的轉發

那麼 MOSN 是如何工作的呢?下圖展示的是使用 Sidecar 方式部署運行 MOSN 的示意圖,服務和 MOSN 分別部署在同機部署的 Pod 上, 您可以在配置文件中設置 MOSN 的上游和下游協議,協議可以在 HTTP、HTTP2.0 以及 SOFARPC 等中選擇。

img

以上內容來自官網 https://mosn.io

RPC 場景下 MOSN 的工作機制

RPC 場景下,MOSN 的工作機制示意圖如下:

img

我們簡單理解一下上面這張圖的意義:

  1. Server 端 MOSN 會將自身 ingress 的協議端口寫入到註冊中心;
  2. Client 端 MOSN 會從註冊中心訂閱地址列表,第一次訂閱也會返回全量地址列表,端口號是 Server 端 ingress 綁定的端口號;
  3. 註冊中心會實時推送地址列表變更到 Client 端(全量);
  4. Client 端發起 rpc 調用時,請求會被 SDK 打到本地 Client 端 MOSN 的 egress 端口上;
  5. Client 端 MOSN 將 RPC 請求通過網絡轉發,將流量通過負載均衡轉發到某一臺 Server 端 MOSN 的 ingress 端口處理;
  6. 最終到了 Server 端 ingress listener,會轉發給本地 Server 應用;
  7. 最終會根據原來的 TCP 鏈路返回。

全局視野下的 MOSN 工作流程

現在我們已經瞭解了 MOSN 的工作機制,那麼 MOSN 作爲 MESH 的數據面,是怎麼進行流量攔截並且將響應原路返回的呢?

1

爲了方便大家理解,我將以上時序圖內容進行拆分,我們一一攻破。

3.1 建立連接

MOSN 在啓動期間,會暴露本地 egress 端口接收 Client 的請求。MOSN 會開啓 2 個協程,分別死循環去對 TCP 進行讀取和寫處理。MOSN 會通過讀協程獲取到請求字節流,進入 MOSN 的協議層處理。

`// 代碼路徑 mosn.io/mosn/pkg/network/connection.go func (c *connection) Start(lctx context.Context) { // udp downstream connection do not use read/write loop if c.network == "udp" && c.rawConnection.RemoteAddr() == nil { return } c.startOnce.Do(func() { // UseNetpollMode = false if UseNetpollMode { c.attachEventLoop(lctx) } else { // 啓動讀/寫循環 c.startRWLoop(lctx) } }) }

func (c *connection) startRWLoop(lctx context.Context) { // 標記讀循環已經啓動 c.internalLoopStarted = true

utils.GoWithRecover(func() { // 開始讀操作 c.startReadLoop() }, func(r interface{}) { c.Close(api.NoFlush, api.LocalClose) }) // 省略。。。 } `

3.2 Protocol 處理

Protocol 作爲多協議引擎層,對數據包進行檢測,並使用對應協議做 decode/encode 處理。MOSN 會循環解碼,一旦收到完整的報文就會創建與其關聯的 xstream,用於保持 tcp 連接用於後續響應。

`// 代碼路徑 mosn.io/mosn/pkg/stream/xprotocol/conn.go func (sc *streamConn) Dispatch(buf types.IoBuffer) { // decode frames for { // 協議 decode,比如 dubbo、bolt 協議等 frame, err := sc.protocol.Decode(streamCtx, buf)

if frame != nil {
  // 創建和請求 frame 關聯的 xstream,用於保持 tcp 連接用於後續響應
  sc.handleFrame(streamCtx, xframe)
}

} }

func (sc *streamConn) handleFrame(ctx context.Context, frame api.XFrame) { switch frame.GetStreamType() { case api.Request: // 創建和請求 frame 關聯的 xstream,用於保持 tcp 連接用於後續響應,之後進入 proxy 層 sc.handleRequest(ctx, frame, false) } }

func (sc *streamConn) handleRequest(ctx context.Context, frame api.XFrame, oneway bool) { // 創建和請求 frame 關聯的 xstream serverStream := sc.newServerStream(ctx, frame) // 進入 proxy 層並創建 downstream serverStream.receiver = sc.serverCallbacks.NewStreamDetect(serverStream.ctx, sender, span) serverStream.receiver.OnReceive(serverStream.ctx, frame.GetHeader(), frame.GetData(), nil) }`

3.3 Proxy 層處理

proxy 層負責 filter 請求/響應鏈、路由匹配、負載均衡最終將請求轉發到集羣的某臺機器上。

downstream 部分

`// 代碼路徑 mosn.io/mosn/pkg/proxy/downstream.go func (s *downStream) OnReceive(ctx context.Context, headers types.HeaderMap, data types.IoBuffer, trailers types.HeaderMap) { s.downstreamReqHeaders = headers // filter 請求/響應鏈、路由匹配、負載均衡 phase = s.receive(s.context, id, phase) }

func (s *downStream) receive(ctx context.Context, id uint32, phase types.Phase) types.Phase { for i := 0; i <= int(types.End-types.InitPhase); i++ { s.phase = phase

switch phase {

// downstream filter 相關邏輯
case types.DownFilter:
  s.printPhaseInfo(phase, id)
  s.tracks.StartTrack(track.StreamFilterBeforeRoute)

  s.streamFilterChain.RunReceiverFilter(s.context, api.BeforeRoute,
    s.downstreamReqHeaders, s.downstreamReqDataBuf, s.downstreamReqTrailers, s.receiverFilterStatusHandler)
  s.tracks.EndTrack(track.StreamFilterBeforeRoute)

  if p, err := s.processError(id); err != nil {
    return p
  }
  phase++

// route 相關邏輯
case types.MatchRoute:
  s.printPhaseInfo(phase, id)

  s.tracks.StartTrack(track.MatchRoute)
  s.matchRoute()
  s.tracks.EndTrack(track.MatchRoute)

  if p, err := s.processError(id); err != nil {
    return p
  }
  phase++
  
// 在集羣中選擇一個機器、包含cluster和loadblance
case types.ChooseHost:
  s.printPhaseInfo(phase, id)

  s.tracks.StartTrack(track.LoadBalanceChooseHost)
  // 這裏很重要,在選中一個機器之後,這裏upstreamRequest對象有兩個作用
  // 1. 這裏通過持有downstream保持着對客戶端app的tcp引用,用來接收請求
  // 2. 轉發服務端tcp引用,轉發客戶端app請求以及響應服務端response時的通知
  s.chooseHost(s.downstreamReqDataBuf == nil && s.downstreamReqTrailers == nil)
  s.tracks.EndTrack(track.LoadBalanceChooseHost)

  if p, err := s.processError(id); err != nil {
    return p
  }
  phase++
}

} }`

upstream 部分

至此已經選中一臺服務端的機器,開始準備轉發。

`// 代碼路徑 mosn.io/mosn/pkg/proxy/upstream.go func (r *upstreamRequest) appendHeaders(endStream bool) {

if r.downStream.oneway { _, streamSender, failReason = r.connPool.NewStream(r.downStream.context, nil) } else { // 會使用 ChooseHost 中選中的機器 host 創建 sender,xstream 是客戶端的流對象 _, streamSender, failReason = r.connPool.NewStream(r.downStream.context, r) } }`

接下來會到達 conn.go 的 handleFrame 的 handleResponse 方法,此時 handleResponse 方法繼續調用 downStream 的 receiveData 方法接收數據。

//代碼路徑 mosn.io/mosn/pkg/stream/xprotocol/conn.go func (sc *streamConn) handleFrame(ctx context.Context, frame api.XFrame) { switch frame.GetStreamType() { case api.Response: // 調用 downStream 的 receiveData 方法接收數據 // 因爲 mosn 在轉發之前修改了請求id,因此會重新 encode 請求 sc.handleResponse(ctx, frame) } }

一旦準備好轉發就會通過 upstreamRequest 選擇的下游主機直接發送 write 請求,請求的協程此時會被阻塞。

`// 代碼路徑 mosn.io/mosn/pkg/stream/xprotocol/stream.go func (s *xStream) endStream() { defer func() { if s.direction == stream.ServerStream { s.DestroyStream() } }()

if log.Proxy.GetLogLevel() >= log.DEBUG { log.Proxy.Debugf(s.ctx, "[stream] [xprotocol] connection %d endStream, direction = %d, requestId = %v", s.sc.netConn.ID(), s.direction, s.id) }

if s.frame != nil { // replace requestID s.frame.SetRequestId(s.id) // 因爲 mosn 在轉發之前修改了請求 id,因此會重新 encode 請求 buf, err := s.sc.protocol.Encode(s.ctx, s.frame) if err != nil { log.Proxy.Errorf(s.ctx, "[stream] [xprotocol] encode error:%s, requestId = %v", err.Error(), s.id) s.ResetStream(types.StreamLocalReset) return }

tracks := track.TrackBufferByContext(s.ctx).Tracks

tracks.StartTrack(track.NetworkDataWrite)
// 一旦準備好轉發就會通過upstreamRequest選擇的下游主機直接發送 write 請求,請求的協程此時會被阻塞
err = s.sc.netConn.Write(buf)
tracks.EndTrack(track.NetworkDataWrite)
}

} } `

3.4 準備將響應寫回客戶端

接下來客戶端 xstream 將通過讀協程接收響應的字節流,proxy.go 的 OnData 方法作爲 proxy 層的數據接收點。

`// 代碼位置 mosn.io/mosn/pkg/proxy/proxy.go func (p *proxy) OnData(buf buffer.IoBuffer) api.FilterStatus { // 這裏會做兩件事 // 1. 調用 protocol 層進行decode // 2. 完成後通知upstreamRequest對象,喚醒downstream阻塞的協程 p.serverStreamConn.Dispatch(buf)

return api.Stop }

// 代碼位置 mosn.io/mosn/pkg/proxy/upstream.go func (r *upstreamRequest) OnReceive(ctx context.Context, headers types.HeaderMap, data types.IoBuffer, trailers types.HeaderMap) { // 結束當前stream r.endStream()

// 喚醒 r.downStream.sendNotify() }`

被喚醒處理收到的響應,重新替換回正確的請求 ID,並調用 protocol 層重新編碼成字節流寫回客戶端,最後銷燬請求相關的資源,流程執行完畢。

`// 比如我的 demo 是 dubbo 協議 func encodeFrame(ctx context.Context, frame *Frame) (types.IoBuffer, error) {

// 1. fast-path, use existed raw data if frame.rawData != nil { // 1.1 replace requestId binary.BigEndian.PutUint64(frame.rawData[IdIdx:], frame.Id)

// hack: increase the buffer count to avoid premature recycle
frame.data.Count(1)
return frame.data, nil

}

// alloc encode buffer frameLen := int(HeaderLen + frame.DataLen) buf := buffer.GetIoBuffer(frameLen) // encode header buf.WriteByte(frame.Magic[0]) buf.WriteByte(frame.Magic[1]) buf.WriteByte(frame.Flag) buf.WriteByte(frame.Status) buf.WriteUint64(frame.Id) buf.WriteUint32(frame.DataLen) // encode payload buf.Write(frame.payload) return buf, nil }`

總結

本文以工作中非常常見的一個思路爲出發點,詳細描述了 MOSN 內部網絡轉發的詳細流程,希望可以幫助小夥伴加深對 MOSN 的理解。

MOSN 是一款非常優秀的開源產品。MOSN 支持多種網絡協議(如HTTP/2, gRPC, Dubbo 等),並且能夠很容易地增加對新協議的支持;MOSN 提供了豐富的流量治理功能,例如限流、熔斷、重試、負載均衡等;MOSN 在性能方面進行了大量優化,比如內存零拷貝、自適應緩衝區、連接池、協程池等,這些都有助於提升其在高併發環境下的表現。此外在連接管理方面,MOSN 設計了多協議連接池;在內存管理方面,MOSN 在 sync.Pool 之上封裝了一層資源對的註冊管理模塊,可以方便的擴展各種類型的對象進行復用和管理。總的來說,MOSN 的設計體現了可擴展性、高性能、安全性以及對現代雲環境的適應性等多方面的考慮。

對於開發者來說,深入研究 MOSN 的代碼和架構,無疑可以學到很多關於高性能網絡編程和雲原生技術的知識。MOSN 社區歡迎您的加入!

2

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章