influxdb源码解析-Inmem Index

本文深入剖析InfluxDB的索引模块,介绍了Index接口及其在Shard中的作用,详细讲解了InmemIndex的结构与功能,包括CreateSeriesListIfNotExists和Rebuild等关键操作。InmemIndex作为默认的内存索引,用于加速查询并缓存series信息。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

​ 这是一个分析inlfuxdb源码的系列。在上一章分析了series 模块。对于series,influxdb提供了series file来作为series 逻辑的统一封装。series 作为influxdb 里面的一个重要模块,被很多其他模块依赖。本章内容分析的是influxdb 的index模块。

Index的介绍

​ 如果上来就开始介绍索引,可能有些唐突。因为毕竟在这之前都没有提到过索引的概念。索引(Index)是数据库中的一个非常重要的模块,几乎所有数据库都会有索引。索引的作用就是加速查询。在influxdb 中也不例外。还是先看一下influxdb 的索引位于那个模块下,或者被什么模块依赖了。

Store

​ 在之前的介绍中,提到了这个模块。Store在influxdb中是存储的抽象。TSDBStore 是influxdb 存储模块。这个结构在tsdb/store.go 下面。

type Store struct {
	mu                sync.RWMutex
	shards            map[uint64]*Shard
	databases         map[string]*databaseState
	sfiles            map[string]*SeriesFile
	SeriesFileMaxSize int64 // Determines size of series file mmap. Can be altered in tests.
	path              string

	// shared per-database indexes, only if using "inmem".
	indexes map[string]interface{}

	// Maintains a set of shards that are in the process of deletion.
	// This prevents new shards from being created while old ones are being deleted.
	pendingShardDeletes map[uint64]struct{}

	// Epoch tracker helps serialize writes and deletes that may conflict. It
	// is stored by shard.
	epochs map[uint64]*epochTracker

	EngineOptions EngineOptions

	baseLogger *zap.Logger
	Logger     *zap.Logger

	closing chan struct{}
	wg      sync.WaitGroup
	opened  bool
}

store 中有多个shard结构,这个结构之前也简单提到过,每个shard就是对应的influxdb一个存储的实际分片。每个database下面有多个shard,具体数量取决于retention policy。这里就不在细讲,不太清楚的可以去回顾一下meta部分,那里面详细的介绍了。

​ shard在influxdb里面是真正存储数据的结构。一个shard可以理解为是一个TSM 引擎,管理多个tsm file。这个后面会展开说说,首先看一下shard的具体组成:

type Shard struct {
	path    string
	walPath string
	id      uint64

	database        string
	retentionPolicy string

	sfile   *SeriesFile
	options EngineOptions

	mu      sync.RWMutex
	_engine Engine
	index   Index
	enabled bool

	// expvar-based stats.
	stats       *ShardStatistics
	defaultTags models.StatisticTags

	baseLogger *zap.Logger
	logger     *zap.Logger

	EnableOnOpen bool

	// CompactionDisabled specifies the shard should not schedule compactions.
	// This option is intended for offline tooling.
	CompactionDisabled bool
}

​ 在这个结构里面可以看到 shard里面主要有以下几个结构:

  • path和wal path。path 指的是tsm file所在的位置,wal path指的是wal所在的位置。
  • database。shard所属的database
  • sfile。注意series 是database的概念的,也就是一个database下面所有的shard是共享这个数据结构的。这一点在存储的结构上也能看出来。
  • engine。tsm engine,负责存储的主要功能。
  • index 索引。

其他的就不再详细说了。这里就发现了今天的主角:Index .这里可以看出来,Index是被Shard依赖了的一个数据结构,主要帮助Shard 快速查询数据。

Index 整体设计

​ influxdb index模块有一个顶层的interface,定义了index都有哪些行为。在tsdb/index.go 模块下的Index结构中。这里我简化一下具体的结构:


