偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

Go并發(fā)編程 — I/O聚合優(yōu)化(動畫講解)

開發(fā) 前端
在存儲系統(tǒng)中,在確保功能不受損的前提下,盡量的減少讀寫I/O的次數(shù)是優(yōu)化的一個重要方向,也就是聚合I/O的場景。讀寫操作雖然都有聚合I/O的需求,但各自的重點和實現(xiàn)方法卻有所不同。接下來,我們將分別探討讀和寫請求的聚合優(yōu)化方法。

背景提要

在存儲系統(tǒng)中,在確保功能不受損的前提下,盡量的減少讀寫I/O的次數(shù)是優(yōu)化的一個重要方向,也就是聚合I/O的場景。讀寫操作雖然都有聚合I/O的需求,但各自的重點和實現(xiàn)方法卻有所不同。接下來,我們將分別探討讀和寫請求的聚合優(yōu)化方法。

讀請求的聚合

以讀操作中,緩存優(yōu)化是一種常見的優(yōu)化手段。具體做法是將讀取的數(shù)據(jù)存儲在內存中,并通過一個唯一的Key來索引這些數(shù)據(jù)。當讀請求來到時,如果該Key在緩存中沒有命中,那么就需要從后端存儲獲取。用戶請求直接穿透到后端存儲,如果并發(fā)很大,這可能是一個很大的風險。

例如,對于 Key:“test”,如果緩存中沒有相應的數(shù)據(jù),并且突然出現(xiàn)大量并發(fā)讀取請求,每個請求都會發(fā)現(xiàn)緩存未命中。如果這些請求全部直接訪問后端存儲,可能會給后端存儲帶來巨大壓力。

為了應對這種情況,我們其實可以只允許一個讀請求去后端讀取數(shù)據(jù),而其他并發(fā)請求則等待這個請求的結果。這就是讀請求聚合的基本原理。

在Go語言中,可以使用singleflight 這類第三方庫完成上述需求。singleflight的設計理念是“單一請求執(zhí)行”,即針對同一個Key,在多個并發(fā)請求中只允許一個請求訪問后端。

01 - 讀請求聚合的使用姿勢

下面是一個使用 singleflight 的示例,展現(xiàn)了如何通過傳入特定的Key和閉包函數(shù)來聚合并發(fā)請求。

package main

import (
  // ...
 "golang.org/x/sync/singleflight"
)

func main() {
   var g singleflight.Group
   var wg sync.WaitGroup

   // 模擬多個 goroutine 并發(fā)請求相同的資源
   for i := 0; i < 5; i++ {
      wg.Add(1)
      go func(idx int) {
          defer wg.Done()
          v, err, shared := g.Do("objectkey", func() (interface{}, error) {
              fmt.Printf("協(xié)程ID:%v 正在執(zhí)行...\n", idx)
              time.Sleep(2 * time.Second)
              return "objectvalue", nil
          })
          if err != nil {
              log.Fatalf("err:%v", err)
          }
          fmt.Printf("協(xié)程ID:%v 請求結果: %v, 是否共享結果: %v\n", idx, v, shared)
      }(i)
   }
   wg.Wait()
}

在這個例子中,多個Goroutine并發(fā)地請求Key為“objectkey”的資源。通過singleflight,我們確保只有一個Goroutine去執(zhí)行實際的數(shù)據(jù)加載操作,而其他請求則等待這個操作的結果。接下來,我們將探討 singleflight 的原理。

02 - singleflight的原理

singleflight 庫提供了一個Group結構體,用于管理不同的請求,意圖在內部實現(xiàn)聚合的效果。定義如下:

type Group struct {
   mu sync.Mutex       // 互斥鎖,包含下面的映射表
   m  map[string]*call // 正在執(zhí)行請求的映射表
}

Group結構的核心就是這個map結構。每個正在執(zhí)行的請求被封裝在 call 結構中,定義如下:

type call struct {
   wg sync.WaitGroup // 用于同步并發(fā)的請求
   val interface{}   // 用于存放執(zhí)行的結果
   err error         // 存放執(zhí)行的結果
   dups  int         // 用于計數(shù)聚合的請求
    // ...其他字段用于處理特殊情況和提高容錯性
}

