🚀 系统设计实战 183:时序数据库
摘要:本文深入剖析系统的核心架构、关键算法和工程实践,提供完整的设计方案和面试要点。
你是否想过,设计时序数据库进阶版背后的技术挑战有多复杂?
1. 系统概述
1.1 业务背景
时序数据库专门用于存储和查询时间序列数据,如监控指标、IoT传感器数据、金融交易数据等。系统需要支持高写入吞吐量、时间范围查询、数据聚合和压缩存储。
1.2 核心功能
- 时间索引:高效的时间戳索引和范围查询
- 数据压缩:时序数据的专用压缩算法
- 聚合查询:时间窗口聚合、降采样、插值
- 数据保留:自动数据过期和分层存储
- 高写入吞吐:批量写入、内存缓冲、异步刷盘
1.3 技术挑战
- 写入性能:处理大量时序数据的高频写入
- 存储效率:时序数据的高效压缩和存储
- 查询优化:时间范围查询和聚合计算优化
- 数据生命周期:自动数据分层和过期管理
- 扩展性:水平扩展和分片策略
2. 架构设计
2.1 整体架构
┌─────────────────────────────────────────────────────────────┐
│ 时序数据库架构 │
├─────────────────────────────────────────────────────────────┤
│ Ingestion Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 数据采集 │ │ 批量写入 │ │ 流式写入 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Query Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 查询引擎 │ │ 聚合计算 │ │ 缓存层 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Storage Engine │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 内存存储 │ │ 时序索引 │ │ 压缩引擎 │ │
│ │ WAL日志 │ │ 数据分片 │ │ 生命周期 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Physical Storage │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 热数据存储 │ │ 温数据存储 │ │ 冷数据存储 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
3. 核心组件设计
3.1 时序存储引擎
// 时间复杂度:O(N),空间复杂度:O(1)
type TimeSeriesEngine struct {
memTable *MemTable
immutableTables []*MemTable
sstables []*TSMFile
wal *WriteAheadLog
compactor *TSMCompactor
index *TimeSeriesIndex
cache *BlockCache
}
type DataPoint struct {
Series SeriesKey
Timestamp int64
Value interface{}
Tags map[string]string
}
type SeriesKey struct {
Measurement string
Tags map[string]string
}
func (tse *TimeSeriesEngine) WritePoints(points []DataPoint) error {
// 1. 写入WAL日志
walEntry := &WALEntry{
Points: points,
Timestamp: time.Now().UnixNano(),
}
if err := tse.wal.Write(walEntry); err != nil {
return err
}
// 2. 写入内存表
tse.memTable.mutex.Lock()
defer tse.memTable.mutex.Unlock()
for _, point := range points {
seriesID := tse.index.GetOrCreateSeriesID(point.Series)
tse.memTable.Write(seriesID, point.Timestamp, point.Value)
}
// 3. 检查是否需要刷新
if tse.memTable.Size() >= tse.memTable.maxSize {
return tse.flushMemTable()
}
return nil
}
type MemTable struct {
data map[SeriesID]*SeriesData
size int64
maxSize int64
mutex sync.RWMutex
}
type SeriesData struct {
timestamps []int64
values []interface{}
mutex sync.RWMutex
}
func (mt *MemTable) Write(seriesID SeriesID, timestamp int64, value interface{}) {
series, exists := mt.data[seriesID]
if !exists {
series = &SeriesData{
timestamps: make([]int64, 0),
values: make([]interface{}, 0),
}
mt.data[seriesID] = series
}
series.mutex.Lock()
defer series.mutex.Unlock()
// 插入排序保持时间顺序
insertPos := sort.Search(len(series.timestamps), func(i int) bool {
return series.timestamps[i] >= timestamp
})
series.timestamps = append(series.timestamps[:insertPos],
append([]int64{timestamp}, series.timestamps[insertPos:]...)...)
series.values = append(series.values[:insertPos],
append([]interface{}{value}, series.values[insertPos:]...)...)
mt.size += int64(unsafe.Sizeof(timestamp) + unsafe.Sizeof(value))
}
func (mt *MemTable) Query(seriesID SeriesID, startTime, endTime int64) (*SeriesData, error) {
series, exists := mt.data[seriesID]
if !exists {
return nil, ErrSeriesNotFound
}
series.mutex.RLock()
defer series.mutex.RUnlock()
// 二分查找时间范围
startIdx := sort.Search(len(series.timestamps), func(i int) bool {
return series.timestamps[i] >= startTime
})
endIdx := sort.Search(len(series.timestamps), func(i int) bool {
return series.timestamps[i] > endTime
})
if startIdx >= len(series.timestamps) || endIdx <= startIdx {
return &SeriesData{}, nil
}
return &SeriesData{
timestamps: series.timestamps[startIdx:endIdx],
values: series.values[startIdx:endIdx],
}, nil
}
3.2 TSM文件格式
type TSMFile struct {
filePath string
header *TSMHeader
index *TSMIndex
blocks map[SeriesID]*TSMBlock
bloomFilter *BloomFilter
}
type TSMHeader struct {
Version uint32
BlockCount uint32
IndexOffset uint64
MinTime int64
MaxTime int64
}
type TSMBlock struct {
SeriesID SeriesID
MinTime int64
MaxTime int64
Count uint32
Timestamps []byte // 压缩后的时间戳
Values []byte // 压缩后的值
Encoding EncodingType
}
type TSMWriter struct {
file *os.File
buffer *bytes.Buffer
compressor *TimeSeriesCompressor
index *TSMIndexBuilder
}
func (tw *TSMWriter) WriteBlock(seriesID SeriesID, timestamps []int64, values []interface{}) error {
// 1. 压缩时间戳
compressedTimestamps, err := tw.compressor.CompressTimestamps(timestamps)
if err != nil {
return err
}
// 2. 压缩值
compressedValues, encoding, err := tw.compressor.CompressValues(values)
if err != nil {
return err
}
// 3. 创建块
block := &TSMBlock{
SeriesID: seriesID,
MinTime: timestamps[0],
MaxTime: timestamps[len(timestamps)-1],
Count: uint32(len(timestamps)),
Timestamps: compressedTimestamps,
Values: compressedValues,
Encoding: encoding,
}
// 4. 写入文件
offset, err := tw.writeBlock(block)
if err != nil {
return err
}
// 5. 更新索引
tw.index.AddEntry(seriesID, block.MinTime, block.MaxTime, offset, block.Size())
return nil
}
func (tw *TSMWriter) writeBlock(block *TSMBlock) (int64, error) {
offset, _ := tw.file.Seek(0, io.SeekCurrent)
// 写入块头
binary.Write(tw.file, binary.BigEndian, block.SeriesID)
binary.Write(tw.file, binary.BigEndian, block.MinTime)
binary.Write(tw.file, binary.BigEndian, block.MaxTime)
binary.Write(tw.file, binary.BigEndian, block.Count)
binary.Write(tw.file, binary.BigEndian, uint32(len(block.Timestamps)))
binary.Write(tw.file, binary.BigEndian, uint32(len(block.Values)))
binary.Write(tw.file, binary.BigEndian, block.Encoding)
// 写入压缩数据
tw.file.Write(block.Timestamps)
tw.file.Write(block.Values)
return offset, nil
}
3.3 时序数据压缩
type TimeSeriesCompressor struct {
timestampCompressor *DeltaOfDeltaCompressor
valueCompressors map[reflect.Type]ValueCompressor
}
type DeltaOfDeltaCompressor struct {
buffer *bytes.Buffer
writer *bitstream.Writer
}
func (ddc *DeltaOfDeltaCompressor) Compress(timestamps []int64) ([]byte, error) {
if len(timestamps) == 0 {
return nil, nil
}
ddc.buffer.Reset()
ddc.writer = bitstream.NewWriter(ddc.buffer)
// 写入第一个时间戳(64位)
ddc.writer.WriteBits(uint64(timestamps[0]), 64)
if len(timestamps) == 1 {
return ddc.buffer.Bytes(), nil
}
// 计算第一个delta
delta := timestamps[1] - timestamps[0]
ddc.writer.WriteBits(uint64(delta), 64)
if len(timestamps) == 2 {
return ddc.buffer.Bytes(), nil
}
// Delta-of-delta压缩
prevDelta := delta
for i := 2; i < len(timestamps); i++ {
currentDelta := timestamps[i] - timestamps[i-1]
deltaOfDelta := currentDelta - prevDelta
// 根据delta-of-delta的大小选择编码方式
if deltaOfDelta == 0 {
ddc.writer.WriteBits(0, 1) // '0'
} else if deltaOfDelta >= -63 && deltaOfDelta <= 64 {
ddc.writer.WriteBits(2, 2) // '10'
ddc.writer.WriteBits(uint64(deltaOfDelta), 7)
} else if deltaOfDelta >= -255 && deltaOfDelta <= 256 {
ddc.writer.WriteBits(6, 3) // '110'
ddc.writer.WriteBits(uint64(deltaOfDelta), 9)
} else if deltaOfDelta >= -2047 && deltaOfDelta <= 2048 {
ddc.writer.WriteBits(14, 4) // '1110'
ddc.writer.WriteBits(uint64(deltaOfDelta), 12)
} else {
ddc.writer.WriteBits(15, 4) // '1111'
ddc.writer.WriteBits(uint64(deltaOfDelta), 32)
}
prevDelta = currentDelta
}
return ddc.buffer.Bytes(), nil
}
type FloatCompressor struct {
buffer *bytes.Buffer
writer *bitstream.Writer
}
func (fc *FloatCompressor) Compress(values []float64) ([]byte, error) {
if len(values) == 0 {
return nil, nil
}
fc.buffer.Reset()
fc.writer = bitstream.NewWriter(fc.buffer)
// 写入第一个值
fc.writer.WriteBits(math.Float64bits(values[0]), 64)
if len(values) == 1 {
return fc.buffer.Bytes(), nil
}
// XOR压缩
prevBits := math.Float64bits(values[0])
prevLeadingZeros := 0
prevTrailingZeros := 0
for i := 1; i < len(values); i++ {
currentBits := math.Float64bits(values[i])
xor := prevBits ^ currentBits
if xor == 0 {
fc.writer.WriteBits(0, 1) // '0' - 值相同
} else {
fc.writer.WriteBits(1, 1) // '1' - 值不同
leadingZeros := bits.LeadingZeros64(xor)
trailingZeros := bits.TrailingZeros64(xor)
// 检查是否可以使用之前的窗口
if leadingZeros >= prevLeadingZeros &&
trailingZeros >= prevTrailingZeros {
fc.writer.WriteBits(0, 1) // '0' - 使用之前的窗口
meaningfulBits := 64 - prevLeadingZeros - prevTrailingZeros
fc.writer.WriteBits(xor>>prevTrailingZeros, meaningfulBits)
} else {
fc.writer.WriteBits(1, 1) // '1' - 新窗口
fc.writer.WriteBits(uint64(leadingZeros), 5)
meaningfulBits := 64 - leadingZeros - trailingZeros
fc.writer.WriteBits(uint64(meaningfulBits), 6)
fc.writer.WriteBits(xor>>trailingZeros, meaningfulBits)
prevLeadingZeros = leadingZeros
prevTrailingZeros = trailingZeros
}
}
prevBits = currentBits
}
return fc.buffer.Bytes(), nil
}
3.4 查询引擎
type QueryEngine struct {
storage *TimeSeriesEngine
aggregator *Aggregator
cache *QueryCache
planner *QueryPlanner
}
type TimeSeriesQuery struct {
Series []SeriesSelector
StartTime int64
EndTime int64
Aggregation *AggregationSpec
GroupBy []string
Limit int
}
type SeriesSelector struct {
Measurement string
Tags map[string]string
Fields []string
}
func (qe *QueryEngine) ExecuteQuery(query *TimeSeriesQuery) (*QueryResult, error) {
// 1. 查询计划优化
plan, err := qe.planner.CreatePlan(query)
if err != nil {
return nil, err
}
// 2. 检查缓存
cacheKey := qe.cache.GenerateKey(query)
if result := qe.cache.Get(cacheKey); result != nil {
return result, nil
}
// 3. 执行查询
result, err := qe.executeQueryPlan(plan)
if err != nil {
return nil, err
}
// 4. 缓存结果
qe.cache.Put(cacheKey, result)
return result, nil
}
func (qe *QueryEngine) executeQueryPlan(plan *QueryPlan) (*QueryResult, error) {
result := &QueryResult{
Series: make([]*SeriesResult, 0),
}
for _, seriesSelector := range plan.SeriesSelectors {
// 查找匹配的时间序列
seriesIDs := qe.storage.index.FindSeries(seriesSelector)
for _, seriesID := range seriesIDs {
seriesData, err := qe.querySeriesData(seriesID, plan.StartTime, plan.EndTime)
if err != nil {
continue
}
// 应用聚合
if plan.Aggregation != nil {
seriesData = qe.aggregator.Aggregate(seriesData, plan.Aggregation)
}
seriesResult := &SeriesResult{
SeriesID: seriesID,
Timestamps: seriesData.timestamps,
Values: seriesData.values,
}
result.Series = append(result.Series, seriesResult)
}
}
return result, nil
}
func (qe *QueryEngine) querySeriesData(seriesID SeriesID, startTime, endTime int64) (*SeriesData, error) {
result := &SeriesData{
timestamps: make([]int64, 0),
values: make([]interface{}, 0),
}
// 1. 查询内存表
memData, err := qe.storage.memTable.Query(seriesID, startTime, endTime)
if err == nil {
result.timestamps = append(result.timestamps, memData.timestamps...)
result.values = append(result.values, memData.values...)
}
// 2. 查询不可变内存表
for _, immutable := range qe.storage.immutableTables {
immData, err := immutable.Query(seriesID, startTime, endTime)
if err == nil {
result.timestamps = append(result.timestamps, immData.timestamps...)
result.values = append(result.values, immData.values...)
}
}
// 3. 查询TSM文件
for _, tsmFile := range qe.storage.sstables {
if tsmFile.ContainsSeries(seriesID) &&
tsmFile.TimeRangeOverlaps(startTime, endTime) {
fileData, err := tsmFile.Query(seriesID, startTime, endTime)
if err == nil {
result.timestamps = append(result.timestamps, fileData.timestamps...)
result.values = append(result.values, fileData.values...)
}
}
}
// 4. 合并和排序结果
qe.mergeAndSort(result)
return result, nil
}
3.5 聚合计算
type Aggregator struct {
functions map[string]AggregateFunction
}
type AggregateFunction interface {
Aggregate(timestamps []int64, values []interface{}, window time.Duration) *AggregateResult
}
type AggregateResult struct {
Timestamps []int64
Values []interface{}
}
type MeanAggregator struct{}
func (ma *MeanAggregator) Aggregate(timestamps []int64, values []interface{}, window time.Duration) *AggregateResult {
if len(timestamps) == 0 {
return &AggregateResult{}
}
result := &AggregateResult{
Timestamps: make([]int64, 0),
Values: make([]interface{}, 0),
}
windowNanos := window.Nanoseconds()
currentWindow := timestamps[0] / windowNanos * windowNanos
var sum float64
var count int
for i, timestamp := range timestamps {
windowStart := timestamp / windowNanos * windowNanos
if windowStart != currentWindow {
// 输出当前窗口的结果
if count > 0 {
result.Timestamps = append(result.Timestamps, currentWindow)
result.Values = append(result.Values, sum/float64(count))
}
// 开始新窗口
currentWindow = windowStart
sum = 0
count = 0
}
// 累加当前值
if val, ok := values[i].(float64); ok {
sum += val
count++
}
}
// 输出最后一个窗口的结果
if count > 0 {
result.Timestamps = append(result.Timestamps, currentWindow)
result.Values = append(result.Values, sum/float64(count))
}
return result
}
type MaxAggregator struct{}
func (ma *MaxAggregator) Aggregate(timestamps []int64, values []interface{}, window time.Duration) *AggregateResult {
result := &AggregateResult{
Timestamps: make([]int64, 0),
Values: make([]interface{}, 0),
}
windowNanos := window.Nanoseconds()
currentWindow := timestamps[0] / windowNanos * windowNanos
var maxValue float64
hasValue := false
for i, timestamp := range timestamps {
windowStart := timestamp / windowNanos * windowNanos
if windowStart != currentWindow {
if hasValue {
result.Timestamps = append(result.Timestamps, currentWindow)
result.Values = append(result.Values, maxValue)
}
currentWindow = windowStart
hasValue = false
}
if val, ok := values[i].(float64); ok {
if !hasValue || val > maxValue {
maxValue = val
hasValue = true
}
}
}
if hasValue {
result.Timestamps = append(result.Timestamps, currentWindow)
result.Values = append(result.Values, maxValue)
}
return result
}
4. 数据生命周期管理
4.1 数据分层存储
type DataLifecycleManager struct {
tiers []*StorageTier
policies []*RetentionPolicy
compactor *TierCompactor
scheduler *LifecycleScheduler
}
type StorageTier struct {
Name string
StorageType StorageType
Compression CompressionLevel
MaxAge time.Duration
CostPerGB float64
}
type RetentionPolicy struct {
Measurement string
Tags map[string]string
Duration time.Duration
Precision time.Duration
ShardDuration time.Duration
}
func (dlm *DataLifecycleManager) ManageDataLifecycle() {
ticker := time.NewTicker(time.Hour)
go func() {
for range ticker.C {
dlm.processDataLifecycle()
}
}()
}
func (dlm *DataLifecycleManager) processDataLifecycle() {
// 1. 数据分层迁移
dlm.migrateDataBetweenTiers()
// 2. 数据过期清理
dlm.expireOldData()
// 3. 数据压缩优化
dlm.compactor.CompactOldData()
}
func (dlm *DataLifecycleManager) migrateDataBetweenTiers() {
for i, tier := range dlm.tiers {
if i == len(dlm.tiers)-1 {
continue // 最后一层不需要迁移
}
nextTier := dlm.tiers[i+1]
cutoffTime := time.Now().Add(-tier.MaxAge)
// 查找需要迁移的数据
dataToMigrate := dlm.findDataOlderThan(tier, cutoffTime)
for _, data := range dataToMigrate {
// 迁移到下一层
if err := dlm.migrateData(data, tier, nextTier); err != nil {
log.Printf("Failed to migrate data: %v", err)
}
}
}
}
func (dlm *DataLifecycleManager) expireOldData() {
for _, policy := range dlm.policies {
cutoffTime := time.Now().Add(-policy.Duration)
// 查找过期数据
expiredData := dlm.findExpiredData(policy, cutoffTime)
for _, data := range expiredData {
if err := dlm.deleteData(data); err != nil {
log.Printf("Failed to delete expired data: %v", err)
}
}
}
}
4.2 自动压缩
type TSMCompactor struct {
levels []CompactionLevel
compactionQueue chan *CompactionTask
workers []*CompactionWorker
}
type CompactionLevel struct {
Level int
MaxFileSize int64
MaxFiles int
MinAge time.Duration
}
type CompactionTask struct {
Level int
Files []*TSMFile
OutputDir string
}
func (tc *TSMCompactor) StartCompaction() {
// 启动压缩工作线程
for i := 0; i < tc.workerCount; i++ {
worker := &CompactionWorker{
id: i,
tasks: tc.compactionQueue,
}
tc.workers = append(tc.workers, worker)
go worker.Run()
}
// 启动压缩调度器
go tc.scheduleCompaction()
}
func (tc *TSMCompactor) scheduleCompaction() {
ticker := time.NewTicker(time.Minute * 5)
for range ticker.C {
for _, level := range tc.levels {
if tc.needsCompaction(level) {
task := tc.createCompactionTask(level)
select {
case tc.compactionQueue <- task:
default:
// 队列满,跳过本次压缩
}
}
}
}
}
func (tc *TSMCompactor) needsCompaction(level CompactionLevel) bool {
files := tc.getFilesAtLevel(level.Level)
// 检查文件数量
if len(files) >= level.MaxFiles {
return true
}
// 检查文件年龄
for _, file := range files {
if time.Since(file.CreatedAt) >= level.MinAge {
return true
}
}
return false
}
type CompactionWorker struct {
id int
tasks chan *CompactionTask
}
func (cw *CompactionWorker) Run() {
for task := range cw.tasks {
if err := cw.executeCompaction(task); err != nil {
log.Printf("Compaction worker %d failed: %v", cw.id, err)
}
}
}
func (cw *CompactionWorker) executeCompaction(task *CompactionTask) error {
// 1. 创建输出文件
outputFile := filepath.Join(task.OutputDir,
fmt.Sprintf("compacted_%d_%d.tsm", task.Level, time.Now().Unix()))
writer, err := NewTSMWriter(outputFile)
if err != nil {
return err
}
defer writer.Close()
// 2. 合并输入文件
merger := NewTSMFileMerger(task.Files)
for merger.HasNext() {
seriesID, timestamps, values := merger.Next()
// 去重和排序
timestamps, values = cw.deduplicateAndSort(timestamps, values)
// 写入输出文件
if err := writer.WriteBlock(seriesID, timestamps, values); err != nil {
return err
}
}
// 3. 删除输入文件
for _, file := range task.Files {
os.Remove(file.FilePath)
}
return nil
}
时序数据库通过专门的存储格式、压缩算法和查询优化,为时间序列数据提供了高性能、高压缩比的存储和查询解决方案。
🎯 场景引入
你打开App,
你打开手机准备使用设计时序数据库进阶版服务。看似简单的操作背后,系统面临三大核心挑战:
- 挑战一:高并发——如何在百万级 QPS 下保持低延迟?
- 挑战二:高可用——如何在节点故障时保证服务不中断?
- 挑战三:数据一致性——如何在分布式环境下保证数据正确?
📈 容量估算
假设 DAU 1000 万,人均日请求 50 次
| 指标 | 数值 |
|---|---|
| 数据总量 | 10 TB+ |
| 日写入量 | ~100 GB |
| 写入 TPS | ~5 万/秒 |
| 读取 QPS | ~20 万/秒 |
| P99 读延迟 | < 10ms |
| 节点数 | 10-50 |
| 副本因子 | 3 |
❓ 高频面试问题
Q1:时序数据库的核心设计原则是什么?
参考正文中的架构设计部分,核心原则包括:高可用(故障自动恢复)、高性能(低延迟高吞吐)、可扩展(水平扩展能力)、一致性(数据正确性保证)。面试时需结合具体场景展开。
Q2:时序数据库在大规模场景下的主要挑战是什么?
- 性能瓶颈:随着数据量和请求量增长,单节点无法承载;2) 一致性:分布式环境下的数据一致性保证;3) 故障恢复:节点故障时的自动切换和数据恢复;4) 运维复杂度:集群管理、监控、升级。
Q3:如何保证时序数据库的高可用?
- 多副本冗余(至少 3 副本);2) 自动故障检测和切换(心跳 + 选主);3) 数据持久化和备份;4) 限流降级(防止雪崩);5) 多机房/多活部署。
Q4:时序数据库的性能优化有哪些关键手段?
- 缓存(减少重复计算和 IO);2) 异步处理(非关键路径异步化);3) 批量操作(减少网络往返);4) 数据分片(并行处理);5) 连接池复用。
Q5:时序数据库与同类方案相比有什么优劣势?
参考方案对比表格。选型时需考虑:团队技术栈、数据规模、延迟要求、一致性需求、运维成本。没有银弹,需根据业务场景权衡取舍。
| 方案一 | 简单实现 | 低 | 适合小规模 | | 方案二 | 中等复杂度 | 中 | 适合中等规模 | | 方案三 | 高复杂度 ⭐推荐 | 高 | 适合大规模生产环境 |
🚀 架构演进路径
阶段一:单机版 MVP(用户量 < 10 万)
- 单体应用 + 单机数据库
- 功能验证优先,快速迭代
- 适用场景:产品早期验证
阶段二:基础版分布式(用户量 10 万 - 100 万)
- 应用层水平扩展(无状态服务 + 负载均衡)
- 数据库主从分离(读写分离)
- 引入 Redis 缓存热点数据
- 适用场景:业务增长期
阶段三:生产级高可用(用户量 > 100 万)
- 微服务拆分,独立部署和扩缩容
- 数据库分库分表(按业务维度分片)
- 引入消息队列解耦异步流程
- 多机房部署,异地容灾
- 全链路监控 + 自动化运维
✅ 架构设计检查清单
| 检查项 | 状态 | 说明 |
|---|---|---|
| 高可用 | ✅ | 多副本部署,自动故障转移,99.9% SLA |
| 可扩展 | ✅ | 无状态服务水平扩展,数据层分片 |
| 数据一致性 | ✅ | 核心路径强一致,非核心最终一致 |
| 安全防护 | ✅ | 认证授权 + 加密 + 审计日志 |
| 监控告警 | ✅ | Metrics + Logging + Tracing 三支柱 |
| 容灾备份 | ✅ | 多机房部署,定期备份,RPO < 1 分钟 |
| 性能优化 | ✅ | 多级缓存 + 异步处理 + 连接池 |
| 灰度发布 | ✅ | 支持按用户/地域灰度,快速回滚 |
⚖️ 关键 Trade-off 分析
🔴 Trade-off 1:一致性 vs 可用性
- 强一致(CP):适用于金融交易等不能出错的场景
- 高可用(AP):适用于社交动态等允许短暂不一致的场景
- 本系统选择:核心路径强一致,非核心路径最终一致
🔴 Trade-off 2:同步 vs 异步
- 同步处理:延迟低但吞吐受限,适用于核心交互路径
- 异步处理:吞吐高但增加延迟,适用于后台计算
- 本系统选择:核心路径同步,非核心路径异步