ホームページ  >  記事  >  バックエンド開発  >  Golang のキャッシュ ライブラリ freecache について学ぶための 1 つの記事

Golang のキャッシュ ライブラリ freecache について学ぶための 1 つの記事

青灯夜游
青灯夜游転載
2022-02-21 10:43:425720ブラウズ

この記事では、Golang キャッシュについて理解し、Golang のキャッシュ ライブラリ freecache を簡単に紹介します。

Golang のキャッシュ ライブラリ freecache について学ぶための 1 つの記事

# Go 開発キャッシュ シナリオでは、通常、マップまたはキャッシュ フレームワークが使用されます。スレッド セーフのために、sync.Map またはスレッド セーフ キャッシュ フレームワークが使用されます。

キャッシュ シナリオ内のデータ量が 100 万レベルを超える場合は、gc に対するデータ型の影響を特別に考慮する必要があります (文字列型の最下層はポインターであることに注意してください) Len Cap なのでポインタ型でもあります) キャッシュキーと値が両方とも非 For ポインタ型の場合は、心配する必要はありません。 [関連する推奨事項: Go ビデオ チュートリアル ]

しかし、実際のアプリケーション シナリオでは、キーと値がポインター型データ (を含む) であることが非常に一般的であるため、キャッシュ フレームワークを使用する場合は、使用時の GC への影響には特別な注意を払う必要があります。GC に影響を与えるかどうかの観点から、キャッシュ フレームワークは大きく 2 つのカテゴリに分類されます。

  • ゼロ GC オーバーヘッド: freecache や bigcache など。層は、ringbuf に基づいており、ポインタの数を減らしています。
  • GC オーバーヘッドがあります。これは、Map に基づいて直接実装されたキャッシュ フレームワークです。

map の場合、gc 中にすべてのキーと値のペアがスキャンされます。すべてが基本タイプの場合、gc はそれらを再度スキャンしません。

以下では、実装原理を分析する例として freecache を使用します。コード例は次のとおりです:

func main() {
   cacheSize := 100 * 1024 * 1024
   cache := freecache.NewCache(cacheSize)

   for i := 0; i < N; i++ {
      str := strconv.Itoa(i)
      _ = cache.Set([]byte(str), []byte(str), 1)
   }

   now := time.Now()
   runtime.GC()
   fmt.Printf("freecache, GC took: %s\n", time.Since(now))
   _, _ = cache.Get([]byte("aa"))

   now = time.Now()
   for i := 0; i < N; i++ {
      str := strconv.Itoa(i)
      _, _ = cache.Get([]byte(str))
   }
   fmt.Printf("freecache, Get took: %s\n\n", time.Since(now))
}

1 Initialization

freecache.NewCache は、ローカル キャッシュ、サイズはストレージを表します スペース サイズに関して、freecache は 256 セグメントを初期化します。各セグメントは独立したストレージ ユニットです。freecache のロック ディメンションもセグメントに基づいています。各セグメントには、初期サイズが size/ のリングバッファがあります。 256. freecache が GC がゼロであると主張する理由は、そのポインターが 512 個のみで固定されており、各セグメントが 2 つ、つまり rb と slotData を持つためです (スライスはポインター型であることに注意してください)。

type segment struct {
   rb            RingBuf // ring buffer that stores data
   segId         int
   _             uint32  // 占位
   missCount     int64
   hitCount      int64
   entryCount    int64
   totalCount    int64      // number of entries in ring buffer, including deleted entries.
   totalTime     int64      // used to calculate least recent used entry.
   timer         Timer      // Timer giving current time
   totalEvacuate int64      // used for debug
   totalExpired  int64      // used for debug
   overwrites    int64      // used for debug
   touched       int64      // used for debug
   vacuumLen     int64      // up to vacuumLen, new data can be written without overwriting old data.
   slotLens      [256]int32 // The actual length for every slot.
   slotCap       int32      // max number of entry pointers a slot can hold.
   slotsData     []entryPtr // 索引指针
}

func NewCacheCustomTimer(size int, timer Timer) (cache *Cache) {
    cache = new(Cache)
    for i := 0; i < segmentCount; i++ {
        cache.segments[i] = newSegment(size/segmentCount, i, timer)
    }
}
func newSegment(bufSize int, segId int, timer Timer) (seg segment) {
    seg.rb = NewRingBuf(bufSize, 0)
    seg.segId = segId
    seg.timer = timer
    seg.vacuumLen = int64(bufSize)
    seg.slotCap = 1
    seg.slotsData = make([]entryPtr, 256*seg.slotCap) // 每个slotData初始化256个单位大小
}

2 読み取りと書き込みのプロセス

freecache のキーと値は両方とも []byte 配列です。使用する場合は自分でシリアル化および逆シリアル化する必要があります。複雑なオブジェクトをキャッシュする シリアル化と逆シリアル化の影響は無視できません。まず、Set プロセスを見てください:

_ = cache.Set([]byte(str), []byte(str), 1)

