Influxdb 資料寫入流程

germo發表於2021-09-09

資料寫入流程分析

  1. 本篇不涉及儲存層的寫入,只分析寫入請求的處理流程

Influxdb名詞介紹
  1. 如果想搞清楚Influxdb資料寫入流程,Influxdb本身的用法和其一些主要的專用詞還是要明白是什麼意思,比如measurement, field key,field value, tag key, tag value, tag set, line protocol, point, series, query, retention policy等;

  2. 相關的專用名詞解釋可參考:

分析入口
  1. 我們還是以http寫請求為入口來分析,在httpd/handler.go中建立Handler時有如下程式碼:

    Route{            "write", // Data-ingest route.
            "POST", "/write", true, writeLogEnabled, h.serveWrite,
        }

因此對寫入請求的處理就在函式 func (h *Handler) serveWrite(w http.ResponseWriter, r *http.Request, user meta.User)中。

  1. Handler.serveWrite流程梳理:
    2.1 獲取寫入的db並判斷db是否存在

database := r.URL.Query().Get("db")    if database == "" {
        h.httpError(w, "database is required", http.StatusBadRequest)        return
    }    if di := h.MetaClient.Database(database); di == nil {
        h.httpError(w, fmt.Sprintf("database not found: %q", database), http.StatusNotFound)        return
    }

2.2 許可權驗證

if h.Config.AuthEnabled {        if user == nil {
            h.httpError(w, fmt.Sprintf("user is required to write to database %q", database), http.StatusForbidden)            return
        }        if err := h.WriteAuthorizer.AuthorizeWrite(user.ID(), database); err != nil {
            h.httpError(w, fmt.Sprintf("%q user is not authorized to write to database %q", user.ID(), database), http.StatusForbidden)            return
        }
    }

2.3 獲取http請求的body部分,如需gzip解壓縮則解壓,並且作body size的校驗,因為有body size大小限制

    body := r.Body    if h.Config.MaxBodySize > 0 {
        body = truncateReader(body, int64(h.Config.MaxBodySize))
    }
    ...
    _, err := buf.ReadFrom(body)

2.4 從http body中解析出 points

points, parseError := models.ParsePointsWithPrecision(buf.Bytes(), time.Now().UTC(),
                       r.URL.Query().Get("precision"))

2.5 將解析出的points寫入db

h.PointsWriter.WritePoints(database, r.URL.Query().Get("rp"), consistency, user, points);
Points的解析
  1. 將http body解析成Points是寫入前的最主要的一步, 相關內容定義在 models/points.go中;

  2. 我們先來看一下一條寫入語句是什麼樣子的: insert test_mea_1,tag1=v1,tag2=v2 cpu=1,memory=10
    其中test_mea_1是measurement, tag key是tag1和tag2, 對應的tag value是v1和v2, field key是cpu和memory, field value是1和10;

  3. 先來看下point的定義,它實現了Point interface

type point struct {
    time time.Time    //這個 key包括了measurement和tag set, 且tag set是排序好的   
    key []byte    // text encoding of field data
    fields []byte    // text encoding of timestamp
    ts []byte    // cached version of parsed fields from data
    cachedFields map[string]interface{}    // cached version of parsed name from key
    cachedName string

    // cached version of parsed tags
    cachedTags Tags    //用來遍歷所有的field
    it fieldIterator
}
  1. 解析出Points

func ParsePointsWithPrecision(buf []byte, defaultTime time.Time, precision string) ([]Point, error) {
    points := make([]Point, 0, bytes.Count(buf, []byte{'n'})+1)
    var (
        pos    int
        block  []byte
        failed []string
    )    for pos < len(buf) {
        pos, block = scanLine(buf, pos)
        pos++
  
        ...

        pt, err := parsePoint(block[start:], defaultTime, precision)        if err != nil {
            failed = append(failed, fmt.Sprintf("unable to parse '%s': %v", string(block[start:]), err))
        } else {
            points = append(points, pt)
        }

    }    return points, nil
}

這裡的解析並沒有用正則之類的方案,純的字串逐次掃描,這裡不詳細展開說了.

PointsWriter分析
  1. 定義在coordinator/points_writer.go

  2. 主要負責將資料寫入到本地的儲存,我們重點分析下WritePointsPrivileged

func (w *PointsWriter) WritePointsPrivileged(database, retentionPolicy string, consistencyLevel models.ConsistencyLevel, points []models.Point) error {
    ....    
    //將point按time對應到相應的Shar上, 這個對應關係儲存在shardMappings裡, 這個MapShareds我們後面會分析
    shardMappings, err := w.MapShards(&WritePointsRequest{Database: database, RetentionPolicy: retentionPolicy, Points: points})    if err != nil {        return err
    }    // Write each shard in it's own goroutine and return as soon as one fails.
    ch := make(chan error, len(shardMappings.Points))    for shardID, points := range shardMappings.Points {    
        // 每個 Shard啟動一個goroutine作寫入操作, 真正的寫入操作w.writeToShard
        go func(shard *meta.ShardInfo, database, retentionPolicy string, points []models.Point) {
            err := w.writeToShard(shard, database, retentionPolicy, points)            if err == tsdb.ErrShardDeletion {
                err = tsdb.PartialWriteError{Reason: fmt.Sprintf("shard %d is pending deletion", shard.ID), Dropped: len(points)}
            }
            ch <- err
        }(shardMappings.Shards[shardID], database, retentionPolicy, points)
    }
    ...    
    // 寫入超時會return ErrTimeout
    timeout := time.NewTimer(w.WriteTimeout)
    defer timeout.Stop()    for range shardMappings.Points {
        select {        case <-w.closing:            return ErrWriteFailed        case <-timeout.C:
            atomic.AddInt64(&w.stats.WriteTimeout, 1)            // return timeout error to caller
            return ErrTimeout        case err := <-ch:            if err != nil {                return err
            }
        }
    }    return err
}
  1. Point到Shard的映謝
    3.1 先根據point的time找到對應的ShardGroup, 沒有就建立新的ShardGroup;
    3.2 按Point的key(measurement + tag set取hash)來散

sgi.Shards[hash%uint64(len(sgi.Shards))]



作者:掃帚的影子
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1868/viewspace-2817409/,如需轉載,請註明出處,否則將追究法律責任。

相關文章