influxdb源码解析-数据写入

前言

   ~~   在前面几章,介绍了influxdb的基本概念,经常的用法,以及怎么编译源码,以及服务启动部分,meta部分。

tree -L 3

在这里插入图片描述
可以看到存储的的结构和我们上图的结构是一致的。本篇文章,从写入的角度,来分析一下整个写入链路的逻辑。

HTTP路由

influxdb启动之后,会监听8086端口,然后提供http服务。在influxdb/influxd/run/server.go中,cmd/influxd/run/server.go:378 open函数,表示了server启动之后,装配哪些service。其中有一个service是appendHTTPDService

	s.appendHTTPDService(s.config.HTTPD)
	s.appendRetentionPolicyService(s.config.Retention)

func (s *Server) appendHTTPDService(c httpd.Config) {
	if !c.Enabled {
		return
	}
	srv := httpd.NewService(c)
	srv.Handler.MetaClient = s.MetaClient
	authorizer := meta.NewQueryAuthorizer(s.MetaClient)
	srv.Handler.QueryAuthorizer = authorizer
	srv.Handler.WriteAuthorizer = meta.NewWriteAuthorizer(s.MetaClient)
	srv.Handler.QueryExecutor = s.QueryExecutor
	srv.Handler.Monitor = s.Monitor
	srv.Handler.PointsWriter = s.PointsWriter
	srv.Handler.Version = s.buildInfo.Version
	srv.Handler.BuildType = "OSS"
	ss := storage.NewStore(s.TSDBStore, s.MetaClient)
	srv.Handler.Store = ss
	if s.config.HTTPD.FluxEnabled {
		srv.Handler.Controller = control.NewController(s.MetaClient, reads.NewReader(ss), authorizer, c.AuthEnabled, s.Logger)
	}

	s.Services = append(s.Services, srv)
}

    ~~~    在第三行,httpd.NewService,通过http.config 新建了一个http service,看一下NewService的逻辑

func NewService(c Config) *Service {
	s := &Service{
		addr:           c.BindAddress,
		https:          c.HTTPSEnabled,
		cert:           c.HTTPSCertificate,
		key:            c.HTTPSPrivateKey,
		limit:          c.MaxConnectionLimit,
		tlsConfig:      c.TLS,
		err:            make(chan error),
		unixSocket:     c.UnixSocketEnabled,
		unixSocketPerm: uint32(c.UnixSocketPermissions),
		bindSocket:     c.BindSocket,
		Handler:        NewHandler(c),
		Logger:         zap.NewNop(),
	}
	if s.tlsConfig == nil {
		s.tlsConfig = new(tls.Config)
	}
	if s.key == "" {
		s.key = s.cert
	}
	if c.UnixSocketGroup != nil {
		s.unixSocketGroup = int(*c.UnixSocketGroup)
	}
	s.Handler.Logger = s.Logger
	return s
}

是一些基本信息的装配,这里主要注意一下Handler字段的赋值,一般在web开发中,Handler都是用来处理用户的http请求的,这里也不例外。在NewHandler中,定义了路由参数:

	h.AddRoutes([]Route{
		Route{
			"query-options", // Satisfy CORS checks.
			"OPTIONS", "/query", false, true, h.serveOptions,
		},
		Route{
			"query", // Query serving route.
			"GET", "/query", true, true, h.serveQuery,
		},
		Route{
			"query", // Query serving route.
			"POST", "/query", true, true, h.serveQuery,
		},
		Route{
			"write-options", // Satisfy CORS checks.
			"OPTIONS", "/write", false, true, h.serveOptions,
		},
		Route{
			"write", // Data-ingest route.
			"POST", "/write", true, writeLogEnabled, h.serveWriteV1,
		},
		Route{
			"write", // Data-ingest route.
			"POST", "/api/v2/write", true, writeLogEnabled, h.serveWriteV2,
		},
		Route{
			"prometheus-write", // Prometheus remote write
			"POST", "/api/v1/prom/write", false, true, h.servePromWrite,
		},
		Route{
			"prometheus-read", // Prometheus remote read
			"POST", "/api/v1/prom/read", true, true, h.servePromRead,
		},
		Route{ // Ping
			"ping",
			"GET", "/ping", false, true, authWrapper(h.servePing),
		},
		Route{ // Ping
			"ping-head",
			"HEAD", "/ping", false, true, authWrapper(h.servePing),
		},
		Route{ // Ping w/ status
			"status",
			"GET", "/status", false, true, authWrapper(h.serveStatus),
		},
		Route{ // Ping w/ status
			"status-head",
			"HEAD", "/status", false, true, authWrapper(h.serveStatus),
		},
		Route{ // Ping
			"ping",
			"GET", "/health", false, true, authWrapper(h.serveHealth),
		},
		Route{
			"prometheus-metrics",
			"GET", "/metrics", false, true, authWrapper(promhttp.Handler().ServeHTTP),
		},
	}...)