Set プロセスは最初にキーをハッシュし、hashVal タイプは uint64 であり、その低セグメント配列に対応する 8 ビット segID、下位 8 ~ 15 ビットは、slotId がslotsData 添字に対応することを示し、上位 16 ビットは、slotsData 添字に対応する []entryPtr 特定のデータを示します。ここでは検索操作が必要です。なお、[]entryPtr配列のサイズはslotCap(初期値は1)で、容量を拡張するとslotCapは2倍になります。

各セグメントはロック (sync.Mutex) に対応するため、ロックが 1 つしかない sync.Map とは異なり、大量の同時実行をサポートできます。

func (cache *Cache) Set(key, value []byte, expireSeconds int) (err error) {
   hashVal := hashFunc(key)
   segID := hashVal & segmentAndOpVal // 低8位
   cache.locks[segID].Lock() // 加锁
   err = cache.segments[segID].set(key, value, hashVal, expireSeconds)
   cache.locks[segID].Unlock()
}

func (seg *segment) set(key, value []byte, hashVal uint64, expireSeconds int) (err error) {
   slotId := uint8(hashVal >> 8)
   hash16 := uint16(hashVal >> 16)
   slot := seg.getSlot(slotId)
   idx, match := seg.lookup(slot, hash16, key)

   var hdrBuf [ENTRY_HDR_SIZE]byte
   hdr := (*entryHdr)(unsafe.Pointer(&hdrBuf[0]))
   if match { // 有数据更新操作
      matchedPtr := &slot[idx]
      seg.rb.ReadAt(hdrBuf[:], matchedPtr.offset)
      hdr.slotId = slotId
      hdr.hash16 = hash16
      hdr.keyLen = uint16(len(key))
      originAccessTime := hdr.accessTime
      hdr.accessTime = now
      hdr.expireAt = expireAt
      hdr.valLen = uint32(len(value))
      if hdr.valCap >= hdr.valLen {
         // 已存在数据value空间能存下此次value大小
         atomic.AddInt64(&seg.totalTime, int64(hdr.accessTime)-int64(originAccessTime))
         seg.rb.WriteAt(hdrBuf[:], matchedPtr.offset)
         seg.rb.WriteAt(value, matchedPtr.offset+ENTRY_HDR_SIZE+int64(hdr.keyLen))
         atomic.AddInt64(&seg.overwrites, 1)
         return
      }
      // 删除对应entryPtr,涉及到slotsData内存copy,ringbug中只是标记删除
      seg.delEntryPtr(slotId, slot, idx)
      match = false
      // increase capacity and limit entry len.
      for hdr.valCap < hdr.valLen {
         hdr.valCap *= 2
      }
      if hdr.valCap > uint32(maxKeyValLen-len(key)) {
         hdr.valCap = uint32(maxKeyValLen - len(key))
      }
   } else { // 无数据
      hdr.slotId = slotId
      hdr.hash16 = hash16
      hdr.keyLen = uint16(len(key))
      hdr.accessTime = now
      hdr.expireAt = expireAt
      hdr.valLen = uint32(len(value))
      hdr.valCap = uint32(len(value))
      if hdr.valCap == 0 { // avoid infinite loop when increasing capacity.
         hdr.valCap = 1
      }
   }
   
   // 数据实际长度为 ENTRY_HDR_SIZE=24 + key和value的长度    
   entryLen := ENTRY_HDR_SIZE + int64(len(key)) + int64(hdr.valCap)
   slotModified := seg.evacuate(entryLen, slotId, now)
   if slotModified {
      // the slot has been modified during evacuation, we need to looked up for the &#39;idx&#39; again.
      // otherwise there would be index out of bound error.
      slot = seg.getSlot(slotId)
      idx, match = seg.lookup(slot, hash16, key)
      // assert(match == false)
   }
   newOff := seg.rb.End()
   seg.insertEntryPtr(slotId, hash16, newOff, idx, hdr.keyLen)
   seg.rb.Write(hdrBuf[:])
   seg.rb.Write(key)
   seg.rb.Write(value)
   seg.rb.Skip(int64(hdr.valCap - hdr.valLen))
   atomic.AddInt64(&seg.totalTime, int64(now))
   atomic.AddInt64(&seg.totalCount, 1)
   seg.vacuumLen -= entryLen
   return
}

seg.evacuate は、ringbuf にキー/値を保存するのに十分な領域があるかどうかを評価します。十分な領域がない場合は、空き領域の終わり (つまり、開始領域) からスキャンを開始します。 (oldOff := seg.rb.End() seg.vacuumLen - seg.rb.Size())、対応するデータが論理的に削除されているか期限切れの場合、メモリのブロックは直接リサイクルできます。リサイクル条件が満たされない場合、エントリはリングの先頭からリングの末尾までスワップされ、その後エントリのインデックスが更新されます。サイクルが 5 回機能しない場合は、メモリのニーズを満たすために現在の oldHdrBuf をリサイクルする必要があります。

seg.evacuate 実行後に必要な領域が十分に確保されてから、インデックスとデータが書き込まれます。insertEntryPtr はインデックスの書き込み操作です。[]entryPtr の要素数の場合seg.slotCap (頭文字 1) より大きい場合、拡張操作が必要です 対応するメソッドについては、seg.expand を参照してください (ここでは説明しません)。