type Index interface {
    Open() error
    Database() string
    MeasurementExists(name []byte) (bool, error)
    CreateSeriesIfNotExists(key, name []byte, tags models.Tags) error
    CreateSeriesListIfNotExists(keys, names [][]byte, tags []models.Tags) error
    DropSeries(seriesID uint64, key []byte, cascade bool) error
    DropSeriesList(seriesID []uint64, key [][]byte, cascade bool) error
    DropMeasurementIfSeriesNotExist(name []byte) (bool, error)

    HasTagKey(name, key []byte) (bool, error)
    HasTagValue(name, key, value []byte) (bool, error)
    MeasurementTagKeysByExpr(name []byte, expr influxql.Expr) (map[string]struct{}, error)
    TagKeyCardinality(name, key []byte) int

    // InfluxQL system iterators
    MeasurementIterator() (MeasurementIterator, error)
    TagKeyIterator(name []byte) (TagKeyIterator, error)
    TagValueIterator(name, key []byte) (TagValueIterator, error)
    MeasurementSeriesIDIterator(name []byte) (SeriesIDIterator, error)
    TagKeySeriesIDIterator(name, key []byte) (SeriesIDIterator, error)
    TagValueSeriesIDIterator(name, key, value []byte) (SeriesIDIterator, error)
    
    // Sets a shared fieldset from the engine.
    FieldSet() *MeasurementFieldSet
    SetFieldSet(fs *MeasurementFieldSet)

    // Size of the index on disk, if applicable.
    DiskSizeBytes() int64
    // To be removed w/ tsi1.
    SetFieldName(measurement []byte, name string)

    Type() string
    // Returns a unique reference ID to the index instance.
    // For inmem, returns a reference to the backing Index, not ShardIndex.
    UniqueReferenceID() uintptr

    Rebuild()
}

简化之后的结构大致如上,可以从顶层的定义中看到index的大致功能。

  • 提供一些基本数据信息的查询,注意,这里可不是meta info。这里指的是measurement,series,tagk,tagv等。
  • influxQL 的支持。
  • field set shared
  • Type()和Rebuild()

其中前三个都很好理解,第四个是Type和Rebuild。这个单独拿出来了,这里说明,index是支持多种类型的,Type()函数正是标注了具体的类型。

Index的注册和获取

​ 上面提到了顶层的Index模块提供了Type函数来标注当前实现的Index类型。这也是influxdb 可以拓展的地方,如果我们要自己开发一个Index,那么只需要实现这些接口。

​ 在顶层的index模块中,提供了Register函数,来注册对应的Index,具体是怎么做的呢?

// NewIndexFunc creates a new index.
type NewIndexFunc func(id uint64, database, path string, seriesIDSet *SeriesIDSet, sfile *SeriesFile, options EngineOptions) Index

// newIndexFuncs is a lookup of index constructors by name.
var newIndexFuncs = make(map[string]NewIndexFunc)

// RegisterIndex registers a storage index initializer by name.
func RegisterIndex(name string, fn NewIndexFunc) {
	if _, ok := newIndexFuncs[name]; ok {
		panic("index already registered: " + name)
	}
	newIndexFuncs[name] = fn
}

​ 这段代码展示了具体的注册逻辑。首先是NewIndexFunc 这是Index的构造函数,所有的Index在注册的时候只需要调用RegisterIndex注册一个NewIndexFunc的实现,这个实现会被保存的map里面。那么Shard是怎么使用的呢?

​ 在influxdb启动的时候,会load 所有的shard,并且新建里面的所有数据结构。在NewShard的时候,会新建Shard相关的索引。

func NewIndex(id uint64, database, path string, seriesIDSet *SeriesIDSet, sfile *SeriesFile, options EngineOptions) (Index, error) {
	// 这里默认使用的是inmem index
	format := options.IndexVersion

	// Use default format unless existing directory exists.
	_, err := os.Stat(path)
	if os.IsNotExist(err) {
		// nop, use default
	} else if err != nil {
		return nil, err
	} else if err == nil {
		format = TSI1IndexName
	}

	// Lookup index by format.
	fn := newIndexFuncs[format]
	if fn == nil {
		return nil, fmt.Errorf("invalid index format: %q", format)
	}
	return fn(id, database, path, seriesIDSet, sfile, options), nil
}

​ 在启动的时候,会指定EngineOptions,这里会指明索引的类型format。然后使用查找这个类型的NewIndexFunc函数,调用这个函数来把索引构建起来。