Group結構的Do方法實現(xiàn)了聚合去重的核心邏輯,代碼實現(xiàn)如下所示:

func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
   g.mu.Lock()
   if g.m == nil {
      g.m = make(map[string]*call)
   }
   // 用 map 結構,來判斷是否已經(jīng)有對應 Key 正在執(zhí)行的請求
   if c, ok := g.m[key]; ok {
      c.dups++
      // 如果有對應 Key 的請求正在執(zhí)行,那么等待結果即可。
      g.mu.Unlock()
      c.wg.Wait()
      // ...
      return c.val, c.err, true
   }
   // 創(chuàng)建一個代表執(zhí)行請求的結構,和 Key 關聯(lián)起來,存入map中
   c := new(call)
   c.wg.Add(1)
   g.m[key] = c
   g.mu.Unlock()
   g.doCall(c, key, fn) // 真正執(zhí)行請求
   return c.val, c.err, c.dups > 0
}

func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
    defer func() {
      // ...省略異常處理
      c.wg.Done()
    }()
    func() {
        // 真正執(zhí)行請求
         c.val, c.err = fn()
    }()
    // ...
}

通過上述代碼,singleflight的Group結構體利用map記錄了正在執(zhí)行的請求,關聯(lián)了請求的Key和執(zhí)行體。當新的請求到來時,先檢查是否有相同Key的正在執(zhí)行的請求,如果有,則等待起結果,從而避免重復執(zhí)行相同的請求。

動畫示意圖:

圖片圖片

對于讀操作,singleflight通過這種方式有效地減少了重復工作。然而,對于寫操作,處理邏輯會有所不同,它需要額外的機制來保證數(shù)據(jù)落盤的時序。

寫請求的聚合

我們先回憶一下寫操作的姿勢。首先通過Write系統(tǒng)調用來寫入數(shù)據(jù),默認情況下此時數(shù)據(jù)可能僅駐留在PageCache中,為了確保數(shù)據(jù)安全落盤,此時我們需要手動調用一次 Sync 系統(tǒng)調用。

然而,Sync操作的成本相當大,并且它除了數(shù)據(jù),還會同步元數(shù)據(jù)等其他信息到磁盤上。對于性能影響巨大。并且,在機械盤的場景下,串行化的執(zhí)行Sync是更好的實踐。

因此,我們面臨的一個問題是:如果在不犧牲數(shù)據(jù)安全性的前提下,能否減少Sync的次數(shù)呢?

對于同一個文件的寫操作,合并Sync操作是可行的。

文件的Sync會將當前時刻文件在內存中的全部數(shù)據(jù)一次性同步到磁盤。無論之前執(zhí)行過多少次Write調用,一次Sync就能全部刷盤。這正是聚合寫請求以優(yōu)化性能的關鍵所在。

01 - 寫聚合的原理

假設對同一個文件寫了三次數(shù)據(jù),每一次都是Write+Sync的操作。那么在合適的時機,三次Sync調用可以優(yōu)化成一次。如下圖所示:

圖片圖片

請求 C 的 Sync 操作是在所有請求的 Write 之后才發(fā)起的,所以它必定能保證在此之前的所有變更的數(shù)據(jù)都安全落盤。這就是寫操作聚合的根本原理。

接下來我們來思考兩個問題。

問題一:有童鞋可能會問,讀寫聚合優(yōu)化感覺有一點相似?那能否用 singleflight 聚合寫操作呢?

例如,當并發(fā)調用 Sync 的時候,如果發(fā)現(xiàn)有正在執(zhí)行的Sync,能否共享這次Sync請求呢?

答案是:不可以。使用singleflight來優(yōu)化寫無法保證數(shù)據(jù)的安全性。

我們必須要保證的是,Sync操作一定要在Write完成之后發(fā)起。只要兩者存在并發(fā)的可能性,那么Sync就不能保證攜帶了這次Write操作的數(shù)據(jù),也就無法保證安全性。

示意圖:

圖片圖片

還是以上面的圖為例來說明,當請求 B 完成 Write 操作后,看到請求 A 已經(jīng)發(fā)起了 Sync 操作。此時它是無法判斷請求 A 的 Sync 操作是否包含了請求 B 的數(shù)據(jù)。從圖示我們也很清晰的看到,請求B的 Write 和請求 A 的 Sync 在時間上存在重疊。