这里就是我们通过http 写入和查询的入口所在,所有的请求都是从这里开始。找到起点,我们就可以开始后续的事情。

serveWriteV1

从上面找到了http的路由定义之后,先看一下写入的逻辑。
在这里插入图片描述
写入有两个版本,对应两个处理函数,这里我们首先研究第一个,也就是serveWriteV1

func (h *Handler) serveWriteV1(w http.ResponseWriter, r *http.Request, user meta.User) {
	precision := r.URL.Query().Get("precision")
	switch precision {
	case "", "n", "ns", "u", "ms", "s", "m", "h":
		// it's valid
	default:
		err := fmt.Sprintf("invalid precision %q (use n, u, ms, s, m or h)", precision)
		h.httpError(w, err, http.StatusBadRequest)
	}

	db := r.URL.Query().Get("db")
	rp := r.URL.Query().Get("rp")

	h.serveWrite(db, rp, precision, w, r, user)
}

serviceWriteV1做了一下基本参数的校验,然后委托给了serveWrite

ServeWrite

serveWrite主要分为以下步骤

  • 参数校验
  • http payload 信息读取
  • point的反序列化
  • point写入到本地
  • 写入结果处理
  • 返回
    这个函数有点长,所以一步一步的分析。

参数校验

atomic.AddInt64(&h.stats.WriteRequests, 1)
	atomic.AddInt64(&h.stats.ActiveWriteRequests, 1)
	defer func(start time.Time) {
		atomic.AddInt64(&h.stats.ActiveWriteRequests, -1)
		atomic.AddInt64(&h.stats.WriteRequestDuration, time.Since(start).Nanoseconds())
	}(time.Now())
	h.requestTracker.Add(r, user)

	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
	}

	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
		}
	}

前面这部分都是参数校验相关,例如校验写入的数据库是不是存在,校验是不是需要开启鉴权等等。这部分就不再赘述。

数据读取

// Handle gzip decoding of the body
	if r.Header.Get("Content-Encoding") == "gzip" {
		b, err := gzip.NewReader(r.Body)
		if err != nil {
			h.httpError(w, err.Error(), http.StatusBadRequest)
			return
		}
		defer b.Close()
		body = b
	}

	var bs []byte
	if r.ContentLength > 0 {
		if h.Config.MaxBodySize > 0 && r.ContentLength > int64(h.Config.MaxBodySize) {
			h.httpError(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge)
			return
		}

		// This will just be an initial hint for the gzip reader, as the
		// bytes.Buffer will grow as needed when ReadFrom is called
		bs = make([]byte, 0, r.ContentLength)
	}
	buf := bytes.NewBuffer(bs)

	_, err := buf.ReadFrom(body)
	if err != nil {
		if err == errTruncated {
			h.httpError(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge)
			return
		}

		if h.Config.WriteTracing {
			h.Logger.Info("Write handler unable to read bytes from request body")
		}
		h.httpError(w, err.Error(), http.StatusBadRequest)
		return
	}
	atomic.AddInt64(&h.stats.WriteRequestBytesReceived, int64(buf.Len()))

	if h.Config.WriteTracing {
		h.Logger.Info("Write body received by handler", zap.ByteString("body", buf.Bytes()))
	}

    ~~~    在校验完参数之后,开始从读取http协议携带的内容。这里因为内容可能是压缩过的,所以首先判断一下header,是不是带有压缩信息。
    ~~~     12-25行,是直接从body里面把携带的信息读取出来。然后接下来对读取到的结果做校验,以及记录一些额外的信息。
读取到这里基本就结束了