​ 到这里我们已经基本知道了这样几个事实:

  • 索引Index是有顶层的抽象接口的,所有具体实现需要实现这个接口。
  • 具体的实现,需要主动调用顶层接口提供的Register函数,注册一个符合当前实现的NewIndexFunc到map中。
  • Shard启动时,会从启动参数里面选择具体的索引类型,并且找到这个索引注册的NewIndexFunc来构建索引。

​ 那么,接下来就看一下。具体的实现。

Inmem Index

​ influxdb 提供了两种索引的实现,在tsdb/index 模块下。这两种索引分别是Inmem Index和tsi Index。上面说到了,索引的具体的实现需要自己注册,这两种索引在init函数里面实现了注册逻辑。

Inmem Index:

// IndexName is the name of this index.
const IndexName = tsdb.InmemIndexName

func init() {
	tsdb.NewInmemIndex = func(name string, sfile *tsdb.SeriesFile) (interface{}, error) { return NewIndex(name, sfile), nil }

	tsdb.RegisterIndex(IndexName, func(id uint64, database, path string, seriesIDSet *tsdb.SeriesIDSet, sfile *tsdb.SeriesFile, opt tsdb.EngineOptions) tsdb.Index {
		return NewShardIndex(id, seriesIDSet, opt)
	})
}

tsi index:

// IndexName is the name of the index.
const IndexName = tsdb.TSI1IndexName

// ErrCompactionInterrupted is returned if compactions are disabled or
// an index is closed while a compaction is occurring.
var ErrCompactionInterrupted = errors.New("tsi1: compaction interrupted")

func init() {
	if os.Getenv("INFLUXDB_EXP_TSI_PARTITIONS") != "" {
		i, err := strconv.Atoi(os.Getenv("INFLUXDB_EXP_TSI_PARTITIONS"))
		if err != nil {
			panic(err)
		}
		DefaultPartitionN = uint64(i)
	}

	tsdb.RegisterIndex(IndexName, func(_ uint64, db, path string, _ *tsdb.SeriesIDSet, sfile *tsdb.SeriesFile, opt tsdb.EngineOptions) tsdb.Index {
		idx := NewIndex(sfile, db,
			WithPath(path),
			WithMaximumLogFileSize(int64(opt.Config.MaxIndexLogFileSize)),
			WithSeriesIDCacheSize(opt.Config.SeriesIDSetCacheSize),
		)
		return idx
	})
}

这一小节首先看一下Inmem index.

Inmem index结构

​ 上面Index只是定义了所有Index 实现的行为,但是成员还是需要实现自己定义。Inmem Index的结构:

// un-exported functions assume the caller will use the appropriate locks.
type Index struct {
	mu sync.RWMutex

	database string
	sfile    *tsdb.SeriesFile
	fieldset *tsdb.MeasurementFieldSet

	// In-memory metadata index, built on load and updated when new series come in
	measurements map[string]*measurement // measurement name to object and index
	series       map[string]*series      // map series key to the Series object

	seriesSketch, seriesTSSketch             estimator.Sketch
	measurementsSketch, measurementsTSSketch estimator.Sketch

	// Mutex to control rebuilds of the index
	rebuildQueue sync.Mutex
}

​ 这个结构还是很简洁的,主要是series file和field,measurement信息。后面两个Sketch是用来评估数量的,可以不管。先看一下NewIndex方法

func NewIndex(database string, sfile *tsdb.SeriesFile) *Index {
	index := &Index{
		database:     database,
		sfile:        sfile,
		measurements: make(map[string]*measurement),
		series:       make(map[string]*series),
	}

	index.seriesSketch = hll.NewDefaultPlus()
	index.seriesTSSketch = hll.NewDefaultPlus()
	index.measurementsSketch = hll.NewDefaultPlus()
	index.measurementsTSSketch = hll.NewDefaultPlus()

	return index
}

NewIndex只传入两个参数,database和series file。其他的都是新建的。这里多提一句,注意看那几个Sketch,都是hll 的默认实现。这里是通过HyperLogLog 算法来实现的基数统计,不太了解的可以去看看这个算法。

CreateSeriesListIfNotExists

​ 为啥把这个函数单独拿出来说说呢,因为这个函数很重要,贯穿全局。不是很了解的,去要去看一下上一章:series部分。看一下inmem index这个函数式怎么实现的。这里的逻辑简化一下,有点长。