因此,當Write完成后,如果發(fā)現(xiàn)有一個Sync正在執(zhí)行,我們不能簡單地復用這個Sync。我們需要啟動一個新的Sync操作。

問題二:那么聚合的時機在哪里呢?

對于讀請求的聚合,其時機相對直觀:一旦發(fā)現(xiàn)有針對同一個 Key 的請求,就可以等待這次的結果并復用該結果。但寫請求的聚合時機則不是,它的聚合時機是在等待中遇到“志同道合“的請求。

讓我們通過一個具體例子來說明(注意,以下所有的請求都是針對相同的文件):

  1. t0 時刻:A 執(zhí)行了 Write,并嘗試發(fā)起Sync,由于此時沒有其他請求在執(zhí)行,A 便執(zhí)行真正的Sync操作。
  2. t1 時刻:B 執(zhí)行了 Write,發(fā)現(xiàn)已經(jīng)有請求在Sync了(即A),因此進入等待狀態(tài),直到A完成。
  3. t2 時刻:C 執(zhí)行了 Write,發(fā)現(xiàn)已經(jīng)有請求在Sync了(即A),因此進入等待狀態(tài),直到A完成。
  4. t3 時刻:D 執(zhí)行了 Write,發(fā)現(xiàn)已經(jīng)有請求在Sync了(即A),因此進入等待狀態(tài),直到A完成。
  5. t4 時刻:A 的Sync操作終于完成。A隨即通知 B、C、D 三位,告知它們可以進行Sync請求了。
  6. t5 時刻:從B、C、D中選擇一個來執(zhí)行一次Sync操作。假設B被選中,則C、D請求則等待B完成Sync即可。B發(fā)起的Sync操作一定包含了B,C,D三者寫的數(shù)據(jù),確保了安全性。
  7. t6:B 的Sync操作完成,C、D被通知操作已完成。如此一來,B、C、D三者的數(shù)據(jù)都確保落盤。

正如上述所演示,寫操作的聚合是在等待前一次Sync操作完成期間收集到的請求。本來需要4次Sync操作,現(xiàn)在僅需2次Sync就可以確保數(shù)據(jù)的安全性。

在高并發(fā)的場景下,這種聚合方式的效益尤為顯著。下面,我們將探討這種策略的具體代碼實現(xiàn)。

02 - 寫聚合的代碼實現(xiàn)

實現(xiàn)寫操作聚合的關鍵在于確保數(shù)據(jù)安全的時序前提下進行聚合。以下是一種典型和實現(xiàn)方式,它是對 sync.Cond 和 sync.Once 的巧妙應用。首先,我們定義一個負責聚合的結構體,如下:

// SyncJob 用于管理一個文件的 Sync 任務
type SyncJob struct {
   *sync.Cond                         // 聚合 Sync 的關鍵
   holding    int32                   // 記錄聚合的個數(shù)
   lastErr    error                   // 記錄執(zhí)行 Sync 結果
   syncPoint  *sync.Once              // 確保同一時間只有一個 Sync 執(zhí)行
   syncFunc   func(interface{}) error // 實際執(zhí)行 Sync 的函數(shù)
}

// SyncJob 的構建函數(shù)
func NewSyncJob(fn func(interface{}) error) *SyncJob {
   return &SyncJob{
      Cond:      sync.NewCond(&sync.Mutex{}),
      syncFunc:  fn,
      syncPoint: &sync.Once{},
   }
}

接下來,我們?yōu)?SyncJob 定義一個執(zhí)行聚合的方法,如下:

func (s *SyncJob) Do(job interface{}) error {
 s.L.Lock()
 if s.holding > 0 {
  // 如果有請求在前面,則等待前一次請求完成。
    // 等待的過程中,會有"志同道合"之人
  s.Wait()
 }
 // 準備要下發(fā)請求了,增加計數(shù)
 s.holding += 1
 syncPoint := s.syncPoint
 s.L.Unlock()

 // "志同道合"的人一起來到這里,此時已經(jīng)滿足 Write 和 Sync 的時序關系。
  // 使用 sync.Once 確保只有請求者執(zhí)行同步操作。
 syncPoint.Do(func() {
  // 執(zhí)行實際的 Sync 操作
  s.lastErr = s.syncFunc(job)

  s.L.Lock()
    // holding 展示本批次有多少個請求
    fmt.Printf("holding:%v\n", s.holding)
  // 本次請求執(zhí)行完成,重置計數(shù)器,準備下一輪聚合
  s.holding = 0
  s.syncPoint = &sync.Once{}
  // 喚醒下一批的請求
  s.Broadcast()
  s.L.Unlock()
 })
 return s.lastErr
}