数据反序列化

    ~~~    写入influxdb的数据都是要遵循influxdb的协议的,influxdb使用这个协议来对数据反序列化,这个协议也叫行协议(line protocol),再复习一下:
在这里插入图片描述
具体的协议内容:influxdb 行协议
这里举个例子:

weather,location=us-midwest,season=summer temperature=82 1465839830100400200

这里有一条数据,其中weather是measurement,location=us-midwest,season=summer 是tag,temperature=82是field, 1465839830100400200是timestamp,至于为啥这样分,行协议可以抽象为:

measurement[,tagk=tagv] fieldkey=fieldv[,fieldkey2=fieldv2] timestamp

这里很多人经常把field和tag搞混,其实很好分辨:
如果measurement后面没有逗号(,)那么这条数据就没有tag
如果有tag,tag直接需要用逗号隔开,如果发现第一个空格,那么空格后面就是field
field之间需要用逗号隔开
知道这几个原则,就能很简单的辨别tag和field。
    ~~~    复习到之后,其实反序列化,就是按照行协议,把byte数组给反序列出来。这部分逻辑在ParsePointsWithPrecision和parsePoint里面里面

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++

		if len(block) == 0 {
			continue
		}

		start := skipWhitespace(block, 0)

		// If line is all whitespace, just skip it
		if start >= len(block) {
			continue
		}

		// lines which start with '#' are comments
		if block[start] == '#' {
			continue
		}

		// strip the newline if one is present
		if block[len(block)-1] == '\n' {
			block = block[:len(block)-1]
		}

		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)
		}

	}
	if len(failed) > 0 {
		return points, fmt.Errorf("%s", strings.Join(failed, "\n"))
	}
	return points, nil

}

    ~~~    ParsePointsWithPrecision做的事情,比较简单,首先跳过所有的空白,找到第一个不是空白的位置。然后把解析任务委托给parsePoint

func parsePoint(buf []byte, defaultTime time.Time, precision string) (Point, error) {
	// scan the first block which is measurement[,tag1=value1,tag2=value2...]
	pos, key, err := scanKey(buf, 0)
	if err != nil {
		return nil, err
	}

	// measurement name is required
	if len(key) == 0 {
		return nil, fmt.Errorf("missing measurement")
	}

	if len(key) > MaxKeyLength {
		return nil, fmt.Errorf("max key length exceeded: %v > %v", len(key), MaxKeyLength)
	}

	// scan the second block is which is field1=value1[,field2=value2,...]
	pos, fields, err := scanFields(buf, pos)
	if err != nil {
		return nil, err
	}

	// at least one field is required
	if len(fields) == 0 {
		return nil, fmt.Errorf("missing fields")
	}

	var maxKeyErr error
	err = walkFields(fields, func(k, v []byte) bool {
		if sz := seriesKeySize(key, k); sz > MaxKeyLength {
			maxKeyErr = fmt.Errorf("max key length exceeded: %v > %v", sz, MaxKeyLength)
			return false
		}
		return true
	})

	if err != nil {
		return nil, err
	}

	if maxKeyErr != nil {
		return nil, maxKeyErr
	}

	// scan the last block which is an optional integer timestamp
	pos, ts, err := scanTime(buf, pos)
	if err != nil {
		return nil, err
	}
}

    ~~~    parsePoint 的逻辑比较长,这里截取部分的核心的逻辑。首先说一下这个parse的设计很好,为所有的字段都设计了一个parser,比如专门解析key的scanKey,解析field的scanFields这部分没看过的同学我建议仔细看看。说一个重要的点:scan操作是一个状态机,scan的过程会随着遇到的字符串流转,这个有点像编译原理的DFA之类的。
    ~~~    我看到这部分代码也是非常的赞赏,可能是没什么见识,发现这段代码写的很优雅。解析的这个我想并不困难,按照协议来解析这是件简单的事情,但是如何把代码写的优雅起来,是一件难事,在学习influxdb的时候,不仅仅是他的设计思想,代码本身写的也是非常值得学习的。
    ~~~    具体的解析逻辑就不再深入了,其实也没啥好说的。parse结束之后,会返回一个Point的slice,表示当前写入的point数组。需要说明的是,这里的parse其实是一种懒加载,这里看Point的定义就能看出来,随便举个例子:

type Point interface {
	// Name return the measurement name for the point.
	Name() []byte
}

Name字段,也就是measurement,是一个byte数组,这里并没有把它转成string,而是直接做了一个slice并且赋值。**这里也是一个优化!**因为influxdb是一种时序数据库,时序数据是写多读少的特点,这里就没有必要去理解具体的字段,可能需要到查询的时候,才需要理解具体含义。

数据写入

继续回到主线链路,parse结束之后,得到了一个point的slice,这个slice就是用来写入的数据。这部分逻辑很长,也是核心的逻辑。为了能够更好地看懂,我墙裂建议没看过meta部分源码解析的同学,去看看,否则后面你会发现你就看不懂了
    ~~~    这里假设你已经精通了meta 部分的逻辑。那么接着往下看!先看一个核心的interface


	type pointsWriterWithContext interface {
		WritePointsWithContext(context.Context, string, string, models.ConsistencyLevel, meta.User, []models.Point) error
	}

这个interface,定义了写入的函数WritePointsWithContext 解析完只有,定义了个writePoints函数,内部调用了pointsWriterWithContext某个实现的WritePointsWithContext方法完后对数据的写入。

writePoints := func() error {
		switch pw := h.PointsWriter.(type) {
		case pointsWriterWithContext:
			var npoints, nvalues int64
			ctx := context.WithValue(context.Background(), coordinator.StatPointsWritten, &npoints)
			ctx = context.WithValue(ctx, coordinator.StatValuesWritten, &nvalues)

			// for now, just store the number of values used.
			err := pw.WritePointsWithContext(ctx, database, retentionPolicy, consistency, user, points)
			atomic.AddInt64(&h.stats.ValuesWrittenOK, nvalues)
			if err != nil {
				return err
			}
			return nil
		default:
			return h.PointsWriter.WritePoints(database, retentionPolicy, consistency, user, points)
		}
	}

	// Write points.
	if err := writePoints(); influxdb.IsClientError(err) {
		atomic.AddInt64(&h.stats.PointsWrittenFail, int64(len(points)))
		h.httpError(w, err.Error(), http.StatusBadRequest)
		return
	} 

    ~~~    这里可能有人看不懂了,尤其是多golang不是很熟悉,但是对influxdb的实现有很好奇的人。这里可以理解是一种闭包调用,writePoints就是函数内定义的函数,然后再函数内部自己调用。这里其实只是一种实现方式,我觉得这里的pointsWriterWithContext放在这里总感觉有点草率,明显是有更加合适的位置的,放在这里极有可能是开源版本删减导致的!但是这并不影响我们分析。还记得我们在服务启动里面介绍的coordinator模块吗influxdb服务启动里面介绍了PointsWriter这个接口,这个接口是Server的一个重要的组成部分,但是被放在了coordinator里面,我们当时的分析是,为了兼容其他的协议,比如opentsdb等等,其实确实是这样的。所以其实pointsWriterWithContext****是被PointsWriter实现的

PointWriter实现的WritePointsWithContext

找到pointWrite,看到具体的实现:

func (w *PointsWriter) WritePointsWithContext(ctx context.Context, database, retentionPolicy string, consistencyLevel models.ConsistencyLevel, user meta.User, points []models.Point) error {
	return w.WritePointsPrivilegedWithContext(ctx, database, retentionPolicy, consistencyLevel, points)
}

这里委托给了WritePointsPrivilegedWithContext那就继续看看WritePointsPrivilegedWithContext的实现
    ~~~    到这里简单休息一下,总结一下我们现在的进度。

  1. 通过http写入的数据完成了解析,得到了一个point的数组
  2. 解析到的点通过pointsWriterWithContext的WritePointsWithContext完成写入
  3. pointsWriterWithContext是一个interface,真正的实现在PointsWriter
  4. PointsWriter是位于coordinator模块下面的一个结构,用来接受点的写入,有多种协议的实现。
  5. PointsWriter实现了WritePointsWithContext,并且委托给了WritePointsPrivilegedWithContext执行
    这就是我们上半场的内容,WritePointsPrivilegedWithContext到这里可以说是万里长征第一步,其实后面还有很复杂的逻辑。所以如果你到这里看不懂了,那么没关系,把开头的文章都看一遍,对着源码翻翻,然后再来看,可能就会恍然大悟。这部分我看过的不下五遍