func (i *Index) CreateSeriesListIfNotExists(seriesIDSet *tsdb.SeriesIDSet, measurements map[string]int,
    keys, names [][]byte, tagsSlice []models.Tags, opt *tsdb.EngineOptions, ignoreLimits bool) error {
    seriesIDs, err := i.sfile.CreateSeriesListIfNotExists(names, tagsSlice)
    if err != nil {
        return err
    }
    i.mu.RLock()
    seriesList := make([]*series, len(seriesIDs))
    for j, key := range keys {
        seriesList[j] = i.series[string(key)]
    }
    i.mu.RUnlock()
    var hasNewSeries bool
    for _, ss := range seriesList {
        if ss == nil {
            hasNewSeries = true
            continue
        }
    }
    if !hasNewSeries {
        return nil
    }
    var newSeriesN int
    for j, key := range keys {
        if seriesList[j] != nil {
            continue
        }

        ss := i.series[string(key)]
        if ss == nil {
            newSeriesN++
            continue
        }
        seriesList[j] = ss
    }
    if newSeriesN == 0 {
        return nil
    }
    for j, key := range keys {
        if seriesList[j] != nil || i.series[string(key)] != nil {
            continue
        }
        skey := string(key)
        ss := newSeries(seriesIDs[j], mms[j], skey, tagsSlice[j].Clone())
        i.series[skey] = ss
        mms[j].AddSeries(ss)
    }
    return nil
}

​ 这里省略的比较多,主要是measurement和加锁的逻辑,因为index是shard 共享的,所以需要加锁。这段代码的核心逻辑也是很清晰的,首先把create new series的任务委托给了SeriesFile,然后拿到seriesId,check新增的series,如果有新增的,那么添加到当前的结构里面。 这里相当于对series 做了一个cache 这段逻辑大致就是如此,也通过这个例子,说明了inmem 的作用。

CreateMeasurementIndexIfNotExists

​ 上面看到inmem index的主要结构是series file和measurement。CreateMeasurementIndexIfNotExists是创建index对应的memsuremnet结构。measurement相关的信息不像series可以有其他模块提供。series 信息在启动的时候series file会加载存储的series 信息。measurement作为一个单独的模块,在重建索引时,会初始化。

Rebuild

rebuild是重建索引。在刚启动时,需要重建索引。
func (i *Index) Rebuild() {
	// Only allow one rebuild at a time.  This will cause all subsequent rebuilds
	// to queue.  The measurement rebuild is idempotent and will not be rebuilt if
	// it does not need to be.
	i.rebuildQueue.Lock()
	defer i.rebuildQueue.Unlock()

	i.ForEachMeasurementName(func(name []byte) error {
		// Measurement never returns an error
		m, _ := i.Measurement(name)
		if m == nil {
			return nil
		}

		i.mu.Lock()
		nm := m.Rebuild()

		i.measurements[string(name)] = nm
		i.mu.Unlock()
		return nil
	})
}

rebuild index时,会初始化measurement相关信息。

总结

​ 本篇文章介绍了influxdb index模块的基本结构和inmem 实现。 inmem 的index是启动默认的,比较简单,不支持持久化。为了解决这个问题,influxdb在tsi index里面,对index做了非常详细的设计,支持持久化,倒排索引等。下一节看一下这个。