ringbuf に書き込むには、rb.Write を実行するだけです。

func (seg *segment) evacuate(entryLen int64, slotId uint8, now uint32) (slotModified bool) {
   var oldHdrBuf [ENTRY_HDR_SIZE]byte
   consecutiveEvacuate := 0
   for seg.vacuumLen < entryLen {
      oldOff := seg.rb.End() + seg.vacuumLen - seg.rb.Size()
      seg.rb.ReadAt(oldHdrBuf[:], oldOff)
      oldHdr := (*entryHdr)(unsafe.Pointer(&oldHdrBuf[0]))
      oldEntryLen := ENTRY_HDR_SIZE + int64(oldHdr.keyLen) + int64(oldHdr.valCap)
      if oldHdr.deleted { // 已删除
         consecutiveEvacuate = 0
         atomic.AddInt64(&seg.totalTime, -int64(oldHdr.accessTime))
         atomic.AddInt64(&seg.totalCount, -1)
         seg.vacuumLen += oldEntryLen
         continue
      }
      expired := oldHdr.expireAt != 0 && oldHdr.expireAt < now
      leastRecentUsed := int64(oldHdr.accessTime)*atomic.LoadInt64(&seg.totalCount) <= atomic.LoadInt64(&seg.totalTime)
      if expired || leastRecentUsed || consecutiveEvacuate > 5 {
      // 可以回收
         seg.delEntryPtrByOffset(oldHdr.slotId, oldHdr.hash16, oldOff)
         if oldHdr.slotId == slotId {
            slotModified = true
         }
         consecutiveEvacuate = 0
         atomic.AddInt64(&seg.totalTime, -int64(oldHdr.accessTime))
         atomic.AddInt64(&seg.totalCount, -1)
         seg.vacuumLen += oldEntryLen
         if expired {
            atomic.AddInt64(&seg.totalExpired, 1)
         } else {
            atomic.AddInt64(&seg.totalEvacuate, 1)
         }
      } else {
         // evacuate an old entry that has been accessed recently for better cache hit rate.
         newOff := seg.rb.Evacuate(oldOff, int(oldEntryLen))
         seg.updateEntryPtr(oldHdr.slotId, oldHdr.hash16, oldOff, newOff)
         consecutiveEvacuate++
         atomic.AddInt64(&seg.totalEvacuate, 1)
      }
   }
}

Freecache の Get プロセスは比較的単純です。ハッシュを通じて対応するセグメントを見つけ、slotId を通じて対応するインデックス スロットを見つけ、バイナリ トラバーサルを通じてデータを検索します。見つからない場合は直接 ErrNotFound を返し、それ以外の場合は更新しますしばらくの間インジケーターです。 Get プロセスでは、キャッシュ ヒット率関連のインジケーターも更新されます。

func (cache *Cache) Get(key []byte) (value []byte, err error) {
   hashVal := hashFunc(key)
   segID := hashVal & segmentAndOpVal
   cache.locks[segID].Lock()
   value, _, err = cache.segments[segID].get(key, nil, hashVal, false)
   cache.locks[segID].Unlock()
   return
}
func (seg *segment) get(key, buf []byte, hashVal uint64, peek bool) (value []byte, expireAt uint32, err error) {
   hdr, ptr, err := seg.locate(key, hashVal, peek) // hash+定位查找
   if err != nil {
      return
   }
   expireAt = hdr.expireAt
   if cap(buf) >= int(hdr.valLen) {
      value = buf[:hdr.valLen]
   } else {
      value = make([]byte, hdr.valLen)
   }

   seg.rb.ReadAt(value, ptr.offset+ENTRY_HDR_SIZE+int64(hdr.keyLen))
}

データを見つけたら、ringbuf を読み取るだけです。通常、読み取られる値は新しく作成されたメモリ空間であるため、[]byte データのコピー操作が含まれることに注意してください。

3 概要

いくつかの一般的なキャッシュ フレームワークのストレス テストのパフォーマンスから判断すると、Set のパフォーマンスの差は大きいですが、定量的なレベルではありません。Get のパフォーマンスの差は大きくないため、ほとんどのキャッシュ フレームワークでは、ほとんどのシナリオでは、Set/Get のパフォーマンスにあまり注意を払う必要はありません。関数がビジネス ニーズと GC の影響を満たしているかどうかに重点を置く必要があります。パフォーマンス ストレス テストの比較については、https://golang2 を参照してください。 .eddycjy.com/posts/ch5/04-performance/

キャッシュには特別なシナリオがあり、すべてのデータをメモリにキャッシュし、更新に関しては完全に更新 (置換) されます。このシナリオではフリーキャッシュを使用していますが、サイズが適切に設定されていない場合、一部のデータが削除される可能性があり、期待どおりではありませんので注意が必要です。この問題を回避するためにフリーキャッシュを使用するには、サイズを「十分な大きさ」に設定する必要がありますが、そのメモリ領域の使用量にも注意する必要があります。

プログラミング関連の知識について詳しくは、プログラミング教育をご覧ください。 !

以上がGolang のキャッシュ ライブラリ freecache について学ぶための 1 つの記事の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.cnで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。