*----------------------------------------------上下半场分割线-------------------------------------------------------
如何你现在对上面的内容都有一个清楚的理解,那么继续写入的流程。

WritePointsPrivilegedWithContext
点的映射

    ~~~    WritePointsPrivilegedWithContext做的第一件事情,是把datapoint给映射到各个shard上去。这个过程大家一般也叫做shuffle。具体的逻辑在这里实现:

	// 这里所有的datapoint 映射到shard上去
	shardMappings, err := w.MapShards(&WritePointsRequest{Database: database, RetentionPolicy: retentionPolicy, Points: points})
	if err != nil {
		return err
	}

MapShards输入的是WritePointsRequest,输出的是ShardMapping结构。ShardMapping的具体组成:

// ShardMapping contains a mapping of shards to points.
type ShardMapping struct {
	n       int
	Points  map[uint64][]models.Point  // The points associated with a shard ID
	Shards  map[uint64]*meta.ShardInfo // The shards that have been mapped, keyed by shard ID
	Dropped []models.Point             // Points that were dropped
}

其中Points这个map的key是shardId,value是这个shard对应的要写入的点组成的slice。Shards这个map的key是shardId,value是shardInfo实例。所以这个函数的作用就是,对于输入的点,把它们映射到合适的Shard上去,并且把这个信息记录在ShardMapping里面。
    ~~~    那么接下来就是看一下映射的规则是什么,核心逻辑在:

	mapping := NewShardMapping(len(wp.Points))
	for _, p := range wp.Points {
		sg := list.ShardGroupAt(p.Time())
		if sg == nil {
			// We didn't create a shard group because the point was outside the
			// scope of the RP.
			mapping.Dropped = append(mapping.Dropped, p)
			atomic.AddInt64(&w.stats.WriteDropped, 1)
			continue
		}

		sh := sg.ShardFor(p.HashID())
		mapping.MapPoint(&sh, p)
	}

这里首先构建了一个ShardMapping结构,然后遍历所有的点,寻找合适的shardgroup(如果这里你不清楚为啥要找shardgroup,那么说明你还是没读懂前面的,从头再读一遍)。找到shardgroup之后,调用shardgroup的ShardFor方法寻找一个shard。这个方法我们在meta部分提到过,就是简单的取模。那么当前点的hashid是怎么生成的呢?
    ~~~    对于一个点,生成这个点的hashId逻辑是如下的代码:

func (p *point) HashID() uint64 {
	h := NewInlineFNV64a()
	h.Write(p.key)
	sum := h.Sum64()
	return sum
}
func (s *InlineFNV64a) Write(data []byte) (int, error) {
	hash := uint64(*s)
	for _, c := range data {
		hash ^= uint64(c)
		hash *= prime64
	}
	*s = InlineFNV64a(hash)
	return len(data), nil
}

这里可以看到,这个点的生成是基于FNV64算法,点的值取决于点的key(也就是measurement和tags),和field没啥关系。最后把这个值转化为一个int64类型的,就生成了hash值。
    ~~~    得到hash值之后,使用MapPoint来点的信息添加到这两个map里:

func (s *ShardMapping) MapPoint(shardInfo *meta.ShardInfo, p models.Point) {
	if cap(s.Points[shardInfo.ID]) < s.n {
		s.Points[shardInfo.ID] = make([]models.Point, 0, s.n)
	}
	// shardInfoId->
	s.Points[shardInfo.ID] = append(s.Points[shardInfo.ID], p)
	// shardInfoId->shardInfo的映射
	s.Shards[shardInfo.ID] = shardInfo
}

这里就没啥好说的了,很简洁。

点的写入