<think>好的,用户问的是关于InfluxDB的cache-snapshot-memory-size配置的问题。我需要先理解这个参数的作用。根据以前的知识,InfluxDB在写入数据时,会先缓存到内存中,称为Cache,当达到一定条件后,再写入TSM文件。cache-snapshot-memory-size应该是指触发内存缓存快照并写入磁盘的内存阈值。 接下来,我需要确认这个参数的具体定义和用途。可能是指当缓存的内存使用量达到这个设定值时,InfluxDB会执行快照,将数据写入TSM文件,并释放内存。正确配置这个值对性能有影响,过小会导致频繁的磁盘写入,影响写入速度;过大可能导致内存占用过高,甚至OOM错误。 然后要考虑用户可能的场景。用户可能是在部署InfluxDB时遇到性能问题,或者想要优化配置。他们可能不太清楚如何根据自身情况调整这个参数,需要一些指导原则。 用户的需求可能包括如何确定合适的值,以及配置时需要注意的其他因素。例如,硬件资源(内存大小)、写入负载(数据量、写入频率)、数据保留策略等。此外,可能需要提醒用户其他相关的配置参数,比如cache-snapshot-write-cold-duration,或者监控内存使用情况的方法。 需要注意的是,不同版本的InfluxDB可能有不同的配置方式,尤其是在1.x和2.x版本之间是否有变化。需要确认该参数是否存在,或者是否有替代参数。比如,InfluxDB 1.x中有cache-snapshot-memory-size,而在2.x中可能有所不同,或者名称有变化。 另外,应该给出示例配置,比如在配置文件中的哪个部分设置这个参数,以及典型的数值范围,比如默认值是多少,如何根据内存调整。例如,默认可能是25MB,但实际生产环境可能需要设置为几百MB甚至更大,但不超过系统可用内存的一定比例。 还要提醒用户不要设置过高,以免影响系统稳定性,同时考虑其他使用内存的部分,比如查询操作、操作系统本身的需求。可能还需要建议用户进行监控和测试,逐步调整,观察效果。 总结回答的结构应该是:参数的作用、配置建议、影响因素、示例配置、注意事项。避免过于技术化的术语,保持解释清晰易懂,适合不同层次的理解。需要确保信息准确,如果有不确定的地方,应建议查阅官方文档或进行测试。</think>在 InfluxDB 中,`cache-snapshot-memory-size` 是一个与内存缓存管理相关的重要配置参数,主要影响数据写入和持久化的行为。以下是关于该参数的详细介绍和配置建议: --- ### **参数作用** - **功能定义**: `cache-snapshot-memory-size` 定义了内存中缓存(Cache)触发快照(Snapshot)并写入磁盘的阈值。当缓存占用的内存达到该值时,InfluxDB 会将缓存中的数据持久化为 TSM 文件(时间序列存储引擎文件),并释放内存空间。 - **适用场景**: 写入高频时间序列数据时,缓存机制能提升写入吞吐量,但需定期持久化以避免内存耗尽。此参数控制缓存释放的触发条件。 --- ### **配置建议** 1. **默认值**: 在 InfluxDB 1.x 版本中,默认值为 `25MB`(不同版本可能略有差异,需以官方文档为准)。 2. **调整依据**: - **内存资源**: 根据服务器可用内存调整。若内存充足,可适当增大该值以减少频繁快照的开销;若内存紧张,需降低以避免 OOM(内存溢出)。 - **写入负载**: 高写入场景(如每秒数千点)建议增大阈值,降低持久化频率;低负载场景可保持默认或小幅调整。 - **数据保留策略**: 若数据保留时间短(如临时缓存),可降低阈值以加速数据淘汰。 3. **典型配置示例**: 在 InfluxDB 配置文件(`influxdb.conf`)中设置: ```toml [data] cache-snapshot-memory-size = "512MB" # 调整为 512MB ``` --- ### **影响因素与注意事项** 1. **性能权衡**: - **过高**:内存占用增加,可能影响查询性能或引发 OOM。 - **过低**:频繁快照导致磁盘 I/O 压力增大,写入延迟上升。 2. **关联参数**: - `cache-snapshot-write-cold-duration`:定义未写入新数据时触发快照的时间间隔(默认 10m)。需与 `cache-snapshot-memory-size` 配合调整。 - `max-values-per-tag`:单标签值数量限制,间接影响内存占用。 3. **监控与调优**: - 通过 InfluxDB 内置监控(如 `_internal` 数据库)观察 `mem_bytes` 等指标,判断缓存使用情况。 - 使用压力测试工具(如 `influx_stress`)模拟真实负载,逐步调整参数。 --- ### **示例场景** - **高内存服务器(32GB+)**: 若专用于 InfluxDB,可设置为 `1GB~2GB`,充分利用内存缓冲写入。 - **低资源环境(4GB 内存)**: 建议保持默认或小幅提升至 `100MB~200MB`,避免内存争用。 --- ### **总结** 合理配置 `cache-snapshot-memory-size` 需结合硬件资源、数据写入模式和业务需求。建议通过监控工具观察内存使用趋势,逐步调整至稳定值,并在生产环境中进行充分验证。如需进一步优化,可参考 InfluxDB 官方文档或社区最佳实践。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值