在這里,我們使用了一個Go的 sync.Cond 來阻塞和通知等待中的請求,并通過 sync.Once 確保同步操作同一時間、同一批只有一個在執(zhí)行。

  • 其實在這個場景下,從代碼實現(xiàn)來講,sync.Cond 也可以使用 Go 的 Channel 來實現(xiàn)相同的效果,用 Ch← 來阻塞,用 close(Ch) 來通知。效果是一樣的,感興趣的童鞋可以改造試試。

現(xiàn)在讓我們來看看這段代碼的實際運行效果:

func main() {
 file, err := os.OpenFile("hello.txt", os.O_RDWR, 0700)
 if err != nil {
  log.Fatal(err)
 }
 defer file.Close()

 // 初始化 Sync 聚合服務
 syncJob := NewSyncJob(func(interface{}) error {
  fmt.Printf("do sync...\n")
    time.Sleep(time.Second())
  return file.Sync()
 })

 wg := sync.WaitGroup{}
 for i := 0; i < 10; i++ {
  wg.Add(1)
  go func() {
   defer wg.Done()
   // 執(zhí)行寫操作 write ...
   fmt.Printf("write...\n")
   // 觸發(fā) sync 操作
   syncJob.Do(file)
  }()
 }
 wg.Wait()
}

通過上述代碼,我們講對文件寫入操作后的 Sync 調用進行有效的聚合。童鞋們可以多次運行程序,觀察其行為??梢酝ㄟ^觀察打印的 holding 字段獲悉每一批聚合的請求是多少個。

思考:從效果來講,上面的代碼無論怎么跑,最少要執(zhí)行兩次 Sync。你知道是為什么嗎?

動畫示意圖:

圖片圖片


總結

上面介紹了讀寫聚合優(yōu)化的兩種實現(xiàn)。讀和寫的聚合是有區(qū)別的。

  1. 讀操作,核心是一個 map,只要有相同Key的讀取正在執(zhí)行,那么等待這份正在執(zhí)行的請求的結果也是符合預期的。同步等待則用的是 sync.WaitGroup 來實現(xiàn)。
  2. 寫操作,核心是要先保證數(shù)據(jù)安全性。它必須保證 Sync 操作在 Write 操作之后。因此當發(fā)現(xiàn)有正在執(zhí)行的Sync操作,那么就等待這次完成,然后必須重新開啟一輪的 Sync 操作,等待的過程也是聚合的時機。我們可以使用 sync.Cond(或者 Channel )來實現(xiàn)阻塞和喚醒,使用 sync.Once 來保證同一時間單個執(zhí)行。

責任編輯:武曉燕 來源: 奇伢云存儲
相關推薦

2018-11-05 11:20:54

緩沖IO

2022-10-17 08:07:13

Go 語言并發(fā)編程

2015-08-10 14:39:46

Java 操作建議

2010-05-11 13:36:50

Unix標準

2019-02-25 08:40:28

Linux磁盤IO

2023-05-08 00:06:45

Go語言機制

2025-06-17 09:32:15

2024-07-08 00:01:00

GPM模型調度器

2023-11-27 18:07:05

Go并發(fā)編程

2023-02-10 09:40:36

Go語言并發(fā)

2023-09-03 22:44:28

I/O高并發(fā)

2009-05-14 10:16:36

Oracle優(yōu)化磁盤

2009-10-10 10:10:29

服務器IO

2025-10-16 02:00:00

2011-01-27 13:52:11

Android 3.0I\O大會

2017-11-10 11:27:48

Go并行算法

2022-04-24 15:29:17

微服務go

2024-09-06 10:48:13

2017-09-01 12:26:18

Linux調度器系統(tǒng)

2025-07-23 08:13:10

點贊
收藏

51CTO技術棧公眾號