拿到mapping信息之后,就知道每个点需要被写入到哪个shard。接着就是shard的写入逻辑。

	ch := make(chan error, len(shardMappings.Points))
	for shardID, points := range shardMappings.Points {
		go func(ctx context.Context, shard *meta.ShardInfo, database, retentionPolicy string, points []models.Point) {
			var numPoints, numValues int64
			ctx = context.WithValue(ctx, tsdb.StatPointsWritten, &numPoints)
			ctx = context.WithValue(ctx, tsdb.StatValuesWritten, &numValues)

			err := w.writeToShardWithContext(ctx, shard, database, retentionPolicy, points)
			if err == tsdb.ErrShardDeletion {
				err = tsdb.PartialWriteError{Reason: fmt.Sprintf("shard %d is pending deletion", shard.ID), Dropped: len(points)}
			}

			if v, ok := ctx.Value(StatPointsWritten).(*int64); ok {
				atomic.AddInt64(v, numPoints)
			}

			if v, ok := ctx.Value(StatValuesWritten).(*int64); ok {
				atomic.AddInt64(v, numValues)
			}

			ch <- err
		}(ctx, shardMappings.Shards[shardID], database, retentionPolicy, points)
	}

    ~~~    写入的逻辑就是遍历上述得到的shardMapping,每个shard一个goroutine并发写入。然后把写入操作委托给writeToShardWithContext。 writeToShardWithContext的逻辑:

	writeToShard := func() error {
		type shardWriterWithContext interface {
			WriteToShardWithContext(context.Context, uint64, []models.Point) error
		}
		switch sw := w.TSDBStore.(type) {
		case shardWriterWithContext:
			if err := sw.WriteToShardWithContext(ctx, shard.ID, points); err != nil {
				return err
			}
		default:
			if err := w.TSDBStore.WriteToShard(shard.ID, points); err != nil {
				return err
			}
		}
		return nil
	}

	// Except tsdb.ErrShardNotFound no error can be handled here
	if err := writeToShard(); err == tsdb.ErrShardNotFound {
}

writeToShardWithContext定义了writeToShard接口,这个接口的实现在TSDBStore注意这里!!! 这里的switch,就是用来选择存储引擎的。如果你想要自己实现一个存储引擎来存储数据,这里是一个很关键的拓展点。毫无疑问,这个条件分支走到了第一个case里面。
    ~~~    位于influxdb/tsdb/store.go的Store结构,实现了shardWriterWithContext接口的WriteToShardWithContext方法。这个方法就简单点。首先寻找具体的Shard结构。

	sh := s.shards[shardID]
	if sh == nil {
		s.mu.RUnlock()
		return ErrShardNotFound
	}

找到Shard结构之后,把写入委托给了ShardWritePointsWithContext方法。Shard的WritePointsWithContext有委托给了EngineWritePointsWithContext方法。到这里,才算是真的开始写入了。逻辑也不是很复杂。
首先把point编码到Value结构里面,得到一个Value的s数组

	values := make(map[string][]Value, len(points))
	var (
		keyBuf    []byte
		baseLen   int
		seriesErr error
		npoints   int64 // total points processed
		nvalues   int64 // total values (fields) processed
	)

	for _, p := range points {	
	 // do encode 代码略
     }

这里的逻辑比较有意思,但是篇幅优先,我们这里不再做仔细的分析,因为这篇文章已经很长了,太长了看着会晕。而且这段编码的逻辑也是influxdb到底是单值逻辑还是多值逻辑的重要证据!所以后面会单独分析。
编码完成之后,写入到Cache和WAL


	// first try to write to the cache
	if err := e.Cache.WriteMulti(values); err != nil {
		return err
	}

	if e.WALEnabled {
		if _, err := e.WAL.WriteMulti(values); err != nil {
			return err
		}
	}

	// if requested, store points written stats
	if pointsWritten, ok := ctx.Value(tsdb.StatPointsWritten).(*int64); ok {
		*pointsWritten = npoints
	}

	// if requested, store values written stats
	if valuesWritten, ok := ctx.Value(tsdb.StatValuesWritten).(*int64); ok {
		*valuesWritten = nvalues
	}

到这里,写入链路算执行了80%,其实后面还有大量的扫尾工作,但是不是核心逻辑,就不再分析了。

总结

    ~~~    这篇文章花了很大的篇幅,梳理了influxdb的写入流程,虽然写了很多,但是其实还有很多细节。比如怎么编码的?怎么保证写入成功?为啥是先写Cache不是先写WAL等等,这些后面单独分析。
    ~~~    同时这里面也出现了大量的结构,比如Engine,Shard,Store,Series等,他们是啥关系?这部分我们在后面会详细分析。

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值